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:
@@ -5,9 +5,16 @@ import path from "node:path";
|
||||
import { createHash } from "node:crypto";
|
||||
import { pathToFileURL } from "node:url";
|
||||
|
||||
const [metaRootArgument] = process.argv.slice(2);
|
||||
const [metaRootArgument, ...argumentsRest] = process.argv.slice(2);
|
||||
if (!metaRootArgument) {
|
||||
throw new Error("Usage: extract-webui-structure.mjs META_ROOT");
|
||||
throw new Error("Usage: extract-webui-structure.mjs META_ROOT [--workspace-root ROOT]");
|
||||
}
|
||||
let explicitWorkspaceRoot;
|
||||
for (let index = 0; index < argumentsRest.length; index++) {
|
||||
if (argumentsRest[index] !== "--workspace-root" || explicitWorkspaceRoot !== undefined || !argumentsRest[index + 1]) {
|
||||
throw new Error("Expected one --workspace-root ROOT option");
|
||||
}
|
||||
explicitWorkspaceRoot = path.resolve(argumentsRest[++index]);
|
||||
}
|
||||
|
||||
const metaRoot = path.resolve(metaRootArgument);
|
||||
@@ -15,12 +22,18 @@ const repositoryCatalog = JSON.parse(
|
||||
fs.readFileSync(path.join(metaRoot, "repositories.json"), "utf8")
|
||||
);
|
||||
const siblingWorkspaceRoot = path.dirname(metaRoot);
|
||||
const configuredWorkspaceRoot = path.resolve(repositoryCatalog.default_parent);
|
||||
const workspaceRoot = fs.existsSync(
|
||||
// Discovery is retained only for old direct callers. An explicit root wins
|
||||
// even when another configured checkout contains more optional modules.
|
||||
const workspaceRoot = explicitWorkspaceRoot ?? (fs.existsSync(
|
||||
path.join(siblingWorkspaceRoot, "govoplan-core", "webui")
|
||||
)
|
||||
? siblingWorkspaceRoot
|
||||
: configuredWorkspaceRoot;
|
||||
) ? siblingWorkspaceRoot : path.resolve(repositoryCatalog.default_parent));
|
||||
if (!fs.statSync(workspaceRoot).isDirectory()) throw new Error("Inventory workspace root must be an existing directory");
|
||||
const actualWorkspaceRoot = fs.realpathSync(workspaceRoot);
|
||||
function withinWorkspace(candidate) {
|
||||
const relative = path.relative(actualWorkspaceRoot, fs.realpathSync(candidate));
|
||||
return relative === "" || (!relative.startsWith(`..${path.sep}`) && relative !== ".." && !path.isAbsolute(relative));
|
||||
}
|
||||
if (!Array.isArray(repositoryCatalog.repositories)) throw new Error("Repository catalog requires a repositories array");
|
||||
const typescriptPath = path.join(
|
||||
workspaceRoot,
|
||||
"govoplan-core",
|
||||
@@ -96,6 +109,7 @@ const contributionTypes = new Map([
|
||||
]);
|
||||
|
||||
const result = {
|
||||
workspaceRoot: actualWorkspaceRoot,
|
||||
fields: [],
|
||||
actions: [],
|
||||
labels: [],
|
||||
@@ -111,8 +125,12 @@ const result = {
|
||||
};
|
||||
|
||||
for (const repository of repositoryCatalog.repositories) {
|
||||
const sourceRoot = path.join(workspaceRoot, repository.path, "webui", "src");
|
||||
if (!repository || typeof repository.path !== "string" || !repository.path || path.isAbsolute(repository.path) || repository.path.split(/[\\/]/).includes("..")) {
|
||||
throw new Error("Inventory repository paths must remain inside the selected workspace");
|
||||
}
|
||||
const sourceRoot = path.join(actualWorkspaceRoot, repository.path, "webui", "src");
|
||||
if (!fs.existsSync(sourceRoot)) continue;
|
||||
if (!withinWorkspace(sourceRoot)) throw new Error("Inventory source root escapes the selected workspace");
|
||||
for (const sourcePath of sourceFiles(sourceRoot)) {
|
||||
inspectSource(repository.name, sourceRoot, sourcePath);
|
||||
}
|
||||
@@ -135,7 +153,7 @@ function sourceFiles(root) {
|
||||
}
|
||||
const candidate = path.join(current, entry.name);
|
||||
if (entry.isDirectory()) pending.push(candidate);
|
||||
else if (/\.(?:ts|tsx)$/.test(entry.name)) files.push(candidate);
|
||||
else if (entry.isFile() && /\.(?:ts|tsx)$/.test(entry.name)) files.push(candidate);
|
||||
}
|
||||
}
|
||||
return files.sort();
|
||||
@@ -150,7 +168,7 @@ function inspectSource(repository, sourceRoot, sourcePath) {
|
||||
true,
|
||||
sourcePath.endsWith(".tsx") ? ts.ScriptKind.TSX : ts.ScriptKind.TS
|
||||
);
|
||||
const relativeFile = path.relative(path.join(workspaceRoot, repository), sourcePath);
|
||||
const relativeFile = path.relative(path.resolve(sourceRoot, "..", ".."), sourcePath);
|
||||
const identityCounters = new Map();
|
||||
|
||||
function location(node) {
|
||||
|
||||
@@ -9,6 +9,7 @@ from collections import Counter
|
||||
from dataclasses import asdict, is_dataclass
|
||||
import importlib
|
||||
import json
|
||||
import os
|
||||
from pathlib import Path
|
||||
import re
|
||||
import subprocess
|
||||
@@ -40,6 +41,11 @@ REFERENCE_LOCALE = "de"
|
||||
|
||||
def main() -> int:
|
||||
parser = argparse.ArgumentParser(description=__doc__)
|
||||
parser.add_argument(
|
||||
"--workspace-root",
|
||||
type=Path,
|
||||
help="Authoritative directory containing registered checkouts; never falls back to another workspace.",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--output-dir",
|
||||
type=Path,
|
||||
@@ -87,8 +93,9 @@ def main() -> int:
|
||||
args = parser.parse_args()
|
||||
|
||||
catalog = json.loads((META_ROOT / "repositories.json").read_text(encoding="utf-8"))
|
||||
workspace_root = _resolve_workspace_root(catalog)
|
||||
webui = _extract_webui()
|
||||
workspace_root = _resolve_workspace_root(catalog, args.workspace_root)
|
||||
_validate_repository_roots(catalog, workspace_root)
|
||||
webui = _extract_webui(workspace_root)
|
||||
backend_endpoints = _extract_backend_endpoints(catalog, workspace_root)
|
||||
manifests = _extract_manifests(catalog, workspace_root)
|
||||
endpoint_declarations = _load_endpoint_declarations(
|
||||
@@ -109,6 +116,10 @@ def main() -> int:
|
||||
else None
|
||||
),
|
||||
)
|
||||
inventory["workspace_root"] = str(workspace_root)
|
||||
inventory["workspace_selection"] = (
|
||||
"explicit" if args.workspace_root is not None else "legacy-discovery"
|
||||
)
|
||||
|
||||
output_dir = args.output_dir.resolve()
|
||||
output_dir.mkdir(parents=True, exist_ok=True)
|
||||
@@ -212,7 +223,16 @@ def _strict_failures(
|
||||
return failures
|
||||
|
||||
|
||||
def _resolve_workspace_root(catalog: dict[str, Any]) -> Path:
|
||||
def _resolve_workspace_root(
|
||||
catalog: dict[str, Any], explicit_root: Path | None = None
|
||||
) -> Path:
|
||||
if explicit_root is not None:
|
||||
root = explicit_root.expanduser().resolve()
|
||||
if not root.is_dir():
|
||||
raise ValueError("The explicit inventory workspace root must be an existing directory")
|
||||
return root
|
||||
# Compatibility for direct legacy callers only. Managed callers always
|
||||
# supply their selected root; a partial checkout must not borrow sources.
|
||||
sibling_root = META_ROOT.parent.resolve()
|
||||
configured_root = Path(str(catalog["default_parent"])).expanduser().resolve()
|
||||
repositories = catalog.get("repositories")
|
||||
@@ -233,15 +253,44 @@ def _resolve_workspace_root(catalog: dict[str, Any]) -> Path:
|
||||
return sibling_root if sibling_count >= configured_count else configured_root
|
||||
|
||||
|
||||
def _extract_webui() -> dict[str, Any]:
|
||||
def _validate_repository_roots(catalog: dict[str, Any], workspace_root: Path) -> None:
|
||||
repositories = catalog.get("repositories")
|
||||
if not isinstance(repositories, list):
|
||||
raise ValueError("repository catalog has no repositories array")
|
||||
for repository in repositories:
|
||||
if not isinstance(repository, dict) or not isinstance(repository.get("path"), str):
|
||||
raise ValueError("Invalid inventory repository path")
|
||||
relative = Path(repository["path"])
|
||||
if not repository["path"] or relative.is_absolute() or ".." in relative.parts:
|
||||
raise ValueError("Inventory repository paths must remain inside the selected workspace")
|
||||
root = workspace_root / relative
|
||||
# Missing optional checkouts are allowed; links to another checkout are
|
||||
# not evidence for the selected workspace.
|
||||
if not root.resolve().is_relative_to(workspace_root):
|
||||
raise ValueError("Inventory repository path escapes the selected workspace")
|
||||
for source in (root / "src", root / "webui/src"):
|
||||
if not source.resolve().is_relative_to(workspace_root):
|
||||
raise ValueError("Inventory source root escapes the selected workspace")
|
||||
|
||||
|
||||
def _extract_webui(workspace_root: Path | None = None) -> dict[str, Any]:
|
||||
helper = META_ROOT / "tools" / "inventory" / "extract-webui-structure.mjs"
|
||||
argv = [os.environ.get("NODE", "node"), str(helper), str(META_ROOT)]
|
||||
if workspace_root is not None:
|
||||
argv.extend(["--workspace-root", str(workspace_root)])
|
||||
completed = subprocess.run(
|
||||
["node", str(helper), str(META_ROOT)],
|
||||
argv,
|
||||
check=True,
|
||||
capture_output=True,
|
||||
text=True,
|
||||
)
|
||||
return json.loads(completed.stdout)
|
||||
result = json.loads(completed.stdout)
|
||||
if workspace_root is not None and (
|
||||
not isinstance(result, dict)
|
||||
or result.get("workspaceRoot") != str(workspace_root.resolve())
|
||||
):
|
||||
raise ValueError("WebUI collector did not confirm the selected inventory workspace")
|
||||
return result
|
||||
|
||||
|
||||
def _extract_backend_endpoints(
|
||||
@@ -255,6 +304,8 @@ def _extract_backend_endpoints(
|
||||
if not source_root.is_dir():
|
||||
continue
|
||||
for source_path in sorted(source_root.rglob("*.py")):
|
||||
if not source_path.resolve().is_relative_to(workspace_root):
|
||||
raise ValueError("Backend source path escapes the selected workspace")
|
||||
try:
|
||||
tree = ast.parse(
|
||||
source_path.read_text(encoding="utf-8"),
|
||||
@@ -346,15 +397,26 @@ def _extract_manifests(
|
||||
catalog: dict[str, Any],
|
||||
workspace_root: Path,
|
||||
) -> list[dict[str, Any]]:
|
||||
_validate_repository_roots(catalog, workspace_root)
|
||||
source_roots = [
|
||||
workspace_root / repository["path"] / "src"
|
||||
for repository in catalog["repositories"]
|
||||
if (workspace_root / repository["path"] / "src").is_dir()
|
||||
]
|
||||
core_root = next(
|
||||
(workspace_root / repository["path"] / "src"
|
||||
for repository in catalog["repositories"]
|
||||
if repository.get("name") == "govoplan-core"),
|
||||
workspace_root / "govoplan-core/src",
|
||||
)
|
||||
if not (core_root / "govoplan_core/core/platform_interfaces.py").is_file():
|
||||
raise ValueError("Inventory requires Core interface sources in the selected workspace")
|
||||
_assert_workspace_imports(workspace_root)
|
||||
sys.path[:0] = [str(path) for path in source_roots]
|
||||
from govoplan_core.core.platform_interfaces import ( # noqa: PLC0415
|
||||
manifest_interface_catalog,
|
||||
)
|
||||
_assert_workspace_imports(workspace_root)
|
||||
|
||||
manifests: list[dict[str, Any]] = []
|
||||
for repository in catalog["repositories"]:
|
||||
@@ -366,7 +428,12 @@ def _extract_manifests(
|
||||
manifest_path.relative_to(source_root).with_suffix("").parts
|
||||
)
|
||||
loaded = importlib.import_module(module_name)
|
||||
source = getattr(loaded, "__file__", None)
|
||||
if not isinstance(source, str) or Path(source).resolve() != manifest_path.resolve():
|
||||
raise ValueError("Manifest import did not resolve to its selected workspace source")
|
||||
_assert_workspace_imports(workspace_root)
|
||||
manifest = loaded.get_manifest()
|
||||
_assert_workspace_imports(workspace_root)
|
||||
frontend = manifest.frontend
|
||||
manifests.append(
|
||||
{
|
||||
@@ -452,6 +519,26 @@ def _extract_manifests(
|
||||
return sorted(manifests, key=lambda item: item["id"])
|
||||
|
||||
|
||||
def _assert_workspace_imports(workspace_root: Path) -> None:
|
||||
# An editable installation or cached import must not stand in for a missing
|
||||
# optional checkout. Direct callers with another workspace use a fresh
|
||||
# process instead of replacing already-loaded application packages.
|
||||
for name, module in list(sys.modules.items()):
|
||||
# The Meta tools may audit a separate checkout; they are not module
|
||||
# contributions and must not be confused with application packages.
|
||||
package = name.partition(".")[0]
|
||||
if not package.startswith("govoplan_") or package in {
|
||||
"govoplan_devkit", "govoplan_release"
|
||||
}:
|
||||
continue
|
||||
filename = getattr(module, "__file__", None)
|
||||
locations = list(getattr(module, "__path__", ()))
|
||||
if isinstance(filename, str):
|
||||
locations.append(filename)
|
||||
if any(not Path(location).resolve().is_relative_to(workspace_root) for location in locations):
|
||||
raise ValueError("A GovOPlaN import originates outside the selected inventory workspace")
|
||||
|
||||
|
||||
def _assemble_inventory(
|
||||
*,
|
||||
webui: dict[str, Any],
|
||||
|
||||
Reference in New Issue
Block a user