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>
3.4 KiB
3.4 KiB
P5a task 03: a document cache for PDFs
Branch: p5a (git switch p5a; git status --short must be empty, otherwise stop)
Commit subject: dcache: PDF document cache (per URL and caller class, whole-document eviction)
Goal
Extracting a thesis takes a second; downloading it can take a minute. Once a document is extracted, later requests for other pages, a grep, or a question must not fetch or extract it again. This task adds two tables to the existing SQLite cache and four functions. Freshness (the 24 h TTL and revalidation) is decided by the caller in task 06, not here.
Files
- Copy:
internal/dcache/pdf_test.go - Create:
internal/dcache/pdf.go - Modify:
internal/dcache/dcache.go(the twoCREATE TABLEs,Size,evict),docs/implementer-log.md
Interfaces
package dcache
// PDFEntry is one cached PDF: how to revalidate it, its metadata (the pdftext.Meta JSON, opaque
// here) and the text of each extracted page (Pages[i] is page i+1).
type PDFEntry struct {
ContentHash string
ETag string
LastModified string
FetchedAt time.Time
Meta []byte
Pages []string
}
// PDFKey is the cache key for a document: a hash of the URL and the caller class, like the
// other keys here (the URL never appears in the clear).
func PDFKey(url, callerClass string) string
func (d *DB) PutPDF(key string, e PDFEntry) // replaces any entry under key, pages included
func (d *DB) GetPDF(key string) (PDFEntry, bool)
func (d *DB) TouchPDF(key string, fetchedAt time.Time) // after a 304: only FetchedAt moves; a miss is a no-op
Rules
- Tables:
pdf_doc(k TEXT PRIMARY KEY, content_hash, etag, last_modified, fetched_at INTEGER, meta BLOB, last_hit INTEGER, nbytes INTEGER)pdf_page(k TEXT, page INTEGER, text TEXT, PRIMARY KEY (k, page))nbytesis the document's size (meta plus every page's text) and counts towardSize().
PutPDFwrites the document and its pages in one transaction, deleting the old pages first, then callsevict.GetPDFupdateslast_hit, and returns the pages in page order; an empty page keeps its place.evicttreats a PDF document like any other entry, bylast_hit, and removes it whole: itspdf_docrow and all itspdf_pagerows together. A cache never holds half a document.- Hit and miss counters: add
PDFH,PDFMbeside the others (the metrics task reads them later). - A nil
*DBis a no-op everywhere, as for the existing methods; a SQL error is logged atWarn(never with the key's text or page text) and behaves like a miss.
Facts
DigestKey,EmbedKeyandSearchKeyshow how keys are hashed (sha256of the parts joined with a separator). Follow the same pattern.- The existing
evictpicks the oldestlast_hitacross the three existing tables; extend that union withpdf_doc.
Steps
- 1.
git switch p5a; copy the given test. 2. See it fail (undefined names). - 3.
dcache.goschema;pdf.go;Sizeandevict. - 4.
gofmt -w;go test -race -count=3 ./internal/dcache/→ok(the existing dcache tests too). - 5.
make gate. 6. Rowp5a/03-dcache; commit.
git add internal/dcache docs/implementer-log.md
git commit -F .state/commit-msg.txt
Done when
- The given and existing dcache tests pass under
-race -count=3;make gateok; given file byte-identical.