feat(pipeline): page-aware extraction — feed the current page into the revision prompt #130
Closed
mathias
wants to merge 3 commits from
feat/106-page-aware-extraction into main
pull from: feat/106-page-aware-extraction
merge into: :main
:main
:fix/pipeline-writes-158
:fix/edit-path-io-192
:fix/create-regular-188
:fix/guard-special-179
:fix/linkcheck-keeps-write-178
:fix/write-dir-redirect-173
:fix/tunnel-detect-174
:fix/pipeline-root-157
:w3/154-mcp-auth-wiring
:fix/tunnel-traversal
:test/in-brain-targets-171
:fix/brain-scan-root-157
:fix/graph-wiki-root-157
:fix/vectorstore-root-157
:fix/notes-root-157
:fix/search-root-157
:fix/brainwide-root-157
:fix/raw-symlinks-153
:feat/symlink-tripwire-157
:fix/rest-auth-149
:fix/123-brain-edit-envelope-check
:feat/106-page-aware-extraction
:fix/105-source-drift-on-main
:fix/104-surface-merge-conflicts-on-main
:agent/b1-compile-on-ingest
:agent/104-surface-merge-conflicts
:agent/105-source-drift
:agent/107-loud-unconfigured-alerting
:feat/brain-read-edit
:fix/capture-i1-refusal-names-target
:renovate/configure
:feat/consolidate-single-harness
:icebox/cmd-routing
:feat/brain-pending-promote
:fix/close-session-classification-refresh
:fix/capture-summarywriter
No Reviewers
Labels
Clear labels
agent/ready
blocked/client
blocked/supplier
blocked/upstream
risk/high
risk/low
risk/medium
size/l
size/m
size/s
size/xs
status:dispatch
status:dispatched
status:review
status:reviewing
type/chore
type/decision
type/docs
type/feat
type/fix
type/ops
type/research
type/test
Meets the agent-ready contract; authorizes unattended execution
Waiting on the client
Waiting on a supplier
Waiting on an upstream project or vendor
Irreversible, security-relevant, or control plane
Reversible, local, test-covered
Touches shared state, deploys or data
Too big - split it
Under two days
Under half a day
Under an hour
Maintenance, no behaviour change
Human decision required; never agent/ready
Documentation only
New capability
Corrects broken behaviour
Infrastructure, deploy, runtime
Investigate; output is a finding, not code
Test coverage only
No labels
Milestone
No items
No Milestone
Projects
Clear projects
No projects
No Assignees
Notifications
Due Date
No due date set.
Dependencies
No dependencies set.
Reference: mathias/hyperguild#130
Reference in New Issue
Block a user
Blocking a user prevents them from interacting with repositories, such as opening or commenting on pull requests or issues. Learn more about blocking a user.
What
Closes #106, the second half of compile-on-ingest. #103 (merged) made the merge asymmetric — on-disk wins, extraction-only sections get added, re-ingest of unchanged content is byte-identical. What it didn't do: the extraction itself was page-blind, writing every page as if fresh. This PR feeds the current page into the prompt and asks for a revision instead.
Two commits:
feat(pipeline)—BuildPromptnow takes the source page's current on-disk content (when one exists) and asks the model to revise it: add what's missing, strengthen what the source now supports, leave the rest alone and reuse existing wording verbatim so unchanged claims are recognized as unchanged. Scoped to the source page only —wiki/sources/<sourceSlug>.mdis the one page whose path is deterministic before extraction runs (a concept/entity page's path only exists once the model has produced a title, so it can't be fed forward the same way within the current chunked-extraction architecture).fix(wiki)— normalises heading casing/whitespace beforeMergedecides "is this the same section," closing the drift the issue names explicitly ("Key Claims" vs "Key claims" forking into two sections).Heading-normalization rule (exact)
normalizeHeading(h) = strings.ToLower(strings.TrimSpace(h)), used only as the comparison key inMerge's section-matching (idxmap) and in the bullet/append strategy lookup (bulletSectionsNorm/appendSectionsNorm, derived from the existing maps via the same function). The on-disk section's original heading text (secsA[i].heading) is never touched — it's what gets written back out. So a human's heading casing is never silently rewritten by a merge; normalization only decides sameness, never display text.I deliberately did not attempt pluralization stemming ("Claims" vs "Claim") — the issue's own motivating example ("Key Claims" vs "Key claims") is a pure casing difference, not a plural/singular one, and naive stemming (e.g. strip trailing "s") is fragile against real headings like "Related Entities" → "Related Entitie". Flagging as a judgment call: if plural drift turns out to be a real observed failure mode, it needs its own targeted rule, not a blanket stemmer.
wiki/merge.go — why I touched a file under today's review scrutiny
Per the task brief,
wiki/merge.gohad six real bugs found across five review rounds earlier today and was flagged as "don't touch unless you find an actual bug or the issue genuinely needs it, and explain why." Heading normalization is #106's own explicit checklist item ("normalise headings before matching") and there is nowhere else to hook it in — it has to live inMerge's section-matching logic. I kept the diff to exactly the two lookup sites (idxconstruction/lookup, and the bullet/append strategy switch) plus the newnormalizeHeading/normalizeSethelpers. No other behavior in the file changed; all 17 pre-existingmerge_test.gotests pass unmodified.Deterministic merge stays the backstop
Unchanged from #103:
wiki.Mergestill keeps on-disk content for any section the model doesn't return, and non-bullet/non-append sections still keep the on-disk version even when the model does return a same-named one. So a model that ignores the "revise" instruction (or hallucinates a rewrite) still cannot destroy human content — page-aware extraction changes what the model is asked to do, not what the merge is permitted to do.Byte-identical-on-unchanged-revision test
TestRun_RevisionEchoingExistingPageStaysByteIdentical(pipeline_test.go) — the issue's explicit ask: "a model given the current page will sometimes echo it back verbatim" must not duplicate or perturb the page. It seeds an on-disk source page, runsRun()with different (hash-changing) source content but aCompletestub that echoes the seeded page's sections back unchanged, and asserts the resulting file via exactassert.Equalagainst the expected byte content (including the generator-ownedlast_updated/source_hashfrontmatter fields, which are expected to update). Passes.Two companion tests, also exact/targeted rather than
Contains-only where it matters:TestRun_ExistingSourcePageIsIncludedInTheExtractionPrompt— asserts a phrase that exists only on the on-disk page (not in the new source content) reaches the prompt, proving the page is actually threaded through rather than the assertion being trivially satisfiable.TestRun_NewSourceHasNoExistingPageInThePrompt— a source with no page on disk yet gets no revision section injected at all.TestMerge_HeadingCaseDriftMatchesSameSection/TestMerge_HeadingWhitespaceDriftMatchesSameSection(merge_test.go) — exact section-count assertions (assert.Equal(t, 1, strings.Count(...))) plus explicit checks that the on-disk casing survives and the fresh extraction's casing does not leak into the output.Judgment calls / flags for review
Mergealready keeps on-disk sections the fresh extraction doesn't return. This should reduce prompt/response size on a re-ingest (partially answering the issue's cost concern) but I haven't measured actual token savings against a real model.wiki/merge.gowas edited — see the section above for exactly why and how narrowly.Verification
cd ingestion && go build ./... && go test ./...— all packages pass, including the two new/changed test files.task checkfrom repo root (lint + test -race + vet + govulncheck + build + gitleaks, both modules) — exit 0, zero FAIL, verified via explicit$?andgrep -c FAIL, not colored output.Not merging this myself — per the task brief, an independent adversarial review happens first.
🤖 Generated with Claude Code
Created via git-mcp on behalf of @mathias
Superseded by #132: same 3 commits, rebased onto current main so the collision fix lands in
wiki.MergeReport(the function production actually calls after #104's split) instead of the now-vestigialMergewrapper this branch predates.Created via git-mcp on behalf of @mathias
Pull request closed