arxo-codegen 0.4.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (152) hide show
  1. arxo_codegen/__init__.py +5 -0
  2. arxo_codegen/__main__.py +7 -0
  3. arxo_codegen/active-outcomes.json +6837 -0
  4. arxo_codegen/check.py +158 -0
  5. arxo_codegen/check_active_canaries.py +262 -0
  6. arxo_codegen/check_api.py +76 -0
  7. arxo_codegen/check_conformance.py +414 -0
  8. arxo_codegen/check_matrix.py +131 -0
  9. arxo_codegen/check_packages.py +281 -0
  10. arxo_codegen/check_semantic_probes.py +293 -0
  11. arxo_codegen/check_xlsx.py +320 -0
  12. arxo_codegen/consumer_runner.py +316 -0
  13. arxo_codegen/core_support.py +203 -0
  14. arxo_codegen/coverage.py +341 -0
  15. arxo_codegen/deliver.py +180 -0
  16. arxo_codegen/examples/xlsx/pricing.lawir.json +409 -0
  17. arxo_codegen/examples/xlsx/pricing.xlsx.json +52 -0
  18. arxo_codegen/excel_plan.py +602 -0
  19. arxo_codegen/harness/verify/ci/gates/backends/check_backends.py +1248 -0
  20. arxo_codegen/harness/verify/ci/gates/backends/l1_fixtures.py +463 -0
  21. arxo_codegen/inert-nodes.json +62 -0
  22. arxo_codegen/inert.py +91 -0
  23. arxo_codegen/inputs.py +59 -0
  24. arxo_codegen/package.py +565 -0
  25. arxo_codegen/plan.py +4053 -0
  26. arxo_codegen/recertify_registry.py +100 -0
  27. arxo_codegen/runtime-cert.json +5259 -0
  28. arxo_codegen/runtime_cert.py +233 -0
  29. arxo_codegen/survey.py +272 -0
  30. arxo_codegen/target_runner.py +101 -0
  31. arxo_codegen/targets/bsl/cert.bsl +113 -0
  32. arxo_codegen/targets/bsl/emit.py +356 -0
  33. arxo_codegen/targets/bsl/json.bsl +243 -0
  34. arxo_codegen/targets/bsl/runner.bsl +210 -0
  35. arxo_codegen/targets/bsl/runtime.bsl +858 -0
  36. arxo_codegen/targets/cs/Cert.cs +90 -0
  37. arxo_codegen/targets/cs/Runner.cs +154 -0
  38. arxo_codegen/targets/cs/Runtime.cs +243 -0
  39. arxo_codegen/targets/cs/emit.py +223 -0
  40. arxo_codegen/targets/dart/cert.dart +99 -0
  41. arxo_codegen/targets/dart/emit.py +206 -0
  42. arxo_codegen/targets/dart/json.dart +166 -0
  43. arxo_codegen/targets/dart/runner.dart +154 -0
  44. arxo_codegen/targets/dart/runtime.dart +293 -0
  45. arxo_codegen/targets/erl/cert.erl +64 -0
  46. arxo_codegen/targets/erl/emit.py +216 -0
  47. arxo_codegen/targets/erl/rt.erl +238 -0
  48. arxo_codegen/targets/erl/runner.erl +130 -0
  49. arxo_codegen/targets/ex/cert.exs +62 -0
  50. arxo_codegen/targets/ex/emit.py +204 -0
  51. arxo_codegen/targets/ex/runner.exs +124 -0
  52. arxo_codegen/targets/ex/runtime.ex +227 -0
  53. arxo_codegen/targets/go/apiBase.go +483 -0
  54. arxo_codegen/targets/go/apicheck.go +97 -0
  55. arxo_codegen/targets/go/cert.go +218 -0
  56. arxo_codegen/targets/go/emit.py +1806 -0
  57. arxo_codegen/targets/go/evaluate.go +1826 -0
  58. arxo_codegen/targets/go/runner.go +38 -0
  59. arxo_codegen/targets/go/runtime.go +2612 -0
  60. arxo_codegen/targets/java/Cert.java +91 -0
  61. arxo_codegen/targets/java/Json.java +148 -0
  62. arxo_codegen/targets/java/Runner.java +151 -0
  63. arxo_codegen/targets/java/Runtime.java +243 -0
  64. arxo_codegen/targets/java/emit.py +198 -0
  65. arxo_codegen/targets/kt/Cert.kt +67 -0
  66. arxo_codegen/targets/kt/Json.kt +103 -0
  67. arxo_codegen/targets/kt/Runner.kt +126 -0
  68. arxo_codegen/targets/kt/Runtime.kt +196 -0
  69. arxo_codegen/targets/kt/emit.py +195 -0
  70. arxo_codegen/targets/lua/cert.lua +82 -0
  71. arxo_codegen/targets/lua/emit.py +250 -0
  72. arxo_codegen/targets/lua/json.lua +133 -0
  73. arxo_codegen/targets/lua/runner.lua +167 -0
  74. arxo_codegen/targets/lua/runtime.lua +541 -0
  75. arxo_codegen/targets/php/cert.php +84 -0
  76. arxo_codegen/targets/php/emit.py +229 -0
  77. arxo_codegen/targets/php/json.php +144 -0
  78. arxo_codegen/targets/php/runner.php +189 -0
  79. arxo_codegen/targets/php/runtime.php +660 -0
  80. arxo_codegen/targets/py/api_base.py +334 -0
  81. arxo_codegen/targets/py/apicheck.py +51 -0
  82. arxo_codegen/targets/py/cert.py +140 -0
  83. arxo_codegen/targets/py/emit.py +1140 -0
  84. arxo_codegen/targets/py/evaluate.py +980 -0
  85. arxo_codegen/targets/py/runner.py +38 -0
  86. arxo_codegen/targets/py/runtime.py +1634 -0
  87. arxo_codegen/targets/rb/cert.rb +73 -0
  88. arxo_codegen/targets/rb/emit.py +203 -0
  89. arxo_codegen/targets/rb/runner.rb +130 -0
  90. arxo_codegen/targets/rb/runtime.rb +255 -0
  91. arxo_codegen/targets/rs/Cargo.lock +142 -0
  92. arxo_codegen/targets/rs/Cargo.toml +16 -0
  93. arxo_codegen/targets/rs/cert.rs +106 -0
  94. arxo_codegen/targets/rs/emit.py +233 -0
  95. arxo_codegen/targets/rs/prefetch.rs +1 -0
  96. arxo_codegen/targets/rs/runner.rs +181 -0
  97. arxo_codegen/targets/rs/runtime.rs +448 -0
  98. arxo_codegen/targets/scala/Cert.scala +77 -0
  99. arxo_codegen/targets/scala/Json.scala +140 -0
  100. arxo_codegen/targets/scala/Runner.scala +121 -0
  101. arxo_codegen/targets/scala/Runtime.scala +269 -0
  102. arxo_codegen/targets/scala/emit.py +193 -0
  103. arxo_codegen/targets/swift/cert.swift +105 -0
  104. arxo_codegen/targets/swift/emit.py +231 -0
  105. arxo_codegen/targets/swift/json.swift +208 -0
  106. arxo_codegen/targets/swift/runner.swift +133 -0
  107. arxo_codegen/targets/swift/runtime.swift +528 -0
  108. arxo_codegen/targets/ts/apiBase.ts +250 -0
  109. arxo_codegen/targets/ts/apicheck.ts +37 -0
  110. arxo_codegen/targets/ts/cert.ts +96 -0
  111. arxo_codegen/targets/ts/emit.py +1214 -0
  112. arxo_codegen/targets/ts/evaluate.ts +894 -0
  113. arxo_codegen/targets/ts/package-lock.json +45 -0
  114. arxo_codegen/targets/ts/package.json +9 -0
  115. arxo_codegen/targets/ts/runner.ts +31 -0
  116. arxo_codegen/targets/ts/runtime.ts +1369 -0
  117. arxo_codegen/targets/xlsx/README.ru.md +176 -0
  118. arxo_codegen/targets/xlsx/emit.py +198 -0
  119. arxo_codegen/temporal.py +59 -0
  120. arxo_codegen/test_alias_types.py +171 -0
  121. arxo_codegen/test_api_contract.py +89 -0
  122. arxo_codegen/test_calendar_policy.py +48 -0
  123. arxo_codegen/test_check.py +49 -0
  124. arxo_codegen/test_check_active_canaries.py +96 -0
  125. arxo_codegen/test_check_conformance.py +132 -0
  126. arxo_codegen/test_check_semantic_probes.py +85 -0
  127. arxo_codegen/test_check_xlsx.py +52 -0
  128. arxo_codegen/test_consumer_runner.py +175 -0
  129. arxo_codegen/test_core_support.py +80 -0
  130. arxo_codegen/test_e0212_decision_time.py +63 -0
  131. arxo_codegen/test_head_aggregate.py +71 -0
  132. arxo_codegen/test_inert.py +143 -0
  133. arxo_codegen/test_instant_types.py +113 -0
  134. arxo_codegen/test_key_conflict_scope.py +92 -0
  135. arxo_codegen/test_key_conflict_targets.py +17 -0
  136. arxo_codegen/test_package_layout.py +163 -0
  137. arxo_codegen/test_plan_language_version.py +44 -0
  138. arxo_codegen/test_positions_refusal.py +43 -0
  139. arxo_codegen/test_recertify_registry.py +77 -0
  140. arxo_codegen/test_ruby_target.py +41 -0
  141. arxo_codegen/test_runtime_cert.py +83 -0
  142. arxo_codegen/test_secondary_key_conflict_scope.py +17 -0
  143. arxo_codegen/test_stage_r.py +108 -0
  144. arxo_codegen/test_standalone_inputs.py +71 -0
  145. arxo_codegen/test_support.py +150 -0
  146. arxo_codegen/test_survey_inputs.py +115 -0
  147. arxo_codegen-0.4.0.dist-info/LICENSE +202 -0
  148. arxo_codegen-0.4.0.dist-info/METADATA +1673 -0
  149. arxo_codegen-0.4.0.dist-info/RECORD +152 -0
  150. arxo_codegen-0.4.0.dist-info/WHEEL +5 -0
  151. arxo_codegen-0.4.0.dist-info/entry_points.txt +13 -0
  152. arxo_codegen-0.4.0.dist-info/top_level.txt +1 -0
@@ -0,0 +1,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