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