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,360 @@
1
+ """CLI entry point for opencode-arch."""
2
+ from __future__ import annotations
3
+
4
+ import argparse
5
+ import asyncio
6
+ import sys
7
+ from pathlib import Path
8
+
9
+
10
+ def main():
11
+ # If no args or first arg looks like a path (not a subcommand), launch interactive
12
+ if len(sys.argv) <= 1 or (
13
+ len(sys.argv) == 2 and not sys.argv[1].startswith("-")
14
+ and sys.argv[1] not in (
15
+ "extract", "generate", "bench", "metrics", "report",
16
+ "regen-loop", "confidence", "calibrate", "export-data", "docs",
17
+ )
18
+ ):
19
+ from opencode_arch.cli.launch import run_launch
20
+ repo_path = sys.argv[1] if len(sys.argv) == 2 else None
21
+ run_launch(repo_path=repo_path)
22
+ return
23
+
24
+ parser = argparse.ArgumentParser(
25
+ prog="opencode-arch",
26
+ description="Architecture-aware development — launch interactive session or run commands",
27
+ )
28
+ subparsers = parser.add_subparsers(dest="command", required=True)
29
+
30
+ # extract
31
+ extract_p = subparsers.add_parser("extract", help="Extract architecture from a repository")
32
+ extract_p.add_argument("repo_path", help="Path to the target repository")
33
+ extract_p.add_argument("--budget", type=int, default=4000, help="Token budget (default: 4000)")
34
+ extract_p.add_argument("--focus", default="all", help="Focus: all, F-block ID, layer name")
35
+ extract_p.add_argument("--target-score", type=int, default=80, help="Min validation score (default: 80)")
36
+ extract_p.add_argument("--model", default=None, help="Model override (provider/model)")
37
+ extract_p.add_argument("--timeout", type=int, default=600, help="Timeout seconds (default: 600)")
38
+
39
+ # generate
40
+ gen_p = subparsers.add_parser("generate", help="Generate code and run tests")
41
+ gen_p.add_argument("repo_path", help="Path to the target repository")
42
+ gen_p.add_argument("--max-iter", type=int, default=3, help="Max retries (default: 3)")
43
+ gen_p.add_argument("--test-command", default=None, help="Custom test command")
44
+ gen_p.add_argument("--model", default=None, help="Model override (provider/model)")
45
+ gen_p.add_argument("--timeout", type=int, default=600, help="Timeout seconds (default: 600)")
46
+
47
+ # bench
48
+ bench_p = subparsers.add_parser("bench", help="Benchmark extraction on multiple repos")
49
+ bench_p.add_argument("repos", nargs="+", help="Paths to target repositories")
50
+ bench_p.add_argument("--output", default=None, help="Output file (JSON)")
51
+ bench_p.add_argument("--model", default=None, help="Model override (provider/model)")
52
+
53
+ # metrics
54
+ metrics_p = subparsers.add_parser("metrics", help="Display recorded metrics")
55
+ metrics_p.add_argument("--tool", default=None, help="Filter by tool name")
56
+ metrics_p.add_argument("--last", type=int, default=10, help="Number of records (default: 10)")
57
+ metrics_p.add_argument("--learning-curve", action="store_true", help="Show learning curve data")
58
+ metrics_p.add_argument("--drift", action="store_true", help="Show documentation drift flags")
59
+
60
+ # report
61
+ report_p = subparsers.add_parser("report", help="Display latest report card")
62
+ report_p.add_argument("--repo", default=None, help="Filter by repository name")
63
+ report_p.add_argument("--last", type=int, default=5, help="Number of report cards (default: 5)")
64
+
65
+ # regen-loop
66
+ regen_p = subparsers.add_parser("regen-loop", help="Run decomposed regen loop")
67
+ regen_p.add_argument("--repo", required=True, help="Path to the target repository")
68
+ regen_p.add_argument("--max-iterations", type=int, default=5, help="Max iterations per subsystem (default: 5)")
69
+ regen_p.add_argument("--target", type=float, default=0.5, help="Target pass rate (default: 0.5)")
70
+ regen_p.add_argument("--subsystem", default=None, help="Process only this subsystem")
71
+ regen_p.add_argument("--blind", action="store_true", default=False, help="Blind mode: agent only gets model context, no source access")
72
+ regen_p.add_argument("--model", default=None, help="Model override (provider/model)")
73
+ regen_p.add_argument("--timeout", type=int, default=600, help="Timeout seconds per LLM call (default: 600)")
74
+
75
+ # confidence
76
+ conf_p = subparsers.add_parser("confidence", help="Show confidence report for extracted model")
77
+ conf_p.add_argument("repo_path", help="Path to the target repository")
78
+
79
+ # calibrate
80
+ cal_p = subparsers.add_parser("calibrate", help="Spot-check confidence by attempting regeneration")
81
+ cal_p.add_argument("repo_path", help="Path to the target repository")
82
+ cal_p.add_argument("--n", type=int, default=3, help="Number of components to calibrate (default: 3)")
83
+ cal_p.add_argument("--min-confidence", type=float, default=0.7, help="Min confidence threshold (default: 0.7)")
84
+
85
+ # export-data
86
+ export_p = subparsers.add_parser("export-data", help="Export training corpus from .architecture/ artifacts")
87
+ export_p.add_argument("repos", nargs="*", default=["."], help="Paths to repos (default: current dir)")
88
+ export_p.add_argument("--output", "-o", default="corpus.jsonl", help="Output file (default: corpus.jsonl)")
89
+ export_p.add_argument("--include-telemetry", action="store_true", help="Include telemetry DB records")
90
+
91
+ # docs (with sub-subcommands: generate, list)
92
+ docs_p = subparsers.add_parser("docs", help="Generate SE documentation")
93
+ docs_sub = docs_p.add_subparsers(dest="docs_command", required=True)
94
+
95
+ # docs generate
96
+ docs_gen_p = docs_sub.add_parser("generate", help="Generate documentation artifacts")
97
+ docs_gen_p.add_argument("project_path", help="Path to the target project")
98
+ docs_gen_p.add_argument("--output-dir", default=None, help="Output directory (default: docs/se/)")
99
+ docs_gen_p.add_argument("--artifacts", default=None, help="Comma-separated artifact IDs to generate")
100
+ docs_gen_p.add_argument("--model-path", default=None, help="Path to .architecture-model.yaml")
101
+ docs_gen_p.add_argument("--model", default=None, help="Model override (provider/model)")
102
+ docs_gen_p.add_argument("--timeout", type=int, default=600, help="Timeout seconds (default: 600)")
103
+
104
+ # docs list
105
+ docs_list_p = docs_sub.add_parser("list", help="List artifacts that would be generated")
106
+ docs_list_p.add_argument("project_path", help="Path to the target project")
107
+ docs_list_p.add_argument("--model-path", default=None, help="Path to .architecture-model.yaml")
108
+
109
+ # docs validate
110
+ docs_val_p = docs_sub.add_parser("validate", help="Validate generated documentation")
111
+ docs_val_p.add_argument("project_path", help="Path to the target project")
112
+ docs_val_p.add_argument("--docs-dir", default=None, help="Docs directory (default: docs/se/)")
113
+ docs_val_p.add_argument("--model-path", default=None, help="Path to .architecture-model.yaml")
114
+
115
+ args = parser.parse_args()
116
+
117
+ if args.command == "extract":
118
+ from opencode_arch.cli.extract import run_extract
119
+ from opencode_arch.runner.opencode import OpencodeRunner
120
+ runner = OpencodeRunner(timeout=args.timeout, model=args.model)
121
+ result = asyncio.run(run_extract(
122
+ repo_path=args.repo_path, runner=runner,
123
+ budget=args.budget, focus=args.focus, target_score=args.target_score,
124
+ ))
125
+ _print_extract_result(result)
126
+
127
+ elif args.command == "generate":
128
+ from opencode_arch.cli.generate import run_generate
129
+ from opencode_arch.runner.opencode import OpencodeRunner
130
+ runner = OpencodeRunner(timeout=args.timeout, model=args.model)
131
+ result = asyncio.run(run_generate(
132
+ repo_path=args.repo_path, runner=runner,
133
+ max_iter=args.max_iter, test_command=args.test_command,
134
+ ))
135
+ _print_generate_result(result)
136
+
137
+ elif args.command == "bench":
138
+ from opencode_arch.cli.bench import run_bench
139
+ from opencode_arch.runner.opencode import OpencodeRunner
140
+ runner = OpencodeRunner(model=args.model)
141
+ results = asyncio.run(run_bench(repos=args.repos, runner=runner))
142
+ _print_bench_results(results, output_file=args.output)
143
+
144
+ elif args.command == "metrics":
145
+ from opencode_arch.cli.metrics import show_metrics
146
+ show_metrics(
147
+ tool=args.tool,
148
+ last=args.last,
149
+ learning_curve=args.learning_curve,
150
+ drift=args.drift,
151
+ )
152
+
153
+ elif args.command == "report":
154
+ from opencode_arch.cli.metrics import show_report
155
+ show_report(repo=args.repo, last=args.last)
156
+
157
+ elif args.command == "regen-loop":
158
+ from opencode_arch.cli.regen_loop import run_regen_loop
159
+ from opencode_arch.runner.opencode import OpencodeRunner
160
+ runner = OpencodeRunner(timeout=args.timeout, model=args.model)
161
+ result = asyncio.run(run_regen_loop(
162
+ repo_path=Path(args.repo),
163
+ runner=runner,
164
+ max_iterations=args.max_iterations,
165
+ target_pass_rate=args.target,
166
+ subsystem_name=args.subsystem,
167
+ blind=args.blind,
168
+ ))
169
+ _print_regen_result(result)
170
+
171
+ elif args.command == "confidence":
172
+ from opencode_arch.cli.confidence import run_confidence
173
+ output = run_confidence(args.repo_path)
174
+ print(output)
175
+
176
+ elif args.command == "calibrate":
177
+ from opencode_arch.cli.calibrate import select_calibration_targets, format_calibration_prompt
178
+ from architecture_model.core.parser import load_model
179
+ from architecture_model.core.confidence import compute_model_confidence
180
+ model_file = Path(args.repo_path) / ".architecture-model.yaml"
181
+ if not model_file.exists():
182
+ model_file = Path(args.repo_path) / ".architecture-model-extracted.yaml"
183
+ if not model_file.exists():
184
+ print("Error: No model found. Run extraction first.")
185
+ sys.exit(1)
186
+ model = load_model(model_file)
187
+ compute_model_confidence(model)
188
+ targets = select_calibration_targets(model, n=args.n, min_confidence=args.min_confidence)
189
+ if not targets:
190
+ print("No calibration targets found.")
191
+ sys.exit(0)
192
+ for comp in targets:
193
+ prompt = format_calibration_prompt(comp)
194
+ print(prompt)
195
+ print()
196
+
197
+ elif args.command == "export-data":
198
+ from opencode_arch.cli.export_data import run_export_data
199
+ run_export_data(
200
+ repos=args.repos,
201
+ output=args.output,
202
+ include_telemetry=args.include_telemetry,
203
+ )
204
+
205
+ elif args.command == "docs":
206
+ from opencode_arch.cli.docs import run_docs_generate, run_docs_list
207
+ if args.docs_command == "generate":
208
+ from opencode_arch.runner.opencode import OpencodeRunner
209
+ runner = OpencodeRunner(timeout=args.timeout, model=args.model)
210
+ output_dir = Path(args.output_dir) if args.output_dir else None
211
+ artifact_filter = args.artifacts.split(",") if args.artifacts else None
212
+ model_path = Path(args.model_path) if args.model_path else None
213
+ result = asyncio.run(run_docs_generate(
214
+ repo_path=Path(args.project_path),
215
+ runner=runner,
216
+ output_dir=output_dir,
217
+ artifact_filter=artifact_filter,
218
+ model_path=model_path,
219
+ ))
220
+ _print_docs_result(result)
221
+ elif args.docs_command == "list":
222
+ model_path = Path(args.model_path) if args.model_path else None
223
+ artifacts = asyncio.run(run_docs_list(
224
+ repo_path=Path(args.project_path),
225
+ model_path=model_path,
226
+ ))
227
+ _print_docs_list(artifacts)
228
+ elif args.docs_command == "validate":
229
+ from opencode_arch.cli.docs_validator import validate_docs, DocsValidationResult
230
+ from architecture_model import load_model, generate_manifest
231
+ project_path = Path(args.project_path)
232
+ model_path = Path(args.model_path) if args.model_path else project_path / ".architecture-model.yaml"
233
+ docs_dir = Path(args.docs_dir) if args.docs_dir else project_path / "docs" / "se"
234
+ model = load_model(model_path)
235
+ try:
236
+ manifest = generate_manifest(project_path)
237
+ except Exception:
238
+ manifest = None
239
+ result = validate_docs(docs_dir, model, manifest)
240
+ _print_validation_result(result)
241
+
242
+
243
+ def _print_docs_result(result):
244
+ from opencode_arch.cli.docs import DocsResult
245
+ if result.error:
246
+ print(f"Docs generation failed: {result.error}")
247
+ sys.exit(1)
248
+ print(f"\nDocs Generation Complete")
249
+ print(f" Output: {result.output_dir}")
250
+ print(f" Generated: {len(result.generated)} artifacts")
251
+ if result.failed:
252
+ print(f" Failed: {len(result.failed)} ({', '.join(result.failed)})")
253
+ print(f" Time: {result.time_seconds:.1f}s")
254
+
255
+
256
+ def _print_docs_list(artifacts: list[dict]):
257
+ print(f"\nArtifacts that would be generated ({len(artifacts)}):")
258
+ print(f"{'ID':<25} {'Name':<25} {'Category':<15} {'Priority'}")
259
+ print("-" * 75)
260
+ for a in artifacts:
261
+ print(f"{a['id']:<25} {a['name']:<25} {a['category']:<15} {a['priority']}")
262
+
263
+
264
+ def _print_validation_result(result):
265
+ from opencode_arch.cli.docs_validator import DocsValidationResult
266
+ status = "PASS" if result.is_valid else "FAIL"
267
+ print(f"\nDocs Validation: {status}")
268
+ print(f" Artifacts: {result.total_artifacts} ({result.passed} passed, {result.failed} failed)")
269
+ if result.issues:
270
+ print(f" Issues: {len(result.issues)}")
271
+ for issue in result.issues[:20]: # cap display
272
+ sev = "ERR" if issue.severity == "error" else "WRN"
273
+ print(f" [{sev}] {issue.artifact_id}:{issue.line} {issue.issue_type}: {issue.message}")
274
+ if len(result.issues) > 20:
275
+ print(f" ... and {len(result.issues) - 20} more")
276
+ if not result.is_valid:
277
+ sys.exit(1)
278
+
279
+
280
+ def _print_extract_result(result: dict):
281
+ if result.get("success"):
282
+ print("Extraction successful!")
283
+ print(f" Score: {result['score']}/100")
284
+ print(f" Tokens: {result['tokens_used']}")
285
+ print(f" Time: {result['time_seconds']:.1f}s")
286
+ print(f" Path: {result.get('path', 'N/A')}")
287
+ if result.get("issues"):
288
+ print(f" Issues: {len(result['issues'])}")
289
+ else:
290
+ print(f"Extraction failed: {result.get('error', 'unknown')}")
291
+ sys.exit(1)
292
+
293
+
294
+ def _print_generate_result(result: dict):
295
+ if result.get("passed"):
296
+ print("Code generation successful!")
297
+ print(f" Pass rate: {result['pass_rate']:.0%}")
298
+ print(f" Tests: {result['total_tests']}")
299
+ print(f" Iterations: {result['iterations']}")
300
+ print(f" Time: {result['time_seconds']:.1f}s")
301
+ else:
302
+ print("Code generation incomplete")
303
+ print(f" Pass rate: {result.get('pass_rate', 0):.0%}")
304
+ if result.get("error"):
305
+ print(f" Error: {result['error']}")
306
+ sys.exit(1)
307
+
308
+
309
+ def _print_bench_results(results: list[dict], output_file: str | None):
310
+ import json
311
+ print(f"\nBenchmark Results ({len(results)} repos)")
312
+ print("-" * 60)
313
+ for r in results:
314
+ status = "PASS" if r.get("success") else "FAIL"
315
+ print(f" [{status}] {r.get('repo', '?'):30} score={r.get('score', 0):3} time={r.get('time_seconds', 0):.1f}s")
316
+ scores = [r["score"] for r in results if r.get("success")]
317
+ if scores:
318
+ print(f"\n Average score: {sum(scores)/len(scores):.0f}/100")
319
+ print(f" Success rate: {len(scores)}/{len(results)}")
320
+ if output_file:
321
+ Path(output_file).write_text(json.dumps(results, indent=2))
322
+ print(f"\n Saved to: {output_file}")
323
+
324
+
325
+ def _print_regen_result(result: dict):
326
+ if result.get("error"):
327
+ print(f"Regen-loop failed: {result['error']}")
328
+ sys.exit(1)
329
+
330
+ print(f"\nRegen-Loop Results")
331
+ print("-" * 60)
332
+ print(f" Subsystems: {result['total_subsystems']}")
333
+ print(f" Converged: {result['converged_subsystems']}/{result['total_subsystems']}")
334
+ print(f" Time: {result['time_seconds']:.1f}s")
335
+
336
+ sub_results = result.get("subsystem_results", {})
337
+ if sub_results:
338
+ print(f"\n Per-subsystem:")
339
+ for name, sub in sub_results.items():
340
+ status = "OK" if sub.get("converged") else "INCOMPLETE"
341
+ print(f" [{status:10}] {name:20} pass_rate={sub.get('pass_rate', 0):.0%} iter={sub.get('iterations', 0)}")
342
+
343
+ full = result.get("full_test_result", {})
344
+ if full.get("total", 0) > 0:
345
+ print(f"\n Full suite: {full['passed']}/{full['total']} ({full['pass_rate']:.0%})")
346
+
347
+ # Show report card if available
348
+ report = result.get("report_card")
349
+ if report:
350
+ print(f"\n Report Card: Grade {report['grade']}")
351
+ print(f" Fidelity: {report['fidelity']:.0%}")
352
+ print(f" Compression: {report['compression_ratio']:.1f}x")
353
+ if report.get("improvement_actions"):
354
+ print(f" Actions:")
355
+ for action in report["improvement_actions"]:
356
+ print(f" - {action}")
357
+
358
+
359
+ if __name__ == "__main__":
360
+ main()
@@ -0,0 +1,186 @@
1
+ """Metrics and report commands - display recorded telemetry."""
2
+ from __future__ import annotations
3
+
4
+ import json
5
+ import time
6
+
7
+ from opencode_arch.telemetry.store import TelemetryStore
8
+
9
+
10
+ def show_metrics(
11
+ tool: str | None = None,
12
+ last: int = 10,
13
+ learning_curve: bool = False,
14
+ drift: bool = False,
15
+ ):
16
+ """Query and display metrics from the telemetry store."""
17
+ store = TelemetryStore()
18
+
19
+ if learning_curve:
20
+ _show_learning_curve(store)
21
+ return
22
+
23
+ if drift:
24
+ _show_drift_flags(store)
25
+ return
26
+
27
+ records = store.query(tool=tool, limit=last)
28
+ print(format_metrics_table(records))
29
+ if tool and records:
30
+ avgs = store.averages(tool=tool)
31
+ print(f"\n Averages for '{tool}':")
32
+ print(f" Tokens: {avgs['avg_context_tokens']:.0f}")
33
+ print(f" Quality: {avgs['avg_output_quality']:.0f}/100")
34
+ print(f" Iterations: {avgs['avg_iterations']:.1f}")
35
+
36
+
37
+ def show_report(repo: str | None = None, last: int = 5):
38
+ """Display report cards from telemetry."""
39
+ store = TelemetryStore()
40
+ cards = store.get_report_cards(repo=repo, limit=last)
41
+
42
+ if not cards:
43
+ print(" No report cards found.")
44
+ return
45
+
46
+ print(f"\nReport Cards (last {last})")
47
+ print("=" * 70)
48
+
49
+ for card in cards:
50
+ grade = card.get("grade", "?")
51
+ repo_name = card.get("repo", "?")
52
+ mode = card.get("mode", "?")
53
+ fidelity = card.get("fidelity", 0.0)
54
+ compression = card.get("compression_ratio", 0.0)
55
+ novel = card.get("novel_patterns", 0)
56
+ ts = card.get("timestamp", "")[:10] # date only
57
+
58
+ # Grade color indicator
59
+ grade_indicator = {"A": "+", "B": "+", "C": "~", "D": "-", "F": "!"}
60
+ indicator = grade_indicator.get(grade, "?")
61
+
62
+ print(f"\n [{indicator}] Grade {grade} | {repo_name} ({mode}) | {ts}")
63
+ print(f" Fidelity: {fidelity:.0%}")
64
+ print(f" Compression: {compression:.1f}x")
65
+ if novel > 0:
66
+ print(f" Novel: {novel} unclassified patterns")
67
+
68
+ # Failure patterns
69
+ patterns_str = card.get("failure_patterns", "{}")
70
+ try:
71
+ patterns = json.loads(patterns_str) if isinstance(patterns_str, str) else patterns_str
72
+ if patterns:
73
+ print(f" Patterns: {', '.join(f'{k}={v}' for k, v in patterns.items())}")
74
+ except (json.JSONDecodeError, TypeError):
75
+ pass
76
+
77
+ # Improvement actions
78
+ actions_str = card.get("improvement_actions", "[]")
79
+ try:
80
+ actions = json.loads(actions_str) if isinstance(actions_str, str) else actions_str
81
+ if actions:
82
+ print(f" Actions:")
83
+ for action in actions:
84
+ print(f" - {action}")
85
+ except (json.JSONDecodeError, TypeError):
86
+ pass
87
+
88
+ # Also show lessons if available
89
+ lessons = store.get_lessons()
90
+ if lessons:
91
+ print(f"\n\nLessons Learned ({len(lessons)} total)")
92
+ print("-" * 50)
93
+ for lesson in lessons[:10]:
94
+ cat = lesson.get("category", "?")
95
+ desc = lesson.get("description", "?")
96
+ repo_name = lesson.get("discovered_repo", "?")
97
+ print(f" [{cat:12}] {desc}")
98
+ print(f" (from {repo_name})")
99
+
100
+
101
+ def _show_learning_curve(store: TelemetryStore):
102
+ """Display learning curve data showing improvement over repos."""
103
+ entries = store.get_learning_curve()
104
+
105
+ if not entries:
106
+ print(" No learning curve data yet.")
107
+ return
108
+
109
+ print(f"\nLearning Curve ({len(entries)} repos processed)")
110
+ print("=" * 80)
111
+ print(f" {'#':<3} {'Repo':<15} {'Mode':<7} {'Conv':>5} {'Pass%':>6} "
112
+ f"{'Iter':>5} {'Compress':>9} {'Time':>7}")
113
+ print(" " + "-" * 75)
114
+
115
+ for entry in entries:
116
+ seq = entry.get("repo_sequence", 0)
117
+ repo = entry.get("repo", "?")[:14]
118
+ mode = entry.get("mode", "?")
119
+ total = entry.get("total_subsystems", 0)
120
+ conv = entry.get("converged_subsystems", 0)
121
+ pass_rate = entry.get("avg_pass_rate", 0)
122
+ iters = entry.get("avg_iterations", 0)
123
+ compression = entry.get("avg_compression_ratio", 0)
124
+ time_s = entry.get("total_time_seconds", 0)
125
+
126
+ conv_str = f"{conv}/{total}"
127
+ print(f" {seq:<3} {repo:<15} {mode:<7} {conv_str:>5} {pass_rate:>5.0%} "
128
+ f"{iters:>5.1f} {compression:>8.1f}x {time_s:>6.0f}s")
129
+
130
+ # Trend summary
131
+ if len(entries) >= 2:
132
+ first = entries[0]
133
+ last = entries[-1]
134
+ fid_first = first.get("converged_subsystems", 0) / max(first.get("total_subsystems", 1), 1)
135
+ fid_last = last.get("converged_subsystems", 0) / max(last.get("total_subsystems", 1), 1)
136
+ comp_first = first.get("avg_compression_ratio", 0)
137
+ comp_last = last.get("avg_compression_ratio", 0)
138
+
139
+ print(f"\n Trends:")
140
+ fid_arrow = "^" if fid_last > fid_first else "v" if fid_last < fid_first else "="
141
+ comp_arrow = "^" if comp_last > comp_first else "v" if comp_last < comp_first else "="
142
+ print(f" Fidelity: {fid_first:.0%} -> {fid_last:.0%} [{fid_arrow}]")
143
+ print(f" Compression: {comp_first:.1f}x -> {comp_last:.1f}x [{comp_arrow}]")
144
+
145
+
146
+ def _show_drift_flags(store: TelemetryStore):
147
+ """Display unresolved documentation drift flags."""
148
+ flags = store.get_drift_flags(resolved=False)
149
+
150
+ if not flags:
151
+ print(" No unresolved drift flags. Documentation is in sync.")
152
+ return
153
+
154
+ print(f"\nDocumentation Drift ({len(flags)} unresolved)")
155
+ print("=" * 70)
156
+
157
+ for flag in flags:
158
+ severity = flag.get("severity", "?")
159
+ file_path = flag.get("file", "?")
160
+ issue = flag.get("issue", "?")
161
+ fixable = "auto-fixable" if flag.get("auto_fixable") else "manual"
162
+ suggested = flag.get("suggested_fix", "")
163
+
164
+ sev_indicator = {"blocker": "!!", "high": "!", "medium": "~", "low": "."}
165
+ indicator = sev_indicator.get(severity, "?")
166
+
167
+ print(f"\n [{indicator}] {severity.upper():8} {file_path}")
168
+ print(f" Issue: {issue}")
169
+ print(f" Fix: {suggested} ({fixable})")
170
+
171
+
172
+ def format_metrics_table(records: list[dict]) -> str:
173
+ """Format records as a readable table."""
174
+ if not records:
175
+ return " No records found."
176
+ lines = []
177
+ lines.append(f" {'Tool':<18} {'Repo':<25} {'Score':>5} {'Tokens':>6} {'Iter':>4} {'Time'}")
178
+ lines.append(" " + "-" * 75)
179
+ for r in records:
180
+ ts = time.strftime("%Y-%m-%d %H:%M", time.localtime(r.get("timestamp", 0)))
181
+ lines.append(
182
+ f" {r.get('tool', '?'):<18} {r.get('repo', '?'):<25} "
183
+ f"{r.get('output_quality', 0):>5} {r.get('context_tokens', 0):>6} "
184
+ f"{r.get('iterations', 0):>4} {ts}"
185
+ )
186
+ return "\n".join(lines)
@@ -0,0 +1,20 @@
1
+ """Prompt templates for agent invocation."""
2
+
3
+ EXTRACT_PROMPT = """\
4
+ Extract the architecture of the repository at: {repo_path}
5
+
6
+ Focus: {focus}
7
+ Token budget: {budget}
8
+ Target validation score: {target_score}+
9
+
10
+ Use the architect_scan, architect_slice, architect_validate, and architect_extract tools to complete the extraction. Output the final YAML model between ```yaml fences.
11
+ """
12
+
13
+ GENERATE_PROMPT = """\
14
+ Generate code for the repository at: {repo_path}
15
+
16
+ Use architect_scan and architect_slice to understand the architecture.
17
+ Then generate code that passes the test suite.
18
+ Use architect_generate to run tests and verify.
19
+ Iterate on failures (max {max_iter} attempts).
20
+ """