A recap of the work in the grundysf/mos repo to date — from the first planning
commit through the complete Sprint-0 foundation and the post-Sprint-0 domain build: auth/RBAC spine, Jobs/
Dispatch/Documents/Employees/Equipment end-to-end, a real Dashboard, the professional design concepts, and
both native mobile plans. Generated from the actual git history; everything below is in the repo.
Sprint 0 (the discovery-independent foundation) is sequenced as five days — all five are now committed. Day 5 shipped with the deliberately minimal placeholder Job schema; the field set widens after the client discovery meeting. Since then, the domain-adjacent entities (Dispatch, Documents, Employees, Equipment) and a real Dashboard have shipped ahead of that meeting too — see §04. Full detail: implementation-plan.html.
docs/discovery-guide.html) —
client back in town next week.Thirty-three commits across six threads: planning & design, deploy infrastructure, design refinement, the Sprint-0 platform build, the mobile app plans, and the post-Sprint-0 domain build (Dispatch/Documents/Employees/Equipment, a real Dashboard, the Job Detail retheme, and the fixes that followed it).
/api/health.docs/ so the client can view the storyboard remotely.python -m mos-platform start error; took control of the build with uv against the committed lockfile.implementation-plan.html and this document — built with /visual-plan//visual-recap as local HTML only, no third-party tools, per the no-egress requirement.pitch.html not being reachable from the hub.design-system.html can be edited directly in Claude Design.apps/api (FastAPI, still serving the storyboard) and apps/web (Next.js 16 + React 19 + Tailwind v4). Deploy retargeted to build from the repo root.uv package; after repeated Nixpacks friction, standardized on the Dockerfile build in Dokploy instead.globals.css as Tailwind v4 CSS-first @theme tokens — flat dark-navy, shadcn/ui-ready (semantic vars, components.json, cn() util). User approved the token showcase.AppShell (sidebar / mobile bottom-nav, top bar) + StatusPill, RoleBadge, DataState (loading/empty/error/populated) + Skeleton, using lucide-react icons.profiles / roles / user_roles via Supabase migrations (RLS on, hardened after an advisor flag); FastAPI verifies Supabase ES256 JWTs via JWKS (no shared secret); require_roles() RBAC; /api/me, /api/me/roles, /api/admin/ping. The account solomonacquah@gmail.com was created and assigned the owner role — verified end to end.jobs table via Supabase migration (RLS on, 4 sample jobs seeded); RBAC-guarded FastAPI CRUD (/api/jobs — reads for any authenticated role, writes for PM/office/owner, deletes owner-only); typed TS client generated from OpenAPI (npm run gen:api); Supabase email/password login in apps/web; live Jobs list + detail wired through TanStack Query + the four-state DataState pattern.sqlalchemy>=2.0.30 was declared without the [asyncio] extra, so greenlet never installed even though create_async_engine/async_sessionmaker need it — surfaced as a 500 on any DB-touching route (/api/jobs, /api/me/roles) the moment a real login was attempted locally.apps/web/public/mos-logo.png, converted from the mislabeled-AVIF source) on /login and the app shell sidebar, with a theme-aware --logo-filter token.invert regardless of the toggle — only the login card (which flips white/graphite) uses the theme-driven filter.docs/storyboard.html converted to the dual-theme system — dual token block (same variable names, new palette) plus ~50 hardcoded inline hex/rgba colors converted to var()-based equivalents so every avatar, status pill, dispatch-board cell, and chart bar re-themes automatically. Squared radii, Iowan Old Style/Palatino serif, always-ink sidebar. Persisted toggle button replaces the old static "switch theme" link. storyboard-light.html is now a redirect (sets light theme, forwards to storyboard.html).useSyncExternalStore). Ink/graphite sidebar, squared mono status pills, serif headings, certificate-block login; both themes verified in-browser./ on plan.sacqu.com for the Next.js app — the API's docs mount moves to /docs (relative cross-links unaffected); apps/web gets its own multi-stage standalone Dockerfile to run as a separate Dokploy service alongside the API.employees/certifications/equipment/job_assignments/documents tables with RBAC-guarded FastAPI routers, Supabase Storage for document upload/download, a drag-and-drop Dispatch board (@dnd-kit), and a keyless Leaflet/OpenStreetMap site-location map on Job Detail./ with KPIs computed from live data; document thumbnails, click-to-open large preview, and version-history grouping shared between /documents and Job Detail./. Moved it into a click-to-open menu on the sidebar avatar in AppShell so it's reachable from any page; removed the now-redundant button from the Jobs page.4a967a8) relocated the storyboard and concept docs to /docs on the API service — still live there, but old bookmarks like plan.sacqu.com/storyboard now 404 against the Next.js app that owns /. Added next.config.ts redirects from the old bare paths to their /docs/*.html home, plus a "Docs & Concepts" link in the sidebar so the new location doesn't need to be memorized.136 tracked files across five groups — client-facing design artifacts, the Next.js app, the FastAPI backend, the Supabase schema, and deploy config.
Where the look started, where the shipped design-system tokens landed, and the two new professional directions now on the table for comparison.
AppShell, StatusPill, RoleBadge, DataStateapps/web/globals.css (the product) and docs/storyboard.html (the client-facing
storyboard), each flipping via the same .dark-class pattern. The 13-screen storyboard was fully
rethemed rather than kept as a historical navy artifact — client and product now show the identical system.apps/web, design tokens live, AppShell + primitives shipped, running locally with npm run dev.apps/api boots without a database configured; verified via TestClient — / → 200 (storyboard), /api/health → 200, /api/me → 401 without a token.profiles/roles/user_roles schema live with RLS; require_roles("owner") enforced on /api/admin/ping.solomonacquah@gmail.com signed up, auto-provisioned a profile via trigger, and was assigned the owner role — confirmed live against the Supabase project.jobs table live in Supabase with 4 seeded samples; /api/jobs CRUD enforces role-tiered writes; the web app signs in with Supabase email/password and renders the live list + detail through the typed OpenAPI client (npm run gen:api) with TanStack Query. Build + lint green.uv package friction), using uv against the committed lockfile. Listens on port 8000 at plan.sacqu.com.supabase/migrations/), not Alembic — Alembic is retained only as a vestigial optional dependency.xcode/mosmobile (separate repo) per the iOS plan — M0 foundation
+ M1 Jobs core built in one pass: GoTrue REST auth (no SDK) with Keychain sessions + actor-single-flight
refresh, Face ID gate, certificate-pinning delegate, typed NetworkService, SwiftData job cache
with offline read fallback, role-gated status transitions, and Plat & Ink / Meridian Color Sets.
47/47 unit tests green on the iPhone 17 simulator (iOS 17.0 target, zero third-party deps).employees/certifications/equipment/job_assignments/documents
tables + RBAC-guarded routers; web screens at /dispatch (full drag-and-drop via
@dnd-kit), /documents (Supabase Storage upload/download via signed URLs),
/employees, and /equipment; a keyless Leaflet/OpenStreetMap site-location map in
Job Detail (jobs.latitude/longitude). iOS added a TabView shell plus the same four
feature verticals (network-first, SwiftData-cache-fallback repositories) with tap-based Dispatch
reassignment and a native MapKit job map — 55/55 iOS tests green. Browser-verified live
against real seeded data (office_staff account): Dispatch board, Employee status toggle, Equipment status
change, and the job map all confirmed working end-to-end.JobStatus enum (no fabricated legal/survey fields ahead of the schema-widening pass). A real
Dashboard route landed at / (KPI row + jobs table, all computed from live jobs/dispatch/
equipment/certification data — cross-checked against the Dispatch and Equipment pages and matched
exactly); the Jobs table moved to /jobs. Documents get real thumbnails for photos, a
click-to-open large preview (native image/PDF view, download fallback otherwise), and version-history
grouping — re-uploading a same-named file still uploads and now surfaces "Updated to vN" instead of a
duplicate row. Browser-verified live against the real seeded Supabase data.apps/api/app/core/storage.py — likely a missing or
wrong SUPABASE_SERVICE_ROLE_KEY / DOCUMENTS_BUCKET in apps/api/.env,
or the documents bucket not yet existing in Storage. Every DB-backed feature (jobs, dispatch,
employees, equipment, dashboard) works fine against the same Supabase project — only Storage calls are
affected. The preview modal now shows a clear error instead of hanging when this happens./
had silently broken sign-out (it only lived in the old Jobs-page header) — now a global account menu on
the sidebar avatar, reachable from every screen. Separately, the docs-off-root move from July 5 had left
old bookmarks like plan.sacqu.com/storyboard 404ing even though the content was live at
/docs/storyboard.html the whole time — added redirects for the old paths plus a permanent
"Docs & Concepts" sidebar link so the URL never needs to be remembered again./settings page.
A single /api/me/profile endpoint returns the signed-in user's identity — profile name/email,
platform roles, and (when they're an employee) their HR fields: employee ID, title, phone, hire date, status —
rendered as a read-only identity card through the typed OpenAPI client. Verified against live data
(solomonacquah@gmail.com → Mike Odell, owner, EMP-001). Typecheck + lint green.POST /api/employees/invite when no account
exists yet, which calls Supabase's admin invite endpoint (service_role key, same pattern as
Storage) and returns the freshly-created profile so the employee record attaches immediately.
Route confirmed registered on the live API; typecheck + lint green. Not yet click-tested in a
browser (tool permissions timed out mid-session) — needs a manual owner → Employees →
Add employee pass with a fresh email before calling this done.android/ (a subfolder of this monorepo, unlike iOS's separate repo) per the
Android plan — Kotlin 2.2, Jetpack Compose, Material 3, minSdk 29.
First feature ported: GoTrue REST auth (no Supabase SDK) with EncryptedSharedPreferences
(MasterKey AES256_GCM) sessions, a Mutex-guarded single-flight token refresh,
androidx.biometric BiometricPrompt re-entry gate (class 3 + device-credential fallback),
and a session-state-driven single NavHost (SignedOut → Login, Locked → BiometricGate,
Active → Home placeholder) — same shape as iOS's AppCoordinator. MOS brand tokens ("Plat & Ink" /
"Meridian") ported into the Material 3 color scheme and squared 2–6dp shape system.
14/14 unit tests green (interface-based fakes, no mocking framework, per constitution
§10). Not yet run on a device/emulator — build and tests verified via Gradle only so far.NavigationBar bottom-tab shell now mirrors iOS's five-tab
TabView one-for-one (Jobs, Dispatch, Documents, Employees, Equipment), replacing
the Milestone 1–2 single-Home-screen shape. Dispatch is a week-at-a-glance board grouped by
employee (confirmed against iOS's actual List+Section layout, not a
2D grid) with tap-based reassignment, conflict-highlighted cards, and schedule/move dialogs;
Employees adds the two-step "look up profile by email, then attach HR fields" add flow and a
view-only certifications detail screen; Equipment splits into Instruments/Vehicles with an
expiring-certifications alert banner. All three network-only, no Room cache, same trim as
Documents. 91/91 unit tests green (45 new). One real, pre-existing bug found
and fixed during live verification: every dropdown picker in the app (Equipment's category
picker, Dispatch's job/crew/day pickers) silently failed to open because the anchor field was
missing Material3 1.4.0's required Modifier.menuAnchor(...) — confirmed via
uiautomator dump and the Material3 sources jar, then fixed everywhere it appeared.
Verified live on the Pixel 10 Pro emulator against the real seeded dataset, including a full
schedule → verify → delete round trip on the Dispatch board.DataStateContent.kt's Loaded branch called content()
directly, never applying the modifier carrying the Scaffold's topBar padding — so
every list (Jobs, Documents, Employees, Equipment, Dispatch) rendered starting at the very top
of the screen, with the opaque TopAppBar drawn over whatever fell in that range. Invisible until
now because every previously-tested list had enough rows to scroll past the hidden region;
Dispatch's near-empty current week was the first list short enough to hide entirely.
One-line fix (content() → Box(modifier) { content() }) corrects every
affected screen at once. Verified live: Dispatch's current week now renders its one real card;
previously-hidden rows on Jobs and Dispatch's prior week both became visible for the first time.
91/91 unit tests still green (pure layout fix, no logic changed) — no Compose UI test added,
since the project has no Compose UI testing infrastructure yet; caught via live device
verification instead.useRoles()/RoleGated client-side gating
pattern (roles-provider.tsx — UX only, server RBAC stays the enforcement layer);
transactional email (core/email.py, Resend over httpx, no-op until configured —
same pattern as APNs); shared PDF rendering (core/pdf.py, fpdf2 promoted to a
runtime dep, latin-1 typography sanitizer); and the unauthenticated tokenized-link router
(api/public.py) that emailed proposal/invoice links will use. Also established
the API's first pytest suite (apps/api/tests/, 10/10 green) — including a guard
proving the public router never accidentally inherits auth.clients/contacts tables plus an additive, non-breaking
jobs.client_id — the migration backfilled one client per distinct historical
client_name and linked every job (40 seeded jobs → 12 distinct clients,
confirmed live). client_name stays a denormalized mirror during the
transition; the jobs write path keeps it in sync with a provided client_id,
and renaming a client cascades to every one of its jobs. New /clients list
(trgm search, reusing the same fuzzy-match index proven on Documents) and
/clients/[id] detail (contacts CRUD + full job history via
StatusPill) — the first real page hung off Phase 0's grouped nav and
client-side role gating. Verified live: all 12 backfilled clients render, search narrows
correctly, a real contact was added through the UI and round-tripped, client detail shows
Anderson Family Trust's actual 7-job history. Schema changes to the live database went
through an explicit confirmation step first — Claude Code's safety layer blocks direct
production migrations on a generic "continue" instruction and requires the specific
action be named.services catalog (name, unit, price) with a /services page
and active/inactive toggle. New employee_rates — an
effective-dated history, never a column on Employee, so a raise never
rewrites the profitability of jobs already worked. Gated by a new
FINANCE_ROLES tier (office_staff + owner) one step up from the usual write
gate, with cost_rate nulled out server-side for anyone who isn't owner. Built
a proper employee-detail page (/employees/[id] — didn't exist before) to hold
the new Billing rates panel, the first real use of client-side financial gating: verified
live by switching between an office_staff test account (sees billable rate, no cost rate,
no add-rate button) and the owner account (sees and can add both figures) — added a real
rate to Jordan Cole ($140/hr billable, $65/hr cost) and confirmed both render correctly for
owner only.mos_test database, with tests/conftest.py rebuilding the schema from the
SQLAlchemy models themselves each run (drop + create_all, plus the same
set_updated_at trigger production uses) and a fixed fake signed-in user whose roles a
test can set exactly via seed_roles(). Caught a real schema-drift bug along the way:
the employee_rates ORM model was missing the
unique(employee_id, effective_date) constraint that the actual Supabase migration has —
meaning the 409-conflict handling in create_rate had never been exercised against a real
constraint. Backfilled 24 new real business-logic tests for Clients/Contacts (Phase 1) and
Services/Employee-rates (Phase 2) — CRUD, role-gated writes, the client-rename-mirrors-onto-jobs
rule, the effective-dated "current rate" resolution, and the cost_rate redaction for non-owner
readers. 37/37 green, twice in a row. This is now a standing gate for every phase
from here on./tasks page and Operations navigation shipped through the generated OpenAPI
client. Ruff, ESLint, TypeScript, and production build green; real Postgres API suite now
42/42 green. Isolated Chrome QA also passed linked task creation,
immutable job notes, and the equipment calibration warning. The production migration was
applied transactionally; live authenticated Chrome smoke tests then passed on Tasks and
Equipment./time
page provides browser-geolocation clock controls, recent entries, leave submission, and
gated approvals. Native screens remain in the mobile catch-up milestone per the web-first
decision. Ruff, ESLint, TypeScript, and build green; API suite 47/47 green.
Isolated Chrome QA passed the time-clock control state plus leave submission and owner
approval; the real GPS button remains privacy-gated while coordinate enforcement is
covered by the API suite. The production migration and live authenticated /time
smoke test also passed./invoices and public /i/[token] pages shipped. Ruff, ESLint,
TypeScript, and build green; API suite 53/53 green. Isolated Chrome QA
passed Sales Order creation, service-priced draft creation, approval transitions,
PO-ceiling warnings, signed-negative Credit Notes, and the tokenized public view. The
production migration, live authenticated invoice empty state, and public invalid-token
privacy response were also verified in Chrome.Both native plans (iOS, Android) commit to identical feature scope per phase — same nine Phase A line items, same wording, same mapping to platform Phases 1–2. This table is the scannable version: what's actually built per feature, per platform, right now. Regenerate it whenever either app ships, so a gap is visible at a glance instead of requiring a side-by-side read of two changelogs.
| Feature | iOS | Android |
|---|---|---|
| Sign in + biometric re-entry | Done — Face ID gate, Keychain, actor-single-flight refresh | Done — BiometricPrompt gate, EncryptedSharedPreferences, Mutex-single-flight refresh |
| My Jobs list + detail | Done | Done — Room-cached offline read fallback |
| Status updates (Assigned → In Progress → Waiting → Completed) | Done — role-gated transitions | Done — role-gated transitions |
| Job notes | Done — immutable server-first creates, job-scoped SwiftData cache and offline read fallback | Done — immutable server-first creates, job-scoped Room cache and offline read fallback |
| Document viewing | Done — QuickLook, cloned from the Documents vertical | Done — global list, category filter, pagination; opens via a signed-URL hand-off to whatever app the OS picks (no in-app renderer) |
| Photo capture + upload (GPS + timestamp stamped) | Done — camera capture, UTC/GPS evidence banner, job-scoped field upload; photo-library fallback on camera-less devices | Done — camera/gallery capture, UTC/GPS evidence banner, adaptive JPEG compression and job-scoped multipart field upload |
| Site maps + navigation hand-off | Done — native MapKit job map | Done — geo-intent hand-off (deliberately not an embedded map, per §07 of the constitution) |
| Daily field reports | Done — job-scoped composer, weather/crew/work/issues/next steps, SwiftData offline reads | Done — matching job-scoped composer, validation, server-first submission and Room offline reads |
| Push notifications (assignment, status, due-date) | Done — APNs registration, token sync, assignment/status alerts, daily due-date cron, job deep links | Done — FCM registration/token sync, platform-aware backend delivery, foreground alerts and job deep links; live delivery activates when Firebase credentials are deployed |
mosmobile scheme passed 83 tests with zero failures or skips on an iPhone 17 / iOS 26 simulator.
A clean Debug simulator build installed and launched successfully. Android remained unchanged throughout the iOS-first gate.| Feature | iOS | Android |
|---|---|---|
| GPS time clock (Phase B) | Done — job/non-billable clock-in, GPS evidence, geofenced arrival/leave reminders, breaks, travel, mileage, notes, active-clock recovery | Done — matching GPS lifecycle, active recovery, job/non-billable scope, breaks/travel/notes and 150 m site reminders |
| Time Off (Phase B) | Done — request form, status history, API-backed submission, offline history cache | Done — native date form, validation, status history, server-first submission and Room offline history |
| Mileage + payroll week (Phase B) | Done — validated odometer delta, persisted shift mileage, current-week hours and completed-entry summary | Done — matching odometer validation, manual fallback, persisted mileage and week/completed summaries |
| Equipment checkout/return (Phase B) | Done — swipe actions, employee assignment, double-checkout conflict protection | Done — self-checkout/return actions with double-checkout and holder/manager conflict protection |
| Management & insight (Phase C) | Done — executive financial dashboard, Swift Charts, cross-entity historical search, server-rendered PDF report library, dispatch adjustments, WidgetKit | Done — role-gated financial dashboard, performance visualization, intelligent search, authenticated PDF library, dispatch adjustments and Glance |
| Native intelligence (Phase D) | Done — on-device voice transcription, reviewable report structuring, private document extraction/summaries, natural-language search, VisionKit document scanning | Done — offline-preferred native speech, reviewable report drafts, private summaries, intelligent search and ML Kit scanning/upload |
| Survey monument intelligence (P9A.2–P9A.4) | Done — sensor evidence, offline queue, reviewed vision extraction and cited packet/PDF | Done — stamped camera/library evidence, Room queue/sync, human confirm/reject, cited parcel packet and authenticated PDF |
| Native MOS Assistant (P9.7) | Done — grounded SSE chat, citations/tools/usage and confirmable job actions | Done — grounded SSE chat, citations/tools, per-process tokens/cost and draft-confirm-cancel actions |
| Dispatch board (assign/reassign crew to jobs by day) | Done — tap-based reassignment | Done — tap-based reassignment, matching iOS |
| Employees (list, detail, add via profile lookup) | Done — existing-profile lookup and attach flow | Done — matching existing-profile lookup and attach flow |
| Equipment (list, add, status, expiring-cert alerts) | Done | Done |
| Home-screen widget | Done — WidgetKit, Phase C item shipped early | Done — Glance active-job count and three-row snapshot, refreshed by authoritative job loads |
MOSFieldWidget on July 8 before Phase A was complete. Phase A is now green on iOS; Dispatch, Employees, and
Equipment were cloned to iOS on July 6 the same way — real, working screens, but outside the nine items either
plan actually itemizes for Phase A; Android caught up to those three in its own Milestone 3 (July 13, table
above), so the widget is now the only platform-exclusive item left. None of this is wrong, but it's exactly
the drift this table exists to catch. The active gap has moved to Phase B and later parity rows.androidx.biometric
BiometricPrompt gate (class 3 + device-credential fallback), GoTrue REST auth with no Supabase SDK,
EncryptedSharedPreferences (MasterKey AES256_GCM) in place of Keychain, and a Mutex-guarded single-flight
refresh in place of iOS's actor. 14/14 unit tests green. The remaining eight Phase A rows should follow this
same one-feature-at-a-time cadence rather than batching.docs/android-parity-plan.md's
ordering — the foundational vertical slice everything else (Documents, the map hand-off, Dispatch) attaches
to. Room-cached offline read fallback (network-first, same pattern as iOS's SwiftData cache), role-gated
status transitions identical to iOS's allowed-transition rules. 31/31 unit tests green; verified live against
the real seeded dataset (5000-row kit) on a Pixel 10 Pro emulator — list rendering, status-pill colors
matching the web app exactly, filtering, and a real status PATCH round-trip all confirmed. One real bug
found and fixed during live verification: availableTransitions was a plain ViewModel property
reading the StateFlow directly rather than a field derived from the observed uiState itself, so
Compose rendered it once but never recomposed it after a status change (the status pill updated correctly;
the transition list below it silently went stale). Fixed by moving it onto JobDetailUiState, with
a regression test added.Intent.ACTION_VIEW, letting
whatever app the OS picks (Chrome, a PDF viewer, …) render it — deliberately no in-app renderer, same
"hand off to the system" philosophy as the new "Get Directions" row on Job Detail, which builds a
geo: URI instead of embedding a map (iOS uses native MapKit; Android's constitution explicitly
rules out a bundled map SDK). 46/46 unit tests green — including a new GeoUriTest pinning the
URI format, since the seeded 5000-row dataset has no jobs with real coordinates to click-test live against
(the four original hand-seeded jobs do, but weren't practical to locate by scrolling). Verified live:
real document metadata renders correctly, category filtering works, and the download/open flow correctly
triggers a real OS app-picker hand-off with a valid signed Storage URL.uv run uvicorn app.main:app --reload --port 8000 in apps/api and
npm run dev in apps/web, sign in at localhost:3000/login, and jobs/dispatch/
employees/equipment/dashboard all render live data. Document upload/download still 502s locally though;
set a valid SUPABASE_SERVICE_ROLE_KEY + DOCUMENTS_BUCKET in
apps/api/.env and confirm the documents bucket exists in the Supabase project's
Storage (see §04)./financials dashboard built from live task, time, employee-rate, invoice,
payment and credit-note records. It reports earned revenue, WIP, AR, collections, backlog, labor cost,
gross profit and margin by job, with department drill-down and explicit missing-rate warnings. Jobs now
support hourly/fixed-fee billing terms and contract value. The fixed-fee recognition policy should receive
a bookkeeper/accountant gut-check before use in formal statements. Full API regression: 55/55 green;
Ruff, ESLint, TypeScript and the Next.js production build are green./p/[token] client presentation. Typed-name acceptance is expiration-aware, row-locked and
idempotent: it creates exactly one fixed-fee job plus proposal-linked task budgets that feed time,
invoicing and Phase 6 financials. Full regression: 60/60 green. Chrome QA covered both internal screens
and a temporary $8,500 public proposal; the QA row was removed without recording a false acceptance.
Commit fbc85aa deployed from main, after which authenticated Chrome smoke tests
passed on the live plan.sacqu.com/proposals and /financials routes.496-520-007-000, lot 7, subdivision ID 1008)
and links Placer County Assessor/Recorder research rather than asserting containment or a recorded-map match./assistant workspace. Its LangChain two-step RAG pipeline retrieves
authorized live jobs, clients, tasks, notes and document metadata, augments them with semantic PDF/text
chunks from a private Supabase pgvector index, and returns evidence cards with every answer. Field crews
are restricted to assigned jobs; the model has no SQL tool. A hash-only audit records status and cited
source IDs without retaining raw questions or answers. P9A.1 adds a typed authenticated SF/San Mateo/Roseville
registry, health/schema monitoring, retry and warm-cache behavior, plus honest county record-portal leads.
P9A.2 adds native iOS camera/library monument capture with GPS accuracy, heading, altitude, SwiftData-first
offline persistence, idempotent retry, and confirmed per-item removal of stale pending/failed captures;
syncing or uploaded evidence is protected. P9A.3 adds structured vision extraction with editable fields and
mandatory human confirm/reject; unconfirmed observations cannot become packets. P9A.4 persists cited parcel
research with official county, FEMA and USGS links, displays it on web and iOS job records, and renders a
cautioned authenticated PDF field packet. Verification: full API regression 91/91, focused
parcel tests 13/13, the complete iOS unit target, Ruff, ESLint, TypeScript, generated OpenAPI
and the Next.js production build all pass. P9A.0's 40-case evaluation, rights confirmation and licensed-
surveyor review remain external production-acceptance gates.docs/android-parity-plan.md for the ordered catch-up plan; Milestone 3
(Dispatch, Employees, Equipment) is complete on both platforms.