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.
- millforge/__init__.py +1174 -0
- millforge/_forge/LICENSE +21 -0
- millforge/_forge/PROVENANCE.json +295 -0
- millforge/_forge/UPDATE_POLICY.md +24 -0
- millforge/_forge/__init__.py +14 -0
- millforge/_forge/adapter.py +2232 -0
- millforge/_forge/base_runner.py +121 -0
- millforge/_forge/clients/__init__.py +10 -0
- millforge/_forge/clients/base.py +200 -0
- millforge/_forge/context/__init__.py +23 -0
- millforge/_forge/context/manager.py +178 -0
- millforge/_forge/context/strategies.py +335 -0
- millforge/_forge/core/__init__.py +16 -0
- millforge/_forge/core/inference.py +433 -0
- millforge/_forge/core/messages.py +119 -0
- millforge/_forge/core/runner.py +479 -0
- millforge/_forge/core/steps.py +108 -0
- millforge/_forge/core/workflow.py +400 -0
- millforge/_forge/errors.py +222 -0
- millforge/_forge/guardrails/__init__.py +21 -0
- millforge/_forge/guardrails/error_tracker.py +71 -0
- millforge/_forge/guardrails/guardrails.py +194 -0
- millforge/_forge/guardrails/nudge.py +47 -0
- millforge/_forge/guardrails/response_validator.py +119 -0
- millforge/_forge/guardrails/step_enforcer.py +183 -0
- millforge/_forge/prompts/__init__.py +16 -0
- millforge/_forge/prompts/nudges.py +95 -0
- millforge/_forge/prompts/templates.py +285 -0
- millforge/_version.py +3 -0
- millforge/artifacts.py +570 -0
- millforge/base/__init__.py +97 -0
- millforge/base/composition.py +402 -0
- millforge/base/context.py +285 -0
- millforge/base/harness.py +138 -0
- millforge/base/identity.py +465 -0
- millforge/base/options.py +34 -0
- millforge/base/platform.py +17 -0
- millforge/base/prompt.py +317 -0
- millforge/base/runner.py +546 -0
- millforge/compiled_plan.py +970 -0
- millforge/compiler/__init__.py +231 -0
- millforge/compiler/artifact_validation.py +257 -0
- millforge/compiler/canonicalization.py +169 -0
- millforge/compiler/capabilities.py +66 -0
- millforge/compiler/catalogs.py +500 -0
- millforge/compiler/diagnostics.py +491 -0
- millforge/compiler/graph.py +678 -0
- millforge/compiler/lowering.py +198 -0
- millforge/compiler/output.py +692 -0
- millforge/compiler/parsing.py +1424 -0
- millforge/compiler/requests.py +1180 -0
- millforge/compiler/schema_validation.py +272 -0
- millforge/compiler/semantic.py +490 -0
- millforge/compiler/service.py +448 -0
- millforge/compiler/source.py +375 -0
- millforge/compiler/validators.py +184 -0
- millforge/connectors/__init__.py +95 -0
- millforge/connectors/admission.py +801 -0
- millforge/connectors/broker.py +202 -0
- millforge/connectors/contracts.py +1159 -0
- millforge/connectors/diagnostics.py +189 -0
- millforge/connectors/fake.py +66 -0
- millforge/connectors/runtime.py +236 -0
- millforge/contracts.py +2860 -0
- millforge/custom_tools/__init__.py +67 -0
- millforge/custom_tools/compiler.py +724 -0
- millforge/custom_tools/contracts.py +1093 -0
- millforge/custom_tools/diagnostics.py +205 -0
- millforge/eval_artifacts.py +952 -0
- millforge/eval_boundary.py +2435 -0
- millforge/eval_fixtures/__init__.py +1 -0
- millforge/eval_fixtures/default_pack/__init__.py +1 -0
- millforge/eval_fixtures/default_pack/fixtures/fixture.08a.bug_diagnosis.traceback.v1.json +52 -0
- millforge/eval_fixtures/default_pack/fixtures/fixture.08a.direct_edit.import_sort.v1.json +52 -0
- millforge/eval_fixtures/default_pack/fixtures/fixture.08a.evidence_discipline.no_source_change.v1.json +51 -0
- millforge/eval_fixtures/default_pack/fixtures/fixture.08a.false_closure.visible_green.v1.json +52 -0
- millforge/eval_fixtures/default_pack/fixtures/fixture.08a.multi_file.api_contract.v1.json +54 -0
- millforge/eval_fixtures/default_pack/fixtures/fixture.08a.recovery.malformed_artifact.v1.json +54 -0
- millforge/eval_fixtures/default_pack/manifest.json +12 -0
- millforge/eval_modes.py +1282 -0
- millforge/eval_presets.py +1398 -0
- millforge/eval_reports.py +2517 -0
- millforge/eval_suite.py +2429 -0
- millforge/eval_trials.py +2632 -0
- millforge/eval_workflow.py +794 -0
- millforge/exceptions.py +122 -0
- millforge/model_backend.py +2098 -0
- millforge/protocols.py +340 -0
- millforge/py.typed +0 -0
- millforge/runtime.py +1791 -0
- millforge/testing/__init__.py +1089 -0
- millforge/tools/__init__.py +83 -0
- millforge/tools/builtin_runtime.py +1339 -0
- millforge/tools/builtins.py +773 -0
- millforge/tools/execution.py +1545 -0
- millforge/tools/path_policy.py +155 -0
- millforge/tools/pi_compat/PI_LICENSE +21 -0
- millforge/tools/pi_compat/PROVENANCE.json +55 -0
- millforge/tools/pi_compat/UPDATE_POLICY.md +36 -0
- millforge/tools/pi_compat/__init__.py +34 -0
- millforge/tools/pi_compat/contracts.py +49 -0
- millforge/tools/pi_compat/editing.py +390 -0
- millforge/tools/pi_compat/mutations.py +57 -0
- millforge/tools/pi_compat/operations.py +401 -0
- millforge/tools/pi_compat/paths.py +155 -0
- millforge/tools/pi_compat/process.py +1375 -0
- millforge/tools/pi_compat/search.py +738 -0
- millforge/tools/pi_compat/truncation.py +267 -0
- millforge/tools/pi_compat_catalog.py +396 -0
- millforge/tools/pi_compat_runtime.py +460 -0
- millforge/tools/registry.py +553 -0
- millforge/tools/results.py +533 -0
- millforge-0.1.0.dist-info/METADATA +844 -0
- millforge-0.1.0.dist-info/RECORD +116 -0
- millforge-0.1.0.dist-info/WHEEL +4 -0
- millforge-0.1.0.dist-info/licenses/LICENSE +201 -0
|
@@ -0,0 +1,491 @@
|
|
|
1
|
+
"""Compiler diagnostic contracts and deterministic helpers."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import json
|
|
6
|
+
import math
|
|
7
|
+
import re
|
|
8
|
+
from enum import Enum
|
|
9
|
+
from collections.abc import Sequence
|
|
10
|
+
|
|
11
|
+
from pydantic import (
|
|
12
|
+
BaseModel,
|
|
13
|
+
ConfigDict,
|
|
14
|
+
Field,
|
|
15
|
+
StrictBool,
|
|
16
|
+
StrictInt,
|
|
17
|
+
StrictStr,
|
|
18
|
+
ValidationInfo,
|
|
19
|
+
field_validator,
|
|
20
|
+
model_validator,
|
|
21
|
+
)
|
|
22
|
+
|
|
23
|
+
from millforge.contracts import RedactionPolicy, redact_diagnostic_text
|
|
24
|
+
from millforge.compiler.validators import (
|
|
25
|
+
validate_harness_id,
|
|
26
|
+
validate_lower_field_key,
|
|
27
|
+
validate_node_id,
|
|
28
|
+
validate_utf8_size,
|
|
29
|
+
)
|
|
30
|
+
|
|
31
|
+
_SECRET_PATTERNS = (
|
|
32
|
+
re.compile(
|
|
33
|
+
r"(?i)\b[A-Z][A-Z0-9_]*(?:SECRET|TOKEN|PASSWORD|API_KEY)[A-Z0-9_]*=[^\s]+"
|
|
34
|
+
),
|
|
35
|
+
re.compile(r"(?i)\bbearer\s+[a-z0-9._~+/=-]{16,}"),
|
|
36
|
+
re.compile(r"(?i)\b(api[_-]?key|token|secret|password)\s*[:=]\s*[^&\s]{8,}"),
|
|
37
|
+
re.compile(r"(?i)://[^/\s:@]+:[^/\s:@]+@"),
|
|
38
|
+
re.compile(r"-----BEGIN [A-Z ]*PRIVATE KEY-----"),
|
|
39
|
+
re.compile(r"\b(sk|pk|org|sess)-[A-Za-z0-9]{16,}\b"),
|
|
40
|
+
)
|
|
41
|
+
MAX_DIAGNOSTIC_MESSAGE_UTF8 = 1024
|
|
42
|
+
MAX_DIAGNOSTIC_FIELDS = 16
|
|
43
|
+
MAX_DIAGNOSTIC_FIELD_STRING_UTF8 = 512
|
|
44
|
+
MAX_RELATED_IDS = 16
|
|
45
|
+
MAX_DIAGNOSTIC_REPORT_BYTES = 256 * 1024
|
|
46
|
+
MAX_DIAGNOSTICS_PER_REPORT = 128
|
|
47
|
+
MAX_DIAGNOSTICS_WITHOUT_TRUNCATION_WARNING = MAX_DIAGNOSTICS_PER_REPORT - 1
|
|
48
|
+
_DIAGNOSTICS_TRUNCATED_MESSAGE = "Diagnostics were truncated."
|
|
49
|
+
_TRUNCATED_SUFFIX = "[truncated]"
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
class CompilerPhase(str, Enum):
|
|
53
|
+
"""Closed compiler phase values used in diagnostics and results."""
|
|
54
|
+
|
|
55
|
+
REQUEST = "request"
|
|
56
|
+
PARSE = "parse"
|
|
57
|
+
SCHEMA = "schema"
|
|
58
|
+
RESOLUTION = "resolution"
|
|
59
|
+
GRAPH = "graph"
|
|
60
|
+
CAPABILITY = "capability"
|
|
61
|
+
ARTIFACT = "artifact"
|
|
62
|
+
LOWERING = "lowering"
|
|
63
|
+
OUTPUT = "output"
|
|
64
|
+
INTERNAL = "internal"
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
class DiagnosticSeverity(str, Enum):
|
|
68
|
+
"""Closed diagnostic severity values."""
|
|
69
|
+
|
|
70
|
+
ERROR = "error"
|
|
71
|
+
WARNING = "warning"
|
|
72
|
+
|
|
73
|
+
|
|
74
|
+
DIAGNOSTIC_REGISTRY: dict[str, tuple[CompilerPhase, DiagnosticSeverity]] = {
|
|
75
|
+
**{
|
|
76
|
+
f"MF-S{number:03d}": (CompilerPhase.REQUEST, DiagnosticSeverity.ERROR)
|
|
77
|
+
for number in (1, 2, 13, 14, 15, 16, 17, 18)
|
|
78
|
+
},
|
|
79
|
+
**{
|
|
80
|
+
f"MF-S{number:03d}": (CompilerPhase.PARSE, DiagnosticSeverity.ERROR)
|
|
81
|
+
for number in range(3, 13)
|
|
82
|
+
},
|
|
83
|
+
**{
|
|
84
|
+
f"MF-S{number:03d}": (CompilerPhase.SCHEMA, DiagnosticSeverity.ERROR)
|
|
85
|
+
for number in range(20, 30)
|
|
86
|
+
},
|
|
87
|
+
**{
|
|
88
|
+
f"MF-R{number:03d}": (CompilerPhase.RESOLUTION, DiagnosticSeverity.ERROR)
|
|
89
|
+
for number in range(1, 12)
|
|
90
|
+
},
|
|
91
|
+
"MF-C001_DISCOVERY_NOT_CATALOG": (
|
|
92
|
+
CompilerPhase.RESOLUTION,
|
|
93
|
+
DiagnosticSeverity.ERROR,
|
|
94
|
+
),
|
|
95
|
+
**{
|
|
96
|
+
f"MF-G{number:03d}": (CompilerPhase.GRAPH, DiagnosticSeverity.ERROR)
|
|
97
|
+
for number in range(1, 14)
|
|
98
|
+
},
|
|
99
|
+
"MF-C001": (CompilerPhase.CAPABILITY, DiagnosticSeverity.ERROR),
|
|
100
|
+
**{
|
|
101
|
+
f"MF-A{number:03d}": (CompilerPhase.ARTIFACT, DiagnosticSeverity.ERROR)
|
|
102
|
+
for number in range(1, 8)
|
|
103
|
+
},
|
|
104
|
+
**{
|
|
105
|
+
f"MF-L{number:03d}": (CompilerPhase.LOWERING, DiagnosticSeverity.ERROR)
|
|
106
|
+
for number in range(1, 5)
|
|
107
|
+
},
|
|
108
|
+
**{
|
|
109
|
+
f"MF-O{number:03d}": (CompilerPhase.OUTPUT, DiagnosticSeverity.ERROR)
|
|
110
|
+
for number in range(1, 6)
|
|
111
|
+
},
|
|
112
|
+
"MF-I001": (CompilerPhase.INTERNAL, DiagnosticSeverity.ERROR),
|
|
113
|
+
"MF-D001": (CompilerPhase.INTERNAL, DiagnosticSeverity.WARNING),
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
DIAGNOSTIC_TRIGGER_MEANINGS: dict[str, str] = {
|
|
117
|
+
"MF-R001": "unknown-model-profile",
|
|
118
|
+
"MF-R002": "unknown-tool-reference",
|
|
119
|
+
"MF-R003": "catalog-entry-identity-mismatch",
|
|
120
|
+
"MF-R004": "malformed-catalog-entry",
|
|
121
|
+
"MF-R005": "duplicate-tool-binding",
|
|
122
|
+
"MF-R006": "duplicate-model-tool-name",
|
|
123
|
+
"MF-R007": "unsupported-tool-schema",
|
|
124
|
+
"MF-R008": "catalog-snapshot-drift",
|
|
125
|
+
"MF-R009": "catalog-internal-failure",
|
|
126
|
+
"MF-R010": "malformed-model-profile",
|
|
127
|
+
"MF-R011": "invalid-tool-reference",
|
|
128
|
+
"MF-C001_DISCOVERY_NOT_CATALOG": "discovery-snapshot-not-catalog",
|
|
129
|
+
"MF-G001": "unknown-prerequisite-node",
|
|
130
|
+
"MF-G002": "self-prerequisite",
|
|
131
|
+
"MF-G003": "duplicate-prerequisite",
|
|
132
|
+
"MF-G004": "prerequisite-cycle",
|
|
133
|
+
"MF-G005": "unreachable-node",
|
|
134
|
+
"MF-G006": "unreachable-terminal",
|
|
135
|
+
"MF-G007": "unreachable-required-node",
|
|
136
|
+
"MF-G008": "terminal-node-required",
|
|
137
|
+
"MF-G009": "terminal-result-illegal",
|
|
138
|
+
"MF-G010": "terminal-result-duplicate",
|
|
139
|
+
"MF-G011": "terminal-missing",
|
|
140
|
+
"MF-G012": "invalid-argument-match",
|
|
141
|
+
"MF-G013": "terminal-prerequisite",
|
|
142
|
+
"MF-C001": "capability-envelope-mismatch",
|
|
143
|
+
"MF-A001": "undeclared-produced-artifact",
|
|
144
|
+
"MF-A002": "tool-cannot-produce-artifact",
|
|
145
|
+
"MF-A003": "unknown-terminal-artifact-policy",
|
|
146
|
+
"MF-A004": "required-artifact-without-producer",
|
|
147
|
+
"MF-A005": "required-artifact-not-terminal-gated",
|
|
148
|
+
"MF-A006": "duplicate-artifact-id",
|
|
149
|
+
"MF-A007": "undeclared-terminal-required-artifact",
|
|
150
|
+
"MF-L001": "lowering-invariant-failed",
|
|
151
|
+
"MF-L002": "compiled-plan-validation-failed",
|
|
152
|
+
"MF-L003": "source-semantic-hash-failed",
|
|
153
|
+
"MF-L004": "compiled-hash-verification-failed",
|
|
154
|
+
"MF-O001": "output-path-invalid",
|
|
155
|
+
"MF-O002": "diagnostics-write-failed",
|
|
156
|
+
"MF-O003": "plan-write-failed",
|
|
157
|
+
"MF-O004": "existing-output-integrity-failed",
|
|
158
|
+
"MF-O005": "temporary-output-cleanup-failed",
|
|
159
|
+
"MF-I001": "compiler-internal-error",
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
_REQUEST_DIAGNOSTIC_ORDER = {
|
|
163
|
+
"MF-S018": 0,
|
|
164
|
+
"MF-S001": 1,
|
|
165
|
+
"MF-S002": 2,
|
|
166
|
+
"MF-S016": 3,
|
|
167
|
+
"MF-S017": 4,
|
|
168
|
+
"MF-S013": 5,
|
|
169
|
+
"MF-S014": 6,
|
|
170
|
+
"MF-S015": 7,
|
|
171
|
+
}
|
|
172
|
+
_INTERNAL_DIAGNOSTIC_ORDER = {
|
|
173
|
+
"MF-I001": 0,
|
|
174
|
+
"MF-D001": 1,
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
|
|
178
|
+
class SourceLocation(BaseModel):
|
|
179
|
+
"""One-based source location using Unicode code point columns."""
|
|
180
|
+
|
|
181
|
+
model_config = ConfigDict(extra="forbid", frozen=True)
|
|
182
|
+
|
|
183
|
+
line: StrictInt = Field(ge=1)
|
|
184
|
+
column: StrictInt = Field(ge=1)
|
|
185
|
+
end_line: StrictInt | None = Field(default=None, ge=1)
|
|
186
|
+
end_column: StrictInt | None = Field(default=None, ge=1)
|
|
187
|
+
|
|
188
|
+
@model_validator(mode="after")
|
|
189
|
+
def _end_not_before_start(self) -> SourceLocation:
|
|
190
|
+
if self.end_line is None and self.end_column is not None:
|
|
191
|
+
raise ValueError("end_column requires end_line")
|
|
192
|
+
if self.end_line is not None and self.end_column is None:
|
|
193
|
+
raise ValueError("end_line requires end_column")
|
|
194
|
+
if self.end_line is not None and self.end_column is not None:
|
|
195
|
+
if (self.end_line, self.end_column) < (self.line, self.column):
|
|
196
|
+
raise ValueError("end location must not precede start location")
|
|
197
|
+
return self
|
|
198
|
+
|
|
199
|
+
|
|
200
|
+
class SourceReference(BaseModel):
|
|
201
|
+
"""Reference to a source field and optional source location."""
|
|
202
|
+
|
|
203
|
+
model_config = ConfigDict(extra="forbid", frozen=True)
|
|
204
|
+
|
|
205
|
+
logical_path: StrictStr
|
|
206
|
+
field_path: StrictStr
|
|
207
|
+
location: SourceLocation | None = None
|
|
208
|
+
|
|
209
|
+
@field_validator("logical_path")
|
|
210
|
+
@classmethod
|
|
211
|
+
def _logical_path_nonblank(cls, value: str) -> str:
|
|
212
|
+
if not value.strip():
|
|
213
|
+
raise ValueError("logical_path must be nonblank")
|
|
214
|
+
return value
|
|
215
|
+
|
|
216
|
+
@field_validator("field_path")
|
|
217
|
+
@classmethod
|
|
218
|
+
def _field_path_valid(cls, value: str) -> str:
|
|
219
|
+
if not value.startswith("/"):
|
|
220
|
+
raise ValueError("field_path must be an RFC 6901 absolute pointer")
|
|
221
|
+
return value
|
|
222
|
+
|
|
223
|
+
|
|
224
|
+
DiagnosticFieldValue = StrictStr | StrictInt | float | StrictBool
|
|
225
|
+
|
|
226
|
+
|
|
227
|
+
class DiagnosticField(BaseModel):
|
|
228
|
+
"""Immutable bounded scalar diagnostic field."""
|
|
229
|
+
|
|
230
|
+
model_config = ConfigDict(extra="forbid", frozen=True)
|
|
231
|
+
|
|
232
|
+
key: StrictStr
|
|
233
|
+
value: DiagnosticFieldValue
|
|
234
|
+
|
|
235
|
+
@field_validator("key")
|
|
236
|
+
@classmethod
|
|
237
|
+
def _key_valid(cls, value: str) -> str:
|
|
238
|
+
return validate_lower_field_key(value)
|
|
239
|
+
|
|
240
|
+
@field_validator("value")
|
|
241
|
+
@classmethod
|
|
242
|
+
def _value_valid(
|
|
243
|
+
cls, value: DiagnosticFieldValue, info: ValidationInfo
|
|
244
|
+
) -> DiagnosticFieldValue:
|
|
245
|
+
if isinstance(value, str):
|
|
246
|
+
key = info.data.get("key")
|
|
247
|
+
field_name = key if isinstance(key, str) else "value"
|
|
248
|
+
policy = RedactionPolicy()
|
|
249
|
+
if detect_secret_candidate(
|
|
250
|
+
field_path=f"/{field_name}",
|
|
251
|
+
field_name=field_name,
|
|
252
|
+
value=value,
|
|
253
|
+
policy=policy,
|
|
254
|
+
):
|
|
255
|
+
return policy.replacement
|
|
256
|
+
redacted = redact_diagnostic_text(value, policy=policy)
|
|
257
|
+
return _truncate_utf8(redacted, MAX_DIAGNOSTIC_FIELD_STRING_UTF8)
|
|
258
|
+
if isinstance(value, float) and not math.isfinite(value):
|
|
259
|
+
raise ValueError("value must be finite")
|
|
260
|
+
return value
|
|
261
|
+
|
|
262
|
+
def __str__(self) -> str:
|
|
263
|
+
return repr(self)
|
|
264
|
+
|
|
265
|
+
def __repr__(self) -> str:
|
|
266
|
+
value = (
|
|
267
|
+
redact_diagnostic_text(self.value)
|
|
268
|
+
if isinstance(self.value, str)
|
|
269
|
+
else self.value
|
|
270
|
+
)
|
|
271
|
+
return f"DiagnosticField(key={self.key!r}, value={value!r})"
|
|
272
|
+
|
|
273
|
+
|
|
274
|
+
class CompilerDiagnostic(BaseModel):
|
|
275
|
+
"""Closed immutable compiler diagnostic."""
|
|
276
|
+
|
|
277
|
+
model_config = ConfigDict(extra="forbid", frozen=True)
|
|
278
|
+
|
|
279
|
+
code: StrictStr
|
|
280
|
+
severity: DiagnosticSeverity
|
|
281
|
+
phase: CompilerPhase
|
|
282
|
+
message: StrictStr
|
|
283
|
+
source_reference: SourceReference | None = None
|
|
284
|
+
harness_id: StrictStr | None = None
|
|
285
|
+
node_id: StrictStr | None = None
|
|
286
|
+
related_ids: tuple[StrictStr, ...] = Field(default_factory=tuple)
|
|
287
|
+
suggested_fix: StrictStr | None = None
|
|
288
|
+
fields: tuple[DiagnosticField, ...] = Field(default_factory=tuple)
|
|
289
|
+
|
|
290
|
+
@field_validator("message")
|
|
291
|
+
@classmethod
|
|
292
|
+
def _message_valid(cls, value: str) -> str:
|
|
293
|
+
if not value.strip():
|
|
294
|
+
raise ValueError("message must be nonblank")
|
|
295
|
+
return validate_utf8_size(
|
|
296
|
+
redact_diagnostic_text(value), "message", MAX_DIAGNOSTIC_MESSAGE_UTF8
|
|
297
|
+
)
|
|
298
|
+
|
|
299
|
+
@field_validator("harness_id")
|
|
300
|
+
@classmethod
|
|
301
|
+
def _harness_id_valid(cls, value: str | None) -> str | None:
|
|
302
|
+
return None if value is None else validate_harness_id(value)
|
|
303
|
+
|
|
304
|
+
@field_validator("node_id")
|
|
305
|
+
@classmethod
|
|
306
|
+
def _node_id_valid(cls, value: str | None) -> str | None:
|
|
307
|
+
return None if value is None else validate_node_id(value)
|
|
308
|
+
|
|
309
|
+
@field_validator("related_ids")
|
|
310
|
+
@classmethod
|
|
311
|
+
def _related_ids_valid(cls, value: tuple[str, ...]) -> tuple[str, ...]:
|
|
312
|
+
if len(value) > MAX_RELATED_IDS:
|
|
313
|
+
raise ValueError("related_ids may contain at most 16 entries")
|
|
314
|
+
for item in value:
|
|
315
|
+
if not item.strip():
|
|
316
|
+
raise ValueError("related_ids must be nonblank")
|
|
317
|
+
return value
|
|
318
|
+
|
|
319
|
+
@field_validator("suggested_fix")
|
|
320
|
+
@classmethod
|
|
321
|
+
def _suggested_fix_valid(cls, value: str | None) -> str | None:
|
|
322
|
+
if value is None:
|
|
323
|
+
return None
|
|
324
|
+
if not value.strip():
|
|
325
|
+
raise ValueError("suggested_fix must be nonblank")
|
|
326
|
+
return validate_utf8_size(
|
|
327
|
+
redact_diagnostic_text(value),
|
|
328
|
+
"suggested_fix",
|
|
329
|
+
MAX_DIAGNOSTIC_MESSAGE_UTF8,
|
|
330
|
+
)
|
|
331
|
+
|
|
332
|
+
@field_validator("fields")
|
|
333
|
+
@classmethod
|
|
334
|
+
def _fields_valid(
|
|
335
|
+
cls, value: tuple[DiagnosticField, ...]
|
|
336
|
+
) -> tuple[DiagnosticField, ...]:
|
|
337
|
+
if len(value) > MAX_DIAGNOSTIC_FIELDS:
|
|
338
|
+
raise ValueError("fields may contain at most 16 entries")
|
|
339
|
+
keys = tuple(field.key for field in value)
|
|
340
|
+
if len(set(keys)) != len(keys):
|
|
341
|
+
raise ValueError("diagnostic field keys must be unique")
|
|
342
|
+
return value
|
|
343
|
+
|
|
344
|
+
@model_validator(mode="after")
|
|
345
|
+
def _registry_matches(self) -> CompilerDiagnostic:
|
|
346
|
+
expected = DIAGNOSTIC_REGISTRY.get(self.code)
|
|
347
|
+
if expected is None:
|
|
348
|
+
raise ValueError("diagnostic code is not registered")
|
|
349
|
+
if (self.phase, self.severity) != expected:
|
|
350
|
+
raise ValueError("diagnostic phase and severity must match registry")
|
|
351
|
+
return self
|
|
352
|
+
|
|
353
|
+
def __str__(self) -> str:
|
|
354
|
+
return repr(self)
|
|
355
|
+
|
|
356
|
+
def __repr__(self) -> str:
|
|
357
|
+
return (
|
|
358
|
+
"CompilerDiagnostic("
|
|
359
|
+
f"code={self.code!r}, severity={self.severity.value!r}, "
|
|
360
|
+
f"phase={self.phase.value!r}, message={redact_diagnostic_text(self.message)!r}, "
|
|
361
|
+
f"source_reference={self.source_reference!r}, harness_id={self.harness_id!r}, "
|
|
362
|
+
f"node_id={self.node_id!r}, related_ids={self.related_ids!r}, "
|
|
363
|
+
f"suggested_fix={redact_diagnostic_text(self.suggested_fix) if self.suggested_fix is not None else None!r}, "
|
|
364
|
+
f"fields={self.fields!r})"
|
|
365
|
+
)
|
|
366
|
+
|
|
367
|
+
|
|
368
|
+
def diagnostic_sort_key(
|
|
369
|
+
diagnostic: CompilerDiagnostic,
|
|
370
|
+
) -> tuple[int, int, int, int, str, str, str, str]:
|
|
371
|
+
"""Return the deterministic diagnostic ordering key."""
|
|
372
|
+
phase_order = tuple(CompilerPhase)
|
|
373
|
+
source = diagnostic.source_reference
|
|
374
|
+
location = source.location if source is not None else None
|
|
375
|
+
if diagnostic.phase == CompilerPhase.REQUEST:
|
|
376
|
+
return (
|
|
377
|
+
phase_order.index(diagnostic.phase),
|
|
378
|
+
_REQUEST_DIAGNOSTIC_ORDER.get(
|
|
379
|
+
diagnostic.code, len(_REQUEST_DIAGNOSTIC_ORDER)
|
|
380
|
+
),
|
|
381
|
+
location.line if location is not None else 0,
|
|
382
|
+
location.column if location is not None else 0,
|
|
383
|
+
source.field_path if source is not None else "",
|
|
384
|
+
diagnostic.code,
|
|
385
|
+
diagnostic.node_id or "",
|
|
386
|
+
diagnostic.message,
|
|
387
|
+
)
|
|
388
|
+
if diagnostic.phase == CompilerPhase.INTERNAL:
|
|
389
|
+
return (
|
|
390
|
+
phase_order.index(diagnostic.phase),
|
|
391
|
+
_INTERNAL_DIAGNOSTIC_ORDER.get(
|
|
392
|
+
diagnostic.code, len(_INTERNAL_DIAGNOSTIC_ORDER)
|
|
393
|
+
),
|
|
394
|
+
location.line if location is not None else 0,
|
|
395
|
+
location.column if location is not None else 0,
|
|
396
|
+
source.field_path if source is not None else "",
|
|
397
|
+
diagnostic.code,
|
|
398
|
+
diagnostic.node_id or "",
|
|
399
|
+
diagnostic.message,
|
|
400
|
+
)
|
|
401
|
+
return (
|
|
402
|
+
phase_order.index(diagnostic.phase),
|
|
403
|
+
0,
|
|
404
|
+
location.line if location is not None else 0,
|
|
405
|
+
location.column if location is not None else 0,
|
|
406
|
+
source.field_path if source is not None else "",
|
|
407
|
+
diagnostic.code,
|
|
408
|
+
diagnostic.node_id or "",
|
|
409
|
+
diagnostic.message,
|
|
410
|
+
)
|
|
411
|
+
|
|
412
|
+
|
|
413
|
+
def sort_diagnostics(
|
|
414
|
+
diagnostics: Sequence[CompilerDiagnostic],
|
|
415
|
+
) -> tuple[CompilerDiagnostic, ...]:
|
|
416
|
+
"""Sort diagnostics according to the compiler contract."""
|
|
417
|
+
return tuple(sorted(diagnostics, key=diagnostic_sort_key))
|
|
418
|
+
|
|
419
|
+
|
|
420
|
+
def bound_diagnostics(
|
|
421
|
+
diagnostics: Sequence[CompilerDiagnostic],
|
|
422
|
+
) -> tuple[CompilerDiagnostic, ...]:
|
|
423
|
+
"""Sort and truncate diagnostics to the fixed report bound."""
|
|
424
|
+
ordered = list(sort_diagnostics(diagnostics))
|
|
425
|
+
if (
|
|
426
|
+
len(ordered) <= MAX_DIAGNOSTICS_PER_REPORT
|
|
427
|
+
and _diagnostic_report_size_bytes(ordered) <= MAX_DIAGNOSTIC_REPORT_BYTES
|
|
428
|
+
):
|
|
429
|
+
return tuple(ordered)
|
|
430
|
+
|
|
431
|
+
warning = _diagnostics_truncated_warning()
|
|
432
|
+
kept = [diagnostic for diagnostic in ordered if diagnostic.code != "MF-D001"]
|
|
433
|
+
kept = kept[:MAX_DIAGNOSTICS_WITHOUT_TRUNCATION_WARNING]
|
|
434
|
+
while True:
|
|
435
|
+
candidate = tuple((*kept, warning))
|
|
436
|
+
if _diagnostic_report_size_bytes(candidate) <= MAX_DIAGNOSTIC_REPORT_BYTES:
|
|
437
|
+
return candidate
|
|
438
|
+
if not kept:
|
|
439
|
+
return (warning,)
|
|
440
|
+
kept.pop()
|
|
441
|
+
|
|
442
|
+
|
|
443
|
+
def detect_secret_candidate(
|
|
444
|
+
*,
|
|
445
|
+
field_path: str,
|
|
446
|
+
field_name: str,
|
|
447
|
+
value: str,
|
|
448
|
+
policy: RedactionPolicy,
|
|
449
|
+
) -> bool:
|
|
450
|
+
"""Return whether a scalar resembles a secret without exposing it."""
|
|
451
|
+
lowered = f"{field_path}/{field_name}".lower()
|
|
452
|
+
compact = lowered.replace("_", "").replace("-", "")
|
|
453
|
+
marker_match = any(
|
|
454
|
+
marker in lowered or marker.replace("-", "") in compact
|
|
455
|
+
for marker in policy.sensitive_field_markers
|
|
456
|
+
)
|
|
457
|
+
if marker_match and value.strip():
|
|
458
|
+
return True
|
|
459
|
+
return any(pattern.search(value) is not None for pattern in _SECRET_PATTERNS)
|
|
460
|
+
|
|
461
|
+
|
|
462
|
+
def _truncate_utf8(value: str, maximum: int) -> str:
|
|
463
|
+
encoded = value.encode("utf-8")
|
|
464
|
+
if len(encoded) <= maximum:
|
|
465
|
+
return value
|
|
466
|
+
suffix = _TRUNCATED_SUFFIX.encode("utf-8")
|
|
467
|
+
prefix = encoded[: max(0, maximum - len(suffix))].decode("utf-8", errors="ignore")
|
|
468
|
+
return f"{prefix}{_TRUNCATED_SUFFIX}"
|
|
469
|
+
|
|
470
|
+
|
|
471
|
+
def _diagnostic_report_size_bytes(
|
|
472
|
+
diagnostics: Sequence[CompilerDiagnostic],
|
|
473
|
+
) -> int:
|
|
474
|
+
payload = [diagnostic.model_dump(mode="json") for diagnostic in diagnostics]
|
|
475
|
+
serialized = json.dumps(
|
|
476
|
+
payload,
|
|
477
|
+
sort_keys=True,
|
|
478
|
+
ensure_ascii=True,
|
|
479
|
+
allow_nan=False,
|
|
480
|
+
separators=(",", ":"),
|
|
481
|
+
)
|
|
482
|
+
return len(serialized.encode("utf-8"))
|
|
483
|
+
|
|
484
|
+
|
|
485
|
+
def _diagnostics_truncated_warning() -> CompilerDiagnostic:
|
|
486
|
+
return CompilerDiagnostic(
|
|
487
|
+
code="MF-D001",
|
|
488
|
+
phase=CompilerPhase.INTERNAL,
|
|
489
|
+
severity=DiagnosticSeverity.WARNING,
|
|
490
|
+
message=_DIAGNOSTICS_TRUNCATED_MESSAGE,
|
|
491
|
+
)
|