import { readFile } from "node:fs/promises"; import { basename, join } from "node:path"; import { isRecord } from "./unknown-value.ts"; import { joinPackageMaterials, readPackageMaterial, } from "./session-opening-materials.ts"; /** * Active auditor seats. * Fixer LLM auditor retired by #242; reviewer-side 审刑院 gate retired by #495 S6. * souls/fixer-auditor.md and souls/reviewer-auditor.md retained-or-deleted on disk as owner decisions; not active. */ export const AUDITOR_SOUL_ROLES = [ "judge", "doctor", ] as const; export type AuditorSoulRole = (typeof AUDITOR_SOUL_ROLES)[number]; /** * Audited-subject input for public 审刑院 (#675 owner): * 「审的是谁」is an input selecting judge-auditor.md / doctor-auditor.md — * not a caller-identity fork. Same env for direct `ak-role auditor --subject` * and nested compliance summons. */ export const AK_ROLE_AUDITOR_SUBJECT_ENV = "AK_ROLE_AUDITOR_SUBJECT" as const; /** * Audited source-run input for public 审刑院 (#675): * same --source-run face for direct `ak-role auditor` and nested compliance summons. * Never falls back to the auditor's own run directory. */ export const AK_ROLE_AUDITOR_SOURCE_RUN_ENV = "AK_ROLE_AUDITOR_SOURCE_RUN" as const; function auditorSoulRelativePath(role: AuditorSoulRole): string { return `souls/${role}-auditor.md`; } /** * #470 auditor session materials. Judge carries audit-law + quality-law; doctor does * not (御批四: 参审席 = 大理寺主会话 + 其审计席; 太医线不动). * Reviewer auditor roster removed with #495 S6 gate retirement. * #675 owner: no generic auditor.md — subject selects this table. */ export const AUDITOR_SESSION_MATERIALS = { judge: [ "CLAUDE.md", "souls/judge-auditor.md", "souls/audit-law.md", "souls/quality-law.md", "docs/adr/0057-schema-narrowing-cuts-the-required-set-not-the-declared-set.md", ], doctor: ["CLAUDE.md", "souls/doctor-auditor.md", "docs/adr/0057-schema-narrowing-cuts-the-required-set-not-the-declared-set.md"], } as const satisfies Record< AuditorSoulRole, readonly [string, string, ...(readonly string[])] >; export function isAuditorSoulRole(value: unknown): value is AuditorSoulRole { return typeof value === "string" && Object.hasOwn(AUDITOR_SESSION_MATERIALS, value); } /** Resolve audited subject from explicit value or the subject-input env. */ export function resolveAuditorSubject(raw?: string): AuditorSoulRole { const value = typeof raw === "string" && raw.trim() !== "" ? raw.trim() : typeof process.env[AK_ROLE_AUDITOR_SUBJECT_ENV] === "string" ? process.env[AK_ROLE_AUDITOR_SUBJECT_ENV].trim() : ""; if (!isAuditorSoulRole(value)) { throw new Error( `auditor subject must be judge|doctor (input --subject / ${AK_ROLE_AUDITOR_SUBJECT_ENV}), got ${value === "" ? "(missing)" : value}`, ); } return value; } /** * Load one complete auditor session afresh for each audit invocation. * Blank-soul identity stays owned here; composition reuses joinPackageMaterials. * Subject selects the materials table (#675 owner — same for direct and nested). */ export async function loadAuditorSoul(role: AuditorSoulRole): Promise { const materials = AUDITOR_SESSION_MATERIALS[role]; const soulPath = auditorSoulRelativePath(role); const soul = await readPackageMaterial(soulPath); if (soul.trim().length === 0) { throw new Error(`The ${role} auditor Soul is blank`); } return soul; } export function loadAuditorReferenceMaterials(role: AuditorSoulRole): Promise { const soulPath = auditorSoulRelativePath(role); return joinPackageMaterials(AUDITOR_SESSION_MATERIALS[role].filter((path) => path !== soulPath)); } /** Runtime loader: subject input decides which soul file to assemble. */ export async function loadAuditorSoulFromSubjectInput(raw?: string): Promise { return loadAuditorSoul(resolveAuditorSubject(raw)); } export function loadAuditorReferenceMaterialsFromSubjectInput(raw?: string): Promise { return loadAuditorReferenceMaterials(resolveAuditorSubject(raw)); } function subjectFromSourceDirectory(sourceRunDirectory: string): AuditorSoulRole | undefined { const role = basename(sourceRunDirectory).split("@")[1]; return isAuditorSoulRole(role) ? role : undefined; } /** * Resume binding already on the admitted page. * Explicit subject wins; otherwise the source-run role is the audited object. */ export async function readAuditorResumeBinding(runDirectory: string): Promise< | { readonly subject: AuditorSoulRole; readonly sourceRunDirectory: string } | undefined > { let raw: unknown; try { raw = JSON.parse(await readFile(join(runDirectory, "admitted-request.json"), "utf8")) as unknown; } catch (error) { if ((error as NodeJS.ErrnoException).code === "ENOENT") return undefined; throw error; } if (!isRecord(raw)) return undefined; const record = raw as { sourceRunPath?: unknown; auditorSubject?: unknown }; if (typeof record.sourceRunPath !== "string" || record.sourceRunPath.trim() === "") return undefined; const subject = isAuditorSoulRole(record.auditorSubject) ? record.auditorSubject : subjectFromSourceDirectory(record.sourceRunPath); if (subject === undefined) return undefined; return { subject, sourceRunDirectory: record.sourceRunPath }; }