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,176 @@
1
+ # Excel как самостоятельный backend
2
+
3
+ `./law dev backend … --target xlsx` компилирует модель в обычную `.xlsx`-книгу.
4
+ После сборки **Excel сам пересчитывает формулы** при изменении входных ячеек.
5
+ Arxo, Python, сервер, сеть, надстройки и макросы для использования книги не нужны.
6
+ Для генерации достаточно стандартной библиотеки Python; новые зависимости
7
+ в Rust, wasm и npm не добавляются.
8
+
9
+ ## Быстрый пример
10
+
11
+ Из корня репозитория:
12
+
13
+ ```sh
14
+ ./law dev backend engines/backends/examples/xlsx/pricing.lawir.json \
15
+ --target xlsx \
16
+ --xlsx-config engines/backends/examples/xlsx/pricing.xlsx.json \
17
+ --out /tmp/arxo-excel-example
18
+ ```
19
+
20
+ Откройте `/tmp/arxo-excel-example/xlsx/model.xlsx`. На листе `Inputs`:
21
+
22
+ - `B4` — дата расчёта;
23
+ - `A9` — включение первой строки: `1` / `0`;
24
+ - `C9` — идентификатор полиса;
25
+ - `D9` — категория `A` или `B`;
26
+ - `E9` — базовая сумма.
27
+
28
+ Начальный результат: `100.05 × 1.10`, округление `HALF_UP` до двух знаков →
29
+ `110.06`. Замена суммы на `200` даёт `220`; замена категории на `B` при
30
+ исходной сумме — `125.06`. Формулы книги остаются теми же.
31
+
32
+ Пример **синтетический**, проверяет механизм генерации и не является тарифом
33
+ ОГПО ВТС. Два правила намеренно выводят один результат: книга устраняет дубликат.
34
+
35
+ ## Состав поставки
36
+
37
+ | Файл | Назначение |
38
+ |---|---|
39
+ | `model.xlsx` | Самостоятельный калькулятор |
40
+ | `cells.json` | Адреса входов и выходов, эффективная конфигурация и лимиты |
41
+ | `plan.json` | Общий план Arxo, из которого построена книга |
42
+ | `law-backend.json` | Профиль, семантический хэш CLIR и SHA-256 файлов |
43
+ | `README.txt` | Краткая инструкция пользователю книги |
44
+
45
+ Листы: `Results` — строки отношений, `Inputs` — редактируемые данные,
46
+ `Calculation` — вычисление и проверка промежуточных значений, `Model` —
47
+ паспорт и ограничения. Якоря источников из API плана, если они есть, находятся
48
+ на `Model`; строка результата содержит идентификатор правила.
49
+
50
+ ## Конфигурация входов
51
+
52
+ `--xlsx-config` принимает JSON-объект:
53
+
54
+ ```json
55
+ {
56
+ "profile": "excel-calculator/0.1",
57
+ "currency": "KZT",
58
+ "legalTime": "2026-09-15",
59
+ "tables": {
60
+ "urn:law:example:excel#quote": {
61
+ "rows": 2,
62
+ "scales": [0, 0, 2],
63
+ "columns": ["Полис", "Категория", "Базовая сумма, KZT"],
64
+ "label": "Данные расчёта",
65
+ "values": [["POLICY-1", "A", "100.05"]]
66
+ }
67
+ }
68
+ }
69
+ ```
70
+
71
+ Имена таблиц — точные `pred` из общего плана. Для каждой таблицы:
72
+
73
+ - `rows` задаёт **число входных слотов**, от 0 до 100; начальные строки `values`
74
+ включены, остальные выключены. При `rows: 0` отношение получает только
75
+ собственные факты пакета и результаты правил.
76
+ - По умолчанию предикат без правил-производителей и собственных фактов получает
77
+ один выключенный входной слот; остальные — ноль. Можно явно добавить входы
78
+ производному отношению. Служебное `always` входом быть не может.
79
+ - `scales` задаёт число десятичных знаков для **каждой** колонки: по умолчанию
80
+ Decimal — 4, Money — 2, Integer/Text/Date — 0. Допустимы 0..6; ненулевой
81
+ масштаб разрешён только Decimal и Money. Точность выходных колонок тоже
82
+ задаётся здесь: генератор не округляет до неё без явного `round` модели.
83
+ - `columns` и `label` необязательны. Количество значений/меток/масштабов должно
84
+ совпадать с арностью. Дата задаётся ISO-строкой, в книге хранится числом.
85
+ - `currency` обязательна для Money. Вся книга работает в одной явно выбранной
86
+ валюте; литерал другой валюты отвергает сборку.
87
+
88
+ Конфигурация необязательна для модели без Money. Если `legalTime` не указана,
89
+ дата остаётся пустой и пользователь должен заполнить её. Дата всегда явная:
90
+ генератор не подставляет `TODAY()` и не зависит от времени запуска.
91
+
92
+ Пустая включённая строка — ошибочный ввод. Отсутствующий факт представляется
93
+ выключенной строкой. Пустой Text во входе вне профиля; строка `"0"` не является
94
+ числом, числовой ноль допустим. Проверки формулами продолжают действовать,
95
+ если вставка через буфер обошла Excel Data Validation.
96
+
97
+ ## Поддерживаемый профиль
98
+
99
+ `excel-calculator/0.1` принимает общий план `law-backend-plan/0.7`:
100
+
101
+ - строгие положительные правила с ациклическими зависимостями предикатов;
102
+ - собственные факты, несколько входных строк, соединения, фильтры и сравнения;
103
+ - Text, Integer, Decimal, Money и Date; Text сравнивается с учётом регистра;
104
+ - `add`, `sub`, `mul`, точное ограниченное `div`, евклидов `mod`, `min`, `max`,
105
+ `abs`, явный `round(…, <литерал 0..6>, "HALF_UP")`;
106
+ - окна правил и редакций, уже пониженные в `[start, end)` по `legal_time`;
107
+ - множество кортежей: повтор одного вывода удаляется; разные значения одного
108
+ ключа сохраняются и отмечаются `KEY_CONFLICT`.
109
+
110
+ Рекурсия, отрицательные/поражаемые правила, приоритеты, агрегаты, замыкания,
111
+ нормативные позиции, `pow`, календарные функции, пользовательские типы,
112
+ перечисления, Boolean, Rational, Instant и Quantity вне этого профиля.
113
+ Неподдерживаемая форма отвергается **даже в правиле с пустым входом**.
114
+ Общий план и другие backend от этого не меняют семантику.
115
+
116
+ Формулы разворачивают декартовы произведения ограниченных входных слотов.
117
+ Максимум — 2000 строк-кандидатов, 100000 ячеек, 10 аргументов отношения,
118
+ 8000 UTF-16 единиц формулы. Увеличить число входных строк можно пересборкой,
119
+ добавление строки вручную под таблицей не расширяет программу.
120
+
121
+ ### Числа и статусы
122
+
123
+ Внутри книги `100.05` при масштабе 2 представлен коэффициентом `10005`.
124
+ Арифметика и сравнения работают над целыми коэффициентами в диапазоне
125
+ `±10^12`; масштаб промежуточного значения — 0..12. Это намеренно более
126
+ узкая область, чем все значения Arxo: переполнение **промежуточного**
127
+ коэффициента тоже требует отказа, даже если итог мог бы поместиться.
128
+
129
+ Деление разрешено, когда целочисленный числитель укладывается в предел и
130
+ частное точно представимо при масштабе `max(6, scale(lhs)-scale(rhs))`.
131
+ `1 / 3`, деление на ноль и потеря разрядов при записи результата не превращаются
132
+ в приближённое число. Округление `HALF_UP` определяется целым остатком,
133
+ включая отрицательные значения на половине шага.
134
+
135
+ | Статус | Значение |
136
+ |---|---|
137
+ | `READY` | Вход и вычисление укладываются в профиль |
138
+ | `INVALID_INPUT` | Пустой/неверный аргумент включённой строки, дата или переключатель |
139
+ | `CALCULATION_REFUSED` | Промежуточная арифметика/масштаб вне профиля либо деление на ноль; в Calculation видна ошибка |
140
+ | `PRESENT` | Данный кортеж выведен |
141
+ | `NEITHER` | Для слота нет вывода; это не доказательство отрицания |
142
+ | `KEY_CONFLICT` | Несколько разных выведенных кортежей с одним ключом |
143
+
144
+ При отказе значения на Results недоступны; частичный ответ не выдаётся.
145
+ Статусы книги относятся к этому профилю, не подменяют канонический документ
146
+ evaluation. Proof graph в книгу не компилируется.
147
+
148
+ Даты ограничены `1900-03-01..9999-12-31`, система Excel 1900. Это исключает
149
+ несуществующий день 29 февраля 1900 года. Книга задаёт автоматический полный
150
+ пересчёт без итераций и не записывает выдуманные кэшированные результаты.
151
+
152
+ ## Проверка
153
+
154
+ ```sh
155
+ .venv-work-profile/bin/python verify/ci/gates/backends/check_xlsx.py --recalculate
156
+ ```
157
+
158
+ `--soffice <путь>` выбирает LibreOffice; `--keep <новый каталог>` сохраняет
159
+ книги и результаты. Без `--recalculate` выполняются только структурные пробы,
160
+ а отсутствие проверки пересчёта явно сообщается. С флагом отсутствие/падение
161
+ LibreOffice считается ошибкой. Он запускается с отдельным временным профилем.
162
+
163
+ Тест изменяет ячейки **однажды сгенерированной** книги и проверяет реальные
164
+ кэши после пересчёта. Полные множества допустимых результатов сверяются с
165
+ оракулом Arxo; отдельно проверяются отсутствие вывода, ошибки, границы,
166
+ конфликт ключа, безопасность текстовых ячеек, детерминированные байты и
167
+ сохранение существующей поставки при отказе. Python-поставка проверяется
168
+ через прежний путь записи текстового backend.
169
+
170
+ Проверено в LibreOffice 26.2.3.2. Microsoft Excel на машине проверки отсутствует;
171
+ его собственный пересчёт пока не проверялся. Импорт, пересчёт и визуальный
172
+ просмотр примера дополнительно выполнены через Artifact Tool. Полный корпус
173
+ и общие ворота не запускались; замороженные пакеты не проверялись.
174
+
175
+ Основание ограничений: [точность Excel и IEEE 754](https://learn.microsoft.com/en-us/troubleshoot/microsoft-365-apps/excel/floating-point-arithmetic-inaccurate-result),
176
+ [лимиты Excel](https://support.microsoft.com/en-us/excel/excel-specifications-and-limits).
@@ -0,0 +1,198 @@
1
+ """XLSX delivery descriptor and deterministic SpreadsheetML serializer.
2
+
3
+ No Office/Node dependency is required to generate or use the workbook.
4
+ All execution-profile decisions and formulas live in excel_plan.py.
5
+ """
6
+ from __future__ import annotations
7
+
8
+ import io
9
+ import json
10
+ import zipfile
11
+ from xml.etree import ElementTree as ET
12
+
13
+ import excel_plan as X
14
+
15
+ MODEL = "model.xlsx"
16
+ PROFILE = X.PROFILE
17
+ PROTOCOL = "workbook"
18
+ CAPABILITIES = set()
19
+ QUERY_KINDS = set()
20
+ NS = "http://schemas.openxmlformats.org/spreadsheetml/2006/main"
21
+ REL = "http://schemas.openxmlformats.org/officeDocument/2006/relationships"
22
+ PKG = "http://schemas.openxmlformats.org/package/2006/relationships"
23
+ CONTENT = "http://schemas.openxmlformats.org/package/2006/content-types"
24
+
25
+
26
+ def available():
27
+ return True, "XLSX: стандартная библиотека Python; пересчёт — Excel/LibreOffice"
28
+
29
+
30
+ def xml(element):
31
+ return ET.tostring(element, encoding="utf-8", xml_declaration=True)
32
+
33
+
34
+ def styles():
35
+ root = ET.Element("styleSheet", xmlns=NS)
36
+ fmts = ET.SubElement(root, "numFmts", count="8")
37
+ formats = ["#,##0"] + ["#,##0." + "0" * n for n in range(1, 7)] + ["yyyy-mm-dd"]
38
+ for i, fmt in enumerate(formats, 164):
39
+ ET.SubElement(fmts, "numFmt", numFmtId=str(i), formatCode=fmt)
40
+ fonts = ET.SubElement(root, "fonts", count="3")
41
+ for size, bold, color in ((10, False, "FF202124"), (14, True, "FF202124"), (10, True, "FFFFFFFF")):
42
+ font = ET.SubElement(fonts, "font")
43
+ ET.SubElement(font, "sz", val=str(size))
44
+ ET.SubElement(font, "name", val="Arial")
45
+ ET.SubElement(font, "color", rgb=color)
46
+ if bold:
47
+ ET.SubElement(font, "b")
48
+ fills = ET.SubElement(root, "fills", count="4")
49
+ for pattern, color in (("none", None), ("gray125", None), ("solid", "FF304860"), ("solid", "FFFFF2CC")):
50
+ p = ET.SubElement(ET.SubElement(fills, "fill"), "patternFill", patternType=pattern)
51
+ if color:
52
+ ET.SubElement(p, "fgColor", rgb=color)
53
+ ET.SubElement(p, "bgColor", indexed="64")
54
+ borders = ET.SubElement(root, "borders", count="1")
55
+ border = ET.SubElement(borders, "border")
56
+ for side in ("left", "right", "top", "bottom", "diagonal"):
57
+ ET.SubElement(border, side)
58
+ base = ET.SubElement(root, "cellStyleXfs", count="1")
59
+ ET.SubElement(base, "xf", numFmtId="0", fontId="0", fillId="0", borderId="0")
60
+ xfs = ET.SubElement(root, "cellXfs")
61
+ ids = {}
62
+
63
+ def add(name, font=0, fill=0, num=0, wrap=False):
64
+ ids[name] = len(ids)
65
+ xf = ET.SubElement(xfs, "xf", numFmtId=str(num), fontId=str(font), fillId=str(fill), borderId="0",
66
+ xfId="0", applyAlignment="1", applyNumberFormat="1")
67
+ alignment = {"vertical": "center"}
68
+ if wrap:
69
+ alignment["wrapText"] = "1"
70
+ if name == "header":
71
+ alignment["horizontal"] = "center"
72
+ ET.SubElement(xf, "alignment", **alignment)
73
+
74
+ add("body")
75
+ add("title", font=1)
76
+ add("header", font=2, fill=2, wrap=True)
77
+ add("inputtext", fill=3)
78
+ for i in range(7):
79
+ add(f"num{i}", num=164 + i)
80
+ add(f"input{i}", fill=3, num=164 + i)
81
+ add("date", num=171)
82
+ add("inputdate", fill=3, num=171)
83
+ xfs.set("count", str(len(ids)))
84
+ cs = ET.SubElement(root, "cellStyles", count="1")
85
+ ET.SubElement(cs, "cellStyle", name="Normal", xfId="0", builtinId="0")
86
+ dxf = ET.SubElement(ET.SubElement(root, "dxfs", count="1"), "dxf")
87
+ font = ET.SubElement(dxf, "font")
88
+ ET.SubElement(font, "b")
89
+ ET.SubElement(font, "color", rgb="FF9C0006")
90
+ fill = ET.SubElement(ET.SubElement(dxf, "fill"), "patternFill", patternType="solid")
91
+ ET.SubElement(fill, "fgColor", rgb="FFFFC7CE")
92
+ ET.SubElement(fill, "bgColor", rgb="FFFFC7CE")
93
+ return xml(root), ids
94
+
95
+
96
+ def sheet_xml(sheet, style_ids):
97
+ root = ET.Element("worksheet", xmlns=NS)
98
+ view = ET.SubElement(ET.SubElement(root, "sheetViews"), "sheetView", workbookViewId="0", showGridLines="0")
99
+ if sheet["name"] == "Calculation":
100
+ ET.SubElement(view, "pane", ySplit="6", topLeftCell="A7", activePane="bottomLeft", state="frozen")
101
+ ET.SubElement(root, "sheetFormatPr", defaultRowHeight="22")
102
+ cols = ET.SubElement(root, "cols")
103
+ widths = {"Results": [24] + [24] * 10 + [36, 60], "Inputs": [28, 14] + [24] * 10 + [3, 24],
104
+ "Calculation": [64, 26, 26, 14], "Model": [29, 115]}[sheet["name"]]
105
+ for i, width in enumerate(widths, 1):
106
+ ET.SubElement(cols, "col", min=str(i), max=str(i), width=str(width), customWidth="1")
107
+ rows = {}
108
+ for cell in sheet["cells"]:
109
+ n = int(''.join(c for c in cell["cell"] if c.isdigit()))
110
+ rows.setdefault(n, []).append(cell)
111
+ data = ET.SubElement(root, "sheetData")
112
+ for n, cells in sorted(rows.items()):
113
+ row = ET.SubElement(data, "row", r=str(n), ht="32" if any(c["style"] == "header" for c in cells) else "24", customHeight="1")
114
+ def index(c):
115
+ out = 0
116
+ for x in c["cell"]:
117
+ if x.isalpha():
118
+ out = out * 26 + ord(x) - 64
119
+ return out
120
+ for cell in sorted(cells, key=index):
121
+ value, formula = cell["value"], cell["formula"]
122
+ c = ET.SubElement(row, "c", r=cell["cell"], s=str(style_ids[cell["style"]]))
123
+ if formula is not None:
124
+ ET.SubElement(c, "f").text = formula
125
+ # No invented cached value: the consuming engine recalculates.
126
+ elif isinstance(value, bool):
127
+ c.set("t", "b")
128
+ ET.SubElement(c, "v").text = "1" if value else "0"
129
+ elif isinstance(value, (int, float)):
130
+ ET.SubElement(c, "v").text = str(value)
131
+ elif value is not None:
132
+ c.set("t", "inlineStr")
133
+ text = ET.SubElement(ET.SubElement(c, "is"), "t")
134
+ text.set("{http://www.w3.org/XML/1998/namespace}space", "preserve")
135
+ text.text = str(value)
136
+ for i, spec in enumerate(sheet.get("warnings", []), 1):
137
+ cf = ET.SubElement(root, "conditionalFormatting", sqref=spec["sqref"])
138
+ rule = ET.SubElement(cf, "cfRule", type="expression", dxfId="0", priority=str(i))
139
+ ET.SubElement(rule, "formula").text = spec["formula"]
140
+ if sheet["validations"]:
141
+ dvs = ET.SubElement(root, "dataValidations", count=str(len(sheet["validations"])))
142
+ for spec in sheet["validations"]:
143
+ dv = ET.SubElement(dvs, "dataValidation", type="custom", sqref=spec["sqref"],
144
+ allowBlank="1", showErrorMessage="1", errorTitle="Неверный ввод",
145
+ error="Значение вне допустимого диапазона", errorStyle="stop")
146
+ ET.SubElement(dv, "formula1").text = spec["formula"]
147
+ ET.SubElement(root, "pageMargins", left="0.3", right="0.3", top="0.4", bottom="0.4", header="0.2", footer="0.2")
148
+ return xml(root)
149
+
150
+
151
+ def serialize(plan):
152
+ if plan.get("format") != X.FORMAT:
153
+ raise ValueError("неизвестная версия Excel-плана")
154
+ style_data, style_ids = styles()
155
+ workbook = ET.Element("workbook", xmlns=NS, **{"xmlns:r": REL})
156
+ ET.SubElement(workbook, "workbookPr", date1904="0")
157
+ ET.SubElement(ET.SubElement(workbook, "bookViews"), "workbookView", activeTab="0")
158
+ sheets = ET.SubElement(workbook, "sheets")
159
+ relationships = ET.Element("Relationships", xmlns=PKG)
160
+ types = ET.Element("Types", xmlns=CONTENT)
161
+ ET.SubElement(types, "Default", Extension="rels", ContentType="application/vnd.openxmlformats-package.relationships+xml")
162
+ ET.SubElement(types, "Default", Extension="xml", ContentType="application/xml")
163
+ entries = {"xl/styles.xml": style_data}
164
+ for i, sheet in enumerate(plan["sheets"], 1):
165
+ ET.SubElement(sheets, "sheet", name=sheet["name"], sheetId=str(i), **{"r:id": f"rId{i}"})
166
+ ET.SubElement(relationships, "Relationship", Id=f"rId{i}", Type=REL + "/worksheet", Target=f"worksheets/sheet{i}.xml")
167
+ ET.SubElement(types, "Override", PartName=f"/xl/worksheets/sheet{i}.xml", ContentType="application/vnd.openxmlformats-officedocument.spreadsheetml.worksheet+xml")
168
+ entries[f"xl/worksheets/sheet{i}.xml"] = sheet_xml(sheet, style_ids)
169
+ ET.SubElement(workbook, "calcPr", calcId="0", calcMode="auto", fullCalcOnLoad="1", forceFullCalc="1", iterate="0")
170
+ ET.SubElement(relationships, "Relationship", Id="styles", Type=REL + "/styles", Target="styles.xml")
171
+ for name, kind in (("workbook", "sheet.main"), ("styles", "styles")):
172
+ ET.SubElement(types, "Override", PartName=f"/xl/{name}.xml", ContentType=f"application/vnd.openxmlformats-officedocument.spreadsheetml.{kind}+xml")
173
+ root_rels = ET.Element("Relationships", xmlns=PKG)
174
+ ET.SubElement(root_rels, "Relationship", Id="rId1", Type=REL + "/officeDocument", Target="xl/workbook.xml")
175
+ entries.update({"[Content_Types].xml": xml(types), "_rels/.rels": xml(root_rels),
176
+ "xl/workbook.xml": xml(workbook), "xl/_rels/workbook.xml.rels": xml(relationships)})
177
+ out = io.BytesIO()
178
+ with zipfile.ZipFile(out, "w", compression=zipfile.ZIP_DEFLATED) as archive:
179
+ for name, data in sorted(entries.items()):
180
+ info = zipfile.ZipInfo(name, date_time=(1980, 1, 1, 0, 0, 0))
181
+ info.compress_type = zipfile.ZIP_DEFLATED
182
+ archive.writestr(info, data)
183
+ return out.getvalue()
184
+
185
+
186
+ def emit(plan, config=None):
187
+ return serialize(X.build(plan, config))
188
+
189
+
190
+ def artifacts(plan, config=None):
191
+ prepared = X.build(plan, config)
192
+ return {MODEL: serialize(prepared),
193
+ "cells.json": (json.dumps(prepared["mapping"], ensure_ascii=False, indent=2) + "\n").encode(),
194
+ "README.txt": ("Откройте model.xlsx в Excel. Меняйте жёлтые ячейки листа Inputs.\n"
195
+ "Дата расчёта обязательна; переключатель 1 включает строку, 0 выключает.\n"
196
+ "Результаты пересчитываются формулами без Arxo, сети и макросов.\n"
197
+ "Границы профиля — на листе Model. Для изменения числа входных строк пересоберите книгу.\n"
198
+ "cells.json описывает расположение ячеек и параметры сборки.\n").encode()}
@@ -0,0 +1,59 @@
1
+ """Current §82.2 admission. Only typed temporal fields are visited."""
2
+ import datetime
3
+ import re
4
+
5
+ DATE = re.compile(r"[0-9]{4}-[0-9]{2}-[0-9]{2}\Z")
6
+ INSTANT = 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")
7
+
8
+
9
+ def date_valid(value):
10
+ if not isinstance(value, str) or not DATE.fullmatch(value):
11
+ return False
12
+ try:
13
+ datetime.date.fromisoformat(value)
14
+ return True
15
+ except ValueError:
16
+ return False
17
+
18
+
19
+ def instant_valid(value):
20
+ m = INSTANT.fullmatch(value) if isinstance(value, str) else None
21
+ return bool(m and date_valid(m[1]) and int(m[2]) < 24 and int(m[3]) < 60
22
+ and int(m[4]) < 60 and (m[6] == "Z" or
23
+ (int(m[6][1:3]) < 24 and int(m[6][4:6]) < 60)))
24
+
25
+
26
+ def validate_ir(ir, refuse):
27
+ def bound(value, side, pointer):
28
+ if side in ("start", "end") and value == ("-infinity" if side == "start" else "infinity"):
29
+ return
30
+ if not (date_valid(value) or instant_valid(value)):
31
+ raise refuse(f"TEMPORAL_BOUND_INVALID: ir:{pointer}")
32
+
33
+ def interval(value, pointer):
34
+ if not isinstance(value, dict):
35
+ raise refuse(f"TEMPORAL_BOUND_INVALID: ir:{pointer}")
36
+ for side in ("start", "end"):
37
+ if side in value:
38
+ bound(value[side], side, pointer + "/" + side)
39
+
40
+ fields = {"assertion": "valid", "calendar_snapshot": "valid", "evidence_item": "valid",
41
+ "support_edge": "valid", "closure_policy": "effective", "constraint": "effective",
42
+ "interpretation": "effective", "rule": "effective", "entrenchment": "window"}
43
+ for i, node in enumerate(ir.get("nodes") or []):
44
+ kind = node.get("kind")
45
+ prefix = f"/nodes/{i}"
46
+ field = fields.get(kind)
47
+ if field and field in node:
48
+ interval(node[field], prefix + "/" + field)
49
+ if kind == "rule":
50
+ for j, value in enumerate(node.get("governs") or []):
51
+ interval(value, f"{prefix}/governs/{j}")
52
+ if kind == "source_edition":
53
+ for j, event in enumerate(node.get("lifecycle") or []):
54
+ if "extent" in event:
55
+ value, pointer = event["extent"], f"{prefix}/lifecycle/{j}/extent"
56
+ if isinstance(value, dict):
57
+ interval(value, pointer)
58
+ else:
59
+ bound(value, None, pointer)
@@ -0,0 +1,171 @@
1
+ """Алиасы типов §40 и сущности `law.std.actors` §261 в срезе (W2, часть 1).
2
+
3
+ Оракул сверяет типы §189 за цепочкой алиасов (`facts._unalias`, E-0097/E-0098);
4
+ план обязан читать те же цепочки: алиас на std-тип, на сущность (в том числе
5
+ чужого пакета мира и цепочкой), на перечисление — прозрачны; цель без пакета
6
+ в мире остаётся отказом с именем цели. Std-акторы — идентичности."""
7
+ from __future__ import annotations
8
+
9
+ from pathlib import Path
10
+ import sys
11
+ import unittest
12
+
13
+ HERE = Path(__file__).resolve().parent
14
+ sys.path.insert(0, str(HERE))
15
+ import plan as P # noqa: E402
16
+
17
+ NS = "urn:law:test:alias"
18
+ WNS = "urn:law:test:alias-world"
19
+ TEXT, NATURAL, INT = "urn:law:std#Text", "urn:law:std#Natural", "urn:law:std#Integer"
20
+ ACTOR = "urn:law:std#Actor"
21
+ H = "sha256:" + "0" * 64
22
+
23
+
24
+ def _rel(ns: str, name: str, kind: str, params: list[tuple[str, str]]) -> dict:
25
+ return {"kind": "symbol_decl", "symbolKind": "relation", "id": f"{ns}#{name}", "package": ns,
26
+ "name": name, "relationKind": kind, "contentHash": H,
27
+ "parameters": [{"id": f"{ns}#{name}/arg/{p}", "name": p, "type": {"name": t}} for p, t in params]}
28
+
29
+
30
+ def _alias(ns: str, name: str, target: str) -> dict:
31
+ return {"kind": "type_decl", "typeKind": "alias", "id": f"{ns}#{name}", "package": ns,
32
+ "name": name, "target": {"name": target}, "contentHash": H}
33
+
34
+
35
+ def _entity(ns: str, name: str) -> dict:
36
+ return {"kind": "type_decl", "typeKind": "entity", "id": f"{ns}#{name}", "package": ns,
37
+ "name": name, "contentHash": H}
38
+
39
+
40
+ def _enum(ns: str, name: str, variants: list[str]) -> dict:
41
+ return {"kind": "type_decl", "typeKind": "enum", "id": f"{ns}#{name}", "package": ns,
42
+ "name": name, "variants": variants, "contentHash": H}
43
+
44
+
45
+ def _lit(pred: str, args: list[dict]) -> dict:
46
+ return {"kind": "literal", "polarity": "positive", "predicate": pred, "args": args}
47
+
48
+
49
+ def _var(i: int) -> dict:
50
+ return {"kind": "var", "var": f"v{i}"}
51
+
52
+
53
+ def _rule(ns: str, name: str, variables: list[tuple[int, str]], body_items: list[dict], head: dict) -> dict:
54
+ body = body_items[0] if len(body_items) == 1 else {"kind": "and", "items": body_items}
55
+ return {"kind": "rule", "id": f"{ns}#{name}", "package": ns, "strength": "strict",
56
+ "variables": [{"id": f"v{i}", "type": {"name": t}} for i, t in variables],
57
+ "body": body, "head": head, "scope": {"kind": "boolean", "value": True},
58
+ "interpretations": [], "contentHash": H}
59
+
60
+
61
+ def _established(lit: dict) -> dict:
62
+ return {"kind": "status", "status": "established", "formula": lit}
63
+
64
+
65
+ def _ir(nodes: list[dict], ns: str = NS, name: str = "test.alias", imports: list[dict] | None = None) -> dict:
66
+ return {"schemaVersion": "law.core.ir/0.7", "semanticVersion": "0.2.11",
67
+ "package": {"name": name, "version": "0.1.0", "namespace": ns, "semanticHash": H},
68
+ "imports": imports or [], "features": ["core-executable"], "nodes": nodes,
69
+ "strata": [], "metadata": {}}
70
+
71
+
72
+ def _plan_types(plan: dict) -> dict:
73
+ return {r["id"]: r for r in plan.get("relations", [])} if isinstance(plan.get("relations"), list) else {}
74
+
75
+
76
+ class AliasTests(unittest.TestCase):
77
+ def test_alias_of_std_type_is_transparent(self):
78
+ # ERR-E0098-ALIAS-VALUE-EVAL на 0.2: `type N = Natural; relation r(n: N)`, гард `n > 3`.
79
+ nodes = [_alias(NS, "N", NATURAL),
80
+ _rel(NS, "r", "empirical", [("n", f"{NS}#N")]),
81
+ _rel(NS, "big", "institutional", [("n", f"{NS}#N")]),
82
+ _rule(NS, "Big", [(0, f"{NS}#N")],
83
+ [{"kind": "comparison", "op": "gt", "left": _var(0),
84
+ "right": {"kind": "value", "type": {"name": INT}, "value": 3}},
85
+ _established(_lit(f"{NS}#r", [_var(0)]))],
86
+ _lit(f"{NS}#big", [_var(0)]))]
87
+ planner = P.Builder(_ir(nodes))
88
+ planner._index()
89
+ self.assertEqual(planner._t(f"{NS}#N"), NATURAL)
90
+ self.assertEqual(planner.ts(f"{NS}#N"), "dec")
91
+ P.build_plan(_ir(nodes))
92
+
93
+ def test_alias_chain_to_entity_and_alias_to_enum(self):
94
+ nodes = [_entity(NS, "Person"), _alias(NS, "Party", f"test.alias#Person"),
95
+ _alias(NS, "Claimant", f"test.alias#Party"),
96
+ _enum(NS, "Color", ["Red", "Blue"]), _alias(NS, "Hue", f"test.alias#Color"),
97
+ _rel(NS, "party", "empirical", [("p", f"{NS}#Party"), ("c", f"{NS}#Hue")]),
98
+ _rel(NS, "claimant", "institutional", [("p", f"{NS}#Claimant")]),
99
+ _rule(NS, "Claim", [(0, f"{NS}#Claimant")],
100
+ [_established(_lit(f"{NS}#party", [_var(0), {"kind": "const_ref", "id": f"{NS}#Red"}]))],
101
+ _lit(f"{NS}#claimant", [_var(0)]))]
102
+ planner = P.Builder(_ir(nodes))
103
+ planner._index()
104
+ self.assertEqual(planner._t(f"{NS}#Claimant"), f"{NS}#Person")
105
+ self.assertEqual(planner.ts(f"{NS}#Claimant"), "text")
106
+ self.assertEqual(planner.ts(f"{NS}#Hue"), "enum")
107
+ P.build_plan(_ir(nodes))
108
+ # Член чужого перечисления в слоте алиаса — тот же отказ §189, что у прямого имени.
109
+ bad = list(nodes)
110
+ bad[3] = _enum(NS, "Color", ["Red", "Blue"])
111
+ bad.append(_enum(NS, "Shape", ["Round"]))
112
+ bad[-2] = _rule(NS, "Claim", [(0, f"{NS}#Claimant")],
113
+ [_established(_lit(f"{NS}#party", [_var(0), {"kind": "const_ref", "id": f"{NS}#Round"}]))],
114
+ _lit(f"{NS}#claimant", [_var(0)]))
115
+ with self.assertRaisesRegex(P.Refused, "§189"):
116
+ P.build_plan(_ir(bad))
117
+
118
+ def test_alias_to_world_entity_and_unresolved_target(self):
119
+ world = _ir([_entity(WNS, "Thing")], ns=WNS, name="test.alias_world")
120
+ nodes = [_alias(NS, "Item", "test.alias_world#Thing"),
121
+ _rel(NS, "owned", "empirical", [("i", f"{NS}#Item")]),
122
+ _rel(NS, "listed", "institutional", [("i", f"{NS}#Item")]),
123
+ _rule(NS, "List", [(0, f"{NS}#Item")],
124
+ [_established(_lit(f"{NS}#owned", [_var(0)]))], _lit(f"{NS}#listed", [_var(0)]))]
125
+ ir = _ir(nodes, imports=[{"package": {"name": "test.alias_world", "version": "0.1.0"}}])
126
+ planner = P.Builder(ir, [world])
127
+ planner._index()
128
+ self.assertEqual(planner._t(f"{NS}#Item"), f"{WNS}#Thing")
129
+ P.build_plan(ir, world=[world])
130
+ # Без снимка пакета цели имя остаётся строкой и называется в отказе.
131
+ with self.assertRaisesRegex(P.Refused, "тип аргумента вне среза генератора: test.alias_world#Thing"):
132
+ P.build_plan(_ir(nodes))
133
+
134
+ def test_std_actor_is_an_entity(self):
135
+ nodes = [_rel(NS, "owns", "empirical", [("a", ACTOR), ("t", TEXT)]),
136
+ _rel(NS, "holder", "institutional", [("a", ACTOR)]),
137
+ _rule(NS, "Holder", [(0, ACTOR), (1, TEXT)],
138
+ [_established(_lit(f"{NS}#owns", [_var(0), _var(1)]))], _lit(f"{NS}#holder", [_var(0)]))]
139
+ planner = P.Builder(_ir(nodes))
140
+ planner._index()
141
+ self.assertEqual(planner.ts(ACTOR), "text")
142
+ self.assertEqual(planner.ts("urn:law:std#Court"), "text")
143
+ P.build_plan(_ir(nodes))
144
+ # W2 часть 3: событие в слоте — идентичность (§79.1: `entity_ref` на снимок);
145
+ # конструктор-запись отвергает раннер, не план.
146
+ self.assertEqual(planner.ts("urn:law:std#Event"), "text")
147
+ P.build_plan(_ir([_rel(NS, "happened", "empirical", [("e", "urn:law:std#Event")]),
148
+ _rel(NS, "noted", "institutional", [("e", "urn:law:std#Event")]),
149
+ _rule(NS, "Note", [(0, "urn:law:std#Event")],
150
+ [_established(_lit(f"{NS}#happened", [_var(0)]))],
151
+ _lit(f"{NS}#noted", [_var(0)]))]))
152
+ # Релиз 0.1.5: LegalContext, LegalRole, Jurisdiction — идентичности.
153
+ for std in ("LegalContext", "LegalRole", "Jurisdiction"):
154
+ self.assertEqual(planner.ts(f"urn:law:std#{std}"), "text")
155
+ P.build_plan(_ir([_rel(NS, "within", "empirical", [("c", "urn:law:std#LegalContext"),
156
+ ("j", "urn:law:std#Jurisdiction")]),
157
+ _rel(NS, "known", "institutional", [("c", "urn:law:std#LegalContext")]),
158
+ _rule(NS, "Known", [(0, "urn:law:std#LegalContext"), (1, "urn:law:std#Jurisdiction")],
159
+ [_established(_lit(f"{NS}#within", [_var(0), _var(1)]))],
160
+ _lit(f"{NS}#known", [_var(0)]))]))
161
+ # Сертифицированные границы — своё значение §259.2, вне среза.
162
+ with self.assertRaisesRegex(P.Refused, "тип аргумента вне среза генератора: urn:law:std#Bounds"):
163
+ P.build_plan(_ir([_rel(NS, "measured", "empirical", [("b", "urn:law:std#Bounds")]),
164
+ _rel(NS, "fine", "institutional", [("b", "urn:law:std#Bounds")]),
165
+ _rule(NS, "Fine", [(0, "urn:law:std#Bounds")],
166
+ [_established(_lit(f"{NS}#measured", [_var(0)]))],
167
+ _lit(f"{NS}#fine", [_var(0)]))]))
168
+
169
+
170
+ if __name__ == "__main__":
171
+ unittest.main()