Skip to content

Commit 60f6371

Browse files
committed
feat: context quality
1 parent d376929 commit 60f6371

3 files changed

Lines changed: 365 additions & 26 deletions

File tree

pyproject.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -111,6 +111,7 @@ module = [
111111
"mcp.*",
112112
"yaml",
113113
"aiosqlite",
114+
"jinja2",
114115
]
115116
ignore_missing_imports = true
116117

src/devscontext/agents/preprocessor.py

Lines changed: 181 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -466,14 +466,30 @@ async def _multi_pass_synthesis(
466466
# === Pass 3: Gap Detection ===
467467
logger.debug("Pass 3: Detecting gaps")
468468

469+
# Rule-based gap detection (reliable, consistent)
470+
rule_gaps = self._detect_gaps(jira_ctx, meeting_ctx, docs_ctx)
471+
472+
# LLM-based gap detection (additional insights)
469473
gap_prompt = GAP_DETECTION_PROMPT.format(context=synthesized)
470474
gap_response = await provider.generate(gap_prompt, max_tokens=500)
471-
gaps = self._parse_gaps(gap_response)
475+
llm_gaps = self._parse_gaps(gap_response)
476+
477+
# Merge gaps (rule-based first, then unique LLM gaps)
478+
all_gaps = list(rule_gaps)
479+
existing_lower = {g.lower() for g in all_gaps}
480+
for gap in llm_gaps:
481+
if gap.lower() not in existing_lower:
482+
all_gaps.append(gap)
483+
existing_lower.add(gap.lower())
472484

473485
# === Calculate Quality Score ===
474486
quality_score = self._calculate_quality_score(jira_ctx, meeting_ctx, docs_ctx)
475487

476-
return synthesized, quality_score, gaps
488+
# === Append Gaps to Synthesized Context ===
489+
if all_gaps:
490+
synthesized = self._append_gaps_to_context(synthesized, all_gaps, quality_score)
491+
492+
return synthesized, quality_score, all_gaps
477493

478494
def _format_jira_for_extraction(self, ctx: JiraContext) -> str:
479495
"""Format Jira context for extraction prompt."""
@@ -579,41 +595,180 @@ def _calculate_quality_score(
579595
) -> float:
580596
"""Calculate context quality score based on completeness.
581597
582-
Score components (0-1 each, averaged):
583-
- Has description: 0.2
584-
- Has acceptance criteria: 0.2
585-
- Has meeting context: 0.2
586-
- Has architecture docs: 0.2
587-
- Has coding standards: 0.2
598+
Scoring rubric (each dimension 0-1, averaged):
599+
1. Has clear requirements/acceptance criteria
600+
2. Has implementation decisions (meeting context)
601+
3. Has architecture context (relevant docs)
602+
4. Has coding standards (standards docs)
603+
5. Has related work context (linked issues)
588604
589605
Returns:
590606
Quality score between 0 and 1.
591607
"""
592-
score = 0.0
608+
dimensions: list[float] = []
593609

594-
# Has description (0.2)
595-
if jira_ctx.ticket.description:
596-
score += 0.2
597-
598-
# Has acceptance criteria (0.2)
599-
if jira_ctx.ticket.acceptance_criteria:
600-
score += 0.2
610+
# 1. Has clear requirements or acceptance criteria
611+
has_requirements = bool(
612+
jira_ctx.ticket.acceptance_criteria
613+
or (jira_ctx.ticket.description and len(jira_ctx.ticket.description) > 100)
614+
)
615+
dimensions.append(1.0 if has_requirements else 0.0)
601616

602-
# Has meeting context (0.2)
603-
if meeting_ctx.meetings:
604-
score += 0.2
617+
# 2. Has implementation decisions (meeting context)
618+
has_meetings = bool(meeting_ctx.meetings)
619+
dimensions.append(1.0 if has_meetings else 0.0)
605620

606-
# Has architecture docs (0.2)
621+
# 3. Has architecture context (relevant docs matched)
607622
has_arch = any(s.doc_type == "architecture" for s in docs_ctx.sections)
608-
if has_arch:
609-
score += 0.2
623+
dimensions.append(1.0 if has_arch else 0.0)
624+
625+
# 4. Has coding standards (standards docs included)
626+
has_standards = any(s.doc_type == "standards" for s in docs_ctx.sections)
627+
dimensions.append(1.0 if has_standards else 0.0)
628+
629+
# 5. Has related work context (linked issues exist)
630+
has_related = bool(jira_ctx.linked_issues)
631+
dimensions.append(1.0 if has_related else 0.0)
632+
633+
# Return average of all dimensions
634+
return sum(dimensions) / len(dimensions) if dimensions else 0.0
635+
636+
def _detect_gaps(
637+
self,
638+
jira_ctx: JiraContext,
639+
meeting_ctx: MeetingContext,
640+
docs_ctx: DocsContext,
641+
) -> list[str]:
642+
"""Detect specific gaps in context and return actionable messages.
643+
644+
Checks for:
645+
- Missing acceptance criteria
646+
- Missing meeting discussions
647+
- Missing architecture documentation
648+
- Missing coding standards
649+
- Missing linked/related tickets
650+
651+
Returns:
652+
List of actionable gap messages.
653+
"""
654+
gaps: list[str] = []
655+
656+
# Check for acceptance criteria
657+
if not jira_ctx.ticket.acceptance_criteria:
658+
gaps.append(
659+
"No acceptance criteria found in Jira ticket — "
660+
"consider adding clear done criteria before implementation"
661+
)
610662

611-
# Has coding standards (0.2)
663+
# Check for meeting discussions
664+
if not meeting_ctx.meetings:
665+
gaps.append(
666+
"No meeting discussions found for this task — "
667+
"consider a planning discussion with the team"
668+
)
669+
670+
# Check for architecture documentation
671+
has_arch = any(s.doc_type == "architecture" for s in docs_ctx.sections)
672+
if not has_arch:
673+
# Try to identify the service area from components or labels
674+
service_area = None
675+
if jira_ctx.ticket.components:
676+
service_area = jira_ctx.ticket.components[0]
677+
elif jira_ctx.ticket.labels:
678+
service_area = jira_ctx.ticket.labels[0]
679+
680+
if service_area:
681+
gaps.append(
682+
f"No architecture documentation found for service area '{service_area}' — "
683+
"consider documenting the architectural approach"
684+
)
685+
else:
686+
gaps.append(
687+
"No architecture documentation found — "
688+
"consider documenting the architectural approach for this feature"
689+
)
690+
691+
# Check for coding standards
612692
has_standards = any(s.doc_type == "standards" for s in docs_ctx.sections)
613-
if has_standards:
614-
score += 0.2
693+
if not has_standards:
694+
gaps.append(
695+
"No coding standards documentation found — "
696+
"check if CLAUDE.md or standards/ docs exist in the repository"
697+
)
698+
699+
# Check for linked/related tickets
700+
if not jira_ctx.linked_issues:
701+
gaps.append(
702+
"No linked or related tickets — "
703+
"this task may lack broader context; consider linking related work"
704+
)
705+
706+
# Additional checks from comments and description
707+
ticket = jira_ctx.ticket
708+
description = (ticket.description or "").lower()
709+
710+
# Check for unclear dependencies
711+
no_ac = not ticket.acceptance_criteria
712+
no_depends_mention = "depends" not in description and "blocked" not in description
713+
if no_ac and no_depends_mention and not jira_ctx.linked_issues:
714+
gaps.append(
715+
"No dependencies identified — "
716+
"verify this task has no blocking dependencies before starting"
717+
)
718+
719+
return gaps
720+
721+
def _append_gaps_to_context(
722+
self,
723+
synthesized: str,
724+
gaps: list[str],
725+
quality_score: float,
726+
) -> str:
727+
"""Append identified gaps to the synthesized context.
728+
729+
Adds a section at the end of the context to inform the AI agent
730+
about missing information that may affect implementation.
731+
732+
Args:
733+
synthesized: The synthesized context markdown.
734+
gaps: List of identified gaps.
735+
quality_score: The calculated quality score.
736+
737+
Returns:
738+
Synthesized context with gaps section appended.
739+
"""
740+
if not gaps:
741+
return synthesized
742+
743+
# Format quality indicator
744+
if quality_score >= 0.8:
745+
quality_label = "Good"
746+
elif quality_score >= 0.6:
747+
quality_label = "Moderate"
748+
elif quality_score >= 0.4:
749+
quality_label = "Limited"
750+
else:
751+
quality_label = "Incomplete"
752+
753+
gaps_section = f"""
754+
755+
---
756+
757+
## ⚠️ Context Quality: {quality_label} ({quality_score:.0%})
758+
759+
The following gaps were identified in the available context.
760+
Consider addressing these before or during implementation:
761+
762+
"""
763+
for gap in gaps:
764+
gaps_section += f"- {gap}\n"
765+
766+
gaps_section += """
767+
*These gaps were automatically detected during context preprocessing.*
768+
*Some may not be relevant to your specific task.*
769+
"""
615770

616-
return score
771+
return synthesized + gaps_section
617772

618773
async def close(self) -> None:
619774
"""Close resources."""

0 commit comments

Comments
 (0)