#!/usr/bin/env node /** * cli:derive-rule-links — entry point. * * Deterministic transport of the module's business rules into the pagespecs' * `linkedBusinessRules[]` — the field DEV-API-008 reads. Companion of * /ba-create-prd; the check mode is the engine of PRD-129/PRD-130. * * Invocation: * npx --prefer-offline tsx skills/business-analyse/create-prd/cli/derive-rule-links/index.ts \ * --spec '{"baRoot":".smartstack/ba","app":"CRM","module":"PIPELINE","mode":"backfill"}' \ * [--workdir ] * * - mode=backfill → REWRITES the pagespec machine blocks (adds codes, never * removes — authored links win; idempotent). * - mode=check → READ-ONLY report consumed by /ba-audit-prd PRD-129/130. * Drift is DATA — exit 0; only spec/IO errors fail. */ import { parseArgs } from 'node:util' import { readFileSync, readdirSync, statSync, writeFileSync } from 'node:fs' import { join } from 'node:path' import { executeEnvelope, failExecute, generateEnvelope, printEnvelope, } from '../../../../lib/output.js' import { loadModuleRules } from '../../../../lib/ba-rules-rows.js' import { validateSpec } from './validate.js' import { deriveRuleLinks, type PagespecSource } from './execute.js' import type { DeriveRuleLinksReport } from './types.js' const COMMAND = 'derive-rule-links' 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 pagespecsDir = join(moduleDir, 'pagespecs') const { rules, warnings: ruleWarnings } = loadModuleRules(validation.resolvedBaRoot, spec.app, spec.module) const pagespecs: PagespecSource[] = [] for (const name of readdirSync(pagespecsDir).filter((n) => n.endsWith('.md')).sort()) { const full = join(pagespecsDir, name) try { if (!statSync(full).isFile()) continue pagespecs.push({ name, md: readFileSync(full, 'utf8') }) } catch { ruleWarnings.push(`${name}: unreadable — skipped.`) } } const { report, rewrites } = deriveRuleLinks({ app: spec.app, module: spec.module, mode: spec.mode, rules, ruleWarnings, pagespecs, moduleDirName: spec.module, }) const resolutionSteps: string[] = [] if (report.totals.needsJudgment > 0) { resolutionSteps.push( `${report.totals.needsJudgment} rule(s) need judgment (module-level, no linked UC) — author the link, ` + `link a UC, or exempt via \`- **Enforcement** : plateforme|manuel\`; NEVER guessed.`, ) } if (report.totals.citedCodeFindings > 0) { resolutionSteps.push( `${report.totals.citedCodeFindings} cited-code finding(s) (dead or ambiguous BR reference) — fix the pagespec or renumber the doc.`, ) } if (spec.mode === 'backfill') { const filesModified: string[] = [] for (const [name, md] of rewrites) { const full = join(pagespecsDir, name) writeFileSync(full, md, 'utf8') filesModified.push(full) } printEnvelope( generateEnvelope(COMMAND, { data: { ...report.totals, report }, filesModified, warnings: report.warnings, nextSteps: [ ...resolutionSteps, 'Re-run `/ba-audit-prd` — PRD-129/130 read this derivation in `"mode":"check"`.', 'Re-run after every règles-métier.md change (new rule = new link to transport).', ], }), ) process.exit(0) } printEnvelope( executeEnvelope(COMMAND, { success: true, data: { ...report.totals, upToDate: report.totals.missingLinks === 0 && report.totals.citedCodeFindings === 0 }, report, warnings: report.warnings, nextSteps: report.totals.missingLinks === 0 && report.totals.citedCodeFindings === 0 ? ['Every enforceable rule is linked and every cited code resolves — nothing to do.'] : [ 'missing-link → run `"mode":"backfill"` (or author the link on the owning pagespec).', ...resolutionSteps, 'Drift is DATA (exit 0) — the verdict belongs to /ba-audit-prd (PRD-129/130).', ], }), ) process.exit(0) } main()