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,48 @@
|
|
|
1
|
+
"""Artifact selection and registry for model-driven SE document generation."""
|
|
2
|
+
|
|
3
|
+
from opencode_arch.artifacts.selector import (
|
|
4
|
+
ArtifactSpec,
|
|
5
|
+
ARTIFACT_REGISTRY,
|
|
6
|
+
SUBSYSTEM_ARTIFACTS,
|
|
7
|
+
SubsystemInfo,
|
|
8
|
+
get_artifact_spec,
|
|
9
|
+
select_artifacts,
|
|
10
|
+
select_capability_detail_artifacts,
|
|
11
|
+
select_subsystem_artifacts,
|
|
12
|
+
should_decompose,
|
|
13
|
+
)
|
|
14
|
+
from opencode_arch.artifacts.templates import ArtifactTemplate, TemplateSection, TEMPLATES, get_template, API_DETAIL_TEMPLATE
|
|
15
|
+
from opencode_arch.artifacts.context import assemble_artifact_context, format_capability_detail_context
|
|
16
|
+
from opencode_arch.artifacts.diagrams import (
|
|
17
|
+
generate_all_diagrams,
|
|
18
|
+
generate_component_diagram,
|
|
19
|
+
generate_dependency_diagram,
|
|
20
|
+
generate_focused_diagram,
|
|
21
|
+
generate_nav_diagram,
|
|
22
|
+
generate_sequence_diagram,
|
|
23
|
+
)
|
|
24
|
+
|
|
25
|
+
__all__ = [
|
|
26
|
+
"API_DETAIL_TEMPLATE",
|
|
27
|
+
"ArtifactSpec",
|
|
28
|
+
"ArtifactTemplate",
|
|
29
|
+
"ARTIFACT_REGISTRY",
|
|
30
|
+
"SUBSYSTEM_ARTIFACTS",
|
|
31
|
+
"SubsystemInfo",
|
|
32
|
+
"TEMPLATES",
|
|
33
|
+
"TemplateSection",
|
|
34
|
+
"assemble_artifact_context",
|
|
35
|
+
"format_capability_detail_context",
|
|
36
|
+
"generate_all_diagrams",
|
|
37
|
+
"generate_component_diagram",
|
|
38
|
+
"generate_dependency_diagram",
|
|
39
|
+
"generate_focused_diagram",
|
|
40
|
+
"generate_nav_diagram",
|
|
41
|
+
"generate_sequence_diagram",
|
|
42
|
+
"get_artifact_spec",
|
|
43
|
+
"get_template",
|
|
44
|
+
"select_artifacts",
|
|
45
|
+
"select_capability_detail_artifacts",
|
|
46
|
+
"select_subsystem_artifacts",
|
|
47
|
+
"should_decompose",
|
|
48
|
+
]
|
|
@@ -0,0 +1,451 @@
|
|
|
1
|
+
"""Context assembler for artifact generation.
|
|
2
|
+
|
|
3
|
+
Given an artifact template + architecture model + optional manifest,
|
|
4
|
+
assembles the formatted context string included in the LLM prompt.
|
|
5
|
+
|
|
6
|
+
Extracts relevant model/manifest data for each template section and
|
|
7
|
+
formats it concisely for LLM consumption.
|
|
8
|
+
"""
|
|
9
|
+
|
|
10
|
+
from __future__ import annotations
|
|
11
|
+
|
|
12
|
+
from opencode_arch.artifacts.diagrams import (
|
|
13
|
+
generate_component_diagram,
|
|
14
|
+
generate_dependency_diagram,
|
|
15
|
+
generate_sequence_diagram,
|
|
16
|
+
)
|
|
17
|
+
from opencode_arch.artifacts.templates import ArtifactTemplate
|
|
18
|
+
from architecture_model.core.types import ArchitectureModel
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
def assemble_artifact_context(
|
|
22
|
+
template: ArtifactTemplate,
|
|
23
|
+
model: ArchitectureModel,
|
|
24
|
+
manifest: dict | None = None,
|
|
25
|
+
max_tokens: int = 4000,
|
|
26
|
+
) -> str:
|
|
27
|
+
"""Assemble formatted context for artifact generation.
|
|
28
|
+
|
|
29
|
+
Returns a structured prompt string containing:
|
|
30
|
+
1. System prompt from template
|
|
31
|
+
2. For each section: heading + extracted data + instructions
|
|
32
|
+
|
|
33
|
+
Token budget is approximate (1 token ~ 4 chars). If total exceeds budget,
|
|
34
|
+
truncate section data (not instructions).
|
|
35
|
+
"""
|
|
36
|
+
max_chars = max_tokens * 4
|
|
37
|
+
|
|
38
|
+
# Build header
|
|
39
|
+
header = f"SYSTEM: {template.system_prompt}\n"
|
|
40
|
+
|
|
41
|
+
# Build sections
|
|
42
|
+
sections: list[str] = []
|
|
43
|
+
for section in template.sections:
|
|
44
|
+
data = _extract_section_data(section.source, model, manifest)
|
|
45
|
+
section_text = (
|
|
46
|
+
f"\n{section.heading}\n"
|
|
47
|
+
f"DATA:\n{data}\n"
|
|
48
|
+
f"INSTRUCTIONS: {section.instructions}\n"
|
|
49
|
+
)
|
|
50
|
+
sections.append(section_text)
|
|
51
|
+
|
|
52
|
+
# Assemble full output
|
|
53
|
+
full_output = header + "".join(sections)
|
|
54
|
+
|
|
55
|
+
# Truncate if over budget
|
|
56
|
+
if len(full_output) <= max_chars:
|
|
57
|
+
return full_output
|
|
58
|
+
|
|
59
|
+
# Truncate strategy: rebuild with trimmed section data
|
|
60
|
+
# Keep header + instructions intact, trim data
|
|
61
|
+
remaining = max_chars - len(header)
|
|
62
|
+
truncated_sections: list[str] = []
|
|
63
|
+
|
|
64
|
+
for section in template.sections:
|
|
65
|
+
# Fixed overhead per section (heading + DATA: + INSTRUCTIONS:)
|
|
66
|
+
frame = (
|
|
67
|
+
f"\n{section.heading}\n"
|
|
68
|
+
f"DATA:\n"
|
|
69
|
+
)
|
|
70
|
+
tail = f"\nINSTRUCTIONS: {section.instructions}\n"
|
|
71
|
+
frame_cost = len(frame) + len(tail)
|
|
72
|
+
|
|
73
|
+
if remaining <= frame_cost:
|
|
74
|
+
# Not enough room for even the frame — skip section
|
|
75
|
+
break
|
|
76
|
+
|
|
77
|
+
data = _extract_section_data(section.source, model, manifest)
|
|
78
|
+
available_for_data = remaining - frame_cost
|
|
79
|
+
if len(data) > available_for_data:
|
|
80
|
+
data = data[:available_for_data].rstrip() + "\n[truncated]"
|
|
81
|
+
|
|
82
|
+
section_text = frame + data + tail
|
|
83
|
+
truncated_sections.append(section_text)
|
|
84
|
+
remaining -= len(section_text)
|
|
85
|
+
|
|
86
|
+
return header + "".join(truncated_sections)
|
|
87
|
+
|
|
88
|
+
|
|
89
|
+
def _extract_section_data(
|
|
90
|
+
source: str, model: ArchitectureModel, manifest: dict | None
|
|
91
|
+
) -> str:
|
|
92
|
+
"""Route to appropriate extractor based on source string."""
|
|
93
|
+
extractors = {
|
|
94
|
+
"meta": lambda: _format_meta(model),
|
|
95
|
+
"components": lambda: _format_components(model),
|
|
96
|
+
"interfaces": lambda: _format_interfaces(model),
|
|
97
|
+
"capabilities": lambda: _format_capabilities(model),
|
|
98
|
+
"behaviors": lambda: _format_behaviors(model),
|
|
99
|
+
"constraints": lambda: _format_constraints(model),
|
|
100
|
+
"layers": lambda: _format_layers(model),
|
|
101
|
+
"relationships": lambda: _format_relationships(model),
|
|
102
|
+
"manifest.tests": lambda: _format_manifest_tests(manifest),
|
|
103
|
+
"manifest.metrics": lambda: _format_manifest_metrics(manifest),
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
extractor = extractors.get(source)
|
|
107
|
+
if extractor is None:
|
|
108
|
+
return "No data available for this section."
|
|
109
|
+
return extractor()
|
|
110
|
+
|
|
111
|
+
|
|
112
|
+
def _format_meta(model: ArchitectureModel) -> str:
|
|
113
|
+
"""Format project metadata."""
|
|
114
|
+
lines = [
|
|
115
|
+
f"Project: {model.meta.project}",
|
|
116
|
+
f"System: {model.meta.system}",
|
|
117
|
+
f"Schema: {model.meta.schema_version}",
|
|
118
|
+
]
|
|
119
|
+
if model.meta.generated_at:
|
|
120
|
+
lines.append(f"Generated: {model.meta.generated_at}")
|
|
121
|
+
return "\n".join(lines)
|
|
122
|
+
|
|
123
|
+
|
|
124
|
+
def _format_components(model: ArchitectureModel) -> str:
|
|
125
|
+
"""Format components list with component diagram."""
|
|
126
|
+
components = model.entities.components
|
|
127
|
+
if not components:
|
|
128
|
+
return "No data available for this section."
|
|
129
|
+
|
|
130
|
+
lines = []
|
|
131
|
+
for c in components:
|
|
132
|
+
line = f"- {c.id}: {c.name} [{c.kind.value}] layer={c.layer} status={c.status.value}"
|
|
133
|
+
if c.files:
|
|
134
|
+
line += f"\n files: {', '.join(c.files)}"
|
|
135
|
+
if c.responsibilities:
|
|
136
|
+
line += f"\n responsibilities: {', '.join(c.responsibilities)}"
|
|
137
|
+
lines.append(line)
|
|
138
|
+
|
|
139
|
+
# Append component diagram if there are enough components
|
|
140
|
+
if len(components) >= 2:
|
|
141
|
+
diagram = generate_component_diagram(model)
|
|
142
|
+
lines.append(f"\nDIAGRAM:\n```plantuml\n{diagram}\n```")
|
|
143
|
+
|
|
144
|
+
return "\n".join(lines)
|
|
145
|
+
|
|
146
|
+
|
|
147
|
+
def _format_interfaces(model: ArchitectureModel) -> str:
|
|
148
|
+
"""Format interfaces list."""
|
|
149
|
+
interfaces = model.entities.interfaces
|
|
150
|
+
if not interfaces:
|
|
151
|
+
return "No data available for this section."
|
|
152
|
+
|
|
153
|
+
lines = []
|
|
154
|
+
for i in interfaces:
|
|
155
|
+
line = f"- {i.id}: {i.name} [{i.type.value}] protocol={i.protocol}"
|
|
156
|
+
line += f"\n provider={i.provider} consumer={i.consumer}"
|
|
157
|
+
line += f"\n endpoints: {len(i.endpoints)}"
|
|
158
|
+
lines.append(line)
|
|
159
|
+
return "\n".join(lines)
|
|
160
|
+
|
|
161
|
+
|
|
162
|
+
def _format_capabilities(model: ArchitectureModel) -> str:
|
|
163
|
+
"""Format capabilities list."""
|
|
164
|
+
capabilities = model.entities.capabilities
|
|
165
|
+
if not capabilities:
|
|
166
|
+
return "No data available for this section."
|
|
167
|
+
|
|
168
|
+
lines = []
|
|
169
|
+
for cap in capabilities:
|
|
170
|
+
line = f"- {cap.id}: {cap.name} [priority={cap.priority.value}] f_block={cap.f_block}"
|
|
171
|
+
lines.append(line)
|
|
172
|
+
return "\n".join(lines)
|
|
173
|
+
|
|
174
|
+
|
|
175
|
+
def _format_behaviors(model: ArchitectureModel) -> str:
|
|
176
|
+
"""Format behaviors list with sequence diagrams."""
|
|
177
|
+
behaviors = model.entities.behaviors
|
|
178
|
+
if not behaviors:
|
|
179
|
+
return "No data available for this section."
|
|
180
|
+
|
|
181
|
+
lines = []
|
|
182
|
+
for b in behaviors:
|
|
183
|
+
line = f"- {b.id}: {b.name} trigger={b.trigger} pattern={b.pattern.value}"
|
|
184
|
+
if b.steps:
|
|
185
|
+
line += f"\n steps: {', '.join(b.steps)}"
|
|
186
|
+
lines.append(line)
|
|
187
|
+
|
|
188
|
+
# Append sequence diagrams for behaviors with steps
|
|
189
|
+
diagrams_added = 0
|
|
190
|
+
for b in behaviors:
|
|
191
|
+
if b.steps and diagrams_added < 3: # cap at 3 to control token usage
|
|
192
|
+
diagram = generate_sequence_diagram(b, model)
|
|
193
|
+
if diagram:
|
|
194
|
+
lines.append(f"\nSEQUENCE ({b.name}):\n```plantuml\n{diagram}\n```")
|
|
195
|
+
diagrams_added += 1
|
|
196
|
+
|
|
197
|
+
return "\n".join(lines)
|
|
198
|
+
|
|
199
|
+
|
|
200
|
+
def _format_constraints(model: ArchitectureModel) -> str:
|
|
201
|
+
"""Format constraints list."""
|
|
202
|
+
constraints = model.entities.constraints
|
|
203
|
+
if not constraints:
|
|
204
|
+
return "No data available for this section."
|
|
205
|
+
|
|
206
|
+
lines = []
|
|
207
|
+
for c in constraints:
|
|
208
|
+
line = f"- {c.id}: {c.name} [{c.type.value}] metric={c.metric} threshold={c.threshold}"
|
|
209
|
+
lines.append(line)
|
|
210
|
+
return "\n".join(lines)
|
|
211
|
+
|
|
212
|
+
|
|
213
|
+
def _format_layers(model: ArchitectureModel) -> str:
|
|
214
|
+
"""Format layers list."""
|
|
215
|
+
layers = model.entities.layers
|
|
216
|
+
if not layers:
|
|
217
|
+
return "No data available for this section."
|
|
218
|
+
|
|
219
|
+
lines = []
|
|
220
|
+
for layer in layers:
|
|
221
|
+
tech = ", ".join(layer.technology) if layer.technology else "none"
|
|
222
|
+
line = f"- {layer.id}: {layer.name} [order={layer.order}] tech={tech}"
|
|
223
|
+
lines.append(line)
|
|
224
|
+
return "\n".join(lines)
|
|
225
|
+
|
|
226
|
+
|
|
227
|
+
def _format_relationships(model: ArchitectureModel) -> str:
|
|
228
|
+
"""Format relationship list with dependency diagram."""
|
|
229
|
+
relationships = model.relationships
|
|
230
|
+
if not relationships:
|
|
231
|
+
return "No data available for this section."
|
|
232
|
+
|
|
233
|
+
lines = []
|
|
234
|
+
for r in relationships:
|
|
235
|
+
line = f"- {r.from_id} --{r.type.value}--> {r.to_id}"
|
|
236
|
+
lines.append(line)
|
|
237
|
+
|
|
238
|
+
# Append dependency diagram if there are relationships
|
|
239
|
+
diagram = generate_dependency_diagram(model)
|
|
240
|
+
if "@startuml" in diagram and "rectangle" in diagram:
|
|
241
|
+
lines.append(f"\nDIAGRAM:\n```plantuml\n{diagram}\n```")
|
|
242
|
+
|
|
243
|
+
return "\n".join(lines)
|
|
244
|
+
|
|
245
|
+
|
|
246
|
+
def _format_manifest_tests(manifest: dict | None) -> str:
|
|
247
|
+
"""Format test data from manifest."""
|
|
248
|
+
if manifest is None or "test_files" not in manifest:
|
|
249
|
+
return "No data available for this section."
|
|
250
|
+
|
|
251
|
+
test_files = manifest["test_files"]
|
|
252
|
+
lines = [f"Test files: {len(test_files)}"]
|
|
253
|
+
for f in test_files:
|
|
254
|
+
lines.append(f"- {f}")
|
|
255
|
+
return "\n".join(lines)
|
|
256
|
+
|
|
257
|
+
|
|
258
|
+
def _format_manifest_metrics(manifest: dict | None) -> str:
|
|
259
|
+
"""Format metrics from manifest."""
|
|
260
|
+
if manifest is None or "metrics" not in manifest:
|
|
261
|
+
return "No data available for this section."
|
|
262
|
+
|
|
263
|
+
metrics = manifest["metrics"]
|
|
264
|
+
lines = []
|
|
265
|
+
for key, value in metrics.items():
|
|
266
|
+
lines.append(f"- {key}: {value}")
|
|
267
|
+
return "\n".join(lines)
|
|
268
|
+
|
|
269
|
+
|
|
270
|
+
# ---------------------------------------------------------------------------
|
|
271
|
+
# Per-Capability Detail Context
|
|
272
|
+
# ---------------------------------------------------------------------------
|
|
273
|
+
|
|
274
|
+
|
|
275
|
+
def format_capability_detail_context(
|
|
276
|
+
cap_id: str,
|
|
277
|
+
model: ArchitectureModel,
|
|
278
|
+
manifest: dict | None = None,
|
|
279
|
+
) -> str:
|
|
280
|
+
"""Assemble rich context for generating a per-capability API detail doc.
|
|
281
|
+
|
|
282
|
+
Extracts the full neighborhood of a single capability:
|
|
283
|
+
- The capability entity itself
|
|
284
|
+
- Realizing component(s) and their files/signatures/constants
|
|
285
|
+
- Related behaviors (steps, triggers, pre/postconditions)
|
|
286
|
+
- Exposed interfaces (endpoints, protocol)
|
|
287
|
+
- Applicable constraints
|
|
288
|
+
- Upstream/downstream relationships
|
|
289
|
+
|
|
290
|
+
Returns a structured text block suitable for LLM prompt injection.
|
|
291
|
+
"""
|
|
292
|
+
from architecture_model.core.types import RelationType
|
|
293
|
+
|
|
294
|
+
# Find the capability
|
|
295
|
+
cap = None
|
|
296
|
+
for c in model.entities.capabilities:
|
|
297
|
+
if c.id == cap_id:
|
|
298
|
+
cap = c
|
|
299
|
+
break
|
|
300
|
+
if cap is None:
|
|
301
|
+
return f"Capability {cap_id} not found in model."
|
|
302
|
+
|
|
303
|
+
lines: list[str] = []
|
|
304
|
+
|
|
305
|
+
# --- Capability identity ---
|
|
306
|
+
lines.append("## Capability")
|
|
307
|
+
lines.append(f"ID: {cap.id}")
|
|
308
|
+
lines.append(f"Name: {cap.name}")
|
|
309
|
+
lines.append(f"Status: {cap.status.value}")
|
|
310
|
+
lines.append(f"F-Block: {cap.f_block}")
|
|
311
|
+
lines.append(f"Priority: {cap.priority.value}")
|
|
312
|
+
if cap.requirements:
|
|
313
|
+
lines.append(f"Requirements: {'; '.join(cap.requirements)}")
|
|
314
|
+
|
|
315
|
+
# --- Realizing components ---
|
|
316
|
+
realizing_comp_ids: list[str] = []
|
|
317
|
+
for rel in model.relationships:
|
|
318
|
+
if rel.type == RelationType.REALIZES and rel.to_id == cap_id:
|
|
319
|
+
realizing_comp_ids.append(rel.from_id)
|
|
320
|
+
|
|
321
|
+
realizing_comps = [
|
|
322
|
+
comp for comp in model.entities.components
|
|
323
|
+
if comp.id in realizing_comp_ids
|
|
324
|
+
]
|
|
325
|
+
|
|
326
|
+
if realizing_comps:
|
|
327
|
+
lines.append("\n## Realizing Components")
|
|
328
|
+
for comp in realizing_comps:
|
|
329
|
+
lines.append(f"\n### {comp.id}: {comp.name}")
|
|
330
|
+
lines.append(f"Kind: {comp.kind.value}")
|
|
331
|
+
lines.append(f"Layer: {comp.layer}")
|
|
332
|
+
lines.append(f"Status: {comp.status.value}")
|
|
333
|
+
if comp.files:
|
|
334
|
+
lines.append(f"Files: {', '.join(comp.files)}")
|
|
335
|
+
if comp.responsibilities:
|
|
336
|
+
lines.append(f"Responsibilities: {'; '.join(comp.responsibilities)}")
|
|
337
|
+
if comp.technology:
|
|
338
|
+
lines.append(f"Technology: {comp.technology}")
|
|
339
|
+
|
|
340
|
+
# Signatures (enriched model v1.4)
|
|
341
|
+
if comp.signatures:
|
|
342
|
+
lines.append(f"\nFunction Signatures ({len(comp.signatures)}):")
|
|
343
|
+
for sig in comp.signatures:
|
|
344
|
+
sig_line = f" - {sig.name}({', '.join(sig.params)}) -> {sig.returns}"
|
|
345
|
+
if sig.body_hint:
|
|
346
|
+
sig_line += f" # {sig.body_hint}"
|
|
347
|
+
lines.append(sig_line)
|
|
348
|
+
|
|
349
|
+
# Constants (enriched model v1.4)
|
|
350
|
+
if comp.constants:
|
|
351
|
+
lines.append(f"\nConstants ({len(comp.constants)}):")
|
|
352
|
+
for const in comp.constants:
|
|
353
|
+
lines.append(f" - {const.name} = {const.value}")
|
|
354
|
+
|
|
355
|
+
# Test contracts (enriched model v1.4)
|
|
356
|
+
if comp.test_contracts:
|
|
357
|
+
lines.append(f"\nTest Contracts ({len(comp.test_contracts)}):")
|
|
358
|
+
for tc in comp.test_contracts[:20]: # cap for token budget
|
|
359
|
+
lines.append(f" - [{tc.test_file}::{tc.test_method}] {tc.assertion}")
|
|
360
|
+
|
|
361
|
+
# --- Related behaviors ---
|
|
362
|
+
# Find behaviors linked to realizing components via 'traces-to' or matching actor
|
|
363
|
+
behavior_ids: set[str] = set()
|
|
364
|
+
for rel in model.relationships:
|
|
365
|
+
if rel.type == RelationType.TRACES_TO and rel.from_id in realizing_comp_ids:
|
|
366
|
+
behavior_ids.add(rel.to_id)
|
|
367
|
+
elif rel.type == RelationType.TRACES_TO and rel.to_id in realizing_comp_ids:
|
|
368
|
+
behavior_ids.add(rel.from_id)
|
|
369
|
+
|
|
370
|
+
behaviors = [b for b in model.entities.behaviors if b.id in behavior_ids]
|
|
371
|
+
if behaviors:
|
|
372
|
+
lines.append("\n## Behavioral Views")
|
|
373
|
+
for beh in behaviors:
|
|
374
|
+
lines.append(f"\n### {beh.id}: {beh.name}")
|
|
375
|
+
lines.append(f"Trigger: {beh.trigger}")
|
|
376
|
+
lines.append(f"Pattern: {beh.pattern.value}")
|
|
377
|
+
if beh.actor:
|
|
378
|
+
lines.append(f"Actor: {beh.actor}")
|
|
379
|
+
if beh.steps:
|
|
380
|
+
lines.append("Steps:")
|
|
381
|
+
for i, step in enumerate(beh.steps, 1):
|
|
382
|
+
lines.append(f" {i}. {step}")
|
|
383
|
+
|
|
384
|
+
# --- Exposed interfaces ---
|
|
385
|
+
interface_ids: set[str] = set()
|
|
386
|
+
for rel in model.relationships:
|
|
387
|
+
if rel.type == RelationType.EXPOSES and rel.from_id in realizing_comp_ids:
|
|
388
|
+
interface_ids.add(rel.to_id)
|
|
389
|
+
|
|
390
|
+
interfaces = [iface for iface in model.entities.interfaces if iface.id in interface_ids]
|
|
391
|
+
if interfaces:
|
|
392
|
+
lines.append("\n## Exposed Interfaces")
|
|
393
|
+
for iface in interfaces:
|
|
394
|
+
lines.append(f"\n### {iface.id}: {iface.name}")
|
|
395
|
+
lines.append(f"Protocol: {iface.protocol}")
|
|
396
|
+
lines.append(f"Provider: {iface.provider}")
|
|
397
|
+
lines.append(f"Consumer: {iface.consumer}")
|
|
398
|
+
if iface.endpoints:
|
|
399
|
+
lines.append("Endpoints:")
|
|
400
|
+
for ep in iface.endpoints:
|
|
401
|
+
lines.append(f" - {ep.get('method', '?')}: {ep.get('path', '?')}")
|
|
402
|
+
|
|
403
|
+
# --- Constraints ---
|
|
404
|
+
constraint_ids: set[str] = set()
|
|
405
|
+
for rel in model.relationships:
|
|
406
|
+
if rel.type == RelationType.CONSTRAINED_BY:
|
|
407
|
+
if rel.from_id in realizing_comp_ids or rel.to_id in realizing_comp_ids:
|
|
408
|
+
constraint_ids.add(rel.from_id)
|
|
409
|
+
constraint_ids.add(rel.to_id)
|
|
410
|
+
|
|
411
|
+
constraints = [c for c in model.entities.constraints if c.id in constraint_ids]
|
|
412
|
+
if constraints:
|
|
413
|
+
lines.append("\n## Constraints")
|
|
414
|
+
for con in constraints:
|
|
415
|
+
lines.append(f"- {con.id}: {con.name} [{con.type.value}]")
|
|
416
|
+
lines.append(f" metric={con.metric}, threshold={con.threshold}")
|
|
417
|
+
|
|
418
|
+
# --- Dependency relationships ---
|
|
419
|
+
dep_lines: list[str] = []
|
|
420
|
+
all_related_ids = set(realizing_comp_ids) | interface_ids | behavior_ids | constraint_ids
|
|
421
|
+
for rel in model.relationships:
|
|
422
|
+
if rel.from_id in all_related_ids or rel.to_id in all_related_ids:
|
|
423
|
+
dep_lines.append(f" {rel.from_id} --{rel.type.value}--> {rel.to_id}")
|
|
424
|
+
|
|
425
|
+
if dep_lines:
|
|
426
|
+
lines.append("\n## Relationship Graph")
|
|
427
|
+
lines.extend(dep_lines)
|
|
428
|
+
|
|
429
|
+
# --- Manifest data for realizing files ---
|
|
430
|
+
if manifest and realizing_comps:
|
|
431
|
+
modules = manifest.get("modules", [])
|
|
432
|
+
comp_files = set()
|
|
433
|
+
for comp in realizing_comps:
|
|
434
|
+
comp_files.update(comp.files)
|
|
435
|
+
|
|
436
|
+
relevant_modules = [m for m in modules if m.get("file", "") in comp_files]
|
|
437
|
+
if relevant_modules:
|
|
438
|
+
lines.append("\n## Manifest Detail (AST-scanned)")
|
|
439
|
+
for mod in relevant_modules:
|
|
440
|
+
lines.append(f"\n### {mod.get('file', '?')}")
|
|
441
|
+
if mod.get("docstring"):
|
|
442
|
+
lines.append(f" Docstring: {mod['docstring'][:200]}")
|
|
443
|
+
if mod.get("functions"):
|
|
444
|
+
lines.append(f" Functions: {', '.join(mod['functions'][:15])}")
|
|
445
|
+
if mod.get("classes"):
|
|
446
|
+
for cls in mod["classes"][:5]:
|
|
447
|
+
cls_name = cls.get("name", "?")
|
|
448
|
+
methods = cls.get("methods", [])
|
|
449
|
+
lines.append(f" Class {cls_name}: {', '.join(methods[:10])}")
|
|
450
|
+
|
|
451
|
+
return "\n".join(lines)
|