Luigit
repositories / pi-ext

pi-ext

bugabingas pi extensions

owned by admin

extensions/the-system/system-config.ts

Raw
import { SystemDocumentError } from "./project-root.ts";

export const SCHEMA_VERSION = 0;

export const DOCUMENT_TYPES = [
	"spec",
	"phase",
	"plan",
	"issue",
	"research",
] as const;
export type DocumentType = (typeof DOCUMENT_TYPES)[number];
export type SystemConfig = Readonly<{
	prefix: string;
	schemaVersion: number;
	title?: string;
	currentPhase?: string;
}>;
export type InitializationSystemConfig = Readonly<{
	config: SystemConfig;
	sourceSchemaVersion?: number;
}>;
export type ParsedDocumentId = Readonly<{
	id: string;
	prefix: string;
	type: DocumentType;
	token: string;
}>;

const PREFIX = /^[A-Z][A-Z0-9]{1,7}$/;
const ID_CHARS = "[A-Z0-9_-]{8}";
const TYPE_BY_TOKEN = new Map(
	DOCUMENT_TYPES.map((type) => [type.toUpperCase(), type]),
);

const objectValue = (
	value: unknown,
	label: string,
): Record<string, unknown> => {
	if (typeof value !== "object" || value === null || Array.isArray(value))
		throw new SystemDocumentError(
			"json.shape",
			`${label} must be a JSON object`,
		);
	return value as Record<string, unknown>;
};

const duplicateTopLevelKeys = (source: string): string[] => {
	const keys: string[] = [];
	const duplicates = new Set<string>();
	let depth = 0;
	let index = 0;
	while (index < source.length) {
		const character = source[index];
		if (character === "{") {
			depth += 1;
			index += 1;
			continue;
		}
		if (character === "}") {
			depth -= 1;
			index += 1;
			continue;
		}
		if (character !== '"') {
			index += 1;
			continue;
		}
		const start = index++;
		let escaped = false;
		while (index < source.length) {
			const current = source[index++];
			if (!escaped && current === '"') break;
			escaped = !escaped && current === "\\";
			if (current !== "\\") escaped = false;
		}
		if (depth !== 1) continue;
		let cursor = index;
		while (/\s/.test(source[cursor] ?? "")) cursor += 1;
		if (source[cursor] !== ":") continue;
		const key = JSON.parse(source.slice(start, index)) as string;
		if (keys.includes(key)) duplicates.add(key);
		keys.push(key);
	}
	return [...duplicates].sort();
};

export function parseSystemConfig(source: string): SystemConfig {
	let parsed: unknown;
	try {
		parsed = JSON.parse(source);
	} catch {
		throw new SystemDocumentError("config.invalid-json", "Invalid config JSON");
	}
	const duplicates = duplicateTopLevelKeys(source);
	if (duplicates.length)
		throw new SystemDocumentError(
			"config.duplicate-key",
			`Duplicate config keys: ${duplicates.join(", ")}`,
		);
	const value = objectValue(parsed, "Config");
	if (value.schemaVersion !== SCHEMA_VERSION)
		throw new SystemDocumentError(
			"config.version",
			`Schema version ${String(value.schemaVersion)} does not match ${SCHEMA_VERSION}; run /system init`,
		);
	const keys = Object.keys(value).sort();
	const validKeys = ["currentPhase", "prefix", "schemaVersion", "title"];
	if (
		!("prefix" in value) ||
		!("schemaVersion" in value) ||
		keys.some((key) => !validKeys.includes(key))
	)
		throw new SystemDocumentError(
			"json.keys",
			`Expected keys prefix, schemaVersion[, title, currentPhase]; received ${keys.join(", ")}`,
		);
	if (typeof value.prefix !== "string" || !PREFIX.test(value.prefix))
		throw new SystemDocumentError("config.prefix", "Invalid project prefix");
	if (
		"title" in value &&
		(typeof value.title !== "string" || value.title.trim() === "")
	)
		throw new SystemDocumentError("config.title", "Invalid project title");
	if ("currentPhase" in value) {
		if (typeof value.currentPhase !== "string")
			throw new SystemDocumentError(
				"config.current-phase",
				"Current phase must be a phase ID",
			);
		const current = parseDocumentId(value.currentPhase, value.prefix);
		if (current.type !== "phase")
			throw new SystemDocumentError(
				"config.current-phase",
				"Current phase must be a phase ID",
			);
	}
	return {
		prefix: value.prefix,
		schemaVersion: value.schemaVersion as number,
		...(typeof value.title === "string" ? { title: value.title } : {}),
		...(typeof value.currentPhase === "string"
			? { currentPhase: value.currentPhase }
			: {}),
	};
}

export function systemConfigForInitialization(
	source: string,
): InitializationSystemConfig {
	try {
		return { config: parseSystemConfig(source) };
	} catch (error) {
		if (
			!(error instanceof SystemDocumentError) ||
			error.code !== "config.version"
		)
			throw error;
		const parsed = objectValue(JSON.parse(source), "Config");
		if (
			duplicateTopLevelKeys(source).length > 0 ||
			typeof parsed.schemaVersion !== "number" ||
			!Number.isInteger(parsed.schemaVersion)
		)
			throw error;
		const config = parseSystemConfig(
			JSON.stringify({
				prefix: parsed.prefix,
				schemaVersion: SCHEMA_VERSION,
				...("title" in parsed ? { title: parsed.title } : {}),
				...("currentPhase" in parsed
					? { currentPhase: parsed.currentPhase }
					: {}),
			}),
		);
		return { config, sourceSchemaVersion: parsed.schemaVersion };
	}
}

export function parseDocumentId(
	source: string,
	expectedPrefix?: string,
): ParsedDocumentId {
	const match = new RegExp(
		`^([A-Z][A-Z0-9]{1,7})-(SPEC|PHASE|PLAN|ISSUE|RESEARCH)-(${ID_CHARS})$`,
	).exec(source);
	if (!match)
		throw new SystemDocumentError(
			"id.invalid",
			`Invalid document ID: ${source}`,
		);
	if (expectedPrefix && match[1] !== expectedPrefix)
		throw new SystemDocumentError(
			"id.prefix",
			`Document ID must use prefix ${expectedPrefix}: ${source}`,
		);
	return {
		id: source,
		prefix: match[1] as string,
		type: TYPE_BY_TOKEN.get(match[2]) as DocumentType,
		token: match[3] as string,
	};
}