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,980 @@
|
|
|
1
|
+
"""Единый вычислительный вход Python-цели (DECISION-0423 §2.2).
|
|
2
|
+
|
|
3
|
+
Дело и вопрос приходят в канонической форме (§168, §172) — той же, что у
|
|
4
|
+
векторов и оракула, — и `evaluate` возвращает ответ исполняемого профиля.
|
|
5
|
+
Раннер и API вызывают только его, поэтому сверка аргументов §189, разворот
|
|
6
|
+
контекста, насыщение и лестница статусов §175 у них одни.
|
|
7
|
+
|
|
8
|
+
Состояние — на вызов (§2.3): таблицы модели строит `model.new_model()`,
|
|
9
|
+
контекст рантайма (правовое время, политика срока, снимок календаря) живёт в
|
|
10
|
+
`contextvars`, сопутствующие множества — в замыкании `_evaluate`. Между
|
|
11
|
+
вызовами ничего не живёт; параллельные вызовы из потоков не делят состояния.
|
|
12
|
+
|
|
13
|
+
Названный отказ среза — `RT.Refused`; ошибка значения вне вопроса `term` и
|
|
14
|
+
вне правил модели становится тем же отказом (раннер печатает его с кодом 3).
|
|
15
|
+
"""
|
|
16
|
+
import contextvars
|
|
17
|
+
import json
|
|
18
|
+
|
|
19
|
+
import runtime as RT
|
|
20
|
+
import model as _MODEL
|
|
21
|
+
|
|
22
|
+
Refused = RT.Refused
|
|
23
|
+
|
|
24
|
+
VALUE_TYPES = {"urn:law:std#Money", "urn:law:std#Decimal", "urn:law:std#Integer", "urn:law:std#Text"}
|
|
25
|
+
MIRROR = {"TRUE_ONLY": "FALSE_ONLY", "FALSE_ONLY": "TRUE_ONLY", "BOTH": "BOTH", "NEITHER": "NEITHER"}
|
|
26
|
+
|
|
27
|
+
# --- вопросы term §57–§59 и collect §52.1 -----------------------------------
|
|
28
|
+
# Срез раннера: comprehension по установленным атомам таблиц с гардами,
|
|
29
|
+
# элемент — ячейка или литерал; агрегаты count/sum/min/max; арифметика §58 и
|
|
30
|
+
# функции рантайма над результатом. Вид результата выводится статически по
|
|
31
|
+
# тем же таблицам §58, что у плана: у оракула вид — часть значения (Integer 5
|
|
32
|
+
# и Decimal 5 — разные байты), у рантайма — нет.
|
|
33
|
+
INT, DEC, MON = "urn:law:std#Integer", "urn:law:std#Decimal", "urn:law:std#Money"
|
|
34
|
+
TEXT, DATE, RAT = "urn:law:std#Text", "urn:law:std#Date", "urn:law:std#Rational"
|
|
35
|
+
INSTANT = "urn:law:std#Instant"
|
|
36
|
+
QTY, BOOL = "urn:law:std#Quantity", "urn:law:std#Boolean"
|
|
37
|
+
NAT = "urn:law:std#Natural"
|
|
38
|
+
NUM = {INT, DEC, MON, RAT, QTY, NAT}
|
|
39
|
+
CMP_OPS = ("lt", "le", "gt", "ge", "eq", "ne")
|
|
40
|
+
# DECISION-0252 п. 3: вид операнда для `arith` рантайма. У вопроса вид каждого
|
|
41
|
+
# значения известен точно — из литерала, слота или исхода операции.
|
|
42
|
+
KIND_CODE = {INT: "i", DEC: "d", MON: "m", RAT: "r", QTY: "q", NAT: "i"}
|
|
43
|
+
# Имя функции рантайма по имени std-функции вопроса (DECISION-0267).
|
|
44
|
+
RT_NAME = {"month_start_of": "month_start", "month_end_of": "month_end"}
|
|
45
|
+
# Виды вопросов протокола раннера; `rows` — внутренний вид API (DECISION-0423 §2.1).
|
|
46
|
+
QUERY_KINDS = ("truth", "term", "collect")
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
def norm(t: str) -> str:
|
|
50
|
+
"""DECISION-0266: Natural считает как Integer (`values._TYPE_KINDS`), тип-строка своя."""
|
|
51
|
+
return INT if t == NAT else t
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
# §59 (errata E-0195): вид элемента ПУСТОЙ коллекции несёт ОБЪЯВЛЕННЫЙ тип
|
|
55
|
+
# проекции comprehension §52.1 (`variable.type.name`) — единственный носитель
|
|
56
|
+
# вида, не зависящий от числа решений generator-а (порт
|
|
57
|
+
# `values.aggregate_element_type`). Таблица — порт `values._TYPE_KINDS` плюс
|
|
58
|
+
# `_OPAQUE_KINDS`: Natural и Integer у оракула один вид `integer`. Имя вне
|
|
59
|
+
# таблицы (сущность, перечисление, чужой символ) вида не предъявляет, и сверка
|
|
60
|
+
# МОЛЧИТ: Unknown есть «утверждать нечего», а не «совместимо со всем».
|
|
61
|
+
EMPTY_ELEMENT_KINDS = {INT: INT, NAT: INT, DEC: DEC, MON: MON, TEXT: TEXT, DATE: DATE,
|
|
62
|
+
BOOL: BOOL, RAT: RAT, QTY: QTY,
|
|
63
|
+
"urn:law:std#Instant": "urn:law:std#Instant",
|
|
64
|
+
"urn:law:std#Magnitude": "urn:law:std#Magnitude",
|
|
65
|
+
"urn:law:std#Bounds": "urn:law:std#Bounds"}
|
|
66
|
+
# §175: коды ошибки значения вопроса со статусом TYPE_ERROR (`queries.query_calc`).
|
|
67
|
+
TYPE_ERROR_CODES = ("TYPE_ERROR", "CURRENCY_MISMATCH", "DIMENSION_MISMATCH", "DATE_INSTANT_MISMATCH")
|
|
68
|
+
|
|
69
|
+
#: E-0105: код ошибки терма → статус результата (`whynot.TERM_ERROR_STATUS`);
|
|
70
|
+
#: код вне таблицы — RUNTIME_ERROR; старшинство — `TERM_ERROR_PRECEDENCE`.
|
|
71
|
+
TERM_ERROR_STATUS = {"MISSING_POLICY": "MISSING_POLICY", "DEADLINE_POLICY_INVALID": "MISSING_POLICY",
|
|
72
|
+
"MISSING_CALENDAR": "MISSING_INPUT", "CALENDAR_DATASET_INVALID": "MISSING_INPUT",
|
|
73
|
+
"CALENDAR_DATASET_HASH_MISMATCH": "MISSING_INPUT", "CALENDAR_OUT_OF_RANGE": "MISSING_INPUT",
|
|
74
|
+
"TYPE_ERROR": "TYPE_ERROR", "CURRENCY_MISMATCH": "TYPE_ERROR",
|
|
75
|
+
"DIMENSION_MISMATCH": "TYPE_ERROR", "DATE_INSTANT_MISMATCH": "TYPE_ERROR", "RESOURCE_LIMIT": "RESOURCE_LIMIT",
|
|
76
|
+
"OPAQUE_NON_EXECUTABLE": "NON_EXECUTABLE", "NON_EXHAUSTIVE_MATCH": "NON_EXECUTABLE",
|
|
77
|
+
"UNKNOWN_FUNCTION": "NON_EXECUTABLE", "NON_EXECUTABLE_FUNCTION_RECURSION": "NON_EXECUTABLE"}
|
|
78
|
+
TERM_ERROR_PRECEDENCE = ("MISSING_INPUT", "MISSING_POLICY", "NON_EXECUTABLE", "EXTERNAL_UNAVAILABLE",
|
|
79
|
+
"TYPE_ERROR", "RUNTIME_ERROR", "RESOURCE_LIMIT")
|
|
80
|
+
#: §175/§181.2 (errata E-0265): код из списка остаётся статусом итога, прочие — NON_EXECUTABLE.
|
|
81
|
+
OUTCOME_CODES = {"PRIORITY_CONFLICT", "INTERPRETATION_REQUIRED", "TYPE_ERROR",
|
|
82
|
+
"RESOURCE_LIMIT", "CONFLICTED_INPUTS", "SOURCE_RESOLUTION_FAILED"}
|
|
83
|
+
|
|
84
|
+
|
|
85
|
+
def arith_kind(op: str, l: str, r: str) -> str:
|
|
86
|
+
l, r = norm(l), norm(r)
|
|
87
|
+
if l not in NUM or r not in NUM:
|
|
88
|
+
raise RT.Refused(f"арифметика вне среза (§58): не числовой операнд {op}")
|
|
89
|
+
if op in ("add", "sub"):
|
|
90
|
+
if l != r:
|
|
91
|
+
raise RT.Refused(f"арифметика вне среза (§58): виды операндов не совпадают у {op}")
|
|
92
|
+
return l
|
|
93
|
+
if op == "mul":
|
|
94
|
+
if l == MON and r == MON:
|
|
95
|
+
raise RT.Refused("арифметика вне среза (§58): произведение двух размерных величин не определено")
|
|
96
|
+
if INT in (l, r):
|
|
97
|
+
return r if l == INT else l
|
|
98
|
+
return MON if MON in (l, r) else DEC
|
|
99
|
+
if op == "div":
|
|
100
|
+
if (l, r) in ((DEC, DEC), (MON, MON)):
|
|
101
|
+
return DEC
|
|
102
|
+
if (l, r) == (MON, DEC):
|
|
103
|
+
return MON
|
|
104
|
+
if (l, r) == (INT, INT):
|
|
105
|
+
raise RT.Refused("арифметика вне среза (§58): частное Integer/Integer — Rational")
|
|
106
|
+
raise RT.Refused("арифметика вне среза (§58): пара вне таблицы делимых")
|
|
107
|
+
if op == "mod":
|
|
108
|
+
if (l, r) != (INT, INT):
|
|
109
|
+
raise RT.Refused("арифметика вне среза (§58): остаток только Integer mod Integer")
|
|
110
|
+
return INT
|
|
111
|
+
raise RT.Refused("операция вне среза раннера: " + op)
|
|
112
|
+
|
|
113
|
+
|
|
114
|
+
def call_kind(fn: str, ks: list) -> str:
|
|
115
|
+
if fn in ("min", "max"):
|
|
116
|
+
if len(ks) != 2 or ks[0] not in NUM or ks[0] != ks[1]:
|
|
117
|
+
raise RT.Refused(f"арифметика вне среза (§58): виды операндов не совпадают у {fn}")
|
|
118
|
+
return ks[0]
|
|
119
|
+
if fn == "abs":
|
|
120
|
+
if len(ks) != 1 or ks[0] not in NUM:
|
|
121
|
+
raise RT.Refused("арифметика вне среза (§58): abs над нечисловым видом")
|
|
122
|
+
return ks[0]
|
|
123
|
+
if fn == "pow":
|
|
124
|
+
if ks != [INT, INT]:
|
|
125
|
+
raise RT.Refused("арифметика вне среза (§58): pow только Integer pow Integer")
|
|
126
|
+
return INT
|
|
127
|
+
if fn == "round":
|
|
128
|
+
if len(ks) != 3:
|
|
129
|
+
raise RT.Refused("round вне среза раннера: ожидаются значение, разряды и режим (§50)")
|
|
130
|
+
if ks[0] == RAT:
|
|
131
|
+
raise RT.Refused("round над Rational вне среза генератора (§50: round не берёт Rational)")
|
|
132
|
+
if ks[0] not in NUM:
|
|
133
|
+
raise RT.Refused("round вне среза генератора: вид операнда не числовой (§50)")
|
|
134
|
+
_policy_kinds("round", ks[1:])
|
|
135
|
+
return ks[0]
|
|
136
|
+
if fn == "div_round":
|
|
137
|
+
# §50/§58 (E-0036, E-0170; DECISION-0259 п. 3): четыре пары видов.
|
|
138
|
+
if len(ks) != 4:
|
|
139
|
+
raise RT.Refused("div_round вне среза генератора: ожидаются делимое, делитель, точность и режим (§50)")
|
|
140
|
+
if RAT in ks[:2]:
|
|
141
|
+
raise RT.Refused("div_round над Rational вне среза генератора (§50/§58, E-0036)")
|
|
142
|
+
pair = tuple(ks[:2])
|
|
143
|
+
if pair not in ((MON, DEC), (QTY, DEC), (DEC, DEC), (MON, MON)):
|
|
144
|
+
raise RT.Refused("div_round вне среза генератора: пара видов вне E-0036/E-0170 (§50/§58)")
|
|
145
|
+
_policy_kinds("div_round", ks[2:])
|
|
146
|
+
return {(MON, DEC): MON, (QTY, DEC): QTY}.get(pair, DEC)
|
|
147
|
+
if fn in ("month_start_of", "month_end_of"):
|
|
148
|
+
# §84.2 (E-0084, DECISION-0267): арность и вид — TYPE_ERROR оракула (`month_bound_value`).
|
|
149
|
+
if len(ks) != 1 or ks[0] != DATE:
|
|
150
|
+
raise RT.TermError("TYPE_ERROR")
|
|
151
|
+
return DATE
|
|
152
|
+
if fn in ("hours_between", "minutes_between"):
|
|
153
|
+
# §84.1 (E-0028/E-0058; W2 часть 2): оба аргумента — Instant, иначе TYPE_ERROR оракула.
|
|
154
|
+
if len(ks) != 2 or ks[0] != INSTANT or ks[1] != INSTANT:
|
|
155
|
+
raise RT.TermError("TYPE_ERROR")
|
|
156
|
+
return INT
|
|
157
|
+
if fn == "add_duration":
|
|
158
|
+
raise RT.Refused("add_duration в вопросе вне среза раннера (§84.1)")
|
|
159
|
+
raise RT.Refused("функция вне среза раннера: " + fn)
|
|
160
|
+
|
|
161
|
+
|
|
162
|
+
def _policy_kinds(fn: str, ks: list) -> None:
|
|
163
|
+
"""§50: точность — Integer либо Decimal, режим — Text (DECISION-0259 п. 3)."""
|
|
164
|
+
if ks[0] not in (INT, DEC):
|
|
165
|
+
raise RT.Refused(f"{fn} вне среза генератора: точность не Integer и не Decimal (§50)")
|
|
166
|
+
if ks[1] != TEXT:
|
|
167
|
+
raise RT.Refused(f"{fn} вне среза генератора: режим не Text (§50)")
|
|
168
|
+
|
|
169
|
+
|
|
170
|
+
def flatten(body: dict, atoms: list, guards: list) -> None:
|
|
171
|
+
k = (body or {}).get("kind")
|
|
172
|
+
if k == "status":
|
|
173
|
+
if body.get("status") != "established":
|
|
174
|
+
raise RT.Refused("статусный тест вне L0-среза: " + str(body.get("status")))
|
|
175
|
+
atoms.append(body["formula"])
|
|
176
|
+
elif k == "literal":
|
|
177
|
+
atoms.append(body)
|
|
178
|
+
elif k == "comparison":
|
|
179
|
+
guards.append(body)
|
|
180
|
+
elif k in ("and", "conjunction"):
|
|
181
|
+
for sub in body.get("items") or body.get("args") or body.get("operands") or []:
|
|
182
|
+
flatten(sub, atoms, guards)
|
|
183
|
+
else:
|
|
184
|
+
raise RT.Refused("форма тела вне среза генератора: " + str(k))
|
|
185
|
+
|
|
186
|
+
|
|
187
|
+
def canon_dec(d) -> str:
|
|
188
|
+
"""Каноническая десятичная запись §18/§208: без хвостовых нулей и minus-zero."""
|
|
189
|
+
u, s = d.u, d.s
|
|
190
|
+
if u == 0:
|
|
191
|
+
return "0"
|
|
192
|
+
while s > 0 and u % 10 == 0:
|
|
193
|
+
u //= 10
|
|
194
|
+
s -= 1
|
|
195
|
+
neg = u < 0
|
|
196
|
+
body = str(-u if neg else u).rjust(s + 1, "0")
|
|
197
|
+
text = body if s == 0 else body[:len(body) - s] + "." + body[len(body) - s:]
|
|
198
|
+
return ("-" if neg and u != 0 else "") + text
|
|
199
|
+
|
|
200
|
+
|
|
201
|
+
def literal(x: tuple) -> dict:
|
|
202
|
+
"""Литерал-терм §53 из typed-значения — ровно то, что печатает оракул.
|
|
203
|
+
Целое — число языка (JSON-число), а не строка."""
|
|
204
|
+
v, t = x
|
|
205
|
+
if t == "entity":
|
|
206
|
+
return {"kind": "entity_ref", "id": v}
|
|
207
|
+
if t == "enum":
|
|
208
|
+
return {"kind": "const_ref", "id": v.id}
|
|
209
|
+
if t == TEXT:
|
|
210
|
+
return {"kind": "value", "type": {"name": TEXT}, "value": v}
|
|
211
|
+
if t == DATE:
|
|
212
|
+
return {"kind": "value", "type": {"name": DATE}, "value": v.v}
|
|
213
|
+
if t == INSTANT:
|
|
214
|
+
return {"kind": "value", "type": {"name": INSTANT}, "value": v.v}
|
|
215
|
+
if t == MON:
|
|
216
|
+
return {"kind": "value", "type": {"name": MON}, "currency": v.cur, "value": canon_dec(v)}
|
|
217
|
+
if t == DEC:
|
|
218
|
+
return {"kind": "value", "type": {"name": DEC}, "value": canon_dec(v)}
|
|
219
|
+
if t == RAT:
|
|
220
|
+
return {"kind": "value", "type": {"name": RAT}, "value": f"{v.n}/{v.d}"}
|
|
221
|
+
if t == BOOL:
|
|
222
|
+
return {"kind": "value", "type": {"name": BOOL}, "value": bool(v)}
|
|
223
|
+
if t == NAT:
|
|
224
|
+
# DECISION-0266: запись дела хранит тип Natural; вычисленное целое — всегда Integer.
|
|
225
|
+
return {"kind": "value", "type": {"name": NAT}, "value": v.u // (10 ** v.s)}
|
|
226
|
+
if t == QTY:
|
|
227
|
+
return {"kind": "value", "type": {"name": QTY}, "unit": v.unit, "value": canon_dec(v)}
|
|
228
|
+
if t == INT:
|
|
229
|
+
if v.u % (10 ** v.s) != 0:
|
|
230
|
+
raise RT.Refused("Integer с дробной частью в результате")
|
|
231
|
+
return {"kind": "value", "type": {"name": INT}, "value": v.u // (10 ** v.s)}
|
|
232
|
+
raise RT.Refused("вид результата вне среза раннера: " + str(t))
|
|
233
|
+
|
|
234
|
+
|
|
235
|
+
def canon(v) -> str:
|
|
236
|
+
"""Канонические байты §208 значения: ключи по порядку, без пробелов, целое — цифрами."""
|
|
237
|
+
if v is None:
|
|
238
|
+
return "null"
|
|
239
|
+
if v is True or v is False:
|
|
240
|
+
return "true" if v else "false"
|
|
241
|
+
if isinstance(v, int):
|
|
242
|
+
return str(v)
|
|
243
|
+
if isinstance(v, str):
|
|
244
|
+
return json.dumps(v, ensure_ascii=False)
|
|
245
|
+
if isinstance(v, list):
|
|
246
|
+
return "[" + ",".join(canon(x) for x in v) + "]"
|
|
247
|
+
return "{" + ",".join(json.dumps(k, ensure_ascii=False) + ":" + canon(v[k]) for k in sorted(v)) + "}"
|
|
248
|
+
|
|
249
|
+
|
|
250
|
+
def _ground(value, subst: dict):
|
|
251
|
+
"""Заземление payload шаблона подстановкой — порт `norms._ground_payload`."""
|
|
252
|
+
if isinstance(value, dict):
|
|
253
|
+
if value.get("kind") == "var":
|
|
254
|
+
return subst.get(value["var"], value)
|
|
255
|
+
return {k: _ground(v, subst) for k, v in value.items()}
|
|
256
|
+
if isinstance(value, list):
|
|
257
|
+
return [_ground(v, subst) for v in value]
|
|
258
|
+
return value
|
|
259
|
+
|
|
260
|
+
|
|
261
|
+
def _canon_json(v) -> str:
|
|
262
|
+
return json.dumps(v, ensure_ascii=False, sort_keys=True)
|
|
263
|
+
|
|
264
|
+
|
|
265
|
+
def effective_context(case: dict) -> dict:
|
|
266
|
+
"""Порт `context.expand` без дозаполнения: профили §89.1 от корня к листу,
|
|
267
|
+
затем поля дела."""
|
|
268
|
+
raw = dict(case.get("context") or {})
|
|
269
|
+
profiles = case.get("contextProfiles") or {}
|
|
270
|
+
parent = raw.pop("extends", None)
|
|
271
|
+
chain, seen, current = [], set(), parent
|
|
272
|
+
while current is not None:
|
|
273
|
+
if current in seen:
|
|
274
|
+
raise RT.Refused("CONTEXT_PROFILE_CYCLE: цикл наследования профилей (§89.1)")
|
|
275
|
+
if current not in profiles:
|
|
276
|
+
raise RT.Refused("CONTEXT_PROFILE_MISSING: профиль не определён (§89.1)")
|
|
277
|
+
seen.add(current)
|
|
278
|
+
chain.append(current)
|
|
279
|
+
current = profiles[current].get("extends")
|
|
280
|
+
effective = {}
|
|
281
|
+
for name in reversed(chain):
|
|
282
|
+
effective.update(profiles[name].get("fields") or {})
|
|
283
|
+
effective.update(raw)
|
|
284
|
+
return effective
|
|
285
|
+
|
|
286
|
+
|
|
287
|
+
def evaluate(case: dict, query: dict, stats: dict | None = None, *, api: bool = False) -> dict:
|
|
288
|
+
"""Ответ исполняемого профиля на вопрос `query` по делу `case`.
|
|
289
|
+
|
|
290
|
+
Каждый вызов — в собственном контексте `contextvars` с новой моделью.
|
|
291
|
+
`stats`, если передан, получает замер модели L1 (в протокол ответа не
|
|
292
|
+
входит, DECISION-0237 этап 6). `api` разрешает внутренний вид `rows`,
|
|
293
|
+
которого протокол раннера не знает."""
|
|
294
|
+
if query.get("kind") == "positions":
|
|
295
|
+
raise RT.Refused("POSITIONS_UNAVAILABLE: positions requires current norm ranks (W6)")
|
|
296
|
+
RT.validate_temporal_case(case)
|
|
297
|
+
return contextvars.copy_context().run(_evaluate, case, query, stats, api)
|
|
298
|
+
|
|
299
|
+
|
|
300
|
+
def _evaluate(case: dict, query: dict, stats: dict | None, api: bool) -> dict:
|
|
301
|
+
RT.begin_context()
|
|
302
|
+
M = _MODEL.new_model()
|
|
303
|
+
for assertion in case.get("assertions") or []:
|
|
304
|
+
if assertion.get("id") in M.NODE_IDS:
|
|
305
|
+
raise RT.Refused("LDC-E1382: case assertion id collides with a program node")
|
|
306
|
+
RT.admit_numeric_inputs(case, query, bool(M.JUDGMENTS))
|
|
307
|
+
|
|
308
|
+
# Слой модели: L1-модель хранит опоры обеих полярностей (DECISION-0237).
|
|
309
|
+
LAYER = getattr(M, "LAYER", "L0")
|
|
310
|
+
ISSUES: list = []
|
|
311
|
+
TYPE_ERROR_PREDICATES: set = set()
|
|
312
|
+
# Запись строки, поданная делом как сущность и как Text, — в рантайме одно
|
|
313
|
+
# значение, у оракула — разные атомы. Для печати результата вид берётся по
|
|
314
|
+
# тому, как запись подана; одна запись в обеих ролях — названный отказ.
|
|
315
|
+
ENTITIES: set = set()
|
|
316
|
+
TEXTS: set = set()
|
|
317
|
+
LAST_TERM_CODES: set = set()
|
|
318
|
+
|
|
319
|
+
def arg(a: dict):
|
|
320
|
+
kind = a.get("kind")
|
|
321
|
+
if kind == "entity_ref":
|
|
322
|
+
ENTITIES.add(a["id"])
|
|
323
|
+
return a["id"]
|
|
324
|
+
if kind == "const_ref":
|
|
325
|
+
# §54: разыменование предшествует исполнению; ниже ссылок нет.
|
|
326
|
+
c = M.CONSTS.get(a.get("id"))
|
|
327
|
+
if c is None:
|
|
328
|
+
# Член перечисления §38 либо имя без объявления: оракул оставляет
|
|
329
|
+
# такой const_ref в ключе атома как есть — значение есть идентичность.
|
|
330
|
+
return RT.sym(str(a.get("id")))
|
|
331
|
+
return c[1]
|
|
332
|
+
if kind != "value":
|
|
333
|
+
raise RT.Refused("аргумент вне среза раннера: " + str(kind))
|
|
334
|
+
t = (a.get("type") or {}).get("name")
|
|
335
|
+
# §208/§48 (errata E-0197, E-0202, E-0203): сырое поле литерала читается по
|
|
336
|
+
# грамматике §208, а не средствами Python. `str(a["value"])` приводил к
|
|
337
|
+
# строке ЛЮБОЙ JSON, а отсутствие поля роняло KeyError — крахом вместо
|
|
338
|
+
# правовой диагностики.
|
|
339
|
+
raw = a.get("value")
|
|
340
|
+
if t == "urn:law:std#Money":
|
|
341
|
+
return RT.lit_money(raw, a.get("currency"))
|
|
342
|
+
if t == "urn:law:std#Decimal":
|
|
343
|
+
return RT.lit_dec(raw)
|
|
344
|
+
if t == "urn:law:std#Integer":
|
|
345
|
+
return RT.lit_int(raw)
|
|
346
|
+
if t == "urn:law:std#Text":
|
|
347
|
+
text = RT.lit_text(raw)
|
|
348
|
+
TEXTS.add(text)
|
|
349
|
+
return text
|
|
350
|
+
if t == "urn:law:std#Date":
|
|
351
|
+
return RT.date(RT.lit_text(raw))
|
|
352
|
+
if t == "urn:law:std#Instant":
|
|
353
|
+
return RT.instant(RT.lit_text(raw))
|
|
354
|
+
if t == "urn:law:std#Rational":
|
|
355
|
+
# §168.1 (DECISION-0252 п. 8): только каноническая запись, иная — отказ.
|
|
356
|
+
return RT.rat(raw)
|
|
357
|
+
if t == "urn:law:std#Quantity" and a.get("unit"):
|
|
358
|
+
# §49 v1 (DECISION-0256 п. 7): значение и тег единицы как записаны.
|
|
359
|
+
return RT.lit_qty(raw, a.get("unit"))
|
|
360
|
+
if t == "urn:law:std#Boolean":
|
|
361
|
+
# DECISION-0266: только JSON-булево; иная запись у оракула — TYPE_ERROR при чтении значения.
|
|
362
|
+
if not isinstance(raw, bool):
|
|
363
|
+
raise RT.Refused("значение Boolean не true/false вне среза раннера (§48)")
|
|
364
|
+
return raw
|
|
365
|
+
if t == "urn:law:std#Natural":
|
|
366
|
+
# DECISION-0266: неотрицательное целое; иное у оракула — TYPE_ERROR при чтении значения.
|
|
367
|
+
if isinstance(raw, bool) or not isinstance(raw, int) or raw < 0:
|
|
368
|
+
raise RT.Refused("значение Natural не неотрицательное целое вне среза раннера (§48)")
|
|
369
|
+
# Диапазон i64 §208 — общей воронкой (errata E-0202).
|
|
370
|
+
return RT.lit_int(raw)
|
|
371
|
+
raise RT.Refused("аргумент вне среза раннера: " + str(t))
|
|
372
|
+
|
|
373
|
+
def arg_type(a: dict):
|
|
374
|
+
"""Тип аргумента, если он известен: у `value` — из узла, у `const_ref` —
|
|
375
|
+
из объявления константы; `entity_ref` типа не несёт (как у оракула).
|
|
376
|
+
Оракул разыменовывает константы §54 ДО сверки (`engine.evaluate`):
|
|
377
|
+
константа-сущность становится `entity_ref`, член перечисления — ссылкой
|
|
378
|
+
вне таблицы констант, и обе не сверяются. Поэтому тип берётся только у
|
|
379
|
+
константы std-типа."""
|
|
380
|
+
if a.get("kind") == "value":
|
|
381
|
+
return (a.get("type") or {}).get("name")
|
|
382
|
+
if a.get("kind") == "const_ref":
|
|
383
|
+
c = M.CONSTS.get(a.get("id"))
|
|
384
|
+
return c[0] if c and str(c[0]).startswith("urn:law:std#") else None
|
|
385
|
+
return None
|
|
386
|
+
|
|
387
|
+
def conforms(actual: str, expected: str) -> bool:
|
|
388
|
+
"""Совпадение типа с наследованием сущностей вверх от фактического."""
|
|
389
|
+
seen = set()
|
|
390
|
+
cur = actual
|
|
391
|
+
while cur is not None and cur not in seen:
|
|
392
|
+
if cur == expected:
|
|
393
|
+
return True
|
|
394
|
+
seen.add(cur)
|
|
395
|
+
cur = M.BASES.get(cur)
|
|
396
|
+
return False
|
|
397
|
+
|
|
398
|
+
def well_typed(lit: dict) -> bool:
|
|
399
|
+
"""Арность и типы аргументов утверждения против сигнатуры §189 (E-0046).
|
|
400
|
+
|
|
401
|
+
Зеркало `check_argument_types` оракула: несогласованное утверждение не
|
|
402
|
+
принимается, причина названа кодом TYPE_ERROR; `entity_ref` типа не несёт
|
|
403
|
+
и не проверяется; предикат без сигнатуры принимается (его никто не читает).
|
|
404
|
+
Проверка идёт ДО преобразования значений: тип вне среза в чужом слоте —
|
|
405
|
+
это TYPE_ERROR, а не отказ раннера."""
|
|
406
|
+
params = M.PARAMS.get(lit["predicate"])
|
|
407
|
+
if params is None:
|
|
408
|
+
return True
|
|
409
|
+
args = lit.get("args") or []
|
|
410
|
+
if len(args) != len(params):
|
|
411
|
+
ISSUES.append("TYPE_ERROR"); TYPE_ERROR_PREDICATES.add(lit["predicate"])
|
|
412
|
+
return False
|
|
413
|
+
for expected, a in zip(params, args):
|
|
414
|
+
actual = arg_type(a)
|
|
415
|
+
if actual is not None and not conforms(actual, expected):
|
|
416
|
+
ISSUES.append("TYPE_ERROR"); TYPE_ERROR_PREDICATES.add(lit["predicate"])
|
|
417
|
+
return False
|
|
418
|
+
return True
|
|
419
|
+
|
|
420
|
+
def query_typed(lit: dict) -> bool:
|
|
421
|
+
params = M.PARAMS.get(lit["predicate"])
|
|
422
|
+
if params is None:
|
|
423
|
+
return False
|
|
424
|
+
args = lit.get("args") or []
|
|
425
|
+
if len(args) != len(params):
|
|
426
|
+
return False
|
|
427
|
+
return all(arg_type(a) is None or conforms(arg_type(a), expected)
|
|
428
|
+
for expected, a in zip(params, args))
|
|
429
|
+
|
|
430
|
+
def typed_arg(a: dict) -> tuple:
|
|
431
|
+
"""Литерал-терм §53 как (значение, тип); тип сущности у entity_ref — "entity"."""
|
|
432
|
+
k = a.get("kind")
|
|
433
|
+
if k == "entity_ref":
|
|
434
|
+
return a["id"], "entity"
|
|
435
|
+
if k == "const_ref":
|
|
436
|
+
# DECISION-0273: параметр A3 в вопросе `term` — за срезом раннера (значение на legal_time дела).
|
|
437
|
+
if a.get("id") in M.A3_TYPES:
|
|
438
|
+
raise RT.Refused("параметр A3 в вопросе вне среза раннера: " + str(a.get("id")))
|
|
439
|
+
c = M.CONSTS.get(a.get("id"))
|
|
440
|
+
if c is None:
|
|
441
|
+
return RT.sym(str(a.get("id"))), "enum"
|
|
442
|
+
return c[1], c[0]
|
|
443
|
+
if k == "value":
|
|
444
|
+
t = (a.get("type") or {}).get("name")
|
|
445
|
+
if t in (MON, DEC, INT, TEXT, DATE, RAT, QTY, BOOL, NAT, INSTANT):
|
|
446
|
+
return arg(a), t
|
|
447
|
+
raise RT.Refused("аргумент вне среза раннера: " + str(t))
|
|
448
|
+
raise RT.Refused("узел терма вне среза раннера: " + str(k))
|
|
449
|
+
|
|
450
|
+
def slot_typed(pred: str, col: int, v) -> tuple:
|
|
451
|
+
t = M.PARAMS[pred][col]
|
|
452
|
+
if isinstance(v, str):
|
|
453
|
+
ent, txt = v in ENTITIES, v in TEXTS
|
|
454
|
+
if ent and txt:
|
|
455
|
+
raise RT.Refused("одна запись подана делом и сущностью, и Text: вид результата не определён в срезе")
|
|
456
|
+
if ent:
|
|
457
|
+
return v, "entity"
|
|
458
|
+
if txt:
|
|
459
|
+
return v, TEXT
|
|
460
|
+
return v, (TEXT if t == TEXT else "entity")
|
|
461
|
+
if isinstance(v, bool):
|
|
462
|
+
return v, BOOL
|
|
463
|
+
if t in (MON, DEC, INT, DATE, RAT, QTY, NAT, INSTANT):
|
|
464
|
+
return v, t
|
|
465
|
+
return v, ("enum" if isinstance(v, RT.Sym) else "entity")
|
|
466
|
+
|
|
467
|
+
def eval_term(node: dict, env: dict) -> tuple:
|
|
468
|
+
k = node.get("kind")
|
|
469
|
+
if k == "var":
|
|
470
|
+
if node["var"] not in env:
|
|
471
|
+
raise RT.Refused("несвязанная переменная терма: " + str(node["var"]))
|
|
472
|
+
return env[node["var"]]
|
|
473
|
+
if k == "value" and (node.get("type") or {}).get("name") == RAT:
|
|
474
|
+
# ERR-E0005: операнд вычисления разбирается как у оракула — сократимая
|
|
475
|
+
# запись нормализуется; каноничность §168.1 — у записей, не у операнда.
|
|
476
|
+
return RT.lit_rat_term(node.get("value")), RAT
|
|
477
|
+
if k in ("value", "entity_ref", "const_ref"):
|
|
478
|
+
return typed_arg(node)
|
|
479
|
+
if k == "binary":
|
|
480
|
+
lv, lt = eval_term(node["left"], env)
|
|
481
|
+
rv, rt = eval_term(node["right"], env)
|
|
482
|
+
op = node["op"]
|
|
483
|
+
if op in CMP_OPS:
|
|
484
|
+
# §57 (`values.binary`, `_check_same_kind`): валюта, единица, затем вид.
|
|
485
|
+
if lt == rt == MON and lv.cur != rv.cur:
|
|
486
|
+
raise RT.TermError("CURRENCY_MISMATCH")
|
|
487
|
+
if lt == rt == QTY and lv.unit != rv.unit:
|
|
488
|
+
raise RT.TermError("DIMENSION_MISMATCH")
|
|
489
|
+
if {lt, rt} == {DATE, INSTANT}:
|
|
490
|
+
raise RT.TermError("DATE_INSTANT_MISMATCH")
|
|
491
|
+
if norm(lt) != norm(rt):
|
|
492
|
+
raise RT.TermError("TYPE_ERROR")
|
|
493
|
+
if lt == BOOL:
|
|
494
|
+
# §57 (DECISION-0266): Boolean сравнимо только на равенство; порядок — TYPE_ERROR оракула.
|
|
495
|
+
if op not in ("eq", "ne"):
|
|
496
|
+
raise RT.TermError("TYPE_ERROR")
|
|
497
|
+
return RT.cmp(lv, op, rv), BOOL
|
|
498
|
+
if lt not in NUM and lt not in (DATE, INSTANT):
|
|
499
|
+
raise RT.Refused("операция вне среза раннера: " + op)
|
|
500
|
+
return RT.cmp(lv, op, rv), BOOL
|
|
501
|
+
if lt not in NUM or rt not in NUM:
|
|
502
|
+
raise RT.Refused(f"арифметика вне среза (§58): не числовой операнд {op}")
|
|
503
|
+
if op not in ("add", "sub", "mul", "div", "mod"):
|
|
504
|
+
raise RT.Refused("операция вне среза раннера: " + op)
|
|
505
|
+
# §58 `values.binary` (DECISION-0252 п. 8): вид исхода — по значению;
|
|
506
|
+
# ошибка значения — TermError, ответ вопроса `term` — RUNTIME_ERROR.
|
|
507
|
+
v = RT.arith(op, lv, rv, KIND_CODE[lt], KIND_CODE[rt])
|
|
508
|
+
if isinstance(v, RT.Rat):
|
|
509
|
+
return v, RAT
|
|
510
|
+
if isinstance(v, RT.Money):
|
|
511
|
+
return v, MON
|
|
512
|
+
if isinstance(v, RT.Qty):
|
|
513
|
+
return v, QTY
|
|
514
|
+
return v, (INT if norm(lt) == norm(rt) == INT else DEC)
|
|
515
|
+
if k == "call" and node.get("function") in M.FUNCTIONS:
|
|
516
|
+
# DECISION-0273: функция §46 модуля; арность и типы аргументов — TYPE_ERROR
|
|
517
|
+
# оракула (`_require_type`), вид результата — объявленный.
|
|
518
|
+
f, params, returns = M.FUNCTIONS[node["function"]]
|
|
519
|
+
args = [eval_term(x, env) for x in node.get("args") or []]
|
|
520
|
+
if len(args) != len(params):
|
|
521
|
+
raise RT.TermError("TYPE_ERROR")
|
|
522
|
+
for (_v, t), want in zip(args, params):
|
|
523
|
+
ok = norm(t) == norm(want) if want.startswith("urn:law:std#") else t in ("enum", want)
|
|
524
|
+
if not ok:
|
|
525
|
+
raise RT.TermError("TYPE_ERROR")
|
|
526
|
+
return f(*[v for v, _t in args]), (returns if returns.startswith("urn:law:std#") else "enum")
|
|
527
|
+
if k == "call":
|
|
528
|
+
fn = str(node.get("function")).split("#")[-1]
|
|
529
|
+
args = [eval_term(x, env) for x in node.get("args") or []]
|
|
530
|
+
t = call_kind(fn, [x[1] for x in args])
|
|
531
|
+
if fn == "round":
|
|
532
|
+
# DECISION-0259: политика §50 проверяется рантаймом; виды вопроса точные.
|
|
533
|
+
return RT.round_mode(args[0][0], args[1][0], args[2][0],
|
|
534
|
+
KIND_CODE[args[0][1]], KIND_CODE[args[1][1]]), t
|
|
535
|
+
if fn == "div_round":
|
|
536
|
+
return RT.div_round(args[0][0], args[1][0], args[2][0], args[3][0],
|
|
537
|
+
KIND_CODE[args[0][1]], KIND_CODE[args[1][1]], KIND_CODE[args[2][1]]), t
|
|
538
|
+
return getattr(RT, RT_NAME.get(fn, fn))(*[x[0] for x in args]), t
|
|
539
|
+
if k == "aggregate":
|
|
540
|
+
return aggregate(node)
|
|
541
|
+
if k == "if":
|
|
542
|
+
# §56 (E-0180, DECISION-0270): чистое data-условие, ветвь вычисляется лениво.
|
|
543
|
+
return eval_term(node["then"] if data_cond(node.get("condition"), env) else node["else"], env)
|
|
544
|
+
raise RT.Refused("узел терма вне среза раннера: " + str(k))
|
|
545
|
+
|
|
546
|
+
def data_cond(f: dict, env: dict) -> bool:
|
|
547
|
+
"""Data-условие §56 (`values.is_data_condition`): boolean, comparison, and, or."""
|
|
548
|
+
k = (f or {}).get("kind")
|
|
549
|
+
if k == "boolean":
|
|
550
|
+
return bool(f.get("value"))
|
|
551
|
+
if k == "comparison":
|
|
552
|
+
return bool(eval_term({"kind": "binary", "op": f["op"], "left": f["left"], "right": f["right"]}, env)[0])
|
|
553
|
+
if k == "and":
|
|
554
|
+
return all(data_cond(x, env) for x in f.get("items") or [])
|
|
555
|
+
if k == "or":
|
|
556
|
+
return any(data_cond(x, env) for x in f.get("items") or [])
|
|
557
|
+
raise RT.Refused("условие терма §56 вне data-условия вне среза раннера: " + str(k))
|
|
558
|
+
|
|
559
|
+
def materialize(comp: dict) -> list:
|
|
560
|
+
"""Материализация §52.1: элементы в порядке канонических байтов, distinct — множество."""
|
|
561
|
+
if (comp or {}).get("kind") != "comprehension":
|
|
562
|
+
raise RT.Refused("aggregate.input вне среза раннера")
|
|
563
|
+
if comp.get("binders"):
|
|
564
|
+
raise RT.Refused("связки comprehension вне среза раннера")
|
|
565
|
+
atoms, guards = [], []
|
|
566
|
+
flatten(comp["generator"], atoms, guards)
|
|
567
|
+
if not atoms:
|
|
568
|
+
raise RT.Refused("тело без перебираемых атомов вне среза генератора")
|
|
569
|
+
for a in atoms:
|
|
570
|
+
if a.get("polarity") != "positive":
|
|
571
|
+
raise RT.Refused("отрицательный литерал в теле вне L0-среза")
|
|
572
|
+
if a["predicate"] not in M.PARAMS:
|
|
573
|
+
raise RT.Refused("предикат comprehension не объявлен в пакете: " + str(a["predicate"]))
|
|
574
|
+
if len(a.get("args") or []) != len(M.PARAMS[a["predicate"]]):
|
|
575
|
+
raise RT.Refused("арность атома тела не совпадает с сигнатурой §189")
|
|
576
|
+
out, seen = [], set()
|
|
577
|
+
# L1 (DECISION-0237): перечисляются только установленные строки (TRUE_ONLY) —
|
|
578
|
+
# в таблицах модели лежат и строки с одной отрицательной либо спорной опорой.
|
|
579
|
+
rows_of = M.established_rows if LAYER == "L1" else (lambda urn: M.TABLES[urn])
|
|
580
|
+
|
|
581
|
+
def walk(i: int, env: dict) -> None:
|
|
582
|
+
if i == len(atoms):
|
|
583
|
+
for g in guards:
|
|
584
|
+
if not RT.cmp(eval_term(g["left"], env)[0], g["op"], eval_term(g["right"], env)[0]):
|
|
585
|
+
return
|
|
586
|
+
val = eval_term(comp["element"], env)
|
|
587
|
+
key = canon(literal(val))
|
|
588
|
+
if comp["distinct"]:
|
|
589
|
+
if key in seen:
|
|
590
|
+
return
|
|
591
|
+
seen.add(key)
|
|
592
|
+
out.append((key, val))
|
|
593
|
+
return
|
|
594
|
+
a = atoms[i]
|
|
595
|
+
for row in rows_of(a["predicate"]):
|
|
596
|
+
nxt = dict(env)
|
|
597
|
+
ok = True
|
|
598
|
+
for j, x in enumerate(a["args"]):
|
|
599
|
+
if x.get("kind") == "var":
|
|
600
|
+
b = nxt.get(x["var"])
|
|
601
|
+
if b is None:
|
|
602
|
+
nxt[x["var"]] = slot_typed(a["predicate"], j, row[j])
|
|
603
|
+
elif not RT.eq(b[0], row[j]):
|
|
604
|
+
ok = False
|
|
605
|
+
elif not RT.eq(eval_term(x, nxt)[0], row[j]):
|
|
606
|
+
ok = False
|
|
607
|
+
if not ok:
|
|
608
|
+
break
|
|
609
|
+
if ok:
|
|
610
|
+
walk(i + 1, nxt)
|
|
611
|
+
|
|
612
|
+
walk(0, {})
|
|
613
|
+
out.sort(key=lambda kv: kv[0].encode("utf-8"))
|
|
614
|
+
return [v for _, v in out]
|
|
615
|
+
|
|
616
|
+
def aggregate(node: dict) -> tuple:
|
|
617
|
+
items = materialize(node.get("input"))
|
|
618
|
+
op = str(node.get("op"))
|
|
619
|
+
if op == "count":
|
|
620
|
+
return RT.dec(str(len(items))), INT
|
|
621
|
+
if op == "only":
|
|
622
|
+
# §51 (errata E-0256): функциональный lookup. Политика — TYPE_ERROR у
|
|
623
|
+
# оракула; пусто и два различных значения (канонические байты §208) —
|
|
624
|
+
# ошибки значения RUNTIME_ERROR, код тот же, что у оракула.
|
|
625
|
+
if node.get("options"):
|
|
626
|
+
raise RT.TermError("TYPE_ERROR")
|
|
627
|
+
if not items:
|
|
628
|
+
raise RT.TermError("EMPTY_AGGREGATE")
|
|
629
|
+
if len({canon(literal(it)) for it in items}) > 1:
|
|
630
|
+
raise RT.TermError("CARDINALITY_CONFLICT")
|
|
631
|
+
return items[0][0], norm(items[0][1])
|
|
632
|
+
if op not in ("sum", "min", "max"):
|
|
633
|
+
raise RT.Refused("агрегат вне среза раннера: " + op)
|
|
634
|
+
if not items:
|
|
635
|
+
empty = (node.get("options") or {}).get("empty")
|
|
636
|
+
if empty is None:
|
|
637
|
+
raise RT.Refused(f"EMPTY_AGGREGATE: {op} над пустой коллекцией без явной empty-политики (§59)")
|
|
638
|
+
if empty.get("kind") != "value":
|
|
639
|
+
# `from_literal_term` оракула: политика — LiteralTerm §53; иной узел
|
|
640
|
+
# даёт TYPE_ERROR раньше сверки вида.
|
|
641
|
+
raise RT.TermError("TYPE_ERROR")
|
|
642
|
+
value = typed_arg(empty)
|
|
643
|
+
# errata E-0195: вид политики обязан совпасть с видом элемента. У оракула
|
|
644
|
+
# это issue `TYPE_ERROR` с текстом «aggregate.empty: вид политики integer
|
|
645
|
+
# не совпадает с видом элемента коллекции money (§59)»; порождённый
|
|
646
|
+
# модуль сообщений не печатает вовсе, и байтовый контракт среза здесь —
|
|
647
|
+
# КОД issue, который тот же. До правки коллекция Money с политикой
|
|
648
|
+
# Integer отвечала `Integer 0` и `COMPUTED` — другой ответ, не меньший.
|
|
649
|
+
expected = EMPTY_ELEMENT_KINDS.get(
|
|
650
|
+
(((node.get("input") or {}).get("variable") or {}).get("type") or {}).get("name"))
|
|
651
|
+
if expected is not None and norm(value[1]) != expected:
|
|
652
|
+
raise RT.TermError("TYPE_ERROR")
|
|
653
|
+
return value
|
|
654
|
+
acc = items[0]
|
|
655
|
+
for it in items[1:]:
|
|
656
|
+
if acc[1] not in NUM or norm(it[1]) != norm(acc[1]):
|
|
657
|
+
raise RT.Refused(f"арифметика вне среза (§58): виды операндов не совпадают у {op}")
|
|
658
|
+
if op == "sum":
|
|
659
|
+
acc = (RT.add(acc[0], it[0]), acc[1])
|
|
660
|
+
elif RT.cmp(it[0], "lt" if op == "min" else "gt", acc[0]):
|
|
661
|
+
acc = it
|
|
662
|
+
if acc[1] not in NUM:
|
|
663
|
+
raise RT.Refused(f"арифметика вне среза (§58): не числовой операнд {op}")
|
|
664
|
+
# DECISION-0266: итог агрегата над Natural у оракула — вид `integer`, то есть Integer.
|
|
665
|
+
return acc[0], norm(acc[1])
|
|
666
|
+
|
|
667
|
+
def term_error_status(goal: dict):
|
|
668
|
+
"""Порт `whynot.term_error_blockers` + `term_error_status`: правила с
|
|
669
|
+
совместимой головой, их ошибки терма и конъюнкты без опоры — вглубь."""
|
|
670
|
+
nonlocal LAST_TERM_CODES
|
|
671
|
+
by_rule: dict = {}
|
|
672
|
+
for name, code in getattr(M, "_ISSUE_SEEN", ()):
|
|
673
|
+
if code != "MISSING_PARAMETER_VALUE":
|
|
674
|
+
by_rule.setdefault(name, []).append(code)
|
|
675
|
+
if not by_rule:
|
|
676
|
+
return None
|
|
677
|
+
index: dict = {}
|
|
678
|
+
for r in M.BACKCHAIN:
|
|
679
|
+
index.setdefault(r["head"]["predicate"], []).append(r)
|
|
680
|
+
|
|
681
|
+
def deref(a):
|
|
682
|
+
if a.get("kind") == "const_ref" and a.get("id") in M.BACKCHAIN_CONSTS:
|
|
683
|
+
return M.BACKCHAIN_CONSTS[a["id"]]
|
|
684
|
+
return a
|
|
685
|
+
|
|
686
|
+
def compatible(head, target):
|
|
687
|
+
if head.get("polarity", "positive") != target.get("polarity", "positive"):
|
|
688
|
+
return None
|
|
689
|
+
if len(head["args"]) != len(target["args"]):
|
|
690
|
+
return None
|
|
691
|
+
subst = {}
|
|
692
|
+
for h, g in zip(head["args"], target["args"]):
|
|
693
|
+
if g.get("kind") == "var":
|
|
694
|
+
continue
|
|
695
|
+
if h.get("kind") == "var":
|
|
696
|
+
if h["var"] in subst and _canon_json(subst[h["var"]]) != _canon_json(g):
|
|
697
|
+
return None
|
|
698
|
+
subst[h["var"]] = g
|
|
699
|
+
elif h.get("kind") not in ("entity_ref", "const_ref", "value"):
|
|
700
|
+
continue
|
|
701
|
+
elif _canon_json(h) != _canon_json(g):
|
|
702
|
+
return None
|
|
703
|
+
return subst
|
|
704
|
+
|
|
705
|
+
def truth(lit) -> str:
|
|
706
|
+
saved = (set(ENTITIES), set(TEXTS))
|
|
707
|
+
try:
|
|
708
|
+
st = M.status(lit["predicate"], [arg(x) for x in lit["args"]])
|
|
709
|
+
finally:
|
|
710
|
+
ENTITIES.clear(); ENTITIES.update(saved[0]); TEXTS.clear(); TEXTS.update(saved[1])
|
|
711
|
+
return MIRROR[st] if lit.get("polarity") == "negative" else st
|
|
712
|
+
|
|
713
|
+
found, visited = set(), set()
|
|
714
|
+
|
|
715
|
+
def walk(target) -> None:
|
|
716
|
+
key = _canon_json(target)
|
|
717
|
+
if key in visited:
|
|
718
|
+
return
|
|
719
|
+
visited.add(key)
|
|
720
|
+
for r in index.get(target.get("predicate"), ()):
|
|
721
|
+
subst = compatible(r["head"], target)
|
|
722
|
+
if subst is None:
|
|
723
|
+
continue
|
|
724
|
+
w = r["window"]
|
|
725
|
+
if w and ((w.get("start") and RT.legal_time() < w["start"])
|
|
726
|
+
or (w.get("end") and RT.legal_time() >= w["end"])):
|
|
727
|
+
continue
|
|
728
|
+
found.update(by_rule.get(r["name"], ()))
|
|
729
|
+
for _status, lit in r["body"]:
|
|
730
|
+
if lit.get("kind") != "literal":
|
|
731
|
+
continue # DECISION-0274: гарды в backchain — только для пути судьи
|
|
732
|
+
args = lit.get("args") or []
|
|
733
|
+
if any(a.get("kind") == "wildcard" for a in args):
|
|
734
|
+
continue
|
|
735
|
+
# DECISION-0270: вычисляемый аргумент (binary/call/if) не заземляется
|
|
736
|
+
# обходом — как несвязанная переменная, конъюнкт идёт вглубь без оценки.
|
|
737
|
+
computed = lambda a: (a or {}).get("kind") in ("binary", "call", "if", "let", "match")
|
|
738
|
+
if any((a.get("kind") == "var" and (a["var"] not in subst or computed(subst[a["var"]])))
|
|
739
|
+
or computed(a) for a in args):
|
|
740
|
+
walk(dict(lit, args=[subst.get(a["var"], a) if a.get("kind") == "var" else a
|
|
741
|
+
for a in args]))
|
|
742
|
+
continue
|
|
743
|
+
ground = dict(lit, args=[subst[a["var"]] if a.get("kind") == "var" else a for a in args])
|
|
744
|
+
if truth(ground) == "NEITHER":
|
|
745
|
+
walk(ground)
|
|
746
|
+
|
|
747
|
+
walk(dict(goal, args=[deref(a) for a in goal.get("args") or []]))
|
|
748
|
+
LAST_TERM_CODES = set(found)
|
|
749
|
+
statuses = {TERM_ERROR_STATUS.get(c, "RUNTIME_ERROR") for c in found}
|
|
750
|
+
return next((st for st in TERM_ERROR_PRECEDENCE if st in statuses), None)
|
|
751
|
+
|
|
752
|
+
def rule_error_status(goal: dict, truth: str):
|
|
753
|
+
"""Статус результата, зависящего от упавшего правила: NEITHER — статус кода
|
|
754
|
+
(E-0105), иная истинность — итог §181.2; смешанные коды — порядок issue,
|
|
755
|
+
которого раннер не воспроизводит, — названный отказ."""
|
|
756
|
+
te = term_error_status(goal)
|
|
757
|
+
codes = set(LAST_TERM_CODES) if te is not None else set()
|
|
758
|
+
ct = carrier_type_error(goal)
|
|
759
|
+
if te is None and ct is None:
|
|
760
|
+
return None
|
|
761
|
+
if truth == "NEITHER":
|
|
762
|
+
return te or ct
|
|
763
|
+
if ct is not None:
|
|
764
|
+
codes.add("TYPE_ERROR")
|
|
765
|
+
kept = [c for c in codes if c in OUTCOME_CODES]
|
|
766
|
+
if not kept:
|
|
767
|
+
return "NON_EXECUTABLE"
|
|
768
|
+
if len(kept) == len(codes) and len(set(kept)) == 1:
|
|
769
|
+
return kept[0]
|
|
770
|
+
raise RT.Refused("статус итога §181.2 зависит от порядка issue вне среза раннера")
|
|
771
|
+
|
|
772
|
+
def carrier_type_error(goal: dict):
|
|
773
|
+
"""E-0243 §181.2: TYPE_ERROR of a fact blocks only its forward readers."""
|
|
774
|
+
if not TYPE_ERROR_PREDICATES:
|
|
775
|
+
return None
|
|
776
|
+
index = getattr(M, "CARRIER_EDGES", {})
|
|
777
|
+
seen = set()
|
|
778
|
+
|
|
779
|
+
def walk(predicate):
|
|
780
|
+
if predicate in seen:
|
|
781
|
+
return False
|
|
782
|
+
seen.add(predicate)
|
|
783
|
+
if predicate in TYPE_ERROR_PREDICATES:
|
|
784
|
+
return True
|
|
785
|
+
return any(walk(child) for child in index.get(predicate, ()))
|
|
786
|
+
return "TYPE_ERROR" if walk(goal.get("predicate")) else None
|
|
787
|
+
|
|
788
|
+
def _jhook_val(t: dict):
|
|
789
|
+
"""Значение литерал-терма для обхода суждения; множества сущностей и текстов дела не двигаются."""
|
|
790
|
+
saved = (set(ENTITIES), set(TEXTS))
|
|
791
|
+
try:
|
|
792
|
+
return arg(t)
|
|
793
|
+
finally:
|
|
794
|
+
ENTITIES.clear(); ENTITIES.update(saved[0]); TEXTS.clear(); TEXTS.update(saved[1])
|
|
795
|
+
|
|
796
|
+
JHOOKS = {"val": _jhook_val, "cell": lambda pred, col, v: literal(slot_typed(pred, col, v))}
|
|
797
|
+
|
|
798
|
+
def judgment_requests(goal: dict, status: str, conflict: bool, tail: str) -> list:
|
|
799
|
+
"""Обход запроса судье §47.3 (DECISION-0274) — порт в рантайме, здесь переводы термов."""
|
|
800
|
+
goal = dict(goal, polarity=goal.get("polarity", "positive"))
|
|
801
|
+
return RT.build_requests(M, RT.judgment_blockers(M, JHOOKS, goal, status, conflict), tail)
|
|
802
|
+
|
|
803
|
+
def key_conflict_blocks(predicate: str) -> bool:
|
|
804
|
+
"""E-0210/E-0242: only the forward closure of a conflicted relation blocks."""
|
|
805
|
+
tainted = set(M.key_conflict_predicates())
|
|
806
|
+
waiting = list(tainted)
|
|
807
|
+
while waiting:
|
|
808
|
+
source = waiting.pop()
|
|
809
|
+
for consumer in getattr(M, "KEY_READERS", {}).get(source, ()):
|
|
810
|
+
if consumer not in tainted:
|
|
811
|
+
tainted.add(consumer)
|
|
812
|
+
waiting.append(consumer)
|
|
813
|
+
return predicate in tainted
|
|
814
|
+
|
|
815
|
+
def calendar_setup() -> None:
|
|
816
|
+
"""Политика §86 и снимок §85 дела до вывода (DECISION-0245 п. 4): порт
|
|
817
|
+
`engine._resolve_deadline_policy` и `engine._calendar_snapshot`."""
|
|
818
|
+
ref = effective_context(case).get("deadlinePolicy")
|
|
819
|
+
policy = None
|
|
820
|
+
if ref is not None:
|
|
821
|
+
if M.CAL_SEMVER.startswith("0.1"):
|
|
822
|
+
if not isinstance(ref, dict):
|
|
823
|
+
raise RT.Refused("case.context.deadlinePolicy вне среза раннера (§86)")
|
|
824
|
+
policy = ref
|
|
825
|
+
elif not isinstance(ref, str):
|
|
826
|
+
raise RT.Refused("case.context.deadlinePolicy: в 0.2 ожидался DeadlinePolicyRef (DECISION-0111 §2.9)")
|
|
827
|
+
elif ref not in M.CAL_POLICIES:
|
|
828
|
+
raise RT.Refused("case.context.deadlinePolicy: ссылка не разрешена в закреплённом CLIR (§86)")
|
|
829
|
+
else:
|
|
830
|
+
policy = M.CAL_POLICIES[ref]
|
|
831
|
+
selected = (case.get("options") or {}).get("calendarSnapshot")
|
|
832
|
+
snapshot = None
|
|
833
|
+
if selected:
|
|
834
|
+
snapshot = next((c for c in M.CAL_SNAPSHOTS if c["id"] == selected), None)
|
|
835
|
+
if snapshot is None:
|
|
836
|
+
raise RT.Refused("MISSING_CALENDAR: calendar_snapshot не объявлен в программе (§85, §168.2)")
|
|
837
|
+
elif len(M.CAL_SNAPSHOTS) >= 2:
|
|
838
|
+
raise RT.Refused("AMBIGUOUS_CALENDAR: в программе несколько calendar_snapshot без выбора (§85, §168.2)")
|
|
839
|
+
elif M.CAL_SNAPSHOTS:
|
|
840
|
+
snapshot = M.CAL_SNAPSHOTS[0]
|
|
841
|
+
RT.set_calendar(policy, snapshot)
|
|
842
|
+
|
|
843
|
+
def rows_answer(query: dict, issues: list) -> dict:
|
|
844
|
+
"""Внутренний вид API `rows` (DECISION-0423 §2.1): установленные строки
|
|
845
|
+
вопроса литерал-термами §53, статус вычисления — как у вопроса о том же
|
|
846
|
+
отношении (конфликт ключа блокирует читателей, E-0242)."""
|
|
847
|
+
pred = query.get("predicate")
|
|
848
|
+
if pred not in M.PARAMS:
|
|
849
|
+
raise RT.Refused("предикат rows не объявлен в пакете: " + str(pred))
|
|
850
|
+
src = M.established_rows(pred) if LAYER == "L1" else M.TABLES.get(pred, [])
|
|
851
|
+
rows = [[literal(slot_typed(pred, col, v)) for col, v in enumerate(r)] for r in src]
|
|
852
|
+
rows.sort(key=lambda r: canon(r).encode("utf-8"))
|
|
853
|
+
evaluation = "NON_EXECUTABLE" if key_conflict_blocks(pred) else "COMPUTED"
|
|
854
|
+
return {"queryId": query["queryId"], "resultKind": "COLLECTION", "rows": rows,
|
|
855
|
+
"evaluationStatus": evaluation, "judgmentRequests": [], "issues": list(issues)}
|
|
856
|
+
|
|
857
|
+
# --- исполнение -----------------------------------------------------------
|
|
858
|
+
try:
|
|
859
|
+
RT.set_legal_time(str((case.get("context") or {}).get("legalTime", "")))
|
|
860
|
+
context = case.get("context") or {}
|
|
861
|
+
# E-0212 §135.1: a supplied decision is the status moment; a default is not.
|
|
862
|
+
if "decisionTime" in context and hasattr(M, "set_status_time"):
|
|
863
|
+
M.set_status_time(str(context["decisionTime"])[:10])
|
|
864
|
+
if hasattr(M, "CAL_SNAPSHOTS"):
|
|
865
|
+
calendar_setup()
|
|
866
|
+
for a in case.get("assertions") or []:
|
|
867
|
+
if a.get("valid") is not None:
|
|
868
|
+
raise RT.Refused("поле assertion.valid вне среза раннера (§72)")
|
|
869
|
+
lit = a["literal"]
|
|
870
|
+
if lit.get("polarity") != "positive" and (LAYER != "L1" or lit.get("polarity") != "negative"):
|
|
871
|
+
raise RT.Refused("§113: отрицательный факт вне L0-среза")
|
|
872
|
+
if lit["predicate"] == "urn:law:std#incompatible" and hasattr(M, "POSITIONS"):
|
|
873
|
+
# §105.1: явная несовместимость позиций — конфликт вне среза (DECISION-0242 K5).
|
|
874
|
+
raise RT.Refused("конфликт позиций §105.1 (incompatible) вне среза раннера (DECISION-0242 K5)")
|
|
875
|
+
if not well_typed(lit):
|
|
876
|
+
continue
|
|
877
|
+
if lit["predicate"] not in M.PARAMS:
|
|
878
|
+
continue
|
|
879
|
+
# §47.3.1 (E-0089, DECISION-0274): ответ судьи не от того органа или не по форме — не принят.
|
|
880
|
+
rejected = RT.judgment_answer(M, a)
|
|
881
|
+
if rejected is not None:
|
|
882
|
+
ISSUES.append(rejected)
|
|
883
|
+
continue
|
|
884
|
+
if LAYER == "L1":
|
|
885
|
+
M.register(lit["predicate"], [arg(x) for x in lit["args"]], lit["polarity"])
|
|
886
|
+
else:
|
|
887
|
+
M.register(lit["predicate"], [arg(x) for x in lit["args"]])
|
|
888
|
+
M.saturate()
|
|
889
|
+
if stats is not None and LAYER == "L1":
|
|
890
|
+
# Замер потребителя (DECISION-0237 этап 6): кандидаты, группы и
|
|
891
|
+
# наибольшая группа конфликта; в протокол ответа не входит.
|
|
892
|
+
stats.update(M.STATS)
|
|
893
|
+
# DECISION-0239 G3: ошибки значения агрегатов в правилах — issues модели.
|
|
894
|
+
ISSUES.extend(getattr(M, "RULE_ISSUES", []))
|
|
895
|
+
# DECISION-0242: диагностики эффектов полномочий §127–§128.
|
|
896
|
+
ISSUES.extend(getattr(M, "NORM_ISSUES", []))
|
|
897
|
+
ISSUES.extend(["KEY_CONFLICT"] * M.key_conflicts())
|
|
898
|
+
kind = query.get("kind")
|
|
899
|
+
if kind == "rows" and api:
|
|
900
|
+
return rows_answer(query, ISSUES)
|
|
901
|
+
if kind == "collect":
|
|
902
|
+
elements = materialize(query["comprehension"])
|
|
903
|
+
if any(isinstance(e[0], str) and e[0].startswith("urn:position:") for e in elements):
|
|
904
|
+
raise RT.Refused("позиция в элементе коллекции вне среза раннера (DECISION-0242 K4)")
|
|
905
|
+
return {"queryId": query["queryId"], "resultKind": "COLLECTION",
|
|
906
|
+
"value": [literal(e) for e in elements], "evaluationStatus": "COMPUTED", "issues": list(ISSUES)}
|
|
907
|
+
if kind == "term":
|
|
908
|
+
try:
|
|
909
|
+
value = literal(eval_term(query["term"], {}))
|
|
910
|
+
except RT.TermError as exc:
|
|
911
|
+
# §58/§175 (DECISION-0252 п. 8): ошибка значения вопроса — без значения;
|
|
912
|
+
# статус по таблице `queries.query_calc`: несовместимость видов —
|
|
913
|
+
# TYPE_ERROR, прочее — RUNTIME_ERROR.
|
|
914
|
+
status = "TYPE_ERROR" if exc.code in TYPE_ERROR_CODES else "RUNTIME_ERROR"
|
|
915
|
+
return {"queryId": query["queryId"], "resultKind": "DATA", "evaluationStatus": status,
|
|
916
|
+
"issues": ISSUES + [exc.code]}
|
|
917
|
+
return {"queryId": query["queryId"], "resultKind": "DATA", "value": value,
|
|
918
|
+
"evaluationStatus": "COMPUTED", "issues": list(ISSUES)}
|
|
919
|
+
if kind != "truth":
|
|
920
|
+
raise RT.Refused("вид запроса вне среза раннера: " + str(kind))
|
|
921
|
+
# Ключ атома у оракула §208 включает ТИП значения: запрос с Integer к
|
|
922
|
+
# утверждению Decimal той же величины атома не находит и отвечает NEITHER
|
|
923
|
+
# без issue. Рантайм сравнивает числа по величине, поэтому запрос
|
|
924
|
+
# сверяется с сигнатурой так же, как утверждение, но без TYPE_ERROR.
|
|
925
|
+
literal_q = query["literal"]
|
|
926
|
+
if any(x.get("kind") == "entity_ref" and str(x.get("id", "")).startswith("urn:position:")
|
|
927
|
+
for x in literal_q.get("args") or []):
|
|
928
|
+
raise RT.Refused("позиция в аргументе вопроса вне среза раннера (DECISION-0242 K4)")
|
|
929
|
+
tail = str(query["queryId"]).rsplit(":", 1)[-1]
|
|
930
|
+
if LAYER == "L1":
|
|
931
|
+
# §62/§63: статус атома по опорам обеих полярностей; отрицательный
|
|
932
|
+
# вопрос зеркалит стороны. Несогласованный по сигнатуре вопрос атома
|
|
933
|
+
# не находит — NEITHER.
|
|
934
|
+
status = "NEITHER"
|
|
935
|
+
if query_typed(literal_q):
|
|
936
|
+
status = M.status(literal_q["predicate"], [arg(x) for x in literal_q["args"]])
|
|
937
|
+
if literal_q.get("polarity") == "negative":
|
|
938
|
+
status = MIRROR[status]
|
|
939
|
+
evaluation = "COMPUTED"
|
|
940
|
+
conflict = literal_q["predicate"] in M.PARAMS and (literal_q["predicate"], RT.key(
|
|
941
|
+
[arg(x) for x in literal_q["args"]])) in getattr(M, "BLOCKED", ())
|
|
942
|
+
# §47.3 (DECISION-0274): судьи по лестнице §175 — конфликт старше судьи, судья старше ошибки терма.
|
|
943
|
+
requests = judgment_requests(literal_q, status, conflict, tail)
|
|
944
|
+
if conflict:
|
|
945
|
+
# §127: эффект заперт непобеждённой парой Power/Immunity.
|
|
946
|
+
evaluation = "UNRESOLVED_NORMATIVE_CONFLICT"
|
|
947
|
+
elif key_conflict_blocks(literal_q["predicate"]):
|
|
948
|
+
evaluation = "NON_EXECUTABLE"
|
|
949
|
+
elif requests:
|
|
950
|
+
evaluation = "REQUIRES_JUDGMENT"
|
|
951
|
+
elif term_error_status(literal_q) or carrier_type_error(literal_q):
|
|
952
|
+
evaluation = rule_error_status(literal_q, status)
|
|
953
|
+
elif status == "NEITHER" and hasattr(M, "BACKCHAIN"):
|
|
954
|
+
# E-0105 (§175/§185): NEITHER над ошибкой терма правила-кандидата.
|
|
955
|
+
evaluation = term_error_status(literal_q) or evaluation
|
|
956
|
+
return {"queryId": query["queryId"], "truthStatus": status, "evaluationStatus": evaluation,
|
|
957
|
+
"judgmentRequests": requests, "issues": list(ISSUES)}
|
|
958
|
+
hit = False
|
|
959
|
+
if query_typed(literal_q):
|
|
960
|
+
want = [arg(x) for x in literal_q["args"]]
|
|
961
|
+
rows = M.TABLES.get(literal_q["predicate"], [])
|
|
962
|
+
hit = any(len(r) == len(want) and all(RT.eq(v, want[i]) for i, v in enumerate(r)) for r in rows)
|
|
963
|
+
# Открытый мир: отсутствие строки — это NEITHER, а не FALSE_ONLY.
|
|
964
|
+
# §47.3 (DECISION-0274): судьи у модели L0 — тот же обход по `backchain`.
|
|
965
|
+
status0 = "TRUE_ONLY" if hit else "NEITHER"
|
|
966
|
+
requests0 = judgment_requests(literal_q, status0, False, tail)
|
|
967
|
+
# Error status is scoped through the compatible producer/backchain of the
|
|
968
|
+
# queried atom. An unrelated failed derivation remains an issue in the
|
|
969
|
+
# document but must not turn this query into TYPE_ERROR.
|
|
970
|
+
rule_error = rule_error_status(literal_q, status0)
|
|
971
|
+
return {"queryId": query["queryId"],
|
|
972
|
+
"truthStatus": status0,
|
|
973
|
+
"evaluationStatus": "NON_EXECUTABLE" if key_conflict_blocks(literal_q["predicate"])
|
|
974
|
+
else ("REQUIRES_JUDGMENT" if requests0 else (rule_error or "COMPUTED")),
|
|
975
|
+
"judgmentRequests": requests0,
|
|
976
|
+
"issues": list(ISSUES)}
|
|
977
|
+
except RT.TermError as exc:
|
|
978
|
+
# Ошибка значения вне вопроса `term` и вне правил модели (гард
|
|
979
|
+
# comprehension вопроса) — названный отказ, а не крах.
|
|
980
|
+
raise RT.Refused("ошибка значения вне среза раннера: " + exc.code) from None
|