/** * Drift lock — `screen-grammar:v1`, three faces that must agree: * 1. the markdown table carried by create-screen/references/smartcomponents.md * (the authoring reference, deployed standalone) ⇔ lib/screen-grammar.ts ; * 2. every bullet a reader is credited with is READ by that reader's source — * screen-blocks.ts switches on `SCREEN_BULLET_KEY.` (no string literal), * lib/ba-screens.ts carries the label in one of its regexes ; * 3. every key screen-blocks.ts switches on has its row, so a bullet that only * the parser knows cannot exist. * Edit the table AND the SSOT AND the parser together, or this suite fails. */ import { readFileSync } from 'node:fs' import { dirname, join } from 'node:path' import { fileURLToPath } from 'node:url' import { describe, expect, it } from 'vitest' import { BA_SCREENS_BULLETS } from '../ba-screens.js' import { SCREEN_BULLETS, SCREEN_BULLET_KEY, SCREEN_GRAMMAR_VERSION, foldBulletLabel, renderScreenGrammarTable, } from '../screen-grammar.js' const skillsRoot = join(dirname(fileURLToPath(import.meta.url)), '..', '..') const CARRIER = 'business-analyse/create-screen/references/smartcomponents.md' const SCREEN_BLOCKS = 'business-analyse/audit-run/cli/audit-ba/rules/screen-blocks.ts' const BA_SCREENS = 'lib/ba-screens.ts' function read(rel: string): string { return readFileSync(join(skillsRoot, rel), 'utf8') } function extractBlock(md: string, marker: string): string { const re = new RegExp(`([\\s\\S]*?)`) const m = md.match(re) if (!m) throw new Error(`marker "${marker}" not found in ${CARRIER}`) return m[1]! } function tableLines(block: string): string[] { return block .split('\n') .map((l) => l.trim()) .filter((l) => l.startsWith('|')) } describe('screen-grammar drift — smartcomponents.md ⇔ lib/screen-grammar.ts', () => { it('carries the screen-grammar:v1 table byte-equal to renderScreenGrammarTable()', () => { const carried = tableLines(extractBlock(read(CARRIER), SCREEN_GRAMMAR_VERSION)).join('\n') expect(carried).toBe(renderScreenGrammarTable()) }) it('keys are the folded labels (aliases excepted, groups keyed by their first word)', () => { for (const b of SCREEN_BULLETS) { const folded = foldBulletLabel(b.label.replace(/\s*«\s*X\s*»\s*$/, '')) expect(b.key, b.label).toBe(folded) expect(b.readers.length, `${b.label} has no reader — a bullet nobody reads is not grammar`).toBeGreaterThan(0) } }) }) describe('screen-grammar drift — lib/screen-grammar.ts ⇔ the parsers', () => { const blocksSrc = read(SCREEN_BLOCKS) const baScreensSrc = read(BA_SCREENS) it('screen-blocks.ts compares on SCREEN_BULLET_KEY.* only — no string literal key', () => { expect(blocksSrc).not.toMatch(/key === '/) expect(blocksSrc).not.toMatch(/key === "/) expect(blocksSrc).not.toMatch(/key\.startsWith\('/) }) it('every SCREEN_BULLET_KEY id used by screen-blocks.ts maps to a row credited to screen-blocks', () => { const used = new Set([...blocksSrc.matchAll(/SCREEN_BULLET_KEY\.(\w+)/g)].map((m) => m[1]!)) expect(used.size).toBeGreaterThan(0) const readKeys = new Set() for (const id of used) { const key = (SCREEN_BULLET_KEY as Record)[id] expect(key, `SCREEN_BULLET_KEY.${id} is not exported by lib/screen-grammar.ts`).toBeDefined() readKeys.add(key!) } for (const key of readKeys) { const row = SCREEN_BULLETS.find( (b) => b.readers.includes('screen-blocks') && (b.key === key || b.aliases.includes(key) || (key === SCREEN_BULLET_KEY.actionsPrefix && b.key.startsWith(key))), ) expect(row, `key "${key}" is switched on by screen-blocks.ts but has no screen-grammar row crediting screen-blocks`).toBeDefined() } }) it('every row credited to screen-blocks is switched on by screen-blocks.ts', () => { const used = new Set([...blocksSrc.matchAll(/SCREEN_BULLET_KEY\.(\w+)/g)].map((m) => (SCREEN_BULLET_KEY as Record)[m[1]!])) for (const b of SCREEN_BULLETS.filter((r) => r.readers.includes('screen-blocks'))) { // Section/Onglet groups are read through GROUP_RE, not the key switch. if (b.key === 'section' || b.key === 'onglet') { expect(blocksSrc, `${b.label} group regex`).toMatch(/\(Section\|Onglet\)/) continue } const hit = used.has(b.key) || (b.key.startsWith(SCREEN_BULLET_KEY.actionsPrefix) && used.has(SCREEN_BULLET_KEY.actionsPrefix)) expect(hit, `row "${b.label}" credits screen-blocks but its key "${b.key}" is not switched on`).toBe(true) } }) it('BA_SCREENS_BULLETS (lib/ba-screens.ts) equals the rows credited to ba-screens', () => { const credited = SCREEN_BULLETS.filter((r) => r.readers.includes('ba-screens')).map((r) => r.key).sort() expect([...BA_SCREENS_BULLETS].sort()).toEqual(credited) }) it('every row credited to ba-screens has its label in a lib/ba-screens.ts regex', () => { const probes: Record = { entite: /Entité/, permission: /Permission/, mode: /\*\*Mode/, colonnes: /\*\*Colonnes/, onglet: /Onglet\\s\*«/, 'onglet lie': /Onglet lié/, 'sans onglets lies': /Sans onglets liés/, 'champ statut': /Champ statut/, carte: /\*\*Carte/, navigation: /\*\*Navigation/, } for (const b of SCREEN_BULLETS.filter((r) => r.readers.includes('ba-screens'))) { const probe = probes[b.key] expect(probe, `no source probe for "${b.label}" — add one when crediting ba-screens`).toBeDefined() expect(baScreensSrc, `${b.label} is credited to ba-screens but its regex is gone`).toMatch(probe!) } }) })