Luigit
repositories / pi-ext

pi-ext

bugabingas pi extensions

owned by admin

extensions/the-system/init.ts

Raw
import { lstatSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
import path from "node:path";
import {
	BOARD_PATH,
	BOARD_SCHEMA_VERSION,
	boardStateForInitialization,
	boardStateSource,
	emptyBoardState,
} from "./board-state.ts";
import { resolveGovernedPath, SystemDocumentError } from "./project-root.ts";
import {
	parseSystemConfig,
	SCHEMA_VERSION,
	systemConfigForInitialization,
} from "./system-config.ts";

export type InitializationInspection = Readonly<{
	root: string;
	prefix?: string;
	title?: string;
	currentPhase?: string;
	cores: readonly Readonly<{ path: string; state: "missing" | "preserve" }>[];
	creates: readonly string[];
	rewrites: readonly string[];
}>;
export type InitializationResult = Readonly<{
	root: string;
	prefix: string;
	changed: readonly string[];
	pending: readonly string[];
}>;

const DIRECTORIES = [".system"] as const;
const COLLECTION_DIRECTORIES = [
	".system/specs",
	".system/phases",
	".system/plans",
	".system/issues",
	".system/research",
] as const;
const CORES = [
	".system/SYSTEM.md",
	".system/MISSION.md",
	".system/RULES.md",
] as const;
const CONFIG = ".system/config.json";
const CORE_CONTENT: Readonly<Record<(typeof CORES)[number], string>> = {
	".system/SYSTEM.md":
		"# SYSTEM\n\nStable project identity, domain truths, architecture invariants, and non-negotiable conventions.\nKeep durable; do not put current tasks here.\n",
	".system/MISSION.md":
		"# MISSION\n\nEnduring high-level project objective and why it matters.\nUpdate only when the end goal changes; current operating priorities belong in a phase.\n",
	".system/RULES.md":
		"# RULES\n\nConcrete operating constraints: commands, safety, style, boundaries, and do/don't rules.\nKeep executable; avoid goals and rationale unless needed.\n",
};

const target = (root: string, relative: string) =>
	resolveGovernedPath(root, relative);
const kind = (file: string) => {
	const stats = lstatSync(file, { throwIfNoEntry: false });
	if (!stats) return "missing" as const;
	if (stats.isSymbolicLink()) return "link" as const;
	if (stats.isDirectory()) return "directory" as const;
	if (stats.isFile()) return "file" as const;
	return "other" as const;
};
const requireKind = (
	root: string,
	relative: string,
	expected: "directory" | "file",
) => {
	const actual = kind(target(root, relative));
	if (actual !== "missing" && actual !== expected)
		throw new SystemDocumentError(
			"init.unsafe-path",
			`${relative} must be a regular ${expected}; found ${actual}`,
		);
	return actual;
};
const projectTitle = (root: string) => path.basename(root) || "Project";
const validatePrefix = (prefix: string) => {
	parseSystemConfig(
		JSON.stringify({
			prefix,
			schemaVersion: SCHEMA_VERSION,
		}),
	);
	return prefix;
};
const configSource = (prefix: string, title: string, currentPhase?: string) =>
	`${JSON.stringify(
		{
			prefix,
			schemaVersion: SCHEMA_VERSION,
			title,
			...(currentPhase ? { currentPhase } : {}),
		},
		null,
		"\t",
	)}\n`;

export function inspectInitialization(
	cwd: string,
	requestedPrefix?: string,
): InitializationInspection {
	const root = path.resolve(cwd);
	for (const relative of DIRECTORIES) requireKind(root, relative, "directory");
	for (const relative of COLLECTION_DIRECTORIES)
		requireKind(root, relative, "directory");
	const configKind = requireKind(root, CONFIG, "file");
	const boardKind = requireKind(root, BOARD_PATH, "file");
	const requested = requestedPrefix
		? validatePrefix(requestedPrefix)
		: undefined;
	const inspected =
		configKind === "file"
			? systemConfigForInitialization(
					readFileSync(target(root, CONFIG), "utf8"),
				)
			: undefined;
	const config = inspected?.config;
	if (config && requested && config.prefix !== requested)
		throw new SystemDocumentError(
			"init.prefix-conflict",
			`Existing prefix ${config.prefix} does not match requested ${requested}`,
		);
	const prefix = config?.prefix ?? requested;
	const board =
		boardKind === "file" && prefix
			? boardStateForInitialization(
					readFileSync(target(root, BOARD_PATH), "utf8"),
					prefix,
				)
			: undefined;
	const creates: string[] = [];
	for (const relative of DIRECTORIES)
		if (kind(target(root, relative)) === "missing") creates.push(relative);
	if (configKind === "missing") creates.push(CONFIG);
	if (boardKind === "missing") creates.push(BOARD_PATH);
	const rewrites = [
		...(inspected?.sourceSchemaVersion === undefined
			? []
			: [
					`${CONFIG} schemaVersion ${inspected.sourceSchemaVersion} -> ${SCHEMA_VERSION}`,
				]),
		...(board?.sourceSchemaVersion === undefined
			? []
			: [
					`${BOARD_PATH} schemaVersion ${board.sourceSchemaVersion} -> ${BOARD_SCHEMA_VERSION}`,
				]),
	];
	return {
		root,
		prefix,
		...(config?.title ? { title: config.title } : {}),
		...(config?.currentPhase ? { currentPhase: config.currentPhase } : {}),
		cores: CORES.map((relative) => ({
			path: relative,
			state:
				kind(target(root, relative)) === "missing" ? "missing" : "preserve",
		})),
		creates,
		rewrites,
	};
}

export function applyInitialization(
	cwd: string,
	requestedPrefix?: string,
	approveRewrite = false,
): InitializationResult {
	const inspection = inspectInitialization(cwd, requestedPrefix);
	if (!inspection.prefix)
		throw new SystemDocumentError(
			"init.prefix-required",
			"An approved project prefix is required before initialization",
		);
	const changed: string[] = [];
	for (const relative of DIRECTORIES) {
		if (kind(target(inspection.root, relative)) !== "missing") continue;
		mkdirSync(target(inspection.root, relative));
		changed.push(relative);
	}
	if (
		kind(target(inspection.root, CONFIG)) === "missing" ||
		(approveRewrite &&
			inspection.rewrites.some((item) => item.startsWith(`${CONFIG} `)))
	) {
		writeFileSync(
			target(inspection.root, CONFIG),
			configSource(
				inspection.prefix,
				inspection.title ?? projectTitle(inspection.root),
				inspection.currentPhase,
			),
			kind(target(inspection.root, CONFIG)) === "missing"
				? { encoding: "utf8", flag: "wx" }
				: "utf8",
		);
		changed.push(CONFIG);
	}
	if (
		kind(target(inspection.root, BOARD_PATH)) === "missing" ||
		(approveRewrite &&
			inspection.rewrites.some((item) => item.startsWith(`${BOARD_PATH} `)))
	) {
		const current =
			kind(target(inspection.root, BOARD_PATH)) === "file"
				? boardStateForInitialization(
						readFileSync(target(inspection.root, BOARD_PATH), "utf8"),
						inspection.prefix,
					).state
				: emptyBoardState();
		writeFileSync(
			target(inspection.root, BOARD_PATH),
			boardStateSource(current),
			kind(target(inspection.root, BOARD_PATH)) === "missing"
				? { encoding: "utf8", flag: "wx" }
				: "utf8",
		);
		changed.push(BOARD_PATH);
	}
	for (const core of inspection.cores) {
		if (core.state !== "missing") continue;
		writeFileSync(
			target(inspection.root, core.path),
			CORE_CONTENT[core.path as keyof typeof CORE_CONTENT],
			{
				encoding: "utf8",
				flag: "wx",
			},
		);
		changed.push(core.path);
	}
	return {
		root: inspection.root,
		prefix: inspection.prefix,
		changed,
		pending: approveRewrite ? [] : inspection.rewrites,
	};
}