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,565 @@
1
+ #!/usr/bin/env python3
2
+ """Устанавливаемый пакет порождённой библиотеки (DECISION-0423 §2.7, шаг 5).
3
+
4
+ python3 package.py <ir.json> --target py --name NAME --version X.Y.Z --out DIR
5
+ python3 package.py <ir.json> --target ts --name @scope/name --version X.Y.Z --out DIR
6
+ python3 package.py <ir.json> --target go --name NAME --version X.Y.Z \\
7
+ --module example.com/org/name --out DIR
8
+
9
+ Модель, рантайм и API печатают те же эмиттеры, что и поставка `deliver.py`:
10
+ пакет меняет раскладку и импорты, но не семантику. Рантайм и модель лежат во
11
+ внутреннем подкаталоге (TypeScript — `src/internal/`, Python — `_internal/`;
12
+ в Go приватность даёт регистр имён, поэтому код — в корневом пакете модуля).
13
+ Наружу выходят функции API, `info()` с паспортом поставки и машинная карта
14
+ `api-map.json` «функция → вопрос или вход → StableId → правила → якоря
15
+ источника». Раннер и план ворот остаются в проверочном комплекте; у Go есть
16
+ команда `cmd/<пакет>-eval` с протоколом раннера (ответ — код 0, названный
17
+ отказ — `{"refused": …}` и код 3).
18
+
19
+ Имя пакета — параметр. Для канонов, которые публикует Arxo, принята семья
20
+ `-lib` (DECISION-0423 §7, выбор владельца): `@arxo/canon-<имя>-lib` (npm),
21
+ `arxo-canon-<имя>-lib` (PyPI), `github.com/arxohq/canon-<имя>-lib` (Go);
22
+ `@arxo/canon-<имя>` без суффикса — пакет самого канона (DECISION-0349 §8). Минимальные версии тулчейнов — константы целей
23
+ `TOOLCHAIN_MIN`, равные версиям проверок CI. TypeScript собирается при
24
+ упаковке собственным тулчейном цели: пакет без `dist/` не устанавливается.
25
+ """
26
+ from __future__ import annotations
27
+
28
+ import argparse
29
+ import hashlib
30
+ import json
31
+ import re
32
+ import shutil
33
+ import subprocess
34
+ import sys
35
+ import tempfile
36
+ from pathlib import Path
37
+
38
+ HERE = Path(__file__).resolve().parent
39
+ sys.path.insert(0, str(HERE))
40
+ import deliver as D # noqa: E402
41
+ import plan as P # noqa: E402
42
+ from core_support import CoreKitError # noqa: E402
43
+
44
+ FORMAT = "law.codegen-package/0.1"
45
+ MAP_FORMAT = "law.codegen-api-map/0.1"
46
+ # Версия ABI поверхности API (DECISION-0423 §2.8): имена функций по языку,
47
+ # `Answer`, ошибки, конструкторы литерал-термов. Меняется вместе с
48
+ # несовместимой правкой поверхности, а не с версией генератора.
49
+ API_ABI = "law.codegen-api/1"
50
+ VALUE_FORMAT = "law.codegen-value/1"
51
+ TARGETS = ("py", "ts", "go")
52
+ EXAMPLE_DATE = "2026-01-01"
53
+
54
+ # Строки импортов плоской раскладки и их замены в пакете. Каждая строка
55
+ # обязана встретиться ровно один раз: незнакомая форма эмиттера — отказ
56
+ # упаковки, а не молча непереписанный импорт.
57
+ PY_REWRITES = {
58
+ "model.py": [("import runtime as RT", "from . import runtime as RT")],
59
+ "evaluate.py": [("import runtime as RT", "from . import runtime as RT"),
60
+ ("import model as _MODEL", "from . import model as _MODEL")],
61
+ "api_base.py": [("import runtime as RT", "from . import runtime as RT"),
62
+ ("from evaluate import evaluate as _evaluate", "from .evaluate import evaluate as _evaluate")],
63
+ "api.py": [("from api_base import (", "from ._internal.api_base import ("),
64
+ ("import api_base as _base", "from ._internal import api_base as _base")],
65
+ }
66
+ PY_FLAT_IMPORT = re.compile(r"^\s*(import|from)\s+(runtime|model|evaluate|api_base|api)\b", re.M)
67
+ TS_INTERNAL = ("runtime.ts", "evaluate.ts", "apiBase.ts")
68
+ GO_FILES = ("runtime.go", "evaluate.go", "apiBase.go")
69
+
70
+
71
+ class PackageError(Exception):
72
+ """Упаковка невозможна: названная причина, пакет не пишется."""
73
+
74
+
75
+ def sha(data: bytes) -> str:
76
+ return "sha256:" + hashlib.sha256(data).hexdigest()
77
+
78
+
79
+ def rewrite(text: str, pairs: list, where: str) -> str:
80
+ lines = text.split("\n")
81
+ for old, new in pairs:
82
+ hits = [i for i, line in enumerate(lines) if line.startswith(old)]
83
+ if len(hits) != 1:
84
+ raise PackageError(f"{where}: ожидалась одна строка «{old}», найдено {len(hits)}")
85
+ lines[hits[0]] = new + lines[hits[0]][len(old):]
86
+ return "\n".join(lines)
87
+
88
+
89
+ def generator_version() -> str:
90
+ """Версия генератора: `pyproject.toml` рядом — исходники, иначе метаданные
91
+ установленного дистрибутива."""
92
+ pyproject = HERE / "pyproject.toml"
93
+ if pyproject.is_file():
94
+ import tomllib
95
+ return tomllib.loads(pyproject.read_text(encoding="utf-8"))["project"]["version"]
96
+ from importlib.metadata import version
97
+ return version("arxo-codegen")
98
+
99
+
100
+ def core_kit_digest() -> str | None:
101
+ """SHA-256 манифеста настроенного комплекта ядра (у исходников без комплекта — нет)."""
102
+ import core_support
103
+ kit = core_support.core_kit_path()
104
+ return sha((Path(kit) / "manifest.json").read_bytes()) if kit else None
105
+
106
+
107
+ def go_package_name(ir: dict) -> str:
108
+ """Имя пакета Go по умолчанию — последний сегмент имени канона без знаков."""
109
+ tail = str((ir.get("package") or {}).get("name") or "canon").split(".")[-1]
110
+ name = re.sub(r"[^a-z0-9]", "", tail.lower()) or "canon"
111
+ return name if not name[0].isdigit() else "p" + name
112
+
113
+
114
+ def py_import_name(name: str) -> str:
115
+ """Имя импорта Python по имени дистрибутива (PEP 503: `-`, `.` → `_`)."""
116
+ base = re.sub(r"[^A-Za-z0-9]+", "_", name.split("/")[-1]).strip("_").lower() or "canon"
117
+ return base if not base[0].isdigit() else "p" + base
118
+
119
+
120
+ # --- карта API и пример ------------------------------------------------------
121
+
122
+ def function_names(lang: str, em, api: dict) -> tuple[dict, dict]:
123
+ """Имена функций цели: вход → (факт, отрицательный факт), вопрос → (вопрос, строки)."""
124
+ inputs, questions = em.api_names(api["inputs"]), em.api_names(api["questions"])
125
+ if lang == "py":
126
+ fi = {k: (f"fact_{v}", f"negated_{v}") for k, v in inputs.items()}
127
+ fq = {k: (f"ask_{v}", f"rows_{v}") for k, v in questions.items()}
128
+ elif lang == "ts":
129
+ fi = {k: (f"facts.{v}", f"negated.{v}") for k, v in inputs.items()}
130
+ fq = {k: (f"ask.{v}", f"rows.{v}") for k, v in questions.items()}
131
+ else:
132
+ fi = {k: (f"Fact{v}", f"Negated{v}") for k, v in inputs.items()}
133
+ fq = {k: (f"Ask{v}", f"Rows{v}") for k, v in questions.items()}
134
+ return fi, fq
135
+
136
+
137
+ def api_map(ir: dict, world: list, pl: dict, lang: str, em, distribution: dict, digest: str) -> dict:
138
+ api = pl.get("api") or {"questions": [], "inputs": []}
139
+ negation = "negation" in (pl.get("capabilities") or ())
140
+ rule_ids = {P._slug(n["id"]): n["id"] for doc in [ir, *world] for n in doc.get("nodes", [])
141
+ if n.get("kind") == "rule"}
142
+ fi, fq = function_names(lang, em, api)
143
+ functions = []
144
+ for q in api["questions"]:
145
+ functions.append({
146
+ "kind": "question", "function": fq[q["name"]][0], "rowsFunction": fq[q["name"]][1],
147
+ "name": q["name"], "predicate": q["pred"], "params": q["params"], "labels": q.get("labels", []),
148
+ "inputs": q.get("inputs", []),
149
+ "rules": [{"id": rule_ids.get(s["rule"], s["rule"]), "labels": s.get("labels", []),
150
+ "anchors": s.get("anchors", [])} for s in q.get("sources", [])]})
151
+ for i in api["inputs"]:
152
+ entry = {"kind": "input", "function": fi[i["name"]][0], "name": i["name"], "predicate": i["pred"],
153
+ "params": i["params"], "labels": i.get("labels", [])}
154
+ if negation:
155
+ entry["negatedFunction"] = fi[i["name"]][1]
156
+ functions.append(entry)
157
+ return {"format": MAP_FORMAT, "target": lang, "package": ir.get("package"), "semanticHash": digest,
158
+ "distribution": distribution, "functions": functions}
159
+
160
+
161
+ def _example_value(param: str, kind: str, lang: str, q: str = "") -> str | None:
162
+ """Литерал примера по объявленному типу; сущность одного типа — один id,
163
+ чтобы факты и вопрос говорили об одном объекте."""
164
+ local = re.sub(r"[^A-Za-z0-9]+", "-", param.split("#")[-1]).strip("-").lower() or "x"
165
+ go = lang == "go"
166
+ if kind == "text" and param == "urn:law:std#Text":
167
+ return f'{q}TextOf("пример")' if go else 'text("пример")'
168
+ if kind == "text":
169
+ return f'{q}EntityOf("urn:example:{local}-1")' if go else f'entity("urn:example:{local}-1")'
170
+ if kind == "money":
171
+ return f'{q}MoneyOf("100", "KZT")' if go else 'money("100", "KZT")'
172
+ if kind == "dec" and param == "urn:law:std#Integer":
173
+ return f"{q}IntegerOf(10)" if go else "integer(10)"
174
+ if kind == "dec" and param == "urn:law:std#Natural":
175
+ return f"{q}NaturalOf(10)" if go else "natural(10)"
176
+ if kind == "dec":
177
+ return f'{q}DecimalOf("10")' if go else 'decimal("10")'
178
+ if kind == "date":
179
+ return f'{q}DateOf("{EXAMPLE_DATE}")' if go else f'date("{EXAMPLE_DATE}")'
180
+ if kind == "bool":
181
+ return {"py": "boolean(True)", "ts": "boolean(true)", "go": f"{q}BooleanOf(true)"}[lang]
182
+ return None
183
+
184
+
185
+ def _example_args(entry: dict, lang: str, q: str = "") -> list | None:
186
+ out = []
187
+ for param, kind in zip(entry["params"], entry["kinds"]):
188
+ value = _example_value(param, kind, lang, q)
189
+ if value is None:
190
+ return None
191
+ out.append(value)
192
+ return out
193
+
194
+
195
+ def example(pl: dict, lang: str, em, import_path: str) -> str | None:
196
+ """Исполняемый пример README: факты по входам первого вопроса с
197
+ синтезируемыми аргументами и сам вопрос; приёмка запускает его как есть."""
198
+ api = pl.get("api") or {"questions": [], "inputs": []}
199
+ fi, fq = function_names(lang, em, api)
200
+ inputs = {i["pred"]: i for i in api["inputs"]}
201
+ for q in api["questions"]:
202
+ q_args = _example_args(q, lang, "lib." if lang == "go" else "")
203
+ if q_args is None:
204
+ continue
205
+ facts = []
206
+ for pred in q.get("inputs", []):
207
+ i = inputs.get(pred)
208
+ args = _example_args(i, lang, "lib." if lang == "go" else "") if i else None
209
+ if args is not None:
210
+ facts.append((fi[i["name"]][0], args))
211
+ if len(facts) == 6:
212
+ break
213
+ used = sorted({re.match(r"(?:lib\.)?(\w+)\(", a).group(1) for _f, args in facts for a in args}
214
+ | {re.match(r"(?:lib\.)?(\w+)\(", a).group(1) for a in q_args})
215
+ ask = fq[q["name"]][0]
216
+ if lang == "py":
217
+ names = sorted({ask, *(f for f, _a in facts)}) + used
218
+ lines = [f"from {import_path} import {', '.join(names)}", "", "facts = ["]
219
+ lines += [f" {f}({', '.join(a)})," for f, a in facts]
220
+ lines += ["]", f'answer = {ask}(facts, "{EXAMPLE_DATE}"{"".join(", " + a for a in q_args)})',
221
+ "print(answer.truth_status, answer.evaluation_status)"]
222
+ return "\n".join(lines)
223
+ if lang == "ts":
224
+ names = ["ask", *(["facts"] if facts else [])] + used
225
+ lines = [f'import {{ {", ".join(names)} }} from "{import_path}";', "", "const known = ["]
226
+ lines += [f" {f}({', '.join(a)})," for f, a in facts]
227
+ lines += ["];", f'const answer = {ask}(known, "{EXAMPLE_DATE}"{"".join(", " + a for a in q_args)});',
228
+ 'if (answer.resultKind === "PROPOSITION") console.log(answer.truthStatus, answer.evaluationStatus);']
229
+ return "\n".join(lines)
230
+ lines = ["package main", "", "import (", '\t"fmt"', "", f'\tlib "{import_path}"', ")", "",
231
+ "func main() {", "\tfacts := []lib.Fact{"]
232
+ lines += [f"\t\tlib.{f}({', '.join(a)})," for f, a in facts]
233
+ lines += ["\t}", f'\tanswer, err := lib.{ask}(facts, "{EXAMPLE_DATE}"{"".join(", " + a for a in q_args)})',
234
+ "\tif err != nil {", "\t\tpanic(err)", "\t}",
235
+ "\tfmt.Println(answer.TruthStatus, answer.EvaluationStatus)", "}"]
236
+ return "\n".join(lines)
237
+ return None
238
+
239
+
240
+ FENCE = {"py": "python", "ts": "ts", "go": "go"}
241
+
242
+
243
+ def readme(ir: dict, lang: str, name: str, version: str, install: str, sample: str | None, mapping: dict) -> str:
244
+ package = ir.get("package") or {}
245
+ canon = f"{package.get('name')}@{package.get('version')}"
246
+ lines = [f"# {name}", "",
247
+ f"Исполняемая модель канона `{canon}` без движка правил: модуль порождён "
248
+ f"arxo-codegen из CLIR канона (профиль `core-executable`, DECISION-0188). "
249
+ "Ответ — статус истинности §62 и статус вычисления §175; доказательства и "
250
+ "хэшей воспроизводимости нет — за ними движок Arxo. Конструкция вне среза "
251
+ "генератора — названный отказ, а не другой ответ.", "",
252
+ "## Установка", "", "```sh", install, "```", ""]
253
+ if sample:
254
+ lines += ["## Пример", "", f"```{FENCE[lang]}", sample, "```", ""]
255
+ lines += ["## Функции", "", "| Функция | Вид | StableId |", "|---|---|---|"]
256
+ for f in mapping["functions"]:
257
+ kind = "вопрос" if f["kind"] == "question" else "вход"
258
+ lines.append(f"| `{f['function']}` | {kind} | `{f['predicate']}` |")
259
+ lines += ["", "Карта «функция → вопрос или вход → StableId → правила → якоря источника» — "
260
+ "`api-map.json`; паспорт поставки (канон, семантический хэш, мир, файлы) — "
261
+ "`info()`.", ""]
262
+ return "\n".join(lines)
263
+
264
+
265
+ # --- раскладки целей ---------------------------------------------------------
266
+
267
+ def _emit(pl: dict, em) -> tuple[str, str]:
268
+ return em.emit(pl), em.emit_api(pl)
269
+
270
+
271
+ def layout_py(root: Path, em, pl: dict, name: str, version: str, ir: dict) -> dict:
272
+ imp = py_import_name(name)
273
+ pkg = root / "src" / imp
274
+ internal = pkg / "_internal"
275
+ internal.mkdir(parents=True)
276
+ model, api = _emit(pl, em)
277
+ sources = {n: (Path(em.__file__).parent / n).read_text(encoding="utf-8")
278
+ for n in ("runtime.py", "evaluate.py", "api_base.py")}
279
+ sources["model.py"] = model
280
+ for n, text in sources.items():
281
+ text = rewrite(text, PY_REWRITES.get(n, []), n)
282
+ if PY_FLAT_IMPORT.search(text):
283
+ raise PackageError(f"{n}: остался импорт плоской раскладки")
284
+ (internal / n).write_text(text, encoding="utf-8")
285
+ (internal / "__init__.py").write_text('"""Внутренняя часть пакета: рантайм, модель, единый вход."""\n',
286
+ encoding="utf-8")
287
+ api = rewrite(api, PY_REWRITES["api.py"], "api.py")
288
+ if PY_FLAT_IMPORT.search(api):
289
+ raise PackageError("api.py: остался импорт плоской раскладки")
290
+ (pkg / "api.py").write_text(api, encoding="utf-8")
291
+ (pkg / "py.typed").write_text("", encoding="utf-8")
292
+ package = ir.get("package") or {}
293
+ description = f"Исполняемая модель канона {package.get('name')} без движка (arxo-codegen)"
294
+ (pkg / "__init__.py").write_text("\n".join([
295
+ f'"""{name}: исполняемая модель канона {package.get("name")}@{package.get("version")} без движка.',
296
+ "",
297
+ "Функции `fact_*`, `ask_*`, `rows_*` и реестры `QUESTIONS`, `INPUTS` — из `api`;",
298
+ '`info()` отдаёт паспорт поставки (law.backend-delivery/1)."""',
299
+ "from .api import * # noqa: F401,F403",
300
+ "from .api import INPUTS, NEGATED, QUESTIONS # noqa: F401",
301
+ "",
302
+ "",
303
+ "def info() -> dict:",
304
+ ' """Паспорт поставки: канон, семантический хэш §209, мир, профиль, файлы."""',
305
+ " from ._internal.passport import passport",
306
+ " return passport()",
307
+ ""]), encoding="utf-8")
308
+ (root / "pyproject.toml").write_text("\n".join([
309
+ "[build-system]",
310
+ 'requires = ["setuptools>=65"]',
311
+ 'build-backend = "setuptools.build_meta"',
312
+ "",
313
+ "[project]",
314
+ f"name = {json.dumps(name)}",
315
+ f"version = {json.dumps(version)}",
316
+ f"description = {json.dumps(description, ensure_ascii=False)}",
317
+ 'readme = "README.md"',
318
+ f'requires-python = ">={em.TOOLCHAIN_MIN}"',
319
+ "dependencies = []",
320
+ "",
321
+ "[tool.setuptools.packages.find]",
322
+ 'where = ["src"]',
323
+ "",
324
+ "[tool.setuptools.package-data]",
325
+ f'{json.dumps(imp)} = ["py.typed", "api-map.json"]',
326
+ ""]), encoding="utf-8")
327
+ return {"import": imp, "install": f"pip install {name}", "api_map": pkg / "api-map.json",
328
+ "passport": lambda text: (internal / "passport.py").write_text(
329
+ '"""Паспорт поставки (law.backend-delivery/1), вшитый при упаковке."""\n'
330
+ "import json as _json\n\n"
331
+ f"_PASSPORT = {text!r}\n\n\n"
332
+ "def passport() -> dict:\n"
333
+ " return _json.loads(_PASSPORT)\n", encoding="utf-8"),
334
+ "unhashed": {f"src/{imp}/_internal/passport.py"}}
335
+
336
+
337
+ def layout_ts(root: Path, em, pl: dict, name: str, version: str, ir: dict) -> dict:
338
+ src = root / "src"
339
+ internal = src / "internal"
340
+ internal.mkdir(parents=True)
341
+ model, api = _emit(pl, em)
342
+ for n in TS_INTERNAL:
343
+ shutil.copyfile(Path(em.__file__).parent / n, internal / n)
344
+ (internal / "model.ts").write_text(model, encoding="utf-8")
345
+ if api.count('from "./apiBase"') != 3:
346
+ raise PackageError('api.ts: ожидались три импорта из "./apiBase"')
347
+ api = api.replace('from "./apiBase"', 'from "./internal/apiBase"')
348
+ if re.search(r'from "\./(runtime|model|evaluate)"', api):
349
+ raise PackageError("api.ts: остался импорт плоской раскладки")
350
+ (src / "api.ts").write_text(api, encoding="utf-8")
351
+ package = ir.get("package") or {}
352
+ (src / "index.ts").write_text("\n".join([
353
+ f"// {name}: исполняемая модель канона {package.get('name')}@{package.get('version')} без движка.",
354
+ "// Функции facts.*, ask.*, rows.* и реестры — из ./api; info() — паспорт поставки.",
355
+ 'export * from "./api";',
356
+ 'import { PASSPORT } from "./internal/passport";',
357
+ "",
358
+ "/** Паспорт поставки (law.backend-delivery/1): канон, семантический хэш §209, мир, файлы. */",
359
+ "export function info(): Record<string, unknown> {",
360
+ " return JSON.parse(PASSPORT);",
361
+ "}",
362
+ ""]), encoding="utf-8")
363
+ (root / "tsconfig.json").write_text(json.dumps({"compilerOptions": {
364
+ "target": "es2022", "module": "commonjs", "lib": ["es2022"], "types": [], "strict": True,
365
+ "declaration": True, "rootDir": "src", "outDir": "dist"}, "include": ["src"]}, indent=2) + "\n",
366
+ encoding="utf-8")
367
+ (root / "package.json").write_text(json.dumps({
368
+ "name": name, "version": version,
369
+ "description": f"Исполняемая модель канона {package.get('name')} без движка (arxo-codegen)",
370
+ "main": "dist/index.js", "types": "dist/index.d.ts",
371
+ "exports": {".": {"types": "./dist/index.d.ts", "default": "./dist/index.js"},
372
+ "./api-map.json": "./api-map.json", "./package.json": "./package.json"},
373
+ "files": ["dist", "src", "api-map.json", "law-backend.json", "README.md"],
374
+ "engines": {"node": f">={em.TOOLCHAIN_MIN}"}, "sideEffects": False},
375
+ ensure_ascii=False, indent=2) + "\n", encoding="utf-8")
376
+
377
+ def passport(text: str) -> None:
378
+ (internal / "passport.ts").write_text(
379
+ "// Паспорт поставки (law.backend-delivery/1), вшитый при упаковке.\n"
380
+ f"export const PASSPORT = {json.dumps(text, ensure_ascii=False)};\n", encoding="utf-8")
381
+ return {"import": name, "install": f"npm install {name}", "api_map": root / "api-map.json",
382
+ "passport": passport, "build": True,
383
+ "unhashed": {"src/internal/passport.ts", "dist/internal/passport.js", "dist/internal/passport.d.ts"}}
384
+
385
+
386
+ def layout_go(root: Path, em, pl: dict, name: str, version: str, ir: dict, module: str, gopkg: str) -> dict:
387
+ model, api = _emit(pl, em)
388
+ files = {n: (Path(em.__file__).parent / n).read_text(encoding="utf-8") for n in GO_FILES}
389
+ files["model.go"], files["api.go"] = model, api
390
+ for n, text in files.items():
391
+ if len(re.findall(r"^package main$", text, re.M)) != 1:
392
+ raise PackageError(f"{n}: ожидалась одна строка «package main»")
393
+ (root / n).write_text(re.sub(r"^package main$", f"package {gopkg}", text, count=1, flags=re.M),
394
+ encoding="utf-8")
395
+ (root / "eval.go").write_text("\n".join([
396
+ f"package {gopkg}", "",
397
+ "// EvaluateJSON — единый вход модели (DECISION-0423 §2.2): дело и вопрос в",
398
+ "// канонической форме §168/§172 → ответ профиля core-executable в JSON.",
399
+ "// Названный отказ среза — ошибка *RefusedError.",
400
+ "func EvaluateJSON(caseJSON, queryJSON []byte) (string, error) {",
401
+ "\treturn evaluate(caseJSON, queryJSON)",
402
+ "}", ""]), encoding="utf-8")
403
+ (root / "go.mod").write_text(f"module {module}\n\ngo {em.TOOLCHAIN_MIN}\n", encoding="utf-8")
404
+ command = root / "cmd" / f"{gopkg}-eval"
405
+ command.mkdir(parents=True)
406
+ (command / "main.go").write_text("\n".join([
407
+ f"// Команда {gopkg}-eval исполняет дело и вопрос канонической формы §168/§172",
408
+ "// и печатает ответ профиля core-executable: код 0 — ответ, код 3 — названный",
409
+ '// отказ среза в виде {"refused": текст}, как у раннера проверочного комплекта.',
410
+ "package main", "",
411
+ "import (", '\t"encoding/json"', '\t"errors"', '\t"fmt"', '\t"os"', "",
412
+ f'\tlib "{module}"', ")", "",
413
+ "func main() {",
414
+ "\tif len(os.Args) != 3 {",
415
+ f'\t\tfmt.Fprintln(os.Stderr, "использование: {gopkg}-eval дело.json вопрос.json")',
416
+ "\t\tos.Exit(2)",
417
+ "\t}",
418
+ "\tkase, err := os.ReadFile(os.Args[1])",
419
+ "\tif err != nil {", "\t\tfmt.Fprintln(os.Stderr, err)", "\t\tos.Exit(2)", "\t}",
420
+ "\tquery, err := os.ReadFile(os.Args[2])",
421
+ "\tif err != nil {", "\t\tfmt.Fprintln(os.Stderr, err)", "\t\tos.Exit(2)", "\t}",
422
+ "\tanswer, err := lib.EvaluateJSON(kase, query)",
423
+ "\tvar refused *lib.RefusedError",
424
+ "\tif errors.As(err, &refused) {",
425
+ '\t\tout, _ := json.Marshal(map[string]string{"refused": refused.Message})',
426
+ "\t\tfmt.Println(string(out))",
427
+ "\t\tos.Exit(3)",
428
+ "\t}",
429
+ "\tif err != nil {", "\t\tfmt.Fprintln(os.Stderr, err)", "\t\tos.Exit(1)", "\t}",
430
+ "\tfmt.Println(answer)",
431
+ "}", ""]), encoding="utf-8")
432
+
433
+ def passport(text: str) -> None:
434
+ (root / "passport.go").write_text("\n".join([
435
+ f"package {gopkg}", "",
436
+ "// Паспорт поставки (law.backend-delivery/1), вшитый при упаковке.",
437
+ f"const passportJSON = {json.dumps(text, ensure_ascii=False)}", "",
438
+ "// Info возвращает паспорт поставки: канон, семантический хэш §209, мир, файлы (JSON).",
439
+ "func Info() string { return passportJSON }", ""]), encoding="utf-8")
440
+ return {"import": module, "install": f"go get {module}@v{version}", "api_map": root / "api-map.json",
441
+ "passport": passport, "unhashed": {"passport.go"}}
442
+
443
+
444
+ # --- сборка пакета -----------------------------------------------------------
445
+
446
+ def build_ts(root: Path, em) -> None:
447
+ modules = em.toolchain()
448
+ if modules is None:
449
+ raise PackageError("тулчейн TypeScript недоступен: пакет без dist/ не устанавливается")
450
+ result = subprocess.run([str(modules / ".bin" / "tsc"), "-p", str(root / "tsconfig.json")],
451
+ capture_output=True, text=True, check=False)
452
+ if result.returncode:
453
+ raise PackageError("сборка TypeScript отказала:\n" + (result.stdout + result.stderr)[-4000:])
454
+
455
+
456
+ def package(ir: dict, lang: str, out: Path, *, name: str, version: str, module: str | None = None,
457
+ go_package: str | None = None, world: list | None = None, resources: dict | None = None) -> dict:
458
+ """Пакет одной цели в новый каталог `out`; возвращает паспорт."""
459
+ if lang not in TARGETS:
460
+ raise PackageError(f"цель {lang}: пакет печатается только для {', '.join(TARGETS)}")
461
+ if not re.fullmatch(r"\d+\.\d+\.\d+(?:[-+][0-9A-Za-z.-]+)?", version):
462
+ raise PackageError(f"версия пакета не по SemVer: {version}")
463
+ if lang == "go" and not module:
464
+ raise PackageError("цель go: нужен --module (путь модуля Go)")
465
+ out = Path(out)
466
+ if out.exists():
467
+ raise PackageError(f"каталог пакета уже существует: {out}")
468
+ out.parent.mkdir(parents=True, exist_ok=True)
469
+ em = D.emitter(lang)
470
+ pl = P.build_plan(ir, world, resources=resources)
471
+ missing = D.missing_capabilities(pl, em)
472
+ if missing:
473
+ raise P.Refused(f"цель {lang} не поддерживает возможности плана: {', '.join(missing)}")
474
+ digest = D.semantic_hash(ir)
475
+ gopkg = go_package or go_package_name(ir)
476
+ distribution = {"format": FORMAT, "name": name, "version": version}
477
+ if lang == "go":
478
+ distribution.update(module=module, package=gopkg)
479
+ with tempfile.TemporaryDirectory(prefix="codegen-package-", dir=out.parent) as temp:
480
+ root = Path(temp) / "package"
481
+ root.mkdir()
482
+ if lang == "py":
483
+ spec = layout_py(root, em, pl, name, version, ir)
484
+ elif lang == "ts":
485
+ spec = layout_ts(root, em, pl, name, version, ir)
486
+ else:
487
+ spec = layout_go(root, em, pl, name, version, ir, module, gopkg)
488
+ mapping = api_map(ir, world or [], pl, lang, em, distribution, digest)
489
+ mapping_text = json.dumps(mapping, ensure_ascii=False, indent=1) + "\n"
490
+ spec["api_map"].write_text(mapping_text, encoding="utf-8")
491
+ if lang == "py":
492
+ (root / "api-map.json").write_text(mapping_text, encoding="utf-8")
493
+ sample = example(pl, lang, em, spec["import"])
494
+ (root / "README.md").write_text(readme(ir, lang, name, version, spec["install"], sample, mapping),
495
+ encoding="utf-8")
496
+ # Паспорт вшивается дважды одним текстом: заглушка до сборки (dist/
497
+ # TypeScript должен её видеть), настоящий — после хэширования файлов.
498
+ spec["passport"]("{}")
499
+ if spec.get("build"):
500
+ build_ts(root, em)
501
+ files = {p.relative_to(root).as_posix(): sha(p.read_bytes())
502
+ for p in sorted(root.rglob("*")) if p.is_file()
503
+ and p.relative_to(root).as_posix() not in spec["unhashed"]}
504
+ passport = {"format": D.FORMAT, "planVersion": pl["planVersion"], "target": lang,
505
+ "package": ir.get("package"), "semanticHash": digest, "world": pl.get("world", []),
506
+ "profile": getattr(em, "PROFILE", "core-executable"), "distribution": distribution,
507
+ # DECISION-0423 §2.8: провенанс артефакта, а не доказательство вычисления.
508
+ "generator": {"name": "arxo-codegen", "version": generator_version()},
509
+ "coreKit": {"manifestSha256": core_kit_digest()},
510
+ "capabilities": sorted(pl.get("capabilities") or ()),
511
+ "api": {"abi": API_ABI, "valueFormat": VALUE_FORMAT},
512
+ "files": files}
513
+ text = json.dumps(passport, ensure_ascii=False, indent=1) + "\n"
514
+ spec["passport"](text)
515
+ if spec.get("build"):
516
+ build_ts(root, em)
517
+ (root / "law-backend.json").write_text(text, encoding="utf-8")
518
+ shutil.move(str(root), str(out))
519
+ return passport
520
+
521
+
522
+ def main() -> int:
523
+ ap = argparse.ArgumentParser(description=__doc__.split("\n\n")[0])
524
+ ap.add_argument("ir", type=Path, help="CLIR пакета (*.lawir.json)")
525
+ ap.add_argument("--target", choices=TARGETS, required=True)
526
+ ap.add_argument("--name", required=True, help="имя пакета (npm, PyPI) или его человекочитаемое имя (Go)")
527
+ ap.add_argument("--version", required=True, help="версия пакета по SemVer")
528
+ ap.add_argument("--module", help="путь модуля Go")
529
+ ap.add_argument("--go-package", help="имя пакета Go (по умолчанию — из имени канона)")
530
+ ap.add_argument("--out", type=Path, required=True, help="новый каталог пакета")
531
+ ap.add_argument("--dep", type=Path, action="append", default=[], help="CLIR зависимости (можно повторять)")
532
+ ap.add_argument("--corpus-root", type=Path, help="явный корень corpus/clir и packs (адаптер law-dsl)")
533
+ ap.add_argument("--resources", type=Path, help="JSON: URI → {path, sha256}; пути относительно JSON")
534
+ ap.add_argument("--core-kit", type=Path, help="проверенный комплект поддержки ядра")
535
+ args = ap.parse_args()
536
+ if args.core_kit:
537
+ import os
538
+ os.environ["LAW_CODEGEN_CORE_KIT"] = str(args.core_kit.resolve())
539
+ ir = json.loads(args.ir.read_text(encoding="utf-8"))
540
+ try:
541
+ from inputs import corpus_resources, dependency_index, load_resources
542
+ extra = [json.loads(p.read_text(encoding="utf-8")) for p in args.dep]
543
+ index = P.world_index(args.corpus_root) if args.corpus_root else {}
544
+ dependency_index(extra)
545
+ world = P.resolve_world(ir, index, extra) if ir.get("imports") else []
546
+ resources = load_resources(args.resources) if args.resources else (
547
+ corpus_resources([ir, *world], args.corpus_root) if args.corpus_root else {})
548
+ passport = package(ir, args.target, args.out, name=args.name, version=args.version,
549
+ module=args.module, go_package=args.go_package, world=world, resources=resources)
550
+ except P.Refused as exc:
551
+ print(f"package: отказ плана — {exc}", file=sys.stderr)
552
+ return 2
553
+ except PackageError as exc:
554
+ print(f"package: {exc}", file=sys.stderr)
555
+ return 2
556
+ except CoreKitError as exc:
557
+ print(f"package: ошибка комплекта ядра — {exc}", file=sys.stderr)
558
+ return 1
559
+ print(f"package: {args.target} → {args.out} ({args.name} {args.version}, "
560
+ f"{passport['semanticHash'][:19]}…, файлов {len(passport['files'])})")
561
+ return 0
562
+
563
+
564
+ if __name__ == "__main__":
565
+ raise SystemExit(main())