diff --git a/.gitattributes b/.gitattributes index 95c59b8f..6f32b754 100644 --- a/.gitattributes +++ b/.gitattributes @@ -14,6 +14,7 @@ *.zip binary harness/scenario-selection-evidence-20260924/** -text whitespace=cr-at-eol harness/scenario-group-spacing-evidence-20260924/** -text whitespace=cr-at-eol +harness/scenario-reorder-interaction-evidence-20260924/** -text whitespace=cr-at-eol # Exact source/recovery oracles and archived ledgers retain their measured bytes. # Keep ordinary text diffs; only disable checkout/check-in newline conversion. diff --git a/harness/scenario-group-reorder-policy-20260924.md b/harness/scenario-group-reorder-policy-20260924.md index 62e9adc3..c1b1e541 100644 --- a/harness/scenario-group-reorder-policy-20260924.md +++ b/harness/scenario-group-reorder-policy-20260924.md @@ -34,6 +34,8 @@ Capture immutable model and exact selection identity before movement. Cache prop Use the nearest displayed slot, then apply parent admission. A farther legal slot must not replace a nearer invalid operand destination. Feedback identifies the selected count, destination/parent and refusal or no-change reason. A Fragment's interior can contain invalid operand slots even when a header anchor would be a no-op. Pointer/camera/focus lifetime, real selection modifiers and native readability need separate adapter verification; a passing pure planner does not establish them. +This nearest-slot rule governs a **geometric drag input**, not a direct semantic destination chooser. An explicit chooser may offer exact before-anchor/end identities from the named common immediate parent, including selected anchors as no-change choices. It must retain the entire original selected set and expose temporal refusal instead of silently skipping invalid same-parent candidates. The [destination experiment](scenario-reorder-interaction-20260924.md) proves that valid saved slots can have identical Y, making one identity unreachable through unmodified nearest-Y input. That picker alone is therefore rejected as a complete group reorder interaction. An exact semantic chooser, order strip or explicit tie disambiguator must provide the missing reachability; this negative decision does not accept a particular UI. + ## Alternatives and acceptance Contiguous-only selection rejects useful sibling groups without simplifying nested temporal validation. Stable semantic ordering is therefore preferred. Parent/child normalization silently drops selected intent, while automatic call/reply expansion has no unique closure in the presence of alternative replies, loops and nested fragments. Strict refusal keeps this first increment precise. Revisit these decisions only with an explicit user workflow and a compatible whole-set specification. diff --git a/harness/scenario-reorder-interaction-20260924.md b/harness/scenario-reorder-interaction-20260924.md index 1028fb4b..8a41788c 100644 --- a/harness/scenario-reorder-interaction-20260924.md +++ b/harness/scenario-reorder-interaction-20260924.md @@ -31,3 +31,20 @@ Baseline Fallback admission is separate from candidate degradation: the frozen s The test-only session and slot experiments are isolated from production input until their results are reviewed. Source-level tests establish transformations, rendering facts and admission invariants; they do not establish discoverability, keyboard access, result comprehension or native usability. Those need a built App adapter, direct Windows workflows at ordinary and compact sizes, then independent review. Linux remains user-paused. The two candidates are materially different: the chooser favors explicit identity and keyboard reachability; the handle favors continuous canvas manipulation but inherits geometric ambiguity. Their shared session is an implementation dependency, not evidence that either user interaction is accepted. Record actual experiment results before choosing or revising either candidate. Aggregate removal and broader feature completeness remain required beyond this experiment. + +## Executed result and resulting decision + +At source `cae7a22297d09810c412dbdf4ce5d8a28164b247`, Windows GNU/LLVM `cargo test -p oa-desktop reorder_ --message-format=json -- --nocapture` passed **22 tests, zero failed, zero ignored** (1,573 filtered). This includes all five new session tests, all four slot experiments and thirteen existing tests selected by this name filter. It is not the complete Scenario or desktop suite. The gate captured 814 unchanged inputs and independently compared their exact inventory and bytes with that commit. Workspace formatting and strict all-target desktop lint also passed with the same exact source proof. [Portable checks and independent review](scenario-reorder-interaction-evidence-20260924/README.md) retain all three complete runs. + +All four geometry fixtures passed real Saved-presentation Ready and core-error-free assertions. Their source-order list is A/B/C/D: + +| Authored message Y | Actual before-slot Y | Observed consequence | +| --- | --- | --- | +| 200 / 500 / 300 / 400 | 158 / 458 / 258 / 358 | Physical A/C/D/B order differs from semantic order; each of the four before-anchors is individually reachable. | +| 200 / 300 / 300 / 500 | 158 / 258 / 258 / 458 | B and C share slot Y; the actual nearest picker chooses B and cannot uniquely address C. Explicit before-C and nearest before-B produce different literal complete-model results for selected D/A. | +| 200 / 300 / 300.125 / 300.25 | 158 / 258 / 258.125 / 258.25 | C is continuously reachable in a subpixel interval, but one-pixel samples at scale 1/pan 0 miss it. This does not claim that Windows always quantizes pointer coordinates. | +| 200 / 300 / 400 / 2000 | 158 / 258 / 358 / 1958 | D cannot win in a fixed 600px-high scale-1/pan-0 viewport; changing the camera can make it reachable. | + +The tied-slot counterexample is decisive for the original fixed-handle candidate using unchanged nearest-Y picking: it cannot be the sole method for selecting every valid semantic destination. A different deterministic tie winner only exchanges which identity is unreachable. Preserve exact semantic destination input or add an explicit disambiguation/order-strip interaction. Crossed and close geometry also require feedback, but do not prove universal unreachability. Offscreen evidence is limited to the stated fixed viewport. + +**Accepted decision:** reject the unchanged nearest-Y-only candidate as a complete group reorder interface. Build the exact destination and before/after semantic preview foundation first; compare click/keyboard access and a graphical handle using explicit destination identity or a disambiguator before final UI acceptance. The current session and tests remain behind `cfg(test)`; no new product command is enabled by this checkpoint. The model/source equality and round-trip oracles pass, but no actual chooser/handle, App lifetime adapter, native comprehension, keyboard workflow, long-name/twenty-sibling usability or new release is claimed. diff --git a/harness/scenario-reorder-interaction-evidence-20260924/README.md b/harness/scenario-reorder-interaction-evidence-20260924/README.md new file mode 100644 index 00000000..938ca7da --- /dev/null +++ b/harness/scenario-reorder-interaction-evidence-20260924/README.md @@ -0,0 +1,27 @@ +# Scenario reorder destination experiment evidence + +[Question, result and bounded decision](../scenario-reorder-interaction-20260924.md). All new code remains under `cfg(test)`; this checkpoint enables no production group reorder command and claims no native UI acceptance. Ordinary group spacing was delivered separately. + +Tested source: `cae7a22297d09810c412dbdf4ce5d8a28164b247`. The three Windows GNU/LLVM captures use Rust/Cargo 1.98.1, `stable-x86_64-pc-windows-gnullvm`, one build job and `CARGO_INCREMENTAL=0`. Each captures 814 unchanged input files; exact inventory and Git-blob comparisons pass. Full environment, command, process, output and source-proof records are inside the archive. + +```powershell +cargo test -p oa-desktop reorder_ --message-format=json -- --nocapture +cargo fmt --all -- --check +cargo clippy -p oa-desktop --all-targets --message-format=json -- -D warnings +``` + +Tests: 22 passed / 0 failed / 0 ignored, 1,573 filtered, including all five session and four slot experiment functions. The other thirteen are existing name-filter matches; this is not full Scenario/desktop testing. Format and strict all-target desktop Clippy each exit 0. No Linux or native process was run for this test-only increment. + +`checks/evidence.zip`: **3,244,466 bytes**, SHA-256 **`b713a5c8c03f4cb4f4a6884bde7eb081fca00d1f5ebf85427ce14bc03acae166`**. Three closed runs, zero failures, 27 logical files, 23 stored members, one deduplicated source capsule and three exact source proofs. These are all attempts for this increment. Previous spacing failures remain in that contribution's separate evidence. + +After checkout, with Python 3.11+: + +```powershell +python -B harness/scenario-reorder-interaction-evidence-20260924/checks/verify.py +``` + +The verifier checks every archive/member pin, captured source capsule, command/result relationship and source-proof receipt. It does not rerun Cargo or the original Git-blob comparison. Reused helper README examples and the manifest's `scenario-selection-cargo-gates` kind are historical naming; actual run names here are `reorder-interaction-test-1`, `reorder-interaction-format-1` and `reorder-interaction-clippy-1`. + +The [independent review](review.md) records the original specification ambiguities, bounded cache correction and static/runtime evidence audit. The [slot experiment notes](slot-experiment.md) retain original expectations followed by actual executed findings. Preparation or “pending execution” statements in their earlier sections describe that earlier phase; later closed receipts and appended results supersede them. + +The result rejects unchanged nearest-Y picking as the only method for choosing exact semantic destinations: admitted equal-Y slots leave one anchor unreachable. It does not accept a chooser's usability, reject every possible graphical handle, prove all nested/append destinations, establish Windows pointer quantization, or claim that offscreen targets cannot be reached after a camera change. See the report for remaining App and native requirements. diff --git a/harness/scenario-reorder-interaction-evidence-20260924/checks/README.md b/harness/scenario-reorder-interaction-evidence-20260924/checks/README.md new file mode 100644 index 00000000..4ce6c714 --- /dev/null +++ b/harness/scenario-reorder-interaction-evidence-20260924/checks/README.md @@ -0,0 +1,14 @@ +# Scenario selection captured checks + +These tools package explicitly named, completed runs from `CHECKOUT/.local/scenario-group-gates` without executing archived source or running Cargo. Adapted only in input-directory and manifest-kind names from the published prerelease Connection/Route evidence tools at `93d2a6f9af40061bf92cd81ba33d5648d0181cb1`. + +```powershell +python -B prepare.py --root CHECKOUT --runs selection-scenario-1 selection-scenario-2 selection-format-1 --output NEW_DIRECTORY +python -B NEW_DIRECTORY/verify.py +``` + +The manifest is authoritative for the selected runs. Failed runs remain failures: archive consistency is not a test pass. Every raw stream and receipt retains its original bytes. Identical input maps and ZIPs are stored by hash; the logical-file map preserves their original run paths. Historical capture helpers and exact source comparators are included as data, never executed by verification. + +Source proofs compare every captured input with a named Git commit; the offline package verifier checks those receipts and their pinned comparator identity but does not repeat the Git comparison. The captured HEAD and the named proof commit retain their respective meanings. The verifier checks archive inventory, hashes, input ZIP contents, process/command/result consistency and executable artifact relationships. It does not establish hermetic builds, native workflow acceptance, UI integration, or release acceptance. + +Use `--check-only` instead of `--output` for an in-memory consistency check. Existing output directories refuse. Python 3.11+ standard library is sufficient; `-B` avoids bytecode writes. diff --git a/harness/scenario-reorder-interaction-evidence-20260924/checks/evidence.zip b/harness/scenario-reorder-interaction-evidence-20260924/checks/evidence.zip new file mode 100644 index 00000000..099af8eb Binary files /dev/null and b/harness/scenario-reorder-interaction-evidence-20260924/checks/evidence.zip differ diff --git a/harness/scenario-reorder-interaction-evidence-20260924/checks/files.json b/harness/scenario-reorder-interaction-evidence-20260924/checks/files.json new file mode 100644 index 00000000..2291acc9 --- /dev/null +++ b/harness/scenario-reorder-interaction-evidence-20260924/checks/files.json @@ -0,0 +1,22 @@ +{ + "prepare.py": { + "bytes": 6308, + "sha256": "2d500f5d43a49893abb3936f880e61b8adc623531e26f0288983502e386894db" + }, + "verify.py": { + "bytes": 11436, + "sha256": "ea8e781003bec21f2ac4af23e21b68ca5639a98eb31505c10b2869f57565e917" + }, + "README.md": { + "bytes": 1641, + "sha256": "4c5c582cbdf02c8704b9add656d273c275b10eed87219131e3bba639a4c4bd8a" + }, + "manifest.json": { + "bytes": 9761, + "sha256": "34b9791fca0d23e6d27e68daf769fe6952cb38aef83dec22594ff8900bbdcdb6" + }, + "evidence.zip": { + "bytes": 3244466, + "sha256": "b713a5c8c03f4cb4f4a6884bde7eb081fca00d1f5ebf85427ce14bc03acae166" + } +} diff --git a/harness/scenario-reorder-interaction-evidence-20260924/checks/manifest.json b/harness/scenario-reorder-interaction-evidence-20260924/checks/manifest.json new file mode 100644 index 00000000..642c4760 --- /dev/null +++ b/harness/scenario-reorder-interaction-evidence-20260924/checks/manifest.json @@ -0,0 +1,203 @@ +{ + "schema": 1, + "kind": "scenario-selection-cargo-gates", + "runs": { + "reorder-interaction-format-1": { + "command": [ + "cargo", + "fmt", + "--all", + "--", + "--check" + ], + "head": "cae7a22297d09810c412dbdf4ce5d8a28164b247", + "exitCode": 0, + "inputCount": 814, + "helper": "helpers/3e4cd38fd3807a4010a5a7102354f9139cab236ca55163c96090e416161e5fd2.py", + "sourceProof": { + "commit": "cae7a22297d09810c412dbdf4ce5d8a28164b247", + "pin": { + "bytes": 607, + "sha256": "b0e53cc3fe43733664c57fcb1894da1a966c9dae07bbd30af03d9e743aeba747" + }, + "comparator": "comparators/7805832d5f563ab2d7a50dd3de700792b5d6f459c68f7b1d8bcfcc9800fa0c3a.py" + } + }, + "reorder-interaction-test-1": { + "command": [ + "cargo", + "test", + "-p", + "oa-desktop", + "reorder_", + "--message-format=json", + "--", + "--nocapture" + ], + "head": "cae7a22297d09810c412dbdf4ce5d8a28164b247", + "exitCode": 0, + "inputCount": 814, + "helper": "helpers/3e4cd38fd3807a4010a5a7102354f9139cab236ca55163c96090e416161e5fd2.py", + "sourceProof": { + "commit": "cae7a22297d09810c412dbdf4ce5d8a28164b247", + "pin": { + "bytes": 605, + "sha256": "c0cae0486b5d47a4d2bfb79e097cb1487a22bd10bbd759af030439a2b94ac7a7" + }, + "comparator": "comparators/7805832d5f563ab2d7a50dd3de700792b5d6f459c68f7b1d8bcfcc9800fa0c3a.py" + } + }, + "reorder-interaction-clippy-1": { + "command": [ + "cargo", + "clippy", + "-p", + "oa-desktop", + "--all-targets", + "--message-format=json", + "--", + "-D", + "warnings" + ], + "head": "cae7a22297d09810c412dbdf4ce5d8a28164b247", + "exitCode": 0, + "inputCount": 814, + "helper": "helpers/3e4cd38fd3807a4010a5a7102354f9139cab236ca55163c96090e416161e5fd2.py", + "sourceProof": { + "commit": "cae7a22297d09810c412dbdf4ce5d8a28164b247", + "pin": { + "bytes": 607, + "sha256": "224c3cfb30b88de8afa9981491826e7332127042542b68a042a87e90b5ffe5bc" + }, + "comparator": "comparators/7805832d5f563ab2d7a50dd3de700792b5d6f459c68f7b1d8bcfcc9800fa0c3a.py" + } + } + }, + "captureHelpers": { + "gate.py": "helpers/3e4cd38fd3807a4010a5a7102354f9139cab236ca55163c96090e416161e5fd2.py", + "source-proof.py": "helpers/c86f51d9b8465341dab73d045923ec306b15ebabf7c97b81beac7afe73bd72a1.py" + }, + "logicalFiles": { + "cargo/reorder-interaction-clippy-1/inputs.json": "capsules/43bfafea79cfd2268d667504dbce7531482ff016ff5daf54065285f0564fa036.json", + "cargo/reorder-interaction-clippy-1/inputs.zip": "capsules/a6aca4ff50b27cefa53e34113988f7574f87678349d594c3d4bb769bb3fe3450.zip", + "cargo/reorder-interaction-clippy-1/process.json": "cargo/reorder-interaction-clippy-1/process.json", + "cargo/reorder-interaction-clippy-1/result.json": "cargo/reorder-interaction-clippy-1/result.json", + "cargo/reorder-interaction-clippy-1/source-proof.json": "cargo/reorder-interaction-clippy-1/source-proof.json", + "cargo/reorder-interaction-clippy-1/started.json": "cargo/reorder-interaction-clippy-1/started.json", + "cargo/reorder-interaction-clippy-1/stderr.log": "cargo/reorder-interaction-clippy-1/stderr.log", + "cargo/reorder-interaction-clippy-1/stdout.log": "cargo/reorder-interaction-clippy-1/stdout.log", + "cargo/reorder-interaction-format-1/inputs.json": "capsules/43bfafea79cfd2268d667504dbce7531482ff016ff5daf54065285f0564fa036.json", + "cargo/reorder-interaction-format-1/inputs.zip": "capsules/a6aca4ff50b27cefa53e34113988f7574f87678349d594c3d4bb769bb3fe3450.zip", + "cargo/reorder-interaction-format-1/process.json": "cargo/reorder-interaction-format-1/process.json", + "cargo/reorder-interaction-format-1/result.json": "cargo/reorder-interaction-format-1/result.json", + "cargo/reorder-interaction-format-1/source-proof.json": "cargo/reorder-interaction-format-1/source-proof.json", + "cargo/reorder-interaction-format-1/started.json": "cargo/reorder-interaction-format-1/started.json", + "cargo/reorder-interaction-format-1/stderr.log": "cargo/reorder-interaction-format-1/stderr.log", + "cargo/reorder-interaction-format-1/stdout.log": "cargo/reorder-interaction-format-1/stdout.log", + "cargo/reorder-interaction-test-1/inputs.json": "capsules/43bfafea79cfd2268d667504dbce7531482ff016ff5daf54065285f0564fa036.json", + "cargo/reorder-interaction-test-1/inputs.zip": "capsules/a6aca4ff50b27cefa53e34113988f7574f87678349d594c3d4bb769bb3fe3450.zip", + "cargo/reorder-interaction-test-1/process.json": "cargo/reorder-interaction-test-1/process.json", + "cargo/reorder-interaction-test-1/result.json": "cargo/reorder-interaction-test-1/result.json", + "cargo/reorder-interaction-test-1/source-proof.json": "cargo/reorder-interaction-test-1/source-proof.json", + "cargo/reorder-interaction-test-1/started.json": "cargo/reorder-interaction-test-1/started.json", + "cargo/reorder-interaction-test-1/stderr.log": "cargo/reorder-interaction-test-1/stderr.log", + "cargo/reorder-interaction-test-1/stdout.log": "cargo/reorder-interaction-test-1/stdout.log", + "comparators/7805832d5f563ab2d7a50dd3de700792b5d6f459c68f7b1d8bcfcc9800fa0c3a.py": "comparators/7805832d5f563ab2d7a50dd3de700792b5d6f459c68f7b1d8bcfcc9800fa0c3a.py", + "helpers/3e4cd38fd3807a4010a5a7102354f9139cab236ca55163c96090e416161e5fd2.py": "helpers/3e4cd38fd3807a4010a5a7102354f9139cab236ca55163c96090e416161e5fd2.py", + "helpers/c86f51d9b8465341dab73d045923ec306b15ebabf7c97b81beac7afe73bd72a1.py": "helpers/c86f51d9b8465341dab73d045923ec306b15ebabf7c97b81beac7afe73bd72a1.py" + }, + "members": { + "capsules/43bfafea79cfd2268d667504dbce7531482ff016ff5daf54065285f0564fa036.json": { + "bytes": 136737, + "sha256": "43bfafea79cfd2268d667504dbce7531482ff016ff5daf54065285f0564fa036" + }, + "capsules/a6aca4ff50b27cefa53e34113988f7574f87678349d594c3d4bb769bb3fe3450.zip": { + "bytes": 3254695, + "sha256": "a6aca4ff50b27cefa53e34113988f7574f87678349d594c3d4bb769bb3fe3450" + }, + "cargo/reorder-interaction-clippy-1/process.json": { + "bytes": 179, + "sha256": "cb0063779120dff8b7a512c94ac157974503a8da4d33e7db72041c8783493df2" + }, + "cargo/reorder-interaction-clippy-1/result.json": { + "bytes": 1958, + "sha256": "ec2e7b0dfd7f5aa2a34783f4b7082afc159a665e6c09f55d1924f1df9f22b17c" + }, + "cargo/reorder-interaction-clippy-1/source-proof.json": { + "bytes": 607, + "sha256": "224c3cfb30b88de8afa9981491826e7332127042542b68a042a87e90b5ffe5bc" + }, + "cargo/reorder-interaction-clippy-1/started.json": { + "bytes": 1563, + "sha256": "9192fb388be502a8e99ce1140ad6ef18b495d9eac752e4a682ea40acf1a04334" + }, + "cargo/reorder-interaction-clippy-1/stderr.log": { + "bytes": 134, + "sha256": "b6b278b2d1f23091255fe43a194eb4c496fe6a51f98838af9f0f1d02f1bdb4b9" + }, + "cargo/reorder-interaction-clippy-1/stdout.log": { + "bytes": 269431, + "sha256": "f2a6d72c9ee496aecb3db4126c8e8a4248746ccc07a04ed199fa354ab51d5a20" + }, + "cargo/reorder-interaction-format-1/process.json": { + "bytes": 116, + "sha256": "99e05ce65425dc3d23db945201b2d10808295189374dd75ee5632d78925af632" + }, + "cargo/reorder-interaction-format-1/result.json": { + "bytes": 1868, + "sha256": "743188d86811f129ba5991228edb82b200844aef3046619de49e7245a945e9b3" + }, + "cargo/reorder-interaction-format-1/source-proof.json": { + "bytes": 607, + "sha256": "b0e53cc3fe43733664c57fcb1894da1a966c9dae07bbd30af03d9e743aeba747" + }, + "cargo/reorder-interaction-format-1/started.json": { + "bytes": 1480, + "sha256": "4df85b8b570f1d1fcd810205efc1c057ce02aeca996739a6616c689d9087ef9e" + }, + "cargo/reorder-interaction-format-1/stderr.log": { + "bytes": 0, + "sha256": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855" + }, + "cargo/reorder-interaction-format-1/stdout.log": { + "bytes": 0, + "sha256": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855" + }, + "cargo/reorder-interaction-test-1/process.json": { + "bytes": 169, + "sha256": "31ddcefd1a64b54ba33eac12325491f94a001e109ba48769548014824d53b2a5" + }, + "cargo/reorder-interaction-test-1/result.json": { + "bytes": 3273, + "sha256": "de71d8a0b8b4b7de401b4ccc1efd03bbb3bf9760bd4b97883ba6c4786d0ada5b" + }, + "cargo/reorder-interaction-test-1/source-proof.json": { + "bytes": 605, + "sha256": "c0cae0486b5d47a4d2bfb79e097cb1487a22bd10bbd759af030439a2b94ac7a7" + }, + "cargo/reorder-interaction-test-1/started.json": { + "bytes": 1548, + "sha256": "056cbfc2ec65f864cdab91fcd314b3be10c72721878e6f659215d3580e247e3c" + }, + "cargo/reorder-interaction-test-1/stderr.log": { + "bytes": 284, + "sha256": "356bc71e25ef09fb5af8521c4a4513f8a77cd55fbe498328ee7ff77f8693e83c" + }, + "cargo/reorder-interaction-test-1/stdout.log": { + "bytes": 295941, + "sha256": "ea183f5dbe6a5d369525f6d0de171e3df6175c60ffc5d2ebcdb1023218d77d84" + }, + "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/scenario-reorder-interaction-evidence-20260924/checks/prepare.py b/harness/scenario-reorder-interaction-evidence-20260924/checks/prepare.py new file mode 100644 index 00000000..10667b2b --- /dev/null +++ b/harness/scenario-reorder-interaction-evidence-20260924/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 in (RAW_FILES, RAW_FILES | {"source-proof.json"}), "exact closed raw files 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": "scenario-selection-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/scenario-reorder-interaction-evidence-20260924/checks/verify.py b/harness/scenario-reorder-interaction-evidence-20260924/checks/verify.py new file mode 100644 index 00000000..618390ee --- /dev/null +++ b/harness/scenario-reorder-interaction-evidence-20260924/checks/verify.py @@ -0,0 +1,194 @@ +"""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"[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"] == "scenario-selection-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(): + 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/scenario-reorder-interaction-evidence-20260924/review.md b/harness/scenario-reorder-interaction-evidence-20260924/review.md new file mode 100644 index 00000000..c1314ab7 --- /dev/null +++ b/harness/scenario-reorder-interaction-evidence-20260924/review.md @@ -0,0 +1,57 @@ +# Scenario reorder session and interaction experiment review + +Bounded independent review in `C:/ar-scenario-9e8r6t`, on the parent's identified base `53817298`. No production edits, Cargo, native actions or Gitea operations. This review distinguishes an isolated semantic foundation from an accepted user interaction. Source/test review will follow the writers' frozen handoff; no gate result is assumed. + +## Initial contract assessment + +The experiment is justified as a foundation. `harness/scenario-reorder-interaction-20260924.md:7-11` supplies a discriminating noncontiguous selection task, preserved saved coordinates, observable before/after order, subsequent ordinary spacing, equal anchors, offscreen destinations and repeated names. This directly addresses the user's graphical editing and low-cognitive-load requirements (`PROJECT_GOAL.md:93-106`). The explicit chooser and labelled handle are materially different candidates; passing chooser transformations alone cannot decide between them or satisfy the user's complaint about menu-driven diagram editing. + +The exact requested representation plus full Scenario Context/Snapshot/token boundary is appropriate. Whole original typed selection must be admitted before no-op; no selected subset, silent representation switch or pruning is permitted. The App's early admission obligation is correctly distinct from consuming a pure session. Isolating both new modules behind `cfg(test)` prevents this experiment from silently enabling production group reorder. + +## Contract questions before production input + +1. **Retire every admitted terminal outcome.** The initial App boundary at interaction line 23 names cancellation, successful/no-op Finish and failed Start, but omits Refused Finish and a failed Document/source commit. A refused current operation can leave all selection/model values unchanged, making duplicate terminal packets a meaningful adversary. Explicitly retire these outcomes too, without changing accepted model/source/history/selection/camera and without touching a newer owner on stale input. This is a missing future-adapter invariant, not a current production defect. +2. **Separate baseline presentation admission from candidate commit policy.** `scenario_layout/presentation.rs:79-85` makes coordinate input available only for Ready, while Fallback retains inspectable automatic geometry and requested Saved identity. A semantic chooser is not coordinate input. Specify whether a Fallback baseline can start each candidate and separately decide how a semantically valid Fallback/Unavailable candidate may finish. A returned command plus status must not be treated as implicit permission to commit. The current test-only foundation may expose both facts while keeping the product decision unresolved. +3. **Choose stationary/origin-return semantics for the drag.** The required camera/threshold/outside rules do not yet settle whether return to the pressed screen location cancels or selects the nearest semantic destination. A saved press need not be nearest its own anchor, particularly with close/crossed rows. Define and test either behavior before adapter acceptance; do not infer an origin-return no-op from the planner's selected-anchor no-op rule. + +These points were sent to the parent. They do not prevent publishing an explicitly incomplete test-only session/slot experiment. Native success still requires users to identify the exact parent/anchor and final semantic order despite preserved authored positions, then perform a normal spacing operation without accidental semantic edits. Repeated long names need a usable visible disambiguator; internal IDs alone do not establish this. A bounded searchable list is an experiment to evaluate, not an automatic low-cognitive-load success. + +## Contract revision and frozen session source review + +Re-read the revised interaction specification: it now explicitly retires every admitted terminal Finish including semantic refusal and source-commit failure, separates baseline Fallback admission from candidate commit policy, and leaves stationary/origin-return drag rules as decisions required before enabling an adapter. The initial three contract findings above are therefore resolved as specification clarifications; the product decisions remain openly pending. + +Reviewed the writer's frozen `behavior/scenario/reorder_session.rs` and its five test functions. **No concrete unresolved semantic/admission defect found in this isolated session.** `Session::new` checks nonnil document/visit/token, exact generic selection context and requested representation's actual Scenario owner, then admits every original member against the actual presented scene before whole-set planner preparation. The private immutable Project and Snapshot retain selection click order and primary; the planner constructs semantic order independently. Current proposal storage is bounded to one rendered proposal. Refusal replaces the current proposal rather than leaving an earlier valid command armed. `finish(self, ...)` compares complete Project, Context, Snapshot (including generation) and token before returning a private cached command, and Rust ownership prevents reuse of that session. It neither mutates Document nor proves App early admission or lifetime retirement. + +One concrete evaluator weakness was repaired during review: pointer equality to an `Option` field would remain true even if that field were recomputed in place. The final test uses test-only proposal-build instrumentation, checking one build for repeated current Slot, a second for another Slot and a third when returning to the original. This now distinguishes the intended bounded cache behavior. + +The tests independently construct the expected complete Project by editing only the literal parent steps, retaining nonempty saved lane/message overrides, metadata and a second saved view. They cover original selection/primary, explicit absence of destination versus selected-anchor no-op, temporal refusal, mixed and visibly cross-parent full sets, foreign/missing visibility, changed Project, epoch/revision/document/visit/token, changed selection order and generation-only primary ABA. Equality with the expected complete Project is stronger than preview/commit agreement alone. The Fallback case starts from authored call/reply geometry already requiring fallback; it establishes baseline Fallback admission and requested Saved identity preservation, **not** a Ready-to-Fallback transition. + +Remaining bounded coverage gaps: no constructed semantically valid candidate yielding Unavailable, no independently exercised Ready-to-Fallback transition, no native feedback, and no App command/history/source transaction or stale-event adapter in this module. Returning a command alongside a degraded status remains information for future policy, not permission to execute it. All new session code is test-only. These omissions must remain explicit and must be addressed where the subsequent adapter's actual policy needs them; they do not justify claiming either interaction accepted. No test execution result is claimed by this reviewer. + +## Frozen slot experiment and final pre-gate assessment + +Reviewed all four `reorder_slot_experiments.rs` tests against actual `Scene::slot_at` (`behavior.rs:1855`), which chooses the global nearest Y before checking parent identity. The experiments resolve actual Saved presentation and assert Ready rather than assuming authored coordinates rendered. Their `exact_change` oracle (`reorder_slot_experiments.rs:102`) alters only the independently specified literal parent order, compares the complete Project, and round-trips source. No concrete fixture/oracle flaw found statically. + +The crossed case (`:142`) distinguishes semantic order from the actual A/C/D/B screen order while checking each before-anchor remains reachable. The equal-Y case (`:161`) demonstrates the B/C tie winner and compares two distinct literal semantic outcomes: exact before-C versus the nearest picker returning before-B. The close case (`:189`) explicitly retains continuous reachability and limits its negative result to integer-pixel samples at a fixed camera; it does not establish platform pointer quantization. The offscreen case (`:207`) fixes scale one, pan zero and a 600-pixel viewport, and separately proves reachability outside that viewport. Neither justifies a claim that scrolling/zooming can never reach the destination. + +These are geometry/planner experiments, not an implemented chooser or drag handle. They make unchanged nearest-Y input an insufficient universal equivalent of exact semantic destinations, but do not select the final interaction. Nested invalid-nearer destinations, repeated long names, twenty-sibling display, keyboard reachability, discoverability, feedback, Ready-to-degraded candidate transitions and a built App lifetime adapter remain outside these four cases. The chooser could still fail the user's direct graphical editing requirement; a handle could add a bounded disambiguator or operation-local order preview. Compare the resulting workflows before acceptance. + +Also reviewed the parent's final temporal test enhancement: it first caches a valid changed order, then chooses the temporally invalid before-call destination and requires Finish to refuse. This meaningfully guards against executing an earlier valid proposal after a later refusal. + +**Final static conclusion: no unresolved material defect found in the bounded test-only foundation.** The specification findings and cache-test weakness were addressed as recorded above. Nine new test functions (five session, four slot experiments) await the parent's gate; this review ran no Cargo and claims no passing test/native result. Review file is stable for handoff. + +## Closed gate and decision audit at cae7a222 + +Read the actual closed receipts and logs for `reorder-interaction-test-1`, `reorder-interaction-format-1` and, now closed, `reorder-interaction-clippy-1` at `cae7a22297d09810c412dbdf4ce5d8a28164b247`. All three report exit 0 and unchanged 814 inputs. Independently rehashed each retained input map, input ZIP and raw stdout/stderr against its receipt, counted all 814 ZIP members, and checked each source-proof receipt's commit/count and exact input-map/result bindings. This confirms receipt consistency; this audit does not claim to rerun the complete Git blob comparison. + +The actual test log has exactly **22 named passing tests: five new session tests, four new slot experiments and thirteen existing tests**, with zero failed, zero ignored and 1,573 filtered. Both formatting and strict `cargo clippy -p oa-desktop --all-targets --message-format=json -- -D warnings` are successful closed runs at that same source. No additional Cargo was run by this reviewer. + +Re-read the interaction document's Executed result/decision section and the policy's new geometric-versus-semantic input clarification. **No material overclaim found.** Actual Ready equal-Y geometry supplies a concrete counterexample to unchanged nearest-Y input being the sole complete semantic destination mechanism; changing only the deterministic tie winner leaves another identity unreachable. This supports the negative decision while leaving chooser, order strip and explicit drag disambiguation open. The text correctly limits the close-slot result to integer samples and the offscreen result to a fixed camera, and explicitly withholds App/native/keyboard/large-list acceptance. The passing filter is not described as a complete Scenario or desktop suite. A final portable package audit remains separate from these closed-run checks. + +## Final portable package audit + +Ran `harness/scenario-reorder-interaction-evidence-20260924/checks/verify.py` successfully: **3 runs, 0 recorded failures, 27 logical files, 23 stored ZIP members, 1 deduplicated source capsule pair and 3 source proofs**. Independently checked the exact ZIP inventory and every member hash, all 24 raw run/logical files byte-for-byte against `.local/scenario-group-gates`, and all external `files.json` pins. Archive `evidence.zip` is **3,244,466 bytes**, SHA256 **`b713a5c8c03f4cb4f4a6884bde7eb081fca00d1f5ebf85427ce14bc03acae166`**. No integrity mismatch found. + +The checks README and manifest kind retain historical Scenario-selection helper naming. At audit time a root package index had not yet been written; parent was asked to explain those reused names and actual reorder run inventory there, preserving the pinned helper bytes. This is delivery clarity, not an evidence defect. The portable verifier checks historical proofs and archive consistency without re-executing source, binaries or Git comparisons. The tested source remains cae7a222; subsequent record edits are not relabelled as having executed those 22 tests. No native interaction or accepted chooser/handle is implied. This review is stable for delivery copy. + +Final delivery clarification: read the newly written root evidence README. It now lists all three actual reorder runs, exact commands/environment, archive identity, reused historical names and the distinction between prior preparation notes and executed results. The delivery-clarity note above is resolved; no material overclaim found. File remains stable for copy. diff --git a/harness/scenario-reorder-interaction-evidence-20260924/slot-experiment.md b/harness/scenario-reorder-interaction-evidence-20260924/slot-experiment.md new file mode 100644 index 00000000..bcc29f94 --- /dev/null +++ b/harness/scenario-reorder-interaction-evidence-20260924/slot-experiment.md @@ -0,0 +1,42 @@ +# Scenario semantic destination reachability experiment + +2026-09-24; production base 53817298 plus parent-owned test-only session and this test module, tested together at cae7a22297d09810c412dbdf4ce5d8a28164b247. Only `crates/oa-desktop/src/behavior/scenario/reorder_slot_experiments.rs` and this local note are owned by this subagent. No production input change, Cargo, GUI or publication performed by this subagent. Parent ran the captured test gate; executed findings and independently inspected records are appended below. The four tests remain frozen without fixture correction. + +## Question, method and literal expectations + +Does the existing nearest-Y picker give every exact same-parent semantic destination a usable pointer target when Saved geometry differs from semantic order? The test module uses actual `presentation::present`, `Scene::slot_at`, `group_reorder::Prepared` and core command execution. Every fixture must be core error-free and actual requested Saved presentation `Ready`. Four independent asynchronous messages have semantic order `[A,B,C,D]`. Expected candidate Projects are constructed by changing only the literal parent step vector in a clone, then compared in full; format/parse is also checked. The fixture is deliberately uncomplicated: it isolates destination identity, not lifecycle admission or user comprehension. + +| Case | Authored send Y | Literal expectation | +| --- | --- | --- | +| Crossed | 200,500,300,400 | Before-slot Y 158,458,258,358; physical order A,C,D,B; each exact anchor still reachable at its own Y. C-then-A selection before D gives `[B,A,C,D]`. | +| Equal | 200,300,300,500 | B/C share slot Y258. B precedes C in the slot vector and wins every tie. Exact chooser before C gives `[B,A,D,C]` for D-then-A selection; nearest at Y258 chooses before B and gives `[A,D,B,C]`. | +| Close | 200,300,300.125,300.25 | C slot258.125 is continuously reachable, but its open nearest-Y interval `(258.0625,258.1875)` contains no integer pixel at scale1/pan0. | +| Offscreen | 200,300,400,2000 | D slot1958 is reachable there; its boundary against C is Y1158, outside a fixed world viewport Y0..600. Exact chooser before D remains valid. | + +The equal case has a mathematical identity argument in addition to a sampled execution check: two slots with equal Y have equal distance for every finite pointer Y, and `Iterator::min_by` keeps the earlier equal minimum. Sampling is not represented as exhaustive proof. Close and offscreen cases are deliberately weaker: there is no claim that Windows quantizes all input to integer pixels, or that a destination remains offscreen after pan/zoom. A fixed-camera handle with camera-change cancellation cannot silently rely on pan to make that case work. + +Source anchors: `behavior.rs:1621-1632` seeds before-slots at each resolved anchor Y minus42; `behavior.rs:1855-1860` minimizes distance before filtering parent, ignores X, and has no tie disambiguator; `behavior.rs:2179-2180` maps screen coordinates using pan/scale. The same-parent root fixture intentionally avoids wrong-parent nearest-slot filtering. Existing policy still requires the drag to expose that refusal instead of searching farther. + +## Design implication, conditional on the gate + +An unchanged nearest-Y handle cannot establish equivalent destination coverage to an exact Slot chooser if the equal case passes its actual Ready assertion. A better hover label does not create C's missing hit region. Candidate remedies are a temporary semantic-order destination strip, an explicit tie chooser, or keyboard cycling through exact slots. Each adds a nonspatial identity mechanism; do not pretend a source-order tie-break alone makes both tied anchors reachable. Crossed coordinates alone do not falsify the handle: they test user comprehension. Close coordinates establish target-size sensitivity, not total continuous unreachability. Offscreen coordinates establish the need for a deliberate camera or alternate destination policy. + +## Minimal shared operation boundary + +The parent-owned `scenario/reorder_session.rs` is consistent with the useful minimum: immutable `Arc`, exact full Scenario `Context`, complete `Snapshot`, fresh nonnil operation token; one admitted `Prepared`; no initially chosen destination; a single cached current `Preview{slot,before,after,outcome,presentation,private command}`; consuming finish returns the cached command plus selection/requested representation. Do not store a scene for every sibling or every pointer sample. Do not add a global gesture abstraction for this experiment. + +Both inputs should feed the same exact `preview(Slot)`. The chooser supplies identity directly. A drag first resolves the actual global nearest displayed slot, then submits that exact identity to the planner; `None` from parent filtering is refusal, never a redirect to the closest legal slot. Repeated current Slot should reuse the candidate. Candidate `Ready`, `Fallback` and `Unavailable` remain separate from semantic Change/Noop/Refused, and requested representation stays Saved even when actual display falls back. Baseline fallback admission is a separate decision: a chooser may use identities visible in the fallback scene, but coordinate input cannot gain permission just because the semantic planner is valid. + +The pure session cannot own the whole input lifetime. The App must compare full context/snapshot/token before taking or mutating the active session. Cancel matches the current token; old Cancel must not discard a newer operation. Every admitted terminal event, including no-op, refusal, failed begin and failed source commit, retires the operation generation so a replayed Begin cannot reopen it on an identical selection. A pointer adapter additionally freezes camera/bounds and defines threshold/outside release/out-and-back; a stationary press/release on a fixed handle must not accidentally select its incidental nearest slot. A chooser can permit passive scrolling only if exact destination identity is stable. Source/Properties dirty state, draft insertion, menu capture, ownership and document-instance/representation-visit ABA admission remain App responsibilities. + +No universal commit policy for candidate Fallback/Unavailable is inferred from a returned valid command. The prototype must show that status and make the product decision explicit before enabling a user path. No GUI/accessibility/comprehension claim follows from these source-level tests. Existing selected body/header group spacing retains its meaning, aggregate removal remains separate, and issue29 is incomplete. + +## Executed findings and negative design decision + +Read back `.local/scenario-group-gates/reorder-interaction-test-1/result.json`, `source-proof.json` and the executed test names/result in `stdout.log`. The parent-run command was `cargo test -p oa-desktop reorder_ --message-format=json -- --nocapture`, with exit0, unchanged inputs, and **22 passed / 0 failed / 0 ignored** at commit **cae7a22297d09810c412dbdf4ce5d8a28164b247**. The source-proof record reports all **814 captured inputs** matching that commit's exact Git blob bytes. This subagent inspected those records; it did not rerun Cargo or the source-proof helper. + +All four named slot experiments and all five new session tests executed successfully. The unmodified equal/coincident fixture passed both core admission and actual requested Saved `Ready` presentation. Thus the missing BeforeC target is a real production geometry/picker combination, not an invalid-coordinate or fallback artifact. The full-model chooser BeforeC and nearest BeforeB changes passed their distinct literal expected orders. Crossed slot ordering, close continuous reachability versus integer-pixel samples, and the explicitly fixed-viewport offscreen result also matched the table above. No fixture corrections were needed. + +**Reject an unmodified nearest-Y fixed handle as the sole exact-destination method.** In the admitted equal-Y case, BeforeB and BeforeC have identical distance functions; the current stable tie winner makes BeforeC unreachable. Reversing that winner would merely make BeforeB unreachable instead. Pan, uniform zoom, a clearer hover label, or a different fixed tie-break cannot distinguish identical coordinates. Recovering both choices requires explicit identity disambiguation, a semantic destination strip with distinct targets, or another mechanism that introduces a choice beyond nearest original Y. This rejects equivalence of that particular sole-input method, not every possible canvas drag design. + +An exact identity chooser remains the recommended next comparator, **not accepted UX**. These tests do not establish discoverability, direct graphical editing quality, keyboard behavior, result comprehension, nested/offscreen navigation policy, full App lifetime admission or native usability. They also do not claim every root append slot or nested wrong-parent case was covered. The bounded reachability question is resolved without broadening tests; the shared session and a concrete chooser/strip prototype still require their own integration and user-workflow evaluation.