import { randomBytes } from "node:crypto"; import { readFileSync } from "node:fs"; import path from "node:path"; import { directoryForType, discoverDocumentBundles, indexBundleDocuments, parseSystemConfig, requireProjectRoot, } from "./documents.js"; export type BundleType = "research" | "spec" | "phase" | "plan" | "issue"; export type BundleSlot = Readonly<{ type: BundleType; title: string; id: string; slug: string; path: string; }>; export function reserveBundleSlot( cwd: string, input: { type: BundleType; title: string }, reserved: Set = new Set(), ): BundleSlot { const root = requireProjectRoot(cwd); const config = parseSystemConfig( readFileSync(path.join(root, ".system/config.json"), "utf8"), ); const discovery = discoverDocumentBundles(root); const indexed = indexBundleDocuments(discovery.sources, config.prefix); const existing = new Set([ ...indexed.documents.map((document) => document.metadata.id), ...reserved, ]); const title = input.title.trim(); if (!title) throw new Error("System bundle title must not be empty"); const id = generateDocumentId(config.prefix, input.type, existing); reserved.add(id); const slug = slugForTitle(title); return { type: input.type, title, id, slug, path: `.system/${directoryForType(input.type)}/${id}-${slug}/index.md`, }; } function generateDocumentId( prefix: string, type: BundleType, existing: ReadonlySet, ): string { while (true) { const token = randomBytes(6) .toString("base64url") .toUpperCase() .replaceAll("-", "_"); const id = `${prefix}-${type.toUpperCase()}-${token}`; if (!existing.has(id)) return id; } } function slugForTitle(title: string): string { const slug = title .normalize("NFKD") .replace(/\p{Mark}/gu, "") .toLowerCase() .replace(/[^a-z0-9]+/g, "-") .replace(/^-|-$/g, "") .slice(0, 48) .replace(/-$/g, ""); return slug || "bundle"; }