Files
zemion 2ffdb23f69
Dependency Audit / dependency-audit (push) Successful in 1m45s
Deployment Installer / deployment-installer (push) Successful in 6s
Security Audit / security-audit (push) Successful in 11m30s
feat(devkit): add resumable workspace automation and UI review tooling
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.
2026-09-09 02:03:17 +02:00

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")