ai-forge-cli 2.0.1__tar.gz → 2.0.2__tar.gz
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.
- {ai_forge_cli-2.0.1/src/ai_forge_cli.egg-info → ai_forge_cli-2.0.2}/PKG-INFO +1 -1
- {ai_forge_cli-2.0.1 → ai_forge_cli-2.0.2}/pyproject.toml +1 -1
- {ai_forge_cli-2.0.1 → ai_forge_cli-2.0.2/src/ai_forge_cli.egg-info}/PKG-INFO +1 -1
- {ai_forge_cli-2.0.1 → ai_forge_cli-2.0.2}/src/cli/commands/graph.py +271 -88
- {ai_forge_cli-2.0.1 → ai_forge_cli-2.0.2}/LICENSE +0 -0
- {ai_forge_cli-2.0.1 → ai_forge_cli-2.0.2}/README.md +0 -0
- {ai_forge_cli-2.0.1 → ai_forge_cli-2.0.2}/setup.cfg +0 -0
- {ai_forge_cli-2.0.1 → ai_forge_cli-2.0.2}/src/ai_forge_cli.egg-info/SOURCES.txt +0 -0
- {ai_forge_cli-2.0.1 → ai_forge_cli-2.0.2}/src/ai_forge_cli.egg-info/dependency_links.txt +0 -0
- {ai_forge_cli-2.0.1 → ai_forge_cli-2.0.2}/src/ai_forge_cli.egg-info/entry_points.txt +0 -0
- {ai_forge_cli-2.0.1 → ai_forge_cli-2.0.2}/src/ai_forge_cli.egg-info/requires.txt +0 -0
- {ai_forge_cli-2.0.1 → ai_forge_cli-2.0.2}/src/ai_forge_cli.egg-info/top_level.txt +0 -0
- {ai_forge_cli-2.0.1 → ai_forge_cli-2.0.2}/src/cli/__init__.py +0 -0
- {ai_forge_cli-2.0.1 → ai_forge_cli-2.0.2}/src/cli/__main__.py +0 -0
- {ai_forge_cli-2.0.1 → ai_forge_cli-2.0.2}/src/cli/bundle.py +0 -0
- {ai_forge_cli-2.0.1 → ai_forge_cli-2.0.2}/src/cli/commands/__init__.py +0 -0
- {ai_forge_cli-2.0.1 → ai_forge_cli-2.0.2}/src/cli/commands/base.py +0 -0
- {ai_forge_cli-2.0.1 → ai_forge_cli-2.0.2}/src/cli/commands/context.py +0 -0
- {ai_forge_cli-2.0.1 → ai_forge_cli-2.0.2}/src/cli/commands/find.py +0 -0
- {ai_forge_cli-2.0.1 → ai_forge_cli-2.0.2}/src/cli/commands/init.py +0 -0
- {ai_forge_cli-2.0.1 → ai_forge_cli-2.0.2}/src/cli/commands/inspect.py +0 -0
- {ai_forge_cli-2.0.1 → ai_forge_cli-2.0.2}/src/cli/commands/list_cmd.py +0 -0
- {ai_forge_cli-2.0.1 → ai_forge_cli-2.0.2}/src/cli/commands/update.py +0 -0
- {ai_forge_cli-2.0.1 → ai_forge_cli-2.0.2}/src/cli/commands/validate.py +0 -0
- {ai_forge_cli-2.0.1 → ai_forge_cli-2.0.2}/src/cli/common.py +0 -0
- {ai_forge_cli-2.0.1 → ai_forge_cli-2.0.2}/src/cli/forge.py +0 -0
- {ai_forge_cli-2.0.1 → ai_forge_cli-2.0.2}/src/cli/framework.yaml +0 -0
- {ai_forge_cli-2.0.1 → ai_forge_cli-2.0.2}/src/cli/index.py +0 -0
- {ai_forge_cli-2.0.1 → ai_forge_cli-2.0.2}/src/cli/skills/forge-build/SKILL.md +0 -0
- {ai_forge_cli-2.0.1 → ai_forge_cli-2.0.2}/src/cli/skills/forge-build/references/framework.md +0 -0
- {ai_forge_cli-2.0.1 → ai_forge_cli-2.0.2}/src/cli/skills/forge-build/references/framework.yaml +0 -0
- {ai_forge_cli-2.0.1 → ai_forge_cli-2.0.2}/src/cli/skills/forge-build/references/templates.yaml +0 -0
- {ai_forge_cli-2.0.1 → ai_forge_cli-2.0.2}/src/cli/skills/forge-cast/SKILL.md +0 -0
- {ai_forge_cli-2.0.1 → ai_forge_cli-2.0.2}/src/cli/skills/forge-cast/references/framework.md +0 -0
- {ai_forge_cli-2.0.1 → ai_forge_cli-2.0.2}/src/cli/skills/forge-cast/references/framework.yaml +0 -0
- {ai_forge_cli-2.0.1 → ai_forge_cli-2.0.2}/src/cli/skills/forge-cast/references/templates.yaml +0 -0
- {ai_forge_cli-2.0.1 → ai_forge_cli-2.0.2}/src/cli/skills/forge-design/SKILL.md +0 -0
- {ai_forge_cli-2.0.1 → ai_forge_cli-2.0.2}/src/cli/skills/forge-design/references/framework.md +0 -0
- {ai_forge_cli-2.0.1 → ai_forge_cli-2.0.2}/src/cli/skills/forge-design/references/framework.yaml +0 -0
- {ai_forge_cli-2.0.1 → ai_forge_cli-2.0.2}/src/cli/skills/forge-design/references/templates.yaml +0 -0
- {ai_forge_cli-2.0.1 → ai_forge_cli-2.0.2}/src/cli/skills/forge-review/SKILL.md +0 -0
- {ai_forge_cli-2.0.1 → ai_forge_cli-2.0.2}/src/cli/skills/forge-review/references/framework.md +0 -0
- {ai_forge_cli-2.0.1 → ai_forge_cli-2.0.2}/src/cli/skills/forge-review/references/framework.yaml +0 -0
- {ai_forge_cli-2.0.1 → ai_forge_cli-2.0.2}/src/cli/skills/forge-review/references/templates.yaml +0 -0
- {ai_forge_cli-2.0.1 → ai_forge_cli-2.0.2}/src/cli/skills/forge-spec/SKILL.md +0 -0
- {ai_forge_cli-2.0.1 → ai_forge_cli-2.0.2}/src/cli/skills/forge-spec/references/framework.md +0 -0
- {ai_forge_cli-2.0.1 → ai_forge_cli-2.0.2}/src/cli/skills/forge-spec/references/framework.yaml +0 -0
- {ai_forge_cli-2.0.1 → ai_forge_cli-2.0.2}/src/cli/skills/forge-spec/references/templates.yaml +0 -0
- {ai_forge_cli-2.0.1 → ai_forge_cli-2.0.2}/src/cli/skills/forge-validate/SKILL.md +0 -0
- {ai_forge_cli-2.0.1 → ai_forge_cli-2.0.2}/src/cli/skills/forge-validate/references/framework.md +0 -0
- {ai_forge_cli-2.0.1 → ai_forge_cli-2.0.2}/src/cli/skills/forge-validate/references/framework.yaml +0 -0
- {ai_forge_cli-2.0.1 → ai_forge_cli-2.0.2}/src/cli/skills/forge-validate/references/templates.yaml +0 -0
- {ai_forge_cli-2.0.1 → ai_forge_cli-2.0.2}/src/cli/walker.py +0 -0
- {ai_forge_cli-2.0.1 → ai_forge_cli-2.0.2}/tests/test_cli.py +0 -0
- {ai_forge_cli-2.0.1 → ai_forge_cli-2.0.2}/tests/test_find.py +0 -0
- {ai_forge_cli-2.0.1 → ai_forge_cli-2.0.2}/tests/test_index.py +0 -0
- {ai_forge_cli-2.0.1 → ai_forge_cli-2.0.2}/tests/test_init.py +0 -0
- {ai_forge_cli-2.0.1 → ai_forge_cli-2.0.2}/tests/test_update.py +0 -0
- {ai_forge_cli-2.0.1 → ai_forge_cli-2.0.2}/tests/test_walker.py +0 -0
|
@@ -24,13 +24,16 @@ DESCRIPTION = (
|
|
|
24
24
|
|
|
25
25
|
_MERMAID_CDN = "https://cdn.jsdelivr.net/npm/mermaid@11/dist/mermaid.min.js"
|
|
26
26
|
|
|
27
|
-
#
|
|
28
|
-
#
|
|
29
|
-
#
|
|
30
|
-
# Types, errors, etc. are NOT rendered as nodes.
|
|
27
|
+
# Dot-path depths for structural kinds.
|
|
28
|
+
# conception.system.domain.module.element = 5 parts
|
|
29
|
+
# conception.system.domain.module = 4 parts
|
|
31
30
|
_ELEMENT_DEPTH = 5
|
|
32
31
|
_MODULE_DEPTH = 4
|
|
33
32
|
|
|
33
|
+
# Kinds rendered as standalone nodes (not subgraph containers).
|
|
34
|
+
# Contracts are shown purely as labelled edges between modules, not as nodes.
|
|
35
|
+
_PERIPHERAL_KINDS = frozenset({"datastore"})
|
|
36
|
+
|
|
34
37
|
|
|
35
38
|
# ===========================================================================
|
|
36
39
|
# Argparse
|
|
@@ -51,6 +54,13 @@ def register(sub: argparse._SubParsersAction) -> None:
|
|
|
51
54
|
"--scope", default=None, metavar="NODE_ID",
|
|
52
55
|
help="Limit graph to the subgraph reachable from this node id.",
|
|
53
56
|
)
|
|
57
|
+
p.add_argument(
|
|
58
|
+
"--show-deployments", action="store_true",
|
|
59
|
+
help=(
|
|
60
|
+
"Group modules inside their deployment environment instead of their "
|
|
61
|
+
"domain. Shows which environment each module is deployed to."
|
|
62
|
+
),
|
|
63
|
+
)
|
|
54
64
|
p.add_argument(
|
|
55
65
|
"--no-open", action="store_true",
|
|
56
66
|
help="Do not auto-open the HTML file after writing.",
|
|
@@ -76,14 +86,18 @@ def run(args: argparse.Namespace) -> int:
|
|
|
76
86
|
return 1
|
|
77
87
|
edges = _filter_to_scope(edges, args.scope)
|
|
78
88
|
|
|
79
|
-
mermaid_text = _render_mermaid(idx, edges)
|
|
89
|
+
mermaid_text = _render_mermaid(idx, edges, show_deployments=args.show_deployments)
|
|
80
90
|
|
|
81
91
|
if args.format == "mermaid":
|
|
82
92
|
print(mermaid_text)
|
|
83
93
|
return 0
|
|
84
94
|
|
|
85
|
-
html = _render_html(
|
|
86
|
-
|
|
95
|
+
html = _render_html(
|
|
96
|
+
mermaid_text,
|
|
97
|
+
scope_label=args.scope,
|
|
98
|
+
conception=idx.conception_name,
|
|
99
|
+
show_deployments=args.show_deployments,
|
|
100
|
+
)
|
|
87
101
|
out_path = Path(args.output).expanduser().resolve()
|
|
88
102
|
try:
|
|
89
103
|
out_path.write_text(html, encoding="utf-8")
|
|
@@ -132,7 +146,7 @@ def _collect_edges(idx: Any) -> list[Edge]:
|
|
|
132
146
|
continue
|
|
133
147
|
data = entry.data
|
|
134
148
|
|
|
135
|
-
#
|
|
149
|
+
# Contracts: producer module → consumer modules
|
|
136
150
|
if entry.kind == "contract":
|
|
137
151
|
producer = data.get("producer")
|
|
138
152
|
consumers = data.get("consumers") or []
|
|
@@ -142,19 +156,18 @@ def _collect_edges(idx: Any) -> list[Edge]:
|
|
|
142
156
|
if isinstance(consumer, str) and consumer:
|
|
143
157
|
edges.add(Edge(producer, consumer, label, "contract"))
|
|
144
158
|
|
|
145
|
-
#
|
|
159
|
+
# Interactions: caller op → callee op (resolved to parent elements)
|
|
146
160
|
elif entry.kind == "interaction":
|
|
147
161
|
caller = data.get("caller")
|
|
148
162
|
callee = data.get("callee")
|
|
149
163
|
label = data.get("name") or entry_id.split(".")[-1]
|
|
150
164
|
if caller and callee:
|
|
151
|
-
# Simplify operation IDs to their parent element IDs
|
|
152
165
|
src = _parent_element(str(caller), idx)
|
|
153
166
|
dst = _parent_element(str(callee), idx)
|
|
154
167
|
if src and dst and src != dst:
|
|
155
168
|
edges.add(Edge(src, dst, label, "interaction"))
|
|
156
169
|
|
|
157
|
-
#
|
|
170
|
+
# Element relationships
|
|
158
171
|
elif entry.kind == "element":
|
|
159
172
|
for rel in (data.get("relationships") or []):
|
|
160
173
|
if not isinstance(rel, dict):
|
|
@@ -164,7 +177,7 @@ def _collect_edges(idx: Any) -> list[Edge]:
|
|
|
164
177
|
if isinstance(target, str) and target:
|
|
165
178
|
edges.add(Edge(entry_id, target, label, "relationship"))
|
|
166
179
|
|
|
167
|
-
#
|
|
180
|
+
# Datastores: consumer modules → datastore node
|
|
168
181
|
elif entry.kind == "datastore":
|
|
169
182
|
label = data.get("name") or entry_id.split(".")[-1]
|
|
170
183
|
for consumer in (data.get("consumers") or []):
|
|
@@ -175,11 +188,7 @@ def _collect_edges(idx: Any) -> list[Edge]:
|
|
|
175
188
|
|
|
176
189
|
|
|
177
190
|
def _parent_element(node_id: str, idx: Any) -> str | None:
|
|
178
|
-
"""Given an operation/property ID, return the parent element ID.
|
|
179
|
-
|
|
180
|
-
Falls back gracefully: if the node itself is an element, return it.
|
|
181
|
-
If the node isn't in the index at all, try truncating to element depth.
|
|
182
|
-
"""
|
|
191
|
+
"""Given an operation/property ID, return the parent element ID."""
|
|
183
192
|
entry = idx.get(node_id)
|
|
184
193
|
if entry is None:
|
|
185
194
|
parts = node_id.split(".")
|
|
@@ -191,7 +200,6 @@ def _parent_element(node_id: str, idx: Any) -> str | None:
|
|
|
191
200
|
if entry.kind == "element":
|
|
192
201
|
return node_id
|
|
193
202
|
if entry.kind in ("operation", "property"):
|
|
194
|
-
# Strip last segment to get the parent element
|
|
195
203
|
parts = node_id.split(".")
|
|
196
204
|
if len(parts) > _ELEMENT_DEPTH:
|
|
197
205
|
return ".".join(parts[:_ELEMENT_DEPTH])
|
|
@@ -224,7 +232,7 @@ def _filter_to_scope(edges: list[Edge], scope_id: str) -> list[Edge]:
|
|
|
224
232
|
|
|
225
233
|
|
|
226
234
|
# ===========================================================================
|
|
227
|
-
# Mermaid rendering
|
|
235
|
+
# Mermaid rendering — helpers
|
|
228
236
|
# ===========================================================================
|
|
229
237
|
|
|
230
238
|
def _safe_id(node_id: str) -> str:
|
|
@@ -232,13 +240,11 @@ def _safe_id(node_id: str) -> str:
|
|
|
232
240
|
|
|
233
241
|
|
|
234
242
|
def _node_shape(node_id: str, idx: Any) -> tuple[str, str]:
|
|
235
|
-
"""Return (open_bracket, close_bracket) for Mermaid node shape."""
|
|
236
243
|
entry = idx.get(node_id)
|
|
237
244
|
if entry is None:
|
|
238
245
|
return ("[", "]")
|
|
239
246
|
shapes = {
|
|
240
247
|
"element": ("[", "]"),
|
|
241
|
-
"module": ("([", "])"),
|
|
242
248
|
"datastore": ("[(", ")]"),
|
|
243
249
|
"contract": ("{", "}"),
|
|
244
250
|
}
|
|
@@ -252,13 +258,17 @@ def _node_label(node_id: str, idx: Any) -> str:
|
|
|
252
258
|
data = entry.data if isinstance(entry.data, dict) else {}
|
|
253
259
|
name = data.get("name") or node_id.split(".")[-1]
|
|
254
260
|
kind = entry.kind or ""
|
|
255
|
-
|
|
261
|
+
if kind == "datastore":
|
|
262
|
+
engine = data.get("engine") or data.get("kind") or ""
|
|
263
|
+
subtitle = f"{engine} · {kind}" if engine else kind
|
|
264
|
+
else:
|
|
265
|
+
subtitle = kind
|
|
266
|
+
return f"{name}<br/><small>{subtitle}</small>"
|
|
256
267
|
|
|
257
268
|
|
|
258
269
|
def _domain_of(node_id: str, idx: Any) -> str | None:
|
|
259
|
-
"""Return the domain ID for
|
|
270
|
+
"""Return the domain ID (3-part dot-path) for a module/element, or None."""
|
|
260
271
|
parts = node_id.split(".")
|
|
261
|
-
# conception.system.domain = 3 parts
|
|
262
272
|
if len(parts) >= 3:
|
|
263
273
|
candidate = ".".join(parts[:3])
|
|
264
274
|
e = idx.get(candidate)
|
|
@@ -267,48 +277,173 @@ def _domain_of(node_id: str, idx: Any) -> str | None:
|
|
|
267
277
|
return None
|
|
268
278
|
|
|
269
279
|
|
|
270
|
-
def
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
for
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
for
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
280
|
+
def _build_module_elements(idx: Any) -> dict[str, list[str]]:
|
|
281
|
+
"""Map module_id → sorted list of element_ids that belong to it."""
|
|
282
|
+
result: dict[str, list[str]] = {}
|
|
283
|
+
for entry in idx.entries.values():
|
|
284
|
+
if entry.kind != "element":
|
|
285
|
+
continue
|
|
286
|
+
parts = entry.id.split(".")
|
|
287
|
+
if len(parts) == _ELEMENT_DEPTH:
|
|
288
|
+
module_id = ".".join(parts[:_MODULE_DEPTH])
|
|
289
|
+
result.setdefault(module_id, []).append(entry.id)
|
|
290
|
+
return {k: sorted(v) for k, v in result.items()}
|
|
291
|
+
|
|
292
|
+
|
|
293
|
+
def _build_env_modules(idx: Any) -> dict[str, list[str]]:
|
|
294
|
+
"""Map environment_id → sorted list of module_ids deployed there."""
|
|
295
|
+
result: dict[str, list[str]] = {}
|
|
296
|
+
for entry in idx.entries.values():
|
|
297
|
+
if entry.kind != "deployment" or not isinstance(entry.data, dict):
|
|
298
|
+
continue
|
|
299
|
+
module = entry.data.get("module")
|
|
300
|
+
env = entry.data.get("environment")
|
|
301
|
+
if module and env:
|
|
302
|
+
result.setdefault(str(env), []).append(str(module))
|
|
303
|
+
return {k: sorted(set(v)) for k, v in result.items()}
|
|
304
|
+
|
|
305
|
+
|
|
306
|
+
def _emit_module_subgraph(
|
|
307
|
+
lines: list[str],
|
|
308
|
+
idx: Any,
|
|
309
|
+
module_id: str,
|
|
310
|
+
module_elements: dict[str, list[str]],
|
|
311
|
+
depth: int,
|
|
312
|
+
) -> None:
|
|
313
|
+
"""Emit a module as a Mermaid subgraph containing its element nodes."""
|
|
314
|
+
pad = " " * depth
|
|
315
|
+
entry = idx.get(module_id)
|
|
316
|
+
name = (
|
|
317
|
+
(entry.data.get("name") if entry and isinstance(entry.data, dict) else None)
|
|
318
|
+
or module_id.split(".")[-1]
|
|
319
|
+
)
|
|
320
|
+
lines.append(f'{pad}subgraph {_safe_id(module_id)}["{name}"]')
|
|
321
|
+
for elem_id in module_elements.get(module_id, []):
|
|
322
|
+
o, c = _node_shape(elem_id, idx)
|
|
323
|
+
label = _node_label(elem_id, idx)
|
|
324
|
+
lines.append(f'{pad} {_safe_id(elem_id)}{o}"{label}"{c}')
|
|
325
|
+
lines.append(f'{pad}end')
|
|
326
|
+
|
|
327
|
+
|
|
328
|
+
def _emit_peripheral_nodes(lines: list[str], idx: Any) -> None:
|
|
329
|
+
"""Emit datastore and contract nodes (standalone, not inside any subgraph)."""
|
|
330
|
+
for entry in sorted(idx.entries.values(), key=lambda e: e.id):
|
|
331
|
+
if entry.kind not in _PERIPHERAL_KINDS:
|
|
332
|
+
continue
|
|
333
|
+
o, c = _node_shape(entry.id, idx)
|
|
334
|
+
label = _node_label(entry.id, idx)
|
|
335
|
+
lines.append(f' {_safe_id(entry.id)}{o}"{label}"{c}')
|
|
336
|
+
|
|
337
|
+
|
|
338
|
+
def _emit_domain_groups(
|
|
339
|
+
lines: list[str], idx: Any, module_elements: dict[str, list[str]]
|
|
340
|
+
) -> None:
|
|
341
|
+
"""Emit domain subgraphs → module subgraphs → element nodes."""
|
|
342
|
+
all_modules = sorted(e.id for e in idx.entries.values() if e.kind == "module")
|
|
343
|
+
|
|
344
|
+
domain_modules: dict[str, list[str]] = {}
|
|
345
|
+
ungrouped_modules: list[str] = []
|
|
346
|
+
for m in all_modules:
|
|
347
|
+
d = _domain_of(m, idx)
|
|
348
|
+
if d:
|
|
349
|
+
domain_modules.setdefault(d, []).append(m)
|
|
284
350
|
else:
|
|
285
|
-
|
|
351
|
+
ungrouped_modules.append(m)
|
|
286
352
|
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
# Emit domain subgraphs
|
|
290
|
-
for domain_id in sorted(domain_nodes):
|
|
353
|
+
for domain_id in sorted(domain_modules):
|
|
291
354
|
entry = idx.get(domain_id)
|
|
292
|
-
|
|
355
|
+
label = (
|
|
293
356
|
(entry.data.get("name") if entry and isinstance(entry.data, dict) else None)
|
|
294
357
|
or domain_id.split(".")[-1]
|
|
295
358
|
)
|
|
296
|
-
lines.append(f' subgraph {_safe_id(domain_id)}["{
|
|
297
|
-
for
|
|
298
|
-
|
|
299
|
-
label = _node_label(node, idx)
|
|
300
|
-
lines.append(f' {_safe_id(node)}{o}"{label}"{c}')
|
|
359
|
+
lines.append(f' subgraph {_safe_id(domain_id)}["{label}"]')
|
|
360
|
+
for module_id in sorted(domain_modules[domain_id]):
|
|
361
|
+
_emit_module_subgraph(lines, idx, module_id, module_elements, depth=2)
|
|
301
362
|
lines.append(" end")
|
|
302
363
|
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
364
|
+
for module_id in sorted(ungrouped_modules):
|
|
365
|
+
_emit_module_subgraph(lines, idx, module_id, module_elements, depth=1)
|
|
366
|
+
|
|
367
|
+
_emit_peripheral_nodes(lines, idx)
|
|
368
|
+
|
|
369
|
+
|
|
370
|
+
def _emit_env_groups(
|
|
371
|
+
lines: list[str], idx: Any, module_elements: dict[str, list[str]]
|
|
372
|
+
) -> None:
|
|
373
|
+
"""Emit environment subgraphs → module subgraphs → element nodes."""
|
|
374
|
+
env_modules = _build_env_modules(idx)
|
|
375
|
+
deployed: set[str] = {m for ms in env_modules.values() for m in ms}
|
|
376
|
+
all_modules = sorted(e.id for e in idx.entries.values() if e.kind == "module")
|
|
377
|
+
|
|
378
|
+
for env_id in sorted(env_modules):
|
|
379
|
+
entry = idx.get(env_id)
|
|
380
|
+
data = entry.data if entry and isinstance(entry.data, dict) else {}
|
|
381
|
+
name = data.get("name") or env_id.split(".")[-1]
|
|
382
|
+
kind_label = data.get("kind") or "environment"
|
|
383
|
+
lines.append(f' subgraph {_safe_id(env_id)}["{name} ({kind_label})"]')
|
|
384
|
+
for module_id in env_modules[env_id]:
|
|
385
|
+
_emit_module_subgraph(lines, idx, module_id, module_elements, depth=2)
|
|
386
|
+
lines.append(" end")
|
|
387
|
+
|
|
388
|
+
for module_id in sorted(m for m in all_modules if m not in deployed):
|
|
389
|
+
_emit_module_subgraph(lines, idx, module_id, module_elements, depth=1)
|
|
390
|
+
|
|
391
|
+
_emit_peripheral_nodes(lines, idx)
|
|
392
|
+
|
|
393
|
+
|
|
394
|
+
# ===========================================================================
|
|
395
|
+
# Mermaid rendering — styles
|
|
396
|
+
# ===========================================================================
|
|
397
|
+
|
|
398
|
+
# Colour palette (works over Mermaid dark theme).
|
|
399
|
+
_C_OUTER = "fill:#0d1829,stroke:#1e3a5e,color:#64748b" # domain / environment
|
|
400
|
+
_C_MODULE = "fill:#1a2d4a,stroke:#2563eb,color:#93c5fd" # module subgraph
|
|
401
|
+
_C_ELEMENT = "fill:#0f2235,stroke:#4a9ef8,color:#bfdbfe" # element node
|
|
402
|
+
_C_STORE = "fill:#0a1f14,stroke:#059669,color:#6ee7b7" # datastore node
|
|
403
|
+
|
|
404
|
+
|
|
405
|
+
def _emit_styles(lines: list[str], idx: Any, show_deployments: bool) -> None:
|
|
406
|
+
lines.append("")
|
|
407
|
+
lines.append(f" classDef elementNode {_C_ELEMENT}")
|
|
408
|
+
lines.append(f" classDef datastoreNode {_C_STORE}")
|
|
409
|
+
|
|
410
|
+
elem_ids = sorted(
|
|
411
|
+
_safe_id(e.id)
|
|
412
|
+
for e in idx.entries.values()
|
|
413
|
+
if e.kind == "element" and len(e.id.split(".")) == _ELEMENT_DEPTH
|
|
414
|
+
)
|
|
415
|
+
if elem_ids:
|
|
416
|
+
lines.append(f" class {','.join(elem_ids)} elementNode")
|
|
417
|
+
|
|
418
|
+
ds_ids = sorted(_safe_id(e.id) for e in idx.entries.values() if e.kind == "datastore")
|
|
419
|
+
if ds_ids:
|
|
420
|
+
lines.append(f" class {','.join(ds_ids)} datastoreNode")
|
|
421
|
+
|
|
422
|
+
outer_kind = "environment" if show_deployments else "domain"
|
|
423
|
+
for e in sorted(idx.entries.values(), key=lambda x: x.id):
|
|
424
|
+
if e.kind == outer_kind:
|
|
425
|
+
lines.append(f" style {_safe_id(e.id)} {_C_OUTER}")
|
|
426
|
+
for e in sorted(idx.entries.values(), key=lambda x: x.id):
|
|
427
|
+
if e.kind == "module":
|
|
428
|
+
lines.append(f" style {_safe_id(e.id)} {_C_MODULE}")
|
|
429
|
+
|
|
430
|
+
|
|
431
|
+
# ===========================================================================
|
|
432
|
+
# Mermaid rendering — main
|
|
433
|
+
# ===========================================================================
|
|
434
|
+
|
|
435
|
+
def _render_mermaid(idx: Any, edges: list[Edge], show_deployments: bool = False) -> str:
|
|
436
|
+
module_elements = _build_module_elements(idx)
|
|
437
|
+
|
|
438
|
+
lines: list[str] = ["graph LR"]
|
|
439
|
+
|
|
440
|
+
if show_deployments:
|
|
441
|
+
_emit_env_groups(lines, idx, module_elements)
|
|
442
|
+
else:
|
|
443
|
+
_emit_domain_groups(lines, idx, module_elements)
|
|
308
444
|
|
|
309
445
|
lines.append("")
|
|
310
446
|
|
|
311
|
-
# Emit edges — dashed for interactions, solid for everything else
|
|
312
447
|
seen: set[tuple[str, str, str]] = set()
|
|
313
448
|
for e in edges:
|
|
314
449
|
src = _safe_id(e.src)
|
|
@@ -328,6 +463,8 @@ def _render_mermaid(idx: Any, edges: list[Edge]) -> str:
|
|
|
328
463
|
|
|
329
464
|
lines.append(f" {src} {arrow} {dst}")
|
|
330
465
|
|
|
466
|
+
_emit_styles(lines, idx, show_deployments)
|
|
467
|
+
|
|
331
468
|
return "\n".join(lines) + "\n"
|
|
332
469
|
|
|
333
470
|
|
|
@@ -335,25 +472,76 @@ def _render_mermaid(idx: Any, edges: list[Edge]) -> str:
|
|
|
335
472
|
# HTML rendering
|
|
336
473
|
# ===========================================================================
|
|
337
474
|
|
|
338
|
-
def _render_html(
|
|
475
|
+
def _render_html(
|
|
476
|
+
mermaid_text: str,
|
|
477
|
+
scope_label: str | None,
|
|
478
|
+
conception: str,
|
|
479
|
+
show_deployments: bool = False,
|
|
480
|
+
) -> str:
|
|
339
481
|
title = f"Forge Graph — {conception}"
|
|
340
482
|
if scope_label:
|
|
341
483
|
title += f" ({scope_label})"
|
|
342
484
|
|
|
343
485
|
indented = textwrap.indent(mermaid_text, " ")
|
|
344
486
|
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
m(["MyModule\\nmodule"])
|
|
351
|
-
end
|
|
352
|
-
d[("MyStore\\ndatastore")]
|
|
353
|
-
e -->|"contract / relationship"| m
|
|
354
|
-
m -->|"datastore name"| d
|
|
355
|
-
e -. "interaction" .-> m
|
|
487
|
+
_key_styles = textwrap.dedent(f"""\
|
|
488
|
+
classDef elementNode {_C_ELEMENT}
|
|
489
|
+
classDef datastoreNode {_C_STORE}
|
|
490
|
+
class eA,eB elementNode
|
|
491
|
+
class ds datastoreNode
|
|
356
492
|
""")
|
|
493
|
+
|
|
494
|
+
if show_deployments:
|
|
495
|
+
key_diagram = textwrap.dedent("""\
|
|
496
|
+
graph LR
|
|
497
|
+
subgraph envA["Production (production)"]
|
|
498
|
+
subgraph modA["ServiceA"]
|
|
499
|
+
eA["ElemA\\nelement"]
|
|
500
|
+
end
|
|
501
|
+
subgraph modB["ServiceB"]
|
|
502
|
+
eB["ElemB\\nelement"]
|
|
503
|
+
end
|
|
504
|
+
end
|
|
505
|
+
ds[("DataStore\\ndatastore")]
|
|
506
|
+
modA -->|"contract"| modB
|
|
507
|
+
eA -. "interaction" .-> eB
|
|
508
|
+
modB -->|"store name"| ds
|
|
509
|
+
""") + textwrap.indent(_key_styles, " ") + textwrap.dedent(f"""\
|
|
510
|
+
style envA {_C_OUTER}
|
|
511
|
+
style modA {_C_MODULE}
|
|
512
|
+
style modB {_C_MODULE}
|
|
513
|
+
""")
|
|
514
|
+
outer_label = "Environment (outer border)"
|
|
515
|
+
outer_desc = (
|
|
516
|
+
"A deployment target (production, staging, …). "
|
|
517
|
+
"The outer border groups the modules deployed there."
|
|
518
|
+
)
|
|
519
|
+
else:
|
|
520
|
+
key_diagram = textwrap.dedent("""\
|
|
521
|
+
graph LR
|
|
522
|
+
subgraph domain["Domain"]
|
|
523
|
+
subgraph modA["ServiceA"]
|
|
524
|
+
eA["ElemA\\nelement"]
|
|
525
|
+
end
|
|
526
|
+
subgraph modB["ServiceB"]
|
|
527
|
+
eB["ElemB\\nelement"]
|
|
528
|
+
end
|
|
529
|
+
end
|
|
530
|
+
ds[("DataStore\\ndatastore")]
|
|
531
|
+
modA -->|"contract"| modB
|
|
532
|
+
eA -. "interaction" .-> eB
|
|
533
|
+
modB -->|"store name"| ds
|
|
534
|
+
""") + textwrap.indent(_key_styles, " ") + textwrap.dedent(f"""\
|
|
535
|
+
style domain {_C_OUTER}
|
|
536
|
+
style modA {_C_MODULE}
|
|
537
|
+
style modB {_C_MODULE}
|
|
538
|
+
""")
|
|
539
|
+
outer_label = "Domain (outer border)"
|
|
540
|
+
outer_desc = (
|
|
541
|
+
"A bounded area of responsibility. "
|
|
542
|
+
"Groups related modules and their elements within a system."
|
|
543
|
+
)
|
|
544
|
+
|
|
357
545
|
key_indented = textwrap.indent(key_diagram, " ")
|
|
358
546
|
|
|
359
547
|
return f"""\
|
|
@@ -409,41 +597,38 @@ def _render_html(mermaid_text: str, scope_label: str | None, conception: str) ->
|
|
|
409
597
|
background: #1a1f2e;
|
|
410
598
|
border: 1px solid #2d3548;
|
|
411
599
|
border-radius: 10px;
|
|
412
|
-
padding:
|
|
600
|
+
padding: 2rem 2.5rem 2.25rem;
|
|
413
601
|
display: grid;
|
|
414
602
|
grid-template-columns: auto 1fr;
|
|
415
|
-
gap: 1.
|
|
603
|
+
gap: 1.75rem 3.5rem;
|
|
416
604
|
align-items: start;
|
|
417
605
|
}}
|
|
418
606
|
.key h3 {{
|
|
419
607
|
grid-column: 1 / -1;
|
|
420
|
-
font-size: 0.
|
|
608
|
+
font-size: 0.75rem;
|
|
421
609
|
font-weight: 700;
|
|
422
610
|
text-transform: uppercase;
|
|
423
611
|
letter-spacing: 0.1em;
|
|
424
612
|
color: #64748b;
|
|
425
|
-
margin-bottom: -0.
|
|
426
|
-
}}
|
|
427
|
-
.key-diagram {{
|
|
428
|
-
/* let Mermaid render naturally; constrain width so it doesn't expand */
|
|
429
|
-
max-width: 480px;
|
|
613
|
+
margin-bottom: -0.5rem;
|
|
430
614
|
}}
|
|
615
|
+
.key-diagram {{ min-width: 640px; }}
|
|
431
616
|
.key-descriptions {{
|
|
432
617
|
display: flex;
|
|
433
618
|
flex-direction: column;
|
|
434
|
-
gap:
|
|
435
|
-
padding-top: 0.
|
|
619
|
+
gap: 1rem;
|
|
620
|
+
padding-top: 0.5rem;
|
|
436
621
|
}}
|
|
437
622
|
.key-row {{
|
|
438
623
|
display: flex;
|
|
439
|
-
gap: 0.
|
|
440
|
-
font-size: 0.
|
|
441
|
-
line-height: 1.
|
|
624
|
+
gap: 0.65rem;
|
|
625
|
+
font-size: 0.85rem;
|
|
626
|
+
line-height: 1.5;
|
|
442
627
|
}}
|
|
443
628
|
.key-row .bullet {{
|
|
444
629
|
color: #475569;
|
|
445
630
|
flex-shrink: 0;
|
|
446
|
-
margin-top: 0.
|
|
631
|
+
margin-top: 0.1rem;
|
|
447
632
|
}}
|
|
448
633
|
.key-row strong {{ color: #cbd5e1; font-weight: 600; }}
|
|
449
634
|
.key-row span {{ color: #64748b; }}
|
|
@@ -466,34 +651,32 @@ def _render_html(mermaid_text: str, scope_label: str | None, conception: str) ->
|
|
|
466
651
|
<div class="key">
|
|
467
652
|
<h3>Key</h3>
|
|
468
653
|
|
|
469
|
-
<!-- left: a real Mermaid diagram so shapes match exactly -->
|
|
470
654
|
<div class="key-diagram">
|
|
471
655
|
<div class="mermaid">
|
|
472
656
|
{key_indented}
|
|
473
657
|
</div>
|
|
474
658
|
</div>
|
|
475
659
|
|
|
476
|
-
<!-- right: plain-English descriptions aligned to the diagram nodes/edges -->
|
|
477
660
|
<div class="key-descriptions">
|
|
478
661
|
<div class="key-row">
|
|
479
662
|
<span class="bullet">▸</span>
|
|
480
|
-
<div><strong>
|
|
663
|
+
<div><strong>{outer_label}</strong> <span>— {outer_desc}</span></div>
|
|
481
664
|
</div>
|
|
482
665
|
<div class="key-row">
|
|
483
666
|
<span class="bullet">▸</span>
|
|
484
|
-
<div><strong>Module</strong> <span>— A deployable artifact (service, worker, function…) that packages one or more elements. Maps to a repo and a deployment unit.</span></div>
|
|
667
|
+
<div><strong>Module (inner border)</strong> <span>— A deployable artifact (service, worker, function…) that packages one or more elements. Maps to a repo and a deployment unit.</span></div>
|
|
485
668
|
</div>
|
|
486
669
|
<div class="key-row">
|
|
487
670
|
<span class="bullet">▸</span>
|
|
488
|
-
<div><strong>
|
|
671
|
+
<div><strong>Element</strong> <span>— The core implementation unit (aggregate, entity, value object, service, or projection). What developers actually build and own.</span></div>
|
|
489
672
|
</div>
|
|
490
673
|
<div class="key-row">
|
|
491
674
|
<span class="bullet">▸</span>
|
|
492
|
-
<div><strong>
|
|
675
|
+
<div><strong>Datastore</strong> <span>— A database, cache, queue, or object store. The connecting arrow is labelled with the datastore name.</span></div>
|
|
493
676
|
</div>
|
|
494
677
|
<div class="key-row">
|
|
495
678
|
<span class="bullet">▸</span>
|
|
496
|
-
<div><strong>Solid arrow</strong> <span>— A contract
|
|
679
|
+
<div><strong>Solid arrow</strong> <span>— A contract reference (producer → consumer, labelled with the contract name) or a structural element relationship.</span></div>
|
|
497
680
|
</div>
|
|
498
681
|
<div class="key-row">
|
|
499
682
|
<span class="bullet">▸</span>
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{ai_forge_cli-2.0.1 → ai_forge_cli-2.0.2}/src/cli/skills/forge-build/references/framework.md
RENAMED
|
File without changes
|
{ai_forge_cli-2.0.1 → ai_forge_cli-2.0.2}/src/cli/skills/forge-build/references/framework.yaml
RENAMED
|
File without changes
|
{ai_forge_cli-2.0.1 → ai_forge_cli-2.0.2}/src/cli/skills/forge-build/references/templates.yaml
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
{ai_forge_cli-2.0.1 → ai_forge_cli-2.0.2}/src/cli/skills/forge-cast/references/framework.yaml
RENAMED
|
File without changes
|
{ai_forge_cli-2.0.1 → ai_forge_cli-2.0.2}/src/cli/skills/forge-cast/references/templates.yaml
RENAMED
|
File without changes
|
|
File without changes
|
{ai_forge_cli-2.0.1 → ai_forge_cli-2.0.2}/src/cli/skills/forge-design/references/framework.md
RENAMED
|
File without changes
|
{ai_forge_cli-2.0.1 → ai_forge_cli-2.0.2}/src/cli/skills/forge-design/references/framework.yaml
RENAMED
|
File without changes
|
{ai_forge_cli-2.0.1 → ai_forge_cli-2.0.2}/src/cli/skills/forge-design/references/templates.yaml
RENAMED
|
File without changes
|
|
File without changes
|
{ai_forge_cli-2.0.1 → ai_forge_cli-2.0.2}/src/cli/skills/forge-review/references/framework.md
RENAMED
|
File without changes
|
{ai_forge_cli-2.0.1 → ai_forge_cli-2.0.2}/src/cli/skills/forge-review/references/framework.yaml
RENAMED
|
File without changes
|
{ai_forge_cli-2.0.1 → ai_forge_cli-2.0.2}/src/cli/skills/forge-review/references/templates.yaml
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
{ai_forge_cli-2.0.1 → ai_forge_cli-2.0.2}/src/cli/skills/forge-spec/references/framework.yaml
RENAMED
|
File without changes
|
{ai_forge_cli-2.0.1 → ai_forge_cli-2.0.2}/src/cli/skills/forge-spec/references/templates.yaml
RENAMED
|
File without changes
|
|
File without changes
|
{ai_forge_cli-2.0.1 → ai_forge_cli-2.0.2}/src/cli/skills/forge-validate/references/framework.md
RENAMED
|
File without changes
|
{ai_forge_cli-2.0.1 → ai_forge_cli-2.0.2}/src/cli/skills/forge-validate/references/framework.yaml
RENAMED
|
File without changes
|
{ai_forge_cli-2.0.1 → ai_forge_cli-2.0.2}/src/cli/skills/forge-validate/references/templates.yaml
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|