Files
RustRaptor/tools/hd_boss_cleanup_reproduce.py
T

181 lines
8.4 KiB
Python

"""Replay a checked batch of existing boss cleanup recipes without new pixel math.
Each frame is passed to the hash-bound hd_boss_cleanup.run_recipe unchanged.
This dispatcher records actual subruns and binds one outer portable recipe.
It does not approve artwork or register assets.
"""
from __future__ import annotations
import argparse
import hashlib
import importlib
import json
from pathlib import Path
import sys
METHOD = "derived-generated-boss-source-geometry-batch-v1"
def sha(path):
return hashlib.sha256(Path(path).read_bytes()).hexdigest()
def digest(value):
return (type(value) is str and len(value) == 64
and all(c in "0123456789abcdef" for c in value))
def contained(base, value):
if type(value) is not str or not value or "\\" in value:
raise ValueError("expected portable relative path")
path = Path(value)
if path.is_absolute() or any(part in (".", "..") for part in path.parts):
raise ValueError("expected contained relative path")
result = (base / path).resolve()
if not result.is_relative_to(base.resolve()) or not result.is_file():
raise ValueError("missing or escaped input")
return result
def checked_ref(base, value):
if type(value) is not dict or set(value) != {"path", "sha256"} or not digest(value["sha256"]):
raise ValueError("expected path/sha256 binding")
path = contained(base, value["path"])
if sha(path) != value["sha256"]:
raise ValueError("input hash mismatch")
return path
def plan(recipe_path, project):
recipe_path = Path(recipe_path).resolve()
recipe = json.loads(recipe_path.read_bytes())
if (type(recipe) is not dict or set(recipe) != {
"format_version", "kind", "method", "cleanup_tool", "decoder", "frames"}
or type(recipe["format_version"]) is not int or recipe["format_version"] != 1
or recipe["kind"] != "boss_cleanup_reproduce_batch" or recipe["method"] != METHOD):
raise ValueError("unsupported batch recipe")
project = Path(project).resolve()
for field, expected in [("cleanup_tool", "tools/hd_boss_cleanup.py"),
("decoder", "tools/hd_frame_policy.py")]:
if type(recipe[field]) is not dict or recipe[field].get("path") != expected:
raise ValueError("unsupported cleanup implementation")
checked_ref(project, recipe[field])
frames = recipe["frames"]
if type(frames) is not list or not 1 <= len(frames) <= 32:
raise ValueError("expected bounded nonempty frame list")
planned, seen = [], set()
for frame in frames:
if type(frame) is not dict or set(frame) != {"asset", "recipe", "expected_output_sha256"}:
raise ValueError("invalid batch frame")
asset = frame["asset"]
if (type(asset) is not str or not 1 <= len(asset) <= 8
or any(c not in "0123456789abcdef" for c in asset) or asset in seen
or not digest(frame["expected_output_sha256"])):
raise ValueError("invalid or duplicate asset/output binding")
seen.add(asset)
subpath = checked_ref(recipe_path.parent, frame["recipe"])
sub = json.loads(subpath.read_bytes())
if type(sub) is not dict or sub.get("asset") != asset:
raise ValueError("subrecipe asset mismatch")
# Validate all declared input bodies before creating any output directory.
for key in ("source", "palette", "parent_raw"):
value = sub.get(key)
if type(value) is not dict or set(value) != {"path", "sha256"} or not digest(value["sha256"]):
raise ValueError("invalid subrecipe input binding")
p = Path(value["path"])
if not p.is_absolute():
p = subpath.parent / p
if not p.is_file() or sha(p) != value["sha256"]:
raise ValueError("subrecipe input hash mismatch")
planned.append((frame, subpath, sub))
return recipe, planned
def run_batch(recipe_path, output, project):
recipe_path, output, project = Path(recipe_path).resolve(), Path(output), Path(project).resolve()
if output.exists():
raise ValueError("output exists; do not overwrite old outputs")
initial_recipe_sha256 = sha(recipe_path)
recipe, planned = plan(recipe_path, project)
if sha(recipe_path) != initial_recipe_sha256:
raise ValueError("batch recipe changed during planning")
sys.path.insert(0, str(project / "tools"))
cleanup = importlib.import_module("hd_boss_cleanup")
decoder = importlib.import_module("hd_frame_policy")
if (Path(cleanup.__file__).resolve() != project / "tools/hd_boss_cleanup.py"
or Path(decoder.__file__).resolve() != project / "tools/hd_frame_policy.py"
or sha(cleanup.__file__) != recipe["cleanup_tool"]["sha256"]
or sha(decoder.__file__) != recipe["decoder"]["sha256"]):
raise ValueError("loaded implementation differs from bound tools")
output.mkdir(parents=True)
subruns, sources, parents, outputs = [], {}, set(), {}
def verify_pinned_inputs():
if sha(recipe_path) != initial_recipe_sha256:
raise ValueError("batch recipe changed during execution")
checked_ref(project, recipe["cleanup_tool"])
checked_ref(project, recipe["decoder"])
for pinned_frame, pinned_path, pinned_sub in planned:
if sha(pinned_path) != pinned_frame["recipe"]["sha256"]:
raise ValueError("subrecipe changed during execution")
for key in ("source", "palette", "parent_raw"):
value = pinned_sub[key]
path = Path(value["path"])
if not path.is_absolute():
path = pinned_path.parent / path
if sha(path) != value["sha256"]:
raise ValueError("subrecipe input changed during execution")
verify_pinned_inputs()
for frame, subpath, sub in planned:
verify_pinned_inputs()
asset = frame["asset"]
destination = output / asset
report = cleanup.run_recipe(subpath, destination)
png = destination / f"{asset}-derived.png"
actual = sha(png)
if actual != frame["expected_output_sha256"]:
raise ValueError(f"accepted PNG not reproduced: {asset}")
expected_execution = {"status": "success",
"method": "derived-generated-boss-source-geometry-rgb-preserved-v1",
"recipe_sha256": frame["recipe"]["sha256"],
"tool_sha256": recipe["cleanup_tool"]["sha256"],
"source_sha256": {asset: sub["source"]["sha256"]},
"parent_raw_sha256": [sub["parent_raw"]["sha256"]],
"output_sha256": {asset: actual}}
if type(report) is not dict or report.get("execution") != expected_execution:
raise ValueError("subrun execution receipt differs from pinned inputs")
verify_pinned_inputs()
sources[asset] = sub["source"]["sha256"]
parents.add(sub["parent_raw"]["sha256"])
outputs[asset] = actual
provenance = destination / "derived-provenance.json"
subruns.append({"asset": asset, "recipe": frame["recipe"],
"execution": report["execution"],
"provenance": {"path": f"{asset}/derived-provenance.json", "sha256": sha(provenance)}})
verify_pinned_inputs()
result = {"format_version": 1, "kind": "boss_cleanup_actual_batch_execution",
"execution": {"status": "success", "method": METHOD,
"recipe_sha256": initial_recipe_sha256, "tool_sha256": sha(__file__),
"source_sha256": sources, "parent_raw_sha256": sorted(parents),
"output_sha256": outputs},
"actual_dispatcher_calls": 1, "actual_cleanup_subruns": len(subruns),
"subruns": subruns, "cleanup_tool": recipe["cleanup_tool"], "decoder": recipe["decoder"],
"scope": "Bookkeeping replay only; existing cleanup pixel algorithm unchanged; no approval/registration."}
(output / "batch-execution.json").write_text(json.dumps(result, indent=2, sort_keys=True) + "\n")
return result
def main():
parser = argparse.ArgumentParser(description=__doc__)
parser.add_argument("recipe", type=Path)
parser.add_argument("output_new_directory", type=Path)
parser.add_argument("project", type=Path)
args = parser.parse_args()
report = run_batch(args.recipe, args.output_new_directory, args.project)
print(json.dumps(report["execution"], sort_keys=True))
if __name__ == "__main__":
main()