Luigit
repositories / pi-ext

pi-ext

bugabingas pi extensions

owned by admin

extensions/the-system/bundle-slot.ts

Raw
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<string> = 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>,
): 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";
}