#!/usr/bin/env node /** * cli:extract-doc — read-only extraction for the /documentation skill. * * Emits an ExecuteEnvelope on stdout and writes NO files. Claude reads the * `report` to author a faithful doc page, then calls scaffold-doc (i18n + * wiring) and ui-polish (theme tokens). * * Usage: * npx --prefer-offline tsx skills/documentation/cli/extract-doc/index.ts \ * --spec '{"type":"user","target":"users","application":"administration","projectPath":"/abs/path"}' */ import { parseArgs } from 'node:util' import { validate } from './validate.js' import { extract } from './extract.js' import { executeEnvelope, failExecute, printEnvelope } from '../../../lib/output.js' import type { ExtractDocInput, ExtractReport } from './types.js' const COMMAND = 'extract-doc' async function main(): Promise { const { values } = parseArgs({ options: { spec: { 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 = validate(raw) if (!validation.valid) { printEnvelope(failExecute(COMMAND, validation.errors)) process.exit(1) } const warnings = [...validation.warnings] try { const report = await extract(raw as ExtractDocInput, warnings) printEnvelope(executeEnvelope(COMMAND, { report, warnings, nextSteps: buildNextSteps(report) })) } catch (err) { printEnvelope(failExecute(COMMAND, [err instanceof Error ? err.message : String(err)])) process.exit(1) } } function buildNextSteps(report: ExtractReport): string[] { const steps: string[] = [] // Corrective gate (two mirror checks): surface forbidden "sales" framing to // strip AND a missing mandatory "objective" to add. The step-03 gate re-runs // this same scan and blocks until the doc is both clean and complete. const forbidden = report.existingDoc.forbiddenSections if (forbidden.length > 0) { const locations = forbidden.map((f) => `${f.file}:${f.line} (${f.token})`).join(', ') steps.push( `EXISTING doc contains forbidden problème/solution / Bénéfices / Avant-Après sections at ${locations} — ` + `REMOVE them while regenerating. Do NOT re-author the benefits/beforeAfter/problem/solution keys ` + `(the doc leads with a factual "objective", not a sales pitch). The step-03 gate re-runs this scan and blocks until clean.`, ) } const missing = report.existingDoc.missingRequired if (missing.includes('objective')) { steps.push( `EXISTING doc has NO "Objectif" section — the mandatory Section 1 (a short factual description of what the ` + `module does, written as an objective). ADD it FIRST as its own section: author the i18n keys ` + `"sections.objective" + "objective" (user) or "overview.objective" (DocRenderer) and render t('objective'). ` + `The header subtitle is NOT a substitute. The step-03 gate re-runs this scan and blocks until present.`, ) } if (missing.includes('summary')) { steps.push( `EXISTING user doc has NO header accent tagline — the one-line accent summary between the title and the ` + `subtitle. ADD the i18n key "summary" and render it as

{t('summary')}

, then the stats line (N {t('header.businessRules')} · N {t('header.apiEndpoints')} · …, ` + `count as plain sibling text, NOT pills). The step-03 gate blocks until present.`, ) } if (report.existingDoc.adviseRolesTable) { steps.push( `EXISTING user doc predates the « Accès & rôles » table while this project HAS a roles source ` + `(accessRoles.source='${report.accessRoles.source}') — ADD the Section 2 role table (access.columns.* / ` + `access.roles.*) from report.accessRoles at this regeneration. NON-blocking advice — the step-03 gate ` + `does NOT require it.`, ) } const overflow = report.existingDoc.overflowRisks if (overflow.length > 0) { const locations = overflow .slice(0, 8) .map((r) => `${r.file}:${r.line}`) .join(', ') steps.push( `EXISTING doc has ${overflow.length} overflow-prone /font-mono element(s) without a break utility ` + `(${locations}${overflow.length > 8 ? ', …' : ''}) — add break-all/break-words (DocPanel is 480px wide). ` + `WARNINGS only, never blocking.`, ) } // ── « Accès & rôles » (Section 2) authoring guidance ────────────────────── if (report.type === 'user') { const ar = report.accessRoles if (ar.source !== 'none') { steps.push( `Section 2 « Accès & rôles »: author the role table from report.accessRoles (${ar.rows.length} row(s), ` + `source='${ar.source}') — columns Rôle | Peut faire | Portée via access.columns.* and one access.roles. ` + `entry per row. Use ONLY the reported rows (never invent grants).` + (ar.source === 'ba' ? ` Source is rbac.md ALONE — render the access.unverified caveat.` : '') + (ar.unmappedCodePermissions.length > 0 ? ` Render the ${ar.unmappedCodePermissions.length} unmapped code permission(s) as a caveat (held by no role).` : ''), ) if (ar.warnings.length > 0) { steps.push( `accessRoles has ${ar.warnings.length} join warning(s) (drift / unmatched actors) — REPORT them to the user ` + `verbatim; they never appear in the doc itself.`, ) } } else { steps.push( 'No roles source (accessRoles.source=none) — Section 2 keeps the simple access card: URL + the plain ' + '« permissions requises » list from apiEndpoints[].permission.', ) } } if (report.type === 'user') { steps.push( report.resolved.pageTsxPath ? `Read ${report.resolved.pageTsxPath} and reproduce it faithfully as annotated Mock UI (do NOT invent a generic table/KPI layout).` : 'No real page found under src/pages — author the Mock UI from the live app or the pagespec.', ) if (report.charts.length > 0) { steps.push( `Match each chart TYPE: ${report.charts .map((c) => `${c.component} → ${c.mockUiPattern}`) .join(' | ')}`, ) } } if (report.resolved.frontendMode === 'client') { steps.push( `CLIENT project detected (@atlashub/smartstack${report.resolved.packageVersion ? `@${report.resolved.packageVersion}` : ''}): ` + `the page MUST call useTranslation('${report.namespace}') (KEBAB namespace — file name = namespace), the page ` + `folder is src/pages/docs/business/// (NO platform|personal segment), and scaffold-doc will emit ` + `${report.suggestedRegistryFile ?? 'src/extensions/Registry.ts'} + an aggregate-component-registry ` + `instruction instead of the source-monorepo wiring (DocRoutes/config.ts/DocPanelContext/UserIndexPage).`, ) } steps.push( 'Then call scaffold-doc to write the i18n files (4 langs) + wire routes/manifest/config, and run ui-polish to enforce theme tokens.', ) return steps } void main()