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.
- arxo_codegen/__init__.py +5 -0
- arxo_codegen/__main__.py +7 -0
- arxo_codegen/active-outcomes.json +6837 -0
- arxo_codegen/check.py +158 -0
- arxo_codegen/check_active_canaries.py +262 -0
- arxo_codegen/check_api.py +76 -0
- arxo_codegen/check_conformance.py +414 -0
- arxo_codegen/check_matrix.py +131 -0
- arxo_codegen/check_packages.py +281 -0
- arxo_codegen/check_semantic_probes.py +293 -0
- arxo_codegen/check_xlsx.py +320 -0
- arxo_codegen/consumer_runner.py +316 -0
- arxo_codegen/core_support.py +203 -0
- arxo_codegen/coverage.py +341 -0
- arxo_codegen/deliver.py +180 -0
- arxo_codegen/examples/xlsx/pricing.lawir.json +409 -0
- arxo_codegen/examples/xlsx/pricing.xlsx.json +52 -0
- arxo_codegen/excel_plan.py +602 -0
- arxo_codegen/harness/verify/ci/gates/backends/check_backends.py +1248 -0
- arxo_codegen/harness/verify/ci/gates/backends/l1_fixtures.py +463 -0
- arxo_codegen/inert-nodes.json +62 -0
- arxo_codegen/inert.py +91 -0
- arxo_codegen/inputs.py +59 -0
- arxo_codegen/package.py +565 -0
- arxo_codegen/plan.py +4053 -0
- arxo_codegen/recertify_registry.py +100 -0
- arxo_codegen/runtime-cert.json +5259 -0
- arxo_codegen/runtime_cert.py +233 -0
- arxo_codegen/survey.py +272 -0
- arxo_codegen/target_runner.py +101 -0
- arxo_codegen/targets/bsl/cert.bsl +113 -0
- arxo_codegen/targets/bsl/emit.py +356 -0
- arxo_codegen/targets/bsl/json.bsl +243 -0
- arxo_codegen/targets/bsl/runner.bsl +210 -0
- arxo_codegen/targets/bsl/runtime.bsl +858 -0
- arxo_codegen/targets/cs/Cert.cs +90 -0
- arxo_codegen/targets/cs/Runner.cs +154 -0
- arxo_codegen/targets/cs/Runtime.cs +243 -0
- arxo_codegen/targets/cs/emit.py +223 -0
- arxo_codegen/targets/dart/cert.dart +99 -0
- arxo_codegen/targets/dart/emit.py +206 -0
- arxo_codegen/targets/dart/json.dart +166 -0
- arxo_codegen/targets/dart/runner.dart +154 -0
- arxo_codegen/targets/dart/runtime.dart +293 -0
- arxo_codegen/targets/erl/cert.erl +64 -0
- arxo_codegen/targets/erl/emit.py +216 -0
- arxo_codegen/targets/erl/rt.erl +238 -0
- arxo_codegen/targets/erl/runner.erl +130 -0
- arxo_codegen/targets/ex/cert.exs +62 -0
- arxo_codegen/targets/ex/emit.py +204 -0
- arxo_codegen/targets/ex/runner.exs +124 -0
- arxo_codegen/targets/ex/runtime.ex +227 -0
- arxo_codegen/targets/go/apiBase.go +483 -0
- arxo_codegen/targets/go/apicheck.go +97 -0
- arxo_codegen/targets/go/cert.go +218 -0
- arxo_codegen/targets/go/emit.py +1806 -0
- arxo_codegen/targets/go/evaluate.go +1826 -0
- arxo_codegen/targets/go/runner.go +38 -0
- arxo_codegen/targets/go/runtime.go +2612 -0
- arxo_codegen/targets/java/Cert.java +91 -0
- arxo_codegen/targets/java/Json.java +148 -0
- arxo_codegen/targets/java/Runner.java +151 -0
- arxo_codegen/targets/java/Runtime.java +243 -0
- arxo_codegen/targets/java/emit.py +198 -0
- arxo_codegen/targets/kt/Cert.kt +67 -0
- arxo_codegen/targets/kt/Json.kt +103 -0
- arxo_codegen/targets/kt/Runner.kt +126 -0
- arxo_codegen/targets/kt/Runtime.kt +196 -0
- arxo_codegen/targets/kt/emit.py +195 -0
- arxo_codegen/targets/lua/cert.lua +82 -0
- arxo_codegen/targets/lua/emit.py +250 -0
- arxo_codegen/targets/lua/json.lua +133 -0
- arxo_codegen/targets/lua/runner.lua +167 -0
- arxo_codegen/targets/lua/runtime.lua +541 -0
- arxo_codegen/targets/php/cert.php +84 -0
- arxo_codegen/targets/php/emit.py +229 -0
- arxo_codegen/targets/php/json.php +144 -0
- arxo_codegen/targets/php/runner.php +189 -0
- arxo_codegen/targets/php/runtime.php +660 -0
- arxo_codegen/targets/py/api_base.py +334 -0
- arxo_codegen/targets/py/apicheck.py +51 -0
- arxo_codegen/targets/py/cert.py +140 -0
- arxo_codegen/targets/py/emit.py +1140 -0
- arxo_codegen/targets/py/evaluate.py +980 -0
- arxo_codegen/targets/py/runner.py +38 -0
- arxo_codegen/targets/py/runtime.py +1634 -0
- arxo_codegen/targets/rb/cert.rb +73 -0
- arxo_codegen/targets/rb/emit.py +203 -0
- arxo_codegen/targets/rb/runner.rb +130 -0
- arxo_codegen/targets/rb/runtime.rb +255 -0
- arxo_codegen/targets/rs/Cargo.lock +142 -0
- arxo_codegen/targets/rs/Cargo.toml +16 -0
- arxo_codegen/targets/rs/cert.rs +106 -0
- arxo_codegen/targets/rs/emit.py +233 -0
- arxo_codegen/targets/rs/prefetch.rs +1 -0
- arxo_codegen/targets/rs/runner.rs +181 -0
- arxo_codegen/targets/rs/runtime.rs +448 -0
- arxo_codegen/targets/scala/Cert.scala +77 -0
- arxo_codegen/targets/scala/Json.scala +140 -0
- arxo_codegen/targets/scala/Runner.scala +121 -0
- arxo_codegen/targets/scala/Runtime.scala +269 -0
- arxo_codegen/targets/scala/emit.py +193 -0
- arxo_codegen/targets/swift/cert.swift +105 -0
- arxo_codegen/targets/swift/emit.py +231 -0
- arxo_codegen/targets/swift/json.swift +208 -0
- arxo_codegen/targets/swift/runner.swift +133 -0
- arxo_codegen/targets/swift/runtime.swift +528 -0
- arxo_codegen/targets/ts/apiBase.ts +250 -0
- arxo_codegen/targets/ts/apicheck.ts +37 -0
- arxo_codegen/targets/ts/cert.ts +96 -0
- arxo_codegen/targets/ts/emit.py +1214 -0
- arxo_codegen/targets/ts/evaluate.ts +894 -0
- arxo_codegen/targets/ts/package-lock.json +45 -0
- arxo_codegen/targets/ts/package.json +9 -0
- arxo_codegen/targets/ts/runner.ts +31 -0
- arxo_codegen/targets/ts/runtime.ts +1369 -0
- arxo_codegen/targets/xlsx/README.ru.md +176 -0
- arxo_codegen/targets/xlsx/emit.py +198 -0
- arxo_codegen/temporal.py +59 -0
- arxo_codegen/test_alias_types.py +171 -0
- arxo_codegen/test_api_contract.py +89 -0
- arxo_codegen/test_calendar_policy.py +48 -0
- arxo_codegen/test_check.py +49 -0
- arxo_codegen/test_check_active_canaries.py +96 -0
- arxo_codegen/test_check_conformance.py +132 -0
- arxo_codegen/test_check_semantic_probes.py +85 -0
- arxo_codegen/test_check_xlsx.py +52 -0
- arxo_codegen/test_consumer_runner.py +175 -0
- arxo_codegen/test_core_support.py +80 -0
- arxo_codegen/test_e0212_decision_time.py +63 -0
- arxo_codegen/test_head_aggregate.py +71 -0
- arxo_codegen/test_inert.py +143 -0
- arxo_codegen/test_instant_types.py +113 -0
- arxo_codegen/test_key_conflict_scope.py +92 -0
- arxo_codegen/test_key_conflict_targets.py +17 -0
- arxo_codegen/test_package_layout.py +163 -0
- arxo_codegen/test_plan_language_version.py +44 -0
- arxo_codegen/test_positions_refusal.py +43 -0
- arxo_codegen/test_recertify_registry.py +77 -0
- arxo_codegen/test_ruby_target.py +41 -0
- arxo_codegen/test_runtime_cert.py +83 -0
- arxo_codegen/test_secondary_key_conflict_scope.py +17 -0
- arxo_codegen/test_stage_r.py +108 -0
- arxo_codegen/test_standalone_inputs.py +71 -0
- arxo_codegen/test_support.py +150 -0
- arxo_codegen/test_survey_inputs.py +115 -0
- arxo_codegen-0.4.0.dist-info/LICENSE +202 -0
- arxo_codegen-0.4.0.dist-info/METADATA +1673 -0
- arxo_codegen-0.4.0.dist-info/RECORD +152 -0
- arxo_codegen-0.4.0.dist-info/WHEEL +5 -0
- arxo_codegen-0.4.0.dist-info/entry_points.txt +13 -0
- 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())
|