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