@@ -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 ] * b r e a k i n g \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