tree-result.ts (2828B)
1 import type { GraphPreview } from './graph.js'; 2 import type { Bundle } from './bundle.js'; 3 import { exportTree, type TreeFormat, type TreeFile } from './tree-export.js'; 4 export const RESULT_NODE_LIMIT = 100_000; 5 export type TreeResultFormat = 'tricu' | 'triangle' | 'dag' | 'ternary'; 6 export function isTreeResult(value: string): value is TreeResultFormat { 7 return ['tricu', 'triangle', 'dag', 'ternary'].includes(value); 8 } 9 // Backend snapshots describe physical sharing, not structural hash-consing. 10 // Reject incomplete/cyclic observations instead of exporting a plausible prefix. 11 export function snapshotBundle(snapshot: GraphPreview): Bundle { 12 if (snapshot.truncated) throw new Error('Result snapshot exceeds 100,000 nodes; refusing a partial export'); 13 const aliases = new Map<number, number>(snapshot.aliases.map(a => [a.from, a.to])); 14 const byId = new Map<number, GraphPreview['nodes'][number]>(snapshot.nodes.map(n => [n.id, n])); 15 const resolve = (raw: number): number => { 16 let id = raw; const seen = new Set<number>(); 17 while (aliases.has(id)) { if (seen.has(id)) throw new Error('Snapshot alias cycle'); seen.add(id); id = aliases.get(id)!; } 18 return id; 19 }; 20 const nodes: Bundle['nodes'] = [], mapped = new Map<number, number>(), active = new Set<number>(); 21 const root = resolve(snapshot.root), stack: Array<[number, boolean]> = [[root, false]]; 22 while (stack.length) { 23 const [raw, leave] = stack.pop()!, id = resolve(raw); 24 if (mapped.has(id)) continue; 25 const node = byId.get(id); if (!node) throw new Error('Incomplete result snapshot'); 26 const arity = { leaf: 0, stem: 1, fork: 2 }[node.kind]; 27 if (arity === undefined || node.children.length !== arity) throw new Error('Invalid snapshot constructor'); 28 if (!leave) { 29 if (active.has(id)) throw new Error('Cyclic result cannot be exported as a DAG'); 30 active.add(id); stack.push([id, true]); 31 for (const child of [...node.children].reverse()) stack.push([child, false]); 32 } else { 33 active.delete(id); const cs = node.children.map(c => mapped.get(resolve(c))!); 34 if (cs.some(c => c === undefined)) throw new Error('Invalid result snapshot order'); 35 mapped.set(id, nodes.length); 36 nodes.push(node.kind === 'leaf' ? { kind: 'leaf' } : node.kind === 'stem' ? { kind: 'stem', child: cs[0]! } : { kind: 'fork', left: cs[0]!, right: cs[1]! }); 37 } 38 } 39 return { nodes, roots: [mapped.get(root)!], exports: [{ name: 'tree', root: mapped.get(root)!, kind: 'term', abi: 'arboricx.abi.tree.v1' }], metadata: {} }; 40 } 41 export function treeResult(snapshot: GraphPreview, format: TreeResultFormat): { type: TreeResultFormat; value: string; file: TreeFile } { 42 const file = exportTree(snapshotBundle(snapshot), 'tree', format as TreeFormat); 43 return { type: format, value: file.text, file }; 44 }