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