/** * cli:derive-permission-floor — derive.ts * * Pure derivation: menu tree → floor mirror rows. Calls the SAME * `floorPathsForNode` / `FLOOR_BY_GRAIN` the seed generator uses * (lib/permission-actions.ts), so the BA mirror and the actual seed can never * disagree on what the floor is. */ import { FLOOR_BY_GRAIN, validateNodeCode, } from '../../../../lib/permission-actions.js' import { listResources, listSections, navCodeOf } from './menu-nodes.js' import type { DerivePermissionFloorReport, FloorNodeRow } from './types.js' export interface FloorSources { sectionsOf: (app: string, module: string) => string[] resourcesOf: (app: string, module: string, section: string) => string[] } /** Default filesystem-backed sources. */ export function fsFloorSources(baRoot: string): FloorSources { return { sectionsOf: (app, module) => listSections(baRoot, app, module), resourcesOf: (app, module, section) => listResources(baRoot, app, module, section), } } export function derivePermissionFloor( appFolder: string, moduleFolder: string, sources: FloorSources, ): Pick { const appCode = navCodeOf(appFolder) const moduleCode = navCodeOf(moduleFolder) const warnings: string[] = [] const rows: FloorNodeRow[] = [ // The application row is PROJECT-scoped — repeated in each module's block // (rbac.md is authoritative at the module) and annotated as such by the // renderer. The seed emits it once per app. { nodePath: appCode, grain: 'application', actions: FLOOR_BY_GRAIN.application }, { nodePath: `${appCode}.${moduleCode}`, grain: 'module', actions: FLOOR_BY_GRAIN.module }, ] const sections = sources.sectionsOf(appFolder, moduleFolder) let resourceCount = 0 for (const section of sections) { const sectionCode = navCodeOf(section) const reserved = validateNodeCode(sectionCode) if (reserved) warnings.push(`section "${section}": ${reserved}`) rows.push({ nodePath: `${appCode}.${moduleCode}.${sectionCode}`, grain: 'section', actions: FLOOR_BY_GRAIN.section, }) for (const resource of sources.resourcesOf(appFolder, moduleFolder, section)) { const resourceCode = navCodeOf(resource) const reservedRes = validateNodeCode(resourceCode) if (reservedRes) warnings.push(`resource "${section}/${resource}": ${reservedRes}`) resourceCount++ rows.push({ nodePath: `${appCode}.${moduleCode}.${sectionCode}.${resourceCode}`, grain: 'resource', actions: FLOOR_BY_GRAIN.resource, }) } } return { app: appFolder, module: moduleFolder, rows, totals: { nodes: rows.length, sections: sections.length, resources: resourceCount, floorPaths: rows.reduce((sum, r) => sum + r.actions.length, 0), }, warnings, } }