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