arxo-codegen 0.4.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 (152) hide show
  1. arxo_codegen/__init__.py +5 -0
  2. arxo_codegen/__main__.py +7 -0
  3. arxo_codegen/active-outcomes.json +6837 -0
  4. arxo_codegen/check.py +158 -0
  5. arxo_codegen/check_active_canaries.py +262 -0
  6. arxo_codegen/check_api.py +76 -0
  7. arxo_codegen/check_conformance.py +414 -0
  8. arxo_codegen/check_matrix.py +131 -0
  9. arxo_codegen/check_packages.py +281 -0
  10. arxo_codegen/check_semantic_probes.py +293 -0
  11. arxo_codegen/check_xlsx.py +320 -0
  12. arxo_codegen/consumer_runner.py +316 -0
  13. arxo_codegen/core_support.py +203 -0
  14. arxo_codegen/coverage.py +341 -0
  15. arxo_codegen/deliver.py +180 -0
  16. arxo_codegen/examples/xlsx/pricing.lawir.json +409 -0
  17. arxo_codegen/examples/xlsx/pricing.xlsx.json +52 -0
  18. arxo_codegen/excel_plan.py +602 -0
  19. arxo_codegen/harness/verify/ci/gates/backends/check_backends.py +1248 -0
  20. arxo_codegen/harness/verify/ci/gates/backends/l1_fixtures.py +463 -0
  21. arxo_codegen/inert-nodes.json +62 -0
  22. arxo_codegen/inert.py +91 -0
  23. arxo_codegen/inputs.py +59 -0
  24. arxo_codegen/package.py +565 -0
  25. arxo_codegen/plan.py +4053 -0
  26. arxo_codegen/recertify_registry.py +100 -0
  27. arxo_codegen/runtime-cert.json +5259 -0
  28. arxo_codegen/runtime_cert.py +233 -0
  29. arxo_codegen/survey.py +272 -0
  30. arxo_codegen/target_runner.py +101 -0
  31. arxo_codegen/targets/bsl/cert.bsl +113 -0
  32. arxo_codegen/targets/bsl/emit.py +356 -0
  33. arxo_codegen/targets/bsl/json.bsl +243 -0
  34. arxo_codegen/targets/bsl/runner.bsl +210 -0
  35. arxo_codegen/targets/bsl/runtime.bsl +858 -0
  36. arxo_codegen/targets/cs/Cert.cs +90 -0
  37. arxo_codegen/targets/cs/Runner.cs +154 -0
  38. arxo_codegen/targets/cs/Runtime.cs +243 -0
  39. arxo_codegen/targets/cs/emit.py +223 -0
  40. arxo_codegen/targets/dart/cert.dart +99 -0
  41. arxo_codegen/targets/dart/emit.py +206 -0
  42. arxo_codegen/targets/dart/json.dart +166 -0
  43. arxo_codegen/targets/dart/runner.dart +154 -0
  44. arxo_codegen/targets/dart/runtime.dart +293 -0
  45. arxo_codegen/targets/erl/cert.erl +64 -0
  46. arxo_codegen/targets/erl/emit.py +216 -0
  47. arxo_codegen/targets/erl/rt.erl +238 -0
  48. arxo_codegen/targets/erl/runner.erl +130 -0
  49. arxo_codegen/targets/ex/cert.exs +62 -0
  50. arxo_codegen/targets/ex/emit.py +204 -0
  51. arxo_codegen/targets/ex/runner.exs +124 -0
  52. arxo_codegen/targets/ex/runtime.ex +227 -0
  53. arxo_codegen/targets/go/apiBase.go +483 -0
  54. arxo_codegen/targets/go/apicheck.go +97 -0
  55. arxo_codegen/targets/go/cert.go +218 -0
  56. arxo_codegen/targets/go/emit.py +1806 -0
  57. arxo_codegen/targets/go/evaluate.go +1826 -0
  58. arxo_codegen/targets/go/runner.go +38 -0
  59. arxo_codegen/targets/go/runtime.go +2612 -0
  60. arxo_codegen/targets/java/Cert.java +91 -0
  61. arxo_codegen/targets/java/Json.java +148 -0
  62. arxo_codegen/targets/java/Runner.java +151 -0
  63. arxo_codegen/targets/java/Runtime.java +243 -0
  64. arxo_codegen/targets/java/emit.py +198 -0
  65. arxo_codegen/targets/kt/Cert.kt +67 -0
  66. arxo_codegen/targets/kt/Json.kt +103 -0
  67. arxo_codegen/targets/kt/Runner.kt +126 -0
  68. arxo_codegen/targets/kt/Runtime.kt +196 -0
  69. arxo_codegen/targets/kt/emit.py +195 -0
  70. arxo_codegen/targets/lua/cert.lua +82 -0
  71. arxo_codegen/targets/lua/emit.py +250 -0
  72. arxo_codegen/targets/lua/json.lua +133 -0
  73. arxo_codegen/targets/lua/runner.lua +167 -0
  74. arxo_codegen/targets/lua/runtime.lua +541 -0
  75. arxo_codegen/targets/php/cert.php +84 -0
  76. arxo_codegen/targets/php/emit.py +229 -0
  77. arxo_codegen/targets/php/json.php +144 -0
  78. arxo_codegen/targets/php/runner.php +189 -0
  79. arxo_codegen/targets/php/runtime.php +660 -0
  80. arxo_codegen/targets/py/api_base.py +334 -0
  81. arxo_codegen/targets/py/apicheck.py +51 -0
  82. arxo_codegen/targets/py/cert.py +140 -0
  83. arxo_codegen/targets/py/emit.py +1140 -0
  84. arxo_codegen/targets/py/evaluate.py +980 -0
  85. arxo_codegen/targets/py/runner.py +38 -0
  86. arxo_codegen/targets/py/runtime.py +1634 -0
  87. arxo_codegen/targets/rb/cert.rb +73 -0
  88. arxo_codegen/targets/rb/emit.py +203 -0
  89. arxo_codegen/targets/rb/runner.rb +130 -0
  90. arxo_codegen/targets/rb/runtime.rb +255 -0
  91. arxo_codegen/targets/rs/Cargo.lock +142 -0
  92. arxo_codegen/targets/rs/Cargo.toml +16 -0
  93. arxo_codegen/targets/rs/cert.rs +106 -0
  94. arxo_codegen/targets/rs/emit.py +233 -0
  95. arxo_codegen/targets/rs/prefetch.rs +1 -0
  96. arxo_codegen/targets/rs/runner.rs +181 -0
  97. arxo_codegen/targets/rs/runtime.rs +448 -0
  98. arxo_codegen/targets/scala/Cert.scala +77 -0
  99. arxo_codegen/targets/scala/Json.scala +140 -0
  100. arxo_codegen/targets/scala/Runner.scala +121 -0
  101. arxo_codegen/targets/scala/Runtime.scala +269 -0
  102. arxo_codegen/targets/scala/emit.py +193 -0
  103. arxo_codegen/targets/swift/cert.swift +105 -0
  104. arxo_codegen/targets/swift/emit.py +231 -0
  105. arxo_codegen/targets/swift/json.swift +208 -0
  106. arxo_codegen/targets/swift/runner.swift +133 -0
  107. arxo_codegen/targets/swift/runtime.swift +528 -0
  108. arxo_codegen/targets/ts/apiBase.ts +250 -0
  109. arxo_codegen/targets/ts/apicheck.ts +37 -0
  110. arxo_codegen/targets/ts/cert.ts +96 -0
  111. arxo_codegen/targets/ts/emit.py +1214 -0
  112. arxo_codegen/targets/ts/evaluate.ts +894 -0
  113. arxo_codegen/targets/ts/package-lock.json +45 -0
  114. arxo_codegen/targets/ts/package.json +9 -0
  115. arxo_codegen/targets/ts/runner.ts +31 -0
  116. arxo_codegen/targets/ts/runtime.ts +1369 -0
  117. arxo_codegen/targets/xlsx/README.ru.md +176 -0
  118. arxo_codegen/targets/xlsx/emit.py +198 -0
  119. arxo_codegen/temporal.py +59 -0
  120. arxo_codegen/test_alias_types.py +171 -0
  121. arxo_codegen/test_api_contract.py +89 -0
  122. arxo_codegen/test_calendar_policy.py +48 -0
  123. arxo_codegen/test_check.py +49 -0
  124. arxo_codegen/test_check_active_canaries.py +96 -0
  125. arxo_codegen/test_check_conformance.py +132 -0
  126. arxo_codegen/test_check_semantic_probes.py +85 -0
  127. arxo_codegen/test_check_xlsx.py +52 -0
  128. arxo_codegen/test_consumer_runner.py +175 -0
  129. arxo_codegen/test_core_support.py +80 -0
  130. arxo_codegen/test_e0212_decision_time.py +63 -0
  131. arxo_codegen/test_head_aggregate.py +71 -0
  132. arxo_codegen/test_inert.py +143 -0
  133. arxo_codegen/test_instant_types.py +113 -0
  134. arxo_codegen/test_key_conflict_scope.py +92 -0
  135. arxo_codegen/test_key_conflict_targets.py +17 -0
  136. arxo_codegen/test_package_layout.py +163 -0
  137. arxo_codegen/test_plan_language_version.py +44 -0
  138. arxo_codegen/test_positions_refusal.py +43 -0
  139. arxo_codegen/test_recertify_registry.py +77 -0
  140. arxo_codegen/test_ruby_target.py +41 -0
  141. arxo_codegen/test_runtime_cert.py +83 -0
  142. arxo_codegen/test_secondary_key_conflict_scope.py +17 -0
  143. arxo_codegen/test_stage_r.py +108 -0
  144. arxo_codegen/test_standalone_inputs.py +71 -0
  145. arxo_codegen/test_support.py +150 -0
  146. arxo_codegen/test_survey_inputs.py +115 -0
  147. arxo_codegen-0.4.0.dist-info/LICENSE +202 -0
  148. arxo_codegen-0.4.0.dist-info/METADATA +1673 -0
  149. arxo_codegen-0.4.0.dist-info/RECORD +152 -0
  150. arxo_codegen-0.4.0.dist-info/WHEEL +5 -0
  151. arxo_codegen-0.4.0.dist-info/entry_points.txt +13 -0
  152. arxo_codegen-0.4.0.dist-info/top_level.txt +1 -0
@@ -0,0 +1,414 @@
1
+ """Check current-release backend conformance without changing baselines.
2
+
3
+ Every exported 0.2.11 / IR 0.7 vector is classified; executable inputs run on requested
4
+ targets against its source ``expected.json``. An explicit active registry (format 0.3,
5
+ one pin per vector with the digest of that vector's inputs) covers vectors absent
6
+ from the historical registry; a pinned outcome must agree, an unpinned match
7
+ must be pinned, and a changed or new vector colours only itself.
8
+ """
9
+ from __future__ import annotations
10
+
11
+ import argparse
12
+ import collections
13
+ import fnmatch
14
+ import hashlib
15
+ import json
16
+ import os
17
+ from pathlib import Path
18
+ import subprocess
19
+ import sys
20
+ import tempfile
21
+ import time
22
+
23
+ HERE = Path(__file__).resolve().parent
24
+ if str(HERE) not in sys.path:
25
+ sys.path.insert(0, str(HERE))
26
+
27
+ import check
28
+ import core_support
29
+ import inert
30
+
31
+ ACTIVE_SEMANTICS = frozenset({"0.2.11"})
32
+ ARCHIVED_SEMANTICS = frozenset()
33
+ FORMAT = "law.codegen-conformance-report/0.1"
34
+
35
+
36
+ def select_vectors(available: set[str], vectors: tuple[str, ...] = (),
37
+ patterns: tuple[str, ...] = ()) -> set[str]:
38
+ """Resolve a union of exact IDs and shell patterns; typos never mean PASS."""
39
+ if not vectors and not patterns:
40
+ return set(available)
41
+ selected = set()
42
+ for ident in vectors:
43
+ if ident not in available:
44
+ raise ValueError(f"unknown active vector: {ident}")
45
+ selected.add(ident)
46
+ for pattern in patterns:
47
+ matches = {ident for ident in available if fnmatch.fnmatchcase(ident, pattern)}
48
+ if not matches:
49
+ raise ValueError(f"vector pattern matched no active vectors: {pattern}")
50
+ selected.update(matches)
51
+ return selected
52
+
53
+
54
+ def selected_inert_witnesses(manifest: dict, selected: set[str]) -> tuple[dict, set[str]]:
55
+ """Selecting either half of an inertness proof includes and checks its pair."""
56
+ constructs = {name: row for name, row in manifest["constructs"].items()
57
+ if selected.intersection((row["witness"], row["bare"]))}
58
+ required = {ident for row in constructs.values() for ident in (row["witness"], row["bare"])}
59
+ return dict(manifest, constructs=constructs), selected | required
60
+
61
+
62
+ def _save(path: Path, report: dict) -> None:
63
+ path.parent.mkdir(parents=True, exist_ok=True)
64
+ path.write_text(json.dumps(report, ensure_ascii=False, indent=2, sort_keys=True) + "\n", encoding="utf-8")
65
+
66
+
67
+ def _bounded_runner(timeout: float, environment: dict[str, str], failures: list[dict]):
68
+ def run(command, cwd=None):
69
+ try:
70
+ result = subprocess.run(command, cwd=cwd, env=environment, text=True,
71
+ capture_output=True, timeout=timeout)
72
+ except subprocess.TimeoutExpired:
73
+ result = subprocess.CompletedProcess(command, 124, "", f"timeout after {timeout}s")
74
+ except OSError as exc:
75
+ result = subprocess.CompletedProcess(command, 127, "", str(exc))
76
+ # Exit 3 is the runner's named refusal protocol. execute() validates
77
+ # its JSON and requires every target to refuse identically; malformed
78
+ # refusal payloads still become crashes. Build errors are handled by
79
+ # build_projects regardless of their exit code.
80
+ if result.returncode not in (0, 3):
81
+ failures.append({
82
+ "command": [str(part) for part in command], "returncode": result.returncode,
83
+ "output": (result.stdout + result.stderr)[:4000],
84
+ })
85
+ return result
86
+ return run
87
+
88
+
89
+ def run(fixtures: Path, core_kit: Path, *, timeout: float,
90
+ targets: tuple[str, ...] = ("py",), outcomes: Path | None = None,
91
+ vectors: tuple[str, ...] = (), vector_globs: tuple[str, ...] = ()) -> tuple[dict, int]:
92
+ core_support.configure_core_kit(core_kit)
93
+ core_support.validate_core_kit(core_kit)
94
+ gate = check.load_harness(fixtures.resolve())
95
+ fixture_manifest = check.validate_fixtures(fixtures)
96
+ if not targets or len(set(targets)) != len(targets) or set(targets) - {"py", "ts", "go"}:
97
+ raise ValueError("targets must be a nonempty distinct subset of py,ts,go")
98
+ emit = {lang: gate.D.emitter(lang) for lang in targets}
99
+ toolchains = {lang: descriptor.available() for lang, descriptor in emit.items()}
100
+ source_paths = sorted([*Path(__file__).parent.glob("*.py"),
101
+ *(p for lang in ("py", "ts", "go")
102
+ for p in (Path(__file__).parent / "targets" / lang).glob("*")
103
+ if p.suffix in (".py", ".ts", ".go", ".json")),
104
+ Path(__file__).parent / "runtime-cert.json",
105
+ Path(__file__).parent / "inert-nodes.json",
106
+ Path(__file__).parent / "harness/verify/ci/gates/backends/check_backends.py"])
107
+ # Tests and documentation are not execution inputs.
108
+ source_paths = [p for p in source_paths if not p.name.startswith("test_")]
109
+ source_hashes = {str(p.relative_to(Path(__file__).parent)): "sha256:" + hashlib.sha256(p.read_bytes()).hexdigest()
110
+ for p in source_paths}
111
+ report: dict = {
112
+ "format": FORMAT,
113
+ "scope": {"activeSemanticVersions": sorted(ACTIVE_SEMANTICS), "targets": list(targets),
114
+ "archivedSemanticVersions": sorted(ARCHIVED_SEMANTICS)},
115
+ "fixtureProvenance": fixture_manifest.get("provenance"),
116
+ "fixturePayloadSha256": fixture_payload_digest(fixture_manifest),
117
+ "generatorInputs": source_hashes,
118
+ "toolchains": {lang: value[1] for lang, value in toolchains.items()},
119
+ "vectors": [],
120
+ "commandFailures": [],
121
+ }
122
+ if not all(value[0] for value in toolchains.values()):
123
+ report["fatal"] = "required target unavailable"
124
+ return report, 1
125
+
126
+ pinned = json.loads(gate.MANIFEST.read_text(encoding="utf-8"))
127
+ if pinned.get("format") != gate.MANIFEST_FORMAT or not isinstance(pinned.get("vectors"), dict):
128
+ report["fatal"] = "invalid legacy backend outcome registry"
129
+ return report, 1
130
+ legacy = pinned["vectors"]
131
+ kinds = set.intersection(*(set(getattr(em, "QUERY_KINDS", {"truth"})) for em in emit.values()))
132
+ manifest_vectors = fixture_manifest.get("vectors")
133
+ if not isinstance(manifest_vectors, list):
134
+ report["fatal"] = "fixture manifest has no vector inventory"
135
+ return report, 1
136
+ declared: dict[str, str] = {}
137
+ for entry in manifest_vectors:
138
+ if not isinstance(entry, dict) or not isinstance(entry.get("id"), str) or not isinstance(entry.get("path"), str):
139
+ report["fatal"] = "fixture manifest has invalid vector entry"
140
+ return report, 1
141
+ if entry["id"] in declared:
142
+ report["fatal"] = "fixture manifest has duplicate vector id"
143
+ return report, 1
144
+ source = fixtures / entry["path"] / "ir.json"
145
+ try:
146
+ semantic = str(json.loads(source.read_text(encoding="utf-8")).get("semanticVersion", "0.2"))
147
+ except (OSError, json.JSONDecodeError) as exc:
148
+ report["fatal"] = "fixture manifest vector has unreadable CLIR: " + entry["id"]
149
+ return report, 1
150
+ declared[entry["id"]] = semantic
151
+
152
+ candidates = list(gate.candidates())
153
+ candidate_ids = [directory.name for directory, _layer in candidates]
154
+ duplicate_candidates = sorted({ident for ident in candidate_ids if candidate_ids.count(ident) > 1})
155
+ missing_candidates = sorted(set(declared) - set(candidate_ids))
156
+ unexpected_candidates = sorted(set(candidate_ids) - set(declared))
157
+ report["coverageFailures"] = {"duplicateCandidates": duplicate_candidates,
158
+ "missingCandidates": missing_candidates,
159
+ "unexpectedCandidates": unexpected_candidates}
160
+
161
+ archive = collections.Counter()
162
+ active: list[tuple[Path, str, str, dict]] = []
163
+ for directory, layer in candidates:
164
+ semantic = str(gate.load(directory, "ir.json").get("semanticVersion", "0.2"))
165
+ if semantic in ARCHIVED_SEMANTICS:
166
+ archive[semantic] += 1
167
+ continue
168
+ if semantic not in ACTIVE_SEMANTICS:
169
+ report.setdefault("unsupportedSemanticVersions", collections.Counter())[semantic] += 1
170
+ continue
171
+ row = {"id": directory.name, "layer": layer, "semanticVersion": semantic,
172
+ "legacyPinned": directory.name in legacy}
173
+ active.append((directory, layer, semantic, row))
174
+
175
+ all_active_ids = {directory.name for directory, *_ in active}
176
+ partial = bool(vectors or vector_globs or fixture_manifest.get("selection", {}).get("mode") == "partial")
177
+ selected = select_vectors(all_active_ids, vectors, vector_globs)
178
+ inert_manifest = inert.load()
179
+ if partial:
180
+ full_inert_manifest = inert_manifest
181
+ inert_manifest, selected = selected_inert_witnesses(inert_manifest, selected)
182
+ absent = selected - all_active_ids
183
+ if absent:
184
+ raise ValueError("selected inertness pair is absent: " + ", ".join(sorted(absent)))
185
+ report["inertWitnessesNotChecked"] = sorted(set(full_inert_manifest["constructs"]) -
186
+ set(inert_manifest["constructs"]))
187
+ active = [item for item in active if item[0].name in selected]
188
+ report["scope"].update(mode="partial" if partial else "full",
189
+ requestedVectors=list(vectors), requestedVectorGlobs=list(vector_globs),
190
+ totalActive=fixture_manifest.get("selection", {}).get("availableVectors", len(all_active_ids)),
191
+ activeInBundle=len(all_active_ids), selectedVectors=sorted(selected))
192
+
193
+ executable: list[tuple[Path, dict, dict]] = []
194
+ for directory, layer, _semantic, row in active:
195
+ outcome, plan, errors = gate.classify(directory, kinds, emit, layer)
196
+ row["classification"] = outcome or "executable"
197
+ if errors:
198
+ row["classificationErrors"] = errors
199
+ if outcome is None:
200
+ executable.append((directory, row, plan))
201
+ else:
202
+ row["actual"] = outcome
203
+ row["legacyExpected"] = legacy.get(directory.name)
204
+ row["legacyMatch"] = outcome == legacy.get(directory.name) if directory.name in legacy else None
205
+ report["vectors"].append(row)
206
+
207
+ environment = dict(os.environ)
208
+ with tempfile.TemporaryDirectory(prefix="codegen-conformance-") as temporary:
209
+ work = Path(temporary)
210
+ (work / "tmp").mkdir()
211
+ environment.update(PYTHONPYCACHEPREFIX=str(work / "pycache"), TMPDIR=str(work / "tmp"),
212
+ GOCACHE=str(work / "gocache"), GOMODCACHE=str(work / "gomodcache"),
213
+ GOMAXPROCS="2", GOFLAGS="-p=2")
214
+ gate.sh = _bounded_runner(timeout, environment, report["commandFailures"])
215
+ projects = {lang: [] for lang in targets}
216
+ for directory, row, plan in executable:
217
+ for lang in targets:
218
+ project = work / directory.name / lang
219
+ gate.D.emit_project(plan, lang, project)
220
+ projects[lang].append(project)
221
+ build_errors = gate.build_projects(emit, projects, work)
222
+ if build_errors:
223
+ report["buildErrors"] = build_errors
224
+ return report, 1
225
+ for directory, row, _plan in executable:
226
+ outcome, errors = gate.execute(emit, directory, work, list(targets))
227
+ row["actual"] = outcome
228
+ row["executionErrors"] = errors
229
+ row["legacyExpected"] = legacy.get(directory.name)
230
+ row["legacyMatch"] = outcome == legacy.get(directory.name) if directory.name in legacy else None
231
+
232
+ report["sourceChangedDuringRun"] = [str(p.relative_to(Path(__file__).parent)) for p in source_paths
233
+ if source_hashes[str(p.relative_to(Path(__file__).parent))] != "sha256:" + hashlib.sha256(p.read_bytes()).hexdigest()]
234
+ check.validate_fixtures(fixtures)
235
+ counts = collections.Counter(row["classification"] for row in report["vectors"])
236
+ executions = collections.Counter(row.get("actual", "not-executed") for row in report["vectors"])
237
+ unpinned = [row["id"] for row in report["vectors"] if not row["legacyPinned"]]
238
+ legacy_mismatches = [row["id"] for row in report["vectors"] if row["legacyPinned"] and not row.get("legacyMatch")]
239
+ golden_mismatches = [row["id"] for row in report["vectors"] if row.get("actual") == "divergence"]
240
+ errors = [row["id"] for row in report["vectors"] if row.get("classificationErrors") or row.get("executionErrors")]
241
+ report["summary"] = {
242
+ "archivedWithdrawn": sum(archive.values()), "archivedBySemanticVersion": dict(sorted(archive.items())),
243
+ "active": len(report["vectors"]), "executable": len(executable),
244
+ "classifications": dict(sorted(counts.items())), "outcomes": dict(sorted(executions.items())),
245
+ "legacyPinned": len(report["vectors"]) - len(unpinned), "unpinned": unpinned,
246
+ "goldenMismatches": golden_mismatches, "legacyMismatches": legacy_mismatches,
247
+ "executionErrors": errors,
248
+ }
249
+ # W1: каждая строка манифеста инертности держится парой-свидетелем;
250
+ # строка без свидетеля, свидетель не «match» или половины пары с разными
251
+ # документами — отказ ворот (CODEGEN-COVERAGE-PLAN §1.1).
252
+ inert_table, inert_failures = inert.witness_failures(inert_manifest, report["vectors"], gate.VECTORS)
253
+ report["inertWitnesses"] = inert_table
254
+ report["summary"]["inertFailures"] = inert_failures
255
+ # Partial development checks can exercise new goldens before release pinning.
256
+ # They do not certify the full registry; an explicit registry stays strict.
257
+ pin_failures = [] if partial else unpinned
258
+ if outcomes is not None:
259
+ registry = json.loads(outcomes.read_text(encoding="utf-8"))
260
+ validate_outcomes(registry, targets)
261
+ digests = vector_input_digests(fixture_manifest)
262
+ pins = registry["vectors"]
263
+ mismatches, unpinned_matches, unpinned_refusals, stale = [], [], [], []
264
+ for row in report["vectors"]:
265
+ pin = pins.get(row["id"])
266
+ actual = row.get("actual")
267
+ if pin is None:
268
+ row["pin"] = "unpinned"
269
+ (unpinned_matches if actual == "match" or str(actual).startswith("structural:")
270
+ else unpinned_refusals).append(row["id"])
271
+ continue
272
+ row["pin"] = "fresh" if pin["inputsSha256"] == digests.get(row["id"]) else "stale"
273
+ row["pinnedExpected"] = pin["outcome"]
274
+ if row["pin"] == "stale":
275
+ stale.append(row["id"])
276
+ if pin["outcome"] != actual:
277
+ mismatches.append(row["id"])
278
+ orphans = [] if fixture_manifest.get("selection", {}).get("mode") == "partial" else sorted(set(pins) - all_active_ids)
279
+ pin_failures = mismatches + unpinned_matches + unpinned_refusals + stale + orphans
280
+ report["orphanPinsNotChecked"] = fixture_manifest.get("selection", {}).get("mode") == "partial"
281
+ report["summary"]["activeRegistryMismatches"] = mismatches
282
+ report["summary"]["unpinnedMatches"] = unpinned_matches
283
+ report["summary"]["unpinnedRefusals"] = unpinned_refusals
284
+ report["summary"]["stalePins"] = stale
285
+ report["summary"]["orphanPins"] = orphans
286
+ report["outcomeRegistry"] = str(outcomes)
287
+ coverage = report["coverageFailures"]
288
+ legacy_failures = [] if partial or outcomes is not None else legacy_mismatches
289
+ return report, int(bool(pin_failures or golden_mismatches or legacy_failures or errors or report["commandFailures"]
290
+ or inert_failures
291
+ or coverage["duplicateCandidates"] or coverage["missingCandidates"]
292
+ or coverage["unexpectedCandidates"] or report.get("unsupportedSemanticVersions")
293
+ or report.get("sourceChangedDuringRun") or not report["vectors"]))
294
+
295
+
296
+ def fixture_payload_digest(manifest: dict) -> str:
297
+ # Publisher provenance and local source paths may change while the exact
298
+ # delivered inputs stay the same. Pin every delivered path and byte hash.
299
+ payload = {"format": manifest.get("format"),
300
+ "files": sorted(({key: row[key] for key in ("path", "sha256", "bytes")}
301
+ for row in manifest["files"]), key=lambda row: row["path"]),
302
+ "vectors": manifest.get("vectors"), "api": manifest.get("api")}
303
+ return "sha256:" + hashlib.sha256(json.dumps(payload, sort_keys=True,
304
+ separators=(",", ":")).encode()).hexdigest()
305
+
306
+
307
+ REGISTRY_FORMAT = "law.codegen-active-outcomes/0.3"
308
+ ALLOWED_OUTCOMES = ("plan: ", "query: ", "golden: ", "structural: ", "runtime: ", "capability: ")
309
+
310
+
311
+ def vector_input_digests(manifest: dict) -> dict[str, str]:
312
+ """Хэш входов КАЖДОГО вектора: пути, байты и размеры его файлов в фикстуре
313
+ (формат реестра 0.3). Изменение или появление одного вектора красит только
314
+ его пин; провенанс фикстуры и чужие векторы на хэш не влияют."""
315
+ files = sorted(({key: row[key] for key in ("path", "sha256", "bytes")} for row in manifest["files"]),
316
+ key=lambda row: row["path"])
317
+ out: dict[str, str] = {}
318
+ for entry in manifest.get("vectors") or []:
319
+ prefix = entry["path"].rstrip("/") + "/"
320
+ own = [row for row in files if row["path"].startswith(prefix)]
321
+ out[entry["id"]] = "sha256:" + hashlib.sha256(
322
+ json.dumps(own, sort_keys=True, separators=(",", ":")).encode()).hexdigest()
323
+ return out
324
+
325
+
326
+ def pin_outcome(pin) -> str | None:
327
+ """Исход пина: формат 0.3 — объект `{outcome, inputsSha256}`; строка
328
+ прежнего формата 0.2 читается ради миграции реестра."""
329
+ if isinstance(pin, dict):
330
+ return pin.get("outcome")
331
+ return pin if isinstance(pin, str) else None
332
+
333
+
334
+ def validate_outcomes(registry: dict, targets: tuple[str, ...]) -> None:
335
+ """A reviewed active registry may add pins, never bless a crash/divergence.
336
+
337
+ Формат 0.3: реестр закреплён по хэшу входов каждого вектора, а не всей
338
+ фикстуры. Полнота относительно активного набора проверяется в `run`
339
+ поимённо: незакреплённое совпадение и расхождение с пином — отказ,
340
+ незакреплённый отказ и устаревший хэш при том же исходе — предупреждение.
341
+ """
342
+ if registry.get("format") != REGISTRY_FORMAT:
343
+ raise ValueError("invalid active outcome registry format")
344
+ if registry.get("semanticVersions") != sorted(ACTIVE_SEMANTICS):
345
+ raise ValueError("active registry semantic scope mismatch")
346
+ if registry.get("targets") != ["py", "ts", "go"] or not set(targets) <= set(registry["targets"]):
347
+ raise ValueError("active registry target scope mismatch")
348
+ vectors = registry.get("vectors")
349
+ if not isinstance(vectors, dict):
350
+ raise ValueError("active registry has no vector pins")
351
+ for ident, pin in vectors.items():
352
+ if not isinstance(pin, dict) or not isinstance(pin.get("inputsSha256"), str) \
353
+ or not pin["inputsSha256"].startswith("sha256:"):
354
+ raise ValueError(f"active registry pin without inputs digest: {ident}")
355
+ value = pin.get("outcome")
356
+ if not isinstance(value, str) or not (value == "match" or value.startswith(ALLOWED_OUTCOMES)):
357
+ raise ValueError("active registry cannot pin a crash, divergence, or unknown outcome")
358
+
359
+
360
+ def main(argv: list[str] | None = None) -> int:
361
+ parser = argparse.ArgumentParser(description=__doc__, allow_abbrev=False)
362
+ parser.add_argument("--fixtures", required=True, type=Path)
363
+ parser.add_argument("--core-kit", required=True, type=Path)
364
+ parser.add_argument("--report", required=True, type=Path)
365
+ parser.add_argument("--targets", default="py", help="comma-separated py,ts,go")
366
+ parser.add_argument("--outcomes", type=Path, help="reviewed codegen-owned active outcome registry")
367
+ parser.add_argument("--vector", action="append", default=[], metavar="ID",
368
+ help="check this exact active vector ID (repeatable; partial report)")
369
+ parser.add_argument("--vector-glob", action="append", default=[], metavar="PATTERN",
370
+ help="check matching active vector IDs (repeatable; quote shell patterns)")
371
+ parser.add_argument("--timeout", type=float, default=30.0, help="seconds per generated runner command")
372
+ args = parser.parse_args(argv)
373
+ if args.timeout <= 0:
374
+ parser.error("--timeout must be positive")
375
+ started = time.perf_counter()
376
+ try:
377
+ report, status = run(args.fixtures, args.core_kit, timeout=args.timeout,
378
+ targets=tuple(args.targets.split(",")), outcomes=args.outcomes,
379
+ vectors=tuple(args.vector), vector_globs=tuple(args.vector_glob))
380
+ except Exception as exc:
381
+ report, status = {"format": FORMAT, "fatal": str(exc),
382
+ "scope": {"mode": "partial" if args.vector or args.vector_glob else "full",
383
+ "requestedVectors": args.vector,
384
+ "requestedVectorGlobs": args.vector_glob}}, 1
385
+ report["elapsedSeconds"] = time.perf_counter() - started
386
+ _save(args.report, report)
387
+ summary = report.get("summary", {})
388
+ if "fatal" in report:
389
+ print("conformance: FAILED — " + report["fatal"])
390
+ return status
391
+ if report.get("scope", {}).get("mode") == "partial":
392
+ print(f"conformance: PARTIAL — selected {len(report['scope']['selectedVectors'])} "
393
+ f"of {report['scope']['totalActive']} active vectors")
394
+ print("conformance: active {active}; executable {executable}; withdrawn {archivedWithdrawn}; "
395
+ "unpinned {unpinned}; mismatches {mismatches}".format(
396
+ active=summary.get("active", 0), executable=summary.get("executable", 0),
397
+ archivedWithdrawn=summary.get("archivedWithdrawn", 0),
398
+ unpinned=len(summary.get("unpinned", [])), mismatches=len(summary.get("goldenMismatches", [])),
399
+ ))
400
+ if "activeRegistryMismatches" in summary:
401
+ print("conformance: registry 0.3 — mismatches {m}; unpinned matches {um}; unpinned refusals {ur}; "
402
+ "stale pins {st}; orphan pins {orph}".format(
403
+ m=len(summary["activeRegistryMismatches"]), um=len(summary.get("unpinnedMatches", [])),
404
+ ur=len(summary.get("unpinnedRefusals", [])), st=len(summary.get("stalePins", [])),
405
+ orph=len(summary.get("orphanPins", []))))
406
+ for label, key in (("mismatch", "activeRegistryMismatches"), ("unpinned match", "unpinnedMatches"),
407
+ ("stale pin", "stalePins")):
408
+ for ident in summary.get(key, [])[:20]:
409
+ print(f" {label}: {ident}")
410
+ return status
411
+
412
+
413
+ if __name__ == "__main__":
414
+ raise SystemExit(main())
@@ -0,0 +1,131 @@
1
+ """Bounded cross-target acceptance: runtime contract and three active models.
2
+
3
+ This does not replace the complete conformance gate or Excel recalculation.
4
+ Unavailable toolchains are reported separately and fail only when required.
5
+ """
6
+ from __future__ import annotations
7
+
8
+ import argparse
9
+ import json
10
+ import os
11
+ from pathlib import Path
12
+ import subprocess
13
+ import sys
14
+ import tempfile
15
+
16
+ HERE = Path(__file__).resolve().parent
17
+ sys.path.insert(0, str(HERE))
18
+ import check
19
+ import core_support
20
+ import runtime_cert
21
+
22
+ PROBES = ("ERR-E0241-SYNC-RANK-WITNESS-V02", "ERR-E0242-KEY-PER-RELATION",
23
+ "ERR-E0248-EXACT-BYTES-HASH-OK")
24
+
25
+
26
+ def main(argv=None) -> int:
27
+ ap = argparse.ArgumentParser(description=__doc__)
28
+ ap.add_argument("--core-kit", type=Path, required=True)
29
+ ap.add_argument("--fixtures", type=Path, required=True)
30
+ ap.add_argument("--targets", default="all")
31
+ ap.add_argument("--require", default="py")
32
+ ap.add_argument("--timeout", type=float, default=90, help="seconds per compiler/runner command")
33
+ ap.add_argument("--report", type=Path, required=True)
34
+ args = ap.parse_args(argv)
35
+ if args.timeout <= 0:
36
+ ap.error("--timeout must be positive")
37
+ core_support.configure_core_kit(args.core_kit)
38
+ core_support.validate_core_kit(args.core_kit)
39
+ gate = check.load_harness(args.fixtures.resolve())
40
+ known = gate.D.targets(protocol="runner")
41
+ wanted = known if args.targets == "all" else args.targets.split(",")
42
+ required = set(filter(None, args.require.split(",")))
43
+ if not wanted or len(set(wanted)) != len(wanted) or set(wanted) - set(known) or required - set(wanted):
44
+ ap.error("targets must be unique known runner targets and include --require")
45
+ vectors = {p.name: (p, layer) for p, layer in gate.candidates() if p.name in PROBES}
46
+ if set(vectors) != set(PROBES):
47
+ raise RuntimeError("acceptance probes missing from fixture kit")
48
+
49
+ command_failures = []
50
+ cache_env = dict(os.environ)
51
+
52
+ def bounded(cmd, cwd=None):
53
+ try:
54
+ result = subprocess.run(cmd, cwd=cwd, env=cache_env, text=True, capture_output=True, timeout=args.timeout)
55
+ except subprocess.TimeoutExpired:
56
+ result = subprocess.CompletedProcess(cmd, 124, "", f"timeout after {args.timeout}s")
57
+ except OSError as exc:
58
+ result = subprocess.CompletedProcess(cmd, 127, "", str(exc))
59
+ if result.returncode:
60
+ command_failures.append({"command": list(map(str, cmd)), "returncode": result.returncode,
61
+ "output": (result.stdout + result.stderr)[:4000]})
62
+ return result
63
+
64
+ gate.sh = bounded
65
+ report = {"format": "law.codegen-matrix/0.1", "scope": "runtime-cert-and-three-active-models",
66
+ "probes": list(PROBES), "targets": {}, "xlsx": "not-in-runner-matrix"}
67
+
68
+ def save():
69
+ args.report.parent.mkdir(parents=True, exist_ok=True)
70
+ args.report.write_text(json.dumps(report, ensure_ascii=False, indent=2) + "\n", encoding="utf-8")
71
+
72
+ with tempfile.TemporaryDirectory(prefix="codegen-matrix-") as tmp:
73
+ work = Path(tmp)
74
+ cache_env.update(GOCACHE=str(work / "go-cache"),
75
+ CLANG_MODULE_CACHE_PATH=str(work / "clang-cache"),
76
+ SWIFT_MODULECACHE_PATH=str(work / "swift-cache"))
77
+ for lang in wanted:
78
+ command_failures.clear()
79
+ em = gate.D.emitter(lang)
80
+ available, reason = em.available()
81
+ if not available:
82
+ report["targets"][lang] = {"status": "unavailable", "reason": reason}
83
+ print(f"matrix: {lang}: unavailable — {reason}", flush=True)
84
+ save()
85
+ continue
86
+ print(f"matrix: {lang}: checking ({reason})", flush=True)
87
+ active = {lang: em}
88
+ target_work = work / lang
89
+ errors = gate.version_probe(active)
90
+ runtime_report = {}
91
+ try:
92
+ runtime_report, cert_errors = runtime_cert.certify_target(
93
+ lang, em, target_work, gate.CERT, gate.D.emit_project,
94
+ gate.build_projects, gate.run_target,
95
+ )
96
+ errors += cert_errors
97
+ except runtime_cert.FixtureError as exc:
98
+ errors.append(f"сертификация {lang}: неверный capability-контракт: {exc}")
99
+ outcomes = {}
100
+ if not errors:
101
+ projects = []
102
+ for name in PROBES:
103
+ directory, layer = vectors[name]
104
+ outcome, plan, problems = gate.classify(directory, set(getattr(em, "QUERY_KINDS", {"truth"})), active, layer)
105
+ if outcome is not None or problems:
106
+ errors.append(f"{name}: not executable: {outcome}; {problems}")
107
+ continue
108
+ project = target_work / name / lang
109
+ gate.D.emit_project(plan, lang, project)
110
+ projects.append(project)
111
+ if not errors:
112
+ errors += gate.build_projects(active, {lang: projects}, target_work)
113
+ if not errors:
114
+ for name in PROBES:
115
+ outcome, problems = gate.execute(active, vectors[name][0], target_work, [lang])
116
+ outcomes[name] = outcome
117
+ errors += problems
118
+ if outcome != "match":
119
+ errors.append(f"{name}: expected match, got {outcome}")
120
+ report["targets"][lang] = {"status": "fail" if errors else "pass", "toolchain": reason,
121
+ "runtimeCases": len(runtime_report.get("checkedFixtureIds", [])),
122
+ "runtimeCertification": runtime_report,
123
+ "modelOutcomes": outcomes, "errors": errors, "failedCommands": list(command_failures)}
124
+ print(f"matrix: {lang}: {report['targets'][lang]['status']}", flush=True)
125
+ save()
126
+ return int(any(row["status"] == "fail" or (lang in required and row["status"] != "pass")
127
+ for lang, row in report["targets"].items()))
128
+
129
+
130
+ if __name__ == "__main__":
131
+ raise SystemExit(main())