pgc-compiler 2.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.
Files changed (177) hide show
  1. compiler/VERSION +1 -0
  2. compiler/__init__.py +1 -0
  3. compiler/atoms/__init__.py +54 -0
  4. compiler/atoms/error_codes.py +160 -0
  5. compiler/atoms/errors.py +125 -0
  6. compiler/atoms/fqdn.py +204 -0
  7. compiler/atoms/invariants.py +106 -0
  8. compiler/atoms/phase.py +99 -0
  9. compiler/atoms/pipeline.py +51 -0
  10. compiler/atoms/snapshot_gate.py +51 -0
  11. compiler/atoms/sorting.py +125 -0
  12. compiler/cli.py +637 -0
  13. compiler/diagnostics/__init__.py +6 -0
  14. compiler/diagnostics/determination.py +148 -0
  15. compiler/diagnostics/machine_block_health.py +199 -0
  16. compiler/governance/__init__.py +6 -0
  17. compiler/governance_engine/__init__.py +0 -0
  18. compiler/governance_engine/artifact_kinds.py +221 -0
  19. compiler/governance_engine/assertions/__init__.py +0 -0
  20. compiler/governance_engine/assertions/handlers/__init__.py +200 -0
  21. compiler/governance_engine/assertions/handlers/assert_ac_declaration_well_formed_v0.py +70 -0
  22. compiler/governance_engine/assertions/handlers/assert_actor_authority_separation_v0.py +17 -0
  23. compiler/governance_engine/assertions/handlers/assert_artifact_content_hash_declared_v0.py +60 -0
  24. compiler/governance_engine/assertions/handlers/assert_assert_not_runtime_referenced_v0.py +49 -0
  25. compiler/governance_engine/assertions/handlers/assert_assert_parity_v0.py +169 -0
  26. compiler/governance_engine/assertions/handlers/assert_atom_output_purity_v0.py +186 -0
  27. compiler/governance_engine/assertions/handlers/assert_authority_constituted_v0.py +56 -0
  28. compiler/governance_engine/assertions/handlers/assert_authority_required_for_execution_v0.py +16 -0
  29. compiler/governance_engine/assertions/handlers/assert_authority_state_well_formed_v0.py +17 -0
  30. compiler/governance_engine/assertions/handlers/assert_binding_integrity_v0.py +64 -0
  31. compiler/governance_engine/assertions/handlers/assert_binding_surface_closed_v0.py +92 -0
  32. compiler/governance_engine/assertions/handlers/assert_cc_capability_binding_valid_v0.py +85 -0
  33. compiler/governance_engine/assertions/handlers/assert_cc_inputs_satisfied_v0.py +89 -0
  34. compiler/governance_engine/assertions/handlers/assert_cc_no_implicit_chaining_v0.py +89 -0
  35. compiler/governance_engine/assertions/handlers/assert_cc_no_missing_dependencies_v0.py +89 -0
  36. compiler/governance_engine/assertions/handlers/assert_cc_no_unused_outputs_v0.py +81 -0
  37. compiler/governance_engine/assertions/handlers/assert_cc_storage_op_conformance_v0.py +74 -0
  38. compiler/governance_engine/assertions/handlers/assert_compiler_governance_declared_v0.py +65 -0
  39. compiler/governance_engine/assertions/handlers/assert_compiler_no_execution_v0.py +86 -0
  40. compiler/governance_engine/assertions/handlers/assert_concern_not_authority_v0.py +51 -0
  41. compiler/governance_engine/assertions/handlers/assert_conformance_assertion_mode_valid_v0.py +35 -0
  42. compiler/governance_engine/assertions/handlers/assert_cryptographic_trust_declared_v0.py +51 -0
  43. compiler/governance_engine/assertions/handlers/assert_cs_isolated_execution_v0.py +35 -0
  44. compiler/governance_engine/assertions/handlers/assert_cs_surface_closed_v0.py +241 -0
  45. compiler/governance_engine/assertions/handlers/assert_cs_traceable_v0.py +35 -0
  46. compiler/governance_engine/assertions/handlers/assert_ct_output_contract_match_v0.py +167 -0
  47. compiler/governance_engine/assertions/handlers/assert_ct_surface_closed_v0.py +173 -0
  48. compiler/governance_engine/assertions/handlers/assert_ct_surface_derived_closed_v0.py +108 -0
  49. compiler/governance_engine/assertions/handlers/assert_ct_test_data_outcome_declared_v0.py +112 -0
  50. compiler/governance_engine/assertions/handlers/assert_ev_append_only_v0.py +70 -0
  51. compiler/governance_engine/assertions/handlers/assert_ev_schema_required_v0.py +59 -0
  52. compiler/governance_engine/assertions/handlers/assert_execution_placement_declared_v0.py +51 -0
  53. compiler/governance_engine/assertions/handlers/assert_execution_scheduling_declared_v0.py +51 -0
  54. compiler/governance_engine/assertions/handlers/assert_fqdn_namespace_authorized_v0.py +38 -0
  55. compiler/governance_engine/assertions/handlers/assert_fqdn_only_references_v0.py +89 -0
  56. compiler/governance_engine/assertions/handlers/assert_governance_declaration_resolves_v0.py +126 -0
  57. compiler/governance_engine/assertions/handlers/assert_handler_registry_closed_v0.py +75 -0
  58. compiler/governance_engine/assertions/handlers/assert_identity_authority_separation_v0.py +19 -0
  59. compiler/governance_engine/assertions/handlers/assert_identity_fqdn_consistency.py +75 -0
  60. compiler/governance_engine/assertions/handlers/assert_implementation_admissible_v0.py +66 -0
  61. compiler/governance_engine/assertions/handlers/assert_in_no_execution_logic_v0.py +82 -0
  62. compiler/governance_engine/assertions/handlers/assert_in_schema_required_v0.py +74 -0
  63. compiler/governance_engine/assertions/handlers/assert_in_workflow_binding_v0.py +101 -0
  64. compiler/governance_engine/assertions/handlers/assert_inspection_capability_read_only_v0.py +114 -0
  65. compiler/governance_engine/assertions/handlers/assert_no_ambient_authority_v0.py +17 -0
  66. compiler/governance_engine/assertions/handlers/assert_no_runtime_authorization_v0.py +18 -0
  67. compiler/governance_engine/assertions/handlers/assert_no_runtime_topology_synthesis_v0.py +18 -0
  68. compiler/governance_engine/assertions/handlers/assert_no_smart_execution_v0.py +102 -0
  69. compiler/governance_engine/assertions/handlers/assert_no_undeclared_behavior_surface_v0.py +52 -0
  70. compiler/governance_engine/assertions/handlers/assert_no_workflow_authorization_logic_v0.py +18 -0
  71. compiler/governance_engine/assertions/handlers/assert_protocol_surface_closed_v0.py +119 -0
  72. compiler/governance_engine/assertions/handlers/assert_rb_binding_policy_conformance_v0.py +124 -0
  73. compiler/governance_engine/assertions/handlers/assert_rb_cs_only_v0.py +68 -0
  74. compiler/governance_engine/assertions/handlers/assert_rb_no_logic_v0.py +81 -0
  75. compiler/governance_engine/assertions/handlers/assert_rb_parameters_declared_v0.py +117 -0
  76. compiler/governance_engine/assertions/handlers/assert_rb_storage_subdomain_owned_v0.py +118 -0
  77. compiler/governance_engine/assertions/handlers/assert_runtime_invariant_wired_v0.py +163 -0
  78. compiler/governance_engine/assertions/handlers/assert_schema_conformance_v0.py +64 -0
  79. compiler/governance_engine/assertions/handlers/assert_schema_description_well_formed_v0.py +97 -0
  80. compiler/governance_engine/assertions/handlers/assert_security_domain_declared_v0.py +51 -0
  81. compiler/governance_engine/assertions/handlers/assert_structure_paths_well_formed_v0.py +118 -0
  82. compiler/governance_engine/assertions/handlers/assert_superseded_not_referenced_v0.py +134 -0
  83. compiler/governance_engine/assertions/handlers/assert_test_data_match_ct_output_v0.py +134 -0
  84. compiler/governance_engine/assertions/handlers/assert_topology_acyclic_v0.py +53 -0
  85. compiler/governance_engine/assertions/handlers/assert_topology_authority_orthogonal_v0.py +110 -0
  86. compiler/governance_engine/assertions/handlers/assert_topology_capability_reference_unique_v0.py +63 -0
  87. compiler/governance_engine/assertions/handlers/assert_topology_contract_closed_v0.py +113 -0
  88. compiler/governance_engine/assertions/handlers/assert_topology_immutable_after_compilation_v0.py +17 -0
  89. compiler/governance_engine/assertions/handlers/assert_topology_input_reference_declared_v0.py +112 -0
  90. compiler/governance_engine/assertions/handlers/assert_topology_routing_complete_v0.py +82 -0
  91. compiler/governance_engine/assertions/handlers/assert_topology_step_declared_v0.py +77 -0
  92. compiler/governance_engine/assertions/handlers/assert_topology_step_id_unique_v0.py +60 -0
  93. compiler/governance_engine/assertions/handlers/assert_topology_surface_canonical_v0.py +180 -0
  94. compiler/governance_engine/assertions/handlers/assert_topology_transport_orthogonal_v0.py +113 -0
  95. compiler/governance_engine/assertions/handlers/assert_trace_authority_binding_required_v0.py +18 -0
  96. compiler/governance_engine/assertions/handlers/assert_transport_canonical_normalization_v0.py +83 -0
  97. compiler/governance_engine/assertions/handlers/assert_transport_no_dynamic_routing_v0.py +93 -0
  98. compiler/governance_engine/assertions/handlers/assert_transport_no_workflow_semantics_v0.py +106 -0
  99. compiler/governance_engine/assertions/handlers/assert_transport_operation_identity_independence_v0.py +56 -0
  100. compiler/governance_engine/assertions/handlers/assert_transport_response_projection_external_v0.py +38 -0
  101. compiler/governance_engine/assertions/handlers/assert_transport_result_class_protocol_independence_v0.py +48 -0
  102. compiler/governance_engine/assertions/handlers/assert_transport_target_exists_v0.py +132 -0
  103. compiler/governance_engine/assertions/handlers/assert_unique_artifact_id_v0.py +58 -0
  104. compiler/governance_engine/assertions/handlers/assert_vocabulary_symbols_well_formed_v0.py +163 -0
  105. compiler/governance_engine/assertions/handlers/assert_wf_announcement_distinct_v0.py +85 -0
  106. compiler/governance_engine/assertions/handlers/assert_wf_cc_only_nodes_v0.py +60 -0
  107. compiler/governance_engine/assertions/handlers/assert_wf_entry_intent_v0.py +68 -0
  108. compiler/governance_engine/assertions/handlers/assert_wf_execution_path_valid_v0.py +89 -0
  109. compiler/governance_engine/assertions/handlers/assert_wf_node_key_binding_unique_v0.py +107 -0
  110. compiler/governance_engine/platform_root.py +102 -0
  111. compiler/governance_engine/structure/__init__.py +56 -0
  112. compiler/governance_engine/structure/discovery/__init__.py +28 -0
  113. compiler/governance_engine/structure/discovery/artifact_locator.py +210 -0
  114. compiler/governance_engine/structure/exceptions.py +113 -0
  115. compiler/governance_engine/structure/loading/__init__.py +88 -0
  116. compiler/governance_engine/structure/loading/fs_reader.py +225 -0
  117. compiler/governance_engine/structure/loading/markdown_parser.py +148 -0
  118. compiler/governance_engine/structure/loading/protocol_loader.py +587 -0
  119. compiler/governance_engine/structure/loading/test_case_provider.py +125 -0
  120. compiler/governance_engine/structure/loading/trace_reader.py +98 -0
  121. compiler/governance_engine/structure/loading/vocabulary_loader.py +174 -0
  122. compiler/governance_engine/structure/loading/yaml_parser.py +209 -0
  123. compiler/governance_engine/structure/resolution/__init__.py +22 -0
  124. compiler/governance_engine/structure/resolution/domain_resolver.py +124 -0
  125. compiler/governance_engine/structure/resolution/layer_resolver.py +572 -0
  126. compiler/governance_engine/structure/resolution/path_registry.py +1054 -0
  127. compiler/graph/__init__.py +38 -0
  128. compiler/graph/addressing.py +153 -0
  129. compiler/graph/edge.py +100 -0
  130. compiler/graph/evidence.py +637 -0
  131. compiler/graph/graph.py +269 -0
  132. compiler/graph/hashing.py +89 -0
  133. compiler/graph/node.py +134 -0
  134. compiler/graph/query.py +215 -0
  135. compiler/graph/state.py +237 -0
  136. compiler/graph/trace.py +97 -0
  137. compiler/graph/types.py +71 -0
  138. compiler/projections/__init__.py +207 -0
  139. compiler/projections/artifact_index.py +225 -0
  140. compiler/projections/canonical.py +173 -0
  141. compiler/projections/dispatch.py +532 -0
  142. compiler/projections/evidence.py +170 -0
  143. compiler/projections/handlers.py +224 -0
  144. compiler/projections/ipm.py +388 -0
  145. compiler/projections/reverse.py +165 -0
  146. compiler/projections/tokenized.py +135 -0
  147. compiler/projections/vocabulary.py +108 -0
  148. compiler/stages/__init__.py +28 -0
  149. compiler/stages/s1_extract.py +867 -0
  150. compiler/stages/s2_canonicalize.py +516 -0
  151. compiler/stages/s3_semantic_addressing.py +66 -0
  152. compiler/stages/s4_govern.py +754 -0
  153. compiler/stages/s5_construct.py +537 -0
  154. compiler/stages/s6_project.py +94 -0
  155. compiler/stages/s7_materialize.py +1170 -0
  156. compiler/stages/s8_verify.py +643 -0
  157. compiler/stages/s9_attest.py +184 -0
  158. compiler/structure_loader.py +132 -0
  159. compiler/visualization/__init__.py +12 -0
  160. compiler/visualization/consumers/__init__.py +28 -0
  161. compiler/visualization/consumers/evidence_projection.py +162 -0
  162. compiler/visualization/consumers/evidence_query.py +296 -0
  163. compiler/visualization/consumers/evidence_reader.py +127 -0
  164. compiler/visualization/views/__init__.py +70 -0
  165. compiler/visualization/views/_png_writer.py +45 -0
  166. compiler/visualization/views/artifact_lineage.py +180 -0
  167. compiler/visualization/views/family_view.py +164 -0
  168. compiler/visualization/views/materialization_view.py +150 -0
  169. compiler/visualization/views/structural_dag.py +158 -0
  170. compiler/visualization/wf_graph_generator.py +534 -0
  171. pgc_compiler-2.0.0.dist-info/METADATA +203 -0
  172. pgc_compiler-2.0.0.dist-info/RECORD +177 -0
  173. pgc_compiler-2.0.0.dist-info/WHEEL +5 -0
  174. pgc_compiler-2.0.0.dist-info/entry_points.txt +2 -0
  175. pgc_compiler-2.0.0.dist-info/licenses/LICENSE +67 -0
  176. pgc_compiler-2.0.0.dist-info/licenses/NOTICE +11 -0
  177. pgc_compiler-2.0.0.dist-info/top_level.txt +1 -0
@@ -0,0 +1,867 @@
1
+ """
2
+ S1 EXTRACT — Discovery + parsing → initial Graph population.
3
+
4
+ Input: State (initial, with structure_config)
5
+ Output: State with Graph populated with nodes (address=-1, ir=None)
6
+
7
+ Each discovered artifact becomes a Node. References become Edges
8
+ with kind=REFERENCES (typed in S2 CANONICALIZE).
9
+ """
10
+
11
+ import hashlib
12
+ import json
13
+ import re
14
+ from pathlib import Path
15
+ from typing import Any
16
+
17
+ import yaml
18
+
19
+ from compiler.graph.types import NodeKind, EdgeKind
20
+ from compiler.graph.node import Node
21
+ from compiler.graph.edge import Edge
22
+ from compiler.graph.graph import GraphBuilder
23
+ from compiler.graph.state import State
24
+ from compiler.graph.trace import TraceEvent
25
+ from compiler.graph.evidence import EventFamily
26
+ from compiler.atoms.errors import CompilerError
27
+ from compiler.atoms.error_codes import ErrorCode
28
+ from compiler.structure_loader import (
29
+ load_structure_artifact,
30
+ get_bootstrap_search_roots,
31
+ )
32
+
33
+ from compiler.governance_engine.structure.resolution.layer_resolver import LayerResolver
34
+
35
+
36
+ # Machine block pattern: ## Machine\n```yaml\n{yaml}\n```
37
+ _MACHINE_BLOCK_PATTERN = re.compile(
38
+ r"^## Machine\s*\n+```yaml\s*\n(?P<machine_yaml>.*?)\n```",
39
+ re.MULTILINE | re.DOTALL,
40
+ )
41
+
42
+ # Artifact type → NodeKind mapping
43
+ def _type_to_kind(artifact_type: str) -> NodeKind | None:
44
+ """Artifact-type prefix → NodeKind, via the single-source-of-truth ArtifactKindRegistry.
45
+
46
+ Replaces the legacy _TYPE_TO_KIND dict. The registry returns the NodeKind as a string (it is a
47
+ governance-layer module and must not import this compiler enum); we map it to NodeKind here.
48
+ """
49
+ from compiler.governance_engine.artifact_kinds import REGISTRY
50
+ nk = REGISTRY.node_kind(artifact_type)
51
+ return NodeKind(nk) if nk is not None else None
52
+
53
+
54
+ def s1_extract(state: State) -> State:
55
+ """
56
+ S1 EXTRACT: Discover artifacts and populate Graph with nodes.
57
+
58
+ Pure function: State → State.
59
+
60
+ Steps:
61
+ 1. Load STRUCTURE_DISCOVERY_V0 + build-specific STRUCTURE config
62
+ 2. Discover artifact files from declared layers
63
+ 3. Parse Machine YAML from each artifact
64
+ 4. Derive FQDNs from STRUCTURE_IDENTITY_V0 rules
65
+ 5. Create Nodes for each artifact
66
+ 6. Extract references and create REFERENCES edges
67
+
68
+ Args:
69
+ state: Initial State (from State.initial())
70
+
71
+ Returns:
72
+ New State with graph populated (nodes + reference edges)
73
+ """
74
+ state = state.with_stage("S1_EXTRACT")
75
+ errors: list[CompilerError] = []
76
+ warnings: list[CompilerError] = []
77
+ trace: list[TraceEvent] = []
78
+
79
+ structure_config = dict(state.structure_config)
80
+ structure_artifact_code = structure_config.get("structure_artifact_code", "")
81
+
82
+ if not structure_artifact_code:
83
+ return state.with_errors(CompilerError(
84
+ code=ErrorCode.E901_INTERNAL_ERROR,
85
+ message="structure_config missing 'structure_artifact_code'",
86
+ phase="S1_EXTRACT",
87
+ ))
88
+
89
+ search_roots = get_bootstrap_search_roots()
90
+
91
+ # --- Step 1: Load discovery and build configs ---
92
+ try:
93
+ discovery_master = load_structure_artifact("STRUCTURE_DISCOVERY_V0", search_roots)
94
+ build_config = load_structure_artifact(structure_artifact_code, search_roots)
95
+ except (FileNotFoundError, RuntimeError, ValueError) as e:
96
+ return state.with_errors(CompilerError(
97
+ code=ErrorCode.E901_INTERNAL_ERROR,
98
+ message=f"Failed to load STRUCTURE artifacts: {e}",
99
+ phase="S1_EXTRACT",
100
+ ))
101
+
102
+ discovery_config = discovery_master.get("discovery", {})
103
+ discovery_layers = discovery_config.get("layers", {})
104
+ discovery_rules = discovery_config.get("rules", {})
105
+
106
+ artifact_discovery = build_config.get("artifact_discovery", {})
107
+ search_layers = artifact_discovery.get("search_layers", [])
108
+ build_artifact_types = artifact_discovery.get("artifact_types", [])
109
+
110
+ if not search_layers:
111
+ return state.with_errors(CompilerError(
112
+ code=ErrorCode.E901_INTERNAL_ERROR,
113
+ message=f"STRUCTURE {structure_artifact_code} artifact_discovery.search_layers is empty",
114
+ phase="S1_EXTRACT",
115
+ ))
116
+
117
+ # --- Step 2: Load identity rules ---
118
+ try:
119
+ identity_master = load_structure_artifact("STRUCTURE_IDENTITY_V0", search_roots)
120
+ identity_rules = (
121
+ identity_master.get("identity", {})
122
+ .get("fqdn", {})
123
+ .get("namespace", {})
124
+ .get("derivation", {})
125
+ .get("rules", [])
126
+ )
127
+ except (FileNotFoundError, RuntimeError, ValueError) as e:
128
+ return state.with_errors(CompilerError(
129
+ code=ErrorCode.E901_INTERNAL_ERROR,
130
+ message=f"Failed to load STRUCTURE_IDENTITY_V0: {e}",
131
+ phase="S1_EXTRACT",
132
+ ))
133
+
134
+ # --- Domain-extension: merge the build manifest's additive layer defs + identity rules ---
135
+ # The platform STRUCTURE_DISCOVERY_V0 / STRUCTURE_IDENTITY_V0 remain immutable and untouched;
136
+ # a domain describes its own layer + namespace rule in its build manifest, merged only here.
137
+ from compiler.governance_engine.structure.resolution.layer_resolver import (
138
+ register_domain_layers, clear_domain_layers,
139
+ )
140
+ domain_layers = build_config.get("layer_definitions", {}) or {}
141
+ clear_domain_layers()
142
+ register_domain_layers(domain_layers) # LayerResolver path resolution
143
+ discovery_layers = {**discovery_layers, **domain_layers} # _discover_artifacts layer-config lookup
144
+ identity_rules = list(build_config.get("identity_rules", []) or []) + list(identity_rules)
145
+
146
+ # Authorized namespace allowlist: every namespace an identity rule can produce. A declared FQDN
147
+ # whose namespace is outside this set is unauthorized (INVARIANT_FQDN_NAMESPACE_AUTHORIZED_V0).
148
+ # This is the identity rules repurposed from derivation to authorization.
149
+ authorized_namespaces = sorted({
150
+ r.get("namespace") for r in identity_rules if r.get("namespace")
151
+ })
152
+
153
+ # --- Step 3: Discover artifact files ---
154
+ discovered = _discover_artifacts(
155
+ search_layers, discovery_layers, discovery_rules,
156
+ build_artifact_types, errors
157
+ )
158
+
159
+ if not discovered and not errors:
160
+ errors.append(CompilerError(
161
+ code=ErrorCode.E001_NO_ARTIFACTS,
162
+ message="No artifacts found in build scope",
163
+ phase="S1_EXTRACT",
164
+ ))
165
+
166
+ if errors:
167
+ return state.with_errors(*errors)
168
+
169
+ # --- Step 4: Derive FQDNs ---
170
+ _derive_fqdns(discovered, identity_rules, errors)
171
+
172
+ if errors:
173
+ return state.with_errors(*errors)
174
+
175
+ trace.append(TraceEvent.create(
176
+ stage="S1_EXTRACT",
177
+ operation="discovery_complete",
178
+ detail={"artifacts_discovered": len(discovered)},
179
+ family=EventFamily.DISCOVERY.value,
180
+ ))
181
+
182
+ # --- Step 5: Parse and build graph ---
183
+ builder = GraphBuilder()
184
+
185
+ # Build artifact registry for reference validation
186
+ artifact_registry: dict[str, list[str]] = {}
187
+ for artifact in discovered:
188
+ code = artifact["artifact_code"]
189
+ ns = artifact["namespace"]
190
+ if code not in artifact_registry:
191
+ artifact_registry[code] = []
192
+ if ns not in artifact_registry[code]:
193
+ artifact_registry[code].append(ns)
194
+
195
+ all_refs: set[str] = set()
196
+ for artifact in discovered:
197
+ node, refs, parse_errors, parse_warnings = _parse_artifact_to_node(
198
+ artifact, artifact_registry
199
+ )
200
+ errors.extend(parse_errors)
201
+ warnings.extend(parse_warnings)
202
+
203
+ if node is not None:
204
+ builder.add_node(node)
205
+ all_refs.update(refs)
206
+ trace.append(TraceEvent.create(
207
+ stage="S1_EXTRACT",
208
+ operation="node_created",
209
+ subject_fqdn=node.fqdn,
210
+ detail={"kind": node.kind.value, "layer_code": node.layer_code},
211
+ family=EventFamily.DISCOVERY.value,
212
+ ))
213
+
214
+ # Create REFERENCES edges for each resolved reference
215
+ for ref_fqdn in refs:
216
+ edge = Edge.create(
217
+ source_fqdn=node.fqdn,
218
+ target_fqdn=ref_fqdn,
219
+ kind=EdgeKind.REFERENCES,
220
+ )
221
+ builder.add_edge(edge)
222
+
223
+ # Carry imported platform CAPABILITIES (CS/CT consumed by this domain) into the graph so they are
224
+ # addressed, wired into CC pipelines, and emitted into this domain's handlers/dispatch (Option A
225
+ # "static link"). Governance imports stay resolve-only (tolerated + dropped in S2).
226
+ _inject_imported_capabilities(builder, all_refs, build_config, errors)
227
+
228
+ # Import platform governance as checked protocol state (design §2). Domain-applicable invariants
229
+ # enter the graph so S4 can assert them against domain artifacts; platform-only invariants are
230
+ # filtered out by declared scope. No-op for the platform build (no import_surface).
231
+ _inject_imported_governance(builder, build_config, errors)
232
+
233
+ # Provenance (design §6): bind the exact governance closure this build was checked against, so a
234
+ # domain can never be re-verified — or paired at runtime — under different governance than it
235
+ # compiled under. Empty on the platform build (nothing imported).
236
+ governance_closure = _compute_governance_closure(builder, build_config)
237
+
238
+ graph = builder.build()
239
+ state = state.with_graph(graph)
240
+
241
+ if errors:
242
+ state = state.with_errors(*errors)
243
+ if warnings:
244
+ state = state.with_warnings(*warnings)
245
+ if trace:
246
+ state = state.with_trace_events(*trace)
247
+
248
+ # Record extraction metadata
249
+ state = state.with_metadata("node_count", len(graph.nodes))
250
+ state = state.with_metadata("edge_count", len(graph.edges))
251
+ state = state.with_metadata("authorized_namespaces", authorized_namespaces)
252
+ if governance_closure is not None:
253
+ state = state.with_metadata("governance_closure", governance_closure)
254
+
255
+ return state
256
+
257
+
258
+ def _compute_governance_closure(
259
+ builder: GraphBuilder,
260
+ build_config: dict[str, Any],
261
+ ) -> dict[str, Any] | None:
262
+ """Deterministic digest over the exact imported governance set (design §6).
263
+
264
+ The hash covers each imported invariant's FQDN + content_hash, sorted — so it changes only
265
+ when the governance that checked this build changes, not when unrelated platform capabilities
266
+ do. Returns None for the platform build (no import_surface, nothing imported).
267
+ """
268
+ imp = (build_config.get("artifact_discovery", {}) or {}).get("import_surface", {}) or {}
269
+ domain = imp.get("domain")
270
+ if not domain:
271
+ return None
272
+
273
+ members = sorted(
274
+ (n.fqdn, n.content_hash)
275
+ for n in builder._nodes.values()
276
+ if (n.metadata or {}).get("import_role") == "governance"
277
+ )
278
+ h = hashlib.sha256()
279
+ for fqdn, content_hash in members:
280
+ h.update(fqdn.encode("utf-8"))
281
+ h.update(b"\x00")
282
+ h.update((content_hash or "").encode("utf-8"))
283
+ h.update(b"\x00")
284
+ return {
285
+ "import_domain": domain,
286
+ "governance_closure_hash": h.hexdigest(),
287
+ "closure_member_count": len(members),
288
+ }
289
+
290
+
291
+ def _discover_artifacts(
292
+ search_layers: list[str],
293
+ discovery_layers: dict[str, Any],
294
+ discovery_rules: dict[str, Any],
295
+ build_artifact_types: list[str],
296
+ errors: list[CompilerError],
297
+ ) -> list[dict[str, Any]]:
298
+ """Discover artifact files from declared layers."""
299
+ all_discovered: list[dict[str, Any]] = []
300
+ seen_paths: set[Path] = set()
301
+
302
+ artifact_pattern = re.compile(discovery_rules.get("filename_pattern", ""))
303
+ excluded_dirs = discovery_rules.get("excluded_directories", [])
304
+
305
+ for layer_code in search_layers:
306
+ layer_config = discovery_layers.get(layer_code)
307
+ if not layer_config:
308
+ errors.append(CompilerError(
309
+ code=ErrorCode.E901_INTERNAL_ERROR,
310
+ message=f"Layer {layer_code} not found in STRUCTURE_DISCOVERY_V0",
311
+ phase="S1_EXTRACT",
312
+ ))
313
+ continue
314
+
315
+ if layer_code == "DOMAINS":
316
+ allowed_domains = layer_config.get("allowed_domains", [])
317
+ if not allowed_domains:
318
+ errors.append(CompilerError(
319
+ code=ErrorCode.E901_INTERNAL_ERROR,
320
+ message="DOMAINS layer missing allowed_domains declaration",
321
+ phase="S1_EXTRACT",
322
+ ))
323
+ continue
324
+
325
+ for domain_name in allowed_domains:
326
+ _scan_layer(
327
+ layer_code, layer_config, artifact_pattern,
328
+ excluded_dirs, all_discovered, seen_paths,
329
+ errors, domain_name=domain_name,
330
+ )
331
+ else:
332
+ _scan_layer(
333
+ layer_code, layer_config, artifact_pattern,
334
+ excluded_dirs, all_discovered, seen_paths,
335
+ errors, domain_name=None,
336
+ )
337
+
338
+ # Apply scope filter
339
+ filtered = [
340
+ a for a in all_discovered
341
+ if a["artifact_type"] in build_artifact_types
342
+ ]
343
+
344
+ return sorted(filtered, key=lambda x: (x["artifact_code"], x["source_path"]))
345
+
346
+
347
+ def _scan_layer(
348
+ layer_code: str,
349
+ layer_config: dict[str, Any],
350
+ artifact_pattern: re.Pattern,
351
+ excluded_dirs: list[str],
352
+ artifacts: list[dict[str, Any]],
353
+ seen_paths: set[Path],
354
+ errors: list[CompilerError],
355
+ domain_name: str | None = None,
356
+ ) -> None:
357
+ """Scan a layer's registry module recursively for artifact files."""
358
+ registry_module = layer_config.get("registry_module", "")
359
+ resolver = LayerResolver()
360
+
361
+ if layer_code == "DOMAINS" and domain_name:
362
+ root_path = resolver.resolve_layer_root(layer_code, domain=domain_name)
363
+ else:
364
+ root_path = resolver.resolve_layer_root(layer_code)
365
+
366
+ if not root_path.exists():
367
+ errors.append(CompilerError(
368
+ code=ErrorCode.E901_INTERNAL_ERROR,
369
+ message=f"Layer {layer_code} registry path does not exist: {root_path}",
370
+ phase="S1_EXTRACT",
371
+ ))
372
+ return
373
+
374
+ for md_path in root_path.rglob("*.md"):
375
+ if md_path.resolve() in seen_paths:
376
+ continue
377
+ seen_paths.add(md_path.resolve())
378
+
379
+ if any(excl in md_path.parts for excl in excluded_dirs):
380
+ continue
381
+
382
+ match = artifact_pattern.match(md_path.name)
383
+ if not match:
384
+ continue
385
+
386
+ artifact_type = match.group("type")
387
+ artifact_name = match.group("name")
388
+ version = match.group("version")
389
+ artifact_code = f"{artifact_type}_{artifact_name}_V{version}"
390
+
391
+ rel_path = md_path.parent.relative_to(root_path)
392
+ if str(rel_path) == ".":
393
+ full_module_path = registry_module
394
+ else:
395
+ full_module_path = f"{registry_module}.{str(rel_path).replace('/', '.')}"
396
+
397
+ entry: dict[str, Any] = {
398
+ "artifact_code": artifact_code,
399
+ "artifact_type": artifact_type,
400
+ "layer_code": layer_code,
401
+ "module_path": full_module_path,
402
+ "source_path": str(md_path),
403
+ "version": version,
404
+ }
405
+
406
+ if domain_name:
407
+ entry["domain_name"] = domain_name
408
+
409
+ artifacts.append(entry)
410
+
411
+
412
+ def _read_declared_fqdn(source_path: str) -> str | None:
413
+ """Extract the authoritative `fqdn` declared in an artifact's Machine block.
414
+
415
+ Identity is declared by the artifact, not derived from its folder. Returns None if the
416
+ file has no Machine block or no `fqdn` field (a hard error upstream).
417
+ """
418
+ try:
419
+ content = Path(source_path).read_text(encoding="utf-8")
420
+ except Exception:
421
+ return None
422
+ m = _MACHINE_BLOCK_PATTERN.search(content)
423
+ if not m:
424
+ return None
425
+ try:
426
+ block = yaml.safe_load(m.group("machine_yaml").rstrip())
427
+ except yaml.YAMLError:
428
+ return None
429
+ if isinstance(block, dict):
430
+ fq = block.get("fqdn")
431
+ return fq if isinstance(fq, str) and "::" in fq else None
432
+ return None
433
+
434
+
435
+ def _derive_fqdns(
436
+ discovered: list[dict[str, Any]],
437
+ identity_rules: list[dict[str, Any]],
438
+ errors: list[CompilerError],
439
+ ) -> None:
440
+ """Resolve each artifact's identity from its DECLARED `fqdn`.
441
+
442
+ The filesystem location has no semantic authority over identity (semantic alignment). Path
443
+ derivation is fully retired: identity is the artifact's own declaration, and the namespace is
444
+ checked against the authorized set by INVARIANT_FQDN_NAMESPACE_AUTHORIZED_V0. `identity_rules`
445
+ is unused here — it supplies only the authorized-namespace allowlist, built in s1_extract.
446
+ """
447
+ for artifact in discovered:
448
+ declared = _read_declared_fqdn(artifact.get("source_path", ""))
449
+ if not declared:
450
+ errors.append(CompilerError(
451
+ code=ErrorCode.E104_INVALID_FQDN,
452
+ message=(
453
+ f"Artifact declares no authoritative `fqdn` in its Machine block "
454
+ f"(artifact_code={artifact['artifact_code']}). Identity must be declared."
455
+ ),
456
+ phase="S1_EXTRACT",
457
+ ))
458
+ continue
459
+
460
+ artifact["namespace"] = declared.split("::", 1)[0]
461
+ artifact["fqdn"] = declared
462
+
463
+
464
+ def _inject_imported_capabilities(
465
+ builder: GraphBuilder,
466
+ all_refs: set[str],
467
+ build_config: dict[str, Any],
468
+ errors: list[CompilerError],
469
+ ) -> None:
470
+ """Lift imported platform capabilities (CS/CT) that this domain CONSUMES into the graph.
471
+
472
+ Governance imports (constitutions, execution structure) are resolve-only — S2 tolerates and drops
473
+ them. But a capability the domain *invokes* in a CC pipeline must be executable here: its compiled
474
+ node is loaded from the imported domain's canonical snapshot (bringing its machine.implementation),
475
+ added to this graph, and then addressed + wired + emitted like a native node. The capability's
476
+ governance/authoring stays platform-owned; the consumer carries only its execution binding.
477
+ """
478
+ import json
479
+
480
+ imp = (build_config.get("artifact_discovery", {}) or {}).get("import_surface", {}) or {}
481
+ domain = imp.get("domain")
482
+ if not domain:
483
+ return
484
+
485
+ from compiler.governance_engine.platform_root import platform_root
486
+ canon_root = platform_root() / "snapshot" / "compiled" / "canonical"
487
+ if not canon_root.is_dir():
488
+ return
489
+
490
+ # Carried capabilities emit under THIS domain's own layer (they become part of its runtime
491
+ # substrate) — whatever that layer is; no domain is named here. metadata.imported marks origin.
492
+ search_layers = (build_config.get("artifact_discovery", {}) or {}).get("search_layers", [])
493
+ if not search_layers:
494
+ return
495
+ domain_layer = search_layers[0]
496
+
497
+ for fqdn in sorted(all_refs):
498
+ if "::" not in fqdn or fqdn in builder._nodes:
499
+ continue
500
+ namespace, artifact_code = fqdn.split("::", 1)
501
+ if artifact_code.split("_")[0] not in ("CS", "CT"):
502
+ continue # only capabilities are carried; governance stays resolve-only (S2)
503
+ matches = list(canon_root.rglob(fqdn.replace("::", "__") + ".json"))
504
+ if not matches:
505
+ continue # not a compiled imported capability — leave to S2 tolerance
506
+ raw = json.loads(matches[0].read_text(encoding="utf-8"))
507
+ kind = _type_to_kind(raw.get("artifact_type"))
508
+ if kind is None:
509
+ continue
510
+ m = re.search(r"_V(\d+)$", artifact_code)
511
+ builder.add_node(Node.create(
512
+ fqdn=fqdn,
513
+ kind=kind,
514
+ namespace=namespace,
515
+ artifact_code=artifact_code,
516
+ version=f"V{m.group(1)}" if m else "V0",
517
+ layer_code=domain_layer,
518
+ content_hash=raw.get("content_hash", ""),
519
+ frontmatter=raw.get("frontmatter", {}), # carries machine.implementation → cs_ir/ct_ir in S5
520
+ domain_name=None,
521
+ metadata={
522
+ "imported": True,
523
+ "import_role": "execution", # linked into the domain runtime AND emitted
524
+ "import_domain": domain,
525
+ "module_path": raw.get("module_path", ""),
526
+ },
527
+ ))
528
+
529
+
530
+ # Artifact kinds a domain build instantiates. An imported invariant is domain-applicable
531
+ # iff its declared scope.applies_to intersects this set (design §2). The set is the single
532
+ # derivation point for domain-vs-platform applicability — never a declared build token.
533
+ _DOMAIN_INSTANTIATED = frozenset({"WF", "CC", "CS", "CT", "RB", "AC", "IN", "EV", "TI", "TE"})
534
+
535
+
536
+ def _inject_imported_governance(
537
+ builder: GraphBuilder,
538
+ build_config: dict[str, Any],
539
+ errors: list[CompilerError],
540
+ ) -> None:
541
+ """Import the platform's normative substrate as checked protocol state.
542
+
543
+ **A domain compile operates over its normative closure, not merely its authored artifacts.**
544
+ A domain is not an independent language — it is written in the PGC language, so every domain
545
+ compilation must begin with that language already in scope. Whatever is required to interpret,
546
+ validate and constrain the domain is lifted from the imported platform snapshot into this
547
+ domain's graph before S4 runs.
548
+
549
+ Two kinds make up that closure today:
550
+
551
+ * INVARIANT — the rules asserted against the domain's artifacts. Applicability is derived,
552
+ not declared: imported iff its applies_to_kinds intersects the domain-instantiated kinds.
553
+ Platform-only invariants (COMPILER/INVARIANT/CONSTITUTION/SNAPSHOT/... scopes) have no
554
+ domain subject and are never imported.
555
+ * VOCABULARY — the symbol space those rules are evaluated against. Imported unconditionally:
556
+ a vocabulary has no "subject" to intersect, and a build carrying no vocabulary cannot be
557
+ measured against one, so omitting it silently disables every symbol rule.
558
+
559
+ Unlike capabilities (import_role="execution", linked + emitted), the closure is
560
+ import_role="governance": it acts on the domain graph and is dropped before materialize
561
+ (canonical projection skips it) — it is the asserter, never a subject, and never part of the
562
+ domain's artifact set. `_compute_governance_closure` digests exactly this set, so anything
563
+ imported here is bound into the domain's governance provenance automatically.
564
+ """
565
+ import json
566
+
567
+ imp = (build_config.get("artifact_discovery", {}) or {}).get("import_surface", {}) or {}
568
+ domain = imp.get("domain")
569
+ if not domain:
570
+ return # platform build: no import_surface, no injection — must stay a no-op
571
+
572
+ from compiler.governance_engine.platform_root import platform_root
573
+ canonical = platform_root() / "snapshot" / "compiled" / "canonical"
574
+
575
+ search_layers = (build_config.get("artifact_discovery", {}) or {}).get("search_layers", [])
576
+ if not search_layers:
577
+ return
578
+ domain_layer = search_layers[0]
579
+
580
+ def _admits_invariant(raw: dict[str, Any]) -> bool:
581
+ proj = (raw.get("frontmatter", {}) or {}).get("assert_projection", {}) or {}
582
+ if not (set(proj.get("applies_to_kinds", []) or []) & _DOMAIN_INSTANTIATED):
583
+ return False # platform-only invariant — no domain subject
584
+ # A layer/surface-scoped invariant governs a specific surface (its allowed-list is that
585
+ # surface's); domains declare their own surface-closure, so it is not generically imported.
586
+ return not (proj.get("scope", {}) or {}).get("applies_to")
587
+
588
+ # The normative closure, by materialization directory. A vocabulary has no subject to
589
+ # intersect, so it carries no admission filter — it is the language, imported whole.
590
+ closure_sources = (
591
+ ("invariants", _admits_invariant),
592
+ ("vocabulary", lambda raw: True),
593
+ )
594
+
595
+ for subdir, admits in closure_sources:
596
+ source_root = canonical / subdir
597
+ if not source_root.is_dir():
598
+ continue
599
+ for path in sorted(source_root.glob("*.json")):
600
+ raw = json.loads(path.read_text(encoding="utf-8"))
601
+ fm = raw.get("frontmatter", {}) or {}
602
+ if not admits(raw):
603
+ continue
604
+ _inject_closure_node(builder, raw, fm, domain, domain_layer)
605
+
606
+
607
+ def _inject_closure_node(
608
+ builder: GraphBuilder,
609
+ raw: dict[str, Any],
610
+ fm: dict[str, Any],
611
+ domain: str,
612
+ domain_layer: str,
613
+ ) -> None:
614
+ """Add one imported normative-closure artifact to the domain graph (import_role=governance)."""
615
+ fqdn = raw.get("fqdn_id")
616
+ if not fqdn or fqdn in builder._nodes:
617
+ return
618
+ namespace, artifact_code = fqdn.split("::", 1)
619
+ kind = _type_to_kind(raw.get("artifact_type"))
620
+ if kind is None:
621
+ return
622
+ m = re.search(r"_V(\d+)$", artifact_code)
623
+ builder.add_node(Node.create(
624
+ fqdn=fqdn,
625
+ kind=kind,
626
+ namespace=namespace,
627
+ artifact_code=artifact_code,
628
+ version=f"V{m.group(1)}" if m else "V0",
629
+ layer_code=domain_layer,
630
+ content_hash=raw.get("content_hash", ""),
631
+ frontmatter=fm, # carries scope + assert_projection → derived ASSERT in S4
632
+ domain_name=None,
633
+ metadata={
634
+ "imported": True,
635
+ "import_role": "governance", # asserts the domain graph; NOT emitted
636
+ "import_domain": domain,
637
+ "module_path": raw.get("module_path", ""),
638
+ },
639
+ ))
640
+
641
+
642
+ def _parse_artifact_to_node(
643
+ artifact: dict[str, Any],
644
+ artifact_registry: dict[str, list[str]],
645
+ ) -> tuple[Node | None, list[str], list[CompilerError], list[CompilerError]]:
646
+ """
647
+ Parse a single artifact file into a Node + reference list.
648
+
649
+ Returns:
650
+ (node_or_None, reference_fqdns, errors, warnings)
651
+ """
652
+ errors: list[CompilerError] = []
653
+ warnings: list[CompilerError] = []
654
+
655
+ fqdn = artifact["fqdn"]
656
+ artifact_code = artifact["artifact_code"]
657
+ source_path = Path(artifact["source_path"])
658
+
659
+ # Read source file
660
+ try:
661
+ content_raw = source_path.read_text(encoding="utf-8")
662
+ except Exception as e:
663
+ errors.append(CompilerError(
664
+ code=ErrorCode.E901_INTERNAL_ERROR,
665
+ message=f"Failed to read file: {e}",
666
+ phase="S1_EXTRACT",
667
+ fqdn_id=fqdn,
668
+ artifact_code=artifact_code,
669
+ ))
670
+ return None, [], errors, warnings
671
+
672
+ # Extract Machine block
673
+ match = _MACHINE_BLOCK_PATTERN.search(content_raw)
674
+ if not match:
675
+ errors.append(CompilerError(
676
+ code=ErrorCode.E101_INVALID_YAML,
677
+ message="No ## Machine block found",
678
+ phase="S1_EXTRACT",
679
+ fqdn_id=fqdn,
680
+ artifact_code=artifact_code,
681
+ ))
682
+ return None, [], errors, warnings
683
+
684
+ try:
685
+ frontmatter = yaml.safe_load(match.group("machine_yaml").rstrip())
686
+ except yaml.YAMLError as e:
687
+ errors.append(CompilerError(
688
+ code=ErrorCode.E101_INVALID_YAML,
689
+ message=f"YAML parse error in Machine block: {e}",
690
+ phase="S1_EXTRACT",
691
+ fqdn_id=fqdn,
692
+ artifact_code=artifact_code,
693
+ ))
694
+ return None, [], errors, warnings
695
+
696
+ if not isinstance(frontmatter, dict):
697
+ errors.append(CompilerError(
698
+ code=ErrorCode.E101_INVALID_YAML,
699
+ message="Machine block YAML must be a dictionary",
700
+ phase="S1_EXTRACT",
701
+ fqdn_id=fqdn,
702
+ artifact_code=artifact_code,
703
+ ))
704
+ return None, [], errors, warnings
705
+
706
+ # Reject deprecated artifacts
707
+ if frontmatter.get("status") == "deprecated":
708
+ warnings.append(CompilerError(
709
+ code=ErrorCode.E005_DEPRECATED_ARTIFACT,
710
+ message=f"Skipping deprecated artifact: {artifact_code}",
711
+ phase="S1_EXTRACT",
712
+ fqdn_id=fqdn,
713
+ artifact_code=artifact_code,
714
+ ))
715
+ return None, [], errors, warnings
716
+
717
+ # Integrity over a CANONICAL FORM of the semantic object, not over the serialization.
718
+ #
719
+ # `2c` MB-3: "Equality and identity MUST be defined over the semantic object. Integrity MUST be
720
+ # computed over a canonical form of the semantic object." `4c` ID-3 and `2d` KV-8 say the same
721
+ # from their own subjects — a representation change preserving meaning must not change identity.
722
+ #
723
+ # This hashed `content_raw`, the whole markdown file, so two artifacts declaring identical
724
+ # meaning hashed differently if their YAML keys were written in a different order or their prose
725
+ # differed by a space. Measured before the change: swapping two sibling keys in a machine block
726
+ # left the parsed mapping identical and moved this hash.
727
+ #
728
+ # The canonical form is the parsed machine block serialised with sorted keys and fixed
729
+ # separators. Prose is excluded because prose declares nothing (MB-1) — an artifact's integrity
730
+ # value should not move when a comment is reworded.
731
+ content_hash = hashlib.sha256(
732
+ json.dumps(frontmatter, sort_keys=True, separators=(",", ":"), default=str).encode("utf-8")
733
+ ).hexdigest()
734
+
735
+ # Determine node kind. `artifact_kind` declared in the Machine block is the SOLE authoritative
736
+ # discriminator (Machine Block §6). The filename prefix is a naming convention only.
737
+ from compiler.governance_engine.artifact_kinds import REGISTRY
738
+ artifact_type = artifact["artifact_type"] # filename prefix — naming convention, not authority
739
+ declared_kind = frontmatter.get("artifact_kind")
740
+ if declared_kind:
741
+ nk = REGISTRY.node_kind_for_kind(declared_kind)
742
+ if nk is None:
743
+ errors.append(CompilerError(
744
+ code=ErrorCode.E103_TYPE_MISMATCH,
745
+ message=f"Unknown artifact_kind: {declared_kind!r}. Not in the Kind Vocabulary.",
746
+ phase="S1_EXTRACT",
747
+ fqdn_id=fqdn,
748
+ artifact_code=artifact_code,
749
+ ))
750
+ return None, [], errors, warnings
751
+ kind = NodeKind(nk)
752
+ else:
753
+ errors.append(CompilerError(
754
+ code=ErrorCode.E102_MISSING_FIELD,
755
+ message="Machine block declares no artifact_kind (the required discriminator).",
756
+ phase="S1_EXTRACT",
757
+ fqdn_id=fqdn,
758
+ artifact_code=artifact_code,
759
+ ))
760
+ return None, [], errors, warnings
761
+
762
+ # Extract references
763
+ references, ref_errors = _extract_references(
764
+ frontmatter, fqdn, artifact_code, artifact_registry
765
+ )
766
+ errors.extend(ref_errors)
767
+
768
+ # Create Node
769
+ node = Node.create(
770
+ fqdn=fqdn,
771
+ kind=kind,
772
+ namespace=artifact["namespace"],
773
+ artifact_code=artifact_code,
774
+ version=artifact["version"],
775
+ layer_code=artifact["layer_code"],
776
+ content_hash=content_hash,
777
+ frontmatter=frontmatter,
778
+ domain_name=artifact.get("domain_name"),
779
+ metadata={
780
+ "source_path": artifact["source_path"],
781
+ "module_path": artifact.get("module_path", ""),
782
+ "content": content_raw,
783
+ "references": sorted(references),
784
+ },
785
+ )
786
+
787
+ return node, references, errors, warnings
788
+
789
+
790
+ def _extract_references(
791
+ frontmatter: dict[str, Any],
792
+ source_fqdn: str,
793
+ artifact_code: str,
794
+ artifact_registry: dict[str, list[str]],
795
+ ) -> tuple[list[str], list[CompilerError]]:
796
+ """
797
+ Extract FQDN references from artifact frontmatter.
798
+
799
+ CONSTITUTIONAL: FQDN-only enforcement — bare codes are rejected.
800
+ """
801
+ references: set[str] = set()
802
+ errors: list[CompilerError] = []
803
+
804
+ def validate_ref(ref_value: str) -> str | None:
805
+ if "::" not in ref_value:
806
+ errors.append(CompilerError(
807
+ code=ErrorCode.E104_INVALID_FQDN,
808
+ message=f"Bare code forbidden: '{ref_value}'. Use FQDN (namespace::code)",
809
+ phase="S1_EXTRACT",
810
+ fqdn_id=source_fqdn,
811
+ artifact_code=artifact_code,
812
+ ))
813
+ return None
814
+
815
+ # Self-reference filter
816
+ if ref_value == source_fqdn:
817
+ return None
818
+
819
+ return ref_value
820
+
821
+ # Singular reference fields
822
+ singular_fields = ["vocabulary_id", "governed_by", "structure", "runtime_binding", "transform"]
823
+ # Plural reference fields. `consults` is an act's declared reach — the bindings it reads and
824
+ # never writes. Extracted as references so an unresolvable one is refused by surface closure;
825
+ # which of the named bindings is owned and which is consulted is read from the declaration
826
+ # itself, never from the resulting edges, because an edge kind is derived from the two node
827
+ # kinds and cannot tell one WF→RB reference from another.
828
+ plural_fields = ["transforms", "side_effects", "consults"]
829
+
830
+ # RB bindings: keys are artifact FQDNs
831
+ core = frontmatter.get("core", {})
832
+ if isinstance(core, dict) and "bindings" in core:
833
+ bindings = core["bindings"]
834
+ if isinstance(bindings, dict):
835
+ for binding_key in bindings.keys():
836
+ resolved = validate_ref(binding_key)
837
+ if resolved:
838
+ references.add(resolved)
839
+
840
+ def scan_recursive(data: Any) -> None:
841
+ if isinstance(data, dict):
842
+ for k, v in data.items():
843
+ if k in singular_fields:
844
+ # A singular reference field is usually a scalar FQDN, but `governed_by` (and
845
+ # potentially others) may be authored as a list of FQDNs. Both must become
846
+ # REFERENCES edges — otherwise a list-valued governed_by is silently dropped and
847
+ # its GOVERNED_BY edge never emitted (CSI Finding #001).
848
+ for item in ([v] if isinstance(v, str) else (v if isinstance(v, list) else [])):
849
+ if isinstance(item, str):
850
+ resolved = validate_ref(item)
851
+ if resolved:
852
+ references.add(resolved)
853
+ elif k in plural_fields and isinstance(v, list):
854
+ for item in v:
855
+ if isinstance(item, str):
856
+ resolved = validate_ref(item)
857
+ if resolved:
858
+ references.add(resolved)
859
+ else:
860
+ scan_recursive(v)
861
+ elif isinstance(data, list):
862
+ for item in data:
863
+ scan_recursive(item)
864
+
865
+ scan_recursive(frontmatter)
866
+
867
+ return sorted(references), errors