Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
162 commits
Select commit Hold shift + click to select a range
1da054b
docs(caring-contacts): binding spec for the synthetic production build
BigSimmo Aug 18, 2026
3634171
docs(caring-contacts): rename to Caring Contacts and add the missing …
BigSimmo Aug 18, 2026
32d408c
feat(caring-contacts): correct the reply boundary, verify the evidenc…
BigSimmo Aug 18, 2026
64f91fb
feat(caring-contacts): seal the domain directory and add the injected…
BigSimmo Aug 18, 2026
b141d5e
docs(caring-contacts): restructure to three buildable phases and stri…
BigSimmo Aug 18, 2026
2842b8d
feat(caring-contacts): plan and contact lifecycles with named refusal…
BigSimmo Aug 18, 2026
0f116d8
feat(caring-contacts): discharge-anchored twelve-month schedule with …
BigSimmo Aug 18, 2026
6729f4e
feat(caring-contacts): hospital status events with irreversible death…
BigSimmo Aug 18, 2026
bd87838
feat(caring-contacts): deny-by-default team-scoped permissions with n…
BigSimmo Aug 18, 2026
143cfc8
feat(caring-contacts): governed message validation with a replaceable…
BigSimmo Aug 18, 2026
00dc494
feat(caring-contacts): frozen audit events that reject patient data
BigSimmo Aug 18, 2026
12e99ad
feat(caring-contacts): configurable retention with idempotent de-iden…
BigSimmo Aug 18, 2026
d9d8e71
feat(caring-contacts): repository contract with idempotent, atomicall…
BigSimmo Aug 18, 2026
71505ad
feat(caring-contacts): system actor and the contact-status write path
BigSimmo Aug 18, 2026
b560951
fix(caring-contacts): record hospital status events as themselves, no…
BigSimmo Aug 18, 2026
420d75b
feat(caring-contacts): deterministic twelve-month simulation proving …
BigSimmo Aug 18, 2026
57270cb
feat(caring-contacts): team-scoped Postgres schema with transactional…
BigSimmo Aug 19, 2026
a7b0093
fix(caring-contacts): make the pause-event kinds type-only so lint pa…
BigSimmo Aug 19, 2026
4be76ab
docs(caring-contacts): tracked Phase 1 handoff so nothing depends on …
BigSimmo Aug 19, 2026
e68445c
docs(caring-contacts): Phase 2A foundations plan
BigSimmo Aug 19, 2026
631e699
refactor(caring-contacts): move patient-visible copy into the sealed …
BigSimmo Aug 19, 2026
27a7816
feat(caring-contacts): name the approval roles and the ten actions th…
BigSimmo Aug 19, 2026
c976ff9
feat(caring-contacts): service safety stop with three-person restart …
BigSimmo Aug 19, 2026
6434817
fix(caring-contacts): make the banner unable to see the incident note…
BigSimmo Aug 19, 2026
33d38ca
feat(caring-contacts): pathway version lifecycle with dual approval a…
BigSimmo Aug 19, 2026
9c43268
test(caring-contacts): cover pathway-not-retirable and success timest…
BigSimmo Aug 19, 2026
9634deb
feat(caring-contacts): referral lifecycle and duplicate-referral routing
BigSimmo Aug 19, 2026
9f51d5b
feat(caring-contacts): plan ownership, reassignment history and coverage
BigSimmo Aug 19, 2026
664ec7d
feat(caring-contacts): within-day contact moves and approved date cha…
BigSimmo Aug 19, 2026
a6c73e9
feat(caring-contacts): typed access-audit events so views can enter t…
BigSimmo Aug 19, 2026
2074119
feat(caring-contacts): opt-in alert classes with identifier-free bodies
BigSimmo Aug 19, 2026
8a5a4aa
feat(caring-contacts): training competencies and live/training data s…
BigSimmo Aug 19, 2026
3272c87
fix(caring-contacts): allowlist objectId identifier shape, not just m…
BigSimmo Aug 19, 2026
88e774c
chore(issues): record the Caring Contacts singleton safety-stop requi…
BigSimmo Aug 19, 2026
6bf9f63
feat(caring-contacts): extend the storage contract for referrals, pat…
BigSimmo Aug 19, 2026
d14a601
docs(caring-contacts): track the Phase 2A build record so nothing dep…
BigSimmo Aug 19, 2026
711f950
docs(caring-contacts): sync the tracked build record with the Task 10…
BigSimmo Aug 19, 2026
944ce32
fix(caring-contacts): close the pathway-version governance bypass and…
BigSimmo Aug 19, 2026
e717b9d
docs(caring-contacts): record Task 10 complete and the Task 11 split
BigSimmo Aug 19, 2026
8b55760
feat(caring-contacts): workspace schema with a singleton service stop…
BigSimmo Aug 19, 2026
8d7319c
fix(caring-contacts): immutable incident rows, same-team assignment k…
BigSimmo Aug 19, 2026
6afce38
wip(caring-contacts): Task 11a fix round 2 — UNVERIFIED, mutations no…
BigSimmo Aug 20, 2026
d2bc957
docs(caring-contacts): archive the Phase 2A build history for session…
BigSimmo Aug 20, 2026
343bedc
docs(caring-contacts): add the Phase 2A continuation prompt
BigSimmo Aug 20, 2026
2c5b066
docs(caring-contacts): verify 6afce3893 and record the fix-round-2 re…
BigSimmo Aug 20, 2026
b273e95
fix(caring-contacts): incident restarts are write-once and the guard …
BigSimmo Aug 20, 2026
60f279c
docs(caring-contacts): record fix round 3 evidence and the one open v…
BigSimmo Aug 20, 2026
c3d211e
docs(caring-contacts): close the open verification — restored tree fu…
BigSimmo Aug 21, 2026
2558d51
docs(caring-contacts): repoint the handoff at the surviving worktree
BigSimmo Aug 21, 2026
6322017
docs(caring-contacts): the tracked build record is now the ledger itself
BigSimmo Aug 21, 2026
0ff792c
docs(caring-contacts): make the handoff true after the worktree loss
BigSimmo Aug 21, 2026
5a8c74d
docs(caring-contacts): rewrite the continuation prompt for the next s…
BigSimmo Aug 21, 2026
dcf48b5
docs(caring-contacts): continuation prompt gains the full context map
BigSimmo Aug 21, 2026
a15127d
docs(caring-contacts): master progress ledger, and make the SDD works…
BigSimmo Aug 21, 2026
1946cb7
docs(caring-contacts): final handoff prompt with the complete file map
BigSimmo Aug 21, 2026
32bfbda
style(caring-contacts): format five files the push guard flagged
BigSimmo Aug 21, 2026
2858690
docs(caring-contacts): retract 'relocate to be safe' — only pushing p…
BigSimmo Aug 21, 2026
43c3b81
docs(caring-contacts): record session 4 baseline and Ruling 35 before…
BigSimmo Aug 21, 2026
5bc86bf
docs(caring-contacts): Ruling 36 — stopId stays internal to the Postg…
BigSimmo Aug 21, 2026
259e5fa
test(caring-contacts): hold BOTH stores to Task 10's behaviour, and r…
BigSimmo Aug 21, 2026
428d9d1
feat(caring-contacts): the Postgres store satisfies the extended repo…
BigSimmo Aug 21, 2026
e65f597
docs(caring-contacts): Rulings 37-38 and the Task 11b evidence
BigSimmo Aug 21, 2026
83003e6
docs(caring-contacts): Task 11b review outcome, Rulings 39-40
BigSimmo Aug 21, 2026
870509f
docs(caring-contacts): track the Task 12-19 briefs
BigSimmo Aug 21, 2026
85e7b7a
fix(caring-contacts): the review's five findings — pinned instants, a…
BigSimmo Aug 21, 2026
cfa7016
docs(caring-contacts): fix round 1 evidence, Rulings 41-43
BigSimmo Aug 21, 2026
495ae3f
test(caring-contacts): close the vacuous-pass hole in the race proof,…
BigSimmo Aug 21, 2026
e1e1d7d
docs(caring-contacts): Task 11b COMPLETE and Checkpoint 2 passed
BigSimmo Aug 21, 2026
0cee63f
feat(caring-contacts): database configuration that cannot resolve to …
BigSimmo Aug 21, 2026
183be3b
feat(caring-contacts): demo role switcher with no credentials
BigSimmo Aug 21, 2026
f5effcc
chore(caring-contacts): run npm run format on Task 12/13 changes
BigSimmo Aug 21, 2026
73bc70d
docs(caring-contacts): Task 12/13 report
BigSimmo Aug 21, 2026
36bbe4e
chore(caring-contacts): format the Task 12/13 report
BigSimmo Aug 21, 2026
acc2e87
docs(caring-contacts): Tasks 12/13 review outcome and Ruling 44
BigSimmo Aug 21, 2026
b95cdf1
fix(caring-contacts): Task 12/13 review round 1 — 4 Important, 6 Minor
BigSimmo Aug 21, 2026
19405fa
docs(caring-contacts): Task 12/13 fix round 1 report
BigSimmo Aug 21, 2026
442544a
docs(caring-contacts): Tasks 12 and 13 COMPLETE
BigSimmo Aug 21, 2026
9b76d64
docs(caring-contacts): mark Tasks 12 and 13 complete in the master le…
BigSimmo Aug 21, 2026
5790cde
feat(caring-contacts): API boundary that audits every view and names …
BigSimmo Aug 21, 2026
e3805d6
docs(caring-contacts): Task 14 report
BigSimmo Aug 21, 2026
36fe7c9
docs(caring-contacts): Task 14 evidence and Rulings 45-48
BigSimmo Aug 21, 2026
547bba5
feat(caring-contacts): audit boundary-denied writes and give every ac…
BigSimmo Aug 21, 2026
8897f4c
docs(caring-contacts): Task 14 review outcome and Ruling 49
BigSimmo Aug 21, 2026
f248899
fix(caring-contacts): make the boundary audit record unsuppressible a…
BigSimmo Aug 21, 2026
b41b9e0
docs(caring-contacts): Task 14 fix round 1 re-review, and the pattern…
BigSimmo Aug 21, 2026
b7f3c16
fix(caring-contacts): trigger the safe-id retry on the audit failure …
BigSimmo Aug 22, 2026
2596599
docs(caring-contacts): note the shared-worktree commit-hook block
BigSimmo Aug 22, 2026
5469641
docs(caring-contacts): Task 14 COMPLETE - reads are audited
BigSimmo Aug 22, 2026
65afc28
feat(caring-contacts): production workspace route group, four-state s…
BigSimmo Aug 22, 2026
579c8ef
docs(caring-contacts): Task 15 first commit, Rulings 51-53
BigSimmo Aug 22, 2026
341807a
fix(caring-contacts): unavailable destinations, a real browser proof,…
BigSimmo Aug 22, 2026
9d230b2
docs(caring-contacts): Task 15 review APPROVED, and what the browser …
BigSimmo Aug 22, 2026
a0865f1
fix(caring-contacts): pin the safeguard's wording, close the width ga…
BigSimmo Aug 22, 2026
1494166
docs(caring-contacts): Task 15 COMPLETE, and the unfalsifiable-assert…
BigSimmo Aug 22, 2026
b8f8199
feat(caring-contacts): service-state banner and the explained-automat…
BigSimmo Aug 22, 2026
b7fc3fd
docs(caring-contacts): Task 16 evidence, Rulings 55-56
BigSimmo Aug 22, 2026
1453258
feat(caring-contacts): require the service state and read it for real…
BigSimmo Aug 22, 2026
772fbb6
docs(caring-contacts): Ruling 56 implemented, and the client-boundary…
BigSimmo Aug 22, 2026
aeecbd2
docs(caring-contacts): browser proof that the async page still render…
BigSimmo Aug 22, 2026
cf2ba34
docs(caring-contacts): Task 16 review APPROVED, and a finding that co…
BigSimmo Aug 22, 2026
85ca58c
fix(caring-contacts): guard the remedy half of 4.4 and the client bou…
BigSimmo Aug 22, 2026
6de6394
docs(caring-contacts): Task 16 COMPLETE
BigSimmo Aug 22, 2026
003a05e
feat(caring-contacts): the frozen 24-overlay definition table, checke…
BigSimmo Aug 22, 2026
a248a85
docs(caring-contacts): Task 17 evidence, Rulings 57-58
BigSimmo Aug 22, 2026
3650085
test(caring-contacts): guard the reserved action-only dismissal membe…
BigSimmo Aug 22, 2026
c439a72
docs(caring-contacts): Task 17 review APPROVED, zero Important findings
BigSimmo Aug 22, 2026
2101390
test(caring-contacts): close the five assertion-strength Minors on th…
BigSimmo Aug 22, 2026
2998e96
docs(caring-contacts): Task 17 COMPLETE, and a mutation lesson
BigSimmo Aug 22, 2026
4354788
feat(caring-contacts): one overlay renderer honouring the frozen moda…
BigSimmo Aug 22, 2026
e739ea9
docs(caring-contacts): Task 18 review, Rulings 60-61, and a guard tha…
BigSimmo Aug 22, 2026
f840b36
fix(caring-contacts): unwind the overlay history entry, plain-words r…
BigSimmo Aug 22, 2026
2636299
docs(caring-contacts): Task 18 re-review - a fix that reintroduced a …
BigSimmo Aug 22, 2026
87cfdd4
fix(caring-contacts): close the inherited-property hole in the refusa…
BigSimmo Aug 22, 2026
c727ac2
docs(caring-contacts): Task 18 COMPLETE
BigSimmo Aug 22, 2026
25495d5
test(caring-contacts): browser proof of the workspace shell and all 2…
BigSimmo Aug 22, 2026
5ae8b89
docs(caring-contacts): Phase 2A visual differences, and unblock docs:…
BigSimmo Aug 22, 2026
3876a40
docs(caring-contacts): queue the Phase 2B screens as outstanding work
BigSimmo Aug 22, 2026
aaf74ab
docs(caring-contacts): Task 19 report — the gate evidence it was wait…
BigSimmo Aug 22, 2026
540b787
fix(caring-contacts): Task 19 review round 1 — record the defects, bo…
BigSimmo Aug 22, 2026
38043d2
docs(caring-contacts): CRITICAL - patient data in plaintext in the id…
BigSimmo Aug 22, 2026
52c3479
fix(caring-contacts): narrow the write reply, freeze the audit trail,…
BigSimmo Aug 22, 2026
3ac8c99
docs(caring-contacts): file the database-suite automation gap
BigSimmo Aug 22, 2026
a230bba
fix(caring-contacts): hash the fingerprint, seal the calendar rule, p…
BigSimmo Aug 22, 2026
0453868
fix(caring-contacts): Ruling 65 — narrow the restart approval so the …
BigSimmo Aug 22, 2026
c3ef20c
fix: gate caring contacts demo access in production
BigSimmo Aug 22, 2026
e93a48b
fix(caring-contacts): pin a condensed bar so a service stop cannot sc…
BigSimmo Aug 22, 2026
41302cd
docs(caring-contacts): report the condensed bar, and the full-suite s…
BigSimmo Aug 22, 2026
04c4a6f
docs(caring-contacts): Ruling 66 — the branch is shared, and a foreig…
BigSimmo Aug 22, 2026
a8eef80
docs(caring-contacts): the phantom failures were a moving tree, not a…
BigSimmo Aug 22, 2026
db8a74e
docs(caring-contacts): gather every human-readable string into one co…
BigSimmo Aug 22, 2026
20fbc43
fix(caring-contacts): fix round 1 — close the empty widths and the gu…
BigSimmo Aug 22, 2026
637bfd3
fix(caring-contacts): stub NODE_ENV instead of assigning it, so the b…
BigSimmo Aug 22, 2026
1f4f5b7
docs(caring-contacts): measure the browser-gate collision and escalat…
BigSimmo Aug 22, 2026
05584f9
fix(caring-contacts): one doubly-flagged exception to the production …
BigSimmo Aug 22, 2026
2063b4d
docs(caring-contacts): bring the ledger and continuation prompt to th…
BigSimmo Aug 22, 2026
cf03f99
docs(caring-contacts): the exception works, and the one residual fail…
BigSimmo Aug 22, 2026
737bef6
merge: sync main and resolve caring contacts conflicts
BigSimmo Aug 22, 2026
faa4541
chore(ledger): record caring contacts PR maintenance
BigSimmo Aug 22, 2026
3193d7b
merge: sync latest main snapshot updates
BigSimmo Aug 22, 2026
13d3710
chore(ledger): record latest caring contacts sync
BigSimmo Aug 22, 2026
6da4774
merge: sync current main snapshot updates
BigSimmo Aug 22, 2026
9490b78
chore(ledger): record current caring contacts sync
BigSimmo Aug 22, 2026
326683f
docs(caring-contacts): avoid malformed link example
BigSimmo Aug 22, 2026
668b2e3
chore(ledger): record caring contacts CI repair
BigSimmo Aug 22, 2026
797165e
fix(caring-contacts): use canonical elevation token
BigSimmo Aug 22, 2026
7ab537f
chore(ledger): record caring contacts design CI repair
BigSimmo Aug 22, 2026
1773d6a
merge: sync latest main and retain caring contacts registrations
BigSimmo Aug 22, 2026
6156d08
chore(ledger): record final caring contacts main sync
BigSimmo Aug 22, 2026
f7d99db
docs(caring-contacts): restore codebase index coverage
BigSimmo Aug 22, 2026
750816f
chore(ledger): record caring contacts index CI repair
BigSimmo Aug 22, 2026
4d1999b
fix(caring-contacts): audit the page render's service-state read; stu…
claude Aug 22, 2026
f122ed8
style(caring-contacts): format codebase index
BigSimmo Aug 22, 2026
dd01257
chore(ledger): record caring contacts format CI repair
BigSimmo Aug 22, 2026
913ae40
merge: sync latest main and retain caring contacts registrations
BigSimmo Aug 22, 2026
b803600
chore(ledger): record caring contacts latest-main sync
BigSimmo Aug 22, 2026
80ce09e
docs(caring-contacts): refresh scripts inventory
BigSimmo Aug 22, 2026
4a0cce5
chore(ledger): record caring contacts inventory CI repair
BigSimmo Aug 22, 2026
f84ae65
fix(caring-contacts): share demo store across Turbopack route boundaries
cursoragent Aug 22, 2026
9d23c94
chore(ledger): record PR 2279 demo-store sharing fix
cursoragent Aug 22, 2026
6a35a11
merge: sync origin/main into caring contacts PR
cursoragent Aug 22, 2026
72adf40
chore(issues): refresh snapshot after latest-main sync
BigSimmo Aug 22, 2026
ebfd3a7
chore(ledger): record caring contacts snapshot CI repair
BigSimmo Aug 22, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
51 changes: 51 additions & 0 deletions caring-contacts/run-db-tests.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
#!/usr/bin/env node
// caring-contacts/run-db-tests.mjs
//
// Runs the caring-contact database suites -- the migration/row-level-security proofs and the
// shared repository contract driven against the Postgres store -- against a real Postgres named
// by CARING_CONTACTS_DATABASE_URL.
//
// It never skips. A missing variable is a hard failure naming the variable, because a suite that
// quietly passes when the database is absent is a check that cannot fail: the row-level security
// this schema exists to prove would then be unproven and reported green.
//
// The database is a local, disposable container. It is NOT the repository's live Supabase
// project, and nothing here touches a hosted service.
import { spawn } from "node:child_process";
import path from "node:path";
import { fileURLToPath } from "node:url";

const VARIABLE = "CARING_CONTACTS_DATABASE_URL";
const projectRoot = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "..");

const url = process.env[VARIABLE];
if (typeof url !== "string" || url.trim() === "") {
const lines = [
`${VARIABLE} is not set, so the caring-contact database suites cannot run.`,
"They are never skipped: row-level security is the control that stops one hospital team",
"seeing another team's patients, and it is only proven against a real database.",
"",
"Start a disposable local Postgres and point the variable at it, for example:",
" docker run --rm -d --name caring-contacts-pg -e POSTGRES_PASSWORD=caring-contacts-local -p 54329:5432 postgres:17",
` export ${VARIABLE}=postgres://postgres:caring-contacts-local@127.0.0.1:54329/postgres`,
"",
];
process.stderr.write(lines.join("\n"));
process.exit(1);
}

const child = spawn(
process.execPath,
[
path.join(projectRoot, "scripts", "run-vitest.mjs"),
"run",
"--project=caring-contacts-db",
...process.argv.slice(2),
],
{ cwd: projectRoot, env: process.env, stdio: "inherit" },
);
child.on("error", (error) => {
process.stderr.write(`${error.message}\n`);
process.exit(1);
});
child.on("close", (status, signal) => process.exit(status === null ? (signal ? 1 : 0) : status));
Original file line number Diff line number Diff line change
@@ -0,0 +1,341 @@
-- caring-contacts/supabase/migrations/0001_caring_contacts_foundation.sql
--
-- The caring-contact schema: tables, constraints, and the transactional audit guard.
--
-- THIS DIRECTORY IS NOT THE REPOSITORY'S `supabase/migrations/`. That directory replays against
-- the live Clinical KB project; nothing here may ever be placed there, and
-- tests/caring-contacts-domain-isolation.test.ts fails if a caring-contact migration appears in it.
--
-- Design notes that are load-bearing rather than stylistic:
--
-- * CULTURAL IDENTITY IS NOT ON THE PATIENT ROW. It lives in
-- caring_contacts.cultural_identity_reports, a reporting projection, so an ordinary clinical
-- read of a plan cannot surface it and de-identification has one table to clear.
-- * ONE NON-TERMINAL PLAN PER PATIENT is a unique PARTIAL index over patient_id alone -- not
-- (team_id, patient_id). Two teams each running a plan for one person is the same
-- duplicate-message hazard as one team doing it twice, and it is the hazard the rule exists for.
-- * ONE DISPATCH RECORD PER (contact, attempt), so a retried request cannot record -- or bill,
-- or later be read as -- a second message that never went out.
-- * EVERY CHANGE CARRIES ITS AUDIT EVENT IN THE SAME TRANSACTION, enforced by a DEFERRABLE
-- CONSTRAINT TRIGGER that fires at commit. A direct UPDATE with no audit event fails; the
-- change and the record of it are committed together or not at all.
-- * NO CREATE INDEX CONCURRENTLY. It cannot run inside a transaction, and every migration here
-- must be a single atomic, replayable unit.
--
-- Replay-safe: every object is created with IF NOT EXISTS, CREATE OR REPLACE, or a guarded DO
-- block, so applying the file twice is a no-op.

begin;

create schema if not exists caring_contacts;

-- ---------------------------------------------------------------------------
-- Roles
--
-- `caring_contacts_app` is what every application statement runs as. It is deliberately NOT the
-- table owner and holds no BYPASSRLS: policies are the only thing standing between one team and
-- another team's patients, so the application role must be subject to them.
--
-- `caring_contacts_anon` is the unauthenticated role. Migration 0002 grants it SELECT on purpose
-- and gives it no policy at all, so the tests prove that ROW-LEVEL SECURITY denies it rather than
-- proving only that somebody forgot a GRANT.
-- ---------------------------------------------------------------------------
do $$
begin
if not exists (select 1 from pg_catalog.pg_roles where rolname = 'caring_contacts_app') then
create role caring_contacts_app nologin;
end if;
if not exists (select 1 from pg_catalog.pg_roles where rolname = 'caring_contacts_anon') then
create role caring_contacts_anon nologin;
end if;
end
$$;

grant caring_contacts_app, caring_contacts_anon to current_user with admin option;

-- ---------------------------------------------------------------------------
-- Session scope
--
-- The team a statement may see is a transaction-local setting, not a column the caller supplies:
-- `set_config('caring_contacts.team_id', ..., true)`. Unset resolves to NULL, and every policy
-- compares `team_id = caring_contacts.current_team_id()`, so an unscoped session matches no row.
-- That is what "deny by default" means here.
-- ---------------------------------------------------------------------------
create or replace function caring_contacts.current_team_id()
returns text
language sql
stable
set search_path = ''
as $$
select nullif(pg_catalog.current_setting('caring_contacts.team_id', true), '')
$$;

-- ---------------------------------------------------------------------------
-- Tables
-- ---------------------------------------------------------------------------

create table if not exists caring_contacts.teams (
id text primary key,
created_at timestamptz not null default now()
);

create table if not exists caring_contacts.actors (
id text primary key,
team_id text not null references caring_contacts.teams (id),
-- Human roles and the one system role are separate columns, not one list, because the two
-- grant tables must never overlap: software cannot acquire a clinical capability by having a
-- role appended, and a person cannot fabricate a delivery receipt.
roles text[] not null default '{}',
system_role text,
created_at timestamptz not null default now(),
constraint actors_roles_are_known check (roles <@ array['coordinator', 'teamLead', 'auditor']),
constraint actors_are_human_or_system check (
(system_role is null) or (system_role = 'contactDispatcher' and cardinality(roles) = 0)
)
);

create table if not exists caring_contacts.referrals (
id text primary key,
team_id text not null references caring_contacts.teams (id),
patient_id text not null,
state text not null check (state in ('awaitingHandover', 'accepted', 'returnedForClarification', 'declined')),
pathway_version_id text,
created_at timestamptz not null default now()
);

create table if not exists caring_contacts.pathway_versions (
id text primary key,
team_id text not null references caring_contacts.teams (id),
state text not null check (state in ('draft', 'inReview', 'approved', 'retired')),
author_id text,
approver_id text,
created_at timestamptz not null default now(),
-- No single actor may both author and approve the same clinical message content.
constraint pathway_versions_no_self_approval check (approver_id is null or approver_id <> author_id)
);

-- The patient row. Note what is NOT here: cultural identity.
create table if not exists caring_contacts.plans (
id text primary key,
team_id text not null references caring_contacts.teams (id),
patient_id text not null,
referral_id text not null,
pathway_version_id text not null,
state text not null check (state in ('draft', 'active', 'paused', 'withdrawn', 'cancelled', 'completed')),
version integer not null check (version > 0),
outcome text not null check (outcome in ('inProgress', 'withdrawn', 'cancelled', 'completed')),
discharge_at timestamptz not null,
completed_at timestamptz,
sending_preference text not null check (sending_preference in ('morning', 'afternoon', 'earlyEvening')),
patient_name text not null,
patient_mobile_number text not null,
patient_identifiers text[] not null default '{}',
created_at timestamptz not null default now()
);

-- Rule 3. Partial, so an ended plan never blocks a new episode; over patient_id alone, so one
-- team cannot start a second concurrent plan for a person another team is already contacting.
create unique index if not exists plans_one_non_terminal_per_patient
on caring_contacts.plans (patient_id)
where state not in ('withdrawn', 'cancelled', 'completed');

create index if not exists plans_team_id_idx on caring_contacts.plans (team_id);

create table if not exists caring_contacts.contacts (
id text primary key,
plan_id text not null references caring_contacts.plans (id) on delete cascade,
team_id text not null references caring_contacts.teams (id),
sequence integer not null check (sequence > 0),
state text not null check (
state in (
'scheduled', 'processing', 'sent', 'delivered', 'notDelivered', 'numberInvalid',
'contactChanged', 'statusUnavailable', 'missed', 'suppressed', 'cancelled'
)
),
version integer not null check (version > 0),
cadence_label text not null,
calendar_day text not null,
-- An absorbed contact keeps a real send instant so the interface can explain the plan. It is
-- kept out of dispatch by its terminal `suppressed` state, never by hiding this column.
send_at timestamptz not null,
message_type text not null check (message_type in ('standard', 'first', 'closing')),
suppressed_reason text,
constraint contacts_unique_sequence unique (plan_id, sequence)
);

create index if not exists contacts_plan_id_idx on caring_contacts.contacts (plan_id);

-- Rule 4. One row per attempt at one contact; a replayed dispatch request cannot become a second
-- recorded message.
create table if not exists caring_contacts.contact_dispatches (
id bigint generated always as identity primary key,
contact_id text not null references caring_contacts.contacts (id) on delete cascade,
team_id text not null references caring_contacts.teams (id),
attempt integer not null check (attempt > 0),
idempotency_key text not null,
started_at timestamptz not null default now(),
constraint contact_dispatches_unique_attempt unique (contact_id, attempt)
);

-- The audit trail. `txn_token` ties every event to the transaction that produced it, which is
-- what the guard below checks; it defaults from the transaction-local setting so a caller cannot
-- forget it and quietly write an orphan record.
create table if not exists caring_contacts.audit_events (
id bigint generated always as identity primary key,
team_id text not null references caring_contacts.teams (id),
actor_id text not null,
actor_roles text[] not null,
action text not null,
object_type text not null,
object_id text not null,
outcome text not null check (outcome in ('allowed', 'denied', 'failed')),
idempotency_key text not null,
-- The AWST ISO-8601 instant the domain builds. Held as text so the audit record is byte-identical
-- to the one the application produced rather than reformatted by the database.
occurred_at text not null,
txn_token uuid default nullif(pg_catalog.current_setting('caring_contacts.audit_token', true), '')::uuid,
recorded_at timestamptz not null default now()
);

create index if not exists audit_events_team_idx on caring_contacts.audit_events (team_id, id);
create index if not exists audit_events_txn_token_idx on caring_contacts.audit_events (txn_token);

-- Whether the service is stopped for a team. Stopping must never be blocked, so the only
-- constraint is that a stop is attributed to somebody.
create table if not exists caring_contacts.service_state (
team_id text primary key references caring_contacts.teams (id),
stopped boolean not null default false,
stopped_by text,
stopped_at timestamptz,
restart_approved_by text,
updated_at timestamptz not null default now(),
constraint service_state_stop_is_attributed check (stopped = false or stopped_by is not null)
);

-- When an episode ended, and when its identifying detail was cleared. The period itself is domain
-- policy and is deliberately not encoded here.
create table if not exists caring_contacts.retention_state (
plan_id text primary key references caring_contacts.plans (id) on delete cascade,
team_id text not null references caring_contacts.teams (id),
terminal_at timestamptz,
cleared_at timestamptz,
constraint retention_state_cleared_after_terminal check (cleared_at is null or terminal_at is not null)
);

-- The cultural-identity reporting projection. This is the ONLY place cultural identity is stored.
create table if not exists caring_contacts.cultural_identity_reports (
plan_id text primary key references caring_contacts.plans (id) on delete cascade,
team_id text not null references caring_contacts.teams (id),
cultural_identity text not null,
recorded_at timestamptz not null default now()
);

-- Replay protection. Scoped per team so one team can never replay another team's result, and
-- holding the ORIGINAL result so a replay returns the first answer rather than recomputing one.
create table if not exists caring_contacts.idempotency_records (
team_id text not null references caring_contacts.teams (id),
idempotency_key text not null,
fingerprint text not null,
result jsonb not null,
created_at timestamptz not null default now(),
primary key (team_id, idempotency_key)
);

-- ---------------------------------------------------------------------------
-- The transactional audit guard
--
-- A DEFERRABLE INITIALLY DEFERRED constraint trigger fires at commit, by which time any audit
-- event written in the same transaction is visible to it. So the order of the two writes does not
-- matter, only that both happened in one transaction.
--
-- SECURITY DEFINER because the guard must be able to see the audit event regardless of the
-- writer's row-level security scope; it returns nothing and leaks nothing.
-- ---------------------------------------------------------------------------
create or replace function caring_contacts.assert_change_audited()
returns trigger
language plpgsql
security definer
set search_path = ''
as $$
declare
token uuid;
begin
token := nullif(pg_catalog.current_setting('caring_contacts.audit_token', true), '')::uuid;
if token is null then
raise exception
'caring-contacts-audit-required: % on %.% ran outside an audited transaction',
tg_op, tg_table_schema, tg_table_name;
end if;
if not exists (select 1 from caring_contacts.audit_events e where e.txn_token = token) then
raise exception
'caring-contacts-audit-required: % on %.% wrote no audit event in its transaction',
tg_op, tg_table_schema, tg_table_name;
end if;
return null;
end;
$$;

-- Attaching the guard is driven from a list rather than written out per table, so a table added
-- without the guard is a visible omission from a list. It is a FUNCTION rather than an inline
-- loop because later migrations create audited tables that do not exist when this file runs, and
-- they must attach the guard through the same mechanism instead of hand-writing their own
-- triggers. It executes DDL, so it is revoked from PUBLIC and left to the migration role.
create or replace function caring_contacts.attach_audit_guard(p_tables text[])
returns void
language plpgsql
as $$
declare
audited_table text;
begin
foreach audited_table in array p_tables
loop
execute format('drop trigger if exists require_audit on caring_contacts.%I', audited_table);
execute format(
'create constraint trigger require_audit
after insert or update or delete on caring_contacts.%I
deferrable initially deferred
for each row execute function caring_contacts.assert_change_audited()',
audited_table
);
end loop;
end;
$$;

revoke execute on function caring_contacts.attach_audit_guard(text[]) from public;

select caring_contacts.attach_audit_guard(array[
'referrals', 'plans', 'contacts', 'contact_dispatches', 'cultural_identity_reports'
]);

-- ---------------------------------------------------------------------------
-- Cross-team questions the application must be able to ask without seeing the answer's contents
--
-- "Does this patient already have an open plan anywhere?" cannot be answered by a team-scoped
-- SELECT, and answering it wrongly is what would let two teams contact one person. These return a
-- boolean and nothing else -- no identifiers, no team, no row.
-- ---------------------------------------------------------------------------
create or replace function caring_contacts.patient_has_non_terminal_plan(p_patient_id text)
returns boolean
language sql
stable
security definer
set search_path = ''
as $$
select exists (
select 1 from caring_contacts.plans p
where p.patient_id = p_patient_id
and p.state not in ('withdrawn', 'cancelled', 'completed')
)
$$;

create or replace function caring_contacts.plan_exists(p_plan_id text)
returns boolean
language sql
stable
security definer
set search_path = ''
as $$
select exists (select 1 from caring_contacts.plans p where p.id = p_plan_id)
$$;

commit;
Loading
Loading