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:
amosblomqvist
2026-09-19 16:52:09 +03:00
parent 7a3e19e95f
commit 1b9122c88b
8 changed files with 57 additions and 14 deletions
+6
View File
@@ -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, []);
});
});