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.
- opencode_arch/__init__.py +3 -0
- opencode_arch/artifacts/__init__.py +48 -0
- opencode_arch/artifacts/context.py +451 -0
- opencode_arch/artifacts/diagrams.py +451 -0
- opencode_arch/artifacts/selector.py +331 -0
- opencode_arch/artifacts/templates.py +444 -0
- opencode_arch/cli/__init__.py +1 -0
- opencode_arch/cli/bench.py +25 -0
- opencode_arch/cli/calibrate.py +208 -0
- opencode_arch/cli/confidence.py +66 -0
- opencode_arch/cli/docs.py +333 -0
- opencode_arch/cli/docs_validator.py +295 -0
- opencode_arch/cli/export_data.py +133 -0
- opencode_arch/cli/extract.py +93 -0
- opencode_arch/cli/gap_analyzer.py +107 -0
- opencode_arch/cli/generate.py +68 -0
- opencode_arch/cli/launch.py +264 -0
- opencode_arch/cli/main.py +360 -0
- opencode_arch/cli/metrics.py +186 -0
- opencode_arch/cli/prompts.py +20 -0
- opencode_arch/cli/regen_loop.py +1028 -0
- opencode_arch/context/__init__.py +29 -0
- opencode_arch/context/formatter.py +492 -0
- opencode_arch/context/pipeline_bridge.py +201 -0
- opencode_arch/extract/__init__.py +8 -0
- opencode_arch/extract/constraint_detector.py +398 -0
- opencode_arch/extract/from_artifacts.py +837 -0
- opencode_arch/extract/from_code.py +646 -0
- opencode_arch/extract/route_detector.py +400 -0
- opencode_arch/extract/table_parser.py +177 -0
- opencode_arch/learning/__init__.py +19 -0
- opencode_arch/learning/adapter.py +157 -0
- opencode_arch/learning/assessor.py +170 -0
- opencode_arch/learning/classifier.py +144 -0
- opencode_arch/learning/lessons.py +139 -0
- opencode_arch/learning/maintainer.py +281 -0
- opencode_arch/learning/patterns.py +51 -0
- opencode_arch/mcp/__init__.py +1 -0
- opencode_arch/mcp/__main__.py +8 -0
- opencode_arch/mcp/server.py +183 -0
- opencode_arch/mcp/tools/__init__.py +1 -0
- opencode_arch/mcp/tools/check.py +159 -0
- opencode_arch/mcp/tools/extract.py +107 -0
- opencode_arch/mcp/tools/feedback.py +65 -0
- opencode_arch/mcp/tools/generate.py +104 -0
- opencode_arch/mcp/tools/group.py +62 -0
- opencode_arch/mcp/tools/ingest.py +101 -0
- opencode_arch/mcp/tools/require.py +77 -0
- opencode_arch/mcp/tools/scan.py +53 -0
- opencode_arch/mcp/tools/slice.py +235 -0
- opencode_arch/mcp/tools/validate.py +59 -0
- opencode_arch/prompts/__init__.py +1 -0
- opencode_arch/prompts/regen.py +36 -0
- opencode_arch/runner/__init__.py +5 -0
- opencode_arch/runner/base.py +21 -0
- opencode_arch/runner/opencode.py +66 -0
- opencode_arch/telemetry/__init__.py +6 -0
- opencode_arch/telemetry/collector.py +40 -0
- opencode_arch/telemetry/recorder.py +12 -0
- opencode_arch/telemetry/store.py +537 -0
- opencode_arch-1.0.0.dist-info/METADATA +247 -0
- opencode_arch-1.0.0.dist-info/RECORD +65 -0
- opencode_arch-1.0.0.dist-info/WHEEL +4 -0
- opencode_arch-1.0.0.dist-info/entry_points.txt +2 -0
- 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
|
+
"""
|