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.
- compiler/VERSION +1 -0
- compiler/__init__.py +1 -0
- compiler/atoms/__init__.py +54 -0
- compiler/atoms/error_codes.py +160 -0
- compiler/atoms/errors.py +125 -0
- compiler/atoms/fqdn.py +204 -0
- compiler/atoms/invariants.py +106 -0
- compiler/atoms/phase.py +99 -0
- compiler/atoms/pipeline.py +51 -0
- compiler/atoms/snapshot_gate.py +51 -0
- compiler/atoms/sorting.py +125 -0
- compiler/cli.py +637 -0
- compiler/diagnostics/__init__.py +6 -0
- compiler/diagnostics/determination.py +148 -0
- compiler/diagnostics/machine_block_health.py +199 -0
- compiler/governance/__init__.py +6 -0
- compiler/governance_engine/__init__.py +0 -0
- compiler/governance_engine/artifact_kinds.py +221 -0
- compiler/governance_engine/assertions/__init__.py +0 -0
- compiler/governance_engine/assertions/handlers/__init__.py +200 -0
- compiler/governance_engine/assertions/handlers/assert_ac_declaration_well_formed_v0.py +70 -0
- compiler/governance_engine/assertions/handlers/assert_actor_authority_separation_v0.py +17 -0
- compiler/governance_engine/assertions/handlers/assert_artifact_content_hash_declared_v0.py +60 -0
- compiler/governance_engine/assertions/handlers/assert_assert_not_runtime_referenced_v0.py +49 -0
- compiler/governance_engine/assertions/handlers/assert_assert_parity_v0.py +169 -0
- compiler/governance_engine/assertions/handlers/assert_atom_output_purity_v0.py +186 -0
- compiler/governance_engine/assertions/handlers/assert_authority_constituted_v0.py +56 -0
- compiler/governance_engine/assertions/handlers/assert_authority_required_for_execution_v0.py +16 -0
- compiler/governance_engine/assertions/handlers/assert_authority_state_well_formed_v0.py +17 -0
- compiler/governance_engine/assertions/handlers/assert_binding_integrity_v0.py +64 -0
- compiler/governance_engine/assertions/handlers/assert_binding_surface_closed_v0.py +92 -0
- compiler/governance_engine/assertions/handlers/assert_cc_capability_binding_valid_v0.py +85 -0
- compiler/governance_engine/assertions/handlers/assert_cc_inputs_satisfied_v0.py +89 -0
- compiler/governance_engine/assertions/handlers/assert_cc_no_implicit_chaining_v0.py +89 -0
- compiler/governance_engine/assertions/handlers/assert_cc_no_missing_dependencies_v0.py +89 -0
- compiler/governance_engine/assertions/handlers/assert_cc_no_unused_outputs_v0.py +81 -0
- compiler/governance_engine/assertions/handlers/assert_cc_storage_op_conformance_v0.py +74 -0
- compiler/governance_engine/assertions/handlers/assert_compiler_governance_declared_v0.py +65 -0
- compiler/governance_engine/assertions/handlers/assert_compiler_no_execution_v0.py +86 -0
- compiler/governance_engine/assertions/handlers/assert_concern_not_authority_v0.py +51 -0
- compiler/governance_engine/assertions/handlers/assert_conformance_assertion_mode_valid_v0.py +35 -0
- compiler/governance_engine/assertions/handlers/assert_cryptographic_trust_declared_v0.py +51 -0
- compiler/governance_engine/assertions/handlers/assert_cs_isolated_execution_v0.py +35 -0
- compiler/governance_engine/assertions/handlers/assert_cs_surface_closed_v0.py +241 -0
- compiler/governance_engine/assertions/handlers/assert_cs_traceable_v0.py +35 -0
- compiler/governance_engine/assertions/handlers/assert_ct_output_contract_match_v0.py +167 -0
- compiler/governance_engine/assertions/handlers/assert_ct_surface_closed_v0.py +173 -0
- compiler/governance_engine/assertions/handlers/assert_ct_surface_derived_closed_v0.py +108 -0
- compiler/governance_engine/assertions/handlers/assert_ct_test_data_outcome_declared_v0.py +112 -0
- compiler/governance_engine/assertions/handlers/assert_ev_append_only_v0.py +70 -0
- compiler/governance_engine/assertions/handlers/assert_ev_schema_required_v0.py +59 -0
- compiler/governance_engine/assertions/handlers/assert_execution_placement_declared_v0.py +51 -0
- compiler/governance_engine/assertions/handlers/assert_execution_scheduling_declared_v0.py +51 -0
- compiler/governance_engine/assertions/handlers/assert_fqdn_namespace_authorized_v0.py +38 -0
- compiler/governance_engine/assertions/handlers/assert_fqdn_only_references_v0.py +89 -0
- compiler/governance_engine/assertions/handlers/assert_governance_declaration_resolves_v0.py +126 -0
- compiler/governance_engine/assertions/handlers/assert_handler_registry_closed_v0.py +75 -0
- compiler/governance_engine/assertions/handlers/assert_identity_authority_separation_v0.py +19 -0
- compiler/governance_engine/assertions/handlers/assert_identity_fqdn_consistency.py +75 -0
- compiler/governance_engine/assertions/handlers/assert_implementation_admissible_v0.py +66 -0
- compiler/governance_engine/assertions/handlers/assert_in_no_execution_logic_v0.py +82 -0
- compiler/governance_engine/assertions/handlers/assert_in_schema_required_v0.py +74 -0
- compiler/governance_engine/assertions/handlers/assert_in_workflow_binding_v0.py +101 -0
- compiler/governance_engine/assertions/handlers/assert_inspection_capability_read_only_v0.py +114 -0
- compiler/governance_engine/assertions/handlers/assert_no_ambient_authority_v0.py +17 -0
- compiler/governance_engine/assertions/handlers/assert_no_runtime_authorization_v0.py +18 -0
- compiler/governance_engine/assertions/handlers/assert_no_runtime_topology_synthesis_v0.py +18 -0
- compiler/governance_engine/assertions/handlers/assert_no_smart_execution_v0.py +102 -0
- compiler/governance_engine/assertions/handlers/assert_no_undeclared_behavior_surface_v0.py +52 -0
- compiler/governance_engine/assertions/handlers/assert_no_workflow_authorization_logic_v0.py +18 -0
- compiler/governance_engine/assertions/handlers/assert_protocol_surface_closed_v0.py +119 -0
- compiler/governance_engine/assertions/handlers/assert_rb_binding_policy_conformance_v0.py +124 -0
- compiler/governance_engine/assertions/handlers/assert_rb_cs_only_v0.py +68 -0
- compiler/governance_engine/assertions/handlers/assert_rb_no_logic_v0.py +81 -0
- compiler/governance_engine/assertions/handlers/assert_rb_parameters_declared_v0.py +117 -0
- compiler/governance_engine/assertions/handlers/assert_rb_storage_subdomain_owned_v0.py +118 -0
- compiler/governance_engine/assertions/handlers/assert_runtime_invariant_wired_v0.py +163 -0
- compiler/governance_engine/assertions/handlers/assert_schema_conformance_v0.py +64 -0
- compiler/governance_engine/assertions/handlers/assert_schema_description_well_formed_v0.py +97 -0
- compiler/governance_engine/assertions/handlers/assert_security_domain_declared_v0.py +51 -0
- compiler/governance_engine/assertions/handlers/assert_structure_paths_well_formed_v0.py +118 -0
- compiler/governance_engine/assertions/handlers/assert_superseded_not_referenced_v0.py +134 -0
- compiler/governance_engine/assertions/handlers/assert_test_data_match_ct_output_v0.py +134 -0
- compiler/governance_engine/assertions/handlers/assert_topology_acyclic_v0.py +53 -0
- compiler/governance_engine/assertions/handlers/assert_topology_authority_orthogonal_v0.py +110 -0
- compiler/governance_engine/assertions/handlers/assert_topology_capability_reference_unique_v0.py +63 -0
- compiler/governance_engine/assertions/handlers/assert_topology_contract_closed_v0.py +113 -0
- compiler/governance_engine/assertions/handlers/assert_topology_immutable_after_compilation_v0.py +17 -0
- compiler/governance_engine/assertions/handlers/assert_topology_input_reference_declared_v0.py +112 -0
- compiler/governance_engine/assertions/handlers/assert_topology_routing_complete_v0.py +82 -0
- compiler/governance_engine/assertions/handlers/assert_topology_step_declared_v0.py +77 -0
- compiler/governance_engine/assertions/handlers/assert_topology_step_id_unique_v0.py +60 -0
- compiler/governance_engine/assertions/handlers/assert_topology_surface_canonical_v0.py +180 -0
- compiler/governance_engine/assertions/handlers/assert_topology_transport_orthogonal_v0.py +113 -0
- compiler/governance_engine/assertions/handlers/assert_trace_authority_binding_required_v0.py +18 -0
- compiler/governance_engine/assertions/handlers/assert_transport_canonical_normalization_v0.py +83 -0
- compiler/governance_engine/assertions/handlers/assert_transport_no_dynamic_routing_v0.py +93 -0
- compiler/governance_engine/assertions/handlers/assert_transport_no_workflow_semantics_v0.py +106 -0
- compiler/governance_engine/assertions/handlers/assert_transport_operation_identity_independence_v0.py +56 -0
- compiler/governance_engine/assertions/handlers/assert_transport_response_projection_external_v0.py +38 -0
- compiler/governance_engine/assertions/handlers/assert_transport_result_class_protocol_independence_v0.py +48 -0
- compiler/governance_engine/assertions/handlers/assert_transport_target_exists_v0.py +132 -0
- compiler/governance_engine/assertions/handlers/assert_unique_artifact_id_v0.py +58 -0
- compiler/governance_engine/assertions/handlers/assert_vocabulary_symbols_well_formed_v0.py +163 -0
- compiler/governance_engine/assertions/handlers/assert_wf_announcement_distinct_v0.py +85 -0
- compiler/governance_engine/assertions/handlers/assert_wf_cc_only_nodes_v0.py +60 -0
- compiler/governance_engine/assertions/handlers/assert_wf_entry_intent_v0.py +68 -0
- compiler/governance_engine/assertions/handlers/assert_wf_execution_path_valid_v0.py +89 -0
- compiler/governance_engine/assertions/handlers/assert_wf_node_key_binding_unique_v0.py +107 -0
- compiler/governance_engine/platform_root.py +102 -0
- compiler/governance_engine/structure/__init__.py +56 -0
- compiler/governance_engine/structure/discovery/__init__.py +28 -0
- compiler/governance_engine/structure/discovery/artifact_locator.py +210 -0
- compiler/governance_engine/structure/exceptions.py +113 -0
- compiler/governance_engine/structure/loading/__init__.py +88 -0
- compiler/governance_engine/structure/loading/fs_reader.py +225 -0
- compiler/governance_engine/structure/loading/markdown_parser.py +148 -0
- compiler/governance_engine/structure/loading/protocol_loader.py +587 -0
- compiler/governance_engine/structure/loading/test_case_provider.py +125 -0
- compiler/governance_engine/structure/loading/trace_reader.py +98 -0
- compiler/governance_engine/structure/loading/vocabulary_loader.py +174 -0
- compiler/governance_engine/structure/loading/yaml_parser.py +209 -0
- compiler/governance_engine/structure/resolution/__init__.py +22 -0
- compiler/governance_engine/structure/resolution/domain_resolver.py +124 -0
- compiler/governance_engine/structure/resolution/layer_resolver.py +572 -0
- compiler/governance_engine/structure/resolution/path_registry.py +1054 -0
- compiler/graph/__init__.py +38 -0
- compiler/graph/addressing.py +153 -0
- compiler/graph/edge.py +100 -0
- compiler/graph/evidence.py +637 -0
- compiler/graph/graph.py +269 -0
- compiler/graph/hashing.py +89 -0
- compiler/graph/node.py +134 -0
- compiler/graph/query.py +215 -0
- compiler/graph/state.py +237 -0
- compiler/graph/trace.py +97 -0
- compiler/graph/types.py +71 -0
- compiler/projections/__init__.py +207 -0
- compiler/projections/artifact_index.py +225 -0
- compiler/projections/canonical.py +173 -0
- compiler/projections/dispatch.py +532 -0
- compiler/projections/evidence.py +170 -0
- compiler/projections/handlers.py +224 -0
- compiler/projections/ipm.py +388 -0
- compiler/projections/reverse.py +165 -0
- compiler/projections/tokenized.py +135 -0
- compiler/projections/vocabulary.py +108 -0
- compiler/stages/__init__.py +28 -0
- compiler/stages/s1_extract.py +867 -0
- compiler/stages/s2_canonicalize.py +516 -0
- compiler/stages/s3_semantic_addressing.py +66 -0
- compiler/stages/s4_govern.py +754 -0
- compiler/stages/s5_construct.py +537 -0
- compiler/stages/s6_project.py +94 -0
- compiler/stages/s7_materialize.py +1170 -0
- compiler/stages/s8_verify.py +643 -0
- compiler/stages/s9_attest.py +184 -0
- compiler/structure_loader.py +132 -0
- compiler/visualization/__init__.py +12 -0
- compiler/visualization/consumers/__init__.py +28 -0
- compiler/visualization/consumers/evidence_projection.py +162 -0
- compiler/visualization/consumers/evidence_query.py +296 -0
- compiler/visualization/consumers/evidence_reader.py +127 -0
- compiler/visualization/views/__init__.py +70 -0
- compiler/visualization/views/_png_writer.py +45 -0
- compiler/visualization/views/artifact_lineage.py +180 -0
- compiler/visualization/views/family_view.py +164 -0
- compiler/visualization/views/materialization_view.py +150 -0
- compiler/visualization/views/structural_dag.py +158 -0
- compiler/visualization/wf_graph_generator.py +534 -0
- pgc_compiler-2.0.0.dist-info/METADATA +203 -0
- pgc_compiler-2.0.0.dist-info/RECORD +177 -0
- pgc_compiler-2.0.0.dist-info/WHEEL +5 -0
- pgc_compiler-2.0.0.dist-info/entry_points.txt +2 -0
- pgc_compiler-2.0.0.dist-info/licenses/LICENSE +67 -0
- pgc_compiler-2.0.0.dist-info/licenses/NOTICE +11 -0
- 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
|