My Prevention Plan source snapshot. Generated from an explicit allowlist; environment files and runtime data are excluded.
===== messages/en.json =====
{
"skip": "Skip to main content",
"workspace": "DEMO WORKSPACE",
"roles": "Demo roles",
"member": "Member",
"coach": "Coach",
"analyst": "Analyst",
"memberSub": "Your plan and choices",
"coachSub": "Support and proposals",
"analystSub": "Rules and decisions",
"synthetic": "Synthetic data",
"fixedWeek": "Demo week: 5-11 October 2026",
"backOverview": "Back to overview",
"demo": "Guided demo",
"loading": "Loading your workspace…",
"reset": "Start again",
"caseStudy": "MY PREVENTION PLAN",
"memberTitle": "Make room for a small step.",
"coachTitle": "Support starts with listening.",
"analystTitle": "Every decision has a reason.",
"subtitle": "A fictional service journey, with choice at every step.",
"scenario": "Explore a scenario",
"scenario_guided": "New guided journey",
"scenario_camille": "Camille: difficulty",
"scenario_jules": "Jules: incomplete data",
"scenario_noor": "Noor: recorded progress",
"nextStep": "YOUR NEXT STEP",
"journey": "Guided journey",
"preferencesGuide": "Save your preferences, then ask for a demo plan. Walking and 10-minute slots are the supported example.",
"acceptGuide": "Review the proposed days. Accept to activate your plan, or decline and choose again.",
"recordGuide": "Record a walk, save a check-in and calculate your weekly review. These are three separate actions.",
"supportGuide": "Review the explanation. If you want help, enable coach sharing and explicitly request support.",
"handoffGuide": "Your support request is saved. Switch to Coach to inspect the shared context and propose an adjustment.",
"decisionGuide": "The coach proposal has not changed your plan. Accept or decline it; then inspect the Analyst view.",
"coachGuide": "Inspect the explicit request, explain a smaller schedule and propose it. Only the member can activate the change.",
"analystGuide": "Inspect rule inputs, outcomes and template versions. Personal notes are excluded from this view.",
"stepPreferences": "Preferences",
"stepPlan": "Agree a plan",
"stepRecord": "Record",
"stepReview": "Review",
"stepCoach": "Coach",
"stepDecision": "Decide",
"preferencesTitle": "What fits your week?",
"goal": "Your practical goal",
"activity": "Preferred activity",
"walking": "Walking",
"cycling": "Cycling (unsupported example)",
"minutesSlot": "Minutes per slot",
"days": "Available days",
"day1": "Monday",
"day2": "Tuesday",
"day3": "Wednesday",
"day4": "Thursday",
"day5": "Friday",
"day6": "Saturday",
"day7": "Sunday",
"savePreferences": "Save preferences",
"propose": "Propose my plan",
"templateLimit": "This fictional template supports member-selected 10-minute walking slots. It is not a health recommendation; other preferences produce an explicit unsupported result.",
"yourPlan": "Your walking plan",
"version": "Version",
"proposed": "Proposed",
"active": "Active",
"declined": "Declined",
"matched": "A demo proposal based on the walking days and short slots you chose.",
"minutes": "min",
"templateDetails": "Template and evidence",
"demoOnly": "Demo template, no clinical validation",
"acceptPlan": "Accept this plan",
"decline": "Decline",
"journal": "Your activity journal",
"date": "Activity date",
"duration": "Duration in minutes",
"addActivity": "Record a walk",
"manual": "Manual record",
"noRecords": "No activities recorded. This does not mean no activity happened.",
"checkInTitle": "How did the plan fit your week?",
"difficulty": "Member check-in",
"difficult": "It was difficult",
"manageable": "It was manageable",
"not_attempted": "I did not attempt it",
"prefer_not_to_say": "I prefer not to say",
"reportingComplete": "I have finished reporting this week's activities",
"optionalNote": "A note for the coach (optional)",
"notePrivacy": "Only your assigned coach can read this note when you enable sharing. The analyst cannot read it.",
"saveCheckIn": "Save check-in",
"calculateReview": "Calculate weekly review",
"unknownActivity": "Activity quantity is unknown; missing records are not zero activity.",
"recordedSessions": "{count} recorded sessions / {target} agreed sessions",
"completeData": "Reporting complete",
"incompleteData": "Reporting incomplete. This does not mean you did not move.",
"reviewDoesNotRequest": "A review never creates a support request automatically.",
"memberChoice": "YOUR CHOICE",
"coachProposal": "A proposal from your coach",
"proposalSchedule": "Proposed walking days: {days}, 10 minutes per slot.",
"unchanged": "Your active plan is still version {version} until you accept.",
"acceptChange": "Accept adjustment",
"permissions": "OPTIONAL SHARING",
"choiceTitle": "You choose what to share.",
"shareCoach": "Share my programme context with the assigned coach",
"shareExplanation": "Share your goal, plan, activities, review and optional note. Turning this off immediately blocks coach access and cancels unresolved support and proposals.",
"importNotImplemented": "Activity importing is not implemented in R1. Jules uses seeded incomplete coverage.",
"fixturePermission": "New journeys start with optional permissions off. Ready-made Camille and Noor scenarios include previously granted fictional coach sharing.",
"supportTitle": "A little support?",
"supportExplanation": "Ask the assigned coach to review your plan. No response deadline is promised.",
"requestSupport": "Request coach support",
"enableSharing": "Enable coach sharing before requesting support.",
"reviewFirst": "Calculate a weekly review before requesting support.",
"goCoach": "Continue as Coach",
"goMember": "Return to Member",
"goAnalyst": "Inspect as Analyst",
"history": "Accepted plan history",
"slots": "walking slots",
"status_requested": "Support requested",
"status_proposed": "Proposal awaiting your choice",
"status_closed": "Support closed",
"status_cancelled": "Support cancelled after revocation",
"assignedToYou": "ASSIGNED TO COACH ALEX",
"sharedContext": "Shared programme context for this fictional member.",
"waitingAcceptance": "Proposal saved. The current plan is unchanged while the member decides.",
"proposeAdjustment": "Propose a smaller schedule",
"coachExample": "Example adjustment: keep the first two current walking days at 10 minutes each. Explain why this could fit better.",
"rationale": "Reason for the proposal",
"sendProposal": "Send proposal for member review",
"emptyQueue": "No unresolved assigned support requests. The member must explicitly request help first.",
"memberDecides": "The member decides.",
"revocationExplanation": "Sharing permission is checked again on every protected read and write.",
"decisionHistory": "Pseudonymous decision history",
"analystPrivacy": "Rule categories and versions only. No names, contact information or personal notes.",
"noDecisions": "No reviews calculated yet. Return to Member and calculate a review.",
"reviewRevision": "Review revision",
"notReported": "Not reported",
"dataQuality": "Data quality",
"saved": "Saved. Your workspace shows the committed result.",
"requestSaved": "Support request saved. Continue as Coach to review it.",
"proposalSaved": "Proposal saved. Return to Member to accept or decline; the active plan has not changed.",
"reload": "Reload current state",
"footer": "Fictional case and synthetic data. No real notifications, wearable integration or clinical advice. Independent portfolio demonstration.",
"outcomes": {
"support_offered": "Support is available if you want it",
"data_check_needed": "Check the missing data",
"check_in_invited": "An optional check-in is available",
"plan_review_offered": "You can review the plan",
"progress_acknowledged": "Recorded progress acknowledged",
"continue_or_adjust": "Continue or choose an adjustment"
},
"errors": {
"service_unavailable": "The service is unavailable. Your save is not confirmed. Check local PostgreSQL and reload.",
"network_error": "The connection failed. Your save is not confirmed. Retry the same action without changing its fields.",
"unauthenticated": "Your demo session has expired. Reload to start a new isolated journey.",
"origin_forbidden": "The request origin is not allowed.",
"invalid_json": "The request could not be read.",
"payload_too_large": "The request is too large.",
"stale_generation": "This journey was reset in another tab. Current state has been reloaded; review it before trying again.",
"stale_actor": "Your role changed in another tab. Current state has been reloaded; review it before trying again.",
"idempotency_conflict": "This retry key was used for a different action. Reload and try again.",
"missing_idempotency_key": "A valid retry key is required.",
"permission_revoked": "Coach sharing is off. Return to Member to inspect your permission choices.",
"role_forbidden": "This action is not available to the current role.",
"resource_not_found": "This resource is unavailable to the current actor.",
"invalid_preferences": "Enter a goal, unique available days and a positive whole-number duration.",
"no_suitable_template": "No demo template matches these preferences. Try walking with 10-minute slots; no new prescription has been generated.",
"invalid_slots": "Select unique walking days with 10-minute slots.",
"invalid_date": "Use a valid activity date on or before 11 October 2026; reviews use the week starting 5 October.",
"invalid_duration": "Enter a whole-number duration between 1 and 1440 minutes.",
"invalid_activity": "This demo records walking activities only.",
"invalid_check_in": "Check the check-in selection and optional note length.",
"invalid_consent": "Choose whether to grant sharing.",
"invalid_request": "The request requires a current review from your active plan.",
"invalid_transition": "This action does not fit the current state. Reload and review the next step.",
"duplicate_request": "There is already one unresolved support request for this plan.",
"stale_version": "The proposal refers to an older plan version. Reload before continuing.",
"etag_mismatch": "The plan has changed. Current state has been reloaded; review it before accepting.",
"precondition_required": "Reload to obtain the current plan version before continuing.",
"invalid_rationale": "Add a reason of up to 500 characters.",
"invalid_decision": "Choose accept or decline.",
"invalid_role": "Choose Member, Coach or Analyst.",
"invalid_scenario": "Choose one of the available demo scenarios."
},
"finishReporting": "Finish reporting without a check-in",
"completedGuide": "The adjustment is accepted. Inspect the Analyst view to trace the earlier review; its snapshot and your original plan version remain preserved."
}
===== messages/nl-BE.json =====
{
"skip": "Ga naar de hoofdinhoud",
"workspace": "DEMO-WERKRUIMTE",
"roles": "Demorollen",
"member": "Deelnemer",
"coach": "Coach",
"analyst": "Analist",
"memberSub": "Je plan en je keuzes",
"coachSub": "Ondersteuning en voorstellen",
"analystSub": "Regels en beslissingen",
"synthetic": "Fictieve gegevens",
"fixedWeek": "Demoweek: 5-11 oktober 2026",
"backOverview": "Terug naar overzicht",
"demo": "Begeleide demo",
"loading": "Je werkruimte wordt geladen…",
"reset": "Opnieuw beginnen",
"caseStudy": "MY PREVENTION PLAN",
"memberTitle": "Maak ruimte voor een kleine stap.",
"coachTitle": "Ondersteuning begint met luisteren.",
"analystTitle": "Elke beslissing heeft een reden.",
"subtitle": "Een fictief traject, met keuzevrijheid bij elke stap.",
"scenario": "Verken een scenario",
"scenario_guided": "Nieuw begeleid traject",
"scenario_camille": "Camille: moeilijkheden",
"scenario_jules": "Jules: onvolledige gegevens",
"scenario_noor": "Noor: geregistreerde vooruitgang",
"nextStep": "JE VOLGENDE STAP",
"journey": "Begeleid traject",
"preferencesGuide": "Bewaar je voorkeuren en vraag daarna een demoplan. Wandelen in blokken van 10 minuten is het ondersteunde voorbeeld.",
"acceptGuide": "Bekijk de voorgestelde dagen. Aanvaard om je plan te activeren, of wijs het af en kies opnieuw.",
"recordGuide": "Registreer een wandeling, bewaar een check-in en bereken je weekoverzicht. Dit zijn drie afzonderlijke acties.",
"supportGuide": "Lees de uitleg. Wil je hulp? Schakel delen met de coach in en vraag zelf ondersteuning.",
"handoffGuide": "Je hulpvraag is bewaard. Ga naar Coach om de gedeelde context te bekijken en een aanpassing voor te stellen.",
"decisionGuide": "Het coachvoorstel heeft je plan nog niet gewijzigd. Aanvaard of weiger het en bekijk daarna de Analist-weergave.",
"coachGuide": "Bekijk de expliciete hulpvraag, leg een kleiner schema uit en stel het voor. Alleen de deelnemer kan de wijziging activeren.",
"analystGuide": "Bekijk de invoercategorieën, resultaten en modelversies. Persoonlijke notities staan niet in deze weergave.",
"stepPreferences": "Voorkeuren",
"stepPlan": "Plan afspreken",
"stepRecord": "Registreren",
"stepReview": "Evalueren",
"stepCoach": "Coach",
"stepDecision": "Beslissen",
"preferencesTitle": "Wat past in je week?",
"goal": "Je praktische doel",
"activity": "Voorkeursactiviteit",
"walking": "Wandelen",
"cycling": "Fietsen (niet ondersteund voorbeeld)",
"minutesSlot": "Minuten per tijdsblok",
"days": "Beschikbare dagen",
"day1": "Maandag",
"day2": "Dinsdag",
"day3": "Woensdag",
"day4": "Donderdag",
"day5": "Vrijdag",
"day6": "Zaterdag",
"day7": "Zondag",
"savePreferences": "Voorkeuren bewaren",
"propose": "Mijn plan voorstellen",
"templateLimit": "Dit fictieve model ondersteunt zelfgekozen wandelblokken van 10 minuten. Het is geen gezondheidsadvies. Andere voorkeuren geven een duidelijke melding dat er geen passend model is.",
"yourPlan": "Je wandelplan",
"version": "Versie",
"proposed": "Voorgesteld",
"active": "Actief",
"declined": "Afgewezen",
"matched": "Een demovoorstel op basis van je gekozen wandeldagen en korte tijdsblokken.",
"minutes": "min",
"templateDetails": "Model en onderbouwing",
"demoOnly": "Demomodel, zonder klinische validatie",
"acceptPlan": "Dit plan aanvaarden",
"decline": "Afwijzen",
"journal": "Je activiteitenlogboek",
"date": "Datum van de activiteit",
"duration": "Duur in minuten",
"addActivity": "Wandeling registreren",
"manual": "Handmatige registratie",
"noRecords": "Geen activiteiten geregistreerd. Dat betekent niet dat er geen activiteit was.",
"checkInTitle": "Hoe paste het plan in je week?",
"difficulty": "Check-in van de deelnemer",
"difficult": "Het was moeilijk",
"manageable": "Het was haalbaar",
"not_attempted": "Ik heb het niet geprobeerd",
"prefer_not_to_say": "Ik zeg het liever niet",
"reportingComplete": "Ik ben klaar met het registreren van de activiteiten van deze week",
"optionalNote": "Een notitie voor de coach (optioneel)",
"notePrivacy": "Alleen je toegewezen coach kan deze notitie lezen als je delen inschakelt. De analist kan ze niet lezen.",
"saveCheckIn": "Check-in bewaren",
"calculateReview": "Weekoverzicht berekenen",
"unknownActivity": "De hoeveelheid activiteit is onbekend. Ontbrekende registraties betekenen niet nul activiteit.",
"recordedSessions": "{count} geregistreerde sessies / {target} afgesproken sessies",
"completeData": "Registratie volledig",
"incompleteData": "Registratie onvolledig. Dat betekent niet dat je niet bewogen hebt.",
"reviewDoesNotRequest": "Een weekoverzicht maakt nooit automatisch een hulpvraag aan.",
"memberChoice": "JOUW KEUZE",
"coachProposal": "Een voorstel van je coach",
"proposalSchedule": "Voorgestelde wandeldagen: {days}, 10 minuten per tijdsblok.",
"unchanged": "Je actieve plan blijft versie {version} tot je het voorstel aanvaardt.",
"acceptChange": "Aanpassing aanvaarden",
"permissions": "OPTIONEEL DELEN",
"choiceTitle": "Jij kiest wat je deelt.",
"shareCoach": "Mijn programmacontext delen met de toegewezen coach",
"shareExplanation": "Deel je doel, plan, activiteiten, weekoverzicht en optionele notitie. Uitschakelen blokkeert meteen de toegang voor de coach en annuleert open hulpvragen en voorstellen.",
"importNotImplemented": "Activiteiten importeren is niet geïmplementeerd in R1. Jules gebruikt vooraf ingevulde onvolledige dekking.",
"fixturePermission": "Nieuwe trajecten starten zonder optionele toestemming. De kant-en-klare scenario's Camille en Noor bevatten eerder verleende fictieve toestemming om met de coach te delen.",
"supportTitle": "Een beetje ondersteuning?",
"supportExplanation": "Vraag de toegewezen coach om je plan te bekijken. Er wordt geen antwoordtermijn beloofd.",
"requestSupport": "Ondersteuning vragen",
"enableSharing": "Schakel delen met de coach in voordat je ondersteuning vraagt.",
"reviewFirst": "Bereken een weekoverzicht voordat je ondersteuning vraagt.",
"goCoach": "Verder als Coach",
"goMember": "Terug naar Deelnemer",
"goAnalyst": "Bekijken als Analist",
"history": "Geschiedenis van aanvaarde plannen",
"slots": "wandelblokken",
"status_requested": "Ondersteuning gevraagd",
"status_proposed": "Voorstel wacht op jouw keuze",
"status_closed": "Hulpvraag afgesloten",
"status_cancelled": "Hulpvraag geannuleerd na intrekking",
"assignedToYou": "TOEGEWEZEN AAN COACH ALEX",
"sharedContext": "Gedeelde programmacontext voor deze fictieve deelnemer.",
"waitingAcceptance": "Voorstel bewaard. Het huidige plan blijft ongewijzigd terwijl de deelnemer beslist.",
"proposeAdjustment": "Een kleiner schema voorstellen",
"coachExample": "Voorbeeld: behoud de eerste twee huidige wandeldagen met telkens 10 minuten. Leg uit waarom dit beter zou kunnen passen.",
"rationale": "Reden voor het voorstel",
"sendProposal": "Voorstel aan de deelnemer bezorgen",
"emptyQueue": "Geen open toegewezen hulpvragen. De deelnemer moet eerst zelf hulp vragen.",
"memberDecides": "De deelnemer beslist.",
"revocationExplanation": "De toestemming om te delen wordt opnieuw gecontroleerd bij elke beschermde lees- en schrijfactie.",
"decisionHistory": "Gepseudonimiseerde beslissingsgeschiedenis",
"analystPrivacy": "Alleen regelcategorieën en versies. Geen namen, contactgegevens of persoonlijke notities.",
"noDecisions": "Nog geen weekoverzichten berekend. Ga terug naar Deelnemer en bereken een overzicht.",
"reviewRevision": "Revisie van het weekoverzicht",
"notReported": "Niet gemeld",
"dataQuality": "Gegevenskwaliteit",
"saved": "Bewaard. Je werkruimte toont het vastgelegde resultaat.",
"requestSaved": "Hulpvraag bewaard. Ga verder als Coach om ze te bekijken.",
"proposalSaved": "Voorstel bewaard. Ga terug naar Deelnemer om het te aanvaarden of af te wijzen. Het actieve plan is niet gewijzigd.",
"reload": "Huidige gegevens herladen",
"footer": "Fictieve case en gegevens. Geen echte meldingen, wearable-integratie of klinisch advies. Onafhankelijke portfoliodemo.",
"outcomes": {
"support_offered": "Ondersteuning is beschikbaar als je dat wilt",
"data_check_needed": "Controleer de ontbrekende gegevens",
"check_in_invited": "Een optionele check-in is beschikbaar",
"plan_review_offered": "Je kunt het plan bekijken",
"progress_acknowledged": "Geregistreerde vooruitgang erkend",
"continue_or_adjust": "Verdergaan of een aanpassing kiezen"
},
"errors": {
"service_unavailable": "De dienst is niet beschikbaar. Je opslag is niet bevestigd. Controleer de lokale PostgreSQL en herlaad.",
"network_error": "De verbinding is verbroken. Je opslag is niet bevestigd. Probeer dezelfde actie opnieuw zonder de velden te wijzigen.",
"unauthenticated": "Je demosessie is verlopen. Herlaad om een nieuw afzonderlijk traject te starten.",
"origin_forbidden": "De oorsprong van dit verzoek is niet toegestaan.",
"invalid_json": "Het verzoek kon niet worden gelezen.",
"payload_too_large": "Het verzoek is te groot.",
"stale_generation": "Dit traject is in een ander tabblad herstart. De huidige gegevens zijn herladen. Bekijk ze voordat je opnieuw probeert.",
"stale_actor": "Je rol is in een ander tabblad gewijzigd. De huidige gegevens zijn herladen. Bekijk ze voordat je opnieuw probeert.",
"idempotency_conflict": "Deze herhaalsleutel is voor een andere actie gebruikt. Herlaad en probeer opnieuw.",
"missing_idempotency_key": "Een geldige herhaalsleutel is vereist.",
"permission_revoked": "Delen met de coach is uitgeschakeld. Ga terug naar Deelnemer om je toestemmingskeuzes te bekijken.",
"role_forbidden": "Deze actie is niet beschikbaar voor de huidige rol.",
"resource_not_found": "Deze gegevens zijn niet beschikbaar voor de huidige gebruiker.",
"invalid_preferences": "Vul een doel, unieke beschikbare dagen en een positief geheel aantal minuten in.",
"no_suitable_template": "Geen demomodel past bij deze voorkeuren. Probeer wandelen in blokken van 10 minuten. Er is geen nieuw voorschrift gemaakt.",
"invalid_slots": "Kies unieke wandeldagen met blokken van 10 minuten.",
"invalid_date": "Gebruik een geldige datum op of vóór 11 oktober 2026. Weekoverzichten gebruiken de week vanaf 5 oktober.",
"invalid_duration": "Vul een geheel aantal minuten tussen 1 en 1440 in.",
"invalid_activity": "Deze demo registreert alleen wandelactiviteiten.",
"invalid_check_in": "Controleer de check-in en de lengte van de optionele notitie.",
"invalid_consent": "Kies of je toestemming geeft om te delen.",
"invalid_request": "De hulpvraag vereist een actueel weekoverzicht van je actieve plan.",
"invalid_transition": "Deze actie past niet bij de huidige status. Herlaad en bekijk de volgende stap.",
"duplicate_request": "Er is al een open hulpvraag voor dit plan.",
"stale_version": "Het voorstel verwijst naar een oudere planversie. Herlaad voordat je verdergaat.",
"etag_mismatch": "Het plan is gewijzigd. De huidige gegevens zijn herladen. Bekijk ze voordat je aanvaardt.",
"precondition_required": "Herlaad om de huidige planversie op te halen voordat je verdergaat.",
"invalid_rationale": "Voeg een reden van maximaal 500 tekens toe.",
"invalid_decision": "Kies aanvaarden of afwijzen.",
"invalid_role": "Kies Deelnemer, Coach of Analist.",
"invalid_scenario": "Kies een van de beschikbare demoscenario's."
},
"finishReporting": "Registratie afronden zonder check-in",
"completedGuide": "De aanpassing is aanvaard. Bekijk de Analist-weergave om het eerdere weekoverzicht te volgen. De momentopname en je oorspronkelijke planversie blijven bewaard."
}
===== migrations/001_foundation.sql =====
CREATE TABLE demo_sessions (
id uuid PRIMARY KEY, token_hash text UNIQUE NOT NULL, actor text NOT NULL DEFAULT 'member' CHECK (actor IN ('member','coach','analyst')),
member_id text NOT NULL DEFAULT 'm-camille', scenario text NOT NULL DEFAULT 'guided' CHECK (scenario IN ('guided','camille','jules','noor')),
generation integer NOT NULL DEFAULT 1, revision integer NOT NULL DEFAULT 1,
created_at timestamptz NOT NULL DEFAULT now(), expires_at timestamptz NOT NULL DEFAULT now() + interval '24 hours'
);
CREATE TABLE members (session_id uuid REFERENCES demo_sessions ON DELETE CASCADE, id text NOT NULL, data jsonb NOT NULL, PRIMARY KEY(session_id,id));
CREATE TABLE plans (session_id uuid, id text NOT NULL, member_id text NOT NULL, data jsonb NOT NULL, PRIMARY KEY(session_id,id), FOREIGN KEY(session_id,member_id) REFERENCES members ON DELETE CASCADE);
CREATE UNIQUE INDEX one_current_plan ON plans(session_id,member_id) WHERE data->>'state' IN ('active','proposed');
CREATE TABLE plan_versions (session_id uuid, id text NOT NULL, plan_id text NOT NULL, data jsonb NOT NULL, PRIMARY KEY(session_id,id), FOREIGN KEY(session_id,plan_id) REFERENCES plans ON DELETE CASCADE);
CREATE TABLE activities (session_id uuid, id text NOT NULL, member_id text NOT NULL, data jsonb NOT NULL, PRIMARY KEY(session_id,id), FOREIGN KEY(session_id,member_id) REFERENCES members ON DELETE CASCADE);
CREATE TABLE check_ins (session_id uuid, id text NOT NULL, plan_id text NOT NULL, data jsonb NOT NULL, PRIMARY KEY(session_id,id), FOREIGN KEY(session_id,plan_id) REFERENCES plans ON DELETE CASCADE);
CREATE TABLE reviews (session_id uuid, id text NOT NULL, plan_id text NOT NULL, data jsonb NOT NULL, PRIMARY KEY(session_id,id), FOREIGN KEY(session_id,plan_id) REFERENCES plans ON DELETE CASCADE);
CREATE TABLE support_requests (session_id uuid, id text NOT NULL, member_id text NOT NULL, data jsonb NOT NULL, PRIMARY KEY(session_id,id), FOREIGN KEY(session_id,member_id) REFERENCES members ON DELETE CASCADE);
CREATE UNIQUE INDEX one_unresolved_support ON support_requests(session_id,(data->>'planId')) WHERE data->>'state' IN ('requested','proposed');
CREATE TABLE plan_changes (session_id uuid, id text NOT NULL, plan_id text NOT NULL, data jsonb NOT NULL, PRIMARY KEY(session_id,id), FOREIGN KEY(session_id,plan_id) REFERENCES plans ON DELETE CASCADE);
CREATE TABLE audit_events (session_id uuid REFERENCES demo_sessions ON DELETE CASCADE, id text NOT NULL, data jsonb NOT NULL, PRIMARY KEY(session_id,id));
CREATE TABLE idempotency_results (session_id uuid REFERENCES demo_sessions ON DELETE CASCADE, generation integer NOT NULL, key text NOT NULL, fingerprint text NOT NULL, data jsonb NOT NULL, PRIMARY KEY(session_id,generation,key));
CREATE INDEX session_expiry ON demo_sessions(expires_at);
===== next.config.ts =====
import createNextIntlPlugin from 'next-intl/plugin';
export default createNextIntlPlugin('./src/i18n/request.ts')({
poweredByHeader: false,
output: 'standalone',
async rewrites() {
return {beforeFiles: [{source: '/evidence/:document.md', destination: '/evidence/rendered/:document.html'}]};
},
experimental: {cpus: 1}
});
===== package.json =====
{
"name": "my-prevention-plan",
"version": "0.1.0",
"private": true,
"scripts": {
"predev": "node scripts/render-evidence.mjs",
"prebuild": "node scripts/render-evidence.mjs",
"dev": "next dev --webpack -H 127.0.0.1",
"build": "next build --webpack",
"start": "next start -H 127.0.0.1",
"typecheck": "tsc --noEmit",
"test": "vitest run tests/unit",
"test:integration": "vitest run tests/integration",
"test:e2e": "playwright test",
"db:migrate": "node --env-file=.env scripts/database.mjs migrate",
"db:cleanup": "node --env-file=.env scripts/database.mjs cleanup",
"db:local": "powershell -NoProfile -ExecutionPolicy Bypass -File scripts/local-postgres.ps1"
},
"engines": {
"node": ">=24.0.0"
},
"dependencies": {
"next": "16.4.0",
"next-intl": "4.14.9",
"pg": "8.23.1",
"react": "19.3.0",
"react-dom": "19.3.0"
},
"devDependencies": {
"@playwright/test": "1.64.0",
"@tailwindcss/postcss": "4.3.3",
"@types/node": "24.10.1",
"@types/pg": "8.15.6",
"@types/react": "19.2.7",
"@types/react-dom": "19.2.3",
"markdown-it": "15.0.2",
"mermaid": "12.2.0",
"tailwindcss": "4.3.3",
"typescript": "5.9.3",
"vitest": "5.0.4"
}
}
===== playwright.config.ts =====
import {defineConfig, devices} from '@playwright/test';
export default defineConfig({testDir: './tests/e2e', workers: 1, use: {baseURL: 'http://127.0.0.1:3000', trace: 'retain-on-failure'}, projects: [{name: 'chromium', use: {...devices['Desktop Chrome']}}], webServer: {command: 'npm run start', url: 'http://127.0.0.1:3000', reuseExistingServer: false, timeout: 60000}});
===== scripts/create-local-databases.mjs =====
import {Client} from 'pg';
const url = new URL(process.env.DATABASE_URL);
if (url.hostname !== '127.0.0.1' || url.port !== '55432') throw new Error('Only the isolated local cluster is allowed');
url.pathname = '/postgres';
const db = new Client({connectionString:url.toString()}); await db.connect();
try {for (const name of ['prevention_dev','prevention_test']) if (!(await db.query('SELECT 1 FROM pg_database WHERE datname=$1',[name])).rowCount) {await db.query(`CREATE DATABASE ${name}`); console.log(`Created ${name}`);}} finally {await db.end();}
===== scripts/database.mjs =====
import {Pool} from 'pg';
import {readdir, readFile} from 'node:fs/promises';
const url = process.env.DATABASE_URL;
if (!url) throw new Error('Configure DATABASE_URL in Code/.env');
const db = new Pool({connectionString: url});
try {
if (process.argv[2] === 'cleanup') {
const result = await db.query('DELETE FROM demo_sessions WHERE expires_at <= now()');
console.log(`Expired sessions removed: ${result.rowCount}`);
} else {
const client = await db.connect();
try {
await client.query('BEGIN');
await client.query("SELECT pg_advisory_xact_lock(723981)");
await client.query('CREATE TABLE IF NOT EXISTS schema_migrations (name text PRIMARY KEY, applied_at timestamptz DEFAULT now())');
for (const name of (await readdir(new URL('../migrations/', import.meta.url))).filter(n => n.endsWith('.sql')).sort()) {
if ((await client.query('SELECT 1 FROM schema_migrations WHERE name=$1', [name])).rowCount) continue;
await client.query(await readFile(new URL(`../migrations/${name}`, import.meta.url), 'utf8'));
await client.query('INSERT INTO schema_migrations(name) VALUES($1)', [name]); console.log(`Applied ${name}`);
}
await client.query('COMMIT');
} catch (e) {await client.query('ROLLBACK'); throw e;} finally {client.release();}
}
} finally {await db.end();}
===== scripts/local-postgres.ps1 =====
param([ValidateSet('start','stop')][string]$Action = 'start')
$ErrorActionPreference = 'Stop'
$codeRoot = Split-Path $PSScriptRoot -Parent
if ($codeRoot.StartsWith('H:', [StringComparison]::OrdinalIgnoreCase)) { throw 'Use scripts/run-local.ps1 to run from a local NTFS workspace, not Google Drive.' }
$localRoot = Join-Path $codeRoot '.local'
$bin = Join-Path $localRoot 'pgsql\bin'
$data = Join-Path $localRoot 'pgdata'
$envFile = Join-Path $codeRoot '.env'
if ($Action -eq 'stop') { & (Join-Path $bin 'pg_ctl.exe') -D $data -m fast stop; exit $LASTEXITCODE }
New-Item -ItemType Directory -Force -Path $localRoot | Out-Null
if (!(Test-Path -LiteralPath (Join-Path $bin 'initdb.exe'))) {
$zipFile = Join-Path $localRoot 'postgresql.zip'
if (!(Test-Path -LiteralPath $zipFile)) { Invoke-WebRequest -UseBasicParsing -Uri 'https://sbp.enterprisedb.com/getfile.jsp?fileid=1260616' -OutFile $zipFile }
Add-Type -AssemblyName System.IO.Compression.FileSystem
$archive = [IO.Compression.ZipFile]::OpenRead($zipFile)
try {
foreach ($entry in $archive.Entries) {
if ($entry.FullName -notmatch '^pgsql/(bin|lib|share)/' -or !$entry.Name) { continue }
$target = [IO.Path]::GetFullPath((Join-Path $localRoot $entry.FullName))
if (!$target.StartsWith(([IO.Path]::GetFullPath($localRoot) + '\'), [StringComparison]::OrdinalIgnoreCase)) { throw 'Unsafe archive path' }
New-Item -ItemType Directory -Force -Path (Split-Path $target -Parent) | Out-Null
[IO.Compression.ZipFileExtensions]::ExtractToFile($entry, $target, $true)
}
} finally { $archive.Dispose() }
}
if (!(Test-Path -LiteralPath (Join-Path $data 'PG_VERSION'))) {
if (Test-Path -LiteralPath $envFile) { throw 'Existing .env found. Configure PostgreSQL manually; it will not be overwritten.' }
$bytes = New-Object byte[] 32
$rng = [Security.Cryptography.RandomNumberGenerator]::Create(); $rng.GetBytes($bytes); $rng.Dispose()
$password = [BitConverter]::ToString($bytes).Replace('-','').ToLowerInvariant()
[IO.File]::WriteAllText($envFile, $password + "`n")
& (Join-Path $bin 'initdb.exe') -D $data -U postgres --encoding=UTF8 --locale=C --auth=scram-sha-256 --pwfile=$envFile
if ($LASTEXITCODE -ne 0) { throw 'initdb failed' }
Add-Content -LiteralPath (Join-Path $data 'postgresql.conf') -Value "`nlisten_addresses = '127.0.0.1'`nport = 55432`n"
[IO.File]::WriteAllText($envFile, "DATABASE_URL=postgresql://postgres:${password}@127.0.0.1:55432/prevention_dev`nTEST_DATABASE_URL=postgresql://postgres:${password}@127.0.0.1:55432/prevention_test`nAPP_ORIGIN=http://127.0.0.1:3000`n")
}
$statusProcess = Start-Process -FilePath (Join-Path $bin 'pg_ctl.exe') -ArgumentList @('-D', ('"' + $data + '"'), 'status') -WindowStyle Hidden -Wait -PassThru -RedirectStandardOutput (Join-Path $localRoot 'postgres-status.log') -RedirectStandardError (Join-Path $localRoot 'postgres-status-error.log')
if ($statusProcess.ExitCode -ne 0) {
& (Join-Path $bin 'pg_ctl.exe') -D $data -l (Join-Path $localRoot 'postgresql.log') -w start
if ($LASTEXITCODE -ne 0) { throw 'PostgreSQL startup failed' }
}
Push-Location $codeRoot
try { node --env-file=.env scripts/create-local-databases.mjs; if ($LASTEXITCODE -ne 0) { throw 'Database creation failed' } } finally { Pop-Location }
===== scripts/migrate-test.mjs =====
import {spawnSync} from 'node:child_process';
const url = new URL(process.env.TEST_DATABASE_URL);
if (!['127.0.0.1','localhost'].includes(url.hostname) || url.pathname !== '/prevention_test' || process.env.TEST_DATABASE_URL === process.env.DATABASE_URL) throw new Error('Use an isolated local prevention_test database');
const result = spawnSync(process.execPath,['scripts/database.mjs','migrate'], {stdio:'inherit',env:{...process.env,DATABASE_URL:process.env.TEST_DATABASE_URL}});
process.exit(result.status ?? 1);
===== scripts/preview.ps1 =====
param([switch]$HostProcess)
$ErrorActionPreference = 'Stop'
$runtimePath = Join-Path $env:LOCALAPPDATA 'PreventionPlanRuntime'
$origin = 'http://127.0.0.1:3000'
if ($HostProcess) {
Set-Location -LiteralPath $runtimePath
$log = Join-Path $runtimePath '.local\application.log'
try {
# Do not wrap pg_ctl startup in a PowerShell pipeline: daemon descendants
# can retain its output handles and prevent this host from reaching Next.js.
& ./scripts/local-postgres.ps1
if ($LASTEXITCODE -ne 0) { throw 'Local PostgreSQL startup failed' }
$node = (Get-Command node.exe -ErrorAction Stop).Source
$app = Start-Process -FilePath $node -ArgumentList @('node_modules/next/dist/bin/next','start','-H','127.0.0.1') -WorkingDirectory $runtimePath -WindowStyle Hidden -Wait -PassThru -RedirectStandardOutput $log -RedirectStandardError (Join-Path $runtimePath '.local\application-error.log')
exit $app.ExitCode
} catch {
Add-Content -LiteralPath $log -Value ("Local preview startup failed at {0}:{1}, category {2}." -f $_.InvocationInfo.ScriptName, $_.InvocationInfo.ScriptLineNumber, $_.FullyQualifiedErrorId)
exit 1
}
}
& (Join-Path $PSScriptRoot 'run-local.ps1') sync
if (!(Test-Path -LiteralPath (Join-Path $runtimePath '.next\BUILD_ID'))) {
throw 'Build the application first using scripts/run-local.ps1 build.'
}
function Test-Preview {
try {
$response = Invoke-WebRequest -Uri "$origin/api/v1/demo/session" -UseBasicParsing -TimeoutSec 3
return $response.StatusCode -eq 200
} catch {
# A missing cookie must give the application's JSON 401, proving the API is listening.
return $_.Exception.Response -and [int]$_.Exception.Response.StatusCode -eq 401
}
}
if (!(Test-Preview)) {
if (Get-NetTCPConnection -LocalPort 3000 -State Listen -ErrorAction SilentlyContinue) {
throw 'Port 3000 is occupied by another process. It has not been stopped.'
}
$powershell = Join-Path $env:SystemRoot 'System32\WindowsPowerShell\v1.0\powershell.exe'
$hostScript = Join-Path $runtimePath 'scripts\preview.ps1'
# WMI owns this hidden host process, independently of the terminal/agent process.
$startup = New-CimInstance -ClassName Win32_ProcessStartup -ClientOnly -Property @{ShowWindow = [uint16]0}
$result = Invoke-CimMethod -ClassName Win32_Process -MethodName Create -Arguments @{
CommandLine = '"' + $powershell + '" -NoProfile -NonInteractive -WindowStyle Hidden -ExecutionPolicy Bypass -File "' + $hostScript + '" -HostProcess'
CurrentDirectory = $runtimePath
ProcessStartupInformation = $startup
}
if ($result.ReturnValue -ne 0) { throw "Independent preview launch failed (WMI code $($result.ReturnValue))." }
Set-Content -LiteralPath (Join-Path $runtimePath '.local\preview-host.pid') -Value $result.ProcessId
}
for ($attempt = 0; $attempt -lt 40; $attempt++) {
if (Test-Preview) {
$page = Invoke-WebRequest -Uri "$origin/en/demo" -UseBasicParsing -TimeoutSec 5
if ($page.StatusCode -eq 200) { Write-Output "Preview ready: $origin/en/demo"; exit 0 }
}
Start-Sleep -Milliseconds 250
}
throw 'Preview did not become ready. Check the runtime .local/application.log, application-error.log and postgresql.log.'
===== scripts/render-evidence.mjs =====
import {mkdir, readdir, readFile, writeFile, copyFile, cp, stat} from 'node:fs/promises';
import {createRequire} from 'node:module';
import {fileURLToPath} from 'node:url';
import path from 'node:path';
import MarkdownIt from 'markdown-it';
const markdown = new MarkdownIt({html: false, linkify: true});
const escape = markdown.utils.escapeHtml;
const defaultFence = markdown.renderer.rules.fence;
markdown.renderer.rules.fence = (tokens, index, options, env, renderer) => {
if (tokens[index].info.trim() !== 'mermaid') return defaultFence(tokens, index, options, env, renderer);
env.hasDiagrams = true;
return `
Loading diagram. Scroll horizontally to see wide diagrams.
\n';
export function renderDocument(source, filename, files = new Set()) {
source = source.replace(/^\uFEFF/, '');
const title = source.match(/^#\s+(.+)$/m)?.[1] ?? filename;
const env = {files};
const body = markdown.render(source, env);
return `
${escape(title)} | My Prevention PlanSkip to document
MY PREVENTION PLAN / DESIGN EVIDENCE
${body}
${env.hasDiagrams ? '' : ''}\n`;
}
export async function renderEvidence(root = fileURLToPath(new URL('../', import.meta.url))) {
const evidence = path.join(root, 'public', 'evidence');
await mkdir(path.join(evidence, 'rendered'), {recursive: true});
await mkdir(path.join(evidence, 'raw'), {recursive: true});
// Host the pinned browser renderer and its lazy-loaded chunks on this site.
const require = createRequire(import.meta.url);
const mermaid = path.dirname(require.resolve('mermaid/package.json'));
await cp(path.join(mermaid, 'dist'), path.join(evidence, 'vendor', 'mermaid'), {
recursive: true,
filter: async source => (await stat(source)).isDirectory() || source.endsWith('.mjs')
});
await copyFile(path.join(mermaid, 'LICENSE'), path.join(evidence, 'vendor', 'mermaid', 'LICENSE'));
let count = 0;
const files = new Set(await readdir(evidence));
for (const filename of files) {
if (!filename.endsWith('.md')) continue;
const source = await readFile(path.join(evidence, filename), 'utf8');
await writeFile(path.join(evidence, 'rendered', filename.replace(/\.md$/, '.html')), renderDocument(source, filename, files));
await copyFile(path.join(evidence, filename), path.join(evidence, 'raw', filename));
count++;
}
console.log(`Rendered ${count} evidence documents`);
}
if (process.argv[1] && path.resolve(process.argv[1]) === fileURLToPath(import.meta.url)) await renderEvidence();
===== scripts/run-local.ps1 =====
param([ValidateSet('setup','dev','build','start','typecheck','test','integration','e2e','sync','stop')][string]$Task = 'dev')
$ErrorActionPreference = 'Stop'
$source = Split-Path $PSScriptRoot -Parent
$runtimePath = Join-Path $env:LOCALAPPDATA 'PreventionPlanRuntime'
New-Item -ItemType Directory -Force -Path $runtimePath | Out-Null
if ([IO.Path]::GetFullPath($source) -ne [IO.Path]::GetFullPath($runtimePath)) {
robocopy $source $runtimePath /E /XD .git .next .local node_modules playwright-report test-results /XF .env .env.local /NFL /NDL /NJH /NJS
if ($LASTEXITCODE -ge 8) { throw 'Runtime synchronization failed' }
}
Push-Location $runtimePath
try {
if ($Task -eq 'sync') { Write-Output "Runtime synchronized: $runtimePath"; exit 0 }
if ($Task -eq 'stop') { & ./scripts/local-postgres.ps1 stop; exit $LASTEXITCODE }
$env:NODE_OPTIONS = '--use-system-ca'
if ($Task -in @('dev','start','integration','e2e')) { & ./scripts/local-postgres.ps1 }
if ($Task -eq 'setup') {
npm ci; if ($LASTEXITCODE -ne 0) { throw 'Dependency installation failed' }
& ./scripts/local-postgres.ps1
npm run db:migrate; if ($LASTEXITCODE -ne 0) { throw 'Migration failed' }
node --env-file=.env scripts/migrate-test.mjs
if ($LASTEXITCODE -ne 0) { throw 'Test database migration failed' }
npx playwright install chromium
} elseif ($Task -eq 'integration') { npm run test:integration }
elseif ($Task -eq 'e2e') { npm run test:e2e }
else { npm run $Task }
if ($LASTEXITCODE -ne 0) { throw "Task failed: $Task" }
} finally {Pop-Location}
===== scripts/source-revision.mjs =====
import {createHash} from 'node:crypto';
import {readFile,readdir,writeFile} from 'node:fs/promises';
import {fileURLToPath} from 'node:url';
import path from 'node:path';
const root=fileURLToPath(new URL('../',import.meta.url));const files=[];
async function scan(dir){for(const entry of await readdir(path.join(root,dir),{withFileTypes:true})){const f=path.join(dir,entry.name);if(entry.isDirectory())await scan(f);else files.push(f.replaceAll('\\','/'));}}
for(const dir of ['src','messages','migrations','tests','scripts','contracts','fixtures'])await scan(dir);
files.push('package.json','package-lock.json','tsconfig.json','next.config.ts','postcss.config.mjs','playwright.config.ts','vitest.config.ts');
const aggregate=createHash('sha256');const manifest=[];
for(const file of files.sort()){const data=await readFile(path.join(root,file));const sha256=createHash('sha256').update(data).digest('hex');aggregate.update(file+'\0'+sha256+'\n');manifest.push({file,sha256});}
const result={algorithm:'SHA-256',revision:aggregate.digest('hex'),files:manifest};
if(process.argv[2])await writeFile(process.argv[2],JSON.stringify(result,null,2)+'\n');
console.log(result.revision);
===== scripts/sync-evidence.mjs =====
import {mkdir, readdir, readFile, writeFile, copyFile} from 'node:fs/promises';
import {fileURLToPath} from 'node:url';
import path from 'node:path';
const root = fileURLToPath(new URL('../', import.meta.url));
const documentation = process.argv[2];
if (!documentation) throw new Error('Pass the Project documentation folder explicitly');
const destination = path.join(root,'public','evidence'); await mkdir(destination,{recursive:true});
for (const name of await readdir(path.join(documentation,'FA Deliverables'))) if (name.endsWith('.md')) await writeFile(path.join(destination,name),(await readFile(path.join(documentation,'FA Deliverables',name),'utf8')).replaceAll('\u2014','-'));
for (const name of ['r1-devready-tickets.md','r1-process.bpmn','r1-uml.md','verification.md','ai-delivery-evidence.md']) await copyFile(path.join(documentation,name),path.join(destination,name));
await mkdir(path.join(destination,'verification-assets'),{recursive:true});
for(const name of await readdir(path.join(documentation,'verification-assets'))) if(/\.(png|json)$/.test(name)) await copyFile(path.join(documentation,'verification-assets',name),path.join(destination,'verification-assets',name));
await copyFile(path.join(root,'contracts','openapi.json'),path.join(destination,'openapi.json'));
const files=[];
async function scan(dir) {for(const entry of await readdir(path.join(root,dir),{withFileTypes:true})) {const relative=path.join(dir,entry.name);if(entry.isDirectory()) await scan(relative);else if(/\.(tsx?|mjs|ps1|sql|json|css)$/.test(entry.name)) files.push(relative);}}
for(const folder of ['src','tests','messages','migrations','scripts']) await scan(folder);
files.push('package.json','next.config.ts','playwright.config.ts','vitest.config.ts');
const source=await Promise.all(files.sort().map(async f => `\n===== ${f.replaceAll('\\','/')} =====\n${await readFile(path.join(root,f),'utf8')}`));
await writeFile(path.join(destination,'implementation-source.txt'),`My Prevention Plan source snapshot. Generated from an explicit allowlist; environment files and runtime data are excluded.\n${source.join('\n')}`);
console.log(`Exported FA documents and ${files.length} source files`);
===== scripts/update-r1-contract.mjs =====
import {readFile,writeFile} from 'node:fs/promises';
const url = new URL('../contracts/openapi.json',import.meta.url);
const api=JSON.parse(await readFile(url,'utf8'));
api.info.version='0.2.0';api.info.description='Portfolio R1 local implementation. Synthetic data only. POC extensions are explicitly marked planned.';
const implemented = new Set(['/members/me','/members/me/consents/{purpose}','/plans','/plans/{planId}','/activities','/plans/{planId}/check-ins/{weekStart}','/plans/{planId}/reviews','/support-requests','/coach/support-requests','/plans/{planId}/changes','/plans/{planId}/changes/{changeId}/decision','/analyst/decisions']);
for(const [p,item] of Object.entries(api.paths)) for(const [method,op] of Object.entries(item)) {
if (!['get','post','patch','put','delete'].includes(method)) continue;
op['x-r1-status']=implemented.has(p) && !(p==='/activities' && !['get','post'].includes(method)) ? 'implemented-local':'planned-poc';
if(op['x-r1-status']==='implemented-local' && method !=='get') op.parameters=[...(op.parameters??[]).filter(p=>!['X-Demo-Generation','X-Demo-Revision'].includes(p.name)), ...['X-Demo-Generation','X-Demo-Revision'].map(name=>({in:'header',name,required:true,schema:{type:'integer',minimum:1}}))];
}
const schemas=api.components.schemas;
schemas.DemoSession={type:'object',required:['generation','revision','role','scenario','expiresAt'],properties:{generation:{type:'integer'},revision:{type:'integer'},role:{enum:['member','coach','analyst']},scenario:{enum:['guided','camille','jules','noor']},expiresAt:{type:'string',format:'date-time'}}};
const controls={
'/demo/start':['post','startDemo',{type:'object'},false],
'/demo/session':['get','readDemoSession',null,true],
'/demo/state':['get','readRoleScopedDemoState',null,true],
'/demo/role':['post','switchDemoRole',{type:'object',required:['role'],properties:{role:{enum:['member','coach','analyst']}}},true],
'/demo/scenario':['post','loadDemoScenario',{type:'object',required:['scenario'],properties:{scenario:{enum:['guided','camille','jules','noor']}}},true],
'/demo/reset':['post','resetOwnDemo',{type:'object'},true]
};
for(const [p,[method,operationId,schema,authorized]] of Object.entries(controls)) {
const op={operationId,summary:operationId,'x-r1-status':'implemented-local',security:authorized?[{demoSession:[]}]:[],responses:{'200':{description:'Current session or role-scoped state, no-store',content:{'application/json':{schema:p==='/demo/state'?{type:'object',description:'Session envelope plus member/coach programme context, or analyst decisions. Protected coach view fails when sharing is off.'}:{$ref:'#/components/schemas/DemoSession'}}}},'401':{description:'Session missing or expired'},'409':{description:'Stale generation/actor or idempotency conflict'},'503':{description:'Save not confirmed'}}};
if(schema) op.requestBody={required:true,content:{'application/json':{schema}}};
if(schema && authorized) op.parameters=[{in:'header',name:'Idempotency-Key',required:true,schema:{type:'string',minLength:8,maxLength:100}},...['X-Demo-Generation','X-Demo-Revision'].map(name=>({in:'header',name,required:true,schema:{type:'integer',minimum:1}}))];
api.paths[p]={[method]:op};
}
api.components.securitySchemes.demoSession={type:'apiKey',in:'cookie',name:'prevention_demo',description:'Opaque HttpOnly SameSite=Lax token. Only its SHA-256 digest is stored. Session actor is server-controlled.'};
api.paths['/activities'].get={operationId:'listActivities','x-r1-status':'implemented-local',responses:{'200':{description:'Own manual activity items'}}};
api.paths['/plans/{planId}/changes'].get={operationId:'listPlanChanges','x-r1-status':'implemented-local',parameters:[{in:'path',name:'planId',required:true,schema:{type:'string'}}],responses:{'200':{description:'Owner or authorized assigned-coach change history'}}};
// Preserve the full-POC schemas and use separate R1 schemas for the narrowed contract.
const clone = name => structuredClone(schemas[name]);
const ref = name => ({$ref:`#/components/schemas/${name}`});
const object = (properties,required=[]) => ({type:'object',properties,required,additionalProperties:false});
schemas.R1Member=clone('Member');delete schemas.R1Member.properties.locale;delete schemas.R1Member.properties.timezone;schemas.R1Member.required=schemas.R1Member.required.filter(p=>!['locale','timezone'].includes(p));schemas.R1Member.properties.reportingComplete={type:'boolean'};
schemas.R1MemberUpdate={anyOf:[ref('Preferences'),object({preferences:ref('Preferences')},['preferences']),object({reportingComplete:{type:'boolean'}},['reportingComplete'])]};
schemas.R1Plan=clone('Plan');schemas.R1Plan.properties.state.enum=['proposed','active','declined'];schemas.R1Plan.properties.currentVersion.minimum=0;
schemas.R1PlanVersion=clone('PlanVersion');schemas.R1PlanVersion.properties.id={type:'string'};schemas.R1PlanVersion.properties.planId={type:'string'};schemas.R1Plan.properties.versions.items=ref('R1PlanVersion');
schemas.R1PlanAction=object({action:{enum:['accept','decline']}},['action']);
schemas.R1ConsentChoice=object({granted:{type:'boolean'}},['granted']);
schemas.R1CheckInInput=clone('CheckInInput');schemas.R1CheckInInput.properties.note.maxLength=500;schemas.R1CheckInInput.properties.barriers.maxItems=3;
schemas.R1CheckIn=clone('CheckIn');schemas.R1CheckIn.properties.note.maxLength=500;schemas.R1CheckIn.properties.barriers.maxItems=3;
schemas.R1ReviewRequest=object({weekStart:{enum:['2026-10-05']}},['weekStart']);
schemas.R1Review=clone('Review');schemas.R1Review.properties.recordedCount={type:'integer',minimum:0};schemas.R1Review.properties.createdAt={type:'string',format:'date-time'};schemas.R1Review.properties.snapshot=object({difficulty:{type:['string','null']},reportingComplete:{type:'boolean'},recordedCount:{type:'integer'}},['difficulty','reportingComplete','recordedCount']);
schemas.R1Support=clone('SupportRequest');schemas.R1Support.properties.state.enum=['requested','proposed','closed','cancelled'];
schemas.R1ChangeInput=clone('ChangeInput');schemas.R1ChangeInput.properties.rationale.maxLength=500;
schemas.R1Change=clone('PlanChange');schemas.R1Change.properties.state.enum.push('cancelled');schemas.R1Change.properties.rationale.maxLength=500;
schemas.R1Decision=object({id:{type:'string'},memberRef:{type:'string'},ruleId:{type:'string'},ruleVersion:{type:'string'},outcome:{type:'string'},inputCategories:object({difficulty:{type:['string','null']},completeness:{enum:['complete','incomplete']},recordedCount:{type:'integer'}},['difficulty','completeness','recordedCount']),planVersion:{type:'integer'},reviewRevision:{type:'integer'},templateId:{type:'string'},templateVersion:{type:'integer'},createdAt:{type:'string',format:'date-time'}},['id','memberRef','ruleId','ruleVersion','outcome','inputCategories','planVersion','reviewRevision','templateId','templateVersion','createdAt']);
const requestOverrides=[['/members/me','patch','R1MemberUpdate'],['/members/me/consents/{purpose}','put','R1ConsentChoice'],['/plans/{planId}','patch','R1PlanAction'],['/plans/{planId}/check-ins/{weekStart}','put','R1CheckInInput'],['/plans/{planId}/reviews','post','R1ReviewRequest'],['/plans/{planId}/changes','post','R1ChangeInput']];
for(const [p,m,n]of requestOverrides)api.paths[p][m].requestBody.content['application/json'].schema=ref(n);
api.paths['/plans'].post.requestBody.content['application/json'].schema=object({});
const responseOverrides=[['/members/me','get','R1Member'],['/members/me','patch','R1Member'],['/plans','post','R1Plan'],['/plans/{planId}','get','R1Plan'],['/plans/{planId}','patch','R1Plan'],['/plans/{planId}/check-ins/{weekStart}','put','R1CheckIn'],['/plans/{planId}/reviews','post','R1Review'],['/support-requests','post','R1Support'],['/plans/{planId}/changes','post','R1Change'],['/plans/{planId}/changes/{changeId}/decision','post','R1Plan']];
for(const[p,m,n]of responseOverrides){api.paths[p][m].responses['200']??={description:'Successful R1 operation'};api.paths[p][m].responses['200'].content={'application/json':{schema:ref(n)}};}
for(const[p,n]of [['/coach/support-requests','R1Support'],['/plans/{planId}/changes','R1Change'],['/analyst/decisions','R1Decision']])api.paths[p].get.responses['200'].content={'application/json':{schema:object({items:{type:'array',items:ref(n)}},['items'])}};
for(const p of ['/plans','/activities','/support-requests','/plans/{planId}/reviews','/plans/{planId}/changes']){api.paths[p].post.responses['201']=structuredClone(api.paths[p].post.responses['200']??api.paths[p].post.responses['201']);delete api.paths[p].post.responses['200'];}
api.paths['/demo/start'].post.responses['201']=structuredClone(api.paths['/demo/start'].post.responses['200']);
await writeFile(url,JSON.stringify(api,null,2)+'\n');
===== scripts/verify-local.mjs =====
import {spawn} from 'node:child_process';
import {mkdir,writeFile} from 'node:fs/promises';
import {chromium} from '@playwright/test';
import path from 'node:path';
const origin='http://127.0.0.1:3001';
let server;
const delay=ms=>new Promise(resolve=>setTimeout(resolve,ms));
async function start() {
server=spawn(process.execPath,['node_modules/next/dist/bin/next','start','-p','3001','-H','127.0.0.1'],{env:{...process.env,APP_ORIGIN:origin,NEXT_TELEMETRY_DISABLED:'1'},stdio:'ignore',windowsHide:true});
for(let i=0;i<100;i++){try{if((await fetch(origin+'/en')).ok)return;}catch{}await delay(150);}
throw new Error('Production startup failed');
}
async function stop(){if(!server || server.exitCode!==null)return;const exit=new Promise(resolve=>server.once('exit',resolve));server.kill();await exit;}
let cookie='',generation=1,revision=1;
async function api(route,body,method=body===undefined?'GET':'POST'){
const headers={Origin:origin,...(cookie?{Cookie:cookie}:{})};
if(body!==undefined)Object.assign(headers,{'Content-Type':'application/json','Idempotency-Key':crypto.randomUUID(),'X-Demo-Generation':String(generation),'X-Demo-Revision':String(revision)});
const response=await fetch(origin+'/api/v1/'+route,{method,headers,body:body===undefined?undefined:JSON.stringify(body)});
const data=await response.json();if(!response.ok)throw new Error(data.code);
if(response.headers.get('set-cookie'))cookie=response.headers.get('set-cookie').split(';')[0];
if(data.generation){generation=data.generation;revision=data.revision;}return data;
}
const output=process.argv[2];if(!output)throw new Error('Pass an output evidence directory');await mkdir(output,{recursive:true});
try{
await start();await api('demo/start',{});await api('demo/scenario',{scenario:'camille'});
await api('activities',{occurredOn:'2026-10-07',durationMinutes:17,activityType:'walking'});
const before=await api('demo/state');await stop();await start();const after=await api('demo/state');
if(JSON.stringify(before)!==JSON.stringify(after))throw new Error('TC38 restart persistence mismatch');
const times=[];for(let i=0;i<100;i++){const t=performance.now();await api('plans/plan-camille/reviews',{weekStart:'2026-10-05'});times.push(performance.now()-t);}
times.sort((a,b)=>a-b);const p95=Number(times[94].toFixed(2));
const browser=await chromium.launch();
try{
const page=await browser.newPage({viewport:{width:1440,height:1000}});
await page.goto(origin+'/en');await page.screenshot({path:path.join(output,'case-desktop.png'),fullPage:true});
const links=await page.locator('a[href^="/evidence/"]').evaluateAll(els=>els.map(e=>e.getAttribute('href')));
for(const link of links)if(!(await fetch(origin+link)).ok)throw new Error('Broken evidence link: '+link);
await page.goto(origin+'/en/demo');await page.getByRole('heading',{name:'What fits your week?'}).waitFor();
await page.screenshot({path:path.join(output,'member-desktop.png'),fullPage:true});
await page.keyboard.press('Tab');await page.keyboard.press('Enter');
// The skip link must move keyboard focus into the main region.
const focus=await page.evaluate(()=>document.activeElement?.id);if(focus!=='main')throw new Error('Skip-link keyboard focus failed');
await page.setViewportSize({width:360,height:800});await page.goto(origin+'/nl-BE/demo');await page.getByRole('heading',{name:'Wat past in je week?'}).waitFor();
await page.screenshot({path:path.join(output,'member-mobile-nl.png'),fullPage:true});
if(!(await page.evaluate(()=>document.documentElement.scrollWidth<=window.innerWidth)))throw new Error('Mobile overflow');
await page.getByLabel('Verken een scenario').selectOption('camille');await page.getByRole('button',{name:'Weekoverzicht berekenen'}).click();await page.getByRole('heading',{name:'Ondersteuning is beschikbaar als je dat wilt'}).waitFor();
await page.screenshot({path:path.join(output,'review-mobile-nl.png'),fullPage:true});
}finally{await browser.close();}
const result={executedAt:new Date().toISOString(),runtime:process.version,postgresql:'17.11',restartPersistence:'passed',reviewBenchmark:{requests:100,p95Ms:p95,targetMs:1000,passed:p95<=1000,mode:'production HTTP, local PostgreSQL, sequential single visitor, growing immutable history'},evidenceLinks:'passed',mobileOverflow:'passed',keyboardSkipLink:'passed'};
await writeFile(path.join(output,'local-checks.json'),JSON.stringify(result,null,2)+'\n');console.log(JSON.stringify(result,null,2));
if(p95>1000)throw new Error('NFR06 benchmark exceeded target');
}finally{await stop();}
===== src/app/[locale]/demo/page.tsx =====
import Demo from '@/components/demo';
export default function DemoPage() {return ;}
===== src/app/[locale]/layout.tsx =====
import {NextIntlClientProvider} from 'next-intl';
import {setRequestLocale} from 'next-intl/server';
import {notFound} from 'next/navigation';
export const metadata = {title: 'My Prevention Plan | Portfolio demo', description: 'A fictional prevention-support case: member choice, coach handoff and traceable decisions.'};
export default async function LocaleLayout({children, params}: {children: React.ReactNode; params: Promise<{locale: string}>}) {
const {locale} = await params; if (!['en','nl-BE'].includes(locale)) notFound();
setRequestLocale(locale);
const messages = (await import(`../../../messages/${locale}.json`)).default;
return {children};
}
===== src/app/[locale]/page.tsx =====
import Link from 'next/link';
export default async function CasePage({params}: {params: Promise<{locale: string}>}) {
const {locale} = await params;
return mpMy Prevention Plan
FUNCTIONAL ANALYSIS · WORKING DEMONSTRATOR
A small plan. A clear next step.
Making prevention support understandable, with member choice at every handoff.
Follow a fictional member from agreeing a walking plan to requesting support, reviewing a coach proposal and choosing whether to accept it. Then inspect the rule behind the decision.
Explore the guided demo ↗
No registration. Your own isolated dataset. English and Belgian Dutch.
FROM ANALYSIS TO BEHAVIOUR
See how it was designed
Analysis
Requirements, rules, models and acceptance cases behind this synthetic service.