Skip to main content

19 · Roadmap & work breakdown

Purpose: The delivery plan for Rasd Forms — phases with goals and exit criteria, epics broken into estimated tasks with owners and dependencies, the team and budget envelope, the risk register, KPIs, the release train, the agency pilot plan and a sprint-level plan for the first 90 days.

Audience: The founding engineering team and anyone budgeting, staffing or scheduling the build; UN/NGO developers who want to know when which package becomes usable and how pilots will be run.

TL;DR

  • Five phases from 1 September 2026 to GA 1.0 on 15 December 2027: Foundations (8 weeks), MVP web (18), Native + builder (17), Scale & ecosystem (16), GA hardening (9).
  • 1,000 person-days (PD) across ~45 epics; the team grows from 2.5 to 5 FTE (frontend lead, backend/platform, React Native, second frontend for the builder, DevRel) plus contractors for design, penetration testing and the WCAG audit.
  • A phase closes on its exit criteria, not on a date; each gate re-checks the invariants in 02 · Requirements.
  • The release train ships a 0.x minor every 4 weeks (fixed-version runtime group, Changesets, npm Trusted Publishing), canary on every merge, and freezes the public API (spine §11) at Phase 4 entry.
  • Two pilot waves — Wave A (friendly NGO, PWA, inside Phase 2) and Wave B (WFP/UNRWA-style agency, native, prepared at the end of Phase 3 and collected in Phase 4) — measured on zero finalized-data loss, ≥ 99.5 % sync success within 7 days and enumerator SUS ≥ 70.
  • The biggest risks are commercial and legal, not technical: a 7-day trial and full runtime gating (Option A) against a free Kobo/ODK baseline; the Option A/B decision is a Phase 0 deliverable and both stay configurable (research/10).
  • The first 90 days deliver @rasd/core 0.1 (REL, engine, validators, testing utilities) and the first end-to-end web slice: render offline → autosave to IndexedDB → sync a finalized submission to the reference server.

1. Planning conventions

ConventionValue
Start dateTuesday 2026-09-01; sprints are 2 weeks and start on Mondays from 2026-09-14.
Person-day (PD)One engineer, one working day, ~6 productive hours. Estimates are for the named role and include unit tests and docs comments; they exclude review, planning and support (budgeted as a 20 % phase buffer from Phase 2 onward; Phases 0–1 run at nominal capacity with named slip candidates, see §3–§4).
Capacity1 FTE ≈ 18 PD/month after meetings, reviews and leave. Phase capacity below = Σ (allocation × 18 PD × months in phase) for the engineering roles in §8; PM and EXT are outside it.
Owner codesFE frontend/web (core, React renderer, themes, builder), RN React Native, BE backend/platform (storage, sync, server, license, CI), DR DevRel/docs, PM founder/product, EXT contractor.
PriorityUses the MoSCoW column of 02 · Requirements: M items are Phase 0–2, S items Phase 2–3, C items Phase 3 or later.
Definition of doneMerged with tests, size-limit and lint green, changeset added, docs page updated, story (web) or Maestro flow (native) where UI is touched.

Planning assumptions (revisit at every gate; each one that fails becomes a risk-register entry):

  • Library versions named below (pnpm 11, TypeScript 6, React 19, RN ≥ 0.81 / Expo SDK ≥ 54, Dexie 4, Vitest 4, Storybook 10, @dnd-kit/react 0.5.x, Yjs 13.6.x, Docusaurus 3.10) are those verified in the research folder on 2026-08-15; they are re-verified in S1 and pinned by Renovate afterwards.
  • Hires join on the dates in §8 (RN1 2026-11-16, FE2 2027-03-01); a slip of more than one sprint moves the dependent epics, not the gate criteria.
  • Two design partners are signed by the Phase 1 gate and a Wave A NGO by 2027-04-01; Wave B recruitment starts at the Phase 2 gate through an innovation programme or implementing partner (§13).
  • No revenue is assumed before 0.5.0; billing automation (E3.4) is the first thing that removes the founder from the token-issuance loop.
  • Dates are indicative: a phase closes on its exit criteria (below), and the Gantt bars are re-drawn at every gate.

Gate protocol. Each phase ends with a gate review (≤ 2 h, whole team + PM) against the checklist in that phase's Exit criteria. An unmet criterion tied to an M requirement blocks the gate: the phase extends by whole sprints, and only next-phase epics with no dependency on the failing epic may start (capped at 20 % of team capacity). Unmet S/C criteria are logged as carry-over with a target release train. Every gate re-baselines the estimates and the risk register (§10) and records its decision as an ADR next to the Option A/B ADR.

Package names, API names and JSON shapes below are those of 00 · Design spine; nothing here redefines them.


2. Phase overview

PhaseDates (2026–27)GoalHeadline deliverablesExit gate
0 FoundationsSep 1 – Oct 23 (8 wk)A publishable, headless core the rest of the stack can build on.Monorepo + CI/CD, ratified spine + JSON Schemas, @rasd/core (types, zod, REL, engine), @rasd/testing, @rasd/storage + MemoryStorage, @rasd/cli (rasd validate).@rasd/core 0.1 canary; coverage ≥ 90 %; REL parses ≥ 90 % of the expression fixture.
1 MVP webOct 26 – Feb 26 (18 wk)A React web/PWA app collects offline, syncs and is licensed end-to-end.@rasd/react, @rasd/themes, @rasd/storage-dexie, @rasd/sync + @rasd/server, @rasd/license + minimal service, web @rasd/media, @rasd/pwa v0, docs, quickstarts, examples.0.5.0; quickstart < 15 min with external devs; NFR-001/003/006 met.
2 Native + builderMar 1 – Jun 25 (17 wk)Same stack on Expo/RN; forms authored in the builder or imported from XLSForm.@rasd/native, @rasd/storage-sqlite, native @rasd/media, @rasd/builder v1, @rasd/xlsform, example-expo, alpha program.0.8.0; Maestro green on Android 8/2 GB; XLSForm corpus ≥ 90 % clean; Wave A pilot started.
3 Scale & ecosystemJun 28 – Oct 15 (16 wk)From "works" to "fits an agency's landscape".@rasd/element + PWA polish, records/cases, collaborative builder, license-dashboard + billing, openrosa transport, element marketplace.0.9.x; openrosa contract tests; billing live with invoice/PO path.
4 GA hardeningOct 18 – Dec 17 (9 wk)Evidence, not features.Security review + MASTG evidence, WCAG 2.2 AA audit + ACR, performance closure, Wave B pilot, API freeze, vendor pack v1.1.0.0 GA on 2027-12-15; 02 · Requirements §7 checklist fully ticked.
gantt
title Rasd Forms master plan, Sep 2026 to Dec 2027
dateFormat YYYY-MM-DD
axisFormat %b %y
section Phases
Phase 0 Foundations :p0, 2026-09-01, 2026-10-23
Phase 1 MVP web :p1, 2026-10-26, 2027-02-26
Phase 2 Native and builder :p2, 2027-03-01, 2027-06-25
Phase 3 Scale and ecosystem :p3, 2027-06-28, 2027-10-15
Phase 4 GA hardening :p4, 2027-10-18, 2027-12-17
section Milestones
core 0.1 canary :milestone, m0, 2026-10-23, 0d
0.5.0 MVP web :milestone, m1, 2027-02-26, 0d
0.8.0 native and builder :milestone, m2, 2027-06-25, 0d
0.9.0 ecosystem :milestone, m3, 2027-10-15, 0d
1.0.0 GA :milestone, m4, 2027-12-15, 0d
section Pilots
Wave A pilot - NGO PWA :wa, 2027-04-26, 2027-06-25
Wave B pilot - agency native :wb, 2027-09-27, 2027-12-03

Pilot bars cover the whole prepare → train → collect → review cycle of §13 (9–11 weeks); Wave B therefore starts its preparation inside Phase 3 so that its report lands before the 1.0.0 tag.


3. Phase 0 — Foundations (2026-09-01 → 2026-10-23)

Goal. Ship the framework-free core so that renderer, storage and sync work can proceed in parallel from week 9. Everything here is Apache-2.0 (spine §12). Story: as a library developer I can createFormEngine(def) in plain Node, drive it headlessly (setValue, validate, finalize) and get the same toSubmission() output that every future renderer will produce. Nominal capacity ≈ 80 PD (FE1, BE1 full; DR1 half; PM outside capacity) against ≈ 85 PD of estimates — tight by design: the fuzzing tail of E0.4, migrateSubmission in E0.5 and E0.9 may spill into S5 without moving the gate, because Phase 1 epics depend only on createFormEngine and StorageAdapter.

EpicKey tasksPDOwnerDepends on
E0.1 Monorepo & toolchainpnpm 11 workspaces + Turborepo; TS 6 strict/isolatedDeclarations/verbatimModuleSyntax; tsdown + react-native-builder-bob templates; ESLint 9 flat + Biome; Vitest 4; Changesets; size-limit, publint, attw; SHA-pinned Actions; SECURITY.md, CODEOWNERS (research/08).8BE
E0.2 Spine ratification & schemasFreeze spine §11 names; ADR for Option A/B gating; docs/schema/rasd-form.schema.json and rasd-theme.schema.json (2020-12); example corpus incl. pdm-gfd-2026 and the pdm-300 benchmark form.6FE + DR
statusSchemas ✅ · corpus ✅ (5 forms + 50 negative fixtures, examples/README.md) · ADR ⏳ founder decision.
E0.3 @rasd/core types & validatorsTS types generated from schema, zod validators, validateFormDefinition, definitionHash (canonical JSON + SHA-256), localized-string helpers, Rasd Mini-Message subset.12FEE0.2
E0.4 REL v1Pratt parser → AST, static dependency extraction, sandboxed evaluator (no eval, step/time budget), stdlib (spine §5 list), ODK aliases in call position, coercion table, RASD_EXPR_PARSE, fuzzing, ≤ 10 kB budget.15FEE0.3
E0.5 Form enginecreateFormEngine: state, topological dependency graph with cycle detection, relevant/required/readonly/calculate/constraint/validators, repeats (add/remove/reorder, ${r[2].f}), pages, validate(), finalize() (drops irrelevant values), toSubmission(), subscribe(), audit events, diffDefinitions + migrateSubmission (plan-driven), cross-runtime golden files.20FEE0.4
E0.6 @rasd/testing v0renderForm() (headless), fakeStorage, fakeClock, faultyNetwork skeleton, fixture loader.6BEE0.5
E0.7 CI/CD & release trainNode 22/24 matrix, coverage gate ≥ 90 % core, OIDC trusted-publishing dry run to @rasd/* 0.0.x, canary dist-tag on merge, Renovate with minimumReleaseAge ≥ 3 d, pnpm sbom.5BEE0.1
E0.8 @rasd/storage contractStorageAdapter interface, MemoryStorage, migrations framework, RasdError codes, @rasd/cli with rasd validate (wraps validateFormDefinition).8BEE0.3
E0.9 Docs skeletonDocusaurus 3.10 (en, ar RTL), TypeDoc → markdown, contribution guide, ADR index.5DRE0.1
statuswebsite/. Serves docs/ directly — one copy, no import step. en + ar with RTL (interface translated; the documents fall back to English per page). TypeDoc → markdown over ten packages at /api. ADR index at /decisions, generated from §14 so it cannot drift. Built in CI with link checking.

Exit criteria

  • @rasd/core 0.1.0 on canary; size-limit ≤ 45 kB (REL ≤ 10 kB); coverage ≥ 90 % lines/branches.
  • REL parses ≥ 90 % of a 500-expression fixture harvested from public XLSForms unchanged (research/01 §3); every failure is classified.
  • NFR-002 desktop: createFormEngine(pdm-300) ≤ 150 ms — 3.8 ms median, asserted in packages/core/test/perf.test.ts against a fixture whose size is asserted first. NFR-012 golden files identical on Node and jsdom.
  • rasd validate rejects every negative fixture with the documented RasdError code — 50 fixtures in examples/invalid/, one per code, and a test in the other direction so a new code without a fixture fails CI.
  • Option A/B ADR signed by the founder (see 15 · Licensing & billing). Blocked on a founder decision, not on engineering — see 00 · Open questions.

4. Phase 1 — MVP web (2026-10-26 → 2027-02-26)

Goal. A developer can add offline collection to a React app in an afternoon and license it. Nominal capacity ≈ 210 PD (FE1, BE1; DR1 half; RN1 at 50 % from 2026-11-16) against ≈ 225 PD of engineering estimates (+ 10 PD EXT design) — the tightest phase in the plan, with no 20 % buffer. Designated slip candidates, in order: the BarcodeDetector fallback and signature polish of E1.7, then E1.8 (@rasd/pwa v0), because apart from the Lighthouse row of E1.10 nothing before Phase 3 (E3.1) depends on them; E1.1, E1.3–E1.6 are on the critical path and do not slip.

EpicKey tasksPDOwnerDepends on
E1.1 @rasd/react rendererRasdProvider, FormRenderer, hooks (useRasdForm, useField, useSubmission, useRasdBusy), registry + defineElement, all §4.3 types (heavy ones code-split), paged/scroll navigation, validateOn, autosave, RTL via dir, accessible-props contract, @layer rasd CSS with rasd-<Component>__<part> classes, unstyled.45FEE0.5
E1.2 @rasd/themesTokens → --rasd-<group>-<key> CSS variables scoped on the provider root (never :root), theme JSON per rasd-theme.schema.json (id/extends/mode/tokens/components/ext), the four defaults rasd-light, rasd-dark, rasd-high-contrast, rasd-field, createTheme(partial, { extends }), fromDtcg/toDtcg, rasd theme check contrast lint, OFL Arabic font subset ≤ 120 kB/weight.12 (+10 EXT design)FEE1.1
status✅ Tokens, the four defaults, createTheme, DTCG and the contrast lint were already in place; this epic added the assets block that three documents specified and nothing read. @font-face from toCss, fonts.notoSansArabic/notoNaskhArabic (Noto Sans Arabic 400/500/700 + Noto Naskh Arabic 400/700, 38–44 kB per web face against the 120 kB budget, SIL OFL, rebuilt by pnpm fonts and hash-checked in CI), fontAssets() for expo-font, and checkThemeAssets in rasd theme check — variable fonts, CDN URLs, unused family keys, and an Arabic subset missing U+2066–2069.
E1.3 @rasd/storage-dexieDexie 4 tables, blob store, navigator.storage.persist(), AES-256-GCM with non-extractable key, migrations, estimate()/quota warnings, export(), wipe(), chaos suite v1 (NFR-010).18BEE0.8
status✅ web only. Dexie tables, blob store, persist(), AES-256-GCM with a non-extractable key, migrations, estimate()/quota events, export()/import()/wipe() were already in place; this epic added chaos suite v1. Each run aborts one IndexedDB transaction — a kill mid-write, which is what a killed tab does — then reopens a fresh adapter over the same database and checks INV-1 (every resolved finalize survives, byte-for-byte, checksum matching its own data), the one-window draft rule, attachment bytes, and outbox uniqueness. 1,000 randomized kills plus a per-transaction sweep, a mid-transaction variant that aborts only after requests have been issued, a kill during open(), and a killed migration that must not half-apply — the last one is verified to actually stop between rows, not merely to pass. RASD_CHAOS_RUNS scales the sweep. The native half of NFR-010 waits on E2.2. Fixed 2026-08-29: the adapter had no schema-version check at all — it relied on Dexie throwing VersionError, which Dexie does not do when the stored version is merely higher; it adopts the on-disk schema and opens. A rolled-back app therefore read AND WROTE a database written by a newer release, which is precisely what docs/09 §6 rule 1 exists to prevent. It now refuses, and export() still reads it through a dynamic-schema reopen, as §6 promises. Also fixed: wipe() cleared _rasd_keys — a real crypto-shred — and thereby left the NEXT open() with no stored key, so the launch after a wipe came up UNENCRYPTED. The old comment conceded it outright. A fresh data key is minted during the wipe, so at-rest protection survives it.
E1.4 @rasd/sync engineOutbox, priority phases, full-jitter backoff 1 s → 5 min, Web Locks leader, connectivity + HEAD probe, tus client, events, syncNow/pause/resume, useSync.22BEE1.3
status✅ Outbox, priority phases, full-jitter backoff, Web Locks leader, connectivity + HEAD probe, the tus subset, events, syncNow/pause/resume and useSync were in place. This epic closed the two gaps: createSyncEngine({ license }) now forwards a piggy-backed X-Rasd-License with the response's Date as serverTime (docs/15 §4.4 channel (b) — previously only @rasd/react bridged it, so a Node worker, <rasd-form> or a native app never saw a renewal), and licenseRefreshed carries exp as docs/17 §7 documents alongside the token it has always sent. Sync chaos tests (docs/18 §5.7) live in @rasd/testing, the one package that may depend on both: seeded drops, 500s, timeouts, partitions and throttling over a caseload, with engine restarts, asserting exactly-once acceptance, every local row synced, and no undocumented SyncPhase.
E1.5 @rasd/server referenceHono handlers, Postgres, S3 + tus; /v1/forms, /v1/datasets, /v1/submissions:batch (idempotent, ≤ 50), /v1/attachments, /v1/devices; version quarantine (422); contract tests run against both client and server; docker-compose.25BEE1.4
status✅ except Postgres and S3 — the reference server runs on the in-memory store, and the persistent backends are the deployment half of the epic. Hono handlers, every route, tus, quarantine and the licence issuer were in place; this epic added rspConformance() (docs/10 §11, docs/17 §14): the portable suite an agency running its own RSP server can point at it. 22 checks over HTTP — ping, device registration, auth, the manifest and its cursor, ETag/304, batches (real @rasd/core checksums and derived idempotency keys, replay, 422 on a reused key, duplicate, conflict, quarantine, the 50-item ceiling), the full tus resume story including a 409 at the wrong offset and a 460 on a bad chunk, dataset paging and tombstones, and the §1.4 error body on every failure the suite provokes. Records, SSE, remote wipe and device revocation need a server-side admin hook the suite does not yet take.
E1.6 @rasd/license + minimal serviceRLT verify (Ed25519 via WebCrypto, @noble/ed25519 fallback), state machine evaluating → trial → active → grace → limited, invalid, refresh via tokenEndpoint and X-Rasd-License, monotonic clock guard, watermark policy, useLicense; service: signup trial token, issue/rotate, JWKS, org secret proxy sample.18BEE1.3
status✅ RLT verify (WebCrypto Ed25519 with a @noble/ed25519 fallback), the evaluating → trial → active → grace → limited machine plus invalid, tokenEndpoint refresh, the monotonic clock guard and clockSuspect, watermark policy and useLicense all ship; the reference issuer (/v1/license/trial, issue/rotate, keys, org secret proxy) is in packages/server/src/license-service.ts. The dev signing key rlt-2026a must be replaced before any commercial release, and Option A vs B (docs/00 §9) is still a founder decision — now a createLicense freeFeatures switch.
E1.7 @rasd/media (web)Geolocation with accuracyThreshold and 60 s stop, camera via <input capture> + resize/EXIF strip, BarcodeDetector with fallback, signature canvas, file picker; capability detection.15FEE1.1
status✅ web. Geolocation (fixes + distanceMeters, the accuracyThreshold auto-accept lives in the renderer per docs/06), camera via <input capture> with resize and EXIF strip, BarcodeDetector with a fallback, the signature canvas, audio with MIME negotiation, the file picker with magic-byte sniffing, and capability/permission detection throughout. Native capture (E2.x) is unimplemented.
E1.8 @rasd/pwa v0registerRasdRoutes, precacheForms, useServiceWorkerUpdate, useInstallPrompt, Background Sync bridge (opportunistic).8FEE1.4
statusregisterRasdRoutes, precacheForms, useServiceWorkerUpdate, useInstallPrompt and useStoragePersistence were in place; this epic added the two names docs/17 §8 lists and nothing implemented. connectBackgroundSync({ sync, registration }) plus the worker-side handleRasdSyncWake() implement the wake design of docs/11 §4 rather than a second queue: the worker posts rasd:sync-wake to every window client and, with no client open, REJECTS so Chromium keeps the tag pending (resolving only on lastChance). It cannot drain the outbox itself — getAuthToken() lives in the page — and a worker replaying requests behind the engine's back would leave rows the server has accepted still sitting at queued, with no audit entry for the pass that delivered them. <InstallHint> carries the iOS Share → Add to Home Screen steps in en/ar/fr with a mirrored share glyph; it is role="note", never an alert. It matters more than it looks: an uninstalled PWA on an iPhone loses script-writable storage to ITP after seven days without Safari interaction (docs/11 §7) — a week of unsynced interviews.
E1.9 Docs, quickstarts, examples"First offline form in 15 min" (Vite, Next.js), server quickstart, license quickstart, API reference, playground-web, example-next, ar translation of the quickstart.25 (+5 FE)DRE1.1–E1.6
status⏳ the twelve quickstarts of docs/21, the API reference and apps/demo (the playground) were in place; this epic added apps/example-next, Quickstart 4 as runnable source, and building it found two real defects. (1) docs/21 §5 tells Next.js hosts to pass storage={() => createDexieStorage(…)} — the provider took no factory, so the documented SSR pattern did not exist. It does now, called once and only where there is a window. (2) RasdStorage.on was narrower than any real adapter's, so <RasdProvider storage={createDexieStorage(…)}> had never type-checked in any TypeScript host — including this repo's own demo app, because pnpm typecheck covered packages/* only. Both fixed; apps/* is now typechecked in CI. Still open: the Arabic translation of the quickstart.
E1.10 Quality gatesStorybook 10 matrix, vitest-axe, Playwright projects en/ar/offline, Lighthouse CI on the PWA example, budgets react ≤ 90 kB / form-runner ≤ 120 kB, two-tab draft test.15FE + BEE1.1
status⏳ partial. Done: the size budgets (tools/check-size.mjs, in CI), vitest-axe over ten pages, and now the §5.6 matrix — locale × colour scheme × contrast, plus a lang/dir check per locale, the 48 px touch floor across all four themes × three densities, and checkThemeContrast over every theme × scheme × contrast (the half jsdom cannot do, since it paints nothing and axe can measure no ratio there) — and the two-tab draft test: a second tab must not roll back what the first wrote, and each tab's patch must survive the other. Not built: Storybook 10, Playwright projects (en/ar/offline), Lighthouse CI. All three need browser binaries and a CI surface this repo does not have yet; none of them can be claimed from a jsdom run.
E1.11 RN spikeCore on Hermes (no import.meta, plural functions), expo-sqlite prototype of StorageAdapter, renderer architecture note, example-expo scaffold on SDK 54 and current.15RNE0.8
status✅ the four named pieces exist: @rasd/core is Hermes-clean (the CLDR plural rules are compiled in core/src/i18n/message.ts and Intl.PluralRules is only a guarded fallback, per docs/13 §4), @rasd/storage-sqlite implements the adapter over expo-sqlite/op-sqlite/sql.js, docs/07 is the renderer architecture note, and apps/example-expo is the scaffold. Added here: tools/check-hermes.mjs, in CI. Two of the things Hermes cannot do fail at PARSE time — a lookbehind assertion or a \p{…} escape throws when the module loads, so one anywhere takes the app down at launch, on a device, where no Node test sees it because V8 supports both. Runtime gaps (structuredClone, the patchy Intl.*) are flagged only when the file shows no feature detection. Verified by reintroducing each defect. Still true and worth repeating: nothing in this project has run on a real device or a simulator.

Exit criteria

  • 0.5.0 published: example-next collects pdm-gfd-2026 offline in Chrome and Android WebView, finalizes, syncs to @rasd/server; trial → token → limited watermark path demonstrated.
  • NFR-001 (≤ 300 ms desktop / ≤ 1.5 s reference device), NFR-003, NFR-006 met; chaos suite: zero finalized-data loss over 1,000 kills.
  • Contract tests pass in both directions; Idempotency-Key replay yields one server copy under 30 % dropped responses (NFR-011).
  • Three external developers complete the quickstart in < 15 min; two design partners have signed up.
gantt
title Phase 0 and Phase 1 epics
dateFormat YYYY-MM-DD
axisFormat %b %d
section Phase 0
E0.1 Monorepo and toolchain :e01, 2026-09-01, 2026-09-11
E0.2 Spine and schemas :e02, 2026-09-01, 2026-09-11
E0.3 Core types and validators :e03, 2026-09-14, 2026-09-25
E0.4 REL v1 :e04, 2026-09-21, 2026-10-09
E0.5 Form engine :e05, 2026-09-28, 2026-10-23
E0.6 Testing utils v0 :e06, 2026-10-05, 2026-10-16
E0.7 CI/CD and release train :e07, 2026-09-14, 2026-09-25
E0.8 Storage contract and CLI :e08, 2026-09-28, 2026-10-16
E0.9 Docs skeleton :e09, 2026-10-05, 2026-10-23
section Phase 1
E1.1 React renderer :e11, 2026-10-26, 2027-01-15
E1.2 Themes :e12, 2026-11-23, 2026-12-18
E1.3 Storage dexie :e13, 2026-10-26, 2026-11-27
E1.4 Sync engine :e14, 2026-11-16, 2026-12-23
E1.5 Reference server :e15, 2026-11-09, 2027-01-15
E1.6 License SDK and service :e16, 2027-01-04, 2027-01-29
E1.7 Media web :e17, 2027-01-04, 2027-01-29
E1.8 PWA v0 :e18, 2027-01-18, 2027-01-29
E1.9 Docs and quickstarts :e19, 2026-12-07, 2027-02-19
E1.10 Quality gates :e110, 2027-01-18, 2027-02-19
E1.11 RN spike :e111, 2026-11-16, 2027-01-29
Release 0.5.0 :milestone, r05, 2027-02-26, 0d

5. Phase 2 — Native + builder (2027-03-01 → 2027-06-25)

Goal. Parity on React Native/Expo and authoring without hand-writing JSON. Story: as an M&E officer I import my XLSForm or build the form in the browser, and enumerators run it in the Expo app fully offline with the same behaviour as the web renderer. Capacity ≈ 350 PD (FE1, FE2, BE1, RN1, DR1) against ≈ 290 PD of estimates (20 % buffer restored).

EpicKey tasksPDOwnerDepends on
E2.1 @rasd/native rendererSame public API as @rasd/react; RN components for every type; useTheme()StyleSheet; useDirection() per-form RTL; allowFontScaling + maxFontScale; virtualised repeats; a11y props; example-expo.60RN (+10 FE)E1.1, E1.11
status⏳ the renderer, provider, registry, useTheme()StyleSheet, per-form RTL, font-scale caps and the a11y prop contract are in place with 213 tests. This pass took rank and matrix off UnsupportedField, where both had been listed alongside the capture and geo families as though they needed a native peer — neither ever did. rank is move-up/move-down only (docs/07 §7's drag is an enhancement over the same value, and SC 2.5.7 requires the button path regardless); it writes the WHOLE order on the first move, because odk:rank requires every choice exactly once, and repairs a stored order that repeats or omits a choice. matrix is a Likert row per row with every cell named by row AND column — "Water — Good" — since on native the accessible name is the only thing a screen reader gets, and "Good" four times down a page is unanswerable. Not done: docs/07 §4's ≥ 600 dp horizontal grid for matrix (the stacked form renders everywhere and is what a phone gets), the capture and geo families (they need E2.3), and virtualised repeats.
E2.2 @rasd/storage-sqliteexpo-sqlite default + op-sqlite driver, SQLCipher with key in expo-secure-store, WAL, PRAGMA user_version migrations, attachments as files, backup-exclusion config plugin, export(), on-device chaos.22RN + BEE1.3, E1.11
status⏳ partial, and thinner than the code volume suggests (audited 2026-08-29). The Dexie-equivalent surface, the expo-sqlite/op-sqlite/sql.js driver split, migrations and export() exist. Not built: attachments as FILES (bytes sit in a BLOB column), the backup-exclusion config plugin (no app.plugin.js anywhere), on-device chaos. Built but unverified: SQLCipher's encrypted branch is dead code in CI; applyPragmas()'s result is discarded, so nothing can report whether WAL actually held. The expo and op driver bodies now run (22 tests): DriverOpenOptions.module lets a test inject the native module, and doubles speaking each real API over sql.js exercise the mapping — execAsync vs runAsync, getFirstAsync's nullundefined, transaction rollback, op-sqlite's two historical rows shapes, and the location/key options passed through or omitted. This proves the drivers CALL the native modules correctly; it proves nothing about JSI, and nothing here has run on a phone. Fixed 2026-08-29: wipe() is a crypto-shred. It ran a DELETE inside one transaction and stopped — the key survived in the secure store (and, on iOS, across a reinstall, which docs/09 §8.2 names as the reason the delete call exists), the file survived unvacuumed, and the process kept a usable key in its heap, so a remote X-Rasd-Wipe on a seized device did nothing a forensic tool could not undo. Now the documented order: plaintext-fallback DELETE + VACUUM (skipped under SQLCipher, where every page is already ciphertext), close, destroy the key via RawKeyProvider.destroy(), delete the database and its -wal/-shm/-journal siblings via a new SqlDriver.erase(), reopen with a fresh key. A host-owned key and an absent erase() are reported as notices rather than passed over. The trap, which a naive implementation walks straight into: secureStoreKeyProvider caches the key for the life of the process. Deleting the store item while leaving the cache means the reopen encrypts the new database with a key that exists only in that heap — and the NEXT launch cannot open it. That is permanent data loss, not a weak wipe, so destroy() clears the cache FIRST. Also fixed: listNamespaces/deleteNamespace now exist, closing the parity gap that let a host enumerate and drop namespaces on web but not on native — the platform where a shared device actually holds two agencies' datasets (docs/09 §12). Both take an injectable directory lister and deleter, because SQLite files sit wherever the host put them and this package cannot guess; deleteNamespace shreds the key before the files, and takes the -wal/-shm/-journal siblings with the database. Also fixed: file-backed local_uri attachment bytes — a photo or signature is stored as a FILE outside the database (docs/09 §5.2), so neither the key shred nor the database deletion reached it and a wipe left every capture on disk in cleartext. createSqliteStorage({ deleteFile }) now takes the deleter; without it the wipe records local-attachments-not-deleted rather than reporting an erase it did not perform. Fixed 2026-08-29: export() on a downgraded database, which docs/09 promises in §6, §11 and §15 and which neither adapter implemented — and, in @rasd/storage-dexie, the downgrade refusal itself, which had never existed (the adapter relied on Dexie throwing VersionError, and Dexie adopts a newer on-disk schema instead, so a rolled-back app opened a database its release had never seen and WROTE to it). Also fixed: the downgrade guard read _rasd_meta, not the PRAGMA user_version docs/09 §6 rule 1 actually names — and user_version is the number HOST migrations advance, so a build shipping a migration to v5 left a file that a build knowing no migrations opened, read and wrote. The same bug as the Dexie one, in the other adapter.
E2.3 @rasd/media (native)Location, camera/gallery + compression + EXIF, barcode, signature, foreground audio, file picker; permission UX; Play/App Store declaration templates (research/11).25RNE2.1
statusall six Expo adapters written and Node-tested — location, camera/gallery, barcode, signature, foreground audio, file picking — plus src/native/permissions.ts. Every Expo module is INJECTED, never imported: the peers are optional (a static import would make them mandatory for every web host), and injecting them is what puts each adapter's whole logic under Node instead of behind a device. Each runs mediaAdapterSuite(), the same contract the six web adapters satisfy. Three pieces of shared logic were extracted so native could not fork them, and the web adapters were rewired onto each: the file allow/deny/size/sniff validator (a security boundary — a second copy drifts, and the failure is a payload one platform stores and the other refuses, both suites green), the signature export geometry (docs/14 §15's ±2 px is now a Node assertion running BOTH adapters, not a claim you can only check on two devices), and AUDIO_PRESETS.sampleRate (a form author picks "standard" by name; it has to mean one thing). canAskAgain gives native the denied-vs-blocked split docs/14 §3.2 needs, observed rather than inferred from web's 200 ms heuristic. Every adapter mutation-swept: 53/53 killed. Not done, and all device-bound: running any of this against real Expo modules, the Play/App Store declaration templates, and the config-plugin usage strings.
status❌ not started (audited 2026-08-29). packages/media has src/web and no native counterpart: no createExpoLocation, createExpoCamera, createExpoBarcode, createSvgSignature, createExpoAudio or createExpoDocumentPicker; no react-native export condition; no per-capability subpath entries; no mediaAdapterSuite(); no store-declaration templates. This is why eleven element types still resolve to UnsupportedField in @rasd/native — the components are blocked on these adapters, not on the renderer. Started 2026-08-30 with the prerequisite: mediaAdapterSuite() (@rasd/media/conformance), written BEFORE any Expo adapter and run against the six existing WEB ones — because a contract suite validated only against code written to satisfy it proves nothing. It found two real defects on its first run: a disposed geolocation adapter never checked and went on to take a real position fix using a permission the host had torn down, and a disposed file picker returned [], which a caller cannot tell from a cancelled dialog. Both fixed; a unit test that had pinned the second is corrected.
E2.4 @rasd/builder v1Palette/canvas/inspector; @dnd-kit/react 0.5.x behind dnd-adapter with non-drag equivalents (SC 2.5.7); undo/redo (zustand + immer); logic editor with REL autocomplete; translations tab; preview via @rasd/react; JSON view; versions/diff with breaking-change guard; defineElement plugins; onPublish.75FE1 + FE2E1.1, E0.5
status⏳ partial (audited 2026-08-29). Palette/canvas/inspector, undo/redo, the logic editor with REL autocomplete and its analyzeLogic graph, the translations tab, preview, JSON view, versions/diff, defineElement plugins and onPublish all exist. Fixed 2026-08-30 — the publish gate. Of the three reported deviations one was a false positive: force IS an admin override for B-class content changes (docs/00 §167, docs/17 §496), and docs/08's parenthetical claiming otherwise was the outlier, now corrected. The other two shared ONE root cause — validateFormDefinition(published) was called with a single argument, and core gates its whole publish-rules pass on opts.previous, so version reuse, monotonicity and the unchanged-content check never ran despite core implementing and testing all three. Passing previous turns them on; the identity blocks run before force is consulted so it gains no new reach; and sortSummaries now uses core's compareVersions instead of localeCompare, because two orderings for one concept is how a monotonicity check passes something it should refuse. Gaps that matter: pointer and keyboard drag are wired but never exercised by a test and there is no E2E layer, so the SC 2.5.7 non-drag path is unproven; the builder's own chrome, DnD instructions and live-region announcements are hardcoded English. Canvas virtualisation and the NFR-008 60 fps work need a browser.
E2.5 @rasd/xlsformXLSForm → RFD (incl. Kobo score/kobomatrix/rank), XPath → REL rewriter with ext["org.getodk.xpath"] fallback + warnings, RFD → XLSForm, XForm instance serializer/parser, pyxform round-trip CI, rasd convert xlsform.25BEE0.4
status⏳ partial (audited 2026-08-29). Import, export, the XPath→REL rewriter with its ext["org.getodk.xpath"] fallback, the XForm serializer/parser and the Kobo constructs are implemented. Fixed 2026-08-29: exportXlsform({ format: 'csv-zip' }) is implemented — the same sheets as CSV in one store-only ZIP, dependency-free (the xlsx peer is optional precisely so an import-only host need not install it, and that host previously could not export at all), deterministic, UTF-8 with a BOM so Excel reads Arabic, RFC 4180 quoting, plus the docs/20 §14 formula-injection guard no export path had. Verified against the system unzip, not only its own reader. importXform now resolves <itext> — the locale set, labels, hints, form="guidance", media and choice labels, plus the verbatim lang names in org.getodk.xlsform.languages for the XLSForm round trip. It also fixed a latent bug in importXlsform: any all-non-Latin language name (العربية, فارسی, русский) slugged to the empty string, picked up the name sanitiser's f- prefix and produced the locale "f-", which core's Bcp47Schema rejects. The lang→code table is now shared and carries endonyms. The known gap is closed (2026-08-30). Repeat-scoped aggregates now survive the round trip: the importer builds a name → repeat-scope map in its naming pass and performs the I_NODESET_REWRITE docs/20 §2.5 specifies, so sum(${m_is_child}) comes back as sum(${hh_members[].m_is_child}) instead of an E_EXPR_UNKNOWN_REF that stopped the form opening. The pinned KNOWN-GAP test is retired exactly as its own comment instructed. Also open: begin_kobomatrix with more than one question column is narrowed to one on import. The pyxform round-trip CI and the ~140-form corpus behind the Phase 2 exit criterion need a Python toolchain.
E2.6 Sync/license parity on nativeNetInfo, expo-background-task accelerator, tus on RN, license refresh, Maestro flows on Android emulator (1 GB profile) + iOS simulator.15RN + BEE2.2
status❌ not started (audited 2026-08-29). NetInfo appears in no source file, so createNetInfoConnectivity() does not exist; there are no AppState/foreground/finalize triggers and syncNow takes no reason argument, so docs/07's syncNow('foreground') cannot be expressed; the expo-background-task accelerator is entirely absent; apps/example-expo does not consume @rasd/sync or @rasd/license at all, so the 0.8.0 exit criterion has no code path; and the one thing that makes tus work on React Native — the file-URI bridge — has zero test coverage.
E2.7 i18n runtimeCompiled CLDR plural functions per locale (Hermes lacks Intl.PluralRules), numbering/calendar display, localeMeta, en/ar/fr packs.12FEE2.1
status✅ the compiled CLDR functions, settings.numbering/calendar and localeMeta were already in core. This pass added the French chrome packs (156 keys in @rasd/react, 72 in @rasd/native, in parity with en and ar and pinned by a test) and fixed the defect finding a home for them exposed: both renderers selected plurals with count === 1, an English rule applied to every locale, while formatMessage in core has done full CLDR selection all along. French takes the singular at zero, so "Il y a 0 problèmes" was ungrammatical; Arabic has six categories, so 2 rendered as "٢ مشكلات" instead of the dual "مشكلتان", and 3 and 11 were wrong too. The .one/.other key pairs are now single keys carrying {count, plural, …}, Arabic declares all six categories, and # formats the number with the locale's own digits. Tested by rendering real counts per locale, and verified non-vacuous by restoring the two-category Arabic string. And the audit found the same defect live on native: packages/native/src/FormRenderer.tsx delegated formatNumber/formatDate to the PROVIDER's, which is built from the bare chrome locale and has no form — so settings.calendar and settings.numbering were honoured on web and ignored on native, six weeks after the web fix. Now built from the form's own config, with tests that compute their expectations from the same Intl they run on. Remaining: Spanish chrome (docs/13 §16 lists es as tier-1; E2.7 asks only for en/ar/fr).
E2.8 DocsExpo quickstart, builder guide, XLSForm migration guide, ar/fr getting-started; SurveyJS JSON best-effort importer (C).20DRE2.1–E2.5
E2.9 Alpha program & vendor pack v02–3 design partners, weekly office hours, triage SLA; architecture one-pager, DPA draft, questionnaire answers v0.10PM + DRE1.9
E2.10 QualityStorybook RN-web stories shared with web, RNTL 14, size budgets, native perf tests (NFR-004).15RN + FEE2.1
status⏳ partial (audited 2026-08-29). The size budgets exist and run in CI. @rasd/native and @rasd/storage-sqlite had no entry at all; they are now measured and reported as informational — 53.1 kB and 16.4 kB min+gz — rather than given invented thresholds, because docs/00 §12 sets no number for either and a budget the tool made up would be the tool arguing for a threshold nobody agreed to. Those two figures are what a founder needs to set the actual budgets. Not built: Storybook in any form; NFR-004 perf tests on either renderer; virtualised repeats on native, which blocks the 60 fps half of NFR-004 regardless. Worth stating plainly: RNTL 14 is not used. The native suite runs @testing-library/react against react-native-web under jsdom, so 219 passing native tests exercise a web DOM, never the real React Native renderer.

Exit criteria

  • 0.8.0: example-expo collects offline with SQLCipher, syncs, refreshes its license; Maestro suite green on Android 8 / 2 GB and iOS simulator.
  • Builder authors pdm-gfd-2026 from scratch (logic + translations) and round-trips it byte-identically through JSON view; XLSForm corpus ≥ 90 % clean imports, ≥ 95 % expressions parse.
  • NFR-004 (200-row repeat) on both renderers; NFR-008 builder at 60 fps on the 300-question form.
  • Wave A pilot in the field with a friendly NGO — collect phase started 2027-05-17 on the current latest web train; review completes by the gate (§13).
gantt
title Phase 2 epics
dateFormat YYYY-MM-DD
axisFormat %b %d
section Native
E2.1 Native renderer :e21, 2027-03-01, 2027-05-14
E2.2 Storage sqlite :e22, 2027-03-01, 2027-04-02
E2.3 Media native :e23, 2027-04-05, 2027-05-07
E2.6 Sync and license parity :e26, 2027-04-19, 2027-05-14
E2.7 i18n runtime :e27, 2027-04-05, 2027-04-23
section Builder and interop
E2.4 Builder v1 :e24, 2027-03-01, 2027-06-04
E2.5 XLSForm import and export :e25, 2027-03-15, 2027-04-30
section Cross-cutting
E2.8 Docs :e28, 2027-04-19, 2027-06-18
E2.9 Alpha program and vendor pack :e29, 2027-03-15, 2027-06-25
E2.10 Quality :e210, 2027-05-17, 2027-06-18
Wave A pilot :wa, 2027-04-26, 2027-06-25
Release 0.8.0 :milestone, r08, 2027-06-25, 0d

6. Phase 3 — Scale & ecosystem (2027-06-28 → 2027-10-15)

Goal. Fit into an agency's existing landscape and make the business run without manual token issuance. Story: as an agency ICT lead I keep my KoboToolbox / ODK Central server, embed <rasd-form> in an existing portal, let supervisors resolve record conflicts, and pay by invoice/PO. Capacity ≈ 315 PD (5 FTE) against ≈ 250 PD of estimates.

EpicKey tasksPDOwnerDepends on
E3.1 PWA polish + @rasd/elementWorkbox strategies, update UX, Safari-eviction guidance, <rasd-form> custom element (open Shadow DOM, CSS-variable theming, rasd:* events), rasd-forms.iife.js.20FEE1.8
E3.2 Records/cases/v1/records pull/push, HLC, per-field LWW, supervisor conflict queue, conflict status, offline record lists, useRecords-style hook (name to be added to spine §11).45BE + FEE1.5
E3.3 Collaborative builderYjs 13.6.x mirror of the builder model, y-indexeddb, Hocuspocus/y-websocket relay with host bearer token, presence, per-user undo; interim optimistic locking (baseHash → 409) ships first (08 · Builder §15).30FE2E2.4
E3.4 license-dashboard + billingOrgs/apps/tokens/seats UI, Stripe Billing + Entitlements + Invoicing (PO, Net-30, tax-exempt), webhooks → issue/rotate, humanitarian-discount attestation, offline license file, rasd license check (research/10 §5).50BE + FE2E1.6
E3.5 openrosa transportformList/manifest, HEAD + multipart submission, X-OpenRosa-Version: 1.0, 201/202/409 handling, profiles kobo/central/ona, Collect QR enrolment, recorded-fixture contract tests; ODK-compatible submission encryption (C).30BEE2.5, E1.4
E3.6 Custom-element marketplacePackaging convention for x:* elements (rasd-element-*), static registry index in docs, builder plugin loading, reference elements x:beneficiary-lookup, x:likert-matrix, x:photo-grid.20FEE2.4
E3.7 Native field-edit mode (C)@rasd/builder/core DOM-free subpath; tablet reorder/labels/choices editing with react-native-sortables.15RNE2.4
E3.8 Scale & performance10k-choice index, 100k dataset rows, 50k-submission migration ≤ 5 min with progress, memory over 50 open/close cycles (NFR-005/007/024).20BE + RNE2.2
E3.9 DevRel & DPG trackIntegration guides (Kobo, ODK Central, Ona/MoDa), marketplace docs, ar/fr, two talks, DPG registry application if Option B is chosen.20DRE3.5

Exit criteria

  • 0.9.0: openrosa transport passes contract tests for all three profiles; a Rasd front-end submits to a real KoboToolbox project.
  • Billing end-to-end: card checkout and invoice/PO both issue tokens automatically; refresh via X-Rasd-License observed on the reference server.
  • Records: disjoint edits merge silently, same-field collisions land in the supervisor queue (FR-068).
  • API-report diff shows only additive changes since 0.5.0; deprecation list frozen for 1.0.

7. Phase 4 — GA hardening (2027-10-18 → 2027-12-17)

Goal. Produce the evidence agencies ask for and stabilise. Story: as an agency security/procurement reviewer I receive the vendor pack (pen-test summary, ACR, DPA, subprocessor list) and a 1.0 whose API and wire formats are frozen. Capacity ≈ 180 PD (5 FTE) plus ≈ 25 PD of contractors against ≈ 140 PD of estimates; feature work is limited to pilot-driven fixes.

EpicKey tasksPDOwnerDepends on
E4.1 Security reviewThreat-model refresh, external penetration test of renderer/storage/sync/license/server, MASTG scripts + MASVS L2 evidence, ASVS 5.0 controls, crypto-shred/secure-delete verification, SBOM + provenance, disclosure policy live (research/11).20 (+15 EXT)BEE3.x
E4.2 Accessibility auditExternal WCAG 2.2 AA audit of renderers, builder, docs; fixes; VPAT/ACR; TalkBack/VoiceOver/NVDA scripts; RTL parity matrix.20 (+10 EXT)FE + RNE3.x
E4.3 Performance closureNightly device lab (Maestro + Playwright), NFR-001..008 closure on Snapdragon 4xx-class Android 8, bundle budgets, heap snapshots.20FE + RN
E4.4 Wave B pilotPreparation starts 2027-09-27 inside Phase 3 (form build/import, partner-hosted @rasd/server or openrosa target, DPA, translation QA); in Phase 4: training week, field support rota, incident handling, hot fixes as patch releases, pilot report (§13).25all + PME2.x, E3.5
E4.5 1.0 & go-to-marketAPI freeze via API-extractor report, migration guide 0.x → 1.0, LTS policy, vendor pack v1 (DPA, subprocessors = none for runtime, questionnaire, WCAG/RTL evidence), pricing page, UNGM registration.15PM + BEE4.1, E4.2
E4.6 Docs GAVersioned docs, compatibility-matrix page generated from CI, ar/fr complete, video walkthroughs.15DR

Exit criteria (GA)

  • Release-gate checklist of 02 · Requirements §7 fully ticked; pen-test findings ≥ High closed, others scheduled.
  • ACR published; zero critical/serious axe violations across the Storybook matrix.
  • Wave B pilot report: zero finalized-data loss, ≥ 99.5 % sync success within 7 days.
  • 1.0.0 published for all packages with provenance; rasd spec version 1.0 and RSP /v1 frozen.
gantt
title Phase 3 and Phase 4 epics
dateFormat YYYY-MM-DD
axisFormat %b %d
section Phase 3
E3.1 PWA polish and element :e31, 2027-06-28, 2027-07-23
E3.2 Records and cases :e32, 2027-06-28, 2027-08-27
E3.3 Collaborative builder :e33, 2027-07-12, 2027-08-27
E3.4 License dashboard and billing :e34, 2027-06-28, 2027-09-10
E3.5 OpenRosa transport :e35, 2027-07-26, 2027-09-17
E3.6 Element marketplace :e36, 2027-08-30, 2027-10-01
E3.7 Native field-edit mode :e37, 2027-08-30, 2027-09-24
E3.8 Scale and performance :e38, 2027-09-06, 2027-10-08
E3.9 DevRel and DPG track :e39, 2027-08-02, 2027-10-15
Release 0.9.0 :milestone, r09, 2027-10-15, 0d
section Phase 4
E4.1 Security review :e41, 2027-10-18, 2027-11-19
E4.2 Accessibility audit :e42, 2027-10-18, 2027-11-19
E4.3 Performance closure :e43, 2027-10-18, 2027-12-03
E4.4 Wave B pilot :e44, 2027-09-27, 2027-12-03
E4.5 API freeze and GTM :e45, 2027-11-08, 2027-12-10
E4.6 Docs GA :e46, 2027-11-01, 2027-12-10
Release 1.0.0 GA :milestone, r10, 2027-12-15, 0d

7.1 Cross-phase quality tracks (security, accessibility, performance)

Security, accessibility and performance are not Phase 4 activities that get "audited in"; each phase carries its own gate items and Phase 4 only produces the external evidence. This table is the summary the gate review walks through.

TrackPhase 0Phase 1Phase 2Phase 3Phase 4
Security & data protectionSHA-pinned Actions, OIDC-only publish, SECURITY.md; REL evaluator sandboxed with step/time budget (no eval).AES-256-GCM at rest with non-extractable key (@rasd/storage-dexie), loud warning when unencrypted; RLT verified 100 % offline; Idempotency-Key + SHA-256 checksums on the wire; PII never in URLs or logs (NFR-042).SQLCipher default with key in expo-secure-store/Keychain/Keystore, backup-exclusion config plugin, store permission declarations; builder treats form definitions as untrusted content.Webhook signature verification and org-secret proxy for billing; openrosa auth per profile; marketplace elements sandboxed behind the registry contract; securityReport() in examples.Threat-model refresh, external pen test, MASVS L2 / ASVS 5.0 evidence, crypto-shred verification, SBOM + provenance, disclosure policy.
Accessibility & i18nLocalized-string helpers and Rasd Mini-Message subset in @rasd/core.Accessible-props contract (label id, described-by, error state), vitest-axe on the Storybook matrix, Playwright ar RTL project, rasd-high-contrast theme, reducedMotion mode.Non-drag equivalents for every drag (WCAG 2.2 SC 2.5.7), RN a11y props, allowFontScaling + typography.maxFontScale, per-form RTL via useDirection(), CLDR plurals on Hermes.Marketplace element a11y checklist; <rasd-form> Shadow DOM label/focus contract; ar/fr docs.External WCAG 2.2 AA audit, ACR/VPAT, TalkBack/VoiceOver/NVDA scripts, RTL parity matrix.
PerformancecreateFormEngine(pdm-300) ≤ 150 ms; REL ≤ 10 kB; core ≤ 45 kB.NFR-001/003/006; @rasd/react ≤ 90 kB, form-runner ≤ 120 kB; heavy element types code-split; Lighthouse CI on the PWA example.NFR-004 (200-row repeat) on both renderers; NFR-008 builder at 60 fps; 1 GB emulator profile in Maestro.NFR-005/007/024: 10k-choice index, 100k dataset rows, 50k-submission migration ≤ 5 min, no leaks over 50 cycles.Nightly device-lab closure of NFR-001..008 on Snapdragon 4xx-class Android 8, heap snapshots, 14 days green.

8. Team plan (3 → 5 people)

RoleProfileJoinsAllocation by phase (0/1/2/3/4)
FE1 Frontend leadSenior TypeScript/React; owns core engine, REL, renderer, themes; de-facto tech lead.Day 1100 / 100 / 100 / 100 / 100
BE1 Backend/platformNode/Postgres, offline storage, sync protocols, crypto hygiene, CI/CD, license service.Day 1100 / 100 / 100 / 100 / 100
DR1 DevRel / technical writerDocs, quickstarts, examples, community, ar/fr coordination; runs usability tests.Day 1 at 50 %50 / 50 / 100 / 100 / 100
RN1 React Native engineerExpo/New Architecture, SQLite/SQLCipher, native media, Maestro.2026-11-16 at 50 %, full from Phase 20 / 50 / 100 / 100 / 100
FE2 Frontend (builder)dnd/a11y-heavy UI, zustand/immer, later collaborative editing and dashboard UI.2027-03-010 / 0 / 100 / 100 / 100
PM FounderProduct, pricing, partners, procurement, pilots; not counted in engineering capacity.Day 1
EXT ContractorsVisual/UX designer (~20 PD, Phases 1–2), penetration testers (~15 PD, Phase 4), WCAG auditor (~10 PD, Phase 4), Arabic/French reviewers (~10 PD).As scheduled

Rules: one owner per epic, at most two epics in flight per person, a written acceptance list before an epic starts, and ≥ 20 % of the tech lead's time on review. If hiring slips: delay E3.3 and E3.7 (C items), then compress E3.6 — never E4.1/E4.2.


9. Budget notes

Payroll dominates; everything else is small. Figures marked R are grounded in research; A are planning assumptions to be replaced by quotes.

LineBasisNote
Engineering payroll~1,000 PD ≈ 56 FTE-months over 15.5 months> 85 % of spend; multiply by your fully-loaded FTE-month cost (A).
Contractors~55 PD design, pen-test, a11y audit, localisationFixed-price engagements; three quotes in Phase 3 (A).
Device lab6 low-end Android phones (Android 8–12, 2 GB), 1 iPhone, 1 Android tabletOwned; needed from Phase 1 for NFR-001 (A).
CI & visual testingGitHub Actions; Chromatic Free 5,000 snapshots/mo, Starter $179/mo beyond (research/08 §10)Snapshot only en/ar × light/dark to stay in the free tier (R).
Hosting (reference server, license service, docs)Small Postgres + object storage + one Node service, EU regionRuntime devices never call Rasd; traffic is developer/CI only (A).
PaymentsStripe Billing 0.7 % + processing, Invoicing 0.4 %/invoice; Paddle 5 % + $0.50 if MoR (research/06, research/10 §2)Cost of revenue, not opex (R).
Accounts & legalApple Developer Program, Google Play Console, npm org, EAS Build; UNGM registration (free); legal review of FSL + Commercial Terms + DPANeeded for pilots, TestFlight and the vendor pack (research/10 §3.2) (A/R).

Cash-flow shape: no revenue before 0.5.0 (Feb 2027), trial-driven until billing automation lands in Phase 3, agency invoices follow pilots — so first significant invoiced revenue is Q1 2028. Plan ≥ 18 months of runway.


10. Risk register

Likelihood/impact on a 1–3 scale; owner reviews the register at each phase gate.

IDCategoryRiskLIMitigation / trigger
R1TechnicalREL cannot express real XLSForm logic; imports flag too much as unmappable.23Corpus-driven fixture from Phase 0; ODK aliases; ext["org.getodk.xpath"] fallback; trigger: < 90 % parse at Phase 2 gate → extend stdlib before builder polish.
R2TechnicalRN churn (0.81 → 0.87 removes deep imports; Strict TS API; Hermes V1 memory regression) breaks @rasd/native (research/08 §1).32No react-native/Libraries/* imports; test matrix RN 0.81/0.85/0.87 and SDK 54/56; Renovate grouped RN bumps.
R3TechnicalPinned pre-1.0 @dnd-kit/react 0.5.x changes API or stalls.22dnd-adapter boundary; non-drag equivalents mean the builder stays usable if DnD is swapped.
R4TechnicalBrowser storage eviction (Safari 7-day rule) or quota exhaustion loses drafts.23persist(), install-to-Home-Screen guidance, quota headroom warnings (NFR-021), RN recommended for multi-day offline; drafts export always available.
R5TechnicalSupply-chain compromise via npm (Shai-Hulud-class worms), CI token theft, or release tooling regressions (pnpm 11.0.8 OIDC bug, TypeDoc vs TS 7).23pnpm 11 minimumReleaseAge + allowBuilds, OIDC-only publish via changeset publish → npm CLI, SHA-pinned actions, Socket/pnpm audit; TS 7 job advisory-only.
R6Adoption7-day trial (shortest in peer set) and Option A gating push teams to free Kobo/ODK.33Phase 0 ADR; auto-extend to 30 days on onboarding milestones; humanitarian discount; Option B stays a features[] config flip (research/10 §5).
R7Adoption"Another platform" perception; agencies will not migrate servers; long procurement cycles outlast runway.33Position as a library; openrosa transport (pull forward if pilots demand); enter via implementing partners and innovation programmes; annual invoice SKU + UNGM registration; ≥ 18 months runway.
R8Legal/licensingFSL packages make the core ineligible as a Digital Public Good; token leakage or apps[] mis-binding causes disputes; clean-room builder challenged.22Apache-2.0 packages stay genuinely usable; DPG application only under Option B; tokens public by design with apps[] binding + short lifetimes; documented no-lineage policy; SQLCipher/OFL attributions in NOTICE.
R9Data protectionPII on shared, low-end Android devices (Auto Backup, forensic remnants, logs) or a misconfigured host (unencrypted web storage, PII in URLs).23SQLCipher default, backup-exclusion plugin, keys only in secure store, crypto-shred on wipe, PII-free logging (NFR-042), loud warnings + securityReport(), MASTG evidence in Phase 4.
R10iOSHome-Screen web apps: no Background Sync, 7-day storage eviction, Data Protection Class C, expo-file-system cannot set backup exclusion; App Store review rejects background location/audio (2.5.4, 5.1.5).32Foreground-driven sync; native recommended on iOS; config plugin for NSURLIsExcludedFromBackupKey; audio foreground-only and location trail off by default; Apple account + TestFlight provisioned in Phase 2 (11 · PWA & embedding).
R11DeliveryBuilder (75 PD) and records (45 PD) overrun; single points of failure in a 5-person team; Phases 0–1 are planned at or slightly above nominal capacity (≈ 85/80 and ≈ 225/210 PD) with no buffer.32Builder split FE1/FE2 with weekly demo; C items are the buffer; FE/RN pairing on renderer parity; named slip candidates for Phases 0–1 (§3–§4) and a Day-90 re-baseline (§14) before Phase 1 is half-way.
R12Adoption / pilotsNo Wave A NGO by 2027-04-01 or no Wave B agency by the Phase 2 gate; a pilot slips into agency holiday freezes and the GA report is late.23Recruit two candidates per wave via implementing partners and innovation programmes; Wave B preparation starts in Phase 3 (§13); if Wave B slips, 1.0.0 still tags on the technical gates and the pilot report ships as 1.0.x collateral (open question below).

11. KPIs per phase

PhaseEngineering KPIsProduct/adoption KPIs
0Core coverage ≥ 90 %; REL fixture parse ≥ 90 %; createFormEngine ≤ 150 ms; CI < 10 min p50.Option A/B decision recorded; 5 example forms in the corpus.
1NFR-001/003/006 met; form-runner ≤ 120 kB; chaos suite 0 loss/1,000 kills; contract tests green.Quickstart < 15 min (3/3 external devs); ≥ 2 design partners; ≥ 100 weekly downloads of @rasd/core.
2Maestro green on Android 8/2 GB; XLSForm ≥ 90 % clean, ≥ 95 % expressions; NFR-004/008 met.Wave A live; ≥ 30 trials; ≥ 500 weekly downloads; builder authors the PDM form in < 60 min.
3openrosa contract tests 3/3 profiles; webhook → token issue p95 < 60 s; 50k migration ≤ 5 min.First paying org; one annual-invoice sale; ≥ 3 marketplace elements; ≥ 1,500 weekly downloads.
40 critical/serious axe; pen-test Highs closed; NFR-001..008 nightly green 14 days straight.Wave B report published; ≥ 5 paying orgs; trial → paid ≥ 5 % (12-month target ≥ 8 %, 01 · Vision §7).

12. Release train & versioning

  • Cadence. A 0.x train of the fixed-version runtime group (@rasd/core, @rasd/react, @rasd/native, @rasd/storage*, @rasd/sync, @rasd/media, @rasd/pwa, @rasd/license, @rasd/themes, @rasd/element, @rasd/builder) leaves every 4 weeks regardless of content — a minor when it carries features, a patch otherwise; the gate versions 0.5.0 / 0.8.0 / 0.9.0 / 1.0.0 are reserved for the phase exits so an intermediate train never overtakes a gate. Data-safety patches ship within 48 h. Tooling packages (@rasd/cli, @rasd/xlsform, @rasd/testing, @rasd/server) version independently.
  • Channels. canary (every merge, 0.x.y-canary.<sha>), next (release candidate one week before the train), latest. Partners and pilots run latest; the docs site builds against next to catch drift.
  • Mechanics. Changesets → "Version Packages" PR → tag → OIDC Trusted Publishing with provenance, tokens disallowed; publint, attw and size-limit are release blockers; SBOM attached to the GitHub Release (research/08 §7, §11).
  • Spec versions. RFD rasd: "1.0" from Phase 0; until GA the schema may change with a converter shipped in @rasd/core; after GA MINOR bumps are ignore-and-preserve and deprecations get ≥ 12 months (spine §4.3b). RSP /v1 and the RLT format freeze additively at the Phase 1 gate (keys rotate via kid).
  • API freeze. Spine §11 names are frozen from Phase 0; shapes freeze at Phase 4 entry with an API-extractor report reviewed per PR; 1.x gets a 12-month maintenance window after 2.0.
  • Per-train checklist. Changesets reviewed · contract tests both directions · Storybook matrix axe = 0 · size budgets · chaos suite nightly green · upgrade test from N-3 · docs versioned · rasd license check on examples · changelog announcement.

13. Pilot plan (WFP/UNRWA-style partner)

Why two waves. A PWA pilot with a friendly NGO de-risks the engine before native parity exists; the agency pilot then measures the native Android stack agencies actually deploy (research/01 §1, §4). Agencies rarely buy from cold procurement, so both partners are recruited through implementing partners or innovation programmes such as the WFP Innovation Accelerator and UNHCR Innovation Service (research/10 §3.3).

ItemWave A (Phase 2)Wave B (prepared end of Phase 3, run in Phase 4)
PartnerNGO with an existing Kobo workflow; public case study in exchange for a free Team plan.Country-office monitoring unit of a WFP/UNRWA-style agency, or its third-party-monitoring contractor.
ScopeOne PDM form (pdm-gfd-2026, en/ar) via installed PWA; 10–15 enumerators; 1 field office; 4 weeks.PDM + site-visit form; 20–40 enumerators; 2 field offices; 4–6 weeks (the PDM window after distribution).
Stack@rasd/react + @rasd/storage-dexie + @rasd/sync + @rasd/pwa (installed PWA, rasd-field theme) → partner-hosted @rasd/server.@rasd/native + @rasd/storage-sqlite (SQLCipher) + @rasd/sync on Expo → partner-hosted @rasd/server or their KoboToolbox / ODK Central via the openrosa transport.
DevicesEnumerators' Android phones (min Android 8, 2 GB).Agency-provisioned Android; TestFlight for 2–3 iOS supervisors.
Data protectionDPA signed; all data on partner infrastructure; Rasd receives no beneficiary data, only aggregate counters the partner chooses to share.Same, plus agency ICT/security review using the vendor pack; consent element in the form; server-side retention.

Timeline per wave: Prepare (2 weeks: form build/import, server deploy, DPA, translation QA) → Train (1 week: rasd-field theme, offline drills, supervisor triage) → Collect (4–6 weeks: weekly review, on-call rota, hot fixes only as patch releases) → Review (2 weeks: report, retrospective, Enterprise offer decision).

Calendar. Wave A: prepare 26 Apr – 7 May 2027, train 10–14 May, collect 17 May – 11 Jun (4 weeks), review 14–25 Jun — the report is an input to the Phase 2 gate. Wave B: prepare 27 Sep – 8 Oct 2027 (inside Phase 3), train 11–15 Oct, collect 18 Oct – 19 Nov (5 weeks; extendable to 26 Nov), review 22 Nov – 3 Dec — the report is an input to the GA gate on 15 Dec. Both waves run on the latest train with hot fixes as 0.x.y patches; no pilot ever runs canary.

Failure modes and how a pilot ends early. (1) Data loss suspected — freeze the train, export every device outbox (storage.export() always works), reconcile against server counts, root-cause before any further collection; this is a GA blocker regardless of date. (2) Sync success < 99.5 % after 7 days — treat as an incident, not a KPI miss; the reference server and @rasd/sync logs (never beneficiary data) are pulled for the post-mortem. (3) Partner cannot sign the DPA or complete the security review — the wave is postponed, not run "unofficially"; the vendor pack (E2.9/E4.5) is updated with whatever question blocked it. (4) License or watermark issue in the field — the pilot runs on a team-plan token with 60-day exp / 30-day grace; a token problem is by definition a P1 bug in @rasd/license, and collection continues in grace.

What to measure

MetricTargetSource
Finalized submissions lost while the device survives0Server counts vs device outbox export; incident log
Sync success within 7 days of reconnecting≥ 99.5 %@rasd/server counters
Attachment upload success (first attempt / eventual)≥ 95 % / 100 %tus logs
Cold start to first page (Android 8, 2 GB)≤ 1.5 s native / ≤ 2 s WebView p95On-device trace sample
Crash-free sessions≥ 99.5 %Partner's crash reporter (not Rasd)
Median interview duration vs partner's Kobo baseline≤ baselineAudit startedAt/finalizedAt
Supervisor rejections (data quality)≤ 5 %Server review status
Enumerator SUS score; RTL defects reported≥ 70; 0 blockingPost-pilot survey (ar/en); issue tracker
Battery drain attributable over an 8-hour day≤ 10 %Partner device sample
Longest offline stretch without degradation≥ 7 days observedSync logs

Exit: a written pilot report shared with the partner, a go/no-go on an Enterprise quote, and every P1/P2 finding converted into a tracked issue before the next train.


14. First 90 days (2026-09-01 → 2026-11-27)

SprintDatesGoalsOwnersDemo at review
S1Sep 1 – Sep 11E0.1 repo/toolchain; E0.2 spine ratification, JSON Schemas, corpus; Option A/B ADR drafted; RN1/FE2 hiring opened; device lab ordered.BE, FE, DR, PMCI green on an empty @rasd/core; rasd validate accepts the 5 example forms and rejects negatives.
S2Sep 14 – Sep 25E0.3 core types, zod, definitionHash; E0.4 REL parser (literals, operators, ${} refs, calls) + dependency extraction; E0.7 release-train dry run.FE, BEREPL evaluates every expression in pdm-gfd-2026; 0.0.1 canary published via OIDC.
S3Sep 28 – Oct 9E0.4 stdlib, ODK aliases, fuzzing; E0.5 engine state, graph, relevant/calculate/constraint; E0.8 StorageAdapter + MemoryStorage; E0.6 renderForm().FE, BEHeadless engine runs the PDM form's skip logic in tests; fixture parse rate reported.
S4Oct 12 – Oct 23E0.5 repeats, finalize()/toSubmission(), diffDefinitions/migrateSubmission; E0.9 docs skeleton; Phase 0 gate review.FE, BE, DRGolden files identical on Node/jsdom; @rasd/core 0.1.0 canary; gate checklist reviewed.
S5Oct 26 – Nov 6E1.1 RasdProvider/FormRenderer + text/number/select/date/note; E1.2 tokens → CSS vars; E1.3 Dexie schema + autosave; E1.5 server skeleton (/v1/forms, /v1/devices).FE, BEplayground-web renders the PDM form with autosave to IndexedDB; RTL toggle works.
S6Nov 9 – Nov 20E1.1 group/repeat/select_multiple/checkbox/consent; E1.3 encryption + export(); E1.4 outbox + submissions:batch; E1.6 RLT verify spike; RN1 starts E1.11.FE, BE, RNOffline → online: a finalized submission reaches the server with Idempotency-Key; token verified offline.
Day 90Nov 27Retro; re-baseline Phase 1 estimates; confirm FE2 start date and design-partner shortlist.allBurn-up vs plan; updated risk register.

Open questions

  • Should the openrosa transport move from Phase 3 to Phase 2 so Wave A can submit to the partner's existing Kobo project? It costs ~30 PD earlier and displaces part of the builder.
  • Is GA on 2027-12-15 acceptable given holiday freezes at agencies, or should 1.0 tag on 2027-12-01 with Phase 4 buffer used for hot fixes only?
  • Does the founder ratify Option A or B in Phase 0? Option B changes the DPG track (E3.9), the pricing page and the watermark policy but no code paths.
  • Which C items are cut first if the team stays at 4: E3.3 collaborative builder or E3.7 native field-edit mode?
  • Are records/cases (E3.2) needed read-only in Phase 2 for Wave A (household lookup), as 02 · Requirements asks?
  • Should the DevRel role start at 100 % on day 1 to translate the docs skeleton into Arabic earlier, at the cost of ~9 PD of engineering budget per month?
  • Phase 1 is planned without its 20 % buffer (§4). Should E1.7 (web @rasd/media) or E1.8 (@rasd/pwa v0) be moved wholesale into early Phase 2 to restore it, or is the RN1 50 % allocation better spent on Phase 1 web work until FE2 joins?
  • If Wave B slips past 2027-12-03 (partner recruitment or holiday freeze), does 1.0.0 still tag on the technical gates with the pilot report shipping as 1.0.x collateral, or is a completed agency pilot a hard GA criterion?