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.
This commit is contained in:
Executable
+142
@@ -0,0 +1,142 @@
|
||||
"""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")
|
||||
Reference in New Issue
Block a user