Skip to main content

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

  1. Alternative A - Rejected because [reason]

  2. 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 """