From c5effece629b7eeadd96a4dd92bbc8c7de0cda71 Mon Sep 17 00:00:00 2001 From: BigSimmo <87357024+BigSimmo@users.noreply.github.com> Date: Tue, 18 Aug 2026 21:10:55 +0800 Subject: [PATCH 1/2] fix(rag): restore the strict source_metadata presence pin on retrieval rows PR #2107 loosened sourceMetadataSchema from .nullable() to .nullish() inside a mixed-scope ledger/runbook PR. That reverses the tranche-1 decision (PR #1946, "throw on mismatch") on the one field the retrieval row contract pins stricter than the rest, precisely so an RPC that drops the source_metadata column fails loudly as RetrievalRowShapeError instead of silently degrading every citation to "unknown" governance defaults. Present-and-null stays accepted; absent is again rejected. Adds the discriminating test the loosening lacked. Ranking, scoring and citation output are unchanged: this restores the exact contract the last green canary (run 32111839806) measured. Co-Authored-By: Claude Fable 5 --- src/lib/rag/rag-row-contracts.ts | 8 +++++++- tests/rag-retrieval-row-contract.test.ts | 14 ++++++++++++++ 2 files changed, 21 insertions(+), 1 deletion(-) diff --git a/src/lib/rag/rag-row-contracts.ts b/src/lib/rag/rag-row-contracts.ts index 9dcb84c0b8..5fafb1797a 100644 --- a/src/lib/rag/rag-row-contracts.ts +++ b/src/lib/rag/rag-row-contracts.ts @@ -41,11 +41,17 @@ const retrievalImageSchema = z.looseObject({ caption: z.string(), }); +// Deliberately `.nullable()`, not `.nullish()`: the key must be PRESENT (object or null). +// This is the one field pinned stricter than the rest so an RPC whose column set drifts +// (source_metadata dropped from the SELECT) fails loudly as RetrievalRowShapeError instead +// of silently degrading every citation to "unknown" governance defaults. Tranche 1 decision +// (PR #1946, "throw on mismatch"); PR #2107 loosened it to `.nullish()` and this PR +// restores it. See docs/outstanding-issues.md #343 for the constraint-backing follow-up. const sourceMetadataSchema = z .record(z.string(), z.unknown(), { message: "source_metadata must be a JSON object", }) - .nullish(); + .nullable(); const retrievalRowSchema = z.looseObject({ id: z.string().min(1), diff --git a/tests/rag-retrieval-row-contract.test.ts b/tests/rag-retrieval-row-contract.test.ts index 32438b3ab6..0c1a597071 100644 --- a/tests/rag-retrieval-row-contract.test.ts +++ b/tests/rag-retrieval-row-contract.test.ts @@ -146,6 +146,20 @@ describe("retrieval row shape contract", () => { expect(thrown?.message).toContain("source_metadata"); }); + it("rejects a row whose source_metadata key is absent (RPC column drift must fail loudly)", () => { + // Present-and-null is accepted (see the next test); ABSENT is not. This is the pin PR #2107 + // loosened to .nullish() and the restore re-tightened: an RPC that drops the column must + // surface as RetrievalRowShapeError, not degrade every citation to "unknown" governance. + let thrown: RetrievalRowShapeError | null = null; + try { + assertRetrievalRows([withoutColumn("source_metadata")], "match_document_chunks_hybrid"); + } catch (error) { + thrown = error as RetrievalRowShapeError; + } + expect(thrown).toBeInstanceOf(RetrievalRowShapeError); + expect(thrown?.message).toContain("source_metadata"); + }); + it("accepts absent or null scores, which downstream already coalesces to 0", () => { expect(() => assertRetrievalRows([withoutColumn("text_rank")], "match_document_chunks")).not.toThrow(); expect(() => assertRetrievalRows([hybridRow({ rrf_score: null })], "match_document_chunks")).not.toThrow(); From 6a8f5b4e9abe137ddb746fe14cffa06590b124f8 Mon Sep 17 00:00:00 2001 From: BigSimmo <87357024+BigSimmo@users.noreply.github.com> Date: Tue, 18 Aug 2026 21:11:05 +0800 Subject: [PATCH 2/2] docs(ledger): record the source_metadata pin restore review Co-Authored-By: Claude Fable 5 --- ...494f75b85ffd05fd9b65fead2f59404ca47158efc100ed27dc6.record.md | 1 + 1 file changed, 1 insertion(+) create mode 100644 docs/branch-review-records/0a18b8e9ba7fa494f75b85ffd05fd9b65fead2f59404ca47158efc100ed27dc6.record.md diff --git a/docs/branch-review-records/0a18b8e9ba7fa494f75b85ffd05fd9b65fead2f59404ca47158efc100ed27dc6.record.md b/docs/branch-review-records/0a18b8e9ba7fa494f75b85ffd05fd9b65fead2f59404ca47158efc100ed27dc6.record.md new file mode 100644 index 0000000000..a6f5d29149 --- /dev/null +++ b/docs/branch-review-records/0a18b8e9ba7fa494f75b85ffd05fd9b65fead2f59404ca47158efc100ed27dc6.record.md @@ -0,0 +1 @@ +| 2026-08-18 | claude/restore-source-metadata-pin | c5effece629b7eeadd96a4dd92bbc8c7de0cda71 | restore .nullable() source_metadata pin in src/lib/rag/rag-row-contracts.ts after PR #2107 loosened it; add absent-key rejection test | strict pin restored; 26/26 contract tests; offline 623/623; 36 golden | vitest rag-retrieval-row-contract 26/26; eval:rag:offline 623/623; check:rag:fixtures; tsc clean |