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,331 @@
1
+ """Model-driven artifact selector.
2
+
3
+ Determines which SE documentation artifacts are appropriate to generate
4
+ based on the richness of an ArchitectureModel. Pure function — no I/O, no LLM calls.
5
+ """
6
+
7
+ from __future__ import annotations
8
+
9
+ from dataclasses import dataclass, field
10
+
11
+ from architecture_model.core.types import ArchitectureModel
12
+
13
+
14
+ @dataclass
15
+ class ArtifactSpec:
16
+ """Specification for a generatable SE documentation artifact."""
17
+
18
+ id: str # e.g. "api-reference"
19
+ name: str # e.g. "API Reference"
20
+ category: str # "architecture" | "design" | "operations" | "requirements"
21
+ requires: list[str] # what model content is needed
22
+ priority: int # 1=always if data exists, 2=recommended, 3=optional
23
+
24
+
25
+ # ---------------------------------------------------------------------------
26
+ # Artifact Registry — the 12 supported artifacts
27
+ # ---------------------------------------------------------------------------
28
+
29
+ ARTIFACT_REGISTRY: list[ArtifactSpec] = [
30
+ ArtifactSpec(
31
+ id="system-overview",
32
+ name="System Overview",
33
+ category="architecture",
34
+ requires=["components"],
35
+ priority=1,
36
+ ),
37
+ ArtifactSpec(
38
+ id="component-catalog",
39
+ name="Component Catalog",
40
+ category="architecture",
41
+ requires=["components"],
42
+ priority=1,
43
+ ),
44
+ ArtifactSpec(
45
+ id="api-reference",
46
+ name="API Reference",
47
+ category="design",
48
+ requires=["interfaces"],
49
+ priority=1,
50
+ ),
51
+ ArtifactSpec(
52
+ id="capability-map",
53
+ name="Capability Map",
54
+ category="requirements",
55
+ requires=["capabilities"],
56
+ priority=1,
57
+ ),
58
+ ArtifactSpec(
59
+ id="behavior-flows",
60
+ name="Behavior Flows",
61
+ category="design",
62
+ requires=["behaviors"],
63
+ priority=2,
64
+ ),
65
+ ArtifactSpec(
66
+ id="constraint-register",
67
+ name="Constraint Register",
68
+ category="requirements",
69
+ requires=["constraints"],
70
+ priority=2,
71
+ ),
72
+ ArtifactSpec(
73
+ id="dependency-graph",
74
+ name="Dependency Graph",
75
+ category="architecture",
76
+ requires=["components", "relationships"],
77
+ priority=2,
78
+ ),
79
+ ArtifactSpec(
80
+ id="layer-architecture",
81
+ name="Layer Architecture",
82
+ category="architecture",
83
+ requires=["layers"],
84
+ priority=2,
85
+ ),
86
+ ArtifactSpec(
87
+ id="deployment-view",
88
+ name="Deployment View",
89
+ category="operations",
90
+ requires=["components", "layers"],
91
+ priority=2,
92
+ ),
93
+ ArtifactSpec(
94
+ id="integration-guide",
95
+ name="Integration Guide",
96
+ category="design",
97
+ requires=["interfaces", "components"],
98
+ priority=3,
99
+ ),
100
+ ArtifactSpec(
101
+ id="test-strategy",
102
+ name="Test Strategy",
103
+ category="operations",
104
+ requires=["manifest.tests"],
105
+ priority=3,
106
+ ),
107
+ ArtifactSpec(
108
+ id="metrics-dashboard",
109
+ name="Metrics Dashboard",
110
+ category="operations",
111
+ requires=["manifest.metrics"],
112
+ priority=3,
113
+ ),
114
+ ]
115
+
116
+
117
+ def _requirement_met(req: str, model: ArchitectureModel, manifest: dict | None) -> bool:
118
+ """Check whether a single requirement string is satisfied."""
119
+ if req == "components":
120
+ return len(model.entities.components) > 0
121
+ elif req == "interfaces":
122
+ return len(model.entities.interfaces) > 0
123
+ elif req == "capabilities":
124
+ return len(model.entities.capabilities) > 0
125
+ elif req == "behaviors":
126
+ return len(model.entities.behaviors) > 0
127
+ elif req == "constraints":
128
+ return len(model.entities.constraints) > 0
129
+ elif req == "layers":
130
+ return len(model.entities.layers) > 0
131
+ elif req == "relationships":
132
+ return len(model.relationships) > 0
133
+ elif req == "manifest.tests":
134
+ if manifest is None:
135
+ return False
136
+ tests = manifest.get("test_files") or manifest.get("tests")
137
+ return bool(tests)
138
+ elif req == "manifest.metrics":
139
+ if manifest is None:
140
+ return False
141
+ return bool(manifest.get("metrics"))
142
+ else:
143
+ return False
144
+
145
+
146
+ def select_artifacts(
147
+ model: ArchitectureModel,
148
+ manifest: dict | None = None,
149
+ include_capability_details: bool = True,
150
+ ) -> list[ArtifactSpec]:
151
+ """Return artifacts appropriate for this model's richness.
152
+
153
+ For each artifact in the registry, check if the model has the required entities.
154
+ Return only those artifacts whose requirements are met.
155
+ Results sorted by priority (1 first), then alphabetically by id.
156
+
157
+ If include_capability_details is True, also generates per-capability
158
+ api-detail-{cap_id} artifacts for every realized capability.
159
+ """
160
+ selected: list[ArtifactSpec] = []
161
+
162
+ for spec in ARTIFACT_REGISTRY:
163
+ if all(_requirement_met(req, model, manifest) for req in spec.requires):
164
+ selected.append(spec)
165
+
166
+ # Add per-capability detail artifacts
167
+ if include_capability_details:
168
+ selected.extend(select_capability_detail_artifacts(model))
169
+
170
+ selected.sort(key=lambda s: (s.priority, s.id))
171
+ return selected
172
+
173
+
174
+ def get_artifact_spec(artifact_id: str) -> ArtifactSpec | None:
175
+ """Look up a single artifact spec by ID."""
176
+ for spec in ARTIFACT_REGISTRY:
177
+ if spec.id == artifact_id:
178
+ return spec
179
+ return None
180
+
181
+
182
+ # ---------------------------------------------------------------------------
183
+ # Subsystem Decomposition Support
184
+ # ---------------------------------------------------------------------------
185
+
186
+
187
+ @dataclass
188
+ class SubsystemInfo:
189
+ """Description of a subsystem within a larger system."""
190
+
191
+ id: str # e.g. "F1", "core", "cli"
192
+ name: str # e.g. "Core Engine", "CLI Commands"
193
+ components: list[str] # component IDs belonging to this subsystem
194
+ file_count: int = 0 # number of source files
195
+ test_count: int = 0 # number of test files
196
+
197
+
198
+ def should_decompose(model: ArchitectureModel, manifest: dict | None = None) -> bool:
199
+ """Determine if system is complex enough to warrant per-subsystem docs.
200
+
201
+ Heuristic: returns True if ANY of:
202
+ - More than 5 functional blocks (distinct f_block values on components)
203
+ - More than 50 source files in manifest
204
+ - More than 20 components
205
+ """
206
+ # Check component count
207
+ if len(model.entities.components) > 20:
208
+ return True
209
+
210
+ # Check distinct f_block values
211
+ fblocks = {c.f_block for c in model.entities.components if c.f_block}
212
+ if len(fblocks) > 5:
213
+ return True
214
+
215
+ # Check file count from manifest
216
+ if manifest is not None:
217
+ # Try metrics.total_files first
218
+ total_files = manifest.get("metrics", {}).get("total_files", 0)
219
+ if total_files > 50:
220
+ return True
221
+ # Fall back to counting modules list
222
+ modules = manifest.get("modules")
223
+ if modules and len(modules) > 50:
224
+ return True
225
+
226
+ return False
227
+
228
+
229
+ # Artifacts appropriate for subsystem-level generation
230
+ SUBSYSTEM_ARTIFACTS: list[str] = [
231
+ "component-catalog",
232
+ "api-reference",
233
+ "behavior-flows",
234
+ "test-strategy",
235
+ ]
236
+
237
+
238
+ def _subsystem_requirement_met(
239
+ req: str,
240
+ subsystem: SubsystemInfo,
241
+ model: ArchitectureModel,
242
+ manifest: dict | None,
243
+ ) -> bool:
244
+ """Check whether a requirement is met scoped to a subsystem's components."""
245
+ if req == "components":
246
+ return len(subsystem.components) > 0
247
+ elif req == "interfaces":
248
+ comp_set = set(subsystem.components)
249
+ return any(
250
+ i.provider in comp_set or i.consumer in comp_set
251
+ for i in model.entities.interfaces
252
+ )
253
+ elif req == "behaviors":
254
+ comp_set = set(subsystem.components)
255
+ return any(b.actor in comp_set for b in model.entities.behaviors)
256
+ elif req == "manifest.tests":
257
+ return subsystem.test_count > 0
258
+ else:
259
+ # Fall back to system-level logic for other requirements
260
+ return _requirement_met(req, model, manifest)
261
+
262
+
263
+ def select_subsystem_artifacts(
264
+ subsystem: SubsystemInfo,
265
+ model: ArchitectureModel,
266
+ manifest: dict | None = None,
267
+ ) -> list[ArtifactSpec]:
268
+ """Select artifacts appropriate for a subsystem (subset of system-level).
269
+
270
+ Only returns artifacts from SUBSYSTEM_ARTIFACTS that:
271
+ 1. Are in the SUBSYSTEM_ARTIFACTS list
272
+ 2. Have their requirements met (scoped to subsystem's components)
273
+
274
+ Returns empty list if subsystem has no components.
275
+ """
276
+ if not subsystem.components:
277
+ return []
278
+
279
+ selected: list[ArtifactSpec] = []
280
+
281
+ for spec in ARTIFACT_REGISTRY:
282
+ if spec.id not in SUBSYSTEM_ARTIFACTS:
283
+ continue
284
+ if all(
285
+ _subsystem_requirement_met(req, subsystem, model, manifest)
286
+ for req in spec.requires
287
+ ):
288
+ selected.append(spec)
289
+
290
+ selected.sort(key=lambda s: (s.priority, s.id))
291
+ return selected
292
+
293
+
294
+ # ---------------------------------------------------------------------------
295
+ # Per-Capability API Detail Artifacts
296
+ # ---------------------------------------------------------------------------
297
+
298
+
299
+ def select_capability_detail_artifacts(
300
+ model: ArchitectureModel,
301
+ ) -> list[ArtifactSpec]:
302
+ """Generate one ArtifactSpec per capability for detailed API docs.
303
+
304
+ Only includes capabilities that have at least one realizing component
305
+ (via 'realizes' relationship). Each gets artifact_id = "api-detail-{cap.id}".
306
+ """
307
+ from architecture_model.core.types import RelationType
308
+
309
+ # Find which capabilities have realizing components
310
+ realized_caps: set[str] = set()
311
+ for rel in model.relationships:
312
+ if rel.type == RelationType.REALIZES:
313
+ realized_caps.add(rel.to_id)
314
+
315
+ cap_ids = {c.id for c in model.entities.capabilities}
316
+ realized_caps = realized_caps & cap_ids # only actual capability targets
317
+
318
+ specs: list[ArtifactSpec] = []
319
+ for cap in model.entities.capabilities:
320
+ if cap.id not in realized_caps:
321
+ continue
322
+ specs.append(ArtifactSpec(
323
+ id=f"api-detail-{cap.id.lower()}",
324
+ name=f"API Detail — {cap.name}",
325
+ category="design",
326
+ requires=["capabilities"],
327
+ priority=2,
328
+ ))
329
+
330
+ specs.sort(key=lambda s: s.id)
331
+ return specs