Submodule consistency today is recomputed ad-hoc per verb by re-parsing the parent tree + .gitmodules; this ticket pins it to FOUR cheap reflog/wtlog records and one shared classifier, so be status/head/get/post all judge "dirty" the same way. The model also fixes SUBS-026 (the ahead-of-pin case reads clean) by giving status a record to compare against. See Submodules, Store.
Four records describe a mounted sub's state; each lives in a known file, no tree walk needed:
refs (.be/<parent>/refs): <sub/path>?<parent/branch>#<full_hash>. A denormalized mirror of .gitmodules path + the 160000 gitlink, so verbs avoid re-parsing the tree/.gitmodules.refs (.be/<sub>/refs): ?.<parent>/<branch>#<full_hash>. The keep-alive dot-branch tip on the sub's side (the parent-driven POST anchor; ?/<sub>/.<parent> seen from the parent)..be/wtlog: the last get/post row, ?<parent/branch>#<base> — the parent wt's branch + base hash.<wt>/<path>/.be: the last get/post row, ?<branch>#<base> — the sub wt's branch + the actually-checked-out sub commit.beagle in beagle-journal): R1=5a95cd15, R4.base=3b41fa69 (advanced), R2 ABSENT (no ?.beagle-journal dot-branch), R3.branch=trunk, and R4 was recorded as an unbound ?#hash want — so status read clean (SUBS-026).SUBSDirty-style) returning the dirty set from the four records (+ the sub's file scan), reused by status/head/get/post instead of per-verb tree re-parsing.abc/.
Coherent ⟺ all agree: R1.hash == R2.hash == R4.base, the sub file-scan matches R4.base, R2.<parent/branch> == R3.branch, and R4 is title-bound (not a bare ?#hash). THREE orthogonal axes; only the first is "dirty":
Dirtiness scale — un-recorded FORWARD state a POST must capture; a 2-bit mask, RECURSIVE (a node carries the bit if it OR any descendant does):
Stale axis — checkout is OFF the pin but NOT forward; GET/PATCH resolve it, POST never does:
Record-health — orthogonal to both; the pin/anchor plumbing itself is broken:
?#hash want with no title → decoupled, status shows a blank pin (get-records-blank-want, SUBS-028).Recursive verb semantics — the recursion is RETURN-CODE driven; the parent inspects each sub and decides:
be get (pre-order, R1→R4): moves each sub to the pin of the parent commit being got, peels off local uncommitted edits and REAPPLIES them onto that pin (3-way; already implemented for subs), re-anchors R4=R1, attaches R2. --nosub opts out (separate story). The committed version is never "lost" — it stays a reachable commit; only the wt moves.be post (post-order, R4→R2→R1) per sub: returns OK (a commit was made) → parent reads the sub's new tip from R4/anchor and stages the gitlink bump; POSTNONE (nothing to do) → parent stages no bump (ignore it); error → parent HARD-ABORTS the whole post before committing anything — no partial pin set (the no-silent-half-apply invariant).--force, to avoid quiet commit loss. The post-commit new-tip ≠ old-pin is the EXPECTED bump, not the err condition — evaluate the check on entry state, the bump on exit state.So a healthy post drives R4→R2→R1 bottom-up; a healthy get drives R1→R4 top-down.
SUBSDirty(parent, subpath) -> {dirty, stale, health} reading R4/R2 + pin by REAL ancestry; dirty mask = Edited|Advanced, stale (behind/diverged) and health (synth/unbound) kept separate. (Edited compute + coord-mismatch are placeholders → SUBS-027b.)be status through the classifier (mod from the dirty mask alone). head/get/post routing → SUBS-027b.test/status/02-sub-pin-classify (Advanced→mod, clean→no-flag). behind/diverged/error + record-health repros → SUBS-027b.—
be status Advanced-detection landed on the beagle trunk: 1585ac47 (repro test/status/02-sub-pin-classify) + 4ae70e29 (SUBSDirty 3-axis classifier in sniff/SUBS.{c,h} + CLASS_BASE_ONLY mod-flag routing in sniff/SNIFF.exe.c).be head pin axis, and the get/post test gaps.