You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
svelte-kit-vice/CLAUDE.md

23 KiB

CLAUDE.md

  1. Think Before Coding Don't assume. Don't hide confusion. Surface tradeoffs.

Before implementing:

State your assumptions explicitly. If uncertain, ask. If multiple interpretations exist, present them - don't pick silently. If a simpler approach exists, say so. Push back when warranted. If something is unclear, stop. Name what's confusing. Ask. 2. Simplicity First Minimum code that solves the problem. Nothing speculative.

No features beyond what was asked. No abstractions for single-use code. No "flexibility" or "configurability" that wasn't requested. No error handling for impossible scenarios. If you write 200 lines and it could be 50, rewrite it. Ask yourself: "Would a senior engineer say this is overcomplicated?" If yes, simplify.

  1. Surgical Changes Touch only what you must. Clean up only your own mess.

When editing existing code:

Don't "improve" adjacent code, comments, or formatting. Don't refactor things that aren't broken. Match existing style, even if you'd do it differently. If you notice unrelated dead code, mention it - don't delete it. When your changes create orphans:

Remove imports/variables/functions that YOUR changes made unused. Don't remove pre-existing dead code unless asked. The test: Every changed line should trace directly to the user's request.

  1. Goal-Driven Execution Define success criteria. Loop until verified.

Transform tasks into verifiable goals:

"Add validation" → "Write tests for invalid inputs, then make them pass" "Fix the bug" → "Write a test that reproduces it, then make it pass" "Refactor X" → "Ensure tests pass before and after" For multi-step tasks, state a brief plan:

  1. [Step] → verify: [check]
  2. [Step] → verify: [check]
  3. [Step] → verify: [check] Strong success criteria let you loop independently. Weak criteria ("make it work") require constant clarification.

This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.

Branch note (active-uix): the legacy layers src/lib/, src/uix/terra/, src/uix/air/ and the old src/routes/ test tree were removed during the UIX refactor. The new ecosystem lives in src/arts/, src/libs/, src/svrs/, and src/uix/{active-uix,soma,sema,eidos,morfo}. Demos are being re-authored under web/routes/ from scratch.

Build / Test Commands

npm run dev          # Start dev server (routes resolve from web/routes)
npm run build        # Production build (static via adapter-static)
npm run test         # Run all tests once
npm run test:unit    # Run tests in watch mode
npx vitest run src/uix/morfo/compile.test.ts   # Run a single test file
npx vitest run -t "describe name"              # Run tests matching a pattern
npm run check        # Type check with svelte-check
npm run lint         # Check formatting (Prettier)
npm run format       # Auto-format

Code Style

  • Tabs for indentation, single quotes, no trailing commas, 100 char print width (Prettier).
  • Comments in English. (Some legacy Spanish comments remain — don't add new ones.)
  • Svelte 5 runes mode is enforced globally (svelte.config.js dynamicCompileOptions).
  • DOM event handlers: lowercase (onclick, onfocus). User callbacks: camelCase (onComplete, onInteractOutside).
  • State: $state() without #. Derived: readonly x = $derived.by(...).

Path Aliases

Aliases live in ONE module, uix.aliases.js at the repo root: vite.config.ts, svelte.config.js, scripts/generate-boot.ts, scripts/docs-check.ts and any app in the workspace import it (docs/consuming.md). Never copy the table. Order is load-bearing (longer specifier first); src/uix/aliases.test.ts guards order, resolution and the absence of a second copy. The table below is a summary.

Alias Target
@ src/
$active-app, $adom, $agent, $auth, $bus, $cache, $clipboard, $color, $connection, $ethereal, $format, $http, $langs, $logger, $motion, $orca, $perf, $perm, $prefs, $scene, $session, $sium, $storage, $timer src/arts/{name}
$libs, $locale, $reactive src/libs, src/libs/locale, src/libs/reactive
$packs src/packs (tier de packs — docs/architecture/packs.md)
$svrs src/svrs/
$uix, $active-uix, $soma src/uix, src/uix/active-uix, src/uix/soma
$blocks src/uix/blocks (tier de blocks — docs/architecture/blocks.md)

Vitest Two-Project Structure

vite.config.ts defines two test projects:

  • client: Browser tests via Playwright for *.svelte.{test,spec}.{js,ts}.
  • server: Node environment for *.{test,spec}.{js,ts} (excludes svelte tests).

Architecture

The UIX is layered around a declarative contract (morfo) that the other layers consume. Each consumer reads the morfo via compileMorfo(morfo), never by walking the raw declaration.

arts/   → runtime artifacts: active-app, adom, color, motion, perf, perm, prefs, ...
libs/   → pure helpers (zero-dep): reactive, days, dom, locale, ...
svrs/   → server-authoritative engines

uix/morfo      → declarative contract: parts, data-attrs, ARIA, keyboard,
                 events with semantic family/intent. Compiler at compile.ts
                 emits CompiledMorfo (cached by WeakMap).
uix/sema       → semantic engine + channels (visual / sound / vibra).
                 The visual channel writes data-event* during a hold window.
                 State attrs (data-state, data-disabled, ...) are NEVER
                 touched by sema — they belong to the morfo runtime.
uix/soma       → headless component layer. Each provider consumes morfo +
                 sema via `runtime.trigger(eventName)` which sequences:
                 prewrite → semantic.emit → handler → render-bag re-derived
                 state attrs (the part's `props` bag is the SINGLE attr
                 pipeline — SSR included; no per-part imperative writes).
uix/eidos      → CSS layer that selects against the data-attrs the morfo
                 contract promises. lint.ts validates a CSS file against
                 compileMorfo(morfo).contracts.cssSelectors.
uix/active-uix → composition root. Two boot modes:
                 - createActiveUix(options)        — standalone (owns services)
                 - attachActiveUix(activeApp)      — attach to external app
uix/blocks     → blocks TIER (not a layer): named compositions of canon
                 components performing a page function (site-header, hero,
                 app-shell…). No morfo, no audit row; B contract guarded by
                 `npm run blocks:check`. Doctrine: docs/architecture/blocks.md

Motion lives in arts/motion (EngineMotion, alias $motion), exposed as uix.motion and consumed by BOTH soma (Presence.motion → motion.run) and eidos (CSS generation + preset registration). It's an art — not part of eidos — so soma can drive JS motion (spring) without a soma→eidos dependency. The DOM arrives injected via a structural MotionDom port (adom satisfies it). Two-moment model + F1–F7: src/arts/motion/README.md + docs/theming/motion.md.

morfo (src/uix/morfo/)

Single source of truth for a component. A Morfo declares:

  • parts[] — kebab + archetype + data + aria + optional keyboard
  • events[] — name + target partRef + semantic + optional prewrite + commits
  • focus — initial / trap / return / restore (for overlay components)

compileMorfo(morfo) returns a CompiledMorfo with pre-resolved AttrPlan[], KeyboardPlan[], ActionPlan[], and contracts.cssSelectors (the closed set of selectors eidos is allowed to use). Cached by morfo identity.

eidos linter

scripts/eidos-lint.ts <component> and scripts/eidos-lint-all.ts classify every [data-*] selector in eidos CSS as morfo-backed / eidos-only / invalid. Use this to detect drift between the morfo contract and the eidos rules.

Key Conventions

  • Never modify the morfo declaration files without updating the eidos selectors and provider sources that depend on them.
  • Read before acting. When told to read a file, read it. Don't paraphrase.
  • Verify before reporting done. Run npm run check and the relevant vitest scope. UI claims need a browser check; if you can't run a browser, say so.
  • Compose existing components; flag gaps. When building a component (or its demo, or any UI), compose the framework's existing soma/eidos components (Button, Field, Popover, Icon, …) — never re-implement a primitive inline. If a needed component is missing, flag the gap so it gets built as a reusable component, never reinvented ad-hoc. Detail: docs/guides/component-guide.md §4.
  • No backward-compat shims when relocating code. Update consumers and delete the old path; don't leave a re-export.
  • No re-export façades between layers (e.g. soma must not re-export dias — consume $libs/days directly).
  • Provider naming: child providers reference the parent as provider, never root. The exported component is <Xxx.Provider>. createAttrs still uses the part kebab 'provider' internally.
  • Data-attr naming: data-{component} (provider) and data-{component}-{kebab} (sub-parts). Never data-soma-*. The compiler emits these via compiled.parts.attrs; runtime queries match exactly.
  • Direction: the resolver has no parent step — a component inheriting from a parent (a submenu from its menu) composes that link at the call site, never inside activeDir(). :dir(rtl) is the canon; [dir='rtl'] is forbidden. The chain, the two attributes and when to stamp: docs/canon/direction-contract.md.
  • CSS token naming (eidos recipes): bare-prefixed by component, never by layer. Public tokens: --{component}-… (e.g. --dialog-content-bg, --toggle-radius-md). Internal recipe tokens: --_{component}-…. Never --eidos-…, --air-…, --terra-…, --soma-…. Migrating air- baselines means stripping the air- prefix, not replacing it.
  • DOM helpers: $adom is the single public surface for DOM-related functionality at component / soma level. It re-exports all of $libs/dom wholesale, plus the reactive createActiveDom runtime and stateful primitives (BodyScrollLock, DOMContext, RovingFocusGroup). Components and soma NEVER import from $libs/dom directly — consume $adom. Only arts/adom/* (the implementation), arts/frontend (peer service fallback), and tests of $libs/dom may reach into it. This includes responsive types (ResponsiveProp<T>, Breakpoint, Breakpoints) — they travel with the runtime, so they live in $adom.
  • Iframe / popup correctness: when you have a uix.dom, prefer uix.dom.getDocument(node?) and uix.dom.getWindow(node?) over the bare functions of the same name. The instance methods respect the active-dom's targetWindow; the bare functions fall back to the global document / window, which is wrong in iframe / popup / happy-dom-test contexts.
  • Layout reads run post-layout, never sync-after-write: layout-forcing reads (getBoundingClientRect, getComputedStyle, offset*, scroll*, client*) flush pending style + layout, so calling one synchronously right after a DOM / style write forces a reflow mid-turn — the "[Violation] Forced reflow while executing JavaScript" class of bug. Defer them: dom.measure(read, node?) queues the read in a coalesced animation frame (the sanctioned vehicle), or run it from a dom.raf callback. It's a timing rule, not an ownership one — a bare deferred read complies; only the sync-after-write ordering is forbidden. The dev-only uix.perf detector (opt-in reflowDetector) names violators at runtime via Long Animation Frames. To turn a theme token into a concrete colour, use eidos.resolveToken(token) (config + $color) — never a getComputedStyle probe.

Critical Rules

  • NEVER delete files without explicit instruction ("delete", "remove", "borra", "elimina"). If ambiguous, ASK first.
  • NEVER create fake translators / mocked services in tests for components. Use real instances composed via createActiveUix or attachActiveUix.
  • NEVER skip hooks (--no-verify) or bypass signing unless explicitly asked. If a hook fails, fix the root cause.
  • $reactive survives, $lib does not. The $reactive alias points to the current src/libs/reactive; the singular $lib alias was removed in the cleanup phase and any code that resurfaces it is a regression.

Reference Documents

Start here: docs/README.md — the documentation entry point. It maps the whole corpus (the strata, the reading order, and where every doc lives) with task-oriented shortcuts ("I want to…"). New sessions and agents should read it first to orient.

The layer reference lives in the book tree under docs/ (old paths hold stubs, so citations keep resolving):

The semantic vocabulary is docs/CANON.md — when in doubt about families, intents, verbs, channels or holds, that's the source (anchored to the book + code; deviations tracked in docs/decisions/book-deviations.md). The old Spanish guide (GUIA_IMPLEMENTACION_SEMAUIX) is a historical seed, NOT authoritative — it lives at docs/decisions/guia-semantica-historica.md.

Sema doctrine — operational rules

The doctrine lives in docs/CANON.md (vocabulary: 8 families, 6 intents, verbs, channels, holds) and docs/architecture/sema.md (engine, cascade 1·2·3·4·5a·5b, packs). Never copy their tables here or anywhere — link them. The rules you must actively obey when touching sema:

  • Typed selector builder is mandatory. Cascade rules in src/uix/sema/components/*.ts construct their selector via semaSelector(morfo, partKebab, matchers?) from $uix/morfo. A hand-written selector string targeting morfo-emitted attrs is an architecture violation — renames must break at type level, not drift.

  • Intent policy is two-axis (intentRequirement type gate + intentGuidance doctrinal hint), encoded in SEMA_FAMILY_POLICY (src/uix/sema/types.ts) — the const IS the source; editing it reshapes the discriminated unions. commit and signal require intent.

  • Channels split by owner: sema executes sound + haptic only (plus the data-event-* visual stamp during the hold); eidos materializes every visual channel (motion / presence / depth / shape / color) in CSS. The engine knows no DOM/CSS. Apps extend SemaChannelSignatures via declaration merging. (Detail: docs/theming/channels.md + docs/CANON.md.)

  • evento.intent = sonido. Two lookups and NOTHING modulates: nombre = per-emit ?? cascada ?? pack ?? morfo ?? familia[verbo] ?? familia.default then sonido = pack[nombre.intent] ?? pack[nombre] ?? nada. An intent SELECTS a whole sound (tick.threat is not a bent tick); where no variant exists the intent is ignored; where no base exists, silence — a valid answer.

  • A pack NAMES a sound; NOBODY writes a parameter — not a component, not a theme, not the app, not a per-emit override. The type accepts a SoundName or SILENT. A product that needs a sound the pack lacks REGISTERS it (declare the name, define it once) and then names it.

  • The verb tier empties the packs. SEMA_MAP.families[f].sounds is keyed by the event's canonical verb (emerge.close → 'close'), so a component writes a rule only when it DIFFERS from the default — 30 sound rules across 71 packs. The verb travels in the signal for this (it used to be discarded, S-40).

  • Two entries must be distinguishable: sound-names.test.ts fails when two differ on fewer than TWO perceptual axes. Born because its absence was caught BY EAR — five names were one 700 Hz note at five volumes and every test passed.

  • Over a recording the intent can only move the LEVEL. playSample reads only sampleUrl + gain. A sample carrying evaluative weight needs ONE FILE PER INTENT. Physics, not policy.

  • Continuous gestures sound by REPETITION — step per emission, so the speed of the gesture is the speed of the ratchet. handle is exempt from the frequency memory, which would otherwise choke it on the first drag.

  • Per-component packs live in src/uix/sema/components/{name}.ts.

  • The perceptual map is THEMEABLE, live: engine.applyMap(seed) / applySounds({ soft: { gain } }) / clearMap() — rebuilt from the authored SEMA_MAP, so applying twice is applying once. Paths address the map (families.…, intents.…, sounds.…) and a bad path throws; it used to create a phantom branch in silence. The catalogue lives IN the map for this reason. The vocabulary stays closed (SoundName = keyof SOUND_CATALOGUE): a theme changes what a name sounds like, never which names exist. The haptic kind → pattern vocabulary lives in the map too (haptics.*), so no perceptual axis is out of a theme's reach.

  • A theme is ONE thing: eidos.applyTheme({ color, …, sound }) composes the six visual axes AND the perceptual one, clearTheme() reverts all seven. It lives on ActiveEidos because the dependency runs eidos → uix (eidos holds uix and drives uix.motion); ActiveUix does not hold eidos.

  • Sound and haptic default off (audible/tactile side effects); haptic opts in via new EngineSemantic({ haptic: true }).

Eidos drift defense — types over lint

When a layer can consume the morfo via TypeScript (anything in .ts / .svelte), it MUST use the typed builder (semaSelector); hand-written morfo-targeting selector strings in TS are an architecture violation, not a lint warning. Plain .css recipes have no builder yet — for those, scripts/eidos-lint.ts is the opt-in safety net (classifies each [data-*] selector as morfo-backed / eidos-only / invalid). The architectural contract is the morfo declaration. (Detail: docs/architecture/eidos.md.)


Behavioral Guidelines

These reduce common LLM coding mistakes. They bias toward caution over speed — for trivial tasks, use judgment.

1. Think before coding

Don't assume. Don't hide confusion. Surface tradeoffs.

  • State assumptions explicitly. If uncertain, ask.
  • If multiple interpretations exist, present them — don't pick silently.
  • If a simpler approach exists, say so. Push back when warranted.
  • If something is unclear, stop. Name what's confusing. Ask.

2. Simplicity first

Minimum code that solves the problem. Nothing speculative.

  • No features beyond what was asked.
  • No abstractions for single-use code.
  • No "flexibility" or "configurability" that wasn't requested.
  • No error handling for impossible scenarios.
  • If you write 200 lines and it could be 50, rewrite it.

3. Surgical changes

Touch only what you must. Clean up only your own mess.

  • Don't "improve" adjacent code, comments, or formatting.
  • Don't refactor things that aren't broken.
  • Match existing style, even if you'd do it differently.
  • If you notice unrelated dead code, mention it — don't delete it.

When your changes create orphans, remove the imports / variables / functions your changes made unused. Don't remove pre-existing dead code unless asked.

4. Goal-driven execution

Define success criteria. Loop until verified.

  • "Add validation" → "Write tests for invalid inputs, then make them pass."
  • "Fix the bug" → "Write a test that reproduces it, then make it pass."
  • "Refactor X" → "Ensure tests pass before and after."

Strong success criteria let you loop independently. Weak criteria require constant clarification.

Powered by TurnKey Linux.