Skip to content

import shared pure modules from server/lib instead of copying them into client/src/lib - #6395

Merged
atomantic merged 1 commit into
mainfrom
claim/issue-6364
Sep 6, 2026
Merged

import shared pure modules from server/lib instead of copying them into client/src/lib#6395
atomantic merged 1 commit into
mainfrom
claim/issue-6364

Conversation

@atomantic

Copy link
Copy Markdown
Owner

Summary

One pure module used by both runtimes was served three different ways: 25 declared byte-for-byte "server mirror" copies in client/src/lib policed by 20 parity tests, a growing set of files that simply import the server leaf (NAV_COMMANDS since #5053, instanceFeatureRegistry, 12 FableLoom files), and two reverse imports where the server loaded client source. The copies drifted anyway (#6303 — the server accepted https:foo, the client rejected it), and per #6363 their parity tests were unreachable from a scoped PR, so the drift reached main.

This settles on one direction: the client imports pure server/lib leaves; the server never imports client source.

  • All 25 declared mirrors in client/src/lib become named re-exports of the server leaf. Client-only extras stay put (INGEST_OPTIONS, appendTriggerWords, sceneShotWarnings, the shot-grammar label maps, clampInt / summarizeLengthProfile).
  • catalogTypes and slashdoCatalog become projections rather than plain re-exports — both client registries carry UI-only fields (editorSections / editorFields, per-button Tailwind tones) the server has no use for, so each decorates the server list instead of restating it. That also retires the hand-copied BIBLE_LIMITS numbers in editableListFields.
  • Three leaves split so the client can import them at all: bibleLimits.js out of storyBible.js (crypto + fileUtils), portosUrls.js out of ports.js (module-scope process.env), youtubeUrlAssert.js out of youtubeUrl.js (ServerError → Node's events).
  • Both reverse edges flipped: personaTraitBlend inlines its own clamp, and avatarStyles moves into server/lib with a client re-export.
  • 20 declaration-equality parity tests and the README-driven mirrorCoverage guard are gone. Behaviour they uniquely covered moved to plain unit tests: isSafeHref's scheme matrix, new youtubeUrl / tribeCadence suites, the lora_effect_probe.py version lockstep, and a client loraEffect suite for the badge table.
  • Two new always-run guards replace the copy-policing: scripts/client-server-import-purity.test.js walks the import graph of every server/lib module the client imports and fails on a Node built-in or an out-of-tree import; scripts/server-imports-no-client.test.js fails on any production server module importing client source, with the 17 remaining test-side cross-imports frozen in a list that may only shrink.

Two deliberate behaviour changes

  • Length Profile picker copy. The client LENGTH_PROFILES carried shorter description strings than the server profiles it claimed to mirror; the dropdown now shows the server wording. The row has no truncation, so a longer string wraps.
  • loraEffect.formatLoraEffect was never a mirror. The server returns a sentence for its log line; the client returns null when the badge already says everything (or the card prints "Unreadable — unreadable"). Only LORA_EFFECT_STATUSES crosses now, and the client function is renamed loraEffectDetail so one name no longer means two things.

The server-suite import budget rises 89,425 → 89,639: the four new leaves are extra nodes on paths that already existed, not new eager edges into a heavy subtree.

Test plan

  • cd server && npm test — 1,997 files / 39,873 tests pass.
  • cd client && npm test — 869 files / 10,564 tests pass.
  • cd client && npm run build — succeeds (the browser bundle now resolves every re-exported server leaf).
  • cd client && npm run lint — clean.
  • Both new guards probed end to end: adding import { randomUUID } from 'crypto' to server/lib/appIdentity.js fails the purity walk with the offending chain; the direction guard's detector is pinned by bypass probes for a multi-line named import, an export … from, a bare import, a same-root import, and a readFileSync of a cross-root path.

Closes #6364

…to client/src/lib

The same need — one pure module used by both runtimes — was served three ways:
25 declared byte-for-byte "server mirror" copies with 20 parity tests policing
them, a growing set of files that just import the server leaf (NAV_COMMANDS since
the server loaded client source. The copies drifted anyway (#6303 accepted
`https:foo` on one side only), and per #6363 their parity tests were unreachable
from a scoped PR, so the drift reached main.

Settle on one direction: the client imports pure server/lib leaves, the server
never imports client source.

- Convert all 25 declared mirrors in client/src/lib into named re-exports of the
  server leaf. Client-only extras stay (INGEST_OPTIONS, appendTriggerWords,
  sceneShotWarnings, the shot-grammar labels, clampInt/summarizeLengthProfile).
- catalogTypes and slashdoCatalog become projections rather than re-exports: the
  client registry carries UI-only editor layout / button tones, so it decorates
  the server list instead of restating it. The hand-copied BIBLE_LIMITS caps in
  editableListFields are gone.
- Split three leaves so the client can import them at all: bibleLimits.js out of
  storyBible.js (crypto + fileUtils), portosUrls.js out of ports.js (module-scope
  process.env), youtubeUrlAssert.js out of youtubeUrl.js (ServerError → events).
- Flip both reverse edges: personaTraitBlend inlines its own clamp, and
  avatarStyles moves to server/lib with a client re-export.
- Delete the 20 parity tests whose only assertion was declaration equality, plus
  mirrorParity's README-driven coverage guard. Behaviour those tests uniquely
  covered moves to plain unit tests (isSafeHref's scheme matrix, new
  youtubeUrl/tribeCadence suites, the lora_effect_probe.py version lockstep, and
  a client loraEffect suite for the badge table).
- Add two always-run guards: scripts/client-server-import-purity.test.js walks the
  import graph of every server/lib module the client imports and fails on a Node
  built-in or an out-of-tree import; scripts/server-imports-no-client.test.js fails
  on any server module importing client source, with the 17 remaining test-side
  cross-imports frozen in a list that may only shrink.

The import budget rises 89,425 → 89,639: the four new leaves are extra nodes on
paths that already existed, not new eager edges into a heavy subtree.

Closes #6364
@atomantic
atomantic merged commit 66b8166 into main Sep 6, 2026
16 of 36 checks passed
@atomantic
atomantic deleted the claim/issue-6364 branch September 6, 2026 15:53
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[code-quality-shared-pure-modules-direction] Retire the server-mirror copies in favour of the client→server import the codebase already uses

1 participant