Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
20 changes: 10 additions & 10 deletions data/differentials-snapshot.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"version": "v10",
"exportedAt": "2026-07-05",
"exportedAt": "2026-07-11",
"presentations": [
{
"id": "acute-confusion-encephalopathy",
Expand Down Expand Up @@ -4910,8 +4910,8 @@
}
},
{
"id": "urgency-urgent",
"title": "Urgency: urgent",
"id": "focused-diagnostic-trap-tables",
"title": "Focused Diagnostic Trap Tables",
"status": "urgent",
"subtitle": "Distinguishes intrusive/obsessional phenomena from psychotic or violent intent.",
"selectedCount": 4,
Expand Down Expand Up @@ -14658,7 +14658,7 @@
"note": "Desire/plan/willingness to harm, ego-syntonic, grievance-based, with preparation"
}
],
"currentPresentation": ["Urgency: urgent"],
"currentPresentation": ["Focused Diagnostic Trap Tables"],
"investigations": [],
"immediateActions": []
},
Expand Down Expand Up @@ -15095,7 +15095,7 @@
"note": "Severe postpartum depression with guilt, nihilism, deserved-punishment ideas. Mood-congruent psychosis"
}
],
"currentPresentation": ["Urgency: urgent", "Focused comparator (perinatal)"],
"currentPresentation": ["Focused Diagnostic Trap Tables", "Focused comparator (perinatal)"],
"investigations": [],
"immediateActions": []
},
Expand Down Expand Up @@ -18958,7 +18958,7 @@
}
],
"currentPresentation": [
"Urgency: urgent",
"Focused Diagnostic Trap Tables",
"Anxiety / Panic / Obsessions / Health Anxiety",
"Panic disorder, Generalised anxiety disorder, OCD",
"CLINICAL HINGE: Abrupt peak over minutes, recurrent unexpected attacks, anticipatory anxiety or avoidance."
Expand Down Expand Up @@ -19600,7 +19600,7 @@
"note": "Desire/plan/willingness to harm, ego-syntonic, grievance-based, with preparation"
}
],
"currentPresentation": ["Urgency: urgent"],
"currentPresentation": ["Focused Diagnostic Trap Tables"],
"investigations": [],
"immediateActions": []
},
Expand Down Expand Up @@ -20257,7 +20257,7 @@
"Postpartum psychosis, Postnatal depression, Perinatal OCD / intrusive infant-harm thoughts",
"CLINICAL HINGE: Psychotic or manic syndrome with abrupt change from baseline in the postpartum period.",
"Focused comparator (perinatal)",
"Urgency: urgent"
"Focused Diagnostic Trap Tables"
],
"investigations": [],
"immediateActions": []
Expand Down Expand Up @@ -21453,7 +21453,7 @@
"note": "Desire/plan/willingness to harm, ego-syntonic, grievance-based, with preparation"
}
],
"currentPresentation": ["Urgency: urgent"],
"currentPresentation": ["Focused Diagnostic Trap Tables"],
"investigations": [],
"immediateActions": []
},
Expand Down Expand Up @@ -22930,7 +22930,7 @@
"note": "Desire/plan/willingness to harm, ego-syntonic, grievance-based, with preparation"
}
],
"currentPresentation": ["Urgency: urgent"],
"currentPresentation": ["Focused Diagnostic Trap Tables"],
"investigations": [],
"immediateActions": []
},
Expand Down
5 changes: 5 additions & 0 deletions docs/process-hardening.md
Original file line number Diff line number Diff line change
Expand Up @@ -303,3 +303,8 @@ hybrid:10}`, all 10 forced-embedding vector cases passed (`force_embedding_failu
- **Full re-triage of the 2026-07-01 audit's H1–H4 and M1–M17 against current main: all 21 are closed.** The 2026-07-02 remediation branch landed (H1/H2/H4+M8+M16, M1–M12, M14, M15, M17 all carry audit-referencing fixes verified in code, not just comments); PR #278 added the P0 carry-over (including the M9 DELETE TOCTOU re-check); M13's migration (`20260702000000`) is in-tree (live-apply verification needs `npm run check:m13-migration` with live keys). H3 is closed by supersession: PR #118 removed the governance penalties it complained about entirely (measured, "do not reintroduce" comment in `retrieval-selection.ts`); the residual `Math.max` floor only affects intent boosts, and un-flooring it was already tried and withdrawn (compounding across selection passes — audit R1). Do not re-open H3 without the golden retrieval eval.
- **New fast-check property suite** (`tests/property-*.test.ts`) pins the text-processing core's clinical invariants over generated inputs: unit-bearing numeric tokens survive sanitization; chunking terminates (including `CHUNK_OVERLAP >= CHUNK_SIZE`, M17) and neither loses nor invents content; table normalization stays rectangular, preserves numeric values, and the low-confidence caveat survives both clipboard export paths (H4). The suite immediately caught a live line-level H2 residue in `stripLowYieldLines` (control markers glued to threshold sentences deleted the whole line), fixed in the same PR.
- **Eval debt (blocking merge of the sanitizer fix, not development):** the environment had no live Supabase/OpenAI keys, so `npm run eval:quality -- --rag-only` (answer-path gate for the sanitizer change) could not run. Run it before merging `claude/audit-sweep-property-tests`. No retrieval-selection/ranking files were touched, so the golden retrieval eval is not required for this branch.

## Differentials export "Urgency: urgent" artifact retitled — live prune pending (2026-07-11)

- The differentials export's trap-tables appendix (`T_Focused_Diagnostic_Trap_Tables.txt`) has no title line, so the parser surfaced its first metadata row ("Urgency: urgent") as a presentation titled "Urgency: urgent" (slug `urgency-urgent`) with detail routes and search visibility. It is a legitimate comparison workflow ("Distinguishes intrusive/obsessional phenomena from psychotic or violent intent"), and 4 of its 7 option diagnoses (`gad-worry-depressive-rumination`, `overvalued-idea`, `psychotic-delusion`, `ptsd-intrusive-memories-flashbacks`) exist in no other entry — so excluding it would orphan them and break the #483 invariant (enforced by `differential-detail.test.ts`) that every diagnosis belongs to a presentation. Fixed at the root: `parseEntryTitle` now falls back to the `=== HEADER ===` text when the first line is a metadata row, retitling the entry "Focused Diagnostic Trap Tables" (slug `focused-diagnostic-trap-tables`). Snapshot stays 31 presentations; no orphans.
- **Operator follow-up (live Supabase, confirmation-required):** the retitle changed the slug, and seeding upserts by slug and never deletes, so already-seeded owners keep the stale `differential_records` row (`kind='presentation'`, `slug='urgency-urgent'`) and — where corpus embedding ran — its registry corpus document/chunks in `documents`/`document_chunks`. `npm run differentials:seed -- --owner-id <uuid> --write` now prunes presentation rows whose slug the current snapshot no longer produces (via `staleSeededPresentations`) and their corpus documents per owner; run it for each seeded owner.
24 changes: 18 additions & 6 deletions scripts/lib/parse-differentials-export.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import { isDifferentialMetadataArtifactTitle } from "@/lib/differential-snapshot";
import type {
DifferentialComparisonCandidate,
DifferentialComparisonCriterion,
Expand Down Expand Up @@ -144,14 +145,25 @@ function parseEntryHeader(content: string) {
return header ? slugify(header) : "unknown";
}

// ALL-CAPS "=== HEADER ===" text to a human title, e.g.
// "FOCUSED DIAGNOSTIC TRAP TABLES" -> "Focused Diagnostic Trap Tables".
function titleFromHeader(header: string) {
return header.toLowerCase().replace(/\b\w/g, (character) => character.toUpperCase());
}

function parseEntryTitle(content: string) {
const header = content.match(/^===\s*(.+?)\s*===\s*\n/m)?.[1]?.trim() ?? "";
const afterHeader = content.replace(/^===\s*.+?\s*===\s*\n/m, "");
return (
afterHeader
.split("\n")
.find((line) => line.trim())
?.trim() ?? "Untitled presentation"
);
const firstLine = afterHeader
.split("\n")
.find((line) => line.trim())
?.trim();
// Most entries put a title on the line after the header. The focused
// diagnostic trap-tables appendix has none, so the first line is actually a
// metadata row ("Urgency: urgent") — fall back to the header text so the
// entry gets an honest title instead of surfacing the metadata as its title.
if (firstLine && !isDifferentialMetadataArtifactTitle(firstLine)) return firstLine;
return header ? titleFromHeader(header) : (firstLine ?? "Untitled presentation");
}

function parseOptionsSection(content: string): ParsedOption[] {
Expand Down
32 changes: 31 additions & 1 deletion scripts/seed-differential-records.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import { loadEnvConfig } from "@next/env";
import { confirm } from "./cli-utils";
import { buildDefaultDifferentialRows, loadDifferentialSnapshot } from "@/lib/differential-fixtures";
import type { DifferentialRecordRow } from "@/lib/differential-records";
import { staleSeededPresentations } from "@/lib/differential-seed";

loadEnvConfig(process.cwd());

Expand Down Expand Up @@ -85,7 +86,7 @@ async function main() {
const supabase = await loadAdminClient();
const { data: existing, error: existingError } = await supabase
.from("differential_records")
.select("kind, slug, source_status, validation_status, last_reviewed_at, review_due_at")
.select("id, kind, slug, source_status, validation_status, last_reviewed_at, review_due_at")
.eq("owner_id", args.ownerId);
if (existingError) {
throw new Error(`Could not read existing governance: ${existingError.message}`);
Expand Down Expand Up @@ -119,6 +120,35 @@ async function main() {
});
if (error) throw new Error(`Upsert failed: ${error.message}`);

// Upserts never delete, so presentation rows the snapshot no longer produces
// (e.g. the removed "urgency-urgent" export artifact) linger for
// already-seeded owners; prune them and their registry corpus documents.
const stale = staleSeededPresentations(existing ?? []);
if (stale.length > 0) {
const { differentialCorpusDocumentId } = await loadRegistryCorpus();
console.log(
`[differentials:seed] Removing ${stale.length} stale presentation row(s) no longer in the snapshot: ${stale
.map((row) => row.slug)
.join(", ")}`,
);
const { error: staleCorpusError } = await supabase
.from("documents")
.delete()
.in(
"id",
stale.map((row) => differentialCorpusDocumentId(row.id)),
);
if (staleCorpusError) throw new Error(`Stale corpus document cleanup failed: ${staleCorpusError.message}`);
const { error: staleRowError } = await supabase
.from("differential_records")
.delete()
.in(
"id",
stale.map((row) => row.id),
);
if (staleRowError) throw new Error(`Stale presentation row cleanup failed: ${staleRowError.message}`);
}

const { count, error: countError } = await supabase
.from("differential_records")
.select("id", { count: "exact", head: true })
Expand Down
13 changes: 12 additions & 1 deletion src/lib/differential-seed.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { buildDefaultDifferentialRows } from "@/lib/differential-fixtures";
import { buildDefaultDifferentialRows, loadDifferentialSnapshot } from "@/lib/differential-fixtures";
import { type DifferentialRecordInsert, type DifferentialRecordRow } from "@/lib/differential-records";
import { safeErrorLogDetails } from "@/lib/privacy";

Expand Down Expand Up @@ -62,3 +62,14 @@ export async function fetchOwnerDifferentialRowsWithSeed(
export function buildDifferentialSeedRows(ownerId: string): DifferentialRecordInsert[] {
return buildDefaultDifferentialRows(ownerId);
}

/** Seeded presentation rows whose slug the current snapshot no longer produces.
* Retitling the trap-tables appendix changed its slug ("urgency-urgent" ->
* "focused-diagnostic-trap-tables"); seeding upserts by slug and never
* deletes, so the old slug lingers for already-seeded owners until pruned.
* Diagnoses are deliberately not pruned: their slugs merge and churn across
* snapshot versions, so set-difference pruning would be unsafe there. */
export function staleSeededPresentations<Row extends { kind: string; slug: string }>(rows: Row[]): Row[] {
const validSlugs = new Set(loadDifferentialSnapshot().presentations.map((presentation) => presentation.id));
return rows.filter((row) => row.kind === "presentation" && !validSlugs.has(row.slug));
}
9 changes: 9 additions & 0 deletions src/lib/differential-snapshot.ts
Original file line number Diff line number Diff line change
Expand Up @@ -89,6 +89,15 @@ export type DifferentialSnapshotGovernance = {
sourceTitle: string;
};

/** Entry files in the differentials export open with metadata rows ("Urgency:
* urgent", "Axis: mixed", "Population: general"). When a file has no title
* line, the first line after the header is one of these rows rather than a
* real title; the parser detects that with this predicate and falls back to
* the header text so the entry gets an honest title (never a metadata row). */
export function isDifferentialMetadataArtifactTitle(title: string) {
return /^(urgency|axis|population)\s*:/i.test(title.trim());
}

export type DifferentialSnapshot = {
version: string;
exportedAt: string;
Expand Down
7 changes: 7 additions & 0 deletions src/lib/registry-corpus.ts
Original file line number Diff line number Diff line change
Expand Up @@ -100,6 +100,13 @@ function registryDocumentId(entry: RegistryCorpusEntry) {
return deterministicUuid(`registry-document:${entry.kind}:${entry.recordId}`);
}

/** Corpus document id for a differential record row. Chunks cascade from the
* document, so deleting this id fully removes a pruned record from the
* corpus. Used by the differentials seed CLI when cleaning up stale rows. */
export function differentialCorpusDocumentId(recordId: string) {
return deterministicUuid(`registry-document:differential:${recordId}`);
}

/** Registry chunk id. */
function registryChunkId(entry: RegistryCorpusEntry) {
return deterministicUuid(`registry-chunk:${entry.kind}:${entry.recordId}`);
Expand Down
66 changes: 65 additions & 1 deletion tests/differentials.test.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,13 @@
import { describe, expect, it } from "vitest";

import { parseEntryFile, parseScenarioPresets, parseSearchAliases } from "../scripts/lib/parse-differentials-export";
import {
buildDifferentialSnapshot,
parseEntryFile,
parseScenarioPresets,
parseSearchAliases,
} from "../scripts/lib/parse-differentials-export";
import { staleSeededPresentations } from "@/lib/differential-seed";
import { isDifferentialMetadataArtifactTitle } from "@/lib/differential-snapshot";
import {
composeDifferentialSearchResults,
differentialDiagnosesCards,
Expand Down Expand Up @@ -128,6 +135,42 @@ OPTIONS:
expect(aliases.tags).toBeUndefined();
expect(aliases.delirium).toEqual(["confusion", "fluctuation"]);
});

it("titles a titleless entry from its header instead of surfacing a metadata row", () => {
// The trap-tables appendix has no title line, so the first line after the
// header is a metadata row ("Urgency: urgent"). The parser must fall back
// to the header text rather than use the metadata row as the title.
const trapEntry = `=== FOCUSED DIAGNOSTIC TRAP TABLES ===

Urgency: urgent
Axis: mixed
Population: general

PURPOSE: Distinguishes intrusive/obsessional phenomena from psychotic or violent intent.

OPTIONS:
1. OCD — Repetitive intrusive thoughts with rituals. Red flags: Functional collapse.

SOURCE: v10`;
const snapshot = buildDifferentialSnapshot({
entryFiles: [
{ name: "01_Delirium.txt", content: deliriumEntry },
{ name: "T_Focused_Diagnostic_Trap_Tables.txt", content: trapEntry },
],
presetsMarkdown: "",
flowsMarkdown: "",
aliasesMarkdown: "",
governanceMarkdown: "",
});
const trap = snapshot.presentations.find((presentation) => presentation.id === "focused-diagnostic-trap-tables");
expect(trap?.title).toBe("Focused Diagnostic Trap Tables");
// No presentation may carry a metadata-row title.
expect(
snapshot.presentations.filter((presentation) => isDifferentialMetadataArtifactTitle(presentation.title)),
).toEqual([]);
// The appendix keeps its options as diagnoses, parented by the retitled entry.
expect(snapshot.diagnoses.find((diagnosis) => diagnosis.slug === "ocd")).toBeDefined();
});
});

describe("differential records", () => {
Expand All @@ -136,6 +179,27 @@ describe("differential records", () => {
expect(snapshot.presentations).toHaveLength(31);
expect(snapshot.diagnoses.length).toBeGreaterThan(100);
expect(differentialRecords.length).toBe(snapshot.diagnoses.length);
// No presentation may ship with a metadata-row title (e.g. "Urgency: urgent"),
// and the mis-titled trap-tables appendix now carries its header title.
expect(
snapshot.presentations.filter((presentation) => isDifferentialMetadataArtifactTitle(presentation.title)),
).toEqual([]);
expect(snapshot.presentations.some((presentation) => presentation.id === "urgency-urgent")).toBe(false);
expect(
snapshot.presentations.find((presentation) => presentation.id === "focused-diagnostic-trap-tables")?.title,
).toBe("Focused Diagnostic Trap Tables");
});

it("flags a retired presentation slug for pruning, leaving diagnoses alone", () => {
// Retitling changed the appendix slug, so the old "urgency-urgent" row is no
// longer produced by the snapshot and must be pruned from seeded owners.
const rows = [
{ kind: "presentation", slug: "urgency-urgent" },
{ kind: "presentation", slug: "focused-diagnostic-trap-tables" },
{ kind: "diagnosis", slug: "urgency-urgent" },
];
const stale = staleSeededPresentations(rows);
expect(stale).toEqual([{ kind: "presentation", slug: "urgency-urgent" }]);
});

it("links cards to routes", () => {
Expand Down