UNPKG

agentfootprint

Version:

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

189 lines • 8.11 kB
/** Strict, bounded JSON data helpers shared by internal validation boundaries. */ import { isHighSurrogate, isLowSurrogate, utf8Length } from './utf8.js'; /** * The UTF-8 bytes `value` serializes to — `JSON.stringify` and all its * escaping included: a control character or a lone surrogate is six bytes * (`\u0001`), a quote or a backslash two, a BMP character up to three, an * astral one four. THE measure for every bound stated in serialized bytes — * {@link copyJson}'s, the answer account's (`answer-account/size.ts`). `0` * for a value JSON writes nothing for (`undefined`, a function). The text * JSON writes holds no lone surrogate (each is escaped), so its UTF-8 length * is exact. */ export function jsonBytes(value) { const text = JSON.stringify(value); return text === undefined ? 0 : utf8Length(text); } /** * The longest prefix of `text`, in whole code points, whose JSON string form * (its quotes excluded) is at most `maxBytes` UTF-8 bytes — `text` itself * when it fits. Each code point is measured by JSON's own escaping * ({@link jsonBytes}), never by a second table of it. * * @example * ```ts * cutToJsonBytes('abc', 2); // 'ab' * cutToJsonBytes('\u0001\u0001', 6); // '\u0001' — JSON writes each as six bytes * ``` */ export function cutToJsonBytes(text, maxBytes) { // UTF-16 length is a lower bound on JSON content bytes. Avoid serializing // an arbitrarily long identity when only its bounded prefix can be returned. if (text.length <= maxBytes && jsonBytes(text) - 2 <= maxBytes) return text; let used = 0; let end = 0; while (end < text.length) { const width = isHighSurrogate(text.charCodeAt(end)) && isLowSurrogate(text.charCodeAt(end + 1)) ? 2 : 1; const bytes = jsonBytes(text.slice(end, end + width)) - 2; if (used + bytes > maxBytes) break; used += bytes; end += width; } return text.slice(0, end); } export function ownData(value, allowed) { if (value === null || typeof value !== 'object' || Array.isArray(value) || ![Object.prototype, null].includes(Object.getPrototypeOf(value))) throw new Error('Expected a plain data object.'); const result = Object.create(null); for (const key of Reflect.ownKeys(value)) { if (typeof key !== 'string' || (allowed && !allowed.includes(key))) throw new Error('Unknown property.'); const descriptor = Object.getOwnPropertyDescriptor(value, key); if (!descriptor?.enumerable || !('value' in descriptor)) throw new Error('Expected an enumerable data property.'); Object.defineProperty(result, key, { value: descriptor.value, enumerable: true }); } return result; } /** * Read a list the way {@link copyJson} reads an array — a plain array only * (`Array.prototype`), its own `length`, the own DATA property at each index — * as a READER that does not refuse an ordinary array: at most `maxEntries` * entries, a hole or an accessor entry read as `undefined` (the getter is never * called), and the entries past the bound counted in `unread`, never walked. It * never calls the list's iterator, an index getter or a method, so an ordinary * array from a record the library did not build cannot hide its entries or run * code inside the reader. `undefined` when `value` is not a plain array: the * caller says what that means for its own question, the conservative way. * * The guarantee covers an ordinary (non-Proxy) array and its own index * accessors and iterator, nothing more. A Proxy around an array passes * `Array.isArray`, and its traps (`getPrototypeOf`, `getOwnPropertyDescriptor`) * run code inside the reader and can throw out of it; a revoked Proxy throws * from `Array.isArray` itself. An entry is handed back as it is: a getter on an * entry object runs when the caller reads that property. * * @example * ```ts * const list = ['a']; * list.length = 3; // a hole at 1 and 2 * Object.defineProperty(list, Symbol.iterator, { value: function* () {} }); // never called * readOwnList(list, 2); // { entries: ['a', undefined], unread: 1 } * ``` */ export function readOwnList(value, maxEntries) { if (!Array.isArray(value) || Object.getPrototypeOf(value) !== Array.prototype) return undefined; const length = Object.getOwnPropertyDescriptor(value, 'length')?.value; if (typeof length !== 'number' || !Number.isSafeInteger(length) || length < 0) return undefined; const read = Math.min(length, maxEntries); const entries = []; for (let index = 0; index < read; index++) { const slot = Object.getOwnPropertyDescriptor(value, String(index)); entries.push(slot !== undefined && 'value' in slot ? slot.value : undefined); } return { entries, unread: length - read }; } /** JSON copy without invoking toJSON/getters or silently dropping data. */ export function copyJson(value, maxBytes, freeze, maxDepth = 100) { if (!Number.isSafeInteger(maxDepth) || maxDepth < 0 || maxDepth > 256) throw new Error('JSON nesting limit.'); let budget = maxBytes; const ancestors = new Set(); const spend = (bytes) => { budget -= bytes; if (budget < 0) throw new Error('JSON byte limit.'); }; const string = (item) => { if (item.length > budget) throw new Error('JSON byte limit.'); spend(jsonBytes(item)); }; const copy = (item, depth) => { if (depth > maxDepth) throw new Error('JSON nesting limit.'); if (item === null) { spend(4); return null; } if (typeof item === 'string') { string(item); return item; } if (typeof item === 'boolean') { spend(item ? 4 : 5); return item; } if (typeof item === 'number') { if (!Number.isFinite(item) || Object.is(item, -0)) throw new Error('Non-lossless JSON number.'); spend(JSON.stringify(item).length); return item; } if (typeof item !== 'object') throw new Error('Non-JSON value.'); if (ancestors.has(item)) throw new Error('Cyclic JSON value.'); ancestors.add(item); spend(2); let output; if (Array.isArray(item)) { if (Object.getPrototypeOf(item) !== Array.prototype) throw new Error('Non-JSON array prototype.'); if (item.length > budget + 1) throw new Error('JSON byte limit.'); const keys = Reflect.ownKeys(item); if (keys.length !== item.length + 1) throw new Error('Sparse or decorated array.'); const result = []; for (let index = 0; index < item.length; index++) { const descriptor = Object.getOwnPropertyDescriptor(item, String(index)); if (!descriptor?.enumerable || !('value' in descriptor)) throw new Error('Sparse or accessor array.'); if (index) spend(1); result.push(copy(descriptor.value, depth + 1)); } output = result; } else { const fields = ownData(item); const result = {}; let index = 0; for (const key of Object.keys(fields)) { if (index++) spend(1); string(key); spend(1); Object.defineProperty(result, key, { value: copy(fields[key], depth + 1), enumerable: true, writable: true, configurable: true, }); } output = result; } ancestors.delete(item); return freeze ? Object.freeze(output) : output; }; return copy(value, 0); } //# sourceMappingURL=boundedJson.js.map