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,334 @@
|
|
|
1
|
+
"""Общая часть API порождённого модуля (DECISION-0423 §2.1, §2.4, §2.5).
|
|
2
|
+
|
|
3
|
+
Значения входа — литерал-термы §53 с явным видом: `integer`, `decimal`,
|
|
4
|
+
`money`, `text`, `entity` и другие конструкторы строят ту же каноническую
|
|
5
|
+
запись, что векторы и оракул. Факты и вопрос уходят в единый вход
|
|
6
|
+
`evaluate` — тот же, что у раннера, поэтому сверка аргументов §189, статусы
|
|
7
|
+
§175 и issues у API и раннера одни. Семантики здесь нет.
|
|
8
|
+
|
|
9
|
+
Ошибка формы вызова — `InputError`: значение не того вида для параметра или
|
|
10
|
+
запись, которую конструктор литерала не принимает. Названный отказ среза —
|
|
11
|
+
`Refused`. Семантический `TYPE_ERROR` (например, Decimal в слоте сущности,
|
|
12
|
+
поданный конструктором `decimal`) — issue и статус ответа, не исключение.
|
|
13
|
+
"""
|
|
14
|
+
from __future__ import annotations
|
|
15
|
+
|
|
16
|
+
import datetime as _dt
|
|
17
|
+
import decimal as _decimal
|
|
18
|
+
import json as _json
|
|
19
|
+
import re as _re
|
|
20
|
+
from dataclasses import dataclass
|
|
21
|
+
|
|
22
|
+
import runtime as RT
|
|
23
|
+
from evaluate import evaluate as _evaluate
|
|
24
|
+
|
|
25
|
+
Refused = RT.Refused
|
|
26
|
+
|
|
27
|
+
STD = "urn:law:std#"
|
|
28
|
+
INT, NAT, DEC, MON = STD + "Integer", STD + "Natural", STD + "Decimal", STD + "Money"
|
|
29
|
+
QTY, RAT, TEXT, BOOL = STD + "Quantity", STD + "Rational", STD + "Text", STD + "Boolean"
|
|
30
|
+
DATE, INSTANT = STD + "Date", STD + "Instant"
|
|
31
|
+
|
|
32
|
+
_DATE_RE = _re.compile(r"^[0-9]{4}-[0-9]{2}-[0-9]{2}\Z")
|
|
33
|
+
_INSTANT_RE = _re.compile(r"^[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})?\Z")
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
class InputError(ValueError):
|
|
37
|
+
"""Ошибка формы вызова API (DECISION-0423 §2.5)."""
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
@dataclass(frozen=True)
|
|
41
|
+
class Term:
|
|
42
|
+
"""Литерал-терм §53 входа или результата: `value` с std-типом и записью
|
|
43
|
+
§208, `entity_ref` или `const_ref`. Неизменяем и сравним по значению записи."""
|
|
44
|
+
kind: str
|
|
45
|
+
type: str | None = None
|
|
46
|
+
value: object = None
|
|
47
|
+
currency: str | None = None
|
|
48
|
+
unit: str | None = None
|
|
49
|
+
id: str | None = None
|
|
50
|
+
|
|
51
|
+
def to_node(self) -> dict:
|
|
52
|
+
if self.kind in ("entity_ref", "const_ref"):
|
|
53
|
+
return {"kind": self.kind, "id": self.id}
|
|
54
|
+
node = {"kind": "value", "type": {"name": self.type}, "value": self.value}
|
|
55
|
+
if self.currency is not None:
|
|
56
|
+
node["currency"] = self.currency
|
|
57
|
+
if self.unit is not None:
|
|
58
|
+
node["unit"] = self.unit
|
|
59
|
+
return node
|
|
60
|
+
|
|
61
|
+
@staticmethod
|
|
62
|
+
def from_node(node: dict) -> "Term":
|
|
63
|
+
kind = node.get("kind")
|
|
64
|
+
if kind in ("entity_ref", "const_ref"):
|
|
65
|
+
return Term(kind, id=node.get("id"))
|
|
66
|
+
if kind == "value":
|
|
67
|
+
return Term("value", type=(node.get("type") or {}).get("name"), value=node.get("value"),
|
|
68
|
+
currency=node.get("currency"), unit=node.get("unit"))
|
|
69
|
+
raise InputError(f"литерал-терм §53 неизвестного вида: {kind!r}")
|
|
70
|
+
|
|
71
|
+
|
|
72
|
+
# Представление значения между канонами (DECISION-0423 §2.7): канонический
|
|
73
|
+
# JSON литерал-терма §53 — ключи по порядку, без пробелов, Integer — числом.
|
|
74
|
+
VALUE_FORMAT = "law.codegen-value/1"
|
|
75
|
+
|
|
76
|
+
|
|
77
|
+
def to_value(term: Term) -> str:
|
|
78
|
+
"""Значение для другого модуля канона или сервиса: текст формата `VALUE_FORMAT`."""
|
|
79
|
+
if not isinstance(term, Term):
|
|
80
|
+
raise InputError("to_value: ожидается литерал-терм (Term)")
|
|
81
|
+
return _json.dumps(term.to_node(), ensure_ascii=False, sort_keys=True, separators=(",", ":"))
|
|
82
|
+
|
|
83
|
+
|
|
84
|
+
def from_value(text: str) -> Term:
|
|
85
|
+
"""Литерал-терм из текста формата `VALUE_FORMAT`."""
|
|
86
|
+
try:
|
|
87
|
+
node = _json.loads(text)
|
|
88
|
+
except (TypeError, ValueError) as exc:
|
|
89
|
+
raise InputError(f"from_value: не JSON — {exc}") from exc
|
|
90
|
+
if not isinstance(node, dict):
|
|
91
|
+
raise InputError("from_value: ожидается объект литерал-терма §53")
|
|
92
|
+
return Term.from_node(node)
|
|
93
|
+
|
|
94
|
+
|
|
95
|
+
def _checked(fn, *args):
|
|
96
|
+
try:
|
|
97
|
+
return fn(*args)
|
|
98
|
+
except (RT.TermError, RT.Refused) as exc:
|
|
99
|
+
raise InputError(str(getattr(exc, "code", exc))) from None
|
|
100
|
+
|
|
101
|
+
|
|
102
|
+
def integer(v: int) -> Term:
|
|
103
|
+
"""Integer §208: целое языка в знаковом 64-битном диапазоне."""
|
|
104
|
+
if isinstance(v, bool) or not isinstance(v, int):
|
|
105
|
+
raise InputError(f"integer: ожидается int, получено {type(v).__name__}")
|
|
106
|
+
_checked(RT.lit_int_text, v)
|
|
107
|
+
return Term("value", type=INT, value=v)
|
|
108
|
+
|
|
109
|
+
|
|
110
|
+
def natural(v: int) -> Term:
|
|
111
|
+
"""Natural §208: неотрицательное целое в знаковом 64-битном диапазоне."""
|
|
112
|
+
if isinstance(v, bool) or not isinstance(v, int) or v < 0:
|
|
113
|
+
raise InputError(f"natural: ожидается неотрицательный int, получено {v!r}")
|
|
114
|
+
_checked(RT.lit_int_text, v)
|
|
115
|
+
return Term("value", type=NAT, value=v)
|
|
116
|
+
|
|
117
|
+
|
|
118
|
+
def _decimal_record(v, where: str):
|
|
119
|
+
if isinstance(v, bool):
|
|
120
|
+
raise InputError(f"{where}: ожидается десятичная запись, получено bool")
|
|
121
|
+
if isinstance(v, _decimal.Decimal):
|
|
122
|
+
if not v.is_finite():
|
|
123
|
+
raise InputError(f"{where}: {v} не конечное число")
|
|
124
|
+
v = str(v)
|
|
125
|
+
if isinstance(v, int):
|
|
126
|
+
_checked(RT.lit_int_text, v)
|
|
127
|
+
return v
|
|
128
|
+
if not isinstance(v, str):
|
|
129
|
+
raise InputError(f"{where}: ожидается строка по лексике §208, Decimal или int, получено {type(v).__name__}")
|
|
130
|
+
_checked(RT.lit_dec_text, v)
|
|
131
|
+
return v
|
|
132
|
+
|
|
133
|
+
|
|
134
|
+
def decimal(v) -> Term:
|
|
135
|
+
"""Decimal §208: строка по лексике §208, `decimal.Decimal` или целое; без float."""
|
|
136
|
+
return Term("value", type=DEC, value=_decimal_record(v, "decimal"))
|
|
137
|
+
|
|
138
|
+
|
|
139
|
+
def money(amount, currency: str) -> Term:
|
|
140
|
+
"""Money §208: десятичная сумма и обязательная валюта."""
|
|
141
|
+
if not isinstance(currency, str) or not currency:
|
|
142
|
+
raise InputError("money: валюта обязательна")
|
|
143
|
+
return Term("value", type=MON, value=_decimal_record(amount, "money"), currency=currency)
|
|
144
|
+
|
|
145
|
+
|
|
146
|
+
def quantity(value, unit: str) -> Term:
|
|
147
|
+
"""Quantity §49 v1: десятичное значение и тег единицы как записан."""
|
|
148
|
+
if not isinstance(unit, str) or not unit:
|
|
149
|
+
raise InputError("quantity: единица обязательна")
|
|
150
|
+
return Term("value", type=QTY, value=_decimal_record(value, "quantity"), unit=unit)
|
|
151
|
+
|
|
152
|
+
|
|
153
|
+
def rational(v: str) -> Term:
|
|
154
|
+
"""Rational §35: каноническая запись `n/d` (§168.1)."""
|
|
155
|
+
if not isinstance(v, str):
|
|
156
|
+
raise InputError(f"rational: ожидается строка n/d, получено {type(v).__name__}")
|
|
157
|
+
_checked(RT.rat, v)
|
|
158
|
+
return Term("value", type=RAT, value=v)
|
|
159
|
+
|
|
160
|
+
|
|
161
|
+
def text(v: str) -> Term:
|
|
162
|
+
"""Text: строка как есть."""
|
|
163
|
+
if not isinstance(v, str):
|
|
164
|
+
raise InputError(f"text: ожидается str, получено {type(v).__name__}")
|
|
165
|
+
return Term("value", type=TEXT, value=v)
|
|
166
|
+
|
|
167
|
+
|
|
168
|
+
def boolean(v: bool) -> Term:
|
|
169
|
+
if not isinstance(v, bool):
|
|
170
|
+
raise InputError(f"boolean: ожидается bool, получено {type(v).__name__}")
|
|
171
|
+
return Term("value", type=BOOL, value=v)
|
|
172
|
+
|
|
173
|
+
|
|
174
|
+
def date(v) -> Term:
|
|
175
|
+
"""Date §2.15: календарный день ISO `YYYY-MM-DD` или `datetime.date`."""
|
|
176
|
+
if isinstance(v, _dt.datetime):
|
|
177
|
+
raise InputError("date: ожидается календарный день, получено datetime")
|
|
178
|
+
if isinstance(v, _dt.date):
|
|
179
|
+
return Term("value", type=DATE, value=v.isoformat())
|
|
180
|
+
if not isinstance(v, str) or not _DATE_RE.match(v):
|
|
181
|
+
raise InputError(f"date: ожидается запись YYYY-MM-DD, получено {v!r}")
|
|
182
|
+
try:
|
|
183
|
+
_dt.date.fromisoformat(v)
|
|
184
|
+
except ValueError:
|
|
185
|
+
raise InputError(f"date: несуществующий день {v!r}") from None
|
|
186
|
+
return Term("value", type=DATE, value=v)
|
|
187
|
+
|
|
188
|
+
|
|
189
|
+
def instant(v) -> Term:
|
|
190
|
+
"""Instant §2.15: запись ISO-мгновения, смещение — часть записи (E-0214)."""
|
|
191
|
+
if isinstance(v, _dt.datetime):
|
|
192
|
+
return Term("value", type=INSTANT, value=v.isoformat())
|
|
193
|
+
if not isinstance(v, str) or not _INSTANT_RE.match(v):
|
|
194
|
+
raise InputError(f"instant: ожидается запись ISO-мгновения, получено {v!r}")
|
|
195
|
+
try:
|
|
196
|
+
_dt.datetime.fromisoformat(v.replace("Z", "+00:00"))
|
|
197
|
+
except ValueError:
|
|
198
|
+
raise InputError(f"instant: несуществующее мгновение {v!r}") from None
|
|
199
|
+
return Term("value", type=INSTANT, value=v)
|
|
200
|
+
|
|
201
|
+
|
|
202
|
+
def entity(id: str) -> Term: # noqa: A002
|
|
203
|
+
"""Ссылка на сущность: идентичность, типа не несёт (§189, как у оракула)."""
|
|
204
|
+
if not isinstance(id, str) or not id:
|
|
205
|
+
raise InputError("entity: ожидается непустой идентификатор")
|
|
206
|
+
return Term("entity_ref", id=id)
|
|
207
|
+
|
|
208
|
+
|
|
209
|
+
def member(id: str) -> Term: # noqa: A002
|
|
210
|
+
"""Член перечисления §38 или константа §54 — ссылка `const_ref`."""
|
|
211
|
+
if not isinstance(id, str) or not id:
|
|
212
|
+
raise InputError("member: ожидается непустой идентификатор")
|
|
213
|
+
return Term("const_ref", id=id)
|
|
214
|
+
|
|
215
|
+
|
|
216
|
+
@dataclass(frozen=True)
|
|
217
|
+
class Fact:
|
|
218
|
+
"""Утверждение дела: предикат и литерал-термы аргументов; `negative` —
|
|
219
|
+
отрицательный факт §113 (только модель L1)."""
|
|
220
|
+
predicate: str
|
|
221
|
+
args: tuple
|
|
222
|
+
negative: bool = False
|
|
223
|
+
|
|
224
|
+
|
|
225
|
+
@dataclass(frozen=True)
|
|
226
|
+
class Answer:
|
|
227
|
+
"""Ответ исполняемого профиля, один для всех целей и слоёв (DECISION-0423 §2.1).
|
|
228
|
+
|
|
229
|
+
`truth_status` — у вопроса истинности (`result_kind == "PROPOSITION"`),
|
|
230
|
+
`rows` — у перечисления строк (`"COLLECTION"`). Хэшей и доказательства нет:
|
|
231
|
+
профиль `core-executable`."""
|
|
232
|
+
query_id: str
|
|
233
|
+
result_kind: str
|
|
234
|
+
truth_status: str | None
|
|
235
|
+
evaluation_status: str
|
|
236
|
+
issues: tuple = ()
|
|
237
|
+
judgment_requests: tuple = ()
|
|
238
|
+
rows: tuple | None = None
|
|
239
|
+
|
|
240
|
+
|
|
241
|
+
def slot(types: tuple, kinds: tuple, i: int, v, where: str) -> Term:
|
|
242
|
+
"""Значение `i`-го параметра: `Term` проходит как есть (сверка §189 — в
|
|
243
|
+
`evaluate`), значение языка переводится по объявленному типу слота; вид,
|
|
244
|
+
который по типу слота не читается однозначно, — `InputError`."""
|
|
245
|
+
if isinstance(v, Term):
|
|
246
|
+
return v
|
|
247
|
+
t, k = types[i], kinds[i]
|
|
248
|
+
if isinstance(v, bool):
|
|
249
|
+
if t == BOOL:
|
|
250
|
+
return boolean(v)
|
|
251
|
+
elif isinstance(v, int):
|
|
252
|
+
if t == INT:
|
|
253
|
+
return integer(v)
|
|
254
|
+
if t == NAT:
|
|
255
|
+
return natural(v)
|
|
256
|
+
if t == DEC:
|
|
257
|
+
return decimal(v)
|
|
258
|
+
elif isinstance(v, _decimal.Decimal):
|
|
259
|
+
if t == DEC:
|
|
260
|
+
return decimal(v)
|
|
261
|
+
elif isinstance(v, _dt.datetime):
|
|
262
|
+
if t == INSTANT:
|
|
263
|
+
return instant(v)
|
|
264
|
+
elif isinstance(v, _dt.date):
|
|
265
|
+
if t == DATE:
|
|
266
|
+
return date(v)
|
|
267
|
+
elif isinstance(v, str):
|
|
268
|
+
if t == TEXT:
|
|
269
|
+
return text(v)
|
|
270
|
+
if t == DATE:
|
|
271
|
+
return date(v)
|
|
272
|
+
if t == INSTANT:
|
|
273
|
+
return instant(v)
|
|
274
|
+
if t == DEC:
|
|
275
|
+
return decimal(v)
|
|
276
|
+
if t == RAT:
|
|
277
|
+
return rational(v)
|
|
278
|
+
if not t.startswith(STD):
|
|
279
|
+
return member(v) if k == "enum" else entity(v)
|
|
280
|
+
raise InputError(f"{where}: аргумент {i + 1} — для типа {t} нужен конструктор литерала "
|
|
281
|
+
f"(integer, decimal, money, text, date, entity, …), получено {type(v).__name__}")
|
|
282
|
+
|
|
283
|
+
|
|
284
|
+
def fact(pred: str, types: tuple, kinds: tuple, args: tuple, where: str, negative: bool = False) -> Fact:
|
|
285
|
+
if len(args) != len(types):
|
|
286
|
+
raise InputError(f"{where}: ожидается аргументов {len(types)}, получено {len(args)}")
|
|
287
|
+
return Fact(pred, tuple(slot(types, kinds, i, a, where) for i, a in enumerate(args)), negative)
|
|
288
|
+
|
|
289
|
+
|
|
290
|
+
def legal_time(on) -> str:
|
|
291
|
+
"""Правовое время дела §31: календарный день ISO или `datetime.date`."""
|
|
292
|
+
if isinstance(on, _dt.date) and not isinstance(on, _dt.datetime):
|
|
293
|
+
return on.isoformat()
|
|
294
|
+
if isinstance(on, str) and _DATE_RE.match(on):
|
|
295
|
+
try:
|
|
296
|
+
_dt.date.fromisoformat(on)
|
|
297
|
+
return on
|
|
298
|
+
except ValueError:
|
|
299
|
+
pass
|
|
300
|
+
raise InputError(f"on: ожидается календарный день YYYY-MM-DD, получено {on!r}")
|
|
301
|
+
|
|
302
|
+
|
|
303
|
+
def case_of(facts, on) -> dict:
|
|
304
|
+
assertions = []
|
|
305
|
+
for f in facts:
|
|
306
|
+
if not isinstance(f, Fact):
|
|
307
|
+
raise InputError(f"facts: ожидается Fact из fact_*/negated_*, получено {type(f).__name__}")
|
|
308
|
+
assertions.append({"literal": {"kind": "literal", "predicate": f.predicate,
|
|
309
|
+
"polarity": "negative" if f.negative else "positive",
|
|
310
|
+
"args": [a.to_node() for a in f.args]}})
|
|
311
|
+
return {"context": {"legalTime": legal_time(on)}, "assertions": assertions}
|
|
312
|
+
|
|
313
|
+
|
|
314
|
+
def ask(pred: str, types: tuple, kinds: tuple, facts, on, args: tuple, name: str, query_id: str | None) -> Answer:
|
|
315
|
+
"""Вопрос истинности: статус §62 атома, статус вычисления §175, issues."""
|
|
316
|
+
if len(args) != len(types):
|
|
317
|
+
raise InputError(f"ask_{name}: ожидается аргументов {len(types)}, получено {len(args)}")
|
|
318
|
+
literal = {"kind": "literal", "predicate": pred, "polarity": "positive",
|
|
319
|
+
"args": [slot(types, kinds, i, a, "ask_" + name).to_node() for i, a in enumerate(args)]}
|
|
320
|
+
query = {"kind": "truth", "queryId": query_id or "urn:query:api:" + name, "literal": literal}
|
|
321
|
+
doc = _evaluate(case_of(facts, on), query, api=True)
|
|
322
|
+
return Answer(query_id=doc["queryId"], result_kind="PROPOSITION", truth_status=doc["truthStatus"],
|
|
323
|
+
evaluation_status=doc["evaluationStatus"], issues=tuple(doc["issues"]),
|
|
324
|
+
judgment_requests=tuple(doc.get("judgmentRequests") or ()))
|
|
325
|
+
|
|
326
|
+
|
|
327
|
+
def rows(pred: str, facts, on, name: str, query_id: str | None) -> Answer:
|
|
328
|
+
"""Установленные строки вопроса литерал-термами §53 (DECISION-0423 §2.1)."""
|
|
329
|
+
query = {"kind": "rows", "queryId": query_id or "urn:query:api:rows:" + name, "predicate": pred}
|
|
330
|
+
doc = _evaluate(case_of(facts, on), query, api=True)
|
|
331
|
+
return Answer(query_id=doc["queryId"], result_kind="COLLECTION", truth_status=None,
|
|
332
|
+
evaluation_status=doc["evaluationStatus"], issues=tuple(doc["issues"]),
|
|
333
|
+
judgment_requests=tuple(doc.get("judgmentRequests") or ()),
|
|
334
|
+
rows=tuple(tuple(Term.from_node(n) for n in r) for r in doc["rows"]))
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""Харнесс проверки API: читает задание {question, on, args, facts, queryId} с
|
|
3
|
+
литерал-термами §53 и отвечает через `api.QUESTIONS`/`api.INPUTS` в том же
|
|
4
|
+
виде, что раннер, — ворота сверяют API с раннером и между целями.
|
|
5
|
+
|
|
6
|
+
Аргументы подаются явными литерал-термами (`api.Term`), поэтому задание может
|
|
7
|
+
нести и неверно типизированное значение: сверка §189 — в `evaluate`, общем с
|
|
8
|
+
раннером. Поля ответа берутся из `Answer` как есть, без подстановок
|
|
9
|
+
(DECISION-0423 §2.6). Пакет `{"batch": [...], "concurrent": bool}` исполняется
|
|
10
|
+
одним процессом — последовательно или потоками — и печатает `{"answers": [...]}`:
|
|
11
|
+
так ворота видят, что состояние между вызовами не протекает."""
|
|
12
|
+
import json
|
|
13
|
+
import sys
|
|
14
|
+
from concurrent.futures import ThreadPoolExecutor
|
|
15
|
+
from pathlib import Path
|
|
16
|
+
sys.path.insert(0, str(Path(__file__).resolve().parent))
|
|
17
|
+
import runtime as RT # noqa: E402
|
|
18
|
+
import api # noqa: E402
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
def answer(job: dict) -> dict:
|
|
22
|
+
try:
|
|
23
|
+
facts = [(api.NEGATED if f.get("polarity") == "negative" else api.INPUTS)[f["input"]](
|
|
24
|
+
*[api.Term.from_node(x) for x in f["args"]])
|
|
25
|
+
for f in job["facts"]]
|
|
26
|
+
a = api.QUESTIONS[job["question"]](facts, job["on"], *[api.Term.from_node(x) for x in job["args"]],
|
|
27
|
+
query_id=job.get("queryId"))
|
|
28
|
+
except RT.Refused as exc:
|
|
29
|
+
return {"refused": str(exc)}
|
|
30
|
+
return {"queryId": a.query_id, "truthStatus": a.truth_status, "evaluationStatus": a.evaluation_status,
|
|
31
|
+
"judgmentRequests": list(a.judgment_requests), "issues": list(a.issues)}
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
def main() -> int:
|
|
35
|
+
job = json.loads(Path(sys.argv[1]).read_text(encoding="utf-8"))
|
|
36
|
+
if "batch" in job:
|
|
37
|
+
batch = job["batch"]
|
|
38
|
+
if job.get("concurrent"):
|
|
39
|
+
with ThreadPoolExecutor(max_workers=8) as pool:
|
|
40
|
+
answers = list(pool.map(answer, batch))
|
|
41
|
+
else:
|
|
42
|
+
answers = [answer(j) for j in batch]
|
|
43
|
+
print(json.dumps({"answers": answers}, ensure_ascii=False))
|
|
44
|
+
return 0
|
|
45
|
+
out = answer(job)
|
|
46
|
+
print(json.dumps(out, ensure_ascii=False))
|
|
47
|
+
return 3 if "refused" in out else 0
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
if __name__ == "__main__":
|
|
51
|
+
sys.exit(main())
|
|
@@ -0,0 +1,140 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""Харнесс сертификации рантайма: исполняет runtime-cert.json и печатает,
|
|
3
|
+
что получилось. Ожидания сверяют ворота, харнесс их не читает — он не
|
|
4
|
+
должен знать, что считается правильным."""
|
|
5
|
+
import json
|
|
6
|
+
import sys
|
|
7
|
+
from pathlib import Path
|
|
8
|
+
|
|
9
|
+
sys.path.insert(0, str(Path(__file__).resolve().parent))
|
|
10
|
+
|
|
11
|
+
import runtime as RT # noqa: E402
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
def lit(x):
|
|
15
|
+
if isinstance(x, str):
|
|
16
|
+
return x
|
|
17
|
+
if "dec" in x:
|
|
18
|
+
return RT.dec(x["dec"])
|
|
19
|
+
if "money" in x:
|
|
20
|
+
return RT.money(x["money"], x["cur"])
|
|
21
|
+
if "text" in x:
|
|
22
|
+
return x["text"]
|
|
23
|
+
if "date" in x:
|
|
24
|
+
return RT.date(x["date"])
|
|
25
|
+
if "instant" in x:
|
|
26
|
+
return RT.instant(x["instant"])
|
|
27
|
+
if "sym" in x:
|
|
28
|
+
return RT.sym(x["sym"])
|
|
29
|
+
if "rat" in x:
|
|
30
|
+
return RT.rat(x["rat"])
|
|
31
|
+
if "qty" in x:
|
|
32
|
+
return RT.qty(x["qty"], x["unit"])
|
|
33
|
+
if "bool" in x:
|
|
34
|
+
return bool(x["bool"])
|
|
35
|
+
if "row" in x:
|
|
36
|
+
return [lit(v) for v in x["row"]]
|
|
37
|
+
raise AssertionError(x)
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
def digits(u: int, s: int) -> str:
|
|
41
|
+
"""Отображение с ЯВНЫМ масштабом: 2.00 и 2 — разные записи одного значения."""
|
|
42
|
+
neg = u < 0
|
|
43
|
+
body = str(-u if neg else u).rjust(s + 1, "0")
|
|
44
|
+
text = body if s == 0 else body[:-s] + "." + body[-s:]
|
|
45
|
+
return ("-" if neg else "") + text
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
def render(v):
|
|
49
|
+
if v is None or isinstance(v, (bool, str)):
|
|
50
|
+
return v
|
|
51
|
+
if isinstance(v, RT.Money):
|
|
52
|
+
return digits(v.u, v.s) + " " + v.cur
|
|
53
|
+
if isinstance(v, RT.Date):
|
|
54
|
+
return v.v
|
|
55
|
+
if isinstance(v, RT.Instant):
|
|
56
|
+
return v.v
|
|
57
|
+
if isinstance(v, RT.Sym):
|
|
58
|
+
return v.id
|
|
59
|
+
if isinstance(v, RT.Rat):
|
|
60
|
+
return f"{v.n}/{v.d}"
|
|
61
|
+
if isinstance(v, RT.Qty):
|
|
62
|
+
return digits(v.u, v.s) + " [" + v.unit + "]"
|
|
63
|
+
return digits(v.u, v.s)
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
def run(case: dict):
|
|
67
|
+
op, a = case["op"], [lit(x) for x in case["args"]]
|
|
68
|
+
if op == "dec":
|
|
69
|
+
return RT.dec(a[0])
|
|
70
|
+
if op == "money":
|
|
71
|
+
return RT.money(a[0], a[1])
|
|
72
|
+
if op == "date":
|
|
73
|
+
return RT.date(a[0])
|
|
74
|
+
if op == "instant":
|
|
75
|
+
return RT.instant(a[0])
|
|
76
|
+
if op == "hoursBetween":
|
|
77
|
+
return RT.hours_between(a[0], a[1])
|
|
78
|
+
if op == "minutesBetween":
|
|
79
|
+
return RT.minutes_between(a[0], a[1])
|
|
80
|
+
if op == "addDuration":
|
|
81
|
+
return RT.add_duration(a[0], int(a[1]), a[2])
|
|
82
|
+
if op == "sym":
|
|
83
|
+
return RT.sym(a[0])
|
|
84
|
+
if op == "rat":
|
|
85
|
+
return RT.rat(a[0])
|
|
86
|
+
if op == "ratTerm":
|
|
87
|
+
return RT.rat_term(a[0])
|
|
88
|
+
if op == "qty":
|
|
89
|
+
return RT.qty(a[0], a[1])
|
|
90
|
+
if op == "counter":
|
|
91
|
+
n, unit = RT.counter(a[0], a[1])
|
|
92
|
+
return f"{n} {unit}"
|
|
93
|
+
if op == "arith":
|
|
94
|
+
return RT.arith(a[0], a[1], a[2], a[3], a[4])
|
|
95
|
+
if op == "mul":
|
|
96
|
+
return RT.mul(a[0], a[1])
|
|
97
|
+
if op == "round":
|
|
98
|
+
return RT.round(a[0], a[1], a[2])
|
|
99
|
+
if op == "roundMode":
|
|
100
|
+
return RT.round_mode(a[0], a[1], a[2], a[3], a[4])
|
|
101
|
+
if op == "divRound":
|
|
102
|
+
return RT.div_round(a[0], a[1], a[2], a[3], a[4], a[5], a[6])
|
|
103
|
+
if op == "cmp":
|
|
104
|
+
return RT.cmp(a[0], a[1], a[2])
|
|
105
|
+
if op == "eq":
|
|
106
|
+
return RT.eq(a[0], a[1])
|
|
107
|
+
if op in ("add", "sub", "div", "mod", "min", "max", "pow"):
|
|
108
|
+
return getattr(RT, op)(a[0], a[1])
|
|
109
|
+
if op == "abs":
|
|
110
|
+
return RT.abs(a[0])
|
|
111
|
+
if op == "monthStart":
|
|
112
|
+
return RT.month_start(a[0])
|
|
113
|
+
if op == "monthEnd":
|
|
114
|
+
return RT.month_end(a[0])
|
|
115
|
+
if op == "keyEq":
|
|
116
|
+
return RT.key(a[0]) == RT.key(a[1])
|
|
117
|
+
if op == "legalTime":
|
|
118
|
+
return RT.legal_time()
|
|
119
|
+
if op == "setLegalTime":
|
|
120
|
+
RT.set_legal_time(a[0])
|
|
121
|
+
return None
|
|
122
|
+
raise AssertionError("неизвестная операция фикстуры: " + op)
|
|
123
|
+
|
|
124
|
+
|
|
125
|
+
def main() -> None:
|
|
126
|
+
fixture = json.loads(Path(sys.argv[1]).read_text(encoding="utf-8"))
|
|
127
|
+
out = []
|
|
128
|
+
for case in fixture["cases"]:
|
|
129
|
+
try:
|
|
130
|
+
got = render(run(case))
|
|
131
|
+
except RT.Refused as exc:
|
|
132
|
+
got = {"refused": str(exc)}
|
|
133
|
+
except RT.TermError as exc:
|
|
134
|
+
got = {"termError": exc.code}
|
|
135
|
+
out.append({"id": case["id"], "got": got})
|
|
136
|
+
print(json.dumps(out, ensure_ascii=False))
|
|
137
|
+
|
|
138
|
+
|
|
139
|
+
if __name__ == "__main__":
|
|
140
|
+
main()
|