docs/plans/p5a: 01 kind+fetch limits, 02 pdftext child + runner, 03 dcache PDF tables, 04 pdfpool, 05 pdfdoc render (map/pages/grep), 06 api, 07 digest citations, metrics, CLI, skill, unit, README. Given tests compile against a panic-only skeleton and fail there for the intended reasons; PDFs for tests are built by internal/pdftest (given), plus two qpdf-encrypted fixtures. Owner changes for the run: vendor go-pdfkit/extract v0.3.0 + reader v0.6.0 (pinned via tools.go until task 02); AGENTS.md, tools/run-plan.sh (enforces a Co-Authored-By trailer for the model), docs/implementer-log.md, `make gate`; gofmt (whitespace) on clean.go and guard; TestUnsupportedType uses a zip now that PDFs are supported; requestDeadline 90→210 s and writeTimeout 120→240 s for the PDF path's worst case. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
5.0 KiB
5.0 KiB
P5a task 05: render an extracted document for an agent
Branch: p5a (git switch p5a; git status --short must be empty, otherwise stop)
Commit subject: pdfdoc: page markers, the map for long documents, page ranges, grep
Goal
An agent's context is small and a thesis is 60k–150k tokens. This task is the pure function that
decides what an agent sees: the whole text with page markers when it fits; otherwise a map
(title, outline with page numbers, the first pages, and how to ask for more); a page range;
or grep results with page numbers. No I/O, no PDFs: it works on a *pdftext.Doc.
Files
- Copy:
internal/pdfdoc/pdfdoc_test.go - Create:
internal/pdfdoc/pdfdoc.go(Render,Full,Marker, the map, page ranges),internal/pdfdoc/grep.go - Modify:
docs/implementer-log.md
Interfaces
package pdfdoc
type Options struct {
Pages string // "" = no range; "a-b", "a" or "a-" (1-based, inclusive)
Grep string // "" = no grep; an RE2 pattern
OutCap int // characters (runes) the content may use
}
type Rendered struct {
Content string
Map bool // Content is the map of a document longer than OutCap
First, Last int // pages whose text is in Content; 0, 0 for a grep answer
GrepHits int // matches found (more may exist than are shown)
Note string // "" or how the answer was cut and how to continue
}
var (
ErrBadPages = errors.New("invalid pages")
ErrBadGrep = errors.New("invalid grep")
)
func Marker(page int) string // "--- page N ---"
func Full(doc *pdftext.Doc) string // every extracted page with its marker
func Render(doc *pdftext.Doc, opt Options) (Rendered, error)
Rules
- Full. For each extracted page p:
Marker(p) + "\n\n" + strings.TrimSpace(text), joined with"\n\n". The given test shows the exact string. - No Pages, no Grep.
- If the rune count of
Full(doc)is at mostOutCap:Content = Full(doc),First = 1,Last= pages extracted. - Otherwise
Map = trueand the content is, in this order:# <Title, or "Untitled PDF"> <Pages> pages · ~<T> tokens[ · pages 1-<E> extracted] ## Outline (only when the outline is not empty) - <title> (p. <page>) (one line per item, indented two spaces per level below 1) This document is longer than one answer: ask for pages="a-b" or grep="pattern". <leading pages>Tis the rune count ofFull(doc)divided by 4.- The
pages 1-E extractedpart appears only when fewer pages were extracted thanPages. The separator is " · " (U+00B7). - Leading pages are whole pages rendered as in
Full, from page 1, as many as fit inOutCap/4runes. At least page 1 is shown, cut to fit if it alone is larger. - The whole content is at most
OutCaprunes; if the outline is too long, cut the outline, not the header or the instruction line. First = 1,Last= the last whole page shown.
- If the rune count of
- Pages.
- Parse after trimming spaces:
a-b,a,a-(a to the last extracted page). Anything else →ErrBadPages. a < 1,b < a, orapast the last extracted page → an error wrappingErrBadPageswhose text ispages out of range (1-E).bpast the end is clamped toE.- At most 100 pages and at most
OutCaprunes: whole pages froma. A single page larger thanOutCapis cut to fit. - When the range was cut,
Notesays so and names the rest aspages="k-b": for examplecut at page 13; ask for pages="14-40".
- Parse after trimming spaces:
- Grep.
- The pattern must be at most 200 bytes and compile with
regexp(RE2), else an error wrappingErrBadGrep. Prefix(?i)unless the pattern contains an upper-case letter ("smart case"). - Search every line of every page in the range (all extracted pages when
Pagesis empty). - The first line of the content is
grep "<pattern>": N matches(1 matchfor one;no matchesfor none). Add, showing the first 100when N is over 100. - Then, per match, up to the first 100: the previous line of the same page as
p. P- <line>, the matching line asp. P: <line>, the next line asp. P- <line>, then a--line. Lines are trimmed of surrounding spaces. GrepHits = N,First = Last = 0. The content is at mostOutCaprunes (drop whole match blocks at the end and say so inNote). No match is not an error.
- The pattern must be at most 200 bytes and compile with
- Every length here is in runes, never bytes: the cap protects an agent's context, and CJK text is three bytes a rune.
Steps
- 1.
git switch p5a; copy the given test. 2. See it fail (undefined names). - 3.
pdfdoc.go, 4.grep.go. - 5.
gofmt -w;go test -race -count=3 ./internal/pdfdoc/→ok. - 6.
make gate. 7. Rowp5a/05-pdfdoc; commit.
git add internal/pdfdoc docs/implementer-log.md
git commit -F .state/commit-msg.txt
Done when
- The given test passes;
make gateok; given file byte-identical.