POST-005: commit-all POST sweeps an untracked file whose mtime aliases a stamp (LANDED)

LANDED eec94934 (2026-06-06). Fixed via the absorbed-tree (theirs) ULOG cursor described in §"Root cause / fix"; repro test/post/28 green and off WILL_FAIL; full ctest 298/298.

HIGH — silent data inclusion. In commit-all (implicit) mode, be post commits an untracked file the user never staged, purely because the file's mtime collides with an in-scope put / patch row's timestamp. be post classifies each on-disk file by looking its mtime up in the wtlog via SNIFFAtRowAtTs (sniff/AT.c), which resolves a row by timestamp alone and never checks the row's path. An untracked sibling whose mtime aliases a put/patch/mod stamp is therefore misattributed as that row's work and ADDed to the commit (sniff/POST.c post_classify_step, the SNIFFAtKnownSNIFFAtRowAtTsPOST_V_ADD branch).

Two variants, one root cause:

Repro (deterministic, local — no submodule)

  1. trunk: commit tracked.c.
  2. child branch b: add feature.c, commit; back to trunk.
  3. be patch ?./b — absorbs feature.c, stamps it with the patch row ts.
  4. printf ... > junk.txt; touch -r feature.c junk.txt — alias the stamp.
  5. be post '#absorb b' — commit-all (a patch row is in scope).
  6. be tree:?<tip> shows BOTH feature.c (correct) AND junk.txt (BUG).

Expected

Commit-all = git commit -a: every dirty tracked file plus files genuinely introduced by an absorbed patch land; an untracked sibling never does unless explicitly be put-staged. feature.c (in the absorbed tree) commits; junk.txt (in no absorbed tree) does not.

Actual

junk.txt is committed because its mtime aliases the patch stamp and POST trusts the ts→row lookup without verifying the row pertains to this path.

Root cause / fix

SNIFFAtRowAtTs is a ts-keyed lookup; the stamp model assumes mtime uniquely identifies the owning row, but ms-granularity mtimes alias (same DIS-023 class). The clean fix procures the absorbed (theirs) tree(s) as a ULOG from keeperKEEPCommitTreeSha on each in-scope SNIFFAtPatchEntries sha, then KEEPTreeULog — and feeds it into the existing N-way classify merge as a 5th cursor (a thr verb, exactly like the baseline base cursor bu). An untracked file (!src_base) reaching the stamp-owned ADD branch is then committed only if it carries a src_thr record (i.e. it is genuinely in an absorbed tree); otherwise it is an alias and is ignored. No separate lookups, no parallel path-set — membership rides the merge. Covers both variants: the put-alias post has no patch rows, so the theirs cursor is empty and the untracked file is never ADDed.

Tests

test/post/28-untracked-alias-not-swept — deterministic local patch-alias repro (touch -r); WILL_FAIL until fixed. The put-alias variant is covered end-to-end once SUBS-002 unblocks test/patch/28.