#!/usr/bin/env node /** * cli:sources-search — entry point. * * THE targeted-extraction path of the BA phases into the client-sources * registry: deterministic accent/case-folded search over the summaries and * §-sections of every `ingested` source.md — nobody reads raw/ and nobody * loads the whole corpus into an agent context (the 394M-token lesson). * * Invocation: * npx --prefer-offline tsx skills/business-analyse/create-sources/cli/search/index.ts \ * --spec '{"baRoot":".smartstack/ba","query":"remise","tags":["regles"]}' [--workdir ] * * READ-ONLY. The counts ALWAYS accompany the hits: « N docs cherchés, * M illisibles » — zero searchable never reads as « nothing matches ». */ import { parseArgs } from 'node:util' import { isAbsolute, join } from 'node:path' import { z } from 'zod' import { executeEnvelope, failExecute, printEnvelope } from '../../../../lib/output.js' import { loadSourcesRegistry, searchSources, sourcesRootFor, type SourceSearchHit, } from '../../../../lib/ba-sources.js' const COMMAND = 'sources-search' const SearchInputSchema = z .object({ baRoot: z.string().min(1), query: z.string().min(2), /** Filter: only sources carrying one of these tags (doc or section). */ tags: z.array(z.string().min(1)).optional(), /** Filter: only these SRC codes. */ codes: z.array(z.string().regex(/^SRC-\d{3}$/)).optional(), }) .strict() export interface SearchReport { query: string registryExists: boolean hits: SourceSearchHit[] searchedDocs: number unreadableDocs: number registryIssues: 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 = SearchInputSchema.safeParse(raw) if (!parsed.success) { printEnvelope( failExecute(COMMAND, parsed.error.issues.map((i) => `[${i.path.join('.')}] ${i.message}`)), ) process.exit(1) } const spec = parsed.data const base = values.workdir ?? process.cwd() const baRoot = isAbsolute(spec.baRoot) ? spec.baRoot : join(base, spec.baRoot) const registry = loadSourcesRegistry(sourcesRootFor(baRoot)) const result = searchSources(registry, spec.query, { tags: spec.tags, codes: spec.codes }) const report: SearchReport = { query: spec.query, registryExists: registry.exists, hits: result.hits, searchedDocs: result.searchedDocs, unreadableDocs: result.unreadableDocs, registryIssues: registry.reconciliation.issues, } const coverage = `${result.searchedDocs} doc(s) cherché(s), ${result.unreadableDocs} illisible(s)` printEnvelope( executeEnvelope(COMMAND, { success: true, data: { hits: result.hits.length, searchedDocs: result.searchedDocs, unreadableDocs: result.unreadableDocs }, report, warnings: registry.reconciliation.issues, nextSteps: !registry.exists ? [`Aucun registre de sources (${sourcesRootFor(baRoot)}) — rien n'a été cherché ; /ba-create-sources pour en créer un.`] : result.hits.length === 0 ? [ `0 hit — ${coverage}. Une liste vide ne conclut que sur ce corpus : reformule la requête ou élargis les tags avant d'affirmer « la source ne dit rien ».`, ] : [`${result.hits.length} hit(s) — ${coverage}. Cite via \`SRC-NNN §n\` (ligne **Sources** de l'item).`], }), ) process.exit(0) } main()