core-runtime-engine 11.5.1__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 (96) hide show
  1. core_runtime/__init__.py +10 -0
  2. core_runtime/__version__.py +17 -0
  3. core_runtime/cli/__init__.py +7 -0
  4. core_runtime/cli/__main__.py +22 -0
  5. core_runtime/cli/bump_version.py +176 -0
  6. core_runtime/cli/contract_preflight.py +69 -0
  7. core_runtime/cli/create_domain.py +66 -0
  8. core_runtime/cli/doctor.py +44 -0
  9. core_runtime/cli/inventory.py +85 -0
  10. core_runtime/cli/lint.py +8 -0
  11. core_runtime/cli/main.py +579 -0
  12. core_runtime/cli/release_check.py +65 -0
  13. core_runtime/cli/repair_artifact_paths.py +48 -0
  14. core_runtime/cli/sync_template.py +67 -0
  15. core_runtime/cli/validate.py +73 -0
  16. core_runtime/core/__init__.py +34 -0
  17. core_runtime/core/audit_event.py +760 -0
  18. core_runtime/core/audit_trail_index.py +100 -0
  19. core_runtime/core/canonicalization.py +55 -0
  20. core_runtime/core/contract_evaluator.py +945 -0
  21. core_runtime/core/contract_executability.py +307 -0
  22. core_runtime/core/contract_loader.py +57 -0
  23. core_runtime/core/contract_probes.py +630 -0
  24. core_runtime/core/contract_program.py +131 -0
  25. core_runtime/core/contract_program_registry.py +104 -0
  26. core_runtime/core/contract_program_v2.py +126 -0
  27. core_runtime/core/dsk_v3.py +142 -0
  28. core_runtime/core/explainability.py +2115 -0
  29. core_runtime/core/numeric_normalization.py +120 -0
  30. core_runtime/core/rule_anchor.py +1388 -0
  31. core_runtime/core/schema_fingerprint.py +69 -0
  32. core_runtime/core/sensor_evidence.py +548 -0
  33. core_runtime/data/contracts/CoreAnchor.sol +109 -0
  34. core_runtime/data/contracts/CoreRuleAnchor.abi.json +111 -0
  35. core_runtime/data/contracts/CoreRuleAnchor.bin +1 -0
  36. core_runtime/data/contracts/CoreRuleAnchor.build.json +20 -0
  37. core_runtime/data/contracts/CoreRuleAnchor.runtime.bin +1 -0
  38. core_runtime/data/contracts/CoreRuleAnchor.sol +74 -0
  39. core_runtime/data/package_data_manifest.v1.json +173 -0
  40. core_runtime/data/schemas/core/causal_trace.v1.json +141 -0
  41. core_runtime/data/schemas/core/context_gate.v1.json +38 -0
  42. core_runtime/data/schemas/core/context_threshold.v1.json +46 -0
  43. core_runtime/data/schemas/core/contract_program.v1.json +186 -0
  44. core_runtime/data/schemas/core/contract_program.v2.json +187 -0
  45. core_runtime/data/schemas/core/control_decision.v1.json +114 -0
  46. core_runtime/data/schemas/core/dsk.v3.json +105 -0
  47. core_runtime/data/schemas/core/effect_result.v1.json +39 -0
  48. core_runtime/data/schemas/core/entropy_signal.v1.json +108 -0
  49. core_runtime/data/schemas/core/execution_receipt.v1.json +93 -0
  50. core_runtime/data/schemas/core/frozen_release_manifest.v1.json +87 -0
  51. core_runtime/data/schemas/core/frozen_release_manifest.v2.json +72 -0
  52. core_runtime/data/schemas/core/frozen_release_manifest.v3.json +38 -0
  53. core_runtime/data/schemas/core/frozen_release_manifest.v4.json +72 -0
  54. core_runtime/data/schemas/core/frozen_release_manifest.v5.json +38 -0
  55. core_runtime/data/schemas/core/frozen_release_manifest.v6.json +116 -0
  56. core_runtime/data/schemas/core/frozen_release_manifest.v7.json +37 -0
  57. core_runtime/data/schemas/core/frozen_release_manifest.v8.json +114 -0
  58. core_runtime/data/schemas/core/frozen_rule_set.v1.json +282 -0
  59. core_runtime/data/schemas/core/memory_artifact.v1.json +120 -0
  60. core_runtime/data/schemas/core/memory_generation_result.v1.json +37 -0
  61. core_runtime/data/schemas/core/operational_learning_event.v1.json +63 -0
  62. core_runtime/data/schemas/core/pattern_candidate.v1.json +114 -0
  63. core_runtime/data/schemas/core/physical_safety_assurance_case.v1.json +676 -0
  64. core_runtime/data/schemas/core/policy_lifecycle.v1.json +99 -0
  65. core_runtime/data/schemas/core/retention_manifest.v1.json +53 -0
  66. core_runtime/data/schemas/core/reversibility_policy.v1.json +107 -0
  67. core_runtime/data/schemas/core/rule_anchor_batch.v1.json +93 -0
  68. core_runtime/data/schemas/core/rule_anchor_chain_evidence.v1.json +56 -0
  69. core_runtime/data/schemas/core/rule_approval.v1.json +49 -0
  70. core_runtime/data/schemas/core/rule_approval_request.v1.json +42 -0
  71. core_runtime/data/schemas/core/state_transition.v1.json +115 -0
  72. core_runtime/data/schemas/core/task_closeout.v1.json +47 -0
  73. core_runtime/data/schemas/core/template_promotion_candidate.v1.json +83 -0
  74. core_runtime/data/schemas/core/unsigned_rule_anchor_deployment.v1.json +106 -0
  75. core_runtime/data/schemas/core/unsigned_rule_anchor_transaction.v1.json +116 -0
  76. core_runtime/tooling/__init__.py +48 -0
  77. core_runtime/tooling/bump_version.py +900 -0
  78. core_runtime/tooling/contract_preflight.py +293 -0
  79. core_runtime/tooling/create_domain.py +254 -0
  80. core_runtime/tooling/diagnostics.py +129 -0
  81. core_runtime/tooling/doctor.py +482 -0
  82. core_runtime/tooling/file_inventory.py +182 -0
  83. core_runtime/tooling/json_checks.py +100 -0
  84. core_runtime/tooling/release_check.py +1017 -0
  85. core_runtime/tooling/repair_artifact_paths.py +458 -0
  86. core_runtime/tooling/report_writer.py +176 -0
  87. core_runtime/tooling/repository_inventory.py +399 -0
  88. core_runtime/tooling/safety_checks.py +172 -0
  89. core_runtime/tooling/sync_template.py +303 -0
  90. core_runtime/tooling/validation.py +507 -0
  91. core_runtime/tooling/version_inventory.py +256 -0
  92. core_runtime_engine-11.5.1.dist-info/METADATA +35 -0
  93. core_runtime_engine-11.5.1.dist-info/RECORD +96 -0
  94. core_runtime_engine-11.5.1.dist-info/WHEEL +5 -0
  95. core_runtime_engine-11.5.1.dist-info/entry_points.txt +2 -0
  96. core_runtime_engine-11.5.1.dist-info/top_level.txt +1 -0
@@ -0,0 +1,1017 @@
1
+ """Release-check wrapper around the authoritative release verification script."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import json
6
+ import py_compile
7
+ import re
8
+ import subprocess
9
+ import sys
10
+ import time
11
+ from dataclasses import dataclass, field
12
+ from pathlib import Path
13
+ from typing import Any, Optional
14
+
15
+ from core_runtime.tooling.diagnostics import DiagnosticCollection
16
+ from core_runtime.tooling.version_inventory import VersionInventory
17
+
18
+ DEFAULT_RELEASE_CHECK_TIMEOUT_SECONDS = 120
19
+ OUTPUT_PREVIEW_LIMIT = 2048
20
+
21
+ RELEASE_CHECK_PROFILES: dict[str, dict[str, tuple[str, ...] | str]] = {
22
+ "fast": {
23
+ "description": "Tooling, metadata, and tooling-test baseline.",
24
+ "groups": ("tooling", "release-metadata", "tests-tooling"),
25
+ "notes": ("Fast developer signal with bounded deterministic checks.",),
26
+ },
27
+ "local": {
28
+ "description": "Fast profile plus bounded developer test slices.",
29
+ "groups": ("tooling", "release-metadata", "tests-tooling", "tests-replay", "tests-integration"),
30
+ "notes": ("Useful for local iteration without the longest contracts/core slices.",),
31
+ },
32
+ "full": {
33
+ "description": "Full deterministic release surface.",
34
+ "groups": (
35
+ "tooling",
36
+ "release-metadata",
37
+ "replay",
38
+ "tests-tooling",
39
+ "tests-replay",
40
+ "tests-integration",
41
+ "tests-contracts",
42
+ "tests-core",
43
+ ),
44
+ "notes": ("Matches the current bounded full release gate.",),
45
+ },
46
+ "release-candidate": {
47
+ "description": "Full release surface with future tag and package metadata expectations.",
48
+ "groups": (
49
+ "tooling",
50
+ "release-metadata",
51
+ "replay",
52
+ "tests-tooling",
53
+ "tests-replay",
54
+ "tests-integration",
55
+ "tests-contracts",
56
+ "tests-core",
57
+ ),
58
+ "notes": (
59
+ "Future package metadata and tag expectation checks remain advisory-only.",
60
+ ),
61
+ },
62
+ }
63
+
64
+ _STRICT_VERSION_RE = re.compile(r"^\d+\.\d+\.\d+$")
65
+
66
+
67
+ def validate_release_target(target: str) -> Optional[str]:
68
+ """Return None when *target* is a supported release target."""
69
+ if target.startswith("v"):
70
+ target = target[1:]
71
+ if not _STRICT_VERSION_RE.match(target):
72
+ return "Invalid release target: '{0}'. Expected MAJOR.MINOR.PATCH or vMAJOR.MINOR.PATCH.".format(
73
+ target
74
+ )
75
+ return None
76
+
77
+
78
+ def normalize_release_target(target: str) -> str:
79
+ """Normalize a target to the canonical release-check form."""
80
+ return target[1:] if target.startswith("v") else target
81
+
82
+
83
+ def release_target_argument(target: str) -> str:
84
+ """Format the normalized target for scripts/verify_release.py."""
85
+ return "v{0}".format(normalize_release_target(target))
86
+
87
+
88
+ def _coerce_text(value: Any) -> str:
89
+ if value is None:
90
+ return ""
91
+ if isinstance(value, bytes):
92
+ return value.decode("utf-8", errors="replace")
93
+ return str(value)
94
+
95
+
96
+ def _truncate_text(text: str, limit: int = OUTPUT_PREVIEW_LIMIT) -> tuple[str, bool]:
97
+ if len(text) <= limit:
98
+ return text, False
99
+ return text[:limit] + "…[truncated]", True
100
+
101
+
102
+ @dataclass
103
+ class SubprocessCapture:
104
+ """Captured subprocess execution details."""
105
+
106
+ command: list[str]
107
+ cwd: str
108
+ expect_json: bool
109
+ returncode: Optional[int]
110
+ stdout: str
111
+ stderr: str
112
+ timed_out: bool = False
113
+ timeout_seconds: Optional[int] = None
114
+ elapsed_seconds: Optional[float] = None
115
+ json_detected: bool = False
116
+ json_payload: dict[str, Any] | None = None
117
+ report_path: Optional[str] = None
118
+ target_argument: Optional[str] = None
119
+ help_contains_target: Optional[bool] = None
120
+ stdout_preview: str = ""
121
+ stderr_preview: str = ""
122
+ stdout_truncated: bool = False
123
+ stderr_truncated: bool = False
124
+
125
+ @property
126
+ def status(self) -> str:
127
+ if self.timed_out:
128
+ return "blocked"
129
+ if self.returncode is None:
130
+ return "internal_error"
131
+ if self.returncode == 0:
132
+ if self.expect_json and not self.json_detected:
133
+ return "internal_error"
134
+ return "pass"
135
+ if self.returncode == 1:
136
+ return "error"
137
+ if self.returncode == 2:
138
+ return "blocked"
139
+ return "internal_error"
140
+
141
+ def to_dict(self) -> dict[str, Any]:
142
+ result: dict[str, Any] = {
143
+ "command": self.command,
144
+ "cwd": self.cwd,
145
+ "expect_json": self.expect_json,
146
+ "exit_code": self.returncode,
147
+ "json_detected": self.json_detected,
148
+ "status": self.status,
149
+ "report_path": self.report_path,
150
+ "stdout_preview": self.stdout_preview,
151
+ "stderr_preview": self.stderr_preview,
152
+ "stdout_truncated": self.stdout_truncated,
153
+ "stderr_truncated": self.stderr_truncated,
154
+ "timed_out": self.timed_out,
155
+ "timeout_seconds": self.timeout_seconds,
156
+ }
157
+ if self.elapsed_seconds is not None:
158
+ result["elapsed_seconds"] = self.elapsed_seconds
159
+ if self.target_argument is not None:
160
+ result["target_argument"] = self.target_argument
161
+ if self.help_contains_target is not None:
162
+ result["help_contains_target"] = self.help_contains_target
163
+ if self.json_payload is not None:
164
+ result["json_payload"] = self.json_payload
165
+ return result
166
+
167
+
168
+ @dataclass
169
+ class ReleaseCheckReport:
170
+ """Normalized release-check wrapper report."""
171
+
172
+ mode: str
173
+ target: str
174
+ target_argument: str
175
+ timeout_seconds: int
176
+ profile: Optional[str] = None
177
+ tool: str = "core-runtime release-check"
178
+ status: str = "internal_error"
179
+ mutation_performed: bool = False
180
+ summary: dict[str, Any] = field(default_factory=dict)
181
+ diagnostics: DiagnosticCollection = field(default_factory=DiagnosticCollection)
182
+ profile_definition: dict[str, Any] | None = None
183
+ profile_runs: list[SubprocessCapture] = field(default_factory=list)
184
+ tooling_lint: SubprocessCapture | None = None
185
+ release_gate_help: SubprocessCapture | None = None
186
+ release_gate: SubprocessCapture | None = None
187
+ preflight_checks: dict[str, Any] = field(default_factory=dict)
188
+ debug: dict[str, Any] | None = None
189
+
190
+ def _summary(self) -> dict[str, Any]:
191
+ summary = dict(self.summary)
192
+ summary.setdefault("tooling_lint_status", self.tooling_lint.status if self.tooling_lint else "skipped")
193
+ summary.setdefault("release_gate_status", self.release_gate.status if self.release_gate else "skipped")
194
+ summary.setdefault("preflight_status", self.preflight_checks.get("status", "skipped"))
195
+ counts = self.diagnostics.count_by_severity()
196
+ summary.setdefault("info", counts["info"])
197
+ summary.setdefault("warning", counts["warning"])
198
+ summary.setdefault("error", counts["error"])
199
+ summary.setdefault("blocked", counts["blocked"])
200
+ return summary
201
+
202
+ def to_dict(self) -> dict[str, Any]:
203
+ result = {
204
+ "tool": self.tool,
205
+ "mode": self.mode,
206
+ "target": self.target,
207
+ "target_argument": self.target_argument,
208
+ "profile": self.profile,
209
+ "status": self.status,
210
+ "mutation_performed": self.mutation_performed,
211
+ "summary": self._summary(),
212
+ "diagnostics": [d.to_dict() for d in self.diagnostics.diagnostics],
213
+ "preflight_checks": self.preflight_checks,
214
+ "timeout_seconds": self.timeout_seconds,
215
+ }
216
+ if self.profile_definition is not None:
217
+ result["profile_definition"] = self.profile_definition
218
+ if self.profile_runs:
219
+ result["profile_runs"] = [capture.to_dict() for capture in self.profile_runs]
220
+ if self.tooling_lint is not None:
221
+ result["tooling_lint"] = self.tooling_lint.to_dict()
222
+ if self.release_gate_help is not None:
223
+ result["release_gate_help"] = self.release_gate_help.to_dict()
224
+ if self.release_gate is not None:
225
+ result["release_gate"] = self.release_gate.to_dict()
226
+ if self.debug is not None:
227
+ result["debug"] = self.debug
228
+ return result
229
+
230
+ def to_markdown(self) -> str:
231
+ counts = self.diagnostics.count_by_severity()
232
+ summary = self._summary()
233
+ lines: list[str] = []
234
+ lines.append("# CORE release-check report")
235
+ lines.append("")
236
+ lines.append("## Summary")
237
+ lines.append("")
238
+ lines.append("| Metric | Value |")
239
+ lines.append("|--------|-------|")
240
+ lines.append("| Tool | core-runtime release-check |")
241
+ lines.append("| Mode | {0} |".format(self.mode))
242
+ lines.append("| Profile | {0} |".format(self.profile or "none"))
243
+ lines.append("| Target | {0} |".format(self.target))
244
+ lines.append("| Target Argument | {0} |".format(self.target_argument))
245
+ lines.append("| Status | {0} |".format(self.status.upper()))
246
+ lines.append("| Mutation Performed | No |")
247
+ lines.append("| Tooling Lint | {0} |".format(summary.get("tooling_lint_status", "skipped")))
248
+ lines.append("| Release Gate | {0} |".format(summary.get("release_gate_status", "skipped")))
249
+ lines.append("| Preflight | {0} |".format(summary.get("preflight_status", "skipped")))
250
+ lines.append("| Info | {0} |".format(counts["info"]))
251
+ lines.append("| Warning | {0} |".format(counts["warning"]))
252
+ lines.append("| Error | {0} |".format(counts["error"]))
253
+ lines.append("| Blocked | {0} |".format(counts["blocked"]))
254
+ lines.append("")
255
+
256
+ lines.append("## Target")
257
+ lines.append("")
258
+ lines.append("- Normalized target: `{0}`".format(self.target))
259
+ lines.append("- Release gate argument: `{0}`".format(self.target_argument))
260
+ lines.append("")
261
+
262
+ lines.append("## Tooling Lint Precheck")
263
+ lines.append("")
264
+ if self.tooling_lint is None:
265
+ lines.append("- Skipped by request.")
266
+ else:
267
+ lines.append("- Command: `{0}`".format(" ".join(self.tooling_lint.command)))
268
+ lines.append("- cwd: `{0}`".format(self.tooling_lint.cwd))
269
+ lines.append("- Exit code: `{0}`".format(self.tooling_lint.returncode))
270
+ lines.append("- JSON detected: `{0}`".format("yes" if self.tooling_lint.json_detected else "no"))
271
+ lines.append("- Status: `{0}`".format(self.tooling_lint.status))
272
+ lines.append("")
273
+
274
+ lines.append("## Preflight Checks")
275
+ lines.append("")
276
+ if self.preflight_checks:
277
+ for key, value in self.preflight_checks.items():
278
+ lines.append("- `{0}`: {1}".format(key, value))
279
+ else:
280
+ lines.append("- None")
281
+ lines.append("")
282
+
283
+ lines.append("## Profile")
284
+ lines.append("")
285
+ if self.profile_definition is None:
286
+ lines.append("- None")
287
+ else:
288
+ lines.append("- Name: `{0}`".format(self.profile or ""))
289
+ lines.append("- Description: {0}".format(self.profile_definition.get("description", "")))
290
+ groups = self.profile_definition.get("groups", ())
291
+ if groups:
292
+ lines.append("- Groups: {0}".format(", ".join(f"`{group}`" for group in groups)))
293
+ notes = self.profile_definition.get("notes", ())
294
+ for note in notes:
295
+ lines.append("- Note: {0}".format(note))
296
+ if self.profile_runs:
297
+ lines.append("- Runs:")
298
+ for capture in self.profile_runs:
299
+ lines.append(
300
+ " - `{0}` -> `{1}` ({2})".format(
301
+ " ".join(capture.command),
302
+ capture.status,
303
+ capture.elapsed_seconds if capture.elapsed_seconds is not None else "n/a",
304
+ )
305
+ )
306
+ lines.append("")
307
+
308
+ lines.append("## Release Gate")
309
+ lines.append("")
310
+ if self.release_gate is None:
311
+ lines.append("- Not run.")
312
+ else:
313
+ lines.append("- Script: `scripts/verify_release.py`")
314
+ lines.append("- Command: `{0}`".format(" ".join(self.release_gate.command)))
315
+ lines.append("- cwd: `{0}`".format(self.release_gate.cwd))
316
+ lines.append("- Exit code: `{0}`".format(self.release_gate.returncode))
317
+ lines.append("- JSON detected: `{0}`".format("yes" if self.release_gate.json_detected else "no"))
318
+ lines.append("- Timed out: `{0}`".format("yes" if self.release_gate.timed_out else "no"))
319
+ lines.append("- Status: `{0}`".format(self.release_gate.status))
320
+ lines.append("- Stdout preview: `{0}`".format(self.release_gate.stdout_preview or ""))
321
+ lines.append("- Stderr preview: `{0}`".format(self.release_gate.stderr_preview or ""))
322
+ lines.append("")
323
+
324
+ lines.append("## Diagnostics")
325
+ lines.append("")
326
+ if self.diagnostics.diagnostics:
327
+ for diagnostic in self.diagnostics.diagnostics:
328
+ lines.append("- `{0}` {1}: {2}".format(diagnostic.severity.value, diagnostic.code, diagnostic.message))
329
+ else:
330
+ lines.append("- None")
331
+ lines.append("")
332
+
333
+ lines.append("## Final Status")
334
+ lines.append("")
335
+ if self.status == "pass":
336
+ lines.append("✅ **PASS** - Release-check completed successfully.")
337
+ elif self.status == "warning":
338
+ lines.append("⚠️ **WARNING** - Release-check completed with warnings.")
339
+ elif self.status == "error":
340
+ lines.append("❌ **ERROR** - Release-check failed.")
341
+ elif self.status == "blocked":
342
+ lines.append("🚫 **BLOCKED** - Release-check could not safely run.")
343
+ else:
344
+ lines.append("⚠️ **INTERNAL ERROR** - Release-check tooling failure.")
345
+ lines.append("")
346
+ return "\n".join(lines)
347
+
348
+
349
+ class ReleaseCheckRunner:
350
+ """Run tooling lint precheck and the authoritative release gate."""
351
+
352
+ def __init__(self, repo_root: Path):
353
+ self.repo_root = repo_root
354
+ self.version_inventory = VersionInventory(repo_root)
355
+
356
+ def resolve_target(self, target: Optional[str]) -> tuple[str, str]:
357
+ """Return (normalized_target, gate_argument)."""
358
+ if target is None:
359
+ canonical = self.version_inventory.get_canonical_version()
360
+ if canonical is None:
361
+ raise ValueError("canonical version missing")
362
+ target = canonical
363
+
364
+ normalized = normalize_release_target(target)
365
+ error = validate_release_target(normalized)
366
+ if error is not None:
367
+ raise ValueError(error)
368
+
369
+ return normalized, release_target_argument(normalized)
370
+
371
+ def run(
372
+ self,
373
+ target: Optional[str] = None,
374
+ skip_tooling_lint: bool = False,
375
+ timeout_seconds: int = DEFAULT_RELEASE_CHECK_TIMEOUT_SECONDS,
376
+ preflight_only: bool = False,
377
+ debug: bool = False,
378
+ group: Optional[str] = None,
379
+ profile: Optional[str] = None,
380
+ list_checks: bool = False,
381
+ plan: bool = False,
382
+ timing_json: Optional[str] = None,
383
+ ) -> ReleaseCheckReport:
384
+ """Execute the release-check workflow."""
385
+ diagnostics = DiagnosticCollection()
386
+ if list_checks and plan:
387
+ diagnostics.add_blocked(
388
+ code="core.release_check.invalid_mode",
389
+ message="List-checks and plan modes cannot be combined.",
390
+ path="core_runtime.cli release-check",
391
+ )
392
+ report = ReleaseCheckReport(
393
+ mode="blocked",
394
+ target="",
395
+ target_argument="",
396
+ timeout_seconds=timeout_seconds,
397
+ diagnostics=diagnostics,
398
+ status="blocked",
399
+ )
400
+ return report
401
+
402
+ if profile and group:
403
+ diagnostics.add_blocked(
404
+ code="core.release_check.invalid_mode",
405
+ message="Profile and group modes cannot be combined.",
406
+ path="core_runtime.cli release-check",
407
+ )
408
+ report = ReleaseCheckReport(
409
+ mode="blocked",
410
+ target="",
411
+ target_argument="",
412
+ timeout_seconds=timeout_seconds,
413
+ diagnostics=diagnostics,
414
+ status="blocked",
415
+ )
416
+ return report
417
+
418
+ if profile and preflight_only:
419
+ diagnostics.add_blocked(
420
+ code="core.release_check.invalid_mode",
421
+ message="Profile and preflight-only modes cannot be combined.",
422
+ path="core_runtime.cli release-check",
423
+ )
424
+ report = ReleaseCheckReport(
425
+ mode="blocked",
426
+ target="",
427
+ target_argument="",
428
+ timeout_seconds=timeout_seconds,
429
+ diagnostics=diagnostics,
430
+ status="blocked",
431
+ )
432
+ return report
433
+
434
+ if profile and profile not in RELEASE_CHECK_PROFILES:
435
+ diagnostics.add_blocked(
436
+ code="core.release_check.profile_unknown",
437
+ message="Unknown release-check profile.",
438
+ path="core_runtime.cli release-check",
439
+ details={"profile": profile, "allowed_profiles": sorted(RELEASE_CHECK_PROFILES)},
440
+ )
441
+ report = ReleaseCheckReport(
442
+ mode="blocked",
443
+ target="",
444
+ target_argument="",
445
+ timeout_seconds=timeout_seconds,
446
+ diagnostics=diagnostics,
447
+ status="blocked",
448
+ )
449
+ return report
450
+
451
+ if list_checks:
452
+ mode = "list-checks"
453
+ elif plan:
454
+ mode = "plan"
455
+ elif profile:
456
+ mode = f"profile:{profile}"
457
+ elif group:
458
+ mode = f"group:{group}"
459
+ elif preflight_only:
460
+ mode = "preflight-only"
461
+ else:
462
+ mode = "full"
463
+ report = ReleaseCheckReport(
464
+ mode=mode,
465
+ target="",
466
+ target_argument="",
467
+ timeout_seconds=timeout_seconds,
468
+ profile=profile,
469
+ diagnostics=diagnostics,
470
+ )
471
+
472
+ try:
473
+ normalized_target, gate_target_argument = self.resolve_target(target)
474
+ except ValueError as exc:
475
+ diagnostics.add_blocked(
476
+ code="core.release_check.invalid_target",
477
+ message=str(exc),
478
+ path="target",
479
+ )
480
+ report.status = "blocked"
481
+ report.preflight_checks["status"] = "blocked"
482
+ report.target = normalize_release_target(target) if target else (self.version_inventory.get_canonical_version() or "")
483
+ return report
484
+
485
+ report.target = normalized_target
486
+ report.target_argument = gate_target_argument
487
+ report.preflight_checks["canonical_version"] = self.version_inventory.get_canonical_version()
488
+ report.preflight_checks["target_argument"] = gate_target_argument
489
+ report.preflight_checks["target_valid"] = True
490
+ if profile:
491
+ profile_definition = self._profile_definition(profile)
492
+ report.profile_definition = profile_definition
493
+ report.preflight_checks["profile"] = profile
494
+ report.preflight_checks["profile_groups"] = list(profile_definition["groups"])
495
+ report.preflight_checks["profile_description"] = profile_definition["description"]
496
+
497
+ if not skip_tooling_lint:
498
+ lint_capture = self._run_tooling_lint(timeout_seconds)
499
+ report.tooling_lint = lint_capture
500
+ report.preflight_checks["tooling_lint"] = lint_capture.status
501
+ if lint_capture.status == "blocked":
502
+ diagnostics.add_blocked(
503
+ code="core.release_check.tooling_lint_blocked",
504
+ message="Tooling lint precheck was blocked; release gate not run.",
505
+ path="core_runtime.cli lint",
506
+ )
507
+ report.preflight_checks["status"] = "blocked"
508
+ report.status = "blocked"
509
+ return report
510
+ if lint_capture.status == "error":
511
+ diagnostics.add_error(
512
+ code="core.release_check.tooling_lint_failed",
513
+ message="Tooling lint precheck failed; release gate not run.",
514
+ path="core_runtime.cli lint",
515
+ details="exit_code={0}".format(lint_capture.returncode),
516
+ )
517
+ report.preflight_checks["status"] = "error"
518
+ report.status = "error"
519
+ return report
520
+ if lint_capture.status == "internal_error":
521
+ diagnostics.add_error(
522
+ code="core.release_check.tooling_lint_internal_error",
523
+ message="Tooling lint precheck had an internal error; release gate not run.",
524
+ path="core_runtime.cli lint",
525
+ details="exit_code={0}".format(lint_capture.returncode),
526
+ )
527
+ report.preflight_checks["status"] = "internal_error"
528
+ report.status = "internal_error"
529
+ return report
530
+ lint_summary = lint_capture.json_payload.get("summary", {}) if lint_capture.json_payload else {}
531
+ warnings = int(lint_summary.get("warning", 0) or 0)
532
+ if warnings:
533
+ diagnostics.add_warning(
534
+ code="core.release_check.tooling_lint_warning",
535
+ message="Tooling lint completed with warnings.",
536
+ path="core_runtime.cli lint",
537
+ details="warning_count={0}".format(warnings),
538
+ )
539
+ report.preflight_checks["tooling_lint_warnings"] = warnings
540
+ else:
541
+ report.preflight_checks["tooling_lint_warnings"] = 0
542
+ else:
543
+ report.preflight_checks["tooling_lint"] = "skipped"
544
+ report.preflight_checks["tooling_lint_warnings"] = None
545
+
546
+ script_path = self.repo_root / "scripts" / "verify_release.py"
547
+ report.preflight_checks["script_path"] = str(script_path)
548
+ script_preflight = self._check_release_script(
549
+ script_path,
550
+ gate_target_argument,
551
+ timeout_seconds,
552
+ require_help=preflight_only,
553
+ debug=debug,
554
+ )
555
+ report.release_gate_help = script_preflight["help_capture"]
556
+ report.preflight_checks["script_exists"] = script_preflight["script_exists"]
557
+ report.preflight_checks["script_compiles"] = script_preflight["script_compiles"]
558
+ report.preflight_checks["help_status"] = script_preflight["help_status"]
559
+ report.preflight_checks["help_contains_target"] = script_preflight["help_contains_target"]
560
+
561
+ if script_preflight["status"] != "pass":
562
+ for diagnostic in script_preflight["diagnostics"]:
563
+ diagnostics.add(diagnostic)
564
+ report.preflight_checks["status"] = script_preflight["status"]
565
+ report.status = script_preflight["status"]
566
+ self._finalize_status(report)
567
+ return report
568
+
569
+ if preflight_only:
570
+ report.preflight_checks["status"] = "pass"
571
+ self._finalize_status(report)
572
+ return report
573
+
574
+ if list_checks or plan:
575
+ gate_capture = self._run_release_gate(
576
+ script_path,
577
+ gate_target_argument,
578
+ timeout_seconds,
579
+ group=group,
580
+ list_checks=list_checks,
581
+ plan=plan,
582
+ timing_json=timing_json,
583
+ )
584
+ report.release_gate = gate_capture
585
+ report.preflight_checks["status"] = "pass"
586
+ report.preflight_checks["release_gate"] = gate_capture.status
587
+ self._finalize_status(report)
588
+ return report
589
+
590
+ if profile:
591
+ profile_groups = list(report.profile_definition.get("groups", ())) if report.profile_definition else []
592
+ capture = None
593
+ for selected_group in profile_groups:
594
+ capture = self._run_release_gate(
595
+ script_path,
596
+ gate_target_argument,
597
+ timeout_seconds,
598
+ group=selected_group,
599
+ list_checks=list_checks,
600
+ plan=plan,
601
+ timing_json=timing_json,
602
+ )
603
+ report.profile_runs.append(capture)
604
+ report.release_gate = capture
605
+ if capture.timed_out:
606
+ diagnostics.add_blocked(
607
+ code="core.release_check.profile_group_timeout",
608
+ message="Release profile subgroup did not complete within the configured timeout.",
609
+ path="scripts/verify_release.py",
610
+ details={"profile": profile, "group": selected_group, "timeout_seconds": timeout_seconds},
611
+ )
612
+ break
613
+ if capture.status == "blocked":
614
+ diagnostics.add_blocked(
615
+ code="core.release_check.profile_group_blocked",
616
+ message="Release profile subgroup returned a blocked result.",
617
+ path="scripts/verify_release.py",
618
+ details={"profile": profile, "group": selected_group, "exit_code": capture.returncode},
619
+ )
620
+ break
621
+ if capture.status == "error":
622
+ diagnostics.add_error(
623
+ code="core.release_check.profile_group_failed",
624
+ message="Release profile subgroup returned a failing result.",
625
+ path="scripts/verify_release.py",
626
+ details={"profile": profile, "group": selected_group, "exit_code": capture.returncode},
627
+ )
628
+ break
629
+ if capture.status == "internal_error":
630
+ diagnostics.add_error(
631
+ code="core.release_check.profile_group_internal_error",
632
+ message="Release profile subgroup had an internal error.",
633
+ path="scripts/verify_release.py",
634
+ details={"profile": profile, "group": selected_group, "exit_code": capture.returncode},
635
+ )
636
+ break
637
+ if capture is None:
638
+ diagnostics.add_blocked(
639
+ code="core.release_check.profile_empty",
640
+ message="Release profile did not resolve to any groups.",
641
+ path="core_runtime.cli release-check",
642
+ details={"profile": profile},
643
+ )
644
+ report.preflight_checks["status"] = "pass"
645
+ else:
646
+ gate_capture = self._run_release_gate(
647
+ script_path,
648
+ gate_target_argument,
649
+ timeout_seconds,
650
+ group=group,
651
+ list_checks=list_checks,
652
+ plan=plan,
653
+ timing_json=timing_json,
654
+ )
655
+ report.release_gate = gate_capture
656
+ report.preflight_checks["status"] = "pass"
657
+ report.preflight_checks["release_gate"] = gate_capture.status
658
+ if gate_capture.timed_out:
659
+ diagnostics.add_blocked(
660
+ code="core.release_check.timeout",
661
+ message="Release gate did not complete within the configured timeout.",
662
+ path="scripts/verify_release.py",
663
+ details={
664
+ "timeout_seconds": timeout_seconds,
665
+ "script": "scripts/verify_release.py",
666
+ },
667
+ )
668
+ elif gate_capture.status == "blocked":
669
+ diagnostics.add_blocked(
670
+ code="core.release_check.release_gate_blocked",
671
+ message="Release gate returned a blocked result.",
672
+ path="scripts/verify_release.py",
673
+ details="exit_code={0}".format(gate_capture.returncode),
674
+ )
675
+ elif gate_capture.status == "error":
676
+ diagnostics.add_error(
677
+ code="core.release_check.release_gate_failed",
678
+ message="Release gate returned a failing result.",
679
+ path="scripts/verify_release.py",
680
+ details="exit_code={0}".format(gate_capture.returncode),
681
+ )
682
+ elif gate_capture.status == "internal_error":
683
+ diagnostics.add_error(
684
+ code="core.release_check.release_gate_internal_error",
685
+ message="Release gate subprocess had an internal error.",
686
+ path="scripts/verify_release.py",
687
+ details="exit_code={0}".format(gate_capture.returncode),
688
+ )
689
+
690
+ if debug:
691
+ report.debug = {
692
+ "tooling_lint_command": report.tooling_lint.command if report.tooling_lint else None,
693
+ "release_gate_help_command": report.release_gate_help.command if report.release_gate_help else None,
694
+ "release_gate_command": report.release_gate.command if report.release_gate else None,
695
+ "release_gate_cwd": report.release_gate.cwd if report.release_gate else None,
696
+ "release_gate_target_argument": gate_target_argument,
697
+ "timeout_seconds": timeout_seconds,
698
+ "elapsed_seconds": report.release_gate.elapsed_seconds if report.release_gate else None,
699
+ "stdout_preview": report.release_gate.stdout_preview if report.release_gate else "",
700
+ "stderr_preview": report.release_gate.stderr_preview if report.release_gate else "",
701
+ }
702
+
703
+ self._finalize_status(report)
704
+ return report
705
+
706
+ def _finalize_status(self, report: ReleaseCheckReport) -> None:
707
+ counts = report.diagnostics.count_by_severity()
708
+ report.summary.update(
709
+ {
710
+ "info": counts["info"],
711
+ "warning": counts["warning"],
712
+ "error": counts["error"],
713
+ "blocked": counts["blocked"],
714
+ "tooling_lint_status": report.tooling_lint.status if report.tooling_lint else report.summary.get("tooling_lint_status", "skipped"),
715
+ "release_gate_status": report.release_gate.status if report.release_gate else report.summary.get("release_gate_status", "skipped"),
716
+ "preflight_status": report.preflight_checks.get("status", "skipped"),
717
+ "release_gate_target_argument": report.target_argument,
718
+ }
719
+ )
720
+ if report.diagnostics.has_blocked():
721
+ report.status = "blocked"
722
+ elif report.diagnostics.has_errors():
723
+ report.status = "error"
724
+ elif counts["warning"] > 0:
725
+ report.status = "warning"
726
+ else:
727
+ report.status = "pass"
728
+
729
+ def _profile_definition(self, profile: str) -> dict[str, Any]:
730
+ definition = RELEASE_CHECK_PROFILES[profile]
731
+ return {
732
+ "name": profile,
733
+ "description": definition["description"],
734
+ "groups": tuple(definition["groups"]),
735
+ "notes": tuple(definition["notes"]),
736
+ }
737
+
738
+ def _check_release_script(
739
+ self,
740
+ script_path: Path,
741
+ target_argument: str,
742
+ timeout_seconds: int,
743
+ require_help: bool,
744
+ debug: bool,
745
+ ) -> dict[str, Any]:
746
+ diagnostics = []
747
+ help_capture: SubprocessCapture | None = None
748
+
749
+ if not script_path.exists():
750
+ diagnostics.append(
751
+ self._blocked_diagnostic(
752
+ code="core.release_check.release_script_missing",
753
+ message="Release gate script is missing.",
754
+ path=str(script_path.relative_to(self.repo_root)) if script_path.is_relative_to(self.repo_root) else str(script_path),
755
+ )
756
+ )
757
+ return {
758
+ "status": "blocked",
759
+ "script_exists": False,
760
+ "script_compiles": False,
761
+ "help_status": "skipped",
762
+ "help_contains_target": False,
763
+ "help_capture": None,
764
+ "diagnostics": diagnostics,
765
+ }
766
+
767
+ try:
768
+ py_compile.compile(str(script_path), doraise=True)
769
+ except py_compile.PyCompileError as exc:
770
+ diagnostics.append(
771
+ self._blocked_diagnostic(
772
+ code="core.release_check.release_script_compile_error",
773
+ message="Release gate script failed to compile.",
774
+ path="scripts/verify_release.py",
775
+ details=str(exc.msg),
776
+ )
777
+ )
778
+ return {
779
+ "status": "blocked",
780
+ "script_exists": True,
781
+ "script_compiles": False,
782
+ "help_status": "skipped",
783
+ "help_contains_target": False,
784
+ "help_capture": None,
785
+ "diagnostics": diagnostics,
786
+ }
787
+
788
+ if require_help:
789
+ help_cmd = [sys.executable, str(script_path), "--help"]
790
+ help_capture = self._run_command(help_cmd, timeout_seconds=timeout_seconds, expect_json=False)
791
+ help_text = "{0}\n{1}".format(help_capture.stdout, help_capture.stderr)
792
+ help_contains_target = "--target" in help_text
793
+ help_capture.help_contains_target = help_contains_target
794
+ if help_capture.status != "pass":
795
+ diagnostics.append(
796
+ self._blocked_diagnostic(
797
+ code="core.release_check.help_check_failed",
798
+ message="Release gate help did not complete successfully.",
799
+ path="scripts/verify_release.py",
800
+ details={
801
+ "timeout_seconds": timeout_seconds,
802
+ "script": "scripts/verify_release.py",
803
+ },
804
+ )
805
+ )
806
+ return {
807
+ "status": "blocked",
808
+ "script_exists": True,
809
+ "script_compiles": True,
810
+ "help_status": help_capture.status,
811
+ "help_contains_target": help_contains_target,
812
+ "help_capture": help_capture,
813
+ "diagnostics": diagnostics,
814
+ }
815
+
816
+ if not help_contains_target:
817
+ diagnostics.append(
818
+ self._blocked_diagnostic(
819
+ code="core.release_check.target_mapping_unverified",
820
+ message="Release gate help output does not advertise the expected target argument.",
821
+ path="scripts/verify_release.py",
822
+ details={
823
+ "expected": "vMAJOR.MINOR.PATCH",
824
+ "observed": "help output missing --target",
825
+ },
826
+ )
827
+ )
828
+ return {
829
+ "status": "blocked",
830
+ "script_exists": True,
831
+ "script_compiles": True,
832
+ "help_status": help_capture.status,
833
+ "help_contains_target": help_contains_target,
834
+ "help_capture": help_capture,
835
+ "diagnostics": diagnostics,
836
+ }
837
+
838
+ if debug:
839
+ help_capture.stdout_preview, help_capture.stdout_truncated = _truncate_text(help_capture.stdout)
840
+ help_capture.stderr_preview, help_capture.stderr_truncated = _truncate_text(help_capture.stderr)
841
+
842
+ return {
843
+ "status": "pass",
844
+ "script_exists": True,
845
+ "script_compiles": True,
846
+ "help_status": help_capture.status,
847
+ "help_contains_target": help_contains_target,
848
+ "help_capture": help_capture,
849
+ "diagnostics": diagnostics,
850
+ }
851
+
852
+ return {
853
+ "status": "pass",
854
+ "script_exists": True,
855
+ "script_compiles": True,
856
+ "help_status": "skipped",
857
+ "help_contains_target": None,
858
+ "help_capture": None,
859
+ "diagnostics": diagnostics,
860
+ }
861
+
862
+ def _run_tooling_lint(self, timeout_seconds: int) -> SubprocessCapture:
863
+ cmd = [
864
+ sys.executable,
865
+ "-m",
866
+ "core_runtime.cli",
867
+ "lint",
868
+ "--scope",
869
+ "tooling",
870
+ "--format",
871
+ "json",
872
+ ]
873
+ return self._run_command(cmd, timeout_seconds=timeout_seconds, expect_json=True)
874
+
875
+ def _run_release_gate(
876
+ self,
877
+ script_path: Path,
878
+ target_argument: str,
879
+ timeout_seconds: int,
880
+ group: Optional[str] = None,
881
+ list_checks: bool = False,
882
+ plan: bool = False,
883
+ timing_json: Optional[str] = None,
884
+ ) -> SubprocessCapture:
885
+ cmd = [
886
+ sys.executable,
887
+ str(script_path),
888
+ "--target",
889
+ target_argument,
890
+ ]
891
+ if group:
892
+ cmd.extend(["--group", group])
893
+ if list_checks:
894
+ cmd.append("--list-checks")
895
+ if plan:
896
+ cmd.append("--plan")
897
+ if timing_json:
898
+ cmd.extend(["--timing-json", timing_json])
899
+ capture = self._run_command(cmd, timeout_seconds=timeout_seconds, expect_json=True, target_argument=target_argument)
900
+ if capture.json_payload is not None and isinstance(capture.json_payload, dict):
901
+ report_path = capture.json_payload.get("report_path")
902
+ if isinstance(report_path, str):
903
+ capture.report_path = report_path
904
+ return capture
905
+
906
+ def _run_command(
907
+ self,
908
+ command: list[str],
909
+ timeout_seconds: int,
910
+ expect_json: bool,
911
+ target_argument: Optional[str] = None,
912
+ ) -> SubprocessCapture:
913
+ cwd = str(self.repo_root)
914
+ start = time.monotonic()
915
+ try:
916
+ completed = subprocess.run(
917
+ command,
918
+ cwd=self.repo_root,
919
+ input="",
920
+ text=True,
921
+ capture_output=True,
922
+ timeout=timeout_seconds,
923
+ check=False,
924
+ )
925
+ elapsed = time.monotonic() - start
926
+ stdout = _coerce_text(completed.stdout)
927
+ stderr = _coerce_text(completed.stderr)
928
+ json_payload: dict[str, Any] | None = None
929
+ json_detected = False
930
+ if expect_json and stdout.strip():
931
+ try:
932
+ json_payload = json.loads(stdout)
933
+ json_detected = True
934
+ except json.JSONDecodeError:
935
+ json_payload = None
936
+
937
+ stdout_preview, stdout_truncated = _truncate_text(stdout)
938
+ stderr_preview, stderr_truncated = _truncate_text(stderr)
939
+ return SubprocessCapture(
940
+ command=command,
941
+ cwd=cwd,
942
+ expect_json=expect_json,
943
+ returncode=completed.returncode,
944
+ stdout=stdout,
945
+ stderr=stderr,
946
+ timed_out=False,
947
+ timeout_seconds=timeout_seconds,
948
+ elapsed_seconds=elapsed,
949
+ json_detected=json_detected,
950
+ json_payload=json_payload,
951
+ report_path=None,
952
+ target_argument=target_argument,
953
+ stdout_preview=stdout_preview,
954
+ stderr_preview=stderr_preview,
955
+ stdout_truncated=stdout_truncated,
956
+ stderr_truncated=stderr_truncated,
957
+ )
958
+ except subprocess.TimeoutExpired as exc:
959
+ elapsed = time.monotonic() - start
960
+ stdout = _coerce_text(exc.output)
961
+ stderr = _coerce_text(exc.stderr)
962
+ stdout_preview, stdout_truncated = _truncate_text(stdout)
963
+ stderr_preview, stderr_truncated = _truncate_text(stderr)
964
+ return SubprocessCapture(
965
+ command=command,
966
+ cwd=cwd,
967
+ expect_json=expect_json,
968
+ returncode=None,
969
+ stdout=stdout,
970
+ stderr=stderr,
971
+ timed_out=True,
972
+ timeout_seconds=timeout_seconds,
973
+ elapsed_seconds=elapsed,
974
+ json_detected=False,
975
+ json_payload=None,
976
+ report_path=None,
977
+ target_argument=target_argument,
978
+ stdout_preview=stdout_preview,
979
+ stderr_preview=stderr_preview,
980
+ stdout_truncated=stdout_truncated,
981
+ stderr_truncated=stderr_truncated,
982
+ )
983
+ except (FileNotFoundError, OSError) as exc:
984
+ elapsed = time.monotonic() - start
985
+ stderr = str(exc)
986
+ stdout_preview, stdout_truncated = _truncate_text("")
987
+ stderr_preview, stderr_truncated = _truncate_text(stderr)
988
+ return SubprocessCapture(
989
+ command=command,
990
+ cwd=cwd,
991
+ expect_json=expect_json,
992
+ returncode=None,
993
+ stdout="",
994
+ stderr=stderr,
995
+ timed_out=False,
996
+ timeout_seconds=timeout_seconds,
997
+ elapsed_seconds=elapsed,
998
+ json_detected=False,
999
+ json_payload=None,
1000
+ report_path=None,
1001
+ target_argument=target_argument,
1002
+ stdout_preview=stdout_preview,
1003
+ stderr_preview=stderr_preview,
1004
+ stdout_truncated=stdout_truncated,
1005
+ stderr_truncated=stderr_truncated,
1006
+ )
1007
+
1008
+ def _blocked_diagnostic(
1009
+ self,
1010
+ code: str,
1011
+ message: str,
1012
+ path: str,
1013
+ details: Any = None,
1014
+ ):
1015
+ diag = DiagnosticCollection()
1016
+ diag.add_blocked(code=code, message=message, path=path, details=details)
1017
+ return diag.diagnostics[-1]