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;
|
unexplained_pt_br: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
interface ContradictionRow {
|
||||||
|
contradiction_id: string;
|
||||||
|
topic: string;
|
||||||
|
topic_pt_br: string | null;
|
||||||
|
chunks: unknown;
|
||||||
|
}
|
||||||
|
|
||||||
function topicSlug(topic: string): string {
|
function topicSlug(topic: string): string {
|
||||||
return topic
|
return topic
|
||||||
.toLowerCase()
|
.toLowerCase()
|
||||||
|
|
@ -158,12 +165,29 @@ function renderBrief(b: BriefRow | null, lang: "pt" | "en"): string {
|
||||||
].join("\n");
|
].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(
|
function buildPrompt(
|
||||||
task: CaseWriterTask,
|
task: CaseWriterTask,
|
||||||
scenes: SearchHit[],
|
scenes: SearchHit[],
|
||||||
evidence: EvidenceRow[],
|
evidence: EvidenceRow[],
|
||||||
witnesses: WitnessRow[],
|
witnesses: WitnessRow[],
|
||||||
gaps: GapRow[],
|
gaps: GapRow[],
|
||||||
|
tensions: ContradictionRow[],
|
||||||
brief: BriefRow | null,
|
brief: BriefRow | null,
|
||||||
lang: "pt" | "en",
|
lang: "pt" | "en",
|
||||||
): string {
|
): string {
|
||||||
|
|
@ -210,6 +234,15 @@ function buildPrompt(
|
||||||
"",
|
"",
|
||||||
renderNamedWitnesses(witnesses),
|
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)",
|
"## Strategic brief (internal — do NOT cite or attribute)",
|
||||||
"",
|
"",
|
||||||
"A separate analyst built the strongest pro-anomaly reading the corpus",
|
"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[]);
|
).catch(() => [] as BriefRow[]);
|
||||||
const brief = briefRows[0] ?? null;
|
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({
|
await audit({
|
||||||
event: "case_writer_grounded",
|
event: "case_writer_grounded",
|
||||||
job_id: task.job_id,
|
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 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.
|
// Case-writer wants more output budget than the other detectives.
|
||||||
const llm = await callClaude({
|
const llm = await callClaude({
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue