From 09f1d485639ee99f401ad789d37fc21069a5e594 Mon Sep 17 00:00:00 2001 From: Claude Date: Sun, 6 Sep 2026 05:35:00 +0000 Subject: [PATCH 1/2] docs(plugins): bring three documented interface blocks up to their shipped types `PARTIAL_EXCERPTS` in `scripts/check-readme-exports.mjs` opened with three entries, each recording drift objectui#6214 was not allowed to fix in the gate's own PR. All three are paid off here and the ledger is now empty. The three are NOT one fix: - `packages/plugin-gantt/README.md::GanttTask` is a genuine excerpt -- the paragraph directly below the block already says `fields` and `hasOwnDates` are populated by `ObjectGantt` itself. It moves to the in-README `PARTIAL_MARKER` above the fence, which is the declaration that means "deliberate"; adding the two keys to the block would contradict that prose. - `packages/plugin-kanban/README.md::KanbanColumn` and `::KanbanCard` are staleness, so the blocks are brought up to the shipped declarations. The key sets are re-derived from what `@object-ui/plugin-kanban` actually exports on a built tree, not copied from the ledger reasons: the column is two keys behind (`collapsed` and the `color` tombstone), not the one the ledger recorded. The ledger's own test case is updated with it. Its non-vacuity guard (`expect(ledgered.length).toBeGreaterThan(0)`) went red on the empty ledger, correctly: the per-ENTRY leg stops asserting. The case does not go vacuous -- `judgeable` becomes the empty set and the equality then reads "no declaration in this tree reds as a stale omission with the ledger off" -- so the guard is replaced by the empty state asserted as a fact, and the per-entry property stays pinned on the fixture tree, which carries an entry by construction. Co-Authored-By: Claude Fable 5.1 Claude-Session: https://claude.ai/code/session_013uAaxiwgYDybsTNV9xwa1M --- packages/plugin-gantt/README.md | 1 + packages/plugin-kanban/README.md | 11 +++++++- .../__tests__/check-readme-exports.test.ts | 24 ++++++++++++++++- scripts/check-readme-exports.mjs | 27 ++++++++++++------- 4 files changed, 52 insertions(+), 11 deletions(-) diff --git a/packages/plugin-gantt/README.md b/packages/plugin-gantt/README.md index 9645c7621c..17b504eb27 100644 --- a/packages/plugin-gantt/README.md +++ b/packages/plugin-gantt/README.md @@ -389,6 +389,7 @@ pulses it — useful in deep or long trees. what `ObjectGantt` produces from each record. Dates are real `Date` objects and the label field is `title` (`src/GanttView.tsx`): + ```typescript interface GanttTask { id: string | number; diff --git a/packages/plugin-kanban/README.md b/packages/plugin-kanban/README.md index 5d17050c6a..9729622688 100644 --- a/packages/plugin-kanban/README.md +++ b/packages/plugin-kanban/README.md @@ -115,8 +115,10 @@ interface KanbanColumn { id: string; title: string; cards: KanbanCard[]; - limit?: number; // Maximum cards allowed + limit?: number; // WIP limit — the count at which the lane warns className?: string; + collapsed?: boolean; // Lane renders collapsed (honoured by KanbanEnhanced) + color?: never; // RETIRED — refused by name; style a lane through className } // Card structure @@ -128,6 +130,13 @@ interface KanbanCard { label: string; variant?: 'default' | 'secondary' | 'destructive' | 'outline'; }>; + cardSubtitle?: string; // Synthesized subtitle, rendered in preference to description + cardFieldCells?: Array<{ // Structured per-field cells; wins over cardSubtitle/description + field: string; + label?: string; + node: React.ReactNode; + }>; + coverImage?: string; // Resolved cover-image URL, from the board's coverImageField } ``` diff --git a/scripts/__tests__/check-readme-exports.test.ts b/scripts/__tests__/check-readme-exports.test.ts index b55bc69da0..ddae340eae 100644 --- a/scripts/__tests__/check-readme-exports.test.ts +++ b/scripts/__tests__/check-readme-exports.test.ts @@ -953,7 +953,29 @@ describe('the PARTIAL_EXCERPTS ledger, as it stands in this repository', () => { const judgeable = new Set(ledgered.filter((key) => !isUnbuilt(packageOfEntry(key)))); const suspendable = ledgered.filter((key) => isUnbuilt(packageOfEntry(key))); - expect(ledgered.length, 'the ledger is empty, so this case asserts nothing').toBeGreaterThan(0); + // THE LEDGER IS EMPTY TODAY, and this line is what says so out loud. + // objectui#7302 paid off the three entries objectui#6214 opened with, and + // the guard that stood here (`expect(ledgered.length).toBeGreaterThan(0)`, + // "the ledger is empty, so this case asserts nothing") went red on that + // content fix. It was right to: the per-ENTRY leg below does stop asserting + // when there is no entry. But this case does NOT go vacuous with it -- + // `judgeable` becomes the empty set, so the equality two blocks down reads + // "NO declaration in this tree reds as a stale omission with the ledger + // off", which is the strongest state this repository can be in and reds on + // the next README that drifts. + // + // So the guard is replaced by the FACT, asserted rather than assumed: an + // entry arriving flips this line, in the same file that explains what the + // entry then has to satisfy. The per-entry property itself never depended + // on the repository carrying an entry -- it is pinned on the fixture tree + // above ('the LEDGER suppresses the omission it records', 'the LEDGER does + // NOT suppress a fabricated key either', and the two stale-entry cases), + // which carry an entry by construction and cannot be emptied by a content + // card. + expect( + ledgered, + 'the ledger is no longer empty -- flip this assertion to the entries and re-read the case above it', + ).toEqual([]); const omissions = off.findings.filter((f) => f.verdict === 'stale-omission'); const suspended = off.findings.filter((f) => f.verdict === 'unjudgeable-type'); diff --git a/scripts/check-readme-exports.mjs b/scripts/check-readme-exports.mjs index c7891001fc..add472da51 100644 --- a/scripts/check-readme-exports.mjs +++ b/scripts/check-readme-exports.mjs @@ -340,9 +340,17 @@ export const MIN_PARTIAL_REASON = 12; * lives in the README and says "deliberate"; this ledger lives here and says * "drift, owed to a content card". objectui#6214 wired this pin and was * explicitly not allowed to edit README CONTENT, so the drift its first run - * found is written down here with the card number instead of being inherited + * found was written down here with the card number instead of being inherited * silently or fixed in the gate's own PR. * + * EMPTY TODAY, and that is a state and not a retirement. objectui#7302 paid off + * the three entries #6214 opened with: `plugin-gantt`'s `GanttTask` was a + * genuine excerpt and moved to the in-README `PARTIAL_MARKER` above, and + * `plugin-kanban`'s `KanbanColumn` and `KanbanCard` were staleness and were + * brought up to the shipped declarations. ⛔ Do not delete this ledger because + * it holds nothing: the next pin that finds drift it may not fix in its own PR + * writes the entry here, exactly as #6214 did. + * * SHRINK-ONLY, enforced rather than asked for: an entry whose declaration is no * longer in the README, or that no longer omits anything, FAILS as a stale * entry. Adding one is an edit a reviewer sees; removing one happens by fixing @@ -350,15 +358,16 @@ export const MIN_PARTIAL_REASON = 12; * * It suppresses `stale-omission` for that declaration and NOTHING else. A * `fabricated-key` is never excludable here -- see the header. + * + * The annotation is load-bearing while the ledger is empty: without it + * `Object.freeze({})` infers `Readonly<{}>`, `Object.entries` hands the test + * suite `unknown` values, and `pnpm type-check:scripts` fails TS18046 at the + * case that reads each reason. Same spelling, and the same reason, as + * `check-doc-snippet-types.mjs`'s `UNGATED_DOCS`. + * + * @type {Readonly>} */ -export const PARTIAL_EXCERPTS = Object.freeze({ - 'packages/plugin-gantt/README.md::GanttTask': - 'objectui#6214, content fix objectui#7302 -- omits `fields` and `hasOwnDates`, which the prose immediately BELOW the block already names as populated by ObjectGantt itself. Genuinely an excerpt, so it wants the in-README PARTIAL_MARKER rather than this ledger; that one-line README edit was deliberately not made in the gate\'s own PR.', - 'packages/plugin-kanban/README.md::KanbanColumn': - 'objectui#6214, content fix objectui#7302 -- omits `collapsed`. Nothing in the page says the block is partial, so this is staleness, not an excerpt.', - 'packages/plugin-kanban/README.md::KanbanCard': - 'objectui#6214, content fix objectui#7302 -- omits `cardSubtitle`, `cardFieldCells` and `coverImage`. Same page, same class as `KanbanColumn` above: the block reads as the whole card shape and is three keys behind it. objectui#6155 may move this set: these are read from what `@object-ui/plugin-kanban` exports, and that card records four disagreeing declarations of the pair.', -}); +export const PARTIAL_EXCERPTS = Object.freeze({}); /** The NUL that `git ls-files -z` delimits with, built from its code point. */ const NUL = String.fromCharCode(0); From b52027439bf6194529c691251c7f9991011ed282 Mon Sep 17 00:00:00 2001 From: Claude Date: Sun, 6 Sep 2026 06:39:43 +0000 Subject: [PATCH 2/2] test(readme-exports): count marker-declared excerpts in the suspension expectation `Test (shard 2/4)` went red at head 09f1d48 with two assertions in this file expecting 0 and receiving 1. CI's shards run `pnpm install` then `pnpm test` and never build, so the tree they judge is unbuilt -- and an unbuilt tree is the only state in which this can fire. The PR's own verification ran on a built tree, where it cannot. The gate counts BOTH excerpt mechanisms in one census counter, deliberately and in one sentence of its header: a declaration that comes back `unjudgeable-type` suspends the shrink-only rule for its ledger entry AND for its in-README `PARTIAL_MARKER`, and "both are counted as `not judged` in the census". The two assertions derived their expected value from the LEDGER alone -- `Object.keys(PARTIAL_EXCERPTS)` filtered by build state -- which was complete only while every excerpt in the tree was a ledger entry. Moving `GanttTask` to the marker, which is the disposition a genuine excerpt is meant to end at, produced a count neither assertion modelled. So the expectation is widened to the population the counter actually counts, under the same per-PACKAGE rule: `suspendedExcerpts()` returns the ledger entries and the bound markers whose OWN package is unbuilt. The gate is unchanged -- no judgement byte and no census byte moves. Derived from the READMEs, not from the counter under test: the ledger half from the exported literal, the marker half by re-reading each README of an unbuilt package through the gate's own exported grammar (`extractCodeBlocks` + `findPartialMarkers`, keyed `::` per README exactly as the gate keys `excerptFor`). It does not re-parse the block to confirm the named type is declared inside it, because a bound marker whose fence declares no such type is reported `stale-partial-marker` in every build state and the same case asserts there are none. The property both assertions exist for is kept and extended, not relaxed: an unbuilt package is still REPORTED and never silently skipped, and a new line asserts that for the marker route too -- every marker-declared excerpt on an unbuilt package must carry its own `unjudgeable-type` finding, the same requirement the ledger half has always had. Co-Authored-By: Claude Fable 5.1 Claude-Session: https://claude.ai/code/session_01MM7kaS4dPpYHV5BsMyu4tQ --- .../__tests__/check-readme-exports.test.ts | 115 ++++++++++++++++-- 1 file changed, 102 insertions(+), 13 deletions(-) diff --git a/scripts/__tests__/check-readme-exports.test.ts b/scripts/__tests__/check-readme-exports.test.ts index ddae340eae..e64cd41ba0 100644 --- a/scripts/__tests__/check-readme-exports.test.ts +++ b/scripts/__tests__/check-readme-exports.test.ts @@ -925,6 +925,66 @@ function buildState(result: ReturnType) { }; } +/** + * Every DECLARED excerpt in this repository whose own package is UNBUILT — the + * exact population whose shrink-only rule the gate suspends, and therefore the + * exact number `census.excerptsNotJudged` has to come back as. + * + * objectui#7302. This used to be spelled `Object.keys(PARTIAL_EXCERPTS)` + * filtered by build state — the LEDGER half, alone. That was complete only + * while every excerpt in the tree was a ledger entry. The moment a content fix + * moved one to the in-README `PARTIAL_MARKER` — which is the disposition a + * GENUINE excerpt is meant to end at, and the ledger's own reason said so — + * the two assertions below expected 0 where the gate counted 1, and they went + * red on CI: on the UNBUILT tree the shards run, which is the only state this + * can fire in. The gate counts BOTH mechanisms in that one counter, on purpose + * and in one sentence of its header ("BOTH suppress `stale-omission` ... both + * are counted as `not judged` in the census"), so both belong in the + * expectation, under the same per-PACKAGE rule the rest of these cases use. + * + * DERIVED FROM THE READMEs, never from the counter under test: the ledger half + * from the exported literal, the marker half by re-reading each README of an + * unbuilt package through the gate's own exported grammar. So these assertions + * still cross-check the gate against something that is not the gate's count. + * + * The marker half is the population of the gate's `excerptFor` map — a marker + * that BINDS a fence and carries a long enough reason — restricted to unbuilt + * packages, and it deliberately does not re-parse the block to confirm the type + * is declared there: a bound marker whose fence declares no type of that name + * is reported `stale-partial-marker` in EVERY build state, and the case below + * asserts there are none of those. Keyed the way the gate keys it + * (`::`, per README), so a repeated marker collapses here exactly + * as it collapses there. + */ +function suspendedExcerpts( + result: ReturnType, + excerpts: Readonly> = PARTIAL_EXCERPTS, +) { + const { isUnbuilt } = buildState(result); + const ledger = Object.keys(excerpts).filter((key) => + isUnbuilt(packageDirOf(repoRoot, key.split('::')[0])), + ); + /** `::::` -> `::`, the form a finding is at. */ + const markers = new Map(); + for (const record of result.packages) { + if (record.state !== 'unbuilt') continue; + for (const readme of record.readmes as string[]) { + const markdown = fs.readFileSync(path.join(repoRoot, readme), 'utf8'); + for (const marker of findPartialMarkers(markdown, extractCodeBlocks(markdown))) { + if (marker.fence === null || marker.reason.length < MIN_PARTIAL_REASON) continue; + markers.set(`${readme}::${marker.fence}::${marker.name}`, `${readme}::${marker.name}`); + } + } + } + return { + ledger, + markers: [...markers.keys()], + /** Where each suspended marker must be REPORTED, in a finding's `::` form. */ + markerSites: [...new Set(markers.values())], + all: [...ledger, ...markers.keys()], + }; +} + describe('the PARTIAL_EXCERPTS ledger, as it stands in this repository', () => { it('is keyed `::` and every entry carries a card number', () => { for (const [key, reason] of Object.entries(PARTIAL_EXCERPTS)) { @@ -1017,13 +1077,34 @@ describe('the PARTIAL_EXCERPTS ledger, as it stands in this repository', () => { // case cannot see the objectui#6214 regression on its own — the one in the // `repo state` block below, with the ledger ON, is the leg that does. // Stated so the pair is not mistaken for one assertion twice. - expect(off.census.excerptsNotJudged).toBe(0); + // + // NOT zero, and that is objectui#7302's correction. `excerpts: {}` turns + // the LEDGER off, and the ledger is the only half it can turn off: a + // README's `PARTIAL_MARKER` lives in the README, so it still declares an + // excerpt in this scan, and on an unbuilt package it is still suspended and + // still counted. The hard-coded 0 was quietly asserting "this repository + // declares no excerpt by marker" under a title about the ledger, and it + // went red the first time one did — on CI's unbuilt tree, the only build + // state in which the marker half of this counter can be non-zero. + const suspendedOff = suspendedExcerpts(off, {}); + expect( + off.census.excerptsNotJudged, + `with the LEDGER OFF the suspension count is not the number of MARKER-declared excerpts whose own package is unbuilt (${suspendedOff.markers.join(', ') || 'none'}). ${note}`, + ).toBe(suspendedOff.markers.length); + + // ...and every one of those is REPORTED `unjudgeable-type`, exactly as a + // ledger entry is three blocks up. A suspended rule must still FAIL, and + // which mechanism declared the excerpt does not change that. + expect( + suspendedOff.markerSites.filter((key) => !suspended.some((f) => at(f) === key)), + `a marker-declared excerpt whose package is unbuilt was not reported \`unjudgeable-type\` -- a suspended rule must still FAIL. ${note}`, + ).toEqual([]); }); }); describe('repo state — assertions that hold in EVERY build state: built, unbuilt, or a mix', () => { const result = scan(repoRoot); - const { unbuiltDirs, unbuiltNames, isUnbuilt, note } = buildState(result); + const { unbuiltDirs, unbuiltNames, note } = buildState(result); it('walked the tree: READMEs, fenced blocks and import bindings were all found', () => { // These three need no `dist/`, so they assert in the test shards too. @@ -1069,19 +1150,27 @@ describe('repo state — assertions that hold in EVERY build state: built, unbui `a README marker was called stale. ${note}`, ).toEqual([]); - // ...and the number of entries whose shrink-only rule is SUSPENDED is the - // number whose own package is unbuilt: the whole ledger on CI, none of it on - // a built tree, and in between in between. The old form hard-coded - // `Object.keys(PARTIAL_EXCERPTS).length`, which is only the CI answer — it is - // the line that failed `expected +0 to be 3` on a half-built tree, where all - // three ledgered packages happen to be among the ones the doc gate builds. - const suspendable = Object.keys(PARTIAL_EXCERPTS).filter((key) => - isUnbuilt(packageDirOf(repoRoot, key.split('::')[0])), - ); + // ...and the number of DECLARED excerpts whose shrink-only rule is + // SUSPENDED is the number whose own package is unbuilt: all of them on CI, + // none of them on a built tree, and in between in between. Two different + // cards have corrected this one line, and the second is why it reads as it + // does now rather than as a literal: + // + // objectui#7460 it hard-coded `Object.keys(PARTIAL_EXCERPTS).length`, + // which is only the CI answer — the line that failed + // `expected +0 to be 3` on a half-built tree. + // objectui#7302 it then counted only the LEDGER half of the population + // the gate counts. The first in-README `PARTIAL_MARKER` to + // land made it expect 0 against a gate reporting 1, red on + // `Test (shard 2/4)` and green on every built tree. + // + // See `suspendedExcerpts` for why the marker half is derived from the + // README bytes rather than read back off the counter it checks. + const suspended = suspendedExcerpts(result); expect( result.census.excerptsNotJudged, - `the ledger's suspension count is not the number of entries whose own package is unbuilt (${suspendable.join(', ') || 'none'}). ${note}`, - ).toBe(suspendable.length); + `the suspension count is not the number of DECLARED excerpts whose own package is unbuilt (ledger: ${suspended.ledger.join(', ') || 'none'}; marker: ${suspended.markers.join(', ') || 'none'}). ${note}`, + ).toBe(suspended.all.length); // An unbuilt package must be REPORTED, never silently skipped. That is the // rule that makes this gate's never-built CI run FAIL instead of passing