Skip to content

Commit 8807206

Browse files
fix(scripts): the objectui changeset digest reads a (breaking …) parenthetical inside a bold lead label (#23131)
Fixes #23118 Clause-②: no ## What changed One file, `scripts/objectui-changeset-digest.mjs`: - **`BREAKING_LEAD_LABEL`**, a new rule beside `BREAKING_BLOCK_OPENER`, which stays byte-identical. A line reads breaking when all three hold: - it opens with `**`, after optional indentation and an optional bullet (`-`, `*`, `+`), so the label is the line's first token; - the `(` sits inside that first bold span: nothing before it closes the span, and the span closes on the same line; - `breaking` is the parenthetical's first word, in any case. - **`hasBreakingAnnotation`** tests the new rule on every line, not only at a block start. A bullet opens a line without opening a block: `- **Strict authoring face.** …` followed by `- **Tolerant face (breaking for invalid documents).** …` is a real objectui shape. - **`markBreaking` / `OPENS_WITH_BREAKING_LABEL`: unchanged, by measurement.** See *Assumption 3* below. - **Self-test:** a new battery, `#23118: a bold LEAD LABEL holding a (breaking …) parenthetical`, with 16 cases and a floor of 16. The roster floor goes from 17 to 18. The rule follows triage's grade on the card: a bold span opening the line, holding a parenthetical whose first word is `breaking`. A `(breaking …)` anywhere else is prose, and so is a bold span that is not the line's first token. The line-start opener rule from PR #23117 is unchanged. ## Corpus reading **Tree:** a blobless bare clone of `objectstack-ai/objectui`, with every head (1143 refs) and every pull-request head (7169 refs); objectui `main` at `45ae6b89e845`. Every blob ever written under `.changeset/*.md` (README excluded) was read from each distinct `.changeset` tree object and fetched in full: **6449 blobs over 4825 paths**. The heads-only subset is 5683 blobs; the earlier reading on the decoration card had 5678 at `main` `5595aabca30b`. Each body was parsed with the digest's own `parseChangeset` and judged with `hasBreakingAnnotation`: the base at `f931e765b` (blob `afaae0663b1a`) against this branch. **Assumption 1 (reproduced on current `main`):** at `f931e765b`, `hasBreakingAnnotation(parseChangeset(text).body)` returns `false` for objectui `8a55f0ccd`'s `.changeset/12059-sidebar-no-reorder.md` (blob `873e53f906`). The positive control, objectui#12242's changeset at `1fb1ced36243`, returns `true`. **Assumption 2, the split of the `(` blobs by position.** On the heads corpus, "breaking" right after `(` occurs in 39 blobs / 23 paths. That matches the earlier reading's 39 / 23 exactly, so it is the same set. | position | heads | all refs | |:--|--:|--:| | inside a bold lead label that opens the line (**flips**) | 33 blobs / 18 paths | 39 blobs / 18 paths | | anywhere else (prose, **no flip**) | 6 blobs / 5 paths | 6 blobs / 5 paths | | **total** | **39 / 23** | **45 / 23** | The 6 prose blobs are four shapes, and the rule leaves each of them unflagged: - a label that has already closed before the parenthetical: `**Behaviour change on the console record page** (breaking in the objectui sense,` (`5896-feeditem-single-constructor.md`, 2 blobs); - an unbolded plain-text label: `Narrowed (breaking for an author who wrote them, …` (`11168-element-family-inputs.md`, `11168-action-family-published-inputs.md`, 1 blob each); - an unbolded code-span label: `` `@object-ui/i18n` (BREAKING; `minor` under this repository's release model …): `` (`11674-shortcuts-dialog-wired-only.md`, 1 blob); - mid-sentence prose: `This narrows the accepted documents (breaking for a document of either shape).` (`11717-dashboard-widget-chart-measure-arity.md`, 1 blob). Two blobs of `6152-unmirrored-round4.md` carry both a lead label and a wrapped prose line that opens with `(breaking for invalid documents).`. They flip because of the label. The prose line on its own does not read breaking: self-test case 4 pins that line. **Flips (blobs whose verdict moves from not-breaking to breaking):** - **39 blobs / 18 paths** over all refs; 33 / 18 on heads; 33 / 18 reachable from `main`. - **Flips outside a lead label: 0.** Every flipped blob carries a line the lead-label rule matches. The other two rules are byte-identical, so no flip can come from them. Across the 39 blobs there are 41 matching lines with 18 distinct texts, and each is a lead label. Examples: ``**`@object-ui/layout` (breaking, declared `minor` per the version policy).**``, `**Narrowed (breaking).**`, ``**Retired (breaking), `@object-ui/types`.**``, `**Accept-set change (breaking semantics, released as minor by the version-alignment rule):**`. - **Lost readings** (blobs the base reads as breaking and this branch does not): **0** of 6449. - **Shapes the corpus never uses, so the rule does not admit them:** a lead label behind `__`, a heading mark, an ordered-list mark or a pictograph; a lead-label span that crosses a line; a bold span holding `(breaking` that is not the line's first token. Each has 0 occurrences. **Assumption 3 (`markBreaking`), measured on the 39 flipped blobs:** none of them opens its summary (the first paragraph) with the lead label. So the `**BREAKING** — ` stamp lands on an ordinary summary in every real case. No rendered artifact below has a stamp followed by another bold label. `OPENS_WITH_BREAKING_LABEL` therefore stays as it is, and it is not widened for the hypothetical case either. A lead label names a package or a topic, not the class, so it is not one of the "breaking labels" the renderer keeps verbatim. The count note says "Each is marked **BREAKING** in the list above", and that is only true if such an entry is stamped. The self-test pins the decision (cases 10 and 14). ## Real hops: the count line before and after The digest ran over the last 11 console pin hops, `0abd4f9f8769` → … → `1fb1ced36243`, plus the prospective next hop `1fb1ced36243..45ae6b89e845`, with the base and with this branch at `b7b8bf6d7`. - **The current pin's hop, `7578323a544e..1fb1ced36243`, is byte-identical:** `→ 10 releasing changeset(s), 2 breaking (0 declared major, 2 annotated), …` before and after. No entry changes, because no lead-label changeset falls in this range. The prospective next hop is also byte-identical (1 breaking). - **Two of the 11 hops change, and nothing else changes in them:** `47b1f0bb7174..20c6d351ad74`, before: ```text → 33 releasing changeset(s), 6 breaking (0 declared major, 6 annotated), 0 release-nothing, 1 commit(s) without a changeset ⚠️ 6 of these carry a breaking change: 6 by the author's own breaking annotation in the changeset body — … ``` After: ```text → 33 releasing changeset(s), 10 breaking (0 declared major, 10 annotated), 0 release-nothing, 1 commit(s) without a changeset ⚠️ 10 of these carry a breaking change: 10 by the author's own breaking annotation in the changeset body — … ``` The 4 entries now marked are objectui `8a55f0ccd` (`12059-sidebar-no-reorder.md`, the card's example; its line now reads `- **minor** — **BREAKING** — The console sidebar no longer lets a user drag …`), `c7b30bd66` (`12063-unified-view-config-chart-retired.md`), `025341692` (`6152-list-view-legacy-chart-retired.md`) and `3fd862510` (`6152-list-view-readers-retired.md`). `a58626c88dc8..f0268ad78485`: `173 releasing …, 4 breaking` → `173 releasing …, 10 breaking`. The 6 entries now marked are `5d77c099b` (`6152-list-view-options-bag.md`), `cf62edf68` (`6152-list-view-blocks-strict.md`), `b13ea3c67` (`6152-grid-default-filters-gantt-map-filter.md`), `1473757bf` (`6152-object-calendar-container-strict.md`), `d7e9e9ab6` (`6152-grid-kanban-calendar-17-7-members.md`) and `d73d98770` (`6152-resizable-columns-retired.md`). `scripts/bump-objectui.sh`, `.objectui-sha` and every `.changeset/` file are untouched. ## Self-test cases (battery `#23118`, 16) Direct, against the criterion and the renderer: 1. The card's example reads breaking: ``**`@object-ui/layout` (breaking, declared `minor` per the version policy).**``, in 12059's own paragraphs, with a guard that the fixture really carries the label. 2. The rule is line-initial. The second bullet of a list, which opens no block, reads breaking (`- **Tolerant face (breaking for invalid documents).**`). 3. A label alone on its line (`**Narrowed (breaking).**`), a label with text after the parenthetical (`**Narrowed (breaking), …**`), and the word in upper case all read breaking. 4. Prose `(breaking …)` outside a lead label does not: the 11717 mid-sentence line, and the round-4 wrapped line that opens with the parenthetical. 5. A lead label without the parenthetical does not (``**`@object-ui/app-shell`.**``, `- **Strict authoring face.**`). 6. A parenthetical after the label has closed does not (the 5896 line, verbatim). 7. A bold span that is not the line's first token does not (`See **… (breaking).** below.`, `**Removed.** **… (breaking).**`). 8. `breaking` must be the parenthetical's first word: `(non-breaking)` and `(not breaking …)` do not read breaking. 9. An unbolded label is prose: the 11168 plain-text label and the 11674 code-span label do not read breaking. 10. `markBreaking` stamps a summary that opens with a lead label. `**BREAKING (v17)**` is still kept verbatim. Through `buildDigest`, in a fixture repo of its own: 11. Exactly the three lead-label annotations are counted (6 releasing, 3 breaking, 3 by annotation), and the three prose shapes are not. 12. The count line reads `⚠️ 3 of these carry a breaking change: 3 by the author's own breaking annotation`. 13. The 12059-shaped entry gets the marker on its line. 14. A summary that opens with a lead label is stamped once, with the label kept verbatim after the stamp. 15. The three prose entries are not marked. 16. A range whose only breaking entry is a lead label still raises the ADR-0087 disposition question. **Every shape-1 pin of PR #23117 holds:** the `#23112` battery (16 cases) and the `#6099` battery (17) are unchanged and green. ## Ablation Both legs ran from the committed `b7b8bf6d7` through `scripts/ablation-replace.mjs` in wrap mode. In each leg the anchor went 1 → 0 and the replacement 0 → 1. After each leg the tool confirmed that the blob equals HEAD (`2e67cb6c2848`) and that `git diff HEAD` is empty. This is a root script run from source, so no build or `dist/` is involved. - **Leg 1, the rule's call site removed:** exactly 8 of the 16 `#23118` cases go red, all of them positives (1, 2, 3, 11, 12, 13, 14, 16). The 7 negatives and the renderer pin stay green, and no other battery moves. Self-test exit 1. The corpus census over the mutated module shows 0 flips. - **Leg 2, the rule widened to `(breaking` anywhere:** 7 cases go red: negatives 4, 6, 7 and 9, and digest cases 11, 12 and 15. The census shows 45 blobs / 23 paths flipping, and **6 blobs / 5 paths flip outside a lead label**. So the corpus check can fail. ## Gates (at `b7b8bf6d7`) - **Derived set:** `node scripts/pm/dispatch-gates.mjs --commands --repo objectstack-ai/objectstack` on the actual change set derives 30 commands, identical to the dispatch's 30. All 30 exit 0. `pnpm check:pm-dispatch-gates` ran detached: `✓ dispatch-gates self-test (fast tier): 1833 cases pass`, exit 0. - **Reconciliation:** `--ran` reports `30 derived, 30 run, 0 NOT-MEASURED, 0 UNRUN`. - **The digest's own suite and its consumers:** - `pnpm check:objectui-changeset`: the digest self-test, 163 checks, `all checks passed`, followed by `objectui-range --self-test` (which imports `classifyRange`), also passed. - `pnpm check:objectui-bump`: `bump-objectui self-test PASSED — 20 assertions across 5 cases`. - `pnpm check:changeset-gate-self-tests`: check-empty-changeset 170 assertions, check-adr-0087-registration 548, check-changeset-no-major 401. - **Lint**, a proven narrowing: - The population is read from `eslint.config.mjs`: the `**/*.{ts,tsx,mts,cts,js,jsx,mjs,cjs}` object covers `scripts/*.mjs`. - `--format json` reports 1 file, 0 errors, 0 warnings. - Every `parserOptions` in the config carries only `ecmaVersion` / `sourceType`, so no type-aware linting is enabled and this diff cannot move the verdict on any file it does not touch. - The full `pnpm lint` run is CI's. - **Changeset:** none. Root `scripts/` ships in no package's `files[]`, so this PR takes `skip-changeset`. ## Acceptance notes - **Out of scope, reported to the seat, not edited here (release text).** Two console changesets still pending on `main` carry the old rule's under-count, and they will publish into `@objectstack/console`'s CHANGELOG at the next release: - `.changeset/console-20c6d351ad74.md` line 43 reads `⚠️ 6 of these carry a breaking change`. The fixed digest reads 10, and 4 entries there are unmarked. - `.changeset/console-f0268ad78485.md` line 111 reads `⚠️ 4 of these carry a breaking change`. The fixed digest reads 10, and 6 entries there are unmarked. - The entries are the ones listed under *Real hops*. The other 9 pending console changesets read the same under both rules. - **Further positions of the same family, outside triage's binding, left as prose.** The 6 prose blobs above are the four positions the grade rules out: a label closed before the parenthetical, an unbolded plain-text label, an unbolded code-span label, and mid-sentence prose. Two of them, the 11674 code-span label and the 11717 mid-sentence line, sit inside the pending hops. They are reported for the family card, not admitted here. --- _Generated by [Claude Code](https://claude.ai/code/session_012zwsLMxQBwHUYJgPyrpEvx)_ Co-authored-by: Claude <noreply@anthropic.com>
1 parent 1ca7a80 commit 8807206

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)