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,458 @@
1
+ """Dry-run-first artifact path repair planning for CORE tooling."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import json
6
+ from dataclasses import dataclass, field
7
+ from pathlib import Path
8
+ from typing import Any, Iterable
9
+
10
+ from core_runtime.tooling.diagnostics import DiagnosticCollection
11
+
12
+
13
+ _TEXT_SUFFIXES = {
14
+ ".json",
15
+ ".jsonl",
16
+ ".md",
17
+ ".txt",
18
+ ".csv",
19
+ ".toml",
20
+ ".yaml",
21
+ ".yml",
22
+ ".py",
23
+ }
24
+
25
+ _IMMUTABLE_PATH_HINTS = (
26
+ "core_runtime/data/runtime_release_manifest_",
27
+ "core_runtime/data/runtime_reports/",
28
+ "docs/archive/",
29
+ "docs/releases/",
30
+ "private/reports/",
31
+ )
32
+
33
+
34
+ @dataclass(frozen=True)
35
+ class ArtifactPathRepairRule:
36
+ """A deterministic source-to-destination path replacement rule."""
37
+
38
+ source: str
39
+ destination: str
40
+ kind: str
41
+
42
+ def to_dict(self) -> dict[str, str]:
43
+ return {
44
+ "source": self.source,
45
+ "destination": self.destination,
46
+ "kind": self.kind,
47
+ }
48
+
49
+
50
+ @dataclass(frozen=True)
51
+ class ArtifactRepairItem:
52
+ """A single planned artifact repair item."""
53
+
54
+ kind: str
55
+ name: str
56
+ path: str
57
+ status: str
58
+ details: dict[str, Any] = field(default_factory=dict)
59
+
60
+ def to_dict(self) -> dict[str, Any]:
61
+ result = {
62
+ "kind": self.kind,
63
+ "name": self.name,
64
+ "path": self.path,
65
+ "status": self.status,
66
+ }
67
+ if self.details:
68
+ result["details"] = self.details
69
+ return result
70
+
71
+
72
+ @dataclass
73
+ class ArtifactRepairReport:
74
+ """Normalized report for `core-runtime repair-artifact-paths`."""
75
+
76
+ tool: str
77
+ command: str
78
+ status: str
79
+ mutation_performed: bool = False
80
+ summary: dict[str, Any] = field(default_factory=dict)
81
+ selection: dict[str, Any] | None = None
82
+ items: list[ArtifactRepairItem] = field(default_factory=list)
83
+ rules: list[ArtifactPathRepairRule] = field(default_factory=list)
84
+ diagnostics: DiagnosticCollection = field(default_factory=DiagnosticCollection)
85
+
86
+ def to_dict(self) -> dict[str, Any]:
87
+ counts = self.diagnostics.count_by_severity()
88
+ summary = dict(self.summary)
89
+ summary.setdefault("info", counts["info"])
90
+ summary.setdefault("warning", counts["warning"])
91
+ summary.setdefault("error", counts["error"])
92
+ summary.setdefault("blocked", counts["blocked"])
93
+ return {
94
+ "tool": self.tool,
95
+ "command": self.command,
96
+ "status": self.status,
97
+ "mutation_performed": self.mutation_performed,
98
+ "summary": summary,
99
+ "selection": self.selection,
100
+ "rules": [rule.to_dict() for rule in self.rules],
101
+ "items": [item.to_dict() for item in self.items],
102
+ "diagnostics": [d.to_dict() for d in self.diagnostics.diagnostics],
103
+ }
104
+
105
+ def to_markdown(self) -> str:
106
+ counts = self.diagnostics.count_by_severity()
107
+ summary = dict(self.summary)
108
+ summary.setdefault("info", counts["info"])
109
+ summary.setdefault("warning", counts["warning"])
110
+ summary.setdefault("error", counts["error"])
111
+ summary.setdefault("blocked", counts["blocked"])
112
+ lines: list[str] = []
113
+ lines.append("# CORE repair-artifact-paths plan")
114
+ lines.append("")
115
+ lines.append("## Summary")
116
+ lines.append("")
117
+ lines.append("| Metric | Value |")
118
+ lines.append("|--------|-------|")
119
+ lines.append("| Tool | {0} |".format(self.tool))
120
+ lines.append("| Command | {0} |".format(self.command))
121
+ lines.append("| Status | {0} |".format(self.status.upper()))
122
+ lines.append("| Mutation Performed | No |")
123
+ lines.append("| Items | {0} |".format(summary.get("item_count", len(self.items))))
124
+ lines.append("| Planned Repairs | {0} |".format(summary.get("planned_repairs", 0)))
125
+ lines.append("| Immutable Hits | {0} |".format(summary.get("immutable_hits", 0)))
126
+ lines.append("| Scanned Files | {0} |".format(summary.get("scanned_files", 0)))
127
+ lines.append("| Info | {0} |".format(counts["info"]))
128
+ lines.append("| Warning | {0} |".format(counts["warning"]))
129
+ lines.append("| Error | {0} |".format(counts["error"]))
130
+ lines.append("| Blocked | {0} |".format(counts["blocked"]))
131
+ lines.append("")
132
+
133
+ if self.selection is not None:
134
+ lines.append("## Selection")
135
+ lines.append("")
136
+ for key, value in self.selection.items():
137
+ lines.append("- **{0}**: {1}".format(key, value))
138
+ lines.append("")
139
+
140
+ if self.rules:
141
+ lines.append("## Repair Rules")
142
+ lines.append("")
143
+ lines.append("| Kind | Source | Destination |")
144
+ lines.append("|------|--------|-------------|")
145
+ for rule in self.rules:
146
+ lines.append("| {0} | {1} | {2} |".format(rule.kind, rule.source, rule.destination))
147
+ lines.append("")
148
+
149
+ if self.items:
150
+ lines.append("## Planned Repairs")
151
+ lines.append("")
152
+ lines.append("| Kind | Name | Path | Status |")
153
+ lines.append("|------|------|------|--------|")
154
+ for item in self.items:
155
+ lines.append("| {0} | {1} | {2} | {3} |".format(item.kind, item.name, item.path, item.status))
156
+ lines.append("")
157
+
158
+ if self.diagnostics.diagnostics:
159
+ lines.append("## Diagnostics")
160
+ lines.append("")
161
+ lines.append("| Severity | Code | Path | Message |")
162
+ lines.append("|----------|------|------|---------|")
163
+ for diagnostic in self.diagnostics.diagnostics:
164
+ path = diagnostic.path or "-"
165
+ message = diagnostic.message.replace("|", "\\|")
166
+ lines.append(
167
+ "| {0} | {1} | {2} | {3} |".format(
168
+ diagnostic.severity.value.upper(),
169
+ diagnostic.code,
170
+ path,
171
+ message,
172
+ )
173
+ )
174
+ lines.append("")
175
+
176
+ return "\n".join(lines)
177
+
178
+
179
+ class ArtifactPathRepairPlanner:
180
+ """Plan artifact path repairs without mutating the repository."""
181
+
182
+ def __init__(self, repo_root: Path):
183
+ self.repo_root = repo_root
184
+
185
+ def build_plan(
186
+ self,
187
+ *,
188
+ dry_run: bool,
189
+ source: str | None = None,
190
+ destination: str | None = None,
191
+ manifest_path: Path | None = None,
192
+ apply: bool = False,
193
+ diagnostics: DiagnosticCollection | None = None,
194
+ ) -> ArtifactRepairReport:
195
+ diagnostics = diagnostics or DiagnosticCollection()
196
+ selection = {
197
+ "mode": "dry-run" if dry_run else "apply",
198
+ "source": source,
199
+ "destination": destination,
200
+ }
201
+
202
+ if apply:
203
+ diagnostics.add_blocked(
204
+ code="core.repair_artifact_paths.apply_not_enabled",
205
+ message="repair-artifact-paths apply mode is not enabled in this slice",
206
+ path="repair-artifact-paths",
207
+ expected="dry-run-only preflight",
208
+ actual="apply requested",
209
+ )
210
+ if not dry_run:
211
+ diagnostics.add_blocked(
212
+ code="core.repair_artifact_paths.dry_run_required",
213
+ message="repair-artifact-paths requires --dry-run in this slice",
214
+ path="repair-artifact-paths",
215
+ expected="--dry-run",
216
+ actual="missing dry-run",
217
+ )
218
+
219
+ rules, rule_selection = self._build_rules(source=source, destination=destination, manifest_path=manifest_path, diagnostics=diagnostics)
220
+ selection.update(rule_selection)
221
+ if diagnostics.has_blocked():
222
+ return ArtifactRepairReport(
223
+ tool="core-runtime repair-artifact-paths",
224
+ command=self._command(source=source, destination=destination, dry_run=dry_run),
225
+ status="blocked",
226
+ summary={"mode": selection["mode"], "item_count": 0, "planned_repairs": 0, "immutable_hits": 0, "scanned_files": 0},
227
+ selection=selection,
228
+ rules=rules,
229
+ diagnostics=diagnostics,
230
+ )
231
+
232
+ scanned_files = list(self._scan_files())
233
+ items: list[ArtifactRepairItem] = []
234
+ planned_repairs = 0
235
+ immutable_hits = 0
236
+ mutable_hits = 0
237
+
238
+ for path in scanned_files:
239
+ try:
240
+ text = path.read_text(encoding="utf-8")
241
+ except (OSError, UnicodeDecodeError):
242
+ continue
243
+
244
+ matches = self._find_matches(text, rules)
245
+ if not matches:
246
+ continue
247
+
248
+ rel_path = path.relative_to(self.repo_root).as_posix()
249
+ immutable = self._is_immutable_path(rel_path)
250
+ replacement_preview = self._preview_replacement(text, matches[0])
251
+ item_details = {
252
+ "match_count": sum(match["occurrences"] for match in matches),
253
+ "matches": matches,
254
+ "replacement_preview": replacement_preview,
255
+ "immutable": immutable,
256
+ }
257
+
258
+ if immutable:
259
+ immutable_hits += 1
260
+ diagnostics.add_blocked(
261
+ code="core.repair_artifact_paths.immutable_reference",
262
+ message="Immutable artifact contains a path reference that must not be rewritten",
263
+ path=rel_path,
264
+ expected="preserve historical evidence",
265
+ actual=matches[0]["source"],
266
+ details=matches[0]["destination"],
267
+ )
268
+ status = "blocked"
269
+ else:
270
+ mutable_hits += 1
271
+ planned_repairs += 1
272
+ diagnostics.add_warning(
273
+ code="core.repair_artifact_paths.mutable_reference",
274
+ message="Mutable artifact contains a path reference that can be repaired in a later apply slice",
275
+ path=rel_path,
276
+ expected=matches[0]["destination"],
277
+ actual=matches[0]["source"],
278
+ details="; ".join(
279
+ "{0}->{1} x{2}".format(match["source"], match["destination"], match["occurrences"]) for match in matches
280
+ ),
281
+ )
282
+ status = "planned_repair"
283
+
284
+ items.append(
285
+ ArtifactRepairItem(
286
+ kind="immutable_artifact" if immutable else "mutable_artifact",
287
+ name=path.name,
288
+ path=rel_path,
289
+ status=status,
290
+ details=item_details,
291
+ )
292
+ )
293
+
294
+ report_status = "pass"
295
+ if diagnostics.has_blocked():
296
+ report_status = "blocked"
297
+ elif diagnostics.has_errors():
298
+ report_status = "error"
299
+ elif planned_repairs or mutable_hits:
300
+ report_status = "warning"
301
+
302
+ return ArtifactRepairReport(
303
+ tool="core-runtime repair-artifact-paths",
304
+ command=self._command(source=source, destination=destination, dry_run=dry_run),
305
+ status=report_status,
306
+ summary={
307
+ "mode": selection["mode"],
308
+ "item_count": len(items),
309
+ "planned_repairs": planned_repairs,
310
+ "immutable_hits": immutable_hits,
311
+ "scanned_files": len(scanned_files),
312
+ "rule_count": len(rules),
313
+ },
314
+ selection=selection,
315
+ items=items,
316
+ rules=rules,
317
+ diagnostics=diagnostics,
318
+ )
319
+
320
+ def _build_rules(
321
+ self,
322
+ *,
323
+ source: str | None,
324
+ destination: str | None,
325
+ manifest_path: Path | None,
326
+ diagnostics: DiagnosticCollection,
327
+ ) -> tuple[list[ArtifactPathRepairRule], dict[str, Any]]:
328
+ selection: dict[str, Any] = {}
329
+ if manifest_path is None:
330
+ manifest_path = self.repo_root / "core_runtime" / "data" / "artifact_migration_manifest.json"
331
+
332
+ if source or destination:
333
+ if not source or not destination:
334
+ diagnostics.add_blocked(
335
+ code="core.repair_artifact_paths.rule_incomplete",
336
+ message="Targeted repair requires both --from and --to",
337
+ path="repair-artifact-paths",
338
+ expected="paired source and destination paths",
339
+ actual="missing rule endpoint",
340
+ )
341
+ return [], selection
342
+ rules = [ArtifactPathRepairRule(source=source, destination=destination, kind="targeted")]
343
+ selection["rules_source"] = "explicit"
344
+ return rules, selection
345
+
346
+ rules = self._rules_from_manifest(manifest_path, diagnostics)
347
+ selection["rules_source"] = str(manifest_path.relative_to(self.repo_root)) if manifest_path.exists() else str(manifest_path)
348
+ return rules, selection
349
+
350
+ def _rules_from_manifest(
351
+ self,
352
+ manifest_path: Path,
353
+ diagnostics: DiagnosticCollection,
354
+ ) -> list[ArtifactPathRepairRule]:
355
+ if not manifest_path.exists():
356
+ diagnostics.add_blocked(
357
+ code="core.repair_artifact_paths.manifest_missing",
358
+ message="Artifact migration manifest not found",
359
+ path=str(manifest_path.relative_to(self.repo_root)),
360
+ expected="artifact migration manifest",
361
+ actual="missing",
362
+ )
363
+ return []
364
+
365
+ try:
366
+ payload = json.loads(manifest_path.read_text(encoding="utf-8"))
367
+ except (OSError, json.JSONDecodeError) as exc:
368
+ diagnostics.add_blocked(
369
+ code="core.repair_artifact_paths.manifest_invalid",
370
+ message="Artifact migration manifest could not be parsed",
371
+ path=str(manifest_path.relative_to(self.repo_root)),
372
+ expected="valid JSON",
373
+ actual=str(exc),
374
+ )
375
+ return []
376
+
377
+ entries = payload.get("migrated_files", [])
378
+ rules: list[ArtifactPathRepairRule] = []
379
+ seen: set[tuple[str, str, str]] = set()
380
+ for entry in entries:
381
+ source = entry.get("source")
382
+ destination = entry.get("destination")
383
+ if not source or not destination:
384
+ continue
385
+ exact = ("exact", source, destination)
386
+ if exact not in seen:
387
+ seen.add(exact)
388
+ rules.append(ArtifactPathRepairRule(source=source, destination=destination, kind="exact"))
389
+
390
+ source_parent = Path(source).parent.as_posix()
391
+ destination_parent = Path(destination).parent.as_posix()
392
+ if source_parent not in {"", "."} and destination_parent not in {"", "."}:
393
+ source_prefix = self._ensure_trailing_slash(source_parent)
394
+ destination_prefix = self._ensure_trailing_slash(destination_parent)
395
+ prefix = ("prefix", source_prefix, destination_prefix)
396
+ if prefix not in seen:
397
+ seen.add(prefix)
398
+ rules.append(ArtifactPathRepairRule(source=source_prefix, destination=destination_prefix, kind="prefix"))
399
+
400
+ rules.sort(key=lambda rule: (-len(rule.source), rule.kind, rule.source, rule.destination))
401
+ return rules
402
+
403
+ def _scan_files(self) -> Iterable[Path]:
404
+ scan_roots = [
405
+ self.repo_root / "core_runtime" / "data",
406
+ self.repo_root / "docs",
407
+ self.repo_root / "private" / "reports",
408
+ ]
409
+ for root in scan_roots:
410
+ if not root.exists():
411
+ continue
412
+ for path in sorted(root.rglob("*")):
413
+ if not path.is_file():
414
+ continue
415
+ if "__pycache__" in path.parts:
416
+ continue
417
+ if not self._is_text_candidate(path):
418
+ continue
419
+ yield path
420
+
421
+ def _find_matches(self, text: str, rules: list[ArtifactPathRepairRule]) -> list[dict[str, Any]]:
422
+ matches: list[dict[str, Any]] = []
423
+ for rule in rules:
424
+ occurrences = text.count(rule.source)
425
+ if occurrences:
426
+ matches.append(
427
+ {
428
+ "source": rule.source,
429
+ "destination": rule.destination,
430
+ "kind": rule.kind,
431
+ "occurrences": occurrences,
432
+ }
433
+ )
434
+ return matches
435
+
436
+ def _preview_replacement(self, text: str, match: dict[str, Any]) -> str:
437
+ replaced = text.replace(match["source"], match["destination"], 1)
438
+ return replaced[:240]
439
+
440
+ def _is_text_candidate(self, path: Path) -> bool:
441
+ if path.suffix.lower() in _TEXT_SUFFIXES:
442
+ return True
443
+ return path.name in {"README", "LICENSE", "Dockerfile"}
444
+
445
+ def _is_immutable_path(self, rel_path: str) -> bool:
446
+ return any(hint in rel_path for hint in _IMMUTABLE_PATH_HINTS)
447
+
448
+ def _command(self, source: str | None, destination: str | None, dry_run: bool) -> str:
449
+ if source and destination:
450
+ base = "repair-artifact-paths --from {0} --to {1}".format(source, destination)
451
+ else:
452
+ base = "repair-artifact-paths"
453
+ if dry_run:
454
+ return base + " --dry-run"
455
+ return base
456
+
457
+ def _ensure_trailing_slash(self, value: str) -> str:
458
+ return value if value.endswith("/") else value + "/"
@@ -0,0 +1,176 @@
1
+ """Report writer - serialize diagnostics to JSON and Markdown."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import json
6
+ from pathlib import Path
7
+ from typing import Optional
8
+
9
+ from core_runtime.tooling.diagnostics import DiagnosticCollection, ExitCode
10
+
11
+
12
+ class ReportWriter:
13
+ """Write lint reports in JSON and Markdown formats."""
14
+
15
+ def __init__(self, repo_root: Path):
16
+ self.repo_root = repo_root
17
+
18
+ def write_json(
19
+ self,
20
+ diagnostics: DiagnosticCollection,
21
+ scope: str,
22
+ output_path: Optional[Path] = None,
23
+ version_sources: Optional[list] = None,
24
+ file_inventory: Optional[dict] = None,
25
+ json_checks: Optional[dict] = None,
26
+ safety_checks: Optional[dict] = None,
27
+ ) -> dict:
28
+ """Generate JSON report structure."""
29
+ exit_code = diagnostics.compute_exit_code()
30
+
31
+ status_map = {
32
+ ExitCode.OK: "pass",
33
+ ExitCode.ERROR: "error",
34
+ ExitCode.BLOCKED: "blocked",
35
+ ExitCode.INTERNAL_ERROR: "internal_error",
36
+ }
37
+
38
+ report = {
39
+ "tool": "core-runtime lint",
40
+ "scope": scope,
41
+ "status": status_map.get(exit_code, "internal_error"),
42
+ "mutation_performed": False,
43
+ "summary": diagnostics.count_by_severity(),
44
+ "diagnostics": [d.to_dict() for d in diagnostics.diagnostics],
45
+ }
46
+
47
+ if version_sources is not None:
48
+ report["version_inventory"] = [v.to_dict() if hasattr(v, "to_dict") else v for v in version_sources]
49
+
50
+ if file_inventory is not None:
51
+ report["file_inventory"] = file_inventory
52
+
53
+ if json_checks is not None:
54
+ report["json_checks"] = json_checks
55
+
56
+ if safety_checks is not None:
57
+ report["safety_checks"] = safety_checks
58
+
59
+ if output_path:
60
+ output_path.parent.mkdir(parents=True, exist_ok=True)
61
+ with output_path.open("w", encoding="utf-8") as f:
62
+ json.dump(report, f, indent=2, ensure_ascii=False)
63
+
64
+ return report
65
+
66
+ def write_markdown(
67
+ self,
68
+ diagnostics: DiagnosticCollection,
69
+ scope: str,
70
+ output_path: Optional[Path] = None,
71
+ version_sources: Optional[list] = None,
72
+ file_inventory: Optional[dict] = None,
73
+ json_checks: Optional[dict] = None,
74
+ safety_checks: Optional[dict] = None,
75
+ ) -> str:
76
+ """Generate Markdown report."""
77
+ exit_code = diagnostics.compute_exit_code()
78
+ counts = diagnostics.count_by_severity()
79
+
80
+ status_map = {
81
+ ExitCode.OK: "PASS",
82
+ ExitCode.ERROR: "ERROR",
83
+ ExitCode.BLOCKED: "BLOCKED",
84
+ ExitCode.INTERNAL_ERROR: "INTERNAL_ERROR",
85
+ }
86
+
87
+ lines = []
88
+ lines.append("# CORE Tooling Lint Report")
89
+ lines.append("")
90
+ lines.append("## Summary")
91
+ lines.append("")
92
+ lines.append("| Metric | Value |")
93
+ lines.append("|--------|-------|")
94
+ lines.append("| Tool | core-runtime lint |")
95
+ lines.append("| Scope | {0} |".format(scope))
96
+ lines.append("| Status | {0} |".format(status_map.get(exit_code, "UNKNOWN")))
97
+ lines.append("| Exit Code | {0} |".format(exit_code.value))
98
+ lines.append("| Mutation Performed | No |")
99
+ lines.append("| Errors | {0} |".format(counts.get("error", 0)))
100
+ lines.append("| Warnings | {0} |".format(counts.get("warning", 0)))
101
+ lines.append("| Info | {0} |".format(counts.get("info", 0)))
102
+ lines.append("| Blocked | {0} |".format(counts.get("blocked", 0)))
103
+ lines.append("")
104
+
105
+ # Diagnostics table
106
+ if diagnostics.diagnostics:
107
+ lines.append("## Diagnostics")
108
+ lines.append("")
109
+ lines.append("| Severity | Code | Path | Message |")
110
+ lines.append("|----------|------|------|---------|")
111
+ for d in diagnostics.diagnostics:
112
+ path = d.path or "-"
113
+ msg = d.message.replace("|", "\\|")
114
+ lines.append("| {0} | {1} | {2} | {3} |".format(d.severity.value.upper(), d.code, path, msg))
115
+ lines.append("")
116
+
117
+ # Version Inventory
118
+ if version_sources:
119
+ lines.append("## Version Inventory")
120
+ lines.append("")
121
+ lines.append("| Source | Version | Canonical |")
122
+ lines.append("|--------|---------|-----------|")
123
+ for v in version_sources:
124
+ if hasattr(v, "version"):
125
+ ver = v.version or "NOT FOUND"
126
+ canon = "Yes" if getattr(v, "is_canonical", False) else "No"
127
+ path = getattr(v, "path", "")
128
+ name = getattr(v, "name", str(path))
129
+ lines.append("| {0} | {1} | {2} |".format(name, ver, canon))
130
+ lines.append("")
131
+
132
+ # File Inventory
133
+ if file_inventory:
134
+ lines.append("## File Inventory")
135
+ lines.append("")
136
+ lines.append("| Path | Exists |")
137
+ lines.append("|------|--------|")
138
+ for path, exists in sorted(file_inventory.items()):
139
+ lines.append("| {0} | {1} |".format(path, "✓" if exists else "✗"))
140
+ lines.append("")
141
+
142
+ # JSON Checks
143
+ if json_checks:
144
+ lines.append("## JSON Checks")
145
+ lines.append("")
146
+ for key, value in json_checks.items():
147
+ lines.append("- **{0}**: {1}".format(key, value))
148
+ lines.append("")
149
+
150
+ # Safety Checks
151
+ if safety_checks:
152
+ lines.append("## Safety Checks")
153
+ lines.append("")
154
+ for key, value in safety_checks.items():
155
+ lines.append("- **{0}**: {1}".format(key, value))
156
+ lines.append("")
157
+
158
+ lines.append("## Final Status")
159
+ lines.append("")
160
+ if exit_code == ExitCode.OK:
161
+ lines.append("✅ **PASS** - No errors or blockers found.")
162
+ elif exit_code == ExitCode.ERROR:
163
+ lines.append("❌ **ERROR** - One or more errors found. Must fix before release.")
164
+ elif exit_code == ExitCode.BLOCKED:
165
+ lines.append("🚫 **BLOCKED** - One or more blockers found. Cannot proceed safely.")
166
+ else:
167
+ lines.append("⚠️ **INTERNAL ERROR** - Tooling failure.")
168
+ lines.append("")
169
+
170
+ markdown = "\n".join(lines)
171
+
172
+ if output_path:
173
+ output_path.parent.mkdir(parents=True, exist_ok=True)
174
+ output_path.write_text(markdown, encoding="utf-8")
175
+
176
+ return markdown