MeridiansMeridians

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:

FolderWhat 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 →coreengineserverclientuifeaturesshell
lib/coretypetypetype
lib/enginetypetype
lib/servertype
lib/clienttype
uitypetypetype
featurestype✓*type
shelltype

* 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 derivationsrc/lib/core/<area>/
A prompt, an AI/generation call, extraction, research logicsrc/lib/engine/<area>/
Something needing Node / filesystem / credentials / the record / an opsrc/lib/server/<area>/
A browser transport, the store, IndexedDB cache, a client contextsrc/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 / pagesrc/app/(instance|gateway|shared)/… — compose only; no domain logic
A testsee §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 in controls/ 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.ts barrel). 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}, plus home/ 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.