#!/usr/bin/env node /** * cli:derive-change-impact — entry point. * * READ-ONLY engine of `/ba-change`: for ONE element added to (or modified in) * a finished BA scope, reports the owner document, the next free code, the * near-duplicates, the fail-closed blockers and the ORDERED downstream * checklist (authoring hand-offs, derive-* CLIs, audit rules, /ba-develop * re-entries). `mode: verify` re-parses the owner doc after the skill's Write. * * Invocation (prefer --spec-file on Windows — multi-line inline JSON breaks * under Git Bash / PowerShell quoting): * npx --prefer-offline tsx skills/business-analyse/change/cli/derive-change-impact/index.ts \ * --spec-file /change.json [--workdir ] * npx --prefer-offline tsx skills/business-analyse/change/cli/derive-change-impact/index.ts \ * --spec '{"baRoot":".smartstack/ba","app":"CRM","module":"PIPELINE","section":"opportunites","kind":"use-case","target":{"title":"Relancer un prospect","actor":"BA-001-AC-001"}}' * * Drift is DATA (exit 0, `blocked[]` may be non-empty); exit 1 = spec error. */ import { parseArgs } from 'node:util' import { executeEnvelope, failExecute, printEnvelope } from '../../../../lib/output.js' import { readSpecArg } from '../../../../lib/spec-arg.js' import { loadScopeCorpus } from './corpus.js' import { executeChangeImpact, executeVerify } from './execute.js' import type { ChangeImpactReport } from './types.js' import { validateSpec } from './validate.js' const COMMAND = 'derive-change-impact' function main(): void { const { values } = parseArgs({ options: { spec: { type: 'string' }, 'spec-file': { type: 'string' }, workdir: { type: 'string' }, }, strict: true, }) const specArg = readSpecArg(values) if ('error' in specArg) { printEnvelope(failExecute(COMMAND, [specArg.error])) process.exit(1) } let raw: unknown try { raw = JSON.parse(specArg.raw) } catch { printEnvelope(failExecute(COMMAND, ['Invalid JSON in --spec / --spec-file'])) process.exit(1) } const validation = validateSpec(raw, values.workdir) if (!validation.valid || !validation.spec || !validation.scope) { printEnvelope(failExecute(COMMAND, validation.errors)) process.exit(1) } const spec = validation.spec const corpus = loadScopeCorpus(validation.scope, spec.projectPath) const report = spec.mode === 'verify' ? executeVerify(spec, corpus) : executeChangeImpact(spec, corpus) const nextSteps: string[] = [] if (report.blocked.length > 0) { for (const b of report.blocked) { nextSteps.push( `BLOCKED ${b.code}: ${b.reason}${b.routeTo ? ` → ${b.routeTo.skill}${b.routeTo.kind ? ` kind=${b.routeTo.kind}` : ''}` : ''}${b.candidates && b.candidates.length > 0 ? ` (candidates: ${b.candidates.join(', ')})` : ''}`, ) } } else if (report.mode === 'verify' && report.verify) { nextSteps.push( report.verify.ok ? `VERIFIED ${spec.verify!.expectCode}: ${report.verify.count} item(s) in ${report.owner.file} (delta ${report.verify.delta >= 0 ? '+' : ''}${report.verify.delta}), nothing lost, machine blocks ${report.verify.machineBlocksIntact === null ? 'not compared' : 'intact'}, sources ${report.verify.sourcesCited}.` : `NOT VERIFIED — fix before continuing: found=${report.verify.found}, delta=${report.verify.delta}, lost=${report.verify.lost.length}, duplicates=${report.verify.duplicates.length}, machineBlocksIntact=${report.verify.machineBlocksIntact}, sources=${report.verify.sourcesCited}.`, ) } else { if (report.existing.exact.length > 0) { nextSteps.push(`${report.existing.exact.length} existing item(s) match — ask: reuse / modify the existing one / continue as new.`) } if (report.allocation) { nextSteps.push( `Next code: ${report.allocation.next} (headings up to ${String(report.allocation.maxTaken).padStart(3, '0')}, ${report.allocation.reserved.length} reserved number(s); limits: ${report.allocation.limits.join('; ')}).`, ) } nextSteps.push(`Load kinds/${spec.kind}.md and walk the ${report.impact.length} impact step(s) IN ORDER; a \`skipped\` step is announced with its reason, never silently dropped.`) nextSteps.push('After the Write of the owner doc, re-run this CLI with mode=verify {expectCode, baselineCount: existing.count, machineBlocksHash: owner.machineBlocksHash}.') } printEnvelope( executeEnvelope(COMMAND, { success: true, data: { kind: report.kind, op: report.op, mode: report.mode, blocked: report.blocked.length, next: report.allocation?.next ?? null, steps: report.impact.length, existingExact: report.existing.exact.length, verified: report.verify?.ok ?? null, }, report, warnings: report.warnings, nextSteps, }), ) process.exit(0) } main()