scripts-adr-expert
""" ADR (Architecture Decision Record) Type Expert
Specializes in understanding what makes a document an "ADR" document. ADRs document architectural decisions with context, decision, and consequences.
Key characteristics:
- Status field (Proposed, Accepted, Deprecated, Superseded)
- Context section explaining the problem
- Decision section with the choice made
- Consequences section (positive and negative)
- ADR numbering pattern (ADR-001, ADR-002) """
import re from typing import Dict, List from pathlib import Path import sys
sys.path.insert(0, str(Path(file).parent.parent)) from core.models import Document, AnalystVote from .base import TypeExpert, TypeAnalysis, ContentEnhancement
class ADRExpert(TypeExpert): """Expert in identifying and enhancing ADR documents."""
expert_type = "adr"
strong_indicators = [
r'ADR[-_]?\d+',
r'architecture\s+decision',
r'decision\s+record',
r'status:\s*(?:proposed|accepted|deprecated|superseded)',
r'##\s*context',
r'##\s*decision',
r'##\s*consequences',
]
analyst_expectations = {
'metadata': ["type: adr in frontmatter", "ADR number", "status field"],
'content': ["## Context", "## Decision", "## Consequences"],
'structural': ["Path contains /adrs/ or /decisions/", "ADR structure"],
'semantic': ["Decision rationale language", "Trade-off analysis"],
'pattern': ["ADR-XXX filename pattern", "Title indicates decision"],
}
def analyze(
self,
document: Document,
analyst_votes: List[AnalystVote]
) -> TypeAnalysis:
"""Analyze if document is truly an ADR."""
content = document.body or document.content
headings = self.extract_headings(content)
h2_texts = [h[1].lower() for h in headings if h[0] == 2]
evidence_for = []
evidence_against = []
# Check strong indicators
for indicator in self.strong_indicators:
if re.search(indicator, content, re.I):
evidence_for.append(f"Contains ADR indicator: '{indicator}'")
# Check for ADR canonical sections (critical)
adr_sections = {'context', 'decision', 'consequences'}
found_sections = []
for section in adr_sections:
if any(section in h for h in h2_texts):
found_sections.append(section)
evidence_for.append(f"Has {section} section")
if len(found_sections) >= 3:
evidence_for.append("Has complete ADR structure (context + decision + consequences)")
# Check for status
if re.search(r'status:\s*(?:proposed|accepted|deprecated|superseded|rejected)', content, re.I):
evidence_for.append("Has ADR status field")
# Check for ADR numbering
if re.search(r'ADR[-_]?\d{3,4}', content, re.I):
evidence_for.append("Has ADR numbering")
# Check for decision language
if re.search(r'we (?:will|decided|chose|selected)', content, re.I):
evidence_for.append("Uses decision language")
# Check for trade-off language
if re.search(r'trade[-\s]?off|pros?\s+and\s+cons?|advantages?|disadvantages?', content, re.I):
evidence_for.append("Discusses trade-offs")
# Check path
path_str = str(document.path).lower()
if '/adr' in path_str or '/decision' in path_str:
evidence_for.append("Located in ADRs/decisions directory")
# Evidence against
if any('step' in h for h in h2_texts):
evidence_against.append("Has step sections - might be guide")
if any('phase' in h for h in h2_texts):
evidence_against.append("Has phase sections - might be workflow")
if re.search(r'you are\b', content, re.I):
evidence_against.append("Has persona language - might be agent")
confidence = min(0.98, len(evidence_for) * 0.14)
if evidence_against:
confidence -= len(evidence_against) * 0.1
is_adr = len(found_sections) >= 2 or (len(evidence_for) >= 3 and confidence > 0.5)
# Missing signals
missing = []
if 'context' not in found_sections:
missing.append('context')
if 'decision' not in found_sections:
missing.append('decision')
if 'consequences' not in found_sections:
missing.append('consequences')
if not re.search(r'status:', content, re.I):
missing.append('status')
disagreeing = self.identify_disagreeing_analysts(analyst_votes, 'adr')
analysts_to_sway = {
name: f"Needs ADR structure (context, decision, consequences) to classify as ADR"
for name in disagreeing
}
return TypeAnalysis(
is_this_type=is_adr,
confidence=max(0, confidence),
evidence_for=evidence_for,
evidence_against=evidence_against,
semantic_purpose=self.analyze_semantic_purpose(document),
missing_signals=missing,
recommended_changes=[],
analysts_to_sway=analysts_to_sway,
expert_type=self.expert_type
)
def generate_enhancements(
self,
document: Document,
analysis: TypeAnalysis
) -> List[ContentEnhancement]:
"""Generate contextual ADR enhancements."""
enhancements = []
content = document.body or document.content
title = document.frontmatter.get('title', 'Architecture Decision')
# Infer the decision topic
topic = self._infer_decision_topic(title, content)
if 'status' in analysis.missing_signals:
enhancements.append(ContentEnhancement(
signal_type='status',
content=self._generate_status(),
insertion_point='after_frontmatter',
reason="ADRs need explicit status",
expected_analyst_boost={'metadata': 0.15, 'pattern': 0.10},
priority=1
))
if 'context' in analysis.missing_signals:
enhancements.append(ContentEnhancement(
signal_type='context',
content=self._generate_context(topic, content),
insertion_point='after_status',
reason="ADRs need context section explaining the problem",
expected_analyst_boost={'content': 0.20, 'semantic': 0.15},
priority=1
))
if 'decision' in analysis.missing_signals:
enhancements.append(ContentEnhancement(
signal_type='decision',
content=self._generate_decision(topic, content),
insertion_point='after_context',
reason="ADRs need decision section documenting the choice",
expected_analyst_boost={'content': 0.20, 'semantic': 0.15},
priority=1
))
if 'consequences' in analysis.missing_signals:
enhancements.append(ContentEnhancement(
signal_type='consequences',
content=self._generate_consequences(topic),
insertion_point='after_decision',
reason="ADRs need consequences section with trade-offs",
expected_analyst_boost={'content': 0.15, 'semantic': 0.10},
priority=1
))
return enhancements
def _infer_decision_topic(self, title: str, content: str) -> str:
"""Infer what decision is being made."""
# Look for decision patterns
decision_match = re.search(r'(?:decide|chose|selected|using)\s+(.+?)(?:\.|,|$)', content, re.I)
if decision_match:
return decision_match.group(1).strip()[:50]
# Look for technology mentions
tech_match = re.search(r'(React|Vue|Angular|PostgreSQL|MongoDB|Python|Rust|TypeScript|Kubernetes|Docker)', content)
if tech_match:
return f"use {tech_match.group(1)}"
return title.lower().replace('adr', '').replace('-', ' ').strip()
def _generate_status(self) -> str:
"""Generate status section."""
return """
Status
Status: Proposed
Date: [Current Date]
Deciders: [Team/Individual] """
def _generate_context(self, topic: str, content: str) -> str:
"""Generate context section."""
# Try to infer problem from content
problem = f"We need to make a decision regarding {topic}"
if re.search(r'problem|issue|challenge', content, re.I):
match = re.search(r'(?:problem|issue|challenge)[:\s]+(.+?)(?:\.|$)', content, re.I)
if match:
problem = match.group(1).strip()
return f"""
Context
{problem}.
Current Situation
- Describe the current state
- Identify the forces at play
- Note any constraints or requirements
Problem Statement
We need to address this situation to ensure [goals/requirements]. """
def _generate_decision(self, topic: str, content: str) -> str:
"""Generate decision section."""
return f"""
Decision
We will {topic}.
Rationale
- Primary reason for this choice
- How it addresses the context
- Why alternatives were not selected
Alternatives Considered
-
Alternative A - Rejected because [reason]
-
Alternative B - Rejected because [reason] """
def _generate_consequences(self, topic: str) -> str: """Generate consequences section.""" return """
Consequences
Positive
- [Benefit 1]
- [Benefit 2]
- [Benefit 3]
Negative
- [Trade-off 1]
- [Trade-off 2]
Risks
- [Risk 1]: Mitigation strategy
- [Risk 2]: Mitigation strategy
Follow-up Actions
- Action item 1
- Action item 2 """