Files
Agent-System-Dev/scripts/workspace/manifest.py
T
weichen b450e51c40 Add safe component editing and explicit legacy workspace transition
Why: Preserve a single Dev entry and independent repository ownership while allowing active feature branches to migrate without losing source or Git state.

Evidence: SELF 29 real Git/Go workspace tests and ordinary component evidence/lock tests passed; detailed RED/GREEN runs are recorded by make evidence in the task card.

Impact: Add fixed cloning, selected-version baselines, development overlays, npm source resolution, and independently preserved legacy snapshots. No other active worktree is modified.

Refs: Versioned dependencies U2/U5; R15; feature/dependency-workspace
2026-10-13 15:58:23 +00:00

95 lines
5.0 KiB
Python

import re
from urllib.parse import urlsplit
from .common import WorkspaceError, read_json, relative_path, safe_path
ROLES = {"application", "go-library", "npm-library", "documentation", "guidelines"}
FIXED_ROLES = {"application", "documentation", "guidelines"}
SHA = re.compile(r"[0-9a-f]{40}\Z")
IDENTIFIER = re.compile(r"[A-Za-z0-9][A-Za-z0-9_-]*\Z")
def validate_repo(repo):
if not isinstance(repo, str) or not repo or repo.startswith("-") or any(c.isspace() for c in repo):
raise WorkspaceError("invalid component repository")
if "://" in repo:
parsed = urlsplit(repo)
if parsed.scheme not in {"ssh", "https", "file"} or parsed.password or parsed.query or parsed.fragment:
raise WorkspaceError("repository must be a credential-free approved Git URL")
if parsed.scheme == "https" and parsed.username:
raise WorkspaceError("repository credentials are forbidden")
if parsed.scheme == "ssh" and parsed.username not in (None, "git"):
raise WorkspaceError("unsupported SSH repository identity")
elif not repo.startswith("/"):
raise WorkspaceError("repository must be an absolute local path or approved URL")
class Manifest:
def __init__(self, root):
self.root = root
document = read_json(root / "components.json")
if not isinstance(document, dict) or document.get("schema") != 1 or not isinstance(document.get("components"), list):
raise WorkspaceError("unsupported components schema")
self.components = {}
modules = set()
for component in document["components"]:
if not isinstance(component, dict):
raise WorkspaceError("invalid component record")
name = component.get("id")
if not isinstance(name, str) or not IDENTIFIER.fullmatch(name) or name in self.components:
raise WorkspaceError("invalid or duplicate component id")
if component.get("path") != ".workspace/" + name:
raise WorkspaceError(f"component path must be .workspace/{name}")
safe_path(root, component["path"])
if component.get("legacy_path"):
safe_path(root, component["legacy_path"])
if component.get("role") not in ROLES:
raise WorkspaceError(f"invalid component role: {name}")
package = component.get("package_name")
if package is not None and (component["role"] != "npm-library" or not isinstance(package, str)
or not re.fullmatch(r"(?:@[a-z0-9][a-z0-9._-]*/)?[a-z0-9][a-z0-9._-]*", package)):
raise WorkspaceError(f"invalid npm package mapping: {name}")
validate_repo(component.get("repo"))
target = component.get("target", "main")
validate_ref(target)
entries = component.get("modules", [])
if not isinstance(entries, list):
raise WorkspaceError("modules must be a list")
for module in entries:
if not isinstance(module, dict) or not isinstance(module.get("path"), str) or not module["path"]:
raise WorkspaceError("invalid module mapping")
if module["path"] in modules or any(c.isspace() for c in module["path"]):
raise WorkspaceError("duplicate or invalid module identity")
relative_path(module.get("directory", "."), allow_dot=True)
modules.add(module["path"])
self.components[name] = component
lock = read_json(root / "components.lock.json")
if not isinstance(lock, dict) or lock.get("schema") != 1 or not isinstance(lock.get("components"), dict):
raise WorkspaceError("unsupported component lock schema")
self.lock = lock["components"]
for name, value in self.lock.items():
if name not in self.components or not isinstance(value, dict) or not SHA.fullmatch(str(value.get("commit", ""))):
raise WorkspaceError(f"invalid locked component: {name}")
if self.components[name]["role"] in ("go-library", "npm-library"):
raise WorkspaceError(f"language library {name} cannot have a competing source lock")
def select(self, name=None, fixed=False):
if name:
if name not in self.components:
raise WorkspaceError(f"unknown component: {name}")
return [self.components[name]]
return [entry for entry in self.components.values() if not fixed or entry["role"] in FIXED_ROLES]
def validate_ref(ref):
if not isinstance(ref, str) or not re.fullmatch(r"[A-Za-z0-9][A-Za-z0-9._/-]*", ref):
raise WorkspaceError("invalid branch/ref name")
if any(token in ref for token in ("..", "//", "/.", ".lock")) or ref.endswith(("/", ".")):
raise WorkspaceError("invalid branch/ref name")
def validate_branch(branch):
validate_ref(branch)
if not branch.startswith(("feature/", "fix/", "chore/", "codex/")):
raise WorkspaceError("editing requires a named task branch")