import { createHash } from "node:crypto"; import { mkdirSync, readFileSync, writeFileSync } from "node:fs"; import { basename, resolve } from "node:path"; import matter from "gray-matter"; import { decideKnowledgeSource, loadKnowledgePolicy, publicizeObsidianMarkdown, scanKnowledgeDraft, } from "./knowledge-policy.ts"; interface Args { sourcePath: string; slug?: string; title?: string; summary: string; claimMode: "factual" | "perspective" | "mixed"; perspectiveOwner?: string; confidence: "high" | "medium" | "low"; sources: Array<{ title: string; url: string }>; } function option(name: string): string | undefined { const index = process.argv.indexOf(name); return index >= 0 ? process.argv[index + 1] : undefined; } function options(name: string): string[] { return process.argv.flatMap((value, index) => value === name && process.argv[index + 1] ? [process.argv[index + 1]] : [], ); } function slugify(value: string): string { return value .toLowerCase() .replace(/[’']/g, "") .replace(/[^a-z0-9]+/g, "-") .replace(/^-+|-+$/g, ""); } function parseArgs(): Args { const sourcePath = process.argv.slice(2).find((arg) => !arg.startsWith("--")); if (!sourcePath) { throw new Error( "Usage: pnpm knowledge:stage --summary [--title ] [--slug <slug>] [--claim-mode factual|perspective|mixed] [--source title=url]", ); } const summary = option("--summary"); if (!summary) throw new Error("--summary is required; do not stage placeholder public copy"); const claimMode = (option("--claim-mode") ?? "factual") as Args["claimMode"]; const confidence = (option("--confidence") ?? "medium") as Args["confidence"]; if (!["factual", "perspective", "mixed"].includes(claimMode)) throw new Error("invalid --claim-mode"); if (!["high", "medium", "low"].includes(confidence)) throw new Error("invalid --confidence"); const sources = options("--source").map((value) => { const separator = value.indexOf("="); if (separator < 1) throw new Error("--source must use title=https://public.example/path"); const title = value.slice(0, separator).trim(); const url = value.slice(separator + 1).trim(); new URL(url); return { title, url }; }); return { sourcePath, slug: option("--slug"), title: option("--title"), summary, claimMode, perspectiveOwner: option("--perspective-owner"), confidence, sources, }; } function main(): void { const args = parseArgs(); const policy = loadKnowledgePolicy(); const decision = decideKnowledgeSource(args.sourcePath, policy); if (decision.decision !== "stage" || !decision.mapping) { throw new Error(`source denied: ${decision.relativePath} (${decision.reason})`); } const source = readFileSync(resolve(args.sourcePath), "utf8"); const parsed = matter(source); const inferredTitle = args.title ?? (typeof parsed.data.title === "string" ? parsed.data.title : undefined) ?? parsed.content.match(/^#\s+(.+)$/m)?.[1] ?? basename(args.sourcePath, ".md"); const slug = args.slug ?? slugify(inferredTitle); const summary = args.summary; const body = publicizeObsidianMarkdown(parsed.content, inferredTitle); const findings = scanKnowledgeDraft(parsed.content, policy); const blocking = findings.filter((finding) => finding.severity === "block"); if (blocking.length > 0) { const details = blocking.map((finding) => `${finding.code}: ${finding.detail}`).join(", "); throw new Error(`source contains blocked material (${details})`); } const now = new Date().toISOString(); const frontmatter = { title: inferredTitle, slug, summary, kind: decision.mapping.kind, status: "evolving", claimMode: args.claimMode, ...(args.perspectiveOwner ? { perspectiveOwner: args.perspectiveOwner } : {}), confidence: args.confidence, topics: [...new Set([ ...decision.mapping.topics, ...(Array.isArray(parsed.data.tags) ? parsed.data.tags.filter((tag): tag is string => typeof tag === "string") : []), ])], related: [], sources: args.sources, aiAssisted: true, generatedBy: "Co", sourceDigest: `sha256:${createHash("sha256").update(source).digest("hex")}`, updated: now, reviewStatus: "draft", }; const outputDirectory = resolve(process.cwd(), "knowledge/staged"); mkdirSync(outputDirectory, { recursive: true }); const outputPath = resolve(outputDirectory, `${slug}.md`); writeFileSync(outputPath, matter.stringify(`${body}\n`, frontmatter), "utf8"); console.log(JSON.stringify({ decision: "staged", sourceClass: decision.mapping.pattern, sourceDigest: frontmatter.sourceDigest, outputPath, findings, next: "Inspect and rewrite the complete draft, add public sources, run knowledge:check, then obtain exact rendered approval before promotion.", }, null, 2)); } try { main(); } catch (error) { console.error(error instanceof Error ? error.message : error); process.exit(1); }