#!/usr/bin/env node /** * cli:derive-test-data — entry point. * * The business test dataset of a module (`jeu-de-test.md`) checked against * the MCD, the rules, the actors and the OTHER modules' datasets — and, in * derive mode, normalized into the JSON `scaffold-seed` consumes as * `testData[]`. Companion of /ba-create-test-data; the check mode is the * engine of DM-030..032. * * Invocation: * npx --prefer-offline tsx skills/business-analyse/create-test-data/cli/derive-test-data/index.ts \ * --spec '{"baRoot":".smartstack/ba","app":"CLIENT","module":"ANNUAIRE","mode":"check"}' \ * [--workdir ] * * Exit codes: 0 = report produced (drift is DATA — the verdict belongs to the * audit); 1 = spec/prerequisite error; 3 = a source that exists could not be * read (an entité.md with zero entity) — never a clean report on a mute parser. */ import { parseArgs } from 'node:util' import { existsSync, readdirSync, statSync } from 'node:fs' import { join } from 'node:path' import { executeEnvelope, failExecute, printEnvelope } from '../../../../lib/output.js' import { loadModuleEntities } from '../../../../lib/ba-entities.js' import { loadEntityContents, normalizeModulePath, parseEntities } from '../../../../lib/ba-relations.js' import { loadModuleRules } from '../../../../lib/ba-rules-rows.js' import { loadAppActors } from '../../../../lib/ba-actors.js' import { loadModuleTestData } from '../../../../lib/ba-test-data.js' import { validateSpec } from './validate.js' import { deriveTestData, type ModuleInputs } from './execute.js' import type { DeriveTestDataReport } from './types.js' const COMMAND = 'derive-test-data' const SKIP_DIRS = new Set(['pagespecs', 'node_modules']) /** Same walk as audit-ba's corpus loader — `_*`, dot-dirs, pagespecs and node_modules are never modules. */ function listDirs(dir: string): string[] { try { return readdirSync(dir).filter((n) => !n.startsWith('_') && !n.startsWith('.') && !SKIP_DIRS.has(n) && statSync(join(dir, n)).isDirectory()).sort() } catch { return [] } } /** Every `APP/MOD` of the tree that carries an entité.md — entities + dataset. */ function loadProjectModules(baRoot: string): { modules: Map; warnings: string[] } { const modules = new Map() const warnings: string[] = [] for (const app of listDirs(baRoot)) { for (const mod of listDirs(join(baRoot, app))) { const entities = loadModuleEntities(baRoot, app, mod) if (entities === null) continue const testData = loadModuleTestData(baRoot, app, mod) const modulePath = `${app}/${mod}` for (const w of entities.warnings) warnings.push(`${modulePath}/entité.md: ${w}`) if (testData.doc) for (const w of testData.doc.warnings) warnings.push(`${modulePath}/jeu-de-test.md: ${w}`) modules.set(normalizeModulePath(modulePath), { modulePath, entities: entities.entities, testData: testData.doc }) } } return { modules, warnings } } 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 baRoot = validation.resolvedBaRoot const { modules, warnings } = loadProjectModules(baRoot) const own = modules.get(normalizeModulePath(`${spec.app}/${spec.module}`)) if (!own || own.entities.length === 0) { printEnvelope( failExecute(COMMAND, [ `${spec.app}/${spec.module}/entité.md exists but no \`### ENT-NNN — Name\` block parses — the dataset cannot be checked against a model nobody can read (fix the heading grammar).`, ]), ) process.exit(3) } const graph = parseEntities(loadEntityContents(baRoot)) const rules = loadModuleRules(baRoot, spec.app, spec.module) const actors = loadAppActors(baRoot, spec.app) const testDataPath = join(baRoot, spec.app, spec.module, 'jeu-de-test.md') const report = deriveTestData({ app: spec.app, module: spec.module, mode: spec.mode, modules, graph, rules: rules.rules, actors: actors.actors, file: existsSync(testDataPath) ? testDataPath : null, warnings: [...warnings, ...rules.warnings.map((w) => `règles-métier: ${w}`), ...actors.warnings.map((w) => `acteur.md: ${w}`)], }) const nextSteps: string[] = [] if (report.status === 'absent') { nextSteps.push('Optional artefact — run /ba-create-test-data to author 5-8 fictitious rows per business entity (DM-029 warns while it is missing).') } else if (report.totals.err > 0) { nextSteps.push(`${report.totals.err} incoherence(s) — fix jeu-de-test.md (or the cited module's dataset) then re-run; the verdict belongs to /ba-audit-data-model (DM-030..032).`) } else { nextSteps.push(spec.mode === 'derive' ? 'Pass `report.derived.sets` as scaffold-seed `testData[]` (Phase 1) — merge `types` with the entity fields[] map for the `needsTypes` attributes.' : 'Dataset coherent — `"mode":"derive"` produces the scaffold-seed input.') } if (report.totals.unresolvedRefs > 0) { nextSteps.push(`${report.totals.unresolvedRefs} unresolved reference(s): the OWNER module must provide the cited rows (same application: /ba-create-test-data on that module; another application: report it).`) } printEnvelope( executeEnvelope(COMMAND, { success: true, data: { ...report.totals, status: report.status, rank: report.derived?.rank ?? null }, report, warnings: report.warnings, nextSteps: [...nextSteps, ...report.scopeNotes], }), ) process.exit(0) } main()