import type { Decision } from "./parse"; /** * Render a model identifier for display, substituting `?` for an empty value. */ function model(name: string): string { return name.length > 0 ? name : "?"; } /** * Format a score to two decimals, matching the gateway header precision. */ function formatScore(value: number): string { return value.toFixed(2); } /** * Collect the parenthesized flag tags present on a decision, in a stable * precedence: offline, cached, decision-only. Used by every branch in * {@link formatLine}; each branch filters out whichever tags are already * encoded in its own arrow/label before appending the rest. */ function flagTags(decision: Decision): string[] { const out: string[] = []; if (decision.offline) out.push("offline"); if (decision.cache === "hit") out.push("cached"); if (decision.decisionOnly) out.push("decision-only"); return out; } /** * Join tags into a trailing ` (a) (b)` suffix, or an empty string when none. */ function suffix(tags: string[]): string { return tags.map((t) => ` (${t})`).join(""); } /** * Format a {@link Decision} into the adaptive one-line footer string. * * Normal (served equals chose): `wf {mode} -> {chose}` with an optional * ` . {score}` and flag tags. Failover: `wf {chose} -> {served} (failover)`. * Offline reroute (served differs, no failover): `wf {mode} -> {served} (offline)`. * * @param decision - The routing decision to render. * @returns A compact one-line string for `ctx.ui.setStatus`. */ export function formatLine(decision: Decision): string { const substituted = decision.servedBy !== decision.chose; if (substituted && decision.failover) { // Extra tags after (failover): a cache hit is mutually exclusive with a // live failover, so only `cached` is excluded; offline may co-occur. const extra = suffix(flagTags(decision).filter((t) => t !== "cached")); return `wf ${model(decision.chose)} โ†’ ${model(decision.servedBy)} (failover)${extra}`; } if (substituted) { // Offline reroute: mode -> served. offline is encoded in the label already; // cached is excluded because a cache hit does not co-occur with a live // reroute (mirroring the failover branch above). const extra = suffix(flagTags(decision).filter((t) => t !== "offline" && t !== "cached")); return `wf ${decision.mode} โ†’ ${model(decision.servedBy)} (offline)${extra}`; } const scorePart = decision.score !== undefined ? ` ยท ${formatScore(decision.score)}` : ""; return `wf ${decision.mode} โ†’ ${model(decision.chose)}${scorePart}${suffix(flagTags(decision))}`; } /** * Format a {@link Decision} into the multi-line detail shown by `/wf`, one * string per line, built entirely from the captured headers (no extra HTTP). * * @param decision - The routing decision to render. * @returns One string per line, ready to join with newlines. */ export function formatDetail(decision: Decision): string[] { const flags: string[] = []; if (decision.offline) flags.push("offline"); if (decision.failover) flags.push("failover"); if (decision.cache !== undefined) flags.push(`cache:${decision.cache}`); if (decision.decisionOnly) flags.push("decision-only"); return [ "wayfinder - last decision", `mode: ${decision.mode}`, `chose: ${model(decision.chose)}`, `served by: ${model(decision.servedBy)}`, `score: ${decision.score !== undefined ? formatScore(decision.score) : "(n/a)"}`, `request-id: ${decision.requestId ?? "(n/a)"}`, `flags: ${flags.length > 0 ? flags.join(", ") : "(none)"}`, ]; }