From e1651b467a3cf8b1ed1b20690b3e6a3164f54840 Mon Sep 17 00:00:00 2001 From: Claude Date: Mon, 26 Oct 2026 08:54:37 +0000 Subject: [PATCH] chore(ui-tooling): drop two test-only scripts and six CLI aliases nothing runs - ui/scripts/coverage-red-cause-census.mjs, its suspects ledger and test: a one-off census whose own header says no workflow runs it; only its test and one markdown-test-inputs row named it. - ui/scripts/extract-mdx-demos.mjs and its test: a one-off MDX-to-catalog migration helper; only its test and one markdown-test-inputs row named it. - ui/package.json: cli, objectui, doctor, studio, generate and g pointed at packages/cli/dist/cli.js; no workflow, script or document invokes them. `check` stays (root ci.yml runs it); start, site:start, test:unit and test:integration stay (live targets, pinned or documented). Co-authored-by: Claude Claude-Session: https://claude.ai/code/session_012eXtKYmXEnCc1GExP4go12 --- ui/package.json | 6 - .../coverage-red-cause-census.test.ts | 264 ----------- .../__tests__/extract-mdx-demos.test.ts | 265 ----------- ui/scripts/coverage-red-cause-census.mjs | 337 -------------- ui/scripts/coverage-red-cause-suspects.json | 16 - ui/scripts/extract-mdx-demos.mjs | 418 ------------------ ui/scripts/markdown-test-inputs.mjs | 16 - 7 files changed, 1322 deletions(-) delete mode 100644 ui/scripts/__tests__/coverage-red-cause-census.test.ts delete mode 100644 ui/scripts/__tests__/extract-mdx-demos.test.ts delete mode 100644 ui/scripts/coverage-red-cause-census.mjs delete mode 100644 ui/scripts/coverage-red-cause-suspects.json delete mode 100644 ui/scripts/extract-mdx-demos.mjs diff --git a/ui/package.json b/ui/package.json index 206648cfd0c..8bd57b623d0 100644 --- a/ui/package.json +++ b/ui/package.json @@ -103,16 +103,10 @@ "check:metadata-write-doors": "node scripts/check-object-metadata-write-doors.mjs", "check:changeset-claims": "node scripts/check-changeset-claims.mjs", "check:pending-changeset-literals": "node scripts/check-pending-changeset-literals.mjs", - "cli": "node packages/cli/dist/cli.js", - "objectui": "node packages/cli/dist/cli.js", "create-plugin": "node packages/create-plugin/dist/index.js", "docs:api": "typedoc", "docs:check-links": "node scripts/check-doc-links.mjs", - "doctor": "node packages/cli/dist/cli.js doctor", - "studio": "node packages/cli/dist/cli.js studio", "check": "node packages/cli/dist/cli.js check", - "generate": "node packages/cli/dist/cli.js generate", - "g": "node packages/cli/dist/cli.js generate", "shadcn:analyze": "node scripts/component-analysis.js", "shadcn:check": "node scripts/shadcn-sync.js --check", "shadcn:update": "node scripts/shadcn-sync.js --update", diff --git a/ui/scripts/__tests__/coverage-red-cause-census.test.ts b/ui/scripts/__tests__/coverage-red-cause-census.test.ts deleted file mode 100644 index 83d353e1e25..00000000000 --- a/ui/scripts/__tests__/coverage-red-cause-census.test.ts +++ /dev/null @@ -1,264 +0,0 @@ -/** - * ObjectUI - * Copyright (c) 2024-present ObjectStack Inc. - * - * This source code is licensed under the MIT license found in the - * LICENSE file in the root directory of this source tree. - */ - -import { describe, expect, it } from 'vitest'; -import { createHash } from 'node:crypto'; -import fs from 'node:fs'; -import path from 'node:path'; -import { fileURLToPath } from 'node:url'; - -import { - attributeRun, - deliveredCoverageReport, - proximateCause, - shardIndexOf, - shardOf, - shardRange, - specFilesFrom, - summarise, -} from '../coverage-red-cause-census.mjs'; - -const repoRoot = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '../..'); - -/** - * Fixtures are module-scope constants on purpose. Nothing here walks the - * corpus — this suite's whole subject is a census of tests that DID walk the - * corpus from inside `it()`, and the cheapest way not to become its own next - * data point is to keep every input tiny and literal. - */ -const SPEC_SAMPLE = [ - 'packages/core/src/registry.test.ts', - 'packages/react/src/SchemaRenderer.test.tsx', - 'scripts/__tests__/check-doc-links.test.ts', - 'scripts/__tests__/check-unused-dependencies.test.ts', - 'apps/console/src/languageSeed.test.ts', - 'eslint-rules/no-inline-style.test.js', - 'examples/schema-catalog/catalog.test.ts', -]; - -const TRACKED_SAMPLE = [ - ...SPEC_SAMPLE, - 'packages/core/src/registry.ts', - 'README.md', - 'e2e/console-rendering.spec.ts', - 'e2e/live/master-detail.spec.ts', -]; - -const COVERAGE_SHARD_JOB = (shard: number, conclusion: string, failedStep: string | null) => ({ - name: `Test (coverage shard ${shard}/4)`, - conclusion, - steps: [ - { name: 'Checkout code', conclusion: 'success' }, - { name: `Run tests with coverage (shard ${shard}/4)`, conclusion: failedStep === 'run' ? 'failure' : 'success' }, - { name: "Upload this shard's blob report", conclusion: failedStep === 'upload' ? 'failure' : 'success' }, - ], -}); - -describe('the delivery probe is artifact presence, never a run conclusion', () => { - it('reads the coverage-report artifact by name', () => { - expect(deliveredCoverageReport([{ name: 'coverage-report' }, { name: 'coverage-blob-1' }])).toBe(true); - expect(deliveredCoverageReport([{ name: 'coverage-blob-1' }, { name: 'coverage-blob-2' }])).toBe(false); - expect(deliveredCoverageReport([])).toBe(false); - }); - - it('does not consult a conclusion field even when one is offered', () => { - // objectui#6055 measured `conclusion=cancelled` runs that HAD delivered, so - // a probe that peeked at the conclusion would mis-read exactly those. - const artifacts = [{ name: 'coverage-report', conclusion: 'cancelled' }]; - expect(deliveredCoverageReport(artifacts)).toBe(true); - }); -}); - -describe('the shard replica reproduces vitest 4.1.10 arithmetic', () => { - it('spreads the remainder over the leading shards, exactly as calculateShardRange does', () => { - // 7 files over 4 shards: sizes 2,2,2,1 — the remainder (3) takes the first three. - expect(shardRange(7, 1, 4)).toEqual([0, 2]); - expect(shardRange(7, 2, 4)).toEqual([2, 4]); - expect(shardRange(7, 3, 4)).toEqual([4, 6]); - expect(shardRange(7, 4, 4)).toEqual([6, 7]); - }); - - it('partitions the whole spec list with no file lost and none duplicated', () => { - const parts = [1, 2, 3, 4].map((i) => shardOf(SPEC_SAMPLE, i, 4)); - expect(parts.flat().sort()).toEqual([...SPEC_SAMPLE].sort()); - expect(new Set(parts.flat()).size).toBe(SPEC_SAMPLE.length); - }); - - it('orders by the sha1 of the ROOT-RELATIVE path with its leading slash', () => { - // The leading slash is not decoration: vitest hashes - // `resolve(root, moduleId).slice(root.length)`, which always starts with - // `/`. Dropping it yields a different digest and therefore a different - // partition, silently. - const expected = [...SPEC_SAMPLE] - .map((p) => ({ p, h: createHash('sha1').update(`/${p}`).digest('hex') })) - .sort((a, b) => (a.h < b.h ? -1 : 1)) - .map((r) => r.p); - expect([1, 2, 3, 4].flatMap((i) => shardOf(SPEC_SAMPLE, i, 4))).toEqual(expected); - }); - - it('moves a file across shards when the spec list grows, with nobody editing it', () => { - // objectui#8545's structural claim, as arithmetic. Grow the list until the - // target's shard changes; it must change for SOME growth, or the card's - // "a file can move across the bound without anyone editing it" is false. - const target = 'scripts/__tests__/check-doc-links.test.ts'; - const before = shardIndexOf(SPEC_SAMPLE, target); - const shardsSeen = new Set([before]); - for (let extra = 1; extra <= 60; extra += 1) { - const grown = [...SPEC_SAMPLE, ...Array.from({ length: extra }, (_, i) => `packages/new/src/added-${i}.test.ts`)]; - shardsSeen.add(shardIndexOf(grown, target)); - } - expect(shardsSeen.size).toBeGreaterThan(1); - }); -}); - -describe('the spec matcher agrees with the vitest projects it stands in for', () => { - it('keeps every vitest spec extension and drops the playwright tree', () => { - expect(specFilesFrom(TRACKED_SAMPLE)).toEqual(SPEC_SAMPLE); - }); - - it('drops non-spec sources outright', () => { - expect(specFilesFrom(['packages/core/src/registry.ts', 'README.md'])).toEqual([]); - }); -}); - -describe('the proximate reading separates a red suite from a broken runner', () => { - it('names the failing shard and calls a non-zero vitest what it is', () => { - expect(proximateCause([COVERAGE_SHARD_JOB(2, 'failure', 'run'), COVERAGE_SHARD_JOB(1, 'success', null)])).toEqual({ - failingShards: [2], - kinds: ['vitest-nonzero'], - }); - }); - - it('does not call a failed artifact upload a test failure', () => { - // The tests in that shard ran green; only the upload died. Counting it as a - // red suite would put an infrastructure fault into the mechanism's column. - expect(proximateCause([COVERAGE_SHARD_JOB(4, 'failure', 'upload')])).toEqual({ - failingShards: [4], - kinds: ['blob-upload'], - }); - }); - - it('reports a cancelled shard without claiming any shard failed', () => { - expect(proximateCause([{ ...COVERAGE_SHARD_JOB(3, 'cancelled', null), conclusion: 'cancelled' }])).toEqual({ - failingShards: [], - kinds: ['run-cancelled'], - }); - }); - - it('ignores jobs that are not coverage shards', () => { - expect(proximateCause([{ name: 'Type Check', conclusion: 'failure', steps: [] }])).toEqual({ - failingShards: [], - kinds: [], - }); - }); - - it('reads a coverage shard at any width, not only the 4-way runs it was written for (objectui#12256)', () => { - const eightWay = { - name: 'Test (coverage shard 7/8)', - conclusion: 'failure', - steps: [{ name: 'Run tests with coverage (shard 7/8)', conclusion: 'failure' }], - }; - expect(proximateCause([eightWay])).toEqual({ failingShards: [7], kinds: ['vitest-nonzero'] }); - }); -}); - -describe('attribution requires BOTH the live window and the computed shard', () => { - const suspects = [ - { name: 'doc-links', path: 'scripts/__tests__/check-doc-links.test.ts', liveFrom: '2026-09-04T15:38:06Z', liveUntil: '2026-09-07T21:59:50Z' }, - ]; - const base = { - createdAt: '2026-09-06T04:33:09Z', - failingShards: [2], - suspectShard: { 'scripts/__tests__/check-doc-links.test.ts': 2 }, - }; - - it('attributes when the failing shard is the shard the suspect computes into', () => { - expect(attributeRun(base, suspects)).toEqual(['doc-links']); - }); - - it('refuses when a different shard failed — the in-window control', () => { - expect(attributeRun({ ...base, failingShards: [3] }, suspects)).toEqual([]); - }); - - it('refuses outside the live window even when the shard matches — the out-of-window control', () => { - // This is the control that makes the census a measurement rather than a - // story: after the hoist the same shard keeps failing sometimes, and those - // runs must NOT be swept into the mechanism's column. - expect(attributeRun({ ...base, createdAt: '2026-09-09T04:33:09Z' }, suspects)).toEqual([]); - }); - - it('refuses when the suspect could not be placed on that tree', () => { - expect(attributeRun({ ...base, suspectShard: { 'scripts/__tests__/check-doc-links.test.ts': null } }, suspects)).toEqual([]); - }); -}); - -describe('the summary counts MERGES, not runs', () => { - const suspects = [ - { name: 'doc-links', path: 'scripts/__tests__/check-doc-links.test.ts', liveFrom: '2026-09-04T00:00:00Z', liveUntil: '2026-09-08T00:00:00Z' }, - ]; - const shard2 = { 'scripts/__tests__/check-doc-links.test.ts': 2 }; - - it('splits delivered from red and buckets the red by cause', () => { - const snapshot = { - runs: [ - { sha: 'aaa', delivered: true, createdAt: '2026-09-05T00:00:00Z', failingShards: [], suspectShard: shard2 }, - { sha: 'bbb', delivered: false, createdAt: '2026-09-05T01:00:00Z', failingShards: [2], suspectShard: shard2 }, - { sha: 'ccc', delivered: false, createdAt: '2026-09-05T02:00:00Z', failingShards: [2], suspectShard: shard2 }, - { sha: 'ddd', delivered: false, createdAt: '2026-09-05T03:00:00Z', failingShards: [3], suspectShard: shard2 }, - ], - }; - expect(summarise(snapshot, suspects)).toEqual({ - runs: 4, - merges: 4, - supersededRuns: 0, - delivered: 1, - red: 3, - redShare: 0.75, - byCause: { 'doc-links': 2, unattributed: 1 }, - }); - }); - - it('does not count a superseded run as a red merge when the same sha delivered', () => { - // objectui#6049's per-sha concurrency group cancels the earlier run and - // starts a fresh one on the SAME head sha. The cancelled one has no - // artifact; the commit still got its merged report. Counting runs here is - // what made this census disagree with objectui#6055's published figures. - const snapshot = { - runs: [ - { sha: 'eee', delivered: false, createdAt: '2026-09-05T04:00:00Z', failingShards: [], suspectShard: shard2 }, - { sha: 'eee', delivered: true, createdAt: '2026-09-05T04:00:30Z', failingShards: [], suspectShard: shard2 }, - ], - }; - const out = summarise(snapshot, suspects); - expect(out).toMatchObject({ runs: 2, merges: 1, supersededRuns: 1, red: 0, delivered: 1 }); - }); - - it('keeps a sha red when none of its runs delivered', () => { - const snapshot = { - runs: [ - { sha: 'fff', delivered: false, createdAt: '2026-09-05T05:00:00Z', failingShards: [], suspectShard: shard2 }, - { sha: 'fff', delivered: false, createdAt: '2026-09-05T05:00:30Z', failingShards: [2], suspectShard: shard2 }, - ], - }; - expect(summarise(snapshot, suspects)).toMatchObject({ merges: 1, red: 1, byCause: { 'doc-links': 1 } }); - }); -}); - -describe('the suspects ledger stays readable by the script that consumes it', () => { - it('declares a path, a live window and the provenance of both bounds', () => { - const ledger = JSON.parse(fs.readFileSync(path.join(repoRoot, 'scripts/coverage-red-cause-suspects.json'), 'utf8')); - expect(ledger.length).toBeGreaterThan(0); - for (const entry of ledger) { - expect(typeof entry.name).toBe('string'); - expect(entry.path).toMatch(/\.test\.tsx?$/); - expect(entry.liveFrom < entry.liveUntil).toBe(true); - // ⛔ A window with no stated provenance is a number nobody can re-derive. - expect(entry.why.length).toBeGreaterThan(40); - } - }); -}); diff --git a/ui/scripts/__tests__/extract-mdx-demos.test.ts b/ui/scripts/__tests__/extract-mdx-demos.test.ts deleted file mode 100644 index c257ca6da3c..00000000000 --- a/ui/scripts/__tests__/extract-mdx-demos.test.ts +++ /dev/null @@ -1,265 +0,0 @@ -import { describe, expect, it } from 'vitest'; -import fs from 'node:fs'; -import path from 'node:path'; -import { fileURLToPath } from 'node:url'; - -// Plain-JS CLI helper. Its types are INFERRED from the .mjs source by -// `tsconfig.scripts.json` (`allowJs`), so no `@ts-expect-error` here — see the -// header of that file and objectui#3494. -import { DEMO_MODULE, findDemoImports, rewriteDemoImports, stripCode } from '../extract-mdx-demos.mjs'; - -/** - * objectui#3788 — the migration script rewrote the demo import by MATCHING ONE - * SPELLING of the statement instead of by reading what the migrated file still - * uses. - * - * `scripts/extract-mdx-demos.mjs` turns `` / `` - * blocks into ``, then swapped the file's import. The swap - * was this regex: - * - * /import\s*\{\s*InteractiveDemo[^}]*\}\s*from\s*['"]@\/app\/components\/ComponentDemo['"];?/ - * - * `\{\s*InteractiveDemo` anchors on the FIRST specifier being `InteractiveDemo`. - * Every file written as `import { ComponentDemo, DemoGrid } from '…'` therefore - * had its content migrated and its import left untouched, and the residue piled - * up: measured on `origin/main` @ `0af9826ab`, 106 files under `content/docs/` - * carried such an import, 84 of them using neither name and 22 using only - * `DemoGrid`. Nothing broke — an unused MDX import is inert — which is precisely - * why it grew unseen for a whole migration wave. - * - * `rewriteDemoImports` decides on USAGE. What this file pins: - * - * 1. The mixed-import case in all three of its outcomes — none / one / both of - * the imported names surviving. All-or-nothing is the wrong shape: 22 real - * files keep `DemoGrid` while losing `ComponentDemo`, so a rewriter that only - * knew "drop the line" or "keep the line" would be wrong 22 times. - * 2. **The red control**: the ORIGINAL regex, verbatim, against the same - * fixture. It is what proves the spelling match — not something else — was - * the cause, and it is what would go green again if someone reverted the fix - * without noticing these tests. - * 3. That a name mentioned only inside a code fence is NOT a use. Docs pages - * print component tags as sample code constantly; counting those would keep - * dead imports alive under a rule that reads as if it were checking usage. - */ - -const repoRoot = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '../..'); - -/** The rewrite `main()` used before this fix — kept as the red control, verbatim. */ -const ORIGINAL_IMPORT_SWAP = - /import\s*\{\s*InteractiveDemo[^}]*\}\s*from\s*['"]@\/app\/components\/ComponentDemo['"];?/; - -const originalRewrite = (mdx: string): string => - mdx.replace(ORIGINAL_IMPORT_SWAP, `import { SchemaExample } from '${DEMO_MODULE}';`); - -/** An MDX page in the shape every file under `content/docs/` uses. */ -const page = (importLine: string, body: string): string => - ['---', 'title: "Sheet"', '---', '', importLine, '', body, ''].join('\n'); - -/** - * The card's specimen, post-migration: content already migrated to - * ``, import still spelled the way the old regex could not see. - * `content/docs/layout/page-header.mdx:6` was the live instance. - */ -const MIXED_IMPORT = `import { ComponentDemo, DemoGrid } from '${DEMO_MODULE}';`; - -describe('rewriteDemoImports — the mixed-import case objectui#3788 is about', () => { - it('drops the whole statement when the migrated file uses neither name', () => { - const before = page(MIXED_IMPORT, ''); - const after = rewriteDemoImports(before); - - expect(after).not.toContain(DEMO_MODULE); - // The import took its own blank line with it, leaving the `---\n\n` - // shape an import-less page has. - expect(after).toBe(['---', 'title: "Sheet"', '---', '', '', ''].join('\n')); - }); - - it('keeps ONLY the name still used — the 22-file partial case, not all-or-nothing', () => { - const before = page(MIXED_IMPORT, '\n\n'); - const after = rewriteDemoImports(before); - - expect(after).toContain(`import { DemoGrid } from '${DEMO_MODULE}';`); - expect(after).not.toContain('ComponentDemo,'); - // `ComponentDemo` survives nowhere but the module specifier itself. - expect(after.replace(DEMO_MODULE, '')).not.toContain('ComponentDemo'); - }); - - it('leaves a statement whose every name is still used exactly as written', () => { - const before = page(MIXED_IMPORT, '\n\n'); - expect(rewriteDemoImports(before)).toBe(before); - }); - - it('does not count the import statement itself as a use of what it imports', () => { - // The trap a naive `mdx.includes('ComponentDemo')` falls into: the statement - // names both bindings AND the module, so every import would look "used". - const before = page(MIXED_IMPORT, 'Prose only.'); - expect(rewriteDemoImports(before)).not.toContain('import'); - }); -}); - -describe('the original spelling-matched regex — the red that proves the cause', () => { - it('leaves the stale import behind on the very fixture the fix prunes', () => { - const before = page(MIXED_IMPORT, ''); - - // RED: the old rewrite cannot see this statement at all, so it survives the - // migration verbatim — one dead import per migrated page, ~106 of them. - expect(originalRewrite(before)).toContain(MIXED_IMPORT); - expect(ORIGINAL_IMPORT_SWAP.test(before)).toBe(false); - - // GREEN: the usage-driven rewrite removes it. - expect(rewriteDemoImports(before)).not.toContain(DEMO_MODULE); - }); - - it('handled only the one spelling it was anchored on', () => { - const before = page(`import { InteractiveDemo } from '${DEMO_MODULE}';`, ''); - // The single case the old regex did cover — kept so the fix is shown to be a - // widening, not a swap of one blind spot for another. - expect(originalRewrite(before)).toContain(`import { SchemaExample } from '${DEMO_MODULE}';`); - expect(rewriteDemoImports(before, { ensure: ['SchemaExample'] })).toContain( - `import { SchemaExample } from '${DEMO_MODULE}';`, - ); - }); -}); - -describe('rewriteDemoImports — `ensure`', () => { - it('adds a name the migrated body uses but no statement declares', () => { - // What `main()` passes: the migration has just inserted `` - // tags, so the import must gain the binding the old regex used to swap in. - const before = page(MIXED_IMPORT, ''); - expect(rewriteDemoImports(before, { ensure: ['SchemaExample'] })).toContain( - `import { SchemaExample } from '${DEMO_MODULE}';`, - ); - }); - - it('never adds an unused name — that would be objectui#3788 in a new spelling', () => { - const before = page(MIXED_IMPORT, 'Prose only, no demo blocks were migrated.'); - expect(rewriteDemoImports(before, { ensure: ['SchemaExample'] })).not.toContain('import'); - }); -}); - -describe('rewriteDemoImports — usage detection', () => { - it('does not treat a tag printed inside a code fence as a use', () => { - const before = page( - `import { ComponentDemo } from '${DEMO_MODULE}';`, - ['```plaintext', '', '```'].join('\n'), - ); - expect(rewriteDemoImports(before)).not.toContain(DEMO_MODULE); - }); - - it('does not treat an inline code span as a use', () => { - const before = page(`import { DemoGrid } from '${DEMO_MODULE}';`, 'Wrap them in `` to lay them out.'); - expect(rewriteDemoImports(before)).not.toContain(DEMO_MODULE); - }); - - it('reads the LOCAL name of a renamed specifier, not the imported one', () => { - const before = page(`import { InteractiveDemo as Demo } from '${DEMO_MODULE}';`, ''); - expect(rewriteDemoImports(before)).toContain(`import { InteractiveDemo as Demo } from '${DEMO_MODULE}';`); - - const dead = page(`import { InteractiveDemo as Demo } from '${DEMO_MODULE}';`, ''); - expect(rewriteDemoImports(dead)).not.toContain(DEMO_MODULE); - }); - - it('matches whole tag names only', () => { - const before = page(`import { DemoGrid } from '${DEMO_MODULE}';`, ''); - expect(rewriteDemoImports(before)).not.toContain(DEMO_MODULE); - }); - - it('collapses several statements from the module into the surviving one', () => { - const before = [ - '---', - '---', - '', - `import { ComponentDemo } from '${DEMO_MODULE}';`, - '', - `import { DemoGrid } from '${DEMO_MODULE}';`, - '', - '', - '', - ].join('\n'); - const after = rewriteDemoImports(before); - expect(findDemoImports(after)).toHaveLength(1); - expect(after).toContain(`import { DemoGrid } from '${DEMO_MODULE}';`); - }); - - it('is a no-op on a file that never imported the module', () => { - const before = ['---', '---', '', '', ''].join('\n'); - expect(rewriteDemoImports(before)).toBe(before); - }); -}); - -describe('findDemoImports / stripCode', () => { - it('reports each specifier with its imported and local name', () => { - const [found] = findDemoImports(page(`import { InteractiveDemo as Demo, DemoGrid } from '${DEMO_MODULE}';`, 'x')); - expect(found.specifiers).toEqual([ - { imported: 'InteractiveDemo', local: 'Demo', source: 'InteractiveDemo as Demo' }, - { imported: 'DemoGrid', local: 'DemoGrid', source: 'DemoGrid' }, - ]); - }); - - it('blanks fenced blocks and inline spans, and leaves real JSX alone', () => { - const stripped: string = stripCode(['', '', '```tsx', '', '```', '', 'and ``'].join('\n')); - expect(stripped).toContain(''); - expect(stripped).not.toContain(''); - expect(stripped).not.toContain(''); - }); -}); - -/** - * The ratchet for the cleanup half. - * - * Deliberately an INDEPENDENT scan rather than a call to `rewriteDemoImports`: - * the cleanup commit was produced by that function, so re-running it here would - * only assert it agrees with its own output. This walks the tree and re-derives - * the verdict, which is what makes it able to disagree — and it catches a dead - * import that arrives by hand as well as one minted by the script. - * - * Both implementations were run against `origin/main` @ `0af9826ab` before the - * cleanup and returned the same partition: 84 statements fully dead, 22 with one - * of two names dead, 11 already clean. - */ -describe('content/docs carries no dead demo import — the cleanup half of objectui#3788', () => { - const docsDir = path.join(repoRoot, 'content/docs'); - - const mdxFiles = (dir: string, out: string[] = []): string[] => { - for (const entry of fs.readdirSync(dir, { withFileTypes: true })) { - const full = path.join(dir, entry.name); - if (entry.isDirectory()) mdxFiles(full, out); - else if (entry.name.endsWith('.mdx')) out.push(full); - } - return out; - }; - - const IMPORT_LINE = /^[ \t]*import\s*\{([^}]*)\}\s*from\s*['"]@\/app\/components\/ComponentDemo['"]\s*;?[ \t]*$/gm; - - it('scans a non-trivial number of pages, so a green cannot mean "found nothing"', () => { - expect(mdxFiles(docsDir).length).toBeGreaterThan(100); - }); - - it('every named import from the demo module is rendered somewhere on its page', () => { - const dead: string[] = []; - for (const file of mdxFiles(docsDir)) { - const source = fs.readFileSync(file, 'utf8'); - IMPORT_LINE.lastIndex = 0; - const statements = [...source.matchAll(IMPORT_LINE)]; - if (statements.length === 0) continue; - - // Judge against the body: strip the import statements, then fenced blocks - // and inline code spans (a printed sample tag is documentation, not a use). - const body = source - .replace(IMPORT_LINE, '') - .replace(/^([ \t]*)(`{3,}|~{3,})[^\n]*\n[\s\S]*?^[ \t]*\2[^\n]*$/gm, '') - .replace(/`[^`\n]*`/g, ''); - - for (const statement of statements) { - for (const raw of statement[1].split(',')) { - const specifier = raw.trim(); - if (!specifier) continue; - const local = specifier.includes(' as ') ? specifier.split(/\s+as\s+/)[1].trim() : specifier; - if (!new RegExp(String.raw`<\/?${local}\b`).test(body)) { - dead.push(`${path.relative(repoRoot, file)}: ${local}`); - } - } - } - } - expect(dead).toEqual([]); - }); -}); diff --git a/ui/scripts/coverage-red-cause-census.mjs b/ui/scripts/coverage-red-cause-census.mjs deleted file mode 100644 index 37f560e602c..00000000000 --- a/ui/scripts/coverage-red-cause-census.mjs +++ /dev/null @@ -1,337 +0,0 @@ -#!/usr/bin/env node -/** - * ObjectUI - * Copyright (c) 2024-present ObjectStack Inc. - * - * This source code is licensed under the MIT license found in the - * LICENSE file in the root directory of this source tree. - */ - -/** - * Census: why the push lane's coverage gate was red, run by run — objectui#8545. - * - * ⚠️ This is a ONE-OFF CENSUS, not a gate. It is deliberately NOT wired into any - * workflow, it never fails a build, and nothing in CI calls it. It exists so the - * numbers objectui#8545 publishes stay REPRODUCIBLE from a file instead of being - * claims about a deleted scratch script. ⛔ Do not add it to `.github/workflows` - * without answering, on a card, what it would be gating. - * - * Run: - * NODE_USE_ENV_PROXY=1 node scripts/coverage-red-cause-census.mjs --fetch --since=2026-09-02 > snap.json - * node scripts/coverage-red-cause-census.mjs --snapshot=snap.json - * - * `NODE_USE_ENV_PROXY=1` is not decoration on a proxied host: Node's built-in - * `fetch` ignores `HTTPS_PROXY`, so `--fetch` answers 403 on the very first - * page and the 403 reads exactly like a permissions problem. Measured on Node - * v22.22.2. The `--snapshot` path needs no network at all. - * - * --------------------------------------------------------------------------- - * THE QUESTION - * --------------------------------------------------------------------------- - * - * objectui#6055 measured that a large share of push-lane merges were red at the - * coverage gate, and said plainly that this was a CORRELATION between two - * measurements — it never classified those failures by cause. This file is that - * classification. - * - * --------------------------------------------------------------------------- - * THE DELIVERY PROBE — artifact presence, ⛔ never a run-level conclusion - * --------------------------------------------------------------------------- - * - * A run is "red at the coverage gate" when the `coverage-report` artifact is - * ABSENT from it. objectui#6055 re-confirmed independently that a run's - * `conclusion` is NOT a delivery proxy: it found `conclusion=cancelled` runs - * that had nevertheless delivered the artifact. `deliveredCoverageReport()` - * below is therefore the only admissible probe, and `conclusion` is carried in - * the snapshot for reporting only. - * - * --------------------------------------------------------------------------- - * WHY THE CAUSE CANNOT BE READ OFF A JOB LOG — the resolution limit, stated - * --------------------------------------------------------------------------- - * - * The push lane runs `pnpm test:coverage --reporter=blob …` (ci.yml, the - * `test-coverage` job). `--reporter=blob` REPLACES the default reporter set, so - * a failing shard's job log contains no failing-test names, no assertion text - * and no `Test timed out in 15000ms` line: it goes from whatever the tests - * themselves printed on stdout straight to `blob report written to …` and - * `ELIFECYCLE Command failed with exit code 1`. The failures do exist, inside - * the `coverage-blob-N` artifact — which is downloadable only, not readable - * through the API. - * - * ⇒ per-run failing-test identity is NOT recoverable from CI. This census - * therefore classifies by the two readings that ARE recoverable, and says so - * rather than inferring a cause from "it was red and something was slow": - * - * (1) WHICH SHARD failed, paired with WHICH SHARD a named suspect file - * computes into on that exact tree — `shardOf()` below is a replica of - * vitest's own sequencer, so this is arithmetic on the tree, not a guess. - * (2) WHETHER THE SAME TREE PASSED UNINSTRUMENTED, from the `merge_group` - * lane, which runs the same 4-way split over the same files without - * `--coverage` (ci.yml gave `test` `if: github.event_name != 'push'` - * when this was written; since objectui#12214 a `merge_group` run - * carries only the affected set, and the uninstrumented whole suite - * runs on `push` and `schedule` beside the coverage lane). - * - * ⚠️ Reading (2) has a selection trap that has to be named: a commit only lands - * if its merge_group run went green, so "every landed sha had a green - * merge_group run" is TAUTOLOGICAL and proves nothing on its own. What is NOT - * tautological is the merge_group runs that FAILED — those trees never landed, - * and they are in the data. Comparing the per-shard failure RATE of the two - * lanes over the same period is the reading that carries weight. - */ - -import { createHash } from 'node:crypto'; -import { execFileSync } from 'node:child_process'; -import { readFile } from 'node:fs/promises'; - -import { isEntrypoint } from './invoked-as.mjs'; - -const API = 'https://api.github.com/repos'; - -/** Files vitest treats as specs, from a tracked-path list. */ -const SPEC_RE = /\.(test|spec)\.(ts|tsx|js|mjs|cts|mts)$/; - -/** - * The spec set of a tree, from `git ls-tree -r --name-only ` output. - * - * `e2e/` is excluded because those are Playwright specs, reached by - * `playwright.config.ts` and by no vitest project. The rule is not assumed: - * against `pnpm exec vitest list --filesOnly` on 6214db63f it reproduces the - * spec set EXACTLY — 2998 paths, zero in either direction — and the 31 paths - * the raw regex adds are all and only `e2e/`. - */ -export function specFilesFrom(trackedPaths) { - return trackedPaths.filter((p) => SPEC_RE.test(p) && !p.startsWith('e2e/')); -} - -/** - * Replica of vitest's `BaseSequencer.calculateShardRange` (vitest 4.1.10). - * Kept as its own export so the test can pin it against the upstream algebra - * rather than only through `shardOf`. - */ -export function shardRange(filesCount, index, count) { - const base = Math.floor(filesCount / count); - const remainder = filesCount % count; - if (remainder >= index) { - const size = base + 1; - return [size * (index - 1), size * index]; - } - const start = remainder * (base + 1) + (index - remainder - 1) * base; - return [start, start + base]; -} - -/** - * Replica of vitest's `BaseSequencer.shard` (vitest 4.1.10): sha1 of the spec - * path relative to the config root (which always begins with `/`), sorted by - * that hex digest, then sliced by `shardRange`. - * - * VALIDATED, not asserted: computed against a real - * `pnpm exec vitest run --shard=3/4 --project @object-ui/console` on 6214db63f, - * the two file sets are identical — 24 of 24, empty in both directions. - * - * ⚠️ Shard membership is a function of the WHOLE spec list, so it moves when - * files are added anywhere in the repository. That is objectui#8545's own - * point: a file can cross a shard boundary with nobody editing it. - * - * ⚠️ This replicates vitest's DEFAULT split, which describes the runs this - * census was written for. Since objectui#12208 the root config's sequencer - * (`scripts/test-shard-sequencer.mjs`) splits by measured duration whenever - * `scripts/test-shard-timings.json` holds a measurement, so for a run on a - * tree whose dataset is non-empty this replica no longer names the shard a - * file ran in. - */ -export function shardOf(specPaths, index, count) { - const rows = specPaths.map((p) => ({ p, h: createHash('sha1').update(`/${p}`).digest('hex') })); - rows.sort((a, b) => (a.h < b.h ? -1 : a.h > b.h ? 1 : 0)); - const [start, end] = shardRange(rows.length, index, count); - return rows.slice(start, end).map((r) => r.p); -} - -/** Which 1-based shard of `count` a single spec path lands in, or null. */ -export function shardIndexOf(specPaths, target, count = 4) { - for (let i = 1; i <= count; i += 1) if (shardOf(specPaths, i, count).includes(target)) return i; - return null; -} - -/** The delivery probe. ⛔ Never read `conclusion` for this. */ -export function deliveredCoverageReport(artifacts) { - return artifacts.some((a) => a.name === 'coverage-report'); -} - -/** - * The proximate reading: which coverage shards failed, and at which STEP. - * The step matters — a shard that died in `Upload this shard's blob report` - * ran its tests green and is not a test failure at all. - */ -export function proximateCause(jobs) { - const shards = new Set(); - const kinds = new Set(); - for (const job of jobs) { - // Any width: the runs this census reads were 4-way, and objectui#12256 - // made the lane 8-way. A width-pinned pattern would read every newer run - // as having no coverage shard at all, which is a silent zero. - const match = /^Test \(coverage shard (\d+)\/\d+\)$/.exec(job.name); - if (!match) continue; - if (job.conclusion === 'cancelled') { - kinds.add('run-cancelled'); - continue; - } - if (job.conclusion !== 'failure') continue; - shards.add(Number(match[1])); - const failed = (job.steps ?? []).filter((s) => s.conclusion === 'failure').map((s) => s.name); - if (failed.some((n) => n.startsWith('Run tests with coverage'))) kinds.add('vitest-nonzero'); - else if (failed.some((n) => n.startsWith("Upload this shard's blob report"))) kinds.add('blob-upload'); - else kinds.add('runner-setup'); - } - return { failingShards: [...shards].sort(), kinds: [...kinds].sort() }; -} - -/** - * Attribute one red run to a named un-hoisted corpus-walk suspect. - * - * A suspect is `{ name, path, liveFrom, liveUntil }` — the window in which its - * walk sat inside the timed assertion window on `main`. A run is attributed when - * it is inside that window AND the shard the suspect computes into on that run's - * own tree is one of the shards that actually failed. - * - * ⚠️ This is an attribution, ⛔ not an observation of the failing test: see the - * resolution-limit section in the header. Its strength comes from the controls - * the census prints alongside it — the same test applied OUTSIDE each suspect's - * live window, where it must and does stop matching. - */ -export function attributeRun(run, suspects) { - const failing = new Set(run.failingShards.map(String)); - return suspects - .filter((s) => run.createdAt >= s.liveFrom && run.createdAt < s.liveUntil) - .filter((s) => run.suspectShard?.[s.path] != null && failing.has(String(run.suspectShard[s.path]))) - .map((s) => s.name); -} - -/** `git ls-tree` for one sha, as a tracked-path list. */ -export function trackedPathsAt(sha, cwd = process.cwd()) { - return execFileSync('git', ['ls-tree', '-r', '--name-only', sha], { cwd, encoding: 'utf8', maxBuffer: 64e6 }) - .split('\n') - .filter(Boolean); -} - -async function getJson(url) { - const res = await fetch(url); - if (!res.ok) throw new Error(`${res.status} ${res.statusText} for ${url}`); - return res.json(); -} - -/** - * Page with explicit `&page=N` to a short page. ⛔ Not `Link: rel="next"`: - * objectui#6055 measured that cursor walking exhausts early and under-counts the - * population by about a quarter, which is how that card's own baseline - * denominator came out short. - */ -async function listRuns(repo, event, since) { - const rows = []; - for (let page = 1; page <= 20; page += 1) { - const url = `${API}/${repo}/actions/workflows/ci.yml/runs?event=${event}&status=completed&per_page=100&page=${page}`; - const body = await getJson(url); - const batch = body.workflow_runs ?? []; - rows.push(...batch); - if (batch.length < 100) break; - if (batch[batch.length - 1].created_at < since) break; - } - return rows.filter((r) => r.created_at >= since); -} - -async function fetchSnapshot(repo, since) { - const push = await listRuns(repo, 'push', since); - const mergeGroup = await listRuns(repo, 'merge_group', since); - const runs = []; - for (const run of push) { - const [{ artifacts = [] }, { jobs = [] }] = await Promise.all([ - getJson(`${API}/${repo}/actions/runs/${run.id}/artifacts?per_page=100`), - getJson(`${API}/${repo}/actions/runs/${run.id}/jobs?per_page=100`), - ]); - runs.push({ - id: run.id, - sha: run.head_sha, - createdAt: run.created_at, - conclusion: run.conclusion, - actor: run.actor?.login ?? null, - delivered: deliveredCoverageReport(artifacts), - ...proximateCause(jobs), - }); - } - return { repo, since, generatedAt: new Date().toISOString(), runs, mergeGroup: mergeGroup.map((r) => ({ id: r.id, sha: r.head_sha, createdAt: r.created_at, conclusion: r.conclusion })) }; -} - -/** - * Summary the card reports: denominator, delivery split, cause split. - * - * ⚠️ THE UNIT IS A MERGE, NOT A RUN, and the difference is not pedantic. The - * per-sha push concurrency group (objectui#6049) cancels a superseded run and - * starts a fresh one on the SAME head sha seconds later; the cancelled one has - * no `coverage-report` artifact and so reads as red, while the commit itself - * got its merged report moments afterwards. Counting runs therefore - * OVER-counts red. Measured over 2026-09-02..09-11 the gap is 4 runs in 700 — - * small, and exactly the 4 that made this census's own first draft disagree - * with objectui#6055's published 454/301 by two in each column. Collapsed by - * sha, both numbers land on objectui#6055's to the unit. - * - * A merge is delivered when ANY of its runs delivered. A red merge's cause is - * taken from its runs that failed. - */ -export function summarise(snapshot, suspects) { - const bySha = new Map(); - for (const run of snapshot.runs) { - if (!bySha.has(run.sha)) bySha.set(run.sha, []); - bySha.get(run.sha).push(run); - } - const byCause = new Map(); - let red = 0; - for (const runs of bySha.values()) { - if (runs.some((r) => r.delivered)) continue; - red += 1; - const names = [...new Set(runs.flatMap((r) => attributeRun(r, suspects)))].sort(); - const key = names.length ? names.join(' + ') : 'unattributed'; - byCause.set(key, (byCause.get(key) ?? 0) + 1); - } - return { - runs: snapshot.runs.length, - merges: bySha.size, - supersededRuns: snapshot.runs.length - bySha.size, - delivered: bySha.size - red, - red, - redShare: red / Math.max(1, bySha.size), - byCause: Object.fromEntries([...byCause].sort((a, b) => b[1] - a[1])), - }; -} - -async function main() { - const arg = (name, fallback) => { - const hit = process.argv.find((a) => a.startsWith(`--${name}=`)); - return hit ? hit.slice(name.length + 3) : fallback; - }; - const repo = arg('repo', 'objectstack-ai/objectui'); - const since = arg('since', '2026-09-02'); - - if (process.argv.includes('--fetch')) { - process.stdout.write(`${JSON.stringify(await fetchSnapshot(repo, since))}\n`); - return; - } - - const file = arg('snapshot', null); - if (!file) { - // ⛔ Refusing to answer over an empty input: a census nobody fed must not - // read as "nothing was red". - process.stderr.write('Pass --snapshot=, or --fetch to build one.\n'); - process.exitCode = 1; - return; - } - const snapshot = JSON.parse(await readFile(file, 'utf8')); - const suspects = JSON.parse(await readFile(arg('suspects', 'scripts/coverage-red-cause-suspects.json'), 'utf8')); - for (const run of snapshot.runs) { - if (run.delivered || run.suspectShard) continue; - const specs = specFilesFrom(trackedPathsAt(run.sha)); - run.suspectShard = Object.fromEntries(suspects.map((s) => [s.path, shardIndexOf(specs, s.path)])); - } - process.stdout.write(`${JSON.stringify(summarise(snapshot, suspects), null, 2)}\n`); -} - -if (isEntrypoint(import.meta.url)) await main(); diff --git a/ui/scripts/coverage-red-cause-suspects.json b/ui/scripts/coverage-red-cause-suspects.json deleted file mode 100644 index be0a8629f89..00000000000 --- a/ui/scripts/coverage-red-cause-suspects.json +++ /dev/null @@ -1,16 +0,0 @@ -[ - { - "name": "doc-links corpus walk (objectui#8419)", - "path": "scripts/__tests__/check-doc-links.test.ts", - "liveFrom": "2026-09-04T15:38:06Z", - "liveUntil": "2026-09-07T21:59:50Z", - "why": "liveFrom is the push run of f7cf7e8a9f 'check-doc-links resolves the #fragment, not just the file (objectui#7644) (#7657)', the change that grew the walk; liveUntil is the push run of 0203a29e9b, the hoist. Both bounds are push-run created_at, not commit time, because the census is indexed by push run." - }, - { - "name": "whole-repository scan (objectui#8479 / objectui#8380)", - "path": "scripts/__tests__/check-unused-dependencies.test.ts", - "liveFrom": "2026-09-07T09:52:35Z", - "liveUntil": "2026-09-08T09:25:57Z", - "why": "liveFrom is the first push run in the window whose failing shard set contains the shard this file computes into; ⛔ it is an OBSERVED onset, not a landing commit — no commit in the window edits this file before the fix, which is objectui#8545's own point that a file can cross the bound with nobody editing it. liveUntil is the push run of 83d4fda160, the hoist." - } -] diff --git a/ui/scripts/extract-mdx-demos.mjs b/ui/scripts/extract-mdx-demos.mjs deleted file mode 100644 index d71c68518f6..00000000000 --- a/ui/scripts/extract-mdx-demos.mjs +++ /dev/null @@ -1,418 +0,0 @@ -#!/usr/bin/env node -/** - * extract-mdx-demos.mjs - * - * Scans an MDX file for `` blocks, evaluates each - * `schema={{ ... }}` JS-object literal, and: - * - * 1. Writes the schema to `examples/schema-catalog/src/schemas//.json` - * 2. Emits a draft MDX replacement to `.migrated` that uses - * `` instead. - * 3. Prints a registry-entry snippet to stdout that should be pasted into - * `examples/schema-catalog/src/index.ts`. - * - * The MDX `` is NOT modified in place; you review `.migrated` then `mv`. - * - * Usage: - * node scripts/extract-mdx-demos.mjs - * - * Example: - * node scripts/extract-mdx-demos.mjs dashboard content/docs/blocks/dashboard.mdx - * - * Limitations: - * - Schemas must be pure JS object literals (no JSX expressions, no fn calls). - * - title/description must be string literals on the same component opening tag. - * - `` self-closing only (`/>`); paired tags are skipped. - */ - -import fs from 'node:fs'; -import path from 'node:path'; -import vm from 'node:vm'; -import { fileURLToPath } from 'node:url'; -import { isEntrypoint } from './invoked-as.mjs'; - -const __dirname = path.dirname(fileURLToPath(import.meta.url)); -const ROOT = path.resolve(__dirname, '..'); -const CATALOG_DIR = path.join(ROOT, 'examples/schema-catalog/src/schemas'); - -function slugify(str) { - return str - .toLowerCase() - .replace(/[^a-z0-9]+/g, '-') - .replace(/^-+|-+$/g, '') - .slice(0, 60); -} - -/** - * Given the source of a single `` block, extract: - * { title, description, schemaSource } for single-schema demos, OR - * { title, description, examplesSource } for multi-example galleries. - * - * Title/description are string-literal props on the opening tag. - * `schemaSource` is the *text* between `schema={` and the matching `}` (one - * level of `{}` nesting, so we strip the outer one). - * `examplesSource` is the *text* between `examples={[` and the matching `]}`. - */ -function parseDemoBlock(blockSrc) { - const titleMatch = blockSrc.match(/title=["']([^"']+)["']/); - const descMatch = blockSrc.match(/description=["']([^"']+)["']/); - - const findBalanced = (anchor, open, close) => { - const a = blockSrc.indexOf(anchor); - if (a === -1) return null; - // Locate the first `open` bracket at or after the end of the anchor. - let openIdx = -1; - for (let i = a + anchor.length - 1; i < blockSrc.length; i++) { - if (blockSrc[i] === open) { openIdx = i; break; } - } - if (openIdx === -1) return null; - let depth = 0; - let inStr = null; - for (let i = openIdx; i < blockSrc.length; i++) { - const c = blockSrc[i]; - if (inStr) { - if (c === '\\') i++; - else if (c === inStr) inStr = null; - continue; - } - if (c === '"' || c === "'" || c === '`') { inStr = c; continue; } - if (c === open) depth++; - else if (c === close) { - depth--; - if (depth === 0) return blockSrc.slice(openIdx + 1, i).trim(); - } - } - return null; - }; - - const schemaInner = findBalanced('schema={', '{', '}'); - if (schemaInner !== null) { - return { - kind: 'single', - title: titleMatch?.[1] ?? null, - description: descMatch?.[1] ?? null, - schemaSource: schemaInner, - }; - } - - // Fall back to multi-example gallery. - const examplesInner = findBalanced('examples={', '[', ']'); - if (examplesInner !== null) { - return { - kind: 'multi', - title: titleMatch?.[1] ?? null, - description: descMatch?.[1] ?? null, - examplesSource: examplesInner, - }; - } - return null; -} - -function evaluateObjectLiteral(src) { - // Wrap in parens so the parser treats it as an expression. - // Strip trailing commas after the last property (lenient JS, but vm may not - // care — keep them, they're valid in modern JS). - return vm.runInNewContext(`(${src})`, {}, { timeout: 1000 }); -} - -function findInteractiveDemoBlocks(mdx, tagNames = ['InteractiveDemo', 'ComponentDemo']) { - const blocks = []; - for (const tagName of tagNames) { - const openTag = new RegExp(`<${tagName}\\b`, 'g'); - let m; - while ((m = openTag.exec(mdx)) !== null) { - const start = m.index; - let depth = 0; - let inStr = null; - let i = start + `<${tagName}`.length; - let end = -1; - for (; i < mdx.length; i++) { - const c = mdx[i]; - if (inStr) { - if (c === '\\') i++; - else if (c === inStr) inStr = null; - continue; - } - if (c === '"' || c === "'" || c === '`') { - inStr = c; - continue; - } - if (c === '{') depth++; - else if (c === '}') depth--; - else if (depth === 0 && c === '/' && mdx[i + 1] === '>') { - end = i + 2; - break; - } - } - if (end === -1) { - console.warn(`[skip] couldn't find /> for <${tagName} at offset ${start}`); - continue; - } - blocks.push({ start, end, src: mdx.slice(start, end), tagName }); - openTag.lastIndex = end; - } - } - // Sort by document order so replacement offsets stay monotonic. - blocks.sort((a, b) => a.start - b.start); - return blocks; -} - -// ── import rewriting ───────────────────────────────────────────────────────── - -const DEMO_MODULE = '@/app/components/ComponentDemo'; - -/** - * Every `import { … } from '@/app/components/ComponentDemo';` statement in `mdx`, - * with its local binding names and the exact source span it occupies. - * - * The span deliberately swallows ONE trailing blank line: an MDX import sits in - * the `---\n\n\n\n` shape every file under `content/docs/` uses, so - * dropping only the import line would leave a double blank behind. - */ -function findDemoImports(mdx) { - const re = new RegExp( - String.raw`^[ \t]*import\s*\{([^}]*)\}\s*from\s*['"]` + - DEMO_MODULE.replace(/[/]/g, String.raw`\/`) + - String.raw`['"]\s*;?[ \t]*\r?\n(\r?\n)?`, - 'gm', - ); - const found = []; - let m; - while ((m = re.exec(mdx)) !== null) { - const specifiers = m[1] - .split(',') - .map((s) => s.trim()) - .filter(Boolean) - .map((s) => { - const parts = s.split(/\s+as\s+/); - // `Imported as Local` — the body references the LOCAL name. - return { imported: parts[0].trim(), local: (parts[1] ?? parts[0]).trim(), source: s }; - }); - found.push({ start: m.index, end: m.index + m[0].length, text: m[0], specifiers }); - } - return found; -} - -/** - * `mdx` with fenced code blocks and inline code spans blanked out. - * - * A `` printed inside a ```plaintext fence is documentation about - * the component, not a use of it — MDX never compiles it to an element. Counting - * it would keep an import alive that nothing renders, which is exactly the class - * of stale import objectui#3788 is about. - */ -function stripCode(mdx) { - return mdx - .replace(/^([ \t]*)(`{3,}|~{3,})[^\n]*\n[\s\S]*?^[ \t]*\2[^\n]*$/gm, '') - .replace(/`[^`\n]*`/g, ''); -} - -/** - * Rewrite the demo-module imports of `mdx` so the named imports are exactly the - * ones the file still USES — the point of objectui#3788. - * - * The old rewrite matched one spelling of the statement - * (`/import\s*\{\s*InteractiveDemo[^}]*\}\s*from\s*…/`) and swapped it wholesale - * for `import { SchemaExample } …`. Files written as - * `import { ComponentDemo, DemoGrid } …` therefore had their CONTENT migrated to - * `` while the import stayed put, and ~106 of them accumulated in - * `content/docs/`. Deciding on usage instead of on spelling is what stops the - * pile from regrowing. - * - * @param {string} mdx MDX source, AFTER the demo blocks have been migrated. - * @param {object} [opts] - * @param {string[]} [opts.ensure] Names to add to the import when the body uses - * them but no import declares them yet (the migration inserts `` - * tags, so it passes `['SchemaExample']`). A name that is NOT used is never - * added — an "ensured" unused import would be the same bug in a new spelling. - * @returns {string} - */ -function rewriteDemoImports(mdx, { ensure = [] } = {}) { - const imports = findDemoImports(mdx); - if (imports.length === 0) return mdx; - - // Usage is judged against the body ONLY: an import statement naming - // `ComponentDemo` must never count as a use of `ComponentDemo`. - let body = ''; - let cursor = 0; - for (const imp of imports) { - body += mdx.slice(cursor, imp.start); - cursor = imp.end; - } - body += mdx.slice(cursor); - const searchable = stripCode(body); - const isUsed = (local) => new RegExp(String.raw`<\/?${local}\b`).test(searchable); - - // One statement carries the survivors; any further statements from the same - // module collapse into it (they are duplicates of the same binding source). - const seen = new Set(); - const keep = []; - for (const imp of imports) { - for (const spec of imp.specifiers) { - if (seen.has(spec.local) || !isUsed(spec.local)) continue; - seen.add(spec.local); - keep.push(spec.source); - } - } - for (const name of ensure) { - if (!seen.has(name) && isUsed(name)) { - seen.add(name); - keep.push(name); - } - } - - // Reuse the first statement's own trailing shape, so a file whose import is - // not followed by a blank line does not gain one. - const trailer = /\r?\n\r?\n$/.test(imports[0].text) ? '\n\n' : '\n'; - const replacement = keep.length - ? `import { ${keep.join(', ')} } from '${DEMO_MODULE}';${trailer}` - : ''; - - let out = ''; - cursor = 0; - for (const [i, imp] of imports.entries()) { - out += mdx.slice(cursor, imp.start); - // Only the first statement is rewritten; the rest are dropped outright. - if (i === 0) out += replacement; - cursor = imp.end; - } - out += mdx.slice(cursor); - return out; -} - -function main() { - const [category, mdxPath] = process.argv.slice(2); - if (!category || !mdxPath) { - console.error('Usage: node scripts/extract-mdx-demos.mjs '); - process.exit(1); - } - const abs = path.resolve(ROOT, mdxPath); - const mdx = fs.readFileSync(abs, 'utf8'); - - const outDir = path.join(CATALOG_DIR, category); - fs.mkdirSync(outDir, { recursive: true }); - - const blocks = findInteractiveDemoBlocks(mdx); - if (blocks.length === 0) { - console.error('No blocks found.'); - process.exit(0); - } - - const entries = []; - const usedSlugs = new Set(); - let newMdx = ''; - let cursor = 0; - - for (const [i, b] of blocks.entries()) { - const parsed = parseDemoBlock(b.src); - if (!parsed) { - console.warn(`[skip] block #${i}: couldn't parse`); - continue; - } - const { title, description, kind } = parsed; - const baseTitleSlug = title ? slugify(title) : `demo-${i + 1}`; - - if (kind === 'single') { - let schema; - try { - schema = evaluateObjectLiteral(parsed.schemaSource); - } catch (err) { - console.error(`[fail] block #${i} ("${title ?? '?'}"): ${err.message}`); - continue; - } - let slug = baseTitleSlug; - let n = 2; - while (usedSlugs.has(slug)) slug = `${baseTitleSlug}-${n++}`; - usedSlugs.add(slug); - const id = `${category}/${slug}`; - fs.writeFileSync( - path.join(outDir, `${slug}.json`), - JSON.stringify(schema, null, 2) + '\n', - ); - entries.push({ id, slug, title, description, multi: false }); - - newMdx += mdx.slice(cursor, b.start); - newMdx += ``; - cursor = b.end; - } else { - // kind === 'multi' - let examplesArr; - try { - examplesArr = evaluateObjectLiteral(`[${parsed.examplesSource}]`); - } catch (err) { - console.error(`[fail] block #${i} ("${title ?? '?'}") multi: ${err.message}`); - continue; - } - const childIds = []; - for (const child of examplesArr) { - const childTitle = child.label ?? child.title ?? `${title} item`; - let slug = slugify(`${baseTitleSlug}-${childTitle}`); - let n = 2; - const base = slug; - while (usedSlugs.has(slug)) slug = `${base}-${n++}`; - usedSlugs.add(slug); - const id = `${category}/${slug}`; - fs.writeFileSync( - path.join(outDir, `${slug}.json`), - JSON.stringify(child.schema, null, 2) + '\n', - ); - entries.push({ - id, - slug, - title: childTitle, - description: child.description ?? '', - multi: true, - }); - childIds.push(id); - } - // Replace the whole block with a - // gallery: heading + one per child. - const gallery = childIds - .map((cid) => ``) - .join('\n\n'); - newMdx += mdx.slice(cursor, b.start); - newMdx += gallery; - cursor = b.end; - } - } - newMdx += mdx.slice(cursor); - - // Rewrite the import so its named imports are exactly what the migrated file - // still uses (plus SchemaExample, which the migration just started using). - // objectui#3788: the previous rewrite matched only the `InteractiveDemo` - // spelling, so `import { ComponentDemo, DemoGrid } …` files kept a dead import. - newMdx = rewriteDemoImports(newMdx, { ensure: ['SchemaExample'] }); - - const outMdx = abs + '.migrated'; - fs.writeFileSync(outMdx, newMdx); - - console.log(`\nWrote ${entries.length} schema file(s) to ${path.relative(ROOT, outDir)}`); - console.log(`Wrote draft MDX to ${path.relative(ROOT, outMdx)}\n`); - console.log('--- Registry entries (paste into examples/schema-catalog/src/index.ts) ---\n'); - for (const e of entries) { - const importIdent = `${category}_${e.slug.replace(/-/g, '_')}`; - console.log( - `import ${importIdent} from './schemas/${category}/${e.slug}.json' with { type: 'json' };`, - ); - } - console.log(); - for (const e of entries) { - const importIdent = `${category}_${e.slug.replace(/-/g, '_')}`; - console.log(` '${e.id}': {`); - console.log(` id: '${e.id}',`); - console.log(` meta: {`); - console.log(` title: ${JSON.stringify(e.title ?? e.slug)},`); - console.log(` description: ${JSON.stringify(e.description ?? '')},`); - console.log(` category: '${category}',`); - console.log(` },`); - console.log(` schema: ${importIdent},`); - console.log(` },`); - } -} - -export { DEMO_MODULE, findDemoImports, rewriteDemoImports, stripCode }; - -// Only run the CLI when executed directly — the import rewriter above is -// imported by scripts/__tests__/extract-mdx-demos.test.ts. -const invokedDirectly = isEntrypoint(import.meta.url); -if (invokedDirectly) main(); diff --git a/ui/scripts/markdown-test-inputs.mjs b/ui/scripts/markdown-test-inputs.mjs index 856db6a41e8..c5a7d1fa645 100644 --- a/ui/scripts/markdown-test-inputs.mjs +++ b/ui/scripts/markdown-test-inputs.mjs @@ -973,15 +973,6 @@ export const ADJUDICATED = new Map([ walker: 'markdown-tree', }, ], - // Reads no markdown: its `README.md` literal is sample input to a pure path-matcher - // assertion -- the suite is a census OF corpus walkers and keeps its own inputs literal. - [ - 'scripts/__tests__/coverage-red-cause-census.test.ts', - { - reads: [], - notRead: ['README.md'], - }, - ], // Reads no markdown: every document literal is fixture text handed to pure functions. [ 'scripts/__tests__/cross-file-line-citation-census.test.ts', @@ -1007,13 +998,6 @@ export const ADJUDICATED = new Map([ reads: ['./*', '.changeset/**', '.claude/skills/**', 'apps/**', 'content/docs/**', 'docs/ARCHITECTURE.md', 'docs/CONSOLE-STREAMLINING-SUMMARY.md', 'docs/adr/**', 'docs/audits/**', 'examples/**', 'packages/**', 'patches/README.md', 'skills/objectui/**'], }, ], - [ - 'scripts/__tests__/extract-mdx-demos.test.ts', - { - reads: ['content/docs/**'], - walker: 'markdown-tree', - }, - ], [ 'scripts/__tests__/layered-read-declared-path-4016.test.ts', {