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,281 @@
|
|
|
1
|
+
"""Пакеты трёх целей против плоского API на проверенном комплекте (DECISION-0423 §2.7).
|
|
2
|
+
|
|
3
|
+
Шаг приёмки шага 5: для API-пакетов комплекта (`corpus/clir`) печатается
|
|
4
|
+
устанавливаемый пакет каждой цели (`package.py`) и ставится так, как его
|
|
5
|
+
получит потребитель: Python — каталогом пакета из `src/` на пути импорта,
|
|
6
|
+
TypeScript — тарболом `npm pack`, установленным без сети, Go — модулем-
|
|
7
|
+
потребителем с `replace`. Задания `check_api` исполняются через
|
|
8
|
+
опубликованную поверхность пакета (реестры вопросов и входов) и обязаны дать
|
|
9
|
+
те же исходы, что плоское API Python; пример из README пакета обязан
|
|
10
|
+
исполниться; паспорт пакета называет семантический хэш канона.
|
|
11
|
+
"""
|
|
12
|
+
from __future__ import annotations
|
|
13
|
+
|
|
14
|
+
import argparse
|
|
15
|
+
import json
|
|
16
|
+
import os
|
|
17
|
+
from pathlib import Path
|
|
18
|
+
import re
|
|
19
|
+
import shutil
|
|
20
|
+
import subprocess
|
|
21
|
+
import sys
|
|
22
|
+
import tempfile
|
|
23
|
+
import tomllib
|
|
24
|
+
|
|
25
|
+
HERE = Path(__file__).resolve().parent
|
|
26
|
+
sys.path.insert(0, str(HERE))
|
|
27
|
+
|
|
28
|
+
FENCE = {"py": "python", "ts": "ts", "go": "go"}
|
|
29
|
+
GO_EXPORTED = ("TermFromNode", "Inputs", "Negated", "Questions", "CallOptions", "RefusedError", "Fact")
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
def _names(lang: str, name: str) -> dict:
|
|
33
|
+
"""Имена пакета проверки: семья имён производной не выбрана (§6), поэтому
|
|
34
|
+
приёмка берёт нейтральные имена, не занимающие ничего в реестрах."""
|
|
35
|
+
return {"py": {"name": f"codegen-check-{name}"},
|
|
36
|
+
"ts": {"name": f"@codegen-check/{name}"},
|
|
37
|
+
"go": {"name": name, "module": f"example.com/codegen-check/{name}"}}[lang]
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
def _example(readme: str, lang: str) -> str | None:
|
|
41
|
+
hit = re.search(rf"```{FENCE[lang]}\n(.*?)```", readme, re.S)
|
|
42
|
+
return hit.group(1) if hit else None
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
def _run(cmd: list, cwd: Path, env: dict | None = None, timeout: float = 600) -> subprocess.CompletedProcess:
|
|
46
|
+
return subprocess.run([str(c) for c in cmd], cwd=cwd, env=env, capture_output=True, text=True,
|
|
47
|
+
timeout=timeout, check=False)
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
def _batch(gate, res: subprocess.CompletedProcess) -> tuple[str, object]:
|
|
51
|
+
"""Разбор ответа харнесса как у `_api_batch` ворот: исход задания —
|
|
52
|
+
('answer', ключ) либо ('refused', текст)."""
|
|
53
|
+
if res.returncode != 0:
|
|
54
|
+
return "crash", f"код {res.returncode}: {gate.crash_line(res.stderr or res.stdout)}"
|
|
55
|
+
try:
|
|
56
|
+
out = json.loads(res.stdout)
|
|
57
|
+
return "answers", [("refused", a["refused"]) if "refused" in a else ("answer", gate.answer_key(a))
|
|
58
|
+
for a in out["answers"]]
|
|
59
|
+
except (ValueError, KeyError, TypeError):
|
|
60
|
+
return "crash", f"ответ пакета не JSON: {res.stdout[:200]!r}"
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
def consume_py(gate, em, pkg: Path, consumer: Path, bpath: Path) -> tuple[tuple, subprocess.CompletedProcess | None]:
|
|
64
|
+
import package as PK
|
|
65
|
+
project = tomllib.loads((pkg / "pyproject.toml").read_text(encoding="utf-8"))["project"]
|
|
66
|
+
imp = PK.py_import_name(project["name"])
|
|
67
|
+
site = consumer / "site"
|
|
68
|
+
shutil.copytree(pkg / "src" / imp, site / imp)
|
|
69
|
+
harness = (Path(em.__file__).parent / "apicheck.py").read_text(encoding="utf-8")
|
|
70
|
+
harness = PK.rewrite(harness, [("import runtime as RT", f"from {imp}._internal import runtime as RT"),
|
|
71
|
+
("import api ", f"import {imp} as api ")], "apicheck.py")
|
|
72
|
+
(consumer / "apicheck.py").write_text(harness, encoding="utf-8")
|
|
73
|
+
env = dict(os.environ, PYTHONPATH=str(site))
|
|
74
|
+
answers = _batch(gate, _run([sys.executable, consumer / "apicheck.py", bpath], consumer, env))
|
|
75
|
+
sample = _example((pkg / "README.md").read_text(encoding="utf-8"), "py")
|
|
76
|
+
if sample is None:
|
|
77
|
+
return answers, None
|
|
78
|
+
(consumer / "example.py").write_text(sample, encoding="utf-8")
|
|
79
|
+
return answers, _run([sys.executable, consumer / "example.py"], consumer, env)
|
|
80
|
+
|
|
81
|
+
|
|
82
|
+
def consume_ts(gate, em, pkg: Path, consumer: Path, bpath: Path) -> tuple[tuple, subprocess.CompletedProcess | None]:
|
|
83
|
+
npm = shutil.which("npm")
|
|
84
|
+
modules = em.toolchain()
|
|
85
|
+
name = json.loads((pkg / "package.json").read_text(encoding="utf-8"))["name"]
|
|
86
|
+
consumer.mkdir(parents=True, exist_ok=True)
|
|
87
|
+
packed = _run([npm, "pack", pkg, "--silent", "--pack-destination", consumer], consumer)
|
|
88
|
+
if packed.returncode:
|
|
89
|
+
return ("crash", "npm pack: " + gate.crash_line(packed.stderr or packed.stdout)), None
|
|
90
|
+
tarball = consumer / packed.stdout.strip().splitlines()[-1]
|
|
91
|
+
(consumer / "package.json").write_text('{"name": "consumer", "private": true}\n', encoding="utf-8")
|
|
92
|
+
installed = _run([npm, "install", "--offline", "--no-audit", "--no-fund", "--silent", tarball], consumer)
|
|
93
|
+
if installed.returncode:
|
|
94
|
+
return ("crash", "npm install: " + gate.crash_line(installed.stderr or installed.stdout)), None
|
|
95
|
+
base = f"./node_modules/{name}/dist/internal"
|
|
96
|
+
harness = (Path(em.__file__).parent / "apicheck.ts").read_text(encoding="utf-8")
|
|
97
|
+
for old, new in (('from "./runtime"', f'from "{base}/runtime"'), ('from "./apiBase"', f'from "{base}/apiBase"'),
|
|
98
|
+
('from "./api"', f'from "{name}"'), ('from "./evaluate"', f'from "{base}/evaluate"')):
|
|
99
|
+
if harness.count(old) != 1:
|
|
100
|
+
return ("crash", f"apicheck.ts: ожидался один импорт {old}"), None
|
|
101
|
+
harness = harness.replace(old, new)
|
|
102
|
+
(consumer / "apicheck.ts").write_text(harness, encoding="utf-8")
|
|
103
|
+
sample = _example((pkg / "README.md").read_text(encoding="utf-8"), "ts")
|
|
104
|
+
files = ["apicheck.ts"]
|
|
105
|
+
if sample is not None:
|
|
106
|
+
(consumer / "example.ts").write_text(sample, encoding="utf-8")
|
|
107
|
+
files.append("example.ts")
|
|
108
|
+
built = _run([modules / ".bin" / "tsc", "--target", "es2022", "--module", "commonjs", "--strict",
|
|
109
|
+
"--typeRoots", modules / "@types", "--types", "node", *files], consumer)
|
|
110
|
+
if built.returncode:
|
|
111
|
+
return ("crash", "tsc потребителя: " + (built.stdout + built.stderr)[:600]), None
|
|
112
|
+
# JS рядом с исходником: относительный путь в node_modules верен и для tsc, и для node.
|
|
113
|
+
answers = _batch(gate, _run(["node", consumer / "apicheck.js", bpath], consumer))
|
|
114
|
+
return answers, (_run(["node", consumer / "example.js"], consumer) if sample is not None else None)
|
|
115
|
+
|
|
116
|
+
|
|
117
|
+
def consume_go(gate, em, pkg: Path, consumer: Path, bpath: Path, module: str) -> tuple[tuple, subprocess.CompletedProcess | None]:
|
|
118
|
+
consumer.mkdir(parents=True, exist_ok=True)
|
|
119
|
+
(consumer / "go.mod").write_text(f"module consumer\n\ngo {em.TOOLCHAIN_MIN}\n\nrequire {module} v0.0.0\n\n"
|
|
120
|
+
f"replace {module} => {pkg}\n", encoding="utf-8")
|
|
121
|
+
harness = (Path(em.__file__).parent / "apicheck.go").read_text(encoding="utf-8")
|
|
122
|
+
harness = harness.replace("//go:build apicheck\n\n", "", 1)
|
|
123
|
+
harness, count = re.subn(r'(\t"sync"\n)\)', rf'\1\n\tlib "{module}"\n)', harness, count=1)
|
|
124
|
+
if count != 1:
|
|
125
|
+
return ("crash", "apicheck.go: блок импортов незнаком"), None
|
|
126
|
+
for ident in GO_EXPORTED:
|
|
127
|
+
harness = re.sub(rf"(?<![\w.]){ident}\b", f"lib.{ident}", harness)
|
|
128
|
+
harness += ("\nfunc asList(v interface{}) []interface{} {\n\tl, _ := v.([]interface{})\n\treturn l\n}\n")
|
|
129
|
+
(consumer / "apicheck").mkdir()
|
|
130
|
+
(consumer / "apicheck" / "main.go").write_text(harness, encoding="utf-8")
|
|
131
|
+
env = dict(os.environ, GOFLAGS="-mod=mod")
|
|
132
|
+
built = _run(["go", "build", "-o", consumer / "bin" / "apicheck", "./apicheck"], consumer, env)
|
|
133
|
+
if built.returncode:
|
|
134
|
+
return ("crash", "go build харнесса: " + (built.stdout + built.stderr)[:600]), None
|
|
135
|
+
answers = _batch(gate, _run([consumer / "bin" / "apicheck", bpath], consumer, env))
|
|
136
|
+
sample = _example((pkg / "README.md").read_text(encoding="utf-8"), "go")
|
|
137
|
+
if sample is None:
|
|
138
|
+
return answers, None
|
|
139
|
+
(consumer / "example").mkdir()
|
|
140
|
+
(consumer / "example" / "main.go").write_text(sample, encoding="utf-8")
|
|
141
|
+
built = _run(["go", "build", "-o", consumer / "bin" / "example", "./example"], consumer, env)
|
|
142
|
+
if built.returncode:
|
|
143
|
+
return answers, built
|
|
144
|
+
return answers, _run([consumer / "bin" / "example"], consumer, env)
|
|
145
|
+
|
|
146
|
+
|
|
147
|
+
SPOIL = ('evaluation_status=doc["evaluationStatus"], issues=tuple(doc["issues"])',
|
|
148
|
+
'evaluation_status="COMPUTED", issues=()')
|
|
149
|
+
|
|
150
|
+
|
|
151
|
+
def canary_py(gate, consumer: Path, bpath: Path, reference: list, name: str) -> list[str]:
|
|
152
|
+
"""Канарейка: в установленном пакете Python общая часть API отвечает
|
|
153
|
+
`COMPUTED` без issues вместо ответа `evaluate`. На заданиях с литералом
|
|
154
|
+
чужого типа эталон — `TYPE_ERROR`, поэтому исходы обязаны разойтись —
|
|
155
|
+
иначе шаг не видит того, что отвечает опубликованная поверхность."""
|
|
156
|
+
bases = list((consumer / "site").glob("*/_internal/api_base.py"))
|
|
157
|
+
if len(bases) != 1:
|
|
158
|
+
return [f"пакет {name}: канарейке нечего портить — нет _internal/api_base.py"]
|
|
159
|
+
text = bases[0].read_text(encoding="utf-8")
|
|
160
|
+
if SPOIL[0] not in text:
|
|
161
|
+
return [f"пакет {name}: канарейка не нашла, что портить в api_base.py"]
|
|
162
|
+
bases[0].write_text(text.replace(*SPOIL), encoding="utf-8")
|
|
163
|
+
try:
|
|
164
|
+
env = dict(os.environ, PYTHONPATH=str(consumer / "site"))
|
|
165
|
+
kind, outs = _batch(gate, _run([sys.executable, consumer / "apicheck.py", bpath], consumer, env))
|
|
166
|
+
finally:
|
|
167
|
+
bases[0].write_text(text, encoding="utf-8")
|
|
168
|
+
if kind == "answers" and outs == reference:
|
|
169
|
+
return [f"пакет {name}: канарейка — порча статуса и issues в пакете не видна шагу"]
|
|
170
|
+
return []
|
|
171
|
+
|
|
172
|
+
|
|
173
|
+
def package_check(gate, emit: dict, active: list[str], work: Path) -> tuple[list[str], int]:
|
|
174
|
+
import package as PK
|
|
175
|
+
P, D = gate.P, gate.D
|
|
176
|
+
problems, checked = [], 0
|
|
177
|
+
for name in gate.API_PACKAGES + gate.API_PACKAGES_L1:
|
|
178
|
+
path = gate.CLIR / f"{name}.lawir.json"
|
|
179
|
+
if not path.exists():
|
|
180
|
+
problems.append(f"пакет {name}: снимка нет")
|
|
181
|
+
continue
|
|
182
|
+
ir = json.loads(path.read_text(encoding="utf-8"))
|
|
183
|
+
world = P.resolve_world(ir, P.world_index(gate.ROOT)) if ir.get("imports") else []
|
|
184
|
+
pl = P.build_plan(ir, world)
|
|
185
|
+
jobs = gate.api_jobs(pl)
|
|
186
|
+
root = work / name
|
|
187
|
+
root.mkdir(parents=True)
|
|
188
|
+
for j, job in enumerate(jobs):
|
|
189
|
+
job["queryId"] = f"urn:query:apicheck:{j}"
|
|
190
|
+
bpath = root / "batch.json"
|
|
191
|
+
bpath.write_text(json.dumps({"batch": jobs, "concurrent": False}, ensure_ascii=False), encoding="utf-8")
|
|
192
|
+
# Эталон — плоское API Python: его с раннером всех целей уже сверил check_api.
|
|
193
|
+
flat = root / "flat-py"
|
|
194
|
+
D.emit_project(pl, "py", flat)
|
|
195
|
+
shutil.copyfile(Path(emit["py"].__file__).parent / "apicheck.py", flat / "apicheck.py")
|
|
196
|
+
kind, reference = gate._api_batch(emit["py"], flat, bpath)
|
|
197
|
+
if kind == "crash":
|
|
198
|
+
problems.append(f"пакет {name}: плоское API Python — КРАХ {reference}")
|
|
199
|
+
continue
|
|
200
|
+
digest = D.semantic_hash(ir)
|
|
201
|
+
for lang in active:
|
|
202
|
+
names = _names(lang, name)
|
|
203
|
+
pkg = root / lang / "package"
|
|
204
|
+
try:
|
|
205
|
+
passport = PK.package(ir, lang, pkg, name=names["name"], version="0.0.0",
|
|
206
|
+
module=names.get("module"), world=world)
|
|
207
|
+
except (PK.PackageError, P.Refused) as exc:
|
|
208
|
+
problems.append(f"пакет {name}/{lang}: упаковка отказала — {exc}")
|
|
209
|
+
continue
|
|
210
|
+
if passport["semanticHash"] != digest:
|
|
211
|
+
problems.append(f"пакет {name}/{lang}: паспорт называет чужой семантический хэш")
|
|
212
|
+
consumer = root / lang / "consumer"
|
|
213
|
+
consumer.mkdir(parents=True, exist_ok=True)
|
|
214
|
+
if lang == "py":
|
|
215
|
+
(kind, outs), sample = consume_py(gate, emit[lang], pkg, consumer, bpath)
|
|
216
|
+
elif lang == "ts":
|
|
217
|
+
(kind, outs), sample = consume_ts(gate, emit[lang], pkg, consumer, bpath)
|
|
218
|
+
else:
|
|
219
|
+
(kind, outs), sample = consume_go(gate, emit[lang], pkg, consumer, bpath, names["module"])
|
|
220
|
+
if kind == "crash":
|
|
221
|
+
problems.append(f"пакет {name}/{lang}: КРАХ {outs}")
|
|
222
|
+
continue
|
|
223
|
+
if outs != reference:
|
|
224
|
+
diff = [j for j, (a, b) in enumerate(zip(outs, reference)) if a != b]
|
|
225
|
+
problems.append(f"пакет {name}/{lang}: исходы разошлись с плоским API, задания {diff[:8]}"
|
|
226
|
+
+ ("" if len(outs) == len(reference) else f", ответов {len(outs)} из {len(reference)}"))
|
|
227
|
+
else:
|
|
228
|
+
checked += len(outs)
|
|
229
|
+
if lang == "py":
|
|
230
|
+
problems += canary_py(gate, consumer, bpath, reference, name)
|
|
231
|
+
if sample is None:
|
|
232
|
+
problems.append(f"пакет {name}/{lang}: в README нет исполняемого примера")
|
|
233
|
+
elif sample.returncode:
|
|
234
|
+
problems.append(f"пакет {name}/{lang}: пример README упал — {gate.crash_line(sample.stderr or sample.stdout)}")
|
|
235
|
+
return problems, checked
|
|
236
|
+
|
|
237
|
+
|
|
238
|
+
def main(argv: list[str] | None = None) -> int:
|
|
239
|
+
ap = argparse.ArgumentParser(description=__doc__, allow_abbrev=False)
|
|
240
|
+
ap.add_argument("--fixtures", required=True, type=Path)
|
|
241
|
+
ap.add_argument("--core-kit", required=True, type=Path)
|
|
242
|
+
ap.add_argument("--targets", default="py,ts,go", help="цели пакетов через запятую")
|
|
243
|
+
ap.add_argument("--require", default="", help="цели, отсутствие тулчейна которых — отказ")
|
|
244
|
+
ap.add_argument("--keep", type=Path, help="сохранить пакеты и потребителей")
|
|
245
|
+
args = ap.parse_args(argv)
|
|
246
|
+
import core_support
|
|
247
|
+
core_support.configure_core_kit(args.core_kit)
|
|
248
|
+
core_support.validate_core_kit(args.core_kit)
|
|
249
|
+
import check
|
|
250
|
+
gate = check.load_harness(args.fixtures.resolve())
|
|
251
|
+
wanted = [x for x in args.targets.split(",") if x]
|
|
252
|
+
emit = {lang: gate.D.emitter(lang) for lang in {"py", *wanted}}
|
|
253
|
+
tools = {lang: emit[lang].available() for lang in wanted}
|
|
254
|
+
if "ts" in tools and tools["ts"][0] and not shutil.which("npm"):
|
|
255
|
+
tools["ts"] = (False, "нет npm для npm pack")
|
|
256
|
+
active = [lang for lang in wanted if tools[lang][0]]
|
|
257
|
+
for lang in wanted:
|
|
258
|
+
if not tools[lang][0]:
|
|
259
|
+
print(f"check_packages: цель {lang} ПРОПУЩЕНА — {tools[lang][1]}")
|
|
260
|
+
missing = [x for x in args.require.split(",") if x and x not in active]
|
|
261
|
+
if missing:
|
|
262
|
+
print(f"check_packages: обязательные цели без тулчейна: {', '.join(missing)}")
|
|
263
|
+
return 1
|
|
264
|
+
if not active:
|
|
265
|
+
print("check_packages: нет ни одной цели с тулчейном")
|
|
266
|
+
return 1
|
|
267
|
+
if args.keep:
|
|
268
|
+
work = args.keep.resolve()
|
|
269
|
+
work.mkdir(parents=True, exist_ok=True)
|
|
270
|
+
problems, checked = package_check(gate, emit, active, work)
|
|
271
|
+
else:
|
|
272
|
+
with tempfile.TemporaryDirectory(prefix="codegen-packages-") as tmp:
|
|
273
|
+
problems, checked = package_check(gate, emit, active, Path(tmp))
|
|
274
|
+
for line in problems:
|
|
275
|
+
print("check_packages: " + line)
|
|
276
|
+
print(f"check_packages: цели {', '.join(active)}; сверено заданий {checked}; проблем {len(problems)}")
|
|
277
|
+
return 1 if problems or not checked else 0
|
|
278
|
+
|
|
279
|
+
|
|
280
|
+
if __name__ == "__main__":
|
|
281
|
+
raise SystemExit(main())
|
|
@@ -0,0 +1,293 @@
|
|
|
1
|
+
"""Verify the published E-0195/E-0202 semantic-probe delivery.
|
|
2
|
+
|
|
3
|
+
The delivery is deliberately self-contained: this checker only needs the
|
|
4
|
+
published CLIR documents and a validated codegen core kit. It does not import
|
|
5
|
+
lawref or a monorepo CI harness.
|
|
6
|
+
"""
|
|
7
|
+
from __future__ import annotations
|
|
8
|
+
|
|
9
|
+
import argparse
|
|
10
|
+
import os
|
|
11
|
+
import hashlib
|
|
12
|
+
import json
|
|
13
|
+
from pathlib import Path
|
|
14
|
+
import subprocess
|
|
15
|
+
import sys
|
|
16
|
+
import tempfile
|
|
17
|
+
|
|
18
|
+
HERE = Path(__file__).resolve().parent
|
|
19
|
+
if str(HERE) not in sys.path:
|
|
20
|
+
sys.path.insert(0, str(HERE))
|
|
21
|
+
|
|
22
|
+
import core_support # noqa: E402
|
|
23
|
+
import deliver as D # noqa: E402
|
|
24
|
+
import plan as P # noqa: E402
|
|
25
|
+
import target_runner as T # noqa: E402
|
|
26
|
+
from check_active_canaries import CanaryError # noqa: E402
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
FORMAT = "law.codegen-semantic-probes/0.1"
|
|
30
|
+
SEMANTIC = "0.2.11"
|
|
31
|
+
FILES = frozenset({
|
|
32
|
+
"ir.json", "case.json", "query.json", "mutation.json", "oracle.json",
|
|
33
|
+
"baseline.json", "mutated.json",
|
|
34
|
+
})
|
|
35
|
+
E0195 = {
|
|
36
|
+
"empty-mismatch": ("absent", "urn:law:std#Integer", "urn:law:std#Money", True),
|
|
37
|
+
"empty-match": ("absent", "urn:law:std#Money", "urn:law:std#Integer", True),
|
|
38
|
+
"nonempty-mismatch": ("order_amount", "urn:law:std#Integer", "urn:law:std#Money", False),
|
|
39
|
+
}
|
|
40
|
+
E0202 = {
|
|
41
|
+
"i64-max": 2 ** 63 - 1,
|
|
42
|
+
"i64-min": -(2 ** 63),
|
|
43
|
+
"i64-over": 2 ** 63,
|
|
44
|
+
"i64-under": -(2 ** 63) - 1,
|
|
45
|
+
"i64-float": 1.5,
|
|
46
|
+
}
|
|
47
|
+
REQUIRED = frozenset(E0195) | frozenset(E0202)
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
def _sha(data: bytes) -> str:
|
|
52
|
+
return "sha256:" + hashlib.sha256(data).hexdigest()
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
def _read_json(path: Path) -> dict:
|
|
57
|
+
try:
|
|
58
|
+
value = json.loads(path.read_bytes())
|
|
59
|
+
except (OSError, UnicodeDecodeError, json.JSONDecodeError) as exc:
|
|
60
|
+
raise CanaryError(f"invalid JSON: {path.name}") from exc
|
|
61
|
+
if not isinstance(value, dict):
|
|
62
|
+
raise CanaryError(f"JSON object required: {path.name}")
|
|
63
|
+
return value
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
def _probe_id(value: object) -> str:
|
|
68
|
+
if not isinstance(value, str) or value not in REQUIRED:
|
|
69
|
+
raise CanaryError("unknown semantic probe id")
|
|
70
|
+
return value
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
|
|
74
|
+
def _type_name(query: dict) -> str | None:
|
|
75
|
+
try:
|
|
76
|
+
value = query["term"]["options"]["empty"]
|
|
77
|
+
return value["type"]["name"]
|
|
78
|
+
except (KeyError, TypeError):
|
|
79
|
+
return None
|
|
80
|
+
|
|
81
|
+
|
|
82
|
+
|
|
83
|
+
def _predicate(query: dict) -> str | None:
|
|
84
|
+
try:
|
|
85
|
+
return query["term"]["input"]["generator"]["formula"]["predicate"]
|
|
86
|
+
except (KeyError, TypeError):
|
|
87
|
+
return None
|
|
88
|
+
|
|
89
|
+
|
|
90
|
+
|
|
91
|
+
def _validate_policy(ident: str, row: dict, base: Path) -> None:
|
|
92
|
+
"""Check the named witness, not only its hash inventory."""
|
|
93
|
+
migration = row.get("migration")
|
|
94
|
+
if (not isinstance(migration, dict) or set(migration) != {"field", "from", "to", "rationale"}
|
|
95
|
+
or any(not isinstance(migration[key], str) or not migration[key].strip() for key in migration)):
|
|
96
|
+
raise CanaryError(f"{ident}: migration provenance is missing")
|
|
97
|
+
query = _read_json(base / "query.json")
|
|
98
|
+
mutation = _read_json(base / "mutation.json")
|
|
99
|
+
if ident in E0195:
|
|
100
|
+
predicate, policy, mutation_policy, changes = E0195[ident]
|
|
101
|
+
expected_predicate = "urn:law:vectors:err-e0195#" + predicate
|
|
102
|
+
if (_predicate(query) != expected_predicate or _predicate(mutation) != expected_predicate
|
|
103
|
+
or _type_name(query) != policy or _type_name(mutation) != mutation_policy):
|
|
104
|
+
raise CanaryError(f"{ident}: invalid E0195 empty-policy witness")
|
|
105
|
+
if row.get("kind") != "E0195" or row.get("mutationChanges") is not changes:
|
|
106
|
+
raise CanaryError(f"{ident}: invalid E0195 mutation policy")
|
|
107
|
+
return
|
|
108
|
+
value = E0202[ident]
|
|
109
|
+
try:
|
|
110
|
+
query_value = query["term"]["value"]
|
|
111
|
+
mutation_value = mutation["term"]["value"]
|
|
112
|
+
except (KeyError, TypeError) as exc:
|
|
113
|
+
raise CanaryError(f"{ident}: invalid E0202 term witness") from exc
|
|
114
|
+
# Equality alone would accept a float representation of a 64-bit integer.
|
|
115
|
+
if (type(query_value) is not type(value) or query_value != value
|
|
116
|
+
or type(mutation_value) is not type(value) or mutation_value != value
|
|
117
|
+
or mutation != query):
|
|
118
|
+
raise CanaryError(f"{ident}: invalid E0202 canonical number witness")
|
|
119
|
+
if row.get("kind") != "E0202" or row.get("mutationChanges") is not False:
|
|
120
|
+
raise CanaryError(f"{ident}: invalid E0202 mutation policy")
|
|
121
|
+
|
|
122
|
+
|
|
123
|
+
|
|
124
|
+
def load(bundle: Path) -> tuple[Path, list[dict]]:
|
|
125
|
+
"""Validate a complete, regular-file-only semantic-probe bundle."""
|
|
126
|
+
supplied = Path(bundle)
|
|
127
|
+
if supplied.is_symlink() or not supplied.is_dir():
|
|
128
|
+
raise CanaryError("semantic-probe bundle is not a directory")
|
|
129
|
+
bundle = supplied.resolve()
|
|
130
|
+
if any(path.is_symlink() for path in bundle.rglob("*")):
|
|
131
|
+
raise CanaryError("semantic-probe bundle contains a symlink")
|
|
132
|
+
manifest = _read_json(bundle / "manifest.json")
|
|
133
|
+
if manifest.get("format") != FORMAT or manifest.get("semanticVersion") != SEMANTIC:
|
|
134
|
+
raise CanaryError("unsupported semantic-probe bundle format or semantic version")
|
|
135
|
+
rows = manifest.get("probes")
|
|
136
|
+
if not isinstance(rows, list) or len(rows) != len(REQUIRED):
|
|
137
|
+
raise CanaryError("semantic-probe inventory is incomplete")
|
|
138
|
+
|
|
139
|
+
known = {"manifest.json"}
|
|
140
|
+
seen: set[str] = set()
|
|
141
|
+
checked: list[dict] = []
|
|
142
|
+
for row in rows:
|
|
143
|
+
if not isinstance(row, dict):
|
|
144
|
+
raise CanaryError("invalid semantic-probe row")
|
|
145
|
+
ident = _probe_id(row.get("id"))
|
|
146
|
+
if ident in seen:
|
|
147
|
+
raise CanaryError("duplicate semantic-probe id")
|
|
148
|
+
seen.add(ident)
|
|
149
|
+
files = row.get("files")
|
|
150
|
+
if not isinstance(files, dict) or set(files) != FILES:
|
|
151
|
+
raise CanaryError(f"{ident}: invalid probe file inventory")
|
|
152
|
+
prefix = Path("probes") / ident
|
|
153
|
+
for name, digest in files.items():
|
|
154
|
+
if not isinstance(digest, str) or not digest.startswith("sha256:"):
|
|
155
|
+
raise CanaryError(f"{ident}: invalid probe hash")
|
|
156
|
+
path = bundle / prefix / name
|
|
157
|
+
if path.is_symlink() or not path.is_file() or digest != _sha(path.read_bytes()):
|
|
158
|
+
raise CanaryError(f"{ident}: probe file hash mismatch")
|
|
159
|
+
known.add((prefix / name).as_posix())
|
|
160
|
+
_validate_policy(ident, row, bundle / prefix)
|
|
161
|
+
checked.append(row)
|
|
162
|
+
if seen != REQUIRED:
|
|
163
|
+
raise CanaryError("semantic-probe inventory is incomplete")
|
|
164
|
+
actual = {path.relative_to(bundle).as_posix() for path in bundle.rglob("*") if path.is_file()}
|
|
165
|
+
if actual != known:
|
|
166
|
+
raise CanaryError("semantic-probe bundle inventory differs from manifest")
|
|
167
|
+
return bundle, sorted(checked, key=lambda row: row["id"])
|
|
168
|
+
|
|
169
|
+
|
|
170
|
+
|
|
171
|
+
def _wire(document: dict) -> dict:
|
|
172
|
+
"""The published normative wire shape, excluding runner-local queryId."""
|
|
173
|
+
if "refused" in document:
|
|
174
|
+
refusal = document["refused"]
|
|
175
|
+
if not isinstance(refusal, str):
|
|
176
|
+
raise CanaryError("generated Python runner returned invalid refusal")
|
|
177
|
+
return {"refused": refusal}
|
|
178
|
+
results = document.get("results") or [{}]
|
|
179
|
+
if not isinstance(results, list) or not results or not isinstance(results[0], dict):
|
|
180
|
+
raise CanaryError("generated Python runner returned invalid results")
|
|
181
|
+
issues = document.get("issues", [])
|
|
182
|
+
if not isinstance(issues, list):
|
|
183
|
+
raise CanaryError("generated Python runner returned invalid issues")
|
|
184
|
+
result = results[0]
|
|
185
|
+
wire: dict = {}
|
|
186
|
+
for key in ("resultKind", "truthStatus", "evaluationStatus", "value"):
|
|
187
|
+
value = result.get(key, document.get(key))
|
|
188
|
+
if value is not None:
|
|
189
|
+
wire[key] = value
|
|
190
|
+
wire["issues"] = [item.get("code") if isinstance(item, dict) else item for item in issues]
|
|
191
|
+
return wire
|
|
192
|
+
|
|
193
|
+
|
|
194
|
+
|
|
195
|
+
def _response(kind: str, payload: object, target: str) -> dict:
|
|
196
|
+
"""Validate the common runner exit protocol and retain the normative wire."""
|
|
197
|
+
if kind == "crash":
|
|
198
|
+
raise CanaryError(f"generated {target} runner crashed: {payload}")
|
|
199
|
+
if kind == "refused":
|
|
200
|
+
if not isinstance(payload, str):
|
|
201
|
+
raise CanaryError(f"generated {target} runner returned invalid refusal")
|
|
202
|
+
return {"refused": payload}
|
|
203
|
+
if kind != "answer" or not isinstance(payload, dict):
|
|
204
|
+
raise CanaryError(f"generated {target} runner returned invalid response")
|
|
205
|
+
if "refused" in payload:
|
|
206
|
+
raise CanaryError(f"generated {target} runner returned refusal with success status")
|
|
207
|
+
return _wire(payload)
|
|
208
|
+
|
|
209
|
+
|
|
210
|
+
def _targets(value: tuple[str, ...]) -> tuple[str, ...]:
|
|
211
|
+
known = set(D.targets(protocol="runner"))
|
|
212
|
+
if not value or len(set(value)) != len(value) or any(target not in known for target in value):
|
|
213
|
+
raise CanaryError("targets must be unique known runner targets")
|
|
214
|
+
unavailable = []
|
|
215
|
+
for target in value:
|
|
216
|
+
available, reason = D.emitter(target).available()
|
|
217
|
+
if not available:
|
|
218
|
+
unavailable.append(f"{target} ({reason})")
|
|
219
|
+
if unavailable:
|
|
220
|
+
raise CanaryError("required semantic-probe targets unavailable: " + ", ".join(unavailable))
|
|
221
|
+
return value
|
|
222
|
+
|
|
223
|
+
|
|
224
|
+
def verify(bundle: Path, core_kit: Path, *, targets: tuple[str, ...] = ("py",)) -> list[str]:
|
|
225
|
+
previous = core_support._configured_kit
|
|
226
|
+
core_support.configure_core_kit(core_kit)
|
|
227
|
+
try:
|
|
228
|
+
core_support.validate_core_kit(core_kit)
|
|
229
|
+
bundle, rows = load(bundle)
|
|
230
|
+
targets = _targets(targets)
|
|
231
|
+
emit = {target: D.emitter(target) for target in targets}
|
|
232
|
+
prepared = []
|
|
233
|
+
for row in rows:
|
|
234
|
+
ident = row["id"]
|
|
235
|
+
base = bundle / "probes" / ident
|
|
236
|
+
prepared.append((row, base, P.build_plan(_read_json(base / "ir.json"), resources={})))
|
|
237
|
+
with tempfile.TemporaryDirectory(prefix="codegen-semantic-probes-") as temporary:
|
|
238
|
+
work = Path(temporary)
|
|
239
|
+
environment = dict(os.environ, GOCACHE=str(work / "go-cache"),
|
|
240
|
+
CLANG_MODULE_CACHE_PATH=str(work / "clang-cache"),
|
|
241
|
+
SWIFT_MODULECACHE_PATH=str(work / "swift-cache"))
|
|
242
|
+
projects = {target: [] for target in targets}
|
|
243
|
+
for row, _base, plan in prepared:
|
|
244
|
+
for target in targets:
|
|
245
|
+
project = work / row["id"] / target
|
|
246
|
+
D.emit_project(plan, target, project)
|
|
247
|
+
projects[target].append(project)
|
|
248
|
+
failures = T.build_projects(emit, projects, work, environment)
|
|
249
|
+
if failures:
|
|
250
|
+
raise CanaryError("semantic-probe build failed: " + "\n".join(failures))
|
|
251
|
+
messages: list[str] = []
|
|
252
|
+
for row, base, _plan in prepared:
|
|
253
|
+
ident = row["id"]
|
|
254
|
+
oracle = _read_json(base / "oracle.json")
|
|
255
|
+
frozen_baseline = _read_json(base / "baseline.json")
|
|
256
|
+
frozen_mutated = _read_json(base / "mutated.json")
|
|
257
|
+
for target in targets:
|
|
258
|
+
descriptor = emit[target]
|
|
259
|
+
project = work / ident / target
|
|
260
|
+
baseline = _response(*T.run_target(descriptor, project, "runner",
|
|
261
|
+
[str(base / "case.json"), str(base / "query.json")], environment), target)
|
|
262
|
+
mutated = _response(*T.run_target(descriptor, project, "runner",
|
|
263
|
+
[str(base / "case.json"), str(base / "mutation.json")], environment), target)
|
|
264
|
+
if baseline != oracle or baseline != frozen_baseline:
|
|
265
|
+
raise CanaryError(f"{ident}/{target}: baseline differs from frozen evidence")
|
|
266
|
+
if mutated != frozen_mutated:
|
|
267
|
+
raise CanaryError(f"{ident}/{target}: mutation differs from frozen evidence")
|
|
268
|
+
if row["mutationChanges"] and mutated == baseline:
|
|
269
|
+
raise CanaryError(f"{ident}/{target}: mutation witness did not change the result")
|
|
270
|
+
messages.append(f"PASS {ident} ({','.join(targets)})")
|
|
271
|
+
return messages
|
|
272
|
+
finally:
|
|
273
|
+
core_support.configure_core_kit(previous)
|
|
274
|
+
|
|
275
|
+
|
|
276
|
+
def main(argv: list[str] | None = None) -> int:
|
|
277
|
+
parser = argparse.ArgumentParser(description=__doc__)
|
|
278
|
+
parser.add_argument("--bundle", type=Path, required=True)
|
|
279
|
+
parser.add_argument("--core-kit", type=Path, required=True)
|
|
280
|
+
parser.add_argument("--targets", default="py", help="required runner targets, comma-separated")
|
|
281
|
+
args = parser.parse_args(argv)
|
|
282
|
+
try:
|
|
283
|
+
targets = tuple(target for target in args.targets.split(",") if target)
|
|
284
|
+
for message in verify(args.bundle, args.core_kit, targets=targets):
|
|
285
|
+
print(message)
|
|
286
|
+
except (CanaryError, core_support.CoreKitError, P.Refused) as exc:
|
|
287
|
+
print(f"FAIL: {exc}", file=sys.stderr)
|
|
288
|
+
return 1
|
|
289
|
+
return 0
|
|
290
|
+
|
|
291
|
+
|
|
292
|
+
if __name__ == "__main__":
|
|
293
|
+
raise SystemExit(main())
|