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,293 @@
|
|
|
1
|
+
"""Advisory-only contract preflight helpers 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
|
|
9
|
+
|
|
10
|
+
from core_runtime.tooling.diagnostics import DiagnosticCollection
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
@dataclass(frozen=True)
|
|
14
|
+
class ContractItem:
|
|
15
|
+
"""A single contract review item."""
|
|
16
|
+
|
|
17
|
+
kind: str
|
|
18
|
+
name: str
|
|
19
|
+
path: str
|
|
20
|
+
status: str
|
|
21
|
+
details: dict[str, Any] = field(default_factory=dict)
|
|
22
|
+
|
|
23
|
+
def to_dict(self) -> dict[str, Any]:
|
|
24
|
+
result = {
|
|
25
|
+
"kind": self.kind,
|
|
26
|
+
"name": self.name,
|
|
27
|
+
"path": self.path,
|
|
28
|
+
"status": self.status,
|
|
29
|
+
}
|
|
30
|
+
if self.details:
|
|
31
|
+
result["details"] = self.details
|
|
32
|
+
return result
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
@dataclass
|
|
36
|
+
class ContractPreflightReport:
|
|
37
|
+
"""Normalized report for `core-runtime contract-preflight`."""
|
|
38
|
+
|
|
39
|
+
tool: str
|
|
40
|
+
command: str
|
|
41
|
+
status: str
|
|
42
|
+
mutation_performed: bool = False
|
|
43
|
+
summary: dict[str, Any] = field(default_factory=dict)
|
|
44
|
+
selection: dict[str, Any] | None = None
|
|
45
|
+
items: list[ContractItem] = field(default_factory=list)
|
|
46
|
+
diagnostics: DiagnosticCollection = field(default_factory=DiagnosticCollection)
|
|
47
|
+
|
|
48
|
+
def to_dict(self) -> dict[str, Any]:
|
|
49
|
+
counts = self.diagnostics.count_by_severity()
|
|
50
|
+
summary = dict(self.summary)
|
|
51
|
+
summary.setdefault("info", counts["info"])
|
|
52
|
+
summary.setdefault("warning", counts["warning"])
|
|
53
|
+
summary.setdefault("error", counts["error"])
|
|
54
|
+
summary.setdefault("blocked", counts["blocked"])
|
|
55
|
+
return {
|
|
56
|
+
"tool": self.tool,
|
|
57
|
+
"command": self.command,
|
|
58
|
+
"status": self.status,
|
|
59
|
+
"mutation_performed": self.mutation_performed,
|
|
60
|
+
"summary": summary,
|
|
61
|
+
"selection": self.selection,
|
|
62
|
+
"items": [item.to_dict() for item in self.items],
|
|
63
|
+
"diagnostics": [d.to_dict() for d in self.diagnostics.diagnostics],
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
def to_markdown(self) -> str:
|
|
67
|
+
counts = self.diagnostics.count_by_severity()
|
|
68
|
+
summary = dict(self.summary)
|
|
69
|
+
summary.setdefault("info", counts["info"])
|
|
70
|
+
summary.setdefault("warning", counts["warning"])
|
|
71
|
+
summary.setdefault("error", counts["error"])
|
|
72
|
+
summary.setdefault("blocked", counts["blocked"])
|
|
73
|
+
lines: list[str] = []
|
|
74
|
+
lines.append("# CORE contract preflight")
|
|
75
|
+
lines.append("")
|
|
76
|
+
lines.append("## Summary")
|
|
77
|
+
lines.append("")
|
|
78
|
+
lines.append("| Metric | Value |")
|
|
79
|
+
lines.append("|--------|-------|")
|
|
80
|
+
lines.append("| Tool | {0} |".format(self.tool))
|
|
81
|
+
lines.append("| Command | {0} |".format(self.command))
|
|
82
|
+
lines.append("| Status | {0} |".format(self.status.upper()))
|
|
83
|
+
lines.append("| Mutation Performed | No |")
|
|
84
|
+
lines.append("| Items | {0} |".format(summary.get("item_count", len(self.items))))
|
|
85
|
+
lines.append("| Info | {0} |".format(counts["info"]))
|
|
86
|
+
lines.append("| Warning | {0} |".format(counts["warning"]))
|
|
87
|
+
lines.append("| Error | {0} |".format(counts["error"]))
|
|
88
|
+
lines.append("| Blocked | {0} |".format(counts["blocked"]))
|
|
89
|
+
lines.append("")
|
|
90
|
+
|
|
91
|
+
if self.selection is not None:
|
|
92
|
+
lines.append("## Selection")
|
|
93
|
+
lines.append("")
|
|
94
|
+
for key, value in self.selection.items():
|
|
95
|
+
lines.append("- **{0}**: {1}".format(key, value))
|
|
96
|
+
lines.append("")
|
|
97
|
+
|
|
98
|
+
if self.items:
|
|
99
|
+
lines.append("## Items")
|
|
100
|
+
lines.append("")
|
|
101
|
+
lines.append("| Kind | Name | Path | Status |")
|
|
102
|
+
lines.append("|------|------|------|--------|")
|
|
103
|
+
for item in self.items:
|
|
104
|
+
lines.append("| {0} | {1} | {2} | {3} |".format(item.kind, item.name, item.path, item.status))
|
|
105
|
+
lines.append("")
|
|
106
|
+
|
|
107
|
+
if self.diagnostics.diagnostics:
|
|
108
|
+
lines.append("## Diagnostics")
|
|
109
|
+
lines.append("")
|
|
110
|
+
lines.append("| Severity | Code | Path | Message |")
|
|
111
|
+
lines.append("|----------|------|------|---------|")
|
|
112
|
+
for diagnostic in self.diagnostics.diagnostics:
|
|
113
|
+
path = diagnostic.path or "-"
|
|
114
|
+
message = diagnostic.message.replace("|", "\\|")
|
|
115
|
+
lines.append(
|
|
116
|
+
"| {0} | {1} | {2} | {3} |".format(
|
|
117
|
+
diagnostic.severity.value.upper(),
|
|
118
|
+
diagnostic.code,
|
|
119
|
+
path,
|
|
120
|
+
message,
|
|
121
|
+
)
|
|
122
|
+
)
|
|
123
|
+
lines.append("")
|
|
124
|
+
|
|
125
|
+
return "\n".join(lines)
|
|
126
|
+
|
|
127
|
+
|
|
128
|
+
class RepositoryContractPreflight:
|
|
129
|
+
"""Build advisory-only contract review reports from public CORE schemas."""
|
|
130
|
+
|
|
131
|
+
def __init__(self, repo_root: Path):
|
|
132
|
+
self.repo_root = repo_root
|
|
133
|
+
|
|
134
|
+
def build_candidate_report(self, name: str, diagnostics: DiagnosticCollection | None = None) -> ContractPreflightReport:
|
|
135
|
+
diagnostics = diagnostics or DiagnosticCollection()
|
|
136
|
+
contract = self._resolve_contract(name)
|
|
137
|
+
selection = {"mode": "candidate", "candidate": name}
|
|
138
|
+
|
|
139
|
+
if contract is None:
|
|
140
|
+
diagnostics.add_blocked(
|
|
141
|
+
code="core.contract_preflight.contract_unknown",
|
|
142
|
+
message="Unknown contract candidate: {0}".format(name),
|
|
143
|
+
path=name,
|
|
144
|
+
expected="known CORE contract name",
|
|
145
|
+
actual=name,
|
|
146
|
+
)
|
|
147
|
+
return ContractPreflightReport(
|
|
148
|
+
tool="core-runtime contract-preflight",
|
|
149
|
+
command="contract-preflight --candidate {0}".format(name),
|
|
150
|
+
status="blocked",
|
|
151
|
+
summary={"mode": "candidate", "item_count": 0},
|
|
152
|
+
selection=selection,
|
|
153
|
+
diagnostics=diagnostics,
|
|
154
|
+
)
|
|
155
|
+
|
|
156
|
+
item = self._contract_item(contract)
|
|
157
|
+
return ContractPreflightReport(
|
|
158
|
+
tool="core-runtime contract-preflight",
|
|
159
|
+
command="contract-preflight --candidate {0}".format(name),
|
|
160
|
+
status="pass",
|
|
161
|
+
summary={
|
|
162
|
+
"mode": "candidate",
|
|
163
|
+
"item_count": 1,
|
|
164
|
+
"required_field_count": len(item.details.get("required_fields", [])),
|
|
165
|
+
"property_count": len(item.details.get("property_names", [])),
|
|
166
|
+
},
|
|
167
|
+
selection=selection,
|
|
168
|
+
items=[item],
|
|
169
|
+
diagnostics=diagnostics,
|
|
170
|
+
)
|
|
171
|
+
|
|
172
|
+
def build_compare_report(
|
|
173
|
+
self,
|
|
174
|
+
left: str,
|
|
175
|
+
right: str,
|
|
176
|
+
diagnostics: DiagnosticCollection | None = None,
|
|
177
|
+
) -> ContractPreflightReport:
|
|
178
|
+
diagnostics = diagnostics or DiagnosticCollection()
|
|
179
|
+
left_contract = self._resolve_contract(left)
|
|
180
|
+
right_contract = self._resolve_contract(right)
|
|
181
|
+
selection = {"mode": "compare", "left": left, "right": right}
|
|
182
|
+
|
|
183
|
+
missing: list[str] = []
|
|
184
|
+
if left_contract is None:
|
|
185
|
+
missing.append(left)
|
|
186
|
+
if right_contract is None:
|
|
187
|
+
missing.append(right)
|
|
188
|
+
if missing:
|
|
189
|
+
diagnostics.add_blocked(
|
|
190
|
+
code="core.contract_preflight.contract_unknown",
|
|
191
|
+
message="Unknown contract candidate(s): {0}".format(", ".join(missing)),
|
|
192
|
+
path=", ".join(missing),
|
|
193
|
+
expected="known CORE contract name",
|
|
194
|
+
actual=", ".join(missing),
|
|
195
|
+
)
|
|
196
|
+
return ContractPreflightReport(
|
|
197
|
+
tool="core-runtime contract-preflight",
|
|
198
|
+
command="contract-preflight --compare {0} {1}".format(left, right),
|
|
199
|
+
status="blocked",
|
|
200
|
+
summary={"mode": "compare", "item_count": 0},
|
|
201
|
+
selection=selection,
|
|
202
|
+
diagnostics=diagnostics,
|
|
203
|
+
)
|
|
204
|
+
|
|
205
|
+
left_item = self._contract_item(left_contract)
|
|
206
|
+
right_item = self._contract_item(right_contract)
|
|
207
|
+
left_required = set(left_item.details.get("required_fields", []))
|
|
208
|
+
right_required = set(right_item.details.get("required_fields", []))
|
|
209
|
+
shared_required = sorted(left_required & right_required)
|
|
210
|
+
left_only = sorted(left_required - right_required)
|
|
211
|
+
right_only = sorted(right_required - left_required)
|
|
212
|
+
status = "pass"
|
|
213
|
+
if left_item.details.get("schema_version") != right_item.details.get("schema_version"):
|
|
214
|
+
status = "warning"
|
|
215
|
+
|
|
216
|
+
return ContractPreflightReport(
|
|
217
|
+
tool="core-runtime contract-preflight",
|
|
218
|
+
command="contract-preflight --compare {0} {1}".format(left, right),
|
|
219
|
+
status=status,
|
|
220
|
+
summary={
|
|
221
|
+
"mode": "compare",
|
|
222
|
+
"item_count": 2,
|
|
223
|
+
"shared_required_fields": len(shared_required),
|
|
224
|
+
"left_only_required_fields": len(left_only),
|
|
225
|
+
"right_only_required_fields": len(right_only),
|
|
226
|
+
"same_schema_version": left_item.details.get("schema_version") == right_item.details.get("schema_version"),
|
|
227
|
+
},
|
|
228
|
+
selection=selection,
|
|
229
|
+
items=[left_item, right_item],
|
|
230
|
+
diagnostics=diagnostics,
|
|
231
|
+
)
|
|
232
|
+
|
|
233
|
+
def _resolve_contract(self, name: str) -> dict[str, Any] | None:
|
|
234
|
+
normalized = name.strip().lower()
|
|
235
|
+
for path in sorted((self.repo_root / "schemas" / "core").glob("*.json")):
|
|
236
|
+
contract = self._load_contract(path)
|
|
237
|
+
if contract is None:
|
|
238
|
+
continue
|
|
239
|
+
candidates = {
|
|
240
|
+
contract.get("title", "").lower(),
|
|
241
|
+
contract.get("schema_version", "").lower(),
|
|
242
|
+
path.stem.lower(),
|
|
243
|
+
}
|
|
244
|
+
if normalized in candidates:
|
|
245
|
+
return contract
|
|
246
|
+
return None
|
|
247
|
+
|
|
248
|
+
def _load_contract(self, path: Path) -> dict[str, Any] | None:
|
|
249
|
+
try:
|
|
250
|
+
payload = json.loads(path.read_text(encoding="utf-8"))
|
|
251
|
+
except (OSError, json.JSONDecodeError):
|
|
252
|
+
return None
|
|
253
|
+
if not isinstance(payload, dict):
|
|
254
|
+
return None
|
|
255
|
+
required = payload.get("required")
|
|
256
|
+
properties = payload.get("properties")
|
|
257
|
+
if not isinstance(properties, dict):
|
|
258
|
+
properties = {}
|
|
259
|
+
return {
|
|
260
|
+
"path": path,
|
|
261
|
+
"title": payload.get("title"),
|
|
262
|
+
"schema_version": self._schema_version(payload),
|
|
263
|
+
"required": required if isinstance(required, list) else [],
|
|
264
|
+
"properties": properties,
|
|
265
|
+
"additionalProperties": payload.get("additionalProperties"),
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
def _schema_version(self, payload: dict[str, Any]) -> str | None:
|
|
269
|
+
properties = payload.get("properties")
|
|
270
|
+
if isinstance(properties, dict):
|
|
271
|
+
schema_version = properties.get("schema_version")
|
|
272
|
+
if isinstance(schema_version, dict):
|
|
273
|
+
const = schema_version.get("const")
|
|
274
|
+
if isinstance(const, str) and const:
|
|
275
|
+
return const
|
|
276
|
+
return None
|
|
277
|
+
|
|
278
|
+
def _contract_item(self, contract: dict[str, Any]) -> ContractItem:
|
|
279
|
+
properties = contract.get("properties", {})
|
|
280
|
+
property_names = sorted(properties.keys()) if isinstance(properties, dict) else []
|
|
281
|
+
required_fields = sorted(str(value) for value in contract.get("required", []))
|
|
282
|
+
return ContractItem(
|
|
283
|
+
kind="contract",
|
|
284
|
+
name=str(contract.get("title") or Path(contract["path"]).stem),
|
|
285
|
+
path=str(Path(contract["path"]).relative_to(self.repo_root)),
|
|
286
|
+
status="passed",
|
|
287
|
+
details={
|
|
288
|
+
"schema_version": contract.get("schema_version"),
|
|
289
|
+
"required_fields": required_fields,
|
|
290
|
+
"property_names": property_names,
|
|
291
|
+
"additional_properties": contract.get("additionalProperties"),
|
|
292
|
+
},
|
|
293
|
+
)
|
|
@@ -0,0 +1,254 @@
|
|
|
1
|
+
"""Dry-run-first domain scaffolding preflight for CORE tooling."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import re
|
|
6
|
+
from dataclasses import dataclass, field
|
|
7
|
+
from pathlib import Path
|
|
8
|
+
from typing import Any
|
|
9
|
+
|
|
10
|
+
from core_runtime.tooling.diagnostics import DiagnosticCollection
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
DOMAIN_NAME_PATTERN = re.compile(r"^[a-z][a-z0-9_]*$")
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
@dataclass(frozen=True)
|
|
17
|
+
class CreateDomainItem:
|
|
18
|
+
"""A planned file or artifact in a domain scaffold."""
|
|
19
|
+
|
|
20
|
+
kind: str
|
|
21
|
+
name: str
|
|
22
|
+
path: str
|
|
23
|
+
status: str
|
|
24
|
+
details: dict[str, Any] = field(default_factory=dict)
|
|
25
|
+
|
|
26
|
+
def to_dict(self) -> dict[str, Any]:
|
|
27
|
+
result = {
|
|
28
|
+
"kind": self.kind,
|
|
29
|
+
"name": self.name,
|
|
30
|
+
"path": self.path,
|
|
31
|
+
"status": self.status,
|
|
32
|
+
}
|
|
33
|
+
if self.details:
|
|
34
|
+
result["details"] = self.details
|
|
35
|
+
return result
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
@dataclass
|
|
39
|
+
class CreateDomainReport:
|
|
40
|
+
"""Normalized report for `core-runtime create-domain`."""
|
|
41
|
+
|
|
42
|
+
tool: str
|
|
43
|
+
command: str
|
|
44
|
+
status: str
|
|
45
|
+
mutation_performed: bool = False
|
|
46
|
+
summary: dict[str, Any] = field(default_factory=dict)
|
|
47
|
+
selection: dict[str, Any] | None = None
|
|
48
|
+
items: list[CreateDomainItem] = field(default_factory=list)
|
|
49
|
+
diagnostics: DiagnosticCollection = field(default_factory=DiagnosticCollection)
|
|
50
|
+
|
|
51
|
+
def to_dict(self) -> dict[str, Any]:
|
|
52
|
+
counts = self.diagnostics.count_by_severity()
|
|
53
|
+
summary = dict(self.summary)
|
|
54
|
+
summary.setdefault("info", counts["info"])
|
|
55
|
+
summary.setdefault("warning", counts["warning"])
|
|
56
|
+
summary.setdefault("error", counts["error"])
|
|
57
|
+
summary.setdefault("blocked", counts["blocked"])
|
|
58
|
+
return {
|
|
59
|
+
"tool": self.tool,
|
|
60
|
+
"command": self.command,
|
|
61
|
+
"status": self.status,
|
|
62
|
+
"mutation_performed": self.mutation_performed,
|
|
63
|
+
"summary": summary,
|
|
64
|
+
"selection": self.selection,
|
|
65
|
+
"items": [item.to_dict() for item in self.items],
|
|
66
|
+
"diagnostics": [d.to_dict() for d in self.diagnostics.diagnostics],
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
def to_markdown(self) -> str:
|
|
70
|
+
counts = self.diagnostics.count_by_severity()
|
|
71
|
+
summary = dict(self.summary)
|
|
72
|
+
summary.setdefault("info", counts["info"])
|
|
73
|
+
summary.setdefault("warning", counts["warning"])
|
|
74
|
+
summary.setdefault("error", counts["error"])
|
|
75
|
+
summary.setdefault("blocked", counts["blocked"])
|
|
76
|
+
lines: list[str] = []
|
|
77
|
+
lines.append("# CORE create-domain plan")
|
|
78
|
+
lines.append("")
|
|
79
|
+
lines.append("## Summary")
|
|
80
|
+
lines.append("")
|
|
81
|
+
lines.append("| Metric | Value |")
|
|
82
|
+
lines.append("|--------|-------|")
|
|
83
|
+
lines.append("| Tool | {0} |".format(self.tool))
|
|
84
|
+
lines.append("| Command | {0} |".format(self.command))
|
|
85
|
+
lines.append("| Status | {0} |".format(self.status.upper()))
|
|
86
|
+
lines.append("| Mutation Performed | No |")
|
|
87
|
+
lines.append("| Items | {0} |".format(summary.get("item_count", len(self.items))))
|
|
88
|
+
lines.append("| Info | {0} |".format(counts["info"]))
|
|
89
|
+
lines.append("| Warning | {0} |".format(counts["warning"]))
|
|
90
|
+
lines.append("| Error | {0} |".format(counts["error"]))
|
|
91
|
+
lines.append("| Blocked | {0} |".format(counts["blocked"]))
|
|
92
|
+
lines.append("")
|
|
93
|
+
|
|
94
|
+
if self.selection is not None:
|
|
95
|
+
lines.append("## Selection")
|
|
96
|
+
lines.append("")
|
|
97
|
+
for key, value in self.selection.items():
|
|
98
|
+
lines.append("- **{0}**: {1}".format(key, value))
|
|
99
|
+
lines.append("")
|
|
100
|
+
|
|
101
|
+
if self.items:
|
|
102
|
+
lines.append("## Planned Artifacts")
|
|
103
|
+
lines.append("")
|
|
104
|
+
lines.append("| Kind | Name | Path | Status |")
|
|
105
|
+
lines.append("|------|------|------|--------|")
|
|
106
|
+
for item in self.items:
|
|
107
|
+
lines.append("| {0} | {1} | {2} | {3} |".format(item.kind, item.name, item.path, item.status))
|
|
108
|
+
lines.append("")
|
|
109
|
+
|
|
110
|
+
if self.diagnostics.diagnostics:
|
|
111
|
+
lines.append("## Diagnostics")
|
|
112
|
+
lines.append("")
|
|
113
|
+
lines.append("| Severity | Code | Path | Message |")
|
|
114
|
+
lines.append("|----------|------|------|---------|")
|
|
115
|
+
for diagnostic in self.diagnostics.diagnostics:
|
|
116
|
+
path = diagnostic.path or "-"
|
|
117
|
+
message = diagnostic.message.replace("|", "\\|")
|
|
118
|
+
lines.append(
|
|
119
|
+
"| {0} | {1} | {2} | {3} |".format(
|
|
120
|
+
diagnostic.severity.value.upper(),
|
|
121
|
+
diagnostic.code,
|
|
122
|
+
path,
|
|
123
|
+
message,
|
|
124
|
+
)
|
|
125
|
+
)
|
|
126
|
+
lines.append("")
|
|
127
|
+
|
|
128
|
+
return "\n".join(lines)
|
|
129
|
+
|
|
130
|
+
|
|
131
|
+
class DomainScaffolder:
|
|
132
|
+
"""Plan a deterministic domain scaffold without mutating the repo."""
|
|
133
|
+
|
|
134
|
+
def __init__(self, repo_root: Path):
|
|
135
|
+
self.repo_root = repo_root
|
|
136
|
+
|
|
137
|
+
def build_plan(self, name: str, template: str = "generic", diagnostics: DiagnosticCollection | None = None) -> CreateDomainReport:
|
|
138
|
+
diagnostics = diagnostics or DiagnosticCollection()
|
|
139
|
+
normalized_name = name.strip()
|
|
140
|
+
normalized_template = template.strip()
|
|
141
|
+
selection = {"domain": normalized_name, "template": normalized_template, "mode": "dry-run"}
|
|
142
|
+
|
|
143
|
+
if not DOMAIN_NAME_PATTERN.fullmatch(normalized_name):
|
|
144
|
+
diagnostics.add_blocked(
|
|
145
|
+
code="core.create_domain.invalid_name",
|
|
146
|
+
message="Domain name must match ^[a-z][a-z0-9_]*$",
|
|
147
|
+
path=normalized_name,
|
|
148
|
+
expected="lowercase identifier",
|
|
149
|
+
actual=normalized_name,
|
|
150
|
+
)
|
|
151
|
+
return CreateDomainReport(
|
|
152
|
+
tool="core-runtime create-domain",
|
|
153
|
+
command="create-domain {0} --dry-run".format(normalized_name),
|
|
154
|
+
status="blocked",
|
|
155
|
+
summary={"mode": "dry-run", "item_count": 0},
|
|
156
|
+
selection=selection,
|
|
157
|
+
diagnostics=diagnostics,
|
|
158
|
+
)
|
|
159
|
+
|
|
160
|
+
planned_items, collisions, risks = self._plan_items(normalized_name, normalized_template)
|
|
161
|
+
if collisions:
|
|
162
|
+
diagnostics.add_blocked(
|
|
163
|
+
code="core.create_domain.collision",
|
|
164
|
+
message="Domain scaffold collides with existing files or directories",
|
|
165
|
+
path=collisions[0],
|
|
166
|
+
expected="unused paths",
|
|
167
|
+
actual=", ".join(collisions),
|
|
168
|
+
)
|
|
169
|
+
status = "blocked"
|
|
170
|
+
else:
|
|
171
|
+
status = "pass"
|
|
172
|
+
|
|
173
|
+
if risks:
|
|
174
|
+
diagnostics.add_warning(
|
|
175
|
+
code="core.create_domain.review_required",
|
|
176
|
+
message="Domain scaffold has reviewable design risks",
|
|
177
|
+
path=normalized_name,
|
|
178
|
+
expected="bounded dry-run review",
|
|
179
|
+
actual=", ".join(risks),
|
|
180
|
+
)
|
|
181
|
+
if status == "pass":
|
|
182
|
+
status = "warning"
|
|
183
|
+
|
|
184
|
+
return CreateDomainReport(
|
|
185
|
+
tool="core-runtime create-domain",
|
|
186
|
+
command="create-domain {0} --dry-run".format(normalized_name),
|
|
187
|
+
status=status,
|
|
188
|
+
summary={
|
|
189
|
+
"mode": "dry-run",
|
|
190
|
+
"item_count": len(planned_items),
|
|
191
|
+
"collision_count": len(collisions),
|
|
192
|
+
"risk_count": len(risks),
|
|
193
|
+
"template": normalized_template,
|
|
194
|
+
},
|
|
195
|
+
selection=selection,
|
|
196
|
+
items=planned_items,
|
|
197
|
+
diagnostics=diagnostics,
|
|
198
|
+
)
|
|
199
|
+
|
|
200
|
+
def _plan_items(self, name: str, template: str) -> tuple[list[CreateDomainItem], list[str], list[str]]:
|
|
201
|
+
collisions: list[str] = []
|
|
202
|
+
risks: list[str] = []
|
|
203
|
+
items: list[CreateDomainItem] = []
|
|
204
|
+
|
|
205
|
+
domain_dir = self.repo_root / "core_runtime" / "domains" / name
|
|
206
|
+
test_path = self.repo_root / "tests" / f"test_domain_{name}.py"
|
|
207
|
+
example_dir = self.repo_root / "examples" / "domains" / name
|
|
208
|
+
docs_path = self.repo_root / "docs" / "domains" / f"{name}.md"
|
|
209
|
+
|
|
210
|
+
planned_paths = [
|
|
211
|
+
domain_dir,
|
|
212
|
+
domain_dir / "__init__.py",
|
|
213
|
+
domain_dir / "task.py",
|
|
214
|
+
domain_dir / "oracle.py",
|
|
215
|
+
domain_dir / "surrogate.py",
|
|
216
|
+
domain_dir / "projection.py",
|
|
217
|
+
domain_dir / "evaluator.py",
|
|
218
|
+
domain_dir / "confidence.py",
|
|
219
|
+
domain_dir / "manifest.json",
|
|
220
|
+
test_path,
|
|
221
|
+
example_dir / "valid_task.json",
|
|
222
|
+
example_dir / "invalid_task.json",
|
|
223
|
+
docs_path,
|
|
224
|
+
]
|
|
225
|
+
|
|
226
|
+
for path in planned_paths:
|
|
227
|
+
if path.exists():
|
|
228
|
+
collisions.append(str(path.relative_to(self.repo_root)))
|
|
229
|
+
items.append(
|
|
230
|
+
CreateDomainItem(
|
|
231
|
+
kind="planned_dir" if path == domain_dir else "planned_file",
|
|
232
|
+
name=path.name,
|
|
233
|
+
path=str(path.relative_to(self.repo_root)),
|
|
234
|
+
status="planned",
|
|
235
|
+
details={
|
|
236
|
+
"template": template,
|
|
237
|
+
"exists": path.exists(),
|
|
238
|
+
},
|
|
239
|
+
)
|
|
240
|
+
)
|
|
241
|
+
|
|
242
|
+
risks.extend(
|
|
243
|
+
[
|
|
244
|
+
"no_runtime_behavior",
|
|
245
|
+
"advisory_only_manifest",
|
|
246
|
+
"fixtures_must_stay_synthetic",
|
|
247
|
+
"tests_require_followup",
|
|
248
|
+
"docs_need_manual_review",
|
|
249
|
+
]
|
|
250
|
+
)
|
|
251
|
+
if template != "generic":
|
|
252
|
+
risks.append(f"template:{template}")
|
|
253
|
+
|
|
254
|
+
return items, collisions, risks
|
|
@@ -0,0 +1,129 @@
|
|
|
1
|
+
"""Diagnostic data structures and severity handling for CORE tooling."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from dataclasses import dataclass, field
|
|
6
|
+
from enum import Enum
|
|
7
|
+
from typing import Any, Optional
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
class Severity(str, Enum):
|
|
11
|
+
"""Diagnostic severity levels."""
|
|
12
|
+
|
|
13
|
+
INFO = "info"
|
|
14
|
+
WARNING = "warning"
|
|
15
|
+
ERROR = "error"
|
|
16
|
+
BLOCKED = "blocked"
|
|
17
|
+
|
|
18
|
+
def exit_code_weight(self) -> int:
|
|
19
|
+
"""Return the weight for exit code computation."""
|
|
20
|
+
return {
|
|
21
|
+
Severity.INFO: 0,
|
|
22
|
+
Severity.WARNING: 0,
|
|
23
|
+
Severity.ERROR: 1,
|
|
24
|
+
Severity.BLOCKED: 2,
|
|
25
|
+
}[self]
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
class ExitCode(Enum):
|
|
29
|
+
"""Exit codes for lint command."""
|
|
30
|
+
|
|
31
|
+
OK = 0
|
|
32
|
+
ERROR = 1
|
|
33
|
+
BLOCKED = 2
|
|
34
|
+
INTERNAL_ERROR = 3
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
@dataclass(frozen=True, order=False)
|
|
38
|
+
class Diagnostic:
|
|
39
|
+
"""A single diagnostic result from a check."""
|
|
40
|
+
|
|
41
|
+
code: str
|
|
42
|
+
severity: Severity
|
|
43
|
+
message: str
|
|
44
|
+
mutation_allowed: bool
|
|
45
|
+
path: Optional[str] = None
|
|
46
|
+
expected: Optional[str] = None
|
|
47
|
+
actual: Optional[str] = None
|
|
48
|
+
details: Optional[str] = None
|
|
49
|
+
|
|
50
|
+
def to_dict(self) -> dict[str, Any]:
|
|
51
|
+
"""Convert to dictionary for JSON serialization."""
|
|
52
|
+
result = {
|
|
53
|
+
"code": self.code,
|
|
54
|
+
"severity": self.severity.value,
|
|
55
|
+
"message": self.message,
|
|
56
|
+
"mutation_allowed": self.mutation_allowed,
|
|
57
|
+
}
|
|
58
|
+
if self.path is not None:
|
|
59
|
+
result["path"] = self.path
|
|
60
|
+
if self.expected is not None:
|
|
61
|
+
result["expected"] = self.expected
|
|
62
|
+
if self.actual is not None:
|
|
63
|
+
result["actual"] = self.actual
|
|
64
|
+
if self.details is not None:
|
|
65
|
+
result["details"] = self.details
|
|
66
|
+
return result
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
@dataclass
|
|
70
|
+
class DiagnosticCollection:
|
|
71
|
+
"""Collection of diagnostics with summary statistics."""
|
|
72
|
+
|
|
73
|
+
diagnostics: list[Diagnostic] = field(default_factory=list)
|
|
74
|
+
|
|
75
|
+
def add(self, diagnostic: Diagnostic) -> None:
|
|
76
|
+
"""Add a diagnostic."""
|
|
77
|
+
self.diagnostics.append(diagnostic)
|
|
78
|
+
|
|
79
|
+
def add_error(self, code: str, message: str, path: Optional[str] = None, **kwargs: Any) -> None:
|
|
80
|
+
"""Add an error severity diagnostic."""
|
|
81
|
+
self.add(Diagnostic(code=code, severity=Severity.ERROR, message=message, path=path, mutation_allowed=False, **kwargs))
|
|
82
|
+
|
|
83
|
+
def add_warning(self, code: str, message: str, path: Optional[str] = None, **kwargs: Any) -> None:
|
|
84
|
+
"""Add a warning severity diagnostic."""
|
|
85
|
+
self.add(Diagnostic(code=code, severity=Severity.WARNING, message=message, path=path, mutation_allowed=False, **kwargs))
|
|
86
|
+
|
|
87
|
+
def add_info(self, code: str, message: str, path: Optional[str] = None, **kwargs: Any) -> None:
|
|
88
|
+
"""Add an info severity diagnostic."""
|
|
89
|
+
self.add(Diagnostic(code=code, severity=Severity.INFO, message=message, path=path, mutation_allowed=False, **kwargs))
|
|
90
|
+
|
|
91
|
+
def add_blocked(self, code: str, message: str, path: Optional[str] = None, **kwargs: Any) -> None:
|
|
92
|
+
"""Add a blocked severity diagnostic."""
|
|
93
|
+
self.add(Diagnostic(code=code, severity=Severity.BLOCKED, message=message, path=path, mutation_allowed=False, **kwargs))
|
|
94
|
+
|
|
95
|
+
def count_by_severity(self) -> dict[str, int]:
|
|
96
|
+
"""Count diagnostics by severity."""
|
|
97
|
+
counts = {s.value: 0 for s in Severity}
|
|
98
|
+
for d in self.diagnostics:
|
|
99
|
+
counts[d.severity.value] += 1
|
|
100
|
+
return counts
|
|
101
|
+
|
|
102
|
+
def has_errors(self) -> bool:
|
|
103
|
+
"""Check if any error or blocked diagnostics exist."""
|
|
104
|
+
return any(d.severity in (Severity.ERROR, Severity.BLOCKED) for d in self.diagnostics)
|
|
105
|
+
|
|
106
|
+
def has_blocked(self) -> bool:
|
|
107
|
+
"""Check if any blocked diagnostics exist."""
|
|
108
|
+
return any(d.severity == Severity.BLOCKED for d in self.diagnostics)
|
|
109
|
+
|
|
110
|
+
def compute_exit_code(self) -> ExitCode:
|
|
111
|
+
"""Compute exit code based on diagnostics."""
|
|
112
|
+
if not self.diagnostics:
|
|
113
|
+
return ExitCode.OK
|
|
114
|
+
|
|
115
|
+
has_errors = any(d.severity == Severity.ERROR for d in self.diagnostics)
|
|
116
|
+
has_blocked = any(d.severity == Severity.BLOCKED for d in self.diagnostics)
|
|
117
|
+
|
|
118
|
+
if has_blocked:
|
|
119
|
+
return ExitCode.BLOCKED
|
|
120
|
+
if has_errors:
|
|
121
|
+
return ExitCode.ERROR
|
|
122
|
+
return ExitCode.OK
|
|
123
|
+
|
|
124
|
+
def to_dict(self) -> dict[str, Any]:
|
|
125
|
+
"""Convert to dictionary for JSON serialization."""
|
|
126
|
+
return {
|
|
127
|
+
"diagnostics": [d.to_dict() for d in self.diagnostics],
|
|
128
|
+
"summary": self.count_by_severity(),
|
|
129
|
+
}
|