eleven / ocho Public

Clone
git clone https://amadan.net/eleven/ocho

Plain git — no account needed to clone.

Download

Download this file

1# πŸ€– Breaking shell.js into modules β€” the ladder and the shellstate seam
2
3shell.js (the chat page host) started 2026-07-14 at ~12,000 lines. It is being
4split into plain ES modules β€” no build step, verbatim moves, one PR per slice,
5each verified on a pinned canary before the next. This doc records the method
6and the one real design decision (the state seam), so any session can continue
7the ladder without re-deriving it.
8
9## Done
10
11- **Dead code first** (never move what you can delete): the legacy typing
12 pipeline (#911), the feed-cache cluster + `#feed` div (#919), the hidden
13 legacy composer + its staging chain (#931). The "retired original chat"
14 CLAUDE.md used to fence off is now fully gone.
15- **Leaf extractions** (sections with little/no shared state):
16 `theme.js` (#913), `lightbox.js` (#928 β€” deduplicated the byte-identical
17 copy chat.js carried), `media.js` (#933).
18
19## The shellstate seam
20
21Seven mutable bindings are read all over the shell (~700 references):
22`session`, `threads`, `threadKeys`, `activeId`, `ws`, `roster`, `myUserId`.
23They are why the remaining sections can't just move: an ES-module import can
24*read* a live binding but never assign it.
25
26Measured (2026-07-14): only **~14 sites assign a whole binding**; everything
27else reads it or mutates properties (`threadKeys[id] = k`), both of which work
28unchanged through an imported live binding.
29
30So the seam is `web/static/shellstate.js`:
31
32- It **owns** the seven bindings: `export let activeId = null;` etc.
33- Whole-binding writes go through exported setters (`setActiveId(v)`,
34 `setThreads(v)`, …) β€” one setter per binding, assignment-only, no logic.
35- Every reader β€” shell.js and each extracted module β€” imports the bindings
36 **under their existing names**; the ~700 reads don't change. Property
37 mutations on the imported objects/maps stay as they are.
38- Nothing else lives in shellstate.js. It is a place for shared *state*, not
39 shared *behaviour*; helpers keep moving into their own modules.
40
41## The verbs seam
42
43The state seam unblocked modules that *read*; the remaining clusters also
44*act* β€” they open threads, reload the list, raise modals. Those verbs can't be
45imported from shell.js (it's the entry module; importing it back is a cycle),
46and injecting them per-module scales into hook soup.
47
48So the same trick again: `web/static/shellverbs.js` exports one empty
49registry object, and shell.js populates it in a single `Object.assign` at
50boot. Modules call `verbs.openThread(id)`. Registration only β€” no logic in
51the seam, nothing imported by it, no cycles. When a verb's implementation
52later moves into its own module, the registry entry moves with it and no
53caller churns. (searchui.js's `wireSearch` predates this seam and stays as
54is.)
55
56## Rules for a slice
57
581. Verbatim move β€” refactor in a separate PR from relocation, never both.
592. `node --check` every touched file; `npm test`; `go build` (go:embed).
603. One PR per slice, squash-merged, then deployed to the pinned session
61 canary and exercised before the next slice starts.
624. Live tenants get traced, not assumed: a "dead" section can house live
63 functions (`openMentionMenu`, `typeaheadItems`) and vice versa.
645. CLAUDE.md's frontend module list gains one line per new module.
65
66## Outcome (2026-07-15)
67
68The ladder completed over two overnight runs: 13 modules out of shell.js
69(12,057 β†’ ~7,650 lines), the state seam (`shellstate.js`) and the behaviour
70seam (`shellverbs.js`, ~43 registered verbs) both in place, every slice
71deployed to a pinned canary and verified before the next.
72
73Two lessons paid for in bug reports, now structural:
74
75- **A verbatim slice's failure mode is the dropped identifier** β€” reads that
76 worked in the old module throw `ReferenceError` in the new one, at runtime
77 only, on paths tests don't walk. Call-site audits are not enough: bare
78 *references* (`if (embed)`, `sealedBranding.name`) crash the same way.
79 `test/module-refs.test.mjs` now scans every extracted module for unresolved
80 identifiers and runs in `npm test`. **Write that checker before the first
81 slice of any future effort like this.**
82- **The boot smoke** (`tools/boot-smoke.mjs`) walks sign-in β†’ boot β†’ send β†’
83 search β†’ picker headlessly and fails on any page error; each escape became
84 a new step. Run it against a `--clean` dev server for anything touching the
85 shell's boot or module graph.
86
87## Remaining ladder
88
89All done (see Outcome). What deliberately stays in shell.js: boot, keys,
90data loading, the WebSocket dispatch, and the render plumbing the verbs
91registry fronts. Possible future work, none owed: verb implementations
92gaining module homes (the registry entry moves with them), and searchui's
93pre-seam `wireSearch` migrating to `verbs.*`.
94