import fs from 'node:fs' import path from 'node:path' import { loadCsf } from 'storybook/internal/csf-tools' // Run from the Track B root: node .storybook/qa/generate-inventory.mjs // Only writes Storybook documentation; never renames or removes stories. const root = process.cwd() if (!fs.existsSync(path.join(root, '.storybook/main.ts'))) throw new Error('Run from the HMDesign repository root') const walk = dir => fs.readdirSync(dir, { withFileTypes: true }).flatMap(e => e.isDirectory() ? walk(path.join(dir, e.name)) : [path.join(dir, e.name)]) const files = walk(path.join(root, 'src')).filter(f => /\.stories\.tsx?$/.test(f)).sort() const inventory = files.map(file => { const csf = loadCsf(fs.readFileSync(file, 'utf8'), { makeTitle: title => title }).parse() const title = csf.meta.title if (!title) throw new Error(`Missing meta.title: ${file}`) const metaTags = csf.meta.tags ?? [] const stories = Object.entries(csf._stories).map(([exportName, story]) => { const storyTags = story.tags ?? [] const effectiveTags = [...new Set([...metaTags, ...storyTags])] const utility = /Playground|Compatibility/.test(exportName) || /Verification$/.test(exportName) || exportName === 'Verification' const category = utility || effectiveTags.includes('internal') ? 'UTILITY' : effectiveTags.includes('verification') ? 'VERIFICATION' : 'PRIMARY' const duplicateCandidate = effectiveTags.includes('verification') && /^(Default|Sizes|Roles|Variants|States|Basic|Selected|Disabled|Horizontal|Vertical)$/.test(exportName) return { id: story.id, exportName, name: story.name, storyTags, effectiveTags, category, decision: effectiveTags.some(tag => ['verification', 'internal'].includes(tag)) ? 'HIDE' : 'KEEP', candidate: duplicateCandidate ? 'MERGE_CANDIDATE' : null, reason: duplicateCandidate ? 'Representative board may cover the same specimen; keep direct QA until behavioral equivalence is reviewed.' : effectiveTags.some(tag => ['verification', 'internal'].includes(tag)) ? 'Keep direct URL and tag-filter access; excluded from default sidebar.' : 'Representative story or distinct reference page.' } }) return { file: path.relative(root, file).replaceAll('\\', '/'), title, metaTags, stories } }) const all = inventory.flatMap(f => f.stories) const summary = { storyFiles: files.length, stories: all.length, categories: Object.fromEntries(['PRIMARY', 'VERIFICATION', 'UTILITY', 'LEGACY / DUPLICATE'].map(c => [c, all.filter(s => s.category === c).length])), decisions: Object.fromEntries(['KEEP', 'HIDE', 'MERGE_CANDIDATE', 'DELETE_CANDIDATE'].map(c => [c, all.filter(s => s.decision === c || s.candidate === c).length])), hierarchy: ['Foundation', 'Components', 'Patterns', 'Center Graphic', 'Theme'] } const output = path.join(root, '.storybook', 'qa') fs.mkdirSync(output, { recursive: true }) fs.writeFileSync(path.join(output, 'story-inventory.json'), JSON.stringify({ summary, inventory }, null, 2) + '\n') const lines = ['# Track B Story inventory', '', 'Generated from every `src/**/*.stories.ts(x)` using the installed Storybook CSF parser. Run `node .storybook/qa/generate-inventory.mjs` from Track B to refresh.', '', 'This is a classification record. No Story IDs, exports or titles are renamed. HIDE uses the existing verification filter, not deletion. MERGE_CANDIDATE is advisory and can coexist with HIDE. Utility includes executable checks and compatibility/playground tools. No legacy or delete candidate is asserted without evidence.', '', '## Summary', '', ...Object.entries(summary.categories).map(([k,v]) => `- ${k}: ${v}`), ...Object.entries(summary.decisions).map(([k,v]) => `- ${k}: ${v}`), `- Story files: ${summary.storyFiles}`, `- Stories: ${summary.stories}`, `- Hierarchy: ${summary.hierarchy.join(' → ')}`, '', '## Full inventory', ''] for (const f of inventory) { lines.push(`### ${f.title}`, '', `Source: \`${f.file}\`; meta.tags: ${JSON.stringify(f.metaTags)}`, '') for (const s of f.stories) lines.push(`- ${s.exportName} — ${s.name}; tags: ${JSON.stringify(s.effectiveTags)}; ${s.category}; ${s.decision}${s.candidate ? `; ${s.candidate}` : ''}. ${s.reason}`) lines.push('') } fs.writeFileSync(path.join(output, 'STORY-INVENTORY.md'), lines.join('\n')) console.log(JSON.stringify(summary))