Today status and post classify dirtiness through TWO separate ladders over shared primitives: status/ls:/spot use shared/classify.js (classify/classifyDir, the JS twin of sniff/SNIFF.exe.c::status_step), while post uses verbs/post/fold-decide.js (decide, the JS twin of sniff/POST.c::post_classify_step). Both re-run their own base ⊕ wt ⊕ put ⊕ del merge and emit different vocabularies (display buckets vs keep/unlink/add), so they can drift — and fold-decide THROWS on an in-scope patch row (POST-005 / JS-051 "theirs" 5th input), so the pat/mrg/cnf states Dirty requires aren't even commit-able. The rework: ONE classifier built as an N-way merge of four ulogs — the *base tree, the put list* (wtlog), the *work-tree* scan, and any *patched-in (theirs) trees* — whose OUTPUT is itself a ulog (per-path bucket + resolved sha/mode). status renders that output ulog; post consumes it (bucket → keep/unlink/add). Folds in the Dirty deltas: the rmv/mov move pair, pat/mrg/cnf (theirs ulogs + a patch-written stamp offset), and conf→cnf. ULOG is already the substrate (JS-031 classify; shared/ulog.js each/drain/write). Scope be/ only. See Dirty, STATUS-002, POST-005; method Issues.
status/ls:/spot → shared/classify.js; post → verbs/post/fold-decide.js. They share only classify.wtScan/wtEqBase and both independently read the base tree + the wtlog (eachPutDelete, boundaries) — same inputs, two routings → drift risk.fold-decide.js explicitly does NOT handle the patch "theirs" input: an in-scope patch row THROWS (POST-005, JS-051 "out of scope"). So a patched tree cannot be committed and pat/mrg/cnf have no commit path.shared/ulog.js (each/drain/write/append) over the abc ULOG family; wtlog.js is a ULOG reader. A git tree, the wt scan, the put/delete list, and a patch's theirs tree are ALL path-keyed ulogs — the classifier is their merge.rmv(source)+mov(dest) move PAIR; pat/mrg/cnf for patch-derived files; the conflict code spelled cnf (code currently conf, get.js:634, patch.js:292).wtEqBase, re-hash not mtime), ok count-only + the eq listing row, .gitignore/meta/nested-repo + submodule-prefix pruning, and post's byte-identical commits for non-patch cases.pat/mrg/cnf stamp axis is NON-FUNCTIONAL — after be patch ?34574bce cleanly takes-theirs views/diff/diff.js, jab status shows mod views/diff/diff.js, NOT pat (the headline DIS-057 deliverable). The patch verb stamps the wt mtime to ts-2 (verbs/patch/patch.js:399-413, io.setMtime(path, ts-2n)) and shared/classify.js:155-165 patchStamps reads the band back at ts-2 (consumed 348/374-380), yet the lookup MISSES → the file falls through to mod (classify.js:385). Suspects, repro-first: (1) io.setMtime↔io.lstat ron60 round-trip precision loss (the on-disk mtime read back as a clean …719000000 ns, sub-ms truncated → w.ts != ts-2); (2) the PERSISTED patch-row ts drifting from the ts used to stamp (a re-nowAfter(tail) at append); (3) patchFloor() scoping the patch row OUT. Fix lands in ~/todo/DIS-057, repro-first (CLAUDE §17).1. base tree ulog — the baseline commit snapshot, path → {sha, mode}; 2. put list ulog — the wtlog put/delete/move staged intents (already a ulog); 3. work-tree ulog — the wt scan, path → {mtime, kind}; 4. patched-in tree ulog(s) — each in-scope patch's theirs tree, path → {sha, mode}, if any.
bucket is the Dirty status (rendered by status), sha/mode is the resolved content (consumed by post). Single source of truth.status becomes a RENDERER over the output ulog (bucket→row/colour, ok count-only, eq for ls:); post becomes a CONSUMER (bucket→keep/unlink/add). No second merge; the POST-005 patch-row throw is subsumed.rmv/mov pair, pat/mrg/cnf, and the conf→cnf rename.be/ (JS ext / JABC) ONLY — do not touch beagle/. Pure JS over the abc ULOG (shared/ulog.js) + libabc/libdog.status and post agree on the SAME tree.test/parity/status) and byte-identical post commits for every non-patch case; land behind the existing post corpora.base ⊕ wt ⊕ put ⊕ del (⊕ theirs).status and post never re-derive — they only map bucket (+ sha/mode) to their own surface.rmv on the source (base-present, wt-absent) and mov on the destination (base-absent, wt-present). BOTH status and ls:/lsr: render the pair (untied from native — RULING 2026-06-29).base stays the ours (pre-patch get/post) tree. So pat = wt == theirs ≠ ours (clean take-theirs), distinct from ok (wt == ours); mrg/cnf = merged bytes ≠ theirs. (The first impl wrongly folded theirs into the baseline via baselineTip, collapsing pat→ok.)pat/mrg/cnf via stamp + theirs ulog: the patch verb (which knows the outcome for free) stamps each patched file's mtime band = pat/mrg/cnf; the merge classifies a wt-vs-ours-modified, patch-stamped file by comparing against the theirs ulog (== theirs → pat; merged → mrg; conflict → cnf). Coarse "patch-derived vs hand-edit" is the load-bearing bit; the stamp carries the refinement with no merge recompute at read time.cnf: rename conf→cnf across be/ (get.js, patch.js, status.js orders, theme.js, tests) in one sweep.adv/stale/health); blocked until Dirty/Submodules resolve the sub axes. Cross-link SUBS-027b.2fdaf80e (shared/classify.js classifyMerge).2fdaf80e (classifyMerge steps 1-4; base = OURS curTip per the 2026-06-29 ruling).rmv/mov pair (classify.js:338/340) and pat/mrg/cnf via stamp (classify.js:155-168) — landed 2fdaf80e+0a8c42f3.status (classify/classifyDir) as a renderer over the output ulog (keep ok count-only + eq listing) — landed 2fdaf80e (views/status/status.js ROW_ORDER incl. pat/mrg/cnf).post (fold-decide) as a consumer of the output ulog (bucket→keep/unlink/add); the POST-005 patch-row throw REMOVED — landed 0a8c42f3 (fold-decide.js header: "No second merge", hasPatch=false, pat/mrg/cnf commit their merged bytes).patch verb: stamp patched files = pat/mrg/cnf — landed 0a8c42f3 (patch.js:281-298, ulog.ronStepMs band: -2 pat, -1 mrg, 0 cnf).conf→cnf across be/ — landed 2fdaf80e (status.js:50-51 orders, theme.js:76-79; no conf bucket remains).rmv+mov + consistent commit (test/post/move); patched file → pat + commit (test/post/patch-pat, test/post/patch-absorb); status/post agree (test/post/agree).d80bdb5c TEST-003 "js tests pass").mod not pat — FIXED in 0a8c42f3: stamp write AND read both step in MILLISECONDS via ulog.ronStepMs (patch.js:281-284, classify.js:155-168), the ns-precision round-trip miss is gone; locked by test/post/patch-pat.post's commit path — preserve byte-identical commits for non-patch cases (parity risk); gate on the post corpora before landing.adv/stale/health axes; track in SUBS-027b / Submodules.ts+1/ts+2 stamp offset rides the mtime stamp-set — confirm it does not perturb the put/post fast-path stamp matching.2fdaf80e "DIS-057: dirty statuses as per the spec" + 0a8c42f3 "DIS-057: unifying the classifier part II" (both 29Jun, be/ repo).status renders it, post consumes it via verbs/post/fold-decide.js ("No second merge"); the POST-005 patch-row throw is gone (pat/mrg/cnf files commit their merged bytes).rmv/mov move pair (classify.js:338/340), pat/mrg/cnf via the patch mtime stamp band (patch.js:281-298 write ⟷ classify.js:155-168 read), conf→cnf rename (status.js/theme.js).mod not pat) fixed in 0a8c42f3: both stamp write and read step in ms via ulog.ronStepMs, killing the ns-truncation round-trip miss.wtlog.attachedBranch unification rode this changeset.