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,507 @@
|
|
|
1
|
+
"""Read-only structural validation helpers for CORE repository surfaces."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import json
|
|
6
|
+
import py_compile
|
|
7
|
+
from dataclasses import dataclass, field
|
|
8
|
+
from pathlib import Path
|
|
9
|
+
from typing import Any
|
|
10
|
+
|
|
11
|
+
from core_runtime.tooling.diagnostics import DiagnosticCollection
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
_REJECTED_PATH_MARKERS = (
|
|
15
|
+
"invalid",
|
|
16
|
+
"rejected",
|
|
17
|
+
"negative_cases",
|
|
18
|
+
"malicious",
|
|
19
|
+
"not_accepted",
|
|
20
|
+
"path_traversal",
|
|
21
|
+
"workspace/",
|
|
22
|
+
"private/",
|
|
23
|
+
".agents/",
|
|
24
|
+
".codex/",
|
|
25
|
+
)
|
|
26
|
+
|
|
27
|
+
@dataclass(frozen=True)
|
|
28
|
+
class ValidationItem:
|
|
29
|
+
"""A single validation result item."""
|
|
30
|
+
|
|
31
|
+
kind: str
|
|
32
|
+
name: str
|
|
33
|
+
path: str
|
|
34
|
+
status: str
|
|
35
|
+
details: dict[str, Any] = field(default_factory=dict)
|
|
36
|
+
|
|
37
|
+
def to_dict(self) -> dict[str, Any]:
|
|
38
|
+
result = {
|
|
39
|
+
"kind": self.kind,
|
|
40
|
+
"name": self.name,
|
|
41
|
+
"path": self.path,
|
|
42
|
+
"status": self.status,
|
|
43
|
+
}
|
|
44
|
+
if self.details:
|
|
45
|
+
result["details"] = self.details
|
|
46
|
+
return result
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
@dataclass
|
|
50
|
+
class ValidationReport:
|
|
51
|
+
"""Normalized report for read-only validation commands."""
|
|
52
|
+
|
|
53
|
+
tool: str
|
|
54
|
+
command: str
|
|
55
|
+
status: str
|
|
56
|
+
mutation_performed: bool = False
|
|
57
|
+
summary: dict[str, Any] = field(default_factory=dict)
|
|
58
|
+
selection: dict[str, Any] | None = None
|
|
59
|
+
items: list[ValidationItem] = field(default_factory=list)
|
|
60
|
+
diagnostics: DiagnosticCollection = field(default_factory=DiagnosticCollection)
|
|
61
|
+
|
|
62
|
+
def to_dict(self) -> dict[str, Any]:
|
|
63
|
+
counts = self.diagnostics.count_by_severity()
|
|
64
|
+
summary = dict(self.summary)
|
|
65
|
+
summary.setdefault("info", counts["info"])
|
|
66
|
+
summary.setdefault("warning", counts["warning"])
|
|
67
|
+
summary.setdefault("error", counts["error"])
|
|
68
|
+
summary.setdefault("blocked", counts["blocked"])
|
|
69
|
+
return {
|
|
70
|
+
"tool": self.tool,
|
|
71
|
+
"command": self.command,
|
|
72
|
+
"status": self.status,
|
|
73
|
+
"mutation_performed": self.mutation_performed,
|
|
74
|
+
"summary": summary,
|
|
75
|
+
"selection": self.selection,
|
|
76
|
+
"items": [item.to_dict() for item in self.items],
|
|
77
|
+
"diagnostics": [d.to_dict() for d in self.diagnostics.diagnostics],
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
def to_markdown(self) -> str:
|
|
81
|
+
counts = self.diagnostics.count_by_severity()
|
|
82
|
+
summary = dict(self.summary)
|
|
83
|
+
summary.setdefault("info", counts["info"])
|
|
84
|
+
summary.setdefault("warning", counts["warning"])
|
|
85
|
+
summary.setdefault("error", counts["error"])
|
|
86
|
+
summary.setdefault("blocked", counts["blocked"])
|
|
87
|
+
lines: list[str] = []
|
|
88
|
+
lines.append("# CORE repository validation")
|
|
89
|
+
lines.append("")
|
|
90
|
+
lines.append("## Summary")
|
|
91
|
+
lines.append("")
|
|
92
|
+
lines.append("| Metric | Value |")
|
|
93
|
+
lines.append("|--------|-------|")
|
|
94
|
+
lines.append("| Tool | {0} |".format(self.tool))
|
|
95
|
+
lines.append("| Command | {0} |".format(self.command))
|
|
96
|
+
lines.append("| Status | {0} |".format(self.status.upper()))
|
|
97
|
+
lines.append("| Mutation Performed | No |")
|
|
98
|
+
lines.append("| Items | {0} |".format(summary.get("item_count", len(self.items))))
|
|
99
|
+
lines.append("| Passed | {0} |".format(summary.get("passed", 0)))
|
|
100
|
+
lines.append("| Failed | {0} |".format(summary.get("failed", 0)))
|
|
101
|
+
lines.append("| Skipped | {0} |".format(summary.get("skipped", 0)))
|
|
102
|
+
lines.append("| Info | {0} |".format(counts["info"]))
|
|
103
|
+
lines.append("| Warning | {0} |".format(counts["warning"]))
|
|
104
|
+
lines.append("| Error | {0} |".format(counts["error"]))
|
|
105
|
+
lines.append("| Blocked | {0} |".format(counts["blocked"]))
|
|
106
|
+
lines.append("")
|
|
107
|
+
|
|
108
|
+
if self.selection is not None:
|
|
109
|
+
lines.append("## Selection")
|
|
110
|
+
lines.append("")
|
|
111
|
+
for key, value in self.selection.items():
|
|
112
|
+
lines.append("- **{0}**: {1}".format(key, value))
|
|
113
|
+
lines.append("")
|
|
114
|
+
|
|
115
|
+
if self.items:
|
|
116
|
+
lines.append("## Items")
|
|
117
|
+
lines.append("")
|
|
118
|
+
lines.append("| Kind | Name | Path | Status |")
|
|
119
|
+
lines.append("|------|------|------|--------|")
|
|
120
|
+
for item in self.items:
|
|
121
|
+
lines.append("| {0} | {1} | {2} | {3} |".format(item.kind, item.name, item.path, item.status))
|
|
122
|
+
lines.append("")
|
|
123
|
+
|
|
124
|
+
if self.diagnostics.diagnostics:
|
|
125
|
+
lines.append("## Diagnostics")
|
|
126
|
+
lines.append("")
|
|
127
|
+
lines.append("| Severity | Code | Path | Message |")
|
|
128
|
+
lines.append("|----------|------|------|---------|")
|
|
129
|
+
for diagnostic in self.diagnostics.diagnostics:
|
|
130
|
+
path = diagnostic.path or "-"
|
|
131
|
+
message = diagnostic.message.replace("|", "\\|")
|
|
132
|
+
lines.append(
|
|
133
|
+
"| {0} | {1} | {2} | {3} |".format(
|
|
134
|
+
diagnostic.severity.value.upper(),
|
|
135
|
+
diagnostic.code,
|
|
136
|
+
path,
|
|
137
|
+
message,
|
|
138
|
+
)
|
|
139
|
+
)
|
|
140
|
+
lines.append("")
|
|
141
|
+
|
|
142
|
+
return "\n".join(lines)
|
|
143
|
+
|
|
144
|
+
|
|
145
|
+
def _is_rejected_path(path: Path) -> bool:
|
|
146
|
+
path_text = str(path).lower()
|
|
147
|
+
return any(marker in path_text for marker in _REJECTED_PATH_MARKERS)
|
|
148
|
+
|
|
149
|
+
|
|
150
|
+
def _walk_strings(payload: Any, prefix: str = "") -> list[tuple[str, str]]:
|
|
151
|
+
results: list[tuple[str, str]] = []
|
|
152
|
+
if isinstance(payload, dict):
|
|
153
|
+
for key, value in payload.items():
|
|
154
|
+
child_prefix = f"{prefix}.{key}" if prefix else str(key)
|
|
155
|
+
results.extend(_walk_strings(value, child_prefix))
|
|
156
|
+
elif isinstance(payload, list):
|
|
157
|
+
for index, value in enumerate(payload):
|
|
158
|
+
child_prefix = f"{prefix}[{index}]"
|
|
159
|
+
results.extend(_walk_strings(value, child_prefix))
|
|
160
|
+
elif isinstance(payload, str):
|
|
161
|
+
results.append((prefix, payload))
|
|
162
|
+
return results
|
|
163
|
+
|
|
164
|
+
|
|
165
|
+
def _load_json(path: Path) -> dict[str, Any] | None:
|
|
166
|
+
try:
|
|
167
|
+
payload = json.loads(path.read_text(encoding="utf-8"))
|
|
168
|
+
except (OSError, json.JSONDecodeError):
|
|
169
|
+
return None
|
|
170
|
+
if isinstance(payload, dict):
|
|
171
|
+
return payload
|
|
172
|
+
return None
|
|
173
|
+
|
|
174
|
+
|
|
175
|
+
class RepositoryValidation:
|
|
176
|
+
"""Build read-only validation reports for specific repository surfaces."""
|
|
177
|
+
|
|
178
|
+
def __init__(self, repo_root: Path):
|
|
179
|
+
self.repo_root = repo_root
|
|
180
|
+
|
|
181
|
+
def build_report(self, kind: str, name: str | None = None, diagnostics: DiagnosticCollection | None = None) -> ValidationReport:
|
|
182
|
+
diagnostics = diagnostics or DiagnosticCollection()
|
|
183
|
+
normalized = kind.lower()
|
|
184
|
+
selection = {"kind": normalized, "name": name}
|
|
185
|
+
if normalized in {"schema", "schemas"}:
|
|
186
|
+
items = self._validate_schemas(diagnostics)
|
|
187
|
+
command = "validate schemas"
|
|
188
|
+
elif normalized in {"example", "examples"}:
|
|
189
|
+
items = self._validate_examples(diagnostics)
|
|
190
|
+
command = "validate examples"
|
|
191
|
+
elif normalized in {"manifest", "manifests"}:
|
|
192
|
+
items = self._validate_manifests(diagnostics)
|
|
193
|
+
command = "validate manifests"
|
|
194
|
+
elif normalized in {"contract", "contracts"}:
|
|
195
|
+
items = self._validate_contracts(diagnostics)
|
|
196
|
+
command = "validate contracts"
|
|
197
|
+
elif normalized in {"domain", "domains"}:
|
|
198
|
+
if not name:
|
|
199
|
+
diagnostics.add_blocked(
|
|
200
|
+
code="core.validate.domain_name_required",
|
|
201
|
+
message="validate domain requires a domain name",
|
|
202
|
+
path="core_runtime/domains/",
|
|
203
|
+
expected="validate domain <name>",
|
|
204
|
+
actual="missing name",
|
|
205
|
+
)
|
|
206
|
+
return ValidationReport(
|
|
207
|
+
tool="core-runtime validate",
|
|
208
|
+
command="validate domain",
|
|
209
|
+
status="blocked",
|
|
210
|
+
summary={"kind": "domain", "item_count": 0, "passed": 0, "failed": 1, "skipped": 0},
|
|
211
|
+
selection=selection,
|
|
212
|
+
diagnostics=diagnostics,
|
|
213
|
+
)
|
|
214
|
+
items = self._validate_domain(name, diagnostics)
|
|
215
|
+
command = "validate domain {0}".format(name)
|
|
216
|
+
else:
|
|
217
|
+
diagnostics.add_blocked(
|
|
218
|
+
code="core.validate.kind_unknown",
|
|
219
|
+
message="Unknown validation kind: {0}".format(kind),
|
|
220
|
+
path=kind,
|
|
221
|
+
expected="schemas|examples|manifests|contracts|domain",
|
|
222
|
+
actual=kind,
|
|
223
|
+
)
|
|
224
|
+
return ValidationReport(
|
|
225
|
+
tool="core-runtime validate",
|
|
226
|
+
command="validate {0}".format(kind),
|
|
227
|
+
status="blocked",
|
|
228
|
+
summary={"kind": kind, "item_count": 0, "passed": 0, "failed": 1, "skipped": 0},
|
|
229
|
+
selection=selection,
|
|
230
|
+
diagnostics=diagnostics,
|
|
231
|
+
)
|
|
232
|
+
|
|
233
|
+
passed = sum(1 for item in items if item.status == "passed")
|
|
234
|
+
failed = sum(1 for item in items if item.status == "failed")
|
|
235
|
+
skipped = sum(1 for item in items if item.status == "skipped")
|
|
236
|
+
status = "pass"
|
|
237
|
+
if diagnostics.has_blocked():
|
|
238
|
+
status = "blocked"
|
|
239
|
+
elif diagnostics.has_errors():
|
|
240
|
+
status = "error"
|
|
241
|
+
elif failed:
|
|
242
|
+
status = "error"
|
|
243
|
+
elif any(item.status == "warning" for item in items):
|
|
244
|
+
status = "warning"
|
|
245
|
+
return ValidationReport(
|
|
246
|
+
tool="core-runtime validate",
|
|
247
|
+
command=command,
|
|
248
|
+
status=status,
|
|
249
|
+
summary={
|
|
250
|
+
"kind": normalized,
|
|
251
|
+
"item_count": len(items),
|
|
252
|
+
"passed": passed,
|
|
253
|
+
"failed": failed,
|
|
254
|
+
"skipped": skipped,
|
|
255
|
+
},
|
|
256
|
+
selection=selection,
|
|
257
|
+
items=items,
|
|
258
|
+
diagnostics=diagnostics,
|
|
259
|
+
)
|
|
260
|
+
|
|
261
|
+
def _validate_schemas(self, diagnostics: DiagnosticCollection) -> list[ValidationItem]:
|
|
262
|
+
schema_dir = self.repo_root / "schemas"
|
|
263
|
+
items: list[ValidationItem] = []
|
|
264
|
+
if not schema_dir.is_dir():
|
|
265
|
+
diagnostics.add_blocked(
|
|
266
|
+
code="core.validate.schemas_missing",
|
|
267
|
+
message="schemas/ directory not found",
|
|
268
|
+
path="schemas/",
|
|
269
|
+
)
|
|
270
|
+
return items
|
|
271
|
+
|
|
272
|
+
for path in sorted(schema_dir.rglob("*.json")):
|
|
273
|
+
if not path.is_file():
|
|
274
|
+
continue
|
|
275
|
+
rel = path.relative_to(self.repo_root)
|
|
276
|
+
if _is_rejected_path(rel):
|
|
277
|
+
continue
|
|
278
|
+
try:
|
|
279
|
+
payload = json.loads(path.read_text(encoding="utf-8"))
|
|
280
|
+
except (OSError, json.JSONDecodeError) as exc:
|
|
281
|
+
diagnostics.add_error(
|
|
282
|
+
code="core.validate.schema_invalid_json",
|
|
283
|
+
message="Invalid JSON schema file: {0}".format(rel),
|
|
284
|
+
path=str(rel),
|
|
285
|
+
details=str(exc),
|
|
286
|
+
)
|
|
287
|
+
items.append(ValidationItem(kind="schema", name=path.stem, path=str(rel), status="failed"))
|
|
288
|
+
continue
|
|
289
|
+
if not isinstance(payload, dict):
|
|
290
|
+
diagnostics.add_error(
|
|
291
|
+
code="core.validate.schema_not_object",
|
|
292
|
+
message="Schema must be a JSON object: {0}".format(rel),
|
|
293
|
+
path=str(rel),
|
|
294
|
+
)
|
|
295
|
+
items.append(ValidationItem(kind="schema", name=path.stem, path=str(rel), status="failed"))
|
|
296
|
+
continue
|
|
297
|
+
errors = []
|
|
298
|
+
for required_key in ("$schema", "title", "type"):
|
|
299
|
+
if not isinstance(payload.get(required_key), str) or not payload.get(required_key):
|
|
300
|
+
errors.append(required_key)
|
|
301
|
+
if errors:
|
|
302
|
+
diagnostics.add_error(
|
|
303
|
+
code="core.validate.schema_missing_metadata",
|
|
304
|
+
message="Schema metadata missing required keys: {0}".format(", ".join(errors)),
|
|
305
|
+
path=str(rel),
|
|
306
|
+
expected="$schema,title,type",
|
|
307
|
+
actual="missing metadata",
|
|
308
|
+
)
|
|
309
|
+
items.append(ValidationItem(kind="schema", name=path.stem, path=str(rel), status="failed", details={"missing": errors}))
|
|
310
|
+
continue
|
|
311
|
+
if payload.get("type") == "object" and "additionalProperties" not in payload:
|
|
312
|
+
diagnostics.add_warning(
|
|
313
|
+
code="core.validate.schema_missing_additional_properties",
|
|
314
|
+
message="Object schema should declare additionalProperties: {0}".format(rel),
|
|
315
|
+
path=str(rel),
|
|
316
|
+
)
|
|
317
|
+
items.append(ValidationItem(kind="schema", name=payload.get("title", path.stem), path=str(rel), status="warning"))
|
|
318
|
+
continue
|
|
319
|
+
items.append(
|
|
320
|
+
ValidationItem(
|
|
321
|
+
kind="schema",
|
|
322
|
+
name=str(payload.get("title", path.stem)),
|
|
323
|
+
path=str(rel),
|
|
324
|
+
status="passed",
|
|
325
|
+
details={"schema_version": self._schema_version(payload)},
|
|
326
|
+
)
|
|
327
|
+
)
|
|
328
|
+
return items
|
|
329
|
+
|
|
330
|
+
def _validate_examples(self, diagnostics: DiagnosticCollection) -> list[ValidationItem]:
|
|
331
|
+
examples_dir = self.repo_root / "examples"
|
|
332
|
+
items: list[ValidationItem] = []
|
|
333
|
+
if not examples_dir.is_dir():
|
|
334
|
+
diagnostics.add_blocked(
|
|
335
|
+
code="core.validate.examples_missing",
|
|
336
|
+
message="examples/ directory not found",
|
|
337
|
+
path="examples/",
|
|
338
|
+
)
|
|
339
|
+
return items
|
|
340
|
+
|
|
341
|
+
for path in sorted(examples_dir.rglob("manifest.json")):
|
|
342
|
+
rel = path.relative_to(self.repo_root)
|
|
343
|
+
if _is_rejected_path(rel):
|
|
344
|
+
continue
|
|
345
|
+
item = self._validate_manifest_file(path, "example", diagnostics)
|
|
346
|
+
items.append(item)
|
|
347
|
+
return items
|
|
348
|
+
|
|
349
|
+
def _validate_manifests(self, diagnostics: DiagnosticCollection) -> list[ValidationItem]:
|
|
350
|
+
items: list[ValidationItem] = []
|
|
351
|
+
for path in sorted(self.repo_root.rglob("*manifest*.json")):
|
|
352
|
+
rel = path.relative_to(self.repo_root)
|
|
353
|
+
if _is_rejected_path(rel):
|
|
354
|
+
continue
|
|
355
|
+
item = self._validate_manifest_file(path, "manifest", diagnostics)
|
|
356
|
+
items.append(item)
|
|
357
|
+
return items
|
|
358
|
+
|
|
359
|
+
def _validate_contracts(self, diagnostics: DiagnosticCollection) -> list[ValidationItem]:
|
|
360
|
+
items: list[ValidationItem] = []
|
|
361
|
+
|
|
362
|
+
contracts_dir = self.repo_root / "contracts"
|
|
363
|
+
if contracts_dir.is_dir():
|
|
364
|
+
for path in sorted(contracts_dir.rglob("*.sol")):
|
|
365
|
+
rel = path.relative_to(self.repo_root)
|
|
366
|
+
text = path.read_text(encoding="utf-8")
|
|
367
|
+
if "contract " not in text and "interface " not in text:
|
|
368
|
+
diagnostics.add_error(
|
|
369
|
+
code="core.validate.contract_missing_contract_decl",
|
|
370
|
+
message="Solidity contract declaration not found: {0}".format(rel),
|
|
371
|
+
path=str(rel),
|
|
372
|
+
)
|
|
373
|
+
items.append(ValidationItem(kind="contract", name=path.stem, path=str(rel), status="failed"))
|
|
374
|
+
continue
|
|
375
|
+
items.append(ValidationItem(kind="contract", name=path.stem, path=str(rel), status="passed"))
|
|
376
|
+
|
|
377
|
+
docs_contracts = self.repo_root / "docs" / "contracts"
|
|
378
|
+
if docs_contracts.is_dir():
|
|
379
|
+
for path in sorted(docs_contracts.rglob("*.json")):
|
|
380
|
+
rel = path.relative_to(self.repo_root)
|
|
381
|
+
item = self._validate_schema_like_json(path, "contract-doc", diagnostics)
|
|
382
|
+
items.append(item)
|
|
383
|
+
return items
|
|
384
|
+
|
|
385
|
+
def _validate_domain(self, name: str, diagnostics: DiagnosticCollection) -> list[ValidationItem]:
|
|
386
|
+
domain_dir = self.repo_root / "core_runtime" / "domains" / name
|
|
387
|
+
items: list[ValidationItem] = []
|
|
388
|
+
if not domain_dir.is_dir():
|
|
389
|
+
diagnostics.add_error(
|
|
390
|
+
code="core.validate.domain_missing",
|
|
391
|
+
message="Domain not found: {0}".format(name),
|
|
392
|
+
path=str(domain_dir.relative_to(self.repo_root)),
|
|
393
|
+
expected="existing domain directory",
|
|
394
|
+
actual="missing",
|
|
395
|
+
)
|
|
396
|
+
return items
|
|
397
|
+
|
|
398
|
+
init_path = domain_dir / "__init__.py"
|
|
399
|
+
if not init_path.is_file():
|
|
400
|
+
diagnostics.add_error(
|
|
401
|
+
code="core.validate.domain_missing_init",
|
|
402
|
+
message="Domain package missing __init__.py: {0}".format(name),
|
|
403
|
+
path=str(init_path.relative_to(self.repo_root)),
|
|
404
|
+
)
|
|
405
|
+
items.append(ValidationItem(kind="domain", name=name, path=str(domain_dir.relative_to(self.repo_root)), status="failed"))
|
|
406
|
+
else:
|
|
407
|
+
items.append(ValidationItem(kind="domain", name=name, path=str(domain_dir.relative_to(self.repo_root)), status="passed"))
|
|
408
|
+
|
|
409
|
+
for path in sorted(domain_dir.rglob("*.py")):
|
|
410
|
+
if "__pycache__" in path.parts:
|
|
411
|
+
continue
|
|
412
|
+
rel = path.relative_to(self.repo_root)
|
|
413
|
+
try:
|
|
414
|
+
py_compile.compile(str(path), doraise=True)
|
|
415
|
+
except py_compile.PyCompileError as exc:
|
|
416
|
+
diagnostics.add_error(
|
|
417
|
+
code="core.validate.domain_python_compile_failed",
|
|
418
|
+
message="Python compile failed for {0}".format(rel),
|
|
419
|
+
path=str(rel),
|
|
420
|
+
details=exc.msg,
|
|
421
|
+
)
|
|
422
|
+
items.append(ValidationItem(kind="domain-python", name=path.stem, path=str(rel), status="failed"))
|
|
423
|
+
continue
|
|
424
|
+
items.append(ValidationItem(kind="domain-python", name=path.stem, path=str(rel), status="passed"))
|
|
425
|
+
|
|
426
|
+
for path in sorted(domain_dir.rglob("*.json")):
|
|
427
|
+
rel = path.relative_to(self.repo_root)
|
|
428
|
+
item = self._validate_schema_like_json(path, "domain-manifest", diagnostics)
|
|
429
|
+
item = ValidationItem(kind=item.kind, name=item.name, path=item.path, status=item.status, details=item.details)
|
|
430
|
+
items.append(item)
|
|
431
|
+
return items
|
|
432
|
+
|
|
433
|
+
def _validate_manifest_file(self, path: Path, kind: str, diagnostics: DiagnosticCollection) -> ValidationItem:
|
|
434
|
+
rel = path.relative_to(self.repo_root)
|
|
435
|
+
try:
|
|
436
|
+
payload = json.loads(path.read_text(encoding="utf-8"))
|
|
437
|
+
except (OSError, json.JSONDecodeError) as exc:
|
|
438
|
+
diagnostics.add_error(
|
|
439
|
+
code="core.validate.manifest_invalid_json",
|
|
440
|
+
message="Invalid JSON manifest: {0}".format(rel),
|
|
441
|
+
path=str(rel),
|
|
442
|
+
details=str(exc),
|
|
443
|
+
)
|
|
444
|
+
return ValidationItem(kind=kind, name=path.stem, path=str(rel), status="failed")
|
|
445
|
+
|
|
446
|
+
if not isinstance(payload, dict) or not payload:
|
|
447
|
+
diagnostics.add_error(
|
|
448
|
+
code="core.validate.manifest_not_object",
|
|
449
|
+
message="Manifest must be a non-empty JSON object: {0}".format(rel),
|
|
450
|
+
path=str(rel),
|
|
451
|
+
)
|
|
452
|
+
return ValidationItem(kind=kind, name=path.stem, path=str(rel), status="failed")
|
|
453
|
+
|
|
454
|
+
path_issues = self._manifest_path_issues(payload)
|
|
455
|
+
if path_issues:
|
|
456
|
+
diagnostics.add_error(
|
|
457
|
+
code="core.validate.manifest_path_invalid",
|
|
458
|
+
message="Manifest contains invalid path-like references: {0}".format(rel),
|
|
459
|
+
path=str(rel),
|
|
460
|
+
details=", ".join(path_issues),
|
|
461
|
+
)
|
|
462
|
+
return ValidationItem(kind=kind, name=path.stem, path=str(rel), status="failed", details={"path_issues": path_issues})
|
|
463
|
+
|
|
464
|
+
return ValidationItem(kind=kind, name=path.stem, path=str(rel), status="passed", details={"keys": sorted(payload)})
|
|
465
|
+
|
|
466
|
+
def _validate_schema_like_json(self, path: Path, kind: str, diagnostics: DiagnosticCollection) -> ValidationItem:
|
|
467
|
+
rel = path.relative_to(self.repo_root)
|
|
468
|
+
try:
|
|
469
|
+
payload = json.loads(path.read_text(encoding="utf-8"))
|
|
470
|
+
except (OSError, json.JSONDecodeError) as exc:
|
|
471
|
+
diagnostics.add_error(
|
|
472
|
+
code="core.validate.json_invalid",
|
|
473
|
+
message="Invalid JSON file: {0}".format(rel),
|
|
474
|
+
path=str(rel),
|
|
475
|
+
details=str(exc),
|
|
476
|
+
)
|
|
477
|
+
return ValidationItem(kind=kind, name=path.stem, path=str(rel), status="failed")
|
|
478
|
+
if not isinstance(payload, dict):
|
|
479
|
+
diagnostics.add_error(
|
|
480
|
+
code="core.validate.json_not_object",
|
|
481
|
+
message="JSON file must be an object: {0}".format(rel),
|
|
482
|
+
path=str(rel),
|
|
483
|
+
)
|
|
484
|
+
return ValidationItem(kind=kind, name=path.stem, path=str(rel), status="failed")
|
|
485
|
+
return ValidationItem(kind=kind, name=path.stem, path=str(rel), status="passed", details={"keys": sorted(payload)})
|
|
486
|
+
|
|
487
|
+
def _schema_version(self, payload: dict[str, Any]) -> str | None:
|
|
488
|
+
properties = payload.get("properties")
|
|
489
|
+
if isinstance(properties, dict):
|
|
490
|
+
schema_version = properties.get("schema_version")
|
|
491
|
+
if isinstance(schema_version, dict):
|
|
492
|
+
const = schema_version.get("const")
|
|
493
|
+
if isinstance(const, str) and const:
|
|
494
|
+
return const
|
|
495
|
+
return None
|
|
496
|
+
|
|
497
|
+
def _manifest_path_issues(self, payload: dict[str, Any]) -> list[str]:
|
|
498
|
+
issues: list[str] = []
|
|
499
|
+
for key, value in _walk_strings(payload):
|
|
500
|
+
lower_key = key.lower()
|
|
501
|
+
if not isinstance(value, str):
|
|
502
|
+
continue
|
|
503
|
+
if not any(token in lower_key for token in ("path", "file", "dir", "document", "artifact", "source", "output")):
|
|
504
|
+
continue
|
|
505
|
+
if value.startswith(("/", "\\")) or ".." in Path(value).parts:
|
|
506
|
+
issues.append(f"{key}:absolute_or_traversal")
|
|
507
|
+
return issues
|