Verified with the coordinated workspace changes by devkit full run 2026-09-08T225814-186389-0000-3e3ed7cd (all seven phases passed). This shared UI pass does not mark the individual module reviews complete.
143 lines
6.0 KiB
Python
Executable File
143 lines
6.0 KiB
Python
Executable File
"""Dependency-free validation against the published portable-project schema."""
|
|
|
|
from __future__ import annotations
|
|
|
|
import math
|
|
from pathlib import Path
|
|
import re
|
|
|
|
from .common import META_ROOT, canonical, read_json
|
|
|
|
|
|
def _schema_value(value, schema: dict, definitions: dict, path: str) -> None:
|
|
if "$ref" in schema:
|
|
return _schema_value(
|
|
value,
|
|
definitions[schema["$ref"].removeprefix("#/$defs/")],
|
|
definitions,
|
|
path,
|
|
)
|
|
kind = schema.get("type")
|
|
numeric = type(value) is int or type(value) is float and math.isfinite(value)
|
|
matches = {
|
|
"object": isinstance(value, dict),
|
|
"array": isinstance(value, list),
|
|
"string": isinstance(value, str),
|
|
"number": numeric,
|
|
"integer": numeric and int(value) == value,
|
|
}
|
|
if kind and not matches[kind]:
|
|
raise ValueError(f"{path} must be a {kind}")
|
|
if "const" in schema and value != schema["const"]:
|
|
raise ValueError(f"{path} has an unsupported value")
|
|
if "enum" in schema and value not in schema["enum"]:
|
|
raise ValueError(f"{path} has an unsupported value")
|
|
if isinstance(value, dict):
|
|
properties = schema.get("properties", {})
|
|
unknown = set(value) - set(properties)
|
|
if schema.get("additionalProperties") is False and unknown:
|
|
names = ", ".join(key[:80] for key in sorted(unknown)[:4])
|
|
raise ValueError(f"Unknown field in {path}: {names}")
|
|
if set(schema.get("required", [])) - set(value):
|
|
raise ValueError(f"{path} is missing a required field")
|
|
for key, item in value.items():
|
|
if key in properties:
|
|
_schema_value(item, properties[key], definitions, f"{path}.{key}")
|
|
if isinstance(value, list):
|
|
if (
|
|
not schema.get("minItems", 0)
|
|
<= len(value)
|
|
<= schema.get("maxItems", len(value))
|
|
):
|
|
raise ValueError(f"{path} exceeds its item bounds")
|
|
if schema.get("uniqueItems") and len(
|
|
{canonical(item) for item in value}
|
|
) != len(value):
|
|
raise ValueError(f"Duplicate value in {path}")
|
|
prefix = schema.get("prefixItems", [])
|
|
for index, item in enumerate(value):
|
|
_schema_value(
|
|
item,
|
|
prefix[index] if index < len(prefix) else schema.get("items", {}),
|
|
definitions,
|
|
f"{path}[{index}]",
|
|
)
|
|
if isinstance(value, str):
|
|
if (
|
|
not schema.get("minLength", 0)
|
|
<= len(value)
|
|
<= schema.get("maxLength", len(value))
|
|
):
|
|
raise ValueError(f"{path} exceeds its string bounds")
|
|
if "pattern" in schema and re.search(schema["pattern"], value) is None:
|
|
raise ValueError(
|
|
f"{path} contains an invalid identifier, path or character"
|
|
)
|
|
if type(value) in {int, float}:
|
|
if (
|
|
"maximum" in schema
|
|
and value > schema["maximum"]
|
|
or "exclusiveMinimum" in schema
|
|
and value <= schema["exclusiveMinimum"]
|
|
):
|
|
raise ValueError(f"{path} exceeds its numeric bounds")
|
|
|
|
|
|
def validate_project(payload: dict, workspace_root: Path) -> None:
|
|
"""Validate every declaration, not just the selected profile/dependencies."""
|
|
schema = read_json(META_ROOT / "tools/devkit/project.schema.json")
|
|
_schema_value(payload, schema, schema["$defs"], "project")
|
|
root = workspace_root.resolve()
|
|
records = payload["repositories"]
|
|
names = {record["name"] for record in records}
|
|
if len(names) != len(records):
|
|
raise ValueError("Duplicate project repository name")
|
|
paths, aliases = set(), set()
|
|
for record in records:
|
|
path = (root / record["path"]).resolve()
|
|
if not path.is_relative_to(root):
|
|
raise ValueError("Repository path escapes the workspace")
|
|
if path in paths:
|
|
raise ValueError("Duplicate project repository path")
|
|
paths.add(path)
|
|
keys = {record["name"], *record.get("aliases", [])}
|
|
if aliases & keys:
|
|
raise ValueError("Repository names and aliases must be unambiguous")
|
|
aliases.update(keys)
|
|
checks = payload.get("checks", [])
|
|
identities = {item["id"] for item in checks}
|
|
if len(identities) != len(checks):
|
|
raise ValueError("Duplicate project check ID")
|
|
for item in checks:
|
|
if set(item.get("repos", [])) - names:
|
|
raise ValueError(f"Unknown repository reference in check {item['id']}")
|
|
if set(item.get("deps", [])) - identities:
|
|
raise ValueError(f"Unknown check dependency in {item['id']}")
|
|
if set(item.get("after", [])) - identities:
|
|
raise ValueError(f"Unknown check ordering reference in {item['id']}")
|
|
if set(item.get("deps", [])) & set(item.get("after", [])):
|
|
raise ValueError(f"Duplicate dependency/ordering reference in {item['id']}")
|
|
if set(item.get("inputs", {}).get("repos", [])) - names:
|
|
raise ValueError(f"Unknown input repository reference in {item['id']}")
|
|
if not (root / item.get("cwd", ".")).resolve().is_relative_to(root):
|
|
raise ValueError(f"Check {item['id']} cwd escapes the workspace")
|
|
for name, selected in payload.get("profiles", {}).items():
|
|
if set(selected) - identities:
|
|
raise ValueError(f"Unknown profile check in {name}")
|
|
remaining = {
|
|
item["id"]: set(item.get("deps", [])) | set(item.get("after", []))
|
|
for item in checks
|
|
}
|
|
while remaining:
|
|
ready = {identity for identity, deps in remaining.items() if not deps}
|
|
if not ready:
|
|
raise ValueError("Cyclic check dependency in project declarations")
|
|
remaining = {
|
|
identity: deps - ready
|
|
for identity, deps in remaining.items()
|
|
if identity not in ready
|
|
}
|
|
for value in payload.get("review", {}).values():
|
|
if not (root / value).resolve().is_relative_to(root):
|
|
raise ValueError("Project review path escapes the workspace")
|