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,107 @@
1
+ """architect_extract MCP tool — validate and store an architecture extraction."""
2
+ from __future__ import annotations
3
+
4
+ from pathlib import Path
5
+ from typing import Any
6
+
7
+ import yaml
8
+
9
+
10
+ async def store_extraction(
11
+ repo_path: str,
12
+ model_yaml: str,
13
+ context_tokens: int = 0,
14
+ ) -> dict[str, Any]:
15
+ """Validate and store an architecture model extraction.
16
+
17
+ Called AFTER the agent has produced a YAML architecture model.
18
+ Validates the model, writes it to .architecture-model.yaml,
19
+ and records telemetry.
20
+
21
+ Args:
22
+ repo_path: Path to the repository root (where to save the model).
23
+ model_yaml: The YAML architecture model produced by the agent.
24
+ context_tokens: How many tokens of context the agent used (for telemetry).
25
+
26
+ Returns:
27
+ Dict with: stored (bool), score (int), issues (list), telemetry_recorded (bool).
28
+ """
29
+ path = Path(repo_path)
30
+
31
+ try:
32
+ # Parse the YAML
33
+ raw = yaml.safe_load(model_yaml)
34
+ if not isinstance(raw, dict):
35
+ return {"stored": False, "error": "YAML did not parse to a dict", "score": 0}
36
+
37
+ # Validate using architecture_model
38
+ from architecture_model.core.parser import _parse_raw
39
+ from architecture_model.core.validator import validate_model
40
+
41
+ model = _parse_raw(raw)
42
+ validation = validate_model(model)
43
+
44
+ score = validation.score
45
+ issues = [str(issue) for issue in validation.issues]
46
+
47
+ # Write to repo
48
+ output_path = path / ".architecture-model.yaml"
49
+ output_path.write_text(model_yaml)
50
+
51
+ # Record telemetry
52
+ telemetry_recorded = False
53
+ try:
54
+ from opencode_arch.telemetry.store import TelemetryStore
55
+ store = TelemetryStore()
56
+ store.record(
57
+ tool="architect_extract",
58
+ repo=str(path.name),
59
+ context_tokens=context_tokens,
60
+ output_quality=score,
61
+ iterations=1,
62
+ )
63
+ telemetry_recorded = True
64
+ except Exception:
65
+ pass # Telemetry failure shouldn't block the tool
66
+
67
+ try:
68
+ from opencode_arch.telemetry.collector import drain_and_store
69
+ drain_and_store(tool="architect_extract", repo=path.name)
70
+ except Exception:
71
+ pass
72
+
73
+ # After the model is stored successfully, persist full project snapshot
74
+ try:
75
+ from architecture_model.manifest.generator import generate_manifest
76
+ from architecture_model.persistence.store import save_project
77
+
78
+ manifest = generate_manifest(Path(repo_path))
79
+
80
+ # Try to compute representativeness
81
+ rep = None
82
+ try:
83
+ from architecture_model.core.representativeness import compute_representativeness
84
+ rep = compute_representativeness(model, manifest)
85
+ except Exception:
86
+ pass
87
+
88
+ save_project(
89
+ Path(repo_path), model, manifest,
90
+ representativeness=rep,
91
+ telemetry={"context_tokens": context_tokens} if context_tokens else None,
92
+ )
93
+ except Exception:
94
+ pass # Persistence failures should never block tool operation
95
+
96
+ return {
97
+ "stored": True,
98
+ "score": score,
99
+ "issues": issues,
100
+ "path": str(output_path),
101
+ "telemetry_recorded": telemetry_recorded,
102
+ }
103
+
104
+ except yaml.YAMLError as e:
105
+ return {"stored": False, "error": f"Invalid YAML: {e}", "score": 0}
106
+ except Exception as e:
107
+ return {"stored": False, "error": f"Validation failed: {e}", "score": 0}
@@ -0,0 +1,65 @@
1
+ """Record user feedback for model improvement and training."""
2
+ from __future__ import annotations
3
+
4
+ import json
5
+ from datetime import datetime, timezone
6
+ from pathlib import Path
7
+ from typing import Any
8
+
9
+
10
+ async def record_feedback(
11
+ repo_path: str,
12
+ feedback_type: str,
13
+ content: str,
14
+ context: dict | None = None,
15
+ rating: int | None = None,
16
+ correction: dict | None = None,
17
+ ) -> dict[str, Any]:
18
+ """Record user feedback to .architecture/feedback.jsonl.
19
+
20
+ Args:
21
+ repo_path: Repository root path
22
+ feedback_type: "correction" | "rating" | "tool_feedback" | "training"
23
+ content: The feedback content (human-readable)
24
+ context: Optional context dict (tool name, prompt, response, etc.)
25
+ rating: Optional 1-5 quality rating
26
+ correction: Optional structured correction {entity_id, field, old, new}
27
+
28
+ Returns:
29
+ {recorded, feedback_id, total_feedback}
30
+ """
31
+ repo = Path(repo_path)
32
+ arch_dir = repo / ".architecture"
33
+ arch_dir.mkdir(parents=True, exist_ok=True)
34
+ feedback_path = arch_dir / "feedback.jsonl"
35
+
36
+ # Count existing entries to generate ID
37
+ existing_count = 0
38
+ if feedback_path.exists():
39
+ existing_count = sum(1 for line in feedback_path.read_text().splitlines() if line.strip())
40
+
41
+ feedback_id = f"FB-{existing_count + 1}"
42
+
43
+ # Build entry
44
+ entry: dict[str, Any] = {
45
+ "id": feedback_id,
46
+ "type": feedback_type,
47
+ "ts": datetime.now(timezone.utc).isoformat(),
48
+ "content": content,
49
+ }
50
+ if context:
51
+ entry["context"] = context
52
+ if rating is not None:
53
+ entry["rating"] = rating
54
+ if correction:
55
+ entry["correction"] = correction
56
+
57
+ # Append to JSONL
58
+ with feedback_path.open("a") as f:
59
+ f.write(json.dumps(entry, default=str) + "\n")
60
+
61
+ return {
62
+ "recorded": True,
63
+ "feedback_id": feedback_id,
64
+ "total_feedback": existing_count + 1,
65
+ }
@@ -0,0 +1,104 @@
1
+ # src/opencode_arch/mcp/tools/generate.py
2
+ """architect_generate MCP tool — run tests on generated code."""
3
+ from __future__ import annotations
4
+
5
+ import json
6
+ import subprocess
7
+ import sys
8
+ from pathlib import Path
9
+ from typing import Any
10
+
11
+
12
+ async def run_tests_on_generated_code(
13
+ repo_path: str,
14
+ test_command: str | None = None,
15
+ ) -> dict[str, Any]:
16
+ """Run the repository's test suite against generated code.
17
+
18
+ This is the quality gate for code generation. The agent generates code,
19
+ then calls this tool to verify it passes the original tests.
20
+
21
+ Args:
22
+ repo_path: Path to the repository with generated code + tests.
23
+ test_command: Custom test command. Defaults to pytest.
24
+
25
+ Returns:
26
+ Dict with: passed (bool), pass_rate (float), total_tests (int),
27
+ passed_tests (int), failures (list of failure descriptions).
28
+ """
29
+ path = Path(repo_path)
30
+ if not path.exists():
31
+ return {"error": f"Path does not exist: {repo_path}"}
32
+
33
+ try:
34
+ if test_command:
35
+ cmd = test_command.split()
36
+ else:
37
+ cmd = [sys.executable, "-m", "pytest", str(path), "-v", "--tb=short", "-q"]
38
+
39
+ result = subprocess.run(
40
+ cmd,
41
+ capture_output=True,
42
+ text=True,
43
+ timeout=120,
44
+ cwd=str(path),
45
+ )
46
+
47
+ # Parse pytest output
48
+ output = result.stdout + result.stderr
49
+ parsed = _parse_pytest_output(output, result.returncode)
50
+
51
+ try:
52
+ from opencode_arch.telemetry.collector import drain_and_store
53
+ drain_and_store(tool="architect_generate", repo=path.name)
54
+ except Exception:
55
+ pass
56
+
57
+ return parsed
58
+
59
+ except subprocess.TimeoutExpired:
60
+ return {"error": "Test execution timed out (120s)", "passed": False, "pass_rate": 0.0, "total_tests": 0}
61
+ except Exception as e:
62
+ return {"error": f"Test execution failed: {e}", "passed": False, "pass_rate": 0.0, "total_tests": 0}
63
+
64
+
65
+ def _parse_pytest_output(output: str, returncode: int) -> dict[str, Any]:
66
+ """Parse pytest output to extract pass/fail counts."""
67
+ total = 0
68
+ passed_count = 0
69
+ failures: list[str] = []
70
+
71
+ for line in output.split("\n"):
72
+ # Look for summary line: "5 passed, 2 failed in 0.5s"
73
+ if "passed" in line or "failed" in line or "error" in line:
74
+ parts = line.strip().split()
75
+ for i, part in enumerate(parts):
76
+ if part == "passed" and i > 0:
77
+ try:
78
+ passed_count = int(parts[i - 1])
79
+ except ValueError:
80
+ pass
81
+ elif part == "failed" and i > 0:
82
+ try:
83
+ total += int(parts[i - 1])
84
+ except ValueError:
85
+ pass
86
+
87
+ # Capture FAILED test names
88
+ if line.startswith("FAILED"):
89
+ failures.append(line.strip())
90
+
91
+ total += passed_count
92
+
93
+ if total == 0 and "no tests ran" in output.lower():
94
+ return {"passed": True, "pass_rate": 0.0, "total_tests": 0, "passed_tests": 0, "failures": []}
95
+
96
+ pass_rate = passed_count / total if total > 0 else 0.0
97
+
98
+ return {
99
+ "passed": returncode == 0,
100
+ "pass_rate": pass_rate,
101
+ "total_tests": total,
102
+ "passed_tests": passed_count,
103
+ "failures": failures,
104
+ }
@@ -0,0 +1,62 @@
1
+ """architect_group MCP tool — group repository modules into logical components."""
2
+ from __future__ import annotations
3
+
4
+ from pathlib import Path
5
+ from typing import Any
6
+
7
+
8
+ async def group_repository(repo_path: str, target_groups: int = 0) -> dict[str, Any]:
9
+ """Group repository modules into logical architecture components.
10
+
11
+ Uses multi-signal affinity (subdirectory, name-prefix, imports) to
12
+ cluster source files into coherent component groups. This provides
13
+ suggested component boundaries for architecture extraction.
14
+
15
+ Args:
16
+ repo_path: Absolute path to the repository root.
17
+ target_groups: Desired number of groups. 0 = auto-calculate.
18
+
19
+ Returns:
20
+ Dict with keys: groups, total_modules, total_groups, filtered_trivial.
21
+ Each group has: name, files, file_count, locked.
22
+ Returns {"error": "..."} on failure.
23
+ """
24
+ path = Path(repo_path)
25
+ if not path.exists():
26
+ return {"error": f"Repository path does not exist: {repo_path}"}
27
+
28
+ try:
29
+ from architecture_model.manifest.generator import generate_manifest
30
+ from architecture_model.manifest.grouping import group_modules
31
+
32
+ manifest = generate_manifest(path)
33
+ target = target_groups if target_groups > 0 else None
34
+ groups = group_modules(
35
+ manifest.modules,
36
+ manifest.interfaces,
37
+ target_groups=target,
38
+ )
39
+
40
+ result_groups = []
41
+ for g in groups:
42
+ result_groups.append({
43
+ "name": g.name,
44
+ "files": g.modules,
45
+ "file_count": len(g.modules),
46
+ "primary_file": g.primary_file,
47
+ })
48
+
49
+ try:
50
+ from opencode_arch.telemetry.collector import drain_and_store
51
+ drain_and_store(tool="architect_group", repo=path.name)
52
+ except Exception:
53
+ pass
54
+
55
+ return {
56
+ "groups": result_groups,
57
+ "total_modules": len(manifest.modules),
58
+ "total_groups": len(groups),
59
+ }
60
+
61
+ except Exception as e:
62
+ return {"error": f"Grouping failed: {e}"}
@@ -0,0 +1,101 @@
1
+ """architect_ingest tool — accept a SourceGraph JSON for any language.
2
+
3
+ Allows agents or external tools to submit dependency/export data for
4
+ non-Python repos. Stores the graph and runs grouping + interface extraction.
5
+ """
6
+ from __future__ import annotations
7
+
8
+ import json
9
+ from pathlib import Path
10
+
11
+
12
+ async def ingest_source_graph(
13
+ repo_path: str,
14
+ source_graph_json: str,
15
+ ) -> dict:
16
+ """Ingest a SourceGraph JSON and generate architecture components.
17
+
18
+ Args:
19
+ repo_path: Absolute path to the repository.
20
+ source_graph_json: JSON string with SourceGraph data.
21
+
22
+ Returns:
23
+ Dict with components, interfaces, and group info.
24
+ """
25
+ try:
26
+ from architecture_model.manifest.protocol import SourceGraph
27
+ from architecture_model.manifest.grouping import group_source_graph, auto_fblocks
28
+ from architecture_model.core.types import (
29
+ ArchitectureModel, Component, Entities, ModelMeta,
30
+ )
31
+ from architecture_model.orchestration.auto_enrich import extract_component_interfaces
32
+ from architecture_model.core.parser import save_model
33
+
34
+ project_root = Path(repo_path)
35
+
36
+ # Parse the source graph
37
+ data = json.loads(source_graph_json)
38
+ graph = SourceGraph.from_json(data)
39
+ graph.root = repo_path
40
+
41
+ # Group into components
42
+ groups = group_source_graph(graph)
43
+ if not groups:
44
+ return {"error": "No non-trivial source units found", "units": len(graph.units)}
45
+
46
+ # Create components from groups
47
+ components: list[Component] = []
48
+ for idx, group in enumerate(groups, 1):
49
+ components.append(Component(
50
+ id=f"COMP-{idx}",
51
+ name=group.name,
52
+ status="ACTIVE",
53
+ files=group.modules,
54
+ ))
55
+
56
+ # Build model
57
+ model = ArchitectureModel(
58
+ meta=ModelMeta(project=project_root.name, schema_version="1.3"),
59
+ entities=Entities(components=components),
60
+ relationships=[],
61
+ )
62
+
63
+ # Extract interface contracts
64
+ n_ifaces = extract_component_interfaces(model, graph)
65
+
66
+ # Enrich components from SourceGraph (signatures, symbols, contracts, patterns)
67
+ from architecture_model.orchestration.auto_enrich import enrich_from_source_graph
68
+ enrich_from_source_graph(model, graph)
69
+
70
+ # Generate F-block config
71
+ fblock_config = auto_fblocks(groups, threshold=3)
72
+
73
+ # Save the model
74
+ model_path = project_root / ".architecture-model-extracted.yaml"
75
+ save_model(model, model_path)
76
+
77
+ # Also save the source graph for later use
78
+ graph_path = project_root / ".architecture-models" / "source-graph.json"
79
+ graph_path.parent.mkdir(parents=True, exist_ok=True)
80
+ graph_path.write_text(json.dumps(graph.to_json(), indent=2))
81
+
82
+ return {
83
+ "stored": True,
84
+ "model_path": str(model_path),
85
+ "graph_path": str(graph_path),
86
+ "components": len(components),
87
+ "interfaces": n_ifaces,
88
+ "fblocks": len(fblock_config),
89
+ "units": len(graph.units),
90
+ "edges": len(graph.edges),
91
+ "language": graph.language,
92
+ "groups": [
93
+ {"name": g.name, "files": g.modules, "file_count": len(g.modules)}
94
+ for g in groups
95
+ ],
96
+ }
97
+
98
+ except json.JSONDecodeError as e:
99
+ return {"error": f"Invalid JSON: {e}"}
100
+ except Exception as e:
101
+ return {"error": str(e)}
@@ -0,0 +1,77 @@
1
+ """Capture functional requirements against architecture components."""
2
+ from __future__ import annotations
3
+
4
+ import yaml
5
+ from datetime import datetime, timezone
6
+ from pathlib import Path
7
+ from typing import Any
8
+
9
+
10
+ async def capture_requirement(
11
+ repo_path: str,
12
+ requirement: str,
13
+ component_id: str | None = None,
14
+ priority: str = "must",
15
+ context: str = "",
16
+ ) -> dict[str, Any]:
17
+ """Store a functional requirement linked to a component.
18
+
19
+ Args:
20
+ repo_path: Repository root path
21
+ requirement: The requirement text
22
+ component_id: Component ID (e.g., "COMP-3"). If None, stored as unlinked.
23
+ priority: must | should | could (MoSCoW)
24
+ context: Additional context from conversation
25
+
26
+ Returns:
27
+ {stored, requirement_id, component, total_requirements}
28
+ """
29
+ repo = Path(repo_path)
30
+ arch_dir = repo / ".architecture"
31
+ arch_dir.mkdir(parents=True, exist_ok=True)
32
+ req_path = arch_dir / "requirements.yaml"
33
+
34
+ # Load existing
35
+ if req_path.exists():
36
+ data = yaml.safe_load(req_path.read_text()) or {}
37
+ else:
38
+ data = {}
39
+
40
+ reqs = data.get("requirements", [])
41
+
42
+ # Generate next ID
43
+ existing_ids = [r.get("id", "") for r in reqs]
44
+ max_num = 0
45
+ for rid in existing_ids:
46
+ if rid.startswith("REQ-"):
47
+ try:
48
+ max_num = max(max_num, int(rid[4:]))
49
+ except ValueError:
50
+ pass
51
+ next_id = f"REQ-{max_num + 1}"
52
+
53
+ # Build requirement entry
54
+ entry = {
55
+ "id": next_id,
56
+ "text": requirement,
57
+ "priority": priority,
58
+ "status": "proposed",
59
+ "created_at": datetime.now(timezone.utc).isoformat(),
60
+ }
61
+ if component_id:
62
+ entry["component"] = component_id
63
+ if context:
64
+ entry["context"] = context
65
+
66
+ reqs.append(entry)
67
+ data["requirements"] = reqs
68
+
69
+ # Write back
70
+ req_path.write_text(yaml.dump(data, default_flow_style=False, sort_keys=False))
71
+
72
+ return {
73
+ "stored": True,
74
+ "requirement_id": next_id,
75
+ "component": component_id or "unlinked",
76
+ "total_requirements": len(reqs),
77
+ }
@@ -0,0 +1,53 @@
1
+ """architect_scan MCP tool — generate reality manifest via AST scanning."""
2
+ from __future__ import annotations
3
+
4
+ from pathlib import Path
5
+ from typing import Any
6
+
7
+
8
+ async def scan_repository(repo_path: str) -> dict[str, Any]:
9
+ """Scan a repository and generate its reality manifest.
10
+
11
+ Performs AST analysis on all source files to produce a ground-truth
12
+ inventory of modules, functions, classes, imports, and metrics.
13
+
14
+ Args:
15
+ repo_path: Absolute path to the repository root.
16
+
17
+ Returns:
18
+ Manifest dict with keys: generated_at, project_root, metrics,
19
+ functional_blocks, modules, interfaces.
20
+ Returns {"error": "..."} on failure.
21
+ """
22
+ path = Path(repo_path)
23
+ if not path.exists():
24
+ return {"error": f"Repository path does not exist: {repo_path}"}
25
+
26
+ try:
27
+ from architecture_model.manifest.generator import generate_manifest
28
+ manifest = generate_manifest(path)
29
+
30
+ # Include suggested component groupings
31
+ suggested = None
32
+ try:
33
+ from architecture_model.manifest.grouping import group_modules
34
+ groups = group_modules(manifest.modules, manifest.interfaces)
35
+ suggested = [
36
+ {"name": g.name, "files": g.modules, "file_count": len(g.modules)}
37
+ for g in groups
38
+ ]
39
+ except Exception:
40
+ pass
41
+
42
+ try:
43
+ from opencode_arch.telemetry.collector import drain_and_store
44
+ drain_and_store(tool="architect_scan", repo=path.name)
45
+ except Exception:
46
+ pass
47
+
48
+ result = manifest
49
+ if suggested and isinstance(result, dict):
50
+ result["suggested_components"] = suggested
51
+ return result
52
+ except Exception as e:
53
+ return {"error": f"Scan failed: {e}"}