diff --git a/.gitattributes b/.gitattributes index 46f1246e..6e706f66 100644 --- a/.gitattributes +++ b/.gitattributes @@ -98,3 +98,6 @@ crates/vendor/iced_widget/** -text whitespace=cr-at-eol # Preserve exact Process removal review and portable evidence bytes. harness/process-group-removal-evidence-20260925/** -text whitespace=cr-at-eol + +# Preserve exact saved-view planner review and portable evidence bytes. +harness/saved-group-removal-evidence-20260925/** -text whitespace=cr-at-eol diff --git a/harness/saved-group-removal-evidence-20260925/checks/README.md b/harness/saved-group-removal-evidence-20260925/checks/README.md new file mode 100644 index 00000000..8008d46b --- /dev/null +++ b/harness/saved-group-removal-evidence-20260925/checks/README.md @@ -0,0 +1,27 @@ +# Saved aggregate removal: captured checks + +Python 3.11+ offline packaging/verification for explicitly named closed +`saved-removal-*` runs under `CHECKOUT/.local/scenario-group-gates`. +These tools reuse the Scenario removal verifier unchanged except for the +manifest kind and run-name prefix. They do not run Cargo, Git, network requests +or archived code. Input capsules are deduplicated by hash; every original +logical path and failed attempt remains in the manifest. + +```text +python -B prepare.py --root CHECKOUT --runs CLOSED_RUN_NAMES --output NEW_DIRECTORY +python -B NEW_DIRECTORY/verify.py +``` + +Supply the complete explicit attempt inventory, including failures. Existing +output directories, incomplete runs, changed-input runs and missing source +proofs are refused. Verification checks exact outer/raw/nested ZIP inventories +and hashes, safe paths, command/process/time consistency, dynamic input counts, +unchanged captures, executable Cargo-record relationships, source-proof bindings +and pinned comparator provenance. All 71 tracked patched-widget files must be +present. Failed exits remain failures even when archive verification passes. + +An offline proof check does not execute the comparator or read Git objects. +The parent separately compares all captured inputs to the named Git commit. +Executables are omitted; only their pins and Cargo metadata are retained. +Neither package integrity nor compilation establishes native usability, +integration, release acceptance or completion of the application. diff --git a/harness/saved-group-removal-evidence-20260925/checks/evidence.zip b/harness/saved-group-removal-evidence-20260925/checks/evidence.zip new file mode 100644 index 00000000..d9329159 Binary files /dev/null and b/harness/saved-group-removal-evidence-20260925/checks/evidence.zip differ diff --git a/harness/saved-group-removal-evidence-20260925/checks/files.json b/harness/saved-group-removal-evidence-20260925/checks/files.json new file mode 100644 index 00000000..b09b7d30 --- /dev/null +++ b/harness/saved-group-removal-evidence-20260925/checks/files.json @@ -0,0 +1,22 @@ +{ + "prepare.py": { + "bytes": 6317, + "sha256": "2355646cad850d3db7df09ea4d497f93689cb0f41e51228e62ab221aa7aa869a" + }, + "verify.py": { + "bytes": 11524, + "sha256": "79a5118ba65274a6a0f738cbf24307c6d20dd87972bea483257ff26f8f003c58" + }, + "README.md": { + "bytes": 1513, + "sha256": "95bfad55702964d7e8173458b101d06d1d82b1a56db60f8b3504195e1ee67328" + }, + "manifest.json": { + "bytes": 9365, + "sha256": "22bef5c097cd51498c02ecc8ed8daeae8a844fb72b44a34fa0825f7c3d15f5ae" + }, + "evidence.zip": { + "bytes": 3316155, + "sha256": "e5fa031725af84cee29ebd9a9337570f4c8de43aa1e9ce708d77ceff28fa8154" + } +} diff --git a/harness/saved-group-removal-evidence-20260925/checks/manifest.json b/harness/saved-group-removal-evidence-20260925/checks/manifest.json new file mode 100644 index 00000000..e8f8bca0 --- /dev/null +++ b/harness/saved-group-removal-evidence-20260925/checks/manifest.json @@ -0,0 +1,203 @@ +{ + "schema": 1, + "kind": "saved-aggregate-removal-cargo-gates", + "runs": { + "saved-removal-tests-1": { + "command": [ + "cargo", + "test", + "-p", + "oa-desktop", + "--message-format=json", + "--", + "diagram::group_removal", + "--test-threads=1" + ], + "head": "7750ecd7510ae39e4170f37e4aa7e101cf392f66", + "exitCode": 0, + "inputCount": 835, + "helper": "helpers/3e4cd38fd3807a4010a5a7102354f9139cab236ca55163c96090e416161e5fd2.py", + "sourceProof": { + "commit": "7750ecd7510ae39e4170f37e4aa7e101cf392f66", + "pin": { + "bytes": 600, + "sha256": "e29fbd8ef01882ce19f2ebeb294b73db9f4290e238d1aef034e6ba9cfd7924c2" + }, + "comparator": "comparators/7805832d5f563ab2d7a50dd3de700792b5d6f459c68f7b1d8bcfcc9800fa0c3a.py" + } + }, + "saved-removal-clippy-1": { + "command": [ + "cargo", + "clippy", + "-p", + "oa-desktop", + "--all-targets", + "--message-format=json", + "--", + "-D", + "warnings" + ], + "head": "7750ecd7510ae39e4170f37e4aa7e101cf392f66", + "exitCode": 0, + "inputCount": 835, + "helper": "helpers/3e4cd38fd3807a4010a5a7102354f9139cab236ca55163c96090e416161e5fd2.py", + "sourceProof": { + "commit": "7750ecd7510ae39e4170f37e4aa7e101cf392f66", + "pin": { + "bytes": 601, + "sha256": "9cc16f94dcf61e218bc72185839409f8b8d7362fdb9c81ca34370e994b3ae2d1" + }, + "comparator": "comparators/7805832d5f563ab2d7a50dd3de700792b5d6f459c68f7b1d8bcfcc9800fa0c3a.py" + } + }, + "saved-removal-fmt-1": { + "command": [ + "cargo", + "fmt", + "--all", + "--", + "--check" + ], + "head": "7750ecd7510ae39e4170f37e4aa7e101cf392f66", + "exitCode": 0, + "inputCount": 835, + "helper": "helpers/3e4cd38fd3807a4010a5a7102354f9139cab236ca55163c96090e416161e5fd2.py", + "sourceProof": { + "commit": "7750ecd7510ae39e4170f37e4aa7e101cf392f66", + "pin": { + "bytes": 598, + "sha256": "d20bae6760d4faa6b53cf7bc5b8f4ce381377e66533e81a919aa5a052e30595d" + }, + "comparator": "comparators/7805832d5f563ab2d7a50dd3de700792b5d6f459c68f7b1d8bcfcc9800fa0c3a.py" + } + } + }, + "captureHelpers": { + "gate.py": "helpers/3e4cd38fd3807a4010a5a7102354f9139cab236ca55163c96090e416161e5fd2.py", + "source-proof.py": "helpers/c86f51d9b8465341dab73d045923ec306b15ebabf7c97b81beac7afe73bd72a1.py" + }, + "logicalFiles": { + "cargo/saved-removal-clippy-1/inputs.json": "capsules/5f2f069fc1bf15f08405c1d4ed9b1cb97f1cb7ec4288b051823f12fba9f287d9.json", + "cargo/saved-removal-clippy-1/inputs.zip": "capsules/784d760955942242770107c37439470b18012c3425a24fa12d0f4a19288664b3.zip", + "cargo/saved-removal-clippy-1/process.json": "cargo/saved-removal-clippy-1/process.json", + "cargo/saved-removal-clippy-1/result.json": "cargo/saved-removal-clippy-1/result.json", + "cargo/saved-removal-clippy-1/source-proof.json": "cargo/saved-removal-clippy-1/source-proof.json", + "cargo/saved-removal-clippy-1/started.json": "cargo/saved-removal-clippy-1/started.json", + "cargo/saved-removal-clippy-1/stderr.log": "cargo/saved-removal-clippy-1/stderr.log", + "cargo/saved-removal-clippy-1/stdout.log": "cargo/saved-removal-clippy-1/stdout.log", + "cargo/saved-removal-fmt-1/inputs.json": "capsules/5f2f069fc1bf15f08405c1d4ed9b1cb97f1cb7ec4288b051823f12fba9f287d9.json", + "cargo/saved-removal-fmt-1/inputs.zip": "capsules/784d760955942242770107c37439470b18012c3425a24fa12d0f4a19288664b3.zip", + "cargo/saved-removal-fmt-1/process.json": "cargo/saved-removal-fmt-1/process.json", + "cargo/saved-removal-fmt-1/result.json": "cargo/saved-removal-fmt-1/result.json", + "cargo/saved-removal-fmt-1/source-proof.json": "cargo/saved-removal-fmt-1/source-proof.json", + "cargo/saved-removal-fmt-1/started.json": "cargo/saved-removal-fmt-1/started.json", + "cargo/saved-removal-fmt-1/stderr.log": "cargo/saved-removal-fmt-1/stderr.log", + "cargo/saved-removal-fmt-1/stdout.log": "cargo/saved-removal-fmt-1/stdout.log", + "cargo/saved-removal-tests-1/inputs.json": "capsules/5f2f069fc1bf15f08405c1d4ed9b1cb97f1cb7ec4288b051823f12fba9f287d9.json", + "cargo/saved-removal-tests-1/inputs.zip": "capsules/784d760955942242770107c37439470b18012c3425a24fa12d0f4a19288664b3.zip", + "cargo/saved-removal-tests-1/process.json": "cargo/saved-removal-tests-1/process.json", + "cargo/saved-removal-tests-1/result.json": "cargo/saved-removal-tests-1/result.json", + "cargo/saved-removal-tests-1/source-proof.json": "cargo/saved-removal-tests-1/source-proof.json", + "cargo/saved-removal-tests-1/started.json": "cargo/saved-removal-tests-1/started.json", + "cargo/saved-removal-tests-1/stderr.log": "cargo/saved-removal-tests-1/stderr.log", + "cargo/saved-removal-tests-1/stdout.log": "cargo/saved-removal-tests-1/stdout.log", + "comparators/7805832d5f563ab2d7a50dd3de700792b5d6f459c68f7b1d8bcfcc9800fa0c3a.py": "comparators/7805832d5f563ab2d7a50dd3de700792b5d6f459c68f7b1d8bcfcc9800fa0c3a.py", + "helpers/3e4cd38fd3807a4010a5a7102354f9139cab236ca55163c96090e416161e5fd2.py": "helpers/3e4cd38fd3807a4010a5a7102354f9139cab236ca55163c96090e416161e5fd2.py", + "helpers/c86f51d9b8465341dab73d045923ec306b15ebabf7c97b81beac7afe73bd72a1.py": "helpers/c86f51d9b8465341dab73d045923ec306b15ebabf7c97b81beac7afe73bd72a1.py" + }, + "members": { + "capsules/5f2f069fc1bf15f08405c1d4ed9b1cb97f1cb7ec4288b051823f12fba9f287d9.json": { + "bytes": 140332, + "sha256": "5f2f069fc1bf15f08405c1d4ed9b1cb97f1cb7ec4288b051823f12fba9f287d9" + }, + "capsules/784d760955942242770107c37439470b18012c3425a24fa12d0f4a19288664b3.zip": { + "bytes": 3328668, + "sha256": "784d760955942242770107c37439470b18012c3425a24fa12d0f4a19288664b3" + }, + "cargo/saved-removal-clippy-1/process.json": { + "bytes": 179, + "sha256": "e48b3ce0f11c2f01aa64c2a13bf2c5660dbb90ded4f42f4fb7ba3282a46dbae1" + }, + "cargo/saved-removal-clippy-1/result.json": { + "bytes": 1958, + "sha256": "253752903386e533febcbb33c3f9e5273e0a4b040c4be576563793ea420bb63f" + }, + "cargo/saved-removal-clippy-1/source-proof.json": { + "bytes": 601, + "sha256": "9cc16f94dcf61e218bc72185839409f8b8d7362fdb9c81ca34370e994b3ae2d1" + }, + "cargo/saved-removal-clippy-1/started.json": { + "bytes": 1563, + "sha256": "48ee5c8c3591314250ac07a066387013e5086cd4f29c9c97a707e1c81b9f8fdb" + }, + "cargo/saved-removal-clippy-1/stderr.log": { + "bytes": 134, + "sha256": "aa99195a7582f495f50056e1db02db3e44240bdb140c98326308259eb0eca959" + }, + "cargo/saved-removal-clippy-1/stdout.log": { + "bytes": 269431, + "sha256": "9a3df08369d0ddfd2caa690aa32e523b91daa7ec38f4708b954a6c5dc4bf0b34" + }, + "cargo/saved-removal-fmt-1/process.json": { + "bytes": 116, + "sha256": "e9e230476da6ef9c42248657be379eb003caecdbef2db8d4733a48000ac60a32" + }, + "cargo/saved-removal-fmt-1/result.json": { + "bytes": 1868, + "sha256": "b7ce3295371118169d997d704e78988821a936fda95a50f7b5171eb8abd68f54" + }, + "cargo/saved-removal-fmt-1/source-proof.json": { + "bytes": 598, + "sha256": "d20bae6760d4faa6b53cf7bc5b8f4ce381377e66533e81a919aa5a052e30595d" + }, + "cargo/saved-removal-fmt-1/started.json": { + "bytes": 1480, + "sha256": "73966031e45aaace8589bae300c9dae6470f5a4f4b7092c0cbd0404ecd5f7e2e" + }, + "cargo/saved-removal-fmt-1/stderr.log": { + "bytes": 0, + "sha256": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855" + }, + "cargo/saved-removal-fmt-1/stdout.log": { + "bytes": 0, + "sha256": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855" + }, + "cargo/saved-removal-tests-1/process.json": { + "bytes": 188, + "sha256": "7a6ba1c96d9aea0ee145bbefd76c1b21f998c04f53e1efa592b7e90c44872c9e" + }, + "cargo/saved-removal-tests-1/result.json": { + "bytes": 4648, + "sha256": "4e6ab4ba207e30991718505743534d24e1b2726f141cf276cd0e0b7a52ac0358" + }, + "cargo/saved-removal-tests-1/source-proof.json": { + "bytes": 600, + "sha256": "e29fbd8ef01882ce19f2ebeb294b73db9f4290e238d1aef034e6ba9cfd7924c2" + }, + "cargo/saved-removal-tests-1/started.json": { + "bytes": 1567, + "sha256": "0e03954bceb84bf91fd319050a96cdcd465e6cd16ec4ec1a1755626c582cf0e3" + }, + "cargo/saved-removal-tests-1/stderr.log": { + "bytes": 284, + "sha256": "f27427a907f6f524a2bbca8639baef6d0bff8bfcfca2a8d5fb3205a844142bde" + }, + "cargo/saved-removal-tests-1/stdout.log": { + "bytes": 295414, + "sha256": "7a39a4b458bdee718918060affcac4c2ad5d96234eca68fac6872fcae166fd52" + }, + "comparators/7805832d5f563ab2d7a50dd3de700792b5d6f459c68f7b1d8bcfcc9800fa0c3a.py": { + "bytes": 10094, + "sha256": "7805832d5f563ab2d7a50dd3de700792b5d6f459c68f7b1d8bcfcc9800fa0c3a" + }, + "helpers/3e4cd38fd3807a4010a5a7102354f9139cab236ca55163c96090e416161e5fd2.py": { + "bytes": 3778, + "sha256": "3e4cd38fd3807a4010a5a7102354f9139cab236ca55163c96090e416161e5fd2" + }, + "helpers/c86f51d9b8465341dab73d045923ec306b15ebabf7c97b81beac7afe73bd72a1.py": { + "bytes": 1396, + "sha256": "c86f51d9b8465341dab73d045923ec306b15ebabf7c97b81beac7afe73bd72a1" + } + } +} diff --git a/harness/saved-group-removal-evidence-20260925/checks/prepare.py b/harness/saved-group-removal-evidence-20260925/checks/prepare.py new file mode 100644 index 00000000..4a68acea --- /dev/null +++ b/harness/saved-group-removal-evidence-20260925/checks/prepare.py @@ -0,0 +1,110 @@ +"""Package explicitly selected closed gate captures without running Cargo.""" +from pathlib import Path +import argparse +import json +import zipfile +from verify import RAW_FILES, pin, parsed, need, run_name, verify_data, verify_package + +HERE = Path(__file__).resolve().parent + + +def collect(root, runs, helpers, comparators=()): + gates = root / ".local/scenario-group-gates" + candidates = [gates / name for name in ("gate.py", "source-proof.py")] + helpers + helper_bytes = [path.read_bytes() for path in candidates] + comparator_paths = [root / ".local/process-source-evidence-tools/verify.py", *comparators] + comparator_bytes = [path.read_bytes() for path in comparator_paths if path.is_file()] + payload, logical, specs = {}, {}, {} + def add(name, data): + stored = ("capsules/" + pin(data)["sha256"] + Path(name).suffix + if name.rsplit("/", 1)[1] in {"inputs.json", "inputs.zip"} else name) + need(stored not in payload or payload[stored] == data, "deduplication collision") + payload[stored] = data + logical[name] = stored + helper_catalog = {} + for name, data in zip(("gate.py", "source-proof.py"), helper_bytes): + helper = "helpers/" + pin(data)["sha256"] + ".py" + add(helper, data) + helper_catalog[name] = helper + for run in runs: + folder = gates / run + need(folder.is_dir(), "run directory missing: " + run) + paths = list(folder.iterdir()) + names = {path.name for path in paths} + need(all(path.is_file() and not path.is_symlink() for path in paths) + and names == RAW_FILES | {"source-proof.json"}, "exact closed raw files and source proof required: " + run) + raw = {path.name: path.read_bytes() for path in paths} + result = parsed(raw["result.json"]) + need("finish" in result and result["inputsUnchanged"] is True, "unfinished/changed run refused: " + run) + matches = [data for data in helper_bytes if pin(data) == result["helper"]] + need(bool(matches), "exact historical helper unavailable for " + run + "; supply --helper PATH") + helper = "helpers/" + result["helper"]["sha256"] + ".py" + add(helper, matches[0]) + for name, data in sorted(raw.items()): + add("cargo/" + run + "/" + name, data) + specs[run] = {key: result[key] for key in ("command", "head", "exitCode", "inputCount")} + specs[run]["helper"] = helper + if "source-proof.json" in raw: + proof = parsed(raw["source-proof.json"]) + matches = [data for data in comparator_bytes if pin(data) == proof["comparator"]] + need(bool(matches), "exact source comparator unavailable for " + run + "; supply --comparator PATH") + comparator = "comparators/" + proof["comparator"]["sha256"] + ".py" + add(comparator, matches[0]) + specs[run]["sourceProof"] = {"commit": proof["commit"], "pin": pin(raw["source-proof.json"]), "comparator": comparator} + manifest = {"schema": 1, "kind": "saved-aggregate-removal-cargo-gates", "runs": specs, + "captureHelpers": helper_catalog, + "logicalFiles": dict(sorted(logical.items())), + "members": {name: pin(data) for name, data in sorted(payload.items())}} + summary = verify_data(manifest, payload.__getitem__) + return payload, manifest, summary + + +def write_json(path, value): + with path.open("xb") as output: + output.write((json.dumps(value, indent=2) + "\n").encode("utf-8")) + + +def main(): + parser = argparse.ArgumentParser(description=__doc__) + parser.add_argument("--root", type=Path, required=True, help="checkout holding .local/scenario-group-gates") + parser.add_argument("--runs", nargs="+", required=True, help="explicit run names, including failures to retain") + parser.add_argument("--helper", type=Path, action="append", default=[], help="additional exact historical helper (repeatable)") + parser.add_argument("--comparator", type=Path, action="append", default=[], help="additional exact historical source comparator (repeatable)") + parser.add_argument("--output", type=Path, help="fresh, nonexistent package directory (required unless --check-only)") + parser.add_argument("--check-only", action="store_true", help="validate raw captures in memory without creating an archive") + args = parser.parse_args() + need(all(run_name(run) for run in args.runs) and len(args.runs) == len(set(args.runs)), "unique safe run names required") + need(args.check_only or args.output is not None, "--output is required unless --check-only") + if not args.check_only: + need(not args.output.exists(), "output must be a fresh directory: " + str(args.output)) + payload, manifest, summary = collect(args.root.resolve(), args.runs, args.helper, args.comparator) + if args.check_only: + print("PASS raw consistency; no package written:", summary) + return + out = args.output.resolve() + out.mkdir(parents=True, exist_ok=False) + for name in ("evidence.zip", "manifest.json", "files.json"): + need(not (out / name).exists(), "existing package output refused: " + name) + for name in ("prepare.py", "verify.py", "README.md"): + data = (HERE / name).read_bytes() + destination = out / name + if destination.exists(): + need(destination.read_bytes() == data, "different existing package tool: " + name) + else: + with destination.open("xb") as output: + output.write(data) + with zipfile.ZipFile(out / "evidence.zip", "x", compression=zipfile.ZIP_DEFLATED, compresslevel=9) as archive: + for name, data in sorted(payload.items()): + info = zipfile.ZipInfo(name, date_time=(2026, 9, 23, 0, 0, 0)) + info.compress_type = zipfile.ZIP_DEFLATED + info.external_attr = 0o100644 << 16 + archive.writestr(info, data) + write_json(out / "manifest.json", manifest) + names = ("prepare.py", "verify.py", "README.md", "manifest.json", "evidence.zip") + write_json(out / "files.json", {name: pin((out / name).read_bytes()) for name in names}) + need(verify_package(out) == summary, "written package differs from validated captures") + print("PASS package readback:", out, summary) + + +if __name__ == "__main__": + main() diff --git a/harness/saved-group-removal-evidence-20260925/checks/verify.py b/harness/saved-group-removal-evidence-20260925/checks/verify.py new file mode 100644 index 00000000..193eeaac --- /dev/null +++ b/harness/saved-group-removal-evidence-20260925/checks/verify.py @@ -0,0 +1,195 @@ +"""Offline consistency checks only; never execute or extract archived inputs.""" +from pathlib import Path, PurePosixPath +from datetime import datetime +import argparse +import hashlib +import io +import json +import math +import re +import stat +import zipfile + +RAW_FILES = {"started.json", "process.json", "result.json", "stdout.log", "stderr.log", "inputs.json", "inputs.zip"} +OUTER_FILES = {"prepare.py", "verify.py", "README.md", "manifest.json", "evidence.zip"} + + +def need(condition, message): + if not condition: + raise ValueError(message) + + +def pin(data): + return {"bytes": len(data), "sha256": hashlib.sha256(data).hexdigest()} + + +def valid_pin(value): + return (isinstance(value, dict) and set(value) == {"bytes", "sha256"} + and type(value["bytes"]) is int and value["bytes"] >= 0 + and isinstance(value["sha256"], str) + and re.fullmatch(r"[0-9a-f]{64}", value["sha256"]) is not None) + + +def safe(name): + return (isinstance(name, str) and bool(name) and "\\" not in name and ":" not in name + and all(ord(char) >= 32 for char in name) + and not PurePosixPath(name).is_absolute() + and all(part not in ("", ".", "..") for part in name.split("/"))) + + +def run_name(name): + return isinstance(name, str) and re.fullmatch(r"saved-removal-[A-Za-z0-9][A-Za-z0-9._-]*", name) is not None + + +def parsed(data): + def pairs(items): + result = {} + for key, value in items: + need(key not in result, "duplicate JSON key: " + key) + result[key] = value + return result + def constant(value): + raise ValueError("nonfinite JSON constant: " + value) + return json.loads(data, object_pairs_hook=pairs, parse_constant=constant) + + +def inventory(archive, expected): + names = archive.namelist() + need(len(names) == len(set(names)) and set(names) == set(expected), "exact ZIP membership") + for entry in archive.infolist(): + need(safe(entry.filename) and not entry.is_dir(), "unsafe ZIP member: " + entry.filename) + need(not stat.S_ISLNK(entry.external_attr >> 16), "ZIP symlink: " + entry.filename) + need(valid_pin(expected[entry.filename]), "invalid member pin: " + entry.filename) + need(entry.file_size == expected[entry.filename]["bytes"], "ZIP recorded size: " + entry.filename) + + +def ordered_times(*values): + times = [datetime.fromisoformat(value) for value in values] + return all(value.utcoffset() is not None for value in times) and times == sorted(times) + + +def verify_data(manifest, read): + """Shared read-only verifier for packaged bytes or prepare's in-memory payload.""" + need(manifest["schema"] == 1 and manifest["kind"] == "saved-aggregate-removal-cargo-gates", "manifest schema") + runs, logical, stored = manifest["runs"], manifest["logicalFiles"], manifest["members"] + helpers = manifest["captureHelpers"] + need(isinstance(helpers, dict) and set(helpers) == {"gate.py", "source-proof.py"}, "capture helper names") + need(all(isinstance(name, str) and re.fullmatch(r"helpers/[0-9a-f]{64}\.py", name) + for name in helpers.values()), "capture helper mapping") + need(isinstance(runs, dict) and bool(runs) and all(run_name(run) for run in runs), "explicit run inventory") + need(all(safe(name) for name in logical) and all(safe(name) for name in stored), "unsafe manifest path") + need(set(logical.values()) == set(stored), "unreferenced or absent stored members") + for name, expected in stored.items(): + need(valid_pin(expected) and pin(read(name)) == expected, "stored member hash: " + name) + expected_logical = {"cargo/" + run + "/" + name for run in runs for name in RAW_FILES} + expected_logical.update(spec["helper"] for spec in runs.values()) + expected_logical.update(helpers.values()) + for run, spec in runs.items(): + need("sourceProof" in spec, run + ": source proof required") + if "sourceProof" in spec: + proof_spec = spec["sourceProof"] + need(set(proof_spec) == {"commit", "pin", "comparator"}, run + ": exact source proof spec") + need(isinstance(proof_spec["commit"], str) and re.fullmatch(r"[0-9a-f]{40}", proof_spec["commit"]), run + ": named full source commit") + need(valid_pin(proof_spec["pin"]), run + ": source proof pin") + need(isinstance(proof_spec["comparator"], str) and re.fullmatch(r"comparators/[0-9a-f]{64}\.py", proof_spec["comparator"]), run + ": comparator path") + expected_logical.update({"cargo/" + run + "/source-proof.json", proof_spec["comparator"]}) + need(set(logical) == expected_logical, "exact logical raw/helper inventory") + for name, destination in logical.items(): + if name.startswith("cargo/") and name.rsplit("/", 1)[1] in {"inputs.json", "inputs.zip"}: + expected = "capsules/" + stored[destination]["sha256"] + PurePosixPath(name).suffix + need(destination == expected, "input capsule mapping: " + name) + else: + need(destination == name, "only input maps/archives may be deduplicated: " + name) + def raw(name): + return read(logical[name]) + for name, helper in helpers.items(): + need(helper == "helpers/" + pin(raw(helper))["sha256"] + ".py", "named helper hash: " + name) + seen_capsules = set() + for run, spec in runs.items(): + prefix = "cargo/" + run + "/" + start = parsed(raw(prefix + "started.json")) + process = parsed(raw(prefix + "process.json")) + result = parsed(raw(prefix + "result.json")) + required_start = {"start", "command", "cwd", "head", "scope", "inputs", "archive", + "inputCount", "environment", "helper", "cargo", "rustc"} + need(required_start <= start.keys(), run + ": incomplete start receipt") + need(all(result.get(key) == value for key, value in start.items()), run + ": start/result relationship") + command = result["command"] + need(isinstance(command, list) and command and command[0] == "cargo" + and all(isinstance(arg, str) for arg in command), run + ": Cargo command") + need(command == process["command"] == spec["command"], run + ": command relationship") + need(result["head"] == spec["head"] and re.fullmatch(r"(?:[0-9a-f]{40}|[0-9a-f]{64})", result["head"]), run + ": HEAD") + need(type(result["exitCode"]) is int and result["exitCode"] == spec["exitCode"], run + ": recorded exit") + need(result["inputsUnchanged"] is True, run + ": changed inputs refused") + need(type(result["seconds"]) in (int, float) and math.isfinite(result["seconds"]) + and result["seconds"] >= 0, run + ": duration") + need(type(process["pid"]) is int and process["pid"] > 0, run + ": process identity") + need(ordered_times(start["start"], process["observed"], result["finish"]), run + ": completed timestamp order") + for field, filename in [("inputs", "inputs.json"), ("archive", "inputs.zip"), ("stdout", "stdout.log"), ("stderr", "stderr.log")]: + need(result[field] == pin(raw(prefix + filename)), run + ": " + field + " pin") + need(valid_pin(result["helper"]) and result["helper"] == pin(raw(spec["helper"])), run + ": helper pin") + need(spec["helper"] == "helpers/" + result["helper"]["sha256"] + ".py", run + ": helper mapping") + inputs = parsed(raw(prefix + "inputs.json")) + need(isinstance(inputs, dict) and all(safe(name) for name in inputs), run + ": safe input inventory") + need(type(result["inputCount"]) is int and len(inputs) == result["inputCount"] == spec["inputCount"], run + ": input counts") + if "sourceProof" in spec: + proof_spec = spec["sourceProof"] + proof_bytes = raw(prefix + "source-proof.json") + proof = parsed(proof_bytes) + need(pin(proof_bytes) == proof_spec["pin"], run + ": source proof pinned bytes") + need(proof["run"] == run and proof["commit"] == proof_spec["commit"], run + ": proof run/commit identity") + need(proof["status"] == "PASS exact captured input inventory and Git blob bytes", run + ": source proof status") + need(proof["inputCount"] == len(inputs) and proof["inputMap"] == result["inputs"], run + ": proof input relationship") + need(proof["result"] == pin(raw(prefix + "result.json")), run + ": proof result relationship") + need(ordered_times(result["finish"], proof["verified"]), run + ": proof time after closed gate") + comparator = proof_spec["comparator"] + need(valid_pin(proof["comparator"]) and pin(raw(comparator)) == proof["comparator"], run + ": comparator bytes") + need(comparator == "comparators/" + proof["comparator"]["sha256"] + ".py", run + ": comparator identity") + need(sum(name.startswith("crates/vendor/iced_widget/") for name in inputs) == 71, run + ": vendor count") + capsule = (logical[prefix + "inputs.json"], logical[prefix + "inputs.zip"]) + if capsule not in seen_capsules: + with zipfile.ZipFile(io.BytesIO(raw(prefix + "inputs.zip"))) as source: + inventory(source, inputs) + for name, expected in inputs.items(): + need(pin(source.read(name)) == expected, run + ": source hash " + name) + seen_capsules.add(capsule) + artifacts = [] + for line in raw(prefix + "stdout.log").decode("utf-8", errors="replace").splitlines(): + try: + value = json.loads(line) + except json.JSONDecodeError: + continue + if isinstance(value, dict) and value.get("reason") == "compiler-artifact" and value.get("executable"): + artifacts.append(value) + need(isinstance(result["artifacts"], list), run + ": artifact records") + need(artifacts == [record["cargo"] for record in result["artifacts"]], run + ": Cargo JSON artifact relationship") + for record in result["artifacts"]: + need(set(record) == {"cargo", "readback"} + and (record["readback"] is None or valid_pin(record["readback"])), run + ": artifact readback pin") + print(run, "recorded exit", result["exitCode"], "inputs", len(inputs), "executable records", len(artifacts)) + return {"runs": len(runs), "recordedFailures": sum(spec["exitCode"] != 0 for spec in runs.values()), + "logicalFiles": len(logical), "storedMembers": len(stored), "sourceCapsulePairs": len(seen_capsules), + "captureHelpers": helpers, "sourceProofs": sum("sourceProof" in spec for spec in runs.values())} + + +def verify_package(root): + files = parsed((root / "files.json").read_bytes()) + need(set(files) == OUTER_FILES, "outer file inventory") + for name, expected in files.items(): + need(valid_pin(expected) and pin((root / name).read_bytes()) == expected, "outer hash: " + name) + manifest = parsed((root / "manifest.json").read_bytes()) + with zipfile.ZipFile(root / "evidence.zip") as archive: + inventory(archive, manifest["members"]) + return verify_data(manifest, archive.read) + + +def main(): + parser = argparse.ArgumentParser(description=__doc__) + parser.add_argument("package", nargs="?", type=Path, default=Path(__file__).resolve().parent) + args = parser.parse_args() + print("PASS offline consistency:", verify_package(args.package)) + print("Historical receipts only; no source/binary execution or native acceptance claim.") + + +if __name__ == "__main__": + main() diff --git a/harness/saved-group-removal-evidence-20260925/package-audit.json b/harness/saved-group-removal-evidence-20260925/package-audit.json new file mode 100644 index 00000000..cc83763e --- /dev/null +++ b/harness/saved-group-removal-evidence-20260925/package-audit.json @@ -0,0 +1,238 @@ +{ + "status": "PASS", + "verified": "2026-09-25T04:30:46.298618+00:00", + "scope": "Independent original logical bytes versus packaged ZIP mapping; exact outer/logical/stored/raw inventories; closed Clippy diagnostics. No Cargo, Git comparator, binaries or native UI executed. Package verify.py separately executed successfully.", + "logicalCount": 27, + "storedCount": 23, + "runs": [ + "saved-removal-clippy-1", + "saved-removal-fmt-1", + "saved-removal-tests-1" + ], + "outerFiles": [ + "README.md", + "evidence.zip", + "files.json", + "manifest.json", + "prepare.py", + "verify.py" + ], + "sourceCommit": "7750ecd7510ae39e4170f37e4aa7e101cf392f66", + "comparisons": [ + { + "logical": "cargo/saved-removal-clippy-1/inputs.json", + "member": "capsules/5f2f069fc1bf15f08405c1d4ed9b1cb97f1cb7ec4288b051823f12fba9f287d9.json", + "bytes": 140332, + "sha256": "5f2f069fc1bf15f08405c1d4ed9b1cb97f1cb7ec4288b051823f12fba9f287d9", + "exactBytes": true + }, + { + "logical": "cargo/saved-removal-clippy-1/inputs.zip", + "member": "capsules/784d760955942242770107c37439470b18012c3425a24fa12d0f4a19288664b3.zip", + "bytes": 3328668, + "sha256": "784d760955942242770107c37439470b18012c3425a24fa12d0f4a19288664b3", + "exactBytes": true + }, + { + "logical": "cargo/saved-removal-clippy-1/process.json", + "member": "cargo/saved-removal-clippy-1/process.json", + "bytes": 179, + "sha256": "e48b3ce0f11c2f01aa64c2a13bf2c5660dbb90ded4f42f4fb7ba3282a46dbae1", + "exactBytes": true + }, + { + "logical": "cargo/saved-removal-clippy-1/result.json", + "member": "cargo/saved-removal-clippy-1/result.json", + "bytes": 1958, + "sha256": "253752903386e533febcbb33c3f9e5273e0a4b040c4be576563793ea420bb63f", + "exactBytes": true + }, + { + "logical": "cargo/saved-removal-clippy-1/source-proof.json", + "member": "cargo/saved-removal-clippy-1/source-proof.json", + "bytes": 601, + "sha256": "9cc16f94dcf61e218bc72185839409f8b8d7362fdb9c81ca34370e994b3ae2d1", + "exactBytes": true + }, + { + "logical": "cargo/saved-removal-clippy-1/started.json", + "member": "cargo/saved-removal-clippy-1/started.json", + "bytes": 1563, + "sha256": "48ee5c8c3591314250ac07a066387013e5086cd4f29c9c97a707e1c81b9f8fdb", + "exactBytes": true + }, + { + "logical": "cargo/saved-removal-clippy-1/stderr.log", + "member": "cargo/saved-removal-clippy-1/stderr.log", + "bytes": 134, + "sha256": "aa99195a7582f495f50056e1db02db3e44240bdb140c98326308259eb0eca959", + "exactBytes": true + }, + { + "logical": "cargo/saved-removal-clippy-1/stdout.log", + "member": "cargo/saved-removal-clippy-1/stdout.log", + "bytes": 269431, + "sha256": "9a3df08369d0ddfd2caa690aa32e523b91daa7ec38f4708b954a6c5dc4bf0b34", + "exactBytes": true + }, + { + "logical": "cargo/saved-removal-fmt-1/inputs.json", + "member": "capsules/5f2f069fc1bf15f08405c1d4ed9b1cb97f1cb7ec4288b051823f12fba9f287d9.json", + "bytes": 140332, + "sha256": "5f2f069fc1bf15f08405c1d4ed9b1cb97f1cb7ec4288b051823f12fba9f287d9", + "exactBytes": true + }, + { + "logical": "cargo/saved-removal-fmt-1/inputs.zip", + "member": "capsules/784d760955942242770107c37439470b18012c3425a24fa12d0f4a19288664b3.zip", + "bytes": 3328668, + "sha256": "784d760955942242770107c37439470b18012c3425a24fa12d0f4a19288664b3", + "exactBytes": true + }, + { + "logical": "cargo/saved-removal-fmt-1/process.json", + "member": "cargo/saved-removal-fmt-1/process.json", + "bytes": 116, + "sha256": "e9e230476da6ef9c42248657be379eb003caecdbef2db8d4733a48000ac60a32", + "exactBytes": true + }, + { + "logical": "cargo/saved-removal-fmt-1/result.json", + "member": "cargo/saved-removal-fmt-1/result.json", + "bytes": 1868, + "sha256": "b7ce3295371118169d997d704e78988821a936fda95a50f7b5171eb8abd68f54", + "exactBytes": true + }, + { + "logical": "cargo/saved-removal-fmt-1/source-proof.json", + "member": "cargo/saved-removal-fmt-1/source-proof.json", + "bytes": 598, + "sha256": "d20bae6760d4faa6b53cf7bc5b8f4ce381377e66533e81a919aa5a052e30595d", + "exactBytes": true + }, + { + "logical": "cargo/saved-removal-fmt-1/started.json", + "member": "cargo/saved-removal-fmt-1/started.json", + "bytes": 1480, + "sha256": "73966031e45aaace8589bae300c9dae6470f5a4f4b7092c0cbd0404ecd5f7e2e", + "exactBytes": true + }, + { + "logical": "cargo/saved-removal-fmt-1/stderr.log", + "member": "cargo/saved-removal-fmt-1/stderr.log", + "bytes": 0, + "sha256": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855", + "exactBytes": true + }, + { + "logical": "cargo/saved-removal-fmt-1/stdout.log", + "member": "cargo/saved-removal-fmt-1/stdout.log", + "bytes": 0, + "sha256": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855", + "exactBytes": true + }, + { + "logical": "cargo/saved-removal-tests-1/inputs.json", + "member": "capsules/5f2f069fc1bf15f08405c1d4ed9b1cb97f1cb7ec4288b051823f12fba9f287d9.json", + "bytes": 140332, + "sha256": "5f2f069fc1bf15f08405c1d4ed9b1cb97f1cb7ec4288b051823f12fba9f287d9", + "exactBytes": true + }, + { + "logical": "cargo/saved-removal-tests-1/inputs.zip", + "member": "capsules/784d760955942242770107c37439470b18012c3425a24fa12d0f4a19288664b3.zip", + "bytes": 3328668, + "sha256": "784d760955942242770107c37439470b18012c3425a24fa12d0f4a19288664b3", + "exactBytes": true + }, + { + "logical": "cargo/saved-removal-tests-1/process.json", + "member": "cargo/saved-removal-tests-1/process.json", + "bytes": 188, + "sha256": "7a6ba1c96d9aea0ee145bbefd76c1b21f998c04f53e1efa592b7e90c44872c9e", + "exactBytes": true + }, + { + "logical": "cargo/saved-removal-tests-1/result.json", + "member": "cargo/saved-removal-tests-1/result.json", + "bytes": 4648, + "sha256": "4e6ab4ba207e30991718505743534d24e1b2726f141cf276cd0e0b7a52ac0358", + "exactBytes": true + }, + { + "logical": "cargo/saved-removal-tests-1/source-proof.json", + "member": "cargo/saved-removal-tests-1/source-proof.json", + "bytes": 600, + "sha256": "e29fbd8ef01882ce19f2ebeb294b73db9f4290e238d1aef034e6ba9cfd7924c2", + "exactBytes": true + }, + { + "logical": "cargo/saved-removal-tests-1/started.json", + "member": "cargo/saved-removal-tests-1/started.json", + "bytes": 1567, + "sha256": "0e03954bceb84bf91fd319050a96cdcd465e6cd16ec4ec1a1755626c582cf0e3", + "exactBytes": true + }, + { + "logical": "cargo/saved-removal-tests-1/stderr.log", + "member": "cargo/saved-removal-tests-1/stderr.log", + "bytes": 284, + "sha256": "f27427a907f6f524a2bbca8639baef6d0bff8bfcfca2a8d5fb3205a844142bde", + "exactBytes": true + }, + { + "logical": "cargo/saved-removal-tests-1/stdout.log", + "member": "cargo/saved-removal-tests-1/stdout.log", + "bytes": 295414, + "sha256": "7a39a4b458bdee718918060affcac4c2ad5d96234eca68fac6872fcae166fd52", + "exactBytes": true + }, + { + "logical": "comparators/7805832d5f563ab2d7a50dd3de700792b5d6f459c68f7b1d8bcfcc9800fa0c3a.py", + "member": "comparators/7805832d5f563ab2d7a50dd3de700792b5d6f459c68f7b1d8bcfcc9800fa0c3a.py", + "bytes": 10094, + "sha256": "7805832d5f563ab2d7a50dd3de700792b5d6f459c68f7b1d8bcfcc9800fa0c3a", + "exactBytes": true + }, + { + "logical": "helpers/3e4cd38fd3807a4010a5a7102354f9139cab236ca55163c96090e416161e5fd2.py", + "member": "helpers/3e4cd38fd3807a4010a5a7102354f9139cab236ca55163c96090e416161e5fd2.py", + "bytes": 3778, + "sha256": "3e4cd38fd3807a4010a5a7102354f9139cab236ca55163c96090e416161e5fd2", + "exactBytes": true + }, + { + "logical": "helpers/c86f51d9b8465341dab73d045923ec306b15ebabf7c97b81beac7afe73bd72a1.py", + "member": "helpers/c86f51d9b8465341dab73d045923ec306b15ebabf7c97b81beac7afe73bd72a1.py", + "bytes": 1396, + "sha256": "c86f51d9b8465341dab73d045923ec306b15ebabf7c97b81beac7afe73bd72a1", + "exactBytes": true + } + ], + "packageFiles": { + "evidence.zip": { + "bytes": 3316155, + "sha256": "e5fa031725af84cee29ebd9a9337570f4c8de43aa1e9ce708d77ceff28fa8154" + }, + "files.json": { + "bytes": 625, + "sha256": "7dcab73f75564a3f1c135ff069351344058efe3d2902a79eb1d74aeda746d4b6" + }, + "manifest.json": { + "bytes": 9365, + "sha256": "22bef5c097cd51498c02ecc8ed8daeae8a844fb72b44a34fa0825f7c3d15f5ae" + }, + "prepare.py": { + "bytes": 6317, + "sha256": "2355646cad850d3db7df09ea4d497f93689cb0f41e51228e62ab221aa7aa869a" + }, + "README.md": { + "bytes": 1513, + "sha256": "95bfad55702964d7e8173458b101d06d1d82b1a56db60f8b3504195e1ee67328" + }, + "verify.py": { + "bytes": 11524, + "sha256": "79a5118ba65274a6a0f738cbf24307c6d20dd87972bea483257ff26f8f003c58" + } + } +} diff --git a/harness/saved-group-removal-evidence-20260925/planner-review.md b/harness/saved-group-removal-evidence-20260925/planner-review.md new file mode 100644 index 00000000..f2a19897 --- /dev/null +++ b/harness/saved-group-removal-evidence-20260925/planner-review.md @@ -0,0 +1,88 @@ +# Saved group removal planner independent review + +Final source rereview: 2026-09-24 21:25 PDT (2026-09-25 04:25 UTC), after parent reported writers frozen, formatting passed, and source committed as 7750ecd7. Commit attribution is parent-provided; reviewer did not run Git. Reviewed filesystem hashes below identify the actual source. + +Status: no unresolved concrete correctness finding in this bounded pure planner. This does not establish runtime acceptance. Reviewer has not executed Cargo or tests. Parent-executed test gate saved-removal-tests-1 has subsequently been independently inspected; see the closed-evidence section below. Planner is test-gated in diagram.rs and has no application wiring. No App/native, source/history, focus/admission, or review UI acceptance is claimed. + +## Scope and basis + +Read AGENTS.md, harness/saved-group-removal-policy-20260925.md and docs/language.rst section 8.1. Reviewed planner and both test modules; traced saved projection creation, exact State row helper, core deletion impact, deferred cascade cleanup, execute_batch, inherited-reference pruning and identity enumeration. Reviewer performed no Git, Gitea, Cargo, GUI, or implementation edits; only this local report was written. + +## Finding fixed during review + +Original planner silently accepted a repeated exact Target because it only inserted semantic IDs into a set. This contradicted original-target uniqueness before normalization. Reported to parent and planner author. Final source explicitly refuses targets[..index].contains(target); tests include repeated Placement rejection. Resolved. + +## Correctness assessment + +- Complete original-target validation precedes cascade reduction. Saved project validation rejects invalid duplicate placements and malformed identities before projection/admission. +- Placement maps through the captured saved diagram and projected exact placement. Port checks projected owner placement and semantic parent. State rows use the existing exact row helper, including current source and actual row visibility; selected ancestors cannot admit invalid members. +- Stored edge mapping accepts a projected edge with an identity in relations or traces. Current saved projector emits relation identities with trace=false and trace identities with trace=true; model uniqueness validation prevents ambiguity across stores. It emits no inferred synthetic edge reusing a stored identity. Therefore the current combined stored-identity check is sufficient for this projector. +- Cascade normalization uses original deletion_impact coverage with deterministic ID tie-breaking. It does not validate or mutate intermediate planning candidates. Deferred conditional exchange-allocation cleanup remains part of core Batch execution. Coverage is never the final impact oracle. +- execute_batch validates the completed transaction after deferred cleanup and inherited-reference pruning. Planner asserts that every selected semantic identity is actually absent and no identity is created. +- Removed identity diff includes elements, relations, diagrams, embedded placements, traces and artifact links through Project::identities. Changed-survivor comparison covers every record map and embedded placements. Routes, filters, anchors and layouts contribute through their diagram/node records. +- Trace deletion retains endpoint counterparts; State rows resolve to their maintained relation, retaining referenced work except when a different selected subject independently removes it. + +## Test source assessment + +The tests independently author expected whole Projects rather than comparing two planner calls as the outcome oracle. Covered source cases include OA/SA ownership and incident-edge overlap, exact ports and owner mixtures, State rows/internal/external transitions, trace/counterpart preservation, cross-view placements, diagram+embedded-placement cascade, artifact cascade, changed surviving semantic relations/elements and presentation records, invalid/hidden/wrong-association/cross-family input, duplicate targets, and invalid duplicate placements. + +Review-requested gaps now have source fixtures: + +- Machine deletion removes its saved State diagram and all embedded placements, including its diagram-owned artifact and a placement in another surviving view. +- Hidden State row is refused while a selected collapsed ancestor would cover its semantic deletion. +- Interface deletion alone fails validation while its union with the selected boundary succeeds. A fixed smallest interface identity forces the invalid-alone deletion first, protecting the no-intermediate-validation requirement. Both click orders are checked against a literal expected Project. +- Conditional tests cover two required endpoint allocations with overlapping deferred cascade, and a direct exchange deletion together with those supports. Reversed IDs and click order are exercised. Module is declared in final source. + +An initial alternative-support fixture was invalid. Independent source inspection confirms why: duplicate_port_allocation forbids duplicate same-endpoint facts; port_allocation_owner requires the function actual performer to equal the component-port owner; simultaneous direct/reverse mappings therefore require equal boundary owners, forbidden by boundary_owner validation. Final policy and tests correctly preserve these invariants and explicitly reject the invalid counterexample. No union-only alternate-support case is claimed. + +## Remaining scope boundary + +The final policy records that deleting exchanges can alter Process occurrences, implicit order and reused caller interpretation even when a caller stored record is unchanged. The pure planner reports changed stored records; derived caller effects require the future App review adapter. Likewise application focus/draft/modal/selection-generation admission, one Document/source transaction, review presentation, Undo/Redo and native workflows remain downstream obligations. The test-gated integration correctly avoids claiming those are shipped. + +## Final reviewed SHA256 values + +| File | SHA256 | +| --- | --- | +| crates/oa-desktop/src/diagram/group_removal.rs | C94C0C6917AE6A726E42CAA44CF312F5D57721ABEFEA169663167CE00F09FD77 | +| crates/oa-desktop/src/diagram/group_removal/tests.rs | 1154DB09D7F9984BFB5ABD8AE35B4AFD9BAB7F971DDBC22070ADAEB434069F7C | +| crates/oa-desktop/src/diagram/group_removal/conditional_tests.rs | 78EEF99FF71352A2DC881D54F2605076BE035BF41547830D4EB10E43028EC910 | +| crates/oa-desktop/src/diagram.rs | 2977885208E03F343E7D5B9A1D0B285EC62F8CF79DE4480C5295CFD054E44429 | +| harness/saved-group-removal-policy-20260925.md | 13E9B78C2AF8411D5F3DABCE266B6EAA7DEC342020AFE12E9A6C1E5B898F015A | +| docs/language.rst | E54EC50AB110A6875C78A44DFD10CB860951B4CEB2D337F381A3FF8317AAA9DB | + +Supporting core/helper source read during review: references.rs B6D2C388815C51469DD73EB62628C77EA6D95D20324C83E184B2B9F896AA8A4A; session.rs 8A54B3F8841EDB894A9D250BE6E95B280CC87A7AF2FE43005D298A717B1CBA65; projection.rs 3521BF3041502F3A3A970D7799894A5AE16C1E8DA754DEEEA5290AC1F8BA11E8; state_canvas.rs 9CCF0682BF1B97DB408662A750D9C29B627EB7DC51CB61056E11CEEAC62BAF88. + + +## Closed parent-executed test evidence inspected + +After the source review, independently read saved-removal-tests-1/result.json, source-proof.json, stdout.log and stderr.log under .local/scenario-group-gates. This is review of parent-executed evidence; reviewer did not invoke Cargo or rerun the test suite. + +- Command: cargo test -p oa-desktop --message-format=json -- diagram::group_removal --test-threads=1. +- Windows stable-x86_64-pc-windows-gnullvm, Rust/Cargo 1.98.1. Closed capture 2026-09-25 04:24:52–04:27:24 UTC; exit 0; inputsUnchanged=true; capture duration 152.272975 seconds. +- Raw stdout lists all 12 planner tests passing: 12 passed, 0 failed, 0 ignored, 1656 filtered, 0.06 seconds. Raw stderr reports successful compilation and test-binary launch without warning/error output. +- Parent source proof states PASS exact captured input inventory and Git blob bytes for commit 7750ecd7510ae39e4170f37e4aa7e101cf392f66, 835 captured inputs. Reviewer inspected this proof but did not independently execute the Git comparator. +- Reviewer independently hashed stdout, stderr, result, input map and input archive and matched their corresponding recorded SHA256 values. Reviewer also compared the actual planner, both test files, diagram.rs and language.rst against the captured inputs.json hashes; all matched. Final source hashes above therefore identify the files present during this test gate. +- stdout SHA256: 7a39a4b458bdee718918060affcac4c2ad5d96234eca68fac6872fcae166fd52. +- stderr SHA256: f27427a907f6f524a2bbca8639baef6d0bff8bfcfca2a8d5fb3205a844142bde. +- result.json SHA256: 4e6ab4ba207e30991718505743534d24e1b2726f141cf276cd0e0b7a52ac0358. +- inputs.json SHA256: 5f2f069fc1bf15f08405c1d4ed9b1cb97f1cb7ec4288b051823f12fba9f287d9. +- inputs.zip SHA256: 784d760955942242770107c37439470b18012c3425a24fa12d0f4a19288664b3. + +Strict Clippy was subsequently closed and independently inspected, as recorded below. Native/App acceptance limitations remain unchanged. + +## Foundation report and format evidence + +Read harness/saved-group-removal-foundation-20260925.md. Its test-only implementation and downstream App/native/source-history boundaries match reviewed source. Suggested one wording refinement: the twelve regressions include refusal-only checks; successful plans, specifically, use full independently authored expected Projects and exact impact sets. No additional substantive claim issue found. + +Read saved-removal-fmt-1/result.json, source-proof.json and both raw logs. Parent-executed cargo fmt --all -- --check closed exit 0 at 2026-09-25 04:28:51 UTC, inputsUnchanged=true, 7.428614 seconds, empty stdout/stderr. Source proof states same 835-input inventory and commit 7750ecd7510ae39e4170f37e4aa7e101cf392f66; input-map and archive hashes match the independently checked test capture. Reviewer did not rerun formatting or source comparator. + + +## Final Clippy and package audit — frozen review + +Independently read closed saved-removal-clippy-1 result, source proof and raw logs. Parent command cargo clippy -p oa-desktop --all-targets --message-format=json -- -D warnings closed exit 0 with unchanged inputs at 2026-09-25 04:29:17 UTC (91.375469 seconds). Source proof identifies the same 835-input commit. Independently parsed raw Cargo JSON: build-finished success=true and no warning/error compiler diagnostics. stderr reports successful checking and completion. + +Executed the published checks/verify.py with bundled Python, without running archived inputs, Cargo, Git comparator, tests, or binaries. Offline verification PASS: 3 runs, 0 recorded failures, 27 logical files, 23 stored members, 1 source capsule pair, 3 source proofs. This establishes package consistency, not historical execution independently reproduced. + +Separately performed an independent byte audit from original local raw captures/helpers/comparator to every logical ZIP mapping. All 27 originals are byte-identical to their packaged members. Exact outer inventory (six files), all three original raw directory inventories (eight files each), logical inventory, and ZIP stored inventory (23 unique members) match. Recorded pins cover all package files. Output: .local/saved-removal-package-audit.json. This audit did not rely on prepare.py for the byte comparison. + +Review is now frozen. No unresolved concrete bounded-planner finding. Parent-executed Windows tests, format, and strict Clippy have independently inspected closed evidence. Application behavior remains unchanged because the planner is cfg(test)-only; no production GUI, source/history, native, release, Linux, or derived-caller review acceptance is claimed. diff --git a/harness/saved-group-removal-evidence-20260925/semantic-review.md b/harness/saved-group-removal-evidence-20260925/semantic-review.md new file mode 100644 index 00000000..bdaeb0e9 --- /dev/null +++ b/harness/saved-group-removal-evidence-20260925/semantic-review.md @@ -0,0 +1,72 @@ +# Saved Architecture / State aggregate model-removal semantic review + +Read-only critique of `afcea12cd96dcc55c42006e65a5ab5a6212c54b7` in `C:/ar-scenario-9e8r6t`, 2026-09-24. Scope: issue 29 saved-view aggregate Delete-from-model, with group Hide retaining its existing placement-only meaning. This is design/source evidence, not test or native acceptance. Initial critique was read-only. Parent subsequently authorized only `diagram/group_removal/conditional_tests.rs`; three independent conditional-support and validity falsifiers were written there. No Cargo/GUI, commits, or Gitea operations were performed. + +## Recommended operation and admission + +Use an explicit **Delete selected from model…** action with one aggregate review; Shift+Delete can invoke the same captured action. Keep unmodified Delete and Hide selected placements as placement-only removal. Never switch unmodified Delete from Hide to model deletion merely because an edge/row/port is added to the selection. This follows the current contract (`harness/multiselect-contract-20260923.md`, Related group actions), avoids surprising meaning changes, and fits the existing singleton distinction. Current architecture interception is `diagram/multiselect.rs:179`; State interception is `behavior/state_canvas.rs:878`; the existing group menu placeholder is `model_menu.rs:512`. + +Admit every original exact visual target against one accepted-model projection and one captured selection before reducing the set: + +| Exact target | Semantic Delete ID | Required evidence | +| --- | --- | --- | +| Placement(id) | Saved diagram node.element | Exact node ID is present in the specified diagram and its final visible projected nodes; element exists. Never delete the placement ID for this action. | +| Port { port, placement } | port | Exact projected port+owner-placement pair exists; stored element is a Port owned by that placement's element. Never promote the port to its owner. | +| Edge(id) | id | Exact projected edge exists AND ID is an actual stored relation or trace. A TraceLink is legitimate: Traceability projection emits trace IDs, not relation IDs. | +| StateRow { placement, relation } | relation | `visible_row_target` confirms the exact row occurrence; stored relation is StateAction or internal StateTransition. Delete the maintained relation, never its referenced activity/function. | +| Whole, AllocationGrouping, Process/Scenario targets | Refuse complete operation | These are not admitted saved-selection action targets at this source. Do not silently discard them or translate navigation highlight identities. | + +`app/multiselect.rs:67-88` is the authoritative existing final-projection visibility mapping. `diagram_usage.rs:112` defines the richer navigation Target union; union membership is not action permission. AllocationGrouping is currently navigation-only (visible() rejects it); a future explicit unallocate action must establish exact selected grouping admission independently. Allocation work placement deletion still means deleting the work model element, including when its visible parent is a derived allocation summary: it does not mean deleting the performer or a manufactured aggregate Allocation relation. + +Saved `projection.rs:397-479` constructs relationship edges from stored relation records, and `:487-520` constructs trace edges from stored trace records. There is no synthetic edge deletion identity in this saved projection path. Reject missing identities defensively. Expanded Process preview/contextual targets have their own adapters and must not be inferred from generic Edge IDs. A stored ContextualLink that actually appears as a saved edge is a maintained relation, not an ephemeral preview identity. + +`behavior/state_canvas.rs:483-594` defines row semantics, including exact-placement validation and current relation source matching. Dedicated State projection has no component/function ports (`oa-core/src/projection.rs:526`); a forged State Port target must therefore fail visibility even when the stored port exists elsewhere. + +## Normalize semantic identities only after full admission + +Preserve all exact visual targets in the captured Snapshot and deduplicate only semantic delete IDs. Current core validation rejects duplicate same-element placements within one diagram (`validation.rs:795`, `duplicate_placement`); never weaken that invariant to demonstrate deduplication. The same element can be placed in different views, and deletion affects all of those placements. Reject repeated exact typed targets before mapping. A review can accurately distinguish selected diagram objects from model subjects; use unique IDs when marking selected impact records. + +Ancestry-only normalization is insufficient. Core deletion also removes incident relations, port-bound functional exchanges, exchange occurrences, port/exchange allocations, trace links, artifact links, diagram-owned placements, and diagrams tied to a deleted State machine or Scenario (`oa-core/src/references.rs:322-423`). Selecting owner+port+edge, state+action-row, or traced object+trace must not send an invalid second Delete after the first cascade removed its ID. `Command::Delete` explicitly errors for missing identities (`session.rs:674-683`). + +Chosen bounded strategy (reviewed in new planner source): validate/admit all targets on the original project; calculate each deduplicated semantic ID's original full `deletion_impact`; repeatedly choose the remaining ID covering the largest number of remaining selected IDs (stable ID tie-break), append one Delete, and subtract covered selected IDs. Execute the collected single Batch against the original project using the actual core pipeline. Never singleton-validate intermediate candidates. Assert every selected semantic ID is absent and all resulting identities existed originally. + +I support this strategy for current pure-delete semantics. Ordinary cascades are monotone reachability through the original dependency structure. Surviving-reference clearing does not add cascade dependencies. The only support-sensitive deletion condition is ExchangeAllocation endpoint-allocation coverage; core explicitly defers that cleanup until batch finalization (`references.rs:430-467`). Consequently that combined loss cannot unexpectedly delete an upcoming selected command before its turn. Original full-impact coverage may safely omit individually covered conditional dependents because a pure-delete batch creates no replacement facts. This argument does not extend to batches with replacement/rewiring commands; revisit normalization if the operation gains those commands. + +Do not treat the union of original singleton impacts as final review impact. Final `execute_batch` performs inheritance pruning and reference/view cleanup, so its actual candidate is authoritative. An attempted stronger alternate-pairing counterexample was falsified by existing validity constraints: `validation.rs:2932` requires different BoundaryExchange participant owners, `ports::actual_performer` resolves one performer per function, and duplicate same-endpoint PortAllocations are forbidden. Therefore two simultaneous complete direct/reverse mappings cannot coexist in a valid model; do not relax validation to make a hypothetical example pass. + +The independent `conditional_tests.rs` valid fixture instead uses two actual performers and selects both endpoint PortAllocations, whose cascades overlap on the same deferred ExchangeAllocation. It compares the complete result to a literal independently edited Project, varies IDs and click order, and preserves unrelated authored route metadata. A second test also selects the visible functional exchange, combining direct and deferred removal of that dependent. A third asserts rejection of the invalid dual-mapping fixture. ExchangeAllocation itself has relation endpoints, so it is not a visible selectable saved edge in this fixture; do not fabricate a selectable Edge target for it. + +## Review actual records and preserve transaction lifetime + +Compute removed IDs as original identities minus final candidate identities. `Project::identities` includes placements, diagrams, artifact links and traces (`model.rs:1787-1804`). Compute changed IDs only for surviving unequal records across elements, relations, diagrams, artifact links and traces; additionally compare surviving embedded diagram nodes. Parent diagram and changed embedded placement are distinct record rows, so describe counts as records rather than semantic elements. Keep removed and changed sets disjoint. Diagram route/filter/anchor changes are represented by changed diagram records because they have no independent model identity. + +Deleted StateAction/internal-transition rows preserve referenced work and states unless those are separately selected or removed by another selected ancestor. Removing a State clears/revises references in surviving Scenario state occurrences rather than claiming all references are removed. The core clears many surviving references and transition trigger/effect lists (`references.rs:487-618`), may leave a “Removed trigger…” replacement marker, and prunes contextual payload values. Review these actual changed records; a list of only deleted objects is materially incomplete. + +Deleting a trace removes only that trace. Deleting one traced counterpart removes its trace links but preserves the opposite-layer counterpart under established core semantics (`references.rs:322`, `:401-408`). Do not promise every opposite-layer object is preserved if the user explicitly selected both counterparts. Suggested disclosure: “Traced counterparts are preserved unless selected or owned by another selected object.” + +Retain the original document-instance, revision, surface, canvas epoch, selection generation/ordered targets/primary, gesture/input lifetime, and modal token. Admit before any source interaction, selection reconciliation, editor cancellation or modal-state mutation; revalidate when confirming. Check raw source/property drafts, active rename/editor/gesture/pending behavior, and action tool mode. Existing `diagram_removal::enabled`, `Selection::matches` and Process/Scenario captured-removal adapters are useful patterns, not justification to omit saved-view gesture checks. Changing primary away and back, selection remove/re-add, model undo/reapply, or active view switch must not revive an old request. + +Commit through exactly one `Document::execute(Batch)`; its source update can still fail after a valid core preview (`document.rs:322-334`). On failure preserve accepted model, source bundle, selection, revision and Undo/Redo history; do not clear the group until successful commit. Cancel preserves the selected set and source. Successful delete reconciles to a surviving safe owner/view and supports one-step Undo/Redo. The review should not promise automatic restoration of transient selection unless implemented and tested. + +## Falsifier cases to prioritize + +1. Invalid same-element duplicate placements refuse as `duplicate_placement`; valid different selected elements with additional placements in other views delete every placement of the selected semantic objects. Group Hide still removes only selected placement records and retains model identities. +2. Selected semantic ancestor+descendant with IDs in both traversal orders; owner+its port+incident functional exchange; State+its action/internal-transition row; element+incident trace. No double-delete failure or silent partial operation. Add an independent valid member to each. +3. Edge-only and mixed node+trace selections in Traceability. Trace-only deletion preserves both counterparts; deletion of one counterpart preserves the unselected other; selection of both deliberately removes both. +4. State action/internal transition rows referencing work used elsewhere. Row-only model deletion preserves referenced work, states, other rows and views; row+state union deletes all actually covered rows once. Folded/reparented/changed-source rows and a forged placement pair refuse before mutation. +5. Exact Port owner mismatch; stale hidden edge; missing semantic ID; AllocationGrouping/navigation focus; Whole and foreign Process/Scenario target mixed with a valid saved placement: every case refuses the complete set, preserving histories and drafts. +6. Allocation work nested visually under a performer (including summary parent) removes work, not performer or fabricated allocation. Removing selected work/ports includes actual allocation-dependent cascades. Select both required port allocations whose conditional cascades overlap on one ExchangeAllocation; add the visible functional exchange as an explicit direct cause and vary ID order. Do not fabricate an invisible ExchangeAllocation edge. +7. Two individually problematic deletions that form a valid union, and a union that violates a remaining invariant: only final candidate semantic validation, all-or-nothing source/document transaction. Coverage planning must never commit or per-singleton-validate. +8. Updated survivors: Scenario lifeline/action/state references, State transition trigger/effect records, process step order, diagram routes/filters/port anchors, artifact links, source layouts. Independent before/after expected records confirm removed and changed review sets; do not just compare planner to its own output. +9. Mixed target group plain Delete remains safe/refuses Hide; explicit Delete-from-model opens aggregate review. Right-click an unselected object must not accidentally delete only that object or substitute it into a captured group. +10. Cancel, stale Begin, stale Confirm, duplicate Confirm, primary ABA, selection ABA, view/document switch, raw source/property draft, inline editor, live drag/pan/route/port edit, source rename and busy I/O. Assert zero source/history/selection changes on rejection; no action through a newer modal token. +11. Successful real source save/reopen and single Undo/Redo recover the entire model/source bundle, including other views and opposite-layer records. Force a source writeback/update failure after valid semantic preview and verify atomic preservation. +12. Native review at ordinary window size with enough removed/changed records for paging/search, long labels, duplicate names and other-view placements. The total count stays aggregate under filtering; every effect is reachable; confirm/cancel remain visible; the action clearly says model deletion rather than Hide. + +Saved deletion can change Process/Scenario definition interpretation indirectly by deleting exchange occurrences or links. Actual record review must disclose those owner changes. A stronger derived-effects/caller review can reuse the Process removal concept, but should not claim complete propagation or semantic stability from unchanged stored caller records. + +## Decision + +Prefer the explicit aggregate model-delete action. Reuse core deletion and one prepared Batch; add no new wire command or independent cascade rules. The principal implementation risks are collapsing visual and semantic identities too early, admitting a valid subset, ancestry-only normalization, excluding legitimate trace edges, understating changed survivors, and expiring or executing requests against the wrong input lifetime. The source supports this direction; runtime acceptance remains for the parent to establish. + +Reviewed follow-up policy: harness/saved-group-removal-policy-20260925.md and docs/language.rst section 8.1 agree with this recommendation, including duplicate-placement validity and atomic source synchronization. One concrete planner mismatch found during review was absent rejection of exact duplicate Target entries; reported to implementer before testing. The planner otherwise matched the inspected admission and impact semantics. Test source is unexecuted pending parent validation. diff --git a/harness/saved-group-removal-foundation-20260925.md b/harness/saved-group-removal-foundation-20260925.md new file mode 100644 index 00000000..02f64e66 --- /dev/null +++ b/harness/saved-group-removal-foundation-20260925.md @@ -0,0 +1,108 @@ +# Saved Architecture and State removal planner foundation + +Branch-specific work for [issue 29](http://217.177.34.108:3000/sorokin.oleg.edu/ARCADIA-Studio/issues/29). +Source checkpoint: `7750ecd7510ae39e4170f37e4aa7e101cf392f66`. +The [operation policy](saved-group-removal-policy-20260925.md) and +`docs/language.rst` section 8.1 define the required behavior. This checkpoint +implements the pure planner under `cfg(test)` only. It does not enable a new +application action or establish native, integrated, or release acceptance. + +## Implemented behavior + +`diagram::group_removal::prepare` admits every exact visible typed target before +normalization. Placements resolve to elements; ports retain their exact owner +placement identity; State rows resolve to maintained relations rather than work; +stored edges may be relations or traces. Empty, duplicate, stale, hidden, +foreign, unsupported and saved Scenario selections refuse as a whole. Existing +same-element placement uniqueness is preserved. + +The planner deterministically reduces overlapping core deletion cascades to one +Batch, with final validation rather than separate singleton transactions. It +returns unique semantic subjects, actual removed identities and changed +surviving records, including embedded placements. Final invariants require all +selected subjects removed and no newly created identities. It changes neither +the input Project nor the core cascade rules. + +Twelve regressions cover success and refusal; successful plans are checked +against independently edited complete Projects and exact impact sets. They +cover OA/SA hierarchy and incident connections across +views, exact ports and anchor pruning, visible/folded State rows, machine-owned +diagrams and embedded placements, traces preserving counterparts, changed +semantic references, whole-set refusal, and conditional allocation overlap. +A fixed-low-ID interface deletion runs before its selected boundary exchange; +the singleton is invalid while the complete batch is valid. This directly +protects final-only validation. + +## Decisions and review + +Plain group Delete retains placement-only Hide semantics; explicit model +deletion is a distinct operation. Semantic normalization uses original core +impact coverage only to choose commands; actual candidate differences determine +the impact report. Ancestry-only normalization would miss incident/dependency +overlap. Immediate scratch cleanup is unnecessary and differs from the core's +deferred exchange-allocation cleanup. + +Independent semantic critique corrected a proposed alternative-support test: +single actual performers, unique port allocations and distinct boundary owners +exclude simultaneous direct/reverse mappings in a valid model. Tests preserve +those constraints, exercise valid overlapping deferred cascades and reject the +invalid fixture. No union-only allocation-cascade test is claimed. + +A fresh source reviewer traced projection visibility, core cascade and batch +validation, checked independent expected models, and found no unresolved +concrete planner defect. Review found and resolved exact duplicate-target +admission, and added missing owned-diagram, hidden-row and invalid-singleton +coverage. Source review alone does not establish test or UI acceptance. +See the [semantic critique](saved-group-removal-evidence-20260925/semantic-review.md) +and [final independent source/evidence review](saved-group-removal-evidence-20260925/planner-review.md). + +## Verification + +Windows GNU/LLVM Rust/Cargo 1.98.1; `CARGO_BUILD_JOBS=1`, +`CARGO_INCREMENTAL=0`. Commands for this bounded foundation: + +```text +cargo test -p oa-desktop --message-format=json -- diagram::group_removal --test-threads=1 +cargo fmt --all -- --check +cargo clippy -p oa-desktop --all-targets --message-format=json -- -D warnings +``` + +All three captured commands passed at the source checkpoint above: **12 tests +passed, 0 failed, 0 ignored, 1656 filtered out**; format and strict desktop +all-target Clippy passed. Tests took 0.06 seconds after compilation; the captured +test command took 152.27 seconds. No full desktop-suite claim is made. + +Each closed run retained the same 835 inputs, verified against the exact Git +inventory and blob bytes. Input-map SHA256: +`5f2f069fc1bf15f08405c1d4ed9b1cb97f1cb7ec4288b051823f12fba9f287d9`. +The [portable records and verifier](saved-group-removal-evidence-20260925/checks/README.md) +contain all three attempts, raw logs, source capsules, source proofs and helper +provenance. No failed Cargo attempt occurred for this checkpoint. Run the +included `verify.py` with Python 3.11+ to check offline consistency; it does not +execute archived source or prove application usability. Executables are omitted +and their pins retained. Parent and independent package verification passed; +the [independent byte audit](saved-group-removal-evidence-20260925/package-audit.json) +also matched all 27 logical original files to the 23 stored ZIP members and +checked the exact inventories. The reviewer inspected closed test, format and +Clippy records; execution remains attributed to the parent. + +Evidence ZIP: 3,316,155 bytes, SHA256 +`e5fa031725af84cee29ebd9a9337570f4c8de43aa1e9ce708d77ceff28fa8154`. + +## Remaining application acceptance + +The next adapter must retain exact document/revision/view/selection/primary and +input lifetime. Ordinary and State canvases currently keep private pointer +state; queued Begin/Confirm during drag, pan, route or port editing must be +rejected before any state change. Existing Process input leases provide a +pattern, not proof of saved-view correctness. + +Deleting an underlying exchange can alter Process occurrences and reused +definitions. Review must disclose changed owner records and derived caller +effects; unchanged caller records do not prove unchanged interpretation. + +The explicit group control/menu/Shift+Delete route, aggregate review, stale and +draft refusal, one atomic Document source transaction, failure preservation, +safe surviving context, exact Undo/Redo/save/reopen, and compact native review +remain required. Existing singleton operations and placement Hide are unchanged +by this test-only module. No new GUI build or Linux verification is claimed.