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,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
+ )