Skip to content

Commit 860248c

Browse files
jkyberneeesclaude
andcommitted
fix(memory): harden buffer excerpt against early-terminator collapse
Adversarial review (AI Verification Protocol) found two defects in summarizeForBuffer: - D-01 (critical): when the only sentence terminator in the window was an early abbreviation/version/domain ("e.g.", "v1.2", "node.js"), the excerpt collapsed to a few runes (e.g. "e.g.…") — destroying the summary. Now a sentence cut is only preferred when it lands at least halfway through the window; otherwise fall back to the word boundary near the cap. - D-02 (minor): an unclosed code fence (unmatched by the fenced-block regex) left a stray backtick in the output. Residual backticks are now stripped after inline-code unwrapping. Adds regression tests for both. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
1 parent 8afd32e commit 860248c

2 files changed

Lines changed: 40 additions & 3 deletions

File tree

internal/memory/summarize.go

Lines changed: 12 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -48,8 +48,10 @@ func summarizeForBuffer(text string) string {
4848
hadCode := reCodeFence.MatchString(text)
4949
text = reCodeFence.ReplaceAllString(text, " ")
5050

51-
// 2. Inline code -> inner text.
51+
// 2. Inline code -> inner text, then drop any residual backticks (e.g. from
52+
// an unclosed fence that step 1 could not match).
5253
text = reInlineCode.ReplaceAllString(text, "$1")
54+
text = strings.ReplaceAll(text, "`", "")
5355

5456
// 3. Images/links -> their visible text.
5557
text = reMdImage.ReplaceAllString(text, "$1")
@@ -84,6 +86,11 @@ func summarizeForBuffer(text string) string {
8486
// cuts at the last sentence terminator (.!?) before the cap, else the last
8587
// space, else hard-cuts at the cap — always on a rune boundary — and appends an
8688
// ellipsis. Never splits a multibyte rune.
89+
//
90+
// A sentence cut is only preferred when it lands at least halfway through the
91+
// window. Otherwise a single early terminator (an abbreviation like "e.g.", a
92+
// version "v1.2", or a domain "node.js") would collapse the summary to a few
93+
// runes; in that case we fall back to the word boundary near the cap instead.
8794
func excerptRunes(s string, max int) string {
8895
if utf8.RuneCountInString(s) <= max {
8996
return s
@@ -97,8 +104,10 @@ func excerptRunes(s string, max int) string {
97104
cut = i + 1 // include the terminator
98105
}
99106
}
100-
if cut <= 0 {
101-
// No sentence end; fall back to the last word boundary.
107+
if cut < max/2 {
108+
// No (or only an early) sentence end; fall back to the last word
109+
// boundary so we keep as much content as possible.
110+
cut = -1
102111
for i := len(window) - 1; i >= 0; i-- {
103112
if window[i] == ' ' {
104113
cut = i

internal/memory/summarize_test.go

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -94,6 +94,34 @@ func TestSummarizeForBuffer_HardCutSingleLongToken(t *testing.T) {
9494
}
9595
}
9696

97+
// TestSummarizeForBuffer_EarlyAbbreviationNoCollapse guards against an early,
98+
// lone sentence terminator (abbreviation / version / domain) collapsing the
99+
// excerpt to a few runes. The cut must fall back to the word boundary near the
100+
// cap and retain most of the content.
101+
func TestSummarizeForBuffer_EarlyAbbreviationNoCollapse(t *testing.T) {
102+
cases := []string{
103+
"e.g., " + strings.Repeat("we should refactor the module ", 20),
104+
"node.js " + strings.Repeat("is a runtime for building scalable apps ", 20),
105+
"v1.2 " + strings.Repeat("introduces many features and improvements ", 20),
106+
}
107+
for _, in := range cases {
108+
got := summarizeForBuffer(in)
109+
if n := utf8.RuneCountInString(got); n < maxBufferSummaryRunes/2 {
110+
t.Errorf("early-abbreviation excerpt collapsed to %d runes: %q", n, got)
111+
}
112+
}
113+
}
114+
115+
// TestSummarizeForBuffer_UnclosedFence ensures an unclosed code fence leaves no
116+
// stray backticks in the summary.
117+
func TestSummarizeForBuffer_UnclosedFence(t *testing.T) {
118+
in := "Here is some code:\n```go\nfunc main() { panic() }\nand more text after"
119+
got := summarizeForBuffer(in)
120+
if strings.Contains(got, "`") {
121+
t.Errorf("summary contains a leftover backtick: %q", got)
122+
}
123+
}
124+
97125
func TestSummarizeForBuffer_MultibyteBoundarySafe(t *testing.T) {
98126
// A long run of 3-byte runes with no spaces/sentence ends: must hard-cut on a
99127
// rune boundary, never splitting a rune.

0 commit comments

Comments
 (0)