#!/usr/bin/env node
/**
* cli:derive-referential-codes — entry point.
*
* A reference value does not carry a code. Only the USER may decide otherwise,
* and that dated decision overrides the rule. This CLI is the inventory that
* lets them decide — and the deterministic backfill that applies what they
* decided. Companion of /ba-create-data-model; the check mode is the engine of
* DM-022.
*
* Invocation:
* npx --prefer-offline tsx skills/business-analyse/create-data-model/cli/derive-referential-codes/index.ts \
* --spec '{"baRoot":".smartstack/ba","app":"SALES","module":"CATALOG","mode":"check"}' \
* [--workdir
]
*
* - mode=check → READ-ONLY inventory. Drift is DATA — exit 0; only spec/IO
* errors fail. The verdict belongs to /ba-audit-data-model (DM-022).
* - mode=backfill → rewrites `entité.md` ONLY. Never an entity carrying a
* `**Code décidé**`, never a CITED code, never the shipped code or the
* migrations.
*/
import { parseArgs } from 'node:util'
import { readFileSync, writeFileSync } from 'node:fs'
import { join } from 'node:path'
import {
executeEnvelope,
failExecute,
generateEnvelope,
printEnvelope,
} from '../../../../lib/output.js'
import { entiteDocPath, validateSpec } from './validate.js'
import { deriveReferentialCodes } from './execute.js'
import { loadCitationSources } from './sources.js'
import type { DeriveReferentialCodesReport } from './types.js'
const COMMAND = 'derive-referential-codes'
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 moduleDir = join(validation.resolvedBaRoot, spec.app, spec.module)
const entitePath = entiteDocPath(moduleDir)!
const entiteMd = readFileSync(entitePath, 'utf8')
const loaded = loadCitationSources(validation.resolvedBaRoot, spec.app, spec.module)
const { report, rewritten } = deriveReferentialCodes({
app: spec.app,
module: spec.module,
mode: spec.mode,
entiteMd,
sources: loaded.sources,
coverage: loaded.coverage,
warnings: loaded.warnings,
})
const blocked = report.entities.filter((e) => e.status === 'blocked')
const resolutionSteps: string[] = []
if (report.totals.nearMiss > 0) {
resolutionSteps.push(
`${report.totals.nearMiss} ligne(s) « Code décidé » non analysable(s) — réécrire la puce ` +
'canonique `- **Code décidé** : — décision utilisateur du `.',
)
}
if (report.totals.allocated > 0) {
resolutionSteps.push(
`${report.totals.allocated} table(s) de référence avec un code ALLOUÉ (**Code pattern**) — ` +
'retirer la ligne : un code décidé est SAISI, jamais alloué.',
)
}
if (blocked.length > 0) {
resolutionSteps.push(
`${blocked.length} entité(s) bloquée(s) : ` +
blocked.map((e) => `${e.entity} (${e.blockedBy})`).join(', ') +
". Lire leurs citations — c'est ce qui casse si le code part.",
)
}
if (report.totals.reprise > 0 && spec.mode === 'check') {
resolutionSteps.push(
`${report.totals.reprise} entité(s) reprenable(s) sans rien casser — \`"mode":"backfill"\`. ` +
"La décision de GARDER un code appartient à l'utilisateur : poser " +
'`- **Code décidé** : … — décision utilisateur du ` AVANT le backfill.',
)
}
if (spec.mode === 'backfill') {
const filesModified: string[] = []
if (rewritten !== null) {
writeFileSync(entitePath, rewritten, 'utf8')
filesModified.push(entitePath)
}
report.filesModified = filesModified
printEnvelope(
generateEnvelope(COMMAND, {
data: { ...report.totals, report },
filesModified,
warnings: report.warnings,
nextSteps: [
...resolutionSteps,
...report.scopeNotes,
'Relancer `"mode":"check"` — 0 err attendu une fois les décisions posées.',
"Le backfill n'écrit que dans entité.md : un `git diff` par module défait tout.",
],
}),
)
process.exit(0)
}
printEnvelope(
executeEnvelope(COMMAND, {
success: true,
data: { ...report.totals, upToDate: report.totals.errors === 0 },
report,
warnings: report.warnings,
nextSteps:
report.totals.errors === 0
? [
'Aucune table de référence ne porte de code non décidé — rien à faire.',
...report.scopeNotes,
]
: [
...resolutionSteps,
...report.scopeNotes,
'Le drift est une DONNÉE (exit 0) — le verdict appartient à /ba-audit-data-model (DM-022).',
],
}),
)
process.exit(0)
}
main()