feat(pipeline): feed the current source page into the extraction prompt (#106) #132

Merged
mathias merged 4 commits from feat/106-page-aware-extraction-v2 into main 2026-09-23 05:45:47 +00:00
2 changed files with 38 additions and 1 deletions
Showing only changes of commit 921bea8738 - Show all commits
+12 -1
View File
@@ -64,9 +64,20 @@ func MergeReport(a, b Page) (Page, []string) {
fmB, _, secsB := parseSections(b.Content)
var conflicts []string
// First occurrence wins on a collision — a page can already carry two
// on-disk sections that only differ by case or whitespace (the exact
// corpus drift this normalization exists to catch going forward, found
// pre-existing in review of PR #130): a plain last-write assignment
// here would silently and deterministically route every future update
// to whichever duplicate happens to sort last in the file, freezing
// the other one forever with nothing to say so.
idx := make(map[string]int, len(secsA))
for i, s := range secsA {
idx[normalizeHeading(s.heading)] = i
key := normalizeHeading(s.heading)
if _, exists := idx[key]; !exists {
idx[key] = i
}
}
for _, sB := range secsB {
+26
View File
@@ -341,6 +341,32 @@ func TestMerge_HeadingWhitespaceDriftMatchesSameSection(t *testing.T) {
assert.Contains(t, got, "What about remote teams?")
}
func TestMerge_PreExistingCaseDuplicateHeadingsRouteToTheFirstOccurrence(t *testing.T) {
// Found in review of PR #130: normalizeHeading's idx map is built by
// iterating secsA with idx[normalizeHeading(s.heading)] = i -- last
// write wins. Before heading normalization existed, two on-disk
// sections differing only by case (real, pre-existing corpus drift --
// the exact pattern #106 itself cites) never collided, since idx keyed
// on the exact string. After normalization they collide, and every
// fresh update silently, deterministically routes to whichever
// duplicate happens to appear LAST in the file -- the first one is
// frozen forever, never receiving updates again, with nothing to say
// so. A Contains-only check doesn't catch this: nothing is literally
// deleted, a bullet just lands under the wrong of two still-present
// headings. Assert it lands under the FIRST occurrence instead —
// deterministic and matches "earliest is canonical," same reasoning
// as every other on-disk-wins default in this file.
a := Page{Path: "p.md", Content: "---\ntitle: X\n---\n\n## Key Claims\n\n- claim one\n\n## KEY CLAIMS\n\n- claim two\n"}
b := Page{Path: "p.md", Content: "---\ntitle: X\n---\n\n## Key Claims\n\n- claim one\n- fresh claim\n"}
got := Merge(a, b).Content
assert.Contains(t, got, "## Key Claims\n\n- claim one\n- fresh claim\n",
"the fresh bullet must land in the first-occurrence section, not silently route to the last duplicate")
assert.Contains(t, got, "## KEY CLAIMS\n\n- claim two\n",
"the second duplicate is untouched by this update, not merged into or overwritten")
}
func TestRefreshOwned_CommentLinesSurviveUnmangled(t *testing.T) {
// Third review round on PR #126: parseFrontmatterBlock dropped the
// sibling api package's comment guard (strings.HasPrefix(trimmed, "#")).