You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
46 lines
6.9 KiB
46 lines
6.9 KiB
|
3 months ago
|
# 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
|