/** * Drift lock: the per-grain permission FLOOR is canonical in * `lib/permission-actions.ts` (FLOOR_BY_GRAIN — user decision 2026-08-07: * app/module carry `access` only, section/resource carry the full 7). * Markdown carriers embed the floor table between * `` markers; this suite pins their parsed rows * to the export. Edit ALL carriers or it fails. * * Code carriers are locked elsewhere by construction: * - scaffold-core-seed derives the floor via FLOOR_BY_GRAIN (floor.test.ts); * - derive-permission-floor calls floorPathsForNode (derive.test.ts). */ import { readFileSync } from 'node:fs' import { dirname, join } from 'node:path' import { fileURLToPath } from 'node:url' import { describe, expect, it } from 'vitest' import { FLOOR_BY_GRAIN, PERMISSION_FLOOR_MARKER, PERMISSION_GRAINS, renderPermissionFloorBlock, } from '../permission-actions.js' const skillsRoot = join(dirname(fileURLToPath(import.meta.url)), '..', '..') const FLOOR_CARRIERS = [ 'business-analyse/create-rbac/SKILL.md', 'development/audit-dev-core/SKILL.md', 'ba-develop/references/gates.md', ] function readSkill(rel: string): string { return readFileSync(join(skillsRoot, rel), 'utf8') } function extractBlock(md: string, file: string): string { const re = new RegExp( `([\\s\\S]*?)`, ) const m = md.match(re) if (!m) throw new Error(`marker "${PERMISSION_FLOOR_MARKER}" not found in ${file}`) return m[1] } /** Parse the floor table rows: grain label → ordered action list. */ function parseFloorRows(block: string): Record { const rows: Record = {} for (const line of block.split('\n')) { const m = line.trim().match(/^\|\s*(Application|Module|Section|Resource)\s*\|[^|]*\|(.*)\|$/) if (!m) continue rows[m[1].toLowerCase()] = [...m[2].matchAll(/`([a-z]+)`/g)].map((x) => x[1]) } return rows } describe.each(FLOOR_CARRIERS)('permission-floor drift — %s', (file) => { it('carries the permission-floor:v1 table equal to FLOOR_BY_GRAIN', () => { const rows = parseFloorRows(extractBlock(readSkill(file), file)) expect(Object.keys(rows).sort()).toEqual([...PERMISSION_GRAINS].sort()) for (const grain of PERMISSION_GRAINS) { expect(rows[grain], `${file} → grain ${grain}`).toEqual([...FLOOR_BY_GRAIN[grain]]) } }) }) describe('renderPermissionFloorBlock', () => { it('round-trips through the same parser the carriers are checked with', () => { const rows = parseFloorRows(extractBlock(renderPermissionFloorBlock(), '')) for (const grain of PERMISSION_GRAINS) { expect(rows[grain]).toEqual([...FLOOR_BY_GRAIN[grain]]) } }) })