Co-authored-by: Mathias <mthbqv@gmail.com>
This commit was merged in pull request #132.
This commit is contained in:
@@ -64,23 +64,50 @@ func MergeReport(a, b Page) (Page, []string) {
|
||||
fmB, _, secsB := parseSections(b.Content)
|
||||
|
||||
var conflicts []string
|
||||
idx := make(map[string]int, len(secsA))
|
||||
|
||||
// Sections in secsA sharing a normalized heading (pre-existing corpus
|
||||
// drift — "## Notes" and "## NOTES" both already on disk, the exact
|
||||
// pattern this normalization exists to catch going forward, found in
|
||||
// review of PR #130/#132) are matched to b's occurrences of that same
|
||||
// heading POSITIONALLY: b's 1st occurrence of the key pairs with a's
|
||||
// 1st, b's 2nd with a's 2nd, and so on. A single "last write wins" (or
|
||||
// even "first write wins") map from key to one index would instead
|
||||
// route every one of b's occurrences to the SAME on-disk section —
|
||||
// which either freezes every duplicate but one (never receiving
|
||||
// updates again) or, worse, compares mismatched pairs and reports a
|
||||
// disagreement between two sections that were never supposed to be
|
||||
// compared, false-positiving on the plain self-merge case a revision
|
||||
// pass produces when the model echoes a page back unchanged (found in
|
||||
// review of PR #132).
|
||||
byKey := make(map[string][]int, len(secsA))
|
||||
for i, s := range secsA {
|
||||
idx[s.heading] = i
|
||||
key := normalizeHeading(s.heading)
|
||||
byKey[key] = append(byKey[key], i)
|
||||
}
|
||||
cursor := make(map[string]int, len(byKey))
|
||||
|
||||
for _, sB := range secsB {
|
||||
i, exists := idx[sB.heading]
|
||||
if !exists {
|
||||
idx[sB.heading] = len(secsA)
|
||||
key := normalizeHeading(sB.heading)
|
||||
indices := byKey[key]
|
||||
pos := cursor[key]
|
||||
cursor[key] = pos + 1
|
||||
|
||||
if pos >= len(indices) {
|
||||
// b has more occurrences of this heading than a does (including
|
||||
// the case where a has none at all) — the extra is new content,
|
||||
// appended as its own section and registered so a THIRD
|
||||
// occurrence of the same heading in b pairs with this one
|
||||
// rather than appending yet another duplicate.
|
||||
byKey[key] = append(byKey[key], len(secsA))
|
||||
secsA = append(secsA, sB)
|
||||
continue
|
||||
}
|
||||
i := indices[pos]
|
||||
sA := secsA[i]
|
||||
switch {
|
||||
case bulletSections[sB.heading]:
|
||||
case bulletSectionsNorm[key]:
|
||||
secsA[i].content = mergeBullets(sA.content, sB.content)
|
||||
case appendSections[sB.heading]:
|
||||
case appendSectionsNorm[key]:
|
||||
secsA[i].content = appendNew(sA.content, sB.content)
|
||||
default:
|
||||
// a wins — but say that b disagreed, rather than dropping it mute.
|
||||
@@ -93,6 +120,34 @@ func MergeReport(a, b Page) (Page, []string) {
|
||||
return Page{Path: a.Path, Content: rebuildContent(refreshOwned(fmA, fmB), preambleA, secsA)}, conflicts
|
||||
}
|
||||
|
||||
// normalizeHeading returns the key Merge uses to decide "is this the same
|
||||
// section" — trimmed and case-folded, so "Key Claims" and "Key claims" match
|
||||
// (hyperguild#106: heading casing drift must not fork a section). It is used
|
||||
// only for that comparison; the on-disk section's original heading text
|
||||
// (secsA[i].heading, untouched here) is what actually gets written out, so a
|
||||
// human's heading casing is never silently rewritten by a merge.
|
||||
func normalizeHeading(h string) string {
|
||||
return strings.ToLower(strings.TrimSpace(h))
|
||||
}
|
||||
|
||||
// bulletSectionsNorm and appendSectionsNorm are bulletSections/appendSections
|
||||
// keyed by normalizeHeading instead of exact text, so a section's merge
|
||||
// strategy (union bullets, append) is chosen by the same normalized identity
|
||||
// Merge uses to match it — otherwise a heading that matched an on-disk
|
||||
// section (case-insensitively) could still fail the exact-string strategy
|
||||
// lookup and silently fall through to "keep a's version," dropping b's
|
||||
// content instead of unioning or appending it.
|
||||
var bulletSectionsNorm = normalizeSet(bulletSections)
|
||||
var appendSectionsNorm = normalizeSet(appendSections)
|
||||
|
||||
func normalizeSet(names map[string]bool) map[string]bool {
|
||||
out := make(map[string]bool, len(names))
|
||||
for k := range names {
|
||||
out[normalizeHeading(k)] = true
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
// generatorOwned names the frontmatter fields the ingest pipeline may refresh
|
||||
// on a page it is revising. Everything else on disk wins.
|
||||
//
|
||||
|
||||
Reference in New Issue
Block a user