UNPKG

agentfootprint

Version:

The explainable agent framework — backtrack a wrong answer to the exact context that caused it (evidence, not guesses). Built on footprintjs.

210 lines • 10.9 kB
"use strict"; /** * runbook/verdicts — the OPTIONAL verdict/rowset projection. * * Pattern: pure selection + rendering over the run's final state, plus one * tiny recorder that harvests decide() evidence for generated * meanings. Selected by `resultKind: 'verdict/*'`; a runbook of any * other kind never sees this module. * Role: core/runbook. * Emits: N/A. * * THE ONE-NUMBER LAW: the rendered table and the structured `verdicts` list * show the SAME rows under the SAME cap. A rendered table beside a longer * structured list is an invitation that gets accepted — rows come back * retyped with subtly wrong identifiers. * * TWO SURFACES, TWO LAWS: a rowset does not always reach its reader through * the model's prose. Where it does, the table ships pre-rendered and the model * is told to output it verbatim (`VERDICT_RENDER_NOTE`); where the HOST draws * the rowset itself, no table ships and the model is told the opposite * (`PANEL_RENDER_NOTE`). The bridge cannot tell which client it is in, so the * caller says — `presentation` on the options bag. * * GENERATED MEANINGS, never hand-restated: `verdict_meanings` is composed * from (a) the named decider's declared branches in the chart's own structure * (branch description, falling back to branch name), overlaid by (b) the rule * LABELS this run's decide() evidence carried — the rule speaking for itself — * and (c) the DEFAULT branch's declared label, carried by the same evidence. * A decider inside a dynamically generated fan-out branch is invisible to (a) * by construction (the branch chart does not exist at build time); (b) still * covers every verdict an executed rule produced. * * (c) exists because the default branch is chosen by NO rule — it fires * exactly when every rule failed, so it appears in no `rules[]` entry and (b) * can never name it. In a generated fan-out branch, where (a) is blind too, it * was the one verdict the rowset could show and the meanings map could not * explain. `decide(scope, rules, { branch, label })` (footprintjs ≥ 9.16.1) puts * that label on the decision evidence, so it arrives here the way every other * label does: harvested from the run, never declared at this boundary. There is * deliberately no caller-supplied meanings map — a map a caller can write is a * map that can describe rules that never ran. */ Object.defineProperty(exports, "__esModule", { value: true }); exports.composeMeanings = exports.meaningsRecorder = exports.resolveDecider = exports.renderVerdictTable = exports.verdictRowsOf = exports.DECLINED_VERDICT = exports.PANEL_RENDER_NOTE = exports.VERDICT_RENDER_NOTE = exports.DEFAULT_MAX_ROWS = void 0; /** Default cap on `verdicts` rows and the rendered table. */ exports.DEFAULT_MAX_ROWS = 50; /** The render law when PROSE is the rowset's only surface (`presentation: * 'prose'`, the default) — stated to the model beside the table. */ exports.VERDICT_RENDER_NOTE = 'table is PRE-RENDERED over the same rows as `verdicts` — output it VERBATIM. Never ' + 'retype an identifier from `verdicts`; a transcribed name that looks right and matches ' + 'nothing is the failure this note exists to stop.'; /** * The render law when the HOST renders the rowset (`presentation: 'panel'`) — * stated to the model INSTEAD of a table, because there is none to ship. * * Same failure, opposite instruction. A rowset the reader can already see does * not need retyping into prose; retyping it is how an identifier arrives * subtly wrong beside a correct one on screen. */ exports.PANEL_RENDER_NOTE = "the rows in `verdicts` are ALREADY on the reader's screen — this host renders the " + 'rowset itself, so no table is shipped here. Do NOT reproduce those rows in prose in ' + 'any form: not as a table, not as bullets, not as one sentence per row. When a finding ' + 'names a row, quote the evidence sentence that row carries VERBATIM, and cite only the ' + 'values the finding rests on, copied byte-for-byte. A retyped identifier that looks ' + 'right and matches nothing is the failure this note exists to stop.'; /** The reserved verdict word for "no classification was reached" — rows * carrying it are counted into the coverage ledger as not-checked ground * (the three-outcome honesty: reached, reached-and-clear, DECLINED). */ exports.DECLINED_VERDICT = 'declined'; /** Read the rowset off the final state's `verdicts` key — an array of bags * each carrying a string `verdict`. Anything else reads as "no rowset". */ function verdictRowsOf(state) { const raw = state.verdicts; if (!Array.isArray(raw)) return []; return raw.filter((row) => row !== null && typeof row === 'object' && !Array.isArray(row) && typeof row.verdict === 'string'); } exports.verdictRowsOf = verdictRowsOf; /** The one place a `—` is written for a value the source did not report — a * blank cell reads as a zero; a dash reads as "not reported". * Backslashes are escaped too, BEFORE pipes, so markdown shows a value's * backslashes as written: unescaped, `a\_b` renders as `a_b` — an identifier * the model was told to copy verbatim, changed — and a `\` just before a `|` * comes out as `\\|`, which a parser that pairs backslashes splits as a new * column. */ function cell(value) { if (value === null || value === undefined || value === '') return '—'; if (value === true) return 'yes'; if (value === false) return 'no'; return String(value).replace(/\\/g, '\\\\').replace(/\|/g, '\\|').replace(/\n/g, ' '); } // LENS · tool-result · persistent-history // reads: the verdict rows ← the runbook walk projection (walk.ts) and the folded coverage ledger (coverage.ts) // law: may omit, never deny; every clause anchored to the call it was composed on. /** * Render the shown rows as one markdown table. Columns are the FIRST row's * own keys in declaration order — the chart writes its rows, so the chart * owns the column vocabulary; the bridge only renders it. */ function renderVerdictTable(rows) { if (rows.length === 0) return 'No rows in the projected set.'; // eslint-disable-next-line @typescript-eslint/no-non-null-assertion const columns = Object.keys(rows[0]); const head = `| ${columns.join(' | ')} |\n|${columns.map(() => '---').join('|')}|\n`; return (head + rows.map((row) => `| ${columns.map((column) => cell(row[column])).join(' | ')} |`).join('\n')); } exports.renderVerdictTable = renderVerdictTable; /** * Find the named decider in the chart's static structure (root graph plus * statically declared subflows) and read its branches. Loop-ref stubs are * skipped FIRST — they deliberately violate node-id uniqueness. */ function resolveDecider(chart, decider) { const spellings = new Set([decider]); const declared = new Map(); const visited = new Set(); const walk = (node) => { if (node === undefined || node.isLoopRef === true || visited.has(node)) return; visited.add(node); if ((node.id === decider || node.name === decider) && (node.children?.length ?? 0) > 0) { if (node.id !== undefined) spellings.add(node.id); if (node.name !== undefined) spellings.add(node.name); for (const child of node.children ?? []) { const branch = child.branchId ?? child.id; if (branch === undefined) continue; const meaning = child.description ?? child.name; if (meaning !== undefined && !declared.has(branch)) declared.set(branch, meaning); } } for (const child of node.children ?? []) walk(child); walk(node.next); }; walk(chart.root); for (const subflow of Object.values(chart.subflows ?? {})) { walk(subflow.root); } return { spellings, declared }; } exports.resolveDecider = resolveDecider; /** A label is a meaning only when it says something — a blank one is recorded * as no meaning at all, never as a verdict that means the empty string. */ function meaningful(label) { return typeof label === 'string' && label.length > 0; } /** * A tiny flow recorder capturing the named decider's decide() evidence as it * fires — collected DURING the traversal, never reconstructed after. Rule * labels are harvested for every rule the evidence lists (matched or not): * a rule that was evaluated has spoken its label, whichever branch won. The * DEFAULT branch's label rides the same evidence and is harvested with them. * * A decider inside a subflow (including a generated fan-out branch) reports * itself PATH-PREFIXED (`per-subject~0/Protection posture`), so the match is * on the LAST `/`-segment — the same last-delimiter reading every upstream * parser of these paths uses, which is what keeps the generated-branch * marker opaque here. */ function meaningsRecorder(identity) { const observed = new Map(); const lastSegment = (path) => path.slice(path.lastIndexOf('/') + 1); const recorder = { id: 'runbook-verdict-meanings', onDecision(event) { if (!identity.spellings.has(lastSegment(event.decider))) return; const evidence = event.evidence; // The default first, the rules over it. The default is the branch NO // rule chose, so it is normally the only speaker for its own name; where // a rule also routes to it, the rule is the sharper sentence and wins. if (typeof evidence?.default === 'string' && meaningful(evidence.defaultLabel)) { observed.set(evidence.default, evidence.defaultLabel); } for (const rule of evidence?.rules ?? []) { if (typeof rule.branch === 'string' && meaningful(rule.label)) { observed.set(rule.branch, rule.label); } } }, }; return { recorder, observed }; } exports.meaningsRecorder = meaningsRecorder; /** Compose the final meanings: declared branches first, the labels this run * observed (rule labels, and the default branch's own) winning where both * speak — the declaration made beside the rules is the sharper sentence. * Undefined when neither source produced anything — absent, never `{}`. */ function composeMeanings(identity, observed) { const meanings = {}; for (const [branch, meaning] of identity.declared) meanings[branch] = meaning; for (const [branch, label] of observed) meanings[branch] = label; return Object.keys(meanings).length > 0 ? meanings : undefined; } exports.composeMeanings = composeMeanings; //# sourceMappingURL=verdicts.js.map