#!/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()