Beagle SCM
SUBS-054: submodule pins as wtlog rows — the parent appends the gitlink to the CHILD's wtlog (no-slash = the external pin)
Context
A worktree tracks whatever it was told to track. For submodules, the parent makes its own get/post, takes the gitlink hash, and appends it to the CHILD's wtlog as an external pin — the sub's local URI with NO trailing slash. The sub appends its own get/post records to the same log, with a trailing slash. Projects pin children, children pin grandchildren. Under the default course of events (full-depth commits, all changed subs) the two hashes are equal and the rows go in pairs — get/get, post/post. An unpinned sub makes them differ; that difference is what status renders. Supersedes SUBS-040; implements URI-016 decisions 5/8/9/10/11.
ONE CHANGE, THREE TICKETS (gritzko, 2026-07-15): DIS-061 + URI-016 + this are parts of the same change and land together — DIS-061 splits commit from branch-advance (the uniform flip), URI-016 removes the dot-branch and puts pins in wtlogs, SUBS-054 is the sub-side row scheme. Landing any one alone is what reddened 40 cases (verbs/post/post.js:726-738 backs the flip out as "unlanded WIP"). DIS-061's 2026-07-13 pin-branch EXEMPTION from the uniform rule dissolves here: with no pin-branches there is nothing to exempt, and the rule becomes literally uniform.
- Pin rows, both in
<wt>/libdog/.be — ts get ///jab/libdog?proj_branch#pin (parent, external) vs ts get ///jab/libdog/?branch#base (sub, own).
- The sub's OWN row carries the worktree's TWO parameters, which are different things: the ref it TRACKS (
?branch) and its BASE commit (#hash) — see worktree.
- A sub MAY track something other than the parent's pin (a remote, say); normally it tracks the pin, and then the URI structure is the same except the slash.
?branch is meaningful but not greatly useful here: only worktree check-out pins are tracked, never every branch.
- Current impl instead tracks sub tips by synthetic dot-branches
/<sub>/.<parent>/.<grandparent>/<gp_branch> (Submodules intro, "Store layout"), which URI-016 decision 5 removes.
- Current spec couples parent and child: "the pin-branch tip IS the gitlink in the immediate parent's base commit; the parent refreshes the ref on any move of its base" (Submodules).
- That coupling is the one carve-out in the POST ruling — "the two motions never couple (pin-branches are the exception)" (POST) — and it dies here.
- Established row shapes today: the parent's own wtlog carries gitlink rows in the no-slash form (
26625Ilq3d put beagle#f425d6a8… in the meta-project .be), and a sub logs its own moves branch-relative (267105jnCd get ?#ac3a6d3e… in be/.be).
- SUBS-040 filed the same ground 2026-06-29 against the dot-branch and was rescoped onto wtlog rows 2026-07-14; it is STALE in favour of this ticket — do not re-fix its I1-I3.
Goals
- A sub's pin lives in the sub's own wtlog as rows, not as a synthetic branch ref anywhere.
- The parent writes ONLY the no-slash external-pin row, on the parent's own get/post; the sub writes ONLY the trailing-slash own row, on its own get/post.
- The sub's own row records track and base separately, so a sub tracking a remote instead of the pin is expressible without a new mechanism.
- Ahead/behind and the post/miss lists derive from last-no-slash vs last-slash in ONE log — no cross-log or cross-ref lookup.
- Existing dot-branch trees convert transparently on the switch: same hashes, only the authority/path part of the URI changes.
- The POST pin-branch exception is gone: a parent's base move no longer refreshes any ref the child owns.
Constraints
- Repro tests first (work); fix in beagle-ext (JS), no C changes.
- Conversion must be transparent — no user-run migration step, no reclone, no hash churn.
- Spec pages are gritzko's (Submodules, POST, Worktree carry his byline): request his edit, do not rewrite them.
- Minimal diff; no edits to unrelated tests.
WIP
Design decisions
-- gritzko (rulings, 2026-07-15)
- Both row kinds live in the SUB's own wtlog
<wt>/<sub>/.be, interleaved by timestamp, told apart by PATTERN not by log (URI-016 decision 9).
- The trailing slash is the discriminator:
///path/to/sub?proj_branch#pin is the external pin (parent-written), ///path/to/sub/?branch#base is the sub's own record.
- The parent, on its OWN get/post, reads its gitlink hash and appends the pin row to the child's wtlog — parents pin children, children pin grandchildren, one level each.
- The sub's own row is the worktree pair: tracked ref in the query, base commit in the fragment — two different things, never conflated.
4a. A URI maps worktree/branch -> hash: the hash is ALWAYS the #fragment, the query is the tracked BRANCH. An empty query is not written at all (?#sha is a composer artifact of URI.make(…, branchKey || "", sha), not a tracked branch).
4b. Corner case (gritzko, 2026-07-15): in the DETACHED state the query may itself carry the hash (?7d37f90f, URI L50-51) — the tracked ref IS a hash. Normal rows keep the hash in the fragment.
- Tracking the parent's pin is the normal case but not the only one (a sub may track a remote); same URI structure either way, the slash tells them apart.
- Pairs (get/get, post/post) are the default full-depth flow; divergence means the sub is unpinned, and status shows ahead/behind plus the post/miss lists.
- Conversion keeps the hashes and changes the authority/path only — a dot-branch pin becomes the no-slash URI form of the same hash.
- An off-pin sub is left ALONE: the parent does not recurse get/patch into it and reports
adv from gitlink != base (URI-016 decision 11).
TODOs
Blockers and bummers
- Not investigated: whether the conversion can read every dot-branch shape in the wild, incl. sub-of-sub
/<title>/.<parent>/.<grandparent> (Submodules "Store layout").
test/sub/pinref inspects the pin as a synthetic-branch REF; its assertions survive verbatim but the mechanism under them becomes a wtlog row — rewrite onto rows, do not retire (test edit needs gritzko's say-so per work 4).
Outcome