UNPKG

agentfootprint

Version:

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

131 lines (130 loc) • 6.58 kB
/** * codeRegions — the fenced code blocks of an answer, read by one linear pass, * closed where a CommonMark renderer closes them. * * Pattern: pure leaf. Imports nothing — it does not know the gate exists. The * one reader of "which bytes of this answer sit inside a fenced * block", asked by the script match (`../scripts/match.ts`) under * `.namesAndNumbersFromEvidence({ code: 'match' })`. Under the * default `code: 'check'` it is never called. * Role: core/ layer (the evidence gate's folder), no state, no events. * * ## Why it must agree with the renderer * * A region that matches a submission is exempt from the evidence check. If * this reader saw a block where a renderer shows prose, a model could submit a * program holding a sentence, show it fenced so that the sentence renders * outside the block, and the sentence would never be checked. So the reader * closes a block exactly where CommonMark closes it. Where a renderer could * pair fences another way, it stops reading blocks altogether. * * ## The grammar (design: docs/design/2026-10-script-artifact.md § 5.1–5.2, aligned with CommonMark) * * The answer is read line by line. A line ends at an LF, a CR or a CRLF, as * CommonMark ends it. * * - An OPENING fence starts at column 0 with three or more backticks, or three * or more tildes, then an optional info string, and ENDS IN A LINE ENDING. * A backtick fence's info string may not contain a backtick. * - The CLOSING fence uses the same character and is at least as long. As in * CommonMark, it may be indented by up to three spaces and is followed only * by spaces. * - The CONTENT is exactly the bytes between the opening line's line ending * and the ONE line ending before the closing line: every space, tab, CR and * code point as written. Before the closing line, an LF is the wrapper's (a * CR before it is content), and so is a lone CR. A closing fence on the line * right after the opening one gives an empty block (content `''`). * - A fence that never closes is NOT a region: the text from it to the end is * prose. * - At most {@link MAX_CODE_REGIONS} regions are read; the rest are prose and * counted in `dropped`. * * ## Where reading stops (`stoppedAt`) * * The reader models CommonMark's top level only. It does not model list * items, block quotes, HTML blocks or the extensions renderers add. At the * first line where a renderer could pair fences another way, reading STOPS. * No region is read from there on: everything after that point is prose and * checked. * * Outside a block, reading stops at: * - a fence indented by one to three spaces. It is either a list item's fence, * which the item scopes, or an indented fence, whose content a renderer * strips (and "as written" and "with the indentation removed" are two * different programs); * - a line that may start an HTML block (up to three spaces, `<`, then a * letter, `/`, `!` or `?`). Inside an HTML block, a fence is not a fence; * - a line opening display math (`$$`), the block extension chat renderers * commonly add. Inside display math, a fence is not a fence; * - a byte-order mark at the very start, which some renderers drop and some * keep. * * Inside a block, reading stops at: * - a closing run followed by a TAB. CommonMark 0.30 closes the block there * and 0.29 does not, so the block is read (it is code either way) and * reading stops after it; * - a run of the block's own fence character, anywhere in its content, as long * as its opening fence. A renderer laxer than CommonMark could close the * block there, so the block is prose and reading stops at its opening line. * The library's own wrapper never writes such a block * (`../scripts/wrapper.ts` · `fenceFor`: the longest run + 1). * * Nothing here normalizes anything: no trimming, no line-ending rewrite, no * Unicode normalization. A region is the program a reader would copy. * * ## Linear by construction * * The text is model output, so the pass is linear in it (the * `lib/linearText.ts` law, pinned by `test/security/linear-scanners.test.ts`). * Each line is read ONCE, character by character, from where the last one * ended: its indentation, its fence run, the rest of it and its line ending. * No regular expression runs over the answer. * * @example * ```ts * const { regions } = codeRegionsOf('Total:\n```python\nprint(1)\n```\nDone.'); * regions[0].content; // 'print(1)' * regions[0].block; // 1 * codeRegionsOf('```\na\n ```\nRevenue was 4321.').regions[0].content; // 'a' * ``` */ /** Regions read per answer; the rest are prose and counted (`dropped`). Design § 4.4. */ export declare const MAX_CODE_REGIONS = 32; /** One fenced block, as the answer wrote it. */ export interface CodeRegion { /** 1-based position among the regions READ, in answer order. */ readonly block: number; /** Index of the opening fence line's first character. */ readonly start: number; /** Index just past the closing fence line (past its line ending when it has one). */ readonly end: number; /** The bytes between the wrapper's two line endings — the program as written. */ readonly content: string; /** The opening line's info string, as written (spaces excluded at the ends). */ readonly info: string; } /** Every region of one answer, and how many more there were. */ export interface CodeRegions { readonly regions: readonly CodeRegion[]; /** Regions past {@link MAX_CODE_REGIONS}: read as prose, never matched. */ readonly dropped: number; /** * Where reading stopped, when it did: the index of the first character no * region is read from. Everything from here on is prose. Absent when the * whole answer was read. */ readonly stoppedAt?: number; /** * The opening run of a fence still open at the END (`'```'`, `'~~~~'`) — * the block a renderer is still inside there. The fence is prose for the * match; a writer that appends after this text closes it first * (`../scripts/attachment.ts` · `attachmentBoundary`). Never set with * `stoppedAt`: after a stop the reader does not know what is open. */ readonly openFence?: string; } /** * The fenced code regions of `text`, in order, how many past the cap, and where * reading stopped. Pure and linear; `max` defaults to {@link MAX_CODE_REGIONS}. */ export declare function codeRegionsOf(text: string, max?: number): CodeRegions;