From adf2fc6b68fac734020ed76978167828a054f743 Mon Sep 17 00:00:00 2001 From: Adam Date: Wed, 28 Oct 2026 12:09:27 +0000 Subject: [PATCH] Ask reads a saved case's R2 and Conclusor paragraphs in full The case summary cut backing-up paragraphs and why-we-win reasons to 240 characters and kept only 3 If-they-say answers at 200. They now pass in full (2,000-character safety bound each, up to 8 answers). A case may contribute 12,000 characters, and all saved results 16,000. --- .../functions/_shared/debate-context.test.ts | 34 ++++++++++++++++++- supabase/functions/_shared/debate-context.ts | 21 +++++++++--- 2 files changed, 49 insertions(+), 6 deletions(-) diff --git a/supabase/functions/_shared/debate-context.test.ts b/supabase/functions/_shared/debate-context.test.ts index ca2695964..3ac934b35 100644 --- a/supabase/functions/_shared/debate-context.test.ts +++ b/supabase/functions/_shared/debate-context.test.ts @@ -2,6 +2,8 @@ import { boundOutputDigests, type DebateOutputCandidate, digestOutput, + MAX_CASE_DIGEST_CHARS, + MAX_CASE_PARAGRAPH_CHARS, MAX_CONTEXT_OUTPUTS, MAX_OUTPUT_DIGEST_CHARS, MAX_OUTPUTS_TOTAL_CHARS, @@ -91,7 +93,9 @@ Deno.test("digests are bounded and whole results are dropped, never cut, when th contentions: Array.from({ length: 10 }, (_, n) => ({ claim: `Claim ${n}`, reasoning: "R".repeat(2_000), impact: "I".repeat(2_000) })), }; const digest = digestOutput("case", longCase); - assert(digest.length <= MAX_OUTPUT_DIGEST_CHARS, "one result exceeded its digest bound"); + assert(digest.length <= MAX_CASE_DIGEST_CHARS, "one case exceeded its digest bound"); + const longAttack = { weaknesses: Array.from({ length: 10 }, () => ({ description: "D".repeat(2_000), howOpponentAttacks: "H".repeat(2_000) })) }; + assert(digestOutput("attack", longAttack).length <= MAX_OUTPUT_DIGEST_CHARS, "an attack exceeded its digest bound"); const items = Array.from({ length: 5 }, (_, n) => ({ id: String(n), digest })); const bounded = boundOutputDigests(items); const used = bounded.included.reduce((total, item) => total + item.digest.length, 0); @@ -102,6 +106,34 @@ Deno.test("digests are bounded and whole results are dropped, never cut, when th assert(empty.empty.length === 1 && empty.omittedForSize.length === 0, "an empty result was reported as too large"); }); +Deno.test("a case's R2 backing-up paragraphs, If they say answers and Conclusor reasons reach the prompt in full", () => { + const paragraph = (label: string) => `${label} opens the paragraph. ${"Evidence and explanation follow here. ".repeat(28)}${label} ENDS HERE.`; + const r2 = { + contentions: [], + reinforce: [ + { argument: "Our first argument: fare-free transport helps those who need it most.", text: paragraph("Backing one") }, + { argument: "Our second argument: free buses are faster.", text: paragraph("Backing two") }, + ], + counters: Array.from({ length: 6 }, (_, n) => ({ targetArgument: `Their claim ${n + 1}`, counterpoint: paragraph(`Answer ${n + 1}`) })), + whyWeWin: [paragraph("Reason one")], + }; + assert(paragraph("x").length > 1_000, "the test paragraphs should be as long as a real R2's"); + const digest = digestOutput("case", r2); + for (const label of ["Backing one", "Backing two", "Answer 1", "Answer 6", "Reason one"]) { + assert(digest.includes(`${label} ENDS HERE.`), `${label} was cut`); + } + assert(!digest.includes("…"), "nothing in a normal R2 should be shortened"); + assert(boundOutputDigests([{ digest }]).included.length === 1, "a full R2 must fit the total budget"); + + const huge = digestOutput("case", { + reinforce: [{ argument: "A", text: "R".repeat(50_000) }], + counters: Array.from({ length: 20 }, () => ({ targetArgument: "T".repeat(5_000), counterpoint: "C".repeat(50_000) })), + }); + assert(huge.length <= MAX_CASE_DIGEST_CHARS, "a huge case exceeded its digest bound"); + assert(!huge.includes("R".repeat(MAX_CASE_PARAGRAPH_CHARS)), "one paragraph exceeded its bound"); + assert((huge.match(/If they say/g) ?? []).length <= 8, "more than 8 answers were kept"); +}); + Deno.test("each feature receives only the debate material its guarantees allow", () => { const loaded = { debateId: DEBATE, diff --git a/supabase/functions/_shared/debate-context.ts b/supabase/functions/_shared/debate-context.ts index 030d09424..a9287ead4 100644 --- a/supabase/functions/_shared/debate-context.ts +++ b/supabase/functions/_shared/debate-context.ts @@ -45,8 +45,18 @@ export const MAX_AUTO_OUTPUTS_PER_KIND = 2; export const MAX_PINNED_OUTPUTS = 3; /** Characters one saved result may contribute. */ export const MAX_OUTPUT_DIGEST_CHARS = 2_500; +/** + * Characters one saved case may contribute. A case is read out in the round, + * so its R2 "backing up" paragraphs, "If they say" answers and Conclusor + * reasons reach the prompt in full: an R2 with 6-8 answers runs ~10,000. + */ +export const MAX_CASE_DIGEST_CHARS = 12_000; +/** Characters kept from one of those spoken paragraphs; a safety bound, not a summary length. */ +export const MAX_CASE_PARAGRAPH_CHARS = 2_000; +/** "If they say" answers kept from one case; R2 writes at most 8. */ +export const MAX_CASE_COUNTERS = 8; /** Characters all saved results together may contribute. */ -export const MAX_OUTPUTS_TOTAL_CHARS = 8_000; +export const MAX_OUTPUTS_TOTAL_CHARS = 16_000; /** * Saved results that help each feature, in order of usefulness. Results of @@ -186,9 +196,9 @@ function digestCase(data: Record): string { text(item.impact, 240) && ` Why it matters: ${text(item.impact, 240)}`, ])), ...list(data.reinforce).map(record).filter(Boolean).slice(0, 3).map((item) => - text(item!.text, 240) && `Backing up ${text(item!.argument, 80) || "an argument"}: ${text(item!.text, 240)}`), - ...list(data.whyWeWin).slice(0, 3).map((item) => text(item, 240) && `Why we win: ${text(item, 240)}`), - ...counters.slice(0, 3).map((item) => `If they say "${text(item.targetArgument, 120)}", we answer: ${text(item.counterpoint, 200)}`), + text(item!.text, MAX_CASE_PARAGRAPH_CHARS) && `Backing up ${text(item!.argument, 200) || "an argument"}: ${text(item!.text, MAX_CASE_PARAGRAPH_CHARS)}`), + ...list(data.whyWeWin).slice(0, 3).map((item) => text(item, MAX_CASE_PARAGRAPH_CHARS) && `Why we win: ${text(item, MAX_CASE_PARAGRAPH_CHARS)}`), + ...counters.slice(0, MAX_CASE_COUNTERS).map((item) => `If they say "${text(item.targetArgument, 200)}", we answer: ${text(item.counterpoint, MAX_CASE_PARAGRAPH_CHARS)}`), ]); } @@ -276,7 +286,8 @@ export function digestOutput(kind: DebateContextOutputKind, data: unknown): stri : kind === "chat" ? digestChat(data) : kind === "crossfire" ? digestCrossfire(value) : digestSparring(value); - return digest.length > MAX_OUTPUT_DIGEST_CHARS ? `${digest.slice(0, MAX_OUTPUT_DIGEST_CHARS - 1)}…` : digest; + const max = kind === "case" ? MAX_CASE_DIGEST_CHARS : MAX_OUTPUT_DIGEST_CHARS; + return digest.length > max ? `${digest.slice(0, max - 1)}…` : digest; } export interface DebateOutputDigest extends DebateOutputRef {