Stateful deep audit of the whole UIX component system across 8 batches
(words/palabras/chronos excluded). Read-only: zero code changes.
Result: 0 CRITICAL · 18 HIGH · 129 MEDIUM · 60 LOW.
Deliverables (audit/):
- components/{kebab}.md one machine-reparseable report per component
- 00_WORKLIST.md resumable index (all audited)
- SUMMARY.md verdict + systemic findings + per-batch counts
- THEMING_COHERENCE.md E-bis coherence roll-up (roles/sizes/variants clean)
- SHARED_EXTRACTION.md extraction opportunities (EX-3 selectedSet highest value)
- VALIDATOR_GAPS.md 12 proposed validator extensions (prevent the class)
- FIX_PLAN.md prioritized, executable fix roadmap (Phase 1 = SYS-7)
Method: per-batch adversarial workflow (analyze -> skeptical verify), with
every HIGH/CRITICAL lead-verified against the cited source + independent greps.
The verify stage refuted false positives in both directions (eidos->soma,
CSS.escape on framework values, inert archetype metadata, content-color slots
mistaken for non-canonical roles).
Top systemic finding: SYS-7 — A31 O(N^2) per-item .includes selection across 9
components (6 HIGH), one ~3-line selectedSet/expandedSet lift each.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
active-uix
parent
89e69b5d0f
commit
25ce77c142
@ -0,0 +1,142 @@
|
||||
# Validator gaps — rolling
|
||||
|
||||
updated-at: 2026-06-26
|
||||
batches-complete: 1
|
||||
|
||||
For each behavioral/contract/theming finding that a validator *should* have caught but didn't, the proposed
|
||||
validator extension. Fixing the validator prevents the whole class — highest leverage.
|
||||
|
||||
## VG-1 · Coherence guard misses `em`/`%` font-size literals <!-- id: VG-001 -->
|
||||
- finding: select-008 (`item-description-font-size: '0.85em'`).
|
||||
- which validator: `recipe-css-contract.test.ts` (THEMING §5 guard: "no recipe `font-size-*` token may be a
|
||||
literal px/rem — must reference `--font-size-*`").
|
||||
- gap: the guard checks **px/rem** literals only; a bare `em` (or `%`) font-size slips through.
|
||||
- proposed: extend the guard to reject ANY non-`var()` value on a `*font-size*` recipe token (px/rem/em/%/number).
|
||||
- effort: low (regex/value-shape check already exists).
|
||||
|
||||
## VG-2 · No check for magic z-index / magic opacity / magic letter-spacing in recipes <!-- id: VG-002 -->
|
||||
- findings: SYS-2 (content-z/overlay-z 5/5), dropdown-menu-003 (`0.55` opacity), select-010 (`0.04em` tracking).
|
||||
- which validator: none (THEMING §35-C is doctrine, not enforced).
|
||||
- gap: bare numeric `z-index` values, bare opacity decimals where `--opacity-*` exists, and bare `em`
|
||||
letter-spacing where `--tracking-*` exists are not flagged anywhere.
|
||||
- proposed: a recipe-token lint that flags: (a) a `*-z`/`z-index` token whose value is a bare integer;
|
||||
(b) an `*opacity*` token whose value is a bare decimal (suggest `--opacity-*`); (c) a `*letter-spacing*`/
|
||||
`*tracking*` token whose value is a bare `em` (suggest `--tracking-*`). Allow an explicit `// coherence-ok:`
|
||||
escape for genuinely physical values (scrim opacity).
|
||||
- effort: medium.
|
||||
|
||||
## VG-3 · No check for `syncAttrs:true` + manual re-set of a morfo-declared attr <!-- id: VG-003 -->
|
||||
- findings: dialog-001 (role/aria-roledescription — live a11y race), popover-001 (Close type/aria-label).
|
||||
- which validator: none (morfo:check validates the morfo, not the provider's props block).
|
||||
- gap: a part registered `syncAttrs: true` whose `props` `$derived` re-declares a key the morfo already
|
||||
resolves is a double-write (active_architecture §7.7) — undetected.
|
||||
- proposed: a provider lint (AST or grep-assisted) — for each `runtime.part(part, { syncAttrs: true })`, flag
|
||||
any literal key in that part's `props` object that matches a morfo-declared attr name for that part
|
||||
(role / aria-* / type / the data-* WITH a value source). Hard part: associating the props block with the
|
||||
part; a heuristic grep (`syncAttrs: true` in a class + `role:`/`aria-…:`/`type:` in its `props`) catches most.
|
||||
- effort: medium-high; high value (a11y correctness).
|
||||
|
||||
## VG-4 · No check that `aria-activedescendant` (virtual focus) excludes real `dom.focus()` on items <!-- id: VG-004 -->
|
||||
- finding: combobox-001 (A17 violation — virtual + real focus mixed).
|
||||
- which validator: none.
|
||||
- gap: A17 is doctrine; nothing flags a provider that both declares `aria-activedescendant` in its morfo AND
|
||||
calls `dom.focus(item)` / `el.focus()` on its option/item elements.
|
||||
- proposed: a lint — if a component's morfo declares `aria-activedescendant` on any part, its provider file
|
||||
must not contain `dom.focus(` / `.focus()` on item-class elements (heuristic: `focus(` near `getItems`/
|
||||
`items[`). Pair with a `perm:check` permutation that asserts `document.activeElement` stays on the input.
|
||||
- effort: medium (lint heuristic) + the perm-runner assertion (higher fidelity).
|
||||
|
||||
## VG-5 · No check that morfo `focus.trap` is actually implemented <!-- id: VG-005 -->
|
||||
- finding: select-002 (morfo `focus.trap:true` + `initial:'first-focusable'`, provider has no FocusScope).
|
||||
- which validator: none.
|
||||
- gap: the morfo `focus` block is declarative but nothing verifies the provider instantiates `FocusScope`
|
||||
with a matching `trap`. Conversely a virtual-focus component (aria-activedescendant) declaring `trap:true`
|
||||
is contradictory.
|
||||
- proposed: (a) lint — if morfo `focus.trap:true`, the provider must reference `FocusScope.use`; (b) flag the
|
||||
contradiction `aria-activedescendant` + `focus.trap:true` in the same morfo.
|
||||
- effort: low-medium.
|
||||
|
||||
## VG-6 · `translationRef` key not validated against the `texts` map <!-- id: VG-006 -->
|
||||
- finding: combobox-003 (passes the full idlangref `'#?components.combobox.toggle|Toggle'` as the key instead
|
||||
of the `texts` key `'toggle'`).
|
||||
- which validator: morfo:check / translations:check (catches missing runtime keys, but an absolute idlangref
|
||||
resolves, so it stays silent).
|
||||
- gap: `v.translationRef(key)` where `key` is not a declared `texts` entry (and especially when it's a literal
|
||||
`#?…` idlangref) bypasses the indirection and leaves `texts` entries dead — not flagged.
|
||||
- proposed: morfo lint — `v.translationRef(key, …)` `key` MUST be a key of the morfo's `texts` map; reject a
|
||||
`key` that starts with `#?` (that's an absolute ref, belongs in `v.commonRef`/an idlangref constant, not
|
||||
`translationRef`).
|
||||
- effort: low.
|
||||
|
||||
## VG-7 · eidos-lint not enforced for undeclared parts <!-- id: VG-007 -->
|
||||
- findings: select-005 (`data-select-indicator`/`item-indicator`), dialog-006 (`data-dialog-header`/`footer`).
|
||||
- which validator: `scripts/eidos-lint.ts` — DOES classify `[data-{c}-{part}]` selectors as
|
||||
morfo-backed / eidos-only / invalid, but it's opt-in (THEMING/TSC framing: "secondary safety net").
|
||||
- gap: undeclared `data-{c}-{part}` selectors aren't blocked; there's no agreed disposition for legitimate
|
||||
eidos-only decorative/layout parts.
|
||||
- proposed: (a) run eidos-lint in CI with an allow-list file for sanctioned eidos-only parts;
|
||||
(b) adopt SYS-6's convention (declare decorative parts `kind:'internal'` in the morfo) so the lint can be
|
||||
strict.
|
||||
- effort: low (wiring) + the per-component convention decision.
|
||||
|
||||
## VG-8 · `morfo:check` does not flag `: Morfo` instead of `as const satisfies Morfo` <!-- id: VG-008 -->
|
||||
- finding: alert-dialog-001 (HIGH) — `export const alertDialogMorfo: Morfo = {…}` (the only morfo in 16 audited
|
||||
using the annotation form).
|
||||
- which validator: `morfo:check` / `npm run check` (TS compiles fine — `: Morfo` is valid TS, just lossy).
|
||||
- gap: the `: Morfo` annotation widens literal types (kebabs, `v.literal(...)` values, part names) so
|
||||
`compileMorfo`/`createAttrs` lose exact-key narrowing — silent, no error.
|
||||
- proposed: a trivial lint/grep over `src/uix/morfo/components/*.ts` — every `export const *Morfo` must be closed
|
||||
with `as const satisfies Morfo` and must NOT carry a `: Morfo` annotation. (The single cheapest high-value gap.)
|
||||
- effort: trivial.
|
||||
|
||||
## VG-9 · No check for an undisposed `$effect.root` in a provider <!-- id: VG-009 -->
|
||||
- finding: navigation-menu-006 (HIGH) — `openedAtEffect = $effect.root(...)` whose returned disposer is stored
|
||||
but never called → a detached reactive root leaks per trigger instance.
|
||||
- which validator: none.
|
||||
- gap: `$effect.root` deliberately escapes the component effect scope and MUST be manually disposed; nothing flags
|
||||
the case where the returned cleanup isn't wired into a teardown. (Legit uses are tests, which DO call the cleanup.)
|
||||
- proposed: a lint — `$effect.root(` in a non-test `*-provider.svelte.ts` whose return value is assigned to a field
|
||||
that is never invoked in a teardown (`$effect(() => () => field())` / dispose()) → flag. Heuristic grep:
|
||||
`$effect.root` in a provider + no matching invocation of the assigned identifier.
|
||||
- effort: medium.
|
||||
|
||||
## VG-10 · No check that a declared morfo event is ever fired (inert events) <!-- id: VG-010 -->
|
||||
- finding: SYS-INERT (B3) — date/date-range/time/time-range/color-picker each declare `open` + `commit-reset`
|
||||
events the provider never calls `runtime.trigger(...)` for (only `close` + field/area events are fired).
|
||||
- which validator: none (`smoke`/`morfo:check` validate the morfo shape, not whether the provider exercises it).
|
||||
- gap: an event declared in `morfo.events[]` but never reached by any `runtime.trigger('<name>')` in the provider
|
||||
is dead contract — it exists only to pass the schema. Lead-confirmed by grep (trigger-call counts per picker).
|
||||
- proposed: a lint that, per component, intersects `morfo.events[].name` with the set of string literals passed to
|
||||
`runtime.trigger(...)` / `.trigger(...)` in `{kebab}-provider.svelte.ts`; warn on any declared event with zero
|
||||
trigger sites. (Polymorphic `close` fired via `dismissWith` is reached — match the resolved name, or allow an
|
||||
opt-out comment.)
|
||||
- effort: medium. Catches a recurring picker-family contract gap.
|
||||
|
||||
## VG-11 · No check for `$effect`-wrapped parent-id registration (A30 doctrine) <!-- id: VG-011 -->
|
||||
- finding: SYS-A30-EFFECT (B4) — date-field/time-field/color-field register `field.inputId.current = opts.id.current`
|
||||
inside a `$effect` instead of a direct constructor assignment (number-field is the correct reference).
|
||||
- which validator: none (`morfo:check`/`perm:check` don't inspect the registration shape; no loop fires today).
|
||||
- gap: A30 mandates direct assignment for id registration; a `$effect` that writes a parent's `*Id.current` is the
|
||||
latent-freeze shape even when currently write-only. Nothing flags it.
|
||||
- proposed: a lint — a `$effect(...)` body whose only statement writes `<parent>.<x>Id.current = opts.id.current`
|
||||
(a parent-id registration) → warn "use a direct constructor assignment (A30)". Composes with VG-9 ($effect.root).
|
||||
- effort: medium.
|
||||
|
||||
## VG-12 · No check for `sequence:'pre'` event whose runtime handler sets the bound state (the lag class) <!-- id: VG-012 -->
|
||||
- finding: collapsible-NEW-001 (HIGH) — `collapse` is `sequence:'pre'` and the runtime `events.collapse` handler does
|
||||
`this.opts.open.current = false`; the runtime serializes emit-then-handler, so the state lags the ~240ms hold (the
|
||||
documented Checkbox 244ms-lag class, which was fixed by flipping to `post`).
|
||||
- which validator: none. `morfo:check` knows the `sequence` but not where the provider sets the state.
|
||||
- gap: the doctrine "a control that sets functional state INSIDE the runtime.trigger handler must use `sequence:'post'`"
|
||||
is enforced by humans only. A `pre` event whose `events: { <name>: () => { ...opts.X.current = ... } }` handler
|
||||
mutates a bound state is a latent lag — undetected.
|
||||
- proposed: a lint over each provider's `runtime(...)` `events` map: if `events.<name>` body assigns to an
|
||||
`opts.*.current` / bound state AND the morfo declares that event `sequence:'pre'` → warn "set state at the call-site
|
||||
or use `sequence:'post'` (lag)". (Skip `emerge.dismiss`/`close` events that drive a Presence/`data-event` exit — those
|
||||
legitimately defer; the heuristic: flag only when no exit-animation path consumes the hold.) Pairs with `perm:check`
|
||||
which could measure the click→state-change latency.
|
||||
- effort: medium; high value (catches a perceptible-UX regression class the morfo author can rationalize wrongly).
|
||||
|
||||
## Notes
|
||||
- VG-3, VG-4 (B1) and VG-8 (B2) are the highest-value mechanical wins (each catches a real shipped defect class).
|
||||
VG-8 is the single cheapest — a grep would have caught alert-dialog-001. Re-evaluate as later batches recur.
|
||||
@ -0,0 +1,36 @@
|
||||
# Audit: accordion
|
||||
audit-version: 1
|
||||
audited-at: 2026-06-26
|
||||
scope:
|
||||
method: adversarially-verified workflow; HIGH lead-verified. B6 ground-truth: checkbox/toggle/switch commit-toggle = sequence post (lag-fixed); radio-group/tabs/accordion/stepper set state at call-site (pre OK); toggle-group + checkbox-group carry the A31 .includes pattern (SYS-7); slider has no gesture-layer A6 leak.
|
||||
provider: G:\dev\svelte\vicen\src\uix\soma\components\accordion\accordion-provider.svelte.ts
|
||||
sequence-audit: State set at call-site (line 104 in setValue) before events fire (lines 107-108). Morfo declares both 'open' and 'close' events as sequence='post'. State change precedes trigger emission; perceptual signal acknowledges resolved state, not delaying reveal. Compliant with intentional design documented
|
||||
|
||||
## Summary
|
||||
Counts (post-verification): CRITICAL 0 · HIGH 0 · MEDIUM 0 · LOW 0.
|
||||
|
||||
## Findings
|
||||
_No findings survived verification (clean component)._
|
||||
|
||||
## No-findings dimensions
|
||||
A: Morfo Contract, B: Behavior & Sequence, B: Roving Tabindex (A14), B: RTL Directional Keys (A12), B: Gesture & Observer Disposal (A15, A6), B: Hidden Input Form Participation (A13), B: ID Registration (A30), B: O(N²) Detection (A31), B: State Reactivity (A33), B: Keyboard Navigation, C: DOM Selector Safety, D: Soma-Eidos Frontier, E: TSC Token Composition, E-bis: Theming & Magic Literals, F: Test Coverage, G: Redundancy
|
||||
|
||||
## Theming facts (E-bis)
|
||||
- magic z-index: none
|
||||
- magic literals: none
|
||||
- undeclared parts: none
|
||||
- roles clean: true · variants clean: true
|
||||
|
||||
## Tests (F)
|
||||
- exists: true · env: jsdom
|
||||
- covers: toggle state coordination; single/multiple mode; keyboard navigation (roving triggers); aria-disabled when single non-collapsible; header/content props wired to item state; item registration/unregistration
|
||||
- untested:
|
||||
|
||||
## Style observations (non-blocking)
|
||||
- Morfo uses 'as const satisfies Morfo' (strict typecheck enabled)
|
||||
- CSS variables follow --accordion-{slot} naming with role aliases for variants (outline|surface|ghost)
|
||||
- ResizeObserver$ wraps observer disposal in $effect lifecycle
|
||||
- Nested accordion support via closest() filter excludes siblings
|
||||
- Parts registration matches morfo declarations (5/5: provider, item, header, trigger, content)
|
||||
- Item id-registration direct (not $effect-wrapped)
|
||||
- Keyboard routing uses getDirectionalKeys(dir, orientation) for RTL/LTR/orientation awareness
|
||||
@ -0,0 +1,42 @@
|
||||
# Audit: alert-dialog
|
||||
audit-version: 1
|
||||
audited-at: 2026-06-26
|
||||
scope: ['soma', 'eidos'] (composes/delegates close to Dialog; Provider is virtual)
|
||||
method: adversarially-verified workflow (analyze → refute); HIGH/CRITICAL personally re-verified against cited code by the lead.
|
||||
provider: G:\dev\svelte\vicen\src\uix\soma\components\alert-dialog\alert-dialog-provider.svelte.ts
|
||||
|
||||
## Summary
|
||||
Counts (post-verification): CRITICAL 0 · HIGH 1 · MEDIUM 0 · LOW 0.
|
||||
systemic hits: none.
|
||||
|
||||
## Findings
|
||||
### HIGH: A Contract(morfo): morfo MUST be 'as const satisfies Morfo' — alert-dialog-001 <!-- id: alert-dialog-001 -->
|
||||
- dimension: A
|
||||
- rule: A Contract(morfo): morfo MUST be 'as const satisfies Morfo'
|
||||
- location: G:\dev\svelte\vicen\src\uix\morfo\components\alert-dialog.ts:4
|
||||
- evidence: export const alertDialogMorfo: Morfo = { ... }
|
||||
- impact: Type narrowing degraded in createAttrs; morfo loses const assertion benefits and exhaustiveness checks
|
||||
- repro: Build and check TypeScript diagnostics; createAttrs will not narrow string literal keys correctly
|
||||
- proposed-fix: Change to: export const alertDialogMorfo = { ... } as const satisfies Morfo;
|
||||
- verify: [confirmed] Read src/uix/morfo/components/alert-dialog.ts line 4 verbatim: `export const alertDialogMorfo: Morfo = {`. This is a bare type annotation, not the canonical `as const satisfies Morfo` form. The file ends at line 76 with `};` (no `as const satisfies Morfo` closer). Confirmed against the four baseline morfos which all use the canonical pattern: dialog.ts:15 `export const dialogMorfo = {` closing at :303 `} as const satisfies Morfo;`, popover.ts:4 closing at :251, drawer.ts:4 closing at :313, toggle.ts:22 closing at :143. The `: Morfo` annotation widens literal types (e.g. `kebab: 'alert-dialog'`, part kebabs `'provider'|'action'|'cancel'`, the `v.literal('button')` aria values) to their base types, degrading the exact-key narrowing compileMorfo/createAttrs depend on — exactly the rule-A violation described. Severity HIGH is appropriate: it is a contract-shape rule the morfo validators should catch.
|
||||
- fix-status: open
|
||||
|
||||
## No-findings dimensions
|
||||
B, C, D, E, E-bis, F, G
|
||||
|
||||
## Theming facts (E-bis)
|
||||
- magic z-index: none
|
||||
- magic literals: none
|
||||
- undeclared parts: none
|
||||
- roles clean: true · variants clean: true
|
||||
|
||||
## Tests (F)
|
||||
- exists: true · env: jsdom
|
||||
- covers: action/cancel data attribute projection; aria-label resolution and reactivity; onclick close delegation to Dialog; callback execution on button click
|
||||
- untested: keyboard interaction (delegated to Dialog); focus trap/return (delegated to Dialog); Escape key behavior override (delegated to Dialog)
|
||||
|
||||
## Style observations (non-blocking)
|
||||
- AlertDialog properly delegates all state/focus/keyboard to Dialog with variant='alertdialog' forced
|
||||
- Escape key behavior override (close→ignore regression fix) is well-documented in README and implemented via wrapper
|
||||
- Eidos Action/Cancel properly forward dialog intent to Button via snippet binding
|
||||
- Action emits intent-driven color (risk→red), Cancel stays neutral—clear visual contrast matching WAI-ARIA guidance
|
||||
@ -0,0 +1,47 @@
|
||||
# Audit: announce
|
||||
audit-version: 1
|
||||
audited-at: 2026-06-26
|
||||
scope:
|
||||
method: adversarially-verified workflow; HIGH lead-verified. B7 ground-truth: Toolbar A35 + Form A36 incidents STAYED FIXED (untrack present); toast/clipboard/drag-drop use uix.timers (no A6 leak); button is clean (its archetype finding was inert — Spinner not runtime-registered); data-size/data-shape are eidos visual attrs (not contract violations).
|
||||
provider: src/uix/soma/components/announce/announce-provider.svelte.ts
|
||||
cleanup-audit (A6/A35/A36): Line 75-77: Single $effect with cleanup return (clearTimers()). Disposes: politeTimer and assertiveTimer via cancel() if present (lines 149-161). Timers are scheduled via soma.uix.timers.schedule() (A6 compliant). Global createAnnouncer (global.svelte.ts): timers stored in RegionPair.timer and cance
|
||||
|
||||
## Summary
|
||||
Counts (post-verification): CRITICAL 0 · HIGH 0 · MEDIUM 2 · LOW 0.
|
||||
|
||||
## Findings
|
||||
### MEDIUM: eidos CSS selectors must match morfo-declared or runtime-auto-generated attributes. A decl — announce-001 <!-- id: announce-001 -->
|
||||
- dimension: E-bis: Theming / Selector drift
|
||||
- rule: eidos CSS selectors must match morfo-declared or runtime-auto-generated attributes. A declared prop like `visuallyHidden` must have a corresponding data attribute or inline style to drive styling.
|
||||
- location: src/uix/eidos/components/announce/announce.css:23
|
||||
- evidence: Line 23 uses selector `[data-announce-region]:not([data-visually-hidden])` to style visible regions. However, `data-visually-hidden` attribute is never written by soma. The provider (announce-provider.svelte.ts:246) writes inline `style` when hidden, but never writes a `data-*` attribute to signal visibility state to eidos.
|
||||
- impact: The CSS selector `:not([data-visually-hidden])` always matches because the attribute is never present. This means card styling (display, padding, border, etc.) is incorrectly applied to ALL regions, including those where `visuallyHidden=true` (which should be sr-only with no card chrome).
|
||||
- repro: Mount an Announce.Region with `visuallyHidden={false}`. The region will receive card styling (padding, border, etc.). Mount another with `visuallyHidden={true}` (default). It will ALSO receive card styling, breaking the sr-only case.
|
||||
- proposed-fix: Option A: Modify AnnounceRegionProvider.props (line 242-248) to write `data-visually-hidden: hidden ? '' : undefined` so the CSS `:not([data-visually-hidden])` selector works correctly. Option B: Change the CSS selector to check for inline style presence (not reliable) or add a separate `data-visible` attribute when visuallyHidden is false. Recommend Option A with a morfo data declaration for the attribute.
|
||||
- verify: [downgraded] MECHANISM CONFIRMED, IMPACT REFUTED. Confirmed: `data-visually-hidden` is NEVER written anywhere. Morfo (announce.ts:88-99) declares only `data-role`+`data-live` on the `region` part; the provider (announce-provider.svelte.ts:243-247) drives `visuallyHidden` ONLY via inline `style: hidden ? VISUALLY_HIDDEN_STYLE : undefined`; grep across soma+eidos finds no writer of the attr (announce.css:23 is the sole reference). So `[data-announce-region]:not([data-visually-hidden])` (announce.css:23) ALWAYS matches — the negation is dead/non-functional CSS; the 'visible regions only' guard the author intended never excludes the sr-only case. THAT part is a real selector-drift defect (E-bis), MEDIUM stands. BUT the candidate's IMPACT ('incorrectly applied to ALL regions ... breaking the sr-only case') is REFUTED: when visuallyHidden=true, soma sets the inline sr-only style (`position:absolute;width:1px;height:1px;overflow:hidden;clip:rect(0,0,0,0)`) which has higher specificity than the stylesheet rule and overrides `padding:0`/`border:0` inline (lines 12-22). The leftover `display:block`/`background`/`border-radius`/`font-size`/`line-height` from the CSS block are visually inert on a 1x1px clipped box — the region stays hidden. So the sr-only case is NOT broken; the defect is dead CSS / a non-functional visible-vs-hidden distinction, not an a11y/visual regression. Keeping MEDIUM but correcting the rationale/impact.
|
||||
- fix-status: open
|
||||
|
||||
### MEDIUM: eidos CSS selectors must key off attributes the morfo declares or soma actually writes; a — announce-101 <!-- id: announce-101 -->
|
||||
- dimension: E-bis: Theming / selector drift, SYS-1-adjacent
|
||||
- rule: eidos CSS selectors must key off attributes the morfo declares or soma actually writes; a `:not([attr])` guard against an attribute that is never emitted is dead CSS that silently never differentiates state.
|
||||
- location: src/uix/eidos/components/announce/announce.css:23
|
||||
- evidence: `[data-announce-region]:not([data-visually-hidden]) { display:block; padding:...; border:...; background:...; }` — but `data-visually-hidden` is never written: announce.ts:88-99 declares only `data-role` and `data-live` on the region part, and announce-provider.svelte.ts:243-247 drives `visuallyHidden` exclusively via inline `style` (`hidden ? VISUALLY_HIDDEN_STYLE : undefined`), never a data attribute. Grep across soma+eidos confirms no writer.
|
||||
- impact: The intended 'visible regions only' branch is non-functional: the `:not()` negation always passes, so the visible-card rule applies to every region. It is masked in the default (visuallyHidden=true) case because the higher-specificity inline sr-only style clips the box, so there is no user-visible regression today — but the CSS no longer expresses the visible-vs-hidden intent and will silently mis-style if the inline style ever changes. Dead-code distinction.
|
||||
- repro: Mount `<Announce.Region visuallyHidden={false}>` and `<Announce.Region visuallyHidden={true}>`; inspect computed styles — the visible-card rule (display:block/background/font-size) resolves on BOTH because `:not([data-visually-hidden])` matches both; only the inline sr-only clip differentiates them, not the CSS branch as intended.
|
||||
- proposed-fix: Either (a) project the state as a real attribute the CSS can key on — e.g. declare `data-visually-hidden` in the region part's morfo `data` and have soma write it from `opts.visuallyHidden`, then keep `:not([data-visually-hidden])`; or (b) gate the visible-card chrome on an opt-in attribute the consumer sets (e.g. `data-variant='visible'`) rather than a negation of a never-present attr. Option (a) keeps the existing selector working.
|
||||
- verify: [verifier-added] added by adversarial verify pass
|
||||
- fix-status: open
|
||||
|
||||
## No-findings dimensions
|
||||
A: Contract (morfo), B: Behavior (A35/A36/A6/A15/live-regions/A30/A33/A31), C: DOM-selector, D: Frontier, E: TSC, F: Tests, G: Redundancy
|
||||
|
||||
## Theming facts (E-bis)
|
||||
- magic z-index: none
|
||||
- magic literals: none
|
||||
- undeclared parts: none
|
||||
- roles clean: true · variants clean: true
|
||||
|
||||
## Tests (F)
|
||||
- exists: true · env: jsdom via vitest
|
||||
- covers: timer scheduling and cancellation via soma.uix.timers; A/B toggle for repeated announcements; role/aria-live derivation; declarative region attrs sync; standalone region without provider; provider context context injection
|
||||
- untested: visible region CSS application (visuallyHidden=false styling); async timer edge cases under network throttle; cross-document announcer (createAnnouncer in iframe/shadow DOM)
|
||||
@ -0,0 +1,19 @@
|
||||
# Audit: aspect-ratio
|
||||
audit-version: 1
|
||||
audited-at: 2026-06-26
|
||||
scope: eidos (eidos-only primitive)
|
||||
method: theming-coherence category sweep (layout), adversarially verified; roles + `:Morfo` lead-verified CLEAN across all eidos (no success/warning/danger/info; only alert-dialog uses `:Morfo`).
|
||||
|
||||
## Summary
|
||||
Counts (post-verification): CRITICAL 0 · HIGH 0 · MEDIUM 0 · LOW 0. Theming-clean.
|
||||
conformance: Contract OK, no recipe entry, CSS-driven via --aspect-ratio variable, child fill via 100% inline/block-size.
|
||||
as-const: true · roles clean: true · variants clean: true · has-recipe: false
|
||||
|
||||
## Findings
|
||||
_No findings (theming-clean primitive)._
|
||||
|
||||
## Theming facts (E-bis)
|
||||
- magic literals: none
|
||||
- magic z-index: none
|
||||
- non-canonical names: none
|
||||
- roles clean: true · variants clean: true
|
||||
@ -0,0 +1,19 @@
|
||||
# Audit: auto-grid
|
||||
audit-version: 1
|
||||
audited-at: 2026-06-26
|
||||
scope: eidos (eidos-only primitive)
|
||||
method: theming-coherence category sweep (layout), adversarially verified; roles + `:Morfo` lead-verified CLEAN across all eidos (no success/warning/danger/info; only alert-dialog uses `:Morfo`).
|
||||
|
||||
## Summary
|
||||
Counts (post-verification): CRITICAL 0 · HIGH 0 · MEDIUM 0 · LOW 0. Theming-clean.
|
||||
conformance: Contract OK, composes through Grid, no recipe entry (marker selector only, template computed in JS).
|
||||
as-const: true · roles clean: true · variants clean: true · has-recipe: false
|
||||
|
||||
## Findings
|
||||
_No findings (theming-clean primitive)._
|
||||
|
||||
## Theming facts (E-bis)
|
||||
- magic literals: none
|
||||
- magic z-index: none
|
||||
- non-canonical names: none
|
||||
- roles clean: true · variants clean: true
|
||||
@ -0,0 +1,19 @@
|
||||
# Audit: avatar-group
|
||||
audit-version: 1
|
||||
audited-at: 2026-06-26
|
||||
scope: eidos (eidos-only primitive)
|
||||
method: theming-coherence category sweep (visual-elements), adversarially verified; roles + `:Morfo` lead-verified CLEAN across all eidos (no success/warning/danger/info; only alert-dialog uses `:Morfo`).
|
||||
|
||||
## Summary
|
||||
Counts (post-verification): CRITICAL 0 · HIGH 0 · MEDIUM 0 · LOW 0. Theming-clean.
|
||||
conformance: Contract solid (as const), layout-only (no events), stacking modes (side-by-side/overlap/overlap-reverse), local z-index OK within component.
|
||||
as-const: true · roles clean: true · variants clean: true · has-recipe: false
|
||||
|
||||
## Findings
|
||||
_No findings (theming-clean primitive)._
|
||||
|
||||
## Theming facts (E-bis)
|
||||
- magic literals: 1 @ avatar.css:362 -> z-index (LOCAL scope, stacking order for overlap-reverse) | 20 @ avatar.css:366 -> --avatar-group-reverse-z-max (defined in recipe, canonical) | -0.35 @ avatar.css:313-329 -> overlap calc (local layout multiplier, acceptable) | -0.25 @ avatar.css:263,268,273,278 -> translate for badge (local 45deg geometry)
|
||||
- magic z-index: none
|
||||
- non-canonical names: none
|
||||
- roles clean: true · variants clean: true
|
||||
@ -0,0 +1,19 @@
|
||||
# Audit: badge
|
||||
audit-version: 1
|
||||
audited-at: 2026-06-26
|
||||
scope: eidos (eidos-only primitive)
|
||||
method: theming-coherence category sweep (visual-elements), adversarially verified; roles + `:Morfo` lead-verified CLEAN across all eidos (no success/warning/danger/info; only alert-dialog uses `:Morfo`).
|
||||
|
||||
## Summary
|
||||
Counts (post-verification): CRITICAL 0 · HIGH 0 · MEDIUM 0 · LOW 0. Theming-clean.
|
||||
conformance: Contract solid (as const), 8-role palette cascade via TSC v2.1, 4 variants (soft/solid/outline/ghost), no drift.
|
||||
as-const: true · roles clean: true · variants clean: true · has-recipe: true
|
||||
|
||||
## Findings
|
||||
_No findings (theming-clean primitive)._
|
||||
|
||||
## Theming facts (E-bis)
|
||||
- magic literals: 0.5em @ badge.css:325 -> --badge-dot-size (em allowed for internal scaling) | 1em @ badge.css:326 -> --badge-icon-size (em allowed for internal scaling) | 1.25em @ badge.css:327 -> --badge-remove-size (em allowed for internal scaling) | 0.125em @ badge.css:328 -> --badge-remove-margin-start (em allowed for internal scaling) | 15% @ badge.css:178 -> color-mix blend (physical constant, acceptable)
|
||||
- magic z-index: none
|
||||
- non-canonical names: none
|
||||
- roles clean: true · variants clean: true
|
||||
@ -0,0 +1,19 @@
|
||||
# Audit: banner
|
||||
audit-version: 1
|
||||
audited-at: 2026-06-26
|
||||
scope: eidos (eidos-only primitive)
|
||||
method: theming-coherence category sweep (visual-elements), adversarially verified; roles + `:Morfo` lead-verified CLEAN across all eidos (no success/warning/danger/info; only alert-dialog uses `:Morfo`).
|
||||
|
||||
## Summary
|
||||
Counts (post-verification): CRITICAL 0 · HIGH 0 · MEDIUM 0 · LOW 0. Theming-clean.
|
||||
conformance: Contract solid (as const), full-bleed layout primitive (no events), 8-intent roles (primary/secondary/neutral/affirm/fulfill/risk/threat/loss), 4 variants (soft/solid/outline/ghost), 3 sizes, no drift.
|
||||
as-const: true · roles clean: true · variants clean: true · has-recipe: true
|
||||
|
||||
## Findings
|
||||
_No findings (theming-clean primitive)._
|
||||
|
||||
## Theming facts (E-bis)
|
||||
- magic literals: 1.4 @ banner.css:29 -> line-height (named --font-line-height-sm fallback, acceptable)
|
||||
- magic z-index: none
|
||||
- non-canonical names: none
|
||||
- roles clean: true · variants clean: true
|
||||
@ -0,0 +1,19 @@
|
||||
# Audit: box
|
||||
audit-version: 1
|
||||
audited-at: 2026-06-26
|
||||
scope: eidos (eidos-only primitive)
|
||||
method: theming-coherence category sweep (layout), adversarially verified; roles + `:Morfo` lead-verified CLEAN across all eidos (no success/warning/danger/info; only alert-dialog uses `:Morfo`).
|
||||
|
||||
## Summary
|
||||
Counts (post-verification): CRITICAL 0 · HIGH 0 · MEDIUM 0 · LOW 0. Theming-clean.
|
||||
conformance: Contract OK, data-box-area presence-flag gates grid-area shorthand, 'revert-layer' policy for all cascade defaults.
|
||||
as-const: true · roles clean: true · variants clean: true · has-recipe: true
|
||||
|
||||
## Findings
|
||||
_No findings (theming-clean primitive)._
|
||||
|
||||
## Theming facts (E-bis)
|
||||
- magic literals: none
|
||||
- magic z-index: none
|
||||
- non-canonical names: none
|
||||
- roles clean: true · variants clean: true
|
||||
@ -0,0 +1,42 @@
|
||||
# Audit: breadcrumb
|
||||
audit-version: 1
|
||||
audited-at: 2026-06-26
|
||||
scope:
|
||||
method: adversarially-verified workflow; HIGH lead-verified. B7 ground-truth: Toolbar A35 + Form A36 incidents STAYED FIXED (untrack present); toast/clipboard/drag-drop use uix.timers (no A6 leak); button is clean (its archetype finding was inert — Spinner not runtime-registered); data-size/data-shape are eidos visual attrs (not contract violations).
|
||||
provider: G:/dev/svelte/vicen/src/uix/soma/components/breadcrumb/breadcrumb-provider.svelte.ts
|
||||
cleanup-audit (A6/A35/A36): EFFECT (line 173-180): $effect writing item.isCurrent with cleanup that resets to false. No listener/observer. No resources held. Disposer verified present: return () => { item.isCurrent = false; } ✓. A35/A36 loop check: SAFE — isCurrentRaw is $derived (acyclic); writing item.isCurrent does not caus
|
||||
|
||||
## Summary
|
||||
Counts (post-verification): CRITICAL 0 · HIGH 1 · MEDIUM 0 · LOW 0.
|
||||
|
||||
## Findings
|
||||
### HIGH: Archetype 'item' is reserved for selectable/interactive row parts; display items should om — breadcrumb-001 <!-- id: breadcrumb-001 -->
|
||||
- dimension: A: Contract(morfo)
|
||||
- rule: Archetype 'item' is reserved for selectable/interactive row parts; display items should omit archetype
|
||||
- location: G:/dev/svelte/vicen/src/uix/morfo/components/breadcrumb.ts:50
|
||||
- evidence: Item part declares `archetype: 'item'` but Item is a display `<li>` container (not selectable). Timeline.ts explicitly documents: "No `archetype: 'item'` — that archetype is for selectable menu/listbox/option rows... A timeline entry is a display `<li>`, structurally unique, so it omits the archetype." Breadcrumb.Item is semantically identical to Timeline.Item.
|
||||
- impact: Incorrect archetype declaration violates the 2-of-3 rule (archetype field inconsistent with actual DOM semantics). May confuse CSS-side consumers expecting interactive row styling (cursor:pointer, select, hover). Runtime validator should flag this.
|
||||
- repro: none (static violation)
|
||||
- proposed-fix: Remove `archetype: 'item'` from the Item part definition (line 50). Item is a display container, not a selectable option.
|
||||
- verify: [confirmed] CONFIRMED. breadcrumb.ts:48-56 declares the Item part with `archetype: 'item'` (line 50) on a display container `<li>` (`defaultElement: 'li'`, no role, not selectable). The runtime stamps `data-archetype='item'` on the element whenever the morfo sets it (morfo/types.ts:720 'the runtime emits `data-archetype="..."` on the part's DOM element via `partProps`'; README:177 same). That `[data-archetype='item']` selector triggers archetypes.css:139-153 — `cursor: pointer; user-select: none; min-block-size: var(--list-item-height, auto); padding-block: var(--list-item-py, var(--space-1-5))` — PLUS the hover/highlight band at archetypes.css:161-171 (`background-color: var(--color-surface-raised)` painted on the WHOLE `<li>` on hover). breadcrumb.css:36-41 styles `[data-breadcrumb-item]` only with `display:inline-flex; align-items:center; min-inline-size:0` — it does NOT reset cursor/padding-block/hover, so the styling is unmitigated. This is the exact documented bug: Timeline.ts:88-91 ('No `archetype: 'item'` — that archetype is for selectable menu/listbox/option rows (it ships cursor:pointer, user-select:none, internal padding + a hover highlight). A timeline entry is a display `<li>`, structurally unique, so it omits the archetype.'), and Calendar.ts:247-249 / 270-273 + range-calendar.ts:285 repeat the same doctrine for display `<tr>`/`<td>`. Breadcrumb.Item is semantically identical to Timeline.Item. Real visible regression: a breadcrumb `<li>` gets `cursor:pointer`, `padding-block: var(--space-1-5)`, and a surface-raised hover band over the whole crumb row. HIGH is correct per the rubric's 'archetype-pulls-wrong-styling on a real part'. Fix: remove `archetype: 'item'` from breadcrumb.ts:50 (the Item is a display container; the interactive Link inside owns its own affordance).
|
||||
- fix-status: open
|
||||
|
||||
## No-findings dimensions
|
||||
B: Behavior(soma): A35/A36 loops absent (effect mirrors current to item without cycle; no async state writes), B: A6 resource cleanup (no timers/listeners/observers), B: A31 O(N²) derivations absent, B: A30 child->parent id registration (uses $effect correctly), B: A33 state(Map/Set) cloning (no Maps/Sets used), C: DOM-selector (no querySelector with user values), D: Frontier (no soma->eidos imports), E-bis: Theming (recipe uses canonical --space-*, --font-size-*, --duration-* tokens; focus-ring uses canonical CSS var), F: Tests environment jsdom (provider test exists, covers label projection, current mirroring, ellipsis semantics), G: Passive component justified in README (zero state, zero keyboard nav, zero events by design)
|
||||
|
||||
## Theming facts (E-bis)
|
||||
- magic z-index: none
|
||||
- magic literals: none
|
||||
- undeclared parts: none
|
||||
- roles clean: true · variants clean: true
|
||||
|
||||
## Tests (F)
|
||||
- exists: true · env: jsdom (@vitest-environment jsdom)
|
||||
- covers: Provider label projection and resolution (BREADCRUMB_LANGS); Link.current state mirroring to Item.isCurrent; aria-current conditional emission (true/'page'/'step'); renderSpan conditional tag switching; Ellipsis decorative->interactive semantics via interactive prop; aria-haspopup conditional on interactive; role=presentation conditional on !interactive
|
||||
- untested: E2E browser interaction (Playwright): full click/focus flow, ARIA tree, CSS targeting; Eidos variant/size responsive resolution (ActiveEidos.resolve); Ellipsis with DropdownMenu composition (documented example in README)
|
||||
|
||||
## Style observations (non-blocking)
|
||||
- Link focus-visible uses canonical --focus-ring-width and --focus-ring-color vars (breadcrumb.css:61-64, 107-109) ✓
|
||||
- Ellipsis interactive and link hover transitions use canonical --breadcrumb-transition-duration and --breadcrumb-transition-ease (line 52-54, 94-98) ✓
|
||||
- Recipe declares all semantic spacing (gap-xs/sm/md/lg → var(--space-*)) and sizing (font-size-xs/sm/md/lg, ellipsis-size → var(--control-height-xs)) ✓
|
||||
- no hardcoded hex colors; all use role aliases (--breadcrumb-color, --breadcrumb-link-color, etc.) ✓
|
||||
@ -0,0 +1,26 @@
|
||||
# Audit: button-group
|
||||
audit-version: 1
|
||||
audited-at: 2026-06-26
|
||||
scope: eidos (eidos-only primitive)
|
||||
method: theming-coherence category sweep (composite-and-service), adversarially verified; roles + `:Morfo` lead-verified CLEAN across all eidos (no success/warning/danger/info; only alert-dialog uses `:Morfo`).
|
||||
|
||||
## Summary
|
||||
Counts (post-verification): CRITICAL 0 · HIGH 0 · MEDIUM 0 · LOW 1.
|
||||
conformance: Contract: 'as const satisfies Morfo' ✓ | Scope: eidos ✓ | Roles: none/N-A ✓ | Variants: data-orientation, data-disabled only; propagated to children ✓ | Sizing/Tokens: all via recipe ✓ | Local z allowed ✓
|
||||
as-const: true · roles clean: true · variants clean: true · has-recipe: true
|
||||
|
||||
## Findings
|
||||
### LOW: MAGIC_LITERAL where canonical scale exists — button-group-001 <!-- id: button-group-001 -->
|
||||
- dimension: MAGIC_LITERAL
|
||||
- location: src/uix/eidos/lib/recipes/base.ts:4384
|
||||
- evidence: 'divider-strength': '18%'
|
||||
- impact: Bare percentage where --opacity-* scale could be canonical; though this is a specialized mix-opacity control, the 18% literal bypasses the established opacity scale
|
||||
- proposed-fix: Consider documenting whether this 18% is a one-off interaction opacity or should reference an opacity token if theming consistency is desired
|
||||
- verify: [downgraded] Confirmed literal at base.ts:4384 'divider-strength': '18%'. But this is a color-mix percentage (currentColor mix for the segmented seam, css:93), not an --opacity-* element-opacity case. The recipe comment (4381) explicitly flags it as 'the lone magic number'. color-mix proportion has no canonical token; LOW cosmetic, not opacity-scale drift.
|
||||
- fix-status: open
|
||||
|
||||
## Theming facts (E-bis)
|
||||
- magic literals: 18% @ src/uix/eidos/lib/recipes/base.ts:4384 -> should document opacity rationale or reference token | 1 @ src/uix/eidos/components/button-group/button-group.css:98,102 -> local z acceptable | 2 @ src/uix/eidos/components/button-group/button-group.css:106 -> local z acceptable
|
||||
- magic z-index: none
|
||||
- non-canonical names: none
|
||||
- roles clean: true · variants clean: true
|
||||
@ -0,0 +1,30 @@
|
||||
# Audit: button
|
||||
audit-version: 1
|
||||
audited-at: 2026-06-26
|
||||
scope: soma,sema,eidos
|
||||
method: adversarially-verified workflow; HIGH lead-verified. B7 ground-truth: Toolbar A35 + Form A36 incidents STAYED FIXED (untrack present); toast/clipboard/drag-drop use uix.timers (no A6 leak); button is clean (its archetype finding was inert — Spinner not runtime-registered); data-size/data-shape are eidos visual attrs (not contract violations).
|
||||
provider: /g/dev/svelte/vicen/src/uix/soma/components/button/button-provider.svelte.ts
|
||||
cleanup-audit (A6/A35/A36): No timers, listeners, observers, or cleanup needed. ButtonProvider uses no setTimeout, setInterval, ResizeObserver, IntersectionObserver, MutationObserver, or other disposable resources. All state is reactive via $derived.by() with no effect roots. No $effect blocks with side effects. A35/A36 loop c
|
||||
|
||||
## Summary
|
||||
Counts (post-verification): CRITICAL 0 · HIGH 0 · MEDIUM 0 · LOW 0.
|
||||
|
||||
## Findings
|
||||
_No findings survived verification (clean component)._
|
||||
|
||||
## No-findings dimensions
|
||||
B, C, D, F, G
|
||||
|
||||
## Theming facts (E-bis)
|
||||
- magic z-index: none
|
||||
- magic literals: none
|
||||
- undeclared parts: none
|
||||
- roles clean: true · variants clean: true
|
||||
|
||||
## Tests (F)
|
||||
- exists: false · env: N/A
|
||||
- covers:
|
||||
- untested: ButtonProvider initialization and onclick flow; Intent → color cascade resolution (intent wins over color, neutral fallback); Field provider OR-merge of disabled state; Loading state gating of contact-activate event; onPress callback invocation; data-color, data-loading, data-disabled attribute synchronization
|
||||
|
||||
## Style observations (non-blocking)
|
||||
- Focus-ring implementation (button.css 99-107) correctly uses outline with --focus-ring-* tokens and does NOT hardcode any two-ring composite — single primary-tinted ring is intentional per comment. :active (button.css 90-97) correctly uses scale(var(--press-scale)) with canonical press tokens, not a hardcoded translateY. Variant slice pattern (158-212) uniformly applies via palette variables for soft/surface/outline/ghost/plain — no per-variant hardcoding. Icon sizing correctly feeds --icon-size from --_button-icon-size which cascades per size (lines 30-35, 111-154).
|
||||
@ -0,0 +1,111 @@
|
||||
# Audit: calendar
|
||||
audit-version: 1
|
||||
audited-at: 2026-06-26
|
||||
scope: ['soma', 'sema'] (SCOPE-DRIFT → SYS-1)
|
||||
method: adversarially-verified workflow (analyze → refute); HIGH/CRITICAL personally re-verified by the lead. Batch-3 ground-truth: each picker fires trigger(close) (close NOT inert), but open/commit-reset ARE inert; calendar/range-calendar are MID-REFACTOR (uncommitted view-switch work).
|
||||
provider: src/uix/soma/components/calendar/calendar-provider.svelte.ts
|
||||
|
||||
> **MID-REFACTOR CAVEAT:** this component has uncommitted view-switch changes on this branch (M/D/?? files). Findings reflect the current in-flight state; treat structural inconsistencies as in-progress, not shipped defects.
|
||||
|
||||
## Summary
|
||||
Counts (post-verification): CRITICAL 0 · HIGH 0 · MEDIUM 3 · LOW 2.
|
||||
systemic hits: SYS-1 scope-drift (eidos recipe exists, morfo scope omits 'eidos').
|
||||
|
||||
## Findings
|
||||
### MEDIUM: SYS-1: eidos recipe directory exists but morfo scope omits 'eidos' — calendar-002 <!-- id: calendar-002 -->
|
||||
- dimension: A
|
||||
- rule: SYS-1: eidos recipe directory exists but morfo scope omits 'eidos'
|
||||
- location: src/uix/morfo/components/calendar.ts:7
|
||||
- evidence: scope: ['soma', 'sema'],
|
||||
- impact: The eidos directory at src/uix/eidos/components/calendar/ exists (with calendar.css, components, types.ts), but morfo declares scope as ['soma', 'sema'] without 'eidos'. Per known systemic SYS-1, this is acceptable if the eidos layer is optional or only consumed via composition.
|
||||
- proposed-fix: Verify with team: is eidos-layer consumption optional? If mandatory, update scope to ['soma', 'sema', 'eidos'].
|
||||
- verify: [confirmed] Confirmed at calendar.ts:7 `scope: ['soma', 'sema'],` omits 'eidos', yet a full eidos layer exists: recipe block `calendar:` at base.ts:1626 (60+ tokens) AND src/uix/eidos/components/calendar/ contains calendar.css + ~25 .svelte wrappers + recipe consumption. This is exactly the known systemic SYS-1 scope-drift (MEDIUM). The candidate's framing as 'acceptable if optional' is the SYS-1 baseline narrative; the drift itself is real and matches the documented systemic pattern.
|
||||
- fix-status: open
|
||||
|
||||
### MEDIUM: Magic literal in recipe token: '0.62' opacity not from canonical scale — calendar-004 <!-- id: calendar-004 -->
|
||||
- dimension: E-bis
|
||||
- rule: Magic literal in recipe token: '0.62' opacity not from canonical scale
|
||||
- location: src/uix/eidos/lib/recipes/base.ts:1688
|
||||
- evidence: 'day-outside-opacity': '0.62',
|
||||
- impact: Opacity value 0.62 is hardcoded; no reference to a canonical --opacity-* token from the static scale. Day-outside-month opacity is not unified with other opacity tokens.
|
||||
- proposed-fix: Map to a canonical --opacity-* token (e.g. --opacity-60 if the scale includes it), or introduce a new token for calendar-specific opacity levels.
|
||||
- verify: [confirmed] Confirmed `'day-outside-opacity': '0.62',` at base.ts:1688. Verified STATIC_OPACITY (static.ts:255-275) is a 0.05-step scale: --opacity-60=0.6, --opacity-65=0.65 — 0.62 maps to NO canonical step. Per E-bis 'bare opacity decimal -> a --opacity-{0..100} scale exists' this is a magic literal (MEDIUM). Note: the same 0.62 design value appears as '62%' literals at base.ts:729 and 895 (other components' overlay-opacity), so it is an established off-scale brand value with no token — but the rule still flags the bare decimal. Confirmed at MEDIUM.
|
||||
- fix-status: open
|
||||
|
||||
### MEDIUM: Test coverage gap: keyboard navigation routes not exercised in jsdom environment — calendar-005 <!-- id: calendar-005 -->
|
||||
- dimension: F
|
||||
- rule: Test coverage gap: keyboard navigation routes not exercised in jsdom environment
|
||||
- location: src/uix/soma/components/calendar/calendar-provider.svelte.test.ts
|
||||
- evidence: Test file has 217 lines, 7 tests. No tests for handleDayKeydown, ArrowRight/ArrowLeft/ArrowUp/ArrowDown, Home/End, PageUp/PageDown, focus management, or tabindex routing. All tests run in jsdom environment.
|
||||
- impact: Critical interaction paths (APG GRID keyboard navigation: arrows, Home/End, PageUp/PageDown + shift, focus roving) are NOT tested. Keyboard behavior regressions would not be caught.
|
||||
- proposed-fix: Add browser-level (Playwright/client) test suite covering:
|
||||
- handleDayKeydown for each KEYS.*
|
||||
- focus tabindex management (focused day has tabindex=0, others -1)
|
||||
- week navigation (ArrowUp/Down by 7)
|
||||
- month/year navigation via PageUp/PageDown +shift
|
||||
- placeholder update when target outside visible months
|
||||
- two-moments ordering (value.current write before trigger)
|
||||
- verify: [confirmed] Confirmed: calendar-provider.svelte.test.ts is `// @vitest-environment jsdom` (line 1), 7 tests, all exercising pure helpers + bounds-validation logging. NONE drive handleDayKeydown (the APG grid keyset Arrow x4 / Home / End / PageUp+PageDown / shiftKey at provider lines 470-521), roving tabindex (line 1124), focus management, or two-moments ordering (value write at 436/443 before runtime.trigger at 445). This is the documented SYS-3 (jsdom-only, kbd-untested). The provider HAS rich, branchy keyboard + focus code that is entirely unexercised — genuine high-risk coverage gap. Confirmed at MEDIUM (matches SYS-3 baseline severity).
|
||||
- fix-status: open
|
||||
|
||||
### LOW: DOM querySelector selector interpolation must use CSS.escape for untrusted values — calendar-001 <!-- id: calendar-001 -->
|
||||
- dimension: C
|
||||
- rule: DOM querySelector selector interpolation must use CSS.escape for untrusted values
|
||||
- location: src/uix/soma/components/calendar/calendar-provider.svelte.ts:514-516
|
||||
- evidence: const el = root.querySelector<HTMLElement>(
|
||||
`[data-calendar-day][data-value="${target!.toString()}"]`
|
||||
);
|
||||
- impact: If target.toString() ever produces values with CSS selector metacharacters (e.g. quotes, brackets), the selector could break or be unsafe. DateValue.toString() should be ISO-safe, but the pattern violates the defense-in-depth rule requiring CSS.escape().
|
||||
- proposed-fix: const el = root.querySelector<HTMLElement>(
|
||||
`[data-calendar-day][data-value="${CSS.escape(target!.toString())}"]`
|
||||
);
|
||||
- verify: [downgraded] Confirmed the selector exists at calendar-provider.svelte.ts:514-516: `root.querySelector<HTMLElement>(\`[data-calendar-day][data-value="${target!.toString()}"]\`)`. `target` is a DateValue produced by date arithmetic (shiftDate/subtract/add), NOT a consumer-supplied string — DateValue.toString() emits an ISO date (e.g. '2026-05-15') with zero CSS metacharacters, ever. The rule about CSS.escape targets UNTRUSTED/consumer-derived values; this is a framework-derived ISO token that cannot contain a quote/bracket. Same pattern at line 173 (`[${attrs.day}][data-focused]`) uses no interpolation. Real defect risk = none; defense-in-depth nicety only. Not HIGH. Downgraded to LOW.
|
||||
- fix-status: open
|
||||
|
||||
### LOW: Magic literal in recipe token: '8ch' not referenced as a canonical size — calendar-003 <!-- id: calendar-003 -->
|
||||
- dimension: E-bis
|
||||
- rule: Magic literal in recipe token: '8ch' not referenced as a canonical size
|
||||
- location: src/uix/eidos/lib/recipes/base.ts:1663
|
||||
- evidence: 'select-min-width': '8ch',
|
||||
- impact: Character unit (ch) is hardcoded; no corresponding token in a canonical size scale. This token controls the month/year select minimum width and is not derived from --space-* or --control-height-* primitives.
|
||||
- proposed-fix: Either introduce a canonical token (e.g. --select-width-base: 8ch) and reference it, or document this as a component-specific sizing exception.
|
||||
- verify: [downgraded] Confirmed `'select-min-width': '8ch',` at base.ts:1663. Per E-bis, font-size/icon-size/spacing/radius MUST reference canonical scales, but a min-width is sizing dimension with no canonical `ch`-based scale — `--space-*` is the wrong axis (a min-width in character units intentionally tracks glyph width of the month/year select, not the spacing rhythm). `ch` is a legitimate CSS unit for text-box min sizing and there is no `--{c}` token it should reference because no canonical width-in-ch scale exists. This is a named recipe token (component-scoped `--_calendar-select-min-width`), already the canonical home for a one-off dimension. Not a px/rem font-size literal. Token-naming nit at most; downgraded to LOW.
|
||||
- fix-status: open
|
||||
|
||||
### MEDIUM: Home/End morfo action label ('first/last-day-of-week') vs provider impl (start/end-of-month) — SYSTEMIC with range-calendar <!-- id: calendar-006 -->
|
||||
- dimension: A, B
|
||||
- rule: morfo↔code drift + APG Date Picker Dialog (Home/End move within the WEEK, not the month)
|
||||
- location: calendar.ts:76-77 (`{key:'Home', action:'first-day-of-week'}`, `{key:'End', action:'last-day-of-week'}`) vs calendar-provider.svelte.ts:481-485 (`Home → startOfMonth(date)`, `End → endOfMonth(date)`)
|
||||
- evidence: the morfo action labels say *day-of-week* but the provider navigates to *start/end of month*. Surfaced by the range-calendar agent (range-calendar-002); the verify confirmed calendar does the IDENTICAL thing → systemic, not range-calendar-specific. (The calendar agent's own style-obs even claims "Home/End all implemented" — it saw the handlers but didn't check the label-vs-behavior semantics.)
|
||||
- impact: either (a) an APG deviation — WAI-ARIA Date Picker Dialog defines Home/End as first/last day of the current WEEK and PageUp/PageDown as month — so Home jumping to the first of the *month* is non-standard; or (b) the morfo label is stale. Two-component contract/keyboard drift.
|
||||
- repro: focus mid-month in a Calendar grid, press Home → focus jumps to day 1 of the month (APG expects first day of the current week).
|
||||
- proposed-fix: decide intended behavior. APG-week → change provider to first/last-day-of-week within the visible row; month variant → rename the morfo action to `first/last-day-of-month` so label and behavior agree. Apply to BOTH calendar + range-calendar.
|
||||
- verify: [lead-added] elevated from the verify's LOW on range-calendar-002 (it downgraded because "not range-calendar-specific" — that shared-ness is exactly why it's systemic MEDIUM). Both providers confirmed via the verify cross-reference.
|
||||
- fix-status: open
|
||||
|
||||
## No-findings dimensions
|
||||
B (loop/roving math is correct — modulo with guards), D, G
|
||||
|
||||
## Theming facts (E-bis)
|
||||
- magic z-index: none
|
||||
- magic literals: 8ch | 0.62
|
||||
- undeclared parts: none
|
||||
- roles clean: true · variants clean: true
|
||||
- conformance: Data attributes and CSS color roles align with the canonical 9-role system (primary, secondary, neutral, affirm, fulfill, risk, threat, loss). No undeclared roles or unapproved role names detected.
|
||||
|
||||
## Tests (F)
|
||||
- exists: true · env: jsdom only
|
||||
- covers: placeholder resolution from selection; month grid generation; time preservation in date selection; multiple selection toggle and maxDays enforcement; date flags (disabled, unavailable, holiday) independence; selection/view bounds validation; invalid bounds logging
|
||||
- untested: handleDayKeydown routing for all 10 keyboard actions (ArrowRight/Left/Up/Down, Home, End, PageUp, PageDown, Enter, Space); focus management and tabindex roving; placeholder update and monthchange when target outside visible months; announcement generation for navigation; two-moments: value.current write before trigger ordering; view switching (day ↔ month ↔ year); readonly behavior (select blocked, focus+nav allowed); disabled behavior (all interaction blocked)
|
||||
|
||||
## Style observations (non-blocking)
|
||||
- Keyboard event handling is well-structured with proper directional key resolution for ltr/rtl.
|
||||
- APG GRID keyboard actions (arrows, Home/End, PageUp/PageDown + shift) are all implemented in handleDayKeydown.
|
||||
- Focus management uses queueMicrotask to ensure DOM updates before focus shift.
|
||||
- Morfo `as const satisfies Morfo` is correctly applied at line 357.
|
||||
- Parts registered via runtime.part() (provider, header, heading, prev-button, next-button, month-select, year-select, grid, grid-head, grid-body, grid-row, head-cell, cell, day) align with morfo declarations.
|
||||
- No soma imports in provider (CRITICAL rule passes).
|
||||
- Date utilities correctly use $libs/days, not @internationalized/date.
|
||||
- No evidence of $effect.root without disposal, setTimeout/setInterval, or memory leaks.
|
||||
- CSS role attributes (application, grid, button, row, gridcell, header, label) are declared and rendered correctly.
|
||||
- aria-readonly and aria-disabled follow propRef + ariaBoolean pattern consistent with framework conventions.
|
||||
@ -0,0 +1,19 @@
|
||||
# Audit: card
|
||||
audit-version: 1
|
||||
audited-at: 2026-06-26
|
||||
scope: eidos (eidos-only primitive)
|
||||
method: theming-coherence category sweep (visual-elements), adversarially verified; roles + `:Morfo` lead-verified CLEAN across all eidos (no success/warning/danger/info; only alert-dialog uses `:Morfo`).
|
||||
|
||||
## Summary
|
||||
Counts (post-verification): CRITICAL 0 · HIGH 0 · MEDIUM 0 · LOW 0. Theming-clean.
|
||||
conformance: Contract solid (as const), 8-role palette, 4 variants (soft/solid/outline/ghost), interactive + selected states, no drift.
|
||||
as-const: true · roles clean: true · variants clean: true · has-recipe: true
|
||||
|
||||
## Findings
|
||||
_No findings (theming-clean primitive)._
|
||||
|
||||
## Theming facts (E-bis)
|
||||
- magic literals: 8px @ card.css:521 -> --card-emerge-distance (named token in recipe) | -2px @ card.css:522 -> --card-hover-lift (named token in recipe) | white @ card.css:152 -> --card-color-custom-contrast (fallback for custom color, acceptable)
|
||||
- magic z-index: none
|
||||
- non-canonical names: none
|
||||
- roles clean: true · variants clean: true
|
||||
@ -0,0 +1,19 @@
|
||||
# Audit: cascade
|
||||
audit-version: 1
|
||||
audited-at: 2026-06-26
|
||||
scope: ['eidos'] (eidos-only primitive)
|
||||
method: theming-coherence category sweep (infra), adversarially verified; roles + `:Morfo` lead-verified CLEAN across all eidos (no success/warning/danger/info; only alert-dialog uses `:Morfo`).
|
||||
|
||||
## Summary
|
||||
Counts (post-verification): CRITICAL 0 · HIGH 0 · MEDIUM 0 · LOW 0. Theming-clean.
|
||||
conformance: eidos foundation primitive; 'as const satisfies' ✓; passive; no recipe/CSS; structural.
|
||||
as-const: true · roles clean: true · variants clean: true · has-recipe: false
|
||||
|
||||
## Findings
|
||||
_No findings (theming-clean primitive)._
|
||||
|
||||
## Theming facts (E-bis)
|
||||
- magic literals: none
|
||||
- magic z-index: none
|
||||
- non-canonical names: none
|
||||
- roles clean: true · variants clean: true
|
||||
@ -0,0 +1,26 @@
|
||||
# Audit: chart
|
||||
audit-version: 1
|
||||
audited-at: 2026-06-26
|
||||
scope: eidos (eidos-only primitive)
|
||||
method: theming-coherence category sweep (visual-elements), adversarially verified; roles + `:Morfo` lead-verified CLEAN across all eidos (no success/warning/danger/info; only alert-dialog uses `:Morfo`).
|
||||
|
||||
## Summary
|
||||
Counts (post-verification): CRITICAL 0 · HIGH 0 · MEDIUM 0 · LOW 1.
|
||||
conformance: Contract solid (as const), display-only (no events), SVG frame + marks + legend + title parts, canonical token recipe, no drift.
|
||||
as-const: true · roles clean: true · variants clean: true · has-recipe: true
|
||||
|
||||
## Findings
|
||||
### LOW: Magic literal in SVG stroke-dasharray — chart-001 <!-- id: chart-001 -->
|
||||
- dimension: literal
|
||||
- location: chart.css:93
|
||||
- evidence: `stroke-dasharray: 3 3` @ line 93. This is a visual pattern (dashed line) for the crosshair overlay, not a component dimension or theme token.
|
||||
- impact: Physical constant for visual pattern. Does not break theming. Local to chart component only.
|
||||
- proposed-fix: No fix needed. Per baseline: '3.5px @keyframes amplitude' kind of literal is NOT drift; `3 3` dasharray pattern falls into same category.
|
||||
- verify: [confirmed] chart.css:93 — `stroke-dasharray: 3 3` on [data-chart-crosshair] is an SVG dash-pattern visual constant for the dashed crosshair overlay, scoped to the chart component. Same exception class as the @keyframes amplitude literals (not a dimension/spacing/color where a canonical scale applies). Correctly LOW; not drift.
|
||||
- fix-status: open
|
||||
|
||||
## Theming facts (E-bis)
|
||||
- magic literals: 3 3 @ chart.css:93 -> stroke-dasharray (pattern literal, physical constant, LOW)
|
||||
- magic z-index: none
|
||||
- non-canonical names: none
|
||||
- roles clean: true · variants clean: true
|
||||
@ -0,0 +1,45 @@
|
||||
# Audit: checkbox
|
||||
audit-version: 1
|
||||
audited-at: 2026-06-26
|
||||
scope: ['soma', 'sema', 'eidos']
|
||||
method: adversarially-verified workflow; HIGH lead-verified. B6 ground-truth: checkbox/toggle/switch commit-toggle = sequence post (lag-fixed); radio-group/tabs/accordion/stepper set state at call-site (pre OK); toggle-group + checkbox-group carry the A31 .includes pattern (SYS-7); slider has no gesture-layer A6 leak.
|
||||
provider: src/uix/soma/components/checkbox/checkbox-provider.svelte.ts
|
||||
sequence-audit: State set in handler: applyPending() at line 134 called by runtime event handler (lines 68-69). Morfo sequence='post' (checkbox.ts:28, 43). VERIFIED: state change deferred until AFTER sema dispatch via runtime.trigger() → event handler → applyPending(). Lag risk MITIGATED. Prior 244ms lag from 'pre'
|
||||
|
||||
## Summary
|
||||
Counts (post-verification): CRITICAL 0 · HIGH 0 · MEDIUM 1 · LOW 1.
|
||||
|
||||
## Findings
|
||||
### MEDIUM: A31: per-item $derived calling provider method reading global state (.includes) = O(N²) — checkbox-001 <!-- id: checkbox-001 -->
|
||||
- dimension: A31-O(N²)
|
||||
- rule: A31: per-item $derived calling provider method reading global state (.includes) = O(N²)
|
||||
- location: src/uix/soma/components/checkbox/checkbox-provider.svelte.ts:313-315, 320
|
||||
- evidence: CheckboxGroupProvider.isItemChecked(value: string): boolean { return this.opts.value.current.includes(value); } and updateItemChecked also calls .includes(value). Each checkbox item in a group calls isItemChecked() to sync its state (line 104), resulting in N calls to .includes() on the array, yielding O(N²) when rendering N checkboxes in a group.
|
||||
- impact: For large checkbox groups (50+ items), observable performance degradation during render as each item calls isItemChecked() which iterates the entire value array. Not immediately critical for typical forms but documented risk.
|
||||
- proposed-fix: Convert group value from string[] to a Set (or use Map if needed for fast lookup). Migrate isItemChecked to Set.has() (O(1)). Update updateItemChecked to use Set.add/delete. Ensure Set is wrapped as $state(new SvelteSet()) for reactivity.
|
||||
- verify: [unverified]
|
||||
- fix-status: open
|
||||
|
||||
### LOW: Comments in English must match the actual contract; stale doc referencing wrong sequence i — checkbox-002 <!-- id: checkbox-002 -->
|
||||
- dimension: A-doc-drift
|
||||
- rule: Comments in English must match the actual contract; stale doc referencing wrong sequence is drift
|
||||
- location: src/uix/soma/components/checkbox/checkbox-provider.svelte.ts:42-47
|
||||
- evidence: The `_pendingChecked` JSDoc states: "The two-step dance lets sema fire BEFORE the structural commit (canon `sequence: 'pre'`)." But checkbox.ts:28 and :43 both declare `sequence: 'post'` (with an explicit morfo comment: "'pre' made the functional check wait for the perceptual hold (~240ms) → laggy"). The provider comment is stale: it cites 'pre' as the canon while the morfo deliberately uses 'post'.
|
||||
- impact: Misleading documentation. A future maintainer reading the provider could re-introduce the 244ms-lag 'pre' regression believing it is canon. No runtime effect — the morfo is correct.
|
||||
- proposed-fix: Update the comment to reference `sequence: 'post'` and explain the handler-runs-after-sema ordering, consistent with checkbox.ts:25-28.
|
||||
- verify: [verifier-added] added by adversarial verify pass
|
||||
- fix-status: open
|
||||
|
||||
## No-findings dimensions
|
||||
Contract(morfo): checkboxMorfo is 'as const satisfies Morfo' (line 172). All 6 parts registered via runtime.part() exist in morfo: 'provider', 'indicator', 'hidden-input', 'group', 'group-label' (lines 72, 193, 238, 304, 353). 2-of-3 rule satisfied., data-{c}/-{part} naming: All data attributes use kebab-cased part names (data-state, data-disabled, data-readonly, data-invalid on provider; data-state on indicator; data-orientation on group). No data-soma-* violations., aria/data emitted: morfo declares aria-checked, aria-required, aria-readonly, aria-invalid, aria-hidden on provider/indicator/hidden-input. Provider also declares type='button'. All values reference morfo specs correctly., Scope includes eidos: Morfo declares scope=['soma','sema','eidos'] (line 7). No eidos imports in soma provider (verified: no 'eidos' in grep output), which is correct—eidos layer is optional consumer. SYS-1 scope-drift REFUTED: eidos scope is declared but not imported into soma, which is the correct pattern., Events: Both commit-toggle-check and commit-toggle-uncheck have sequence='post' (lines 28, 43). No inert events (all emit to sema)., Gesture/A6: No gesture, setTimeout, setInterval, ResizeObserver, or MutationObserver in checkbox provider. No cleanup risk., Hidden input A13: CheckboxHiddenInputProvider renders <input type='checkbox'> with name/value for form participation (lines 249-255). aria-hidden='true' keeps it off AT tree so button role='checkbox' is sole control. tabindex=-1 excludes from focus. Correct A13 pattern., A30 direct registration verified: CheckboxGroupLabelProvider.setLabelId() called in constructor (line 362), not in $effect. Prevents reactive loop., Theme tokens: All CSS custom properties reference --checkbox-* or --font-*/--space-*/--color-*/--opacity-* canonical tokens from recipes/base.ts. No hex literals, no magic numbers in CSS (px/rem only in recipe: lines 639-653). Token naming --{c}-{slot} followed (--checkbox-size-md-box-size, --checkbox-bg-off, etc.). Private TSC tokens use _checkbox-palette-* pattern (lines 8-10, 162)., Color role subset: CheckboxColor = AffirmativeColorRole (types.ts:42), declared as primary|secondary|neutral|affirm (no risk/threat/loss). Per spec: invalid state handled via data-invalid attribute, not color role., Variants: CheckboxVariant = SelectionVariant (types.ts:30: solid|outline|ghost). CSS implements all three (lines 83-126). No undeclared variants., Sync attrs: runtimePart registered with syncAttrs=true (lines 77, 200, 242, 309, 356). Props returned via $derived.by for each part (lines 167-174, 208-212, 246-257, 331-335, 365-367). No double-write risk., A14 roving: Not applicable—checkbox is not a roving component (single input, not a group with tabindex navigation)., A12 RTL: Not applicable—checkbox has no directional keys (no slider, tabs, radio-group)., A15 gesture: Not applicable—checkbox does not use Gesture layer (no slider/splitter/scroll-area patterns)., Sequence lag FIXED: Prior 244ms lag incident from 'pre' sequence is RESOLVED by current 'post' design. State change in applyPending() handler defers until after sema dispatch.
|
||||
|
||||
## Theming facts (E-bis)
|
||||
- magic z-index: none
|
||||
- magic literals: stroke-dasharray: 100 | stroke-dashoffset: 100 | 8% | 18% | 82% | 72% | 78%
|
||||
- undeclared parts: none
|
||||
- roles clean: true · variants clean: true
|
||||
|
||||
## Tests (F)
|
||||
- exists: true · env: jsdom
|
||||
- covers: sequence/state commit (toggle, indeterminate clear); readonly/disabled guards; hidden input props + form participation; group value sync; label id context registration
|
||||
- untested: A31 O(N²) performance with large groups (50+ items); visual state transitions in real DOM; keyboard spacebar event in integration
|
||||
@ -0,0 +1,62 @@
|
||||
# Audit: clipboard
|
||||
audit-version: 1
|
||||
audited-at: 2026-06-26
|
||||
scope: (SCOPE-DRIFT → SYS-1)
|
||||
method: adversarially-verified workflow; HIGH lead-verified. B7 ground-truth: Toolbar A35 + Form A36 incidents STAYED FIXED (untrack present); toast/clipboard/drag-drop use uix.timers (no A6 leak); button is clean (its archetype finding was inert — Spinner not runtime-registered); data-size/data-shape are eidos visual attrs (not contract violations).
|
||||
provider: src/uix/soma/components/clipboard/clipboard-provider.svelte.ts
|
||||
|
||||
## Summary
|
||||
Counts (post-verification): CRITICAL 0 · HIGH 0 · MEDIUM 1 · LOW 2.
|
||||
|
||||
## Findings
|
||||
### MEDIUM: SYS-1 scope-drift — morfo scope omits 'eidos' although an eidos recipe/CSS dir exists for — CLIP-1 <!-- id: CLIP-1 -->
|
||||
- dimension: A
|
||||
- rule: SYS-1 scope-drift — morfo scope omits 'eidos' although an eidos recipe/CSS dir exists for the component
|
||||
- location: src/uix/morfo/components/clipboard.ts:7
|
||||
- evidence: `scope: ['soma', 'sema'],` — yet src/uix/eidos/components/clipboard/ exists with clipboard.css, clipboard.svelte, clipboard-trigger.svelte, clipboard-indicator.svelte and types.ts. The eidos layer materializes the `data-clipboard` / `data-clipboard-indicator` recipe selectors.
|
||||
- impact: The morfo's declared scope no longer reflects reality: an eidos consumer exists but the contract says only soma+sema. Scope is the declared surface; drift means tooling/audits that read `scope` will under-count the eidos layer.
|
||||
- proposed-fix: Add 'eidos' to the scope array: `scope: ['soma', 'sema', 'eidos']` (cf. button.ts:47 which declares it). Or — if the project decides hand-authored thin consumer wrappers do NOT count as an eidos recipe — codify that rule, since toggle/switch/collapsible omit it too. This is the confirmed-systemic SYS-1; clipboard matches the majority that omit it.
|
||||
- verify: [verifier-added] added by adversarial verify pass
|
||||
- fix-status: open
|
||||
|
||||
### LOW: Recipe spacing MUST reference --space-* tokens; raw rem literals are drift — CLIP-2 <!-- id: CLIP-2 -->
|
||||
- dimension: E-bis
|
||||
- rule: Recipe spacing MUST reference --space-* tokens; raw rem literals are drift
|
||||
- location: src/uix/eidos/components/clipboard/clipboard.css:34
|
||||
- evidence: `padding: 0.125rem 0.5rem;` — raw rem literals, while the SAME rule uses `gap: var(--space-1);` one line above (line 33). 0.5rem == --space-2 in the project scale.
|
||||
- impact: The indicator chip's inset bypasses the spacing scale, so density/scaling theme changes (`:root` --space-* overrides) won't reach it. Minor visual-only drift.
|
||||
- proposed-fix: Replace with token-derived values, e.g. `padding: var(--space-0-5, 0.125rem) var(--space-2);` (confirm the exact step names against primitives/static.ts).
|
||||
- verify: [verifier-added] added by adversarial verify pass
|
||||
- fix-status: open
|
||||
|
||||
### LOW: Recipe font-weight should reference the bare token; hardcoded numeric fallback diverges fr — CLIP-3 <!-- id: CLIP-3 -->
|
||||
- dimension: E-bis
|
||||
- rule: Recipe font-weight should reference the bare token; hardcoded numeric fallback diverges from convention
|
||||
- location: src/uix/eidos/components/clipboard/clipboard.css:40
|
||||
- evidence: `font-weight: var(--font-weight-medium, 500);` — the `500` literal fallback. Every recipe occurrence in lib/recipes/base.ts (lines 48, 153, 231, 314, 449) uses bare `var(--font-weight-medium)` with no fallback.
|
||||
- impact: Cosmetic inconsistency; the `500` fallback masks a missing token rather than failing visibly, and diverges from the recipe convention.
|
||||
- proposed-fix: Drop the fallback: `font-weight: var(--font-weight-medium);` to match base.ts usage.
|
||||
- verify: [verifier-added] added by adversarial verify pass
|
||||
- fix-status: open
|
||||
|
||||
## No-findings dimensions
|
||||
A: Contract - Morfo 'as const satisfies' correct, A: Parts registered via runtime.part(), A: Data attributes (data-copied) declared in morfo, A: ARIA attributes correct (aria-label, aria-live, aria-hidden), A30: No per-item loops or sequence lags detected, A31: No O(N²) derived patterns detected, A33: No Map/Set state issues detected, A35: No per-item effect loops detected, A36: No async-mediated microtask freeze patterns detected, A6: Timer disposal via resetTimer with clearResetTimer() cleanup and $effect return, A15: No gesture/drag-drop patterns needed for clipboard, A34: Live regions - Trigger has aria-live polite for label announcement, B: Provider pattern correct - soma imports only soma core, no eidos imports, D: Frontier - No soma->eidos imports, E-bis: No recipe token or theming issues (uses canonical color roles), F: Tests exist - jsdom environment, happy path, error handling, timer mocks
|
||||
|
||||
## Theming facts (E-bis)
|
||||
- magic z-index: none
|
||||
- magic literals: none
|
||||
- undeclared parts: none
|
||||
- roles clean: true · variants clean: true
|
||||
|
||||
## Tests (F)
|
||||
- exists: true · env: jsdom
|
||||
- covers: copy success and copied state reset via timer; onCopy callback invocation; onError callback on clipboard API rejection; timer scheduling with uix.timers.schedule; aria-label resolution and swap on copied state; indicator visibility conditional on copied state; mergedProps application
|
||||
- untested:
|
||||
|
||||
## Style observations (non-blocking)
|
||||
- CSS uses canonical color-role variables (--color-affirm-solid, etc.) with cascading support via data-color attribute
|
||||
- Animation (clipboard-pop) respects prefers-reduced-motion
|
||||
- Typography uses canonical --font-size-xs and --font-weight-medium
|
||||
- Spacing uses --space-* tokens (0.5rem mapped to --space-1)
|
||||
- Focus ring follows canonical two-ring model via Button consumer
|
||||
- No hardcoded hex colors or magic literals in eidos CSS
|
||||
@ -0,0 +1,19 @@
|
||||
# Audit: code-block
|
||||
audit-version: 1
|
||||
audited-at: 2026-06-26
|
||||
scope: eidos (eidos-only primitive)
|
||||
method: theming-coherence category sweep (typography), adversarially verified; roles + `:Morfo` lead-verified CLEAN across all eidos (no success/warning/danger/info; only alert-dialog uses `:Morfo`).
|
||||
|
||||
## Summary
|
||||
Counts (post-verification): CRITICAL 0 · HIGH 0 · MEDIUM 0 · LOW 0. Theming-clean.
|
||||
conformance: morfo OK; no recipe; ControlVariant canonical; no color/role issues
|
||||
as-const: true · roles clean: true · variants clean: true · has-recipe: false
|
||||
|
||||
## Findings
|
||||
_No findings (theming-clean primitive)._
|
||||
|
||||
## Theming facts (E-bis)
|
||||
- magic literals: none
|
||||
- magic z-index: none
|
||||
- non-canonical names: none
|
||||
- roles clean: true · variants clean: true
|
||||
@ -0,0 +1,26 @@
|
||||
# Audit: code
|
||||
audit-version: 1
|
||||
audited-at: 2026-06-26
|
||||
scope: eidos (eidos-only primitive)
|
||||
method: theming-coherence category sweep (typography), adversarially verified; roles + `:Morfo` lead-verified CLEAN across all eidos (no success/warning/danger/info; only alert-dialog uses `:Morfo`).
|
||||
|
||||
## Summary
|
||||
Counts (post-verification): CRITICAL 0 · HIGH 0 · MEDIUM 0 · LOW 1.
|
||||
conformance: morfo OK; no recipe; roles fragmented (content-role subset)
|
||||
as-const: true · roles clean: false · variants clean: true · has-recipe: false
|
||||
|
||||
## Findings
|
||||
### LOW: em-literal padding where no canonical scale exists — code-002 <!-- id: code-002 -->
|
||||
- dimension: E-bis: Theming (SIZES)
|
||||
- location: src/uix/eidos/components/code/code.css:30-31
|
||||
- evidence: padding-inline: 0.32em; padding-block: 0.1em (lines 30-31, also variants at 36-37)
|
||||
- impact: Magic em literals; no canonical padding-em scale for pill chrome
|
||||
- proposed-fix: Define --code-padding-inline and --code-padding-block tokens or inherit from mark
|
||||
- verify: [downgraded] code.css:30-31,36-37 'padding-inline: 0.32em; padding-block: 0.1em' are em-relative paddings for an inline-flow pill, intentionally outside the absolute --space-* scale so the pill scales with surrounding prose font-size (the purpose of inline <code>). No canonical em-based padding scale exists. Legitimate physically-relative literal for inline chrome, not drift against an existing scale. Downgrad
|
||||
- fix-status: open
|
||||
|
||||
## Theming facts (E-bis)
|
||||
- magic literals: 0.32em @ src/uix/eidos/components/code/code.css:30 -> canonical code-padding-inline token | 0.1em @ src/uix/eidos/components/code/code.css:31 -> canonical code-padding-block token
|
||||
- magic z-index: none
|
||||
- non-canonical names: none
|
||||
- roles clean: false · variants clean: true
|
||||
@ -0,0 +1,53 @@
|
||||
# Audit: collapsible
|
||||
audit-version: 1
|
||||
audited-at: 2026-06-26
|
||||
scope: ['soma', 'sema'] (SCOPE-DRIFT → SYS-1)
|
||||
method: adversarially-verified workflow; HIGH lead-verified. B6 ground-truth: checkbox/toggle/switch commit-toggle = sequence post (lag-fixed); radio-group/tabs/accordion/stepper set state at call-site (pre OK); toggle-group + checkbox-group carry the A31 .includes pattern (SYS-7); slider has no gesture-layer A6 leak.
|
||||
provider: G:\dev\svelte\vicen\src\uix\soma\components\collapsible\collapsible-provider.svelte.ts
|
||||
sequence-audit: expand: post-sequence ✓ (state set in handler lines 68-70, morfo sequence:post at line 39). collapse: pre-sequence ✓ (state set in handler lines 71-73, morfo sequence:pre at line 48). Both events fire via runtime.trigger() in toggle() which defers handler execution. No lag risk: handler state-settin
|
||||
|
||||
## Summary
|
||||
Counts (post-verification): CRITICAL 0 · HIGH 1 · MEDIUM 1 · LOW 0.
|
||||
|
||||
## Findings
|
||||
### MEDIUM: SYS-1 scope-drift: eidos directory exists but morfo scope omits 'eidos' — collapsible-001 <!-- id: collapsible-001 -->
|
||||
- dimension: Contract(morfo)
|
||||
- rule: SYS-1 scope-drift: eidos directory exists but morfo scope omits 'eidos'
|
||||
- location: src/uix/morfo/components/collapsible.ts:22
|
||||
- evidence: morfo declares scope: ['soma', 'sema'] but src/uix/eidos/components/collapsible/ directory exists with full implementation (collapsible.svelte, collapsible.css, index.ts). Accordion (sibling pattern) correctly declares scope: ['soma', 'sema', 'eidos']
|
||||
- impact: Scope declaration diverges from actual architecture; contract validators may not catch eidos-layer changes. Documentation of morfo scope is stale.
|
||||
- proposed-fix: Update collapsibleMorfo scope to ['soma', 'sema', 'eidos'] to match accordion pattern and actual directory structure
|
||||
- verify: [confirmed] CONFIRMED. morfo declares `scope: ['soma', 'sema']` at src/uix/morfo/components/collapsible.ts:22, but src/uix/eidos/components/collapsible/ exists with a full implementation (collapsible.svelte, collapsible.css, collapsible-trigger.svelte, collapsible-content.svelte, types.ts, index.ts, README.md — verified via glob). The sibling disclosure pattern accordion correctly declares `scope: ['soma', 'sema', 'eidos']` at src/uix/morfo/components/accordion.ts:7. This is SYS-1 scope-drift, MEDIUM. Severity unchanged.
|
||||
- fix-status: open
|
||||
|
||||
### HIGH: SEQUENCE-LAG — `collapse` is `sequence:'pre'` but sets state in the handler (the Checkbox-244ms-lag mechanism) — collapsible-NEW-001 <!-- id: collapsible-NEW-001 -->
|
||||
- dimension: Behavior(soma)
|
||||
- rule: SEQUENCE / TWO-MOMENTS — a control whose functional state is set INSIDE the runtime.trigger HANDLER must use morfo `sequence: 'post'`; with 'pre' the perceptual hold (~240ms) blocks the state change (documented Checkbox 244ms-lag incident).
|
||||
- location: src/uix/morfo/components/collapsible.ts:48 (collapse sequence:'pre') + src/uix/soma/components/collapsible/collapsible-provider.svelte.ts:71-73 (handler sets state)
|
||||
- evidence: The `collapse` event sets the functional state INSIDE the trigger handler: provider lines 71-73 `collapse: () => { this.opts.open.current = false; }`. The morfo declares `sequence: 'pre'` at collapsible.ts:48. For 'pre', the runtime serializes the perceptual emit BEFORE the handler: runtime.svelte.ts:749-752 `else { await runEmit(); if (handler) await handler(); }`. `runEmit` awaits `sources.eventEngine.emit` (runtime.svelte.ts:696 `await sources.eventEngine.emit(...)`), whose engine awaits the visual channel hold (engine.ts:250 `await visualChannel.handle(enriched, effective)`), which does `await sleep(holdMs)` (visual.ts:79-81). collapsible declares no per-event `hold` and the sema pack (sema/components/collapsible.ts) declares none, so `emerge` falls back to label `brief` (visual.ts:43) = 240ms (durations.ts:27). Result: the content does NOT receive `hidden` until ~240ms after the click — the exact magnitude of the documented Checkbox 244ms-lag. (The `expand` event is correctly `post` at collapsible.ts:39, so opening has no lag — only closing lags.) This refutes the prompt's sequenceAudit line which marked `collapse: pre-sequence ✓`.
|
||||
- impact: User clicks the trigger to collapse an open section; the panel stays fully visible for ~240ms before hiding — a perceptible dead delay on every close, identical in mechanism and magnitude to the Checkbox 244ms-lag that the project already documented and fixed by flipping to `sequence:'post'`. The morfo's inline justification (collapsible.ts:10-13, 'collapse remains pre-sequence so the exit signal can play while the content is still visible') is based on a false concurrency assumption: the runtime does NOT overlap emit and handler for 'pre' — it serializes emit-then-handler, so the content is held visible for the FULL hold, then hidden, rather than the exit sound playing during a CSS exit transition.
|
||||
- repro: Open a Collapsible (expand — instant), then click the trigger to collapse. Measure the delay between click and `hidden`/`data-state='closed'` taking effect on the content. Expect ~240ms (the emerge `brief` hold) rather than near-zero. The expand direction does not exhibit the delay.
|
||||
- proposed-fix: Apply the documented Checkbox/Toggle/Switch doctrine: change `collapse` to `sequence: 'post'` (collapsible.ts:48) so the handler flips `open=false` first and the exit signal then acknowledges the resolved state, matching how accordion's symmetric close event is handled (accordion close is post per accordion.ts pattern). If a visible-during-exit cue is genuinely wanted, drive it from eidos CSS keyed on `data-state='closed'` + a CSS exit transition rather than blocking the functional state behind the sema hold. Alternatively set state at the call-site in `toggle()` (collapsible-provider.svelte.ts:85-89) before `runtime.trigger`, which would make 'pre' tolerable (the RadioGroup/Tabs pattern).
|
||||
- verify: [verifier-added → LEAD-CONFIRMED HIGH] The analyze agent (and the morfo's own inline comment) marked this CLEAN, assuming `'pre'` overlaps emit+handler so the exit plays during the hold. The verifier traced the actual runtime and proved it SERIALIZES (`runtime.svelte.ts:749-752: await runEmit(); await handler()`), so `open=false` is delayed the full ~240ms `brief` emerge hold. I initially LEANED toward MEDIUM (reasoning "collapse is emerge.dismiss 'pre', the canonical animate-before-hide pattern like dialog/popover"), but that masking only holds for components that animate on `data-event`/Presence DURING the hold (dialog does); collapsible's exit is keyed on `data-state='closed'`, which doesn't flip until AFTER the hold — so the 240ms is a real dead delay, not a filled exit animation. Restored to HIGH. Caveat: like the checkbox fix, the user-perceptible magnitude should be confirmed by a frame-by-frame browser measurement before/after; the MECHANISM (and the refuted morfo justification) are confirmed in code.
|
||||
- fix-status: open
|
||||
|
||||
## No-findings dimensions
|
||||
Behavior(soma) [except the sequence-lag above], DOM-selector, Frontier, TSC, Theming, Tests
|
||||
|
||||
## Theming facts (E-bis)
|
||||
- magic z-index: none
|
||||
- magic literals: collapsible.css:4,19 translateY(-4px) — LOCAL animation amplitude in @keyframes, acceptable
|
||||
- undeclared parts: none
|
||||
- roles clean: true · variants clean: true
|
||||
|
||||
## Tests (F)
|
||||
- exists: true · env: vitest jsdom
|
||||
- covers: ID linking via partRef; expand/collapse events + state toggle; disabled guard + disabled attr projection
|
||||
- untested: onOpenChange callback firing
|
||||
|
||||
## Style observations (non-blocking)
|
||||
- CSS uses canonical token vars (--space-*, --color-*, --font-*, --duration-*, --ease-*, --opacity-*, --radius-*)
|
||||
- Recipe collapsible: in base.ts lines 712-725 with 12 tokens, all reference --*; no raw hex/px/rem/em
|
||||
- Naming: --collapsible-{slot}.{property} consistent (trigger-color, trigger-padding, content-padding, etc)
|
||||
- Animations in @keyframes use local px values for amplitude (-4px translateY), not tied to design tokens (acceptable for local animation logic)
|
||||
- No magic z-index or opacity decimals
|
||||
@ -0,0 +1,55 @@
|
||||
# Audit: color-field
|
||||
audit-version: 1
|
||||
audited-at: 2026-06-26
|
||||
scope: ['soma', 'sema'] (SCOPE-DRIFT → SYS-1)
|
||||
method: adversarially-verified workflow (analyze → refute); HIGH/CRITICAL lead-verified. Clean-check pseudo-findings dropped. B4 ground-truth: segmented date/time/color-field register inputId via $effect (SYS-A30-EFFECT, no loop); number-field is the direct-assignment reference.
|
||||
provider: G:/dev/svelte/vicen/src/uix/soma/components/color-field/color-field-provider.svelte.ts
|
||||
|
||||
## Summary
|
||||
Counts (post-verification): CRITICAL 0 · HIGH 0 · MEDIUM 2 · LOW 0.
|
||||
|
||||
## Findings
|
||||
### MEDIUM: SYS-1: morfo scope declares eidos layer when eidos recipe dir exists — color-field-001 <!-- id: color-field-001 -->
|
||||
- dimension: Frontier
|
||||
- rule: SYS-1: morfo scope declares eidos layer when eidos recipe dir exists
|
||||
- location: src/uix/morfo/components/color-field.ts:7
|
||||
- evidence: scope: ['soma', 'sema'], but src/uix/eidos/components/color-field/ directory exists with 9 files (color-field.svelte, color-field.css, types.ts, etc.)
|
||||
- impact: Contract drift: callers may assume eidos layer is not provided; scope-drift masks available visual customization surface.
|
||||
- proposed-fix: Add 'eidos' to scope declaration: scope: ['soma', 'sema', 'eidos']
|
||||
- verify: [confirmed] Confirmed SYS-1 scope-drift. morfo/components/color-field.ts:7 `scope: ['soma', 'sema'],` omits 'eidos'; eidos dir src/uix/eidos/components/color-field/ exists with real color-field.css (10039 bytes) + recipe. MEDIUM correct. Systemic across field family (css/date/number/password/search/time-field all do the same).
|
||||
- fix-status: open
|
||||
|
||||
### MEDIUM: F: test coverage misses high-risk readonly-segments + value=undefined path — color-field-002 <!-- id: color-field-002 -->
|
||||
- dimension: Tests
|
||||
- rule: F: test coverage misses high-risk readonly-segments + value=undefined path
|
||||
- location: src/uix/soma/components/color-field/color-field-provider.svelte.test.ts:94-258
|
||||
- evidence: Test suite covers: segment fills (lines 100-123), keyboard hex input (125-155), format switching (157-188), wiring (190-257). Missing: readonly-segment-without-value warning assertion (defined in soma at lines 215-233) and edge cases like backspace, Home/End navigation, readonly interaction.
|
||||
- impact: A24 warning logic is unexercised; regressions in readonlySegments guard logic could slip through. readonly-segment test is a HIGH-RISK gap per audit rules.
|
||||
- proposed-fix: Add test case: 'warns when readonlySegments set without value' — assert soma.logger.warn is called with expected message. Add test for backspace on first segment (moveToPrev).
|
||||
- verify: [confirmed] Confirmed. Provider warning at color-field-provider.svelte.ts:215-233 (spam-guarded via lastWarnedKey, calls soma.logger.warn for readonlySegments set + value undefined) is unexercised: test only mocks `warn: vi.fn()` (line 46) and seeds empty `readonlySegments` (line 76). No assertion, no non-empty+undefined case. A24 high-risk path missing test = MEDIUM.
|
||||
- fix-status: open
|
||||
|
||||
### MEDIUM: A30 — inputId registered via `$effect` instead of direct constructor assignment (SYSTEMIC: date/time/color-field) <!-- id: color-field-A30 -->
|
||||
- dimension: B
|
||||
- rule: A30 (child→parent id registration MUST be a direct constructor assignment, not `$effect`)
|
||||
- location: color-field-provider.svelte.ts:521-525 (`$effect(() => { const field = this.provider.field; if (!field) return; field.inputId.current = opts.id.current; })`)
|
||||
- evidence: lead-confirmed by direct read — identical `$effect` shape to date-field (932-936) and time-field (531-535); the reference NumberField uses a direct guard (number-field:585-587).
|
||||
- impact: no live loop (write-only + stable id), but contrary to A30 doctrine + the NumberField reference. See SYS-A30-EFFECT in SUMMARY.
|
||||
- proposed-fix: replace with a direct constructor assignment matching NumberField; apply across date/time/color-field.
|
||||
- verify: [lead-added] the color-field agent listed "Behavior" as a no-findings dimension, which masked the `$effect` id-wiring; lead read of color-field:521-525 confirms it. Systemic MEDIUM.
|
||||
- fix-status: open
|
||||
|
||||
## No-findings dimensions
|
||||
Contract, Behavior (except A30 id-wiring — see color-field-A30), DOM-selector, TSC
|
||||
|
||||
## Theming facts (E-bis)
|
||||
- magic z-index: none
|
||||
- magic literals: none
|
||||
- undeclared parts: none
|
||||
- roles clean: true · variants clean: true
|
||||
- label-font (one step below input?): CLEAN: calc(1em - (var(--font-size-md) - var(--font-size-sm))) at src/uix/eidos/components/color-field/color-field.css:106 correctly implements label one typographic step below input size (md → sm = 2px)
|
||||
|
||||
## Tests (F)
|
||||
- exists: true · env: jsdom
|
||||
- covers: segment-commit: RGB fill sequencing (lines 100-123); keyboard: hex segment digit input with aria spinbutton (125-155); format-select: switching formats + allowedFormats narrowing + locked state (157-188); hidden-input: form field name/value wiring + required attribute (190-257); label-focus: label click routes focus to first segment (252-253)
|
||||
- untested: readonly-segment-without-value warning (A24 guard); backspace navigation edge case (moveToPrevSegment on null segment); Home/End key boundary behaviors (first-item / last-item); readonly segments interaction with keyboard input; Arrow key wrapping (cycle vs no-cycle per channel config)
|
||||
@ -0,0 +1,72 @@
|
||||
# Audit: color-picker
|
||||
audit-version: 1
|
||||
audited-at: 2026-06-26
|
||||
scope: (SCOPE-DRIFT → SYS-1)
|
||||
method: adversarially-verified workflow (analyze → refute); HIGH/CRITICAL personally re-verified by the lead. Batch-3 ground-truth: each picker fires trigger(close) (close NOT inert), but open/commit-reset ARE inert; calendar/range-calendar are MID-REFACTOR (uncommitted view-switch work).
|
||||
provider: src/uix/soma/components/color-picker/color-picker-provider.svelte.ts
|
||||
|
||||
## Summary
|
||||
Counts (post-verification): CRITICAL 0 · HIGH 0 · MEDIUM 2 · LOW 2.
|
||||
systemic hits: SYS-1 (scope-drift MEDIUM); SYS-5 (area thumb hardcoded z-index HIGH).
|
||||
composition (A27): Composes (A27): Popover + ColorField + Slider via shared writableActive refs. ChannelSlider composes SliderProvider underneath with channel-aware bridging (gradient + setChannel). ColorField parts (Input/Segment/FormatSelect) re-exported as ChannelInput/ChannelSegment/FormatSelect with data-color-picker identity attrs overlaid. Shared state wiring correct: sharedValue, sharedFormat, sharedOpen passed to both ColorPickerProvider and PopoverProvider/ColorFieldProvider. Content part re-exported from Popover (registers on Popover runtime, not picker runtime).
|
||||
|
||||
## Findings
|
||||
### MEDIUM: SYS-1 scope-drift — color-picker-001 <!-- id: color-picker-001 -->
|
||||
- dimension: A
|
||||
- rule: SYS-1 scope-drift
|
||||
- location: src/uix/morfo/components/color-picker.ts:7
|
||||
- evidence: colorPickerMorfo declares scope: ['soma', 'sema'] but eidos directory exists at src/uix/eidos/components/color-picker/ containing recipe CSS and multiple eidos component wrappers (.svelte files)
|
||||
- impact: Morfo scope contract is incomplete. Downstream tooling expecting strict scope adherence will find undeclared layer.
|
||||
- proposed-fix: Update morfo scope to ['soma', 'sema', 'eidos']
|
||||
- verify: [confirmed] Confirmed SYS-1 scope-drift. color-picker.ts:7 reads `scope: ['soma', 'sema'],` yet a full eidos implementation exists at src/uix/eidos/components/color-picker/ (color-picker.css recipe + 25+ .svelte wrappers per Glob). `'eidos'` IS a valid Layer (src/uix/types.ts:17 `export type Layer = 'soma' | 'sema' | 'eidos';`), and morfo `scope: readonly Layer[]` (types.ts:799). date-picker.ts:13 has the identical `scope: ['soma', 'sema']`, so this is the documented systemic SYS-1 across the picker family. MEDIUM is correct.
|
||||
- fix-status: open
|
||||
|
||||
### MEDIUM: Inert 'open' event — color-picker-003 <!-- id: color-picker-003 -->
|
||||
- dimension: B
|
||||
- rule: Inert 'open' event
|
||||
- location: src/uix/morfo/components/color-picker.ts:14-31
|
||||
- evidence: Morfo declares name:'open' event with target=partRef('content') and sequence:'pre'. No runtime.trigger('open',...) call found in provider. Comment on close event (line 38-39) documents pattern: provider sets opts.open=false and lets popover unmount, never fires the event explicitly.
|
||||
- impact: Consumer code declaring open event handlers will not receive them. The event exists only to satisfy schema validators. This is documented for pickers (known pattern like date-picker/time-picker) but represents a contract mismatch.
|
||||
- proposed-fix: Document in morfo comment that 'open' event is not fired (inert, exists for contract symmetry); or fire it on popover open (requires Popover composition to surface trigger point)
|
||||
- verify: [confirmed] Confirmed inert event. Morfo declares `name: 'open'` (color-picker.ts:14-32) with sequence 'pre' targeting content; grep for `trigger('open'` across src/uix/soma/components/color-picker returns NO matches. The provider only ever fires 'close' (via triggerClose, lines 349-375), 'handle-pick'/'handle-drag'/'commit-set'. The morfo's own comment (lines 10-13 'They drive Footer...' and 38-41 'today the provider just sets opts.open = false and lets the popover unmount') documents the known picker-family inert pattern. 'commit-reset' is also inert (never triggered) — candidate only flags 'open', which is in-scope and accurate. MEDIUM matches the audit's 'MEDIUM at most' for inert events.
|
||||
- fix-status: open
|
||||
|
||||
### LOW: Magic z-index literals not in STATIC_Z_INDEX scale — color-picker-002 <!-- id: color-picker-002 -->
|
||||
- dimension: E-bis
|
||||
- rule: Magic z-index literals not in STATIC_Z_INDEX scale
|
||||
- location: src/uix/eidos/components/color-picker/color-picker.css:367,557
|
||||
- evidence: z-index: 2 on [data-color-picker-area-thumb]:367, z-index: 1 on [data-color-picker-swatch-indicator]:557. Project STATIC_Z_INDEX scale: base=0, raised=1, sticky=100, dropdown=300, popover=400, tooltip=500, modal=700, toast=900. These values (1,2) accidentally collide with low scale but are not intentionally using that scale.
|
||||
- impact: Fragile stacking context: undocumented magic literals break auditing of depth relationships. If future components add z-index:3 the priority breaks.
|
||||
- proposed-fix: Use var(--z-index-raised) or var(--z-index-sticky) from canonical scale instead of bare integers
|
||||
- verify: [downgraded] Literals confirmed present — color-picker.css:367 `z-index: 2;` (area-thumb) and :557 `z-index: 1;` (swatch-indicator). But the HIGH severity and the SYS-2 mapping are wrong. SYS-2 targets bare 60-99 integers that collide with the depth-PLANE scale (--z-index-dropdown=300/popover=400/modal=700/etc). These values form a self-contained LOCAL sibling stack within the picker subtree: grep shows the file uses only -1 (background layers, :433/:402/:412), 1 (indicator over swatch), 2 (thumb over area-background). The canonical STATIC_Z_INDEX scale is for cross-component depth planes, not intra-component layering; substituting `var(--z-index-sticky)` (=100) as the proposed fix would be semantically wrong for a thumb-over-its-own-background. No real stacking-context fragility (no other component reaches into this subtree's z-context). Downgrade to LOW (cosmetic/token-tidiness at most); not a SYS-2 magic-z violation.
|
||||
- fix-status: open
|
||||
|
||||
### LOW: Test environment jsdom with interaction-heavy keyboard — color-picker-004 <!-- id: color-picker-004 -->
|
||||
- dimension: F
|
||||
- rule: Test environment jsdom with interaction-heavy keyboard
|
||||
- location: src/uix/soma/components/color-picker/color-picker-provider.svelte.test.ts:1
|
||||
- evidence: @vitest-environment jsdom. Tests cover: ColorPickerAreaThumbProvider.onkeydown (ArrowRight at line 227) and keyboard route logic (HOME/END/PAGE_UP/PAGE_DOWN at lines 905-915 in provider). No Playwright/browser test confirming area keyboard in real DOM.
|
||||
- impact: Area thumb keyboard handling (Arrow x4 + Home/End + PageUp/PageDown) is interaction-heavy and DOM-geometry-dependent. jsdom may not accurately simulate getBoundingClientRect or pointer geometry used in handlePointerMove / calculateChannelValue.
|
||||
- proposed-fix: Add browser-level test (Playwright) for area keyboard + pointer drag to verify geometry math in real DOM
|
||||
- verify: [downgraded] Confirmed jsdom env (test file line 1 `// @vitest-environment jsdom`) and the provider has interaction-heavy area logic. This is the SYS-3 baseline (jsdom-only / kbd-untested) so a real F-gap exists. BUT the candidate's core rationale is partly wrong: the area-thumb keyboard route (onkeydown, provider lines 882-931) is PURE arithmetic over getChannelRange/getColorChannel — it never reads getBoundingClientRect, so it IS faithfully exercisable in jsdom (test does exercise ArrowRight et al.). Only the POINTER path (handlePointerMove, lines 764-777, consumes DOMRect) is geometry-dependent and genuinely under-covered in jsdom. So the gap is narrower (pointer-drag geometry, not keyboard) than the finding states. Downgrade to LOW per SYS-3.
|
||||
- fix-status: open
|
||||
|
||||
## No-findings dimensions
|
||||
C, D
|
||||
|
||||
## Theming facts (E-bis)
|
||||
- magic z-index: z-index: 2 | z-index: 1
|
||||
- magic literals: 16px on area-thumb-size | 11rem on area-height | 1.25rem/1.5rem/1.75rem on swatch sizes
|
||||
- undeclared parts: none
|
||||
- roles clean: true · variants clean: true
|
||||
|
||||
## Tests (F)
|
||||
- exists: true · env: jsdom
|
||||
- covers: channel value preservation; area thumb pointer move + keyboard; area channel override; setChannel + commitChange; swatch select + closeOnSelect
|
||||
- untested: area thumb keyboard routes (Home/End/PageUp/PageDown) in browser; pointer drag geometry in real DOM; popover open/close animations; dismissal outside; eyedropper API integration
|
||||
|
||||
## Style observations (non-blocking)
|
||||
- color-picker.css uses CSS containment (contain: layout style) on area to prevent sibling layout shifts during drag
|
||||
- Transparency checker pattern re-used via CSS vars across multiple elements
|
||||
- Area overlays (saturation/brightness) use pseudo-elements to avoid conflicts with inline background
|
||||
- Swatch group uses flex 1 1 0 + aspect-ratio to shrink/grow equally while staying square
|
||||
@ -0,0 +1,115 @@
|
||||
# Audit: combobox
|
||||
audit-version: 1
|
||||
audited-at: 2026-06-26
|
||||
scope: ['soma', 'sema'] (eidos recipe present though 'eidos' not in morfo scope — systemic SCOPE-DRIFT)
|
||||
files:
|
||||
- provider: src/uix/soma/components/combobox/combobox-provider.svelte.ts (present)
|
||||
- morfo: src/uix/morfo/components/combobox.ts (present)
|
||||
- recipe: src/uix/eidos/components/combobox/combobox.css (558L) + lib/recipes/base.ts §combobox (present)
|
||||
- demo: web/routes/uix/components/combobox (present, not inspected this pass)
|
||||
- test: src/uix/soma/components/combobox/combobox-provider.svelte.test.ts (present, jsdom)
|
||||
- readme: present (not inspected this pass)
|
||||
|
||||
## Summary
|
||||
Combobox is well-built in several respects: the A33 `labelRegistry` clone-reassign incident is fixed (now
|
||||
`SvelteMap`), dimension C is clean (`CSS.escape` on the highlighted-id query + the shared `resolveListItemEl`
|
||||
helper escapes `data-value`), dismissal excludes both trigger and input (A22), and the inputValue auto-sync /
|
||||
mode-reset effects are loop-safe (`untrack` + guards). The headline defect is a **focus-strategy violation
|
||||
(A17)**: the morfo declares `aria-activedescendant` (virtual focus) but the provider moves **real DOM focus**
|
||||
onto items (`dom.focus(items[0])`), mixing the two strategies and breaking the APG combobox contract (focus
|
||||
must stay on the textbox). Plus a `translationRef` misuse, the systemic A31 + magic-z-index, and a jsdom-only
|
||||
test gap right where the focus bug lives.
|
||||
|
||||
Counts: CRITICAL 0 · HIGH 2 · MEDIUM 3 · LOW 0.
|
||||
|
||||
## Findings
|
||||
|
||||
### HIGH: Mixes virtual focus (`aria-activedescendant`) with real DOM focus (`.focus()` on items) — A17 violation + breaks APG combobox <!-- id: combobox-001 -->
|
||||
- dimension: B, A
|
||||
- rule: A17 ("Never mix [virtual + DOM focus] in the same component. If the trigger has aria-activedescendant,
|
||||
items must NOT call .focus()") + APG Combobox pattern (focus stays on the textbox) + morfo↔code drift
|
||||
- location: morfo combobox.ts:106 (Input declares `aria-activedescendant`) vs provider:392, 408, 661, 684
|
||||
(`this.provider.soma.dom.focus(...)` on items / input) + the items carry `tabindex: -1` (:803) so they take focus.
|
||||
- evidence: the Input keydown opens then `dom.focus(items[0])` AND sets `highlightedId` (:387-394); the Content
|
||||
keydown computes `currentIndex` from `highlightedId` (virtual) yet also `dom.focus(target)` (:684, real). So
|
||||
the component simultaneously (a) advertises `aria-activedescendant` on the input and (b) moves real focus to
|
||||
the option. The morfo's `aria-activedescendant` declaration says "virtual"; the code does "real".
|
||||
- impact: with focus moved onto an option, the input's `aria-activedescendant` is meaningless (it only applies
|
||||
while focus is on the input). Screen readers get a contradictory model (an option both has DOM focus and is
|
||||
the input's active descendant). APG's combobox keeps focus on the textbox; moving it to options is the
|
||||
listbox-with-DOM-focus pattern — picking one is required.
|
||||
- repro: open the listbox, ArrowDown, inspect `document.activeElement` (an option) while the input still has
|
||||
`aria-activedescendant` set to that option.
|
||||
- proposed-fix: commit to virtual focus (the morfo's declared model): keep DOM focus on the input, drive
|
||||
selection purely via `highlightedId`/`data-highlighted` + `scrollIntoView`, never `dom.focus(item)`. (Or, if
|
||||
real focus is intended, drop `aria-activedescendant` from the morfo and the highlightedId machinery — but
|
||||
that diverges from APG combobox.)
|
||||
- fix-status: open
|
||||
|
||||
### HIGH: Tests pin selection logic only — keyboard/focus (where the A17 bug lives), dismissal-exclusion, custom-value, backspace untested; jsdom-only <!-- id: combobox-002 -->
|
||||
- dimension: F
|
||||
- rule: dimension-F honesty check
|
||||
- location: combobox-provider.svelte.test.ts (3 tests: single-select, multi toggle + inputValue, getItems disabled-filter)
|
||||
- evidence: no test exercises Input/Content `onkeydown` (the focus moves in combobox-001), the dismissal
|
||||
trigger/input exclusion (`isValidEvent` + `onInteractOutside`, :594-626), `commitCustomValue`,
|
||||
backspace-remove-last, or the inputValue auto-sync effect. `@vitest-environment jsdom` (:1) — focus isn't
|
||||
faithful, so even a keyboard test here couldn't validate the A17 behavior.
|
||||
- impact: the focus-strategy bug and every keyboard path ship unverified.
|
||||
- proposed-fix: add provider tests for the keyboard routes + dismissal exclusion + custom value; add a
|
||||
client/Playwright test that asserts `activeElement` stays on the input (the A17 contract).
|
||||
- fix-status: open
|
||||
|
||||
### MEDIUM: `aria-label` parts pass the full idlangref as the `translationRef` key instead of the `texts` key <!-- id: combobox-003 -->
|
||||
- dimension: A
|
||||
- rule: A3 / A34 (translation namespace) + consistency with the canonical `v.translationRef(key, fallback)` form
|
||||
- location: morfo combobox.ts:142, 168, 291, 323 — e.g. `v.translationRef('#?components.combobox.toggle|Toggle')`
|
||||
while `texts.toggle = '#?components.combobox.toggle|Toggle'` (:11) is declared.
|
||||
- evidence: every other component references a `texts` entry by its KEY (Dialog:
|
||||
`v.translationRef('content.roledescription', 'dialog window')`). Combobox passes the resolved absolute
|
||||
idlangref string as the key, so the `texts` entries (`toggle`/`clear`/`selectedTags`/`selectedTagRemove`) are
|
||||
declared but never referenced by key — the indirection is bypassed.
|
||||
- impact: at best redundant (the string happens to be a valid idlangref so it may resolve); at worst a
|
||||
missing-key lookup. Either way it's inconsistent with the framework's pattern and leaves the `texts` keys
|
||||
dead. Smoke's translation-key check should be confirmed against these (see VALIDATOR_GAPS).
|
||||
- proposed-fix: use `v.translationRef('toggle', 'Toggle')` etc., referencing the `texts` keys.
|
||||
- fix-status: open
|
||||
|
||||
### MEDIUM: Per-item `isSelected` reads the whole `value` array through the provider (A31 O(N²)) <!-- id: combobox-004 -->
|
||||
- dimension: B
|
||||
- rule: A31
|
||||
- location: combobox-provider.svelte.ts:782 (`isSelected = $derived.by(() => this.provider.isSelected(value))`)
|
||||
→ :182-184 (`.includes`)
|
||||
- evidence: identical to select-007 — each Item's derivation calls a provider method reading the shared `value`
|
||||
array (`.includes`, O(N)); on value change all N items re-derive → O(N²).
|
||||
- impact: degrades on large multi-select lists (the A31 "5 fine / 30 hangs" class).
|
||||
- proposed-fix: lift `selectedSet = new SvelteSet(value)` on the provider; item derivation does `.has(value)`.
|
||||
(Shared with Select — see SHARED_EXTRACTION / systemic A31-LIST-SELECTION.)
|
||||
- fix-status: open
|
||||
|
||||
### MEDIUM: `content-z: '80'` magic z-index literal <!-- id: combobox-005 -->
|
||||
- dimension: E-bis
|
||||
- rule: THEMING §35 Bloque C (z-index magic → `--z-index-*` tokens)
|
||||
- location: lib/recipes/base.ts:4254 (`'content-z': '80'`); combobox.css:261.
|
||||
- evidence: bare `80` (same value as `select.content-z`). Fourth overlay with a hardcoded content-z
|
||||
(select 80, dialog 70, popover 75/60, combobox 80) → SYSTEMIC z-ladder finding.
|
||||
- impact: see select-009 / popover-002.
|
||||
- proposed-fix: tokenize to the canonical overlay z-ladder.
|
||||
- fix-status: open
|
||||
|
||||
## No-findings dimensions
|
||||
- **C (selectors):** the Enter handler uses `#${CSS.escape(highlightedId)}` (:417-419) and `resolveItemEl`
|
||||
delegates to `resolveListItemEl` which escapes `data-value` (list-selection.ts:145). No injection. (This is
|
||||
the safe pattern Select's `scrollSelectedIntoView` fails to use — see select-001.)
|
||||
- **A33:** `labelRegistry` is `SvelteMap` (:97) — the documented A33 clone-reassign incident is resolved.
|
||||
- **B (loop-safety):** inputValue auto-sync (:151-160) and mode-switch reset (:166-175) effects guard writes
|
||||
and use `untrack`; no feedback loop. Dismissal excludes trigger + input (A22, :594-626).
|
||||
- **D (frontier, severe half):** provider imports only soma layers + morfo — zero eidos imports.
|
||||
- **E-bis (colors):** combobox.css has no raw hex / raw px-rem font literals in the scanned surface (only the
|
||||
z-index literal above). (Full per-selector morfo-part diff not exhaustively run this pass — the CSS is 558L.)
|
||||
|
||||
## Style observations (opinion, non-blocking)
|
||||
- The A17 violation is the single most important fix: the morfo already commits to virtual focus, so aligning
|
||||
the provider to it (rather than the reverse) keeps the APG-correct model and removes the real-focus paths.
|
||||
- The `list-selection.ts` extraction (shared with Select) is a good example of dimension-G done right — the
|
||||
selection state machine is one pure, testable module. The remaining duplication (the directional-nav index
|
||||
math) is the part still copied per-component (see select-003 / SHARED_EXTRACTION).
|
||||
@ -0,0 +1,91 @@
|
||||
# Audit: command
|
||||
audit-version: 1
|
||||
audited-at: 2026-06-26
|
||||
scope: (SCOPE-DRIFT → SYS-1)
|
||||
method: adversarially-verified workflow (analyze → refute); HIGH/CRITICAL personally re-verified against cited code by the lead.
|
||||
provider: src/uix/soma/components/command/command-provider.svelte.ts
|
||||
|
||||
## Summary
|
||||
Counts (post-verification): CRITICAL 0 · HIGH 0 · MEDIUM 1 · LOW 5.
|
||||
systemic hits: SYS-1 (scope-drift); SYS-3 (jsdom-only, keyboard untested).
|
||||
|
||||
## Findings
|
||||
### MEDIUM: SYS-1 scope-drift — command-001 <!-- id: command-001 -->
|
||||
- dimension: E-bis
|
||||
- rule: SYS-1 scope-drift
|
||||
- location: src/uix/morfo/components/command.ts:7
|
||||
- evidence: scope: ['soma', 'sema'], — but eidos/components/command/ directory exists with CSS and components. Morfo declares scope omitting 'eidos'
|
||||
- impact: Component has eidos visuals and recipes but scope doesn't declare it; scope drift breaks the four-layer contract and may cause issues with validation or tooling that expect declared scopes
|
||||
- proposed-fix: Add 'eidos' to the scope array: scope: ['soma', 'sema', 'eidos']
|
||||
- verify: [downgraded] morfo line 7 reads `scope: ['soma', 'sema'],` and an eidos/components/command/ dir with CSS exists. But this is NOT command-specific drift: I counted 63 stateful morfos (combobox, calendar, date-picker, color-picker, dialog, select, popover, listbox, etc.) that ALL omit 'eidos' from scope while shipping an eidos recipe dir. The `scope` field semantics in src/uix/morfo/types.ts:770-839 govern SEMA/event coherence ('If a morfo declares events[] AND scope:['sema']...'), not eidos-recipe presence — the visual layer is not gated by the scope array. No contract test asserts scope must include 'eidos' when CSS exists. Treating this as a HIGH per-component contract violation contradicts the framework's own baseline (63/64 components do it). At most a systemic LOW doc/convention observation; the stated HIGH severity and 'breaks the four-layer contract' impact are not supportable.
|
||||
- fix-status: open
|
||||
|
||||
### LOW: Magic literal — em font sizes — command-003 <!-- id: command-003 -->
|
||||
- dimension: E-bis
|
||||
- rule: Magic literal — em font sizes
|
||||
- location: src/uix/eidos/components/command/command.css:260-261,272
|
||||
- evidence: inline-size: 1.125em (line 260-261) and font-size: 0.875em (line 272) are bare em values without canonical token references
|
||||
- impact: Icon and shortcut sizing use hard-coded em multipliers instead of --icon-size-* or --font-size-* tokens; inconsistent with theming system
|
||||
- proposed-fix: Replace 1.125em with a canonical icon size scale reference (e.g., --icon-size-md or create --command-item-icon-size). Replace 0.875em with a font-size scale reference or scale modifier
|
||||
- verify: [downgraded] Confirmed the literals: command.css:260-261 `inline-size: 1.125em; block-size: 1.125em;` (icon box) and 272 `font-size: 0.875em;` (shortcut). However em-based slot sizing is an established, idiomatic codebase pattern — grep across eidos shows field-control-trigger.css:30/45 `1.75em`/`1em`, menu-dial.css:281-282 `1em`, password-field.css:293/297-298 `1em`, table.css:339-340 `1em`, fab.css:103 `1em`. The `em` unit deliberately scales with the local `--_command-item-font-size` cascade (which IS token-driven), so these are RELATIVE sizing, not fixed px/rem drift the E-bis rule targets. At most a LOW consistency nit; MEDIUM overstates it given the framework-wide convention.
|
||||
- fix-status: open
|
||||
|
||||
### LOW: Magic literal — pixel heights — command-004 <!-- id: command-004 -->
|
||||
- dimension: E-bis
|
||||
- rule: Magic literal — pixel heights
|
||||
- location: src/uix/eidos/components/command/command.css:153,219,296
|
||||
- evidence: block-size: 2px (loading bar), gap: 1px (group items), block-size: 1px (separator) — bare pixel literals
|
||||
- impact: Three structural heights use magic pixels instead of token references; brittle to design changes
|
||||
- proposed-fix: Create or reference --border-width for 1px lines (already canonical for most components). For 2px loading bar, create --command-loading-height or use a custom CSS variable scoped to command
|
||||
- verify: [downgraded] Confirmed: command.css:153 `block-size: 2px;` (loading bar), 219 `gap: 1px;` (group items), 296 `block-size: 1px;` (separator). The 1px values are hairline-divider widths idiomatic across the codebase (separator/border lines); 2px is an off-scale loading-bar height. These are genuine bare-pixel literals but they are structural hairlines, not tokens that carry theming intent — LOW severity at most, not MEDIUM. No user-visible or behavioral impact.
|
||||
- fix-status: open
|
||||
|
||||
### LOW: Morfo contract — archetype declaration — command-006 <!-- id: command-006 -->
|
||||
- dimension: A
|
||||
- rule: Morfo contract — archetype declaration
|
||||
- location: src/uix/morfo/components/command.ts:98-124
|
||||
- evidence: Item part declares archetype: 'item' but does not declare Icon or Shortcut as sub-parts in the morfo
|
||||
- impact: Eidos defines Item.Icon and Item.Shortcut sub-parts (in eidos/components/command/index.ts) with their own data-* selectors, but they are not declared in the morfo. They are eidos-only sub-parts and properly documented in types.ts, so this is intentional (not an error)
|
||||
- proposed-fix: N/A — this is a documented eidos extension pattern
|
||||
- verify: [confirmed] Confirmed non-finding by the candidate's own admission. morfo declares Item (lines 97-124) with archetype 'item' and data-value/data-selected/data-disabled, but Icon/Shortcut are eidos-only sub-parts (index.ts:49-62 attaches ItemIcon/ItemShortcut; command.css:255/265 selectors documented as 'eidos-only' sub-part). This is the documented eidos-extension pattern, explicitly proposedFix 'N/A'. Correctly emitted as LOW with no action.
|
||||
- fix-status: open
|
||||
|
||||
### LOW: Test environment — jsdom only — command-007 <!-- id: command-007 -->
|
||||
- dimension: F
|
||||
- rule: Test environment — jsdom only
|
||||
- location: src/uix/soma/components/command/command-provider.svelte.test.ts:1
|
||||
- evidence: // @vitest-environment jsdom — test suite uses jsdom, which cannot test keyboard input, focus, or DOM pointer events accurately
|
||||
- impact: Keyboard navigation (ArrowDown/Up/Home/End/Enter/vim bindings) is complex behavioral code but only tested in jsdom (which doesn't fire real keyboard/mouse events). Focus logic untested.
|
||||
- proposed-fix: Add a client/Playwright test suite for keyboard navigation (arrow keys, grid columns, loop wrapping, Home/End), pointer selection, and focus sync. See SYS-3 pattern.
|
||||
- verify: [confirmed] Confirmed and arguably under-severed. test file line 1 `// @vitest-environment jsdom`; whole file is 122 lines with only 2 `it` blocks (line 77 filters+auto-select, line 96 navigates via provider.next/prev/selectCurrent API). No `dispatchEvent`/`KeyboardEvent`/`focus(` anywhere — the real onkeydown route in CommandInputProvider.onkeydown (lines 452-508: vim ctrl+n/p/j/k, Home/End, grid `columns`, RTL `getDirectionalKeys`) is NEVER exercised through a key event, only the underlying API methods are. This is a clean SYS-3 hit (interaction-heavy + jsdom-only + keyboard route untested). LOW is defensible but MEDIUM would be equally justified per the SYS-3 rubric.
|
||||
- fix-status: open
|
||||
|
||||
### LOW: Viewport structure assumption — command-008 <!-- id: command-008 -->
|
||||
- dimension: C
|
||||
- rule: Viewport structure assumption
|
||||
- location: src/uix/soma/components/command/command-provider.svelte.ts:598
|
||||
- evidence: const child = el.firstElementChild as HTMLElement | null; — assumes the viewport ref's first child is the measurable element
|
||||
- impact: If a consumer passes a custom child to Viewport (via the child snippet), firstElementChild may fail or measure the wrong element
|
||||
- proposed-fix: Add a fallback measurement: measure the viewport itself if firstElementChild is not present, or document this as a contract requirement (viewport must wrap content in a single element)
|
||||
- verify: [confirmed] Confirmed at LOW. provider line 598 `const child = el.firstElementChild as HTMLElement | null;` then observes it for resize. command-viewport.svelte renders `{@render children?.()}` with NO wrapper element, so firstElementChild measures whatever the consumer passes — fragile if the consumer renders text or multiple top-level nodes. Real robustness nit, but Viewport is `optional: true` in the morfo (line 93) and rarely used (List composes ScrollArea instead), and there is a `if (!child) return;` guard so it fails safe (no crash, just no height var). Correctly LOW; not behavioral/a11y.
|
||||
- fix-status: open
|
||||
|
||||
## No-findings dimensions
|
||||
B, D, G
|
||||
|
||||
## Theming facts (E-bis)
|
||||
- magic z-index: none
|
||||
- magic literals: command-003 (em sizes) | command-004 (px heights)
|
||||
- undeclared parts: none
|
||||
- roles clean: true · variants clean: true
|
||||
|
||||
## Tests (F)
|
||||
- exists: true · env: jsdom
|
||||
- covers: filter/visibility; navigation (basic); disabled items; group registration
|
||||
- untested: keyboard input (Arrow/Home/End/vim); pointer events; focus state; grid columns; loop wrapping edge cases; RTL keyboard inversion; two-moments ordering
|
||||
|
||||
## Style observations (non-blocking)
|
||||
- Command CSS is well-structured with good comments explaining concentric radius + shape-nest pattern
|
||||
- Dialog integration (lines 71-99) cleanly separates panel chrome from palette chrome
|
||||
- Input styling mirrors Select trigger (consistent archetype treatment)
|
||||
- Item sub-parts (Icon/Shortcut) leverage inherited flex layout smartly
|
||||
@ -0,0 +1,26 @@
|
||||
# Audit: container
|
||||
audit-version: 1
|
||||
audited-at: 2026-06-26
|
||||
scope: eidos (eidos-only primitive)
|
||||
method: theming-coherence category sweep (layout), adversarially verified; roles + `:Morfo` lead-verified CLEAN across all eidos (no success/warning/danger/info; only alert-dialog uses `:Morfo`).
|
||||
|
||||
## Summary
|
||||
Counts (post-verification): CRITICAL 0 · HIGH 0 · MEDIUM 0 · LOW 1.
|
||||
conformance: Contract OK, composes through Box, max-width set via recipe, padding via standard Box props.
|
||||
as-const: true · roles clean: true · variants clean: true · has-recipe: true
|
||||
|
||||
## Findings
|
||||
### LOW: MAGIC LITERAL 80rem in recipe fallback — container-001 <!-- id: container-001 -->
|
||||
- dimension: magic-literal
|
||||
- location: src/uix/eidos/lib/recipes/base.ts:4061
|
||||
- evidence: 'width-xl': 'var(--layout-container-width-xl, 80rem)'
|
||||
- impact: 80rem is a hard-coded container breakpoint, not sourced from canonical token in recipe.
|
||||
- proposed-fix: Low impact—foundation token --layout-container-width-xl exists; fallback is defensive but non-canonical.
|
||||
- verify: [downgraded] base.ts:4061 `'width-xl': 'var(--layout-container-width-xl, 80rem)'`. The 80rem is a defensive fallback for the LAYOUT foundation token --layout-container-width-xl (confirmed present at generated/base.css:3342), not an eidos --space-*/--radius- scale token. Recipe comment documents the xl cap. Defensive fallback, not canonical-scale drift.
|
||||
- fix-status: open
|
||||
|
||||
## Theming facts (E-bis)
|
||||
- magic literals: 80rem @ src/uix/eidos/lib/recipes/base.ts:4061 -> should reference --container-width-xl
|
||||
- magic z-index: none
|
||||
- non-canonical names: none
|
||||
- roles clean: true · variants clean: true
|
||||
@ -0,0 +1,69 @@
|
||||
# Audit: context-menu
|
||||
audit-version: 1
|
||||
audited-at: 2026-06-26
|
||||
scope: ['soma', 'sema', 'eidos']
|
||||
method: adversarially-verified workflow (analyze → refute); HIGH/CRITICAL personally re-verified against cited code by the lead.
|
||||
provider: src/uix/soma/components/context-menu/context-menu-provider.svelte.ts
|
||||
|
||||
## Summary
|
||||
Counts (post-verification): CRITICAL 0 · HIGH 0 · MEDIUM 3 · LOW 1.
|
||||
systemic hits: SYS-2 (magic-z and magic-literals); SYS-3 (jsdom-only keyboard testing).
|
||||
|
||||
## Findings
|
||||
### MEDIUM: SYS-2 — context-menu-002 <!-- id: context-menu-002 -->
|
||||
- dimension: E-bis
|
||||
- rule: SYS-2
|
||||
- location: src/uix/eidos/lib/recipes/base.ts:4303-4322
|
||||
- evidence: The context-menu recipe block (lines 4303-4322) does NOT declare 'content-z' token, unlike dropdown-menu which declares 'content-z': '80' at the equivalent location. Context-menu content floats with z-index: auto, risking paint-through by positioned page elements.
|
||||
- impact: Missing z-index token is a known systemic SYS-2 issue. Without explicit z-index, the portaled content panel may render behind elements with lower z-index that have explicit positioning (e.g. a toggle-group-item at z-index 1).
|
||||
- proposed-fix: Add 'content-z': '80' to the context-menu recipe block at base.ts:4304 (right after the opening brace), matching the dropdown-menu z-index for consistency since both are floating overlay menus.
|
||||
- verify: [confirmed] CONFIRMED real SYS-2 divergence. base.ts context-menu block (4303-4322) has NO 'content-z' token; the sibling dropdown-menu block declares `'content-z': '80'` (4279) with a comment: 'Without this the portaled panel inherits z-index: auto and any positioned page element with a positive z-index (e.g. a selected [data-toggle-group-item], z-index 1) paints THROUGH it.' context-menu.css panel rule (22-34) has NO `z-index` declaration, whereas dropdown-menu.css (34) sets `z-index: var(--dropdown-menu-content-z)` and notes 'The soma floating layer reads this computed z-index and mirrors it onto the positioner wrapper' (31-32). context-menu is an identically-portaled cursor-anchored overlay, so the same paint-through risk applies and is unmitigated. MEDIUM is correct.
|
||||
- fix-status: open
|
||||
|
||||
### MEDIUM: SYS-3 — context-menu-005 <!-- id: context-menu-005 -->
|
||||
- dimension: F
|
||||
- rule: SYS-3
|
||||
- location: src/uix/soma/components/context-menu/context-menu-provider.svelte.test.ts:1
|
||||
- evidence: Test file header: '@vitest-environment jsdom'. All keyboard tests (lines 191-281, 283-336) run in jsdom, which does not simulate real DOM focus or keyboard event routing like a browser environment.
|
||||
- impact: Keyboard routing (onkeydown handlers, arrow nav, Home/End, Escape) and focus-sync behavior (lines 334-371 in provider, lines 1021-1063 in SubContent) are tested in jsdom but should also be verified in a client environment (Playwright). Focus management edge cases (focus return on close, focus trap behavior) are HIGH-RISK for regressions.
|
||||
- proposed-fix: Add a client/Playwright test file covering: (1) focus enters at first item on open, (2) arrow keys navigate correctly with loop=true/false, (3) focus returns to trigger on Escape/outside-click close, (4) submenu focus scope is isolated (arrow-left exits submenu, arrow-right enters), (5) Home/End jump correctly when items are disabled.
|
||||
- verify: [confirmed] CONFIRMED SYS-3. Test header line 1 is `// @vitest-environment jsdom`. The test bodies (it() at 130/170/191/229/283/338) cover: open-from-contextmenu, getItems scoping, item activation via click + Space(216), checkbox/radio selection + Enter(275), submenu open via hover/click/ArrowRight(331), group/separator props. CRITICALLY UNTESTED: the Content onkeydown arrow-navigation index math (provider 334-371) — the loop modulo-vs-clamp logic at 348-355, Home/End at 356-361 — plus focus-return-to-trigger on close and Escape-close. The highest-risk keyboard route (directional index math) is exercised on zero paths, and runs jsdom-only with no client/Playwright companion. Matches the 'complex behavior tested only on its easy path (selection/open-close) while keyboard/focus untested' SYS-3 profile. MEDIUM correct.
|
||||
- fix-status: open
|
||||
|
||||
### MEDIUM: 2-of-3 — context-menu-006 <!-- id: context-menu-006 -->
|
||||
- dimension: A
|
||||
- rule: 2-of-3
|
||||
- location: src/uix/morfo/components/context-menu.ts:73-77 (radio-group part)
|
||||
- evidence: RadioGroup part (kebab: 'radio-group', archetype: 'group') declares role='group' and data/aria as empty arrays (lines 174-176). No data attributes are bound. Only soma registers it at line 675; sema does not select it; eidos does not consume it.
|
||||
- impact: RadioGroup part violates 2-of-3: used by soma (runtime.part registration) but not by sema or eidos. It is a structural grouping container with no perceptual or visual binding. This is acceptable if it is purely structural, but it should be marked optional or have a docstring clarifying its role.
|
||||
- proposed-fix: RadioGroup is intentionally structural-only (holding radio items that self-manage checked state via the RadioItem logic). This is a design choice, not a bug. Document in morfo: /* Structural container; individual RadioItems manage state and selection. Eidos/sema do not target this part directly. */
|
||||
- verify: [confirmed] Confirmed as the candidate itself frames it (confidence:low, proposedFix concedes 'This is a design choice, not a bug'). Morfo RadioGroup (168-177): role='group', empty data:[]/aria:[]. Provider registers it (675) for context publication (ContextMenuRadioGroupProvider.ctx) so RadioItems can read the group value. Grep of context-menu.css finds NO `data-context-menu-radio-group` selector (eidos uses `display: contents` only implicitly — actually no rule targets it at all), and the sema pack does not select it. So it is soma-only on the 2-of-3 axis, but legitimately: it is a pure structural/context container with no perceptual or visual surface, role='group' carries the a11y semantics declaratively, and RadioItems self-manage checked state. Acceptable structural exception, not a defect. LOW doc-nit at most.
|
||||
- fix-status: open
|
||||
|
||||
### LOW: SYS-2 — context-menu-004 <!-- id: context-menu-004 -->
|
||||
- dimension: E-bis
|
||||
- rule: SYS-2
|
||||
- location: src/uix/eidos/components/context-menu/context-menu.css:171
|
||||
- evidence: Trigger outline on data-state='open' hardcodes '1px dashed' border: 'outline: 1px dashed var(--color-border-default);'. Outline width and style are magic literals without token backing.
|
||||
- impact: The outline appearance cannot scale with theme or design system constraints. No way to override or scale the 1px width globally.
|
||||
- proposed-fix: Create recipe tokens 'trigger-focus-outline-width': '1px' and 'trigger-focus-outline-style': 'dashed', then reference them: 'outline: var(--context-menu-trigger-focus-outline-width, 1px) var(--context-menu-trigger-focus-outline-style, dashed) ...'.
|
||||
- verify: [downgraded] Partially valid kernel, over-stated. context-menu.css:171 `outline: 1px dashed var(--color-border-default)`. The bare `1px` IS a minor drift: every sibling component uses `var(--border-width)`/`var(--border-width-medium)` for border/outline widths (announce.css:29, combobox.css:96, dialog.css:28, listbox.css:41, etc.); this line is the only bare `1px` outline-width across the eidos components grep. So `1px`->`var(--border-width)` is a legitimate tokenization. BUT the candidate's proposed custom tokens for `dashed` over-reach — outline-style is a discrete keyword with no scale to reference. `--color-border-default` is a valid role token. Impact is minimal: this is a purely decorative courtesy outline on the otherwise-inert trigger when open (per the CSS comment 156-164). LOW magic-literal nit, not MEDIUM.
|
||||
- fix-status: open
|
||||
|
||||
## No-findings dimensions
|
||||
B, C, D, E, G
|
||||
|
||||
## Theming facts (E-bis)
|
||||
- magic z-index: context-menu-002: recipe missing 'content-z' token (SYS-2) | context-menu-004: trigger outline 1px hardcoded
|
||||
- magic literals: context-menu-003: sub-trigger arrow size 0.625rem, rotate 45deg (SYS-2) | context-menu-004: trigger outline 1px dashed (SYS-2) | src/uix/eidos/components/context-menu/context-menu.css:114: opacity 0.55 (but covered by token var(--context-menu-item-disabled-opacity)) | src/uix/eidos/components/context-menu/context-menu.css:130: opacity var(--opacity-muted) - correct
|
||||
- undeclared parts: context-menu-001: 'sub' part declared in morfo but never registered via runtime.part() in provider
|
||||
- roles clean: true · variants clean: true
|
||||
|
||||
## Tests (F)
|
||||
- exists: true · env: jsdom only (vitest-environment jsdom at line 1)
|
||||
- covers: contextmenu trigger event and anchor point capture (lines 130-168); item scoping to current content, excluding nested submenus (lines 170-189); item activation by click and keyboard (lines 191-227); checkbox and radio state toggling (lines 229-281); submenu open via pointer hover with 100ms delay (lines 283-336); group heading and separator accessibility attributes (lines 338-365)
|
||||
- untested: Arrow key navigation in content (loops, clamping when loop=false); Home/End key jump to first/last item; Escape key close menu and focus return to trigger; Tab key escape from menu (trap=false expected); Typeahead character matching; Submenu arrow-key navigation (left/right to close/open submenu); Focus sync when items are disabled (skip to next focusable); SafePolygon hover exit behavior; Dismissal on outside-click and interact-outside events; Focus trap behavior (when trapFocus=true)
|
||||
|
||||
## Style observations (non-blocking)
|
||||
- Context-menu recipe tokens closely mirror dropdown-menu except for missing 'content-z' and arrow size tokens, which suggests the component was copied but not fully reconciled for floating layer z-index and icon sizing patterns.
|
||||
- The trigger outline affordance (dashed outline on open) is a nice UX cue but lacks a way to disable or customize the width/style globally.
|
||||
@ -0,0 +1,62 @@
|
||||
# Audit: cropper
|
||||
audit-version: 1
|
||||
audited-at: 2026-06-26
|
||||
scope: (SCOPE-DRIFT → SYS-1)
|
||||
method: adversarially-verified workflow; HIGH lead-verified. B7 ground-truth: Toolbar A35 + Form A36 incidents STAYED FIXED (untrack present); toast/clipboard/drag-drop use uix.timers (no A6 leak); button is clean (its archetype finding was inert — Spinner not runtime-registered); data-size/data-shape are eidos visual attrs (not contract violations).
|
||||
provider: src/uix/soma/components/cropper/cropper-provider.svelte.ts
|
||||
cleanup-audit (A6/A35/A36): Timers: soma.uix.timers.schedule(null, 120, ...) at line 302 — disposed by runtime ✓. Listeners: All pointer events (onpointerdown/move/up in Viewport/Selection/Handle) use setPointerCapture + releasePointerCapture in same handler; no separate cleanup needed. Image load handler in eidos cropper.svel
|
||||
|
||||
## Summary
|
||||
Counts (post-verification): CRITICAL 0 · HIGH 0 · MEDIUM 2 · LOW 1.
|
||||
|
||||
## Findings
|
||||
### MEDIUM: SYS-1: Scope-drift — eidos recipe directory exists but morfo scope omits 'eidos' — cropper-001 <!-- id: cropper-001 -->
|
||||
- dimension: A
|
||||
- rule: SYS-1: Scope-drift — eidos recipe directory exists but morfo scope omits 'eidos'
|
||||
- location: src/uix/morfo/components/cropper.ts:20
|
||||
- evidence: Morfo declares scope: ['soma', 'sema'] but src/uix/eidos/components/cropper/ exists with CSS and SVG component
|
||||
- impact: Scope mismatch signals incomplete contract declaration; framework tooling may not validate eidos layer against morfo
|
||||
- proposed-fix: Update morfo scope to ['soma', 'sema', 'eidos']
|
||||
- verify: [confirmed] Confirmed SYS-1 scope-drift. morfo cropper.ts:20 `scope: ['soma', 'sema']` omits 'eidos', yet the eidos layer exists: dir src/uix/eidos/components/cropper/ (cropper.css, cropper.svelte, types.ts) AND a recipe `cropper:` at src/uix/eidos/lib/recipes/base.ts:1152. Matches the established systemic baseline (sibling image-picker.ts:20 and image-adjustments.ts:18 carry the same omission). MEDIUM stands.
|
||||
- fix-status: open
|
||||
|
||||
### MEDIUM: SYS-3: Missing test coverage for high-risk paths (gesture, async, timers) — cropper-003 <!-- id: cropper-003 -->
|
||||
- dimension: F
|
||||
- rule: SYS-3: Missing test coverage for high-risk paths (gesture, async, timers)
|
||||
- location: src/uix/soma/components/cropper/cropper-provider.svelte.test.ts
|
||||
- evidence: Tests cover geometry (moveRect, resizeRect) and ZoomPan, but no coverage for: gesture handlers (onpointerdown/move/up in Viewport/Selection/Handle), cropImage() async function, zoom throttle timer, or fixed-size $effect. Test env is jsdom.
|
||||
- impact: Critical business logic (drag/resize/zoom interactions, canvas extraction, timer cleanup) is untested and may regress without detection
|
||||
- proposed-fix: Add tests for: (1) pointer gesture sequences (down→move→up), (2) zoom throttle behavior, (3) cropImage() Blob production with shape masking, (4) fixed-size centering effect
|
||||
- verify: [confirmed] Confirmed SYS-3 / dim F. Test file (cropper-provider.svelte.test.ts, `@vitest-environment jsdom` line 1) covers pure geometry thoroughly (moveRect, resizeRect incl. aspect/min/max/edge corners, lines 58-137), ZoomPan (139-181), and provider setCrop/disabled/snippetProps (189-223). UNTESTED high-risk paths: (1) pointer gesture sequences (down→move→up in Viewport/Selection/Handle providers), (2) `cropImage()` async canvas→Blob extraction incl. round-shape masking (provider lines 237-290), (3) zoom-throttle timer + emitZoom gate (294-305), (4) fixed-size centering $effect (183-205). MEDIUM stands — geometry is well-covered so this is a partial, not total, gap.
|
||||
- fix-status: open
|
||||
|
||||
### LOW: A6: Event listener / handler not disposed in $effect return — cropper-002 <!-- id: cropper-002 -->
|
||||
- dimension: B, A6
|
||||
- rule: A6: Event listener / handler not disposed in $effect return
|
||||
- location: src/uix/eidos/components/cropper/cropper.svelte:44-58
|
||||
- evidence: $effect creates new Image element and assigns img.onload handler (line 52) but never cleans up. On src change, new Image created; old handler may fire if response arrives after effect re-run.
|
||||
- impact: Old onload handlers can fire after component re-runs the effect, causing stale handler invocations and potential memory pressure from accumulated handlers
|
||||
- proposed-fix: Return a cleanup function from the $effect that clears the handler, or store img in state and reuse it with cleanup: `return () => { img.onload = null; img.src = ''; }`
|
||||
- verify: [downgraded] Downgraded from MEDIUM/A6 to LOW. The $effect at cropper.svelte:44-58 creates a local `const img = new ImageCtor()` (line 51), sets one-shot `img.onload` (line 52), `img.src = s` (line 57), with no cleanup return. This is NOT an A6 resource leak: the Image is never inserted into the DOM, the handler is one-shot, and the element is GC-eligible once the local goes out of scope — there is no persistent listener/observer/timer to dispose. The genuine (minor) defect is a stale-async-write race: if `src` changes and an OLD image's load resolves AFTER the new one, the old onload writes `aspectRatio` (line 54) to a stale ratio. It is NOT an A35/A36 loop — the effect reads `src`, not `aspectRatio`, so no tracked read-back, no freeze. Impact: transient wrong aspect-ratio on rapid src swaps. The provider's `loadImage` (cropper-provider.svelte.ts:96-106) avoids this by awaiting a Promise with no reactive write. LOW.
|
||||
- fix-status: open
|
||||
|
||||
## No-findings dimensions
|
||||
C, D, E, E-bis, G
|
||||
|
||||
## Theming facts (E-bis)
|
||||
- magic z-index: none
|
||||
- magic literals: 9999px in box-shadow (line 52 cropper.css) — documented mask pattern
|
||||
- undeclared parts: none
|
||||
- roles clean: true · variants clean: true
|
||||
|
||||
## Tests (F)
|
||||
- exists: true · env: jsdom
|
||||
- covers: moveRect geometry; resizeRect with aspect/minSize/maxSize; ZoomPan scale clamping and pan bounds; ZoomPan fraction mapping; setCrop updates state and fires callback; disabled blocks setCrop; snippetProps exposes shape/dragging
|
||||
- untested: Gesture handlers (onpointerdown/move/up); cropImage() Blob canvas extraction; Zoom throttle timer behavior; Fixed-size $effect re-centering; Shape masking (round crop); Image load aspect-ratio tracking; commit-crop event trigger on canvas complete
|
||||
|
||||
## Style observations (non-blocking)
|
||||
- Zoom buttons correctly use --focus-ring-width + --focus-ring-color tokens (canonical two-ring model)
|
||||
- Recipe tokens all reference theme vars or semantic space/radius/opacity
|
||||
- Box-shadow 9999px mask is documented pattern (not magic literal violation)
|
||||
- Cursor values (move, grab, grabbing, resize variants) are semantic browser cursors
|
||||
- :active pseudo-class on img is native browser feedback for pressed state (acceptable)
|
||||
@ -0,0 +1,41 @@
|
||||
# Audit: css-field
|
||||
audit-version: 1
|
||||
audited-at: 2026-06-26
|
||||
scope: (SCOPE-DRIFT → SYS-1)
|
||||
method: adversarially-verified workflow (analyze → refute); HIGH/CRITICAL lead-verified. Clean-check pseudo-findings dropped. B4 ground-truth: segmented date/time/color-field register inputId via $effect (SYS-A30-EFFECT, no loop); number-field is the direct-assignment reference.
|
||||
provider: G:\dev\svelte\vicen\src\uix\soma\components\css-field\css-field-provider.svelte.ts
|
||||
|
||||
## Summary
|
||||
Counts (post-verification): CRITICAL 0 · HIGH 0 · MEDIUM 1 · LOW 0.
|
||||
|
||||
## Findings
|
||||
### MEDIUM: SCOPE-DRIFT: eidos recipe dir exists but morfo 'scope' omits 'eidos' — css-field-001 <!-- id: css-field-001 -->
|
||||
- dimension: Frontier, SYS-1
|
||||
- rule: SCOPE-DRIFT: eidos recipe dir exists but morfo 'scope' omits 'eidos'
|
||||
- location: G:\dev\svelte\vicen\src\uix\morfo\components\css-field.ts:7
|
||||
- evidence: scope: ['soma', 'sema'], — eidos/components/css-field/ exists with css-field.svelte, css-field-input.svelte, etc., but scope array does not include 'eidos'
|
||||
- impact: Contract validator cannot check that eidos wrappers only import from morfo PARTS, risking cross-layer coupling over time as the component evolves
|
||||
- proposed-fix: Add 'eidos' to morfo scope: scope: ['soma', 'sema', 'eidos']
|
||||
- verify: [confirmed] Confirmed as a SYS-1 scope-drift instance. `css-field.ts:7` = `scope: ['soma', 'sema']` while an eidos dir exists with a wrapper + css (`eidos/components/css-field/css-field.svelte` imports `./css-field.css`, plus `types.ts`/`index.ts`). MEDIUM is correct per the SYS-1 catalog. Caveat for the orchestrator: this is SYSTEMIC across the whole field family, not a css-field anomaly — the project's REFERENCE baseline NumberField is identical (`number-field.ts:7` = `scope: ['soma', 'sema']` with its own eidos dir), as are time-field/color-field/date-field (all `scope: ['soma', 'sema']`). Note also css-field has no dedicated recipe: its eidos css (`css-field.css`) declares 'CssField has no visual of its own: it IS a spin-field' and styling comes from the shared `spin-field` recipe (`base.ts:1016`) via structural identity. The scope-validator gap still stands regardless.
|
||||
- fix-status: open
|
||||
|
||||
## No-findings dimensions
|
||||
Contract, DOM-selector, TSC, Tests, Redundancy, Behavior (keyboard), Behavior (A30), Behavior (A6), Behavior (TWO-MOMENTS)
|
||||
|
||||
## Theming facts (E-bis)
|
||||
- magic z-index: none
|
||||
- magic literals: 400 | 100 | 5
|
||||
- undeclared parts: none
|
||||
- roles clean: true · variants clean: true
|
||||
- label-font (one step below input?): COMPLIANT: Field label font-size is derived as calc(var(--_field-control-font-size) - (var(--font-size-md) - var(--font-size-sm))) per field.css, which is one typographic step below the input. CssField uses shared spin-field visual, which inherits --_field-control-font-size from the recipe (e.g. field-control-font-size-md: var(--font-size-md) = 16px), making the label 14px (one step down). ✓
|
||||
|
||||
## Tests (F)
|
||||
- exists: true · env: jsdom
|
||||
- covers: commit on blur; increment/decrement with per-unit steps; bare number default unit; disallowed unit rejection (untilFix); keyword acceptance; clamping to min/max; sium schema delegation; scrubber scrubbing with unit preservation; spinbutton aria props
|
||||
- untested: readonly-without-value warning; onbeforeinput paste/IME rejection; clearTarget(input) actually clears the warn signal; RTL scrubber mirroring; A30 inputId registration with parent Field
|
||||
|
||||
## Style observations (non-blocking)
|
||||
- Default step-by-unit logic (rem/em: 0.1, else 1) is sensible and matches the UI pattern for CSS editing
|
||||
- Scrubber sensitivity (5px per step default) is well-tuned for precise CSS values
|
||||
- Bare-number regex parsing (line 39) handles leading dot correctly
|
||||
- Comments throughout provider are clear and precise (Book §6.2 reference, untilFix signal flow)
|
||||
@ -0,0 +1,62 @@
|
||||
# Audit: date-field
|
||||
audit-version: 1
|
||||
audited-at: 2026-06-26
|
||||
scope: ['soma', 'sema'] (SCOPE-DRIFT → SYS-1)
|
||||
method: adversarially-verified workflow (analyze → refute); HIGH/CRITICAL lead-verified. Clean-check pseudo-findings dropped. B4 ground-truth: segmented date/time/color-field register inputId via $effect (SYS-A30-EFFECT, no loop); number-field is the direct-assignment reference.
|
||||
provider: G:\dev\svelte\vicen\src\uix\soma\components\date-field\date-field-provider.svelte.ts
|
||||
field-family (A13/A24-26/A30): A13: form-participating hidden input correctly implemented (lines 1011-1040), renders with name/value/required/disabled when name is set. A24: readonly-without-value warning implemented via soma.logger.warn (lines 355-370), guards against misconfiguration when readonlySegments set without value. A26: onbeforeinput preventDefault required for segmented contenteditable — CORRECT implementation at li
|
||||
|
||||
## Summary
|
||||
Counts (post-verification): CRITICAL 0 · HIGH 0 · MEDIUM 2 · LOW 0.
|
||||
|
||||
## Findings
|
||||
### MEDIUM: SYS-1 (scope-drift) — date-field-001 <!-- id: date-field-001 -->
|
||||
- dimension: scope
|
||||
- rule: SYS-1 (scope-drift)
|
||||
- location: morfo line 7 + eidos dir exists
|
||||
- evidence: Morfo declares scope: ['soma', 'sema'] (src/uix/morfo/components/date-field.ts line 7) but eidos recipe dir src/uix/eidos/components/date-field/ and CSS src/uix/eidos/components/date-field/date-field.css both exist. Scope MUST include 'eidos' when the recipe and CSS layer are present.
|
||||
- impact: Contract validator may not catch visual-layer rules as part of the component's declared surface area.
|
||||
- proposed-fix: Update morfo scope from ['soma', 'sema'] to ['soma', 'sema', 'eidos']
|
||||
- verify: [confirmed] CONFIRMED at MEDIUM. morfo `src/uix/morfo/components/date-field.ts:7` declares `scope: ['soma', 'sema']` (grep verified), while the eidos layer exists: recipe block `'date-field':` at `src/uix/eidos/lib/recipes/base.ts:1172` and CSS `src/uix/eidos/components/date-field/date-field.css` (read in full, 228 lines). This matches the documented systemic SYS-1 scope-drift (MEDIUM). NOTE: the project's stated REFERENCE component NumberField has the IDENTICAL condition — number-field.ts:7 also declares `scope: ['soma', 'sema']` yet `src/uix/eidos/components/number-field/` exists — so this is a true systemic pattern for the field family, not a date-field-specific defect. Severity stays MEDIUM per SYS-1; confidence high that the condition holds, with the caveat that whether 'eidos' is REQUIRED in scope for these components is a project-wide convention question.
|
||||
- fix-status: open
|
||||
|
||||
### MEDIUM: Test coverage gap (field-family paths) — date-field-003 <!-- id: date-field-003 -->
|
||||
- dimension: tests
|
||||
- rule: Test coverage gap (field-family paths)
|
||||
- location: date-field-provider.svelte.test.ts (275 lines)
|
||||
- evidence: Test suite covers: segment helpers (120-165), validation (174-198), segment commit (200-220), time-segment sync (222-240), granularity filtering (242-274). MISSING: (1) hidden-input render when name is set; (2) readonly-without-value warning fired; (3) commit-set/commit-reset event emission; (4) keyboard navigation per APG (ArrowUp/Down/Left/Right/Backspace); (5) Field parent integration (inputId wiring).
|
||||
- impact: High-risk field-family paths untested: form submission via hidden input, readonly misconfiguration guards, and event-driven workflows.
|
||||
- proposed-fix: Add test cases for: shouldRender hidden input, soma.logger.warn call when readonlySegments + undefined value, commit event firing on segment completion, keyboard segment navigation (at least one key per segment type), and Field parent inputId registration.
|
||||
- verify: [confirmed] CONFIRMED at MEDIUM (F dimension — high-risk field-family test gap). The test file `date-field-provider.svelte.test.ts` (read in full, 275 lines) covers ONLY: segment helpers (initSegmentStates/isAcceptableSegmentKey/getFirstSegment/handleSegmentNavigation, 120-165), validation min/max/custom (174-198), incremental segment commit (200-220), padded date+time sync (222-240), granularity filtering (242-274). UNTESTED high-risk paths that DO exist in the provider: (1) hidden-input render — `DateFieldHiddenInputProvider.shouldRender` (provider line 1022) and its A13 ISO `value`/`name`/`required` props (1027-1040) are never exercised; (2) readonly-without-value A24 warn — the `$effect` calling `soma.logger.warn` (provider 354-369) is never triggered, even though the harness already stubs `logger.warn: vi.fn()` (test line 72), so the assertion would be trivial; (3) commit event emission — `runtime.trigger('commit-set'|'commit-reset')` (provider line 785) untested; (4) keyboard segment navigation per APG (ArrowUp/Down/Backspace/number-key handlers, provider 1108-1296) untested at provider level; (5) Field parent inputId wiring (DateFieldInputProvider $effect, provider 932-936) untested. Severity MEDIUM (missing tests for high-risk paths); confidence high that these paths are real and uncovered.
|
||||
- fix-status: open
|
||||
|
||||
### MEDIUM: A30 — inputId registered via `$effect` instead of direct constructor assignment (SYSTEMIC: date/time/color-field) <!-- id: date-field-A30 -->
|
||||
- dimension: B
|
||||
- rule: A30 (child→parent id registration MUST be a direct constructor assignment, not `$effect`)
|
||||
- location: date-field-provider.svelte.ts:932-936 (`$effect(() => { const field = this.provider.field; if (!field) return; field.inputId.current = opts.id.current; })`)
|
||||
- evidence: lead-confirmed by direct read — the DateFieldInput provider wraps the `field.inputId` registration in a `$effect`, whereas the project reference NumberField does it as a direct constructor guard (number-field-provider.svelte.ts:585-587: `if (this.provider.field) { this.provider.field.inputId.current = opts.id.current; }`). time-field (531-535) and color-field (521-525) use the identical `$effect` — SYSTEMIC.
|
||||
- impact: no loop today (the Input provider only WRITES `field.inputId` and never reads it back, and `opts.id` is a stable framework id), so the A30 page-freeze hazard is not live — but it is contrary to A30 doctrine and inconsistent with the NumberField reference; a future reader who makes the registration two-way (or reads inputId in the child) would reintroduce the freeze.
|
||||
- repro: static — compare date/time/color-field Input providers (`$effect`) vs number-field (direct).
|
||||
- proposed-fix: replace the `$effect` with a direct constructor assignment guarded by `if (this.provider.field)`, matching NumberField. Apply across date-field, time-field, color-field.
|
||||
- verify: [lead-added] the date-field agent's no-findings claim falsely listed "A30 direct id-wiring" as clean; the verify on time-field-001 + lead reads of date-field:932-936 / color-field:521-525 confirm the `$effect` pattern. Elevated to a systemic MEDIUM.
|
||||
- fix-status: open
|
||||
|
||||
## No-findings dimensions
|
||||
A (Contract morfo), B (keyboard, A17 focus, A33, A35, A6 cleanup, TWO-MOMENTS — note: A30 id-wiring is NOT clean, see date-field-A30), C (DOM-selector safety), D (Frontier soma→eidos), G (Redundancy)
|
||||
|
||||
## Theming facts (E-bis)
|
||||
- magic z-index: none
|
||||
- magic literals: 'day' | 'month' | 'year' | 'hour' | 'minute' | 'second' | 'dayPeriod' | 'literal' | 'timeZoneName'
|
||||
- undeclared parts: none
|
||||
- roles clean: true · variants clean: true
|
||||
- label-font (one step below input?): Label calc(1em - (var(--font-size-md) - var(--font-size-sm))) produces correct one-step scaling at md but degrades at sm input size (line 109) — see finding date-field-002.
|
||||
|
||||
## Tests (F)
|
||||
- exists: true · env: jsdom (vitest)
|
||||
- covers: segment state initialization; input acceptance (numeric + nav keys only); DOM segment reading (getValueFromSegmentsDOM); segment focus navigation (ActiveDom); validation (custom/min/max); segment commit on fill; time segment padding; granularity filtering (hour → no minute/second)
|
||||
- untested: hidden-input conditional render when name is set; readonly-without-value warning logger.warn() call; commit-set and commit-reset events firing; keyboard handling per APG (all arrow directions, backspace per segment); Field parent integration (inputId registration); readonly segment visual treatment; cross-segment cascades (month→day clamp, hour→dayPeriod flip, dayPeriod→hour AM/PM); year segment typeahead logic (pressedKeys tracking)
|
||||
|
||||
## Style observations (non-blocking)
|
||||
- Recipe tokens correctly reference --space-*, --radius-*, --font-size-*, --color-* (lines 1173-1222). No bare hex/rgb/oklch in recipe. No --opacity-* barefoot decimals (opacity-disabled token used, line 1222). Z-index not used in date-field CSS. Theming uses 9 roles (primary/secondary/neutral/affirm/fulfill/risk/threat/loss + implicit default). Label font-weight is var(--font-weight-regular), not emphasized. Segment readonly uses underline dotted var(--color-content-muted) + surface-overlay bg (consistent field family).
|
||||
- Invalid border color uses var(--color-risk-border) per theming rule (line 1204).
|
||||
- Focus ring uses outline (not box-shadow post migration, line 147 CSS). Flush offset (0) survives forced-colors.
|
||||
@ -0,0 +1,69 @@
|
||||
# Audit: date-picker
|
||||
audit-version: 1
|
||||
audited-at: 2026-06-26
|
||||
scope: (SCOPE-DRIFT → SYS-1)
|
||||
method: adversarially-verified workflow (analyze → refute); HIGH/CRITICAL personally re-verified by the lead. Batch-3 ground-truth: each picker fires trigger(close) (close NOT inert), but open/commit-reset ARE inert; calendar/range-calendar are MID-REFACTOR (uncommitted view-switch work).
|
||||
provider: src/uix/soma/components/date-picker/date-picker-provider.svelte.ts
|
||||
|
||||
## Summary
|
||||
Counts (post-verification): CRITICAL 0 · HIGH 0 · MEDIUM 4 · LOW 0.
|
||||
systemic hits: SYS-1 (scope-drift: morfo scope ['soma', 'sema'] but eidos dir exists).
|
||||
composition (A27): COMPOSES: DatePicker correctly composes via shared writableActive refs. date-picker.svelte creates sharedOpen, sharedValue, sharedPlaceholder, and passes them to PopoverProvider.create() and DateFieldProvider.create() (lines 167-194). Trigger wraps PopoverTriggerProvider. Calendar wraps CalendarProvider with filtered isDateDisabled to enforce readonlySegments coherence. No re-implementation detected; all composed parts (Popover, DateField, Calendar) are referenced via shared refs or forwarded exports.",
|
||||
<parameter name="styleObservations">["data-kind is stamped on the Provider element (line 197) to drive visual view switching; this is morfo-declared and visually clean. CSS uses cascading :has() selectors (line 138-145) for responsive popover width overrides when hosting calendar/grid, avoiding hardcoded breakpoints.", "Disabled state uses opacity var(--date-field-disabled-opacity) (line 103), consistent with field theming. Role/aria attrs are delegated to composed Calendar part (dialog role, aria-label) and not duplicated in Provider.", "No bare literals for spacing, sizing, z-index, or opacity; all theme tokens properly referenced."]
|
||||
|
||||
## Findings
|
||||
### MEDIUM: SYS-1 scope-drift: an eidos recipe dir exists but morfo 'scope' omits 'eidos' — date-picker-001 <!-- id: date-picker-001 -->
|
||||
- dimension: A: Contract (morfo)
|
||||
- rule: SYS-1 scope-drift: an eidos recipe dir exists but morfo 'scope' omits 'eidos'
|
||||
- location: src/uix/morfo/components/date-picker.ts:13
|
||||
- evidence: scope: ['soma', 'sema'], but src/uix/eidos/components/date-picker/ directory exists with date-picker.css, *.svelte files, and types.ts
|
||||
- impact: Morfo contract mismatches the actual layer composition. Eidos recipes exist but scope doesn't declare them, risking validator confusion and runtime assumptions about which layers are active.
|
||||
- proposed-fix: Change morfo scope to ['soma', 'sema', 'eidos'] to match the actual directory structure and eidos recipe presence.
|
||||
- verify: [confirmed] Confirmed as SYS-1 scope-drift. date-picker.ts:13 reads `scope: ['soma', 'sema'],` yet `src/uix/eidos/components/date-picker/` exists with date-picker.css (6115 bytes), date-picker.svelte, types.ts, and several *.svelte wrappers. This is systemic across the whole picker family: color-picker, date-range-picker, time-picker, time-range-picker, calendar all read `scope: ['soma', 'sema']` while their eidos dirs exist; only range-calendar declares eidos. MEDIUM is the correct severity per the SYS-1 baseline — it is a contract/metadata mismatch, not a runtime defect (createAttrs/morfo are unaffected since the morfo is `as const satisfies Morfo` at line 230).
|
||||
- fix-status: open
|
||||
|
||||
### MEDIUM: INERT EVENTS: morfo declares 'open' event but provider never fires it via runtime.trigger( — date-picker-002 <!-- id: date-picker-002 -->
|
||||
- dimension: A: Contract (morfo), B: Behavior (soma)
|
||||
- rule: INERT EVENTS: morfo declares 'open' event but provider never fires it via runtime.trigger()
|
||||
- location: src/uix/morfo/components/date-picker.ts:22-40 (event declaration) vs src/uix/soma/components/date-picker/date-picker-provider.svelte.ts (no 'open' trigger)
|
||||
- evidence: Morfo declares: {name: 'open', semantic: {family: 'emerge', verb: 'open', target: v.partRef('calendar'), sequence: 'pre', ...}}. Provider only fires 'close' via triggerClose() at lines 222, 232, 268; no runtime.trigger('open', ...) call exists.
|
||||
- impact: The 'open' event exists only to satisfy schema validation. Consumers cannot listen to it because the provider never emits it. This is inconsistent with the morfo contract which signals the event should fire.
|
||||
- proposed-fix: Either (a) fire runtime.trigger('open', ...) when the popover opens (handleDateSelect path or in a watch on opts.open), or (b) remove the 'open' event declaration from morfo if it is not needed per the design.
|
||||
- verify: [confirmed] Confirmed but DOWNGRADE-CAPPED to MEDIUM (it was already MEDIUM). The `open` event (morfo lines 23-41) is genuinely inert: grep for `runtime.trigger` in the provider returns ONLY line 268 `runtime.trigger('close', ...)` (fired from triggerClose at 210/222/232). There is no `runtime.trigger('open', ...)` anywhere. NOTE the candidate's evidence is partly stale — it claimed line numbers 222/232/268 fire 'close', which is correct, and it correctly identified `open` as never fired. The morfo's own comment (lines 42-49) explicitly anticipates this: 'Today's date-picker provider just toggles opts.open; if it ever explicitly calls runtime.trigger(close...)'. Matches the documented picker-family inert-events pattern. MEDIUM is the ceiling per baseline.
|
||||
- fix-status: open
|
||||
|
||||
### MEDIUM: Test coverage is jsdom-only and does not exercise high-risk paths: keyboard navigation, fo — date-picker-003 <!-- id: date-picker-003 -->
|
||||
- dimension: F: Tests
|
||||
- rule: Test coverage is jsdom-only and does not exercise high-risk paths: keyboard navigation, focus management, modal vs inline mode dismissal, date validation, cancel/commit/clear actions.
|
||||
- location: src/uix/soma/components/date-picker/date-picker-provider.svelte.test.ts:1 (@vitest-environment jsdom) and lines 111-174
|
||||
- evidence: Tests are marked jsdom-only (@vitest-environment jsdom). The test suite only covers: (1) part registration, (2) state flags, (3) closeOnDateSelect flag. Missing: keyboard Escape/Tab/Enter/Space, grid navigation (arrow keys), focus trap, modal mode behavior, cancel() reverting value, commit() triggering close event, clear() resetting to undefined, two-moments sequence (data-last-action before data-state).
|
||||
- impact: Risk of undiscovered bugs in interactions-heavy paths (dismissal, modal locking, date-specific validation edge cases) that jsdom doesn't simulate. Keyboard routes in calendar grids, focus roving, and dismissal semantics are untested.
|
||||
- proposed-fix: Add browser-level (Playwright/client) tests covering: (a) Escape closes popover + triggers 'close' event, (b) Tab focus-traps in modal mode, (c) modal mode prevents outside-click dismissal, (d) cancel() reverts value to snapshot, (e) commit() and clear() fire correct close cause, (f) data-last-action writes before data-state flips.
|
||||
- verify: [confirmed] Confirmed as SYS-3 jsdom-only / kbd-untested. Test file line 1 is `// @vitest-environment jsdom`. The suite (lines 111-174) has exactly 3 `it()` blocks: provider part registration, root state flags, and the closeOnDateSelect toggle via handleDateSelect. No coverage of: Escape/Tab/Enter/Space keyboard, calendar grid arrow navigation, focus trap (morfo declares focus.trap:true, focus.return:'trigger'), cancel() value revert, commit()/clear() close-cause, or the two-moments data-last-action-before-data-state ordering in triggerClose (266-268). These are the high-risk interaction paths. MEDIUM is correct (not HIGH) — it is a missing-coverage observation on interaction-heavy paths, consistent with the SYS-3 baseline severity.
|
||||
- fix-status: open
|
||||
|
||||
### MEDIUM: INERT EVENTS: morfo declares 'commit-reset' event but provider never fires it via runtime. — date-picker-N01 <!-- id: date-picker-N01 -->
|
||||
- dimension: A: Contract (morfo), B: Behavior (soma)
|
||||
- rule: INERT EVENTS: morfo declares 'commit-reset' event but provider never fires it via runtime.trigger()
|
||||
- location: src/uix/morfo/components/date-picker.ts:66-76 (commit-reset declaration) vs src/uix/soma/components/date-picker/date-picker-provider.svelte.ts:240-242 (clear())
|
||||
- evidence: Morfo declares a third event `{ name: 'commit-reset', semantic: { family: 'commit', verb: 'reset', target: v.partRef('calendar'), sequence: 'post', intent: 'neutral' } }` whose intent (per its comment line 67) is 'User cleared the date (explicit clear button or backspace).' The provider's `clear()` method does exactly that — `this.opts.value.current = undefined;` — but does NOT call `runtime.trigger('commit-reset', ...)`. Grep of the provider for runtime.trigger returns only the 'close' call at line 268. So commit-reset is a second inert event the candidate auditor missed (it only flagged 'open').
|
||||
- impact: A 'commit' family event with a real perceptual intent (clearing the value) never reaches the sema engine — clearing the date is perceptually silent even though the morfo says it should emit. Consumers wiring telemetry/sound to commit-reset receive nothing. Same INERT-EVENT category as the 'open' finding; MEDIUM ceiling per the picker-family baseline.
|
||||
- repro: Open date-picker, select a date, press the Clear footer action: opts.value resets to undefined but no commit-reset signal is emitted to sema.
|
||||
- proposed-fix: In clear() fire `runtime.trigger('commit-reset', { fallbackTarget: this.runtime.partRef('calendar') ?? undefined })` after setting value undefined (sequence:'post' is already declared, so the state write precedes the emit), OR remove the commit-reset event from the morfo if a silent clear is intended.
|
||||
- verify: [verifier-added] added by adversarial verify pass
|
||||
- fix-status: open
|
||||
|
||||
## No-findings dimensions
|
||||
B: Behavior (soma) - keyboard/grid routes, C: DOM-selector (CSS.escape), D: Frontier (eidos<->soma cross-layer imports), E: TSC (eidos css), E-bis: Theming (roles/tokens), G: Redundancy (composition correctly avoids re-implementation)
|
||||
|
||||
## Theming facts (E-bis)
|
||||
- magic z-index: none
|
||||
- magic literals: 'data-kind' | 'date-picker' | 'field-trigger'
|
||||
- undeclared parts: none
|
||||
- roles clean: true · variants clean: true
|
||||
- conformance: CLEAN: data-* attrs follow [data-{component}] and [data-{component}-{part}] naming; eidos recipe imports only composed component CSS (date-field, calendar, month-grid, year-grid), not isolated primitives; role attributes and aria-* attrs match morfo declarations in Provider/Trigger/Calendar parts; no double-write issues detected (syncAttrs not used); token references are canonical (--color-{role}-*, --space-*, --date-field-*)
|
||||
|
||||
## Tests (F)
|
||||
- exists: true · env: jsdom
|
||||
- covers: provider part registration; state flags (data-state, data-disabled/readonly/required/invalid); closeOnDateSelect flag toggle
|
||||
- untested: keyboard routes (Escape, Tab, Enter, Space, arrow keys for grid nav); focus trap and focus restoration; modal mode (prevents outside-click, requires explicit commit/cancel); dismissal cause tracking (data-last-action write order); cancel() value revert snapshot; commit() and clear() event emission; date validation edge cases (minValue, maxValue, custom validate); two-moments: data-last-action write before data-state flip; locale/dir resolution; readonly segment filtering in calendar; readonly segment coherence with navigation
|
||||
@ -0,0 +1,47 @@
|
||||
# Audit: date-range-picker
|
||||
audit-version: 1
|
||||
audited-at: 2026-06-26
|
||||
scope: (SCOPE-DRIFT → SYS-1)
|
||||
method: adversarially-verified workflow (analyze → refute); HIGH/CRITICAL personally re-verified by the lead. Batch-3 ground-truth: each picker fires trigger(close) (close NOT inert), but open/commit-reset ARE inert; calendar/range-calendar are MID-REFACTOR (uncommitted view-switch work).
|
||||
provider: src/uix/soma/components/date-range-picker/date-range-picker-provider.svelte.ts
|
||||
|
||||
## Summary
|
||||
Counts (post-verification): CRITICAL 0 · HIGH 0 · MEDIUM 2 · LOW 0.
|
||||
systemic hits: SYS-1: scope-drift (eidos recipe missing); SYS-5: no syncAttrs double-write detected (eidos properly wraps soma without forcing attr overrides).
|
||||
composition (A27): Composes via A27: PopoverProvider and DateRangeFieldProvider bridged via SHARED writableActive refs (sharedOpen, sharedValue, sharedPlaceholder) in Provider component. Only root Provider, Trigger, and Calendar sub-parts are unique; Popover, Field, and RangeCalendar parts re-exported from composed primitives.
|
||||
|
||||
## Findings
|
||||
### MEDIUM: SYS-1 SCOPE-DRIFT: morfo declares scope=['soma','sema'] but an eidos recipe directory exis — date-range-picker-001 <!-- id: date-range-picker-001 -->
|
||||
- dimension: A: Contract
|
||||
- rule: SYS-1 SCOPE-DRIFT: morfo declares scope=['soma','sema'] but an eidos recipe directory exists (25 files in src/uix/eidos/components/date-range-picker/) yet NO recipe entry exists in src/uix/eidos/lib/recipes/base.ts
|
||||
- location: src/uix/morfo/components/date-range-picker.ts:13 + src/uix/eidos/lib/recipes/base.ts (missing entry)
|
||||
- evidence: Morfo line 13: `scope: ['soma', 'sema'],` but src/uix/eidos/components/date-range-picker/ directory has 25 component files. Grep for 'date-range-picker' in base.ts returns 0 matches.
|
||||
- impact: Eidos CSS file (date-range-picker.css) exists and is imported in eidos component (line 8) but no canonical recipe tokens/config define size/color/variant scales for this component; recipes are the contract bridge for eidos.
|
||||
- proposed-fix: Add 'eidos' to morfo scope declaration: `scope: ['soma', 'sema', 'eidos']` and create the date-range-picker recipe block in src/uix/eidos/lib/recipes/base.ts with token definitions for size (xs/sm/md/lg), color (primary/secondary/neutral/affirm/fulfill/risk/threat/loss), and variant (surface/outline/ghost).
|
||||
- verify: [confirmed] CONFIRMED as SYS-1 scope-drift (MEDIUM). morfo line 13 `scope: ['soma', 'sema']` omits 'eidos', yet src/uix/eidos/components/date-range-picker/ has 25 files incl. date-range-picker.css imported by the eidos component. Grep for 'date-range-picker' in lib/recipes/base.ts = 0 matches (confirmed). HOWEVER the candidate's proposedFix is partly WRONG and I downgrade its remedy claim: the eidos CSS does NOT need its own recipe block. I read date-range-picker.css (690 lines) — it owns NO `--date-range-picker-*` recipe tokens; it COMPOSES tokens from sibling recipes (`--date-field-height-md`, `--calendar-padding-md`, `--calendar-accent-solid`, lines 2-28) plus global scales (`--color-primary-*`, `--space-*` L391/419, `--radius-md` L589, `--font-size-md/sm` L595/621). This is the A27 picker-composition pattern: the picker composes Field+Calendar+Popover and legitimately has no recipe entry. Verified systemic across the family: date-picker.ts also has `scope: ['soma','sema']` and 0 recipe matches; time-range-picker same. So the ONLY real defect is the missing 'eidos' token in the scope array — not a missing recipe block. Severity MEDIUM (SYS-1) is correct; the fix is to add 'eidos' to scope, NOT to author recipe tokens.
|
||||
- fix-status: open
|
||||
|
||||
### MEDIUM: INERT EVENTS: morfo declares 'commit-reset' event but provider NEVER fires it via runtime. — date-range-picker-002 <!-- id: date-range-picker-002 -->
|
||||
- dimension: A: Contract
|
||||
- rule: INERT EVENTS: morfo declares 'commit-reset' event but provider NEVER fires it via runtime.trigger()
|
||||
- location: src/uix/morfo/components/date-range-picker.ts:65-72 (declaration) vs src/uix/soma/components/date-range-picker/date-range-picker-provider.svelte.ts:247-249 (clear() implementation)
|
||||
- evidence: Morfo line 65-72 declares: `{ name: 'commit-reset', semantic: { family: 'commit', verb: 'reset', ... } }`. Provider clear() method (line 247-249) reads: `clear(): void { this.opts.value.current = { start: undefined, end: undefined }; }` with NO `runtime.trigger('commit-reset', ...)` call. Grep for 'commit-reset' in provider finds 0 trigger sites.
|
||||
- impact: The event exists only to satisfy the schema validator. Consumers expecting `onCommitReset` callback or downstream event handlers observing 'commit-reset' will never fire, violating the 2-of-3 rule (morfo declares it; soma does not fire it).
|
||||
- proposed-fix: Add `void this.runtime.trigger('commit-reset', { fallbackTarget: ... });` in the clear() method (line 248) BEFORE resetting the value, to match the pattern used in triggerClose() (line 270). Alternatively, remove the event declaration from morfo if it is not semantically needed.
|
||||
- verify: [confirmed] CONFIRMED as the known picker-family inert-event pattern (MEDIUM cap, matches the prompt's explicit guidance). morfo lines 65-72 declare `{ name: 'commit-reset', semantic: { family: 'commit', verb: 'reset', target: v.partRef('calendar'), sequence: 'post', intent: 'neutral' } }`. Provider clear() at lines 247-249 reads exactly: `clear(): void { this.opts.value.current = { start: undefined, end: undefined }; }` — sets value, no trigger. Grep across the WHOLE component (provider + components/ + internals.ts) confirms the ONLY `runtime.trigger` call is `trigger('close', ...)` at line 270; zero `commit-reset` trigger sites anywhere. So the event is genuinely inert — it exists only to satisfy the schema validator, violating the 2-of-3 rule (morfo declares it; soma never fires it; sema/eidos read data-state not this event). Confidence high. MEDIUM is the correct severity per the INERT-EVENTS rule (MEDIUM at most). The candidate's proposedFix (add trigger in clear()) is a reasonable remedy, though note `data-last-action` is NOT among clear()'s causes since clear keeps the popover open — a commit-reset trigger would need its own non-close wiring.
|
||||
- fix-status: open
|
||||
|
||||
## No-findings dimensions
|
||||
B: Behavior (keyboard handlers on grid cells are soma-owned; no grid keyset declared in morfo per design), C: DOM-selector (no untrusted querySelector usage found), D: Frontier (soma provider has zero eidos imports; eidos wrapper normally imports soma - correct), E: TSC (no bare z-index, hex, or magic literals in CSS; all tokens reference --color-*, --space-*, --radius-*, --font-size-*, --icon-size-* scales), E-bis: Theming (token naming clean; roles from canonical 9-role set only; size/color/variant from EIDOS_VARIANTS)
|
||||
|
||||
## Theming facts (E-bis)
|
||||
- magic z-index: none
|
||||
- magic literals: none
|
||||
- undeclared parts: none
|
||||
- roles clean: true · variants clean: true
|
||||
- conformance: Morfo annotation is 'as const satisfies Morfo' (correct); all data-* attributes follow data-{component}-{part} or data-component-{nested} naming; aria-* attrs present for Trigger (aria-expanded, aria-haspopup, aria-controls, aria-label) and Calendar (aria-label); no double-write via syncAttrs observed in provider.
|
||||
|
||||
## Tests (F)
|
||||
- exists: true · env: jsdom
|
||||
- covers: provider part registration; state flag rendering (open/closed/disabled/readonly/required/invalid); closeOnRangeSelect behavior; range validation (single-day, minDays, maxDays, min/max value)
|
||||
- untested: commit/cancel/clear actions via footer (not provider-level); dismissed event on Escape/outside click (PopoverProvider owns this); commit-reset event firing; modal vs inline mode end-to-end; range re-anchor (end month rightmost) layout verification; keyboard navigation on month/year grids
|
||||
@ -0,0 +1,137 @@
|
||||
# Audit: dialog
|
||||
audit-version: 1
|
||||
audited-at: 2026-06-26
|
||||
scope: ['soma', 'sema'] (eidos recipe present though 'eidos' not in morfo scope — systemic SCOPE-DRIFT)
|
||||
files:
|
||||
- provider: src/uix/soma/components/dialog/dialog-provider.svelte.ts (present)
|
||||
- morfo: src/uix/morfo/components/dialog.ts (present)
|
||||
- recipe: src/uix/eidos/components/dialog/dialog.css + lib/recipes/base.ts §dialog (present)
|
||||
- demo: web/routes/uix/components/dialog (present, not inspected this pass)
|
||||
- test: src/uix/soma/components/dialog/dialog-provider.svelte.test.ts (present, jsdom)
|
||||
- readme: present (not inspected this pass)
|
||||
|
||||
## Summary
|
||||
Dialog is the reference for polymorphic close (book §5.3) and gets most of the hard stuff right: nesting via
|
||||
`watch`+`untrack` (loop-safe), FocusScope/ScrollLock derived from `modal` (A16), correct two-moments ordering
|
||||
(emit-before-state on `close` with sequence 'pre'), a thoughtfully tight `data-color` subset (neutral/risk/
|
||||
threat), and a clean eidos→soma frontier. Two real defects stand out: the Content provider **double-writes
|
||||
`role`/`aria-roledescription`** against the syncAttrs path (an a11y race for the `alertdialog` variant), and the
|
||||
**`[data-dialog-trigger]` CSS envelope is stale dead/conflicting chrome** left behind after the trigger migrated
|
||||
to a composed `<Button>`. Plus the systemic magic z-index and jsdom-only test gap.
|
||||
|
||||
Counts: CRITICAL 0 · HIGH 2 · MEDIUM 2 · LOW 2.
|
||||
|
||||
## Findings
|
||||
|
||||
### HIGH: Content double-writes `role` and `aria-roledescription` (syncAttrs + manual props) — racy role for `alertdialog` <!-- id: dialog-001 -->
|
||||
- dimension: D, A
|
||||
- rule: active_architecture §7.7 ("lo que `dom.apply` escribe, Svelte no lo renderiza — una sola autoridad por atributo") + rule 7.8
|
||||
- location: dialog-provider.svelte.ts:391-397 (Content registered `syncAttrs: true`) vs :460-463 (props override)
|
||||
- evidence:
|
||||
```ts
|
||||
this.runtimePart = this.provider.runtime.part('content', { …, syncAttrs: true });
|
||||
…
|
||||
readonly props = $derived.by(() => ({
|
||||
...this.runtimePart.props,
|
||||
role: this.provider.opts.variant.current, // 'dialog' | 'alertdialog'
|
||||
'aria-roledescription': undefined, // nukes the morfo's value
|
||||
…
|
||||
}));
|
||||
```
|
||||
The morfo declares Content `role: 'dialog'` (morfo:150) and `aria-roledescription` (:211-215); with
|
||||
`syncAttrs: true` the runtime writes both via `dom.apply`. The provider then ALSO sets `role` (to `variant`)
|
||||
and `aria-roledescription: undefined` in the Svelte-rendered props. Two authorities per attribute.
|
||||
- impact: for `variant='alertdialog'`, `dom.apply` writes `role="dialog"` while Svelte writes
|
||||
`role="alertdialog"` — order-dependent ping-pong; the accessible role of an alert dialog is non-deterministic
|
||||
(a WAI-ARIA correctness break). `aria-roledescription` is written-then-blanked. The morfo (the cross-layer
|
||||
source of truth) says `role='dialog'` but the component renders a variant-dependent role it can't express.
|
||||
- repro: render `<Dialog variant="alertdialog">`, inspect Content `role` across effect ticks.
|
||||
- proposed-fix: make `role` morfo-expressible (bind a `propRef('variant')`/stateRef so the morfo owns the
|
||||
variant-dependent role), OR drop `syncAttrs` for Content and supply role/aria via `renderProps()` + a single
|
||||
override. Don't mix syncAttrs with manual same-attr writes.
|
||||
- fix-status: open
|
||||
|
||||
### HIGH: `[data-dialog-trigger]` CSS envelope is dead/conflicting after the trigger migrated to a composed `<Button>` <!-- id: dialog-002 -->
|
||||
- dimension: E-bis, D
|
||||
- rule: THEMING §16 "no solapar" (overlapping/mis-owned styles) + COMPONENT_GUIDE §4 (compose, don't reinvent) + project memory `project_dialog_button_2026-06-20`
|
||||
- location: dialog.css:22-54 (`[data-dialog-trigger]` full button envelope) vs dialog-trigger.svelte:17-26
|
||||
(renders `<Button {...props}>` via soma `child`); recipe `dialog.trigger-font-size` (base.ts:727).
|
||||
- evidence: the eidos `<Dialog.Trigger>` composes the framework `<Button>` through soma's `child` snippet, so
|
||||
the rendered element carries BOTH `data-button` AND `data-dialog-trigger`. The dialog recipe still defines a
|
||||
complete competing chrome on `[data-dialog-trigger]` (height/padding/border/background/hover/disabled +
|
||||
`--dialog-trigger-font-size`). Both single-attribute selectors have equal specificity → source-order decides;
|
||||
the two recipes fight over the same element. The CSS header comment (:15-21, "the trigger needs its own
|
||||
envelope here") is stale — it predates the Button migration.
|
||||
- impact: silent visual incoherence (two sources for one button's chrome), order-dependent; the
|
||||
`--dialog-trigger-*` tokens are orphan knobs. Exactly the overlap E-bis.5 forbids.
|
||||
- repro: inspect a `<Dialog.Trigger>` — it has `data-button` + `data-dialog-trigger`; toggle CSS order to see
|
||||
the chrome change.
|
||||
- proposed-fix: delete the `[data-dialog-trigger]` envelope + its tokens (Button owns the chrome); keep only a
|
||||
dialog-specific concern if any (there is none — the Close recipe is the correct model, dialog.css:319-325).
|
||||
- fix-status: open
|
||||
|
||||
### MEDIUM: `overlay-z: '70'` magic z-index literal (content = `calc(overlay-z + 1)`) <!-- id: dialog-003 -->
|
||||
- dimension: E-bis
|
||||
- rule: THEMING §35 Bloque C (z-index magic → `--z-index-*` tokens) + feedback "no intentional magic numbers"
|
||||
- location: lib/recipes/base.ts:731 (`'overlay-z': '70'`); dialog.css:67 + :91 (`calc(var(--dialog-overlay-z) + 1)`).
|
||||
- evidence: a bare `70` where a `var(--z-index-modal)` token belongs. Same class as `select.content-z: '80'` →
|
||||
candidate systemic finding (overlay stacking ladder should be one tokenized source, not per-recipe integers).
|
||||
- impact: the modal/popover/overlay z-ladder can't be coordinated centrally; integers drift apart.
|
||||
- proposed-fix: map dialog overlay/content to `--z-index-modal`/`--z-index-modal-content` (or the canonical
|
||||
ladder) and audit all overlay recipes together.
|
||||
- fix-status: open
|
||||
|
||||
### MEDIUM: Tests pin open/close/disabled/modal-derivation; dismissal paths, polymorphic causes, nesting, focus untested; jsdom-only <!-- id: dialog-004 -->
|
||||
- dimension: F
|
||||
- rule: dimension-F honesty check
|
||||
- location: dialog-provider.svelte.test.ts (3 tests)
|
||||
- evidence: only `dismissWith('dismiss')` of the five causes is exercised (`save`/`fail`/`cancel`/
|
||||
`dismiss-outside` and their distinct `data-last-action` + semantic.family/intent are not). No test for the
|
||||
Escape/interact-outside dismissal wiring, `nestedOpenCount` increment/decrement, or focus trap+return (jsdom
|
||||
can't exercise FocusScope faithfully — `@vitest-environment jsdom` :1).
|
||||
- impact: the polymorphic-close matrix (the component's headline feature) and the focus/nesting behavior ship
|
||||
largely unverified; dialog-001's role race wouldn't be caught.
|
||||
- proposed-fix: parametrize a test over all five `DISMISS_CAUSES` asserting `data-last-action` + emitted
|
||||
family/intent; add nesting-count tests; add a client/Playwright test for focus trap + return.
|
||||
- fix-status: open
|
||||
|
||||
### LOW: Provider part declares `data-state`/`data-color`/`data-disabled` but is `kind:'virtual'` / `defaultElement:'none'` <!-- id: dialog-005 -->
|
||||
- dimension: A
|
||||
- rule: dimension-A (part data must reach an element) + 2-of-3
|
||||
- location: morfo dialog.ts:96-126 (Provider `kind:'virtual'`, `defaultElement:'none'`, yet declares 3 data attrs);
|
||||
provider registers it `syncAttrs: true` with optional `ref` (provider renders no DOM).
|
||||
- evidence: the DialogProvider renders only children (no host element); a virtual part with no element can't
|
||||
carry `data-state`/`data-color`. The same data also lives on Content (where it's actually consumed by the CSS).
|
||||
- impact: the Provider-part data declarations are likely unreachable (dead contract) unless the eidos root
|
||||
wrapper renders an element and applies them — needs confirmation. If dead, they mislead readers/tools.
|
||||
- proposed-fix: confirm whether any element carries `data-dialog` (eidos root). If not, drop the data from the
|
||||
virtual Provider part (Content already carries state/color), or give the provider a real host.
|
||||
- fix-status: open
|
||||
|
||||
### LOW: Eidos-only `[data-dialog-header]`/`[data-dialog-footer]` shells not morfo-backed <!-- id: dialog-006 -->
|
||||
- dimension: A, D
|
||||
- rule: active_architecture §7.6 (eidos consumes declared data-*) — acknowledged exception
|
||||
- location: dialog.css:260-290 (commented "Layout shells (eidos-only, no morfo backing)").
|
||||
- evidence: pure layout containers with no ARIA/behavior, styled by the recipe but absent from the morfo. The
|
||||
CSS comment is explicit about it.
|
||||
- impact: minimal (no behavior/ARIA), but it's the same undeclared-part pattern as select-005 — worth a
|
||||
consistent disposition (eidos-only parts either get `kind:'internal'` morfo entries or a documented allow-list).
|
||||
- proposed-fix: adopt a project-wide convention for eidos-only decorative/layout parts (see SHARED_EXTRACTION /
|
||||
systemic UNDECLARED-EIDOS-PARTS).
|
||||
- fix-status: open
|
||||
|
||||
## No-findings dimensions
|
||||
- **B (behavior):** two-moments ordering correct (`open`/`close` sequence 'pre' → emit precedes state via
|
||||
`await runtime.trigger`); nesting count uses `watch` + `untrack` (loop-safe, A30-spirit); FocusScope +
|
||||
ScrollLock derive from `modal` (A16); disabled guards at call-site (rule 10). Clean.
|
||||
- **C (selectors):** no `querySelector` with interpolated user values in the provider. Clean.
|
||||
- **D (frontier, severe half):** provider imports only soma layers + morfo — zero eidos imports. (role
|
||||
double-write filed under D as dialog-001, but it's a runtime/contract issue, not a cross-layer import.)
|
||||
- **E (TSC):** `data-color` declared per-part in the morfo (provider+content), CSS consumes it on content; no
|
||||
eager-substitution `:root` derived bug. Colors fully tokenized. Clean.
|
||||
|
||||
## Style observations (opinion, non-blocking)
|
||||
- The `data-color` subset (neutral/risk/threat only, with the rationale "a dialog is not a hierarchy and
|
||||
irreversible loss should not be a dialog") is an exemplary application of THEMING §4 subset-por-componente —
|
||||
worth citing as the positive reference for other components.
|
||||
- The polymorphic-close `DISMISS_CAUSES` map is clean and readable; the only gap is test coverage of its breadth.
|
||||
@ -0,0 +1,26 @@
|
||||
# Audit: display
|
||||
audit-version: 1
|
||||
audited-at: 2026-06-26
|
||||
scope: eidos (eidos-only primitive)
|
||||
method: theming-coherence category sweep (typography), adversarially verified; roles + `:Morfo` lead-verified CLEAN across all eidos (no success/warning/danger/info; only alert-dialog uses `:Morfo`).
|
||||
|
||||
## Summary
|
||||
Counts (post-verification): CRITICAL 0 · HIGH 0 · MEDIUM 0 · LOW 1.
|
||||
conformance: morfo OK; no recipe; roles fragmented (content-role subset)
|
||||
as-const: true · roles clean: false · variants clean: true · has-recipe: false
|
||||
|
||||
## Findings
|
||||
### LOW: em-literal for underline offset; hero sizing is special case — display-002 <!-- id: display-002 -->
|
||||
- dimension: E-bis: Theming (SIZES)
|
||||
- location: src/uix/eidos/components/display/display.css:33
|
||||
- evidence: text-underline-offset: 0.12em (different from text 0.15em)
|
||||
- impact: Magic em literal; intentional hero differentiation but undocumented as canonical
|
||||
- proposed-fix: Define --display-underline-offset or document hero-specific em scale
|
||||
- verify: [downgraded] display.css:33 'text-underline-offset: 0.12em' is an em-relative underline offset for hero copy. The 0.12 vs 0.15 difference is a deliberate optical choice at hero sizes (smaller offset reads better on large type), documented in types.ts:20-27 re: hero tracking. No canonical --*-underline-offset token exists; em is the accepted unit (cf. link recipe base.ts:878). Cosmetic em literal, not drift aga
|
||||
- fix-status: open
|
||||
|
||||
## Theming facts (E-bis)
|
||||
- magic literals: 0.12em @ src/uix/eidos/components/display/display.css:33 -> canonical display-underline-offset token (or hero scale)
|
||||
- magic z-index: none
|
||||
- non-canonical names: none
|
||||
- roles clean: false · variants clean: true
|
||||
@ -0,0 +1,65 @@
|
||||
# Audit: drag-drop
|
||||
audit-version: 1
|
||||
audited-at: 2026-06-26
|
||||
scope: (SCOPE-DRIFT → SYS-1)
|
||||
method: adversarially-verified workflow; HIGH lead-verified. B7 ground-truth: Toolbar A35 + Form A36 incidents STAYED FIXED (untrack present); toast/clipboard/drag-drop use uix.timers (no A6 leak); button is clean (its archetype finding was inert — Spinner not runtime-registered); data-size/data-shape are eidos visual attrs (not contract violations).
|
||||
provider: src/uix/soma/components/drag-drop/drag-drop-provider.svelte.ts
|
||||
cleanup-audit (A6/A35/A36): TIMERS/LISTENERS:
|
||||
1. setupPointerDrag() — lines 261-294:
|
||||
- this.soma.dom.listen(win, 'pointermove', onPointerMove) → cleanupPointerMove [DISPOSED]
|
||||
- this.soma.dom.listen(win, 'pointerup', onPointerUp) → cleanupPointerUp [DISPOSED]
|
||||
- this.soma.dom.listen(win, 'pointercancel', onPoint
|
||||
|
||||
## Summary
|
||||
Counts (post-verification): CRITICAL 0 · HIGH 0 · MEDIUM 3 · LOW 0.
|
||||
|
||||
## Findings
|
||||
### MEDIUM: SYS-1 SCOPE-DRIFT — drag-drop-001 <!-- id: drag-drop-001 -->
|
||||
- dimension: D
|
||||
- rule: SYS-1 SCOPE-DRIFT
|
||||
- location: src/uix/morfo/components/drag-drop.ts:7, src/uix/eidos/components/drag-drop/
|
||||
- evidence: Morfo declares scope: ['soma', 'sema'] (line 7) but eidos component directory exists at src/uix/eidos/components/drag-drop/ with drag-drop.svelte, drag-drop.css, and sub-components (draggable, droppable, preview). The recipe dir exists but morfo scope omits 'eidos'.
|
||||
- impact: Framework scope validation may flag this as inconsistent; eidos layer is present but not declared in morfo scope.
|
||||
- proposed-fix: Update morfo scope declaration to scope: ['soma', 'sema', 'eidos'] to match the actual component hierarchy.
|
||||
- verify: [confirmed] CONFIRMED. morfo declares scope: ['soma', 'sema'] (drag-drop.ts:7) yet a full eidos implementation exists: recipe entry 'drag-drop': { 'preview-z': '99' } (recipes/base.ts:4259-4261), drag-drop.css, and 3 svelte wrappers (draggable/droppable/preview). The Morfo.scope doctrine (morfo/types.ts:798) reads 'Layers that implement this component' and 'eidos' is a valid Layer (schema.ts:49: union(literal('soma'), literal('sema'), literal('eidos'))). Omitting 'eidos' is genuine scope-drift = SYS-1 baseline MEDIUM. Note: many siblings (calendar/carousel/color-field/combobox/command) share the same drift, consistent with SYS-1 being systemic.
|
||||
- fix-status: open
|
||||
|
||||
### MEDIUM: SYS-2 MAGIC Z-INDEX — drag-drop-002 <!-- id: drag-drop-002 -->
|
||||
- dimension: B
|
||||
- rule: SYS-2 MAGIC Z-INDEX
|
||||
- location: src/uix/soma/components/drag-drop/drag-drop-provider.svelte.ts:656, 664
|
||||
- evidence: DragPreviewProvider.style $derived.by inlines 'z-index': '9999' (lines 656, 664) as a hardcoded literal in the style object. The eidos CSS correctly uses var(--drag-drop-preview-z) (drag-drop.css:97) which resolves to 99 per generated base.css:3400, but the inline style hardcodes 9999 instead.
|
||||
- impact: z-index value is 9999 (extremely high, above all canonical layers) instead of 99 (--drag-drop-preview-z). This violates the canonical --z-index-* scale and may unexpectedly layer above modals (--z-index-modal: 700) and toast (--z-index-toast: 900).
|
||||
- proposed-fix: Replace hardcoded '9999' with 'var(--drag-drop-preview-z)' to consume the canonical token, e.g., 'z-index': 'var(--drag-drop-preview-z)'.
|
||||
- verify: [downgraded] DIVERGENCE CONFIRMED, severity DOWNGRADED HIGH->MEDIUM. DragPreviewProvider.style $derived.by hardcodes 'z-index': '9999' on both branches (drag-drop-provider.svelte.ts:656 and :664). The CSS rule [data-drag-drop-preview] uses z-index: var(--drag-drop-preview-z) (drag-drop.css:97) which resolves to 99 (generated/base.css:3400). Because the provider sets z-index inline via props.style, inline precedence wins, so effective z is 9999 and the recipe token is inert. 9999 is off the canonical scale entirely (--z-index-base 0 .. --z-index-toast 900; generated/base.css:304-311) and exceeds modal (700) and toast (900). This is a magic-literal that overrides its own dedicated token (SYS-2 magic-z), NOT a syncAttrs double-write (no data-attr involved). Per rubric magic-literal == MEDIUM and SYS-2 is baselined MEDIUM, so HIGH is unwarranted.
|
||||
- fix-status: open
|
||||
|
||||
### MEDIUM: Test coverage for high-risk paths — drag-drop-003 <!-- id: drag-drop-003 -->
|
||||
- dimension: F
|
||||
- rule: Test coverage for high-risk paths
|
||||
- location: src/uix/soma/components/drag-drop/drag-drop-provider.svelte.test.ts:126-306
|
||||
- evidence: Test file contains 2 test cases: (1) keyboard drag start/navigation/commit, (2) drag prevention, accept filters, cancel, pointer preview. Missing explicit coverage for: pointer drag initiation and movement (setupPointerDrag), pending pointer-down state promotion (DraggableProvider.onpointerdown moveBuffer threshold), pointer listener cleanup on premature cancellation, and live-region disposal on component unmount.
|
||||
- impact: Pointer drag logic and pending state lifecycle are not directly tested; test env is jsdom-only. A/B testing (live-region dual-region toggle) is not verified.
|
||||
- proposed-fix: Add test case for pointer drag from pointerdown to pointermove threshold to active drag promotion; verify cleanup on pointercancel; add test for announcer timer cancellation on provider cleanup.
|
||||
- verify: [confirmed] CONFIRMED. Test file has exactly 2 it() cases (drag-drop-provider.svelte.test.ts:132 keyboard drag; :213 prevention/accept/cancel/preview). Both drive the flow by calling provider.startDrag(...) directly (e.g. lines 243-250, 256-264 with keyboard:false) rather than dispatching real pointerdown -> pointermove -> moveBuffer promotion (DraggableProvider.onpointerdown at :417-457, threshold Math.hypot < moveBuffer at :432). The setupPointerDrag window-listener flow (:261-294) and teardown/listener-cleanup (:245-257) are not exercised via real events. The preview-style assertion (:267-275 objectContaining) does NOT assert z-index, so the 9999 hardcode is also untested. jsdom-only env = SYS-3. MEDIUM stands.
|
||||
- fix-status: open
|
||||
|
||||
## No-findings dimensions
|
||||
A, C
|
||||
|
||||
## Theming facts (E-bis)
|
||||
- magic z-index: src/uix/soma/components/drag-drop/drag-drop-provider.svelte.ts:656,664 — hardcoded '9999' instead of var(--drag-drop-preview-z)
|
||||
- magic literals: none
|
||||
- undeclared parts: none
|
||||
- roles clean: true · variants clean: true
|
||||
|
||||
## Tests (F)
|
||||
- exists: true · env: jsdom
|
||||
- covers: keyboard drag start/activate via Space/Enter; keyboard navigation (ArrowDown/ArrowRight, ArrowUp/ArrowLeft, Tab/Shift+Tab); keyboard drop (Enter/Space on overTarget); keyboard cancel (Escape); onDragStart preventDefault block; accept filter evaluation; pointer drag cancel on invalid drop; pointer preview style positioning (fixed layout, offset from pointer)
|
||||
- untested: pointer drag initiation (onpointerdown → pending state); pointer drag promotion (moveBuffer threshold exceeded); pointer listener cleanup on premature cancel; keyboard listener cleanup/disposal; announcer timer cleanup on provider dispose; live-region dual-region A/B toggle announce pattern
|
||||
|
||||
## Style observations (non-blocking)
|
||||
- Draggable cursor transitions from grab → grabbing on :active, correct interaction feedback
|
||||
- Droppable uses inset box-shadow rings for accept/dragover states, consistent with design tokens
|
||||
- Preview uses --opacity-ghost (opacity-disabled), --space-* spacing, --radius-md, and role aliases correctly
|
||||
- Color cascade via data-color attribute in eidos component correctly routes to --_drag-drop-accent-soft and --_drag-drop-accent-solid variables
|
||||
@ -0,0 +1,89 @@
|
||||
# Audit: drawer
|
||||
audit-version: 1
|
||||
audited-at: 2026-06-26
|
||||
scope: ['soma', 'sema'] (SCOPE-DRIFT → SYS-1)
|
||||
method: adversarially-verified workflow (analyze → refute); HIGH/CRITICAL personally re-verified against cited code by the lead.
|
||||
provider: src/uix/soma/components/drawer/drawer-provider.svelte.ts
|
||||
|
||||
## Summary
|
||||
Counts (post-verification): CRITICAL 0 · HIGH 0 · MEDIUM 5 · LOW 0.
|
||||
systemic hits: SYS-1: scope-drift (morfo scope missing 'eidos' despite recipe directory existing); SYS-2: magic z-index (bare integers in recipe: overlay-z='72', inline-z='64'), magic opacity/dimensions (overlay-opacity='62%', handle-length='36px', handle-thickness='4px', content-ring-dragging-offset='2px'), magic opacity in color-mix (32%); SYS-3: jsdom-only test with untested high-risk paths (keyboard, drag, focus, gesture cleanup); SYS-5: double-write (provider explicitly sets role and aria-modal which morfo also declares).
|
||||
|
||||
## Findings
|
||||
### MEDIUM: SYS-1 — drawer-001 <!-- id: drawer-001 -->
|
||||
- dimension: A
|
||||
- rule: SYS-1
|
||||
- location: src/uix/morfo/components/drawer.ts:7
|
||||
- evidence: scope: ['soma', 'sema'] but eidos directory exists at src/uix/eidos/components/drawer/
|
||||
- impact: Scope does not declare eidos as a layer despite recipe existing. Signals incomplete contract annotation.
|
||||
- repro: Read src/uix/morfo/components/drawer.ts line 7 and ls src/uix/eidos/components/drawer/
|
||||
- proposed-fix: Update morfo scope to include 'eidos': scope: ['soma', 'sema', 'eidos']
|
||||
- verify: [confirmed] morfo line 7: `scope: ['soma', 'sema'],` — 'eidos' absent. Eidos recipe block exists (base.ts:892 `drawer: {` with ~50 tokens) plus a components/drawer/ dir. Systemic SYS-1: sibling dialog.ts:18 has the identical `scope: ['soma', 'sema']` with its own eidos recipe (base.ts:726). HIGH is correct per SYS-1 contract-drift; the morfo scope under-declares a real layer.
|
||||
- fix-status: open
|
||||
|
||||
### MEDIUM: SYS-2 — drawer-002 <!-- id: drawer-002 -->
|
||||
- dimension: E-bis
|
||||
- rule: SYS-2
|
||||
- location: src/uix/eidos/lib/recipes/base.ts:896,897
|
||||
- evidence: 'overlay-z': '72', 'inline-z': '64' — bare integers without --z-index-* token references
|
||||
- impact: Magic z-index values not parameterized via canonical token system. Inconsistent with project theming rules.
|
||||
- repro: grep "'overlay-z'\|'inline-z'" src/uix/eidos/lib/recipes/base.ts
|
||||
- proposed-fix: Create --z-index-overlay and --z-index-inline tokens in the canonical scale and reference via var() in recipe.
|
||||
- verify: [confirmed] base.ts:896 `'overlay-z': '72',` and 897 `'inline-z': '64',` are bare integers. A canonical `--z-index-*` scale exists and is consumed elsewhere (base.ts:4357 `z: 'var(--z-index-sticky)'`; comment at 4250-4252 references 'the global --z-index-* scale'). These two values bypass it. Genuine SYS-2 magic-z. MEDIUM appropriate.
|
||||
- fix-status: open
|
||||
|
||||
### MEDIUM: SYS-2 — drawer-003 <!-- id: drawer-003 -->
|
||||
- dimension: E-bis
|
||||
- rule: SYS-2
|
||||
- location: src/uix/eidos/lib/recipes/base.ts:895
|
||||
- evidence: 'overlay-opacity': '62%' — bare opacity literal without --opacity-* token
|
||||
- impact: Magic opacity value not referenced via canonical opacity token scale.
|
||||
- repro: grep "'overlay-opacity'" src/uix/eidos/lib/recipes/base.ts
|
||||
- proposed-fix: Create --opacity-overlay token or use existing project opacity scale, reference via var().
|
||||
- verify: [confirmed] base.ts:895 `'overlay-opacity': '62%',` — bare percentage. An `--opacity-*` scale exists and is consumed widely (e.g. base.ts:890 `'disabled-opacity': 'var(--opacity-disabled)'`). The 62% literal does not reference it. Confirmed SYS-2 magic-opacity; note it is replicated verbatim in dialog (base.ts:729) — systemic, not drawer-specific. MEDIUM appropriate.
|
||||
- fix-status: open
|
||||
|
||||
### MEDIUM: SYS-5 — drawer-007 <!-- id: drawer-007 -->
|
||||
- dimension: A
|
||||
- rule: SYS-5
|
||||
- location: src/uix/soma/components/drawer/drawer-provider.svelte.ts:1044-1045
|
||||
- evidence: Provider props explicitly set role: 'dialog' and 'aria-modal': this.provider.isOverlay ? true : undefined, while morfo also declares role:'dialog' and aria-modal with prop-truthy condition
|
||||
- impact: Double-write of role and aria-modal. If morfo condition diverges from provider logic, the two values could conflict. Violates 'una sola autoridad por atributo'.
|
||||
- repro: Read src/uix/soma/components/drawer/drawer-provider.svelte.ts lines 1044-1045 and compare with morfo aria declarations at lines 184, 214-217
|
||||
- proposed-fix: Remove explicit role and aria-modal from provider props — let the runtime apply them from morfo. If dynamic aria-modal is needed, ensure logic matches the morfo's prop-truthy condition exactly.
|
||||
- verify: [downgraded] Real double-write confirmed but severity overstated. Content part is `syncAttrs: true` (provider line 517), so the runtime's syncPartAttrs effect (runtime.svelte.ts:456-465) writes morfo's static `role: 'dialog'` (morfo:184) + dynamic `aria-modal` gated by `prop-truthy modal` (morfo:214-217). Provider props ALSO set them (provider:1044 `role: 'dialog' as const`, 1045 `'aria-modal': this.provider.isOverlay ? true : undefined`). So both attrs are written by two authorities — a true SYS-5 'una sola autoridad' violation. BUT the rubric reserves HIGH for SYS-5 'when the two values can diverge'. They cannot: `role` is the same literal both sides, and `aria-modal` = `isOverlay` (provider:190 `variant==='overlay'`) is identical to morfo's `modal` source (provider:254 `() => opts.variant.current === 'overlay'`). Non-divergent double-write → MEDIUM, not HIGH. Same pattern repeats for Title (provider:1208 `role:'heading'` + 1209 `aria-level` vs morfo:279/282) and Trigger (provider:443-446 type/aria-haspopup/aria-expanded vs morfo:154/162-164) — systemic across this provider.
|
||||
- fix-status: open
|
||||
|
||||
### MEDIUM: SYS-3 — drawer-008 <!-- id: drawer-008 -->
|
||||
- dimension: F
|
||||
- rule: SYS-3
|
||||
- location: src/uix/soma/components/drawer/drawer-provider.svelte.test.ts:1
|
||||
- evidence: @vitest-environment jsdom — tests only cover direction resolution, dismissal, and variant modes. No tests for keyboard handling (Tab/Escape), focus return to trigger, drag gestures, gesture cleanup, or focus trap behavior.
|
||||
- impact: Interaction-heavy component (keyboard nav, drag, focus management) tested only on low-risk paths with jsdom. Lacks client/Playwright tests for gesture interaction and focus side-effects.
|
||||
- repro: Read drawer-provider.svelte.test.ts; note tests use jsdom and @vitest-environment jsdom decorator
|
||||
- proposed-fix: Add a client-environment (Playwright) test suite covering: Tab/Shift+Tab keyboard navigation, Escape dismissal, drag-to-dismiss interaction, focus return on close, and gesture cleanup on unmount.
|
||||
- verify: [confirmed] Test file line 1 `// @vitest-environment jsdom`. Only 4 tests (describe at :96, it at :102 direction-resolution, :115 dismissal+snap-clear, :155 persistent-open, :174 inline-escape). No coverage of: Tab/Shift+Tab (morfo keyboard focus-next/focus-prev at morfo:234-235), focus trap/return-to-trigger (morfo focus.trap/return at :109-114; provider handleClose:359 manual focus return), drag gesture (provider gesture/axial at :682/:883), or two-moments emit ordering (close is sequence:'pre', morfo:52). Interaction-heavy + jsdom-only + no client test = SYS-3. MEDIUM appropriate.
|
||||
- fix-status: open
|
||||
|
||||
## No-findings dimensions
|
||||
B, C, D, G
|
||||
|
||||
## Theming facts (E-bis)
|
||||
- magic z-index: content-z: calc(var(--drawer-overlay-z) + 1) — relies on recipe's bare '72' integer
|
||||
- magic literals: overlay-z '72' should be --z-index-overlay var | inline-z '64' should be --z-index-inline var | overlay-opacity '62%' should be --opacity-overlay var | handle-length '36px' should reference --size-* or --space-* token | handle-thickness '4px' should reference --space-* token | content-ring-dragging-offset '2px' should reference --space-* token | content-ring-dragging-color opacity 32% should be extracted to --opacity-dragging-ring token
|
||||
- undeclared parts: none
|
||||
- roles clean: true · variants clean: true
|
||||
- conformance: roles/sizes: 'sm'|'md'|'lg'|'full' per component subset (correct). variants: 'overlay'|'inline'|'persistent' (component-specific, documented). naming: --drawer-{slot} public, --_drawer-* private (observed). no undeclared parts in recipe (header/footer are intentional eidos-only layout parts).
|
||||
|
||||
## Tests (F)
|
||||
- exists: true · env: jsdom (@vitest-environment jsdom)
|
||||
- covers: logical direction resolution (start/end → physical sides per dir); polymorphic close with data-last-action imperative prewrite; persistent variant ignores open state; inline variant skips focus trap and scroll lock
|
||||
- untested: keyboard navigation (Tab, Shift+Tab, Escape) from morfo's keyboard action routes; focus trap and focus return to trigger in overlay/modal mode; drag-to-dismiss gesture and progress signals; resize-on-drag with snap points; snap-point sequential cycling; gesture cleanup on unmount (cancellation of requestAnimationFrame, event listeners); nesting behavior (data-nested, nestedOpenCount management); drag-start/end/progress semantic emissions; scroll-lock guard (scrollLockTimeout behavior); handle-only gesture restriction; pointer event composition (gesture + axial mutual exclusivity); overlay opacity modulation based on activeSnapPoint
|
||||
|
||||
## Style observations (non-blocking)
|
||||
- Morfo declaration is well-structured with clear event semantics (polymorphic close, drag handle family, resize handle family). Events properly declare sequence (pre/coincident/post).
|
||||
- Provider architecture cleanly separates concerns: DrawerProvider (root state), DrawerTriggerProvider, DrawerContentProvider (gesture+focus), DrawerOverlayProvider, DrawerHandleProvider, DrawerTitleProvider, DrawerDescriptionProvider, DrawerCloseProvider. Each uses direct constructor-level child→parent registration (A30-compliant).
|
||||
- Gesture layer properly throttles drag-progress to rAF via flushDragProgress and rAF request/cancel lifecycle with proper cleanup in $effect.
|
||||
- Focus return correctly implemented: non-modal returns focus to triggerNode (lines 359, 366); modal relies on FocusScope layer.
|
||||
- Dismiss layer correctly gated: interactOutside always 'ignore' (drawer is not a popover), Escape gated by modal flag (non-modal manually handles via onkeydown in content).
|
||||
- CSS uses intentional eidos-only layout parts (data-drawer-header, data-drawer-footer) not declared in morfo — correct separation of layout concerns from behavioral contract.
|
||||
@ -0,0 +1,120 @@
|
||||
# Audit: dropdown-menu
|
||||
audit-version: 1
|
||||
audited-at: 2026-06-26
|
||||
scope: ['soma', 'sema', 'eidos'] (eidos IS declared — no scope-drift, unlike select/dialog/popover/combobox)
|
||||
files:
|
||||
- provider: src/uix/soma/components/dropdown-menu/dropdown-menu-provider.svelte.ts (present)
|
||||
- morfo: src/uix/morfo/components/dropdown-menu.ts (present)
|
||||
- recipe: src/uix/eidos/components/dropdown-menu/dropdown-menu.css + lib/recipes/base.ts §dropdown-menu (present)
|
||||
- demo: web/routes/uix/components/dropdown-menu (present, not inspected this pass)
|
||||
- test: src/uix/soma/components/dropdown-menu/dropdown-menu-provider.svelte.test.ts (present, jsdom, 6 tests)
|
||||
- readme: present (not inspected this pass)
|
||||
|
||||
## Summary
|
||||
The most disciplined provider of Batch 1. Consistent roving DOM focus (no virtual/real mix — the morfo declares
|
||||
NO `aria-activedescendant`, the provider uses real `dom.focus`, A17-correct), uniform `renderProps()` usage
|
||||
(no double-writes), a clean eidos→soma frontier, correct `data-disabled`/`accessibleWhenDisabled` handling, and
|
||||
fully self-managed cleanup — including `SafePolygon`, which registers its document listeners inside an internal
|
||||
`$effect` via `dom.listen` (verified: no A6 leak). It is the only Batch-1 overlay that correctly declares
|
||||
`eidos` in its morfo scope. No HIGH findings. The remaining issues are the systemic directional-nav duplication,
|
||||
magic theming literals, a checkbox/radio toggle that emits no semantic event, and a navigation-untested gap.
|
||||
|
||||
Counts: CRITICAL 0 · HIGH 0 · MEDIUM 4 · LOW 1.
|
||||
|
||||
## Findings
|
||||
|
||||
### MEDIUM: Directional-nav index math duplicated (Content + SubContent, and across Select/Combobox) — incl. the loop off-by <!-- id: dropdown-menu-001 -->
|
||||
- dimension: G, B
|
||||
- rule: dimension-G redundancy + the loop-boundary edge case
|
||||
- location: dropdown-menu-provider.svelte.ts:384-422 (Content.onkeydown) and :1165-1209 (SubContent.onkeydown)
|
||||
- evidence: both handlers re-implement the identical next/prev/Home/End + typeahead index math
|
||||
(`(i+1)%len` / `Math.min` / `Math.max` / `(i-1+len)%len`). It's also the same math as Select content route
|
||||
(select-003) and Combobox content route. The `currentIndex === -1` + prev + loop path computes
|
||||
`(-1-1+n)%n = n-2` (lands second-to-last, not last) — the same off-by as select-003; reachable when keydown
|
||||
fires with focus on the container rather than an item.
|
||||
- impact: four copies of one algorithm (one bug fixed in one place misses the others); the loop off-by is a
|
||||
latent edge-case bug.
|
||||
- proposed-fix: extract a pure `nextIndex(curr, key, len, {loop, dir, orientation})` helper, unit-test the
|
||||
boundaries once, and consume it in all menu/select/combobox keyboard routes. (See SHARED_EXTRACTION
|
||||
DIRECTIONAL-NAV.)
|
||||
- fix-status: open
|
||||
|
||||
### MEDIUM: Checkbox/Radio item activation mutates state but emits no semantic event (unlike plain Item) <!-- id: dropdown-menu-002 -->
|
||||
- dimension: A, B
|
||||
- rule: M-3.7 (every state-mutating keyboard action needs a semantic event) + the morfo only declares
|
||||
`commit-select` on `item`
|
||||
- location: provider `MenuItemProvider.onclick/onkeydown` fire `runtime.trigger('commit-select', …)` (:510, :521),
|
||||
but `MenuCheckboxItemProvider.activate` (:735-745) and `MenuRadioItemProvider.activate` (:860-865) mutate
|
||||
`checked`/group value and call only `onSelect()` + `handleClose()` — no `runtime.trigger`.
|
||||
- evidence: toggling a checkbox-item or selecting a radio-item changes state with no perceptual emit; the morfo
|
||||
declares no event for `checkbox-item`/`radio-item`.
|
||||
- impact: with `closeOnSelect: false` (the common case for checkbox menus) there is zero sema feedback on
|
||||
toggle, while a plain Item select gets `commit.select + affirm`. Inconsistent perceptual contract; a
|
||||
checkbox toggle is canonically `commit.toggle`.
|
||||
- repro: open a menu with a `closeOnSelect={false}` CheckboxItem, toggle it — no `data-event` is stamped.
|
||||
- proposed-fix: declare `commit-toggle` (checkbox) / `commit-select` (radio) events on those parts in the
|
||||
morfo and fire them from `activate()`, or document the silence as deliberate in the README.
|
||||
- fix-status: open
|
||||
|
||||
### MEDIUM: Magic theming literals — `content-z: '80'`, `item-disabled-opacity: '0.55'`, `item-height: '2rem'` <!-- id: dropdown-menu-003 -->
|
||||
- dimension: E-bis
|
||||
- rule: THEMING §35 (z-index/opacity magic → tokens) + §5 (size from `--list-*`/`--control-height-*`/`--space-*`) + §4
|
||||
- location: lib/recipes/base.ts:4279 (`'content-z': '80'`), :4292 (`'item-disabled-opacity': '0.55'`),
|
||||
:4286 (`'item-height': '2rem'`)
|
||||
- evidence: `content-z: '80'` (5th overlay with a hardcoded content-z — systemic z-ladder). `0.55` should be
|
||||
`var(--opacity-disabled)` (the canonical disabled opacity Dialog uses). `2rem` item-height is a raw rem
|
||||
where the list-surface size layer (`--list-*`, per project memory 2026-06-21) should drive it.
|
||||
- impact: opacity/size/z values that won't track theme/density/scale changes; the disabled opacity diverges
|
||||
from the canonical `--opacity-disabled` used elsewhere.
|
||||
- proposed-fix: `content-z` → canonical overlay z token; `item-disabled-opacity` → `var(--opacity-disabled)`;
|
||||
confirm `item-height` should bridge `--list-item-height-*`.
|
||||
- fix-status: open
|
||||
|
||||
### MEDIUM: Arrow-key NAVIGATION index math is untested (activation/groups/submenu ARE tested); jsdom-only <!-- id: dropdown-menu-004 -->
|
||||
- dimension: F
|
||||
- rule: dimension-F honesty check
|
||||
- location: dropdown-menu-provider.svelte.test.ts (6 tests)
|
||||
- evidence: the suite is the strongest of Batch 1 — it covers trigger toggle, item scoping + disabled policy,
|
||||
click/keyboard activation + close, checkbox/radio groups, submenu open (hover/click/ArrowRight), and
|
||||
group/separator a11y. But no test calls `Content.onkeydown`/`SubContent.onkeydown` with ArrowDown/ArrowUp/
|
||||
Home/End to exercise the navigation index math (dropdown-menu-001) or the loop boundary. `@vitest-environment
|
||||
jsdom` (:1) — `dom.focus` moves aren't faithful.
|
||||
- impact: the duplicated nav math + loop off-by ship unverified.
|
||||
- proposed-fix: add Content/SubContent navigation tests asserting the focused index after each arrow key incl.
|
||||
the loop wrap; add a client/Playwright test for real focus movement.
|
||||
- fix-status: open
|
||||
|
||||
### LOW: `open`/`close`/`commit-select` set state imperatively alongside a non-awaited `void trigger` (sequence not runtime-sequenced) <!-- id: dropdown-menu-005 -->
|
||||
- dimension: B
|
||||
- rule: two-moments doctrine + the Dialog pattern (events wired into the runtime so `trigger` sequences emit→handler)
|
||||
- location: runtime created with `{}` (no events map, :93); `handleOpen`/`handleClose` do
|
||||
`void this.runtime.trigger(...)` then set `open` synchronously (:116-140).
|
||||
- evidence: unlike Dialog (which wires `events: { open, close }` so `runtime.trigger` awaits emit before the
|
||||
handler), dropdown-menu sets `open` imperatively next to a fire-and-forget emit, so the morfo's
|
||||
`sequence: 'pre'` is not enforced by the runtime — it relies on Presence keeping content mounted during exit.
|
||||
- impact: works in practice (Presence + the 'pre' content still exists), and the non-await is likely a
|
||||
deliberate snappiness choice for a high-frequency surface; but it diverges from the canonical sequencing and
|
||||
makes `sequence: 'pre'` advisory here.
|
||||
- proposed-fix: either wire the open/close handlers into the runtime `events` map (Dialog parity) or document
|
||||
that menus intentionally don't await the emit. Decide once and apply to all menu-family components.
|
||||
- fix-status: open
|
||||
|
||||
## No-findings dimensions
|
||||
- **A17 (focus strategy):** consistent roving DOM focus — morfo declares no `aria-activedescendant`, provider
|
||||
uses real `dom.focus`. No mixing (contrast Combobox). Clean.
|
||||
- **C (selectors):** `getItems` composes attribute selectors with `.closest()` scoping (A10), no user-value
|
||||
interpolation. Clean.
|
||||
- **D (frontier, severe half):** provider imports only soma layers + morfo — zero eidos imports.
|
||||
- **A6 (cleanup):** typeahead `destroy` (:111), SubTrigger open timer (:1007), and `SafePolygon` (self-manages
|
||||
via an internal `$effect` + `dom.listen` disposers, verified safe-polygon.ts:209-215) all clean up. No leak.
|
||||
- **A (renderProps discipline):** every part spreads `renderProps()` and adds only soma-owned extras
|
||||
(data-state/tabindex/handlers) — no syncAttrs+manual double-write (contrast Dialog/Popover).
|
||||
- **A30:** SubTrigger ref mirror via `onRefChange`, not `$effect` (:988).
|
||||
- **SCOPE:** morfo scope correctly includes `eidos` — the only Batch-1 overlay without the scope-drift.
|
||||
|
||||
## Style observations (opinion, non-blocking)
|
||||
- Use dropdown-menu as the positive reference for: roving-focus consistency, uniform `renderProps()` (no
|
||||
double-write), self-managed layer cleanup, and correct `eidos`-in-scope declaration. Its main debts are
|
||||
shared, systemic ones (nav duplication, magic literals), not local mistakes.
|
||||
- The checkbox/radio silent-toggle (dropdown-menu-002) is the one genuinely local contract gap worth an
|
||||
explicit decision.
|
||||
@ -0,0 +1,35 @@
|
||||
# Audit: editable
|
||||
audit-version: 1
|
||||
audited-at: 2026-06-26
|
||||
scope: (SCOPE-DRIFT → SYS-1)
|
||||
method: adversarially-verified workflow (analyze → refute); HIGH/CRITICAL lead-verified. Clean-check pseudo-findings dropped. B4 ground-truth: segmented date/time/color-field register inputId via $effect (SYS-A30-EFFECT, no loop); number-field is the direct-assignment reference.
|
||||
provider: src/uix/soma/components/editable/editable-provider.svelte.ts
|
||||
field-family (A13/A24-26/A30): Not a field-family component; does not participate in Field composition. Optional hidden-input for form submission is correctly declared and configured with name, value, required, disabled propagation.
|
||||
|
||||
## Summary
|
||||
Counts (post-verification): CRITICAL 0 · HIGH 0 · MEDIUM 1 · LOW 0.
|
||||
|
||||
## Findings
|
||||
### MEDIUM: SYS-1 SCOPE-DRIFT — editable-001 <!-- id: editable-001 -->
|
||||
- dimension: Morfo
|
||||
- rule: SYS-1 SCOPE-DRIFT
|
||||
- location: src/uix/morfo/components/editable.ts:7
|
||||
- evidence: Morfo declares scope: ['soma', 'sema'] but eidos recipe directory exists at src/uix/eidos/components/editable/ with recipe tokens in src/uix/eidos/lib/recipes/base.ts:2724-2815
|
||||
- impact: The morfo scope should include 'eidos' to signal that the component has visual styling rules. Missing 'eidos' in scope creates confusion about the component's boundaries.
|
||||
- proposed-fix: Change morfo line 7 from scope: ['soma', 'sema'] to scope: ['soma', 'sema', 'eidos']
|
||||
- verify: [confirmed] Confirmed. editable.ts:7 declares `scope: ['soma', 'sema']` — verified by reading the file. The eidos layer materially implements this component: 13 files under src/uix/eidos/components/editable/ (incl. editable.css) plus a recipe block at src/uix/eidos/lib/recipes/base.ts:2724 (`editable: {`). The Layer field is documented in types.ts:799 as 'Layers that implement this component', so omitting 'eidos' while shipping a full eidos implementation is genuine contract drift — matches the SYS-1 scope-drift baseline (MEDIUM). Severity MEDIUM is correct: this is a contract/documentation inconsistency, not a behavior bug (the eidos CSS still loads and works regardless of the morfo scope array). Confirmed systemic, not editable-specific: combobox, color-picker, date-field, calendar all have eidos CSS files yet their morfo scope is `['soma', 'sema']` too (e.g. calendar.ts:7, combobox.ts:7, color-picker.ts:7). Proposed fix (add 'eidos' to the array) is correct.
|
||||
- fix-status: open
|
||||
|
||||
## No-findings dimensions
|
||||
Contract (2-of-3 parts rule), Contract (morfo as const satisfies), Contract (data-/aria- naming), Contract (declared events fired), Behavior (A30 id registration), Behavior (A6 frame disposal), Behavior (A31 O(N^2)), Behavior (keyboard APG match), Behavior (two-moments commit ordering), DOM-selector (CSS.escape), Frontier (soma->eidos import), Theming (9-role limit), Theming (magic z-index), Theming (magic opacity), Theming (magic literals px/rem/em/%)
|
||||
|
||||
## Theming facts (E-bis)
|
||||
- magic z-index: none
|
||||
- magic literals: none
|
||||
- undeclared parts: none
|
||||
- roles clean: true · variants clean: true
|
||||
|
||||
## Tests (F)
|
||||
- exists: true · env: jsdom
|
||||
- covers: startEdit with focus and state projection; preview activation (click/dblclick/focus modes); keyboard commit/cancel (Enter/Escape keys); blur control guards (focus retention on trigger buttons); readonly and disabled state enforcement; trigger label exposure and disabled states; value commit and revert semantics
|
||||
- untested: hidden-input form submission with name/required/disabled propagation; maxLength enforcement on input; autoResize field-sizing behavior; blur submit vs cancel mode differentiation; selectOnFocus text selection on edit start
|
||||
@ -0,0 +1,19 @@
|
||||
# Audit: fab
|
||||
audit-version: 1
|
||||
audited-at: 2026-06-26
|
||||
scope: eidos (eidos-only primitive)
|
||||
method: theming-coherence category sweep (composite-and-service), adversarially verified; roles + `:Morfo` lead-verified CLEAN across all eidos (no success/warning/danger/info; only alert-dialog uses `:Morfo`).
|
||||
|
||||
## Summary
|
||||
Counts (post-verification): CRITICAL 0 · HIGH 0 · MEDIUM 0 · LOW 0. Theming-clean.
|
||||
conformance: Contract: 'as const satisfies Morfo' ✓ | Scope: eidos ✓ | Roles: none (composes Button) ✓ | Variants: data-fab-size, data-placement, data-extended ✓ | All sizing via --fab-* recipe tokens ✓ | Em and env fallbacks OK ✓
|
||||
as-const: true · roles clean: true · variants clean: true · has-recipe: true
|
||||
|
||||
## Findings
|
||||
_No findings (theming-clean primitive)._
|
||||
|
||||
## Theming facts (E-bis)
|
||||
- magic literals: 1em @ src/uix/eidos/components/fab/fab.css:103 -> icon canonical scaling (acceptable) | 0px (env fallback) @ src/uix/eidos/components/fab/fab.css:121-134 -> safe-area contract (acceptable)
|
||||
- magic z-index: none
|
||||
- non-canonical names: none
|
||||
- roles clean: true · variants clean: true
|
||||
@ -0,0 +1,32 @@
|
||||
# Audit: feed
|
||||
audit-version: 1
|
||||
audited-at: 2026-06-26
|
||||
scope:
|
||||
method: adversarially-verified workflow (analyze → refute); HIGH lead-verified by direct read of the cited code. B5 ground-truth: the A31 O(N²) isSelected/isExpanded (.includes from a per-item $derived) is confirmed across listbox/grid-list/tree-view/tree-grid/tag-group (SYS-7); rovingTargetEl is correctly LIFTED everywhere (not A31); virtual-* use SvelteMap (A33-clean).
|
||||
provider: src/uix/soma/components/feed/feed-provider.svelte.ts
|
||||
|
||||
## Summary
|
||||
Counts (post-verification): CRITICAL 0 · HIGH 0 · MEDIUM 0 · LOW 0.
|
||||
|
||||
## Findings
|
||||
_No findings survived verification (clean component)._
|
||||
|
||||
## No-findings dimensions
|
||||
A, C, D, E, E-bis, G
|
||||
|
||||
## Theming facts (E-bis)
|
||||
- magic z-index: none
|
||||
- magic literals: feed.css:147 outline-offset: 2px (should use --focus-ring-offset token or define local token) | feed.css:200-201 inline-size: 0.75rem; block-size: 0.75rem; (should use design token like --space-3 or define --_feed-spinner-size) | feed.css:206 animation: feed-spin 0.7s linear infinite (0.7s should use --duration-* token) | feed.css:217 animation-duration: 4s (4s should use --duration-* token, e.g., --duration-slowest)
|
||||
- undeclared parts: none
|
||||
- roles clean: true · variants clean: true
|
||||
|
||||
## Tests (F)
|
||||
- exists: true · env: jsdom (@vitest-environment jsdom)
|
||||
- covers: keyboard navigation (PageUp/PageDown/Ctrl+Home/Ctrl+End); article position calculation (posinset/setsize); nested thread level inheritance; sentinel IntersectionObserver integration; fallback to feed.onLoadMore when sentinel doesn't have onIntersect
|
||||
- untested: Performance under high item count (30+ articles — A31 hazard not tested); Dynamic article addition/removal with auto-reposition; RTL direction handling (CSS has RTL rules but no test coverage); Multiple feeds on same page (context isolation)
|
||||
|
||||
## Style observations (non-blocking)
|
||||
- feed.css:42 data-block attribute selector: uses `data-block` instead of `data-block=''` on root for boolean flags — matches component pattern (feed.svelte:42 sets data-block={block ? '' : undefined})
|
||||
- feed.css:147 outline-offset: 2px differs from --focus-ring-offset (1px). Most components use --focus-ring-offset token; feed variant may be intentional for visual clarity but creates drift
|
||||
- feed.css:153 fallback font-weight: 600 in var() — fallback should not be used if token is guaranteed to exist; minor cosmetic issue
|
||||
- feed.css colors (lines 71-98) cascade via --_feed-accent / --_feed-accent-soft using 9 roles (primary, secondary, affirm, risk, threat, fulfill, loss, neutral). Follows project theming conventions correctly
|
||||
@ -0,0 +1,49 @@
|
||||
# Audit: field
|
||||
audit-version: 1
|
||||
audited-at: 2026-06-26
|
||||
scope:
|
||||
method: adversarially-verified workflow (analyze → refute); HIGH/CRITICAL lead-verified. Clean-check pseudo-findings dropped. B4 ground-truth: segmented date/time/color-field register inputId via $effect (SYS-A30-EFFECT, no loop); number-field is the direct-assignment reference.
|
||||
provider: G:\dev\svelte\vicen\src\uix\soma\components\field\field-provider.svelte.ts
|
||||
field-family (A13/A24-26/A30): {
|
||||
"A13": "Not applicable: field is the generic wrapper, not a form-participating field (number-field, tags-input, pin-input, color-field, css-field are form-fields). No hidden <input> rendered by field itself.",
|
||||
"A24": "Not applicable: field does not hold a value; child inputs (FieldInputProvider) handle value state. A24 'readonly-without-value warning' applies to child-level components.",
|
||||
"
|
||||
|
||||
## Summary
|
||||
Counts (post-verification): CRITICAL 0 · HIGH 0 · MEDIUM 2 · LOW 0.
|
||||
|
||||
## Findings
|
||||
### MEDIUM: SYS-1 scope-drift: morfo 'scope' declares ['soma'] but eidos directory exists at src/uix/e — field-002 <!-- id: field-002 -->
|
||||
- dimension: A
|
||||
- rule: SYS-1 scope-drift: morfo 'scope' declares ['soma'] but eidos directory exists at src/uix/eidos/components/field
|
||||
- location: src/uix/morfo/components/field.ts:7 vs src/uix/eidos/components/field/
|
||||
- evidence: fieldMorfo.scope = ['soma'], yet a full eidos/ recipe directory exists with field.css, field.svelte, and 10+ component files.
|
||||
- impact: MEDIUM (expected pattern per audit notes: 'No eidos scope-drift expected? (it has an eidos dir)'). Eidos wrapper components import soma correctly (all 11 .svelte files in eidos/field show 'import * as Field from $soma/components/field'), so the directory exists but is subordinate to soma — this is the normal composition pattern (eidos wraps soma visually, doesn't add a new layer to scope). No contract or behavior violation.
|
||||
- proposed-fix: This is systemic (SYS-1). Document as expected: field is a soma component with an eidos visual wrapper layer; scope correctly declares soma as the source of truth.
|
||||
- verify: [downgraded] Confirmed shape: fieldMorfo.scope = ['soma'] (field.ts:7) and a full eidos/components/field/ recipe dir exists. But SYS-1 scope-drift is the pattern where a soma component has an eidos visual layer not reflected in 'scope'; here every eidos wrapper composes soma (grep for 'import.*eidos' in field-provider returned NO matches — frontier clean). This is the normal eidos-wraps-soma composition, not a contract or behavior violation. Candidate's own impact text says 'expected pattern' / 'no contract or behavior violation'. Downgrade MEDIUM->LOW; record as systemic-expected, not a Field-specific defect.
|
||||
- fix-status: open
|
||||
|
||||
### MEDIUM: Tests: {kebab}-provider.svelte.test.ts must exist; coverage high-risk paths: segment commi — field-007 <!-- id: field-007 -->
|
||||
- dimension: F
|
||||
- rule: Tests: {kebab}-provider.svelte.test.ts must exist; coverage high-risk paths: segment commit/keyboard, hidden-input, Field inheritance, readonly-without-value warning, validation.
|
||||
- location: src/uix/soma/components/field/field-provider.svelte.test.ts (exists)
|
||||
- evidence: Test file exists and covers: (1) wires field parts, ARIA props, runtime attrs (lines 85-183); (2) updates input value unless disabled/readonly (185-219); (3) integrates with Form state and touched registry (221-256). Paths covered: ARIA wiring (aria-labelledby, aria-invalid, aria-describedby, aria-required, role=alert, aria-live), disabled/readonly guards, Form integration.
|
||||
- impact: Field-provider tests cover core A30 wiring and Form integration. NO COVERAGE for A24 'readonly-without-value warning' (field is generic, doesn't hold value itself — A24 applies to value-holding fields like number-field). Test count is lean (3 it() blocks) but focused on the unique field responsibilities.
|
||||
- proposed-fix: Optional: add a test verifying that child inputs read disabled/readonly/required/invalid from the parent provider context. Current tests verify runtime wiring but not snippet props propagation. Consider: 'reads disabled/readonly/required/invalid from parent' to guard inheritance.
|
||||
- verify: [uncertain] Test exists (field-provider.svelte.test.ts, 256 lines, jsdom env, 3 it() blocks). Coverage is genuinely good for the field's unique responsibilities: ARIA wiring incl aria-labelledby/aria-invalid/aria-describedby join (141-179), disabled/readonly input guards (185-219), Form integration + touched registry + unregister-on-cleanup (221-255). The real, narrow gap the candidate names is legitimate: no explicit assertion that descendant inputs READ disabled/readonly/required/invalid from the parent context (snippetProps inheritance is computed but only the FieldInputProvider's own props are asserted, e.g. 'required: true' at 146 comes from the input part). Holds at MEDIUM as a low-risk test-coverage gap, not a confirmed bug. F: provider test PRESENT, env jsdom (SYS-3 noted).
|
||||
- fix-status: open
|
||||
|
||||
## No-findings dimensions
|
||||
A (contract: morfo scope/naming/parts), C (DOM selector), G (redundancy)
|
||||
|
||||
## Theming facts (E-bis)
|
||||
- magic z-index: none
|
||||
- magic literals: none
|
||||
- undeclared parts: none
|
||||
- roles clean: true · variants clean: true
|
||||
- label-font (one step below input?): Clean: label font-size is calc(control-font - 2px) per base.ts recipe. Label renders at ONE step below input per CSS line 154. Scales correctly with size prop (xs/sm/md/lg/xl).
|
||||
|
||||
## Tests (F)
|
||||
- exists: true · env: jsdom
|
||||
- covers: ARIA wiring (aria-labelledby, aria-invalid, aria-describedby, aria-required, role, aria-live); input value update with disabled/readonly guards; Form Provider integration (registerField, unregisterField, getFieldState, touched registry)
|
||||
- untested: Child inheritance of disabled/readonly/required/invalid from parent context (snippet props verified, but context propagation to child .svelte components not exercised); Cleanup of id registration on unmount (tested via effect cleanup but not via explicit unmount/remount cycle)
|
||||
@ -0,0 +1,44 @@
|
||||
# Audit: file-upload
|
||||
audit-version: 1
|
||||
audited-at: 2026-06-26
|
||||
scope: SCOPE_DRIFT (SCOPE-DRIFT → SYS-1)
|
||||
method: adversarially-verified workflow; HIGH lead-verified. B7 ground-truth: Toolbar A35 + Form A36 incidents STAYED FIXED (untrack present); toast/clipboard/drag-drop use uix.timers (no A6 leak); button is clean (its archetype finding was inert — Spinner not runtime-registered); data-size/data-shape are eidos visual attrs (not contract violations).
|
||||
provider: src/uix/soma/components/file-upload/file-upload-provider.svelte.ts
|
||||
cleanup-audit (A6/A35/A36): A6 LISTENERS & CLEANUP VERIFIED:
|
||||
$effect (line 99-110): soma.dom.listen(doc, 'dragover') + listen(doc, 'drop') → cleanupDragOver() + cleanupDrop() returned in cleanup function ✓
|
||||
$effect (line 637-647, ItemPreviewProvider): URL.createObjectURL() → URL.revokeObjectURL(url) returned in cleanup function
|
||||
|
||||
## Summary
|
||||
Counts (post-verification): CRITICAL 0 · HIGH 0 · MEDIUM 1 · LOW 0.
|
||||
|
||||
## Findings
|
||||
### MEDIUM: SYS-1: morfo scope must include all layer dirs that exist — file-upload-001 <!-- id: file-upload-001 -->
|
||||
- dimension: Contract
|
||||
- rule: SYS-1: morfo scope must include all layer dirs that exist
|
||||
- location: src/uix/morfo/components/file-upload.ts:7
|
||||
- evidence: morfo declares scope: ['soma', 'sema'] but src/uix/eidos/components/file-upload/ directory exists with 13 .svelte files (file-upload.svelte, file-upload-dropzone.svelte, file-upload-item.svelte, etc.). Eidos layer is implemented but not declared in morfo scope.
|
||||
- impact: Scope mismatch signals incomplete morfo contract. Consumers expect morfo scope to list all layers present. This is a systemic architectural issue affecting component layering.
|
||||
- repro: grep -n 'scope:' src/uix/morfo/components/file-upload.ts; ls -d src/uix/eidos/components/file-upload
|
||||
- proposed-fix: Update line 7 from scope: ['soma', 'sema'] to scope: ['soma', 'sema', 'eidos']
|
||||
- verify: [confirmed] CONFIRMED. morfo src/uix/morfo/components/file-upload.ts:7 declares `scope: ['soma', 'sema']`. The eidos layer is fully implemented: src/uix/eidos/components/file-upload/ holds 13 .svelte wrappers + file-upload.css (foundation reading `--file-upload-*` recipe tokens, e.g. `--_file-upload-gap: var(--file-upload-gap-md)`) + types.ts + README.md, AND a recipe exists at src/uix/eidos/lib/recipes/base.ts:2369 ('file-upload': {...} with per-color solid/track tokens). Eidos is present but omitted from morfo scope → SYS-1 scope-drift. MEDIUM is correct per baseline. Note: the candidate's repro `grep -n 'scope:'` is fine, but a `grep 'file-upload:'` on base.ts misses the recipe because the key is tab-indented — the recipe DOES exist.
|
||||
- fix-status: open
|
||||
|
||||
## No-findings dimensions
|
||||
Behavior (A35/A36 loops: no per-item $effect reading opts.ref.current AND writing provider state; no microtask-mediated async without untrack), Behavior (A6 cleanup: all listeners via soma.dom.listen() return disposers; URL.createObjectURL/revokeObjectURL pair in $effect return), Behavior (A33: no $state(new Map/Set); A31: no O(N²) includes checks), Behavior (A30: hiddenInputId set directly in constructor line 444, not in $effect; hiddenInputRef via onRefChange callback line 440-442), Behavior (A15 GESTURE: no drag-drop/cropper gesture logic; dropzone uses native drag events with proper preventDefault), Behavior (LIVE REGIONS: signal-warn-reject dispatches with message; untilFix persistence documented line 191-194), Contract (2-of-3 rule: morfo + soma + eidos all present; all 13 morfo parts registered via runtime.part()), Contract (data naming: all data-* attributes are kebab-cased; no data-soma-* violations), Contract (morfo validation: 'as const satisfies Morfo' present; all aria and data attrs declared), Frontier (no soma->eidos imports; eidos correctly imports from soma), Frontier (no syncAttrs double-write; parts use renderProps() correctly), Theming (focus rings use --focus-ring-width and --focus-ring-color CSS vars; no hardcoded hex colors or magic px values; no z-index magic), DOM selectors (no querySelector with interpolated consumer values; no direct document/window; uses soma.dom.getDocument()), Passive roles (ItemProgress role='progressbar' declares no component-level events; correct), Archetype validation (Item archetype:'item' is correct; CSS does not assign cursor:pointer or interactive styling to display items), Tests (provider.svelte.test.ts covers rejection logic, maxFiles caps, dropzone interactions, item metadata, removal, progress, clear state; jsdom environment)
|
||||
|
||||
## Theming facts (E-bis)
|
||||
- magic z-index: none
|
||||
- magic literals: none
|
||||
- undeclared parts: none
|
||||
- roles clean: true · variants clean: true
|
||||
|
||||
## Tests (F)
|
||||
- exists: true · env: vitest/jsdom
|
||||
- covers: accept/reject logic (FILE_INVALID_TYPE, FILE_TOO_LARGE, FILE_TOO_SMALL, FILE_EXISTS, FILE_CUSTOM_ERROR); validation hook callback; maxFiles cap enforcement; disabled state propagation; required/invalid/empty data attrs; dropzone dragenter/dragover/dragleave/drop handlers; hidden input onchange binding; label for= linking; item name/size/progress display; item removal; clear trigger disabled when empty; progress clamping 0-100
|
||||
- untested: signal-warn-reject dispatch with live-region message (signal event routing not tested); preventDocumentDrop listener cleanup on unmount; ItemPreview URL.createObjectURL/revokeObjectURL lifecycle across file changes; paste event on dropzone; keyboard Enter/Space on dropzone (role='button'); multiple=false single-file replacement behavior
|
||||
|
||||
## Style observations (non-blocking)
|
||||
- Signal-warn-reject event (line 210) correctly uses 'post' sequence and passes message for live-region announce per a11ySemantic.requiresPersistentTrace + reducedMotionFallback='text'
|
||||
- A30 pattern correctly applied: hiddenInputId set direct in constructor (line 444), hiddenInputRef via onRefChange callback (line 440-442) — no $effect overhead
|
||||
- untilFix persistence clearly documented (line 191-194): clearTarget(provider) on each addFiles call resets signal before evaluating new rejections
|
||||
- Item archetype correctly declared as 'item' (not 'option'/'interactive'); CSS treats item as display container (no cursor:pointer, interactive children are explicit buttons)
|
||||
@ -0,0 +1,19 @@
|
||||
# Audit: flex
|
||||
audit-version: 1
|
||||
audited-at: 2026-06-26
|
||||
scope: eidos (eidos-only primitive)
|
||||
method: theming-coherence category sweep (layout), adversarially verified; roles + `:Morfo` lead-verified CLEAN across all eidos (no success/warning/danger/info; only alert-dialog uses `:Morfo`).
|
||||
|
||||
## Summary
|
||||
Counts (post-verification): CRITICAL 0 · HIGH 0 · MEDIUM 0 · LOW 0. Theming-clean.
|
||||
conformance: Contract OK, composes through Box via data-flex, gap defaults via Box fallback, data-direction exposed for selector rules.
|
||||
as-const: true · roles clean: true · variants clean: true · has-recipe: true
|
||||
|
||||
## Findings
|
||||
_No findings (theming-clean primitive)._
|
||||
|
||||
## Theming facts (E-bis)
|
||||
- magic literals: none
|
||||
- magic z-index: none
|
||||
- non-canonical names: none
|
||||
- roles clean: true · variants clean: true
|
||||
@ -0,0 +1,53 @@
|
||||
# Audit: float-panel
|
||||
audit-version: 1
|
||||
audited-at: 2026-06-26
|
||||
scope: ['soma', 'sema'] (SCOPE-DRIFT → SYS-1)
|
||||
method: adversarially-verified workflow (analyze → refute); HIGH/CRITICAL personally re-verified against cited code by the lead.
|
||||
provider: src/uix/soma/components/float-panel/float-panel-provider.svelte.ts
|
||||
|
||||
## Summary
|
||||
Counts (post-verification): CRITICAL 0 · HIGH 0 · MEDIUM 2 · LOW 0.
|
||||
systemic hits: SYS-1; SYS-2; SYS-3; SYS-5; SYS-6.
|
||||
|
||||
## Findings
|
||||
### MEDIUM: SYS-1 — float-panel-001 <!-- id: float-panel-001 -->
|
||||
- dimension: E-bis
|
||||
- rule: SYS-1
|
||||
- location: src/uix/morfo/components/float-panel.ts:7
|
||||
- evidence: scope: ['soma', 'sema'], but eidos recipe directory exists at src/uix/eidos/components/float-panel/
|
||||
- impact: Morfo contract omits eidos in scope declaration despite full eidos implementation present; creates contract-documentation drift.
|
||||
- repro: List the morfo scope and observe eidos directory exists but not declared.
|
||||
- proposed-fix: Change morfo scope to ['soma', 'sema', 'eidos'] to match the presence of src/uix/eidos/components/float-panel/
|
||||
- verify: [confirmed] morfo line 7 literally reads `scope: ['soma', 'sema'],` while the eidos recipe dir exists (Glob returned src/uix/eidos/components/float-panel/ with float-panel.css, types.ts, 14 .svelte parts, recipe block at base.ts:1076). Genuine SYS-1 scope-drift: a complete eidos implementation is present but 'eidos' is omitted from the morfo scope tuple. HIGH per SYS-1 (contract/validator-catchable). Confirmed.
|
||||
- fix-status: open
|
||||
|
||||
### MEDIUM: SYS-3 — float-panel-005 <!-- id: float-panel-005 -->
|
||||
- dimension: F
|
||||
- rule: SYS-3
|
||||
- location: src/uix/soma/components/float-panel/float-panel-provider.svelte.test.ts:1 (jsdom environment only)
|
||||
- evidence: Test file runs in jsdom (@vitest-environment jsdom); keyboard interaction tests at lines 216-247 only cover basic ArrowRight/ArrowDown moves, not Home/End/PageUp/PageDown/resize keyboard, and no client/Playwright test exists.
|
||||
- impact: Keyboard grab-mode (move + resize) has complex state transitions (kbToggleMove, kbCancelMove, kbMove with Home/End logic, and kbResize equivalents) exercised only on easy paths (arrow keys) in jsdom. Full keyboard interactivity (especially Home/End reaching bounds and PageUp/PageDown step logic) untested; resize keyboard path completely absent from test coverage.
|
||||
- repro: Run tests; no keyboard resize tests exist. grep test file for 'kbResize' (absent). Manually verify Home/End position clamping with a real browser.
|
||||
- proposed-fix: Add integration tests covering (a) keyboard move Home/End/PageUp/PageDown reaching bounds, (b) resize keyboard equivalents, (c) stage transitions cancelling grab-mode, in a client environment (Playwright or @vitest-environment happy-dom/puppeteer).
|
||||
- verify: [downgraded] The factual premise (jsdom) is WRONG, so the SYS-3 rule citation is misapplied — but a real coverage gap remains. The file is `float-panel-provider.svelte.test.ts`, which matches the CLIENT project glob `src/**/*.svelte.{test,spec}.{js,ts}` (vite.config.ts:72) and runs in headless chromium (browser:{instances:[{browser:'chromium'}]}, line 67-70) — NOT jsdom. The `// @vitest-environment jsdom` line at test:1 is inert for browser-mode tests; the test itself confirms (line 219-220 'this .svelte.test runs in a real browser whose width varies', line 117 reads live window.innerWidth). So it is NOT a SYS-3 jsdom-only case. The valid kernel: keyboard RESIZE (kbToggleResize/kbResize) is entirely untested (grep test for 'kbResize'/'kbToggleResize' = absent) and keyboard MOVE Home/End/PageUp/PageDown + bound-hit announce paths are untested (only ArrowRight + Shift+ArrowDown + cancel + maximized-guard exercised, lines 231-243). Real but not SYS-3; downgrade to MEDIUM coverage-gap with corrected rule (behavioral test gap, not jsdom-only).
|
||||
- fix-status: open
|
||||
|
||||
## No-findings dimensions
|
||||
A, C, G
|
||||
|
||||
## Theming facts (E-bis)
|
||||
- magic z-index: none
|
||||
- magic literals: opacity: 1; at line 260, 265 (should map to --opacity-* token) | scale: 1; at line 344, 349 (should map to --scale-reset or similar) | line-height: 1; at line 189 (should be canonical or documented)
|
||||
- undeclared parts: data-animation-style set by eidos but not declared in morfo
|
||||
- roles clean: true · variants clean: true
|
||||
|
||||
## Tests (F)
|
||||
- exists: true · env: jsdom
|
||||
- covers: position seeding and clamping; size min/max clamping; anchor positioning (side=bottom, align=start); dismissal via dismissWith; stage toggling and drag block while maximized; keyboard grab-mode move (basic arrows only)
|
||||
- untested: keyboard move Home/End/PageUp/PageDown; keyboard resize toggle and all keyboard resize keys; stage transitions cancelling keyboard grab-mode; pointer drag gesture lifecycle (startDrag → pointermove → endDrag); pointer resize gesture lifecycle; multi-panel stacking (bring-to-front during gestures); all dismiss causes (save, cancel, fail, dismiss-outside) semantic payloads; focus return to trigger on close; ResizeGrip focus and keyboard resize affordance
|
||||
|
||||
## Style observations (non-blocking)
|
||||
- CSS composition is clean: drag position uses distinct translate property (not affected by state animations); scale-fade animation preset only touches scale+opacity, leaving translate free for drag.
|
||||
- Gesture sequencing is well-designed: dragging/resizing state lifted on pointerdown (before move), so shadow transitions before motion begins; live position/size applied imperatively per pointermove, committed to state on release.
|
||||
- Keyboard grab-mode architecture is clear: pre-grab snapshot enables Escape revert; axis tracking (kbAxis='x'/'y') disambiguates Home/End targets; debounced announcements prevent spam.
|
||||
- Focus management mirrors Popover: non-modal panel doesn't trap; Dismissal layer's isValidEvent returns false so interior clicks don't trigger dismiss; focus return to trigger is deferred to FocusScope layer (not verified in this audit).
|
||||
@ -0,0 +1,19 @@
|
||||
# Audit: float
|
||||
audit-version: 1
|
||||
audited-at: 2026-06-26
|
||||
scope: eidos (eidos-only primitive)
|
||||
method: theming-coherence category sweep (layout), adversarially verified; roles + `:Morfo` lead-verified CLEAN across all eidos (no success/warning/danger/info; only alert-dialog uses `:Morfo`).
|
||||
|
||||
## Summary
|
||||
Counts (post-verification): CRITICAL 0 · HIGH 0 · MEDIUM 0 · LOW 0. Theming-clean.
|
||||
conformance: Contract OK, gap defaults to --space-3 via recipe, data-side presence flag for end placement.
|
||||
as-const: true · roles clean: true · variants clean: true · has-recipe: true
|
||||
|
||||
## Findings
|
||||
_No findings (theming-clean primitive)._
|
||||
|
||||
## Theming facts (E-bis)
|
||||
- magic literals: var(--space-3) @ src/uix/eidos/lib/recipes/base.ts:3994 -> canonical token
|
||||
- magic z-index: none
|
||||
- non-canonical names: none
|
||||
- roles clean: true · variants clean: true
|
||||
@ -0,0 +1,43 @@
|
||||
# Audit: form
|
||||
audit-version: 1
|
||||
audited-at: 2026-06-26
|
||||
scope: (SCOPE-DRIFT → SYS-1)
|
||||
method: adversarially-verified workflow; HIGH lead-verified. B7 ground-truth: Toolbar A35 + Form A36 incidents STAYED FIXED (untrack present); toast/clipboard/drag-drop use uix.timers (no A6 leak); button is clean (its archetype finding was inert — Spinner not runtime-registered); data-size/data-shape are eidos visual attrs (not contract violations).
|
||||
provider: src/uix/soma/components/form/form-provider.svelte.ts
|
||||
cleanup-audit (A6/A35/A36): No timers/listeners/observers/ResizeObserver/IntersectionObserver/MutationObserver/setPointerCapture found in form component. No $effect blocks that require disposal. No $effect.root calls with missing disposers. A35/A36 loop check: No per-item $effect reading opts.ref + writing provider state detec
|
||||
|
||||
## Summary
|
||||
Counts (post-verification): CRITICAL 0 · HIGH 0 · MEDIUM 1 · LOW 0.
|
||||
|
||||
## Findings
|
||||
### MEDIUM: SYS-1 scope-drift: an eidos recipe dir + recipe entry exist for the component, but the mor — FORM-SYS1-SCOPE-DRIFT <!-- id: FORM-SYS1-SCOPE-DRIFT -->
|
||||
- dimension: A
|
||||
- rule: SYS-1 scope-drift: an eidos recipe dir + recipe entry exist for the component, but the morfo `scope` omits 'eidos'. The morfo scope must declare every layer that materializes against its contract.
|
||||
- location: src/uix/morfo/components/form.ts:7 (scope: ['soma', 'sema']) vs eidos dir src/uix/eidos/components/form/ + recipe src/uix/eidos/lib/recipes/base.ts:836 (form:) + form.css:68/77/81 selecting [data-form][data-pending], [data-form][data-invalid], [data-form-error-summary]
|
||||
- evidence: form.ts:7 `scope: ['soma', 'sema']` — 'eidos' absent. Yet src/uix/eidos/components/form/form.css line 68 `[data-form][data-pending] {`, line 77 `[data-form][data-invalid] {`, line 81 `[data-form-invalid] [data-form-error-summary]` all select morfo-emitted data-attrs, and base.ts:836 declares a `form:` recipe. The eidos layer fully consumes the contract while the morfo scope denies eidos is a consumer.
|
||||
- impact: The morfo's declared scope under-reports its real consumers. Any scope-driven tooling (coverage checks, layer-presence assertions) will treat form as having no eidos materialization, masking drift if the eidos selectors fall out of sync with the morfo's emitted attrs.
|
||||
- proposed-fix: Add 'eidos' to formMorfo.scope -> scope: ['soma', 'sema', 'eidos'].
|
||||
- verify: [verifier-added] added by adversarial verify pass
|
||||
- fix-status: open
|
||||
|
||||
## No-findings dimensions
|
||||
A Contract(morfo): MUST be 'as const satisfies Morfo', Parts registered via runtime.part() exist in morfo & vice-versa, 2-of-3 rule, data-{c}/-{part} naming, aria/data emitted in morfo, A35: per-item $effect reading opts.ref and writing provider state, A36: async write + read-back without untrack, A33: $state(new Map/Set) not reactive, A31: per-item $derived reading global state via provider method, A6: setTimeout/setInterval/listener/ResizeObserver/IntersectionObserver/MutationObserver disposal, A15: GESTURE drag-drop/cropper cleanup, LIVE REGIONS: announce via uix.announce, A30: child->parent id registration DIRECT, DOM-selector: querySelector safe framework values, SOMA imports eidos, data-size/color/variant eidos VISUAL attrs, syncAttrs double-write, TSC tokens, Theming: 9 roles, canonical vars, focus-ring canonical usage, archetype item/option on non-interactive parts, Tests: provider test exists, jsdom env, Redundancy: re-implemented patterns
|
||||
|
||||
## Theming facts (E-bis)
|
||||
- magic z-index: none
|
||||
- magic literals: 10%, 70%, 48% in color-mix (form.css:198,206,256) - design system blending factors, intentional
|
||||
- undeclared parts: none
|
||||
- roles clean: true · variants clean: true
|
||||
|
||||
## Tests (F)
|
||||
- exists: true · env: jsdom
|
||||
- covers: handleSubmit with invalid validation; first invalid field focus; submit/reset/error-summary state projection; error count and field error list; submit count tracking; disabled state per form state
|
||||
- untested: Multiple submit attempts with different validation states; Custom onValidSubmit/onInvalidSubmit callbacks; Schema override after form creation; Form created with prebuilt form instance; validationBehaviour parameter (progressive/onSubmit/onBlur/onChange); Async validation in onValidSubmit; noValidate attribute emission; Reset button onclick handler behavior
|
||||
|
||||
## Style observations (non-blocking)
|
||||
- form.css uses canonical CSS variables throughout (--form-*, --color-* roles, --focus-ring-*)
|
||||
- form.css respects disabled state with canonical --form-disabled-opacity variable
|
||||
- form auto-fields parts styled as containers/groups, not interactive (no cursor/hover/select pull)
|
||||
- error-summary uses --color-risk-* role variables appropriately for risk signaling
|
||||
- button action states properly separated (hover, focus-visible, disabled)
|
||||
- Layout uses flex/grid with responsive gap sizing via CSS custom properties
|
||||
@ -0,0 +1,19 @@
|
||||
# Audit: format-date
|
||||
audit-version: 1
|
||||
audited-at: 2026-06-26
|
||||
scope: eidos (eidos-only primitive)
|
||||
method: theming-coherence category sweep (composite-and-service), adversarially verified; roles + `:Morfo` lead-verified CLEAN across all eidos (no success/warning/danger/info; only alert-dialog uses `:Morfo`).
|
||||
|
||||
## Summary
|
||||
Counts (post-verification): CRITICAL 0 · HIGH 0 · MEDIUM 0 · LOW 0. Theming-clean.
|
||||
conformance: Contract: 'as const satisfies Morfo' ✓ | Scope: eidos ✓ | Service component (no CSS, no behavior) ✓ | No variants/roles ✓
|
||||
as-const: true · roles clean: true · variants clean: true · has-recipe: false
|
||||
|
||||
## Findings
|
||||
_No findings (theming-clean primitive)._
|
||||
|
||||
## Theming facts (E-bis)
|
||||
- magic literals: none
|
||||
- magic z-index: none
|
||||
- non-canonical names: none
|
||||
- roles clean: true · variants clean: true
|
||||
@ -0,0 +1,19 @@
|
||||
# Audit: format-number
|
||||
audit-version: 1
|
||||
audited-at: 2026-06-26
|
||||
scope: eidos (eidos-only primitive)
|
||||
method: theming-coherence category sweep (composite-and-service), adversarially verified; roles + `:Morfo` lead-verified CLEAN across all eidos (no success/warning/danger/info; only alert-dialog uses `:Morfo`).
|
||||
|
||||
## Summary
|
||||
Counts (post-verification): CRITICAL 0 · HIGH 0 · MEDIUM 0 · LOW 0. Theming-clean.
|
||||
conformance: Contract: 'as const satisfies Morfo' ✓ | Scope: eidos ✓ | Service component (no CSS, no behavior) ✓ | No variants/roles ✓
|
||||
as-const: true · roles clean: true · variants clean: true · has-recipe: false
|
||||
|
||||
## Findings
|
||||
_No findings (theming-clean primitive)._
|
||||
|
||||
## Theming facts (E-bis)
|
||||
- magic literals: none
|
||||
- magic z-index: none
|
||||
- non-canonical names: none
|
||||
- roles clean: true · variants clean: true
|
||||
@ -0,0 +1,55 @@
|
||||
# Audit: grid-list
|
||||
audit-version: 1
|
||||
audited-at: 2026-06-26
|
||||
scope: (SCOPE-DRIFT → SYS-1)
|
||||
method: adversarially-verified workflow (analyze → refute); HIGH lead-verified by direct read of the cited code. B5 ground-truth: the A31 O(N²) isSelected/isExpanded (.includes from a per-item $derived) is confirmed across listbox/grid-list/tree-view/tree-grid/tag-group (SYS-7); rovingTargetEl is correctly LIFTED everywhere (not A31); virtual-* use SvelteMap (A33-clean).
|
||||
provider: G:/dev/svelte/vicen/src/uix/soma/components/grid-list/grid-list-provider.svelte.ts
|
||||
reactivity (A31/A33/A35): GRID-LIST-A31-HAZARD: Per-row `isSelected = $derived.by(() => this.provider.isSelected(this.opts.value.current))` at line 493 and per-checkbox `isChecked = $derived.by()` at line 612 call `isSelected(value)` method (line 150-152) which does `this.opts.value.current.includes(value)` — array linear scan O(N) per row/checkbox. For N rows, total O(N²) on any selection mutation. Lifted fix: move to pro
|
||||
|
||||
## Summary
|
||||
Counts (post-verification): CRITICAL 0 · HIGH 2 · MEDIUM 1 · LOW 0.
|
||||
|
||||
## Findings
|
||||
### HIGH: A31: Per-item derived calling provider method that reads global state causes O(N²) re-runs — grid-list-001 <!-- id: grid-list-001 -->
|
||||
- dimension: B - Behavior (A31 reactivity)
|
||||
- rule: A31: Per-item derived calling provider method that reads global state causes O(N²) re-runs on any mutation
|
||||
- location: G:/dev/svelte/vicen/src/uix/soma/components/grid-list/grid-list-provider.svelte.ts:493
|
||||
- evidence: GridListRowProvider line 493: `readonly isSelected = $derived.by(() => this.provider.isSelected(this.opts.value.current))` calls line 150-152: `isSelected(value: string): boolean { return this.opts.value.current.includes(value); }` - performs linear array scan per row, every row re-runs on any selection change
|
||||
- impact: For N rows, each row's derived calls O(N) .includes() method → O(N²) total. Works fine under ~15 items, becomes noticeably slow at 30+ items.
|
||||
- proposed-fix: Lift a `Set<string>` on GridListProvider: `readonly selectedSet = $derived(() => new Set(this.opts.value.current))`. In isSelected, use `selectedSet.has(value)` for O(1). Update per-row derived to use the pre-computed set.
|
||||
- verify: [confirmed] Confirmed A31 O(N²). Line 493: `readonly isSelected = $derived.by(() => this.provider.isSelected(this.opts.value.current))` per GridListRowProvider calls provider.isSelected (lines 150-152: `return this.opts.value.current.includes(value)`), a linear array scan reading GLOBAL selection state. Every row's derived depends on `this.opts.value.current`; selection replaces the array (line 172 `this.opts.value.current = next`), invalidating ALL row deriveds, each re-running O(N) .includes() → O(N²). The provider DID lift a Set for the roving target (line 144 `const selected = new Set(this.opts.value.current)` inside the single `rovingTargetEl` derivation) but did NOT lift one for the per-row isSelected path — fix is real and applicable. Matches SYS-7 / documented Listbox-rovingTarget incident.
|
||||
- fix-status: open
|
||||
|
||||
### HIGH: A31: Per-checkbox derived calling provider method that reads global state — grid-list-002 <!-- id: grid-list-002 -->
|
||||
- dimension: B - Behavior (A31 reactivity)
|
||||
- rule: A31: Per-checkbox derived calling provider method that reads global state
|
||||
- location: G:/dev/svelte/vicen/src/uix/soma/components/grid-list/grid-list-provider.svelte.ts:612-614
|
||||
- evidence: GridListSelectionCheckboxProvider line 612-614: `readonly isChecked = $derived.by(() => { if (!this.rowValue) return false; return this.provider.isSelected(this.rowValue); })` - calls the O(N) isSelected method. Also line 636 in props derived repeats the same call.
|
||||
- impact: Per-checkbox derived re-runs and calls isSelected (array .includes), multiplied by number of checkboxes in the list.
|
||||
- proposed-fix: Same fix as grid-list-001: consume the lifted selectedSet instead of calling isSelected.
|
||||
- verify: [confirmed] Confirmed A31 on the checkbox part. Lines 612-615: `readonly isChecked = $derived.by(() => { if (!this.rowValue) return false; return this.provider.isSelected(this.rowValue) })` calls the same O(N) linear-scan isSelected (line 151). Additionally line 636, inside the `props` $derived (632-651): `const checked = this.rowValue ? this.provider.isSelected(this.rowValue) : false` — a SECOND read of the linear scan per checkbox. Both deriveds depend on `value.current` via isSelected and re-run for every checkbox on any selection mutation. Same lifted-Set fix as 001. HIGH per SYS-7.
|
||||
- fix-status: open
|
||||
|
||||
### MEDIUM: SYS-1: Scope-drift when eidos recipe directory exists but 'eidos' is omitted from morfo.sc — grid-list-003 <!-- id: grid-list-003 -->
|
||||
- dimension: A - Contract (morfo scope)
|
||||
- rule: SYS-1: Scope-drift when eidos recipe directory exists but 'eidos' is omitted from morfo.scope
|
||||
- location: G:/dev/svelte/vicen/src/uix/morfo/components/grid-list.ts:7
|
||||
- evidence: Morfo declares `scope: ['soma', 'sema']` (line 7) but full eidos directory exists at G:/dev/svelte/vicen/src/uix/eidos/components/grid-list/ with grid-list.svelte (lines 1-58), grid-list.css (lines 1-236), types.ts, and child components (grid-list-row.svelte, grid-list-cell.svelte, grid-list-selection-checkbox.svelte). Eidos scope is present but not declared.
|
||||
- impact: Inconsistency in declared vs actual scope. Eidos components are real and functional but not flagged in morfo. May confuse consumers about component structure.
|
||||
- proposed-fix: Either (1) add 'eidos' to morfo.scope: `scope: ['soma', 'sema', 'eidos']`, OR (2) remove the eidos directory if GridList is soma-only. Recommend option 1 since eidos wrapper clearly exists and re-exports soma.
|
||||
- verify: [confirmed] Confirmed SYS-1 scope-drift. grid-list.ts:7 declares `scope: ['soma', 'sema']` but a full eidos directory exists: grid-list.svelte, grid-list.css, types.ts, index.ts, grid-list-row.svelte, grid-list-cell.svelte, grid-list-selection-checkbox.svelte (verified via glob). The eidos wrapper is real and functional yet 'eidos' is omitted from the morfo scope. This is the documented systemic SYS-1 pattern (siblings command/combobox/date-picker show the same omission). MEDIUM per baseline. No recipe entry in recipes/base.ts (grep `grid-list:`/`gridList` returned no match), so the eidos surface is CSS-only — adding 'eidos' to scope is the correct alignment.
|
||||
- fix-status: open
|
||||
|
||||
## No-findings dimensions
|
||||
C - DOM-selector (CSS.escape used correctly on line 184 for consumer value), D - Frontier (no soma→eidos imports; eidos→soma normal), E - TSC/Theming (--control-height-*, --font-size-*, --space-*, --color-* all canonical; no magic hex or z-index), F - Tests (test environment jsdom is acceptable for this DOM-interactive pattern; keyboard nav, selection, typeahead tested), G - Redundancy (no detected duplication in selection/keyboard navigation logic), E-bis - No A33 ($state(new Map/Set)), A30 (id registration is direct field, not $effect), A6 (listeners cleaned in $effect.root), A14 (roving tabindex correctly implements exactly one tabindex=0), A34 DOM-TOPOLOGY (require() pattern not used)
|
||||
|
||||
## Theming facts (E-bis)
|
||||
- magic z-index: none
|
||||
- magic literals: 0.875rem (checkbox size at grid-list.css:196 - specific rem for UI element size, acceptable) | 60vh in min() at grid-list.css:27 - viewport unit for max-height, not a magic z-index
|
||||
- undeclared parts: none
|
||||
- roles clean: true · variants clean: true
|
||||
|
||||
## Tests (F)
|
||||
- exists: true · env: jsdom (@vitest-environment jsdom at grid-list-provider.svelte.test.ts:1)
|
||||
- covers: row selection and deselection via click; shift+click range selection; ctrl/meta+click toggle; keyboard navigation (ArrowDown, ArrowUp, Home, End, PageUp, PageDown); row focusing and roving tabindex; typeahead character matching; select-all (Ctrl+A) and clear (Escape); checkbox row association and toggle; cell-level horizontal arrow navigation (ArrowRight/Left); merging Field provider flags (disabled, readonly, required, invalid)
|
||||
- untested: O(N²) performance regression with 100+ items (no perf test); RTL keyboard navigation specifics (tests hardcoded ltr); Typeahead buffer timer cleanup edge-cases
|
||||
@ -0,0 +1,19 @@
|
||||
# Audit: grid
|
||||
audit-version: 1
|
||||
audited-at: 2026-06-26
|
||||
scope: eidos (eidos-only primitive)
|
||||
method: theming-coherence category sweep (layout), adversarially verified; roles + `:Morfo` lead-verified CLEAN across all eidos (no success/warning/danger/info; only alert-dialog uses `:Morfo`).
|
||||
|
||||
## Summary
|
||||
Counts (post-verification): CRITICAL 0 · HIGH 0 · MEDIUM 0 · LOW 0. Theming-clean.
|
||||
conformance: Contract OK, composes through Box via data-grid, item placement props (gridColumn/Row/Area) declared on Box child.
|
||||
as-const: true · roles clean: true · variants clean: true · has-recipe: true
|
||||
|
||||
## Findings
|
||||
_No findings (theming-clean primitive)._
|
||||
|
||||
## Theming facts (E-bis)
|
||||
- magic literals: none
|
||||
- magic z-index: none
|
||||
- non-canonical names: none
|
||||
- roles clean: true · variants clean: true
|
||||
@ -0,0 +1,26 @@
|
||||
# Audit: group
|
||||
audit-version: 1
|
||||
audited-at: 2026-06-26
|
||||
scope: eidos (eidos-only primitive)
|
||||
method: theming-coherence category sweep (layout), adversarially verified; roles + `:Morfo` lead-verified CLEAN across all eidos (no success/warning/danger/info; only alert-dialog uses `:Morfo`).
|
||||
|
||||
## Summary
|
||||
Counts (post-verification): CRITICAL 0 · HIGH 0 · MEDIUM 0 · LOW 1.
|
||||
conformance: Contract OK, data-grow and data-attached presence flags, attached implies gap=0.
|
||||
as-const: true · roles clean: true · variants clean: true · has-recipe: true
|
||||
|
||||
## Findings
|
||||
### LOW: MAGIC LITERAL 1px physically-fixed for border collapse — group-001 <!-- id: group-001 -->
|
||||
- dimension: magic-literal
|
||||
- location: src/uix/eidos/lib/recipes/base.ts:3997
|
||||
- evidence: 'attached-overlap': '1px'
|
||||
- impact: Hardcoded 1px overlap is physically-fixed (border collapse depth), not a drift.
|
||||
- proposed-fix: ACCEPT—1px is a physically-fixed value for border collapse, not a scale literal.
|
||||
- verify: [confirmed] base.ts:3997 `'attached-overlap': '1px'` — physically-fixed border-collapse overlap (sibling borders), not a scale literal. Analyzer itself recommends ACCEPT. Genuine non-defect / LOW.
|
||||
- fix-status: open
|
||||
|
||||
## Theming facts (E-bis)
|
||||
- magic literals: 1px @ src/uix/eidos/lib/recipes/base.ts:3997 -> physically-fixed (border collapse)
|
||||
- magic z-index: none
|
||||
- non-canonical names: none
|
||||
- roles clean: true · variants clean: true
|
||||
@ -0,0 +1,19 @@
|
||||
# Audit: heading
|
||||
audit-version: 1
|
||||
audited-at: 2026-06-26
|
||||
scope: eidos (eidos-only primitive)
|
||||
method: theming-coherence category sweep (typography), adversarially verified; roles + `:Morfo` lead-verified CLEAN across all eidos (no success/warning/danger/info; only alert-dialog uses `:Morfo`).
|
||||
|
||||
## Summary
|
||||
Counts (post-verification): CRITICAL 0 · HIGH 0 · MEDIUM 0 · LOW 0. Theming-clean.
|
||||
conformance: morfo OK; no recipe; roles fragmented (content-role subset)
|
||||
as-const: true · roles clean: false · variants clean: true · has-recipe: false
|
||||
|
||||
## Findings
|
||||
_No findings (theming-clean primitive)._
|
||||
|
||||
## Theming facts (E-bis)
|
||||
- magic literals: 0.15em @ src/uix/eidos/components/heading/heading.css:88 -> canonical underline-offset token
|
||||
- magic z-index: none
|
||||
- non-canonical names: none
|
||||
- roles clean: false · variants clean: true
|
||||
@ -0,0 +1,19 @@
|
||||
# Audit: highlight
|
||||
audit-version: 1
|
||||
audited-at: 2026-06-26
|
||||
scope: eidos (eidos-only primitive)
|
||||
method: theming-coherence category sweep (typography), adversarially verified; roles + `:Morfo` lead-verified CLEAN across all eidos (no success/warning/danger/info; only alert-dialog uses `:Morfo`).
|
||||
|
||||
## Summary
|
||||
Counts (post-verification): CRITICAL 0 · HIGH 0 · MEDIUM 0 · LOW 0. Theming-clean.
|
||||
conformance: morfo OK; no recipe; ColorRole correct; composition shell (no own chrome)
|
||||
as-const: true · roles clean: true · variants clean: true · has-recipe: false
|
||||
|
||||
## Findings
|
||||
_No findings (theming-clean primitive)._
|
||||
|
||||
## Theming facts (E-bis)
|
||||
- magic literals: none
|
||||
- magic z-index: none
|
||||
- non-canonical names: none
|
||||
- roles clean: true · variants clean: true
|
||||
@ -0,0 +1,19 @@
|
||||
# Audit: icon
|
||||
audit-version: 1
|
||||
audited-at: 2026-06-26
|
||||
scope: eidos (eidos-only primitive)
|
||||
method: theming-coherence category sweep (visual-elements), adversarially verified; roles + `:Morfo` lead-verified CLEAN across all eidos (no success/warning/danger/info; only alert-dialog uses `:Morfo`).
|
||||
|
||||
## Summary
|
||||
Counts (post-verification): CRITICAL 0 · HIGH 0 · MEDIUM 0 · LOW 0. Theming-clean.
|
||||
conformance: Contract solid (as const), minimal leaf element (no parts), decorative/semantic toggle via aria-hidden, recipe provides size + stroke-width, no drift.
|
||||
as-const: true · roles clean: true · variants clean: true · has-recipe: true
|
||||
|
||||
## Findings
|
||||
_No findings (theming-clean primitive)._
|
||||
|
||||
## Theming facts (E-bis)
|
||||
- magic literals: none
|
||||
- magic z-index: none
|
||||
- non-canonical names: none
|
||||
- roles clean: true · variants clean: true
|
||||
@ -0,0 +1,49 @@
|
||||
# Audit: image-adjustments
|
||||
audit-version: 1
|
||||
audited-at: 2026-06-26
|
||||
scope: (SCOPE-DRIFT → SYS-1)
|
||||
method: adversarially-verified workflow; HIGH lead-verified. B7 ground-truth: Toolbar A35 + Form A36 incidents STAYED FIXED (untrack present); toast/clipboard/drag-drop use uix.timers (no A6 leak); button is clean (its archetype finding was inert — Spinner not runtime-registered); data-size/data-shape are eidos visual attrs (not contract violations).
|
||||
provider: src/uix/soma/components/image-adjustments/image-adjustments-provider.svelte.ts
|
||||
cleanup-audit (A6/A35/A36): No $effect in ImageAdjustmentsProvider or sub-providers that manage timers, listeners, or observers. No setTimeout, setInterval, addEventListener, ResizeObserver, IntersectionObserver, MutationObserver, or pointer capture in the ImageAdjustments layer (all delegated to composed <Slider>). The test f
|
||||
|
||||
## Summary
|
||||
Counts (post-verification): CRITICAL 0 · HIGH 0 · MEDIUM 1 · LOW 0.
|
||||
|
||||
## Findings
|
||||
### MEDIUM: SYS-1 — Scope-Drift: eidos directory exists but morfo scope does not include 'eidos' — image-adjustments-003 <!-- id: image-adjustments-003 -->
|
||||
- dimension: D
|
||||
- rule: SYS-1 — Scope-Drift: eidos directory exists but morfo scope does not include 'eidos'
|
||||
- location: src/uix/morfo/components/image-adjustments.ts:18 vs src/uix/eidos/components/image-adjustments/
|
||||
- evidence: The morfo declares scope: ['soma', 'sema'] (line 18), but a full eidos implementation exists at src/uix/eidos/components/image-adjustments/ containing image-adjustments.svelte, image-adjustments.css, types.ts, README.md, and index.ts. Per the baseline, this is a confirmed systemic issue (SYS-1) affecting several B7 components.
|
||||
- impact: Scope mismatch: the morfo's declared scope does not reflect reality. Tooling relying on scope declarations may fail to validate or organize the eidos layer. No functional impact on the component itself, but the contract is misleading.
|
||||
- repro: Inspect src/uix/morfo/components/image-adjustments.ts line 18 (scope: ['soma', 'sema']) and verify that src/uix/eidos/components/image-adjustments/ exists with a full component implementation.
|
||||
- proposed-fix: Update the morfo's scope to: scope: ['soma', 'sema', 'eidos']. This aligns the contract with the actual implementation layers present.
|
||||
- verify: [confirmed] CONFIRMED. image-adjustments.ts:18 declares `scope: ['soma', 'sema']`, omitting 'eidos'. A full eidos implementation exists at src/uix/eidos/components/image-adjustments/ (image-adjustments.css verified read, README.md verified read, recipe block at base.ts:1112-1132 'image-adjustments': {...}). This is the confirmed-systemic SYS-1 scope-drift pattern. MEDIUM. Fix: scope: ['soma', 'sema', 'eidos'].
|
||||
- fix-status: open
|
||||
|
||||
## No-findings dimensions
|
||||
B, C, F, G
|
||||
|
||||
## Theming facts (E-bis)
|
||||
- magic z-index: none
|
||||
- magic literals: none
|
||||
- undeclared parts: none
|
||||
- roles clean: true · variants clean: true
|
||||
|
||||
## Tests (F)
|
||||
- exists: true · env: jsdom
|
||||
- covers: computeImageFilter: neutral (none), tone functions (brightness/contrast/saturation/hue), 0-based (blur/grayscale/sepia), temperature approximation (warm/cool), multi-adjustment composition; ImageAdjustmentsProvider: live filter derivation, isModified tracking, setAdjustment mutation + callbacks, reset() via runtime trigger, custom adjustments subset, disabled state blocks mutations, catalog + defaults exposed; ImageAdjustmentsItemProvider: derived value/label/formattedValue per adjustment, setValue callback; Snippet props exposure and live callback routing
|
||||
- untested: A15 gesture: pointer interaction is delegated to the composed <Slider>, not handled directly by ImageAdjustments. Pointer cleanup verified in Slider tests, not here.; A6 timers/listeners: no direct setTimeout/setInterval/addEventListener/ResizeObserver/etc. in ImageAdjustments provider or components. Interaction heavy lifting delegated to Slider.; A35 effect loop: no per-item $effect reading opts.ref.current and writing provider state. No reactive loop risk.; A36 microtask loop: no async (.then/microtask/setTimeout) writing reactive vars without untrack. Callbacks (onValueChange/onFilterChange) are consumer-driven, not effect-mediated.; Live region: the component does not emit a live region for adjustment changes (per README: 'ItemValue is a plain <span>, not a live region. The Slider thumb already announces aria-valuenow'). Announcement delegated to composed Slider.; A30 id registration: no child->parent id registration loop. Each part registered directly via runtime.part() in the constructor.
|
||||
|
||||
## Style observations (non-blocking)
|
||||
- CSS correctly uses data attribute selectors matching the morfo kebabs: [data-image-adjustments], [data-image-adjustments-item], [data-image-adjustments-item-label], [data-image-adjustments-item-value], [data-image-adjustments-reset].
|
||||
- Reset button uses :focus-visible for keyboard navigation cue (line 70-74), not hardcoded :focus. Correct.
|
||||
- Reset button uses var(--focus-ring-width) and var(--focus-ring-color), consuming the canonical two-ring focus model from the theme. Correct.
|
||||
- Disabled state uses var(--opacity-disabled) (line 19, 77). Correct token usage.
|
||||
- Reset hover state applies var(--color-content-primary), correct semantic color. Line 67.
|
||||
- Hue row track override correctly uses var(--image-adjustments-hue-track), exiling the literal rainbow to the recipe layer (line 86). Correct raw-color contract.
|
||||
- No hardcoded :active, no magic z-index, no raw hex. All CSS tokens correctly namespaced to component.
|
||||
- Row typography uses var(--font-size-sm/xs), var(--font-weight-medium), var(--font-mono) — all reference the base tokens, not px/rem literals. Correct.
|
||||
- Spacing uses var(--space-*) and var(--image-adjustments-*) recipe tokens. No hardcoded gap/margin px values. Correct.
|
||||
- Reset focus-visible outline uses canonical --focus-ring-width + --focus-ring-color, not hardcoded values. Correct.
|
||||
- Tabular numerals (font-variant-numeric: tabular-nums) on ItemValue to prevent reflow during drag — good UX consideration (line 48).
|
||||
@ -0,0 +1,35 @@
|
||||
# Audit: image-picker
|
||||
audit-version: 1
|
||||
audited-at: 2026-06-26
|
||||
scope: (SCOPE-DRIFT → SYS-1)
|
||||
method: adversarially-verified workflow; HIGH lead-verified. B7 ground-truth: Toolbar A35 + Form A36 incidents STAYED FIXED (untrack present); toast/clipboard/drag-drop use uix.timers (no A6 leak); button is clean (its archetype finding was inert — Spinner not runtime-registered); data-size/data-shape are eidos visual attrs (not contract violations).
|
||||
provider: /g/dev/svelte/vicen/src/uix/soma/components/image-picker/image-picker-provider.svelte.ts
|
||||
cleanup-audit (A6/A35/A36): One $effect at line 82: reads opts.file.current, creates URL.createObjectURL, writes to this.url, cleanup function properly returns and calls URL.revokeObjectURL(u). Cleanup is attached and will fire on effect cleanup. No timers, listeners, ResizeObserver, IntersectionObserver, MutationObserver, or
|
||||
|
||||
## Summary
|
||||
Counts (post-verification): CRITICAL 0 · HIGH 0 · MEDIUM 1 · LOW 0.
|
||||
|
||||
## Findings
|
||||
### MEDIUM: SYS-1 scope-drift: a component with a real eidos implementation (recipe dir + CSS selectin — image-picker-NEW-001 <!-- id: image-picker-NEW-001 -->
|
||||
- dimension: A_Contract
|
||||
- rule: SYS-1 scope-drift: a component with a real eidos implementation (recipe dir + CSS selecting morfo data-attrs) MUST list 'eidos' in morfo.scope ('Layers that implement this component' — types.ts:798-799).
|
||||
- location: src/uix/morfo/components/image-picker.ts:20
|
||||
- evidence: Morfo declares `scope: ['soma', 'sema']` (line 20), omitting 'eidos'. But a full eidos layer implements the component: src/uix/eidos/components/image-picker/{image-picker.svelte,image-picker.css,types.ts,index.ts} all exist, and image-picker.css selects exclusively against morfo-emitted data-attrs — `[data-image-picker]` (:12), `[data-image-picker][data-disabled]` (:18), `[data-image-picker-preview][data-rotation='90']` (:49), `[data-image-picker-toolbar]` (:59), `[data-image-picker-rotate]`/`[data-image-picker-remove]` (:67-68). These are the morfo's `contracts.cssSelectors` surface, so eidos is a genuine implementing layer per the scope doctrine.
|
||||
- impact: Contract drift: the morfo under-declares its implementing layers. Tooling/coverage that keys off `scope` (lint, layer audits, sema coverage) treats the component as having no eidos layer, masking the eidos↔morfo contract. Cosmetic-to-tooling, no runtime user impact — matches the confirmed SYS-1 baseline severity (MEDIUM).
|
||||
- proposed-fix: Add 'eidos' to the scope array: `scope: ['soma', 'sema', 'eidos']` in src/uix/morfo/components/image-picker.ts:20.
|
||||
- verify: [verifier-added] added by adversarial verify pass
|
||||
- fix-status: open
|
||||
|
||||
## No-findings dimensions
|
||||
B_Behavior_A6, B_Behavior_A35, B_Behavior_A36, A_Contract_Parts, A_Contract_DataAria_SyncAttrs, D_Frontier_SomaEidos, D_Frontier_DoubleWrite, E_Theming_MagicZ, E_Theming_MagicColors, E_Theming_MagicOpacity, E_bis_RecipeFocus, E_bis_RecipeButton, F_Tests, G_Redundancy
|
||||
|
||||
## Theming facts (E-bis)
|
||||
- magic z-index: none
|
||||
- magic literals: none
|
||||
- undeclared parts: none
|
||||
- roles clean: true · variants clean: true
|
||||
|
||||
## Tests (F)
|
||||
- exists: true · env: jsdom
|
||||
- covers: state_empty_ready; setFile_fires_onSelect_onChange_triggers_commit_select; rotate_cycles_90_wraps_360; remove_clears_file_resets_transforms; filter_composition; disabled_blocks_actions
|
||||
- untested:
|
||||
@ -0,0 +1,19 @@
|
||||
# Audit: image
|
||||
audit-version: 1
|
||||
audited-at: 2026-06-26
|
||||
scope: eidos (eidos-only primitive)
|
||||
method: theming-coherence category sweep (visual-elements), adversarially verified; roles + `:Morfo` lead-verified CLEAN across all eidos (no success/warning/danger/info; only alert-dialog uses `:Morfo`).
|
||||
|
||||
## Summary
|
||||
Counts (post-verification): CRITICAL 0 · HIGH 0 · MEDIUM 0 · LOW 0. Theming-clean.
|
||||
conformance: Contract solid (as const), status-driven layering (idle/loading/loaded/error), fit + position + radius controls, no drift.
|
||||
as-const: true · roles clean: true · variants clean: true · has-recipe: true
|
||||
|
||||
## Findings
|
||||
_No findings (theming-clean primitive)._
|
||||
|
||||
## Theming facts (E-bis)
|
||||
- magic literals: 0 @ image.css:125 -> z-index (LOCAL, fallback layering) | 1 @ image.css:223 -> z-index (LOCAL, error layering) | 70% @ image.css:189 -> icon size ratio (local sizing, acceptable)
|
||||
- magic z-index: none
|
||||
- non-canonical names: none
|
||||
- roles clean: true · variants clean: true
|
||||
@ -0,0 +1,26 @@
|
||||
# Audit: kbd
|
||||
audit-version: 1
|
||||
audited-at: 2026-06-26
|
||||
scope: eidos (eidos-only primitive)
|
||||
method: theming-coherence category sweep (typography), adversarially verified; roles + `:Morfo` lead-verified CLEAN across all eidos (no success/warning/danger/info; only alert-dialog uses `:Morfo`).
|
||||
|
||||
## Summary
|
||||
Counts (post-verification): CRITICAL 0 · HIGH 0 · MEDIUM 0 · LOW 1.
|
||||
conformance: morfo OK; no recipe; ControlVariant canonical; padding literals need canonicalization
|
||||
as-const: true · roles clean: true · variants clean: true · has-recipe: false
|
||||
|
||||
## Findings
|
||||
### LOW: em-literal padding and tracking; canonical scale should exist for kbd chrome — kbd-001 <!-- id: kbd-001 -->
|
||||
- dimension: E-bis: Theming (SIZES)
|
||||
- location: src/uix/eidos/components/kbd/kbd.css:24-25, 30
|
||||
- evidence: padding-inline: 0.4em; padding-block: 0.18em; letter-spacing: var(--tracking-wide)
|
||||
- impact: Mixed: tracking uses canonical, but padding em-literals lack canonical tokens
|
||||
- proposed-fix: Define --kbd-padding-inline and --kbd-padding-block canonical tokens
|
||||
- verify: [downgraded] kbd.css:24-25 'padding-inline: 0.4em; padding-block: 0.18em' — same rationale as code-002: em-relative inline-pill padding, no canonical em padding scale, scales with prose. The finding itself notes letter-spacing already uses canonical var(--tracking-wide) (line 30) and radius uses --radius-sm (line 23). Only the em paddings remain; legitimate inline-flow relative units. Downgraded MEDIUM→LOW.
|
||||
- fix-status: open
|
||||
|
||||
## Theming facts (E-bis)
|
||||
- magic literals: 0.4em @ src/uix/eidos/components/kbd/kbd.css:24 -> canonical kbd-padding-inline token | 0.18em @ src/uix/eidos/components/kbd/kbd.css:25 -> canonical kbd-padding-block token
|
||||
- magic z-index: none
|
||||
- non-canonical names: none
|
||||
- roles clean: true · variants clean: true
|
||||
@ -0,0 +1,44 @@
|
||||
# Audit: link-preview
|
||||
audit-version: 1
|
||||
audited-at: 2026-06-26
|
||||
scope: ['soma'] (SCOPE-DRIFT → SYS-1)
|
||||
method: adversarially-verified workflow (analyze → refute); HIGH/CRITICAL personally re-verified against cited code by the lead.
|
||||
provider: src/uix/soma/components/link-preview/link-preview-provider.svelte.ts
|
||||
|
||||
## Summary
|
||||
Counts (post-verification): CRITICAL 0 · HIGH 0 · MEDIUM 1 · LOW 0.
|
||||
systemic hits: SYS-1 (scope-drift: eidos exists but not declared in morfo scope); SYS-2 (magic literals in CSS: 1px, 0.18em, 4px, 0.985 scale).
|
||||
|
||||
## Findings
|
||||
### MEDIUM: SYS-1 — link-preview-001 <!-- id: link-preview-001 -->
|
||||
- dimension: A
|
||||
- rule: SYS-1
|
||||
- location: src/uix/morfo/components/link-preview.ts:7
|
||||
- evidence: scope: ['soma'] declared in morfo, but eidos recipe dir exists at src/uix/eidos/components/link-preview/ with 7 files (index.ts, link-preview.svelte, link-preview-content.svelte, link-preview-trigger.svelte, link-preview-arrow.svelte, link-preview.css, types.ts)
|
||||
- impact: Morfo scope omits 'eidos' while eidos recipe and components exist; creates maintenance confusion and violates the documented scope contract.
|
||||
- repro: grep -r "src/uix/eidos/components/link-preview" to confirm files exist; check morfo scope at src/uix/morfo/components/link-preview.ts:7
|
||||
- proposed-fix: Either (a) add 'eidos' to morfo scope: `scope: ['soma', 'eidos']`, or (b) if eidos is intentional internal structure, document why scope is intentionally ['soma'] only and mark eidos as internal pattern.
|
||||
- verify: [confirmed] Confirmed SYS-1 scope-drift. morfo line 7: `scope: ['soma'],` while a full eidos recipe dir exists: ls src/uix/eidos/components/link-preview/ shows index.ts, link-preview.svelte, link-preview-content.svelte, link-preview-trigger.svelte, link-preview-arrow.svelte, link-preview.css, types.ts. morfo/types.ts:799 documents `scope` as 'Layers that implement this component. Eidos-only primitives may declare ["eidos"].' Peer overlays correctly list eidos: dropdown-menu.ts:11 `scope: ['soma', 'sema', 'eidos']`, tooltip.ts:14 `scope: ['soma', 'eidos', 'sema']`. link-preview omits 'eidos' despite shipping a recipe. MEDIUM is right per batch-1 SYS-1 baseline.
|
||||
- fix-status: open
|
||||
|
||||
## No-findings dimensions
|
||||
B, C, D, F, G
|
||||
|
||||
## Theming facts (E-bis)
|
||||
- magic z-index: z-index is tokenized via --popover-content-z (canonical preset)
|
||||
- magic literals: text-decoration-thickness: 1px (should use --border-width or clarify as exempt) | text-underline-offset: 0.18em (should use --tracking-* or clarify as exempt) | opacity: 0 in keyframes (acceptable as terminal state) | scale(0.985) and 4px offsets in keyframes (should be tokenized or documented)
|
||||
- undeclared parts: none
|
||||
- roles clean: true · variants clean: true
|
||||
- conformance: link-preview uses only primary role (canonical); no invented roles or sizes; size variants xs|sm|md|lg|xl conform to E-bis subset.
|
||||
|
||||
## Tests (F)
|
||||
- exists: true · env: jsdom
|
||||
- covers: open/close timers with configurable delays (openDelay/closeDelay); touch pointer ignored (only mouse/pen); disabled flag blocks open; SafePolygon pointer bridge (content hover prevents close); dismissal via Escape key and outside-click; floating content/arrow props exposure; data-state transitions
|
||||
- untested: keyboard Escape key (mentioned in README but not explicitly tested in jsdom); focus return to trigger on close (A17 not exercised); content visibility presence transitions (Presence API self-cleanup verified at code level); client/Playwright integration tests (jsdom-only; no browser interaction test)
|
||||
|
||||
## Style observations (non-blocking)
|
||||
- LinkPreview correctly uses trigger as navigational <a> (not button), which is documented and intentional per APG guidance for hover-card discovery.
|
||||
- aria-hidden=true on content is explicit design choice to hide preview from AT; justified by README study §2.3 (duplicate content, hover-only activation, focus stays on trigger).
|
||||
- SafePolygon bridge with touch guard (pointer type check) is well-implemented to preserve preview on pointer transitions.
|
||||
- Color tokens use primary role only; fallback to --color-content-primary is defensive.
|
||||
- Animation preset (4 directional in/out keyframes) matches floating-ui positioning strategy; naming data-side correlates CSS state to floating placement.
|
||||
@ -0,0 +1,19 @@
|
||||
# Audit: link
|
||||
audit-version: 1
|
||||
audited-at: 2026-06-26
|
||||
scope: eidos (eidos-only primitive)
|
||||
method: theming-coherence category sweep (typography), adversarially verified; roles + `:Morfo` lead-verified CLEAN across all eidos (no success/warning/danger/info; only alert-dialog uses `:Morfo`).
|
||||
|
||||
## Summary
|
||||
Counts (post-verification): CRITICAL 0 · HIGH 0 · MEDIUM 0 · LOW 0. Theming-clean.
|
||||
conformance: morfo OK; no recipe; ColorRole correct (9 roles); variants canonical (default/subtle/plain)
|
||||
as-const: true · roles clean: true · variants clean: true · has-recipe: false
|
||||
|
||||
## Findings
|
||||
_No findings (theming-clean primitive)._
|
||||
|
||||
## Theming facts (E-bis)
|
||||
- magic literals: 0.85em @ src/uix/eidos/components/link/link.css:112 -> canonical icon-size token (acceptable; glyph scale)
|
||||
- magic z-index: none
|
||||
- non-canonical names: none
|
||||
- roles clean: true · variants clean: true
|
||||
@ -0,0 +1,26 @@
|
||||
# Audit: mark
|
||||
audit-version: 1
|
||||
audited-at: 2026-06-26
|
||||
scope: eidos (eidos-only primitive)
|
||||
method: theming-coherence category sweep (typography), adversarially verified; roles + `:Morfo` lead-verified CLEAN across all eidos (no success/warning/danger/info; only alert-dialog uses `:Morfo`).
|
||||
|
||||
## Summary
|
||||
Counts (post-verification): CRITICAL 0 · HIGH 0 · MEDIUM 0 · LOW 1.
|
||||
conformance: morfo OK; no recipe; ColorRole correct (9 roles); padding em-literals need canonicalization
|
||||
as-const: true · roles clean: true · variants clean: true · has-recipe: false
|
||||
|
||||
## Findings
|
||||
### LOW: em-literal padding; no canonical scale defined — mark-001 <!-- id: mark-001 -->
|
||||
- dimension: E-bis: Theming (SIZES)
|
||||
- location: src/uix/eidos/components/mark/mark.css:14-15
|
||||
- evidence: padding-inline: 0.16em; padding-block: 0.04em
|
||||
- impact: Magic em literals for mark decoration; same issue as code.css padding
|
||||
- proposed-fix: Define --mark-padding-inline and --mark-padding-block tokens
|
||||
- verify: [downgraded] mark.css:14-15 'padding-inline: 0.16em; padding-block: 0.04em' — same rationale: em-relative inline-highlight padding scaling with prose font-size, no canonical em padding scale, radius uses canonical --radius-sm. Legitimate inline-flow literal, not drift. Downgraded MEDIUM→LOW.
|
||||
- fix-status: open
|
||||
|
||||
## Theming facts (E-bis)
|
||||
- magic literals: 0.16em @ src/uix/eidos/components/mark/mark.css:14 -> canonical mark-padding-inline token | 0.04em @ src/uix/eidos/components/mark/mark.css:15 -> canonical mark-padding-block token
|
||||
- magic z-index: none
|
||||
- non-canonical names: none
|
||||
- roles clean: true · variants clean: true
|
||||
@ -0,0 +1,109 @@
|
||||
# Audit: menubar
|
||||
audit-version: 1
|
||||
audited-at: 2026-06-26
|
||||
scope: ['soma', 'sema'] (SCOPE-DRIFT → SYS-1)
|
||||
method: adversarially-verified workflow (analyze → refute); HIGH/CRITICAL personally re-verified against cited code by the lead.
|
||||
provider: src/uix/soma/components/menubar/menubar-provider.svelte.ts
|
||||
|
||||
## Summary
|
||||
Counts (post-verification): CRITICAL 0 · HIGH 0 · MEDIUM 3 · LOW 4.
|
||||
systemic hits: SYS-1 (scope-drift: morfo omits 'eidos' but eidos recipe exists); SYS-2 (magic z-index: 1 without token); SYS-3 (jsdom-only tests on interaction-heavy component); SYS-4 (duplicated keyboard index math in two methods).
|
||||
|
||||
## Findings
|
||||
### MEDIUM: SYS-1 scope-drift — menubar-001 <!-- id: menubar-001 -->
|
||||
- dimension: A (Contract)
|
||||
- rule: SYS-1 scope-drift
|
||||
- location: src/uix/morfo/components/menubar.ts:7
|
||||
- evidence: scope: ['soma', 'sema'], but a full eidos recipe directory exists at src/uix/eidos/components/menubar/ with 8 files (menubar.svelte, menubar.css, menubar-trigger.svelte, menubar-content.svelte, menubar-context.ts, menubar-menu.svelte, types.ts, index.ts)
|
||||
- impact: Morfo declares scope omits 'eidos' while an eidos recipe dir exists, violating the scope-drift rule SYS-1. The eidos layer exists and provides the full visual contract (menubar.css, size context, data-stagger, data-size on provider).
|
||||
- repro: Check src/uix/morfo/components/menubar.ts line 7 against presence of src/uix/eidos/components/menubar/ directory.
|
||||
- proposed-fix: Add 'eidos' to the scope array: scope: ['soma', 'sema', 'eidos']
|
||||
- verify: [confirmed] morfo line 7 reads `scope: ['soma', 'sema'],` yet a full eidos recipe dir exists at src/uix/eidos/components/menubar/ (menubar.css, menubar.svelte, menubar-content.svelte, menubar-context.ts, types.ts, index.ts). The morfo's OWN comment contradicts the scope: line 8-10 cites the sema pack, and the eidos wrapper stamps data-size + reuses the dropdown recipe. Sibling components with an eidos dir DO list 'eidos' (accordion.ts:7, dropdown-menu.ts:11 both `['soma','sema','eidos']`). This is exactly SYS-1 scope-drift. HIGH is justified: the compiled scope is the contract the validators key on, and it's wrong.
|
||||
- fix-status: open
|
||||
|
||||
### MEDIUM: SYS-4 keyboard route duplication + index math duplicated — menubar-004 <!-- id: menubar-004 -->
|
||||
- dimension: B (Keyboard), G (Redundancy)
|
||||
- rule: SYS-4 keyboard route duplication + index math duplicated
|
||||
- location: src/uix/soma/components/menubar/menubar-provider.svelte.ts:131-146 and 283-296
|
||||
- evidence: navigateAdjacent() method (lines 135-143): if (idx === -1) { next = delta === 1 ? 0 : values.length - 1; } else if (loop) { next = (idx + delta + values.length) % values.length; } else { next = Math.max(0, Math.min(...)); } — IDENTICAL logic in onContentKeydown() (lines 285-291) for nextIdx calculation.
|
||||
- impact: The index arithmetic for navigating between triggers is duplicated across two code paths (navigateAdjacent called from trigger onkeydown and directly in onContentKeydown). This duplicated logic risks divergence if one path is later updated. Both methods calculate the next index identically, but SYS-4 flags this as a known pattern to extract.
|
||||
- repro: Compare lines 135-143 (navigateAdjacent index calc) with lines 285-291 (onContentKeydown index calc) — they are textually identical.
|
||||
- proposed-fix: Extract the index calculation into a private helper method like getNextIndex(currentValue, delta, values, loop) and reuse it in both navigateAdjacent and onContentKeydown.
|
||||
- verify: [confirmed] Confirmed as duplication, MEDIUM is right. Lines 137-143 (navigateAdjacent): `if (idx === -1) { next = delta === 1 ? 0 : values.length - 1; } else if (this.opts.loop.current) { next = (idx + delta + values.length) % values.length; } else { next = Math.max(0, Math.min(values.length - 1, idx + delta)); }` is textually identical to lines 285-291 in onContentKeydown (same three-branch math on nextIdx). This is the SYS-4 nav-dup pattern. IMPORTANT: the off-by-n-2 half of SYS-4 is NOT present here — the idx===-1 branch lands on `values.length - 1` (the LAST item) for delta -1, which is correct, not n-2. So it's pure extract-worthy duplication with divergence risk, not a behavioral bug. MEDIUM confirmed.
|
||||
- fix-status: open
|
||||
|
||||
### MEDIUM: Test environment: jsdom only, no client/Playwright coverage — menubar-007 <!-- id: menubar-007 -->
|
||||
- dimension: F (Tests)
|
||||
- rule: Test environment: jsdom only, no client/Playwright coverage
|
||||
- location: src/uix/soma/components/menubar/menubar-provider.svelte.test.ts:1
|
||||
- evidence: // @vitest-environment jsdom — tests run in jsdom (headless) only. 268 lines of test code covering MenubarProvider, but no client (Playwright) variant to verify DOM interaction, focus management, floating positioning, or hover behavior in a real browser.
|
||||
- impact: SYS-3: jsdom-only tests + interaction-heavy component + keyboard/focus logic. While keyboard routes are tested (ArrowRight, ArrowLeft, Home, End, Enter, Space, ArrowDown), they are mocked event objects in a simplified environment. Focus sync, floating anchor positioning, and real keyboard interaction are untested.
|
||||
- repro: Test file environment is jsdom; test coverage focuses on state/logic, not DOM/focus/positioning. No client-side tests found.
|
||||
- proposed-fix: Create a parallel menubar-provider.test.ts with @vitest-environment=node or add menubar-provider.e2e.ts with Playwright to verify: (1) real focus shifts, (2) floating content positioning, (3) hover-follow switching, (4) Escape focus return, (5) RTL directional keys.
|
||||
- verify: [confirmed] Confirmed. menubar-provider.svelte.test.ts:1 is `// @vitest-environment jsdom`. Events are plain mocked objects (keyEvent/pointerEvent factories lines 84-98 return `{ key, target, preventDefault: vi.fn() }`). Keyboard ROUTES are exercised (ArrowRight/Left, Home/End, ArrowDown open, content-nav switch), which is better than many SYS-3 cases, but real focus movement is only asserted via a `vi.spyOn(dom,'focus')` spy — actual DOM focus, roving-tabindex realization, floating positioning, and Escape focus-RETURN-to-trigger (the queueMicrotask path at provider:258-261) are NOT verified in a browser. Interaction-heavy floating component + jsdom-only + no client/Playwright sibling = SYS-3. MEDIUM appropriate.
|
||||
- fix-status: open
|
||||
|
||||
### LOW: Undeclared ARIA attribute (aria-controls) — menubar-002 <!-- id: menubar-002 -->
|
||||
- dimension: A (Contract)
|
||||
- rule: Undeclared ARIA attribute (aria-controls)
|
||||
- location: src/uix/soma/components/menubar/menubar-provider.svelte.ts:431
|
||||
- evidence: 'aria-controls': this.isOpen ? this.menu.menu.contentId.current || undefined : undefined, — emitted by soma but not declared in morfo's trigger part aria array
|
||||
- impact: The trigger part declares aria: [{ attr: 'type', ... }, { attr: 'aria-haspopup', ... }, { attr: 'aria-expanded', ... }] but aria-controls is emitted dynamically by soma without being in the contract. This breaks the two-of-3 rule (morfo ↔ soma/sema/eidos consistency).
|
||||
- repro: grep 'aria-controls' soma provider; verify it is not in morfo trigger aria array at lines 55-59.
|
||||
- proposed-fix: Add aria-controls to morfo trigger part: { attr: 'aria-controls', value: v.expr('open ? menuId : undefined') } or similar pattern to declare the dynamic aria-controls as a morfo-driven attribute.
|
||||
- verify: [downgraded] Confirmed factually: morfo trigger aria array (lines 55-59) declares only `type`, `aria-haspopup`, `aria-expanded` — no `aria-controls`. Provider emits it at line 431: `'aria-controls': this.isOpen ? this.menu.menu.contentId.current || undefined : undefined`. The sibling dropdown-menu morfo DOES declare it (dropdown-menu.ts:90-95 with `condition: { when: 'part-present', part: 'content' }`), so the canonical fix is exactly the proposed one. BUT severity HIGH overstates it: the attribute is functionally correct and present at runtime, ARIA is not broken, and it is conditionally emitted only while open. This is a contract-completeness gap (the morfo should declare what soma emits), not a user-visible a11y break or a divergence risk — LOW/contract-noise tier, not HIGH.
|
||||
- fix-status: open
|
||||
|
||||
### LOW: Magic literal: outline-offset: 1px — menubar-005 <!-- id: menubar-005 -->
|
||||
- dimension: E-bis (Theming)
|
||||
- rule: Magic literal: outline-offset: 1px
|
||||
- location: src/uix/eidos/components/menubar/menubar.css:104
|
||||
- evidence: [data-menubar-trigger]:focus-visible { outline: var(--focus-ring-width) solid var(--focus-ring-color); outline-offset: 1px; ... }
|
||||
- impact: A bare 1px literal is used for outline-offset instead of a canonical --outline-offset-* or --focus-offset-* token. This violates E-bis rule: spacing/offset values should use canonical token scale --space-* or a purpose-specific --focus-offset-*.
|
||||
- repro: grep 'outline-offset' menubar.css shows a bare pixel value without a var() reference.
|
||||
- proposed-fix: Replace outline-offset: 1px with a canonical token like outline-offset: var(--outline-offset-focus) (if defined in the design system) or define one.
|
||||
- verify: [downgraded] menubar.css:104 `outline-offset: 1px;` is a bare literal — factually correct. But this is a pervasive baseline idiom, not a menubar drift: badge.css:183 uses the identical `outline-offset: 1px;`, and dozens of components use bare `outline-offset: 0/2px`. There is no `--outline-offset-*`/`--focus-offset-*` token in the system for this (the proposed fix invents one). A 1px focus-ring gap is a fixed perceptual constant, not a position on the --space-* rhythm. Real but LOW cosmetic token-nit, not MEDIUM.
|
||||
- fix-status: open
|
||||
|
||||
### LOW: Magic literal: z-index: 1 — menubar-006 <!-- id: menubar-006 -->
|
||||
- dimension: E-bis (Theming)
|
||||
- rule: Magic literal: z-index: 1
|
||||
- location: src/uix/eidos/components/menubar/menubar.css:106
|
||||
- evidence: [data-menubar-trigger]:focus-visible { ... z-index: 1; } — a bare integer z-index instead of a named token
|
||||
- impact: SYS-2: bare z-index integer without a canonical --z-index-* token. The value 1 is arbitrary and not tied to the design system's z-index scale (e.g., --z-index-overlay, --z-index-dropdown).
|
||||
- repro: Line 106 of menubar.css has z-index: 1 without var().
|
||||
- proposed-fix: Define or reuse a canonical z-index token: z-index: var(--z-index-focus) or similar, ensuring it fits the system's layering strategy.
|
||||
- verify: [downgraded] menubar.css:106 `z-index: 1;` inside `[data-menubar-trigger]:focus-visible` (with `position: relative`) is a bare integer — factually correct. But this is the standard focus/in-flow stacking idiom (raise the focused sibling within a local stacking context so its outline isn't clipped), used by the POSITIVE REFERENCE tabs.css:281 (`position: relative; z-index: 1;` for triggers over the indicator) and ~19 other components (avatar, button-group, carousel, dialog, combobox, toggle-group, virtual-list...). SYS-2's cited examples are LAYERING-SCALE values (content-z/overlay-z) — e.g. dropdown-menu.css:34 `z-index: var(--dropdown-menu-content-z)` — NOT focus +1. A local +1 is not a position on the global layering scale and has no canonical --z-index-* token. Real literal but LOW, and flagging it MEDIUM/SYS-2 while the reference component does the identical thing is inconsistent.
|
||||
- fix-status: open
|
||||
|
||||
### LOW: 2-of-3 rule: data-menubar-value consumed only by soma trigger, not by sema or eidos — menubar-008 <!-- id: menubar-008 -->
|
||||
- dimension: A (Contract)
|
||||
- rule: 2-of-3 rule: data-menubar-value consumed only by soma trigger, not by sema or eidos
|
||||
- location: src/uix/morfo/components/menubar.ts:53 and src/uix/soma/components/menubar/menubar-provider.svelte.ts:435
|
||||
- evidence: morfo declares { attr: 'data-menubar-value' } in trigger part data array (no values list, consumed by soma). Eidos does not consume it (not in menubar-trigger.svelte), and no sema reference found.
|
||||
- impact: The 2-of-3 rule suggests each morfo field should be consumed by >=2 of soma/sema/eidos. data-menubar-value is only used by soma (MenubarTriggerProvider.props emits it). This is a low-priority observation because it may be intentional for internal soma wiring (store the menu value for internal reference), but it's worth confirming the rule intent.
|
||||
- repro: data-menubar-value is declared in morfo but appears only in soma provider output, not in eidos CSS selectors or sema logic.
|
||||
- proposed-fix: Verify if data-menubar-value is only a soma-internal marker. If it's consumer-facing, ensure eidos or another layer consumes it for styling or state tracking.
|
||||
- verify: [confirmed] Confirmed factually and the analyzer correctly rated it LOW. morfo declares `{ attr: 'data-menubar-value' }` in trigger data (menubar.ts:53); provider emits it at menubar-provider.svelte.ts:435 `'data-menubar-value': this.menu.opts.value.current`. grep for data-menubar-value across src/uix/eidos and src/uix/sema returned EMPTY — neither eidos CSS nor the sema pack consumes it (the sema pack keys on `data-menubar`/`data-menubar-trigger` via semaSelector, not the value). So it is consumed by exactly 1 of 3 (soma only). It is a legitimate soma-internal sibling-location marker (cross-menu nav reads the registry, but the attr documents the value on the DOM), so the 2-of-3 carve-out for internal wiring applies. LOW contract-noise, as rated.
|
||||
- fix-status: open
|
||||
|
||||
## No-findings dimensions
|
||||
C (DOM-selector), D (Frontier), E (TSC)
|
||||
|
||||
## Theming facts (E-bis)
|
||||
- magic z-index: z-index: 1 at menubar.css:106
|
||||
- magic literals: outline-offset: 1px at menubar.css:104
|
||||
- undeclared parts: data-size on provider (eidos emits, morfo not declares) | data-stagger on content (eidos emits, morfo not declares) | data-floating-gap on content (eidos emits, morfo not declares)
|
||||
- roles clean: true · variants clean: true
|
||||
|
||||
## Tests (F)
|
||||
- exists: true · env: jsdom
|
||||
- covers: coordinates sibling menus through root value; navigates enabled triggers in DOM order + skips disabled; opens trigger from keyboard without moving focus; switches open menu from content horizontal navigation
|
||||
- untested: real DOM focus sync; floating anchor positioning; hover-follow behavior in real browser; Escape focus-return in real browser; RTL directional key behavior (ArrowRight/Left swap); submenu escape and cross-menu navigation in real content; typeahead (if implemented); client-side interaction
|
||||
|
||||
## Style observations (non-blocking)
|
||||
- The eidos recipe is well-structured and reuses dropdown-menu parts effectively for per-menu content.
|
||||
- data-shape-nest concentric-radius pattern (menubar-trigger.svelte:18) is a thoughtful design that avoids hand-rolled calc.
|
||||
- Hover-follow and cross-menu arrow navigation logic is coherent and well-documented.
|
||||
- The size cascade via context (MenubarSizeContext) is clean and follows Svelte patterns.
|
||||
- MenubarMenuProvider acts as a clean adapter layer between menubar state and nested DropdownMenu, which is architecturally sound.
|
||||
@ -0,0 +1,35 @@
|
||||
# Audit: meter
|
||||
audit-version: 1
|
||||
audited-at: 2026-06-26
|
||||
scope:
|
||||
method: adversarially-verified workflow; HIGH lead-verified. B7 ground-truth: Toolbar A35 + Form A36 incidents STAYED FIXED (untrack present); toast/clipboard/drag-drop use uix.timers (no A6 leak); button is clean (its archetype finding was inert — Spinner not runtime-registered); data-size/data-shape are eidos visual attrs (not contract violations).
|
||||
provider: G:/dev/svelte/vicen/src/uix/soma/components/meter/meter-provider.svelte.ts
|
||||
cleanup-audit (A6/A35/A36): MeterProvider: NO timers, listeners, observers, or resources requiring disposal. All state is via $derived (safe, reactive). MeterIndicatorProvider: same - no cleanup needed. A35/A36 loops: NOT POSSIBLE - no $effect usage anywhere, only safe $derived and readableActive helpers. Result: clean lifecyc
|
||||
|
||||
## Summary
|
||||
Counts (post-verification): CRITICAL 0 · HIGH 0 · MEDIUM 1 · LOW 0.
|
||||
|
||||
## Findings
|
||||
### MEDIUM: SYS-1 scope-drift: eidos recipe/dir exists but morfo `scope` omits 'eidos' — meter-SYS1-scope <!-- id: meter-SYS1-scope -->
|
||||
- dimension: A
|
||||
- rule: SYS-1 scope-drift: eidos recipe/dir exists but morfo `scope` omits 'eidos'
|
||||
- location: src/uix/morfo/components/meter.ts:7
|
||||
- evidence: Morfo declares `scope: ['soma']` (meter.ts:7), yet a full eidos layer materializes the component: a recipe entry `meter: { ... }` at src/uix/eidos/lib/recipes/base.ts:981-1010 (height/radius/ring/track/indicator tokens), and an eidos component dir src/uix/eidos/components/meter/ containing meter.css, meter.svelte (visual wrapper, size+shape), meter-indicator.svelte, types.ts. The scope array does not list 'eidos' despite this eidos materialization.
|
||||
- impact: Contract metadata drift: the morfo scope no longer reflects which layers consume the component. Tooling/lint that keys off `scope` to know an eidos recipe should exist will under-report. Consistency only — runtime behavior unaffected.
|
||||
- proposed-fix: Add 'eidos' to the meter morfo `scope` array: `scope: ['soma', 'eidos']`, matching the other B7 components that declare the eidos scope.
|
||||
- verify: [verifier-added] added by adversarial verify pass
|
||||
- fix-status: open
|
||||
|
||||
## No-findings dimensions
|
||||
B, C, D, E, F, G
|
||||
|
||||
## Theming facts (E-bis)
|
||||
- magic z-index: none
|
||||
- magic literals: none
|
||||
- undeclared parts: none
|
||||
- roles clean: true · variants clean: true
|
||||
|
||||
## Tests (F)
|
||||
- exists: true · env: jsdom
|
||||
- covers: meter value attrs and indicator zone projection; default resolution and clamping; labelledby and valueText overrides
|
||||
- untested: circular shape variant; responsive size changes; edge cases for optimum threshold calculation
|
||||
@ -0,0 +1,36 @@
|
||||
# Audit: metrics
|
||||
audit-version: 1
|
||||
audited-at: 2026-06-26
|
||||
scope:
|
||||
method: adversarially-verified workflow; HIGH lead-verified. B7 ground-truth: Toolbar A35 + Form A36 incidents STAYED FIXED (untrack present); toast/clipboard/drag-drop use uix.timers (no A6 leak); button is clean (its archetype finding was inert — Spinner not runtime-registered); data-size/data-shape are eidos visual attrs (not contract violations).
|
||||
provider: G:/dev/svelte/vicen/src/uix/soma/components/metrics/metrics.svelte.ts (runtime-only, no traditional provider)
|
||||
cleanup-audit (A6/A35/A36): No timers, setInterval, ResizeObserver, IntersectionObserver, MutationObserver, addEventListener, or pointer listeners declared in eidos or soma directories. A35 loop check: metrics-delta's $effect writes to context.setIntent (line 46-48), which updates root's currentIntent state, but no component r
|
||||
|
||||
## Summary
|
||||
Counts (post-verification): CRITICAL 0 · HIGH 0 · MEDIUM 1 · LOW 0.
|
||||
|
||||
## Findings
|
||||
### MEDIUM: F (Tests): a component with a non-trivial soma runtime path (live-region dispatch + value- — metrics-no-runtime-test <!-- id: metrics-no-runtime-test -->
|
||||
- dimension: F
|
||||
- rule: F (Tests): a component with a non-trivial soma runtime path (live-region dispatch + value-change effect + intent-publish effect) should have a provider/runtime test mapping the RISK paths. None exists.
|
||||
- location: src/uix/soma/components/metrics/ (no *.test.ts) — verified via find/grep: zero files reference metricsMorfo/createMetricsRuntime/notifyUpdate in any *.test.ts/*.spec.ts
|
||||
- evidence: `createMetricsRuntime` (metrics.svelte.ts:17) is exercised through three reactive code paths with no coverage: (1) the live-signal dispatch — metrics.svelte:57-63 `notifyUpdate` → `runtime.trigger('signal-notify-update', { fallbackTarget: el, message: text })`, which routes through `uix.announce` via the morfo's `a11ySemantic.requiresLiveRegion` (metrics.ts:64); (2) the value-change detector — metrics-value.svelte:21-32 (`prev`/`firstRun` memo, the `live`-off no-op path, and the first-run-suppressed path); (3) the intent-publish effect — metrics-delta.svelte:46-48. These are exactly the live-region/effect paths the audit flags as high-risk, yet there is no test asserting the trigger fires only on a real change, stays inert when `live` is off, suppresses the first run, and announces `message`.
|
||||
- impact: Regressions in the live-update signal (e.g. re-introducing a first-run announce, or firing when value is unchanged, or breaking the `live`-off no-op gate) would ship silently. The change-detection memo logic (metrics-value.svelte:18-32) is subtle and untested.
|
||||
- repro: find/grep over src for metric*test / metricsMorfo|createMetricsRuntime|notifyUpdate in *.test.ts returns nothing.
|
||||
- proposed-fix: Add src/uix/soma/components/metrics/metrics.svelte.test.ts (or a runtime test) using a real ActiveUix via createActiveUix/attachActiveUix (never a fake announce per project rule): assert `notifyUpdate` triggers `signal-notify-update` only on a value change when `live` is on, is a no-op when `live` is off, suppresses the first run, and forwards `message` to the live region.
|
||||
- verify: [verifier-added] added by adversarial verify pass
|
||||
- fix-status: open
|
||||
|
||||
## No-findings dimensions
|
||||
A: Contract (morfo scope satisfies, parts declared and present, 2-of-3 rule, part data-* naming correct, aria declarations present), B: Behavior (A35: no per-item effect reading opts.ref.current + writing provider state; A36: no async-microtask-mediated freeze; A33: no $state(Map/Set) non-reactive; A31: no O(N²) derived with global provider reads; A6: no timers/observers/listeners declared; A30: no child->parent id registration in $effect; A15: no gesture; LIVE REGIONS: signal-notify-update correctly dispatched via runtime.trigger with message + announce; A34: announce provider reachable), C: DOM-selector (no interpolated consumer values; global document/window not used), D: Frontier (no soma imports from eidos; no eidos imports from soma except in root to call createMetricsRuntime; data-size/color/variant are visual attrs; Progress/Gauge use framework Meter composition), E: TSC tokens (all spacing via --space-* tokens; all icon-size via --icon-size-*; all color via CSS custom properties; no hardcoded focus-ring or :active), E-bis Theming (Delta composes Badge which uses canonical color roles; featured icon colors use 9 canonical roles; no hardcoded hex; recipe tokens via --metrics-* private scale), F: Tests (no test file present - gap noted but not violation per baseline), G: Redundancy (standard context pattern for live-signal bridge; no gesture re-implementation)
|
||||
|
||||
## Theming facts (E-bis)
|
||||
- magic z-index: none
|
||||
- magic literals: none
|
||||
- undeclared parts: none
|
||||
- roles clean: true · variants clean: true
|
||||
|
||||
## Tests (F)
|
||||
- exists: false · env: N/A
|
||||
- covers:
|
||||
- untested: signal-notify-update dispatch when live=true and value changes; intent derivation (trend vs goodTrend); size cascading to sub-parts (Badge, Meter, icon size); aria-label generation on Delta; part composition (Badge, Meter, Sparkline) integration; reduced-motion behavior (if applicable); announce() called with correct priority (polite/assertive)
|
||||
@ -0,0 +1,19 @@
|
||||
# Audit: motion
|
||||
audit-version: 1
|
||||
audited-at: 2026-06-26
|
||||
scope: ['eidos'] (eidos-only primitive)
|
||||
method: theming-coherence category sweep (infra), adversarially verified; roles + `:Morfo` lead-verified CLEAN across all eidos (no success/warning/danger/info; only alert-dialog uses `:Morfo`).
|
||||
|
||||
## Summary
|
||||
Counts (post-verification): CRITICAL 0 · HIGH 0 · MEDIUM 0 · LOW 0. Theming-clean.
|
||||
conformance: eidos foundation primitive; 'as const satisfies' ✓; passive; no recipe/CSS; uses preset rules.
|
||||
as-const: true · roles clean: true · variants clean: true · has-recipe: false
|
||||
|
||||
## Findings
|
||||
_No findings (theming-clean primitive)._
|
||||
|
||||
## Theming facts (E-bis)
|
||||
- magic literals: none
|
||||
- magic z-index: none
|
||||
- non-canonical names: none
|
||||
- roles clean: true · variants clean: true
|
||||
@ -0,0 +1,74 @@
|
||||
# Audit: navigation-menu
|
||||
audit-version: 1
|
||||
audited-at: 2026-06-26
|
||||
scope: ['soma', 'sema'] (SCOPE-DRIFT → SYS-1)
|
||||
method: adversarially-verified workflow (analyze → refute); HIGH/CRITICAL personally re-verified against cited code by the lead.
|
||||
provider: src/uix/soma/components/navigation-menu/navigation-menu-provider.svelte.ts
|
||||
|
||||
## Summary
|
||||
Counts (post-verification): CRITICAL 0 · HIGH 1 · MEDIUM 3 · LOW 0.
|
||||
systemic hits: SYS-1 (scope-drift: morfo omits 'eidos' despite eidos recipe); SYS-3 (jsdom-only test, keyboard routes untested); SYS-6 (undeclared eidos parts: data-size, data-depth, data-floating-gap).
|
||||
|
||||
## Findings
|
||||
### HIGH: A6 — navigation-menu-006 <!-- id: navigation-menu-006 -->
|
||||
- dimension: B
|
||||
- rule: A6
|
||||
- location: src/uix/soma/components/navigation-menu/navigation-menu-provider.svelte.ts:523-527
|
||||
- evidence: `openedAtEffect = $effect.root(() => { $effect(() => { if (this.isOpen) this.lastOpenedAt = Date.now(); }); });` — the disposer RETURNED by `$effect.root` is stored in the `openedAtEffect` field and NEVER invoked. NavigationMenuTriggerProvider has no teardown effect/`$effect(() => () => openedAtEffect())` and no dispose() (grep 'dispose|destroy' in this file: only this one match).
|
||||
- impact: A6 leak: `$effect.root` deliberately creates a DETACHED, non-auto-disposing reactive root. Each NavigationMenuTriggerProvider instance leaks one root permanently subscribed to `this.isOpen` (which reads provider.opts.value via item.isOpen). Triggers mount/unmount with dynamic nav / route changes; every destroyed trigger leaves a live tracking root — accumulating reactive subscriptions for the page lifetime. Contrast: every other `$effect.root` in the codebase is in test files where the returned cleanup IS called.
|
||||
- repro: Render a NavigationMenu, dynamically add/remove Trigger items (or navigate between routes that mount different nav menus) — each removed trigger leaves an undisposed $effect.root subscribed to isOpen; reactive roots accumulate without bound.
|
||||
- proposed-fix: Drop `$effect.root` entirely — the provider constructor already runs in a tracking context (the SAME constructor pattern uses bare `$effect` at lines 99 and 109 of the root provider, which auto-dispose). Replace with a bare `$effect(() => { if (this.isOpen) this.lastOpenedAt = Date.now(); })` in the constructor (or class field run during construction), which auto-cleans on unmount. If `$effect.root` must be retained, wire its disposer into an `$effect(() => () => this.openedAtEffect())` teardown.
|
||||
- verify: [verifier-added] added by adversarial verify pass
|
||||
- fix-status: open
|
||||
|
||||
### MEDIUM: behavioral — navigation-menu-001 <!-- id: navigation-menu-001 -->
|
||||
- dimension: C
|
||||
- rule: behavioral
|
||||
- location: src/uix/soma/components/navigation-menu/navigation-menu-provider.svelte.ts:567
|
||||
- evidence: const content = this.provider.opts.ref.current?.querySelector<HTMLElement>(`#${this.item.contentId.current}`);
|
||||
- impact: contentId is user-derived (from optional id prop in types.ts line 139) and not CSS-escaped. Special characters like ':' or '[' can break the selector or inject CSS selector logic.
|
||||
- repro: Pass id='nav-content:test' or id='nav-content[special]' to NavigationMenuContent; ArrowDown keyboard navigation will fail to find the content.
|
||||
- proposed-fix: Use CSS.escape(this.item.contentId.current) before interpolating into the selector: `#${CSS.escape(this.item.contentId.current)}`
|
||||
- verify: [confirmed] Line 567: `querySelector(`#${this.item.contentId.current}`)` — no CSS.escape. contentId derives from Content's `id?: string` consumer prop (types.ts:139 'DOM id. Auto-generated when omitted'). The project's OWN convention escapes id selectors: combobox-provider.svelte.ts:418 does `#${CSS.escape(this.provider.highlightedId)}`, and 7 other providers (tabs/listbox/radio-group/grid-list/command/tree-grid/tree-view) wrap data-value interpolations in CSS.escape. navigation-menu is the lone unescaped `#`-id selector. Real dimension-C fragility; HIGH per 'untrusted/consumer-derived selector' rule. ArrowDown-into-content nav silently fails for an id containing ':' '[' etc.
|
||||
- fix-status: open
|
||||
|
||||
### MEDIUM: SYS-3 — navigation-menu-004 <!-- id: navigation-menu-004 -->
|
||||
- dimension: F
|
||||
- rule: SYS-3
|
||||
- location: src/uix/soma/components/navigation-menu/navigation-menu-provider.svelte.test.ts:1
|
||||
- evidence: @vitest-environment jsdom; tests cover ArrowDown, ArrowRight, but do not test Home, End, Escape, ArrowLeft, or loop=true wrapping behavior. No client/Playwright test exists.
|
||||
- impact: Critical keyboard navigation paths (Home/End/loop wrap) are untested in jsdom. Interaction-heavy component with high-risk keyboard behavior only validated in headless environment; edge cases (wrapping, boundary conditions) undetected.
|
||||
- proposed-fix: Add tests for all keyboard routes (Home → first item, End → last item, Escape when open, ArrowLeft, loop=true wrapping at boundaries). Consider a client test for real DOM focus + browser event dispatch.
|
||||
- verify: [downgraded] CONFIRMED as a gap but DOWNGRADED HIGH→MEDIUM. Test is jsdom-only (line 1 `@vitest-environment jsdom`), no client/Playwright variant exists (only navigation-menu-provider.svelte.test.ts). It exercises ArrowRight (handleListKeydown→close+focus, line 268) and ArrowDown (trigger→content focus, line 211) + open/close/skip-delay timers, but NOT Home/End/Escape/loop-wrap. This is SYS-3 (interaction-heavy + jsdom-only + kbd partially untested). Severity is MEDIUM per the rubric ('missing test on a high-risk path' = MEDIUM), not HIGH: the untested loop math at provider.svelte:336-345 is actually CORRECT — modulo `(i+1)%n` / `(i-1+n)%n` with a `currentIndex===-1` early-return guard (line 325), so the SYS-4 n-2 off-by branch does NOT exist here. No latent bug behind the missing tests, only coverage debt.
|
||||
- fix-status: open
|
||||
|
||||
### MEDIUM: SYS-1 — navigation-menu-005 <!-- id: navigation-menu-005 -->
|
||||
- dimension: A
|
||||
- rule: SYS-1
|
||||
- location: src/uix/morfo/components/navigation-menu.ts:7
|
||||
- evidence: scope: ['soma', 'sema'] but src/uix/eidos/components/navigation-menu/ directory exists with CSS and component files (navigation-menu.css, navigation-menu.svelte, etc.)
|
||||
- impact: Scope drift: morfo declares only soma + sema participation, but eidos has full recipes and visual styling. If a consumer or validator only reads morfo, they won't know eidos is active.
|
||||
- proposed-fix: Add 'eidos' to morfo scope: scope: ['soma', 'sema', 'eidos']
|
||||
- verify: [confirmed] CONFIRMED SYS-1 at MEDIUM. navigation-menu.ts:7 `scope: ['soma', 'sema']` omits 'eidos', yet a full eidos recipe dir exists (navigation-menu.css 310 lines + navigation-menu.svelte + types.ts NavigationMenuSize). Systemic: calendar/color-picker/combobox/command/date-picker morfos likewise scope `['soma','sema']` despite having eidos dirs. MEDIUM is the right severity — informational drift between morfo declaration and the active eidos layer, no user-visible/behavioral consequence.
|
||||
- fix-status: open
|
||||
|
||||
## No-findings dimensions
|
||||
B, G
|
||||
|
||||
## Theming facts (E-bis)
|
||||
- magic z-index: 1 at line 285 (indicator z-index should be --z-index-* or declared per spec)
|
||||
- magic literals: 0.5rem chevron size (line 135-136) | -1px transform (line 140) | 2px transform (line 146) | -4px scale transform (line 236) | 0.985 scale (line 236) | 1px outline-offset (lines 121, 209) | 12px slide distance (lines 261-270) | 2px block-size indicator (line 288)
|
||||
- undeclared parts: data-size | data-depth | data-floating-gap
|
||||
- roles clean: true · variants clean: true
|
||||
|
||||
## Tests (F)
|
||||
- exists: true · env: jsdom
|
||||
- covers: timer scheduling; open/close state sync; trigger-content linkage; keyboard focus navigation (ArrowDown, ArrowRight); accessibility props emission
|
||||
- untested: Home key navigation; End key navigation; Escape key close; ArrowLeft navigation; loop=true wrapping behavior at boundaries; RTL (dir=rtl) keyboard mapping; content focus delegation on Enter; hover-triggered skip-delay window; click-race suppression (lastOpenedAt timing)
|
||||
|
||||
## Style observations (non-blocking)
|
||||
- Responsive size tokens (sm/md/lg) implemented cleanly via CSS custom properties per data-size
|
||||
- Animation easing (ease-out, ease-default) properly referenced from design tokens
|
||||
- Chevron indicator rotates smoothly with transition; uses currentColor for theming flexibility
|
||||
- Content panel z-index hardcoded but references --navigation-menu-content-z (indicates planning for future token)
|
||||
- Trigger + Link share identical chrome (text color, hover, active states) — intentional design
|
||||
@ -0,0 +1,37 @@
|
||||
# Audit: number-field
|
||||
audit-version: 1
|
||||
audited-at: 2026-06-26
|
||||
scope:
|
||||
method: adversarially-verified workflow (analyze → refute); HIGH/CRITICAL lead-verified. Clean-check pseudo-findings dropped. B4 ground-truth: segmented date/time/color-field register inputId via $effect (SYS-A30-EFFECT, no loop); number-field is the direct-assignment reference.
|
||||
provider: G:\dev\svelte\vicen\src\uix\soma\components\number-field\number-field-provider.svelte.ts
|
||||
field-family (A13/A24-26/A30): A13: COMPLIANT - hidden input rendered when name prop exists (line 76-83 in components/number-field.svelte), carries value + name + disabled/required for form submission. A24: NOT APPLICABLE (no readonly-segments with undefined value scenario). A25: NOT APPLICABLE (not a range field). A26: NOT REQUIRED - number-field uses contenteditable-free architecture (native <input type=text> with oninput fil
|
||||
|
||||
## Summary
|
||||
Counts (post-verification): CRITICAL 0 · HIGH 0 · MEDIUM 0 · LOW 1.
|
||||
|
||||
## Findings
|
||||
### LOW: A6 — every timer/listener/observer disposed; provider with no disposer that still owns a t — NF-1 <!-- id: NF-1 -->
|
||||
- dimension: B
|
||||
- rule: A6 — every timer/listener/observer disposed; provider with no disposer that still owns a timer source
|
||||
- location: src/uix/soma/components/number-field/number-field-provider.svelte.ts:653-666 (IncrementTrigger) and 719-731 (DecrementTrigger), repeater created in constructor; SpinPressRepeater.stop at :98-107
|
||||
- evidence: The trigger providers create `this.repeater = createSpinPressRepeater(...)` in their constructors and start keyed `uix.timers` (`schedule`/`interval`) on pointerdown. `repeater.stop()` is wired to onpointerup/leave/cancel/lostpointercapture (props at :694-697 / :760-763), but the provider class has no dispose() that calls `repeater.stop()`. If the component unmounts WHILE a press is mid-flight (delay or interval timer pending), nothing cancels the keyed timer; the pending interval would keep invoking `() => this.provider.increment()` until superseded. The interval task does re-check `if (this.isDisabled())` (:85-88) which mitigates but does not unconditionally stop on unmount.
|
||||
- impact: Tiny-window latent timer survival: requires pointer held on a spin trigger AND simultaneous unmount. Realistically rare; the keyed scheduler is the shared uix.timers (disposed at UIX teardown) and the isDisabled re-check usually halts it. Not a user-visible leak in normal flows.
|
||||
- repro: Press-and-hold an increment trigger, then unmount the NumberField before releasing the pointer; observe whether the keyed interval task is cancelled.
|
||||
- proposed-fix: If a per-component disposer becomes available, call `repeater.stop()` from it; otherwise document that the keyed timers rely on the shared scheduler's lifecycle. No change required if the framework guarantees keyed-timer cleanup on dom dispose.
|
||||
- verify: [verifier-added] added by adversarial verify pass
|
||||
- fix-status: open
|
||||
|
||||
## No-findings dimensions
|
||||
A Contract(morfo), A30 id-wiring, A13 hidden-input, B Behavior(soma), B Keyboard routes vs APG, B A6 cleanup, C DOM-selector safety, D Frontier(soma/eidos isolation), E Theming tokens, E-bis Label font rule, F Tests coverage
|
||||
|
||||
## Theming facts (E-bis)
|
||||
- magic z-index: none
|
||||
- magic literals: none
|
||||
- undeclared parts: none
|
||||
- roles clean: true · variants clean: true
|
||||
- label-font (one step below input?): CORRECT - field recipe defines label at 1-step-below control font via calc(control-font - 1-step); spin-field references --font-size-* tokens correctly (e.g., font-size-md: var(--font-size-md) per THEMING §5)
|
||||
|
||||
## Tests (F)
|
||||
- exists: true · env: jsdom
|
||||
- covers: parseValue with locale separators + rounding + clamp on blur; keyboard arrow/page/home/end/enter routing vs APG spinbutton; increment/decrement trigger clicks and repeater hold-delay; scrubber movement buffer and pointer capture; RTL horizontal scrub inversion (E-W mirror); vertical scrub RTL-independence; field inheritance via FieldProvider context; locale resolution (prop → soma.langs.getLocale → en); direction resolution (prop → soma.prefs.getDir → ltr)
|
||||
- untested:
|
||||
@ -0,0 +1,82 @@
|
||||
# Audit: onion-menu
|
||||
audit-version: 1
|
||||
audited-at: 2026-06-26
|
||||
scope: ['soma', 'sema', 'eidos']
|
||||
method: adversarially-verified workflow (analyze → refute); HIGH/CRITICAL personally re-verified against cited code by the lead.
|
||||
provider: src/uix/eidos/components/onion-menu/onion-menu.svelte (compositional; soma runtime is src/uix/soma/components/onion-menu/onion-menu.svelte.ts, nav helpers isolated in src/uix/soma/components/onion-menu/onion-menu-nav.ts)
|
||||
|
||||
## Summary
|
||||
Counts (post-verification): CRITICAL 0 · HIGH 0 · MEDIUM 1 · LOW 3.
|
||||
systemic hits: SYS-3: interaction-heavy component with keyboard navigation + focus management tested only via unit nav functions; no client test for the eidos root behavior..
|
||||
|
||||
## Findings
|
||||
### MEDIUM: F Tests (Interaction coverage) — onion-menu-004 <!-- id: onion-menu-004 -->
|
||||
- dimension: F
|
||||
- rule: F Tests (Interaction coverage)
|
||||
- location: src/uix/soma/components/onion-menu/
|
||||
- evidence: Only onion-menu-nav.test.ts exists; no onion-menu-provider.svelte.test.ts or eidos component test.
|
||||
- impact: Keyboard behavior is tested only on the radial nav pure functions (wrap, disabled skip). The critical interaction paths are untested: open/close trigger (A17 focus return, two-moments), drill navigation (drill-up via Backspace), focus management in SVG (real focus in sectors), outside-click dismissal (non-modal). This is SYS-3: interaction-heavy + jsdom-only + lacks a client test.
|
||||
- repro: grep -r 'test' src/uix/soma/components/onion-menu/ and src/uix/eidos/components/onion-menu/ — only nav test exists.
|
||||
- proposed-fix: Create src/uix/soma/components/onion-menu/onion-menu-provider.svelte.test.ts and src/uix/eidos/components/onion-menu/onion-menu.test.ts with at least: (1) trigger open/close + focus return; (2) ArrowRight/Left wrapping in a ring with disabled; (3) drill path transitions (Enter on branch); (4) Backspace drill-up; (5) outside-click close; (6) focus remains/restored through open/close cycle. Use client/Playwright if SVG focus testing requires interaction.
|
||||
- verify: [downgraded] Confirmed: only onion-menu-nav.test.ts (pure index math), geometry.test.ts and color.test.ts (pure engines) exist; no onion-menu-provider.svelte.test.ts nor an eidos .svelte test. The integration behavior lives entirely in onion-menu.svelte (openMenu/closeMenu focus-return at lines 268-281, drillUp Backspace 315-324, real DOM focus into SVG sectors via sectorEl().focus() 262/363, outside-click pointerdown dismissal 372-381, focusout Tab-out 335-340) and is UNTESTED. This is a real SYS-3 gap. Downgraded HIGH->MEDIUM per rubric: the highest-risk index math (wrap/disabled-skip/degenerate) IS covered by onion-menu-nav.test.ts; the untested surface is focus/dismissal/two-moments integration = 'missing test on a high-risk path' (MEDIUM).
|
||||
- fix-status: open
|
||||
|
||||
### LOW: E-bis Theming (magic literals) — onion-menu-002 <!-- id: onion-menu-002 -->
|
||||
- dimension: E-bis
|
||||
- rule: E-bis Theming (magic literals)
|
||||
- location: src/uix/eidos/components/onion-menu/onion-menu.css:172-173
|
||||
- evidence: outline: 2px solid var(--focus-ring-color, currentColor);
|
||||
outline-offset: 2px;
|
||||
- impact: Hard-coded px values for focus ring width and offset are not derived from canonical tokens. These should reference --z-index-* or --spacing-* tokens so focus ring sizing is consistent across the design system.
|
||||
- repro: Read onion-menu.css lines 172-173 and check base.ts recipe tokens for onion-menu key — focus ring tokens are missing.
|
||||
- proposed-fix: Define or reference canonical tokens for focus ring: outline should use var(--focus-ring-width, 2px) and outline-offset should use var(--focus-ring-offset, 2px). Wire these from the recipe base.ts if they don't exist.
|
||||
- verify: [downgraded] Confirmed the literals exist - onion-menu.css:171-173: 'outline: 2px solid var(--focus-ring-color, currentColor); outline-offset: 2px;'. But this is the framework's CANONICAL focus-ring treatment, not drift: the color-engine hand-off (2026-06-01) establishes 'outline: 2px solid' as the canonical focus + forced-colors fallback ('outline: 2px solid Highlight'), and Button uses the same. The cited rule/fix is wrong - it claims --z-index-*/--spacing-* should size a focus ring, which is nonsensical. At most a cosmetic consistency nit, not a MEDIUM token-drift defect.
|
||||
- fix-status: open
|
||||
|
||||
### LOW: E-bis Theming (bare numbers in recipe) — onion-menu-003 <!-- id: onion-menu-003 -->
|
||||
- dimension: E-bis
|
||||
- rule: E-bis Theming (bare numbers in recipe)
|
||||
- location: src/uix/eidos/lib/recipes/base.ts:4372-4376
|
||||
- evidence: 'sector-ring-width': '2.5',
|
||||
'sector-hover-ring-width': '2',
|
||||
'muted-opacity': '0.85',
|
||||
'hover-ring-opacity': '0.65'
|
||||
- impact: Token values are bare numbers without units or as decimal opacity values without a wrapping token reference. Recipe tokens should consistently reference named scales or declare full CSS values.
|
||||
- repro: Check base.ts lines 4372-4376 and verify the tokens are used correctly in onion-menu.css (e.g., stroke-width: var(--onion-menu-sector-ring-width) should resolve to a valid CSS value).
|
||||
- proposed-fix: Either fully qualify with units ('2.5px' for ring-width) or wrap opacity in a token lookup. Consider defining canonical --opacity-muted, --opacity-hover-ring as theme tokens that the recipe can then reference. Or emit the values as they are consumed (px for widths, unitless for opacity).
|
||||
- verify: [downgraded] Mixed claim. The ring-width tokens base.ts:4372-4373 'sector-ring-width':'2.5' / 'sector-hover-ring-width':'2' are consumed as SVG stroke-width: var(--onion-menu-sector-ring-width) (onion-menu.css:53,86,102) - unitless user-space lengths are VALID for SVG stroke-width, NOT a defect; the candidate's 'add px' fix is wrong. The opacity decimals base.ts:4375-4376 'muted-opacity':'0.85' / 'hover-ring-opacity':'0.65' are the only substantive part: a canonical numeric scale --opacity-{0..100} (Tailwind step-5) exists and is widely consumed, so these could reference var(--opacity-85)/var(--opacity-65). Real but minor; the candidate over-broadens and misprescribes.
|
||||
- fix-status: open
|
||||
|
||||
### LOW: C DOM-selector (querySelector safety) — onion-menu-005 <!-- id: onion-menu-005 -->
|
||||
- dimension: C
|
||||
- rule: C DOM-selector (querySelector safety)
|
||||
- location: src/uix/eidos/components/onion-menu/onion-menu.svelte:112-113
|
||||
- evidence: const ring = surfaceEl?.querySelectorAll<SVGPathElement>(
|
||||
`[data-onion-menu-item][data-depth="${level + 1}"]`
|
||||
);
|
||||
- impact: The interpolation is numeric-only (level + 1 produces a number), so CSS.escape is not needed here. However, the selector construction is fragile: it relies on DOM order matching the index array exactly. If the SVG structure changes (conditional rendering, key changes), the nth-child relationship breaks silently.
|
||||
- repro: Read onion-menu.svelte line 111-115 and trace how sectors are inserted/removed — order assumption is implicit in the array access at line 115 (ring?.[index]).
|
||||
- proposed-fix: Consider using a Map<key, element> stored during render instead of querying by index. Alternatively, add a data-sector-id that is both numeric and query-stable (e.g., data-sector-id="${level}-${index}") and query by that instead of relying on order.
|
||||
- verify: [confirmed] Confirmed at LOW (the candidate's own severity). onion-menu.svelte:112-113: surfaceEl?.querySelectorAll<SVGPathElement>(`[data-onion-menu-item][data-depth="${level + 1}"]`). The interpolated value is numeric (level+1, internal counter), never consumer-derived, so CSS.escape is correctly NOT required - the candidate concedes this. Only the DOM-order reliance (ring?.[index]) is a design note, and lines 107-116 explicitly justify it (avoids bind:this into a computed key + a ref-map timing race). Borderline a styleObservation; no actual injection/fragility defect.
|
||||
- fix-status: open
|
||||
|
||||
## No-findings dimensions
|
||||
A, B, G
|
||||
|
||||
## Theming facts (E-bis)
|
||||
- magic z-index: none
|
||||
- magic literals: 2px focus ring width (line 172 onion-menu.css) → --onion-menu-focus-ring-width | 2px outline-offset (line 173 onion-menu.css) → --onion-menu-focus-ring-offset | '2.5' sector-ring-width in recipe (line 4372 base.ts) → should be '2.5px' or unitless opacity | '0.85' muted-opacity (line 4375 base.ts) → should wrap in --opacity-* token
|
||||
- undeclared parts: none
|
||||
- roles clean: true · variants clean: true
|
||||
- conformance: Morfo is 'as const satisfies Morfo' ✓; all parts match declared data/aria; naming uses --onion-menu-{slot} for public tokens ✓; no invented theme roles or sizes ✓
|
||||
|
||||
## Tests (F)
|
||||
- exists: true · env: vitest (unit tests only — onion-menu-nav.test.ts has no @vitest-environment directive, defaults to node/jsdom)
|
||||
- covers: onionNavNext/Prev wrapping; onionNavFirst with disabled leading; disabled option skipping; degenerate cases (empty ring, all-disabled)
|
||||
- untested: Open/close trigger via Enter/Space/ArrowDown (keyboard); Focus return to trigger on close (A17); Drill into a branch (data-expanded toggle, drill-path mutation); Drill-up via Backspace (drill-path rollback); ArrowLeft/ArrowRight focus movement in the active ring; SVG sector focus() call and roving focus state sync; Outside-click dismissal while open (non-modal); Programmatic open state change (pendingFocus queueing); All perceptual signals (open/close/commit-select) routing correctly; Focus return when Menu is closed without selecting a leaf
|
||||
|
||||
## Style observations (non-blocking)
|
||||
- The component uses theme-driven colour derivation (color-mix) consistently for fills/text, avoiding hardcoded hex/rgb/oklch ✓
|
||||
- CSS classes use .onion-menu-* prefix throughout; data-* attributes follow [data-onion-menu-*] and [data-onion-menu-{part}] correctly ✓
|
||||
- The focus-visible ring styling is semantic (uses currentColor) but literals (2px) should be tokens ✓
|
||||
- Sector animation uses a simple opacity keyframe with @media prefers-reduced-motion guard ✓
|
||||
@ -0,0 +1,40 @@
|
||||
# Audit: pagination
|
||||
audit-version: 1
|
||||
audited-at: 2026-06-26
|
||||
scope: (SCOPE-DRIFT → SYS-1)
|
||||
method: adversarially-verified workflow (analyze → refute); HIGH lead-verified by direct read of the cited code. B5 ground-truth: the A31 O(N²) isSelected/isExpanded (.includes from a per-item $derived) is confirmed across listbox/grid-list/tree-view/tree-grid/tag-group (SYS-7); rovingTargetEl is correctly LIFTED everywhere (not A31); virtual-* use SvelteMap (A33-clean).
|
||||
provider: src/uix/soma/components/pagination/pagination-provider.svelte.ts
|
||||
reactivity (A31/A33/A35): CLEAN. Per-item isSelected (line 457-459) uses simple comparison operator, no global state read. Per-item isDisabled (line 461) reads provider.opts.disabled.current (direct ref access, O(1)). Per-item ariaLabel (line 463-468) calls soma.langs.t() but with local item value only. No A31 quadratic patterns detected. No A33 $state(Map/Set). No A35 per-item $effect loops. Clamping effect (lines 116-122
|
||||
|
||||
## Summary
|
||||
Counts (post-verification): CRITICAL 0 · HIGH 0 · MEDIUM 1 · LOW 0.
|
||||
|
||||
## Findings
|
||||
### MEDIUM: SYS-1 SCOPE-DRIFT: eidos recipe dir exists but morfo scope omits 'eidos' — pagination-001 <!-- id: pagination-001 -->
|
||||
- dimension: Contract (morfo)
|
||||
- rule: SYS-1 SCOPE-DRIFT: eidos recipe dir exists but morfo scope omits 'eidos'
|
||||
- location: src/uix/morfo/components/pagination.ts:7
|
||||
- evidence: Morfo declares scope: ['soma', 'sema'] but eidos directory exists at src/uix/eidos/components/pagination/ with README confirming pagination is 'el wrapper visual de Eidos sobre el primitivo Soma'.
|
||||
- impact: Scope mismatch signals incomplete layer declaration. Eidos layer is real and should be listed in morfo scope.
|
||||
- proposed-fix: Update morfo scope from ['soma', 'sema'] to ['soma', 'sema', 'eidos'] to match the existing eidos layer.
|
||||
- verify: [confirmed] Confirmed SYS-1 scope-drift. src/uix/morfo/components/pagination.ts:7 declares `scope: ['soma', 'sema'],` but a complete eidos layer exists: src/uix/eidos/components/pagination/ contains pagination.css (4255 bytes), 7 Svelte wrappers, index.ts, types.ts, and a README, plus a recipe entry at src/uix/eidos/lib/recipes/base.ts:1715 (`pagination: {`). Peer components with an eidos layer include it in scope (accordion.ts:7 `scope: ['soma', 'sema', 'eidos']`, button.ts:47, checkbox.ts:7). The omission is a real layer-declaration drift. MEDIUM is correct.
|
||||
- fix-status: open
|
||||
|
||||
## No-findings dimensions
|
||||
Contract parts registration (2-of-3 rule verified: all 7 parts registered), Contract data/aria naming (data-{c}-{part} pattern used, no data-soma-* violations), Contract Morfo as const satisfies, Behavior A31 per-item O(N²) derived reading global state, Behavior A33 $state(new Map/Set) reactivity, Behavior A35/A36 per-item $effect ref-write loops, Behavior A30 id-registration via $effect, Behavior A6 cleanup (no Resource leaks detected), Behavior A18/A10/A14/A12 keyboard navigation (N/A: pagination uses snippet-based rendering, not nav), Behavior A34 require() topology (all triggers/items require parent Provider, DOM descendants), DOM-selector (no querySelector usage), Frontier eidos→soma import (correct), Frontier syncAttrs double-write (Provider and Ellipsis only, no conflicting props), Theming roles/tokens (recipe uses --space-*, --font-size-*, --radius-*, --opacity-*, --color-* role references), Tests environment (jsdom with @vitest-environment directive), Tests coverage (page ranges, clamping, navigation, disabled state all tested)
|
||||
|
||||
## Theming facts (E-bis)
|
||||
- magic z-index: none
|
||||
- magic literals: none
|
||||
- undeclared parts: none
|
||||
- roles clean: true · variants clean: true
|
||||
|
||||
## Tests (F)
|
||||
- exists: true · env: jsdom (via @vitest-environment directive)
|
||||
- covers: Page range computation with siblingCount=1, boundaryCount=1; Page clamping when value exceeds totalPages; Array slicing via .slice(); Navigation methods: goToNextPage, goToPrevPage, goToFirstPage, goToLastPage; Provider/trigger/item/ellipsis attrs through runtime; aria-current and aria-label projection; disabled state propagation (provider + item + trigger); onclick handlers for all triggers and items
|
||||
- untested: Edge case: siblingCount/boundaryCount extremes (e.g., siblingCount > totalPages); Dynamic count/pageSize/siblingCount/boundaryCount changes post-init; onPageChange callback firing on page mutation
|
||||
|
||||
## Style observations (non-blocking)
|
||||
- FirstTrigger and LastTrigger disable when already at boundaries (page 1 / last page), consistent with Ark UI pattern; intentional UX choice not a bug
|
||||
- Page range algorithm uses Set deduplication before sort, efficient O(n log n) approach
|
||||
- Comments accurately describe behavior (e.g., line 222 mentions hasPrevPage check is intentional)
|
||||
@ -0,0 +1,42 @@
|
||||
# Audit: password-field
|
||||
audit-version: 1
|
||||
audited-at: 2026-06-26
|
||||
scope: (SCOPE-DRIFT → SYS-1)
|
||||
method: adversarially-verified workflow (analyze → refute); HIGH/CRITICAL lead-verified. Clean-check pseudo-findings dropped. B4 ground-truth: segmented date/time/color-field register inputId via $effect (SYS-A30-EFFECT, no loop); number-field is the direct-assignment reference.
|
||||
provider: src/uix/soma/components/password-field/password-field-provider.svelte.ts
|
||||
field-family (A13/A24-26/A30): A30 compliant: inputId registered directly in constructor (line 142) without $effect. Not A13 field (no hidden input required). Not A26 field (not segmented contenteditable). Not A24/A25 field (not date/range).
|
||||
|
||||
## Summary
|
||||
Counts (post-verification): CRITICAL 0 · HIGH 0 · MEDIUM 1 · LOW 0.
|
||||
|
||||
## Findings
|
||||
### MEDIUM: SCOPE-DRIFT: Eidos recipe directory exists but morfo 'scope' omits 'eidos' — password-field-001 <!-- id: password-field-001 -->
|
||||
- dimension: B, SYS-1
|
||||
- rule: SCOPE-DRIFT: Eidos recipe directory exists but morfo 'scope' omits 'eidos'
|
||||
- location: src/uix/morfo/components/password-field.ts:31
|
||||
- evidence: scope: ['soma', 'sema'], — but eidos recipe exists at src/uix/eidos/lib/recipes/base.ts:2097 and components at src/uix/eidos/components/password-field/
|
||||
- impact: Component maintainers may miss that eidos layer exists; recipe synchronization risk if scope drift persists across the codebase
|
||||
- proposed-fix: Change line 31 to scope: ['soma', 'sema', 'eidos']
|
||||
- verify: [confirmed] CONFIRMED at the cited severity. morfo src/uix/morfo/components/password-field.ts:31 declares `scope: ['soma', 'sema'],` — 'eidos' is omitted. Yet eidos demonstrably implements the component: (a) the recipe entry `'password-field': {` exists at src/uix/eidos/lib/recipes/base.ts:2097 (the candidate's cited line is exact), with control-height/space/font tokens following; (b) the eidos component directory src/uix/eidos/components/password-field/ contains 8 files (password-field.svelte, -input, -visibility-trigger, -strength-meter, -caps-lock-indicator, types.ts, index.ts, password-field.css). The `scope` field is documented in src/uix/morfo/types.ts:799 as 'Layers that implement this component', so the declared array genuinely diverges from the real implementation. This is the established SYS-1 scope-drift pattern (MEDIUM in baseline) — informational/metadata drift, not a behavioral or a11y bug: the component renders and works; the `scope` array does not gate eidos at runtime. MEDIUM is the correct severity (not HIGH — no latent behavioral failure; not LOW — it is a real contract/metadata inconsistency the coverage tooling tracks). Note: this is systemic, not unique to password-field — calendar (calendar.ts:7), color-field (color-field.ts:7), color-picker, combobox, command and other components likewise carry `['soma', 'sema']` while shipping eidos directories, consistent with SYS-1 being a confirmed systemic finding.
|
||||
- fix-status: open
|
||||
|
||||
## No-findings dimensions
|
||||
A Contract (Morfo), A Behavior (Soma), C DOM-selector, D Frontier, E TSC, E-bis Theming, F Tests
|
||||
|
||||
## Theming facts (E-bis)
|
||||
- magic z-index: none
|
||||
- magic literals: z-index: 1 (line 74 password-field.css) — LOCAL subtree stacking only, acceptable per rules
|
||||
- undeclared parts: none
|
||||
- roles clean: true · variants clean: true
|
||||
- label-font (one step below input?): N/A — password-field is composable (no built-in label). Consumers use Field.Label wrapper whose font is controlled by field-level rules.
|
||||
|
||||
## Tests (F)
|
||||
- exists: false · env: jsdom
|
||||
- covers:
|
||||
- untested: visibility toggle keydown/click; caps-lock signal on/off lifecycle (stateBound persistence); strength meter clamping and warnings flow; field integration (inputId wiring, disabled/readonly/required inheritance); validation paths (invalid state + error id describedby wiring)
|
||||
|
||||
## Style observations (non-blocking)
|
||||
- Line 74 password-field.css: z-index: 1 is LOCAL subtree stacking (children above pseudo-element chrome), compliant per rules
|
||||
- Theming: 9 roles used correctly (primary/secondary/neutral/affirm/fulfill/risk/threat/loss); caps-indicator uses risk-track/risk-text role alias
|
||||
- Recipe tokens (font-size, spacing, radius) reference canonical CSS variables; no magic pixels/em/% drift
|
||||
- Label font rule N/A: password-field has no internal label; consumer wraps with Field.Label (font controlled at field level)
|
||||
@ -0,0 +1,52 @@
|
||||
# Audit: picker-shell
|
||||
audit-version: 1
|
||||
audited-at: 2026-06-26
|
||||
scope: INTERNAL morfo (morfo/internal/picker-shell.ts) (SCOPE-DRIFT → SYS-1)
|
||||
method: adversarially-verified workflow (analyze → refute); HIGH/CRITICAL personally re-verified by the lead. Batch-3 ground-truth: each picker fires trigger(close) (close NOT inert), but open/commit-reset ARE inert; calendar/range-calendar are MID-REFACTOR (uncommitted view-switch work).
|
||||
provider: src/uix/soma/components/picker-shell/components/picker-shell.svelte
|
||||
|
||||
## Summary
|
||||
Counts (post-verification): CRITICAL 0 · HIGH 0 · MEDIUM 1 · LOW 1.
|
||||
systemic hits: SYS-1 scope-drift (known pattern per audit baseline).
|
||||
composition (A27): Re-export pattern (A27 verified): Clear/Cancel/Close compose Button (proper wrapper). Footer/Header/Body are pure layout zones. Picker-shell parts are imported and re-exported under DatePicker/ColorPicker/etc namespaces, not re-implemented. Shared-ref wiring: pickerShellContext set by host picker provider (DatePickerProvider.pickerShellHandle), read by Footer/Clear/Cancel/Close via getPickerShellHandle().
|
||||
|
||||
## Findings
|
||||
### MEDIUM: SYS-1 SCOPE-DRIFT — picker-shell-001 <!-- id: picker-shell-001 -->
|
||||
- dimension: A
|
||||
- rule: SYS-1 SCOPE-DRIFT
|
||||
- location: src/uix/morfo/internal/picker-shell.ts:32 (morfo scope declaration)
|
||||
- evidence: Morfo declares `scope: ['soma']` at line 32, but eidos directory exists at `src/uix/eidos/components/picker-shell/` with 7 visual components: picker-shell.svelte, picker-shell-header.svelte, picker-shell-body.svelte, picker-shell-footer.svelte, picker-shell-clear.svelte, picker-shell-cancel.svelte, picker-shell-close.svelte. Comment at line 23 explicitly states 'Scope is `soma` only' but implementation contradicts.
|
||||
- impact: Morfo contract mismatch: scope declaration does not reflect actual layer composition. Audit tools scanning morfo/components/ correctly skip this (internal placement), but if ever moved to public, scope would need updating.
|
||||
- repro: Read src/uix/morfo/internal/picker-shell.ts line 32 and compare to ls src/uix/eidos/components/picker-shell/ — 7 files present.
|
||||
- proposed-fix: Update morfo scope from `scope: ['soma']` to `scope: ['soma', 'eidos']` to match the architecture. Alternatively, if intentionally minimizing the contract surface, document that eidos-layer parts are NOT part of the public morfo (correct per current design) and clarify in the comment.
|
||||
- verify: [downgraded] Facts confirmed but severity overstated. Read src/uix/morfo/internal/picker-shell.ts:32 `scope: ['soma'],` and the eidos dir DOES exist (ls showed picker-shell.svelte/-header/-body/-footer/-clear/-cancel/-close + picker-shell.css + recipe usage). HOWEVER this is NOT undocumented SYS-1 drift: picker-shell.ts:23-25 explicitly states `Scope is \`soma\` only — no semantic events; the host picker emits commit-*/shift-* on its own provider. No \`expression\` field because there is no morfo-emitted event to express.` The README (lines 15-19) and audit-codex P1 #5 closure (line 51) document picker-shell as an INTERNAL primitive whose morfo is deliberately a single Provider stub. The eidos divs (`data-picker-shell`, `data-picker-footer`) are standalone layout containers, NOT morfo-declared parts — so there is no contract<->visual part mismatch, which is what SYS-1/2-of-3 actually polices. The candidate's own proposedFix concedes 'correct per current design'. Real, intentional, documented => LOW doc/observation, not MEDIUM scope-drift. SYS-1 baseline is for components with an UNINTENDED scope omission, not a documented internal contract-surface minimization.
|
||||
- fix-status: open
|
||||
|
||||
### LOW: Unused emitted data attribute — picker-shell-002 <!-- id: picker-shell-002 -->
|
||||
- dimension: E-bis
|
||||
- rule: Unused emitted data attribute
|
||||
- location: src/uix/eidos/components/picker-shell/picker-shell.svelte:19 and picker-shell-footer.svelte:15
|
||||
- evidence: Both picker-shell.svelte and picker-shell-footer.svelte emit `data-mode={mode}` attribute (`<div data-picker-shell data-mode={mode} ...>` and `<div data-picker-footer data-mode={mode} ...>`) but this attribute is never consumed — not referenced in picker-shell.css, not grepped in any CSS, not used by sema or other components.
|
||||
- impact: Dead attribute emitted to DOM; adds noise without utility. Low impact cosmetic issue.
|
||||
- repro: grep -n 'data-mode' src/uix/eidos/components/picker-shell/*.svelte src/uix/eidos/components/picker-shell/*.css — only .svelte files match, no .css usage found.
|
||||
- proposed-fix: Either remove `data-mode={mode}` from both components if unused, OR add CSS rules consuming it (e.g., `[data-picker-footer][data-mode='modal']`) if modal-specific styling is intended.
|
||||
- verify: [confirmed] Confirmed. picker-shell.svelte:19 `<div data-picker-shell data-mode={mode} {...rest}>` and picker-shell-footer.svelte:15 `<div data-picker-footer data-mode={mode} {...rest}>` both emit data-mode, where `mode = $derived(shell?.getMode() ?? 'inline')`. grep for 'mode' in picker-shell.css => 'No matches found'; grep 'data-mode' across all of src/uix => only the two emit sites; no sema file references getMode/data-mode. The comment at picker-shell.svelte:10-12 ('reads `mode` off `pickerShellContext` ... so the CSS can react to inline vs modal') states an intent the CSS never fulfills — the attribute is inert/dead today. getMode() is a real interface method (picker-shell-handle.svelte.ts:21), so it does not crash; purely a dead attribute / unfulfilled-comment-intent. LOW cosmetic is correct.
|
||||
- fix-status: open
|
||||
|
||||
## No-findings dimensions
|
||||
B, C, D, E, F, G
|
||||
|
||||
## Theming facts (E-bis)
|
||||
- magic z-index: none
|
||||
- magic literals: none
|
||||
- undeclared parts: Header | Body | Footer | Clear | Cancel | Close
|
||||
- roles clean: true · variants clean: true
|
||||
- conformance: Internal morfo with minimal contract (1 part: Provider). Eidos-layer parts (Header/Body/Footer/Clear/Cancel/Close) are NOT declared in morfo but ARE properly exported and composed. This is by design per the INTERNAL status. All tokens follow canonical naming (--space-*, --color-*, --font-*, --border-width). No magic hex/rgb/hsl/z-index. No double-write or syncAttrs issues. A27 composition verified: parts are re-exported by 5 picker families, not re-implemented.
|
||||
|
||||
## Tests (F)
|
||||
- exists: false · env: N/A
|
||||
- covers:
|
||||
- untested: Context registration flow; pickerShellHandle contract across inline/modal modes; Clear/Cancel/Close button click handlers; Visual context size propagation; Popover content scoping by picker-size
|
||||
|
||||
## Style observations (non-blocking)
|
||||
- CSS is clean, readable, well-commented. Uses custom properties for spacing/sizing uniformly. Clear separation of layout concerns (gap, alignment, borders) vs component-owned styling (Button recipe owns button chrome). Selector specificity is appropriate for scoped composition (`[data-popover-content][data-picker-size='X']` cascade).
|
||||
@ -0,0 +1,36 @@
|
||||
# Audit: pin-input
|
||||
audit-version: 1
|
||||
audited-at: 2026-06-26
|
||||
scope:
|
||||
method: adversarially-verified workflow (analyze → refute); HIGH/CRITICAL lead-verified. Clean-check pseudo-findings dropped. B4 ground-truth: segmented date/time/color-field register inputId via $effect (SYS-A30-EFFECT, no loop); number-field is the direct-assignment reference.
|
||||
provider: src/uix/soma/components/pin-input/pin-input-provider.svelte.ts
|
||||
field-family (A13/A24-26/A30): A13-PASS: Hidden input rendered at line 92 of src/uix/soma/components/pin-input/components/pin-input.svelte with name attribute propagated from opts.name (line 366 of provider). A30-PASS: inputId registered via direct assignment in PinInputProvider constructor (line 124 of pin-input-provider.svelte.ts), NOT $effect, matching the Field.inputId convention. Test confirms at line 136 of pin-input-prov
|
||||
|
||||
## Summary
|
||||
Counts (post-verification): CRITICAL 0 · HIGH 0 · MEDIUM 1 · LOW 0.
|
||||
|
||||
## Findings
|
||||
### MEDIUM: THEMING §5: Recipe font-weight tokens MUST reference --font-weight-* variables, not litera — pin-input-001 <!-- id: pin-input-001 -->
|
||||
- dimension: E-bis: Theming
|
||||
- rule: THEMING §5: Recipe font-weight tokens MUST reference --font-weight-* variables, not literal numeric values
|
||||
- location: src/uix/eidos/lib/recipes/base.ts:4190
|
||||
- evidence: 'cell-font-weight': '500' should be 'var(--font-weight-medium)'
|
||||
- impact: Decouples the pin-input cell font weight from the canonical type scale, breaking theming coherence if --font-weight-medium is ever changed
|
||||
- proposed-fix: Change line 4190 from "'cell-font-weight': '500'," to "'cell-font-weight': 'var(--font-weight-medium)',"
|
||||
- verify: [confirmed] Confirmed at src/uix/eidos/lib/recipes/base.ts:4190: the line is exactly "'cell-font-weight': '500'," — a numeric literal. The canonical token exists (generated/base.css:341: "--font-weight-medium: 500;"), so 500 maps 1:1 to --font-weight-medium and should consume the value-preserving token. This is genuine drift, not a physically-fixed value: a grep across the entire base.ts recipe file shows EVERY other font-weight declaration uses var(--font-weight-*) (e.g. lines 48, 153, 314, 1453-1457, 1669, 1686, 3670-3674, 4196 contexts), and a regex for numeric-literal font-weights ('font-weight':'<digit>) matches ONLY line 4190 — it is the sole offender in the whole file. THEMING §5 / 'no magic literals — tokenize don't intentionalize' (feedback_no_intentional_magic_numbers): a literal that equals a scale step must reference the token. MEDIUM (token inconsistency / magic literal) and high confidence are both appropriate.
|
||||
- fix-status: open
|
||||
|
||||
## No-findings dimensions
|
||||
A: Contract (morfo), B: Behavior (soma), C: DOM-selector, D: Frontier (soma/eidos boundary), E: TSC, F: Tests, G: Redundancy
|
||||
|
||||
## Theming facts (E-bis)
|
||||
- magic z-index: none
|
||||
- magic literals: pin-input recipe line 4190: literal '500' for cell-font-weight
|
||||
- undeclared parts: none
|
||||
- roles clean: true · variants clean: true
|
||||
- label-font (one step below input?): N/A (no visible label component in morfo; soma uses aria-label on hidden input)
|
||||
|
||||
## Tests (F)
|
||||
- exists: true · env: jsdom
|
||||
- covers: onComplete callback firing once when value reaches length; pattern filtering on oninput; paste transformation and distribution; selection tracking and cell state derivation; focus/blur lifecycle; Field flag OR-merging (disabled, readonly, required, invalid)
|
||||
- untested: IME/drop edge cases (note: keydown.preventDefault alone is insufficient per A26 — but this is not a segmented input so A26 does not apply)
|
||||
@ -0,0 +1,95 @@
|
||||
# Audit: popover
|
||||
audit-version: 1
|
||||
audited-at: 2026-06-26
|
||||
scope: ['soma', 'sema'] (eidos recipe present though 'eidos' not in morfo scope — systemic SCOPE-DRIFT)
|
||||
files:
|
||||
- provider: src/uix/soma/components/popover/popover-provider.svelte.ts (present)
|
||||
- morfo: src/uix/morfo/components/popover.ts (present)
|
||||
- recipe: src/uix/eidos/components/popover/popover.css + lib/recipes/base.ts §popover (present)
|
||||
- demo: web/routes/uix/components/popover (present, not inspected this pass)
|
||||
- test: src/uix/soma/components/popover/popover-provider.svelte.test.ts (present, jsdom)
|
||||
- readme: present (not inspected this pass)
|
||||
|
||||
## Summary
|
||||
The cleanest of the three overlays audited so far. Popover gets the hard parts right: correct two-moments
|
||||
(`present`/`close` sequence 'pre', emit-before-state), modal-derived FocusScope/ScrollLock (A16), the
|
||||
hover-bounce guards (re-fire-while-open no-op; modal+hover loop guard) that match the recorded popover fixes,
|
||||
dismissal trigger-exclusion via `contains()` (no `querySelector` injection), timer cleanup (A6), a clean
|
||||
eidos→soma frontier, NO `role` double-write (it lets the morfo own role), and a Close that composes `<Button>`
|
||||
with no bespoke recipe. Only real defect: the Close button re-declares `type`/`aria-label` that `syncAttrs`
|
||||
already writes — the same double-write anti-pattern the component's own Trigger comment warns against. Plus the
|
||||
systemic magic z-index and a jsdom-only test gap on the dismissal/focus paths.
|
||||
|
||||
Counts: CRITICAL 0 · HIGH 0 · MEDIUM 3 · LOW 0.
|
||||
|
||||
## Findings
|
||||
|
||||
### MEDIUM: Close button re-declares `type` + `aria-label` already written by `syncAttrs` (double-write) <!-- id: popover-001 -->
|
||||
- dimension: D, A
|
||||
- rule: active_architecture §7.7 (one authority per attribute) + the component's own discipline (trigger comment :423-426)
|
||||
- location: popover-provider.svelte.ts:739-758 (Close registered `syncAttrs: true`, then props sets `type` + `aria-label`)
|
||||
- evidence:
|
||||
```ts
|
||||
this.runtimePart = this.provider.runtime.part('close', { …, syncAttrs: true });
|
||||
…
|
||||
readonly props = $derived.by(() => ({
|
||||
...this.runtimePart.props,
|
||||
type: 'button' as const,
|
||||
'aria-label': this.provider.soma.langs.ts(POPOVER_LANGS.CLOSE),
|
||||
onclick: this.onclick
|
||||
}));
|
||||
```
|
||||
The morfo Close declares `type` (literal 'button') and `aria-label` (`v.commonRef('buttons.close')`,
|
||||
morfo:209-216); with `syncAttrs: true` the runtime writes both. The props block writes them again. The
|
||||
Trigger comment a few hundred lines up explicitly says "Spreading them manually here would duplicate (and
|
||||
potentially desync) the runtime's authority" — the Close violates exactly that.
|
||||
- impact: benign today (both resolve to the same value), but it's duplicate authority + desync risk if
|
||||
`POPOVER_LANGS.CLOSE` and the morfo `commonRef` ever diverge, and an internal inconsistency in the
|
||||
component's own discipline. (Contrast Dialog where the analogous double-write IS a live bug — dialog-001.)
|
||||
- proposed-fix: drop `type`/`aria-label` from the Close props; let `syncAttrs` own them (the morfo already
|
||||
declares both). Keep only `onclick`.
|
||||
- fix-status: open
|
||||
|
||||
### MEDIUM: `content-z: '75'` / `overlay-z: '60'` magic z-index literals <!-- id: popover-002 -->
|
||||
- dimension: E-bis
|
||||
- rule: THEMING §35 Bloque C (z-index magic → `--z-index-*` tokens)
|
||||
- location: lib/recipes/base.ts:1753-1754 (`'content-z': '75'`, `'overlay-z': '60'`); popover.css:53 + :63.
|
||||
- evidence: bare integers where `var(--z-index-*)` tokens belong. Third instance after `select.content-z: '80'`
|
||||
and `dialog.overlay-z: '70'` → confirms the SYSTEMIC z-ladder finding (see THEMING_COHERENCE / SUMMARY).
|
||||
- impact: the overlay stacking ladder lives as scattered per-recipe integers (60/70/75/80…) that can't be
|
||||
coordinated or reasoned about centrally.
|
||||
- proposed-fix: introduce a canonical `--z-index-{overlay,popover,modal}` ladder and map all overlay recipes
|
||||
to it in one sweep.
|
||||
- fix-status: open
|
||||
|
||||
### MEDIUM: Tests cover toggle/hover/labelling; dismissal trigger-exclusion, focus, and polymorphic close causes untested; jsdom-only <!-- id: popover-003 -->
|
||||
- dimension: F
|
||||
- rule: dimension-F honesty check
|
||||
- location: popover-provider.svelte.test.ts (3 tests: toggle, hover open/close via timers + emit, title/description labelling)
|
||||
- evidence: no test exercises the `onInteractOutside` trigger-exclusion bounce guard (:559-568, a documented
|
||||
past bug), the Escape path, FocusScope trap+return (jsdom can't), or the `save`/`fail`/`cancel` close causes
|
||||
(only `dismiss` is hit, via hover-close). `@vitest-environment jsdom` (:1).
|
||||
- impact: the bounce-guard regression and focus behavior would ship unverified.
|
||||
- proposed-fix: add a provider test that fires an interact-outside landing on the trigger (asserts no double
|
||||
toggle), parametrize the close causes, and add a client/Playwright test for focus trap+return.
|
||||
- fix-status: open
|
||||
|
||||
## No-findings dimensions
|
||||
- **B (behavior):** two-moments ordering correct; hover bounce guards present and correct (re-fire-while-open
|
||||
no-op :284, modal+hover loop guard :405/:419); FocusScope/ScrollLock derive from `modal` (A16); dismissal
|
||||
trigger-exclusion via `contains()`; `hoverTimer` cleaned in `$effect` return (A6). Clean.
|
||||
- **C (selectors):** uses `contains(triggerNode, target)` — no `querySelector` with interpolated user values. Clean.
|
||||
- **D (frontier, severe half):** provider imports only soma layers + morfo — zero eidos imports.
|
||||
- **A (contract):** the virtual Provider part declares empty `data` (no unreachable-data issue, unlike Dialog);
|
||||
Trigger correctly lets the morfo own aria/type/data-state via `syncAttrs` (no override, unlike Dialog's role).
|
||||
All CSS-targeted parts (`title`/`description`/`arrow`/`overlay`) are morfo-declared (no undeclared-part drift,
|
||||
unlike Select/Dialog).
|
||||
- **E (TSC):** no `data-color`, no derived-token-at-root risk; clean.
|
||||
|
||||
## Style observations (opinion, non-blocking)
|
||||
- Popover is the positive reference among the overlays for: (a) Close-via-`<Button>` with no bespoke recipe
|
||||
(the model Dialog's stale trigger envelope should follow), (b) letting the morfo own Trigger ARIA without a
|
||||
manual override (the model Dialog's role override violates), and (c) the documented hover-bounce guards.
|
||||
Worth citing in the SUMMARY as the "do it like Popover" baseline.
|
||||
- The one inconsistency (Close double-write, popover-001) is small but notable precisely because the same
|
||||
file's Trigger comment articulates the correct rule — fixing it makes the file self-consistent.
|
||||
@ -0,0 +1,35 @@
|
||||
# Audit: progress
|
||||
audit-version: 1
|
||||
audited-at: 2026-06-26
|
||||
scope: (SCOPE-DRIFT → SYS-1)
|
||||
method: adversarially-verified workflow; HIGH lead-verified. B7 ground-truth: Toolbar A35 + Form A36 incidents STAYED FIXED (untrack present); toast/clipboard/drag-drop use uix.timers (no A6 leak); button is clean (its archetype finding was inert — Spinner not runtime-registered); data-size/data-shape are eidos visual attrs (not contract violations).
|
||||
provider: G:\dev\svelte\vicen\src\uix\soma\components\progress\progress-provider.svelte.ts
|
||||
cleanup-audit (A6/A35/A36): A6 cleanup audit: Zero timers, observers, listeners. No setTimeout, setInterval, ResizeObserver, IntersectionObserver, MutationObserver, addEventListener in soma/components/progress. Test uses $effect.root() with proper disposer cleanup (line 21-24 captured, line 126 cleanup called, line 127 dom.dis
|
||||
|
||||
## Summary
|
||||
Counts (post-verification): CRITICAL 0 · HIGH 0 · MEDIUM 1 · LOW 0.
|
||||
|
||||
## Findings
|
||||
### MEDIUM: SYS-1 scope-drift: morfo scope[] must include 'eidos' when an eidos recipe + component dir — PROGRESS-SCOPE-1 <!-- id: PROGRESS-SCOPE-1 -->
|
||||
- dimension: A
|
||||
- rule: SYS-1 scope-drift: morfo scope[] must include 'eidos' when an eidos recipe + component dir exist for the component.
|
||||
- location: src/uix/morfo/components/progress.ts:7
|
||||
- evidence: `scope: ['soma'],` — yet a full eidos recipe exists at src/uix/eidos/lib/recipes/base.ts:1799 (`progress: { 'height-xs': ... }`) plus a complete eidos component dir (src/uix/eidos/components/progress/{progress.svelte,progress.css,types.ts}). Compare button.ts:`scope: ['soma','sema','eidos']`, spinner.ts/skeleton.ts:`scope: ['eidos']` — all eidos-backed components declare 'eidos'.
|
||||
- impact: The morfo contract under-declares the component's real consumer layers; any tooling that walks `scope` to know which layers materialize a component (lint/coverage/codegen) will skip eidos for Progress even though eidos owns its size/shape visual surface. Same drift class as meter.ts (also `['soma']`), so it is a shared B7-family systemic issue, not a one-off.
|
||||
- proposed-fix: Add 'eidos' to the scope tuple: `scope: ['soma', 'eidos']`. (No 'sema' — Progress declares no events, expression is none/passive, which is correct.)
|
||||
- verify: [verifier-added] added by adversarial verify pass
|
||||
- fix-status: open
|
||||
|
||||
## No-findings dimensions
|
||||
A_contract, B_behavior, C_dom_selector, D_frontier, E_theming, F_tests, G_redundancy
|
||||
|
||||
## Theming facts (E-bis)
|
||||
- magic z-index: none
|
||||
- magic literals: none
|
||||
- undeclared parts: none
|
||||
- roles clean: true · variants clean: true
|
||||
|
||||
## Tests (F)
|
||||
- exists: true · env: jsdom
|
||||
- covers: Provider state transitions (indeterminate, loading, loaded); Value/min/max/orientation ARIA/data attributes; Label id registration and aria-labelledby wiring; ValueText snippet props and default text generation; Indicator state inheritance; Vertical orientation support
|
||||
- untested:
|
||||
@ -0,0 +1,19 @@
|
||||
# Audit: qr-code
|
||||
audit-version: 1
|
||||
audited-at: 2026-06-26
|
||||
scope: eidos (eidos-only primitive)
|
||||
method: theming-coherence category sweep (visual-elements), adversarially verified; roles + `:Morfo` lead-verified CLEAN across all eidos (no success/warning/danger/info; only alert-dialog uses `:Morfo`).
|
||||
|
||||
## Summary
|
||||
Counts (post-verification): CRITICAL 0 · HIGH 0 · MEDIUM 0 · LOW 0. Theming-clean.
|
||||
conformance: Contract solid (as const), display-only (no events), cell-shape variant (square/rounded/dots), fixed-tone by design for QR scannability, no drift.
|
||||
as-const: true · roles clean: true · variants clean: true · has-recipe: true
|
||||
|
||||
## Findings
|
||||
_No findings (theming-clean primitive)._
|
||||
|
||||
## Theming facts (E-bis)
|
||||
- magic literals: #18181b @ qr-code recipe:1948 -> fg (INTENTIONAL fixed-tone for scanability) | #ffffff @ qr-code recipe:1949 -> bg (INTENTIONAL fixed-tone for scanability) | #ffffff @ qr-code recipe:1958 -> overlay-bg (INTENTIONAL fixed-tone per quiet zone)
|
||||
- magic z-index: none
|
||||
- non-canonical names: none
|
||||
- roles clean: true · variants clean: true
|
||||
@ -0,0 +1,46 @@
|
||||
# Audit: radio-cards
|
||||
audit-version: 1
|
||||
audited-at: 2026-06-26
|
||||
scope: eidos (eidos-only primitive)
|
||||
method: theming-coherence category sweep (composite-and-service), adversarially verified; roles + `:Morfo` lead-verified CLEAN across all eidos (no success/warning/danger/info; only alert-dialog uses `:Morfo`).
|
||||
|
||||
## Summary
|
||||
Counts (post-verification): CRITICAL 0 · HIGH 0 · MEDIUM 1 · LOW 2.
|
||||
conformance: Contract: 'as const satisfies Morfo' ✓ | Scope: eidos ✓ | Roles: reuses radio-group (none new) ✓ | Variants: data-size, data-orientation, data-columns, data-invalid ✓ | Sizing: via recipe (padding/gap/icon-size/title-font-size) ✓ | Color: reused --_radio-group-palette-* inlined (documented) ✓
|
||||
as-const: true · roles clean: true · variants clean: true · has-recipe: true
|
||||
|
||||
## Findings
|
||||
### MEDIUM: one-off easing literal where --ease-* scale exists — radio-cards-easing-001 <!-- id: radio-cards-easing-001 -->
|
||||
- dimension: MAGIC_LITERAL, NO_OVERLAP
|
||||
- location: src/uix/eidos/components/radio-cards/radio-cards.css:252
|
||||
- evidence: transition: transform var(--radio-cards-transition-duration) cubic-bezier(0.4, 0, 0.2, 1);
|
||||
- impact: Bespoke Material-standard easing curve inlined where the canonical --ease-* token scale exists. Sibling split-button.css:22 correctly uses var(--ease-default). The duration here correctly uses a recipe token (--radio-cards-transition-duration); only the easing bypasses the scale, so a theme that retunes --ease-default won't reach this indicator transition.
|
||||
- proposed-fix: Replace cubic-bezier(0.4, 0, 0.2, 1) with var(--ease-default) (or the appropriate registered --ease-* token) to keep the indicator pop on the canonical easing scale.
|
||||
- verify: [verifier-added] added by adversarial verify pass
|
||||
- fix-status: open
|
||||
|
||||
### LOW: bare percentages in color-mix() — radio-cards-001 <!-- id: radio-cards-001 -->
|
||||
- dimension: MAGIC_LITERAL
|
||||
- location: src/uix/eidos/components/radio-cards/radio-cards.css:98, 117
|
||||
- evidence: color-mix(in srgb, var(--_radio-group-palette-solid) 12%, var(--color-surface-default)); (line 98)
|
||||
color-mix(in srgb, var(--_radio-group-palette-solid) 7%, var(--color-surface-default)); (line 117)
|
||||
- impact: Hardcoded tint opacity (12% for hover, 7% for checked) — these are interaction opacity values that bypass --opacity-* scale; however, they are documented in the recipe comments and locked to the card presentation logic
|
||||
- proposed-fix: Document or add --radio-cards-*-opacity tokens if cross-component theming consistency is desired, but the current inlining is acceptable given the component-specific palette context
|
||||
- verify: [downgraded] Confirmed css:98 (12%) and css:117 (7%) color-mix proportions tinting --_radio-group-palette-solid over --color-surface-default. These are color-mix tint ratios, not element opacity; a :root token referencing the palette would be invalid-at-computed-value (comment css:106-113). No canonical token applies to a mix proportion. LOW, not MEDIUM.
|
||||
- fix-status: open
|
||||
|
||||
### LOW: bare percentage / em in pseudo-element — radio-cards-002 <!-- id: radio-cards-002 -->
|
||||
- dimension: MAGIC_LITERAL
|
||||
- location: src/uix/eidos/components/radio-cards/radio-cards.css:247-248, 252
|
||||
- evidence: inline-size: 46%; block-size: 46%; (line 247-248)
|
||||
transform: scale(0); ... cubic-bezier(0.4, 0, 0.2, 1); (line 252)
|
||||
- impact: 46% is the inner dot radius (fixed ratio to the outer ring); scale(0) is a keyframe/initial state; cubic-bezier is acceptable easing (not a one-off deviation from standard preset)
|
||||
- proposed-fix: No fix — 46% is a fixed geometry ratio, scale(0) is structural, and cubic-bezier(0.4, 0, 0.2, 1) is a standard animation easing
|
||||
- verify: [downgraded] Confirmed css:247-248 46% (fixed inner-dot geometry ratio) and css:252 scale(0) (structural initial state) — both LOW physical/structural, not drift. HOWEVER css:252 also carries a bespoke easing cubic-bezier(0.4, 0, 0.2, 1) where --ease-* tokens exist and split-button.css:22 correctly uses var(--ease-default); that easing sliver is the real MEDIUM (see new finding radio-cards-easing-001). The 46%
|
||||
- fix-status: open
|
||||
|
||||
## Theming facts (E-bis)
|
||||
- magic literals: 12% @ src/uix/eidos/components/radio-cards/radio-cards.css:98 -> hover tint opacity (component-specific, documented in recipe comments) | 7% @ src/uix/eidos/components/radio-cards/radio-cards.css:117 -> checked tint opacity (component-specific, documented) | 46% @ src/uix/eidos/components/radio-cards/radio-cards.css:247-248 -> inner dot geometry ratio (fixed, acceptable) | cubic-bezier(0.4, 0, 0.2, 1) @ src/uix/eidos/components/radio-cards/radio-cards.css:252 -> standard easing preset (acceptable)
|
||||
- magic z-index: none
|
||||
- non-canonical names: none
|
||||
- roles clean: true · variants clean: true
|
||||
@ -0,0 +1,42 @@
|
||||
# Audit: radio-group
|
||||
audit-version: 1
|
||||
audited-at: 2026-06-26
|
||||
scope:
|
||||
method: adversarially-verified workflow; HIGH lead-verified. B6 ground-truth: checkbox/toggle/switch commit-toggle = sequence post (lag-fixed); radio-group/tabs/accordion/stepper set state at call-site (pre OK); toggle-group + checkbox-group carry the A31 .includes pattern (SYS-7); slider has no gesture-layer A6 leak.
|
||||
provider: G:\dev\svelte\vicen\src\uix\soma\components\radio-group\radio-group-provider.svelte.ts
|
||||
sequence-audit: state set at CALL-SITE (selectItem line 127: this.opts.value.current = value) BEFORE runtime.trigger (line 131). Morfo sequence: 'pre' (line 24). DOCTRINE: radio-group is call-site + pre, no lag risk. Prior batch verified 47ms acceptable for held-key nav. MATCH ✓
|
||||
|
||||
## Summary
|
||||
Counts (post-verification): CRITICAL 0 · HIGH 0 · MEDIUM 1 · LOW 0.
|
||||
|
||||
## Findings
|
||||
### MEDIUM: MAGIC-LITERALS: invalid-focus-ring shadow uses hardcoded px instead of canonical tokens — radio-group-001 <!-- id: radio-group-001 -->
|
||||
- dimension: E: Theming
|
||||
- rule: MAGIC-LITERALS: invalid-focus-ring shadow uses hardcoded px instead of canonical tokens
|
||||
- location: src/uix/eidos/components/radio-group/radio-group.css:177
|
||||
- evidence: box-shadow: 0 0 0 2px var(--color-surface-default), 0 0 0 4px var(--color-threat-element);
|
||||
- impact: The invalid state focus ring uses hardcoded 2px and 4px values instead of the canonical --focus-ring-offset and --focus-ring-width tokens. This breaks theming consistency and diverges from the pattern established in calendar (line 1712) and other components. If --focus-ring-width changes, this focus ring won't update.
|
||||
- repro: View a radio-group in invalid state with focus; compare the focus ring thickness to other components. Check base.ts focus-ring token definitions.
|
||||
- proposed-fix: Replace with: box-shadow: 0 0 0 var(--focus-ring-offset) var(--color-surface-default), 0 0 0 calc(var(--focus-ring-offset) + var(--focus-ring-width)) var(--color-threat-element); (matching the canonical pattern from calendar.css and input.css)
|
||||
- verify: [confirmed] Confirmed at radio-group.css:177 — `box-shadow: 0 0 0 2px var(--color-surface-default), 0 0 0 4px var(--color-threat-element);`. Canonical token-driven double-ring pattern exists in archetypes.css:123-124, recipes/base.ts:1693/1712/2835 and accordion.css:160-162 using `var(--focus-ring-offset)` + `calc(var(--focus-ring-offset) + var(--focus-ring-width))`. Tokens resolve to offset 1px / width 2px (generated/base.css:178-179), so canonical rings are 1px/3px while this hardcode renders 2px/4px — a real visual divergence, not just naming. No radio-group focus token override in the recipe; normal focus (line 122-124) is already tokenized. MEDIUM correct, confidence high.
|
||||
- fix-status: open
|
||||
|
||||
## No-findings dimensions
|
||||
A: Contract (morfo) — as const satisfies Morfo ✓, all 5 parts registered (provider/item/indicator/hidden-input/label), 2-of-3 data-{c}-{part} naming ✓, A: Contract — aria-hidden correctly set on indicator (line 132) and hidden-input (line 147), B: Behavior — Sequence audit: state set at CALL-SITE (selectItem line 127) before runtime.trigger, morfo sequence='pre' ✓ (matches doctrine for radio-group: call-site state + pre tolerates no lag per prior work, 47ms test), B: Behavior — A14 roving tabindex: exactly one tabindex=0 when selected or fallback ✓ (lines 222-226 and test 218-224), B: Behavior — A12 RTL: getDirectionalKeys(dir, orientation) called ✓ (line 242), no transform bug, B: Behavior — A13 hidden input: rendered, type=radio, name propagates ✓ (lines 344-355, test 226-233), B: Behavior — A30 id registration: direct assignment in constructor ✓ (line 382, not in $effect), A31 isChecked derivation: O(1) simple equality ✓ (line 220, no .includes loop), B: Behavior — No A6 gesture/observer/timer leaks (no setTimeout/setInterval/Gesture/ResizeObserver/MutationObserver used), C: DOM-selector — CSS.escape used on consumer value (line 93) ✓, D: Frontier — soma does not import eidos ✓, E: TSC composition — no cross-recipe TSC (TSC v2.1 palette local to radio-group, not shared), E: Theming — only 9 roles used ✓, no raw hex ✓, token references for font-size/icon-size/spacing ✓ (exceptions: focus ring magic 2px/4px found), F: Tests — test env jsdom ✓, covers roving (239-276), keyboard (261-269), state (197-237), hidden input (226-233), readonly/disabled guards (278-312)
|
||||
|
||||
## Theming facts (E-bis)
|
||||
- magic z-index: none
|
||||
- magic literals: 2px and 4px in invalid focus-ring shadow (line 177)
|
||||
- undeclared parts: none
|
||||
- roles clean: true · variants clean: true
|
||||
|
||||
## Tests (F)
|
||||
- exists: true · env: jsdom
|
||||
- covers: roving tabindex fallback (239-276: navigation, auto-select); keyboard navigation with loop/directional keys (237-268); state selection sync (197-237: onclick, isChecked derivation); hidden input name/value/checked form participation (226-233); readonly and disabled guards across group/item (278-312)
|
||||
- untested: RTL directional keys (dir='rtl' orientation swap verified in type but no jsdom RTL test); A30 label id registration edge case (id.current mutations)
|
||||
|
||||
## Style observations (non-blocking)
|
||||
- Focus ring pattern divergence: invalid state uses 2px/4px shadow offsets (hardcoded) vs canonical --focus-ring-offset/--focus-ring-width across other components
|
||||
- Data-attribute naming clean: data-radio-group provider, data-radio-group-{item|indicator|hidden-input|label} parts follow the kebab pattern correctly
|
||||
- Theming palette: _palette-solid TSC v2.1 correctly declared on host [data-radio-group], inherited by items — no double-declaration or cross-recipe contamination
|
||||
- No color role violations: only primary/secondary/neutral/affirm used (never risk/threat/loss)
|
||||
@ -0,0 +1,67 @@
|
||||
# Audit: range-calendar
|
||||
audit-version: 1
|
||||
audited-at: 2026-06-26
|
||||
scope: (SCOPE-DRIFT → SYS-1)
|
||||
method: adversarially-verified workflow (analyze → refute); HIGH/CRITICAL personally re-verified by the lead. Batch-3 ground-truth: each picker fires trigger(close) (close NOT inert), but open/commit-reset ARE inert; calendar/range-calendar are MID-REFACTOR (uncommitted view-switch work).
|
||||
provider: src/uix/soma/components/range-calendar/range-calendar-provider.svelte.ts
|
||||
|
||||
> **MID-REFACTOR CAVEAT:** this component has uncommitted view-switch changes on this branch (M/D/?? files). Findings reflect the current in-flight state; treat structural inconsistencies as in-progress, not shipped defects.
|
||||
|
||||
## Summary
|
||||
Counts (post-verification): CRITICAL 0 · HIGH 0 · MEDIUM 2 · LOW 2.
|
||||
systemic hits: SYS-1 scope-drift (morfo includes eidos but no recipe entry).
|
||||
|
||||
## Findings
|
||||
### MEDIUM: Keyboard route must match morfo contract; Home/End APG grid pattern — range-calendar-002 <!-- id: range-calendar-002 -->
|
||||
- dimension: B: Behavior (soma)
|
||||
- rule: Keyboard route must match morfo contract; Home/End APG grid pattern
|
||||
- location: src/uix/soma/components/range-calendar/range-calendar-provider.svelte.ts:566-567
|
||||
- evidence: Morfo declares (line 88-89): `{ key: 'Home', action: 'first-day-of-week' }, { key: 'End', action: 'last-day-of-week' }`. Provider implements (lines 566-567): `else if (e.key === KEYS.HOME) target = startOfMonth(date); else if (e.key === KEYS.END) target = endOfMonth(date);`. This is month-based, not week-based.
|
||||
- impact: User pressing Home/End navigates to first/last day of month instead of first/last day of current week, violating both the morfo contract and APG grid pattern expectations.
|
||||
- proposed-fix: Replace `startOfMonth(date)` with calculation to first day of current week using weekStartsOnResolved. Replace `endOfMonth(date)` with calculation to last day of current week.
|
||||
- verify: [downgraded] Behavior is real but NOT a range-calendar-specific HIGH. Provider lines 566-567: `else if (e.key === KEYS.HOME) target = startOfMonth(date); else if (e.key === KEYS.END) target = endOfMonth(date);` — month-based, while morfo lines 88-89 declare actions named `first-day-of-week`/`last-day-of-week`. HOWEVER the sibling `calendar` provider does the IDENTICAL thing: calendar-provider.svelte.ts:481-485 `if (e.key === KEYS.HOME) { const monthStart = startOfMonth(date); ... } else if (e.key === KEYS.END) { target = endOfMonth(date); }`, against the IDENTICAL morfo declaration calendar.ts:76-77 (`{ key: 'Home', action: 'first-day-of-week' }, { key: 'End', action: 'last-day-of-week' }`). This is a family-wide naming/implementation divergence, and `keyboard[].action` is advisory morfo metadata (not enforced by the runtime — the provider's keydown handler owns the actual behavior). Not a latent bug introduced here, not a contract-validator catch; at most a LOW doc/naming inconsistency that should be raised against the whole calendar family, not range-calendar alone.
|
||||
- fix-status: open
|
||||
|
||||
### MEDIUM: INERT EVENTS: declared keyboard action never fired via runtime.trigger — range-calendar-003 <!-- id: range-calendar-003 -->
|
||||
- dimension: A: Contract (morfo)
|
||||
- rule: INERT EVENTS: declared keyboard action never fired via runtime.trigger
|
||||
- location: src/uix/morfo/components/range-calendar.ts:92
|
||||
- evidence: Morfo declares (line 92): `{ key: 'Escape', action: 'cancel-selection' }`. Grep for 'Escape' or 'escape' or 'KEYS.ESCAPE' in range-calendar provider + components returns no matches. No runtime.trigger fires this action.
|
||||
- impact: User pressing Escape expects to cancel/clear the range selection, but nothing happens. Event is declared only to satisfy schema validation.
|
||||
- proposed-fix: Implement Escape key handling in handleDayKeydown to call clearSelection(). Or remove Escape from morfo if not intended.
|
||||
- verify: [confirmed] CONFIRMED as MEDIUM. Morfo line 92 declares `{ key: 'Escape', action: 'cancel-selection' }`. I read the entire keydown chain `handleDayKeydown` (provider lines 555-602): branches exist for horizNext/horizPrev, ArrowDown/Up, Home, End, PageUp, PageDown, Enter/Space — but NO `KEYS.ESCAPE` branch. There is also no other keydown handler on any part (the Day part's `onkeydown` at line 1172-1174 delegates solely to `handleDayKeydown`). The provider never fires `commit-reset` or `clearSelection()` in response to Escape. The declared `cancel-selection` action is therefore inert — it exists only to satisfy the morfo schema, exactly the INERT-events class the rubric calls out. Severity MEDIUM is appropriate: a user mid-selection who presses Escape expecting to abandon the partial range gets nothing, but it is not an a11y/data-corruption break.
|
||||
- fix-status: open
|
||||
|
||||
### LOW: querySelector with interpolated user/runtime value must use CSS.escape — range-calendar-004 <!-- id: range-calendar-004 -->
|
||||
- dimension: C: DOM-selector
|
||||
- rule: querySelector with interpolated user/runtime value must use CSS.escape
|
||||
- location: src/uix/soma/components/range-calendar/range-calendar-provider.svelte.ts:595-596
|
||||
- evidence: Code: `const el = root.querySelector<HTMLElement>(`[data-range-calendar-day][data-value="${target!.toString()}"]`);` The value is interpolated without CSS.escape. While ISO date format (2026-05-15) currently has no special CSS characters, the pattern is unsafe and violates defensive coding.
|
||||
- impact: If date format changes or special characters are introduced (e.g., localized formats), the selector could fail to find the element or select an unintended element. Violates the untrusted selector safety pattern.
|
||||
- proposed-fix: Use `CSS.escape(target!.toString())` to safely escape the interpolated value.
|
||||
- verify: [downgraded] DOWNGRADED. Provider lines 595-596: `root.querySelector<HTMLElement>(`[data-range-calendar-day][data-value="${target!.toString()}"]`)`. The interpolated value is `DateValue.toString()` — an ISO `YYYY-MM-DD` string produced by `$libs/days`, a FRAMEWORK-controlled value, never user/consumer-derived input. ISO dates contain only digits and a hyphen — no CSS-special characters can ever appear, so CSS.escape changes nothing functionally. Dimension C targets interpolation of *user/consumer-derived* values; a synthesized date string is neither. The same pattern is used family-wide (calendar-provider.svelte.ts:514-516 `[data-calendar-day][data-value="${target!.toString()}"]`). Defensible-hardening LOW at most, not a HIGH untrusted-selector defect.
|
||||
- fix-status: open
|
||||
|
||||
### LOW: MAGIC NUMBERS: bare pixel/em/rem values should use canonical spacing tokens — range-calendar-005 <!-- id: range-calendar-005 -->
|
||||
- dimension: E-bis: Theming (recipe + css)
|
||||
- rule: MAGIC NUMBERS: bare pixel/em/rem values should use canonical spacing tokens
|
||||
- location: src/uix/eidos/components/range-calendar/range-calendar.css:323,332
|
||||
- evidence: Lines 323, 332: `box-shadow: inset 2px 0 0 0 var(--_calendar-range-start-border);` and `box-shadow: inset -2px 0 0 0 var(--_calendar-range-end-border);` use bare `2px` and `-2px` values instead of canonical border or spacing tokens.
|
||||
- impact: Spacing is hardcoded, not themable. Breaks token consistency and makes the stripe width inflexible for different design systems.
|
||||
- proposed-fix: Define a token like `--_calendar-range-endpoint-stripe-width` and reference it instead: `box-shadow: inset calc(var(--_calendar-range-endpoint-stripe-width, 2px) * 1) 0 0 0 ...`
|
||||
- verify: [confirmed] CONFIRMED as LOW (severity already correct). range-calendar.css line 323 `box-shadow: inset 2px 0 0 0 var(--_calendar-range-start-border);` and line 332 `box-shadow: inset -2px 0 0 0 var(--_calendar-range-end-border);` hardcode the `2px`/`-2px` endpoint-stripe width as bare literals rather than a recipe/border token. Genuine bare-literal drift, lowest severity — it is a decorative 2px hairline accent stripe on the range start/end endpoints, not a layout-load-bearing or themed dimension. Tokenizing as `--_calendar-range-endpoint-stripe-width` would be the canonical fix but the impact is purely cosmetic theme-flexibility.
|
||||
- fix-status: open
|
||||
|
||||
## No-findings dimensions
|
||||
D: Frontier (soma/eidos isolation), F: Tests (test coverage for existing keyboard/focus paths)
|
||||
|
||||
## Theming facts (E-bis)
|
||||
- magic z-index: none
|
||||
- magic literals: 2px inset stripe width (lines 323, 332)
|
||||
- undeclared parts: none
|
||||
- roles clean: true · variants clean: true
|
||||
|
||||
## Tests (F)
|
||||
- exists: true · env: jsdom
|
||||
- covers: initial placeholder resolution; range selection ordering; range length bounds enforcement; partial range clearing; endpoint amendment; transient anchor resilience; placeholder auto-page prevention; validation bounds checking
|
||||
- untested: keyboard navigation (Home/End/arrows); Escape key; roving focus management; two-moments event sequencing
|
||||
@ -0,0 +1,39 @@
|
||||
# Audit: rating-group
|
||||
audit-version: 1
|
||||
audited-at: 2026-06-26
|
||||
scope: (SCOPE-DRIFT → SYS-1)
|
||||
method: adversarially-verified workflow (analyze → refute); HIGH lead-verified by direct read of the cited code. B5 ground-truth: the A31 O(N²) isSelected/isExpanded (.includes from a per-item $derived) is confirmed across listbox/grid-list/tree-view/tree-grid/tag-group (SYS-7); rovingTargetEl is correctly LIFTED everywhere (not A31); virtual-* use SvelteMap (A33-clean).
|
||||
provider: src/uix/soma/components/rating-group/rating-group-provider.svelte.ts
|
||||
reactivity (A31/A33/A35): CLEAN: items computed once via $derived (line 130-142); per-item state lookup via direct array index (line 350), not provider method call. No $state(Map/Set), no per-item effects reading global state. No A31/A33/A35 hazards detected."
|
||||
|
||||
## Summary
|
||||
Counts (post-verification): CRITICAL 0 · HIGH 0 · MEDIUM 1 · LOW 0.
|
||||
|
||||
## Findings
|
||||
### MEDIUM: SYS-1 scope-drift — rating-group-001 <!-- id: rating-group-001 -->
|
||||
- dimension: scope, contract
|
||||
- rule: SYS-1 scope-drift
|
||||
- location: src/uix/morfo/components/rating-group.ts:7
|
||||
- evidence: Morfo declares scope: ['soma', 'sema'] but eidos recipe directory exists at src/uix/eidos/components/rating-group/ with full recipe tokens (base.ts:2018-2044) and eidos wrapper component
|
||||
- impact: Eidos layer is functionally present but not declared in scope; discovery tooling and consumers may miss that eidos layer exists or assume morfo scope is incomplete
|
||||
- repro: Inspect src/uix/morfo/components/rating-group.ts line 7 and confirm eidos directory + recipe tokens exist
|
||||
- proposed-fix: Update morfo scope from ['soma', 'sema'] to ['soma', 'sema', 'eidos']
|
||||
- verify: [confirmed] Confirmed SYS-1 scope-drift. src/uix/morfo/components/rating-group.ts:7 declares `scope: ['soma', 'sema'],` while a full eidos layer exists: recipe block at src/uix/eidos/lib/recipes/base.ts:2018 (`'rating-group': { 'gap-xs': ... 'item-color-active': 'var(--color-fulfill-solid)' ... }`) plus eidos wrapper/css/types/README under src/uix/eidos/components/rating-group/ (rating-group.svelte, rating-group.css, types.ts, README.md). Direct peers that ALSO have eidos recipes DO declare it: radio-group.ts:7, checkbox.ts:7, toggle-group.ts:10 all read `scope: ['soma', 'sema', 'eidos']`. So 'eidos' is the project's convention when an eidos recipe is present, and rating-group omits it. MEDIUM per SYS-1 baseline. (Note: a broader population of soma-rooted components — calendar, combobox, command — also omit 'eidos' despite having eidos css; this is a wider systemic pattern, but rating-group's drift is real and matches the SYS-1 rule against its rating-control peers.) Proposed fix: change to `scope: ['soma', 'sema', 'eidos']`.
|
||||
- fix-status: open
|
||||
|
||||
## No-findings dimensions
|
||||
contract, parts_registered, aria_data_declared_vs_emitted, events_inert, field_merge, reactivity_A31, reactivity_A33, reactivity_A35, reactivity_A30, reactivity_A6, dom_selector_escape, direct_global_access, morfo_as_const, soma_imports_eidos, double_write_divergence, require_topology, theming_roles, theming_literals_magic, theming_z_index, theming_opacity, theming_spacing, theming_sizing
|
||||
|
||||
## Theming facts (E-bis)
|
||||
- magic z-index: none
|
||||
- magic literals: none
|
||||
- undeclared parts: none
|
||||
- roles clean: true · variants clean: true
|
||||
|
||||
## Tests (F)
|
||||
- exists: true · env: jsdom
|
||||
- covers: slider_attr_projection; field_flag_merging; item_states; hover_preview; pointer_click_commit; clearable_click; keyboard_nav; rtl_arrows; readonly_disabled_guards
|
||||
- untested: pointer_touch_isolation; clearable_disabled_edge_case; large_max_regression; value_change_callback_ordering
|
||||
|
||||
## Style observations (non-blocking)
|
||||
- CSS uses exclusively role aliases (color-fulfill-solid, color-neutral-border, color-surface-default, etc.) and canonical tokens (--space-*, --font-size-*, --radius-*). Opacity via --rating-group-disabled-opacity (line 52), not raw decimal. Transitions use --duration-fast/--ease-default. Gap/item-size responsive via eidos size prop. No magic z-index, no bare hex colors, no opacity literals. Sizing: item-size in px literal (14-34px) tied to size variants; font-size via --font-size-* refs. Naming conventions correct: --rating-group-{gap,item-size,font-size,item-color}-*.
|
||||
@ -0,0 +1,19 @@
|
||||
# Audit: relative-time
|
||||
audit-version: 1
|
||||
audited-at: 2026-06-26
|
||||
scope: eidos (eidos-only primitive)
|
||||
method: theming-coherence category sweep (composite-and-service), adversarially verified; roles + `:Morfo` lead-verified CLEAN across all eidos (no success/warning/danger/info; only alert-dialog uses `:Morfo`).
|
||||
|
||||
## Summary
|
||||
Counts (post-verification): CRITICAL 0 · HIGH 0 · MEDIUM 0 · LOW 0. Theming-clean.
|
||||
conformance: Contract: 'as const satisfies Morfo' ✓ | Scope: eidos ✓ | Service component (no CSS, no behavior) ✓ | No variants/roles ✓
|
||||
as-const: true · roles clean: true · variants clean: true · has-recipe: false
|
||||
|
||||
## Findings
|
||||
_No findings (theming-clean primitive)._
|
||||
|
||||
## Theming facts (E-bis)
|
||||
- magic literals: none
|
||||
- magic z-index: none
|
||||
- non-canonical names: none
|
||||
- roles clean: true · variants clean: true
|
||||
@ -0,0 +1,19 @@
|
||||
# Audit: s-text-virtual-list
|
||||
audit-version: 1
|
||||
audited-at: 2026-06-26
|
||||
scope: eidos (eidos-only primitive)
|
||||
method: theming-coherence category sweep (typography), adversarially verified; roles + `:Morfo` lead-verified CLEAN across all eidos (no success/warning/danger/info; only alert-dialog uses `:Morfo`).
|
||||
|
||||
## Summary
|
||||
Counts (post-verification): CRITICAL 0 · HIGH 0 · MEDIUM 0 · LOW 0. Theming-clean.
|
||||
conformance: morfo OK; recipe present (base.ts:3534); no color/role/variant issues
|
||||
as-const: true · roles clean: true · variants clean: true · has-recipe: true
|
||||
|
||||
## Findings
|
||||
_No findings (theming-clean primitive)._
|
||||
|
||||
## Theming facts (E-bis)
|
||||
- magic literals: none
|
||||
- magic z-index: none
|
||||
- non-canonical names: none
|
||||
- roles clean: true · variants clean: true
|
||||
@ -0,0 +1,19 @@
|
||||
# Audit: s-text
|
||||
audit-version: 1
|
||||
audited-at: 2026-06-26
|
||||
scope: eidos (eidos-only primitive)
|
||||
method: theming-coherence category sweep (typography), adversarially verified; roles + `:Morfo` lead-verified CLEAN across all eidos (no success/warning/danger/info; only alert-dialog uses `:Morfo`).
|
||||
|
||||
## Summary
|
||||
Counts (post-verification): CRITICAL 0 · HIGH 0 · MEDIUM 0 · LOW 0. Theming-clean.
|
||||
conformance: morfo OK; recipe present (base.ts:3529); roles fragmented (inherits from Text)
|
||||
as-const: true · roles clean: false · variants clean: true · has-recipe: true
|
||||
|
||||
## Findings
|
||||
_No findings (theming-clean primitive)._
|
||||
|
||||
## Theming facts (E-bis)
|
||||
- magic literals: none
|
||||
- magic z-index: none
|
||||
- non-canonical names: none
|
||||
- roles clean: false · variants clean: true
|
||||
@ -0,0 +1,40 @@
|
||||
# Audit: scroll-area
|
||||
audit-version: 1
|
||||
audited-at: 2026-06-26
|
||||
scope: soma
|
||||
method: adversarially-verified workflow; HIGH lead-verified. B6 ground-truth: checkbox/toggle/switch commit-toggle = sequence post (lag-fixed); radio-group/tabs/accordion/stepper set state at call-site (pre OK); toggle-group + checkbox-group carry the A31 .includes pattern (SYS-7); slider has no gesture-layer A6 leak.
|
||||
provider: src/uix/soma/components/scroll-area/scroll-area-provider.svelte.ts
|
||||
sequence-audit: N/A - scroll-area is not a form control (not checkbox/toggle/switch). Uses pointer events for drag which set scrollTop/scrollLeft directly on the viewport element (not Svelte state), so lag risk does not apply. setPointerCapture is immediate, not deferred.
|
||||
|
||||
## Summary
|
||||
Counts (post-verification): CRITICAL 0 · HIGH 0 · MEDIUM 1 · LOW 0.
|
||||
|
||||
## Findings
|
||||
### MEDIUM: Morfo declares parts and their data attributes; provider must emit only declared attribute — scroll-area-001 <!-- id: scroll-area-001 -->
|
||||
- dimension: A - Contract
|
||||
- rule: Morfo declares parts and their data attributes; provider must emit only declared attributes or register undeclared ones (2-of-3 rule)
|
||||
- location: src/uix/morfo/components/scroll-area.ts:51-56 (Scrollbar part) vs src/uix/soma/components/scroll-area/scroll-area-provider.svelte.ts:428-436
|
||||
- evidence: Morfo Scrollbar declares data-state, data-orientation, data-hover, data-dragging. Provider emits those PLUS data-overflow-x and data-overflow-y (lines 432-433) which are NOT declared in the Scrollbar part. Only the Provider part declares overflow-* attributes (lines 21-24). Same issue on Viewport: declared data-at-top/bottom/left/right but provider also emits data-overflow-x/y (lines 195-196).
|
||||
- impact: Contract violation. Runtime validators should catch undeclared attributes. data-overflow-x/y belong on Provider only, or must be registered in each part's morfo data array.
|
||||
- proposed-fix: Add data-overflow-x and data-overflow-y to the Scrollbar and Viewport parts in the morfo (src/uix/morfo/components/scroll-area.ts), or remove them from the provider's rendered props for those parts.
|
||||
- verify: [downgraded] Drift CONFIRMED but severity downgraded HIGH->MEDIUM. The morfo declares data-overflow-x/data-overflow-y ONLY on the Provider part (src/uix/morfo/components/scroll-area.ts:22-23). The provider emits them additionally on Viewport (scroll-area-provider.svelte.ts:195-196 `'data-overflow-x': boolToEmptyStrOrUndef(this.provider.hasOverflowX), 'data-overflow-y': ...`) and on Scrollbar (lines 432-433, same expressions). Note the candidate UNDER-reports: the Thumb part also emits undeclared attrs — Thumb props at lines 566-567 stamp `'data-hover'`/`'data-dragging'` which the Thumb morfo (lines 83-86: only data-state, data-orientation) does NOT declare. So this is the same undeclared-attribute drift across Viewport+Scrollbar+Thumb. However this is a contract-lint concern (SYS-1-adjacent), not a user-facing behavior break: the attrs are informational duplicates of the Provider's own data-overflow-* and the eidos CSS (scroll-area.css) never selects data-overflow-x/y on scrollbar/viewport/thumb. No wrong behavior, no a11y break -> MEDIUM per the severity rubric (token/contract inconsistency), not HIGH.
|
||||
- fix-status: open
|
||||
|
||||
## No-findings dimensions
|
||||
B - Behavior (no sequence/lag; pointer drag uses direct dom setPointerCapture; no Gesture layer needed), C - DOM-selector (no interpolation of consumer values; no unsafe querySelector), D - Frontier (no soma->eidos import; data-size/color/variant not used; no syncAttrs divergence), A30/A31/A33/A35/A36 (no id-registration loops; no .includes() in item derivations; no $state(new Map); no SVG loop issues), A6 gesture-layer (not applicable; thumb drag uses native setPointerCapture, not Gesture layer), A10 nested querySelector (not applicable; getItems not used), A12 RTL directional-keys (not applicable; no keyboard directional nav), A13 form participation (scroll-area is not a form control; no hidden input needed), A14 roving-tabindex (not applicable; no radio-group/toggle-group/tabs-style selection), A15 scroll-area-specific gesture (uses native pointer events, not Gesture layer), A6 ResizeObserver disposal (properly disposed in $effect cleanup at line 64-65; resizeCleanups array cleared), A6 Timer disposal (hideTimer is cleared in $effect cleanup at line 263-265; clearHideTimer called on unmount), Event listener disposal (pointerenter/leave cleanup properly returned in $effect at lines 298-303)
|
||||
|
||||
## Theming facts (E-bis)
|
||||
- magic z-index: none
|
||||
- magic literals: scroll-area.css:92,97,135 — opacity: 0 / opacity: 1 (should use --opacity-* or --scroll-area-opacity-*) | scroll-area.css:56,102,131 — color-mix percentages 60%, 84% (should use --scroll-area-track-opacity-* tokens) | scroll-area-provider.svelte.ts:141,145 — 'var(--scroll-area-scrollbar-size, 8px)' (8px fallback is reasonable but ideally --scroll-area-scrollbar-size is always defined) | scroll-area-provider.svelte.ts:482,487 — Math.max(ratio * track{Height|Width}, 20) (20px minimum thumb size is hardcoded; should be a token)
|
||||
- undeclared parts: none
|
||||
- roles clean: true · variants clean: true
|
||||
|
||||
## Tests (F)
|
||||
- exists: true · env: jsdom (vitest @vitest-environment jsdom)
|
||||
- covers: viewport overflow measurement and state props; scrollbar visibility and timer scheduling; track click scrolling; thumb drag with setPointerCapture and scroll updates; corner visibility logic; resize observer lifecycle
|
||||
- untested: RTL scroll direction (dir='rtl' passed but not exercised in drag tests); interaction with different type modes exhaustively (type='always'/'auto'/'scroll' touched but combined scenarios not covered); gesture cancellation edge-cases (immediate setPointerCapture timing vs moveBuffer deference not tested — not a Gesture layer concern)
|
||||
|
||||
## Style observations (non-blocking)
|
||||
- Eidos CSS is well-structured with logical nesting and comprehensive state coverage (data-state, data-orientation, data-hover, data-dragging, data-autosize). Token fallbacks are generous but some hardcoded values (opacity decimals, color-mix percentages, 20px thumb minimum) should be parameterized.
|
||||
- Soma provider correctly manages overflow detection, viewport measurement via ResizeObserver, scrollbar hide timers, and pointer drag. Cleanup lifecycle is sound.
|
||||
- Thumb positioning uses inline transform tokens (--scroll-area-thumb-width/height) which is correct for dynamic sizing.
|
||||
@ -0,0 +1,46 @@
|
||||
# Audit: search-field
|
||||
audit-version: 1
|
||||
audited-at: 2026-06-26
|
||||
scope:
|
||||
method: adversarially-verified workflow (analyze → refute); HIGH/CRITICAL lead-verified. Clean-check pseudo-findings dropped. B4 ground-truth: segmented date/time/color-field register inputId via $effect (SYS-A30-EFFECT, no loop); number-field is the direct-assignment reference.
|
||||
provider: src/uix/soma/components/search-field/search-field-provider.svelte.ts
|
||||
field-family (A13/A24-26/A30): search-field IS field-composable (calls FieldProvider.get(), registers inputId at line 105 via direct assignment, NOT $effect—A30 correct). Does NOT declare hidden input (A13 not applicable—not form-participating). Does not emit segmented contenteditable (A26 not applicable). Does not read readonlySegments (A24/A25 not applicable). Field composition integration is clean: aria-labelledby/aria-descr
|
||||
|
||||
## Summary
|
||||
Counts (post-verification): CRITICAL 0 · HIGH 0 · MEDIUM 2 · LOW 0.
|
||||
|
||||
## Findings
|
||||
### MEDIUM: SYS-1 scope-drift — eidos recipe/CSS dir exists but morfo 'scope' omits 'eidos' — SF-SYS1-scope-drift <!-- id: SF-SYS1-scope-drift -->
|
||||
- dimension: A
|
||||
- rule: SYS-1 scope-drift — eidos recipe/CSS dir exists but morfo 'scope' omits 'eidos'
|
||||
- location: src/uix/morfo/components/search-field.ts:7
|
||||
- evidence: `scope: ['soma', 'sema']` — yet a full eidos surface exists: a recipe block `'search-field': { ... }` at src/uix/eidos/lib/recipes/base.ts:2045 and a complete component dir src/uix/eidos/components/search-field/ (search-field.css, types.ts, search-field.svelte, plus icon/input/clear-trigger/loading-indicator wrappers).
|
||||
- impact: The morfo's declared scope under-reports the layers that consume the contract. Any tooling that keys off `scope` (lint coverage, layer maps) will skip eidos for search-field even though eidos CSS selects against the morfo-emitted data-attrs (data-search-field, data-search-field-input, data-disabled/empty/focused, etc.).
|
||||
- proposed-fix: Add 'eidos' to the morfo `scope` array: `scope: ['soma', 'sema', 'eidos']`.
|
||||
- verify: [verifier-added] added by adversarial verify pass
|
||||
- fix-status: open
|
||||
|
||||
### MEDIUM: Test coverage of a high-risk path — debounced onValueChange has no test — SF-F-debounce-untested <!-- id: SF-F-debounce-untested -->
|
||||
- dimension: F
|
||||
- rule: Test coverage of a high-risk path — debounced onValueChange has no test
|
||||
- location: src/uix/soma/components/search-field/search-field-provider.svelte.test.ts:60
|
||||
- evidence: The fixture sets `debounceMs: state(0)` and never exercises `debounceMs > 0`. The debounce branch in `commit()` (provider lines 176-196: schedules `this.soma.uix.timers.schedule(...)`, replaces pending timers, fires only the latest value), plus `flushDebounce()` on submit (lines 128-137) and `cancelDebounce()` on clear/unmount (lines 119-125, 108-110), are entirely uncovered. The test stubs `soma` as a partial object with no `uix.timers`, so a debounced path would not even resolve a timer service.
|
||||
- impact: The most behaviorally subtle logic in this provider (debounce coalescing, flush-before-submit ordering so onSubmit sees the latest value, cancel-on-clear, cancel-on-unmount A6) is unverified. A regression in timer keying/replace or flush ordering would ship silently.
|
||||
- proposed-fix: Add a test with debounceMs>0 driving the provider through a real `uix.timers` (via createActiveUix/attachActiveUix harness): assert onValueChange fires once with the latest value after the delay, that submit flushes the pending value before onSubmit, and that clear cancels the pending callback.
|
||||
- verify: [verifier-added] added by adversarial verify pass
|
||||
- fix-status: open
|
||||
|
||||
## No-findings dimensions
|
||||
A, B, C, D, E, E-bis, F, G
|
||||
|
||||
## Theming facts (E-bis)
|
||||
- magic z-index: none
|
||||
- magic literals: line-height: 1 at search-field.css:189 (icon alignment; systemic pattern across all icons, not search-field-specific)
|
||||
- undeclared parts: none
|
||||
- roles clean: true · variants clean: true
|
||||
- label-font (one step below input?): N/A — search-field composes Field; Field renders the label. No direct label in search-field recipe.
|
||||
|
||||
## Tests (F)
|
||||
- exists: true · env: @vitest-environment jsdom
|
||||
- covers: root state attrs projection; focus/blur state tracking; input commit and value change; submit on Enter; clear on Escape and click; debounce behavior; Field composition (OR-merge of flags, labelId integration)
|
||||
- untested:
|
||||
@ -0,0 +1,26 @@
|
||||
# Audit: section
|
||||
audit-version: 1
|
||||
audited-at: 2026-06-26
|
||||
scope: eidos (eidos-only primitive)
|
||||
method: theming-coherence category sweep (layout), adversarially verified; roles + `:Morfo` lead-verified CLEAN across all eidos (no success/warning/danger/info; only alert-dialog uses `:Morfo`).
|
||||
|
||||
## Summary
|
||||
Counts (post-verification): CRITICAL 0 · HIGH 0 · MEDIUM 0 · LOW 1.
|
||||
conformance: Contract OK, size-keyed padding tokens defined locally (sm/md/lg/xl), width:100% set inline.
|
||||
as-const: true · roles clean: true · variants clean: true · has-recipe: false
|
||||
|
||||
## Findings
|
||||
### LOW: MAGIC LITERAL calc() with hardcoded 1.5 multiplier — section-001 <!-- id: section-001 -->
|
||||
- dimension: magic-literal
|
||||
- location: src/uix/eidos/components/section/section.css:17
|
||||
- evidence: 'padding-block-xl': 'calc(var(--space-16) * 1.5)'
|
||||
- impact: Multiplier 1.5 is a bespoke amplitude; breaks theming uniformity if space scale changes.
|
||||
- proposed-fix: Add --space-24 token or define --section-padding-block-xl in foundation to replace calc().
|
||||
- verify: [downgraded] base.ts:4071 / section.css:17 `calc(var(--space-16) * 1.5)`. NOT a bare literal — it composes the canonical --space-16 token, so it stays density/scaling-aware. Spacing scale tops out at --space-16=64px (no --space-24 exists; confirmed grep), so xl=96px is legitimately off-scale and expressed via the nearest canonical token. The 1.5 multiplier is a documented amplitude (recipe + css comment: xl->9
|
||||
- fix-status: open
|
||||
|
||||
## Theming facts (E-bis)
|
||||
- magic literals: 1.5 @ src/uix/eidos/components/section/section.css:17 -> bespoke calc() amplitude
|
||||
- magic z-index: none
|
||||
- non-canonical names: none
|
||||
- roles clean: true · variants clean: true
|
||||
@ -0,0 +1,210 @@
|
||||
# Audit: select
|
||||
audit-version: 1
|
||||
audited-at: 2026-06-26
|
||||
scope: ['soma', 'sema'] (eidos recipe present though 'eidos' not in morfo scope — see systemic SCOPE-DRIFT)
|
||||
files:
|
||||
- provider: src/uix/soma/components/select/select-provider.svelte.ts (present)
|
||||
- morfo: src/uix/morfo/components/select.ts (present)
|
||||
- recipe: src/uix/eidos/components/select/select.css + lib/recipes/base.ts §select (present)
|
||||
- demo: web/routes/uix/components/select (present, not yet inspected this pass)
|
||||
- test: src/uix/soma/components/select/select-provider.svelte.test.ts (present, jsdom)
|
||||
- readme: soma + eidos README present (not yet inspected this pass)
|
||||
|
||||
## Summary
|
||||
Select is the canonical hard overlay: portal-split (`data-color` on trigger+content via TSC v2.2),
|
||||
virtual focus (aria-activedescendant), typeahead, dismissal-with-trigger-exclusion, floating + scroll-lock.
|
||||
The soma provider respects the eidos→soma frontier (zero eidos imports) and uses `SvelteMap` for the label
|
||||
registry (A33-correct). But the audit found **2 contract-vs-behavior contradictions** (focus.trap declared
|
||||
yet not implemented; eidos styles two parts the morfo never declares), a **keyboard double-route with a real
|
||||
behavioral asymmetry + an off-by loop bug**, an **untrusted-selector fragility**, an **A31 O(N²) selection
|
||||
read**, and theming-coherence drift (magic z-index, an `em`-literal font-size that escapes the px/rem guard).
|
||||
Test coverage pins only the easy path (selection logic) — every hard path (keyboard, typeahead, dismissal,
|
||||
focus return) is untested, jsdom-only.
|
||||
|
||||
Counts: CRITICAL 0 · HIGH 4 · MEDIUM 5 · LOW 2.
|
||||
|
||||
## Findings
|
||||
|
||||
### HIGH: Untrusted value interpolated into querySelector without CSS.escape <!-- id: select-001 -->
|
||||
- dimension: C
|
||||
- rule: behavioral (dimension-C selector robustness) + active_architecture §7.5 local-read frontier
|
||||
- location: select-provider.svelte.ts:244-246 (`scrollSelectedIntoView`)
|
||||
- evidence:
|
||||
```ts
|
||||
const selectedEl = container.querySelector<HTMLElement>(
|
||||
`[${attrs.item}][data-value="${selectedValue}"]`
|
||||
);
|
||||
```
|
||||
`selectedValue = this.opts.value.current[0]` is consumer-supplied option-value text, interpolated raw.
|
||||
- impact: a value containing `"`, `]`, or a backslash produces an invalid selector → `querySelector`
|
||||
throws (`SyntaxError`) on open, or silently matches nothing → the selected item never scrolls into view
|
||||
and `highlightedId` is left stale. Blast radius: any app whose option values aren't plain identifiers
|
||||
(URLs, emails, JSON-ish keys, i18n strings with quotes).
|
||||
- repro: `<Select value={['a"b']}>` with an item `value="a\"b"`, open the popup → throw / no scroll.
|
||||
- proposed-fix: `CSS.escape(selectedValue)` in the attribute selector, or reuse the registry/`resolveListItemEl`
|
||||
attribute-equality path (which compares `el.dataset.value` in JS, not via a selector).
|
||||
- fix-status: open
|
||||
|
||||
### HIGH: focus.trap / initial:'first-focusable' declared in morfo but provider implements virtual focus (no FocusScope) <!-- id: select-002 -->
|
||||
- dimension: A, B
|
||||
- rule: "don't trust docs over code or code over docs — flag drift" + A17 (virtual vs DOM focus) + M-... morfo contract
|
||||
- location: morfo select.ts:60-65 vs select-provider.svelte.ts (Content provider has Floating + Dismissal +
|
||||
ScrollLock but NO `FocusScope`); A17 declares Select as the canonical **virtual-focus** component.
|
||||
- evidence: morfo declares
|
||||
```ts
|
||||
focus: { initial: 'first-focusable', trap: true, return: 'trigger', restore: true }
|
||||
```
|
||||
The Content provider never instantiates `FocusScope`; focus stays on the Trigger (`aria-activedescendant`),
|
||||
Content is `tabindex: -1`. A focus trap + "focus first focusable item" is the *roving/DOM-focus* model,
|
||||
which contradicts the implemented virtual-focus model.
|
||||
- impact: the `focus` block is dead contract — nothing consumes `trap`/`initial`. Worse, it misdescribes the
|
||||
component to any reader/tool that trusts the morfo (and to a future FocusScope wiring that would BREAK
|
||||
virtual focus if someone "implements the declared trap"). `return: 'trigger'` IS honored manually
|
||||
(`handleClose` → `dom.focus(triggerRef)`), so half the block is real and half is fiction.
|
||||
- repro: static — read morfo vs provider.
|
||||
- proposed-fix: change the morfo `focus` to reflect virtual focus (`initial: 'none'` / trap:false, keep
|
||||
return:'trigger'), OR document why a listbox-virtual-focus component carries a trap declaration. Cross-check
|
||||
combobox (same strategy) for the same drift.
|
||||
- fix-status: open
|
||||
|
||||
### HIGH: Two keyboard routes duplicate index-math; content route uses real-DOM-focus (contradicts virtual focus) and diverges from the trigger route <!-- id: select-003 -->
|
||||
- dimension: B, G
|
||||
- rule: A17 (never mix virtual + DOM focus) + dimension-B keyboard-route divergence + dimension-G redundancy
|
||||
- location: trigger route select-provider.svelte.ts:304-387 (virtual: `highlightedId` + `findIndex(el.id===…)`);
|
||||
content route :559-612 (real focus: `active = dom.activeElement(container)` + `items.indexOf(active)`)
|
||||
- evidence: the two `onkeydown` handlers each re-implement next/prev/Home/End/typeahead index math.
|
||||
The content route resolves the current index from `activeElement` — but Select keeps focus on the trigger
|
||||
(virtual focus), so inside the content route `currentIndex` is ~always `-1`. Divergent no-selection landing:
|
||||
- trigger route, ArrowUp from no highlight → `targetIndex = items.length - 1` (LAST). (:337-344)
|
||||
- content route, ArrowUp from no active, `loop=false` → `Math.max(-1-1,0) = 0` (FIRST); `loop=true` →
|
||||
`(-1-1+n)%n = n-2` (second-to-last — an off-by bug). (:577-581)
|
||||
- impact: maintenance hazard (one nav model expressed twice) + a latent correctness bug in the content
|
||||
route's loop path + an A17 violation (content route reads `activeElement` though the component is
|
||||
virtual-focus). The content route is largely unreachable in normal operation, which makes it untested
|
||||
dead-ish code that will rot.
|
||||
- repro: force focus into the content element and press ArrowUp with `loop` on → lands on n-2, not last.
|
||||
- proposed-fix: extract one pure `nextIndex(curr, key, len, loop, dir)` helper unit-tested once; delete the
|
||||
content route or, if a real-focus fallback is wanted, make it consistent with the trigger route's
|
||||
no-selection semantics. (Shared extraction candidate — see SHARED_EXTRACTION directional-nav.)
|
||||
- fix-status: open
|
||||
|
||||
### HIGH: Tests pin selection logic only — keyboard, typeahead, dismissal-exclusion, focus-return untested; jsdom-only <!-- id: select-004 -->
|
||||
- dimension: F
|
||||
- rule: dimension-F honesty check ("complex behavior tested only for its easy path")
|
||||
- location: select-provider.svelte.test.ts (3 tests: single-select, multi-select toggle, getItems disabled-filter)
|
||||
- evidence: no test exercises `SelectTriggerProvider.onkeydown` / `SelectContentProvider.onkeydown` (the
|
||||
index math in select-003), `Typeahead`, the `onInteractOutside` trigger-exclusion (:532-541), focus return
|
||||
on close (:217), or `highlightedId`/`aria-activedescendant` sync. `@vitest-environment jsdom` (:1) — focus
|
||||
and layout aren't faithful for a portal+floating+virtual-focus widget.
|
||||
- impact: the highest-risk behaviors (and the select-001/002/003 defects) would not be caught by the suite.
|
||||
Regressions in keyboard nav ship green.
|
||||
- repro: n/a (coverage gap).
|
||||
- proposed-fix: add provider tests for both keyboard routes (incl. the loop boundary), typeahead match,
|
||||
dismissal trigger-exclusion, and a client/Playwright test for focus return + activedescendant.
|
||||
- fix-status: open
|
||||
|
||||
### MEDIUM: Eidos recipe styles `[data-select-indicator]` and `[data-select-item-indicator]` — parts the morfo never declares <!-- id: select-005 -->
|
||||
- dimension: A, D
|
||||
- rule: active_architecture §7.6 ("Eidos consume DOM y data-* … debe estar declarado en morfo") + dimension-D frontier
|
||||
- location: select.css:149-163 (`[data-select-indicator]`), :340-358 (`[data-select-item-indicator]`).
|
||||
morfo parts list (select.ts:67-259) has no `Indicator` / `ItemIndicator`.
|
||||
- evidence: the recipe binds layout + the open-state chevron rotation + the check animation to two
|
||||
`data-select-*` attributes that are NOT morfo parts (and have no `kind:'internal'/'private'` declaration).
|
||||
- impact: undeclared contract surface — eidos-lint would classify these as eidos-only/invalid; renaming or
|
||||
removing the eidos wrapper's decorative spans silently breaks the rules with no morfo signal. The
|
||||
`data-{component}-{part}` namespace is being used for parts outside the morfo.
|
||||
- repro: `scripts/eidos-lint.ts select` would surface `[data-select-indicator]` as not morfo-backed.
|
||||
- proposed-fix: declare `Indicator` + `ItemIndicator` in the morfo as `kind:'internal'` decorative parts
|
||||
(they carry no ARIA), or document them as eidos-only in the eidos README §recipe.
|
||||
- fix-status: open
|
||||
|
||||
### MEDIUM: Provider emits `aria-invalid` on Trigger; morfo's Trigger does not declare it <!-- id: select-006 -->
|
||||
- dimension: A
|
||||
- rule: dimension-A ("undeclared attr emitted by the provider but absent from the morfo") + 2-of-3
|
||||
- location: select-provider.svelte.ts:398 (`'aria-invalid': this.provider.opts.invalid.current || undefined`);
|
||||
morfo Trigger `aria` (select.ts:101-118) declares type/haspopup/expanded/controls/activedescendant/required —
|
||||
no `aria-invalid`. (The morfo puts `aria-invalid` only on the **Provider** part, :83.)
|
||||
- evidence: the combobox-role trigger gets `aria-invalid` from soma, off-contract.
|
||||
- impact: a real, screen-reader-meaningful ARIA attr lives only in the provider — the morfo (the cross-layer
|
||||
source of truth) under-describes the trigger; eidos can't reliably select on it; drift risk on rename.
|
||||
- proposed-fix: move `aria-invalid` to the Trigger part in the morfo (conditional on `invalid`), let
|
||||
`renderProps()` supply it.
|
||||
- fix-status: open
|
||||
|
||||
### MEDIUM: Per-item `isSelected` reads the whole `value` array through the provider (A31 O(N²)) <!-- id: select-007 -->
|
||||
- dimension: B
|
||||
- rule: A31 (per-entity `$derived` must not read global state through the provider)
|
||||
- location: select-provider.svelte.ts:708 (`isSelected = $derived.by(() => this.provider.isSelected(value))`)
|
||||
→ :158-160 (`isSelected(v) { return this.opts.value.current.includes(v) }`)
|
||||
- evidence: each Item's `isSelected` derivation calls a provider method that reads the shared `value` array
|
||||
and runs `.includes` (O(N)). On any value change all N items re-derive → O(N²). Matches the A31 incident
|
||||
signature (Listbox rovingTarget).
|
||||
- impact: fine for single-select / few items; degrades with large multi-select lists (the documented A31
|
||||
"works with 5, hangs with 30+" class).
|
||||
- proposed-fix: lift `selectedSet = new SvelteSet(value)` on the provider; item derivation becomes
|
||||
`selectedSet.has(value)` (O(1)).
|
||||
- fix-status: open
|
||||
|
||||
### MEDIUM: `item-description-font-size: '0.85em'` — literal font-size in recipe escapes the px/rem coherence guard <!-- id: select-008 -->
|
||||
- dimension: E-bis
|
||||
- rule: THEMING §5 ("ningún token font-size-* de recipe puede ser literal — DEBE referenciar --font-size-*") + R-2.2
|
||||
- location: lib/recipes/base.ts:2243 (`'item-description-font-size': '0.85em'`); consumed select.css:330.
|
||||
- evidence: every other `select.*font-size-*` token references `var(--font-size-*)` (1:1, :2154-2158). The
|
||||
description token is a raw `em`, breaking the type-scale link (`--scaling` / `applyTypeScale` no longer
|
||||
reach it). The §5 guard checks **px/rem** literals — an `em` literal slips through (see VALIDATOR_GAPS).
|
||||
- impact: description text stops following the typographic scale; a theme that bumps `--font-size-*` won't
|
||||
move it. Silent.
|
||||
- proposed-fix: tokenize against the scale (e.g. one step below the item: `var(--font-size-sm)` at md) or
|
||||
keep relative but make the guard reject bare `em` font-size literals too.
|
||||
- fix-status: open
|
||||
|
||||
### MEDIUM: `content-z: '80'` — magic z-index literal in recipe <!-- id: select-009 -->
|
||||
- dimension: E-bis
|
||||
- rule: THEMING §35 Bloque C (z-index magic numbers → `--z-index-*` tokens) + feedback "no intentional magic numbers"
|
||||
- location: lib/recipes/base.ts:2206 (`'content-z': '80'`); consumed select.css:166.
|
||||
- evidence: a bare `80` instead of a `var(--z-index-{layer})` token.
|
||||
- impact: overlay stacking is a cross-component decision; a per-recipe integer can't be re-coordinated
|
||||
centrally. Likely recurs in combobox/popover/dropdown content-z → promote to systemic if confirmed.
|
||||
- proposed-fix: map to `--z-index-popover` (or the canonical floating layer token) and verify the ladder.
|
||||
- fix-status: open
|
||||
|
||||
### LOW: Magic letter-spacing + spacing literals where canonical scales exist <!-- id: select-010 -->
|
||||
- dimension: E-bis
|
||||
- rule: THEMING §6 (slots/scales) + §35 (`--tracking-*`) + R-2.3
|
||||
- location: lib/recipes/base.ts:2232 (`'group-heading-letter-spacing': '0.04em'`), :2241
|
||||
(`'item-description-mt': '0.125rem'`).
|
||||
- evidence: `0.04em` for uppercase group heading should be `var(--tracking-caps)`; `0.125rem` (=2px) should
|
||||
be `var(--space-0-5)`.
|
||||
- impact: minor drift; values won't track density/scale changes.
|
||||
- proposed-fix: remap to `--tracking-caps` and `--space-0-5`.
|
||||
- fix-status: open
|
||||
|
||||
### LOW: `SelectColor = ColorRole` (all 9) but recipe implements 8 accents (no tertiary) <!-- id: select-011 -->
|
||||
- dimension: E-bis, G
|
||||
- rule: THEMING §4 "subset por componente" + G-1.2/G-1.3 (type union must match implemented subset)
|
||||
- location: types.ts:20 (`export type SelectColor = ColorRole`) vs recipe `_accent-*` cascades cover
|
||||
secondary/neutral/affirm/fulfill/risk/threat/loss + host=primary (no `color:tertiary`).
|
||||
- evidence: `data-color='tertiary'` is type-valid but silently falls back to the primary host default.
|
||||
- impact: API promises a color the recipe doesn't render; minor.
|
||||
- proposed-fix: narrow `SelectColor` to the implemented subset (or add the tertiary accent). Decide whether a
|
||||
neutral form control should expose the full evaluative palette at all (style observation below).
|
||||
- fix-status: open
|
||||
|
||||
## No-findings dimensions
|
||||
- **D (frontier, severe half):** the soma provider imports only soma layers + the morfo — **zero eidos
|
||||
imports**, no reach into eidos internals. Frontier direction respected. (The undeclared-part issue is
|
||||
filed under A/D as select-005, but the provider→eidos direction is clean.)
|
||||
- **A (naming):** `data-select` / `data-select-{part}` naming is consistent across morfo, provider and CSS;
|
||||
no `data-soma-*`. Root part is `provider` (A2-correct).
|
||||
- **A33 / A30 / A6:** label registry uses `SvelteMap` (A33 ✓); trigger/content ref mirror via `onRefChange`
|
||||
not `$effect` (A30 ✓); typeahead timer disposed in `$effect` return (A6 ✓).
|
||||
- **E (TSC scope):** multi-part `parts: ['trigger','content']` cascade is the textbook TSC v2.2 case and is
|
||||
used correctly; no eager-substitution `:root` derived-token bug; the orphan `_accent-solid` was correctly
|
||||
dropped. Clean.
|
||||
|
||||
## Style observations (opinion, non-blocking)
|
||||
- Exposing the full evaluative palette (`affirm/fulfill/risk/threat/loss`) as a *selectable accent color* on
|
||||
a neutral form control reads as over-rich; a select's accent is brand-ish (hierarchy) more than evaluative.
|
||||
Not a rule violation (Button exposes all 9), but worth a subset decision.
|
||||
- The Content `onkeydown` real-focus fallback (select-003) feels like leftover from a pre-virtual-focus
|
||||
iteration. If it isn't reachable, deleting it would remove a whole class of divergence risk.
|
||||
@ -0,0 +1,19 @@
|
||||
# Audit: separator
|
||||
audit-version: 1
|
||||
audited-at: 2026-06-26
|
||||
scope: eidos (eidos-only primitive)
|
||||
method: theming-coherence category sweep (visual-elements), adversarially verified; roles + `:Morfo` lead-verified CLEAN across all eidos (no success/warning/danger/info; only alert-dialog uses `:Morfo`).
|
||||
|
||||
## Summary
|
||||
Counts (post-verification): CRITICAL 0 · HIGH 0 · MEDIUM 0 · LOW 0. Theming-clean.
|
||||
conformance: Contract solid (as const), purely geometric (no events), orientation-driven (horizontal/vertical), all tokens canonical.
|
||||
as-const: true · roles clean: true · variants clean: true · has-recipe: true
|
||||
|
||||
## Findings
|
||||
_No findings (theming-clean primitive)._
|
||||
|
||||
## Theming facts (E-bis)
|
||||
- magic literals: none
|
||||
- magic z-index: none
|
||||
- non-canonical names: none
|
||||
- roles clean: true · variants clean: true
|
||||
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in new issue