fix(pdf): split pagesRead from pagesConsulted — the honest badge count
Live-smoke follow-up the user approved (2026-09-19): the GEB read showed
"78 pages consulted" when the reader genuinely READ ~12 - pdf_search hit
snippets inflated the count. PdfResult now carries both:
- pagesRead: extract/render output entered the loop - the honest count
the badge and brief footer show ("read N pages", "Pages read: ...").
- pagesConsulted: every page whose content entered the loop, search-hit
context lines included - unchanged, still the citation audit set (the
reader's instructions still say "Pages consulted: N, N, ..." and the
footer still lists it when it exceeds the read set).
The unread-finish gate deliberately stays on CONSULTED: search hits are
genuine consultation (anti-fabrication duty), so a search-only brief is
honest and must deliver - pagesRead is a reporting refinement, not a
delivery gate. Pinned by the search-only faux test (read [], consulted
[1], delivered).
UI tolerates pre-split persisted records via ?? fallbacks (old steers
lack pagesRead).
This commit is contained in:
@@ -422,13 +422,14 @@ describe("steer card rendering (the delivered artifact)", () => {
|
||||
path: "/Users/x/course/chapter-3.pdf",
|
||||
question: "What is the integral of x dx?",
|
||||
brief: "The power rule appears on **page 1**: the integral of x dx is x^2/2 + C.",
|
||||
pagesRead: [1],
|
||||
pagesConsulted: [1, 2],
|
||||
},
|
||||
);
|
||||
const html = renderToStaticMarkup(<SubagentSteerCard message={steer as never} />);
|
||||
assert.match(html, /PDF/);
|
||||
assert.match(html, /reading complete/);
|
||||
assert.match(html, /2 pages consulted/);
|
||||
assert.match(html, /read 1 page/, "the badge counts READ pages, not search hits");
|
||||
assert.match(html, /<details[^>]*data-testid="pdf-complete-badge"/, "a real disclosure like the research badge");
|
||||
assert.ok(!/<details[^>]*open/.test(html), "collapsed by default");
|
||||
assert.ok(html.includes("power rule"), "the brief rides inside the details");
|
||||
@@ -439,7 +440,7 @@ describe("steer card rendering (the delivered artifact)", () => {
|
||||
it("renders the PDF failure brief with the failed badge when the reader FAILED", () => {
|
||||
const steer = buildSubagentSteer(
|
||||
{ id: "jpf2", kind: "pdf", toolCallId: "cpf2" },
|
||||
{ path: "/tmp/doc.pdf", question: "q", brief: "PDF failed: the file could not be opened", pagesConsulted: [] },
|
||||
{ path: "/tmp/doc.pdf", question: "q", brief: "PDF failed: the file could not be opened", pagesRead: [], pagesConsulted: [] },
|
||||
);
|
||||
const html = renderToStaticMarkup(<SubagentSteerCard message={steer as never} />);
|
||||
assert.match(html, /pdf-brief-card/);
|
||||
|
||||
+3
-1
@@ -292,7 +292,7 @@ describe("steer helpers (contracts.ts)", () => {
|
||||
});
|
||||
|
||||
it("pdf steers (sources v0): brief + page citations on success, PDF failed: shape on failure, abort wording per kind", () => {
|
||||
const pdf: PdfResult = { path: "/tmp/doc.pdf", question: "q?", brief: "The formula is on p. 2.", pagesConsulted: [1, 2] };
|
||||
const pdf: PdfResult = { path: "/tmp/doc.pdf", question: "q?", brief: "The formula is on p. 2.", pagesRead: [2], pagesConsulted: [1, 2] };
|
||||
const ok = buildSubagentSteer({ id: "jp", kind: "pdf", toolCallId: "cp" }, pdf);
|
||||
assert.ok(ok.parts[0].type === "text" && ok.parts[0].text.includes("page citations"), "the teacher is told to cite pages");
|
||||
assert.ok(ok.parts[0].type === "text" && ok.parts[0].text.includes("The formula is on p. 2."));
|
||||
@@ -622,6 +622,7 @@ describe("pdf runner (sources v0)", () => {
|
||||
const result = done.result as PdfResult;
|
||||
assert.equal(result.path, PDF_FIXTURE);
|
||||
assert.match(result.brief, /integral rule/);
|
||||
assert.deepEqual(result.pagesRead, [1, 2], "extracted pages are READ pages");
|
||||
assert.deepEqual(result.pagesConsulted, [1, 2]);
|
||||
|
||||
assert.equal(delivery.steers.length, 1, "one job, one steer");
|
||||
@@ -651,6 +652,7 @@ describe("pdf runner (sources v0)", () => {
|
||||
assert.equal(done.status, "error", "the failure brief is a subagent failure");
|
||||
const result = done.result as PdfResult;
|
||||
assert.match(result.brief, /^PDF failed:/);
|
||||
assert.deepEqual(result.pagesRead, []);
|
||||
assert.deepEqual(result.pagesConsulted, []);
|
||||
|
||||
const steer = delivery.steers[0]!;
|
||||
|
||||
@@ -64,6 +64,7 @@ describe("pdf-reader loop (faux engine)", () => {
|
||||
);
|
||||
assert.equal(result.path, FIXTURE);
|
||||
assert.match(result.brief, /integral rule/);
|
||||
assert.deepEqual(result.pagesRead, [1, 2], "extracted pages are read pages");
|
||||
assert.deepEqual(result.pagesConsulted, [1, 2]);
|
||||
});
|
||||
|
||||
@@ -85,7 +86,9 @@ describe("pdf-reader loop (faux engine)", () => {
|
||||
{ engine, onActivity: (line) => void (line && (nudged ||= line.startsWith("pdf_search"))) },
|
||||
);
|
||||
assert.match(result.brief, /Found it on page 1/);
|
||||
assert.deepEqual(result.pagesRead, [], "search hits consult but do not read (the split)");
|
||||
assert.deepEqual(result.pagesConsulted, [1]);
|
||||
// and the gate lets it through: search hits ARE genuine consultation
|
||||
// the first (invention) answer must NOT be the delivered brief
|
||||
assert.notEqual(result.brief, "The answer is 42, trust me.");
|
||||
});
|
||||
@@ -102,6 +105,7 @@ describe("pdf-reader loop (faux engine)", () => {
|
||||
{ path: FIXTURE, question: "what is the force formula?" },
|
||||
{ engine, headless },
|
||||
);
|
||||
assert.deepEqual(result.pagesRead, [2, 3], "rendered pages are read pages");
|
||||
assert.deepEqual(result.pagesConsulted, [2, 3]);
|
||||
assert.deepEqual(headless.calls, ["render:2,3"]);
|
||||
});
|
||||
@@ -137,6 +141,7 @@ describe("pdf-reader loop (faux engine)", () => {
|
||||
},
|
||||
);
|
||||
assert.match(result.brief, /Extracted instead/);
|
||||
assert.deepEqual(result.pagesRead, [1]);
|
||||
assert.deepEqual(result.pagesConsulted, [1]);
|
||||
});
|
||||
|
||||
@@ -149,6 +154,7 @@ describe("pdf-reader loop (faux engine)", () => {
|
||||
// had the model been consulted, the brief would be its text instead
|
||||
assert.match(result.brief, /^PDF failed:/);
|
||||
assert.ok(!result.brief.includes("never reached"));
|
||||
assert.deepEqual(result.pagesRead, []);
|
||||
assert.deepEqual(result.pagesConsulted, []);
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user