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
autosaveMswindow (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
| Item | Rule |
|---|---|
| IDs | FR-nnn functional, NFR-nnn non-functional, INV-n invariant; ranges per area (§6) with intentional gaps. |
| MoSCoW | M Must (v1, blocking) · S Should (v1 target, may slip one minor) · C Could (phase 2–3) · W Won't (not in v1). |
| Normativity | Names, 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 device | 2 GB-RAM Android 7–9 phone, Chrome/WebView ≥ 100 (Galaxy A10 class); 3G throttle for network tests; p95 over 20 runs. |
| Actors | Enumerator (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.
| ID | Invariant | Verification |
|---|---|---|
| INV-1 | Finalized 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-2 | Irrelevant 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-3 | License state never blocks export, sync or finishing drafts — export(), syncNow(), draft completion and reads work in all six license states. | Matrix: 6 states × export/sync/draft-finish succeed. |
| INV-4 | The 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-5 | No 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)
| ID | Requirement | P | Rationale | Acceptance | Doc |
|---|---|---|---|---|---|
| FR-001 | One RFD JSON document (layout, logic, translations, theme hints, custom payload) validated by schema/rasd-form.schema.json via validateFormDefinition(). | M | P2: form is data | Negative fixtures fail RASD_SCHEMA_INVALID; all examples/*.form.json pass. | 04 |
| FR-002 | Every object node accepts ext: { [vendorKey]: unknown }, validated as an object and round-tripped unchanged through engine, builder, storage, sync, XLSForm export. | M | P4 | Deep-equal round-trip with ext at every level. | 04 |
| FR-003 | Custom types x:<name> via defineElement(); unregistered x: types render a placeholder, never crash. | M | Host widgets | Unknown type renders with warning, no console error. | 06 |
| FR-004 | Localized strings string | { [bcp47]: string } everywhere, restricted to the Rasd Mini-Message subset ({var}, plural, select, #, '). | M | Multi-language norm | Out-of-subset syntax fails validation; Arabic six-category plurals render. | 13 |
| FR-005 | version monotonic per id; published versions immutable; definitionHash = SHA-256 of canonical JSON. | M | Reproducibility | Hash stable across key order; server rejects re-publish. | 04 |
| FR-006 | diffDefinitions() 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. | M | Central-style safety (research/12) | 21 taxonomy rows have fixtures; migrating a finalized submission throws. | 04, 08 |
| FR-007 | Within an RFD MAJOR: unknown properties ignored-and-preserved; unknown element type → placeholder + degraded; unknown REL function → refuse to open. | M | Version skew | rasd:"1.9" fixture loads on 1.0 client, re-saves byte-identical. | 04 |
| FR-008 | Preload ${meta.*} (deviceId, userId, username, startedAt, now, locale, formVersion, appVersion, platform, submissionId, custom.*) in REL and Submission.meta; settings.instanceName computed on finalize. | M | ODK preload parity | Each key resolves in a calculate; instanceName shown in lists. | 05 |
4.2 Element types (FR-010 – FR-015)
| ID | Requirement | P | Rationale | Acceptance | Doc |
|---|---|---|---|---|---|
| FR-010 | All spine §4.3 element types with their props on both renderers (text … repeat, incl. consent, matrix, geo, media, barcode, signature, note, hidden, calculate, group). | M | XLSForm vocabulary (research/01 §4) | Storybook matrix type × {en, ar} × {light, dark} × {web, native} passes axe. | 06, 07 |
| FR-011 | Value shapes exactly per spine (select_multiple → string[], geopoint/consent objects, media → attachmentRef), enforced by zod. | M | Server contract | toSubmission() validates per type. | 04 |
| FR-012 | Selects: list/inline choices[], choiceFilter, search, other, randomize, minSelected/maxSelected, exclusive. | M | Cascades, "other" | Governorate→district cascade works; exclusive:["none"] clears others. | 06 |
| FR-013 | repeat (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. | M | Rosters; ODK semantics | count 5→3→5 restores rows 4–5; cross-group move is compatible. | 05 |
| FR-014 | consent stores text version, locale, timestamp, method, withdrawal; bind.sensitive/saveIncomplete/trackChanges/index drive encryption, redaction, audit, indexes. | M | Data protection by design | Withdrawal audited without deleting original; sensitive fields field-encrypted, hidden from "sent" lists. | 16 |
| FR-015 | validators[] (regex, range, length, expr, custom) with severity: error | warning | info; warnings never block finalize and are audited; per-locale media with auto-play audio. | M | Soft checks; low literacy | Warning allows finalize + audit event; Arabic audio plays offline. | 05, 14 |
4.3 Logic (FR-020 – FR-029)
| ID | Requirement | P | Rationale | Acceptance | Doc |
|---|---|---|---|---|---|
| FR-020 | REL v1: Pratt-parsed, no eval/new Function, static dependency extraction; sandbox AST ≤ 5k nodes, ≤ 10 ms per evaluation with abort, prototype-safe access. | M | CSP/RN safe; hostile authors | 10k-case fuzz never throws uncaught; CPU-bomb aborts with RASD_EXPR_BUDGET. | 05 |
| FR-021 | References ${name}, ${../name}, ${/root}, ${rep[2].f}, ${rep[].f}, ${meta.*} per spine §5. | M | Repeats | 3-level nested repeat resolves all ref kinds. | 05 |
| FR-022 | Full 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}). | M | Real XLSForms parse unchanged | ≥ 95 % of corpus expressions parse; conformance table vs @getodk/xpath. | 05 |
| FR-023 | relevant: hidden, not validated, required off, value kept in draft, excluded on finalize (INV-2); cascades from group/page. | M | ODK bind semantics | Hide→show restores value; finalize omits hidden. | 05 |
| FR-024 | required (bool/REL) enforced only when relevant; constraint evaluated only when non-empty, on change and finalize; localized messages. | M | Constraint ignores empty | [] fails required, 0 passes; empty value skips constraint. | 05 |
| FR-025 | calculate recomputed in topological order; cycles fail at load; once()/default.expr evaluate once; recomputation batched per microtask, memoised. | M | Deterministic derived values | Cycle fixture fails validation; once(uuid()) stable across reload; only affected fields re-render. | 05 |
| FR-026 | Triggers logic.triggers[] (when → ordered actions[]: complete, setValue, jump, message) evaluated after each change batch. | M | Early-exit consent flows | ${consent}='no' completes with localized message + audit. | 05 |
| FR-027 | Cascading selects via choiceFilter and dataset lists (filterKeys), equality filters pushed to storage ≥ 1k rows; pulldata()/datasets.query() offline; dataset answers snapshot {value,label}. | M | 10k admin units | 10k-row filter < 50 ms/keystroke on device; label survives dataset update. | 05, 09 |
| FR-028 | Headless createFormEngine() (getState, setValue, addRepeat, removeRepeat, validate, finalize, subscribe, toSubmission) with no React/DOM/RN imports. | M | P3 | Node CLI finalizes a fixture. | 03 |
| FR-029 | Optional XPath-coercion mode per imported form (empty→NaN, string booleans, anchored regex). | S | Fidelity traps (research/14 §8) | Flagged form evaluates regex() anchored like JavaRosa. | 20 |
4.4 Rendering (FR-030 – FR-040)
| ID | Requirement | P | Rationale | Acceptance | Doc |
|---|---|---|---|---|---|
| FR-030 | settings.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. | M | Ergonomics; WCAG 3.3.x | Both modes render; summary click focuses field; axe passes. | 06 |
| FR-031 | Autosave every change ≤ settings.autosaveMs (default 2000) and immediately on page change, blur, visibilitychange:hidden, background, finalize. | M | Never lose data | Kill loses ≤ one window; page navigation loses nothing. | 06, 09 |
| FR-032 | Drafts (allowDrafts): list, resume at last page/scroll, delete with confirmation; pinned to formVersion + definitionHash and opened with that version. | M | Collect behaviour | Draft on v2 opens with v2 after v3 installs. | 06 |
| FR-033 | Runtime 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. | M | Arabic-first (P5) | en↔ar switch keeps values and focus; bidi checklist (research/13 §5) as en/ar/en-XB snapshots. | 13 |
| FR-034 | readOnly 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 / S | Supervisor review; receipts | Zero writable controls and storage writes; summary omits irrelevant/redacted. | 06, 07 |
| FR-035 | Identical public API on @rasd/react and @rasd/native (<RasdProvider>, <FormRenderer>, hooks, defineElement, registry). | M | One mental model | Shared behavioural suite green on both. | 07 |
| FR-036 | Repeat UI: add/remove/reorder, confirmDelete, collapsed headers from itemLabel, virtualised above 30 items. | M | 200-row rosters | See NFR-004; delete confirmation audited. | 06 |
| FR-037 | Progress (showProgress) over relevant pages only; big-touch defaults via rasd-field (48 px). | M | One-hand use | Progress = relevant index / relevant pages. | 06 |
| FR-038 | Audit 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. | M | TPM evidence; ODK audit.csv | Export matches ODK event vocabulary. | 16 |
| FR-039 | Rich text sanitised: DOMPurify allow-list (web), markdown-to-native, no HTML (RN); media URLs limited to mediaAllowList; violations → onPolicyViolation. | M | XSS from hostile definitions | mXSS corpus inert; off-list/javascript: URLs dropped. | 16 |
| FR-040 | useRasdBusy() / isDirty() so hosts defer SW updates and navigation while a draft is dirty. | M | No mid-entry reload | Recipe test defers SW waiting while dirty. | 11 |
4.5 Builder (FR-041 – FR-049)
| ID | Requirement | P | Rationale | Acceptance | Doc |
|---|---|---|---|---|---|
| FR-041 | Three-pane shell (palette · canvas · inspector), tabs Designer/Logic/Translations/Preview/JSON/Versions gated by features; separate lazy chunk never loaded by field runners. | M | Consensus UX (research/03 §4) | features={{logic:false}} hides Logic; size-limit proves runner excludes builder. | 08 |
| FR-042 | DnD 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). | M | Accessibility; phones | Every reorder achievable pointer-only and keyboard-only; axe passes. | 08 |
| FR-043 | Inspector 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". | M | Full RFD coverage | Every schema property reachable; invalid JSON blocked with location. | 08 |
| FR-044 | Logic 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. | M | Kobo/Formbricks pattern | GUI rule round-trips to identical REL; parse error < 100 ms. | 08 |
| FR-045 | Translations 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. | M | ar/en/fr forms | Missing Arabic few flagged; export/import round-trips all cells. | 08 |
| FR-046 | Preview uses the real <FormRenderer> with device frame, RTL toggle, locale switch, offline simulation; preview data never enqueued. | M | Verify logic | Preview finalize creates no outbox row. | 08 |
| FR-047 | Versions/publish: onPublish with suggested next version, semantic changelog from diffDefinitions, breaking-change block/force, "changes since v(n-1)" diff. | M | Central-style safety | Publish disabled on reused version or unchanged hash. | 08 |
| FR-048 | Undo/redo via immer patches, keystroke coalescing ≤ 500 ms, ≥ 100 steps, last batches persisted for crash recovery. | M | Editor baseline | Ctrl/Cmd+Z restores; reload after crash offers recovery. | 08 |
| FR-049 | Plugins via defineElement({type:'x:foo', component, builder:{icon,label,inspector}, valueSchema}); question/section library of fragments (S); theme editor panel (C). | M / S / C | P4; standard modules | Plugin in palette + inspector; fragment drop auto-suffixes clashing names. | 08 |
4.6 Offline storage (FR-050 – FR-057)
| ID | Requirement | P | Rationale | Acceptance | Doc |
|---|---|---|---|---|---|
| FR-050 | StorageAdapter (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. | M | P1 | Conformance suite passes on all three; two-tab upgrade loses nothing. | 09 |
| FR-051 | Attachments as unindexed Blobs (web) / app-private files (native), content-hash addressed, sizeOf/listByStatus. | M | Photos are the bytes | 200 × 300 KB photos round-trip with matching sha256. | 09 |
| FR-052 | Quota: estimate(); web calls navigator.storage.persist() after first sync/install; warn < 100 MB headroom; RASD_STORAGE_QUOTA surfaced as recoverable "cannot save offline". | M | Safari eviction (research/04 §1.3) | Simulated quota error keeps in-memory draft. | 09 |
| FR-053 | Encryption 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. | M | UN security reviews | securityReport() shows status; on-disk payload not plaintext-searchable. | 16 |
| FR-054 | export() streams JSONL + blobs offline in every license state (INV-3), optionally encrypted to recipient keys; re-import lossless. | M | P6 | 1,000-submission export offline; re-imports. | 09 |
| FR-055 | Migrations: ordered, checksum-tracked; schema steps in engine transaction; data steps chunked (500 rows), resumable via _rasd_migrations. | M | Crash mid-migration | Kill mid-migration → resume completes; checksum tampering refuses. | 09 |
| FR-056 | Retention: keep versions referenced by drafts/outbox plus last 3; purge finalized submissions and attachments after ack (default on, metadata kept). | M | Storage; privacy | After ack get(id) returns metadata only; referenced versions never GC'd. | 09 |
| FR-057 | wipe() crypto-shreds keys, deletes DB/attachments; with unsynced data requires two-step host confirmation showing the count. | M | Handover; remote wipe | onLocalWipeRequested({unsyncedCount}) fires before deletion. | 16 |
4.7 Sync (FR-060 – FR-069)
| ID | Requirement | P | Rationale | Acceptance | Doc |
|---|---|---|---|---|---|
| FR-060 | Outbox: submission and outbox op written in one local transaction on finalize; FIFO per form; peek/ack/fail with nextAttemptAt. | M | INV-1 | Single-transaction test. | 10 |
| FR-061 | RSP 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(). | M | Idempotent retries | Replaying an acked batch yields duplicate; state-machine test forbids other transitions. | 10 |
| FR-062 | Attachments via tus 1.0.0 (POST/PATCH/HEAD, Upload-Metadata = submissionId, field, sha256), resumable across restarts, hash echoed. | M | 2G/3G uploads | Kill at 40 % → resume from offset; mismatch → back to queued. | 10 |
| FR-063 | Pull 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 / S | Offline-first updates; recovery | Tampered definition refused; re-sync leaves outbox intact. | 10 |
| FR-064 | Foreground-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. | M | Safari/Firefox lack Background Sync (research/05) | Two tabs: one syncs, both get events; Firefox/WebKit sync on online. | 10 |
| FR-065 | Backoff 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. | M | Captive Wi-Fi, flaky 3G | 5 × 503 → delays in [1, 300] s then success. | 10 |
| FR-066 | useSync() → last sync time, pending counts per state, per-attachment progress, current error; syncNow(), pause(), resume(); events progress, error, conflict, formUpdated, datasetUpdated, licenseRefreshed. | M | Enumerator confidence | Counts update ≤ 200 ms after state change. | 10 |
| FR-067 | Auth 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 / S | Host owns identity; fleet policy | http:// in production throws; wipe flag → onRemoteWipe after nonce check. | 10 |
| FR-068 | Records/cases: per-field LWW by HLC; true collisions → conflict in a supervisor queue with both values; enumerators never see merge dialogs. | S | Longitudinal (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. | M | formpack 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)
| ID | Requirement | P | Rationale | Acceptance | Doc |
|---|---|---|---|---|---|
| FR-070 | npm ESM packages with 'use client' boundaries for Vite, Next.js App Router (ssr:false builder), Expo web. | M | Primary distribution | playground-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. | M | Non-React hosts | Plain HTML + IIFE fills and finalizes offline; picker styled inside shadow root. | 11 |
| FR-072 | SW 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). | M | OneSignal/Firebase pattern | Vite, Next+Serwist, Expo web recipes tested; airplane-mode form renders with images and Arabic font. | 11 |
| FR-073 | useInstallPrompt() (Chromium prompt, iOS Add-to-Home-Screen guidance) and useServiceWorkerUpdate() (waiting → prompt → skipWaiting → reload) deferring while isDirty(). | M | Escape Safari 7-day purge | Update never reloads while dirty. | 11 |
| FR-074 | Strict-CSP compatible (no eval, constructed stylesheets/<link>, self-hosted fonts/WASM); every chunk < 2 MB uncompressed. | M | Agency IT policies | Runs under script-src 'self'; style-src 'self'; per-chunk size check. | 11 |
| FR-075 | Diagnostics (estimate(), persisted, standalone, queue length, SW state) and rasd doctor offline-readiness CLI. | S | Replaces "Lighthouse PWA score" | Doctor flags missing persist(), icons, non-https. | 11 |
| FR-076 | Web Push with Declarative Web Push payloads (C); hosted iframe embed (W). | C / W | iOS 18.4+; partitioning | — | 11 |
4.9 Media & field capture (FR-080 – FR-087)
| ID | Requirement | P | Rationale | Acceptance | Doc |
|---|---|---|---|---|---|
| FR-080 | GPS: 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}. | M | Collect UX (research/09 §4.1) | Fake 30 m fix warns and allows accept; timeout tested. | 14 |
| FR-081 | Geotrace/geoshape manual, tap-to-place, automatic (intervalSeconds); distance()/area(); offline basemaps via MapLibre + PMTiles (optional module). | S | Site mapping | Auto mode filters by accuracy; shape closes; area computed. | 14 |
| FR-082 | Photo: 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. | M | Bandwidth; privacy | 12 MP capture ≤ 350 KB; no GPS EXIF; sidecar present. | 14 |
| FR-083 | Barcode/QR: feature-detect BarcodeDetector, fall back to barcode-detector ponyfill with self-hosted WASM; native expo-camera; formats, allowManual. | M | ~76 % availability | Scans offline in Firefox/Safari; manual entry available. | 14 |
| FR-084 | Signature: canvas pad (web), Skia/native pad (RN, no WebView), trimmed PNG ≤ 30 KB, penColor, clear/redo. | M | Consent, receipts | Keyboard-accessible clear; RTL-safe. | 14 |
| FR-085 | Audio 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 / M | Store policy; sync feasibility | 1-min recording ≤ 250 KB; over-budget file rejected with localized message. | 14 |
| FR-086 | Capture adapters behind @rasd/media interfaces, code-split per capability, fakes in @rasd/testing; missing adapter → "capture unavailable", never crash. | M | P3; budget | Renderer without media adapters still renders media questions. | 14 |
| FR-087 | Permission 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. | M | Play/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)
| ID | Requirement | P | Rationale | Acceptance | Doc |
|---|---|---|---|---|---|
| FR-090 | Theme 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. | M | Non-developer branding | Theme extending rasd-field validates and renders inside a rasd-light host. | 12 |
| FR-091 | Web: --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. | M | Two agency themes per page; host CSS wins | Two themed forms coexist; unlayered host rule wins without !important. | 12 |
| FR-092 | Native: same tokens via useTheme() → plain StyleSheet; per-part styles; allowFontScaling with typography.maxFontScale 2.0. | M | No styling-engine dependency | Snapshot matrix font scale 1.0/1.3/2.0 × ltr/rtl × light/dark/high-contrast. | 12 |
| FR-093 | Per-part classNames/styles/render, unstyled, components registry, per-type renderers; overrides honour the accessible-props contract (label id, described-by, error state). | M | Design-system integration | Replacing TextField passes the contract test. | 12 |
| FR-094 | Modes 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. | M | Outdoor readability; a11y | prefers-color-scheme: dark selects dark; rasd theme check passes all four; reduced motion → 0 ms. | 12 |
| FR-095 | Fonts as offline theme assets (WOFF2 precached, static TTF native), default OFL Arabic family, fontFamilyRtl; subsets keep mark/mkmk/rlig and bidi controls. | M | Arabic shaping offline | Airplane-mode Arabic label uses bundled font. | 12 |
| FR-096 | fromDtcg()/toDtcg() (DTCG 2025.10) and rasd theme check (schema + WCAG contrast lint). | S | Figma/Style-Dictionary pipelines | Default theme round-trips DTCG losslessly. | 12 |
| FR-097 | UN-agency brand-pack sample themes with brand accent separated from text tokens. | C | Adoption accelerator | Samples pass rasd theme check. | 12 |
4.11 i18n & accessibility (FR-100 – FR-106)
| ID | Requirement | P | Rationale | Acceptance | Doc |
|---|---|---|---|---|---|
| FR-100 | Core 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. | M | Hermes gaps (research/13 §1) | Hermes-like Intl shim passes ar/fa/uk/my plural tests. | 13 |
| FR-101 | Locale negotiation: lookup with truncation + macro-language best-fit; explicit non-fallbacks (ckb≠ku); per-string fallback chain shown as "showing default" with dir="auto". | M | No blank labels | ar-JO → ar → defaultLocale; fallback text dir="auto". | 13 |
| FR-102 | settings.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. | M | Digits are per-region | Typing ٣٤ stores 34; Hijri displays without Intl support. | 13 |
| FR-103 | Chrome 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. | S | Differentiator | en, ar, fr complete at v1, others ≥ 80 %; missing key warns once. | 13 |
| FR-104 | WCAG 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. | M | Legal/ethical baseline | axe zero violations; manual VoiceOver/TalkBack/NVDA script per release. | 13 |
| FR-105 | RN a11y: accessibilityLabel/LabelledBy/Hint/State/Role, announceForAccessibility for errors, reduce-motion/high-contrast signals honoured. | M | TalkBack on field devices | RNTL a11y assertions per element type. | 07 |
| FR-106 | Arabic-aware search/sort: Intl.Collator(base, ignorePunctuation, numeric) + normalizer (NFKC, tashkeel strip, alef/yaa/taa-marbuta folding) stored as label_norm at import. | M | 10k Arabic lists | "امان" matches "أمّان"; sort stable across engines. | 13 |
4.12 Licensing (FR-110 – FR-117)
| ID | Requirement | P | Rationale | Acceptance | Doc |
|---|---|---|---|---|---|
| FR-110 | RLT = 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). | M | Zero phone-home | Unknown key → invalid; expired valid signature → grace/limited. | 15 |
| FR-111 | 7-day trial: signup token (one per org domain) or zero-config first-run local trial; evaluating indefinitely on dev origins. | M | Adoption | localhost renders without watermark; production without token → trial 7 d then limited. | 15 |
| FR-112 | Monthly 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 tolerance | Fake clock: 85 days offline stays functional; day 91 → limited. | 15 |
| FR-113 | limited + 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). | M | Never lose field data | State × action matrix; watermark visible. | 15 |
| FR-114 | Clock tampering: monotonic guard on last-seen server time; rollback freezes state, never regresses it. | M | Wrong field clocks | Device time −30 d keeps state and grace. | 15 |
| FR-115 | useLicense()/getState() expose state, plan, exp, features, refresh(); gating by features[] and apps[] (origins with wildcards, bundle IDs); state persisted in kv. | M | Option A/B is configuration | Token without "builder" → builder read-only; origin mismatch → invalid. | 15 |
| FR-116 | Billing: Stripe Billing + Entitlements + Invoicing (POs, Net-30, tax-exempt) → license service issues/rotates tokens; manual "PO paid" mints 12-month tokens. | S | UN procurement | Webhook and manual paths mint valid tokens. | 15 |
| FR-117 | rasd 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 / C | Most complaints are UX | Command exits non-zero on limited. | 15 |
4.13 Security & data protection (FR-120 – FR-128)
| ID | Requirement | P | Rationale | Acceptance | Doc |
|---|---|---|---|---|---|
| FR-120 | SecurityPolicy 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(). | M | MASVS L2 | Report lists active controls; defaults match doc table. | 16 |
| FR-121 | Definition hardening: ≤ 2 MB, strings ≤ 64 KB, inline lists ≤ 10k, datasets ≤ 100k rows (host-configurable); __proto__/constructor/prototype keys rejected. | M | Hostile server/author | Oversized/polluting fixtures fail with RASD_SCHEMA_INVALID. | 16 |
| FR-122 | Expo config plugin: Android dataExtractionRules/allowBackup excluding DB + attachments (merged with expo-secure-store rules); iOS isExcludedFromBackup on library dirs. | M | Backup leakage (T8) | Emulator backup/restore leaves no DB; iOS attribute dump shows exclusion. | 16 |
| FR-123 | Redacting logger (answers, entity rows, tokens, tus URLs never logged), createSentryScrubber(); sensitive-field hardening (importantForAutofill="no", autoComplete="off", autoCorrect off, contextMenuHidden, optional screen-capture prevention). | M | Side channels (T9); MASTG | Debug output contains no fixture answers; static prop check. | 16 |
| FR-124 | settings.encryption.mode:"submission": per-submission AES-256-GCM key wrapped to publicKeyId; ODK-compatible envelope for Central managed encryption. | S | Sensitive protection data | Server cannot read payload; ODK envelope decrypts with Central passphrase. | 16 |
| FR-125 | App lock (onLock/onUnlockRequired, key-bound biometrics via SecureStore requireAuthentication), enumerator profiles (logout() hides, never deletes unsynced), supervisor admin PIN with audit. | S | Shared devices (T2) | Idle 5 min → onLock('idle'); other profile's drafts hidden yet synced. | 16 |
| FR-126 | Remote wipe on validated policy nonce → crypto-shred + delete + onRemoteWipe({reason, unsyncedCount}); web Clear-Site-Data guidance. | S | Lost/stolen device | No readable payload after wipe; unsynced count reported. | 16 |
| FR-127 | Integrity: SHA-256 on submissions/attachments echoed by server; optional JWS-signed definitions. | M | Tamper evidence | Ack hash mismatch → back to queued. | 10 |
| FR-128 | Vendor pack: MASVS/ASVS mapping with MASTG evidence, CycloneDX SBOM, provenance, DPIA template, residual-risk register; form-level legalBasis + sensitivity data-inventory export (C). | S / C | UN vendor questionnaires | Pack published per minor release. | 16 |
4.14 Interoperability (FR-130 – FR-135)
| ID | Requirement | P | Rationale | Acceptance | Doc |
|---|---|---|---|---|---|
| FR-130 | XLSForm → 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. | M | Adoption (research/01 §5) | ≥ 90 % of a 140-form corpus imports with zero errors. | 20 |
| FR-131 | RFD → XLSForm export that pyxform 4.x compiles; CI round-trip compares canonical XForm XML. | M | Kobo/ODK back-ends | Import→export→pyxform succeeds; diff report published. | 20 |
| FR-132 | XForm 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. | S | Kobo / Central / MoDa | Instances validate against pyxform XForms; contract tests per profile. | 20 |
| FR-133 | Server 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 / C | Power BI templates | 3-version form yields union columns with per-row version. | 20 |
| FR-134 | ODK-style entity lists (name, label, __version) as datasets with create_if/update_if → records; optional PowerSync/Electric/CouchDB transports. | C | Longitudinal interop | — | 20 |
| FR-135 | Import of SurveyJS/Form.io JSON. | W | Different market | — | — |
4.15 Observability (FR-140 – FR-143)
| ID | Requirement | P | Rationale | Acceptance | Doc |
|---|---|---|---|---|---|
| FR-140 | Every engine exposes on(event, handler)/subscribe() returning an unsubscribe; renderer onChange/onSave/onFinalize; hosts derive metrics from events. | M | Host-owned metrics | Event contract tests; unsubscribe stops delivery. | 17 |
| FR-141 | Structured redacting logger with levels and pluggable sink; default warn; debug stripped in production; no vendor telemetry (INV-5). | M | Diagnose without PII | Sink receives structured objects; network capture clean. | 16 |
| FR-142 | RasdError 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. | M | Programmatic handling | Every thrown error in tests is a RasdError with a documented code. | 17 |
| FR-143 | securityReport() and rasd doctor emit machine-readable, PII-free diagnostics. | S | Support tickets | Output validates against its JSON schema. | 21 |
4.16 Developer experience (FR-150 – FR-155)
| ID | Requirement | P | Rationale | Acceptance | Doc |
|---|---|---|---|---|---|
| FR-150 | Strict TypeScript types for RFD, Submission, Theme, engine, hooks; isolatedDeclarations; public API report per PR; rasd types generates a form's data type. | M | Developer adoption | tsc --strict on examples; API diff blocks unintended breaks; generated type matches toSubmission(). | 18 |
| FR-151 | Docs: 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. | M | Time-to-first-form | Usability 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. | M | Automation | CLI tests per command. | 21 |
| FR-153 | @rasd/testing: renderForm(), fake storage, fake clock, network fault injection, form fixtures, fake media adapters. | M | Hosts test their forms | Host asserts relevance/validation of its RFD in Vitest/Jest without a browser. | 18 |
| FR-154 | Example 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 / S | Living recipes | Playwright + Maestro pass per release. | 18 |
| FR-155 | Changesets + 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. | M | Supply chain; long-lived forms | Every 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.
| ID | Requirement | P | Rationale | Acceptance | Doc |
|---|---|---|---|---|---|
| NFR-001 | Cold 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. | M | Enumerator at the door | TTI trace in CI. | 06 |
| NFR-002 | 300-question form load (parse + validate + graph + first page) ≤ 1,000 ms device; createFormEngine() ≤ 150 ms desktop. | M | Large PDM tools | Benchmark 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-003 | Value change → recompute → re-render ≤ 50 ms p95 device, ≤ 16 ms desktop in the 300-question form; only affected fields re-render. | M | Typing feels instant | React Profiler commit-count assertion. | 06 |
| NFR-004 | 200-row repeat: add row ≤ 100 ms, sum() ≤ 10 ms, 60 fps scroll (virtualised), memory growth ≤ 30 MB. | M | Household rosters | Perf test on both renderers. | 06, 07 |
| NFR-005 | 10k-choice select: filter < 50 ms per keystroke (pre-normalised index, store-side filter); first paint ≤ 100 ms. | M | Admin-unit lists | Benchmark with 10k Arabic labels. | 09 |
| NFR-006 | Autosave write ≤ 50 ms p95, never blocks input; language switch ≤ 200 ms for 300 questions. | M | No dropped keystrokes | 200 chars at 100 ms interval lose none. | 09 |
| NFR-007 | Sync: 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. | S | Field bandwidth; long deployments | Throttled Playwright project; nightly device test. | 10 |
| NFR-008 | Builder 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. | S | Designer productivity | Playwright perf trace; benchmark. | 08 |
5.2 Reliability & data safety (NFR-010 – NFR-013)
| ID | Requirement | P | Rationale | Acceptance | Doc |
|---|---|---|---|---|---|
| NFR-010 | Zero loss of finalized data under crash/kill/reload/process eviction (INV-1); drafts lose ≤ one autosaveMs window. | M | Interviews cannot be repeated | Chaos suite kills at random points during entry, finalize, sync, migration; counts and checksums preserved over 1,000 runs. | 09 |
| NFR-011 | Idempotent sync: any operation retried without duplicates (Idempotency-Key, submissionId, tus offsets). | M | Dropped responses | Drop 30 % of responses → one copy per submission server-side. | 10 |
| NFR-012 | Corruption detection via checksums; corrupted rows quarantined (RASD_STORAGE_CORRUPT), never skipped silently; engine deterministic across web/native/Node (same inputs → same toSubmission() and definitionHash). | M | Flash wear; server re-validation | Corrupt-a-row test; cross-platform golden files. | 09 |
| NFR-013 | Graceful degradation for missing adapters/features (media, encryption, BarcodeDetector); two-tab draft edit detected via clientRev with loser prompted (S). | M / S | Heterogeneous devices | Feature-flag matrix; two-tab Playwright test. | 03 |
5.3 Capacity, battery, bundle (NFR-020 – NFR-025)
| ID | Requirement | P | Rationale | Acceptance | Doc |
|---|---|---|---|---|---|
| NFR-020 | Native ≥ 5,000 submissions and ≥ 20,000 attachments (≥ 2 GB) per device; web with persist() ≥ 1,000 submissions / 500 MB; datasets to 100k rows. | M | Multi-week offline | Device-farm load test. | 09 |
| NFR-021 | Headroom warning < 100 MB; new attachment capture blocked (answers still saved) < 20 MB. | M | Avoid quota crash | Simulated estimate(). | 09 |
| NFR-022 | Battery: 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. | M | 12-hour field days | Battery Historian: < 1 % attributable drain over 1 h idle. | 14 |
| NFR-023 | Bundle (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. | M | Low-end WebView | size-limit in CI. | 18 |
| NFR-024 | Memory: 300-question form ≤ 150 MB JS heap on device; no leaks over 50 open/close cycles. | S | 2 GB phones | Heap snapshot test. | 06 |
| NFR-025 | Attachment 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. | M | Sync on 2G/3G; hygiene | Fixture and GC tests. | 14 |
5.4 Support matrix (NFR-030 – NFR-032)
| ID | Requirement | P | Rationale | Acceptance | Doc |
|---|---|---|---|---|---|
| NFR-030 | React 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). | M | 2026 baseline (research/08) | CI matrix; example-expo builds on SDK 54 and current. | 18 |
| NFR-031 | Android 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(). | M | Field fleet reality | Playwright Chromium/Firefox/WebKit + device farm; feature-toggle tests. | 18 |
| NFR-032 | Node ≥ 20 for @rasd/server, ≥ 22.13 for repo tooling; runtime packages have no Node requirement. | M | Tooling baseline | engines fields. | 18 |
5.5 Security & privacy (NFR-040 – NFR-043)
| ID | Requirement | P | Rationale | Acceptance | Doc |
|---|---|---|---|---|---|
| NFR-040 | OWASP MASVS v2.1 STORAGE/PLATFORM at L2 for mobile packages; ASVS 5.0 relevant controls for web; MASTG verification scripts shipped. | M | Vendor questionnaires | MASTG mapping with evidence per release. | 16 |
| NFR-041 | No 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. | M | P7; 2025–26 npm worms | Dependency audit, lint, release checklist. | 18 |
| NFR-042 | Privacy: zero vendor telemetry; PII never in logs, notifications, crash reports or URLs; Play Data-safety / App Store privacy-label templates provided. | M | UN PDPP; store policy | INV-5 tests; templates in docs. | 16 |
| NFR-043 | Vulnerability disclosure policy (90-day SLA, GHSA advisories); residual risks (rooted devices, coerced unlock, host misconfiguration) documented with securityReport() warnings. | M | Honest posture | Policy and register published. | 16 |
5.6 Accessibility, maintainability, compatibility (NFR-050 – NFR-054)
| ID | Requirement | P | Rationale | Acceptance | Doc |
|---|---|---|---|---|---|
| NFR-050 | WCAG 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. | M | Public-sector procurement | axe zero violations + manual AT script; snapshot matrix. | 13 |
| NFR-051 | Coverage: @rasd/core ≥ 90 % lines/branches, renderers ≥ 80 %; every bug fix adds a regression test. | M | Maintainability | Coverage gate. | 18 |
| NFR-052 | Toolchain 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. | M | Boring standard tech (P7) | Repo scaffold. | 18 |
| NFR-053 | Semver 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. | M | Long-lived deployments | Upgrade tests from N-3 releases; API extractor diff. | 04, 18 |
| NFR-054 | RSP 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. | M | Skew; procurement clarity | Contract tests both directions; license scan in CI. | 10, 15 |
6. Traceability matrix
| Area | IDs | Primary doc | Supporting |
|---|---|---|---|
| Form definition & payload | FR-001–008, NFR-053 | 04 · Form schema spec | 05, 08, 09 |
| Element types | FR-010–015 | 06 · Renderer (React), 07 · Renderer (native) | 04, 05, 14, 16 |
| Logic & expressions | FR-020–029, NFR-002/003/012 | 05 · Logic & expressions | 03, 16, 20 |
| Rendering | FR-030–040, NFR-001/003/004/024 | 06, 07 | 09, 11, 12, 13, 16 |
| Builder | FR-041–049, NFR-008 | 08 · Builder | 04, 05, 12, 13 |
| Offline storage | FR-050–057, NFR-006/010/012/020/021/025 | 09 · Offline storage | 16 |
| Sync | FR-060–069, NFR-007/011/022/054 | 10 · Sync protocol | 09, 11, 16, 17, 20 |
| PWA & embedding | FR-070–076, NFR-031 | 11 · PWA & embedding | 18, 21 |
| Media & capture | FR-080–087, NFR-022/025 | 14 · Media & field capture | 03, 16 |
| Theming | FR-090–097, NFR-023/050 | 12 · Theming | 07, 13 |
| i18n & accessibility | FR-100–106, NFR-005/006/050 | 13 · i18n, RTL & accessibility | 07, 09, 18 |
| Licensing | FR-110–117, NFR-054 | 15 · Licensing & billing | 17 |
| Security & data protection | FR-120–128, NFR-040–043 | 16 · Security & data protection | 07, 09, 10, 18, 20 |
| Interoperability | FR-130–135, NFR-008 | 20 · Interoperability | 10 |
| Observability | FR-140–143 | 17 · API reference | 03, 16, 18, 21 |
| Developer experience | FR-150–155, NFR-023/030–032/041/051–053 | 18 · Engineering practices | 04, 17, 21 |
| Cross-cutting architecture | FR-025/028/086/140, NFR-013 | 03 · Architecture | all |
| Phasing of S/C items | all S/C rows | 19 · 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-limitpasses 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?
Related documents
- 00 · Decisions & conventions · 01 · Vision & scope · 03 · Architecture
- 04 · Form schema spec · 05 · Logic & expressions · 06 · Renderer (React) · 07 · Renderer (native) · 08 · Builder
- 09 · Offline storage · 10 · Sync protocol · 11 · PWA & embedding · 12 · Theming · 13 · i18n, RTL & accessibility · 14 · Media & field capture
- 15 · Licensing & billing · 16 · Security & data protection · 17 · API reference · 18 · Engineering practices · 19 · Roadmap & work breakdown · 20 · Interoperability · 21 · Getting started
- Research: 01 · 03 · 04 · 05 · 06 · 07 · 08 · 09 · 10 · 11 · 12 · 13 · 14