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..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)) { @@ -953,7 +1013,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'); @@ -995,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. @@ -1047,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 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);