Skip to content

Commit b7b8bf6

Browse files
committed
fix(scripts): the objectui changeset digest reads a breaking parenthetical inside a bold lead label
A line that opens, after an optional bullet, with a bold span holding a parenthetical whose first word is "breaking" now reads as an author's breaking annotation (`BREAKING_LEAD_LABEL`, beside the unchanged block opener). The span must be the line's first token and close on that line; a `(breaking ...)` anywhere else stays prose. `markBreaking` keeps stamping such an entry: the label names a package or topic, not the class. Claude-Session: https://claude.ai/code/session_012zwsLMxQBwHUYJgPyrpEvx Co-authored-by: Claude <noreply@anthropic.com>
1 parent f931e76 commit b7b8bf6

1 file changed

Lines changed: 242 additions & 2 deletions

File tree

‎scripts/objectui-changeset-digest.mjs‎

Lines changed: 242 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -237,6 +237,7 @@ const SELF_TEST_BATTERIES = Object.freeze({
237237
'and one in RC pre-mode': 15,
238238
'#6099: the `minor` + prose-annotation family': 17,
239239
'#23112: a DECORATED annotation — a pictograph before the word': 16,
240+
'#23118: a bold LEAD LABEL holding a `(breaking …)` parenthetical': 16,
240241
'end-to-end through the shell driver': 5,
241242
'an unwalkable range: REFUSE, loudly (#14178)': 1,
242243
'the INITIAL pin still degrades, and says so': 1,
@@ -255,7 +256,7 @@ const SELF_TEST_BATTERIES = Object.freeze({
255256

256257
// DELETING an entry silences that battery's floor exactly as effectively as
257258
// zeroing it, so the roster's own size is pinned too.
258-
const SELF_TEST_BATTERY_FLOOR = 17;
259+
const SELF_TEST_BATTERY_FLOOR = 18;
259260

260261
// The key an assertion is filed under when no battery is open. It is not a
261262
// declared battery, so it reds by the same set difference rather than silently
@@ -482,13 +483,57 @@ const BREAKING_BLOCK_OPENER = new RegExp(
482483
'iu',
483484
);
484485

486+
/**
487+
* A bold LEAD LABEL that carries the annotation in a parenthetical (#23118):
488+
* ``**`@object-ui/layout` (breaking, declared `minor` per the version policy).**``,
489+
* `**Narrowed (breaking).**`, `- **Tolerant face (breaking for invalid documents).**`.
490+
* The word is not first, so the opener above cannot see it, and the label names
491+
* no word first either, so neither can `BREAKING_EMPHASIS_LABEL`.
492+
*
493+
* A SECOND POSITION, beside the opener and not inside it, with all three parts
494+
* required:
495+
*
496+
* * the line OPENS with `**`, after optional indentation and an optional
497+
* bullet — the label is the line's first token;
498+
* * the `(` sits INSIDE that first bold span: nothing before it may close the
499+
* span, and the span closes on the same line;
500+
* * `breaking` is the parenthetical's FIRST word, so `(non-breaking)` and
501+
* `(not breaking)` stay what they say.
502+
*
503+
* So a `(breaking …)` anywhere else is prose — mid-sentence, after a label that
504+
* already closed (`**Behaviour change** (breaking …`), in a bold span that is not
505+
* the line's first token — and so is a label with no parenthetical.
506+
*
507+
* Judged on EVERY line, not only at a block start: a bullet opens a line without
508+
* opening a block (`- **Strict authoring face.** …` then `- **Tolerant face
509+
* (breaking for invalid documents).** …`), and a wrap column cannot manufacture
510+
* a closed bold span holding the parenthetical the way it can land a bare word
511+
* first on a line.
512+
*
513+
* Measured over objectui's changeset history (every `.changeset/*.md` blob on
514+
* every head and every pull-request head of a clone, objectui `main` at
515+
* `45ae6b89e845`: 6449 blobs), the label opens 41 lines in 39 blobs (18 paths),
516+
* none of them read as breaking before, and each is a lead label. It never sits
517+
* behind `__`, a heading mark, an ordered-list mark or a pictograph, and its
518+
* span never crosses a line, so none of those is admitted.
519+
*
520+
* `markBreaking` STAMPS such an entry. The label names a package or a topic, not
521+
* the class, so it is not a label `OPENS_WITH_BREAKING_LABEL` keeps verbatim, and
522+
* the uniform marker is what keeps the count note's "Each is marked" true. None
523+
* of those 39 blobs opens its summary with the label, so the stamp lands on an
524+
* ordinary first paragraph there.
525+
*/
526+
const BREAKING_LEAD_LABEL = /^[ \t]*(?:[-*+][ \t]+)?\*\*(?:(?!\*\*)[^\n])*?\([ \t]*breaking\b(?:(?!\*\*)[^\n])*\*\*/i;
527+
485528
/**
486529
* Did the AUTHOR annotate this change as breaking, in the changeset body?
487530
*
488531
* THE PRECISION RULE IS POSITIONAL, NOT LEXICAL. "breaking" is an ordinary
489532
* English word that appears in changeset prose all the time; what makes an
490533
* occurrence a *declaration* is where the author put it — in an emphasis label,
491-
* or at the head of a block. It is never enough that the word merely occurs.
534+
* at the head of a block, or as the first word of a parenthetical inside the
535+
* bold lead label that opens a line (`BREAKING_LEAD_LABEL`, #23118). It is never
536+
* enough that the word merely occurs.
492537
*
493538
* Measured over `f5bc4c78be76..f995a452d2ca` (65 changesets, 5 mention the word):
494539
*
@@ -522,6 +567,8 @@ export function hasBreakingAnnotation(body) {
522567
atBlockStart = true;
523568
continue;
524569
}
570+
// #23118: line-initial, unlike the opener below — see `BREAKING_LEAD_LABEL`.
571+
if (BREAKING_LEAD_LABEL.test(line)) return true;
525572
// A heading opens a block whether or not a blank line precedes it.
526573
if (atBlockStart || /^[ \t]*#{1,6}[ \t]+/.test(line)) {
527574
if (BREAKING_BLOCK_OPENER.test(line)) return true;
@@ -1933,6 +1980,199 @@ function selfTest() {
19331980
`breaking=${onlyA.breaking} scaffold=${onlyA.adr0087Scaffold}`,
19341981
);
19351982

1983+
// --- #23118: a bold LEAD LABEL holding a `(breaking …)` parenthetical -----
1984+
// objectui `8a55f0ccd`'s `.changeset/12059-sidebar-no-reorder.md` annotates its
1985+
// change in a per-package lead label, and the word is neither first in the
1986+
// block nor first in the emphasis run, so the digest read it as not breaking.
1987+
// Every positive below is a shape measured in objectui's own changeset
1988+
// history (see `BREAKING_LEAD_LABEL`); every negative is a `(breaking …)`
1989+
// the rule must leave as prose, most of them verbatim from that same history.
1990+
battery('#23118: a bold LEAD LABEL holding a `(breaking …)` parenthetical');
1991+
const label12059 = '**`@object-ui/layout` (breaking, declared `minor` per the version policy).**';
1992+
// `.changeset/12059-sidebar-no-reorder.md` at objectui `8a55f0ccd`, paragraphs trimmed.
1993+
const body12059 =
1994+
"The console sidebar no longer lets a user drag the app's menu into a private order; the " +
1995+
'Pinned section is now the one place a user orders entries (objectui#12059).\n\n' +
1996+
`${label12059} \`NavigationRenderer\` retires \`enableReorder\` and \`onReorder\`.\n\n` +
1997+
"**`@object-ui/app-shell`.** The sidebar no longer reads or writes the per-user menu order.\n";
1998+
check(
1999+
"#23118 the card's example reads breaking (``**`@object-ui/layout` (breaking, …).**``)",
2000+
body12059.includes(`\n\n${label12059} `) && hasBreakingAnnotation(body12059),
2001+
);
2002+
check(
2003+
'#23118 the rule is LINE-initial: a bulleted label that opens no block reads breaking',
2004+
// objectui `6152-object-form-unmirrored-members.md`: the second bullet of a list.
2005+
hasBreakingAnnotation(
2006+
'What an author sees change:\n\n' +
2007+
'- **Strict authoring face.** A document using any of these keys is no longer refused.\n' +
2008+
'- **Tolerant face (breaking for invalid documents).** A value of the wrong type is refused.',
2009+
),
2010+
);
2011+
check(
2012+
'#23118 the label alone on its line, with more after the parenthetical, and in any case',
2013+
hasBreakingAnnotation('a\n\n**Narrowed (breaking).**\n\n`object-grid` x') &&
2014+
hasBreakingAnnotation("a\n\n**Narrowed (breaking), `@object-ui/types`.** `@objectstack/spec`'s x") &&
2015+
hasBreakingAnnotation('a\n\n**`@object-ui/i18n` (BREAKING, declared `minor`).** `console.nav` x'),
2016+
);
2017+
check(
2018+
'#23118 a prose `(breaking …)` outside a lead label does not read breaking',
2019+
// objectui `11717-dashboard-widget-chart-measure-arity.md` (mid-sentence) and
2020+
// `6152-unmirrored-round4.md` (a wrapped line that opens with the parenthetical).
2021+
!hasBreakingAnnotation(
2022+
'a\n\nThis narrows the accepted documents (breaking for a document of either shape). The fix: x',
2023+
) &&
2024+
!hasBreakingAnnotation(
2025+
'a\n\nA wrong-typed value at either key is now refused at its path, where it used to be kept\n' +
2026+
'unexamined\n(breaking for invalid documents).',
2027+
),
2028+
);
2029+
check(
2030+
'#23118 a lead label WITHOUT the parenthetical does not read breaking',
2031+
!hasBreakingAnnotation('a\n\n**`@object-ui/app-shell`.** The sidebar no longer reads x') &&
2032+
!hasBreakingAnnotation('a\n\n- **Strict authoring face.** A document using any of these keys x'),
2033+
);
2034+
check(
2035+
'#23118 a parenthetical AFTER the label has closed is prose (`**…** (breaking …`)',
2036+
// objectui `5896-feeditem-single-constructor.md`, verbatim.
2037+
!hasBreakingAnnotation(
2038+
'a\n\n**Behaviour change on the console record page** (breaking in the objectui sense,\n' +
2039+
"shipped `minor` — objectui's `major` tracks `@objectstack`):",
2040+
),
2041+
);
2042+
check(
2043+
"#23118 a bold span that is NOT the line's first token does not read breaking",
2044+
!hasBreakingAnnotation('a\n\nSee **`@object-ui/layout` (breaking).** below.') &&
2045+
!hasBreakingAnnotation('a\n\n**Removed.** **`@object-ui/layout` (breaking).** x'),
2046+
);
2047+
check(
2048+
"#23118 `breaking` must be the parenthetical's FIRST word (`(non-breaking)`, `(not breaking …)`)",
2049+
!hasBreakingAnnotation('a\n\n**`@object-ui/layout` (non-breaking).** x') &&
2050+
!hasBreakingAnnotation('a\n\n**Cleanup (not breaking for any author).** x'),
2051+
);
2052+
check(
2053+
'#23118 an UNBOLDED label is prose: plain text and a code span stay unread',
2054+
// objectui `11168-element-family-inputs.md` and `11674-shortcuts-dialog-wired-only.md`.
2055+
!hasBreakingAnnotation(
2056+
'a\n\nNarrowed (breaking for an author who wrote them, which the spec already\nrefuses):',
2057+
) &&
2058+
!hasBreakingAnnotation(
2059+
"a\n\n`@object-ui/i18n` (BREAKING; `minor` under this repository's release model): x",
2060+
),
2061+
);
2062+
check(
2063+
'#23118 markBreaking STAMPS a summary that opens with a lead label — it is not a breaking label',
2064+
markBreaking('**`@object-ui/types` (breaking, hence `minor`).** x') ===
2065+
'**BREAKING** — **`@object-ui/types` (breaking, hence `minor`).** x' &&
2066+
markBreaking('**BREAKING (v17)** — x') === '**BREAKING (v17)** — x',
2067+
);
2068+
2069+
// Through the real digest, in a repo of its own so the counts above stay
2070+
// pinned. (A) is the 12059 shape, (B) the bulleted label, (C) opens its
2071+
// SUMMARY with a lead label (no measured specimen; it pins the renderer
2072+
// through the real path), (D)-(F) are the prose shapes the rule must leave.
2073+
const uiLead = join(tmp, 'objectui-lead-label');
2074+
mkdirSync(join(uiLead, '.changeset'), { recursive: true });
2075+
const gLead = (...args) => git(uiLead, args);
2076+
gLead('init', '-q', '-b', 'main');
2077+
gLead('config', 'user.email', 'selftest@objectstack.ai');
2078+
gLead('config', 'user.name', 'self test');
2079+
gLead('config', 'commit.gpgsign', 'false');
2080+
const commitLead = (subject, files) => {
2081+
for (const [path, content] of Object.entries(files)) {
2082+
mkdirSync(dirname(join(uiLead, path)), { recursive: true });
2083+
writeFileSync(join(uiLead, path), content);
2084+
}
2085+
gLead('add', '-A');
2086+
gLead('commit', '-q', '-m', subject);
2087+
return gLead('rev-parse', 'HEAD').trim();
2088+
};
2089+
const baseLead = commitLead('chore: base', { 'README.md': 'base\n' });
2090+
const shaLeadA = commitLead('feat(layout): the sidebar menu is not user-reorderable (#12059)', {
2091+
'.changeset/12059-sidebar-no-reorder.md': "---\n'@object-ui/layout': minor\n---\n\n" + body12059,
2092+
'src/layout.ts': 'a\n',
2093+
});
2094+
commitLead('feat(types): the object-form mirror declares its members (#6152)', {
2095+
'.changeset/6152-object-form-unmirrored-members.md':
2096+
"---\n'@object-ui/types': minor\n---\n\n" +
2097+
'The `object-form` zod mirror declares the members its TypeScript twin declared.\n\n' +
2098+
'- **Strict authoring face.** A document using any of these keys is no longer refused.\n' +
2099+
'- **Tolerant face (breaking for invalid documents).** A value of the wrong type is refused.\n',
2100+
'src/form.ts': 'b\n',
2101+
});
2102+
commitLead('feat(types): a list view retires its legacy chart axes (#6152)', {
2103+
'.changeset/lead-label-summary.md':
2104+
"---\n'@object-ui/types': minor\n---\n\n" +
2105+
'**`@object-ui/types` (breaking for a writer of a retired key, hence `minor`).** The legacy\n' +
2106+
'chart axes are refused at the key.\n',
2107+
'src/chart.ts': 'c\n',
2108+
});
2109+
commitLead('feat(dashboard): a widget measure arity (#11717)', {
2110+
'.changeset/11717-dashboard-widget-chart-measure-arity.md':
2111+
"---\n'@object-ui/plugin-dashboard': patch\n---\n\n" +
2112+
'A chart widget refuses a measure it cannot draw.\n\n' +
2113+
'This narrows the accepted documents (breaking for a document of either shape). The fix: x\n',
2114+
'src/dashboard.ts': 'd\n',
2115+
});
2116+
commitLead('refactor(app-shell): one feed item constructor (#5896)', {
2117+
'.changeset/5896-feeditem-single-constructor.md':
2118+
"---\n'@object-ui/app-shell': patch\n---\n\n" +
2119+
'The record feed builds every item through one constructor.\n\n' +
2120+
'**Behaviour change on the console record page** (breaking in the objectui sense,\n' +
2121+
"shipped `minor` — objectui's `major` tracks `@objectstack`):\n",
2122+
'src/feed.ts': 'e\n',
2123+
});
2124+
commitLead('feat(app-shell): the sidebar keeps no private order', {
2125+
'.changeset/app-shell-no-private-order.md':
2126+
"---\n'@object-ui/app-shell': patch\n---\n\n" +
2127+
'**`@object-ui/app-shell`.** The sidebar no longer reads or writes the per-user menu order.\n',
2128+
'src/sidebar.ts': 'f\n',
2129+
});
2130+
const headLead = gLead('rev-parse', 'HEAD').trim();
2131+
const lead = buildDigest({ objectuiRoot: uiLead, frameworkRoot: fwPlain, from: baseLead, to: headLead });
2132+
const leadFlag = (frag) => lead.releasing.find((r) => r.path.includes(frag))?.breaking;
2133+
check(
2134+
'#23118 exactly the three lead-label annotations are counted, the three prose shapes are not',
2135+
lead.releasing.length === 6 &&
2136+
lead.breaking === 3 &&
2137+
lead.breakingByAnnotation === 3 &&
2138+
leadFlag('12059') === true &&
2139+
leadFlag('6152-object-form') === true &&
2140+
leadFlag('lead-label-summary') === true,
2141+
`releasing=${lead.releasing.length} breaking=${lead.breaking} byAnnotation=${lead.breakingByAnnotation}`,
2142+
);
2143+
check(
2144+
'#23118 the count line reads 3, by annotation',
2145+
lead.body.includes("⚠️ 3 of these carry a breaking change: 3 by the author's own breaking annotation"),
2146+
lead.body,
2147+
);
2148+
check(
2149+
'#23118 the 12059-shaped entry GETS the marker on its line',
2150+
lead.body.includes("- **minor** — **BREAKING** — The console sidebar no longer lets a user drag the app's menu"),
2151+
lead.body,
2152+
);
2153+
check(
2154+
'#23118 a summary opening with a lead label is stamped ONCE, the label kept verbatim after it',
2155+
lead.body.includes(
2156+
'- **minor** — **BREAKING** — **`@object-ui/types` (breaking for a writer of a retired key, hence `minor`).** The legacy',
2157+
) && !lead.body.includes('**BREAKING** — **BREAKING'),
2158+
lead.body,
2159+
);
2160+
check(
2161+
'#23118 the three prose entries are NOT marked',
2162+
lead.body.includes('- **patch** — A chart widget refuses a measure it cannot draw.') &&
2163+
lead.body.includes('- **patch** — The record feed builds every item through one constructor.') &&
2164+
lead.body.includes('- **patch** — **`@object-ui/app-shell`.** The sidebar no longer reads'),
2165+
lead.body,
2166+
);
2167+
// The family card's second consequence: a range whose ONLY breaking entry is
2168+
// a lead label used to raise no count, and so no ADR-0087 disposition prompt.
2169+
const onlyLead = buildDigest({ objectuiRoot: uiLead, frameworkRoot: fwPlain, from: baseLead, to: shaLeadA });
2170+
check(
2171+
'#23118 a range whose ONLY breaking entry is a lead label still raises the ADR-0087 question',
2172+
onlyLead.breaking === 1 && onlyLead.adr0087Scaffold === true && onlyLead.body.includes(ADR_0087_SCAFFOLD),
2173+
`breaking=${onlyLead.breaking} scaffold=${onlyLead.adr0087Scaffold}`,
2174+
);
2175+
19362176
// --- end-to-end through the shell driver -------------------------------
19372177
battery('end-to-end through the shell driver');
19382178
const fwRun = join(tmp, 'fw-run');

0 commit comments

Comments
 (0)