Module map: where shared code lives
A discoverability contract for the four "shared code" directories, so future growth has a rule to check against instead of regrowing a second grab-bag. Decide where a new helper goes by its dependencies and its consumers, not by vibe.
src/lib/— the single home for generic cross-cutting utilities with no dependency oncore/,schemas/,connectors/, orwebapp/. It is the lowest layer; everything may import it and it imports nothing upward (enforced by keeping it a leaf — verify withpnpm lint:circular). Examples:content-hash,mimetype,file-exists,public-url,sleep,atomic-write(writeFileAtomic, crash-safe whole-file replacement for small state and credential stores),awake-timeout,git*/paths/box-shape(promoted fromcli/libin the Track G reorg),time(getBoxTime),format(chalk),box-config. Check here before writing your own — a hand-rolled copy of something already inlib/is the regrowth pattern this directory exists to prevent.src/shared/— code shared specifically between the frontend and the backend (isomorphic; must run in the browser bundle and Node). It may encode a small amount of domain knowledge (tool names, card-name parsing, markdoc config, nav routes) but must stay bundler-safe (nonode:builtins, no server-only deps). If a helper is backend-only, it belongs inlib/orcore/, not here. Ashared/module may import a bundler-safelib/module (e.g.lib/invariant.ts, which is dependency-free) —lib/is the lower leaf layer, soshared/ → lib/is a downward edge, not a cycle. It may NOT importcore/,schemas/,webapp/, or anynode:-touchinglib/module. Ref/path algebra lives here, in exactly one module:shared/ref-path.ts(parseRef,resolveRefPath) owns the 3-form rule and fail-closed containment for every in-box ref, backend and frontend alike — building onshared/attach-path.ts(attach-scope naming) andshared/box-path.ts(the box-relative canonical form). A new consumer imports it; it never re-derives the rules withpath.resolveor a segment split. Frontend-consumed leaf helper? When a dependency-freelib/helper (e.g.is-record,invariant,error-guards) is also needed by the browser — which the import-boundary rule forbids from reaching into backendlib/— keep the implementation inlib/(solib/stays a leaf) and add a thinshared/<name>.tsthat re-exports it as the@shared/<name>entry point. Do NOT invert this by moving the impl intoshared/and re-exporting fromlib/: that makeslib/import upward intoshared/, breaking the leaf invariant.src/types/— ambient.d.tsdeclarations only: module augmentations and global/ambient types (e.g. thebeebox/view-widgetsspecifier surface). A real module that exports runtime or interface values does not belong here — it goes incore/(domain contract) orlib/(generic).types/views.tswas the counterexample that moved tocore/views/types.tsbecause it importedcore/(a layer inversion).src/cli/lib/— CLI/session-domain helpers coupled tocore/, not generic. What remains after the Track G promotion is session-transcript parsing (session*.ts, which importcore/chat/*) andfetch.ts(importsschemas/). Because these depend upward oncore/schemas, they can't live in the dependency-freelib/. Don't add a generic utility here — that's what re-created the "secondlib/" the reorg collapsed; put generic helpers insrc/lib/.
Rule of thumb: no core deps → src/lib/; frontend+backend both → src/shared/; ambient .d.ts → src/types/; core-coupled CLI/session helper → src/cli/lib/.