fix(release): act on the fourth Codex pass

- wrapText left a word longer than the line budget intact, so a valid
  60-character unbroken highlight rendered past the edge of the 1200px card
  and was cropped. Overlong words are now broken at the budget, and a test
  asserts no returned line can exceed it. The previous test asserting the
  old behavior encoded the bug.
- The announcement commands are documented as `pnpm --silent run`:
  reproduced that plain `pnpm run release:notes:telegram > post` writes
  pnpm's lifecycle banner into the file ahead of the post, so the
  advertised paste-ready output was not paste-ready.

186 tests passing.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
4grayandClaude Fable 5 committed 2026-08-27 10:11:37 +02:00
1 parent 8c3aebecac
commit 62359c80fc
8 files changed
+60 -11

No files matched your search

+21 -2
View File
@@ -61,8 +61,13 @@ export function escapeXml(text) {
/**
* Greedy word wrap by character budget. SVG has no automatic text layout and
* font metrics vary by host, so the budget is conservative; a single word
* longer than the budget gets its own line rather than being cut.
* font metrics vary by host, so the budget is conservative.
*
* Every returned line is at most `maxChars` long, including when a single
* word exceeds the budget: such a word is broken at the budget rather than
* left whole. Leaving it whole is what a naive wrap does, and a 60-character
* unbroken headline — well inside the validated limit — then rendered past
* the edge of the 1200px card and was cropped.
*
* @param {string} text
* @param {number} maxChars
@@ -75,6 +80,20 @@ export function wrapText(text, maxChars, maxLines) {
let current = '';
for (const word of words) {
if (word.length > maxChars) {
if (current) {
lines.push(current);
}
for (let start = 0; start < word.length; start += maxChars) {
lines.push(word.slice(start, start + maxChars));
}
// Keep the final chunk open so a following short word can join it.
current = lines.pop() ?? '';
continue;
}
const candidate = current ? `${current} ${word}` : word;
if (candidate.length <= maxChars || !current) {
+22 -2
View File
@@ -78,13 +78,33 @@ describe('wrapText', () => {
]);
});
it('gives an overlong single word its own line instead of cutting it', () => {
it('breaks a word longer than the budget instead of overflowing the card', () => {
assert.deepEqual(wrapText('supercalifragilistic ok', 10, 3), [
'supercalifragilistic',
'supercalif',
'ragilistic',
'ok',
]);
});
it('never returns a line wider than the budget', () => {
// A 60-character unbroken highlight is valid input; on a 34-character
// feature-card budget it must still fit inside the canvas.
for (const [text, maxChars, maxLines] of [
['W'.repeat(60), 34, 2],
['W'.repeat(60), 30, 2],
['W'.repeat(60), 60, 1],
['a'.repeat(200), 34, 2],
['short words only here', 34, 2],
]) {
for (const line of wrapText(text, maxChars, maxLines)) {
assert.ok(
line.length <= maxChars,
`"${line}" is ${line.length} > ${maxChars}`
);
}
}
});
it('ellipsizes the last kept line on overflow', () => {
const lines = wrapText('aaa bbb ccc ddd eee', 3, 2);