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
arxo_codegen/check.py ADDED
@@ -0,0 +1,158 @@
1
+ """Run the existing backend harness against a verified exported fixture kit.
2
+
3
+ The harness is transitional: its source remains in law-dsl until ownership is
4
+ switched. Only fixtures exported from a trusted core checkout should be used.
5
+ """
6
+ from __future__ import annotations
7
+
8
+ import argparse
9
+ import hashlib
10
+ import importlib.util
11
+ import json
12
+ import os
13
+ from pathlib import Path
14
+ import sys
15
+
16
+ HERE = Path(__file__).resolve().parent
17
+ sys.path.insert(0, str(HERE))
18
+
19
+ import runtime_cert
20
+
21
+
22
+ def validate_fixtures(root: Path) -> dict:
23
+ root = root.resolve()
24
+ manifest = json.loads((root / "manifest.json").read_text(encoding="utf-8"))
25
+ if manifest.get("format") != "law.codegen-fixtures/0.1":
26
+ raise ValueError("unsupported codegen fixture format")
27
+ seen = set()
28
+ for row in manifest["files"]:
29
+ rel = row["path"]
30
+ path = root / rel
31
+ if (not isinstance(rel, str) or Path(rel).is_absolute() or ".." in Path(rel).parts
32
+ or rel in seen or path.is_symlink() or not path.resolve().is_relative_to(root)):
33
+ raise ValueError("invalid codegen fixture path")
34
+ seen.add(rel)
35
+ data = path.read_bytes()
36
+ if row["sha256"] != "sha256:" + hashlib.sha256(data).hexdigest() or row["bytes"] != len(data):
37
+ raise ValueError(f"codegen fixture hash mismatch: {rel}")
38
+ required = {"verify/ci/gates/backends/check_backends.py", "verify/ci/gates/backends/l1_fixtures.py",
39
+ "runtime/runtime-cert.json", "data/backends-vectors.json"}
40
+ if not required <= seen:
41
+ raise ValueError("incomplete codegen fixture kit")
42
+ actual = {p.relative_to(root).as_posix() for p in root.rglob("*") if p.is_file()}
43
+ if actual != seen | {"manifest.json"}:
44
+ raise ValueError("unlisted codegen fixture files")
45
+ return manifest
46
+
47
+
48
+ def load_harness(root: Path):
49
+ validate_fixtures(root)
50
+ import core_support
51
+ import deliver
52
+ import plan
53
+ owned_harness = HERE / "harness/verify/ci/gates/backends"
54
+ # Once extracted, harness code is owned/versioned with codegen. During
55
+ # transition the exported copy keeps source-checkout invocation working.
56
+ gate_dir = owned_harness if owned_harness.is_dir() else root / "verify/ci/gates/backends"
57
+ old_path = sys.path[:]
58
+ old_bytecode = sys.dont_write_bytecode
59
+ old_fx = sys.modules.pop("l1_fixtures", None)
60
+ try:
61
+ sys.dont_write_bytecode = True
62
+ sys.path.insert(0, str(gate_dir))
63
+ spec = importlib.util.spec_from_file_location("_codegen_fixture_gate", gate_dir / "check_backends.py")
64
+ gate = importlib.util.module_from_spec(spec)
65
+ spec.loader.exec_module(gate)
66
+ finally:
67
+ sys.path[:] = old_path
68
+ sys.dont_write_bytecode = old_bytecode
69
+ sys.modules.pop("l1_fixtures", None)
70
+ if old_fx is not None:
71
+ sys.modules["l1_fixtures"] = old_fx
72
+ gate.ROOT = root
73
+ gate.BACKENDS = HERE
74
+ gate.VECTORS = root / "vectors"
75
+ gate.CLIR = root / "corpus/clir"
76
+ gate.CERT = root / "runtime/runtime-cert.json"
77
+ gate.MANIFEST = root / "data/backends-vectors.json"
78
+ gate.P = plan
79
+ gate.D = deliver
80
+ gate.canon_value = lambda value: core_support.canonical_bytes(value, document=False).decode("utf-8")
81
+ _install_profiled_certifier(gate)
82
+ return gate
83
+
84
+
85
+ def _install_profiled_certifier(gate) -> None:
86
+ """Apply the installable runtime contract without modifying the archived gate.
87
+
88
+ The source-tree gate deliberately keeps its historical all-fixture
89
+ certification during the transition. The packaged wrapper instead gives
90
+ each target the fixture subset declared in :mod:`runtime_cert`; returning
91
+ the same ``list[str]`` shape keeps the gate's normal failure path intact.
92
+ """
93
+ def certify(emit: dict, work: Path, active: list[str]) -> list[str]:
94
+ errors, reports = [], {}
95
+ for lang in active:
96
+ try:
97
+ report, target_errors = runtime_cert.certify_target(
98
+ lang, emit[lang], work, gate.CERT, gate.D.emit_project,
99
+ gate.build_projects, gate.run_target,
100
+ )
101
+ reports[lang] = report
102
+ errors.extend(target_errors)
103
+ except runtime_cert.FixtureError as exc:
104
+ errors.append(f"сертификация {lang}: неверный capability-контракт: {exc}")
105
+ gate.RUNTIME_CERT_REPORTS = reports
106
+ summary = []
107
+ for lang in active:
108
+ report = reports.get(lang)
109
+ if report is None:
110
+ continue
111
+ skipped_ops = ",".join(report["unsupportedOperations"]) or "—"
112
+ skipped_literals = ",".join(report["unsupportedLiteralTags"]) or "—"
113
+ summary.append(f"{lang}: {len(report['checkedFixtureIds'])} cases; "
114
+ f"ops вне профиля: {skipped_ops}; literals вне профиля: {skipped_literals}")
115
+ if summary:
116
+ print("check_backends: installed runtime profiles — " + " | ".join(summary))
117
+ return errors
118
+
119
+ gate.certify = certify
120
+
121
+
122
+ def main(argv: list[str] | None = None) -> int:
123
+ ap = argparse.ArgumentParser(description=__doc__, allow_abbrev=False)
124
+ ap.add_argument("--fixtures", required=True, type=Path)
125
+ ap.add_argument("--core-kit", required=True, type=Path)
126
+ ap.add_argument("--active-canaries", type=Path)
127
+ ap.add_argument("--active-canary-targets")
128
+ args, remaining = ap.parse_known_args(argv)
129
+ import core_support
130
+ core_support.configure_core_kit(args.core_kit)
131
+ core_support.validate_core_kit(args.core_kit)
132
+ gate = load_harness(args.fixtures.resolve())
133
+ if args.active_canaries is not None:
134
+ def active_value_canaries(_emit, _work):
135
+ import check_active_canaries
136
+ capable = tuple(lang for lang in ("py", "ts", "go") if lang in _emit)
137
+ if args.active_canary_targets is not None:
138
+ requested = tuple(x for x in args.active_canary_targets.split(',') if x)
139
+ if set(capable) != set(requested) or len(requested) != len(capable):
140
+ return ["active value canaries must cover all active primary targets: " + ",".join(capable)], []
141
+ if not capable:
142
+ return [], ["active value canaries: no primary targets in this profile"]
143
+ try:
144
+ lines = check_active_canaries.verify(args.active_canaries, core_kit=args.core_kit, targets=capable)
145
+ except check_active_canaries.CanaryError as exc:
146
+ return ["active value canaries: " + str(exc)], []
147
+ return [], ["active value canaries: " + line for line in lines]
148
+ gate.value_canaries = active_value_canaries
149
+ old_argv = sys.argv[:]
150
+ try:
151
+ sys.argv = ["arxo-codegen-check", *remaining]
152
+ return gate.main()
153
+ finally:
154
+ sys.argv[:] = old_argv
155
+
156
+
157
+ if __name__ == "__main__":
158
+ raise SystemExit(main())
@@ -0,0 +1,262 @@
1
+ """Standalone verifier for a published active-canary bundle.
2
+
3
+ The bundle contains frozen lawref outcomes produced by the publisher. This
4
+ consumer never imports lawref or the monorepo CI gate: it verifies the bundle
5
+ inventory, rebuilds each supported plan with the installed code generator, and
6
+ executes the Python baseline and named mutation against the frozen oracle.
7
+ """
8
+ from __future__ import annotations
9
+
10
+ import argparse
11
+ import copy
12
+ import hashlib
13
+ import json
14
+ from pathlib import Path
15
+ import subprocess
16
+ import sys
17
+ import tempfile
18
+ import os
19
+
20
+ HERE = Path(__file__).resolve().parent
21
+ if str(HERE) not in sys.path:
22
+ sys.path.insert(0, str(HERE))
23
+ # Keep one flat core_support module with plan/deliver, both in a checkout and
24
+ # in the installed package layout (deliver deliberately uses this convention).
25
+ import core_support # noqa: E402
26
+ import deliver as D # noqa: E402
27
+ import plan as P # noqa: E402
28
+ import target_runner as TR # noqa: E402
29
+
30
+ FORMAT = "law.codegen-active-canaries/0.1"
31
+ SEMANTIC = "0.2.11"
32
+ REQUIRED = {
33
+ "LAY-L0-RAT-DYN-DECIMAL-KEPT": "kindChecks",
34
+ "LAY-L0-RAT-GUARD-DYN-MIX": "err",
35
+ "LAY-L0-RAT-HEAD-DIV-ZERO": "headFallible",
36
+ "LAY-L0-RAT-DYN-RATIONAL-KEPT": "lk",
37
+ "LAY-L0-QTY-GUARD-DIMENSION": "err",
38
+ "LAY-L0-QTY-ADD-DIMENSION": "headFallible",
39
+ }
40
+ EXPECTED_REFUSALS = {"LAY-L0-RAT-GUARD-DYN-MIX": "NUMBER_UNAVAILABLE"}
41
+ SOURCE_FILES = {"source.law", "source.ir.json", "source.case.json", "source.query.json"}
42
+ DERIVED_FILES = {"ir.json", "case.json", "query.json", "oracle.outcome.json",
43
+ "backend.baseline.json", "backend.mutated.json"}
44
+
45
+
46
+ class CanaryError(RuntimeError):
47
+ pass
48
+
49
+
50
+ def _sha(data: bytes) -> str:
51
+ return "sha256:" + hashlib.sha256(data).hexdigest()
52
+
53
+
54
+ def _read_json(path: Path) -> dict:
55
+ try:
56
+ obj = json.loads(path.read_bytes())
57
+ except (OSError, UnicodeDecodeError, json.JSONDecodeError) as exc:
58
+ raise CanaryError(f"invalid JSON: {path.name}") from exc
59
+ if not isinstance(obj, dict):
60
+ raise CanaryError(f"JSON object required: {path.name}")
61
+ return obj
62
+
63
+
64
+ def _safe_relative(value: object) -> Path:
65
+ if not isinstance(value, str):
66
+ raise CanaryError("invalid manifest path")
67
+ path = Path(value)
68
+ if path.is_absolute() or not value or ".." in path.parts:
69
+ raise CanaryError("unsafe manifest path")
70
+ return path
71
+
72
+
73
+ def _canary_id(value: object) -> str:
74
+ if not isinstance(value, str) or not value or Path(value).name != value or not value.isascii() \
75
+ or any(char not in "ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789-" for char in value):
76
+ raise CanaryError("unsafe canary id")
77
+ return value
78
+
79
+
80
+ def _outcome(document: dict) -> dict:
81
+ if "refused" in document:
82
+ return {"refused": document["refused"]}
83
+ issues = document.get("issues", [])
84
+ if not isinstance(issues, list):
85
+ raise CanaryError("backend outcome has invalid issues")
86
+ codes = sorted(item.get("code") if isinstance(item, dict) else str(item)
87
+ for item in issues if isinstance(item, str) or isinstance(item, dict) and item.get("code"))
88
+ result = (document.get("results") or [{}])[0]
89
+ if not isinstance(result, dict):
90
+ raise CanaryError("oracle result is invalid")
91
+ return {"truthStatus": result.get("truthStatus", document.get("truthStatus")),
92
+ "evaluationStatus": result.get("evaluationStatus", document.get("evaluationStatus")),
93
+ "issues": codes}
94
+
95
+
96
+ def _mutate(plan: dict, field: str) -> dict:
97
+ out = copy.deepcopy(plan)
98
+ for rule in out.get("rules", []):
99
+ if field in ("kindChecks", "headFallible"):
100
+ rule.pop(field, None)
101
+ elif field == "err":
102
+ for guard in rule.get("guards", []):
103
+ guard.pop("err", None)
104
+ if field == "lk":
105
+ stack = [out]
106
+ while stack:
107
+ item = stack.pop()
108
+ if isinstance(item, dict):
109
+ if item.get("k") == "bin" and item.get("op") == "div" and "lk" in item:
110
+ item["lk"] = item["rk"] = "d"
111
+ stack.extend(value for value in item.values() if isinstance(value, (dict, list)))
112
+ elif isinstance(item, list):
113
+ stack.extend(item)
114
+ return out
115
+
116
+
117
+ def _run(plan: dict, case: Path, query: Path, target: str = "py", env=None) -> dict:
118
+ with tempfile.TemporaryDirectory(prefix="codegen-canary-") as temp:
119
+ project = Path(temp) / target
120
+ D.emit_project(plan, target, project)
121
+ emit={target:D.emitter(target)}
122
+ failures=TR.build_projects(emit,{target:[project]},Path(temp),env)
123
+ if failures: raise CanaryError(failures[0])
124
+ kind,response=TR.run_target(emit[target],project,"runner",[str(case),str(query)],env)
125
+ if kind == "refused": return _outcome({"refused": response})
126
+ if kind != "answer": raise CanaryError(f"generated {target} runner {kind}: {response}")
127
+ return _outcome(response)
128
+
129
+
130
+ def load(bundle: Path) -> tuple[dict, list[dict]]:
131
+ bundle = Path(bundle).resolve()
132
+ if bundle.is_symlink() or not bundle.is_dir():
133
+ raise CanaryError("canary bundle is not a directory")
134
+ if any(path.is_symlink() for path in bundle.rglob("*")):
135
+ raise CanaryError("canary bundle contains a symlink")
136
+ manifest = _read_json(bundle / "manifest.json")
137
+ if manifest.get("format") != FORMAT or manifest.get("semanticVersion") != SEMANTIC:
138
+ raise CanaryError("unsupported canary bundle format or semantic version")
139
+ rows = manifest.get("canaries")
140
+ unsupported = manifest.get("unsupported")
141
+ if not isinstance(rows, list) or not isinstance(unsupported, list):
142
+ raise CanaryError("canary bundle has invalid rows")
143
+ known = {"manifest.json"}
144
+ seen = set()
145
+ all_rows: dict[str, dict] = {}
146
+ for row in rows:
147
+ if not isinstance(row, dict):
148
+ raise CanaryError("invalid canary row")
149
+ ident = _canary_id(row.get("id"))
150
+ if ident in seen:
151
+ raise CanaryError("duplicate canary id")
152
+ seen.add(ident); all_rows[ident] = row
153
+ prefix = Path("canaries") / ident
154
+ known.update((prefix / name).as_posix() for name in (
155
+ "source.law", "source.ir.json", "source.case.json", "source.query.json",
156
+ "ir.json", "case.json", "query.json", "oracle.outcome.json",
157
+ "backend.baseline.json", "backend.mutated.json"))
158
+ if row.get("semanticVersion") != SEMANTIC or row.get("mutation") != REQUIRED.get(ident):
159
+ raise CanaryError("invalid canary semantic provenance")
160
+ if row.get("expectedRefusal") != EXPECTED_REFUSALS.get(ident):
161
+ raise CanaryError("invalid current refusal qualification")
162
+ for key, required in (("sourceFiles", SOURCE_FILES), ("files", DERIVED_FILES)):
163
+ inventory = row.get(key)
164
+ if not isinstance(inventory, dict) or set(inventory) != required:
165
+ raise CanaryError("canary row lacks file provenance")
166
+ for name, digest in inventory.items():
167
+ path = prefix / _safe_relative(name)
168
+ if digest != _sha((bundle / path).read_bytes()):
169
+ raise CanaryError("canary file hash mismatch: " + path.as_posix())
170
+ for row in unsupported:
171
+ if not isinstance(row, dict):
172
+ raise CanaryError("invalid unsupported canary row")
173
+ ident = _canary_id(row.get("id"))
174
+ if ident in all_rows or row.get("mutation") != REQUIRED.get(ident) or not isinstance(row.get("reason"), str):
175
+ raise CanaryError("invalid unsupported canary provenance")
176
+ all_rows[ident] = row
177
+ if set(all_rows) != set(REQUIRED):
178
+ raise CanaryError("required canary inventory is incomplete")
179
+ actual = {path.relative_to(bundle).as_posix() for path in bundle.rglob("*") if path.is_file()}
180
+ if actual != known:
181
+ raise CanaryError("canary bundle inventory differs from manifest")
182
+ return manifest, rows
183
+
184
+
185
+ def verify(bundle: Path, *, allow_partial: bool = False, core_kit: Path | None = None,
186
+ targets: tuple[str,...] = ("py",)) -> list[str]:
187
+ if core_kit is None:
188
+ raise CanaryError("--core-kit is required for standalone canary verification")
189
+ previous = core_support._configured_kit
190
+ core_support.configure_core_kit(core_kit)
191
+ try:
192
+ if not targets or len(set(targets)) != len(targets) or any(t not in ("py","ts","go") for t in targets): raise CanaryError("unknown active canary target")
193
+ core_support.validate_core_kit(core_kit)
194
+ bundle = Path(bundle).resolve()
195
+ manifest, rows = load(bundle)
196
+ messages: list[str] = []
197
+ cache_directory = tempfile.TemporaryDirectory(prefix="codegen-active-env-")
198
+ env = dict(os.environ, GOCACHE=str(Path(cache_directory.name) / "gocache"),
199
+ GOMODCACHE=str(Path(cache_directory.name) / "gomodcache"))
200
+ for row in rows:
201
+ base = bundle / "canaries" / row["id"]
202
+ ir, case, query = (_read_json(base / name) for name in ("ir.json", "case.json", "query.json"))
203
+ if row.get("expectedRefusal"):
204
+ try:
205
+ P.build_plan(ir, resources={})
206
+ except P.Refused as exc:
207
+ if not str(exc).startswith(row["expectedRefusal"] + ":"):
208
+ raise CanaryError("wrong qualified canary refusal") from exc
209
+ for name in ("backend.baseline.json", "backend.mutated.json"):
210
+ if _read_json(base / name) != {"refused": str(exc)}:
211
+ raise CanaryError("qualified refusal differs from frozen evidence")
212
+ else:
213
+ raise CanaryError("qualified canary unexpectedly admitted")
214
+ messages.append(f"EXPECTED_REFUSAL {row['id']}: {row['expectedRefusal']}")
215
+ continue
216
+ plan = P.build_plan(ir, resources={})
217
+ mutation = _mutate(plan, row["mutation"])
218
+ if mutation == plan:
219
+ raise CanaryError(f"{row['id']}: mutation does not change rebuilt plan")
220
+ expected = _read_json(base / "oracle.outcome.json")
221
+ frozen_baseline = _read_json(base / "backend.baseline.json")
222
+ frozen_mutated = _read_json(base / "backend.mutated.json")
223
+ for target in targets:
224
+ baseline = _run(plan, base / "case.json", base / "query.json",target,env)
225
+ changed = _run(mutation, base / "case.json", base / "query.json",target,env)
226
+ if baseline != expected or baseline != frozen_baseline:
227
+ raise CanaryError(f"{row['id']} {target}: baseline differs from frozen evidence")
228
+ if changed != frozen_mutated or changed == baseline:
229
+ raise CanaryError(f"{row['id']} {target}: mutation witness did not fail")
230
+ messages.append(f"PASS {row['id']} ({','.join(targets)})")
231
+ unsupported = manifest["unsupported"]
232
+ if unsupported:
233
+ names = ", ".join(str(row["id"]) for row in unsupported)
234
+ if not allow_partial:
235
+ raise CanaryError("unsupported required canaries: " + names)
236
+ messages.append("PARTIAL unsupported required canaries: " + names)
237
+ return messages
238
+ finally:
239
+ if "cache_directory" in locals():
240
+ cache_directory.cleanup()
241
+ core_support.configure_core_kit(previous)
242
+
243
+
244
+ def main(argv: list[str] | None = None) -> int:
245
+ parser = argparse.ArgumentParser(description=__doc__)
246
+ parser.add_argument("--bundle", type=Path, required=True)
247
+ parser.add_argument("--core-kit", type=Path)
248
+ parser.add_argument("--allow-partial", action="store_true", help="report unsupported rows as PARTIAL, never PASS")
249
+ parser.add_argument("--targets", default="py", help="comma-separated py,ts,go")
250
+ args = parser.parse_args(argv)
251
+ try:
252
+ for line in verify(args.bundle, allow_partial=args.allow_partial, core_kit=args.core_kit,
253
+ targets=tuple(x for x in args.targets.split(',') if x)):
254
+ print(line)
255
+ except (CanaryError, core_support.CoreKitError, P.Refused) as exc:
256
+ print(f"FAIL: {exc}", file=sys.stderr)
257
+ return 1
258
+ return 0
259
+
260
+
261
+ if __name__ == "__main__":
262
+ raise SystemExit(main())
@@ -0,0 +1,76 @@
1
+ """API порождённого модуля против раннера на проверенном комплекте (DECISION-0423 §2.6).
2
+
3
+ Шаг приёмки: для API-пакетов комплекта (`corpus/clir`) печатаются проекты
4
+ целей с API, и каждое задание — положительное, с отрицательным фактом L1, с
5
+ литералом чужого типа (§189), с синтетическим конфликтом ключа §45.1 —
6
+ исполняется раннером и харнессом API; исходы обязаны совпасть у API и раннера
7
+ каждой цели и у целей между собой. Затем пакет заданий с повтором
8
+ исполняется одним процессом последовательно и параллельно, и канарейка
9
+ портит общую часть API Python — расхождение обязано найтись.
10
+ """
11
+ from __future__ import annotations
12
+
13
+ import argparse
14
+ import json
15
+ from pathlib import Path
16
+ import sys
17
+ import tempfile
18
+
19
+ HERE = Path(__file__).resolve().parent
20
+ sys.path.insert(0, str(HERE))
21
+
22
+
23
+ def main(argv: list[str] | None = None) -> int:
24
+ ap = argparse.ArgumentParser(description=__doc__, allow_abbrev=False)
25
+ ap.add_argument("--fixtures", required=True, type=Path)
26
+ ap.add_argument("--core-kit", required=True, type=Path)
27
+ ap.add_argument("--targets", default="py,ts,go", help="цели с API через запятую")
28
+ ap.add_argument("--require", default="", help="цели, отсутствие тулчейна которых — отказ")
29
+ ap.add_argument("--keep", type=Path, help="сохранить проекты")
30
+ ap.add_argument("--report", type=Path)
31
+ args = ap.parse_args(argv)
32
+ import core_support
33
+ core_support.configure_core_kit(args.core_kit)
34
+ core_support.validate_core_kit(args.core_kit)
35
+ import check
36
+ gate = check.load_harness(args.fixtures.resolve())
37
+ wanted = [x for x in args.targets.split(",") if x]
38
+ emit = {}
39
+ for lang in wanted:
40
+ em = gate.D.emitter(lang)
41
+ if not (getattr(em, "API", None) and hasattr(em, "emit_api")):
42
+ print(f"check_api: у цели {lang} нет API")
43
+ return 1
44
+ emit[lang] = em
45
+ tools = {lang: emit[lang].available() for lang in wanted}
46
+ active = [lang for lang in wanted if tools[lang][0]]
47
+ for lang in wanted:
48
+ if not tools[lang][0]:
49
+ print(f"check_api: цель {lang} ПРОПУЩЕНА — {tools[lang][1]}")
50
+ missing = [x for x in args.require.split(",") if x and x not in active]
51
+ if missing:
52
+ print(f"check_api: обязательные цели без тулчейна: {', '.join(missing)}")
53
+ return 1
54
+ if not active:
55
+ print("check_api: нет ни одной цели с тулчейном")
56
+ return 1
57
+ emit = {lang: emit[lang] for lang in active}
58
+ if args.keep:
59
+ work = args.keep.resolve()
60
+ work.mkdir(parents=True, exist_ok=True)
61
+ problems, checked = gate.api_check(emit, work, active)
62
+ else:
63
+ with tempfile.TemporaryDirectory(prefix="codegen-api-") as tmp:
64
+ problems, checked = gate.api_check(emit, Path(tmp), active)
65
+ for line in problems:
66
+ print("check_api: " + line)
67
+ print(f"check_api: цели {', '.join(active)}; сверено заданий {checked}; проблем {len(problems)}")
68
+ if args.report:
69
+ args.report.write_text(json.dumps({"format": "law.codegen-api-check/0.1", "targets": active,
70
+ "checked": checked, "problems": problems},
71
+ ensure_ascii=False, indent=1) + "\n", encoding="utf-8")
72
+ return 1 if problems or not checked else 0
73
+
74
+
75
+ if __name__ == "__main__":
76
+ raise SystemExit(main())