arxo-codegen 0.4.0__py3-none-any.whl

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