opencode-arch 1.0.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (65) hide show
  1. opencode_arch/__init__.py +3 -0
  2. opencode_arch/artifacts/__init__.py +48 -0
  3. opencode_arch/artifacts/context.py +451 -0
  4. opencode_arch/artifacts/diagrams.py +451 -0
  5. opencode_arch/artifacts/selector.py +331 -0
  6. opencode_arch/artifacts/templates.py +444 -0
  7. opencode_arch/cli/__init__.py +1 -0
  8. opencode_arch/cli/bench.py +25 -0
  9. opencode_arch/cli/calibrate.py +208 -0
  10. opencode_arch/cli/confidence.py +66 -0
  11. opencode_arch/cli/docs.py +333 -0
  12. opencode_arch/cli/docs_validator.py +295 -0
  13. opencode_arch/cli/export_data.py +133 -0
  14. opencode_arch/cli/extract.py +93 -0
  15. opencode_arch/cli/gap_analyzer.py +107 -0
  16. opencode_arch/cli/generate.py +68 -0
  17. opencode_arch/cli/launch.py +264 -0
  18. opencode_arch/cli/main.py +360 -0
  19. opencode_arch/cli/metrics.py +186 -0
  20. opencode_arch/cli/prompts.py +20 -0
  21. opencode_arch/cli/regen_loop.py +1028 -0
  22. opencode_arch/context/__init__.py +29 -0
  23. opencode_arch/context/formatter.py +492 -0
  24. opencode_arch/context/pipeline_bridge.py +201 -0
  25. opencode_arch/extract/__init__.py +8 -0
  26. opencode_arch/extract/constraint_detector.py +398 -0
  27. opencode_arch/extract/from_artifacts.py +837 -0
  28. opencode_arch/extract/from_code.py +646 -0
  29. opencode_arch/extract/route_detector.py +400 -0
  30. opencode_arch/extract/table_parser.py +177 -0
  31. opencode_arch/learning/__init__.py +19 -0
  32. opencode_arch/learning/adapter.py +157 -0
  33. opencode_arch/learning/assessor.py +170 -0
  34. opencode_arch/learning/classifier.py +144 -0
  35. opencode_arch/learning/lessons.py +139 -0
  36. opencode_arch/learning/maintainer.py +281 -0
  37. opencode_arch/learning/patterns.py +51 -0
  38. opencode_arch/mcp/__init__.py +1 -0
  39. opencode_arch/mcp/__main__.py +8 -0
  40. opencode_arch/mcp/server.py +183 -0
  41. opencode_arch/mcp/tools/__init__.py +1 -0
  42. opencode_arch/mcp/tools/check.py +159 -0
  43. opencode_arch/mcp/tools/extract.py +107 -0
  44. opencode_arch/mcp/tools/feedback.py +65 -0
  45. opencode_arch/mcp/tools/generate.py +104 -0
  46. opencode_arch/mcp/tools/group.py +62 -0
  47. opencode_arch/mcp/tools/ingest.py +101 -0
  48. opencode_arch/mcp/tools/require.py +77 -0
  49. opencode_arch/mcp/tools/scan.py +53 -0
  50. opencode_arch/mcp/tools/slice.py +235 -0
  51. opencode_arch/mcp/tools/validate.py +59 -0
  52. opencode_arch/prompts/__init__.py +1 -0
  53. opencode_arch/prompts/regen.py +36 -0
  54. opencode_arch/runner/__init__.py +5 -0
  55. opencode_arch/runner/base.py +21 -0
  56. opencode_arch/runner/opencode.py +66 -0
  57. opencode_arch/telemetry/__init__.py +6 -0
  58. opencode_arch/telemetry/collector.py +40 -0
  59. opencode_arch/telemetry/recorder.py +12 -0
  60. opencode_arch/telemetry/store.py +537 -0
  61. opencode_arch-1.0.0.dist-info/METADATA +247 -0
  62. opencode_arch-1.0.0.dist-info/RECORD +65 -0
  63. opencode_arch-1.0.0.dist-info/WHEEL +4 -0
  64. opencode_arch-1.0.0.dist-info/entry_points.txt +2 -0
  65. opencode_arch-1.0.0.dist-info/licenses/LICENSE +21 -0
@@ -0,0 +1,157 @@
1
+ """Adaptive prompt optimization based on classified failure patterns.
2
+
3
+ Queries historical telemetry for similar subsystems and applies learned
4
+ strategies to modify prompt construction BEFORE the first attempt.
5
+ """
6
+ from __future__ import annotations
7
+
8
+ from dataclasses import dataclass, field
9
+ from pathlib import Path
10
+ from typing import Any
11
+
12
+ from opencode_arch.learning.patterns import (
13
+ FailureClassification,
14
+ PatternType,
15
+ Strategy,
16
+ )
17
+
18
+
19
+ @dataclass
20
+ class PromptAdaptation:
21
+ """A specific adaptation to apply to prompt construction."""
22
+ strategy: Strategy
23
+ reason: str # why this adaptation is being applied
24
+ params: dict[str, Any] = field(default_factory=dict)
25
+ # params vary by strategy:
26
+ # EXPAND_DEP_CONTEXT: {"dep_modules": ["_base", "_config"], "include_body_hints": True}
27
+ # INCLUDE_SOURCE_EXCERPT: {"functions": ["complex_func"]}
28
+ # PRIORITIZE_CONSTANTS: {"boost_factor": 2.0}
29
+ # FIX_SIGNATURES: {"symbols": ["MyClass.method"]}
30
+ # INCREASE_CONTRACT_CAP: {"new_cap": 100}
31
+
32
+
33
+ def get_adaptations(
34
+ subsystem_name: str,
35
+ dependency_count: int,
36
+ signature_count: int,
37
+ contract_count: int,
38
+ body_hint_coverage: float,
39
+ historical_patterns: list[dict[str, Any]] | None = None,
40
+ ) -> list[PromptAdaptation]:
41
+ """Determine prompt adaptations based on subsystem characteristics and history.
42
+
43
+ This is called BEFORE the first prompt attempt to proactively adjust
44
+ based on what we've learned from previous repos.
45
+
46
+ Args:
47
+ subsystem_name: Name of the current subsystem.
48
+ dependency_count: Number of upstream dependencies.
49
+ signature_count: Number of function signatures in model.
50
+ contract_count: Number of test contracts available.
51
+ body_hint_coverage: Fraction of functions with body_hints (0.0-1.0).
52
+ historical_patterns: Past failure patterns from telemetry for similar subsystems.
53
+
54
+ Returns:
55
+ List of adaptations to apply to prompt construction.
56
+ """
57
+ adaptations: list[PromptAdaptation] = []
58
+
59
+ # Rule 1: High dependency count → expand dep context proactively
60
+ if dependency_count >= 3:
61
+ adaptations.append(PromptAdaptation(
62
+ strategy=Strategy.EXPAND_DEP_CONTEXT,
63
+ reason=f"Subsystem has {dependency_count} dependencies (threshold: 3)",
64
+ params={"include_body_hints": True},
65
+ ))
66
+
67
+ # Rule 2: Low contract count → increase cap and prioritize what we have
68
+ if contract_count < 10:
69
+ adaptations.append(PromptAdaptation(
70
+ strategy=Strategy.INCREASE_CONTRACT_CAP,
71
+ reason=f"Only {contract_count} contracts available (low coverage)",
72
+ params={"new_cap": 200}, # Don't cap at all for low-contract subsystems
73
+ ))
74
+
75
+ # Rule 3: Low body_hint coverage → likely to fail on implementation details
76
+ if body_hint_coverage < 0.5 and signature_count > 5:
77
+ adaptations.append(PromptAdaptation(
78
+ strategy=Strategy.INCLUDE_SOURCE_EXCERPT,
79
+ reason=f"Body hint coverage is {body_hint_coverage:.0%} (threshold: 50%)",
80
+ params={"coverage_threshold": body_hint_coverage},
81
+ ))
82
+
83
+ # Rule 4: Historical patterns suggest specific strategies
84
+ if historical_patterns:
85
+ pattern_counts: dict[str, int] = {}
86
+ for entry in historical_patterns:
87
+ pattern = entry.get("pattern", "")
88
+ if pattern:
89
+ pattern_counts[pattern] = pattern_counts.get(pattern, 0) + 1
90
+
91
+ # If cross_dep failures dominate history for similar subsystems
92
+ if pattern_counts.get("cross_dep", 0) >= 2:
93
+ if not any(a.strategy == Strategy.EXPAND_DEP_CONTEXT for a in adaptations):
94
+ adaptations.append(PromptAdaptation(
95
+ strategy=Strategy.EXPAND_DEP_CONTEXT,
96
+ reason=f"Historical: {pattern_counts['cross_dep']} cross-dep failures in similar subsystems",
97
+ params={"include_body_hints": True, "historical": True},
98
+ ))
99
+
100
+ # If wrong_constant failures are common
101
+ if pattern_counts.get("wrong_constant", 0) >= 2:
102
+ adaptations.append(PromptAdaptation(
103
+ strategy=Strategy.PRIORITIZE_CONSTANTS,
104
+ reason=f"Historical: {pattern_counts['wrong_constant']} constant mismatches in similar subsystems",
105
+ params={"boost_factor": 2.0},
106
+ ))
107
+
108
+ return adaptations
109
+
110
+
111
+ def apply_adaptations(
112
+ adaptations: list[PromptAdaptation],
113
+ contract_cap: int = 50,
114
+ include_dep_body_hints: bool = False,
115
+ ) -> dict[str, Any]:
116
+ """Apply adaptations and return modified prompt parameters.
117
+
118
+ Returns a dict of overrides to pass to _build_prompt:
119
+ - contract_cap: int (max contracts to include)
120
+ - include_dep_body_hints: bool (include body_hints in dep context)
121
+ - extra_context: str (additional context to append)
122
+ - source_excerpts: list[str] (functions needing full source)
123
+ """
124
+ result: dict[str, Any] = {
125
+ "contract_cap": contract_cap,
126
+ "include_dep_body_hints": include_dep_body_hints,
127
+ "extra_context": "",
128
+ "source_excerpts": [],
129
+ "adaptations_applied": [],
130
+ }
131
+
132
+ for adaptation in adaptations:
133
+ result["adaptations_applied"].append(
134
+ f"{adaptation.strategy.value}: {adaptation.reason}"
135
+ )
136
+
137
+ if adaptation.strategy == Strategy.EXPAND_DEP_CONTEXT:
138
+ result["include_dep_body_hints"] = True
139
+
140
+ elif adaptation.strategy == Strategy.INCREASE_CONTRACT_CAP:
141
+ new_cap = adaptation.params.get("new_cap", 100)
142
+ result["contract_cap"] = max(result["contract_cap"], new_cap)
143
+
144
+ elif adaptation.strategy == Strategy.INCLUDE_SOURCE_EXCERPT:
145
+ functions = adaptation.params.get("functions", [])
146
+ result["source_excerpts"].extend(functions)
147
+
148
+ elif adaptation.strategy == Strategy.PRIORITIZE_CONSTANTS:
149
+ # Signal to put constants section BEFORE signatures in prompt
150
+ result["extra_context"] += "\n# NOTE: Constants are critical for this subsystem — use exact values.\n"
151
+
152
+ elif adaptation.strategy == Strategy.FIX_SIGNATURES:
153
+ symbols = adaptation.params.get("symbols", [])
154
+ if symbols:
155
+ result["extra_context"] += f"\n# IMPORTANT: Match exact signatures for: {', '.join(symbols)}\n"
156
+
157
+ return result
@@ -0,0 +1,170 @@
1
+ """Report card generation and grading for regen loop runs.
2
+
3
+ Produces a self-assessment after each repo run: grade (A-F),
4
+ trending metrics, failure pattern breakdown, and improvement actions.
5
+ """
6
+ from __future__ import annotations
7
+
8
+ import json
9
+ from dataclasses import dataclass, field
10
+ from typing import Any
11
+
12
+
13
+ @dataclass
14
+ class ReportCard:
15
+ """Self-assessment report for a regen loop run."""
16
+ repo: str
17
+ mode: str # "normal" or "blind"
18
+
19
+ # Core metrics
20
+ fidelity: float # converged/total (0.0-1.0)
21
+ compression_ratio: float # source_equiv / prompt_tokens
22
+ time_per_subsystem: float # avg seconds
23
+ total_subsystems: int
24
+ converged_subsystems: int
25
+
26
+ # Trend (vs previous)
27
+ fidelity_trend: str = "STABLE" # "UP", "DOWN", "STABLE"
28
+ compression_trend: str = "STABLE"
29
+
30
+ # Pattern breakdown
31
+ failure_patterns: dict[str, int] = field(default_factory=dict)
32
+ novel_patterns: int = 0
33
+
34
+ # Grade + actions
35
+ grade: str = "C"
36
+ improvement_actions: list[str] = field(default_factory=list)
37
+
38
+
39
+ def _compute_grade(fidelity: float, compression_ratio: float, novel_patterns: int) -> str:
40
+ """Compute letter grade from metrics.
41
+
42
+ A: >90% fidelity, compression >5x, 0 novel patterns
43
+ B: >75% fidelity, compression >3x
44
+ C: >60% fidelity
45
+ D: >40% fidelity
46
+ F: <40% fidelity
47
+ """
48
+ if fidelity >= 0.9 and compression_ratio >= 5.0 and novel_patterns == 0:
49
+ return "A"
50
+ elif fidelity >= 0.75 and compression_ratio >= 3.0:
51
+ return "B"
52
+ elif fidelity >= 0.6:
53
+ return "C"
54
+ elif fidelity >= 0.4:
55
+ return "D"
56
+ else:
57
+ return "F"
58
+
59
+
60
+ def _compute_trend(current: float, previous: float | None) -> str:
61
+ """Determine trend direction."""
62
+ if previous is None:
63
+ return "STABLE"
64
+ diff = current - previous
65
+ if diff > 0.05:
66
+ return "UP"
67
+ elif diff < -0.05:
68
+ return "DOWN"
69
+ return "STABLE"
70
+
71
+
72
+ def _compute_improvement_actions(
73
+ fidelity: float,
74
+ compression_ratio: float,
75
+ failure_patterns: dict[str, int],
76
+ novel_patterns: int,
77
+ ) -> list[str]:
78
+ """Generate actionable improvement suggestions."""
79
+ actions = []
80
+
81
+ if fidelity < 0.7:
82
+ dominant_pattern = max(failure_patterns, key=failure_patterns.get) if failure_patterns else None
83
+ if dominant_pattern == "cross_dep":
84
+ actions.append("Expand dependency context: include body_hints for upstream modules")
85
+ elif dominant_pattern == "missing_impl":
86
+ actions.append("Increase body_hint detail level for complex functions")
87
+ elif dominant_pattern == "wrong_constant":
88
+ actions.append("Verify constant extraction captures all test-expected values")
89
+ else:
90
+ actions.append(f"Investigate dominant failure pattern: {dominant_pattern}")
91
+
92
+ if compression_ratio < 3.0:
93
+ actions.append("Model is not providing enough compression — review token allocation")
94
+
95
+ if novel_patterns > 0:
96
+ actions.append(f"Classify {novel_patterns} novel failure patterns and add to taxonomy")
97
+
98
+ if fidelity >= 0.9 and compression_ratio >= 5.0:
99
+ actions.append("System performing well — consider adding a new benchmark repo")
100
+
101
+ return actions
102
+
103
+
104
+ def generate_report_card(
105
+ repo: str,
106
+ mode: str,
107
+ subsystem_results: dict[str, dict[str, Any]],
108
+ previous_fidelity: float | None = None,
109
+ previous_compression: float | None = None,
110
+ ) -> ReportCard:
111
+ """Generate a report card from regen loop results.
112
+
113
+ Args:
114
+ repo: Repository name.
115
+ mode: "normal" or "blind".
116
+ subsystem_results: Dict of {subsystem_name: result_dict} from run_regen_loop.
117
+ previous_fidelity: Fidelity from previous repo (for trend).
118
+ previous_compression: Compression from previous repo (for trend).
119
+
120
+ Returns:
121
+ ReportCard with grade, trends, and improvement actions.
122
+ """
123
+ total = len(subsystem_results)
124
+ converged = sum(1 for r in subsystem_results.values() if r.get("converged", False))
125
+ fidelity = converged / total if total > 0 else 0.0
126
+
127
+ # Compute avg compression from token metrics
128
+ compressions = []
129
+ times = []
130
+ for r in subsystem_results.values():
131
+ metrics = r.get("token_metrics", {})
132
+ if metrics.get("compression_ratio", 0) > 0:
133
+ compressions.append(metrics["compression_ratio"])
134
+ times.append(r.get("time_seconds", 0))
135
+
136
+ avg_compression = sum(compressions) / len(compressions) if compressions else 0.0
137
+ avg_time = sum(times) / len(times) if times else 0.0
138
+
139
+ # Aggregate failure patterns from classifications
140
+ failure_patterns: dict[str, int] = {}
141
+ novel = 0
142
+ for r in subsystem_results.values():
143
+ patterns = r.get("failure_patterns", {})
144
+ for pattern, count in patterns.items():
145
+ if pattern == "unknown":
146
+ novel += count
147
+ else:
148
+ failure_patterns[pattern] = failure_patterns.get(pattern, 0) + count
149
+
150
+ # Compute grade and trends
151
+ grade = _compute_grade(fidelity, avg_compression, novel)
152
+ fidelity_trend = _compute_trend(fidelity, previous_fidelity)
153
+ compression_trend = _compute_trend(avg_compression, previous_compression)
154
+ actions = _compute_improvement_actions(fidelity, avg_compression, failure_patterns, novel)
155
+
156
+ return ReportCard(
157
+ repo=repo,
158
+ mode=mode,
159
+ fidelity=fidelity,
160
+ compression_ratio=avg_compression,
161
+ time_per_subsystem=avg_time,
162
+ total_subsystems=total,
163
+ converged_subsystems=converged,
164
+ fidelity_trend=fidelity_trend,
165
+ compression_trend=compression_trend,
166
+ failure_patterns=failure_patterns,
167
+ novel_patterns=novel,
168
+ grade=grade,
169
+ improvement_actions=actions,
170
+ )
@@ -0,0 +1,144 @@
1
+ """Dual-level failure pattern classifier.
2
+
3
+ Level 1 (Raw): Regex matching on pytest output text
4
+ Level 2 (Structured): Parsed test results cross-referenced with model data
5
+ """
6
+ from __future__ import annotations
7
+
8
+ import re
9
+ from dataclasses import dataclass
10
+ from pathlib import Path
11
+
12
+ from opencode_arch.learning.patterns import (
13
+ FailureClassification,
14
+ PatternType,
15
+ Strategy,
16
+ PATTERN_STRATEGIES,
17
+ )
18
+
19
+
20
+ # --- Level 1: Raw regex patterns ---
21
+
22
+ _RAW_PATTERNS: list[tuple[str, PatternType, float]] = [
23
+ # CROSS_DEP: ImportError from project module
24
+ (r"ImportError: cannot import name '(\w+)' from '([^']+)'", PatternType.CROSS_DEP, 0.95),
25
+ (r"ModuleNotFoundError: No module named '(\w[\w.]*)'", PatternType.CROSS_DEP, 0.8),
26
+ (r"NameError: name '(\w+)' is not defined", PatternType.CROSS_DEP, 0.6),
27
+
28
+ # MISSING_IMPL: AttributeError on generated code
29
+ (r"AttributeError: (?:module |type object )?'(\w+)' has no attribute '(\w+)'", PatternType.MISSING_IMPL, 0.9),
30
+ (r"AttributeError: '(\w+)' object has no attribute '(\w+)'", PatternType.MISSING_IMPL, 0.9),
31
+
32
+ # WRONG_CONSTANT: Assertion with literal values
33
+ (r"AssertionError: assert ['\"](.+)['\"] == ['\"](.+)['\"]", PatternType.WRONG_CONSTANT, 0.85),
34
+ (r"AssertionError: assert (\d+) == (\d+)", PatternType.WRONG_CONSTANT, 0.85),
35
+ (r"AssertionError: (?:assert )?(.+) != (.+)", PatternType.WRONG_CONSTANT, 0.7),
36
+
37
+ # API_MISMATCH: Wrong number of arguments
38
+ (r"TypeError: (\w+)\(\) takes (\d+) positional argument", PatternType.API_MISMATCH, 0.9),
39
+ (r"TypeError: (\w+)\(\) got an unexpected keyword argument '(\w+)'", PatternType.API_MISMATCH, 0.9),
40
+ (r"TypeError: (\w+)\(\) missing (\d+) required positional", PatternType.API_MISMATCH, 0.9),
41
+
42
+ # TEST_INFRA: Test helper imports failing
43
+ (r"ModuleNotFoundError: No module named 'tests\.", PatternType.TEST_INFRA, 0.95),
44
+ (r"ModuleNotFoundError: No module named 'conftest'", PatternType.TEST_INFRA, 0.95),
45
+ (r"ImportError: cannot import name .+ from 'tests\.", PatternType.TEST_INFRA, 0.9),
46
+ ]
47
+
48
+
49
+ def _classify_raw(test_output: str) -> list[FailureClassification]:
50
+ """Level 1: Classify failures from raw pytest output using regex."""
51
+ classifications: list[FailureClassification] = []
52
+ seen_patterns: set[tuple[PatternType, str]] = set()
53
+
54
+ for regex, pattern_type, confidence in _RAW_PATTERNS:
55
+ for match in re.finditer(regex, test_output):
56
+ # Deduplicate: same pattern + same primary symbol
57
+ key = (pattern_type, match.group(1) if match.groups() else "")
58
+ if key in seen_patterns:
59
+ continue
60
+ seen_patterns.add(key)
61
+
62
+ # Extract affected symbols
63
+ symbols = [g for g in match.groups() if g]
64
+
65
+ classifications.append(FailureClassification(
66
+ pattern=pattern_type,
67
+ confidence=confidence,
68
+ raw_signal=match.group(0),
69
+ structured_signal={"groups": symbols, "regex": regex},
70
+ suggested_strategy=PATTERN_STRATEGIES[pattern_type],
71
+ affected_symbols=symbols,
72
+ ))
73
+
74
+ return classifications
75
+
76
+
77
+ def _classify_structured(
78
+ test_output: str,
79
+ pass_rate: float,
80
+ total_tests: int,
81
+ failed_tests: int,
82
+ ) -> list[FailureClassification]:
83
+ """Level 2: Structured classification from parsed test results."""
84
+ classifications: list[FailureClassification] = []
85
+
86
+ # COMPLEX_BEHAVIOR: Many failures in same test module suggest behavioral complexity
87
+ if failed_tests > 5 and pass_rate < 0.5:
88
+ classifications.append(FailureClassification(
89
+ pattern=PatternType.COMPLEX_BEHAVIOR,
90
+ confidence=0.7,
91
+ raw_signal=f"{failed_tests}/{total_tests} tests failed (pass_rate={pass_rate:.0%})",
92
+ structured_signal={
93
+ "failed_count": failed_tests,
94
+ "total_count": total_tests,
95
+ "pass_rate": pass_rate,
96
+ },
97
+ suggested_strategy=Strategy.INCREASE_CONTRACT_CAP,
98
+ ))
99
+
100
+ # If ALL tests fail with import errors, it's likely a single root cause
101
+ import_errors = len(re.findall(r"(?:Import|Module)Error", test_output))
102
+ if import_errors > 0 and import_errors >= failed_tests * 0.8:
103
+ # Most failures are import-related — likely a single cross-dep issue
104
+ classifications.append(FailureClassification(
105
+ pattern=PatternType.CROSS_DEP,
106
+ confidence=0.9,
107
+ raw_signal=f"{import_errors} import errors out of {failed_tests} failures",
108
+ structured_signal={"import_error_ratio": import_errors / max(failed_tests, 1)},
109
+ suggested_strategy=Strategy.EXPAND_DEP_CONTEXT,
110
+ ))
111
+
112
+ return classifications
113
+
114
+
115
+ def classify_failures(
116
+ test_output: str,
117
+ pass_rate: float = 0.0,
118
+ total_tests: int = 0,
119
+ failed_tests: int = 0,
120
+ ) -> list[FailureClassification]:
121
+ """Classify test failures using both raw and structured analysis.
122
+
123
+ Args:
124
+ test_output: Raw pytest stdout/stderr text.
125
+ pass_rate: Overall pass rate (0.0-1.0).
126
+ total_tests: Total number of tests run.
127
+ failed_tests: Number of failed tests.
128
+
129
+ Returns:
130
+ List of classified failures, sorted by confidence (highest first).
131
+ """
132
+ raw_results = _classify_raw(test_output)
133
+ structured_results = _classify_structured(test_output, pass_rate, total_tests, failed_tests)
134
+
135
+ # Merge: structured adds context but don't duplicate pattern types
136
+ raw_patterns = {c.pattern for c in raw_results}
137
+ merged = list(raw_results)
138
+ for sc in structured_results:
139
+ if sc.pattern not in raw_patterns:
140
+ merged.append(sc)
141
+
142
+ # Sort by confidence descending
143
+ merged.sort(key=lambda c: c.confidence, reverse=True)
144
+ return merged
@@ -0,0 +1,139 @@
1
+ """Lesson extraction and storage.
2
+
3
+ Extracts insights from regen loop outcomes that can inform future runs.
4
+ Lessons are automatically derived from patterns and stored for retrieval.
5
+ """
6
+ from __future__ import annotations
7
+
8
+ import hashlib
9
+ import json
10
+ from dataclasses import dataclass, field
11
+ from typing import Any
12
+
13
+
14
+ @dataclass
15
+ class Lesson:
16
+ """A learned insight from regen loop experience."""
17
+ lesson_id: str # unique hash
18
+ discovered_repo: str # where it was first observed
19
+ category: str # "pattern", "optimization", "limitation", "success"
20
+ description: str # human-readable insight
21
+ evidence: dict[str, Any] = field(default_factory=dict)
22
+
23
+
24
+ def _make_lesson_id(category: str, description: str) -> str:
25
+ """Generate a stable lesson ID from content."""
26
+ content = f"{category}:{description}"
27
+ return hashlib.sha256(content.encode()).hexdigest()[:12]
28
+
29
+
30
+ def extract_lessons(
31
+ repo: str,
32
+ mode: str,
33
+ subsystem_results: dict[str, dict[str, Any]],
34
+ ) -> list[Lesson]:
35
+ """Extract lessons from a completed regen loop run.
36
+
37
+ Looks for:
38
+ - Correlation between features and convergence
39
+ - Novel patterns not in taxonomy
40
+ - Success patterns worth replicating
41
+ - Limitations to document
42
+
43
+ Args:
44
+ repo: Repository name.
45
+ mode: "normal" or "blind".
46
+ subsystem_results: Results from run_regen_loop.
47
+
48
+ Returns:
49
+ List of lessons extracted.
50
+ """
51
+ lessons: list[Lesson] = []
52
+
53
+ # Analyze convergence patterns
54
+ converged_features: list[dict] = []
55
+ failed_features: list[dict] = []
56
+
57
+ for name, result in subsystem_results.items():
58
+ features = result.get("features", {})
59
+ if result.get("converged", False):
60
+ converged_features.append(features)
61
+ else:
62
+ failed_features.append(features)
63
+
64
+ # Lesson: Contract count threshold
65
+ if converged_features and failed_features:
66
+ avg_conv_contracts = sum(f.get("contract_count", 0) for f in converged_features) / len(converged_features)
67
+ avg_fail_contracts = sum(f.get("contract_count", 0) for f in failed_features) / len(failed_features)
68
+
69
+ if avg_conv_contracts > avg_fail_contracts * 2:
70
+ desc = (f"Subsystems with >{int(avg_fail_contracts)} contracts converge "
71
+ f"({avg_conv_contracts:.0f} avg vs {avg_fail_contracts:.0f} avg)")
72
+ lessons.append(Lesson(
73
+ lesson_id=_make_lesson_id("pattern", desc),
74
+ discovered_repo=repo,
75
+ category="pattern",
76
+ description=desc,
77
+ evidence={
78
+ "avg_converged_contracts": avg_conv_contracts,
79
+ "avg_failed_contracts": avg_fail_contracts,
80
+ "mode": mode,
81
+ },
82
+ ))
83
+
84
+ # Lesson: Signature count correlation
85
+ if converged_features and failed_features:
86
+ avg_conv_sigs = sum(f.get("signature_count", 0) for f in converged_features) / len(converged_features)
87
+ avg_fail_sigs = sum(f.get("signature_count", 0) for f in failed_features) / len(failed_features)
88
+
89
+ if avg_fail_sigs > avg_conv_sigs * 1.5:
90
+ desc = (f"High signature count ({avg_fail_sigs:.0f}) correlates with failure — "
91
+ "complex modules need more context")
92
+ lessons.append(Lesson(
93
+ lesson_id=_make_lesson_id("limitation", desc),
94
+ discovered_repo=repo,
95
+ category="limitation",
96
+ description=desc,
97
+ evidence={
98
+ "avg_converged_signatures": avg_conv_sigs,
99
+ "avg_failed_signatures": avg_fail_sigs,
100
+ },
101
+ ))
102
+
103
+ # Lesson: Perfect fidelity on simple subsystems
104
+ simple_converged = [
105
+ name for name, r in subsystem_results.items()
106
+ if r.get("converged") and r.get("features", {}).get("signature_count", 0) < 10
107
+ ]
108
+ if len(simple_converged) >= 3:
109
+ desc = f"Simple subsystems (<10 signatures) reliably converge: {', '.join(simple_converged[:5])}"
110
+ lessons.append(Lesson(
111
+ lesson_id=_make_lesson_id("success", desc),
112
+ discovered_repo=repo,
113
+ category="success",
114
+ description=desc,
115
+ evidence={"subsystems": simple_converged},
116
+ ))
117
+
118
+ # Lesson: All failures are same pattern (systemic issue)
119
+ all_patterns: dict[str, int] = {}
120
+ for r in subsystem_results.values():
121
+ if not r.get("converged"):
122
+ for pattern, count in r.get("failure_patterns", {}).items():
123
+ all_patterns[pattern] = all_patterns.get(pattern, 0) + count
124
+
125
+ if all_patterns:
126
+ dominant = max(all_patterns, key=all_patterns.get)
127
+ total_failures = sum(all_patterns.values())
128
+ if all_patterns[dominant] / total_failures > 0.7:
129
+ desc = (f"Dominant failure pattern '{dominant}' accounts for "
130
+ f"{all_patterns[dominant]}/{total_failures} failures — systemic issue")
131
+ lessons.append(Lesson(
132
+ lesson_id=_make_lesson_id("pattern", desc),
133
+ discovered_repo=repo,
134
+ category="pattern",
135
+ description=desc,
136
+ evidence={"pattern_distribution": all_patterns},
137
+ ))
138
+
139
+ return lessons