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.
- core_runtime/__init__.py +10 -0
- core_runtime/__version__.py +17 -0
- core_runtime/cli/__init__.py +7 -0
- core_runtime/cli/__main__.py +22 -0
- core_runtime/cli/bump_version.py +176 -0
- core_runtime/cli/contract_preflight.py +69 -0
- core_runtime/cli/create_domain.py +66 -0
- core_runtime/cli/doctor.py +44 -0
- core_runtime/cli/inventory.py +85 -0
- core_runtime/cli/lint.py +8 -0
- core_runtime/cli/main.py +579 -0
- core_runtime/cli/release_check.py +65 -0
- core_runtime/cli/repair_artifact_paths.py +48 -0
- core_runtime/cli/sync_template.py +67 -0
- core_runtime/cli/validate.py +73 -0
- core_runtime/core/__init__.py +34 -0
- core_runtime/core/audit_event.py +760 -0
- core_runtime/core/audit_trail_index.py +100 -0
- core_runtime/core/canonicalization.py +55 -0
- core_runtime/core/contract_evaluator.py +945 -0
- core_runtime/core/contract_executability.py +307 -0
- core_runtime/core/contract_loader.py +57 -0
- core_runtime/core/contract_probes.py +630 -0
- core_runtime/core/contract_program.py +131 -0
- core_runtime/core/contract_program_registry.py +104 -0
- core_runtime/core/contract_program_v2.py +126 -0
- core_runtime/core/dsk_v3.py +142 -0
- core_runtime/core/explainability.py +2115 -0
- core_runtime/core/numeric_normalization.py +120 -0
- core_runtime/core/rule_anchor.py +1388 -0
- core_runtime/core/schema_fingerprint.py +69 -0
- core_runtime/core/sensor_evidence.py +548 -0
- core_runtime/data/contracts/CoreAnchor.sol +109 -0
- core_runtime/data/contracts/CoreRuleAnchor.abi.json +111 -0
- core_runtime/data/contracts/CoreRuleAnchor.bin +1 -0
- core_runtime/data/contracts/CoreRuleAnchor.build.json +20 -0
- core_runtime/data/contracts/CoreRuleAnchor.runtime.bin +1 -0
- core_runtime/data/contracts/CoreRuleAnchor.sol +74 -0
- core_runtime/data/package_data_manifest.v1.json +173 -0
- core_runtime/data/schemas/core/causal_trace.v1.json +141 -0
- core_runtime/data/schemas/core/context_gate.v1.json +38 -0
- core_runtime/data/schemas/core/context_threshold.v1.json +46 -0
- core_runtime/data/schemas/core/contract_program.v1.json +186 -0
- core_runtime/data/schemas/core/contract_program.v2.json +187 -0
- core_runtime/data/schemas/core/control_decision.v1.json +114 -0
- core_runtime/data/schemas/core/dsk.v3.json +105 -0
- core_runtime/data/schemas/core/effect_result.v1.json +39 -0
- core_runtime/data/schemas/core/entropy_signal.v1.json +108 -0
- core_runtime/data/schemas/core/execution_receipt.v1.json +93 -0
- core_runtime/data/schemas/core/frozen_release_manifest.v1.json +87 -0
- core_runtime/data/schemas/core/frozen_release_manifest.v2.json +72 -0
- core_runtime/data/schemas/core/frozen_release_manifest.v3.json +38 -0
- core_runtime/data/schemas/core/frozen_release_manifest.v4.json +72 -0
- core_runtime/data/schemas/core/frozen_release_manifest.v5.json +38 -0
- core_runtime/data/schemas/core/frozen_release_manifest.v6.json +116 -0
- core_runtime/data/schemas/core/frozen_release_manifest.v7.json +37 -0
- core_runtime/data/schemas/core/frozen_release_manifest.v8.json +114 -0
- core_runtime/data/schemas/core/frozen_rule_set.v1.json +282 -0
- core_runtime/data/schemas/core/memory_artifact.v1.json +120 -0
- core_runtime/data/schemas/core/memory_generation_result.v1.json +37 -0
- core_runtime/data/schemas/core/operational_learning_event.v1.json +63 -0
- core_runtime/data/schemas/core/pattern_candidate.v1.json +114 -0
- core_runtime/data/schemas/core/physical_safety_assurance_case.v1.json +676 -0
- core_runtime/data/schemas/core/policy_lifecycle.v1.json +99 -0
- core_runtime/data/schemas/core/retention_manifest.v1.json +53 -0
- core_runtime/data/schemas/core/reversibility_policy.v1.json +107 -0
- core_runtime/data/schemas/core/rule_anchor_batch.v1.json +93 -0
- core_runtime/data/schemas/core/rule_anchor_chain_evidence.v1.json +56 -0
- core_runtime/data/schemas/core/rule_approval.v1.json +49 -0
- core_runtime/data/schemas/core/rule_approval_request.v1.json +42 -0
- core_runtime/data/schemas/core/state_transition.v1.json +115 -0
- core_runtime/data/schemas/core/task_closeout.v1.json +47 -0
- core_runtime/data/schemas/core/template_promotion_candidate.v1.json +83 -0
- core_runtime/data/schemas/core/unsigned_rule_anchor_deployment.v1.json +106 -0
- core_runtime/data/schemas/core/unsigned_rule_anchor_transaction.v1.json +116 -0
- core_runtime/tooling/__init__.py +48 -0
- core_runtime/tooling/bump_version.py +900 -0
- core_runtime/tooling/contract_preflight.py +293 -0
- core_runtime/tooling/create_domain.py +254 -0
- core_runtime/tooling/diagnostics.py +129 -0
- core_runtime/tooling/doctor.py +482 -0
- core_runtime/tooling/file_inventory.py +182 -0
- core_runtime/tooling/json_checks.py +100 -0
- core_runtime/tooling/release_check.py +1017 -0
- core_runtime/tooling/repair_artifact_paths.py +458 -0
- core_runtime/tooling/report_writer.py +176 -0
- core_runtime/tooling/repository_inventory.py +399 -0
- core_runtime/tooling/safety_checks.py +172 -0
- core_runtime/tooling/sync_template.py +303 -0
- core_runtime/tooling/validation.py +507 -0
- core_runtime/tooling/version_inventory.py +256 -0
- core_runtime_engine-11.5.1.dist-info/METADATA +35 -0
- core_runtime_engine-11.5.1.dist-info/RECORD +96 -0
- core_runtime_engine-11.5.1.dist-info/WHEEL +5 -0
- core_runtime_engine-11.5.1.dist-info/entry_points.txt +2 -0
- 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
|