agentfootprint
Version:
The explainable agent framework — backtrack a wrong answer to the exact context that caused it (evidence, not guesses). Built on footprintjs.
190 lines • 6.79 kB
JavaScript
;
/**
* units — a reply split into the units a claim is read in: Markdown-aware, then by sentence,
* then by clause.
*
* Pattern: one linear pass over lines. A fenced block's lines are CODE units; a table row is one
* unit across its cells; a list item and a heading are one unit each; paragraph text is
* split into sentences (a `.`/`!`/`?` before a space or the end, so an identifier's
* dots never split one). Every unit is then split into clauses at "but", "however",
* "except", "although" and `;`, each clause its own unit with its own polarity.
* Role: integrity/ layer (L3), read by `detect.ts` only.
*
* Inline code (a backtick run closed by a run of the same length, CommonMark's rule) is returned
* as spans, so a claim entirely inside one is noted, not judged.
*/
Object.defineProperty(exports, "__esModule", { value: true });
exports.insideAny = exports.unitsOf = void 0;
const matcher_js_1 = require("./matcher.js");
function linesOf(text) {
const out = [];
let start = 0;
for (let i = 0; i <= text.length; i += 1) {
if (i === text.length || text[i] === '\n') {
const end = i > start && text[i - 1] === '\r' ? i - 1 : i;
out.push({ start, end, text: text.slice(start, end) });
start = i + 1;
}
}
return out;
}
const FENCE = /^ {0,3}(`{3,}|~{3,})/;
const TABLE_SEPARATOR = /^\s*\|?[\s:|-]*-[\s:|-]*\|?\s*$/;
const LIST_ITEM = /^\s*(?:[-*+]|\d{1,9}[.)])\s+/;
const HEADING = /^ {0,3}#{1,6}\s+/;
const QUOTE = /^\s*>\s?/;
const CLAUSE_WORDS = new Set(['but', 'however', 'except', 'although']);
/** `[start, end)` with surrounding whitespace removed; undefined when nothing is left. */
function trimmed(text, start, end) {
let s = start;
let e = end;
while (s < e && /\s/u.test(text[s]))
s += 1;
while (e > s && /\s/u.test(text[e - 1]))
e -= 1;
return s < e ? { start: s, end: e } : undefined;
}
/** A block of paragraph text, split at sentence ends. */
function sentencesOf(text, start, end) {
const out = [];
let from = start;
for (let i = start; i < end; i += 1) {
const ch = text[i];
if (ch !== '.' && ch !== '!' && ch !== '?')
continue;
let j = i + 1;
while (j < end && (text[j] === '.' || text[j] === '!' || text[j] === '?'))
j += 1;
if (j === end || /\s/u.test(text[j])) {
const span = trimmed(text, from, j);
if (span !== undefined)
out.push(span);
from = j;
}
i = j - 1;
}
const rest = trimmed(text, from, end);
if (rest !== undefined)
out.push(rest);
return out;
}
/** A unit split at clause words and semicolons; the clause word opens the next clause. */
function clausesOf(text, span) {
const cuts = [];
for (let i = span.start; i < span.end; i += 1)
if (text[i] === ';')
cuts.push(i);
for (const token of (0, matcher_js_1.tokensOf)(text, span.start, span.end)) {
if (CLAUSE_WORDS.has(token.text) && token.start > span.start)
cuts.push(token.start);
}
if (cuts.length === 0)
return [span];
cuts.sort((a, b) => a - b);
const out = [];
let from = span.start;
for (const cut of cuts) {
const piece = trimmed(text, from, cut);
if (piece !== undefined)
out.push(piece);
from = text[cut] === ';' ? cut + 1 : cut;
}
const last = trimmed(text, from, span.end);
if (last !== undefined)
out.push(last);
return out;
}
/** Inline code spans of one line: a backtick run closed by the next run of the same length. */
function inlineCodeOf(line) {
const out = [];
let open;
let i = 0;
while (i < line.text.length) {
if (line.text[i] !== '`') {
i += 1;
continue;
}
let j = i;
while (j < line.text.length && line.text[j] === '`')
j += 1;
const length = j - i;
if (open === undefined)
open = { length, start: line.start + j };
else if (open.length === length) {
out.push({ start: open.start, end: line.start + i });
open = undefined;
}
i = j;
}
return out;
}
/** Split a reply into units. */
function unitsOf(text) {
const units = [];
const inlineCode = [];
const push = (span, fenced) => {
if (span === undefined)
return;
for (const clause of clausesOf(text, span))
units.push({ ...clause, fenced });
};
let fence;
let paragraph;
const flush = () => {
if (paragraph === undefined)
return;
for (const sentence of sentencesOf(text, paragraph.start, paragraph.end))
push(sentence, false);
paragraph = undefined;
};
for (const line of linesOf(text)) {
const marker = FENCE.exec(line.text);
if (fence !== undefined) {
const closes = marker !== null &&
marker[1]?.[0] === fence.char &&
(marker[1]?.length ?? 0) >= fence.length &&
line.text.slice(marker[0].length).trim() === '';
if (closes)
fence = undefined;
else
for (const s of sentencesOf(text, line.start, line.end))
push(s, true);
continue;
}
if (marker !== null) {
flush();
fence = { char: marker[1]?.[0] ?? '`', length: marker[1]?.length ?? 3 };
continue;
}
if (line.text.trim() === '') {
flush();
continue;
}
inlineCode.push(...inlineCodeOf(line));
const body = line.text.replace(QUOTE, (m) => ' '.repeat(m.length));
if (body.trimStart().startsWith('|')) {
flush();
if (!TABLE_SEPARATOR.test(body))
push(trimmed(text, line.start, line.end), false);
continue;
}
const item = LIST_ITEM.exec(body) ?? HEADING.exec(body);
if (item !== null) {
flush();
push(trimmed(text, line.start + item[0].length, line.end), false);
continue;
}
const start = line.start + (body.length - body.trimStart().length);
paragraph =
paragraph === undefined ? { start, end: line.end } : { ...paragraph, end: line.end };
}
flush();
return { units, inlineCode };
}
exports.unitsOf = unitsOf;
/** Whether a span lies entirely inside one of the code spans. */
function insideAny(span, code) {
return code.some((c) => c.start <= span.start && span.end <= c.end);
}
exports.insideAny = insideAny;
//# sourceMappingURL=units.js.map