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,1248 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""Ворота кодогенерации (DECISION-0188).
|
|
3
|
+
|
|
4
|
+
План строится из CLIR один раз и печатается в целевые языки. Каждый бэкенд
|
|
5
|
+
исполняет L0-векторы и обязан совпасть С ГОЛДЕНОМ и С ОСТАЛЬНЫМИ бэкендами.
|
|
6
|
+
|
|
7
|
+
Три исхода исполнения различаются протоколом выхода раннера, а не одним
|
|
8
|
+
ненулевым кодом: ответ (код 0, JSON), названный отказ среза (код 3, JSON
|
|
9
|
+
`{"refused": текст}`) и крах (любой другой код). Крах валит ворота. Отказ
|
|
10
|
+
обязан быть единогласным и дословно одинаковым у всех целей: отказ одной
|
|
11
|
+
цели при ответе другой — расхождение, а не «отказ рантайма». До этого
|
|
12
|
+
протокола ворота прерывали вектор на первом ненулевом коде и не видели, что
|
|
13
|
+
на `ERR-E0046-CASE-ARG-TYPE` две цели отвечают `TRUE_ONLY` против голдена.
|
|
14
|
+
|
|
15
|
+
Поражаемый слой (DECISION-0237): векторы `layer: L1` идут тем же путём, но
|
|
16
|
+
со своим полом храповика; голден без результата у L1-вектора обязан быть
|
|
17
|
+
структурным отказом плана с той же причиной. Фикстуры `l1_fixtures.py`
|
|
18
|
+
держат решающую норму канарейками и порядок — перестановкой.
|
|
19
|
+
|
|
20
|
+
Исход каждого L0- и L1-вектора закреплён в манифесте `verify/ci/data/backends-vectors.json`:
|
|
21
|
+
совпадение, отказ плана, вид запроса, голден без результата, отказ рантайма.
|
|
22
|
+
Закреплённый исход обязан повториться; закреплённый вектор обязан
|
|
23
|
+
существовать; незакреплённое совпадение — ошибка регистрации
|
|
24
|
+
(`--write-manifest`). Одно число исполненных векторов позволило бы потерять
|
|
25
|
+
старый случай и компенсировать его новым, поэтому пол `MIN_CHECKED` остался
|
|
26
|
+
лишь второй защитой.
|
|
27
|
+
|
|
28
|
+
Цели — каталоги `engines/backends/targets/<цель>/` с дескриптором в
|
|
29
|
+
`emit.py` (`available`, `MODEL`, `layout`, `build`, `run`). По умолчанию
|
|
30
|
+
исполняются `ts,py,go`; `--targets` выбирает другие или `all`. Цель Python не
|
|
31
|
+
требует тулчейна и проверяется ВСЕГДА: без неё ворота сообщали бы «зелено»,
|
|
32
|
+
не исполнив ни одного бэкенда. Отсутствие тулчейна НАЗЫВАЕТСЯ в отчёте;
|
|
33
|
+
`--require` делает его отказом ворот.
|
|
34
|
+
"""
|
|
35
|
+
from __future__ import annotations
|
|
36
|
+
|
|
37
|
+
import argparse
|
|
38
|
+
import collections
|
|
39
|
+
import copy
|
|
40
|
+
import json
|
|
41
|
+
import shutil
|
|
42
|
+
import subprocess
|
|
43
|
+
import sys
|
|
44
|
+
import tempfile
|
|
45
|
+
from pathlib import Path
|
|
46
|
+
|
|
47
|
+
ROOT = Path(__file__).resolve().parents[4]
|
|
48
|
+
BACKENDS = ROOT / "engines" / "backends"
|
|
49
|
+
VECTORS = ROOT / "verify" / "vectors"
|
|
50
|
+
MANIFEST = ROOT / "verify" / "ci" / "data" / "backends-vectors.json"
|
|
51
|
+
MANIFEST_FORMAT = "law.backends-vectors/1"
|
|
52
|
+
CERT = BACKENDS / "runtime-cert.json"
|
|
53
|
+
REFUSED_EXIT = 3
|
|
54
|
+
DEFAULT_TARGETS = "ts,py,go"
|
|
55
|
+
|
|
56
|
+
sys.path.insert(0, str(BACKENDS))
|
|
57
|
+
sys.path.insert(0, str(Path(__file__).resolve().parent))
|
|
58
|
+
import plan as P # noqa: E402
|
|
59
|
+
import deliver as D # noqa: E402
|
|
60
|
+
import l1_fixtures as FX # noqa: E402
|
|
61
|
+
|
|
62
|
+
# Пол храповика: столько векторов обязаны исполниться и совпасть. Вторая защита
|
|
63
|
+
# после манифеста: манифест ловит подмену случая, пол — массовую потерю.
|
|
64
|
+
# 16.09.2026 (DECISION-0266–0268, перепин манифеста после 0252/0256/0259): 82 → 225.
|
|
65
|
+
# 16.09.2026 (DECISION-0270, условный терм и вычисляемый аргумент тела): 225 → 234.
|
|
66
|
+
# 16.09.2026 (DECISION-0273, функции §46, параметры A3, let/match): 234 → 251.
|
|
67
|
+
# 16.09.2026 (DECISION-0274, канал суждения §47.3): 251 → 255.
|
|
68
|
+
MIN_CHECKED = 255
|
|
69
|
+
# Пол поражаемого слоя (DECISION-0237): совпадения L1-векторов плюс голдены
|
|
70
|
+
# без результата, сверенные как структурный отказ. Отдельный от L0: рост
|
|
71
|
+
# одного слоя не должен прятать потерю в другом.
|
|
72
|
+
# 16.09.2026 (DECISION-0269, датированные приоритеты): 37 → 46.
|
|
73
|
+
# 16.09.2026 (DECISION-0270): 46 → 59.
|
|
74
|
+
# 16.09.2026 (DECISION-0274, канал суждения §47.3): 59 → 60.
|
|
75
|
+
MIN_CHECKED_L1 = 60
|
|
76
|
+
# Пол нормативных позиций (DECISION-0242): совпадения векторов слоёв L2 и L3,
|
|
77
|
+
# исполняющих позиции; отдельный от L0 и L1.
|
|
78
|
+
# 16.09.2026 (DECISION-0274, канал суждения §47.3: LAY-L2-JUDGMENT-*, E-0100/E-0120/E-0179, позиции): 35 → 71.
|
|
79
|
+
MIN_CHECKED_L2 = 71
|
|
80
|
+
|
|
81
|
+
# Голден без результата у L1-вектора — фатальная диагностика оракула до
|
|
82
|
+
# вывода. Срез обязан отказать планом по той же причине; иной отказ или
|
|
83
|
+
# построенный план — ошибка ворот, а не «голден без результата».
|
|
84
|
+
STRUCTURAL = {
|
|
85
|
+
"NON_EXECUTABLE_UNSTRATIFIED": P.UNSTRATIFIED,
|
|
86
|
+
"NON_EXECUTABLE_STRATUM_BARRIER": "дефитер без обычного кандидата той же полярности "
|
|
87
|
+
"(§110 п. 5, NON_EXECUTABLE_STRATUM_BARRIER)",
|
|
88
|
+
"LDC-E4101": "несвязанная переменная в голове",
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
# Коды issues, которые срез порождает сам: по ним ответ сверяется с голденом,
|
|
92
|
+
# а не только между целями. Иначе KEY_CONFLICT, не влияющий на статус, был бы
|
|
93
|
+
# невидим воротам: модуль, молча не считающий ключи §45.1, совпадал бы с
|
|
94
|
+
# голденом по статусу и с соседями по пустому списку. Остальные диагностики
|
|
95
|
+
# голдена срез не порождает и не сверяет.
|
|
96
|
+
BACKEND_ISSUES = {"TYPE_ERROR", "KEY_CONFLICT", "DATE_INSTANT_MISMATCH", "UNSUPPORTED_IN_SLICE",
|
|
97
|
+
# DECISION-0274: ответ судьи §47.3.1 не от того органа / не по форме.
|
|
98
|
+
"JUDGMENT_AUTHORITY_MISMATCH", "JUDGMENT_PAYLOAD_MISMATCH",
|
|
99
|
+
# DECISION-0252: ошибки значения §58 в правилах плана 0.12 и в вопросе `term`.
|
|
100
|
+
"DIVISION_BY_ZERO", "INEXACT_DIVISION", "CURRENCY_MISMATCH",
|
|
101
|
+
# DECISION-0242 K1: эффекты полномочий §127–§128.
|
|
102
|
+
"POWER_INVALID_EXERCISE", "EFFECT_BLOCKED_BY_IMMUNITY", "UNRESOLVED_NORMATIVE_CONFLICT",
|
|
103
|
+
# DECISION-0239 G3: ошибка значения агрегата в правиле — issue и пропуск.
|
|
104
|
+
"EMPTY_AGGREGATE", "MONOTONE_GUARD_NEGATIVE_ADDEND",
|
|
105
|
+
# DECISION-0245: ошибки календаря §85/§86 в термах правил.
|
|
106
|
+
"MISSING_POLICY", "DEADLINE_POLICY_INVALID", "MISSING_CALENDAR", "CALENDAR_OUT_OF_RANGE",
|
|
107
|
+
"CALENDAR_DATASET_INVALID", "CALENDAR_DATASET_HASH_MISMATCH"}
|
|
108
|
+
|
|
109
|
+
# Пакеты корпуса с поверхностью API (DECISION-0234): снимки CLIR, у которых
|
|
110
|
+
# план строит вопросы. Проверка API — раннер против API одной цели и цели
|
|
111
|
+
# между собой на синтетических делах; векторы `pub` не несут, поэтому
|
|
112
|
+
# здесь читается корпусный снимок, а не вектор.
|
|
113
|
+
API_PACKAGES = ("codegen-api-strict", "codegen-api-aggregate")
|
|
114
|
+
# Пакеты с поверхностью API над L1-планом (DECISION-0237 этап 5).
|
|
115
|
+
API_PACKAGES_L1 = ("codegen-api-defeasible",)
|
|
116
|
+
CLIR = ROOT / "corpus" / "clir"
|
|
117
|
+
|
|
118
|
+
|
|
119
|
+
def version_probe(emit: dict) -> list[str]:
|
|
120
|
+
"""Отрицательная проба: план чужой версии обязан быть отвергнут каждым
|
|
121
|
+
эмиттером. Без пробы проверка версии — договорённость, а не ворота."""
|
|
122
|
+
empty = {"package": {}, "tables": [], "rules": [], "facts": [], "consts": {}, "bases": {}}
|
|
123
|
+
failures = []
|
|
124
|
+
for lang, em in emit.items():
|
|
125
|
+
try:
|
|
126
|
+
em.emit(dict(empty, planVersion="law-backend-plan/999"))
|
|
127
|
+
except ValueError:
|
|
128
|
+
continue
|
|
129
|
+
failures.append(f"эмиттер {lang} принял план версии law-backend-plan/999")
|
|
130
|
+
for em in emit.values():
|
|
131
|
+
em.emit(dict(empty, planVersion=P.PLAN_VERSION))
|
|
132
|
+
# DECISION-0237: L1-план уходит только цели, объявившей его возможности;
|
|
133
|
+
# цель без объявления обязана отвергнуть и сам план, и поставку.
|
|
134
|
+
for version, cap in ((P.PLAN_VERSION_L1, "defeasible"), (P.PLAN_VERSION_09, "closure"),
|
|
135
|
+
(P.PLAN_VERSION_10, "norm"), (P.PLAN_VERSION_11, "calendar:business")):
|
|
136
|
+
probe = dict(empty, planVersion=version, capabilities=[cap])
|
|
137
|
+
for lang, em in emit.items():
|
|
138
|
+
if D.missing_capabilities(probe, em):
|
|
139
|
+
try:
|
|
140
|
+
em.emit(probe)
|
|
141
|
+
failures.append(f"эмиттер {lang} без возможности {cap} принял план {version}")
|
|
142
|
+
except ValueError:
|
|
143
|
+
pass
|
|
144
|
+
return failures
|
|
145
|
+
|
|
146
|
+
|
|
147
|
+
def candidates():
|
|
148
|
+
"""L0- и L1-векторы с промежуточным представлением, делом, запросом и голденом."""
|
|
149
|
+
for v in sorted(VECTORS.rglob("vector.json")):
|
|
150
|
+
meta = json.loads(v.read_text(encoding="utf-8"))
|
|
151
|
+
if meta.get("layer") not in LAYERS:
|
|
152
|
+
continue
|
|
153
|
+
d = v.parent
|
|
154
|
+
if not all((d / f).exists() for f in ("ir.json", "case.json", "query.json", "expected.json")):
|
|
155
|
+
continue
|
|
156
|
+
yield d, meta["layer"]
|
|
157
|
+
|
|
158
|
+
|
|
159
|
+
def load(d: Path, name: str):
|
|
160
|
+
return json.loads((d / name).read_text(encoding="utf-8"))
|
|
161
|
+
|
|
162
|
+
|
|
163
|
+
def golden_code(doc: dict) -> str | None:
|
|
164
|
+
"""Код фатальной диагностики голдена без результата: `error.code` либо
|
|
165
|
+
первый fatal issue."""
|
|
166
|
+
if isinstance(doc.get("error"), dict):
|
|
167
|
+
return doc["error"].get("code")
|
|
168
|
+
return next((i.get("code") for i in doc.get("issues") or [] if i.get("severity") == "fatal"), None)
|
|
169
|
+
|
|
170
|
+
|
|
171
|
+
def classify(d: Path, kinds: set, emit: dict, layer: str = "L0"):
|
|
172
|
+
"""Исход до исполнения: отказ плана, вид запроса, голден. `None` — исполнять.
|
|
173
|
+
|
|
174
|
+
`kinds` — виды вопросов, которые исполняют ВСЕ активные цели (пересечение
|
|
175
|
+
`QUERY_KINDS` дескрипторов): вектор другого вида не исполняется, иначе
|
|
176
|
+
цель без поддержки отказала бы при ответе соседей, и это читалось бы
|
|
177
|
+
расхождением, а не границей среза. Так же с возможностями плана
|
|
178
|
+
(DECISION-0237): их обязаны объявить ВСЕ активные цели.
|
|
179
|
+
|
|
180
|
+
Возвращает (исход, план, ошибки ворот)."""
|
|
181
|
+
doc = load(d, "expected.json")
|
|
182
|
+
n = len(doc.get("results") or [])
|
|
183
|
+
code = golden_code(doc) if n == 0 else None
|
|
184
|
+
try:
|
|
185
|
+
ir = load(d, "ir.json")
|
|
186
|
+
# Мир вектора с импортом — его `producer.ir.json` (DECISION-0236);
|
|
187
|
+
# снимки корпуса векторам не нужны.
|
|
188
|
+
extra = [load(d, "producer.ir.json")] if (d / "producer.ir.json").exists() else []
|
|
189
|
+
world = P.resolve_world(ir, {}, extra) if ir.get("imports") else []
|
|
190
|
+
# Отрицательный факт дела требует модели с опорами обеих полярностей.
|
|
191
|
+
negative = any((a.get("literal") or {}).get("polarity") == "negative"
|
|
192
|
+
for a in load(d, "case.json").get("assertions") or [])
|
|
193
|
+
# Байты снимка §85 у вектора — `calendar.resource.json` рядом (DECISION-0245).
|
|
194
|
+
resource = d / "calendar.resource.json"
|
|
195
|
+
resources = {n.get("resourceUri"): resource.read_bytes() for n in ir["nodes"]
|
|
196
|
+
if n.get("kind") == "calendar_snapshot"} if resource.exists() else {}
|
|
197
|
+
pl = P.build_plan(ir, world, {"negation"} if negative else None, resources)
|
|
198
|
+
except P.Refused as exc:
|
|
199
|
+
head = str(exc).split(":")[0]
|
|
200
|
+
if layer == "L1" and n == 0:
|
|
201
|
+
if STRUCTURAL.get(code) == head:
|
|
202
|
+
return f"structural: {code}", None, []
|
|
203
|
+
# Иная СТРУКТУРНАЯ причина — подмена диагностики, ошибка ворот.
|
|
204
|
+
# Отказ по форме вне среза (терм, функция, тип) закреплён
|
|
205
|
+
# манифестом как отказ плана: срез не отвечает, а до проверки
|
|
206
|
+
# страт не доходит — форма, замыкающая цикл, сама вне среза
|
|
207
|
+
# (ERR-E0176-DEFEASIBLE-SELF-CYCLE: условный терм головы §56).
|
|
208
|
+
if head in STRUCTURAL.values():
|
|
209
|
+
return f"plan: {head}", None, [f"голден — структурный отказ {code}, план отказал иной причиной: {exc}"]
|
|
210
|
+
return f"plan: {head}", None, []
|
|
211
|
+
return f"plan: {head}", None, []
|
|
212
|
+
missing = sorted({c for em in emit.values() for c in D.missing_capabilities(pl, em)})
|
|
213
|
+
if missing:
|
|
214
|
+
return f"capability: {', '.join(missing)}", None, []
|
|
215
|
+
kind = load(d, "query.json").get("kind")
|
|
216
|
+
if kind not in kinds:
|
|
217
|
+
return f"query: {kind}", None, []
|
|
218
|
+
if n != 1:
|
|
219
|
+
errors = [f"голден — структурный отказ {code}, а план построен"] if layer == "L1" and n == 0 else []
|
|
220
|
+
return f"golden: результатов {n}", None, errors
|
|
221
|
+
return None, pl, []
|
|
222
|
+
|
|
223
|
+
|
|
224
|
+
def crash_line(stderr: str) -> str:
|
|
225
|
+
"""Строка stderr, называющая причину: у Go это первая `panic:`, у Node —
|
|
226
|
+
строка с `Error:`, у Python — последняя строка трассировки."""
|
|
227
|
+
lines = [line.strip() for line in stderr.splitlines() if line.strip()]
|
|
228
|
+
if not lines:
|
|
229
|
+
return "(stderr пуст)"
|
|
230
|
+
for line in lines:
|
|
231
|
+
if line.startswith("panic:") or "Error:" in line or "Error [" in line or "error:" in line:
|
|
232
|
+
return line[:200]
|
|
233
|
+
return lines[-1][:200]
|
|
234
|
+
|
|
235
|
+
|
|
236
|
+
def sh(cmd, cwd=None) -> subprocess.CompletedProcess:
|
|
237
|
+
return subprocess.run(cmd, capture_output=True, text=True, cwd=cwd)
|
|
238
|
+
|
|
239
|
+
|
|
240
|
+
def build_projects(emit: dict, projs: dict[str, list[Path]], work: Path) -> list[str]:
|
|
241
|
+
"""Сборка всех проектов каждой цели; список отказов сборки."""
|
|
242
|
+
problems = []
|
|
243
|
+
for lang, dirs in projs.items():
|
|
244
|
+
em = emit[lang]
|
|
245
|
+
if not dirs:
|
|
246
|
+
continue
|
|
247
|
+
if hasattr(em, "build_many"):
|
|
248
|
+
for offset in range(0, len(dirs), 32):
|
|
249
|
+
batch = dirs[offset:offset + 32]
|
|
250
|
+
cmd, cwd = em.build_many([str(d) for d in batch], work / "out")
|
|
251
|
+
res = sh(cmd, cwd)
|
|
252
|
+
if res.returncode != 0:
|
|
253
|
+
problems.append(f"сборка {lang} отказала\n{(res.stdout + res.stderr)[:2000]}")
|
|
254
|
+
break
|
|
255
|
+
continue
|
|
256
|
+
if lang == "go" and len(dirs) > 1:
|
|
257
|
+
from concurrent.futures import ThreadPoolExecutor
|
|
258
|
+
def build_one(directory):
|
|
259
|
+
command, cwd = em.build(str(directory))
|
|
260
|
+
result = sh(command, cwd)
|
|
261
|
+
return (f"сборка {lang} отказала на {directory.relative_to(work)}\n"
|
|
262
|
+
+ (result.stdout + result.stderr)[:1200]) if result.returncode else None
|
|
263
|
+
# Go's cache supports concurrent writers; these are distinct projects.
|
|
264
|
+
with ThreadPoolExecutor(max_workers=4) as pool:
|
|
265
|
+
problems.extend(error for error in pool.map(build_one, dirs) if error)
|
|
266
|
+
continue
|
|
267
|
+
for d in dirs:
|
|
268
|
+
step = em.build(str(d))
|
|
269
|
+
if step is None:
|
|
270
|
+
continue
|
|
271
|
+
cmd, cwd = step
|
|
272
|
+
res = sh(cmd, cwd)
|
|
273
|
+
if res.returncode != 0:
|
|
274
|
+
problems.append(f"сборка {lang} отказала на {d.relative_to(work)}\n{(res.stdout + res.stderr)[:1200]}")
|
|
275
|
+
return problems
|
|
276
|
+
|
|
277
|
+
|
|
278
|
+
def run_target(em, proj: Path, main: str, args: list[str]):
|
|
279
|
+
"""Один запуск: ('answer', кортеж) | ('refused', текст) | ('crash', причина)."""
|
|
280
|
+
cmd, cwd = em.run(str(proj), main)
|
|
281
|
+
res = sh(cmd + args, cwd)
|
|
282
|
+
if res.returncode == 0:
|
|
283
|
+
try:
|
|
284
|
+
a = json.loads(res.stdout)
|
|
285
|
+
if main == "cert":
|
|
286
|
+
return "answer", a
|
|
287
|
+
return "answer", answer_key(a)
|
|
288
|
+
except (ValueError, KeyError, TypeError):
|
|
289
|
+
return "crash", f"код 0, но ответ не JSON ответа: {res.stdout[:200]!r}"
|
|
290
|
+
if res.returncode == REFUSED_EXIT:
|
|
291
|
+
try:
|
|
292
|
+
return "refused", json.loads(res.stdout)["refused"]
|
|
293
|
+
except (ValueError, KeyError, TypeError):
|
|
294
|
+
return "crash", f"код {REFUSED_EXIT} без JSON отказа: {res.stdout[:200]!r}"
|
|
295
|
+
return "crash", f"код {res.returncode}: {crash_line(res.stderr or res.stdout)}"
|
|
296
|
+
|
|
297
|
+
|
|
298
|
+
def canon_value(value) -> str:
|
|
299
|
+
"""Канонические байты §208 значения результата — оракульским каноном, а
|
|
300
|
+
не сериализатором цели: порядок элементов коллекции и запись чисел у
|
|
301
|
+
раннера проверяются против той же функции, что печатает голден."""
|
|
302
|
+
sys.path.insert(0, str(ROOT / "engines" / "lawref"))
|
|
303
|
+
from lawref.canon import canonical_bytes
|
|
304
|
+
return canonical_bytes(value, document=False).decode("utf-8")
|
|
305
|
+
|
|
306
|
+
|
|
307
|
+
LAYERS = ("L0", "L1", "L2", "L3")
|
|
308
|
+
|
|
309
|
+
|
|
310
|
+
def positions_key(status: str, positions: list, supports: list) -> str:
|
|
311
|
+
"""Ответ `positions` без id и доказательства (DECISION-0242 K4): сводный
|
|
312
|
+
статус, мультимножество позиций (шаблон, модальность, держатель,
|
|
313
|
+
beneficiary, заземлённый payload, статус) и мультимножество опор статуса.
|
|
314
|
+
Держатель-заглушка `<позиция>/holder` зависит от счётчика позиций и
|
|
315
|
+
сравнивается как заглушка."""
|
|
316
|
+
def actor(v):
|
|
317
|
+
return "<fallback>" if str(v).startswith("urn:position:") else v
|
|
318
|
+
|
|
319
|
+
items = []
|
|
320
|
+
for p in positions:
|
|
321
|
+
item = {"template": p["template"], "modality": p["modality"], "holder": actor(p.get("holder")),
|
|
322
|
+
"status": p["status"], "payload": p["payload"]}
|
|
323
|
+
if "beneficiary" in p:
|
|
324
|
+
item["beneficiary"] = actor(p["beneficiary"])
|
|
325
|
+
items.append(canon_value(item))
|
|
326
|
+
sups = sorted(canon_value([s["status"], s["truthStatus"]]) for s in supports)
|
|
327
|
+
return "NORM_POSITION:" + status + ":" + canon_value(sorted(items)) + ":" + canon_value(sups)
|
|
328
|
+
|
|
329
|
+
|
|
330
|
+
def answer_key(a: dict) -> tuple:
|
|
331
|
+
"""Сравнимая тройка ответа: у truth — статус истинности, у term/collect —
|
|
332
|
+
канонические байты значения, у positions — позиции без id; далее статус
|
|
333
|
+
вычисления и issues."""
|
|
334
|
+
if "normativeStatus" in a:
|
|
335
|
+
head = positions_key(a["normativeStatus"], a["positions"], a["supports"])
|
|
336
|
+
elif "truthStatus" in a:
|
|
337
|
+
head = a["truthStatus"]
|
|
338
|
+
else:
|
|
339
|
+
# RUNTIME_ERROR вопроса `term` значения не несёт (DECISION-0252 п. 9) —
|
|
340
|
+
# как голден, а не `null`.
|
|
341
|
+
head = a["resultKind"] + ":" + (canon_value(a["value"]) if "value" in a else "")
|
|
342
|
+
return (head, a["evaluationStatus"], tuple(a.get("issues") or []), requests_key(a.get("judgmentRequests") or []))
|
|
343
|
+
|
|
344
|
+
|
|
345
|
+
def requests_key(requests: list) -> str:
|
|
346
|
+
"""DECISION-0274: запросы судье §47.3 сверяются с голденом целиком, кроме
|
|
347
|
+
proof-полей (`proof` — узел proof-графа, которого у среза нет): id,
|
|
348
|
+
отношение, орган, объект запроса, форма."""
|
|
349
|
+
return canon_value([{k: v for k, v in r.items() if k != "proof"} for r in requests])
|
|
350
|
+
|
|
351
|
+
|
|
352
|
+
def golden_requests(doc: dict) -> str:
|
|
353
|
+
return requests_key((doc["results"][0] or {}).get("judgmentRequests") or [])
|
|
354
|
+
|
|
355
|
+
|
|
356
|
+
def golden_key(doc: dict) -> tuple:
|
|
357
|
+
exp = doc["results"][0]
|
|
358
|
+
if exp.get("resultKind") == "NORM_POSITION":
|
|
359
|
+
return (positions_key(exp.get("normativeStatus"), doc.get("positions") or [],
|
|
360
|
+
exp.get("normativeStatusSupports") or []), exp.get("evaluationStatus"))
|
|
361
|
+
if "truthStatus" in exp:
|
|
362
|
+
return (exp.get("truthStatus"), exp.get("evaluationStatus"))
|
|
363
|
+
head = exp.get("resultKind") + ":" + (canon_value(exp["value"]) if "value" in exp else "")
|
|
364
|
+
return (head, exp.get("evaluationStatus"))
|
|
365
|
+
|
|
366
|
+
|
|
367
|
+
def certify(emit: dict, work: Path, active: list[str]) -> list[str]:
|
|
368
|
+
"""Сертификация рантайма: фикстура `runtime-cert.json` исполняется каждой
|
|
369
|
+
целью, ответ каждого случая сверяется с ожиданием, записанным из контракта
|
|
370
|
+
README, а не снятым с реализации — сравнение целей между собой общую
|
|
371
|
+
ошибку не находит (коллизия ключа `["a|e:b","c"]` / `["a","b|e:c"]` жила
|
|
372
|
+
во всех трёх рантаймах одинаково)."""
|
|
373
|
+
fixture = json.loads(CERT.read_text(encoding="utf-8"))
|
|
374
|
+
expect = {c["id"]: c["expect"] for c in fixture["cases"]}
|
|
375
|
+
problems = []
|
|
376
|
+
projs = {}
|
|
377
|
+
for lang in active:
|
|
378
|
+
d = work / "cert" / lang
|
|
379
|
+
D.emit_project({}, lang, d, main="cert")
|
|
380
|
+
projs[lang] = [d]
|
|
381
|
+
problems += build_projects(emit, projs, work / "cert")
|
|
382
|
+
if problems:
|
|
383
|
+
return ["сертификация: " + p for p in problems]
|
|
384
|
+
for lang in active:
|
|
385
|
+
kind, payload = run_target(emit[lang], work / "cert" / lang, "cert", [str(CERT)])
|
|
386
|
+
if kind != "answer":
|
|
387
|
+
problems.append(f"сертификация {lang}: {kind.upper()}, {payload}")
|
|
388
|
+
continue
|
|
389
|
+
got = {row["id"]: row["got"] for row in payload}
|
|
390
|
+
for cid, want in expect.items():
|
|
391
|
+
if cid not in got:
|
|
392
|
+
problems.append(f"сертификация {lang}: случай {cid} не исполнен")
|
|
393
|
+
elif got[cid] != want:
|
|
394
|
+
problems.append(f"сертификация {lang}: {cid}: ожидалось {json.dumps(want, ensure_ascii=False)}, "
|
|
395
|
+
f"получено {json.dumps(got[cid], ensure_ascii=False)}")
|
|
396
|
+
return problems
|
|
397
|
+
|
|
398
|
+
|
|
399
|
+
def synth_arg(kind: str, i: int, param: str = ""):
|
|
400
|
+
"""Синтетический литерал-терм §53 по виду столбца; enum — нет (члены плану неизвестны).
|
|
401
|
+
Тип значения — объявленный тип параметра: синтетика положительных заданий
|
|
402
|
+
канонична и по §189 принимается и раннером, и API."""
|
|
403
|
+
if kind == "text":
|
|
404
|
+
if param == "urn:law:std#Text":
|
|
405
|
+
return {"kind": "value", "type": {"name": param}, "value": f"apicheck-{i}"}
|
|
406
|
+
return {"kind": "entity_ref", "id": f"urn:apicheck:e{i}"}
|
|
407
|
+
if kind == "money":
|
|
408
|
+
return {"kind": "value", "type": {"name": "urn:law:std#Money"}, "value": str(100 + i), "currency": "KZT"}
|
|
409
|
+
if kind == "dec":
|
|
410
|
+
name = param if param in ("urn:law:std#Integer", "urn:law:std#Natural") else "urn:law:std#Decimal"
|
|
411
|
+
# §208 (errata E-0197): канон литерала `Integer` — целое JSON-ЧИСЛО, а
|
|
412
|
+
# `Decimal` — строка. До errata обе реализации и все три цели читали
|
|
413
|
+
# `"10"` в слоте Integer средствами своего языка; теперь это TYPE_ERROR
|
|
414
|
+
# у оракула, и синтетический аргумент ворот обязан быть каноничен.
|
|
415
|
+
if name != "urn:law:std#Decimal":
|
|
416
|
+
return {"kind": "value", "type": {"name": name}, "value": 10 + i}
|
|
417
|
+
return {"kind": "value", "type": {"name": name}, "value": str(10 + i)}
|
|
418
|
+
if kind == "date":
|
|
419
|
+
return {"kind": "value", "type": {"name": "urn:law:std#Date"}, "value": "2026-01-01"}
|
|
420
|
+
return None
|
|
421
|
+
|
|
422
|
+
|
|
423
|
+
def wrong_arg(kind: str, param: str):
|
|
424
|
+
"""Литерал заведомо чужого типа для слота (§189, DECISION-0423 §2.6):
|
|
425
|
+
Decimal в слоте сущности и Text, Decimal в Integer, Integer в Decimal,
|
|
426
|
+
Decimal в Money, Text в Date. Раннер и оракул не принимают такое
|
|
427
|
+
утверждение (`TYPE_ERROR`); API обязан ответить так же."""
|
|
428
|
+
dec = {"kind": "value", "type": {"name": "urn:law:std#Decimal"}, "value": "10.5"}
|
|
429
|
+
if kind == "text":
|
|
430
|
+
return dec
|
|
431
|
+
if kind == "dec":
|
|
432
|
+
if param in ("urn:law:std#Integer", "urn:law:std#Natural"):
|
|
433
|
+
return dec
|
|
434
|
+
return {"kind": "value", "type": {"name": "urn:law:std#Integer"}, "value": 10}
|
|
435
|
+
if kind == "money":
|
|
436
|
+
return dec
|
|
437
|
+
if kind == "date":
|
|
438
|
+
return {"kind": "value", "type": {"name": "urn:law:std#Text"}, "value": "2026-01-01"}
|
|
439
|
+
return None
|
|
440
|
+
|
|
441
|
+
|
|
442
|
+
def _job(q: dict, facts: list, args: list, tag: str) -> dict:
|
|
443
|
+
return {"question": q["name"], "pred": q["pred"], "on": "2026-01-01", "args": args, "facts": facts, "tag": tag}
|
|
444
|
+
|
|
445
|
+
|
|
446
|
+
def api_jobs(pl: dict) -> list:
|
|
447
|
+
"""Задания проверки API (DECISION-0423 §2.6). Для каждого вопроса:
|
|
448
|
+
дело из одного факта на каждый его вход (одна и та же сущность во всех
|
|
449
|
+
аргументах, чтобы правила с соединением по переменной срабатывали);
|
|
450
|
+
у модели L1 — тот же вопрос с первым входом, поданным отрицательным
|
|
451
|
+
фактом; и отрицательный случай — первый аргумент первого факта подан
|
|
452
|
+
литералом чужого типа (§189)."""
|
|
453
|
+
jobs = []
|
|
454
|
+
inputs = {i["name"]: i for i in pl["api"]["inputs"]}
|
|
455
|
+
by_pred = {i["pred"]: i["name"] for i in pl["api"]["inputs"]}
|
|
456
|
+
for q in pl["api"]["questions"]:
|
|
457
|
+
args = [synth_arg(k, 0, t) for k, t in zip(q["kinds"], q["params"])]
|
|
458
|
+
facts = []
|
|
459
|
+
for pred in q["inputs"]:
|
|
460
|
+
i = inputs[by_pred[pred]]
|
|
461
|
+
fa = [synth_arg(k, 0, t) for k, t in zip(i["kinds"], i["params"])]
|
|
462
|
+
if any(a is None for a in fa):
|
|
463
|
+
facts = None
|
|
464
|
+
break
|
|
465
|
+
facts.append({"input": i["name"], "pred": pred, "args": fa})
|
|
466
|
+
if facts is None or any(a is None for a in args):
|
|
467
|
+
continue
|
|
468
|
+
jobs.append(_job(q, facts, args, "positive"))
|
|
469
|
+
# L1 (DECISION-0237): тот же вопрос с первым входом, поданным
|
|
470
|
+
# отрицательным фактом, — путь `negated_*` API и §113 раннера.
|
|
471
|
+
if facts and "negation" in (pl.get("capabilities") or ()):
|
|
472
|
+
neg = [dict(facts[0], polarity="negative")] + facts[1:]
|
|
473
|
+
jobs.append(_job(q, neg, args, "negated"))
|
|
474
|
+
if facts:
|
|
475
|
+
i = inputs[facts[0]["input"]]
|
|
476
|
+
bad = wrong_arg(i["kinds"][0], i["params"][0]) if i["params"] else None
|
|
477
|
+
if bad is not None:
|
|
478
|
+
wrong = [dict(facts[0], args=[bad] + facts[0]["args"][1:])] + facts[1:]
|
|
479
|
+
jobs.append(_job(q, wrong, args, "wrong-type"))
|
|
480
|
+
return jobs
|
|
481
|
+
|
|
482
|
+
|
|
483
|
+
def key_conflict_plan(pl: dict):
|
|
484
|
+
"""Синтетический ключ §45.1 на первом входе арности не меньше двух: план
|
|
485
|
+
с `keys = [[0]]` у его таблицы и имя входа. Дело с двумя фактами одного
|
|
486
|
+
первого аргумента даёт `KEY_CONFLICT`; вопрос, читающий вход, — у
|
|
487
|
+
раннера `NON_EXECUTABLE` (E-0242), независимый — `COMPUTED`. Норма общая
|
|
488
|
+
для любого пакета, поэтому ключ синтетический (DECISION-0423 §2.6)."""
|
|
489
|
+
for i in pl["api"]["inputs"]:
|
|
490
|
+
if len(i["params"]) < 2 or any(synth_arg(k, 1, t) is None for k, t in zip(i["kinds"], i["params"])):
|
|
491
|
+
continue
|
|
492
|
+
mutated = copy.deepcopy(pl)
|
|
493
|
+
for t in mutated["tables"]:
|
|
494
|
+
if t["pred"] == i["pred"]:
|
|
495
|
+
t["keys"] = [[0]]
|
|
496
|
+
return mutated, i
|
|
497
|
+
return None, None
|
|
498
|
+
|
|
499
|
+
|
|
500
|
+
def key_conflict_jobs(pl: dict, keyed: dict) -> list:
|
|
501
|
+
jobs = []
|
|
502
|
+
for job in api_jobs(pl):
|
|
503
|
+
if job["tag"] != "positive":
|
|
504
|
+
continue
|
|
505
|
+
extra = {"input": keyed["name"], "pred": keyed["pred"],
|
|
506
|
+
"args": [synth_arg(keyed["kinds"][0], 0, keyed["params"][0])]
|
|
507
|
+
+ [synth_arg(k, 1, t) for k, t in zip(keyed["kinds"][1:], keyed["params"][1:])]}
|
|
508
|
+
first = {"input": keyed["name"], "pred": keyed["pred"],
|
|
509
|
+
"args": [synth_arg(k, 0, t) for k, t in zip(keyed["kinds"], keyed["params"])]}
|
|
510
|
+
facts = [f for f in job["facts"] if f["input"] != keyed["name"]] + [first, extra]
|
|
511
|
+
jobs.append(dict(job, facts=facts, tag="key-conflict"))
|
|
512
|
+
return jobs
|
|
513
|
+
|
|
514
|
+
|
|
515
|
+
def _api_case_query(job: dict, j: int) -> tuple[dict, dict]:
|
|
516
|
+
case = {"context": {"legalTime": job["on"]},
|
|
517
|
+
"assertions": [{"literal": {"kind": "literal", "predicate": f["pred"],
|
|
518
|
+
"polarity": f.get("polarity", "positive"), "args": f["args"]}}
|
|
519
|
+
for f in job["facts"]]}
|
|
520
|
+
query = {"kind": "truth", "queryId": job["queryId"],
|
|
521
|
+
"literal": {"kind": "literal", "predicate": job["pred"], "polarity": "positive", "args": job["args"]}}
|
|
522
|
+
return case, query
|
|
523
|
+
|
|
524
|
+
|
|
525
|
+
def _api_emit(emit: dict, pl: dict, active: list[str], root: Path) -> tuple[dict, list[str]]:
|
|
526
|
+
projs, problems = {}, []
|
|
527
|
+
for lang in active:
|
|
528
|
+
proj = root / lang
|
|
529
|
+
D.emit_project(pl, lang, proj)
|
|
530
|
+
harness = BACKENDS / "targets" / lang / emit[lang].MAINS["apicheck"]
|
|
531
|
+
shutil.copyfile(harness, proj / harness.name)
|
|
532
|
+
projs[lang] = [proj]
|
|
533
|
+
problems += build_projects(emit, projs, root)
|
|
534
|
+
for lang in active:
|
|
535
|
+
if hasattr(emit[lang], "build_api"):
|
|
536
|
+
cmd, cwd = emit[lang].build_api(str(projs[lang][0]))
|
|
537
|
+
res = sh(cmd, cwd)
|
|
538
|
+
if res.returncode != 0:
|
|
539
|
+
problems.append(f"сборка харнесса {lang} отказала\n{(res.stdout + res.stderr)[:1200]}")
|
|
540
|
+
return projs, problems
|
|
541
|
+
|
|
542
|
+
|
|
543
|
+
def _api_batch(em, proj: Path, path: Path) -> tuple[str, object]:
|
|
544
|
+
"""Пакет заданий в одном процессе харнесса: ('answers', [исход…]) либо
|
|
545
|
+
('crash', причина). Исход задания — ('answer', ключ) или ('refused', текст)."""
|
|
546
|
+
cmd, cwd = em.run(str(proj), "apicheck")
|
|
547
|
+
res = sh(cmd + [str(path)], cwd)
|
|
548
|
+
if res.returncode != 0:
|
|
549
|
+
return "crash", f"код {res.returncode}: {crash_line(res.stderr or res.stdout)}"
|
|
550
|
+
try:
|
|
551
|
+
out = json.loads(res.stdout)
|
|
552
|
+
return "answers", [("refused", a["refused"]) if "refused" in a else ("answer", answer_key(a))
|
|
553
|
+
for a in out["answers"]]
|
|
554
|
+
except (ValueError, KeyError, TypeError):
|
|
555
|
+
return "crash", f"ответ пакета не JSON: {res.stdout[:200]!r}"
|
|
556
|
+
|
|
557
|
+
|
|
558
|
+
def _api_run(emit: dict, projs: dict, active: list[str], jobs: list, root: Path, label: str,
|
|
559
|
+
problems: list) -> tuple[int, dict]:
|
|
560
|
+
"""Каждое задание: раннер и API каждой цели дают один исход. Возвращает
|
|
561
|
+
число сверенных заданий и исходы раннера по номеру задания."""
|
|
562
|
+
checked, runner = 0, {}
|
|
563
|
+
for j, job in enumerate(jobs):
|
|
564
|
+
d = root / "jobs" / f"{j:03d}"
|
|
565
|
+
d.mkdir(parents=True, exist_ok=True)
|
|
566
|
+
job["queryId"] = f"urn:query:apicheck:{j}"
|
|
567
|
+
case, query = _api_case_query(job, j)
|
|
568
|
+
(d / "job.json").write_text(json.dumps(job, ensure_ascii=False), encoding="utf-8")
|
|
569
|
+
(d / "case.json").write_text(json.dumps(case, ensure_ascii=False), encoding="utf-8")
|
|
570
|
+
(d / "query.json").write_text(json.dumps(query, ensure_ascii=False), encoding="utf-8")
|
|
571
|
+
got = {}
|
|
572
|
+
for lang in active:
|
|
573
|
+
proj = projs[lang][0]
|
|
574
|
+
got[f"{lang}/runner"] = run_target(emit[lang], proj, "runner", [str(d / "case.json"), str(d / "query.json")])
|
|
575
|
+
got[f"{lang}/api"] = run_target(emit[lang], proj, "apicheck", [str(d / "job.json")])
|
|
576
|
+
crashes = [f"{k}: {msg}" for k, (kind, msg) in got.items() if kind == "crash"]
|
|
577
|
+
if crashes:
|
|
578
|
+
problems.append(f"api {label} {job['question']} ({job['tag']}): КРАХ {crashes}")
|
|
579
|
+
continue
|
|
580
|
+
if len(set(got.values())) != 1:
|
|
581
|
+
problems.append(f"api {label} {job['question']} ({job['tag']}): ответы разошлись {got}")
|
|
582
|
+
continue
|
|
583
|
+
runner[j] = got[f"{active[0]}/runner"]
|
|
584
|
+
checked += 1
|
|
585
|
+
return checked, runner
|
|
586
|
+
|
|
587
|
+
|
|
588
|
+
def api_check(emit: dict, work: Path, active: list[str]) -> tuple[list[str], int]:
|
|
589
|
+
"""API против раннера (DECISION-0234, DECISION-0423 §2.6): те же факты и
|
|
590
|
+
тот же вопрос дают один исход у API и раннера каждой цели и у всех целей
|
|
591
|
+
между собой — на положительных заданиях, отрицательных фактах L1,
|
|
592
|
+
литералах чужого типа (§189) и синтетическом конфликте ключа §45.1 при
|
|
593
|
+
читающем и независимом вопросе. Затем пакет всех заданий с повтором
|
|
594
|
+
первого исполняется одним процессом харнесса — последовательно и
|
|
595
|
+
параллельно (потоки Python, горутины Go): состояние между вызовами не
|
|
596
|
+
протекает. Канарейка: порча статуса и issues в общей части API Python
|
|
597
|
+
обязана дать расхождение."""
|
|
598
|
+
problems, checked = [], 0
|
|
599
|
+
for name in API_PACKAGES + API_PACKAGES_L1:
|
|
600
|
+
path = CLIR / f"{name}.lawir.json"
|
|
601
|
+
if not path.exists():
|
|
602
|
+
problems.append(f"api {name}: снимка {path.relative_to(ROOT)} нет")
|
|
603
|
+
continue
|
|
604
|
+
try:
|
|
605
|
+
ir = json.loads(path.read_text(encoding="utf-8"))
|
|
606
|
+
world = P.resolve_world(ir, P.world_index(ROOT)) if ir.get("imports") else []
|
|
607
|
+
pl = P.build_plan(ir, world)
|
|
608
|
+
except P.Refused as exc:
|
|
609
|
+
problems.append(f"api {name}: план отказал — {exc}")
|
|
610
|
+
continue
|
|
611
|
+
jobs = api_jobs(pl)
|
|
612
|
+
if not jobs:
|
|
613
|
+
problems.append(f"api {name}: ни одного вопроса с синтезируемыми входами")
|
|
614
|
+
continue
|
|
615
|
+
projs, build = _api_emit(emit, pl, active, work / "api" / name)
|
|
616
|
+
problems += [f"api {name}: {p}" for p in build]
|
|
617
|
+
if build:
|
|
618
|
+
return problems, checked
|
|
619
|
+
n, runner = _api_run(emit, projs, active, jobs, work / "api" / name, name, problems)
|
|
620
|
+
checked += n
|
|
621
|
+
# Пакет в одном процессе: A, B, …, A — повторный вызов не видит прежних фактов.
|
|
622
|
+
batch = jobs + jobs[:1]
|
|
623
|
+
expect = [runner.get(j) for j in range(len(jobs))] + [runner.get(0)]
|
|
624
|
+
for concurrent in (False, True):
|
|
625
|
+
bpath = work / "api" / name / f"batch-{'par' if concurrent else 'seq'}.json"
|
|
626
|
+
bpath.write_text(json.dumps({"batch": batch, "concurrent": concurrent}, ensure_ascii=False), encoding="utf-8")
|
|
627
|
+
for lang in active:
|
|
628
|
+
kind, outs = _api_batch(emit[lang], projs[lang][0], bpath)
|
|
629
|
+
if kind == "crash":
|
|
630
|
+
problems.append(f"api {name}: пакет {lang} {'параллельно' if concurrent else 'последовательно'} — КРАХ {outs}")
|
|
631
|
+
continue
|
|
632
|
+
for j, (want, have) in enumerate(zip(expect, outs)):
|
|
633
|
+
if want is not None and want != have:
|
|
634
|
+
problems.append(f"api {name}: пакет {lang} {'параллельно' if concurrent else 'последовательно'}, "
|
|
635
|
+
f"задание {j}: {have} против раннера {want}")
|
|
636
|
+
if len(outs) != len(expect):
|
|
637
|
+
problems.append(f"api {name}: пакет {lang} ответил {len(outs)} исходами из {len(expect)}")
|
|
638
|
+
keyed_plan, keyed = key_conflict_plan(pl)
|
|
639
|
+
if keyed_plan is not None:
|
|
640
|
+
kjobs = key_conflict_jobs(keyed_plan, keyed)
|
|
641
|
+
kprojs, build = _api_emit(emit, keyed_plan, active, work / "api" / name / "key-conflict")
|
|
642
|
+
problems += [f"api {name} key-conflict: {p}" for p in build]
|
|
643
|
+
if not build and kjobs:
|
|
644
|
+
n, _ = _api_run(emit, kprojs, active, kjobs, work / "api" / name / "key-conflict",
|
|
645
|
+
name + " key-conflict", problems)
|
|
646
|
+
checked += n
|
|
647
|
+
if "py" in active:
|
|
648
|
+
problems += _api_canary(emit, projs["py"][0], jobs, work / "api" / name / "canary", name)
|
|
649
|
+
return problems, checked
|
|
650
|
+
|
|
651
|
+
|
|
652
|
+
def _api_canary(emit: dict, proj: Path, jobs: list, root: Path, name: str) -> list[str]:
|
|
653
|
+
"""Канарейка API: копия проекта `py`, у которой общая часть API отвечает
|
|
654
|
+
`COMPUTED` без issues вместо ответа `evaluate`. На заданиях с литералом
|
|
655
|
+
чужого типа раннер даёт `TYPE_ERROR`, поэтому хоть одно расхождение
|
|
656
|
+
обязано найтись — иначе ворота не видят статуса и issues API."""
|
|
657
|
+
wrong = [j for j in jobs if j["tag"] == "wrong-type"]
|
|
658
|
+
if not wrong:
|
|
659
|
+
return []
|
|
660
|
+
if root.exists():
|
|
661
|
+
shutil.rmtree(root)
|
|
662
|
+
shutil.copytree(proj, root)
|
|
663
|
+
base = root / "api_base.py"
|
|
664
|
+
if not base.exists():
|
|
665
|
+
return [f"api {name}: канарейке нечего портить — в проекте py нет api_base.py"]
|
|
666
|
+
text = base.read_text(encoding="utf-8")
|
|
667
|
+
spoiled = text.replace('evaluation_status=doc["evaluationStatus"], issues=tuple(doc["issues"])',
|
|
668
|
+
'evaluation_status="COMPUTED", issues=()')
|
|
669
|
+
if spoiled == text:
|
|
670
|
+
return [f"api {name}: канарейка не нашла, что портить в api_base.py"]
|
|
671
|
+
base.write_text(spoiled, encoding="utf-8")
|
|
672
|
+
for j, job in enumerate(wrong):
|
|
673
|
+
d = root / "jobs" / f"{j:03d}"
|
|
674
|
+
d.mkdir(parents=True, exist_ok=True)
|
|
675
|
+
job = dict(job, queryId=f"urn:query:apicheck:c{j}")
|
|
676
|
+
case, query = _api_case_query(job, j)
|
|
677
|
+
(d / "job.json").write_text(json.dumps(job, ensure_ascii=False), encoding="utf-8")
|
|
678
|
+
(d / "case.json").write_text(json.dumps(case, ensure_ascii=False), encoding="utf-8")
|
|
679
|
+
(d / "query.json").write_text(json.dumps(query, ensure_ascii=False), encoding="utf-8")
|
|
680
|
+
a = run_target(emit["py"], root, "apicheck", [str(d / "job.json")])
|
|
681
|
+
r = run_target(emit["py"], root, "runner", [str(d / "case.json"), str(d / "query.json")])
|
|
682
|
+
if a != r:
|
|
683
|
+
return []
|
|
684
|
+
return [f"api {name}: канарейка — порча статуса и issues в api_base.py не видна воротам"]
|
|
685
|
+
|
|
686
|
+
|
|
687
|
+
def fixtures_check(emit: dict, work: Path, active: list[str]) -> tuple[list[str], list[str]]:
|
|
688
|
+
"""Фикстуры поражаемого слоя (DECISION-0237): ответ из прозы на всех
|
|
689
|
+
целях, тот же ответ в перестановке правил и фактов, и канарейка —
|
|
690
|
+
программа без решающей нормы обязана дать ДРУГОЙ, тоже записанный из
|
|
691
|
+
прозы ответ, то есть ровно на своём случае провалить сверку с ожиданием.
|
|
692
|
+
Структурные фикстуры сверяют причину отказа плана."""
|
|
693
|
+
problems, report = [], []
|
|
694
|
+
jobs = []
|
|
695
|
+
for fx in FX.FIXTURES:
|
|
696
|
+
if "plan" not in fx and (fx["canary"]["expect"], sorted(fx["canary"].get("issues", fx.get("issues", [])))) \
|
|
697
|
+
== (fx["expect"], sorted(fx.get("issues", []))):
|
|
698
|
+
problems.append(f"фикстура {fx['name']}: ожидание канарейки совпадает с ожиданием — она не упадёт")
|
|
699
|
+
if "plan" in fx:
|
|
700
|
+
try:
|
|
701
|
+
P.build_plan(FX.program(fx), None, None, FX.resources(fx))
|
|
702
|
+
problems.append(f"фикстура {fx['name']}: план построен, ожидался отказ «{fx['plan']}»")
|
|
703
|
+
except P.Refused as exc:
|
|
704
|
+
if str(exc).split(":")[0] != fx["plan"]:
|
|
705
|
+
problems.append(f"фикстура {fx['name']}: отказ «{exc}», ожидался «{fx['plan']}»")
|
|
706
|
+
continue
|
|
707
|
+
base = (fx["expect"], tuple(sorted(fx.get("issues", []))))
|
|
708
|
+
canary = (fx["canary"]["expect"], tuple(sorted(fx["canary"].get("issues", fx.get("issues", [])))))
|
|
709
|
+
drop = tuple(fx["canary"]["drop"])
|
|
710
|
+
variants = [("base", FX.program(fx), FX.case(fx), base, ()),
|
|
711
|
+
("perm", FX.program(fx, reverse=True), FX.case(fx, reverse=True), base, ()),
|
|
712
|
+
("canary", FX.program(fx, drop=drop), FX.case(fx, drop=drop), canary, drop)]
|
|
713
|
+
for tag, ir, kase, want, dropped in variants:
|
|
714
|
+
d = work / "fixtures" / f"{fx['name']}-{tag}"
|
|
715
|
+
try:
|
|
716
|
+
pl = P.build_plan(ir, None, None, FX.resources(fx, dropped))
|
|
717
|
+
except P.Refused as exc:
|
|
718
|
+
problems.append(f"фикстура {fx['name']} ({tag}): план отказал — {exc}")
|
|
719
|
+
continue
|
|
720
|
+
d.mkdir(parents=True, exist_ok=True)
|
|
721
|
+
(d / "case.json").write_text(json.dumps(kase, ensure_ascii=False), encoding="utf-8")
|
|
722
|
+
(d / "query.json").write_text(json.dumps(FX.query(fx), ensure_ascii=False), encoding="utf-8")
|
|
723
|
+
jobs.append((fx, tag, d, want))
|
|
724
|
+
for lang in active:
|
|
725
|
+
D.emit_project(pl, lang, d / lang)
|
|
726
|
+
if problems:
|
|
727
|
+
return problems, report
|
|
728
|
+
build = build_projects(emit, {lang: [d / lang for _fx, _t, d, _w in jobs] for lang in active}, work / "fixtures")
|
|
729
|
+
if build:
|
|
730
|
+
return [f"фикстуры: {b}" for b in build], report
|
|
731
|
+
for fx, tag, d, want in jobs:
|
|
732
|
+
args = [str(d / "case.json"), str(d / "query.json")]
|
|
733
|
+
got = {lang: run_target(emit[lang], d / lang, "runner", args) for lang in active}
|
|
734
|
+
bad = {lang: v for lang, v in got.items() if v[0] != "answer"}
|
|
735
|
+
if bad:
|
|
736
|
+
problems.append(f"фикстура {fx['name']} ({tag}): не ответ {bad}")
|
|
737
|
+
continue
|
|
738
|
+
# У ответа `positions` ключ начинается сводным статусом (DECISION-0242).
|
|
739
|
+
head = lambda k: k.split(":")[1] if k.startswith("NORM_POSITION:") else k
|
|
740
|
+
statuses = {lang: (head(v[1][0]), tuple(sorted(c for c in v[1][2] if c in BACKEND_ISSUES)))
|
|
741
|
+
for lang, v in got.items()}
|
|
742
|
+
if len(set(statuses.values())) != 1:
|
|
743
|
+
problems.append(f"фикстура {fx['name']} ({tag}): цели разошлись {statuses}")
|
|
744
|
+
continue
|
|
745
|
+
status = next(iter(statuses.values()))
|
|
746
|
+
if status != want:
|
|
747
|
+
problems.append(f"фикстура {fx['name']} ({tag}): {status} против ожидания {want}")
|
|
748
|
+
elif tag == "canary":
|
|
749
|
+
# Канарейка упала ровно на своём случае: снятая норма меняет ответ.
|
|
750
|
+
shown = lambda st, codes: st + (f" {list(codes)}" if codes else " без issues")
|
|
751
|
+
report.append(f"канарейка {fx['name']}: без {', '.join(fx['canary']['drop'])} — "
|
|
752
|
+
f"{shown(*status)}, сверка с ожиданием "
|
|
753
|
+
f"{shown(fx['expect'], sorted(fx.get('issues', [])))} падает")
|
|
754
|
+
if not problems:
|
|
755
|
+
n = sum(1 for fx in FX.FIXTURES if "plan" not in fx)
|
|
756
|
+
report.insert(0, f"фикстуры L1 — {n} с ответом (и перестановкой), "
|
|
757
|
+
f"{len(FX.FIXTURES) - n} структурных отказов, совпали на целях {', '.join(active)}")
|
|
758
|
+
return problems, report
|
|
759
|
+
|
|
760
|
+
|
|
761
|
+
# DECISION-0252/0256: мутационные канарейки плана значений. Решающую норму
|
|
762
|
+
# Rational и Quantity держат векторы `LAY-L0-RAT-*`/`LAY-L0-QTY-*`, но вектор
|
|
763
|
+
# подтверждает только то, что модуль С нормой совпал; модуль, из плана которого
|
|
764
|
+
# норма снята, обязан с голденом РАЗОЙТИСЬ — иначе норма воротам не видна.
|
|
765
|
+
# Язык фикстур L1 деления и Rational не знает, поэтому канарейка — мутация
|
|
766
|
+
# плана вектора, а не программы. Исполняется целью py (она всегда активна).
|
|
767
|
+
VALUE_CANARIES = (
|
|
768
|
+
("LAY-L0-RAT-DYN-DECIMAL-KEPT", "kindChecks", "сверка вида головы E-0077"),
|
|
769
|
+
("LAY-L0-RAT-GUARD-DYN-MIX", "err", "ошибка значения гарда — issue"),
|
|
770
|
+
("LAY-L0-RAT-HEAD-DIV-ZERO", "headFallible", "ошибка значения головы — issue"),
|
|
771
|
+
("LAY-L0-RAT-DYN-RATIONAL-KEPT", "lk", "статический вид операнда"),
|
|
772
|
+
("LAY-L0-QTY-GUARD-DIMENSION", "err", "разные единицы в гарде — issue"),
|
|
773
|
+
("LAY-L0-QTY-ADD-DIMENSION", "headFallible", "разные единицы в голове — issue"),
|
|
774
|
+
# DECISION-0259 п. 10: политика округления §50 в плане 0.14.
|
|
775
|
+
("LAY-L0-RND-DIVROUND-GUARD-ZERO", "err", "ошибка значения div_round в гарде — issue"),
|
|
776
|
+
("LAY-L0-RND-DIVROUND-HEAD-ZERO", "headFallible", "ошибка значения div_round в голове — issue"),
|
|
777
|
+
("LAY-L0-RND-GUARD-CEILING", "mode", "режим округления из литерала"),
|
|
778
|
+
("LAY-L0-RND-DIVROUND-MONEY-RATIO", "precision", "точность политики: округление не пропущено"),
|
|
779
|
+
("LAY-L0-RND-DIVROUND-MONEY-RATIO", "ak", "статический вид аргумента политики"),
|
|
780
|
+
)
|
|
781
|
+
|
|
782
|
+
|
|
783
|
+
def _mutate_value_plan(pl: dict, what: str) -> dict:
|
|
784
|
+
out = copy.deepcopy(pl)
|
|
785
|
+
for r in out.get("rules", []):
|
|
786
|
+
if what in ("kindChecks", "headFallible"):
|
|
787
|
+
r.pop(what, None)
|
|
788
|
+
elif what == "err":
|
|
789
|
+
for g in r.get("guards", []):
|
|
790
|
+
g.pop("err", None)
|
|
791
|
+
if what in ("mode", "precision", "ak"):
|
|
792
|
+
# DECISION-0259: режим — на HALF_UP, литеральная точность — на единицу
|
|
793
|
+
# меньше, вид делимого — Decimal вместо плана.
|
|
794
|
+
stack = [out]
|
|
795
|
+
while stack:
|
|
796
|
+
x = stack.pop()
|
|
797
|
+
if isinstance(x, dict):
|
|
798
|
+
if x.get("k") == "call" and "ak" in x:
|
|
799
|
+
mode, prec = x["args"][-1], x["args"][-2]
|
|
800
|
+
if what == "mode" and mode.get("k") == "lit":
|
|
801
|
+
mode["v"] = "HALF_UP"
|
|
802
|
+
elif what == "precision" and prec.get("k") == "lit" and int(prec["v"]) > 0:
|
|
803
|
+
prec["v"] = str(int(prec["v"]) - 1)
|
|
804
|
+
elif what == "ak":
|
|
805
|
+
x["ak"][0] = "d" if x["ak"][0] != "d" else "m"
|
|
806
|
+
stack.extend(v for v in x.values() if isinstance(v, (dict, list)))
|
|
807
|
+
elif isinstance(x, list):
|
|
808
|
+
stack.extend(x)
|
|
809
|
+
if what == "lk":
|
|
810
|
+
stack = [out]
|
|
811
|
+
while stack:
|
|
812
|
+
x = stack.pop()
|
|
813
|
+
if isinstance(x, dict):
|
|
814
|
+
if x.get("k") == "bin" and x.get("op") == "div" and "lk" in x:
|
|
815
|
+
x["lk"] = x["rk"] = "d"
|
|
816
|
+
stack.extend(v for v in x.values() if isinstance(v, (dict, list)))
|
|
817
|
+
elif isinstance(x, list):
|
|
818
|
+
stack.extend(x)
|
|
819
|
+
return out
|
|
820
|
+
|
|
821
|
+
|
|
822
|
+
def value_canaries(emit: dict, work: Path) -> tuple[list[str], list[str]]:
|
|
823
|
+
"""Канарейки VALUE_CANARIES: мутированный план обязан разойтись с голденом."""
|
|
824
|
+
problems, report, jobs = [], [], []
|
|
825
|
+
for vid, what, why in VALUE_CANARIES:
|
|
826
|
+
d = VECTORS / "lay" / vid
|
|
827
|
+
try:
|
|
828
|
+
pl = P.build_plan(load(d, "ir.json"))
|
|
829
|
+
except P.Refused as exc:
|
|
830
|
+
# DECISION-0372: архивный вектор 0.1 план не строит — канарейке
|
|
831
|
+
# нужен вектор поддерживаемой версии, а не обход отказа.
|
|
832
|
+
problems.append(f"канарейка {vid}: план отказал — {exc}")
|
|
833
|
+
continue
|
|
834
|
+
mutated = _mutate_value_plan(pl, what)
|
|
835
|
+
if mutated == pl:
|
|
836
|
+
problems.append(f"канарейка {vid}: мутация «{what}» план не меняет — нормы в плане нет")
|
|
837
|
+
continue
|
|
838
|
+
# Одна программа может нести несколько канареек: проект — на пару (вектор, мутация).
|
|
839
|
+
proj = work / "value-canaries" / f"{vid}-{what}"
|
|
840
|
+
D.emit_project(mutated, "py", proj)
|
|
841
|
+
jobs.append((vid, what, why, d, proj))
|
|
842
|
+
if problems:
|
|
843
|
+
return problems, report
|
|
844
|
+
build = build_projects(emit, {"py": [j[4] for j in jobs]}, work / "value-canaries")
|
|
845
|
+
if build:
|
|
846
|
+
return [f"канарейки значений: {b}" for b in build], report
|
|
847
|
+
for vid, what, why, d, proj in jobs:
|
|
848
|
+
doc = load(d, "expected.json")
|
|
849
|
+
want = golden_key(doc)
|
|
850
|
+
want_issues = sorted(i["code"] for i in doc.get("issues") or [] if i.get("code") in BACKEND_ISSUES)
|
|
851
|
+
kind, payload = run_target(emit["py"], proj, "runner",
|
|
852
|
+
[str(d.resolve() / "case.json"), str(d.resolve() / "query.json")])
|
|
853
|
+
if kind == "crash":
|
|
854
|
+
problems.append(f"канарейка {vid}: без «{what}» — крах {payload}")
|
|
855
|
+
elif kind == "answer" and payload[:2] == want \
|
|
856
|
+
and sorted(c for c in payload[2] if c in BACKEND_ISSUES) == want_issues:
|
|
857
|
+
problems.append(f"канарейка {vid}: без «{what}» ответ совпал с голденом — норма «{why}» воротам не видна")
|
|
858
|
+
else:
|
|
859
|
+
shown = payload[:2] if kind == "answer" else f"отказ «{payload}»"
|
|
860
|
+
report.append(f"канарейка {vid}: без «{what}» ({why}) — {shown}, сверка с голденом падает")
|
|
861
|
+
return problems, report
|
|
862
|
+
|
|
863
|
+
|
|
864
|
+
# Политика пустой коллекции §59 (errata E-0195). Векторы `ERR-E0195-EMPTY-POLICY-*`
|
|
865
|
+
# несут в comprehension СВЯЗКУ, и раннер отказывает до агрегата — норма о сверке
|
|
866
|
+
# вида политики с видом элемента им не проверяется ни у одной цели. Пробы ниже
|
|
867
|
+
# берут тот же пакет и то же дело вектора, но comprehension без связок: путь
|
|
868
|
+
# агрегата раннера достижим. Ожидания записаны ИЗ errata («вид политики обязан
|
|
869
|
+
# совпасть с видом элемента ПУСТОЙ коллекции; у оракула несовпадение —
|
|
870
|
+
# TYPE_ERROR»), а не сняты с реализации; первая проба — свидетель нормы (без
|
|
871
|
+
# сверки цели отвечают `Integer 0` и `COMPUTED`, то есть ДРУГИМ ответом, а не
|
|
872
|
+
# меньшим), две остальные держат её от переусердствования: совпавший вид и
|
|
873
|
+
# непустая коллекция обязаны отвечать как прежде.
|
|
874
|
+
EMPTY_POLICY_VECTOR = "errata/ERR-E0195-EMPTY-POLICY-KIND-RUNTIME"
|
|
875
|
+
_EP_MONEY = {"kind": "value", "type": {"name": "urn:law:std#Money"}, "value": "0", "currency": "KZT"}
|
|
876
|
+
_EP_INTEGER = {"kind": "value", "type": {"name": "urn:law:std#Integer"}, "value": 0}
|
|
877
|
+
_EP_SUM = {"kind": "value", "type": {"name": "urn:law:std#Money"}, "value": "80", "currency": "KZT"}
|
|
878
|
+
EMPTY_POLICY_PROBES = (
|
|
879
|
+
("несовпадение вида над пустой коллекцией", "absent", _EP_INTEGER, None, "TYPE_ERROR", ("TYPE_ERROR",)),
|
|
880
|
+
("совпадение вида над пустой коллекцией", "absent", _EP_MONEY, _EP_MONEY, "COMPUTED", ()),
|
|
881
|
+
("несовпадение вида над НЕпустой коллекцией", "order_amount", _EP_INTEGER, _EP_SUM, "COMPUTED", ()),
|
|
882
|
+
)
|
|
883
|
+
|
|
884
|
+
|
|
885
|
+
def empty_policy_probes(emit: dict, work: Path, active: list[str]) -> tuple[list[str], list[str]]:
|
|
886
|
+
"""Сверка вида empty-политики §59 в раннере каждой цели (errata E-0195)."""
|
|
887
|
+
d = VECTORS / EMPTY_POLICY_VECTOR
|
|
888
|
+
if not d.exists():
|
|
889
|
+
return [f"проба политики §59: вектора {EMPTY_POLICY_VECTOR} нет"], []
|
|
890
|
+
problems, report = [], []
|
|
891
|
+
try:
|
|
892
|
+
pl = P.build_plan(load(d, "ir.json"))
|
|
893
|
+
except P.Refused as exc:
|
|
894
|
+
return [f"проба политики §59: план вектора {EMPTY_POLICY_VECTOR} отказал — {exc}"], report
|
|
895
|
+
base = work / "empty-policy"
|
|
896
|
+
for lang in active:
|
|
897
|
+
D.emit_project(pl, lang, base / lang)
|
|
898
|
+
build = build_projects(emit, {lang: [base / lang] for lang in active}, base)
|
|
899
|
+
if build:
|
|
900
|
+
return [f"проба политики §59: {b}" for b in build], report
|
|
901
|
+
template = load(d, "query.json")
|
|
902
|
+
for n, (why, pred, policy, value, status, issues) in enumerate(EMPTY_POLICY_PROBES):
|
|
903
|
+
q = copy.deepcopy(template)
|
|
904
|
+
q["term"]["input"].pop("binders", None)
|
|
905
|
+
q["term"]["input"]["generator"]["formula"]["predicate"] = f"urn:law:vectors:err-e0195#{pred}"
|
|
906
|
+
q["term"]["options"] = {"empty": policy}
|
|
907
|
+
job = base / f"q{n}"
|
|
908
|
+
job.mkdir(parents=True, exist_ok=True)
|
|
909
|
+
(job / "query.json").write_text(json.dumps(q, ensure_ascii=False), encoding="utf-8")
|
|
910
|
+
want = ("DATA:" + (canon_value(value) if value is not None else ""), status, tuple(issues))
|
|
911
|
+
got = {}
|
|
912
|
+
for lang in active:
|
|
913
|
+
kind, payload = run_target(emit[lang], base / lang, "runner",
|
|
914
|
+
[str(d.resolve() / "case.json"), str(job / "query.json")])
|
|
915
|
+
got[lang] = (kind, payload[:3] if kind == "answer" else payload)
|
|
916
|
+
bad = {lang: v for lang, v in got.items() if v[0] != "answer"}
|
|
917
|
+
if bad:
|
|
918
|
+
problems.append(f"проба политики §59 ({why}): не ответ {bad}")
|
|
919
|
+
continue
|
|
920
|
+
answers = {lang: v[1] for lang, v in got.items()}
|
|
921
|
+
if len(set(answers.values())) != 1:
|
|
922
|
+
problems.append(f"проба политики §59 ({why}): цели разошлись {answers}")
|
|
923
|
+
continue
|
|
924
|
+
answer = next(iter(answers.values()))
|
|
925
|
+
if answer != want:
|
|
926
|
+
problems.append(f"проба политики §59 ({why}): {answer} против ожидания {want}")
|
|
927
|
+
if not problems:
|
|
928
|
+
report.append(f"политика пустой коллекции §59 (E-0195) — {len(EMPTY_POLICY_PROBES)} пробы "
|
|
929
|
+
f"совпали с нормой на целях {', '.join(active)}")
|
|
930
|
+
return problems, report
|
|
931
|
+
|
|
932
|
+
|
|
933
|
+
# Граница JSON-числа канона §208 (errata E-0202). Вектор
|
|
934
|
+
# `ERR-E0202-CANON-NUMBER-I64-MAX` держит ОДНУ сторону границы — целое i64
|
|
935
|
+
# читается точно; вторая сторона воротам недостижима: у float и у целого вне
|
|
936
|
+
# i64 голден есть error-объект §172 без результата, и `classify` такой вектор
|
|
937
|
+
# не исполняет. Между тем ровно там ts терял знаки (`JSON.parse` сводит число
|
|
938
|
+
# к double), и без пробы норма держалась бы на слове. Ожидания записаны ИЗ
|
|
939
|
+
# errata: целое знакового 64-битного диапазона законно, всё прочее — отказ
|
|
940
|
+
# одним текстом у всех целей, потому что канала error-объекта у профиля
|
|
941
|
+
# `core-executable` нет.
|
|
942
|
+
CANON_NUMBER_VECTOR = "errata/ERR-E0202-CANON-NUMBER-I64-MAX"
|
|
943
|
+
CANON_NUMBER_REFUSAL = ("JSON-число вне канона §208 (только целое знакового 64-битного "
|
|
944
|
+
"диапазона) вне среза раннера")
|
|
945
|
+
CANON_NUMBER_PROBES = (
|
|
946
|
+
("i64 max читается точно", 2 ** 63 - 1, str(2 ** 63 - 1)),
|
|
947
|
+
("i64 min читается точно", -(2 ** 63), str(-(2 ** 63))),
|
|
948
|
+
("целое над i64 — отказ", 2 ** 63, None),
|
|
949
|
+
("целое под i64 — отказ", -(2 ** 63) - 1, None),
|
|
950
|
+
("JSON-float — отказ", 1.5, None),
|
|
951
|
+
)
|
|
952
|
+
|
|
953
|
+
|
|
954
|
+
def canon_number_probes(emit: dict, work: Path, active: list[str]) -> tuple[list[str], list[str]]:
|
|
955
|
+
"""Граница i64 §208 в раннере каждой цели (errata E-0202)."""
|
|
956
|
+
d = VECTORS / CANON_NUMBER_VECTOR
|
|
957
|
+
if not d.exists():
|
|
958
|
+
return [f"проба числа канона §208: вектора {CANON_NUMBER_VECTOR} нет"], []
|
|
959
|
+
problems, report = [], []
|
|
960
|
+
try:
|
|
961
|
+
pl = P.build_plan(load(d, "ir.json"))
|
|
962
|
+
except P.Refused as exc:
|
|
963
|
+
return [f"проба числа канона §208: план вектора {CANON_NUMBER_VECTOR} отказал — {exc}"], report
|
|
964
|
+
base = work / "canon-number"
|
|
965
|
+
for lang in active:
|
|
966
|
+
D.emit_project(pl, lang, base / lang)
|
|
967
|
+
build = build_projects(emit, {lang: [base / lang] for lang in active}, base)
|
|
968
|
+
if build:
|
|
969
|
+
return [f"проба числа канона §208: {b}" for b in build], report
|
|
970
|
+
template = load(d, "query.json")
|
|
971
|
+
for n, (why, value, want_digits) in enumerate(CANON_NUMBER_PROBES):
|
|
972
|
+
q = copy.deepcopy(template)
|
|
973
|
+
q["term"]["value"] = value
|
|
974
|
+
job = base / f"q{n}"
|
|
975
|
+
job.mkdir(parents=True, exist_ok=True)
|
|
976
|
+
job.joinpath("query.json").write_text(json.dumps(q, ensure_ascii=False), encoding="utf-8")
|
|
977
|
+
got = {}
|
|
978
|
+
for lang in active:
|
|
979
|
+
kind, payload = run_target(emit[lang], base / lang, "runner",
|
|
980
|
+
[str(d.resolve() / "case.json"), str(job / "query.json")])
|
|
981
|
+
got[lang] = (kind, payload[:2] if kind == "answer" else payload)
|
|
982
|
+
crashes = {lang: v for lang, v in got.items() if v[0] == "crash"}
|
|
983
|
+
if crashes:
|
|
984
|
+
problems.append(f"проба числа канона §208 ({why}): КРАХ {crashes}")
|
|
985
|
+
continue
|
|
986
|
+
if len({v for v in got.values()}) != 1:
|
|
987
|
+
problems.append(f"проба числа канона §208 ({why}): цели разошлись {got}")
|
|
988
|
+
continue
|
|
989
|
+
kind, payload = next(iter(got.values()))
|
|
990
|
+
if want_digits is None:
|
|
991
|
+
if kind != "refused" or payload != CANON_NUMBER_REFUSAL:
|
|
992
|
+
problems.append(f"проба числа канона §208 ({why}): ожидался отказ "
|
|
993
|
+
f"«{CANON_NUMBER_REFUSAL}», получено {kind} {payload}")
|
|
994
|
+
else:
|
|
995
|
+
want = ('DATA:{"kind":"value","type":{"name":"urn:law:std#Integer"},'
|
|
996
|
+
f'"value":{want_digits}}}', "COMPUTED")
|
|
997
|
+
if kind != "answer" or tuple(payload) != want:
|
|
998
|
+
problems.append(f"проба числа канона §208 ({why}): {kind} {payload} против {want}")
|
|
999
|
+
if not problems:
|
|
1000
|
+
report.append(f"граница i64 §208 (E-0202) — {len(CANON_NUMBER_PROBES)} пробы совпали "
|
|
1001
|
+
f"с нормой на целях {', '.join(active)}")
|
|
1002
|
+
return problems, report
|
|
1003
|
+
|
|
1004
|
+
|
|
1005
|
+
def execute(emit: dict, d: Path, work: Path, active: list[str]):
|
|
1006
|
+
"""Исход исполнения вектора всеми целями и список ошибок ворот по нему."""
|
|
1007
|
+
doc = load(d, "expected.json")
|
|
1008
|
+
want = golden_key(doc)
|
|
1009
|
+
want_issues = sorted(i["code"] for i in doc.get("issues") or [] if i.get("code") in BACKEND_ISSUES)
|
|
1010
|
+
args = [str(d.resolve() / "case.json"), str(d.resolve() / "query.json")]
|
|
1011
|
+
got = {lang: run_target(emit[lang], work / d.name / lang, "runner", args) for lang in active}
|
|
1012
|
+
crashes = [f"КРАХ {lang}: {msg}" for lang, (k, msg) in got.items() if k == "crash"]
|
|
1013
|
+
if crashes:
|
|
1014
|
+
return "crash", crashes
|
|
1015
|
+
refused = {lang: msg for lang, (k, msg) in got.items() if k == "refused"}
|
|
1016
|
+
if refused and len(refused) != len(got):
|
|
1017
|
+
answered = {lang: v for lang, (k, v) in got.items() if k == "answer"}
|
|
1018
|
+
return "divergence", [f"отказ не единогласен: отказали {sorted(refused)}, ответили {answered}"]
|
|
1019
|
+
if refused:
|
|
1020
|
+
if len(set(refused.values())) != 1:
|
|
1021
|
+
return "divergence", [f"тексты отказа различаются: {refused}"]
|
|
1022
|
+
return f"runtime: {next(iter(refused.values()))}", []
|
|
1023
|
+
answers = {lang: v for lang, (_k, v) in got.items()}
|
|
1024
|
+
if len(set(answers.values())) != 1:
|
|
1025
|
+
return "divergence", [f"РАСХОЖДЕНИЕ между бэкендами: {answers}"]
|
|
1026
|
+
answer = next(iter(answers.values()))
|
|
1027
|
+
if answer[:2] != want:
|
|
1028
|
+
return "divergence", [f"расхождение с голденом: {answer[:2]} против {want}"]
|
|
1029
|
+
got_issues = sorted(c for c in answer[2] if c in BACKEND_ISSUES)
|
|
1030
|
+
if got_issues != want_issues:
|
|
1031
|
+
return "divergence", [f"расхождение issues с голденом: {got_issues} против {want_issues}"]
|
|
1032
|
+
if answer[3] != golden_requests(doc):
|
|
1033
|
+
# DECISION-0274: судьи §47.3 — часть ответа, сверяются кроме proof-полей.
|
|
1034
|
+
return "divergence", [f"расхождение judgmentRequests с голденом: {answer[3]} против {golden_requests(doc)}"]
|
|
1035
|
+
return "match", []
|
|
1036
|
+
|
|
1037
|
+
|
|
1038
|
+
def main() -> int:
|
|
1039
|
+
ap = argparse.ArgumentParser()
|
|
1040
|
+
ap.add_argument("--keep", metavar="DIR", help="сохранить порождённые проекты")
|
|
1041
|
+
ap.add_argument("--targets", default=DEFAULT_TARGETS, metavar="ЦЕЛИ",
|
|
1042
|
+
help=f"цели через запятую либо all (по умолчанию {DEFAULT_TARGETS})")
|
|
1043
|
+
ap.add_argument("--require", default="", metavar="ЦЕЛИ",
|
|
1044
|
+
help="цели через запятую, отсутствие тулчейна которых — отказ ворот")
|
|
1045
|
+
ap.add_argument("--manifest", type=Path, default=MANIFEST)
|
|
1046
|
+
ap.add_argument("--write-manifest", action="store_true",
|
|
1047
|
+
help="закрепить исходы всех L0- и L1-векторов (только при отсутствии крахов и расхождений)")
|
|
1048
|
+
args = ap.parse_args()
|
|
1049
|
+
|
|
1050
|
+
known = D.targets(protocol="runner")
|
|
1051
|
+
wanted = known if args.targets == "all" else [x for x in args.targets.split(",") if x]
|
|
1052
|
+
unknown = [x for x in wanted if x not in known]
|
|
1053
|
+
if unknown:
|
|
1054
|
+
print(f"check_backends: неизвестные цели: {', '.join(unknown)} (есть: {', '.join(known)})")
|
|
1055
|
+
return 1
|
|
1056
|
+
emit = {lang: D.emitter(lang) for lang in known}
|
|
1057
|
+
failures = version_probe(emit)
|
|
1058
|
+
if failures:
|
|
1059
|
+
for line in failures:
|
|
1060
|
+
print("check_backends: " + line)
|
|
1061
|
+
return 1
|
|
1062
|
+
l1_targets = sorted(lang for lang, em in emit.items() if not D.missing_capabilities(
|
|
1063
|
+
{"capabilities": ["defeasible", "negation", "priority"]}, em))
|
|
1064
|
+
v09_targets = sorted(lang for lang, em in emit.items() if not D.missing_capabilities(
|
|
1065
|
+
{"capabilities": ["aggregate", "closure"]}, em))
|
|
1066
|
+
v10_targets = sorted(lang for lang, em in emit.items() if not D.missing_capabilities(
|
|
1067
|
+
{"capabilities": ["norm"]}, em))
|
|
1068
|
+
v11_targets = sorted(lang for lang, em in emit.items() if not D.missing_capabilities(
|
|
1069
|
+
{"capabilities": sorted(P.CAL_CAPS)}, em))
|
|
1070
|
+
print(f"check_backends: план {P.PLAN_VERSION}; проба чужой версии отвергнута всеми эмиттерами ({len(emit)}); "
|
|
1071
|
+
f"план {P.PLAN_VERSION_L1} печатают {', '.join(l1_targets)}, {P.PLAN_VERSION_09} — "
|
|
1072
|
+
f"{', '.join(v09_targets)}, {P.PLAN_VERSION_10} — {', '.join(v10_targets)}, "
|
|
1073
|
+
f"{P.PLAN_VERSION_11} — {', '.join(v11_targets)}, остальные отвергают")
|
|
1074
|
+
|
|
1075
|
+
tools = {lang: emit[lang].available() for lang in wanted}
|
|
1076
|
+
active = [lang for lang in wanted if tools[lang][0]]
|
|
1077
|
+
if "py" not in active:
|
|
1078
|
+
print("check_backends: цель py обязательна всегда")
|
|
1079
|
+
return 1
|
|
1080
|
+
print("check_backends: цели —", ", ".join(f"{k} ({tools[k][1]})" for k in active))
|
|
1081
|
+
for lang in wanted:
|
|
1082
|
+
if not tools[lang][0]:
|
|
1083
|
+
print(f"check_backends: цель {lang} ПРОПУЩЕНА — {tools[lang][1]}")
|
|
1084
|
+
required = [x for x in args.require.split(",") if x]
|
|
1085
|
+
missing = [x for x in required if x not in active]
|
|
1086
|
+
if missing:
|
|
1087
|
+
print(f"check_backends: обязательные цели без тулчейна: {', '.join(missing)}")
|
|
1088
|
+
return 1
|
|
1089
|
+
emit = {lang: emit[lang] for lang in active}
|
|
1090
|
+
|
|
1091
|
+
kinds = set.intersection(*[set(getattr(emit[lang], "QUERY_KINDS", {"truth"})) for lang in active])
|
|
1092
|
+
print(f"check_backends: виды вопросов у всех активных целей — {', '.join(sorted(kinds))}")
|
|
1093
|
+
outcomes: dict[str, str] = {}
|
|
1094
|
+
layers: dict[str, str] = {}
|
|
1095
|
+
accepted = []
|
|
1096
|
+
problems: list[str] = []
|
|
1097
|
+
for d, layer in candidates():
|
|
1098
|
+
layers[d.name] = layer
|
|
1099
|
+
outcome, pl, errors = classify(d, kinds, emit, layer)
|
|
1100
|
+
problems += [f"{d.name}: {e}" for e in errors]
|
|
1101
|
+
if outcome is None:
|
|
1102
|
+
accepted.append((d, pl))
|
|
1103
|
+
else:
|
|
1104
|
+
outcomes[d.name] = outcome
|
|
1105
|
+
by_layer = collections.Counter(layers.values())
|
|
1106
|
+
print(f"check_backends: векторов L0 рассмотрено {by_layer['L0']}, L1 — {by_layer['L1']}; "
|
|
1107
|
+
f"план построен и запрос поддержан у {len(accepted)}")
|
|
1108
|
+
|
|
1109
|
+
work = Path(args.keep) if args.keep else Path(tempfile.mkdtemp(prefix="law-backends-"))
|
|
1110
|
+
work.mkdir(parents=True, exist_ok=True)
|
|
1111
|
+
|
|
1112
|
+
cert_problems = certify(emit, work, active)
|
|
1113
|
+
if cert_problems:
|
|
1114
|
+
for line in cert_problems:
|
|
1115
|
+
print(" " + line)
|
|
1116
|
+
print("check_backends: сертификация рантайма НЕ пройдена")
|
|
1117
|
+
return 1
|
|
1118
|
+
n_cases = len(json.loads(CERT.read_text(encoding="utf-8"))["cases"])
|
|
1119
|
+
print(f"check_backends: сертификация рантайма — {n_cases} случаев на целях {', '.join(active)}")
|
|
1120
|
+
|
|
1121
|
+
api_problems, api_checked = api_check(emit, work, active)
|
|
1122
|
+
if api_problems:
|
|
1123
|
+
for line in api_problems:
|
|
1124
|
+
print(" " + line)
|
|
1125
|
+
print("check_backends: проверка API НЕ пройдена")
|
|
1126
|
+
return 1
|
|
1127
|
+
print(f"check_backends: API — {api_checked} вопросов на {len(API_PACKAGES) + len(API_PACKAGES_L1)} пакетах совпали с раннером на целях {', '.join(active)}")
|
|
1128
|
+
|
|
1129
|
+
fx_problems, fx_report = fixtures_check(emit, work, active)
|
|
1130
|
+
for line in fx_report:
|
|
1131
|
+
print("check_backends: " + line)
|
|
1132
|
+
if fx_problems:
|
|
1133
|
+
for line in fx_problems:
|
|
1134
|
+
print(" " + line)
|
|
1135
|
+
print("check_backends: фикстуры поражаемого слоя НЕ пройдены")
|
|
1136
|
+
return 1
|
|
1137
|
+
|
|
1138
|
+
vc_problems, vc_report = value_canaries(emit, work)
|
|
1139
|
+
for line in vc_report:
|
|
1140
|
+
print("check_backends: " + line)
|
|
1141
|
+
if vc_problems:
|
|
1142
|
+
for line in vc_problems:
|
|
1143
|
+
print(" " + line)
|
|
1144
|
+
print("check_backends: канарейки значений (DECISION-0252/0256) НЕ пройдены")
|
|
1145
|
+
return 1
|
|
1146
|
+
|
|
1147
|
+
ep_problems, ep_report = empty_policy_probes(emit, work, active)
|
|
1148
|
+
for line in ep_report:
|
|
1149
|
+
print("check_backends: " + line)
|
|
1150
|
+
if ep_problems:
|
|
1151
|
+
for line in ep_problems:
|
|
1152
|
+
print(" " + line)
|
|
1153
|
+
print("check_backends: пробы политики пустой коллекции §59 (errata E-0195) НЕ пройдены")
|
|
1154
|
+
return 1
|
|
1155
|
+
|
|
1156
|
+
cn_problems, cn_report = canon_number_probes(emit, work, active)
|
|
1157
|
+
for line in cn_report:
|
|
1158
|
+
print("check_backends: " + line)
|
|
1159
|
+
if cn_problems:
|
|
1160
|
+
for line in cn_problems:
|
|
1161
|
+
print(" " + line)
|
|
1162
|
+
print("check_backends: пробы границы JSON-числа канона §208 (errata E-0202) НЕ пройдены")
|
|
1163
|
+
return 1
|
|
1164
|
+
|
|
1165
|
+
# Проекты печатает та же поставка, что отдаёт модуль потребителю
|
|
1166
|
+
# (`deliver.emit_project`): ворота проверяют ровно то, что уезжает.
|
|
1167
|
+
projs = {lang: [] for lang in active}
|
|
1168
|
+
for d, pl in accepted:
|
|
1169
|
+
for lang in active:
|
|
1170
|
+
proj = work / d.name / lang
|
|
1171
|
+
D.emit_project(pl, lang, proj)
|
|
1172
|
+
projs[lang].append(proj)
|
|
1173
|
+
build_problems = build_projects(emit, projs, work)
|
|
1174
|
+
if build_problems:
|
|
1175
|
+
for line in build_problems:
|
|
1176
|
+
print("check_backends: " + line)
|
|
1177
|
+
return 1
|
|
1178
|
+
|
|
1179
|
+
for d, _ in accepted:
|
|
1180
|
+
outcome, errors = execute(emit, d, work, active)
|
|
1181
|
+
outcomes[d.name] = outcome
|
|
1182
|
+
problems += [f"{d.name}: {e}" for e in errors]
|
|
1183
|
+
if not args.keep:
|
|
1184
|
+
shutil.rmtree(work, ignore_errors=True)
|
|
1185
|
+
|
|
1186
|
+
for layer in LAYERS:
|
|
1187
|
+
stat = collections.Counter(o.split(":")[0] for name, o in outcomes.items() if layers[name] == layer)
|
|
1188
|
+
if stat:
|
|
1189
|
+
print(f"check_backends: {layer} — " + ", ".join(f"{key}: {n}" for key, n in sorted(stat.items())))
|
|
1190
|
+
matched_l2 = sum(1 for name, o in outcomes.items() if layers[name] in ("L2", "L3") and o == "match")
|
|
1191
|
+
matched = sum(1 for name, o in outcomes.items() if layers[name] == "L0" and o == "match")
|
|
1192
|
+
matched_l1 = sum(1 for name, o in outcomes.items() if layers[name] == "L1"
|
|
1193
|
+
and (o == "match" or o.startswith("structural:")))
|
|
1194
|
+
|
|
1195
|
+
if problems:
|
|
1196
|
+
for line in problems:
|
|
1197
|
+
print(" " + line)
|
|
1198
|
+
return 1
|
|
1199
|
+
|
|
1200
|
+
if args.write_manifest:
|
|
1201
|
+
args.manifest.write_text(json.dumps(
|
|
1202
|
+
{"format": MANIFEST_FORMAT, "targets": active, "vectors": dict(sorted(outcomes.items()))},
|
|
1203
|
+
ensure_ascii=False, indent=1) + "\n", encoding="utf-8")
|
|
1204
|
+
print(f"check_backends: манифест записан — {len(outcomes)} векторов, {matched} совпадений L0, "
|
|
1205
|
+
f"{matched_l1} L1, {matched_l2} L2/L3")
|
|
1206
|
+
return 0
|
|
1207
|
+
|
|
1208
|
+
if not args.manifest.exists():
|
|
1209
|
+
print(f"check_backends: манифеста нет — {args.manifest} (--write-manifest)")
|
|
1210
|
+
return 1
|
|
1211
|
+
pinned = json.loads(args.manifest.read_text(encoding="utf-8"))
|
|
1212
|
+
if pinned.get("format") != MANIFEST_FORMAT:
|
|
1213
|
+
print(f"check_backends: формат манифеста не {MANIFEST_FORMAT}")
|
|
1214
|
+
return 1
|
|
1215
|
+
registry: dict[str, str] = pinned["vectors"]
|
|
1216
|
+
for name, expected in sorted(registry.items()):
|
|
1217
|
+
actual = outcomes.get(name)
|
|
1218
|
+
if actual is None:
|
|
1219
|
+
problems.append(f"{name}: закреплён как «{expected}», вектора нет")
|
|
1220
|
+
elif actual != expected:
|
|
1221
|
+
problems.append(f"{name}: закреплено «{expected}», получено «{actual}»")
|
|
1222
|
+
unpinned = sorted(n for n in outcomes if n not in registry)
|
|
1223
|
+
for name in unpinned:
|
|
1224
|
+
if outcomes[name] == "match" or outcomes[name].startswith("structural:"):
|
|
1225
|
+
problems.append(f"{name}: совпал (или структурный отказ), но не закреплён в манифесте (--write-manifest)")
|
|
1226
|
+
if unpinned:
|
|
1227
|
+
print(f"check_backends: незакреплённых векторов {len(unpinned)} "
|
|
1228
|
+
f"(без совпадений: {sum(1 for n in unpinned if outcomes[n] != 'match')})")
|
|
1229
|
+
if problems:
|
|
1230
|
+
for line in problems:
|
|
1231
|
+
print(" " + line)
|
|
1232
|
+
return 1
|
|
1233
|
+
if matched < MIN_CHECKED:
|
|
1234
|
+
print(f"check_backends: исполнено {matched} векторов при поле храповика {MIN_CHECKED}")
|
|
1235
|
+
return 1
|
|
1236
|
+
if matched_l1 < MIN_CHECKED_L1:
|
|
1237
|
+
print(f"check_backends: L1 — {matched_l1} совпадений при поле храповика {MIN_CHECKED_L1}")
|
|
1238
|
+
return 1
|
|
1239
|
+
if matched_l2 < MIN_CHECKED_L2:
|
|
1240
|
+
print(f"check_backends: L2/L3 — {matched_l2} совпадений при поле храповика {MIN_CHECKED_L2}")
|
|
1241
|
+
return 1
|
|
1242
|
+
print(f"check_backends: OK — L0 {matched} векторов совпали, L1 {matched_l1} (совпадения и "
|
|
1243
|
+
f"структурные отказы), L2/L3 {matched_l2} на целях {', '.join(active)}; манифест {len(registry)} исходов повторён")
|
|
1244
|
+
return 0
|
|
1245
|
+
|
|
1246
|
+
|
|
1247
|
+
if __name__ == "__main__":
|
|
1248
|
+
raise SystemExit(main())
|