/** * Session opening materials (#443 / #524). Missing files fail as native readFile * errors. Main-role materials derive from PUBLIC_ROLE_RECORDS (including * gatekeeper and navigator). Province map reuses that public gatekeeper record * plus officer material constants (notary shared). Auditor composition stays in * loadAuditorSoul(subject); loaders share joinPackageMaterials. */ import { existsSync } from "node:fs"; import { readFile } from "node:fs/promises"; import { dirname, join } from "node:path"; import { fileURLToPath, pathToFileURL } from "node:url"; import { INSPECTOR_SESSION_MATERIALS, NOTARY_SESSION_MATERIALS, PUBLIC_ROLE_RECORDS, type PackagedRole, } from "./packaged-role-registry.ts"; /** * Resolve the install package root from a module URL. * Works for src/ layout and for bundled artifacts under dist/ (walk up until * package.json + souls/ — the shipped material tree). * Sole package-root walk for deferred loaders that lack an injected packageRoot * (#443 materials; #580/#636 runtime-constructed imports). */ export function resolvePackageRootDir(moduleUrl: string = import.meta.url): string { let dir = dirname(fileURLToPath(moduleUrl)); for (let i = 0; i < 8; i += 1) { if (existsSync(join(dir, "package.json")) && existsSync(join(dir, "souls"))) { return dir; } const parent = dirname(dir); if (parent === dir) break; dir = parent; } // Legacy fallback: module lived directly under src/. return fileURLToPath(new URL("..", moduleUrl)); } const packageRootUrl = pathToFileURL(resolvePackageRootDir() + "/").href; /** Self-locate one package-relative material. Native I/O errors propagate. */ export async function readPackageMaterial(relativePath: string): Promise { return readFile(fileURLToPath(new URL(relativePath, packageRootUrl)), "utf8"); } /** Read materials in order and join with a blank line. */ export async function joinPackageMaterials( relativePaths: readonly string[], ): Promise { const chunks: string[] = []; for (const relativePath of relativePaths) chunks.push(await readPackageMaterial(relativePath)); return chunks.join("\n\n"); } function roleSoulPath(role: string, materials: readonly string[]): string { const suffix = `/souls/${role}.md`; const path = materials.find((candidate) => `/${candidate}`.endsWith(suffix)); if (path === undefined) throw new Error(`session materials omit the ${role} Soul`); return path; } async function loadSeparatedSessionPart( role: string, materials: readonly string[], part: "soul" | "references", ): Promise { const soul = roleSoulPath(role, materials); return part === "soul" ? readPackageMaterial(soul) : joinPackageMaterials(materials.filter((path) => path !== soul)); } type PublicRoleMaterials = { readonly [Role in PackagedRole]: Extract< (typeof PUBLIC_ROLE_RECORDS)[number], { role: Role } >["sessionMaterials"]; }; /** Derived projection: public records (#639 includes navigator/gatekeeper). */ export const MAIN_ROLE_SESSION_MATERIALS = { ...(Object.fromEntries( PUBLIC_ROLE_RECORDS.map((record) => [record.role, record.sessionMaterials]), ) as PublicRoleMaterials), } as const; export type MainRoleSession = keyof typeof MAIN_ROLE_SESSION_MATERIALS; export function loadMainRoleSessionMaterials(role: MainRoleSession): Promise { return loadSeparatedSessionPart(role, MAIN_ROLE_SESSION_MATERIALS[role], "soul"); } export function loadMainRoleReferenceMaterials(role: MainRoleSession): Promise { return loadSeparatedSessionPart(role, MAIN_ROLE_SESSION_MATERIALS[role], "references"); } /** Gatekeeper province; notary reuses the public notary materials definition. */ export const GATEKEEPER_SESSION_MATERIALS = { // #639: single authority — the public gatekeeper record owns the province list. gatekeeper: PUBLIC_ROLE_RECORDS.find((entry) => entry.role === "gatekeeper")!.sessionMaterials, inspector: INSPECTOR_SESSION_MATERIALS, notary: NOTARY_SESSION_MATERIALS, } as const; export type GatekeeperSessionRole = keyof typeof GATEKEEPER_SESSION_MATERIALS; export function loadGatekeeperSessionMaterials(role: GatekeeperSessionRole): Promise { return loadSeparatedSessionPart(role, GATEKEEPER_SESSION_MATERIALS[role], "soul"); }