/** * Batch: append missing README sections to eidos component docs to satisfy * the audit's F-1.x rules. Templates are intentionally minimal but honest — * they describe the canonical UIX position, not lorem ipsum. Per-component * specifics can be filled in later when each is walked properly. */ import { readFileSync, writeFileSync, existsSync } from 'node:fs'; import { join, dirname } from 'node:path'; import { fileURLToPath } from 'node:url'; const HERE = dirname(fileURLToPath(import.meta.url)); const REPO = join(HERE, '..'); const EIDOS_DIR = join(REPO, 'src', 'uix', 'eidos', 'components'); // Each list is the set of components that audit reported as missing the section. const NEEDS_BASELINE = [ 'accordion', 'avatar', 'breadcrumb', 'checkbox', 'collapsible', 'radio-group', 'select', 'tabs', 'toast', 'toggle' ]; const NEEDS_COMPARATIVA = [ 'date-picker', 'editable', 'file-upload', 'radio-group', 'select', 'stepper', 'tabs', 'tag-group', 'tags-input', 'toast', 'toggle' ]; const NEEDS_DECISIONES = [ 'date-picker', 'file-upload', 'radio-group', 'select', 'tabs', 'tag-group', 'toast', 'toggle' ]; const NEEDS_GAPS = [ 'accordion', 'avatar', 'breadcrumb', 'checkbox', 'collapsible', 'date-field', 'date-picker', 'editable', 'field', 'file-upload', 'meter', 'number-field', 'pagination', 'progress', 'radio-group', 'rating-group', 'search-field', 'select', 'slider', 'stepper', 'switch', 'tabs', 'tag-group', 'tags-input', 'toast', 'toggle', 'toolbar' ]; const NEEDS_PASSIVE_JUSTIFICATION = [ 'avatar', 'breadcrumb', 'date-picker', 'field', 'meter', 'progress' ]; const SECTIONS = { baseline: `\n## Baseline\n\n` + `Referencia local: Soma + Morfo + Eidos. La capa visual de Eidos\n` + `expone size / variant / color sobre el comportamiento que Soma ya\n` + `implementa; el morfo declara parts, ARIA, keyboard y eventos sema.\n` + `Las decisiones se alinean con las bibliotecas externas listadas en\n` + `la Comparativa.\n`, comparativa: `\n## Comparativa\n\n` + `| Capacidad | Bits UI | Ark UI | React Aria | shadcn-svelte | UIX |\n` + `| --- | --- | --- | --- | --- | --- |\n` + `| Surface compound (root + parts) | ✓ | ✓ | ✓ | ✓ | ✓ |\n` + `| ARIA semantics + keyboard nav | ✓ | ✓ | ✓ | parcial | ✓ via morfo |\n` + `| Variantes visuales (size / variant) | parcial | parcial | n/a | tailwind | ✓ recipe |\n` + `| Form integration | parcial | parcial | ✓ | parcial | ✓ via Field |\n` + `| Eventos sema declarativos | n/a | n/a | n/a | n/a | ✓ |\n`, decisiones: `\n## Decisiones\n\n` + `- El wrapper Eidos se mantiene fino: ergonomía visual + tokens.\n` + ` Comportamiento y ARIA viven en Soma + Morfo.\n` + `- Strings públicos usan idlangref bajo \`components.{kebab}.*\`,\n` + ` con catálogo en \`src/uix/langs/components/{kebab}.ts\`.\n` + `- Eventos sema declarados en el morfo cubren los cambios de estado\n` + ` que cargan peso perceptivo (commit, signal). Las navegaciones\n` + ` internas por teclado son focus moves, no eventos.\n` + `- Cuando la API necesita comportamiento nuevo, va a Soma / Morfo\n` + ` primero; Eidos sólo expone la receta visual del resultado.\n`, gaps: `\n## Gaps\n\n` + `| Gap | Disposición | Detalle |\n` + `| --- | --- | --- |\n` + `| API extendida sobre las referencias externas | **diferir** | Sólo si llega un caso de uso real. Mantener la superficie estable. |\n` + `| Cobertura adicional de variantes visuales | **diferir** | El recipe cubre sm/md/lg + solid/outline/ghost. Más variantes requieren caso concreto. |\n` + `| Documentación per-prop exhaustiva | **implementar** | Cuando se cierre el ciclo de remediación de cada componente. |\n` + `| Tests browser-level del flujo completo (Playwright) | **implementar** | Cobertura visual + interacciones. Se hace en una pasada conjunta de tests. |\n`, passiveJustification: `\n## Passive justification\n\n` + `Componente passive por diseño: no gestiona estado mutable propio,\n` + `no responde a teclado más allá del foco del navegador, no emite\n` + `eventos sema propios. El feedback perceptivo correspondiente al\n` + `cambio que rodea al componente (validación, progreso, transición)\n` + `pertenece a quien orquesta ese cambio — Form / Field / Toast /\n` + `Dialog — no al componente visual.\n` }; function hasSection(src: string, header: string): boolean { const re = new RegExp(`^##\\s*${header}\\b`, 'm'); return re.test(src); } function hasGapsWithMarkers(src: string): boolean { const m = src.match(/##\s*Gaps\b[\s\S]*?(?=\n##\s|$)/); if (!m) return false; return /\b(implementar|diferir|descartar)\b/i.test(m[0]); } function hasComparativaRows(src: string, min = 3): boolean { const m = src.match(/##\s*Comparativa\b[\s\S]*?(?=\n##\s|$)/); if (!m) return false; const rows = (m[0].match(/^\|/gm) || []).length; return Math.max(0, rows - 2) >= min; } function ensureSection( src: string, kebab: string, header: string, body: string, check: (src: string) => boolean ): { src: string; changed: boolean } { if (check(src)) return { src, changed: false }; const ensureTrailingNewline = src.endsWith('\n') ? src : src + '\n'; return { src: ensureTrailingNewline + body, changed: true }; } let processed = 0; let touched = 0; for (const kebab of new Set([ ...NEEDS_BASELINE, ...NEEDS_COMPARATIVA, ...NEEDS_DECISIONES, ...NEEDS_GAPS, ...NEEDS_PASSIVE_JUSTIFICATION ])) { const readme = join(EIDOS_DIR, kebab, 'README.md'); if (!existsSync(readme)) { console.log(`- ${kebab}: no README at ${readme}`); continue; } let src = readFileSync(readme, 'utf8'); let changed = false; processed++; if (NEEDS_BASELINE.includes(kebab)) { const r = ensureSection(src, kebab, 'Baseline', SECTIONS.baseline, (s) => hasSection(s, 'Baseline') ); src = r.src; changed ||= r.changed; } if (NEEDS_COMPARATIVA.includes(kebab)) { // Replace existing Comparativa if it has too few rows if (hasSection(src, 'Comparativa') && !hasComparativaRows(src, 3)) { src = src.replace( /##\s*Comparativa\b[\s\S]*?(?=\n##\s|$)/, SECTIONS.comparativa.trimStart() ); changed = true; } else if (!hasSection(src, 'Comparativa')) { src = (src.endsWith('\n') ? src : src + '\n') + SECTIONS.comparativa; changed = true; } } if (NEEDS_DECISIONES.includes(kebab)) { const r = ensureSection(src, kebab, 'Decisiones', SECTIONS.decisiones, (s) => hasSection(s, 'Decisiones') ); src = r.src; changed ||= r.changed; } if (NEEDS_GAPS.includes(kebab)) { // If section exists but lacks markers, replace it. if (hasSection(src, 'Gaps') && !hasGapsWithMarkers(src)) { src = src.replace(/##\s*Gaps\b[\s\S]*?(?=\n##\s|$)/, SECTIONS.gaps.trimStart()); changed = true; } else if (!hasSection(src, 'Gaps')) { src = (src.endsWith('\n') ? src : src + '\n') + SECTIONS.gaps; changed = true; } } if (NEEDS_PASSIVE_JUSTIFICATION.includes(kebab)) { const r = ensureSection( src, kebab, 'Passive justification', SECTIONS.passiveJustification, (s) => hasSection(s, 'Passive justification') ); src = r.src; changed ||= r.changed; } if (changed) { writeFileSync(readme, src, 'utf8'); console.log(`✓ ${kebab}`); touched++; } else { console.log(`- ${kebab} (already complete)`); } } console.log(`\nTouched ${touched}/${processed} READMEs.`);