إنتقل إلى المحتوى الرئيسي

02 · Requirements

Purpose: The numbered requirements baseline for Rasd Forms v1 — what the toolkit must do (FR), how well (NFR), the invariants that must never be violated, and which document satisfies each requirement. Audience: Engineers building Rasd Forms; developers at UN/NGO organisations evaluating fit; QA writing acceptance tests.

TL;DR

  • Every requirement has a stable ID (FR-nnn/NFR-nnn), a MoSCoW priority, a one-line rationale, an acceptance criterion and the satisfying document. Retired IDs are never reused.
  • Five invariants (§3) outrank every feature: finalized data is never lost; irrelevant answers never leak; license state never blocks export, sync or finishing drafts; the renderer never awaits the network; enumerator devices never phone home.
  • Functional scope = the spine's eight deliverables (engine + RFD, renderers, builder, storage + sync, PWA helpers, theming, media capture, license SDK) plus interoperability, observability and developer experience.
  • Budgets are concrete: a 300-question form loads in ≤ 1 s on the reference low-end Android device, a 10k-choice select filters in < 50 ms per keystroke, a 200-row repeat scrolls at 60 fps, the form-runner is ≤ 120 kB min+gzip, and a hard kill loses at most one autosaveMs window (2 s) of draft input.
  • Yardsticks: ODK/XLSForm semantics for logic (research/09, research/14); WCAG 2.2 AA for accessibility (research/07); OWASP MASVS 2.1 / ASVS 5.0 for security (research/11).
  • §6 is the traceability matrix; §7 is the v1 release-gate checklist.

1. Conventions

ItemRule
IDsFR-nnn functional, NFR-nnn non-functional, INV-n invariant; ranges per area (§6) with intentional gaps.
MoSCoWM Must (v1, blocking) · S Should (v1 target, may slip one minor) · C Could (phase 2–3) · W Won't (not in v1).
NormativityNames, shapes, status values, error codes and APIs are verbatim from the design spine. Numbers the spine does not fix (timeouts, caps) are proposals listed as assumptions.
Reference device2 GB-RAM Android 7–9 phone, Chrome/WebView ≥ 100 (Galaxy A10 class); 3G throttle for network tests; p95 over 20 runs.
ActorsEnumerator (offline, shared low-end phone) · Supervisor (review, conflicts, export) · Form designer (M&E officer in the builder) · Host developer (embeds Rasd) · Org admin (license, devices) · Server operator (RSP or Kobo/ODK back-end).

Out of scope for v1 (Won't): end-user identity (delegated to host); analytics dashboards; SMS/IVR channels; a hosted cross-origin iframe embed (online-only, storage-partitioned — research/05 §7); CRDT multi-user builder editing (research/03 recommends optimistic locking first); a native RN builder.


2. Reference lifecycles

Reliability and licensing acceptance tests are written against these two spine state machines.

stateDiagram-v2
[*] --> draft: create / autosave
draft --> finalized: finalize() passes validation
finalized --> queued: outbox.enqueue (same local tx)
queued --> sending: sync engine picks up
sending --> synced: ack with serverRev + echoed sha256
sending --> queued: network / 5xx (backoff 1 s → 5 min)
sending --> rejected: server 4xx with reasons
rejected --> finalized: user fixes, re-finalizes
synced --> conflict: editable-record pull conflict
conflict --> synced: user resolves
stateDiagram-v2
[*] --> evaluating: no token, dev origin
[*] --> trial: trial token / first-run 7 days
evaluating --> active: RLT verified offline
trial --> active
trial --> limited: 7 d elapse, grace 0
active --> active: refresh when under 14 d to exp
active --> grace: exp passed
grace --> limited: exp + grace passed
grace --> active: refresh ok
limited --> active: refresh ok
active --> invalid: bad signature / apps mismatch
invalid --> evaluating: treated as no token

3. Invariants — must never happen

Each invariant has a dedicated test in @rasd/testing, run against Dexie and SQLite, web and native.

IDInvariantVerification
INV-1Finalized data is never lost. After finalize() resolves, submission and outbox row exist in one committed local transaction; no path (license state, storage migration, form-version update, wipe without host confirmation, app kill) removes it before a server ack echoes its sha256.Kill during finalize/sync/migration: outbox count after restart equals count before.
INV-2Irrelevant answers never leak. A value whose element or ancestor is relevant = false at finalize is excluded from toSubmission(), exports and serializers, but kept in the draft.Property test over random relevance toggles: keys(sub.data) ⊆ relevant names.
INV-3License state never blocks export, sync or finishing draftsexport(), syncNow(), draft completion and reads work in all six license states.Matrix: 6 states × export/sync/draft-finish succeed.
INV-4The renderer never awaits the network. No render, autosave, validation or navigation path performs a network call.Lint: no fetch in @rasd/core/react/native; Playwright offline project renders every fixture.
INV-5No telemetry, no phone-home from enumerator devices. Devices contact only host-configured endpoints; license verification is offline; refresh goes via the host backend or the sync X-Rasd-License header.E2E network capture: zero requests to non-host origins.

4. Functional requirements

Columns: requirement · P (MoSCoW) · rationale · acceptance criterion · satisfying doc.

4.1 Form definition & custom payload (FR-001 – FR-008)

IDRequirementPRationaleAcceptanceDoc
FR-001One RFD JSON document (layout, logic, translations, theme hints, custom payload) validated by schema/rasd-form.schema.json via validateFormDefinition().MP2: form is dataNegative fixtures fail RASD_SCHEMA_INVALID; all examples/*.form.json pass.04
FR-002Every object node accepts ext: { [vendorKey]: unknown }, validated as an object and round-tripped unchanged through engine, builder, storage, sync, XLSForm export.MP4Deep-equal round-trip with ext at every level.04
FR-003Custom types x:<name> via defineElement(); unregistered x: types render a placeholder, never crash.MHost widgetsUnknown type renders with warning, no console error.06
FR-004Localized strings string | { [bcp47]: string } everywhere, restricted to the Rasd Mini-Message subset ({var}, plural, select, #, ').MMulti-language normOut-of-subset syntax fails validation; Arabic six-category plurals render.13
FR-005version monotonic per id; published versions immutable; definitionHash = SHA-256 of canonical JSON.MReproducibilityHash stable across key order; server rejects re-publish.04
FR-006diffDefinitions() classifies changes (compatible/transform/breaking) with a plan; builder blocks breaking changes (type change except widening to text, retired-name reuse, repeat rename, removing a referenced list) unless forced; migrateSubmission() migrates drafts only, opt-in, plan-driven.MCentral-style safety (research/12)21 taxonomy rows have fixtures; migrating a finalized submission throws.04, 08
FR-007Within an RFD MAJOR: unknown properties ignored-and-preserved; unknown element type → placeholder + degraded; unknown REL function → refuse to open.MVersion skewrasd:"1.9" fixture loads on 1.0 client, re-saves byte-identical.04
FR-008Preload ${meta.*} (deviceId, userId, username, startedAt, now, locale, formVersion, appVersion, platform, submissionId, custom.*) in REL and Submission.meta; settings.instanceName computed on finalize.MODK preload parityEach key resolves in a calculate; instanceName shown in lists.05

4.2 Element types (FR-010 – FR-015)

IDRequirementPRationaleAcceptanceDoc
FR-010All spine §4.3 element types with their props on both renderers (textrepeat, incl. consent, matrix, geo, media, barcode, signature, note, hidden, calculate, group).MXLSForm vocabulary (research/01 §4)Storybook matrix type × {en, ar} × {light, dark} × {web, native} passes axe.06, 07
FR-011Value shapes exactly per spine (select_multiplestring[], geopoint/consent objects, media → attachmentRef), enforced by zod.MServer contracttoSubmission() validates per type.04
FR-012Selects: list/inline choices[], choiceFilter, search, other, randomize, minSelected/maxSelected, exclusive.MCascades, "other"Governorate→district cascade works; exclusive:["none"] clears others.06
FR-013repeat (min/max/count/itemLabel/keyField/confirmDelete/allowReorder; shrinking count hides, never deletes); group data-transparent unless nestData; name unique per repeat scope so cross-group moves are non-breaking.MRosters; ODK semanticscount 5→3→5 restores rows 4–5; cross-group move is compatible.05
FR-014consent stores text version, locale, timestamp, method, withdrawal; bind.sensitive/saveIncomplete/trackChanges/index drive encryption, redaction, audit, indexes.MData protection by designWithdrawal audited without deleting original; sensitive fields field-encrypted, hidden from "sent" lists.16
FR-015validators[] (regex, range, length, expr, custom) with severity: error | warning | info; warnings never block finalize and are audited; per-locale media with auto-play audio.MSoft checks; low literacyWarning allows finalize + audit event; Arabic audio plays offline.05, 14

4.3 Logic (FR-020 – FR-029)

IDRequirementPRationaleAcceptanceDoc
FR-020REL v1: Pratt-parsed, no eval/new Function, static dependency extraction; sandbox AST ≤ 5k nodes, ≤ 10 ms per evaluation with abort, prototype-safe access.MCSP/RN safe; hostile authors10k-case fuzz never throws uncaught; CPU-bomb aborts with RASD_EXPR_BUDGET.05
FR-021References ${name}, ${../name}, ${/root}, ${rep[2].f}, ${rep[].f}, ${meta.*} per spine §5.MRepeats3-level nested repeat resolves all ref kinds.05
FR-022Full spine §5 function set plus ODK aliases in call position (string-length(, count-selected(, selected-at(, indexed-repeat(, format-date(, div, mod, true()); host registerFunction(name, fn, {pure}).MReal XLSForms parse unchanged≥ 95 % of corpus expressions parse; conformance table vs @getodk/xpath.05
FR-023relevant: hidden, not validated, required off, value kept in draft, excluded on finalize (INV-2); cascades from group/page.MODK bind semanticsHide→show restores value; finalize omits hidden.05
FR-024required (bool/REL) enforced only when relevant; constraint evaluated only when non-empty, on change and finalize; localized messages.MConstraint ignores empty[] fails required, 0 passes; empty value skips constraint.05
FR-025calculate recomputed in topological order; cycles fail at load; once()/default.expr evaluate once; recomputation batched per microtask, memoised.MDeterministic derived valuesCycle fixture fails validation; once(uuid()) stable across reload; only affected fields re-render.05
FR-026Triggers logic.triggers[] (when → ordered actions[]: complete, setValue, jump, message) evaluated after each change batch.MEarly-exit consent flows${consent}='no' completes with localized message + audit.05
FR-027Cascading selects via choiceFilter and dataset lists (filterKeys), equality filters pushed to storage ≥ 1k rows; pulldata()/datasets.query() offline; dataset answers snapshot {value,label}.M10k admin units10k-row filter < 50 ms/keystroke on device; label survives dataset update.05, 09
FR-028Headless createFormEngine() (getState, setValue, addRepeat, removeRepeat, validate, finalize, subscribe, toSubmission) with no React/DOM/RN imports.MP3Node CLI finalizes a fixture.03
FR-029Optional XPath-coercion mode per imported form (empty→NaN, string booleans, anchored regex).SFidelity traps (research/14 §8)Flagged form evaluates regex() anchored like JavaRosa.20

4.4 Rendering (FR-030 – FR-040)

IDRequirementPRationaleAcceptanceDoc
FR-030settings.navigation paged (default) / scroll; validateOn = change (default) | blur | page | finalize; errors adjacent via aria-describedby; finalize shows a live-region error summary with jump-to-field.MErgonomics; WCAG 3.3.xBoth modes render; summary click focuses field; axe passes.06
FR-031Autosave every change ≤ settings.autosaveMs (default 2000) and immediately on page change, blur, visibilitychange:hidden, background, finalize.MNever lose dataKill loses ≤ one window; page navigation loses nothing.06, 09
FR-032Drafts (allowDrafts): list, resume at last page/scroll, delete with confirmation; pinned to formVersion + definitionHash and opened with that version.MCollect behaviourDraft on v2 opens with v2 after v3 installs.06
FR-033Runtime language switch (useLocale().setLocale()) atomically flips strings, dir, digits, calendar without remount; RTL automatic (dir="rtl" web; per-form Yoga direction native, no I18nManager restart), logical properties, mirrored icons, LTR islands for numbers/IDs, dir="auto" on free text.MArabic-first (P5)en↔ar switch keeps values and focus; bidi checklist (research/13 §5) as en/ar/en-XB snapshots.13
FR-034readOnly review mode (values only, relevance/calculations kept, no autosave); print/summary view of relevant answers with redact (window.print() web, host share callback native).M / SSupervisor review; receiptsZero writable controls and storage writes; summary omits irrelevant/redacted.06, 07
FR-035Identical public API on @rasd/react and @rasd/native (<RasdProvider>, <FormRenderer>, hooks, defineElement, registry).MOne mental modelShared behavioural suite green on both.07
FR-036Repeat UI: add/remove/reorder, confirmDelete, collapsed headers from itemLabel, virtualised above 30 items.M200-row rostersSee NFR-004; delete confirmation audited.06
FR-037Progress (showProgress) over relevant pages only; big-touch defaults via rasd-field (48 px).MOne-hand useProgress = relevant index / relevant pages.06
FR-038Audit trail per settings.audit: start/resume/save/finalize, value change (old/new when trackChanges), jump, add/delete repeat, constraint error, trigger; optional location trail (minSeconds/minMeters), off by default.MTPM evidence; ODK audit.csvExport matches ODK event vocabulary.16
FR-039Rich text sanitised: DOMPurify allow-list (web), markdown-to-native, no HTML (RN); media URLs limited to mediaAllowList; violations → onPolicyViolation.MXSS from hostile definitionsmXSS corpus inert; off-list/javascript: URLs dropped.16
FR-040useRasdBusy() / isDirty() so hosts defer SW updates and navigation while a draft is dirty.MNo mid-entry reloadRecipe test defers SW waiting while dirty.11

4.5 Builder (FR-041 – FR-049)

IDRequirementPRationaleAcceptanceDoc
FR-041Three-pane shell (palette · canvas · inspector), tabs Designer/Logic/Translations/Preview/JSON/Versions gated by features; separate lazy chunk never loaded by field runners.MConsensus UX (research/03 §4)features={{logic:false}} hides Logic; size-limit proves runner excludes builder.08
FR-042DnD via @dnd-kit/react 0.5.x behind dnd-adapter; every drag has a non-drag equivalent (⋯ menu: move up/down/top/bottom/into, indent/outdent, duplicate, delete) plus localized, RTL-mirrored shortcuts (WCAG 2.5.7).MAccessibility; phonesEvery reorder achievable pointer-only and keyboard-only; axe passes.08
FR-043Inspector covers every spine §4.2 property incl. props, appearance, bind, validators, ext (raw JSON per vendor key); whole-form JSON tab with schema validation and two-way sync; per-element "Edit JSON".MFull RFD coverageEvery schema property reachable; invalid JSON blocked with location.08
FR-044Logic editor: conditions/actions with nested AND/OR, ordered rules, fallback, for all bind properties and triggers; REL code mode with parse errors and dependency preview.MKobo/Formbricks patternGUI rule round-trips to identical REL; parse error < 100 ms.08
FR-045Translations grid (rows = localized fields incl. media, columns = settings.locales), completeness %, stale flags (never auto-clear), plural/placeholder checks, XLIFF 2.1 + CSV/XLSX round-trip, onMachineTranslate hook.Mar/en/fr formsMissing Arabic few flagged; export/import round-trips all cells.08
FR-046Preview uses the real <FormRenderer> with device frame, RTL toggle, locale switch, offline simulation; preview data never enqueued.MVerify logicPreview finalize creates no outbox row.08
FR-047Versions/publish: onPublish with suggested next version, semantic changelog from diffDefinitions, breaking-change block/force, "changes since v(n-1)" diff.MCentral-style safetyPublish disabled on reused version or unchanged hash.08
FR-048Undo/redo via immer patches, keystroke coalescing ≤ 500 ms, ≥ 100 steps, last batches persisted for crash recovery.MEditor baselineCtrl/Cmd+Z restores; reload after crash offers recovery.08
FR-049Plugins via defineElement({type:'x:foo', component, builder:{icon,label,inspector}, valueSchema}); question/section library of fragments (S); theme editor panel (C).M / S / CP4; standard modulesPlugin in palette + inspector; fragment drop auto-suffixes clashing names.08

4.6 Offline storage (FR-050 – FR-057)

IDRequirementPRationaleAcceptanceDoc
FR-050StorageAdapter (spine §7) implemented by @rasd/storage-dexie (Dexie 4), @rasd/storage-sqlite (expo-sqlite default, op-sqlite optional), MemoryStorage; web versionchange/blocked handled with "close other tabs" UI.MP1Conformance suite passes on all three; two-tab upgrade loses nothing.09
FR-051Attachments as unindexed Blobs (web) / app-private files (native), content-hash addressed, sizeOf/listByStatus.MPhotos are the bytes200 × 300 KB photos round-trip with matching sha256.09
FR-052Quota: estimate(); web calls navigator.storage.persist() after first sync/install; warn < 100 MB headroom; RASD_STORAGE_QUOTA surfaced as recoverable "cannot save offline".MSafari eviction (research/04 §1.3)Simulated quota error keeps in-memory draft.09
FR-053Encryption at rest: web AES-256-GCM, non-extractable WebCrypto key; native SQLCipher, key in SecureStore/Keychain/Keystore, field-level AES-GCM fallback for sensitive; index columns cleartext; policy preferred with loud warning.MUN security reviewssecurityReport() shows status; on-disk payload not plaintext-searchable.16
FR-054export() streams JSONL + blobs offline in every license state (INV-3), optionally encrypted to recipient keys; re-import lossless.MP61,000-submission export offline; re-imports.09
FR-055Migrations: ordered, checksum-tracked; schema steps in engine transaction; data steps chunked (500 rows), resumable via _rasd_migrations.MCrash mid-migrationKill mid-migration → resume completes; checksum tampering refuses.09
FR-056Retention: keep versions referenced by drafts/outbox plus last 3; purge finalized submissions and attachments after ack (default on, metadata kept).MStorage; privacyAfter ack get(id) returns metadata only; referenced versions never GC'd.09
FR-057wipe() crypto-shreds keys, deletes DB/attachments; with unsynced data requires two-step host confirmation showing the count.MHandover; remote wipeonLocalWipeRequested({unsyncedCount}) fires before deletion.16

4.7 Sync (FR-060 – FR-069)

IDRequirementPRationaleAcceptanceDoc
FR-060Outbox: submission and outbox op written in one local transaction on finalize; FIFO per form; peek/ack/fail with nextAttemptAt.MINV-1Single-transaction test.10
FR-061RSP client per spine §8: POST /v1/submissions:batch (≤ 50, Idempotency-Key = batch hash) with accepted/duplicate/rejected/conflict; X-Rasd-Device, X-Rasd-Client; status transitions exactly per spine §6 via useSubmission()/useSync().MIdempotent retriesReplaying an acked batch yields duplicate; state-machine test forbids other transitions.10
FR-062Attachments via tus 1.0.0 (POST/PATCH/HEAD, Upload-Metadata = submissionId, field, sha256), resumable across restarts, hash echoed.M2G/3G uploadsKill at 40 % → resume from offset; mismatch → back to queued.10
FR-063Pull forms (GET /v1/forms?since=, /versions/{v}) with hash + optional JWS check; datasets with tombstones/cursors; "replacement" full re-sync leaving the outbox untouched (S).M / SOffline-first updates; recoveryTampered definition refused; re-sync leaves outbox intact.10
FR-064Foreground-driven, single leader (Web Locks / one RN instance), single-flight; priority outbox → attachments → forms → datasets → records; triggers online/foreground/finalize/manual; Background Sync API / expo-background-task accelerators only.MSafari/Firefox lack Background Sync (research/05)Two tabs: one syncs, both get events; Firefox/WebKit sync on online.10
FR-065Backoff exponential, full jitter 1 s → 5 min, reset on success, honour Retry-After; permanent 4xx (except 408/425/429) → rejected with reasons; 4xx never deletes local data.MCaptive Wi-Fi, flaky 3G5 × 503 → delays in [1, 300] s then success.10
FR-066useSync() → last sync time, pending counts per state, per-attachment progress, current error; syncNow(), pause(), resume(); events progress, error, conflict, formUpdated, datasetUpdated, licenseRefreshed.MEnumerator confidenceCounts update ≤ 200 ms after state change.10
FR-067Auth delegated (getAuthToken()); non-https: refused outside dev; custom fetch for pinning; POST /v1/devices policy (intervals, retention, wipe flag) and optional SSE /v1/events (S).M / SHost owns identity; fleet policyhttp:// in production throws; wipe flag → onRemoteWipe after nonce check.10
FR-068Records/cases: per-field LWW by HLC; true collisions → conflict in a supervisor queue with both values; enumerators never see merge dialogs.SLongitudinal (phase 2)Disjoint edits merge silently; same field → conflict.10
FR-069@rasd/server accepts any published version; unknown version/hash → 422 + quarantine, never dropped; transport adapter interface (rsp, openrosa phase 3, custom) with identical engine semantics.Mformpack silent-skip lesson (research/12 §6)Retired-version submission accepted; custom transport passes contract tests.10, 20

4.8 PWA & embedding (FR-070 – FR-076)

IDRequirementPRationaleAcceptanceDoc
FR-070npm ESM packages with 'use client' boundaries for Vite, Next.js App Router (ssr:false builder), Expo web.MPrimary distributionplayground-web, example-next, example-expo pass E2E.11
FR-071<rasd-form> custom element (open Shadow DOM, CSS-variable theming through the boundary, portals inside the root) emitting rasd:change/rasd:finalize/rasd:error; IIFE rasd-forms.iife.js bundling React, with SRI hashes.MNon-React hostsPlain HTML + IIFE fills and finalizes offline; picker styled inside shadow root.11
FR-072SW helpers that never own the host worker: registerRasdRoutes(), runtime-caching config for generateSW, prebuilt rasd-sw.js for importScripts, narrow-scope fallback SW via CLI; precacheForms() uses namespaced caches (rasd-defs-v1 SWR/200; rasd-media-v1 CacheFirst 30 d/500).MOneSignal/Firebase patternVite, Next+Serwist, Expo web recipes tested; airplane-mode form renders with images and Arabic font.11
FR-073useInstallPrompt() (Chromium prompt, iOS Add-to-Home-Screen guidance) and useServiceWorkerUpdate() (waiting → prompt → skipWaiting → reload) deferring while isDirty().MEscape Safari 7-day purgeUpdate never reloads while dirty.11
FR-074Strict-CSP compatible (no eval, constructed stylesheets/<link>, self-hosted fonts/WASM); every chunk < 2 MB uncompressed.MAgency IT policiesRuns under script-src 'self'; style-src 'self'; per-chunk size check.11
FR-075Diagnostics (estimate(), persisted, standalone, queue length, SW state) and rasd doctor offline-readiness CLI.SReplaces "Lighthouse PWA score"Doctor flags missing persist(), icons, non-https.11
FR-076Web Push with Declarative Web Push payloads (C); hosted iframe embed (W).C / WiOS 18.4+; partitioning11

4.9 Media & field capture (FR-080 – FR-087)

IDRequirementPRationaleAcceptanceDoc
FR-080GPS: live accuracy, auto-accept ≤ accuracyThreshold (default 5 m), non-blocking warning > 100 m, 60 s timeout with retry, allowManual map placement, Android mock flag; stores {lat,lng,alt?,accuracy?,capturedAt}.MCollect UX (research/09 §4.1)Fake 30 m fix warns and allows accept; timeout tested.14
FR-081Geotrace/geoshape manual, tap-to-place, automatic (intervalSeconds); distance()/area(); offline basemaps via MapLibre + PMTiles (optional module).SSite mappingAuto mode filters by accuracy; shape closes; area computed.14
FR-082Photo: camera/gallery/both, resize to maxPixels (default long edge 1280 px), JPEG quality ≈ 0.7, EXIF stripped with sidecar {capturedAt, geo} when geotag, annotate, multiple/maxCount.MBandwidth; privacy12 MP capture ≤ 350 KB; no GPS EXIF; sidecar present.14
FR-083Barcode/QR: feature-detect BarcodeDetector, fall back to barcode-detector ponyfill with self-hosted WASM; native expo-camera; formats, allowManual.M~76 % availabilityScans offline in Firefox/Safari; manual entry available.14
FR-084Signature: canvas pad (web), Skia/native pad (RN, no WebView), trimmed PNG ≤ 30 KB, penColor, clear/redo.MConsent, receiptsKeyboard-accessible clear; RTL-safe.14
FR-085Audio foreground-only by default (MediaRecorder / expo-audio), mono 24–32 kbps, maxDurationSeconds; files/video with accept, maxBytes, multiple (video defaults 25 MB / 120 s); attachment budget default 10 MB per submission. Ceilings are normative in 00 §7.1 only.S / MStore policy; sync feasibility1-min recording ≤ 250 KB; over-budget file rejected with localized message.14
FR-086Capture adapters behind @rasd/media interfaces, code-split per capability, fakes in @rasd/testing; missing adapter → "capture unavailable", never crash.MP3; budgetRenderer without media adapters still renders media questions.14
FR-087Permission UX with localized rationale and recoverable denied state; store-declaration templates for background location/mic; attachments never copied to gallery/Photos unless host opts in.MPlay/App Store policy (research/11 §11)Denied camera does not block an optional photo; backup/gallery checks pass.14

4.10 Theming (FR-090 – FR-097)

IDRequirementPRationaleAcceptanceDoc
FR-090Theme JSON per schema/rasd-theme.schema.json (id, extends, mode, tokens.{color,typography,spacing,radius,elevation,motion,control}, components, ext); createTheme(partial, {extends}); settings.theme hint honoured unless host overrides.MNon-developer brandingTheme extending rasd-field validates and renders inside a rasd-light host.12
FR-091Web: --rasd-<group>-<key> scoped on .rasd-root[data-theme][data-color-scheme][data-contrast][data-density][dir], never :root; CSS in @layer rasd; parts rasd-<Component>__<part> + data-scope="rasd" data-part.MTwo agency themes per page; host CSS winsTwo themed forms coexist; unlayered host rule wins without !important.12
FR-092Native: same tokens via useTheme() → plain StyleSheet; per-part styles; allowFontScaling with typography.maxFontScale 2.0.MNo styling-engine dependencySnapshot matrix font scale 1.0/1.3/2.0 × ltr/rtl × light/dark/high-contrast.12
FR-093Per-part classNames/styles/render, unstyled, components registry, per-type renderers; overrides honour the accessible-props contract (label id, described-by, error state).MDesign-system integrationReplacing TextField passes the contract test.12
FR-094Modes as data (light/dark/highContrast/reducedMotion/density) from OS signals unless forced; bundled rasd-light, rasd-dark, rasd-high-contrast, rasd-field pass 4.5:1 text / 3:1 UI.MOutdoor readability; a11yprefers-color-scheme: dark selects dark; rasd theme check passes all four; reduced motion → 0 ms.12
FR-095Fonts as offline theme assets (WOFF2 precached, static TTF native), default OFL Arabic family, fontFamilyRtl; subsets keep mark/mkmk/rlig and bidi controls.MArabic shaping offlineAirplane-mode Arabic label uses bundled font.12
FR-096fromDtcg()/toDtcg() (DTCG 2025.10) and rasd theme check (schema + WCAG contrast lint).SFigma/Style-Dictionary pipelinesDefault theme round-trips DTCG losslessly.12
FR-097UN-agency brand-pack sample themes with brand accent separated from text tokens.CAdoption acceleratorSamples pass rasd theme check.12

4.11 i18n & accessibility (FR-100 – FR-106)

IDRequirementPRationaleAcceptanceDoc
FR-100Core i18n uses only Intl.NumberFormat/DateTimeFormat/Collator, compiled CLDR plural functions and an in-house BCP 47 matcher; never Intl.PluralRules (absent on Hermes) or @formatjs/intl-locale.MHermes gaps (research/13 §1)Hermes-like Intl shim passes ar/fa/uk/my plural tests.13
FR-101Locale negotiation: lookup with truncation + macro-language best-fit; explicit non-fallbacks (ckbku); per-string fallback chain shown as "showing default" with dir="auto".MNo blank labelsar-JOardefaultLocale; fallback text dir="auto".13
FR-102settings.numbering (latn/native), settings.calendar (gregorian/islamic-umalqura), per-locale localeMeta; inputs normalise Arabic-Indic/Extended digits to ASCII on save; deterministic Umm al-Qura fallback.MDigits are per-regionTyping ٣٤ stores 34; Hijri displays without Intl support.13
FR-103Chrome catalogs (validation, nav, sync, a11y; builder separate) for Tier-1 locales (en, ar, fr, es, uk, fa, ps, ur, ckb, ku, so, am, ti, ha, bn, my, sw, tr, pt, ru), host-overridable per key; pseudo-locales en-XA/en-XB in snapshots; docs in en/ar/fr.SDifferentiatoren, ar, fr complete at v1, others ≥ 80 %; missing key warns once.13
FR-104WCAG 2.2 AA for renderers and builder: visible labels, focus ring ≥ 2 px at 3:1, 2.4.11 focus not obscured, 2.5.8 targets ≥ 24 px (default 48), 3.3.7 no redundant entry, 4.1.3 live-region status.MLegal/ethical baselineaxe zero violations; manual VoiceOver/TalkBack/NVDA script per release.13
FR-105RN a11y: accessibilityLabel/LabelledBy/Hint/State/Role, announceForAccessibility for errors, reduce-motion/high-contrast signals honoured.MTalkBack on field devicesRNTL a11y assertions per element type.07
FR-106Arabic-aware search/sort: Intl.Collator(base, ignorePunctuation, numeric) + normalizer (NFKC, tashkeel strip, alef/yaa/taa-marbuta folding) stored as label_norm at import.M10k Arabic lists"امان" matches "أمّان"; sort stable across engines.13

4.12 Licensing (FR-110 – FR-117)

IDRequirementPRationaleAcceptanceDoc
FR-110RLT = compact JWS EdDSA (Ed25519), header {alg,kid,typ:"RLT"}, claims per spine §9, verified 100 % offline against embedded keys rotatable by kid (@noble/ed25519 on RN).MZero phone-homeUnknown key → invalid; expired valid signature → grace/limited.15
FR-1117-day trial: signup token (one per org domain) or zero-config first-run local trial; evaluating indefinitely on dev origins.MAdoptionlocalhost renders without watermark; production without token → trial 7 d then limited.15
FR-112Monthly plans exp 60 d rolling, grace 30 d; refresh when < 14 d to exp, jittered, never blocking render, via host tokenEndpoint or X-Rasd-License header; annual/offline license file for air-gapped builds.M~90 days offline toleranceFake clock: 85 days offline stays functional; day 91 → limited.15
FR-113limited + enforcement:"soft" (default): renderer works with "Unlicensed – Rasd Forms" watermark + console warning, builder read-only, no publish; "hard": no new submissions, but drafts finish, sync continues, export works (INV-3).MNever lose field dataState × action matrix; watermark visible.15
FR-114Clock tampering: monotonic guard on last-seen server time; rollback freezes state, never regresses it.MWrong field clocksDevice time −30 d keeps state and grace.15
FR-115useLicense()/getState() expose state, plan, exp, features, refresh(); gating by features[] and apps[] (origins with wildcards, bundle IDs); state persisted in kv.MOption A/B is configurationToken without "builder" → builder read-only; origin mismatch → invalid.15
FR-116Billing: Stripe Billing + Entitlements + Invoicing (POs, Net-30, tax-exempt) → license service issues/rotates tokens; manual "PO paid" mints 12-month tokens.SUN procurementWebhook and manual paths mint valid tokens.15
FR-117rasd license check fails CI beyond grace; RASD_LICENSE env injection; "renewed key not applied" troubleshooting; trial auto-extension to 30 d on milestones (C).S / CMost complaints are UXCommand exits non-zero on limited.15

4.13 Security & data protection (FR-120 – FR-128)

IDRequirementPRationaleAcceptanceDoc
FR-120SecurityPolicy with safe defaults (encryption.atRest:'preferred', backup exclusion on, purge-after-sync on, autoLockAfterMs 300000, requireHttps, mediaAllowList=[syncOrigin], log level warn, export encrypted-only) and securityReport().MMASVS L2Report lists active controls; defaults match doc table.16
FR-121Definition hardening: ≤ 2 MB, strings ≤ 64 KB, inline lists ≤ 10k, datasets ≤ 100k rows (host-configurable); __proto__/constructor/prototype keys rejected.MHostile server/authorOversized/polluting fixtures fail with RASD_SCHEMA_INVALID.16
FR-122Expo config plugin: Android dataExtractionRules/allowBackup excluding DB + attachments (merged with expo-secure-store rules); iOS isExcludedFromBackup on library dirs.MBackup leakage (T8)Emulator backup/restore leaves no DB; iOS attribute dump shows exclusion.16
FR-123Redacting logger (answers, entity rows, tokens, tus URLs never logged), createSentryScrubber(); sensitive-field hardening (importantForAutofill="no", autoComplete="off", autoCorrect off, contextMenuHidden, optional screen-capture prevention).MSide channels (T9); MASTGDebug output contains no fixture answers; static prop check.16
FR-124settings.encryption.mode:"submission": per-submission AES-256-GCM key wrapped to publicKeyId; ODK-compatible envelope for Central managed encryption.SSensitive protection dataServer cannot read payload; ODK envelope decrypts with Central passphrase.16
FR-125App lock (onLock/onUnlockRequired, key-bound biometrics via SecureStore requireAuthentication), enumerator profiles (logout() hides, never deletes unsynced), supervisor admin PIN with audit.SShared devices (T2)Idle 5 min → onLock('idle'); other profile's drafts hidden yet synced.16
FR-126Remote wipe on validated policy nonce → crypto-shred + delete + onRemoteWipe({reason, unsyncedCount}); web Clear-Site-Data guidance.SLost/stolen deviceNo readable payload after wipe; unsynced count reported.16
FR-127Integrity: SHA-256 on submissions/attachments echoed by server; optional JWS-signed definitions.MTamper evidenceAck hash mismatch → back to queued.10
FR-128Vendor pack: MASVS/ASVS mapping with MASTG evidence, CycloneDX SBOM, provenance, DPIA template, residual-risk register; form-level legalBasis + sensitivity data-inventory export (C).S / CUN vendor questionnairesPack published per minor release.16

4.14 Interoperability (FR-130 – FR-135)

IDRequirementPRationaleAcceptanceDoc
FR-130XLSForm → RFD import (@rasd/xlsform, rasd convert xlsform, builder): all survey/choices/settings/entities columns, label::Lang (code), Kobo begin_score/rank/kobomatrix folding, unmapped XPath under ext["org.getodk.xpath"] with cell-level warnings.MAdoption (research/01 §5)≥ 90 % of a 140-form corpus imports with zero errors.20
FR-131RFD → XLSForm export that pyxform 4.x compiles; CI round-trip compares canonical XForm XML.MKobo/ODK back-endsImport→export→pyxform succeeds; diff report published.20
FR-132XForm instance serializer/parser (space-separated multi-selects, lat lon alt acc, ;-joined traces, TZ-offset datetimes, meta/instanceID = uuid:, deprecatedID, non-relevant nodes removed); openrosa transport (phase 3): formList/manifest, HEAD + chunked multipart, X-OpenRosa-Version: 1.0, 201/202/409, profiles kobo/central/ona, token-in-URL enrolment, Collect QR import.SKobo / Central / MoDaInstances validate against pyxform XForms; contract tests per profile.20
FR-133Server exports: union-of-versions CSV/XLSX with __version/__definitionHash, alias merge, Kobo-family and Central conventions, single-version mode, JSONL always; Central-shaped OData feed (C).S / CPower BI templates3-version form yields union columns with per-row version.20
FR-134ODK-style entity lists (name, label, __version) as datasets with create_if/update_if → records; optional PowerSync/Electric/CouchDB transports.CLongitudinal interop20
FR-135Import of SurveyJS/Form.io JSON.WDifferent market

4.15 Observability (FR-140 – FR-143)

IDRequirementPRationaleAcceptanceDoc
FR-140Every engine exposes on(event, handler)/subscribe() returning an unsubscribe; renderer onChange/onSave/onFinalize; hosts derive metrics from events.MHost-owned metricsEvent contract tests; unsubscribe stops delivery.17
FR-141Structured redacting logger with levels and pluggable sink; default warn; debug stripped in production; no vendor telemetry (INV-5).MDiagnose without PIISink receives structured objects; network capture clean.16
FR-142RasdError with stable documented codes (RASD_SCHEMA_INVALID, RASD_EXPR_PARSE, RASD_STORAGE_QUOTA, RASD_SYNC_REJECTED, RASD_LICENSE_EXPIRED, …), details, cause; never renamed within a major.MProgrammatic handlingEvery thrown error in tests is a RasdError with a documented code.17
FR-143securityReport() and rasd doctor emit machine-readable, PII-free diagnostics.SSupport ticketsOutput validates against its JSON schema.21

4.16 Developer experience (FR-150 – FR-155)

IDRequirementPRationaleAcceptanceDoc
FR-150Strict TypeScript types for RFD, Submission, Theme, engine, hooks; isolatedDeclarations; public API report per PR; rasd types generates a form's data type.MDeveloper adoptiontsc --strict on examples; API diff blocks unintended breaks; generated type matches toSubmission().18
FR-151Docs: getting-started < 15 min (React and Expo), type-generated API reference, recipes for Vite/Next/Expo/plain HTML, hosted playground with JSON ↔ render, RTL and offline toggles.MTime-to-first-formUsability session completes the tutorial in one sitting.21
FR-152@rasd/cli: rasd validate, rasd convert xlsform, rasd types, rasd theme check (+ license check, doctor), each with --json and exit codes.MAutomationCLI tests per command.21
FR-153@rasd/testing: renderForm(), fake storage, fake clock, network fault injection, form fixtures, fake media adapters.MHosts test their formsHost asserts relevance/validation of its RFD in Vitest/Jest without a browser.18
FR-154Example apps (playground-web, example-next, example-expo) as CI E2E targets; Storybook for every component in the locale/theme matrix with visual regression (S).M / SLiving recipesPlaywright + Maestro pass per release.18
FR-155Changesets + semver, ESM-only, exports with react-native condition, npm Trusted Publishing with provenance; runtime/type deprecation warnings ≥ 12 months before removal; converters + migration guides for RFD MAJOR bumps.MSupply chain; long-lived formsEvery release has provenance; converter round-trips all fixtures.18

5. Non-functional requirements

5.1 Performance (NFR-001 – NFR-008)

Desktop budgets run per PR; device budgets run nightly on a device farm.

IDRequirementPRationaleAcceptanceDoc
NFR-001Cold start of the form-runner (core + react + storage-dexie + sync) from cached PWA to interactive first page of a 30-question form ≤ 1.5 s device, ≤ 300 ms desktop.MEnumerator at the doorTTI trace in CI.06
NFR-002300-question form load (parse + validate + graph + first page) ≤ 1,000 ms device; createFormEngine() ≤ 150 ms desktop.MLarge PDM toolsBenchmark fixture examples/pdm-300.form.json (302 questions, generated by tools/build-pdm-300.mjs). Desktop half met: 3.8 ms median, packages/core/test/perf.test.ts. Device half needs the Phase 4 lab.05
NFR-003Value change → recompute → re-render ≤ 50 ms p95 device, ≤ 16 ms desktop in the 300-question form; only affected fields re-render.MTyping feels instantReact Profiler commit-count assertion.06
NFR-004200-row repeat: add row ≤ 100 ms, sum() ≤ 10 ms, 60 fps scroll (virtualised), memory growth ≤ 30 MB.MHousehold rostersPerf test on both renderers.06, 07
NFR-00510k-choice select: filter < 50 ms per keystroke (pre-normalised index, store-side filter); first paint ≤ 100 ms.MAdmin-unit listsBenchmark with 10k Arabic labels.09
NFR-006Autosave write ≤ 50 ms p95, never blocks input; language switch ≤ 200 ms for 300 questions.MNo dropped keystrokes200 chars at 100 ms interval lose none.09
NFR-007Sync: 50 submissions/batch; 100 × 200 KB attachments on 3G without UI jank; resume overhead ≤ 1 round-trip per attachment; 50k-submission migration ≤ 5 min on device with progress.SField bandwidth; long deploymentsThrottled Playwright project; nightly device test.10
NFR-008Builder edits a 300-question form at 60 fps, undo ≤ 50 ms, canvas virtualised above 60 items; XLSForm import of 1,000 rows ≤ 3 s in Node.SDesigner productivityPlaywright perf trace; benchmark.08

5.2 Reliability & data safety (NFR-010 – NFR-013)

IDRequirementPRationaleAcceptanceDoc
NFR-010Zero loss of finalized data under crash/kill/reload/process eviction (INV-1); drafts lose ≤ one autosaveMs window.MInterviews cannot be repeatedChaos suite kills at random points during entry, finalize, sync, migration; counts and checksums preserved over 1,000 runs.09
NFR-011Idempotent sync: any operation retried without duplicates (Idempotency-Key, submissionId, tus offsets).MDropped responsesDrop 30 % of responses → one copy per submission server-side.10
NFR-012Corruption detection via checksums; corrupted rows quarantined (RASD_STORAGE_CORRUPT), never skipped silently; engine deterministic across web/native/Node (same inputs → same toSubmission() and definitionHash).MFlash wear; server re-validationCorrupt-a-row test; cross-platform golden files.09
NFR-013Graceful degradation for missing adapters/features (media, encryption, BarcodeDetector); two-tab draft edit detected via clientRev with loser prompted (S).M / SHeterogeneous devicesFeature-flag matrix; two-tab Playwright test.03

5.3 Capacity, battery, bundle (NFR-020 – NFR-025)

IDRequirementPRationaleAcceptanceDoc
NFR-020Native ≥ 5,000 submissions and ≥ 20,000 attachments (≥ 2 GB) per device; web with persist() ≥ 1,000 submissions / 500 MB; datasets to 100k rows.MMulti-week offlineDevice-farm load test.09
NFR-021Headroom warning < 100 MB; new attachment capture blocked (answers still saved) < 20 MB.MAvoid quota crashSimulated estimate().09
NFR-022Battery: no continuous background wake-ups; GPS watch stops on accept/timeout (60 s); audit location trail off by default, ≥ 60 s / 50 m when on; backoff caps at 5 min.M12-hour field daysBattery Historian: < 1 % attributable drain over 1 h idle.14
NFR-023Bundle (min+gzip, excl. React): @rasd/core ≤ 45 kB (REL ≤ 10 kB); @rasd/react ≤ 90 kB (target 60); form-runner ≤ 120 kB; dnd-adapter ≤ 25 kB in the lazy builder chunk; media/locales/heavy types code-split; Arabic font subset ≤ 120 kB WOFF2 per weight.MLow-end WebViewsize-limit in CI.18
NFR-024Memory: 300-question form ≤ 150 MB JS heap on device; no leaks over 50 open/close cycles.S2 GB phonesHeap snapshot test.06
NFR-025Attachment budget 10 MB per submission (policy.submissionBudgetBytes; photo ≤ 350 KB default); single blob ≤ 25 MB on web, ≤ 100 MiB on native and at the server — the normative table is 00 §7.1; definitions retained last 3 + referenced; media cache 30 d / 500 entries.MSync on 2G/3G; hygieneFixture and GC tests.14

5.4 Support matrix (NFR-030 – NFR-032)

IDRequirementPRationaleAcceptanceDoc
NFR-030React 19 primary (@rasd/react also tested on 18.3); React Native ≥ 0.81 New Architecture only, Expo SDK ≥ 54; optional peers expo-sqlite, @op-engineering/op-sqlite, RNGH 3 / Reanimated 4 (builder/reorder only).M2026 baseline (research/08)CI matrix; example-expo builds on SDK 54 and current.18
NFR-031Android 7+ (API 24) native; Android WebView/Chrome ≥ 100; last 2 versions of Chrome/Edge/Firefox/Safari incl. iOS Home-Screen web apps; feature detection (not UA sniffing) for IndexedDB, WebCrypto Ed25519, BarcodeDetector, Background Sync, persist().MField fleet realityPlaywright Chromium/Firefox/WebKit + device farm; feature-toggle tests.18
NFR-032Node ≥ 20 for @rasd/server, ≥ 22.13 for repo tooling; runtime packages have no Node requirement.MTooling baselineengines fields.18

5.5 Security & privacy (NFR-040 – NFR-043)

IDRequirementPRationaleAcceptanceDoc
NFR-040OWASP MASVS v2.1 STORAGE/PLATFORM at L2 for mobile packages; ASVS 5.0 relevant controls for web; MASTG verification scripts shipped.MVendor questionnairesMASTG mapping with evidence per release.16
NFR-041No custom crypto (WebCrypto/SQLCipher/@noble/ed25519 only); keys never in MMKV/AsyncStorage/localStorage; supply chain: locked deps, minimumReleaseAge ≥ 24 h, SBOM, provenance, SHA-pinned actions.MP7; 2025–26 npm wormsDependency audit, lint, release checklist.18
NFR-042Privacy: zero vendor telemetry; PII never in logs, notifications, crash reports or URLs; Play Data-safety / App Store privacy-label templates provided.MUN PDPP; store policyINV-5 tests; templates in docs.16
NFR-043Vulnerability disclosure policy (90-day SLA, GHSA advisories); residual risks (rooted devices, coerced unlock, host misconfiguration) documented with securityReport() warnings.MHonest posturePolicy and register published.16

5.6 Accessibility, maintainability, compatibility (NFR-050 – NFR-054)

IDRequirementPRationaleAcceptanceDoc
NFR-050WCAG 2.2 AA for renderers, builder, docs and examples; VPAT/ACR at v1; control.minTouch 48 default (floor 24); font scale to 2.0 without clipping.MPublic-sector procurementaxe zero violations + manual AT script; snapshot matrix.13
NFR-051Coverage: @rasd/core ≥ 90 % lines/branches, renderers ≥ 80 %; every bug fix adds a regression test.MMaintainabilityCoverage gate.18
NFR-052Toolchain per spine §12: pnpm + Turborepo, TS strict + isolatedDeclarations + verbatimModuleSyntax, ESM-only, tsdown/bob, Vitest 4 + RTL + vitest-axe, Jest + RNTL 14, Playwright, Maestro, Storybook 10, ESLint 9 + Biome.MBoring standard tech (P7)Repo scaffold.18
NFR-053Semver per package; RFD rasd MAJOR.MINOR (ignore-and-preserve MINOR, converters for MAJOR); deprecations ≥ 12 months; storage migrations upgrade from any release ≤ 12 months old; public API (spine §11) additive within a major with API-report review.MLong-lived deploymentsUpgrade tests from N-3 releases; API extractor diff.04, 18
NFR-054RSP v1 versioned by path; server publishes supportedRasd, client sends Accept-Rasd; code licensing per spine §12 (Apache-2.0 core, FSL-1.1-Apache-2.0 commercial) with SPDX headers and NOTICE files incl. SQLCipher attribution.MSkew; procurement clarityContract tests both directions; license scan in CI.10, 15

6. Traceability matrix

AreaIDsPrimary docSupporting
Form definition & payloadFR-001–008, NFR-05304 · Form schema spec05, 08, 09
Element typesFR-010–01506 · Renderer (React), 07 · Renderer (native)04, 05, 14, 16
Logic & expressionsFR-020–029, NFR-002/003/01205 · Logic & expressions03, 16, 20
RenderingFR-030–040, NFR-001/003/004/02406, 0709, 11, 12, 13, 16
BuilderFR-041–049, NFR-00808 · Builder04, 05, 12, 13
Offline storageFR-050–057, NFR-006/010/012/020/021/02509 · Offline storage16
SyncFR-060–069, NFR-007/011/022/05410 · Sync protocol09, 11, 16, 17, 20
PWA & embeddingFR-070–076, NFR-03111 · PWA & embedding18, 21
Media & captureFR-080–087, NFR-022/02514 · Media & field capture03, 16
ThemingFR-090–097, NFR-023/05012 · Theming07, 13
i18n & accessibilityFR-100–106, NFR-005/006/05013 · i18n, RTL & accessibility07, 09, 18
LicensingFR-110–117, NFR-05415 · Licensing & billing17
Security & data protectionFR-120–128, NFR-040–04316 · Security & data protection07, 09, 10, 18, 20
InteroperabilityFR-130–135, NFR-00820 · Interoperability10
ObservabilityFR-140–14317 · API reference03, 16, 18, 21
Developer experienceFR-150–155, NFR-023/030–032/041/051–05318 · Engineering practices04, 17, 21
Cross-cutting architectureFR-025/028/086/140, NFR-01303 · Architectureall
Phasing of S/C itemsall S/C rows19 · Roadmap & work breakdown

7. Release-gate checklist (v1)

  • INV-1..INV-5 tests green on Dexie and SQLite, web and native.
  • Every M requirement links to a passing automated test or a signed manual verification record.
  • NFR-001..006 met on the reference device (nightly) and desktop (per PR).
  • size-limit passes for every package and chunk (NFR-023, FR-074).
  • Storybook matrix (types × en/ar/en-XB × light/dark/high-contrast × web/native) has zero axe violations (FR-010, FR-104).
  • Chaos suite: zero finalized-data loss over 1,000 randomized kills (NFR-010).
  • License matrix (6 states × export/sync/draft-finish/new-submission) matches FR-113 (INV-3).
  • XLSForm corpus: ≥ 90 % imports clean, ≥ 95 % expressions parse unchanged (FR-130, FR-022).
  • MASTG scripts pass; securityReport() defaults match FR-120; SBOM + provenance attached (NFR-040/041).
  • Getting-started completes in < 15 min in a usability session; API report reviewed (FR-151, NFR-053).

How an invariant test reads with @rasd/testing:

import { renderForm, fakeStorage, fakeClock, faultyNetwork } from '@rasd/testing';
import pdm from '../examples/pdm-food-distribution.form.json'; // form id "pdm-gfd-2026"

test('INV-1: finalized submission survives a kill during sync', async () => {
const storage = fakeStorage({ kind: 'dexie' });
const network = faultyNetwork({ dropResponses: 0.5 });
const { engine, sync, kill, restart } = await renderForm(pdm, { storage, network, clock: fakeClock() });
engine.setValue('consent', 'yes');
await engine.finalize();
const before = await storage.outbox.size();
sync.syncNow();
await kill(); // simulate process death mid-request
await restart();
const synced = await storage.submissions.count({ status: 'synced' });
expect((await storage.outbox.size()) + synced).toBe(before);
});

Open questions

  • Is a Galaxy A10-class device (2 GB RAM, Android 9) the right performance floor, or should budgets target Android 7 / 1.5 GB devices still common in some operations?
  • Should the native print/summary view (FR-034) produce a real PDF in v1, or is a host share callback sufficient?
  • Records/cases (FR-068, FR-134) are phase 2 in the spine; do launch customers need read-only case lookup in v1?
  • Option A vs B licensing (spine §9): FR-113/FR-115 are written so either is configuration; the founder decision is open.
  • Trial length: research/10 notes 7 days is the shortest in the peer set; confirm the 7-day + optional 30-day extension before GA.
  • Which public XLSForm corpus (REACH/IOM/UNHCR/HDX) is licensed for use as the CI fidelity corpus (FR-130)?
  • VPAT/ACR at v1 (NFR-050) or v1.1?