From e835d491d161a12ce986dd0f87835991bb0afc78 Mon Sep 17 00:00:00 2001 From: Claude Date: Tue, 13 Oct 2026 00:15:00 +0000 Subject: [PATCH 1/2] fix(scripts): the changeset digest reads a BREAKING annotation behind a pictograph MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit objectui's changeset authors decorate the annotation with a warning sign (`## ⚠️ BREAKING, priced as minor ...`), and the block-opener rule admitted nothing between the heading or bullet mark and the word, so the digest counted such an entry as not breaking. A decoration is now admitted: a run of pictographic symbols, each with its optional variation selector, on either side of the emphasis mark. The rule stays anchored at the line start and admits no letter, digit or ASCII punctuation. The summary-label test that keeps an author's own label from being restated admits the same decoration, so a summary opening with `⚠️ **BREAKING (...)**` renders verbatim rather than behind a second marker. Claude-Session: https://claude.ai/code/session_012zwsLMxQBwHUYJgPyrpEvx Co-authored-by: Claude --- scripts/objectui-changeset-digest.mjs | 180 +++++++++++++++++++++++++- 1 file changed, 176 insertions(+), 4 deletions(-) diff --git a/scripts/objectui-changeset-digest.mjs b/scripts/objectui-changeset-digest.mjs index 15e8753eb74..29063f6486b 100644 --- a/scripts/objectui-changeset-digest.mjs +++ b/scripts/objectui-changeset-digest.mjs @@ -236,6 +236,7 @@ import { firstPartyModuleClosure, stageFirstPartyClosure } from './first-party-c const SELF_TEST_BATTERIES = Object.freeze({ 'and one in RC pre-mode': 15, '#6099: the `minor` + prose-annotation family': 17, + '#23112: a DECORATED annotation — a pictograph before the word': 16, 'end-to-end through the shell driver': 5, 'an unwalkable range: REFUSE, loudly (#14178)': 1, 'the INITIAL pin still degrades, and says so': 1, @@ -254,7 +255,7 @@ const SELF_TEST_BATTERIES = Object.freeze({ // DELETING an entry silences that battery's floor exactly as effectively as // zeroing it, so the roster's own size is pinned too. -const SELF_TEST_BATTERY_FLOOR = 16; +const SELF_TEST_BATTERY_FLOOR = 17; // The key an assertion is filed under when no battery is open. It is not a // declared battery, so it reds by the same set difference rather than silently @@ -438,16 +439,48 @@ export function clampSummary(summary, limit = 180) { */ const BREAKING_EMPHASIS_LABEL = /(\*\*|__)[ \t]*breaking\b[^*_\n]{0,48}\1/i; +/** + * A DECORATION an author may put in front of the word (#23112): a run of + * pictographic symbols, each with its optional variation selector — `⚠️` is + * U+26A0 WARNING SIGN + U+FE0F, and `⚠` without the selector is the same sign. + * It sits AFTER the heading or bullet mark, on either side of the emphasis mark + * (`## ⚠️ BREAKING`, `⚠️ **BREAKING`, `**⚠️ Breaking`), and admits nothing else: + * no letter, no digit, no ASCII punctuation, so the line-start anchor below is + * exactly as strict as it was for every line that carries no pictograph. + * + * Measured over objectui's whole changeset history (every blob under + * `.changeset/` on every head of a full clone, objectui `main` at `5595aabca30b`: + * 5678 blobs, 704 block-head lines carrying the word), the ONLY decoration that + * occurs is `⚠️`, in four placements on 51 blobs, and 47 of those were read as + * not breaking — objectui#12242's `## ⚠️ BREAKING, priced as minor …` among them. + * + * Why a CLASS rather than the one sign the corpus shows, and why not "any + * non-word run": ASCII punctuation in front of the word is a PROSE shape in that + * same corpus — `(breaking …)`, `"Breaking for TypeScript readers" below`, + * `— breaking for a top-level logo` — and a code span (`` `breaking` ``) names + * an identifier, so admitting it would trade this miss for false positives. A + * pictograph in front of the word at the head of a block is never prose; closing + * the class keeps the next author's `🚨 BREAKING` from being the same miss again. + */ +const DECORATION = String.raw`(?:\p{Extended_Pictographic}[\u{FE0E}\u{FE0F}]?[ \t]*)*`; + /** The same label, but required to OPEN the text — used to avoid restating it. */ -const OPENS_WITH_BREAKING_LABEL = /^[ \t]*(?:\*\*|__)[ \t]*breaking\b/i; +const OPENS_WITH_BREAKING_LABEL = new RegExp( + String.raw`^[ \t]*${DECORATION}(?:\*\*|__)[ \t]*${DECORATION}breaking\b`, + 'iu', +); /** * A block (paragraph, heading or top-level bullet) that OPENS with the word * "breaking" — `## Breaking note …`, `Breaking for anyone reading \`node.ui\` …`, * `BREAKING CHANGE: …` (the conventional-commit footer needs no separate rule; - * it opens a block by construction). + * it opens a block by construction) — optionally behind a `DECORATION` + * (`## ⚠️ BREAKING, priced as minor …`). */ -const BREAKING_BLOCK_OPENER = /^[ \t]*(?:#{1,6}[ \t]+)?(?:[-*+][ \t]+)?(?:\*\*|__|\*|_)?[ \t]*breaking\b/i; +const BREAKING_BLOCK_OPENER = new RegExp( + String.raw`^[ \t]*(?:#{1,6}[ \t]+)?(?:[-*+][ \t]+)?${DECORATION}(?:\*\*|__|\*|_)?[ \t]*${DECORATION}breaking\b`, + 'iu', +); /** * Did the AUTHOR annotate this change as breaking, in the changeset body? @@ -1749,6 +1782,145 @@ function selfTest() { `bump=${annotated.bump} downgraded=${annotated.downgradedMajor}`, ); + // --- #23112: a DECORATED annotation — a pictograph before the word ------- + // objectui#12242's changeset opens its annotation `## ⚠️ BREAKING, priced as + // minor …`, and the opener used to admit nothing between the heading mark and + // the word, so the digest read it as not breaking. Every positive below is a + // placement measured in objectui's own changeset history (see `DECORATION`); + // every negative is a line the decoration must NOT turn into a declaration. + battery('#23112: a DECORATED annotation — a pictograph before the word'); + // Verbatim heading of `.changeset/12242-types-field-metadata-family.md` at + // objectui `1fb1ced36243`, with its paragraphs trimmed. + const heading12242 = "## ⚠️ BREAKING, priced as minor under the fixed group's version policy"; + const body12242 = + 'The exported field metadata types stop declaring the members `FieldSchema` refuses by name ' + + '(objectui#12242).\n\n' + + '- `BaseFieldMetadata`: FROM `filterable?: boolean` → TO refused.\n\n' + + `${heading12242}\n\n` + + 'TypeScript that writes a retired member no longer compiles.\n'; + check( + "#23112 objectui#12242's changeset body reads breaking (`## ⚠️ BREAKING, …`)", + body12242.includes('## ⚠️ BREAKING') && hasBreakingAnnotation(body12242), + ); + check( + '#23112 the plain-heading control still reads breaking (`## BREAKING, …`)', + hasBreakingAnnotation(body12242.replace('## ⚠️ BREAKING', '## BREAKING')), + ); + check( + '#23112 a decorated PROSE sentence containing "breaking" does not read breaking', + !hasBreakingAnnotation('⚠️ Upgrade note: nothing in this change is breaking for an existing author.') && + !hasBreakingAnnotation('## ⚠️ Read before upgrading — this is not breaking') && + !hasBreakingAnnotation('⚠️ Non-breaking cleanup; a groundbreaking refactor.'), + ); + check( + '#23112 the sign BEFORE the emphasis mark reads breaking (`⚠️ **BREAKING (…)**`)', + hasBreakingAnnotation('a\n\n⚠️ **BREAKING (scored `minor` per the version-alignment rule)** — x'), + ); + check( + '#23112 the sign INSIDE the emphasis mark reads breaking (`**⚠️ Breaking …**`)', + hasBreakingAnnotation('a\n\n**⚠️ Breaking for consumers that read `result.data`.** `validateSchema` x'), + ); + check( + '#23112 the sign opening a plain paragraph reads breaking (`⚠️ Breaking in places, …`)', + hasBreakingAnnotation("a\n\n⚠️ Breaking in places, marked `minor` under this repo's rule."), + ); + check( + '#23112 the sign without its variation selector reads breaking (`## ⚠ BREAKING`)', + hasBreakingAnnotation('a\n\n## ⚠ BREAKING'), + ); + check( + '#23112 the rule is the pictograph CLASS, not the one sign (`- 🚨 BREAKING:` behind a bullet)', + hasBreakingAnnotation('a\n\n- 🚨 BREAKING: `onReorder` is retired.'), + ); + check( + '#23112 ASCII punctuation is NOT decoration — a code span and a quotation stay prose', + !hasBreakingAnnotation('a\n\n`breaking` joins `ChangeEntry` as an optional boolean.') && + !hasBreakingAnnotation('a\n\n"Breaking for TypeScript readers", below, lists the members.'), + ); + check( + "#23112 markBreaking keeps an author's DECORATED label verbatim — never restated", + markBreaking('⚠️ **BREAKING (scored `minor`)** — x') === '⚠️ **BREAKING (scored `minor`)** — x' && + markBreaking('**⚠️ Breaking for consumers.** y') === '**⚠️ Breaking for consumers.** y' && + markBreaking('⚠️ An ordinary summary.') === '**BREAKING** — ⚠️ An ordinary summary.', + ); + + // Through the real digest, in a repo of its own so the #6099 counts above + // stay pinned. Commit (A) is the 12242 shape, (B) opens its SUMMARY with a + // decorated label (objectui `10719-retire-phase35-validation-types.md`), (C) + // is decorated prose. + const uiDeco = join(tmp, 'objectui-decorated'); + mkdirSync(join(uiDeco, '.changeset'), { recursive: true }); + const gDeco = (...args) => git(uiDeco, args); + gDeco('init', '-q', '-b', 'main'); + gDeco('config', 'user.email', 'selftest@objectstack.ai'); + gDeco('config', 'user.name', 'self test'); + gDeco('config', 'commit.gpgsign', 'false'); + const commitDeco = (subject, files) => { + for (const [path, content] of Object.entries(files)) { + mkdirSync(dirname(join(uiDeco, path)), { recursive: true }); + writeFileSync(join(uiDeco, path), content); + } + gDeco('add', '-A'); + gDeco('commit', '-q', '-m', subject); + return gDeco('rev-parse', 'HEAD').trim(); + }; + const baseDeco = commitDeco('chore: base', { 'README.md': 'base\n' }); + const shaA = commitDeco('fix(types): field metadata family (#12242)', { + '.changeset/12242-types-field-metadata-family.md': + "---\n'@object-ui/types': minor\n---\n\n" + body12242, + 'src/types.ts': 'a\n', + }); + commitDeco('fix(core): retire the phase-3.5 validation types (#10719)', { + '.changeset/10719-retire-phase35-validation-types.md': + "---\n'@object-ui/core': minor\n---\n\n" + + '⚠️ **BREAKING (scored `minor` per the version-alignment rule)** — the phase-3.5 ' + + 'validation types are retired.\n', + 'src/core.ts': 'b\n', + }); + commitDeco('fix(fields): a picker note (#3336)', { + '.changeset/picker-note.md': + "---\n'@object-ui/fields': patch\n---\n\n" + + '⚠️ Upgrade note: nothing in this change is breaking for an existing author.\n', + 'src/fields.ts': 'c\n', + }); + const headDeco = gDeco('rev-parse', 'HEAD').trim(); + const decorated = buildDigest({ objectuiRoot: uiDeco, frameworkRoot: fwPlain, from: baseDeco, to: headDeco }); + check( + '#23112 exactly the two decorated annotations are counted, the decorated prose is not', + decorated.releasing.length === 3 && decorated.breaking === 2 && decorated.breakingByAnnotation === 2, + `releasing=${decorated.releasing.length} breaking=${decorated.breaking} byAnnotation=${decorated.breakingByAnnotation}`, + ); + check( + '#23112 the count line reads 2, by annotation', + decorated.body.includes("⚠️ 2 of these carry a breaking change: 2 by the author's own breaking annotation"), + decorated.body, + ); + check( + '#23112 the heading-annotated entry GETS the marker on its line', + decorated.body.includes('- **minor** — **BREAKING** — The exported field metadata types stop declaring'), + decorated.body, + ); + check( + "#23112 a summary opening with a decorated label renders it verbatim, never restated", + decorated.body.includes('- **minor** — ⚠️ **BREAKING (scored `minor` per the version-alignment rule)** —') && + !decorated.body.includes('**BREAKING** — ⚠️ **BREAKING'), + decorated.body, + ); + check( + '#23112 the decorated prose entry is NOT marked', + decorated.body.includes('- **patch** — ⚠️ Upgrade note: nothing in this change is breaking'), + decorated.body, + ); + // The card's second consequence: a range whose ONLY breaking entry is the + // decorated heading used to raise no count at all, and so no ADR-0087 + // disposition prompt. + const onlyA = buildDigest({ objectuiRoot: uiDeco, frameworkRoot: fwPlain, from: baseDeco, to: shaA }); + check( + '#23112 a range whose ONLY breaking entry is the decorated heading still raises the ADR-0087 question', + onlyA.breaking === 1 && onlyA.adr0087Scaffold === true && onlyA.body.includes(ADR_0087_SCAFFOLD), + `breaking=${onlyA.breaking} scaffold=${onlyA.adr0087Scaffold}`, + ); + // --- end-to-end through the shell driver ------------------------------- battery('end-to-end through the shell driver'); const fwRun = join(tmp, 'fw-run'); From ea30d08d654faef6189aee44b8a289ba2e6cf3a3 Mon Sep 17 00:00:00 2001 From: Claude Date: Tue, 13 Oct 2026 00:18:04 +0000 Subject: [PATCH 2/2] test(scripts): pin the decorated-annotation cases on the real wrapped shapes The before-the-emphasis case now uses objectui's own wrapped label, whose run crosses a line, so the emphasis-label scan cannot answer it and only the block opener can. The verbatim-label case asserts the entry is flagged before it reads the rendered line, and the variation-selector cases spell the code points as escapes. Claude-Session: https://claude.ai/code/session_012zwsLMxQBwHUYJgPyrpEvx Co-authored-by: Claude --- scripts/objectui-changeset-digest.mjs | 30 +++++++++++++++++++-------- 1 file changed, 21 insertions(+), 9 deletions(-) diff --git a/scripts/objectui-changeset-digest.mjs b/scripts/objectui-changeset-digest.mjs index 29063f6486b..afaae0663b1 100644 --- a/scripts/objectui-changeset-digest.mjs +++ b/scripts/objectui-changeset-digest.mjs @@ -1800,11 +1800,11 @@ function selfTest() { 'TypeScript that writes a retired member no longer compiles.\n'; check( "#23112 objectui#12242's changeset body reads breaking (`## ⚠️ BREAKING, …`)", - body12242.includes('## ⚠️ BREAKING') && hasBreakingAnnotation(body12242), + body12242.includes('## \u26A0\uFE0F BREAKING') && hasBreakingAnnotation(body12242), ); check( '#23112 the plain-heading control still reads breaking (`## BREAKING, …`)', - hasBreakingAnnotation(body12242.replace('## ⚠️ BREAKING', '## BREAKING')), + hasBreakingAnnotation(body12242.replace('## \u26A0\uFE0F BREAKING', '## BREAKING')), ); check( '#23112 a decorated PROSE sentence containing "breaking" does not read breaking', @@ -1814,7 +1814,13 @@ function selfTest() { ); check( '#23112 the sign BEFORE the emphasis mark reads breaking (`⚠️ **BREAKING (…)**`)', - hasBreakingAnnotation('a\n\n⚠️ **BREAKING (scored `minor` per the version-alignment rule)** — x'), + // Wrapped as objectui `10719-retire-phase35-validation-types.md` wraps it, so + // the label's run crosses a line and `BREAKING_EMPHASIS_LABEL` cannot see it: + // only the opener can answer. + hasBreakingAnnotation( + "a\n\n⚠️ **BREAKING (scored `minor` per this repository's version-alignment\n" + + 'convention) — x.**', + ), ); check( '#23112 the sign INSIDE the emphasis mark reads breaking (`**⚠️ Breaking …**`)', @@ -1826,7 +1832,7 @@ function selfTest() { ); check( '#23112 the sign without its variation selector reads breaking (`## ⚠ BREAKING`)', - hasBreakingAnnotation('a\n\n## ⚠ BREAKING'), + hasBreakingAnnotation('a\n\n## \u26A0 BREAKING'), ); check( '#23112 the rule is the pictograph CLASS, not the one sign (`- 🚨 BREAKING:` behind a bullet)', @@ -1870,11 +1876,12 @@ function selfTest() { "---\n'@object-ui/types': minor\n---\n\n" + body12242, 'src/types.ts': 'a\n', }); - commitDeco('fix(core): retire the phase-3.5 validation types (#10719)', { + commitDeco('fix(types): retire the phase-3.5 validation types (#10719)', { '.changeset/10719-retire-phase35-validation-types.md': - "---\n'@object-ui/core': minor\n---\n\n" + - '⚠️ **BREAKING (scored `minor` per the version-alignment rule)** — the phase-3.5 ' + - 'validation types are retired.\n', + "---\n'@object-ui/types': minor\n---\n\n" + + "⚠️ **BREAKING (scored `minor` per this repository's version-alignment\n" + + 'convention) — `@object-ui/types` no longer exports the eight "Phase 3.5"\n' + + 'validation types (objectui#10719).**\n', 'src/core.ts': 'b\n', }); commitDeco('fix(fields): a picker note (#3336)', { @@ -1902,7 +1909,12 @@ function selfTest() { ); check( "#23112 a summary opening with a decorated label renders it verbatim, never restated", - decorated.body.includes('- **minor** — ⚠️ **BREAKING (scored `minor` per the version-alignment rule)** —') && + // The `breaking` clause is load-bearing: an UNFLAGGED entry renders this + // same line, so the text clauses alone would pass for an empty reason. + decorated.releasing.find((r) => r.path.includes('10719'))?.breaking === true && + decorated.body.includes( + "- **minor** — ⚠️ **BREAKING (scored `minor` per this repository's version-alignment convention)", + ) && !decorated.body.includes('**BREAKING** — ⚠️ **BREAKING'), decorated.body, );