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.
454 lines
18 KiB
Python
Executable File
454 lines
18 KiB
Python
Executable File
"""Explicit suite-plan coverage; never execute or guess nested shell/npm flows."""
|
|
|
|
from __future__ import annotations
|
|
|
|
import hashlib
|
|
import os
|
|
from pathlib import Path
|
|
import re
|
|
import shlex
|
|
import stat
|
|
|
|
from .common import digest, redact_argv, reject_symlinks
|
|
from .package_tests import CORE_COMPONENT_SUITES, declared_tests, discovered_sources
|
|
from .workspace import load_project
|
|
|
|
DISPOSITIONS = ("planned", "covered_elsewhere", "excluded", "unsupported")
|
|
MAX_SUITES = 4096
|
|
|
|
|
|
def _read_canonical(path: Path) -> str:
|
|
reject_symlinks(path)
|
|
descriptor = os.open(
|
|
path, os.O_RDONLY | getattr(os, "O_NOFOLLOW", 0) | getattr(os, "O_NONBLOCK", 0)
|
|
)
|
|
with os.fdopen(descriptor, "rb") as handle:
|
|
metadata = os.fstat(handle.fileno())
|
|
if not stat.S_ISREG(metadata.st_mode) or metadata.st_size > 1024 * 1024:
|
|
raise ValueError("Canonical focused gate must be a bounded regular file")
|
|
encoded = handle.read(1024 * 1024 + 1)
|
|
if len(encoded) > 1024 * 1024:
|
|
raise ValueError("Canonical focused gate exceeds its size bound")
|
|
return encoded.decode("utf-8")
|
|
|
|
|
|
def _node_argv(argv: list[str], cwd: Path) -> tuple[str, ...] | None:
|
|
if (
|
|
not argv
|
|
or argv[0] not in {"node", "{node}", "$NODE"}
|
|
and Path(argv[0]).name not in {"node", "nodejs"}
|
|
):
|
|
return None
|
|
offset = 2 if len(argv) > 1 and argv[1] == "--test" else 1
|
|
if len(argv) != offset + 1:
|
|
return None
|
|
target = Path(argv[offset])
|
|
if "$" in str(target):
|
|
return None
|
|
return ("node", *argv[1:offset], str((cwd / target).resolve()))
|
|
|
|
|
|
def canonical_invocations(workspace_root: Path, meta: Path, core: Path) -> dict:
|
|
"""Recognize direct commands in exact registered phase wrappers or legacy top level.
|
|
|
|
Shell is not evaluated. Bodies, conditionals, loops, functions, npm hooks and
|
|
recursive commands do not grant coverage. Unknown cwd invalidates matches.
|
|
The registered marked wrappers are the only function bodies admitted.
|
|
"""
|
|
path = meta / "tools/checks/check-focused.sh"
|
|
try:
|
|
text = _read_canonical(path)
|
|
except (OSError, ValueError):
|
|
return {
|
|
"path": str(path),
|
|
"sha256": None,
|
|
"npm": [],
|
|
"node": [],
|
|
"node_phases": [],
|
|
"notes": [
|
|
"Canonical focused script is unavailable/unreadable; no package coverage inferred."
|
|
],
|
|
}
|
|
values = {
|
|
"WORKSPACE_ROOT": str(workspace_root),
|
|
"META_ROOT": str(meta),
|
|
"ROOT": str(core),
|
|
}
|
|
|
|
def expand(value: str) -> str | None:
|
|
value = re.sub(
|
|
r"\$\{(WORKSPACE_ROOT|META_ROOT|ROOT)\}|\$(WORKSPACE_ROOT|META_ROOT|ROOT)(?![A-Za-z0-9_])",
|
|
lambda match: values[match[1] or match[2]],
|
|
value,
|
|
)
|
|
return None if "$" in value or "`" in value else value
|
|
|
|
result = {
|
|
"path": str(path),
|
|
"sha256": hashlib.sha256(text.encode()).hexdigest(),
|
|
"npm": [],
|
|
"node": [],
|
|
"node_phases": [],
|
|
"notes": [],
|
|
}
|
|
metadata = meta / "tools/checks/focused-phases.json"
|
|
blocks = {None: text}
|
|
if metadata.exists() or metadata.is_symlink() or "# devkit-phase:" in text:
|
|
from .catalog import focused_phases, focused_phase_bodies
|
|
|
|
try:
|
|
phases = focused_phases(meta)
|
|
blocks = focused_phase_bodies(text, phases)
|
|
result["phase_ids"] = [phase["id"] for phase in phases]
|
|
result["metadata_sha256"] = digest(phases)
|
|
except (OSError, ValueError):
|
|
result["notes"].append(
|
|
"Focused phase metadata/marked bodies are invalid or unavailable; no phase coverage inferred."
|
|
)
|
|
return result
|
|
cwd: Path | None = meta
|
|
body_end, nesting = None, 0
|
|
current_phase = object()
|
|
for phase_id, line in (
|
|
(phase_id, line)
|
|
for phase_id, block in blocks.items()
|
|
for line in block.replace("\\\n", " ").splitlines()
|
|
):
|
|
if current_phase != phase_id:
|
|
cwd, body_end, nesting = meta, None, 0
|
|
current_phase = phase_id
|
|
stripped = line.strip()
|
|
if body_end is not None:
|
|
if stripped == body_end:
|
|
body_end = None
|
|
continue
|
|
heredoc = re.search(r"<<-?\s*['\"]?([A-Za-z_][A-Za-z0-9_]*)['\"]?", line)
|
|
if heredoc:
|
|
body_end = heredoc[1]
|
|
continue
|
|
if not stripped or stripped.startswith("#"):
|
|
continue
|
|
if re.match(
|
|
r"(?:if|for|while|until|case|select)\b|(?:function\s+\w+|\w+\s*\(\s*\))",
|
|
stripped,
|
|
):
|
|
nesting += 1
|
|
continue
|
|
if re.match(r"(?:fi|done|esac)\b|^}\s*;?$", stripped):
|
|
nesting = max(0, nesting - 1)
|
|
continue
|
|
if nesting:
|
|
if re.search(r"\bcd\s", stripped):
|
|
cwd = None
|
|
continue
|
|
try:
|
|
parts = shlex.split(line, comments=True)
|
|
except ValueError:
|
|
result["notes"].append(
|
|
"An unparseable shell line grants no suite coverage."
|
|
)
|
|
continue
|
|
if not parts:
|
|
continue
|
|
if parts[0] == "cd":
|
|
destination = expand(parts[1]) if len(parts) == 2 else None
|
|
candidate = (
|
|
(cwd / destination).resolve()
|
|
if destination and cwd
|
|
else Path(destination).resolve()
|
|
if destination and Path(destination).is_absolute()
|
|
else None
|
|
)
|
|
cwd = (
|
|
candidate
|
|
if candidate and candidate.is_relative_to(workspace_root)
|
|
else None
|
|
)
|
|
continue
|
|
if cwd is None:
|
|
continue
|
|
if (
|
|
parts[0] in {"$NPM", "${NPM}", "npm"}
|
|
and len(parts) >= 3
|
|
and parts[1] == "run"
|
|
and re.fullmatch(r"test(?::[A-Za-z0-9_.:-]+)?", parts[2])
|
|
):
|
|
arguments = parts[3:]
|
|
if arguments and (
|
|
arguments[0] != "--"
|
|
or any(
|
|
not re.fullmatch(r"[A-Za-z0-9_.-]+", item) for item in arguments[1:]
|
|
)
|
|
):
|
|
continue
|
|
result["npm"].append(
|
|
{
|
|
"cwd": str(cwd),
|
|
"name": parts[2],
|
|
"args": arguments[1:] if arguments else [],
|
|
"phase": phase_id,
|
|
}
|
|
)
|
|
elif parts[0] in {"$NODE", "${NODE}", "node"}:
|
|
expanded = [expand(part) for part in parts[1:]]
|
|
if all(part is not None for part in expanded):
|
|
command = _node_argv(["node", *expanded], cwd)
|
|
if command:
|
|
result["node"].append(command)
|
|
result["node_phases"].append({"argv": command, "phase": phase_id})
|
|
result["notes"] = list(dict.fromkeys(result["notes"]))[:8]
|
|
return result
|
|
|
|
|
|
def coverage_inventory(
|
|
workspace_root: Path, profile: str, project_file: Path | None, stages: list[dict]
|
|
) -> dict:
|
|
workspace_root = workspace_root.resolve()
|
|
project = load_project(workspace_root, project_file)
|
|
stage_map = {item["id"]: item for item in stages}
|
|
direct = {}
|
|
explicit_npm = {}
|
|
for stage in stages:
|
|
argv, cwd = stage["argv"], Path(stage["cwd"])
|
|
normalized = _node_argv(argv, cwd)
|
|
if normalized:
|
|
direct[normalized] = stage["id"]
|
|
if (
|
|
len(argv) == 3
|
|
and (argv[0] in {"npm", "{npm}"} or Path(argv[0]).name == "npm")
|
|
and argv[1] == "run"
|
|
):
|
|
explicit_npm[(str(cwd.resolve()), argv[2])] = stage["id"]
|
|
paths = {repo.name: repo.path for repo in project.repositories}
|
|
canonical = (
|
|
canonical_invocations(
|
|
workspace_root,
|
|
paths.get("govoplan", workspace_root / "govoplan"),
|
|
paths.get("govoplan-core", workspace_root / "govoplan-core"),
|
|
)
|
|
if project_file is None
|
|
else None
|
|
)
|
|
canonical_stages = {}
|
|
if canonical is not None:
|
|
legacy = stage_map.get("focused-workspace")
|
|
if legacy and legacy["argv"] == ["bash", canonical["path"]]:
|
|
canonical_stages[None] = "focused-workspace"
|
|
for identity in canonical.get("phase_ids", []):
|
|
check = stage_map.get("focused." + identity)
|
|
if check and check["argv"] == [
|
|
"bash",
|
|
canonical["path"],
|
|
"--phase",
|
|
identity,
|
|
]:
|
|
canonical_stages[identity] = check["id"]
|
|
full = bool(canonical_stages)
|
|
invocations = (
|
|
[
|
|
{**item, "covering_stage": canonical_stages[item.get("phase")]}
|
|
for item in canonical["npm"]
|
|
if item.get("phase") in canonical_stages
|
|
]
|
|
if canonical
|
|
else []
|
|
)
|
|
canonical_nodes = (
|
|
{
|
|
tuple(item["argv"]): canonical_stages[item.get("phase")]
|
|
for item in canonical.get("node_phases", [])
|
|
if item.get("phase") in canonical_stages
|
|
}
|
|
if canonical
|
|
else {}
|
|
)
|
|
suites, notes = [], []
|
|
if canonical:
|
|
notes.extend(canonical["notes"])
|
|
core_package = (
|
|
paths.get("govoplan-core", workspace_root / "govoplan-core")
|
|
/ "webui/package.json"
|
|
)
|
|
core_requests = [
|
|
item
|
|
for item in invocations
|
|
if item["cwd"] == str(core_package.parent) and item["name"] == "test:components"
|
|
]
|
|
requested_components = set()
|
|
for item in core_requests:
|
|
requested_components.update(
|
|
CORE_COMPONENT_SUITES
|
|
if not item["args"] or item["args"] == ["all"]
|
|
else item["args"]
|
|
)
|
|
requested_components.intersection_update(CORE_COMPONENT_SUITES)
|
|
|
|
for repo in project.repositories:
|
|
for location in ("package.json", "webui/package.json"):
|
|
package = repo.path / location
|
|
if not package.exists() and not package.is_symlink():
|
|
continue
|
|
entries = declared_tests(repo, package)
|
|
if location == "webui/package.json":
|
|
declared_argv = {
|
|
tuple(item["_argv"]) for item in entries if item["_argv"]
|
|
}
|
|
entries.extend(
|
|
item
|
|
for item in discovered_sources(repo)
|
|
if not item["_argv"] or tuple(item["_argv"]) not in declared_argv
|
|
)
|
|
for item in entries:
|
|
private_argv = item.pop("_argv")
|
|
component = item["component_suite"]
|
|
identity = (str(package.parent), item["name"])
|
|
normalized = (
|
|
_node_argv(private_argv, package.parent) if private_argv else None
|
|
)
|
|
matching = [
|
|
value
|
|
for value in invocations
|
|
if (value["cwd"], value["name"]) == identity
|
|
]
|
|
item.update(disposition="excluded", covering_stage=None)
|
|
if component is not None:
|
|
all_or_selected = (
|
|
component == "all" or component in requested_components
|
|
)
|
|
if "core.component-batch" in stage_map:
|
|
item.update(
|
|
disposition="planned"
|
|
if component == "all"
|
|
else "covered_elsewhere",
|
|
covering_stage="core.component-batch",
|
|
reason="UI explicitly runs the shared component batch; quick never compiles it.",
|
|
)
|
|
elif full and core_requests and all_or_selected:
|
|
item.update(
|
|
disposition="planned"
|
|
if component == "all"
|
|
else "covered_elsewhere",
|
|
covering_stage=core_requests[0]["covering_stage"],
|
|
reason=f"Canonical focused gate explicitly selects {len(requested_components)}/{len(CORE_COMPONENT_SUITES)} component suites; this is not the complete component batch.",
|
|
)
|
|
else:
|
|
item["reason"] = (
|
|
"No component compilation in quick/backend; UI runs all components. Full runs only its explicitly named subset."
|
|
)
|
|
if component == "all":
|
|
selected = (
|
|
list(CORE_COMPONENT_SUITES)
|
|
if "core.component-batch" in stage_map
|
|
else sorted(requested_components)
|
|
)
|
|
item.update(
|
|
covered_components=selected,
|
|
excluded_components=[
|
|
name
|
|
for name in CORE_COMPONENT_SUITES
|
|
if name not in selected
|
|
],
|
|
)
|
|
elif identity in explicit_npm:
|
|
item.update(
|
|
disposition="planned",
|
|
covering_stage=explicit_npm[identity],
|
|
reason="Explicit project check invokes this exact package suite.",
|
|
)
|
|
elif matching:
|
|
if any(not value["args"] for value in matching):
|
|
item.update(
|
|
disposition="planned",
|
|
covering_stage=next(
|
|
value["covering_stage"]
|
|
for value in matching
|
|
if not value["args"]
|
|
),
|
|
reason="The canonical focused script directly invokes this exact package suite.",
|
|
)
|
|
else:
|
|
item["reason"] = (
|
|
"Canonical invocation supplies arguments; complete suite coverage cannot be inferred."
|
|
)
|
|
elif normalized in direct:
|
|
item.update(
|
|
disposition="planned",
|
|
covering_stage=direct[normalized],
|
|
reason="A selected stage runs this exact direct source-test command.",
|
|
)
|
|
elif normalized in canonical_nodes:
|
|
item.update(
|
|
disposition="covered_elsewhere",
|
|
covering_stage=canonical_nodes[normalized],
|
|
reason="Canonical focused script directly runs this suite's exact Node target.",
|
|
)
|
|
elif private_argv is None:
|
|
item["disposition"] = "unsupported"
|
|
elif item["name"] in {
|
|
"test:module-permutations",
|
|
"test:vite-cache-isolation",
|
|
}:
|
|
item["reason"] = (
|
|
"Separate environment/permutation suite; absent from the selected stage plan."
|
|
)
|
|
else:
|
|
item["reason"] = (
|
|
"Not directly present in this profile's stage plan; nested npm scripts/hooks are not inferred."
|
|
)
|
|
suites.append(item)
|
|
if len(suites) > MAX_SUITES:
|
|
raise ValueError("Coverage inventory exceeds its bounded suite count")
|
|
if project_file is not None:
|
|
for check in project.config.get("checks", []):
|
|
identity = check["id"]
|
|
suites.append(
|
|
{
|
|
"repo": ",".join(check.get("repos", [])) or "project",
|
|
"name": identity,
|
|
"kind": "project-check",
|
|
"package_path": None,
|
|
"argv": redact_argv(check["argv"]),
|
|
"command_sha256": digest(check["argv"]),
|
|
"disposition": "planned" if identity in stage_map else "excluded",
|
|
"covering_stage": identity if identity in stage_map else None,
|
|
"reason": "Selected declared check or dependency."
|
|
if identity in stage_map
|
|
else "Declared check is outside this profile/selection.",
|
|
}
|
|
)
|
|
if len(suites) > MAX_SUITES:
|
|
raise ValueError("Coverage inventory exceeds its bounded suite count")
|
|
counts = {
|
|
kind: sum(item["disposition"] == kind for item in suites)
|
|
for kind in DISPOSITIONS
|
|
}
|
|
notes.append(
|
|
"Coverage describes planned suite invocations, not passing tests, per-test coverage, or completed UI review."
|
|
)
|
|
if full:
|
|
notes.append(
|
|
"Full is the canonical focused gate, not every declared package test; recursive commands and npm hooks are deliberately not guessed."
|
|
)
|
|
return {
|
|
"schema_version": 1,
|
|
"profile": profile,
|
|
"scope": "Declared root/webui package test scripts, discovered UI structural checks, and custom project checks across registered repositories.",
|
|
"suite_count": len(suites),
|
|
"stages": list(stage_map),
|
|
"counts": counts,
|
|
"suites": suites,
|
|
"canonical_gate": {
|
|
key: canonical[key]
|
|
for key in ("path", "sha256", "metadata_sha256")
|
|
if key in canonical
|
|
}
|
|
if canonical
|
|
else None,
|
|
"notes": notes,
|
|
"summary": [
|
|
"Suite coverage: "
|
|
+ ", ".join(f"{value} {key}" for key, value in counts.items()),
|
|
*notes,
|
|
],
|
|
}
|