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/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