K8: record blocked owner review and release evidence
This commit is contained in:
729
scripts/generate_release_closure.py
Normal file
729
scripts/generate_release_closure.py
Normal file
@@ -0,0 +1,729 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Build deterministic K8 compatibility, license and release evidence reports.
|
||||
|
||||
The reports intentionally distinguish a verified implementation slice from a
|
||||
release-acceptable result. Missing upstream oracles, source metadata, browser
|
||||
coverage and unassigned manifest seeds stay visible blockers; no derived
|
||||
configuration result is promoted to an accepted golden.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import argparse
|
||||
import hashlib
|
||||
import json
|
||||
import re
|
||||
from collections import Counter
|
||||
from fnmatch import fnmatchcase
|
||||
from pathlib import Path
|
||||
from typing import Any, Iterable
|
||||
|
||||
|
||||
CONTEXT = Path("context-lock.json")
|
||||
SOURCE_INDEX = Path("coverage/source-index.jsonl")
|
||||
MANIFEST = Path("coverage/manifest.jsonl")
|
||||
CONFIGURATION = Path("coverage/configuration-regression.report.json")
|
||||
PYTHON_MANIFEST = Path("python-runtime/manifest.json")
|
||||
PYTHON_REPORT = Path("coverage/python-runtime.report.json")
|
||||
DOWNLOAD_INDEX = Path("vendor/downloads/index.jsonl")
|
||||
FULL_PORT_DOC = Path("docs/linuxcnc-web-wasm-full-port-implementation.md")
|
||||
LICENSE_INDEX = Path("coverage/license-source-index.jsonl")
|
||||
IMPLEMENTATION_LICENSE_DECISION = Path("docs/linuxcnc-web-wasm-license-decision.md")
|
||||
|
||||
REPORT_PATHS = (
|
||||
"coverage/configuration-regression.report.json",
|
||||
"coverage/corexy-hal.report.json",
|
||||
"coverage/coupled-kinematics.report.json",
|
||||
"coverage/delta-kinematics.report.json",
|
||||
"coverage/fiveaxis-kinematics.report.json",
|
||||
"coverage/golden-schema.json",
|
||||
"coverage/genhex-kinematics.report.json",
|
||||
"coverage/genser-kinematics.report.json",
|
||||
"coverage/hal-runtime.report.json",
|
||||
"coverage/interpreter-runtime.report.json",
|
||||
"coverage/io-runtime.report.json",
|
||||
"coverage/kinematics-abi-contract.json",
|
||||
"coverage/kinematics-feedback.report.json",
|
||||
"coverage/kinematics-golden-schema.json",
|
||||
"coverage/kinematics-home-limit.report.json",
|
||||
"coverage/kinematics-motion.report.json",
|
||||
"coverage/kinematics-mapping-contract.json",
|
||||
"coverage/kinematics-numeric-policy.json",
|
||||
"coverage/kinematics-oracle.report.json",
|
||||
"coverage/kinematics-registry.report.json",
|
||||
"coverage/kinematics-runtime.report.json",
|
||||
"coverage/kinematics-source-closure.report.json",
|
||||
"coverage/motion-runtime.report.json",
|
||||
"coverage/native-runner.report.json",
|
||||
"coverage/posemath-runtime.report.json",
|
||||
"coverage/robot-kinematics.report.json",
|
||||
"coverage/runtime-mapping.report.json",
|
||||
"coverage/switchkins-kinematics.report.json",
|
||||
"coverage/test-entries.report.json",
|
||||
"coverage/tripod-kinematics.report.json",
|
||||
"coverage/trivkins-integration.report.json",
|
||||
"coverage/trt-kinematics.report.json",
|
||||
"coverage/userkins-kinematics.report.json",
|
||||
"coverage/workspace-recovery.report.json",
|
||||
"coverage/workspace-migration.report.json",
|
||||
"coverage/workspace-rollback.package.json",
|
||||
"coverage/offline-cache.package.json",
|
||||
)
|
||||
|
||||
DOMAIN_REPORTS = {
|
||||
"configuration": "coverage/configuration-regression.report.json",
|
||||
"protocol": "coverage/runtime-mapping.report.json",
|
||||
"posemath": "coverage/posemath-runtime.report.json",
|
||||
"interpreter": "coverage/interpreter-runtime.report.json",
|
||||
"motion": "coverage/motion-runtime.report.json",
|
||||
"kinematics": "coverage/kinematics-runtime.report.json",
|
||||
"hal": "coverage/hal-runtime.report.json",
|
||||
"io": "coverage/io-runtime.report.json",
|
||||
"python": "coverage/python-runtime.report.json",
|
||||
"recovery": "coverage/workspace-recovery.report.json",
|
||||
}
|
||||
|
||||
|
||||
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:
|
||||
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) -> dict[str, Any]:
|
||||
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
|
||||
|
||||
|
||||
def read_jsonl(root: Path, path: Path) -> list[dict[str, Any]]:
|
||||
rows = []
|
||||
for number, line in enumerate((root / path).read_text(encoding="utf-8").splitlines(), 1):
|
||||
if not line.strip():
|
||||
continue
|
||||
value = json.loads(line)
|
||||
if not isinstance(value, dict):
|
||||
raise ValueError(f"{path}:{number} must contain an object")
|
||||
rows.append(value)
|
||||
return rows
|
||||
|
||||
|
||||
LICENSE_SPDX = {
|
||||
"BSD-3-Clause": "BSD-3-Clause",
|
||||
"Boost-1.0": "BSL-1.0",
|
||||
"CC-BY-3.0": "CC-BY-3.0",
|
||||
"GFDL-1.1+": "GFDL-1.1-or-later",
|
||||
"GNU-All-Permissive": "FSFAP",
|
||||
"GPL-2": "GPL-2.0-only",
|
||||
"GPL-2+": "GPL-2.0-or-later",
|
||||
"GPL-3+": "GPL-3.0-or-later",
|
||||
"ISC": "ISC",
|
||||
"LGPL-2": "LGPL-2.0-only",
|
||||
"LGPL-2.1+": "LGPL-2.1-or-later",
|
||||
"MIT": "MIT",
|
||||
}
|
||||
|
||||
|
||||
def parse_copyright_scopes(text: str) -> list[dict[str, Any]]:
|
||||
"""Parse Debian copyright file scopes without interpreting license text."""
|
||||
scopes: list[dict[str, Any]] = []
|
||||
for order, paragraph in enumerate(re.split(r"\n\s*\n", text)):
|
||||
fields: dict[str, list[str]] = {}
|
||||
current: str | None = None
|
||||
for line in paragraph.splitlines():
|
||||
match = re.match(r"^([A-Za-z][A-Za-z-]*):\s*(.*)$", line)
|
||||
if match:
|
||||
current = match.group(1)
|
||||
fields[current] = [match.group(2).strip()] if match.group(2).strip() else []
|
||||
elif current and line.startswith(" "):
|
||||
value = line.strip()
|
||||
if value:
|
||||
fields[current].append(value)
|
||||
patterns = [pattern for value in fields.get("Files", []) for pattern in value.split()]
|
||||
license_id = fields.get("License", [""])[0].strip()
|
||||
if patterns and license_id:
|
||||
scopes.append({
|
||||
"order": order,
|
||||
"patterns": patterns,
|
||||
"license": license_id,
|
||||
"spdx": LICENSE_SPDX.get(license_id),
|
||||
})
|
||||
return scopes
|
||||
|
||||
|
||||
def match_copyright_scope(relative_path: str, scopes: list[dict[str, Any]]) -> dict[str, Any] | None:
|
||||
matches: list[tuple[int, int, str, dict[str, Any]]] = []
|
||||
for scope in scopes:
|
||||
for pattern in scope["patterns"]:
|
||||
if fnmatchcase(relative_path, pattern):
|
||||
specificity = sum(character not in "*?[" for character in pattern)
|
||||
matches.append((specificity, scope["order"], pattern, scope))
|
||||
if not matches:
|
||||
return None
|
||||
_, _, pattern, scope = max(matches, key=lambda item: (item[0], item[1], item[2]))
|
||||
return {"pattern": pattern, **scope}
|
||||
|
||||
|
||||
def write_jsonl(path: Path, rows: Iterable[dict[str, Any]]) -> dict[str, Any]:
|
||||
content = "".join(f"{canonical(row)}\n" for row in rows)
|
||||
path.parent.mkdir(parents=True, exist_ok=True)
|
||||
path.write_text(content, encoding="utf-8")
|
||||
return {"path": path.as_posix(), "bytes": len(content.encode("utf-8")), "sha256": file_digest(path), "status": "present"}
|
||||
|
||||
|
||||
def write_report(path: Path, report: dict[str, Any]) -> dict[str, Any]:
|
||||
report["report_sha256"] = digest({key: value for key, value in report.items() if key != "report_sha256"})
|
||||
path.parent.mkdir(parents=True, exist_ok=True)
|
||||
path.write_text(json.dumps(report, ensure_ascii=False, sort_keys=True, indent=2) + "\n", encoding="utf-8")
|
||||
return report
|
||||
|
||||
|
||||
def artifact(root: Path, path: Path, role: str) -> dict[str, Any]:
|
||||
full = root / path
|
||||
if not full.is_file():
|
||||
return {"path": path.as_posix(), "role": role, "status": "missing"}
|
||||
value: dict[str, Any] = {
|
||||
"path": path.as_posix(),
|
||||
"role": role,
|
||||
"status": "present",
|
||||
"bytes": full.stat().st_size,
|
||||
"sha256": file_digest(full),
|
||||
}
|
||||
if full.suffix == ".json":
|
||||
try:
|
||||
parsed = json.loads(full.read_text(encoding="utf-8"))
|
||||
except (OSError, UnicodeError, json.JSONDecodeError):
|
||||
parsed = None
|
||||
if isinstance(parsed, dict):
|
||||
for key in ("schema", "$id", "status", "implementation_status"):
|
||||
if key in parsed:
|
||||
value[key] = parsed[key]
|
||||
if isinstance(parsed.get("oracle"), dict):
|
||||
value["oracle_status"] = parsed["oracle"].get("status")
|
||||
return value
|
||||
|
||||
|
||||
def report_status(report: dict[str, Any]) -> str:
|
||||
if report.get("implementation_status") == "verified":
|
||||
return "verified"
|
||||
status = report.get("status")
|
||||
return str(status) if status else "unknown"
|
||||
|
||||
|
||||
def nested_report_value(report: dict[str, Any], key: str, nested: str) -> Any:
|
||||
value = report.get(key)
|
||||
return value.get(nested) if isinstance(value, dict) else None
|
||||
|
||||
|
||||
def source_observation(root: Path) -> dict[str, Any]:
|
||||
lock = read_json(root, CONTEXT)
|
||||
observed: dict[str, dict[str, Any]] = {
|
||||
"sim_abi": {"value": lock["schema_versions"].get("sim_abi"), "source": "context-lock.json"},
|
||||
"protocol": {"value": lock["schema_versions"].get("protocol"), "source": "context-lock.json"},
|
||||
"snapshot": {"value": lock["schema_versions"].get("snapshot"), "source": "context-lock.json"},
|
||||
"event": {"value": lock["schema_versions"].get("event"), "source": "context-lock.json"},
|
||||
"workspace": {"value": lock["schema_versions"].get("workspace"), "source": "context-lock.json"},
|
||||
"manifest": {"value": lock["schema_versions"].get("manifest"), "source": "context-lock.json"},
|
||||
}
|
||||
checks = (
|
||||
("protocol", Path("web/worker/sim-session.mjs"), r"PROTOCOL_SCHEMA\s*=\s*1"),
|
||||
("event", Path("web/worker/replay.mjs"), r'event-log\.v1'),
|
||||
("snapshot", Path("web/worker/replay.mjs"), r'snapshot\.v1'),
|
||||
("workspace", Path("web/workspace/recovery.mjs"), r'simulation-recovery\.v1'),
|
||||
)
|
||||
for name, path, pattern in checks:
|
||||
text = (root / path).read_text(encoding="utf-8") if (root / path).is_file() else ""
|
||||
observed[name]["source_observed"] = bool(re.search(pattern, text))
|
||||
observed[name]["source"] = path.as_posix()
|
||||
observed["manifest"]["source_observed"] = (root / MANIFEST).is_file()
|
||||
observed["sim_abi"]["source_observed"] = (root / Path("coverage/kinematics-abi-contract.json")).is_file()
|
||||
for value in observed.values():
|
||||
value["status"] = "verified" if value.get("value") == 1 and value.get("source_observed") else "blocked"
|
||||
return observed
|
||||
|
||||
|
||||
def generate_compatibility(root: Path, out: Path) -> dict[str, Any]:
|
||||
lock = read_json(root, CONTEXT)
|
||||
config = read_json(root, CONFIGURATION)
|
||||
migration = read_json(root, Path("coverage/workspace-migration.report.json"))
|
||||
reports = {name: read_json(root, Path(path)) for name, path in DOMAIN_REPORTS.items() if (root / Path(path)).is_file()}
|
||||
domains = []
|
||||
for name, path in DOMAIN_REPORTS.items():
|
||||
report = reports.get(name, {})
|
||||
domains.append({
|
||||
"domain": name,
|
||||
"artifact": path,
|
||||
"implementation_status": report_status(report),
|
||||
"runtime_status": report.get("status", "missing"),
|
||||
"oracle_status": report.get("oracle", {}).get("status") if isinstance(report.get("oracle"), dict) else None,
|
||||
"test_evidence": report.get("native_tests") or report.get("native_wasm") or report.get("native_and_wasm") or nested_report_value(report, "runtime", "native_wasm"),
|
||||
"status": "blocked" if nested_report_value(report, "oracle", "status") == "blocked" or report.get("status") in {"partial", "blocked"} else report_status(report),
|
||||
})
|
||||
browser = lock.get("browser_matrix", {})
|
||||
browser_ready = browser.get("status") == "verified" and all(browser.get("minimum", {}).values())
|
||||
contract = source_observation(root)
|
||||
migration_ready = migration.get("status") == "passed" and migration.get("implementation_status") == "verified" and migration.get("migration", {}).get("round_trip_equal") is True and migration.get("rollback", {}).get("status") == "passed" and migration.get("offline", {}).get("status") == "passed" and (root / Path("coverage/workspace-rollback.package.json")).is_file() and (root / Path("coverage/offline-cache.package.json")).is_file()
|
||||
hash_checks = []
|
||||
for key, path in (
|
||||
("configuration_regression_report_sha256", CONFIGURATION),
|
||||
("python_runtime_report_sha256", PYTHON_REPORT),
|
||||
("source_index_sha256", SOURCE_INDEX),
|
||||
("manifest_seed_sha256", MANIFEST),
|
||||
):
|
||||
expected = lock.get(key)
|
||||
actual = file_digest(root / path) if (root / path).is_file() else None
|
||||
hash_checks.append({"lock_key": key, "path": path.as_posix(), "expected": expected, "actual": actual, "status": "verified" if expected == actual else "blocked"})
|
||||
report = {
|
||||
"schema": "release-compatibility-matrix.v1",
|
||||
"context_id": lock.get("context_id"),
|
||||
"status": "blocked",
|
||||
"release_decision": "blocked_not_release_candidate",
|
||||
"source_baseline": {
|
||||
"linuxcnc_version": lock.get("linuxcnc_version"),
|
||||
"linuxcnc_source_sha": lock.get("linuxcnc_source_sha"),
|
||||
"linuxcnc_version_sha256": lock.get("linuxcnc_version_sha256"),
|
||||
"context_lock": {"path": CONTEXT.as_posix(), "sha256": file_digest(root / CONTEXT)},
|
||||
},
|
||||
"contracts": {
|
||||
"schema_versions": lock.get("schema_versions", {}),
|
||||
"observed": contract,
|
||||
"status": "verified" if all(value["status"] == "verified" for value in contract.values()) else "blocked",
|
||||
},
|
||||
"hash_checks": hash_checks,
|
||||
"domains": domains,
|
||||
"configuration": {
|
||||
"entries": config.get("counts", {}).get("registered_configuration_entries"),
|
||||
"entrypoints": config.get("counts", {}).get("config_entrypoints"),
|
||||
"startup_preflight": config.get("counts", {}).get("startup_preflight"),
|
||||
"resource_status": config.get("counts", {}).get("resource_status"),
|
||||
"derived_results_are_not_official": config.get("oracle", {}).get("derived_configuration_results_are_not_official"),
|
||||
},
|
||||
"migration_rollback_offline": {
|
||||
"status": "verified" if migration_ready else "blocked",
|
||||
"artifact": artifact(root, Path("coverage/workspace-migration.report.json"), "migration_rollback_offline"),
|
||||
"rollback_package": artifact(root, Path("coverage/workspace-rollback.package.json"), "rollback_package"),
|
||||
"offline_package": artifact(root, Path("coverage/offline-cache.package.json"), "offline_package"),
|
||||
"format_schema": migration.get("format", {}).get("schema"),
|
||||
"supported_versions": migration.get("format", {}).get("supported_versions", []),
|
||||
"round_trip_equal": migration.get("migration", {}).get("round_trip_equal"),
|
||||
"rollback_recovery": migration.get("rollback", {}).get("recovery_status"),
|
||||
"offline_start": migration.get("offline", {}).get("offline_start"),
|
||||
"no_silent_downgrade": migration.get("recovery_endpoint", {}).get("no_silent_downgrade") is True,
|
||||
},
|
||||
"browser_matrix": {
|
||||
"status": "verified" if browser_ready else "blocked",
|
||||
"declared_status": browser.get("status"),
|
||||
"minimum": browser.get("minimum", {}),
|
||||
"reason": "minimum Chromium/Firefox/Safari versions are TBD" if not browser_ready else None,
|
||||
},
|
||||
"oracle": {
|
||||
"status": "blocked",
|
||||
"official_root": "linuxcnc/unit_tests/kinematics",
|
||||
"present": (root / "linuxcnc/unit_tests/kinematics").is_dir(),
|
||||
"reason": "official kinematics and recovery oracle is absent from the locked source",
|
||||
},
|
||||
"real_excluded": [
|
||||
{"id": "hardware_driver", "reason": "physical hardware and device access are outside browser simulation"},
|
||||
{"id": "linux_realtime_kernel", "reason": "hard real-time kernel scheduling is outside WebAssembly"},
|
||||
{"id": "host_device_access", "reason": "host paths, sockets and devices are denied by the Worker runtime"},
|
||||
],
|
||||
"blockers": [
|
||||
"official_oracle_missing",
|
||||
"browser_matrix_unverified",
|
||||
"configuration_resource_gaps",
|
||||
"manifest_seed_unassigned",
|
||||
"license_metadata_not_per_entry",
|
||||
] + ([] if migration_ready else ["migration_and_rollback_pending"]),
|
||||
"inputs": [artifact(root, CONTEXT, "context_lock"), artifact(root, SOURCE_INDEX, "source_index"), artifact(root, MANIFEST, "manifest_seed"), artifact(root, CONFIGURATION, "configuration_regression"), artifact(root, PYTHON_MANIFEST, "python_runtime_manifest"), artifact(root, FULL_PORT_DOC, "implementation_contract"), artifact(root, Path("coverage/workspace-migration.report.json"), "migration_rollback_offline"), artifact(root, Path("coverage/workspace-rollback.package.json"), "rollback_package"), artifact(root, Path("coverage/offline-cache.package.json"), "offline_package")],
|
||||
"report_sha256": None,
|
||||
}
|
||||
return write_report(out, report)
|
||||
|
||||
|
||||
def generate_license(root: Path, out: Path) -> dict[str, Any]:
|
||||
lock = read_json(root, CONTEXT)
|
||||
sources = read_jsonl(root, SOURCE_INDEX)
|
||||
manifest = read_jsonl(root, MANIFEST)
|
||||
test_report = read_json(root, Path("coverage/test-entries.report.json"))
|
||||
test_rows = read_jsonl(root, Path("coverage/test-entries.jsonl"))
|
||||
copyright_path = Path("linuxcnc/debian/copyright")
|
||||
copyright_text = (root / copyright_path).read_text(encoding="utf-8")
|
||||
declared = sorted(set(re.findall(r"^License:\s*(.+)$", copyright_text, re.MULTILINE)))
|
||||
download_rows = read_jsonl(root, DOWNLOAD_INDEX)
|
||||
python_manifest = read_json(root, PYTHON_MANIFEST)
|
||||
artifact_names = {str(row.get("name")) for row in download_rows}
|
||||
python_artifacts = python_manifest.get("artifacts", [])
|
||||
download_valid = all(str(row.get("name")) in artifact_names and row.get("license") for row in python_artifacts)
|
||||
path_counts = Counter(str(row.get("path", "")).split("/", 1)[0] for row in sources)
|
||||
real_rows = [row for row in test_rows if row.get("portability") == "REAL-EXCLUDED"]
|
||||
source_lookup = {str(row.get("source_id")): row for row in sources}
|
||||
copyright_scopes = parse_copyright_scopes(copyright_text)
|
||||
license_entries: list[dict[str, Any]] = []
|
||||
for row in sorted(manifest, key=lambda value: str(value.get("id"))):
|
||||
source_id = str(row.get("source"))
|
||||
source_row = source_lookup.get(source_id)
|
||||
source_path = str(source_row.get("path")) if source_row else ""
|
||||
source_root = source_path.split("/", 1)[0] if source_path else "unknown"
|
||||
source_hash = source_row.get("sha256") if source_row else None
|
||||
manifest_hash = row.get("source_sha")
|
||||
source_hash_status = "verified" if source_hash and manifest_hash == source_hash else "blocked"
|
||||
evidence = ["coverage/manifest.jsonl", "coverage/source-index.jsonl"]
|
||||
owner = None
|
||||
owner_source = None
|
||||
license_scope = None
|
||||
license_declared = None
|
||||
license_spdx = None
|
||||
license_status = "blocked_unassigned"
|
||||
closure_status = "blocked"
|
||||
if source_root == "linuxcnc":
|
||||
owner = "upstream-linuxcnc-license-audit"
|
||||
owner_source = "source_root_policy"
|
||||
relative_path = source_path[len("linuxcnc/"):] if source_path.startswith("linuxcnc/") else source_path
|
||||
scope = match_copyright_scope(relative_path, copyright_scopes)
|
||||
if scope and scope.get("spdx"):
|
||||
license_scope = scope["pattern"]
|
||||
license_declared = scope["license"]
|
||||
license_spdx = scope["spdx"]
|
||||
license_status = "scoped_declared"
|
||||
closure_status = "verified_scoped"
|
||||
evidence.append("linuxcnc/debian/copyright")
|
||||
else:
|
||||
license_status = "blocked_scope_unmatched"
|
||||
evidence.append("linuxcnc/debian/copyright")
|
||||
elif source_root in {"wasm-core", "web"}:
|
||||
owner = "web-wasm-license-owner"
|
||||
owner_source = "implementation_license_decision"
|
||||
evidence.append(IMPLEMENTATION_LICENSE_DECISION.as_posix())
|
||||
else:
|
||||
owner = "source-provenance-license-owner"
|
||||
owner_source = "source_root_policy"
|
||||
license_status = "blocked_scope_unknown"
|
||||
evidence_status = "complete" if source_hash_status == "verified" and all((root / path).is_file() for path in evidence) else "incomplete"
|
||||
license_entries.append({
|
||||
"id": row.get("id"),
|
||||
"source": source_id,
|
||||
"source_path": source_path or None,
|
||||
"source_root": source_root,
|
||||
"source_kind": row.get("kind"),
|
||||
"source_sha256": source_hash,
|
||||
"source_hash_status": source_hash_status,
|
||||
"owner": owner,
|
||||
"owner_source": owner_source,
|
||||
"license_scope": license_scope,
|
||||
"license_declared": license_declared,
|
||||
"license_spdx": license_spdx,
|
||||
"license_status": license_status,
|
||||
"closure_status": closure_status,
|
||||
"evidence": evidence,
|
||||
"evidence_status": evidence_status,
|
||||
"manifest_mapping_status": row.get("mapping_status"),
|
||||
"manifest_domain": row.get("domain"),
|
||||
})
|
||||
license_index_path = root / LICENSE_INDEX
|
||||
write_jsonl(license_index_path, license_entries)
|
||||
license_index_artifact = artifact(root, LICENSE_INDEX, "license_source_index")
|
||||
license_status_counts = Counter(entry["license_status"] for entry in license_entries)
|
||||
closure_status_counts = Counter(entry["closure_status"] for entry in license_entries)
|
||||
owner_status_counts = Counter("assigned" if entry["owner"] else "unassigned" for entry in license_entries)
|
||||
source_hash_counts = Counter(entry["source_hash_status"] for entry in license_entries)
|
||||
implementation_entries = [entry for entry in license_entries if entry["source_root"] in {"wasm-core", "web"}]
|
||||
report = {
|
||||
"schema": "release-license-closure.v1",
|
||||
"context_id": lock.get("context_id"),
|
||||
"status": "blocked",
|
||||
"release_decision": "license_closure_incomplete",
|
||||
"source_inventory": {
|
||||
"entries": len(sources),
|
||||
"manifest_entries": len(manifest),
|
||||
"path_roots": dict(sorted(path_counts.items())),
|
||||
"license_field_in_manifest": any("license" in row for row in manifest),
|
||||
"manifest_license_field_missing_entries": len(manifest),
|
||||
"unassigned_license_entries": license_status_counts.get("blocked_unassigned", 0),
|
||||
"status": "partial",
|
||||
"reason": "manifest v1 has no license field; the external deterministic index below records scoped upstream evidence and keeps unassigned implementation licenses blocked",
|
||||
},
|
||||
"entry_closure": {
|
||||
"index": license_index_artifact,
|
||||
"entries": len(license_entries),
|
||||
"owner_semantics": "license_audit_owner; does not replace manifest feature owner or promote needs_owner entries",
|
||||
"source_hash_status": dict(sorted(source_hash_counts.items())),
|
||||
"owner_status": dict(sorted(owner_status_counts.items())),
|
||||
"license_status": dict(sorted(license_status_counts.items())),
|
||||
"closure_status": dict(sorted(closure_status_counts.items())),
|
||||
"all_entries_have_owner": all(entry["owner"] for entry in license_entries),
|
||||
"all_entries_have_evidence_status": all(entry["evidence_status"] for entry in license_entries),
|
||||
"status": "blocked" if implementation_entries or any(entry["closure_status"] == "blocked" for entry in license_entries) else "verified",
|
||||
},
|
||||
"upstream_declarations": {
|
||||
"files": [artifact(root, Path("linuxcnc/COPYING"), "license_text"), artifact(root, Path("linuxcnc/COPYING.more"), "license_text"), artifact(root, copyright_path, "scoped_license_declarations")],
|
||||
"declared_spdx_or_legacy_ids": declared,
|
||||
"default_scope": "linuxcnc/* (subject to debian/copyright path exceptions)",
|
||||
"matched_entries": len(license_entries) - len(implementation_entries),
|
||||
"status": "verified_scoped",
|
||||
},
|
||||
"implementation_license": {
|
||||
"paths": ["wasm-core/**", "web/**"],
|
||||
"status": "blocked",
|
||||
"decision": "unassigned_and_blocked",
|
||||
"reason": "no repository LICENSE/NOTICE/SPDX metadata assigns a license to the Web/WASM implementation files; upstream LinuxCNC declarations are not inherited",
|
||||
"entries": len(implementation_entries),
|
||||
"decision_artifact": artifact(root, IMPLEMENTATION_LICENSE_DECISION, "implementation_license_decision"),
|
||||
},
|
||||
"download_artifacts": {
|
||||
"status": "verified" if download_valid else "blocked",
|
||||
"index": artifact(root, DOWNLOAD_INDEX, "download_index"),
|
||||
"python_runtime_manifest": artifact(root, PYTHON_MANIFEST, "python_runtime_manifest"),
|
||||
"artifacts": python_artifacts,
|
||||
},
|
||||
"real_excluded": {
|
||||
"test_entry_count": len(real_rows),
|
||||
"test_report_count": test_report.get("portability", {}).get("REAL-EXCLUDED", 0),
|
||||
"test_source_ids": sorted(str(row.get("test_source_id")) for row in real_rows),
|
||||
"runtime_categories": ["hardware_driver", "linux_realtime_kernel", "host_device_access"],
|
||||
"status": "explicit",
|
||||
},
|
||||
"blockers": ["wasm_web_license_not_declared"],
|
||||
"report_sha256": None,
|
||||
}
|
||||
return write_report(out, report)
|
||||
|
||||
|
||||
def manifest_evidence(root: Path, rows: Iterable[dict[str, Any]]) -> tuple[list[dict[str, Any]], dict[str, int]]:
|
||||
required = ("source", "inputs", "outputs", "test_id", "oracle")
|
||||
entries = []
|
||||
missing = Counter()
|
||||
for row in rows:
|
||||
if row.get("mapping_status") == "needs_owner":
|
||||
continue
|
||||
owner = row.get("domain") if row.get("domain") not in {None, "", "unassigned"} else None
|
||||
if owner is None:
|
||||
missing["owner"] += 1
|
||||
entry = {
|
||||
"id": row.get("id"),
|
||||
"source": row.get("source"),
|
||||
"owner": owner,
|
||||
"owner_source": "manifest.domain",
|
||||
"feature_id": row.get("feature_id"),
|
||||
"inputs": row.get("inputs", []),
|
||||
"outputs": row.get("outputs", []),
|
||||
"test_id": row.get("test_id"),
|
||||
"difference": row.get("difference"),
|
||||
"difference_status": "none_recorded" if row.get("difference") is None else "recorded",
|
||||
"oracle_status": row.get("oracle", {}).get("status") if isinstance(row.get("oracle"), dict) else None,
|
||||
"context_id": row.get("context_id"),
|
||||
"mapping_status": row.get("mapping_status"),
|
||||
}
|
||||
for field in required:
|
||||
value = entry["oracle_status"] if field == "oracle" else entry[field]
|
||||
if value is None or value == [] or value == "":
|
||||
missing[field] += 1
|
||||
entries.append(entry)
|
||||
entries.sort(key=lambda item: str(item["id"]))
|
||||
return entries, dict(sorted(missing.items()))
|
||||
|
||||
|
||||
def generate_index(root: Path, out: Path, compatibility: dict[str, Any], license_report: dict[str, Any]) -> dict[str, Any]:
|
||||
lock = read_json(root, CONTEXT)
|
||||
manifest = read_jsonl(root, MANIFEST)
|
||||
mapped, missing = manifest_evidence(root, manifest)
|
||||
artifacts = [artifact(root, Path(path), "coverage_evidence") for path in REPORT_PATHS]
|
||||
artifacts.extend([
|
||||
artifact(root, Path("coverage/source-index.jsonl"), "source_index"),
|
||||
artifact(root, Path("coverage/manifest.jsonl"), "manifest_seed"),
|
||||
artifact(root, Path("python-runtime/manifest.json"), "python_runtime_manifest"),
|
||||
artifact(root, Path("coverage/compatibility-matrix.report.json"), "compatibility_matrix"),
|
||||
artifact(root, Path("coverage/license-closure.report.json"), "license_closure"),
|
||||
artifact(root, LICENSE_INDEX, "license_source_index"),
|
||||
artifact(root, IMPLEMENTATION_LICENSE_DECISION, "implementation_license_decision"),
|
||||
])
|
||||
accepted = sum(1 for row in manifest if row.get("sim_status") == "accepted" or row.get("web_status") == "accepted")
|
||||
unassigned = sum(1 for row in manifest if row.get("mapping_status") == "needs_owner")
|
||||
config = read_json(root, CONFIGURATION)
|
||||
blockers = [
|
||||
{"id": "manifest_seed_unassigned", "count": unassigned, "status": "blocked"},
|
||||
{"id": "accepted_entries_missing", "count": accepted, "status": "blocked" if accepted == 0 else "verified"},
|
||||
{"id": "official_oracle_missing", "count": 1, "status": "blocked"},
|
||||
{"id": "configuration_resources_missing", "count": config.get("counts", {}).get("resource_missing_rows", 0), "status": "blocked"},
|
||||
{"id": "browser_matrix_unverified", "count": 1, "status": "blocked"},
|
||||
{"id": "license_closure_incomplete", "count": len(license_report.get("blockers", [])), "status": "blocked"},
|
||||
{"id": "migration_rollback_pending", "count": 0 if compatibility.get("migration_rollback_offline", {}).get("status") == "verified" else 1, "status": "verified" if compatibility.get("migration_rollback_offline", {}).get("status") == "verified" else "blocked"},
|
||||
]
|
||||
report = {
|
||||
"schema": "release-evidence-index.v1",
|
||||
"context_id": lock.get("context_id"),
|
||||
"status": "blocked",
|
||||
"release_decision": "blocked_not_release_candidate",
|
||||
"context_lock": {"path": CONTEXT.as_posix(), "sha256": file_digest(root / CONTEXT), "schema_versions": lock.get("schema_versions")},
|
||||
"reports": artifacts,
|
||||
"manifest_summary": {
|
||||
"total": len(manifest),
|
||||
"affected": len(mapped),
|
||||
"needs_owner": unassigned,
|
||||
"accepted": accepted,
|
||||
"required_field_missing": missing,
|
||||
"affected_entries_complete": not missing,
|
||||
"owner_derivation": "manifest.domain; unassigned is never treated as an owner",
|
||||
},
|
||||
"affected_manifest_entries": mapped,
|
||||
"compatibility_report": {"path": "coverage/compatibility-matrix.report.json", "sha256": compatibility["report_sha256"]},
|
||||
"license_report": {"path": "coverage/license-closure.report.json", "sha256": license_report["report_sha256"]},
|
||||
"migration_report": {"path": "coverage/workspace-migration.report.json", "sha256": compatibility.get("migration_rollback_offline", {}).get("artifact", {}).get("sha256")},
|
||||
"real_excluded": license_report.get("real_excluded"),
|
||||
"blockers": blockers,
|
||||
"integrity": {
|
||||
"no_orphans": config.get("closure", {}).get("graph_orphan_entry_ids") == [] and config.get("closure", {}).get("resource_orphan_entrypoints") == [],
|
||||
"no_silent_downgrade": bool(config.get("oracle", {}).get("derived_configuration_results_are_not_official")) and all(row.get("startup_preflight") for row in config.get("configuration_entries", [])),
|
||||
"derived_configuration_not_accepted": bool(config.get("oracle", {}).get("derived_configuration_results_are_not_official")),
|
||||
"all_affected_entries_have_required_evidence": not missing,
|
||||
"migration_rollback_offline_verified": compatibility.get("migration_rollback_offline", {}).get("status") == "verified",
|
||||
"license_source_index_complete": bool(license_report.get("entry_closure", {}).get("all_entries_have_owner")) and bool(license_report.get("entry_closure", {}).get("all_entries_have_evidence_status")),
|
||||
"implementation_license_blocked_explicitly": license_report.get("implementation_license", {}).get("decision") == "unassigned_and_blocked",
|
||||
},
|
||||
"report_sha256": None,
|
||||
}
|
||||
return write_report(out, report)
|
||||
|
||||
|
||||
def generate_release_gate(
|
||||
root: Path,
|
||||
out: Path,
|
||||
aggregate: dict[str, Any],
|
||||
compatibility: dict[str, Any],
|
||||
license_report: dict[str, Any],
|
||||
index: dict[str, Any],
|
||||
) -> dict[str, Any]:
|
||||
lock = read_json(root, CONTEXT)
|
||||
recovery = read_json(root, Path("coverage/workspace-recovery.report.json"))
|
||||
migration = read_json(root, Path("coverage/workspace-migration.report.json"))
|
||||
context_id = lock.get("context_id")
|
||||
context_reports = [aggregate, compatibility, license_report, index, recovery, migration]
|
||||
context_ids_match = all(report.get("context_id") in {None, context_id} for report in context_reports)
|
||||
recovery_hash_expected = lock.get("workspace_recovery_report_sha256")
|
||||
recovery_hash_actual = file_digest(root / Path("coverage/workspace-recovery.report.json"))
|
||||
recovery_hash_locked = recovery_hash_expected == recovery_hash_actual
|
||||
context_consistent = context_ids_match and recovery_hash_locked
|
||||
schema_versions = lock.get("schema_versions", {})
|
||||
schemas_locked = bool(schema_versions) and all(value == 1 for value in schema_versions.values()) and compatibility.get("contracts", {}).get("status") == "verified"
|
||||
evidence_integrity = all(value is True for key, value in aggregate.get("integrity", {}).items() if key not in {"implementation_license_blocked_explicitly"})
|
||||
license_index_complete = license_report.get("entry_closure", {}).get("all_entries_have_owner") is True and license_report.get("entry_closure", {}).get("all_entries_have_evidence_status") is True
|
||||
migration_verified = compatibility.get("migration_rollback_offline", {}).get("status") == "verified" and migration.get("status") == "passed"
|
||||
blockers = [blocker for blocker in aggregate.get("blockers", []) if blocker.get("status") == "blocked"]
|
||||
reasons = {
|
||||
"manifest_seed_unassigned": "manifest seed entries still have no feature owner",
|
||||
"accepted_entries_missing": "no manifest entry is accepted for release",
|
||||
"official_oracle_missing": "locked source has no official kinematics/recovery oracle root",
|
||||
"configuration_resources_missing": "configuration startup/resource gaps remain",
|
||||
"browser_matrix_unverified": "Chromium/Firefox/Safari minimum versions remain TBD",
|
||||
"license_closure_incomplete": "57 Web/WASM entries have no approved SPDX/NOTICE assignment",
|
||||
}
|
||||
hard_blockers = [
|
||||
{
|
||||
**blocker,
|
||||
"reason": reasons.get(blocker.get("id"), "release gate prerequisite is not verified"),
|
||||
}
|
||||
for blocker in blockers
|
||||
]
|
||||
input_paths = (
|
||||
(CONTEXT, "context_lock"),
|
||||
(Path("coverage/release-closure.report.json"), "release_closure"),
|
||||
(Path("coverage/release-evidence-index.json"), "release_evidence_index"),
|
||||
(Path("coverage/compatibility-matrix.report.json"), "compatibility_matrix"),
|
||||
(Path("coverage/license-closure.report.json"), "license_closure"),
|
||||
(LICENSE_INDEX, "license_source_index"),
|
||||
(Path("coverage/workspace-recovery.report.json"), "workspace_recovery"),
|
||||
(Path("coverage/workspace-migration.report.json"), "workspace_migration"),
|
||||
(Path("coverage/workspace-rollback.package.json"), "rollback_package"),
|
||||
(Path("coverage/offline-cache.package.json"), "offline_package"),
|
||||
(IMPLEMENTATION_LICENSE_DECISION, "implementation_license_decision"),
|
||||
)
|
||||
report = {
|
||||
"schema": "release-candidate-gate.v1",
|
||||
"context_id": context_id,
|
||||
"status": "blocked" if hard_blockers else "passed",
|
||||
"release_decision": "blocked_not_release_candidate" if hard_blockers else "release_candidate_eligible",
|
||||
"release_action": "STOP_RELEASE" if hard_blockers else "ALLOW_RELEASE_CANDIDATE",
|
||||
"candidate_eligible": not hard_blockers,
|
||||
"source_baseline": {
|
||||
"linuxcnc_version": lock.get("linuxcnc_version"),
|
||||
"linuxcnc_source_sha": lock.get("linuxcnc_source_sha"),
|
||||
"context_lock": artifact(root, CONTEXT, "context_lock"),
|
||||
},
|
||||
"checks": {
|
||||
"context_consistency": {"status": "verified" if context_consistent else "blocked", "context_ids_match": context_ids_match, "context_id": context_id, "recovery_report_hash_locked": recovery_hash_locked, "recovery_report_hash": recovery_hash_actual},
|
||||
"locked_schema_contracts": {"status": "verified" if schemas_locked else "blocked", "schema_versions": schema_versions, "compatibility_contract_status": compatibility.get("contracts", {}).get("status")},
|
||||
"evidence_integrity": {"status": "verified" if evidence_integrity else "blocked", "all_aggregate_integrity_flags": evidence_integrity},
|
||||
"license_source_index": {"status": "verified" if license_index_complete else "blocked", "entries": license_report.get("entry_closure", {}).get("entries"), "all_entries_have_owner": license_report.get("entry_closure", {}).get("all_entries_have_owner"), "all_entries_have_evidence_status": license_report.get("entry_closure", {}).get("all_entries_have_evidence_status")},
|
||||
"migration_rollback_offline": {"status": "verified" if migration_verified else "blocked", "compatibility_status": compatibility.get("migration_rollback_offline", {}).get("status"), "migration_status": migration.get("status")},
|
||||
"release_prerequisites": {"status": "blocked" if hard_blockers else "verified", "blocked_count": len(hard_blockers), "verified_count": sum(1 for blocker in aggregate.get("blockers", []) if blocker.get("status") == "verified")},
|
||||
},
|
||||
"blockers": hard_blockers,
|
||||
"blocked_count": len(hard_blockers),
|
||||
"rebuild": {
|
||||
"commands": [
|
||||
"python3 scripts/generate_release_closure.py",
|
||||
"node web/worker/test_replay.mjs",
|
||||
"node web/workspace/test_recovery.mjs",
|
||||
"node web/workspace/test_migration.mjs",
|
||||
"python3 -m unittest discover -s scripts -p 'test_*.py' -v",
|
||||
],
|
||||
"deterministic_inputs": [path.as_posix() for path, _ in input_paths],
|
||||
},
|
||||
"failure_policy": {
|
||||
"on_missing_input": "STOP_RELEASE",
|
||||
"on_hash_mismatch": "STOP_RELEASE",
|
||||
"on_blocked_prerequisite": "STOP_RELEASE",
|
||||
"forbidden": ["delete_samples", "lower_checks", "reclassify_as_REAL-EXCLUDED", "promote_blocked_to_accepted"],
|
||||
},
|
||||
"inputs": [artifact(root, path, role) for path, role in input_paths],
|
||||
"report_sha256": None,
|
||||
}
|
||||
return write_report(out, report)
|
||||
|
||||
|
||||
def generate(root: Path, output_dir: Path) -> dict[str, dict[str, Any]]:
|
||||
root = root.resolve()
|
||||
output_dir = output_dir if output_dir.is_absolute() else root / output_dir
|
||||
compatibility = generate_compatibility(root, output_dir / "compatibility-matrix.report.json")
|
||||
license_report = generate_license(root, output_dir / "license-closure.report.json")
|
||||
index = generate_index(root, output_dir / "release-evidence-index.json", compatibility, license_report)
|
||||
aggregate = write_report(output_dir / "release-closure.report.json", {
|
||||
"schema": "release-closure-report.v1",
|
||||
"context_id": compatibility["context_id"],
|
||||
"status": "blocked",
|
||||
"release_decision": "blocked_not_release_candidate",
|
||||
"compatibility": {"path": "coverage/compatibility-matrix.report.json", "sha256": compatibility["report_sha256"]},
|
||||
"license": {"path": "coverage/license-closure.report.json", "sha256": license_report["report_sha256"]},
|
||||
"migration": {"path": "coverage/workspace-migration.report.json", "sha256": compatibility.get("migration_rollback_offline", {}).get("artifact", {}).get("sha256")},
|
||||
"evidence_index": {"path": "coverage/release-evidence-index.json", "sha256": index["report_sha256"]},
|
||||
"blockers": index["blockers"],
|
||||
"integrity": index["integrity"],
|
||||
"report_sha256": None,
|
||||
})
|
||||
gate = generate_release_gate(root, output_dir / "release-candidate-gate.report.json", aggregate, compatibility, license_report, index)
|
||||
return {"compatibility": compatibility, "license": license_report, "index": index, "aggregate": aggregate, "gate": gate}
|
||||
|
||||
|
||||
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-dir", type=Path, default=Path("coverage"))
|
||||
args = parser.parse_args()
|
||||
reports = generate(args.repo_root, args.output_dir)
|
||||
blocked_count = sum(1 for blocker in reports["index"]["blockers"] if blocker.get("status") == "blocked")
|
||||
print("release_closure=" + reports["index"]["status"] + " gate=" + reports["gate"]["status"] + " action=" + reports["gate"]["release_action"] + " blockers=" + str(blocked_count))
|
||||
return 0
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
raise SystemExit(main())
|
||||
Reference in New Issue
Block a user