#!/usr/bin/env node /** * cli:derive-nav-resources — entry point. * * §24 bridge: derives one nav RESOURCE per distinct pagespec (routeParent, * routeFamily) so the componentKeys scaffold-routes registers in Phase 3b are * REACHABLE — DynamicRouter only builds a 4th-level route from * `section.resources` (seeded) or an IMPLICIT_SUFFIX; anything else redirects * silently to /applications. Runs as a Phase 3b closing step, AFTER * scaffold-routes; the emitted resources are merged into the Phase 0 core-seed * spec and scaffold-core-seed is re-invoked (additive, state-diffed). * * Invocation: * npx --prefer-offline tsx skills/ba-develop/cli/derive-nav-resources/index.ts \ * --spec '{"moduleRoot":".smartstack/ba//"}' * * Read-only: writes nothing — the output feeds buildCoreSeedSpec's * `resources[]` (shape-compatible with MenuResourceInput). */ import { parseArgs } from 'node:util' import { validate } from './validate.js' import { deriveNavResources } from './derive.js' import { executeEnvelope, failExecute, printEnvelope } from '../../../lib/output.js' import { readSpecArg } from '../../../lib/spec-arg.js' import type { DeriveNavResourcesReport } from './types.js' const COMMAND = 'derive-nav-resources' function main(): void { // `--spec-file` is part of the shared CLI contract (lib/spec-arg): parseArgs // is strict everywhere, so an orchestrator passing the spec by file — the // sanctioned way around the argv limit — must not hard-fail here. const { values } = parseArgs({ options: { spec: { type: 'string' }, 'spec-file': { 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'])) process.exit(1) } const validation = validate(raw) if (!validation.valid || !validation.spec) { printEnvelope(failExecute(COMMAND, validation.errors)) process.exit(1) } const report = deriveNavResources(validation.spec) // A route-family collision is NOT seedable: two entities would share one // [NavRoute], and the loser's pages/lookups would resolve against the // winner — the bug this CLI exists to prevent. Refuse the run (exit 1) the // way the other derive CLIs refuse an unresolved target, instead of handing // back a "merge these" instruction the orchestrator would follow blindly. if (report.collisions.length > 0) { printEnvelope( executeEnvelope(COMMAND, { success: false, data: report.totals, report, errors: report.collisions.map( (c) => `route family '${c.family}' (section '${c.sectionCode}') is claimed by ${c.entities.join(', ')} — one family serves ONE entity.`, ), warnings: report.warnings, nextSteps: [ 'BLOCKING — give each satellite a distinct routeFamily in its pagespecs (PRD-108), then re-run. Do NOT seed these resources: the colliding entities would share one [NavRoute].', ], }), ) process.exit(1) } const nextSteps: string[] = report.resources.length === 0 ? ['No satellite routeFamily found — nothing to seed, the section slugs cover every componentKey.'] : [ `Merge the ${report.resources.length} resource(s) into the Phase 0 core-seed spec (\`resources[]\`) and re-invoke scaffold-core-seed — additive second pass, the state diff keeps it idempotent.`, 'Then run audit-dev-frontend DEV-UI-046 — every registry componentKey must resolve to an implicit suffix or a seeded nav node.', ] printEnvelope( executeEnvelope(COMMAND, { success: true, data: report.totals, report, warnings: report.warnings, nextSteps, }), ) process.exit(0) } main()