4.6 KiB
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
- 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:7scope: ['soma', 'sema', 'eidos'], button.ts:47, checkbox.ts:7). The omission is a real layer-declaration drift. MEDIUM is correct. - fix-status: fixed (
212624e0)
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)