# Continue with — STUMBLES S2–S6 + catalog coherence (hand-off 2026-07-03) > **Kickoff for a new session**: *"Lee docs/process/continue-with-stumbles-s2-s6.md > y continúa."* Everything below is verified against the code at commit > `116594e0`. Working rules unchanged: respond in Castilian, docs/comments in > English; NEVER touch `words/` `palabras/` `chronos/` `media-player` (foreign > WIP); commit per batch, `git reset -q` + stage only your own files; commit > messages via `-F file` (backticks in `-m` run command substitution — see the > memory note). Each systemic fix ships its guard. ## Where things stand (verified) - `component:audit`: **82 PASS / 48 NEEDS-WORK / 0 BROKEN**. The remaining NEEDS-WORK is almost all **docs completeness** (E-2.3 missing README, F-1.x sections) and **demos** (D-*, deferred to the web phase) — NOT functional. - `npm run check`: ~**64–65 errors**, ALL pre-existing/foreign (heroscrolling + animation demos, arts/connection, libs/reactive — the parallel runed/tabbable port drifted the tree; my baseline earlier this session was 59). None in the maintained catalog files I touched. Confirm with a stash-test if a number moves: my changes were net-neutral-to-negative on the count. - `npm run docs:check`: **0 errors, 11 warnings** (all foreign/known targets). - `vitest src/uix/eidos`: **289/289 green** (the long-red suite was triaged). - **Uncommitted foreign WIP in the tree** (do NOT commit): the runed/tabbable 0-dep port — `src/libs/dom/*`, `src/libs/reactive/index.ts`, `src/arts/adom/index.ts`, `package-lock.json`, deleted `CONTINUE*.md` / `src/audit-opus-4-6-26.md`. Another session owns these. ## Done this session (committed, on `alpha-0.1-sec-dom`) - **C7 size-bundle** (`6001d9bb`, `e83ca6ea`): 34 recipes consume the bundle coordinate; guard forbids the raw primitive; theming/notes size-canon row ⚠️→✅. - **Eidos test triage** (`bbd9c467`): 15 long-red tests → 289/289. Real fixes inside: pin-input readonly-bg declared, 5 dead tokens removed, time-picker `size` clamped to PopoverSize (check 61→59). - **onion-menu + surface-subtle bug** (`46d68ff2`): default export; and `--color-surface-subtle` was a PHANTOM token → table/tree-grid headers rendered with no background → repointed to `--color-surface-muted` (verified in browser). - **Functional flags + C10 reframe** (`730812c0`): 4 service E-2.2 exceptions (format-date/number, relative-time, trans), alert-dialog R-1.1, card A-1.3. **C10 reframed**: tier = audit verdict — stable=PASS (82), preview=NEEDS-WORK on docs/demo only (~35, incl. mature button/table — a README gap is not immaturity), experimental/WIP=real functional gap or foreign. C10 folds into C8. - **S1 — generated vocabularies appendix** (`116594e0`): `npm run docs:vocabularies` → `docs/canon/vocabularies.md` from the consts (STUMBLES #1). New `ARCHETYPE_DESCRIPTIONS` in morfo/types.ts; docs-check I7 freshness guard (negative-tested); linked from README/CANON/morfo/component-audit. ## NEXT — S2, S3, S4 ALL DONE; re-scope S5/S6 (and wire the Knob) A read-only scoping workflow (`scope-stumbles-s2-s6`) mapped each against the real code. S2/S3/S4 returned high-confidence plans (below). **S5 and S6 did the research but did not emit a structured plan — re-scope them first** (relaunch the workflow for just those two, or scope by hand; their prompts are in the committed workflow script `…/workflows/scripts/scope-stumbles-s2-s6-*.js`). ### S3 — `part-absent` morfo condition — ✅ DONE (`11504043`) Shipped as `{ when: 'part-absent', part }`, the exact inverse of `part-present`, across the four condition sites (types union + JSDoc, schema discriminated member, compile `collectConditionDeps`, resolver `shouldEmitMorfoEntry`) plus schema/compile tests and the `morfo.md` condition list. **One deviation from the plan below**: the plan claimed `checkCondition` "already covers it (doesn't discriminate)" — it DOES discriminate (`schema.ts` had `c.when === 'part-present'` hard-coded), so the unknown-part guard was extended to `part-present || part-absent` so an unknown-part `part-absent` throws too. `npm run check` = 59 errors (none mine); the pre-existing red morfo test `hoists role into staticAttrs` (Dialog `content` role intentionally undeclared, dialog-001) is unrelated → flagged separately. Original plan preserved below for provenance. The common ARIA pattern "aria-label only when there is no Label part" is not declarable. Add the inverse of `part-present`. Four surgical edits + a test, mirroring `part-present` exactly: - `src/uix/morfo/types.ts` (MorfoCondition union, ~L169–175): add `{ when: 'part-absent'; part: string }`; update the JSDoc. - `src/uix/morfo/schema.ts` (conditionObjectSchema discriminated union, ~L143–153): add `object({ when: literal('part-absent'), part: string() })`. The `checkCondition` part-reference validator (~L556–573) already covers it (it doesn't discriminate) — no change there. - `src/uix/morfo/compile.ts` (`collectConditionDeps`, ~L666–681): add the `condition.when === 'part-absent'` branch → `partRefs.add(condition.part)` (same as part-present). - `src/uix/morfo/resolver.ts` (`shouldEmitMorfoEntry`, ~L39–50): add the branch → `return !Boolean(bindings.parts?.[condition.part])` (inverted part-present at L44). - Test (`compile.test.ts` / runtime test): schema accepts it; unknown-part throws; compiler collects the dep; resolver returns the value when the part is absent/falsy and `undefined` when present. Integration: a Dialog-like aria-label gated on `{ when: 'part-absent', part: 'label' }`. - **Risk**: semantics are "part not in bindings.parts OR falsy value" (not strictly DOM-absent). Verify provider init populates the parts map for every part (even optional-unrendered) so absence is unambiguous. Non-breaking (no existing morfo uses it). ### S2 — "Continuous components" doctrine in `docs/architecture/sema.md` — ✅ DONE (`1da36ca6`) Shipped as a new `## Continuous components (drag, swipe, hold)` section after the emit contract (before `## The per-family intent policy`), covering: the single `runtime.trigger` door + `void` vs `await` (hold opt-out/in), `coincident` vs `post` for a moving value (Slider worked example), rAF throttling of continuous pointer emits vs unthrottled keyboard commit-set, and "a continuous event's cascade rule must not override the primitives the per-emit `overrides` control". Every claim adversarially fact-checked against the Slider/Drawer providers + morfo + sema pack (zero discrepancies). `docs:check` = 0 errors. Original plan preserved below for provenance. Pure doc addition, grounded in the REAL Slider/Drawer code (verified answers to the three open questions): - **(a) turn hold**: `runtime.trigger()` returns a Promise that resolves AFTER emit + the perceptual hold (`runtime.svelte.ts:742–749, 765–768`). `void trigger()` = fire-and-forget; `await trigger()` blocks until the hold ends. - **(b) sanctioned path**: ONLY `runtime.trigger()` — there is NO `emitEvent()` in the codebase. Slider uses trigger exclusively (handle-pick L297, handle-drag L263/322/330, commit-set L155); Drawer likewise (drag-start/progress/end L661/645/679). - **(c) throttling**: keyboard `commit-set` fires on EVERY keydown (`SliderThumbProvider.onkeydown` L485–522) — no built-in throttle. Throttling is drag-only, via rAF (Drawer `flushDragProgress` ~88ms L604/624–648; Slider `queueHandleDrag` L244–260). The Slider sema cascade for handle-drag sets only `channels:['sound','haptic']` and does NOT override pitch/gain, so per-emit overrides stay the source of truth (`sema/components/slider.ts:30–32`). - **Deliverable**: new `## Continuous components (drag, swipe, hold)` section after "## The emit contract" (~L121) covering: sequence `coincident` vs `post` (perceptual+structural indivisible vs commit-then-signal), route-through-trigger doctrine, rAF throttling for continuous gestures, and "a cascade rule must not override the primitives that per-emit values control". Slider = worked example. Keep prose on the invariant ("what the Promise represents"), point to code for tuning constants (don't hard-code ms). Verify with `docs:check`. ### S4 — `Gesture.rotate` layer (STUMBLES #3, the flagged gap) — ✅ DONE (`18b0d44e`) Shipped in `src/uix/soma/layers/gesture/` as the fourth specialization (`GestureRotate extends GestureBase`): `angleAbsolute` (0°=up, CW+, [0,360), CSS-rotate convention), `angleDelta` (seam-unwrapped, multi-turn), `angularVelocityDeg` (radius-independent via a dedicated tracker fed the accumulated angle), `AngularSnap` detents (wrap-aware), dismiss-by-degrees, `center: 'element'|{x,y}`. 9 tests, 0 type errors in the layer. Deviations from the plan below (all deliberate): angle convention taken from the REAL Knob consumer (`atan2(dx,-dy)` not `atan2(dy,dx)`); `referenceSize` omitted (no meaning for a radius-independent angle); `AxialDrag` is in `layers/manipulation` not the gesture layer (plan conflated it); CW/CCW lock deferred as planned. Two release-time snap bugs found by adversarial review were fixed pre-merge (px→deg projection scale + capped below a half-turn; sequential guard made circular under wrap). **A real consumer now exists AND is wired**: the Fable-built Knob was relocated out of `src/uix/active-uix/knob/` into its per-layer homes (morfo/langs/sema/soma/eidos) and rewired to consume `Gesture.rotate` (`c13cad74`): 162→59 type errors (0 in any knob file), 4 provider tests, no regressions; morfo now uses the S3 `part-absent` for aria-label; knob added to `recipe-css-contract` WIP_TRACKS until its recipe tokens land (css still md-only, no variants — the remaining polish). Original plan below. New angular gesture layer mirroring the existing ones in `src/uix/soma/layers/gesture/` (GestureBase → GestureDrag/GestureResize; AxialDrag is the single-axis peer). Consumers: Knob, AngleSlider, hue ring, clock dial. - `layers/gesture/types.ts`: add `RotateState` (angleDelta°, angleAbsolute 0–360, angularVelocityDeg), `RotateReleaseState` (+snappedTo, dismissed), `GestureRotateOpts` (extends GestureBaseOpts: `center` (`Active<{x,y}|'element'>`), `referenceSize`, `direction?`, `snapPoints?`, `snapToSequential?`, `dismissDegrees?`, `velocityThreshold?`, `wrap?=true`, onRotate/onRelease/ onDismiss/onSnapChange), `GestureRotateState`, `AngularSnap` (number = 0–1 fraction of 360 / >1 degrees / `'45deg'`/`'45°'`/`'50%'`). - `layers/gesture/gesture.svelte.ts`: `GestureRotate extends GestureBase`. Center detection (`'element'` → getBoundingClientRect center vs explicit coords); angle via `atan2(dy,dx)` normalized [0,360); `unwrapDelta` for the 0/360 crossing (`|delta|>180 ? delta∓360`); velocity on the unit circle (VelocityTracker, 100ms window / 50ms staleness — reuse the existing tracker); override handlePointerDown/onMove/onRelease/onCancelled; `snapToAbsoluteDegrees` + `resolveSnapAt` with the sequential-neighbour guard (`|i-current|>1` blocked, like AxialDrag). Export `Gesture.rotate(opts)` matching drag/resize. - `layers/gesture/index.ts`: export the new types. - Test (`gesture.test.ts`): center detection; angle unwrap across 0°/180°; velocity (stationary/flick-capped/stale); snap (closest, sequential guard, velocity projection); dismissal by absolute offset; single-pointer only. - **Risks**: 0/360 unwrap correctness (normalize delta to [-180,180] before distance in snap, esp. with wrap so 350°↔10° are adjacent); angular velocity via unit-circle derivative underestimates at large radii; no CW/CCW direction locking in v1 (defer). All dom listeners via `dom.listen` like the peers. ### S5 / S6 — re-scope, then implement - **S5** (soma→eidos CSS-var contract, STUMBLES #7): the agent researched `--drawer-progress`-style vars but didn't emit the plan. Re-scope to get 2–3 concrete options (e.g. a morfo `cssVars` declaration validated against provider writes + eidos reads; or an eidos-lint cross-check of provider-written vs recipe-consumed vars) with a recommendation. Design-open — get the tradeoffs before implementing. - **S6** (minor frictions): four doc notes to author from the real defs — `state()` vs `$state` (pass-by-ref vs local), `role` on a DOM-rendering Provider (required vs omittable), `Without<>`/`PrimitiveDivAttributes` definitions, gesture pointermove/up ownership. Land them in soma.md / soma-architecture.md. ## After the S-series — remaining coherence plan `docs/process/PLAN-component-coherence.md` is the master. Left: - **C8** (heaviest): 28 missing READMEs (E-2.3) + incomplete F-1.x sections. This is where the deferred exceptions land: announce/clipboard R-1.5 (live-region / composed-focus), natural-time-picker R-2.1 (fixed-tone sky) + A-3.1, card/card-group/drag-drop/virtual-list. Write real Baseline/Comparativa/ Decisiones/Gaps (per-component reference research) — no skeletons. - **C9**: motion eventful gap (which interactives should express an event signature; criterion: high-frequency = sober, D.5). - **Real gaps tracked, NOT silenced**: float-panel **A-3.7** (28 mutating keyboard actions, 6 events — morfo-completeness question); **card-group** has no morfo (morfo-first violation, `KNOWN_MISSING_MORFO` in lint.test.ts); natural-time-picker **A-3.1**. ## Verify-before-done checklist (unchanged doctrine) `npm run check` (compare to the drifting foreign baseline, not 59) · relevant `vitest` scope · `component:audit --only {kebab}` per touched component · `npm run docs:check` after doc/vocab changes · browser check for anything visual (Playwright from repo-root; the preview MCP is flaky — memory note).