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,203 @@
1
+ """Pinned core support loaded by code-generation backends.
2
+
3
+ Set ``LAW_CODEGEN_CORE_KIT`` to a directory made by
4
+ ``apps/registry/codegen_core_pack.py``, or call :func:`configure_core_kit`
5
+ before using the helpers. The selected delivery is validated before its code
6
+ is imported. A standalone backend never searches a monorepo for support
7
+ sources.
8
+ """
9
+ from __future__ import annotations
10
+
11
+ import copy
12
+ import hashlib
13
+ import importlib.util
14
+ import json
15
+ import os
16
+ from pathlib import Path
17
+ import sys
18
+ from types import ModuleType
19
+ from typing import Any
20
+
21
+
22
+ HERE = Path(__file__).resolve().parent
23
+ ROOT = HERE.parents[1]
24
+ FORMAT = "law.codegen-core-kit/0.1"
25
+ LOCK_FORMAT = "law.codegen-core-lock/0.1"
26
+ MANIFEST = "manifest.json"
27
+ LOCK = "codegen-core.lock.json"
28
+ COMPATIBILITY = {
29
+ "semanticVersions": ["0.2.11"],
30
+ "languageSemantics": ["law.core/0.2.11"],
31
+ "clirSchemaVersions": ["law.core.ir/0.7"],
32
+ }
33
+ PROVENANCE = {
34
+ "lawref": {"package": "lawref", "version": "0.0.1", "license": "Apache-2.0",
35
+ "requiresPython": ">=3.12", "runtimeDependencies": []},
36
+ "calendarDatasetSchema": {"license": "CC-BY-4.0", "source": "spec/schema/calendar-dataset.schema.json"},
37
+ }
38
+ REQUIRED_PATHS = frozenset({
39
+ "python/lawref/__init__.py", "python/lawref/canon.py", "python/lawref/hashing.py",
40
+ "schemas/calendar-dataset.schema.json", "licenses/Apache-2.0.txt", "licenses/CC-BY-4.0.txt",
41
+ "NOTICE", LOCK,
42
+ })
43
+ _configured_kit: Path | None = None
44
+ _loaded: tuple[Path | None, ModuleType, dict[str, Any]] | None = None
45
+
46
+
47
+ class CoreKitError(RuntimeError):
48
+ """The selected core kit cannot safely provide backend support."""
49
+
50
+
51
+ # Kept as a source-compatible spelling for early B1 callers.
52
+ CodegenCoreError = CoreKitError
53
+
54
+
55
+ def _sha(data: bytes) -> str:
56
+ return "sha256:" + hashlib.sha256(data).hexdigest()
57
+
58
+
59
+ def _regular(path: Path, label: str) -> None:
60
+ if path.is_symlink() or not path.is_file():
61
+ raise CoreKitError(f"{label} is not a regular file")
62
+
63
+
64
+ def validate_core_kit(kit: Path) -> dict[str, Any]:
65
+ """Validate the exact payload before dynamically importing it."""
66
+ supplied = Path(kit)
67
+ if supplied.is_symlink() or not supplied.is_dir():
68
+ raise CoreKitError("codegen core kit is not a directory")
69
+ kit = supplied.resolve()
70
+ for path in kit.rglob("*"):
71
+ if path.is_symlink():
72
+ raise CoreKitError("codegen core kit contains a symlink")
73
+ _regular(kit / MANIFEST, "core-kit manifest")
74
+ _regular(kit / LOCK, "core-kit lock")
75
+ try:
76
+ manifest = json.loads((kit / MANIFEST).read_bytes())
77
+ lock = json.loads((kit / LOCK).read_bytes())
78
+ except (UnicodeDecodeError, json.JSONDecodeError) as error:
79
+ raise CoreKitError("invalid codegen core-kit JSON") from error
80
+ if not isinstance(manifest, dict) or manifest.get("format") != FORMAT:
81
+ raise CoreKitError("unsupported codegen core-kit manifest")
82
+ if not isinstance(lock, dict) or lock.get("format") != LOCK_FORMAT:
83
+ raise CoreKitError("unsupported codegen core-kit lock")
84
+ if lock.get("sourceCommit") != manifest.get("sourceCommit"):
85
+ raise CoreKitError("codegen core-kit source commit mismatch")
86
+ if lock.get("compatibility") != COMPATIBILITY:
87
+ raise CoreKitError("unsupported codegen core-kit compatibility")
88
+ if lock.get("provenance") != PROVENANCE:
89
+ raise CoreKitError("unsupported codegen core-kit provenance")
90
+ files = manifest.get("files")
91
+ if not isinstance(files, list):
92
+ raise CoreKitError("codegen core-kit has no inventory")
93
+ expected = REQUIRED_PATHS
94
+ seen: set[str] = set()
95
+ hashes: dict[str, str] = {}
96
+ for row in files:
97
+ if not isinstance(row, dict):
98
+ raise CoreKitError("invalid codegen core-kit inventory row")
99
+ rel = row.get("path")
100
+ if not isinstance(rel, str) or not rel or rel.startswith("/") or ".." in Path(rel).parts or rel in seen:
101
+ raise CoreKitError("invalid codegen core-kit inventory path")
102
+ seen.add(rel)
103
+ path = kit / rel
104
+ _regular(path, "codegen core-kit payload")
105
+ if row.get("sha256") != _sha(path.read_bytes()) or row.get("bytes") != path.stat().st_size:
106
+ raise CoreKitError("codegen core-kit payload hash mismatch")
107
+ hashes[rel] = row["sha256"]
108
+ actual = {p.relative_to(kit).as_posix() for p in kit.rglob("*") if p.is_file()}
109
+ if actual != seen | {MANIFEST} or seen != expected:
110
+ raise CoreKitError("codegen core-kit inventory differs from files")
111
+ if lock.get("files") != {name: digest for name, digest in hashes.items() if name != LOCK}:
112
+ raise CoreKitError("codegen core-kit lock differs from inventory")
113
+ return manifest
114
+
115
+
116
+ def configure_core_kit(path: str | Path | None) -> None:
117
+ """Explicitly select a core kit; ``None`` restores environment selection."""
118
+ global _configured_kit, _loaded
119
+ _configured_kit = Path(path) if path is not None else None
120
+ _loaded = None
121
+
122
+
123
+ def core_kit_path() -> Path | None:
124
+ """Return the explicit kit selected by API/environment, if any."""
125
+ if _configured_kit is not None:
126
+ return _configured_kit
127
+ value = os.environ.get("LAW_CODEGEN_CORE_KIT")
128
+ return Path(value) if value else None
129
+
130
+
131
+ def using_monorepo_fallback() -> bool:
132
+ """Compatibility spelling; standalone delivery never falls back."""
133
+ return False
134
+
135
+
136
+ def _load_package(package: Path, identity: str) -> ModuleType:
137
+ package_name = f"_law_codegen_core_{identity}"
138
+ hashing_name = package_name + ".hashing"
139
+ existing = sys.modules.get(hashing_name)
140
+ if existing is not None:
141
+ return existing
142
+ package_spec = importlib.util.spec_from_file_location(
143
+ package_name, package / "__init__.py", submodule_search_locations=[str(package)]
144
+ )
145
+ if package_spec is None or package_spec.loader is None:
146
+ raise CoreKitError("cannot load codegen core-kit namespace")
147
+ namespace = importlib.util.module_from_spec(package_spec)
148
+ hashing_spec = importlib.util.spec_from_file_location(hashing_name, package / "hashing.py")
149
+ if hashing_spec is None or hashing_spec.loader is None:
150
+ raise CoreKitError("cannot load codegen core-kit hashing")
151
+ module = importlib.util.module_from_spec(hashing_spec)
152
+ # The delivery is immutable. In particular, importing it must not produce
153
+ # an unlisted ``__pycache__`` directory which would make a later integrity
154
+ # check fail. Restore the interpreter-wide flag even when imported code
155
+ # raises; the small critical section runs only during initial kit loading.
156
+ previous_bytecode_setting = sys.dont_write_bytecode
157
+ sys.dont_write_bytecode = True
158
+ try:
159
+ sys.modules[package_name] = namespace
160
+ package_spec.loader.exec_module(namespace)
161
+ sys.modules[hashing_name] = module
162
+ hashing_spec.loader.exec_module(module)
163
+ except Exception:
164
+ sys.modules.pop(hashing_name, None)
165
+ sys.modules.pop(package_name, None)
166
+ raise
167
+ finally:
168
+ sys.dont_write_bytecode = previous_bytecode_setting
169
+ return module
170
+
171
+
172
+ def _support() -> tuple[ModuleType, dict[str, Any]]:
173
+ global _loaded
174
+ selected = core_kit_path()
175
+ if _loaded is not None and _loaded[0] == selected:
176
+ return _loaded[1], _loaded[2]
177
+ if selected is None:
178
+ raise CoreKitError("explicit LAW_CODEGEN_CORE_KIT or configure_core_kit() is required")
179
+ manifest = validate_core_kit(selected)
180
+ module = _load_package(selected / "python/lawref", hashlib.sha256(
181
+ json.dumps(manifest, sort_keys=True).encode()).hexdigest()[:20]
182
+ )
183
+ schema = json.loads((selected / "schemas/calendar-dataset.schema.json").read_bytes())
184
+ _loaded = (selected, module, schema)
185
+ return module, schema
186
+
187
+
188
+ def semantic_hash(ir: dict) -> str:
189
+ """Core semantic hash (§209) from the selected, validated support kit."""
190
+ module, _ = _support()
191
+ return module.semantic_hash(ir)
192
+
193
+
194
+ def canonical_bytes(value: Any, **kwargs: Any) -> bytes:
195
+ """Core canonical JSON bytes (§208) from the selected support kit."""
196
+ module, _ = _support()
197
+ return module.canonical_bytes(value, **kwargs)
198
+
199
+
200
+ def calendar_schema() -> dict[str, Any]:
201
+ """Return a private copy of the selected calendar dataset schema."""
202
+ _, schema = _support()
203
+ return copy.deepcopy(schema)
@@ -0,0 +1,341 @@
1
+ #!/usr/bin/env python3
2
+ """Реестр покрытия по конструкциям (CODEGEN-COVERAGE-PLAN §1.1, волна W0).
3
+
4
+ Обходит CLIR корпуса, пакетов вне корпуса и conformance-векторов, строит
5
+ план каждого документа (`plan.survey_plan`) и сводит для каждой конструкции
6
+ CLIR — вида узла, поля правила, силы, формы терма, статусного теста,
7
+ агрегата, std-функции, типа аргумента, вида вопроса — четыре числа: в
8
+ скольких снимках она встречается, в скольких из них план построен, в
9
+ скольких векторах она есть и сколько из них совпали с голденом по реестру
10
+ исходов. Статус конструкции выводится из этих чисел и из явных списков:
11
+
12
+ отказ — конструкцию называет причина отказа плана либо раннера;
13
+ принята — есть вектор с конструкцией, совпавший с голденом;
14
+ принята·корпус — вектора нет, но есть снимок корпуса с планом;
15
+ инертна — названа в манифесте инертности `inert-nodes.json`
16
+ (пока пуст: инертность доказывается вектором, §1.1);
17
+ вне профиля — аналитика и `core-auditable` (§1.3–§1.4);
18
+ не покрыта — встречается только в отвергнутых снимках, отказ её не
19
+ называет: атрибуция неизвестна.
20
+
21
+ Реестр — не голден: он пересобирается генератором и описывает состояние,
22
+ а не ожидание. Ожидания живут в `active-outcomes.json` и в манифестах ворот.
23
+ """
24
+ from __future__ import annotations
25
+
26
+ import argparse
27
+ import collections
28
+ import datetime
29
+ import json
30
+ import sys
31
+ from pathlib import Path
32
+
33
+ HERE = Path(__file__).resolve().parent
34
+ sys.path.insert(0, str(HERE))
35
+ import plan as P # noqa: E402
36
+
37
+ FORMAT = "law.codegen-coverage/0.1"
38
+
39
+ # §1.4 плана: ответ не входит в исполняемый профиль без отдельного решения.
40
+ OUTSIDE_PROFILE = {
41
+ "query:argumentation": "аналитика над proof-графом (DECISION-0188 п. 1)",
42
+ "query:precedent": "аналитика §… прецедентов, читает proof",
43
+ "query:interpretation_analysis": "аналитика прочтений §156",
44
+ "node:precedent_decl": "данные прецедентов для аналитики",
45
+ "node:counterfactual_decl": "контрфактический поиск §186 — аналитика",
46
+ "node:evidence_policy": "политика доказательств §… — L3",
47
+ "node:temporal_resolution_policy": "политика разрешения моментов — вне среза значений",
48
+ }
49
+
50
+ # Причина отказа → конструкция, которую она называет.
51
+ REASON_PREFIXES = (
52
+ ("объявление вне среза генератора: ", "node:"),
53
+ ("тип аргумента вне среза генератора: ", "type:"),
54
+ ("тип значения вне среза генератора: ", "type:"),
55
+ ("операция вне среза генератора: ", "op:"),
56
+ ("функция вне среза генератора: ", "fn:"),
57
+ ("узел терма вне среза генератора: ", "term:"),
58
+ ("статусный тест вне среза генератора: ", "status:"),
59
+ ("статусный тест вне L0-среза: ", "status:"),
60
+ ("поле правила вне среза генератора: ", "rulefield:"),
61
+ ("сила правила вне среза генератора: ", "strength:"),
62
+ ("сила правила вне L0-среза: ", "strength:"),
63
+ ("агрегат вне среза генератора: ", "agg:"),
64
+ ("вид запроса вне среза раннера: ", "query:"),
65
+ )
66
+
67
+
68
+ def construct_of(reason: str) -> str:
69
+ """Конструкция, названная причиной, либо сама причина как структурный отказ."""
70
+ for prefix, family in REASON_PREFIXES:
71
+ if reason.startswith(prefix):
72
+ value = reason[len(prefix):].strip()
73
+ if family == "node:" and value.endswith("/None"):
74
+ value = value[:-5]
75
+ if family == "type:":
76
+ return "type:" + value if value.startswith("urn:law:std#") else "type:<foreign-entity>"
77
+ if family == "fn:":
78
+ return fn_construct(value)
79
+ return family + value
80
+ head = reason.split(":", 1)[0].strip()
81
+ return "reason:" + head
82
+
83
+
84
+ def type_construct(name: str, own: str) -> str:
85
+ """Типы std — поимённо; сущности — своего пакета либо чужого (мир §92.3)."""
86
+ if name.startswith("urn:law:std#"):
87
+ return "type:" + name
88
+ return "type:<own-entity>" if name.split("#", 1)[0] == own else "type:<foreign-entity>"
89
+
90
+
91
+ def fn_construct(name: str) -> str:
92
+ """std-функции — поимённо; функции пакета и параметры A3 — родами."""
93
+ if name.startswith("urn:law:std#"):
94
+ return "fn:" + name
95
+ return "fn:<a3-parameter>" if "/value/" in name else "fn:<package-function>"
96
+
97
+
98
+ def inventory(ir: dict) -> set[str]:
99
+ """Конструкции CLIR документа."""
100
+ out: set[str] = set()
101
+ own = (ir.get("package") or {}).get("namespace") or ""
102
+
103
+ def terms(node, in_type=False):
104
+ if isinstance(node, dict):
105
+ k = node.get("kind")
106
+ if k and not in_type:
107
+ out.add("term:" + k)
108
+ if k == "status":
109
+ out.add("status:" + str(node.get("status")))
110
+ elif k == "aggregate":
111
+ out.add("agg:" + str(node.get("op")))
112
+ elif k == "call":
113
+ out.add(fn_construct(str(node.get("function"))))
114
+ elif k == "value":
115
+ t = (node.get("type") or {}).get("name")
116
+ if t:
117
+ out.add(type_construct(t, own))
118
+ for key, value in node.items():
119
+ terms(value, in_type or key == "type")
120
+ elif isinstance(node, list):
121
+ for value in node:
122
+ terms(value, in_type)
123
+
124
+ for n in ir.get("nodes", []):
125
+ kind = n.get("kind")
126
+ sub = n.get("symbolKind")
127
+ out.add("node:" + (f"{kind}/{sub}" if sub else str(kind)))
128
+ if kind == "rule":
129
+ out.update("rulefield:" + f for f in n if f not in P.RULE_META)
130
+ out.add("strength:" + str(n.get("strength")))
131
+ for v in n.get("variables") or []:
132
+ t = (v.get("type") or {}).get("name")
133
+ if t:
134
+ out.add(type_construct(t, own))
135
+ terms(n.get("body"))
136
+ terms(n.get("head"))
137
+ terms(n.get("scope"))
138
+ elif kind == "symbol_decl":
139
+ for p in n.get("params") or n.get("parameters") or []:
140
+ t = (p.get("type") or p).get("name") if isinstance(p, dict) else None
141
+ if t:
142
+ out.add(type_construct(t, own))
143
+ elif kind == "assertion":
144
+ terms(n.get("formula") or n.get("literal"))
145
+ elif kind in ("norm_template", "priority_rule", "closure_policy", "constraint", "function_decl"):
146
+ terms({k: v for k, v in n.items() if k not in ("kind", "id", "package", "labels", "anchors",
147
+ "provenance", "contentHash")})
148
+ return out
149
+
150
+
151
+ def survey(ir: dict, world: list) -> list[str]:
152
+ try:
153
+ return P.survey_plan(ir, world)
154
+ except P.Refused as exc:
155
+ return [str(exc)]
156
+ except Exception as exc: # крах плана — тоже исход, но не отказ
157
+ return ["крах плана: " + type(exc).__name__ + ": " + str(exc)[:120]]
158
+
159
+
160
+ def corpus_documents(root: Path):
161
+ paths = sorted((root / "corpus" / "clir").glob("*.lawir.json"))
162
+ for p in sorted((root / "packs").rglob("*.lawir.json")):
163
+ if "deps" not in p.parts and ".build" not in p.parts:
164
+ paths.append(p)
165
+ for path in paths:
166
+ yield path, json.loads(path.read_text(encoding="utf-8"))
167
+
168
+
169
+ def vector_documents(root: Path):
170
+ for meta_path in sorted((root / "verify" / "vectors").rglob("vector.json")):
171
+ d = meta_path.parent
172
+ if not (d / "ir.json").is_file():
173
+ continue
174
+ meta = json.loads(meta_path.read_text(encoding="utf-8"))
175
+ ir = json.loads((d / "ir.json").read_text(encoding="utf-8"))
176
+ query = json.loads((d / "query.json").read_text(encoding="utf-8")) if (d / "query.json").is_file() else {}
177
+ producer = d / "producer.ir.json"
178
+ world = [json.loads(producer.read_text(encoding="utf-8"))] if producer.is_file() else []
179
+ yield d, meta, ir, query, world
180
+
181
+
182
+ def main(argv=None) -> int:
183
+ ap = argparse.ArgumentParser(description=__doc__.split("\n\n")[0])
184
+ ap.add_argument("--root", type=Path, default=HERE.parent.parent, help="checkout law-dsl")
185
+ ap.add_argument("--outcomes", type=Path, default=HERE / "active-outcomes.json")
186
+ ap.add_argument("--inert", type=Path, default=HERE / "inert-nodes.json")
187
+ ap.add_argument("--json", type=Path, default=HERE / "coverage.json")
188
+ ap.add_argument("--markdown", type=Path, default=HERE / "coverage.md")
189
+ ap.add_argument("--wheel", default="")
190
+ ap.add_argument("--from-json", action="store_true", help="перерисовать статусы и markdown из готового coverage.json")
191
+ args = ap.parse_args(argv)
192
+ root = args.root.resolve()
193
+ if args.from_json:
194
+ data = json.loads(args.json.read_text(encoding="utf-8"))
195
+ stats = {c["id"]: {k: c[k] for k in ("packages", "packagesPlanned", "packagesNamedByRefusal",
196
+ "vectors", "vectorsMatched", "vectorsNamedByRefusal", "vectorsWithdrawn")}
197
+ for c in data["constructs"]}
198
+ return finish(args, stats, data["packages"], data["vectors"], data["summary"].get("wheel", args.wheel))
199
+ try:
200
+ import core_support
201
+ kit = root / "vendor" / "codegen" / "core-kit"
202
+ if kit.is_dir():
203
+ core_support.configure_core_kit(kit)
204
+ except Exception:
205
+ pass
206
+
207
+ outcomes = json.loads(args.outcomes.read_text(encoding="utf-8")).get("vectors", {}) if args.outcomes.is_file() else {}
208
+ index = P.world_index(root)
209
+ stats: dict[str, dict] = collections.defaultdict(lambda: {
210
+ "packages": 0, "packagesPlanned": 0, "packagesNamedByRefusal": 0,
211
+ "vectors": 0, "vectorsMatched": 0, "vectorsNamedByRefusal": 0, "vectorsWithdrawn": 0,
212
+ })
213
+ packages, vectors = [], []
214
+
215
+ for path, ir in corpus_documents(root):
216
+ name = (ir.get("package") or {}).get("name") or path.stem
217
+ try:
218
+ world = P.resolve_world(ir, index, []) if ir.get("imports") else []
219
+ except Exception as exc:
220
+ world, pre = [], ["мир не разрешён: " + str(exc)[:120]]
221
+ else:
222
+ pre = []
223
+ reasons = pre or survey(ir, world)
224
+ named = {construct_of(r) for r in reasons}
225
+ present = inventory(ir)
226
+ for c in present | {c for c in named if c.startswith("reason:")}:
227
+ s = stats[c]
228
+ if c in present:
229
+ s["packages"] += 1
230
+ if not reasons:
231
+ s["packagesPlanned"] += 1
232
+ if c in named:
233
+ s["packagesNamedByRefusal"] += 1
234
+ packages.append({"name": name, "path": str(path.relative_to(root)), "planned": not reasons,
235
+ "rules": sum(1 for n in ir.get("nodes", []) if n.get("kind") == "rule"),
236
+ "reasons": sorted(collections.Counter(construct_of(r) for r in reasons).items())})
237
+
238
+ for d, meta, ir, query, world in vector_documents(root):
239
+ vid = meta.get("id") or d.name
240
+ outcome = outcomes.get(vid)
241
+ if isinstance(outcome, dict): # реестр формата 0.3: пин — объект с хэшем входов
242
+ outcome = outcome.get("outcome")
243
+ withdrawn = str(ir.get("semanticVersion")) == "0.1.1"
244
+ present = inventory(ir)
245
+ qk = query.get("kind")
246
+ if qk:
247
+ present.add("query:" + qk)
248
+ reasons = [] if withdrawn else survey(ir, world)
249
+ named = {construct_of(r) for r in reasons}
250
+ if isinstance(outcome, str):
251
+ if outcome.startswith("query: "):
252
+ named.add("query:" + outcome[7:].strip())
253
+ elif outcome.startswith("runtime: "):
254
+ named.add(construct_of(outcome[9:]))
255
+ matched = outcome == "match"
256
+ for c in present | {c for c in named if c.startswith("reason:")}:
257
+ s = stats[c]
258
+ if c in present:
259
+ s["vectors"] += 1
260
+ if withdrawn:
261
+ s["vectorsWithdrawn"] += 1
262
+ if matched:
263
+ s["vectorsMatched"] += 1
264
+ if c in named:
265
+ s["vectorsNamedByRefusal"] += 1
266
+ vectors.append({"id": vid, "layer": meta.get("layer"), "query": qk,
267
+ "semanticVersion": ir.get("semanticVersion"),
268
+ "outcome": outcome if isinstance(outcome, str) else ("withdrawn" if withdrawn else "unpinned"),
269
+ "planRefusals": sorted(named)})
270
+
271
+ return finish(args, stats, packages, vectors, args.wheel)
272
+
273
+
274
+ def finish(args, stats, packages, vectors, wheel) -> int:
275
+ inert = json.loads(args.inert.read_text(encoding="utf-8")) if args.inert.is_file() else {"constructs": {}}
276
+
277
+ def status(c: str, s: dict) -> str:
278
+ if c in inert.get("constructs", {}):
279
+ return "инертна"
280
+ if c in OUTSIDE_PROFILE:
281
+ return "вне профиля"
282
+ named = s["packagesNamedByRefusal"] or s["vectorsNamedByRefusal"]
283
+ if named and (s["vectorsMatched"] or s["packagesPlanned"]):
284
+ # Конструкция и принята, и названа отказом: срез зависит от формы
285
+ # или значения (пустое/непустое поле, тип слота, вид эффекта).
286
+ return "частично"
287
+ if named or c.startswith("reason:"):
288
+ return "отказ"
289
+ if s["vectorsMatched"]:
290
+ return "принята"
291
+ if s["packagesPlanned"]:
292
+ return "принята·корпус"
293
+ return "не покрыта"
294
+
295
+ constructs = []
296
+ for c in sorted(stats):
297
+ s = stats[c]
298
+ constructs.append({"id": c, "family": c.split(":", 1)[0], "status": status(c, s), **s,
299
+ **({"note": OUTSIDE_PROFILE[c]} if c in OUTSIDE_PROFILE else {})})
300
+ families = collections.defaultdict(collections.Counter)
301
+ for c in constructs:
302
+ families[c["family"]][c["status"]] += 1
303
+ planned = sum(1 for p in packages if p["planned"])
304
+ active = [v for v in vectors if v["outcome"] not in ("withdrawn", "unpinned")]
305
+ summary = {
306
+ "format": FORMAT, "date": datetime.date.today().isoformat(), "wheel": wheel, # determinism: allow(отчёт о состоянии, не артефакт семантики)
307
+ "packages": len(packages), "packagesPlanned": planned,
308
+ "vectors": len(vectors), "vectorsActive": len(active),
309
+ "vectorsMatched": sum(1 for v in active if v["outcome"] == "match"),
310
+ "vectorsWithdrawn": sum(1 for v in vectors if v["outcome"] == "withdrawn"),
311
+ "vectorsUnpinned": sum(1 for v in vectors if v["outcome"] == "unpinned"),
312
+ "constructs": len(constructs),
313
+ "byFamily": {f: dict(c) for f, c in sorted(families.items())},
314
+ }
315
+ args.json.write_text(json.dumps({"summary": summary, "constructs": constructs, "packages": packages,
316
+ "vectors": vectors}, ensure_ascii=False, indent=1) + "\n", encoding="utf-8")
317
+
318
+ md = ["# Реестр покрытия по конструкциям", "",
319
+ f"Собрано `coverage.py` {summary['date']}, wheel `{wheel or '?'}`. "
320
+ f"Снимков {summary['packages']}, с планом {planned}. Векторов с CLIR {summary['vectors']}: "
321
+ f"активных {summary['vectorsActive']}, совпало {summary['vectorsMatched']}, отозвано (0.1.1) "
322
+ f"{summary['vectorsWithdrawn']}, вне реестра {summary['vectorsUnpinned']}.", "",
323
+ "| семейство | " + " | ".join(("принята", "принята·корпус", "частично", "отказ", "инертна", "вне профиля", "не покрыта")) + " |",
324
+ "|---|---:|---:|---:|---:|---:|---:|---:|"]
325
+ for f, c in sorted(families.items()):
326
+ md.append(f"| `{f}` | " + " | ".join(str(c.get(k, 0)) for k in
327
+ ("принята", "принята·корпус", "частично", "отказ", "инертна", "вне профиля", "не покрыта")) + " |")
328
+ md += ["", "| конструкция | статус | снимков | с планом | назван отказом | векторов | совпало | назван отказом | отозвано |",
329
+ "|---|---|---:|---:|---:|---:|---:|---:|---:|"]
330
+ order = {"отказ": 0, "частично": 1, "не покрыта": 2, "принята·корпус": 3, "принята": 4, "инертна": 5, "вне профиля": 6}
331
+ for c in sorted(constructs, key=lambda c: (order[c["status"]], -c["packages"], c["id"])):
332
+ md.append(f"| `{c['id']}` | {c['status']} | {c['packages']} | {c['packagesPlanned']} | "
333
+ f"{c['packagesNamedByRefusal']} | {c['vectors']} | {c['vectorsMatched']} | "
334
+ f"{c['vectorsNamedByRefusal']} | {c['vectorsWithdrawn']} |")
335
+ args.markdown.write_text("\n".join(md) + "\n", encoding="utf-8")
336
+ print(json.dumps(summary, ensure_ascii=False, indent=1))
337
+ return 0
338
+
339
+
340
+ if __name__ == "__main__":
341
+ raise SystemExit(main())