================ ONE-SHOT BUILD CONTRACT (read first) ================ Build this in Google AI Studio "Build" in ONE shot — a complete, working app, no follow-up turns. These are hard rules, not suggestions: 1. TARGET = Full-Stack Web (Node server runtime, secrets, Firebase allowed). If you target Native Android instead, you MUST drop all server/DB/Workspace/ secrets and persist locally (Room / SharedPreferences) only. 2. PIN EVERY MODEL CALL — never let the agent auto-select (it downgrades on quota): - Reasoning / text -> gemini-3.5-flash (thinkingLevel: minimal|low|medium|high) - 4K image + legible text -> gemini-3-pro-image (image_size "4K", up to 14 refs) - High-volume image -> gemini-3.1-flash-image - Expressive TTS -> gemini-3.1-flash-tts-preview (inline tags e.g. [whispers]) - Realtime audio/video (WebSocket) -> gemini-3.1-flash-live-preview - Sandboxed agent -> antigravity-preview-05-2026 3. DIVISION OF LABOR — the model ONLY parses/extracts to a strict responseSchema. ALL math, money (store currency as integer minor units / cents), sorting, balancing and graph logic run in deterministic TypeScript/Python. The model must never compute totals, splits or balances itself. 4. responseSchema sanitation — no regex patterns, no fixed-length tuples, no format validators in the schema (they crash the OpenAPI engine). Enforce those in server-side code AFTER parsing the JSON. 5. responseSchema and google_search grounding are MUTUALLY EXCLUSIVE in one call. 6. CODEGEN — split large output into modular, single-responsibility files so no file is truncated by the output-token cap. 7. Every external call gets a graceful fallback (e.g. manual paste if a Workspace read fails). Never a silent dead end. 8. ROBUST STORAGE & CANVAS — Wrap all `localStorage`/`sessionStorage` operations (especially JSON parsing and writes) in `try-catch` blocks to prevent crashes in private windows or quota overflows. Canvas drawing elements must dynamically handle window resize and scale pixel density (`window.devicePixelRatio`) to avoid blurry graphics on retina displays. ===================================================================== # MUST OBEY — Mobile-first build requirements This app's PRIMARY surface is a mobile phone. Build it impeccably on mobile FIRST, then verify on tablet and desktop. Treat the rules below as non-negotiable hard constraints, not suggestions. ## Viewports to verify (every screen, every state) - 320 px, 360 px, 375 px, 390 px, 414 px, 480 px - 768 px, 834 px (iPad portrait / Pro 11) - 1024 px, 1280 px, 1440 px, 1920 px, 2560 px - Plus: 200% browser zoom, landscape orientation on every mobile width, iPhone with safe-area insets visible ## Hard layout rules - Mobile-first CSS. Default styles target mobile; `@media (min-width: ...)` for larger viewports. - Use `dvh` and `svh` instead of `vh` for full-height surfaces (iOS Safari URL-bar bug). - Use `clamp()` for fluid typography across all viewports. - Prefer container queries (`@container`) over media queries for component-level responsiveness. - Use `min(100%, ...)` widths so content never overflows. Zero horizontal overflow at any viewport. - Add `` to every page. - Apply `padding: max(safe-area-inset-X, fallback)` on every edge-bleeding container so notched iPhones in landscape never clip content. - Wide tables and code blocks scroll INSIDE their container (`overflow-x: auto`), never push the body. - Use `background-attachment: scroll` on mobile, not `fixed` (iOS Safari repaint bug). - Avoid `backdrop-filter` on animated elements. Use it sparingly on static surfaces only. - **Canvas Scaling**: Canvases must dynamically scale with window resize events and properly handle high-DPI screens (`window.devicePixelRatio`). Set physical dimensions (`canvas.width`/`canvas.height`) using pixel ratio and render relative to this grid, using CSS to control responsive viewport scaling. - **Robust Storage**: Every access to `localStorage`/`sessionStorage` (especially `JSON.parse` of loaded state or writes) MUST be wrapped in a `try-catch` block to handle disabled storage, private browsing mode, quota limits, or corrupted JSON gracefully. Fall back to a robust in-memory object store. ## Touch & accessibility - Tap targets ≥ 44 × 44 px on touch (Apple HIG). Increase to 48 px under `@media (hover: none) and (pointer: coarse)`. - All interactive controls reachable by keyboard with a visible focus ring; respect `:focus-visible`. - Color contrast ≥ 4.5:1 for body text, 3:1 for UI components. - All images have meaningful `alt`. Decorative images use `alt=""`. - Respect `prefers-reduced-motion: reduce` — zero animation durations under that query. - Forms validate inline; error messages are specific, not "Invalid input". - Modals: focus trap, `Esc` closes, `role="dialog"`, `aria-modal="true"`, focus restored on close. ## Performance bar (Lighthouse mobile, throttled 3G/4G) - LCP < 2.5 s · INP < 200 ms · CLS < 0.1 - JS bundle gzip < 200 KB mobile-first; lazy-load non-critical screens via `React.lazy` / dynamic imports. - No render-blocking resources above the fold. - Images: WebP/AVIF preferred, `loading="lazy"`, explicit `width`/`height` attributes (zero CLS), `srcset` for retina. - Videos: `preload="metadata"`, low-resolution poster, max 720p mobile fallback. Never autoplay with audio. - Fonts: `font-display: swap`; preload only the one used above the fold. - Smooth scroll honoured via CSS `scroll-behavior: smooth` with reduced-motion fallback. ## Pre-ship mobile checklist (the deployer MUST verify before declaring done) 1. Open at 375 px in DevTools — every screen scrolls vertically only; zero horizontal scroll. 2. Browser zoom 200% — layout reflows without overlap. 3. iPhone Safari with the URL bar visible AND landscape — no content under the home indicator; no notch clipping. 4. iPad portrait (768 px) and landscape (1024 px) — no awkward gaps; tablet-specific breakpoints land cleanly. 5. Tap every interactive element with a thumb at real-device size — every target is easy to hit. 6. `prefers-reduced-motion: reduce` — every transition / animation skips cleanly, scroll-behavior becomes instant. 7. Lighthouse mobile score ≥ 90 across all 4 categories. 8. Zero `console.error` and zero CLS shift in real-device testing on a mid-tier Android (e.g. Pixel 6a) and an iPhone SE. --- The original template starts below. All rules above apply on TOP of whatever this template specifies. --- # Mutual Aid Roster ## 1. Project **Mutual Aid Roster** is a hyper-local coordination app for the neighbour who somehow ends up running the help thread when a storm hits. Neighbours send voice memos — "I can shovel three driveways tomorrow morning, until noon" or "my mother is 73, alone, the path to the door is iced over, anything would help" — and the app turns each one into a structured offer or request, holds a shared roster that the whole block can see, and proposes specific matches without the coordinator manually pairing names at midnight. The volunteer sees the three addresses on a map and confirms; the person who asked sees a name and a window; the coordinator sees only what she needs to see, no more. This is the kind of app a barangay captain in Quezon City opens when a typhoon shuts the markets and the elders on her street can't get to the relief truck. It is also the kind of app a block captain in Buffalo opens during a four-day lake-effect snow event when ninety neighbours have already texted her the same three sentences in twelve different ways, and a volunteer with a Ford F-150 has just offered to do welfare checks but she is too tired to remember which three apartments asked. Same shape of moment, different weather, different language, same coordinator at the kitchen table at midnight trying to keep track on a notepad. The single demo that proves the magic: a volunteer records one voice memo — "I can shovel three driveways tomorrow morning, I have a truck, I'm out from eight until noon, I'm on Elmwood between Bidwell and Lafayette" — and five seconds later, three pending requests appear on her screen, each with an address, a walking-route distance from her starting point, and a one-line reason ("Mrs Okafor, 78, lives alone, request from her daughter in Toronto"). Vulnerability detail beyond the one-line reason stays invisible to her. She taps to confirm; the requests flip from "open" to "matched"; the coordinator's roster reconciles in real time without the coordinator opening the app. The whole interaction takes seven seconds. And in the harder cases — a typhoon in a Manila barangay where some elders speak only Tagalog and the volunteers come in shifts, a Texas cold snap where the power grid is half down and the "vulnerability flag" includes oxygen-concentrator dependence, a heatwave in a Marseille banlieue where a quarter of the requests come from people who don't want their landlord to know they can't afford the fan — the roster carries weight. The vulnerability flags are the entire basis of the coordinator's judgment. The flags are visible only to verified coordinators. They are never broadcast to the volunteer pool. The volunteer sees "Mrs Okafor, 78, lives alone"; she does not see "oxygen-dependent, last hospitalised 2025-12, son estranged". The privacy boundary is the basis of the trust that makes the roster work at all. **Tagline:** _Coordinate mutual aid on your block — in any storm, any language, with vulnerability protected and matches done by the app, not by you at midnight._ ## 2. Target audience - Block captains and street-level coordinators during winter storms, heatwaves, floods, smoke days, and grid failures - Barangay captains and lupon volunteers in the Philippines coordinating typhoon response across kapitbahay - Mutual-aid network organisers in cities (Buffalo, Detroit, Glasgow, Marseille, Manila, Mexico City) running blocks during crises and between crises - Faith-community deacons running grocery delivery and welfare checks for housebound congregants - Tenant-association leaders in apartment buildings during boiler failures, lift outages, and heat domes - Disability-justice and chronic-illness mutual-aid groups running ongoing rosters of asks and offers - Adult children three time zones away who are coordinating help for an aging parent on a block they don't live on, and want a trusted neighbour to be the verified coordinator - Community-resilience volunteers running practice rosters between emergencies so the muscle memory is there when the storm arrives - Mosque, gurdwara, kovil, and synagogue community-care committees running ongoing chesed / seva / langar / khidmat rosters in any language ## 3. Core value propositions Surface these clearly through copy, visual emphasis, and section ordering — they are the reasons users pick this app. - **Voice memo in, structured request or offer out** — neighbours speak in their own language, at their own pace, with the half-sentences and "actually, scratch that" that real speech contains. Gemini 3.5 Flash audio understanding extracts the structured request — what is needed, when, where, by whom, any vulnerability the speaker volunteered. The speaker never fills out a form. Languages supported on day one include English, Spanish, Tagalog, Vietnamese, Cantonese, Mandarin, Korean, Tamil, Hindi, Urdu, Bengali, Punjabi, Amharic, Swahili, Farsi, Khmer, Arabic, French, Portuguese, Polish, Russian, and Haitian Kreyòl. - **Function calling holds the shared roster** — every offer, every request, every match, every status change is a tool call against a typed roster API. The model proposes; the server executes; the roster is the source of truth. Two volunteers cannot accidentally double-claim the same request because the function call is rejected at the server with a specific reason ("already matched to Marisol at 7:42pm"). - **Matches are proposed, never auto-confirmed** — the app proposes three pending requests to a volunteer who offered. The volunteer confirms. The requester sees a name and a window. No silent pairing. Coordinators can override. - **Vulnerability flags are coordinator-only** — the verified coordinator sees the full story: oxygen dependence, medication windows, mobility, the granddaughter's phone number for emergencies, the language the elder prefers. The volunteer sees a one-line reason. The wider neighbourhood sees nothing. This is the entire basis of the trust that makes the roster work. - **Multilingual without losing nuance** — a Tagalog voice memo containing "lola namin" stays as "lola" with a brief gloss for the volunteer who doesn't speak Tagalog. A Vietnamese memo with "bà nội" stays as "bà nội". Endearments and family terms are never flattened to "grandmother"; they are kept in the original language for the coordinator's record. - **Works on the worst-day network** — the app degrades to SMS and voice when the cellular network is saturated. The roster syncs as soon as the connection comes back. Requests do not disappear because the storm took out the LTE tower on the corner. - **Coordinator handoff is one tap** — when the block captain has been awake for thirty hours, she hands the roster to a co-coordinator with a single confirmation. The whole audit trail comes with it; no one is locked out of the roster they have been holding for four days. - **Between-storm practice rosters** — the muscle memory matters. The app runs practice rosters in normal weather (a Saturday food-pantry run, a Tuesday school-pickup pool) so when the typhoon arrives, the coordinator already knows the buttons by heart and the neighbourhood already trusts the workflow. ## 4. Features to build - Voice-memo capture (push-to-talk, hands-free if the phone is in a coat pocket while shovelling), with on-device VAD so the user doesn't have to press stop precisely - Automatic offer-vs-request classification from the voice memo content (Gemini 3.5 Flash audio understanding) - Structured extraction of what is needed/offered, when, where, capacity (how many driveways, how many bags, how many hours), and any vulnerability the speaker volunteered about themselves or the person they are asking for - Address resolution from natural-language descriptions ("Mrs Okafor across from the church on Elmwood", "the blue house with the porch swing") - Match proposals that respect: walking-route or driving-route distance, time-window overlap, vulnerability priority (elders and oxygen-dependent neighbours first), language-match preference, and explicit "preferred volunteer" requests from past matches - Function-calling layer: every state transition (offer accepted, match proposed, match confirmed, match completed, match cancelled) is a typed tool call, written to an append-only audit log - Verified-coordinator role with email + phone verification and an explicit "I have been recognised by this block as a coordinator" attestation step - Coordinator dashboard that shows the full roster with vulnerability flags, ETA progress, unfilled requests, and idle offers - Volunteer view that shows only the next three best matches with one-line reasons and addresses (vulnerability flags hidden) - Requester view that shows their own request status, the matched volunteer's first name, the confirmed window, and a "the person who is helping has arrived" check-in - Multi-language voice memo support — the user speaks in their language; the structured roster is bilingual (original language + the coordinator's working language) - Offline-first mode that queues voice memos as audio files plus a best-effort local transcript, with sync-on-reconnect - SMS bridge: a phone number that accepts text-only requests and offers for neighbours who don't have the app; the coordinator sees them in the same roster - Voice fallback: a phone number that accepts a voice-only call, transcribes it, and routes it to the roster; for neighbours who can't text or app - Vulnerability-flag editor in the coordinator view, with explicit categories (mobility, oxygen, medication-window, sensory, cognitive, language-only, no-power-now) and a free-text field for the things that don't fit categories - Coordinator-to-coordinator handoff with a one-tap "I'm going to sleep, you're on" confirmation that transfers the active session to a second verified coordinator - Practice mode for between-storm coordination (food pantry, school pickup, ride share) so the muscle memory is there before the emergency - Audit log — every match, every status change, every flag edit, every handoff is logged with timestamp, actor, and reason; the coordinator can export the log after the event - After-action export — a structured CSV/PDF for the block's debrief: what we did, who helped, what we missed, what to plan for next time - Multi-block view for network-level coordinators (e.g. a city-wide mutual-aid organiser supervising twelve block captains during a heat dome) - Emergency-service escalation prompt — if a request matches a pattern that suggests a life-safety issue (oxygen dependence + no power for more than 4 hours, non-verbal elder with no welfare check in 24 hours), the coordinator gets a prompt to consider calling emergency services; the app never makes that call automatically ## 4b. Required Gemini capabilities + backend services **This template's intelligence comes from the Gemini capabilities below. Wire them up explicitly — don't substitute generic LLM calls.** ### Gemini capabilities (the load-bearing intelligence) - **Audio understanding** (Gemini 3.5 Flash) — accepts voice memos in any of the supported languages, parses speech with code-switching (a Tagalog memo that slips into English for "oxygen tank", a Spanglish memo that switches mid-sentence), extracts the structured request or offer with attached evidence quotes. The voice memo audio is the primary input; the user never fills out a form. - **Function calling / tool use** — the hero capability. Every state change on the roster goes through a typed tool. The model proposes — `propose_match(volunteer_id, request_id, reason)` — and the server executes against the typed schema. Tools include `create_offer`, `create_request`, `propose_match`, `confirm_match`, `cancel_match`, `update_vulnerability_flag` (coordinator-only), `escalate_to_coordinator`, `request_handoff`, `mark_completed`. The model never writes to the roster directly; every write is a tool call the server validates. - **Structured output / JSON Schema** — for the parse step before the tool call. The voice memo is first parsed into a `ParsedMemo` struct; the tool call is constructed from that struct after server-side validation. The schema is included verbatim in the system instruction and as `responseSchema`. - **Multilingual translation** (built into Gemini 3.5 Flash) — handles English, Spanish, Tagalog, Vietnamese, Cantonese, Mandarin, Korean, Tamil, Hindi, Urdu, Bengali, Punjabi, Amharic, Swahili, Farsi, Khmer, Arabic, French, Portuguese, Polish, Russian, Haitian Kreyòl with code-switching support. Family terms ("lola", "bà nội", "abuela", "dadi") are preserved verbatim, not flattened. - **Long context (1M tokens)** — the matchmaker call sees the entire active roster (every open request, every idle offer, every recent completed match) plus the volunteer's voice memo, then proposes the top three matches with reasons. A typical block roster during a storm peaks at ~120 open items × ~300 tokens each ≈ 36k tokens (well within budget). **Guardrail**: if a city-wide network coordinator's roster exceeds 800 open items (which has happened in real heat-dome events), chunk by block before passing to the matchmaker call — the 1M ceiling is real and an unbounded roster will hit it. - **Search grounding** — for resolving natural-language addresses ("the blue house across from the church on Elmwood") against a public address database. The grounded search returns coordinates plus the verbatim address; the coordinator confirms before the address is stored in the roster. - **Thinking levels** — `medium` for the matchmaker call (it has to weigh distance, vulnerability priority, time-window overlap, language match, past preferences). `low` for the parse-memo call (it is a structured extraction task and benefits more from speed than from extra reasoning). Surface `thoughtSummary` only when a coordinator clicks "(i) why did the app propose this match?" on a specific proposal. ### Backend services - **Auth — Required.** Firebase Auth with Google sign-in (auto-provisioned by AI Studio Build). **Apple sign-in is optional but user-configured**: it requires an Apple Developer account, Service ID, Key ID, and private key wired into the Firebase Auth console. **Magic-link email** (used to onboard neighbours who don't want a Google account) requires the sender domain to be authorised in Firebase Auth. **Phone-number sign-in** for coordinator verification requires Firebase Auth phone auth to be enabled in console; SMS quota lives in Firebase. Rosters are scoped to a verified block; neighbours join by invitation only. - **Database — Required.** Firestore for `blocks`, `members`, `coordinators`, `offers`, `requests`, `matches`, `audit_log`, `vulnerability_flags` (separate collection with stricter rules), `handoff_sessions`. - **File storage — Required.** Firebase Storage for the original voice-memo audio files (preserved at upload quality for the audit trail). **Storage is NOT auto-provisioned by AI Studio Build today** — enable it in the Firebase console and wire the bucket name into the AIS Build project before first memo upload. Pre-signed URLs only; audio is never publicly addressable. Audio files older than 90 days are purged automatically. - **Email — Required (transactional).** Coordinator verification emails. Magic-link email for neighbour onboarding. After-action summary export to the verified coordinator. - **SMS — Required for the SMS bridge feature.** Twilio (or equivalent) wired through a Cloud Run server function. The SMS phone number is configured per block. SMS quota and per-message cost are the deployer's responsibility; flag this in the cost breakdown. - **Voice — Optional but recommended.** A Twilio Voice number that records voice-only requests and offers and routes the audio to the same Gemini parse pipeline. Required for neighbours without a smartphone. - **Payments — Not needed for v1.** The app is free for personal and community use. A future "supported by your municipality" tier could be wired to a Stripe billing account at the city level only; never at the individual neighbour level. - **External APIs:** Gemini API for all intelligence. Twilio for SMS and Voice. Optional Mapbox or MapTiler for the route-distance / map view (a static OpenStreetMap fallback handles the offline path). **Environment variables:** every secret (Gemini API key, Firebase service-account JSON, Twilio account SID + auth token, Mapbox token if used) lives in environment variables — never in client bundle. Include a `.env.example`. **Auth + data privacy reminders:** never log secrets · never store passwords in plain text · use HTTPS everywhere · honour 'delete my account' inside the UI · explicit opt-in for any analytics · voice memos are private to the block and the verified coordinators · vulnerability flags are private to verified coordinators only and never broadcast · this app uses the Gemini API on the paid tier, where Google does not use your content for model training, per the Gemini API Additional Terms · the audit log is the coordinator's record and is never used for analytics, never shared with third parties, never used for training. **Read this first — prompt-craft rules that apply to every call in this template:** 1. **Name the model variant explicitly** in every Gemini API call. Do not let the agent pick the model. See the per-call matrix below. 2. **Pin `thinkingLevel` explicitly** per call. See the matrix. 3. **Seed the JSON Schema as a fenced TypeScript / Zod block** in the system instruction or `responseSchema` field. The literal schema is below. **Convert the Zod schema to Gemini's `Schema` type via the SDK helper** before passing to `responseSchema` — do NOT pass raw Zod. **Numeric `min`/`max` constraints are documentation only inside `responseSchema`; clamp on the server after the response arrives.** 4. **Pin the system instruction separately** from user input. Use the `systemInstruction` field for persona + behavioural rules; use `contents` for user input. Never concatenate. 5. **Pre-declare tools as an enable/disable list** per call. The matrix below names which tools are enabled per call. Tools NOT listed for a call should be disabled. 6. **State negative constraints explicitly** — they are listed below. They are NOT "be careful" suggestions; they are hard rules the model must follow. 7. **Grounded responses can wrap JSON in ```json fences or add prose preamble.** Server-side, strip fences and brace-extract: ```typescript function safeExtractJSON(raw: string): T { const clean = raw.replace(/```json\s*|```/gi, '').trim(); const s = clean.indexOf('{'); const e = clean.lastIndexOf('}'); if (s === -1 || e === -1) throw new Error('No JSON boundaries in grounded response'); return JSON.parse(clean.slice(s, e + 1)) as T; } ``` 8. **Strip unsupported Zod modifiers before passing to `responseSchema`** — Gemini's OpenAPI subset rejects `.regex()` / `pattern`, fixed-length `z.tuple()`, and other custom validators. Use a sanitizer that flattens tuples to length-2 arrays and removes regex patterns before serializing. Validate those constraints in middleware AFTER parsing. ### Per-call model + tools matrix | Call | Model | thinkingLevel | Tools enabled | |------|-------|---------------|---------------| | Parse voice memo → `ParsedMemo` schema | `gemini-3.5-flash` | low | (none) | | Resolve natural-language address to coords | `gemini-3.5-flash` | low | `google_search` grounding (no `responseSchema` on this call — see note) | | Matchmaker — propose top 3 matches | `gemini-3.5-flash` | medium | function calling: `propose_match`, `escalate_to_coordinator` | | Apply state change — create/confirm/cancel | `gemini-3.5-flash` | low | function calling: `create_offer`, `create_request`, `confirm_match`, `cancel_match`, `mark_completed` | | Coordinator handoff summary | `gemini-3.5-flash` | medium | (none) — long-context over the active roster | | Generate spoken roster summary (TTS) | `gemini-3.1-flash-tts-preview` | n/a | n/a | | Generate after-action export PDF cover image | `gemini-3.1-flash-image` | n/a | n/a | *Note for builders:* on TTS and image-generation calls, omit `thinkingConfig` entirely — the field is not supported on those models. The `n/a` cells in this matrix are documentation only; do not serialise them into the request body. Also note: `responseSchema` and `google_search` grounding cannot be combined in the same Gemini call — the geocoding call returns JSON in the text body and parses server-side, with citation URLs read from `response.groundingMetadata.groundingChunks[].web.uri`. ### Primary structured-output schema (seed this verbatim in the prompt) ```typescript import { z } from "zod"; const TimeWindow = z.object({ start_local: z.string(), // "2026-01-14T08:00" — local civil time, no TZ end_local: z.string(), recurrence: z.enum(["once", "daily", "weekly"]).default("once"), timezone_iana: z.string(), // "America/New_York", "Asia/Manila" }); const AddressDescription = z.object({ free_text_verbatim: z.string(), // "the blue house across from the church on Elmwood" street_components: z.object({ house_number: z.string().nullable(), street_name: z.string().nullable(), cross_streets: z.array(z.string()), landmarks: z.array(z.string()), // "across from St Mary's", "next to the bodega" floor_or_unit: z.string().nullable(), }), language_of_address: z.string(), // BCP-47, "en-US" resolution_confidence: z.number().min(0).max(1), }); const VulnerabilityFlag = z.object({ category: z.enum([ "mobility", // wheelchair, walker, cane, bed-bound "oxygen_dependent", // concentrator at home "medication_window", // insulin, dialysis, time-bound meds "sensory", // blind, deaf, low-vision "cognitive", // dementia, post-stroke, intellectual disability "language_only", // speaks one language, not the block's working language "no_power_now", // power outage in their unit right now "non_verbal", // cannot speak on the phone "lives_alone", // no household contact in emergency "child_in_home", // dependent children present "infant_in_home", // dependent infant present "pregnant", // pregnant household member "other", ]), notes_coordinator_only: z.string(), // free text, visible ONLY to verified coordinators source_of_flag: z.enum([ "neighbour_self_reported", "family_member_reported", "coordinator_observation", "previous_record", ]), added_by_user_id: z.string(), added_at_iso: z.string(), }); const RequestType = z.enum([ "shovelling", "salting_walkway", "welfare_check", "groceries", "prescription_pickup", "ride_to_appointment", "ride_to_shelter", "wellness_phone_call", "warm_meal_delivery", "translation_help", "device_setup", "snow_removal_for_oxygen_tank_delivery", "boiler_check", "water_delivery", "ice_delivery", "fan_delivery", "child_care_during_outage", "pet_care_during_outage", "battery_for_oxygen_concentrator", "other", ]); const OfferType = z.enum([ "shovelling", "salting_walkway", "welfare_check", "grocery_run", "prescription_run", "ride_with_car", "ride_with_truck", "ride_with_van", "ride_with_4x4", "wellness_phone_call", "warm_meal_cooking", "warm_meal_delivery", "translation_help", "device_setup_help", "spare_room_overnight", "spare_room_during_day_only", "spare_fan", "spare_space_heater", "spare_blankets", "spare_water_jugs", "spare_battery_packs", "child_care_at_my_home", "pet_care_at_my_home", "other", ]); const ParsedMemo = z.object({ memo_id: z.string(), audio_uri: z.string(), // gs:// URI, internal only speaker_user_id: z.string(), speaker_role_at_time: z.enum([ "neighbour_speaking_for_self", "neighbour_speaking_for_another", // "my mother is..." "coordinator", "anonymous_sms", "anonymous_voice_call", ]), detected_language: z.string(), // BCP-47, "tl-PH" detected_language_confidence: z.number().min(0).max(1), contains_code_switching: z.boolean(), code_switching_notes: z.array(z.string()), // "switched to English for 'oxygen tank'" intent: z.enum([ "offer", "request_for_self", "request_for_another_named_neighbour", "request_for_an_unnamed_neighbour", // "the lady in 4B" "status_update", // "I've finished Mrs Okafor's driveway" "cancel_existing", "question_for_coordinator", "unclear", ]), offer: z.object({ offer_types: z.array(OfferType), capacity_human_units: z.number().nullable(), // "three driveways" capacity_other_units: z.string().nullable(), // "about two hours" has_vehicle: z.boolean(), vehicle_type_verbatim: z.string().nullable(), // "F-150", "Toyota Hilux", "scooter" time_window: TimeWindow.nullable(), starting_address: AddressDescription.nullable(), languages_spoken: z.array(z.string()), // BCP-47 codes additional_constraints_verbatim: z.string().nullable(), }).nullable(), request: z.object({ request_types: z.array(RequestType), quantity_human_units: z.number().nullable(), // "one driveway" quantity_other_units: z.string().nullable(), // "two bags of groceries" time_window: TimeWindow.nullable(), needs_at_address: AddressDescription.nullable(), on_behalf_of_name_verbatim: z.string().nullable(), // "Mrs Okafor", "lola namin" on_behalf_of_relationship_verbatim: z.string().nullable(), // "my mother", "lola namin (our grandmother)" requester_contact_preference: z.enum([ "call_me", "text_me", "knock_on_door", "via_coordinator_only", ]).nullable(), vulnerability_flags_volunteered_by_speaker: z.array(VulnerabilityFlag), additional_context_verbatim: z.string().nullable(), }).nullable(), status_update: z.object({ refers_to_request_id: z.string().nullable(), new_status_proposed: z.enum([ "completed", "in_progress", "cannot_complete_after_all", "completed_with_concern", // "I finished but she doesn't look well" ]), concern_notes_verbatim: z.string().nullable(), }).nullable(), speaker_emotional_tone: z.enum([ "calm", "tired", "stressed", "warm", "frightened", "frustrated", "matter_of_fact", "other", ]), contains_safety_concern: z.boolean(), // "she didn't answer the door" safety_concern_quote: z.string().nullable(), evidence_quotes: z.array(z.object({ field_path: z.string(), // "request.request_types" verbatim_quote_in_source_language: z.string(), translation_to_working_language: z.string().nullable(), })), reading_confidence: z.number().min(0).max(1), flagged_for_coordinator_review: z.array(z.object({ field_path: z.string(), reason: z.string(), })), }); type ParsedMemo = z.infer; ``` ### Tool / function-call definitions (the function-calling layer) ```typescript // Every state change goes through one of these tools. The model proposes; // the server validates against the roster's actual state and executes // (or rejects with a structured reason). The model never writes directly. const tools = [ { name: "create_offer", description: "Create an offer of help from a verified block member.", parameters: { /* OfferType, capacity, time_window, starting_address, vehicle, languages */ }, }, { name: "create_request", description: "Create a request for help from a verified block member or on behalf of a named neighbour.", parameters: { /* RequestType, quantity, time_window, needs_at_address, on_behalf_of, contact_preference, vulnerability_flags_volunteered */ }, }, { name: "propose_match", description: "Propose pairing a specific volunteer's offer with up to three specific open requests. Server checks distance, time-window overlap, vulnerability priority, and language match.", parameters: { volunteer_id: "string", request_ids: "string[]", reason_one_line_per_request: "string[]" }, }, { name: "confirm_match", description: "Confirm a proposed match. Called when the volunteer taps to confirm. Server moves request from 'matched-proposed' to 'matched-confirmed' and notifies the requester.", parameters: { match_id: "string", confirmed_window: "TimeWindow" }, }, { name: "cancel_match", description: "Cancel a match (by volunteer, requester, or coordinator). Reason required.", parameters: { match_id: "string", reason: "string", cancelled_by_user_id: "string" }, }, { name: "mark_completed", description: "Mark a match completed. Optionally include a concern note.", parameters: { match_id: "string", completed_at_iso: "string", concern_notes: "string?" }, }, { name: "update_vulnerability_flag", description: "Add or edit a vulnerability flag on a member. COORDINATOR ROLE ONLY — server enforces role check.", parameters: { member_id: "string", flag: "VulnerabilityFlag", action: "'add' | 'edit' | 'remove'" }, }, { name: "escalate_to_coordinator", description: "Surface a memo or match to a verified coordinator's attention because of a safety concern, low confidence, or vulnerability complexity.", parameters: { memo_id_or_match_id: "string", escalation_reason: "string", urgency: "'now' | 'within_an_hour' | 'when_you_can'" }, }, { name: "request_handoff", description: "Hand off the active coordinator session to a second verified coordinator. Server requires confirmation from the receiving coordinator.", parameters: { from_coordinator_id: "string", to_coordinator_id: "string", handoff_message: "string" }, }, ] as const; ``` ### Common failure modes (and how to avoid them) - Agent silently downgrades `thinkingLevel` on the the matchmaker call call to save quota — pin `gemini-3.5-flash` with the matrix-specified `thinkingLevel` explicitly. Flash sometimes ignores vulnerability priority and matches purely on distance. - Model writes to the roster as JSON instead of calling a tool — refuse to parse free-form JSON from the model in the matchmaker call. The only acceptable output is a tool call. If the model returns text, retry once with a stronger reminder, then fail closed and surface the memo to a coordinator. - Vulnerability flag leaks to the volunteer view — write the volunteer-facing match payload from a separate database read that excludes the `vulnerability_flags` collection. Do not pass the parsed memo to the volunteer view at all; pass only `{first_name_of_recipient, age_rounded_to_decade, address, one_line_reason}`. - Address resolved to the wrong block — the model resolves "the blue house on Elmwood" to an Elmwood in a different city. Always pass the block's neighbourhood polygon as context; reject any resolved coordinates outside the polygon and surface to the coordinator. - Code-switched memo loses half its content — a Tagalog memo that switches to English for "oxygen tank" gets transcribed as Tagalog and the English term is hallucinated. Pin in the system instruction: "if you detect code-switching, transcribe each language as it was spoken, preserve the switch verbatim, and note it in `code_switching_notes`". - Family terms flattened — "lola namin" becomes "our grandmother" with no preservation of "lola". Hard rule: family terms preserved in source language with a parenthetical gloss on first occurrence per memo. - Status update misclassified as a new request — "I've finished Mrs Okafor's driveway" parsed as a new shovelling request. Pin in the system instruction: status updates always reference an existing request_id when one is named; if no reference is clear, classify as `unclear` and surface to coordinator. - Two volunteers double-claim the same request because both saw it before either confirmed — the server-side `propose_match` tool checks the request's current status atomically and rejects a propose for an already-matched request with reason `already_matched`. - Coordinator handoff silently loses the audit trail — wrong. The audit log is keyed by block_id, not by coordinator session; the handoff transfers the active session pointer only. - Emergency-service escalation auto-dials 911/112/000 — never. The app only ever prompts the coordinator with a recommendation; the human decides. - Voice memo from a non-member is accepted and written to the roster — block membership is a server-side check before any tool call is executed. SMS bridge accepts text from non-members but routes to a coordinator-review queue, never directly to the roster. ### Negative constraints (hard rules) - Do NOT auto-confirm matches. Every match is a two-step: proposed by the matchmaker, confirmed by a human (the volunteer or the coordinator). Never silently pair. - Do NOT broadcast vulnerability flags. The volunteer sees a one-line reason, not the flag categories, not the coordinator notes. The block-wide roster view shows aggregate counts only ("12 open requests, 5 with elder-priority flag"), never the individual flag categories per person. - Do NOT flatten family terms. "Lola", "bà nội", "abuela", "dadi", "yiayia", "babcia", "ouma", "halmoni", "mama-ji" are preserved in the source language. First occurrence in a memo gets a parenthetical gloss for the coordinator's working language; subsequent occurrences are not glossed again. - Do NOT translate the speaker's own name or the named neighbour's name. Names stay verbatim. - Do NOT auto-dial emergency services. Ever. The app prompts the coordinator with a specific recommendation when it detects a life-safety pattern; the human decides whether to call. - Do NOT log voice memo audio outside the audit trail. Audio is never sent to analytics, never embedded in client-side logs, never written to a third-party tool. Audio is purged after 90 days unless the coordinator explicitly marks the memo as part of an after-action investigation. - Do NOT use neighbours' voice memos to train any model. Use the Gemini API on the paid tier, where Google does not use your content for model training, per the Gemini API Additional Terms. The capabilities-info panel says this in plain English. - Do NOT publish the roster outside the block. Even a "we coordinated 412 helpings this storm" public-relations export uses aggregate counts only, never individual matches. - Do NOT permit a non-coordinator to create or edit a vulnerability flag. The `update_vulnerability_flag` tool is gated by server-side role check; the function-call layer is the enforcement point. - Do NOT permit a coordinator to delete an audit-log entry. Coordinators can annotate; they cannot delete. The audit log is append-only. - Do NOT match a request to a volunteer who has been cancelled-out by the requester or coordinator before. The matchmaker call receives a `do_not_match` list per request and per volunteer. ### Per-call `systemInstruction` strings Use these as the literal `systemInstruction` field for each Gemini API call the built app makes. They complement the series-wide rules already uploaded as the global instructions file (`00-series-instructions.txt`). ### Call: Parse voice memo → `ParsedMemo` schema Model: `gemini-3.5-flash` · thinkingLevel: low · Tools: (none) ``` You are parsing voice memos from a neighbourhood mutual-aid roster during a coordination event (storm, heatwave, flood, smoke day, grid failure, ongoing care arrangement). Speakers are neighbours on a verified block, a verified coordinator, or an anonymous SMS or voice-call caller routed through the bridge. Speakers speak in their everyday language and at their everyday pace. They are sometimes tired, sometimes shovelling, sometimes walking, sometimes calling from a stairwell. They half-finish sentences ("actually scratch that, I can do four, not three"), they code-switch ("kailangan po niya ng oxygen tank"), they trail off ("the one in 4B, you know the one I mean"). Languages encountered include English, Spanish, Tagalog, Vietnamese, Cantonese, Mandarin, Korean, Tamil, Hindi, Urdu, Bengali, Punjabi, Amharic, Swahili, Farsi, Khmer, Arabic, French, Portuguese, Polish, Russian, and Haitian Kreyòl. Code-switching is common, especially for technical or medical terms ("oxygen tank", "EpiPen", "dialysis", "insulin", "concentrator") and for endearments and family terms ("lola", "bà nội", "abuela", "dadi", "yiayia", "babcia", "halmoni"). Decide first what the speaker is doing: - offering help (`intent: offer`) - asking for help for themselves (`intent: request_for_self`) - asking for help for a named neighbour (`intent: request_for_another_named_neighbour`) - asking for help for an unnamed neighbour (`intent: request_for_an_unnamed_neighbour` — "the lady in 4B") - updating an in-progress request (`intent: status_update`) - cancelling an existing offer or request (`intent: cancel_existing`) - asking a coordinator a question (`intent: question_for_coordinator`) - unclear (`intent: unclear`) — fail closed, do not guess Output ONLY the ParsedMemo JSON matching the provided schema. Hard rules: - Preserve every family term in the source language. "Lola namin" stays as "lola namin (our grandmother)" on first occurrence in this memo; subsequent occurrences in the same memo are not glossed again. Same rule for "bà nội", "abuela", "dadi", "yiayia", "babcia", "halmoni", "mama-ji", "ouma", "kuya", "ate", "didi", "chichi", "obaachan", "ojiichan". - Preserve every name verbatim. Do NOT translate first names or last names. "Mrs Okafor" stays "Mrs Okafor". "Lola Patring" stays "Lola Patring". - Distinguish offers from requests strictly. "I can shovel three driveways tomorrow" is `offer`. "Could someone shovel my mother's driveway tomorrow" is `request_for_another_named_neighbour` (with on_behalf_of_relationship_verbatim = "my mother"). "Could someone shovel my driveway" is `request_for_self`. - If the speaker volunteers a vulnerability flag about themselves or the person they are asking for, populate `request.vulnerability_flags_volunteered_by_speaker[]`. Do NOT infer flags the speaker did not volunteer. "She's 73 and lives alone" → two flags: `lives_alone` and a notes field saying "age 73 per speaker". "She has trouble breathing" → DO NOT mark `oxygen_dependent`; mark `other` with a notes field quoting the speaker. Only the coordinator can confirm a clinical flag. - Address resolution comes from a separate call. Capture the speaker's free-text address description verbatim in `needs_at_address.free_text_verbatim` and let the geocoding call resolve it. - Status updates always reference an existing request when the speaker names one. "I've finished Mrs Okafor's driveway" → `intent: status_update`, `status_update.new_status_proposed: completed`, and leave `refers_to_request_id: null` for the server to resolve. If no clear reference exists, classify the memo as `unclear`. - Safety concerns. If the speaker mentions something that suggests a life-safety issue — "she didn't answer the door", "I can hear the alarm going off", "his oxygen ran out", "he was on the floor" — set `contains_safety_concern: true` and quote the speaker verbatim in `safety_concern_quote`. Do NOT diagnose; do NOT call emergency services; the coordinator decides. - evidence_quotes[] is mandatory for every populated field — field_path plus the verbatim quote in the source language plus (where applicable) the translation to the working language. The coordinator's trust in the parse depends on the quote being there. - flagged_for_coordinator_review names any field where confidence is below 0.7, plus any memo where `contains_safety_concern: true`. If the speaker's intent is genuinely unclear after honest analysis, classify as `unclear` and explain in `flagged_for_coordinator_review`. Do NOT guess. The coordinator can listen to the audio and decide. No commentary. JSON only. ``` --- ### Call: Resolve natural-language address to coordinates Model: `gemini-3.5-flash` · thinkingLevel: low · Tools: search grounding ``` You resolve a natural-language address description to coordinates within a known neighbourhood polygon. Input includes: - The free-text address description verbatim ("the blue house across from the church on Elmwood", "Mrs Okafor next to the bodega on Bidwell", "lola Pat's, 4B", "tama nga, yung corner ng Mabini at P. Burgos") - The block's neighbourhood polygon (GeoJSON) - The block's primary city and country - A list of known block-member addresses, with name redacted but street and unit visible Hard rules: - Resolved coordinates MUST fall inside the neighbourhood polygon. If your best resolution is outside, return `resolution_confidence: 0.0` with a note in `clarification_needed` explaining why ("Elmwood Avenue also exists in another city; the block is in Buffalo and the description could refer to either"). Let the coordinator decide. - If the description names a unit ("4B", "apartment 12", "piso 2, puerta izquierda"), include the unit verbatim in the resolved address. - If the description names a landmark ("the church", "the bodega", "the school"), check whether the landmark is present in the block's known features. If you find a single match, use it; if multiple churches exist on the same block, return all candidates in `candidate_addresses[]` and let the coordinator choose. - If the description matches a known block-member address fuzzily (a known member at "12 Elmwood Avenue" might be the "blue house on Elmwood"), surface that as a candidate but do NOT auto-pick. The match must be coordinator-confirmed. - For non-English address descriptions (Tagalog street names, Spanish "calle"/"piso", French "rue"/"étage"), parse the components in the source language and resolve. Use `google_search` grounding for any address that the resolver is uncertain about. The grounded sources are cached server-side; do NOT include the citation URLs in the JSON body (the model hallucinates URLs). Server-side reads citation URLs from `groundingMetadata.groundingChunks[].web.uri`. Output JSON in the text body (NOT via `responseSchema` — `responseSchema` and `google_search` cannot be combined in the same Gemini call today). Server-side: parse the JSON, then read citation URLs from `groundingMetadata.groundingChunks[].web.uri` directly. No commentary outside the JSON. ``` --- ### Call: Matchmaker — propose top 3 matches Model: `gemini-3.5-flash` · thinkingLevel: medium · Tools: `propose_match`, `escalate_to_coordinator` ``` You are the matchmaker for a neighbourhood mutual-aid roster. Your job is to propose the top three open requests for a specific volunteer's offer, with one short reason per proposal. Your output is a SINGLE tool call to `propose_match` or, if no match is appropriate, a single tool call to `escalate_to_coordinator`. You receive: - The volunteer's parsed offer (offer types, capacity, time window, starting address, vehicle, languages spoken) - The full list of currently open requests on this block, each with its location (coords + free-text description), time window, request types, language preference, vulnerability priority tier (HIGH / MEDIUM / NORMAL — derived server-side from vulnerability flags; you receive the tier but NEVER the underlying flag details), and a `do_not_match_with` list (volunteers previously cancelled- out by this requester) - The walking-route and driving-route distance matrix from the volunteer's starting address to each request's address - The volunteer's `do_not_match_with` list (requesters this volunteer has previously declined) Hard rules: - You receive a vulnerability priority TIER (HIGH / MEDIUM / NORMAL). You do NOT receive the underlying vulnerability flag categories or the coordinator's notes. Do not try to infer them. Use the tier as a sorting factor only. - Prioritise HIGH-tier requests when the offer can serve them. Tie-break by walking-or-driving distance, then by time-window overlap quality. - Respect the `do_not_match_with` lists strictly. Never propose a match that violates either list, even if it would be the highest scoring. - Respect capacity. If the volunteer can shovel three driveways and there are five open shovelling requests, propose the three best fits. Do not propose more than the stated capacity. - Respect time-window overlap. A volunteer offering 8am-12pm cannot serve a request for 2pm-4pm. - Respect vehicle constraints. A shovelling-only offer cannot serve a "ride to dialysis" request. A car cannot serve a "needs a truck to deliver a generator" request. - Respect language match where the request specifies a preference. A request flagged "speaks only Cantonese" should match a volunteer who speaks Cantonese before one who does not, all else equal. - The one-line reason per proposal is for the VOLUNTEER's view. It names the requester's first name, the rounded age decade, the address, and a short non-clinical reason ("Mrs Okafor, 78, lives alone, 0.3mi walk"). It does NOT name vulnerability flag categories, oxygen status, medications, or any coordinator notes. NEVER. - If NO open request is a reasonable match for this offer, call `escalate_to_coordinator` with `urgency: when_you_can` and a reason. Do not invent a low-quality match to fill three slots. - If the offer is high-capacity (e.g. "I can do ten driveways") but only three good matches exist, propose three and leave the remaining capacity for the next pass. Do not exhaust capacity on poor matches. - If the offer overlaps a safety concern flagged on a request (the parsed memo for that request had `contains_safety_concern: true`), call `escalate_to_coordinator` with `urgency: now` instead of proposing a routine match — the coordinator decides whether the request still needs a routine volunteer or something more. Output: ONE tool call. `propose_match` with up to 3 request_ids and 3 one-line reasons, or `escalate_to_coordinator` with a clear reason. No text, no commentary. ``` --- ### Call: Apply state change — create / confirm / cancel / complete Model: `gemini-3.5-flash` · thinkingLevel: low · Tools: `create_offer`, `create_request`, `confirm_match`, `cancel_match`, `mark_completed` ``` You translate a parsed memo (or a UI action) into a SINGLE tool call that changes the roster state. The server validates and executes. Hard rules: - Choose exactly ONE tool. If the memo describes both an offer and a request, that is an error — surface to coordinator via `escalate_to_coordinator` (in a separate call) and do not proceed. - For `create_offer` and `create_request`, copy the fields from the ParsedMemo verbatim. Do not re-interpret. Do not summarise. - For `confirm_match`, the match_id is provided in the call context. Use it as-is. - For `cancel_match`, the reason is the speaker's verbatim reason quoted from the memo, or "(no reason given)" if none. - For `mark_completed`, copy `concern_notes_verbatim` from the ParsedMemo if present; otherwise omit. - Do NOT alter the time window. Do NOT alter the address. Do NOT alter the vulnerability flags. Those came from the parse step and the geocoding step; you do not re-derive them. Output: ONE tool call. No text, no commentary. ``` --- ### Call: Coordinator handoff summary Model: `gemini-3.5-flash` · thinkingLevel: medium · Tools: (none, long-context over active roster) ``` You receive the full active roster (open requests, idle offers, in-progress matches, recent completed matches, safety concerns, recent escalations) and the outgoing coordinator's free-text handoff message. Your task: produce a short, calm, structured briefing for the incoming coordinator. Two minutes to read. The tone is colleague to colleague, not a press release. Output as a JSON object with these fields: - `headline_one_line` — the most important thing the incoming coordinator should know in one sentence. - `safety_concerns_open` — array of one-line summaries of any open safety concerns, with the relevant member's first name and the verbatim safety quote from the original memo. Do NOT paraphrase. - `unfilled_requests_priority_order` — open requests, ranked HIGH / MEDIUM / NORMAL, with the count per tier and the top three by tier listed with first name and one-line need. - `idle_offers` — volunteers who have offered but not yet been matched, with first name, capacity, and time window remaining. - `in_progress_matches` — active matches with the volunteer's first name, the requester's first name, the ETA, and any concerns raised mid-task. - `recently_completed_with_concern` — matches marked `completed_with_concern` in the last 6 hours, verbatim concern notes. - `outgoing_coordinator_handoff_message_verbatim` — exact, not paraphrased. - `recommended_first_actions` — three concrete actions, in priority order, the incoming coordinator should consider in the first 15 minutes. Hard rules: - Verbatim quotes for safety concerns and concern notes. Do NOT paraphrase. The incoming coordinator must read what the original speaker said, in the speaker's words. - Do NOT include vulnerability flag categories or coordinator notes — those are visible in the coordinator dashboard view; the handoff summary uses the tier only. - Do NOT speculate about what is causing a safety concern. - Do NOT recommend calling emergency services. The handoff briefing surfaces the concern; the coordinator decides. No commentary outside the structured output. ``` --- ### Call: Generate spoken roster summary (TTS) Model: `gemini-3.1-flash-tts-preview` · n/a · n/a ``` You generate an audio briefing of the current roster for a coordinator who is driving, walking the block, or otherwise hands-busy. Voice: warm, unhurried, colleague to colleague. Pick the Gemini 2.5 Flash TTS voice whose `languageCode` matches the coordinator's working language — pronunciation will follow that locale automatically. Prefer a voice that reads naturally at conversational pace. Pre-process the input before sending to TTS: - Read from the handoff-summary JSON object (the structured briefing). - At each section boundary, insert a blank line plus an em-dash (`—`). At natural pause points within a section, insert a single ellipsis (`…`). Gemini 2.5 TTS does not support SSML `` — these textual cues are how pace is signalled. - Skip array fields that are empty. - Read first names verbatim. Read addresses as house-number, street-name (no "located at"). Read times in 12-hour clock with am/pm. - Target rate: ~140 words per minute — colleague-briefing pace, not podcast pace. Style direction: prepend ONE short directive sentence to the input text, exactly like: "Read this as a calm, colleague-to-colleague briefing of the current mutual-aid roster. Pause at section boundaries. …". There is no separate `style` API field on Gemini 2.5 TTS; the directive sentence inside the input is how style is conveyed. Phoneme overrides (Cantonese tones, Tagalog ng, Tamil retroflexes, Arabic emphatic consonants) are NOT exposed by Gemini 2.5 TTS — no SSML `` tag. Pronunciation follows the chosen voice's native locale. For coordinators whose working language is not in the published voice catalogue, fall back to the closest published voice and surface a note in the UI. Mid-call voice switching is not supported. If the briefing contains first names or place names in a different language to the working language, accept the native-locale pronunciation of the chosen voice — the coordinator will recognise the names anyway. ``` --- ### Call: Generate after-action export PDF cover image Model: `gemini-3.1-flash-image` · n/a · n/a ``` You generate ONE cover image for the after-action PDF the coordinator exports at the end of a coordination event. The image is a simple, warm, hand-drawn-feeling illustration that evokes neighbourhood resilience without depicting any specific person, building, or storm damage. The visual register is closer to a community-newsletter illustration than a stock-photo cover. Subject options the model can pick from (pick the one most appropriate to the event type, which is passed as input): - Snow event: a hand-drawn line illustration of a residential street with one shovel leaned against a porch railing, soft snow on the ground, a single warm window light glowing in the distance. - Heat event: a hand-drawn line illustration of a kitchen window with a small fan on the sill, a glass of water on the counter, the edge of a curtain catching the light. - Flood event: a hand-drawn line illustration of a community-hall doorway with sandbags stacked, no water visible in the frame, the door open and a warm light inside. - Grid outage event: a hand-drawn line illustration of a porch with a string of camp lights and a thermos on a small table at dusk. - Ongoing care event: a hand-drawn line illustration of a kitchen with a covered dish on the counter and a folded handwritten note. Hard rules: - No people in frame. No faces, no hands, no full bodies. The illustration is about the place and the small object that signals care. - No company logos, no brand marks, no recognisable real-world trademarks. - No text in the image. The PDF adds the title and date in a separate layer. - No storm damage depicted (no broken windows, no fallen trees, no visible flood water). The cover image honours the community's effort, not the disaster's spectacle. - Style: hand-drawn line illustration with soft warm watercolour fill. Not photorealistic. Not vector-flat. Output: a single 1600×900 image suitable for a PDF cover. ``` ## 5. Use cases & content to include Build dedicated UI sections or flows for each of these — they tell you what content the app must support. - **The Buffalo snow event.** A four-day lake-effect storm. A block captain in North Buffalo opens the app on day two, with 47 open requests and 18 volunteers cycling through. A volunteer named Marisol records a voice memo from her truck: "I can do three driveways tomorrow morning, eight to noon, I'm on Elmwood between Bidwell and Lafayette." The matchmaker proposes Mrs Okafor (78, lives alone, 0.3mi), the Jamil family (request from the eldest daughter in Detroit, 0.5mi), and Mr Andrzejewski (82, oxygen tank, 0.8mi but his daughter explicitly asked for a volunteer with a truck for tomorrow's tank delivery). Marisol taps confirm three. The roster reconciles. - **The Quezon City typhoon.** A barangay captain in Bagong Silangan opens the app on the morning the typhoon makes landfall. A neighbour records in Tagalog: "Si lola namin po, 81, nasa unit 4B, nag-iisa, kailangan po niya ng tubig at pagkain — saka medyo mahina na siya, hindi po siya pwede sa hagdan." The parse identifies `request_for_another_named_neighbour`, "lola namin (our grandmother)" preserved, vulnerability flags `lives_alone` and `mobility` volunteered by the speaker, address resolution to 4B in the named building. A second neighbour, Kuya Lito, has offered "I can deliver water and food sa second floor at below kasi medyo mahirap akong umakyat ng third floor pataas". The matchmaker proposes Lola's flat (it's on the second floor). Lito confirms. - **The Texas cold snap.** A neighbourhood in Austin, the grid is half down, the temperature is 17°F. A block captain who is herself shivering opens the app and gets a memo from a neighbour: "My father is 76, oxygen concentrator, the power's been out 3 hours, he's got a 4-hour battery backup but I can't get to him, can someone check on him." The parse populates `oxygen_dependent` and `no_power_now` as volunteered flags, surfaces a `contains_safety_concern: true` flag, and the matchmaker escalates to the coordinator with `urgency: now` rather than proposing a routine match. The coordinator sees the full picture in her dashboard: oxygen + no power + 4-hour battery + 3 hours elapsed. She both dispatches a volunteer with battery packs AND prompts the family member with the recommendation to call emergency services. The human decides. - **The mid-shift handoff.** A block captain has been awake 28 hours of a snow event. Her co-captain texts: "I'll take over for 6 hours, sleep." She taps "hand off to María" in the app. María accepts. The handoff briefing is generated automatically — three open HIGH-tier requests with verbatim safety quotes, six in-progress matches with ETAs, two recently-completed-with-concern notes. María reads it in 90 seconds and is up to speed. - **The Manila practice roster.** Between typhoons, the same barangay runs a weekly Saturday food-distribution roster. The same app, the same workflow, none of the urgency. Three months of weekly use means when the typhoon arrives, the block already trusts the buttons. - **The Marseille heatwave.** A summer night in a banlieue where temperatures stay at 38°C past midnight. A neighbour records in French: "Madame Diallo au 3ème étage, elle est enceinte de huit mois, elle n'a pas de climatisation, on a un ventilateur de rechange chez nous." The parse populates `pregnant` as a volunteered flag and the offer is "spare_fan". The matchmaker proposes the match within seconds. - **The Cantonese-only grandmother.** A block in Toronto with several Cantonese-speaking elders. A grandson records in English: "My po-po lives at 47 Sherbourne, she only speaks Cantonese, she needs groceries on Tuesday." The parse populates `language_only` as a volunteered flag with notes "Cantonese only per grandson". The matchmaker prioritises a volunteer who lists Cantonese in `languages_spoken`. A different volunteer who speaks only English is not proposed even though they live closer. - **The SMS-only neighbour.** An older neighbour without a smartphone texts the block's SMS number: "Need shovel my walk Tuesday morning 99 Bidwell." The text routes to a coordinator-review queue, the coordinator confirms the address and the block membership, and the request enters the roster. - **The after-action.** The day after the storm ends, the coordinator exports the after-action PDF. It contains: a one-page cover, an aggregate count ("412 helpings, 31 volunteers, 67 distinct requesters"), a list of the most-frequent request types, the longest unfilled time-to-match (with no individual names), the safety concerns that escalated, and a "what worked / what would I do differently" free-text section the coordinator fills in. No individual vulnerability flags appear. No individual names appear in any aggregate count. The PDF is shared with the block's neighbours; the audit log stays with the verified coordinator only. ## 6. Page structure Build the following screens / sections in this order. Adjust copy to fit the voice, but keep the structural intent. 1. **Welcome / sign-in.** A photographed-looking image of a kitchen counter at midnight — a notebook open with neighbour names half-listed in pencil, a phone on the counter showing a half-recorded voice memo, a thermos at the edge of frame. One paragraph: "Coordinate help on your block during a storm — by voice, in any language, with vulnerability protected." Single Google sign-in button; Apple sign-in next to it; "or sign in with phone" (for coordinator verification). Below: "Try with the sample block" → loads the demo block in section 8a. 2. **Block selector.** If the signed-in user is on more than one block, show a list of blocks with the current activity state ("North Buffalo — 47 open requests · 18 volunteers · ACTIVE STORM"). The default view on entry is the user's primary block. A "join a block" button leads to invitation flow. 3. **Voice memo capture (primary action).** A single big push-to-talk button at the bottom of the screen. Above it, a one-line prompt that adapts to the user's role: "Speak — offering help, asking for help, or telling us how the request is going." Below it, a row of three secondary options: "Text instead" → text input, "Upload a memo" → file picker, "Call the bridge" → reveals the SMS+Voice bridge phone number. 4. **Parse review (briefly, for the speaker).** After recording, the speaker sees the parsed summary as one sentence: "Got it — you can shovel three driveways tomorrow, 8am to noon, starting on Elmwood." Two buttons: "Yes, that's right" → submits to roster; "No, let me try again" → re-records. If the parse confidence is low, this screen also asks one or two clarifying questions ("Which Elmwood — between Bidwell and Lafayette?"). 5. **Roster — block-wide view.** Three vertical sections, stacked on mobile, three-column on desktop: "Help asked for" (open requests with one-line non-vulnerability descriptions), "Help offered" (idle offers with capacity and time window), "Happening now" (in-progress matches with first names and ETA). Filter chips at the top: "this morning", "this afternoon", "tonight", "this whole storm". Vulnerability flag categories never appear in this view; only the tier (HIGH / MEDIUM / NORMAL) appears as a small dot beside the request, visible only to verified coordinators. 6. **Volunteer match view.** Triggered when a volunteer just submitted an offer. Shows the top three proposed matches as cards, each with: requester's first name, rounded age decade, address with a small map, walking/driving distance, the one-line non-vulnerability reason, and the time-window overlap. Two buttons per card: "Confirm this match", "Skip — show me the next". A bottom button: "Confirm all three" (for the volunteer who already said "three driveways"). 7. **Requester view.** The requester sees: the status of their open request, the matched volunteer's first name and confirmed window when the match is confirmed, a "the person who is helping has arrived" check-in, and a "the help is no longer needed" cancel button. The matched volunteer's vulnerability flags (if any are on the volunteer side, e.g. the volunteer is themselves chronically ill) are NOT shown to the requester; they are visible only to the coordinator who proposed the match. 8. **Coordinator dashboard.** The verified coordinator's full view: every open request with its vulnerability tier AND flag categories AND coordinator notes; every match with route progress; an alert lane for `contains_safety_concern: true` memos and escalated matches; a one-tap "hand off to another coordinator" button. A toggle in the top-right switches between "coordinator view" and "neighbour view" so the coordinator can see what a regular neighbour sees. 9. **Vulnerability flag editor (coordinator-only).** A dedicated screen, reached from a member's profile in the coordinator dashboard. Each flag is added with its category, notes, and source. The history of edits is visible (added by, edited by, when, why). A clear copy block at the top: "These flags are visible only to verified coordinators. They are never shown to other volunteers, requesters, or the block at large." 10. **Handoff flow.** Coordinator A selects "hand off"; picks coordinator B from a list of verified coordinators on this block; types a one-paragraph free-text handoff message. The system generates the structured briefing automatically and combines it with A's free text. B receives a notification, reviews, and taps "I'm on" to accept. A is now read-only; B is now write-enabled. 11. **Practice mode.** A toggle at the top of the roster view: "Practice mode — Saturday food pantry" or "Practice mode — Tuesday school pickup". Practice mode runs the full workflow but with explicit "PRACTICE" labelling on every screen and a different visual tint (cooler, less urgent). Practice runs do not appear in any after-action export of a real event. 12. **After-action export.** Reached from the coordinator dashboard at the end of an event. The coordinator chooses an event type (snow / heat / flood / grid outage / ongoing care), reviews the aggregate counts the app proposes, fills in a free-text "what worked / what would I do differently", and exports a PDF. The PDF cover is generated by Nano Banana 2 (see section 4b). The PDF is downloaded locally and (optionally) emailed to a list the coordinator nominates. 13. **Settings & member admin.** Coordinator-only screens for managing block membership (invite, remove, change role), the SMS bridge phone number, the voice bridge phone number, the languages spoken on the block (used by the matchmaker for language-match scoring), and data retention (90-day audio purge default; coordinator can extend specific memos for investigation). 14. **Footer.** "Built for the block, not for the platform." Privacy: "Vulnerability flags are visible only to verified coordinators. We never broadcast." Capabilities `(i)` icon in header. ## 6b. First-visit onboarding Show a **first-visit onboarding** the first time a visitor lands on the app (detect via `localStorage` flag; do not show on return visits). Three slides, dismissible at any time. Persistent re-entry: a `?` icon in the header reopens it. **Slide 1 — What this is.** - Headline: "Welcome to Mutual Aid Roster." - Subhead: "Coordinate help on your block — in any storm, any language, with vulnerability protected and matches done by the app, not by you at midnight." - One paragraph (≤ 60 words) explaining who this is for and what makes it different from a generic group chat: neighbours speak in their own language, the app structures the offer or request, the matchmaker proposes specific pairings respecting vulnerability and distance, the coordinator confirms, and vulnerability flags are visible only to verified coordinators. - Visual: a small annotated illustration of a single voice-memo card showing the offer, the parse, and the three proposed matches — not a generic chat bubble. **Slide 2 — Try it now.** - One short prompt: "Try with the sample block". - A live demo with the North Buffalo seed block from section 8a, pre-loaded with three open requests and one idle volunteer. - 1-2 sentences pointing at the specific page elements where the Gemini magic happens (the voice memo parse, the matchmaker's three reasons, the vulnerability tier dot visible only in the coordinator view). **Slide 3 — How to remix this.** - Headline: "Make this yours." - Three short bullets: - "Swap the sample block in `/data/seed-block/` for your own block's seed roster." - "Adjust the system instructions in `/server/prompts/` to fit your block's languages and request types." - "Wire up your Gemini API key, Firebase project, and Twilio account via the env-var list in the capabilities panel." - Primary CTA: "Use this template" → links to AI Studio Build remix entry point. - Secondary: "Just exploring — close" (sets localStorage flag, never auto-shows again). **Accessibility:** focus trap, `Esc` closes, `role="dialog"`, `aria-modal="true"`, `aria-labelledby`, focus restored to trigger on close. Respect `prefers-reduced-motion`. **Don't:** - Don't gate content behind the modal. The page beneath must be fully usable. - Don't auto-reshow on return visits. Use `localStorage['onboarding-seen-v1']`. - Don't include unrelated CTAs (newsletter signup, social follow). Keep it about the template only. ## 6c. Capabilities info button (persistent in header) Add a persistent `(i)` icon in the top-right of the header (next to the primary nav). Click → opens a modal/panel titled **"What powers this app"**. **Panel contents (in this order):** **Gemini capabilities used (the hero list):** - **Gemini 3.5 Flash (audio understanding)** — parses voice memos in 22+ languages, with code-switching support, into a structured offer or request. The user never fills out a form. - **Gemini 3.5 Flash (function calling)** — the load-bearing intelligence. Every state change on the roster (create offer, create request, propose match, confirm match, cancel, complete) is a typed tool call. The model proposes; the server executes. No silent writes. - **Gemini 3.5 Flash (multilingual)** — preserves family terms like "lola", "bà nội", "abuela", "dadi", "halmoni" verbatim; never flattens to "grandmother". - **Gemini 3.5 Flash (long context)** — the matchmaker call sees the whole active roster (every open request, every idle offer, every recent match) plus the volunteer's parsed offer, and proposes the top three matches with reasons. - **Gemini 3.5 Flash + grounded search** — resolves natural-language addresses ("the blue house across from the church on Elmwood") to coordinates within the block's polygon. - **Gemini TTS** — generates spoken roster briefings for coordinators who are driving or walking the block. - **Nano Banana 2** — generates the after-action PDF cover image, in a hand-drawn community-newsletter style. - **Firebase Auth** — Google and Apple sign-in for neighbours, phone-number sign-in for coordinator verification, magic-link email for invitations. - **Firestore** — stores the roster, syncs across devices in real time. - **Firebase Storage** — keeps voice memo audio for the audit trail (90-day default retention). - **Twilio (SMS + Voice)** — bridges neighbours who don't have the app into the same roster. - **Cost note** — see the detailed breakdown in 6d. A typical block running an active 3-day storm event with 400 helpings coordinated costs about $1.80 of Gemini API spend, plus Twilio SMS/voice fees. - **Privacy note** — vulnerability flags are visible only to verified coordinators and are never broadcast. Voice memos are private to the block. This app uses the Gemini API on the paid tier, where Google does not use your content for model training, per the Gemini API Additional Terms. The audit log is the coordinator's record and is never used for analytics or training. **Backend services this app depends on:** - Auth: see section 4b - Database: see section 4b - Storage: see section 4b - Email: see section 4b - SMS: see section 4b — Twilio account required - Voice: see section 4b — Twilio account required - Payments: see section 4b (not used in v1) - External APIs: see section 4b **Environment variables you'll need to configure:** - `GEMINI_API_KEY` — your Google AI Studio API key - `FIREBASE_PROJECT_ID` — your Firebase project id - `FIREBASE_SERVICE_ACCOUNT` — service-account JSON (server-side only) - `TWILIO_ACCOUNT_SID` — Twilio account SID - `TWILIO_AUTH_TOKEN` — Twilio auth token - `TWILIO_SMS_NUMBER` — the SMS bridge number for this block - `TWILIO_VOICE_NUMBER` — the Voice bridge number for this block - `MAPBOX_TOKEN` — optional, only if you want richer base-map tiles than the included OpenStreetMap fallback **Cost + privacy notes:** - One short paragraph per cost-sensitive capability: the matchmaker call is the most frequent Gemini call during an active event — one per offer submitted. A 3-day storm event coordinating 400 helpings might trigger ~120 matchmaker calls, each ~5k input tokens (the active roster) + ~1k output tokens, ~$0.012 each, ~$1.50 for the event. The function-calling state-change layer adds ~$0.30. Voice-memo parses add ~$0.001 each. - One short paragraph on privacy: voice memo audio lives in your Firebase Storage bucket, scoped to the block, and is purged after 90 days unless explicitly retained for an investigation. Vulnerability flags live in a separate Firestore collection with Firestore security rules that check coordinator role on every read; they are NEVER returned by the volunteer or requester views. The audit log is append-only and visible only to verified coordinators. **Documentation links:** - AI Studio Build docs - Gemini API audio understanding, function calling, structured output, long-context docs - Firebase Auth, Firestore, Firebase Storage, Firestore security rules docs - Twilio Programmable Messaging + Voice docs - A short note on what "function calling" means for the deployer who has not used it before **Accessibility:** same standards as the onboarding modal — focus trap, `Esc`, ARIA, restored focus. **Behaviour:** - Always available — single click from anywhere in the app. - Tooltip on the `(i)` icon: "How this app is built". - Mobile: opens as a full-screen sheet that slides up. - Should be the most honest part of the app — never hand-wave service requirements; never say "AI" without naming the specific Gemini model and capability. ## 6d. Detailed cost breakdown (deployer reads this BEFORE shipping) - **Parse voice memo (Gemini 3.5 Flash audio understanding, low thinking)** — typical 30-second memo ≈ 8 seconds of audio tokens + ~500 output tokens. ~$0.001/memo. - **Geocode (Gemini 3.5 Flash + grounded search)** — ~$0.001/address. Most blocks have a stable set of known addresses; a typical event has 30-60 distinct natural-language address descriptions to resolve. - **Matchmaker (Gemini 3.5 Flash, medium thinking, long-context across the roster)** — typical active-event roster ~40k input tokens, ~1k output tokens. ~$0.012/match-propose call. Runs once per offer submitted. - **State-change tool call (Gemini 3.5 Flash, low thinking)** — ~$0.0003/call. Runs once per state change. - **Coordinator handoff summary (Gemini 3.5 Flash, medium thinking, long-context)** — runs once per handoff. ~$0.015/handoff for a typical event. - **TTS briefing (Gemini 2.5 Flash TTS)** — billed per output token (~$10/M output tokens), effectively ~$0.000003/character. A 90-second briefing ≈ $0.003 per generation. Cached per state snapshot. - **After-action cover image (Nano Banana 2)** — ~$0.03/image, runs once per event. - **Expected per-event cost (3-day storm coordinating 400 helpings):** ~$1.80 of Gemini API spend. - **Audio storage:** Firebase Storage standard tier, ~$0.026/GB/month. A 30-second voice memo at standard quality is ~120 KB; a 3-day event with 800 voice memos uses ~100 MB ≈ ~$0.003/month. With 90-day default retention, total audio storage stays well under $0.20/month per active block. - **SMS bridge:** Twilio Programmable Messaging at standard US/Canada pricing ~$0.0079/SMS inbound, ~$0.0079 outbound. A typical event with 200 SMS interactions ≈ ~$1.60/event in SMS fees. - **Voice bridge:** Twilio Programmable Voice at standard US/Canada pricing ~$0.0085/minute. A typical event with 30 voice-bridge calls averaging 90 seconds each ≈ ~$0.40/event in voice fees. - **Total per-event variable cost (Gemini + Twilio):** ~$3.80 for a 3-day storm coordinating 400 helpings. ## 7. Design language - **Mood:** A kitchen-table coordination tool. Not a crisis dashboard. Not a SaaS product. The hum of a kettle at midnight, a notepad with neighbours' names half-listed in pencil, a phone face-down beside the thermos because the coordinator already knows what she needs to do — she just needs a tool that does not make her open a thread of 90 unread messages to find out. - **Typography:** Humanist sans for body and chrome (Inter, IBM Plex Sans, or a similar warm grotesque). A subtle slab serif for headers and the block name (Roboto Slab, Source Serif Pro). A clean monospace for addresses and time windows (JetBrains Mono, IBM Plex Mono). The text avoids "tech product" feel — it should read like a community-centre noticeboard. - **Palette:** Warm off-white background `#FBF8F2` for primary content surfaces, deep slate `#1F2933` for body text, a coordinator-only sage accent `#637D67` for the verified-coordinator UI lane (so the coordinator's eye learns where the privileged information lives), a faded brick `#A8503C` only for HIGH-tier vulnerability flags in the coordinator view, a muted dusk blue `#3B5B7A` for the volunteer match cards. The palette is borrowed from a community-hall noticeboard, not from a SaaS design system. A "practice mode" tint of cooler `#EFF3F5` distinguishes practice rosters from real events. - **Imagery:** No stock photography of disasters. No people in distress. The hero image is a kitchen counter at midnight; the after-action PDF cover is a hand-drawn community-newsletter illustration. The app honours the work, not the spectacle. - **Hand-feel touches:** The voice-memo capture button has a faint warm glow when listening; the glow fades to slate when the memo is parsed. The match cards have a subtle sage edge on the coordinator view and no special treatment on the volunteer view — the visual hierarchy reinforces who-sees-what. - **Spacing:** consistent 4-px base. Generous whitespace; the roster is dense by content, not by visual noise. - **Radius:** consistent token set (e.g. 6 / 12 / 20 px). Match cards use 12; the voice-memo button uses 20; the small pills use 6. - **Shadows:** subtle, layered, warm-grey tinted. The match cards have a slight elevation only when the cursor / touch is on them. - **Motion:** purposeful — entrance fades, list reconciliation animations when the roster updates, a small pulse on the voice-memo button while recording. Respect `prefers-reduced-motion`. No bouncing splash animations. No theatrical hero animations. The reconciliation animation when a match is confirmed (the request slides from the "Open" column to the "Happening now" column) is the canonical reduced-motion check — verify it jumps instead of animating. - **States:** every interactive element has hover, focus, active, and disabled. Loading uses skeletons that match the eventual layout, not spinners. Empty states have helpful next-action guidance ("This block has no open requests right now — record a memo if you have an offer, or invite a neighbour to join"). ## 8. Content generation rules - Write **realistic, specific copy**. NO Lorem Ipsum. NO generic placeholders like 'Your tagline here'. - Invent plausible names, dates, locations, voice-memo content, and addresses that fit the domain (use the seed content in section 8a as a starting point). When inventing, lean on multi-cultural neighbourhood patterns — Filipino, Polish, Nigerian, Mexican, Vietnamese, Korean — without stereotyping. Never claim a fictional voice memo is a real recording. - Tone: calm, direct, free of corporate language. This template is for a coordinator who is already tired, not for a sales prospect. - Headlines: punchy and concrete. No 'Empower your X' filler. No 'Revolutionize'. No 'Seamless'. - Body copy: short paragraphs (2-4 sentences). Use lists where appropriate. - Plain language. Avoid jargon — except where the coordinator already speaks the jargon (the seasoned block captain knows what "HIGH tier" means and what "handoff" means; the new neighbour signing in needs the plain version). - Where the app outputs AI-generated content, never label it as "AI says" — let it speak naturally. Use small uncertainty cues only where epistemic honesty requires them (a low-confidence parse asks one clarifying question before submitting to the roster). ## 8a. Seed content (use these specific examples) Anchor every generated copy + sample data point in the concrete content below. Use these names, numbers, dates, and snippets verbatim where helpful, or generate close variants that sit in the same world. **Sample blocks (sidebar):** - "North Buffalo — Elmwood & Bidwell" (active snow event, 47 open requests, 18 volunteers) — primary working language English, with 4 Polish, 3 Somali, 2 Bengali, 1 Karen-speaking households. - "Bagong Silangan — Sitio 4" (active typhoon event, 62 open requests, 22 volunteers) — primary working language Tagalog, with 6 Bisaya-speaking households. - "Austin East — Govalle" (active grid outage, 31 open requests, 14 volunteers) — primary working language English, with 12 Spanish-speaking households. - "Marseille Nord — Bassens" (active heatwave, 28 open requests, 11 volunteers) — primary working language French, with 8 Arabic-speaking households, 5 Comorian Shikomori-speaking households. - "Toronto Regent Park — Building 4" (ongoing care roster, 19 open requests, 9 volunteers) — primary working language English, with 6 Cantonese, 4 Tamil, 3 Tigrinya-speaking households. **Sample voice memo in detail (this is what the demo should show):** *Memo 1 — Marisol's offer, in English:* - **Speaker (verbatim):** "Marisol Reyes" - **Speaker role:** verified block member - **Detected language:** English (en-US), confidence 0.98 - **Code switching:** none - **Intent:** offer - **Offer types:** ["shovelling", "ride_with_truck"] - **Capacity human units:** 3 - **Has vehicle:** true - **Vehicle type verbatim:** "F-150" - **Time window:** local "2026-01-14T08:00" to "2026-01-14T12:00", America/New_York - **Starting address:** free text "Elmwood between Bidwell and Lafayette", street_name "Elmwood Avenue", cross_streets ["Bidwell Parkway", "Lafayette Avenue"], confidence 0.94 - **Languages spoken:** ["en-US", "es-MX"] - **Speaker emotional tone:** "matter_of_fact" - **Evidence quotes:** - field "offer.offer_types" → "I can shovel three driveways tomorrow morning" / null - field "offer.has_vehicle" → "I have a truck" / null - field "offer.capacity_human_units" → "three driveways" / null - field "offer.starting_address.free_text_verbatim" → "I'm on Elmwood between Bidwell and Lafayette" / null - **Reading confidence:** 0.96 *Memo 2 — the daughter's request, in English (request on behalf of a vulnerable elder):* - **Speaker (verbatim):** "Adaeze Okafor-Jamil" - **Speaker role:** verified block member, requesting for another named neighbour - **Detected language:** English (en-US), confidence 0.97 - **Intent:** request_for_another_named_neighbour - **Request types:** ["shovelling"] - **Quantity human units:** 1 - **Time window:** local "2026-01-14T08:00" to "2026-01-14T14:00", America/New_York - **Needs at address:** free text "my mother, Mrs Okafor, 32 Bidwell Parkway, the small house with the green shutters", confidence 0.92 - **On behalf of name verbatim:** "Mrs Okafor" - **On behalf of relationship verbatim:** "my mother" - **Requester contact preference:** "via_coordinator_only" - **Vulnerability flags volunteered:** - `lives_alone`, source "family_member_reported", notes "age 78 per speaker, lives alone since 2024" - **Additional context verbatim:** "I'm in Detroit, I can't get back tomorrow — she'll be fine if someone just clears the front walk so the meals-on-wheels driver can get to the door" - **Speaker emotional tone:** "warm" - **Evidence quotes:** - field "request.request_types" → "could someone shovel my mother's front walk" / null - field "request.on_behalf_of_name_verbatim" → "Mrs Okafor" / null - field "request.vulnerability_flags_volunteered_by_speaker[0]" → "she's 78, lives alone since 2024" / null - **Reading confidence:** 0.95 *Memo 3 — the Tagalog request from Quezon City (typhoon event):* - **Speaker (verbatim):** "Aling Marites Bautista" - **Speaker role:** verified block member, requesting for another named neighbour - **Detected language:** Tagalog (tl-PH), confidence 0.96 - **Code switching:** yes, switched to English for "oxygen tank" - **Intent:** request_for_another_named_neighbour - **Request types:** ["water_delivery", "warm_meal_delivery", "welfare_check"] - **Quantity other units:** "isang bigas sa maghapon, tubig na maiinom, tingnan kung okay siya" - **Needs at address:** free text "Si lola namin, 4B sa Building 2, may oxygen tank siya at 81 na", street_components captured, confidence 0.88 - **On behalf of name verbatim:** "lola namin" - **On behalf of relationship verbatim:** "lola namin (our grandmother)" - **Vulnerability flags volunteered:** - `oxygen_dependent`, source "neighbour_self_reported", notes "speaker says lola has oxygen tank at home" - `mobility`, notes "medyo mahina na siya, hindi pwede sa hagdan (she is weak, cannot manage stairs) per speaker" - `lives_alone`, notes "nag-iisa per speaker" - **Speaker emotional tone:** "stressed" - **Contains safety concern:** false - **Evidence quotes:** - field "request.request_types" → "kailangan po niya ng tubig at pagkain" / "she needs water and food" - field "request.on_behalf_of_name_verbatim" → "Si lola namin po" / "Our grandmother" - field "request.vulnerability_flags_volunteered_by_speaker[0]" → "may oxygen tank siya" / "she has an oxygen tank" - **Reading confidence:** 0.93 **Sample matchmaker proposal (in response to Memo 1):** - Volunteer: Marisol - Proposed requests (in order): 1. Mrs Okafor (78, lives alone, 32 Bidwell — 0.3mi walk from Marisol's start). Reason shown to Marisol: "Mrs Okafor, 78, lives alone, 0.3mi away — her daughter in Detroit asked for someone to clear her walk." Vulnerability flag categories NOT shown to Marisol. 2. The Jamil family (multi-generational household, 7 Lafayette — 0.5mi). Reason shown to Marisol: "Jamil family, 0.5mi away — request for help clearing both driveways before tomorrow's school run." 3. Mr Andrzejewski (82, request for tank delivery support, 14 Bidwell — 0.8mi). Reason shown to Marisol: "Mr Andrzejewski, 82, 0.8mi away — his daughter asked for a volunteer with a truck for tomorrow's oxygen tank delivery." Note that "oxygen tank" is named because the daughter named it in HER memo as part of the request — the vulnerability flag category remains coordinator-only. **Sample coordinator handoff briefing (generated from the active roster):** - Headline: "Three HIGH-tier open requests, one safety concern still uninvestigated, six in-progress matches with ETAs." - Safety concerns open: "Apartment 4B in Building 2 — speaker said 'hindi siya sumasagot sa pintuan kanina, baka tulog lang' ('she didn't answer the door earlier, maybe she's just sleeping') — recommend wellness check within the hour." - Unfilled HIGH-tier requests: Lola at 4B (oxygen + lives alone), Mr Andrzejewski (oxygen + tank delivery), Madame Diallo (pregnant + no AC). - Idle offers: Marisol (3 driveways, 8am-noon, truck), Kuya Lito (water delivery 2nd floor only), Achille (welfare-check phone calls, 8 calls/hour). - In-progress: 6 matches with ETAs. - Recently-completed-with-concern (last 6 hours): "Mrs Akinwale — finished her shopping but she seemed disoriented, I didn't know who else to tell" — verbatim from volunteer Tomasz. - Outgoing handoff message verbatim: "I'm going to sleep. The Lola in 4B is the one I'm worried about — if she doesn't get a check by 9am please call her granddaughter Aling Marites first, then if no answer the barangay health worker. Thanks María — I'll be on at 3pm." **Sample voice copy:** - Onboarding: "Press the big button and say what you can offer or what you need. We'll structure the rest." - Empty roster: "This block has no open requests right now. Tap the button to record an offer — or invite a neighbour to join." - Voice memo recording: "Listening…" - Parse confirmation: "Got it — you can shovel three driveways tomorrow, 8am to noon, starting on Elmwood. Yes that's right · No, let me try again." - Match proposal: "Three matches near you — tap to confirm." - Match confirmed: "Confirmed. Mrs Okafor's daughter has been notified. The roster is updated." - Cancel: "Cancelled. The coordinator has been notified." - Safety concern: "We've flagged this for the coordinator. The coordinator will decide whether to call for help." - Handoff complete: "You're off the clock. María has the roster. Sleep well." - Practice mode banner: "Practice mode — this roster is not a real event. Confirmations don't notify anyone." **Sample family invitation email subject + body:** - Subject: "Marisol — would you join the Elmwood block's mutual-aid roster?" - Body: "Hi Marisol — we're using a small app to coordinate help on our block during storms. It takes voice memos in any language, structures them into a roster, and proposes matches. Tap to join and tell us what you can offer (or ask for)." [Join Block] ## 9. Media & assets - **Hero image (landing screen):** A photographed-looking shot of a kitchen counter at midnight — a notebook half-listed with neighbours' names in pencil, a phone face-down on the counter, a thermos at the edge of frame, a single warm pendant light above. Generate via Nano Banana 2 with a prompt emphasising "wooden kitchen counter, warm pendant light, late evening, no people in frame, slight motion blur on the thermos as if someone just set it down". - **App icon / wordmark:** Set in the humanist sans with a small slab-serif "R" for "Roster". A subtle paper-grain background. No icon — just type. - **Empty-state illustration:** A simple line drawing of a notepad with a pencil resting on it. Hand-drawn aesthetic, not a flat icon. - **Match card map thumbnails:** Small static map tiles showing the volunteer's start point and the requester's address with a route between them. Use OpenStreetMap raster tiles as the offline fallback; if Mapbox is configured, use a muted "warm grey" style. - **After-action PDF cover:** Generated per the prompt in section 4b — Nano Banana 2 prompts that produce hand-drawn community-newsletter illustrations specific to the event type. Never an action shot. Never a disaster scene. - **Stock fallbacks:** If image generation fails, fall back to a built-in SVG illustration from `/public/illustrations/`. Never to a "🤝" emoji. - **Generated imagery:** prefer Nano Banana 2 over stock photography. Prompt for warmth, asymmetry, and slight imperfection — avoid the glossy 'AI render' look. Never depict people, faces, or specific buildings. - **Optimisation:** WebP/AVIF, `loading="lazy"`, explicit `width`/`height` to prevent layout shift. - **Icons:** `lucide-react` for UI. Use sparingly — never decorative-only. A small sage dot for the coordinator-only UI lane; a small dusk-blue circle for the volunteer match cards; a faded brick dot for HIGH-tier vulnerability priority (visible only in coordinator views). ### Build-time asset manifest (explicit specs) Every image, illustration, and visual reference mentioned above must resolve to ONE of the three buckets below — runtime-generated, seed-shipped, or user-supplied. Do NOT ship `` tags whose `src` is not listed here. Do NOT depend on bare "section 8a prompts" without binding them to explicit paths and model IDs. **Bucket 1 — Runtime-generated (Nano Banana Pro `gemini-3-pro-image` for hero/demo photographs; Nano Banana 2 `gemini-3.1-flash-image` for in-app illustrations and reference-conditioned variants).** Cached to Firebase Storage; served via signed URL. Every reference above to "Nano Banana 2" or "Nano Banana Pro" MUST be wired to one of these specific calls with an explicit model id: - `/public/generated/hero.webp` (2400×1500, WebP) — model `gemini-3-pro-image` — uses the literal prompt described as "Hero image (landing screen)" above. Run once at build; commit a `/public/samples/hero-fallback.webp` (1600×1000) generated from the same prompt with `gemini-3.1-flash-image` so the page renders if quota is exhausted. - `/public/generated/demo/{demo-slug}-{NN}.webp` (1600×1200, WebP) — model `gemini-3.1-flash-image` (reference-conditioned where the prior frame is passed as input) — one path per "Demo X" image referenced above. The slug derives from the seed example in section 8a; the NN index covers each frame in the demo sequence. - `/public/generated/illustrations/{name}.webp` (1024×1024, WebP) — model `gemini-3.1-flash-image` — one path per named illustration above ("Empty-state illustration", "Recipe-card hero illustrations", "Curriculum picker imagery", "Period-style frames", etc.). Each illustration's prompt is the literal description above; ship a deterministic seed in the request so re-runs are reproducible. **Bucket 2 — Seed assets shipped with the deliverable.** Every "Stock fallback" path referenced above (e.g. `/public/samples/sample-X.jpg`) is generated once via Nano Banana 2 (`gemini-3.1-flash-image`) at 1024×1024 WebP using the same prompt as its Bucket-1 counterpart, then committed to the repo so the page renders identically if Gemini quota is exhausted or the user is offline. Replace any `.jpg` extension above with `.webp` to match the optimisation rule. Also commit these empty-state seeds (1024×1024 WebP, single-stroke hand-drawn line, no colour fill): - `/public/samples/empty-state-primary.webp` — line drawing of the app's primary empty surface (the named "Empty-state illustration" above), generated from that exact prompt. - `/public/samples/empty-state-archive.webp` — line drawing of an empty saved/archive view, single-stroke outline. - `/public/samples/empty-state-error.webp` — line drawing of a hand placing a single object aside with care, used when an AI call fails. **Bucket 3 — User-supplied.** Uploads from the user's camera / file picker land at the Firebase Storage path conventional for this template (named in section 4b). The build ships with Bucket-1 + Bucket-2 only; no user-supplied images at first paint. **Hard rules** - Every `` tag MUST have a `src` that resolves to a path listed in Bucket 1, Bucket 2, or a Bucket 3 upload path. Anything else is a build error. - No bare `image.jpg` / `hero.jpg` / `placeholder.png` references anywhere in the code. - Model IDs: `gemini-3-pro-image` for hero-quality photographic generation; `gemini-3.1-flash-image` for in-app illustrations, reference-conditioned variants, empty-state seeds, and stock fallbacks. Never use a legacy model id (no `imagen-*`, no `gemini-1.5-*-image`). - File format: WebP everywhere (AVIF acceptable where the target browsers support it). No `.jpg` / `.jpeg` / `.png` in `/public/samples/`. ## 10. Interactivity & states - Every interactive element has hover, focus, active, and disabled states. - Forms validate inline and show specific error messages (not "Invalid input"). A parse-confidence-low message says "We're not sure about the address — which Elmwood, between Bidwell and Lafayette?" not "Address invalid". - Loading states use skeletons that match the eventual layout, not spinners. The matchmaker "thinking" state shows three skeleton match cards animating in sequence. - Empty states explain the next action with a button whose label fits THIS app's domain: "Record an offer of help", "Invite a neighbour", "Add a vulnerability flag" — never a generic "Add your first item". - Smooth scroll for in-page anchors. - All AI-generated content streams in token-by-token where supported, with a clear "listening…" / "parsing…" / "matching…" indicator before content starts arriving. - If an AI call fails, show a calm, specific error ("We couldn't parse this memo — try a clearer recording, or text the request instead?") and offer retry. - Low-confidence parse fields are faintly underlined; tapping reveals the alternates the model considered. The user can override before submitting. - The roster reconciliation animation (request slides from "Open" to "Happening now" when a match is confirmed) takes 500 ms with `prefers-reduced-motion` falling back to instant. - Push notifications are minimal and respect quiet hours. Defaults: HIGH-tier safety concerns push immediately to the coordinator; routine match proposals push to the volunteer only between 7am and 10pm local time; status updates push to the requester only. ## 11. Tech & responsive requirements - **TTS markdown-stripping preprocessor:** before sending any user-authored markdown to `gemini-3.1-flash-tts-preview`, strip non-spoken markdown: `#`/`##`/`###` headings (keep the title text), `**bold**` (keep the inner text), `[label](url)` (keep `label`, drop URL), `` ``` `` fenced code blocks (skip entirely), `>` block-quote markers (keep the text), and `|` table pipes (read row-by-row as sentences). Insert `…` between sentences for a short pause and a blank line plus `—` between paragraphs for a long pause. The model does not understand markdown; raw markdown will be read aloud as literal characters ("asterisk asterisk"). - **File downloads on Safari / Firefox:** when offering local-disk save of any export (PDF, CSV, MP3, ZIP, JSON, image), fall back to `` with a blob URL — the File System Access API (`showSaveFilePicker()`) is Chromium-only. Detect with `'showSaveFilePicker' in window`; otherwise use the anchor-download path. - **Stack:** React + TypeScript + Tailwind CSS. Functional components + hooks. Use Shadcn UI primitives where appropriate. - **Build runtime:** AI Studio Build — full-stack with Cloud Run server-side functions. All Gemini API calls happen server-side; API key lives in Secrets Manager, never in client bundle. - **Model selection:** explicitly pin `gemini-3.5-flash` for parse-memo / matchmaker / handoff-summary and `gemini-3.5-flash` for geocoding and state-change tool calls. Set `thinkingLevel` explicitly per call. - **Function calling:** the matchmaker call MUST be configured to return a tool call, never free-form text. Reject free-form text responses and retry with a stronger reminder once before failing closed. - **Database:** Firestore (auto-provisioned by AI Studio Build). Firestore security rules MUST enforce: (a) only verified coordinators can read the `vulnerability_flags` collection; (b) only block members can read the block's `offers`, `requests`, `matches` collections; (c) the `audit_log` is append-only (no update, no delete); (d) the `update_vulnerability_flag` tool call is gated server-side, not client-side. - **Auth:** Firebase Auth — Google sign-in by default; Apple sign-in next to it; phone-number sign-in for coordinator verification; magic-link email for invitation onboarding. - **Storage:** Firebase Storage for voice memo audio. Pre-signed URLs only. 90-day automatic purge with explicit retain-for-investigation override (coordinator only). - **SMS + Voice bridge:** Twilio webhook endpoints exposed via Cloud Run. SMS phone number and Voice phone number are configured per block. - **Mobile-first.** Verify layouts at 375 px (iPhone SE), 768 px (iPad), 1024 px, 1440 px+. - Use `clamp()` for fluid typography. Prefer container queries over media queries for component-level responsiveness. - Use `dvh` / `svh` instead of `vh`. Respect safe-area insets on iOS — the voice-memo button sits above the iOS home indicator. - Zero horizontal overflow at any width. Zero layout shift on load. - Persist user data in Firestore. Use real-time listeners on the roster view. - Optimistic UI on writes; reconcile on response. Match confirmations are NOT optimistic — they require server confirmation before flipping the UI state, because a stale confirmation could cause two volunteers to take the same request. - Voice memo recording uses the Web Audio API on the web and the native microphone on mobile (PWA). Audio is uploaded as Opus or AAC at modest bitrate (32-48 kbps) — high enough to parse, low enough to upload over a degraded cellular link. - **iOS Safari gotchas (graceful degradation):** Safari `MediaRecorder` only supports `audio/mp4` (AAC) — feature-detect and fall back to AAC at 32-48 kbps; microphone permission does NOT persist across reloads on iOS — re-request on every voice-memo open; an incoming call interrupts the audio session (`MediaStreamTrack.onmute` fires) — auto-pause and prompt resume; Web Speech API on-device transcript is unreliable on iOS Safari — always queue the raw audio for server-side fallback transcription. - Offline mode queues voice memos as local audio blobs with a best-effort on-device transcript (Web Speech API where available), and syncs as soon as the connection comes back. The coordinator dashboard shows pending-sync items with a small clock icon. ## 12. Accessibility (WCAG 2.2 AA) - Semantic HTML — `header`, `nav`, `main`, `section`, `article`, `footer`. - All interactive controls reachable by keyboard with a visible focus ring. - Color contrast ≥ 4.5:1 for body, 3:1 for large text and UI components. The sage coordinator-only accent and the dusk-blue volunteer card colour both meet AA against the warm off-white background. - All images have meaningful `alt` text. The match-card map thumbnails have `alt` describing the route ("walking route from Elmwood and Bidwell to 32 Bidwell Parkway, 0.3 miles"). - Form fields have associated `