W5.9: case-writer pulls Dupin contradictions as narrative "tensions"
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) <noreply@anthropic.com>
This commit is contained in:
parent
3e88a64933
commit
eca177b079
1 changed files with 44 additions and 1 deletions
|
|
@ -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<Record<string, unknown>> : [];
|
||||
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<ContradictionRow>(
|
||||
`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({
|
||||
|
|
|
|||
Loading…
Reference in a new issue