The be/ JS port treats submodules as a READ-time concern only: the recursing READ views (status, diff, ls/lsr) walk mounted subs in-process (core/recurse.js, views/status/status.js, views/diff/diff.js, views/ls/ls.js) and classify gitlink-vs-tip dirtiness with a full 3-axis SUBSDirty model (shared/subs.js), but the STATE verbs do NOT implement Submodules' "seamless recursive experience where clone, checkout, and commit descend into subs by default". get records a 160000 leaf and never fetches/mounts/checks-out the child (get.js:607 "gitlink: recorded, not written"; checkout.js:138-140); post/put/delete skip gitlink subtrees and never run a child commit (fold-decide.js:154-201, stage.js:149/261, delete.js:78); there is NO synthetic/dot-branch creation or tracking anywhere; the Title-from-.gitmodules-URL-basename rule and the .gitmodules-URI fallback fetch are absent; and per shared/subs.js the dirty Edited axis (sub-local file edits ≠ R4.base) is INERT, so a parent is NOT marked dirty for an uncommitted edit inside a sub — contradicting Submodules' "dirty if contains anything dirty". This is an AUDIT/umbrella ticket: it inventories the gaps D1..D14 and confirms two REVERSE cases where the JS exceeds the spec (the 3-axis classifier; the behind/diverged stale axes) so Submodules is the laggard there. Individual fixes should spawn their own SUBS/DIS sub-tickets. Scope is be/ only (not beagle/); sibling get/post audits are DIS-055/DIS-054; the dirty-classifier sibling is DIS-057; method per CLAUDE. See Submodules, Dirty, Store, Title, SUBS-027b, SUBS-037.
be/ against Submodules (every bullet + the Recursion / Store-layout sections), verified by reading shared/subs.js, core/recurse.js, shared/checkout.js, shared/store.js, shared/stage.js, shared/wtlog.js, verbs/get/get.js, verbs/post/post.js + verbs/post/fold-commit.js + verbs/post/fold-decide.js, verbs/put/put.js, verbs/delete/delete.js, views/status/status.js, views/diff/diff.js, views/ls/ls.js, views/lsr/lsr.js — read, NOT run (TRACE-LEVEL throughout; nothing here was executed)..gitmodules DECLARATION order with a tree-gitlink + <sub>/.be-mount gate (recurse.js:74-89); status.js recurses subs DEPTH-FIRST and aggregates each sub's hunk into the parent report (status.js:301-323); diff.js recurses bumped gitlink pin-ranges (diff.js:275-303); ls/lsr cross store boundaries into mounted subs (ls.js:4-7,180-182). subs.js::enumerate walks the baseline tree for 160000 gitlinks and classifies each mount (subs.js:120-139).require subs/recurse in get.js/post.js/put.js/delete.js beyond classification helpers). The "seamless recursive clone/checkout/commit" is the central spec promise and is unimplemented for the verbs that change state.kind==="s" → return (get.js:607), checkout.js skips kind==="s" (checkout.js:138-140). No child get be://store?/submodule is invoked in a subdir (the spec's URI-recursion-driven descent, Submodules bullet 2). The reconcile/leaf fan-out is single-store only.leaf.kind === "s", store.js:510; classify.js:235 records pin: b.sha) but is never USED to check out a child — there is no child checkout..gitmodules URI fallback — OMITTED (HIGH): Submodules §1 "tries to retrieve the child files from the same source, falls back to .gitmodules URI". No .gitmodules-URL parse for a FETCH anywhere (only views/status read .gitmodules for the recurse ORDER, not for a URL); wire.js is never called per-sub. Absent./child/.parent/parent_branch". No synthetic/dot-branch is written or tracked on get; grep for .parent/dot-branch ref writes finds none in the state verbs (only get.js writes a normal ?<branch>#<sha> wtlog row for the PARENT). Absent.subPrefixes, fold-decide.js:154-201) and never recurses into a sub to commit it. Absent.be put <sub>#<40-hex> records the pin (fold-decide.js:178-184, SUBS-019) and post.js commits that 160000 entry into the parent tree (fold-commit.js MODE.s=0o160000, buildSubtree). So the parent CAN record a sub commit, but only one the USER supplied by hand — there is no AUTOMATIC "commit the child, then record its new hash" (which is D6's job). Divergence: the recording exists, the post-order auto-derivation does not.?<branch> ref via advanceRef/store.set, post.js:416). Absent (twin of D5).b.kind === "s" → continue, stage.js:149 bare walk, stage.js:261 dir-form); delete.js skips gitlink leaves (delete.js:78). A be put <sub>/file is NOT delegated to the sub. (The MANUAL be put <sub>#<sha> gitlink bump from D7 is the only sub-touching put.)verbs/head was located in the be/ tree during this audit (head is referenced by DIS-053 as a native concern). If a JS head exists it inherits get's no-descend gap (D2); confirm whether a JS head verb exists and whether it recurses — currently treated as absent..be/<title>/, Title from .gitmodules URL basename — PARTIAL/OMITTED (MEDIUM): Submodules Store-layout "the sub's objects sit in .be/<title>/, same level as the parent's"; "Title from the .gitmodules URL basename, .git/trailing-/ stripped". store.js::shardDir resolves a flat <store>/.be/<project>/ shard (store.js:124-148) and subs.js opens the sub's sibling shard by subRepo.project (subs.js:89) — so the FLAT-same-level layout is consistent for an ALREADY-mounted sub. But because no verb CLONES a sub (D2/D4), nothing ever DERIVES the Title from a .gitmodules URL basename (grep for basename/.git-strip of a sub URL: none). The read path assumes the shard already exists with a title; the create path is absent.<wt>/<path>/.be (the mount anchor / R4 tip) — IMPLEMENTED on READ, not on WRITE (LOW): Submodules Store-layout "submodule worktree log is at <wt>/<path>/.be". be.find (core/discover.js) treats a .be FILE as a secondary-wt wtlog anchor whose row-0 redirect names the store+project; subs.js::classifyMount reads R4 (the sub-wt cur tip) through it (subs.js:59-75); recurse.js gates on <sub>/.be being a regular file (recurse.js:32-34). So the anchor is CONSUMED correctly. But it is never WRITTEN by a JS get (D2: no sub mount is created), so the mount only works for a sub mounted by native be.ok, NOT dirty — directly contradicting the spec's "dirty if contains anything dirty". The ADVANCED axis (R4 tip descends the pin → adv) IS detected (subs.js:96-100). Recursive aggregation partially compensates: status.js recurses and the sub's OWN hunk would show the sub's edited files — but the PARENT's gitlink row stays ok, and a --nosub run (or a non-recursing consumer) sees nothing. Mirrors native C until SUBS-027b; cross-link SUBS-027b.adv (the only dirty axis), BEHIND→stale="behind", DIVERGED→stale="diverged", EQUAL→ok, with a conservative-Advanced fallback when the sub shard is unresolvable (subs.js:89-90,102-111). Submodules only says "dirty if contains anything dirty" and names no behind/diverged/adv distinction. The spec lags; it should document the adv/stale taxonomy (and the adv row + sub-tip ts, SUBS-030). Cross-link SUBS-037.--nosub opt-out IS implemented for the READ views (status.js:74 --nosub; diff.js:283 --nosub) and is DEFAULT-ON recursion there (status.js:64-74) — matching Submodules bullet 7. But Submodules does not state that --nosub is read-view-only in the JS; the spec implies it gates ALL verbs. Since the state verbs never recurse (D1), --nosub is a no-op for them. Note for the spec + the eventual state-verb recursion: wire --nosub through get/post/put/delete when D1-D10 land. (Also note DIS-053: a native URI-less be get --nosub regressed a sub wt — a related hazard.)be patch <commit> skips every gitlink — patch.js:89 reads a kind === "s" leaf as ABSENT, and patch.js:222-225 short-circuits any ours/theirs/wt gitlink with st.noop++; return ("gitlinks never go through the blob merge — parent recursion re-gets the pin in native"). REPRO (this ~/todo/DIS-058 tree): be patch ?34574bce over a commit that advances the test sub left test/ UNTOUCHED — no pin bump, no sub-file merge, no status row; the put test#b617054b wtlog row was a SEPARATE manual stage, NOT patch's. So an absorbed commit SILENTLY DROPS all submodule changes. Twin of D10 (put/delete no-descend) for patch; spin out repro-first (with SUBS-039).be self-locator as a MOUNT → bogus adv — CODE GAP (MEDIUM, read-side twin of GET-037): jab status prints adv be for the be -> . self-locator because views/status/status.js:414 isMount (and core/recurse.js:35) call io.stat(<wt>/be/.be) — which FOLLOWS the symlink to <wt>/.be (a regular file) — so the self-locator reads as mounted, classifyMount runs, the project's own cur tip descends the baseline self-pin → adv. shared/subs.js:47 isMountAt has the correct NOFOLLOW lstat==="lnk"→false guard (GET-036/SNIFFSubIsMount); the two isMount copies do NOT. C be shows mod be, so JS/C disagree. Fix: add the lstat lnk NOFOLLOW guard to both isMount copies.ok64 — a UX nicety to PRESERVE in any refuse path added for the unimplemented sub forms.~/todo/DIS-058 changeset, awaiting the commit gate)shared/submount.js mount() + get.js mountGitlink/recurseSubMounts: pre-order child fetch (same-source + .gitmodules-URL fallback, Title from the URL basename), checkout at the parent pin, and the sub wtlog anchor <wt>/<path>/.be written on the synthetic branch /<title>/.<parent>[/<branch>].postTree→postSubs→postOne: post-order child commit FIRST, automatic gitlink-bump into the parent, synthetic-branch update; fold-commit.js skips the blob read for a 160000 add..gitmodules URL on clone, flat sibling shard at .be/<title>/, sub wtlog anchor written by get.be self-gitlink (no .gitmodules) materialises be -> . instead of sub-mounting; no partial checkout / raw ENOTDIR on a sub-mount failure.be: local clone never worked (wire.js classify routed it to ssh); now a local keeper exec, so the local cycle repro is possible.be-js-sub-cycle (clone→edit→commit→re-clone) + be-js-sub-selflink; ctest otherwise green (2 pre-existing fails).seedLocal now resolves <wt>/.be's row-0 redirect to the REAL store anchor (friendly GETWTSRC refuse if none, no partial wt); jab status classifies instead of all-unk. Test be-js-sub-wtsrc.test/post/move + test/post/patch-pat and the changeset landed (da0c12c3+da7fa5a7, 29Jun); d80bdb5c TEST-003 "js tests pass".be/ submodule handling meets, diverges from, or omits Submodules — so each gap can be lifted into a focused SUBS/DIS sub-ticket with a clear spec citation + code site.--nosub scope) so the spec stops lagging the code.be/ (JS ext / JABC) ONLY — do not touch beagle/. Pure JS over libabc+libdog (+libdog wire for fetch/push).be/test/ cases per discrepancy (a RED test proving e.g. be get of a repo with a sub leaves the sub unchecked-out; a parent reading ok over a dirty sub edit — D14) BEFORE any fix..gitmodules-order gate, status/diff/ls/lsr sub relay + aggregation, the 3-axis classifier, the manual be put <sub>#<sha> gitlink bump (D7) and its post-commit into the parent tree.ok64.walk + joinPrefix + the .gitmodules-order/mount gate rather than re-rolling a parallel walk.be put <sub>#<sha> bump as the primitive and layer the automatic post-order bump on top (post recurses, commits the child, then synthesises the same put <sub>#<newsha> bump), so the tree-build path stays single.be-js-sub-cycle/be-js-sub-selflink prove <sub>/.be + child files land). DONE.be-js-sub-cycle). DONE.be: local clone. DONE.jab status all-unk) — landed with the DIS-058 changeset (da7fa5a7).1c4a97c8 (staging files in submodules) + SUBS-044 a715b0d3 (put.js:436-449 bare-put pre-order sub descent, delete.js:67-94 bare-delete sub sweep into the SUB's own wtlog, sub-crossing path delegate put.js:515). SUBS-039 one-liner is stale — mark it landed there too.be patch sub descend LANDED (da7fa5a7): patch.js:69-73 routes gitlinks off the blob merge, patch.js:305-328 recurses be patch into each MOUNTED advanced sub post-order + records the D7 gitlink bump (mirrors post.js postSubs).adv be self-locator FIXED via SUBS-045 36b1aec0, mechanism differs from the NOFOLLOW plan: classify.js:194-196 gates sub-classification on .gitmodules DECLARATION (an undeclared base-gitlink like be -> . is never sub-classified); recurse.js:36 also grew the lstat lnk guard. NB status.js:423 isMount still stat-follows, but undeclared gitlinks no longer reach it. SUBS-043 one-liner is stale — mark it landed.ok (should be dirty); shared/subs.js:24-34 still declares the Edited axis INERT (verified 2026-07-07). Open; with SUBS-027b.4370d67e, push cb3881f8, GIT-013 pushRemote post.js:298), but post.js:433 still gates !isPush so a Host-slot post NEVER descends subs — the Submodules §2 per-sub same-remote push remains absent. Open.verbs/head/head.js EXISTS and does NOT recurse subs (no recurse/subs use, verified 2026-07-07) — the Submodules §4 "HEAD pre-order, same as get" gap is real. Open.--nosub in Submodules (wiki/Submodules.mkd:13 still generic, verified 2026-07-07). Open.4370d67e client, cb3881f8 git protocol push, GIT-013 pushRemote in post.js; test/post/slots asserts the Host-slot push). Remaining D8 work is only the per-sub descent on a push (post.js:433 !isPush).mountSub in no tree, nothing in the store) — so D1-D9 are NET-NEW here; DIS-058 owns the redo, SUBS-037 is superseded.be get --nosub regressed a sub wt to its stale .be pin) — keep in view when wiring --nosub through the JS state verbs (D16).da0c12c3 "DIS-058: submodule handling as per the spec" + da7fa5a7 "DIS-058: implement more submodule handling" (29Jun) — the get/post recursion core: D1 (get/post), D2-D5 (get descend/fetch/checkout/synthetic-branch), D6/D7-auto/D9 (post-order child commit + auto gitlink-bump + synth-branch), D12/D13 (Title-on-clone + sub wtlog write), D17 (be patch sub descend, patch.js:305-328), GET-037/GET-038/JS-098; the DIS-057-goldens merge blocker cleared (test/post/{move,patch-pat} in-tree).1c4a97c8 + SUBS-044 a715b0d3 (put/delete sub delegation + bare-verb sub sweeps); D18 via SUBS-045 36b1aec0 (.gitmodules-declaration gate, classify.js:194-196 — no bogus adv be); SUBS-041 nesting cap 6388c34e; GET-040 sub-reset recursion fix 0092271c.4370d67e/cb3881f8; post.js:433 !isPush still skips postSubs); D11 HEAD no-descend (verbs/head/head.js exists, no recursion); D14 the INERT Edited dirty axis (shared/subs.js:24-34, with SUBS-027b); D15/D16 Submodules spec updates (adv/stale taxonomy + --nosub scope).