UNPKG

agentfootprint

Version:

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

191 lines • 8.05 kB
"use strict"; /** * ontology/score — did an answer name the declared gap, and where the need * would be met? Scored off the record, by declared strings, with no judge. * * Pattern: pure functions over the served spec and one turn's own facts * (the answer text, the tool names called, the library's own * unsupported-values verdict). No model, no clock, no I/O. * Role: Lens, for a BENCH. It is the measuring stick the design page * promised (docs/design/2026-09-ontology.md § Measured): the first * number for "the answer names the source" was a regex over prose; * this is the rule that replaces it, so every wording change is * measured the same way. Exported on the `agentfootprint/ontology` * door because the map's consumer runs the bench on ITS host with * ITS questions, and the rules must be the library's, not re-derived * there. * Emits: N/A. * * THE LAW OF THE SCORE * A check matches DECLARED STRINGS ONLY — a term's or a source's id and * aliases, a tool's name — whole-word, case-insensitive, an `_` in an * id standing for a space or a hyphen, a plural `s` allowed. Nothing fuzzier. So an answer that * says "UCS Manager" when the source's id is `influx_ucs` and no alias says * "UCS Manager" scores NO, and that is the point: the declaration must * carry the words people use, because those are the words the model * reads too. The scorer never decides an answer is good; it counts what * the answer names. * * THE EXPECTATION IS THE BENCH AUTHOR'S, DECLARED BEFORE THE RUN * `expected.gap` names the term or source the question turns on — the * `_findings.expect` precedent: declared before the call, judged after. A * gap that names nothing the spec declares is refused, naming it: a bench * whose oracle lies must fail loudly, as a map naming an unregistered tool * does at build. */ Object.defineProperty(exports, "__esModule", { value: true }); exports.summarizeAbsence = exports.scoreAbsence = void 0; /** * The header's own words, and the ask's: what the piece calls itself and * what the person should not hear. `ontology` and its forms; `map` exact * (with its plural) — `mapping` as a verb is a judgment this list does not * make. NOT `declar…` (9.109.1): a host's own answer footer says "declared * by the tools that produced it", so that stem counted the host's boilerplate * as the model citing the map — a false positive the first scored arm showed. */ const MAP_WORDS = [/\bontolog\w*/gi, /\bmaps?\b/gi]; /** * Score one turn against the served map and the bench's expectation. * Throws when `expected.gap` names nothing the spec declares. */ function scoreAbsence(spec, turn, expected = {}) { const counts = { toolCalls: turn.toolCalls.length, unsupportedValues: turn.unsupportedValues, mapWords: wordsOf(turn.answer), }; if (expected.gap === undefined) { return { gap: undefined, gapKind: 'none', namedGap: undefined, namedWhere: undefined, where: [], neighbours: [], ...counts, }; } const gap = expected.gap; const kind = kindOf(spec, gap); if (kind === undefined) { throw new Error(`scoreAbsence: expected gap '${gap}' is neither a term nor a source of ontology ` + `'${spec.id}' — the expectation must name what the map declares.`); } const names = namesOf(spec, gap); const neighbours = neighboursOf(spec, gap, kind); const where = neighbours.filter((id) => mentions(turn.answer, namesOf(spec, id))); return { gap, gapKind: kind, namedGap: mentions(turn.answer, names), namedWhere: neighbours.length === 0 ? undefined : where.length > 0, where, neighbours, ...counts, }; } exports.scoreAbsence = scoreAbsence; /** The tally: `k` of `n` per yes/no check over the turns that had one; counts summed. */ function summarizeAbsence(scores) { const tally = (pick) => { const asked = scores.filter((s) => pick(s) !== undefined); return { k: asked.filter((s) => pick(s) === true).length, n: asked.length }; }; const withVerdict = scores.filter((s) => s.unsupportedValues !== undefined); return { turns: scores.length, namedGap: tally((s) => s.namedGap), namedWhere: tally((s) => s.namedWhere), citedMap: tally((s) => s.mapWords.length > 0), toolCalls: scores.reduce((n, s) => n + s.toolCalls, 0), unsupportedValues: withVerdict.length === 0 ? undefined : withVerdict.reduce((n, s) => n + (s.unsupportedValues ?? 0), 0), }; } exports.summarizeAbsence = summarizeAbsence; // ─── The declared neighbourhood ──────────────────────────────────────── function kindOf(spec, id) { if (Object.prototype.hasOwnProperty.call(spec.nodes, id)) return 'term'; if (Object.prototype.hasOwnProperty.call(spec.sources, id)) return 'source'; return undefined; } /** * Every declared id one step from the gap, in declaration order, no repeats: * for a term — the sources holding it, the tools reading it, the terms at * either end of a relation with it; for a source — the terms it holds and * the tools reading through it. Tools are ids here too: a tool name is a * declared string the answer can quote. */ function neighboursOf(spec, gap, kind) { const out = []; const add = (id) => { if (id !== gap && !out.includes(id)) out.push(id); }; if (kind === 'term') { for (const held of spec.nodes[gap]?.sources ?? []) { add(held.source); for (const tool of held.via ?? []) add(tool); } for (const edge of spec.edges ?? []) { if (edge.from === gap) add(edge.to); if (edge.to === gap) add(edge.from); } } else { for (const [id, node] of Object.entries(spec.nodes)) { for (const held of node.sources ?? []) { if (held.source !== gap) continue; add(id); for (const tool of held.via ?? []) add(tool); } } } return out; } /** The strings that name one declared id: a term's or a source's id and aliases; a tool's bare name. */ function namesOf(spec, id) { const has = (o) => Object.prototype.hasOwnProperty.call(o, id); const declared = has(spec.nodes) ? spec.nodes[id] : has(spec.sources) ? spec.sources[id] : undefined; return declared === undefined ? [id] : [id, ...(declared.aliases ?? [])]; } // ─── Matching — declared strings, whole word, nothing fuzzier ───────── function mentions(answer, names) { return names.some((name) => patternOf(name).test(answer)); } /** * One declared string as a whole-word, case-insensitive pattern; an `_` in * the id stands for a space, a hyphen or itself, so `vmkernel_log` meets * "vmkernel log" and `esxi_host` meets "ESXi host"; a trailing `s` is * allowed (9.109.1), so "vmkernel logs" and "change records" meet their * ids — a plural is a form of the declared word, not another word. * Everything else is escaped: a declared string is matched as itself. */ function patternOf(name) { const escaped = name.replace(/[.*+?^${}()|[\]\\]/g, '\\$&').replace(/_/g, '[ _-]'); return new RegExp(`(?<![\\w])${escaped}s?(?![\\w])`, 'i'); } function wordsOf(answer) { const found = []; for (const re of MAP_WORDS) { for (const m of answer.matchAll(re)) { const word = m[0].toLowerCase(); if (!found.includes(word)) found.push(word); } } return found; } //# sourceMappingURL=score.js.map