/** * Bump when the token grammar below changes in a way that alters output for any * input. It is a coarse, human-readable companion to the behavioural harness * digest — the harness digest is what actually enforces this, but a reader * comparing two reports should not have to diff hex to see that the format moved. */ export declare const DIGEST_FORMAT = 1; /** * Anything that absorbs a stream of text. Node's `crypto.Hash` satisfies it, and * so does a three-line test double. * * The CALLER supplies this, and therefore owns the algorithm and the result. * Deciding what the canonical byte stream IS depends on parseman's node shapes * and on nothing else; deciding that you wanted sha256-hex does not. */ export type DigestTarget = { update(chunk: string): void; }; /** * Raised when a walk exceeds its visit budget. * * A distinct class, and deliberately NOT an ordinary `Error` a caller might * lump in with a parse failure: this says the TOOL gave up, which is the * opposite of a fact about the grammar. */ export declare class CanonicalBudgetError extends Error { readonly name = "CanonicalBudgetError"; constructor(budget: number, projected?: number); } /** * Default visit budget. * * A backstop against unbounded work, not a correctness feature. Any walk that * finishes under budget writes exactly the bytes it always did, so this cannot * move a recorded digest — which is the only reason it can be added to a shipped * format at all. * * It is NOT a fix for the underlying asymmetry. The fix is to dedupe by node * IDENTITY instead of by ancestor path, and that rewrites the byte stream for * every value with any sharing in it: `{ left: shared, right: shared }` stops * being two full writes and becomes a back-reference. That invalidates every * digest, every committed baseline, and {@link DIGEST_FORMAT} itself. It is a * format decision for the owner, not something to slip in behind a bug fix. * * ## The tempting middle road does not exist * * Stated because it looks obviously right and is obviously right about the wrong * thing: memoise the canonical BYTES of a subtree by node identity, splice the * cached bytes in at each of the two places, and you get the identical output * with the work done once. It is sound — a subtree's bytes depend only on the * subtree, except for a back-edge leaving it, which is exactly the case * {@link projectVisits} declines — and it does not help. * * It does not help because the walk is ALREADY linear in its own output, and it * is the OUTPUT that is exponential: a depth-`d` two-way unroll writes 33·2^d * chars, measured, and every one of them has to reach the hash. Caching removes * re-derivation, of which there is a constant factor's worth; it cannot remove * bytes the format requires. At depth 40 that is 36 TB of canonical text — 7 * hours of sha256 at the 1.37 Gchar/s this machine manages, with traversal, * allocation and splicing all free. Depth 40 has no answer to compute quickly. * There is only refusing quickly, which is what the probe does. */ export declare const DEFAULT_MAX_VISITS = 100000000; /** Options shared by every entry point that walks a value. */ export type CanonicalOptions = { /** * Maximum object visits before {@link CanonicalBudgetError}. Defaults to * {@link DEFAULT_MAX_VISITS}. A walk that finishes under budget is byte-for-byte * unaffected, so this can never change a digest. * * MUST be a non-negative safe integer. `NaN`, `Infinity`, a negative and a * fractional value are all REJECTED rather than clamped — see * {@link newState} for why a bad budget is a thrown error and not a shrug. */ maxVisits?: number | undefined; }; /** * The canonical token string for a value. Exported because a moved digest is * only actionable if you can see WHAT moved: diff two canonical strings and the * answer is a line, not a hex mismatch. * * This MATERIALISES the projection, so it is bounded by the maximum JS string * length and by available memory. Use it to explain a move you already know * about; take the digest itself with {@link digestInto}, which is bounded by * neither. */ export declare function canonicalize(value: unknown, options?: CanonicalOptions): string; /** * Stream a value's canonical projection into a caller-owned hash. * * This is the primitive: deterministic serialization of ONE parse result, which * is the part only parseman can do — it is parseman's node shapes that decide * which distinctions are semantically meaningful. The caller brings the hash and * keeps the result, so the algorithm, the encoding and the digest width are all * theirs. * * ```ts * const sha = createHash('sha256') * digestInto(sha, tree) * const digest = sha.digest('hex') * ``` * * `prefix` is written ahead of the first token with NO separator, for callers * that discriminate one digest space from another (parseman's own oracle writes * `OK:` or `ERR:`). Pass `''` when there is nothing to discriminate. * * Equivalent to `hash(prefix + canonicalize(value))`, and preferable in every * case where you do not need to READ the projection. * * ## Two sharp edges, stated because the target is YOURS * * **A throw leaves the target written-to.** The token stream is pushed at * `target` as the walk proceeds, so a {@link CanonicalBudgetError}, a throwing * getter, or any other failure mid-walk leaves an ARBITRARY PREFIX of the * projection already absorbed. The hash object is then polluted: its digest is * neither the value's nor anything else's, and it is not recoverable — a * `crypto.Hash` cannot be rewound. On any throw, DISCARD the target and start a * fresh one. {@link digestValue} does exactly that by owning its hash for a * single call. * * **Two calls against one target concatenate with NO delimiter.** There is no * record separator between calls: `digestInto(t, a); digestInto(t, b)` writes * exactly `canonicalize(a) + canonicalize(b)`, with nothing in between. That is * ambiguous across the call boundary — the byte stream does not record where one * value ended, so a differently-split sequence can produce the identical stream * and therefore the identical digest. Concretely, `digestInto(t, 1); * digestInto(t, 2)` and the single call `digestInto(t, 2, '#1')` both write * `#1#2`. (Within ONE call the projection is unambiguous — every token is * self-contained and NUL-joined; it is only the seam between calls that is not.) * * This is DOCUMENTED rather than fixed. Any delimiter at the seam — written * before, after, or between — changes the bytes some existing caller already * hashes, and moving a recorded digest is a {@link DIGEST_FORMAT} decision for * the owner, not a patch. Callers hashing a SEQUENCE should digest a wrapper * value (`digestInto(t, [a, b])`), or write their own unambiguous separator * between calls, or use one target per value. */ export declare function digestInto(target: DigestTarget, value: unknown, prefix?: string, options?: CanonicalOptions): void; /** * Full 64-hex sha256 of a value's canonical projection, streamed. * * The convenience wrapper over {@link digestInto} for the common case. * Identical to `sha256(prefix + canonicalize(value))` for every value the * latter can survive, and unbounded where it is not. */ export declare function digestValue(value: unknown, prefix?: string, options?: CanonicalOptions): string; //# sourceMappingURL=digest.d.ts.map