Files
2026-08-06 19:21:08 +02:00

138 lines
5.6 KiB
Python

#!/usr/bin/env python3
"""Validate DoGaMa specification schemas, examples and internal links."""
from __future__ import annotations
import hashlib
import json
import re
import sys
from pathlib import Path
import yaml
from jsonschema import Draft202012Validator
ROOT = Path(__file__).resolve().parent.parent
def load_json(path: Path):
with path.open("r", encoding="utf-8") as handle:
return json.load(handle)
def load_yaml(path: Path):
with path.open("r", encoding="utf-8") as handle:
return yaml.safe_load(handle)
def validate(schema_path: Path, document_path: Path) -> dict:
schema = load_json(schema_path)
Draft202012Validator.check_schema(schema)
document = load_yaml(document_path)
errors = sorted(Draft202012Validator(schema).iter_errors(document), key=lambda item: list(item.path))
if errors:
rendered = "\n".join(f" {document_path}:{'/'.join(map(str, error.path))}: {error.message}" for error in errors)
raise ValueError(f"Schema validation failed:\n{rendered}")
return document
def validate_links() -> None:
pattern = re.compile(r"\[[^]]+\]\(([^)]+)\)")
failures = []
for markdown in ROOT.rglob("*.md"):
text = markdown.read_text(encoding="utf-8")
for target in pattern.findall(text):
target = target.split("#", 1)[0].strip()
if not target or target.startswith(("http://", "https://", "mailto:")):
continue
resolved = (markdown.parent / target).resolve()
if not resolved.exists():
failures.append(f"{markdown.relative_to(ROOT)} -> {target}")
if failures:
raise ValueError("Broken internal links:\n " + "\n ".join(failures))
def validate_cross_references(template: dict, manifest: dict) -> list[str]:
warnings = []
port_ids = {item["id"] for item in template["container"]["ports"]}
mount_ids = {item["id"] for item in template["storage"]["mounts"]}
capabilities = set(template["capabilities"])
integration = template.get("integration")
if integration:
assert integration["port_id"] in port_ids, "Template integration references an unknown port"
assert integration["module_id"] == manifest["id"], "Template and manifest module IDs disagree"
assert template["game"]["id"] in manifest["game_ids"], "Manifest does not support template game ID"
assert set(manifest["permissions"]["network"]["port_ids"]) <= port_ids, "Manifest references an unknown template port"
assert set(manifest["capabilities"]) == capabilities, "Template and reference manifest capabilities disagree"
assert set(template["backup"]["source_mounts"]) <= mount_ids, "Backup references an unknown mount"
assert template["imports"]["destination_mount"] in mount_ids, "Import references an unknown mount"
mods = template.get("mods", {})
if mods.get("supported"):
assert mods.get("destination_mount") in mount_ids, "Mods reference an unknown mount"
for asset in template["container"].get("assets", []):
path = (ROOT / "catalog" / "palworld" / asset["source"]).resolve()
assert path.is_file(), f"Missing packaged asset: {path}"
digest = hashlib.sha256(path.read_bytes()).hexdigest()
assert digest == asset["sha256"], f"Asset checksum mismatch: {path}"
wasm_digest = manifest["artifacts"]["sha256"]
wasm_path = ROOT / "modules" / manifest["id"] / manifest["artifacts"]["wasm"]
if wasm_digest == "0" * 64 and not wasm_path.exists():
warnings.append("Palworld module is a source specification: module.wasm and its final checksum are intentionally pending.")
elif wasm_path.is_file():
assert hashlib.sha256(wasm_path.read_bytes()).hexdigest() == wasm_digest, "WASM checksum mismatch"
else:
raise AssertionError("Manifest names a missing WASM artifact without the documented placeholder")
return warnings
def validate_coverage() -> None:
required = {
"Docker agent": "docs/architecture/docker-agent.md",
"WebAssembly": "docs/architecture/wasm-modules.md",
"threat model": "docs/security/security-and-threat-model.md",
"SQLite": "docs/domain/data-model.md",
"backup": "docs/operations/backups-import-export.md",
"Discord": "docs/operations/notifications-and-audit.md",
"30 days": "docs/operations/notifications-and-audit.md",
"manager": "docs/domain/authorization.md",
"Palworld": "catalog/palworld/README.md",
"acceptance": "docs/product/acceptance-criteria.md",
"roadmap": "docs/product/roadmap.md",
"Codex": "docs/contributing/ai-codex-guide.md",
}
missing = []
for needle, relative in required.items():
if needle.casefold() not in (ROOT / relative).read_text(encoding="utf-8").casefold():
missing.append(f"{needle!r} in {relative}")
if missing:
raise ValueError("Missing required coverage: " + ", ".join(missing))
def main() -> int:
template = validate(ROOT / "specs/template.schema.json", ROOT / "catalog/palworld/template.yaml")
manifest = validate(ROOT / "specs/module-manifest.schema.json", ROOT / "modules/palworld-rest/manifest.yaml")
load_yaml(ROOT / "compose.yaml")
for fixture in ROOT.rglob("*.json"):
load_json(fixture)
validate_links()
warnings = validate_cross_references(template, manifest)
validate_coverage()
print("DoGaMa specification validation passed.")
for warning in warnings:
print(f"WARNING: {warning}")
return 0
if __name__ == "__main__":
try:
raise SystemExit(main())
except Exception as error:
print(f"ERROR: {error}", file=sys.stderr)
raise SystemExit(1)