From eca177b079d6f5288bd2f7cda8b827fa576feff7 Mon Sep 17 00:00:00 2001 From: Luiz Gustavo Date: Mon, 25 May 2026 01:31:59 -0300 Subject: [PATCH] W5.9: case-writer pulls Dupin contradictions as narrative "tensions" MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Enthusiast-grade intrigue, not skeptic framing. When the corpus disagrees with itself — one witness says the fireball was green, another says red; one report dates an event, another contradicts it — that is suspense material, not a debunk. The narrator now receives a "Tensions in the record" section (Dupin's contradictions for the topic, top 4) and is told to use them to build the question the record itself could not settle. - New ContradictionRow type + renderTensions() (bilingual, cites chunks) - Query: contradictions matching topic ILIKE, top 4 by recency - buildPrompt gains a tensions param + "## Tensions in the record" section framed as intrigue ("these are intrigue, not failure") Deferred deploy: the bulk entity-enrichment run is still finishing the organization class inside the live investigator container; rebuilding now would kill it. Will deploy this + reprocess the flagship green-fireballs case_report once the bulk completes. Co-Authored-By: Claude Opus 4.7 (1M context) --- .../src/detectives/case_writer.ts | 45 ++++++++++++++++++- 1 file changed, 44 insertions(+), 1 deletion(-) diff --git a/investigator-runtime/src/detectives/case_writer.ts b/investigator-runtime/src/detectives/case_writer.ts index 6e23365..e605c15 100644 --- a/investigator-runtime/src/detectives/case_writer.ts +++ b/investigator-runtime/src/detectives/case_writer.ts @@ -69,6 +69,13 @@ interface BriefRow { unexplained_pt_br: string; } +interface ContradictionRow { + contradiction_id: string; + topic: string; + topic_pt_br: string | null; + chunks: unknown; +} + function topicSlug(topic: string): string { return topic .toLowerCase() @@ -158,12 +165,29 @@ function renderBrief(b: BriefRow | null, lang: "pt" | "en"): string { ].join("\n"); } +function renderTensions(rows: ContradictionRow[], lang: "pt" | "en"): string { + if (rows.length === 0) return "_(no tensions in the record)_"; + return rows.map((c) => { + const topic = lang === "pt" ? (c.topic_pt_br ?? c.topic) : c.topic; + const positions = Array.isArray(c.chunks) ? c.chunks as Array> : []; + const lines = positions.slice(0, 3).map((p) => { + const stmt = lang === "pt" + ? (p.statement_pt_br as string) ?? (p.statement as string) + : (p.statement as string); + const pageStr = String(p.page ?? "").padStart(3, "0"); + return ` - ${stmt} [[${p.doc_id}/p${pageStr}#${p.chunk_id}]]`; + }).join("\n"); + return `### ${topic}\n${lines}`; + }).join("\n\n"); +} + function buildPrompt( task: CaseWriterTask, scenes: SearchHit[], evidence: EvidenceRow[], witnesses: WitnessRow[], gaps: GapRow[], + tensions: ContradictionRow[], brief: BriefRow | null, lang: "pt" | "en", ): string { @@ -210,6 +234,15 @@ function buildPrompt( "", renderNamedWitnesses(witnesses), "", + "## Tensions in the record", + "", + "Points where the documents disagree with each other — a witness says", + "green, another says red; one report fixes a date, another contradicts", + "it. These are intrigue, not failure. Use them to build suspense: the", + "record itself could not settle the question.", + "", + renderTensions(tensions, lang), + "", "## Strategic brief (internal — do NOT cite or attribute)", "", "A separate analyst built the strongest pro-anomaly reading the corpus", @@ -321,6 +354,16 @@ export async function runCaseWriter(task: CaseWriterTask): Promise< ).catch(() => [] as BriefRow[]); const brief = briefRows[0] ?? null; + // Tensions — Dupin's contradictions on this topic. Enthusiast-grade + // intrigue ("the record disagreed with itself"), not skeptic framing. + const tensions = await query( + `SELECT contradiction_id, topic, topic_pt_br, chunks + FROM public.contradictions + WHERE LOWER(topic) LIKE $1 OR LOWER(COALESCE(topic_pt_br,'')) LIKE $1 + ORDER BY created_at DESC LIMIT 4`, + [filter], + ).catch(() => [] as ContradictionRow[]); + await audit({ event: "case_writer_grounded", job_id: task.job_id, @@ -340,7 +383,7 @@ export async function runCaseWriter(task: CaseWriterTask): Promise< } const systemPrompt = await readFile(PROMPT_PATH, "utf-8"); - const prompt = buildPrompt(task, scenes, evidence, witnesses, gaps, brief, lang); + const prompt = buildPrompt(task, scenes, evidence, witnesses, gaps, tensions, brief, lang); // Case-writer wants more output budget than the other detectives. const llm = await callClaude({