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,463 @@
|
|
|
1
|
+
"""Фикстуры поражаемого слоя для ворот кодогенерации (DECISION-0237, этап 4).
|
|
2
|
+
|
|
3
|
+
Векторы L1 держат совпадение с голденом, но не держат РЕШАЮЩУЮ норму: модуль,
|
|
4
|
+
который потерял приоритет или дефитер, на части векторов всё ещё отвечал бы
|
|
5
|
+
верно. Поэтому у каждой фикстуры с ответом есть канарейка — та же программа
|
|
6
|
+
без решающего узла. Ожидание канарейки записано из прозы, как и ожидание
|
|
7
|
+
фикстуры, и обязано ОТЛИЧАТЬСЯ от него: если снятие нормы ответа не меняет,
|
|
8
|
+
ворота этой нормы не видят, и канарейка валит ворота.
|
|
9
|
+
|
|
10
|
+
Каждая фикстура исполняется ещё и в перестановке — правила, приоритеты,
|
|
11
|
+
факты пакета и дела в обратном порядке: ответ не зависит от порядка
|
|
12
|
+
кандидатов (DECISION-0237 п. 10).
|
|
13
|
+
|
|
14
|
+
Структурные фикстуры ответа не несут: план обязан отказать ровно названной
|
|
15
|
+
причиной (§110, §111) либо построиться (монотонная рекурсия §103).
|
|
16
|
+
|
|
17
|
+
Ожидания записаны из SPEC; сверка с оракулом 14.09.2026 — в DECISION-0237,
|
|
18
|
+
раздел «Исполнение»: у позднего производителя через строгую цепочку оракул
|
|
19
|
+
отвечает не по прозе, срез отказывает.
|
|
20
|
+
|
|
21
|
+
Язык описания: литерал `p(X, a)` — переменная с заглавной, сущность
|
|
22
|
+
`urn:entity:<имя>` со строчной; `!p(X)` — отрицательная полярность;
|
|
23
|
+
`not_known[p(X)]` и другие статусы §65 — статусный конъюнкт; `&` — конъюнкция.
|
|
24
|
+
Число в аргументе — Decimal. Агрегат §59 в гарде (DECISION-0239):
|
|
25
|
+
`count{Y | p(X, Y); q(Y)} >= 2` — элемент, генератор через `;`, сравнение,
|
|
26
|
+
порог-Integer; `sum{V | fee(X, V)} >= 0` — порог-Decimal. У предиката арность
|
|
27
|
+
либо список типов (`["Text", "Decimal"]`); `closures` — (имя, предикат, домен).
|
|
28
|
+
Ожидание `issues` — коды диагностик среза, сверяются вместе со статусом.
|
|
29
|
+
|
|
30
|
+
Календарь (DECISION-0245): `@2026-01-31` — Date, `5 business_day` —
|
|
31
|
+
Quantity-счётчик, `add_business_days(D, 5 business_day)` — вызов std;
|
|
32
|
+
сравнение термов `F > add_business_days(D, 3 business_day)` — гард.
|
|
33
|
+
`calendar` — датасет снимка §85 (узел и байты ресурса), `policy` — политика
|
|
34
|
+
§86: узел `deadline_policy` программы, а контекст дела несёт только его
|
|
35
|
+
StableId — `DeadlinePolicyRef` 0.2 (DECISION-0111 §2.9). Канарейка снимает
|
|
36
|
+
`Overrides` (исключения календаря) или `Policy` (ссылку дела на политику) так
|
|
37
|
+
же, как правило.
|
|
38
|
+
"""
|
|
39
|
+
from __future__ import annotations
|
|
40
|
+
|
|
41
|
+
import hashlib
|
|
42
|
+
import json
|
|
43
|
+
import re
|
|
44
|
+
|
|
45
|
+
NS = "urn:law:backends:l1"
|
|
46
|
+
|
|
47
|
+
_WEEK = [{"day": d, "working": d not in ("saturday", "sunday"),
|
|
48
|
+
"intervals": [{"start": "09:00:00", "end": "18:00:00"}] if d not in ("saturday", "sunday") else []}
|
|
49
|
+
for d in ("monday", "tuesday", "wednesday", "thursday", "friday", "saturday", "sunday")]
|
|
50
|
+
CAL_2026 = {"schemaVersion": "law.calendar/0.1", "id": "urn:law:backends:calendar:2026", "timezone": "Asia/Qyzylorda",
|
|
51
|
+
"timezoneDbSnapshot": "iana-tzdb@2026a", "valid": {"start": "2026-01-01", "endExclusive": "2027-01-01"},
|
|
52
|
+
"weeklySchedule": _WEEK, "sourceArtifacts": ["urn:law:backends:calendar-source:2026"],
|
|
53
|
+
"overrides": [{"date": "2026-03-09", "working": False, "intervals": [], "reason": "перенос выходного"}]}
|
|
54
|
+
POLICY = {"start_count": "next_day", "include_end": True, "roll": "no_roll"}
|
|
55
|
+
|
|
56
|
+
CAL_FIXTURES = [
|
|
57
|
+
# --- календарные функции §84–§86 (DECISION-0245) ----------------------
|
|
58
|
+
{"name": "business-days-holiday", "basis": "§85.1/§86: нерабочий день исключения не считается днём срока",
|
|
59
|
+
"preds": {"got": ["Text", "Date"], "filed": ["Text", "Date"], "late": 1},
|
|
60
|
+
"rules": [("Late", "strict", "late(X)", "got(X, D) & filed(X, F) & F > add_business_days(D, 3 business_day)")],
|
|
61
|
+
"calendar": CAL_2026, "policy": POLICY,
|
|
62
|
+
"case": ["got(a, @2026-03-05)", "filed(a, @2026-03-11)"], "query": "late(a)", "expect": "NEITHER",
|
|
63
|
+
"canary": {"drop": ["Overrides"], "expect": "TRUE_ONLY"}},
|
|
64
|
+
{"name": "calendar-month-end", "basis": "§84.2/§86 (E-0026): 31 января + месяц — последний день февраля",
|
|
65
|
+
"preds": {"start": ["Text", "Date"], "expires": ["Text", "Date"]},
|
|
66
|
+
"rules": [("Expire", "strict", "expires(X, add_calendar_period(D, 1 calendar_month))", "start(X, D)")],
|
|
67
|
+
"policy": {"start_count": "same_day", "include_end": True, "roll": "no_roll", "month_end": "last_day_of_month"},
|
|
68
|
+
"case": ["start(a, @2026-01-31)"], "query": "expires(a, @2026-02-28)", "expect": "TRUE_ONLY",
|
|
69
|
+
"canary": {"drop": ["Policy"], "expect": "NEITHER", "issues": ["MISSING_POLICY"]}},
|
|
70
|
+
{"name": "guard-out-of-range", "basis": "§85.1 (T040): срок за покрытием снимка — CALENDAR_OUT_OF_RANGE, "
|
|
71
|
+
"правило не срабатывает",
|
|
72
|
+
"preds": {"got": ["Text", "Date"], "filed": ["Text", "Date"], "late": 1},
|
|
73
|
+
"rules": [("Late", "strict", "late(X)", "got(X, D) & filed(X, F) & F > add_business_days(D, 5 business_day)")],
|
|
74
|
+
"calendar": CAL_2026, "policy": POLICY,
|
|
75
|
+
"case": ["got(a, @2026-12-28)", "filed(a, @2027-01-20)"], "query": "late(a)", "expect": "NEITHER",
|
|
76
|
+
"issues": ["CALENDAR_OUT_OF_RANGE"],
|
|
77
|
+
"canary": {"drop": ["Policy"], "expect": "NEITHER", "issues": ["MISSING_POLICY"]}},
|
|
78
|
+
{"name": "days-between-l0", "basis": "§84.4 (E-0015): календарные дни между датами, модель L0",
|
|
79
|
+
"preds": {"pair": ["Text", "Date", "Date"], "long": 1},
|
|
80
|
+
"rules": [("Long", "strict", "long(X)", "pair(X, A, B) & days_between(A, B) >= 30")],
|
|
81
|
+
"case": ["pair(a, @2026-01-01, @2026-02-15)"], "query": "long(a)", "expect": "TRUE_ONLY",
|
|
82
|
+
"canary": {"drop": ["Long"], "expect": "NEITHER"}},
|
|
83
|
+
{"name": "window-calendar-bound", "basis": "§135/§84.2: граница окна обязанности — календарный терм; "
|
|
84
|
+
"без политики граница неизвестна (issue)",
|
|
85
|
+
"preds": {"owes": ["Text", "Date"], "paid": 1},
|
|
86
|
+
"templates": [("Pay", "duty", {"kind": "duty", "bearer": "X", "goal": {
|
|
87
|
+
"kind": "achievement", "condition": "paid(X)",
|
|
88
|
+
"window": ["D", "add_calendar_period(D, 1 calendar_month)"]}})],
|
|
89
|
+
"rules": [("MakePay", "strict", "norm:Pay", "owes(X, D)")],
|
|
90
|
+
"policy": {"start_count": "same_day", "include_end": True, "roll": "no_roll", "month_end": "last_day_of_month"},
|
|
91
|
+
"case": ["owes(a, @2026-08-01)"], "query": "positions", "expect": "UNDETERMINED",
|
|
92
|
+
"canary": {"drop": ["Policy"], "expect": "ACTIVE", "issues": ["MISSING_POLICY"]}},
|
|
93
|
+
]
|
|
94
|
+
|
|
95
|
+
L2_FIXTURES = [
|
|
96
|
+
# --- нормативные позиции L2 (DECISION-0242) ---------------------------
|
|
97
|
+
{"name": "duty-satisfied", "basis": "§136: условие цели TRUE_ONLY — обязанность SATISFIED",
|
|
98
|
+
"preds": {"owes": 1, "receipt": 1, "paid": 1},
|
|
99
|
+
"templates": [("Pay", "duty", {"kind": "duty", "bearer": "X", "goal": {
|
|
100
|
+
"kind": "achievement", "condition": "paid(X)", "window": ["2026-01-01", "2026-12-31"]}})],
|
|
101
|
+
"rules": [("MakePay", "strict", "norm:Pay", "owes(X)"), ("Paid", "strict", "paid(X)", "receipt(X)")],
|
|
102
|
+
"case": ["owes(a)", "receipt(a)"], "query": "positions", "expect": "SATISFIED",
|
|
103
|
+
"canary": {"drop": ["Paid"], "expect": "ACTIVE"}},
|
|
104
|
+
{"name": "duty-violated-by-closure", "basis": "§136.3/§70: после окна явный негатив замыкания — VIOLATED",
|
|
105
|
+
"preds": {"owes": 1, "debtor": 1, "paid": 1},
|
|
106
|
+
"templates": [("Pay", "duty", {"kind": "duty", "bearer": "X", "goal": {
|
|
107
|
+
"kind": "achievement", "condition": "paid(X)", "window": ["2026-01-01", "2026-06-30"]}})],
|
|
108
|
+
"rules": [("MakePay", "strict", "norm:Pay", "owes(X)")],
|
|
109
|
+
"closures": [("PaidRegistry", "paid", "debtor")],
|
|
110
|
+
"case": ["owes(a)", "debtor(a)"], "query": "positions", "expect": "VIOLATED",
|
|
111
|
+
"canary": {"drop": ["PaidRegistry"], "expect": "UNDETERMINED"}},
|
|
112
|
+
{"name": "maintenance-completeness", "basis": "§137: закрытие покрывает окно — maintenance SATISFIED",
|
|
113
|
+
"preds": {"holder": 1, "insured": 1, "registry": 1},
|
|
114
|
+
"templates": [("Insure", "duty", {"kind": "duty", "bearer": "X", "goal": {
|
|
115
|
+
"kind": "maintenance", "condition": "insured(X)", "window": ["2026-01-01", "2026-06-30"]}})],
|
|
116
|
+
"rules": [("MakeInsure", "strict", "norm:Insure", "holder(X)")],
|
|
117
|
+
"closures": [("InsuredRegistry", "insured", "registry")],
|
|
118
|
+
"case": ["holder(a)", "insured(a)", "registry(a)"], "query": "positions", "expect": "SATISFIED",
|
|
119
|
+
"canary": {"drop": ["InsuredRegistry"], "expect": "UNDETERMINED"}},
|
|
120
|
+
{"name": "power-effect", "basis": "§127: действительное осуществление полномочия создаёт эффект",
|
|
121
|
+
"preds": {"minister": 1, "appoint_act": 1, "signed": 1, "authorised": 1, "appointed": 1},
|
|
122
|
+
"templates": [("Appoint", "power", {"kind": "power", "holder": "X", "exercise": "appoint_act(X)",
|
|
123
|
+
"validWhen": "authorised(X)", "effect": {"kind": "create",
|
|
124
|
+
"after": "appointed(X)"}})],
|
|
125
|
+
"rules": [("MakeAppoint", "strict", "norm:Appoint", "minister(X)"),
|
|
126
|
+
("Auth", "strict", "authorised(X)", "signed(X)")],
|
|
127
|
+
"case": ["minister(a)", "appoint_act(a)", "signed(a)"], "query": "appointed(a)", "expect": "TRUE_ONLY",
|
|
128
|
+
"canary": {"drop": ["Auth"], "expect": "NEITHER", "issues": ["POWER_INVALID_EXERCISE"]}},
|
|
129
|
+
{"name": "immunity-blocks", "basis": "§128/§106: иммунитет старшего правила блокирует эффект",
|
|
130
|
+
"preds": {"minister": 1, "appoint_act": 1, "authorised": 1, "appointed": 1, "protected": 1},
|
|
131
|
+
"templates": [("Appoint", "power", {"kind": "power", "holder": "X", "exercise": "appoint_act(X)",
|
|
132
|
+
"validWhen": "authorised(X)", "effect": {"kind": "create",
|
|
133
|
+
"after": "appointed(X)"}}),
|
|
134
|
+
("Protect", "immunity", {"kind": "immunity", "holder": "X",
|
|
135
|
+
"protectedEffect": {"after": "appointed(X)"}})],
|
|
136
|
+
"rules": [("MakeAppoint", "strict", "norm:Appoint", "minister(X)"),
|
|
137
|
+
("MakeProtect", "strict", "norm:Protect", "protected(X)")],
|
|
138
|
+
"priorities": [("MakeProtect", "MakeAppoint")],
|
|
139
|
+
"case": ["minister(a)", "appoint_act(a)", "authorised(a)", "protected(a)"], "query": "appointed(a)",
|
|
140
|
+
"expect": "NEITHER", "issues": ["EFFECT_BLOCKED_BY_IMMUNITY"],
|
|
141
|
+
"canary": {"drop": ["prio0"], "expect": "NEITHER", "issues": ["UNRESOLVED_NORMATIVE_CONFLICT"]}},
|
|
142
|
+
{"name": "l2-feedback", "basis": "DECISION-0242 п. 1: норм-правило читает эффект полномочия",
|
|
143
|
+
"preds": {"minister": 1, "appoint_act": 1, "authorised": 1, "appointed": 1},
|
|
144
|
+
"templates": [("Appoint", "power", {"kind": "power", "holder": "X", "exercise": "appoint_act(X)",
|
|
145
|
+
"validWhen": "authorised(X)", "effect": {"kind": "create",
|
|
146
|
+
"after": "appointed(X)"}}),
|
|
147
|
+
("Protect", "immunity", {"kind": "immunity", "holder": "X",
|
|
148
|
+
"protectedEffect": {"after": "appointed(X)"}})],
|
|
149
|
+
"rules": [("MakeAppoint", "strict", "norm:Appoint", "minister(X)"),
|
|
150
|
+
("MakeProtect", "strict", "norm:Protect", "appointed(X)")],
|
|
151
|
+
"plan": "обратная связь L2 вне среза генератора (DECISION-0242 п. 1)"},
|
|
152
|
+
{"name": "norm-defeater", "basis": "DECISION-0242 K6: дефитер норм-правила вне среза",
|
|
153
|
+
"preds": {"owes": 1, "paid": 1, "exempt": 1},
|
|
154
|
+
"templates": [("Pay", "duty", {"kind": "duty", "bearer": "X", "goal": {
|
|
155
|
+
"kind": "achievement", "condition": "paid(X)", "window": ["2026-01-01", "2026-12-31"]}})],
|
|
156
|
+
"rules": [("MakePay", "strict", "norm:Pay", "owes(X)"), ("Exempt", "defeater", "norm:Pay", "exempt(X)")],
|
|
157
|
+
"plan": "дефитер норм-правила вне среза генератора (DECISION-0242 K6)"},
|
|
158
|
+
]
|
|
159
|
+
|
|
160
|
+
FIXTURES = [
|
|
161
|
+
{"name": "both-survive", "basis": "§108/§112: несравнимые кандидаты обеих сторон выживают — BOTH",
|
|
162
|
+
"preds": {"p": 1, "q": 1},
|
|
163
|
+
"rules": [("D1", "defeasible", "q(X)", "p(X)"), ("D2", "defeasible", "!q(X)", "p(X)")],
|
|
164
|
+
"case": ["p(a)"], "query": "q(a)", "expect": "BOTH",
|
|
165
|
+
"canary": {"drop": ["D2"], "expect": "TRUE_ONLY"}},
|
|
166
|
+
{"name": "strict-over-defeasible", "basis": "§107.1: strict-опора комплемента побеждает defeasible",
|
|
167
|
+
"preds": {"p": 1, "r": 1, "q": 1},
|
|
168
|
+
"rules": [("D", "defeasible", "q(X)", "p(X)"), ("S", "strict", "!q(X)", "r(X)")],
|
|
169
|
+
"case": ["p(a)", "r(a)"], "query": "q(a)", "expect": "FALSE_ONLY",
|
|
170
|
+
"canary": {"drop": ["S"], "expect": "TRUE_ONLY"}},
|
|
171
|
+
{"name": "defeater-overcome", "basis": "§107.3: более сильное правило преодолевает дефитер",
|
|
172
|
+
"preds": {"p": 1, "b": 1, "q": 1},
|
|
173
|
+
"rules": [("D", "defeasible", "q(X)", "p(X)"), ("X", "defeater", "q(X)", "b(X)")],
|
|
174
|
+
"priorities": [("D", "X")],
|
|
175
|
+
"case": ["p(a)", "b(a)"], "query": "q(a)", "expect": "TRUE_ONLY",
|
|
176
|
+
"canary": {"drop": ["prio0"], "expect": "NEITHER"}},
|
|
177
|
+
{"name": "defeater-blocks", "basis": "§107.3: дефитер блокирует несравнимого кандидата и опор не создаёт",
|
|
178
|
+
"preds": {"p": 1, "b": 1, "q": 1},
|
|
179
|
+
"rules": [("D", "defeasible", "q(X)", "p(X)"), ("X", "defeater", "q(X)", "b(X)")],
|
|
180
|
+
"case": ["p(a)", "b(a)"], "query": "q(a)", "expect": "NEITHER",
|
|
181
|
+
"canary": {"drop": ["X"], "expect": "TRUE_ONLY"}},
|
|
182
|
+
{"name": "one-survivor", "basis": "§107.2/§109: побеждённый D3 всё равно поражает D1 (полный снимок), "
|
|
183
|
+
"D2 выживает один",
|
|
184
|
+
"preds": {"p": 1, "s": 1, "t": 1, "q": 1},
|
|
185
|
+
"rules": [("D1", "defeasible", "q(X)", "p(X)"), ("D2", "defeasible", "q(X)", "s(X)"),
|
|
186
|
+
("D3", "defeasible", "!q(X)", "t(X)")],
|
|
187
|
+
"priorities": [("D3", "D1"), ("D2", "D3")],
|
|
188
|
+
"case": ["p(a)", "s(a)", "t(a)"], "query": "q(a)", "expect": "TRUE_ONLY",
|
|
189
|
+
"canary": {"drop": ["D2"], "expect": "FALSE_ONLY"}},
|
|
190
|
+
{"name": "default-negation", "basis": "§113: not_known истинен при FALSE_ONLY; вывод по умолчанию "
|
|
191
|
+
"в страте выше производителя",
|
|
192
|
+
"preds": {"req": 1, "ban": 1, "ok": 1},
|
|
193
|
+
"rules": [("Ban", "defeasible", "ban(X)", "req(X)"), ("Ok", "defeasible", "ok(X)", "req(X) & not_known[ban(X)]")],
|
|
194
|
+
"case": ["req(a)"], "query": "ok(a)", "expect": "NEITHER",
|
|
195
|
+
"canary": {"drop": ["Ban"], "expect": "TRUE_ONLY"}},
|
|
196
|
+
# --- агрегаты §59 и замыкания §70 (DECISION-0239) ---------------------
|
|
197
|
+
{"name": "count-over-derived", "basis": "§59/E-0112: count читает завершённое множество strict-вывода",
|
|
198
|
+
"preds": {"joined": 2, "member": 2, "board": 1, "quorum": 1},
|
|
199
|
+
"rules": [("Join", "strict", "member(C, P)", "joined(C, P)"),
|
|
200
|
+
("Quorum", "strict", "quorum(C)", "board(C) & count{P | member(C, P)} >= 2")],
|
|
201
|
+
"case": ["board(c)", "joined(c, a)", "joined(c, b)"], "query": "quorum(c)", "expect": "TRUE_ONLY",
|
|
202
|
+
"canary": {"drop": ["Join"], "expect": "NEITHER"}},
|
|
203
|
+
{"name": "empty-sum-issue", "basis": "§59: sum над пустой коллекцией без empty — EMPTY_AGGREGATE, "
|
|
204
|
+
"правило не срабатывает (DECISION-0239 G3)",
|
|
205
|
+
"preds": {"board": 1, "fee": ["Text", "Decimal"], "charged": 1},
|
|
206
|
+
"rules": [("Charge", "strict", "charged(C)", "board(C) & sum{V | fee(C, V)} >= 0")],
|
|
207
|
+
"case": ["board(c)"], "query": "charged(c)", "expect": "NEITHER", "issues": ["EMPTY_AGGREGATE"],
|
|
208
|
+
"canary": {"drop": ["Charge"], "expect": "NEITHER", "issues": []}},
|
|
209
|
+
{"name": "closure-negative", "basis": "§70: член домена без позитивной записи получает явный негатив",
|
|
210
|
+
"preds": {"company": 1, "licensed": 1},
|
|
211
|
+
"closures": [("LicenceRegistry", "licensed", "company")],
|
|
212
|
+
"case": ["company(a)"], "query": "licensed(a)", "expect": "FALSE_ONLY",
|
|
213
|
+
"canary": {"drop": ["LicenceRegistry"], "expect": "NEITHER"}},
|
|
214
|
+
{"name": "closure-strict-positive", "basis": "§70: strict-вывод позитива снимает негатив замыкания",
|
|
215
|
+
"preds": {"company": 1, "permit": 1, "licensed": 1},
|
|
216
|
+
"rules": [("Permit", "strict", "licensed(X)", "permit(X)")],
|
|
217
|
+
"closures": [("LicenceRegistry", "licensed", "company")],
|
|
218
|
+
"case": ["company(a)", "permit(a)"], "query": "licensed(a)", "expect": "TRUE_ONLY",
|
|
219
|
+
"canary": {"drop": ["Permit"], "expect": "FALSE_ONLY"}},
|
|
220
|
+
# E-0174: читатель, опровергаемый негативом замыкания, ждёт его
|
|
221
|
+
# материализации. Канарейка без замыкания отвечает TRUE_ONLY — тот же
|
|
222
|
+
# ответ дал бы и модуль, исполнивший читателя до замыкания.
|
|
223
|
+
{"name": "closure-unknown-reader", "basis": "§70/E-0174: strict unknown над закрытым предикатом исполняется "
|
|
224
|
+
"после замыкания — NEITHER",
|
|
225
|
+
"preds": {"company": 1, "licensed": 1, "pending": 1},
|
|
226
|
+
"rules": [("Pending", "strict", "pending(X)", "company(X) & unknown[licensed(X)]")],
|
|
227
|
+
"closures": [("LicenceRegistry", "licensed", "company")],
|
|
228
|
+
"case": ["company(a)"], "query": "pending(a)", "expect": "NEITHER",
|
|
229
|
+
"canary": {"drop": ["LicenceRegistry"], "expect": "TRUE_ONLY"}},
|
|
230
|
+
{"name": "closure-transitive-default", "basis": "§70/§113/E-0174: not_known над strict-выводом из негатива "
|
|
231
|
+
"замыкания ждёт замыкание транзитивно — NEITHER",
|
|
232
|
+
"preds": {"company": 1, "licensed": 1, "barred": 1, "clean": 1},
|
|
233
|
+
"rules": [("Barred", "strict", "barred(X)", "company(X) & !licensed(X)"),
|
|
234
|
+
("Clean", "strict", "clean(X)", "company(X) & not_known[barred(X)]")],
|
|
235
|
+
"closures": [("LicenceRegistry", "licensed", "company")],
|
|
236
|
+
"case": ["company(a)"], "query": "clean(a)", "expect": "NEITHER",
|
|
237
|
+
"canary": {"drop": ["LicenceRegistry"], "expect": "TRUE_ONLY"}},
|
|
238
|
+
{"name": "monotone-recursion", "basis": "§103: рекурсия строгих правил через monotone(P) допустима",
|
|
239
|
+
"preds": {"link": 2, "reach": 2},
|
|
240
|
+
"rules": [("Base", "strict", "reach(X, Y)", "link(X, Y)"),
|
|
241
|
+
("Step", "strict", "reach(X, Z)", "link(X, Y) & monotone[reach(Y, Z)]")],
|
|
242
|
+
"case": ["link(a, b)", "link(b, c)"], "query": "reach(a, c)", "expect": "TRUE_ONLY",
|
|
243
|
+
"canary": {"drop": ["Step"], "expect": "NEITHER"}},
|
|
244
|
+
# --- структурные контрпримеры: план обязан отказать названной причиной ---
|
|
245
|
+
{"name": "default-cycle", "basis": "§110/§113: цикл через not_known не стратифицируем",
|
|
246
|
+
"preds": {"req": 1, "a": 1, "b": 1},
|
|
247
|
+
"rules": [("C1", "defeasible", "a(X)", "req(X) & not_known[b(X)]"),
|
|
248
|
+
("C2", "defeasible", "b(X)", "req(X) & not_known[a(X)]")],
|
|
249
|
+
"plan": "программа не стратифицируема (§110, NON_EXECUTABLE_UNSTRATIFIED)"},
|
|
250
|
+
{"name": "late-producer", "basis": "§111: читатель стоит в страте, где строгая цепочка над поражаемым "
|
|
251
|
+
"выводом ещё не завершена (оракул отвечает NEITHER вместо TRUE_ONLY)",
|
|
252
|
+
"preds": {"a": 1, "q": 1, "p": 1, "r": 1},
|
|
253
|
+
"rules": [("D1", "defeasible", "q(X)", "a(X)"), ("S", "strict", "p(X)", "monotone[q(X)]"),
|
|
254
|
+
("D2", "defeasible", "r(X)", "p(X)")],
|
|
255
|
+
"plan": "поздний производитель (§111, LDC-E4103)"},
|
|
256
|
+
{"name": "completion-cycle", "basis": "§110/E-0112: цикл через перечисление не стратифицируем",
|
|
257
|
+
"preds": {"seed": 1, "p": 1},
|
|
258
|
+
"rules": [("P", "strict", "p(X)", "seed(X) & count{Y | p(Y)} >= 1")],
|
|
259
|
+
"plan": "программа не стратифицируема (§110, NON_EXECUTABLE_UNSTRATIFIED)"},
|
|
260
|
+
{"name": "monotone-guard-derived", "basis": "E-0116 над выводимым предикатом вне среза (DECISION-0239 G2)",
|
|
261
|
+
"preds": {"joined": 2, "member": 2, "board": 1, "quorum": 1},
|
|
262
|
+
"rules": [("Join", "strict", "member(C, P)", "joined(C, P)"),
|
|
263
|
+
("Quorum", "strict", "quorum(C)", "board(C) & count{P | monotone[member(C, P)]} >= 2")],
|
|
264
|
+
"plan": "монотонный агрегатный гард (E-0116) над выводимым предикатом вне среза генератора (DECISION-0239 G2)"},
|
|
265
|
+
{"name": "closure-over-defeasible", "basis": "§70: замыкание над поражаемым производителем (DECISION-0239 G5)",
|
|
266
|
+
"preds": {"company": 1, "applied": 1, "licensed": 1},
|
|
267
|
+
"rules": [("Apply", "defeasible", "licensed(X)", "applied(X)")],
|
|
268
|
+
"closures": [("LicenceRegistry", "licensed", "company")],
|
|
269
|
+
"plan": "замыкание §70 над поражаемым производителем вне среза генератора (DECISION-0239 G5)"},
|
|
270
|
+
{"name": "orphan-defeater", "basis": "§110 п. 5: дефитер без обычного кандидата той же полярности",
|
|
271
|
+
"preds": {"a": 1, "b": 1, "q": 1},
|
|
272
|
+
"rules": [("D", "defeasible", "q(X)", "a(X)"), ("X", "defeater", "!q(X)", "b(X)")],
|
|
273
|
+
"plan": "дефитер без обычного кандидата той же полярности (§110 п. 5, NON_EXECUTABLE_STRATUM_BARRIER)"},
|
|
274
|
+
]
|
|
275
|
+
|
|
276
|
+
|
|
277
|
+
def _split(text: str) -> list:
|
|
278
|
+
"""Аргументы через запятую верхнего уровня (вызов внутри аргумента)."""
|
|
279
|
+
out, depth, cur = [], 0, ""
|
|
280
|
+
for ch in text:
|
|
281
|
+
if ch == "," and depth == 0:
|
|
282
|
+
out.append(cur)
|
|
283
|
+
cur = ""
|
|
284
|
+
continue
|
|
285
|
+
depth += (ch == "(") - (ch == ")")
|
|
286
|
+
cur += ch
|
|
287
|
+
return out + [cur]
|
|
288
|
+
|
|
289
|
+
|
|
290
|
+
def _term(a: str) -> dict:
|
|
291
|
+
a = a.strip()
|
|
292
|
+
m = re.fullmatch(r"(\w+)\((.*)\)", a)
|
|
293
|
+
if m:
|
|
294
|
+
return {"kind": "call", "function": f"urn:law:std#{m.group(1)}", "args": [_term(x) for x in _split(m.group(2))]}
|
|
295
|
+
if a.startswith("@"):
|
|
296
|
+
return {"kind": "value", "type": {"name": "urn:law:std#Date"}, "value": a[1:]}
|
|
297
|
+
m = re.fullmatch(r"(\d+) (\w+)", a)
|
|
298
|
+
if m:
|
|
299
|
+
return {"kind": "value", "type": {"name": "urn:law:std#Quantity"}, "unit": m.group(2), "value": m.group(1)}
|
|
300
|
+
if a[:1].isupper():
|
|
301
|
+
return {"kind": "var", "var": a}
|
|
302
|
+
if re.fullmatch(r"-?\d+(\.\d+)?", a):
|
|
303
|
+
return {"kind": "value", "type": {"name": "urn:law:std#Decimal"}, "value": a}
|
|
304
|
+
return {"kind": "entity_ref", "id": f"urn:entity:{a}"}
|
|
305
|
+
|
|
306
|
+
|
|
307
|
+
def literal(text: str) -> dict:
|
|
308
|
+
text = text.strip()
|
|
309
|
+
polarity = "positive"
|
|
310
|
+
if text.startswith("!"):
|
|
311
|
+
polarity, text = "negative", text[1:]
|
|
312
|
+
m = re.fullmatch(r"(\w+)\((.*)\)", text)
|
|
313
|
+
return {"kind": "literal", "polarity": polarity, "predicate": f"{NS}#{m.group(1)}",
|
|
314
|
+
"args": [_term(a) for a in _split(m.group(2))]}
|
|
315
|
+
|
|
316
|
+
|
|
317
|
+
OPS = {">=": "ge", ">": "gt", "<=": "le", "<": "lt", "==": "eq", "!=": "ne"}
|
|
318
|
+
|
|
319
|
+
|
|
320
|
+
def conjunct(text: str) -> dict:
|
|
321
|
+
text = text.strip()
|
|
322
|
+
m = re.fullmatch(r"(count|sum|min|max)\{(\w+) \| (.*)\} *(>=|<=|==|!=|>|<) *(-?[\d.]+)", text)
|
|
323
|
+
if m:
|
|
324
|
+
op, var, gen, cmp, num = m.groups()
|
|
325
|
+
items = [conjunct(x) for x in gen.split(";")]
|
|
326
|
+
threshold = {"kind": "value", "value": int(num) if op == "count" else num,
|
|
327
|
+
"type": {"name": "urn:law:std#Integer" if op == "count" else "urn:law:std#Decimal"}}
|
|
328
|
+
return {"kind": "comparison", "op": OPS[cmp], "right": threshold,
|
|
329
|
+
"left": {"kind": "aggregate", "op": op, "input": {
|
|
330
|
+
"kind": "comprehension", "distinct": True, "binders": [],
|
|
331
|
+
"element": {"kind": "var", "var": var},
|
|
332
|
+
"variable": {"id": var, "type": {"name": "urn:law:std#Text"}},
|
|
333
|
+
"generator": items[0] if len(items) == 1 else {"kind": "and", "items": items}}}}
|
|
334
|
+
m = re.fullmatch(r"(.+?) (>=|<=|==|!=|>|<) (.+)", text)
|
|
335
|
+
if m and "{" not in text:
|
|
336
|
+
right = m.group(3).strip()
|
|
337
|
+
if re.fullmatch(r"-?\d+", right):
|
|
338
|
+
right_term = {"kind": "value", "type": {"name": "urn:law:std#Integer"}, "value": int(right)}
|
|
339
|
+
else:
|
|
340
|
+
right_term = _term(right)
|
|
341
|
+
return {"kind": "comparison", "op": OPS[m.group(2)], "left": _term(m.group(1)), "right": right_term}
|
|
342
|
+
m = re.fullmatch(r"(\w+)\[(.*)\]", text)
|
|
343
|
+
if m:
|
|
344
|
+
return {"kind": "status", "status": m.group(1), "formula": literal(m.group(2))}
|
|
345
|
+
return {"kind": "status", "status": "established", "formula": literal(text)}
|
|
346
|
+
|
|
347
|
+
|
|
348
|
+
def _vars(lit: dict) -> set:
|
|
349
|
+
if lit.get("kind") == "comparison":
|
|
350
|
+
return set()
|
|
351
|
+
out, stack = set(), list(lit["args"])
|
|
352
|
+
while stack:
|
|
353
|
+
a = stack.pop()
|
|
354
|
+
if a["kind"] == "var":
|
|
355
|
+
out.add(a["var"])
|
|
356
|
+
stack.extend(a.get("args") or [])
|
|
357
|
+
return out
|
|
358
|
+
|
|
359
|
+
|
|
360
|
+
def _calendar(fx: dict, drop: tuple = ()) -> dict | None:
|
|
361
|
+
if "calendar" not in fx:
|
|
362
|
+
return None
|
|
363
|
+
cal = dict(fx["calendar"])
|
|
364
|
+
if "Overrides" in drop:
|
|
365
|
+
cal["overrides"] = []
|
|
366
|
+
return cal
|
|
367
|
+
|
|
368
|
+
|
|
369
|
+
def resources(fx: dict, drop: tuple = ()) -> dict:
|
|
370
|
+
"""Байты ресурса снимка §85 фикстуры по `resourceUri`."""
|
|
371
|
+
cal = _calendar(fx, drop)
|
|
372
|
+
return {} if cal is None else {"artifact:fixture.calendar.json": json.dumps(cal, sort_keys=True).encode()}
|
|
373
|
+
|
|
374
|
+
|
|
375
|
+
def program(fx: dict, drop: tuple = (), reverse: bool = False) -> dict:
|
|
376
|
+
"""CLIR фикстуры; `drop` — локальные имена снятых узлов, `reverse` —
|
|
377
|
+
обратный порядок правил, приоритетов и фактов."""
|
|
378
|
+
nodes = []
|
|
379
|
+
for p, n in fx["preds"].items():
|
|
380
|
+
kinds = ["Text"] * n if isinstance(n, int) else n
|
|
381
|
+
nodes.append({"id": f"{NS}#{p}", "kind": "symbol_decl", "symbolKind": "relation", "name": p,
|
|
382
|
+
"package": NS, "relationKind": "institutional",
|
|
383
|
+
"parameters": [{"id": f"{NS}#{p}/arg/{i}", "name": f"a{i}",
|
|
384
|
+
"type": {"name": f"urn:law:std#{k}"}} for i, k in enumerate(kinds)]})
|
|
385
|
+
rules = []
|
|
386
|
+
for rid, strength, head, body in fx.get("rules", []):
|
|
387
|
+
items = [conjunct(x) for x in body.split("&")]
|
|
388
|
+
norm = head.startswith("norm:")
|
|
389
|
+
head_node = {"kind": "norm_template_ref", "id": f"{NS}#{head[5:]}"} if norm else literal(head)
|
|
390
|
+
names = sorted(set().union(*[_vars(i.get("formula") or i) for i in items])
|
|
391
|
+
| (set() if norm else _vars(head_node)))
|
|
392
|
+
rules.append({"id": f"{NS}#{rid}", "kind": "rule", "package": NS, "strength": strength,
|
|
393
|
+
"head": head_node, "body": {"kind": "and", "items": items},
|
|
394
|
+
"scope": {"kind": "boolean", "value": True}, "interpretations": [],
|
|
395
|
+
"variables": [{"id": v, "type": {"name": "urn:law:std#Text"}} for v in names]})
|
|
396
|
+
prios = [{"id": f"{NS}#prio{i}", "kind": "priority_rule", "package": NS, "priorityKind": "direct",
|
|
397
|
+
"condition": {"kind": "boolean", "value": True},
|
|
398
|
+
"higher": {"kind": "const_ref", "id": f"{NS}#{hi}"},
|
|
399
|
+
"lower": {"kind": "const_ref", "id": f"{NS}#{lo}"}}
|
|
400
|
+
for i, (hi, lo) in enumerate(fx.get("priorities", []))]
|
|
401
|
+
closures = [{"id": f"{NS}#{cid}", "kind": "closure_policy", "package": NS, "predicate": f"{NS}#{pred}",
|
|
402
|
+
"domain": {"kind": "const_ref", "id": f"{NS}#{dom}"}, "snapshot": f"{NS}#snapshot",
|
|
403
|
+
"completeAsOf": "2026-09-14T00:00:00+05:00", "deriveExplicitNegative": True}
|
|
404
|
+
for cid, pred, dom in fx.get("closures", [])]
|
|
405
|
+
templates = [{"id": f"{NS}#{tid}", "kind": "norm_template", "package": NS, "modality": modality,
|
|
406
|
+
"payload": _payload(payload)} for tid, modality, payload in fx.get("templates", [])]
|
|
407
|
+
nodes += templates
|
|
408
|
+
if "policy" in fx:
|
|
409
|
+
nodes.append({"id": f"{NS}#Policy", "kind": "deadline_policy", "package": NS, "policy": dict(fx["policy"])})
|
|
410
|
+
cal = resources(fx, drop)
|
|
411
|
+
for uri, data in cal.items():
|
|
412
|
+
nodes.append({"id": f"{NS}#calendar", "kind": "calendar_snapshot", "package": NS, "resourceUri": uri,
|
|
413
|
+
"datasetHash": "sha256:" + hashlib.sha256(data).hexdigest(), "format": "law.calendar/0.1"})
|
|
414
|
+
tail = [n for n in rules + prios + closures if n["id"].split("#")[1] not in drop]
|
|
415
|
+
if reverse:
|
|
416
|
+
tail.reverse()
|
|
417
|
+
return {"schemaVersion": "law.core.ir/0.7", "semanticVersion": "0.2.11", "features": ["core-executable"],
|
|
418
|
+
"imports": [], "metadata": {}, "strata": [],
|
|
419
|
+
"package": {"name": "backends.l1_fixture", "namespace": NS, "version": "0.1.0",
|
|
420
|
+
"semanticHash": "sha256:" + "0" * 64},
|
|
421
|
+
"nodes": nodes + tail}
|
|
422
|
+
|
|
423
|
+
|
|
424
|
+
def case(fx: dict, reverse: bool = False, drop: tuple = ()) -> dict:
|
|
425
|
+
facts = list(fx.get("case", []))
|
|
426
|
+
if reverse:
|
|
427
|
+
facts.reverse()
|
|
428
|
+
context = {"legalTime": "2026-09-14"}
|
|
429
|
+
if "policy" in fx and "Policy" not in drop:
|
|
430
|
+
context["deadlinePolicy"] = f"{NS}#Policy"
|
|
431
|
+
return {"assertions": [{"id": f"{NS}#case{i}", "kind": "assertion", "literal": literal(f),
|
|
432
|
+
"origin": "case_input", "package": NS, "evidence": []}
|
|
433
|
+
for i, f in enumerate(facts)],
|
|
434
|
+
"context": context, "options": {}}
|
|
435
|
+
|
|
436
|
+
|
|
437
|
+
def _payload(value, key: str = ""):
|
|
438
|
+
"""Мини-запись payload шаблона → CLIR: литералы строками, окно — пара дат,
|
|
439
|
+
участники — термы, exercise — вызов."""
|
|
440
|
+
if isinstance(value, dict):
|
|
441
|
+
return {k: _payload(v, k) for k, v in value.items()}
|
|
442
|
+
if key == "window":
|
|
443
|
+
bound = (lambda v: _term(v) if v[:1].isupper() or "(" in v
|
|
444
|
+
else {"kind": "value", "type": {"name": "urn:law:std#Date"}, "value": v})
|
|
445
|
+
return {"kind": "interval", "start": bound(value[0]), "end": bound(value[1]),
|
|
446
|
+
"startInclusive": True, "endInclusive": True}
|
|
447
|
+
if key in ("condition", "validWhen", "after"):
|
|
448
|
+
return literal(value)
|
|
449
|
+
if key == "exercise":
|
|
450
|
+
lit = literal(value)
|
|
451
|
+
return {"kind": "call", "function": lit["predicate"], "args": lit["args"]}
|
|
452
|
+
if key in ("bearer", "holder", "beneficiary", "against"):
|
|
453
|
+
return _term(value)
|
|
454
|
+
return value
|
|
455
|
+
|
|
456
|
+
|
|
457
|
+
def query(fx: dict) -> dict:
|
|
458
|
+
if fx["query"] == "positions":
|
|
459
|
+
return {"kind": "positions", "queryId": f"urn:query:{fx['name']}"}
|
|
460
|
+
return {"kind": "truth", "queryId": f"urn:query:{fx['name']}", "literal": literal(fx["query"])}
|
|
461
|
+
|
|
462
|
+
|
|
463
|
+
FIXTURES += L2_FIXTURES + CAL_FIXTURES
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
{
|
|
2
|
+
"format": "law.codegen-inert-nodes/0.1",
|
|
3
|
+
"about": "Инертные конструкции CLIR: план пропускает их без отказа, потому что на ответ поддержанных видов вопросов они не влияют (CODEGEN-COVERAGE-PLAN §1.1, волна W1). Пропуск без свидетеля — отказ ворот check_conformance.",
|
|
4
|
+
"criterion": "Пара векторов над одной программой, делом и вопросом — с конструкцией (witness) и без неё (bare) — у оракула даёт один документ по results, positions, issues, conflicts и proofGraph; manifest и resultHash различаются по построению (§209 покрывает конверт, §208 — manifest). Половина с конструкцией обязана совпасть с голденом на каждой активной цели.",
|
|
5
|
+
"constructs": {
|
|
6
|
+
"node:source_work": {
|
|
7
|
+
"witness": "LAY-L0-INERT-SOURCE-WORK",
|
|
8
|
+
"bare": "LAY-L0-INERT-SOURCE-WORK-BARE",
|
|
9
|
+
"spec": "§28",
|
|
10
|
+
"note": "Акт как понятие; до W1 пропускался списком KNOWN_NODES — перенесён в манифест как образец."
|
|
11
|
+
},
|
|
12
|
+
"node:anchor": {
|
|
13
|
+
"witness": "LAY-L0-INERT-ANCHOR",
|
|
14
|
+
"bare": "LAY-L0-INERT-ANCHOR-BARE",
|
|
15
|
+
"spec": "§33",
|
|
16
|
+
"note": "Привязка нормы к фрагменту (E-0240): провенанс; конверт с anchor пишется law.core.ir/0.4, что меняет только manifest."
|
|
17
|
+
},
|
|
18
|
+
"node:revision": {
|
|
19
|
+
"witness": "LAY-L0-INERT-REVISION",
|
|
20
|
+
"bare": "LAY-L0-INERT-REVISION-BARE",
|
|
21
|
+
"spec": "§32",
|
|
22
|
+
"note": "Ребро графа редакций; операции §32.1 читает миграция, не исполнение."
|
|
23
|
+
},
|
|
24
|
+
"node:entrenchment": {
|
|
25
|
+
"witness": "LAY-L0-INERT-ENTRENCHMENT",
|
|
26
|
+
"bare": "LAY-L0-INERT-ENTRENCHMENT-BARE",
|
|
27
|
+
"spec": "§92.3",
|
|
28
|
+
"note": "Окно защищённости источника; читает миграция."
|
|
29
|
+
},
|
|
30
|
+
"node:factor_vocabulary": {
|
|
31
|
+
"witness": "LAY-L0-INERT-FACTOR-VOCABULARY",
|
|
32
|
+
"bare": "LAY-L0-INERT-FACTOR-VOCABULARY-BARE",
|
|
33
|
+
"spec": "§201",
|
|
34
|
+
"note": "Словарь факторов аналитического вида вопроса precedent (вне профиля core-executable)."
|
|
35
|
+
},
|
|
36
|
+
"node:precedent_decl": {
|
|
37
|
+
"witness": "LAY-L0-INERT-PRECEDENT-DECL",
|
|
38
|
+
"bare": "LAY-L0-INERT-PRECEDENT-DECL-BARE",
|
|
39
|
+
"spec": "§201",
|
|
40
|
+
"note": "Прецедент аналитического вида precedent."
|
|
41
|
+
},
|
|
42
|
+
"node:counterfactual_decl": {
|
|
43
|
+
"witness": "LAY-L0-INERT-COUNTERFACTUAL-DECL",
|
|
44
|
+
"bare": "LAY-L0-INERT-COUNTERFACTUAL-DECL-BARE",
|
|
45
|
+
"spec": "§186",
|
|
46
|
+
"note": "Данные аналитического вида counterfactual."
|
|
47
|
+
},
|
|
48
|
+
"rulefield:visibility": {
|
|
49
|
+
"witness": "LAY-L0-INERT-VISIBILITY",
|
|
50
|
+
"bare": "LAY-L0-INERT-VISIBILITY-BARE",
|
|
51
|
+
"spec": "§23.2",
|
|
52
|
+
"note": "Экспорт имени `pub` (DECISION-0386); на исполнение не влияет."
|
|
53
|
+
}
|
|
54
|
+
},
|
|
55
|
+
"notInert": {
|
|
56
|
+
"node:reference": {
|
|
57
|
+
"spec": "§30.2, §170",
|
|
58
|
+
"evidence": "LAY-L0-DYNAMIC-REFERENCE-V02",
|
|
59
|
+
"note": "Динамическая ссылка резолвится на legal_time дела: без применимой редакции результаты пусты и issue SOURCE_RESOLUTION_FAILED, при двух редакциях в силе — SOURCE_RESOLUTION_AMBIGUOUS; при удачной резолюции proofGraph получает узел source_resolution. Остаётся отказом плана до порта резолюции §30.2 (проверено оракулом 27.09.2026)."
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
}
|