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,29 @@
|
|
|
1
|
+
"""Context compression and token brokering."""
|
|
2
|
+
|
|
3
|
+
from opencode_arch.context.formatter import (
|
|
4
|
+
format_model_context,
|
|
5
|
+
format_fblock_context,
|
|
6
|
+
format_artifact_context,
|
|
7
|
+
query_model,
|
|
8
|
+
impact_analysis,
|
|
9
|
+
)
|
|
10
|
+
from opencode_arch.context.pipeline_bridge import (
|
|
11
|
+
get_model,
|
|
12
|
+
get_artifact_context,
|
|
13
|
+
get_fblock_context,
|
|
14
|
+
get_model_summary,
|
|
15
|
+
enrich_manifest_slice,
|
|
16
|
+
)
|
|
17
|
+
|
|
18
|
+
__all__ = [
|
|
19
|
+
"format_model_context",
|
|
20
|
+
"format_fblock_context",
|
|
21
|
+
"format_artifact_context",
|
|
22
|
+
"query_model",
|
|
23
|
+
"impact_analysis",
|
|
24
|
+
"get_model",
|
|
25
|
+
"get_artifact_context",
|
|
26
|
+
"get_fblock_context",
|
|
27
|
+
"get_model_summary",
|
|
28
|
+
"enrich_manifest_slice",
|
|
29
|
+
]
|
|
@@ -0,0 +1,492 @@
|
|
|
1
|
+
"""
|
|
2
|
+
LLM Context Formatter: Produce compact model representations for LLM prompt injection.
|
|
3
|
+
|
|
4
|
+
Implements the LLM Integration Protocol:
|
|
5
|
+
- LOAD: Serialize model (or slice) into compact text for system prompt
|
|
6
|
+
- QUERY: Answer structural questions from model data
|
|
7
|
+
- IMPACT: Determine what entities are affected by a proposed change
|
|
8
|
+
|
|
9
|
+
The key constraint is TOKEN BUDGET — we need maximum information density.
|
|
10
|
+
Format: structured YAML-like text, ~25:1 compression vs full artifact markdown.
|
|
11
|
+
"""
|
|
12
|
+
|
|
13
|
+
from __future__ import annotations
|
|
14
|
+
|
|
15
|
+
from typing import Optional
|
|
16
|
+
|
|
17
|
+
from architecture_model.core.types import (
|
|
18
|
+
ArchitectureModel,
|
|
19
|
+
Actor,
|
|
20
|
+
Behavior,
|
|
21
|
+
Capability,
|
|
22
|
+
Component,
|
|
23
|
+
Constraint,
|
|
24
|
+
Interface,
|
|
25
|
+
Layer,
|
|
26
|
+
Relationship,
|
|
27
|
+
Status,
|
|
28
|
+
)
|
|
29
|
+
from architecture_model.core.slicer import slice_by_fblock, slice_for_artifact
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
# ---------------------------------------------------------------------------
|
|
33
|
+
# Public API
|
|
34
|
+
# ---------------------------------------------------------------------------
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
def format_model_context(
|
|
38
|
+
model: ArchitectureModel,
|
|
39
|
+
max_tokens: int = 4000,
|
|
40
|
+
detail_level: str = "standard",
|
|
41
|
+
) -> str:
|
|
42
|
+
"""
|
|
43
|
+
Format the full model as compact LLM context.
|
|
44
|
+
|
|
45
|
+
Args:
|
|
46
|
+
model: Architecture model to format.
|
|
47
|
+
max_tokens: Approximate token budget (1 token ~ 4 chars).
|
|
48
|
+
detail_level: "minimal", "standard", or "full".
|
|
49
|
+
|
|
50
|
+
Returns:
|
|
51
|
+
Formatted text suitable for LLM system prompt injection.
|
|
52
|
+
"""
|
|
53
|
+
char_budget = max_tokens * 4
|
|
54
|
+
|
|
55
|
+
# Progressive summarization: add sections by priority, stop before exceeding budget
|
|
56
|
+
priority_1: list[str] = []
|
|
57
|
+
priority_2: list[str] = []
|
|
58
|
+
priority_3: list[str] = []
|
|
59
|
+
priority_4: list[str] = []
|
|
60
|
+
|
|
61
|
+
# Priority 1: Header + component names (always included)
|
|
62
|
+
priority_1.append(_format_header(model))
|
|
63
|
+
if model.entities.components:
|
|
64
|
+
lines = [f"\n## Components ({len(model.entities.components)})"]
|
|
65
|
+
for comp in model.entities.components:
|
|
66
|
+
file_count = len(comp.files) if comp.files else 0
|
|
67
|
+
lines.append(f" {comp.id}: {comp.name} ({file_count} files)")
|
|
68
|
+
# Include interface contracts at full detail
|
|
69
|
+
if detail_level == "full" and hasattr(comp, "interfaces") and comp.interfaces:
|
|
70
|
+
for iface in comp.interfaces:
|
|
71
|
+
sym_str = f" [{', '.join(iface.symbols[:5])}]" if iface.symbols else ""
|
|
72
|
+
lines.append(f" {iface.kind}: {iface.target_component}{sym_str}")
|
|
73
|
+
priority_1.append("\n".join(lines))
|
|
74
|
+
|
|
75
|
+
# Priority 2: Key relationships (grouped by type, top connections)
|
|
76
|
+
if model.relationships:
|
|
77
|
+
priority_2.append(_format_relationships_compact(model))
|
|
78
|
+
|
|
79
|
+
# Priority 3: Capabilities + behaviors (compact)
|
|
80
|
+
if detail_level in ("standard", "full"):
|
|
81
|
+
priority_3.append(_format_capabilities(model))
|
|
82
|
+
priority_3.append(_format_actors(model))
|
|
83
|
+
if detail_level == "full":
|
|
84
|
+
priority_3.append(_format_behaviors(model))
|
|
85
|
+
else:
|
|
86
|
+
priority_3.append(_format_behaviors_compact(model))
|
|
87
|
+
|
|
88
|
+
# Priority 4: Full detail (interfaces, layers, constraints)
|
|
89
|
+
if detail_level == "full":
|
|
90
|
+
priority_4.append(_format_interfaces(model))
|
|
91
|
+
priority_4.append(_format_layers(model))
|
|
92
|
+
priority_4.append(_format_constraints(model))
|
|
93
|
+
elif detail_level == "standard":
|
|
94
|
+
priority_4.append(_format_interfaces_compact(model))
|
|
95
|
+
priority_4.append(_format_layers_compact(model))
|
|
96
|
+
|
|
97
|
+
# Progressively add sections until budget is reached
|
|
98
|
+
result_parts: list[str] = []
|
|
99
|
+
used = 0
|
|
100
|
+
|
|
101
|
+
for section_group in [priority_1, priority_2, priority_3, priority_4]:
|
|
102
|
+
for section in section_group:
|
|
103
|
+
if not section:
|
|
104
|
+
continue
|
|
105
|
+
section_len = len(section)
|
|
106
|
+
if used + section_len <= char_budget:
|
|
107
|
+
result_parts.append(section)
|
|
108
|
+
used += section_len
|
|
109
|
+
else:
|
|
110
|
+
# Don't truncate mid-section; stop here
|
|
111
|
+
break
|
|
112
|
+
else:
|
|
113
|
+
continue
|
|
114
|
+
break
|
|
115
|
+
|
|
116
|
+
return "\n".join(result_parts)
|
|
117
|
+
|
|
118
|
+
|
|
119
|
+
def format_fblock_context(
|
|
120
|
+
model: ArchitectureModel,
|
|
121
|
+
f_block: str,
|
|
122
|
+
max_tokens: int = 2000,
|
|
123
|
+
project_root: "Path | None" = None,
|
|
124
|
+
) -> str:
|
|
125
|
+
"""
|
|
126
|
+
Format context for a single F-block (for artifact section regeneration).
|
|
127
|
+
|
|
128
|
+
Produces: capability description, related UCs, components, interfaces.
|
|
129
|
+
If *project_root* is given, sub-models are auto-loaded for richer detail,
|
|
130
|
+
and per-block manifests are consumed for function-level context.
|
|
131
|
+
"""
|
|
132
|
+
sliced = slice_by_fblock(model, f_block, project_root=project_root)
|
|
133
|
+
base_context = format_model_context(sliced, max_tokens=max_tokens, detail_level="full")
|
|
134
|
+
|
|
135
|
+
# Consume per-block manifest if available (reduces compression ratio)
|
|
136
|
+
if project_root:
|
|
137
|
+
block_manifest = _load_block_manifest(project_root, f_block)
|
|
138
|
+
if block_manifest:
|
|
139
|
+
char_budget = max_tokens * 4
|
|
140
|
+
remaining = char_budget - len(base_context)
|
|
141
|
+
if remaining > 200:
|
|
142
|
+
manifest_section = _format_block_manifest(block_manifest, remaining)
|
|
143
|
+
if manifest_section:
|
|
144
|
+
base_context += "\n" + manifest_section
|
|
145
|
+
|
|
146
|
+
return base_context
|
|
147
|
+
|
|
148
|
+
|
|
149
|
+
def format_artifact_context(
|
|
150
|
+
model: ArchitectureModel,
|
|
151
|
+
artifact_name: str,
|
|
152
|
+
max_tokens: int = 3000,
|
|
153
|
+
) -> str:
|
|
154
|
+
"""
|
|
155
|
+
Format context appropriate for regenerating a specific artifact.
|
|
156
|
+
|
|
157
|
+
Uses artifact-specific slicing then formats at appropriate detail level.
|
|
158
|
+
"""
|
|
159
|
+
sliced = slice_for_artifact(model, artifact_name)
|
|
160
|
+
|
|
161
|
+
detail_map = {
|
|
162
|
+
"functional-architecture": "full",
|
|
163
|
+
"logical-architecture": "full",
|
|
164
|
+
"use-cases": "full",
|
|
165
|
+
"icd": "full",
|
|
166
|
+
"requirements-analysis": "standard",
|
|
167
|
+
"readme": "minimal",
|
|
168
|
+
}
|
|
169
|
+
detail = detail_map.get(artifact_name, "standard")
|
|
170
|
+
|
|
171
|
+
return format_model_context(sliced, max_tokens=max_tokens, detail_level=detail)
|
|
172
|
+
|
|
173
|
+
|
|
174
|
+
def query_model(model: ArchitectureModel, question: str) -> str:
|
|
175
|
+
"""
|
|
176
|
+
Answer a structural question from model data.
|
|
177
|
+
|
|
178
|
+
Supports questions like:
|
|
179
|
+
- "What realizes F3?" → list behaviors with tag F3
|
|
180
|
+
- "What does UC-14 depend on?" → follow depends-on relationships
|
|
181
|
+
- "What interfaces does F4 expose?" → filter interfaces by provider
|
|
182
|
+
"""
|
|
183
|
+
q = question.lower().strip()
|
|
184
|
+
|
|
185
|
+
# Pattern: "what realizes <X>?"
|
|
186
|
+
if "realizes" in q:
|
|
187
|
+
import re
|
|
188
|
+
|
|
189
|
+
m = re.search(r"(f\d+|cap-\w+)", q, re.IGNORECASE)
|
|
190
|
+
if m:
|
|
191
|
+
target = m.group(1).upper()
|
|
192
|
+
realizers = [
|
|
193
|
+
r.from_id
|
|
194
|
+
for r in model.relationships
|
|
195
|
+
if r.type.value == "realizes" and target in r.to_id.upper()
|
|
196
|
+
]
|
|
197
|
+
if realizers:
|
|
198
|
+
lines = [f"Entities realizing {target}:"]
|
|
199
|
+
for rid in realizers:
|
|
200
|
+
beh = next((b for b in model.entities.behaviors if b.id == rid), None)
|
|
201
|
+
if beh:
|
|
202
|
+
lines.append(f" - {beh.id}: {beh.name} [{beh.status.value}]")
|
|
203
|
+
else:
|
|
204
|
+
lines.append(f" - {rid}")
|
|
205
|
+
return "\n".join(lines)
|
|
206
|
+
return f"No entities realize {target}"
|
|
207
|
+
|
|
208
|
+
# Pattern: "what does <X> depend on?"
|
|
209
|
+
if "depend" in q:
|
|
210
|
+
import re
|
|
211
|
+
|
|
212
|
+
m = re.search(r"(uc-\d+|[a-z][\w-]+)", q, re.IGNORECASE)
|
|
213
|
+
if m:
|
|
214
|
+
source = m.group(1)
|
|
215
|
+
deps = [
|
|
216
|
+
r.to_id
|
|
217
|
+
for r in model.relationships
|
|
218
|
+
if r.from_id.lower() == source.lower() and r.type.value == "depends-on"
|
|
219
|
+
]
|
|
220
|
+
if deps:
|
|
221
|
+
return f"{source} depends on: {', '.join(deps)}"
|
|
222
|
+
return f"{source} has no recorded dependencies"
|
|
223
|
+
|
|
224
|
+
# Pattern: count/summary
|
|
225
|
+
if "how many" in q or "count" in q:
|
|
226
|
+
return (
|
|
227
|
+
f"Model contains: {model.entity_count} entities, {model.relationship_count} relationships\n"
|
|
228
|
+
f" Actors: {len(model.entities.actors)}\n"
|
|
229
|
+
f" Capabilities: {len(model.entities.capabilities)}\n"
|
|
230
|
+
f" Behaviors: {len(model.entities.behaviors)}\n"
|
|
231
|
+
f" Interfaces: {len(model.entities.interfaces)}\n"
|
|
232
|
+
f" Constraints: {len(model.entities.constraints)}\n"
|
|
233
|
+
f" Layers: {len(model.entities.layers)}\n"
|
|
234
|
+
f" Components: {len(model.entities.components)}"
|
|
235
|
+
)
|
|
236
|
+
|
|
237
|
+
return f"Unable to answer: {question}\nTry: 'what realizes F3?', 'what does UC-14 depend on?', 'how many entities?'"
|
|
238
|
+
|
|
239
|
+
|
|
240
|
+
def impact_analysis(model: ArchitectureModel, entity_id: str, depth: int = 2) -> str:
|
|
241
|
+
"""
|
|
242
|
+
Determine what entities are affected if a given entity changes.
|
|
243
|
+
|
|
244
|
+
Traces relationships transitively up to `depth` levels.
|
|
245
|
+
"""
|
|
246
|
+
affected: dict[str, int] = {} # entity_id -> distance
|
|
247
|
+
frontier = {entity_id}
|
|
248
|
+
current_depth = 0
|
|
249
|
+
|
|
250
|
+
while frontier and current_depth < depth:
|
|
251
|
+
next_frontier: set[str] = set()
|
|
252
|
+
for eid in frontier:
|
|
253
|
+
for rel in model.relationships:
|
|
254
|
+
# Forward direction: what depends on this?
|
|
255
|
+
if rel.to_id == eid and rel.from_id not in affected and rel.from_id != entity_id:
|
|
256
|
+
affected[rel.from_id] = current_depth + 1
|
|
257
|
+
next_frontier.add(rel.from_id)
|
|
258
|
+
# Reverse for 'realizes': if this behavior changes, its capability is affected
|
|
259
|
+
if rel.from_id == eid and rel.to_id not in affected and rel.to_id != entity_id:
|
|
260
|
+
if rel.type.value in ("realizes", "contains", "exposes"):
|
|
261
|
+
affected[rel.to_id] = current_depth + 1
|
|
262
|
+
next_frontier.add(rel.to_id)
|
|
263
|
+
frontier = next_frontier
|
|
264
|
+
current_depth += 1
|
|
265
|
+
|
|
266
|
+
if not affected:
|
|
267
|
+
return f"No entities are directly affected by changes to {entity_id}"
|
|
268
|
+
|
|
269
|
+
lines = [f"Impact analysis for {entity_id} (depth={depth}):"]
|
|
270
|
+
for eid, dist in sorted(affected.items(), key=lambda x: x[1]):
|
|
271
|
+
# Try to find name
|
|
272
|
+
name = _find_entity_name(model, eid)
|
|
273
|
+
lines.append(f" {' ' * dist}[depth {dist}] {eid}: {name}")
|
|
274
|
+
|
|
275
|
+
return "\n".join(lines)
|
|
276
|
+
|
|
277
|
+
|
|
278
|
+
# ---------------------------------------------------------------------------
|
|
279
|
+
# Formatters
|
|
280
|
+
# ---------------------------------------------------------------------------
|
|
281
|
+
|
|
282
|
+
|
|
283
|
+
def _format_header(model: ArchitectureModel) -> str:
|
|
284
|
+
return (
|
|
285
|
+
f"# Architecture Model: {model.meta.project}\n"
|
|
286
|
+
f"System: {model.meta.system} | Schema: {model.meta.schema_version}\n"
|
|
287
|
+
f"Entities: {model.entity_count} | Relationships: {model.relationship_count}\n"
|
|
288
|
+
f"Sources: {', '.join(model.meta.source_artifacts)}"
|
|
289
|
+
)
|
|
290
|
+
|
|
291
|
+
|
|
292
|
+
def _format_capabilities(model: ArchitectureModel) -> str:
|
|
293
|
+
if not model.entities.capabilities:
|
|
294
|
+
return ""
|
|
295
|
+
lines = ["\n## Capabilities (F-blocks)"]
|
|
296
|
+
for cap in model.entities.capabilities:
|
|
297
|
+
lines.append(f" {cap.id} ({cap.f_block}): {cap.name} [{cap.status.value}]")
|
|
298
|
+
return "\n".join(lines)
|
|
299
|
+
|
|
300
|
+
|
|
301
|
+
def _format_actors(model: ArchitectureModel) -> str:
|
|
302
|
+
if not model.entities.actors:
|
|
303
|
+
return ""
|
|
304
|
+
lines = ["\n## Actors"]
|
|
305
|
+
for actor in model.entities.actors:
|
|
306
|
+
goals = "; ".join(actor.goals[:3]) if actor.goals else ""
|
|
307
|
+
lines.append(f" {actor.id}: {actor.name} ({actor.type.value}) — {goals}")
|
|
308
|
+
return "\n".join(lines)
|
|
309
|
+
|
|
310
|
+
|
|
311
|
+
def _format_behaviors(model: ArchitectureModel) -> str:
|
|
312
|
+
if not model.entities.behaviors:
|
|
313
|
+
return ""
|
|
314
|
+
lines = ["\n## Behaviors (Use Cases)"]
|
|
315
|
+
for beh in model.entities.behaviors:
|
|
316
|
+
post = beh.postconditions[0][:60] if beh.postconditions else ""
|
|
317
|
+
lines.append(
|
|
318
|
+
f" {beh.id}: {beh.name} [{beh.status.value}] "
|
|
319
|
+
f"actor={beh.actor} freq={beh.frequency} pri={beh.priority.value}"
|
|
320
|
+
)
|
|
321
|
+
if post:
|
|
322
|
+
lines.append(f" acceptance: {post}")
|
|
323
|
+
return "\n".join(lines)
|
|
324
|
+
|
|
325
|
+
|
|
326
|
+
def _format_behaviors_compact(model: ArchitectureModel) -> str:
|
|
327
|
+
if not model.entities.behaviors:
|
|
328
|
+
return ""
|
|
329
|
+
lines = ["\n## Behaviors (30 UCs)"]
|
|
330
|
+
for beh in model.entities.behaviors:
|
|
331
|
+
tag = beh.tags[0] if beh.tags else "?"
|
|
332
|
+
lines.append(f" {beh.id}: {beh.name} [{beh.status.value}] {tag} pri={beh.priority.value}")
|
|
333
|
+
return "\n".join(lines)
|
|
334
|
+
|
|
335
|
+
|
|
336
|
+
def _format_interfaces(model: ArchitectureModel) -> str:
|
|
337
|
+
if not model.entities.interfaces:
|
|
338
|
+
return ""
|
|
339
|
+
lines = ["\n## Interfaces"]
|
|
340
|
+
for iface in model.entities.interfaces:
|
|
341
|
+
lines.append(
|
|
342
|
+
f" {iface.id}: {iface.type.value} | {iface.provider} -> {iface.consumer} "
|
|
343
|
+
f"via {iface.protocol} [{iface.status.value}]"
|
|
344
|
+
)
|
|
345
|
+
return "\n".join(lines)
|
|
346
|
+
|
|
347
|
+
|
|
348
|
+
def _format_interfaces_compact(model: ArchitectureModel) -> str:
|
|
349
|
+
if not model.entities.interfaces:
|
|
350
|
+
return ""
|
|
351
|
+
lines = [f"\n## Interfaces ({len(model.entities.interfaces)})"]
|
|
352
|
+
for iface in model.entities.interfaces:
|
|
353
|
+
lines.append(f" {iface.id}: {iface.provider} -> {iface.consumer} ({iface.type.value})")
|
|
354
|
+
return "\n".join(lines)
|
|
355
|
+
|
|
356
|
+
|
|
357
|
+
def _format_layers(model: ArchitectureModel) -> str:
|
|
358
|
+
if not model.entities.layers:
|
|
359
|
+
return ""
|
|
360
|
+
lines = ["\n## Layers"]
|
|
361
|
+
for layer in model.entities.layers:
|
|
362
|
+
comp_count = sum(1 for c in model.entities.components if c.layer == layer.id)
|
|
363
|
+
lines.append(f" {layer.id}: {layer.name} (order={layer.order}, {comp_count} components)")
|
|
364
|
+
if layer.directories:
|
|
365
|
+
lines.append(f" dirs: {', '.join(layer.directories)}")
|
|
366
|
+
return "\n".join(lines)
|
|
367
|
+
|
|
368
|
+
|
|
369
|
+
def _format_layers_compact(model: ArchitectureModel) -> str:
|
|
370
|
+
if not model.entities.layers:
|
|
371
|
+
return ""
|
|
372
|
+
lines = [f"\n## Layers ({len(model.entities.layers)})"]
|
|
373
|
+
for layer in model.entities.layers:
|
|
374
|
+
lines.append(f" {layer.id}: {layer.name}")
|
|
375
|
+
return "\n".join(lines)
|
|
376
|
+
|
|
377
|
+
|
|
378
|
+
def _format_components(model: ArchitectureModel) -> str:
|
|
379
|
+
if not model.entities.components:
|
|
380
|
+
return ""
|
|
381
|
+
lines = [f"\n## Components ({len(model.entities.components)})"]
|
|
382
|
+
for comp in model.entities.components:
|
|
383
|
+
files = ", ".join(comp.files[:2]) if comp.files else ""
|
|
384
|
+
lines.append(f" {comp.id}: {comp.name} (layer={comp.layer}, {comp.f_block}) [{files}]")
|
|
385
|
+
return "\n".join(lines)
|
|
386
|
+
|
|
387
|
+
|
|
388
|
+
def _format_constraints(model: ArchitectureModel) -> str:
|
|
389
|
+
if not model.entities.constraints:
|
|
390
|
+
return ""
|
|
391
|
+
lines = [f"\n## Constraints ({len(model.entities.constraints)})"]
|
|
392
|
+
for con in model.entities.constraints:
|
|
393
|
+
lines.append(f" {con.id}: {con.name} ({con.type.value}) threshold={con.threshold}")
|
|
394
|
+
return "\n".join(lines)
|
|
395
|
+
|
|
396
|
+
|
|
397
|
+
def _format_relationships_compact(model: ArchitectureModel) -> str:
|
|
398
|
+
if not model.relationships:
|
|
399
|
+
return ""
|
|
400
|
+
# Group by type
|
|
401
|
+
by_type: dict[str, list[Relationship]] = {}
|
|
402
|
+
for rel in model.relationships:
|
|
403
|
+
by_type.setdefault(rel.type.value, []).append(rel)
|
|
404
|
+
|
|
405
|
+
lines = [f"\n## Relationships ({len(model.relationships)})"]
|
|
406
|
+
for rtype, rels in by_type.items():
|
|
407
|
+
lines.append(f" {rtype} ({len(rels)}):")
|
|
408
|
+
for rel in rels[:10]: # Limit per type
|
|
409
|
+
lines.append(f" {rel.from_id} -> {rel.to_id}")
|
|
410
|
+
if len(rels) > 10:
|
|
411
|
+
lines.append(f" ... +{len(rels) - 10} more")
|
|
412
|
+
return "\n".join(lines)
|
|
413
|
+
|
|
414
|
+
|
|
415
|
+
def _find_entity_name(model: ArchitectureModel, entity_id: str) -> str:
|
|
416
|
+
"""Find the human-readable name for an entity ID."""
|
|
417
|
+
for lst in [
|
|
418
|
+
model.entities.actors,
|
|
419
|
+
model.entities.capabilities,
|
|
420
|
+
model.entities.behaviors,
|
|
421
|
+
model.entities.interfaces,
|
|
422
|
+
model.entities.constraints,
|
|
423
|
+
model.entities.layers,
|
|
424
|
+
model.entities.components,
|
|
425
|
+
]:
|
|
426
|
+
for e in lst:
|
|
427
|
+
if e.id == entity_id:
|
|
428
|
+
return e.name
|
|
429
|
+
return entity_id
|
|
430
|
+
|
|
431
|
+
|
|
432
|
+
# ---------------------------------------------------------------------------
|
|
433
|
+
# Block manifest consumption
|
|
434
|
+
# ---------------------------------------------------------------------------
|
|
435
|
+
|
|
436
|
+
|
|
437
|
+
def _load_block_manifest(project_root: "Path", f_block: str) -> dict | None:
|
|
438
|
+
"""Load per-block manifest.json if it exists."""
|
|
439
|
+
import json
|
|
440
|
+
from pathlib import Path
|
|
441
|
+
|
|
442
|
+
# Try common locations
|
|
443
|
+
for subdir in (f_block, f_block.lower(), f"block_{f_block}"):
|
|
444
|
+
manifest_path = Path(project_root) / ".architecture-models" / subdir / "manifest.json"
|
|
445
|
+
if manifest_path.exists():
|
|
446
|
+
try:
|
|
447
|
+
return json.loads(manifest_path.read_text())
|
|
448
|
+
except (json.JSONDecodeError, OSError):
|
|
449
|
+
pass
|
|
450
|
+
return None
|
|
451
|
+
|
|
452
|
+
|
|
453
|
+
def _format_block_manifest(manifest: dict, char_budget: int) -> str:
|
|
454
|
+
"""Format block manifest data as compact context.
|
|
455
|
+
|
|
456
|
+
Includes: module names, key function signatures, imports.
|
|
457
|
+
This fills the semantic gap that causes cross_dep failures.
|
|
458
|
+
"""
|
|
459
|
+
lines = ["\n## Block Manifest (function-level detail)"]
|
|
460
|
+
|
|
461
|
+
modules = manifest.get("modules", [])
|
|
462
|
+
for mod in modules:
|
|
463
|
+
file_path = mod.get("file", mod.get("path", "unknown"))
|
|
464
|
+
lines.append(f" {file_path}:")
|
|
465
|
+
|
|
466
|
+
# Functions with signatures (critical for cross_dep)
|
|
467
|
+
functions = mod.get("functions", [])
|
|
468
|
+
for fn in functions[:10]:
|
|
469
|
+
name = fn.get("name", "") if isinstance(fn, dict) else fn
|
|
470
|
+
sig = fn.get("signature", "") if isinstance(fn, dict) else ""
|
|
471
|
+
if name.startswith("_"):
|
|
472
|
+
continue
|
|
473
|
+
if sig:
|
|
474
|
+
lines.append(f" {name}{sig}")
|
|
475
|
+
else:
|
|
476
|
+
lines.append(f" {name}()")
|
|
477
|
+
|
|
478
|
+
# Classes
|
|
479
|
+
classes = mod.get("classes", [])
|
|
480
|
+
for cls in classes[:5]:
|
|
481
|
+
name = cls.get("name", "") if isinstance(cls, dict) else cls
|
|
482
|
+
if not name.startswith("_"):
|
|
483
|
+
lines.append(f" class {name}")
|
|
484
|
+
|
|
485
|
+
# Check budget
|
|
486
|
+
current = "\n".join(lines)
|
|
487
|
+
if len(current) >= char_budget * 0.9:
|
|
488
|
+
lines.append(" ... (truncated)")
|
|
489
|
+
break
|
|
490
|
+
|
|
491
|
+
result = "\n".join(lines)
|
|
492
|
+
return result[:char_budget]
|