Files
linuxcncWebWASM/scripts/generate_manifest_ownership_remediation.py

274 lines
12 KiB
Python

#!/usr/bin/env python3
"""Generate deterministic ownership assignments for unassigned manifest seeds.
This report is deliberately additive. Manifest v1 has no owner field, so the
assignment is a reviewable workstream proposal and does not mutate the locked
manifest or context. Release eligibility remains blocked until the proposal
is reviewed and encoded in a new, explicitly versioned context.
"""
from __future__ import annotations
import argparse
import hashlib
import json
from pathlib import Path
from typing import Any
CONTEXT = Path("context-lock.json")
SOURCE_INDEX = Path("coverage/source-index.jsonl")
MANIFEST = Path("coverage/manifest.jsonl")
GATE = Path("coverage/release-candidate-gate.report.json")
INTAKE = Path("coverage/release-remediation-intake.report.json")
OUTPUT = Path("coverage/manifest-ownership-remediation.report.json")
OWNER_POLICIES = {
"ws-0-baseline-owner": {
"workstream": "WS-0",
"review_owner": "release-closure-owner",
"basis": "baseline, source inventory, or documentation path",
},
"ws-1-core-runtime-owner": {
"workstream": "WS-1",
"review_owner": "native-wasm-reviewer",
"basis": "core interpreter, task, TP, posemath, motion, or kinematics path",
},
"ws-2-worker-runtime-owner": {
"workstream": "WS-2",
"review_owner": "worker-runtime-reviewer",
"basis": "HAL, IO, virtual-device, or Worker path",
},
"ws-3-workspace-owner": {
"workstream": "WS-3",
"review_owner": "workspace-recovery-reviewer",
"basis": "workspace, journal, migration, or recovery path",
},
"ws-4-python-compat-owner": {
"workstream": "WS-4",
"review_owner": "python-security-reviewer",
"basis": "Python, remap, filter, or VCP compatibility path",
},
"ws-5-frontend-owner": {
"workstream": "WS-5",
"review_owner": "frontend-compatibility-reviewer",
"basis": "frontend or browser workflow path",
},
"ws-6-reference-test-owner": {
"workstream": "WS-6",
"review_owner": "reference-test-reviewer",
"basis": "test or unit-test path",
},
"ws-7-release-owner": {
"workstream": "WS-7",
"review_owner": "release-gate-owner",
"basis": "release-only evidence path",
},
}
def canonical(value: Any) -> str:
return json.dumps(value, ensure_ascii=False, sort_keys=True, separators=(",", ":"))
def digest(value: Any) -> str:
return hashlib.sha256(canonical(value).encode("utf-8")).hexdigest()
def file_digest(path: Path) -> str | None:
if not path.is_file():
return None
handle = hashlib.sha256()
with path.open("rb") as stream:
for block in iter(lambda: stream.read(1024 * 1024), b""):
handle.update(block)
return handle.hexdigest()
def read_json(root: Path, path: Path) -> tuple[dict[str, Any], str | None]:
try:
value = json.loads((root / path).read_text(encoding="utf-8"))
if not isinstance(value, dict):
raise ValueError(f"{path} must contain an object")
return value, None
except (OSError, UnicodeError, json.JSONDecodeError, ValueError) as exc:
return {}, str(exc)
def read_jsonl(root: Path, path: Path) -> tuple[list[dict[str, Any]], str | None]:
try:
rows = []
with (root / path).open(encoding="utf-8") as stream:
for line_number, line in enumerate(stream, 1):
if not line.strip():
continue
value = json.loads(line)
if not isinstance(value, dict):
raise ValueError(f"{path}:{line_number} must contain an object")
rows.append(value)
return rows, None
except (OSError, UnicodeError, json.JSONDecodeError, ValueError) as exc:
return [], str(exc)
def classify(source: str, kind: str) -> tuple[str, str]:
path = source.split(":", 1)[1] if ":" in source else source
if source.startswith(("build:", "doc:")):
return "ws-0-baseline-owner", "build/doc source prefix"
if source.startswith(("test:", "unit_test:")) or kind == "test":
return "ws-6-reference-test-owner", "test source prefix"
if path.startswith("wasm-core/"):
return "ws-1-core-runtime-owner", "wasm-core source prefix"
if path.startswith("web/worker/"):
return "ws-2-worker-runtime-owner", "web/worker source prefix"
if path.startswith("web/workspace/"):
return "ws-3-workspace-owner", "web/workspace source prefix"
if path.startswith("web/"):
return "ws-5-frontend-owner", "web source prefix"
if "/python/" in path or "/remap/" in path or "/vcp/" in path or path.startswith("linuxcnc/src/emc/python/"):
return "ws-4-python-compat-owner", "Python/remap/VCP path"
if (
path.startswith("linuxcnc/src/hal/")
or "/iotask/" in path
or "/io/" in path
or "/motion/" in path
or "/motioncontrol/" in path
):
return "ws-2-worker-runtime-owner", "HAL/IO/motion virtual-device path"
if path.startswith("linuxcnc/src/emc/") or path.startswith("linuxcnc/src/libnml/"):
return "ws-1-core-runtime-owner", "LinuxCNC core path"
if path.startswith("linuxcnc/src/"):
return "ws-1-core-runtime-owner", "LinuxCNC source path"
if path.startswith("linuxcnc/" ):
return "ws-0-baseline-owner", "LinuxCNC non-runtime path"
return "ws-7-release-owner", "unmatched release evidence path"
def artifact(root: Path, path: Path, role: str) -> dict[str, Any]:
full = root / path
sha = file_digest(full)
if sha is None:
return {"path": path.as_posix(), "role": role, "status": "missing"}
return {"path": path.as_posix(), "role": role, "status": "present", "bytes": full.stat().st_size, "sha256": sha}
def generate(root: Path, output: Path = OUTPUT) -> dict[str, Any]:
root = root.resolve()
lock, lock_error = read_json(root, CONTEXT)
gate, gate_error = read_json(root, GATE)
intake, intake_error = read_json(root, INTAKE)
source_rows, source_error = read_jsonl(root, SOURCE_INDEX)
manifest_rows, manifest_error = read_jsonl(root, MANIFEST)
errors = {
path.as_posix(): error
for path, error in (
(CONTEXT, lock_error),
(GATE, gate_error),
(INTAKE, intake_error),
(SOURCE_INDEX, source_error),
(MANIFEST, manifest_error),
)
if error
}
source_by_id = {str(row.get("source_id")): row for row in source_rows}
rows: list[dict[str, Any]] = []
owner_counts: dict[str, int] = {}
all_valid = not errors and len(source_rows) == len(manifest_rows)
for manifest in manifest_rows:
if manifest.get("mapping_status") != "needs_owner":
continue
source = str(manifest.get("source", ""))
owner, rule = classify(source, str(manifest.get("kind", "")))
policy = OWNER_POLICIES[owner]
source_row = source_by_id.get(source)
source_match = bool(source_row) and source_row.get("sha256") == manifest.get("source_sha")
valid = bool(manifest.get("id")) and bool(source_row) and source_match
row = {
"id": manifest.get("id"),
"source": source,
"source_sha": manifest.get("source_sha"),
"kind": manifest.get("kind"),
"owner": owner,
"owner_type": "workstream_role",
"workstream": policy["workstream"],
"review_owner": policy["review_owner"],
"assignment_rule": rule,
"assignment_basis": policy["basis"],
"original_mapping_status": manifest.get("mapping_status"),
"assignment_status": "proposed_blocked_review" if valid else "blocked_invalid_source",
"release_status": "blocked",
"evidence_status": "source_hash_verified" if valid else "blocked",
}
rows.append(row)
owner_counts[owner] = owner_counts.get(owner, 0) + 1
all_valid = all_valid and valid
rows.sort(key=lambda row: str(row["id"]))
expected_ids = sorted(str(row.get("id")) for row in manifest_rows if row.get("mapping_status") == "needs_owner")
observed_ids = sorted(str(row.get("id")) for row in rows)
coverage_ok = expected_ids == observed_ids and len(observed_ids) == len(set(observed_ids))
stop_ok = (
gate.get("release_action") == "STOP_RELEASE"
and gate.get("candidate_eligible") is False
and gate.get("blocked_count") == 6
and intake.get("release_action") == "STOP_RELEASE"
and intake.get("candidate_eligible") is False
)
checks = {
"manifest_source_coverage": {"status": "verified" if coverage_ok else "blocked", "expected": len(expected_ids), "observed": len(observed_ids)},
"source_hashes": {"status": "verified" if all_valid else "blocked", "verified": sum(row["evidence_status"] == "source_hash_verified" for row in rows)},
"deterministic_owner_policy": {"status": "verified" if all(row["owner"] in OWNER_POLICIES for row in rows) else "blocked", "owners": owner_counts},
"context_consistency": {"status": "verified" if lock.get("context_id") and intake.get("context_id") == lock.get("context_id") else "blocked", "context_id": lock.get("context_id")},
"release_stop_preserved": {"status": "verified" if stop_ok else "blocked", "release_action": "STOP_RELEASE" if stop_ok else None},
"manifest_unchanged": {"status": "verified", "reason": "manifest v1 has no owner field; proposal is additive and requires review before encoding"},
}
checks_ok = not errors and all(value["status"] == "verified" for value in checks.values())
report: dict[str, Any] = {
"schema": "manifest-ownership-remediation.v1",
"context_id": lock.get("context_id"),
"task_id": "K8-REL-08",
"status": "complete" if checks_ok else "blocked",
"remediation_status": "blocked_pending_manifest_review",
"release_decision": "blocked_not_release_candidate",
"release_action": "STOP_RELEASE",
"candidate_eligible": False,
"source_baseline": {"linuxcnc_version": lock.get("linuxcnc_version"), "linuxcnc_source_sha": lock.get("linuxcnc_source_sha")},
"inputs": [
artifact(root, CONTEXT, "context_lock"),
artifact(root, SOURCE_INDEX, "source_index"),
artifact(root, MANIFEST, "manifest_seed"),
artifact(root, GATE, "release_candidate_gate"),
artifact(root, INTAKE, "remediation_intake"),
],
"counts": {"manifest_total": len(manifest_rows), "needs_owner": len(rows), "mapped": len(rows), "accepted_by_this_artifact": 0},
"owner_counts": dict(sorted(owner_counts.items())),
"checks": checks,
"assignments": rows,
"review_policy": {
"owner_semantics": "workstream role proposal; not a named human approval",
"required_before_manifest_update": ["feature owner confirmation", "feature_id/test_id/oracle evidence", "new context_id if manifest hash changes"],
"forbidden": ["promote_to_accepted", "delete_unassigned_rows", "reclassify_as_REAL-EXCLUDED", "overwrite_context_lock_in_place"],
},
"errors": errors,
"report_sha256": None,
}
report["report_sha256"] = digest({key: value for key, value in report.items() if key != "report_sha256"})
destination = output if output.is_absolute() else root / output
destination.parent.mkdir(parents=True, exist_ok=True)
destination.write_text(json.dumps(report, ensure_ascii=False, sort_keys=True, indent=2) + "\n", encoding="utf-8")
return report
def main() -> int:
parser = argparse.ArgumentParser(description=__doc__)
parser.add_argument("--repo-root", type=Path, default=Path(__file__).resolve().parents[1])
parser.add_argument("--output", type=Path, default=OUTPUT)
args = parser.parse_args()
report = generate(args.repo_root, args.output)
print("manifest_ownership_remediation=" + report["status"] + " release=" + report["release_action"] + " mapped=" + str(report["counts"]["mapped"]))
return 0
if __name__ == "__main__":
raise SystemExit(main())