UNPKG

agentfootprint

Version:

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

179 lines • 7.93 kB
"use strict"; /** * matcher — the prose grammar's machinery: a reply's tokens with their offsets, and the phrase * matcher over them. It owns NO spelling rule: every token is spelled by the library's one * spelling leaf (`lib/normalize.ts` · `foldCase`), and names compare by its `sameName`. * * Pattern: pure functions, linear in the text. A token is a run of letters, digits and marks, * with `- _ . / : @ #` kept INSIDE an identifier (`host-07.dc1.example.com`, `eth1/3`) * and an apostrophe kept inside a word (`doesn't`); every step of the one tokenizer * regex consumes a character, so no input backtracks. Spans are offsets into the text * as given; only each token's own text is spelled. * Role: integrity/ layer (L3), read by `units.ts`, `detect.ts`, `language.ts`, * `resolve.ts` and `containment.ts`. */ Object.defineProperty(exports, "__esModule", { value: true }); exports.allHits = exports.firstHit = exports.matchPhrase = exports.numberOf = exports.nameAt = exports.holdsName = exports.holdsAnySaid = exports.findWords = exports.holdsWords = exports.spell = exports.tokensOf = exports.spellToken = void 0; const normalize_js_1 = require("../../lib/normalize.js"); const TOKEN = /[\p{L}\p{N}\p{M}](?:[\p{L}\p{N}\p{M}]|['\u{2019}](?=\p{L})|[-_./:@#](?=[\p{L}\p{N}]))*/gu; /** One token as the grammar compares it: the leaf's case fold, a curly apostrophe straightened. */ function spellToken(raw) { return (0, normalize_js_1.foldCase)(raw.normalize('NFC')).replace(/\u{2019}/gu, "'"); } exports.spellToken = spellToken; /** The tokens of `text[from, to)`, with offsets into `text`. */ function tokensOf(text, from = 0, to = text.length) { const out = []; const slice = text.slice(from, to); for (const m of slice.matchAll(TOKEN)) { const start = from + (m.index ?? 0); out.push({ text: spellToken(m[0]), start, end: start + m[0].length }); } return out; } exports.tokensOf = tokensOf; /** A phrase's or a said word's tokens, as text. */ function spell(phrase, work) { work?.charge(phrase.length + 1); return tokensOf(phrase).map((t) => t.text); } exports.spell = spell; /** Whether `tokens` hold `words` as consecutive tokens, anywhere. */ function holdsWords(tokens, words, work) { return findWords(tokens, words, 0, work) !== -1; } exports.holdsWords = holdsWords; /** The first index at which `tokens` hold `words` consecutively, or -1. */ function findWords(tokens, words, from = 0, work) { if (words.length === 0) return -1; outer: for (let i = from; i + words.length <= tokens.length; i += 1) { for (let j = 0; j < words.length; j += 1) { work?.charge(1); if (tokens[i + j]?.text !== words[j]) continue outer; } return i; } return -1; } exports.findWords = findWords; /** Whether any of the said phrases is held by `tokens`. */ function holdsAnySaid(tokens, said, work) { return (said ?? []).some((phrase) => holdsWords(tokens, spell(phrase, work), work)); } exports.holdsAnySaid = holdsAnySaid; /** Whether the unit holds `name` as one of its token runs, by the leaf's name rule (`sameName`). */ function holdsName(text, tokens, name, work) { return nameAt(text, tokens, name, 0, work) !== -1; } exports.holdsName = holdsName; /** The first token index at which the unit holds `name` (by `sameName`), from `from`; or -1. */ function nameAt(text, tokens, name, from, work) { const words = spell(name, work); for (let at = findWords(tokens, words, from, work); at !== -1; at = findWords(tokens, words, at + 1, work)) { const first = tokens[at]; const last = tokens[at + words.length - 1]; work?.charge(last.end - first.start + name.length + 1); if ((0, normalize_js_1.sameName)(text.slice(first.start, last.end), name)) return at; } return -1; } exports.nameAt = nameAt; // ─── the phrase matcher ────────────────────────────────────────────────── const NUMBER_WORDS = new Set('one two three four five six seven eight nine ten eleven twelve thirteen fourteen fifteen sixteen seventeen eighteen nineteen twenty'.split(' ')); /** A number word's value, or a digit string's. */ function numberOf(token) { if (/^\d{1,6}$/.test(token)) return Number(token); const words = [...NUMBER_WORDS]; const at = words.indexOf(token); return at === -1 ? undefined : at + 1; } exports.numberOf = numberOf; const startsWithLetter = (token) => /^\p{L}/u.test(token); function compileSlot(raw) { const optional = raw.endsWith('?') && raw.length > 1; const body = optional ? raw.slice(0, -1) : raw; if (body === '<word>') return { optional, test: startsWithLetter }; if (body === '<num>') return { optional, test: (t) => numberOf(t) !== undefined }; if (body === '<dur>') return { optional, test: (t) => /^\d{1,6}[mhdw]$/.test(t) }; if (body.startsWith('!')) { const except = new Set(body.slice(1).split('|')); return { optional, test: (t) => startsWithLetter(t) && !except.has(t) }; } const options = new Set(body.split('|')); return { optional, test: (t) => options.has(t) }; } const COMPILED = new WeakMap(); function compiled(phrase) { const known = COMPILED.get(phrase); if (known !== undefined) return known; const anchored = phrase.literal !== true && phrase.words[0] === '^'; const words = anchored ? phrase.words.slice(1) : phrase.words; const made = { anchored, slots: words.map((word) => phrase.literal === true ? { optional: false, test: (token) => token === word } : compileSlot(word)), }; COMPILED.set(phrase, made); return made; } /** Only spaces between two matched tokens — a comma or a full stop breaks a phrase. */ const spaced = (text, left, right) => /^[ \t\u{A0}]*$/u.test(text.slice(left.end, right.start)); /** The token index after a match of `slots` from `slot`/`at`, or -1. Optional slots fork. */ function matchFrom(text, tokens, slots, slot, at, previous) { if (slot === slots.length) return at; const s = slots[slot]; const token = tokens[at]; if (token !== undefined && s.test(token.text) && (previous === undefined || spaced(text, previous, token))) { const end = matchFrom(text, tokens, slots, slot + 1, at + 1, token); if (end !== -1) return end; } return s.optional ? matchFrom(text, tokens, slots, slot + 1, at, previous) : -1; } /** The first hit of `phrase` in a unit's tokens, or undefined. */ function matchPhrase(text, tokens, phrase) { const { anchored, slots } = compiled(phrase); const starts = anchored ? Math.min(1, tokens.length) : tokens.length; for (let i = 0; i < starts; i += 1) { const end = matchFrom(text, tokens, slots, 0, i, undefined); if (end > i) return { phrase, first: i, last: end - 1 }; } return undefined; } exports.matchPhrase = matchPhrase; /** The earliest hit of any phrase of a pack (ties go to pack order), or undefined. */ function firstHit(text, tokens, pack) { let best; for (const phrase of pack) { const hit = matchPhrase(text, tokens, phrase); if (hit !== undefined && (best === undefined || hit.first < best.first)) best = hit; } return best; } exports.firstHit = firstHit; /** Every phrase of a pack that matches somewhere in the unit. */ function allHits(text, tokens, pack) { const out = []; for (const phrase of pack) { const hit = matchPhrase(text, tokens, phrase); if (hit !== undefined) out.push(hit); } return out; } exports.allHits = allHits; //# sourceMappingURL=matcher.js.map