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
JavaScript
;
/**
* 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