Files
sift/docs/plans/p5a/07-api-cache-pages.md

90 lines
4.4 KiB
Markdown
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# P5a task 07: the PDF document cache, `pages`, `grep`, and the question limit
**Branch:** `p5a` (`git switch p5a`; `git status --short` must be empty, otherwise stop)
**Commit subject:** `api: PDF cache with revalidation; pages and grep; refuse questions on very long documents`
## Goal
Task 06 serves PDFs but fetches and extracts every time. After this task, a PDF already
extracted is answered from the cache: fresh for 24 h, then revalidated with a conditional GET.
An agent can ask for `pages` or `grep`. A question on a document too long for the digest is
refused with a pointer to those two.
## Files
- Copy: nothing (the given `internal/api/pdf_test.go` came in task 06)
- Modify: `internal/api/pdf.go` (all new logic), `internal/api/api.go` (only the call in rule 1
and the two request fields' use in rule 3), `docs/implementer-log.md`
## What already exists
- Task 06's `extractPDF`, `pdfError`, `renderPDF`, the `Server` fields (`PDFTTL`,
`PDFMaxQuestionChunks`), the `v1Req` fields (`Pages`, `Grep`), and `DefaultPDFTTL` /
`DefaultPDFMaxQuestionChunks`.
- `internal/dcache`:
```go
type PDFEntry struct{ ContentHash, ETag, LastModified string; FetchedAt time.Time; Meta []byte; Pages []string }
func PDFKey(url, callerClass string) string
func (d *DB) PutPDF(key string, e PDFEntry)
func (d *DB) GetPDF(key string) (PDFEntry, bool)
func (d *DB) TouchPDF(key string, fetchedAt time.Time)
```
A nil `*DB` is a no-op.
- `internal/fetch`: `func (f *Fetcher) FetchIf(ctx, rawURL, etag, lastModified string) (*Resource, error)`.
A 304 returns `Status 304`, an empty body and a nil error.
- `internal/digest`: `Split(text string, maxTok int) []string` and `ChunkTokens`.
## Rules
1. **Cache first, in `doFetch`**, before `acquire` and `s.Fetch.Fetch`: call
`s.pdfFromCache(ctx, c, rawURL) (res *fetch.Resource, doc *pdftext.Doc, ok bool, err error)`.
The key is `dcache.PDFKey(rawURL, s.callerClass(c))`; the cache is `s.cache()` (may be nil).
- **Miss:** `ok == false`; go on as today.
- **Fresh** (`time.Since(e.FetchedAt) < ttl`, where `ttl` is `s.PDFTTL` or `DefaultPDFTTL`):
build the doc from the entry (unmarshal `Meta` into `doc.Meta`; `doc.Text = e.Pages`), and a
synthetic `res`: `URL` and `FinalURL` = rawURL, `Status` 200,
`ContentType "application/pdf"`, `FetchedAt` = `e.FetchedAt`, `Hash` = `e.ContentHash`, no
body. No network call and no child.
- **Stale:** `s.Fetch.FetchIf(ctx, rawURL, e.ETag, e.LastModified)`.
- 304 → `TouchPDF(key, now)`, then answer as fresh.
- 200 → return that response with `ok == false`, so `doFetch` goes on without a second
GET: the new document is extracted and stored.
- An error → return it.
- The source in the `/v1/fetch` envelope says `"cached": true` for an answer from the cache
(fresh or after a 304).
2. **Store after extraction:** after `extractPDF` succeeds, `PutPDF` under the URL key with:
- `ContentHash: res.SHA256()`;
- `ETag` and `Last-Modified` from the response headers;
- `FetchedAt: now`;
- `Meta`: the JSON of `doc.Meta`;
- `Pages: doc.Text`.
3. **`pages` and `grep` (`/v1/fetch`):**
- Pass `req.Pages` and `req.Grep` into `pdfdoc.Options` in `renderPDF`.
- `pdfdoc.ErrBadPages` or `pdfdoc.ErrBadGrep` (use `errors.Is`) → 400, with the error's own
text as the message.
- `pages` or `grep` on a non-PDF → `400 pages and grep apply to PDFs only`.
4. **Question limit:** in mode `digest` with a doc, compute
`n := len(digest.Split(pdfdoc.Full(doc), digest.ChunkTokens))`. If `n` exceeds
`s.PDFMaxQuestionChunks` (or its default), answer
`422 document too long for question (<n> chunks); use grep or pages` **before** any digest
call.
## Steps
- [ ] **1.** `git switch p5a`. **2. See the three tests fail:**
`go test -count=1 -run 'TestPDFPagesAndGrep|TestPDFCache|TestPDFQuestionTooLong' ./internal/api/`.
- [ ] **3.** `pdfFromCache` and the store (rules 1–2), **4.** pages and grep, **5.** the question limit.
- [ ] **6.** `gofmt -w`; `go test -race -count=3 ./internal/api/` → `ok` (all ten PDF tests and
every existing test).
- [ ] **7.** `make gate` → `gate: ok`. **8.** Row `p5a/07-api-cache-pages`; commit.
```sh
git add internal/api docs/implementer-log.md
git commit -F .state/commit-msg.txt
```
## Done when
- `go test -race -count=3 ./internal/api/` is ok; `make gate` ok; the given file is still
byte-identical.