Skip to content

fix(notify): stop cutting "what changed" mid-word - #552

Merged
Smana merged 1 commit into
mainfrom
fix/what-changed-word-boundary
Aug 24, 2026
Merged

fix(notify): stop cutting "what changed" mid-word#552
Smana merged 1 commit into
mainfrom
fix/what-changed-word-boundary

Conversation

@Smana

@Smana Smana commented Aug 24, 2026

Copy link
Copy Markdown
Owner

The What changed metadata field was capped at 200 runes with the generic truncate helper, so a live card ended:

…consistent with Coder provisioning workspaces outside Argo/Fl…

Two things were wrong at once.

The cut landed inside a word. That reads as a broken renderer rather than an intentional elision.

200 was tighter than what models actually put in change_ref. Some cards carry a revision range; others carry a sentence explaining the change — and the explanatory form is the more useful of the two.

The fix

truncateWords backs the cut off to the last space, and the field rises to 600 runes — enough to hold an explanatory change_ref, still leaving the two-column metadata grid readable, and well inside the 2000-rune per-field cap add() already applies.

The cap stays enforced. The back-off only searches the final half of the allowance: a tail with no space in it is one long token — a sha, a URL — and that still gets the hard cut rather than losing half its value.

truncate is untouched and still the right helper for single-token values and hard protocol limits; the doc comment on truncateWords says which to reach for.

Guard

TestSlackWhatChangedTruncatesOnWordBoundary feeds the live over-long change_ref and asserts both halves: the rendered tail before the ellipsis is a whole word of the source, and the output carries more than the old 200-rune cap allowed.

Fixes F7.

@Smana
Smana force-pushed the fix/what-changed-word-boundary branch from a59d8c8 to 1757ca8 Compare August 24, 2026 13:28
The field was capped at 200 runes with the generic truncate helper, so a
live card ended "…consistent with Coder provisioning workspaces outside
Argo/Fl…". That reads as a broken renderer rather than an intentional
elision, and 200 was tighter than what models actually put in change_ref:
some cards carry a revision range, others a sentence explaining the
change, and the explanatory form is the more useful one.

truncateWords backs the cut off to the last space, and the field rises to
600 runes. The cap stays enforced — a tail with no space in its final
half is one long token (a sha, a URL) and still gets the hard cut rather
than losing half its value.

The scan starts at the FIRST DROPPED rune, not the last kept one, which
is what makes the word-boundary case ordinary rather than special: when
the cap lands exactly on a space the prefix already ends on a boundary,
and starting there finds it instead of scanning past a whole word that
fitted. The live fixture is that case — at 600 it cuts right after "the",
and the first attempt threw that word away.

The value is capped BEFORE escaping, reversing this file's other two
truncate sites deliberately. Those cap at 2900, a last-resort guard where
it does not matter that "&" costs 5 of the budget. This cap fires
routinely, so measuring escaped runes would hand a change_ref describing
"1.14.2 -> 1.15.0 && <prod>" a materially smaller allowance than one
without meta characters — and capping the source also means truncateWords
only ever sees real prose, so no cut can sever an entity.

n <= 1 is guarded: no caller passes it today, but the doc invites reuse
and a caller deriving n from a remaining budget reaches 0 by subtraction,
where r[:n-1] panicked. A panic in the notifier takes down the card for
an incident already in progress.

The guard is measured, not asserted: TestTruncateWords is the unit table
covering the hard cut, the exact boundary and the degenerate caps, and
the card-level test now measures the rendered FIELD. An earlier version
asserted len(blocksText(...)) > 260, which the header, verdict and footer
satisfy on their own — it passed with the cap set to 60, so the half of
the guard meant to pin the raised cap pinned nothing. Mutation-tested
against caps of 60 and 200 and against reverting to plain truncate.

internal/thread.truncateWords is the byte-budget twin of this function;
both doc comments now say so.
@Smana
Smana force-pushed the fix/what-changed-word-boundary branch from 21bb70f to 4bc2bb6 Compare August 24, 2026 16:36
@Smana
Smana merged commit 621fe65 into main Aug 24, 2026
3 checks passed
@Smana
Smana deleted the fix/what-changed-word-boundary branch August 24, 2026 16:40
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant