Beagle SCM
GET-051: cheap pre-discard guard in every tree update — size-check a file before taking theirs/ours over it
Context
Ordered by gritzko 2026-07-19 after the STATUS-008 data loss: classify false-cleaned dirty files, so today's bare get in //STATUS-008-f21 took the base version of core/loop.js and silently discarded an uncommitted edit (blob in no store, unrecoverable). BOTH lost/spared files differed from the baseline IN SIZE (29376 vs 29343 B; 19078 vs 16770 B) — a size compare alone would have refused the discard.
- The classification layer can lie (STATUS-008 live repro; GET-050 stale stamps false-clean too) — so the OVERWRITE/DISCARD site needs its own independent guard, not another upstream scan.
- Full read+hash of every touched file on every update is rejected up front: too slow ("reading and hashing will slow everything down" — gritzko).
Goals
Every tree-update write path (get checkout/weave, post, any theirs/ours decision that discards on-disk bytes) double-checks, immediately before overwriting or discarding a file it believes clean, that the on-disk file actually matches the presumed baseline blob — by SIZE at minimum.
- Cheap by construction: stat().size (already in hand or one syscall) vs the baseline blob's stored length (known from the object header/index) — O(1) per file, no content read.
- On mismatch: the file IS modified — never silent-discard; divert to the weave/conflict path or refuse loudly (plain words), naming the file.
- One shared helper at the write seam, used by get AND post — not per-verb copies.
- Known residue: a size-preserving edit slips the cheap gate — that class stays with hashing paths / GET-050 band stamps; optional strict mode may hash ON SIZE MATCH only, never by default.
Constraints
- The guard sits at the write/discard site (the last touch point), independent of classify's verdict — it must fire even when classify is wrong, which is the whole point.
- No new tree scan, no mtime reliance (mtime is what fooled status); size vs stored blob length only.
- Repro-first: the STATUS-008 shape as the red test — dirty file + false-clean classification + upstream change → today's code discards; guarded code refuses/weaves.
- Related: GET-050 (stamp bands), STATUS-008 (the incident + live repro tree — do NOT use
//STATUS-008-f21 itself as a test bed).
WIP
Design decisions
TODOs
Blockers and bummers
- 2026-07-19 scope note from GET-052 (NO-REPRO, DONT): TIP get's decision is already content-based (checkout.leafUnchanged full-read, get.js:1024/1032) — the stamp fast path never gates it. This guard's remaining value: the POST paths, an assert-shaped invariant at the write seam (catches future regressions and any misclassification class), and users running old-code trees via the jsrc climb (the actual STATUS-008 loss vector).
Outcome