12 KiB
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
- 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 blockcalendar: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: fixed (
212624e0)
MEDIUM: Magic literal in recipe token: '0.62' opacity not from canonical scale — 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
- 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
- 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(
[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(
[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
- 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 canonicalch-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).chis 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
- 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-monthso 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 Morfois 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.