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,894 @@
|
|
|
1
|
+
// Единый вычислительный вход TypeScript-цели (DECISION-0423 §2.2).
|
|
2
|
+
//
|
|
3
|
+
// Дело и вопрос приходят в канонической форме (§168, §172) — той же, что у
|
|
4
|
+
// векторов и оракула, — и `evaluate` возвращает ответ исполняемого профиля.
|
|
5
|
+
// Раннер и API вызывают только его, поэтому сверка аргументов §189, разворот
|
|
6
|
+
// контекста, насыщение и лестница статусов §175 у них одни. Состояние — на вызов
|
|
7
|
+
// (§2.3): модель строит `newModel()`, контекст рантайма ставится на время вызова,
|
|
8
|
+
// сопутствующие множества живут в замыкании. Ответ — БЕЗ доказательства и
|
|
9
|
+
// хэшей: профиль core-executable. Целые в значениях — носитель `{int: "цифры"}`.
|
|
10
|
+
import * as RT from "./runtime";
|
|
11
|
+
import { Val } from "./runtime";
|
|
12
|
+
import { newModel } from "./model";
|
|
13
|
+
|
|
14
|
+
export type EvaluateOptions = { stats?: Record<string, number>; api?: boolean };
|
|
15
|
+
|
|
16
|
+
// Канонические байты §208 значения: ключи по порядку, без пробелов, целое — цифрами.
|
|
17
|
+
export const canonJson = (v: any): string => {
|
|
18
|
+
if (v === null || v === undefined) return "null";
|
|
19
|
+
if (typeof v === "boolean") return v ? "true" : "false";
|
|
20
|
+
if (typeof v === "string") return JSON.stringify(v);
|
|
21
|
+
if (typeof v === "number") return String(v);
|
|
22
|
+
if (typeof v === "bigint") return v.toString();
|
|
23
|
+
if (Array.isArray(v)) return "[" + v.map(canonJson).join(",") + "]";
|
|
24
|
+
if (typeof v === "object" && "int" in v && Object.keys(v).length === 1) return String(v.int);
|
|
25
|
+
return "{" + Object.keys(v).sort().map((k) => JSON.stringify(k) + ":" + canonJson(v[k])).join(",") + "}";
|
|
26
|
+
};
|
|
27
|
+
|
|
28
|
+
// Порядок байтов UTF-8 строк без Buffer: для корректного Unicode он совпадает с
|
|
29
|
+
// порядком кодовых точек, поэтому модуль не зависит от Node (DECISION-0423 §2.7).
|
|
30
|
+
export const cmpUtf8 = (a: string, b: string): number => {
|
|
31
|
+
let i = 0;
|
|
32
|
+
let j = 0;
|
|
33
|
+
while (i < a.length && j < b.length) {
|
|
34
|
+
const x = a.codePointAt(i) as number;
|
|
35
|
+
const y = b.codePointAt(j) as number;
|
|
36
|
+
if (x !== y) return x < y ? -1 : 1;
|
|
37
|
+
i += x > 0xffff ? 2 : 1;
|
|
38
|
+
j += y > 0xffff ? 2 : 1;
|
|
39
|
+
}
|
|
40
|
+
const restA = i < a.length;
|
|
41
|
+
const restB = j < b.length;
|
|
42
|
+
return restA === restB ? 0 : restA ? 1 : -1;
|
|
43
|
+
};
|
|
44
|
+
|
|
45
|
+
// Ответ на вопрос `query` по делу `kase`. Названный отказ среза — `RT.Refused`;
|
|
46
|
+
// ошибка значения вне вопроса `term` и вне правил модели становится тем же отказом.
|
|
47
|
+
export const evaluate = (kase: any, query: any, opts: EvaluateOptions = {}): any => {
|
|
48
|
+
if (query?.kind === "positions")
|
|
49
|
+
throw new RT.Refused("POSITIONS_UNAVAILABLE: positions requires current norm ranks (W6)");
|
|
50
|
+
RT.validateTemporalCase(kase);
|
|
51
|
+
const prev = RT.beginContext();
|
|
52
|
+
try {
|
|
53
|
+
return evaluateIn(kase, query, opts);
|
|
54
|
+
} catch (e) {
|
|
55
|
+
if (e instanceof RT.TermError) throw new RT.Refused("ошибка значения вне среза раннера: " + e.code);
|
|
56
|
+
throw e;
|
|
57
|
+
} finally {
|
|
58
|
+
RT.endContext(prev);
|
|
59
|
+
}
|
|
60
|
+
};
|
|
61
|
+
|
|
62
|
+
const evaluateIn = (kase: any, query: any, opts: EvaluateOptions): any => {
|
|
63
|
+
const M = newModel();
|
|
64
|
+
for (const assertion of kase.assertions ?? [])
|
|
65
|
+
if (M.NODE_IDS.includes(assertion.id)) throw new RT.Refused("LDC-E1382: case assertion id collides with a program node");
|
|
66
|
+
RT.admitNumericInputs(kase, query, Object.keys(M.JUDGMENTS).length > 0);
|
|
67
|
+
|
|
68
|
+
const ISSUES: string[] = [];
|
|
69
|
+
const TYPE_ERROR_PREDICATES = new Set<string>();
|
|
70
|
+
// Запись строки, поданная делом как сущность и как Text, — в рантайме одно
|
|
71
|
+
// значение, у оракула — разные атомы. Для печати результата вид берётся по
|
|
72
|
+
// тому, как запись подана; одна запись в обеих ролях — названный отказ.
|
|
73
|
+
const ENTITIES = new Set<string>();
|
|
74
|
+
const TEXTS = new Set<string>();
|
|
75
|
+
|
|
76
|
+
const arg = (a: any): Val => {
|
|
77
|
+
if (a.kind === "entity_ref") { ENTITIES.add(a.id as string); return a.id as string; }
|
|
78
|
+
if (a.kind === "const_ref") {
|
|
79
|
+
// §54: разыменование предшествует исполнению; ниже ссылок нет.
|
|
80
|
+
const c = M.CONSTS[a.id];
|
|
81
|
+
// Член перечисления §38 либо имя без объявления: оракул оставляет такой
|
|
82
|
+
// const_ref в ключе атома как есть — значение есть идентичность.
|
|
83
|
+
if (c === undefined) return RT.sym(String(a.id));
|
|
84
|
+
return c[1];
|
|
85
|
+
}
|
|
86
|
+
if (a.kind !== "value") throw new RT.Refused("аргумент вне среза раннера: " + String(a.kind));
|
|
87
|
+
const t = a.type?.name;
|
|
88
|
+
// §208/§48 (errata E-0197, E-0202, E-0203): сырое поле литерала читается по
|
|
89
|
+
// грамматике §208, а не `String()`/`Number()` цели. `String(a.value)` давал
|
|
90
|
+
// литералу без `value` значение "undefined", а целое i64 приходило из
|
|
91
|
+
// JSON.parse уже потерявшим знаки.
|
|
92
|
+
const raw = a.value;
|
|
93
|
+
if (t === "urn:law:std#Money") return RT.litMoney(raw, a.currency);
|
|
94
|
+
if (t === "urn:law:std#Decimal") return RT.litDec(raw);
|
|
95
|
+
if (t === "urn:law:std#Integer") return RT.litInt(raw);
|
|
96
|
+
if (t === "urn:law:std#Text") { const s = RT.litText(raw); TEXTS.add(s); return s; }
|
|
97
|
+
if (t === "urn:law:std#Date") return RT.date(RT.litText(raw));
|
|
98
|
+
if (t === "urn:law:std#Instant") return RT.instant(RT.litText(raw));
|
|
99
|
+
// §168.1 (DECISION-0252 п. 8): только каноническая запись, иная — отказ.
|
|
100
|
+
if (t === "urn:law:std#Rational") return RT.rat(raw);
|
|
101
|
+
// §49 v1 (DECISION-0256 п. 7): значение и тег единицы как записаны.
|
|
102
|
+
if (t === "urn:law:std#Quantity" && a.unit) return RT.litQty(raw, a.unit);
|
|
103
|
+
if (t === "urn:law:std#Boolean") {
|
|
104
|
+
// DECISION-0266: только JSON-булево; иная запись у оракула — TYPE_ERROR при чтении значения.
|
|
105
|
+
if (typeof raw !== "boolean") throw new RT.Refused("значение Boolean не true/false вне среза раннера (§48)");
|
|
106
|
+
return raw;
|
|
107
|
+
}
|
|
108
|
+
if (t === "urn:law:std#Natural") {
|
|
109
|
+
// DECISION-0266: неотрицательное целое; иное у оракула — TYPE_ERROR при чтении значения.
|
|
110
|
+
const [k, text] = RT.rawLit(raw);
|
|
111
|
+
if (k !== "i" || text.startsWith("-")) throw new RT.Refused("значение Natural не неотрицательное целое вне среза раннера (§48)");
|
|
112
|
+
return RT.litInt(raw);
|
|
113
|
+
}
|
|
114
|
+
throw new RT.Refused("аргумент вне среза раннера: " + String(t));
|
|
115
|
+
};
|
|
116
|
+
|
|
117
|
+
// Тип аргумента, если известен: у value — из узла, у const_ref — из
|
|
118
|
+
// объявления константы; entity_ref типа не несёт (как у оракула).
|
|
119
|
+
const argType = (a: any): string | undefined => {
|
|
120
|
+
if (a.kind === "value") return a.type?.name;
|
|
121
|
+
// Оракул разыменовывает константы §54 до сверки: сущность и член
|
|
122
|
+
// перечисления типа не несут, тип — только у константы std-типа.
|
|
123
|
+
if (a.kind === "const_ref") {
|
|
124
|
+
const t = M.CONSTS[a.id]?.[0];
|
|
125
|
+
return t !== undefined && t.startsWith("urn:law:std#") ? t : undefined;
|
|
126
|
+
}
|
|
127
|
+
return undefined;
|
|
128
|
+
};
|
|
129
|
+
|
|
130
|
+
// Совпадение типа с наследованием сущностей вверх от фактического.
|
|
131
|
+
const conforms = (actual: string, expected: string): boolean => {
|
|
132
|
+
const seen = new Set<string>();
|
|
133
|
+
let cur: string | undefined = actual;
|
|
134
|
+
while (cur !== undefined && !seen.has(cur)) {
|
|
135
|
+
if (cur === expected) return true;
|
|
136
|
+
seen.add(cur);
|
|
137
|
+
cur = M.BASES[cur];
|
|
138
|
+
}
|
|
139
|
+
return false;
|
|
140
|
+
};
|
|
141
|
+
|
|
142
|
+
// Арность и типы аргументов утверждения против сигнатуры §189 (E-0046) —
|
|
143
|
+
// зеркало check_argument_types оракула: несогласованное утверждение не
|
|
144
|
+
// принимается, причина названа кодом TYPE_ERROR; entity_ref типа не несёт;
|
|
145
|
+
// предикат без сигнатуры принимается (его никто не читает). Проверка идёт
|
|
146
|
+
// ДО преобразования значений.
|
|
147
|
+
const wellTyped = (lit: any): boolean => {
|
|
148
|
+
const params = M.PARAMS[lit.predicate];
|
|
149
|
+
if (params === undefined) return true;
|
|
150
|
+
const args: any[] = lit.args ?? [];
|
|
151
|
+
if (args.length !== params.length) { ISSUES.push("TYPE_ERROR"); TYPE_ERROR_PREDICATES.add(lit.predicate); return false; }
|
|
152
|
+
for (let i = 0; i < args.length; i++) {
|
|
153
|
+
const actual = argType(args[i]);
|
|
154
|
+
if (actual !== undefined && !conforms(actual, params[i])) { ISSUES.push("TYPE_ERROR"); TYPE_ERROR_PREDICATES.add(lit.predicate); return false; }
|
|
155
|
+
}
|
|
156
|
+
return true;
|
|
157
|
+
};
|
|
158
|
+
|
|
159
|
+
const queryTyped = (lit: any): boolean => {
|
|
160
|
+
const params = M.PARAMS[lit.predicate];
|
|
161
|
+
if (params === undefined) return false;
|
|
162
|
+
const args: any[] = lit.args ?? [];
|
|
163
|
+
if (args.length !== params.length) return false;
|
|
164
|
+
return args.every((a, i) => argType(a) === undefined || conforms(argType(a) as string, params[i]));
|
|
165
|
+
};
|
|
166
|
+
|
|
167
|
+
// --- вопросы term §57–§59 и collect §52.1 -------------------------------------
|
|
168
|
+
// Срез раннера: comprehension по установленным атомам таблиц с гардами,
|
|
169
|
+
// элемент — ячейка или литерал; агрегаты count/sum/min/max; арифметика §58
|
|
170
|
+
// и функции рантайма над результатом. Вид результата выводится статически
|
|
171
|
+
// по тем же таблицам §58, что у плана: у оракула вид — часть значения
|
|
172
|
+
// (Integer 5 и Decimal 5 — разные байты), у рантайма — нет.
|
|
173
|
+
const INT = "urn:law:std#Integer", DEC = "urn:law:std#Decimal", MON = "urn:law:std#Money";
|
|
174
|
+
const TEXT = "urn:law:std#Text", DATE = "urn:law:std#Date", RAT = "urn:law:std#Rational";
|
|
175
|
+
const INSTANT = "urn:law:std#Instant";
|
|
176
|
+
const QTY = "urn:law:std#Quantity", BOOL = "urn:law:std#Boolean", NAT = "urn:law:std#Natural";
|
|
177
|
+
const CMP_OPS = ["lt", "le", "gt", "ge", "eq", "ne"];
|
|
178
|
+
type Typed = { v: Val; t: string }; // t — URN типа std либо "entity" | "enum"
|
|
179
|
+
type Env = Record<string, Typed>;
|
|
180
|
+
|
|
181
|
+
const isNumT = (t: string): boolean => t === INT || t === DEC || t === MON || t === RAT || t === QTY || t === NAT;
|
|
182
|
+
// DECISION-0252 п. 3: вид операнда для `arith` рантайма. У вопроса вид каждого
|
|
183
|
+
// значения известен точно — из литерала, слота или исхода операции.
|
|
184
|
+
const KIND_CODE: Record<string, string> = { [INT]: "i", [DEC]: "d", [MON]: "m", [RAT]: "r", [QTY]: "q", [NAT]: "i" };
|
|
185
|
+
// DECISION-0266: Natural считает как Integer (`values._TYPE_KINDS`), тип-строка при этом своя.
|
|
186
|
+
const norm = (t: string): string => (t === NAT ? INT : t);
|
|
187
|
+
// §59 (errata E-0195): вид элемента ПУСТОЙ коллекции несёт ОБЪЯВЛЕННЫЙ тип
|
|
188
|
+
// проекции comprehension §52.1 (`variable.type.name`) — единственный носитель
|
|
189
|
+
// вида, не зависящий от числа решений generator-а (порт
|
|
190
|
+
// `values.aggregate_element_type`). Таблица — порт `values._TYPE_KINDS` плюс
|
|
191
|
+
// `_OPAQUE_KINDS`: Natural и Integer у оракула один вид `integer`. Имя вне
|
|
192
|
+
// таблицы (сущность, перечисление, чужой символ) вида не предъявляет, и сверка
|
|
193
|
+
// МОЛЧИТ: Unknown есть «утверждать нечего», а не «совместимо со всем».
|
|
194
|
+
const EMPTY_ELEMENT_KINDS: Record<string, string> = {
|
|
195
|
+
[INT]: INT, [NAT]: INT, [DEC]: DEC, [MON]: MON, [TEXT]: TEXT, [DATE]: DATE,
|
|
196
|
+
[BOOL]: BOOL, [RAT]: RAT, [QTY]: QTY,
|
|
197
|
+
"urn:law:std#Instant": "urn:law:std#Instant",
|
|
198
|
+
"urn:law:std#Magnitude": "urn:law:std#Magnitude",
|
|
199
|
+
"urn:law:std#Bounds": "urn:law:std#Bounds",
|
|
200
|
+
};
|
|
201
|
+
|
|
202
|
+
const arithKind = (op: string, l: string, r: string): string => {
|
|
203
|
+
l = norm(l); r = norm(r);
|
|
204
|
+
if (!isNumT(l) || !isNumT(r)) throw new RT.Refused("арифметика вне среза (§58): не числовой операнд " + op);
|
|
205
|
+
if (op === "add" || op === "sub") {
|
|
206
|
+
if (l !== r) throw new RT.Refused("арифметика вне среза (§58): виды операндов не совпадают у " + op);
|
|
207
|
+
return l;
|
|
208
|
+
}
|
|
209
|
+
if (op === "mul") {
|
|
210
|
+
if (l === MON && r === MON) throw new RT.Refused("арифметика вне среза (§58): произведение двух размерных величин не определено");
|
|
211
|
+
if (l === INT || r === INT) return l === INT ? r : l;
|
|
212
|
+
return l === MON || r === MON ? MON : DEC;
|
|
213
|
+
}
|
|
214
|
+
if (op === "div") {
|
|
215
|
+
if ((l === DEC && r === DEC) || (l === MON && r === MON)) return DEC;
|
|
216
|
+
if (l === MON && r === DEC) return MON;
|
|
217
|
+
if (l === INT && r === INT) throw new RT.Refused("арифметика вне среза (§58): частное Integer/Integer — Rational");
|
|
218
|
+
throw new RT.Refused("арифметика вне среза (§58): пара вне таблицы делимых");
|
|
219
|
+
}
|
|
220
|
+
if (op === "mod") {
|
|
221
|
+
if (l !== INT || r !== INT) throw new RT.Refused("арифметика вне среза (§58): остаток только Integer mod Integer");
|
|
222
|
+
return INT;
|
|
223
|
+
}
|
|
224
|
+
throw new RT.Refused("операция вне среза раннера: " + op);
|
|
225
|
+
};
|
|
226
|
+
|
|
227
|
+
const callKind = (fn: string, ks: string[]): string => {
|
|
228
|
+
if (fn === "min" || fn === "max") {
|
|
229
|
+
if (ks.length !== 2 || !isNumT(ks[0]) || ks[0] !== ks[1]) throw new RT.Refused("арифметика вне среза (§58): виды операндов не совпадают у " + fn);
|
|
230
|
+
return ks[0];
|
|
231
|
+
}
|
|
232
|
+
if (fn === "abs") {
|
|
233
|
+
if (ks.length !== 1 || !isNumT(ks[0])) throw new RT.Refused("арифметика вне среза (§58): abs над нечисловым видом");
|
|
234
|
+
return ks[0];
|
|
235
|
+
}
|
|
236
|
+
if (fn === "pow") {
|
|
237
|
+
if (ks.length !== 2 || ks[0] !== INT || ks[1] !== INT) throw new RT.Refused("арифметика вне среза (§58): pow только Integer pow Integer");
|
|
238
|
+
return INT;
|
|
239
|
+
}
|
|
240
|
+
if (fn === "round") {
|
|
241
|
+
if (ks.length !== 3) throw new RT.Refused("round вне среза раннера: ожидаются значение, разряды и режим (§50)");
|
|
242
|
+
if (ks[0] === RAT) throw new RT.Refused("round над Rational вне среза генератора (§50: round не берёт Rational)");
|
|
243
|
+
if (!isNumT(ks[0])) throw new RT.Refused("round вне среза генератора: вид операнда не числовой (§50)");
|
|
244
|
+
policyKinds("round", ks.slice(1));
|
|
245
|
+
return ks[0];
|
|
246
|
+
}
|
|
247
|
+
if (fn === "div_round") {
|
|
248
|
+
// §50/§58 (E-0036, E-0170; DECISION-0259 п. 3): четыре пары видов.
|
|
249
|
+
if (ks.length !== 4) throw new RT.Refused("div_round вне среза генератора: ожидаются делимое, делитель, точность и режим (§50)");
|
|
250
|
+
if (ks[0] === RAT || ks[1] === RAT) throw new RT.Refused("div_round над Rational вне среза генератора (§50/§58, E-0036)");
|
|
251
|
+
const pairIs = (x: string, y: string): boolean => ks[0] === x && ks[1] === y;
|
|
252
|
+
if (!(pairIs(MON, DEC) || pairIs(QTY, DEC) || pairIs(DEC, DEC) || pairIs(MON, MON))) {
|
|
253
|
+
throw new RT.Refused("div_round вне среза генератора: пара видов вне E-0036/E-0170 (§50/§58)");
|
|
254
|
+
}
|
|
255
|
+
policyKinds("div_round", ks.slice(2));
|
|
256
|
+
return pairIs(MON, DEC) ? MON : pairIs(QTY, DEC) ? QTY : DEC;
|
|
257
|
+
}
|
|
258
|
+
if (fn === "month_start_of" || fn === "month_end_of") {
|
|
259
|
+
// §84.2 (E-0084, DECISION-0267): арность и вид — TYPE_ERROR оракула (`month_bound_value`).
|
|
260
|
+
if (ks.length !== 1 || ks[0] !== DATE) throw new RT.TermError("TYPE_ERROR");
|
|
261
|
+
return DATE;
|
|
262
|
+
}
|
|
263
|
+
if (fn === "hours_between" || fn === "minutes_between") {
|
|
264
|
+
// §84.1 (E-0028/E-0058; W2 часть 2): оба аргумента — Instant, иначе TYPE_ERROR оракула.
|
|
265
|
+
if (ks.length !== 2 || ks[0] !== INSTANT || ks[1] !== INSTANT) throw new RT.TermError("TYPE_ERROR");
|
|
266
|
+
return INT;
|
|
267
|
+
}
|
|
268
|
+
if (fn === "add_duration") throw new RT.Refused("add_duration в вопросе вне среза раннера (§84.1)");
|
|
269
|
+
throw new RT.Refused("функция вне среза раннера: " + fn);
|
|
270
|
+
};
|
|
271
|
+
// Имя функции рантайма по имени std-функции вопроса.
|
|
272
|
+
const RT_NAME: Record<string, string> = { month_start_of: "month_start", month_end_of: "month_end" };
|
|
273
|
+
|
|
274
|
+
// §50: точность — Integer либо Decimal, режим — Text (DECISION-0259 п. 3).
|
|
275
|
+
const policyKinds = (fn: string, ks: string[]): void => {
|
|
276
|
+
if (ks[0] !== INT && ks[0] !== DEC) throw new RT.Refused(fn + " вне среза генератора: точность не Integer и не Decimal (§50)");
|
|
277
|
+
if (ks[1] !== TEXT) throw new RT.Refused(fn + " вне среза генератора: режим не Text (§50)");
|
|
278
|
+
};
|
|
279
|
+
|
|
280
|
+
// Литерал-терм §53 как typed-значение; тип сущности у entity_ref — "entity".
|
|
281
|
+
const typedArg = (a: any): Typed => {
|
|
282
|
+
if (a.kind === "entity_ref") return { v: a.id as string, t: "entity" };
|
|
283
|
+
if (a.kind === "const_ref") {
|
|
284
|
+
// DECISION-0273: параметр A3 в вопросе `term` — за срезом раннера (значение на legalTime дела).
|
|
285
|
+
if (M.A3_TYPES[a.id] !== undefined) throw new RT.Refused("параметр A3 в вопросе вне среза раннера: " + String(a.id));
|
|
286
|
+
const c = M.CONSTS[a.id];
|
|
287
|
+
if (c === undefined) return { v: RT.sym(String(a.id)), t: "enum" };
|
|
288
|
+
return { v: c[1], t: c[0] };
|
|
289
|
+
}
|
|
290
|
+
if (a.kind === "value") {
|
|
291
|
+
const t = a.type?.name;
|
|
292
|
+
if (t === MON || t === DEC || t === INT || t === TEXT || t === DATE || t === RAT || t === QTY || t === BOOL || t === NAT || t === INSTANT) return { v: arg(a), t };
|
|
293
|
+
throw new RT.Refused("аргумент вне среза раннера: " + String(t));
|
|
294
|
+
}
|
|
295
|
+
throw new RT.Refused("узел терма вне среза раннера: " + String(a.kind));
|
|
296
|
+
};
|
|
297
|
+
|
|
298
|
+
const slotTyped = (pred: string, col: number, v: Val): Typed => {
|
|
299
|
+
const t = M.PARAMS[pred][col];
|
|
300
|
+
if (typeof v === "string") {
|
|
301
|
+
const ent = ENTITIES.has(v), txt = TEXTS.has(v);
|
|
302
|
+
if (ent && txt) throw new RT.Refused("одна запись подана делом и сущностью, и Text: вид результата не определён в срезе");
|
|
303
|
+
if (ent) return { v, t: "entity" };
|
|
304
|
+
if (txt) return { v, t: TEXT };
|
|
305
|
+
return { v, t: t === TEXT ? TEXT : "entity" };
|
|
306
|
+
}
|
|
307
|
+
if (typeof v === "boolean") return { v, t: BOOL };
|
|
308
|
+
if (t === MON || t === DEC || t === INT || t === DATE || t === RAT || t === QTY || t === NAT || t === INSTANT) return { v, t };
|
|
309
|
+
return { v, t: RT.isSym(v) ? "enum" : "entity" };
|
|
310
|
+
};
|
|
311
|
+
|
|
312
|
+
const flatten = (body: any, atoms: any[], guards: any[]): void => {
|
|
313
|
+
const k = body?.kind;
|
|
314
|
+
if (k === "status") {
|
|
315
|
+
if (body.status !== "established") throw new RT.Refused("статусный тест вне L0-среза: " + String(body.status));
|
|
316
|
+
atoms.push(body.formula);
|
|
317
|
+
} else if (k === "literal") atoms.push(body);
|
|
318
|
+
else if (k === "comparison") guards.push(body);
|
|
319
|
+
else if (k === "and" || k === "conjunction") {
|
|
320
|
+
for (const s of body.items ?? body.args ?? body.operands ?? []) flatten(s, atoms, guards);
|
|
321
|
+
} else throw new RT.Refused("форма тела вне среза генератора: " + String(k));
|
|
322
|
+
};
|
|
323
|
+
|
|
324
|
+
const evalTerm = (node: any, env: Env): Typed => {
|
|
325
|
+
const k = node.kind;
|
|
326
|
+
if (k === "var") {
|
|
327
|
+
const b = env[node.var];
|
|
328
|
+
if (b === undefined) throw new RT.Refused("несвязанная переменная терма: " + String(node.var));
|
|
329
|
+
return b;
|
|
330
|
+
}
|
|
331
|
+
if (k === "value" && node.type?.name === RAT) {
|
|
332
|
+
// ERR-E0005: операнд вычисления разбирается как у оракула — сократимая
|
|
333
|
+
// запись нормализуется; каноничность §168.1 — у записей, не у операнда.
|
|
334
|
+
return { v: RT.ratTerm(node.value), t: RAT };
|
|
335
|
+
}
|
|
336
|
+
if (k === "value" || k === "entity_ref" || k === "const_ref") return typedArg(node);
|
|
337
|
+
if (k === "binary") {
|
|
338
|
+
const l = evalTerm(node.left, env), r = evalTerm(node.right, env);
|
|
339
|
+
const op = String(node.op);
|
|
340
|
+
if (CMP_OPS.includes(op)) {
|
|
341
|
+
// §57 (`values.binary`, `_check_same_kind`): валюта, единица, затем вид.
|
|
342
|
+
if (l.t === MON && r.t === MON && (l.v as RT.Money).cur !== (r.v as RT.Money).cur) throw new RT.TermError("CURRENCY_MISMATCH");
|
|
343
|
+
if (l.t === QTY && r.t === QTY && (l.v as RT.Qty).unit !== (r.v as RT.Qty).unit) throw new RT.TermError("DIMENSION_MISMATCH");
|
|
344
|
+
if ((l.t === DATE && r.t === INSTANT) || (l.t === INSTANT && r.t === DATE)) throw new RT.TermError("DATE_INSTANT_MISMATCH");
|
|
345
|
+
if (norm(l.t) !== norm(r.t)) throw new RT.TermError("TYPE_ERROR");
|
|
346
|
+
if (l.t === BOOL) {
|
|
347
|
+
// §57 (DECISION-0266): Boolean сравнимо только на равенство; порядок — TYPE_ERROR оракула.
|
|
348
|
+
if (op !== "eq" && op !== "ne") throw new RT.TermError("TYPE_ERROR");
|
|
349
|
+
return { v: RT.cmp(l.v, op, r.v), t: BOOL };
|
|
350
|
+
}
|
|
351
|
+
if (!isNumT(l.t) && l.t !== DATE && l.t !== INSTANT) throw new RT.Refused("операция вне среза раннера: " + op);
|
|
352
|
+
// Исход сравнения — булево §57 (DECISION-0266: Boolean — значение рантайма).
|
|
353
|
+
return { v: RT.cmp(l.v, op, r.v), t: BOOL };
|
|
354
|
+
}
|
|
355
|
+
if (!isNumT(l.t) || !isNumT(r.t)) throw new RT.Refused("арифметика вне среза (§58): не числовой операнд " + op);
|
|
356
|
+
if (!["add", "sub", "mul", "div", "mod"].includes(op)) throw new RT.Refused("операция вне среза раннера: " + op);
|
|
357
|
+
// §58 `values.binary` (DECISION-0252 п. 8): вид исхода — по значению;
|
|
358
|
+
// ошибка значения — TermError, ответ вопроса `term` — RUNTIME_ERROR.
|
|
359
|
+
const v = RT.arith(op, l.v, r.v, KIND_CODE[l.t], KIND_CODE[r.t]);
|
|
360
|
+
if (v instanceof RT.Rat) return { v, t: RAT };
|
|
361
|
+
if (RT.isMoney(v)) return { v, t: MON };
|
|
362
|
+
if (RT.isQty(v)) return { v, t: QTY };
|
|
363
|
+
return { v, t: norm(l.t) === INT && norm(r.t) === INT ? INT : DEC };
|
|
364
|
+
}
|
|
365
|
+
if (k === "call" && M.FUNCTIONS[String(node.function)] !== undefined) {
|
|
366
|
+
// DECISION-0273: функция §46 модуля; арность и типы аргументов — TYPE_ERROR
|
|
367
|
+
// оракула (`_require_type`), вид результата — объявленный.
|
|
368
|
+
const uf = M.FUNCTIONS[String(node.function)];
|
|
369
|
+
const args = (node.args ?? []).map((a: any) => evalTerm(a, env));
|
|
370
|
+
if (args.length !== uf.params.length) throw new RT.TermError("TYPE_ERROR");
|
|
371
|
+
args.forEach((a: Typed, i: number) => {
|
|
372
|
+
const want = uf.params[i];
|
|
373
|
+
const ok = want.startsWith("urn:law:std#") ? norm(a.t) === norm(want) : a.t === "enum" || a.t === want;
|
|
374
|
+
if (!ok) throw new RT.TermError("TYPE_ERROR");
|
|
375
|
+
});
|
|
376
|
+
return { v: uf.fn(args.map((a: Typed) => a.v)), t: uf.returns.startsWith("urn:law:std#") ? uf.returns : "enum" };
|
|
377
|
+
}
|
|
378
|
+
if (k === "call") {
|
|
379
|
+
const fn = String(node.function).split("#").pop() as string;
|
|
380
|
+
const args = (node.args ?? []).map((a: any) => evalTerm(a, env));
|
|
381
|
+
const t = callKind(fn, args.map((a: Typed) => a.t));
|
|
382
|
+
if (fn === "round") {
|
|
383
|
+
// DECISION-0259: политика §50 проверяется рантаймом; виды вопроса точные.
|
|
384
|
+
return { v: RT.round_mode(args[0].v, args[1].v, args[2].v, KIND_CODE[args[0].t], KIND_CODE[args[1].t]), t };
|
|
385
|
+
}
|
|
386
|
+
if (fn === "div_round") {
|
|
387
|
+
return { v: RT.div_round(args[0].v, args[1].v, args[2].v, args[3].v,
|
|
388
|
+
KIND_CODE[args[0].t], KIND_CODE[args[1].t], KIND_CODE[args[2].t]), t };
|
|
389
|
+
}
|
|
390
|
+
const f = (RT as any)[RT_NAME[fn] ?? fn] as (...a: Val[]) => Val;
|
|
391
|
+
return { v: f(...args.map((a: Typed) => a.v)), t };
|
|
392
|
+
}
|
|
393
|
+
if (k === "aggregate") return aggregate(node);
|
|
394
|
+
if (k === "if") {
|
|
395
|
+
// §56 (E-0180, DECISION-0270): чистое data-условие, ветвь вычисляется лениво.
|
|
396
|
+
return dataCond(node.condition, env) ? evalTerm(node.then, env) : evalTerm(node.else, env);
|
|
397
|
+
}
|
|
398
|
+
throw new RT.Refused("узел терма вне среза раннера: " + String(k));
|
|
399
|
+
};
|
|
400
|
+
|
|
401
|
+
// Data-условие §56 (`values.is_data_condition`): boolean, comparison, and, or.
|
|
402
|
+
const dataCond = (f: any, env: Env): boolean => {
|
|
403
|
+
const k = f?.kind;
|
|
404
|
+
if (k === "boolean") return Boolean(f.value);
|
|
405
|
+
if (k === "comparison") {
|
|
406
|
+
const v = evalTerm({ kind: "binary", op: f.op, left: f.left, right: f.right }, env);
|
|
407
|
+
return v.v as unknown as boolean;
|
|
408
|
+
}
|
|
409
|
+
if (k === "and") return (f.items ?? []).every((x: any) => dataCond(x, env));
|
|
410
|
+
if (k === "or") return (f.items ?? []).some((x: any) => dataCond(x, env));
|
|
411
|
+
throw new RT.Refused("условие терма §56 вне data-условия вне среза раннера: " + String(k));
|
|
412
|
+
};
|
|
413
|
+
|
|
414
|
+
// Материализация §52.1: элементы в порядке канонических байтов, distinct — множество.
|
|
415
|
+
const materialize = (comp: any): Typed[] => {
|
|
416
|
+
if (comp?.kind !== "comprehension") throw new RT.Refused("aggregate.input вне среза раннера");
|
|
417
|
+
if ((comp.binders ?? []).length) throw new RT.Refused("связки comprehension вне среза раннера");
|
|
418
|
+
const atoms: any[] = [], guards: any[] = [];
|
|
419
|
+
flatten(comp.generator, atoms, guards);
|
|
420
|
+
if (!atoms.length) throw new RT.Refused("тело без перебираемых атомов вне среза генератора");
|
|
421
|
+
for (const a of atoms) {
|
|
422
|
+
if (a.polarity !== "positive") throw new RT.Refused("отрицательный литерал в теле вне L0-среза");
|
|
423
|
+
if (!(a.predicate in M.PARAMS)) throw new RT.Refused("предикат comprehension не объявлен в пакете: " + String(a.predicate));
|
|
424
|
+
if ((a.args ?? []).length !== M.PARAMS[a.predicate].length) throw new RT.Refused("арность атома тела не совпадает с сигнатурой §189");
|
|
425
|
+
}
|
|
426
|
+
const out: { key: string; val: Typed }[] = [];
|
|
427
|
+
const seen = new Set<string>();
|
|
428
|
+
const walk = (i: number, env: Env): void => {
|
|
429
|
+
if (i === atoms.length) {
|
|
430
|
+
for (const g of guards) {
|
|
431
|
+
if (!RT.cmp(evalTerm(g.left, env).v, g.op, evalTerm(g.right, env).v)) return;
|
|
432
|
+
}
|
|
433
|
+
const val = evalTerm(comp.element, env);
|
|
434
|
+
const key = canon(literal(val));
|
|
435
|
+
if (comp.distinct) { if (seen.has(key)) return; seen.add(key); }
|
|
436
|
+
out.push({ key, val });
|
|
437
|
+
return;
|
|
438
|
+
}
|
|
439
|
+
const a = atoms[i];
|
|
440
|
+
// L1 (DECISION-0237): только установленные строки (TRUE_ONLY), без BOTH.
|
|
441
|
+
for (const row of M.established(a.predicate)) {
|
|
442
|
+
const next: Env = { ...env };
|
|
443
|
+
let ok = true;
|
|
444
|
+
for (let j = 0; j < row.length && ok; j++) {
|
|
445
|
+
const x = a.args[j];
|
|
446
|
+
if (x.kind === "var") {
|
|
447
|
+
const b = next[x.var];
|
|
448
|
+
if (b === undefined) next[x.var] = slotTyped(a.predicate, j, row[j]);
|
|
449
|
+
else if (!RT.eq(b.v, row[j])) ok = false;
|
|
450
|
+
} else if (!RT.eq(evalTerm(x, next).v, row[j])) ok = false;
|
|
451
|
+
}
|
|
452
|
+
if (ok) walk(i + 1, next);
|
|
453
|
+
}
|
|
454
|
+
};
|
|
455
|
+
walk(0, {});
|
|
456
|
+
out.sort((p, q) => cmpUtf8(p.key, q.key));
|
|
457
|
+
return out.map((e) => e.val);
|
|
458
|
+
};
|
|
459
|
+
|
|
460
|
+
const aggregate = (node: any): Typed => {
|
|
461
|
+
const items = materialize(node.input);
|
|
462
|
+
const op = String(node.op);
|
|
463
|
+
if (op === "count") return { v: RT.dec(String(items.length)), t: INT };
|
|
464
|
+
if (op === "only") {
|
|
465
|
+
// §51 (errata E-0256): функциональный lookup. Политика — TYPE_ERROR у
|
|
466
|
+
// оракула; пусто и два различных значения (канонические байты §208) —
|
|
467
|
+
// ошибки значения RUNTIME_ERROR, код тот же, что у оракула.
|
|
468
|
+
if (node.options && Object.keys(node.options).length) throw new RT.TermError("TYPE_ERROR");
|
|
469
|
+
if (!items.length) throw new RT.TermError("EMPTY_AGGREGATE");
|
|
470
|
+
if (new Set(items.map((it) => canon(literal(it)))).size > 1) throw new RT.TermError("CARDINALITY_CONFLICT");
|
|
471
|
+
return { v: items[0].v, t: norm(items[0].t) };
|
|
472
|
+
}
|
|
473
|
+
if (op !== "sum" && op !== "min" && op !== "max") throw new RT.Refused("агрегат вне среза раннера: " + op);
|
|
474
|
+
if (!items.length) {
|
|
475
|
+
const empty = node.options?.empty;
|
|
476
|
+
if (empty === undefined) throw new RT.Refused("EMPTY_AGGREGATE: " + op + " над пустой коллекцией без явной empty-политики (§59)");
|
|
477
|
+
// `from_literal_term` оракула: политика — LiteralTerm §53; иной узел даёт
|
|
478
|
+
// TYPE_ERROR раньше сверки вида.
|
|
479
|
+
if (empty?.kind !== "value") throw new RT.TermError("TYPE_ERROR");
|
|
480
|
+
const value = typedArg(empty);
|
|
481
|
+
// errata E-0195: вид политики обязан совпасть с видом элемента. У оракула
|
|
482
|
+
// это issue `TYPE_ERROR` с текстом «aggregate.empty: вид политики integer
|
|
483
|
+
// не совпадает с видом элемента коллекции money (§59)»; порождённый модуль
|
|
484
|
+
// сообщений не печатает вовсе, и байтовый контракт среза здесь — КОД issue,
|
|
485
|
+
// который тот же. До правки коллекция Money с политикой Integer отвечала
|
|
486
|
+
// `Integer 0` и `COMPUTED` — другой ответ, не меньший.
|
|
487
|
+
const expected = EMPTY_ELEMENT_KINDS[String(node.input?.variable?.type?.name)];
|
|
488
|
+
if (expected !== undefined && norm(value.t) !== expected) throw new RT.TermError("TYPE_ERROR");
|
|
489
|
+
return value;
|
|
490
|
+
}
|
|
491
|
+
let acc = items[0];
|
|
492
|
+
for (const it of items.slice(1)) {
|
|
493
|
+
if (!isNumT(acc.t) || norm(it.t) !== norm(acc.t)) throw new RT.Refused("арифметика вне среза (§58): виды операндов не совпадают у " + op);
|
|
494
|
+
if (op === "sum") acc = { v: RT.add(acc.v, it.v), t: acc.t };
|
|
495
|
+
else if (RT.cmp(it.v, op === "min" ? "lt" : "gt", acc.v)) acc = it;
|
|
496
|
+
}
|
|
497
|
+
if (!isNumT(acc.t)) throw new RT.Refused("арифметика вне среза (§58): не числовой операнд " + op);
|
|
498
|
+
// DECISION-0266: итог агрегата над Natural у оракула — вид `integer`, то есть Integer.
|
|
499
|
+
return { v: acc.v, t: norm(acc.t) };
|
|
500
|
+
};
|
|
501
|
+
|
|
502
|
+
// Каноническая десятичная запись §18/§208: без хвостовых нулей и minus-zero.
|
|
503
|
+
const canonDec = (d: RT.Dec): string => {
|
|
504
|
+
let { u, s } = d;
|
|
505
|
+
if (u === 0n) return "0";
|
|
506
|
+
while (s > 0 && u % 10n === 0n) { u /= 10n; s -= 1; }
|
|
507
|
+
const neg = u < 0n;
|
|
508
|
+
const body = (neg ? -u : u).toString().padStart(s + 1, "0");
|
|
509
|
+
const text = s === 0 ? body : body.slice(0, body.length - s) + "." + body.slice(body.length - s);
|
|
510
|
+
return (neg && u !== 0n ? "-" : "") + text;
|
|
511
|
+
};
|
|
512
|
+
|
|
513
|
+
// Литерал-терм §53 из typed-значения — ровно то, что печатает оракул.
|
|
514
|
+
const literal = (x: Typed): any => {
|
|
515
|
+
if (x.t === "entity") return { kind: "entity_ref", id: x.v };
|
|
516
|
+
if (x.t === "enum") return { kind: "const_ref", id: (x.v as RT.Sym).sym };
|
|
517
|
+
if (x.t === TEXT) return { kind: "value", type: { name: TEXT }, value: x.v };
|
|
518
|
+
if (x.t === DATE) return { kind: "value", type: { name: DATE }, value: (x.v as RT.Date).day };
|
|
519
|
+
if (x.t === INSTANT) return { kind: "value", type: { name: INSTANT }, value: (x.v as RT.Instant).at };
|
|
520
|
+
if (x.t === MON) return { kind: "value", type: { name: MON }, currency: (x.v as RT.Money).cur, value: canonDec(x.v as RT.Dec) };
|
|
521
|
+
if (x.t === DEC) return { kind: "value", type: { name: DEC }, value: canonDec(x.v as RT.Dec) };
|
|
522
|
+
if (x.t === RAT) {
|
|
523
|
+
const q = x.v as RT.Rat;
|
|
524
|
+
return { kind: "value", type: { name: RAT }, value: q.n.toString() + "/" + q.d.toString() };
|
|
525
|
+
}
|
|
526
|
+
if (x.t === BOOL) return { kind: "value", type: { name: BOOL }, value: Boolean(x.v) };
|
|
527
|
+
if (x.t === NAT) {
|
|
528
|
+
// DECISION-0266: запись дела хранит тип Natural; вычисленное целое — всегда Integer.
|
|
529
|
+
const d = x.v as RT.Dec;
|
|
530
|
+
return { kind: "value", type: { name: NAT }, value: { int: (d.u / 10n ** BigInt(d.s)).toString() } };
|
|
531
|
+
}
|
|
532
|
+
if (x.t === QTY) return { kind: "value", type: { name: QTY }, unit: (x.v as RT.Qty).unit, value: canonDec(x.v as RT.Dec) };
|
|
533
|
+
if (x.t === INT) {
|
|
534
|
+
const d = x.v as RT.Dec;
|
|
535
|
+
if (d.u % (10n ** BigInt(d.s)) !== 0n) throw new RT.Refused("Integer с дробной частью в результате");
|
|
536
|
+
return { kind: "value", type: { name: INT }, value: { int: (d.u / 10n ** BigInt(d.s)).toString() } };
|
|
537
|
+
}
|
|
538
|
+
throw new RT.Refused("вид результата вне среза раннера: " + x.t);
|
|
539
|
+
};
|
|
540
|
+
|
|
541
|
+
// Канонические байты §208 значения: ключи по порядку, без пробелов, целое — цифрами.
|
|
542
|
+
const canon = (v: any): string => {
|
|
543
|
+
if (v === null) return "null";
|
|
544
|
+
if (typeof v === "boolean") return v ? "true" : "false";
|
|
545
|
+
if (typeof v === "string") return JSON.stringify(v);
|
|
546
|
+
// Целое из JSON шаблона (payload позиции, DECISION-0242) — цифрами.
|
|
547
|
+
if (typeof v === "number") return String(v);
|
|
548
|
+
if (Array.isArray(v)) return "[" + v.map(canon).join(",") + "]";
|
|
549
|
+
if (typeof v === "object" && "int" in v && Object.keys(v).length === 1) return String(v.int);
|
|
550
|
+
return "{" + Object.keys(v).sort().map((k) => JSON.stringify(k) + ":" + canon(v[k])).join(",") + "}";
|
|
551
|
+
};
|
|
552
|
+
|
|
553
|
+
// Заземление payload шаблона подстановкой — порт `norms._ground_payload`.
|
|
554
|
+
const ground = (value: any, subst: Record<string, any>): any => {
|
|
555
|
+
if (Array.isArray(value)) return value.map((v) => ground(v, subst));
|
|
556
|
+
if (value !== null && typeof value === "object") {
|
|
557
|
+
if (value.kind === "var" && value.var in subst) return subst[value.var];
|
|
558
|
+
const out: Record<string, any> = {};
|
|
559
|
+
for (const k of Object.keys(value)) out[k] = ground(value[k], subst);
|
|
560
|
+
return out;
|
|
561
|
+
}
|
|
562
|
+
return value;
|
|
563
|
+
};
|
|
564
|
+
|
|
565
|
+
// E-0105: код ошибки терма → статус результата (`whynot.TERM_ERROR_STATUS`);
|
|
566
|
+
// код вне таблицы — RUNTIME_ERROR; старшинство — `TERM_ERROR_PRECEDENCE`.
|
|
567
|
+
const TERM_ERROR_STATUS: Record<string, string> = {
|
|
568
|
+
MISSING_POLICY: "MISSING_POLICY", DEADLINE_POLICY_INVALID: "MISSING_POLICY", MISSING_CALENDAR: "MISSING_INPUT",
|
|
569
|
+
CALENDAR_DATASET_INVALID: "MISSING_INPUT", CALENDAR_DATASET_HASH_MISMATCH: "MISSING_INPUT",
|
|
570
|
+
CALENDAR_OUT_OF_RANGE: "MISSING_INPUT", TYPE_ERROR: "TYPE_ERROR", CURRENCY_MISMATCH: "TYPE_ERROR",
|
|
571
|
+
DIMENSION_MISMATCH: "TYPE_ERROR", DATE_INSTANT_MISMATCH: "TYPE_ERROR", RESOURCE_LIMIT: "RESOURCE_LIMIT",
|
|
572
|
+
OPAQUE_NON_EXECUTABLE: "NON_EXECUTABLE", NON_EXHAUSTIVE_MATCH: "NON_EXECUTABLE", UNKNOWN_FUNCTION: "NON_EXECUTABLE",
|
|
573
|
+
NON_EXECUTABLE_FUNCTION_RECURSION: "NON_EXECUTABLE",
|
|
574
|
+
};
|
|
575
|
+
const TERM_ERROR_PRECEDENCE = ["MISSING_INPUT", "MISSING_POLICY", "NON_EXECUTABLE", "EXTERNAL_UNAVAILABLE",
|
|
576
|
+
"TYPE_ERROR", "RUNTIME_ERROR", "RESOURCE_LIMIT"];
|
|
577
|
+
const sortedJson = (v: any): string => {
|
|
578
|
+
if (Array.isArray(v)) return "[" + v.map(sortedJson).join(",") + "]";
|
|
579
|
+
if (v !== null && typeof v === "object") {
|
|
580
|
+
return "{" + Object.keys(v).sort().map((k) => JSON.stringify(k) + ":" + sortedJson(v[k])).join(",") + "}";
|
|
581
|
+
}
|
|
582
|
+
return JSON.stringify(v);
|
|
583
|
+
};
|
|
584
|
+
|
|
585
|
+
// Порт `whynot.term_error_blockers` + `term_error_status`: правила с совместимой
|
|
586
|
+
// головой, их ошибки терма и конъюнкты без опоры — вглубь.
|
|
587
|
+
let LAST_TERM_CODES = new Set<string>();
|
|
588
|
+
const termErrorStatus = (goal: any): string | null => {
|
|
589
|
+
const Mx = M as any;
|
|
590
|
+
const byRule = new Map<string, string[]>();
|
|
591
|
+
for (const k of (Mx.ISSUE_SEEN ?? new Set<string>()) as Set<string>) {
|
|
592
|
+
const at = k.lastIndexOf("|"), name = k.slice(0, at), code = k.slice(at + 1);
|
|
593
|
+
if (code === "MISSING_PARAMETER_VALUE") continue;
|
|
594
|
+
byRule.set(name, [...(byRule.get(name) ?? []), code]);
|
|
595
|
+
}
|
|
596
|
+
if (byRule.size === 0) return null;
|
|
597
|
+
const index = new Map<string, any[]>();
|
|
598
|
+
for (const r of Mx.BACKCHAIN) index.set(r.head.predicate, [...(index.get(r.head.predicate) ?? []), r]);
|
|
599
|
+
const deref = (a: any): any => (a.kind === "const_ref" && a.id in Mx.BACKCHAIN_CONSTS ? Mx.BACKCHAIN_CONSTS[a.id] : a);
|
|
600
|
+
const compatible = (head: any, target: any): Record<string, any> | null => {
|
|
601
|
+
if ((head.polarity ?? "positive") !== (target.polarity ?? "positive")) return null;
|
|
602
|
+
if (head.args.length !== target.args.length) return null;
|
|
603
|
+
const subst: Record<string, any> = {};
|
|
604
|
+
for (let i = 0; i < head.args.length; i++) {
|
|
605
|
+
const h = head.args[i], g = target.args[i];
|
|
606
|
+
if (g.kind === "var") continue;
|
|
607
|
+
if (h.kind === "var") {
|
|
608
|
+
if (h.var in subst && sortedJson(subst[h.var]) !== sortedJson(g)) return null;
|
|
609
|
+
subst[h.var] = g;
|
|
610
|
+
} else if (!["entity_ref", "const_ref", "value"].includes(h.kind)) {
|
|
611
|
+
continue;
|
|
612
|
+
} else if (sortedJson(h) !== sortedJson(g)) {
|
|
613
|
+
return null;
|
|
614
|
+
}
|
|
615
|
+
}
|
|
616
|
+
return subst;
|
|
617
|
+
};
|
|
618
|
+
const MIRROR: Record<string, string> = { TRUE_ONLY: "FALSE_ONLY", FALSE_ONLY: "TRUE_ONLY", BOTH: "BOTH", NEITHER: "NEITHER" };
|
|
619
|
+
const truth = (lit: any): string => {
|
|
620
|
+
const ents = new Set(ENTITIES), txts = new Set(TEXTS);
|
|
621
|
+
try {
|
|
622
|
+
const st: string = Mx.status(lit.predicate, lit.args.map(arg));
|
|
623
|
+
return lit.polarity === "negative" ? MIRROR[st] : st;
|
|
624
|
+
} finally {
|
|
625
|
+
ENTITIES.clear(); ents.forEach((x) => ENTITIES.add(x)); TEXTS.clear(); txts.forEach((x) => TEXTS.add(x));
|
|
626
|
+
}
|
|
627
|
+
};
|
|
628
|
+
const found = new Set<string>(), visited = new Set<string>();
|
|
629
|
+
const walk = (target: any): void => {
|
|
630
|
+
const key = sortedJson(target);
|
|
631
|
+
if (visited.has(key)) return;
|
|
632
|
+
visited.add(key);
|
|
633
|
+
for (const r of index.get(target.predicate) ?? []) {
|
|
634
|
+
const subst = compatible(r.head, target);
|
|
635
|
+
if (subst === null) continue;
|
|
636
|
+
const w = r.window;
|
|
637
|
+
if (w && ((w.start && RT.legalTime() < w.start) || (w.end && RT.legalTime() >= w.end))) continue;
|
|
638
|
+
for (const c of byRule.get(r.name) ?? []) found.add(c);
|
|
639
|
+
for (const [, lit] of r.body) {
|
|
640
|
+
if (lit.kind !== "literal") continue; // DECISION-0274: гарды в backchain — только для пути судьи
|
|
641
|
+
const args: any[] = lit.args ?? [];
|
|
642
|
+
if (args.some((a) => a.kind === "wildcard")) continue;
|
|
643
|
+
// DECISION-0270: вычисляемый аргумент (binary/call/if) не заземляется
|
|
644
|
+
// обходом — как несвязанная переменная, конъюнкт идёт вглубь без оценки.
|
|
645
|
+
const computed = (a: any): boolean => ["binary", "call", "if", "let", "match"].includes(a?.kind);
|
|
646
|
+
if (args.some((a) => (a.kind === "var" && !(a.var in subst)) || computed(a) || (a.kind === "var" && computed(subst[a.var])))) {
|
|
647
|
+
walk({ ...lit, args: args.map((a) => (a.kind === "var" && a.var in subst ? subst[a.var] : a)) });
|
|
648
|
+
continue;
|
|
649
|
+
}
|
|
650
|
+
const ground = { ...lit, args: args.map((a) => (a.kind === "var" ? subst[a.var] : a)) };
|
|
651
|
+
if (truth(ground) === "NEITHER") walk(ground);
|
|
652
|
+
}
|
|
653
|
+
}
|
|
654
|
+
};
|
|
655
|
+
walk({ ...goal, args: (goal.args ?? []).map(deref) });
|
|
656
|
+
LAST_TERM_CODES = new Set(found);
|
|
657
|
+
const statuses = new Set([...found].map((c) => TERM_ERROR_STATUS[c] ?? "RUNTIME_ERROR"));
|
|
658
|
+
return TERM_ERROR_PRECEDENCE.find((st) => statuses.has(st)) ?? null;
|
|
659
|
+
};
|
|
660
|
+
|
|
661
|
+
// E-0243 §181.2: assertion TYPE_ERROR blocks precisely relations reading it.
|
|
662
|
+
const carrierTypeError = (goal: any): string | null => {
|
|
663
|
+
if (TYPE_ERROR_PREDICATES.size === 0) return null;
|
|
664
|
+
const Mx = M as any, index: Record<string, string[]> = Mx.CARRIER_EDGES ?? {}, seen = new Set<string>();
|
|
665
|
+
const walk = (pred: string): boolean => {
|
|
666
|
+
if (seen.has(pred)) return false; seen.add(pred);
|
|
667
|
+
return TYPE_ERROR_PREDICATES.has(pred) || (index[pred] ?? []).some(walk);
|
|
668
|
+
};
|
|
669
|
+
return walk(goal.predicate) ? "TYPE_ERROR" : null;
|
|
670
|
+
};
|
|
671
|
+
|
|
672
|
+
// §175/§181.2 (errata E-0265): статус результата, зависящего от упавшего правила.
|
|
673
|
+
// NEITHER над ошибкой терма — статус кода (E-0105); иная истинность — итог §181.2:
|
|
674
|
+
// код из списка ниже остаётся статусом, прочие — NON_EXECUTABLE. Если коды
|
|
675
|
+
// смешаны, итог оракула решает первая блокирующая issue документа — порядок,
|
|
676
|
+
// которого раннер не воспроизводит: названный отказ.
|
|
677
|
+
const OUTCOME_CODES = new Set(["PRIORITY_CONFLICT", "INTERPRETATION_REQUIRED", "TYPE_ERROR",
|
|
678
|
+
"RESOURCE_LIMIT", "CONFLICTED_INPUTS", "SOURCE_RESOLUTION_FAILED"]);
|
|
679
|
+
const ruleErrorStatus = (goal: any, truth: string): string | null => {
|
|
680
|
+
const te = termErrorStatus(goal);
|
|
681
|
+
const codes = new Set(te !== null ? LAST_TERM_CODES : []);
|
|
682
|
+
const ct = carrierTypeError(goal);
|
|
683
|
+
if (te === null && ct === null) return null;
|
|
684
|
+
if (truth === "NEITHER") return (te ?? ct) as string;
|
|
685
|
+
if (ct !== null) codes.add("TYPE_ERROR");
|
|
686
|
+
const kept = [...codes].filter((c) => OUTCOME_CODES.has(c));
|
|
687
|
+
if (kept.length === 0) return "NON_EXECUTABLE";
|
|
688
|
+
if (kept.length === codes.size && new Set(kept).size === 1) return kept[0];
|
|
689
|
+
throw new RT.Refused("статус итога §181.2 зависит от порядка issue вне среза раннера");
|
|
690
|
+
};
|
|
691
|
+
|
|
692
|
+
// Обход запроса судье §47.3 (DECISION-0274): переводы термов раннера для рантайма;
|
|
693
|
+
// множества сущностей и текстов дела обходом не двигаются.
|
|
694
|
+
const jhooks: RT.JHooks = {
|
|
695
|
+
val: (t: any): Val => {
|
|
696
|
+
const ents = new Set(ENTITIES), txts = new Set(TEXTS);
|
|
697
|
+
try { return arg(t); } finally { ENTITIES.clear(); ents.forEach((x) => ENTITIES.add(x)); TEXTS.clear(); txts.forEach((x) => TEXTS.add(x)); }
|
|
698
|
+
},
|
|
699
|
+
cell: (pred: string, col: number, v: Val): any => literal(slotTyped(pred, col, v)),
|
|
700
|
+
};
|
|
701
|
+
const judgmentRequests = (goal: any, status: string, conflict: boolean, tail: string): any[] =>
|
|
702
|
+
RT.buildRequests(M, RT.judgmentBlockers(M, jhooks, { ...goal, polarity: goal.polarity ?? "positive" }, status, conflict), tail);
|
|
703
|
+
|
|
704
|
+
// Порт `context.expand` без дозаполнения: профили §89.1 от корня к листу, затем дело.
|
|
705
|
+
const effectiveContext = (kase: any): Record<string, any> => {
|
|
706
|
+
const raw: Record<string, any> = { ...(kase.context ?? {}) };
|
|
707
|
+
const profiles = kase.contextProfiles ?? {};
|
|
708
|
+
const parent = raw.extends ?? null;
|
|
709
|
+
delete raw.extends;
|
|
710
|
+
const chain: string[] = [], seen = new Set<string>();
|
|
711
|
+
let current: string | null = parent;
|
|
712
|
+
while (current !== null && current !== undefined) {
|
|
713
|
+
if (seen.has(current)) throw new RT.Refused("CONTEXT_PROFILE_CYCLE: цикл наследования профилей (§89.1)");
|
|
714
|
+
if (!(current in profiles)) throw new RT.Refused("CONTEXT_PROFILE_MISSING: профиль не определён (§89.1)");
|
|
715
|
+
seen.add(current);
|
|
716
|
+
chain.push(current);
|
|
717
|
+
current = profiles[current].extends ?? null;
|
|
718
|
+
}
|
|
719
|
+
const effective: Record<string, any> = {};
|
|
720
|
+
for (const name of chain.reverse()) Object.assign(effective, profiles[name].fields ?? {});
|
|
721
|
+
return Object.assign(effective, raw);
|
|
722
|
+
};
|
|
723
|
+
|
|
724
|
+
const keyConflictBlocks = (predicate: string): boolean => {
|
|
725
|
+
const tainted = new Set<string>((M as any).keyConflictPredicates());
|
|
726
|
+
const waiting = [...tainted];
|
|
727
|
+
while (waiting.length) {
|
|
728
|
+
const source = waiting.pop()!;
|
|
729
|
+
for (const consumer of ((M as any).KEY_READERS?.[source] ?? [])) {
|
|
730
|
+
if (!tainted.has(consumer)) { tainted.add(consumer); waiting.push(consumer); }
|
|
731
|
+
}
|
|
732
|
+
}
|
|
733
|
+
return tainted.has(predicate);
|
|
734
|
+
};
|
|
735
|
+
|
|
736
|
+
// Политика §86 и снимок §85 дела до вывода (DECISION-0245 п. 4): порт
|
|
737
|
+
// `engine._resolve_deadline_policy` и `engine._calendar_snapshot`.
|
|
738
|
+
const calendarSetup = (kase: any): void => {
|
|
739
|
+
const Mx = M as any;
|
|
740
|
+
const ref = effectiveContext(kase).deadlinePolicy;
|
|
741
|
+
let policy: Record<string, any> | null = null;
|
|
742
|
+
if (ref !== undefined && ref !== null) {
|
|
743
|
+
if (String(Mx.CAL_SEMVER).startsWith("0.1")) {
|
|
744
|
+
if (typeof ref !== "object" || Array.isArray(ref)) throw new RT.Refused("case.context.deadlinePolicy вне среза раннера (§86)");
|
|
745
|
+
policy = ref;
|
|
746
|
+
} else if (typeof ref !== "string") {
|
|
747
|
+
throw new RT.Refused("case.context.deadlinePolicy: в 0.2 ожидался DeadlinePolicyRef (DECISION-0111 §2.9)");
|
|
748
|
+
} else if (!(ref in Mx.CAL_POLICIES)) {
|
|
749
|
+
throw new RT.Refused("case.context.deadlinePolicy: ссылка не разрешена в закреплённом CLIR (§86)");
|
|
750
|
+
} else {
|
|
751
|
+
policy = Mx.CAL_POLICIES[ref];
|
|
752
|
+
}
|
|
753
|
+
}
|
|
754
|
+
const selected = kase.options?.calendarSnapshot;
|
|
755
|
+
const snapshots: RT.Snapshot[] = Mx.CAL_SNAPSHOTS;
|
|
756
|
+
let snapshot: RT.Snapshot | null = null;
|
|
757
|
+
if (selected) {
|
|
758
|
+
snapshot = snapshots.find((c) => c.id === selected) ?? null;
|
|
759
|
+
if (snapshot === null) throw new RT.Refused("MISSING_CALENDAR: calendar_snapshot не объявлен в программе (§85, §168.2)");
|
|
760
|
+
} else if (snapshots.length >= 2) {
|
|
761
|
+
throw new RT.Refused("AMBIGUOUS_CALENDAR: в программе несколько calendar_snapshot без выбора (§85, §168.2)");
|
|
762
|
+
} else if (snapshots.length === 1) {
|
|
763
|
+
snapshot = snapshots[0];
|
|
764
|
+
}
|
|
765
|
+
RT.setCalendar(policy, snapshot);
|
|
766
|
+
};
|
|
767
|
+
|
|
768
|
+
|
|
769
|
+
// Внутренний вид API `rows` (DECISION-0423 §2.1): установленные строки вопроса
|
|
770
|
+
// литерал-термами §53, статус вычисления — как у вопроса о том же отношении.
|
|
771
|
+
const rowsAnswer = (query: any): any => {
|
|
772
|
+
const pred = String(query.predicate);
|
|
773
|
+
if (!(pred in M.PARAMS)) throw new RT.Refused("предикат rows не объявлен в пакете: " + pred);
|
|
774
|
+
const src: Val[][] = M.LAYER === "L1" ? (M as any).established(pred) : (M.TABLES[pred] ?? []);
|
|
775
|
+
const rows = src.map((r) => r.map((v, col) => literal(slotTyped(pred, col, v))));
|
|
776
|
+
rows.sort((p, q) => cmpUtf8(canon(p), canon(q)));
|
|
777
|
+
const evaluation = keyConflictBlocks(pred) ? "NON_EXECUTABLE" : "COMPUTED";
|
|
778
|
+
return { queryId: query.queryId, resultKind: "COLLECTION", rows, evaluationStatus: evaluation,
|
|
779
|
+
judgmentRequests: [], issues: [...ISSUES] };
|
|
780
|
+
};
|
|
781
|
+
|
|
782
|
+
// --- исполнение ---
|
|
783
|
+
RT.setLegalTime(String(kase.context?.legalTime ?? ""));
|
|
784
|
+
// E-0212 §135.1: only an explicitly supplied decision changes status time.
|
|
785
|
+
if (Object.prototype.hasOwnProperty.call(kase.context ?? {}, "decisionTime") &&
|
|
786
|
+
typeof (M as any).setStatusTime === "function") {
|
|
787
|
+
(M as any).setStatusTime(String(kase.context.decisionTime).slice(0, 10));
|
|
788
|
+
}
|
|
789
|
+
if ((M as any).CAL_SNAPSHOTS !== undefined) calendarSetup(kase);
|
|
790
|
+
|
|
791
|
+
for (const a of kase.assertions ?? []) {
|
|
792
|
+
if (a.valid != null) throw new RT.Refused("поле assertion.valid вне среза раннера (§72)");
|
|
793
|
+
const lit = a.literal;
|
|
794
|
+
if (lit.polarity !== "positive" && (M.LAYER !== "L1" || lit.polarity !== "negative")) {
|
|
795
|
+
throw new RT.Refused("§113: отрицательный факт вне L0-среза");
|
|
796
|
+
}
|
|
797
|
+
if (lit.predicate === "urn:law:std#incompatible" && "POSITIONS" in M) {
|
|
798
|
+
// §105.1: явная несовместимость позиций — конфликт вне среза (DECISION-0242 K5).
|
|
799
|
+
throw new RT.Refused("конфликт позиций §105.1 (incompatible) вне среза раннера (DECISION-0242 K5)");
|
|
800
|
+
}
|
|
801
|
+
if (!wellTyped(lit)) continue;
|
|
802
|
+
if (!(lit.predicate in M.PARAMS)) continue;
|
|
803
|
+
// §47.3.1 (E-0089, DECISION-0274): ответ судьи не от того органа или не по форме — не принят.
|
|
804
|
+
const rejected = RT.judgmentAnswer(M, a);
|
|
805
|
+
if (rejected !== null) { ISSUES.push(rejected); continue; }
|
|
806
|
+
if (lit.polarity === "negative") M.registerNegative(lit.predicate, lit.args.map(arg));
|
|
807
|
+
else M.register(lit.predicate, lit.args.map(arg));
|
|
808
|
+
}
|
|
809
|
+
M.saturate();
|
|
810
|
+
if (opts.stats !== undefined && M.LAYER === "L1" && (M as any).STATS !== undefined) Object.assign(opts.stats, (M as any).STATS);
|
|
811
|
+
// DECISION-0239 G3: ошибки значения агрегатов в правилах — issues модели.
|
|
812
|
+
ISSUES.push(...(((M as any).RULE_ISSUES ?? []) as string[]));
|
|
813
|
+
// DECISION-0242: диагностики эффектов полномочий §127–§128.
|
|
814
|
+
ISSUES.push(...(((M as any).NORM_ISSUES ?? []) as string[]));
|
|
815
|
+
for (let n = M.keyConflicts(); n > 0; n--) ISSUES.push("KEY_CONFLICT");
|
|
816
|
+
|
|
817
|
+
if (query.kind === "rows" && opts.api) return rowsAnswer(query);
|
|
818
|
+
if (query.kind === "collect") {
|
|
819
|
+
const elements = materialize(query.comprehension);
|
|
820
|
+
if (elements.some((e) => typeof e.v === "string" && e.v.startsWith("urn:position:"))) {
|
|
821
|
+
throw new RT.Refused("позиция в элементе коллекции вне среза раннера (DECISION-0242 K4)");
|
|
822
|
+
}
|
|
823
|
+
return { queryId: query.queryId, resultKind: "COLLECTION", value: elements.map((e) => literal(e)),
|
|
824
|
+
evaluationStatus: "COMPUTED", issues: [...ISSUES] };
|
|
825
|
+
}
|
|
826
|
+
if (query.kind === "term") {
|
|
827
|
+
let value: any;
|
|
828
|
+
try {
|
|
829
|
+
value = literal(evalTerm(query.term, {}));
|
|
830
|
+
} catch (e) {
|
|
831
|
+
if (!(e instanceof RT.TermError)) throw e;
|
|
832
|
+
// §58/§175 (DECISION-0252 п. 8): ошибка значения вопроса — без значения;
|
|
833
|
+
// статус по таблице `queries.query_calc`: несовместимость видов —
|
|
834
|
+
// TYPE_ERROR, прочее — RUNTIME_ERROR.
|
|
835
|
+
const status = ["TYPE_ERROR", "CURRENCY_MISMATCH", "DIMENSION_MISMATCH", "DATE_INSTANT_MISMATCH"]
|
|
836
|
+
.includes(e.code) ? "TYPE_ERROR" : "RUNTIME_ERROR";
|
|
837
|
+
return { queryId: query.queryId, resultKind: "DATA", evaluationStatus: status, issues: [...ISSUES, e.code] };
|
|
838
|
+
}
|
|
839
|
+
return { queryId: query.queryId, resultKind: "DATA", value, evaluationStatus: "COMPUTED", issues: [...ISSUES] };
|
|
840
|
+
}
|
|
841
|
+
if (query.kind !== "truth") throw new RT.Refused("вид запроса вне среза раннера: " + String(query.kind));
|
|
842
|
+
if ((query.literal.args ?? []).some((x: any) => x.kind === "entity_ref" && String(x.id).startsWith("urn:position:"))) {
|
|
843
|
+
throw new RT.Refused("позиция в аргументе вопроса вне среза раннера (DECISION-0242 K4)");
|
|
844
|
+
}
|
|
845
|
+
const tail = String(query.queryId).split(":").pop() as string;
|
|
846
|
+
if (M.LAYER === "L1") {
|
|
847
|
+
// §62/§63: статус атома по опорам обеих полярностей; отрицательный вопрос
|
|
848
|
+
// зеркалит стороны; несогласованный по сигнатуре вопрос — NEITHER.
|
|
849
|
+
const MIRROR: Record<string, string> = { TRUE_ONLY: "FALSE_ONLY", FALSE_ONLY: "TRUE_ONLY", BOTH: "BOTH", NEITHER: "NEITHER" };
|
|
850
|
+
let status = "NEITHER";
|
|
851
|
+
if (queryTyped(query.literal)) status = M.status(query.literal.predicate, query.literal.args.map(arg));
|
|
852
|
+
if (query.literal.polarity === "negative") status = MIRROR[status];
|
|
853
|
+
let evaluation = "COMPUTED";
|
|
854
|
+
const blocked: Set<string> | undefined = (M as any).BLOCKED;
|
|
855
|
+
const conflict = blocked !== undefined && query.literal.predicate in M.PARAMS
|
|
856
|
+
&& blocked.has(query.literal.predicate + "\u0000" + RT.key(query.literal.args.map(arg)));
|
|
857
|
+
// §47.3 (DECISION-0274): судьи по лестнице §175 — конфликт старше судьи, судья старше ошибки терма.
|
|
858
|
+
const requests = judgmentRequests(query.literal, status, conflict, tail);
|
|
859
|
+
if (conflict) {
|
|
860
|
+
// §127: эффект заперт непобеждённой парой Power/Immunity.
|
|
861
|
+
evaluation = "UNRESOLVED_NORMATIVE_CONFLICT";
|
|
862
|
+
} else if (keyConflictBlocks(query.literal.predicate)) {
|
|
863
|
+
evaluation = "NON_EXECUTABLE";
|
|
864
|
+
} else if (requests.length > 0) {
|
|
865
|
+
evaluation = "REQUIRES_JUDGMENT";
|
|
866
|
+
} else if (termErrorStatus(query.literal) !== null || carrierTypeError(query.literal) !== null) {
|
|
867
|
+
evaluation = ruleErrorStatus(query.literal, status) as string;
|
|
868
|
+
} else if (status === "NEITHER" && (M as any).BACKCHAIN !== undefined) {
|
|
869
|
+
// E-0105 (§175/§185): NEITHER над ошибкой терма правила-кандидата.
|
|
870
|
+
evaluation = termErrorStatus(query.literal) ?? evaluation;
|
|
871
|
+
}
|
|
872
|
+
return { queryId: query.queryId, truthStatus: status, evaluationStatus: evaluation, judgmentRequests: requests, issues: [...ISSUES] };
|
|
873
|
+
}
|
|
874
|
+
// Ключ атома у оракула §208 включает ТИП значения: запрос с Integer к
|
|
875
|
+
// утверждению Decimal той же величины атома не находит — NEITHER без issue.
|
|
876
|
+
let hit = false;
|
|
877
|
+
if (queryTyped(query.literal)) {
|
|
878
|
+
const want = query.literal.args.map(arg);
|
|
879
|
+
const rows = M.TABLES[query.literal.predicate] ?? [];
|
|
880
|
+
hit = rows.some((row) => row.length === want.length && row.every((v, i) => RT.eq(v, want[i])));
|
|
881
|
+
}
|
|
882
|
+
// Открытый мир: отсутствие строки — это NEITHER, а не FALSE_ONLY.
|
|
883
|
+
// §47.3 (DECISION-0274): судьи у модели L0 — тот же обход по `backchain`.
|
|
884
|
+
const status0 = hit ? "TRUE_ONLY" : "NEITHER";
|
|
885
|
+
const requests0 = judgmentRequests(query.literal, status0, false, tail);
|
|
886
|
+
return {
|
|
887
|
+
queryId: query.queryId,
|
|
888
|
+
truthStatus: status0,
|
|
889
|
+
evaluationStatus: keyConflictBlocks(query.literal.predicate) ? "NON_EXECUTABLE"
|
|
890
|
+
: (requests0.length > 0 ? "REQUIRES_JUDGMENT" : (ruleErrorStatus(query.literal, status0) ?? "COMPUTED")),
|
|
891
|
+
judgmentRequests: requests0,
|
|
892
|
+
issues: [...ISSUES],
|
|
893
|
+
};
|
|
894
|
+
};
|