Files
AetherForge/server/web/src/help/pathTracerTimeline.ts
AetherForge d605ef4adb
Some checks failed
CI Docker Mining Proof / Linux agent hashrate proof (push) Has been cancelled
Add Strain Hospice for graceful low-win strain retirement.
Archive failed epidemiology strains to museum hospice with SQLite persistence, operator/AI/court triggers, breeding and graft guards, topology museum nodes, and Seer plus oath ledger accountability.
2026-06-07 09:26:39 -07:00

130 lines
3.5 KiB
TypeScript

/**
* Onion timeline fork/merge helpers for Path Tracer UI + Seer/AI context.
*/
import { spreadStrainFromJoinLane } from './fleetTopologyEpidemiology';
export type TimelineBranchStatus =
| 'canonical'
| 'running'
| 'won'
| 'lost'
| 'merged'
| 'failed'
| 'aborted';
export interface PathTraceTimelineBranch {
id: string;
parent_id?: string;
fork_hop_index: number;
target_agent_id?: string;
target_agent_name?: string;
persona?: string;
spread_lanes?: string[];
status: TimelineBranchStatus;
mining_linked?: boolean;
hashrate?: number;
active_tier?: string;
error?: string;
is_ghost: boolean;
created_at?: string;
merged_at?: string;
}
export interface PathTraceTimelineWS {
session_id: string;
event: string;
branch?: PathTraceTimelineBranch;
branches?: PathTraceTimelineBranch[];
mermaid?: string;
target_agent_id?: string;
}
export function branchStatusLabel(status: TimelineBranchStatus): string {
switch (status) {
case 'canonical':
return 'canonical';
case 'running':
return 'exploring';
case 'won':
return 'mining linked';
case 'merged':
return 'merged';
case 'lost':
return 'lost';
case 'failed':
return 'failed';
case 'aborted':
return 'aborted';
default:
return status;
}
}
export function branchStatusClass(status: TimelineBranchStatus): string {
switch (status) {
case 'won':
case 'merged':
return 'won';
case 'running':
return 'running';
case 'failed':
case 'lost':
case 'aborted':
return 'failed';
case 'canonical':
return 'canonical';
default:
return 'ghost';
}
}
/** Ghost branches grouped under fork hop index for tree rendering. */
export function ghostBranchesByHop(branches: PathTraceTimelineBranch[]): Map<number, PathTraceTimelineBranch[]> {
const map = new Map<number, PathTraceTimelineBranch[]>();
for (const b of branches) {
if (!b.is_ghost) continue;
const list = map.get(b.fork_hop_index) ?? [];
list.push(b);
map.set(b.fork_hop_index, list);
}
return map;
}
function branchPrimaryLane(branch: PathTraceTimelineBranch): string {
return branch.spread_lanes?.[0]?.trim() ?? '';
}
/** Spread strain from join lane — mirrors Go SpreadStrainFromJoinLane for hospice guards. */
export function spreadStrainFromPersonaLane(lane: string): string {
if (!lane) return '';
return spreadStrainFromJoinLane(lane).toLowerCase();
}
export function pickMergeCandidate(
branches: PathTraceTimelineBranch[],
hospiceStrains?: Set<string>,
): PathTraceTimelineBranch | null {
const hospice = hospiceStrains ?? new Set<string>();
const eligible = (b: PathTraceTimelineBranch) => {
const lane = branchPrimaryLane(b);
if (!lane || hospice.size === 0) return true;
const strain = spreadStrainFromPersonaLane(lane);
return strain === '' || !hospice.has(strain);
};
const ghosts = branches.filter((b) => b.is_ghost && eligible(b));
const won = ghosts.find((b) => b.status === 'won' || b.mining_linked);
if (won) return won;
const running = ghosts.find((b) => b.status === 'running');
return running ?? null;
}
export function mergeMermaidStyles(mermaid: string): string {
const styles = `classDef won fill:#0a3d2e,stroke:#00ffaa,color:#00ffaa
classDef running fill:#3d320a,stroke:#ffc800,color:#ffc800
classDef failed fill:#3d0a0a,stroke:#ff5050,color:#ff5050
classDef ghost fill:#1a1a2e,stroke:#888,color:#ccc`;
if (mermaid.includes('classDef won')) return mermaid;
return `${mermaid.trim()}\n${styles}`;
}