/** * lib/screen-grammar.ts — THE list of `screen.md` bullets the CLI's deterministic * parsers actually READ (`screen-grammar:v1`). * * The bullet grammar of a screen block is authored across create-screen/SKILL.md * and five `levels/*.md` files, and read by TWO parsers that grew separately: * - audit-run/cli/audit-ba/rules/screen-blocks.ts — field LISTS + groups + * actions (the audit's SCR-017/018/019 evidence), * - lib/ba-screens.ts — headings, Entité/Permission/Mode, the related-tab and * the kanban grammars (derive-related-tabs, derive-kanban-spec, lookup grants). * Nothing pinned the three together: a bullet added to one level file and read by * neither parser was documented-but-dead, and a key read by a parser that no * level file taught stayed invisible to the author. This module is the SSOT the * parsers import their keys from, and `create-screen/references/smartcomponents.md` * carries the same table under the `screen-grammar:v1` marker — drift-locked by * lib/__tests__/screen-grammar-drift.test.ts in BOTH directions (table ⇔ this * file ⇔ the parsers' source). * * Deliberately NOT listed: bullets the LLM synthesis (create-prd) reads but no * deterministic parser does (`Segments`, `Tri par défaut`, `État vide`, `Résumé`, * `Widgets`, `QuickLinks`, `Sources`, `Mobile`, list-level `Navigation`). Adding a * parser for one of them means adding its row here — that is the point. */ export const SCREEN_GRAMMAR_VERSION = 'screen-grammar:v1' as const export type ScreenBulletReader = 'screen-blocks' | 'ba-screens' export interface ScreenBulletDef { /** The bullet as authored between `**…**` (label form, French, accents kept). */ label: string /** The folded key the parsers compare on (NFD-stripped, lower-cased, `’` → `'`). */ key: string /** Folded spellings the CLI ALSO accepts (authoring typos it tolerates on purpose). */ aliases: readonly string[] /** The deterministic readers of this bullet. */ readers: readonly ScreenBulletReader[] /** SmartComponent kinds the bullet is valid on (`*` = every kind). */ kinds: readonly string[] /** The authoring reference that teaches it (create-screen/…). */ level: string } /** * Folded keys screen-blocks.ts switches on. Named so the parser reads * `SCREEN_BULLET_KEY.colonnes` instead of a string literal — the drift test * refuses a `key === '…'` literal in that file. */ export const SCREEN_BULLET_KEY = { entite: 'entite', casDUsageLies: "cas d'usage lies", /** Tolerated spelling without the apostrophe (`Cas dusage liés`). */ casDUsageLiesAlias: 'cas dusage lies', mode: 'mode', colonnes: 'colonnes', filtres: 'filtres', indicateurs: 'indicateurs', champs: 'champs', cycleDeVie: 'cycle de vie', /** `Actions` and `Actions personnalisées` share the prefix the parser tests. */ actionsPrefix: 'actions', } as const export const SCREEN_BULLETS: readonly ScreenBulletDef[] = [ { label: 'Entité', key: 'entite', aliases: [], readers: ['screen-blocks', 'ba-screens'], kinds: ['*'], level: 'SKILL.md' }, { label: 'Permission', key: 'permission', aliases: [], readers: ['ba-screens'], kinds: ['*'], level: 'SKILL.md' }, { label: "Cas d'usage liés", key: "cas d'usage lies", aliases: ['cas dusage lies'], readers: ['screen-blocks'], kinds: ['*'], level: 'SKILL.md' }, { label: 'Mode', key: 'mode', aliases: [], readers: ['screen-blocks', 'ba-screens'], kinds: ['SmartForm'], level: 'levels/form-screens.md' }, { label: 'Colonnes', key: 'colonnes', aliases: [], readers: ['screen-blocks', 'ba-screens'], kinds: ['SmartListView', 'SmartKanban'], level: 'levels/list-screens.md, levels/kanban-screens.md' }, { label: 'Filtres', key: 'filtres', aliases: [], readers: ['screen-blocks'], kinds: ['SmartListView'], level: 'levels/list-screens.md' }, { label: 'Indicateurs', key: 'indicateurs', aliases: [], readers: ['screen-blocks'], kinds: ['SmartListView'], level: 'levels/list-screens.md' }, { label: 'Champs', key: 'champs', aliases: [], readers: ['screen-blocks'], kinds: ['SmartForm'], level: 'levels/form-screens.md' }, { label: 'Section « X »', key: 'section', aliases: [], readers: ['screen-blocks'], kinds: ['SmartForm'], level: 'levels/form-screens.md' }, { label: 'Onglet « X »', key: 'onglet', aliases: [], readers: ['screen-blocks', 'ba-screens'], kinds: ['SmartForm'], level: 'levels/form-screens.md' }, { label: 'Onglet lié « X »', key: 'onglet lie', aliases: [], readers: ['ba-screens'], kinds: ['SmartForm'], level: 'levels/form-screens.md' }, { label: 'Sans onglets liés', key: 'sans onglets lies', aliases: [], readers: ['ba-screens'], kinds: ['SmartForm'], level: 'levels/form-screens.md' }, { label: 'Cycle de vie', key: 'cycle de vie', aliases: [], readers: ['screen-blocks'], kinds: ['SmartForm'], level: 'levels/form-screens.md' }, { label: 'Actions', key: 'actions', aliases: [], readers: ['screen-blocks'], kinds: ['*'], level: 'SKILL.md' }, { label: 'Actions personnalisées', key: 'actions personnalisees', aliases: [], readers: ['screen-blocks'], kinds: ['*'], level: 'SKILL.md' }, { label: 'Champ statut', key: 'champ statut', aliases: [], readers: ['ba-screens'], kinds: ['SmartKanban'], level: 'levels/kanban-screens.md' }, { label: 'Carte', key: 'carte', aliases: [], readers: ['ba-screens'], kinds: ['SmartKanban'], level: 'levels/kanban-screens.md' }, { label: 'Navigation', key: 'navigation', aliases: [], readers: ['ba-screens'], kinds: ['SmartKanban'], level: 'levels/kanban-screens.md' }, ] /** Fold an authored bullet label to its comparison key — mirror of audit-ba's `fold`. */ export function foldBulletLabel(label: string): string { return label .normalize('NFD') .replace(/[̀-ͯ]/g, '') .replace(/’/g, "'") .toLowerCase() .trim() } /** Render the `screen-grammar:v1` markdown table exactly as smartcomponents.md must carry it. */ export function renderScreenGrammarTable(): string { const rows = SCREEN_BULLETS.map((b) => `| \`**${b.label}**\` | \`${b.key}\` | ${b.aliases.length === 0 ? '—' : b.aliases.map((a) => `\`${a}\``).join(', ')} | ${b.readers.join(', ')} | ${b.kinds.join(', ')} | ${b.level} |`, ) return [ '| Puce | Clé repliée | Alias tolérés | Lecteurs | Types | Référence |', '|------|-------------|---------------|----------|-------|-----------|', ...rows, ].join('\n') }