fix(agent): gadfly round 4 — the answer can come BEFORE the pointer
CI / Tidy (pull_request) Successful in 9m22s
CI / Build & Test (pull_request) Successful in 9m46s

My round-3 fix has the same defect it fixed, mirrored. bareAbovePointer
cut back to the previous FULL STOP, not the previous clause, so anything
sharing the reference's sentence ahead of it was swallowed:

    "Ship Tuesday, as shown above."                  bare=true
    "OK. The verdict is guilty, as detailed above."  bare=true
    "The answer is sixty minutes, as computed above." bare=true

All three read as pure pointers and were replaced by an earlier turn.
The comment called it a clause cut; the code cut a sentence. It survived
a round because every case I had written put the answer AFTER the
pointer — I fixed the direction I had thought of and tested only that.
clauseBoundaryChars now includes , ; : and the em dash.

Three more, all real:

- pointsAbove and bareAbovePointer each did their own FindStringIndex
  plus offset check, so the two could disagree about what a pointer is.
  One aboveRefLoc now answers that.
- fillerWords was extracted to stop drift while its separator class was
  still copy-pasted beside it — the same defect one token over. fillerSep
  shares it too.
- A missing blank comment line ran two paragraphs together.

The padded 96-byte "bare pointer" fixture is gone rather than bent a
third time. Its premise does not survive the rule: a genuinely bare
pointer is its own clause and nothing else, so it is SHORT, and the two
recovery bars cannot disagree about a short terminal. I had twice
reshaped that string to keep a case alive whose scenario the code no
longer admits. The modeBackRef path stays covered by the bare-pointer
cases in TestIsWeakFinal and the "(Already answered above.)" end-to-end.

Break-check: fifteen mutations including a regression to the sentence
cut, each killed by a named test; the harness now also fails a mutation
that only breaks the build.
This commit is contained in:
2026-08-22 00:28:53 -04:00
parent 1756910ef0
commit 184627c570
2 changed files with 48 additions and 32 deletions
+30 -8
View File
@@ -153,9 +153,17 @@ var aboveRefRe = regexp.MustCompile(`(?i)\babove\b[ \t]*([.,;:!?)\]"'’”—]|
// only safe under isWeakFinal's 120-byte cap. Folding the two into one
// predicate for tidiness would hand isSummaryCloser an unanchored match across
// 300 bytes — widening the gate, not deduplicating it.
func pointsAbove(t string) bool {
func pointsAbove(t string) bool { return aboveRefLoc(t) != nil }
// aboveRefLoc returns the span of the qualifying deictic reference, or nil.
// One place performs the match and the offset test, so pointsAbove and
// bareAbovePointer cannot disagree about what counts as a pointer.
func aboveRefLoc(t string) []int {
loc := aboveRefRe.FindStringIndex(t)
return loc != nil && loc[0] <= backRefHeadChars
if loc == nil || loc[0] > backRefHeadChars {
return nil
}
return loc
}
// bareAbovePointer reports whether the terminal is a deictic back-reference and
@@ -176,18 +184,28 @@ func pointsAbove(t string) bool {
// bare, and a comparative "above," with an interjection after it keeps the rest
// of its own sentence for the same reason.
func bareAbovePointer(t string) bool {
loc := aboveRefRe.FindStringIndex(t)
if loc == nil || loc[0] > backRefHeadChars {
loc := aboveRefLoc(t)
if loc == nil {
return false
}
start := 0
if k := strings.LastIndexAny(t[:loc[0]], ".!?\n"); k >= 0 {
if k := strings.LastIndexAny(t[:loc[0]], clauseBoundaryChars); k >= 0 {
start = k + 1
}
rest := strings.TrimSpace(t[:start]) + " " + strings.TrimSpace(t[loc[1]:])
return bareRemainderRe.MatchString(strings.Trim(rest, pointerResidueCutset))
}
// clauseBoundaryChars ends the clause the reference belongs to. Commas,
// semicolons, colons and dashes are in it, not just sentence terminators,
// because the answer can share the reference's SENTENCE: "Ship Tuesday, as
// shown above." is a decision plus a pointer, and cutting back only to the
// previous full stop swallowed the decision and made the whole thing look
// bare. That is the same defect as treating a pointer as disposable because
// something followed it — mirrored, and it survived a round of review because
// every case I had written put the answer AFTER the pointer.
const clauseBoundaryChars = ".!?\n,;:—"
// pointerResidueCutset is trimmed from both ends of what survives cutting the
// pointer's clause — the brackets, quotes, and punctuation a model wraps a
// back-reference in ("(Already answered above.)").
@@ -196,7 +214,7 @@ const pointerResidueCutset = " \t\r\n.,;:!?()[]{}\"'“”‘’*_-—"
// bareRemainderRe matches a remainder that carries no answer: empty, or only
// the filler a closer opens with. Shares fillerWords with summaryPreface so the
// two lists cannot drift.
var bareRemainderRe = regexp.MustCompile(`(?i)^(` + fillerWords + `[\s,.!:—-]*)*$`)
var bareRemainderRe = regexp.MustCompile(`(?i)^(` + fillerWords + fillerSep + `*)*$`)
// compressionMarkerRe matches a model announcing that what follows is the
// short form of something longer ("Short version: …", "TL;DR: …", "In short,
@@ -213,6 +231,7 @@ var bareRemainderRe = regexp.MustCompile(`(?i)^(` + fillerWords + `[\s,.!:—-]*
// A marker WITHOUT a pointer stays out of scope, as summaryCloserRe's own
// comment explains: a user who asked for brevity is answered with exactly that
// shape.
//
// The marker must OPEN a sentence. Mid-sentence the same words are ordinary
// prose carrying new content — "Given the analysis above, the bottom line is
// that we need a different vendor" states a conclusion, it does not announce a
@@ -238,8 +257,11 @@ const (
// fillerWords are the throat-clearing tokens a closer opens with. Shared
// with bareRemainderRe, which has to recognise exactly the same set as
// "not answer content".
fillerWords = `(done|all set|ok(ay)?)`
summaryPreface = `(` + fillerWords + `[\s,.!:—-]+)?` // optional "Done —" style opener
fillerWords = `(done|all set|ok(ay)?)`
// fillerSep is the punctuation a filler word trails. Shared for the same
// reason fillerWords is: two copies of one separator class drift.
fillerSep = `[\s,.!:—-]`
summaryPreface = `(` + fillerWords + fillerSep + `+)?` // optional "Done —" style opener
summaryNouns = `(citations?|sources?|references?|claims?)`
// "all" appears here AND in summaryArticle on purpose: as a quantifier
// between noun and verb ("Citations all logged.") and as a determiner