core-runtime-engine 11.5.1__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (96) hide show
  1. core_runtime/__init__.py +10 -0
  2. core_runtime/__version__.py +17 -0
  3. core_runtime/cli/__init__.py +7 -0
  4. core_runtime/cli/__main__.py +22 -0
  5. core_runtime/cli/bump_version.py +176 -0
  6. core_runtime/cli/contract_preflight.py +69 -0
  7. core_runtime/cli/create_domain.py +66 -0
  8. core_runtime/cli/doctor.py +44 -0
  9. core_runtime/cli/inventory.py +85 -0
  10. core_runtime/cli/lint.py +8 -0
  11. core_runtime/cli/main.py +579 -0
  12. core_runtime/cli/release_check.py +65 -0
  13. core_runtime/cli/repair_artifact_paths.py +48 -0
  14. core_runtime/cli/sync_template.py +67 -0
  15. core_runtime/cli/validate.py +73 -0
  16. core_runtime/core/__init__.py +34 -0
  17. core_runtime/core/audit_event.py +760 -0
  18. core_runtime/core/audit_trail_index.py +100 -0
  19. core_runtime/core/canonicalization.py +55 -0
  20. core_runtime/core/contract_evaluator.py +945 -0
  21. core_runtime/core/contract_executability.py +307 -0
  22. core_runtime/core/contract_loader.py +57 -0
  23. core_runtime/core/contract_probes.py +630 -0
  24. core_runtime/core/contract_program.py +131 -0
  25. core_runtime/core/contract_program_registry.py +104 -0
  26. core_runtime/core/contract_program_v2.py +126 -0
  27. core_runtime/core/dsk_v3.py +142 -0
  28. core_runtime/core/explainability.py +2115 -0
  29. core_runtime/core/numeric_normalization.py +120 -0
  30. core_runtime/core/rule_anchor.py +1388 -0
  31. core_runtime/core/schema_fingerprint.py +69 -0
  32. core_runtime/core/sensor_evidence.py +548 -0
  33. core_runtime/data/contracts/CoreAnchor.sol +109 -0
  34. core_runtime/data/contracts/CoreRuleAnchor.abi.json +111 -0
  35. core_runtime/data/contracts/CoreRuleAnchor.bin +1 -0
  36. core_runtime/data/contracts/CoreRuleAnchor.build.json +20 -0
  37. core_runtime/data/contracts/CoreRuleAnchor.runtime.bin +1 -0
  38. core_runtime/data/contracts/CoreRuleAnchor.sol +74 -0
  39. core_runtime/data/package_data_manifest.v1.json +173 -0
  40. core_runtime/data/schemas/core/causal_trace.v1.json +141 -0
  41. core_runtime/data/schemas/core/context_gate.v1.json +38 -0
  42. core_runtime/data/schemas/core/context_threshold.v1.json +46 -0
  43. core_runtime/data/schemas/core/contract_program.v1.json +186 -0
  44. core_runtime/data/schemas/core/contract_program.v2.json +187 -0
  45. core_runtime/data/schemas/core/control_decision.v1.json +114 -0
  46. core_runtime/data/schemas/core/dsk.v3.json +105 -0
  47. core_runtime/data/schemas/core/effect_result.v1.json +39 -0
  48. core_runtime/data/schemas/core/entropy_signal.v1.json +108 -0
  49. core_runtime/data/schemas/core/execution_receipt.v1.json +93 -0
  50. core_runtime/data/schemas/core/frozen_release_manifest.v1.json +87 -0
  51. core_runtime/data/schemas/core/frozen_release_manifest.v2.json +72 -0
  52. core_runtime/data/schemas/core/frozen_release_manifest.v3.json +38 -0
  53. core_runtime/data/schemas/core/frozen_release_manifest.v4.json +72 -0
  54. core_runtime/data/schemas/core/frozen_release_manifest.v5.json +38 -0
  55. core_runtime/data/schemas/core/frozen_release_manifest.v6.json +116 -0
  56. core_runtime/data/schemas/core/frozen_release_manifest.v7.json +37 -0
  57. core_runtime/data/schemas/core/frozen_release_manifest.v8.json +114 -0
  58. core_runtime/data/schemas/core/frozen_rule_set.v1.json +282 -0
  59. core_runtime/data/schemas/core/memory_artifact.v1.json +120 -0
  60. core_runtime/data/schemas/core/memory_generation_result.v1.json +37 -0
  61. core_runtime/data/schemas/core/operational_learning_event.v1.json +63 -0
  62. core_runtime/data/schemas/core/pattern_candidate.v1.json +114 -0
  63. core_runtime/data/schemas/core/physical_safety_assurance_case.v1.json +676 -0
  64. core_runtime/data/schemas/core/policy_lifecycle.v1.json +99 -0
  65. core_runtime/data/schemas/core/retention_manifest.v1.json +53 -0
  66. core_runtime/data/schemas/core/reversibility_policy.v1.json +107 -0
  67. core_runtime/data/schemas/core/rule_anchor_batch.v1.json +93 -0
  68. core_runtime/data/schemas/core/rule_anchor_chain_evidence.v1.json +56 -0
  69. core_runtime/data/schemas/core/rule_approval.v1.json +49 -0
  70. core_runtime/data/schemas/core/rule_approval_request.v1.json +42 -0
  71. core_runtime/data/schemas/core/state_transition.v1.json +115 -0
  72. core_runtime/data/schemas/core/task_closeout.v1.json +47 -0
  73. core_runtime/data/schemas/core/template_promotion_candidate.v1.json +83 -0
  74. core_runtime/data/schemas/core/unsigned_rule_anchor_deployment.v1.json +106 -0
  75. core_runtime/data/schemas/core/unsigned_rule_anchor_transaction.v1.json +116 -0
  76. core_runtime/tooling/__init__.py +48 -0
  77. core_runtime/tooling/bump_version.py +900 -0
  78. core_runtime/tooling/contract_preflight.py +293 -0
  79. core_runtime/tooling/create_domain.py +254 -0
  80. core_runtime/tooling/diagnostics.py +129 -0
  81. core_runtime/tooling/doctor.py +482 -0
  82. core_runtime/tooling/file_inventory.py +182 -0
  83. core_runtime/tooling/json_checks.py +100 -0
  84. core_runtime/tooling/release_check.py +1017 -0
  85. core_runtime/tooling/repair_artifact_paths.py +458 -0
  86. core_runtime/tooling/report_writer.py +176 -0
  87. core_runtime/tooling/repository_inventory.py +399 -0
  88. core_runtime/tooling/safety_checks.py +172 -0
  89. core_runtime/tooling/sync_template.py +303 -0
  90. core_runtime/tooling/validation.py +507 -0
  91. core_runtime/tooling/version_inventory.py +256 -0
  92. core_runtime_engine-11.5.1.dist-info/METADATA +35 -0
  93. core_runtime_engine-11.5.1.dist-info/RECORD +96 -0
  94. core_runtime_engine-11.5.1.dist-info/WHEEL +5 -0
  95. core_runtime_engine-11.5.1.dist-info/entry_points.txt +2 -0
  96. core_runtime_engine-11.5.1.dist-info/top_level.txt +1 -0
@@ -0,0 +1,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