/** * Generic lexicon packaging pipeline. * * Orchestrates: generate → manifest → collect rules → collect skills → * assemble BundleSpec → compute integrity → attach metadata. */ import { readFileSync, readdirSync, writeFileSync, mkdirSync, rmSync } from "fs"; import { dirname, join } from "path"; import type { BundleSpec, LexiconManifest } from "../lexicon"; import { computeIntegrity } from "../lexicon-integrity"; import type { GenerateResult } from "./generate"; import { scanRulesWithSources } from "./docs-rule-scanning"; import { buildLexiconOkfBundle } from "./okf-lexicon"; // ── Types ────────────────────────────────────────────────────────── export interface PackageOptions { verbose?: boolean; force?: boolean; } export interface PackageResult { spec: BundleSpec; stats: { resources: number; properties: number; enums: number; ruleCount: number; skillCount: number; }; } export interface PackagePipelineConfig { /** Run generation and return artifacts. */ generate: (opts: { verbose?: boolean; force?: boolean }) => Promise; /** Build the lexicon manifest from the generate result. */ buildManifest: (genResult: GenerateResult) => LexiconManifest; /** Source directory for collecting rules. */ srcDir: string; /** Rule directories relative to srcDir (default: ["lint/rules", "lint/post-synth"]). */ ruleDirs?: string[]; /** Collect skill definitions. Returns map of filename → content. */ collectSkills: () => Map; /** Package version for metadata. */ version?: string; } // ── Pipeline ─────────────────────────────────────────────────────── /** * Run the packaging pipeline with the supplied config. */ export async function packagePipeline( config: PackagePipelineConfig, opts: PackageOptions = {}, ): Promise { const log = opts.verbose ? (msg: string) => console.error(msg) : (_msg: string) => {}; // Step 1: Run the generation pipeline log("Running generation pipeline..."); const result = await config.generate({ verbose: opts.verbose, force: opts.force }); // Step 2: Build manifest log("Building manifest..."); const manifest = config.buildManifest(result); // Step 3: Collect rules log("Collecting rules..."); const rules = collectRules(config.srcDir, config.ruleDirs); // Step 4: Collect skills log("Collecting skills..."); const skills = config.collectSkills(); // Step 5: Assemble BundleSpec, with the OKF knowledge bundle over the same // artifacts (#1060) — derived here so it cannot drift from the registry and // rules it describes. log("Building OKF knowledge bundle..."); const okf = buildLexiconOkfBundle({ name: manifest.name, registry: result.lexiconJSON, typesDTS: result.typesDTS, rules: scanRulesWithSources(config.srcDir), }); const spec: BundleSpec = { manifest, registry: result.lexiconJSON, typesDTS: result.typesDTS, rules, skills, okf, }; // Step 6: Compute integrity log("Computing integrity..."); spec.integrity = computeIntegrity(spec); // Step 7: Populate metadata spec.metadata = { generatedAt: new Date().toISOString(), chantVersion: "0.1.0", generatorVersion: config.version ?? "0.0.0", sourceSchemaCount: result.resources, }; log(`Package assembled: ${rules.size} rules, ${skills.size} skills`); return { spec, stats: { resources: result.resources, properties: result.properties, enums: result.enums, ruleCount: rules.size, skillCount: skills.size, }, }; } // ── Utilities ────────────────────────────────────────────────────── /** * Collect lint rule source files from a lexicon package. * Auto-discovers .ts files in the specified directories, * skipping test files, re-export files (index.ts), and non-.ts files. */ export function collectRules( srcDir: string, dirs: string[] = ["lint/rules", "lint/post-synth"], ): Map { const rules = new Map(); for (const dir of dirs) { const fullDir = join(srcDir, dir); let entries: string[]; try { entries = readdirSync(fullDir); } catch { continue; } for (const entry of entries) { if (!entry.endsWith(".ts")) continue; if (entry.endsWith(".test.ts")) continue; if (entry === "index.ts") continue; try { const content = readFileSync(join(fullDir, entry), "utf-8"); rules.set(entry, content); } catch { // Skip unreadable files } } } return rules; } /** * Write a BundleSpec to the given dist directory. * * Creates the directory structure and writes all artifacts: * manifest.json, meta.json, types/index.d.ts, rules/*, skills/*, okf/*, * integrity.json. */ export function writeBundleSpec(spec: BundleSpec, distDir: string): void { mkdirSync(join(distDir, "types"), { recursive: true }); mkdirSync(join(distDir, "rules"), { recursive: true }); mkdirSync(join(distDir, "skills"), { recursive: true }); writeFileSync(join(distDir, "manifest.json"), JSON.stringify(spec.manifest, null, 2)); writeFileSync(join(distDir, "meta.json"), spec.registry); writeFileSync(join(distDir, "types", "index.d.ts"), spec.typesDTS); for (const [name, content] of spec.rules) { writeFileSync(join(distDir, "rules", name), content); } for (const [name, content] of spec.skills) { writeFileSync(join(distDir, "skills", name), content); } if (spec.okf) { // Replace rather than merge — a concept whose resource type was removed // upstream must not survive as a stale file. rmSync(join(distDir, "okf"), { recursive: true, force: true }); for (const file of spec.okf) { const target = join(distDir, "okf", file.path); mkdirSync(dirname(target), { recursive: true }); writeFileSync(target, file.content); } } if (spec.integrity) { writeFileSync(join(distDir, "integrity.json"), JSON.stringify(spec.integrity, null, 2)); } } /** * Collect skills from a plugin's skill definitions. */ export function collectSkills( skillDefs: Array<{ name: string; content: string }>, ): Map { const skills = new Map(); for (const s of skillDefs) { skills.set(`${s.name}.md`, s.content); } return skills; }