UNPKG

agentfootprint

Version:

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

237 lines • 12.3 kB
"use strict"; Object.defineProperty(exports, "__esModule", { value: true }); exports.byHeading = exports.atxHeadings = void 0; const constants_js_1 = require("./constants.js"); const shared_js_1 = require("./shared.js"); const linearText_js_1 = require("../../lib/linearText.js"); /** * ATX headings only (`# Title`). Setext (`Title\n=====`) is rare in docs corpora. * * Exactly the lines `/^(#{1,6})[ \t]+(.+?)[ \t]*$/gm` matches, with the same * title, found in one pass. The regex is not used because a document is input * a deployer may not trust: on `# a` followed by a long run of blanks and one * more letter, its lazy title stops at every blank and re-reads the rest of the * run to see whether the line ends there — quadratic in the run (16,000 blanks * take ~0.2 s in V8). */ function atxHeadings(text) { const found = []; let lineStart = 0; for (;;) { let lineEnd = lineStart; while (lineEnd < text.length && !(0, linearText_js_1.isLineBreak)(text.charCodeAt(lineEnd))) lineEnd++; const heading = atxHeadingOn(text, lineStart, lineEnd); if (heading !== undefined) found.push(heading); if (lineEnd >= text.length) return found; lineStart = lineEnd + 1; } } exports.atxHeadings = atxHeadings; /** The heading on the line `[start, end)`, if it is one. */ function atxHeadingOn(text, start, end) { let level = 0; while (start + level < end && text.charCodeAt(start + level) === 0x23) level++; if (level === 0 || level > 6) return undefined; const afterHashes = start + level; if (afterHashes >= end || !isBlank(text.charCodeAt(afterHashes))) return undefined; let titleStart = afterHashes; while (titleStart < end && isBlank(text.charCodeAt(titleStart))) titleStart++; let titleEnd = end; while (titleEnd > titleStart && isBlank(text.charCodeAt(titleEnd - 1))) titleEnd--; if (titleEnd > titleStart) { return { offset: start, end, level, title: text.slice(titleStart, titleEnd) }; } // Hashes and blanks only. The pattern still matches once there are two // blanks: `[ \t]+` gives one back and the title is the last blank. if (end - afterHashes >= 2) { return { offset: start, end, level, title: text.slice(end - 1, end) }; } return undefined; } /** `[ \t]` */ function isBlank(code) { return code === 0x20 || code === 0x09; } function byHeading(options = {}) { const maxChars = options.maxChars ?? constants_js_1.DEFAULT_MAX_CHARS; const overlapChars = options.overlapChars ?? constants_js_1.DEFAULT_OVERLAP_CHARS; const maxLevel = options.maxLevel ?? 6; const minChars = (0, shared_js_1.resolveMinChars)(options.minChars, maxChars); return { name: 'byHeading', split(doc) { const text = doc.text; // Where each section starts, and what it is called. const starts = []; for (const found of atxHeadings(text)) { if (found.level > maxLevel) continue; starts.push({ offset: found.offset, heading: found.title, headingEnd: found.end }); } // A document with no headings at all is a paragraph document; say so by // doing the paragraph thing rather than returning one giant chunk. if (starts.length === 0) { const packed = (0, shared_js_1.packSpans)((0, shared_js_1.paragraphSpans)(text), maxChars); const cut = packed.flatMap((s) => s.end - s.start > maxChars ? (0, shared_js_1.hardCut)(s, maxChars, overlapChars) : [s]); const floored = (0, shared_js_1.mergeShortSpansForward)((0, shared_js_1.foldRunts)(cut), minChars); return (0, shared_js_1.toPieces)(doc, (0, shared_js_1.applyOverlap)(floored, overlapChars)); } const sections = []; // Preamble before the first heading — its own section, unnamed. Its // whole span IS its body. const firstStart = starts[0]?.offset ?? 0; if (firstStart > 0) sections.push({ start: 0, end: firstStart, bodyStart: 0 }); for (let i = 0; i < starts.length; i++) { const start = starts[i]?.offset ?? 0; const end = starts[i + 1]?.offset ?? text.length; const heading = starts[i]?.heading; const bodyStart = Math.min(end, starts[i]?.headingEnd ?? start); sections.push({ start, end, bodyStart, ...(heading !== undefined && { heading }) }); } // Sections that fit stay whole; the rest are packed by paragraph WITHIN // the section, so a boundary never merges two headings' content. Kept // GROUPED per section here, because the floor pass below must reach the // FIRST chunk of a section and only that one. const groups = sections.map((section) => { if (section.end - section.start <= maxChars) { return [ { start: section.start, end: section.end, ...(section.heading !== undefined && { heading: section.heading }), }, ]; } // Paragraphs of the BODY, with the heading line glued onto the first // one. Splitting the section's own text used to make the heading line // its own paragraph unit — and when the first body paragraph was too // big to pack with it, the heading shipped as a chunk of its own. That // is the exact heading-only failure this release closes; the heading // is a label, never a unit. const inner = (0, shared_js_1.paragraphSpans)(text.slice(section.bodyStart, section.end)).map((s) => ({ start: section.bodyStart + s.start, end: section.bodyStart + s.end, ...(section.heading !== undefined && { heading: section.heading }), })); const firstInner = inner[0]; if (firstInner !== undefined && section.start < section.bodyStart) { inner[0] = { start: section.start, end: firstInner.end, ...(section.heading !== undefined && { heading: section.heading }), }; } const spans = []; for (const packedSpan of (0, shared_js_1.packSpans)(inner, maxChars)) { if (packedSpan.end - packedSpan.start > maxChars) { spans.push(...(0, shared_js_1.hardCut)(packedSpan, maxChars, overlapChars)); } else { spans.push(packedSpan); } } return spans; }); // ── The floor pass (8.20.0). ───────────────────────────────────────── // A short section is by construction a single span (its whole span is // under the floor, which is under maxChars), so merging it forward means // extending the FIRST span of the next full section back over it — the // rest of that section's spans keep their own heading. Consecutive short // sections accumulate; an accumulation that clears the floor and has a // real body is emitted as its own chunk rather than diluting a full // neighbour. const out = []; let pending; for (let i = 0; i < sections.length; i++) { // sections and groups are built index-for-index above. // eslint-disable-next-line @typescript-eslint/no-non-null-assertion const section = sections[i]; // eslint-disable-next-line @typescript-eslint/no-non-null-assertion const group = groups[i]; const bodyLength = text.slice(section.bodyStart, section.end).trim().length; const bare = bodyLength === 0; // heading-plus-whitespace: merged UNCONDITIONALLY const short = !bare && bodyLength < minChars; if (bare || short) { if (pending === undefined) { pending = { start: section.start, bodyChars: bodyLength, ...(section.heading !== undefined && { heading: section.heading }), }; } else { pending.bodyChars += bodyLength; } // Adjacent shorts whose BODIES together clear the floor become their // own chunk — the same measure shortness is judged by, so a single // short section can never self-emit on the strength of its markup. if (minChars > 0 && pending.bodyChars >= minChars) { out.push({ start: pending.start, end: section.end, ...(pending.heading !== undefined && { heading: pending.heading }), }); pending = undefined; } continue; } if (pending !== undefined) { const first = group[0]; if (first !== undefined) { // The merged chunk STARTS at the short section's heading, so the // short section's heading is the honest label. A heading-less // preamble keeps the chunk heading-less: a chunk is never labelled // with a heading its text only reaches later. group[0] = { start: pending.start, end: first.end, ...(pending.heading !== undefined && { heading: pending.heading }), }; } pending = undefined; } out.push(...group); } if (pending !== undefined) { const lastSectionEnd = sections[sections.length - 1]?.end ?? text.length; const previous = out.pop(); if (previous !== undefined) { // Trailing shorts have no next chunk — the one edge that merges // BACKWARD, extending the previous chunk under its own heading. out.push({ start: previous.start, end: lastSectionEnd, ...(previous.heading !== undefined && { heading: previous.heading }), }); } else if (pending.bodyChars > 0) { // The whole document is short sections. No neighbour exists, and // "never dropped" wins: one merged chunk. out.push({ start: pending.start, end: lastSectionEnd, ...(pending.heading !== undefined && { heading: pending.heading }), }); } // else: nothing but headings and whitespace — coordinates without a // passage anywhere. No chunk is emitted; `indexDocuments` makes the // same refusal one layer up for a passage-less document. } // Within-section tail runts still fold BACKWARD (same heading — the // direction that cannot mislabel), now up to the same floor. const runtFloor = minChars > 0 ? minChars : undefined; return (0, shared_js_1.toPieces)(doc, (0, shared_js_1.applyOverlap)((0, shared_js_1.foldRunts)(out, runtFloor), overlapChars)); }, }; } exports.byHeading = byHeading; //# sourceMappingURL=byHeading.js.map