gds-framework 0.2.2__tar.gz → 0.2.3__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 (56) hide show
  1. {gds_framework-0.2.2 → gds_framework-0.2.3}/PKG-INFO +1 -1
  2. {gds_framework-0.2.2 → gds_framework-0.2.3}/gds/__init__.py +18 -2
  3. gds_framework-0.2.3/gds/compiler/__init__.py +19 -0
  4. {gds_framework-0.2.2 → gds_framework-0.2.3}/gds/compiler/compile.py +184 -62
  5. {gds_framework-0.2.2 → gds_framework-0.2.3}/gds/ir/models.py +30 -2
  6. {gds_framework-0.2.2 → gds_framework-0.2.3}/gds/verification/generic_checks.py +90 -19
  7. {gds_framework-0.2.2 → gds_framework-0.2.3}/pyproject.toml +4 -1
  8. gds_framework-0.2.3/tests/test_compiler.py +456 -0
  9. {gds_framework-0.2.2 → gds_framework-0.2.3}/tests/test_verification.py +105 -6
  10. gds_framework-0.2.2/gds/compiler/__init__.py +0 -5
  11. gds_framework-0.2.2/tests/test_compiler.py +0 -223
  12. {gds_framework-0.2.2 → gds_framework-0.2.3}/.gitignore +0 -0
  13. {gds_framework-0.2.2 → gds_framework-0.2.3}/CITATION.cff +0 -0
  14. {gds_framework-0.2.2 → gds_framework-0.2.3}/CLAUDE.md +0 -0
  15. {gds_framework-0.2.2 → gds_framework-0.2.3}/LICENSE +0 -0
  16. {gds_framework-0.2.2 → gds_framework-0.2.3}/README.md +0 -0
  17. {gds_framework-0.2.2 → gds_framework-0.2.3}/gds/blocks/__init__.py +0 -0
  18. {gds_framework-0.2.2 → gds_framework-0.2.3}/gds/blocks/base.py +0 -0
  19. {gds_framework-0.2.2 → gds_framework-0.2.3}/gds/blocks/composition.py +0 -0
  20. {gds_framework-0.2.2 → gds_framework-0.2.3}/gds/blocks/errors.py +0 -0
  21. {gds_framework-0.2.2 → gds_framework-0.2.3}/gds/blocks/roles.py +0 -0
  22. {gds_framework-0.2.2 → gds_framework-0.2.3}/gds/canonical.py +0 -0
  23. {gds_framework-0.2.2 → gds_framework-0.2.3}/gds/helpers.py +0 -0
  24. {gds_framework-0.2.2 → gds_framework-0.2.3}/gds/ir/__init__.py +0 -0
  25. {gds_framework-0.2.2 → gds_framework-0.2.3}/gds/ir/serialization.py +0 -0
  26. {gds_framework-0.2.2 → gds_framework-0.2.3}/gds/parameters.py +0 -0
  27. {gds_framework-0.2.2 → gds_framework-0.2.3}/gds/py.typed +0 -0
  28. {gds_framework-0.2.2 → gds_framework-0.2.3}/gds/query.py +0 -0
  29. {gds_framework-0.2.2 → gds_framework-0.2.3}/gds/serialize.py +0 -0
  30. {gds_framework-0.2.2 → gds_framework-0.2.3}/gds/spaces.py +0 -0
  31. {gds_framework-0.2.2 → gds_framework-0.2.3}/gds/spec.py +0 -0
  32. {gds_framework-0.2.2 → gds_framework-0.2.3}/gds/state.py +0 -0
  33. {gds_framework-0.2.2 → gds_framework-0.2.3}/gds/tagged.py +0 -0
  34. {gds_framework-0.2.2 → gds_framework-0.2.3}/gds/types/__init__.py +0 -0
  35. {gds_framework-0.2.2 → gds_framework-0.2.3}/gds/types/interface.py +0 -0
  36. {gds_framework-0.2.2 → gds_framework-0.2.3}/gds/types/tokens.py +0 -0
  37. {gds_framework-0.2.2 → gds_framework-0.2.3}/gds/types/typedef.py +0 -0
  38. {gds_framework-0.2.2 → gds_framework-0.2.3}/gds/verification/__init__.py +0 -0
  39. {gds_framework-0.2.2 → gds_framework-0.2.3}/gds/verification/engine.py +0 -0
  40. {gds_framework-0.2.2 → gds_framework-0.2.3}/gds/verification/findings.py +0 -0
  41. {gds_framework-0.2.2 → gds_framework-0.2.3}/gds/verification/spec_checks.py +0 -0
  42. {gds_framework-0.2.2 → gds_framework-0.2.3}/gds/visualization.py +0 -0
  43. {gds_framework-0.2.2 → gds_framework-0.2.3}/tests/__init__.py +0 -0
  44. {gds_framework-0.2.2 → gds_framework-0.2.3}/tests/conftest.py +0 -0
  45. {gds_framework-0.2.2 → gds_framework-0.2.3}/tests/test_blocks.py +0 -0
  46. {gds_framework-0.2.2 → gds_framework-0.2.3}/tests/test_helpers.py +0 -0
  47. {gds_framework-0.2.2 → gds_framework-0.2.3}/tests/test_ir.py +0 -0
  48. {gds_framework-0.2.2 → gds_framework-0.2.3}/tests/test_query.py +0 -0
  49. {gds_framework-0.2.2 → gds_framework-0.2.3}/tests/test_serialize.py +0 -0
  50. {gds_framework-0.2.2 → gds_framework-0.2.3}/tests/test_spaces.py +0 -0
  51. {gds_framework-0.2.2 → gds_framework-0.2.3}/tests/test_spec.py +0 -0
  52. {gds_framework-0.2.2 → gds_framework-0.2.3}/tests/test_spec_checks.py +0 -0
  53. {gds_framework-0.2.2 → gds_framework-0.2.3}/tests/test_state.py +0 -0
  54. {gds_framework-0.2.2 → gds_framework-0.2.3}/tests/test_types.py +0 -0
  55. {gds_framework-0.2.2 → gds_framework-0.2.3}/tests/test_v02_features.py +0 -0
  56. {gds_framework-0.2.2 → gds_framework-0.2.3}/tests/test_visualization.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: gds-framework
3
- Version: 0.2.2
3
+ Version: 0.2.3
4
4
  Summary: Generalized Dynamical Systems — typed compositional specifications for complex systems
5
5
  Project-URL: Homepage, https://github.com/BlockScience/gds-core
6
6
  Project-URL: Repository, https://github.com/BlockScience/gds-core
@@ -6,7 +6,7 @@ cybernetics (Ghani, Hedges et al.) into a single, dependency-light
6
6
  Python framework.
7
7
  """
8
8
 
9
- __version__ = "0.2.1"
9
+ __version__ = "0.2.3"
10
10
 
11
11
  # ── Composition algebra ─────────────────────────────────────
12
12
  from gds.blocks.base import AtomicBlock, Block
@@ -32,7 +32,14 @@ from gds.blocks.roles import (
32
32
 
33
33
  # ── Canonical projection ───────────────────────────────────
34
34
  from gds.canonical import CanonicalGDS, project_canonical
35
- from gds.compiler.compile import compile_system
35
+ from gds.compiler.compile import (
36
+ StructuralWiring,
37
+ WiringOrigin,
38
+ compile_system,
39
+ extract_hierarchy,
40
+ extract_wirings,
41
+ flatten_blocks,
42
+ )
36
43
 
37
44
  # ── Convenience helpers ────────────────────────────────────
38
45
  from gds.helpers import (
@@ -52,8 +59,10 @@ from gds.ir.models import (
52
59
  CompositionType,
53
60
  FlowDirection,
54
61
  HierarchyNodeIR,
62
+ InputIR,
55
63
  SystemIR,
56
64
  WiringIR,
65
+ sanitize_id,
57
66
  )
58
67
  from gds.ir.serialization import IRDocument, IRMetadata, load_ir, save_ir
59
68
 
@@ -122,6 +131,7 @@ __all__ = [
122
131
  "HierarchyNodeIR",
123
132
  "IRDocument",
124
133
  "IRMetadata",
134
+ "InputIR",
125
135
  "Interface",
126
136
  "Mechanism",
127
137
  "NonNegativeFloat",
@@ -138,6 +148,7 @@ __all__ = [
138
148
  "SpecWiring",
139
149
  "StackComposition",
140
150
  "StateVariable",
151
+ "StructuralWiring",
141
152
  "SystemIR",
142
153
  "Tagged",
143
154
  "TemporalLoop",
@@ -148,6 +159,7 @@ __all__ = [
148
159
  "Wire",
149
160
  "Wiring",
150
161
  "WiringIR",
162
+ "WiringOrigin",
151
163
  "all_checks",
152
164
  "check_canonical_wellformedness",
153
165
  "check_completeness",
@@ -157,12 +169,16 @@ __all__ = [
157
169
  "check_type_safety",
158
170
  "compile_system",
159
171
  "entity",
172
+ "extract_hierarchy",
173
+ "extract_wirings",
174
+ "flatten_blocks",
160
175
  "gds_check",
161
176
  "get_custom_checks",
162
177
  "interface",
163
178
  "load_ir",
164
179
  "port",
165
180
  "project_canonical",
181
+ "sanitize_id",
166
182
  "save_ir",
167
183
  "space",
168
184
  "spec_to_dict",
@@ -0,0 +1,19 @@
1
+ """Generic compilation pipeline: Block tree → flat SystemIR."""
2
+
3
+ from gds.compiler.compile import (
4
+ StructuralWiring,
5
+ WiringOrigin,
6
+ compile_system,
7
+ extract_hierarchy,
8
+ extract_wirings,
9
+ flatten_blocks,
10
+ )
11
+
12
+ __all__ = [
13
+ "StructuralWiring",
14
+ "WiringOrigin",
15
+ "compile_system",
16
+ "extract_hierarchy",
17
+ "extract_wirings",
18
+ "flatten_blocks",
19
+ ]
@@ -6,13 +6,19 @@ The compiler performs three transformations:
6
6
  2. **Wire** — extracts explicit wirings and auto-wires stack compositions.
7
7
  3. **Hierarchy** — captures the composition tree structure for visualization.
8
8
 
9
+ Each stage is exposed as a standalone generic function (``flatten_blocks``,
10
+ ``extract_wirings``, ``extract_hierarchy``) so domain packages can reuse the
11
+ DFS traversal with custom callbacks instead of forking the compiler.
12
+
9
13
  Domain packages provide a ``block_compiler`` callback to convert their
10
- specific atomic block types into BlockIR.
14
+ specific atomic block types into BlockIR, and optionally a
15
+ ``wiring_emitter`` callback to transform structural wirings into domain IR.
11
16
  """
12
17
 
13
18
  from __future__ import annotations
14
19
 
15
- import re
20
+ from dataclasses import dataclass
21
+ from enum import StrEnum
16
22
  from typing import TYPE_CHECKING
17
23
 
18
24
  from gds.blocks.base import AtomicBlock, Block
@@ -28,8 +34,10 @@ from gds.ir.models import (
28
34
  CompositionType,
29
35
  FlowDirection,
30
36
  HierarchyNodeIR,
37
+ InputIR,
31
38
  SystemIR,
32
39
  WiringIR,
40
+ sanitize_id,
33
41
  )
34
42
 
35
43
  if TYPE_CHECKING:
@@ -38,12 +46,126 @@ if TYPE_CHECKING:
38
46
  from gds.types.interface import Port
39
47
 
40
48
 
49
+ # ---------------------------------------------------------------------------
50
+ # Structural intermediates
51
+ # ---------------------------------------------------------------------------
52
+
53
+
54
+ class WiringOrigin(StrEnum):
55
+ """How a structural wiring was discovered during DFS traversal."""
56
+
57
+ AUTO = "auto"
58
+ EXPLICIT = "explicit"
59
+ FEEDBACK = "feedback"
60
+ TEMPORAL = "temporal"
61
+
62
+
63
+ @dataclass(frozen=True)
64
+ class StructuralWiring:
65
+ """Protocol-internal intermediate between DFS traversal and IR emission.
66
+
67
+ The DFS walk produces these; the wiring emitter callback transforms them
68
+ into domain-specific IR (e.g. ``WiringIR`` for GDS, flow edges for OGS).
69
+ """
70
+
71
+ source_block: str
72
+ source_port: str
73
+ target_block: str
74
+ target_port: str
75
+ direction: FlowDirection
76
+ origin: WiringOrigin
77
+
78
+
79
+ # ---------------------------------------------------------------------------
80
+ # Stage 1: flatten_blocks
81
+ # ---------------------------------------------------------------------------
82
+
83
+
84
+ def flatten_blocks[B](
85
+ root: Block,
86
+ block_compiler: Callable[[AtomicBlock], B],
87
+ ) -> list[B]:
88
+ """Flatten the composition tree and map each leaf through *block_compiler*.
89
+
90
+ Args:
91
+ root: Root of the composition tree.
92
+ block_compiler: Callback that converts an AtomicBlock into domain IR.
93
+ For GDS, this produces ``BlockIR``; OGS can produce ``OpenGameIR``.
94
+
95
+ Returns:
96
+ Ordered list of compiled block IR objects.
97
+ """
98
+ return [block_compiler(b) for b in root.flatten()]
99
+
100
+
101
+ # ---------------------------------------------------------------------------
102
+ # Stage 2: extract_wirings
103
+ # ---------------------------------------------------------------------------
104
+
105
+
106
+ def extract_wirings[W](
107
+ root: Block,
108
+ wiring_emitter: Callable[[StructuralWiring], W] | None = None,
109
+ ) -> list[W]:
110
+ """Walk the composition tree and emit all wirings through *wiring_emitter*.
111
+
112
+ The DFS traversal discovers explicit wirings, auto-wired connections,
113
+ feedback wirings, and temporal wirings — each tagged with a
114
+ ``WiringOrigin``. The emitter callback transforms each
115
+ ``StructuralWiring`` into domain-specific IR.
116
+
117
+ Args:
118
+ root: Root of the composition tree.
119
+ wiring_emitter: Callback that converts a ``StructuralWiring`` into
120
+ domain IR. If None, uses the default GDS emitter that produces
121
+ ``WiringIR``.
122
+
123
+ Returns:
124
+ Ordered list of emitted wiring IR objects.
125
+ """
126
+ if wiring_emitter is None:
127
+ wiring_emitter = _default_wiring_emitter # type: ignore[assignment]
128
+
129
+ structural: list[StructuralWiring] = []
130
+ _walk_structural_wirings(root, structural)
131
+ return [wiring_emitter(sw) for sw in structural]
132
+
133
+
134
+ # ---------------------------------------------------------------------------
135
+ # Stage 3: extract_hierarchy
136
+ # ---------------------------------------------------------------------------
137
+
138
+
139
+ def extract_hierarchy(root: Block) -> HierarchyNodeIR:
140
+ """Build a ``HierarchyNodeIR`` tree from the composition tree.
141
+
142
+ Sequential and parallel chains are flattened from binary trees into
143
+ n-ary groups for cleaner visualization.
144
+
145
+ Args:
146
+ root: Root of the composition tree.
147
+
148
+ Returns:
149
+ Root ``HierarchyNodeIR`` with flattened chains.
150
+ """
151
+ counter = [0]
152
+ hierarchy = _extract_hierarchy(root, counter)
153
+ return _flatten_sequential_chains(hierarchy)
154
+
155
+
156
+ # ---------------------------------------------------------------------------
157
+ # compile_system — thin wrapper over the three stages
158
+ # ---------------------------------------------------------------------------
159
+
160
+
41
161
  def compile_system(
42
162
  name: str,
43
163
  root: Block,
44
164
  block_compiler: Callable[[AtomicBlock], BlockIR] | None = None,
165
+ wiring_emitter: Callable[[StructuralWiring], WiringIR] | None = None,
45
166
  composition_type: CompositionType = CompositionType.SEQUENTIAL,
46
167
  source: str = "",
168
+ inputs: list[InputIR] | None = None,
47
169
  ) -> SystemIR:
48
170
  """Compile a Block tree into a flat SystemIR.
49
171
 
@@ -52,34 +174,36 @@ def compile_system(
52
174
  root: Root of the composition tree.
53
175
  block_compiler: Domain-specific function to convert AtomicBlock → BlockIR.
54
176
  If None, uses a default that extracts name + interface.
177
+ wiring_emitter: Domain-specific function to convert StructuralWiring →
178
+ WiringIR. If None, uses the default GDS emitter.
55
179
  composition_type: Top-level composition type.
56
180
  source: Source identifier.
181
+ inputs: External inputs to include in the SystemIR. Layer 0 never
182
+ infers inputs — domain packages supply them.
57
183
  """
58
184
  if block_compiler is None:
59
185
  block_compiler = _default_block_compiler
60
186
 
61
- # 1. Flatten
62
- atomic_blocks = root.flatten()
63
- block_irs = [block_compiler(b) for b in atomic_blocks]
64
-
65
- # 2. Wire
66
- wirings = _extract_wirings(root)
67
-
68
- # 3. Hierarchy
69
- counter = [0]
70
- hierarchy = _extract_hierarchy(root, counter)
71
- hierarchy = _flatten_sequential_chains(hierarchy)
187
+ blocks = flatten_blocks(root, block_compiler)
188
+ wirings = extract_wirings(root, wiring_emitter)
189
+ hierarchy = extract_hierarchy(root)
72
190
 
73
191
  return SystemIR(
74
192
  name=name,
75
- blocks=block_irs,
193
+ blocks=blocks,
76
194
  wirings=wirings,
195
+ inputs=inputs or [],
77
196
  composition_type=composition_type,
78
197
  hierarchy=hierarchy,
79
198
  source=source,
80
199
  )
81
200
 
82
201
 
202
+ # ---------------------------------------------------------------------------
203
+ # Default callbacks
204
+ # ---------------------------------------------------------------------------
205
+
206
+
83
207
  def _default_block_compiler(block: AtomicBlock) -> BlockIR:
84
208
  """Default block compiler — extracts name and interface slots."""
85
209
  return BlockIR(
@@ -93,6 +217,18 @@ def _default_block_compiler(block: AtomicBlock) -> BlockIR:
93
217
  )
94
218
 
95
219
 
220
+ def _default_wiring_emitter(sw: StructuralWiring) -> WiringIR:
221
+ """Default wiring emitter — converts StructuralWiring to WiringIR."""
222
+ return WiringIR(
223
+ source=sw.source_block,
224
+ target=sw.target_block,
225
+ label=sw.source_port,
226
+ direction=sw.direction,
227
+ is_feedback=sw.origin == WiringOrigin.FEEDBACK,
228
+ is_temporal=sw.origin == WiringOrigin.TEMPORAL,
229
+ )
230
+
231
+
96
232
  def _ports_to_sig(ports: tuple[Port, ...]) -> str:
97
233
  """Convert a tuple of Ports to the IR signature string format."""
98
234
  if not ports:
@@ -101,48 +237,53 @@ def _ports_to_sig(ports: tuple[Port, ...]) -> str:
101
237
 
102
238
 
103
239
  # ---------------------------------------------------------------------------
104
- # Wiring extraction
240
+ # DFS wiring traversal (produces StructuralWiring intermediates)
105
241
  # ---------------------------------------------------------------------------
106
242
 
107
243
 
108
- def _extract_wirings(block: Block) -> list[WiringIR]:
109
- """Recursively walk the block tree and collect all wirings."""
110
- wirings: list[WiringIR] = []
111
- _walk_wirings(block, wirings)
112
- return wirings
113
-
114
-
115
- def _walk_wirings(block: Block, wirings: list[WiringIR]) -> None:
116
- """Recursively walk the composition tree, collecting all wirings."""
244
+ def _walk_structural_wirings(block: Block, out: list[StructuralWiring]) -> None:
245
+ """Recursively walk the composition tree, collecting StructuralWirings."""
117
246
  if isinstance(block, AtomicBlock):
118
247
  return
119
248
 
120
249
  if isinstance(block, StackComposition):
121
- _walk_wirings(block.first, wirings)
122
- _walk_wirings(block.second, wirings)
250
+ _walk_structural_wirings(block.first, out)
251
+ _walk_structural_wirings(block.second, out)
123
252
 
124
253
  for w in block.wiring:
125
- wirings.append(_wiring_to_ir(w))
254
+ out.append(_wiring_to_structural(w, WiringOrigin.EXPLICIT))
126
255
 
127
256
  if not block.wiring:
128
- _auto_wire_stack(block.first, block.second, wirings)
257
+ _auto_wire_stack(block.first, block.second, out)
129
258
 
130
259
  elif isinstance(block, ParallelComposition):
131
- _walk_wirings(block.left, wirings)
132
- _walk_wirings(block.right, wirings)
260
+ _walk_structural_wirings(block.left, out)
261
+ _walk_structural_wirings(block.right, out)
133
262
 
134
263
  elif isinstance(block, FeedbackLoop):
135
- _walk_wirings(block.inner, wirings)
264
+ _walk_structural_wirings(block.inner, out)
136
265
  for fw in block.feedback_wiring:
137
- wirings.append(_wiring_to_ir(fw, is_feedback=True))
266
+ out.append(_wiring_to_structural(fw, WiringOrigin.FEEDBACK))
138
267
 
139
268
  elif isinstance(block, TemporalLoop):
140
- _walk_wirings(block.inner, wirings)
269
+ _walk_structural_wirings(block.inner, out)
141
270
  for w in block.temporal_wiring:
142
- wirings.append(_wiring_to_ir(w, is_temporal=True))
271
+ out.append(_wiring_to_structural(w, WiringOrigin.TEMPORAL))
272
+
273
+
274
+ def _wiring_to_structural(wiring: Wiring, origin: WiringOrigin) -> StructuralWiring:
275
+ """Convert a DSL Wiring to a StructuralWiring intermediate."""
276
+ return StructuralWiring(
277
+ source_block=wiring.source_block,
278
+ source_port=wiring.source_port,
279
+ target_block=wiring.target_block,
280
+ target_port=wiring.target_port,
281
+ direction=wiring.direction,
282
+ origin=origin,
283
+ )
143
284
 
144
285
 
145
- def _auto_wire_stack(first: Block, second: Block, wirings: list[WiringIR]) -> None:
286
+ def _auto_wire_stack(first: Block, second: Block, out: list[StructuralWiring]) -> None:
146
287
  """Auto-wire matching forward_out→forward_in ports in stack compositions."""
147
288
  first_leaves = _get_leaf_names(first)
148
289
  second_leaves = _get_leaf_names(second)
@@ -156,32 +297,18 @@ def _auto_wire_stack(first: Block, second: Block, wirings: list[WiringIR]) -> No
156
297
  target = (
157
298
  _find_port_owner(second, in_port, "forward_in") or second_leaves[0]
158
299
  )
159
- wirings.append(
160
- WiringIR(
161
- source=source,
162
- target=target,
163
- label=out_port.name,
300
+ out.append(
301
+ StructuralWiring(
302
+ source_block=source,
303
+ source_port=out_port.name,
304
+ target_block=target,
305
+ target_port=in_port.name,
164
306
  direction=FlowDirection.COVARIANT,
307
+ origin=WiringOrigin.AUTO,
165
308
  )
166
309
  )
167
310
 
168
311
 
169
- def _wiring_to_ir(
170
- wiring: Wiring,
171
- is_feedback: bool = False,
172
- is_temporal: bool = False,
173
- ) -> WiringIR:
174
- """Convert a DSL Wiring to an IR WiringIR."""
175
- return WiringIR(
176
- source=wiring.source_block,
177
- target=wiring.target_block,
178
- label=wiring.source_port,
179
- direction=wiring.direction,
180
- is_feedback=is_feedback,
181
- is_temporal=is_temporal,
182
- )
183
-
184
-
185
312
  def _get_leaf_names(block: Block) -> list[str]:
186
313
  """Get names of all leaf (atomic) blocks."""
187
314
  return [b.name for b in block.flatten()]
@@ -201,16 +328,11 @@ def _find_port_owner(block: Block, target_port: Port, slot: str) -> str | None:
201
328
  # ---------------------------------------------------------------------------
202
329
 
203
330
 
204
- def _sanitize_id(name: str) -> str:
205
- """Convert a name to a valid ID (alphanumeric + underscore only)."""
206
- return re.sub(r"[^A-Za-z0-9_]", "_", name)
207
-
208
-
209
331
  def _extract_hierarchy(block: Block, counter: list[int]) -> HierarchyNodeIR:
210
332
  """Recursively build a HierarchyNodeIR from the composition tree."""
211
333
  if isinstance(block, AtomicBlock):
212
334
  return HierarchyNodeIR(
213
- id=f"leaf_{_sanitize_id(block.name)}",
335
+ id=f"leaf_{sanitize_id(block.name)}",
214
336
  name=block.name,
215
337
  composition_type=None,
216
338
  block_name=block.name,
@@ -7,6 +7,7 @@ generic models with their own block types and metadata.
7
7
 
8
8
  from __future__ import annotations
9
9
 
10
+ import re
10
11
  from enum import StrEnum
11
12
  from typing import Any
12
13
 
@@ -15,6 +16,18 @@ from pydantic import BaseModel, Field
15
16
  from gds.parameters import ParameterSchema
16
17
 
17
18
 
19
+ def sanitize_id(name: str) -> str:
20
+ """Convert an arbitrary name to a valid IR/Mermaid identifier.
21
+
22
+ Replaces any character that is not alphanumeric or underscore with ``_``.
23
+ Prepends ``_`` if the result starts with a digit.
24
+ """
25
+ sanitized = re.sub(r"[^A-Za-z0-9_]", "_", name)
26
+ if sanitized and sanitized[0].isdigit():
27
+ sanitized = "_" + sanitized
28
+ return sanitized
29
+
30
+
18
31
  class FlowDirection(StrEnum):
19
32
  """Direction of an information flow in a block composition.
20
33
 
@@ -60,7 +73,9 @@ class WiringIR(BaseModel):
60
73
  """A directed connection (edge) between blocks in the IR.
61
74
 
62
75
  ``is_feedback`` and ``is_temporal`` flags distinguish special wiring
63
- categories for verification.
76
+ categories for verification. The ``category`` field is an open string
77
+ that domain packages can use for domain-specific edge classification;
78
+ the generic protocol only interprets ``"dataflow"``.
64
79
  """
65
80
 
66
81
  source: str
@@ -70,6 +85,7 @@ class WiringIR(BaseModel):
70
85
  direction: FlowDirection
71
86
  is_feedback: bool = False
72
87
  is_temporal: bool = False
88
+ category: str = "dataflow"
73
89
 
74
90
 
75
91
  class HierarchyNodeIR(BaseModel):
@@ -87,6 +103,18 @@ class HierarchyNodeIR(BaseModel):
87
103
  exit_condition: str = ""
88
104
 
89
105
 
106
+ class InputIR(BaseModel, frozen=True):
107
+ """An external input to the system.
108
+
109
+ Layer 0 defines only ``name`` and a generic ``metadata`` bag.
110
+ Domain packages store their richer fields (e.g., input_type, schema_hint)
111
+ inside ``metadata`` when projecting to SystemIR.
112
+ """
113
+
114
+ name: str
115
+ metadata: dict[str, Any] = Field(default_factory=dict)
116
+
117
+
90
118
  class SystemIR(BaseModel):
91
119
  """A complete composed system — the top-level IR unit.
92
120
 
@@ -97,7 +125,7 @@ class SystemIR(BaseModel):
97
125
  name: str
98
126
  blocks: list[BlockIR] = Field(default_factory=list)
99
127
  wirings: list[WiringIR] = Field(default_factory=list)
100
- inputs: list[dict[str, Any]] = Field(default_factory=list)
128
+ inputs: list[InputIR] = Field(default_factory=list)
101
129
  composition_type: CompositionType = CompositionType.SEQUENTIAL
102
130
  hierarchy: HierarchyNodeIR | None = None
103
131
  source: str = ""
@@ -98,27 +98,100 @@ def check_g002_signature_completeness(system: SystemIR) -> list[Finding]:
98
98
 
99
99
 
100
100
  def check_g003_direction_consistency(system: SystemIR) -> list[Finding]:
101
- """G-003: Covariant wirings should not be typed as backward;
102
- contravariant wirings should not be typed as forward.
101
+ """G-003: Validate direction flag consistency and contravariant port-slot matching.
103
102
 
104
- This is a generic structural check — domain packages define their
105
- own wiring_type semantics.
103
+ Two validations:
104
+
105
+ A) Flag consistency — ``direction``, ``is_feedback``, ``is_temporal`` must
106
+ not contradict:
107
+ - COVARIANT + is_feedback → ERROR (feedback implies contravariant)
108
+ - CONTRAVARIANT + is_temporal → ERROR (temporal implies covariant)
109
+
110
+ B) Contravariant port-slot matching — for CONTRAVARIANT wirings, the label
111
+ must be a token-subset of the source's backward_out (signature[3]) or
112
+ the target's backward_in (signature[2]). G-001 already covers the
113
+ covariant side.
106
114
  """
107
115
  findings = []
116
+ block_sigs = {b.name: b.signature for b in system.blocks}
117
+
108
118
  for wiring in system.wirings:
109
- # Generic check: just verify the wiring has a direction set
110
- findings.append(
111
- Finding(
112
- check_id="G-003",
113
- severity=Severity.INFO,
114
- message=(
115
- f"Wiring {wiring.label!r} ({wiring.source} -> {wiring.target}): "
116
- f"direction={wiring.direction.value}"
117
- ),
118
- source_elements=[wiring.source, wiring.target],
119
- passed=True,
119
+ # A) Flag consistency
120
+ if wiring.direction == FlowDirection.COVARIANT and wiring.is_feedback:
121
+ findings.append(
122
+ Finding(
123
+ check_id="G-003",
124
+ severity=Severity.ERROR,
125
+ message=(
126
+ f"Wiring {wiring.label!r} "
127
+ f"({wiring.source} -> {wiring.target}): "
128
+ f"COVARIANT + is_feedback — contradiction"
129
+ ),
130
+ source_elements=[wiring.source, wiring.target],
131
+ passed=False,
132
+ )
133
+ )
134
+ continue
135
+
136
+ if wiring.direction == FlowDirection.CONTRAVARIANT and wiring.is_temporal:
137
+ findings.append(
138
+ Finding(
139
+ check_id="G-003",
140
+ severity=Severity.ERROR,
141
+ message=(
142
+ f"Wiring {wiring.label!r} "
143
+ f"({wiring.source} -> {wiring.target}): "
144
+ f"CONTRAVARIANT + is_temporal — contradiction"
145
+ ),
146
+ source_elements=[wiring.source, wiring.target],
147
+ passed=False,
148
+ )
149
+ )
150
+ continue
151
+
152
+ # B) Contravariant port-slot matching (G-001 covers covariant)
153
+ if wiring.direction == FlowDirection.CONTRAVARIANT:
154
+ if wiring.source not in block_sigs or wiring.target not in block_sigs:
155
+ # Non-block endpoints — G-004 handles dangling references
156
+ continue
157
+
158
+ src_bwd_out = block_sigs[wiring.source][3] # backward_out
159
+ tgt_bwd_in = block_sigs[wiring.target][2] # backward_in
160
+
161
+ if not src_bwd_out and not tgt_bwd_in:
162
+ findings.append(
163
+ Finding(
164
+ check_id="G-003",
165
+ severity=Severity.ERROR,
166
+ message=(
167
+ f"Wiring {wiring.label!r} "
168
+ f"({wiring.source} -> {wiring.target}): "
169
+ f"CONTRAVARIANT but both backward "
170
+ f"ports are empty"
171
+ ),
172
+ source_elements=[wiring.source, wiring.target],
173
+ passed=False,
174
+ )
175
+ )
176
+ continue
177
+
178
+ compatible = tokens_subset(wiring.label, src_bwd_out) or tokens_subset(
179
+ wiring.label, tgt_bwd_in
180
+ )
181
+ findings.append(
182
+ Finding(
183
+ check_id="G-003",
184
+ severity=Severity.ERROR,
185
+ message=(
186
+ f"Wiring {wiring.label!r}: "
187
+ f"{wiring.source} bwd_out={src_bwd_out!r} -> "
188
+ f"{wiring.target} bwd_in={tgt_bwd_in!r}"
189
+ + ("" if compatible else " — MISMATCH")
190
+ ),
191
+ source_elements=[wiring.source, wiring.target],
192
+ passed=compatible,
193
+ )
120
194
  )
121
- )
122
195
 
123
196
  return findings
124
197
 
@@ -127,10 +200,8 @@ def check_g004_dangling_wirings(system: SystemIR) -> list[Finding]:
127
200
  """G-004: Flag wirings whose source or target is not in the system."""
128
201
  findings = []
129
202
  known_names = {b.name for b in system.blocks}
130
- # Also include input names
131
203
  for inp in system.inputs:
132
- if isinstance(inp, dict) and "name" in inp:
133
- known_names.add(inp["name"])
204
+ known_names.add(inp.name)
134
205
 
135
206
  for wiring in system.wirings:
136
207
  src_ok = wiring.source in known_names
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "gds-framework"
3
- version = "0.2.2"
3
+ dynamic = ["version"]
4
4
  description = "Generalized Dynamical Systems — typed compositional specifications for complex systems"
5
5
  readme = "README.md"
6
6
  license = "Apache-2.0"
@@ -47,6 +47,9 @@ Documentation = "https://blockscience.github.io/gds-core"
47
47
  requires = ["hatchling"]
48
48
  build-backend = "hatchling.build"
49
49
 
50
+ [tool.hatch.version]
51
+ path = "gds/__init__.py"
52
+
50
53
  [tool.hatch.build.targets.wheel]
51
54
  packages = ["gds"]
52
55