Implement configuration, runtime, frontend, and task governance closure
This commit is contained in:
@@ -7,6 +7,7 @@ import argparse
|
||||
import hashlib
|
||||
import json
|
||||
import re
|
||||
import subprocess
|
||||
from pathlib import Path
|
||||
from typing import Iterable
|
||||
|
||||
@@ -32,6 +33,7 @@ HAL_COMMANDS = {"source": "hal_source", "loadusr": "hal_user_component"}
|
||||
TOKEN_RE = re.compile(r"(?<![A-Za-z0-9_./~-])([^\s\"'=;,]+)")
|
||||
EXT_RE = re.compile(r"\.(?:ini|hal|tbl|var|ngc|nc|tap|py|xml|ui|glade|tcl|clp|pref|prefs|dat|png|jpg|jpeg|gif|stl|obj)$", re.I)
|
||||
_SHA_CACHE: dict[Path, str] = {}
|
||||
_LOCKED_SUBMODULE_CACHE: dict[Path, set[str] | None] = {}
|
||||
|
||||
|
||||
def sha256(path: Path) -> str:
|
||||
@@ -99,6 +101,44 @@ def config_files(config_root: Path) -> list[Path]:
|
||||
return sorted(path for path in config_root.rglob("*") if path.is_file())
|
||||
|
||||
|
||||
def locked_submodule_files(repo_root: Path) -> set[str] | None:
|
||||
"""Return untracked LinuxCNC paths excluded from the locked inventory.
|
||||
|
||||
Ignored build outputs are intentionally retained because the existing
|
||||
context lock was generated from that checkout. Only explicit untracked
|
||||
source/config fixtures are excluded from the locked inventory.
|
||||
"""
|
||||
submodule = repo_root / "linuxcnc"
|
||||
key = submodule.resolve()
|
||||
if key in _LOCKED_SUBMODULE_CACHE:
|
||||
return _LOCKED_SUBMODULE_CACHE[key]
|
||||
try:
|
||||
completed = subprocess.run(
|
||||
["git", "-C", str(submodule), "status", "--porcelain=v1", "--untracked-files=all"],
|
||||
check=False,
|
||||
capture_output=True,
|
||||
text=True,
|
||||
)
|
||||
if completed.returncode != 0:
|
||||
result = None
|
||||
else:
|
||||
result = {line[3:] for line in completed.stdout.splitlines() if line.startswith("?? ")}
|
||||
except (OSError, UnicodeError):
|
||||
result = None
|
||||
_LOCKED_SUBMODULE_CACHE[key] = result
|
||||
return result
|
||||
|
||||
|
||||
def is_locked_path(repo_root: Path, path: Path, locked: set[str] | None) -> bool:
|
||||
if locked is None:
|
||||
return True
|
||||
try:
|
||||
relative = path.resolve().relative_to((repo_root / "linuxcnc").resolve()).as_posix()
|
||||
except ValueError:
|
||||
return False
|
||||
return relative not in locked
|
||||
|
||||
|
||||
def add_row(rows: list[dict[str, object]], repo_root: Path, config_root: Path, entrypoint: Path,
|
||||
target: Path | None, raw: str, kind: str, edge_kind: str, line: int, status: str | None = None,
|
||||
origin: Path | None = None) -> None:
|
||||
@@ -128,6 +168,7 @@ def add_row(rows: list[dict[str, object]], repo_root: Path, config_root: Path, e
|
||||
|
||||
def scan_entrypoint(repo_root: Path, config_root: Path, entrypoint: Path) -> list[dict[str, object]]:
|
||||
rows: list[dict[str, object]] = []
|
||||
locked = locked_submodule_files(repo_root)
|
||||
ini_rows = parse_ini(entrypoint)
|
||||
row_numbers = {(section, key, value): index for index, (section, key, value) in enumerate(ini_rows, 1)}
|
||||
for section, key, value in ini_rows:
|
||||
@@ -152,7 +193,7 @@ def scan_entrypoint(repo_root: Path, config_root: Path, entrypoint: Path) -> lis
|
||||
add_row(rows, repo_root, config_root, entrypoint, target, token, RESOURCE_EXTENSIONS.get(Path(token).suffix.lower(), "external"), f"ini:{section}:{key}", row_numbers[(section, key, value)], status if target is None else None)
|
||||
|
||||
seen_paths = {str(row["path"]) for row in rows}
|
||||
for path in sorted(path for path in entrypoint.parent.rglob("*") if path.is_file() and path != entrypoint):
|
||||
for path in sorted(path for path in entrypoint.parent.rglob("*") if path.is_file() and path != entrypoint and is_locked_path(repo_root, path, locked)):
|
||||
path_key = rel(repo_root, path)
|
||||
if path.suffix.lower() in RESOURCE_EXTENSIONS and path_key not in seen_paths:
|
||||
add_row(rows, repo_root, config_root, entrypoint, path, rel(repo_root, path), RESOURCE_EXTENSIONS[path.suffix.lower()], "directory_member", 0)
|
||||
@@ -187,7 +228,8 @@ def generate(repo_root: Path, output: Path, report_path: Path) -> tuple[int, str
|
||||
config_root = repo_root / CONFIG_ROOT
|
||||
if not config_root.is_dir():
|
||||
raise FileNotFoundError(f"required config root is missing: {CONFIG_ROOT}")
|
||||
entrypoints = sorted(path for path in config_root.rglob("*.ini") if path.is_file())
|
||||
locked = locked_submodule_files(repo_root)
|
||||
entrypoints = sorted(path for path in config_root.rglob("*.ini") if path.is_file() and is_locked_path(repo_root, path, locked))
|
||||
rows: list[dict[str, object]] = []
|
||||
for entrypoint in entrypoints:
|
||||
rows.extend(scan_entrypoint(repo_root, config_root, entrypoint))
|
||||
|
||||
Reference in New Issue
Block a user