The "I want to… use a runtime artifact" row in docs/README.md now routes
through the composition root (architecture/active-app.md) → the arts map
(arts/README.md) → the per-artifact README. Map/aliases were already complete
(9e48b80b). docs:check 0 errors.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Move the three superseded arts design-phase records into the E3 corpus
(already indexed in docs/decisions.md), verbatim-as-historical:
- docs/decisions/design-connection.md (from src/arts/connection/DESIGN_CONN.md)
- docs/decisions/design-session.md (from src/arts/session/DESIGN.md)
- docs/decisions/design-timer.md (from src/arts/timer/DESIGN_TIMR.md, kept Spanish)
Each gets a `status: historical` frontmatter block; the current reference for
each subsystem is its README.md. A stub at every old src/arts/* path keeps the
~30 source provenance citations (`DESIGN_TIMR §12.8`, `DESIGN.md §7.Q1`, …)
resolving. Bodies kept verbatim (no reformat). docs/decisions.md index + naming
note updated to the new paths. docs:check 0 errors.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
- ValueField: optionally-editable centre value composing NumberField, wired to
the knob's value (time-picker-hour-slider pattern); de-chromed to read as
text, width-locked to the max value's digit count, and its pointer/keyboard
events isolated from the rotate gesture so editing never rotates or resets
the dial.
- handle-drag: continuous rAF-throttled perceptual tick during rotation,
mirroring the Slider (morfo event + provider emit + sema rule).
- NumberField: first-class maxLength prop (native maxlength, unlimited when
unset); the knob caps the field to the max value's digit count.
- Rotation: publish --knob-start-angle / --knob-sweep so the arc reacts to the
sweep; snap to the nearest end inside the bottom dead zone so overshooting the
max lands on the max and never wraps around to the min.
- Register knobSema in the uix demo layout so the knob emits sound + haptic.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Move the active-app reference into the book tree as the arts composition root,
the App-side analogue of active-uix.md:
- new docs/architecture/active-app.md (frontmatter + fixed relative paths); the
dated Spanish "Handoff 2026-05-13" heading becomes a timeless "Quick start".
- extract that Handoff block verbatim (ES) to docs/process/handoffs-2026-05.md,
alongside the other extracted arts/uix handoffs.
- stub src/arts/active-app/README.md pointing at the chapter (active-uix pattern).
- wire the chapter into the docs/README.md E1 map.
docs:check 0 errors (11 pre-existing warnings unrelated).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Translate the last Spanish README in the arts reconciliation (~1457 lines).
Prose only; all code blocks, ORCA_* constants, type names and the A1 API
corrections that landed in 9e48b80b are preserved verbatim. arts:check 0/0/22.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Full Spanish -> English translation of sium/README.md (862 lines; faithful).
Code blocks, schema examples, issue codes and `#?sium.errors.*` idlangrefs kept
verbatim. Also fixed a stale ref: `/test/sium` -> `/active/docs/sium`. Carries
the A1 additions (cssLength/cssValue in Included Refines).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Full Spanish -> English translation of cache/README.md (faithful; code blocks,
CACHE_* constants and scope/policy values kept verbatim). The mixed file's
Spanish sections (What It Solves, Keys, Scopes, Policies, Modes, Tags, Mutate,
Explain, Adapters, Events/Logger, Security, Roadmap) translated; the already-
English "Reacting to identity" section left as-is. Carries the A1 fixes
(defineActiveCache signature, ActiveEngine contract surface, demo URL).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Full Spanish -> English translation of adom/README.md (faithful; code blocks and
the ActiveDom interface kept verbatim, incl. the Backlog chronicle entries).
Carries the A1 additions (prefersReducedMotion/writeProperty/removeProperty,
listen 4 overloads, standalone rune-helpers table, /active/docs/adom).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Full Spanish -> English translation of connection/README.md (faithful; code
blocks, event/error constants and state diagrams kept verbatim). Carries the A1
fixes (required `{ timers }`, the 13 ActiveConnections getters, `/active/docs/conn`
demo, the no-own-scheduler paragraph).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Full Spanish -> English translation of auth/README.md (faithful; code blocks,
route/error constants and the `$lib/server/*` SvelteKit-consumer imports kept
verbatim). Example Svelte UI strings translated (Validating.../Hello/Anonymous
session). Also fixed a stale ref: `/test/auth` -> `/active/docs/auth`.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Full Spanish -> English translation of format/README.md (faithful). Code blocks,
tables and es-ES formatter OUTPUT strings in comments (e.g. "hace 2 horas",
"lunes, 25 de mayo") kept verbatim — they demonstrate locale output, they are
data, not prose. Carries the A1 fixes (units/ path, formatAs/convertAs,
convertToDefault, $format/currency alias).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Full Spanish -> English translation of prefs/README.md (faithful; all code /
text blocks and the `prefs::*` error strings kept verbatim). Carries the A1
signature fixes already landed (applyBrowserEnvironment/watchBrowserEnvironment,
createPrefsStorageBridge `engine`).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
First A2 batch (Spanish -> English, faithful; code blocks + i18n example data
left untouched):
- clipboard: full translation (was fully Spanish).
- langs: light pass — the "Handoff 2026-05-12" Spanish paragraph only (the rest
was already English; example i18n records like 'Aceptar' are data, not prose).
- logger: light pass — the "Framework Logger Contract" + "Framework Diagnostics"
Spanish spans (rest already English).
No residual Spanish prose in the three files (verified by grep). arts:check 0/0/22.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Second A1 batch — every fix verified against the cited code symbol, not the
plan (the plan repeatedly under- or mis-counted; verifying against code caught
both extra drift and already-fixed items):
- format: `unts/`->`units/` path typo; documented currency `formatAs`/`convertAs`
and units `convertToDefault`; fixed the `$formats/currency` alias (plural, no
such alias) -> `$format/currency` in format + currency READMEs.
- timer: FS diagram completed (11 -> the full 16 source files), "50 tests" -> 47,
reschedule behaviour/errors reconciled (from a prior uncommitted session; all
verified accurate against timer-handle.ts).
- bus: Layer-split diagram corrected (consts/errors/matching/diagnostics/contracts
live in libs/bus, not arts/bus; only EngineBus/EngineBusOptions + engine/svelte
in arts/bus; added silent-bus.ts); documented all 3 listener error modes
(log-and-continue/throw/collect); fixed stale preset names -> the real
cache-clear-on-*/connections-*/perm-invalidate-*/session-auto-refresh/standard.
- connection: `createEngineConnections()` -> `{ timers }` (required); documented
the 6 missing ActiveConnections getters (13-getter surface); conn-chat demo
(nonexistent script + npm task) -> real `/active/docs/conn`; fixed a paragraph
that contradicted the required-timers reality.
- sium: documented `cssLength`/`cssValue`/`CSS_LENGTH_REGEX` refines (were absent);
verified the "codes" item was already covered (siumLangs has all 26).
- prefs: `createPrefsStorageBridge({ prefs, storage, key })` -> `{ engine,
storage, onError?, skipHydrate? }` (prefs->engine, key was phantom);
`applyBrowserEnvironment(prefs,...)` -> `(engine,...)`;
`watchBrowserEnvironment(prefs,...)` -> `(apply,...)` (first arg is a callback).
- http: `DEFAULT_RETRY`/`DEFAULT_TIMEOUT` -> `HTTP_DEFAULT_*`.
- motion: documented the missing `resolve`/`has`/`list`/`exit`; noted `JsDriver
'svelte'` is reserved/unimplemented (README + engine-motion.ts JSDoc, which
falsely listed it as running).
- color: types.ts JSDoc `ActiveEidos.setCssVariables` -> `applyColorScheme`; bare
`THEMING.md` ref -> `../../uix/eidos/THEMING.md`.
- systemic (not in the plan): `defineActive*`/`defineEngine*` imported from
`$active-app/services` (the schema-contract module, which does NOT export them)
-> `$active-app/service-factories`, in clipboard + auth + storage + active-app.
arts:check: 0 errors, 0 warnings, 22 arts. Note: bus/connection/format/prefs/sium/
timer carried prior uncommitted arts-reconciliation edits from an earlier session,
folded in here (all coherent, same initiative).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Closes the provider→recipe direction of the CSS-var drift stumble the
architecture-faithful way: the morfo is the source of truth. A component now
declares the functional custom properties its soma provider writes and its eidos
recipe reads:
cssVars: [{ name: 'progress', … }, { name: 'angle', … }] // → --knob-progress, --knob-angle
- types: `MorfoCssVar` + optional `cssVars` on `Morfo` (additive; the 133
existing morfos are unaffected).
- schema: validates `cssVars` (array of `{ name, description? }`).
- compile: exposes `contracts.cssVars` as full names `--{kebab}-{name}`.
- knob morfo declares its two contract vars.
- guard (recipe-css-contract.test.ts): enumerates morfos via `import.meta.glob`
and verifies BOTH sides honour each declaration — the provider writes
`--{kebab}-{name}` AND the recipe reads it — so renaming one side without the
other fails loudly. Proven non-vacuous (a bogus cssVar is flagged on both
sides).
Why A1 and not a grep cross-check (A2): in the CSS a provider-written functional
var and a consumer-override alias are indistinguishable, and providers publish
hook vars the recipe doesn't consume — a grep gives ~17 false positives (proven
earlier). The morfo declaration is what disambiguates.
npm run check 59 (baseline, 0 in touched files); eidos recipe-css-contract 24/24;
morfo suite green (only the pre-existing dialog-role test fails); docs:check 0.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Empirically confirmed the grep approach can't distinguish a provider-written
functional var from a consumer-override alias (identical in CSS), and providers
publish API-hook vars the recipe doesn't consume (17 such, none read by var()).
Only A1 (morfo cssVars declaration) can close the provider→recipe direction.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Closes the recipe→theme direction of the soma→eidos CSS-var drift stumble. New
check in recipe-css-contract.test.ts: every NO-FALLBACK `var(--x)` in a
component recipe must resolve to a token declared somewhere in the eidos CSS
tree (foundation + shared layers + the component's own self-declarations). A
`var(--x, default)` stays exempt — those are runtime-optional by construction
(provider/floating vars whose fallback IS the default), matching the existing
`collectOwnPublicVariables` rule.
This is the exact class that silently broke the Knob (`--color-neutral-content`,
`--state-hover`, `--color-neutral-bg` resolved to nothing, only visible in the
render). The guard immediately caught 3 MORE real phantoms — fixed, not silenced
(silencing would be the anti-pattern of a guard exempting the gap it exists to
catch):
- card-group: `--color-content-default` → `--color-content-primary`
- link-preview: `--leading-body` → `--leading-normal`
- textarea: `--font-family-body` → `--font-family-primary`
Full eidos suite 290/290. The provider→recipe direction (renaming a
provider-written `--knob-angle` breaking the recipe) remains open as option A
(morfo `cssVars` declaration or a grep cross-check) — noted in STUMBLES.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Ethereal (was undocumented — the miss that prompted this pass):
- new src/arts/ethereal/README.md, API drawn from the code (computePosition,
the 7 middleware, autoUpdate, selectPositioningStrategy, dual JS/CSS-anchor
engine, +1-frame read-phase model)
- wired into src/arts/README.md Map + aliases + cross-deps; $ethereal added to
the CLAUDE.md alias table; the also-missing bus/orca/prefs aliases + Map rows
completed against vite.config.ts
A1 drift (every fix verified against the cited code symbol, not the plan):
- orca: ~13 phantom identifiers across 7 sections (priority, execution /
ORCA_EXEC_*, tokenTimeoutMs, onFatal / ORCA_ON_FATAL_*, onTimeout, the whole
multi-level timeout model, the transaction-port model + ORCA_TX_*,
OrcaMaybePromise, OrcaCompensationContext) reconciled to the real API
(parallel waves, actionTimeoutMs-only, transaction:string tag + compensate,
fatal via returning OrcaFatal); added the real missing fields fanIn / parallel
/ compensate; glossary OrcaRun -> OrcaRunResult
- cache: residual createActiveApp({cache}) -> services:{cache:defineActiveCache()};
documented the ActiveEngine contract surface (loading / lastError / disposed /
snapshot / clearError / onChange / dispose); dangling demo URL ->
/active/get-started/ecosystem
- adom: added prefersReducedMotion / writeProperty / removeProperty; listen
1 -> 4 typed overloads; documented 12 undocumented standalone rune helpers;
/test/adom -> /active/docs/adom
Guard (scripts/arts-check.ts): removed the ethereal README exemption (set empty)
and promoted A-index from warn to error — the net no longer passes green with a
real gap. arts:check: 0 errors, 0 warnings, 22 arts.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Adds a '### Authoring notes' subsection to soma.md §6 grounded in the real
defs — state<T>() vs $state, role optionality on a Provider, Without<>/
PrimitiveDivAttributes, and gesture pointermove/up ownership. Closes STUMBLES
#9; only #7 (soma->eidos CSS-var contract, design-open) remains.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Knob now built across all 5 layers, validating the exercise. #7 confirmed
first-hand: the Knob build hit phantom theme tokens + uncontracted provider
CSS vars (silent drift), exactly as predicted.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The relocated Knob didn't render (only the number showed). Root causes, all
found by inspecting the loaded stylesheet (not guessing):
- A stray `*/` inside the header comment (`--scale-*/--primitive-*`) closed the
comment early and corrupted the FIRST rule, so the base `[data-knob]` rule
(which sets `--_knob-diameter`) never loaded → the control collapsed to height 0.
- Phantom tokens inherited from the Fable draft that don't exist in the theme:
`--color-neutral-content` (→ `--color-neutral-text`), `--state-hover`
(→ a `filter: brightness` hover); the pointer was transparent because its
fallback token didn't exist.
Design (per the user's reference image + directives):
- Value readout centred in the dial's hole (grid place-items).
- Donut arc: replaced an improvised hard-edged radial mask (aliased inner edge)
with an opaque face plate over the conic pie — a border-radius circle is
GPU-antialiased, so the inner ring is clean. Colours use theme-adaptive
tokens (surface-raised face, neutral-track ring, neutral-text value,
neutral-text-strong pointer).
- Value + label fonts scale with the knob via the same size coordinate as the
diameter (`--size-{k}-*` bundle), not an invented "one step below" rule.
Demo: value composed inside the Control (centre), plus a `needle` control that
conditionally composes `<Knob.Indicator>` (the compositional way to show/hide it).
Verified structurally (sizes, tokens, toggle) via DOM inspection; the screenshot
tool timed out all session, so the final pixel check is the user's. Recipe
contract green.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Two corrections after verifying the relocated Knob against the recipe contract:
1. The Knob was NEVER a contract violation — every `--knob-*` alias in the css
is consumed WITH a fallback (`var(--knob-x, default)`), which the
recipe-css-contract treats as runtime-optional, so no recipe-token
declaration is required. Removing `knob` from `WIP_TRACKS` keeps the full
eidos suite green (289) — it's now first-class, not an excluded WIP track.
2. But `size` was a visual no-op: the css hardcoded `--size-md-*` everywhere,
so `data-size` did nothing. Introduce private `--_knob-*` size vars selected
by `[data-size]` off the global `--size-{k}-*` bundle (public `--knob-*`
still overrides; the size-driven private is only the default; value readout
sits one step below). Browser-verified: control diameter now sm 60 / md 72 /
lg 88 px (was fixed 72 at any size).
npm run check unchanged at 59 (baseline); no console errors.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Canonical-depth demo at web/routes/uix/components/knob mirroring the Slider
demo (Live / API / morfo / sema / recipe / a11y tabs; every public prop is a
live control; sema event trace + play buttons; soma/eidos code snippets).
Serves as the visual/behavioral verification the relocation lacked. Confirmed
live at /uix/components/knob:
- renders role=slider, aria-valuemin/max/now, aria-roledescription resolved
from the langs catalog ("dial giratorio" under the es locale);
- provider-published `--knob-angle` / `--knob-progress` drive the conic-gradient
arc + rotated indicator (angle = startAngle + progress*sweep, verified exact);
- keyboard loop works end-to-end (ArrowUp/PageUp → value 40→52, aria-valuenow +
CSS vars + bound ValueText all update); no console errors.
npm run check: 0 errors in the demo (total unchanged at the 59 baseline).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Ships the net that batches A1-A3 / B3 land against, mirroring
scripts/docs-check.ts (textual invariants, no Svelte-graph imports):
- A-readme (error): every src/arts/{name}/ has a README.md. ethereal is
exempt via a self-cleaning README_PENDING set until batch A3 authors it.
- A-naming (error): the Engine*/Active* split holds — no Engine* root is
declared in a .svelte.ts (engines are stateless), and every reactive
`class Active*` root lives in a .svelte.ts (Active...Error exempt).
- A-index (warn): the src/arts/README.md Map lists every art. WARN, not
error — the four current gaps (bus, ethereal, orca, prefs) are batch B3's
worklist and drive to zero there.
Green now: 0 errors, 4 warnings (the B3 worklist), 22 arts.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
User decided (mirroring F7's corpus-shape call): arts stay
E5-in-place-reconciled with only active-app promoted to an architecture
chapter; orca stays E5 for now; the DESIGN docs move to docs/decisions/
translated. B1/B2 locked; A1/A2 (drift + language) unaffected.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The docs reconciliation (F6 drift-fix + F7 corpus-libro) covered
active-uix (morfo/soma/sema/eidos) but left the active-app side — the 22
arts/ runtime artifacts — as E5-in-place, un-reconciled. This plan does
for arts what F6+F7 did for the UIX layers, grounded in a 22-agent
per-art audit (every drift verified against the code): ~40 doc-vs-code
mismatches across 15 arts, 12 READMEs needing ES->EN, ethereal missing a
README, 3 historical DESIGN docs, dead route/demo cross-refs. Batches
A0 (guard) / A1 (drift sweep) / A2 (ES->EN) / A3 (ethereal README) / B1
(active-app -> architecture chapter, the missing analogue of
active-uix) / B2 (DESIGN docs -> decisions/) / B3 (index+TOC). Three
structural open questions for the user (arts E5 vs book; orca chapter;
DESIGN docs move) mirror F7's corpus-shape decision.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The Fable-built Knob lived entirely under `src/uix/active-uix/knob/` — the
wrong home for a multi-layer component — with guessed APIs (103 type errors),
missing sub-part wrappers, junk `mnt/user-data/outputs/` paths, and its angle
math inlined against a fabricated `Gesture.base` surface. Dismember it into the
canonical per-layer directories, against the real APIs, consuming the real
`Gesture.rotate` (STUMBLES S4):
- **morfo** → `morfo/components/knob.ts`. Applies the S3 `part-absent` condition
to `aria-label` (emit only when no Label part) — the file's own NOTE asked
for it once the condition existed.
- **langs** → `langs/components/knob.ts` (+ registered in the langs index).
- **sema** → `sema/components/knob.ts` (+ registered in the sema index); valid
nominal earcons + haptic kinds, `channels` declared, risk-commit composes
(`op: 'add'`) over the intent profile per doctrine.
- **soma** → `soma/components/knob/`: provider (rewired to `Gesture.rotate` in
the constructor — mapping `angleAbsolute` onto the [startAngle,endAngle]
sweep, folding [0,360) into a symmetric range), `state<T>()` for the label id,
the missing `KnobIndicatorProvider`, root + five sub-part wrappers, exports,
index, and a provider test (ARIA slider contract + gesture wiring + keyboard
stepping/commit + disabled guard + dblclick reset).
- **eidos** → `eidos/components/knob/`: recipe css, size-aware root wrapper
(`ActiveEidos.resolve`), five pass-through wrappers, types, namespace index.
The old `active-uix/knob/` (untracked, added mid-session) is removed. Knob is
added to `recipe-css-contract`'s WIP_TRACKS until its recipe tokens land (css
still hardcodes md sizes, no variants). `npm run check`: 0 errors in every knob
file (total back to the 59 pre-existing baseline, from 162). Tests: 4 knob
provider + no regressions across eidos/sema/langs (174).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Ran the full 65-test port suite on WebKit + Firefox (UIX_CROSS_BROWSER=1),
not just chromium. That caught one real robustness gap: Firefox stores
scrollTop as a subpixel value (~59.93 for a 60px scroll), so ScrollState's
exact-equality assertion is relaxed to toBeCloseTo — the port faithfully
passes the browser's own scrollTop through.
New coverage:
- tabbable.ts WRAPPER (getTabbableCandidates / getTabbableEdges /
getTabbableFrom / getTabbableFromFocusable) — the surface soma's
focus-scope actually consumes, previously untested.
- resource: AbortController cancellation + onCleanup on rapid source change
(aborted request must not clobber state with an AbortError).
- FiniteStateMachine: from/to/event/args lifecycle metadata, send-arg
forwarding, and no-action-for-event (no transition).
All green on chromium + webkit + firefox. (Repo check baseline is ~214 due
to the unrelated in-progress Knob component; 0 errors in these files.)
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Fourth gesture specialization alongside base/drag/resize, for radial controls
(Knob, and future AngleSlider / colour wheel / rotary time picker). The Knob
built by Fable (active-uix/knob) currently does its angle math inline against a
guessed `Gesture.base` API and flags the gap — this is the primitive it needs.
`GestureRotate extends GestureBase`, mirroring the existing peers (protected
onMove/onRelease/onCancelled overrides + its own opts callbacks):
- `angleAbsolute` — pointer angle around a centre, 0° = up (12 o'clock),
clockwise positive, [0, 360). Matches CSS `rotate()`, so it pipes straight
into a `--rotation` var (the convention the real Knob consumer uses).
- `angleDelta` — signed rotation since start, unwrapped across the 0/360 seam
(355°→5° is +10°, not −350°); multi-turn friendly.
- `angularVelocityDeg` — deg/ms via a dedicated VelocityTracker fed the
accumulated angle, so it reads d(angle)/dt (radius-independent).
- Detents (`AngularSnap`: fraction / degrees / '45deg' / '50%'), wrap-aware
distance, dismiss by accumulated degrees, `center: 'element' | {x,y}`
(captured once at pointerdown — no per-move layout read).
Deviations from the scoped plan, all deliberate: angle convention taken from
the real Knob consumer (`atan2(dx,-dy)`, not the plan's `atan2(dy,dx)`);
`referenceSize` omitted (no meaning for a radius-independent angle);
CW/CCW direction locking deferred to v1 as the plan itself noted.
Two release-time snap bugs (found by adversarial review) fixed before landing:
- angular velocity projection now scales for the deg/ms domain and caps below a
half-turn, instead of reusing the px-scaled `VelocityTracker.project` (a
1 deg/ms flick would otherwise project 400° and cross a full turn);
- the sequential-detent guard measures neighbour adjacency modulo the detent
count under `wrap`, so the first/last detents are treated as physically
adjacent (the linear `|i-current|>1` from drag broke across the seam).
Tests: 9 new cases (angle convention on all cardinals, seam unwrap, onRotate,
detent resolution, dismiss by degrees + no-dismiss under threshold,
single-pointer, cancel). `npm run check` = 0 errors in the gesture layer.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The emit contract explains discrete events (one gesture → one emit → one
hold) but says nothing about direct-manipulation surfaces (Slider, Drawer,
future knob/swipe) where the pointer moves at sample rate while the value
updates continuously. Add a `## Continuous components (drag, swipe, hold)`
section after the emit contract, grounded entirely in the real Slider/Drawer
code (adversarially fact-checked, zero discrepancies):
- One door: `runtime.trigger()` is the sole path (there is no `emitEvent`).
Its Promise represents the whole occurrence incl. the visual hold — so
`void trigger()` is fire-and-forget (correct for high-frequency emits) and
`await trigger()` blocks until the hold ends (correct when a structural
change must observe the resolved signal, e.g. `close`).
- `coincident` vs `post` for a moving value: the move (handle-drag,
drag-progress) is `coincident` (signal + mutation indivisible); the commit
(commit-set) is `post` (handler settles, then the signal celebrates).
Slider is the worked example.
- Throttle continuous POINTER emits, not keyboard: pointer emits are coalesced
to animation frames + floored to a component-tuned interval via ActiveDom's
requestFrame (Slider DRAG_SIGNAL_MS, Drawer DRAG_PROGRESS_SIGNAL_MS);
keyboard commit-set fires once per keydown (already human-paced). Tuning ms
point to the code, never hard-coded here.
- The per-emit `overrides` payload owns pitch/gain/contour; a continuous
event's cascade rule sets only `channels` and MUST NOT override those
primitives, or it would clobber the live payload every frame.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
A line-by-line audit of the ports against runed's original source found two
faithfulness regressions in unused F3 utilities:
- IsIdle seeded `lastActive` to 0 (epoch) instead of the construction
timestamp, so `Date.now() - lastActive` read ~56 years until first activity.
Now `$state(Date.now())`, matching runed.
- IsInViewport passed `threshold: undefined`, which the IntersectionObserver
spec coerces to 0 (fires at a single pixel). runed defaults it to 0.1;
restored (and rootMargin -> '0px').
Guarded IsIdle's baseline in its test. check=59.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
65 tests in real chromium covering every ported utility, so the removed
runed + tabbable deps can be relied on with confidence:
- tabbable-core: order, tabindex, disabled, real display/visibility, inert,
disabled fieldset, radio groups, contenteditable, details, shadow DOM.
- $libs/reactive runes: watch / watchOnce / watch.pre, Context, Previous,
Debounced/debounce, Throttled/throttle, interval, StateHistory,
FiniteStateMachine, resource, IsMounted, onCleanup.
- $adom DOM runes: ElementSize, ElementRect, ActiveElement, IsFocusWithin,
IsIdle, IsDocumentVisible, IsInViewport, onClickOutside, PressedKeys,
ScrollState, TextareaAutosize, AnimationFrames.
Patterns: $effect.root + flushSync for runes, vi.useFakeTimers for
debounce/throttle/interval/idle, vi.waitFor for real observers/rAF, and
rendered fixtures (context-probe, scroll-state-probe) for the two utilities
that need a component context. `npm run check` = 59 (baseline).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Completes the runed + tabbable removal (F1+F2 in d0f41fb2). Ports runed's 11
DOM-reactive utilities into $adom, window-correct by resolving the window from
the node (getWindow) or an optional window/document — not runed's defaultWindow:
AnimationFrames, ActiveElement (+ activeElement singleton), IsDocumentVisible,
IsFocusWithin, IsInViewport, onClickOutside, PressedKeys, ScrollState,
TextareaAutosize, ElementRect, IsIdle.
Follows the committed ElementSize precedent (raw observers/listeners live in the
arts/adom implementation layer) rather than threading a `dom` param.
PersistedState is intentionally NOT ported: the $storage art already supersedes
it (its README names runed/PersistedState as the thing it replaces) — same call
as boolAttr being covered by boolToEmptyStrOrUndef.
`npm run check` = 59 (baseline, 0 in ported files); server tests unchanged
(pre-existing failures only). The runed + tabbable port is now complete.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The common ARIA pattern "emit aria-label only when there is no Label part"
was not declarable. Add `{ when: 'part-absent', part }` as the logical
inverse of `part-present`, plumbed through the four condition sites:
- types.ts: MorfoCondition union member + JSDoc.
- schema.ts: discriminated-union member (structural accept) + extend the
checkCondition unknown-part guard to cover part-absent (throws on unknown
part, same as part-present — the prior guard only discriminated part-present).
- compile.ts: collectConditionDeps adds the referenced part to partRefs, so
the runtime re-evaluates when the part's presence toggles.
- resolver.ts: shouldEmitMorfoEntry returns !bindings.parts?.[part] — emit
when the part is absent/falsy, suppress when present.
Runtime + keyboard paths delegate to these helpers (evalAttrPlan /
shouldEmitMorfoEntry), so no runtime change is needed. Correct by symmetry
with part-present, which reads the same presence-accurate bindings.parts.
Tests: schema accepts a part-absent condition on a known part and throws on
an unknown part; the compiler collects the part dep; the resolver emits the
value when the part is absent and undefined when present.
Docs: morfo.md condition list gains the part-absent form.
Non-breaking: no existing morfo uses it.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Removes the two runtime dependencies that broke the "framework depends on
nobody" doctrine, porting them into our own code (same move as
floating-ui -> $ethereal).
F1 (dropped the deps):
- tabbable -> src/libs/dom/tabbable-core.ts (getComputedStyle/CSS.escape
resolved from the node's own window, iframe-safe); consumed by the
existing tabbable.ts wrapper and surfaced via $adom.
- runed Context + watch/watch.pre/watchOnce -> $libs/reactive.
- runed ElementSize -> $adom/element-size.svelte.ts, rebuilt window-correct
on the node's own window (not defaultWindow).
- Rewired ~33 watch + 3 Context imports; floating (ElementSize) and
focus-scope (tabbable) now consume $adom.
F2 (rest of the pure reactive runes -> $libs/reactive):
- Previous, IsMounted, onCleanup, interval, debounce/Debounced,
throttle/Throttled, StateHistory, FiniteStateMachine, resource.
- Dropped runed's React-style use* prefix; reused framework toValue
(=extract) + MaybeActiveOrGetter; added Setter<T>.
runed + tabbable removed from package.json. `npm run check` = 59 (baseline,
0 in ported files); tests unchanged (pre-existing failures only).
F3 (remaining DOM-reactive runes -> $adom on ActiveDom, + PersistedState
-> $storage) is pending: see docs/process/continue-runed-tabbable-port.md.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Continuation doc for tomorrow: full session state (C7 + eidos triage +
onion-menu/surface-subtle + functional flags + C10 reframe + S1
vocabularies), plus the three high-confidence implementation plans the
scope-stumbles-s2-s6 workflow verified against the real code (S3
part-absent condition, S2 continuous-components doctrine, S4
Gesture.rotate) with exact file:line edit maps. S5/S6 flagged for
re-scope. Foreign runed/tabbable WIP in the tree left untouched.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
An agent building a component from the docs-book alone could not assign
archetypes / holds / haptic kinds: the closed sets live only in code consts
and the corpus (correctly) forbids copying them into prose, so they were
invisible from the docs. STUMBLES #1, the top stumble.
Fix: docs/canon/vocabularies.md is GENERATED from the consts by
'npm run docs:vocabularies' — the ONE sanctioned place the lists are spelled
out (generated = no drift objection), which every other doc links to. It
covers part archetypes (with a one-line role each), sema families with their
default hold/persistence + per-intent overrides, verbs by family, intents,
the perceptual-duration scale, haptic kinds, the 33 palette scales, sizes,
variant archetypes, and the shared common.* strings.
- scripts/docs-vocabularies.ts imports the consts (robust — no fragile
JSDoc/regex parsing) and formats them; the generation is an exported
function so docs-check can compare.
- Added ARCHETYPE_DESCRIPTIONS to morfo/types.ts (co-located with
ARCHETYPE_VOCABULARY, satisfies Record<MorfoArchetype,string>) as the
authoritative one-liners the appendix reads — descriptions become
first-class data instead of inline union JSDoc a tool would have to parse.
- docs-check gains I7: it regenerates in-memory and fails if the committed
file drifts from the consts (negative-tested: corrupt -> error, regen ->
green). The appendix's own counts pass I1, so I1 doubles as a second guard.
- Linked from README (E2 canon row + two 'I want to' shortcuts), CANON.md
(doctrine here, enumerated lists there), architecture/morfo.md (pick a
part's archetype), and the component-audit minimum-package list (STUMBLES
#8).
docs:check 0 errors; npm run check unchanged in my files.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Cleared the error-severity flags that were mature components missing a
documented exception, not real gaps:
- 4 service components (format-date, format-number, relative-time, trans)
get their E-2.2 exception: they render text via $format/$langs with no
visual recipe — no {name}.css by design.
- alert-dialog gets its R-1.1 exception: it composes Dialog (soma reuses the
Dialog runtime, data-dialog-* markers reach the DOM, dialog.css paints the
chrome) — a [data-alert-dialog] root rule would style nothing.
- card gets its missing A-1.3 texts.label (+ langs entry, registered) —
card had no label at all.
C10 (maturity tiers) reframed after the classification exercise showed the
audit VERDICT is the maturity oracle, and 'experimental' is nearly empty
once exceptions are documented: stable = PASS (82); preview = NEEDS-WORK on
docs/demo only (E-2.3/F-1.x/D-*) — functionally sound but underdocumented,
which includes mature components like button/table/listbox (a README gap is
not immaturity); experimental/WIP = a real FUNCTIONAL gap or foreign track.
C10 folds into C8 (document the tier + exceptions when writing READMEs).
Real gaps registered, NOT silenced: float-panel A-3.7 (28 mutating keyboard
actions, 6 events — a morfo-completeness question), card-group (no morfo),
natural-time-picker A-3.1; announce/clipboard R-1.5 (live-region /
composed-focus) await their READMEs in C8 to host the exception.
npm run check at the 59 baseline; alert-dialog -> PASS.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Two real defects the audit's error-severity rules caught:
- onion-menu shipped no default export (E-1.3), unlike every sibling compound;
add 'export default OnionMenu'.
- table + tree-grid referenced 'var(--color-surface-subtle)' — a token that
does not exist (the surface ladder is default/raised/muted/overlay). With no
fallback it resolved to guaranteed-invalid, so the table header + striped rows
rendered with NO background (a silent visual bug R-2.6 flagged). Repointed both
to '--color-surface-muted' (the existing subtle-surface semantic, used for
readonly/muted contexts); verified in the browser the header now paints muted.
A dedicated 'subtle' surface tier is a theme-canon decision, deferred; chronos
(WIP, untouchable) keeps the same reference for its author.
npm run check at the 59 baseline; tree-grid R-2.6 clears.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Accumulated test debt (the eidos suite had been red across several feature
landings — THEME-SYS-1 named z-index, palette-first static donor library,
spin-field extraction, icon stroke-map refinement — none of them regressions;
baselined against HEAD). Each triaged stale-vs-bug by confirming current
behavior in code before touching a line.
Stale tests (code correct/improved, assertion not updated):
- z-index '76' -> 'var(--z-index-overlay-tooltip)' (the code is guarded by
THEME-SYS-1 in contracts.test.ts — the raw integer is forbidden).
- frost '80%' -> 'var(--depth-overlay-translucency, 80%)' (parameterized).
- C7 size-bundle coordinate ('--control-height-md' -> '--size-md-control-height').
- icon visual-stroke ratios 1.5->1.125, 1->0.923 (stroke map md 2->1.5, xxl
icon 48->52px; the arithmetic follows).
- delegate/CSS-only purple-9: the slim foundation legitimately emits the
role-referenced donor scales (purple=primary) as overridable :root defaults;
the real delegation guarantee is 'no baked [data-theme] block', which the
assertion now checks.
Test over-flagging (fixed the test, not the code):
- CSS-only shared layer: spin-field (no wrapper/index) excluded from the
wrapper/barrel/namespace contracts (CSS_ONLY_LAYERS).
- soma-runtime vars: 'var(--x, fallback)' is runtime-optional (the STUMBLES #7
soma->eidos surface) — only bare 'var(--x)' requires a recipe declaration.
- orphan check made corpus-wide: composition means a sibling consumes a
component's tokens (date-picker/month-grid consume calendar-control-*).
- natural-time-picker fixed-tone sky colors documented-exempt (Words-rail
doctrine).
- dialog drift-detector converted to a synthetic fixture (dialog.css no longer
ships the dead rule it self-tested against).
Real code fixes:
- declare '--pin-input-cell-readonly-bg' (I consumed it in C2b without
declaring it).
- remove 5 truly-dead tokens (drawer content-bg + 4 color-field
format-select/segment — 0 consumers anywhere in eidos/soma/web).
- clamp time-picker's 'size' to PopoverSize before forwarding (xs->sm):
TimePickerSize has xs, PopoverSize doesn't — a latent type bug my recipe
change surfaced via a complexity-budget flip. The fix dropped check 61->59,
also clearing navigation-menu-content of the same class.
card-group flagged as a REAL gap (KNOWN_MISSING_MORFO): it ships parts + chrome
but no morfo (morfo-first violation) — tracked, not silenced.
Verified: vitest src/uix/eidos 23 files / 289 tests green; npm run check 59
(better than the 61 baseline); component:audit 0 BROKEN.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The ⚠️ 'canon + guard, not consumed' asterisk is retired: the C7 sweep
made all 34 size-bearing recipes consume the bundle coordinate, so the
row is ✅ (canon + enforcement + consumption). The footnote also drops
the stale control/compact/dense archetypes (superseded by the 2026-06-17
1:1 override) — control text follows the typographic scale 1:1.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The size bundle (--size-{k}-control-height / -font-size / -icon-size,
theming/reference §5) was emitted + guarded but consumed by ONE recipe
(the toggle pilot). This sweep points every recipe at the canonical
size COORDINATE instead of the raw primitive of the same coordinate:
366 refs across 34 recipes (--control-height-{k} -> --size-{k}-control-
height, and the font-size / icon-size coordinates likewise). Retuning a
size's bundle now reaches every consumer; a recipe that maps a key to a
DIFFERENT size's coordinate stays visible as a deliberate deviation.
The bundle vars are pure aliases of the primitives, so this is
computed-value identical — verified in a live browser: the accordion
chain --accordion-trigger-min-height-md = --size-md-control-height =
--control-height-md all resolve to calc(36px * 1 * 1). The generated
diff is 366-for-366 pure name swaps, nothing else moved. base/xxxl have
no size bundle and correctly stay on the typographic primitive.
Guard: recipe-css-contract forbids the raw size-coordinate primitive in
recipe token values (var(--control-height-{k}) etc.) — the drift can't
creep back. This is the ⚠️->✓ that theming/notes' comparison table flags
as the size canon's remaining asterisk (canon + guard, now + consumption).
component:audit 0 BROKEN; npm run check at the 61-error baseline; the
size-bundle guard + palette-cascade guard both green.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Two user findings from the Knob build exercise (an agent building a new
component from the docs alone — STUMBLES.md).
Quarantine: the superseded fossils no longer share shelf space with the
live corpus. docs/old-deprecated/ (with an index README explaining what
lands there and pointing readers at docs/README.md) now holds the
executed audits and fix plans: fable_audit, fable-eidos-audit,
inherit_audit + inherit_fix_plan, ARCHETYPE_COHERENCE_AUDIT_2026-06-19
(still citable — the component-guide banner and the eidos components
README repoint to it), COMPONENT_COHERENCE_AUDIT. pendiente.md (a live
pending list, not a fossil) moved to docs/process/. docs-check treats
the folder as sealed chronicle (I1/I2 exempt; I6 skips its internal
links, as its README promises). Root-level *.md is now: README, CLAUDE,
AGENTS + the user's own working files.
STUMBLES fixes applied on the spot (the doc-class ones):
- #2 kind drift: the REAL enum is 'public' | 'private' | 'virtual'
(MorfoPartKind, 671/9/14 uses) — morfo.md omitted 'private', the
checklist invented 'internal' (0 uses). Both fixed; I2 gains the
phantom-'internal' guard. A-2.1's row now says what the audit script
actually checks (kebab only — the archetype may vary, the Toggle
provider-IS-trigger doctrine).
- #6: the langs catalog SHAPE (flat keys, per-language leaves, named
export, index registration) is now shown in morfo.md instead of only
its location.
- #8: component-audit s0 defines the minimum brief package as an
explicit 8-file list.
The engineering-class stumbles are registered as plan batches S1-S6
(generated vocabularies appendix, continuous-gesture trigger doctrine,
part-absent condition, Gesture.rotate, the soma->eidos CSS-var
contract, minor frictions). docs:check 0 errors, 11-warn baseline.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
palette-{slot} is now RESERVED recipe vocabulary: normalizeRecipeTokens
(render-css) appends the full per-scale color:{scale} cascade for every
donor scale in PALETTE_SCALES to any token named palette-track/element/
border/solid/solid-hover/text/contrast — author-declared color:*
declarations win, only absent scales are appended. This is the
structural end of the hand-maintained-subset era: a recipe opts into
the per-instance palette by naming the token, and the generator
guarantees all scales (the 31-vs-33 drift class cannot recur at this
layer). Applies to app-config recipes too, since the hook sits where
every RecipeTokenSet is normalized.
Button's seven manual spreads are gone from base.ts (the helpers moved
into the generator); its generated output is byte-identical. Toggle —
which already exposed palette-* tokens — universalized itself: +330
generated lines, and <Toggle color="teal"> / color="steel" verified
resolving in a live browser (steel and fuchsia are exactly the two
scales the hand-kept lists used to miss). eidos-lint now classifies
donor-scale values on data-color as sanctioned eidos-only vocabulary
(the TSC color:* axis extension) instead of invalid — the morfo enum
keeps declaring the semantic roles, per the pilot's deliberate
runtime-open design. Guard: recipe-css-contract pins the LAST scale of
PALETTE_SCALES for every palette-bearing recipe in the generated css.
Also: the stale '31 physical color scales' test TITLE says 33 (its body
already asserted 33). Recorded in the plan: the eidos suite carries 15
pre-existing failures in 6 files (baselined against HEAD before this
change) — their triage is its own batch. npm run check at the 61-error
baseline; lint toggle baseline green again.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>