Record saved-view removal planner Windows verification

This commit is contained in:
2026-09-24 21:31:53 -07:00
parent 7750ecd751
commit 8688faf8c5
11 changed files with 1066 additions and 0 deletions
+3
View File
@@ -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
@@ -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.
@@ -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"
}
}
@@ -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"
}
}
}
@@ -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()
@@ -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()
@@ -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"
}
}
}
@@ -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.
@@ -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.
@@ -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.