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
3 changes: 3 additions & 0 deletions .env.example
Original file line number Diff line number Diff line change
Expand Up @@ -60,6 +60,7 @@ OPENAI_INDEXING_MODEL=gpt-5.6-terra
# commitment). Must stay well above reasoning headroom: the old 4000 starved the
# strong route's reasoning budget and discarded the answer after 60-90s (GEN-C1).
# Matches the env.ts default.
OPENAI_RERANK_MODEL=gpt-5.6-luna
OPENAI_MAX_OUTPUT_TOKENS=16000
OPENAI_QUERY_CACHE_SIZE=200
# Max inputs per embeddings request (OpenAI caps a request at 2048 inputs / ~300k tokens).
Expand Down Expand Up @@ -98,6 +99,8 @@ RAG_PROVIDER_MODE=auto
# Optional JSON override for app-layer ranking weights (see src/lib/ranking-config.ts).
# Omit for current defaults. Example (enable diversity demotion + linear freshness):
# RAG_RANKING_CONFIG={"documentDiversityPenalty":0.03,"freshness":{"mode":"linear"}}
# Ambiguity-only structured semantic reranking. Keep false until the retrieval canary is approved.
RAG_SEMANTIC_RERANK_ENABLED=false
# Append OR-relaxed recall behind weak-but-nonzero strict text matches (P8b extension).
# Opt-in experiment ONLY and OFF by default (matches the src/lib/env.ts default): it is known to
# regress the golden retrieval eval (buries some correct docs after re-ranking). Leave false;
Expand Down
3 changes: 3 additions & 0 deletions .gitleaksignore
Original file line number Diff line number Diff line change
Expand Up @@ -19,3 +19,6 @@ d2b7b89852a2090d56ce6aed1dd39417aedf332d:.gitleaksignore:generic-api-key:13
# False positive: two synthetic chunk identifiers in a citation-array assertion
# were parsed as a generic API key in the original remediation commit.
b30bcb030edce84e2a0704a8714ee3ef1dcf642d:tests/rag-offline-answer.test.ts:generic-api-key:205
# False positive: a synthetic hard-negative ranking candidate identifier was
# interpreted as a generic API key. The fixture contains no credential material.
d650ecb89bcdfece0af77ad9ea0d62d8eaa42233:scripts/build-ranking-snapshot.ts:generic-api-key:107
138 changes: 138 additions & 0 deletions scripts/build-ranking-snapshot.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,138 @@
import { createHash } from "node:crypto";
import { readFileSync, writeFileSync } from "node:fs";
import { resolve } from "node:path";
import type { RagQueryClass } from "../src/lib/types";
import { candidateFeatures, labelMatches, type ArtifactCandidate } from "./lib/ranking-snapshot-builder";
import {
RANKING_SNAPSHOT_VERSION,
type RankingCandidateFeatures,
type RankingSnapshot,
type RankingSnapshotCandidate,
validateRankingSnapshot,
} from "./lib/ranking-tuning";

type ArtifactCase = {
id: string;
query: string;
expectedQueryClass?: RagQueryClass;
actualQueryClass?: RagQueryClass;
expectedDocumentSubstrings?: string[];
expectedContentTerms?: string[];
topResults?: ArtifactCandidate[];
};

type RetrievalArtifact = { results?: ArtifactCase[] };

function argument(name: string): string | undefined {
const index = process.argv.indexOf(name);
return index < 0 ? undefined : process.argv[index + 1];
}

function candidateHash(value: string): string {
return `sha256:${createHash("sha256").update(value).digest("hex")}`;
}

const hardNegativeTemplates: Array<{
caseId: string;
key: string;
category: NonNullable<RankingSnapshotCandidate["hardNegative"]>["category"];
risk: NonNullable<RankingSnapshotCandidate["hardNegative"]>["risk"];
features: RankingCandidateFeatures;
}> = [
["agitation-im-po-options", "dose-boilerplate-1", "dose_administration_boilerplate", "high"],
["opioid-withdrawal-doses", "dose-boilerplate-2", "dose_administration_boilerplate", "high"],
["clozapine-anc-threshold", "wrong-medication-1", "wrong_medication_document", "high"],
["lithium-therapy-monitoring", "wrong-medication-2", "wrong_medication_document", "high"],
["alcohol-ciwa-threshold", "wrong-threshold-1", "mismatched_threshold", "high"],
["clozapine-cbc-abbreviation-threshold", "wrong-threshold-2", "mismatched_threshold", "high"],
["flowchart-next-step", "flowchart-no-action-1", "flowchart_missing_action", "high"],
["flowchart-next-step", "flowchart-no-action-2", "flowchart_missing_action", "high"],
["patient-safety-plan-include", "stale-duplicate-1", "document_version_duplicate", "medium"],
["active-community-patient-ed", "stale-duplicate-2", "document_version_duplicate", "medium"],
["admission-discharge-comparison", "single-doc-crowding-1", "comparison_single_document_crowding", "medium"],
["depression-adults-vs-children", "single-doc-crowding-2", "comparison_single_document_crowding", "medium"],
].map(([caseId, key, category, risk], index) => ({
caseId,
key,
category,
risk,
features: {
hybridRelevance: index >= 8 ? 0.78 : 0.7,
lexicalCoverage: index >= 10 ? 0.08 : 0.02,
reciprocalRankFusion: index % 2 === 0 ? 0.03 : 0,
titleSectionRelevance: index >= 8 ? 0.16 : 0.08,
metadataRelevance: 0.04,
clinicalEvidence: index >= 10 ? 0.04 : -0.02,
fixedAdjustment: risk === "high" ? -0.3 : -0.18,
},
})) as Array<{
caseId: string;
key: string;
category: NonNullable<RankingSnapshotCandidate["hardNegative"]>["category"];
risk: NonNullable<RankingSnapshotCandidate["hardNegative"]>["risk"];
features: RankingCandidateFeatures;
}>;

function convertArtifact(artifact: RetrievalArtifact): RankingSnapshot {
if (!Array.isArray(artifact.results) || artifact.results.length !== 36) {
throw new Error("Expected a retrieval artifact containing exactly 36 cases");
}
const cases = artifact.results.map((testCase) => {
const expectedDocuments = testCase.expectedDocumentSubstrings ?? [];
const expectedContent = testCase.expectedContentTerms ?? [];
const candidates = (testCase.topResults ?? []).map((candidate, index): RankingSnapshotCandidate => {
const documentText = `${candidate.title ?? ""} ${candidate.file_name ?? ""}`.toLowerCase();
const contentText = (candidate.content_preview ?? "").toLowerCase();
const documentMatch = expectedDocuments.some((label) => labelMatches(documentText, label));
const contentMatch =
expectedContent.length > 0 && expectedContent.every((label) => labelMatches(contentText, label));
return {
candidateHash: candidateHash(`${testCase.id}:${candidate.chunk_id ?? `rank-${index + 1}`}`),
relevanceGrade: documentMatch && contentMatch ? 3 : documentMatch ? 2 : contentMatch ? 1 : 0,
documentMatch,
contentMatch,
features: candidateFeatures(candidate),
};
});
for (const hardNegative of hardNegativeTemplates.filter((item) => item.caseId === testCase.id)) {
candidates.push({
candidateHash: candidateHash(`${testCase.id}:${hardNegative.key}`),
relevanceGrade: 0,
documentMatch: false,
contentMatch: false,
features: hardNegative.features,
hardNegative: { category: hardNegative.category, risk: hardNegative.risk },
});
}
return {
id: testCase.id,
query: testCase.query,
queryClass: testCase.actualQueryClass ?? testCase.expectedQueryClass ?? "unsupported_or_general",
expectedLabels: { documents: expectedDocuments, content: expectedContent },
candidates,
};
});
return {
schema: "rag-ranking-candidate-snapshot",
version: RANKING_SNAPSHOT_VERSION,
sourceCaseCount: cases.length,
sanitization: {
candidateIdentity: "sha256",
excludes: ["raw_uuid", "source_passage", "patient_data", "provider_metadata", "document_storage_path"],
},
cases,
};
}

function main() {
const input = argument("--input");
const output = argument("--output");
if (!input || !output)
throw new Error("Usage: build-ranking-snapshot --input <artifact.json> --output <snapshot.json>");
const artifact = JSON.parse(readFileSync(resolve(input), "utf8")) as RetrievalArtifact;
const snapshot = validateRankingSnapshot(convertArtifact(artifact));
writeFileSync(resolve(output), `${JSON.stringify(snapshot, null, 2)}\n`, "utf8");
console.log(JSON.stringify({ output: resolve(output), cases: snapshot.cases.length, version: snapshot.version }));
}

main();
Loading