Intelligence
Artifacts
Browse the repository, read documents, and manage the governance folders. Source, runtime, and infrastructure are read-only.
Repository
repositories/aaf-holdings/hq01/lib/missions/governance.ts
13.2 KB
import fs from "node:fs";
import path from "node:path";
import { missionDir, readMission } from "./store";
import type {
BlockReason,
Decision,
Dependency,
DependencyStatus,
DependencyType,
GovernanceEvent,
GovernanceEventType,
Kpi,
KpiStatus,
Risk,
RiskLikelihood,
RiskSeverity,
RiskStatus,
WithHistory,
} from "./types";
/**
* Governance instruments (PASS M6): Risks, Decisions, Dependencies, KPIs.
* Each is a mission-owned record with append-only history. Records only — no
* intelligence, no automation, no automatic transitions. Decisions are
* immutable (corrections create new decisions).
*/
export class GovernanceError extends Error {
constructor(
message: string,
readonly status = 400,
) {
super(message);
this.name = "GovernanceError";
}
}
function nowIso(): string {
return new Date().toISOString();
}
/** A small filesystem store for one instrument kind under a mission. */
function instrument<T extends { id: string }>(
kind: string,
jsonName: string,
prefix: string,
) {
const idRe = new RegExp(`^${prefix}-\\d{6}$`);
const dir = (mid: string) => path.join(missionDir(mid), kind);
const objDir = (mid: string, id: string) => path.join(dir(mid), id);
const jsonPath = (mid: string, id: string) => path.join(objDir(mid, id), jsonName);
const histPath = (mid: string, id: string) =>
path.join(objDir(mid, id), "history", "log.jsonl");
function nextId(mid: string): string {
let max = 0;
let entries: fs.Dirent[] = [];
try {
entries = fs.readdirSync(dir(mid), { withFileTypes: true });
} catch {
/* none */
}
for (const e of entries) {
if (e.isDirectory() && idRe.test(e.name)) {
const n = Number(e.name.slice(prefix.length + 1));
if (Number.isFinite(n) && n > max) max = n;
}
}
return `${prefix}-${String(max + 1).padStart(6, "0")}`;
}
function write(mid: string, obj: T): void {
const file = jsonPath(mid, obj.id);
fs.mkdirSync(path.dirname(file), { recursive: true });
const tmp = `${file}.tmp`;
fs.writeFileSync(tmp, JSON.stringify(obj, null, 2) + "\n", "utf8");
fs.renameSync(tmp, file);
}
function read(mid: string, id: string): T | null {
if (!idRe.test(id)) return null;
try {
return JSON.parse(fs.readFileSync(jsonPath(mid, id), "utf8")) as T;
} catch {
return null;
}
}
function list(mid: string): T[] {
let entries: fs.Dirent[];
try {
entries = fs.readdirSync(dir(mid), { withFileTypes: true });
} catch {
return [];
}
return entries
.filter((e) => e.isDirectory() && idRe.test(e.name))
.map((e) => read(mid, e.name))
.filter((x): x is T => Boolean(x))
.sort((a, b) => a.id.localeCompare(b.id));
}
function append(
mid: string,
id: string,
type: GovernanceEventType,
extra: Partial<GovernanceEvent> = {},
): void {
const file = histPath(mid, id);
fs.mkdirSync(path.dirname(file), { recursive: true });
const ev: GovernanceEvent = { type, at: nowIso(), ...extra };
fs.appendFileSync(file, JSON.stringify(ev) + "\n", "utf8");
}
function history(mid: string, id: string): GovernanceEvent[] {
let raw: string;
try {
raw = fs.readFileSync(histPath(mid, id), "utf8");
} catch {
return [];
}
return raw
.split("\n")
.filter(Boolean)
.map((l) => {
try {
return JSON.parse(l) as GovernanceEvent;
} catch {
return null;
}
})
.filter((e): e is GovernanceEvent => Boolean(e));
}
function listWithHistory(mid: string): WithHistory<T>[] {
return list(mid).map((item) => ({
item,
history: history(mid, item.id).sort((a, b) => b.at.localeCompare(a.at)),
}));
}
return { nextId, write, read, list, append, history, listWithHistory };
}
const risks = instrument<Risk>("risks", "risk.json", "RISK");
const decisions = instrument<Decision>("decisions", "decision.json", "DECISION");
const dependencies = instrument<Dependency>("dependencies", "dependency.json", "DEPENDENCY");
const kpis = instrument<Kpi>("kpis", "kpi.json", "KPI");
function ensureMission(mid: string): void {
if (!readMission(mid)) throw new GovernanceError("Mission not found.", 404);
}
// --- Risks -----------------------------------------------------------------
export interface CreateRiskInput {
title: string;
description: string;
severity: RiskSeverity;
likelihood: RiskLikelihood;
owner_executive?: string;
mitigation?: string;
}
export interface UpdateRiskInput {
title?: string;
description?: string;
severity?: RiskSeverity;
likelihood?: RiskLikelihood;
owner_executive?: string | null;
status?: RiskStatus;
mitigation?: string;
}
export function createRisk(mid: string, input: CreateRiskInput): Risk {
ensureMission(mid);
if (!input.title?.trim()) throw new GovernanceError("Title is required.");
const id = risks.nextId(mid);
const created = nowIso();
const risk: Risk = {
id,
mission_id: mid,
title: input.title.trim(),
description: input.description?.trim() || "",
severity: input.severity || "Medium",
likelihood: input.likelihood || "Medium",
owner_executive: input.owner_executive?.trim() || null,
status: "Open",
mitigation: input.mitigation?.trim() || "",
created_at: created,
updated_at: created,
};
risks.write(mid, risk);
risks.append(mid, id, "created", { detail: risk.title });
return risk;
}
export function updateRisk(mid: string, id: string, patch: UpdateRiskInput): Risk {
const risk = risks.read(mid, id);
if (!risk) throw new GovernanceError("Risk not found.", 404);
const next: Risk = { ...risk };
const prevStatus = risk.status;
if (patch.title !== undefined) next.title = patch.title.trim();
if (patch.description !== undefined) next.description = patch.description.trim();
if (patch.severity !== undefined) next.severity = patch.severity;
if (patch.likelihood !== undefined) next.likelihood = patch.likelihood;
if (patch.mitigation !== undefined) next.mitigation = patch.mitigation.trim();
if (patch.owner_executive !== undefined) next.owner_executive = patch.owner_executive?.trim() || null;
if (patch.status !== undefined) next.status = patch.status;
next.updated_at = nowIso();
risks.write(mid, next);
risks.append(mid, id, "updated");
if (patch.status !== undefined && patch.status !== prevStatus) {
risks.append(mid, id, "state_changed", { previous: prevStatus, next: patch.status });
}
return next;
}
export const listRisks = (mid: string) => risks.list(mid);
export const listRisksWithHistory = (mid: string) => risks.listWithHistory(mid);
// --- Decisions (immutable) -------------------------------------------------
export interface CreateDecisionInput {
title: string;
description?: string;
decision: string;
rationale?: string;
alternatives_considered?: string[];
approved_by: string;
}
export function createDecision(mid: string, input: CreateDecisionInput): Decision {
ensureMission(mid);
if (!input.title?.trim()) throw new GovernanceError("Title is required.");
if (!input.decision?.trim()) throw new GovernanceError("Decision is required.");
if (!input.approved_by?.trim()) throw new GovernanceError("Approved By is required.");
const id = decisions.nextId(mid);
const decision: Decision = {
id,
mission_id: mid,
title: input.title.trim(),
description: input.description?.trim() || "",
decision: input.decision.trim(),
rationale: input.rationale?.trim() || "",
alternatives_considered: (input.alternatives_considered ?? [])
.map((a) => a.trim())
.filter(Boolean),
approved_by: input.approved_by.trim(),
created_at: nowIso(),
};
decisions.write(mid, decision);
decisions.append(mid, id, "created", { detail: decision.title });
return decision;
}
export const listDecisions = (mid: string) => decisions.list(mid);
export const listDecisionsWithHistory = (mid: string) => decisions.listWithHistory(mid);
// --- Dependencies ----------------------------------------------------------
export interface CreateDependencyInput {
depends_on: string;
type: DependencyType;
description?: string;
blocking?: boolean;
}
export interface UpdateDependencyInput {
depends_on?: string;
type?: DependencyType;
description?: string;
blocking?: boolean;
status?: DependencyStatus;
}
export function createDependency(mid: string, input: CreateDependencyInput): Dependency {
ensureMission(mid);
if (!input.depends_on?.trim()) throw new GovernanceError("Depends On is required.");
const id = dependencies.nextId(mid);
const created = nowIso();
const dep: Dependency = {
id,
mission_id: mid,
depends_on: input.depends_on.trim(),
type: input.type || "External",
status: "Pending",
blocking: Boolean(input.blocking),
description: input.description?.trim() || "",
created_at: created,
updated_at: created,
};
dependencies.write(mid, dep);
dependencies.append(mid, id, "created", { detail: `${dep.type}: ${dep.depends_on}` });
return dep;
}
export function updateDependency(mid: string, id: string, patch: UpdateDependencyInput): Dependency {
const dep = dependencies.read(mid, id);
if (!dep) throw new GovernanceError("Dependency not found.", 404);
const next: Dependency = { ...dep };
const prevStatus = dep.status;
if (patch.depends_on !== undefined) next.depends_on = patch.depends_on.trim();
if (patch.type !== undefined) next.type = patch.type;
if (patch.description !== undefined) next.description = patch.description.trim();
if (patch.blocking !== undefined) next.blocking = patch.blocking;
if (patch.status !== undefined) next.status = patch.status;
next.updated_at = nowIso();
dependencies.write(mid, next);
dependencies.append(mid, id, "updated");
if (patch.status !== undefined && patch.status !== prevStatus) {
dependencies.append(mid, id, "state_changed", { previous: prevStatus, next: patch.status });
}
return next;
}
export const listDependencies = (mid: string) => dependencies.list(mid);
export const listDependenciesWithHistory = (mid: string) => dependencies.listWithHistory(mid);
// --- KPIs (informational) --------------------------------------------------
export interface CreateKpiInput {
name: string;
description?: string;
current_value?: string;
target_value?: string;
unit?: string;
status?: KpiStatus;
}
export interface UpdateKpiInput {
name?: string;
description?: string;
current_value?: string;
target_value?: string;
unit?: string;
status?: KpiStatus;
}
export function createKpi(mid: string, input: CreateKpiInput): Kpi {
ensureMission(mid);
if (!input.name?.trim()) throw new GovernanceError("Name is required.");
const id = kpis.nextId(mid);
const created = nowIso();
const kpi: Kpi = {
id,
mission_id: mid,
name: input.name.trim(),
description: input.description?.trim() || "",
current_value: input.current_value?.trim() || "",
target_value: input.target_value?.trim() || "",
unit: input.unit?.trim() || "",
status: input.status || "On Track",
created_at: created,
updated_at: created,
};
kpis.write(mid, kpi);
kpis.append(mid, id, "created", { detail: kpi.name });
return kpi;
}
export function updateKpi(mid: string, id: string, patch: UpdateKpiInput): Kpi {
const kpi = kpis.read(mid, id);
if (!kpi) throw new GovernanceError("KPI not found.", 404);
const next: Kpi = { ...kpi };
const prevStatus = kpi.status;
if (patch.name !== undefined) next.name = patch.name.trim();
if (patch.description !== undefined) next.description = patch.description.trim();
if (patch.current_value !== undefined) next.current_value = patch.current_value.trim();
if (patch.target_value !== undefined) next.target_value = patch.target_value.trim();
if (patch.unit !== undefined) next.unit = patch.unit.trim();
if (patch.status !== undefined) next.status = patch.status;
next.updated_at = nowIso();
kpis.write(mid, next);
kpis.append(mid, id, "updated", { detail: `${next.current_value}${next.unit ? " " + next.unit : ""}` });
if (patch.status !== undefined && patch.status !== prevStatus) {
kpis.append(mid, id, "state_changed", { previous: prevStatus, next: patch.status });
}
return next;
}
export const listKpis = (mid: string) => kpis.list(mid);
export const listKpisWithHistory = (mid: string) => kpis.listWithHistory(mid);
// --- Mission block reasons (governance, never automatic) -------------------
/**
* Reasons the mission is eligible to be Blocked: an active blocking dependency,
* or a critical unresolved risk. These are displayed to the operator; the Block
* transition itself is always manual.
*/
export function missionBlockReasons(mid: string): BlockReason[] {
const reasons: BlockReason[] = [];
for (const d of dependencies.list(mid)) {
if (d.blocking && d.status === "Active") {
reasons.push({
kind: "dependency",
id: d.id,
detail: `Active blocking dependency: ${d.type} — ${d.depends_on}`,
});
}
}
for (const r of risks.list(mid)) {
if (r.severity === "Critical" && !["Resolved", "Closed", "Accepted"].includes(r.status)) {
reasons.push({
kind: "risk",
id: r.id,
detail: `Critical unresolved risk: ${r.title}`,
});
}
}
return reasons;
}
root · /srv/aaf