Intelligence

Artifacts

Browse the repository, read documents, and manage the governance folders. Source, runtime, and infrastructure are read-only.

Repository
README.md
CONSTITUTION_COMPLIANCE_AUDIT_V1.mdREADME.md
repositories/aaf-holdings/hq01/lib/missions/objectives.ts
8.6 KB
import fs from "node:fs";
import path from "node:path";
import { missionDir, readMission } from "./store";
import type {
  CreateObjectiveInput,
  MissionProgress,
  Objective,
  ObjectiveEvent,
  ObjectiveEventType,
  ObjectiveStatus,
  ObjectiveView,
  UpdateObjectiveInput,
} from "./types";

/**
 * Objectives (PASS M2) — measurable outcomes physically owned by a mission.
 * Stored under each mission: MISSION-xxxxx/objectives/OBJECTIVE-000001/.
 *
 * Objectives are NOT work orders, assignments, or tasks, and they do NOT
 * dispatch. They organize future execution and gate mission completion.
 */

export class ObjectiveError extends Error {
  constructor(
    message: string,
    readonly status = 400,
  ) {
    super(message);
    this.name = "ObjectiveError";
  }
}

const OID_RE = /^OBJECTIVE-\d{6}$/;
/** Statuses that count toward mission progress (active set). */
const ACTIVE_STATUSES: ObjectiveStatus[] = [
  "Draft",
  "Planned",
  "Active",
  "Blocked",
  "Completed",
];
/** Statuses that block a mission from completing. */
const INCOMPLETE_STATUSES: ObjectiveStatus[] = ["Draft", "Planned", "Active", "Blocked"];

function nowIso(): string {
  return new Date().toISOString();
}

function isValidObjectiveId(id: string): boolean {
  return OID_RE.test(id);
}

function objectivesDir(missionId: string): string {
  return path.join(missionDir(missionId), "objectives");
}
function objectiveDir(missionId: string, oid: string): string {
  return path.join(objectivesDir(missionId), oid);
}
function objectiveJsonPath(missionId: string, oid: string): string {
  return path.join(objectiveDir(missionId, oid), "objective.json");
}
function objHistoryLog(missionId: string, oid: string): string {
  return path.join(objectiveDir(missionId, oid), "history", "log.jsonl");
}

function writeJsonAtomic(file: string, data: unknown): void {
  fs.mkdirSync(path.dirname(file), { recursive: true });
  const tmp = `${file}.tmp`;
  fs.writeFileSync(tmp, JSON.stringify(data, null, 2) + "\n", "utf8");
  fs.renameSync(tmp, file);
}

function writeObjective(o: Objective): void {
  writeJsonAtomic(objectiveJsonPath(o.mission_id, o.id), o);
}

function readObjective(missionId: string, oid: string): Objective | null {
  if (!isValidObjectiveId(oid)) return null;
  try {
    return JSON.parse(
      fs.readFileSync(objectiveJsonPath(missionId, oid), "utf8"),
    ) as Objective;
  } catch {
    return null;
  }
}

function appendObjEvent(
  missionId: string,
  oid: string,
  type: ObjectiveEventType,
  extra: Partial<ObjectiveEvent> = {},
): void {
  const file = objHistoryLog(missionId, oid);
  fs.mkdirSync(path.dirname(file), { recursive: true });
  const ev: ObjectiveEvent = { type, at: nowIso(), ...extra };
  fs.appendFileSync(file, JSON.stringify(ev) + "\n", "utf8");
}

function readObjHistory(missionId: string, oid: string): ObjectiveEvent[] {
  let raw: string;
  try {
    raw = fs.readFileSync(objHistoryLog(missionId, oid), "utf8");
  } catch {
    return [];
  }
  return raw
    .split("\n")
    .filter(Boolean)
    .map((l) => {
      try {
        return JSON.parse(l) as ObjectiveEvent;
      } catch {
        return null;
      }
    })
    .filter((e): e is ObjectiveEvent => Boolean(e));
}

function nextObjectiveId(missionId: string): string {
  let max = 0;
  let entries: fs.Dirent[] = [];
  try {
    entries = fs.readdirSync(objectivesDir(missionId), { withFileTypes: true });
  } catch {
    /* none yet */
  }
  for (const e of entries) {
    if (e.isDirectory() && isValidObjectiveId(e.name)) {
      const n = Number(e.name.slice("OBJECTIVE-".length));
      if (Number.isFinite(n) && n > max) max = n;
    }
  }
  return `OBJECTIVE-${String(max + 1).padStart(6, "0")}`;
}

export function listObjectives(missionId: string): Objective[] {
  let entries: fs.Dirent[];
  try {
    entries = fs.readdirSync(objectivesDir(missionId), { withFileTypes: true });
  } catch {
    return [];
  }
  return entries
    .filter((e) => e.isDirectory() && isValidObjectiveId(e.name))
    .map((e) => readObjective(missionId, e.name))
    .filter((o): o is Objective => Boolean(o))
    .sort((a, b) => a.id.localeCompare(b.id));
}

export function getObjective(missionId: string, oid: string): Objective | null {
  return readObjective(missionId, oid);
}

export function getObjectiveView(
  missionId: string,
  oid: string,
): ObjectiveView | null {
  const objective = readObjective(missionId, oid);
  if (!objective) return null;
  return {
    objective,
    history: readObjHistory(missionId, oid).sort((a, b) =>
      b.at.localeCompare(a.at),
    ),
  };
}

export function createObjective(
  missionId: string,
  input: CreateObjectiveInput,
): Objective {
  if (!readMission(missionId)) throw new ObjectiveError("Mission not found.", 404);
  const title = input.title?.trim();
  if (!title) throw new ObjectiveError("Title is required.");
  const description = input.description?.trim();
  if (!description) throw new ObjectiveError("Description is required.");
  const owner = input.owner_executive?.trim();
  if (!owner) throw new ObjectiveError("Owner executive is required.");
  if (!input.priority) throw new ObjectiveError("Priority is required.");

  const id = nextObjectiveId(missionId);
  const created = nowIso();
  const objective: Objective = {
    id,
    title,
    description,
    mission_id: missionId,
    owner_executive: owner,
    status: "Draft",
    priority: input.priority,
    success_criteria: (input.success_criteria ?? [])
      .map((s) => s.trim())
      .filter(Boolean),
    completion_percentage: 0,
    created_at: created,
    updated_at: created,
  };
  fs.mkdirSync(path.join(objectiveDir(missionId, id), "history"), {
    recursive: true,
  });
  writeObjective(objective);
  appendObjEvent(missionId, id, "objective_created", { detail: title });
  appendObjEvent(missionId, id, "executive_changed", { detail: owner });
  return objective;
}

export function updateObjective(
  missionId: string,
  oid: string,
  patch: UpdateObjectiveInput,
): Objective {
  const obj = readObjective(missionId, oid);
  if (!obj) throw new ObjectiveError("Objective not found.", 404);
  const prevOwner = obj.owner_executive;
  const next: Objective = { ...obj };
  if (patch.title !== undefined) next.title = patch.title.trim();
  if (patch.description !== undefined) next.description = patch.description.trim();
  if (patch.priority !== undefined) next.priority = patch.priority;
  if (patch.success_criteria !== undefined) {
    next.success_criteria = patch.success_criteria.map((s) => s.trim()).filter(Boolean);
  }
  if (patch.completion_percentage !== undefined) {
    next.completion_percentage = Math.max(
      0,
      Math.min(100, Math.round(patch.completion_percentage)),
    );
  }
  if (patch.owner_executive !== undefined) {
    next.owner_executive = patch.owner_executive?.trim() || null;
  }
  next.updated_at = nowIso();
  writeObjective(next);
  appendObjEvent(missionId, oid, "objective_updated");
  if (patch.owner_executive !== undefined && next.owner_executive !== prevOwner) {
    appendObjEvent(missionId, oid, "executive_changed", {
      detail: next.owner_executive ?? "(unassigned)",
    });
  }
  return next;
}

/** Change an objective's status, recording a state_changed event. */
export function setObjectiveStatus(
  missionId: string,
  oid: string,
  to: ObjectiveStatus,
  opts: { actor?: string; reason?: string } = {},
): Objective {
  const obj = readObjective(missionId, oid);
  if (!obj) throw new ObjectiveError("Objective not found.", 404);
  const from = obj.status;
  const next: Objective = { ...obj, status: to, updated_at: nowIso() };
  if (to === "Completed") next.completion_percentage = 100;
  writeObjective(next);
  appendObjEvent(missionId, oid, "state_changed", {
    previous_state: from,
    new_state: to,
    actor: opts.actor ?? "CEO",
    reason: opts.reason,
    detail: `${from} → ${to}`,
  });
  return next;
}

/** Mission progress: completed / total over the active objective set. */
export function missionProgress(missionId: string): MissionProgress {
  const objectives = listObjectives(missionId);
  const active = objectives.filter((o) => ACTIVE_STATUSES.includes(o.status));
  const completed = active.filter((o) => o.status === "Completed").length;
  const total = active.length;
  const percentage = total === 0 ? 0 : Math.round((completed / total) * 100);
  return { total, completed, percentage };
}

/**
 * Whether a mission is blocked from completing: true if any objective is still
 * in an incomplete state (Draft/Planned/Active/Blocked). A mission with no such
 * objectives is eligible (vacuously, if it has none at all).
 */
export function missionCompletionBlocked(missionId: string): boolean {
  return listObjectives(missionId).some((o) =>
    INCOMPLETE_STATUSES.includes(o.status),
  );
}

root · /srv/aaf