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,1369 @@
|
|
|
1
|
+
// Рантайм L0 для порождённого кода. Ровно то, чего в TypeScript нет своего:
|
|
2
|
+
// точная десятичная арифметика §58, деньги с валютой, названный режим
|
|
3
|
+
// округления, календарный день, член перечисления.
|
|
4
|
+
export type Dec = { u: bigint; s: number }; // значение = u * 10^(-s)
|
|
5
|
+
export type Money = { u: bigint; s: number; cur: string };
|
|
6
|
+
// Календарный день §2.15 в канонической записи ISO YYYY-MM-DD: порядок
|
|
7
|
+
// записи и есть порядок дня. Не string: у ключа атома и у сравнения свой вид.
|
|
8
|
+
export type Date = { day: string };
|
|
9
|
+
// Мгновение §2.15 (W2 часть 2): запись хранится как есть — идентичность атома
|
|
10
|
+
// (E-0214), порядок и равенство берутся по МОМЕНТУ (E-0048): `23:01+01:00` и
|
|
11
|
+
// `22:01Z` равны. Запись без смещения моментом не является: сравнивать её со
|
|
12
|
+
// смещённой — отказ (§2.15), между собой такие записи сравнимы как один пояс.
|
|
13
|
+
export type Instant = { at: string };
|
|
14
|
+
// Член перечисления §38 либо иная ссылка const_ref без объявления: значение —
|
|
15
|
+
// stable qualified identity, равенство по ней (§57), порядок не определён.
|
|
16
|
+
export type Sym = { sym: string };
|
|
17
|
+
// Boolean (DECISION-0266): значение первого класса; равенство по значению,
|
|
18
|
+
// порядок не определён (§57), в ключе атома — свой вид, отличный от Integer 0/1.
|
|
19
|
+
export type Val = string | boolean | Dec | Money | Date | Instant | Sym | Rat | Qty;
|
|
20
|
+
|
|
21
|
+
// Названный отказ рантайма: то, что срез не считает, а не сбой. Раннер
|
|
22
|
+
// печатает его как JSON с кодом выхода 3; любая другая ошибка — крах.
|
|
23
|
+
export class Refused extends Error {}
|
|
24
|
+
|
|
25
|
+
const p10 = (n: number): bigint => 10n ** BigInt(n);
|
|
26
|
+
|
|
27
|
+
export const dec = (lit: string): Dec => litDec(lit);
|
|
28
|
+
|
|
29
|
+
export const money = (lit: string, cur: string): Money => ({ ...dec(lit), cur });
|
|
30
|
+
export const date = (lit: string): Date => { dateParts(lit); return { day: lit }; };
|
|
31
|
+
export const instant = (lit: string): Instant => { moment({ at: lit }); return { at: lit }; };
|
|
32
|
+
export const sym = (id: string): Sym => ({ sym: id });
|
|
33
|
+
|
|
34
|
+
export const isMoney = (v: Val): v is Money => typeof v === "object" && "cur" in v;
|
|
35
|
+
export const isDate = (v: Val): v is Date => typeof v === "object" && "day" in v;
|
|
36
|
+
export const isInstant = (v: Val): v is Instant => typeof v === "object" && "at" in v;
|
|
37
|
+
export const isSym = (v: Val): v is Sym => typeof v === "object" && "sym" in v;
|
|
38
|
+
export const isQty = (v: Val): v is Qty => v instanceof Qty;
|
|
39
|
+
const isNum = (v: Val): v is Dec => typeof v === "object" && "u" in v;
|
|
40
|
+
|
|
41
|
+
// --- виды -------------------------------------------------------------------
|
|
42
|
+
|
|
43
|
+
// §48/§58: сложение, вычитание, min/max требуют совпадения видов; у денег — и
|
|
44
|
+
// валюты, у величин — и единицы (DECISION-0256: ошибка значения).
|
|
45
|
+
const same = (a: Val, b: Val, op: string): [Dec, Dec] => {
|
|
46
|
+
if (isQty(a) || isQty(b)) {
|
|
47
|
+
if (!(isQty(a) && isQty(b))) throw new TermError("TYPE_ERROR");
|
|
48
|
+
if (a.unit !== b.unit) throw new TermError("DIMENSION_MISMATCH");
|
|
49
|
+
return [a, b];
|
|
50
|
+
}
|
|
51
|
+
if (!isNum(a) || !isNum(b) || isMoney(a) !== isMoney(b)) throw new Refused("§58: виды операндов не совпадают: " + op);
|
|
52
|
+
if (isMoney(a) && isMoney(b) && a.cur !== b.cur) throw new Refused("§58: разные валюты: " + op);
|
|
53
|
+
return [a, b];
|
|
54
|
+
};
|
|
55
|
+
|
|
56
|
+
const boundedCoefficient = (u: bigint, s = 0): void => {
|
|
57
|
+
if (u < -(1n << 127n) || u >= (1n << 127n) || (u !== 0n && s > 4096)) throw new Refused("NUMERIC_LIMIT_UNAVAILABLE: current i64/i128 arithmetic boundary");
|
|
58
|
+
};
|
|
59
|
+
const pair = (a: Dec, b: Dec): [bigint, bigint, number] => {
|
|
60
|
+
const s = Math.max(a.s, b.s), x = a.u * p10(s - a.s), y = b.u * p10(s - b.s);
|
|
61
|
+
boundedCoefficient(x, s); boundedCoefficient(y, s);
|
|
62
|
+
return [x, y, s];
|
|
63
|
+
};
|
|
64
|
+
|
|
65
|
+
const make = (u: bigint, s: number, like: Val): Dec => {
|
|
66
|
+
boundedCoefficient(u, s);
|
|
67
|
+
return isQty(like) ? new Qty(u, s, like.unit) : isMoney(like) ? ({ u, s, cur: like.cur } as Money) : { u, s };
|
|
68
|
+
};
|
|
69
|
+
|
|
70
|
+
// Целое значение: дробная часть нулевая (защита в глубину под статикой плана).
|
|
71
|
+
const integral = (v: Val): v is Dec => isNum(v) && !isMoney(v) && !isQty(v) && v.u % p10(v.s) === 0n;
|
|
72
|
+
const toInt = (v: Dec): bigint => v.u / p10(v.s);
|
|
73
|
+
|
|
74
|
+
// --- арифметика §58 ---------------------------------------------------------
|
|
75
|
+
|
|
76
|
+
export const add = (a: Val, b: Val): Dec | Rat => {
|
|
77
|
+
if (a instanceof Rat || b instanceof Rat) return ratPair(a, b, "add");
|
|
78
|
+
const [x, y] = same(a, b, "add");
|
|
79
|
+
const [px, py, s] = pair(x, y);
|
|
80
|
+
return make(px + py, s, x);
|
|
81
|
+
};
|
|
82
|
+
|
|
83
|
+
export const sub = (a: Val, b: Val): Dec | Rat => {
|
|
84
|
+
if (a instanceof Rat || b instanceof Rat) return ratPair(a, b, "sub");
|
|
85
|
+
const [x, y] = same(a, b, "sub");
|
|
86
|
+
const [px, py, s] = pair(x, y);
|
|
87
|
+
return make(px - py, s, x);
|
|
88
|
+
};
|
|
89
|
+
|
|
90
|
+
// Правило скаляра §49: безразмерное масштабирует размерное; масштаб результата —
|
|
91
|
+
// сумма масштабов. Два размерных не перемножаются.
|
|
92
|
+
export const mul = (a: Val, b: Val): Dec => {
|
|
93
|
+
if (!isNum(a) || !isNum(b)) throw new Refused("§58: виды операндов не совпадают: mul");
|
|
94
|
+
const dim = (v: Val): boolean => isMoney(v) || isQty(v);
|
|
95
|
+
if (dim(a) && dim(b)) throw new Refused("§58: произведение двух размерных величин не определено");
|
|
96
|
+
return make(a.u * b.u, a.s + b.s, dim(a) ? a : b);
|
|
97
|
+
};
|
|
98
|
+
|
|
99
|
+
const gcd = (a: bigint, b: bigint): bigint => {
|
|
100
|
+
a = a < 0n ? -a : a; b = b < 0n ? -b : b;
|
|
101
|
+
while (b !== 0n) { [a, b] = [b, a % b]; }
|
|
102
|
+
return a;
|
|
103
|
+
};
|
|
104
|
+
|
|
105
|
+
// Точное десятичное частное num/den при 10-гладком знаменателе (§18), иначе null.
|
|
106
|
+
const exact = (num: bigint, den: bigint): [bigint, number] | null => {
|
|
107
|
+
const g = gcd(num, den) || 1n;
|
|
108
|
+
num /= g; den /= g;
|
|
109
|
+
if (den < 0n) { num = -num; den = -den; }
|
|
110
|
+
let twos = 0, fives = 0;
|
|
111
|
+
while (den % 2n === 0n) { den /= 2n; twos++; }
|
|
112
|
+
while (den % 5n === 0n) { den /= 5n; fives++; }
|
|
113
|
+
if (den !== 1n) return null;
|
|
114
|
+
const k = Math.max(twos, fives);
|
|
115
|
+
return [num * (2n ** BigInt(k - twos)) * (5n ** BigInt(k - fives)), k];
|
|
116
|
+
};
|
|
117
|
+
|
|
118
|
+
// Таблица делимых §58 в срезе: Decimal/Decimal, Money/Decimal, Money/Money одной
|
|
119
|
+
// валюты. Integer/Integer даёт Rational — вне среза, отвергается планом статически.
|
|
120
|
+
export const div = (a: Val, b: Val): Dec => {
|
|
121
|
+
if (!isNum(a) || !isNum(b)) throw new Refused("§58: деление вне таблицы делимых");
|
|
122
|
+
if (b.u === 0n) throw new Refused("деление на ноль — evaluation error (§58)");
|
|
123
|
+
let like: Money | null = null;
|
|
124
|
+
if (isMoney(a) && isMoney(b)) {
|
|
125
|
+
if (a.cur !== b.cur) throw new Refused("CURRENCY_MISMATCH: §58 делит Money на Money только при тождественной валюте");
|
|
126
|
+
} else if (isMoney(a)) {
|
|
127
|
+
like = a;
|
|
128
|
+
} else if (isMoney(b)) {
|
|
129
|
+
throw new Refused("§58: деление вне таблицы делимых");
|
|
130
|
+
}
|
|
131
|
+
const q = exact(a.u * p10(b.s), b.u * p10(a.s));
|
|
132
|
+
if (q === null) {
|
|
133
|
+
if (like !== null) throw new Refused("INEXACT_DIVISION: округление только явной политикой (§50/§18)");
|
|
134
|
+
throw new Refused("§58: частное не 10-гладкое — Rational вне среза");
|
|
135
|
+
}
|
|
136
|
+
return like !== null ? ({ u: q[0], s: q[1], cur: like.cur } as Money) : { u: q[0], s: q[1] };
|
|
137
|
+
};
|
|
138
|
+
|
|
139
|
+
// Евклидов остаток §58: 0 <= r < |b| при любых знаках операндов.
|
|
140
|
+
export const mod = (a: Val, b: Val): Dec => {
|
|
141
|
+
if (!integral(a) || !integral(b)) throw new Refused("§58: mod определён только для Integer");
|
|
142
|
+
const y = toInt(b);
|
|
143
|
+
if (y === 0n) throw new Refused("остаток от деления на ноль — evaluation error (§58)");
|
|
144
|
+
const m = y < 0n ? -y : y;
|
|
145
|
+
const r = ((toInt(a) % m) + m) % m;
|
|
146
|
+
return { u: r, s: 0 };
|
|
147
|
+
};
|
|
148
|
+
|
|
149
|
+
// --- Rational §35/§58 и арифметика по видам (DECISION-0252) ------------------
|
|
150
|
+
|
|
151
|
+
// Quantity §49 v1 (DECISION-0256 п. 5): точное десятичное и тег единицы —
|
|
152
|
+
// байты записи; реестра единиц и алиасов §49.2 здесь нет. Не Money: у ключа
|
|
153
|
+
// и у сравнения свой вид, 1 KZT денег и величина 1 [KZT] — разные значения.
|
|
154
|
+
export class Qty {
|
|
155
|
+
readonly u: bigint;
|
|
156
|
+
readonly s: number;
|
|
157
|
+
readonly unit: string;
|
|
158
|
+
constructor(u: bigint, s: number, unit: string) {
|
|
159
|
+
this.u = u;
|
|
160
|
+
this.s = s;
|
|
161
|
+
this.unit = unit;
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
// Величина §49: значение и тег единицы как записаны.
|
|
166
|
+
export const qty = (lit: string, unit: string): Qty => {
|
|
167
|
+
const d = dec(lit);
|
|
168
|
+
return new Qty(d.u, d.s, String(unit));
|
|
169
|
+
};
|
|
170
|
+
|
|
171
|
+
// Rational §35 (DECISION-0252 п. 6): несократимая дробь на целых произвольной
|
|
172
|
+
// длины, знаменатель положителен, ноль — 0/1. Не Dec: у ключа строки и у
|
|
173
|
+
// сравнения свой вид, 2/1 и Decimal 2 — разные значения §48.
|
|
174
|
+
export class Rat {
|
|
175
|
+
readonly n: bigint;
|
|
176
|
+
readonly d: bigint;
|
|
177
|
+
constructor(n: bigint, d: bigint) {
|
|
178
|
+
if (d === 0n) throw new TermError("DIVISION_BY_ZERO");
|
|
179
|
+
if (d < 0n) { n = -n; d = -d; }
|
|
180
|
+
const g = gcd(n, d) || 1n;
|
|
181
|
+
this.n = n / g;
|
|
182
|
+
this.d = d / g;
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
// --- сырое поле литерала §208 -------------------------------------------------
|
|
187
|
+
// Правило одно на три цели (errata E-0197, E-0202, E-0203): сырое поле литерала
|
|
188
|
+
// читается НЕ средствами языка цели, а по грамматике §208. `Number()` и
|
|
189
|
+
// `parseFloat` берут `1e2` и `Infinity` и теряют точность выше 2^53; `int()` и
|
|
190
|
+
// `Decimal()` Python берут пробел, `_` и юникодную цифру; `math/big` Go — свои
|
|
191
|
+
// допуски. Норма у всех одна, поэтому алфавит назван явно и одинаково.
|
|
192
|
+
|
|
193
|
+
// §18/§208 (errata E-0203): лексика ЗАПИСИ десятичного значения — дословный
|
|
194
|
+
// порт `lawref/values.py::_DECIMAL_LEXIS`. Экспонента, ведущие и хвостовые
|
|
195
|
+
// нули, знак `+`, `.5` и `5.` остаются законной записью входа и нормализуются;
|
|
196
|
+
// сужены ровно пробельный символ, `_`, не-ASCII цифра и `Infinity`/`NaN`.
|
|
197
|
+
// В JavaScript `$` без флага `m` уже строг — хвостового перевода строки, из-за
|
|
198
|
+
// которого оракулу понадобился `\Z`, он не пропускает.
|
|
199
|
+
const DECIMAL_LEXIS = /^[+-]?(?:[0-9]+(?:\.[0-9]*)?|\.[0-9]+)(?:[eE][+-]?[0-9]+)?$/;
|
|
200
|
+
// §35/§208 (errata E-0203): лексика ЦЕЛОГО внутри записи `<n>/<d>`.
|
|
201
|
+
const FRACTION_PART_LEXIS = /^[+-]?[0-9]+$/;
|
|
202
|
+
// §208 (errata E-0202): знаковый 64-битный носитель JSON-числа канона.
|
|
203
|
+
const I64_MIN = -(2n ** 63n), I64_MAX = 2n ** 63n - 1n;
|
|
204
|
+
// Канон §208 отвергает JSON-float и целое вне i64 ОТКАЗОМ СОБРАТЬ ДОКУМЕНТ
|
|
205
|
+
// (error-объект §172), а не issue. У порождённого модуля канала error-объекта
|
|
206
|
+
// нет — профиль core-executable документа не строит, — поэтому тот же вход даёт
|
|
207
|
+
// названный отказ раннера: «меньше ответа», а не другой ответ.
|
|
208
|
+
export const CANON_NUMBER = "JSON-число вне канона §208 (только целое знакового 64-битного диапазона) вне среза раннера";
|
|
209
|
+
|
|
210
|
+
// Вид JSON-носителя сырого поля литерала и его текст: "s" — JSON-строка,
|
|
211
|
+
// "i" — целое JSON-число канона, "x" — любая другая форма (в том числе
|
|
212
|
+
// отсутствие поля: схема LiteralTerm не требует `value`, E-0197).
|
|
213
|
+
export const rawLit = (raw: any): [string, string] => {
|
|
214
|
+
if (typeof raw === "string") return ["s", raw];
|
|
215
|
+
if (typeof raw === "boolean") return ["x", ""];
|
|
216
|
+
// Носитель точного целого из parseJson: {"int":"<цифры>"} — та же запись, в
|
|
217
|
+
// которой раннер целое и ПЕЧАТАЕТ, и которую читает харнесс API.
|
|
218
|
+
if (raw !== null && typeof raw === "object" && !Array.isArray(raw)
|
|
219
|
+
&& Object.keys(raw).length === 1 && typeof raw.int === "string") {
|
|
220
|
+
const v = BigInt(raw.int);
|
|
221
|
+
if (v < I64_MIN || v > I64_MAX) throw new Refused(CANON_NUMBER);
|
|
222
|
+
return ["i", v.toString()];
|
|
223
|
+
}
|
|
224
|
+
if (typeof raw === "number") {
|
|
225
|
+
if (!Number.isInteger(raw)) throw new Refused(CANON_NUMBER);
|
|
226
|
+
return ["i", BigInt(raw).toString()];
|
|
227
|
+
}
|
|
228
|
+
return ["x", ""];
|
|
229
|
+
};
|
|
230
|
+
|
|
231
|
+
// Поле, чей канон §208 — JSON-строка (Text, Date, Instant, Rational, валюта,
|
|
232
|
+
// единица). Порт `values._json_string`: до E-0197 цели приводили к строке ЛЮБОЙ
|
|
233
|
+
// JSON, и литерал без `value` выходил значением "undefined" у ts и "" у go —
|
|
234
|
+
// расхождение в результате, а не в тексте issue.
|
|
235
|
+
export const litText = (raw: any): string => {
|
|
236
|
+
const [k, text] = rawLit(raw);
|
|
237
|
+
if (k !== "s") throw new TermError("TYPE_ERROR");
|
|
238
|
+
return text;
|
|
239
|
+
};
|
|
240
|
+
|
|
241
|
+
// Поле, чей канон §208 — целое JSON-число (Integer). Порт `values._json_int`.
|
|
242
|
+
export const litIntText = (raw: any): string => {
|
|
243
|
+
const [k, text] = rawLit(raw);
|
|
244
|
+
if (k !== "i") throw new TermError("TYPE_ERROR");
|
|
245
|
+
return text;
|
|
246
|
+
};
|
|
247
|
+
|
|
248
|
+
// Поле Decimal/Money/Quantity: строка по лексике §208 либо целое JSON-число
|
|
249
|
+
// (запись числом остаётся законной — E-0203, долг п. 3).
|
|
250
|
+
export const litDecText = (raw: any): string => {
|
|
251
|
+
const [k, text] = rawLit(raw);
|
|
252
|
+
if (k === "i") return text;
|
|
253
|
+
if (k !== "s" || !DECIMAL_LEXIS.test(text)) throw new TermError("TYPE_ERROR");
|
|
254
|
+
return text;
|
|
255
|
+
};
|
|
256
|
+
|
|
257
|
+
// Точное десятичное из записи, уже прошедшей DECIMAL_LEXIS: знак, необязательная
|
|
258
|
+
// экспонента, `.5` и `5.`. Нормализация — как у `Decimal(text)` оракула, чей
|
|
259
|
+
// канон §208 печатает non-exponent форму.
|
|
260
|
+
export const decLexis = (text: string): Dec => {
|
|
261
|
+
let body = text;
|
|
262
|
+
const neg = body.startsWith("-");
|
|
263
|
+
if (body.startsWith("+") || body.startsWith("-")) body = body.slice(1);
|
|
264
|
+
const at = body.search(/[eE]/);
|
|
265
|
+
const exp = at < 0 ? 0 : Number(body.slice(at + 1));
|
|
266
|
+
const mant = at < 0 ? body : body.slice(0, at);
|
|
267
|
+
const dot = mant.indexOf(".");
|
|
268
|
+
const frac = dot < 0 ? "" : mant.slice(dot + 1);
|
|
269
|
+
const digits = ((dot < 0 ? mant : mant.slice(0, dot)) + frac) || "0";
|
|
270
|
+
let s = frac.length - exp, u = BigInt(digits);
|
|
271
|
+
if (u === 0n && Math.abs(s) > 4096) return { u: 0n, s: 0 };
|
|
272
|
+
const canonicalScale = s - (digits.length - digits.replace(/0+$/, "").length);
|
|
273
|
+
if (u !== 0n && (canonicalScale > 4096 || s < -4096)) throw new Refused("DECIMAL_EXPANSION_UNAVAILABLE: E-0320 limit 4096");
|
|
274
|
+
if (s < 0) { u *= 10n ** BigInt(-s); s = 0; }
|
|
275
|
+
return { u: neg ? -u : u, s };
|
|
276
|
+
};
|
|
277
|
+
|
|
278
|
+
export const litDec = (raw: any): Dec => decLexis(litDecText(raw));
|
|
279
|
+
export const litInt = (raw: any): Dec => ({ u: BigInt(litIntText(raw)), s: 0 });
|
|
280
|
+
export const litMoney = (raw: any, cur: any): Money => ({ ...litDec(raw), cur: litText(cur) });
|
|
281
|
+
export const litQty = (raw: any, unit: any): Qty => {
|
|
282
|
+
const d = litDec(raw);
|
|
283
|
+
return new Qty(d.u, d.s, litText(unit));
|
|
284
|
+
};
|
|
285
|
+
|
|
286
|
+
// Каноническая запись `n/d` (§168.1): иная — названный отказ, а не молчаливое
|
|
287
|
+
// сокращение — оракул хранит запись как есть, и сокращённая дала бы другой ответ.
|
|
288
|
+
export const rat = (lit: any): Rat => {
|
|
289
|
+
const text = litText(lit);
|
|
290
|
+
const at = text.indexOf("/");
|
|
291
|
+
const num = at < 0 ? text : text.slice(0, at), den = at < 0 ? "" : text.slice(at + 1);
|
|
292
|
+
if (!FRACTION_PART_LEXIS.test(num) || !FRACTION_PART_LEXIS.test(den)) {
|
|
293
|
+
throw new Refused("§168.1: запись Rational не n/d: " + text);
|
|
294
|
+
}
|
|
295
|
+
const n = BigInt(num), d = BigInt(den);
|
|
296
|
+
if (d <= 0n || gcd(n, d) !== 1n || `${n}/${d}` !== text) throw new Refused("§168.1: неканоническая запись Rational: " + text);
|
|
297
|
+
return new Rat(n, d);
|
|
298
|
+
};
|
|
299
|
+
|
|
300
|
+
// Литерал Rational в вычислимом терме вопроса `term`: разбор оракула
|
|
301
|
+
// (`values.from_literal_term`) — запись `n/d` с целыми частями по лексике §208
|
|
302
|
+
// (E-0203), сократимая и со знаком в знаменателе нормализуется; не `n/d` —
|
|
303
|
+
// TYPE_ERROR, знаменатель 0 — DIVISION_BY_ZERO. Каноничность §168.1 требуется
|
|
304
|
+
// от записей дела и вопроса `truth`, не от операнда вычисления (ERR-E0005).
|
|
305
|
+
export const ratTerm = (lit: any): Rat => {
|
|
306
|
+
const parts = litText(lit).split("/");
|
|
307
|
+
if (parts.length !== 2 || !FRACTION_PART_LEXIS.test(parts[0]) || !FRACTION_PART_LEXIS.test(parts[1])) {
|
|
308
|
+
throw new TermError("TYPE_ERROR");
|
|
309
|
+
}
|
|
310
|
+
const n = BigInt(parts[0]), d = BigInt(parts[1]);
|
|
311
|
+
if (d === 0n) throw new TermError("DIVISION_BY_ZERO");
|
|
312
|
+
return new Rat(n, d);
|
|
313
|
+
};
|
|
314
|
+
|
|
315
|
+
// §208/§18 (errata E-0202): `JSON.parse` сводит число к double, и целое
|
|
316
|
+
// знакового 64-битного диапазона теряет знаки уже НА ЧТЕНИИ —
|
|
317
|
+
// 9223372036854775807 приходило в раннер как 9223372036854776000, то есть
|
|
318
|
+
// ДРУГИМ ответом, а не меньшим. Число разбирается по тексту: целое, которого
|
|
319
|
+
// `Number` не несёт точно, заменяется носителем {"int":"<цифры>"}. Строки
|
|
320
|
+
// проходятся отдельной ветвью, поэтому цифры ВНУТРИ строки не трогаются.
|
|
321
|
+
const NUM_TOKEN = /-?(?:0|[1-9][0-9]*)(?:\.[0-9]+)?(?:[eE][-+]?[0-9]+)?/y;
|
|
322
|
+
const SAFE = BigInt(Number.MAX_SAFE_INTEGER);
|
|
323
|
+
export const parseJson = (text: string): any => {
|
|
324
|
+
const out: string[] = [];
|
|
325
|
+
let i = 0;
|
|
326
|
+
while (i < text.length) {
|
|
327
|
+
const ch = text[i];
|
|
328
|
+
if (ch === '"') {
|
|
329
|
+
let j = i + 1;
|
|
330
|
+
while (j < text.length) {
|
|
331
|
+
if (text[j] === "\\") { j += 2; continue; }
|
|
332
|
+
if (text[j] === '"') { j += 1; break; }
|
|
333
|
+
j += 1;
|
|
334
|
+
}
|
|
335
|
+
out.push(text.slice(i, j));
|
|
336
|
+
i = j;
|
|
337
|
+
continue;
|
|
338
|
+
}
|
|
339
|
+
if (ch === "-" || (ch >= "0" && ch <= "9")) {
|
|
340
|
+
NUM_TOKEN.lastIndex = i;
|
|
341
|
+
const m = NUM_TOKEN.exec(text);
|
|
342
|
+
if (m !== null && m.index === i) {
|
|
343
|
+
const tok = m[0];
|
|
344
|
+
const plain = !/[.eE]/.test(tok);
|
|
345
|
+
const big = plain && (BigInt(tok) > SAFE || BigInt(tok) < -SAFE);
|
|
346
|
+
out.push(big ? `{"int":${JSON.stringify(tok)}}` : tok);
|
|
347
|
+
i += tok.length;
|
|
348
|
+
continue;
|
|
349
|
+
}
|
|
350
|
+
}
|
|
351
|
+
out.push(ch);
|
|
352
|
+
i += 1;
|
|
353
|
+
}
|
|
354
|
+
return JSON.parse(out.join(""));
|
|
355
|
+
};
|
|
356
|
+
|
|
357
|
+
// Целая часть с округлением вниз — `//` эталона; BigInt `/` усекает к нулю.
|
|
358
|
+
const floorDiv = (a: bigint, b: bigint): bigint => {
|
|
359
|
+
const q = a / b;
|
|
360
|
+
return a % b !== 0n && (a < 0n) !== (b < 0n) ? q - 1n : q;
|
|
361
|
+
};
|
|
362
|
+
const intOf = (v: Dec): bigint => floorDiv(v.u, p10(v.s));
|
|
363
|
+
|
|
364
|
+
// add/sub/min/max над Rational: оба операнда Rational, иначе TYPE_ERROR §48.
|
|
365
|
+
const ratPair = (a: Val, b: Val, op: string): Rat => {
|
|
366
|
+
if (!(a instanceof Rat && b instanceof Rat)) throw new TermError("TYPE_ERROR");
|
|
367
|
+
if (op === "add") return new Rat(a.n * b.d + b.n * a.d, a.d * b.d);
|
|
368
|
+
if (op === "sub") return new Rat(a.n * b.d - b.n * a.d, a.d * b.d);
|
|
369
|
+
if (op === "min") return a.n * b.d <= b.n * a.d ? a : b;
|
|
370
|
+
return a.n * b.d >= b.n * a.d ? a : b;
|
|
371
|
+
};
|
|
372
|
+
|
|
373
|
+
// Вид значения динамического терма (DECISION-0252 п. 5): `r` Rational, `d` Decimal.
|
|
374
|
+
export const kindOf = (v: Val): string => (v instanceof Rat ? "r" : "d");
|
|
375
|
+
|
|
376
|
+
const KINDS = ["i", "d", "m", "r", "q"];
|
|
377
|
+
|
|
378
|
+
// Статический вид плана → вид значения; `n` — Decimal либо Rational по классу.
|
|
379
|
+
const resolve = (v: Val, k: string): string => {
|
|
380
|
+
if (k === "n") k = v instanceof Rat ? "r" : "d";
|
|
381
|
+
if (!KINDS.includes(k) || (k === "r") !== (v instanceof Rat) || (k === "m") !== isMoney(v)
|
|
382
|
+
|| (k === "q") !== isQty(v)) {
|
|
383
|
+
throw new Refused("вид операнда расходится с планом: " + String(k));
|
|
384
|
+
}
|
|
385
|
+
return k;
|
|
386
|
+
};
|
|
387
|
+
|
|
388
|
+
// Точное десятичное num/den (§18) видом `like` (Money, Quantity либо Dec) или null.
|
|
389
|
+
const exactOf = (num: bigint, den: bigint, like: Val | null): Dec | null => {
|
|
390
|
+
const q = exact(num, den);
|
|
391
|
+
if (q === null) return null;
|
|
392
|
+
if (like !== null && isQty(like)) return new Qty(q[0], q[1], like.unit);
|
|
393
|
+
return like !== null && isMoney(like) ? ({ u: q[0], s: q[1], cur: like.cur } as Money) : { u: q[0], s: q[1] };
|
|
394
|
+
};
|
|
395
|
+
|
|
396
|
+
// Таблица §58 `values.binary` по видам операндов (DECISION-0252 п. 3–4).
|
|
397
|
+
// Виды — из плана: `i` Integer, `d` Decimal, `m` Money, `r` Rational, `n` —
|
|
398
|
+
// Decimal либо Rational по значению. Integer и Decimal рантайм по значению не
|
|
399
|
+
// различает, поэтому пара решается видами, а не классами. Ошибка значения —
|
|
400
|
+
// TermError с кодом оракула, в его порядке проверок.
|
|
401
|
+
const arithUnchecked = (op: string, a: Val, b: Val, lk: string, rk: string): Val => {
|
|
402
|
+
const l = resolve(a, lk), r = resolve(b, rk);
|
|
403
|
+
const has = (k: string): boolean => l === k || r === k;
|
|
404
|
+
const pairIs = (x: string, y: string): boolean => (l === x && r === y) || (l === y && r === x);
|
|
405
|
+
if (op === "add" || op === "sub") {
|
|
406
|
+
if (l === "m" && r === "m" && (a as Money).cur !== (b as Money).cur) throw new TermError("CURRENCY_MISMATCH");
|
|
407
|
+
if (l === "q" && r === "q" && (a as Qty).unit !== (b as Qty).unit) throw new TermError("DIMENSION_MISMATCH"); // §49, T023
|
|
408
|
+
if (l !== r) throw new TermError("TYPE_ERROR");
|
|
409
|
+
if (l === "r") return ratPair(a, b, op);
|
|
410
|
+
return op === "add" ? add(a, b) : sub(a, b);
|
|
411
|
+
}
|
|
412
|
+
if (op === "mul") {
|
|
413
|
+
if ((l === "i" && r === "i") || (has("i") && (has("m") || has("d") || has("q")))) return mul(a, b);
|
|
414
|
+
if (pairIs("i", "r")) {
|
|
415
|
+
const [q, k] = l === "r" ? [a as Rat, b as Dec] : [b as Rat, a as Dec];
|
|
416
|
+
return new Rat(q.n * intOf(k), q.d);
|
|
417
|
+
}
|
|
418
|
+
if (has("i")) throw new TermError("TYPE_ERROR");
|
|
419
|
+
if (l === "r" && r === "r") return new Rat((a as Rat).n * (b as Rat).n, (a as Rat).d * (b as Rat).d);
|
|
420
|
+
if (pairIs("m", "r")) {
|
|
421
|
+
// E-0085: доля масштабирует деньги точно; 10-гладкость — после сокращения.
|
|
422
|
+
const [m, q] = l === "m" ? [a as Money, b as Rat] : [b as Money, a as Rat];
|
|
423
|
+
const out = exactOf(m.u * q.n, p10(m.s) * q.d, m);
|
|
424
|
+
if (out === null) throw new TermError("INEXACT_DIVISION");
|
|
425
|
+
return out;
|
|
426
|
+
}
|
|
427
|
+
if (has("r") || (l === "m" && r === "m") || (l === "q" && r === "q") || pairIs("m", "q")) throw new TermError("TYPE_ERROR");
|
|
428
|
+
return mul(a, b);
|
|
429
|
+
}
|
|
430
|
+
if (op === "div") {
|
|
431
|
+
if (l === "r" && r === "r") {
|
|
432
|
+
const x = a as Rat, y = b as Rat;
|
|
433
|
+
if (y.n === 0n) throw new TermError("DIVISION_BY_ZERO");
|
|
434
|
+
return new Rat(x.n * y.d, x.d * y.n);
|
|
435
|
+
}
|
|
436
|
+
if (l === "i" && r === "i") {
|
|
437
|
+
if (intOf(b as Dec) === 0n) throw new TermError("DIVISION_BY_ZERO");
|
|
438
|
+
return new Rat(intOf(a as Dec), intOf(b as Dec));
|
|
439
|
+
}
|
|
440
|
+
if (has("r")) throw new TermError("TYPE_ERROR");
|
|
441
|
+
const x = a as Dec, y = b as Dec;
|
|
442
|
+
const num = x.u * p10(y.s), den = y.u * p10(x.s);
|
|
443
|
+
if (l === "m" && r === "m") {
|
|
444
|
+
// E-0083: частное сумм одной валюты безразмерно — Decimal либо Rational.
|
|
445
|
+
if ((a as Money).cur !== (b as Money).cur) throw new TermError("CURRENCY_MISMATCH");
|
|
446
|
+
if (y.u === 0n) throw new TermError("DIVISION_BY_ZERO");
|
|
447
|
+
return exactOf(num, den, null) ?? new Rat(num, den);
|
|
448
|
+
}
|
|
449
|
+
if (l === "q" && r === "q") {
|
|
450
|
+
// E-0068: частное одной единицы безразмерно; разные единицы — TYPE_ERROR раньше нуля.
|
|
451
|
+
if ((a as Qty).unit !== (b as Qty).unit) throw new TermError("TYPE_ERROR");
|
|
452
|
+
if (y.u === 0n) throw new TermError("DIVISION_BY_ZERO");
|
|
453
|
+
return exactOf(num, den, null) ?? new Rat(num, den);
|
|
454
|
+
}
|
|
455
|
+
if (!((l === "d" && r === "d") || (l === "m" && r === "d") || (l === "q" && r === "d"))) throw new TermError("TYPE_ERROR");
|
|
456
|
+
if (y.u === 0n) throw new TermError("DIVISION_BY_ZERO");
|
|
457
|
+
const out = exactOf(num, den, a);
|
|
458
|
+
if (out !== null) return out;
|
|
459
|
+
if (l === "d") return new Rat(num, den);
|
|
460
|
+
throw new TermError("INEXACT_DIVISION");
|
|
461
|
+
}
|
|
462
|
+
if (op === "mod") {
|
|
463
|
+
if (l === "i" && r === "i") {
|
|
464
|
+
const yv = intOf(b as Dec);
|
|
465
|
+
if (yv === 0n) throw new TermError("DIVISION_BY_ZERO");
|
|
466
|
+
const m = yv < 0n ? -yv : yv;
|
|
467
|
+
return { u: ((intOf(a as Dec) % m) + m) % m, s: 0 };
|
|
468
|
+
}
|
|
469
|
+
throw new TermError("TYPE_ERROR");
|
|
470
|
+
}
|
|
471
|
+
throw new Refused("операция вне среза рантайма: " + String(op));
|
|
472
|
+
};
|
|
473
|
+
|
|
474
|
+
export const min = (a: Val, b: Val): Dec | Rat => {
|
|
475
|
+
if (a instanceof Rat || b instanceof Rat) return ratPair(a, b, "min");
|
|
476
|
+
const [x, y] = same(a, b, "min");
|
|
477
|
+
const [px, py] = pair(x, y);
|
|
478
|
+
return px <= py ? x : y;
|
|
479
|
+
};
|
|
480
|
+
|
|
481
|
+
export const max = (a: Val, b: Val): Dec | Rat => {
|
|
482
|
+
if (a instanceof Rat || b instanceof Rat) return ratPair(a, b, "max");
|
|
483
|
+
const [x, y] = same(a, b, "max");
|
|
484
|
+
const [px, py] = pair(x, y);
|
|
485
|
+
return px >= py ? x : y;
|
|
486
|
+
};
|
|
487
|
+
|
|
488
|
+
export const arith = (op: string, a: Val, b: Val, lk: string, rk: string): Val => {
|
|
489
|
+
for (const v of [a, b]) if (isNum(v)) boundedCoefficient(v.u, v.s);
|
|
490
|
+
const out = arithUnchecked(op, a, b, lk, rk);
|
|
491
|
+
if (isNum(out)) {
|
|
492
|
+
boundedCoefficient(out.u, out.s);
|
|
493
|
+
if (lk === "i" && rk === "i" && ["add", "sub", "mul", "mod"].includes(op) && (out.u < -(1n << 63n) || out.u >= (1n << 63n))) throw new Refused("NUMERIC_LIMIT_UNAVAILABLE: current i64/i128 arithmetic boundary");
|
|
494
|
+
}
|
|
495
|
+
return out;
|
|
496
|
+
};
|
|
497
|
+
|
|
498
|
+
export const abs = (v: Val): Dec | Rat => {
|
|
499
|
+
if (isNum(v) && v.s === 0 && v.u === -(1n << 63n)) throw new Refused("NUMERIC_LIMIT_UNAVAILABLE: current i64/i128 arithmetic boundary");
|
|
500
|
+
if (v instanceof Rat) return new Rat(v.n < 0n ? -v.n : v.n, v.d);
|
|
501
|
+
if (!isNum(v)) throw new Refused("§58: abs — не числовой вид");
|
|
502
|
+
return make(v.u < 0n ? -v.u : v.u, v.s, v);
|
|
503
|
+
};
|
|
504
|
+
|
|
505
|
+
const POW_EXP_LIMIT = 4096n;
|
|
506
|
+
const POW_MIN = -(2n ** 63n);
|
|
507
|
+
const POW_MAX = 2n ** 63n - 1n;
|
|
508
|
+
|
|
509
|
+
// §58 (E-0075): только Integer pow Integer; порядок проверок — знак, предел,
|
|
510
|
+
// вычисление, диапазон — часть контракта.
|
|
511
|
+
export const pow = (base: Val, exp: Val): Dec => {
|
|
512
|
+
if (!integral(base) || !integral(exp)) throw new Refused("§58: pow определён только для Integer");
|
|
513
|
+
const e = toInt(exp);
|
|
514
|
+
if (e < 0n) throw new Refused("§58: pow — показатель отрицателен");
|
|
515
|
+
if (e > POW_EXP_LIMIT) throw new Refused("RESOURCE_LIMIT: pow — показатель превышает 4096");
|
|
516
|
+
const r = toInt(base) ** e;
|
|
517
|
+
if (r < POW_MIN || r > POW_MAX) throw new Refused("RESOURCE_LIMIT: pow — результат вне 64-битного диапазона");
|
|
518
|
+
return { u: r, s: 0 };
|
|
519
|
+
};
|
|
520
|
+
|
|
521
|
+
export const round = (m: Val, digitsIn: number | Val, mode: string): Dec => {
|
|
522
|
+
if (mode !== "HALF_UP") throw new Refused("§50: режим округления не реализован: " + mode);
|
|
523
|
+
if (!isNum(m)) throw new Refused("§50: round — не числовой вид");
|
|
524
|
+
const digits = typeof digitsIn === "number" ? digitsIn
|
|
525
|
+
: isNum(digitsIn) ? Number(digitsIn.u / p10(digitsIn.s)) : 0;
|
|
526
|
+
if (m.s <= digits) return make(m.u * p10(digits - m.s), digits, m);
|
|
527
|
+
const drop = p10(m.s - digits);
|
|
528
|
+
const neg = m.u < 0n;
|
|
529
|
+
const a = neg ? -m.u : m.u;
|
|
530
|
+
const q = a / drop;
|
|
531
|
+
const r = a % drop;
|
|
532
|
+
const up = r * 2n >= drop ? q + 1n : q; // HALF_UP — от нуля
|
|
533
|
+
return make(neg ? -up : up, digits, m);
|
|
534
|
+
};
|
|
535
|
+
|
|
536
|
+
// --- политика округления §50 (DECISION-0259) ---------------------------------
|
|
537
|
+
|
|
538
|
+
const ROUNDING_MODES = ["CEILING", "DOWN", "FLOOR", "HALF_DOWN", "HALF_EVEN", "HALF_UP", "UP"];
|
|
539
|
+
const DIV_ROUND_MAX_PRECISION = 34n;
|
|
540
|
+
|
|
541
|
+
// Порт `values._round_fraction` на целых: num/den (den > 0) к целому по режиму
|
|
542
|
+
// §50; `2*tail` против знаменателя, ветви в порядке оракула.
|
|
543
|
+
const roundInt = (num: bigint, den: bigint, mode: string): bigint => {
|
|
544
|
+
const negative = num < 0n;
|
|
545
|
+
const a = negative ? -num : num;
|
|
546
|
+
let head = a / den;
|
|
547
|
+
const tail = a % den;
|
|
548
|
+
let up: boolean;
|
|
549
|
+
switch (mode) {
|
|
550
|
+
case "HALF_UP": up = 2n * tail >= den; break;
|
|
551
|
+
case "HALF_EVEN": up = 2n * tail > den || (2n * tail === den && head % 2n === 1n); break;
|
|
552
|
+
case "HALF_DOWN": up = 2n * tail > den; break;
|
|
553
|
+
case "UP": up = tail > 0n; break;
|
|
554
|
+
case "DOWN": up = false; break;
|
|
555
|
+
case "CEILING": up = tail > 0n && !negative; break;
|
|
556
|
+
case "FLOOR": up = tail > 0n && negative; break;
|
|
557
|
+
default: throw new Refused("§50: режим округления не реализован: " + mode);
|
|
558
|
+
}
|
|
559
|
+
if (up) head += 1n;
|
|
560
|
+
return negative ? -head : head;
|
|
561
|
+
};
|
|
562
|
+
|
|
563
|
+
// Точность политики: Integer либо целый Decimal, вид — из плана. Дробная
|
|
564
|
+
// точность — TYPE_ERROR; проверка на целом bigint до любого перевода в Number.
|
|
565
|
+
const policyScale = (p: Val, pk: string): bigint => {
|
|
566
|
+
if (pk !== "i" && pk !== "d") throw new Refused("вид точности расходится с планом: " + String(pk));
|
|
567
|
+
resolve(p, pk);
|
|
568
|
+
const d = p as Dec;
|
|
569
|
+
if (d.u % p10(d.s) !== 0n) throw new TermError("TYPE_ERROR");
|
|
570
|
+
return d.u / p10(d.s);
|
|
571
|
+
};
|
|
572
|
+
|
|
573
|
+
const policyMode = (mode: Val): string => {
|
|
574
|
+
if (typeof mode !== "string" || !ROUNDING_MODES.includes(mode)) throw new TermError("TYPE_ERROR");
|
|
575
|
+
return mode;
|
|
576
|
+
};
|
|
577
|
+
|
|
578
|
+
// §50 `round` с проверкой политики (DECISION-0259 п. 4, 7): порядок
|
|
579
|
+
// `values.round_value` — вид, точность (целое >= 0), режим. Integer — тождество
|
|
580
|
+
// (E-0110); точность не короче дробной части — значение то же (E-0184).
|
|
581
|
+
export const round_mode = (v: Val, p: Val, mode: Val, vk: string, pk: string): Val => {
|
|
582
|
+
const k = resolve(v, vk);
|
|
583
|
+
if (k === "r") throw new TermError("TYPE_ERROR");
|
|
584
|
+
const scale = policyScale(p, pk);
|
|
585
|
+
if (scale < 0n) throw new TermError("TYPE_ERROR");
|
|
586
|
+
const m = policyMode(mode);
|
|
587
|
+
const x = v as Dec;
|
|
588
|
+
if (k === "i" || scale >= BigInt(x.s)) return v;
|
|
589
|
+
const s = Number(scale); // s < x.s: перевод точен
|
|
590
|
+
return make(roundInt(x.u, p10(x.s - s), m), s, v);
|
|
591
|
+
};
|
|
592
|
+
|
|
593
|
+
// §50/§58 `div_round` (E-0036, E-0170; DECISION-0259 п. 4, 7): пара → валюта →
|
|
594
|
+
// ноль → точность 0..34 → режим. Частное точное, округление всегда;
|
|
595
|
+
// Money/Money — доля Decimal без тега.
|
|
596
|
+
export const div_round = (a: Val, b: Val, p: Val, mode: Val, ak: string, bk: string, pk: string): Val => {
|
|
597
|
+
const l = resolve(a, ak), r = resolve(b, bk);
|
|
598
|
+
if (!((l === "m" && r === "d") || (l === "q" && r === "d") || (l === "d" && r === "d") || (l === "m" && r === "m"))) {
|
|
599
|
+
throw new TermError("TYPE_ERROR");
|
|
600
|
+
}
|
|
601
|
+
if (l === "m" && r === "m" && (a as Money).cur !== (b as Money).cur) throw new TermError("CURRENCY_MISMATCH");
|
|
602
|
+
const x = a as Dec, y = b as Dec;
|
|
603
|
+
if (y.u === 0n) throw new TermError("DIVISION_BY_ZERO");
|
|
604
|
+
const scale = policyScale(p, pk);
|
|
605
|
+
if (!(scale >= 0n && scale <= DIV_ROUND_MAX_PRECISION)) throw new TermError("TYPE_ERROR");
|
|
606
|
+
const m = policyMode(mode);
|
|
607
|
+
const s = Number(scale);
|
|
608
|
+
let num = x.u * p10(y.s + s), den = y.u * p10(x.s);
|
|
609
|
+
if (den < 0n) { num = -num; den = -den; }
|
|
610
|
+
const q = roundInt(num, den, m);
|
|
611
|
+
return r === "m" ? { u: q, s } : make(q, s, a);
|
|
612
|
+
};
|
|
613
|
+
|
|
614
|
+
// --- сравнение и равенство ---------------------------------------------------
|
|
615
|
+
|
|
616
|
+
export const eq = (a: Val, b: Val): boolean => {
|
|
617
|
+
if (typeof a === "boolean" || typeof b === "boolean") return typeof a === "boolean" && typeof b === "boolean" && a === b;
|
|
618
|
+
if (a instanceof Rat || b instanceof Rat) return a instanceof Rat && b instanceof Rat && a.n === b.n && a.d === b.d;
|
|
619
|
+
if (isQty(a) || isQty(b)) {
|
|
620
|
+
if (!(isQty(a) && isQty(b)) || a.unit !== b.unit) return false;
|
|
621
|
+
const [x, y] = pair(a, b);
|
|
622
|
+
return x === y;
|
|
623
|
+
}
|
|
624
|
+
if (isSym(a) || isSym(b)) return isSym(a) && isSym(b) && a.sym === b.sym;
|
|
625
|
+
if (isDate(a) || isDate(b)) return isDate(a) && isDate(b) && a.day === b.day;
|
|
626
|
+
if (isInstant(a) || isInstant(b)) return isInstant(a) && isInstant(b) && momentCmp(a, b, "eq") === 0;
|
|
627
|
+
if (typeof a === "string" || typeof b === "string") return a === b;
|
|
628
|
+
if (isMoney(a) !== isMoney(b)) return false;
|
|
629
|
+
if (isMoney(a) && isMoney(b) && a.cur !== b.cur) return false;
|
|
630
|
+
const [x, y] = pair(a as Dec, b as Dec);
|
|
631
|
+
return x === y;
|
|
632
|
+
};
|
|
633
|
+
|
|
634
|
+
export const cmp = (a: Val, op: string, b: Val): boolean => {
|
|
635
|
+
if (typeof a === "boolean" || typeof b === "boolean") {
|
|
636
|
+
// §57 (DECISION-0266): Boolean сравнимо только на равенство и только с Boolean.
|
|
637
|
+
const sameBool = typeof a === "boolean" && typeof b === "boolean" && a === b;
|
|
638
|
+
if (op === "eq") return sameBool;
|
|
639
|
+
if (op === "ne") return !sameBool;
|
|
640
|
+
throw new Refused("§57: вид Boolean не упорядочен");
|
|
641
|
+
}
|
|
642
|
+
if (a instanceof Rat || b instanceof Rat) {
|
|
643
|
+
// §57/§48 (DECISION-0252 п. 6): Rational сравнивается только с Rational;
|
|
644
|
+
// иной вид — TYPE_ERROR оракула, а не false.
|
|
645
|
+
if (!(a instanceof Rat && b instanceof Rat)) throw new TermError("TYPE_ERROR");
|
|
646
|
+
const x = a.n * b.d, y = b.n * a.d;
|
|
647
|
+
switch (op) {
|
|
648
|
+
case "lt": return x < y;
|
|
649
|
+
case "le": return x <= y;
|
|
650
|
+
case "gt": return x > y;
|
|
651
|
+
case "ge": return x >= y;
|
|
652
|
+
case "eq": return x === y;
|
|
653
|
+
case "ne": return x !== y;
|
|
654
|
+
default: throw new Refused("сравнение вне среза рантайма: " + op);
|
|
655
|
+
}
|
|
656
|
+
}
|
|
657
|
+
if (isQty(a) || isQty(b)) {
|
|
658
|
+
// §49/§57 (DECISION-0256 п. 5): величина — только с величиной той же единицы.
|
|
659
|
+
const [qa, qb] = same(a, b, "cmp");
|
|
660
|
+
const [x, y] = pair(qa, qb);
|
|
661
|
+
switch (op) {
|
|
662
|
+
case "lt": return x < y;
|
|
663
|
+
case "le": return x <= y;
|
|
664
|
+
case "gt": return x > y;
|
|
665
|
+
case "ge": return x >= y;
|
|
666
|
+
case "eq": return x === y;
|
|
667
|
+
case "ne": return x !== y;
|
|
668
|
+
default: throw new Refused("сравнение вне среза рантайма: " + op);
|
|
669
|
+
}
|
|
670
|
+
}
|
|
671
|
+
if (isSym(a) || isSym(b)) {
|
|
672
|
+
const sameSym = isSym(a) && isSym(b) && a.sym === b.sym;
|
|
673
|
+
if (op === "eq") return sameSym;
|
|
674
|
+
if (op === "ne") return !sameSym;
|
|
675
|
+
throw new Refused("§57: вид enum не упорядочен");
|
|
676
|
+
}
|
|
677
|
+
if (isInstant(a) || isInstant(b)) {
|
|
678
|
+
if (isInstant(a) && isInstant(b)) {
|
|
679
|
+
const c = momentCmp(a, b, op);
|
|
680
|
+
switch (op) {
|
|
681
|
+
case "lt": return c < 0;
|
|
682
|
+
case "le": return c <= 0;
|
|
683
|
+
case "gt": return c > 0;
|
|
684
|
+
case "ge": return c >= 0;
|
|
685
|
+
case "eq": return c === 0;
|
|
686
|
+
case "ne": return c !== 0;
|
|
687
|
+
default: throw new Refused("сравнение вне среза рантайма: " + op);
|
|
688
|
+
}
|
|
689
|
+
}
|
|
690
|
+
// §57/§2.15: Date и Instant не сравниваются (DATE_INSTANT_MISMATCH оракула).
|
|
691
|
+
throw new Refused("§57: Instant сравнивается только с Instant");
|
|
692
|
+
}
|
|
693
|
+
if (isDate(a) || isDate(b)) {
|
|
694
|
+
if (isDate(a) && isDate(b)) {
|
|
695
|
+
switch (op) {
|
|
696
|
+
case "lt": return a.day < b.day;
|
|
697
|
+
case "le": return a.day <= b.day;
|
|
698
|
+
case "gt": return a.day > b.day;
|
|
699
|
+
case "ge": return a.day >= b.day;
|
|
700
|
+
case "eq": return a.day === b.day;
|
|
701
|
+
case "ne": return a.day !== b.day;
|
|
702
|
+
default: throw new Refused("сравнение вне среза рантайма: " + op);
|
|
703
|
+
}
|
|
704
|
+
}
|
|
705
|
+
if (op === "eq") return false;
|
|
706
|
+
if (op === "ne") return true;
|
|
707
|
+
throw new Refused("§57: Date сравнивается только с Date");
|
|
708
|
+
}
|
|
709
|
+
if (typeof a === "string" || typeof b === "string") {
|
|
710
|
+
if (op === "eq") return a === b;
|
|
711
|
+
if (op === "ne") return a !== b;
|
|
712
|
+
throw new Refused("порядок на Text вне среза рантайма: " + op);
|
|
713
|
+
}
|
|
714
|
+
if (isMoney(a) !== isMoney(b)) throw new Refused("§49: сравнение денег с безразмерным");
|
|
715
|
+
if (isMoney(a) && isMoney(b) && a.cur !== b.cur) throw new Refused("§49: разные валюты");
|
|
716
|
+
const [x, y] = pair(a as Dec, b as Dec);
|
|
717
|
+
switch (op) {
|
|
718
|
+
case "lt": return x < y;
|
|
719
|
+
case "le": return x <= y;
|
|
720
|
+
case "gt": return x > y;
|
|
721
|
+
case "ge": return x >= y;
|
|
722
|
+
case "eq": return x === y;
|
|
723
|
+
case "ne": return x !== y;
|
|
724
|
+
default: throw new Refused("сравнение вне среза рантайма: " + op);
|
|
725
|
+
}
|
|
726
|
+
};
|
|
727
|
+
|
|
728
|
+
const norm = (d: Dec): string => {
|
|
729
|
+
let { u, s } = d;
|
|
730
|
+
if (u === 0n) return "0e-0";
|
|
731
|
+
while (s > 0 && u % 10n === 0n) { u /= 10n; s -= 1; }
|
|
732
|
+
return u.toString() + "e-" + s;
|
|
733
|
+
};
|
|
734
|
+
|
|
735
|
+
// Разделитель и экран внутри текста экранируются: иначе ["a|e:b","c"] и
|
|
736
|
+
// ["a","b|e:c"] склеивались в один ключ, и второй факт терялся.
|
|
737
|
+
const esc = (text: string): string => text.replace(/\\/g, "\\\\").replace(/\|/g, "\\|");
|
|
738
|
+
|
|
739
|
+
export const key = (row: Val[]): string =>
|
|
740
|
+
row.map((v) => (typeof v === "string" ? "e:" + esc(v) : typeof v === "boolean" ? "b:" + (v ? "true" : "false") : isSym(v) ? "c:" + esc(v.sym) : isDate(v) ? "t:" + v.day : isInstant(v) ? "i:" + esc(v.at)
|
|
741
|
+
: v instanceof Rat ? "r:" + v.n.toString() + "/" + v.d.toString()
|
|
742
|
+
: isQty(v) ? "q:" + esc(v.unit) + ":" + norm(v)
|
|
743
|
+
: isMoney(v) ? "m:" + v.cur + ":" + norm(v) : "d:" + norm(v))).join("|");
|
|
744
|
+
|
|
745
|
+
// Контекст одного вычисления (DECISION-0423 §2.3): правовое время дела (правило
|
|
746
|
+
// вне окна `effective` не применяется), политика срока §86 и снимок календаря
|
|
747
|
+
// §85. `evaluate` синхронен: он ставит свежий контекст на время вызова и в
|
|
748
|
+
// `finally` возвращает прежний, поэтому состояние вызова его не переживает.
|
|
749
|
+
type Context = { legalTime: string; policy: Policy | null; snapshot: Snapshot | null };
|
|
750
|
+
const freshContext = (): Context => ({ legalTime: "", policy: null, snapshot: null });
|
|
751
|
+
let CTX: Context = freshContext();
|
|
752
|
+
export const beginContext = (): Context => { const prev = CTX; CTX = freshContext(); return prev; };
|
|
753
|
+
export const endContext = (prev: Context): void => { CTX = prev; };
|
|
754
|
+
export const setLegalTime = (t: string): void => { CTX.legalTime = t; };
|
|
755
|
+
export const legalTime = (): string => {
|
|
756
|
+
if (!CTX.legalTime) throw new Refused("legalTime не подан делом");
|
|
757
|
+
return CTX.legalTime;
|
|
758
|
+
};
|
|
759
|
+
|
|
760
|
+
// --- календарь §84–§86 (DECISION-0245) ---------------------------------------
|
|
761
|
+
// Порт `lawref.values` и `lawref.calendar` дословно по порядку проверок: код
|
|
762
|
+
// ошибки — часть ответа (issue модели). Даты — ISO-строки, арифметика дней в UTC.
|
|
763
|
+
|
|
764
|
+
export class TermError extends Error { constructor(public code: string) { super(code); } }
|
|
765
|
+
|
|
766
|
+
export type Snapshot = { id: string; error?: string; start?: string; endExclusive?: string;
|
|
767
|
+
weekly?: boolean[]; overrides?: Record<string, boolean> };
|
|
768
|
+
type Policy = Record<string, any>;
|
|
769
|
+
export const setCalendar = (policy: Policy | null, snapshot: Snapshot | null): void => {
|
|
770
|
+
CTX.policy = policy; CTX.snapshot = snapshot;
|
|
771
|
+
};
|
|
772
|
+
|
|
773
|
+
// Date.UTC сдвигает годы 0–99 на 1900: полная дата ставится setUTCFullYear.
|
|
774
|
+
const utc = (y: number, m: number, d: number): number => {
|
|
775
|
+
const t = new globalThis.Date(0);
|
|
776
|
+
t.setUTCFullYear(y, m, d);
|
|
777
|
+
return t.getTime();
|
|
778
|
+
};
|
|
779
|
+
const ms = (iso: string): number => utc(+iso.slice(0, 4), +iso.slice(5, 7) - 1, +iso.slice(8, 10));
|
|
780
|
+
const iso = (t: number): string => {
|
|
781
|
+
const d = new globalThis.Date(t);
|
|
782
|
+
return String(d.getUTCFullYear()).padStart(4, "0") + "-" + String(d.getUTCMonth() + 1).padStart(2, "0")
|
|
783
|
+
+ "-" + String(d.getUTCDate()).padStart(2, "0");
|
|
784
|
+
};
|
|
785
|
+
const plusDays = (day: string, n: number): string => iso(ms(day) + n * 86400000);
|
|
786
|
+
|
|
787
|
+
const calendar = (): Snapshot => {
|
|
788
|
+
const snap = CTX.snapshot;
|
|
789
|
+
if (snap === null || snap.error === "MISSING_CALENDAR") throw new TermError("MISSING_CALENDAR");
|
|
790
|
+
if (snap.error !== undefined) throw new TermError(snap.error);
|
|
791
|
+
return snap;
|
|
792
|
+
};
|
|
793
|
+
const coverage = (cal: Snapshot, day: string): void => {
|
|
794
|
+
if (!((cal.start as string) <= day && day < (cal.endExclusive as string))) throw new TermError("CALENDAR_OUT_OF_RANGE");
|
|
795
|
+
};
|
|
796
|
+
const working = (cal: Snapshot, day: string): boolean => {
|
|
797
|
+
coverage(cal, day);
|
|
798
|
+
const o = (cal.overrides as Record<string, boolean>)[day];
|
|
799
|
+
if (o !== undefined) return o;
|
|
800
|
+
return (cal.weekly as boolean[])[(new globalThis.Date(ms(day)).getUTCDay() + 6) % 7];
|
|
801
|
+
};
|
|
802
|
+
const previousWorking = (cal: Snapshot, day: string): string => {
|
|
803
|
+
coverage(cal, day);
|
|
804
|
+
let current = plusDays(day, -1);
|
|
805
|
+
for (;;) {
|
|
806
|
+
coverage(cal, current);
|
|
807
|
+
if (working(cal, current)) return current;
|
|
808
|
+
current = plusDays(current, -1);
|
|
809
|
+
}
|
|
810
|
+
};
|
|
811
|
+
const nextWorking = (cal: Snapshot, day: string): string => {
|
|
812
|
+
while (!working(cal, day)) { day = plusDays(day, 1); coverage(cal, day); }
|
|
813
|
+
return day;
|
|
814
|
+
};
|
|
815
|
+
const lastDay = (year: number, month: number): number => new globalThis.Date(utc(year, month, 0)).getUTCDate();
|
|
816
|
+
const pad = (n: number, w: number): string => String(n).padStart(w, "0");
|
|
817
|
+
const addMonths = (day: string, months: number, monthEnd: string): string => {
|
|
818
|
+
const total = +day.slice(5, 7) - 1 + months;
|
|
819
|
+
const year = +day.slice(0, 4) + Math.floor(total / 12), month = ((total % 12) + 12) % 12 + 1;
|
|
820
|
+
const last = lastDay(year, month), d = +day.slice(8, 10);
|
|
821
|
+
if (d <= last) return `${pad(year, 4)}-${pad(month, 2)}-${pad(d, 2)}`;
|
|
822
|
+
if (monthEnd === "last_day_of_month") return `${pad(year, 4)}-${pad(month, 2)}-${pad(last, 2)}`;
|
|
823
|
+
throw new TermError("DEADLINE_POLICY_INVALID");
|
|
824
|
+
};
|
|
825
|
+
const CAL_UNITS = ["calendar_month", "calendar_year"];
|
|
826
|
+
const needsCalendar = (unit: string, policy: Policy | null): boolean => {
|
|
827
|
+
if (![...CAL_UNITS, "calendar_week"].includes(unit)) return true;
|
|
828
|
+
const p = policy ?? {};
|
|
829
|
+
return p.roll !== "no_roll" || p.include_end !== true;
|
|
830
|
+
};
|
|
831
|
+
const FIELDS = new Set(["start_count", "include_end", "roll", "cutoff", "month_end"]);
|
|
832
|
+
|
|
833
|
+
// Порт `calendar.compute_deadline` без приостановлений (терм их не несёт).
|
|
834
|
+
const deadline = (cal: Snapshot | null, after: string, count: number, policy: Policy | null,
|
|
835
|
+
afterTime: string | null, unit: string, calendarDayStep = false): string => {
|
|
836
|
+
if (policy === null) throw new TermError("MISSING_POLICY");
|
|
837
|
+
if (Object.keys(policy).some((k) => !FIELDS.has(k))) throw new TermError("DEADLINE_POLICY_INVALID");
|
|
838
|
+
const startCount = policy.start_count, roll = policy.roll, includeEnd = policy.include_end;
|
|
839
|
+
const cutoff = policy.cutoff === undefined ? null : policy.cutoff;
|
|
840
|
+
if (!["same_day", "next_day"].includes(startCount) || !["no_roll", "next_working_day", "previous_working_day"].includes(roll)
|
|
841
|
+
|| typeof includeEnd !== "boolean" || !(cutoff === null || typeof cutoff === "string")) {
|
|
842
|
+
throw new TermError("DEADLINE_POLICY_INVALID");
|
|
843
|
+
}
|
|
844
|
+
if (count < 0) throw new TermError("DEADLINE_POLICY_INVALID");
|
|
845
|
+
const monthEnd = policy.month_end === undefined ? null : policy.month_end;
|
|
846
|
+
if (CAL_UNITS.includes(unit)) {
|
|
847
|
+
if (!["last_day_of_month", "reject_nonexistent"].includes(monthEnd)) throw new TermError("DEADLINE_POLICY_INVALID");
|
|
848
|
+
} else if (monthEnd !== null && monthEnd !== "last_day_of_month" && monthEnd !== "reject_nonexistent") {
|
|
849
|
+
throw new TermError("DEADLINE_POLICY_INVALID");
|
|
850
|
+
}
|
|
851
|
+
let start = after;
|
|
852
|
+
const wants = calendarDayStep ? false : needsCalendar(unit, policy);
|
|
853
|
+
if (wants) coverage(cal as Snapshot, start);
|
|
854
|
+
if (cutoff !== null && afterTime !== null && afterTime > cutoff) {
|
|
855
|
+
start = plusDays(start, 1);
|
|
856
|
+
if (wants) coverage(cal as Snapshot, start);
|
|
857
|
+
}
|
|
858
|
+
if (startCount === "next_day") {
|
|
859
|
+
start = plusDays(start, 1);
|
|
860
|
+
if (wants) coverage(cal as Snapshot, start);
|
|
861
|
+
}
|
|
862
|
+
let due: string;
|
|
863
|
+
if (CAL_UNITS.includes(unit)) {
|
|
864
|
+
const months = count * (unit === "calendar_year" ? 12 : 1);
|
|
865
|
+
due = count > 0 ? addMonths(start, months, monthEnd) : start;
|
|
866
|
+
if (count > 0 && !includeEnd) { coverage(cal as Snapshot, due); due = previousWorking(cal as Snapshot, due); }
|
|
867
|
+
} else if (calendarDayStep || unit === "calendar_week") {
|
|
868
|
+
const multiplier = calendarDayStep ? 1 : 7;
|
|
869
|
+
due = count > 0 ? plusDays(start, multiplier * count) : start;
|
|
870
|
+
if (count > 0 && !includeEnd) { coverage(cal as Snapshot, due); due = previousWorking(cal as Snapshot, due); }
|
|
871
|
+
} else if (count > 0) {
|
|
872
|
+
due = start;
|
|
873
|
+
let counted = 0;
|
|
874
|
+
for (;;) {
|
|
875
|
+
coverage(cal as Snapshot, due);
|
|
876
|
+
if (unit === "business_day" ? working(cal as Snapshot, due) : true) {
|
|
877
|
+
counted += 1;
|
|
878
|
+
if (counted === count) break;
|
|
879
|
+
}
|
|
880
|
+
due = plusDays(due, 1);
|
|
881
|
+
}
|
|
882
|
+
if (!includeEnd) due = previousWorking(cal as Snapshot, due);
|
|
883
|
+
} else {
|
|
884
|
+
due = start;
|
|
885
|
+
}
|
|
886
|
+
const skipRoll = (calendarDayStep || [...CAL_UNITS, "calendar_week"].includes(unit)) && roll === "no_roll";
|
|
887
|
+
if (!skipRoll && !working(cal as Snapshot, due) && roll !== "no_roll") {
|
|
888
|
+
due = roll === "next_working_day" ? nextWorking(cal as Snapshot, due) : previousWorking(cal as Snapshot, due);
|
|
889
|
+
}
|
|
890
|
+
return due;
|
|
891
|
+
};
|
|
892
|
+
|
|
893
|
+
// §84.2 (E-0084, DECISION-0267): границы месяца — чистые функции над Date; ни
|
|
894
|
+
// политика §86, ни снимок §85 не читаются (`values.month_bound_value`).
|
|
895
|
+
const monthParts = (d: Val): [number, number, number] => {
|
|
896
|
+
if (!isDate(d)) throw new TermError("TYPE_ERROR");
|
|
897
|
+
const m = /^(\d{4})-(\d{2})-(\d{2})$/.exec(d.day);
|
|
898
|
+
if (!m) throw new TermError("TYPE_ERROR");
|
|
899
|
+
const y = Number(m[1]), mo = Number(m[2]), da = Number(m[3]);
|
|
900
|
+
const last = new globalThis.Date(globalThis.Date.UTC(y, mo, 0)).getUTCDate();
|
|
901
|
+
if (mo < 1 || mo > 12 || da < 1 || da > last) throw new TermError("TYPE_ERROR");
|
|
902
|
+
return [y, mo, last];
|
|
903
|
+
};
|
|
904
|
+
const pad2 = (n: number): string => String(n).padStart(2, "0");
|
|
905
|
+
export const month_start = (d: Val): Date => {
|
|
906
|
+
const [y, mo] = monthParts(d);
|
|
907
|
+
return { day: `${String(y).padStart(4, "0")}-${pad2(mo)}-01` };
|
|
908
|
+
};
|
|
909
|
+
export const month_end = (d: Val): Date => {
|
|
910
|
+
const [y, mo, last] = monthParts(d);
|
|
911
|
+
return { day: `${String(y).padStart(4, "0")}-${pad2(mo)}-${pad2(last)}` };
|
|
912
|
+
};
|
|
913
|
+
|
|
914
|
+
// §84.4 (E-0015): календарные дни от `a` до `b`, со знаком.
|
|
915
|
+
export const days_between = (a: Date, b: Date): Dec => ({ u: BigInt(Math.round((ms(b.day) - ms(a.day)) / 86400000)), s: 0 });
|
|
916
|
+
|
|
917
|
+
// §82.2: validate first; seconds are safe integers throughout years 0001–9999.
|
|
918
|
+
// The fraction stays text, so no precision is lost at any length.
|
|
919
|
+
const DATE_RE = /^[0-9]{4}-[0-9]{2}-[0-9]{2}$/;
|
|
920
|
+
const dateParts = (raw: string): globalThis.Date => {
|
|
921
|
+
if (typeof raw !== "string" || !DATE_RE.test(raw) || raw.length !== 10) throw new TermError("TYPE_ERROR");
|
|
922
|
+
const y = +raw.slice(0, 4), m = +raw.slice(5, 7), d = +raw.slice(8, 10);
|
|
923
|
+
const t = new globalThis.Date(0);
|
|
924
|
+
t.setUTCFullYear(y, m - 1, d); t.setUTCHours(0, 0, 0, 0);
|
|
925
|
+
if (y < 1 || t.getUTCFullYear() !== y || t.getUTCMonth() !== m - 1 || t.getUTCDate() !== d) throw new TermError("TYPE_ERROR");
|
|
926
|
+
return t;
|
|
927
|
+
};
|
|
928
|
+
const INSTANT_RE = /^([0-9]{4}-[0-9]{2}-[0-9]{2})T([0-9]{2}):([0-9]{2}):([0-9]{2})(?:\.([0-9]+))?(Z|[+-][0-9]{2}:[0-9]{2})$/;
|
|
929
|
+
const moment = (v: Instant): { seconds: number; fraction: string } => {
|
|
930
|
+
const m = typeof v.at === "string" ? INSTANT_RE.exec(v.at) : null;
|
|
931
|
+
if (m === null || m[0].length !== v.at.length) throw new TermError("TYPE_ERROR");
|
|
932
|
+
const [, day, h, mi, s, frac = "", off] = m;
|
|
933
|
+
let seconds = dateParts(day).getTime() / 1000;
|
|
934
|
+
if (+h > 23 || +mi > 59 || +s > 59) throw new TermError("TYPE_ERROR");
|
|
935
|
+
seconds += +h * 3600 + +mi * 60 + +s;
|
|
936
|
+
if (off !== "Z") {
|
|
937
|
+
const oh = +off.slice(1, 3), om = +off.slice(4, 6);
|
|
938
|
+
if (oh > 23 || om > 59) throw new TermError("TYPE_ERROR");
|
|
939
|
+
seconds -= (off[0] === "-" ? -1 : 1) * (oh * 3600 + om * 60);
|
|
940
|
+
}
|
|
941
|
+
return { seconds, fraction: frac.replace(/0+$/, "") };
|
|
942
|
+
};
|
|
943
|
+
const momentCmp = (a: Instant, b: Instant, op: string): number => {
|
|
944
|
+
const x = moment(a), y = moment(b);
|
|
945
|
+
return x.seconds < y.seconds ? -1 : x.seconds > y.seconds ? 1 :
|
|
946
|
+
x.fraction < y.fraction ? -1 : x.fraction > y.fraction ? 1 : 0;
|
|
947
|
+
};
|
|
948
|
+
export const validateTemporalCase = (kase: any): void => {
|
|
949
|
+
for (const [i, assertion] of (kase.assertions ?? []).entries()) {
|
|
950
|
+
if (!("valid" in assertion)) continue;
|
|
951
|
+
const w = assertion.valid, base = `TEMPORAL_BOUND_INVALID: request:/case/assertions/${i}/valid`;
|
|
952
|
+
if (w === null || typeof w !== "object" || Array.isArray(w)) throw new Refused(base);
|
|
953
|
+
for (const side of ["start", "end"]) {
|
|
954
|
+
if (!(side in w) || w[side] === (side === "start" ? "-infinity" : "infinity")) continue;
|
|
955
|
+
try { if (typeof w[side] === "string" && DATE_RE.test(w[side])) dateParts(w[side]); else moment({ at: w[side] }); }
|
|
956
|
+
catch (e) { if (e instanceof TermError) throw new Refused(base + "/" + side); throw e; }
|
|
957
|
+
}
|
|
958
|
+
}
|
|
959
|
+
};
|
|
960
|
+
// §84.1 (E-0028/E-0058): полных единиц между мгновениями, знак сохранён, дробь к нулю.
|
|
961
|
+
const durationBetween = (a: Val, b: Val, unit: number, where: string): Dec => {
|
|
962
|
+
if (!isInstant(a) || !isInstant(b)) throw new TermError("TYPE_ERROR");
|
|
963
|
+
const x = moment(a), y = moment(b);
|
|
964
|
+
let seconds = y.seconds - x.seconds;
|
|
965
|
+
if (seconds > 0 && y.fraction < x.fraction) seconds -= 1;
|
|
966
|
+
else if (seconds < 0 && y.fraction > x.fraction) seconds += 1;
|
|
967
|
+
return { u: BigInt(Math.trunc(seconds / unit)), s: 0 };
|
|
968
|
+
};
|
|
969
|
+
export const hours_between = (a: Val, b: Val): Dec => durationBetween(a, b, 3600, "hours_between");
|
|
970
|
+
export const minutes_between = (a: Val, b: Val): Dec => durationBetween(a, b, 60, "minutes_between");
|
|
971
|
+
// §84.1 (E-0185): мгновение через целую Quantity секунд/минут/часов/суток; канон UTC без дробных секунд.
|
|
972
|
+
const DURATION_SECONDS: Record<string, number> = { second: 1, minute: 60, hour: 3600, day: 86400 };
|
|
973
|
+
export const add_duration = (after: Val, count: number, unit: string): Instant => {
|
|
974
|
+
if (!isInstant(after)) throw new TermError("TYPE_ERROR");
|
|
975
|
+
const factor = DURATION_SECONDS[unit];
|
|
976
|
+
if (factor === undefined) throw new TermError("TYPE_ERROR");
|
|
977
|
+
const x = moment(after);
|
|
978
|
+
const total = x.seconds + count * factor;
|
|
979
|
+
const d = new globalThis.Date(total * 1000);
|
|
980
|
+
if (Number.isNaN(d.getTime()) || d.getUTCFullYear() < 1 || d.getUTCFullYear() > 9999) throw new TermError("RESOURCE_LIMIT");
|
|
981
|
+
const p = (n: number): string => String(n).padStart(2, "0");
|
|
982
|
+
return { at: `${String(d.getUTCFullYear()).padStart(4, "0")}-${p(d.getUTCMonth() + 1)}-${p(d.getUTCDate())}T${p(d.getUTCHours())}:${p(d.getUTCMinutes())}:${p(d.getUTCSeconds())}Z` };
|
|
983
|
+
};
|
|
984
|
+
|
|
985
|
+
// §84.2 (E-0029): `TermEnv.calendar_period` — снимок не читается.
|
|
986
|
+
export const add_calendar_period = (after: Date, count: number, unit: string): Date => {
|
|
987
|
+
// E-0187: calendar_day validates but does not apply month_end.
|
|
988
|
+
const p = CTX.policy ?? {};
|
|
989
|
+
if (!(p.roll === undefined || p.roll === null || p.roll === "no_roll") || (p.cutoff !== undefined && p.cutoff !== null)
|
|
990
|
+
|| p.include_end !== true) {
|
|
991
|
+
throw new TermError("MISSING_POLICY");
|
|
992
|
+
}
|
|
993
|
+
return { day: deadline(null, after.day, count, p, null, unit, unit === "calendar_day") };
|
|
994
|
+
};
|
|
995
|
+
|
|
996
|
+
const COUNTER_UNITS: Record<string, string[]> = {
|
|
997
|
+
add_business_days: ["business_day", "calendar_day"],
|
|
998
|
+
add_calendar_period: ["calendar_day", "calendar_month", "calendar_year", "calendar_week"],
|
|
999
|
+
add_legal_term: ["business_day", "calendar_day", "calendar_month", "calendar_year", "calendar_week"],
|
|
1000
|
+
};
|
|
1001
|
+
|
|
1002
|
+
// §86.1 (DECISION-0256 п. 6): счётчик-связыватель — Quantity с единицей,
|
|
1003
|
+
// допустимой функции, целый; иначе TYPE_ERROR (`values.add_*_value`). Знак не
|
|
1004
|
+
// проверяется: отрицательный счётчик отвергает сама операция политикой §86.
|
|
1005
|
+
export const counter = (v: Val, fn: string): [number, string] => {
|
|
1006
|
+
if (!isQty(v) || !COUNTER_UNITS[fn].includes(v.unit) || v.u % p10(v.s) !== 0n) throw new TermError("TYPE_ERROR");
|
|
1007
|
+
return [Number(v.u / p10(v.s)), v.unit];
|
|
1008
|
+
};
|
|
1009
|
+
|
|
1010
|
+
// §86.1: ветвь по единице на деле — рабочий счёт либо календарный шаг.
|
|
1011
|
+
export const add_legal_term = (after: Date, count: number, unit: string): Date => {
|
|
1012
|
+
if (unit === "business_day" || unit === "calendar_day") return add_business_days(after, count, unit);
|
|
1013
|
+
return add_calendar_period(after, count, unit);
|
|
1014
|
+
};
|
|
1015
|
+
|
|
1016
|
+
// §86: `TermEnv.deadline` — политика, затем снимок, если срок его спрашивает.
|
|
1017
|
+
export const add_business_days = (after: Date, count: number, unit: string, afterTime?: string): Date => {
|
|
1018
|
+
const POLICY = CTX.policy;
|
|
1019
|
+
if (POLICY !== null && POLICY.cutoff !== undefined && POLICY.cutoff !== null && afterTime === undefined) {
|
|
1020
|
+
throw new TermError("MISSING_POLICY");
|
|
1021
|
+
}
|
|
1022
|
+
const cal = needsCalendar(unit, POLICY) ? calendar() : null;
|
|
1023
|
+
return { day: deadline(cal, after.day, count, POLICY, afterTime === undefined ? null : afterTime, unit) };
|
|
1024
|
+
};
|
|
1025
|
+
|
|
1026
|
+
// ---------------------------------------------------------------------------
|
|
1027
|
+
// §47.3 Канал суждения (DECISION-0274): порт `whynot.judgment_blockers`,
|
|
1028
|
+
// `defeater_judgment_blockers`, `power_judgment_blockers` и
|
|
1029
|
+
// `position_judgment_blockers` над ГОТОВЫМ состоянием модели. Один код для
|
|
1030
|
+
// раннера и API: модель и переводы термов приходят параметрами. Запрос
|
|
1031
|
+
// называет судью только там, где его ответ может изменить исход: живость
|
|
1032
|
+
// правила по конъюнктам (E-0101), по поражению вывода (E-0103), по способности
|
|
1033
|
+
// дефитера снять вывод (E-0100), по иммунитету (E-0120); порядок запросов —
|
|
1034
|
+
// канонические байты объекта `request` (E-0111).
|
|
1035
|
+
export type JHooks = {
|
|
1036
|
+
val: (term: any) => Val; // литерал-терм §53 → значение
|
|
1037
|
+
cell: (pred: string, col: number, v: Val) => any; // ячейка таблицы → литерал-терм
|
|
1038
|
+
};
|
|
1039
|
+
export const JUDGMENT_ENUM_CAP = 8;
|
|
1040
|
+
const JUDGMENT_TESTABLE = new Set(["established", "supported", "monotone", "refuted"]);
|
|
1041
|
+
const JMIRROR: Record<string, string> = { TRUE_ONLY: "FALSE_ONLY", FALSE_ONLY: "TRUE_ONLY", BOTH: "BOTH", NEITHER: "NEITHER" };
|
|
1042
|
+
const CONJUNCT_STATUS: Record<string, string> = { TRUE_ONLY: "SATISFIED", FALSE_ONLY: "NOT_SATISFIED", BOTH: "CONFLICTED", NEITHER: "UNDETERMINED" };
|
|
1043
|
+
const JCOMPUTED = new Set(["binary", "call", "if", "let", "match", "aggregate"]);
|
|
1044
|
+
|
|
1045
|
+
// Канонические байты §208 объекта запроса: ключи по порядку, строки как JSON,
|
|
1046
|
+
// целое из JSON — цифрами; порядок строк — по кодам символов (UTF-8 у оракула).
|
|
1047
|
+
export const jcanon = (v: any): string => {
|
|
1048
|
+
if (v === null || v === undefined) return "null";
|
|
1049
|
+
if (typeof v === "boolean") return v ? "true" : "false";
|
|
1050
|
+
if (typeof v === "string") return JSON.stringify(v);
|
|
1051
|
+
if (typeof v === "number") return String(v);
|
|
1052
|
+
if (Array.isArray(v)) return "[" + v.map(jcanon).join(",") + "]";
|
|
1053
|
+
if (typeof v === "object" && "int" in v && Object.keys(v).length === 1) return String(v.int);
|
|
1054
|
+
return "{" + Object.keys(v).sort().map((k) => JSON.stringify(k) + ":" + jcanon(v[k])).join(",") + "}";
|
|
1055
|
+
};
|
|
1056
|
+
export const requestKey = (lit: any): string =>
|
|
1057
|
+
jcanon({ relation: lit.predicate, polarity: lit.polarity ?? "positive", args: lit.args ?? [] });
|
|
1058
|
+
export const orderRequests = (lits: any[]): any[] => {
|
|
1059
|
+
const found = new Map<string, any>();
|
|
1060
|
+
for (const l of lits) { const k = requestKey(l); if (!found.has(k)) found.set(k, l); }
|
|
1061
|
+
return [...found.keys()].sort().map((k) => found.get(k));
|
|
1062
|
+
};
|
|
1063
|
+
|
|
1064
|
+
const jtruth = (M: any, hooks: JHooks, lit: any): string => {
|
|
1065
|
+
const st: string = M.status(lit.predicate, (lit.args ?? []).map(hooks.val));
|
|
1066
|
+
return (lit.polarity ?? "positive") === "negative" ? JMIRROR[st] : st;
|
|
1067
|
+
};
|
|
1068
|
+
const jvars = (t: any, out: Set<string>): void => {
|
|
1069
|
+
if (Array.isArray(t)) { for (const x of t) jvars(x, out); return; }
|
|
1070
|
+
if (t === null || typeof t !== "object") return;
|
|
1071
|
+
if (t.kind === "var") { out.add(String(t.var)); return; }
|
|
1072
|
+
for (const k of Object.keys(t)) jvars(t[k], out);
|
|
1073
|
+
};
|
|
1074
|
+
const jkind = (name: string): string => {
|
|
1075
|
+
if (name === "urn:law:std#Integer" || name === "urn:law:std#Natural") return "i";
|
|
1076
|
+
if (name === "urn:law:std#Decimal") return "d";
|
|
1077
|
+
if (name === "urn:law:std#Money") return "m";
|
|
1078
|
+
if (name === "urn:law:std#Rational") return "r";
|
|
1079
|
+
if (name === "urn:law:std#Quantity") return "q";
|
|
1080
|
+
return "x";
|
|
1081
|
+
};
|
|
1082
|
+
// Гард §57–§58 под подстановкой: переменные, литералы, константы §54 и
|
|
1083
|
+
// арифметика §58; иное — исключение, у оракула это UNDETERMINED (E-0101).
|
|
1084
|
+
const jterm = (M: any, hooks: JHooks, t: any, subst: Record<string, any>): { v: Val; k: string } => {
|
|
1085
|
+
if (t.kind === "var") {
|
|
1086
|
+
if (!(t.var in subst)) throw new Refused("несвязанная переменная гарда");
|
|
1087
|
+
return jterm(M, hooks, subst[t.var], subst);
|
|
1088
|
+
}
|
|
1089
|
+
if (t.kind === "const_ref" && M.BACKCHAIN_CONSTS && t.id in M.BACKCHAIN_CONSTS) return jterm(M, hooks, M.BACKCHAIN_CONSTS[t.id], subst);
|
|
1090
|
+
if (t.kind === "value") return { v: hooks.val(t), k: jkind(String(t.type?.name)) };
|
|
1091
|
+
if (t.kind === "entity_ref" || t.kind === "const_ref") return { v: hooks.val(t), k: "x" };
|
|
1092
|
+
if (t.kind === "binary" && ["add", "sub", "mul", "div", "mod"].includes(String(t.op))) {
|
|
1093
|
+
const l = jterm(M, hooks, t.left, subst), r = jterm(M, hooks, t.right, subst);
|
|
1094
|
+
if (l.k === "x" || r.k === "x") throw new Refused("арифметика над нечисловым операндом");
|
|
1095
|
+
const v = arith(String(t.op), l.v, r.v, l.k, r.k);
|
|
1096
|
+
const k = v instanceof Rat ? "r" : isMoney(v) ? "m" : isQty(v) ? "q" : l.k === "i" && r.k === "i" ? "i" : "d";
|
|
1097
|
+
return { v, k };
|
|
1098
|
+
}
|
|
1099
|
+
throw new Refused("терм гарда вне обхода суждения: " + String(t.kind));
|
|
1100
|
+
};
|
|
1101
|
+
const jguard = (M: any, hooks: JHooks, g: any, subst: Record<string, any>): boolean => {
|
|
1102
|
+
const l = jterm(M, hooks, g.left, subst), r = jterm(M, hooks, g.right, subst);
|
|
1103
|
+
return cmp(l.v, String(g.op), r.v);
|
|
1104
|
+
};
|
|
1105
|
+
|
|
1106
|
+
type JReport = { trigger: string; ground: any } | null;
|
|
1107
|
+
// Порт `whynot._conjunct_report`: null — конъюнкт с несвязанной переменной.
|
|
1108
|
+
const jconjunct = (M: any, hooks: JHooks, status: string, lit: any, subst: Record<string, any>): JReport => {
|
|
1109
|
+
if (status === "guard") {
|
|
1110
|
+
const vars = new Set<string>(); jvars(lit, vars);
|
|
1111
|
+
if ([...vars].some((v) => !(v in subst))) return null;
|
|
1112
|
+
let ok: boolean;
|
|
1113
|
+
try { ok = jguard(M, hooks, lit, subst); } catch (e) { return { trigger: "UNDETERMINED", ground: null }; }
|
|
1114
|
+
return { trigger: ok ? "SATISFIED" : "NOT_SATISFIED", ground: null };
|
|
1115
|
+
}
|
|
1116
|
+
if (lit?.kind !== "literal") return { trigger: "UNDETERMINED", ground: null };
|
|
1117
|
+
const args: any[] = lit.args ?? [];
|
|
1118
|
+
const vars = new Set<string>(); jvars(args, vars);
|
|
1119
|
+
if ([...vars].some((v) => !(v in subst))) return null;
|
|
1120
|
+
if (args.some((a) => JCOMPUTED.has(a?.kind))) return { trigger: "UNDETERMINED", ground: null };
|
|
1121
|
+
const shown = { ...lit, args: args.map((a) => (a.kind === "var" ? subst[a.var] : a)) };
|
|
1122
|
+
if (args.some((a) => a?.kind === "wildcard")) {
|
|
1123
|
+
// §98 (E-0165): свидетель существования, значение не связывается.
|
|
1124
|
+
if ((lit.polarity ?? "positive") !== "positive" || !["established", "supported", "monotone"].includes(status)) {
|
|
1125
|
+
return { trigger: "UNDETERMINED", ground: shown };
|
|
1126
|
+
}
|
|
1127
|
+
const rows: Val[][] = M.TABLES[lit.predicate] ?? [];
|
|
1128
|
+
const want = shown.args.map((a: any) => (a.kind === "wildcard" ? null : hooks.val(a)));
|
|
1129
|
+
const witness = rows.some((row) => row.length === want.length && row.every((v, i) => want[i] === null || key([v]) === key([want[i] as Val]))
|
|
1130
|
+
&& (status === "established" ? M.status(lit.predicate, row) === "TRUE_ONLY" : ["TRUE_ONLY", "BOTH"].includes(M.status(lit.predicate, row))));
|
|
1131
|
+
return { trigger: witness ? "SATISFIED" : "UNDETERMINED", ground: shown };
|
|
1132
|
+
}
|
|
1133
|
+
const pair = jtruth(M, hooks, shown);
|
|
1134
|
+
if (status === "not_known") return { trigger: pair === "NEITHER" ? "SATISFIED" : "NOT_SATISFIED", ground: shown };
|
|
1135
|
+
if (status === "supported" || status === "monotone") {
|
|
1136
|
+
return { trigger: pair === "TRUE_ONLY" || pair === "BOTH" ? "SATISFIED" : "UNDETERMINED", ground: shown };
|
|
1137
|
+
}
|
|
1138
|
+
if (status === "refuted" || status === "unknown") {
|
|
1139
|
+
const want = status === "refuted" ? "FALSE_ONLY" : "NEITHER";
|
|
1140
|
+
return { trigger: pair === want ? "SATISFIED" : "NOT_SATISFIED", ground: shown };
|
|
1141
|
+
}
|
|
1142
|
+
return { trigger: CONJUNCT_STATUS[pair] ?? "UNDETERMINED", ground: shown };
|
|
1143
|
+
};
|
|
1144
|
+
|
|
1145
|
+
// Порт `unify_head`: подстановка головы под ground-цель; переменная дважды — одинаково.
|
|
1146
|
+
const junify = (head: any, target: any): Record<string, any> | null => {
|
|
1147
|
+
if (head?.kind !== "literal" || target?.kind !== "literal") return null;
|
|
1148
|
+
if (head.predicate !== target.predicate) return null;
|
|
1149
|
+
if ((head.polarity ?? "positive") !== (target.polarity ?? "positive")) return null;
|
|
1150
|
+
const h: any[] = head.args ?? [], g: any[] = target.args ?? [];
|
|
1151
|
+
if (h.length !== g.length) return null;
|
|
1152
|
+
const subst: Record<string, any> = {};
|
|
1153
|
+
for (let i = 0; i < h.length; i++) {
|
|
1154
|
+
if (h[i].kind === "var") {
|
|
1155
|
+
if (h[i].var in subst && jcanon(subst[h[i].var]) !== jcanon(g[i])) return null;
|
|
1156
|
+
subst[h[i].var] = g[i];
|
|
1157
|
+
} else if (jcanon(h[i]) !== jcanon(g[i])) return null;
|
|
1158
|
+
}
|
|
1159
|
+
return subst;
|
|
1160
|
+
};
|
|
1161
|
+
|
|
1162
|
+
// DECISION-0036 (б): перечисление судейского конъюнкта по активному домену —
|
|
1163
|
+
// аргументы всех атомов таблиц, порядок §208, кап ДО фильтра по NEITHER.
|
|
1164
|
+
const jenumerate = (M: any, hooks: JHooks, lit: any, subst: Record<string, any>, unbound: string[]): any[] => {
|
|
1165
|
+
const args: any[] = lit.args ?? [];
|
|
1166
|
+
if (args.some((a) => a?.kind === "wildcard")) return [];
|
|
1167
|
+
const domain = new Map<string, any>();
|
|
1168
|
+
for (const pred of Object.keys(M.TABLES)) {
|
|
1169
|
+
for (const row of M.TABLES[pred] as Val[][]) {
|
|
1170
|
+
row.forEach((v, i) => { const t = hooks.cell(pred, i, v); domain.set(jcanon(t), t); });
|
|
1171
|
+
}
|
|
1172
|
+
}
|
|
1173
|
+
const terms = [...domain.keys()].sort().map((k) => domain.get(k));
|
|
1174
|
+
if (terms.length === 0) return [];
|
|
1175
|
+
const names = [...unbound].sort();
|
|
1176
|
+
const out: any[] = [];
|
|
1177
|
+
let n = 0;
|
|
1178
|
+
const total = Math.pow(terms.length, names.length);
|
|
1179
|
+
for (let idx = 0; idx < total && n < JUDGMENT_ENUM_CAP; idx++, n++) {
|
|
1180
|
+
// itertools.product: первая переменная меняется медленнее всех.
|
|
1181
|
+
const full: Record<string, any> = { ...subst };
|
|
1182
|
+
let rest = idx;
|
|
1183
|
+
for (let j = names.length - 1; j >= 0; j--) { full[names[j]] = terms[rest % terms.length]; rest = Math.floor(rest / terms.length); }
|
|
1184
|
+
const ground = { ...lit, args: args.map((a) => (a.kind === "var" ? full[a.var] : a)) };
|
|
1185
|
+
if (jtruth(M, hooks, ground) === "NEITHER") out.push(ground);
|
|
1186
|
+
}
|
|
1187
|
+
return out;
|
|
1188
|
+
};
|
|
1189
|
+
|
|
1190
|
+
const jprio = (M: any): Set<string> => (M.PRIO_ACTIVE ?? M.PRIO ?? new Set<string>());
|
|
1191
|
+
const jrowKey = (hooks: JHooks, lit: any): string => key((lit.args ?? []).map(hooks.val));
|
|
1192
|
+
|
|
1193
|
+
// E-0103: поражён ли был бы вывод поражаемого правила при любом ответе судьи —
|
|
1194
|
+
// дефитер цели уже дал применение, и правило не старше его по §106.
|
|
1195
|
+
const jconclusionDefeated = (M: any, hooks: JHooks, rule: any, target: any): boolean => {
|
|
1196
|
+
if (rule.strength !== "defeasible") return false;
|
|
1197
|
+
const prio = jprio(M), tkey = jrowKey(hooks, target), pos = (target.polarity ?? "positive") !== "negative";
|
|
1198
|
+
for (const a of (M.APPLIED ?? []) as any[]) {
|
|
1199
|
+
if (!a.defeater || a.urn !== target.predicate || a.key !== tkey || a.positive !== pos) continue;
|
|
1200
|
+
if (!prio.has(rule.name + ">" + a.rule)) return true;
|
|
1201
|
+
}
|
|
1202
|
+
return false;
|
|
1203
|
+
};
|
|
1204
|
+
// E-0100: способен ли живой дефитер снять СТОЯЩИЙ вывод — строгая опора не
|
|
1205
|
+
// поражается; каждое уцелевшее поражаемое применение обязано уступать ему.
|
|
1206
|
+
const jdefeaterCanRemove = (M: any, hooks: JHooks, rule: any, target: any): boolean => {
|
|
1207
|
+
const row = (target.args ?? []).map(hooks.val);
|
|
1208
|
+
const bits: number = typeof M.supportBits === "function" ? M.supportBits(target.predicate, row) : 0;
|
|
1209
|
+
const pos = (target.polarity ?? "positive") !== "negative";
|
|
1210
|
+
if ((bits & (pos ? 1 : 2)) !== 0) return false;
|
|
1211
|
+
const tkey = key(row), prio = jprio(M);
|
|
1212
|
+
const apps = ((M.APPLIED ?? []) as any[]).filter((a) => !a.defeater && !a.defeated && a.urn === target.predicate && a.key === tkey && a.positive === pos);
|
|
1213
|
+
if (apps.length === 0) return false;
|
|
1214
|
+
for (const a of apps) if (prio.has(a.rule + ">" + rule.name)) return false;
|
|
1215
|
+
return true;
|
|
1216
|
+
};
|
|
1217
|
+
|
|
1218
|
+
// Порт `whynot._collect_judgments`: общий обход §185 от цели без опоры
|
|
1219
|
+
// (rootDefeaters=false) и от стоящей цели через дефитеры её головы (true).
|
|
1220
|
+
export const collectJudgments = (M: any, hooks: JHooks, goal: any, rootDefeaters: boolean): any[] => {
|
|
1221
|
+
const J: Record<string, any> = M.JUDGMENTS ?? {};
|
|
1222
|
+
const index = new Map<string, any[]>();
|
|
1223
|
+
for (const r of (M.BACKCHAIN ?? []) as any[]) index.set(r.head.predicate, [...(index.get(r.head.predicate) ?? []), r]);
|
|
1224
|
+
const found = new Map<string, any>(), visited = new Set<string>();
|
|
1225
|
+
const now = legalTime();
|
|
1226
|
+
const dead = (reports: [string, any, JReport][]): boolean => {
|
|
1227
|
+
for (const [st, , rep] of reports) {
|
|
1228
|
+
if (rep === null || (rep.trigger !== "NOT_SATISFIED" && rep.trigger !== "CONFLICTED")) continue;
|
|
1229
|
+
// DECISION-0036 (а): refuted-тест над самим отношением суждения при паре NEITHER решает судья.
|
|
1230
|
+
if (st === "refuted" && rep.ground !== null && rep.ground.predicate in J && jtruth(M, hooks, rep.ground) === "NEITHER") continue;
|
|
1231
|
+
return true;
|
|
1232
|
+
}
|
|
1233
|
+
return false;
|
|
1234
|
+
};
|
|
1235
|
+
const walk = (target: any, defeatersOnly: boolean): void => {
|
|
1236
|
+
const vkey = jcanon({ p: target.predicate, a: target.args ?? [], n: target.polarity ?? "positive" });
|
|
1237
|
+
if (visited.has(vkey)) return;
|
|
1238
|
+
visited.add(vkey);
|
|
1239
|
+
for (const r of index.get(target.predicate) ?? []) {
|
|
1240
|
+
if (defeatersOnly && r.strength !== "defeater") continue;
|
|
1241
|
+
const subst = junify(r.head, target);
|
|
1242
|
+
if (subst === null) continue;
|
|
1243
|
+
const w = r.window;
|
|
1244
|
+
if (w && ((w.start && now < w.start) || (w.end && now >= w.end))) continue;
|
|
1245
|
+
const reports: [string, any, JReport][] = (r.body as [string, any][]).map(([st, lit]) => [st, lit, jconjunct(M, hooks, st, lit, subst)]);
|
|
1246
|
+
if (dead(reports)) continue;
|
|
1247
|
+
if (jconclusionDefeated(M, hooks, r, target)) continue;
|
|
1248
|
+
if (defeatersOnly && !jdefeaterCanRemove(M, hooks, r, target)) continue;
|
|
1249
|
+
for (const [st, lit, rep] of reports) {
|
|
1250
|
+
if (rep === null) {
|
|
1251
|
+
if (JUDGMENT_TESTABLE.has(st) && lit?.kind === "literal" && lit.predicate in J) {
|
|
1252
|
+
const vars = new Set<string>(); jvars(lit.args ?? [], vars);
|
|
1253
|
+
const unbound = [...vars].filter((v) => !(v in subst));
|
|
1254
|
+
for (const g of jenumerate(M, hooks, lit, subst, unbound)) { const k = requestKey(g); if (!found.has(k)) found.set(k, g); }
|
|
1255
|
+
}
|
|
1256
|
+
continue;
|
|
1257
|
+
}
|
|
1258
|
+
const ground = rep.ground;
|
|
1259
|
+
if (ground === null || ground.kind !== "literal" || (ground.args ?? []).some((a: any) => a?.kind === "wildcard")) continue;
|
|
1260
|
+
if (rep.trigger === "UNDETERMINED") {
|
|
1261
|
+
if (ground.predicate in J) { const k = requestKey(ground); if (!found.has(k)) found.set(k, ground); }
|
|
1262
|
+
else walk(ground, false);
|
|
1263
|
+
} else if (st === "refuted" && rep.trigger === "NOT_SATISFIED" && ground.predicate in J && jtruth(M, hooks, ground) === "NEITHER") {
|
|
1264
|
+
const k = requestKey(ground); if (!found.has(k)) found.set(k, ground);
|
|
1265
|
+
}
|
|
1266
|
+
}
|
|
1267
|
+
}
|
|
1268
|
+
};
|
|
1269
|
+
walk(goal, rootDefeaters);
|
|
1270
|
+
return [...found.values()];
|
|
1271
|
+
};
|
|
1272
|
+
|
|
1273
|
+
const nlitToLiteral = (hooks: JHooks, n: any): any =>
|
|
1274
|
+
({ kind: "literal", polarity: n.positive ? "positive" : "negative", predicate: n.pred, args: (n.row as Val[]).map((v, i) => hooks.cell(n.pred, i, v)) });
|
|
1275
|
+
const nlitStatus = (M: any, n: any): string => { const st: string = M.status(n.pred, n.row); return n.positive ? st : JMIRROR[st]; };
|
|
1276
|
+
|
|
1277
|
+
// E-0100, носитель `valid_when` §127: судьи действительности осуществлённого
|
|
1278
|
+
// полномочия, чей эффект есть цель; E-0120 — не при эффекте, запертом иммунитетом.
|
|
1279
|
+
export const powerJudgments = (M: any, hooks: JHooks, goal: any): any[] => {
|
|
1280
|
+
const J: Record<string, any> = M.JUDGMENTS ?? {};
|
|
1281
|
+
if ((goal.polarity ?? "positive") !== "positive" || !M.POSITIONS) return [];
|
|
1282
|
+
const gkey = goal.predicate + "" + jrowKey(hooks, goal);
|
|
1283
|
+
if (M.BLOCKED_IMMUNITY && M.BLOCKED_IMMUNITY.has(gkey)) return [];
|
|
1284
|
+
const out: any[] = [];
|
|
1285
|
+
for (const p of M.POSITIONS as any[]) {
|
|
1286
|
+
if (p.modality !== "power" || p.status !== "ACTIVE") continue;
|
|
1287
|
+
const eff = p.spec.effect;
|
|
1288
|
+
if (!eff || !eff.positive || eff.pred !== goal.predicate || key(eff.row) !== jrowKey(hooks, goal)) continue;
|
|
1289
|
+
if (!p.spec.exercise || !["TRUE_ONLY", "BOTH"].includes(nlitStatus(M, p.spec.exercise))) continue;
|
|
1290
|
+
const vw = p.spec.validWhen;
|
|
1291
|
+
if (!vw || !(vw.pred in J)) continue;
|
|
1292
|
+
if (nlitStatus(M, vw) === "NEITHER") out.push(nlitToLiteral(hooks, vw));
|
|
1293
|
+
}
|
|
1294
|
+
return orderRequests(out);
|
|
1295
|
+
};
|
|
1296
|
+
// E-0100, носитель `condition` цели §123/§124: судьи статуса позиции обязанности.
|
|
1297
|
+
export const positionJudgments = (M: any, hooks: JHooks): any[] => {
|
|
1298
|
+
const J: Record<string, any> = M.JUDGMENTS ?? {};
|
|
1299
|
+
if (!M.POSITIONS) return [];
|
|
1300
|
+
const out: any[] = [];
|
|
1301
|
+
for (const p of M.POSITIONS as any[]) {
|
|
1302
|
+
if (p.status !== "ACTIVE" && p.status !== "UNDETERMINED") continue;
|
|
1303
|
+
const cond = p.spec.cond;
|
|
1304
|
+
if (!cond || !(cond.pred in J)) continue;
|
|
1305
|
+
if (nlitStatus(M, cond) === "NEITHER") out.push(nlitToLiteral(hooks, cond));
|
|
1306
|
+
}
|
|
1307
|
+
return orderRequests(out);
|
|
1308
|
+
};
|
|
1309
|
+
|
|
1310
|
+
// JudgmentRequest схемы evaluation по упорядоченным блокерам (без proof-узла).
|
|
1311
|
+
export const buildRequests = (M: any, blockers: any[], tail: string): any[] =>
|
|
1312
|
+
blockers.map((b, i) => {
|
|
1313
|
+
const d = M.JUDGMENTS[b.predicate] ?? {};
|
|
1314
|
+
const r: any = { id: `urn:judgment:${tail}:${i}`, predicate: b.predicate, authority: d.authority ?? b.predicate,
|
|
1315
|
+
request: { relation: b.predicate, polarity: b.polarity ?? "positive", args: b.args ?? [] } };
|
|
1316
|
+
if (d.requestSchema !== undefined) r.requestSchema = d.requestSchema;
|
|
1317
|
+
return r;
|
|
1318
|
+
});
|
|
1319
|
+
|
|
1320
|
+
// Порт ветвей `queries.query_truth`: цель — отношение суждения без опоры;
|
|
1321
|
+
// цель без опоры — тела правил и `valid_when`; стоящая цель — дефитеры головы.
|
|
1322
|
+
export const judgmentBlockers = (M: any, hooks: JHooks, goal: any, status: string, conflict: boolean): any[] => {
|
|
1323
|
+
const J: Record<string, any> = M.JUDGMENTS ?? {};
|
|
1324
|
+
if (Object.keys(J).length === 0) return [];
|
|
1325
|
+
const unresolved = status === "NEITHER" && !conflict;
|
|
1326
|
+
if (goal.predicate in J && unresolved) return [goal];
|
|
1327
|
+
if (unresolved) return orderRequests([...collectJudgments(M, hooks, goal, false), ...powerJudgments(M, hooks, goal)]);
|
|
1328
|
+
if (status === "TRUE_ONLY" && !conflict) return orderRequests(collectJudgments(M, hooks, goal, true));
|
|
1329
|
+
return [];
|
|
1330
|
+
};
|
|
1331
|
+
|
|
1332
|
+
// §47.3.1 (E-0089): ответ судьи с объектом `judgment` — орган и форма payload.
|
|
1333
|
+
// Возвращает код issue либо null (ответ принят).
|
|
1334
|
+
const JKINDS: Record<string, string> = {
|
|
1335
|
+
"urn:law:std#Boolean": "boolean", "urn:law:std#Decimal": "decimal", "urn:law:std#Text": "text", "urn:law:std#Date": "date",
|
|
1336
|
+
"urn:law:std#Instant": "instant", "urn:law:std#Integer": "integer", "urn:law:std#Natural": "integer",
|
|
1337
|
+
"urn:law:std#Money": "money", "urn:law:std#Quantity": "quantity", "urn:law:std#Rational": "rational",
|
|
1338
|
+
};
|
|
1339
|
+
export const judgmentAnswer = (M: any, assertion: any): string | null => {
|
|
1340
|
+
const judgment = assertion.judgment;
|
|
1341
|
+
if (judgment === null || typeof judgment !== "object") return null;
|
|
1342
|
+
const pred = assertion.literal?.predicate;
|
|
1343
|
+
const d = (M.JUDGMENTS ?? {})[pred] ?? {};
|
|
1344
|
+
if (judgment.authority !== (d.authority ?? pred)) return "JUDGMENT_AUTHORITY_MISMATCH";
|
|
1345
|
+
const payload = judgment.payload;
|
|
1346
|
+
if (payload !== undefined && payload !== null) {
|
|
1347
|
+
const schema = d.requestSchema;
|
|
1348
|
+
if (schema === undefined || schema === null) return "JUDGMENT_PAYLOAD_MISMATCH";
|
|
1349
|
+
const want = JKINDS[String(schema.name)];
|
|
1350
|
+
if (want !== undefined) {
|
|
1351
|
+
const got = payload.kind === "value" ? JKINDS[String(payload.type?.name)] : payload.kind === "entity_ref" ? "entity_ref" : "enum";
|
|
1352
|
+
if (got !== want) return "JUDGMENT_PAYLOAD_MISMATCH";
|
|
1353
|
+
}
|
|
1354
|
+
}
|
|
1355
|
+
return null;
|
|
1356
|
+
};
|
|
1357
|
+
|
|
1358
|
+
export const admitNumericInputs = (kase: any, query: any, judgments = false): void => {
|
|
1359
|
+
const stack = [query, ...(kase.assertions ?? []).map((a: any) => a.literal ?? {})];
|
|
1360
|
+
while (stack.length) {
|
|
1361
|
+
const n = stack.pop();
|
|
1362
|
+
if (Array.isArray(n)) stack.push(...n);
|
|
1363
|
+
else if (n !== null && typeof n === "object") {
|
|
1364
|
+
if (n.kind === "value" && n.type?.name === "urn:law:std#Number") throw new Refused("NUMBER_UNAVAILABLE: exact Number and slot narrowing (E-0316/E-0319/E-0334) are outside this slice");
|
|
1365
|
+
if (judgments && ["aggregate", "comprehension"].includes(n.kind)) throw new Refused("AGGREGATE_JUDGMENT_UNAVAILABLE: open aggregate members require E-0357");
|
|
1366
|
+
stack.push(...Object.values(n).filter(v => v !== null && typeof v === "object"));
|
|
1367
|
+
}
|
|
1368
|
+
}
|
|
1369
|
+
};
|