import type { DerivedDefinition } from './types'; import { buildFormulaContext, evaluateFormula } from './formula'; export class CycleError extends Error { constructor(message: string) { super(message); this.name = 'CycleError'; } } /** Kahn topological sort of derived definitions by dependsOn keys (derived.* or attr.* etc.). */ export function sortDerived(defs: DerivedDefinition[]): DerivedDefinition[] { const byKey = new Map(defs.map((d) => [d.key, d])); const indeg = new Map(); const adj = new Map(); for (const d of defs) { indeg.set(d.key, 0); adj.set(d.key, []); } for (const d of defs) { for (const dep of d.dependsOn) { const depKey = dep.startsWith('derived.') ? dep.slice('derived.'.length) : null; if (depKey && byKey.has(depKey)) { adj.get(depKey)!.push(d.key); indeg.set(d.key, (indeg.get(d.key) ?? 0) + 1); } } } const queue = [...indeg.entries()].filter(([, n]) => n === 0).map(([k]) => k); const ordered: DerivedDefinition[] = []; while (queue.length) { const k = queue.shift()!; const def = byKey.get(k); if (def) ordered.push(def); for (const next of adj.get(k) ?? []) { const n = (indeg.get(next) ?? 0) - 1; indeg.set(next, n); if (n === 0) queue.push(next); } } if (ordered.length !== defs.length) { throw new CycleError('Derived definitions contain a cycle'); } return ordered; } export function computeDerived( defs: DerivedDefinition[], values: { attributes?: Record; skills?: Record; resources?: Record; }, ): Record { const ordered = sortDerived(defs); const derived: Record = {}; for (const d of ordered) { const ctx = buildFormulaContext({ ...values, derived }); derived[d.key] = evaluateFormula(d.formula, ctx, d.round ?? 'none'); } return derived; }