4.0 KiB
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 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
- 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 keyeduix.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 callsrepeater.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-checkif (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: