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,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)
|
arxo_codegen/coverage.py
ADDED
|
@@ -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())
|