millforge 0.1.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 (116) hide show
  1. millforge/__init__.py +1174 -0
  2. millforge/_forge/LICENSE +21 -0
  3. millforge/_forge/PROVENANCE.json +295 -0
  4. millforge/_forge/UPDATE_POLICY.md +24 -0
  5. millforge/_forge/__init__.py +14 -0
  6. millforge/_forge/adapter.py +2232 -0
  7. millforge/_forge/base_runner.py +121 -0
  8. millforge/_forge/clients/__init__.py +10 -0
  9. millforge/_forge/clients/base.py +200 -0
  10. millforge/_forge/context/__init__.py +23 -0
  11. millforge/_forge/context/manager.py +178 -0
  12. millforge/_forge/context/strategies.py +335 -0
  13. millforge/_forge/core/__init__.py +16 -0
  14. millforge/_forge/core/inference.py +433 -0
  15. millforge/_forge/core/messages.py +119 -0
  16. millforge/_forge/core/runner.py +479 -0
  17. millforge/_forge/core/steps.py +108 -0
  18. millforge/_forge/core/workflow.py +400 -0
  19. millforge/_forge/errors.py +222 -0
  20. millforge/_forge/guardrails/__init__.py +21 -0
  21. millforge/_forge/guardrails/error_tracker.py +71 -0
  22. millforge/_forge/guardrails/guardrails.py +194 -0
  23. millforge/_forge/guardrails/nudge.py +47 -0
  24. millforge/_forge/guardrails/response_validator.py +119 -0
  25. millforge/_forge/guardrails/step_enforcer.py +183 -0
  26. millforge/_forge/prompts/__init__.py +16 -0
  27. millforge/_forge/prompts/nudges.py +95 -0
  28. millforge/_forge/prompts/templates.py +285 -0
  29. millforge/_version.py +3 -0
  30. millforge/artifacts.py +570 -0
  31. millforge/base/__init__.py +97 -0
  32. millforge/base/composition.py +402 -0
  33. millforge/base/context.py +285 -0
  34. millforge/base/harness.py +138 -0
  35. millforge/base/identity.py +465 -0
  36. millforge/base/options.py +34 -0
  37. millforge/base/platform.py +17 -0
  38. millforge/base/prompt.py +317 -0
  39. millforge/base/runner.py +546 -0
  40. millforge/compiled_plan.py +970 -0
  41. millforge/compiler/__init__.py +231 -0
  42. millforge/compiler/artifact_validation.py +257 -0
  43. millforge/compiler/canonicalization.py +169 -0
  44. millforge/compiler/capabilities.py +66 -0
  45. millforge/compiler/catalogs.py +500 -0
  46. millforge/compiler/diagnostics.py +491 -0
  47. millforge/compiler/graph.py +678 -0
  48. millforge/compiler/lowering.py +198 -0
  49. millforge/compiler/output.py +692 -0
  50. millforge/compiler/parsing.py +1424 -0
  51. millforge/compiler/requests.py +1180 -0
  52. millforge/compiler/schema_validation.py +272 -0
  53. millforge/compiler/semantic.py +490 -0
  54. millforge/compiler/service.py +448 -0
  55. millforge/compiler/source.py +375 -0
  56. millforge/compiler/validators.py +184 -0
  57. millforge/connectors/__init__.py +95 -0
  58. millforge/connectors/admission.py +801 -0
  59. millforge/connectors/broker.py +202 -0
  60. millforge/connectors/contracts.py +1159 -0
  61. millforge/connectors/diagnostics.py +189 -0
  62. millforge/connectors/fake.py +66 -0
  63. millforge/connectors/runtime.py +236 -0
  64. millforge/contracts.py +2860 -0
  65. millforge/custom_tools/__init__.py +67 -0
  66. millforge/custom_tools/compiler.py +724 -0
  67. millforge/custom_tools/contracts.py +1093 -0
  68. millforge/custom_tools/diagnostics.py +205 -0
  69. millforge/eval_artifacts.py +952 -0
  70. millforge/eval_boundary.py +2435 -0
  71. millforge/eval_fixtures/__init__.py +1 -0
  72. millforge/eval_fixtures/default_pack/__init__.py +1 -0
  73. millforge/eval_fixtures/default_pack/fixtures/fixture.08a.bug_diagnosis.traceback.v1.json +52 -0
  74. millforge/eval_fixtures/default_pack/fixtures/fixture.08a.direct_edit.import_sort.v1.json +52 -0
  75. millforge/eval_fixtures/default_pack/fixtures/fixture.08a.evidence_discipline.no_source_change.v1.json +51 -0
  76. millforge/eval_fixtures/default_pack/fixtures/fixture.08a.false_closure.visible_green.v1.json +52 -0
  77. millforge/eval_fixtures/default_pack/fixtures/fixture.08a.multi_file.api_contract.v1.json +54 -0
  78. millforge/eval_fixtures/default_pack/fixtures/fixture.08a.recovery.malformed_artifact.v1.json +54 -0
  79. millforge/eval_fixtures/default_pack/manifest.json +12 -0
  80. millforge/eval_modes.py +1282 -0
  81. millforge/eval_presets.py +1398 -0
  82. millforge/eval_reports.py +2517 -0
  83. millforge/eval_suite.py +2429 -0
  84. millforge/eval_trials.py +2632 -0
  85. millforge/eval_workflow.py +794 -0
  86. millforge/exceptions.py +122 -0
  87. millforge/model_backend.py +2098 -0
  88. millforge/protocols.py +340 -0
  89. millforge/py.typed +0 -0
  90. millforge/runtime.py +1791 -0
  91. millforge/testing/__init__.py +1089 -0
  92. millforge/tools/__init__.py +83 -0
  93. millforge/tools/builtin_runtime.py +1339 -0
  94. millforge/tools/builtins.py +773 -0
  95. millforge/tools/execution.py +1545 -0
  96. millforge/tools/path_policy.py +155 -0
  97. millforge/tools/pi_compat/PI_LICENSE +21 -0
  98. millforge/tools/pi_compat/PROVENANCE.json +55 -0
  99. millforge/tools/pi_compat/UPDATE_POLICY.md +36 -0
  100. millforge/tools/pi_compat/__init__.py +34 -0
  101. millforge/tools/pi_compat/contracts.py +49 -0
  102. millforge/tools/pi_compat/editing.py +390 -0
  103. millforge/tools/pi_compat/mutations.py +57 -0
  104. millforge/tools/pi_compat/operations.py +401 -0
  105. millforge/tools/pi_compat/paths.py +155 -0
  106. millforge/tools/pi_compat/process.py +1375 -0
  107. millforge/tools/pi_compat/search.py +738 -0
  108. millforge/tools/pi_compat/truncation.py +267 -0
  109. millforge/tools/pi_compat_catalog.py +396 -0
  110. millforge/tools/pi_compat_runtime.py +460 -0
  111. millforge/tools/registry.py +553 -0
  112. millforge/tools/results.py +533 -0
  113. millforge-0.1.0.dist-info/METADATA +844 -0
  114. millforge-0.1.0.dist-info/RECORD +116 -0
  115. millforge-0.1.0.dist-info/WHEEL +4 -0
  116. millforge-0.1.0.dist-info/licenses/LICENSE +201 -0
@@ -0,0 +1,231 @@
1
+ """Compiler front-end contracts for Millforge harness sources."""
2
+
3
+ from millforge.compiler.diagnostics import (
4
+ DIAGNOSTIC_REGISTRY,
5
+ DIAGNOSTIC_TRIGGER_MEANINGS,
6
+ CompilerDiagnostic,
7
+ CompilerPhase,
8
+ DiagnosticField,
9
+ DiagnosticSeverity,
10
+ SourceLocation,
11
+ SourceReference,
12
+ detect_secret_candidate,
13
+ sort_diagnostics,
14
+ )
15
+ from millforge.compiler.catalogs import (
16
+ CatalogLookupClassification,
17
+ CatalogMetadataError,
18
+ CatalogSnapshotMetadata,
19
+ ModelProfileCatalogLookup,
20
+ ModelProfileCatalogSnapshot,
21
+ RawToolDescriptor,
22
+ ToolCatalogEntry,
23
+ ToolCatalogLookup,
24
+ ToolCatalogSnapshot,
25
+ admit_model_profile,
26
+ capture_catalog_snapshot_metadata,
27
+ )
28
+ from millforge.compiler.artifact_validation import (
29
+ ArtifactProducerEvidence,
30
+ ArtifactValidationResult,
31
+ validate_artifacts,
32
+ )
33
+ from millforge.compiler.capabilities import (
34
+ CapabilityValidationResult,
35
+ validate_capability_grants,
36
+ )
37
+ from millforge.compiler.canonicalization import (
38
+ canonical_semantic_bytes,
39
+ canonical_semantic_payload,
40
+ source_sha256,
41
+ )
42
+ from millforge.compiler.graph import (
43
+ GraphValidationResult,
44
+ ResolvedNodeDescriptor,
45
+ validate_harness_graph,
46
+ )
47
+ from millforge.compiler.lowering import (
48
+ COMPILER_BUILD_ID,
49
+ COMPILER_NAME,
50
+ CompiledPlanValidationError,
51
+ LoweringInvariantError,
52
+ SourceSemanticHashError,
53
+ compiler_identity,
54
+ lower_resolved_harness,
55
+ )
56
+ from millforge.compiler.output import (
57
+ compiled_plan_output_path,
58
+ diagnostics_output_path,
59
+ persist_compile_outputs,
60
+ request_identity_sha256,
61
+ )
62
+ from millforge.compiler.parsing import (
63
+ HarnessSourceParser,
64
+ ParsedHarnessSource,
65
+ SourceDocument,
66
+ )
67
+ from millforge.compiler.requests import (
68
+ CompileInvocation,
69
+ CompileStatus,
70
+ DefaultHarnessCompileRequestAdmission,
71
+ DiagnosticReportState,
72
+ HarnessCompileRequest,
73
+ HarnessCompileRequestAdmission,
74
+ HarnessCompileResult,
75
+ HarnessRequestAdmissionResult,
76
+ PlanCommitCertainty,
77
+ )
78
+ from millforge.compiler.service import (
79
+ HarnessCompiler,
80
+ InMemoryHarnessCompileError,
81
+ compile,
82
+ compile_harness_source_in_memory,
83
+ compile_raw,
84
+ )
85
+ from millforge.compiler.semantic import (
86
+ ResolvedHarness,
87
+ ResolvedToolBinding,
88
+ ResolvedToolBindingRef,
89
+ SemanticCompileResult,
90
+ compile_semantic,
91
+ compile_semantic_from_admission,
92
+ )
93
+ from millforge.compiler.schema_validation import (
94
+ SCHEMA_ALLOWED_KEYWORDS,
95
+ SCHEMA_ANNOTATION_KEYWORDS,
96
+ SCHEMA_STRUCTURAL_KEYWORDS,
97
+ SchemaSubsetError,
98
+ normalize_json_schema,
99
+ normalized_schema_bytes,
100
+ property_schema_compatibility_bytes,
101
+ validate_json_schema_subset,
102
+ )
103
+ from millforge.compiler.source import (
104
+ ArgumentMatchSource,
105
+ ArtifactPolicySource,
106
+ BudgetSource,
107
+ ContextPolicySource,
108
+ HarnessGraphSource,
109
+ HarnessNodeSource,
110
+ HarnessSource,
111
+ PrerequisiteSource,
112
+ PromptSource,
113
+ StageScopeSource,
114
+ TerminalArtifactPolicySource,
115
+ )
116
+ from millforge.compiler.validators import (
117
+ ToolReference,
118
+ parse_tool_reference,
119
+ validate_argument_name,
120
+ validate_artifact_id,
121
+ validate_canonical_tool_id,
122
+ validate_harness_id,
123
+ validate_node_id,
124
+ validate_policy_id,
125
+ validate_profile_id,
126
+ validate_request_id,
127
+ validate_stage_kind_id,
128
+ validate_terminal_result,
129
+ validate_tool_reference,
130
+ )
131
+
132
+ __all__ = [
133
+ "ArgumentMatchSource",
134
+ "ArtifactProducerEvidence",
135
+ "ArtifactPolicySource",
136
+ "ArtifactValidationResult",
137
+ "BudgetSource",
138
+ "CapabilityValidationResult",
139
+ "CatalogLookupClassification",
140
+ "CatalogMetadataError",
141
+ "CatalogSnapshotMetadata",
142
+ "canonical_semantic_bytes",
143
+ "canonical_semantic_payload",
144
+ "CompileInvocation",
145
+ "CompileStatus",
146
+ "CompilerDiagnostic",
147
+ "CompilerPhase",
148
+ "ContextPolicySource",
149
+ "DIAGNOSTIC_REGISTRY",
150
+ "DIAGNOSTIC_TRIGGER_MEANINGS",
151
+ "DefaultHarnessCompileRequestAdmission",
152
+ "DiagnosticReportState",
153
+ "DiagnosticField",
154
+ "DiagnosticSeverity",
155
+ "HarnessCompileRequest",
156
+ "HarnessCompileRequestAdmission",
157
+ "HarnessCompileResult",
158
+ "HarnessCompiler",
159
+ "InMemoryHarnessCompileError",
160
+ "HarnessGraphSource",
161
+ "HarnessNodeSource",
162
+ "HarnessRequestAdmissionResult",
163
+ "HarnessSource",
164
+ "HarnessSourceParser",
165
+ "GraphValidationResult",
166
+ "LoweringInvariantError",
167
+ "COMPILER_BUILD_ID",
168
+ "COMPILER_NAME",
169
+ "CompiledPlanValidationError",
170
+ "ModelProfileCatalogLookup",
171
+ "ModelProfileCatalogSnapshot",
172
+ "ParsedHarnessSource",
173
+ "PlanCommitCertainty",
174
+ "PrerequisiteSource",
175
+ "PromptSource",
176
+ "SourceDocument",
177
+ "SourceLocation",
178
+ "SourceReference",
179
+ "StageScopeSource",
180
+ "TerminalArtifactPolicySource",
181
+ "RawToolDescriptor",
182
+ "ResolvedHarness",
183
+ "ResolvedNodeDescriptor",
184
+ "ResolvedToolBinding",
185
+ "ResolvedToolBindingRef",
186
+ "SCHEMA_ALLOWED_KEYWORDS",
187
+ "SCHEMA_ANNOTATION_KEYWORDS",
188
+ "SCHEMA_STRUCTURAL_KEYWORDS",
189
+ "SchemaSubsetError",
190
+ "SemanticCompileResult",
191
+ "SourceSemanticHashError",
192
+ "source_sha256",
193
+ "ToolCatalogEntry",
194
+ "ToolCatalogLookup",
195
+ "ToolCatalogSnapshot",
196
+ "ToolReference",
197
+ "admit_model_profile",
198
+ "capture_catalog_snapshot_metadata",
199
+ "compiler_identity",
200
+ "compiled_plan_output_path",
201
+ "compile",
202
+ "compile_harness_source_in_memory",
203
+ "compile_raw",
204
+ "compile_semantic",
205
+ "compile_semantic_from_admission",
206
+ "diagnostics_output_path",
207
+ "detect_secret_candidate",
208
+ "normalize_json_schema",
209
+ "normalized_schema_bytes",
210
+ "lower_resolved_harness",
211
+ "persist_compile_outputs",
212
+ "parse_tool_reference",
213
+ "property_schema_compatibility_bytes",
214
+ "request_identity_sha256",
215
+ "sort_diagnostics",
216
+ "validate_artifacts",
217
+ "validate_argument_name",
218
+ "validate_artifact_id",
219
+ "validate_canonical_tool_id",
220
+ "validate_harness_id",
221
+ "validate_harness_graph",
222
+ "validate_capability_grants",
223
+ "validate_node_id",
224
+ "validate_policy_id",
225
+ "validate_profile_id",
226
+ "validate_request_id",
227
+ "validate_stage_kind_id",
228
+ "validate_terminal_result",
229
+ "validate_tool_reference",
230
+ "validate_json_schema_subset",
231
+ ]
@@ -0,0 +1,257 @@
1
+ """Artifact satisfiability validation for semantic compiler resolution."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from collections import defaultdict
6
+ from collections.abc import Mapping
7
+
8
+ from pydantic import BaseModel, ConfigDict, Field, StrictStr
9
+
10
+ from millforge.compiler.catalogs import ToolCatalogEntry
11
+ from millforge.compiler.diagnostics import (
12
+ CompilerDiagnostic,
13
+ CompilerPhase,
14
+ DiagnosticField,
15
+ DiagnosticSeverity,
16
+ sort_diagnostics,
17
+ )
18
+ from millforge.compiler.graph import GraphValidationResult, ResolvedNodeDescriptor
19
+ from millforge.compiler.source import HarnessNodeSource, HarnessSource
20
+
21
+
22
+ class ArtifactProducerEvidence(BaseModel):
23
+ """Producer evidence for one artifact ID."""
24
+
25
+ model_config = ConfigDict(extra="forbid", frozen=True)
26
+
27
+ artifact_id: StrictStr
28
+ all_producer_node_ids: tuple[StrictStr, ...] = Field(default_factory=tuple)
29
+ terminal_gated_producer_node_ids: tuple[StrictStr, ...] = Field(
30
+ default_factory=tuple
31
+ )
32
+
33
+
34
+ class ArtifactValidationResult(BaseModel):
35
+ """Immutable artifact validation result."""
36
+
37
+ model_config = ConfigDict(extra="forbid", frozen=True)
38
+
39
+ diagnostics: tuple[CompilerDiagnostic, ...] = Field(default_factory=tuple)
40
+ producer_evidence: tuple[ArtifactProducerEvidence, ...] = Field(
41
+ default_factory=tuple
42
+ )
43
+
44
+ @property
45
+ def ok(self) -> bool:
46
+ return not self.diagnostics
47
+
48
+
49
+ def validate_artifacts(
50
+ source: HarnessSource,
51
+ resolved_nodes: Mapping[str, ToolCatalogEntry | ResolvedNodeDescriptor],
52
+ graph_result: GraphValidationResult,
53
+ ) -> ArtifactValidationResult:
54
+ """Validate source artifact declarations against resolved producer evidence."""
55
+ nodes_by_id = {node.node_id: node for node in source.graph.nodes}
56
+ descriptors = _resolved_entries(resolved_nodes)
57
+ declared = set(source.artifacts.declared_artifact_ids)
58
+ diagnostics: list[CompilerDiagnostic] = []
59
+ valid_producers: dict[str, list[str]] = defaultdict(list)
60
+
61
+ for artifact_id in _duplicates(source.artifacts.declared_artifact_ids):
62
+ diagnostics.append(
63
+ _diagnostic(
64
+ "MF-A006",
65
+ f"Declared artifact {artifact_id!r} is duplicated.",
66
+ artifact_id=artifact_id,
67
+ )
68
+ )
69
+
70
+ for node in sorted(source.graph.nodes, key=lambda item: item.node_id):
71
+ descriptor = descriptors.get(node.node_id)
72
+ if descriptor is None:
73
+ continue
74
+ descriptor_artifacts = set(descriptor.produced_artifact_ids)
75
+ for artifact_id in _duplicates(node.produces):
76
+ diagnostics.append(
77
+ _diagnostic(
78
+ "MF-A006",
79
+ f"Node {node.node_id!r} duplicates produced artifact {artifact_id!r}.",
80
+ node_id=node.node_id,
81
+ artifact_id=artifact_id,
82
+ )
83
+ )
84
+ for artifact_id in sorted(set(node.produces)):
85
+ if artifact_id not in declared:
86
+ diagnostics.append(
87
+ _diagnostic(
88
+ "MF-A001",
89
+ f"Node {node.node_id!r} produces undeclared artifact {artifact_id!r}.",
90
+ node_id=node.node_id,
91
+ artifact_id=artifact_id,
92
+ )
93
+ )
94
+ if artifact_id not in descriptor_artifacts:
95
+ diagnostics.append(
96
+ _diagnostic(
97
+ "MF-A002",
98
+ f"Node {node.node_id!r} cannot produce artifact {artifact_id!r}.",
99
+ node_id=node.node_id,
100
+ artifact_id=artifact_id,
101
+ )
102
+ )
103
+ if artifact_id in declared and artifact_id in descriptor_artifacts:
104
+ valid_producers[artifact_id].append(node.node_id)
105
+
106
+ terminal_results = set(graph_result.terminal_result_map.values())
107
+ terminal_nodes_by_result = {
108
+ result: node_id for node_id, result in graph_result.terminal_result_map.items()
109
+ }
110
+ for requirement in source.artifacts.required_by_terminal:
111
+ for artifact_id in _duplicates(requirement.artifact_ids):
112
+ diagnostics.append(
113
+ _diagnostic(
114
+ "MF-A006",
115
+ f"Terminal artifact policy {requirement.terminal_result!r} duplicates artifact {artifact_id!r}.",
116
+ artifact_id=artifact_id,
117
+ terminal_result=requirement.terminal_result,
118
+ )
119
+ )
120
+ if requirement.terminal_result not in terminal_results:
121
+ diagnostics.append(
122
+ _diagnostic(
123
+ "MF-A003",
124
+ f"Artifact policy references unknown terminal result {requirement.terminal_result!r}.",
125
+ terminal_result=requirement.terminal_result,
126
+ )
127
+ )
128
+ continue
129
+ for artifact_id in sorted(set(requirement.artifact_ids)):
130
+ if artifact_id not in declared:
131
+ diagnostics.append(
132
+ _diagnostic(
133
+ "MF-A007",
134
+ f"Terminal-required artifact {artifact_id!r} is not declared.",
135
+ artifact_id=artifact_id,
136
+ terminal_result=requirement.terminal_result,
137
+ )
138
+ )
139
+
140
+ producer_evidence: list[ArtifactProducerEvidence] = []
141
+ terminal_gated: dict[str, set[str]] = defaultdict(set)
142
+ if not graph_result.diagnostics:
143
+ ancestors_by_terminal = {
144
+ terminal_node_id: _ancestors(terminal_node_id, nodes_by_id)
145
+ for terminal_node_id in graph_result.terminal_node_ids
146
+ }
147
+ globally_required = set(graph_result.required_node_ids)
148
+ for requirement in source.artifacts.required_by_terminal:
149
+ terminal_node_id = terminal_nodes_by_result.get(requirement.terminal_result)
150
+ if terminal_node_id is None:
151
+ continue
152
+ gated_nodes = ancestors_by_terminal[terminal_node_id] | globally_required
153
+ for artifact_id in sorted(set(requirement.artifact_ids)):
154
+ if artifact_id not in declared:
155
+ continue
156
+ producers = set(valid_producers.get(artifact_id, ()))
157
+ gated_producers = producers & gated_nodes
158
+ terminal_gated[artifact_id].update(gated_producers)
159
+ if not producers:
160
+ diagnostics.append(
161
+ _diagnostic(
162
+ "MF-A004",
163
+ f"Terminal-required artifact {artifact_id!r} has no valid producer.",
164
+ artifact_id=artifact_id,
165
+ terminal_result=requirement.terminal_result,
166
+ )
167
+ )
168
+ elif not gated_producers:
169
+ diagnostics.append(
170
+ _diagnostic(
171
+ "MF-A005",
172
+ f"Terminal-required artifact {artifact_id!r} has no terminal-gated producer.",
173
+ artifact_id=artifact_id,
174
+ terminal_result=requirement.terminal_result,
175
+ )
176
+ )
177
+
178
+ for artifact_id in sorted(declared):
179
+ producer_evidence.append(
180
+ ArtifactProducerEvidence(
181
+ artifact_id=artifact_id,
182
+ all_producer_node_ids=tuple(
183
+ sorted(valid_producers.get(artifact_id, ()))
184
+ ),
185
+ terminal_gated_producer_node_ids=tuple(
186
+ sorted(terminal_gated.get(artifact_id, ()))
187
+ ),
188
+ )
189
+ )
190
+
191
+ return ArtifactValidationResult(
192
+ diagnostics=sort_diagnostics(diagnostics),
193
+ producer_evidence=tuple(producer_evidence),
194
+ )
195
+
196
+
197
+ def _resolved_entries(
198
+ resolved_nodes: Mapping[str, ToolCatalogEntry | ResolvedNodeDescriptor],
199
+ ) -> dict[str, ToolCatalogEntry]:
200
+ entries: dict[str, ToolCatalogEntry] = {}
201
+ for node_id, value in resolved_nodes.items():
202
+ entries[node_id] = (
203
+ value.descriptor if isinstance(value, ResolvedNodeDescriptor) else value
204
+ )
205
+ return entries
206
+
207
+
208
+ def _ancestors(
209
+ terminal_node_id: str,
210
+ nodes_by_id: Mapping[str, HarnessNodeSource],
211
+ ) -> set[str]:
212
+ reverse = {
213
+ node.node_id: tuple(prereq.node_id for prereq in node.prerequisites)
214
+ for node in nodes_by_id.values()
215
+ }
216
+ ancestors: set[str] = set()
217
+ stack = list(reverse[terminal_node_id])
218
+ while stack:
219
+ node_id = stack.pop()
220
+ if node_id in ancestors or node_id not in reverse:
221
+ continue
222
+ ancestors.add(node_id)
223
+ stack.extend(reverse[node_id])
224
+ return ancestors
225
+
226
+
227
+ def _duplicates(values: tuple[str, ...]) -> tuple[str, ...]:
228
+ seen: set[str] = set()
229
+ duplicates: set[str] = set()
230
+ for value in values:
231
+ if value in seen:
232
+ duplicates.add(value)
233
+ seen.add(value)
234
+ return tuple(sorted(duplicates))
235
+
236
+
237
+ def _diagnostic(
238
+ code: str,
239
+ message: str,
240
+ *,
241
+ node_id: str | None = None,
242
+ artifact_id: str | None = None,
243
+ terminal_result: str | None = None,
244
+ ) -> CompilerDiagnostic:
245
+ fields = []
246
+ if artifact_id is not None:
247
+ fields.append(DiagnosticField(key="artifact_id", value=artifact_id))
248
+ if terminal_result is not None:
249
+ fields.append(DiagnosticField(key="terminal_result", value=terminal_result))
250
+ return CompilerDiagnostic(
251
+ code=code,
252
+ phase=CompilerPhase.ARTIFACT,
253
+ severity=DiagnosticSeverity.ERROR,
254
+ message=message,
255
+ node_id=node_id,
256
+ fields=tuple(fields),
257
+ )
@@ -0,0 +1,169 @@
1
+ """Canonical semantic payload construction for deterministic compiler hashing."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import copy
6
+ import hashlib
7
+ from collections.abc import Mapping
8
+ from typing import Any, TypeAlias
9
+
10
+ from millforge import canonical_json_serialize
11
+ from millforge.compiler.semantic import ResolvedHarness
12
+
13
+ JsonScalar: TypeAlias = str | int | float | bool | None
14
+ JsonValue: TypeAlias = JsonScalar | list["JsonValue"] | dict[str, "JsonValue"]
15
+ JsonObject: TypeAlias = dict[str, JsonValue]
16
+
17
+
18
+ def canonical_semantic_payload(resolved: ResolvedHarness) -> JsonObject:
19
+ """Return the validated source semantics used for ``source_sha256``.
20
+
21
+ The payload is built from fresh JSON-compatible containers and excludes
22
+ request identity, parser metadata, source locations, output paths, catalog
23
+ snapshot identity, and catalog-owned objects.
24
+ """
25
+ source = resolved.source
26
+ return _fresh_json_object(
27
+ {
28
+ "schema_version": source.schema_version,
29
+ "kind": source.kind,
30
+ "harness_id": source.harness_id,
31
+ "harness_version": source.harness_version,
32
+ "stage_kind_ids": sorted(source.stage_scope.stage_kind_ids),
33
+ "model_profile": _fresh_json_value(
34
+ resolved.model_profile.model_dump(mode="json")
35
+ ),
36
+ "prompt": {
37
+ "policy_id": source.prompt.policy_id,
38
+ "system_instructions": source.prompt.system_instructions,
39
+ "include_request_context": source.prompt.include_request_context,
40
+ },
41
+ "budgets": {
42
+ "max_iterations": source.budgets.max_iterations,
43
+ "max_validation_retries": source.budgets.max_validation_retries,
44
+ "max_tool_errors": source.budgets.max_tool_errors,
45
+ "max_prerequisite_violations": (
46
+ source.budgets.max_prerequisite_violations
47
+ ),
48
+ "max_premature_terminal_attempts": (
49
+ source.budgets.max_premature_terminal_attempts
50
+ ),
51
+ },
52
+ "context": {
53
+ "strategy_id": source.context.strategy_id,
54
+ "budget_tokens": source.context.budget_tokens,
55
+ "keep_recent_iterations": source.context.keep_recent_iterations,
56
+ "phase_thresholds": list(source.context.phase_thresholds),
57
+ },
58
+ "nodes": [_node_payload(node) for node in resolved.resolved_nodes],
59
+ "required_node_ids": sorted(resolved.required_node_ids),
60
+ "required_capability_ids": sorted(resolved.required_capability_ids),
61
+ "terminal_result_map": dict(sorted(resolved.terminal_result_map.items())),
62
+ "artifacts": {
63
+ "declared_artifact_ids": sorted(source.artifacts.declared_artifact_ids),
64
+ "required_by_terminal": [
65
+ {
66
+ "terminal_result": item.terminal_result,
67
+ "artifact_ids": sorted(item.artifact_ids),
68
+ }
69
+ for item in sorted(
70
+ source.artifacts.required_by_terminal,
71
+ key=lambda item: item.terminal_result,
72
+ )
73
+ ],
74
+ "producer_evidence": [
75
+ {
76
+ "artifact_id": item.artifact_id,
77
+ "all_producer_node_ids": sorted(item.all_producer_node_ids),
78
+ "terminal_gated_producer_node_ids": sorted(
79
+ item.terminal_gated_producer_node_ids
80
+ ),
81
+ }
82
+ for item in sorted(
83
+ resolved.artifact_evidence,
84
+ key=lambda item: item.artifact_id,
85
+ )
86
+ ],
87
+ },
88
+ }
89
+ )
90
+
91
+
92
+ def canonical_semantic_bytes(resolved: ResolvedHarness) -> bytes:
93
+ """Return UTF-8 canonical JSON bytes for a resolved semantic payload."""
94
+ return canonical_json_serialize(canonical_semantic_payload(resolved)).encode(
95
+ "utf-8"
96
+ )
97
+
98
+
99
+ def source_sha256(resolved: ResolvedHarness) -> str:
100
+ """Return SHA-256 over the canonical validated semantic payload."""
101
+ return hashlib.sha256(canonical_semantic_bytes(resolved)).hexdigest()
102
+
103
+
104
+ def _node_payload(node: Any) -> JsonObject:
105
+ source = node.source
106
+ descriptor = node.descriptor
107
+ return _fresh_json_object(
108
+ {
109
+ "node_id": node.node_id,
110
+ "required": source.required,
111
+ "terminal_result": source.terminal_result,
112
+ "prerequisites": [
113
+ {
114
+ "node_id": prerequisite.node_id,
115
+ "argument_matches": [
116
+ {
117
+ "prior_argument": match.prior_argument,
118
+ "current_argument": match.current_argument,
119
+ }
120
+ for match in sorted(
121
+ prerequisite.argument_matches,
122
+ key=lambda item: (
123
+ item.prior_argument,
124
+ item.current_argument,
125
+ ),
126
+ )
127
+ ],
128
+ }
129
+ for prerequisite in sorted(
130
+ source.prerequisites, key=lambda item: item.node_id
131
+ )
132
+ ],
133
+ "produces": sorted(source.produces),
134
+ "binding": {
135
+ "tool_id": node.binding.tool_id,
136
+ "tool_version": node.binding.tool_version,
137
+ "descriptor_sha256": node.binding.descriptor_sha256,
138
+ "implementation_id": node.binding.implementation_id,
139
+ },
140
+ "descriptor": {
141
+ "model_tool_name": descriptor.model_tool_name,
142
+ "description": descriptor.description,
143
+ "input_schema": _fresh_json_value(descriptor.input_schema),
144
+ "side_effect_class": descriptor.side_effect_class.value,
145
+ "idempotency": descriptor.idempotency.value,
146
+ "required_capabilities": sorted(descriptor.required_capabilities),
147
+ "produced_artifact_ids": sorted(descriptor.produced_artifact_ids),
148
+ },
149
+ }
150
+ )
151
+
152
+
153
+ def _fresh_json_object(value: Mapping[str, Any]) -> JsonObject:
154
+ converted = _fresh_json_value(value)
155
+ if not isinstance(converted, dict):
156
+ raise TypeError("expected JSON object")
157
+ return converted
158
+
159
+
160
+ def _fresh_json_value(value: Any) -> JsonValue:
161
+ if value is None or isinstance(value, str | int | bool):
162
+ return value
163
+ if isinstance(value, float):
164
+ return value
165
+ if isinstance(value, Mapping):
166
+ return {str(key): _fresh_json_value(item) for key, item in value.items()}
167
+ if isinstance(value, tuple | list):
168
+ return [_fresh_json_value(item) for item in value]
169
+ return copy.deepcopy(value)