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,602 @@
|
|
|
1
|
+
"""Bounded, acyclic L0 -> scalar spreadsheet formulas.
|
|
2
|
+
|
|
3
|
+
This is a separate execution profile, not an alternative interpretation of
|
|
4
|
+
the common plan. Rows are finite slots; integers encode decimal coefficients.
|
|
5
|
+
Unsupported structure refuses before export; out-of-domain edits refuse in
|
|
6
|
+
the workbook. The XLSX emitter only serializes this cell plan.
|
|
7
|
+
"""
|
|
8
|
+
from __future__ import annotations
|
|
9
|
+
|
|
10
|
+
import datetime as dt
|
|
11
|
+
import itertools
|
|
12
|
+
import json
|
|
13
|
+
from dataclasses import dataclass
|
|
14
|
+
from decimal import Decimal, InvalidOperation
|
|
15
|
+
|
|
16
|
+
import plan as P
|
|
17
|
+
|
|
18
|
+
PROFILE = "excel-calculator/0.1"
|
|
19
|
+
FORMAT = "law.excel-plan/1"
|
|
20
|
+
LIMIT = 10 ** 12
|
|
21
|
+
MAX_SLOTS = 2000
|
|
22
|
+
MAX_CELLS = 100000
|
|
23
|
+
MAX_SCALE = 12
|
|
24
|
+
STD = P.STD
|
|
25
|
+
KINDS = {STD + "Text": "text", STD + "Integer": "dec", STD + "Decimal": "dec",
|
|
26
|
+
STD + "Money": "money", STD + "Date": "date"}
|
|
27
|
+
EPOCH = dt.date(1899, 12, 30)
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
def refuse(message):
|
|
31
|
+
raise P.Refused(f"{PROFILE}: {message}")
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
def fields(obj, allowed, where):
|
|
35
|
+
if not isinstance(obj, dict):
|
|
36
|
+
refuse(f"{where}: ожидается объект")
|
|
37
|
+
unknown = set(obj) - set(allowed)
|
|
38
|
+
if unknown:
|
|
39
|
+
refuse(f"{where}: неизвестные поля {', '.join(sorted(unknown))}")
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
def col(i):
|
|
43
|
+
out = ""
|
|
44
|
+
while i:
|
|
45
|
+
i, n = divmod(i - 1, 26)
|
|
46
|
+
out = chr(65 + n) + out
|
|
47
|
+
return out
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
def quoted(value):
|
|
51
|
+
return '"' + str(value).replace('"', '""') + '"'
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
def conjunction(items):
|
|
55
|
+
items = list(items)
|
|
56
|
+
if not items:
|
|
57
|
+
return "TRUE()"
|
|
58
|
+
# Excel permits 255 function arguments. Binary nesting is unnecessary.
|
|
59
|
+
while len(items) > 100:
|
|
60
|
+
items = ["AND(" + ",".join(items[i:i + 100]) + ")" for i in range(0, len(items), 100)]
|
|
61
|
+
return items[0] if len(items) == 1 else "AND(" + ",".join(items) + ")"
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
def date_number(value):
|
|
65
|
+
try:
|
|
66
|
+
day = dt.date.fromisoformat(value)
|
|
67
|
+
except (ValueError, TypeError):
|
|
68
|
+
refuse(f"неверная дата {value!r}")
|
|
69
|
+
if day < dt.date(1900, 3, 1):
|
|
70
|
+
refuse("даты до 1900-03-01 вне профиля (Excel 1900 date system)")
|
|
71
|
+
return (day - EPOCH).days
|
|
72
|
+
|
|
73
|
+
|
|
74
|
+
def decimal_parts(value):
|
|
75
|
+
try:
|
|
76
|
+
d = Decimal(str(value))
|
|
77
|
+
if not d.is_finite() or abs(d) > LIMIT:
|
|
78
|
+
refuse(f"число вне диапазона: {value!r}")
|
|
79
|
+
sign, digits, exponent = d.as_tuple()
|
|
80
|
+
if exponent < -MAX_SCALE or exponent > MAX_SCALE:
|
|
81
|
+
refuse(f"масштаб числа вне профиля: {value!r}")
|
|
82
|
+
u, s = int(''.join(map(str, digits))) * (-1 if sign else 1), max(0, -exponent)
|
|
83
|
+
u *= 10 ** max(0, exponent)
|
|
84
|
+
while s and u % 10 == 0:
|
|
85
|
+
u //= 10
|
|
86
|
+
s -= 1
|
|
87
|
+
if abs(u) > LIMIT:
|
|
88
|
+
refuse(f"коэффициент числа вне ±{LIMIT}: {value!r}")
|
|
89
|
+
return u, s
|
|
90
|
+
except (InvalidOperation, ValueError):
|
|
91
|
+
refuse(f"неверное число {value!r}")
|
|
92
|
+
|
|
93
|
+
|
|
94
|
+
@dataclass
|
|
95
|
+
class Value:
|
|
96
|
+
ref: str
|
|
97
|
+
kind: str
|
|
98
|
+
scale: int = 0
|
|
99
|
+
currency: str | None = None
|
|
100
|
+
|
|
101
|
+
|
|
102
|
+
@dataclass
|
|
103
|
+
class Row:
|
|
104
|
+
active: str
|
|
105
|
+
args: list[Value]
|
|
106
|
+
source: str
|
|
107
|
+
|
|
108
|
+
|
|
109
|
+
class Planner:
|
|
110
|
+
def __init__(self, plan, config=None):
|
|
111
|
+
self.plan = plan
|
|
112
|
+
if config is not None and not isinstance(config, dict):
|
|
113
|
+
refuse("конфигурация должна быть объектом")
|
|
114
|
+
self.config = dict(config or {})
|
|
115
|
+
self.sheets = {n: {"name": n, "cells": [], "validations": [], "warnings": []}
|
|
116
|
+
for n in ("Results", "Inputs", "Calculation", "Model")}
|
|
117
|
+
self.mapping = {"format": "law.excel-cells/1", "profile": PROFILE,
|
|
118
|
+
"status": "Results!B3", "legalTime": "Inputs!B4", "inputs": {}, "tables": {}}
|
|
119
|
+
self.nr = {n: 7 for n in self.sheets}
|
|
120
|
+
self.cells = 0
|
|
121
|
+
self.slots = 0
|
|
122
|
+
self.rows = {}
|
|
123
|
+
self.input_checks = []
|
|
124
|
+
self.numeric_currency = self.config.get("currency")
|
|
125
|
+
|
|
126
|
+
def put(self, sheet, row, column, value=None, formula=None, style="body"):
|
|
127
|
+
self.cells += 1
|
|
128
|
+
if self.cells > MAX_CELLS:
|
|
129
|
+
refuse(f"более {MAX_CELLS} ячеек; уменьшите число входных строк")
|
|
130
|
+
if formula and len(formula.encode("utf-16-le")) // 2 > 8000:
|
|
131
|
+
refuse("формула длиннее 8000 символов; уменьшите число строк")
|
|
132
|
+
for text in (value, formula):
|
|
133
|
+
if isinstance(text, str) and (len(text) > 30000 or any(
|
|
134
|
+
(ord(c) < 32 and c not in '\t\n\r') or 0xD800 <= ord(c) <= 0xDFFF or ord(c) in (0xFFFE, 0xFFFF) for c in text)):
|
|
135
|
+
refuse("текст ячейки слишком длинный либо содержит запрещённые XML-символы")
|
|
136
|
+
address = f"{col(column)}{row}"
|
|
137
|
+
self.sheets[sheet]["cells"].append({"cell": address, "value": value, "formula": formula, "style": style})
|
|
138
|
+
return f"'{sheet}'!{address}"
|
|
139
|
+
|
|
140
|
+
def helper(self, expression, label, gate="TRUE()", bounded=False):
|
|
141
|
+
n = self.nr["Calculation"]
|
|
142
|
+
self.nr["Calculation"] += 1
|
|
143
|
+
self.put("Calculation", n, 1, label)
|
|
144
|
+
raw = self.put("Calculation", n, 2, formula=f'IF({gate},{expression},0)')
|
|
145
|
+
checked = self.put("Calculation", n, 3,
|
|
146
|
+
formula=f'IF(ABS({raw})<={LIMIT},{raw},NA())' if bounded else raw)
|
|
147
|
+
self.put("Calculation", n, 4, formula=f'IF(ISERROR({checked}),1,0)')
|
|
148
|
+
return checked
|
|
149
|
+
|
|
150
|
+
def warning(self, sheet, reference):
|
|
151
|
+
cell = reference.rsplit("!", 1)[-1]
|
|
152
|
+
formula = 'OR(' + ','.join(f'{cell}={quoted(status)}' for status in
|
|
153
|
+
("INVALID_INPUT", "CALCULATION_REFUSED", "KEY_CONFLICT")) + ')'
|
|
154
|
+
self.sheets[sheet]["warnings"].append({"sqref": cell, "formula": formula})
|
|
155
|
+
|
|
156
|
+
def number(self, expr, scale, kind="dec", currency=None, gate="TRUE()", label="Число"):
|
|
157
|
+
if not 0 <= scale <= MAX_SCALE:
|
|
158
|
+
refuse(f"масштаб промежуточного значения {scale} вне 0..{MAX_SCALE}")
|
|
159
|
+
return Value(self.helper(expr, label, gate, bounded=True), kind, scale, currency)
|
|
160
|
+
|
|
161
|
+
def rescale(self, value, scale, gate):
|
|
162
|
+
delta = scale - value.scale
|
|
163
|
+
if delta == 0:
|
|
164
|
+
return value
|
|
165
|
+
if delta > 0:
|
|
166
|
+
expression = f'{value.ref}*{10 ** delta}'
|
|
167
|
+
else:
|
|
168
|
+
d = 10 ** -delta
|
|
169
|
+
expression = f'IF(MOD({value.ref},{d})=0,ROUND({value.ref}/{d},0),NA())'
|
|
170
|
+
return self.number(expression, scale, value.kind, value.currency, gate, "Точный масштаб")
|
|
171
|
+
|
|
172
|
+
def same_kind(self, a, b):
|
|
173
|
+
if a.kind != b.kind or a.currency != b.currency:
|
|
174
|
+
refuse("разные виды или валюты в операции")
|
|
175
|
+
|
|
176
|
+
def compare(self, a, b, op, gate):
|
|
177
|
+
self.same_kind(a, b)
|
|
178
|
+
if a.kind == "text":
|
|
179
|
+
if op not in ("eq", "ne"):
|
|
180
|
+
refuse("порядок на Text вне профиля")
|
|
181
|
+
expr = f'EXACT({a.ref},{b.ref})'
|
|
182
|
+
return expr if op == "eq" else f'NOT({expr})'
|
|
183
|
+
if a.kind in ("money", "dec"):
|
|
184
|
+
s = max(a.scale, b.scale)
|
|
185
|
+
a, b = self.rescale(a, s, gate), self.rescale(b, s, gate)
|
|
186
|
+
operator = {"eq": "=", "ne": "<>", "lt": "<", "le": "<=", "gt": ">", "ge": ">="}.get(op)
|
|
187
|
+
if not operator:
|
|
188
|
+
refuse(f"неизвестное сравнение {op}")
|
|
189
|
+
return f'{a.ref}{operator}{b.ref}'
|
|
190
|
+
|
|
191
|
+
def term(self, term, env, gate):
|
|
192
|
+
k = term.get("k")
|
|
193
|
+
allowed = {"var": {"k", "row", "col", "t"}, "lit": {"k", "t", "v", "cur"},
|
|
194
|
+
"bin": {"k", "op", "l", "r"}, "call": {"k", "fn", "args"}}
|
|
195
|
+
if k not in allowed:
|
|
196
|
+
refuse(f"терм {k} вне профиля")
|
|
197
|
+
fields(term, allowed[k], "терм")
|
|
198
|
+
if k == "var":
|
|
199
|
+
try:
|
|
200
|
+
v = env[term["row"]].args[term["col"]]
|
|
201
|
+
except (IndexError, KeyError):
|
|
202
|
+
refuse("ссылка переменной вне аргументов тела")
|
|
203
|
+
if v.kind != term["t"]:
|
|
204
|
+
refuse("вид переменной не совпадает с колонкой")
|
|
205
|
+
return v
|
|
206
|
+
if k == "lit":
|
|
207
|
+
kind = term["t"]
|
|
208
|
+
if kind in ("dec", "money"):
|
|
209
|
+
u, s = decimal_parts(term["v"])
|
|
210
|
+
cur = term.get("cur")
|
|
211
|
+
if kind == "money" and cur != self.numeric_currency:
|
|
212
|
+
refuse("валюта литерала не совпадает с currency конфигурации")
|
|
213
|
+
return Value(str(u), kind, s, cur)
|
|
214
|
+
if kind == "date":
|
|
215
|
+
return Value(str(date_number(term["v"])), kind)
|
|
216
|
+
if kind == "text":
|
|
217
|
+
return Value(quoted(term["v"]), kind)
|
|
218
|
+
refuse(f"литерал {kind} вне профиля")
|
|
219
|
+
if k == "bin":
|
|
220
|
+
a, b = self.term(term["l"], env, gate), self.term(term["r"], env, gate)
|
|
221
|
+
if a.kind not in ("dec", "money") or b.kind not in ("dec", "money"):
|
|
222
|
+
refuse("арифметика над нечисловыми значениями")
|
|
223
|
+
op = term["op"]
|
|
224
|
+
if op in ("add", "sub"):
|
|
225
|
+
self.same_kind(a, b)
|
|
226
|
+
scale = max(a.scale, b.scale)
|
|
227
|
+
a, b = self.rescale(a, scale, gate), self.rescale(b, scale, gate)
|
|
228
|
+
return self.number(f'{a.ref}{"+" if op == "add" else "-"}{b.ref}', scale,
|
|
229
|
+
a.kind, a.currency, gate, op)
|
|
230
|
+
if op == "mul":
|
|
231
|
+
if a.kind == b.kind == "money":
|
|
232
|
+
refuse("произведение двух Money вне профиля")
|
|
233
|
+
like = a if a.kind == "money" else b
|
|
234
|
+
return self.number(f'{a.ref}*{b.ref}', a.scale + b.scale, like.kind, like.currency, gate, "mul")
|
|
235
|
+
if op == "div":
|
|
236
|
+
if b.kind == "money":
|
|
237
|
+
self.same_kind(a, b)
|
|
238
|
+
scale = max(6, a.scale - b.scale)
|
|
239
|
+
numerator = self.number(f'{a.ref}*{10 ** (scale + b.scale - a.scale)}', 0,
|
|
240
|
+
gate=gate, label="Числитель деления")
|
|
241
|
+
expr = f'IF({b.ref}=0,NA(),IF(MOD({numerator.ref},ABS({b.ref}))=0,ROUND({numerator.ref}/{b.ref},0),NA()))'
|
|
242
|
+
return self.number(expr, scale, "money" if a.kind == "money" and b.kind != "money" else "dec",
|
|
243
|
+
a.currency if b.kind != "money" else None, gate, "Точное деление")
|
|
244
|
+
if op == "mod":
|
|
245
|
+
self.same_kind(a, b)
|
|
246
|
+
if a.kind != "dec":
|
|
247
|
+
refuse("mod требует Integer")
|
|
248
|
+
a, b = self.rescale(a, 0, gate), self.rescale(b, 0, gate)
|
|
249
|
+
return self.number(f'IF({b.ref}=0,NA(),MOD({a.ref},ABS({b.ref})))', 0, gate=gate, label="mod")
|
|
250
|
+
refuse(f"операция {op} вне профиля")
|
|
251
|
+
fn, args = term["fn"], term["args"]
|
|
252
|
+
if fn == "round":
|
|
253
|
+
if len(args) != 3 or args[1].get("k") != "lit" or args[2] != {"k": "lit", "t": "text", "v": "HALF_UP"}:
|
|
254
|
+
refuse("round требует литерал разрядности и режим HALF_UP")
|
|
255
|
+
n, ns = decimal_parts(args[1]["v"])
|
|
256
|
+
if ns or not 0 <= n <= 6:
|
|
257
|
+
refuse("round: разрядность вне 0..6")
|
|
258
|
+
a = self.term(args[0], env, gate)
|
|
259
|
+
if a.kind not in ("dec", "money"):
|
|
260
|
+
refuse("round требует числовое значение")
|
|
261
|
+
if n >= a.scale:
|
|
262
|
+
return self.rescale(a, n, gate)
|
|
263
|
+
drop = 10 ** (a.scale - n)
|
|
264
|
+
# Integer remainder determines HALF_UP, including negative ties.
|
|
265
|
+
q = self.helper(f'QUOTIENT(ABS({a.ref}),{drop})', "Целая часть округления", gate)
|
|
266
|
+
expr = f'SIGN({a.ref})*({q}+IF(MOD(ABS({a.ref}),{drop})*2>={drop},1,0))'
|
|
267
|
+
return self.number(expr, n, a.kind, a.currency, gate, "HALF_UP")
|
|
268
|
+
vals = [self.term(a, env, gate) for a in args]
|
|
269
|
+
if fn == "abs" and len(vals) == 1 and vals[0].kind in ("dec", "money"):
|
|
270
|
+
a = vals[0]
|
|
271
|
+
return self.number(f'ABS({a.ref})', a.scale, a.kind, a.currency, gate, fn)
|
|
272
|
+
if fn in ("min", "max") and len(vals) == 2:
|
|
273
|
+
a, b = vals
|
|
274
|
+
self.same_kind(a, b)
|
|
275
|
+
if a.kind not in ("dec", "money"):
|
|
276
|
+
refuse(f"{fn} требует числовые значения")
|
|
277
|
+
s = max(a.scale, b.scale)
|
|
278
|
+
a, b = self.rescale(a, s, gate), self.rescale(b, s, gate)
|
|
279
|
+
return self.number(f'{fn.upper()}({a.ref},{b.ref})', s, a.kind, a.currency, gate, fn)
|
|
280
|
+
refuse(f"функция {fn} вне профиля")
|
|
281
|
+
|
|
282
|
+
def validate(self):
|
|
283
|
+
p, c = self.plan, self.config
|
|
284
|
+
fields(p, {"planVersion", "package", "world", "tables", "rules", "facts", "excluded", "consts", "bases", "api"}, "план")
|
|
285
|
+
if p.get("planVersion") != P.PLAN_VERSION:
|
|
286
|
+
refuse(f"поддержан только {P.PLAN_VERSION}")
|
|
287
|
+
fields(c, {"profile", "currency", "legalTime", "tables", "semanticHash"}, "конфигурация")
|
|
288
|
+
if c.get("profile", PROFILE) != PROFILE:
|
|
289
|
+
refuse("неизвестная версия конфигурации")
|
|
290
|
+
if self.numeric_currency is not None and (not isinstance(self.numeric_currency, str) or len(self.numeric_currency) != 3):
|
|
291
|
+
refuse("currency: требуется трёхбуквенный код")
|
|
292
|
+
self.tables = {t["pred"]: t for t in p["tables"]}
|
|
293
|
+
if len(self.tables) != len(p["tables"]):
|
|
294
|
+
refuse("повтор предиката")
|
|
295
|
+
settings = c.get("tables", {})
|
|
296
|
+
fields(settings, self.tables, "tables конфигурации")
|
|
297
|
+
self.settings = {}
|
|
298
|
+
heads = {r["head"]["pred"] for r in p["rules"]}
|
|
299
|
+
facts = {f["pred"] for f in p["facts"]}
|
|
300
|
+
for pred, t in self.tables.items():
|
|
301
|
+
fields(t, {"pred", "name", "arity", "params", "keys", "kinds"}, pred)
|
|
302
|
+
if not 0 <= t["arity"] <= 10 or len(t["params"]) != t["arity"] or len(t["kinds"]) != t["arity"]:
|
|
303
|
+
refuse(f"{pred}: арность вне 0..10 или не совпадает с колонками")
|
|
304
|
+
for typ, kind in zip(t["params"], t["kinds"]):
|
|
305
|
+
if KINDS.get(typ) != kind:
|
|
306
|
+
refuse(f"{pred}: тип {typ} вне профиля")
|
|
307
|
+
if kind == "money" and not self.numeric_currency:
|
|
308
|
+
refuse("Money требует явного currency конфигурации")
|
|
309
|
+
fields(settings.get(pred, {}), {"rows", "values", "scales", "columns", "label"}, pred + " config")
|
|
310
|
+
s = dict(settings.get(pred, {}))
|
|
311
|
+
fields(s, {"rows", "values", "scales", "columns", "label"}, pred + " config")
|
|
312
|
+
capacity = s.get("rows", 0 if pred in heads | facts else 1)
|
|
313
|
+
if type(capacity) is not int or not 0 <= capacity <= 100:
|
|
314
|
+
refuse(f"{pred}: rows вне 0..100")
|
|
315
|
+
if pred == P.ALWAYS and capacity:
|
|
316
|
+
refuse("служебное отношение always не является входом")
|
|
317
|
+
scales = s.get("scales", [0 if typ != STD + "Decimal" and typ != STD + "Money" else
|
|
318
|
+
2 if typ == STD + "Money" else 4 for typ in t["params"]])
|
|
319
|
+
if not isinstance(scales, list) or len(scales) != t["arity"] or any(type(n) is not int or not 0 <= n <= 6 for n in scales):
|
|
320
|
+
refuse(f"{pred}: scales — по одному целому 0..6 на колонку")
|
|
321
|
+
for typ, scale in zip(t["params"], scales):
|
|
322
|
+
if typ not in (STD + "Decimal", STD + "Money") and scale:
|
|
323
|
+
refuse(f"{pred}: масштаб допустим только для Decimal/Money")
|
|
324
|
+
columns = s.get("columns", [f'Аргумент {i + 1} ({typ.split("#")[-1]})' for i, typ in enumerate(t["params"])])
|
|
325
|
+
values = s.get("values", [])
|
|
326
|
+
if not isinstance(columns, list) or any(not isinstance(x, str) for x in columns) or \
|
|
327
|
+
not isinstance(values, list) or len(columns) != t["arity"] or len(values) > capacity or \
|
|
328
|
+
any(not isinstance(r, list) or len(r) != t["arity"] for r in values):
|
|
329
|
+
refuse(f"{pred}: columns/values не совпадают с арностью или rows")
|
|
330
|
+
if not isinstance(s.get("label", t["name"]), str):
|
|
331
|
+
refuse(f"{pred}: label должна быть строкой")
|
|
332
|
+
self.settings[pred] = dict(s, rows=capacity, scales=scales, columns=columns, values=values,
|
|
333
|
+
label=s.get("label", t["name"]))
|
|
334
|
+
deps = {p: set() for p in self.tables}
|
|
335
|
+
for r in p["rules"]:
|
|
336
|
+
fields(r, {"name", "id", "window", "atoms", "joins", "filters", "guards", "head"}, "правило")
|
|
337
|
+
fields(r["head"], {"pred", "args"}, "голова")
|
|
338
|
+
if r["head"]["pred"] not in deps:
|
|
339
|
+
refuse("необъявленная голова правила")
|
|
340
|
+
for a in r["atoms"]:
|
|
341
|
+
fields(a, {"pred"}, "атом")
|
|
342
|
+
if a["pred"] not in deps:
|
|
343
|
+
refuse("необъявленный предикат тела")
|
|
344
|
+
deps[r["head"]["pred"]].add(a["pred"])
|
|
345
|
+
if len(r["head"]["args"]) != self.tables[r["head"]["pred"]]["arity"]:
|
|
346
|
+
refuse("арность головы не совпадает с объявлением")
|
|
347
|
+
if r["window"]:
|
|
348
|
+
fields(r["window"], {"start", "end"}, "окно")
|
|
349
|
+
for v in r["window"].values():
|
|
350
|
+
if v is not None:
|
|
351
|
+
date_number(v)
|
|
352
|
+
self.order = []
|
|
353
|
+
while deps:
|
|
354
|
+
ready = sorted(k for k, reads in deps.items() if not reads)
|
|
355
|
+
if not ready:
|
|
356
|
+
refuse("рекурсивные зависимости предикатов вне профиля")
|
|
357
|
+
self.order.extend(ready)
|
|
358
|
+
deps = {k: reads - set(ready) for k, reads in deps.items() if k not in ready}
|
|
359
|
+
# Reject unsupported terms even in rules with no potential rows.
|
|
360
|
+
for r in p["rules"]:
|
|
361
|
+
dummy = [Row("FALSE()", [Value("0", k, self.settings[a["pred"]]["scales"][i],
|
|
362
|
+
self.numeric_currency if k == "money" else None)
|
|
363
|
+
for i, k in enumerate(self.tables[a["pred"]]["kinds"])], "") for a in r["atoms"]]
|
|
364
|
+
args = [self.term(term, dummy, "FALSE()") for term in r["head"]["args"]]
|
|
365
|
+
self.normalize(r["head"]["pred"], args, "FALSE()", "проверка головы")
|
|
366
|
+
for g in r["guards"]:
|
|
367
|
+
fields(g, {"op", "l", "r"}, "гард")
|
|
368
|
+
self.compare(self.term(g["l"], dummy, "FALSE()"), self.term(g["r"], dummy, "FALSE()"), g["op"], "FALSE()")
|
|
369
|
+
for f in r["filters"]:
|
|
370
|
+
fields(f, {"row", "col", "t", "term"}, "фильтр")
|
|
371
|
+
if f.get("t") != dummy[f["row"]].args[f["col"]].kind:
|
|
372
|
+
refuse("вид фильтра не совпадает с колонкой")
|
|
373
|
+
self.compare(dummy[f["row"]].args[f["col"]], self.term(f["term"], dummy, "FALSE()"), "eq", "FALSE()")
|
|
374
|
+
for fact in p["facts"]:
|
|
375
|
+
fields(fact, {"pred", "args"}, "факт")
|
|
376
|
+
if fact["pred"] not in self.tables:
|
|
377
|
+
refuse("факт необъявленного предиката")
|
|
378
|
+
self.normalize(fact["pred"], [self.term(a, [], "FALSE()") for a in fact["args"]], "FALSE()", "проверка факта")
|
|
379
|
+
# Validation must not leave unreachable calculation cells behind.
|
|
380
|
+
self.sheets["Calculation"]["cells"].clear()
|
|
381
|
+
self.nr["Calculation"] = 7
|
|
382
|
+
self.cells = 0
|
|
383
|
+
|
|
384
|
+
def input_value(self, value, kind, scale):
|
|
385
|
+
if value is None:
|
|
386
|
+
return None
|
|
387
|
+
if kind == "text":
|
|
388
|
+
if not isinstance(value, str):
|
|
389
|
+
refuse("Text: начальное значение не строка")
|
|
390
|
+
return value
|
|
391
|
+
if kind == "date":
|
|
392
|
+
return date_number(value)
|
|
393
|
+
u, s = decimal_parts(value)
|
|
394
|
+
if s > scale or abs(u) * 10 ** (scale - s) > LIMIT:
|
|
395
|
+
refuse("начальное значение вне scales/диапазона входной колонки")
|
|
396
|
+
return float(Decimal(str(value))) if scale else int(Decimal(str(value)))
|
|
397
|
+
|
|
398
|
+
def input_rows(self, pred):
|
|
399
|
+
t, s = self.tables[pred], self.settings[pred]
|
|
400
|
+
result, mapped = [], []
|
|
401
|
+
if not s["rows"]:
|
|
402
|
+
return result
|
|
403
|
+
n = self.nr["Inputs"]
|
|
404
|
+
self.put("Inputs", n, 1, s["label"], style="title")
|
|
405
|
+
for j, v in enumerate(["Включить (0/1)", "Строка"] + s["columns"]):
|
|
406
|
+
self.put("Inputs", n + 1, j + 1, v, style="header")
|
|
407
|
+
self.put("Inputs", n + 1, t["arity"] + 3, "Состояние", style="header")
|
|
408
|
+
for i in range(s["rows"]):
|
|
409
|
+
n += 1
|
|
410
|
+
rownum = n + 1
|
|
411
|
+
initial = s["values"][i] if i < len(s["values"]) else None
|
|
412
|
+
enable = self.put("Inputs", rownum, 1, 1 if initial is not None else 0, style="input0")
|
|
413
|
+
self.put("Inputs", rownum, 2, i + 1)
|
|
414
|
+
args, validations, refs = [], [], []
|
|
415
|
+
for j, (kind, scale) in enumerate(zip(t["kinds"], s["scales"])):
|
|
416
|
+
raw = self.put("Inputs", rownum, j + 3,
|
|
417
|
+
self.input_value(initial[j], kind, scale) if initial else None,
|
|
418
|
+
style="inputdate" if kind == "date" else f"input{scale}" if kind in ("dec", "money") else "inputtext")
|
|
419
|
+
refs.append(raw.replace("'", ""))
|
|
420
|
+
if kind == "text":
|
|
421
|
+
valid = f'AND(ISTEXT({raw}),LEN({raw})>0,LEN({raw})<=1000)'
|
|
422
|
+
expression = raw
|
|
423
|
+
elif kind == "date":
|
|
424
|
+
valid = f'AND(ISNUMBER({raw}),{raw}>=61,{raw}<=2958465,{raw}=INT({raw}))'
|
|
425
|
+
expression = raw
|
|
426
|
+
else:
|
|
427
|
+
valid = f'AND(ISNUMBER({raw}),ABS({raw})<={LIMIT / 10 ** scale:g},{raw}=ROUND({raw},{scale}))'
|
|
428
|
+
expression = f'ROUND({raw}*{10 ** scale},0)'
|
|
429
|
+
validations.append(f'IFERROR({valid},FALSE())')
|
|
430
|
+
local_valid = valid.replace("'Inputs'!", "")
|
|
431
|
+
self.sheets["Inputs"]["validations"].append({"sqref": f'{col(j + 3)}{rownum}', "formula": f'IFERROR({local_valid},FALSE())'})
|
|
432
|
+
gate = f'AND({enable}=1,IFERROR({valid},FALSE()))'
|
|
433
|
+
args.append(Value(self.helper(expression, s["columns"][j], gate), kind, scale,
|
|
434
|
+
self.numeric_currency if kind == "money" else None))
|
|
435
|
+
valid = conjunction(validations)
|
|
436
|
+
state = self.put("Inputs", rownum, t["arity"] + 3,
|
|
437
|
+
formula=f'IFERROR(IF(AND(ISNUMBER({enable}),{enable}=0),"OFF",IF(AND(ISNUMBER({enable}),{enable}=1,{valid}),"READY","INVALID_INPUT")),"INVALID_INPUT")')
|
|
438
|
+
self.input_checks.append(f'{state}<>"INVALID_INPUT"')
|
|
439
|
+
self.warning("Inputs", state)
|
|
440
|
+
self.sheets["Inputs"]["validations"].append({"sqref": f'A{rownum}', "formula": f'AND(ISNUMBER(A{rownum}),OR(A{rownum}=0,A{rownum}=1))'})
|
|
441
|
+
result.append(Row(f'{state}="READY"', args, f'Inputs row {rownum}'))
|
|
442
|
+
mapped.append({"enabled": enable.replace("'", ""), "args": refs, "status": state.replace("'", "")})
|
|
443
|
+
self.nr["Inputs"] = n + 4
|
|
444
|
+
self.mapping["inputs"][pred] = mapped
|
|
445
|
+
return result
|
|
446
|
+
|
|
447
|
+
def normalize(self, pred, args, gate, source):
|
|
448
|
+
t, s = self.tables[pred], self.settings[pred]
|
|
449
|
+
if len(args) != t["arity"]:
|
|
450
|
+
refuse(f"{pred}: арность головы/факта не совпадает")
|
|
451
|
+
normalized = []
|
|
452
|
+
for j, (a, kind, scale) in enumerate(zip(args, t["kinds"], s["scales"])):
|
|
453
|
+
if a.kind != kind:
|
|
454
|
+
refuse(f"{pred}: вид значения головы/факта не совпадает")
|
|
455
|
+
if kind in ("dec", "money"):
|
|
456
|
+
a = self.rescale(a, scale, gate)
|
|
457
|
+
a = Value(self.helper(a.ref, source + f' / {j + 1}', gate), a.kind, a.scale, a.currency)
|
|
458
|
+
normalized.append(a)
|
|
459
|
+
# Head errors must propagate into row presence, not become an absence.
|
|
460
|
+
active = self.helper(f'IF({conjunction(f"NOT(ISERROR({a.ref}))" for a in normalized)},TRUE(),NA())',
|
|
461
|
+
source + " / применимо", gate)
|
|
462
|
+
return Row(active, normalized, source)
|
|
463
|
+
|
|
464
|
+
def rule_rows(self, rule):
|
|
465
|
+
tables = [self.rows[a["pred"]] for a in rule["atoms"]]
|
|
466
|
+
count = 1
|
|
467
|
+
for rows in tables:
|
|
468
|
+
count *= len(rows)
|
|
469
|
+
if count + self.slots > MAX_SLOTS:
|
|
470
|
+
refuse(f"развёртка правил превышает {MAX_SLOTS} строк")
|
|
471
|
+
for env in itertools.product(*tables):
|
|
472
|
+
gate = self.helper(conjunction(row.active for row in env), rule["id"] + " / тело")
|
|
473
|
+
if rule["window"]:
|
|
474
|
+
for key, op in (("start", ">="), ("end", "<")):
|
|
475
|
+
if rule["window"].get(key):
|
|
476
|
+
gate = self.helper(f"'Inputs'!B4{op}{date_number(rule['window'][key])}", "Окно правила", gate)
|
|
477
|
+
for (ai, aj, _), (bi, bj, _) in rule["joins"]:
|
|
478
|
+
gate = self.helper(self.compare(env[ai].args[aj], env[bi].args[bj], "eq", gate), "Соединение", gate)
|
|
479
|
+
for f in rule["filters"]:
|
|
480
|
+
gate = self.helper(self.compare(env[f["row"]].args[f["col"]], self.term(f["term"], env, gate), "eq", gate), "Фильтр", gate)
|
|
481
|
+
for g in rule["guards"]:
|
|
482
|
+
gate = self.helper(self.compare(self.term(g["l"], env, gate), self.term(g["r"], env, gate), g["op"], gate), "Условие", gate)
|
|
483
|
+
args = [self.term(t, env, gate) for t in rule["head"]["args"]]
|
|
484
|
+
yield self.normalize(rule["head"]["pred"], args, gate, rule["id"])
|
|
485
|
+
|
|
486
|
+
def unique(self, row, previous):
|
|
487
|
+
gate = row.active
|
|
488
|
+
for prev in previous:
|
|
489
|
+
same = conjunction(self.compare(a, b, "eq", gate) for a, b in zip(row.args, prev.args))
|
|
490
|
+
gate = self.helper(f'NOT(AND({prev.active},{same}))', "Устранение дубликата", gate)
|
|
491
|
+
return Row(gate, row.args, row.source)
|
|
492
|
+
|
|
493
|
+
def output(self, pred):
|
|
494
|
+
rows, t, s = self.rows[pred], self.tables[pred], self.settings[pred]
|
|
495
|
+
n = self.nr["Results"]
|
|
496
|
+
self.put("Results", n, 1, s["label"], style="title")
|
|
497
|
+
for j, v in enumerate(["Состояние"] + s["columns"] + ["Ключ", "Основание"]):
|
|
498
|
+
self.put("Results", n + 1, j + 1, v, style="header")
|
|
499
|
+
mapped = []
|
|
500
|
+
for row in rows:
|
|
501
|
+
n += 1
|
|
502
|
+
r = n + 1
|
|
503
|
+
status = self.put("Results", r, 1, formula=f'IF($B$3<>"READY",$B$3,IF({row.active},"PRESENT","NEITHER"))')
|
|
504
|
+
refs = []
|
|
505
|
+
for j, a in enumerate(row.args):
|
|
506
|
+
expr = f'{a.ref}/{10 ** a.scale}' if a.kind in ("dec", "money") else a.ref
|
|
507
|
+
refs.append(self.put("Results", r, j + 2, formula=f'IF({status}="PRESENT",{expr},"")',
|
|
508
|
+
style="date" if a.kind == "date" else f"num{a.scale}" if a.kind in ("dec", "money") else "body").replace("'", ""))
|
|
509
|
+
conflicts = []
|
|
510
|
+
for key in t["keys"]:
|
|
511
|
+
for other in rows:
|
|
512
|
+
if other is row:
|
|
513
|
+
continue
|
|
514
|
+
same = conjunction(self.compare(row.args[i], other.args[i], "eq", "TRUE()") for i in key)
|
|
515
|
+
conflicts.append(f'AND({other.active},{same})')
|
|
516
|
+
conflict = "FALSE()" if not conflicts else f'NOT({conjunction("NOT(" + c + ")" for c in conflicts)})'
|
|
517
|
+
keycell = self.put("Results", r, t["arity"] + 2,
|
|
518
|
+
formula=f'IF({status}="PRESENT",IF({conflict},"KEY_CONFLICT",""),"")')
|
|
519
|
+
self.warning("Results", status)
|
|
520
|
+
self.warning("Results", keycell)
|
|
521
|
+
self.put("Results", r, t["arity"] + 3, row.source)
|
|
522
|
+
mapped.append({"status": status.replace("'", ""), "args": refs, "key": keycell.replace("'", "")})
|
|
523
|
+
if not rows:
|
|
524
|
+
self.put("Results", n + 2, 1, formula='IF($B$3="READY","NEITHER",$B$3)')
|
|
525
|
+
self.nr["Results"] = n + 5
|
|
526
|
+
self.mapping["tables"][pred] = mapped
|
|
527
|
+
|
|
528
|
+
def build(self):
|
|
529
|
+
self.validate()
|
|
530
|
+
for name in self.sheets:
|
|
531
|
+
title = {"Results": "Arxo · Результаты", "Inputs": "Входные данные", "Calculation": "Вычисление правил", "Model": "Паспорт модели"}[name]
|
|
532
|
+
self.put(name, 2, 1, title, style="title")
|
|
533
|
+
self.put("Inputs", 4, 1, "Дата расчёта")
|
|
534
|
+
on = self.config.get("legalTime")
|
|
535
|
+
self.put("Inputs", 4, 2, date_number(on) if on else None, style="inputdate")
|
|
536
|
+
self.put("Inputs", 5, 1, "Жёлтые ячейки — ввод. 1 включает строку, 0 исключает. Новые строки требуют новой сборки.")
|
|
537
|
+
self.sheets["Inputs"]["validations"].append({"sqref": "B4", "formula": 'AND(ISNUMBER(B4),B4=INT(B4),B4>=61,B4<=2958465)'})
|
|
538
|
+
self.input_checks.append('IFERROR(AND(ISNUMBER(\'Inputs\'!B4),\'Inputs\'!B4=INT(\'Inputs\'!B4),\'Inputs\'!B4>=61,\'Inputs\'!B4<=2958465),FALSE())')
|
|
539
|
+
for pred in self.order:
|
|
540
|
+
base = self.input_rows(pred)
|
|
541
|
+
candidates = []
|
|
542
|
+
for row in base:
|
|
543
|
+
gate = self.helper(row.active, row.source, "'Inputs'!B3=\"READY\"")
|
|
544
|
+
candidates.append(self.normalize(pred, row.args, gate, row.source))
|
|
545
|
+
for fact in self.plan["facts"]:
|
|
546
|
+
fields(fact, {"pred", "args"}, "факт")
|
|
547
|
+
if fact["pred"] not in self.tables:
|
|
548
|
+
refuse("факт необъявленного предиката")
|
|
549
|
+
if fact["pred"] == pred:
|
|
550
|
+
args = [self.term(a, [], "'Inputs'!B3=\"READY\"") for a in fact["args"]]
|
|
551
|
+
candidates.append(self.normalize(pred, args, "'Inputs'!B3=\"READY\"", "Факт пакета"))
|
|
552
|
+
for rule in self.plan["rules"]:
|
|
553
|
+
if rule["head"]["pred"] == pred:
|
|
554
|
+
candidates.extend(self.rule_rows(rule))
|
|
555
|
+
self.slots += len(candidates)
|
|
556
|
+
if self.slots > MAX_SLOTS:
|
|
557
|
+
refuse(f"более {MAX_SLOTS} строк")
|
|
558
|
+
self.rows[pred] = []
|
|
559
|
+
for row in candidates:
|
|
560
|
+
self.rows[pred].append(self.unique(row, self.rows[pred]))
|
|
561
|
+
self.put("Inputs", 3, 1, "Состояние ввода")
|
|
562
|
+
self.put("Inputs", 3, 2, formula=f'IF({conjunction(self.input_checks)},"READY","INVALID_INPUT")')
|
|
563
|
+
for pred in reversed(self.order):
|
|
564
|
+
if pred != P.ALWAYS:
|
|
565
|
+
self.output(pred)
|
|
566
|
+
self.put("Results", 3, 1, "Состояние расчёта")
|
|
567
|
+
self.put("Results", 3, 2,
|
|
568
|
+
formula=f'IF(\'Inputs\'!B3<>"READY","INVALID_INPUT",IF(SUM(\'Calculation\'!D7:D{max(7, self.nr["Calculation"] - 1)})>0,"CALCULATION_REFUSED","READY"))')
|
|
569
|
+
self.warning("Results", "B3")
|
|
570
|
+
self.warning("Inputs", "B3")
|
|
571
|
+
self.put("Results", 4, 1, "PRESENT — строка выведена; NEITHER — вывод отсутствует. KEY_CONFLICT — несколько значений одного ключа.")
|
|
572
|
+
self.put("Results", 5, 1, "CALCULATION_REFUSED — точность/диапазон числа или деление. Ошибка видна на листе Calculation.")
|
|
573
|
+
for j, v in enumerate(["Шаг / основание", "Вычислено", "После проверки", "Ошибка"]):
|
|
574
|
+
self.put("Calculation", 6, j + 1, v, style="header")
|
|
575
|
+
metadata = [("Профиль", PROFILE), ("Версия плана", self.plan["planVersion"]),
|
|
576
|
+
("Пакет", (self.plan.get("package") or {}).get("name", "")),
|
|
577
|
+
("Семантический хэш", self.config.get("semanticHash", "не задан")),
|
|
578
|
+
("Валюта", self.numeric_currency or "не используется"),
|
|
579
|
+
("Точность", f"Целочисленные коэффициенты до ±{LIMIT}; масштаб входа 0..6; промежуточный 0..12"),
|
|
580
|
+
("Деление", "Точное десятичное частное до 6 разрядов (либо больший исходный масштаб); иначе отказ"),
|
|
581
|
+
("Даты", "1900-03-01..9999-12-31; система дат Excel 1900"),
|
|
582
|
+
("Граница", "Строгие положительные правила без рекурсии; без канонического proof-документа"),
|
|
583
|
+
("Пересчёт", "Автоматический; книга работает самостоятельно, без Arxo и макросов"),
|
|
584
|
+
("Пустые значения", "Пустой аргумент включённой строки — INVALID_INPUT; отсутствующий факт — строка выключена"),
|
|
585
|
+
("Сравнение текста", "С учётом регистра; пустой текст вне входного профиля"),
|
|
586
|
+
("Редактирование", "Меняйте только жёлтые ячейки Inputs. Остальные ячейки — программа модели.")]
|
|
587
|
+
metadata += [("Зависимость", json.dumps(w, ensure_ascii=False)) for w in self.plan.get("world", [])]
|
|
588
|
+
for q in (self.plan.get("api") or {}).get("questions", []):
|
|
589
|
+
for source in q.get("sources", []):
|
|
590
|
+
for anchor in source.get("anchors", []):
|
|
591
|
+
metadata.append(("Источник / " + source["rule"], json.dumps(anchor, ensure_ascii=False)))
|
|
592
|
+
for i, (label, value) in enumerate(metadata, 4):
|
|
593
|
+
self.put("Model", i, 1, label)
|
|
594
|
+
self.put("Model", i, 2, value)
|
|
595
|
+
# Effective configuration is a reproducible input contract.
|
|
596
|
+
self.mapping["config"] = dict(self.config, profile=PROFILE, tables=self.settings)
|
|
597
|
+
self.mapping["limits"] = {"integerCoefficient": LIMIT, "candidateRows": MAX_SLOTS, "cells": MAX_CELLS}
|
|
598
|
+
return {"format": FORMAT, "profile": PROFILE, "sheets": list(self.sheets.values()), "mapping": self.mapping}
|
|
599
|
+
|
|
600
|
+
|
|
601
|
+
def build(plan, config=None):
|
|
602
|
+
return Planner(plan, config).build()
|