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