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,233 @@
1
+ """Capability contracts and bounded certification for backend runtimes.
2
+
3
+ ``runtime-cert.json`` is deliberately broader than the minimal runtime used by
4
+ some targets. A target is certified against the operations its ``cert``
5
+ runner dispatches, never by attempting unsupported calls and reclassifying the
6
+ result afterwards. The common contract remains mandatory for every target.
7
+ """
8
+ from __future__ import annotations
9
+
10
+ import json
11
+ from pathlib import Path
12
+ from typing import Callable
13
+
14
+
15
+ FORMAT = "law.backends-runtime-cert/1"
16
+
17
+ # These names are the common dispatch surface in every targets/*/cert.*
18
+ # harness. Keep this explicit: adding a target must state its contract here,
19
+ # rather than getting an accidental partial PASS from the fixture contents.
20
+ BASELINE_OPERATIONS = frozenset({
21
+ "abs", "add", "cmp", "date", "dec", "div", "eq", "keyEq",
22
+ "legalTime", "max", "min", "mod", "money", "mul", "pow", "round",
23
+ "setLegalTime", "sub", "sym",
24
+ })
25
+ EXTENSION_OPERATIONS = frozenset({
26
+ "arith", "counter", "divRound", "monthEnd", "monthStart", "qty",
27
+ "rat", "ratTerm", "roundMode",
28
+ # W2 часть 2: Instant — момент, порядок по моменту, длительности §84.1.
29
+ "instant", "hoursBetween", "minutesBetween", "addDuration",
30
+ })
31
+ ALL_OPERATIONS = BASELINE_OPERATIONS | EXTENSION_OPERATIONS
32
+
33
+ # A cert operation is only meaningful with literals its harness can decode.
34
+ # Every common harness accepts scalar strings plus these tagged literal forms;
35
+ # `rat`, `qty`/`unit`, and `bool` are decoded only by the full harnesses.
36
+ COMMON_LITERAL_TAGS = frozenset({"dec", "money", "cur", "text", "date", "sym", "row"})
37
+ EXTENSION_LITERAL_TAGS = frozenset({"rat", "qty", "unit", "bool", "instant"})
38
+ ALL_LITERAL_TAGS = COMMON_LITERAL_TAGS | EXTENSION_LITERAL_TAGS
39
+
40
+ # Evidence is the explicit operation switch/table in each target's cert
41
+ # harness, audited 2026-09-24. The three full runtimes expose every fixture
42
+ # operation; every other runner has the baseline table only.
43
+ FULL_TARGETS = frozenset({"go", "py", "ts"})
44
+ BASELINE_TARGETS = frozenset({
45
+ "bsl", "cs", "dart", "erl", "ex", "java", "kt", "lua", "php", "rb",
46
+ "rs", "scala", "swift",
47
+ })
48
+ TARGET_OPERATIONS = {
49
+ **{target: ALL_OPERATIONS for target in FULL_TARGETS},
50
+ **{target: BASELINE_OPERATIONS for target in BASELINE_TARGETS},
51
+ }
52
+ TARGET_LITERAL_TAGS = {
53
+ **{target: ALL_LITERAL_TAGS for target in FULL_TARGETS},
54
+ **{target: COMMON_LITERAL_TAGS for target in BASELINE_TARGETS},
55
+ }
56
+
57
+
58
+ class FixtureError(ValueError):
59
+ """The fixture cannot support a trustworthy certification run."""
60
+
61
+
62
+ def validate_contract() -> None:
63
+ """Reject an incomplete or stale target declaration before execution."""
64
+ overlap = FULL_TARGETS & BASELINE_TARGETS
65
+ if overlap:
66
+ raise FixtureError("цель одновременно в full и baseline контрактах: " + ", ".join(sorted(overlap)))
67
+ for target, operations in TARGET_OPERATIONS.items():
68
+ missing = BASELINE_OPERATIONS - operations
69
+ unknown = operations - ALL_OPERATIONS
70
+ if missing or unknown:
71
+ details = []
72
+ if missing:
73
+ details.append("нет baseline: " + ", ".join(sorted(missing)))
74
+ if unknown:
75
+ details.append("неизвестны: " + ", ".join(sorted(unknown)))
76
+ raise FixtureError(f"контракт {target}: " + "; ".join(details))
77
+ literals = TARGET_LITERAL_TAGS.get(target)
78
+ if literals is None or not COMMON_LITERAL_TAGS <= literals or not literals <= ALL_LITERAL_TAGS:
79
+ raise FixtureError(f"контракт {target}: неполная или неизвестная literal-поверхность")
80
+
81
+
82
+ def load_fixture(path: Path) -> dict:
83
+ """Load and validate fixture IDs and operations before filtering it."""
84
+ try:
85
+ fixture = json.loads(Path(path).read_text(encoding="utf-8"))
86
+ except (OSError, json.JSONDecodeError) as exc:
87
+ raise FixtureError(f"runtime-cert не прочитан: {exc}") from exc
88
+ if not isinstance(fixture, dict) or fixture.get("format") != FORMAT:
89
+ raise FixtureError(f"runtime-cert должен иметь format {FORMAT}")
90
+ cases = fixture.get("cases")
91
+ if not isinstance(cases, list) or not cases:
92
+ raise FixtureError("runtime-cert должен содержать непустой cases")
93
+ ids, operations = set(), set()
94
+ for number, case in enumerate(cases, 1):
95
+ if not isinstance(case, dict):
96
+ raise FixtureError(f"runtime-cert: случай {number} не объект")
97
+ case_id, operation = case.get("id"), case.get("op")
98
+ if not isinstance(case_id, str) or not case_id:
99
+ raise FixtureError(f"runtime-cert: случай {number} без id")
100
+ if case_id in ids:
101
+ raise FixtureError(f"runtime-cert: повтор id {case_id}")
102
+ if not isinstance(operation, str) or operation not in ALL_OPERATIONS:
103
+ raise FixtureError(f"runtime-cert: {case_id}: неизвестная операция {operation!r}")
104
+ unknown_tags = literal_tags(case.get("args", [])) - ALL_LITERAL_TAGS
105
+ if unknown_tags:
106
+ raise FixtureError(f"runtime-cert: {case_id}: неизвестные literal-теги: "
107
+ + ", ".join(sorted(unknown_tags)))
108
+ if "expect" not in case:
109
+ raise FixtureError(f"runtime-cert: {case_id}: нет expect")
110
+ ids.add(case_id)
111
+ operations.add(operation)
112
+ missing = BASELINE_OPERATIONS - operations
113
+ if missing:
114
+ raise FixtureError("runtime-cert: нет обязательных baseline операций: " + ", ".join(sorted(missing)))
115
+ validate_contract()
116
+ return fixture
117
+
118
+
119
+ def literal_tags(value: object) -> set[str]:
120
+ """Tagged literal keys used by the cert wire fixture, recursively."""
121
+ if isinstance(value, dict):
122
+ return set(value) | set().union(*(literal_tags(item) for item in value.values()))
123
+ if isinstance(value, list):
124
+ return set().union(*(literal_tags(item) for item in value)) if value else set()
125
+ return set()
126
+
127
+
128
+ def profile(target: str, fixture: dict) -> dict:
129
+ """Return a reviewable target profile and the fixture to execute."""
130
+ if target not in TARGET_OPERATIONS:
131
+ raise FixtureError(f"runtime-cert: для runner-цели {target} нет capability-контракта")
132
+ declared = TARGET_OPERATIONS[target]
133
+ literal_tags_for_target = TARGET_LITERAL_TAGS[target]
134
+ cases = fixture["cases"]
135
+ checked, unsupported_operation, unsupported_literal = [], [], []
136
+ for case in cases:
137
+ if case["op"] not in declared:
138
+ unsupported_operation.append(case)
139
+ continue
140
+ unsupported = literal_tags(case.get("args", [])) - literal_tags_for_target
141
+ if unsupported:
142
+ unsupported_literal.append((case, sorted(unsupported)))
143
+ continue
144
+ checked.append(case)
145
+ checked_ops = {case["op"] for case in checked}
146
+ # load_fixture verifies this for the whole fixture; retaining it here makes
147
+ # direct callers fail safely as well.
148
+ missing = BASELINE_OPERATIONS - checked_ops
149
+ if missing:
150
+ raise FixtureError(f"runtime-cert {target}: не отобраны baseline операции: " + ", ".join(sorted(missing)))
151
+ # Declaring an extension without any compatible fixture would make a
152
+ # target look fully certified while that extension was never exercised.
153
+ # This is stricter than checking raw fixture presence: a case carrying an
154
+ # unsupported literal form does not cover the declared operation either.
155
+ declared_without_checked_cases = sorted(declared - checked_ops)
156
+ if declared_without_checked_cases:
157
+ raise FixtureError(f"runtime-cert {target}: нет проверяемых случаев для объявленных операций: "
158
+ + ", ".join(declared_without_checked_cases))
159
+ return {
160
+ "target": target,
161
+ "declaredOperations": sorted(declared),
162
+ "checkedOperations": sorted(checked_ops),
163
+ "literalTags": sorted(literal_tags_for_target),
164
+ "unsupportedOperations": sorted({case["op"] for case in unsupported_operation}),
165
+ "unsupportedLiteralTags": sorted({tag for _case, tags in unsupported_literal for tag in tags}),
166
+ "declaredOperationsWithoutCheckedCases": declared_without_checked_cases,
167
+ "checkedFixtureIds": [case["id"] for case in checked],
168
+ "unsupportedFixtureIds": [case["id"] for case in unsupported_operation] +
169
+ [case["id"] for case, _tags in unsupported_literal],
170
+ "unsupportedOperationFixtureIds": [case["id"] for case in unsupported_operation],
171
+ "unsupportedOperandFixtureIds": [case["id"] for case, _tags in unsupported_literal],
172
+ "unsupportedOperandForms": {case["id"]: tags for case, tags in unsupported_literal},
173
+ "fixture": {key: value for key, value in fixture.items() if key != "cases"} | {"cases": checked},
174
+ }
175
+
176
+
177
+ def validate_answer(target: str, checked_cases: list[dict], payload: object) -> list[str]:
178
+ """Compare one target answer, refusing silent omissions and surplus rows."""
179
+ if not isinstance(payload, list):
180
+ return [f"сертификация {target}: ответ cert должен быть массивом"]
181
+ expected = {case["id"]: case["expect"] for case in checked_cases}
182
+ received: dict[str, object] = {}
183
+ problems = []
184
+ for number, row in enumerate(payload, 1):
185
+ if not isinstance(row, dict) or not isinstance(row.get("id"), str) or "got" not in row:
186
+ problems.append(f"сертификация {target}: строка ответа {number} не имеет id/got")
187
+ continue
188
+ case_id = row["id"]
189
+ if case_id in received:
190
+ problems.append(f"сертификация {target}: повтор ответа {case_id}")
191
+ elif case_id not in expected:
192
+ problems.append(f"сертификация {target}: ответ для невыбранного случая {case_id}")
193
+ else:
194
+ received[case_id] = row["got"]
195
+ for case_id, want in expected.items():
196
+ if case_id not in received:
197
+ problems.append(f"сертификация {target}: случай {case_id} не исполнен")
198
+ elif received[case_id] != want:
199
+ problems.append(f"сертификация {target}: {case_id}: ожидалось "
200
+ f"{json.dumps(want, ensure_ascii=False)}, получено "
201
+ f"{json.dumps(received[case_id], ensure_ascii=False)}")
202
+ return problems
203
+
204
+
205
+ def certify_target(target: str, descriptor, work: Path, fixture_path: Path,
206
+ emit_project: Callable, build_projects: Callable,
207
+ run_target: Callable) -> tuple[dict, list[str]]:
208
+ """Emit, build, and run one target against its declared fixture subset.
209
+
210
+ The three callables are supplied by the invoking gate, keeping this module
211
+ independent from CI and allowing its subprocess/time-limit policy to stay
212
+ in force.
213
+ """
214
+ fixture = load_fixture(fixture_path)
215
+ report = profile(target, fixture)
216
+ cert_dir = Path(work) / "cert" / target
217
+ selected = Path(work) / "cert" / f"runtime-cert-{target}.json"
218
+ selected.parent.mkdir(parents=True, exist_ok=True)
219
+ selected.write_text(json.dumps(report.pop("fixture"), ensure_ascii=False, indent=1) + "\n", encoding="utf-8")
220
+ report["selectedFixture"] = selected.name
221
+ try:
222
+ emit_project({}, target, cert_dir, main="cert")
223
+ except Exception as exc:
224
+ return report, [f"сертификация {target}: не создан cert-проект: {exc}"]
225
+ errors = ["сертификация: " + error for error in build_projects({target: descriptor}, {target: [cert_dir]}, Path(work) / "cert")]
226
+ if errors:
227
+ return report, errors
228
+ kind, payload = run_target(descriptor, cert_dir, "cert", [str(selected)])
229
+ if kind != "answer":
230
+ return report, [f"сертификация {target}: {str(kind).upper()}, {payload}"]
231
+ checked_ids = set(report["checkedFixtureIds"])
232
+ return report, validate_answer(target, [case for case in fixture["cases"]
233
+ if case["id"] in checked_ids], payload)
arxo_codegen/survey.py ADDED
@@ -0,0 +1,272 @@
1
+ #!/usr/bin/env python3
2
+ """Обзор явного набора CLIR планом: какие пакеты срез принимает сегодня и
3
+ почему отказывает остальным. Считает ВСЕ отказы пакета, а не первый.
4
+
5
+ Зачем. Следующий шаг среза выбирается по потребителю, а не по числу: пакет,
6
+ которому до плана не хватает одной формы, стоит одной формы; пакет с
7
+ десятью — не стоит ничего. Первая причина отказа эту разницу скрывает.
8
+
9
+ arxo-codegen-survey PROGRAM.lawir.json [PROGRAM2.lawir.json ...]
10
+ arxo-codegen-survey --input-dir FIXTURES --dep DEP.lawir.json
11
+ arxo-codegen-survey --corpus-root LAW_DSL_ROOT # переходный adapter
12
+ """
13
+ from __future__ import annotations
14
+
15
+ import argparse
16
+ import collections
17
+ import json
18
+ import sys
19
+ from pathlib import Path
20
+
21
+ HERE = Path(__file__).resolve().parent
22
+ sys.path.insert(0, str(HERE))
23
+ import plan as P # noqa: E402
24
+
25
+
26
+ def snapshots(root: Path) -> list[Path]:
27
+ """CLIR корпуса и собственные снимки пакетов вне корпуса; `deps/` и
28
+ `.build/` — копии чужих снимков, они не считаются дважды."""
29
+ out = sorted((root / "corpus" / "clir").glob("*.lawir.json"))
30
+ for p in sorted((root / "packs").rglob("*.lawir.json")):
31
+ if "deps" in p.parts or ".build" in p.parts:
32
+ continue
33
+ out.append(p)
34
+ return out
35
+
36
+
37
+ def head(reason: str) -> str:
38
+ return reason.split(":")[0]
39
+
40
+
41
+ def inputs(paths: list[Path], directories: list[Path], corpus_root: Path | None) -> tuple[list[Path], list[Path]]:
42
+ """Collect only explicitly named inputs; corpus scanning needs its adapter flag."""
43
+ selected = [Path(path) for path in paths]
44
+ bases = [Path(directory) for directory in directories]
45
+ for directory in bases:
46
+ if not directory.is_dir():
47
+ raise ValueError(f"survey: каталога входов нет: {directory}")
48
+ selected += sorted(directory.rglob("*.lawir.json"))
49
+ if corpus_root is not None:
50
+ root = Path(corpus_root)
51
+ if not root.is_dir():
52
+ raise ValueError(f"survey: corpus-root не каталог: {root}")
53
+ if not selected:
54
+ selected = snapshots(root)
55
+ bases.append(root)
56
+ unique = []
57
+ seen = set()
58
+ for path in selected:
59
+ resolved = path.resolve()
60
+ if resolved not in seen:
61
+ seen.add(resolved)
62
+ unique.append(resolved)
63
+ if not unique:
64
+ raise ValueError("survey: укажите CLIR-файлы, --input-dir или --corpus-root")
65
+ missing = [str(path) for path in unique if not path.is_file()]
66
+ if missing:
67
+ raise ValueError(f"survey: входного CLIR нет: {', '.join(missing)}")
68
+ return unique, bases
69
+
70
+
71
+ def display_path(path: Path, bases: list[Path]) -> str:
72
+ """Stable useful name without assuming an installed package has law-dsl ROOT."""
73
+ for base in bases:
74
+ try:
75
+ return path.resolve().relative_to(base.resolve()).as_posix()
76
+ except ValueError:
77
+ continue
78
+ return path.name
79
+
80
+
81
+ def named_documents(paths: list[Path]) -> tuple[list[tuple[Path, dict]], dict[str, Path], dict[str, dict]]:
82
+ """Read the bounded input set once and reject ambiguous package names."""
83
+ documents: list[tuple[Path, dict]] = []
84
+ locations: dict[str, Path] = {}
85
+ by_name: dict[str, dict] = {}
86
+ for path in paths:
87
+ document = json.loads(path.read_text(encoding="utf-8"))
88
+ documents.append((path, document))
89
+ name = (document.get("package") or {}).get("name")
90
+ if not name:
91
+ continue
92
+ if name in locations:
93
+ raise ValueError(f"survey: неоднозначный CLIR пакета {name}: {locations[name]} и {path}")
94
+ locations[name] = path
95
+ by_name[name] = document
96
+ return documents, locations, by_name
97
+
98
+
99
+ def main(argv: list[str] | None = None) -> int:
100
+ ap = argparse.ArgumentParser()
101
+ ap.add_argument("input", nargs="*", type=Path, metavar="CLIR", help="корневой CLIR")
102
+ ap.add_argument("--input-dir", action="append", type=Path, default=[], metavar="DIR",
103
+ help="рекурсивно взять *.lawir.json только из указанного каталога")
104
+ ap.add_argument("--dep", action="append", type=Path, default=[], metavar="CLIR",
105
+ help="явная CLIR-зависимость (можно повторять)")
106
+ ap.add_argument("--corpus-root", type=Path,
107
+ help="переходный adapter: индекс corpus/clir и packs; без входов — обзор этого корпуса")
108
+ ap.add_argument("--core-kit", type=Path, help="проверенный комплект поддержки ядра")
109
+ ap.add_argument("--report", type=Path, help="markdown-отчёт")
110
+ ap.add_argument("--package", help="имя пакета: печатать все его отказы")
111
+ args = ap.parse_args(argv)
112
+
113
+ try:
114
+ selected, bases = inputs(args.input, args.input_dir, args.corpus_root)
115
+ documents, selected_index, selected_by_name = named_documents(selected)
116
+ extra = [json.loads(path.read_text(encoding="utf-8")) for path in args.dep]
117
+ from inputs import dependency_index
118
+ explicit = dependency_index(extra)
119
+ for name, document in explicit.items():
120
+ if name in selected_by_name and document != selected_by_name[name]:
121
+ raise ValueError(f"survey: неоднозначный CLIR пакета {name}: --dep и входной набор различаются")
122
+ index = P.world_index(args.corpus_root) if args.corpus_root else {}
123
+ # Files named on the command line are a bounded dependency bundle.
124
+ # They supplement the transition adapter and never require discovery.
125
+ index.update(selected_index)
126
+ if args.core_kit:
127
+ import core_support
128
+ core_support.configure_core_kit(args.core_kit)
129
+ core_support.validate_core_kit(args.core_kit)
130
+ except (OSError, ValueError, json.JSONDecodeError, P.Refused) as exc:
131
+ ap.error(str(exc))
132
+
133
+ rows = []
134
+ caps: dict = {}
135
+ for path, ir in documents:
136
+ name = (ir.get("package") or {}).get("name") or path.stem
137
+ try:
138
+ world = P.resolve_world(ir, index, extra) if ir.get("imports") else []
139
+ reasons = P.survey_plan(ir, world)
140
+ except P.Refused as exc:
141
+ reasons = [str(exc)]
142
+ rules = sum(1 for n in ir["nodes"] if n.get("kind") == "rule")
143
+ # Возможности поражаемого слоя (DECISION-0237): у принятого пакета
144
+ # они и есть ответ «L0- или L1-план».
145
+ caps[name] = []
146
+ if not reasons:
147
+ # DECISION-0252: `value:rational` видна только проходом компиляции,
148
+ # как в `build_plan`; без неё план 0.12 читался бы L0-планом.
149
+ probe = P.Builder(ir, world)
150
+ probe.survey()
151
+ caps[name] = P.Builder(ir, world, P.value_caps(probe) or None).capabilities()
152
+ rows.append((name, display_path(path, bases), rules, reasons))
153
+
154
+ if args.package:
155
+ for name, rel, rules, reasons in rows:
156
+ if name == args.package or rel.endswith(args.package):
157
+ print(f"{name} ({rel}): правил {rules}, отказов {len(reasons)}")
158
+ for r in reasons:
159
+ print(" " + r)
160
+ return 0
161
+
162
+ accepted = [r for r in rows if not r[3]]
163
+ by_head = collections.Counter()
164
+ pkg_by_head = collections.Counter()
165
+ for _name, _rel, _rules, reasons in rows:
166
+ by_head.update(head(r) for r in reasons)
167
+ pkg_by_head.update(set(head(r) for r in reasons))
168
+ distinct = sorted(rows, key=lambda r: (len(set(head(x) for x in r[3])), len(r[3])))
169
+ near = [r for r in distinct if r[3] and len(set(head(x) for x in r[3])) == 1]
170
+
171
+ l1 = [r for r in accepted if caps.get(r[0])]
172
+ lines = []
173
+ lines.append(f"снимков рассмотрено: {len(rows)}; план строится у {len(accepted)} "
174
+ f"(L0 {len(accepted) - len(l1)}, L1 {len(l1)})")
175
+ for name, rel, rules, _ in accepted:
176
+ lines.append(f" принят: {name} ({rel}), правил {rules}"
177
+ + (f"; возможности: {', '.join(caps[name])}" if caps.get(name) else ""))
178
+ lines.append("")
179
+ lines.append("причины отказа: пакетов с причиной / вхождений")
180
+ for k, n in pkg_by_head.most_common():
181
+ lines.append(f" {n:4d} / {by_head[k]:5d} {k}")
182
+ detail = collections.defaultdict(collections.Counter)
183
+ for _name, _rel, _rules, reasons in rows:
184
+ for r in reasons:
185
+ if ":" in r:
186
+ detail[head(r)][r.split(":", 1)[1].strip()] += 1
187
+ lines.append("")
188
+ lines.append("уточнения (что именно вне среза), по пакетам:")
189
+ for k in ("объявление вне среза генератора", "тип аргумента вне среза генератора",
190
+ "сила правила вне L0-среза", "тип значения вне среза генератора",
191
+ "узел терма вне среза генератора", "операция вне среза генератора",
192
+ "функция вне среза генератора"):
193
+ pk = collections.Counter()
194
+ for _name, _rel, _rules, reasons in rows:
195
+ pk.update({r.split(":", 1)[1].strip() for r in reasons if head(r) == k and ":" in r})
196
+ lines.append(f" {k}:")
197
+ for det, n in pk.most_common(12):
198
+ lines.append(f" {n:4d} {det}")
199
+ lines.append("")
200
+ lines.append(f"пакеты с ЕДИНСТВЕННЫМ видом причины: {len(near)}")
201
+ for name, rel, rules, reasons in near[:40]:
202
+ lines.append(f" {name}: {head(reasons[0])} × {len(reasons)} (правил {rules})")
203
+ print("\n".join(lines))
204
+
205
+ if args.report:
206
+ # Версия плана следует из возможностей так же, как в `build_plan`.
207
+ def version(name):
208
+ c = set(caps.get(name) or [])
209
+ return (P.PLAN_VERSION_19 if "judgment" in c
210
+ else P.PLAN_VERSION_18 if c & P.FN_CAPS
211
+ else P.PLAN_VERSION_17 if c & P.TERM_CAPS
212
+ else P.PLAN_VERSION_16 if "priority:dated" in c
213
+ else P.PLAN_VERSION_15 if c & P.V15_CAPS
214
+ else P.PLAN_VERSION_14 if c & {"value:round", "value:div_round"}
215
+ else P.PLAN_VERSION_13 if "value:quantity" in c
216
+ else P.PLAN_VERSION_12 if "value:rational" in c else P.PLAN_VERSION_11 if c & P.CAL_CAPS
217
+ else P.PLAN_VERSION_10 if "norm" in c
218
+ else P.PLAN_VERSION_09 if c & {"aggregate", "closure"} else P.PLAN_VERSION_L1 if c
219
+ else P.PLAN_VERSION)
220
+ byv = collections.Counter(version(r[0]) for r in accepted)
221
+ md = ["# Обзор корпуса планом кодогенерации", "",
222
+ f"Снимков рассмотрено: {len(rows)}. План строится у {len(accepted)}: "
223
+ f"`{P.PLAN_VERSION}` (L0) у {byv[P.PLAN_VERSION]}, `{P.PLAN_VERSION_L1}` "
224
+ f"(поражаемый слой, DECISION-0237) у {byv[P.PLAN_VERSION_L1]}, `{P.PLAN_VERSION_09}` "
225
+ f"(агрегаты и замыкания, DECISION-0239) у {byv[P.PLAN_VERSION_09]}, `{P.PLAN_VERSION_10}` "
226
+ f"(нормативные позиции, DECISION-0242) у {byv[P.PLAN_VERSION_10]}, `{P.PLAN_VERSION_11}` "
227
+ f"(календарные функции, DECISION-0245) у {byv[P.PLAN_VERSION_11]}, `{P.PLAN_VERSION_12}` "
228
+ f"(Rational, DECISION-0252) у {byv[P.PLAN_VERSION_12]}, `{P.PLAN_VERSION_13}` "
229
+ f"(Quantity, DECISION-0256) у {byv[P.PLAN_VERSION_13]}, `{P.PLAN_VERSION_14}` "
230
+ f"(round/div_round, DECISION-0259) у {byv[P.PLAN_VERSION_14]}, `{P.PLAN_VERSION_15}` "
231
+ f"(Boolean/Natural, границы месяца, `_`, DECISION-0266–0268) у {byv[P.PLAN_VERSION_15]}, "
232
+ f"`{P.PLAN_VERSION_16}` (датированные приоритеты, DECISION-0269) у {byv[P.PLAN_VERSION_16]}, "
233
+ f"`{P.PLAN_VERSION_17}` (условный терм и вычисляемый аргумент тела, DECISION-0270) у {byv[P.PLAN_VERSION_17]}, "
234
+ f"`{P.PLAN_VERSION_18}` (функции §46, параметры A3, let/match, DECISION-0273) у {byv[P.PLAN_VERSION_18]}, "
235
+ f"`{P.PLAN_VERSION_19}` (канал суждения §47.3, DECISION-0274) у {byv[P.PLAN_VERSION_19]}.",
236
+ "", "## Принятые пакеты", ""]
237
+ md += [f"- `{name}` ({rel}), правил {rules}"
238
+ + (f"; возможности: {', '.join(caps[name])}" if caps.get(name) else "")
239
+ for name, rel, rules, _ in accepted] or ["- нет"]
240
+ capc = collections.Counter(c for r in l1 for c in caps[r[0]])
241
+ md += ["", "## Возможности L1-планов", "", "| возможность | пакетов |", "|---|---:|"]
242
+ md += [f"| `{c}` | {n} |" for c, n in capc.most_common()]
243
+ md += ["", "## Причины отказа", "", "| причина | пакетов | вхождений |", "|---|---:|---:|"]
244
+ md += [f"| {k} | {n} | {by_head[k]} |" for k, n in pkg_by_head.most_common()]
245
+ md += ["", "## Уточнения: что именно вне среза (пакетов)", ""]
246
+ for k in ("объявление вне среза генератора", "тип аргумента вне среза генератора",
247
+ "сила правила вне L0-среза", "тип значения вне среза генератора",
248
+ "узел терма вне среза генератора", "операция вне среза генератора",
249
+ "функция вне среза генератора"):
250
+ pk = collections.Counter()
251
+ for _name, _rel, _rules, reasons in rows:
252
+ pk.update({r.split(":", 1)[1].strip() for r in reasons if head(r) == k and ":" in r})
253
+ md += [f"**{k}**", "", "| что | пакетов |", "|---|---:|"]
254
+ md += [f"| `{det}` | {n} |" for det, n in pk.most_common(12)] + [""]
255
+ md += ["## Пакеты с единственным видом причины", "",
256
+ "Ближайшие кандидаты: устранение одной формы даёт план.", "",
257
+ "| пакет | причина | вхождений | правил |", "|---|---|---:|---:|"]
258
+ md += [f"| `{name}` | {head(reasons[0])} | {len(reasons)} | {rules} |"
259
+ for name, rel, rules, reasons in near]
260
+ md += ["", "## Все пакеты по числу видов причин", "",
261
+ "| пакет | видов | вхождений | правил | причины |", "|---|---:|---:|---:|---|"]
262
+ for name, rel, rules, reasons in distinct:
263
+ kinds = collections.Counter(head(r) for r in reasons)
264
+ md.append(f"| `{name}` | {len(kinds)} | {len(reasons)} | {rules} | "
265
+ + "; ".join(f"{k} × {n}" for k, n in kinds.most_common()) + " |")
266
+ args.report.write_text("\n".join(md) + "\n", encoding="utf-8")
267
+ print(f"отчёт: {args.report}")
268
+ return 0
269
+
270
+
271
+ if __name__ == "__main__":
272
+ raise SystemExit(main())
@@ -0,0 +1,101 @@
1
+ """Target build/run protocol shared by standalone backend consumers.
2
+
3
+ This module deliberately has no CI, corpus, or oracle imports. Target
4
+ descriptors remain the only language-specific boundary.
5
+ """
6
+ from __future__ import annotations
7
+
8
+ import json
9
+ import subprocess
10
+ from typing import Mapping
11
+ from pathlib import Path
12
+
13
+
14
+ REFUSED_EXIT = 3
15
+
16
+
17
+ def shell(command: list[str], cwd: str | Path | None = None,
18
+ env: Mapping[str, str] | None = None) -> subprocess.CompletedProcess:
19
+ """Run a target command; optional environment keeps host toolchains isolated."""
20
+ return subprocess.run(command, capture_output=True, text=True, cwd=cwd, env=env)
21
+
22
+
23
+ def crash_line(stderr: str) -> str:
24
+ lines = [line.strip() for line in stderr.splitlines() if line.strip()]
25
+ if not lines:
26
+ return "(stderr пуст)"
27
+ return next((line[:200] for line in lines if line.startswith("panic:") or "Error:" in line
28
+ or "Error [" in line or "error:" in line), lines[-1][:200])
29
+
30
+
31
+ def build_projects(emit: dict, projects: dict[str, list[Path]], work: Path,
32
+ env: Mapping[str, str] | None = None) -> list[str]:
33
+ """Build every emitted project and return named failures."""
34
+ failures = []
35
+ for language, directories in projects.items():
36
+ if not directories:
37
+ continue
38
+ descriptor = emit[language]
39
+ if hasattr(descriptor, "build_many"):
40
+ command, cwd = descriptor.build_many([str(path) for path in directories], work / "out")
41
+ result = shell(command, cwd, env)
42
+ if result.returncode:
43
+ failures.append(f"сборка {language} отказала\n{(result.stdout + result.stderr)[:1200]}")
44
+ continue
45
+ for directory in directories:
46
+ step = descriptor.build(str(directory))
47
+ if step is None:
48
+ continue
49
+ command, cwd = step
50
+ result = shell(command, cwd, env)
51
+ if result.returncode:
52
+ failures.append(f"сборка {language} отказала на {directory}\n{(result.stdout + result.stderr)[:1200]}")
53
+ return failures
54
+
55
+
56
+ def run_target(descriptor, project: Path, main: str, arguments: list[str],
57
+ env: Mapping[str, str] | None = None) -> tuple[str, object]:
58
+ """Return ``answer`` (decoded JSON), named ``refused``, or ``crash``."""
59
+ command, cwd = descriptor.run(str(project), main)
60
+ result = shell(command + arguments, cwd, env)
61
+ if result.returncode == 0:
62
+ try:
63
+ return "answer", json.loads(result.stdout)
64
+ except json.JSONDecodeError:
65
+ return "crash", f"код 0, но ответ не JSON: {result.stdout[:200]!r}"
66
+ if result.returncode == REFUSED_EXIT:
67
+ try:
68
+ return "refused", json.loads(result.stdout)["refused"]
69
+ except (json.JSONDecodeError, KeyError, TypeError):
70
+ return "crash", f"код {REFUSED_EXIT} без JSON отказа: {result.stdout[:200]!r}"
71
+ return "crash", f"код {result.returncode}: {crash_line(result.stderr or result.stdout)}"
72
+
73
+
74
+ def answer_key(answer: dict) -> tuple:
75
+ """Gate-compatible comparable answer without importing the CI harness."""
76
+ from core_support import canonical_bytes
77
+
78
+ def canon(value) -> str:
79
+ return canonical_bytes(value, document=False).decode("utf-8")
80
+
81
+ def actor(value):
82
+ return "<fallback>" if str(value).startswith("urn:position:") else value
83
+
84
+ if "normativeStatus" in answer:
85
+ positions = []
86
+ for position in answer.get("positions") or []:
87
+ item = {"template": position["template"], "modality": position["modality"],
88
+ "holder": actor(position.get("holder")), "status": position["status"],
89
+ "payload": position["payload"]}
90
+ if "beneficiary" in position:
91
+ item["beneficiary"] = actor(position["beneficiary"])
92
+ positions.append(canon(item))
93
+ supports = sorted(canon([item["status"], item["truthStatus"]]) for item in answer.get("supports") or [])
94
+ head = "NORM_POSITION:" + answer["normativeStatus"] + ":" + canon(sorted(positions)) + ":" + canon(supports)
95
+ elif "truthStatus" in answer:
96
+ head = answer["truthStatus"]
97
+ else:
98
+ head = answer["resultKind"] + ":" + (canon(answer["value"]) if "value" in answer else "")
99
+ requests = [{key: value for key, value in row.items() if key != "proof"}
100
+ for row in answer.get("judgmentRequests") or []]
101
+ return head, answer["evaluationStatus"], tuple(answer.get("issues") or []), canon(requests)