From 6aea6a4e2cbdb5713b74e9fe63dc2a2c984e98a6 Mon Sep 17 00:00:00 2001 From: John O'Keefe Date: Sat, 26 Sep 2026 22:01:35 -0400 Subject: [PATCH] fix(sync): countTextCharsBefore multiplied the document by node depth MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit countTextCharsBefore started its sibling walk at the target node itself and recursed after counting the current node's subtree, so every ancestor level re-counted everything accumulated so far — a node at depth 4 in the single-document 1984 epub reported 2.38M chars before it in a 589k-char document, producing healed percentages of 4.04 (>1) and a 500 on the reading_progress percentage check constraint. Every healed progress save from a fresh client failed; only exact-context matches (no heal) stored. The walk now starts at target.PrevSibling: strictly the characters before the node, per the contract all three callers already assume (healed book offsets, CRE convert percentage, kepub offsets). Found by the real-phone validation pass: the phone's first healed submission 500ed where the emulator's had matched context exactly and never taken the heal path. --- internal/sync/cfi_converter.go | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/internal/sync/cfi_converter.go b/internal/sync/cfi_converter.go index 366784d..4a4c141 100644 --- a/internal/sync/cfi_converter.go +++ b/internal/sync/cfi_converter.go @@ -1321,9 +1321,16 @@ func countTextChars(n *html.Node) int { return count } +// countTextCharsBefore counts the text characters preceding target within +// its document: preceding siblings at each ancestor level, strictly +// EXCLUDING target's own subtree (callers add the in-node offset +// separately). Starting the walk at target itself — or recursing after +// counting the current node's subtree — would re-count the accumulated +// document once per ancestor level and multiply the result by the node's +// depth. func countTextCharsBefore(target *html.Node) int { count := 0 - for c := target; c != nil; c = c.PrevSibling { + for c := target.PrevSibling; c != nil; c = c.PrevSibling { count += countTextChars(c) } if target.Parent != nil {