#!/usr/bin/env node /** * cli:derive-lookup-grants — entry point. * * Deterministic companion of /ba-create-rbac for the DERIVED `lookup` grants * (SmartStack ≥ 3.62). Runs as a MANDATORY post-step of /ba-create-data-model * (the FKs are its input) and re-runs after /ba-reconcile-menu renames. * * Invocation: * npx --prefer-offline tsx skills/business-analyse/create-rbac/cli/derive-lookup-grants/index.ts \ * --spec '{"baRoot":".smartstack/ba","app":"CRM","module":"COMMANDES","mode":"derive"}' \ * [--workdir ] * * - mode=derive → REWRITES the machine-owned block of the consumer module's * rbac.md between `` (the ONLY * part of rbac.md a CLI owns — human rows are never touched; the block is * always rewritten in full, so re-runs are idempotent). * - mode=check → READ-ONLY drift report against the current block, consumed * by /ba-audit-rbac RBAC-008. Drift is DATA — exit 0; only spec/parse/IO * errors fail. */ import { parseArgs } from 'node:util' import { readFileSync, writeFileSync } from 'node:fs' import { join } from 'node:path' import { executeEnvelope, failExecute, failGenerate, generateEnvelope, printEnvelope, } from '../../../../lib/output.js' import { validateSpec } from './validate.js' import { parseRbacRows } from './rbac-rows.js' import { deriveLookupGrants, fsLookupSources } from './derive.js' import { checkLookupDrift, renderDerivedBlock, spliceDerivedBlock } from './block.js' import type { DeriveLookupReport } from './types.js' const COMMAND = 'derive-lookup-grants' function main(): void { const { values } = parseArgs({ options: { spec: { type: 'string' }, workdir: { type: 'string' }, }, strict: true, }) if (!values.spec) { printEnvelope(failExecute(COMMAND, ['--spec is required'])) process.exit(1) } let raw: unknown try { raw = JSON.parse(values.spec) } catch { printEnvelope(failExecute(COMMAND, ['Invalid JSON in --spec'])) process.exit(1) } const validation = validateSpec(raw, values.workdir) if (!validation.valid || !validation.spec || !validation.resolvedBaRoot) { printEnvelope(failExecute(COMMAND, validation.errors)) process.exit(1) } const spec = validation.spec const baRoot = validation.resolvedBaRoot const consumerRbacPath = join(baRoot, spec.app, spec.module, 'rbac.md') const consumerContent = readFileSync(consumerRbacPath, 'utf8') // --- Ground truth (lazy per-app/module caches live in the factory) --- const sources = fsLookupSources(baRoot, parseRbacRows(consumerContent)) const core = deriveLookupGrants(spec.app, spec.module, sources) const block = renderDerivedBlock(core.rows, core.needsResolution, core.skipped) const resolutionStep = core.needsResolution.length > 0 ? [ `${core.needsResolution.length} producteur(s) irrésolu(s) — compléter screen.md (écran liste) ou le menu de la section producteur, puis relancer.`, ] : [] if (spec.mode === 'derive') { const next = spliceDerivedBlock(consumerContent, block) const blockWritten = next !== null if (next !== null) { writeFileSync(consumerRbacPath, next, 'utf8') } printEnvelope( generateEnvelope(COMMAND, { data: { ...core.totals, blockWritten, report: { mode: 'derive', ...core } }, filesModified: blockWritten ? [consumerRbacPath] : [], warnings: core.warnings, nextSteps: [ ...resolutionStep, 'Les chemins du bloc sont app-qualifiés (4 segments) — la Phase 0 de /ba-develop les transcrit tels quels, sans re-préfixer.', 'Relancer ce CLI après toute modification des Relations dans entité.md, et après /ba-reconcile-menu.', '`/ba-audit-rbac` (RBAC-008) vérifie la fraîcheur du bloc via `"mode":"check"`.', ], }), ) process.exit(0) } // --- mode=check (read-only) --- // Same function audit-ba's RBAC-008 calls — the drift verdict cannot differ // between the writer and the verifier. const drift = checkLookupDrift(consumerContent, core) const { staleRows, missingRows } = drift const report: DeriveLookupReport = { mode: 'check', ...core, drift } printEnvelope( executeEnvelope(COMMAND, { success: true, data: { ...core.totals, ...drift, staleRows: staleRows.length, missingRows: missingRows.length }, report, warnings: core.warnings, nextSteps: drift.upToDate ? ['Bloc dérivé à jour — rien à faire.'] : [ 'Drift détecté — relancer en `"mode":"derive"` pour réécrire le bloc machine (jamais à la main).', ...resolutionStep, 'Le drift est une DONNÉE (exit 0) — le verdict appartient à /ba-audit-rbac (RBAC-008).', ], }), ) process.exit(0) } main()