UNPKG

agentfootprint

Version:

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

220 lines • 7.31 kB
"use strict"; /** * strictJson — ONE reading of a JSON text, with every ambiguity refused. * * `JSON.parse` resolves ambiguity silently: a duplicated key keeps its LAST * value, while another reader may keep the first. A check that reads a body one * way and a consumer that reads it another is a parser differential — a crafted * answer passes the check and is used as something else. So the transport reads * an answer once, here, refusing what two readers could disagree on, and hands * the client the canonical re-serialization of exactly the value it checked. * * Refused, by name: a duplicated object key, trailing data after the value, a * leading byte-order mark, nesting deeper than {@link MAX_DEPTH}, a number * out of range (its re-serialization would be `null`), and anything that is * not RFC 8259 JSON. * * Objects are built with own properties only (a `__proto__` key is data, never * a prototype). * * Pattern: recursive-descent parser. Role: outer ring, the one reader. */ Object.defineProperty(exports, "__esModule", { value: true }); exports.parseStrictJson = exports.MAX_DEPTH = exports.AmbiguousJsonError = void 0; /** Why a text was refused. */ class AmbiguousJsonError extends Error { reason; constructor(reason) { super(`ambiguous JSON: ${reason}`); this.reason = reason; this.name = 'AmbiguousJsonError'; } } exports.AmbiguousJsonError = AmbiguousJsonError; /** How deep a value may nest. */ exports.MAX_DEPTH = 256; /** * The one value `text` holds. * * @throws AmbiguousJsonError when the text is not exactly one unambiguous value. */ function parseStrictJson(text) { if (text.charCodeAt(0) === 0xfeff) throw new AmbiguousJsonError('a leading byte-order mark'); const reader = new Reader(text); reader.space(); const value = reader.value(0); reader.space(); if (!reader.done()) throw new AmbiguousJsonError('trailing data after the value'); return value; } exports.parseStrictJson = parseStrictJson; const NUMBER = /-?(?:0|[1-9]\d*)(?:\.\d+)?(?:[eE][+-]?\d+)?/y; /** Characters a string may hold as they are: not a quote, a backslash or a control character. */ // eslint-disable-next-line no-control-regex const PLAIN_RUN = /[^"\\\u0000-\u001f]+/y; class Reader { text; at = 0; constructor(text) { this.text = text; } done() { return this.at >= this.text.length; } space() { while (this.at < this.text.length) { const c = this.text.charCodeAt(this.at); if (c !== 0x20 && c !== 0x09 && c !== 0x0a && c !== 0x0d) return; this.at++; } } value(depth) { if (depth > exports.MAX_DEPTH) throw new AmbiguousJsonError('nesting too deep'); const c = this.text[this.at]; if (c === '{') return this.object(depth); if (c === '[') return this.array(depth); if (c === '"') return this.string(); if (this.word('true')) return true; if (this.word('false')) return false; if (this.word('null')) return null; return this.number(); } object(depth) { this.at++; // { const out = {}; const seen = new Set(); this.space(); if (this.text[this.at] === '}') { this.at++; return out; } for (;;) { this.space(); if (this.text[this.at] !== '"') throw new AmbiguousJsonError('an object key that is not a string'); const key = this.string(); // The key is not named: it is the answer's text, and a refusal repeats none. if (seen.has(key)) throw new AmbiguousJsonError('a duplicated object key'); seen.add(key); this.space(); this.expect(':'); this.space(); Object.defineProperty(out, key, { value: this.value(depth + 1), enumerable: true, writable: true, configurable: true, }); this.space(); if (this.text[this.at] === ',') { this.at++; continue; } this.expect('}'); return out; } } array(depth) { this.at++; // [ const out = []; this.space(); if (this.text[this.at] === ']') { this.at++; return out; } for (;;) { this.space(); out.push(this.value(depth + 1)); this.space(); if (this.text[this.at] === ',') { this.at++; continue; } this.expect(']'); return out; } } string() { this.at++; // opening quote let out = ''; for (;;) { if (this.at >= this.text.length) throw new AmbiguousJsonError('an unterminated string'); const c = this.text[this.at]; const code = c.charCodeAt(0); if (c === '"') { this.at++; return out; } if (code < 0x20) throw new AmbiguousJsonError('a control character inside a string'); if (c !== '\\') { // A run of plain characters at once — a large string is read in slices. PLAIN_RUN.lastIndex = this.at; const run = PLAIN_RUN.exec(this.text)?.[0] ?? c; out += run; this.at += run.length; continue; } const e = this.text[this.at + 1]; this.at += 2; if (e === 'u') { const hex = this.text.slice(this.at, this.at + 4); if (!/^[0-9a-fA-F]{4}$/.test(hex)) throw new AmbiguousJsonError('a malformed \\u escape'); out += String.fromCharCode(parseInt(hex, 16)); this.at += 4; continue; } const simple = ESCAPES[e ?? '']; if (simple === undefined) throw new AmbiguousJsonError('an unknown escape'); out += simple; } } number() { NUMBER.lastIndex = this.at; const match = NUMBER.exec(this.text); if (match === null || match[0] === '') throw new AmbiguousJsonError('a value that is not JSON'); this.at += match[0].length; const value = Number(match[0]); // Re-serialized, a number out of range would become `null` — another value. if (!Number.isFinite(value)) throw new AmbiguousJsonError('a number out of range'); return value; } word(word) { if (!this.text.startsWith(word, this.at)) return false; this.at += word.length; return true; } expect(c) { if (this.text[this.at] !== c) throw new AmbiguousJsonError(`a missing '${c}'`); this.at++; } } const ESCAPES = { '"': '"', '\\': '\\', '/': '/', b: '\b', f: '\f', n: '\n', r: '\r', t: '\t', }; //# sourceMappingURL=strictJson.js.map