scrum_master: tree-split + scrum_reviews.jsonl writer + truncation warning
Extends the scrum-master pipeline to handle input overflow on large source files (>6KB). Previously, the review prompt truncated the file to first-chunk, which caused false-positive "field is missing" findings whenever the actual field was past the cutoff. Now each file >FILE_TREE_SPLIT_THRESHOLD (6000) is sharded at FILE_SHARD_SIZE (3500), each shard summarized via gpt-oss:120b cloud, and the distillations merged into a scratchpad. The review then runs against the scratchpad with an explicit truncation-awareness clause in the prompt: "DO NOT claim any field, function, or feature is 'missing' based on its absence from this distillation." Also writes each accepted review as a JSONL row to data/_kb/scrum_reviews.jsonl (file, reviewed_at, accepted_model, accepted_on_attempt, attempts_made, tree_split_fired, preview). This is the source the auditor's kb_query reads to surface per-file scrum reviews on PRs that touch those files (cohesion plan Phase C). Verified: scrum review of 92KB playbook_memory.rs → 27 shards via cloud → distilled scratchpad → qwen3.5 local 7B accepted on attempt 1 (5931 chars). Tree-split fires, jsonl row appended, output file contains structured suggestions.
This commit is contained in:
parent
a7aba31935
commit
89d188074b
@ -27,6 +27,14 @@ const CHUNK_SIZE = 800;
|
|||||||
const CHUNK_OVERLAP = 120;
|
const CHUNK_OVERLAP = 120;
|
||||||
const TOP_K_CONTEXT = 5;
|
const TOP_K_CONTEXT = 5;
|
||||||
const MAX_ATTEMPTS = 6;
|
const MAX_ATTEMPTS = 6;
|
||||||
|
// Files larger than this get tree-split instead of truncated. Fixes the
|
||||||
|
// 6KB false-positive class (model claiming a field is "missing" when
|
||||||
|
// it exists past the context cutoff).
|
||||||
|
const FILE_TREE_SPLIT_THRESHOLD = 6000;
|
||||||
|
const FILE_SHARD_SIZE = 3500;
|
||||||
|
// Appended jsonl so auditor's kb_query can surface scrum findings for
|
||||||
|
// files touched by a PR under review. Part of cohesion plan Phase C.
|
||||||
|
const SCRUM_REVIEWS_JSONL = "/home/profit/lakehouse/data/_kb/scrum_reviews.jsonl";
|
||||||
const OUT_DIR = `/home/profit/lakehouse/tests/real-world/runs/scrum_${Date.now().toString(36)}`;
|
const OUT_DIR = `/home/profit/lakehouse/tests/real-world/runs/scrum_${Date.now().toString(36)}`;
|
||||||
|
|
||||||
const PRD_PATH = "/home/profit/lakehouse/docs/PRD.md";
|
const PRD_PATH = "/home/profit/lakehouse/docs/PRD.md";
|
||||||
@ -61,6 +69,8 @@ type Chunk = { id: string; text: string; embedding: number[]; origin: string; of
|
|||||||
interface FileReview {
|
interface FileReview {
|
||||||
file: string;
|
file: string;
|
||||||
file_bytes: number;
|
file_bytes: number;
|
||||||
|
tree_split_fired: boolean;
|
||||||
|
shards_summarized: number;
|
||||||
top_prd_chunks: Array<{ origin: string; offset: number; score: number }>;
|
top_prd_chunks: Array<{ origin: string; offset: number; score: number }>;
|
||||||
top_proposal_chunks: Array<{ origin: string; offset: number; score: number }>;
|
top_proposal_chunks: Array<{ origin: string; offset: number; score: number }>;
|
||||||
attempts_made: number;
|
attempts_made: number;
|
||||||
@ -153,6 +163,47 @@ function retrieveTopK(query_emb: number[], pool: Chunk[], k: number): Chunk[] {
|
|||||||
.map(x => ({ ...x.c, _score: x.score } as any));
|
.map(x => ({ ...x.c, _score: x.score } as any));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Tree-split a large file: shard it, summarize each shard against
|
||||||
|
// the review question, merge into a scratchpad. Uses cloud because
|
||||||
|
// the summarization step needs quality > speed. Returns the
|
||||||
|
// scratchpad (full-file distillation) and the cloud-call count.
|
||||||
|
async function treeSplitFile(
|
||||||
|
filePath: string,
|
||||||
|
content: string,
|
||||||
|
): Promise<{ scratchpad: string; shards: number; cloud_calls: number }> {
|
||||||
|
const shards: Array<{ from: number; to: number; text: string }> = [];
|
||||||
|
for (let i = 0; i < content.length; i += FILE_SHARD_SIZE) {
|
||||||
|
const end = Math.min(i + FILE_SHARD_SIZE, content.length);
|
||||||
|
shards.push({ from: i, to: end, text: content.slice(i, end) });
|
||||||
|
}
|
||||||
|
let scratchpad = "";
|
||||||
|
let cloud_calls = 0;
|
||||||
|
log(` tree-split: ${content.length} chars → ${shards.length} shards of ${FILE_SHARD_SIZE}`);
|
||||||
|
for (const [si, shard] of shards.entries()) {
|
||||||
|
const prompt = `You are summarizing ONE SHARD of a source file as part of a multi-shard review. File: ${filePath}. Shard ${si + 1}/${shards.length} (bytes ${shard.from}..${shard.to}).
|
||||||
|
|
||||||
|
─────── shard source ───────
|
||||||
|
${shard.text}
|
||||||
|
─────── end shard ───────
|
||||||
|
|
||||||
|
Scratchpad of prior shards (if empty, this is shard 1):
|
||||||
|
${scratchpad || "(empty)"}
|
||||||
|
|
||||||
|
Extract ONLY facts useful for reviewing this file against its PRD: function names + purposes, struct fields + types, invariants, edge cases, TODO markers, error-handling style. Under 150 words. No prose outside the extracted facts.`;
|
||||||
|
const r = await chat({
|
||||||
|
provider: "ollama_cloud",
|
||||||
|
model: "gpt-oss:120b",
|
||||||
|
prompt,
|
||||||
|
max_tokens: 400,
|
||||||
|
});
|
||||||
|
cloud_calls += 1;
|
||||||
|
if (r.content) {
|
||||||
|
scratchpad += `\n--- shard ${si + 1} (bytes ${shard.from}..${shard.to}) ---\n${r.content.trim()}`;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return { scratchpad, shards: shards.length, cloud_calls };
|
||||||
|
}
|
||||||
|
|
||||||
async function reviewFile(
|
async function reviewFile(
|
||||||
filePath: string,
|
filePath: string,
|
||||||
prd_chunks: Chunk[],
|
prd_chunks: Chunk[],
|
||||||
@ -180,11 +231,39 @@ async function reviewFile(
|
|||||||
...topPlan.map(c => `[PLAN @${c.offset}]\n${c.text.slice(0, 600)}`),
|
...topPlan.map(c => `[PLAN @${c.offset}]\n${c.text.slice(0, 600)}`),
|
||||||
].join("\n\n");
|
].join("\n\n");
|
||||||
|
|
||||||
|
// Files bigger than FILE_TREE_SPLIT_THRESHOLD get tree-split.
|
||||||
|
// Summarize each shard to a scratchpad, then review against the
|
||||||
|
// scratchpad instead of the truncated first chunk. Prevents the
|
||||||
|
// false-positive pattern where the model claims a field is
|
||||||
|
// "missing" because it's past the context cutoff.
|
||||||
|
let sourceForPrompt: string;
|
||||||
|
let treeSplitFired = false;
|
||||||
|
let shardsSummarized = 0;
|
||||||
|
let extraCloudCalls = 0;
|
||||||
|
if (content.length > FILE_TREE_SPLIT_THRESHOLD) {
|
||||||
|
treeSplitFired = true;
|
||||||
|
const ts = await treeSplitFile(rel, content);
|
||||||
|
shardsSummarized = ts.shards;
|
||||||
|
extraCloudCalls = ts.cloud_calls;
|
||||||
|
sourceForPrompt = `[FULL-FILE SCRATCHPAD — distilled from ${ts.shards} shards via tree-split]\n${ts.scratchpad}`;
|
||||||
|
} else {
|
||||||
|
sourceForPrompt = content;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Prompt — when tree-split fired, include an explicit instruction
|
||||||
|
// not to claim a field/function is "missing" because the scratchpad
|
||||||
|
// is a distillation not the full file. Attacks the rubric-tuning
|
||||||
|
// concern J called out.
|
||||||
|
const truncationWarning = treeSplitFired
|
||||||
|
? `\nIMPORTANT: the "source" below is a multi-shard distillation (tree-split across ${shardsSummarized} shards), NOT the full raw file. DO NOT claim any field, function, or feature is "missing" based on its absence from this distillation — the distillation may have elided it. Only call out gaps that appear DIRECTLY contradicted by the PRD excerpts.\n`
|
||||||
|
: "";
|
||||||
|
|
||||||
const baseTask = `You are reviewing one source file against the Lakehouse PRD and an active cohesion-integration plan.
|
const baseTask = `You are reviewing one source file against the Lakehouse PRD and an active cohesion-integration plan.
|
||||||
|
|
||||||
FILE: ${rel}
|
FILE: ${rel} (${content.length} bytes${treeSplitFired ? `, tree-split into ${shardsSummarized} shards` : ""})
|
||||||
|
${truncationWarning}
|
||||||
─────── source ───────
|
─────── source ───────
|
||||||
${content.slice(0, 6000)}${content.length > 6000 ? "\n[... truncated after 6KB ...]" : ""}
|
${sourceForPrompt}
|
||||||
─────── end source ───────
|
─────── end source ───────
|
||||||
|
|
||||||
${contextBlock}
|
${contextBlock}
|
||||||
@ -234,9 +313,11 @@ Respond with markdown. Be specific, not generic. Cite file-region + PRD-chunk-of
|
|||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
|
||||||
return {
|
const review: FileReview = {
|
||||||
file: rel,
|
file: rel,
|
||||||
file_bytes: content.length,
|
file_bytes: content.length,
|
||||||
|
tree_split_fired: treeSplitFired,
|
||||||
|
shards_summarized: shardsSummarized,
|
||||||
top_prd_chunks: topPrd.map(c => ({ origin: c.origin, offset: c.offset, score: (c as any)._score })),
|
top_prd_chunks: topPrd.map(c => ({ origin: c.origin, offset: c.offset, score: (c as any)._score })),
|
||||||
top_proposal_chunks: topPlan.map(c => ({ origin: c.origin, offset: c.offset, score: (c as any)._score })),
|
top_proposal_chunks: topPlan.map(c => ({ origin: c.origin, offset: c.offset, score: (c as any)._score })),
|
||||||
attempts_made: history.length,
|
attempts_made: history.length,
|
||||||
@ -246,6 +327,31 @@ Respond with markdown. Be specific, not generic. Cite file-region + PRD-chunk-of
|
|||||||
suggestions: accepted ?? "[no acceptable answer after escalation ladder exhausted]",
|
suggestions: accepted ?? "[no acceptable answer after escalation ladder exhausted]",
|
||||||
duration_ms: Date.now() - t0,
|
duration_ms: Date.now() - t0,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// Append to the shared scrum-reviews jsonl so the auditor's
|
||||||
|
// kb_query check can surface relevant reviews for files in a
|
||||||
|
// PR diff. Cohesion plan Phase C wire.
|
||||||
|
if (accepted) {
|
||||||
|
const { appendFile, mkdir } = await import("node:fs/promises");
|
||||||
|
const { dirname } = await import("node:path");
|
||||||
|
await mkdir(dirname(SCRUM_REVIEWS_JSONL), { recursive: true });
|
||||||
|
const row = {
|
||||||
|
file: rel,
|
||||||
|
reviewed_at: new Date().toISOString(),
|
||||||
|
accepted_model: acceptedModel,
|
||||||
|
accepted_on_attempt: acceptedOn,
|
||||||
|
attempts_made: history.length,
|
||||||
|
tree_split_fired: treeSplitFired,
|
||||||
|
suggestions_preview: accepted.slice(0, 2000),
|
||||||
|
};
|
||||||
|
try {
|
||||||
|
await appendFile(SCRUM_REVIEWS_JSONL, JSON.stringify(row) + "\n");
|
||||||
|
} catch (e) {
|
||||||
|
console.error(`[scrum] failed to append scrum_reviews.jsonl: ${(e as Error).message}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return review;
|
||||||
}
|
}
|
||||||
|
|
||||||
async function loadAndChunk(path: string, origin_tag: string): Promise<Chunk[]> {
|
async function loadAndChunk(path: string, origin_tag: string): Promise<Chunk[]> {
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user