docs(website): settle Phase 3's shape, correct the library build, record slice 0 #135

Merged
whitlocktech merged 2 commits from docs/module-phase3-plan into main 2026-08-11 06:43:15 +00:00
Member

Phase 2 closed with website#136 / docs#134, so Phase 3 — the extraction — needs a plan before any of it moves. Four decisions, one finding that set the first of them, and then slice 0's outcome including a correction to the normative contract.

The finding that set the slicing

The server and client halves are independent. Because §1.2 preserves API URLs exactly, core's client keeps calling /api/v1/public/shard/status after that route is served by the module, and a module page calls the same URL while core still serves it. Nothing forces a feature's two halves to move together — so the extraction is server-first, then client, sliced by feature, which keeps each PR inside one layer.

The four decisions

  1. Slicing and merge order — ten slices (§2.7.1's table). Within each, module-uo merges before website. The loader's ownedByCore probe stops a module loading while core owns its prefix, but the module's own CI never loads it into core, so its PR merges fine beforehand — and edge then serves the feature from core right up to the moment core drops it, never sitting in a state where the branch is missing a feature outright.
  2. Criterion 1's grep reads code, not prose — filenames, import specifiers, route path literals, declared identifiers. Core's marketing copy legitimately says "shard" in a dozen places; a literal word grep would have failed CI on each while proving nothing about the boundary. That copy is rewritten in slice 8 instead.
  3. module-uo's CI clones core at a pinned ref to generate its frozen route manifest. A hand-frozen manifest goes stale silently, and the failure it would have caught is a route that moved.
  4. The module-rust dry run lands as docs/modules/rust-dryrun.md; Phase 5's Integration Kit links to it rather than copying it.

§3.6 is corrected — external and the aliases do not compose

Slice 0 built against the contract as written and got a chunk that could not load. Rollup asks external before Vite's alias resolver runs, so a specifier listed in both is marked external and never aliased; the chunk emits bare import 'react', which no browser can resolve without an import map, and CSP forbids the inline script an import map has to be. It built cleanly and emitted exactly that.

§3.6 now shows alias only, external empty, with the full alias table — the anchoring is what stops react also capturing react/jsx-runtime. What external was guarding (a missed alias welding a second React into the chunk) moves to a resolution-time build plugin, and two of its properties are contract because both were wrong first:

  • it hooks transform, not loadload is first-wins, so written against it the guard never ran, and a deliberately-broken alias produced a 24 kB chunk with react-router bundled and a green build;
  • its forbidden-package list is stated, not derived from the alias list — deriving it means deleting an alias also deletes the guard against what that alias prevented.

Slice 0 recorded (§2.7.1)

Module-uo#2. Verified against a real core, not asserted: loads, mounts, reaches started, is published by /api/v1/public/modules; chunk serves no-cache from the entry's directory while the module's server source and module.json 404; and in Chrome under the enforced script-src 'self' every shared dependency is identity-equal to core's, zero CSP reports.

Two findings worth carrying forward:

  • The boundary check failed on its own documentation — the comments describing what it catches are written in the syntax it catches. It strips comments and template literals with a character walk (a URL in a string contains a comment opener; a comment contains quotes) and carries its own test suite. Slice 8's §5.2 grep has the same problem waiting for it, in a codebase that discusses modules constantly.
  • The loader skips a symlinked module directory silentlyfilter(e => e.isDirectory()) reports a junction as a symlink. Harmless for a real install, and the first thing to check when a module fails to appear locally.

Also recorded

  • The measured surface, superseding the Phase 0 estimate: 72 server files / ~9,700 lines, 51 client files / ~3,700 lines, 32 of core's 82 server test files.
  • One kit gap, deliberately not closedlib/format.js is vendored by the module rather than becoming an eighth §3.4 member.

Documentation only. Slice 1, the atlas, is next.


  • AI-assisted (Claude Code / Claude Opus 5)
Phase 2 closed with website#136 / docs#134, so Phase 3 — the extraction — needs a plan before any of it moves. Four decisions, one finding that set the first of them, and then slice 0's outcome including **a correction to the normative contract**. ## The finding that set the slicing **The server and client halves are independent.** Because §1.2 preserves API URLs exactly, core's client keeps calling `/api/v1/public/shard/status` after that route is served by the module, and a module page calls the same URL while core still serves it. Nothing forces a feature's two halves to move together — so the extraction is **server-first, then client**, sliced by feature, which keeps each PR inside one layer. ## The four decisions 1. **Slicing and merge order** — ten slices (§2.7.1's table). Within each, `module-uo` merges before `website`. The loader's `ownedByCore` probe stops a module *loading* while core owns its prefix, but the module's own CI never loads it into core, so its PR merges fine beforehand — and `edge` then serves the feature from core right up to the moment core drops it, never sitting in a state where the branch is missing a feature outright. 2. **Criterion 1's grep reads code, not prose** — filenames, import specifiers, route path literals, declared identifiers. Core's marketing copy legitimately says "shard" in a dozen places; a literal word grep would have failed CI on each while proving nothing about the boundary. That copy is rewritten in **slice 8** instead. 3. **`module-uo`'s CI clones core at a pinned ref** to generate its frozen route manifest. A hand-frozen manifest goes stale silently, and the failure it would have caught is a route that moved. 4. **The `module-rust` dry run lands as `docs/modules/rust-dryrun.md`**; Phase 5's Integration Kit links to it rather than copying it. ## §3.6 is corrected — `external` and the aliases do not compose Slice 0 built against the contract as written and got a chunk that could not load. Rollup asks `external` **before** Vite's alias resolver runs, so a specifier listed in both is marked external and never aliased; the chunk emits bare `import 'react'`, which no browser can resolve without an import map, and CSP forbids the inline script an import map has to be. It built cleanly and emitted exactly that. §3.6 now shows **alias only, `external` empty**, with the full alias table — the anchoring is what stops `react` also capturing `react/jsx-runtime`. What `external` was guarding (a missed alias welding a second React into the chunk) moves to a resolution-time build plugin, and two of its properties are contract because **both were wrong first**: - it hooks `transform`, not `load` — `load` is first-wins, so written against it the guard never ran, and a deliberately-broken alias produced a 24 kB chunk with react-router bundled and a green build; - its forbidden-package list is **stated, not derived** from the alias list — deriving it means deleting an alias also deletes the guard against what that alias prevented. ## Slice 0 recorded (§2.7.1) Module-uo#2. Verified against a real core, not asserted: loads, mounts, reaches `started`, is published by `/api/v1/public/modules`; chunk serves `no-cache` from the entry's directory while the module's server source and `module.json` 404; and in Chrome under the enforced `script-src 'self'` every shared dependency is **identity-equal** to core's, zero CSP reports. Two findings worth carrying forward: - **The boundary check failed on its own documentation** — the comments describing what it catches are written in the syntax it catches. It strips comments and template literals with a character walk (a URL in a string contains a comment opener; a comment contains quotes) and carries its own test suite. **Slice 8's §5.2 grep has the same problem waiting for it**, in a codebase that discusses modules constantly. - **The loader skips a symlinked module directory silently** — `filter(e => e.isDirectory())` reports a junction as a symlink. Harmless for a real install, and the first thing to check when a module fails to appear locally. ## Also recorded - **The measured surface**, superseding the Phase 0 estimate: 72 server files / ~9,700 lines, 51 client files / ~3,700 lines, 32 of core's 82 server test files. - **One kit gap, deliberately not closed** — `lib/format.js` is vendored by the module rather than becoming an eighth §3.4 member. Documentation only. Slice 1, the atlas, is next. --- - [x] AI-assisted (Claude Code / Claude Opus 5)
wtclaude added 1 commit 2026-08-11 06:10:51 +00:00
Phase 2 is closed, so Phase 3 needs a plan before any of it is extracted.
Four decisions, and one finding that set the first of them.

The finding: because API URLs are preserved (§1.2), a feature's server and
client halves are independent. Core's client keeps calling
/api/v1/public/shard/status after the module serves it, and a module page
calls the same URL while core still does. Nothing forces vertical slices, so
the extraction is server-first then client, sliced by feature, ten slices.

Merge order within a slice is module-uo first, then website. The loader's
ownedByCore probe stops a module LOADING while core owns its prefix, but the
module's own CI never loads it into core, so its PR merges fine beforehand --
and edge then serves the feature from core right up to the moment core drops
it, with no window where the branch is missing it outright.

Criterion 1's grep reads code, not prose: filenames, import specifiers, route
path literals and declared identifiers. Core's marketing copy legitimately
says "shard" in a dozen places and a literal word grep would have failed CI on
each while proving nothing about the boundary. That copy is rewritten in its
own slice instead, which is real work with a real review rather than an
exemption hidden in a pattern.

module-uo's CI clones core at a pinned ref to freeze its route manifest --
nothing else proves the URLs it claims are the URLs it serves -- and the
module-rust dry run lands in docs/modules/ where §2.10 already aggregates
module documentation.

Also records the measured surface (72 server files, 51 client, 32 test files),
which supersedes the Phase 0 estimate, and the one kit gap: lib/format.js is
vendored by the module rather than becoming an eighth §3.4 member.

Co-Authored-By: Claude <noreply@anthropic.com>
wtclaude added 1 commit 2026-08-11 06:33:46 +00:00
Slice 0 built the module bundle skeleton against the contract and found that
§3.6 does not work as written. It shows Rollup's `external` alongside the
resolve aliases, and the two do not compose: Rollup asks `external` BEFORE
Vite's alias resolver runs, so a specifier in both is marked external and never
aliased. The chunk then emits bare `import "react"`, which no browser can
resolve without an import map, and CSP forbids the inline script an import map
has to be. It built cleanly and emitted exactly that.

§3.6 is corrected: alias only, `external` empty, with the alias table shown in
full because the anchoring is what stops `react` also capturing
`react/jsx-runtime`. What `external` was guarding -- a missed alias welding a
second React into the chunk -- moves to a resolution-time build plugin, and two
properties of that plugin are now contract because both were wrong first: it
hooks `transform` rather than `load` (first-wins, so it never ran), and its
forbidden-package list is stated rather than derived from the alias list
(deriving it means deleting an alias also deletes the guard).

Also records slice 0's outcome in §2.7.1, including the finding that generalises
past this repo: the boundary check failed on its own documentation, because the
comments describing what it catches are written in the syntax it catches. Slice
8's §5.2 grep has the same problem waiting for it. And the loader skips a
SYMLINKED module directory silently, which is the first thing to check when a
module fails to appear locally.

Co-Authored-By: Claude <noreply@anthropic.com>
wtclaude changed title from docs(website): settle Phase 3's shape, slices and merge order to docs(website): settle Phase 3's shape, correct the library build, record slice 0 2026-08-11 06:34:08 +00:00
whitlocktech merged commit 9930b375f2 into main 2026-08-11 06:43:15 +00:00
whitlocktech deleted branch docs/module-phase3-plan 2026-08-11 06:43:16 +00:00
Sign in to join this conversation.
No description provided.