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
36 changes: 24 additions & 12 deletions scripts/classify-documents.ts
Original file line number Diff line number Diff line change
Expand Up @@ -197,6 +197,8 @@ function preservedReviewMetadata(metadata: unknown) {
const preserved: Record<string, unknown> = {};
for (const key of [
"review_status",
"label_review_status",
"hidden",
"reviewed_at",
"reviewed_by",
"review_note",
Expand Down Expand Up @@ -409,21 +411,31 @@ async function writeClassifications(supabase: SupabaseAdmin, plans: Classificati
const documentIds = batch.map((plan) => plan.document.id);
const generatedLabels = dedupeGeneratedLabels(batch.flatMap((plan) => generatedLabelsForPlan(plan, stampedAt)));
const desiredLabelKeys = new Set(generatedLabels.map(labelIdentity));
const { data: existingGenerated, error: existingGeneratedError } = (await supabase
.from("document_labels")
.select("id,document_id,label_type,label,metadata")
.in("document_id", documentIds)
.eq("source", "generated")
.in("label_type", [...generatedLabelTypes])) as {
data: ExistingGeneratedLabelRow[] | null;
error: DatabaseError | null;
};
if (existingGeneratedError) throw new Error(existingGeneratedError.message);
// Paged, because 100 documents can hold more generated labels than one response returns,
// and a label missing here would lose its review state on upsert and never be cleaned up.
const existingGenerated: ExistingGeneratedLabelRow[] = [];
const existingPageSize = 1000;
for (let start = 0; ; start += existingPageSize) {
const { data, error: existingGeneratedError } = (await supabase
.from("document_labels")
.select("id,document_id,label_type,label,metadata")
.in("document_id", documentIds)
.eq("source", "generated")
.in("label_type", [...generatedLabelTypes])
.order("id", { ascending: true })
.range(start, start + existingPageSize - 1)) as {
data: ExistingGeneratedLabelRow[] | null;
error: DatabaseError | null;
};
if (existingGeneratedError) throw new Error(existingGeneratedError.message);
existingGenerated.push(...(data ?? []));
if ((data ?? []).length < existingPageSize) break;
}

const labelsToDelete = (existingGenerated ?? [])
const labelsToDelete = existingGenerated
.filter((label) => !desiredLabelKeys.has(labelIdentity(label)))
.map((label) => label.id);
const existingGeneratedByKey = new Map((existingGenerated ?? []).map((label) => [labelIdentity(label), label]));
const existingGeneratedByKey = new Map(existingGenerated.map((label) => [labelIdentity(label), label]));
const labelsForUpsert = generatedLabels.map((label) => {
const preserved = preservedReviewMetadata(existingGeneratedByKey.get(labelIdentity(label))?.metadata);
return {
Expand Down
26 changes: 26 additions & 0 deletions scripts/compare-retrieval-eval.ts
Original file line number Diff line number Diff line change
Expand Up @@ -190,6 +190,26 @@ export function compareRetrievalEval(baseline: EvalSummary, candidate: EvalSumma
return { rows, missingRequired };
}

const PINNED_RECALL_METRICS = new Set(["document_recall_at_5", "content_recall_at_5"]);

/**
* Recall rows that fail the canary-pair protocol: doc and content recall are pinned at 1.0, so a
* candidate below 1.0, or below its baseline, is a regression even when every per-case rank holds.
*/
export function recallFailures(rows: readonly ComparisonRow[]): string[] {
const failures: string[] = [];
for (const row of rows) {
if (!PINNED_RECALL_METRICS.has(row.name) || !row.candidate.present) continue;
const candidate = row.candidate.value;
if (candidate < 1) failures.push(`${row.name} is ${candidate.toFixed(row.digits)}, below the pinned 1.0`);
else if (row.baseline.present && candidate < row.baseline.value)
failures.push(
`${row.name} fell from ${row.baseline.value.toFixed(row.digits)} to ${candidate.toFixed(row.digits)}`,
);
}
return failures;
}

function formatCell(row: ComparisonRow): string {
if (!row.baseline.present || !row.candidate.present) {
const candidate = row.candidate.present ? row.candidate.value.toFixed(row.digits) : "n/a";
Expand Down Expand Up @@ -224,6 +244,12 @@ function main() {
console.log(` ${row.name}: ${formatCell(row)}`);
}

const recallDrops = recallFailures(rows);
if (failOnRegression && recallDrops.length > 0) {
console.error(`\nRecall gate failed: ${recallDrops.join("; ")}.`);
process.exitCode = 1;
}

if (missingRequired.length > 0) {
console.error(`\nMissing required metric(s), refusing a clean comparison: ${missingRequired.join(", ")}`);
console.error("A missing decision metric is not the same as 0 — regenerate the eval JSON with the full summary.");
Expand Down
51 changes: 36 additions & 15 deletions scripts/embed-registry-records.ts
Original file line number Diff line number Diff line change
Expand Up @@ -74,25 +74,45 @@ function registryKinds(kind: EmbedKind): RegistryRecordKind[] {
return [];
}

/**
* Every row a query matches. One PostgREST response stops at the project's row cap (1000 by
* default), so an unpaged read silently skips records once a table outgrows it.
*/
async function readAllPages<Row>(
page: (from: number, to: number) => PromiseLike<{ data: unknown[] | null; error: { message: string } | null }>,
what: string,
): Promise<Row[]> {
const pageSize = 1000;
const rows: Row[] = [];
for (let from = 0; ; from += pageSize) {
const { data, error } = await page(from, from + pageSize - 1);
if (error) throw new Error(`Could not load ${what}: ${error.message}`);
rows.push(...((data ?? []) as Row[]));
if ((data ?? []).length < pageSize) return rows;
}
}

/** Load registry rows. */
async function loadRegistryRows(supabase: Awaited<ReturnType<typeof loadAdminClient>>, args: Args, ownerId: string) {
const kinds = registryKinds(args.kind);
if (kinds.length === 0) return [] as RegistryRecordRow[];
let query = supabase.from("clinical_registry_records").select("*").eq("owner_id", ownerId).in("kind", kinds);
if (args.slug) query = query.eq("slug", args.slug);
const { data, error } = await query.order("kind").order("title");
if (error) throw new Error(`Could not load registry records: ${error.message}`);
return (data ?? []) as RegistryRecordRow[];
const page = (from: number, to: number) => {
let query = supabase.from("clinical_registry_records").select("*").eq("owner_id", ownerId).in("kind", kinds);
if (args.slug) query = query.eq("slug", args.slug);
return query.order("kind").order("title").order("id").range(from, to);
};
return readAllPages<RegistryRecordRow>(page, "registry records");
}

/** Load medication rows. */
async function loadMedicationRows(supabase: Awaited<ReturnType<typeof loadAdminClient>>, args: Args, ownerId: string) {
if (args.kind !== "all" && args.kind !== "medication") return [] as MedicationRecordRow[];
let query = supabase.from("medication_records").select("*").eq("owner_id", ownerId);
if (args.slug) query = query.eq("slug", args.slug);
const { data, error } = await query.order("name");
if (error) throw new Error(`Could not load medication records: ${error.message}`);
return (data ?? []) as MedicationRecordRow[];
const page = (from: number, to: number) => {
let query = supabase.from("medication_records").select("*").eq("owner_id", ownerId);
if (args.slug) query = query.eq("slug", args.slug);
return query.order("name").order("id").range(from, to);
};
return readAllPages<MedicationRecordRow>(page, "medication records");
}

/** Load differential rows. */
Expand All @@ -102,11 +122,12 @@ async function loadDifferentialRows(
ownerId: string,
) {
if (args.kind !== "all" && args.kind !== "differential") return [] as DifferentialRecordRow[];
let query = supabase.from("differential_records").select("*").eq("owner_id", ownerId);
if (args.slug) query = query.eq("slug", args.slug);
const { data, error } = await query.order("kind").order("title");
if (error) throw new Error(`Could not load differential records: ${error.message}`);
return (data ?? []) as DifferentialRecordRow[];
const page = (from: number, to: number) => {
let query = supabase.from("differential_records").select("*").eq("owner_id", ownerId);
if (args.slug) query = query.eq("slug", args.slug);
return query.order("kind").order("title").order("id").range(from, to);
};
return readAllPages<DifferentialRecordRow>(page, "differential records");
}

type OwnerCounts = {
Expand Down
11 changes: 10 additions & 1 deletion scripts/eval-rag.ts
Original file line number Diff line number Diff line change
Expand Up @@ -98,7 +98,9 @@ function parseArgs(argv: string[]): EvalArgs {

for (let index = 0; index < argv.length; index += 1) {
const token = argv[index];
if (!token.startsWith("--")) continue;
// A mistyped flag used to be skipped, or to swallow the next token, so a run could
// quietly drop --fail-on-threshold and report green. Refuse anything not recognised.
if (!token.startsWith("--")) throw new Error(`Unexpected argument: ${token}`);

if (token === "--json") {
args.json = true;
Expand All @@ -113,6 +115,9 @@ function parseArgs(argv: string[]): EvalArgs {
continue;
}

if (!["--owner-email", "--owner-id", "--limit", "--question"].includes(token)) {
throw new Error(`Unknown flag: ${token}`);
}
const value = argv[index + 1];
if (!value || value.startsWith("--")) throw new Error(`Missing value for ${token}`);
index += 1;
Expand Down Expand Up @@ -350,6 +355,10 @@ async function main() {
}
}

if (args.failOnThreshold && results.length === 0) {
console.error("--fail-on-threshold: no eval cases were selected, so nothing was checked.");
process.exit(1);
}
if (args.failOnThreshold && thresholdFailures.length > 0) process.exit(1);
}

Expand Down
15 changes: 14 additions & 1 deletion scripts/ledger-inbox.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -549,9 +549,22 @@ function assertReopenIdentity(markdown, request) {
}
}

/**
* Known encoding damage in the hand-written preamble, which no request can reach. An arrow saved
* through a Windows code page became "→" (#WWTRM5). Repaired only inside a real reconcile, so
* a batch with no requests still reproduces its base byte for byte for check:ledger-write-discipline.
*/
const LEDGER_MOJIBAKE_REPAIRS = [["\u0393\u00e5\u00c6", "\u2192"]];

export function repairLedgerMojibake(markdown) {
let next = markdown;
for (const [broken, intended] of LEDGER_MOJIBAKE_REPAIRS) next = next.split(broken).join(intended);
return next;
}

export function applyRequestBatch(markdown, requests, options = {}) {
const plan = planRequestBatch(requests, options);
let next = markdown;
let next = requests.length > 0 ? repairLedgerMojibake(markdown) : markdown;
const closedInBatch = new Set();
const ulidToDisplayId = new Map();

Expand Down
2 changes: 1 addition & 1 deletion scripts/sql/operator-export-site-content-population.sql
Original file line number Diff line number Diff line change
Expand Up @@ -73,7 +73,7 @@ select jsonb_pretty(jsonb_build_object(
'embedding', null
)
order by r.logical_id
),
) filter (where r.release_id is not null),
'[]'::jsonb
)
)) as population_export
Expand Down
2 changes: 2 additions & 0 deletions src/components/dsm/dsm-search-page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -510,6 +510,8 @@ export function DsmSearchPage({
</section>
) : (
<section aria-label="DSM diagnosis results" className="grid gap-3">
{/* Keeps the cards' h3 titles from skipping a level (a11y A11Y-12). */}
<h2 className="sr-only">Matching diagnoses</h2>
{results.map((result) => (
<DsmSearchResultCard
key={result.slug}
Expand Down
11 changes: 5 additions & 6 deletions src/components/on-call/on-call-entry-editor.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -87,12 +87,11 @@ type DetailFieldSpec = {
* So every optional control on the Compliance form carries this: expiry,
* lead time, issuer, evidence link, URL.
*
* The same is true of the other five section forms, whose optional controls
* still cannot be cleared — a decommissioned ward extension can be corrected
* but not removed. That is older than this page and is filed as follow-up
* rather than fixed here, because it changes saving behaviour on surfaces
* this change does not otherwise touch, and because two of the branches in
* `handleSave` (`list` and `escalationSteps`) do not read this flag at all.
* `handleSave` no longer reads this flag. Its text and select branch clears
* every emptied optional control on every section form, not only Compliance,
* and the `number` branch clears an empty box too. The `list` and
* `escalationSteps` branches write what is on screen. The flag now records
* intent only, so do not rely on removing it to keep a stored value.
*
* Never on a REQUIRED control, and the reason is worth stating because the
* obvious fix for a blank required select is to put it here. A blank
Expand Down
5 changes: 3 additions & 2 deletions src/components/therapy-compass/screens/recommend-screen.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -124,9 +124,10 @@ export function RecommendScreen() {
<>
<div className="mb-3.5 flex flex-wrap items-center justify-between gap-3">
<div className="min-w-0">
<div className="text-base-minus font-semibold text-[color:var(--text-heading)]">
{/* A real heading, so the ranked rows' h3 titles do not skip a level (a11y A11Y-12). */}
<h2 className="m-0 text-base-minus font-semibold text-[color:var(--text-heading)]">
Ranked clinical matches
</div>
</h2>
<p className="mt-0.5 text-xs font-semibold text-[color:var(--text-muted)]">{matchLabel}</p>
</div>
<div className="flex flex-wrap items-center gap-2">
Expand Down
22 changes: 13 additions & 9 deletions src/components/therapy-compass/screens/search-screen.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -270,15 +270,19 @@ export function SearchScreen() {
onClearSearch={b.clearSearch}
/>
) : results.length > 0 ? (
// One hairline card with divided compact rows (no cards inside cards).
<ul
data-testid="therapy-result-list"
className="m-0 list-none divide-y divide-[color:var(--border)] overflow-hidden rounded-lg border border-[color:var(--border)] bg-[color:var(--surface)] p-0"
>
{shown.map((t, index) => (
<CompactResultRow key={t.slug} therapy={t} featured={!browsing && hasRankedQuery && index === 0} />
))}
</ul>
// One hairline card with divided compact rows (no cards inside cards). The hidden
// heading keeps the rows' h3 titles from skipping a level (a11y A11Y-12).
<>
<h2 className="sr-only">Matching therapies</h2>
<ul
data-testid="therapy-result-list"
className="m-0 list-none divide-y divide-[color:var(--border)] overflow-hidden rounded-lg border border-[color:var(--border)] bg-[color:var(--surface)] p-0"
>
{shown.map((t, index) => (
<CompactResultRow key={t.slug} therapy={t} featured={!browsing && hasRankedQuery && index === 0} />
))}
</ul>
</>
) : null}
{outside.length > 0 ? (
<div data-testid="therapy-outside-filter" className="mt-2 grid gap-2">
Expand Down
3 changes: 2 additions & 1 deletion src/components/work-screens/admin/requests-page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -559,7 +559,8 @@ function RequestCard({
<WorkButton variant="primary" icon={Copy} onClick={onCopy} testId="admin-requests-copy">
Copy and mark sent
</WorkButton>
{isExampleRecord(request) ? (
{/* A blocked message never goes into an href, where a middle click or copied link would skip onClick. */}
{isExampleRecord(request) || requestProblem(request) ? (
<WorkButton variant="secondary" icon={Mail} onClick={() => void onEmail()} testId="admin-requests-email">
Email draft
</WorkButton>
Expand Down
18 changes: 18 additions & 0 deletions src/lib/legacy-home-redirect.ts
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,16 @@ const aliasOnlyModePaths = {
tools: "/tools",
} as const;

/**
* Mode names people type or bookmark that are not the mode's id. `/?mode=medications`
* used to fail the mode check and land on Answer with no sign anything was wrong
* (design audit D26-52), so these keep the visitor on `/` under the mode they meant.
*/
const renamedModeIds = {
medication: "prescribing",
medications: "prescribing",
} as const;

/**
* Dropped when a mode forwards to its own page: `mode` is consumed by the
* pathname, and `run=1` would submit the draft on a page with nothing to
Expand All @@ -49,6 +59,14 @@ export function legacyHomeRedirectUrl(requestUrl: LegacyHomeRequestUrl, method:

const mode = requestUrl.searchParams.get("mode");

const renamedMode = mode ? renamedModeIds[mode as keyof typeof renamedModeIds] : undefined;
if (renamedMode) {
const renamedDestination = new URL(requestUrl.toString());
renamedDestination.searchParams.set("mode", renamedMode);
renamedDestination.hash = "";
return renamedDestination;
}

const aliasOnlyPath = mode ? aliasOnlyModePaths[mode as keyof typeof aliasOnlyModePaths] : undefined;
if (aliasOnlyPath) {
const aliasDestination = new URL(requestUrl.toString());
Expand Down
1 change: 1 addition & 0 deletions src/lib/teaching/assessments/model.ts
Original file line number Diff line number Diff line change
Expand Up @@ -926,6 +926,7 @@ export function assessmentsReducer(s: AssessmentsState, a: AssessmentsAction): A
if (![1, 2, 3, 4].includes(a.epa) || !SUPERVISION_LEVELS.some((l) => l.id === a.level)) return s;
if (a.complexity !== undefined && !validComplexity(a.complexity)) return s;
const note = a.note?.trim();
if (!validText(note)) return s;
const done: EpaRequest = {
epa: a.epa,
who: "sup",
Expand Down
22 changes: 21 additions & 1 deletion tests/compare-retrieval-eval.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ import { mkdtempSync, rmSync, writeFileSync } from "node:fs";
import { tmpdir } from "node:os";
import { join } from "node:path";
import { afterAll, describe, expect, it } from "vitest";
import { comparePerCaseRanks, compareRetrievalEval } from "../scripts/compare-retrieval-eval";
import { comparePerCaseRanks, compareRetrievalEval, recallFailures } from "../scripts/compare-retrieval-eval";

// A complete retrieval eval summary as emitted by summarizeGoldenRetrievalResults, trimmed to the
// fields the comparison reads.
Expand Down Expand Up @@ -199,3 +199,23 @@ describe("compare-retrieval-eval CLI --fail-on-regression", () => {
expect(out.status).toBe(1);
});
});

describe("recallFailures", () => {
it("passes a pair that holds recall at 1.0", () => {
expect(recallFailures(compareRetrievalEval(summary(), summary()).rows)).toEqual([]);
});

it("fails a candidate below the pinned 1.0 even when the baseline was lower", () => {
const rows = compareRetrievalEval(
summary({ document_recall_at_5: 0.9 }),
summary({ document_recall_at_5: 0.95 }),
).rows;
expect(recallFailures(rows)).toEqual(["document_recall_at_5 is 0.9500, below the pinned 1.0"]);
});

it("fails a content recall drop", () => {
const rows = compareRetrievalEval(summary(), summary({ content_recall_at_5: 0.9583 })).rows;
expect(recallFailures(rows)).toHaveLength(1);
expect(recallFailures(rows)[0]).toContain("content_recall_at_5");
});
});
Loading
Loading