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