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/assets/ledger.ts
8.5 KB
import fs from "node:fs";
import path from "node:path";
import { getMission } from "@/lib/missions/manager";
import { getObjective } from "@/lib/missions/objectives";
import { readWorkOrder } from "@/lib/missions/work-orders";
import { readAssignment } from "@/lib/missions/assignments";
import { getReport } from "@/lib/missions/reports";
import { ASSETS_ROOT } from "./config";
import type {
  Asset,
  AssetEvent,
  AssetEventType,
  AssetFilters,
  AssetStatus,
  AssetType,
  AssetView,
  RegisterAssetInput,
  UpdateAssetInput,
} from "./types";

/**
 * The Asset Ledger manager. A global registry where every durable asset has
 * exactly one originating mission and an immutable origin lineage. Assets
 * reference files; they never duplicate them, and they outlive their mission.
 */

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

const AID_RE = /^ASSET-\d{6}$/;

function nowIso(): string {
  return new Date().toISOString();
}
function assetDir(id: string): string {
  return path.join(ASSETS_ROOT, id);
}
function assetJson(id: string): string {
  return path.join(assetDir(id), "asset.json");
}
function assetHistory(id: string): string {
  return path.join(assetDir(id), "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 append(id: string, type: AssetEventType, detail?: string): void {
  const file = assetHistory(id);
  fs.mkdirSync(path.dirname(file), { recursive: true });
  const ev: AssetEvent = { type, at: nowIso(), detail };
  fs.appendFileSync(file, JSON.stringify(ev) + "\n", "utf8");
}

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

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

export function readAsset(id: string): Asset | null {
  if (!AID_RE.test(id)) return null;
  try {
    return JSON.parse(fs.readFileSync(assetJson(id), "utf8")) as Asset;
  } catch {
    return null;
  }
}

export function listAssets(): Asset[] {
  let entries: fs.Dirent[];
  try {
    entries = fs.readdirSync(ASSETS_ROOT, { withFileTypes: true });
  } catch {
    return [];
  }
  return entries
    .filter((e) => e.isDirectory() && AID_RE.test(e.name))
    .map((e) => readAsset(e.name))
    .filter((a): a is Asset => Boolean(a))
    .sort((a, b) => b.created_at.localeCompare(a.created_at));
}

/** Search/filter the ledger by mission, type, repository, executive, tag, status. */
export function searchAssets(filters: AssetFilters): Asset[] {
  return listAssets().filter((a) => {
    if (filters.mission && a.mission_id !== filters.mission) return false;
    if (filters.type && a.type !== filters.type) return false;
    if (filters.status && a.status !== filters.status) return false;
    if (filters.executive && a.created_by_executive !== filters.executive) return false;
    if (filters.repository && !a.repository.includes(filters.repository)) return false;
    if (filters.tag && !a.tags.includes(filters.tag)) return false;
    return true;
  });
}

export function listAssetsForMission(missionId: string): Asset[] {
  return listAssets().filter((a) => a.mission_id === missionId);
}
export function listAssetsForAssignment(assignmentId: string): Asset[] {
  return listAssets().filter((a) => a.assignment_id === assignmentId);
}
export function listAssetsForReport(reportId: string): Asset[] {
  return listAssets().filter((a) => a.report_id === reportId);
}

function inferType(relativePath: string): AssetType {
  const ext = path.extname(relativePath).toLowerCase();
  if ([".pdf"].includes(ext)) return "PDF";
  if ([".md", ".txt", ".rtf", ".doc", ".docx"].includes(ext)) return "Document";
  if ([".png", ".jpg", ".jpeg", ".gif", ".svg", ".webp"].includes(ext)) return "Image";
  if ([".mp4", ".mov", ".webm", ".avi"].includes(ext)) return "Video";
  if (
    [".ts", ".tsx", ".js", ".jsx", ".py", ".go", ".rs", ".rb", ".java", ".c", ".cpp", ".sh"].includes(ext)
  ) {
    return "Code";
  }
  if ([".json", ".yaml", ".yml", ".toml", ".ini", ".env"].includes(ext)) return "Configuration";
  return "Other";
}

/**
 * Register a referenced artifact from a report into the ledger. The origin
 * lineage is taken from the report and is immutable. Idempotent: an artifact
 * already registered for the same report is returned, never duplicated.
 */
export function registerAssetFromReport(input: RegisterAssetInput): Asset {
  const report = getReport(input.mission_id, input.assignment_id, input.report_id);
  if (!report) throw new AssetError("Report not found for this asset.", 404);
  const relPath = input.relative_path?.trim();
  if (!relPath) throw new AssetError("A relative_path is required.", 400);

  const existing = listAssets().find(
    (a) => a.report_id === report.report_id && a.relative_path === relPath,
  );
  if (existing) return existing;

  const id = nextAssetId();
  const asset: Asset = {
    asset_id: id,
    name: input.name?.trim() || path.basename(relPath),
    type: input.type || inferType(relPath),
    mission_id: report.mission_id,
    objective_id: report.objective_id,
    work_order_id: report.work_order_id,
    assignment_id: report.assignment_id,
    report_id: report.report_id,
    repository: report.repository,
    relative_path: relPath,
    created_at: nowIso(),
    created_by_executive: report.executive,
    status: "Active",
    tags: (input.tags ?? []).map((t) => t.trim()).filter(Boolean),
    description: input.description?.trim() || "",
  };
  // Never overwrite an existing asset.
  fs.mkdirSync(path.join(assetDir(id), "history"), { recursive: true });
  writeJsonAtomic(assetJson(id), asset);
  append(id, "asset_registered", `${relPath} from ${report.report_id}`);
  return asset;
}

/** Update mutable metadata only — origin lineage can never change. */
export function updateAsset(id: string, patch: UpdateAssetInput): Asset {
  const asset = readAsset(id);
  if (!asset) throw new AssetError("Asset not found.", 404);
  const prevStatus = asset.status;
  const next: Asset = { ...asset };
  if (patch.name !== undefined) next.name = patch.name.trim() || asset.name;
  if (patch.type !== undefined) next.type = patch.type;
  if (patch.description !== undefined) next.description = patch.description.trim();
  if (patch.tags !== undefined) next.tags = patch.tags.map((t) => t.trim()).filter(Boolean);
  if (patch.status !== undefined) next.status = patch.status;
  writeJsonAtomic(assetJson(id), next);
  append(id, "asset_updated");
  if (patch.status !== undefined && patch.status !== prevStatus) {
    append(id, "state_changed", `${prevStatus} → ${patch.status}`);
  }
  return next;
}

export function getAssetView(id: string): AssetView | null {
  const asset = readAsset(id);
  if (!asset) return null;
  return {
    asset,
    history: readHistory(id).sort((a, b) => b.at.localeCompare(a.at)),
    mission: getMission(asset.mission_id),
    objective: getObjective(asset.mission_id, asset.objective_id),
    work_order: readWorkOrder(asset.mission_id, asset.work_order_id),
    assignment: readAssignment(asset.mission_id, asset.assignment_id),
    report: getReport(asset.mission_id, asset.assignment_id, asset.report_id),
  };
}

/** All distinct values for the filter dropdowns. */
export function assetFacets(): {
  types: string[];
  repositories: string[];
  executives: string[];
  statuses: string[];
  tags: string[];
} {
  const all = listAssets();
  const uniq = (xs: string[]) => [...new Set(xs.filter(Boolean))].sort();
  return {
    types: uniq(all.map((a) => a.type)),
    repositories: uniq(all.map((a) => a.repository)),
    executives: uniq(all.map((a) => a.created_by_executive)),
    statuses: uniq(all.map((a) => a.status)),
    tags: uniq(all.flatMap((a) => a.tags)),
  };
}

root · /srv/aaf