@discourser/design-system 0.26.0 → 0.27.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,667 @@
1
+ # Component Catalog Generation Pipeline — Implementation Plan
2
+
3
+ > **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking.
4
+
5
+ **Goal:** Build a fully automated pipeline that generates `docs/component-catalog.md` from the live component exports and catalog story, and validates catalog coverage at build time.
6
+
7
+ **Architecture:** A new `scripts/generate-component-catalog.ts` script parses `src/components/index.ts` and `stories/ComponentCatalog.stories.tsx` to generate a structured markdown catalog. `scripts/validate-exports.ts` gains a second validation phase that warns when exported components lack catalog story entries. Both scripts are wired into the `build` npm sequence.
8
+
9
+ **Tech Stack:** Node.js ESM, `tsx`, TypeScript, `node:fs`, `node:path` — same stack as `validate-exports.ts`. No new dependencies.
10
+
11
+ ---
12
+
13
+ ## Important Design Decision: Phase 2 Exit Behavior
14
+
15
+ The spec states Phase 2 (catalog coverage check) should use `process.exit(1)` when components are missing. However, 6 components are currently missing from `ComponentCatalog.stories.tsx` (see baseline table below). Making this fatal would block the build before the catalog is complete.
16
+
17
+ **Decision:** Phase 2 is **non-fatal (warning only)** — uses `console.warn` + `YELLOW` color, does not contribute to `process.exit(1)`. The mechanism is tested by the mutation test below. Phase 2 should be tightened to fatal once all components have catalog entries (a follow-up task). This is a known, deliberate divergence from the spec's literal text, motivated by the spec's own Phase 2 instruction: "If any stub warnings appear, list which components are missing catalog entries and **flag them for follow-up**."
18
+
19
+ ---
20
+
21
+ ## Phase 1 Coverage Baseline
22
+
23
+ **The component name parser in this pipeline extracts component identity from the _source path_ of each export statement, not from individual symbol names.** This means `AddScenarioDialog` (exported as a named symbol from `./ScenarioQueue`) is invisible to the parser — the parser sees `ScenarioQueue` (already in the catalog). Only the 6 components below will appear as stubs.
24
+
25
+ | Component | In index.ts | In Catalog Story | Will Appear as Stub |
26
+ |---|---|---|---|
27
+ | Button | ✅ | ✅ | — |
28
+ | ButtonGroup | ✅ | ✅ | — |
29
+ | IconButton | ✅ | ✅ | — |
30
+ | Input | ✅ | ✅ | — |
31
+ | InputAddon | ✅ | ✅ | — |
32
+ | InputGroup | ✅ | ✅ | — |
33
+ | Textarea | ✅ | ✅ | — |
34
+ | Header | ✅ | ✅ | — |
35
+ | **Divider** | ✅ | ❌ | ✅ |
36
+ | Badge | ✅ | ✅ | — |
37
+ | Spinner | ✅ | ✅ | — |
38
+ | Toaster | ✅ | ✅ | — |
39
+ | Card | ✅ | ✅ | — |
40
+ | Dialog | ✅ | ✅ | — |
41
+ | Switch | ✅ | ✅ | — |
42
+ | Accordion | ✅ | ✅ | — |
43
+ | Drawer | ✅ | ✅ | — |
44
+ | Tabs | ✅ | ✅ | — |
45
+ | Checkbox | ✅ | ✅ | — |
46
+ | RadioGroup | ✅ | ✅ | — |
47
+ | Select | ✅ | ✅ | — |
48
+ | Slider | ✅ | ✅ | — |
49
+ | Avatar | ✅ | ✅ | — |
50
+ | Progress | ✅ | ✅ | — |
51
+ | Skeleton | ✅ | ✅ | — |
52
+ | Popover | ✅ | ✅ | — |
53
+ | Tooltip | ✅ | ✅ | — |
54
+ | CloseButton | ✅ | ✅ (as `CloseButtonNS`) | — |
55
+ | **Icon** | ✅ | ❌ | ✅ |
56
+ | **AbsoluteCenter** | ✅ | ❌ | ✅ |
57
+ | **Group** | ✅ | ❌ | ✅ |
58
+ | Breadcrumb | ✅ | ✅ | — |
59
+ | ContentCard | ✅ | ✅ | — |
60
+ | Stepper | ✅ | ✅ | — |
61
+ | NavigationMenu | ✅ | ✅ | — |
62
+ | **SettingsPopover** | ✅ | ❌ | ✅ |
63
+ | ScenarioSettings | ✅ | ✅ | — |
64
+ | **StudioControls** | ✅ | ❌ | ✅ |
65
+ | ScenarioQueue | ✅ | ✅ | — |
66
+ | ScenarioCard | ✅ | ✅ | — |
67
+
68
+ **6 stub warnings expected:** AbsoluteCenter, Divider, Group, Icon, SettingsPopover, StudioControls
69
+
70
+ ---
71
+
72
+ ## File Map
73
+
74
+ | File | Action | Responsibility |
75
+ |---|---|---|
76
+ | `scripts/generate-component-catalog.ts` | **Create** | Parse index.ts + catalog story → write `docs/component-catalog.md` |
77
+ | `scripts/validate-exports.ts` | **Modify** (append) | Add Phase 2: catalog story coverage warning |
78
+ | `package.json` | **Modify** | Add `catalog:generate` script; insert into `build` |
79
+ | `docs/component-catalog.md` | **Generated** | Written by the script — never hand-edited |
80
+
81
+ ---
82
+
83
+ ## Task 1: Create `scripts/generate-component-catalog.ts`
84
+
85
+ **Files:**
86
+ - Create: `scripts/generate-component-catalog.ts`
87
+
88
+ Write the complete file in one step, assembling all sections:
89
+
90
+ - [ ] **Step 1A: Write the complete script**
91
+
92
+ ```typescript
93
+ #!/usr/bin/env tsx
94
+ /**
95
+ * generate-component-catalog.ts
96
+ *
97
+ * Reads src/components/index.ts and stories/ComponentCatalog.stories.tsx,
98
+ * then writes a complete docs/component-catalog.md.
99
+ *
100
+ * Parser note: component identity is derived from the *source path* of each
101
+ * export statement, not from individual symbol names. Named symbols exported
102
+ * from a compound module (e.g. AddScenarioDialog from ./ScenarioQueue) are
103
+ * not individually tracked — only the module-level name (ScenarioQueue) is.
104
+ *
105
+ * Run manually: pnpm catalog:generate
106
+ * Run in build: included after exports:validate in pnpm build
107
+ */
108
+
109
+ import fs from 'node:fs';
110
+ import path from 'node:path';
111
+ import { fileURLToPath } from 'node:url';
112
+
113
+ const __dirname = path.dirname(fileURLToPath(import.meta.url));
114
+ const root = path.resolve(__dirname, '..');
115
+
116
+ // ── Color constants (match validate-exports.ts) ───────────────────────────────
117
+ const RED = '\x1b[31m';
118
+ const YELLOW = '\x1b[33m';
119
+ const GREEN = '\x1b[32m';
120
+ const RESET = '\x1b[0m';
121
+
122
+ // ── 1. Parse src/components/index.ts ─────────────────────────────────────────
123
+
124
+ type ComponentEntry = { name: string; type: 'Simple' | 'Compound' };
125
+
126
+ function parseComponentIndex(): ComponentEntry[] {
127
+ const indexPath = path.join(root, 'src/components/index.ts');
128
+ const source = fs.readFileSync(indexPath, 'utf8');
129
+ const entries: ComponentEntry[] = [];
130
+
131
+ // export * as Dialog from './Dialog' → Compound
132
+ const namespaceRe = /export\s+\*\s+as\s+(\w+)\s+from\s+['"][^'"]+['"]/g;
133
+ for (const [, name] of source.matchAll(namespaceRe)) {
134
+ entries.push({ name, type: 'Compound' });
135
+ }
136
+
137
+ // export { Button, ... } from './Button' → Simple
138
+ // Captures only the first path segment — Icons/ entries are skipped.
139
+ // Individual symbols from compound modules (e.g. AddScenarioDialog from
140
+ // ./ScenarioQueue) are not separately tracked; only the module name is.
141
+ const namedRe = /export\s+\{[^}]+\}\s+from\s+['"]\.\/([^/'"]+)/g;
142
+ for (const [, srcPath] of source.matchAll(namedRe)) {
143
+ if (srcPath === 'Icons') continue;
144
+ const normalized =
145
+ srcPath.charAt(0).toUpperCase() + srcPath.slice(1);
146
+ if (!entries.some((e) => e.name === normalized)) {
147
+ entries.push({ name: normalized, type: 'Simple' });
148
+ }
149
+ }
150
+
151
+ return entries.sort((a, b) => a.name.localeCompare(b.name));
152
+ }
153
+
154
+ // ── 2. Parse stories/ComponentCatalog.stories.tsx ────────────────────────────
155
+
156
+ function parseCatalogImports(): Set<string> {
157
+ const storyPath = path.join(root, 'stories/ComponentCatalog.stories.tsx');
158
+ const source = fs.readFileSync(storyPath, 'utf8');
159
+
160
+ // Find the import { ... } from '../src' block
161
+ const importBlockRe = /import\s+\{([^}]+)\}\s+from\s+['"]\.\.\/src['"]/s;
162
+ const match = source.match(importBlockRe);
163
+ if (!match) return new Set();
164
+
165
+ const importedNames = new Set<string>();
166
+ for (const token of match[1].split(',')) {
167
+ const trimmed = token.trim();
168
+ if (!trimmed || trimmed.startsWith('type ')) continue;
169
+ // Handle "CloseButton as CloseButtonNS" → extract "CloseButton"
170
+ const baseName = trimmed.split(/\s+as\s+/)[0].trim();
171
+ if (baseName) importedNames.add(baseName);
172
+ }
173
+ return importedNames;
174
+ }
175
+
176
+ // ── 3. Extract JSX usage examples ────────────────────────────────────────────
177
+
178
+ function extractUsageExample(
179
+ componentName: string,
180
+ source: string,
181
+ ): string | null {
182
+ // Find first self-closing or opening JSX tag for this component.
183
+ // Handles namespace components like <Card.Root ...> via <Card.*
184
+ const tagRe = new RegExp(
185
+ `<${componentName}(?:\\.[A-Z]\\w*)?[\\s\\S]{0,200}?(?:/>|>)`,
186
+ );
187
+ const match = source.match(tagRe);
188
+ if (!match) return null;
189
+
190
+ // Collapse whitespace to a single representative line
191
+ return match[0]
192
+ .replace(/\s+/g, ' ')
193
+ .replace(/\{ /g, '{')
194
+ .replace(/ \}/g, '}')
195
+ .trim();
196
+ }
197
+
198
+ // ── 4. Generate docs/component-catalog.md ────────────────────────────────────
199
+
200
+ function generateCatalog(): void {
201
+ const components = parseComponentIndex();
202
+ const catalogImports = parseCatalogImports();
203
+
204
+ const storyPath = path.join(root, 'stories/ComponentCatalog.stories.tsx');
205
+ const storySource = fs.readFileSync(storyPath, 'utf8');
206
+
207
+ const pkgPath = path.join(root, 'package.json');
208
+ const pkg = JSON.parse(fs.readFileSync(pkgPath, 'utf8')) as {
209
+ version?: string;
210
+ };
211
+ const version = pkg.version ?? '0.0.0';
212
+ const now = new Date().toISOString().split('T')[0];
213
+
214
+ const lines: string[] = [];
215
+
216
+ // ── Header ────────────────────────────────────────────────────────────────
217
+ lines.push('# Component Catalog');
218
+ lines.push('');
219
+ lines.push(
220
+ '> **Status:** Generated — auto-produced by `scripts/generate-component-catalog.ts`',
221
+ );
222
+ lines.push(
223
+ '> **Source:** `stories/ComponentCatalog.stories.tsx` + `src/components/index.ts`',
224
+ );
225
+ lines.push(`> **Design System Version:** ${version}`);
226
+ lines.push(`> **Generated:** ${now}`);
227
+ lines.push(
228
+ '> **Do not hand-edit** — this file is overwritten on every build',
229
+ );
230
+ lines.push('');
231
+ lines.push('---');
232
+ lines.push('');
233
+ lines.push('## Overview');
234
+ lines.push('');
235
+ lines.push(`${components.length} components in the Discourser Design System.`);
236
+ lines.push('Run `pnpm catalog:generate` to regenerate after changes.');
237
+ lines.push('');
238
+ lines.push('---');
239
+ lines.push('');
240
+
241
+ // ── Component sections ────────────────────────────────────────────────────
242
+ const stubWarnings: string[] = [];
243
+
244
+ for (const { name, type } of components) {
245
+ lines.push(`## ${name}`);
246
+ lines.push('');
247
+ lines.push(`**Type:** ${type}`);
248
+ lines.push(
249
+ `**Import:** \`import { ${name} } from '@discourser/design-system'\``,
250
+ );
251
+ lines.push('');
252
+
253
+ if (!catalogImports.has(name)) {
254
+ lines.push(
255
+ '> ⚠️ No catalog entry found in ComponentCatalog.stories.tsx — add an example to keep this catalog accurate.',
256
+ );
257
+ stubWarnings.push(name);
258
+ } else {
259
+ const example = extractUsageExample(name, storySource);
260
+ if (example) {
261
+ lines.push('**Usage:**');
262
+ lines.push('```tsx');
263
+ lines.push(example);
264
+ lines.push('```');
265
+ } else {
266
+ lines.push('**Usage:** *(see ComponentCatalog.stories.tsx)*');
267
+ }
268
+ }
269
+
270
+ lines.push('');
271
+ lines.push('---');
272
+ lines.push('');
273
+ }
274
+
275
+ // ── Write output ──────────────────────────────────────────────────────────
276
+ const outPath = path.join(root, 'docs/component-catalog.md');
277
+ fs.mkdirSync(path.dirname(outPath), { recursive: true });
278
+ fs.writeFileSync(outPath, lines.join('\n'), 'utf8');
279
+
280
+ console.log('');
281
+ console.log(
282
+ `${GREEN}✓ Generated docs/component-catalog.md — ${components.length} components${RESET}`,
283
+ );
284
+
285
+ if (stubWarnings.length > 0) {
286
+ console.log('');
287
+ console.log(
288
+ `${YELLOW} ⚠️ ${stubWarnings.length} components have no catalog entry:${RESET}`,
289
+ );
290
+ for (const name of stubWarnings) {
291
+ console.log(`${YELLOW} • ${name}${RESET}`);
292
+ }
293
+ console.log(
294
+ `${YELLOW} Add these to stories/ComponentCatalog.stories.tsx to complete the catalog.${RESET}`,
295
+ );
296
+ }
297
+
298
+ console.log('');
299
+ }
300
+
301
+ generateCatalog();
302
+ ```
303
+
304
+ - [ ] **Step 1B: Run the script and verify it passes**
305
+
306
+ ```bash
307
+ cd /Users/willstreeter/WebstormProjects/vibe-coding/shifu-project/Discourser-Design-System
308
+ pnpm tsx scripts/generate-component-catalog.ts
309
+ ```
310
+
311
+ Expected output (script exits 0):
312
+ ```
313
+ ✓ Generated docs/component-catalog.md — N components
314
+
315
+ ⚠️ 6 components have no catalog entry:
316
+ • AbsoluteCenter
317
+ • Divider
318
+ • Group
319
+ • Icon
320
+ • SettingsPopover
321
+ • StudioControls
322
+ Add these to stories/ComponentCatalog.stories.tsx to complete the catalog.
323
+ ```
324
+
325
+ Then verify:
326
+ ```bash
327
+ # File exists and is non-empty
328
+ wc -l docs/component-catalog.md
329
+
330
+ # Every component heading appears exactly once (should print nothing)
331
+ grep "^## " docs/component-catalog.md | sort | uniq -d
332
+
333
+ # Version and date headers are present
334
+ head -10 docs/component-catalog.md
335
+ ```
336
+
337
+ - [ ] **Step 1C: Commit**
338
+
339
+ ```bash
340
+ git add scripts/generate-component-catalog.ts docs/component-catalog.md
341
+ git commit -m "feat(catalog): add generate-component-catalog.ts script
342
+
343
+ Generates docs/component-catalog.md from src/components/index.ts and
344
+ stories/ComponentCatalog.stories.tsx. Stubs the 6 components missing
345
+ from the catalog story with a warning comment."
346
+ ```
347
+
348
+ ---
349
+
350
+ ## Task 2: Extend `scripts/validate-exports.ts` with catalog coverage check
351
+
352
+ **Files:**
353
+ - Modify: `scripts/validate-exports.ts`
354
+
355
+ **Design:** Phase 2 is **warning-only** (non-fatal) because 6 components currently lack catalog entries. The mutation test below confirms the detection mechanism works. Phase 2 should be tightened to `process.exit(1)` once all components have catalog entries (tracked separately).
356
+
357
+ The existing file has two early exits that must both be removed:
358
+ 1. `process.exit(0)` at line ~112 (inside the success branch) — defer so Phase 2 runs
359
+ 2. `process.exit(1)` at line ~152 (bottom of file) — replace with `let phase1Failed`
360
+
361
+ ### Exact diff for the existing file:
362
+
363
+ - [ ] **Step 2A: Remove the early `process.exit(0)` and defer the bottom `process.exit(1)`**
364
+
365
+ In `scripts/validate-exports.ts`, make these two edits:
366
+
367
+ **Edit 1** — lines 107–113: remove the inner `process.exit(0)`:
368
+
369
+ Replace:
370
+ ```typescript
371
+ if (missing.length === 0 && extra.length === 0) {
372
+ console.log(
373
+ `${GREEN}✓ All exported components have matching package.json export entries.${RESET}`,
374
+ );
375
+ console.log('');
376
+ process.exit(0);
377
+ }
378
+ ```
379
+
380
+ With:
381
+ ```typescript
382
+ if (missing.length === 0 && extra.length === 0) {
383
+ console.log(
384
+ `${GREEN}✓ All exported components have matching package.json export entries.${RESET}`,
385
+ );
386
+ console.log('');
387
+ // Phase 2 runs next — do not exit here
388
+ }
389
+ ```
390
+
391
+ **Edit 2** — lines 150–153: replace the bottom `process.exit(1)` with a flag:
392
+
393
+ Replace:
394
+ ```typescript
395
+ // Fail the build only when components are missing from exports
396
+ if (missing.length > 0) {
397
+ process.exit(1);
398
+ }
399
+ ```
400
+
401
+ With:
402
+ ```typescript
403
+ // Fail the build only when components are missing from exports (Phase 1)
404
+ const phase1Failed = missing.length > 0;
405
+ ```
406
+
407
+ - [ ] **Step 2B: Append Phase 2 to the end of `scripts/validate-exports.ts`**
408
+
409
+ Append this block after the last line of the current file (after the `phase1Failed` declaration):
410
+
411
+ ```typescript
412
+ // ── Phase 2: Validate ComponentCatalog.stories.tsx coverage ──────────────────
413
+ //
414
+ // Warning-only (non-fatal) while the catalog is being built out. Tighten to
415
+ // process.exit(1) once all exported components have catalog story entries.
416
+
417
+ console.log('');
418
+ console.log(
419
+ '📖 Validating ComponentCatalog.stories.tsx coverage against src/components/index.ts...',
420
+ );
421
+ console.log('');
422
+
423
+ const storyPath = path.join(root, 'stories/ComponentCatalog.stories.tsx');
424
+ const storySource = fs.readFileSync(storyPath, 'utf8');
425
+
426
+ // Parse the import { ... } from '../src' block
427
+ const importBlockRe = /import\s+\{([^}]+)\}\s+from\s+['"]\.\.\/src['"]/s;
428
+ const importMatch = storySource.match(importBlockRe);
429
+
430
+ const catalogImports = new Set<string>();
431
+ if (importMatch) {
432
+ for (const token of importMatch[1].split(',')) {
433
+ const trimmed = token.trim();
434
+ if (!trimmed || trimmed.startsWith('type ')) continue;
435
+ // Handle "CloseButton as CloseButtonNS" → extract "CloseButton"
436
+ const baseName = trimmed.split(/\s+as\s+/)[0].trim();
437
+ if (baseName) catalogImports.add(baseName);
438
+ }
439
+ }
440
+
441
+ const missingFromCatalog: string[] = [];
442
+ for (const component of exportedComponents) {
443
+ if (!catalogImports.has(component)) {
444
+ missingFromCatalog.push(component);
445
+ }
446
+ }
447
+
448
+ const extraInCatalog: string[] = [];
449
+ for (const name of catalogImports) {
450
+ // Only check PascalCase names (component names, not lowercase utilities like `toaster`)
451
+ if (name.charAt(0) !== name.charAt(0).toUpperCase()) continue;
452
+ const found = [...exportedComponents].some(
453
+ (c) => c.toLowerCase() === name.toLowerCase(),
454
+ );
455
+ if (!found) extraInCatalog.push(name);
456
+ }
457
+
458
+ if (missingFromCatalog.length === 0) {
459
+ console.log(
460
+ `${GREEN}✓ All exported components are present in ComponentCatalog.stories.tsx.${RESET}`,
461
+ );
462
+ console.log('');
463
+ } else {
464
+ // Warning-only — see comment above. Change to console.error + phase2Failed=true to harden.
465
+ console.warn(
466
+ `${YELLOW}⚠ Components exported from index.ts but not yet in ComponentCatalog.stories.tsx:${RESET}`,
467
+ );
468
+ for (const name of missingFromCatalog.sort()) {
469
+ console.warn(` ${YELLOW}• ${name}${RESET}`);
470
+ }
471
+ console.warn('');
472
+ console.warn(
473
+ `${YELLOW} Add these to stories/ComponentCatalog.stories.tsx to complete the catalog.${RESET}`,
474
+ );
475
+ console.warn('');
476
+ }
477
+
478
+ if (extraInCatalog.length > 0) {
479
+ console.warn(
480
+ `${YELLOW}⚠ ComponentCatalog.stories.tsx imports components not found in index.ts (may be intentional):${RESET}`,
481
+ );
482
+ for (const name of extraInCatalog.sort()) {
483
+ console.warn(` ${YELLOW}• ${name}${RESET}`);
484
+ }
485
+ console.warn('');
486
+ }
487
+
488
+ // ── Final exit ────────────────────────────────────────────────────────────────
489
+ if (phase1Failed) {
490
+ process.exit(1);
491
+ }
492
+
493
+ process.exit(0);
494
+ ```
495
+
496
+ - [ ] **Step 2C: Run with current state — confirm exits 0, Phase 2 warns about 6 missing**
497
+
498
+ ```bash
499
+ pnpm exports:validate
500
+ ```
501
+
502
+ Expected: exits code 0. Output includes both phases. Phase 2 warns:
503
+ ```
504
+ ⚠ Components exported from index.ts but not yet in ComponentCatalog.stories.tsx:
505
+ • AbsoluteCenter
506
+ • Divider
507
+ • Group
508
+ • Icon
509
+ • SettingsPopover
510
+ • StudioControls
511
+ ```
512
+
513
+ - [ ] **Step 2D: Mutation test — temporarily break one catalog import**
514
+
515
+ In `stories/ComponentCatalog.stories.tsx`, change `NavigationMenu,` to `FakeComponent,` in the import block.
516
+
517
+ ```bash
518
+ pnpm exports:validate
519
+ ```
520
+
521
+ Expected: exits code 0 (still non-fatal), Phase 2 now also warns about `NavigationMenu` in addition to the 6 known stubs.
522
+
523
+ Restore `FakeComponent` → `NavigationMenu`.
524
+
525
+ ```bash
526
+ pnpm exports:validate
527
+ ```
528
+
529
+ Expected: exits code 0, back to only the 6 known stubs in the Phase 2 warning.
530
+
531
+ > **Why exit 0 in the mutation test:** Phase 2 is warning-only by design. The test confirms the detection mechanism (missing components are correctly identified and reported) rather than the exit code. When Phase 2 is hardened to fatal in a follow-up task, the mutation test expectation should be updated to exit 1.
532
+
533
+ - [ ] **Step 2E: Commit**
534
+
535
+ ```bash
536
+ git add scripts/validate-exports.ts
537
+ git commit -m "feat(catalog): add catalog coverage check to validate-exports.ts
538
+
539
+ Adds Phase 2 that checks every component exported from src/components/index.ts
540
+ appears in ComponentCatalog.stories.tsx. Non-fatal (warns) while 6 components
541
+ lack catalog entries. Tighten to fatal after catalog is complete."
542
+ ```
543
+
544
+ ---
545
+
546
+ ## Task 3: Wire into `package.json`
547
+
548
+ **Files:**
549
+ - Modify: `package.json`
550
+
551
+ Two changes needed (`docs/` is already in the `files` array — it covers all files under `docs/`, including the generated catalog):
552
+
553
+ - [ ] **Step 3A: Add `catalog:generate` script to `package.json`**
554
+
555
+ In the `scripts` object, add this entry (place it near `exports:validate`):
556
+ ```json
557
+ "catalog:generate": "tsx scripts/generate-component-catalog.ts",
558
+ ```
559
+
560
+ - [ ] **Step 3B: Update the `build` script**
561
+
562
+ Current value:
563
+ ```
564
+ "pnpm build:panda && pnpm typecheck && pnpm build:lib && pnpm build:types && pnpm exports:validate && pnpm codex:generate"
565
+ ```
566
+
567
+ Updated value (insert `catalog:generate` after `exports:validate`, before `codex:generate`):
568
+ ```
569
+ "pnpm build:panda && pnpm typecheck && pnpm build:lib && pnpm build:types && pnpm exports:validate && pnpm catalog:generate && pnpm codex:generate"
570
+ ```
571
+
572
+ - [ ] **Step 3C: Run the full build and confirm it passes**
573
+
574
+ ```bash
575
+ pnpm build
576
+ ```
577
+
578
+ Expected:
579
+ - Build completes with exit code 0
580
+ - `docs/component-catalog.md` is updated with fresh date/version
581
+ - Build output shows `catalog:generate` running between `exports:validate` and `codex:generate`
582
+
583
+ - [ ] **Step 3D: Confirm catalog file will ship with the package**
584
+
585
+ `docs/` is already in the `files` array (`package.json` line 197: `"docs"`), so `docs/component-catalog.md` is covered without adding a more specific entry. Verify:
586
+
587
+ ```bash
588
+ pnpm pack --dry-run 2>&1 | grep component-catalog
589
+ ```
590
+
591
+ Expected: `docs/component-catalog.md` appears in the output.
592
+
593
+ - [ ] **Step 3E: Commit**
594
+
595
+ ```bash
596
+ git add package.json
597
+ git commit -m "feat(catalog): wire catalog:generate into build pipeline
598
+
599
+ Adds catalog:generate npm script and inserts it between exports:validate
600
+ and codex:generate in the build sequence. The docs/ entry in the files
601
+ array already covers docs/component-catalog.md for package publishing."
602
+ ```
603
+
604
+ ---
605
+
606
+ ## Task 4: Final Verification
607
+
608
+ Run each of these in order and confirm all pass:
609
+
610
+ - [ ] **4A:** `pnpm exports:validate` → exits 0; Phase 1 prints success; Phase 2 warns about 6 missing components
611
+ - [ ] **4B:** `pnpm catalog:generate` → exits 0; `docs/component-catalog.md` is updated
612
+ - [ ] **4C:** `pnpm build` → exits 0; full build succeeds end to end
613
+ - [ ] **4D:** Inspect `docs/component-catalog.md`:
614
+ - `grep "^## " docs/component-catalog.md | sort | uniq -d` prints nothing (no duplicate headings)
615
+ - Version header matches `package.json` version (`0.26.0`)
616
+ - Date is today (`2026-04-03`)
617
+ - Exactly 6 stub warnings appear for: AbsoluteCenter, Divider, Group, Icon, SettingsPopover, StudioControls
618
+
619
+ - [ ] **4E: Open a PR**
620
+
621
+ ```bash
622
+ # Confirm you are on a feature branch (not dev or main)
623
+ git branch
624
+
625
+ # If all 3 commits are on the current branch:
626
+ git push -u origin HEAD
627
+
628
+ gh pr create --title "feat: component catalog generation pipeline" \
629
+ --body "$(cat <<'EOF'
630
+ ## Summary
631
+
632
+ - Adds `scripts/generate-component-catalog.ts` that auto-generates `docs/component-catalog.md` from live exports and the catalog story
633
+ - Extends `scripts/validate-exports.ts` with Phase 2: catalog coverage check (non-fatal warning while 6 components lack catalog entries)
634
+ - Wires `catalog:generate` into the `build` sequence after `exports:validate`
635
+
636
+ ## Parser note
637
+
638
+ Component identity is derived from *source path* of export statements, not individual symbol names. Named symbols exported from compound modules (e.g. `AddScenarioDialog` from `./ScenarioQueue`) are not separately tracked — only the module-level name is.
639
+
640
+ ## Known stub warnings (expected)
641
+
642
+ 6 components are exported but have no entry in `ComponentCatalog.stories.tsx`:
643
+ AbsoluteCenter, Divider, Group, Icon, SettingsPopover, StudioControls
644
+
645
+ These warn at build time. A follow-up task should add them to the catalog story and harden Phase 2 to `process.exit(1)`.
646
+
647
+ ## Test plan
648
+
649
+ - [ ] `pnpm exports:validate` exits 0, both phases print, Phase 2 warns about 6 components
650
+ - [ ] `pnpm catalog:generate` exits 0, file non-empty
651
+ - [ ] `pnpm build` exits 0 end to end
652
+ - [ ] `docs/component-catalog.md` has correct version/date, no duplicate headings
653
+
654
+ 🤖 Generated with [Claude Code](https://claude.com/claude-code)
655
+ EOF
656
+ )"
657
+ ```
658
+
659
+ ---
660
+
661
+ ## Constraints
662
+
663
+ - Do **not** modify `stories/ComponentCatalog.stories.tsx` — it is the source of truth, not a target
664
+ - Do **not** modify `src/components/index.ts` — read it, never write it
665
+ - No hard-coded component names in either script
666
+ - Use `tsx`-compatible TypeScript (no transpilation step)
667
+ - Match `validate-exports.ts` code style exactly: same color constants (`RED`, `YELLOW`, `GREEN`, `RESET`), same ESM imports (`node:fs`, `node:path`, `fileURLToPath`), same comment header style
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@discourser/design-system",
3
- "version": "0.26.0",
3
+ "version": "0.27.0",
4
4
  "description": "Aesthetic-agnostic design system with Panda CSS and Ark UI",
5
5
  "type": "module",
6
6
  "publishConfig": {
@@ -198,7 +198,7 @@
198
198
  ],
199
199
  "scripts": {
200
200
  "dev": "pnpm build:css && pnpm docs:generate && storybook dev -p 6006",
201
- "build": "pnpm build:panda && pnpm typecheck && pnpm build:lib && pnpm build:types && pnpm exports:validate && pnpm codex:generate",
201
+ "build": "pnpm build:panda && pnpm typecheck && pnpm build:lib && pnpm build:types && pnpm exports:validate && pnpm catalog:generate && pnpm codex:generate",
202
202
  "build:panda": "panda codegen",
203
203
  "build:css": "panda cssgen --outfile dist/styles.css",
204
204
  "build:lib": "tsup",
@@ -233,6 +233,7 @@
233
233
  "version": "changeset version",
234
234
  "ci:version": "pnpm exec changeset version",
235
235
  "release": "pnpm build && changeset publish --access public",
236
+ "catalog:generate": "tsx scripts/generate-component-catalog.ts",
236
237
  "exports:validate": "tsx scripts/validate-exports.ts"
237
238
  },
238
239
  "peerDependencies": {