STRUCTURE — how the codebase is organized (agent-first)
From the Meridians Wiki · Public · Maintained · joint
Read this before adding or moving a file. It is the mechanical answer to two questions an implementer (human or agent) asks constantly: "where does this go?" and "what may it import?" The rules here are enforced by lint (
eslint.config.mjs) and surfaced in the generated map (knowledge-base/generated/TREE.md), so they are checkable, not just advisory. Architecture narrative: ARCHITECTURE.md. Runtime roles: CLAUDE.md → "One app, four roles".
1. The two layered constitutions
The repo has two parallel layer stacks, each with a strict one-directional import rule. Everything below hangs off these.
1a. Library layers (authority) — src/lib/
Ordered by who owns truth and where effects happen:
core → (nothing above it) pure domain model, reducer, rules, derivations
engine → core AI providers, prompts, extraction, research
server → core, engine Node-only: record, ops, scheduler, sync, credentials
client → core (+ engine types) browser projection, transports, store, cache
1b. UI tiers (composition) — src/ui, src/features, src/shell
Ordered by what composes what. Composition flows down, never up:
shell → features → ui the app frame (Stage, rail, topbar, inspector)
features → ui one product capability each (its components/hooks/state)
ui → (nothing above it) leaf primitives: atoms, icons, Modal, effects, StreamingText
Both stacks share one meta-rule: ui/features/shell are client
surfaces — none may import @/lib/server/* (reach server capability through
@/lib/client transports).
1c. Canonical subfolder taxonomy (parallel structure)
Inside a ui/, features/<capability>/, or shell/<area>/ dir, use these
structural-role folder names consistently so any dir is predictable:
| Folder | What lives in it |
|---|---|
frame/ | the chrome/shell of the area — its container, bars, rails, backdrop (e.g. stage/frame, constellation/frame, slides/frame) |
views/ | the main content surfaces (e.g. stage/views/*, constellation/views) |
panels/ | side / detail / inspector panels (always plural) |
modals/ | dialogs (always plural) |
detail/ | per-entity detail inspectors (inspector/detail) |
kit/ | that area's shared internal primitives (inspector/kit) |
Beyond these, a dir may use domain-specific sub-area folders that name a slice
of the capability (e.g. generation/{branch,thinking,coordination,fields},
game/{scenario,play,setup,table}, research/{run,schedule,operations},
access/{auth,onboarding,profile,people}, gateway/{landing,console,admin}). The
ui/ tier groups by kind instead: controls/ feedback/ display/ entity/ brand/ icons/ effects/. Keep small single-capability dirs flat — don't over-folder.
2. The import matrix (what the lint enforces)
✓ = allowed · type = type-only allowed (erased at build, no runtime coupling) ·
✗ = forbidden (lint warn). Rows import columns.
| from ↓ / to → | core | engine | server | client | ui | features | shell |
|---|---|---|---|---|---|---|---|
| lib/core | ✓ | type | type | type | — | — | — |
| lib/engine | ✓ | ✓ | type | type | — | — | — |
| lib/server | ✓ | ✓ | ✓ | type | — | — | — |
| lib/client | ✓ | ✓ | type | ✓ | — | — | — |
| ui | ✓ | — | type | ✓ | ✓ | type | type |
| features | ✓ | ✓ | type | ✓ | ✓ | ✓* | type |
| shell | ✓ | ✓ | type | ✓ | ✓ | ✓ | ✓ |
* cross-feature imports go through the other feature's index.ts barrel, and
should be rare — prefer lifting shared logic to @/lib or a primitive to @/ui.
@/components/*is a retired tier. The directory is gone (§6), but the lint config still names it in its deny-lists so a reintroduced path fails loudly rather than silently. Don't add the column back to this table — treat any@/components/*import as an error, not a legacy allowance.
Enforcement is warn, not error: it surfaces the pre-existing violations without
breaking CI, while making every new violation visible in the lint output. The current
count, split by layer and listed file-by-file, is the generated boundary ledger
(npm run gen:boundaries) — read the number there rather than from this page, which
cannot stay current. Fixing a flagged import is: move the dependency to a lower layer,
convert to import type if it's only a type, or route through the correct tier.
The number is a ratchet. npm run check:boundaries (run by the pre-push hook) compares
the live violations to the committed ledger edge by edge and fails the push on anything new
— a fix cannot pay for a new violation, so the total only moves down. Conceding one
deliberately is npm run gen:boundaries + commit: the raised budget then shows up in the diff
as a reviewable line rather than a silent warning.
3. Where does this file go? (decision table)
| I'm writing… | It goes in… |
|---|---|
| A deterministic rule / reducer action / pure derivation | src/lib/core/<area>/ |
| A prompt, an AI/generation call, extraction, research logic | src/lib/engine/<area>/ |
| Something needing Node / filesystem / credentials / the record / an op | src/lib/server/<area>/ |
| A browser transport, the store, IndexedDB cache, a client context | src/lib/client/<area>/ |
| A shared visual primitive with no domain knowledge (button, chip, chart atom, icon, modal) | src/ui/ |
| The UI for one product capability (its views + hooks + commands) | src/features/<capability>/ |
| App chrome that composes features (Stage frame, rail, topbar, layout, routing) | src/shell/<area>/ |
| A Next.js route / page | src/app/(instance|gateway|shared)/… — compose only; no domain logic |
| A test | see §5 |
Rule of thumb for UI: if it knows about a capability (research, generation, routes, a domain concept) → it's a feature. If it only arranges things → shell. If it knows nothing and is reused everywhere → ui.
4. Header descriptions for the generated map
The tree generator (scripts/gen/tree.mjs) derives each file's description from
its leading comment and writes it to the generated file map.
// <FileName> — one-line description (already the convention).
Keep that first comment block to one scan-friendly sentence: it is the description that appears in the generated file map. Put detailed rationale, invariants, and implementation notes after a blank line in a second comment block. That keeps the generated map useful without discarding the explanation where the implementation needs it.
Regenerate after structural changes: npm run gen:tree.
5. Tests — src/__tests__/
Subdivided by kind: top-level = subsystem/integration tests; react/ = UI-hook +
provider behaviour; fixtures/ + helpers/ = shared test data and utilities.
Pure-unit tests may also be colocated next to the module (foo.test.ts beside
foo.ts). When adding a test, match the nearest existing pattern for its subsystem.
6. Current state
The retired @/components/* tier is fully dissolved into the three tiers, each deeply
sub-categorised so a file's path infers its role:
- ✅
src/lib/{core,engine,server,client}— the authority split, lint-enforced. - ✅
src/app/(instance|gateway|shared)— route groups split. - ✅
src/ui/— leaf primitives incontrols/ feedback/ display/ entity/ brand/ icons/ effects/. Self-contained (zero upward imports), lint-enforced. See src/ui/README.md. - ✅
src/features/— one capability per dir, each with intent subfolders, e.g.generation/{branch,coordination,thinking,panels,fields},game/{scenario,play, setup,table,panels,hooks},research/{compose,run,query,schedule,operations},access/{auth,onboarding,profile,people,plans,buttons},gateway/{landing,console,admin,auth},constellation/{views,frame,modals},onboarding/{steps,primitives,hooks},timeline/{charts,eval}(+index.tsbarrel). Small single-capability dirs stay flat. - ✅
src/shell/— the app frame:stage/{frame,variables,views/{base,content,mind, program,watch}}(views grouped by nav section),topbar/{bar,indicators,modals, context},inspector/{detail,panels,chat,kit},sidebar/{rail,panels,media,modals,surveys}, plushome/ mobile/ layout/.
The per-dir truth is generated, not curated here: the file map shows files and the boundary ledger shows direction debt. This section states the shape; when they disagree, they are right.
Direction check: shell → features → ui is the intended flow. shell→features
imports are correct (the shell composes features); features→shell imports are the
debt to watch — when a features/ file reaches into a shell/ view for something
reusable, that something is usually a primitive that should be promoted to @/ui
(as RoomUI→@/ui/entity/PerspectivePrimitives and the position sparklines were).
The working rule for new/touched code: put new UI in the right tier and
subfolder; if a features/ file needs a shell/ internal, promote the shared
piece to @/ui instead of reaching across.