#!/usr/bin/env node /** * cli:sources-status — entry point. * * Health of the client-sources registry: index ↔ disk reconciliation, * per-status counts, blocked sources named, raw/ presence. READ-ONLY — * drift is DATA (exit 0); the verdict belongs to audit-ba (SRC-001..007). * * Invocation: * npx --prefer-offline tsx skills/business-analyse/create-sources/cli/status/index.ts \ * --spec '{"baRoot":".smartstack/ba"}' [--workdir ] */ import { parseArgs } from 'node:util' import { existsSync } from 'node:fs' import { isAbsolute, join } from 'node:path' import { z } from 'zod' import { executeEnvelope, failExecute, printEnvelope } from '../../../../lib/output.js' import { loadSourcesRegistry, sourcesRootFor, type SourceStatus } from '../../../../lib/ba-sources.js' const COMMAND = 'sources-status' const StatusInputSchema = z.object({ baRoot: z.string().min(1) }).strict() export interface StatusReport { registryExists: boolean root: string reconciliation: { status: 'ok' | 'suspect'; issues: string[] } controls: { anchors: number; parsed: number } counts: Record nextSeq: number | null blocked: Array<{ code: string; title: string; status: string }> missingRaw: string[] warnings: string[] } 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 parsed = StatusInputSchema.safeParse(raw) if (!parsed.success) { printEnvelope( failExecute(COMMAND, parsed.error.issues.map((i) => `[${i.path.join('.')}] ${i.message}`)), ) process.exit(1) } const base = values.workdir ?? process.cwd() const baRoot = isAbsolute(parsed.data.baRoot) ? parsed.data.baRoot : join(base, parsed.data.baRoot) const registry = loadSourcesRegistry(sourcesRootFor(baRoot)) const counts: Record = { ingested: 0, 'blocked/needs-export': 0, 'blocked/unreadable': 0, superseded: 0, } const blocked: StatusReport['blocked'] = [] const missingRaw: string[] = [] if (registry.index) { for (const entry of Object.values(registry.index.sources)) { counts[entry.status] = (counts[entry.status] ?? 0) + 1 if (entry.status.startsWith('blocked/')) { blocked.push({ code: entry.code, title: entry.title, status: entry.status }) } if (entry.rawFile && !existsSync(join(registry.root, entry.code, ...entry.rawFile.split('/')))) { missingRaw.push(`${entry.code}: ${entry.rawFile}`) } } } const report: StatusReport = { registryExists: registry.exists, root: registry.root, reconciliation: registry.reconciliation, controls: registry.controls, counts, nextSeq: registry.index?.nextSeq ?? null, blocked, missingRaw, warnings: registry.warnings, } const nextSteps: string[] = [] if (!registry.exists) { nextSteps.push('Aucun registre — état légitime (« sans objet ») tant qu’aucune source client n’existe.') } else { if (registry.reconciliation.status === 'suspect') { nextSteps.push('Registre SUSPECT — répare index.json/les dossiers avant toute écriture ; SRC-001 rendra err tant que ça diverge.') } if (blocked.length > 0) { nextSteps.push(`${blocked.length} source(s) bloquée(s) non exploitée(s) — exporte puis rattache via ingest \`"as":"SRC-NNN"\`.`) } if (missingRaw.length > 0) { nextSteps.push(`${missingRaw.length} copie(s) raw/ manquante(s) — restaure-les ou ré-ingère (la citation doit rester vérifiable).`) } if (nextSteps.length === 0) nextSteps.push('Registre sain.') } printEnvelope( executeEnvelope(COMMAND, { success: true, data: { exists: registry.exists, reconciliation: registry.reconciliation.status, issues: registry.reconciliation.issues.length, ...counts, }, report, warnings: [...registry.reconciliation.issues, ...registry.warnings], nextSteps, }), ) process.exit(0) } main()