P5a plan: PDF documents — seven Ornith tasks with acceptance tests first

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>
This commit is contained in:
2026-09-25 20:47:38 -07:00
co-authored by Claude Opus 5.5
parent 3f2b78d4aa
commit cfde35397a
115 changed files with 26422 additions and 43 deletions
@@ -0,0 +1,44 @@
package main
// P5a task 07: `sift fetch --pages` and `--grep`.
import (
"encoding/json"
"strings"
"testing"
)
func TestParseFetchOptsPDF(t *testing.T) {
o, err := parseFetchOpts([]string{"https://example.org/t.pdf", "--pages", "41-60", "--grep", "attention head"})
if err != nil {
t.Fatal(err)
}
if o.URL != "https://example.org/t.pdf" || o.Pages != "41-60" || o.Grep != "attention head" {
t.Errorf("got %+v", o)
}
o, err = parseFetchOpts([]string{"--grep", "x", "--question", "why", "https://example.org/t.pdf"})
if err != nil || o.URL != "https://example.org/t.pdf" || o.Grep != "x" || o.Question != "why" {
t.Errorf("flags before the URL: %+v %v", o, err)
}
if !strings.Contains(fetchUsage, "--pages") || !strings.Contains(fetchUsage, "--grep") {
t.Errorf("usage lacks the new flags: %s", fetchUsage)
}
}
func TestFetchBodyPDFFields(t *testing.T) {
b, _ := json.Marshal(fetchBody(fetchOpts{URL: "https://example.org/a.pdf"}))
if strings.Contains(string(b), `"pages"`) || strings.Contains(string(b), `"grep"`) {
t.Errorf("unset pages/grep must not be sent: %s", b)
}
b, _ = json.Marshal(fetchBody(fetchOpts{URL: "https://example.org/a.pdf", Pages: "2-3", Grep: "fox"}))
var m map[string]any
_ = json.Unmarshal(b, &m)
if m["pages"] != "2-3" || m["grep"] != "fox" || m["url"] != "https://example.org/a.pdf" || m["mode"] != "clean" {
t.Errorf("body = %s", b)
}
b, _ = json.Marshal(fetchBody(fetchOpts{URL: "u", Question: "q"}))
_ = json.Unmarshal(b, &m)
if m["mode"] != "digest" || m["question"] != "q" || m["context"] != true {
t.Errorf("question body = %s (mode must default to digest, context on)", b)
}
}