ai-forge-cli 0.1.4__tar.gz → 0.1.5__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.
Files changed (32) hide show
  1. {ai_forge_cli-0.1.4/src/ai_forge_cli.egg-info → ai_forge_cli-0.1.5}/PKG-INFO +1 -1
  2. {ai_forge_cli-0.1.4 → ai_forge_cli-0.1.5}/pyproject.toml +1 -1
  3. {ai_forge_cli-0.1.4 → ai_forge_cli-0.1.5/src/ai_forge_cli.egg-info}/PKG-INFO +1 -1
  4. {ai_forge_cli-0.1.4 → ai_forge_cli-0.1.5}/src/cli/__init__.py +1 -1
  5. {ai_forge_cli-0.1.4 → ai_forge_cli-0.1.5}/src/cli/walker.py +50 -0
  6. {ai_forge_cli-0.1.4 → ai_forge_cli-0.1.5}/tests/test_walker.py +138 -0
  7. {ai_forge_cli-0.1.4 → ai_forge_cli-0.1.5}/LICENSE +0 -0
  8. {ai_forge_cli-0.1.4 → ai_forge_cli-0.1.5}/README.md +0 -0
  9. {ai_forge_cli-0.1.4 → ai_forge_cli-0.1.5}/setup.cfg +0 -0
  10. {ai_forge_cli-0.1.4 → ai_forge_cli-0.1.5}/src/ai_forge_cli.egg-info/SOURCES.txt +0 -0
  11. {ai_forge_cli-0.1.4 → ai_forge_cli-0.1.5}/src/ai_forge_cli.egg-info/dependency_links.txt +0 -0
  12. {ai_forge_cli-0.1.4 → ai_forge_cli-0.1.5}/src/ai_forge_cli.egg-info/entry_points.txt +0 -0
  13. {ai_forge_cli-0.1.4 → ai_forge_cli-0.1.5}/src/ai_forge_cli.egg-info/requires.txt +0 -0
  14. {ai_forge_cli-0.1.4 → ai_forge_cli-0.1.5}/src/ai_forge_cli.egg-info/top_level.txt +0 -0
  15. {ai_forge_cli-0.1.4 → ai_forge_cli-0.1.5}/src/cli/__main__.py +0 -0
  16. {ai_forge_cli-0.1.4 → ai_forge_cli-0.1.5}/src/cli/bundle.py +0 -0
  17. {ai_forge_cli-0.1.4 → ai_forge_cli-0.1.5}/src/cli/commands/__init__.py +0 -0
  18. {ai_forge_cli-0.1.4 → ai_forge_cli-0.1.5}/src/cli/commands/base.py +0 -0
  19. {ai_forge_cli-0.1.4 → ai_forge_cli-0.1.5}/src/cli/commands/context.py +0 -0
  20. {ai_forge_cli-0.1.4 → ai_forge_cli-0.1.5}/src/cli/commands/find.py +0 -0
  21. {ai_forge_cli-0.1.4 → ai_forge_cli-0.1.5}/src/cli/commands/init.py +0 -0
  22. {ai_forge_cli-0.1.4 → ai_forge_cli-0.1.5}/src/cli/commands/inspect.py +0 -0
  23. {ai_forge_cli-0.1.4 → ai_forge_cli-0.1.5}/src/cli/commands/list_cmd.py +0 -0
  24. {ai_forge_cli-0.1.4 → ai_forge_cli-0.1.5}/src/cli/commands/update.py +0 -0
  25. {ai_forge_cli-0.1.4 → ai_forge_cli-0.1.5}/src/cli/common.py +0 -0
  26. {ai_forge_cli-0.1.4 → ai_forge_cli-0.1.5}/src/cli/forge.py +0 -0
  27. {ai_forge_cli-0.1.4 → ai_forge_cli-0.1.5}/src/cli/index.py +0 -0
  28. {ai_forge_cli-0.1.4 → ai_forge_cli-0.1.5}/tests/test_cli.py +0 -0
  29. {ai_forge_cli-0.1.4 → ai_forge_cli-0.1.5}/tests/test_find.py +0 -0
  30. {ai_forge_cli-0.1.4 → ai_forge_cli-0.1.5}/tests/test_index.py +0 -0
  31. {ai_forge_cli-0.1.4 → ai_forge_cli-0.1.5}/tests/test_init.py +0 -0
  32. {ai_forge_cli-0.1.4 → ai_forge_cli-0.1.5}/tests/test_update.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: ai-forge-cli
3
- Version: 0.1.4
3
+ Version: 0.1.5
4
4
  Summary: Context walker for the Forge L0-L5 spec system
5
5
  Requires-Python: >=3.11
6
6
  License-File: LICENSE
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "ai-forge-cli"
7
- version = "0.1.4"
7
+ version = "0.1.5"
8
8
  description = "Context walker for the Forge L0-L5 spec system"
9
9
  requires-python = ">=3.11"
10
10
  dependencies = ["pyyaml>=6.0"]
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: ai-forge-cli
3
- Version: 0.1.4
3
+ Version: 0.1.5
4
4
  Summary: Context walker for the Forge L0-L5 spec system
5
5
  Requires-Python: >=3.11
6
6
  License-File: LICENSE
@@ -1,2 +1,2 @@
1
1
  """Forge CLI — context walker for the L0-L5 spec system."""
2
- __version__ = "0.1.4"
2
+ __version__ = "0.1.5"
@@ -113,6 +113,13 @@ def expand_atom(idx: Index, atom: Entry, unresolved: list[str]) -> OrderedDict[s
113
113
  for aid in sorted(called_atom_ids):
114
114
  called_sigs[aid] = _atom_signature(idx, aid, unresolved)
115
115
 
116
+ shaped_fields = OrderedDict()
117
+ shaped_fields[atom.id] = _atom_shaped_fields(atom.data)
118
+ for aid in sorted(called_atom_ids):
119
+ entry = idx.get(aid)
120
+ if entry and entry.kind == "atom":
121
+ shaped_fields[aid] = _atom_shaped_fields(entry.data)
122
+
116
123
  l0_slice = _build_l0_slice(
117
124
  idx,
118
125
  type_ids=type_ids,
@@ -134,6 +141,7 @@ def expand_atom(idx: Index, atom: Entry, unresolved: list[str]) -> OrderedDict[s
134
141
  bundle["policies_applied"] = policies_applied
135
142
  bundle["l3_atom"] = atom.data
136
143
  bundle["called_atom_signatures"] = called_sigs
144
+ bundle["shaped_fields"] = shaped_fields
137
145
  if training_artifact is not None:
138
146
  bundle["training_artifact"] = training_artifact
139
147
  bundle["l4_callers"] = callers
@@ -238,6 +246,7 @@ def expand_journey(idx: Index, journey: Entry, unresolved: list[str]) -> Ordered
238
246
 
239
247
  handler_atoms: OrderedDict[str, Any] = OrderedDict()
240
248
  invoked_flows: OrderedDict[str, Any] = OrderedDict()
249
+ shaped_fields: OrderedDict[str, Any] = OrderedDict()
241
250
  aggregated_type_ids: set[str] = set()
242
251
  aggregated_errors: set[str] = set()
243
252
  aggregated_constants: set[str] = set()
@@ -250,6 +259,7 @@ def expand_journey(idx: Index, journey: Entry, unresolved: list[str]) -> Ordered
250
259
  entry = idx.get(aid)
251
260
  if entry:
252
261
  handler_atoms[aid] = entry.data
262
+ shaped_fields[aid] = _atom_shaped_fields(entry.data)
253
263
  spec = entry.data.get("spec") or {}
254
264
  aggregated_type_ids |= _collect_type_ids(spec)
255
265
  aggregated_errors |= _collect_error_codes(spec)
@@ -297,6 +307,7 @@ def expand_journey(idx: Index, journey: Entry, unresolved: list[str]) -> Ordered
297
307
  bundle["l4_journey"] = journey.data
298
308
  bundle["handler_atoms"] = handler_atoms
299
309
  bundle["invoked_orchestrations"] = invoked_flows
310
+ bundle["shaped_fields"] = shaped_fields
300
311
  bundle["l5_operations"] = idx.l5
301
312
  return bundle
302
313
 
@@ -309,6 +320,7 @@ def expand_flow(idx: Index, flow: Entry, unresolved: list[str]) -> OrderedDict[s
309
320
  sequence = flow.data.get("sequence") or []
310
321
 
311
322
  step_sigs: OrderedDict[str, Any] = OrderedDict()
323
+ shaped_fields: OrderedDict[str, Any] = OrderedDict()
312
324
  aggregated_type_ids: set[str] = set()
313
325
  aggregated_errors: set[str] = set()
314
326
  aggregated_markers: set[str] = set()
@@ -326,6 +338,7 @@ def expand_flow(idx: Index, flow: Entry, unresolved: list[str]) -> OrderedDict[s
326
338
  step_sigs[aid] = _atom_signature(idx, aid, unresolved)
327
339
  entry = idx.get(aid)
328
340
  if entry and entry.kind == "atom":
341
+ shaped_fields[aid] = _atom_shaped_fields(entry.data)
329
342
  spec = entry.data.get("spec") or {}
330
343
  aggregated_type_ids |= _collect_type_ids(spec)
331
344
  aggregated_errors |= _collect_error_codes(spec)
@@ -354,6 +367,7 @@ def expand_flow(idx: Index, flow: Entry, unresolved: list[str]) -> OrderedDict[s
354
367
  bundle["l2_entry_points"] = entry_points
355
368
  bundle["l4_orchestration"] = flow.data
356
369
  bundle["step_atom_signatures"] = step_sigs
370
+ bundle["shaped_fields"] = shaped_fields
357
371
  bundle["l5_operations"] = idx.l5
358
372
  return bundle
359
373
 
@@ -392,8 +406,12 @@ def expand_artifact(idx: Index, artifact: Entry, unresolved: list[str]) -> Order
392
406
  unresolved.append(sid)
393
407
 
394
408
  consumers = OrderedDict()
409
+ shaped_fields: OrderedDict[str, Any] = OrderedDict()
395
410
  for cid in artifact.data.get("consumers") or []:
396
411
  consumers[cid] = _atom_signature(idx, cid, unresolved)
412
+ entry = idx.get(cid)
413
+ if entry and entry.kind == "atom":
414
+ shaped_fields[cid] = _atom_shaped_fields(entry.data)
397
415
 
398
416
  l0_slice = _build_l0_slice(
399
417
  idx,
@@ -415,6 +433,7 @@ def expand_artifact(idx: Index, artifact: Entry, unresolved: list[str]) -> Order
415
433
  bundle["producer_atom_signature"] = producer_sig
416
434
  bundle["source_artifacts"] = source_artifacts
417
435
  bundle["consumer_signatures"] = consumers
436
+ bundle["shaped_fields"] = shaped_fields
418
437
  return bundle
419
438
 
420
439
 
@@ -437,6 +456,37 @@ def _atom_signature(idx: Index, atom_id: str, unresolved: list[str]) -> dict[str
437
456
  }
438
457
 
439
458
 
459
+ def _atom_shaped_fields(atom_data: dict[str, Any]) -> OrderedDict[str, Any]:
460
+ spec = atom_data.get("spec") or {}
461
+ shaped = OrderedDict()
462
+ for section_name, fields in (
463
+ ("input", spec.get("input")),
464
+ ("output.success", (spec.get("output") or {}).get("success")),
465
+ ("props", spec.get("props")),
466
+ ("local_state", spec.get("local_state")),
467
+ ("input_distribution", spec.get("input_distribution")),
468
+ ("output_distribution", spec.get("output_distribution")),
469
+ ):
470
+ _collect_shaped_fields(fields, section_name, shaped)
471
+ return shaped
472
+
473
+
474
+ def _collect_shaped_fields(obj: Any, prefix: str, out: OrderedDict[str, Any]) -> None:
475
+ if not isinstance(obj, dict):
476
+ return
477
+ if "type" in obj and "shape" in obj and isinstance(obj["shape"], dict):
478
+ out[prefix] = {
479
+ "type": obj.get("type"),
480
+ "nullable": obj.get("nullable"),
481
+ "shape": obj.get("shape"),
482
+ }
483
+ return
484
+ for key, value in obj.items():
485
+ if isinstance(value, dict):
486
+ child = f"{prefix}.{key}" if prefix else key
487
+ _collect_shaped_fields(value, child, out)
488
+
489
+
440
490
  def _resolve(idx: Index, entity_id: str, expected_kind: str, unresolved: list[str]) -> Any:
441
491
  entry = idx.get(entity_id)
442
492
  if entry is None or entry.kind != expected_kind:
@@ -1,6 +1,9 @@
1
1
  """Walker tests against src/example/ fixtures."""
2
2
 
3
+ from pathlib import Path
4
+
3
5
  from cli import walker
6
+ from cli.index import Entry, Index
4
7
 
5
8
 
6
9
  # ---------- Atom ----------
@@ -66,6 +69,141 @@ def test_atom_unresolved_signatures(idx):
66
69
  assert aid in unresolved
67
70
 
68
71
 
72
+ def test_atom_context_preserves_shape_metadata_for_target_and_called_signatures():
73
+ target_input_shape = {
74
+ "kind": "tagged_union",
75
+ "discriminator": "form",
76
+ "variants": [
77
+ {"form": "broadcast", "value": "all"},
78
+ {"form": "session", "pattern": "session:<int>"},
79
+ ],
80
+ }
81
+ target_output_shape = {
82
+ "kind": "enum",
83
+ "values": ["queued", "sent"],
84
+ }
85
+ called_output_shape = {
86
+ "kind": "json_schema",
87
+ "schema": {
88
+ "type": "array",
89
+ "items": {"type": "integer"},
90
+ },
91
+ }
92
+
93
+ idx = Index(spec_dir=Path("."))
94
+ idx.l1 = {"failure": {"propagation": {}}}
95
+ idx.l5 = {}
96
+ idx.entries = {
97
+ "MSG": Entry(
98
+ id="MSG",
99
+ kind="module",
100
+ data={
101
+ "id": "MSG",
102
+ "owned_atoms": ["atm.msg.send_message", "atm.msg.resolve_targets"],
103
+ "policies": [],
104
+ "interface": {"entry_points": []},
105
+ },
106
+ ),
107
+ "atm.msg.send_message": Entry(
108
+ id="atm.msg.send_message",
109
+ kind="atom",
110
+ data={
111
+ "id": "atm.msg.send_message",
112
+ "kind": "PROCEDURAL",
113
+ "owner_module": "MSG",
114
+ "description": "Send a message to a resolved target set.",
115
+ "spec": {
116
+ "input": {
117
+ "target": {
118
+ "type": "string",
119
+ "nullable": False,
120
+ "shape": target_input_shape,
121
+ },
122
+ "body": {
123
+ "type": "string",
124
+ "nullable": False,
125
+ },
126
+ },
127
+ "output": {
128
+ "success": {
129
+ "delivery_status": {
130
+ "type": "string",
131
+ "nullable": False,
132
+ "shape": target_output_shape,
133
+ }
134
+ },
135
+ "errors": [],
136
+ },
137
+ "logic": ["CALL atm.msg.resolve_targets"],
138
+ },
139
+ },
140
+ ),
141
+ "atm.msg.resolve_targets": Entry(
142
+ id="atm.msg.resolve_targets",
143
+ kind="atom",
144
+ data={
145
+ "id": "atm.msg.resolve_targets",
146
+ "kind": "PROCEDURAL",
147
+ "owner_module": "MSG",
148
+ "description": "Resolve a target selector into session ids.",
149
+ "spec": {
150
+ "input": {
151
+ "target": {
152
+ "type": "string",
153
+ "nullable": False,
154
+ "shape": target_input_shape,
155
+ },
156
+ },
157
+ "output": {
158
+ "success": {
159
+ "resolved_targets": {
160
+ "type": "string",
161
+ "nullable": False,
162
+ "shape": called_output_shape,
163
+ }
164
+ },
165
+ "errors": [],
166
+ },
167
+ "logic": [],
168
+ },
169
+ },
170
+ ),
171
+ }
172
+
173
+ bundle, unresolved = walker.walk(idx, "atm.msg.send_message")
174
+
175
+ assert unresolved == []
176
+ assert bundle["l3_atom"]["spec"]["input"]["target"]["shape"] == target_input_shape
177
+ assert bundle["l3_atom"]["spec"]["output"]["success"]["delivery_status"]["shape"] == target_output_shape
178
+ called_sig = bundle["called_atom_signatures"]["atm.msg.resolve_targets"]
179
+ assert called_sig["input"]["target"]["shape"] == target_input_shape
180
+ assert called_sig["output"]["success"]["resolved_targets"]["shape"] == called_output_shape
181
+ assert bundle["shaped_fields"]["atm.msg.send_message"] == {
182
+ "input.target": {
183
+ "type": "string",
184
+ "nullable": False,
185
+ "shape": target_input_shape,
186
+ },
187
+ "output.success.delivery_status": {
188
+ "type": "string",
189
+ "nullable": False,
190
+ "shape": target_output_shape,
191
+ },
192
+ }
193
+ assert bundle["shaped_fields"]["atm.msg.resolve_targets"] == {
194
+ "input.target": {
195
+ "type": "string",
196
+ "nullable": False,
197
+ "shape": target_input_shape,
198
+ },
199
+ "output.success.resolved_targets": {
200
+ "type": "string",
201
+ "nullable": False,
202
+ "shape": called_output_shape,
203
+ },
204
+ }
205
+
206
+
69
207
  # ---------- Module ----------
70
208
 
71
209
  def test_module_bundle_shape(idx):
File without changes
File without changes
File without changes