UNPKG

agentfootprint

Version:

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

262 lines • 12.1 kB
"use strict"; /** * containment — the prose door's notes (design § 5.2, § 5.3): what a typed citation would have * made exact, said about a prose claim against the reads it rests on. A note never withholds. * * Pattern: the reads of one entity are grouped by search (`readings.ts · readingKey`) and each * search's reads merged. Each search gets its OWN notes: how it read (`call-not-read`, * `empty-undeclared`, `clarify-open`, the rows rule), its row (`coverage-unverified`, * `declaration-overclaimed`), its gaps by facet (`coverage-gap`, `cap-reached`, `paged`, * `partial`, `gap-relevance-unknown`), its period (`period-*` where the unheld time * overlaps the claim's window), containment (`scope-wider`: a fixed qualifier or a * filter in effect the claim does not name through the declared words, no window over * a search with one, a window wider than the one read, an unbounded word), and the * sequence (`later-read-failed`, `superseded-by-write`). * * A search with no note ESTABLISHES the claim; the claim is then noted only by what the * other searches BEAR — rows that contradict it, reads that disagree, unheld time over * its window. With no establishing search, every search's notes stand. So an extra * read clears no other read's objection. * Role: integrity/ layer (L3), read by `prose.ts`. * * Words decide only through declared `said` lists; a hedge never scopes; a host-filled identity * filter is the caller's own view and is never asked of a prose claim. */ Object.defineProperty(exports, "__esModule", { value: true }); exports.entityNotes = exports.claimWordsOf = void 0; const period_js_1 = require("../../semantics/coverage/period.js"); const valueWords_js_1 = require("../../semantics/coverage/search/valueWords.js"); const range_js_1 = require("../../time/range.js"); const matcher_js_1 = require("./matcher.js"); const phrases_js_1 = require("./phrases.js"); const readings_js_1 = require("./readings.js"); /** The notes a sibling search BEARS on a claim another search established. */ const BEARING = new Set([ 'contradicted-by-rows', 'readings-disagree', 'period-not-held', 'period-partly-held', 'period-unknown', ]); function claimWordsOf(claim, text, clock, work) { return { claim, text, window: claimWindowOf(text, claim.tokens, clock), unbounded: (0, matcher_js_1.firstHit)(text, claim.tokens, phrases_js_1.NEGATIVE_CLAIM_PHRASES.unbounded) !== undefined, sourceMarker: (0, matcher_js_1.firstHit)(text, claim.tokens, phrases_js_1.NEGATIVE_CLAIM_PHRASES.source) !== undefined, work, }; } exports.claimWordsOf = claimWordsOf; const UNIT_LETTER = { minute: 'm', minutes: 'm', hour: 'h', hours: 'h', day: 'd', days: 'd', week: 'w', weeks: 'w', }; /** * The window a unit names ("in the last 2 hours") as the time layer's half-open look-back at the * turn's clock (`time/range.ts` · `lookbackRange`), or undefined when it names none or it cannot * be placed. */ function claimWindowOf(text, tokens, clock) { const hit = (0, matcher_js_1.firstHit)(text, tokens, phrases_js_1.NEGATIVE_CLAIM_PHRASES.window); if (hit === undefined) return undefined; const words = tokens.slice(hit.first, hit.last + 1).map((t) => t.text); const last = words[words.length - 1] ?? ''; const unit = UNIT_LETTER[last]; const spelled = unit === undefined ? last : `${(0, matcher_js_1.numberOf)(words[words.length - 2] ?? '') ?? 1}${unit}`; try { return (0, range_js_1.lookbackRange)(clock, spelled, 'mhdw'); } catch { return undefined; } } /** The value of a filter the claim names through the filter's declared words. */ function namedValue(words, tool, argument) { const said = tool?.search?.arguments.find((a) => a.name === argument)?.said; for (const value of said === undefined ? [] : Object.keys(said)) { if ((0, matcher_js_1.holdsAnySaid)(words.claim.tokens, (0, valueWords_js_1.valueWords)(said, value), words.work)) return value; } return undefined; } const namesFixed = (words, tool, name) => (0, matcher_js_1.holdsAnySaid)(words.claim.tokens, tool?.search?.fixed?.find((f) => f.name === name)?.said, words.work); /** Whether the source is the declared one the claim names, or a marker puts it on the lookup. */ function sourceScoped(words, items, tool) { if (words.sourceMarker) return true; const sent = items.some((item) => item.facet === 'source' && item.from === 'call'); return !sent && (0, matcher_js_1.holdsAnySaid)(words.claim.tokens, tool?.search?.source.said, words.work); } /** Whether a gap bears on the claim: on a facet it leaves open, or on a value it names. */ function bears(bearsOn, words, items, tool) { if (bearsOn === undefined) return true; if (typeof bearsOn === 'object') { // A filter gap concerns its `values` (none listed = every value). It bears when the claim // names one of them, or leaves the filter open — an open claim covers every value; it does // not bear only when the claim names a value the gap does not concern. const values = bearsOn.values; const value = namedValue(words, tool, bearsOn.filter); if (value === undefined || values === undefined) return true; return values.some((v) => (0, readings_js_1.sameValue)(v, value)); } if (bearsOn === 'source') return !sourceScoped(words, items, tool); // Missing coverage also limits absence: an unread part of the source may contain a match. return true; } /** The rows rule: rows contradict only a claim whose every named qualifier the search applied. */ function rowsContradict(words, items, window, tool) { for (const fixed of tool?.search?.fixed ?? []) { if (namesFixed(words, tool, fixed.name) && !items.some((i) => i.from === 'fixed' && i.argument === fixed.name)) return false; } for (const argument of tool?.search?.arguments ?? []) { if (argument.role !== 'filter') continue; const value = namedValue(words, tool, argument.name); if (value === undefined) continue; const applied = items.find((i) => i.facet === 'filter' && i.argument === argument.name); if (applied === undefined || applied.any === true || !(0, readings_js_1.sameValue)(applied.value, value)) return false; } if (words.window !== undefined) return window !== undefined && (0, readings_js_1.holdsRange)(words.window, window); return true; } /** The scope notes: every qualifier in effect named, a window that fits, no unbounded word. */ function scopeNotes(words, items, window, tool) { let wider = words.unbounded; for (const item of items) { if (item.facet !== 'filter' || item.any === true || item.from === 'host') continue; const argument = item.argument ?? ''; const named = item.from === 'fixed' ? namesFixed(words, tool, argument) : (0, readings_js_1.sameValue)(namedValue(words, tool, argument), item.value); if (!named) wider = true; } const hasPeriod = items.some((item) => item.facet === 'period'); if (hasPeriod && (words.window === undefined || window === undefined || !(0, readings_js_1.holdsRange)(window, words.window))) { wider = true; } return wider ? ['scope-wider'] : []; } /** The period notes: a verdict short of covered, where its unheld time meets the claim's window. */ function periodNotes(call, items, words) { const period = call.row?.period; if (period === undefined) { return items.some((item) => item.facet === 'period') ? ['period-undeclared'] : []; } let verdict; try { verdict = (0, period_js_1.periodVerdict)(period); } catch { verdict = 'unknown'; } if (verdict === 'covered') return []; const unheld = (0, readings_js_1.unheldRanges)(period); const bearing = words.window === undefined ? unheld.length > 0 : unheld.some((s) => (0, readings_js_1.meets)(s, words.window)); return bearing ? [`period-${verdict}`] : []; } /** One search's own notes, from its merged reading. */ function readingNotes(reading, words, record) { const tool = record.tools[reading.toolName]; const items = (0, readings_js_1.scopeOf)(reading) ?? []; const row = reading.row; if (reading.emptiness === 'clarify') return ['clarify-open']; if (!(0, readings_js_1.returnedReading)(reading)) return ['call-not-read']; const window = (0, readings_js_1.searchWindow)(reading); const notes = []; if (reading.emptiness === 'non-empty' && words.claim.kind === 'none') { notes.push(row !== undefined && rowsContradict(words, items, window, tool) ? 'contradicted-by-rows' : 'rows-undecided'); } if (row === undefined) { if (reading.emptiness !== 'non-empty') notes.push('empty-undeclared'); return notes; } if (row.composed !== 'local') notes.push('coverage-unverified'); if ((row.overclaim ?? []).length > 0) notes.push('declaration-overclaimed'); if (reading.emptiness === 'undeclared-empty') notes.push('empty-undeclared'); for (const gap of row.gaps) { if (gap.kind === 'prose') notes.push('gap-relevance-unknown'); else if (gap.kind === 'cap-reached' || gap.kind === 'paged' || gap.kind === 'partial' || gap.kind === 'ranking') notes.push(gap.kind); else if (gap.kind !== 'period' && bears(gap.bears, words, items, tool)) notes.push('coverage-gap'); } if (words.claim.kind === 'complete' && !row.complete) notes.push('search-incomplete'); notes.push(...scopeNotes(words, items, window, tool), ...periodNotes(reading, items, words)); return notes; } /** The sequence notes of one search: a failure of it, or a write, after its read. */ function sequenceNotes(reading, key, record, peers) { const at = peers.indexOf(reading); const notes = []; for (const other of peers) { if (other === reading || !other.dispatched) continue; if (!(peers.indexOf(other) > at || other.batch === reading.batch)) continue; const otherKey = (0, readings_js_1.readingKey)(other, record.tools[other.toolName], record.clock); const same = otherKey === undefined ? other.toolName === reading.toolName : otherKey === key; if (same && other.outcome !== 'ok') notes.push('later-read-failed'); if (!(0, readings_js_1.isRead)(other)) notes.push('superseded-by-write'); } return notes; } /** The notes of one entity's reads: establishing searches clear all but what siblings bear. */ function entityNotes(calls, words, record, peers) { const byKey = new Map(); for (const call of calls) { const key = (0, readings_js_1.readingKey)(call, record.tools[call.toolName], record.clock) ?? `\u{1}${call.resultId}`; byKey.set(key, [...(byKey.get(key) ?? []), call]); } const perKey = [...byKey.entries()].map(([key, group]) => { const { reading, disagree } = (0, readings_js_1.mergeKeyReadings)(group); if (disagree) return ['readings-disagree']; if (reading === undefined) return ['call-not-read']; return [...readingNotes(reading, words, record), ...sequenceNotes(reading, key, record, peers)]; }); const establishing = perKey.some((notes) => notes.length === 0); return establishing ? perKey.flatMap((notes) => notes.filter((note) => BEARING.has(note))) : perKey.flat(); } exports.entityNotes = entityNotes; //# sourceMappingURL=containment.js.map