/** * blocks-check — the mechanical guard of the blocks tier's B contract * (docs/architecture/blocks.md). Small on purpose: blocks are exempt from the * acceptance matrix, NOT from discipline. * * Checks: * B-2 no raw interactive natives (`' ), [] ); expectRules( 'a real native BELOW a multi-line comment is still flagged', scanBlockFile( inHero, 'fixture', '\n' ), ['B-2'] ); expectRules( 'README without the landmark declaration flagged', scanReadme( 'fixture', '# Hero\n\n## Function\n\n## Composition map\n\n## Decisions\n\n## Equivalencias\n\n## Gaps\n' ), ['B-8'] ); expectRules( 'README without the equivalence matrix flagged', scanReadme( 'fixture', '# Hero\n\n## Function\n\n## Composition map\n\n**Landmark + headings**: a section.\n\n## Decisions\n\n## Gaps\n' ), ['B-9'] ); expectRules( 'README missing a template section flagged', scanReadme( 'fixture', '# Hero\n\n## Function\n\n**Landmark + headings**: a `
`.\n\n## Gaps\n' ), ['B-9', 'B-9', 'B-9'] ); expectRules( 'a complete README clean', scanReadme( 'fixture', '# Hero\n\n## Function\n\n## Composition map\n\n**Landmark + headings**: a `
`.\n\n## Decisions\n\n## Equivalencias\n\n## Gaps\n' ), [] ); const catalog = parseCatalog( "[{ slug: 'hero', label: 'Hero', shipped: true }, { slug: 'kanban', label: 'Kanban', shipped: false }]" ); if (catalog.get('hero') !== true || catalog.get('kanban') !== false || catalog.size !== 2) failures.push(`catalog parse: expected hero=true kanban=false, got ${[...catalog]}`); if (parseCatalog("[{ label: 'Hero', shipped: true }]").size !== 0) failures.push('catalog parse: an entry without a slug must not become a row'); expectRules( 'block without a catalog entry flagged', scanCatalog(['hero'], new Map(), 'fixture'), ['B-9'] ); expectRules( 'block whose entry says shipped: false flagged', scanCatalog(['hero'], new Map([['hero', false]]), 'fixture'), ['B-9'] ); expectRules( 'catalog shipping a block that does not exist flagged', scanCatalog([], new Map([['ghost', true]]), 'fixture'), ['B-9'] ); expectRules( 'tree and catalog in agreement clean', scanCatalog( ['hero'], new Map([ ['hero', true], ['kanban', false] ]), 'fixture' ), [] ); return failures; } function main(): void { const selfFailures = selfTest(); if (selfFailures.length > 0) { for (const f of selfFailures) console.error(`SELF-TEST FAIL — ${f}`); console.error(`\nblocks-check: the guard's own detectors are broken (${selfFailures.length})`); process.exit(1); } const violations: Violation[] = []; let blockFiles: string[] = []; let blockDirs: string[] = []; if (existsSync(BLOCKS_DIR)) { blockFiles = walk(BLOCKS_DIR, /\.(ts|svelte|css)$/); blockDirs = readdirSync(BLOCKS_DIR).filter((e) => statSync(join(BLOCKS_DIR, e)).isDirectory()); } for (const file of blockFiles) { const rel = relative(REPO, file); if (file.endsWith('.css')) { violations.push({ rule: 'D-BLK.2', file: rel, line: 1, detail: 'a block ships no .css file — layout-components-first' }); continue; } if (/\.test\./.test(file)) continue; violations.push(...scanBlockFile(file, rel, readFileSync(file, 'utf8'))); } const catalog = existsSync(CATALOG_FILE) ? parseCatalog(readFileSync(CATALOG_FILE, 'utf8')) : null; if (catalog === null) { violations.push({ rule: 'B-9', file: relative(REPO, CATALOG_FILE), line: 1, detail: 'the demo catalog is missing — the rail and the gallery read it as their single list' }); } const shippedBlocks = new Set(); for (const dir of blockDirs) { const hasCode = walk(join(BLOCKS_DIR, dir), /\.(ts|svelte)$/).length > 0; if (!hasCode) continue; shippedBlocks.add(dir); const readme = join(BLOCKS_DIR, dir, 'README.md'); if (!existsSync(readme)) { violations.push({ rule: 'B-9', file: `src/uix/blocks/${dir}`, line: 1, detail: 'block folder without README.md (Function · Composition map · Decisions · Gaps)' }); } else { violations.push(...scanReadme(relative(REPO, readme), readFileSync(readme, 'utf8'))); } if (!existsSync(join(DEMO_DIR, dir, '+page.svelte'))) { violations.push({ rule: 'B-9', file: `src/uix/blocks/${dir}`, line: 1, detail: `block without a demo route (web/routes/blocks/${dir}/+page.svelte)` }); } } if (catalog !== null) { violations.push(...scanCatalog([...shippedBlocks], catalog, relative(REPO, CATALOG_FILE))); } let outsideCount = 0; for (const root of OUTSIDE_ROOTS) { if (!existsSync(root)) continue; for (const file of walk(root, /\.(ts|svelte)$/)) { outsideCount++; violations.push(...scanOutsideFile(file, relative(REPO, file), readFileSync(file, 'utf8'))); } } if (violations.length > 0) { for (const v of violations) { console.error(`ERROR [${v.rule}] ${v.file}:${v.line} — ${v.detail}`); } console.error( `\nblocks-check: ${violations.length} error(s) across ${blockDirs.length} block(s)` ); process.exit(1); } console.log( `blocks-check: self-test OK · 0 error(s) across ${blockDirs.length} block(s) (${blockFiles.length} files; ${outsideCount} canon/arts/libs/packs files scanned for direction)` ); } main();