repositories / pi-ext
pi-ext
bugabingas pi extensions
owned by admin
extensions/the-system/init.ts
Rawimport { 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,
};
}