// SPDX-License-Identifier: Apache-2.0
// ust-protocol — reference implementation of UST 1.0 (the official STATELESS base; the public verification lib) (REV 26), LIGHT floor first.
// §16: ONE version source — the conformance runner asserts spec/package/vectors all carry the same rc.
export const VERSION = { wire: '1.0', spec: '1.0.0-rc.73', revision: 94 };   // #75 P1-09: machine-readable {wire, spec, revision} — Status line & appendix must agree
// ─── #43 — OUTBOUND REQUESTS CARRY A LABEL. Measured 2026-08-15: nothing here set one, so every call from this
//     tree left as the runtime's bare default (`node`/`undici`), indistinguishable from any other script on both
//     our own logs and a provider's. Being identifiable is PROTECTION for a client that behaves: anonymous
//     programmatic traffic is the first an API sheds, while a named client with a contact URL is the one a
//     provider writes to before blocking. The two versions stay SEPARATE on purpose — `ust/<wire>` says what the
//     traffic IS, the component's package version says which implementation is calling; folded into one number
//     they diverge at wire 1.1. The value carries WHAT THE SOFTWARE IS and nothing about the user: no install id,
//     no counter, no document. It is a label on a request the consumer's own code already makes, never telemetry
//     we collect. Browsers forbid the header and drop it silently, so the same seam is safe in a browser build.
//     CLOSED 2026-08-15 — every network surface in this tree labels its calls, and `tools/user-agent-gate.mjs`
//     enumerates them FROM SOURCE and compares each version against its own package.json, so a new surface has
//     to opt in and an old label cannot go stale in silence.
export const REPO_URL = 'https://github.com/thelabmd/UST-Protocol';
export const userAgent = (component, version) => `ust/${VERSION.wire} (${component}/${version}; +${REPO_URL})`;
// ONE SEAM, never a per-call-site edit: the DEFAULT `fetchImpl` is signed, so every existing call inherits it and
// a new one cannot forget. A caller supplying its own `fetchImpl` is supplying its own client, and we do not
// relabel someone else's requests.
export const labelledFetch = (component, version, impl = fetch) => (url, init = {}) =>
  impl(url, { ...init, headers: { ...(init?.headers || {}), 'user-agent': userAgent(component, version) } });
// The core's own client, captured ONCE. The package version comes from `VERSION.spec` rather than a literal:
// a second copy of that number would go stale at the next rc while still reading as current.
const ustFetch = labelledFetch('ust-protocol', VERSION.spec);
// Written FROM THE SPEC (§ references inline), NOT copied from the vector generator — so running it against
// the vectors is a cross-check between two independently-written artifacts. Zero-dependency: node:crypto
// (Ed25519 + SHA-256). Portable note: WebCrypto (SubtleCrypto Ed25519) or @noble/{ed25519,hashes} for
// browsers/Workers; same rules.
// #143: the platform's cryptographic faculty comes from ONE internal module, chosen at BUILD time. It is not
// an option and not reachable from the data path — see `_crypto.mjs` for why that distinction is load-bearing.
import { sha256Hex, ed25519Verify, ed25519Sign, aesGcmEncrypt, aesGcmDecrypt,
  AEAD_IMPLEMENTED, xchachaEncrypt, xchachaDecrypt } from './_crypto.mjs';
import { utf8, utf8Len, decodeUtf8, concatBytes, toHex, fromHex, toB64url, fromB64url } from './_bytes.mjs';
import { beginSignatureResolution, endSignatureResolution, unresolvedSignatures, recordSignature, consultSignature } from './_sigmemo.mjs';   // #144 — the async signature faculty is VERIFIER-OWNED and internal, for the same reason the clock is: a caller-supplied answer to "did this verify" is a forgery oracle
import { witnessNow } from './_clock.mjs';   // rev33 R4 — the witness-budget clock is a VERIFIER-OWNED faculty in an INTERNAL module, never a caller-supplied opts field (round-29 P0-02)

// ─── §6 Canonicalization (JCS tightened) ────────────────────────────────────────────────────────────
// keys sorted by UTF-16 code unit, no whitespace, string-only leaves (reject number/bool/null), NFC,
// unique member names. Throws {code:'E-CANON'} on violation.
export function canon(v) {
  if (v === null || typeof v === 'number' || typeof v === 'boolean') throw err('E-CANON', 'non-string leaf');
  if (typeof v === 'string') { if (v.normalize('NFC') !== v) throw err('E-CANON', 'non-NFC string'); return JSON.stringify(v); }
  if (Array.isArray(v)) return '[' + v.map(canon).join(',') + ']';          // §6.3 order significant
  if (typeof v === 'object') {
    const k = Object.keys(v);
    if (new Set(k).size !== k.length) throw err('E-CANON', 'duplicate key'); // §6.1
    for (const x of k) if (x.normalize('NFC') !== x) throw err('E-CANON', 'non-NFC member name'); // §6 — NAMES too, not just leaves (F6)
    return '{' + k.slice().sort().map(x => JSON.stringify(x) + ':' + canon(v[x])).join(',') + '}';
  }
  throw err('E-CANON', 'unsupported');
}

// ─── §7 Domain-separated hash: H_t(x) = "sha256:" || hex(SHA256(ascii(t) || 0x00 || x)) ──────────────
function sha(buf) { return 'sha256:' + sha256Hex(buf); }
export const H = (tag, strInput) => sha(concatBytes([utf8(tag), new Uint8Array([0]), utf8(strInput)])); // x = utf8 string
export const Hbytes = (tag, rawBuf) => sha(concatBytes([utf8(tag), new Uint8Array([0]), rawBuf]));                     // x = raw bytes

// ─── §12.2/§17 key_id = H("ust:keylog", raw_pub_bytes) — raw = base64url-decode(pub), NOT plain SHA256(pub)
export const keyId = (pubB64url) => Hbytes('ust:keylog', fromB64url(pubB64url));

// ─── §4.4 per-partition hash — UNIFORM: EVERY partition binds its publisher (domain_shard). The partition NAME
//     is carried as a VALUE (`partition:`), never as a key, so it can never overwrite a protocol field (closes the
//     reserved-name collision). The old domain-less `computed` mode is REMOVED: its "cross-engine corroboration"
//     was forgeable (anyone copies a domain-less hash to fake agreement) — real corroboration compares two
//     publisher-BOUND values a layer up. `kind` is now descriptive metadata only and does NOT affect the hash.
export function partitionHash({ domain_shard, ust_id, name, value, commit }) {
  if (commit !== undefined) return Hbytes('ust:shard', utf8(commit)); // §4.4 private: hash over its commit
  return H('ust:shard', canon({ domain_shard, ust_id, partition: name, value }));
}

// ─── §7 content_hash = H("ust:state", canon({ust, state})) — the unique document descriptor ─────────────
export const signedContent = (doc) => canon({ ust: doc.ust, state: doc.state });
export const contentHash = (doc) => H('ust:state', signedContent(doc));

// ─── §9.4 seed / §9.2 Merkle root ───────────────────────────────────────────────────────────────────
export const seed = (contentHashes) => H('ust:seed', canon(contentHashes));           // pinned signed order
export function merkleRoot(contentHashes) {                                            // byte-ascending sort, ust:leaf/ust:node
  let lvl = contentHashes.slice().sort().map(h => Hbytes('ust:leaf', utf8(h)));
  while (lvl.length > 1) {
    const nx = [];
    for (let i = 0; i < lvl.length; i += 2)
      nx.push(i + 1 < lvl.length ? Hbytes('ust:node', utf8(lvl[i] + lvl[i + 1])) : lvl[i]);
    lvl = nx;
  }
  return lvl[0];
}

// ─── §10 PRIVACY — blinded commitment (frame-bound, G23; nonce MUST be fresh & unique per commit, Z2) ────
// commit = H_shard(canon({domain_shard, ust_id, nonce, <name>: value}))  — verifier reproduces from a disclosure.
export const blindedCommit = ({ domain_shard, ust_id, name, value, nonce }) =>
  H('ust:shard', canon({ domain_shard, ust_id, nonce, partition: name, value }));   // name as VALUE, non-colliding
// producer helper: build a blinded PRIVATE partition envelope + its hashes entry (§4.4 private hash = H over commit).
export function blindPartition(name, value, { domain_shard, ust_id, nonce, kind = 'captured' }) {
  const commit = blindedCommit({ domain_shard, ust_id, name, value, nonce });
  return { partition: { kind, privacy: 'blinded', commit }, hash: partitionHash({ commit }) };
}
// §10 encrypted, SPLIT so the key never crosses into a caller that must not hold one (#178, owner 2026-09-02).
//
// `encryptPartition` does two things: it derives what must be sealed, and it seals it. Only the second needs a
// key. A publisher that must not hold one — an agent, whose tool arguments travel through a model's context and
// into every transcript recording them — can do the first half itself and hand the result to whoever owns the key.
//
// The producer corollary (F.7a.2) demands ONE derivation, not one process: if the sealer receives exactly the
// string the commitment was taken over, the two channels cannot drift, because there is only one plaintext in
// existence. `sealingRequest` produces that string and nothing else invents another.
//
// AND THE SEAM IS CHECKED. The IV is `H('ust:enc-iv', commit)[0..width]`, so the ciphertext's nonce prefix is
// DETERMINED by a commitment any key-free party can recompute. `attachEncryption` therefore verifies that the
// sealer used the IV the commitment implies — one axis of the key-holder's work, checked by a caller holding no
// key. What it cannot check is the AEAD binding itself; that needs the key, and §14 step 8 is where it happens.
//
// HONEST BOUNDARY, and an integrator must be told rather than left to find it: the key-holder receives the
// PLAINTEXT. That is inherent — they are the party entitled to read it, and would decrypt it anyway — but when
// the key-holder is a third party rather than the publisher's own operator, the value now travels somewhere it
// did not before. **The key-holder sees plaintext by construction; choose the key-holder accordingly.**
export function sealingRequest(name, value, { domain_shard, ust_id, nonce, alg = 'AES-256-GCM' }) {
  const width = nonceWidth(alg);
  if (!width) throw err('E-MALFORMED', 'unregistered AEAD for sealingRequest: ' + String(alg));
  const commit = blindedCommit({ domain_shard, ust_id, name, value, nonce });
  return {
    commit,
    plaintext: canon({ nonce, partition: name, value }),          // EXACTLY what §14 step 8 compares against
    iv: toB64url(fromB64url(H('ust:enc-iv', commit).slice(7)).subarray(0, width)),
    alg,
  };
}

// Assemble the partition from a block a key-holder sealed. No key here, and none needed.
export function attachEncryption(name, { commit, alg = 'AES-256-GCM', key_id, ct, kind = 'captured' }) {
  const width = nonceWidth(alg);
  if (!width) throw err('E-MALFORMED', 'unregistered AEAD for attachEncryption: ' + String(alg));
  if (!KEYID_FORM.test(commit || '')) throw err('E-MALFORMED', 'attachEncryption needs the sha256: commit from the sealing request');
  if (typeof key_id !== 'string' || !key_id || typeof ct !== 'string') throw err('E-MALFORMED', 'attachEncryption needs `key_id` and a base64url `ct`');
  const want = fromB64url(H('ust:enc-iv', commit).slice(7)).subarray(0, width);
  const got = fromB64url(ct).subarray(0, width);
  if (toB64url(want) !== toB64url(got))
    throw err('E-COMMIT', 'the ciphertext does not carry the IV this commitment implies — the sealer worked from a different derivation, and no key is needed to see it: ' + name);
  return { partition: { kind, privacy: 'encrypted', commit, enc: { alg, key_id, ct } }, hash: partitionHash({ commit }) };
}

// §10 encrypted PRODUCER — the mirror of `blindPartition`, added for #175 (F.7a.2 corollary).
//
// WHY THE TWO OUTPUTS COME FROM ONE OPERATION. The model states that the admissible plaintext is DETERMINED by
// the commitment: a producer that encrypts one encoding of the value while committing to another emits a
// document that is well-formed, decryptable, and refused — nobody's key opens it admissibly. So the commitment
// and the ciphertext are derived here in one place, from one `canon`, and cannot drift apart by a caller's
// mistake. That is the whole reason this is a function rather than a documented recipe.
//
// THE IV IS DERIVED, NOT RANDOM, AND THE DERIVATION IS THE POINT. §10 permits AES-256-GCM "only with a stated
// unique-nonce-per-key derivation", because GCM under a repeated IV is catastrophic. A random 96-bit IV would
// collide by birthday bound long before a busy publisher's key rotates. Here `iv = H('ust:enc-iv', commit)[0..12]`,
// so IV uniqueness REDUCES to commitment uniqueness, which §10 already requires of the producer (a fresh unique
// nonce per commitment). Sealing the identical (frame, name, nonce, value) twice reproduces the same IV and the
// same ciphertext — which leaks nothing, because it reproduces the same `commit` too.
export function encryptPartition(name, value, { domain_shard, ust_id, nonce, key_id, key, kind = 'captured', alg = 'AES-256-GCM' }) {
  const width = nonceWidth(alg);
  // A producer may not emit an algorithm the registry does not name (the verifier would refuse it as the
  // DOCUMENT's defect), nor one this build cannot run (it would emit a document it cannot itself read back).
  if (!width || !AEAD_IMPLEMENTED.includes(alg)) throw new Error('unsupported AEAD for encryptPartition: ' + String(alg));
  const commit = blindedCommit({ domain_shard, ust_id, name, value, nonce });
  const plaintext = canon({ nonce, partition: name, value });          // EXACTLY what §14 step 8 compares against
  const iv = fromB64url(H('ust:enc-iv', commit).slice(7)).subarray(0, width);   // strip the `sha256:` prefix
  const { body, tag } = alg === 'XChaCha20-Poly1305'
    ? xchachaEncrypt(fromB64url(key), iv, utf8(plaintext))
    : aesGcmEncrypt(fromB64url(key), iv, utf8(plaintext));
  const ct = toB64url(new Uint8Array([...iv, ...body, ...tag]));       // nonce ‖ body ‖ tag — the layout aeadDecrypt reads
  return { partition: { kind, privacy: 'encrypted', commit, enc: { alg, key_id, ct } }, hash: partitionHash({ commit }) };
}

// §10/§17 encrypted: AEAD-decrypt to recover the committed plaintext canon({nonce,<name>:value}).
// Registry discipline (MTI): AES-256-GCM is MANDATORY-to-implement (node:crypto, zero-dep); XChaCha20-Poly1305 is
// OPTIONAL — a conforming verifier that does not implement it returns INDETERMINATE(unsupported_alg), NEVER a
// silent null/INVALID (the document may be honest; the verifier just cannot decide). 'unsupported' marks that.
function aeadDecrypt(enc, keyRawB64url) {
  // The decision is taken on the build's DECLARATION, before the ciphertext is touched. Calling and catching
  // cannot work: an absent primitive and an unauthentic tag are the same observation, so one signal would carry
  // two facts and the verifier would have to guess which — and it guessed E-COMMIT, blaming the publisher for
  // its own gap (measured 2026-08-31, #176). `AEAD_IMPLEMENTED ⊆ AEAD_ALGS`; an algorithm outside the registry
  // never reaches here, having been refused as the document's defect at admission.
  if (!AEAD_IMPLEMENTED.includes(enc.alg)) return 'unsupported';        // this build's limit — INDETERMINATE, never INVALID
  try {
    const raw = fromB64url(enc.ct), key = fromB64url(keyRawB64url);
    const n = nonceWidth(enc.alg);                                // §17: the nonce is a PREFIX of ct, its width fixed per algorithm
    const iv = raw.subarray(0, n), tag = raw.subarray(raw.length - 16), body = raw.subarray(n, raw.length - 16);
    const plain = enc.alg === 'XChaCha20-Poly1305' ? xchachaDecrypt(key, iv, tag, body) : aesGcmDecrypt(key, iv, tag, body);
    // #143: decoded with TextDecoder, not `Buffer.from(plain)`. Two reasons, and both are the point of the
    // round: TextDecoder is portable, and `Buffer.from(<identifier>)` is a raw-byte DOOR the byte-door lint
    // must account for. Sidestepping the door beats allowlisting it — `plain` is ours, but the next such line
    // might not be, and an allowlist entry is where that distinction goes quietly missing.
    return plain === null ? null : new TextDecoder().decode(plain);     // the committed plaintext (utf8 canon)
  } catch { return null; }                                             // auth-tag failure ⇒ null ⇒ E-COMMIT
}

// ─── crypto helpers (strict Ed25519 via the build's faculty — see `_crypto.mjs`) ─────────────────────
/**
 * The Ed25519 group order L. Present here as ARITHMETIC, not as cryptography: comparing a 32-byte little-endian
 * scalar against a constant is the same kind of work as comparing lengths, and it is the one strictness we must
 * not borrow.
 *
 * WHY WE STOPPED BORROWING IT (#144, measured 2026-08-09). `S >= L` is a non-canonical signature and a strict
 * verifier MUST reject it (S7/N6). Until now nothing here checked that — the rejection came from whichever
 * library the build's faculty wrapped, and the conformance vector `malleability-nonCanonicalS` was therefore
 * testing OpenSSL, not us. That is tolerable while one implementation answers; it stops being tolerable the
 * moment a second one does. Implementations of Ed25519 disagree exactly here — non-canonical S, small-order
 * points, the RFC-8032/ZIP-215 split — and I4 says two conforming verifiers compute the SAME value. A verdict
 * that depends on which platform's library ran is not that value.
 *
 * Measured on the border, both APIs of one platform agreeing (S=L, S=L+1, S=0, zero pub, zero sig): node:crypto
 * and WebCrypto answered identically. That measurement is honest about its reach — it compares two APIs that
 * likely share one OpenSSL underneath, NOT Node against a real browser, which is where the split is documented.
 * Checking it ourselves removes the question instead of re-asking it per platform.
 */
/**
 * §7/N6 asks for four things: reject `S >= L`, reject small-order `A`/`R`, reject non-canonical `A`/`R` encodings,
 * and verify cofactorlessly. MEASURED 2026-08-09 against the reference edge-case corpus (Chalkias et al., 12
 * vectors) on three implementations — node:crypto, WebCrypto and Go's crypto/ed25519:
 *
 *   · all three AGREE on all 12 — the divergence this project feared between platforms did not appear;
 *   · all three are cofactorless (the cofactored-only vector is rejected by each), so that requirement holds;
 *   · `S >= L` is rejected by each, so that one holds too — through them, not through us;
 *   · and all three ACCEPT small-order `A`/`R` (vectors 0,1,2) and one non-canonical `A` (vector 11).
 *
 * So the gap was not "libraries disagree". It was that OUR OWN SPEC demands two rejections that NO library
 * performs, and nothing here performed them either — the requirement was written and never executed. Both are
 * decidable from the wire bytes, which is why they belong here rather than in a cryptographic routine:
 *
 *   · small order — the eight encodings below, a finite set, compared byte-for-byte;
 *   · canonicality — `y < p`, plus the "negative zero" case: `x == 0` (which is exactly `y ∈ {1, p-1}`) with the
 *     sign bit set. Measured: vectors 8-11 are non-canonical in the SECOND sense, not the first — `y` there is
 *     `p-1`, comfortably inside the field, and a `y >= p` check alone would have missed all four.
 *
 * Applying both changes exactly four verdicts (0,1,2,11 accept → reject) and touches nothing else; the mixed-order
 * vector 3 stays accepted, which is correct — §7/N6 does not forbid mixed order, and forbidding it would need a
 * subgroup check, i.e. scalar multiplication, i.e. cryptography this build deliberately does not carry.
 *
 * CLOSED 2026-08-09 (round 190) for the two rejections above: both are performed here and the reference corpus
 * runs in the suite as `ed25519-point-admission`. STANDING: cofactorlessness and `S >= L` are satisfied by every
 * library measured, and the check kept here for `S >= L` changes no verdict today — it exists so the rule does
 * not depend on which build runs. OPEN and named: a subgroup check is not performed, so mixed-order points are
 * admitted; that is what §7/N6 asks for, and if it ever must change, it needs cryptography, not more bytes.
 */
const ED25519_L = 7237005577332262213973186563042994240857116359379907606001950938285454250989n;
const ED25519_P = 2n ** 255n - 19n;
/** The eight small-order encodings, derived from the 8-torsion y-coordinates and verified against the corpus. */
const SMALL_ORDER = new Set([
  '0000000000000000000000000000000000000000000000000000000000000000',
  '0000000000000000000000000000000000000000000000000000000000000080',
  '0100000000000000000000000000000000000000000000000000000000000000',
  '26e8958fc2b227b045c3f489f2ef98f0d5dfac05d3c63339b13802886d53fc05',
  '26e8958fc2b227b045c3f489f2ef98f0d5dfac05d3c63339b13802886d53fc85',
  'c7176a703d4dd84fba3c0b760d10670f2a2053fa2c39ccc64ec7fd7792ac037a',
  'c7176a703d4dd84fba3c0b760d10670f2a2053fa2c39ccc64ec7fd7792ac03fa',
  'ecffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff7f',
]);
const yCoord = (p32) => { let v = 0n; for (let i = 31; i >= 0; i--) v = (v << 8n) | BigInt(i === 31 ? p32[i] & 0x7f : p32[i]); return v; };
/**
 * A point encoding this verifier admits: in-field `y`, no negative zero, not small order. §7/N6, decided on bytes.
 *
 * EXPORTED because it is a normative rule, not an implementation detail: a port must reject the same encodings,
 * and no library will do it for them — measured on three (node:crypto, WebCrypto, Go), none rejects small order.
 * Taking bytes rather than a document is deliberate: the reference edge-case corpus is byte-level, and a
 * string-shaped entry point cannot express it, which is precisely why that corpus had never been run here.
 */
export function admitEd25519Point(p32) {
  // Through THE byte door, not around it: `snapshotBytes` is the one admission that runs no caller getter and
  // rejects a lying Proxy on the intrinsic slots. Doing the copy here by hand would have been a second door, and
  // a second door is how the two drift. Non-bytes are not an error at this surface — they are a refusal.
  const snap = snapshotBytes(p32, 32, 'E-BYTES-SIZE');
  if (snap.error || snap.bytes.length !== 32) return false;
  return admitPoint(snap.bytes);
}
function admitPoint(p32) {
  if (SMALL_ORDER.has(toHex(p32))) return false;
  const y = yCoord(p32);
  if (y >= ED25519_P) return false;
  return !((y === 1n || y === ED25519_P - 1n) && (p32[31] & 0x80) !== 0);
}
const canonicalS = (sig64) => {                       // sig = R(32) ‖ S(32), S little-endian
  let s = 0n;
  for (let i = 63; i >= 32; i--) s = (s << 8n) | BigInt(sig64[i]);
  return s < ED25519_L;
};

export function edVerifyStrict(pubB64url, msgUtf8, sigB64url) {
  if (strictB64url(pubB64url, 32) === null || strictB64url(sigB64url, 64) === null) return false;   // round-36 P1-02 — canonical Pub32/Sig64 wire encoding IS part of "strict": Node's base64url decoder is permissive (a trailing-bit alias maps to the same bytes), so a non-canonical pub/sig would verify identically and split cross-language. Enforcing it at the crypto LEAF makes EVERY caller (incl. the provenance src_sig path) canonical-safe with no per-site guard.
  // §7/N6 enforced HERE, on the wire bytes, because measurement showed no library performs these two: A and R of
  // small order, and non-canonical point encodings (including the negative-zero case). `S >= L` is checked too —
  // every library rejects it today, so this line changes no verdict, and it is kept so the rule is ours rather
  // than borrowed from whichever build we happen to run on.
  const sigBytes = fromB64url(sigB64url);
  if (!canonicalS(sigBytes)) return false;
  if (!admitPoint(fromB64url(pubB64url)) || !admitPoint(sigBytes.subarray(0, 32))) return false;
  // #144 — the ASYNC faculty, consulted AFTER every strict wire check above, so a pre-resolved answer can only
  // ever stand for a triple this door would itself have put to the primitive. Outside a resolution session this
  // answers `null` and the synchronous primitive decides, exactly as before — Node is unchanged.
  const resolved = consultSignature(pubB64url, msgUtf8, sigB64url);
  if (resolved !== null) return resolved;
  try { return ed25519Verify(utf8(msgUtf8), fromB64url(pubB64url), fromB64url(sigB64url)); }
  // #144 — THE THIRD OUTCOME IS NOT `false`. This `catch` predates the browser build's refusal and is right for
  // what it was written for: a malformed key or signature is not an exception, it is a `false`. It could not tell
  // "this input is bad" from "I am NOT ABLE to check", so a build without the Ed25519 faculty had its refusal
  // converted into `INVALID:E-SIG` — a verdict accusing an honest document, which is worse than overclaiming:
  // an overclaim leaves a careful reader something to audit, an accusation leaves nothing to notice.
  // The channel for the third outcome already existed — the primitive THROWS — and this line was closing it.
  catch (e) { if (e?.code === 'E-UNSUPPORTED') throw e; return false; }
}

// ─── #75 STRICT ENCODERS — Node's permissive decoders make DISTINCT byte-strings verify identically, breaking
//     I4 raw-byte determinism + cross-language agreement. Each of these is EXACT: decode is only accepted if the
//     canonical re-encode reproduces the input byte-for-byte (P1-01/02/03).
// Strict UNPADDED base64url of EXACTLY `bytes` bytes → Buffer, or null (padding, non-alphabet char, wrong length,
// or a non-canonical trailing-bit encoding all fail: decode → re-encode → require identity).
export function strictB64url(s, bytes) {
  if (typeof s !== 'string' || !/^[A-Za-z0-9_-]+$/.test(s)) return null;   // unpadded base64url alphabet only ('=' rejected)
  let buf; try { buf = fromB64url(s); } catch { return null; }
  if (buf.length !== bytes) return null;                                   // exact length (Ed25519 pub=32, sig=64)
  if (toB64url(buf) !== s) return null;                                    // canonical: no non-zero trailing bits, no alias
  return buf;
}
// round-34 P0-01 — Pub32: a canonical unpadded base64url of EXACTLY 32 bytes (strictB64url already rejects a non-canonical
// trailing-bit alias, line 102). MIRRORS the kernel `strictPub`. Every AUTHORITY-bearing public field must pass this
// BEFORE keyId()/Ed25519 verify — Node's base64url decoder is permissive (a `...YKc`→`...YKd` alias maps to the same 32
// bytes), so `keyId(pub) === key_id` alone admits a non-canonical alias the kernel rejects (a public↔kernel split).
const strictPub = (p) => strictB64url(p, 32) !== null;
// A cadence is SECONDS as a canonical positive integer STRING — no fraction, no leading zero, no sign, bounded.
export function parseCadenceInt(s) {
  if (typeof s !== 'string' || !/^[1-9][0-9]*$/.test(s)) return null;      // "1.5", "030", "-1", "1e2", 30(number) all fail
  const n = Number(s);
  return (Number.isSafeInteger(n) && n > 0 && n <= BOUNDS.cadenceMax) ? n : null;
}
// round-24 P0-02 — a CANONICAL non-negative integer from a signed genesis ceremony field. Signed ceremony values
// (capacity max_partitions/max_transcript_bytes, recovery threshold) must be exact decimal STRINGS, never coerced by
// Number(...): `["4096"]`, `"0x80"`, `"1.28e2"`, `" 128 "`, `"+128"`, `"0128"` all collapse to a number under Number()
// and would manufacture authority-bearing capacity / lower a recovery takeover threshold. → a safe integer, or undefined.
const canonUint = (s) => { if (typeof s !== 'string' || !/^(0|[1-9][0-9]*)$/.test(s)) return undefined; const n = Number(s); return Number.isSafeInteger(n) && n >= 0 ? n : undefined; };
// round-48 P0-01 — THE ONE sound byte-admission door. GPT round-48 refuted the "immutable byte-string, order-independent BY
// CONSTRUCTION" claim on resolveKeysBytes/resolveCadenceBytes: those did `Buffer.from(arg)` directly, and Buffer.from runs an
// ARRAY-LIKE's indexed GETTERS while copying — so a getter in arg1 mutated the still-live bytes of arg2 BEFORE arg2 was
// captured (a revoked key restored → verdict flip). The domain must be ENFORCED, not assumed. Accept ONLY an EXACT native
// Uint8Array — a Proxy/subclass/array-like/string is rejected (the intrinsic byteLength/buffer getters read an internal slot a
// Proxy lacks → throw; a subclass has a different prototype) — then copy into a FRESH immutable buffer with the intrinsic
// setter, so NO caller code runs and no argument can touch a sibling before capture. checkAuthorityProofBytes already admitted
// here (its own copy, round-8/9 P0-01); the two resolvers were the entries that BYPASSED it. Now there is ONE door, no drift.
const TA_BYTELENGTH = Object.getOwnPropertyDescriptor(Object.getPrototypeOf(Uint8Array.prototype), 'byteLength').get;
const TA_BUFFER = Object.getOwnPropertyDescriptor(Object.getPrototypeOf(Uint8Array.prototype), 'buffer').get;
export function snapshotBytes(input, maxBytes, sizeErr) {
  let len, buf;
  try {   // round-50 P1-02 — `instanceof` / `Object.getPrototypeOf` RUN a Proxy's getPrototypeOf trap; keep them INSIDE the try so a throwing trap → structured reject, never a host throw escaping the door
    if (!(input instanceof Uint8Array) || Object.getPrototypeOf(input) !== Uint8Array.prototype) return { error: 'E-BYTES-TYPE' };   // EXACT native Uint8Array — a subclass (Buffer/its proto differs) or Proxy is rejected
    len = TA_BYTELENGTH.call(input); buf = TA_BUFFER.call(input);   // intrinsic getters throw on a lying Proxy (no internal slot) — never an overridden accessor
  } catch { return { error: 'E-BYTES-TYPE' }; }
  if (typeof SharedArrayBuffer !== 'undefined' && buf instanceof SharedArrayBuffer) return { error: 'E-BYTES-SHARED' };
  if (maxBytes !== undefined && len > maxBytes) return { error: sizeErr || 'E-BYTES-SIZE' };
  const copy = new Uint8Array(len); copy.set(input); return { bytes: copy };   // fresh immutable copy via the intrinsic set — no caller getter runs
}
// round-49 P1-02 — the same door for verifyJson's BROADER binary domain (a UTF-8 string OR ArrayBuffer/TypedArray/Buffer/
// DataView). `snapshotBytes` is the exact-Uint8Array case; here the length and bytes are read through INTRINSIC getters only,
// so a subclass with an OWN `byteLength` getter cannot forge the transport measurement, and the decode runs on the immutable
// copy — closing the verifyJson budget bypass and making the intrinsic snapshot the ONLY measured/decoded form.
const AB_BYTELENGTH = Object.getOwnPropertyDescriptor(ArrayBuffer.prototype, 'byteLength').get;
const TA_BYTEOFFSET = Object.getOwnPropertyDescriptor(Object.getPrototypeOf(Uint8Array.prototype), 'byteOffset').get;
const DV = typeof DataView !== 'undefined' ? DataView.prototype : null;
const DV_BUFFER = DV ? Object.getOwnPropertyDescriptor(DV, 'buffer').get : null;
const DV_BYTEOFFSET = DV ? Object.getOwnPropertyDescriptor(DV, 'byteOffset').get : null;
const DV_BYTELENGTH = DV ? Object.getOwnPropertyDescriptor(DV, 'byteLength').get : null;
function snapshotBinary(input) {
  const fromBuf = (buf, off, len) => { if (typeof SharedArrayBuffer !== 'undefined' && buf instanceof SharedArrayBuffer) return null; const c = new Uint8Array(len); c.set(new Uint8Array(buf, off, len)); return c; };
  try { if (input instanceof ArrayBuffer) { const n = AB_BYTELENGTH.call(input); const c = new Uint8Array(n); c.set(new Uint8Array(input, 0, n)); return c; } } catch { return null; }   // round-50 P1-02 — `instanceof` runs a Proxy trap; guard it (a throwing trap → null, never a host throw)
  try { return fromBuf(TA_BUFFER.call(input), TA_BYTEOFFSET.call(input), TA_BYTELENGTH.call(input)); } catch { /* not a TypedArray (intrinsic getter threw — no trap invoked) */ }
  try { if (DV_BUFFER) return fromBuf(DV_BUFFER.call(input), DV_BYTEOFFSET.call(input), DV_BYTELENGTH.call(input)); } catch { /* not a DataView */ }
  return null;
}
// Strict UTF-8 decode: Node's Buffer.toString('utf8') silently maps invalid bytes to U+FFFD, so 0xFF and the real
// 3-byte U+FFFD collapse to one string. fatal:true rejects invalid UTF-8 instead (P1-01). → string | null.
function strictUtf8(bytes) {
  const b = snapshotBinary(bytes);   // round-50 P1-02 — ALWAYS through the total intrinsic door; NO `instanceof` on untrusted input (it runs a Proxy trap that would escape)
  if (b === null) return null;
  try { return new TextDecoder('utf-8', { fatal: true, ignoreBOM: false }).decode(b); }
  catch { return null; }
}
// round-19 P1-01 — ONE Unicode byte-admission (§6), shared by the discovery resolver AND the byte-checker TCB, so
// authority material has ONE canonical Unicode domain (a BOM/surrogate that the byte checker rejects must not upgrade
// a document via discovery). (1) a leading UTF-8 BOM (EF BB BF) is REJECTED, not silently stripped — TextDecoder would
// alias two distinct byte-strings to one decoded object (M-BYTE injectivity). → { text } | { err:'BOM' } | { err:'UTF8' }.
export function admitUtf8(bytes) {
  const b = snapshotBinary(bytes);   // round-50 P1-02 — ALWAYS through the total intrinsic door; no `instanceof` fast-path (a Proxy trap would escape)
  if (b === null) return { err: 'UTF8' };
  if (b.length >= 3 && b[0] === 0xEF && b[1] === 0xBB && b[2] === 0xBF) return { err: 'BOM' };
  const t = strictUtf8(b);
  return t === null ? { err: 'UTF8' } : { text: t };
}
// (2) a JSON `\uD800` escape decodes to a JS string holding an UNPAIRED UTF-16 surrogate — not a Unicode SCALAR; other
// languages' canonicalizers replace/reject it, so it breaks language-neutral canon. Checked over the PARSED tree (keys +
// values), ITERATIVE (a recursive walk overflows on deep arrays before a depth guard runs — round-14 P2-01).
const hasLoneSurrogate = (s) => {
  for (let i = 0; i < s.length; i++) {
    const c = s.charCodeAt(i);
    if (c >= 0xDC00 && c <= 0xDFFF) return true;                                                   // unpaired low
    if (c >= 0xD800 && c <= 0xDBFF) { const n = s.charCodeAt(i + 1); if (!(n >= 0xDC00 && n <= 0xDFFF)) return true; i++; }   // unpaired high
  }
  return false;
};
export function anyLoneSurrogate(root) {
  const stack = [root];
  while (stack.length) {
    const v = stack.pop();
    if (typeof v === 'string') { if (hasLoneSurrogate(v)) return true; }
    else if (Array.isArray(v)) { for (let i = 0; i < v.length; i++) stack.push(v[i]); }
    else if (v !== null && typeof v === 'object') { for (const k of Object.keys(v)) { if (hasLoneSurrogate(k)) return true; stack.push(v[k]); } }
  }
  return false;
}
// round-19 P1-02 — DEFENSIVE boundary admission (HARDENING, not a §14-domain fix). The math domain of the §14 totality
// theorem is the received document BYTES (model §I4: "a total deterministic function of `d`'s bytes"), and JSON.parse of
// bytes NEVER yields a Proxy / throwing accessor — every P1-02 repro passes an INTEGRATOR-supplied hostile object as
// opts/key-log/transport, not untrusted bytes, so this is not a reachable soundness bug. But the reference checker CLAIMS
// totality, so a caller record carrying a throwing accessor / Proxy trap maps to a STRUCTURED reject, never a host throw:
// snapshot OWN data via descriptors (getOwnPropertyDescriptor does NOT fire a getter), contained in try (a Proxy
// ownKeys/descriptor trap that throws → null). null/undefined → {} (the rev15 null-total default). → inert record | null.
function admitOpts(v) {
  if (v === undefined || v === null) return {};
  if (typeof v !== 'object') return null;
  try {
    const out = Object.create(null);                                             // round-20 P1-01 — null-proto: a JSON-native "__proto__" own key becomes plain own data, never a prototype swap
    for (const k of Reflect.ownKeys(v)) {
      if (typeof k === 'symbol') continue;                                        // string keys only (config is a string-keyed record)
      if (k === '__proto__' || k === 'prototype' || k === 'constructor') continue;   // round-20 P1-01 — never admit a key that could install inherited authority config (evidence must be an OWN admitted field of C — model ℐ_C)
      const d = Object.getOwnPropertyDescriptor(v, k);
      if (!d) continue;
      if (d.get || d.set) return null;                                            // an accessor is not an inert record field
      // rev39 R3 (round-31) — DEEP-admit each nested DATA value ONCE, closing the whole nested-untrusted-doc class at ONE
      // control: a resolver can no longer hold a live nested caller object (opts.genesis / config.checkpoint / a chain) to
      // re-read after verification. A FUNCTION is a capability (fetchImpl/substrateVerify) → preserved as-is; a
      // verifier-MINTED branded token (anchor/served/fresh/handle) → preserved by admitDeep; everything else is frozen inert.
      const val = (typeof d.value === 'function') ? d.value : admitDeep(d.value);
      if (val === ADMIT_REJECT) return null;                                      // a nested non-inert value (accessor at depth, non-plain proto, cycle) → the whole opts record is a structured reject
      Object.defineProperty(out, k, { value: val, writable: false, enumerable: true, configurable: false });   // read-only own data (round-41 P1-01 — the admitted snapshot is INERT: a preserved capability function is invoked with this record as `this`, so a writable field let a hostile resolveRef mutate `this.acceptConsumerOverride=true` and flip LIGHT→HIGH)
    }
    return Object.freeze(out);   // round-41 P1-01 (R1) — the top-level opts snapshot is FROZEN, matching admitDeep's frozen nested values: "after 𝒜, x is DEAD" means IMMUTABLE, not merely copied — nothing can mutate x̂ during verification (a capability `this`-write is now a no-op / strict-mode throw, caught fail-closed)
  } catch { return null; }
}
// round-26 (rev24 Class C+D) — the ONE untrusted-input boundary. `admitDeep` takes a DEEP inert snapshot of a caller
// object: every value is read EXACTLY ONCE at snapshot time, so a live getter cannot return one value during signature
// verification and another during handle construction (the getter-TOCTOU that minted a genuine EvidenceHandle with
// UNSIGNED facts — round-26 P0-03). An accessor at ANY depth, a non-plain prototype, a cycle, or an over-deep graph is
// NOT an inert record → ADMIT_REJECT (the caller returns a structured malformed result, never a host throw or a
// second read). For JSON-sourced input (no getters) the snapshot is value-identical, so canon(admitDeep(x)) == canon(x)
// and no verdict changes. This is admitOpts made total + recursive; it dissolves malformed-non-null (C) and TOCTOU (D)
// at the same seam. `ADMIT_REJECT` is a module-private sentinel (never null — null is a legal admitted value).
const ADMIT_REJECT = Symbol('admit-reject');
// round-27 (canon-exactness, self-audit): admitDeep must be BYTE-TRANSPARENT to canon — for any x, `canon(admitDeep(x))`
// is the SAME bytes as `canon(x)`, and admitDeep REJECTS exactly what canon THROWS on — else the snapshot silently flips
// a verdict. canon throws on a function/symbol value, so admitDeep REJECTS it (an earlier DROP diverged). canon has NO
// depth cap (it recurses to the stack limit, bounded by the §13 transport size); so admitDeep has NONE either — an earlier
// depth-64 cap FALSE-REJECTED a valid deep `class:data` doc that canon accepts. Cycles are refused by the `seen` visited
// set; a pathological non-cyclic depth overflows the stack in BOTH canon and admitDeep and is caught as a structured reject.
export const admitDeep = (v, seen = new WeakSet()) => {   // THE input-boundary primitive — exported so its canon-transparency is TESTABLE (and a consumer can admit its own untrusted object)
  if (v === null) return null;
  const t = typeof v;
  if (t === 'function' || t === 'symbol') return ADMIT_REJECT;                         // canon throws on a function/symbol value → so do we (canon-exact); the only helper-carrying input (a keylog `prove` closure) is a BUILDER field, never passed as signed proof data
  if (t !== 'object') return v;                                                        // primitive scalar passes through
  if (HANDLE_BRAND.has(v) || VERIFIED_ANCHOR.has(v) || VERIFIED_SERVED.has(v) || VERIFIED_FRESH.has(v) || VERIFIED_MAP_ROOT.has(v)) return v;   // #42 — a minted token is preserved BY IDENTITY: cloning it here would strip the very unforgeability it exists for, silently, and the branch would simply never be taken   // round-27/31 — a verifier-MINTED brand (a verified handle, or an anchor/served/fresh token) is ALREADY a trusted inert snapshot: pass it through, never deep-copy off the brand (a copy loses the WeakSet identity the resolvers check). A caller cannot forge a brand, so this is sound; it lets admitOpts DEEP-admit nested data (round-31) without stripping branded tokens.
  if (seen.has(v)) return ADMIT_REJECT;                                                // cycle → refuse (total, never infinite); non-cyclic depth is unbounded, matching canon
  seen.add(v);
  try {
    if (Array.isArray(v)) {
      const out = new Array(v.length);                                                // round-28 P1-01 — PRESERVE holes + length (canon uses `.map`, which SKIPS holes); densifying to `undefined` diverged
      for (let i = 0; i < v.length; i++) {
        if (!(i in v)) continue;                                                       // a HOLE — leave it a hole (canon's `.map` skips it); do not densify
        const d = Object.getOwnPropertyDescriptor(v, i);
        if (d.get || d.set) return ADMIT_REJECT;                                       // no accessor elements (TOCTOU)
        const r = admitDeep(v[i], seen);
        if (r === ADMIT_REJECT) return ADMIT_REJECT;
        out[i] = r;
      }
      return Object.freeze(out);                                                       // non-index own props are ignored, exactly as canon's `.map` ignores them
    }
    const proto = Object.getPrototypeOf(v);
    if (proto !== Object.prototype && proto !== null) return ADMIT_REJECT;             // plain / null-proto ONLY (Date/Map/Set/class instances rejected — fail-closed per round-28 div2: stricter-than-canon means REJECT the whole input, never accept-and-rewrite)
    const out = Object.create(null);
    // round-28 P0-01 — the EXACT canon key domain: `Object.keys` (ENUMERABLE OWN STRING keys). This DROPS non-enumerable
    // keys and symbols (canon does too) and — critically — INCLUDES `__proto__`/`constructor`/`prototype` as OWN DATA
    // (dropping them let an attacker attach an unsigned member under those names and still get VALID — a false accept).
    // On a null-proto target these names are plain data via defineProperty; the verifier's exact-key grammar then REJECTS
    // the extra member (E-MALFORMED), never erases it. admitDeep must be NEVER LOOSER than canon.
    for (const k of Object.keys(v)) {
      const d = Object.getOwnPropertyDescriptor(v, k);
      if (d.get || d.set) return ADMIT_REJECT;                                         // a DECLARED accessor at ANY depth → not inert (round-26 TOCTOU closure)
      const r = admitDeep(v[k], seen);                                                 // rev32 R3 — read the VALUE through canon's OWN channel ([[Get]] / v[k]), NOT descriptor.value: a Proxy answering the descriptor one value and [[Get]] another must have its [[Get]] face (the one canon/contentHash read) snapshotted and verified, so a tampered get-face fails the signature instead of a signed descriptor-face passing (round-29 P0-01)
      if (r === ADMIT_REJECT) return ADMIT_REJECT;
      Object.defineProperty(out, k, { value: r, enumerable: true });                  // read-only own data (safe for __proto__ on a null-proto object)
    }
    return Object.freeze(out);
  } catch { return ADMIT_REJECT; }
  finally { seen.delete(v); }                                                          // a DAG (same object in sibling positions) is fine; only a true cycle is refused
};
// a caller-supplied array (key-log): a NATIVE array only; snapshot inside try so a Proxy length/index trap → null.
// null/undefined → [] (default). → inert array | null.
function admitArray(v) {
  if (v === undefined || v === null) return [];
  try {   // round-51 — `Array.isArray` THROWS on a REVOKED Proxy; it must be INSIDE the try (the type-probe runs caller-reachable ES semantics, like instanceof/getPrototypeOf — round-50/51 totality class)
    if (!Array.isArray(v)) return null;
    const out = []; const n = v.length; for (let i = 0; i < n; i++) { const el = admitDeep(v[i]); if (el === ADMIT_REJECT) return null; out[i] = el; } return Object.freeze(out);
  } catch { return null; }
}   // round-42 P0-01 (R1/R3) — DEEP-admit each element into a frozen snapshot (was a shallow ref-copy): the resolveKeys reducer verifies verify(e) then RE-READS e.state/e.sig/contentHash(e), so a two-face key-log entry showed a SIGNED key B to verify and an UNSIGNED key C to the re-reads → authorized C. One frozen graph, one face.
// round-42 P1-02 (R1) — a STRICT optional-boolean admission for GRANTING policy coordinates (acceptConsumerOverride /
// noForkConfirmed / corroborated): undefined/null → false (absent/default); a real boolean → itself; ANY other present
// value → ADMIT_REJECT (a wrong-typed grant like the string "false" is truthy and would activate the override — malformed, not truthy).
const admitBool = (x, dflt = false) => (x === undefined || x === null) ? dflt : (typeof x === 'boolean' ? x : ADMIT_REJECT);   // round-43 — per-field DEFAULT (requirePerFrameValid defaults true; the grants default false): a wrong-typed present value is always ADMIT_REJECT
// round-43 P1-02 — the ONE optional authority-SELECTOR admission: undefined/null → absent (a sentinel); a present array is DEEP-admitted
// (frozen elements, never a live two-face element); anything else (a falsy scalar / non-array) → ADMIT_REJECT. Replaces every
// `Array.isArray(x) ? x : []` coalesce that silently turned a present malformed key-log into an empty (retirement-erasing) log.
const ABSENT_SELECTOR = Symbol('absent-selector');
const admitOptionalKeylog = (x) => { if (x === undefined || x === null) return ABSENT_SELECTOR; try { return Array.isArray(x) ? admitArray(x) : null; } catch { return null; } };   // round-52 S1 — Array.isArray THROWS on a revoked Proxy; total door (the class round-51 closed at admitArray/reducePackage)

// ─── producer: §7 seal — sign canon({ust,state}) with an Ed25519 private key ─────────────────────────
// DETACHED SIGNING — assemble a transcript from a signature made ELSEWHERE (#178).
//
// `seal` does two things: it signs, and it assembles. A caller that cannot hand over a private key can still do
// the second half, and several already do — an agent over MCP receives `signing_input`, signs with its own key
// and assembles by hand; `ust-web-signer` assembles by hand too, taking `key_id` from the STATE while `pub` comes
// from the signer, so the two can disagree and the document fails E-SIG at a reader. Three hand-written
// assemblers, three ways to mismatch the same two fields.
//
// `key_id` is DERIVED from `pub` here, never accepted: §12.2 defines it as `H("ust:keylog", raw_pub)`, so a
// caller supplying both is being asked to restate something computable, and the only thing that can come of it
// is disagreement. One class of malformed document stops being expressible.
//
// AND IT VERIFIES WHAT IT ASSEMBLED before returning it. That is not belt-and-braces: the whole reason to bring
// assembly inside is that the surface handing out `signing_input` today takes nothing back, so there is no place
// where "the parts you put together actually verify" is ever checked. An agent that skips its own verification
// publishes a document every reader refuses, and hears about it from the reader or never.
export function attachSignature(state, { pub, sig, alg = 'Ed25519' }) {
  if (typeof pub !== 'string' || typeof sig !== 'string') throw err('E-MALFORMED', 'attachSignature needs a base64url `pub` and `sig`');
  const doc = { ust: '1.0', state, sig: { alg, key_id: keyId(pub), pub, sig } };   // §19 wire, as `seal` writes it
  const v = verify(doc, { context: 'data' });
  if (!isValid(v) && v.error === 'E-SIG')
    throw err('E-SIG', 'the signature does not verify against this state and key — refusing to return a document a reader would reject; sign `signedContent({ust,state})`, not the state alone');
  return doc;
}

export function seal(state, privKeyObj, pubB64url) {
  // final protocol-law check (rc.12): a sealed transcript may NEVER exceed the ABS — fail closed
  // at the producer, not only at verifiers.
  {
    const sBytes = utf8Len(canon({ ust: '1.0', state }));
    if (sBytes > BOUNDS.sizeBytes) throw Object.assign(new Error(`E-BOUNDS: canonical transcript ${sBytes} B > ABS ${BOUNDS.sizeBytes}`), { code: 'E-BOUNDS' });
  }
  const doc = { ust: '1.0', state };
  const sig = toB64url(ed25519Sign(utf8(signedContent(doc)), privKeyObj));
  return { ust: '1.0', state, sig: { alg: 'Ed25519', key_id: state.id.key_id, pub: pubB64url, sig } };
}

// ─── producer helpers: assemble a State with the per-partition `hashes` auto-computed (§4.4). `seal` signs it.
//     `id` = {domain_shard, ust_id, key_id[, parent_ust]}. `data` = {name: {kind,value} | {kind,privacy,commit,enc?}}.
export function buildState(id, time, data, provenance, opts) {
  // §13 capacity guard (rc.10): a producer never SILENTLY emits past its capacity. The anonymous
  // floor is 64; pass {maxPartitions} matching your genesis declaration to go higher (ABS 4096).
  const n = Object.keys(data).length;
  const cap = Math.min(Number(opts?.maxPartitions ?? BOUNDS.floorPartitions), BOUNDS.partitions);
  if (n > cap) throw Object.assign(
    new Error(`E-BOUNDS: ${n} partitions > ${cap} — declare {maxPartitions} matching your genesis max_partitions (§12.1; ABS ${BOUNDS.partitions})`),
    { code: 'E-BOUNDS' });
  const hashes = {};
  for (const [name, part] of Object.entries(data))
    hashes[name] = part.commit !== undefined ? partitionHash({ commit: part.commit })
      : partitionHash({ domain_shard: id.domain_shard, ust_id: id.ust_id, name, value: part.value });
  const state = { id, time, data, hashes };
  if (provenance) state.provenance = provenance;
  // #69 E2 — the EXACT normative metric: UTF-8 bytes of the signed content canon({ust, state}). The sig is
  // NOT signed content, so no envelope pad (the old +300 over-counted → false rejects near the limit). This
  // is byte-identical to what seal()'s final ABS guard and the verifier's checkBounds measure — one metric.
  const stBytes = utf8Len(signedContent({ ust: '1.0', state }));
  const capB = Math.min(Number(opts?.maxTranscriptBytes ?? BOUNDS.floorSizeBytes), BOUNDS.sizeBytes);
  if (stBytes > capB) throw Object.assign(
    new Error(`E-BOUNDS: signed content ${stBytes} B > ${capB} — declare {maxTranscriptBytes} matching your genesis max_transcript_bytes (§12.1; ABS ${BOUNDS.sizeBytes})`),
    { code: 'E-BOUNDS' });
  return state;
}
export const buildAttestation = (id, time, data, constituents, prev) =>            // §9.2 constituents + Merkle root
  buildState({ ...id, class: 'attestation' }, time, data, { constituents, root: merkleRoot(constituents), ...(prev !== undefined ? { prev } : {}) });
export const buildDerivation = (id, time, data, basedOn, prev) =>                  // §9.3/§9.4 based_on + seed
  buildState({ ...id, class: 'derivation' }, time, data, { based_on: basedOn.map((b) => ({ hash: b.hash })), seed: seed(basedOn.map(b => b.hash)), ...(prev !== undefined ? { prev } : {}) });   // round-54 (UST-0q7) — PRODUCER-FORWARD: emit HASH-ONLY based_on; a `url` in the signed provenance is a swappable location hint that lends false authority, so producers stop emitting it (location → unsigned discovery §20.1). Verifiers still IGNORE any url present, so existing docs stay valid — NOT invalidated.
export const buildGenesis = (id, time, pub, maxPartitions, maxTranscriptBytes, cadence, checkpointAuthority, recovery, roles) => {  // §12.1 self-signed name-binding root
  // AN EMPTY `roles` IS AN EXPRESSED INTENT, NOT AN ABSENT ONE (rev92). Dropping it, as the spread below does,
  // is strictly better than writing it — a signed `roles: []` declares nothing per §12.1 yet `resolveKeys`
  // refuses it as E-GENESIS, i.e. a publishable identity from which no consumer can ever resolve a key. But
  // dropping it SILENTLY is its own defect, measured: a ceremony passing `[]` produces a genesis with no
  // separation while the operator believes they declared it — on a field that cannot be changed without
  // re-rooting. `undefined` means "not declaring" and stays silent; `[]` means "declaring nothing", which is
  // not a state this protocol has. The CLI already refuses it; a direct library caller had only the silence.
  if (roles !== undefined && roles !== null && !roles.length)
    throw err('E-GENESIS', 'roles is present but EMPTY — omit the argument to publish without role separation; an empty array declares nothing, and this protocol has no such state (§12.1, UST#131)');
  return buildState({ ...id, class: 'genesis' }, time, { genesis: { kind: 'captured', value: {
    pub, role: 'name-binding-root',
    ...(maxPartitions !== undefined ? { max_partitions: String(maxPartitions) } : {}),           // §13 ladder (≠ ceiling; ABS 4096)
    ...(maxTranscriptBytes !== undefined ? { max_transcript_bytes: String(maxTranscriptBytes) } : {}), // §13 ladder (≠ ceiling; ABS 64 MiB)
    ...(cadence !== undefined ? { cadence: String(cadence) } : {}),                               // §11.3 C — SIGNED cadence (sec) → the expected grid; resolved not free-chosen (#69 C)
    // §12.2 — role separation is a DECLARED refinement: presence of a non-empty `roles` turns it on for THIS
    // publisher. Absent, the key set stays undifferentiated and nothing about an existing publisher changes —
    // §11.3's continuity law forbids an operator change, including this one, from invalidating issued data.
    // §12.1: "a genesis whose value carries a NON-EMPTY `roles` array DECLARES role separation" — so non-empty is
    // what decides, and `!== undefined` was the wrong predicate in two directions. `null` reached the spread and
    // threw a raw TypeError (a builder must not host-throw), and `[]` was WRITTEN into the signed genesis, where it
    // declares nothing per the spec yet is refused by `resolveKeys` as E-GENESIS. That second one is the dangerous
    // half: the document VERIFIES on its own, so a ceremony could sign and publish an identity from which no
    // consumer can ever resolve a key. The spec already decided this; the builder now says the same thing.
    ...(roles?.length ? { roles: [...roles].map(String) } : {}),
    // P1-04 — the genesis CARRIES its own checkpoint-authority + recovery set, so `authority_root` is RESOLVED from
    // the signed genesis, not a raw caller pin. `recovery.keys` is a {key_id: pub} map (each key_id = H(ust:keylog, pub)).
    ...(checkpointAuthority ? { checkpoint_authority: { key_id: checkpointAuthority.key_id, pub: checkpointAuthority.pub } } : {}),
    ...(recovery ? { recovery: { keys: recovery.keys, threshold: String(recovery.threshold) } } : {}),
  } } });
};
// P1-04 — resolve the checkpoint-authority + recovery roots FROM the signed genesis value (typed, key_id = keyId(pub)
// validated), never from a raw caller option. Returns {genesisAuthority?, recoveryKeys?, recoveryThreshold?} or null.
export function resolveCheckpointRoots(genesis) {
  // P0-2 (rc.35 audit) — roots are extracted ONLY from a VERIFIED, self-signed `class:"genesis"` document. Without this,
  // a raw unsigned object {state:{data:{genesis:{value:{checkpoint_authority}}}}} installed an ATTACKER checkpoint root
  // (authority_root:"genesis") and derived corroborated/attested freshness. Verify the doc BEFORE trusting its fields.
  const G = admitDeep(genesis); if (G === ADMIT_REJECT) return null;                   // round-27 P0-01 — snapshot ONCE: verify(G) and the .value extraction below read the SAME bytes (a getter can't return the signed authority during verify and an attacker authority on the extraction read)
  if (G?.state?.id?.class !== 'genesis' || !isValid(verify(G, { context: 'key' }))) return null;
  const gv = G?.state?.data?.genesis?.value; if (!gv) return null;
  const out = {};
  const ca = gv.checkpoint_authority;
  if (admitAuthorityKey(ca)) out.genesisAuthority = { key_id: ca.key_id, pub: ca.pub };   // round-36 — the ONE authority-key admission (exact { key_id, pub }, strict Pub32, key_id === keyId(pub))
  const rec = gv.recovery;
  if (rec && rec.keys && typeof rec.keys === 'object') {
    const keys = {}; let ok = true;
    for (const [kid, pub] of Object.entries(rec.keys)) { if (!admitAuthorityKey({ key_id: kid, pub })) ok = false; else keys[kid] = pub; }   // round-36 — each genesis recovery key is a usable { key_id === keyId(pub) } pair via the ONE primitive
    if (ok && Object.keys(keys).length) { const t = canonUint(rec.threshold); if (t !== undefined && t >= 1) { out.recoveryKeys = keys; out.recoveryThreshold = t; } }   // round-24 P0-02 — canonical decimal threshold ≥1 only; a coercible non-string (`["1"]`) no longer lowers the takeover threshold
  }
  return out;
}
// M2 (rc.35 refactor, UST-985) — the canonical genesis_epoch is DERIVED from the active genesis; the publisher can never
// choose the uniqueness namespace (epoch-split). Everything downstream reads this, never a body field.
export const genesisEpoch = (activeGenesis) => H('ust:genesis-epoch', activeGenesis);
// K2 (rc.37) — the ONE authority namespace is a function of the WHOLE verified genesis: scope_id =
// H('ust:authority-scope', contentHash(g)). Since active_genesis = contentHash(g), a single input suffices, and it
// binds domain + genesis key + checkpoint authority + recovery + capacity + cadence at once — nothing downstream can
// pick a namespace by choosing domain/epoch, because they are not in the preimage (and not transmitted). The old
// canon({domain, active_genesis, genesis_epoch}) triple was redundant (all three functions of contentHash) and
// weaker (bound 3 fields). `genesis_epoch` survives only as diagnostic metadata / the legacy wire field.
export const authorityScopeId = (activeGenesis) => H('ust:authority-scope', activeGenesis);

// ─── K3 (rc.37, UST-znh) — OPAQUE HANDLE REGISTRY. The topology rule: no public API past the first level accepts a
//     caller-NAMED `Verified*` object. A handle is a frozen value MINTED only inside this module and registered in a
//     module-private brand; downstream requires the brand, so a caller cannot construct one, serialize it, or rebuild
//     it from JSON. Four kinds: 'genesis' (VerifiedAuthorityContext), 'chain' (verified checkpoint chain +
//     PinnedCheckpointState), 'evidence' (image of VerifyEvidence_C), 'predicate-graph' (the proven atoms the
//     assembler reads). The round-2 WeakSet brand generalized to EVERY verified object — the runtime witness of
//     image-membership. `isVerifiedHandle(kind, x)` is the ONLY exported reader (a boolean; never a constructor).
const HANDLE_BRAND = new WeakSet(), HANDLE_KIND = new WeakMap();
// round-24 P0-01 — DEEP-freeze before branding. `Object.freeze` froze only the outer shell, so a caller holding a genuine
// branded handle could mutate a NESTED authority field (checkpoint_authority / recoveryKeys / authority_for_next) AFTER
// verification and have the same brand authorize attacker-signed checkpoints. The brand must prove the CONSUMED values are
// the verified values — freeze the whole tree (the isFrozen guard makes it terminating + cycle-safe).
// round-25 P0-03 — visited-set by WeakSet, NOT `Object.isFrozen`: a container that was ALREADY shallow-frozen (e.g.
// verifyEvidenceReceipt's `Object.freeze({...facts})`) must NOT stop the recursion, or its nested values stay mutable and
// a caller mutates a genuine EvidenceHandle after verification. Traverse every own value regardless; WeakSet is cycle-safe.
const deepFreeze = (o, seen = new WeakSet()) => { if (!o || typeof o !== 'object' || seen.has(o)) return o; seen.add(o); for (const k of Object.keys(o)) deepFreeze(o[k], seen); return Object.freeze(o); };
const mintHandle = (kind, obj) => { const h = deepFreeze(obj); HANDLE_BRAND.add(h); HANDLE_KIND.set(h, kind); return h; };
const isHandle = (kind, x) => x !== null && typeof x === 'object' && HANDLE_BRAND.has(x) && HANDLE_KIND.get(x) === kind;
export const isVerifiedHandle = (kind, x) => isHandle(kind, x);   // consumers may TEST provenance; they can never MINT it

// M2 — the SOLE producer of an authority SCOPE. verifyGenesis (§2.1 design record): verify the doc, then DERIVE the
// immutable scope {domain, active_genesis, genesis_epoch, scope_id}. Returns null iff the genesis does not verify as a
// self-signed class:"genesis"; otherwise the context every checkpoint/uniqueness/recovery predicate is a function of.
// K3: the returned context is a BRANDED, frozen GenesisHandle — the chain verifier requires the brand (a caller-shaped
// look-alike is rejected, closing round-3 P0-1).
export function verifiedGenesisContext(genesis) {
  const G = admitDeep(genesis); if (G === ADMIT_REJECT) return null;               // round-26 (rev24 D) — snapshot ONCE; the sig-verified genesis and the hashed/scoped genesis are the SAME bytes (no getter divergence)
  const roots = resolveCheckpointRoots(G);                                         // P0-2: verifies class:genesis + self-sig
  if (!roots) return null;
  const active_genesis = contentHash(G), domain = G.state?.id?.domain_shard;
  const genesis_epoch = genesisEpoch(active_genesis);                                // diagnostic / legacy wire only
  const scope_id = authorityScopeId(active_genesis);                                 // K2: scope binds the whole genesis
  return mintHandle('genesis', { scope_id, domain, active_genesis, genesis_epoch,
    checkpoint_authority: roots.genesisAuthority,
    ...(roots.recoveryKeys ? { recoveryKeys: roots.recoveryKeys, recoveryThreshold: roots.recoveryThreshold } : {}) });
}
export const buildKeyLogEntry = (id, time, keyOp, prev) =>                         // §12.2 add|rotate|revoke
  buildState({ ...id, class: 'key' }, time, { key_op: { kind: 'captured', value: keyOp } }, { prev });
// §11.3 M5 — a STREAM checkpoint: a point in the DATA chain. The name says which chain it belongs to because its
// sibling `buildAuthorityCheckpoint` builds a point in the KEY chain, and `checkpoint` alone names both (#112).
// `buildCheckpoint` stays as an alias below: a published export is not renamed away, it is joined.
export const buildStreamCheckpoint = (id, time, head, frameCount, prev, interval) =>   // (interval = {from,to} for completeness, #69 C)
  buildState({ ...id, class: 'attestation' }, time, { checkpoint: { kind: 'computed', value: { head, frame_count: String(frameCount), ...(interval ? { from: interval.from, to: interval.to } : {}) } } }, { prev });
// DEPRECATED NAME, kept because removing a published export is a compatibility event and #112 chose the additive
// form: identical function, and the qualified name above is the one to write in new code.
export const buildCheckpoint = buildStreamCheckpoint;

export const buildGap = (id, time, prev, reason) =>                               // §11.3 C2 — a signed gap record: this slot (id.ust_id) is HONESTLY absent
  buildState({ ...id, class: 'attestation' }, time, { gap: { kind: 'computed', value: { reason: reason || 'no-frame' } } }, { prev });
// §11.3 / F.5u — a BATCH COMMITMENT: this key committed `root` to `anchor.substrate` over the window
// `[anchor.from, anchor.to]`. It is the third prev-only subtype and the only one that carries a root, because
// the batch's members are NOT enumerated: membership is an §11.2 AnchorProof and nothing else. No count is
// taken — a size claim in a commitment is refutable only against an honest publisher (F.5u.4).
export const buildAnchorCommitment = (id, time, root, prev, anchor) =>
  buildState({ ...id, class: 'attestation' }, time, { anchor: { kind: 'computed', value: { substrate: anchor.substrate, from: anchor.from, to: anchor.to } } }, { prev, root });
export const buildAbsence = (id, time, name, reason, extra = {}, prev) =>         // §4.4 #39 — a NEGATIVE observation: partition `name` asserts non-occurrence/unavailability (reason unreachable|no-event|unchanged); `extra` MAY carry {from,to} (the window a no-event covers) / subject
  buildState({ ...id, class: 'observation' }, time, { [name]: { kind: 'absence', value: { reason, ...extra } } }, prev ? { prev } : undefined);
export const buildCadenceEntry = (id, time, cadence, effectiveFrom, prev) =>      // §11.3 continuity — a signed cadence CHANGE (key-log pattern); resolved at a slot's time
  buildState({ ...id, class: 'cadence' }, time, { cadence_op: { kind: 'computed', value: { cadence: String(cadence), effective_from: effectiveFrom } } }, { prev });

// ─── reserved-key sets (§3/§4.2/§17) ─────────────────────────────────────────────────────────────────
const RESERVED = { transcript: ['ust','state','sig','proof'], state: ['id','time','data','hashes','provenance'],
  id: ['domain_shard','ust_id','key_id','class','parent_ust'], envelope: ['kind','value','privacy','commit','enc'],
  provenance: ['sources','constituents','based_on','root','seed','prev'], sig: ['alg','key_id','pub','sig'] };
// §17: "Reserved names MUST NOT be used as partition or source names" — the FULL registry, every level
// (the 11th audit found the old set enforced only state+id keys — a spec-impl mismatch).
const RES_PARTITION_NAMES = new Set([...RESERVED.transcript, ...RESERVED.state, ...RESERVED.id,
  ...RESERVED.envelope, ...RESERVED.provenance, ...RESERVED.sig, 'partition', 'nonce', '__proto__', 'constructor', 'prototype']);
// §4.4 partition kinds: captured (observed), computed (derived), absence (#39 — a NORMATIVE NEGATIVE: 'source
// unreachable' / 'no-event' / 'value unchanged'). `absence` is machine-distinguishable from a captured-empty reading
// and from a not-published transcript. ABSENCE_REASONS is a RECOMMENDED (not closed) set — the value MUST carry a
// non-empty `reason`; publishers MAY use others, but a consumer branches on these three.
const KINDS = ['captured', 'computed', 'absence'], PRIVACY = ['blinded', 'encrypted'];   // §S4/D1: secret-url is a disclosure CHANNEL (§out-of-scope), not a privacy mode
const ABSENCE_REASONS = ['unreachable', 'no-event', 'unchanged'];
// §17 AEAD registry, and the nonce width each algorithm carries as the PREFIX of `ct` (`nonce ‖ body ‖ tag`).
// The two are one object, so a registry entry without a stated nonce width is not expressible: the widths differ
// (96-bit for GCM, 192-bit for XChaCha — the whole reason §10 calls the latter misuse-resistant), and a reader
// that assumed one width for both would slice the ciphertext wrong and report the DOCUMENT as unauthentic.
// The widths are STRINGS because the registry is canonicalized into `registry_digest`, and §5's value model is
// string-only. Writing them as numbers is not a style question: it made `canon` refuse the registry outright and
// every document verified INVALID(E-CANON) until they were strings — the protocol catching its own registry with
// the rule it applies to everyone else's data. Read once, here, and never re-parsed downstream.
const AEAD_NONCE_BYTES = { 'AES-256-GCM': '12', 'XChaCha20-Poly1305': '24' };
const nonceWidth = (alg) => Number(AEAD_NONCE_BYTES[alg]) || 0;
const AEAD_ALGS = Object.keys(AEAD_NONCE_BYTES), B64URL = /^[A-Za-z0-9_-]+$/;
// the verdict is tier-scoped (`VALID:LIGHT|HIGH|TOP`); this is the ONE place code should test "did it verify" —
// a bare `r.result === 'VALID'` is intentionally no longer valid (it forces callers to face the tier).
export const isValid = (r) => typeof r?.result === 'string' && r.result.slice(0, 6) === 'VALID:';
/**
 * #144 — "I could not run the check" is a THIRD reading of a verdict, and every consumer of one must face it.
 *
 * `isValid(v)` answers "did it verify"; its negation lumps together two things that must never be lumped:
 * a document that FAILED and a verifier that could not decide. Measured 2026-08-09 across the 16 internal
 * consumers of `verify()`: seven turned inability into an accusation (`E-GENESIS`, `E-KEY`, `E-SIG`, `null`),
 * and one — the stream's per-frame integrity step — tested `v.error`, which `INDETERMINATE` does not carry, so
 * a build that checked NO signature had its frames counted as intact. That direction is the worse one: an
 * accusation is at least visible, a silent confirmation is what a forgery would want.
 * CLOSED 2026-08-09 (round 189): all sixteen consumers route inability through this predicate, and the vector
 * `faculty-absent-ed25519` pins both halves — with the faculty and without it.
 *
 * NOT every `INDETERMINATE` is this. `unavailable` means the network could not bind a name while the signature
 * WAS checked — integrity holds, only the tier is denied. This predicate names the narrow case where the
 * verifier lacked the faculty itself.
 */
export const cannotDecide = (r) => r?.result === 'INDETERMINATE' && r?.reason === 'unsupported_alg';
const CLASSES = ['observation','attestation','derivation','genesis','key','cadence'];
// §14.5 / §F.5e.4 — the verification ROLE is a PARTITION of CLASSES: the trust layer's OWN documents on one side,
// a publisher's claims about the world on the other. Both refusals read this ONE set, so they cannot drift apart.
// They did: `data` refused these three while `key` checked nothing, so a key-form `class:"observation"` verified
// VALID:LIGHT in the key role and the shared served-log reader took it as a log entry (round 78). A partition
// enforced on one side is not a partition — the class simply has two homes and its role is decided by whichever
// caller reached it first. `cadence` sits here because the cadence log is read exactly as the key log is (§11.3).
const AUTHORITY_CLASSES = new Set(['genesis', 'key', 'cadence']);
// §12.2 / F.5e.1 — THE NORMATIVE CLASS-SET PER OPERATING ROLE, which is what makes `admits(k, c)` real.
//
// Until rev89 the model defined `admits(k, c)` as "`k ∈ active(r)` for some `r` whose normative class-set
// contains `c`" and NO class-set was ever stated, so the predicate could not be evaluated and no verification
// path called it. Three of the five roles authorized (the ceremony-set ones are enforced where their objects
// are verified) and the two an operator actually assigns authorized nothing — one word, two mechanisms.
//
// WHY THIS CUT. An issued document ATTESTS to what the publisher observed; it does not CREATE the observation.
// So an `issuance` key may sign an `attestation` — a receipt is one — and may never mint a primary
// `observation` or a `derivation`. That is the containment the role exists for: a leaked issuance key cannot
// fabricate what the publisher saw. `data` carries the publisher's stream in all its shapes.
//
// The class-sets are NOT disjoint, and need not be: F.5e.1 partitions the KEY set (`active : Role ⇀ 𝒫(Keys)`,
// one role per key), not the class space. `issuance ⊊ data` is a hierarchy of capability over a partition of
// keys, which is exactly how the ceremony roles already read.
export const ROLE_CLASSES = Object.freeze({
  // The ROOT is in this table too (rev91), because a closed vocabulary with one member left without a stated
  // meaning has the identical defect for that member as an open field would for all of them (F.7c): "what did
  // this signature mean" becomes a question addressed to the publisher. Its set is its FUNCTION — bind the name
  // and authorize the log — which is the key context, and nothing else.
  //
  // This is not containment and does not pretend to be: a compromised root simply adds itself a `data` key.
  // What it buys is that the addition leaves a SIGNED, CHAINED entry in the public key log, so the quiet act
  // becomes a loud one. And it makes the declaration honest — a publisher that says "I separate my keys" now
  // says it about every key including the strongest, instead of about everyone but itself.
  'name-binding-root': Object.freeze(['genesis', 'key', 'cadence']),
  data: Object.freeze(['observation', 'derivation', 'attestation']),
  issuance: Object.freeze(['attestation']),
});
// §11.3 C2 — the prev-only attestation subtypes. The subtype is the NAMED DATA PARTITION, never a shape, so
// this list IS the vocabulary a verifier reads: `checkpoint` asserts a stream interval, `gap` asserts a
// declared absence, `anchor` asserts a batch root committed to a substrate (rev84, F.5u). Ordered, frozen and
// exported because a consumer that must decide "what did this signature mean" may not be answered by the
// publisher — an open subtype field would make the meaning of a document a question for its author.
export const PREV_ONLY_SUBTYPES = Object.freeze(['checkpoint', 'gap', 'anchor']);
// §12.2/§17/§F.5e.1 — the key-ROLE vocabulary is FIXED at five, and split by WELL-FOUNDEDNESS, not preference:
// a role whose keys AUTHORIZE the key log cannot be assigned BY the log (the log's authority derives from them,
// so it would be circular), while a role whose keys merely OPERATE under it can. Hence three ceremony-set and
// two log-assigned. Free-form names are refused because a CONSUMER reads the role: an open field would make
// "what does this signature mean" a question for the publisher rather than for the protocol.
const AUTHORIZING_ROLES = new Set(['name-binding-root', 'checkpoint-recovery', 'authority-checkpoint']);   // ceremony only — QUALIFIED: each names the chain its key serves (the bare words name two mechanisms each, §F.5e.3)
const OPERATING_ROLES = new Set(['data', 'issuance']);                                 // key log only
const DATA_CLASSES = CLASSES.filter((c) => !AUTHORITY_CLASSES.has(c));   // the complement, never a second hand-list
// §6 pinned RFC3339 UTC-Z with VALID RANGES — month 01-12, day 01-31, hour 00-23, min/sec 00-59.
// Rejects leap seconds (:60) and out-of-range (:99, hour 99) so two conforming verifiers ALWAYS agree (I4).
// Publishers MUST smear leap seconds to :59 (there is no representable :60).
const TS = /^\d{4}-(0[1-9]|1[0-2])-(0[1-9]|[12]\d|3[01])T([01]\d|2[0-3]):[0-5]\d:[0-5]\dZ$/;
// §8 ust_id = ust:YYYYMMDD.HH[MM[SS]] as a VALID UTC frame — month 01-12, day 01-31, hour 00-23, min/sec 00-59 (F8).
const USTID = /^ust:\d{4}(0[1-9]|1[0-2])(0[1-9]|[12]\d|3[01])\.([01]\d|2[0-3])(([0-5]\d)([0-5]\d)?)?$/;
// §14.5 semantic-consistency: a date must exist on the REAL calendar (Feb 31 / Apr 31 pass the range regex but are
// not dates). Round-trip through Date.UTC and require the components to survive — deterministic on every engine.
function calendarValid(y, mo, d) {
  const t = new Date(Date.UTC(+y, +mo - 1, +d));
  return t.getUTCFullYear() === +y && t.getUTCMonth() === +mo - 1 && t.getUTCDate() === +d;
}
const tsCalendarOk = (ts) => calendarValid(ts.slice(0, 4), ts.slice(5, 7), ts.slice(8, 10));
const ustIdCalendarOk = (u) => calendarValid(u.slice(4, 8), u.slice(8, 10), u.slice(10, 12));
// §4/§12 typed identity namespace: `domain_shard` is a NAME (dns) or a self-certifying KEY-ID. A key-form shard
// MUST equal `state.id.key_id` — the identity IS the signing key, and that equality is a checked obligation.
const KEYID_FORM = /^sha256:[0-9a-f]{64}$/;

// ─── §13 structural bounds — hard ceilings; exceed ⇒ E-BOUNDS ─────────────────────────────────────────
const BOUNDS = { depth: 8, array: 4096, partitions: 4096, floorPartitions: 64, breadth: 64, sizeBytes: 67108864, floorSizeBytes: 1048576, cadenceMax: 31622400,  // cadenceMax = 366 d in seconds (#75: bounded integer cadence)
  witnessEntries: 256, witnessActive: 16, anchorsPerGenesis: 8,  // round-20 P1-02 — F.9 structural fan-out budget: a body under the byte ceiling still bounds connector CALLS (≤ witnessActive × anchorsPerGenesis substrate invocations per resolution)
  forkCandidates: 64 };  // round-21 P1-02 — forkChoice candidate budget (a plain JSON array of N copies must not fan out into N verifications)
export function checkBounds(rawDoc) {
  const doc = admitDeep(rawDoc);                                              // round-39 P1-02 (R1) — the EXPORTED bounds validator admits its UNTRUSTED doc ONCE: a hostile accessor is a RETURNED bounds refusal, never a host throw. The internal verifyCore caller already holds an admitted snapshot → calls boundsOf directly (no re-clone on the hot path).
  if (doc === ADMIT_REJECT) return 'non-inert document (accessor/getter at some depth — bounds undecidable, §14 admission)';
  return boundsOf(doc);
}
function boundsOf(doc) {
  // #69 E3 — the normative VOLUME metric is the signed content canon({ust, state}), NOT the transport object:
  // sig/proof are bounded by transport admission (verifyJson maxInputBytes), never by the signed-content ABS.
  // Counting a large anchor proof here would falsely reject a valid signed content near 64 MiB.
  let signedBytes; try { signedBytes = utf8Len(signedContent(doc)); } catch { signedBytes = utf8Len(JSON.stringify(doc)); }
  if (signedBytes > BOUNDS.sizeBytes) return 'signed content > 64 MiB';
  if (doc.state?.data && Object.keys(doc.state.data).length > BOUNDS.partitions) return 'partitions > 4096';
  let bad = null;
  (function walk(v, d) {
    if (bad) return;
    if (d > BOUNDS.depth) { bad = 'depth > 8'; return; }
    if (Array.isArray(v)) { if (v.length > BOUNDS.array) { bad = 'array > 4096'; return; } for (const x of v) walk(x, d + 1); }
    else if (v && typeof v === 'object') for (const k of Object.keys(v)) walk(v[k], d + 1);
  })(doc.state ?? doc, 0);
  const pr = doc.state?.provenance;
  if (!bad && pr) for (const f of ['based_on', 'constituents']) if (Array.isArray(pr[f]) && pr[f].length > BOUNDS.breadth) bad = f + ' breadth > 64';
  return bad;
}

// ─── §14 Verification (LIGHT floor: steps 1,2,4,5,8 + §13 bounds). Returns the three-outcome result. ──
// opts: { requireVersion:'1.0', context:'data'|'key' } ; HIGH/TOP (steps 3,6,7) are separate, later.
// round-27 (3) — THE ONE INPUT BOUNDARY. `verify` (public) admits its untrusted `doc` ONCE at the door (a getter/accessor
// at any depth → E-MALFORMED; `admitDeep` is canon-exact so a legit doc is byte-transparent) and hands an inert snapshot to
// `verifyCore`. INTERNAL machinery that already holds an admitted/getter-free object (verifyAsync's JSON snapshot + the
// substrate-receipt identity shim, forkChoice) calls `verifyCore` directly — so the door snapshot does not re-clone inside
// the hot core and break the internal identity coupling. One place admits; the core trusts. This is the single seat.
export function verify(doc, opts = {}) {
  const O = admitOpts(opts);   // round-46 self-audit (Theorem R — trusted before untrusted) — admit the TRUSTED opts FIRST, THEN the untrusted doc: admitDeep(doc) reads the doc's [[Get]] face (the SIGNED face, round-29 P0-01), which FIRES a hostile doc getter; if opts were admitted after, that getter would mutate the still-live opts (drop requireAuthoritative, set acceptConsumerOverride) and rewrite the consumer policy the verdict uses — the r45 cross-argument mutation class, found in the MAIN verify path. (round-40 P1-01 — one opts admission → one inert snapshot, every opts read consistent; functions preserved.)
  if (O === null) return floorTier(bad('E-MALFORMED', 'opts must be an inert record (round-40 P1-01 — a two-face opts Proxy cannot show one maxSupportedBytes to the budget and another to the enforcement guard)'));
  const D = admitDeep(doc);
  if (D === ADMIT_REJECT) return floorTier(bad('E-MALFORMED', 'document is not an inert record — an accessor/getter cannot sign one payload and disclose another (round-27: the ONE input boundary)'));
  const verdict = verifyCore(D, O);
  // rev32 R3 (non-bypass output) — EMIT id(x̂): the content hash of the ADMITTED snapshot the verdict is about. A consumer
  // addresses the transcript by THIS returned id (a projection of the admitted artifact), never by re-hashing the raw input
  // `doc` — so a live/mutable/Proxy object that shows one face to `verify` and another to a later `contentHash(doc)` cannot
  // split the verdict from the identity. The verdict and the id come from the SAME single admission.
  return finishVerdict(D, verdict);
}
// The tail BOTH public doors share. It lived inside `verify` alone, so `verifyAsync` silently returned a
// verdict WITHOUT `id` — the projection rev32 added precisely so a consumer addresses the transcript by what
// the verifier ADMITTED rather than by re-hashing a live object. Two public entries answering one question
// must return one shape, or "switch to the async door" quietly costs a guarantee nobody announced giving up.
function finishVerdict(D, verdict) {
  let id; try { if (D && typeof D === 'object' && D.state !== undefined) id = contentHash(D); } catch { /* malformed → no addressable id */ }
  return floorTier(id === undefined ? verdict : { ...verdict, id });
}
// F.5.1f — the refusal SEAM. Thirteen exits in the body answer INDETERMINATE, and each carried whatever its own
// return statement happened to include: measured 2026-08-14, 5 of 13 named the verifier, 1 of those also named the
// registry digest, none carried `absent`. Editing thirteen statements is how the fourteenth is wrong, so the shape
// is attached ONCE, here. `assurance` is NOT synthesized: it rides only from an exit that MEASURED it, because the
// lattice bottom is not the honest report of an unfinished run — the integrity floor is `invalid`, a FINDING, and a
// refusal may not make it. `tier` never rides: it is the field a consumer branches on to proceed, and a refusal
// shaped like an answer is consumed as one (§14/#44).
// CLOSED 2026-08-14 by this seam (round 217): the counts above are what the tree looked like BEFORE it, kept as the
// record of what was being fixed. One STANDING gap is named beside the checks — the early exits, which no reachable
// input in this corpus drives to INDETERMINATE, so their coverage belongs to the build that lacks the primitive.
function verifyCore(doc, opts = {}) {
  const v = verifyCoreInner(doc, opts);
  if (v?.result !== 'INDETERMINATE') return v;
  const { tier: _dropped, ...facts } = v;
  return { ...facts, absent: absentInputs(opts, doc), verifier: VERSION, registry_digest: registryDigest() };
}
function verifyCoreInner(doc, opts = {}) {
  try {
    // step 1 — structural admission (§14.1)
    if (typeof doc !== 'object' || doc === null) return bad('E-MALFORMED', 'not an object');
    if (doc.ust === undefined || doc.state === undefined || doc.sig === undefined) return bad('E-MALFORMED', 'missing ust/state/sig');
    // §19 — A VERIFIER NEVER EXPIRES, and the two halves of that rule are different verdicts (#138, F.5.1a-bis).
    //
    // A different MAJOR is a different protocol: refusal is correct and terminal, and `E-MALFORMED` is the right
    // statement about bytes this verifier has no standing to read.
    //
    // A newer MINOR is NOT. The bytes are well-formed under a contract that only ADDS; what is missing is this
    // verifier's RULESET — a term belonging to neither party (attributing it to the publisher would advise
    // authoring an older document; to the consumer, an upgrade the protocol has no standing to demand). Answering
    // `INVALID` here would return, for a correct document, the observation reserved for a corrupt one — F.5t on
    // the time axis — and send the reader to debug the publisher while the verifier is what is out of date. It
    // would also make every consumer that has not upgraded start reporting healthy publishers as broken, in good
    // faith, on the day a minor ships.
    //
    // So the honest answer is the one already used for every other unreached axis: INDETERMINATE, naming BOTH
    // minors so a CLOSED consumer — which has no discovery surface to learn from — can act on the answer alone.
    {
      const m = /^(\d+)\.(\d+)$/.exec(String(doc.ust ?? ''));
      if (!m) return bad('E-MALFORMED', 'unknown version ' + doc.ust);
      const [docMajor, docMinor] = [Number(m[1]), Number(m[2])];
      const [ourMajor, ourMinor] = VERSION.wire.split('.').map(Number);
      // A different MAJOR is ALSO not a statement about bytes. This verifier has no rules for major `docMajor`,
      // so it cannot say those bytes are wrong — only that it cannot reach them. `INVALID` would be the same
      // overclaim the minor case fixes, one step further out.
      //
      // THE LINE, stated once: `INVALID` means *I applied MY rules and they were violated*. Everything else —
      // a ruleset I lack, a faculty I was not given, a capacity I was not granted — is INDETERMINATE.
      //
      // Minor and major differ in REACH, not in validity: under the additive-only minor contract this verifier
      // still evaluates what `1.0` defines, while a different major leaves it nothing at all to evaluate.
      if (docMajor !== ourMajor)
        return { result: 'INDETERMINATE', reason: 'unsupported_major',
          detail: `document declares ${doc.ust}; this verifier implements ${VERSION.wire}. A different major is a different protocol — nothing here is evaluated, and nothing about these bytes is claimed. Supply a verifier implementing ${docMajor}.x.` };
      if (docMinor > ourMinor)
        return { result: 'INDETERMINATE', reason: 'unsupported_minor',
          detail: `document declares ${doc.ust}; this verifier implements ${VERSION.wire}. Its bytes are not in question — a minor only ADDS, so what it adds is UNEVALUATED here, not invalid. Supply a verifier implementing ${doc.ust} to evaluate the rest.` };
    }
    // §4.1 top-level is EXACTLY {ust,state,sig,proof} — REJECT unknown members (fail-closed). An "ignore unknown"
    // rule would reopen the K1/F2 class: an unsigned member riding next to a VALID verdict.
    for (const k of Object.keys(doc)) if (!RESERVED.transcript.includes(k)) return bad('E-MALFORMED', 'unknown top-level member: ' + k);
    const bnd = boundsOf(doc); if (bnd) return bad('E-BOUNDS', bnd);                  // §13 bounds — doc is the already-admitted snapshot (verify→admitDeep→verifyCore), so call boundsOf directly (checkBounds re-admits at the public door)
    const st = doc.state;
    for (const k of Object.keys(st)) if (!RESERVED.state.includes(k)) return bad('E-MALFORMED', 'reserved-key: state.' + k);
    if (!st.id || !st.time || !st.data || !st.hashes) return bad('E-MALFORMED', 'state missing id/time/data/hashes');
    for (const k of Object.keys(st.id)) if (!RESERVED.id.includes(k)) return bad('E-MALFORMED', 'reserved-key: id.' + k);
    // §I3 partition NAME non-reserved (defense-in-depth) + envelope-key + kind/privacy registry (§4.4)
    for (const name of Object.keys(st.data)) {
      if (RES_PARTITION_NAMES.has(name)) return bad('E-MALFORMED', 'reserved partition name: ' + name);
      const part = st.data[name];
      for (const k of Object.keys(part)) if (!RESERVED.envelope.includes(k)) return bad('E-MALFORMED', 'reserved-key: data.' + name + '.' + k);
      // §4.4 CLOSED per-mode schema: `kind` (captured|computed) is REQUIRED for EVERY partition — a private value
      // was still captured-or-computed; a partition with no kind is malformed, not "private enough to skip."
      if (!KINDS.includes(part.kind)) return bad('E-MALFORMED', 'unknown partition kind: ' + name + '.' + part.kind);
      // §4.4 #39 — a PUBLIC absence assertion MUST name WHY it is absent (non-empty `value.reason`), so 'source down'
      // can never be silently confused with 'source returned empty' (a captured partition with an empty value). A
      // private (blinded/encrypted) absence carries its reason inside the sealed value, so the plain check is skipped.
      if (part.kind === 'absence' && part.privacy === undefined && (typeof part.value?.reason !== 'string' || part.value.reason.length === 0))
        return bad('E-MALFORMED', 'absence partition requires a non-empty value.reason: ' + name);
      if (part.privacy !== undefined && !PRIVACY.includes(part.privacy)) return bad('E-MALFORMED', 'unknown privacy mode: ' + name + '.' + part.privacy);
      // §4.4 CLOSED envelope variants — ENFORCE the public/private XOR, not just the key allowlist (self-audit rc.35,
      // agent-found P0/P2). The per-partition hash is taken over `commit` WHENEVER present, so a PUBLIC partition that
      // also carried a `commit` would bind the hash to the commit while DISPLAYING an unrelated `value` — "what you see
      // ≠ what is signed", and two verifiers (mode-by-commit vs mode-by-privacy) disagree (I4). So: PUBLIC (no privacy)
      // MUST carry `value` and MUST NOT carry `commit`/`enc`; PRIVATE MUST carry `commit` and MUST NOT carry a plaintext `value`.
      if (part.privacy === undefined) {
        if (part.commit !== undefined || part.enc !== undefined) return bad('E-MALFORMED', 'public partition must not carry commit/enc (§4.4 public = {kind,value}): ' + name);
        if (part.value === undefined) return bad('E-MALFORMED', 'public partition requires value (§4.4): ' + name);
      } else {
        if (part.commit === undefined) return bad('E-MALFORMED', 'private partition requires commit (§4.4): ' + name);
        if (part.value !== undefined) return bad('E-MALFORMED', 'private partition must not carry a plaintext value (§4.4): ' + name);
      }
    }
    // step 2 — canonical, content_hash, bijection, per-partition hashes (§14.2, G19, §4.4)
    let S; try { S = signedContent(doc); } catch (e) { return bad('E-CANON', e.detail || 'canon'); }
    const ch = H('ust:state', S);
    const dk = Object.keys(st.data).sort(), hk = Object.keys(st.hashes).sort();
    if (dk.length !== hk.length || dk.some((k, i) => k !== hk[i])) return bad('E-MALFORMED', 'hashes⇄data not a bijection (G19)');
    for (const name of dk) {
      const part = st.data[name];
      let recomputed;
      try {
        recomputed = part.commit !== undefined
          ? partitionHash({ commit: part.commit })
          : partitionHash({ domain_shard: st.id.domain_shard, ust_id: st.id.ust_id, name, value: part.value });
      } catch (e) { return bad('E-CANON', 'partition canon: ' + name); }
      if (recomputed !== st.hashes[name]) return bad('E-CANON', 'partition hash mismatch: ' + name, { obligation: '§4.4 partition-hash', partition: name, expected: st.hashes[name], actual: recomputed });
    }
    // step 5 — well-formed shape + SEMANTIC consistency (§14.5): shape regex AND real-calendar existence AND
    // cross-field invariants — local shape alone let "Feb 31" through (audit E, M-02).
    if (!USTID.test(st.id.ust_id)) return bad('E-MALFORMED', 'ust_id shape');
    if (!ustIdCalendarOk(st.id.ust_id)) return bad('E-MALFORMED', 'ust_id date not on the calendar');
    if (!TS.test(st.time.generated_at) || !TS.test(st.time.valid_from) || !TS.test(st.time.valid_to)) return bad('E-MALFORMED', 'timestamp not pinned RFC3339-Z');
    for (const t of [st.time.generated_at, st.time.valid_from, st.time.valid_to])
      if (!tsCalendarOk(t)) return bad('E-MALFORMED', 'timestamp date not on the calendar: ' + t);
    if (st.time.valid_from > st.time.valid_to) return bad('E-MALFORMED', 'valid_from > valid_to');
    // §4/§12 typed identity: a key-form domain_shard is SELF-CERTIFYING and MUST equal the signing key_id —
    // claiming ANOTHER key's shard is malformed (the identity IS the key; the equality is an obligation).
    const shardMode = KEYID_FORM.test(st.id.domain_shard) ? 'key' : 'name';
    if (shardMode === 'key' && st.id.domain_shard !== st.id.key_id) return bad('E-MALFORMED', 'key-form domain_shard != key_id (self-certifying identity must be the signing key)');
    // §4.3a #40 HOMOGRAPH GUARD — a name-form domain_shard MUST be an A-label: ASCII only (punycode `xn--` for IDN),
    // never raw Unicode. 'аpple.com' (Cyrillic а, U+0430) renders identically to 'apple.com' but is a DIFFERENT
    // string — a homograph genesis would impersonate a name to a human reading the verdict. Rejecting U-labels means
    // a consumer sees either plain ASCII or a visibly-distinct `xn--…`, never deceptive glyphs. (NFC alone does NOT
    // catch this: U+0430 is a single, already-NFC code point.) The floor stays light — no confusables table needed.
    if (shardMode === 'name' && /[^\x00-\x7f]/.test(st.id.domain_shard))
      return bad('E-MALFORMED', 'name-form domain_shard must be an A-label (ASCII; punycode xn-- for IDN), not raw Unicode glyphs (homograph guard)', { obligation: '§4.3a name-form A-label' });
    // §13 capacity ladder (rc.10): ≤64 = the anonymous floor, admissible for everyone (LIGHT-anywhere).
    // Above it, capacity is EARNED BY CEREMONY: a name-form shard admits up to its genesis-declared
    // max_partitions (≤ ABS 4096). A key-form identity can hold no ceremony → the floor is its law.
    // Without the capacity-bearing genesis the question cannot complete → INDETERMINATE('unavailable'),
    // never INVALID (violation unprovable) and never VALID (floor unpassable) — the honest tier ladder.
    // §13 NORMATIVE size metric (rc.12): UTF-8 bytes of the SIGNED CONTENT canon({ust,state}) — the
    // string S already computed for the hash. Transport formatting can never flip a verdict (P0-3).
    const sBytes = utf8Len(S);
    if (sBytes > BOUNDS.sizeBytes) return bad('E-BOUNDS', `canonical transcript ${sBytes} B > 64 MiB ABS`);
    // verifier CAPABILITY ceiling (rc.12): protocol-valid but beyond THIS verifier ⇒ honest refusal.
    const maxSupported = admitBudget(opts.maxSupportedBytes, BOUNDS.sizeBytes);   // round-39 P1-01 (R4) — a caller CAPABILITY ceiling may only TIGHTEN the 64 MiB reference, never disable it: 0/NaN (falsy-bypass) and Infinity (disable) are now REFUSED, not honored — a sibling of the maxInputBytes/refBudget escapes
    if (maxSupported === null) return bad('E-MALFORMED', 'maxSupportedBytes must be a finite positive integer of bytes (a verifier capability ceiling may only tighten, never disable, the size check)');
    if (opts.maxSupportedBytes !== undefined && sBytes > maxSupported)
      return { result: 'INDETERMINATE', reason: 'resource_limit', detail: `canonical transcript ${sBytes} B > this verifier's capability ${opts.maxSupportedBytes} B` };
    if (dk.length > BOUNDS.floorPartitions || sBytes > BOUNDS.floorSizeBytes) {
      const over = dk.length > BOUNDS.floorPartitions
        ? `partitions ${dk.length} > ${BOUNDS.floorPartitions}` : `canonical size ${sBytes} B > 1 MiB`;
      if (shardMode === 'key') return bad('E-BOUNDS', `${over} anonymous floor (key-form identity cannot declare capacity)`);
      // Capacity is a TRUSTED GRANT (rc.12, P0-4): the caller passes opts.capacity AFTER establishing
      // authority (resolveAuthority → .capacity, or a pin/policy). A raw caller-supplied genesis no
      // longer expands anything — a self-signed genesis was a self-issued budget.
      const cap = opts.capacity ?? {};
      if (dk.length > BOUNDS.floorPartitions) {
        const granted = Number(cap.maxPartitions ?? NaN);
        if (!Number.isInteger(granted) || granted < 1 || granted > BOUNDS.partitions)
          // F.5.1d — this branch holds TWO cases and the literal spoke only to the first. No grant at all is a SUPPLY
          // gap; a grant that arrived unusable is a REPLACE one, and telling that caller to pass what it just passed
          // is the same false promise one axis over. The helper yields the supply clause exactly when the slot is
          // empty, so the replace clause is what remains when it is not.
          return { result: 'INDETERMINATE', reason: 'unavailable', detail: `${over} floor — no trusted capacity grant for ${st.id.domain_shard}`
            + supplyRemedy(opts, 'capacity', ' (resolve authority, then pass opts.capacity)')
            + (suppliedInput(opts, 'capacity') ? ` — the grant supplied carries maxPartitions ${JSON.stringify(cap.maxPartitions)}, which is not an integer in 1..${BOUNDS.partitions}; resolve authority again and pass the grant it returns` : '') };
        if (dk.length > granted) return bad('E-BOUNDS', `partitions ${dk.length} > granted ${granted}`);
      }
      if (sBytes > BOUNDS.floorSizeBytes) {
        const grantedB = Number(cap.maxTranscriptBytes ?? NaN);
        if (!Number.isInteger(grantedB) || grantedB < 1 || grantedB > BOUNDS.sizeBytes)
          return { result: 'INDETERMINATE', reason: 'unavailable', detail: `canonical size ${sBytes} B > 1 MiB floor — no trusted capacity grant for ${st.id.domain_shard}` };
        if (sBytes > grantedB) return bad('E-BOUNDS', `canonical size ${sBytes} B > granted ${grantedB}`);
      }
    }
    if (!CLASSES.includes(st.id.class)) return bad('E-MALFORMED', 'unknown class ' + st.id.class);
    if (dk.length < 1) return bad('E-MALFORMED', 'no partition');
    const HASH = /^sha256:[0-9a-f]{64}$/;
    // §6 — a partition NAME travels into `disclosed`, so it is an identifier the verdict carries and may not hold
    // a character a renderer reads as structure. Measured 2026-09-01: a private partition named with an embedded
    // newline put a forged line into the reference CLI's own output on a VALID verdict — where a reader has no
    // reason to distrust what they see. Values stay exempt: content is the renderer's to escape.
    for (const name of dk) {
      if (FORGES_STRUCTURE.test(name)) return bad('E-MALFORMED', 'partition name carries a control or bidi-override character — an identifier the verdict quotes may not forge structure (§6)');
    }
    for (const name of dk) {                                            // §S4/F5 — private partition schema
      const part = st.data[name];
      if (part.privacy !== undefined) {
        if (!HASH.test(part.commit || '')) return bad('E-MALFORMED', 'private partition commit not sha256:hex: ' + name);
        // §4.4 — the two private alternatives are SEPARATE productions, and `enc` belongs to exactly one. Measured
        // 2026-09-01 (#177) — CLOSED here: the grammar carried `enc` as unconditionally optional, so a partition
        // declared `blinded` could ship a ciphertext, and the AEAD branch below — keyed on the MODE — never ran.
        // The document verified VALID with the partition reported fully disclosed, while a key-holder decrypting
        // out of band recovered a DIFFERENT value from the one the commitment binds. Two values fixed at t, and
        // the record accountable for one. A channel the mode does not declare is examinable by nobody, so it is
        // refused at admission rather than checked (model F.7a.2, unexaminable-channel corollary).
        if (part.privacy === 'blinded' && part.enc !== undefined)
          return bad('E-MALFORMED', 'blinded partition carries an `enc` block — a channel its mode does not declare, which no rule reaches: ' + name);
        if (part.privacy === 'encrypted') {                             // encrypted MUST carry a well-formed AEAD block
          const e = part.enc;
          if (typeof e?.key_id === 'string' && FORGES_STRUCTURE.test(e.key_id)) return bad('E-MALFORMED', 'enc.key_id carries a control or bidi-override character — the verdict quotes it as `needs_key_id`, and an identifier may not forge structure (§6): ' + name);
          if (!e || !AEAD_ALGS.includes(e.alg) || typeof e.key_id !== 'string' || !B64URL.test(e.ct || '')) return bad('E-MALFORMED', 'encrypted partition missing/invalid enc{alg,key_id,ct}: ' + name);
        }
      } else if (part.value === undefined) return bad('E-MALFORMED', 'public partition without value: ' + name);
    }
    // §S4/F4 — class ↔ provenance consistency (§14.5, MUST). A signed gap record is the ONLY attestation whose
    // constituents may be empty (class:attestation + provenance.prev; §14 step 5).
    const pr = st.provenance;
    if (st.id.class === 'observation' && (pr?.constituents !== undefined || pr?.root !== undefined)) return bad('E-MALFORMED', 'observation MUST NOT carry constituents/root');
    if (st.id.class === 'derivation' && (pr?.based_on === undefined || pr?.seed === undefined)) return bad('E-MALFORMED', 'derivation MUST carry based_on + seed');
    // §11.3 continuity — a cadence entry is the key-log pattern for the stream CADENCE: prev-chained, carrying a
    // cadence_op {cadence, effective_from}. Resolved at a slot's time, so a cadence CHANGE never invalidates old
    // data (old slots verify under the cadence in force AT THEIR time) — the "no operator change breaks history" law.
    if (st.id.class === 'cadence' && (pr?.prev === undefined || st.data?.cadence_op === undefined)) return bad('E-MALFORMED', 'cadence entry MUST carry provenance.prev + a cadence_op partition (§11.3)');
    if (st.id.class === 'attestation') {
      // #69 C2 — an empty-constituents attestation used to be a bare shape (prev + no constituents), which
      // COLLIDED a checkpoint with a gap record (same shape, different meaning). The subtype is now EXPLICIT via
      // a required, named data partition (UST idiom: the partition name IS the typed content): a `set` attestation
      // carries constituents + root; a prev-only attestation MUST carry EXACTLY ONE of `checkpoint`, `gap` or
      // `anchor` — never two, never none.
      //
      // rev84 (F.5u) — `root` FOLLOWS the subtype instead of deciding it. The flat prohibition that used to
      // stand here ("a checkpoint/gap attestation MUST NOT carry a root") left an operator holding a batch
      // root exactly two conforming moves: ENUMERATE what is under it, or say nothing. Enumeration is not an
      // input to the §11.2 inclusion predicate — a consumer walking a path never reads a constituent list —
      // and publishing it hands every reader a membership ORACLE over other principals' documents, which a
      // batching operator may not do. So the honest form is a root WITHOUT constituents, and its absence is
      // what drove the reference operator to publish its roots unsigned beside the protocol.
      const empty = pr?.constituents === undefined || pr.constituents.length === 0;
      if (empty) {
        if (pr?.prev === undefined) return bad('E-MALFORMED', 'a no-constituents attestation MUST carry provenance.prev (checkpoint, gap or anchor, §11.3)');
        const named = PREV_ONLY_SUBTYPES.filter(n => st.data?.[n] !== undefined);
        if (named.length !== 1) return bad('E-MALFORMED', 'a prev-only attestation MUST carry EXACTLY ONE of ' + PREV_ONLY_SUBTYPES.map(n => 'data.' + n).join(' / ') + ' — the §11.3 subtype, never two, never none');
        // The subtype decides the root, in BOTH directions — a rule enforced on one side only is not enforced.
        const rooted = named[0] === 'anchor';
        if (rooted && pr?.root === undefined) return bad('E-MALFORMED', 'an anchor attestation MUST carry provenance.root — a batch commitment whose root is absent commits to nothing (§11.3, F.5u)');
        if (!rooted && pr?.root !== undefined) return bad('E-MALFORMED', 'a ' + named[0] + ' attestation MUST NOT carry a root — it asserts an interval, not a batch (§11.3 C2)');
      } else if (pr?.root === undefined) return bad('E-MALFORMED', 'a set attestation MUST carry constituents + root');
    }
    // W3 class-role: the partition, enforced in BOTH directions from the ONE set (§F.5e.4).
    if (opts.context === 'data' && AUTHORITY_CLASSES.has(st.id.class)) return bad('E-MALFORMED', 'class ' + st.id.class + ' not valid in data context (W3)');
    if (opts.context === 'key' && !AUTHORITY_CLASSES.has(st.id.class)) return bad('E-MALFORMED', 'class ' + st.id.class + ' not valid in key context (W3) — the key role admits exactly ' + [...AUTHORITY_CLASSES].join('/'));
    // step 4 — authenticity (§14.4): closed sig schema + declared alg + key_id consistency + strict Ed25519 over S
    if (typeof doc.sig !== 'object' || doc.sig === null) return bad('E-SIG', 'sig missing');
    for (const k of Object.keys(doc.sig)) if (!RESERVED.sig.includes(k)) return bad('E-SIG', 'unknown sig member: ' + k);
    if (doc.sig.alg !== 'Ed25519') return bad('E-SIG', 'sig.alg must be Ed25519');
    if (doc.sig.key_id !== st.id.key_id) return bad('E-SIG', 'sig.key_id != state.id.key_id');
    if (doc.sig.pub === undefined) return bad('E-KEY', 'no carried pub (LIGHT)');
    // #75 P1-02 — STRICT unpadded base64url of EXACT length: Node's base64url decode ignores padding / stray chars,
    // so `sig + '='` or non-alphabet bytes verify identically. Reject anything whose canonical re-encode differs.
    if (strictB64url(doc.sig.pub, 32) === null) return bad('E-SIG', 'sig.pub is not canonical unpadded base64url of a 32-byte Ed25519 key', { obligation: '§14.4 sig encoding' });
    if (strictB64url(doc.sig.sig, 64) === null) return bad('E-SIG', 'sig.sig is not canonical unpadded base64url of a 64-byte Ed25519 signature', { obligation: '§14.4 sig encoding' });
    if (keyId(doc.sig.pub) !== st.id.key_id) return bad('E-SIG', 'key_id != H(ust:keylog, pub)', { obligation: '§4.2 key_id-binding', expected: st.id.key_id, actual: keyId(doc.sig.pub) });
    if (!edVerifyStrict(doc.sig.pub, S, doc.sig.sig)) return bad('E-SIG', 'Ed25519 verify failed', { obligation: '§14.2 whole-state-signature' });
    // #75 ROOT 1 — TWO-PHASE: verify the anchor FIRST so its PROVEN time flows INTO authority resolution below
    // (revocation / retirement / freshness / the K_n(t) window are judged against the proven upper bound, not a
    // caller-supplied or absent anchorTime — the P0-01 gap). §S3/F3: an EMBEDDED proof MUST verify (present-but-bad
    // ⇒ E-ANCHOR, never a VALID doc beside an unchecked proof); the availability STATUS is carried, never flattened.
    let timeField = { strength: 'unproven', status: 'none' };
    if (doc.proof !== undefined) {
      const a = verifyAnchorCore(ch, doc.proof, opts);   // internal: doc.proof already admitted at the verify door
      // F.9.5-c.5 (round 201) — THREE outcomes, not two. `!a.inclusion` is a LOOSE read, and a withheld answer is
      // falsy, so before this line existed the withholding introduced one layer down was flattened right back into
      // `INVALID/E-ANCHOR` here: the document was blamed for a construction this build cannot compute. Measured on
      // the way in: the same input answered `VALID:LIGHT` before the round and `INVALID` after it — two wrong
      // answers, and the second one arrived with a green conformance run, because the new checks were aimed at
      // `verifyAnchor` and never came through `verify`. Inability is not guilt (§14, #144), and the enumeration of
      // loose readers this rule promised is only kept if each one is actually answered.
      if (!('inclusion' in a)) {
        // round-235 (#172, F.9.5-c.5 Realization) — round 201 answered "is the document GUILTY of the verifier's
        // inability" (no: INDETERMINATE, never INVALID) and the unqualified answer became the whole verdict: this
        // returned before step 3, so identity/capacity/no-fork/witness were not withheld but NEVER DERIVED. `⊘` is a
        // statement about the TIME carrier and the identity carrier is (genesis, key log, witness) — disjoint, neither
        // a factor of the other (rev92, swept for the cadence at round 233 and not here). Measured live: the same
        // document resolved VALID:HIGH with NO proof and INDETERMINATE with its own correct, substrate-final proof
        // attached — attaching true evidence LOWERED the verdict, which inverts the incentive to anchor at all.
        // Carried, not returned: `status:'unavailable'` + `unknown` is the THIRD state, unmistakable for the
        // `status:'none'` of a document that carries no proof — only this one is repaired by installing a connector
        // (`unsupported_construction` is a `faculties` term). A consumer that ASKED for the coordinate still gets a
        // refusal: `reqAnch` below sees tier !== TOP and returns INDETERMINATE naming this status.
        timeField = { strength: 'unproven', status: a.status ?? 'unavailable', unknown: { reason: a.reason ?? 'unavailable', ...(a.detail ? { detail: a.detail } : {}) } };
      } else if (!a.inclusion) { return bad('E-ANCHOR', a.detail || 'embedded proof does not verify'); }   // READABLE and not reaching its root is a verdict; inability is not
      else {
        // §14.6 N9 — a document cannot be generated AFTER the anchor that contains it (pinned RFC3339-Z compare as instants).
        if (a.time === 'anchored' && a.anchorTime && st.time.generated_at > a.anchorTime)
          return bad('E-ANCHOR', 'generated_at after the anchor time (N9: the document postdates its own anchor)');
        timeField = { strength: a.time, status: a.status, inclusion: true, ...(a.anchorTime ? { anchorTime: a.anchorTime } : {}), ...(a.assurance ? { assurance: a.assurance } : {}) };
      }
    }
    const provenAnchorTime = timeField.strength === 'anchored' ? timeField.anchorTime : undefined;   // the proven upper bound U (else undefined)
    // step 3 — name authority (§14.3): HIGH resolves genesis+key-log; else self-asserted (a bare signed KEY, no domain
    // claim). round-53 — the `pinned`/TOFU rung was REMOVED: a domain claim survives hijack / key-loss / compromise ONLY
    // via the keylog (rotation / revocation / compromised_since), which a TOFU pin cannot — so a domain claim requires
    // HIGH (genesis+key-log), never a fragile middle tier. A bare key stays self-asserted (LIGHT, domain_shard = key-form).
    // round-16 P0-01 — ONLY the PROVEN anchor time feeds the K_n(t) query; a raw caller `opts.anchorTime` is NOT a
    // proven upper bound and must never become the coordinate (it made a retired-key doc VALID:HIGH with a forged/absent
    // string while the honest late U rejected it). No proof ⇒ U is undefined ⇒ a closed key lifecycle fails closed.
    const acceptOverride = admitBool(opts.acceptConsumerOverride);   // round-42 P1-02 (R1) — strict: the string "false" (or any non-boolean) is TRUTHY and would activate the HIGH override projection; a wrong-typed grant is malformed, not truthy
    const reqAuth = admitBool(opts.requireAuthoritative), reqFresh = admitBool(opts.requireFreshKeylog), reqAnch = admitBool(opts.requireAnchored), reqIndep = admitBool(opts.requireIndependentAuthority);   // round-43 — strict: a falsy-coerced restriction (requireAuthoritative:0) would SILENTLY DROP the caller's policy (a coerced value is malformed, never a dropped requirement — the measured-input rule applies to restrictions too)
    if (reqAuth === ADMIT_REJECT || reqFresh === ADMIT_REJECT || reqAnch === ADMIT_REJECT || reqIndep === ADMIT_REJECT) return bad('E-MALFORMED', 'requireAuthoritative/requireFreshKeylog/requireAnchored/requireIndependentAuthority must be booleans (round-43 — a coerced restriction must not silently drop the caller policy)');
    if (acceptOverride === ADMIT_REJECT) return bad('E-MALFORMED', 'acceptConsumerOverride must be a boolean (round-42 P1-02 — a non-boolean grant would flip the consumer-override projection by truthiness)');
    let identity;
    if (opts.genesis != null) identity = resolveAuthority(doc, { ...opts, anchorTime: provenAnchorTime !== undefined ? provenAnchor(provenAnchorTime) : undefined });   // round-41 P1-02 — a PRESENT genesis (incl. a falsy one) resolves authority; a malformed genesis returns E-GENESIS (propagated below), never a silent self-asserted (round-17 P0-02 — mint a proven-anchor TOKEN; a raw opts.anchorTime is dropped and can never reach K_n(t))
    else identity = { strength: 'self-asserted', status: 'verified' };   // round-53 — `pinned`/TOFU rung removed: a bare key is self-asserted; a domain claim requires genesis+key-log (HIGH), no fragile middle
    if (identity.error) return bad(identity.error, identity.detail);              // forked genesis / broken key-log
    // round-53 (UST-ybn) — the name-form-requires-binding rule is applied at TIER level below: it depends on the FINAL
    // tier (which includes consumer-override HIGH), not the intermediate identity.strength.
    if (reqAuth && !(identity.strength === 'authoritative' && identity.status === 'verified'))
      return identity.status === 'unavailable'
        ? { result: 'INDETERMINATE', reason: 'unavailable', identity, detail: identity.detail }   // W1: retry, NOT failure
        : bad('E-GENESIS', 'authoritative required but ' + identity.strength + '/' + identity.status);
    // INDEPENDENCE IS A SEPARATE QUESTION FROM STRENGTH, and both routes to `authoritative` answer it the same way.
    // A publisher that vouches for itself — through a map it runs, or a witness sitting in its own trust domain —
    // gives a real property (with an anchored map it can no longer equivocate undetectably, F.5a.3) but NOT
    // independence. Those two must be distinguishable in the verdict from the first day they can differ: leave them
    // fused and every verdict issued before an independent authority exists becomes retroactively ambiguous, and a
    // consumer that needs independence has nothing to require it ON. The coordinate is CONSUMER-owned (F.5a.1
    // clause 2) — the vouching party cannot label its own domain — so SILENCE means unestablished, never independent.
    if (reqIndep && identity.strength === 'authoritative') {
      const vouchDomain = identity.map_authority_domain ?? identity.trust_domain;
      if (vouchDomain === undefined || vouchDomain === st.id.domain_shard)
        return { result: 'INDETERMINATE', reason: 'unavailable', identity,
          detail: vouchDomain === undefined
            ? 'independence required, and the consumer assigned NO trust domain to the vouching authority — independence is consumer-owned and is never inferred from silence (§12.1a, F.5a.1)'
            : 'independence required, and the vouching authority sits in the PUBLISHER\'s own trust domain — self-vouching under an anchored map is non-equivocating, which is a real property and not this one' };
    }
    // §12.2a #40 — a consumer that needs a CURRENT key-log (revocation may have propagated) sets requireFreshKeylog:
    // an `unverified` freshness (a possibly-stale cache) ⇒ INDETERMINATE (retry: re-fetch the key-log from the
    // authoritative discovery surface or supply a VERIFIED keylogHeadAnchor), NEVER a silent accept on a stale view.
    if (reqFresh && identity.freshness === 'unverified')
      // F.5.1d, second instance — found only after the gate's option roster stopped missing DESTRUCTURED names.
      // The guard is `freshness === 'unverified'`, which does NOT imply the anchor is absent: it also holds for an
      // anchor that was supplied and did not verify, and telling that caller to supply one is the promise their own
      // call refutes. Re-fetching is a REPLACE act and stays unconditional — it names bytes no call holds.
      return { result: 'INDETERMINATE', reason: 'stale_keylog', identity, detail: 'key-log freshness unverified (possibly-stale cache); re-fetch from authoritative discovery'
        + supplyRemedy(opts, 'keylogHeadAnchor', ' or supply a verified keylogHeadAnchor (§12.2a)')
        + (suppliedInput(opts, 'keylogHeadAnchor') ? ' — the keylogHeadAnchor supplied did not establish freshness (§12.2a)' : '') };
    // step 8 — privacy (§14.8/§10): if the caller discloses {nonce,value}, REPRODUCE the commit; for
    // `encrypted`, AEAD-decrypt must reproduce the SAME committed plaintext (E-COMMIT on mismatch). Never brute-force.
    // AUTHORIZATION IS PER-CHANNEL (§14.8, model F.7a.1 per-channel corollary). An `encrypted` partition has two
    // channels opened by DIFFERENT secrets, and a party may hold either without the other. Measured 2026-08-31
    // (#177) — CLOSED 2026-09-01 by this branch: a partition disclosed WITHOUT its key was pushed into `disclosed`
    // exactly like one whose ciphertext had been checked, and the two verdicts compared equal field by field. On
    // the divergence vector that is the same `disclosed: ["secret"]` for a document whose channels agree and one
    // whose channels contradict each other — the verifier stating more than it established.
    // The distinction sits on the POSITIVE side on purpose: `disclosed` means EVERY channel the publisher provided
    // was checked. A partition opened by commitment alone lands in `disclosed_partial` naming the channel that was
    // not examined, so the simplest reading of a verdict — a name appears in `disclosed` — is never true of a state
    // nobody verified. An auxiliary flag would have left that reading intact and merely documented the trap.
    const disclosed = [], disclosedPartial = [];
    for (const name of dk) {
      const part = st.data[name];
      if (part.privacy === undefined) continue;
      const disc = opts.disclosures?.[name];
      if (!disc) continue;                                                // not authorized — commit stands, opaque
      const reproduced = blindedCommit({ domain_shard: st.id.domain_shard, ust_id: st.id.ust_id, name, value: disc.value, nonce: disc.nonce });
      if (reproduced !== part.commit) return bad('E-COMMIT', 'blinded commit mismatch: ' + name);
      if (part.privacy === 'encrypted' && part.enc) {
        if (!opts.decKeys?.[part.enc.key_id]) {                            // the second channel exists and is CLOSED to this reader
          disclosedPartial.push({ partition: name, checked: 'commit', unchecked: 'aead', needs_key_id: part.enc.key_id });
          continue;
        }
        const pt = aeadDecrypt(part.enc, opts.decKeys[part.enc.key_id]);  // → canon({nonce,<p>:value}) plaintext
        if (pt === 'unsupported')                                          // §17 MTI: optional alg ⇒ cannot decide, NOT invalid
          return { result: 'INDETERMINATE', reason: 'unsupported_alg', detail: 'AEAD ' + part.enc.alg + ' is OPTIONAL and not implemented by this verifier: ' + name };
        if (pt === null || pt !== canon({ nonce: disc.nonce, partition: name, value: disc.value })) return bad('E-COMMIT', 'AEAD↔commit mismatch: ' + name);
      }
      disclosed.push(name);
    }
    // step 9 — provenance (§14.9): bound source identity (§9.1, I8). A source with a verifiable `src_sig`
    // (the source's own signature over its `addr`) is AUTHENTICATED; without one it is an operator LABEL —
    // marked `unauthenticated`, and a consumer MUST NOT surface it as source attribution.
    const sources = {};
    if (st.provenance?.sources) for (const [sid, s] of Object.entries(st.provenance.sources)) {
      const key = opts.sourceKeys?.[sid];
      sources[sid] = (key && s.src_sig && edVerifyStrict(key, s.addr, s.src_sig)) ? 'authenticated' : 'unauthenticated';
    }
    // §14.9/§14a OBLIGATIONS TABLE — every commitment-bearing provenance member carries a RECOMPUTE obligation.
    // No member may be "present but unchecked" (audit E: the root was checked while the seed was not — the
    // asymmetry class this table abolishes). Shapes first, then recomputes:
    const HASHREF = /^sha256:[0-9a-f]{64}$/;
    if (pr?.constituents !== undefined) {
      if (!Array.isArray(pr.constituents) || pr.constituents.some((h) => !HASHREF.test(h))) return bad('E-MALFORMED', 'constituents must be sha256:hex content_hashes');
      if (new Set(pr.constituents).size !== pr.constituents.length) return bad('E-MALFORMED', 'duplicate hash in constituents (double-counts the Merkle root, §9.4)');
      if (pr.root !== undefined && merkleRoot(pr.constituents) !== pr.root) return bad('E-ROOT', 'attestation root mismatch', { obligation: '§9.4 attestation-root', expected: pr.root, actual: merkleRoot(pr.constituents) });
    }
    if (pr?.based_on !== undefined) {
      if (!Array.isArray(pr.based_on) || pr.based_on.some((b) => !b || !HASHREF.test(b.hash || ''))) return bad('E-MALFORMED', 'based_on entries must carry sha256:hex `hash`');
      if (new Set(pr.based_on.map((b) => b.hash)).size !== pr.based_on.length) return bad('E-MALFORMED', 'duplicate hash in based_on (citing a referent twice has no composite meaning, §9.4)');
      if (seed(pr.based_on.map((b) => b.hash)) !== pr.seed) return bad('E-SEED', 'derivation seed != H(ust:seed, canon(based_on hashes))', { obligation: '§9.4 derivation-seed', expected: pr.seed, actual: seed(pr.based_on.map((b) => b.hash)) });
    }
    if (pr?.prev !== undefined && !HASHREF.test(pr.prev)) return bad('E-MALFORMED', 'prev must be a sha256:hex content_hash');
    // §14.9 bounded referent walk (I14: depth-0 default). The RESULT always reports how deep verification went —
    // a consumer can see `referents:'unverified'` instead of assuming the chain was walked (audit E, H-04).
    let provenanceReport = { depth: 0, referents: (pr?.based_on?.length || pr?.constituents?.length) ? 'unverified' : 'none' };
    if (opts.provenanceDepth > 0 && typeof opts.resolveRef === 'function') {
      // §13 P4: a GLOBAL verified-node budget (default 256, opts.refBudget) — exhaustion fails the WHOLE
      // walk (E-BOUNDS), never a partial success, so traversal order cannot affect any verdict (I4).
      const refCap = admitBudget(opts.refBudget, 256);                        // round-39 P1-01 (R4) — a REFUSED budget (0/NaN/Infinity/fractional/string) must FAIL CLOSED, never coalesce back to the 256 default: the round-38 `admitBudget(...) ?? 256` swallowed the null-refusal into the FULL budget (the `?? default` defeats admission — the footgun removed)
      if (refCap === null) return bad('E-MALFORMED', 'refBudget must be a finite positive integer of nodes (a caller may only tighten the 256-node reference-walk cap, never expand or disable it)');
      const refBudget = { left: refCap };
      const walked = walkReferents(st, opts, Math.min(opts.provenanceDepth, BOUNDS.depth), new Set([ch]), refBudget);
      if (walked.error) return bad(walked.error, walked.detail);
      provenanceReport = { depth: walked.depth, referents: walked.referents };
    }
    // §Y3: `domain_shard` is surfaced as `publisher` ONLY at `authoritative` strength; otherwise it is a
    // self-asserted/pinned LABEL — `publisher_claimed` — so a consumer that never read Y3 cannot over-attribute.
    // (Pinning authenticates the KEY, not the name.)
    // P0-2 audit — a raw `consumer-override` reaches the name-authoritative TIER only when the consumer CONSCIOUSLY
    // honors its own out-of-band assertion (opts.acceptConsumerOverride); the verdict still carries independently_verified:false.
    // round-237 (#171) — the test is `override_liftable`, not the STRENGTH that carries it. A liftable axiom used to
    // exist only on a `consumer-override`, because an axiom displaced whatever was earned; now that the witness is
    // always asked, the same axiom rides on the EARNED `corroborated` — and keying the lift to the strength would
    // have made a consumer strictly worse off for having ALSO been corroborated. The rest is unchanged: only an
    // EXPLICIT boolean axiom is liftable (a look-alike servedNoFork is `override_liftable: false` and never reaches
    // authoritative), the consumer must consciously honour it, and the verdict still carries independently_verified:false.
    const nameAuthoritative = identity.strength === 'authoritative' || (identity.override_liftable === true && acceptOverride === true && identity.status === 'verified');   // round-42 P1-01 + round-237
    const nameField = nameAuthoritative ? { publisher: st.id.domain_shard } : { publisher_claimed: st.id.domain_shard };
    // (the anchor was verified in phase 1 above; `timeField`/`provenAnchorTime` already carry its proven time.)
    // The verdict CARRIES ITS SCOPE: `VALID:LIGHT|HIGH|TOP`, so a consumer cannot read "valid" without reading
    // valid-AT-WHAT (a bare `=== 'VALID'` no longer matches — the same forcing function as publisher_claimed).
    // tier = the highest fully-satisfied rung (monotonic, §3.1). The NAME is bound to the key at both `corroborated`
    // and `authoritative` (§12.1a F.5a) — both reach HIGH. The no-fork STRENGTH separates them: only `authoritative`
    // (independent non-membership) reaches TOP, so an anchored-but-only-corroborated name never overclaims TOP.
    const verified = identity.status === 'verified';
    const authoritative = nameAuthoritative && verified;
    // §3.1/§15 — TOP = authoritative identity + anchored time. HIGH = name-bound (corroborated or authoritative).
    // Stream COMPLETENESS is a separate RANGE verdict (verifyStream), never a single-document claim.
    // P1-03/C3 — ONE assembler: verify() maps the consumer-override π_override projection (an explicit consumer
    // axiom, applied BEFORE assembly — never a hidden boolean inside it) and hands the SEAM VERDICTS to
    // deriveAssurance; the lattice IS the machine (no second inline tier formula).
    // round-237 (#171) — the π_override projection follows LIFTABILITY for the same reason the test above does: the
    // axiom now rides on whatever strength was earned, so gating the projection on `consumer-override` would refuse to
    // lift exactly the consumer that ALSO got corroborated.
    const idVerdict = (identity.override_liftable === true && nameAuthoritative && identity.strength !== 'authoritative') ? { ...identity, strength: 'authoritative' } : identity;
    const report = deriveAssurance(sealPredicateGraph(provePredicates({ identity: idVerdict, anchor: doc.proof !== undefined ? { inclusion: timeField.inclusion === true, time: timeField.strength } : undefined })));   // round-25 P0-01: verify() is the ONLY seat that seals the graph — over REAL seam verdicts, never caller labels
    const assurance = report.strength;
    const tier = report.tier;
    // §3.1/F.5b DOWNGRADE RESISTANCE — the symmetric floor to requireAuthoritative. A consumer requiring TOP
    // MUST reject anything the evidence proves below TOP, NEVER silently accept a lower tier (stripping the anchor
    // can only LOWER the tier, W1: it cannot forge upward). The rejection NAMES the missing coordinate: a
    // non-authoritative identity fails the name axis first (E-GENESIS / INDETERMINATE-on-unavailable, as
    // requireAuthoritative); an authoritative doc with NO proof attached is a structural downgrade (E-ANCHOR); a
    // proof that is PRESENT + inclusion-valid but whose substrate is unreachable / not-yet-buried is retryable,
    // not a forgery (INDETERMINATE). (A malformed / non-reaching proof already returned E-ANCHOR above, ln 410.)
    if (reqAnch && tier !== 'TOP') {
      if (!authoritative)
        return identity.status === 'unavailable'
          ? { result: 'INDETERMINATE', reason: 'unavailable', identity, detail: identity.detail }
          : bad('E-GENESIS', 'anchored (TOP) required but identity is ' + identity.strength + '/' + identity.status);
      if (doc.proof === undefined)
        return bad('E-ANCHOR', 'anchored (TOP) required but no anchor proof is attached (downgrade rejected)');
      return { result: 'INDETERMINATE', reason: 'unavailable', detail: 'anchored (TOP) required; proof present but substrate is ' + timeField.status + '/' + timeField.strength + ' — retry' };
    }
    // round-53 (UST-ybn — the LIGHT ambiguity fix, unified rule): a NAME-FORM domain_shard is a DOMAIN CLAIM, valid only
    // when the name binding is CONFIRMED — i.e. the doc reaches a NAME-BOUND tier (HIGH/TOP). At HIGH/TOP the identity axis
    // established a binding: an authoritative genesis+key-log, corroboration, or a consciously-honored liftable override. A
    // LIGHT tier means the identity axis did NOT reach a name binding — the domain label is unbacked ⇒ "cannot confirm ⇒
    // INDETERMINATE": never a bare VALID (the forgery-misread) and never INVALID (a legit doc verified offline / key-log
    // pending is not malformed). The trigger is the TIER, not `nameAuthoritative`: nameAuthoritative (ln 778, publisher vs
    // publisher_claimed) is STRICTER — a corroborated doc is name-bound at HIGH yet still publisher_claimed; it must stay
    // VALID:HIGH, not collapse to INDETERMINATE. A self-asserted KEY-IDENTITY uses key-form domain_shard (= key_id) and
    // stays VALID:LIGHT.
    // EXEMPT the authority-establishing classes: a `genesis` DECLARES the name it roots (self-asserted by design — the
    // consumer pins it), and `key`/`cadence` log entries continue the key-log chain; they are name-form by nature and
    // verified via their own chain, not standalone domain claims. The rule is for STATE documents (observation/attestation/derivation).
    if (tier === 'LIGHT' && shardMode === 'name' && !AUTHORITY_CLASSES.has(st.id.class))
      // F.5.1d — the guard is the TIER, which does not imply genesis is absent: a call that supplied one and still
      // resolved to LIGHT (no independent no-fork evidence) was told to supply it again, byte-identically to a call
      // that supplied nothing. The key-form clause is NOT a supply remedy — it names other BYTES to author (the x̂
      // term), which no call can hand in — so it survives unconditionally, and that is the discrimination.
      return { result: 'INDETERMINATE', reason: 'unavailable', assurance, identity: { ...identity, mode: shardMode }, detail: 'name-form domain_shard is a domain claim the verifier could not confirm (tier resolved to LIGHT — the identity axis reached no name binding)' + supplyRemedy(opts, 'genesis', ': supply genesis to bind the name (→ HIGH)') + '. A key-form domain_shard = key_id yields a self-asserted key-identity document (→ VALID:LIGHT). "cannot confirm" ⇒ INDETERMINATE (UST-ybn — the unified rule); explainLadder() names the inputs THIS call did not supply' };
    // A verdict that does not say WHO judged it cannot be re-checked later. `verifier` + `registry_digest` bind this
    // result to the rules that produced it: pin the pair and the same verdict is reproducible after the protocol has
    // moved on, which is what lets the rules keep changing without redefining verdicts already handed out.
    return { result: 'VALID:' + tier, tier, assurance, identity: { ...identity, mode: shardMode }, disclosed, ...(disclosedPartial.length ? { disclosed_partial: disclosedPartial } : {}), sources, ...nameField,
      ...(identity.noFork ? { no_fork: identity.noFork } : {}),
      ust_id: st.id.ust_id, class: st.id.class, content_hash: ch, time: timeField, provenance: provenanceReport,
      completeness: 'not_evaluated', verifier: VERSION, registry_digest: registryDigest() };
  } catch (e) {
    // #144 — FAIL-CLOSED IS THE RIGHT DIRECTION FOR A DEFECT IN THE DOCUMENT, AND THE WRONG ONE FOR A DEFECT IN
    // MY OWN FACULTY. Both arrive here as exceptions, and until 2026-08-09 both left as `INVALID`. But the two
    // answer different questions: a malformed document IS refused (§14/I10), while a verifier that cannot run the
    // check has no verdict to give at all — F-theorem clause 5, "declines to compute … the honest report is still
    // INDETERMINATE(reason), never a guessed verdict". Availability is not failure; inability is not guilt.
    if (e?.code === 'E-UNSUPPORTED')
      return { result: 'INDETERMINATE', reason: 'unsupported_alg', detail: e.detail || String(e), verifier: VERSION };
    return bad(e.code || 'E-MALFORMED', e.detail || String(e));         // fail-closed (§14/I10)
  }
}

// ─── §14.9 bounded referent walk (I14): resolve each based_on/constituents hash via the caller's resolver,
//     verify each referent (context:'data'), recurse to `depth`, visited-set ⇒ E-CYCLE, bounds ⇒ E-BOUNDS.
//     A hash the resolver cannot supply leaves referents:'partial' (availability ≠ failure); a resolved referent
//     that verifies INVALID is a REAL failure. Returns {depth, referents} or {error, detail}.
function walkReferents(st, opts, depth, visited, budget) {
  const refs = [...(st.provenance?.constituents ?? []), ...(st.provenance?.based_on ?? []).map((b) => b.hash)];
  if (!refs.length) return { depth: 0, referents: 'none' };
  let sawUnresolved = false, reached = 1;
  for (const h of refs) {
    if (visited.has(h)) return { error: 'E-CYCLE', detail: 'referent cycle at ' + h };
    if (visited.size > BOUNDS.array) return { error: 'E-BOUNDS', detail: 'referent walk exceeds bounds (§13)' };
    const rawRef = opts.resolveRef(h);
    if (!rawRef) { sawUnresolved = true; continue; }
    // rev35 R3 (round-30 P0-02) — ADMIT the resolved referent ONCE and verify + RECURSE over that SAME frozen snapshot.
    // Previously verify(refDoc) ran, then the walk recursed through the RAW refDoc.state — a stateful Proxy from resolveRef
    // showed the signed face to verify and a provenance-stripped face to the walk, so a missing nested referent was falsely
    // reported 'verified'. R3: the provenance report is a projection over the admitted snapshot, never a live re-read.
    const refDoc = admitDeep(rawRef);
    if (refDoc === ADMIT_REJECT) { sawUnresolved = true; continue; }     // a non-inert referent is unresolved (availability ≠ failure), never a host throw
    if (--budget.left < 0) return { error: 'E-BOUNDS', detail: 'referent walk exceeds the verified-node budget (§13 P4 — default 256, opts.refBudget)' };
    const rv = verify(refDoc, { ...opts, provenanceDepth: 0 });          // verify the ADMITTED referent (one level)
    if (cannotDecide(rv)) return { error: 'E-UNSUPPORTED', detail: 'referent ' + h + ' not decidable by this build: ' + rv.detail };   // #144
    if (!isValid(rv)) return { error: rv.error || 'E-SIG', detail: 'referent ' + h + ' invalid: ' + (rv.detail || rv.error) };
    if (rv.content_hash !== h) return { error: 'E-MALFORMED', detail: 'resolver returned a different document for ' + h };
    if (depth > 1) {
      const sub = walkReferents(refDoc.state, opts, depth - 1, new Set([...visited, h]), budget);   // recurse over the ADMITTED snapshot, not raw
      if (sub.error) return sub;
      if (sub.referents === 'partial') sawUnresolved = true;
      reached = Math.max(reached, 1 + sub.depth);
    }
  }
  return { depth: reached, referents: sawUnresolved ? 'partial' : 'verified' };
}

// a REAL RFC3339-Z calendar instant (not just the regex shape — 9999-99-99T99:99:99Z matches the shape but is not a
// time); module-level so resolveKeys, resolveAuthority and resolveByDiscovery share ONE definition (round-15 P1-01).
const isRealRfc3339Z = (x) => { if (typeof x !== 'string' || !/^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}Z$/.test(x)) return false; const t = Date.parse(x); return !Number.isNaN(t) && new Date(t).toISOString().slice(0, 19) + 'Z' === x; };
// round-18 P0-02 — the ONE closed substrate-receipt decoder (F.5.0/C3/I4): an "anchored" determination requires a
// verified status from the seam with a real anchored instant — `final` a STRICT Boolean true AND `time` a real
// RFC3339-Z. Every substrate decision (verifyAnchor AND the witness genesisAnchored path) routes through it, so a
// truthy-non-Boolean ({final:"yes"}) or a timeless/typeless receipt ({final:true,time:{}}) can never mint F_t anywhere.
// round-19 P0-02 — the substrate seam is a CLOSED TYPED verdict (F.5.0/C3: "a bare label or a caller boolean earns
// nothing"; "a look-alike … earns nothing"). Decode to an INERT record from OWN PRIMITIVE DATA only: an accessor,
// an inherited field, or a non-plain object earns nothing — Object.create({final:true,time}) has empty own-data and
// must NOT mint anchored. `assurance` (#71 trust-model basis) is the ONE optional own-string; UNKNOWN extra fields are
// ignored, NOT rejected (rejecting them would drop a legitimate `assurance`). → inert {final,time,assurance?} | null.
const decodeSubstrate = (sub) => {
  // THE total+inert door for a NOT-OURS substrate module return (trust-boundary law UST-5tm). Every caller — the sync
  // seam verifyAnchorCore AND the async seam anchoredByProofs (via substrateFinal) — becomes total BY CONSTRUCTION
  // through this one door: the intrinsic reflection reads below (getPrototypeOf / getOwnPropertyDescriptor) THROW on a
  // revoked Proxy or a throwing reflection trap, so the body is guarded — a hostile module return is a structured reject
  // (null), never a host throw (round-52 P1 / S1 module-seam totality).
  try {
    if (sub === null || typeof sub !== 'object') return null;
    const proto = Object.getPrototypeOf(sub);
    if (proto !== Object.prototype && proto !== null) return null;                 // plain / null-proto ONLY — no class instances, Promises, or prototype look-alikes
    const fd = Object.getOwnPropertyDescriptor(sub, 'final');
    if (!fd || !('value' in fd) || fd.value !== true) return null;                 // OWN data `final === true` (an accessor has no `value`; an inherited field has no own descriptor)
    const td = Object.getOwnPropertyDescriptor(sub, 'time');
    if (!td || !('value' in td) || !isRealRfc3339Z(td.value)) return null;         // OWN data real RFC3339-Z instant
    const out = Object.create(null); out.final = true; out.time = td.value;
    const ad = Object.getOwnPropertyDescriptor(sub, 'assurance');                  // optional #71 basis — OWN string only
    if (ad && ('value' in ad) && typeof ad.value === 'string') out.assurance = ad.value;
    return out;
  } catch { return null; }
};
// A STATED REFUSAL is not a malformed receipt, and until now both left through the same door.
//
// MEASURED 2026-08-07: a plugin returning `{final:false, time:'unproven', reason:'proof-attests-another-root'}` —
// a careful, correct answer — reached the consumer as *substrate not a typed FINAL receipt (final must be an OWN
// Boolean true…)*, byte-identical to what `{final:'yes'}` earns. The reason the plugin computed was DISCARDED, and
// the consumer was told its connector was broken when the connector had just told it the truth. CLOSED 2026-08-07
// by this decoder, which gives the negative answer its own door.
//
// `reason` IS HOSTILE INPUT and is admitted as a SLUG, never as prose: a not-ours module must not be able to write
// sentences into a verdict a human reads. Lowercase kebab, bounded — anything else is dropped and the refusal still
// stands, because the refusal is the fact and the reason is the courtesy.
const REASON_SLUG = /^[a-z][a-z0-9-]{0,47}$/;
const decodeSubstrateRefusal = (sub) => {
  try {
    if (sub === null || typeof sub !== 'object') return null;
    const proto = Object.getPrototypeOf(sub);
    if (proto !== Object.prototype && proto !== null) return null;
    const fd = Object.getOwnPropertyDescriptor(sub, 'final');
    if (!fd || !('value' in fd) || fd.value !== false) return null;                // OWN data `final === false` — a stated NO
    const out = Object.create(null); out.final = false;
    const rd = Object.getOwnPropertyDescriptor(sub, 'reason');
    if (rd && ('value' in rd) && typeof rd.value === 'string' && REASON_SLUG.test(rd.value)) out.reason = rd.value;
    return out;
  } catch { return null; }
};
const substrateFinal = (sub) => decodeSubstrate(sub) !== null;
// §12.2 — the SHARED key-log walk (genesis self-signed root + prev-chained entries each signed by a CURRENT
// valid key). Returns { validKeys: Map<key_id,pub>, revoked: Map } or { error, detail }. Used by BOTH
// resolveAuthority (name authority) AND resolveCadence — a cadence-log entry MUST be signed by an AUTHORIZED
// key (not any LIGHT doc with the same domain_shard), the P0 the cadence-log missed.
// The SOUND bytes-in boundary for the key-log reducer (round-47 rev70 — the same split as `resolveCadenceBytes`): a pure
// function of IMMUTABLE byte-strings — order-independent because EACH argument is admitted through `snapshotBytes` (an EXACT
// native Uint8Array copied into a fresh immutable buffer with NO caller getter run) BEFORE either is parsed, so no argument can
// mutate a sibling (round-48 P0-01 — the domain is ENFORCED at the door, not assumed). THIS is the sound public boundary;
// `resolveKeys` (below) is a CONVENIENCE object adapter over it.
export function resolveKeysBytes(genesisBytes, keylogBytes) {
  const gS = snapshotBytes(genesisBytes);                                          // reject array-like/Proxy/subclass BEFORE any copy; capture into an immutable buffer
  if (gS.error) return { error: 'E-GENESIS', detail: 'genesis bytes must be a native Uint8Array (' + gS.error + ')' };
  let kCopy = null;
  if (keylogBytes !== undefined && keylogBytes !== null) {
    const kS = snapshotBytes(keylogBytes);
    if (kS.error) return { error: 'E-MALFORMED', detail: 'key-log bytes must be a native Uint8Array (' + kS.error + ')' };
    kCopy = kS.bytes;
  }
  let genesis, keylog;
  try {
    genesis = JSON.parse(decodeUtf8(gS.bytes));                  // gS.bytes/kCopy are immutable copies — Buffer.from here runs no getters
    keylog = kCopy === null ? [] : JSON.parse(decodeUtf8(kCopy));
  } catch { return { error: 'E-GENESIS', detail: 'resolveKeys arguments must be canonical UTF-8 JSON byte-strings' }; }
  if (!genesis || typeof genesis !== 'object') return { error: 'E-GENESIS', detail: 'no genesis' };
  if (!Array.isArray(keylog)) return { error: 'E-MALFORMED', detail: 'key-log must be an array' };
  // rev35 R3 (round-30 P0-01) — the reducer reads ONE inert snapshot; every emitted quantity is a projection over it, nothing
  // re-reads a raw live object (the byte-in form makes this hold a fortiori — there is no live object to re-read).
  const gv = verify(genesis);                                                     // genesis is inert (parsed from immutable bytes) — verify re-admits it idempotently; every read below is of this snapshot
  if (cannotDecide(gv)) return { error: 'E-UNSUPPORTED', detail: 'genesis not decidable by this build: ' + gv.detail };   // #144 — inability, not a bad genesis
  if (!isValid(gv)) return { error: 'E-GENESIS', detail: 'genesis invalid: ' + gv.error };
  if (genesis.state.id.class !== 'genesis') return { error: 'E-GENESIS', detail: 'not class:genesis' };
  if (genesis.sig.key_id !== genesis.state.id.key_id) return { error: 'E-GENESIS', detail: 'genesis not self-signed' };
  if (keylog.length > 256) return { error: 'E-BOUNDS', detail: 'key-log > 256 (§13)' };
  let prevHash = contentHash(genesis);
  const gKid = genesis.state.id.key_id, gPub = genesis.sig.pub, gDomain = genesis.state.id.domain_shard;
  let prevTime = genesis.state.time.generated_at;                                   // round-16 P1-02: the key-log timeline must be NONDECREASING along the prev-chain, else intervals are unordered/inverted and the K_n(t) shortcuts over intervals[0]/last are unjustified (M-KEY-INTERVAL)
  // §12.2 #75 ROOT 2 — the key-log is a TEMPORAL STATE MACHINE (reducer), not a growing set. Two sets that used to
  // be ONE (the bug behind P0-02): `all` = every key ever authorized (key_id→pub) for DOCUMENT BINDING (a retired
  // key's earlier doc still binds, then X1 judges it by time — continuity); `active` = the keys that may sign the
  // NEXT log/cadence entry (SHRINKS on revoke/rotate). `revoked` carries the §12.2 X1 end-record for every key that
  // left active (reason retired|compromised). `history` records per-key lifecycle for the K_n(t) temporal query.
  const all = new Map([[gKid, gPub]]);
  const active = new Map([[gKid, gPub]]);
  const revoked = new Map();                                                      // key_id → {reason, compromised_since?, at}
  let supersededBy = null;                                                        // F.5z — set by a terminal `reroot`; the root's own signed pointer to its successor
  const compromised = new Set();                                                  // MONOTONIC (round-14 P0-01): compromise is TERMINAL — a compromised key is never re-authorized, and its status can never be downgraded (compromised → retired) by a later revoke
  const history = new Map([[gKid, { pub: gPub, intervals: [{ from: genesis.state.time.generated_at, to: null, end: null }] }]]);
  // §12.2/§F.5e.1 — role separation is DECLARED per publisher. Absent, this whole block is inert and the key set
  // stays undifferentiated: a publisher that changed nothing must not stop verifying because the protocol gained
  // a concept (§11.3 continuity). Declared, a role becomes REQUIRED on every add — a missing field must never be
  // the strongest possible claim, so the fail direction here is closed.
  const declRaw = genesis.state?.data?.genesis?.value?.roles;
  let declared = null;
  if (declRaw !== undefined) {
    if (!Array.isArray(declRaw) || declRaw.length === 0 || !declRaw.every((r) => typeof r === 'string' && OPERATING_ROLES.has(r)))
      return { error: 'E-GENESIS', detail: 'genesis `roles` must be a NON-EMPTY array of operating roles (' + [...OPERATING_ROLES].join('/') + ') — the authorizing roles are ceremony-set and cannot be declared here (§17)' };
    declared = new Set(declRaw);
  }
  const roles = new Map([[gKid, 'name-binding-root']]);   // the root's own role is fixed by §12.1, never declared
  // §F.5e.3 — AUTHORITY TO MUTATE THE LOG IS NOT AUTHORITY TO SIGN. The admissibility invariant asked only
  // `signer ∈ active`, which made an OPERATIONAL key as powerful as the root: measured 2026-07-29, a data key
  // added another key that then verified `authoritative`, and the same data key revoked the GENESIS key as
  // `compromised` — terminal, irrecoverable. The operational key is the one living in a running service's
  // environment, so this handed the most exposed key the power to destroy the name binding permanently, which
  // nullifies the separation an offline root is kept for.
//
// CLOSED 2026-07-29 by `ac9d1d1f` — wip(rev97): key-log mutation restricted to the genesis root — code,
// vectors, BMC. In this tree a narration is written in the commit that fixes what it describes, and blame
// places this paragraph there; noted 2026-08-05, appended rather than rewritten.
  //
  // Roles are not normative yet (#106), but the genesis ALREADY names the party this needs: the root it self-signs
  // with. So the restriction lands now on what the genesis carries rather than waiting for the vocabulary — and
  // when roles arrive they generalize this, they do not replace it.
  //
  // The genesis recovery set is deliberately NOT admitted here. It looked like it belonged — a lost root would
  // otherwise strand the name — but those keys never sign key-log entries: they threshold-sign a RecoveryClaim
  // that re-roots the AUTHORITY CHECKPOINT chain (§12.3.2). One word, two mechanisms. Measured: a recovery key
  // signing a key-log entry is already refused as NOT-ACTIVE, so admitting it would have widened authority on the
  // strength of a shared name and nothing else.
    // `rotate` REMOVED (rev97, §F.5e.0). It was signed by the key it replaced, so a key compromised but NOT YET
  // DECLARED could name a successor the attacker chose — the operator then revokes what it knows about while the
  // attacker keeps authority through a key nobody saw. Self-authorized succession turns a detected compromise into
  // an undetected one, and compromise is terminal only once declared, so the window is exactly the blind one.
  // Nothing ever emitted it and no key log in existence carries one (measured 2026-07-29 on the reference
  // operator's served and mirrored logs), so removal costs no compatibility. `supersedes` on a ROOT-authorized
  // `add` replaces what it was for: it records succession without granting anything, and it earns its place in
  // this strict allowlist under §F.5e.2 because the verifier ACTS on it — role inheritance is derived from it.
  // §12.1 P2 / F.5z — `reroot` is the SIGNED half of a supersession, and the key log is the only carrier the root
  // may use: under a declared regime it admits {genesis, key, cadence}, and of those `genesis` would make the class
  // mean two things while `cadence` has no relation to the name. The op is NOT called `supersede`: that word already
  // names key-to-key succession one field over, and three earlier collisions in this project each caused a wrong
  // edit before anyone noticed.
//
// CLOSED 2026-07-29 by `780a17d3` — protocol(rev97): remove `rotate` — a self-authorized succession let a
// compromised key name its own successor. In this tree a narration is written in the commit that fixes what
// it describes, and blame places this paragraph there; noted 2026-08-05, appended rather than rewritten.
  const OP_FIELDS = Object.assign(Object.create(null), { add: ['op', 'pub', 'new_key_id', 'supersedes', 'role'], revoke: ['op', 'pub', 'reason', 'compromised_since'], reroot: ['op', 'to_genesis'] });   // null-proto (UST-Protocol round-13 P2-01): OP_FIELDS["toString"] must be undefined, not an inherited function
  const derive = (i, pub, label) => {                                            // strict pub + derived key_id
    if (strictB64url(pub, 32) === null) return { error: { error: 'E-KEY', detail: 'entry ' + i + ' ' + label + ' pub not a 32-byte base64url key' } };
    return { kid: keyId(pub) };
  };
  for (const [i, e] of keylog.entries()) {                                        // §12.2 walk: prev-chained, signed by a CURRENTLY-ACTIVE key
    const ev = verify(e, { context: 'key' });
    if (cannotDecide(ev)) return { error: 'E-UNSUPPORTED', detail: 'key-log entry ' + i + ' not decidable by this build: ' + ev.detail };   // #144
    if (!isValid(ev)) return { error: 'E-KEY', detail: 'key-log entry ' + i + ' invalid: ' + ev.error };
    if (e.state.id.class !== 'key') return { error: 'E-KEY', detail: 'entry ' + i + ' not class:key' };
    if (e.state.id.domain_shard !== gDomain) return { error: 'E-KEY', detail: 'entry ' + i + ' domain_shard ≠ genesis domain (round-15 P0-03: the single-domain key-log invariant lives IN the reducer, not only in the NameBound caller — the reducer is a TCB unit and must be sound in isolation, since resolveAuthority consumes it directly)' };
    if (e.state.provenance?.prev !== prevHash) return { error: 'E-PREV', detail: 'entry ' + i + ' prev not chained' };
    // round-16 P1-02 — the key-log timeline is NONDECREASING: a later prev-chain entry cannot claim an EARLIER
    // generated_at, else the emitted intervals invert ({from > to}) and the ordered-window precondition breaks.
    if (e.state.time.generated_at < prevTime) return { error: 'E-MALFORMED', detail: 'entry ' + i + ' generated_at precedes an earlier key-log entry (non-monotone timeline — intervals would be unordered, M-KEY-INTERVAL)' };
    prevTime = e.state.time.generated_at;
    // #75 P0-02a/b — the signer MUST be ACTIVE at this point, not merely ever-seen: a revoked or rotated-out key
    // can no longer authorize a later entry.
    const sKid = keyId(e.sig.pub);
    if (active.get(sKid) !== e.sig.pub) return { error: 'E-KEY', detail: 'entry ' + i + ' not signed by a currently-active key (revoked / rotated-out / never-authorized)' };
    // §F.5e.3 second conjunct: active is NECESSARY, not SUFFICIENT. ONLY the genesis root may mutate the log —
    // NOT a genesis-named recovery key, which belongs to the authority-checkpoint chain and is deliberately not
    // admitted here (see the block above; the shared word "recovery" is exactly what F.5e.3 warns about, and an
    // earlier form of THIS comment carried the error the code never had). This is a RESTRICTION — every sequence
    // admissible after it was admissible before — so no non-mutating document that verified yesterday changes verdict.
    if (sKid !== gKid) return { error: 'E-KEY', detail: 'entry ' + i + ' key-log mutation requires the GENESIS ROOT (§F.5e.3) — an operational key may sign documents, not grant or destroy authority' };
    const op = e.state?.data?.key_op?.value;
    // #75 P0-02d/e + P1-07 — CLOSED exact schema per op: an unknown op or a stray field is an ERROR, never a no-op.
    if (typeof op !== 'object' || op === null || typeof op.op !== 'string' || !Object.hasOwn(OP_FIELDS, op.op)) return { error: 'E-KEY', detail: 'entry ' + i + ' unknown or missing key_op.op (add|revoke|reroot)' };
    for (const k of Object.keys(op)) if (!OP_FIELDS[op.op].includes(k)) return { error: 'E-MALFORMED', detail: 'entry ' + i + ' stray field in ' + op.op + ': ' + k };
    // F.5z.3 — a `reroot` is TERMINAL. After it the root has named its successor, so any later entry is authority
    // exercised after its own hand-over. Decidable locally, which is the point: the log's own head proves the epoch
    // ended, with no external evidence required. Checked BEFORE the op is applied so a trailing entry cannot slip in.
    if (supersededBy !== null) return { error: 'E-KEY', detail: 'entry ' + i + ' follows a `reroot` — the key log is TERMINAL once the root has named its successor (§12.1 P2, F.5z.3)' };
    if (op.op === 'reroot') {
      if (typeof op.to_genesis !== 'string' || !isHashStr(op.to_genesis)) return { error: 'E-MALFORMED', detail: 'entry ' + i + ' reroot to_genesis must be a sha256 content_hash of the successor genesis' };
      if (op.to_genesis === contentHash(genesis)) return { error: 'E-KEY', detail: 'entry ' + i + ' reroot names its OWN genesis as successor — a cycle, not a supersession' };
      supersededBy = op.to_genesis;
    } else if (op.op === 'add') {
      const d = derive(i, op.pub, op.op); if (d.error) return d.error;
      if (op.new_key_id !== undefined && op.new_key_id !== d.kid) return { error: 'E-KEY', detail: 'entry ' + i + ' new_key_id != H(ust:keylog, pub)' };
      if (compromised.has(d.kid)) return { error: 'E-KEY', detail: 'entry ' + i + ' cannot re-authorize a COMPROMISED key (terminal, round-14 P0-01)' };
      // §12.2 — the role, under the DECLARED regime only. Inheritance PROPAGATES down a lineage and can never
      // INTRODUCE: a parallel add has no lineage, so it must state its own role (§F.5e.1).
      const inherited = op.supersedes !== undefined ? roles.get(op.supersedes) : undefined;
      if (declared === null) {
        if (op.role !== undefined) return { error: 'E-MALFORMED', detail: 'entry ' + i + ' carries a `role` but this genesis declares no role separation — a field the verifier cannot act on is refused (§F.5e.2)' };
      } else {
        const r = op.role ?? inherited;
        if (r === undefined) return { error: 'E-KEY', detail: 'entry ' + i + ' has no `role` and inherits none — this genesis DECLARES role separation, and a missing role must not be the strongest possible claim (§12.2)' };
        if (!declared.has(r)) return { error: 'E-KEY', detail: 'entry ' + i + ' role "' + r + '" is not one this genesis declared (' + [...declared].join('/') + ')' };
        roles.set(d.kid, r);
      }
      all.set(d.kid, op.pub); active.set(d.kid, op.pub);
      { const h = history.get(d.kid), t = e.state.time.generated_at;                // round-15 P0-02: re-add after retirement OPENS A NEW interval (never reuses the old authorized_at)
        if (!h) history.set(d.kid, { pub: op.pub, intervals: [{ from: t, to: null, end: null }] });
        else if (h.intervals[h.intervals.length - 1].to !== null) h.intervals.push({ from: t, to: null, end: null }); }
      // `supersedes` records the SUCCESSION and grants nothing: the superseded key leaves `active` here, but the
      // entry is authorized by whoever signed it — the root — not by the key being replaced. That distinction is the
      // whole of §F.5e.0: a self-authorized succession let a compromised-but-undeclared key name its own successor.
      // Retirement of the superseded key still requires an explicit `revoke`; this only closes its ability to sign
      // FURTHER entries, which is what `rotate` used to do as a side effect of being signed by it.
      if (op.supersedes !== undefined) {
        if (typeof op.supersedes !== 'string' || !all.has(op.supersedes))
          return { error: 'E-KEY', detail: 'entry ' + i + ' supersedes a key that was never authorized' };
        if (op.supersedes === d.kid)
          return { error: 'E-KEY', detail: 'entry ' + i + ' supersedes itself' };
        active.delete(op.supersedes);
        const h = history.get(op.supersedes);
        if (h) { const iv = h.intervals[h.intervals.length - 1]; if (iv.to === null) iv.to = e.state.time.generated_at; }
      }
    } else {  // revoke
      const d = derive(i, op.pub, 'revoke'); if (d.error) return d.error;
      if (!all.has(d.kid)) return { error: 'E-KEY', detail: 'entry ' + i + ' revoke of a never-authorized key' };
      if (op.reason !== 'retired' && op.reason !== 'compromised') return { error: 'E-MALFORMED', detail: 'entry ' + i + ' revoke reason MUST be "retired" | "compromised"' };
      if (op.reason === 'compromised') {
        if (!isRealRfc3339Z(op.compromised_since)) return { error: 'E-MALFORMED', detail: 'entry ' + i + ' compromised requires a REAL RFC3339-Z compromised_since (round-15 P1-01: shape ≠ time — 9999-99-99T99:99:99Z passes a regex but is not a calendar instant; the string-compare U ≥ C then always failed, silently downgrading E-KEY to suspect)' };
      } else if (op.compromised_since !== undefined) return { error: 'E-MALFORMED', detail: 'entry ' + i + ' retired MUST NOT carry compromised_since' };
      if (compromised.has(d.kid)) return { error: 'E-KEY', detail: 'entry ' + i + ' COMPROMISED is terminal — cannot re-revoke or downgrade (round-14 P0-01)' };
      active.delete(d.kid);
      if (op.reason === 'compromised') compromised.add(d.kid);                     // monotonic — never cleared
      revoked.set(d.kid, { reason: op.reason, compromised_since: op.compromised_since, at: e.state.time.generated_at });
      const h = history.get(d.kid); if (h) { const iv = h.intervals[h.intervals.length - 1]; if (iv.to === null) { iv.to = e.state.time.generated_at; iv.end = op.reason; } }   // round-15 P0-02: close the current active interval with the end reason
    }
    prevHash = contentHash(e);
  }
  return { validKeys: all, active, revoked, history, roles, declaredRoles: declared, head: prevHash, supersededBy };   // supersededBy (F.5z) — ABSENT unless the root signed a terminal reroot; a consumer holding this genesis reads its successor from a surface it already fetched            // validKeys = the all-ever BINDING map; head (§12.2a) = last entry content_hash (genesis if empty)
}
// CONVENIENCE object adapter over `resolveKeysBytes` (round-47 rev70) — serialize each live argument to canonical bytes (the
// structural keylog before the self-verifying genesis, whose own mutation fails its signature), then call the SOUND bytes-core.
// NOT the hostile-getter security boundary: there is no fully order-independent multi-live-object reduction in JS (a getter fires
// on any traversal) — a caller needing soundness against a hostile Proxy passes pre-serialized bytes to `resolveKeysBytes`.
export function resolveKeys(genesis, keylog = []) {
  const _kl = admitArray(keylog);   // round-19 P1-02 — a native array snapshot; a Proxy length/index trap is a structured reject, never a host throw
  if (_kl === null) return { error: 'E-MALFORMED', detail: 'key-log must be a native array (round-17 P1-02 / round-19 P1-02 — the reducer is TOTAL: a hostile accessor/Proxy is a structured reject)' };
  const enc = (s) => new Uint8Array(utf8(s));
  let kEnc, gEnc;
  try { kEnc = enc(canon(_kl)); gEnc = enc(canon(admitDeep(genesis))); } catch { return { error: 'E-GENESIS', detail: 'genesis/keylog is not an inert record (round-47 — reduced to canonical bytes at the door)' }; }
  return resolveKeysBytes(gEnc, kEnc);
}

// ─── §12 HIGH name-authority resolution. STATELESS: the caller (@ust-protocol/operator/engine) supplies the genesis +
//     key-log transcripts (retrieval is the stateful layer's job) and asserts no-fork from the witness (W1).
// ─── §12.1a / P0-2 (audit) — NAME NO-FORK EVIDENCE. `authoritative` name-authority is EARNED, never self-declared:
//     it requires a TYPED, domain-separated no-fork claim signed by a witness the CONSUMER trusts (resolved against
//     `trustRoots`: issuer_id → pub | {pub[, trust_domain]}), bound to this domain + active genesis. The witness
//     CANNOT grant itself a trust domain — it is NOT in the signed claim; the consumer owns the issuer→domain map
//     (self-declared `trust_domain`/`issuer_id` inside a claim is rejected). A raw `noForkConfirmed` boolean is NOT
//     evidence: it is a distinct `consumer-override` (independently_verified:false), never silently `authoritative`
//     (the removed overclaim class — the same as `mapInclusion:true`). Independent-map `authoritative` stays #42.
export function noForkClaim({ domain_shard, active_genesis, map_checkpoint, map_sequence }) {   // round-35 — no `valid_as_of`: a signer cannot self-declare TIME in a signed authority claim (assurance-never-self-declared; time comes from an external anchor, not the witness)
  return { purpose: 'ust:name-no-fork', domain_shard, active_genesis,
    ...(map_checkpoint !== undefined ? { map_checkpoint } : {}),
    ...(map_sequence !== undefined ? { map_sequence } : {}) };
}
export function buildNoForkEvidence(fields, privKeyObj, issuerPubB64url) {
  const claim = noForkClaim(fields);
  const sig = toB64url(ed25519Sign(utf8(canon(claim)), privKeyObj));
  return { claim, issuer_id: keyId(issuerPubB64url), sig: { alg: 'Ed25519', key_id: keyId(issuerPubB64url), pub: issuerPubB64url, sig } };
}
// Verify an envelope { claim, issuer_id, sig } against the target domain + active genesis and consumer trustRoots.
// Independence is CONSUMER-OWNED: the issuer must be accepted in trustRoots; any trust_domain INSIDE the claim is
// rejected (that would be self-declared independence, P0-2). Returns { ok, witness_id, trust_domain, detail }.
export function verifyNoForkEvidence(evidence, config) {
  const c = admitOpts(config); if (c === null) return { ok: false, detail: 'config must be an inert record (round-24 P1-01 totality)' };   // round-24 P1-01 — total for null/hostile config
  const { domain_shard, active_genesis, trustRoots = {} } = c;
  { const E = admitDeep(evidence); if (E === ADMIT_REJECT) return { ok: false, detail: 'evidence is not an inert record (round-27 — the witness claim is read once; no getter re-read after canon)' }; evidence = E; }   // round-27 — snapshot the primary signed input, uniform with the authority-graph boundary
  if (!evidence || typeof evidence !== 'object') return { ok: false, detail: 'no evidence' };
  const { claim, issuer_id, sig } = evidence || {};
  if (!closedNoForkWitness(evidence)) return { ok: false, detail: 'no-fork witness is not a closed typed ADT (round-35 P0-03 — exact { claim, issuer_id, sig } + admitSigner (issuer_id === sig.key_id === keyId(pub), exact Ed25519 wrapper, Pub32/Sig64) + exact typed no-fork claim; a signed authority witness now in the closed-ADT sweep)' };
  if (claim.domain_shard !== domain_shard) return { ok: false, detail: 'claim domain_shard mismatch' };
  if (claim.active_genesis !== active_genesis) return { ok: false, detail: 'claim not bound to this active genesis' };
  const root = trustRoots[issuer_id];
  if (!root) return { ok: false, detail: 'issuer not in consumer trustRoots' };
  const rootPub = typeof root === 'string' ? root : root.pub;
  if (rootPub !== sig.pub) return { ok: false, detail: 'issuer pub not the configured trust root' };   // signer identity (issuer_id===sig.key_id===keyId(pub)) already bound by closedNoForkWitness → admitSigner
  let msg; try { msg = canon(claim); } catch { return { ok: false, detail: 'claim not canonicalizable (round-25 P1-02 — a malformed non-null claim returns structured, never a thrown E-CANON)' }; }   // the leaf canon boundary is inside the result algebra (mirrors verifyCheckpointUniqueness)
  if (!edVerifyStrict(sig.pub, msg, sig.sig)) return { ok: false, detail: 'Ed25519 verify failed' };
  return { ok: true, witness_id: issuer_id, ...(typeof root === 'object' && root.trust_domain ? { trust_domain: root.trust_domain } : {}) };
}

// ─── #76 Phase A — CONNECTOR EVIDENCE ALGEBRA. A connector returns VERIFIED FACTS only, never an assurance label —
//     the CORE derives the class. Two algebra ops the checkpoint/authority layers consume: `compareEvidenceOrder`
//     (temporal order is a PROOF RELATION, not a comparison of RFC3339 fields) and `quorumTrustDomains` (quorum counts
//     DISTINCT CONSUMER-resolved trust domains, never connectors/URLs/mirrors, never a self-declared `trust_domain`).
//     M3 (rc.36): `verifiedEvidence()` builds the RAW facts shape only — its output is NOT VerifiedEvidence and no
//     strong rung accepts it (the rc.35 round-2 forge). Provenance-bearing evidence = buildEvidenceReceipt (a signed
//     connector receipt) verified through verifyEvidenceReceipt against consumer-admitted connectors.
export function verifiedEvidence({ proof_kind, subject, source_id, facts = {}, verifier_id, verifier_version }) {
  if (!proof_kind || !subject || !source_id) throw Object.assign(new Error('E-EVIDENCE: proof_kind, subject, source_id required'), { code: 'E-EVIDENCE' });
  for (const k of ['assurance', 'strength', 'trust_domain', 'independent'])            // facts-only: no self-declared class/independence
    if (k in facts) throw Object.assign(new Error(`E-EVIDENCE: a connector must not self-declare '${k}' (facts only; the core derives it)`), { code: 'E-EVIDENCE' });
  return { proof_kind, subject, source_id, facts, ...(verifier_id ? { verifier_id } : {}), ...(verifier_version ? { verifier_version } : {}) };
}
// The CORE derives the assurance CLASS from the proof_kind — never the connector. `transparency-log` inclusion+
// consistency is NOT non-membership; only a keyed-uniqueness/map proof-kind yields non-membership. Unknown ⇒ opaque.
export function evidenceClass(proof_kind) {
  switch (proof_kind) {
    case 'pow-header-chain':  return 'external-commitment+order+time';
    case 'transparency-log':  return 'append-only-inclusion+consistency';   // NOT non-membership
    case 'authenticated-map': return 'keyed-membership+non-membership';
    case 'content-addressed': return 'content-equality+availability';
    case 'rfc3161-tsa':       return 'trusted-timestamp';
    default:                  return 'opaque';                               // ⇒ INDETERMINATE(unsupported) upstream
  }
}
// #102 / F.5o — the SERVING axis of the same invariant, and the core owns it so no tool decides it again.
// `content-addressed` above already names what a companion copy earns: content-equality + availability. It does
// NOT name independence, and this is the function that keeps it that way. Given an expected hash and the copies a
// publisher (or a consumer) named, the verdict is decidable from BYTES ALONE — fetch, hash, compare. Whether two
// locators sit in different failure domains is invisible in what they serve: two hostnames on one provider, one
// account, one region produce an observation identical to two genuinely separate vendors (Theorem F.5o). So the
// result shape has no independence coordinate, and a caller asking for one is REFUSED rather than humoured —
// the same closure `verifiedEvidence` applies on the witness axis, where a self-declared `independent` throws.
export function replicationAgreement({ expected, copies = [], ...rest }) {
  for (const k of ['independent', 'trust_domain', 'vendor', 'assurance', 'strength'])   // serving-axis twin of the facts-only rule
    if (k in rest) throw Object.assign(new Error(`E-REPLICATION: byte-agreement is decidable from bytes; '${k}' is not (F.5o) — independence enters from consumer configuration or external evidence, never from the locator list`), { code: 'E-REPLICATION' });
  if (typeof expected !== 'string' || !expected) throw Object.assign(new Error('E-REPLICATION: expected content_hash required'), { code: 'E-REPLICATION' });
  const agreed = [], disagreed = [];
  for (const c of copies) (c && c.hash === expected ? agreed : disagreed).push(c && c.locator);
  // NOT ATTESTED with nothing to compare: an empty list is not agreement, and silence must never read as a pass.
  return deepFreeze({ property: 'byte-agreement', expected, agreed, disagreed, attested: copies.length > 0 && disagreed.length === 0 });
}
// #102 / F.5p — ABSENCE IS TWO FACTS. A surface that did not answer is either one this operator does not run
// (settled: unattestable now and later) or one that exists and was unreachable (unknown: attestable tomorrow).
// Observation alone cannot separate them, so both used to render as one `skip` and the distinction was discarded.
// The §20 profile is what separates them — and it is safe BY SHAPE, not by trust: declaring a surface can only
// turn a skip into a FAILURE, never into a pass, and a surface that is PRESENT is attested whether or not the
// profile mentions it, so silence can never hide evidence. The verdict is the core's so no tool re-derives it.
export function surfaceVerdict({ surface, declared = false, observed, standardLocation = true }) {
  // F.5p: resolution roots at the well-known location and ONLY there. A profile that named its own location for a
  // standard surface would root authority in bytes served by the host whose authority is in question, so this is
  // refused at the boundary rather than ranked lower — a relocated root is not weaker evidence, it is not evidence.
  if (!standardLocation) throw Object.assign(new Error(`E-DISCOVERY: '${surface}' resolves at its §20.1 well-known location and only there; a profile locator is an ADDITIONAL copy compared by content_hash (F.5o), never a substitute (F.5p)`), { code: 'E-DISCOVERY' });
  if (observed !== 'present' && observed !== 'absent') throw Object.assign(new Error(`E-DISCOVERY: observed must be 'present' or 'absent', not ${JSON.stringify(observed)} — a third state would decide the 2x2 by falling through it`), { code: 'E-DISCOVERY' });
  if (observed === 'present') return deepFreeze({ surface, status: 'attested', reason: 'served' });
  return declared
    ? deepFreeze({ surface, status: 'failed', reason: 'declared-and-absent' })     // a promise not kept
    : deepFreeze({ surface, status: 'not-offered', reason: 'undeclared-and-absent' });   // settled, not a transient
}
// #120 / F.5q — the ANCHORING axis. A publisher anchors a closed window into SEVERAL substrates, and one silent
// leg is three different incidents wearing one observation: the publisher printed nothing anywhere it promised
// to, one leg is down while the chain still prints, or that substrate was never part of the commitment.
//
// The per-substrate verdict is `surfaceVerdict` UNCHANGED — same 2x2, different subject — so this reuses it
// rather than restating the reasoning. What is new is the roll-up, and it carries the theorem: `dark` quantifies
// over the DECLARED set, and a quantifier over an undeclared set is not a claim. With no declaration the honest
// answer is `unknown`, never `printing` — an observer that never had grounds for the universal statement must
// not manufacture the reassuring one.
//
// Written after making the mistake it forbids: one substrate was checked and the conclusion was written about
// the publisher. A single silent leg reads as EXISTS, never as FOR ALL.
export function anchorRollup({ declared = [], observed = {} } = {}) {
  if (!Array.isArray(declared)) throw Object.assign(new Error('E-DISCOVERY: `declared` must be an array of substrate names — the universal claim quantifies over it'), { code: 'E-DISCOVERY' });
  const perSubstrate = [];
  // every substrate MENTIONED anywhere: declared ones carry an obligation, observed-only ones can still supply
  // evidence, because the anchored row never consults the declaration (F.5p admissibility, unchanged).
  for (const s of new Set([...declared, ...Object.keys(observed)]))
    perSubstrate.push(surfaceVerdict({ surface: s, declared: declared.includes(s), observed: observed[s] === 'anchored' ? 'present' : 'absent' }));

  const onDeclared = perSubstrate.filter((v) => declared.includes(v.surface));
  const silent = onDeclared.filter((v) => v.status === 'failed').map((v) => v.surface);
  const status = declared.length === 0 ? 'unknown'          // no domain ⇒ no universal claim is available at all
    : silent.length === declared.length ? 'dark'            // nothing on ANY substrate it promised
    : silent.length > 0 ? 'partial'                         // named legs down, the chain still prints
    : 'printing';
  return deepFreeze({ status, declared: [...declared], silent, perSubstrate: deepFreeze(perSubstrate) });
}
// ── THE LADDER, REPORTED (#137, F.5.1 + F.5p.2) ───────────────────────────────────────────────────────────────
// A verdict says where a document sits. An integrator needs what stands between it and the next rung — and,
// decisively, whether that thing is theirs to move. Measured 2026-08-03 on a live publisher: three barriers
// crossed in three separate production deploys, each visible only after the previous was removed, all three
// decidable from the SAME bytes and the SAME faculties at the first.
//
// CLOSED 2026-08-04 by `d46fd26e` — protocol(rev104): the ladder REPORT lands in core — coordinates plus the
// inputs never supplied (F.5.1, F.5p.2, #137). In this tree a narration is written in the commit that fixes
// what it describes, and blame places this paragraph there; noted 2026-08-05, appended rather than
// rewritten.
//
// THIS IS NOT A SECOND IMPLEMENTATION, and that constraint shapes the whole design. A table of "what would make
// this HIGH" would be a second copy of the decision relation, and the two would drift into disagreeing about the
// same bytes. So the report never states what is NEEDED. It states two things it can read without duplicating
// anything: the coordinates the relation ACTUALLY produced, and which inputs were ABSENT from the call — each
// labelled with the term of R2 it belongs to, because that is what decides who may supply it (F.5.1).
//
// NOT-ATTEMPTED IS NOT MET (F.5p.2). An axis nobody asked about must not read like an axis that passed. Silence
// carries "nothing was wrong" and "nothing was asked" alike, and the reader deciding whether to act cannot tell
// them apart — the same collapse F.5p separates on the observed surface.
//
// IT GRANTS NOTHING. The report is a function OF the relation, never an input to it: reading what would raise a
// document must not be a step toward raising it. Nothing here is fed back into any verdict.
// WHY HINTS ARE SAFE ON ONE SIDE AND NOT THE OTHER, stated because the line is easy to draw in the wrong place.
//
// The danger is NOT that a publisher raises its own verdict — it cannot: `noForkConfirmed` and trust roots live in
// the CONSUMER's verifier, and a publisher passing them to its own changes nothing for anybody else. The danger is
// that a publisher who self-verifies with a consumer override forms a FALSE BELIEF about what consumers see, and
// then states it in good faith: *we verify HIGH*. That is `signed ≠ true` arriving through a help message.
//
// So the term already decides the shape of the hint:
//   x̂ / its neighbourhood — the publisher can change the WORLD (publish a witness log, anchor a window, author
//     fewer partitions). Every consumer sees the difference. A concrete instruction is correct here.
//   ℐ_v / ρ_v — the consumer's own faculty. No instruction. What IS useful is the counterfactual: what a
//     well-equipped consumer would see — which informs the publisher without inviting it to supply the thing.
const R2_HINT = deepFreeze(Object.assign(Object.create(null), {
  genesis:        'publish `/.well-known/ust-genesis` and let the verifier fetch and check it — every consumer gains the same',
  keylog:         'publish `/.well-known/ust-keylog` chained from that genesis — the name resolves for everyone, not just for you',
  noForkEvidence: 'NOT yours to produce — an INDEPENDENT witness must attest that no rival genesis exists; your own attestation carries nothing here. What you CAN do is publish a witness log for one to anchor against',
  trustRoots:      'NOT yours to supply. A consumer holding roots that admit your witness would resolve the name; you supplying them here would only change what YOU see',
  substrateVerify: 'NOT yours to supply. A consumer with a substrate connector would check your anchors; without one the time axis stays unproven for THEM regardless of what you pass',
  noForkConfirmed: 'NOT yours to supply — it is the consumer\'s own air-gap assertion. Passing it to your own verifier proves nothing to anyone and invites you to believe a tier no consumer will see',
}));

const R2_TERM = deepFreeze(Object.assign(Object.create(null), {
  // what the caller supplies about the SUBJECT and its neighbourhood — the publisher may move these by authoring
  // differently or by publishing an artifact the verifier then VERIFIES itself
  genesis:        'x̂-neighbourhood — the publisher PUBLISHES it; the input is this verifier\'s own check of it',
  keylog:         'x̂-neighbourhood — the publisher PUBLISHES it; the input is this verifier\'s own check of it',
  // A FOURTH PARTY, and lumping it with the publisher was an error. The operator CANNOT produce this: evidence
  // that no rival genesis exists is worth nothing when the party attesting is the party in question. It is not
  // NOBODY either — an independent witness can, and the protocol describes exactly that (§12.1, F.5o). A column
  // with only publisher/consumer/nobody silently tells an operator to do something it is structurally barred
  // from doing.
  noForkEvidence: 'WITNESS — an INDEPENDENT third party publishes it; neither the publisher (self-attestation is worth nothing here) nor the consumer can produce it',
  // the verifier's OWN faculties — the CONSUMER only, never the publisher (R4, F.5.1a)
  trustRoots:      'ℐ_v — the CONSUMER\'s own faculty; a publisher advised to supply this is advised to self-grant',
  substrateVerify: 'ℐ_v — the CONSUMER\'s own faculty (a connector), never the publisher\'s',
  noForkConfirmed: 'ℐ_v — the CONSUMER\'s own override, never the publisher\'s',
}));

// F.5.1f — ONE derivation of the absent set, read from the CALL. It was inline in `explainLadder`, which reaches
// it through `verify`; the refusal path needs the same array and cannot call `explainLadder` without recursion.
// Extracting it is what keeps F.5.1b honest — a second copy is how a report and a verdict come to disagree about
// the same bytes, and here they would be two answers to the question `what did this call not bring`.
// F.5.1g — WHICH INPUTS ARE SETTLED FOR THIS DOCUMENT, which is a question about `d` and therefore unanswerable
// from `o`. A key-form `domain_shard` binds its identity by construction (§4.3a, §12.1): the axis is
// self-asserted and no neighbourhood input moves it — measured, the same document verifies VALID:LIGHT with and
// without its genesis. So `genesis`/`keylog` are not unmet rungs here, they are FINISHED ones, and F.5.1c fixes
// the shape a finished input owes. Read TOTALLY: a hostile document must not turn a report into a throw.
const SETTLED_BY_KEY_FORM = deepFreeze(['genesis', 'keylog']);
const settledInputs = (doc) => {
  let shard; try { shard = doc?.state?.id?.domain_shard; } catch { shard = undefined; }
  return (typeof shard === 'string' && KEYID_FORM.test(shard)) ? SETTLED_BY_KEY_FORM : [];
};
// The hint on a settled input REFUSES rather than instructs — the same discipline F.5.1a already imposes on a
// consumer faculty, for the same reason: an act named in a hint is an act the reader will perform. Following the
// old one was worse than inert. It moved `status` from `verified` to `unavailable` and pointed the publisher at
// no-fork evidence for a name it never claimed.
const R2_SETTLED_HINT = deepFreeze(Object.assign(Object.create(null), {
  genesis: 'NOTHING to publish and nothing to move: a key-form `domain_shard` IS the key, so identity is self-asserted by construction (§4.3a) and no genesis raises it. There is no name to serve one under. To be name-bound, publish under a dns-name shard instead — a different identity, not a repair of this one',
  keylog:  'NOTHING to move: rotation is meaningful for a NAME whose binding outlives the key. A key-form shard is derived from the key, so a new key is a new identity and a key log raises nothing here',
}));
const absentInputs = (O, doc) => {
  const absent = [];
  const settled = settledInputs(doc);
  for (const name of Object.keys(R2_TERM)) {
    let brought; try { brought = O?.[name] !== undefined; } catch { brought = false; }   // TOTAL: a hostile getter must not turn a refusal into a malformed verdict
    if (!brought) {
      const term = R2_TERM[name];
      // WHO may move it — three named parties and a residual, not a boolean. A boolean collapsed the witness
      // into the publisher and told an operator to produce what it is structurally barred from producing.
      const party = term.startsWith('x̂') ? 'publisher' : term.startsWith('WITNESS') ? 'witness' : 'consumer';
      const isSettled = settled.includes(name);
      absent.push(deepFreeze({ input: name, term, hint: isSettled ? R2_SETTLED_HINT[name] : R2_HINT[name],
        party, movable: party === 'publisher' && !isSettled, settled: isSettled }));
    }
  }
  return deepFreeze(absent);
};

export function explainLadder(doc, opts = {}) {
  const bad = (error, detail) => ({ error, detail });
  try {
    const O = (typeof opts === 'object' && opts !== null && !Array.isArray(opts)) ? opts : null;
    if (O === null) return bad('E-MALFORMED', 'opts must be an inert record');
    // CAPACITY IS EARNED, NOT HANDED IN — and a report that stopped short of earning it would name a barrier the
    // caller has already cleared. Measured twice, on two different surfaces: passing genesis/keylog as options
    // leaves `no trusted capacity grant`, because the grant is minted by RESOLUTION. Doing it here is not a second
    // implementation — it is the same relation, called one step further, and offline.
    let O2 = O;
    if (O.genesis !== undefined && O.capacity === undefined) {
      const auth = resolveAuthority(doc, O);
      if (auth && !auth.error && auth.capacity !== undefined) O2 = { ...O, capacity: auth.capacity };
    }
    const v = verify(doc, O2);
    // Coordinates as the relation produced them. An INDETERMINATE verdict carries none — that absence is itself
    // reportable and must not be filled in with guesses.
    const coordinates = isValid(v)
      ? deepFreeze({ identity: v.identity, time: v.time, completeness: v.completeness })
      : null;
    // ABSENT inputs, classified. Read from the CALL, not from a rule table — so nothing here can disagree with
    // what the relation actually saw.
    const absent = absentInputs(O, doc);
    return deepFreeze({
      verdict: v.result,
      reason: v.reason ?? null,
      detail: v.detail ?? null,
      coordinates,                                   // null ⇒ the relation did not reach them; NOT "all fine"
      attempted: deepFreeze(Object.keys(R2_TERM).filter((n) => O[n] !== undefined)),
      absent: deepFreeze(absent),
    });
  } catch (e) {
    return bad('E-MALFORMED', 'ladder not derivable: ' + (e instanceof Error ? e.message : String(e)));
  }
}

// ── §20 OPERATOR PROFILE — the CLOSED half, and only it (#135, F.5p.1) ────────────────────────────────────────
// A profile both BINDS and DESCRIBES, and one extension rule cannot serve both: dropping an unknown BINDING key
// turns a stated obligation into no obligation (the reason §20.1 answers E-REPLICATION rather than accepting and
// ignoring an independence coordinate), while refusing an unknown DESCRIBING key makes the document unextendable
// — one added line of operator prose would fail every deployed verifier. A verifier cannot choose between these
// from the key itself: whether a key binds is fixed by the SPEC, and an unknown key is precisely one this
// version does not define. So the boundary is POSITIONAL — `declares` is closed, everything else is prose — and
// never a naming convention, which would put the choice of whether a statement binds into the hands of the party
// the statement is about (F.5a.1).
//
// This function returns the CLOSED half and nothing else. Handing a verifier the prose alongside it is how a
// later surface starts reading a field that binds nothing.
const PROFILE_DECLARES = deepFreeze(['serves', 'substrates', 'copies', 'commitment_rhythm']);   // CLOSED — extending it is a spec change
// `commitment_rhythm` and NOT `cadence`: the §11.3 stream cadence is a DIFFERENT mechanism under a name that
// would have collided — signed, prev-chained, and load-bearing for `complete`, because a publisher free to shrink
// that grid would declare away the slots it missed. This one is monotone in obligation (F.5p.3): declaring a
// coarser rhythm exposes the publisher to FEWER checks and grants no verdict, since a commitment that is present
// counts whether or not it was declared. So it needs no signature and lives here, beside the substrates.
// ABSENT is the floor and it is SETTLED, never pending: an event-driven publisher — a business stream whose
// commitments follow occurrences rather than a clock — promised no rhythm, and nothing is owed where nothing was
// promised. It is not a deficiency and must not read as one.
const COPY_FIELDS = deepFreeze(['artifact', 'url']);
const COPY_ARTIFACTS = deepFreeze(['genesis', 'keylog', 'cadence', 'witness']);
const EMPTY_DECLARES = deepFreeze({ serves: deepFreeze([]), substrates: deepFreeze([]), copies: deepFreeze([]), commitment_rhythm: null });
export function parseProfile(profile) {
  const bad = (error, detail) => ({ error, detail });
  // TOTAL by construction. The input is UNTRUSTED wire — a profile fetched from the very domain whose claims are
  // in question — so an object whose traps throw must produce a REFUSAL, never an exception. A verifier mid
  // resolution treats those as different outcomes: a refusal is a profile it cannot honour, a throw is a crash.
  try {
  if (typeof profile !== 'object' || profile === null || Array.isArray(profile))
    return bad('E-DISCOVERY', 'profile must be a JSON object');
  // ABSENT `declares` is the honest floor of F.5p, not an error: it declares nothing, every optional surface
  // reports NOT OFFERED, and every profile published before this section existed keeps working unchanged.
  if (!Object.hasOwn(profile, 'declares')) return EMPTY_DECLARES;
  const d = profile.declares;
  if (typeof d !== 'object' || d === null || Array.isArray(d))
    return bad('E-DISCOVERY', '`declares` must be an object — the closed half of the profile');
  // AN UNKNOWN BINDING MEMBER IS REFUSED — AND THE REFUSAL NAMES WHOSE GAP IT IS. Dropping it is forbidden
  // (F.5p.1: a binding key silently ignored is an obligation relocated into a channel no verifier reads), but
  // the refusal is NOT a finding about the publisher. `unknown` means exactly that the version this reader
  // implements does not define the key, which is the ruleset term of R2 — the same line round 165 drew for the
  // version marker: INVALID means MY rules were violated, and a ruleset I lack is outside that sentence.
  // MEASURED before this: the profile check reported `fail` on the operator, so the first publisher to declare a
  // newer member would be called broken by every older reader, and the consumer sent to debug the wrong party.
  // `unsupported` is ADDITIVE — a consumer that ignores it refuses exactly as before.
  const unsupported = Object.keys(d).filter((k) => !PROFILE_DECLARES.includes(k));
  if (unsupported.length)
    return { ...bad('E-DISCOVERY', 'member(s) of `declares` this reader does not implement: ' + unsupported.join(', ') + ' — refused rather than dropped, because a binding key ignored is an obligation relocated out of sight. This is the READER\'s reach, not a defect in the profile'), unsupported: deepFreeze([...unsupported]), attributed: 'verifier' };
  const out = { serves: [], substrates: [], copies: [], commitment_rhythm: null };
  for (const k of ['serves', 'substrates']) {
    if (!Object.hasOwn(d, k)) continue;
    if (!Array.isArray(d[k]) || d[k].some((x) => typeof x !== 'string'))
      return bad('E-DISCOVERY', '`declares.' + k + '` must be an array of names');
    out[k] = [...d[k]];
  }
  if (Object.hasOwn(d, 'commitment_rhythm')) {
    // SECONDS, as a §5 string like every other declared value. A rhythm that cannot be READ is not a weaker
    // declaration than none — it is a served statement a verifier cannot honour, so it is refused rather than
    // rounded down to the floor: the floor means «promised nothing», and this publisher promised something.
    const r = d.commitment_rhythm;
    if (typeof r !== 'string' || !/^[1-9][0-9]{0,7}$/.test(r))
      return bad('E-DISCOVERY', '`declares.commitment_rhythm` must be a positive integer number of seconds as a string — an unreadable rhythm is not the same as an undeclared one');
    out.commitment_rhythm = r;
  }
  if (Object.hasOwn(d, 'copies')) {
    if (!Array.isArray(d.copies)) return bad('E-DISCOVERY', '`declares.copies` must be an array');
    for (let i = 0; i < d.copies.length; i++) {
      const c = d.copies[i];
      if (typeof c !== 'object' || c === null || Array.isArray(c)) return bad('E-DISCOVERY', 'copy ' + i + ' must be an object');
      // A STRAY FIELD IS REFUSED, not dropped — §20.1 names this outcome for the independence coordinate, and the
      // argument never depended on which field it was: a coordinate accepted and ignored is one some later
      // surface starts reading, and a copy carries a LOCATOR and never an assurance (F.5o).
      for (const k of Object.keys(c))
        if (!COPY_FIELDS.includes(k))
          return bad('E-REPLICATION', 'copy ' + i + ' carries `' + k + '` — a copy declares a locator and nothing else');
      if (!COPY_ARTIFACTS.includes(c.artifact))
        return bad('E-DISCOVERY', 'copy ' + i + ' names no known artifact — a locator without one leaves the FETCHED party to say what it is a copy of');
      if (typeof c.url !== 'string' || !/^https:\/\/[^\s]+$/.test(c.url))
        return bad('E-DISCOVERY', 'copy ' + i + ' url must be an absolute https locator');
      out.copies.push(deepFreeze({ artifact: c.artifact, url: c.url }));
    }
  }
  return deepFreeze({ serves: deepFreeze(out.serves), substrates: deepFreeze(out.substrates), copies: deepFreeze(out.copies), commitment_rhythm: out.commitment_rhythm });
  } catch { return bad('E-DISCOVERY', 'profile is not readable as a plain object'); }
}

// UST-0ol Phase 2 — evidence CAPABILITY as a SET (P2-02: capabilities are not a scalar rank). A predicate is
// satisfiable ONLY by an admissible capability; strong derivation checks this before trusting a piece of evidence,
// so a connector can never exceed its declared power (content-addressed is not temporal; unknown ⇒ no capability).
const EVIDENCE_CAPS = deepFreeze(Object.assign(Object.create(null), {   // round-25 P0-04 — DEEP-frozen registry (null-proto per round-12 P0-01): no in-process mutation of the capability vocabulary
  'pow-header-chain':  ['order', 'time'],
  'transparency-log':  ['inclusion', 'consistency', 'order'],
  'authenticated-map': ['membership', 'non-membership'],
  'content-addressed': ['content-equality', 'availability'],
  'rfc3161-tsa':       ['time'],
}));
export const evidenceCaps = (proof_kind) => Object.freeze((Object.hasOwn(EVIDENCE_CAPS, proof_kind) ? EVIDENCE_CAPS[proof_kind] : []).slice());   // round-24 P1-03 — a FROZEN COPY: a caller can never mutate the internal capability array to make check_C history-dependent

// ─── M3 (UST-6vj C2) — THE EVIDENCE SEAM. Provenance is a THEOREM, not an assumption: a strong rung consumes
//     evidence only from image(VerifyEvidence_C) — either a proof the core verifies inline (terminality / map /
//     attestation verifiers = the DirectCryptographicProof arm of RawEvidence) or a SIGNED CONNECTOR RECEIPT admitted
//     by CONSUMER config (this block). A caller-minted look-alike (rc.35 round-2 verifiedEvidence-forge) is not in
//     the image, carries no capability, and lifts no rung. trust_domain flows from the CONSUMER config, never the
//     receipt; the receipt carries FACTS only — a capability/assurance/independence/threshold field is rejected.
const BANNED_EVIDENCE_FACTS = ['assurance', 'strength', 'trust_domain', 'independent', 'capability', 'attested', 'threshold'];
const RFC3339Z = /^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}Z$/;
export function evidenceReceiptClaim({ domain_shard, active_genesis, genesis_epoch, subject, proof_kind, facts = {}, payload_digest, issued_at }) {
  if (typeof domain_shard !== 'string' || !domain_shard || !isHashStr(active_genesis)) throw Object.assign(new Error('E-EVIDENCE: domain_shard/active_genesis required'), { code: 'E-EVIDENCE' });
  if (typeof subject !== 'string' || !subject || typeof proof_kind !== 'string' || !proof_kind) throw Object.assign(new Error('E-EVIDENCE: subject and proof_kind required'), { code: 'E-EVIDENCE' });
  if (!facts || typeof facts !== 'object' || Array.isArray(facts)) throw Object.assign(new Error('E-EVIDENCE: facts must be an object'), { code: 'E-EVIDENCE' });
  for (const k of BANNED_EVIDENCE_FACTS)                                             // facts-only, extended ban (design §2.2)
    if (k in facts) throw Object.assign(new Error(`E-EVIDENCE: a receipt must not carry '${k}' (facts only; capability/class is core-derived, trust_domain is consumer config)`), { code: 'E-EVIDENCE' });
  if (typeof issued_at !== 'string' || !RFC3339Z.test(issued_at)) throw Object.assign(new Error('E-EVIDENCE: issued_at must be RFC3339 Z (a SIGNED claim, never proven time)'), { code: 'E-EVIDENCE' });
  return { version: '1', purpose: 'ust:evidence-receipt', domain_shard, active_genesis,
    genesis_epoch: genesis_epoch ?? genesisEpoch(active_genesis),                    // canonical by construction (M2); verify re-checks
    subject, proof_kind, facts, ...(payload_digest !== undefined ? { payload_digest } : {}), issued_at };
}
export function buildEvidenceReceipt(fields, privKeyObj, issuerPubB64url) {
  const claim = evidenceReceiptClaim(fields);
  const sig = toB64url(ed25519Sign(utf8(canon({ purpose: 'ust:evidence-receipt-signature', claim })), privKeyObj));
  return { claim, issuer_id: keyId(issuerPubB64url), sig: { alg: 'Ed25519', key_id: keyId(issuerPubB64url), pub: issuerPubB64url, sig } };
}
export const evidenceReceiptId = (r) => H('ust:evidence-receipt', canon({ claim: r.claim, sig: r.sig }));
// K3: a VerifiedEvidence value is a branded 'evidence' handle (the M3 witness that VerifyEvidence_C produced it) —
// portability = the signed receipt, safety = the brand.
// VerifyEvidence_C — the ONLY producer of VerifiedEvidence. Seven checks IN ORDER (M3.2): bounds/shape → signature →
// round-33 P0-01 — the public receipt admission must apply the SAME CLOSED TYPED ADT the reference KERNEL's
// `closedReceipt` does (reference-checker.mjs: decodeRec + RECEIPT_CLAIM + SIG_ENV + FACTS_SCHEMA), so a receipt that is
// KERNEL-INVALID — an extra envelope/claim/facts key, a shape-valid-but-impossible `issued_at`, an unregistered
// `proof_kind`, or wrong per-kind facts — can NEVER mint a branded VerifiedEvidence handle (the divergence GPT proved in
// round-33: consumer-side order typing cannot repair a capability handle minted from a kernel-invalid receipt). These
// schemas MIRROR the kernel field-for-field (isRealRfc3339Z = pRFC, decSeq = decodeSeq); the two independent derivations
// AGREE (conformance cross-check). `decodeExact` is the kernel `decodeRec`: own-key membership only, exact keys, typed leaves.
const _evId = (x) => typeof x === 'string' && x.length > 0, _evSeq = (x) => decSeq(x) !== null;
const decodeExact = (o, schema) => {
  try {   // round-52 S1 — total on a hostile `o` (Array.isArray/Object.keys THROW on a revoked Proxy); callers feed inert JSON/admitted data, this closes the class defensively
    if (o === null || typeof o !== 'object' || Array.isArray(o)) return false;
    for (const k of Object.keys(o)) if (!Object.hasOwn(schema, k)) return false;               // no key the schema does not OWN (round-12 P0-01 prototype-name close)
    for (const k of Object.keys(schema)) { const s = schema[k]; if (!Object.hasOwn(o, k)) { if (!s.opt) return false; continue; } if (!s.t(o[k])) return false; }
    return true;
  } catch { return false; }
};
const _evHash = (x) => isHashStr(x);   // arrow-wrapped: isHashStr is declared later in the module (resolved at call time, not load time — no TDZ)
const EV_SIG_SCHEMA = { alg: { t: (x) => x === 'Ed25519' }, key_id: { t: _evHash }, pub: { t: (x) => strictB64url(x, 32) !== null }, sig: { t: (x) => strictB64url(x, 64) !== null } };
const EV_CLAIM_SCHEMA = { version: { t: (x) => x === '1' }, purpose: { t: (x) => x === 'ust:evidence-receipt' }, domain_shard: { t: _evId }, active_genesis: { t: _evHash }, genesis_epoch: { t: _evHash }, subject: { t: _evId }, proof_kind: { t: _evId }, facts: { t: (x) => x !== null && typeof x === 'object' && !Array.isArray(x) }, issued_at: { t: (x) => isRealRfc3339Z(x) }, payload_digest: { t: _evHash, opt: true } };
const EV_FACTS_SCHEMA = Object.freeze(Object.assign(Object.create(null), {   // null-proto (round-12 P0-01) — keyed by an attacker-influenced proof_kind; exact facts per registered kind, mirrors the kernel FACTS_SCHEMA
  'pow-header-chain':  { substrate: { t: _evId }, position: { t: _evSeq } },
  'transparency-log':  { log_id: { t: _evId }, index: { t: _evSeq } },
  'rfc3161-tsa':       { clock_id: { t: _evId }, not_before: { t: isRealRfc3339Z }, not_after: { t: isRealRfc3339Z } },
  'authenticated-map': {},
  'content-addressed': {},
}));
const isKnownEvidenceKind = (k) => Object.hasOwn(EV_FACTS_SCHEMA, k);   // the CLOSED registry of admissible proof_kinds (own-key, not prototype-name)
// round-34 P0-01..04 — the PUBLIC authority verifiers must apply the SAME CLOSED TYPED ADT the kernel does to every
// SIGNED witness (checkpoint / epoch-transition / uniqueness / recovery) BEFORE minting a branded handle or an authority
// capability — else a witness the kernel rejects (an extra signed field, a shape-valid-impossible value, an unclosed sig
// wrapper, a non-canonical Pub32) mints a public brand (the round-33 receipt class, swept across ALL witness types).
// These schemas MIRROR the kernel SIG_ENV / CHECKPOINT_BODY / TRANSITION_CLAIM / VOTE_CLAIM field-for-field (decodeExact
// = the kernel decodeRec); the two independent derivations AGREE (conformance cross-check).
const _evRec = (schema) => (x) => decodeExact(x, schema);                 // a nested EXACT typed record — mirrors the kernel rec()
const _evObj = { t: (x) => x !== null && typeof x === 'object' && !Array.isArray(x) };
const AUTH_SIG_SCHEMA = { alg: { t: (x) => x === 'Ed25519' }, key_id: { t: _evHash }, pub: { t: strictPub }, sig: { t: (x) => strictB64url(x, 64) !== null } };   // mirrors SIG_ENV
// round-35 P0-01/02/03 — THE ONE authority signer-admission primitive (the audit choke-point). EVERY signed authority
// witness admits its signer HERE before grouping / identity / verdict / mint: an EXACT Ed25519 sig wrapper + the
// redundant identity fields bound to ONE value (expectedKeyId === sig.key_id === keyId(pub)) + canonical Pub32/Sig64.
// A foreign sig.key_id, an alg:RSA, an extra wrapper field, or a non-canonical pub/sig admits NOTHING. Returns the pub.
const admitSigner = (sig, expectedKeyId) => (decodeExact(sig, AUTH_SIG_SCHEMA) && typeof expectedKeyId === 'string' && expectedKeyId === sig.key_id && keyId(sig.pub) === sig.key_id) ? sig.pub : null;
// round-36 P1-01 — the ONE nested-authority-key admission: a { key_id, pub } authority record is usable ONLY if it is an
// EXACT pair, `pub` is canonical Pub32, and the identity RELATION `key_id === keyId(pub)` holds. Typing the two fields
// INDEPENDENTLY (rev44) admitted an internally-contradictory pair (key_id of A, pub of B) as a recovered authority.
// Every { key_id, pub } authority pair — recovery replacement, transition destination, checkpoint rotation, genesis
// authority, pinned prior — routes through this so a contradictory pair mints nothing.
const AUTH_KEY_SCHEMA = { key_id: { t: _evHash }, pub: { t: strictPub } };
const admitAuthorityKey = (a) => decodeExact(a, AUTH_KEY_SCHEMA) && keyId(a.pub) === a.key_id;
// round-37 P0-01 — the ONE Merkle co-path admission: `siblings` is an array of the EXPECTED length whose EVERY element is
// a canonical sha256 hash. Node preimages are then built from admitted hash STRINGS, never a generic `+` coercion — a
// `[]` sibling coerced to '' forged a root under a non-protocol grammar, and a null-proto object threw a host TypeError.
// Both Merkle verifiers (key-log terminality + SMT map) route through it, matching the kernel's typed hash arrays (pHashArr).
const admitHashPath = (siblings, expectedLen) => { try { return (Array.isArray(siblings) && siblings.length === expectedLen && siblings.every(isHashStr)) ? siblings : null; } catch { return null; } };   // round-52 S1 — Array.isArray/.every THROW on a revoked Proxy; total door
// round-38 P1-03 (R4 — the input may only TIGHTEN a faculty, never expand or disable its mechanism) — a caller resource
// scalar: a finite positive integer → min(reference, supplied); undefined → the reference default; anything else
// (Infinity / NaN / fractional / ≤0) → null (a structured refusal). Applied at EVERY public resource-policy scalar.
const admitBudget = (supplied, reference) => supplied === undefined ? reference : ((Number.isInteger(supplied) && supplied >= 1) ? Math.min(reference, supplied) : null);
const KEYLOG_COMMIT_SCHEMA = { root: { t: _evHash }, length: { t: _evSeq }, head: { t: _evHash } };
const CHK_AUTHORITY_SCHEMA = { current_key_id: { t: _evHash }, next_key_id: { t: _evHash, opt: true }, next_pub: { t: strictPub, opt: true }, effective_sequence: { t: _evSeq, opt: true } };
const CHECKPOINT_BODY_SCHEMA = { version: { t: (x) => x === '1' }, purpose: { t: (x) => x === 'ust:authority-checkpoint' }, domain_shard: { t: _evId }, genesis_epoch: { t: _evHash }, sequence: { t: _evSeq }, active_genesis: { t: _evHash }, checkpoint_authority: { t: _evRec(CHK_AUTHORITY_SCHEMA) }, keylog: { t: _evRec(KEYLOG_COMMIT_SCHEMA) }, previous_checkpoint: { t: _evHash, opt: true }, previous_epoch_final_checkpoint: { t: _evHash, opt: true } };
const TRANSITION_CLAIM_SCHEMA = { purpose: { t: (x) => x === 'ust:genesis-epoch-transition' }, domain_shard: { t: _evId }, from_genesis_epoch: { t: _evHash }, from_final_checkpoint: { t: _evHash }, from_sequence: { t: _evSeq }, to_active_genesis: { t: _evHash }, to_initial_sequence: { t: _evSeq }, to_genesis_epoch: { t: _evHash }, to_checkpoint_authority: { t: admitAuthorityKey } };   // round-36 P1-01 — the destination authority is a usable { key_id === keyId(pub) } pair, not two independent strings
const VOTE_CLAIM_SCHEMA = { purpose: { t: (x) => x === 'ust:checkpoint-uniqueness-attestation' }, domain_shard: { t: _evId }, genesis_epoch: { t: _evHash }, sequence: { t: _evSeq }, checkpoint: { t: _evHash } };
const RECOVERY_CLAIM_SCHEMA = { purpose: { t: (x) => x === 'ust:checkpoint-authority-recovery' }, domain_shard: { t: _evId }, genesis_epoch: { t: _evHash }, last_accepted_checkpoint: { t: _evHash }, effective_sequence: { t: _evSeq }, replacement_authority: { t: admitAuthorityKey } };   // round-36 P1-01 — the replacement authority is a usable { key_id === keyId(pub) } pair, not two independent strings; round-35 tuple (no human `reason`)
const NOFORK_CLAIM_SCHEMA = { purpose: { t: (x) => x === 'ust:name-no-fork' }, domain_shard: { t: _evId }, active_genesis: { t: _evHash }, map_checkpoint: { t: _evHash, opt: true }, map_sequence: { t: _evSeq, opt: true } };   // round-35 — no self-declared time (valid_as_of removed): a signer-declared instant is not part of the signed authority claim
// closed-witness decoders — exact envelope + the ONE admitSigner signer-admission + exact+typed body/claim. Every witness
// that carries its own issuer_id binds the signer HERE (round-35 P0-01/02/03); checkpoint/transition bind against the
// resolved authority key at the verifier (no envelope issuer_id).
const closedCheckpointWitness = (cw) => decodeExact(cw, { body: _evObj, sig: _evObj }) && decodeExact(cw.sig, AUTH_SIG_SCHEMA) && decodeExact(cw.body, CHECKPOINT_BODY_SCHEMA);   // exactly { body, sig } (round-34 P0-02: no unsigned extra field can shift the checkpoint id)
const closedTransitionWitness = (cm) => decodeExact(cm, { claim: _evObj, sig: _evObj, issuer_id: { t: _evHash, opt: true } }) && decodeExact(cm.sig, AUTH_SIG_SCHEMA) && (cm.issuer_id === undefined || cm.issuer_id === cm.sig.key_id) && decodeExact(cm.claim, TRANSITION_CLAIM_SCHEMA);
const closedVoteWitness = (vw) => decodeExact(vw, { claim: _evObj, issuer_id: { t: _evHash }, sig: _evObj }) && admitSigner(vw.sig, vw.issuer_id) !== null && decodeExact(vw.claim, VOTE_CLAIM_SCHEMA);   // issuer_id REQUIRED + bound to sig.key_id === keyId(pub) (round-35 P0-01)
const closedRecoveryWitness = (rw) => decodeExact(rw, { claim: _evObj, issuer_id: { t: _evHash }, sig: _evObj }) && admitSigner(rw.sig, rw.issuer_id) !== null && decodeExact(rw.claim, RECOVERY_CLAIM_SCHEMA);   // round-35 P0-02
const closedNoForkWitness = (nw) => decodeExact(nw, { claim: _evObj, issuer_id: { t: _evHash }, sig: _evObj }) && admitSigner(nw.sig, nw.issuer_id) !== null && decodeExact(nw.claim, NOFORK_CLAIM_SCHEMA);   // round-35 P0-03 — no-fork is a signed authority witness, now in the closed-ADT sweep
// ─── #42 / F.5a.2 — THE NAME-MAP ROOT AS A SIGNED, ANCHORED ARTIFACT.
//     Round 216 measured the defect: `authoritative` by map inclusion was decided against a root the CONSUMER pinned,
//     so its configuration had to be updated at the map's own cadence, and the verdict carried no date. The fix
//     factorizes the coordinate: the AUTHORITY is admitted once (`trust.mapAuthorities`, time-invariant, one entry per
//     authority), while the per-epoch part — WHICH root — proves itself by the authority's signature plus an anchor,
//     i.e. it moves from `C` into `Fₜ`. What that does NOT do is answer WHICH MAP (F.5a.2c is a negative result:
//     a namespace authority is not derivable from the substrate), which is exactly why the authority stays in `C`.
//     The claim carries the ROOT and nothing else: no time, because a signer may not self-declare it (the round-35
//     rule that kept `valid_as_of` out of the no-fork claim) — the date comes from the anchor; and no domain, because
//     one signature covering EVERY name is the map's whole advantage over a witness (F.5a.3), and binding the root to
//     a domain would hand the authority the per-name silence that advantage exists to remove.
const NAMEMAP_ROOT_CLAIM_SCHEMA = { purpose: { t: (x) => x === 'ust:name-map-root' }, map_root: { t: _evHash } };
const closedNameMapRootWitness = (w) => decodeExact(w, { claim: _evObj, issuer_id: { t: _evHash }, sig: _evObj }) && admitSigner(w.sig, w.issuer_id) !== null && decodeExact(w.claim, NAMEMAP_ROOT_CLAIM_SCHEMA);
export function nameMapRootClaim({ map_root }) { return { purpose: 'ust:name-map-root', map_root }; }
export function buildNameMapRoot(fields, privKeyObj, issuerPubB64url) {
  const claim = nameMapRootClaim(fields);
  const sig = toB64url(ed25519Sign(utf8(canon(claim)), privKeyObj));
  return { claim, issuer_id: keyId(issuerPubB64url), sig: { alg: 'Ed25519', key_id: keyId(issuerPubB64url), pub: issuerPubB64url, sig } };
}
// Admit a signed root against the CONSUMER's map authorities. Same discipline as `verifyNoForkEvidence`: the
// authority cannot name itself — admission is a function of `C`, never of the statement (F.5a.1 clause 2).
export function verifyNameMapRoot(statement, config) {
  const c = admitOpts(config); if (c === null) return { ok: false, detail: 'config must be an inert record (totality)' };
  const S = admitDeep(statement); if (S === ADMIT_REJECT) return { ok: false, detail: 'root statement is not an inert record' };
  if (!S || typeof S !== 'object') return { ok: false, detail: 'no root statement' };
  if (!closedNameMapRootWitness(S)) return { ok: false, detail: 'root statement is not a closed typed ADT { claim:{purpose,map_root}, issuer_id, sig }' };
  const admitted = c.mapAuthorities && typeof c.mapAuthorities === 'object' ? c.mapAuthorities[S.issuer_id] : undefined;
  if (!admitted) return { ok: false, detail: 'map authority not in the consumer mapAuthorities — admission is the consumer\'s, never the statement\'s' };
  const pub = typeof admitted === 'string' ? admitted : admitted.pub;
  if (pub !== S.sig.pub) return { ok: false, detail: 'authority pub is not the configured one' };
  let msg; try { msg = canon(S.claim); } catch { return { ok: false, detail: 'claim not canonicalizable' }; }
  if (!edVerifyStrict(S.sig.pub, msg, S.sig.sig)) return { ok: false, detail: 'Ed25519 verify failed' };
  // The TRUST DOMAIN is the consumer's label for this authority, exactly as `trustRoots` labels a witness: the
  // authority cannot name its own (a `trust_domain` inside the signed claim would be the self-granted independence
  // F.5a.1 clause 2 forbids, and the closed schema has no slot for one). Absent ⇒ ABSENT: no independence is
  // asserted from silence.
  return { ok: true, authority_id: S.issuer_id, map_root: S.claim.map_root,
    ...(typeof admitted === 'object' && typeof admitted.trust_domain === 'string' && admitted.trust_domain ? { trust_domain: admitted.trust_domain } : {}) };
}
// The UNFORGEABLE token, same discipline as VERIFIED_ANCHOR / VERIFIED_FRESH: only this function — which actually
// ran BOTH checks — mints one, so a caller's look-alike `{map_root, as_of}` earns nothing. ASYNC because the
// substrate seam is, and the receipt is bound by identity to the one call that produced it (the verifyAsync pattern).
const VERIFIED_MAP_ROOT = new WeakSet();
export async function proveMapRootAnchor(statement, proof, opts = {}) {
  const O = admitOpts(opts); if (O === null) return { ok: false, detail: 'opts must be an inert record (totality)' };
  const vr = verifyNameMapRoot(statement, O);
  if (!vr.ok) return { ok: false, detail: 'root statement rejected — ' + vr.detail };
  const Pf = admitDeep(proof); if (Pf === ADMIT_REJECT) return { ok: false, detail: 'anchor proof is not an inert record' };
  const capA = Pf?.anchor, capR = Pf?.root;
  let receipt; try { receipt = O.substrateVerify ? await O.substrateVerify(capA, capR) : null; } catch { receipt = null; }
  const av = verifyAnchorCore(vr.map_root, Pf, { ...O, substrateVerify: (a, r) => (a === capA && r === capR ? receipt : null) });   // CORE, not the public door: the door re-admits the proof, and a second clone breaks the `a === capA` identity binding that ties the receipt to THIS call — the same reason verifyAsync calls verifyCore
  if (!(av.inclusion === true && av.time === 'anchored' && isRealRfc3339Z(av.anchorTime)))
    return { ok: false, detail: 'the root is signed but NOT anchor-proven — ' + (av.detail || av.status || 'no proven time'), authority_id: vr.authority_id };
  const token = deepFreeze({ map_root: vr.map_root, authority_id: vr.authority_id, as_of: av.anchorTime, ...(vr.trust_domain ? { trust_domain: vr.trust_domain } : {}) });
  VERIFIED_MAP_ROOT.add(token);
  return { ok: true, token, map_root: vr.map_root, authority_id: vr.authority_id, as_of: av.anchorTime, ...(vr.trust_domain ? { trust_domain: vr.trust_domain } : {}) };
}
// subject binding → scope binding → admission (consumer connectors) → role (allowed_proof_kinds, B4) → total.
// Tamper/malformation ⇒ INVALID(E-EVIDENCE); not-admitted-for-THIS-consumer/scope/subject ⇒ INDETERMINATE
// (evidence_unverified) — absence of admission is not proof of fraud, but it earns nothing (fail-closed).
export function verifyEvidenceReceipt(receipt, config) {
  const c = admitOpts(config); if (c === null) return { result: 'INVALID', error: 'E-EVIDENCE', detail: 'config must be an inert record (round-24 P1-01 totality)' };   // round-24 P1-01 — total for null/hostile config
  const { subject, scope = {}, connectors = {} } = c;
  const R = admitDeep(receipt); if (R === ADMIT_REJECT) return { result: 'INVALID', error: 'E-EVIDENCE', detail: 'receipt is not an inert record — an accessor/getter input cannot mint an EvidenceHandle (round-26 P0-03: canon/verify/id/handle all read ONE snapshot)' };   // round-26 P0-03 — snapshot ONCE; no getter-TOCTOU between verify and handle construction
  try {
    const c = R?.claim, s = R?.sig;
    const bad = (detail) => ({ result: 'INVALID', error: 'E-EVIDENCE', detail });
    const unv = (detail) => ({ result: 'INDETERMINATE', reason: 'evidence_unverified', detail });
    // round-33 P0-01/02 — the CLOSED typed ADT over the ADMITTED snapshot R (NEVER the raw `receipt`), mirroring the
    // kernel closedReceipt: exact envelope { claim, issuer_id, sig }, typed sig wrapper, exact+typed claim (real-calendar
    // issued_at), registered proof_kind, exact per-kind facts. A receipt the kernel would reject can no longer mint a handle.
    if (!decodeExact(R, { claim: { t: (x) => x !== null && typeof x === 'object' && !Array.isArray(x) }, issuer_id: { t: isHashStr }, sig: { t: (x) => x !== null && typeof x === 'object' && !Array.isArray(x) } })) return bad('receipt envelope not closed (exactly { claim, issuer_id, sig })');
    if (!decodeExact(s, EV_SIG_SCHEMA)) return bad('receipt sig wrapper not typed (exactly { alg:Ed25519, key_id, pub, sig })');
    if (!decodeExact(c, EV_CLAIM_SCHEMA)) return bad('receipt claim not typed (exact keys + real-calendar issued_at — round-33 P0-01: 9999-99-99T99:99:99Z is shape-valid, not a real instant)');
    if (!isKnownEvidenceKind(c.proof_kind)) return bad(`proof_kind '${c.proof_kind}' is not a registered kind`);
    if (!decodeExact(c.facts, EV_FACTS_SCHEMA[c.proof_kind])) return bad(`receipt facts not typed for proof_kind '${c.proof_kind}' (round-33 P0-01: exact per-kind facts, no extra field)`);
    if (c.genesis_epoch !== genesisEpoch(c.active_genesis)) return bad('genesis_epoch ≠ canonical H("ust:genesis-epoch", active_genesis) (M2)');
    if (admitSigner(s, R.issuer_id) === null) return bad('signer not admitted — issuer_id === sig.key_id === keyId(pub) over an exact Ed25519 wrapper (round-35 admitSigner; issuer_id read from the admitted snapshot R)');
    if (!edVerifyStrict(s.pub, canon({ purpose: 'ust:evidence-receipt-signature', claim: c }), s.sig)) return bad('Ed25519 verify failed');
    if (subject !== undefined && c.subject !== subject) return unv('receipt subject is not the required subject (binding, M3.2/3)');
    if (c.domain_shard !== scope.domain_shard || c.active_genesis !== scope.active_genesis || c.genesis_epoch !== scope.genesis_epoch) return unv('receipt scope ≠ the authority scope (binding, M3.2/4)');
    const conn = connectors?.[s.key_id];
    if (!conn || conn.pub !== s.pub) return unv('issuer is not a consumer-admitted connector (admission, M3.2/5)');
    if (!Array.isArray(conn.allowed_proof_kinds) || !conn.allowed_proof_kinds.includes(c.proof_kind)) return unv(`connector is not admitted for proof_kind '${c.proof_kind}' (role, M3.2/6 — B4)`);
    const evidence = mintHandle('evidence', { evidence_id: evidenceReceiptId(R),
      authority_scope_id: authorityScopeId(scope.active_genesis),                     // K2: scope = H(tag, contentHash(g))
      subject_id: c.subject, proof_kind: c.proof_kind, verified_facts: Object.freeze({ ...c.facts }), issuer_id: s.key_id,
      ...(conn.trust_domain !== undefined ? { trust_domain: conn.trust_domain } : {}), basis: 'admitted-connector-receipt' });
    return { result: 'VALID', evidence };
  } catch (e) { return { result: 'INVALID', error: 'E-EVIDENCE', detail: 'receipt verification threw: ' + (e?.message || e) }; }   // M3.2/7 — total
}
// Freshness-side admission: a pre-verified token re-checks only its BINDING (scope + subject, never the crypto);
// a raw {claim, sig} receipt goes through the full seam; anything else is the forge and earns nothing.
function admitFreshnessEvidence(x, subject, scope, trust) {
  if (isHandle('evidence', x))
    return x.authority_scope_id === authorityScopeId(scope.active_genesis) && x.subject_id === subject
      ? { evidence: x } : { detail: 'core-verified evidence is bound to a different scope/subject' };
  if (x && typeof x === 'object' && x.claim && x.sig) { const r = verifyEvidenceReceipt(x, { subject, scope, connectors: trust?.connectors }); return r.evidence ? { evidence: r.evidence } : { detail: r.detail }; }
  return { detail: 'caller-supplied evidence is neither a signed connector receipt nor core-verified (M3 — a minted look-alike earns nothing)' };
}
const temporalOrderCapable = (ev) => { const c = evidenceCaps(ev?.proof_kind); return c.includes('order') || c.includes('time'); };
// round-32 P0-01 (M3 evidence-seam soundness / R2 processing-purity) — THE public evidence-order path
// (deriveCheckpointFreshness → compareEvidenceOrder) must apply the SAME closed typed decode the reference KERNEL's
// `orderSemantic` does (ORDER_COORD/FACTS_SCHEMA in reference-checker.mjs), so the two INDEPENDENT derivations agree.
// A connector's `facts` are untyped strings; the order coordinate is read ONLY from the proof_kind's authorised fields
// and is proof-kind-NAMESPACED — a transparency-log's log-index is never comparable to a pow chain's block-height, a
// rfc3161-tsa interval is a SAME-CLOCK pair of REAL calendar instants with not_before ≤ not_after (isRealRfc3339Z, the
// one real-calendar validator, = the kernel's pRFC). Cross-kind, non-calendar, inverted, cross-clock, or half-open
// inputs decode to NOTHING ⇒ no `proven-after` (the corroborated-freshness forge). Total, fail-closed, never throws.
const decSeq = (s) => (typeof s === 'string' && /^(0|[1-9]\d*)$/.test(s)) ? s : null;      // CanonicalSeq — mirrors reference-checker decodeSeq/pSeq
function decodeOrderFacts(proof_kind, facts) {
  if (!facts || typeof facts !== 'object' || Array.isArray(facts)) return null;
  const pid = (s) => (typeof s === 'string' && s.length > 0) ? s : null;                    // a non-empty identifier — mirrors the kernel's pId
  switch (proof_kind) {                                                                     // the CLOSED admissible-kind set = ORDER_COORD keys (no free substrate)
    case 'pow-header-chain': { const sub = pid(facts.substrate), pos = decSeq(facts.position);   // Position(substrate, position)
      return (sub && pos) ? { order: { ns: 'pow-header-chain ' + sub, pos } } : null; }
    case 'transparency-log': { const log = pid(facts.log_id), idx = decSeq(facts.index);        // Position(log_id, index) — the field is `index`, NOT `log_index`/`substrate`
      return (log && idx) ? { order: { ns: 'transparency-log ' + log, pos: idx } } : null; }
    case 'rfc3161-tsa': { const clk = pid(facts.clock_id);                                       // Interval(clock_id, not_before ≤ not_after)
      if (!clk || !isRealRfc3339Z(facts.not_before) || !isRealRfc3339Z(facts.not_after) || facts.not_before > facts.not_after) return null;   // clock_id + BOTH real-calendar bounds, well-formed
      return { interval: { id: 'rfc3161-tsa ' + clk, nb: facts.not_before, na: facts.not_after } }; }
    default: return null;                                                                       // authenticated-map / content-addressed (no order coord) + unknown ⇒ nothing
  }
}
// compareEvidenceOrder(a, b): is `a` PROVEN to be after `b`? Routed through the ONE closed decoder — positions in the
// SAME proof-kind order-namespace are a total order; else a SAME-CLOCK real-calendar interval relation (`a.not_before ≥
// b.not_after` proves after, `b.not_before ≥ a.not_after` proves not-after). Cross-kind, cross-namespace, cross-clock,
// or two-bounds-unrelated inputs prove nothing ⇒ `unproven`.
export function compareEvidenceOrder(a, b) {
  const A = admitDeep(a), B = admitDeep(b);   // round-38 P1-02 (R1) — ADMIT both operands once (branded evidence handles pass through); a hostile Proxy is a structured 'unproven', never a host throw
  if (A === ADMIT_REJECT || B === ADMIT_REJECT) return 'unproven';
  const da = decodeOrderFacts(A?.proof_kind, A?.verified_facts ?? A?.facts ?? (A && typeof A === 'object' ? A : {}));   // M3: VerifiedEvidence carries proof_kind + verified_facts
  const db = decodeOrderFacts(B?.proof_kind, B?.verified_facts ?? B?.facts ?? (B && typeof B === 'object' ? B : {}));
  if (!da || !db) return 'unproven';
  if (da.order && db.order && da.order.ns === db.order.ns)                                 // same kind + same substrate/log ⇒ one total order
    return BigInt(da.order.pos) > BigInt(db.order.pos) ? 'proven-after' : 'not-after';
  if (da.interval && db.interval && da.interval.id === db.interval.id) {                   // same clock ⇒ comparable (cross-clock proves nothing — kernel P0-03)
    if (da.interval.nb >= db.interval.na) return 'proven-after';
    if (db.interval.nb >= da.interval.na) return 'not-after';
  }
  return 'unproven';                                                                       // two upper bounds, cross-namespace, or undecodable
}
// quorumTrustDomains(list, { domains, threshold }): count DISTINCT CONSUMER-resolved trust domains. `domains` maps a
// verified source_id → trustDomain (consumer config). Sources absent from `domains` are NOT admitted; a `trust_domain`
// carried on the evidence itself is ignored. Multiple sources in one domain count once.
export function quorumTrustDomains(list, config) {
  const c = admitOpts(config); if (c === null) return { count: 0, domains: [], met: false, detail: 'config must be an inert record (round-23 P1-02)' };   // round-23 P1-02 — total boundary
  const { domains = {}, threshold } = c;
  const seen = new Set();
  const admittedList = admitDeep(list);   // round-38 P1-02 (R1) — admit the list once (read by index, not by a caller-controlled Symbol.iterator); a hostile Proxy is a structured count:0, never a host throw
  for (const e of (admittedList !== ADMIT_REJECT && Array.isArray(admittedList)) ? admittedList : []) {
    const sid = e?.source_id ?? e?.facts?.source_id;
    const dom = sid !== undefined ? domains[sid] : undefined;                          // consumer-resolved ONLY
    if (typeof dom === 'string' && dom.length && !hasLoneSurrogate(dom)) seen.add(dom.normalize('NFC'));   // round-23 P0-01 + round-24 P0-03 — a non-empty Unicode-SCALAR NFC string only (a lone surrogate is outside the §6 domain and would fake an independent domain)
  }
  const arr = [...seen].sort();                                                        // round-23 P1-01 — canonical order
  // M5 ValidThreshold — uniform across EVERY quorum surface (the rc.35 round-2 sibling of P0-4: threshold ≤ 0 here
  // made `met: true` from an EMPTY evidence list). A non-positive/non-integer threshold never satisfies a quorum.
  if (threshold !== undefined && (!Number.isInteger(threshold) || threshold < 1))
    return { count: arr.length, domains: arr, met: false, detail: 'threshold must be a positive integer (got ' + threshold + ')' };
  return { count: arr.length, domains: arr, ...(threshold !== undefined ? { met: arr.length >= threshold } : {}) };
}

// ─── UST-0ol Phase 1 — the TRUST BOUNDARY (I_C). A map root is admissible ONLY if the CONSUMER independently holds
//     it (trust.mapRoots — anchored/pinned out-of-band); it is NEVER taken from the same bundle as the proof. Absence
//     of admission ⇒ no strong rung (fail-safe). This is the separation of trust-configuration from evidence.
//     F.5a.2 — and it returns a CURRENCY BASIS, never a boolean. An inclusion proof establishes uniqueness UNDER its
//     root; `activeGenesis(n) = A` is a claim about NOW, and the step between them is `R = R_t` — non-membership over
//     the authority's root sequence, RELOCATED by prefix-uniqueness rather than discharged by it. A pinned root with
//     no anchor coordinate supplies that step by AXIOM, so the verdict must LABEL it (`map_root_currency`) exactly as
//     Proposition F.5a.1 labels the no-fork override. One basis is realized; `anchored` is the open work of #42, and
//     its absence is a named absence, never an emitted rung.
const mapRootBasis = (trust, root, token) => {
  // `anchored-authority` FIRST: it is the earned basis, and it is decided by an UNFORGEABLE token — only
  // `proveMapRootAnchor`, which ran both the authority signature and the anchor, mints one, so a caller's
  // look-alike `{map_root, as_of}` is not in the set and earns nothing. The per-epoch coordinate lives in `Fₜ`
  // here; the `C`-part left over is the AUTHORITY, admitted once (F.5a.2, and F.5a.2c on why it cannot be zero).
  if (token !== null && typeof token === 'object' && VERIFIED_MAP_ROOT.has(token) && token.map_root === root)
    return { basis: 'anchored-authority', as_of: token.as_of, authority: token.authority_id, ...(token.trust_domain ? { authority_domain: token.trust_domain } : {}) };
  if (Array.isArray(trust?.mapRoots) && trust.mapRoots.includes(root)) return { basis: 'consumer-asserted' };
  return null;
};
// P0-1 (rc.35 audit) — a module-private capability set. Only resolveByDiscovery (which actually ran witnessNoFork)
// mints a servedNoFork object into it; a transcript caller cannot add to it, so a plain look-alike object earns nothing.
const VERIFIED_SERVED = new WeakSet();
// round-16 P0-02 — the SAME unforgeable-token discipline for key-log freshness. `fresh` is EARNED by an authenticated
// fetch (F.5d), never a raw caller timestamp (a bare string, even 9999-99-99…, minted `fresh` and bypassed
// requireFreshKeylog). Only resolveByDiscovery, which actually fetched /.well-known/ust-keylog, mints an observation
// token into this set, bound to (domain, active_genesis, observed_at). A raw string/object is a caller assertion.
const VERIFIED_FRESH = new WeakSet();
// round-17 P0-02 — the SAME unforgeable-token discipline for the K_n(t) upper bound U. resolveAuthority is stateless and
// cannot itself verify an anchor (that needs the async substrate), so U is an input — and a RAW caller string is a
// forgeable coordinate (F.5e/Reach_C: no coordinate from a bare caller label; the MCP resolver tool exposed exactly this).
// Only the verified anchor seam (verify/verifyAsync) mints a proven-U token into this set; a raw string yields NO U ⇒
// a closed key lifecycle fails closed. `provenAnchor` is module-private so a caller cannot forge it.
const VERIFIED_ANCHOR = new WeakSet();
const provenAnchor = (t) => { const tok = { anchorTime: t }; VERIFIED_ANCHOR.add(tok); return tok; };
export function resolveAuthority(doc, opts = {}) {
  const O = admitOpts(opts);   // round-46 self-audit (Theorem R — trusted before untrusted) — admit opts BEFORE admitDeep(doc): admitDeep reads the doc's [[Get]] face (round-29 P0-01), which FIRES a hostile doc getter; if opts were admitted after, that getter would mutate the still-live opts (genesis / trustRoots / the noForkConfirmed & corroborated override grants) the resolution reads — the r45 cross-argument mutation class. (round-19 P1-02 — a throwing accessor/Proxy trap → null → structured reject, not a host throw.)
  if (O === null) return { error: 'E-MALFORMED', detail: 'opts must be an inert record (round-19 P1-02 totality — a hostile accessor/Proxy is a structured reject)' };
  { const D = admitDeep(doc); if (D === ADMIT_REJECT) return { error: 'E-MALFORMED', detail: 'document is not an inert record (round-28 totality — a hostile getter cannot throw a host exception at this door)' }; doc = D; }   // round-28 P1-02 — admit at the door; totality
  if (!doc || typeof doc !== 'object' || !doc.state?.id?.domain_shard || !doc.sig?.pub) return { error: 'E-MALFORMED', detail: 'document must be a UST object with state.id and sig (round-17 P1-02 totality)' };
  let { genesis, keylog = [], noForkConfirmed = false, noForkEvidence, nameMap, trustRoots, corroborated = false, servedNoFork, anchorTime, keylogFreshAsOf, keylogHeadAnchor, substrateVerify, trust } = O;   // round-18 P1-01 — destructure from the admitted record so a null/hostile opts is a structured reject/default, not a host throw
  const ncf = admitBool(noForkConfirmed), cor = admitBool(corroborated);   // round-42 P1-02 (R1) — the consumer-override GRANT booleans admit strictly: a wrong-typed truthy ("yes"/1/{}) must not activate the override
  if (ncf === ADMIT_REJECT || cor === ADMIT_REJECT) return { error: 'E-MALFORMED', detail: 'noForkConfirmed/corroborated must be booleans (round-42 P1-02 — a wrong-typed grant is malformed, not truthy)' };
  // rev38 R3 (round-31 P0-01) — admitOpts is SHALLOW (it preserves function capabilities); a NESTED untrusted DATA object in
  // opts stays a live caller object. resolveAuthority verifies `genesis` via resolveKeys (which admits its OWN snapshot) but
  // then RE-READS the raw nested genesis for contentHash + max_partitions — a two-face Proxy served the signed face to
  // resolveKeys and an unsigned face carrying elevated capacity to the outer reads. Every nested untrusted DATA object a
  // resolver verifies must be DEEP-admitted ONCE and read only from the frozen snapshot (R3 across the WHOLE input graph).
  if (genesis != null) { const G = admitDeep(genesis); if (G === ADMIT_REJECT || typeof G !== 'object' || Array.isArray(G)) return { error: 'E-GENESIS', detail: 'genesis must be an inert record (round-41 P1-02 — a PRESENT falsy/scalar/array genesis is MALFORMED, not absent; round-31 R3 — admitted once, read only from the snapshot)' }; genesis = G; }
  // round-17 P0-02 — U comes ONLY from a proven-anchor token (verify/verifyAsync mint it); a raw string never reaches K_n(t).
  const U = (anchorTime && typeof anchorTime === 'object' && VERIFIED_ANCHOR.has(anchorTime) && isRealRfc3339Z(anchorTime.anchorTime)) ? anchorTime.anchorTime : undefined;
  if (genesis == null) return { strength: 'self-asserted', status: 'verified' };   // LIGHT — nothing to resolve (ONLY a genuinely ABSENT genesis; a present malformed genesis already returned E-GENESIS above — round-41 P1-02, falsy ≠ absent)
  if (genesis.state?.id?.domain_shard !== doc.state.id.domain_shard) return { error: 'E-GENESIS', detail: 'genesis domain mismatch' };
  const rk = resolveKeys(genesis, keylog);
  if (rk.error) return { error: rk.error, detail: rk.detail };
  const { validKeys, revoked, history } = rk;
  // §12.2a KEY-LOG FRESHNESS — "this key is still valid" is an authenticated NON-MEMBERSHIP claim: a CACHED key-log
  // proves only "revoke ∉ my view", never "revoke does not exist".
  // UST-0ol Phase 3 (P0-03) — the legacy `keylogHeadAnchor → attested` shortcut is DELETED. An anchored key-log HEAD
  // proves membership AT its anchor time, NOT that it is the LATEST head at the document's time: a revoke that
  // FOLLOWS the anchored prefix is invisible to it. Strong key-log freshness (corroborated/attested) is reachable
  // ONLY through the one checkpoint derivation (deriveCheckpointFreshness): authorization + strict terminality +
  // proven-after ordering + independent uniqueness. resolveAuthority reports at most `fresh` (single-view): a
  // round-16 P0-02 / F.5d — `fresh` is EARNED from an authenticated fetch, NEVER a raw caller string (that self-declared
  // the FreshnessStrength axis and bypassed requireFreshKeylog). Only a token minted by resolveByDiscovery into
  // VERIFIED_FRESH — bound to THIS domain + active genesis and observed no earlier than the proven U — lifts it; a bare
  // string/object is a caller assertion that stays `unverified` (⇒ requireFreshKeylog still floors to INDETERMINATE).
  let freshness = 'unverified';
  if (keylogFreshAsOf && typeof keylogFreshAsOf === 'object' && VERIFIED_FRESH.has(keylogFreshAsOf)
      && keylogFreshAsOf.domain === doc.state.id.domain_shard && keylogFreshAsOf.active_genesis === contentHash(genesis)
      && isRealRfc3339Z(keylogFreshAsOf.observed_at) && (!U || keylogFreshAsOf.observed_at >= U)) freshness = 'fresh';
  // rc.12: surface the ceremony-declared CAPACITY so callers can pass it as opts.capacity to verify()
  // once authority is established — the grant flows FROM resolution, never from a raw genesis.
  const gvCap = genesis.state?.data?.genesis?.value ?? {};
  const capacity = {   // round-24 P0-02 — canonical decimal strings only; a coercible `["4096"]` grants NO elevated capacity (falls to the floor)
    ...(canonUint(gvCap.max_partitions) !== undefined ? { maxPartitions: canonUint(gvCap.max_partitions) } : {}),
    ...(canonUint(gvCap.max_transcript_bytes) !== undefined ? { maxTranscriptBytes: canonUint(gvCap.max_transcript_bytes) } : {}),
  };
  // authority is granted ONLY if the doc's key_id maps to the doc's ACTUAL signing pub (binding, not membership).
  if (validKeys.get(doc.state.id.key_id) !== doc.sig.pub)
    return { strength: 'self-asserted', status: 'verified', detail: 'doc key not bound in this key-log' };
  // `admits(k, c)` — REFUSAL, not a downgrade (rev89). The key IS bound: the publisher's own log declares this
  // key and declares what it is for, and this document is outside it. Falling to `self-asserted` would leave
  // the document VALID at the LIGHT floor, which is precisely the outcome the role exists to prevent — a
  // leaked issuance key still producing verifying documents. The three ceremony-set roles already refuse
  // (a checkpoint signed by a non-authority is INVALID, never downgraded); this extends the same rule rather
  // than inventing a second one. Only under a DECLARED regime: a publisher with no `roles` is unaffected.
  {
    // THE REGIME GATE IS LOAD-BEARING AGAIN, and the reason is worth keeping. In rev89 it was unreachable —
    // under an undeclared regime no OPERATING role is assigned, so `allowed` was already absent — and it was
    // removed rather than left as a protection that protects nothing. rev91 gives the ROOT a class-set, and
    // the root carries its role ALWAYS, declared regime or not. Without this condition a minimal publisher
    // that signs everything with its own root key — no key log at all — would suddenly answer E-KEY on every
    // document. "A publisher that does NOTHING is unaffected in every respect" (§12.2) is the invariant, and
    // this single condition is what keeps it true.
    const r = rk.roles?.get(doc.state.id.key_id);
    const allowed = r === undefined ? null : ROLE_CLASSES[r];
    if (rk.declaredRoles && allowed && !allowed.includes(doc.state.id.class))
      return { error: 'E-KEY', detail: 'key role "' + r + '" does not admit class "' + doc.state.id.class + '" — this publisher DECLARED role separation, and the role admits ' + allowed.join('/') + ' (§12.2 admits(k,c))' };
  }
  // §12.2/#75 ROOT 1 — K_n(t) is a TWO-SIDED window over ORDERED authorization intervals (round-15 P0-02). A document is
  // key-active iff its proven anchor U lands INSIDE some active interval [from, to]; U before the FIRST authorization ⇒
  // premature; U in a retired GAP between intervals (add→retire→re-add→retire) ⇒ expired. Only decidable WITH a proven U.
  const hk = history.get(doc.state.id.key_id);                                     // U (the proven anchor upper bound) is resolved from the token at the top of resolveAuthority
  if (U && hk && U < hk.intervals[0].from)
    return { strength: 'self-asserted', status: 'premature', detail: 'document anchored before its signing key was authorized (K_n(t) lower bound §12.2)' };
  // §12.2 X1 — the UPPER bound, decided against the proven anchor U. Compromise is terminal; retirement leaves a GAP.
  // Per-interval "active until": the revoke transition R (v.to) ends the authorization window — F.5e: a key leaves the
  // active process at R, and the compromise estimate C can only SHORTEN/taint the window, never LENGTHEN it past R
  // (round-16 P1-01: rev12 used C alone, so a future C let a doc long after R still count as in-window). So the
  // compromised upper bound is min(R, C); a retired/open interval uses its own close time. A doc must land inside SOME
  // window; a doc in a retired GAP between intervals (add→retire→re-add→…) ⇒ expired.
  const rev = revoked.get(doc.state.id.key_id);
  const minStr = (a, b) => (a === null ? b : b === null ? a : a < b ? a : b);
  const inWindow = (U && hk) ? hk.intervals.some((v) => { const upper = v.end === 'compromised' ? minStr(v.to, rev.compromised_since) : v.to; return U >= v.from && (upper === null || U <= upper); }) : false;
  // round-16 P0-01 — a key that has LEFT the active set (its LAST interval is closed: retired/revoked, not re-added)
  // cannot be authorized WITHOUT a proven U: window membership is undecidable, so it must fail closed rather than fall
  // through to authoritative. (An OPEN last interval = currently active; that path is unaffected.)
  const lastClosed = !!(hk && hk.intervals[hk.intervals.length - 1].to !== null);
  let suspect = false;
  if (rev) {
    if (rev.reason === 'compromised') {
      if (!U) return { strength: 'self-asserted', status: 'revoked-untrusted', detail: 'compromised key + UNANCHORED doc → untrusted (X1)' };
      if (U >= rev.compromised_since) return { error: 'E-KEY', detail: 'signature not provably before compromise (U ≥ compromised_since, X1)' };
      if (!inWindow) return { strength: 'self-asserted', status: 'expired', detail: 'provably pre-compromise but signed outside every active window — retired GAP or past the revoke transition (K_n(t) two-sided §12.2)' };
      suspect = true;                                                             // provably pre-compromise AND within an active window, but C is a publisher estimate
    } else if (rev.reason === 'retired') {
      if (!U && lastClosed) return { strength: 'self-asserted', status: 'expired', detail: 'retired key currently out of the active set + UNANCHORED — cannot prove the signature predates retirement (K_n(t) needs a proven U, round-16 P0-01)' };
      if (U && !inWindow) return { strength: 'self-asserted', status: 'expired', detail: 'signed outside the key active interval — retired GAP / after retirement (K_n(t) two-sided §12.2 X1)' };
    }
  }
  // §12.1a / formal model F.5a — the name is now KEY-BOUND (K_n: doc key ∈ resolved set). The no-fork STRENGTH
  // is a function of the BASIS of the non-membership evidence, and honesty forbids collapsing them:
  //   · INDEPENDENT non-membership — an anchored verifiable-map inclusion (prefix-uniqueness ⇒ ¬∃rival) OR an
  //     out-of-band caller assertion (air-gap) — ⇒ `authoritative`; the map/caller does not rely on the publisher.
  //   · the publisher's OWN served witness list shows no rival ⇒ `corroborated` — a real, bounded fact (membership
  //     of A in the published set), but NOT independent non-membership (the publisher can omit a rival, F.5a).
  //   · no no-fork evidence at all ⇒ DENIED (status unavailable → the name-authority tier is not reached, W1).
  const st2 = suspect ? 'suspect' : 'verified';
  // #69 A2 P1 / P0-2 audit — `authoritative` requires INDEPENDENT non-membership, EARNED not self-declared:
  //   · a verified NAME NO-FORK EVIDENCE — a typed claim signed by a witness the CONSUMER trusts (trustRoots),
  //     bound to this domain + active genesis ⇒ `authoritative`, independently_verified, basis accepted-external-witness.
  //   · the independent name-map VERIFIER (#42) ⇒ `authoritative` once a real VerifiedMapReceipt is checked (future).
  //   · a raw `noForkConfirmed` boolean is NOT evidence — the caller vouches out-of-band (air-gap). It is a distinct
  //     `consumer-override` (independently_verified:false), NEVER silently `authoritative` (the removed overclaim,
  //     same class as `mapInclusion:true`). A consumer consciously honoring its own override sets acceptConsumerOverride
  //     at verify(); the verdict stays transparent. Present-but-invalid evidence never upgrades (fail-safe).
  const nmCur = nameMap ? mapRootBasis(trust, nameMap.mapRoot, nameMap.rootProof) : null;               // F.5a.2b — the currency basis, or NOTHING; a self-supplied root never reaches here
  if (nameMap && nmCur) {                                                            // #42 — name-map inclusion, root CONSUMER-ADMITTED (Phase 1)
    const nm = verifyActiveGenesisUniqueness(nameMap.proof, { domain_shard: doc.state.id.domain_shard, active_genesis: contentHash(genesis), mapRoot: nameMap.mapRoot });
    if (nm.authoritative) return { strength: 'authoritative', noFork: 'map-inclusion', independently_verified: true,
      basis: 'authenticated-map-uniqueness', map_root: nm.map_root, map_root_currency: nmCur.basis, ...(nmCur.as_of ? { map_root_as_of: nmCur.as_of } : {}), ...(nmCur.authority ? { map_authority: nmCur.authority } : {}), ...(nmCur.authority_domain ? { map_authority_domain: nmCur.authority_domain } : {}), status: st2, capacity, freshness };   // F.5a.2b — the ROTATION-bearing key space: an unlabelled currency would be an overstatement (a superseded binding verifies under its own root)
  }
  if (noForkEvidence !== undefined) {
    const ev = verifyNoForkEvidence(noForkEvidence, { domain_shard: doc.state.id.domain_shard, active_genesis: contentHash(genesis), trustRoots: trustRoots || {} });
    if (ev.ok) return { strength: 'authoritative', noFork: 'accepted-external-witness', independently_verified: true,
      basis: 'accepted-external-witness', witness_id: ev.witness_id, ...(ev.trust_domain ? { trust_domain: ev.trust_domain } : {}), status: st2, capacity, freshness };
  }
  // VERIFIED served-list no-fork ⇒ corroborated (HIGH, honest: membership in the published set, NOT independent
  // non-membership, F.5a). P0-1 (rc.35 audit) — the trust is an UNFORGEABLE internal token: only resolveByDiscovery
  // (which actually ran witnessNoFork: network fetch + anchor cross-check) can mint a servedNoFork into VERIFIED_SERVED.
  // A transcript caller's plain object {confirmed:true, active_genesis:<public hash>} is NOT in the set — the binding
  // hash it holds is public, so without the token it earns nothing beyond a caller assertion.
  if (servedNoFork?.confirmed === true && VERIFIED_SERVED.has(servedNoFork) && servedNoFork.active_genesis === contentHash(genesis))
    // round-237 (#171) — the JOIN, and it is the half the probe repair alone does not give. Once the witness is always
    // asked, a caller that ALSO asserts no-fork lands here rather than in the override branch below, and returning a
    // bare `corroborated` would silently drop its axiom — the same information loss in the other direction, now costing
    // the consumer the lift it consciously asked for (measured: the honored-override case fell from TOP to HIGH). Both
    // bases are carried: the strength is the EARNED one, and `override_liftable` rides on it so `acceptConsumerOverride`
    // still reaches `authoritative`. `independently_verified` stays false — a join with an axiom never earns it.
    return { strength: 'corroborated', noFork: 'served-list', status: st2, capacity, freshness,
      ...((ncf === true || cor === true) ? { override_liftable: true, independently_verified: false, axiom: 'caller-asserted', detail: 'served-list corroboration AND a caller no-fork axiom — the earned strength is reported, the axiom rides beside it (round-237)' } : {}) };
  // a bare `corroborated`/`noForkConfirmed` boolean OR an unminted `servedNoFork` is a CALLER ASSERTION, not a verified
  // predicate (a STATELESS verifier cannot fetch a served list). Like noForkConfirmed it is consumer-override, NEVER a
  // silent corroborated (self-audit rc.35 — same class as the removed `corroborated:true`/`mapInclusion:true`).
  if (ncf === true || cor === true) return { strength: 'consumer-override', noFork: 'caller-asserted', override_liftable: true, independently_verified: false, status: st2, capacity, freshness,
    ...(noForkEvidence !== undefined ? { detail: 'noForkEvidence rejected → consumer-override only (not independently verified)' } : {}) };   // round-42 P1-01 — ONLY an explicit boolean AXIOM (noForkConfirmed/corroborated===true) creates a LIFTABLE consumer-override (acceptConsumerOverride may raise it to authoritative — the consumer consciously honors ITS OWN out-of-band claim)
  if (servedNoFork != null) return { strength: 'consumer-override', noFork: 'served-lookalike', override_liftable: false, independently_verified: false, status: st2, capacity, freshness, detail: 'an unminted servedNoFork is a caller look-alike, not verified evidence and not an explicit axiom — it DIVERTS from corroborated (rc35-P0a) but is NOT liftable to authoritative by acceptConsumerOverride (round-42 P1-01)' };   // a present-but-unminted servedNoFork: neither corroborated nor a HIGH-reachable override
  // rev93 — an empty slot takes ABSENCE, never an invented word. `unconfirmed` denoted nothing: measured 0 occurrences
  // in the spec, the formal model and the vectors, while riding out under the spec's OWN `no_fork` field name, where a
  // consuming agent reading the field alone takes it for a basis that was established. Absence is licensed here, and
  // only because `status: 'unavailable'` is the named sibling that carries the meaning — the verdict door's spread
  // drops `no_fork` when `noFork` is unset. The STRENGTH on this branch is a separate, open question (#96): the text
  // does not settle it, and a sibling branch ships `corroborated` with a non-`verified` status deliberately.
  return { strength: 'self-asserted', status: 'unavailable', capacity, freshness,
    detail: 'no independent no-fork evidence; a served witness only corroborates (§12.1a F.5a) → authority pending, retry' };
}

// ─── TOP §11.2 anchor-proof: recompute the Merkle inclusion path content_hash→root (RFC 6962, domain-sep
//     ust:leaf/ust:node). The SUBSTRATE check (e.g. bitcoin-ots) is DELEGATED to opts.substrateVerify (needs
//     external Bitcoin access — the caller/@ust-protocol/operator's job). Returns { inclusion, time, status, anchorTime? }.
// round-28 P1-02 — public DOOR admits the proof once (a hostile getter → structured E-ANCHOR, never a host throw); the
// internal verifyCore calls verifyAnchorCore over the ALREADY-admitted doc.proof, so the substrate-receipt identity shim
// (`a === capA`) is not broken by a re-clone. Same public-door/internal-core split as verify.
export function verifyAnchor(contentHash, proof, opts = {}) {
  if (typeof contentHash !== 'string') return { inclusion: false, error: 'E-ANCHOR', detail: 'contentHash must be a string (round-29 P1-01 totality — a hostile object at arg 0 cannot throw at this door)' };
  const O = admitOpts(opts);   // round-46 self-audit (Theorem R — trusted before untrusted) — admit opts BEFORE admitDeep(proof) fires the proof's [[Get]] face: else a hostile proof getter mutates the still-live opts.substrateVerify (the substrate oracle verifyAnchorCore trusts) — the r45 cross-argument class. admitOpts ALSO freezes → the guard read `if(!opts.substrateVerify)` and the call `opts.substrateVerify(...)` see the SAME oracle (a two-face opts cannot pass the guard with one function and be called with another).
  if (O === null) return { inclusion: false, error: 'E-ANCHOR', detail: 'opts must be an inert record (round-46 totality — a hostile accessor/Proxy at opts is a structured reject)' };
  const Pf = admitDeep(proof);
  if (Pf === ADMIT_REJECT) return { inclusion: false, error: 'E-ANCHOR', detail: 'proof is not an inert record (round-28 totality — a hostile getter cannot throw at this door)' };
  return verifyAnchorCore(contentHash, Pf, O);
}
function verifyAnchorCore(contentHash, proof, opts = {}) {
  opts = opts || {};                                             // round-18 P1-01 — a default param only catches `undefined`; coerce `null` too (total boundary)
  // fail-closed on a malformed proof: validate shape BEFORE recomputing (no TypeError, no dir!=L ⇒ R fallthrough).
  const HASH = /^sha256:[0-9a-f]{64}$/;
  // F.9.5-c.6 (round 205) — `path` IS the reference construction's body, not a member every proof owes. A proof
  // declaring another construction carries that construction's body in `inclusion` and has no `path` at all;
  // demanding one here refused the very shape §11.2 now registers, and the refusal arrived as E-ANCHOR — a verdict
  // about the evidence for what was our own grammar mismatch. Measured: the operator's own producer output, handed
  // over verbatim, was rejected at this line. `root` stays REQUIRED for every construction: it is the commitment
  // handed to the substrate, and no body replaces it.
  // CLOSED 2026-08-12 (round 205): `path` is required only for the reference construction; a declared body stands
  // in its place, and the producer output now verifies verbatim.
  if (!proof || typeof proof !== 'object' || !HASH.test(proof.root || ''))
    return { inclusion: false, time: 'unproven', status: 'verified', error: 'E-ANCHOR', detail: 'malformed anchor proof' };
  if (proof.inclusion === undefined && !Array.isArray(proof.path))
    return { inclusion: false, time: 'unproven', status: 'verified', error: 'E-ANCHOR', detail: 'malformed anchor proof: no `inclusion` body and no `path` — the reference construction requires `path`' };
  // ── inclusion is a CONNECTOR, not a protocol constant (owner call 2026-07-26; core+connectors at every level is what
  // makes this adoptable). The tagged `ust:leaf`/`ust:node` walk below is the BUNDLED reference connector — one tree
  // convention among several, not the convention. An operator anchoring with an RFC 6962 log (leaf = SHA256(0x00‖·),
  // node = SHA256(0x01‖L‖R) over raw digests, which is what Certificate Transparency uses and what the reference
  // operator already runs in production) supplies `opts.inclusionVerify` and needs no protocol change.
  //
  // The default is kept deliberately: every anchor proof issued before this is still confirmed with no adapter, so
  // delegation costs nothing already in the field. What changes is normativity — the core no longer NAMES a substrate's
  // tree, which is the boundary the operator-side split recorded and the code had drifted from.
  // The BUNDLED reference connector, NAMED because two call sites need it: no connector supplied at all, and a composed
  // router that declined every plugin. Two copies of one hash walk is precisely the drift class this repo keeps paying for.
  const bundledWalk = () => {
    for (const p of proof.path) if (!p || (p.dir !== 'L' && p.dir !== 'R') || !HASH.test(p.hash || '')) return { shapeError: true };
    let node = Hbytes('ust:leaf', utf8(contentHash));
    for (const p of proof.path) node = Hbytes('ust:node', utf8(p.dir === 'L' ? p.hash + node : node + p.hash));
    return { ok: node === proof.root };
  };
  let inclusion;
  if (typeof opts.inclusionVerify === 'function') {
    // A not-ours module: its INVOCATION and its RETURN are hostile (trust-boundary law UST-5tm). The whole seam is ONE
    // total door — identical discipline to the substrate seam below, deliberately not a second pattern.
    try {
      const inc = opts.inclusionVerify(contentHash, proof);
      // round-236 (#173) — a promise is `⊘`, not `⊥`. This returned `inclusion: false`, which `verify` reads as a path
      // that does not reach its root, so the document was called INVALID because the READER's connector was async —
      // inability reported as guilt, the class F.9.5-c.5 exists to forbid. It decides a whole host family rather than a
      // case: `sha256` is async in every browser, so EVERY browser realization of EVERY construction is a promise, and
      // under `⊥` no browser could ever admit a foreign construction. Withheld by OMITTING `inclusion`, the same
      // representation the unsupported-construction branch below uses, and the async door now pre-resolves the seam so
      // this is a genuine mode mismatch rather than the only door.
      if (inc && typeof inc.then === 'function')
        return { time: 'unproven', status: 'unavailable', reason: 'unsupported_construction', detail: 'inclusion connector is ASYNC and this is the SYNCHRONOUS door — the answer is WITHHELD, never a refusal of the proof (F.9.5-c.5, round-236). Use verifyAsync() or resolveByDiscovery(), which resolve the connector before deciding.' };
      // A CLOSED, TYPED leaf: strict Boolean only. A truthy non-Boolean ('yes', {}) must never mint inclusion.
      // `null` is the ROUTER's "no installed connector claimed this proof" — distinct from a refusal. Falling through to
      // the bundled walk keeps a composed router safe to pass unconditionally: a caller need not know in advance whether it
      // holds a connector for this substrate. Any OTHER non-Boolean is still a refusal (a typed closed leaf, F.5.0/C3/I4).
      if (inc === null || inc === undefined) { inclusion = undefined; }
      else if (inc !== true && inc !== false)
        return { inclusion: false, time: 'unproven', status: 'unavailable', detail: 'inclusion connector must return a strict Boolean (a typed closed leaf, F.5.0/C3/I4)' };
      else inclusion = inc;
    } catch {
      return { inclusion: false, time: 'unproven', status: 'unavailable', detail: 'inclusion seam is not-ours (UST-5tm): a hostile connector return/throw is a structured reject, never a host throw' };
    }
  }
  if (inclusion === undefined) {
    // F.9.5-c.4/c.5 (round 201) — BEFORE walking, read WHICH construction the proof declares. F.9.5-c.3 already
    // required a produced proof to NAME its tree; nothing read the name, so naming constrained nobody. Measured
    // 2026-08-11 on the tree as it stood: a proof declaring ANY construction — `acme-tree-v9`, `42`, `null`, `{}` —
    // whose path satisfied the reference walk was answered `inclusion: true`. The core confirmed membership under a
    // name it never examined, and answered `false` — blaming the proof — for an honest foreign tree it could not
    // walk. Worse than either: a consumer holding a connector and a consumer holding none returned DIFFERENT
    // verdicts on the same document, while §11.2 rests agreement on determinism.
    //
    // `name` is TOTAL (F.9.5-c.4): a proof that declares nothing declares the reference construction, so every
    // proof ever issued is decided exactly as before. That is the compatibility clause — it lives in the definition,
    // not in a grandfather branch here.
    //
    // The answer for a construction this build cannot compute OMITS `inclusion` rather than carrying a third value.
    // Measured over this tree, consumers read the member two ways: `x.inclusion === true` and `!x.inclusion`. A third
    // VALUE is minted as inclusion by the loose form when truthy; omission makes the strict form structurally unable
    // to mint it — the dangerous direction — and leaves every loose reader an enumerated site answered by name.
    // ONE rule — "a declared construction binds the reader" — with TWO carriers today, and the second one is the
    // reason this is not merely forward-looking. The normative carrier is `proof.inclusion.construction`: §11.2
    // holds membership and substrate to be TWO INDEPENDENT proofs that must not be conflated, and `anchor` is the
    // substrate's Locator, so a tree name does not belong inside it. But the shipped `@ust-protocol/rekor-verify`
    // connector already routes on `proof.anchor.inclusion.scheme`, and the operator's producer already emits it.
    // Leaving that carrier unread would leave the measured defect exactly where it lives: a consumer holding the
    // rekor connector and a consumer holding none would still return different verdicts on that very shape. It is
    // read here ONLY to withhold — never to confirm — so a legacy declaration can lower the answer and never lift
    // it. Moving the carrier and deleting this branch is thelabmd/UST-Protocol#149, not this round.
    // PRESENCE of the carrier is the declaration, and the extraction is total. A first cut read
    // `carrier.scheme ?? carrier.construction`, which let `{scheme: null}` fall through to the reference walk —
    // `??` skips null — so a proof carrying an inclusion object with no readable name was still confirmed. Nothing
    // about "I brought a construction object" says "walk it with yours", whatever the member holds.
    const carrier = proof.inclusion !== undefined ? proof.inclusion : proof.anchor?.inclusion;
    const declared = carrier !== null && typeof carrier === 'object' && !Array.isArray(carrier)
      ? (carrier.construction !== undefined ? carrier.construction : carrier.scheme)
      : carrier;
    if (carrier !== undefined && declared !== REGISTRY.referenceConstruction) {
      // #153 — the refusal ROUTES. The verdict is unchanged and must be: this is `ℐ_v` in the F.5.1 table, the
      // consumer's column, so the honest report names WHICH connector claims the declared construction. Generated
      // from REGISTRY.inclusionConstructions, never spelled here — the set is open by §11.2, and a name the map
      // does not carry gets the registry itself rather than a sentence that reads as "no such thing exists".
      const named = typeof declared === 'string' ? `'${declared.slice(0, 40)}'` : `of type ${typeof declared}`;
      const claimant = typeof declared === 'string' ? REGISTRY.inclusionConstructions[declared] : undefined;
      const route = claimant
        ? `That construction is claimed by ${claimant}.`
        : `No registered construction carries that name either — registered: ${Object.keys(REGISTRY.inclusionConstructions).map((k) => `'${k}'`).join(', ')}.`;
      return { time: 'unproven', status: 'unavailable', reason: 'unsupported_construction',
        detail: `anchor proof declares inclusion construction ${named} — this build implements '${REGISTRY.referenceConstruction}' and no installed connector claimed it. ${route} The answer is WITHHELD, never the reference walk applied to another tree (F.9.5-c.5)` };
    }
    // No connector, or a router that claimed nothing. MEASURED before this existed: an empty router killed a VALID proof
    // with 'inclusion path does not reach root' — blaming the proof for the caller having no plugin installed. The shape
    // error names the BUNDLED connector, not the protocol, and points at where another tree plugs in.
    const w = bundledWalk();
    if (w.shapeError) return { inclusion: false, time: 'unproven', status: 'verified', error: 'E-ANCHOR', detail: 'malformed path entry (dir must be "L"|"R", hash sha256:hex) — shape of the BUNDLED inclusion connector; another tree supplies opts.inclusionVerify' };
    inclusion = w.ok;
  }
  if (!inclusion) return { inclusion: false, time: 'unproven', status: 'verified', detail: 'inclusion path does not reach root' };
  if (!opts.substrateVerify) return { inclusion: true, time: 'unproven', status: 'unavailable', detail: 'inclusion OK; substrate not verified (caller job)' };
  // The substrate oracle is a NOT-OURS module (OTS/Rekor/git/IPFS/Bitcoin — open-ended, third-party). Per the
  // trust-boundary law (UST-5tm), its INVOCATION and its RETURN are HOSTILE input: a hostile plugin may throw on
  // the call itself, or return a revoked/throwing-getter Proxy that host-throws when we touch `.then` / decode it.
  // The whole seam is ONE TOTAL door — any host throw becomes a structured `unavailable` reject, never propagates
  // (round-52 P1 / S1 module-seam totality). The behavioral gate drives this seam with the hostile BATTERY.
  let dec, ref = null;
  try {
    const sub = opts.substrateVerify(proof.anchor, proof.root);          // → { final, time }
    // #69 E1 — the official substrate plugins are ASYNC; a sync verify() cannot await one. Detect the thenable
    // and say so HONESTLY (not a silent 'unproven') — the caller must use verifyAsync / resolveByDiscovery,
    // which pre-resolve the substrate. Fail-safe either way: a Promise is never mistaken for a final anchor.
    if (sub && typeof sub.then === 'function') return { inclusion: true, time: 'unproven', status: 'unavailable', detail: 'substrate check is ASYNC — use verifyAsync() or resolveByDiscovery() (they await it), not sync verify()' };
    if (!sub) return { inclusion: true, time: 'unproven', status: 'unavailable', detail: 'substrate unreachable' };
    // round-17 P1-01 — the substrate result is a CLOSED, TYPED leaf (F.5.0/C3/I4): `final` must be a strict Boolean and
    // a final anchor MUST carry a REAL RFC3339-Z instant. A truthy non-Boolean ("yes") or a non-string/empty time
    // ({}) can no longer mint TimeStrength=anchored (and can no longer coerce past the N9 generated_at ≤ anchor check).
    dec = decodeSubstrate(sub);   // round-19 P0-02 — read the anchored instant + assurance from the INERT decoded record, never the live seam object
    // The negative answer is decoded HERE, inside the same guarded door: reading a not-ours object outside the
    // try would be a second, unguarded touch of the very value the door exists for.
    if (!dec) ref = decodeSubstrateRefusal(sub);
  } catch {
    return { inclusion: true, time: 'unproven', status: 'unavailable', detail: 'substrate seam is not-ours (trust-boundary law UST-5tm): a hostile module return/throw is a structured reject, never a host throw (round-52 P1 / S1 module-seam totality)' };
  }
  if (!dec && ref) {
    // The plugin was reached and answered NO. That used to share a sentence with a malformed receipt; they are
    // two different facts, and only one of them is the consumer's problem to fix.
    return { inclusion: true, time: 'unproven', status: 'unavailable', detail: `substrate answered NOT final${ref.reason ? ` (${ref.reason})` : ''} — a STATED refusal from the connector, not a malformed receipt and not an unreachable substrate` };
  }
  if (!dec) return { inclusion: true, time: 'unproven', status: 'unavailable', detail: 'substrate not a typed FINAL receipt (final must be an OWN Boolean true AND carry an OWN real RFC3339-Z instant on a plain record; a prototype/accessor look-alike earns nothing) — no F_t (round-17 P1-01 / round-18 P0-02 / round-19 P0-02)' };
  // #71 — carry the substrate's ASSURANCE basis so TOP names its trust model honestly (an OTS plugin that
  // corroborates via independent explorers reports `explorer-corroborated`; an operator real-node/SPV plugin
  // would report `bitcoin-node`). TOP is earned either way; assurance says HOW, never inflating the tier.
  return { inclusion: true, time: 'anchored', status: 'verified', anchorTime: dec.time, ...(dec.assurance ? { assurance: dec.assurance } : {}) };
}

// #69 E1 — the ONE async entry: verify() is deliberately sync (portable, no await in the hot path), but the
// official substrate plugins are async. verifyAsync pre-resolves a doc.proof's substrate ONCE (await), then
// runs the sync verifier with the resolved receipt as a sync shim — so TOP is reachable with async plugins
// through a single contract, and verify() never has to become async. Everything else is identical to verify().
// #144 — the SECOND async faculty. `verifyCore` is synchronous; a browser offers Ed25519 only through the
// asynchronous `crypto.subtle`. Run the core; if it refused for want of the primitive — `INDETERMINATE
// (unsupported_alg)`, the honest answer the sync door already produces — resolve the signatures it asked about
// and run it again with real answers. See `_sigmemo.mjs` for why the memo is verifier-owned and why the set of
// triples is found by replay rather than by walking the document.
//
// Every exit keeps the fail direction: a faculty that is absent in BOTH forms, a triple the async form could not
// answer, or a run that did not converge all return the refusal unchanged. Nothing here can turn "I could not
// check" into `false`.
const SIG_RESOLUTION_MAX_PASSES = 64;   // a document carries finitely many signatures; this is a runaway backstop, not a limit anyone should reach
const subtleEd25519 = async (pubB64url, msgUtf8, sigB64url) => {
  try {
    const k = await crypto.subtle.importKey('raw', fromB64url(pubB64url), { name: 'Ed25519' }, false, ['verify']);
    return await crypto.subtle.verify({ name: 'Ed25519' }, k, fromB64url(sigB64url), utf8(msgUtf8)) === true;
  } catch { return null; }   // NOT `false`: a build without Ed25519 in `crypto.subtle` is INABILITY, and the caller keeps its refusal
};
// round-239 (UST-mbso) — the faculty is a property of a PURE SYNCHRONOUS CALL, not of `verifyCore`. It was written
// for one caller and the wrapper stayed there, so every other synchronous consumer of `edVerifyStrict` kept refusing
// in a browser that HAS the primitive: measured on the live page, `resolveAuthority` answered "genesis not decidable
// by this build" while `crypto.subtle` Ed25519 was available, and HIGH was unreachable for every name-form document.
// The replay loop never depended on what it replayed — only on PURITY, which is why the memo lives at the leaf
// (`edVerifyStrict` → `consultSignature`) and already covered genesis, key-log, cadence and checkpoint signatures.
// So the loop is generalized over the call, and its precondition is stated where it can be checked: `runSync` MUST
// be pure and clock-free, because a pass is DISCARDED and re-run. `resolveByDiscovery` is not — it fetches — which
// is exactly why the wrapper goes around its pure sub-calls and never around the door itself.
// `memo` is threaded so one discovery resolves each signature ONCE across genesis → cadence → authority.
async function withSignatureFaculty(runSync, refused, memo = new Map()) {
  const sync = runSync();
  if (!refused(sync)) return sync;
  if (typeof crypto === 'undefined' || !crypto?.subtle) return sync;
  for (let pass = 0; pass < SIG_RESOLUTION_MAX_PASSES; pass++) {
    let verdict, asked;
    beginSignatureResolution(memo);
    try { verdict = runSync(); } finally { asked = unresolvedSignatures(); endSignatureResolution(); }
    // Nothing new was asked: every question this run put had its REAL answer, so this pass IS the real run.
    if (!asked.length) return verdict;
    for (const t of asked) {
      const ok = await subtleEd25519(t.pub, t.msg, t.sig);
      if (ok === null) return sync;   // the async faculty is missing too — the synchronous refusal is still the honest answer
      recordSignature(memo, t.pub, t.msg, t.sig, ok);
    }
  }
  return sync;
}
const verifyCoreWithSignatureFaculty = (doc, opts) => withSignatureFaculty(() => verifyCore(doc, opts), cannotDecide);
// The two REFUSAL SHAPES, each read from the registry that owns it rather than from a literal spelled here. A §14
// verdict says `INDETERMINATE(unsupported_alg)`; a resolver returns a record with no verdict to give and fails CLOSED
// with an E-code held in its OWN set (`REGISTRY.resolverErrorCodes`). Testing the DETAIL STRING instead would pin this
// wrapper to today's wording and start defending it the day the wording changes.
const resolverCannotDecide = (r) => REGISTRY.resolverErrorCodes.includes(r?.error);

export async function verifyAsync(doc, opts = {}) {
  opts = admitOpts(opts);                                        // round-19 P1-02 — inert snapshot; a throwing accessor/Proxy trap → null → structured reject (not a host throw)
  if (opts === null) return { result: 'INVALID', error: 'E-MALFORMED', detail: 'opts must be an inert record (round-19 P1-02 totality)' };
  const D = admitDeep(doc);                                      // round-27 (3) — admit ONCE at this public door; below runs verifyCore over the inert snapshot (a getter can't split the substrate await from the sync verify)
  if (D === ADMIT_REJECT) return { result: 'INVALID', error: 'E-MALFORMED', detail: 'document is not an inert record (round-27: the ONE input boundary)' };
  doc = D;
  if (!doc?.proof || (!opts.substrateVerify && !opts.inclusionVerify) || opts.offline) return finishVerdict(doc, await verifyCoreWithSignatureFaculty(doc, opts));
  // round-17 P0-01 — verify an IMMUTABLE SNAPSHOT. The live object could be swapped between the await and the sync
  // verify (TOCTOU): the substrate receipt is obtained for root A, then a mutated document B with root B reuses it and
  // gets VALID:TOP for evidence that was never its own (I4/F.5c: TimeStrength=anchored must be evidence for content_hash(d)).
  // Snapshot before the await; the sync shim ALSO binds the receipt to the captured (anchor, root), so a different root
  // can never claim it.
  let snap; try { snap = JSON.parse(JSON.stringify(doc)); } catch { return finishVerdict(doc, await verifyCoreWithSignatureFaculty(doc, opts)); }
  const capA = snap.proof?.anchor, capR = snap.proof?.root;
  let receipt; try { receipt = opts.substrateVerify ? await opts.substrateVerify(capA, capR) : null; } catch { receipt = null; }
  // round-236 (#173) — the INCLUSION seam gets the treatment the substrate seam has had all along, because the reason is
  // identical: a connector this host can only compute asynchronously is unobservable at the synchronous core, and until
  // now that made every browser connector unusable. Resolved ONCE here, then handed to the sync core behind a shim bound
  // to the (content_hash, proof) it was obtained for — the same TOCTOU defence as `a === capA`: a settled `true` must
  // never be replayable against a different document or a swapped proof. A connector that throws is `null` (the router's
  // "unclaimed"), which falls through to the declared-construction check rather than minting an answer.
  // `contentHash` is computed INSIDE the guard, not above it: this door is reached by malformed input too (the seam
  // battery drives a bare `{proof}` with no state), and a throw here would be a host throw at a public entry point —
  // the S1 violation this file spends its totality discipline on. Hoisting it cost exactly that, and the seam gate
  // caught it. No connector ⇒ nothing to pre-resolve ⇒ the hash is never needed.
  const capP = snap.proof;
  let capCH, incReceipt = null;
  if (opts.inclusionVerify) {
    try { capCH = contentHash(snap); } catch { capCH = undefined; }
    if (capCH !== undefined) { try { incReceipt = await opts.inclusionVerify(capCH, capP); } catch { incReceipt = null; } }
  }
  const shim = { ...opts, substrateVerify: (a, r) => (a === capA && r === capR ? receipt : null) };
  if (opts.inclusionVerify && capCH !== undefined) shim.inclusionVerify = (c, p) => (c === capCH && p === capP ? incReceipt : null);
  return finishVerdict(snap, await verifyCoreWithSignatureFaculty(snap, shim));   // verifyCore (NOT verify): no re-clone, so both receipt-identity bindings hold across the door
}

// §3.1/F.5c FORK-CHOICE — canonical = anchor-included. One `ust_id` may have several candidate documents with
// DISTINCT content_hashes (the honest dual-writer race — main + failover both seal the slot — or an adversary
// offering two states). The CANONICAL document is the one whose content_hash is in the authority's anchored hour
// root; a consumer holding more than one resolves DETERMINISTICALLY from the chain, not from local arrival order
// (Proposition F.5c). Async because "anchor-included" means substrate-final, which verifyAsync awaits. Fail-safe:
// with no substrateVerify NO candidate is anchored ⇒ INDETERMINATE, never a guessed winner. Returns ONE verdict.
export async function forkChoice(candidates, opts = {}) {
  opts = admitOpts(opts);                                              // round-20 P2-01 — forkChoice missed admitOpts in rev16; a hostile opts Proxy threw at `{...opts}`. Now the same inert admission as the other boundaries.
  if (opts === null) return { kind: 'fork-choice', result: 'REFUSED', error: 'E-MALFORMED', detail: 'opts must be an inert record (round-20 P2-01 totality)' };
  try { if (!Array.isArray(candidates) || candidates.length === 0) return { kind: 'fork-choice', result: 'REFUSED', error: 'E-MALFORMED', detail: 'forkChoice needs a non-empty array of candidate documents' }; }
  catch { return { kind: 'fork-choice', result: 'REFUSED', error: 'E-MALFORMED', detail: 'forkChoice needs a non-empty array of candidate documents' }; }   // round-51 — Array.isArray/.length THROW on a revoked Proxy; guard the first type-probe (totality class)
  if (candidates.length > BOUNDS.forkCandidates)                      // round-21 P1-02 — F.9 fan-out: refuse an over-budget candidate count BEFORE snapshotting/verifying (never truncate a fork-choice input)
    return { kind: 'fork-choice', result: 'INDETERMINATE', reason: 'resource_limit', detail: `forkChoice got ${candidates.length} candidates > ${BOUNDS.forkCandidates} (§F.9 fan-out — refused, never truncated; round-21 P1-02)` };
  // round-19 P0-01 — SNAPSHOT every candidate to an inert clone BEFORE ANY read, INCLUDING the ust_id grouping below.
  // rev15 still (a) read `c.state.id.ust_id` off the LIVE object to group, and (b) fell back to the live object (`d=c`)
  // when JSON cloning threw — so a one-shot throwing toJSON, or a ust_id accessor that lies once, let the classified/
  // returned value diverge from the verified snapshot. A snapshot failure is now a structured E-MALFORMED, NEVER a
  // live-object fallback: the value grouped, verified, and returned as canonical is the SAME frozen bytes
  // (F.5c: canonical(ust_id) = the unique dᵢ whose content_hash ∈ leaves — a mutable original re-read mid-await breaks it).
  const snaps = [];
  for (const c of candidates) {
    let d; try { d = JSON.parse(JSON.stringify(c)); } catch { return { kind: 'fork-choice', result: 'REFUSED', error: 'E-MALFORMED', detail: 'forkChoice candidate is not an inert JSON snapshot (throwing toJSON / hostile accessor) — no live-object fallback (round-19 P0-01)' }; }
    if (d === null || typeof d !== 'object') return { kind: 'fork-choice', result: 'REFUSED', error: 'E-MALFORMED', detail: 'forkChoice candidate did not snapshot to a JSON object (round-19 P0-01)' };
    snaps.push(d);
  }
  const ids = new Set(snaps.map((d) => d?.state?.id?.ust_id));         // group from the SNAPSHOT, never the live object
  if (ids.size !== 1 || ids.has(undefined))                            // fork-choice is PER-SLOT — a mixed batch is a caller bug, not a fork
    return { kind: 'fork-choice', result: 'REFUSED', error: 'E-MALFORMED', detail: 'forkChoice candidates must all share one ust_id (fork-choice is per-slot)' };
  const ust_id = [...ids][0];
  // verify each at its NATURAL tier (strip the floors — forkChoice does its own tier logic). A candidate is
  // ANCHOR-INCLUDED iff it VERIFIES and its content_hash sits in a substrate-final anchored root (time 'anchored').
  const vopts = { ...opts, requireAnchored: false, requireAuthoritative: false };
  // the object returned as canonical MUST be the exact snapshot dᵢ whose content_hash is in F_t (F.5c) — verify the
  // SAME frozen dᵢ that is grouped and returned, so classification, content_hash, and returned value cannot diverge.
  // round-21 P1-02 — the candidate BUDGET above bounds the fan-out; there is NO dedupe. rev18 dedup'd by content_hash,
  // which was LOSSY (round-22 self-catch): content_hash covers the STATE, not the `proof`, so two candidates with one
  // state but different anchor proofs collapsed — dropping a VALID proof behind an invalid one and HIDING an
  // equivocation (the same lossy-projection class the witness fix closed). Verify EVERY snapshot; the budget caps the work.
  const verds = await Promise.all(snaps.map(async (d) => ({ doc: d, v: await verifyAsync(d, vopts) })));
  const anchored = [], losers = [], invalid = [], unauthenticated = [];
  for (const { doc, v } of verds) {
    if (!/^VALID:/.test(v.result || '')) { invalid.push({ result: v.result, detail: v.detail }); continue; }
    // #75 P0-03a — the authority is the RESOLVED one (key ∈ K_A via genesis+key-log), NEVER the unverified LIGHT
    // `domain_shard` claim. A candidate whose key is NOT bound to its claimed authority (self-asserted / pinned)
    // is an IMPOSTER for that name and can NEVER be canonical under it — the math: content_hash commits the
    // CLAIMED domain, authority resolution proves the claim (ROOT 1+2). No manifest needed to close this.
    // rev94 — the decision must AGREE with the projection, and neither report field alone does. Measured over the
    // seam grid: the DERIVED coordinate is a loosening (π_override lifts a `consumer-override` seam to
    // `authoritative` before derivation, so it can carry a rung a caller BOOLEAN minted — a fork adjudicated by the
    // assertion of the party asking); the SEAM label alone disagreed with the coordinate in 6 of 10 states — every
    // one where C3 neutralizes it (`suspect`, `unavailable`, `expired`, `premature`) still read as binding.
    // The conjunction equals `derived identity ≥ corroborated ∧ ¬consumer-lifted`, with zero disagreements: the
    // first conjunct excludes the lift (the verdict deliberately KEEPS the seam label beside the lifted coordinate,
    // which is what makes the exclusion expressible), the second reproduces C3.
    // The seam-only reading was observationally sound only because a neutralized label floors the tier to LIGHT and
    // §14 turns a name-form LIGHT document into INDETERMINATE — but §14 EXEMPTS genesis/key/cadence, which stay
    // VALID:LIGHT and DO reach here. The guard was standing two functions away, in a filter whose exemption list is
    // a separate decision.
    const bound = v.identity
      && (v.identity.strength === 'authoritative' || v.identity.strength === 'corroborated')
      && v.identity.status === 'verified';
    if (!bound) { unauthenticated.push({ content_hash: v.content_hash, claimed: doc?.state?.id?.domain_shard, anchored: v.time?.strength === 'anchored' }); continue; }
    const rec = { doc, content_hash: v.content_hash, authority: doc.state.id.domain_shard, tier: v.tier };   // domain_shard now VERIFIED-bound (resolveAuthority checked genesis.domain == doc.domain)
    (v.time?.strength === 'anchored' ? anchored : losers).push(rec);   // anchored = in the chain; else a valid non-anchored candidate
  }
  if (anchored.length === 0)                                           // no BOUND candidate in Fₜ yet — undecidable, wait or resolve at HIGH
    return { kind: 'fork-choice', result: 'INDETERMINATE', ust_id, reason: 'no authoritative anchor-included candidate', detail: 'no candidate is BOTH bound to its claimed authority (key ∈ key-log) AND in a substrate-final anchored root', losers: losers.length, invalid: invalid.length, ...(unauthenticated.length ? { unauthenticated } : {}) };
  // group anchored by AUTHORITY (domain_shard). A single authority with ≥2 DISTINCT anchored content_hashes for
  // one ust_id is EQUIVOCATION — it signed a root containing both = a non-repudiable, punishable fault (E-PREV).
  const byAuth = new Map();
  for (const a of anchored) { if (!byAuth.has(a.authority)) byAuth.set(a.authority, new Set()); byAuth.get(a.authority).add(a.content_hash); }
  for (const [authority, hashes] of byAuth) if (hashes.size >= 2)
    return { kind: 'fork-choice', result: 'REFUSED', error: 'E-PREV', ust_id, authority, detail: `operator equivocation: authority ${authority} anchored ${hashes.size} distinct content_hashes for one ust_id`, content_hashes: [...hashes].sort() };   // sorted — set-determined, not arrival-order
  if (byAuth.size > 1)                                                 // distinct NAMES sharing a ust_id string — not a fork; canonicity is per-authority
    return { kind: 'fork-choice', result: 'MULTI_AUTHORITY', ust_id, detail: 'distinct authorities anchored the same ust_id string — not a fork (canonicity is per-authority)', canonicals: anchored.map((a) => ({ authority: a.authority, content_hash: a.content_hash })).sort((x, y) => (x.authority + x.content_hash < y.authority + y.content_hash ? -1 : 1)) };
  // round-22 P1-01 — the returned canonical must be a function of the candidate SET, not arrival order (F.5c: "two
  // consumers with the same candidate set … emit the SAME canonical"). All `anchored` here share ONE content_hash (byAuth
  // passed), but they may be equal-state / different-valid-proof variants; pick the one with the smallest canonical
  // serialization so `forkChoice([A,B])` and `forkChoice([B,A])` return the identical document.
  // round-23 P1-03 — the ordering key must be TOTAL over the admitted candidate domain: canon(doc) can THROW on outer
  // proof extras (a valid TOP doc whose proof carries numeric diagnostics), which left winner=anchored[0] (arrival order).
  // JSON.stringify over the inert snapshot never throws and is a total order, so min is set-determined for EVERY candidate.
  const jkey = (d) => { try { return JSON.stringify(d); } catch { return ''; } };
  let winner = anchored[0], winnerKey = jkey(winner.doc);
  for (const a of anchored) { const k = jkey(a.doc); if (k < winnerKey) { winner = a; winnerKey = k; } }
  // round-22 P1-01 + round-23 P1-04 — the WHOLE return is a function of the candidate SET; every diagnostic array is
  // sorted by its FULL record (a total tie-break), because content_hash alone leaves equal-hash records in arrival order.
  const bj = (x, y) => { const a = jkey(x), b = jkey(y); return a < b ? -1 : a > b ? 1 : 0; };
  return { kind: 'fork-choice', result: 'CANONICAL', ust_id, authority: winner.authority, content_hash: winner.content_hash, tier: winner.tier, canonical: winner.doc,
    losers: losers.map((l) => ({ content_hash: l.content_hash, tier: l.tier, reason: 'valid but not anchor-included for this slot (out-raced or unanchored)' })).sort(bj),
    ...(unauthenticated.length ? { unauthenticated: unauthenticated.slice().sort(bj) } : {}),   // key-unbound impostors under a claimed name — recorded, never canonical
    ...(invalid.length ? { invalid: invalid.slice().sort(bj) } : {}) };
}

// ─── #44 AGENT-SAFETY: throw-on-non-VALID. `isValid(r)` returns a bool an agent can IGNORE (catch the error,
// use the data anyway — the exact laziness the audit named). `verifyOrThrow` puts verification in the CONTROL
// FLOW: a non-VALID verdict THROWS, so the language forces the agent to handle it — it cannot silently continue.
// The thrown error CARRIES the full structured verdict (`.verdict`) for branching. The distinction matters:
// UstInvalid = a real integrity failure (forged/tampered/broken chain); UstIndeterminate = "cannot decide yet"
// (substrate unreachable, unsupported alg) — an agent must treat these DIFFERENTLY (reject vs retry), so they are
// distinct classes, not one. The soft path (read the verdict as data) stays available: just call `verify` directly.
export class UstInvalid extends Error {
  constructor(verdict) { super('UST verification failed: ' + (verdict?.error || 'INVALID') + (verdict?.detail ? ' — ' + verdict.detail : '')); this.name = 'UstInvalid'; this.verdict = verdict; this.code = verdict?.error; }
}
export class UstIndeterminate extends Error {
  constructor(verdict) { super('UST verification indeterminate: ' + (verdict?.reason || 'unknown') + (verdict?.detail ? ' — ' + verdict.detail : '')); this.name = 'UstIndeterminate'; this.verdict = verdict; this.reason = verdict?.reason; }
}
// Assert a verdict is VALID (any tier) or THROW the typed error. Works on the result of verify OR verifyAsync —
// so an async/TOP caller does `assertValid(await verifyAsync(doc, opts))`. Returns the verdict for chaining.
export function assertValid(verdict) {
  if (/^VALID:/.test(verdict?.result || '')) return verdict;
  if (verdict?.result === 'INDETERMINATE') throw new UstIndeterminate(verdict);
  throw new UstInvalid(verdict || { error: 'E-MALFORMED', detail: 'no verdict' });
}
// The SYNC one-call agent entrypoint: verify + assert in the control flow. For anchored/async substrates use
// `assertValid(await verifyAsync(doc, opts))`.
export function verifyOrThrow(doc, opts = {}) { return assertValid(verify(doc, opts)); }

// ─── #69 C — the EXPECTED GRID. `ust_id` IS the time coordinate, so the slots a cadence implies over an
// interval are COMPUTED, not stored: parse `ust:YYYYMMDD.HH[MM[SS]]` ↔ UTC epoch, step by the cadence. The
// grid's precision follows the cadence (a multiple of 3600 → hour, of 60 → minute, else second).
function ustToEpoch(ustId) {
  const m = /^ust:(\d{4})(\d{2})(\d{2})\.(\d{2})(\d{2})?(\d{2})?$/.exec(ustId || '');
  return m ? Date.UTC(+m[1], +m[2] - 1, +m[3], +m[4], +(m[5] || 0), +(m[6] || 0)) / 1000 : null;
}
function epochToUst(epoch, prec) {
  const d = new Date(epoch * 1000), p = (n) => String(n).padStart(2, '0');
  let s = `ust:${d.getUTCFullYear()}${p(d.getUTCMonth() + 1)}${p(d.getUTCDate())}.${p(d.getUTCHours())}`;
  if (prec !== 'hour') s += p(d.getUTCMinutes());
  if (prec === 'second') s += p(d.getUTCSeconds());
  return s;
}
export function ustGrid(from, to, cadenceSec) {
  const e0 = ustToEpoch(from), e1 = ustToEpoch(to);
  if (e0 === null || e1 === null || !(cadenceSec > 0) || e1 < e0) return null;
  const prec = cadenceSec % 3600 === 0 ? 'hour' : cadenceSec % 60 === 0 ? 'minute' : 'second';
  const grid = [];
  for (let e = e0; e <= e1; e += cadenceSec) { grid.push(epochToUst(e, prec)); if (grid.length > 200000) return null; }  // bound (§13 discipline)
  return grid;
}

// §11.3 continuity — resolve the cadence IN FORCE at a slot time from the genesis value + a cadence-log (the
// key-log pattern applied to cadence: a signed, prev-chained sequence of changes). Old data verifies under the
// cadence signed for ITS time, so an operator changing cadence NEVER retroactively invalidates history. Each
// entry is a normal transcript, verified by §14; the log is genesis-rooted and prev-chained. → {cadence}|{error}.
// round-47 P0-01 (structural rework rev69, the CALCULATOR boundary DONE, not claimed) — Theorem R's exact sound form is a pure
// function of canonical BYTE-STRINGS: byte-strings are IMMUTABLE and `JSON.parse` invokes no caller code, so the per-argument
// reductions ARE genuinely independent and order IS irrelevant (unlike an object API, where `admitDeep` fires the [[Get]] face
// and reducing one signed argument can mutate a still-live sibling — the signed-vs-signed cross-argument class). THIS is the
// sound public boundary, the same shape as `checkAuthorityProofBytes` — the calculator eats bytes. `resolveCadence` (below) is
// a CONVENIENCE object adapter over it, not the security boundary.
export function resolveCadenceBytes(genesisBytes, cadenceLogBytes, atTime, keylogBytes) {
  // round-48 P0-01 — admit EACH byte argument through `snapshotBytes` (exact native Uint8Array → immutable copy, no caller
  // getter) BEFORE parsing any of them, so the "independent reductions" claim above is ENFORCED, not assumed (a `Buffer.from`
  // on a raw array-like arg would run its getters and mutate a still-live sibling). `atTime` is a value, not bytes.
  const gS = snapshotBytes(genesisBytes);
  if (gS.error) return { error: 'E-MALFORMED', detail: 'genesis bytes must be a native Uint8Array (' + gS.error + ')' };
  const cS = snapshotBytes(cadenceLogBytes);
  if (cS.error) return { error: 'E-MALFORMED', detail: 'cadence-log bytes must be a native Uint8Array (' + cS.error + ')' };
  let kCopy = null;
  if (keylogBytes !== undefined && keylogBytes !== null) {
    const kS = snapshotBytes(keylogBytes);
    if (kS.error) return { error: 'E-MALFORMED', detail: 'key-log bytes must be a native Uint8Array (' + kS.error + ')' };
    kCopy = kS.bytes;
  }
  let genesis, cadenceLog, keylog;
  try {
    genesis = JSON.parse(decodeUtf8(gS.bytes));
    cadenceLog = JSON.parse(decodeUtf8(cS.bytes));
    keylog = kCopy === null ? undefined : JSON.parse(decodeUtf8(kCopy));
  } catch { return { error: 'E-MALFORMED', detail: 'resolveCadence arguments must be canonical UTF-8 JSON byte-strings' }; }
  if (cadenceLog !== undefined && cadenceLog !== null && !Array.isArray(cadenceLog)) return { error: 'E-MALFORMED', detail: 'cadenceLog must be an array' };
  cadenceLog = cadenceLog ?? [];   // round-43 — ONLY an absent (undefined/null) cadence-log defaults to empty; a present non-array already returned E-MALFORMED above (never the `Array.isArray(X)?X:[]` coalesce that hides a malformed selector)
  // #75 P1-03 — cadence is a canonical positive-integer STRING of seconds ("1.5" / "030" / 1e2 rejected).
  const gCad = genesis?.state?.data?.genesis?.value?.cadence;
  if (gCad !== undefined && parseCadenceInt(gCad) === null) return { error: 'E-MALFORMED', detail: 'genesis cadence not a canonical positive integer of seconds (§11.3)' };
  let cadence = gCad !== undefined ? parseCadenceInt(gCad) : null;                 // the genesis value is authorized by construction (self-signed)
  if (!Array.isArray(cadenceLog) || !cadenceLog.length) return { cadence };
  if (cadenceLog.length > 256) return { error: 'E-BOUNDS', detail: 'cadence-log > 256 (§13)' };
  // #71-followup P0 — a cadence CHANGE is an OPERATOR AUTHORITY parameter, not "any LIGHT doc with the same
  // domain_shard". Resolve the authorized key set ONCE (genesis + key-log) and reject an entry signed OUTSIDE
  // it. Without the key-log only the genesis key can authorize a change (fail-closed, not fail-open).
  const kl = admitOptionalKeylog(keylog);   // round-43 P1-02 — a PRESENT non-array keylog is MALFORMED, never coalesced to [] (that erased a retirement entry and accepted a change signed by a retired key)
  if (kl === null) return { error: 'E-MALFORMED', detail: 'keylog must be a native array of inert key-log entries (round-43 P1-02 — a present non-array selector must not silently become an empty log)' };
  const rk = resolveKeys(genesis, kl === ABSENT_SELECTOR ? [] : kl);
  if (rk.error) return { error: rk.error, detail: 'cadence authority: ' + rk.detail };
  // #75 P0-02c — a cadence entry MUST be signed by a currently-ACTIVE key (not merely ever-seen): a retired or
  // rotated-out or revoked key can no longer move the grid. `active` already excludes all of them (state machine).
  //
  // §F.5e.3 / #107 — and ACTIVE is NECESSARY, not SUFFICIENT. `mutating(c)` names TWO classes, and only `key` was
  // realized: measured 2026-07-29, an operational key moved the grid and was ACCEPTED. Cadence is what makes
  // `complete` mean anything — widen it INSIDE one precision class (30s→90s) and a stream with empty slots reads
  // `complete` with no frame added, every signature still valid, nothing failing anywhere. Widening ACROSS a class
  // (30s→3600s, the example the issue was filed with) does NOT work: the frames fall off the coarser grid and
  // #75 P0-04 grid EQUALITY already returns E-PREV — measured, so the reachable attack is the SMALL step, which is
  // also the one an operator is less likely to notice. That is a quieter power than revoking the root and
  // harder to see, so it is restricted the SAME way, by the SAME conjunct, in the SAME shape as resolveKeys: the
  // signer must be active AND be the genesis root. A RESTRICTION — every log admissible after it was admissible
  // before. The key-log stays load-bearing even though one key may sign: it is what reveals a REVOKED root.
  const gKid = genesis.state.id.key_id;   // resolveKeys already proved the genesis self-signed, so this IS the root
  const atE = ustToEpoch(atTime);
  let prev = contentHash(genesis), lastEff = null;
  for (const [i, e] of cadenceLog.entries()) {
    const ev = verify(e, { context: 'key' });
    if (cannotDecide(ev)) return { error: 'E-UNSUPPORTED', detail: 'cadence entry ' + i + ' not decidable by this build: ' + ev.detail };   // #144
    if (!isValid(ev)) return { error: 'E-KEY', detail: 'cadence entry ' + i + ' invalid: ' + (ev.error || ev.result) };
    if (e.state.id.class !== 'cadence') return { error: 'E-MALFORMED', detail: 'cadence-log entry ' + i + ' not class:cadence' };
    if (e.state.id.domain_shard !== genesis.state.id.domain_shard) return { error: 'E-AUTHORITY', detail: 'cadence entry ' + i + ' domain mismatch' };
    if (e.state.provenance?.prev !== prev) return { error: 'E-PREV', detail: 'cadence entry ' + i + ' not chained' };
    const sKid = keyId(e.sig.pub);
    if (rk.active.get(sKid) !== e.sig.pub) return { error: 'E-KEY', detail: 'cadence entry ' + i + ' NOT signed by a currently-active key (retired/revoked/unauthorized, §12.2)' };
    if (sKid !== gKid) return { error: 'E-KEY', detail: 'cadence entry ' + i + ' cadence mutation requires the GENESIS ROOT (§F.5e.3, §11.3) — an operational key may sign documents, not redefine what "complete" means' };
    const op = e.state.data.cadence_op.value;
    const effE = ustToEpoch(op.effective_from);
    if (effE === null) return { error: 'E-MALFORMED', detail: 'cadence entry ' + i + ' bad effective_from' };
    if (lastEff !== null && effE < lastEff) return { error: 'E-PREV', detail: 'cadence effective_from not monotonic (entry ' + i + ')' };
    if (parseCadenceInt(op.cadence) === null) return { error: 'E-MALFORMED', detail: 'cadence entry ' + i + ' cadence not a canonical positive integer of seconds' };
    if (atE !== null && effE <= atE) cadence = parseCadenceInt(op.cadence);        // latest change in force at atTime wins
    lastEff = effE; prev = contentHash(e);
  }
  return { cadence };
}
// CONVENIENCE object adapter over `resolveCadenceBytes` — serialize each live argument to canonical bytes, then call the SOUND
// bytes-core. This is NOT the hostile-getter security boundary: there is NO fully order-independent multi-object reduction in JS
// (a getter fires on any traversal, so serializing one argument can mutate a still-live sibling). A caller that needs soundness
// against a hostile Proxy passes PRE-SERIALIZED immutable bytes to `resolveCadenceBytes`. The structural-arg-first serialization
// here (mutation-vulnerable cadenceLog captured before the self-verifying genesis) closes the common object-caller case.
export function resolveCadence(genesis, cadenceLog = [], atTime, opts) {
  const _o = admitOpts(opts); if (_o === null) return { error: 'E-MALFORMED', detail: 'opts is not an inert record (round-29 P1-01 totality — a hostile 4th arg cannot throw at this door)' };
  const enc = (s) => new Uint8Array(utf8(s));
  let gEnc, cEnc, kEnc;
  try {
    cEnc = enc(canon(admitDeep(cadenceLog)));            // structural arg first
    gEnc = enc(canon(admitDeep(genesis)));               // self-verifying anchor last (its own mutation fails its signature)
    kEnc = (_o.keylog === undefined || _o.keylog === null) ? undefined : enc(canon(admitDeep(_o.keylog)));
  } catch { return { error: 'E-MALFORMED', detail: 'genesis/cadenceLog/keylog is not an inert record' }; }
  return resolveCadenceBytes(gEnc, cEnc, atTime, kEnc);
}
//
// CLOSED 2026-07-29 by `4bca4e0` — protocol(rev98): cadence mutation is ROOT-ONLY — the second half of
// `mutating(c)` (#107). The guard this paragraph explains landed with it; noted 2026-08-05, appended rather
// than rewritten.

// ─── #76/#77 AUTHORITY CHECKPOINT — the latest-head authority object (distinct from the STREAM `buildCheckpoint`).
//     THREE LAYERS (a signer cannot sign its own signature): unsigned CheckpointBody → preimage
//     canon({purpose:"ust:authority-checkpoint-signature", body}) → signed {body, sig}, with
//     authorityCheckpointId = H("ust:authority-checkpoint", canon({body, sig})). External evidence (anchors, map
//     proofs) is NEVER inside the id. Authority is carried IN-BAND and NON-CIRCULARLY: genesis authorizes C₀'s
//     signer; each Cₙ₋₁ authorizes the signer of Cₙ (its `checkpoint_authority.next_*`, effective at seq n); a
//     checkpoint NEVER authorizes its own signer. The expected signer is resolved from PRIOR state BEFORE the
//     signature is trusted; the carried `current_key_id` is diagnostic and MUST equal that resolved signer.
export function buildAuthorityCheckpoint({ domain_shard, genesis_epoch, sequence, previous_checkpoint = null, previous_epoch_final_checkpoint, active_genesis, current_key_id, next_key_id, next_pub, effective_sequence, keylog }) {
  const ca = { current_key_id,
    ...(next_key_id !== undefined ? { next_key_id } : {}),
    ...(next_pub !== undefined ? { next_pub } : {}),
    ...(effective_sequence !== undefined ? { effective_sequence } : {}) };
  return { version: '1', purpose: 'ust:authority-checkpoint', domain_shard, genesis_epoch, sequence: String(sequence),
    ...(previous_checkpoint !== undefined && previous_checkpoint !== null ? { previous_checkpoint } : {}),   // C₀ is genesis-rooted: no prior
    ...(previous_epoch_final_checkpoint !== undefined ? { previous_epoch_final_checkpoint } : {}),           // epoch-B initial: binds epoch-A final
    active_genesis, checkpoint_authority: ca, keylog };
}
export function sealAuthorityCheckpoint(body, privKeyObj, pubB64url) {
  const sig = toB64url(ed25519Sign(utf8(canon({ purpose: 'ust:authority-checkpoint-signature', body })), privKeyObj));
  return { body, sig: { alg: 'Ed25519', key_id: keyId(pubB64url), pub: pubB64url, sig } };
}
export const authorityCheckpointId = (cp) => H('ust:authority-checkpoint', canon({ body: cp.body, sig: cp.sig }));   // ONLY body+sig — external evidence excluded
const isSeq = (s) => typeof s === 'string' && /^(0|[1-9]\d*)$/.test(s);                                              // canonical decimal, no leading zeroes
function authorityCheckpointSigOk(cp, expectedKeyId, expectedPub) {
  const s = cp?.sig, spub = admitSigner(s, expectedKeyId);   // round-35 — the ONE signer admission: key_id === expectedKeyId === keyId(pub), exact Ed25519 wrapper, Pub32/Sig64
  if (spub === null || spub !== expectedPub) return { ok: false, detail: 'signer is not the authorized checkpoint authority (round-35 admitSigner)' };
  if (!edVerifyStrict(spub, canon({ purpose: 'ust:authority-checkpoint-signature', body: cp.body }), s.sig)) return { ok: false, detail: 'Ed25519 verify failed' };
  return { ok: true };
}
// Verify a chain of authority checkpoints. Root the FIRST element's signer in the genesis-authorized checkpoint key
// (`genesisAuthority = {key_id, pub}`, the reference profile's dedicated key) OR a pinned prior checkpoint
// (`pinnedPrior = {checkpoint_id, authority:{key_id, pub}, sequence}`). No root ⇒ INDETERMINATE(authority_unresolved).
// ─── #76 §1.7 CHECKPOINT RECOVERY — a genesis-authorized N-of-M multisig that re-authorizes the checkpoint authority
//     after key loss WITHOUT bypassing checkpoint validation. Role-separated from data/checkpoint keys; the recovery
//     set is genesis-fixed (immutable within the epoch). A DORMANT emergency mechanism, NOT a normal rotation: it
//     authorizes ONLY the next checkpoint's replacement key, bound to (domain, epoch, last_accepted_checkpoint, seq).
export function checkpointRecoveryClaim({ domain_shard, genesis_epoch, last_accepted_checkpoint, replacement_key_id, replacement_pub, effective_sequence }) {   // round-35 P1-01 — the NORMATIVE recovery tuple; a human `reason` is NOT part of the signed authority claim (portability divergence removed)
  return { purpose: 'ust:checkpoint-authority-recovery', domain_shard, genesis_epoch, last_accepted_checkpoint,
    replacement_authority: { key_id: replacement_key_id, pub: replacement_pub }, effective_sequence: String(effective_sequence) };
}
export function buildRecoveryStatement(fields, privKeyObj, issuerPubB64url) {
  const claim = checkpointRecoveryClaim(fields);
  const sig = toB64url(ed25519Sign(utf8(canon(claim)), privKeyObj));
  return { claim, issuer_id: keyId(issuerPubB64url), sig: { alg: 'Ed25519', key_id: keyId(issuerPubB64url), pub: issuerPubB64url, sig } };
}
export function verifyCheckpointRecovery(statements, config) {
  const c = admitOpts(config); if (c === null) return { recovered: false, detail: 'config must be an inert record (round-23 P1-02 totality)' };   // round-23 P1-02 — total: admit config BEFORE destructure
  const { domain_shard, genesis_epoch, last_accepted_checkpoint, effective_sequence, recoveryKeys = {}, threshold = 2 } = c;
  const S = admitDeep(statements); if (S === ADMIT_REJECT) return { recovered: false, detail: 'recovery statements are not an inert record (round-27 P0-03 — a getter on replacement_authority cannot re-sign the quorum after canonicalization)' };   // round-27 P0-03 — snapshot ONCE; the quorum payload is a frozen copy of the exact canonical claim whose signature was checked
  statements = S;
  if (!Array.isArray(statements) || statements.length === 0) return { recovered: false, detail: 'no recovery statements' };
  // M5 instance — the recovery quorum is the SAME algebra (admit → group → count → adjudicate): voter = the
  // genesis-authorized recovery signer, ValidThreshold bounded by |recoveryKeys| (a closed voter set), >1 winning
  // replacement = CONFLICT never first-wins (P0-05), and a malformed leaf admits nothing instead of throwing
  // (the rc.35 round-2 recovery-DoS: canon(claim) threw through the whole verification).
  const r = quorumAdjudicate(statements, { threshold, maxVoters: Object.keys(recoveryKeys).length, admit: (s) => {
    if (!closedRecoveryWitness(s)) return null;                                         // round-34 P0-04 — CLOSED envelope { claim, issuer_id, sig } + typed sig wrapper (Ed25519, strict Pub32, Sig64) + exact typed recovery claim (purpose, replacement_authority strict Pub32), mirroring the kernel; an open/untyped statement mints nothing
    const { claim, issuer_id, sig } = s;
    if (claim.domain_shard !== domain_shard || claim.genesis_epoch !== genesis_epoch || claim.last_accepted_checkpoint !== last_accepted_checkpoint) return null;   // scope binding (shape/purpose already typed)
    if (claim.effective_sequence !== String(effective_sequence)) return null;           // authorizes ONLY the next checkpoint (effective_sequence already a CanonicalSeq)
    const ra = claim.replacement_authority, cc = canon(claim);                          // ra key_id/pub already typed + strict Pub32 by the closed decode
    const pub = recoveryKeys[issuer_id];
    if (!pub || pub !== sig.pub) return null;                                           // genesis-authorized recovery signer pub (signer identity already bound by closedRecoveryWitness → admitSigner)
    if (!edVerifyStrict(sig.pub, cc, sig.sig)) return null;                             // sig bytes already Sig64-typed by the closed decode
    return { key: cc, voter: issuer_id, payload: ra };
  } });
  if (r.outcome === 'conflict') return { recovered: false, conflict: true, detail: 'recovery equivocation: ' + r.count + ' conflicting replacements each reached threshold' };
  if (r.outcome === 'invalid-threshold') return { recovered: false, detail: 'invalid recovery threshold ' + threshold + ' (must be 1..' + Object.keys(recoveryKeys).length + ')' };
  return r.outcome === 'accepted'
    ? { recovered: true, replacement_authority: r.payload, threshold: String(threshold), signers: r.voters }
    : { recovered: false, detail: 'recovery quorum not met (no claim reached ' + threshold + ' distinct signers)' };
}

// ─── #76 (audit-8) GENESIS-EPOCH TRANSITION — a new genesis epoch must NOT silently reset the authority chain. The
//     A→B transition is a typed statement SIGNED BY EPOCH A's checkpoint authority, binding A's final checkpoint and
//     naming epoch B's initial checkpoint authority + initial sequence. Epoch B's C₀ then binds that final checkpoint.
export function epochTransitionClaim({ domain_shard, from_genesis_epoch, from_final_checkpoint, from_sequence, to_active_genesis, to_genesis_epoch, to_key_id, to_pub, to_initial_sequence = '0' }) {
  // M4.4 — a transition hands authority to a VERIFIED new genesis, not a free epoch label: it binds
  // to_active_genesis, and to_genesis_epoch is CANONICAL to it (derived when omitted; verify re-checks).
  const toEpoch = to_genesis_epoch ?? (isHashStr(to_active_genesis) ? genesisEpoch(to_active_genesis) : undefined);
  return { purpose: 'ust:genesis-epoch-transition', domain_shard, from_genesis_epoch, from_final_checkpoint,
    ...(from_sequence !== undefined ? { from_sequence: String(from_sequence) } : {}),   // M-ERA: bind the epoch-A FINAL sequence (the reference checker requires it; UST-Protocol round-11 P1-01)
    ...(to_active_genesis !== undefined ? { to_active_genesis } : {}),                 // absence is caught at verify (M4.4), kept out of canon
    ...(toEpoch !== undefined ? { to_genesis_epoch: toEpoch } : {}),
    to_checkpoint_authority: { key_id: to_key_id, pub: to_pub }, to_initial_sequence: String(to_initial_sequence) };
}
export function buildEpochTransition(fields, privKeyObj, issuerPubB64url) {
  const claim = epochTransitionClaim(fields);
  const sig = toB64url(ed25519Sign(utf8(canon(claim)), privKeyObj));
  return { claim, issuer_id: keyId(issuerPubB64url), sig: { alg: 'Ed25519', key_id: keyId(issuerPubB64url), pub: issuerPubB64url, sig } };
}
export function verifyEpochTransition(statement, config) {
  const c = admitOpts(config); if (c === null) return { ok: false, detail: 'config must be an inert record (round-24 P1-01 totality)' };   // round-24 P1-01 — total for null/hostile config
  const { domain_shard, from_genesis_epoch, from_final_checkpoint, from_sequence, fromAuthority } = c;
  const St = admitDeep(statement); if (St === ADMIT_REJECT) return { ok: false, detail: 'transition statement is not an inert record (round-27 — the returned to_active_genesis/to_checkpoint_authority must equal the SIGNED bytes, no getter re-read)' };   // round-27 — snapshot: the return re-reads claim.* after canon-verify; a getter could sign one destination and hand back another
  const { claim, sig } = St || {};
  if (!fromAuthority) return { ok: false, detail: 'no from-authority' };
  if (!closedTransitionWitness(St)) return { ok: false, detail: 'transition witness not typed (round-34 P0-04 — CLOSED envelope { claim, sig, issuer_id? } + Ed25519 sig wrapper + strict Pub32 + exact typed transition claim, mirroring the kernel closedTransition; an extra field / alg:RSA / non-canonical pub mints nothing)' };
  if (claim.domain_shard !== domain_shard || claim.from_genesis_epoch !== from_genesis_epoch || claim.from_final_checkpoint !== from_final_checkpoint) return { ok: false, detail: 'transition not bound to this (domain, from-epoch, from-final-checkpoint)' };
  // round-24 P1-02 — the signed `from_sequence` MUST equal epoch A's verified FINAL sequence (the reference checker
  // requires it; the public verifier skipped the coordinate). Require it present + canonical; when the caller supplies the
  // verified prior sequence, require equality — a transition cannot claim it bridges from a sequence epoch A never reached.
  // round-25 P1-01 — ONE CanonicalSeq decoder (isSeq: canonical decimal string, no leading zero, no `String([...])`
  // coercion) at EVERY signed sequence coordinate. The signed from_sequence must be canonical, AND the caller MUST
  // supply the verified prior-chain FINAL sequence as a canonical coordinate: an omitted comparison coordinate can no
  // longer yield ok:true trusting an attacker's signed from_sequence (a full transition-verification API is not partial).
  if (!isSeq(claim.from_sequence)) return { ok: false, detail: 'transition from_sequence missing or non-canonical (round-24 P1-02)' };
  if (!isSeq(from_sequence)) return { ok: false, detail: 'epoch transition requires the verified prior-chain FINAL sequence as a canonical from_sequence coordinate (round-25 P1-01 — no partial verification)' };
  if (from_sequence !== claim.from_sequence) return { ok: false, detail: `transition from_sequence ${claim.from_sequence} ≠ epoch A final sequence ${from_sequence} (round-24 P1-02)` };
  if (!isSeq(claim.to_initial_sequence)) return { ok: false, detail: 'transition to_initial_sequence non-canonical (round-25 P1-01 — a coercible `["0"]` no longer seeds the next epoch)' };
  // M4.4 — the destination is a VERIFIED genesis, never a free epoch label: the transition MUST bind
  // to_active_genesis, and to_genesis_epoch MUST be canonical to it (the M2 hygiene, uniform).
  if (!isHashStr(claim.to_active_genesis) || claim.to_genesis_epoch !== genesisEpoch(claim.to_active_genesis)) return { ok: false, detail: 'transition does not bind a verified destination genesis (to_active_genesis missing or to_genesis_epoch non-canonical, M4.4)' };
  const ta = claim.to_checkpoint_authority;
  if (!admitAuthorityKey(ta)) return { ok: false, detail: 'to_checkpoint_authority not a usable { key_id === keyId(pub) } pair (round-36 admitAuthorityKey; also bound by the closed transition claim schema)' };
  const tpub = admitSigner(sig, fromAuthority.key_id);   // round-35 — key_id === fromAuthority.key_id === keyId(pub), exact Ed25519 wrapper
  if (tpub === null || tpub !== fromAuthority.pub) return { ok: false, detail: 'transition not signed by epoch A checkpoint authority (round-35 admitSigner)' };
  if (!edVerifyStrict(tpub, canon(claim), sig.sig)) return { ok: false, detail: 'Ed25519 verify failed' };
  return { ok: true, to_active_genesis: claim.to_active_genesis, to_genesis_epoch: claim.to_genesis_epoch, to_checkpoint_authority: ta, to_initial_sequence: claim.to_initial_sequence };
}

const CP_BODY_KEYS = new Set(['version', 'purpose', 'domain_shard', 'genesis_epoch', 'sequence', 'previous_checkpoint', 'previous_epoch_final_checkpoint', 'active_genesis', 'checkpoint_authority', 'keylog']);
const CP_CA_KEYS = new Set(['current_key_id', 'next_key_id', 'next_pub', 'effective_sequence']);
const isHashStr = (s) => typeof s === 'string' && /^sha256:[0-9a-f]{64}$/.test(s);
// ─── F.5s THE STREAM FLOOR — the boundary of the OBLIGATION domain, and the one coordinate measured to invert W1.
//     `¬∃ state at t` is TWO claims split at the address where the identity began: below it the operator owes no
//     coverage (a document addressed there is a RETROSPECTIVE claim, an anchor later than its address); at or above
//     it an empty slot is a coverage fact (§11.3). So the direction of error is asymmetric — a floor EARLIER than
//     the truth is conservative, a floor LATER DISOWNS real gaps.
//     Root-ness (`¬∃ transition into my epoch`) is authenticated non-membership and is NOT decidable from what the
//     publisher serves: a genesis carries no predecessor reference, and `verifyEpochTransition` reads only hashes.
//     A publisher WITHHOLDING its earliest transitions therefore moves the derived floor LATER — removal IMPROVES
//     its answer, which is the reverse of monotone erosion (F.5b) and holds nowhere else in this verifier.
//     Hence: a publisher-supplied chain earns a CANDIDATE; the floor is ESTABLISHED only when root-ness enters from
//     outside the publisher's influence — a consumer-held root today, an anchored map when #42 ships.
//     TOTAL by construction: every input is untrusted wire, so a hostile accessor is a structured refusal.
export function deriveStreamFloor(config = {}) {
  const c = admitOpts(config);
  if (c === null) return { established: false, detail: 'config must be an inert record (totality — a hostile accessor is a refusal, never a throw)' };
  const G = admitDeep(c.genesis);
  if (G === ADMIT_REJECT || !G || typeof G !== 'object' || Array.isArray(G)) return { established: false, detail: 'genesis must be an inert record' };
  const Ts = admitDeep(c.transitions ?? []);
  if (Ts === ADMIT_REJECT || !Array.isArray(Ts)) return { established: false, detail: 'transitions must be an inert array' };
  const shard = G.state?.id?.domain_shard;
  if (typeof shard !== 'string' || !shard) return { established: false, detail: 'genesis carries no domain_shard' };
  const hashOf = (d) => { try { return contentHash(d); } catch { return null; } };   // canon can refuse; a refusal is not a throw here
  // Index by the epoch each transition ARRIVES AT — the direction the walk needs. Two transitions arriving at one
  // epoch from different predecessors is a FORKED chain: it determines no root, and picking one would be first-wins.
  const into = new Map();
  for (const t of Ts) {
    const cl = t?.claim;
    if (!cl || cl.purpose !== 'ust:genesis-epoch-transition' || cl.domain_shard !== shard) continue;
    if (typeof cl.to_genesis_epoch !== 'string' || typeof cl.from_genesis_epoch !== 'string') continue;
    if (into.has(cl.to_genesis_epoch) && into.get(cl.to_genesis_epoch) !== cl.from_genesis_epoch)
      return { established: false, detail: 'two transitions arrive at one epoch from DIFFERENT predecessors — the chain forks and determines no root' };
    into.set(cl.to_genesis_epoch, cl.from_genesis_epoch);
  }
  const gh = hashOf(G);
  if (gh === null) return { established: false, detail: 'genesis is not canonicalizable' };
  let epoch = genesisEpoch(gh), traced = 0;
  const seen = new Set([epoch]);
  while (into.has(epoch)) {
    const prev = into.get(epoch);
    if (seen.has(prev)) return { established: false, detail: 'the transition chain cycles — no root is reachable' };
    seen.add(prev); epoch = prev; traced++;
  }
  const base = { established: false, root_epoch: epoch, epochs_traced: traced };
  // A walk of ZERO steps means the supplied genesis IS the chain's earliest — so it is the root DOCUMENT, and
  // demanding a second copy of it would be ceremony. It is still only a CANDIDATE: zero steps is also what a
  // withheld chain looks like, which is precisely F.5s-b.
  if (c.rootGenesis === undefined && traced > 0)
    return { ...base, detail: 'no root genesis supplied — the floor is an ADDRESS and lives only in that document; an epoch is a one-way hash of it' };
  const R = c.rootGenesis === undefined ? G : admitDeep(c.rootGenesis);
  if (R === ADMIT_REJECT || !R || typeof R !== 'object' || Array.isArray(R)) return { ...base, detail: 'rootGenesis must be an inert record' };
  const rh = hashOf(R);
  if (rh === null) return { ...base, detail: 'rootGenesis is not canonicalizable' };
  if (genesisEpoch(rh) !== epoch) return { ...base, detail: 'the supplied rootGenesis is not the earliest epoch of THIS chain (its epoch does not match the walk)' };
  const addr = R.state?.id?.ust_id;
  if (typeof addr !== 'string' || !addr) return { ...base, detail: 'rootGenesis carries no ust_id — no address, no floor' };
  if (!(Array.isArray(c.trust?.streamRoots) && c.trust.streamRoots.includes(rh)))
    return { ...base, candidate_floor: addr, basis: 'publisher-chain',
      detail: 'CANDIDATE only (F.5s-c): the chain came from the publisher, which can withhold its earliest transitions and move the floor LATER — root-ness must enter from a consumer-held root or an anchored map' };
  return { established: true, floor: addr, root_epoch: epoch, epochs_traced: traced, basis: 'consumer-admitted-root' };
}
export function verifyAuthorityCheckpointChain(chain, config) {
  // round-27 P0-01/P0-02 — snapshot the COMPLETE (chain, config) authority graph ONCE, DEEP. rev24 put admitDeep on the
  // evidence/genesis-context entries but NOT here, so the chain verifier held live `cp.body` / raw `genesis` / `recoveries`
  // references and re-read them AFTER signature verification — a getter signed a no-rotation body then minted an attacker
  // rotation (P0-02), and a raw genesis TOCTOU installed an unsigned authority (P0-01). No live caller reference survives.
  // admitDeep PASSES BRANDED HANDLES THROUGH (context / a chain-handle pinnedPrior stay branded); raw fields are deep-copied.
  if (config != null && typeof config !== 'object') return { error: 'E-MALFORMED', detail: 'config must be an inert record (round-24 P1-01 totality)' };
  const cfg = config == null ? {} : admitDeep(config); if (cfg === ADMIT_REJECT) return { error: 'E-MALFORMED', detail: 'config is not an inert record — an accessor/getter in the authority config cannot re-read after verification (round-27)' };
  const C = admitDeep(chain); if (C === ADMIT_REJECT) return { error: 'E-MALFORMED', detail: 'chain is not an inert record — a getter on a checkpoint body cannot sign one body and mint another (round-27 P0-02)' };
  chain = C;
  let { genesis, context, genesisAuthority, pinnedPrior, recoveries, recoveryKeys, recoveryThreshold, epochTransitions, keylogEntries } = cfg;   // let — the body may re-resolve some fields (e.g. recovery)
  for (const [nm, v] of [['genesisAuthority', genesisAuthority], ['recoveryKeys', recoveryKeys], ['pinnedPrior', pinnedPrior]])   // round-44 P1-01 — the authority SELECTORS fail closed: a PRESENT falsy/scalar/array value is malformed, never silently overridden by a genesis-derived root or coalesced to {} (a `recoveryKeys:false` erased the genesis-authorized recovery set)
    if (v != null && (typeof v !== 'object' || Array.isArray(v))) return { result: 'INVALID', error: 'E-MALFORMED', detail: nm + ' must be an inert record (round-44 P1-01 — a present falsy/scalar authority selector is malformed, not absent)' };
  if (!Array.isArray(chain) || chain.length === 0) return { error: 'E-MALFORMED', detail: 'empty checkpoint chain' };
  // M4.2 (C4 bounds-before-work) — the optional full prefix-extension witness is the key-log ENTRY vector itself,
  // capped by the §13 resolution ceiling BEFORE any Merkle work.
  if (keylogEntries !== undefined && (!Array.isArray(keylogEntries) || keylogEntries.length > 256 || keylogEntries.some((e) => !isHashStr(e))))
    return { result: 'INVALID', error: 'E-BOUNDS', detail: 'keylogEntries must be ≤ 256 sha256 entry hashes (§13 key-log ceiling)' };
  // P1-04 — prefer roots RESOLVED from the signed genesis; a raw genesisAuthority is a consumer PIN, reported as such.
  // C1 (M2) — a VerifiedAuthorityContext (the verifiedGenesisContext seam output) is the PREFERRED downstream root:
  // scope + authority + recovery keys all come from ONE verified derivation, never re-read from raw genesis fields.
  let authority_root = 'consumer-pin', ctxGenesis = null;
  // K3 — the context root MUST be a BRANDED GenesisHandle (minted only by verifiedGenesisContext). A caller-shaped
  // {scope_id, checkpoint_authority} object is NOT a handle, so it is ignored → round-3 P0-1 (forged verified-context)
  // is closed at the type level, not by comparing extra fields. A non-handle object passed as `context` is rejected.
  if (context !== undefined) {
    if (!isHandle('genesis', context)) return { result: 'INVALID', error: 'E-AUTHORITY', detail: 'context is not a verified GenesisHandle (K3 — a caller-shaped context cannot root a chain; use verifiedGenesisContext)' };
    // round-26 P0-01/P0-02 (rev25, model M2 L642 "never raw fields" + F.5l "recovery immutable within the epoch") — a
    // branded context is the SOLE authority root: EVERY scope/authority/recovery parameter is a function of contentHash(g)
    // via the ONE derivation. A raw authority-root field ALONGSIDE the context is a mixed configuration that substitutes
    // the root the context fixes — a foreign `pinnedPrior` seized the chain scope/authority (P0-01) and raw recovery was
    // INJECTED when the context carried none (P0-02), both while still reporting authority_root:"verified-context". Reject
    // the whole raw family; recovery comes ONLY from the context. (The raw family stays live WITHOUT a context — consumer-pin.)
    if (pinnedPrior !== undefined || genesis !== undefined || genesisAuthority !== undefined || recoveryKeys !== undefined || recoveryThreshold !== undefined)
      return { result: 'INVALID', error: 'E-AUTHORITY', detail: 'a verified GenesisHandle context is the SOLE authority root — no raw pinnedPrior / genesis / genesisAuthority / recoveryKeys / recoveryThreshold may accompany it (round-26 P0-01/P0-02: never raw fields, M2/F.5l)' };
    genesisAuthority = context.checkpoint_authority; ctxGenesis = context.active_genesis; authority_root = 'verified-context';
    if (context.recoveryKeys) { recoveryKeys = context.recoveryKeys; recoveryThreshold = context.recoveryThreshold; }   // recovery is FIXED by the genesis, never a call argument
  } else if (genesis != null) {   // round-43 P1-02 — a PRESENT genesis selector must be a record; a falsy/scalar `genesis:false` is MALFORMED, never silently skipped to a pinned/fallback root
    if (typeof genesis !== 'object' || Array.isArray(genesis)) return { result: 'INVALID', error: 'E-GENESIS', detail: 'genesis must be an inert record (round-43 P1-02 — a present falsy/scalar genesis is malformed, not absent)' };
    const gr = resolveCheckpointRoots(genesis); if (gr?.genesisAuthority) { genesisAuthority = gr.genesisAuthority; authority_root = 'genesis'; } if (gr?.recoveryKeys && recoveryKeys === undefined) { recoveryKeys = gr.recoveryKeys; recoveryThreshold = recoveryThreshold ?? gr.recoveryThreshold; } }
  if (!genesisAuthority && !pinnedPrior) return { result: 'INDETERMINATE', reason: 'authority_unresolved', detail: 'no genesis-rooted or pinned-prior checkpoint authority to resolve the first signer' };
  // K5 (round-3 P0-2) — a mid-chain cold start roots ONLY in a full PinnedCheckpointState (a SCOPED snapshot), never a
  // bare {id, authority, sequence}. Admissible as a branded CheckpointChainHandle (produced by a prior verification) OR
  // a complete consumer-supplied snapshot; either way it carries scope_id + keylog, so the continuation is scope-bound
  // and append-only holds from the pin. A scope-free pin used to let a continuation jump into a new domain/genesis.
  let prev = null;
  if (pinnedPrior !== undefined) {
    const pp = pinnedPrior, a = pp?.authority_for_next;
    const full = isHandle('chain', pp) || (pp && typeof pp === 'object'
      && isHashStr(pp.scope_id) && isHashStr(pp.checkpoint_id) && isSeq(pp.sequence)
      && admitAuthorityKey(a)   // round-36 — the ONE authority-key admission on a raw pinnedPrior authority (exact { key_id, pub }, strict Pub32, key_id === keyId(pub))
      && isSeq(pp.keylog_size) && isHashStr(pp.keylog_root) && isHashStr(pp.keylog_head));
    if (!full) return { result: 'INVALID', error: 'E-AUTHORITY', detail: 'pinnedPrior must be a branded CheckpointChainHandle or a full PinnedCheckpointState {scope_id, checkpoint_id, sequence, authority_for_next, keylog_size, keylog_root, keylog_head} — a scope-free pin is rejected (round-3 P0-2)' };
    prev = { id: pp.checkpoint_id, authority: a, sequence: pp.sequence, pinScope: pp.scope_id,
      body: { keylog: { length: pp.keylog_size, root: pp.keylog_root, head: pp.keylog_head } } };   // synthetic prior — carries keylog for append-only; no domain/epoch (scope check binds them)
  }
  for (let i = 0; i < chain.length; i++) {
    const cp = chain[i], b = cp?.body;
    if (!b || b.purpose !== 'ust:authority-checkpoint') return { result: 'INVALID', error: 'E-MALFORMED', detail: 'not an authority-checkpoint body' };
    if (!isSeq(b.sequence)) return { result: 'INVALID', error: 'E-MALFORMED', detail: 'sequence is not a canonical decimal string' };
    // P1-01 — enforce the FIXED CheckpointBody grammar BEFORE trusting a (validly-signed) body: version + exact-key
    // allowlists (body / checkpoint_authority / keylog) + keylog field shapes. A correct signature over junk is junk.
    if (b.version !== '1') return { result: 'INVALID', error: 'E-MALFORMED', detail: 'checkpoint version must be "1"' };
    if (Object.keys(b).some((k) => !CP_BODY_KEYS.has(k))) return { result: 'INVALID', error: 'E-MALFORMED', detail: 'unknown checkpoint body field' };
    const ca0 = b.checkpoint_authority;
    if (!ca0 || typeof ca0 !== 'object' || Object.keys(ca0).some((k) => !CP_CA_KEYS.has(k))) return { result: 'INVALID', error: 'E-MALFORMED', detail: 'malformed or unknown checkpoint_authority field' };
    const kl0 = b.keylog;
    if (!kl0 || typeof kl0 !== 'object' || Object.keys(kl0).some((k) => !['root', 'length', 'head'].includes(k)) || !isHashStr(kl0.root) || !isSeq(kl0.length) || !isHashStr(kl0.head)) return { result: 'INVALID', error: 'E-MALFORMED', detail: 'malformed keylog {root,length,head}' };
    if (!closedCheckpointWitness(cp)) return { result: 'INVALID', error: 'E-MALFORMED', detail: 'checkpoint witness is not a closed typed ADT (round-34 P0-02/P0-01 — exactly { body, sig }, typed sig wrapper with strict Pub32, exact typed body, mirroring the kernel closedCheckpoint; an unsigned extra sig field must not shift the checkpoint id, a non-canonical Pub32 must not sign)' };   // BEFORE authorityCheckpointId / sig verify
    // M2 (rc.36 refactor) — genesis_epoch is CANONICAL: it MUST equal H("ust:genesis-epoch", active_genesis). The publisher
    // cannot pick the uniqueness namespace: two rival C₀ with the same active_genesis but different epoch (epoch-split)
    // used to key to DIFFERENT map slots and both attest; now a non-canonical epoch is malformed, so rivals collide in ONE slot.
    if (!isHashStr(b.active_genesis) || b.genesis_epoch !== genesisEpoch(b.active_genesis))
      return { result: 'INVALID', error: 'E-MALFORMED', detail: 'genesis_epoch ≠ canonical H("ust:genesis-epoch", active_genesis) — publisher-chosen namespace rejected (M2, epoch-split)' };
    // 1) resolve the EXPECTED signer from PRIOR state (genesis / pinned / previous checkpoint) — before trusting cp
    let expected;
    if (prev === null) {
      // P0-3 (rc.35 audit) — a genesis/authority-rooted START is C₀: sequence "0", NO previous-checkpoint fields, and
      // (when the genesis doc is known) active_genesis == contentHash(genesis). Else a RETIRED genesis key could present
      // an arbitrary nonzero singleton with no previous_checkpoint and RE-ROOT the chain, bypassing every rotation. A
      // start from a later point needs pinnedPrior (carrying the prior sequence + authority), never the raw root.
      if (b.sequence !== '0' || b.previous_checkpoint !== undefined || b.previous_epoch_final_checkpoint !== undefined)
        return { result: 'INVALID', error: 'E-SEQ', detail: 'a genesis/authority-rooted first checkpoint must be sequence "0" with no previous_checkpoint (re-root rejected)' };
      if (authority_root === 'genesis' && b.active_genesis !== contentHash(genesis))
        return { result: 'INVALID', error: 'E-GENESIS', detail: 'C₀ active_genesis ≠ contentHash(genesis) — checkpoint not bound to its rooting genesis' };
      if (authority_root === 'verified-context' && b.active_genesis !== ctxGenesis)
        return { result: 'INVALID', error: 'E-GENESIS', detail: 'C₀ active_genesis ≠ the verified context active_genesis — checkpoint not bound to its rooting scope (C1/M2)' };
      expected = { key_id: genesisAuthority.key_id, pub: genesisAuthority.pub };       // C₀ rooted in the genesis-authorized key
    } else if (prev.body && prev.body.genesis_epoch !== undefined && b.genesis_epoch !== prev.body.genesis_epoch) {   // a scope-pin has no genesis_epoch — its continuation is a normal step bound by scope, not an epoch transition
      // GENESIS-EPOCH TRANSITION — a new epoch must NOT silently reset: it needs an authenticated A→B transition
      // signed by epoch A's authority (prev.authority), binding A's final checkpoint (prev.id). Same domain.
      if (b.domain_shard !== prev.body.domain_shard) return { result: 'INVALID', error: 'E-MALFORMED', detail: 'domain_shard changes within the chain' };
      const et = epochTransitions && epochTransitions[b.genesis_epoch] ? verifyEpochTransition(epochTransitions[b.genesis_epoch], { domain_shard: b.domain_shard, from_genesis_epoch: prev.body.genesis_epoch, from_final_checkpoint: prev.id, from_sequence: prev.body.sequence, fromAuthority: prev.authority }) : { ok: false };   // round-24 P1-02 — pass epoch A's verified FINAL sequence so the transition's signed from_sequence is checked
      if (!et.ok || et.to_genesis_epoch !== b.genesis_epoch) return { result: 'INVALID', error: 'E-MALFORMED', detail: 'genesis_epoch changes without an authenticated epoch transition (no silent reset)' };
      // M4.4 — the epoch-initial checkpoint must LIVE IN the genesis the transition bound: a transition to genesis B
      // cannot seed a chain for genesis B'. With M2 canonical epochs on BOTH sides this equality is derivable
      // (epoch match ⇒ genesis match); the explicit check is the hash-collision belt, kept for defense-in-depth.
      if (b.active_genesis !== et.to_active_genesis) return { result: 'INVALID', error: 'E-GENESIS', detail: 'epoch-initial active_genesis ≠ the transition to_active_genesis (M4.4 — the destination genesis is bound, not a label)' };
      if (b.previous_epoch_final_checkpoint !== prev.id) return { result: 'INVALID', error: 'E-PREV', detail: 'epoch-initial checkpoint does not bind the prior-epoch final checkpoint' };
      if (b.sequence !== et.to_initial_sequence) return { result: 'INVALID', error: 'E-SEQ', detail: 'epoch-initial sequence ≠ the transition to_initial_sequence' };   // round-25 P1-01: both canonical (isSeq), no String() coercion
      expected = { key_id: et.to_checkpoint_authority.key_id, pub: et.to_checkpoint_authority.pub };
    } else {
      const pca = prev.body ? prev.body.checkpoint_authority : null;                    // the authority Cₙ₋₁ committed for THIS sequence, else unchanged
      expected = (pca && pca.next_key_id !== undefined && pca.effective_sequence === b.sequence) ? { key_id: pca.next_key_id, pub: pca.next_pub } : prev.authority;
      if (b.previous_checkpoint !== prev.id) return { result: 'INVALID', error: 'E-PREV', detail: 'previous_checkpoint ≠ id of the prior accepted checkpoint' };
      if (b.sequence !== String(BigInt(prev.sequence) + 1n)) return { result: 'INVALID', error: 'E-SEQ', detail: 'sequence is not prev+1' };
      // K5 — a scope-pinned continuation MUST live in the pin's scope (round-3 P0-2): scope(g) = H(tag, active_genesis)
      // binds the whole genesis, so this is stronger than a domain match and covers the pin's missing domain/epoch.
      if (prev.pinScope !== undefined && authorityScopeId(b.active_genesis) !== prev.pinScope)
        return { result: 'INVALID', error: 'E-GENESIS', detail: 'pinned-continuation active_genesis is not in the pinned scope (round-3 P0-2 — a scope-free jump is rejected)' };
      if (prev.body?.domain_shard !== undefined && b.domain_shard !== prev.body.domain_shard) return { result: 'INVALID', error: 'E-MALFORMED', detail: 'domain_shard changes within the chain' };
      // M4.2 ChainConsistent — the key log is APPEND-ONLY ACROSS same-epoch checkpoints (closes keylog-rewind). Length
      // monotone + equal-length-identical are UNCONDITIONAL; a GROWTH edge REQUIRES the prefix-extension witness
      // (keylogEntries) — without it append-only across the growth is unproven (round-3 P0-3), so the chain is
      // INDETERMINATE, never VALID. Length alone does not prove [A]→[X,Y] is an append.
      if (prev.body) {
        const Lp = BigInt(prev.body.keylog.length), Ln = BigInt(b.keylog.length);
        if (Ln < Lp) return { result: 'INVALID', error: 'E-COMMIT', detail: 'keylog length ' + Ln + ' < prior checkpoint ' + Lp + ' — a signed key-log rewind (append-only violated, M4.2)' };
        if (Ln === Lp && (b.keylog.root !== prev.body.keylog.root || b.keylog.head !== prev.body.keylog.head))
          return { result: 'INVALID', error: 'E-COMMIT', detail: 'equal-length keylog with a different root/head — a same-length history rewrite (M4.2)' };
        if (Ln > Lp && keylogEntries === undefined)
          return { result: 'INDETERMINATE', reason: 'chain_consistency_unproven', detail: 'a growth edge (keylog ' + Lp + '→' + Ln + ') requires a prefix-extension witness (keylogEntries); append-only across the growth cannot be proven from length alone (round-3 P0-3)' };
      }
    }
    // M4.2 prefix-extension witness — when the consumer supplies the key-log ENTRY vector (it already holds it for
    // resolveKeys), EVERY checkpoint's committed keylog must be the commitment over a PREFIX of that ONE vector; all
    // prefixes of one vector are mutually consistent, so this proves ChainConsistent for the whole chain.
    if (keylogEntries !== undefined) {
      const L = Number(BigInt(b.keylog.length));
      if (L > keylogEntries.length) return { result: 'INVALID', error: 'E-COMMIT', detail: 'checkpoint commits a keylog of length ' + L + ' but only ' + keylogEntries.length + ' entries were supplied (M4.2)' };
      const kc = buildKeylogCommitment(keylogEntries.slice(0, L));
      if (kc.root !== b.keylog.root || kc.head !== b.keylog.head)
        return { result: 'INVALID', error: 'E-COMMIT', detail: 'checkpoint keylog is not the commitment over a prefix of the supplied key-log entries (M4.2 prefix-extension)' };
    }
    if (!expected || !expected.key_id || !expected.pub) return { result: 'INDETERMINATE', reason: 'authority_unresolved', detail: 'cannot resolve the expected signer for sequence ' + b.sequence };
    // 2) the signer must be the RESOLVED authority — OR, after key loss, a genesis-recovery-threshold REPLACEMENT for
    //    this exact sequence (bound to the prior checkpoint). Recovery re-authorizes the signer; it does NOT bypass any
    //    later checkpoint check. Both candidates are resolved from PRIOR state before cp's signature is trusted.
    const candidates = [expected];
    let recoveredWith = null;
    if (recoveries && prev && recoveries[b.sequence]) {
      const rec = verifyCheckpointRecovery(recoveries[b.sequence], { domain_shard: b.domain_shard, genesis_epoch: b.genesis_epoch, last_accepted_checkpoint: prev.id, effective_sequence: b.sequence, recoveryKeys: recoveryKeys ?? {}, threshold: recoveryThreshold });
      if (rec.recovered) { recoveredWith = rec.replacement_authority; candidates.push(recoveredWith); }
    }
    const matched = candidates.find((c) => c && c.key_id && c.pub && authorityCheckpointSigOk(cp, c.key_id, c.pub).ok);
    if (!matched) return { result: 'INVALID', error: 'E-AUTHORITY', detail: 'checkpoint not signed by the authorized' + (recoveredWith ? ' (or recovery-replacement)' : '') + ' checkpoint authority' };
    // 3) the carried current_key_id is diagnostic — it must EQUAL the matched signer, never resolve it
    if (b.checkpoint_authority?.current_key_id !== matched.key_id) return { result: 'INVALID', error: 'E-AUTHORITY', detail: 'carried current_key_id ≠ the authorized signer' };
    // 4) rotation exactness — all-or-none; keyId(next_pub)==next_key_id; effective_sequence == seq+1 (no arbitrary future activation)
    const ca = b.checkpoint_authority || {};
    const rot = [ca.next_key_id, ca.next_pub, ca.effective_sequence].filter((x) => x !== undefined).length;
    if (rot !== 0 && rot !== 3) return { result: 'INVALID', error: 'E-MALFORMED', detail: 'checkpoint_authority rotation fields must be all-present or all-absent' };
    if (rot === 3) {
      if (!admitAuthorityKey({ key_id: ca.next_key_id, pub: ca.next_pub })) return { result: 'INVALID', error: 'E-KEY', detail: 'rotation next authority not a usable { key_id === keyId(pub) } pair' };   // round-36 — the ONE authority-key admission
      if (ca.effective_sequence !== String(BigInt(b.sequence) + 1n)) return { result: 'INVALID', error: 'E-SEQ', detail: 'effective_sequence ≠ sequence+1' };
    }
    prev = { id: authorityCheckpointId(cp), authority: matched, sequence: b.sequence, body: b };
  }
  const lb = chain[chain.length - 1].body, lca = lb.checkpoint_authority || {};
  const activeAuthority = (lca.next_key_id !== undefined) ? { key_id: lca.next_key_id, pub: lca.next_pub, effective_sequence: lca.effective_sequence } : prev.authority;
  // K3 — a VALID chain also mints a branded CheckpointChainHandle carrying the full PinnedCheckpointState (§5 of the
  // calculus): the ONLY admissible mid-chain cold-start root besides a genesis (K5 requires the brand, closing the
  // scope-free-pin round-3 P0-2). `pin` is a scoped snapshot, never a bare key.
  const pin = mintHandle('chain', { scope_id: authorityScopeId(lb.active_genesis), checkpoint_id: prev.id, sequence: lb.sequence,
    authority_for_next: activeAuthority, keylog_size: lb.keylog?.length, keylog_root: lb.keylog?.root, keylog_head: lb.keylog?.head });
  return { result: 'VALID', head: prev.id, length: String(chain.length), sequence: lb.sequence, active_genesis: lb.active_genesis, keylog: lb.keylog, activeAuthority, authority_root, pin };
}

// ─── #76 Phase C — CHECKPOINT UNIQUENESS (independent anti-equivocation). `attested` needs a `¬∃ rival at
//     (domain, genesis_epoch, sequence)` proof the PUBLISHER does not control: an `accepted-witness-quorum` (≥
//     threshold DISTINCT consumer-resolved trust domains signing the BYTE-IDENTICAL typed uniqueness claim) — or the
//     `authenticated-map-uniqueness` map path (#42). Independence is CONSUMER-owned (issuer→domain), never
//     self-declared; a bare-observation co-sign is corroboration, not uniqueness (wrong purpose ⇒ not admitted).
export function checkpointUniquenessClaim({ domain_shard, genesis_epoch, sequence, checkpoint }) {   // round-34 P0-03 — EXACTLY the kernel VOTE_CLAIM: no as_of (a signer can't self-declare time) / observed_map_root; builder == public verifier == kernel
  return { purpose: 'ust:checkpoint-uniqueness-attestation', domain_shard, genesis_epoch, sequence: String(sequence), checkpoint };
}
export function buildUniquenessAttestation(fields, privKeyObj, issuerPubB64url) {
  const claim = checkpointUniquenessClaim(fields);
  const sig = toB64url(ed25519Sign(utf8(canon(claim)), privKeyObj));
  return { claim, issuer_id: keyId(issuerPubB64url), sig: { alg: 'Ed25519', key_id: keyId(issuerPubB64url), pub: issuerPubB64url, sig } };
}
// ─── M5 (UST-6vj) — ONE QUORUM ALGEBRA: admit (authenticate FIRST, total) → group by canon(claim) AFTER admission →
//     count DISTINCT consumer-resolved voters per group → adjudicate. Uniqueness attestations and recovery statements
//     are INSTANCES of this core, so the guarantees hold once for both: an unauthenticated element can never poison
//     the group reference (the rc.35 round-2 quorum-poison: `ref` was locked to the FIRST binding claim BEFORE its
//     signature was checked, so a garbage-signed variant suppressed the honest quorum); the winner is independent of
//     iteration order; >1 winning group is CONFLICT/equivocation, never first-wins; malformed input admits nothing
//     and throws nothing (total). ValidThreshold: integer, 1 ≤ t (≤ |voters| where the voter set is closed).
function quorumAdjudicate(items, { threshold, maxVoters, admit }) {
  if (!Number.isInteger(threshold) || threshold < 1 || (maxVoters !== undefined && threshold > maxVoters))
    return { outcome: 'invalid-threshold', detail: 'threshold must be an integer in 1..' + (maxVoters ?? '∞') + ' (got ' + threshold + ')' };
  const groups = new Map();                                                             // canon(claim) → { voters:Set, tags:Set, payload }
  for (const it of Array.isArray(items) ? items : []) {
    let a; try { a = admit(it); } catch { a = null; }                                   // total: a malformed element admits nothing, never throws
    if (!a) continue;
    let g = groups.get(a.key); if (!g) { g = { voters: new Set(), tags: new Set(), payload: a.payload }; groups.set(a.key, g); }
    g.voters.add(a.voter); if (a.tag !== undefined) g.tags.add(a.tag);
  }
  const winners = [...groups.values()].filter((g) => g.voters.size >= threshold);
  if (winners.length === 0) return { outcome: 'quorum_not_met', detail: 'no claim reached ' + threshold + ' distinct voters (groups: ' + groups.size + ')' };
  if (winners.length > 1) return { outcome: 'conflict', detail: 'equivocation: ' + winners.length + ' conflicting claims each reached threshold', count: winners.length };
  const w = winners[0];
  return { outcome: 'accepted', voters: [...w.voters].sort(), tags: [...w.tags].sort(), payload: w.payload };   // round-23 P1-01 — canonical order: the M5 result is set-determined, not iteration-order (uniqueness + recovery both inherit this)
}
export function verifyCheckpointUniqueness(attestations, config) {
  const c = admitOpts(config); if (c === null) return { attested: false, detail: 'config must be an inert record (round-23 P1-02 totality)' };   // round-23 P1-02 — total: admit config BEFORE destructure (a null/hostile config is a structured result, never a host throw)
  { const A = admitDeep(attestations); if (A === ADMIT_REJECT) return { attested: false, detail: 'attestations are not an inert record (round-27 — quorum reads each claim once)' }; attestations = A; }   // round-27 — snapshot the signed inputs, uniform boundary
  const { domain_shard, genesis_epoch, sequence, checkpoint, trustRoots = {}, domains = {}, threshold = 2 } = c;
  if (!Array.isArray(attestations) || attestations.length === 0) return { attested: false, detail: 'no attestations' };
  // M5 instance — voter = the CONSUMER-resolved trust domain (many issuers in one domain count once); admission =
  // typed purpose + no self-declared independence + binding + consumer-accepted issuer + strict signature, all
  // BEFORE any grouping. P0-4: threshold ≥ 1 (0 distinct ≥ 0 earned attested from an empty set).
  const r = quorumAdjudicate(attestations, { threshold, admit: (a) => {
    if (!closedVoteWitness(a)) return null;                                             // round-34 P0-03 — CLOSED envelope { claim, issuer_id?, sig } + Ed25519 sig wrapper (strict Pub32, Sig64) + exact VOTE_CLAIM (purpose + no as_of / trust_domain / issuer_id in the claim, no extra field), mirroring the kernel; the official builder emits exactly this claim
    const { claim, issuer_id, sig } = a;
    if (claim.domain_shard !== domain_shard || claim.genesis_epoch !== genesis_epoch || claim.sequence !== String(sequence) || claim.checkpoint !== checkpoint) return null;   // scope binding (sequence already a CanonicalSeq by the closed decode)
    const cc = canon(claim);                                                            // may throw on a malformed leaf → the core skips it (total)
    const root = trustRoots[issuer_id]; const pub = typeof root === 'string' ? root : root?.pub;
    if (!pub || pub !== sig.pub) return null;                                           // consumer-accepted issuer pub (signer identity issuer_id===sig.key_id===keyId(pub) already bound by closedVoteWitness → admitSigner)
    if (!edVerifyStrict(sig.pub, cc, sig.sig)) return null;                             // sig bytes already Sig64-typed by the closed decode
    const dom = domains[issuer_id]; if (typeof dom !== 'string' || !dom.length || hasLoneSurrogate(dom)) return null;   // round-23 P0-01 + round-24 P0-03 — a NON-EMPTY Unicode-SCALAR NFC string only (object/array/number/null, or a lone UTF-16 surrogate outside §6, is NOT an admitted domain and would fake an independent quorum)
    return { key: cc, voter: dom.normalize('NFC'), tag: issuer_id };
  } });
  if (r.outcome === 'conflict') return { attested: false, conflict: true, detail: r.detail };   // two rival uniqueness claims each with quorum = equivocation, never first-wins
  return r.outcome === 'accepted'
    ? { attested: true, basis: 'accepted-witness-quorum', threshold: String(threshold), accepted_witnesses: r.tags, trust_domains: r.voters }
    : { attested: false, detail: r.outcome === 'invalid-threshold' ? r.detail : 'quorum not met: no byte-identical claim reached ' + threshold + ' distinct trust domains' };
}

// ─── #76/#42 AUTHENTICATED-MAP UNIQUENESS — the INDEPENDENT (non-publisher) non-membership coordinate, via a sparse
//     Merkle tree keyed by H(typed-key): the key's position is a deterministic function of the key, so an inclusion
//     proof for a key returning a value IS the non-membership proof for every rival value at that key (prefix/position
//     uniqueness). Two TYPED, domain-separated key/value spaces (NEVER a generic flag): checkpoint-map (attested
//     freshness) and name-map (authoritative identity). The consumer trusts the (anchored, independent) map root.
const SMT_DEPTH = 256;
const smtDefaults = (() => { const d = new Array(SMT_DEPTH + 1); d[SMT_DEPTH] = H('ust:smt-empty', ''); for (let i = SMT_DEPTH - 1; i >= 0; i--) d[i] = H('ust:smt-node', d[i + 1] + '|' + d[i + 1]); return d; })();
const smtHex = (kh) => kh.replace(/^sha256:/, '');
const smtBit = (hex, i) => (parseInt(hex[i >> 2], 16) >> (3 - (i & 3))) & 1;                 // MSB-first bit i of the 256-bit key hash
const smtLeaf = (kh, vh) => H('ust:smt-leaf', kh + '|' + vh);
function smtRoot(depth, entries) {                                                          // entries: [{hex, kh, vh}]
  if (entries.length === 0) return smtDefaults[depth];
  if (depth === SMT_DEPTH) return smtLeaf(entries[0].kh, entries[0].vh);
  const L = entries.filter((e) => smtBit(e.hex, depth) === 0), R = entries.filter((e) => smtBit(e.hex, depth) === 1);
  return H('ust:smt-node', smtRoot(depth + 1, L) + '|' + smtRoot(depth + 1, R));
}
// Build a verifiable map from typed leaves; returns the root and a prover (inclusion co-path, top→bottom).
export function buildVerifiableMap(leaves) {
  const seen = new Set();                                                             // P1-05: an authenticated dictionary is ONE value per key — reject duplicates (root would be input-order dependent otherwise)
  for (const l of leaves) { if (seen.has(l.key)) throw Object.assign(new Error('E-MALFORMED: duplicate typed key in verifiable map (one value per key)'), { code: 'E-MALFORMED' }); seen.add(l.key); }
  const entries = leaves.map((l) => ({ hex: smtHex(l.key), kh: l.key, vh: l.value }));
  const root = smtRoot(0, entries);
  const prove = (key) => { const hex = smtHex(key); const sib = []; let ents = entries;
    for (let d = 0; d < SMT_DEPTH; d++) { const goR = smtBit(hex, d) === 1;
      const L = ents.filter((e) => smtBit(e.hex, d) === 0), R = ents.filter((e) => smtBit(e.hex, d) === 1);
      sib.push(smtRoot(d + 1, goR ? L : R)); ents = goR ? R : L; }
    return { siblings: sib }; };
  return { root, prove };
}
// verify inclusion (value present) or non-membership (value=null) of key under root, using the co-path
function smtVerify(root, key, value, proof) {
  const sibs = admitHashPath(proof?.siblings, SMT_DEPTH); if (sibs === null) return false;   // round-37 P0-01 — every SMT co-path element is a canonical sha256 hash (no `+` coercion grammar, no host throw on a null-proto object)
  const hex = smtHex(key);
  let node = value === null ? smtDefaults[SMT_DEPTH] : smtLeaf(key, value);
  for (let d = SMT_DEPTH - 1; d >= 0; d--) node = smtBit(hex, d) === 0 ? H('ust:smt-node', node + '|' + sibs[d]) : H('ust:smt-node', sibs[d] + '|' + node);
  return node === root;
}
// TYPED, domain-separated key/value spaces — one map may serve both predicates with NO collision (a checkpoint proof
// is not a name proof). Exported so operators/verifiers build leaves without recomputing the hashing.
export const checkpointMapLeaf = ({ domain_shard, genesis_epoch, sequence, checkpoint }) => ({ key: H('ust:checkpoint-map-key', canon({ domain_shard, genesis_epoch, sequence: String(sequence) })), value: H('ust:checkpoint-map-value', canon({ checkpoint })) });
export const nameMapLeaf = ({ domain_shard, active_genesis }) => ({ key: H('ust:name-map-key', canon({ domain_shard })), value: H('ust:name-map-value', canon({ active_genesis })) });
// TWO distinct TYPED predicates over the SAME map infra — never a generic `verifyMapInclusion(flag)`:
export function verifyCheckpointMapUniqueness(proof, config) {
  const c = admitOpts(config); if (c === null) return { attested: false, detail: 'config must be an inert record (round-24 P1-01 totality)' };   // round-24 P1-01 — total for null/hostile config
  { const Pf = admitDeep(proof); if (Pf === ADMIT_REJECT) return { attested: false, detail: 'proof is not an inert record (round-27)' }; proof = Pf; }   // round-27 — snapshot
  const { domain_shard, genesis_epoch, sequence, checkpoint, mapRoot } = c;
  if (typeof domain_shard !== 'string' || typeof genesis_epoch !== 'string' || typeof checkpoint !== 'string' || typeof mapRoot !== 'string' || !isSeq(sequence)) return { attested: false, detail: 'missing/invalid uniqueness fields (round-24 P1-01 + round-25 P1-01: sequence must be a CanonicalSeq) — canon over undefined/coercible would throw or ambiguate the leaf' };   // guard the leaf inputs so a null/empty/non-canonical config returns structured, never a thrown E-CANON or a coerced `["0"]` leaf
  const { key, value } = checkpointMapLeaf({ domain_shard, genesis_epoch, sequence, checkpoint });
  if (smtVerify(mapRoot, key, value, proof)) return { attested: true, basis: 'authenticated-map-uniqueness', map_root: mapRoot };
  if (smtVerify(mapRoot, key, null, proof)) return { attested: false, absent: true, detail: 'no checkpoint bound at (domain, genesis_epoch, sequence) under mapRoot (proven non-membership)' };
  return { attested: false, detail: 'checkpoint not the unique value at (domain, genesis_epoch, sequence) under mapRoot (a rival value is bound)' };
}
export function verifyActiveGenesisUniqueness(proof, config) {
  const c = admitOpts(config); if (c === null) return { authoritative: false, detail: 'config must be an inert record (round-24 P1-01 totality)' };   // round-24 P1-01 — total for null/hostile config
  { const Pf = admitDeep(proof); if (Pf === ADMIT_REJECT) return { authoritative: false, detail: 'proof is not an inert record (round-27)' }; proof = Pf; }   // round-27 — snapshot
  const { domain_shard, active_genesis, mapRoot } = c;
  if (typeof domain_shard !== 'string' || typeof active_genesis !== 'string' || typeof mapRoot !== 'string') return { authoritative: false, detail: 'missing/invalid uniqueness fields (round-24 P1-01) — canon over undefined would throw' };   // round-24 P1-01 — guard the leaf inputs (null/empty config → structured, never a thrown E-CANON)
  const { key, value } = nameMapLeaf({ domain_shard, active_genesis });
  if (smtVerify(mapRoot, key, value, proof)) return { authoritative: true, basis: 'authenticated-map-uniqueness', map_root: mapRoot };
  if (smtVerify(mapRoot, key, null, proof)) return { authoritative: false, absent: true, detail: 'domain has no active_genesis binding under mapRoot (proven non-membership)' };
  return { authoritative: false, detail: 'active_genesis not the unique value for domain under mapRoot (a rival value is bound)' };
}

// ─── #77 STRICT KEY-LOG TERMINALITY — head is the LAST entry of a length-L log: inclusion of the entry at position
//     L-1 AND non-membership at position L (no successor). A positioned SMT (F.5k) keyed by index — strictly stronger
//     than the earlier `head ∈ root` membership (which a hidden successor could satisfy). keylog.root = this SMT root.
// UST-0ol Phase 4 (P0-02) — a positioned SMT keyed by HASHED index cannot prove "nothing follows the head": absence
// at index L says nothing about L+1, L+2 (a hidden entry at a non-adjacent index passed as terminal). Replaced by a
// SIZE-BOUND ordered VECTOR COMMITMENT: root = H("ust:keylog-commit", {length, merkle_root}) over an ordered Merkle
// of EXACTLY L leaves (padded to a power of two with a domain-separated empty leaf). Terminality = head is the leaf
// at index L-1 AND every RIGHT sibling on its path is the empty-subtree default ⇒ NOTHING exists beyond the head.
const KL_EMPTY = H('ust:keylog-empty', '');
export const keylogLeaf = (entryHash) => H('ust:keylog-leaf', canon({ h: entryHash }));
const klNode = (l, r) => H('ust:keylog-node', l + '|' + r);
const klEmptyDefault = (d) => { let e = KL_EMPTY; for (let i = 0; i < d; i++) e = klNode(e, e); return e; };
export function buildKeylogCommitment(entryHashes) {
  const L = entryHashes.length;
  let width = 1; while (width < L) width <<= 1;
  const layers = [entryHashes.map(keylogLeaf)]; while (layers[0].length < width) layers[0].push(KL_EMPTY);
  while (layers[layers.length - 1].length > 1) {
    const cur = layers[layers.length - 1], nxt = [];
    for (let j = 0; j < cur.length; j += 2) nxt.push(klNode(cur[j], cur[j + 1]));
    layers.push(nxt);
  }
  const merkle_root = layers[layers.length - 1][0];
  const prove = (index) => { const sib = []; for (let d = 0, i = index; d < layers.length - 1; d++, i >>= 1) sib.push(layers[d][i ^ 1]); return { index: String(index), siblings: sib }; };
  return { root: H('ust:keylog-commit', canon({ length: String(L), merkle_root })), length: String(L), head: entryHashes[L - 1], merkle_root, headProof: prove(L - 1), prove };
}
export function verifyKeylogTerminality(head_, proof = {}) {
  const h = admitOpts(head_); if (h === null) return { terminal: false, detail: 'malformed key-log head record (round-24 P1-01 totality)' };   // round-24 P1-01 — the FIRST arg is destructured; total for null/hostile
  const { root, length, head } = h;
  if (!isSeq(length)) return { terminal: false, detail: 'length is not a canonical decimal string (round-26 B — a coercible `["1"]`/non-canonical length no longer decodes via BigInt())' };   // round-26 B — CanonicalSeq before BigInt: BigInt(["1"]) === 1n would coerce an array
  const L = BigInt(length);
  if (L < 1n) return { terminal: false, detail: 'empty key-log' };
  const p0 = admitDeep(proof); const p = (p0 !== ADMIT_REJECT && p0 && typeof p0 === 'object') ? p0 : {};   // round-25 P1-02 null-total + round-27 snapshot: a getter on the Merkle proof (index/siblings) is read once, never re-read after the depth/root check
  const hp = p.headProof || p;
  if (!hp || !Array.isArray(hp.siblings) || !isSeq(hp.index) || BigInt(hp.index) !== L - 1n) return { terminal: false, detail: 'head proof missing or index ≠ length-1 (round-26 B — the Merkle index is a CanonicalSeq, no String([...]) coercion)' };
  // P0-5 (rc.35 audit) — the proof depth MUST be EXACTLY ceil(log2(width)), width = next-pow2(L). An UNDER-DEPTH proof
  // (fewer siblings than the tree has levels) recomputes the root over a SMALLER tree; with an attacker-chosen `root`
  // it FORGES terminality for a key-log that actually has successors — re-opening the P0-02 false-terminality class.
  let width = 1n, depth = 0n; while (width < L) { width <<= 1n; depth++; }
  const sibs = admitHashPath(hp.siblings, Number(depth)); if (sibs === null) return { terminal: false, detail: 'proof depth/co-path invalid: siblings must be a length-' + depth + ' co-path of canonical sha256 hashes (round-37 P0-01 — under/over-depth or a non-hash element earns nothing; no coercion grammar)' };
  let node = keylogLeaf(head), i = L - 1n;
  for (let d = 0; d < sibs.length; d++) {
    const sib = sibs[d], weAreLeft = (i & 1n) === 0n;                                   // LEFT child ⇒ sibling is to the RIGHT (higher indices) ⇒ MUST be an empty subtree
    if (weAreLeft && sib !== klEmptyDefault(d)) return { terminal: false, detail: 'a later key-log entry exists beyond the head (right subtree at level ' + d + ' is not empty)' };
    node = weAreLeft ? klNode(node, sib) : klNode(sib, node);                           // combine using the INDEX-derived side, not a proof field
    i >>= 1n;
  }
  if (i !== 0n) return { terminal: false, detail: 'index not fully consumed by the proof (head index outside the tree)' };
  return H('ust:keylog-commit', canon({ length: String(L), merkle_root: node })) === root ? { terminal: true } : { terminal: false, detail: 'commitment root mismatch (length/merkle not bound in root)' };
}

// ─── #76 Phase B — publisher-checkpoint CORROBORATED freshness. Compose the AUTHORIZED chain (F.5h) with the key-log
//     head's membership in the committed root + a VERIFIED external commitment ordered AFTER the target document
//     (F.5g `compareEvidenceOrder`, never a timestamp compare). This CLOSES the P0-05 stale-prefix overclaim by
//     earning `corroborated`, NEVER `attested`: a single publisher cannot prove split-view absence — independent
//     anti-equivocation is Phase C/#42. (Strict last-index terminality is the #77 refinement; here `head ∈ root`.)
// STABILITY (rc.37 UST-znh K1 — the ship-gate freeze). LIGHT/HIGH are STABLE. This checkpoint-freshness subsystem is
// EXPERIMENTAL until the kernel gates (one closed API + mandatory append-only consistency proof + scope-bound pinning
// + shared node/web core + boundary fuzzing + independent clean-run/audit). Its top rung `attested` fell in all three
// audit rounds via its SUPPORTING layer, so the STABLE verifier does not EMIT it: attested requires the caller to
// consciously opt into the experimental extension (`opts.allowExperimentalAttested === true`); without it the verdict
// is capped at `corroborated` and carries `attested_withheld:'experimental-gate'`. Nothing else changes — this is a
// contract-honesty gate, not a logic change; the closed kernel (UST-znh) replaces this whole function.
export const STABILITY = Object.freeze({ light: 'stable', high: 'stable', corroborated: 'experimental-usable', attested: 'experimental-extension' });
export function deriveCheckpointFreshness(chain, config) {
  const c = admitOpts(config); if (c === null) return { result: 'INVALID', detail: 'config must be an inert record (round-24 P1-01 totality)' };   // round-24 P1-01 (self-audit) — the parallel freshness surface, total for null/hostile config
  // rev38 R3 (round-31 P0-03) — admit the CHAIN ONCE here: verifyAuthorityCheckpointChain admits its OWN snapshot, but this
  // assembler then RE-READ the raw `chain[last].body` for scope/sequence/active-genesis — a two-face Proxy served the signed
  // chain to the verifier and an unsigned body (sequence "999", fake active genesis) to the assembly. Verify + read the SAME frozen chain.
  { const Ch = admitDeep(chain); if (Ch === ADMIT_REJECT) return { result: 'INVALID', detail: 'checkpoint chain is not an inert record (round-31 R3 — the chain is admitted once and read only from the snapshot)' }; chain = Ch; }
  const { genesis, context, genesisAuthority, pinnedPrior, keylogEntries, target, commitment, terminality, uniqueness, trust, allowExperimentalAttested = false } = c;
  const aea = admitBool(allowExperimentalAttested, false);   // round-43 P1-02 — strict: a truthy non-boolean (the string "false") would ENABLE the experimental attested rung the stable verifier must withhold
  if (aea === ADMIT_REJECT) return { result: 'INVALID', detail: 'allowExperimentalAttested must be a boolean (round-43 P1-02 — a coerced value must not enable the experimental attested rung)' };
  const chn = verifyAuthorityCheckpointChain(chain, { genesis, context, genesisAuthority, pinnedPrior, keylogEntries });   // K5: forward the prefix witness so a growth chain can reach corroborated (round-3 P0-3)
  if (chn.result !== 'VALID') return chn.result === 'INDETERMINATE' ? chn
    : { result: 'INVALID', error: chn.error, detail: 'checkpoint chain not authorized: ' + (chn.detail || chn.error), keylog_freshness: 'unverified' };
  const b = chain[chain.length - 1].body, headId = chn.head;
  if (target) {                                                                         // bind to the target's authority epoch + domain
    if (target.active_genesis !== undefined && b.active_genesis !== target.active_genesis) return { result: 'INVALID', error: 'E-GENESIS', detail: 'checkpoint active_genesis ≠ target', keylog_freshness: 'unverified' };
    if (target.domain_shard !== undefined && b.domain_shard !== target.domain_shard) return { result: 'INVALID', error: 'E-GENESIS', detail: 'checkpoint domain ≠ target', keylog_freshness: 'unverified' };
  }
  const term = verifyKeylogTerminality({ root: b.keylog?.root, length: b.keylog?.length, head: b.keylog?.head }, terminality || {});  // head = LAST entry (position L-1) AND no successor at L
  if (!term.terminal) return { result: 'INDETERMINATE', reason: 'terminality_unproven', detail: term.detail || 'key-log head terminality not proven', keylog_freshness: 'unverified' };
  if (!commitment) return { result: 'INDETERMINATE', reason: 'unavailable', detail: 'no external-commitment evidence supplied for the checkpoint id', keylog_freshness: 'unverified' };
  if (!target || !target.anchor) return { result: 'INDETERMINATE', reason: 'unavailable', detail: 'no target anchor evidence to order against', keylog_freshness: 'unverified' };
  if (typeof target.subject !== 'string' || !target.subject) return { result: 'INDETERMINATE', reason: 'unavailable', detail: 'no target.subject id to bind the anchor evidence to', keylog_freshness: 'unverified' };
  // M3 — evidence enters ONLY through the seam: a signed receipt of a consumer-admitted connector (or a pre-verified
  // token), scope-bound to THIS chain's authority scope and subject-bound to the checkpoint id / target subject.
  // A caller-minted facts object (the rc.35 round-2 forge) is not in image(VerifyEvidence_C) and earns nothing.
  const scope = { domain_shard: b.domain_shard, active_genesis: b.active_genesis, genesis_epoch: b.genesis_epoch };
  const cAdm = admitFreshnessEvidence(commitment, headId, scope, trust);
  if (!cAdm.evidence) return { result: 'INDETERMINATE', reason: 'evidence_unverified', detail: 'commitment: ' + cAdm.detail, keylog_freshness: 'unverified' };
  const aAdm = admitFreshnessEvidence(target.anchor, target.subject, scope, trust);
  if (!aAdm.evidence) return { result: 'INDETERMINATE', reason: 'evidence_unverified', detail: 'anchor: ' + aAdm.detail, keylog_freshness: 'unverified' };
  if (!temporalOrderCapable(cAdm.evidence) || !temporalOrderCapable(aAdm.evidence))    // Phase 2: proof_kind must ESTABLISH temporal order (content-addressed / map / opaque cannot)
    return { result: 'INDETERMINATE', reason: 'order_unproven', detail: 'commitment/anchor evidence class does not establish temporal order (capability check: ' + evidenceClass(cAdm.evidence.proof_kind) + ')', keylog_freshness: 'unverified' };
  const ord = compareEvidenceOrder(cAdm.evidence, aAdm.evidence);                       // F.5g proof relation, NOT a timestamp compare
  if (ord !== 'proven-after') return { result: 'INDETERMINATE', reason: 'order_unproven', detail: 'checkpoint commitment not proven-after the target (' + ord + ')', keylog_freshness: 'unverified' };
  // corroborated holds. Phase C — an INDEPENDENT anti-equivocation proof over THIS checkpoint upgrades to `attested`.
  // Uniqueness on an UNAUTHORIZED/UNBOUND checkpoint never reaches here (the corroborated conjunction above already
  // failed) ⇒ `attested ⇒ corroborated ∧ independent-uniqueness`; uniqueness alone never earns `attested`.
  if (uniqueness) {
    let uq = null;                                                                       // two INDEPENDENT bases for the SAME predicate
    const cpCur = uniqueness.map ? mapRootBasis(trust, uniqueness.map.mapRoot, uniqueness.map.rootProof) : null;   // F.5a.2b — SAME coordinate, second typed space: here the key is write-once by intent, so an unlabelled currency is an unchecked PREMISE (a later root binding a rival value is equivocation, the very event this rung claims to exclude)
    if (uniqueness.map && cpCur) uq = { ...verifyCheckpointMapUniqueness(uniqueness.map.proof, { domain_shard: b.domain_shard, genesis_epoch: b.genesis_epoch, sequence: b.sequence, checkpoint: headId, mapRoot: uniqueness.map.mapRoot }), map_root_currency: cpCur.basis, ...(cpCur.as_of ? { map_root_as_of: cpCur.as_of } : {}) };   // Phase 1: map root must be consumer-admitted (trust.mapRoots)
    if ((!uq || !uq.attested) && uniqueness.attestations) uq = verifyCheckpointUniqueness(uniqueness.attestations, { domain_shard: b.domain_shard, genesis_epoch: b.genesis_epoch, sequence: b.sequence, checkpoint: headId, trustRoots: uniqueness.trustRoots, domains: uniqueness.domains, threshold: uniqueness.threshold });
    if (uq && uq.attested) {
      // K1 ship-gate: the STABLE verifier does not emit `attested`. Without the explicit experimental opt-in the
      // proof still HOLDS (uniqueness verified) but the reported rung is capped at `corroborated`, with the withheld
      // rung named — an honest downgrade, never a silent one.
      if (aea !== true) return { result: 'VALID', keylog_freshness: 'corroborated', basis: uq.basis, anti_equivocation: 'attested',
        attested_withheld: 'experimental-gate', stability: 'experimental-extension',
        ...(uq.map_root ? { map_root: uq.map_root } : {}), ...(uq.map_root_currency ? { map_root_currency: uq.map_root_currency } : {}), ...(uq.map_root_as_of ? { map_root_as_of: uq.map_root_as_of } : {}), head: headId, sequence: b.sequence, active_genesis: b.active_genesis };
      return { result: 'VALID', keylog_freshness: 'attested', basis: uq.basis, anti_equivocation: 'attested', stability: 'experimental-extension',
        ...(uq.threshold ? { threshold: uq.threshold, accepted_witnesses: uq.accepted_witnesses, trust_domains: uq.trust_domains } : {}),
        ...(uq.map_root ? { map_root: uq.map_root } : {}), ...(uq.map_root_currency ? { map_root_currency: uq.map_root_currency } : {}), ...(uq.map_root_as_of ? { map_root_as_of: uq.map_root_as_of } : {}), head: headId, sequence: b.sequence, active_genesis: b.active_genesis };
    }
  }
  return { result: 'VALID', keylog_freshness: 'corroborated', basis: 'publisher-checkpoint', anti_equivocation: 'unverified',  // ceiling without independent uniqueness
    head: headId, sequence: b.sequence, active_genesis: b.active_genesis };
}

// ─── K4/Closed-Proof-Kernel (rc.37, UST-yoe) — THE ONE PUBLIC AUTHORITY ENTRYPOINT is now PROVER ∘ check_C: the
//     producer stack is DEMOTED to an untrusted prover that only PROPOSES a proof term; the reference checker
//     (packages/ust-protocol/reference-checker.mjs) is the SOLE acceptance oracle for any strong verdict. Trust comes
//     ONLY from config, never inputs (round-4 P0-02); D1 returns base + anti-equivocation basis, never a scalar
//     `attested`. Differential fuzz (rnd/fuzz-differential.mjs) proved check_C is strictly stricter than the old
//     producer path on exactly the round-4 residual holes. `verifyAuthorityBundle` + `buildAuthorityProof` are
//     RE-EXPORTED from the checker module (call-time cyclic import is safe — the checker uses index's leaf primitives
//     only inside function bodies).
export { verifyAuthorityBundle, buildAuthorityProof, checkAuthorityProof, checkAuthorityProofBytes, RULE_CONTRACTS, REFERENCE_CHECKER_RULES, REFERENCE_CHECKER_VERSION } from './reference-checker.mjs';

// ─── #78 ASSURANCE PRODUCT-LATTICE (formal-model F.5.0, CODE realization — the math must pass through code +
//     vectors + guard before it ships). The linear tier LIGHT ⊆ HIGH ⊆ TOP is ONE policy projection of a PRODUCT of
//     FOUR orthogonal, independently-strengthening STRENGTH axes — identity and freshness strengthen SEPARATELY
//     (F.5 gap 1/3, product-incomparability M1.4). Each axis is a finite CHAIN (a rank); AssuranceState is their
//     product under the componentwise (partial) order — a LATTICE of 2·3·4·2 = 48 states (identity is 3 rungs
//     since round-53 dropped `pinned`; the comment read 64 until 2026-08-14, when the arithmetic had not followed
//     the removal — spec §F.5.0 and formal-model F.5.0 carried 48 correctly throughout): meet = per-axis min,
//     join = per-axis max. `projectTier` reads ONLY identity+time (the classic tier); freshness rides alongside.
//     M1.1 (rc.36): EvidenceBasis is NOT a strength axis — a capability set is a Boolean lattice (P(Caps), ⊆), not a
//     4-chain; the rc.35 five-axis product (256) was self-contradictory ("a SET" yet "every axis is a total order").
//     STRENGTH (what is proven, per coordinate) is separate from SUPPORT (which capabilities the admitted evidence
//     supplies — EVIDENCE_CAPS_UNIVERSE below); support DERIVES strength, it is not a fifth coordinate.
export const ASSURANCE_AXES = deepFreeze({   // round-25 P0-04 — DEEP-frozen: mutating a nested axis array (e.g. reordering `identity`) would change projectTier's ranks and the exported REGISTRY alias → history-dependent TCB
  integrity: ['invalid', 'valid'],                                            // the §14 floor (canon/hash/sig) — the Integrity axis
  identity:  ['self-asserted', 'corroborated', 'authoritative'],    // A_id: name-binding + active-genesis uniqueness (§12.1a). round-53 — 'pinned'/TOFU rung removed: a domain claim needs the keylog, not a fragile pin
  freshness: ['unverified', 'fresh', 'corroborated', 'attested'],             // A_fresh: terminality + order + checkpoint uniqueness (§12.2a / §12.3.5)
  time:      ['unproven', 'anchored'],                                        // Fₜ: the anchor filtration (§11.2)
});
// M1.1 — the capability-support UNIVERSE (single-sourced from EVIDENCE_CAPS): |Caps| = 8, support ∈ (P(Caps), ⊆).
// A predicate is discharged only by an admissible capability (B4); no composition step manufactures one (B3).
export const EVIDENCE_CAPS_UNIVERSE = Object.freeze([...new Set(Object.values(EVIDENCE_CAPS).flat())].sort());
const AXES = Object.keys(ASSURANCE_AXES);
// round-39 P1-02 (R1) — the assurance lattice DOOR mirrors the input door: a malformed assurance value is a RETURNED
// reject sentinel (like admitDeep→ADMIT_REJECT), never a throw — so every lattice SURFACE (meet/join/le/projectTier/
// capAssurance) is a TOTAL function that RETURNS on any operand and passes the from-code hostile-argument sweep. The
// half-measure (round-37/38 coded-E-ASSURANCE throw) left the family partial: a surface consumer host-threw at position 0.
const ASSURANCE_REJECT = Symbol('assurance-reject');
export const axisRank = (axis, v) => { const chain = ASSURANCE_AXES[axis]; return Array.isArray(chain) ? chain.indexOf(v) : -1; };   // -1 ⇒ not a value of this axis (a non-axis key is TOTAL — returns -1, never a `undefined.indexOf` throw)
const ASSURANCE_BOTTOM = Object.freeze(Object.fromEntries(AXES.map((ax) => [ax, ASSURANCE_AXES[ax][0]])));   // ⊥ — the per-axis floor; the fail-closed value of a lattice op on an un-admittable operand (a meet under-reports, a join treats it as the identity — neither ever over-reports)
const axisLE = (axis, a, b) => { const ra = axisRank(axis, a), rb = axisRank(axis, b); return ra >= 0 && rb >= 0 && ra <= rb; };
// AssuranceState = a full 5-tuple; every axis present with an in-range value, else E-ASSURANCE (fail-closed).
export function assuranceState(s = {}) {
  const S = admitDeep(s); if (S === ADMIT_REJECT) return ASSURANCE_REJECT;   // round-39 P1-02 (R1) — the assurance DOOR RETURNS a reject sentinel (mirrors admitDeep→ADMIT_REJECT), never a throw: a hostile getter is a RETURNED reject so every lattice surface stays total-by-return under the sweep (round-38: admit ONCE into a frozen snapshot; a two-face Proxy cannot pass a weak face to the rank check and emit a strong one)
  const out = {};
  for (const ax of AXES) { const v = S[ax]; if (axisRank(ax, v) < 0) return ASSURANCE_REJECT; out[ax] = v; }   // (R3) read each axis ONCE from the admitted snapshot; a missing/out-of-range axis is a RETURNED reject sentinel, not a throw
  return out;
}
// The product order (F.5 gap 1): a ≤ b iff a ≤ b on EVERY axis — a PARTIAL order (identity & freshness independent,
// so most pairs are incomparable). meet/join make it a LATTICE.
export const assuranceLE = (a, b) => { const A = assuranceState(a), B = assuranceState(b); return A !== ASSURANCE_REJECT && B !== ASSURANCE_REJECT && AXES.every((ax) => axisLE(ax, A[ax], B[ax])); };   // round-39 P1-02 — TOTAL: an un-admittable operand ⇒ FALSE (an order we cannot establish is never asserted), never a throw (round-37: admit both into the in-range product before comparing)
const axisMin = (axis, a, b) => (axisRank(axis, a) <= axisRank(axis, b) ? a : b);
const axisMax = (axis, a, b) => (axisRank(axis, a) >= axisRank(axis, b) ? a : b);
export const meetAssurance = (a, b) => { const A = assuranceState(a), B = assuranceState(b); return (A === ASSURANCE_REJECT || B === ASSURANCE_REJECT) ? { ...ASSURANCE_BOTTOM } : Object.fromEntries(AXES.map((ax) => [ax, axisMin(ax, A[ax], B[ax])])); };   // round-39 P1-02 — TOTAL: a malformed operand ⇒ ⊥ (meet under-reports; no lattice op repairs a malformed operand into an earned state), never a throw
export const joinAssurance = (a, b) => { const A = assuranceState(a), B = assuranceState(b); const A2 = A === ASSURANCE_REJECT ? ASSURANCE_BOTTOM : A, B2 = B === ASSURANCE_REJECT ? ASSURANCE_BOTTOM : B; return Object.fromEntries(AXES.map((ax) => [ax, axisMax(ax, A2[ax], B2[ax])])); };   // round-39 P1-02 — TOTAL: a malformed operand contributes ⊥ (the join-identity), so a join NEVER synthesizes strength from garbage; both malformed ⇒ ⊥, never a throw
// PolicyProjection (F.5 gap 1 / F.5b): the classic tier reads ONLY identity+time. TOP = authoritative name ∧ anchored
// time; HIGH = name-bound (identity ≥ corroborated); LIGHT = the integrity floor; below the floor there is NO tier.
// This is the CANONICAL projection; the inline §14 verify tier is conformance-pinned to agree with it (no 2nd truth).
export function projectTier(state) {
  // Model rev39/rev91: every lattice surface is TOTAL and fails closed by RETURN — «a malformed operand must map to ⊥
  // and the op must RETURN, not throw … projectTier → NONE». `null`/`undefined` threw a TypeError, which is a coded throw
  // at a consumer boundary: the exact residual that statement was written to close.
  if (state === null || typeof state !== 'object') return 'NONE';
  const s = assuranceState(state);
  if (s === ASSURANCE_REJECT) return 'NONE';                                  // round-39 P1-02 — TOTAL: an un-admittable state has NO tier (fail-closed), never a throw
  if (!axisLE('integrity', 'valid', s.integrity)) return 'NONE';              // integrity floor unmet ⇒ INVALID upstream
  if (s.identity === 'authoritative' && s.time === 'anchored') return 'TOP';
  if (axisLE('identity', 'corroborated', s.identity)) return 'HIGH';          // corroborated ≤ identity ⇒ name-bound
  return 'LIGHT';
}
export const TIER_RANK = { NONE: -1, LIGHT: 0, HIGH: 1, TOP: 2 };
// capAssurance (F.5 gap 2 — the CAPPED term ℐ_C): the reported state is the MEET of what is PROVEN and the ceiling
// ADMISSIBLE under the consumer config C (a per-axis max: no accepted trust domains ⇒ freshness caps below
// `attested`; no pinned/accepted roots ⇒ identity caps at `self-asserted`). Assurance is EARNED by proof and CAPPED
// by trust — never self-declared; an unspecified ceiling axis imposes no cap (tops out).
export function capAssurance(state, ceiling) {
  const s = assuranceState(state);
  if (s === ASSURANCE_REJECT) return { ...ASSURANCE_BOTTOM };                  // round-39 P1-02 — TOTAL: an un-admittable proven state caps to ⊥
  if (ceiling === undefined || ceiling === null) return s;                     // round-40 P1-02 — ONLY a genuinely ABSENT ceiling is identity; a falsy `0`/`''`/`NaN`/`false` is MALFORMED, not absent (the `if (!ceiling)` footgun conflated them → preserved TOP)
  const C = admitDeep(ceiling);                                               // round-39 P1-02 (R1) — ADMIT the consumer ceiling ONCE (it is UNTRUSTED consumer config C, not a post-verification value): a hostile getter is a RETURNED ⊥ cap, never a host throw
  if (C === ADMIT_REJECT || typeof C !== 'object' || Array.isArray(C)) return { ...ASSURANCE_BOTTOM };   // round-40 P1-02 — a NON-RECORD ceiling (a scalar/array like 1/'x'/[]) is malformed → ⊥; the old code filled every missing axis to the axis MAX, so a non-record preserved TOP instead of capping to ⊥ (downgrade-resistance F.5 gap 2)
  const cap = assuranceState(Object.fromEntries(AXES.map((ax) => [ax, C[ax] ?? ASSURANCE_AXES[ax][ASSURANCE_AXES[ax].length - 1]])));
  if (cap === ASSURANCE_REJECT) return { ...ASSURANCE_BOTTOM };               // an out-of-range ceiling axis caps to ⊥ (never LIFTS the reported state)
  return meetAssurance(s, cap);
}

// ─── C3 (UST-6vj, M1.2) — THE ONE ASSURANCE ASSEMBLER. Pure, deterministic, total: strength coordinates are DERIVED
//     from SEAM VERDICTS (the resolveAuthority / deriveCheckpointFreshness / verifyAnchor result objects) by fixed
//     rules — never from caller labels or booleans; capability SUPPORT is the union of capabilities of evidence that
//     is actually in image(VerifyEvidence_C) (the WeakSet witness) — a minted look-alike contributes nothing (B3: no
//     step manufactures a capability). Runs strictly AFTER the §14 integrity floor: an INVALID document never reaches
//     assembly, so integrity is 'valid' by position, not by parameter (Reach_C: the verifier only emits tuples whose
//     coordinates were each earned by a predicate; the confinement property is pinned in conformance, Phase V).
// C3/K3 — provePredicates maps SEAM VERDICTS (resolveAuthority / deriveCheckpointFreshness / verifyAnchor results,
// plus image('evidence') handles) to the proven-atom strength coordinates and mints a branded PREDICATE-GRAPH handle.
// It is the ONLY producer of that handle. The seam verdicts it reads are produced by the verifiers, never by a caller
// label. (K4/K7: the kernel calls this over verdicts it derived from RAW inputs; a caller cannot reach it with fakes.)
// K7 (rc.37, calculus §7) — the HORN inference layer. provePredicates extracts the proven ATOMS from seam verdicts
// (never caller labels), then a fixed rule set derives the rungs as a least-closure; the trace records each derived
// predicate with its premises, so every strong verdict is explainable. `deriveAssurance` projects the closure. The
// coordinate values are unchanged — the win is that assurance is a DERIVATION over admitted atoms, not a passed value.
const HORN_RULES = [
  { concl: 'IdentityAuthoritative', premises: ['name-bound', 'active-genesis-unique'] },
  { concl: 'FreshnessCorroborated', premises: ['checkpoint-authorized', 'scope-bound', 'keylog-append-only', 'snapshot-terminal', 'checkpoint-committed', 'checkpoint-after-target'] },
  { concl: 'FreshnessAttested', premises: ['FreshnessCorroborated', 'checkpoint-unique'] },
  { concl: 'TierTOP', premises: ['integrity-valid', 'IdentityAuthoritative', 'time-anchored'] },
  { concl: 'TierHIGH', premises: ['integrity-valid', 'name-bound'] },
  { concl: 'TierLIGHT', premises: ['integrity-valid'] },
];
const hornClosure = (atomSet) => {                                                    // least fixed point over HORN_RULES
  const closed = new Set(atomSet), trace = [];
  let grew = true;
  while (grew) { grew = false; for (const r of HORN_RULES) if (!closed.has(r.concl) && r.premises.every((p) => closed.has(p))) { closed.add(r.concl); trace.push({ rule: r.concl, premises: r.premises.slice() }); grew = true; } }
  return { closed, trace };
};
export function provePredicates(seams = {}) {
  const S = admitDeep(seams);   // round-46 self-audit (totality) — extend the round-25 P1-02 null-tolerance to a HOSTILE Proxy: a getter/accessor seams object would fire on destructuring below → a host throw at this public (unbranded) door. admitDeep → an inert snapshot (or ADMIT_REJECT → the LIGHT floor); the output is unbranded regardless (only verify() seals), so a hostile input simply floors, never throws.
  const { identity, freshness, anchor, evidence = [] } = (S && typeof S === 'object' && S !== ADMIT_REJECT) ? S : {};   // round-25 P1-02 — null-total: `provePredicates(null)` no longer throws on destructuring (a malformed non-null arg floors to LIGHT)
  const verified = identity?.status === 'verified';                                  // 'suspect' (pre-compromise window) never name-binds — mirrors §14 exactly
  const idStr = !verified ? 'self-asserted'
    : identity.strength === 'authoritative' ? 'authoritative'
    : identity.strength === 'corroborated' ? 'corroborated'
    : 'self-asserted';   // round-53 — no `pinned` rung: identity is self-asserted (bare key) OR name-bound (corroborated/authoritative)
  const frStr = (freshness?.result === 'VALID' && (freshness.keylog_freshness === 'corroborated' || freshness.keylog_freshness === 'attested')) ? freshness.keylog_freshness
    : identity?.freshness === 'fresh' ? 'fresh' : 'unverified';                       // fresh = the single-view §12.2a rung carried by the identity resolution
  const tmStr = anchor?.inclusion === true && anchor?.time === 'anchored' ? 'anchored' : 'unproven';
  const support = [...new Set((Array.isArray(evidence) ? evidence : [])
    .filter((e) => isHandle('evidence', e))                                          // K3: capability only from image(VerifyEvidence_C) (B3)
    .flatMap((e) => evidenceCaps(e.proof_kind)))].sort();
  // K7 — the proven ATOMS the seams established (the coordinate strengths are read from these). The Horn closure
  // derives the composite rungs + a premise trace; a coordinate never lifts without its atom (no-upward-forge).
  const atomSet = ['integrity-valid'];
  if (idStr === 'authoritative') atomSet.push('name-bound', 'active-genesis-unique');
  else if (idStr === 'corroborated') atomSet.push('name-bound');                          // corroborated ≥ the HIGH threshold ⇒ name-bound (TierHIGH)
  if (frStr === 'corroborated' || frStr === 'attested') atomSet.push('checkpoint-authorized', 'scope-bound', 'keylog-append-only', 'snapshot-terminal', 'checkpoint-committed', 'checkpoint-after-target');
  if (frStr === 'attested') atomSet.push('checkpoint-unique');
  if (tmStr === 'anchored') atomSet.push('time-anchored');
  const { trace } = hornClosure(atomSet);
  // round-25 P0-01 — provePredicates is the PURE seam→atom MAPPER and returns an UNBRANDED graph. It does NOT mint the
  // predicate-graph brand: minting from caller-shaped labels was a public forgery oracle (deriveAssurance blessed it → TOP
  // with zero verified evidence). The brand is minted ONLY by sealPredicateGraph, called ONLY inside verify() (the TCB),
  // which passes REAL seam verdicts. A consumer can still inspect this projection, but deriveAssurance rejects an
  // unbranded graph — so caller labels earn no authoritative verdict. (isHandle('predicate-graph', provePredicates(x)) === false.)
  return Object.freeze({ atoms: Object.freeze({ integrity: 'valid', identity: idStr, freshness: frStr, time: tmStr }),
    support: Object.freeze(support), provenAtoms: Object.freeze(atomSet.slice()), derivation: Object.freeze(trace.map((t) => Object.freeze({ ...t, premises: Object.freeze(t.premises) }))) });
}
// MODULE-PRIVATE (never exported — not in package `exports`, so no consumer path reaches it): the sole minter of the
// predicate-graph brand. Only verify() calls it, and only over seam verdicts produced by the real verification seams.
const sealPredicateGraph = (graph) => mintHandle('predicate-graph', graph);
// The assembler. Takes ONLY a branded PredicateGraph (K3 — a caller-shaped {identity:'authoritative'} is NOT a graph,
// so no coordinate lifts: round-3 P0-4 closed at the type level). Emits the frozen assurance report + the K7 trace.
export function deriveAssurance(graph) {
  if (!isHandle('predicate-graph', graph)) return Object.freeze({ error: 'E-ASSURANCE', detail: 'deriveAssurance requires a verified PredicateGraph handle (K3 — build it with provePredicates over seam verdicts; a caller-shaped object earns nothing)' });
  const raw = assuranceState({ integrity: 'valid', ...graph.atoms });
  if (raw === ASSURANCE_REJECT) return Object.freeze({ error: 'E-ASSURANCE', detail: 'derived atoms out of range (a branded graph carries in-range atoms; this is the totality floor at the one internal lattice-door caller)' });   // round-39 P1-02 — the door sentinel is handled here; graph.atoms is trusted (sealed graph) so this is defensive
  const strength = Object.freeze(raw);
  return Object.freeze({ strength, support: graph.support, tier: projectTier(strength), derivation: graph.derivation, provenAtoms: graph.provenAtoms });
}

// ─── #oy8 CANONICAL REGISTRY — the SINGLE SOURCE OF TRUTH for the protocol's machine-checkable STRING SETS. The spec's
//     registry blocks (§15/§17) are GENERATED from this (tools/gen-spec-registry.mjs), and tools/spec-code-sync.mjs
//     asserts the code's ACTUAL literal usage (H/Hbytes domains, `purpose:` strings, thrown `E-` codes) equals these
//     sets — so spec prose, this registry, and code usage cannot silently diverge (the spec↔code drift seam). Enums
//     that already exist as code (ASSURANCE_AXES, TIER_RANK) are REFERENCED, never re-declared.
export const REGISTRY = deepFreeze({   // round-25 P0-04 — DEEP-frozen: the canonical string sets are TCB truth; no in-process mutation
  // hash domain tags (§7/§17) — the tag passed to H()/Hbytes(). MEASURED against actual usage by spec-code-sync.
  hashDomains: ['ust:state', 'ust:shard', 'ust:seed', 'ust:keylog', 'ust:leaf', 'ust:node',
    'ust:authority-checkpoint', 'ust:checkpoint-map-key', 'ust:checkpoint-map-value', 'ust:name-map-key', 'ust:name-map-value',
    'ust:keylog-empty', 'ust:keylog-leaf', 'ust:keylog-node', 'ust:keylog-commit', 'ust:smt-empty', 'ust:smt-node', 'ust:smt-leaf',
    'ust:genesis-epoch', 'ust:authority-scope', 'ust:evidence-receipt', 'ust:registry', 'ust:enc-iv'],
  // signed `canon` preimage purposes (§12.1a/§12.3) — domain-separated, never interchangeable.
  purposes: ['ust:name-no-fork', 'ust:authority-checkpoint', 'ust:authority-checkpoint-signature',
    'ust:checkpoint-authority-recovery', 'ust:genesis-epoch-transition', 'ust:checkpoint-uniqueness-attestation',
    'ust:evidence-receipt', 'ust:evidence-receipt-signature', 'ust:name-map-root'],   // #42 — the signed name-map ROOT statement (§12.3.4, F.5a.2)
  // §17 AEAD registry — the algorithms `enc.alg` may name, each with the nonce width it carries as the PREFIX of
  // `ct`. Exposed HERE, and not left as a module-private constant, for the reason #154 taught with partition
  // kinds: a set that only the code knows drifts from the set the spec states, and two clean-room verifiers
  // implement whichever they can see. The widths are part of the registry because the layout is only readable
  // if the width is known — a reader slicing 12 bytes off an XChaCha ciphertext recovers nothing and reports the
  // DOCUMENT as unauthentic, naming the publisher for its own misparse.
  aeadAlgs: AEAD_ALGS,
  aeadNonceBytes: AEAD_NONCE_BYTES,
  // PARTITION KINDS (§4.4/§5, F.1.1) — the domain `K` a verifier admits, and the ONLY definition of it. The
  // obligation is EQUALITY: admitting more accepts a tag the standard never defined; admitting fewer returns a
  // false INVALID on a conforming document, and the verdict then names the publisher rather than the verifier.
  // Measured #154 (2026-08-13): `absence` was missing from the browser verifier and the extension — two shipped,
  // clean-room implementations — and every live document of the reference operator was refused, because every
  // slot carries an `absence` partition for each source that did not answer. Both were faithful to the registry
  // they read; the registry named two kinds and the §4.4 grammar named three. The four independent enumerations
  // are why this set is here: the spec's registry section is GENERATED from it, and every implementation's
  // literal — including the clean-room ones, which must NOT import this module — is diffed against it.
  partitionKinds: ['captured', 'computed', 'absence'],
  // ANCHOR REFUSAL REASONS (§11.2, F.5c.1, F.5.1e) — the CLOSED set a substrate connector may return beside
  // `final: false`, mapped to WHO CAN ACT on it. The three values are the F.5.1 table collapsed to its only
  // decision-relevant axis: `evidence` = the publisher publishes different bytes; `faculties` = the consumer
  // brings a connector, a network, a trust root; `ruleset` = NEITHER party, this build has no rules for it.
  // A conjunction refusing without a reason collapses all three into one word, and the collapse always resolves
  // toward the reader, who is present — measured #155 (2026-08-13): a 32-bit narrowing in the browser verifier's
  // inclusion climb reached readers as `pending — a browser cannot decide`, and stayed there sixteen days
  // because the sentence was about them and therefore not falsifiable by them.
  anchorRefusalReasons: {
    'proof-absent': 'evidence',                  // the anchor carries no proof, and no pointer to fetch one
    'unreadable-entry': 'evidence',              // the proof is present and does not decode
    // `unsupported-proof-form` replaces the shipped `wrong-entry-kind` (2026-08-07 → #155): a rekor entry kind
    // this connector does not implement and an OTS op it does not implement are ONE thought, and the old name
    // also carried the wrong TERM — "wrong" reads as the publisher's error where the term is the ruleset's.
    'unsupported-proof-form': 'ruleset',         // a proof form this build has no rules for
    'entry-attests-another-root': 'evidence',    // the entry is sound and commits to a DIFFERENT root
    'inclusion-failed': 'evidence',              // the RFC 6962 path does not reach the proof's rootHash
    'checkpoint-unsigned': 'evidence',           // rootHash is not a head the log's pinned key signed
    'substrate-unreachable': 'faculties',        // a surface this verifier must read did not answer
  },
  // INVALID error codes (§15) — every code the verifier/API can emit. Ordered as §15 lists them.
  errorCodes: ['E-MALFORMED', 'E-CANON', 'E-BOUNDS', 'E-CYCLE', 'E-SIG', 'E-KEY', 'E-GENESIS', 'E-ANCHOR',
    'E-COMMIT', 'E-ROOT', 'E-SEED', 'E-PREV', 'E-AUTHORITY', 'E-SEQ', 'E-EVIDENCE', 'E-ASSURANCE', 'E-BYTES', 'E-REPLICATION', 'E-DISCOVERY'],   // E-BYTES — the byte-admission door class (round-48 P0-01: snapshotBytes rejects a non-native-Uint8Array as E-BYTES-TYPE/-SHARED/-SIZE)
  // RESOLVER-LEVEL codes — NOT verdicts about a document, and kept in their OWN set for exactly that reason (#144).
  // A resolver (`resolveKeys`, `resolveCadence`, the referent walk) returns a record, not a §14 verdict, so it has
  // no INDETERMINATE to return; when THIS BUILD lacks the faculty to decide, it must still fail CLOSED — a caller
  // that reads `.error` stops, whereas a silent empty result would read as "no keys" and pass. Listing these
  // beside the INVALID codes would state the opposite of what the code does: the document-verifier answers the
  // very same condition with `INDETERMINATE(unsupported_alg)`, never with an E-code. Inability is not guilt
  // (F-theorem clause 5); it is also not silence.
  resolverErrorCodes: ['E-UNSUPPORTED'],
  // INDETERMINATE reasons — the §14 document-verifier's CLOSED set, and the §12.3.6 authority-checkpoint set (distinct).
  // `unsupported_minor` (#138) is DISTINCT from `unsupported_alg`: the algorithm case is a primitive this verifier
  // cannot compute, the minor case is a ruleset it does not have. One word for two mechanisms is the collision this
  // registry exists to prevent.
  // `unsupported_construction` (round 201) joins the `unsupported_*` row rather than starting a parallel one: it is
  // the same statement as `unsupported_alg` — this BUILD cannot compute what the document asks for — made about the
  // inclusion tree instead of the signature suite. A second vocabulary for one thought is the collision this
  // registry exists to prevent.
  indeterminateReasons: { document: ['unavailable', 'unsupported_alg', 'unsupported_construction', 'unsupported_minor', 'unsupported_major', 'resource_limit', 'stale_keylog'],
    checkpoint: ['authority_unresolved', 'terminality_unproven', 'order_unproven', 'evidence_unverified', 'chain_consistency_unproven'] },
  tiers: Object.keys(TIER_RANK),                                    // NONE/LIGHT/HIGH/TOP — single-sourced from TIER_RANK
  assuranceAxes: ASSURANCE_AXES,                                    // single-sourced from the #78 lattice (§F.5.0)
  evidenceOrder: ['proven-after', 'not-after', 'unproven'],        // compareEvidenceOrder returns (§12.3.5)
  // §14 verdict RESULTS and §11.3 completeness words. Registered here — and MEASURED against actual usage by
  // spec-code-sync — because a machine-checkable string set that is NOT in this registry becomes a hand-typed
  // roster in whichever gate needs it, which is how `OP_FIELDS` let a retired op survive in a producer contract
  // for three rounds (round 84). `results` is the VERIFY verdict only: `forkChoice` returns a `result: 'CANONICAL'`
  // under the same field name for a different question (which candidate is canonical), and that collision is
  // named in the sync check rather than absorbed by widening this set.
  // The three OUTCOME KINDS §15 enumerates, and now nothing else: round 103 moved the totality guards of `verifyAsync` and
  // `resolveByDiscovery` from `{result:'E-MALFORMED'}` to `{result:'INVALID', error:'E-MALFORMED'}` — the shape every
  // other refusal already used. `forkChoice` still answers a DIFFERENT question under this field name and keeps its
  // own set below; qualifying that is a breaking change and stays open as thelabmd/UST-Protocol#111.
  results: ['VALID', 'INVALID', 'INDETERMINATE'],
  completeness: ['none', 'provisional', 'chain-consistent', 'complete'],
  // `forkChoice` answers a DIFFERENT question under the SAME field name, and the collision is registered rather
  // than absorbed: it overlaps `results` at INDETERMINATE and borrows an ERROR CODE, `E-PREV`, as a result value.
  // A consumer reading `result` cannot tell which vocabulary it is in without knowing which call produced it.
  // Registered so the set is MEASURED and cannot grow silently; resolving the collision is thelabmd/UST-Protocol#111.
  // `forkChoice` answers a DIFFERENT question under the same field name, and round 103 made that impossible to
  // misread WITHOUT renaming anything: every return now carries `kind: 'fork-choice'`, so a consumer always knows
  // which vocabulary it is in, and the error codes moved to `error` beside `result: 'REFUSED'` — the same line the
  // verdict guards were brought onto. Additive: no published field changed its name or disappeared.
  forkChoiceResults: ['CANONICAL', 'MULTI_AUTHORITY', 'INDETERMINATE', 'REFUSED'],
  // The DISCRIMINATOR itself, registered (round 199) because the sentence above claims a property no artifact was
  // checking. `spec-code-sync` now enumerates every `return {` in `forkChoice` and fails on one that omits this
  // exact string — a discriminator carried on ten paths and dropped on the eleventh is worse than none, since the
  // consumer's test passes often enough to be trusted. ASYMMETRIC on purpose (F.5e.2a): the §14 verdict carries no
  // `kind`, so PRESENT means "not a §14 verdict" and ABSENT means "it is one". A future answer that is neither must
  // register its own kind rather than lean on absence.
  forkChoiceKind: 'fork-choice',
  // F.9.5-c.4 (round 201) — the name of the BUNDLED inclusion construction: the tagged `ust:leaf`/`ust:node` walk
  // over ASCII `sha256:` strings (§7/§11.2). Registered, not spelled inline, because `name(π)` is total: a proof
  // declaring nothing declares THIS, and a proof declaring anything else is withheld rather than walked with it.
  // §11.2 keeps the construction a CONNECTOR — registering the reference one names the default, it does not make
  // the protocol prescribe a tree.
  referenceConstruction: 'ust-merkle-tagged',
  // #153 — the construction → CLAIMANT map. A refusal for an unsupported construction is CORRECT and must not
  // change (walking a foreign construction with the reference tree is the proof-substitution hole §11.2 exists to
  // refuse), but a refusal that is right and unactionable still costs its reader an afternoon. By the F.5.1 table
  // this remedy is `ℐ_v` — *bring a connector* — and that column is the CONSUMER's to move, so naming which one
  // is the attribution the model prescribes rather than a courtesy.
  //
  // It is DATA, not a sentence, for the reason §11.2 makes the construction a connector at all: the set is open.
  // A third party publishing a construction is the ordinary case, so a name absent from this map must produce a
  // message that routes to the registry, never a dead end that reads as *no such thing exists*.
  inclusionConstructions: {
    'ust-merkle-tagged': 'bundled — implemented by this build (§7/§11.2)',
    'rfc6962-raw': '@ust-protocol/rfc6962-verify — inject its `inclusionVerify` as the connector (re-exported by @ust-protocol/rekor-verify for consumers installed before round 236)',
  },
  // #42 — the MECHANISM is named here; the AUTHORITY is not, and must not be. Registering a mechanism says how a
  // name-map root is carried and checked, which every implementation must agree on. Registering an authority would
  // spend the specification's neutrality to bless one operator, and F.5a.2c shows it would not even close the
  // question — a namespace authority is not derivable, so it belongs in the consumer's configuration by construction.
  nameMapConstructions: {
    'ust-name-map/v1': 'a depth-256 sparse Merkle map over H("ust:name-map-key", canon({domain_shard})); its root is carried as a CLOSED signed statement { claim: { purpose: "ust:name-map-root", map_root }, issuer_id, sig } and admitted against the consumer\'s `mapAuthorities`, with the DATE taken from an anchor and never from the signer (§12.3.4, F.5a.2)',
  },
  verifiedEvidenceFields: { required: ['proof_kind', 'subject', 'source_id', 'facts'], optional: ['verifier_id', 'verifier_version'] },
  // M3 — the SIGNED connector-receipt claim (§12.3.5): facts only; a capability/assurance/independence field is E-EVIDENCE.
  evidenceReceiptClaimFields: { required: ['version', 'purpose', 'domain_shard', 'active_genesis', 'genesis_epoch', 'subject', 'proof_kind', 'facts', 'issued_at'], optional: ['payload_digest'] },
});

// The digest of the frozen vocabulary this verifier decides by: the canonical string sets that define what a
// verdict can even SAY (tiers, error codes, indeterminate reasons, assurance axes, hash domains, purposes). Emitted in
// every VALID verdict beside `verifier`, so a held verdict names the rules it was judged under and stays re-checkable
// after those rules move. Memoized: REGISTRY is deep-frozen, so the digest is a constant of the build.
// NOTE the deliberate naming: this is the DOCUMENT verifier (VERSION.revision), NOT the L1 authority checker
// (REFERENCE_CHECKER_VERSION, a separate revision line). Calling this field \`checker_version\` would conflate two
// different frozen surfaces that move independently.
let _regDigest = null;
export const registryDigest = () => (_regDigest ??= H('ust:registry', canon(REGISTRY)));


// ─── TOP §11.3 completeness: a sequenced stream is prev-chained; first frame's prev = genesis content_hash
//     (M4); per-frame validity is verified too (X2 — completeness ≠ validity); duplicate ust_id / shared prev
//     = a fork ⇒ E-PREV (Y1). A covering checkpoint (M5) proves 'chain-consistent' (no-deletion); the open tail
//     is 'provisional'. 'complete' (no-omission, needs the signed-cadence grid, F.4) is a future rung (#69 C).
export function verifyStream(frames, config) {
  const c = admitOpts(config); if (c === null) return { complete: 'none', detail: 'config must be an inert record (round-24 P1-01 totality)' };   // round-24 P1-01 (self-audit) — the parallel stream surface, total for null/hostile config
  { const Fr = admitDeep(frames); if (Fr === ADMIT_REJECT) return { complete: 'none', detail: 'frames are not an inert record (round-27)' }; frames = Fr; }   // round-27 — snapshot
  let { genesis, keylog, checkpoint, cadenceLog, requirePerFrameValid = true } = c;
  const rpfv = admitBool(requirePerFrameValid, true);   // round-43 P1-02 — strict: a present falsy non-boolean (0/"") would DISABLE X2 per-frame signature verification and pass a tampered frame as complete
  if (rpfv === ADMIT_REJECT) return { complete: 'none', detail: 'requirePerFrameValid must be a boolean (round-43 P1-02 — a coerced value must not disable per-frame signature verification)' };
  // rev38 R3 (round-31 P0-02) — admitOpts is SHALLOW: the NESTED `genesis` and `checkpoint` in config stay live caller objects.
  // verifyStream calls verify(checkpoint) but then re-reads the raw checkpoint's class/head/count (and reads raw genesis for
  // prevHash) — a two-face Proxy served the signed face to verify and an unsigned face to the reads. Admit both nested docs once.
  if (genesis != null) { const G = admitDeep(genesis); if (G === ADMIT_REJECT || typeof G !== 'object' || Array.isArray(G)) return { complete: 'none', detail: 'genesis must be an inert record (round-41 P1-02 — a PRESENT falsy/scalar/array genesis is MALFORMED, not absent; round-31 R3 — nested config docs are admitted once)' }; genesis = G; }
  if (checkpoint != null) { const C = admitDeep(checkpoint); if (C === ADMIT_REJECT || typeof C !== 'object' || Array.isArray(C)) return { complete: 'none', detail: 'checkpoint must be an inert record (round-41 P1-02 — a PRESENT falsy/scalar/array checkpoint is MALFORMED, not absent; round-31 R3 — nested config docs are admitted once)' }; checkpoint = C; }
  if (!Array.isArray(frames) || !frames.length) return { complete: 'none' };
  const authority = frames[0]?.state?.id?.domain_shard;                // §11.3: a stream belongs to ONE authority
  if (typeof authority !== 'string') return { complete: 'none', detail: 'malformed first frame — no stream authority (round-24 self-audit totality)' };   // round-24 (self-audit) — a malformed frame is a structured result, never a host throw
  let prevHash = genesis ? contentHash(genesis) : null;
  // #75 P0-03b — when a genesis is supplied, EACH frame's key MUST be BOUND to that authority's key-log (key ∈
  // K_A), not merely claim the `domain_shard`. Otherwise an impostor's frames (key ∉ K_A), prev-chained to the
  // victim genesis hash, read as a `complete` stream under the victim's name. Math: the LIGHT `domain_shard` is a
  // CLAIM; binding (ROOT 1+2) is the proof. No manifest needed — the authority is the resolved key set.
  let boundKeys = null;
  if (genesis) {
    if (genesis.state?.id?.domain_shard !== authority) return { error: 'E-AUTHORITY', detail: 'genesis domain_shard != stream authority (' + authority + ')' };
    const rk = resolveKeys(genesis, keylog == null ? [] : keylog);   // round-42 P1-02 — do NOT coalesce a PRESENT non-array keylog to [] (that hid a malformed selector as absent); resolveKeys admits it and returns E-MALFORMED for a present non-array
    if (rk.error) return { error: rk.error, detail: 'stream authority: ' + rk.detail };
    boundKeys = rk.validKeys;
  }
  const seenUstId = new Set(), seenPrev = new Set();
  let lastE = null;
  for (const [i, f] of frames.entries()) {
    // X2 — per-frame INTEGRITY (signature). #144: `v.error` is present on INVALID and ABSENT on INDETERMINATE, so a
    // verdict of "I could not check this signature" read as a passing frame — a build that verified NOTHING
    // reported the same `provisional` stream as one that verified everything. X2 is an assertion ABOUT the
    // signature; a verifier that did not evaluate it cannot make that assertion, and must say so instead.
    if (rpfv) {
      const v = verify(f, { context: 'data' });
      if (cannotDecide(v)) return { complete: 'none', reason: 'unsupported_alg', detail: 'frame ' + i + ': ' + v.detail };
      // round-53 (UST-ybn): a name-form frame with a VALID signature is INDETERMINATE (identity unconfirmed at
      // bare LIGHT), NOT an error — the stream's identity/completeness is a SEPARATE axis, so accept
      // INDETERMINATE here. That acceptance is right and stays; #144 only removes the ONE reading of
      // INDETERMINATE it was never meant to cover, where the signature was not evaluated at all.
      if (v.error) return { error: 'E-SIG', detail: 'frame ' + i + ' invalid: ' + v.error };
    }   // …so X2 fails only on a real integrity error (round-43 — the ADMITTED boolean, never the coerced opts field)
    if (f.state.id.domain_shard !== authority) return { error: 'E-AUTHORITY', detail: 'frame ' + i + ' domain_shard != stream authority (' + authority + ') — mixed-authority stream' };
    if (boundKeys && boundKeys.get(f.state.id.key_id) !== f.sig.pub) return { error: 'E-AUTHORITY', detail: 'frame ' + i + ' key not bound to the authority key-log — impersonation (key ∉ K_A, §12.2)' };
    if (seenUstId.has(f.state.id.ust_id)) return { error: 'E-PREV', detail: 'duplicate ust_id (fork, Y1): ' + f.state.id.ust_id };
    seenUstId.add(f.state.id.ust_id);
    // #69 C P0 — the prev-chain must ALSO be CHRONOLOGICAL: `ust_id` strictly increasing in chain order. Else a
    // publisher could permute slots in TIME while keeping the chain valid and the grid-set covered (a real
    // reordering, hidden). `ust_id` is the time coordinate; compare by epoch.
    const fe = ustToEpoch(f.state.id.ust_id);
    if (i > 0 && fe !== null && lastE !== null && fe <= lastE) return { error: 'E-PREV', detail: 'frame ' + i + ' ust_id ' + f.state.id.ust_id + ' not chronologically after its predecessor — reordered stream (§11.3)' };
    lastE = fe;
    const p = f.state.provenance?.prev;
    if (i === 0) { if (genesis && p !== prevHash) return { error: 'E-PREV', detail: 'first frame prev != genesis content_hash (M4)' }; }
    else if (p !== prevHash) return { error: 'E-PREV', detail: 'frame ' + i + ' prev dangling (broken chain)' };
    if (p && seenPrev.has(p)) return { error: 'E-PREV', detail: 'two frames share a prev (fork, Y1)' };
    if (p) seenPrev.add(p);
    prevHash = contentHash(f);
  }
  // M5 — a COVERING checkpoint over a genesis-BOUND origin closes the interval. Without a genesis the origin is
  // unbound → 'provisional'. #69 C / F.4 — even WITH the checkpoint, the chain + count prove NO-DELETION over the
  // SHOWN chain, NOT no-omission: a never-emitted slot leaves a self-consistent chain with a hole (link t+1→t-1).
  // So the honest ceiling here is 'chain-consistent'; 'complete' (no-omission) is decidable only against the
  // EXPECTED GRID, which needs the operator's SIGNED CADENCE in ℐ — §11.3's next mechanism, not asserted yet.
  if (checkpoint) {
    if (!genesis) return { complete: 'provisional', head: prevHash, reason: 'origin-unbound: no genesis, cannot bound completeness (TOP needs a HIGH origin)' };
    const cv = verify(checkpoint, { context: 'data' });
    if (cv.error || checkpoint.state.id.class !== 'attestation') return { error: 'E-PREV', detail: 'invalid checkpoint' };   // round-53 — INTEGRITY (signature), not tier: a name-form checkpoint with a valid signature is INDETERMINATE (identity), NOT an error; the stream's authority is resolved separately
    if (checkpoint.state.id.domain_shard !== authority) return { error: 'E-AUTHORITY', detail: 'checkpoint not from the stream authority (' + authority + ') — TOP completeness cannot cross authority' };
    if (boundKeys && boundKeys.get(checkpoint.state.id.key_id) !== checkpoint.sig.pub) return { error: 'E-AUTHORITY', detail: 'checkpoint key not bound to the authority key-log (impersonation, §12.2)' };
    const a = checkpoint.state.data.checkpoint?.value;
    // NOTE (honest scope) — with a genesis, `frame_count === frames.length` + `first frame.prev === genesis`
    // means this verifies the ORIGIN-TO-CHECKPOINT PREFIX (the interval's `from` MUST be the stream's first
    // frame), not an arbitrary middle `[from,to]`. A true middle-range verdict needs a PREVIOUS checkpoint and a
    // cumulative-count DELTA (currentCount − previousCount === frames.length) — a tracked follow-up, not silently
    // assumed here. The `complete`/`chain-consistent` verdict below is over this prefix.
    if (!a || a.head !== prevHash || !isSeq(a.frame_count) || a.frame_count !== String(frames.length))   // round-26 B — the signed frame_count is a CanonicalSeq; a coercible `["N"]` no longer matches via String()
      return { error: 'E-PREV', detail: 'checkpoint contradicts observed set (M5)' };
    // #69 C — no-deletion is proven. no-OMISSION needs the EXPECTED GRID: the operator's SIGNED cadence
    // (genesis value + cadence-log, RESOLVED at the interval — never a free per-checkpoint choice) AND the
    // checkpoint's interval bounds, then every slot must be a frame OR a signed gap record (§11.3 C2).
    if (a.from !== undefined && a.to !== undefined) {
      // P0 — the checkpoint's interval MUST FAITHFULLY BOUND the observed set: first==from, last==to (so
      // head==hash(frame at to)), and NO frame outside [from,to]. Else the checkpoint does not cover THIS range.
      const fromE = ustToEpoch(a.from), toE = ustToEpoch(a.to);
      if (frames[0].state.id.ust_id !== a.from) return { error: 'E-PREV', detail: 'first frame != checkpoint `from` (' + a.from + ')' };
      if (frames[frames.length - 1].state.id.ust_id !== a.to) return { error: 'E-PREV', detail: 'last frame != checkpoint `to` (' + a.to + ')' };
      for (const f of frames) { const e = ustToEpoch(f.state.id.ust_id); if (e === null || fromE === null || toE === null || e < fromE || e > toE) return { error: 'E-PREV', detail: 'frame ' + f.state.id.ust_id + ' outside the checkpoint interval [' + a.from + ',' + a.to + ']' }; }
      // continuity — resolve the cadence at BOTH ends; a change inside the interval means the grid is not uniform,
      // so the interval must be SPLIT at the boundary (old data stays complete under its own cadence). Never invalidate.
      const cF = resolveCadence(genesis, cadenceLog, a.from, { keylog }), cT = resolveCadence(genesis, cadenceLog, a.to, { keylog });
      if (cF.error) return { error: cF.error, detail: 'cadence: ' + cF.detail };
      if (cT.error) return { error: cT.error, detail: 'cadence: ' + cT.detail };
      if (cF.cadence !== cT.cadence) return { complete: 'chain-consistent', head: prevHash, interval: { from: a.from, to: a.to }, reason: 'cadence-change', detail: 'interval crosses a cadence change — split it at the boundary; each side is `complete` under its own cadence (continuity)' };
      const grid = cF.cadence > 0 ? ustGrid(a.from, a.to, cF.cadence) : null;
      if (grid) {
        const gridSet = new Set(grid);
        const covered = new Set();
        for (const f of frames) {
          const c = f.state.id.class;
          const slotBearing = c === 'observation' || c === 'derivation' || (c === 'attestation' && f.state.data?.gap !== undefined);
          if (!slotBearing) continue;
          // #75 P0-04 — grid EQUALITY, not just coverage: EVERY frame must sit ON the grid. An off-grid frame
          // (e.g. 00:15 under a 30s cadence) is an extra commitment inside a declared slot region → commitment
          // grinding. Coverage alone (every grid slot filled) let it through; equality rejects it.
          if (!gridSet.has(f.state.id.ust_id)) return { error: 'E-PREV', detail: 'off-grid frame ' + f.state.id.ust_id + ' is not a slot of the signed cadence grid (§11.3 — grid equality, no commitment grinding)' };
          covered.add(f.state.id.ust_id);
        }
        const hole = grid.find((g) => !covered.has(g));
        if (hole) return { complete: 'chain-consistent', head: prevHash, interval: { from: a.from, to: a.to }, hole, detail: 'grid slot ' + hole + ' has no frame and no signed gap — chain intact, not complete (§11.3 C)' };
        // grid EQUALITY holds: every frame ∈ grid (above) ∧ every grid slot covered (here) ∧ no dup ust_id (Y1) ⇒ bijection.
        return { complete: 'complete', head: prevHash, interval: { from: a.from, to: a.to }, cadence: String(cF.cadence), grid_slots: String(grid.length) };
      }
    }
    // windowed checkpoint but no signed cadence (grid null) OR a checkpoint with no interval bounds. When the interval
    // WAS validated (a.from/a.to present — first==from, last==to, no frame outside) return it so a no-event claim over
    // it is completeness-backed; no-deletion over [from,to] is proven even without the grid (that only adds no-omission).
    // #161 — a shortfall NAMES its missing input (F.5.1e). Two situations reach this return and they are not the
    // same shortfall: with bounds, no-deletion over [from,to] is proven and only the GRID is missing; without
    // bounds there is no interval for completeness to be measured over at all. Reported as one word until 2026-08-13.
    const bounded = a.from !== undefined && a.to !== undefined;
    return { complete: 'chain-consistent', head: prevHash, ...(bounded ? { interval: { from: a.from, to: a.to } } : {}),
      reason: bounded
        ? 'no-grid: the interval is bounded and no-deletion over it is proven, but the publisher declares no signed cadence — no-OMISSION cannot be judged without the expected grid (§11.3)'
        : 'unbounded-interval: the checkpoint declares no from/to, so there is no interval for completeness to be measured over — no-deletion between the frames held is proven, nothing about the ends' };
  }
  // no checkpoint → open tail (P5). The two ends are bounded by DIFFERENT artifacts, so the refusal says which
  // one is absent rather than reporting the same word for both (#161).
  return { complete: 'provisional', head: prevHash,
    reason: genesis
      ? 'open-tail: no checkpoint, so the observed set has no declared extent — the chain proves no-deletion BETWEEN the frames held, never that they are all of them'
      : 'unbounded: neither a genesis nor a checkpoint — the origin is unbound and the extent undeclared; the chain proves no-deletion between the frames held and nothing about either end' };
}

// §11.3 #39 — a NO-EVENT claim (a `kind:'absence'`/`reason:'no-event'` partition over a ust_id window) is only as
// strong as the stream's COMPLETENESS *and* OBSERVATIONAL coverage over that window. Verdicts:
//   'completeness-backed' — window ⊆ a `complete` interval AND every covered slot was POSITIVELY observed (no blind slot);
//   'observation-gap'     — complete + covered, but the publisher was UNREACHABLE at a covered slot, so a hidden event is
//                           NOT impossible there — the no-event breaks at that slot (self-audit rc.35 #2, agent-found);
//   'observation-unchecked' — complete + covered, but `frames` not supplied, so observational coverage cannot be checked;
//   'no-deletion-only'    — a `chain-consistent` interval: no EMITTED frame deleted, but an OMITTED slot could hide it;
//   'publisher-asserted' — no covering verified interval; 'not-applicable' — no window.
// PRECONDITION the CALLER MUST also enforce (NOT checked here): the stream OBSERVES the claim's SUBJECT — a `complete`
// stream about partition X does not, on its own, deny an event about Y (agent-found). Pass `frames` (the same array
// verifyStream verified) to check observational coverage; without it the strongest verdict is withheld.
export function noEventBacking(claimWindow, streamResult, frames) {
  { const W = admitDeep(claimWindow), S = admitDeep(streamResult), F = admitDeep(frames); if (W === ADMIT_REJECT || S === ADMIT_REJECT || F === ADMIT_REJECT) return 'not-applicable'; claimWindow = W; streamResult = S; frames = F; }   // round-28 P1-02 — admit at the door; a hostile getter → the safe floor, never a host throw
  if (!claimWindow || claimWindow.from === undefined || claimWindow.to === undefined) return 'not-applicable';
  if (streamResult?.complete !== 'chain-consistent' && streamResult?.complete !== 'complete') return 'publisher-asserted';
  const iv = streamResult.interval;   // the range verifyStream ITSELF validated (first==from,last==to,no frame outside) — not a caller checkpoint, so unspoofable
  if (!iv || iv.from === undefined || iv.to === undefined) return 'publisher-asserted';
  const w0 = ustToEpoch(claimWindow.from), w1 = ustToEpoch(claimWindow.to), i0 = ustToEpoch(iv.from), i1 = ustToEpoch(iv.to);
  if ([w0, w1, i0, i1].some((x) => x === null) || !(i0 <= w0 && i1 >= w1)) return 'publisher-asserted';   // the verified interval must CONTAIN the window
  if (streamResult.complete !== 'complete') return 'no-deletion-only';   // chain-consistent: no-deletion only, an omitted slot could still hide the event
  if (!Array.isArray(frames)) return 'observation-unchecked';            // complete, but no frames to confirm the publisher actually observed each slot
  // OBSERVATIONAL coverage + SUBJECT binding (rc.35 audit P2 — GPT + agent). Every covered slot must POSITIVELY observe
  // the claim's subject: a captured/computed partition, OR an absence that OBSERVED non-occurrence (reason `no-event` /
  // `unchanged`). ANY OTHER absence (`unreachable`, `source-timeout`, … — reason-AGNOSTIC, since the recommended set is
  // open) is BLIND — the publisher saw nothing, a hidden event is not impossible. And when `claimWindow.subject` is set,
  // the observation must be OF that subject: a `complete` stream about partition X does not, alone, deny an event about Y.
  const POSITIVE = new Set(['no-event', 'unchanged']);
  // rev86 / F.5v — COVERAGE IS NOT OBSERVATION. A grid slot is covered either by a slot-bearing frame or by a
  // signed GAP RECORD, and only the first involves looking at anything. A gap record is the publisher's own
  // signed statement that it produced NO frame for that slot — the strongest possible disclaimer of
  // observation — so it may never strengthen a negative claim. It did: `data.gap` is a `computed` partition,
  // the predicate below accepted any `computed` partition, and a window covered by gap records graded
  // `completeness-backed`. That inverts the record's purpose, since the more honestly a publisher confesses
  // its outages the stronger its "nothing happened" became.
  //
  // The fix is CLASS-FIRST rather than name-first: an attestation speaks about DOCUMENTS, an observation about
  // the WORLD, so no attestation ever contributes an observation whatever partitions it carries. And the
  // question is asked ONLY of frames that COVER a slot — the same rule verifyStream uses for completeness — so
  // a stream checkpoint or a batch commitment inside the window is neither asked nor counted.
  // FAIL-CLOSED, and the direction is the whole point. Asking "does this frame COVER a slot?" and skipping the
  // ones that do not match is fail-OPEN: a frame whose class is unrecognised falls out of the question and is
  // silently treated as harmless. The question here is "was this window safely observed?", so the default must
  // be BLIND. Therefore only a POSITIVELY identified non-covering shape is excused — an attestation that is not
  // a gap record, i.e. a stream checkpoint or a batch commitment, which cover no grid slot by construction —
  // and everything else in the window is asked and must answer with an observation. (Measured while writing
  // this: the first version excused anything it did not recognise, and the existing blind-slot check caught it.)
  const coversNoSlot = (f) => f?.state?.id?.class === 'attestation' && f?.state?.data?.gap === undefined;
  const observedSubject = (f, subj) => f?.state?.id?.class !== 'attestation'
    && Object.entries(f?.state?.data || {}).some(([name, p]) => (subj === undefined || name === subj) && p && (p.kind === 'captured' || p.kind === 'computed' || (p.kind === 'absence' && POSITIVE.has(p.value?.reason))));
  const gap = frames.some((f) => { const e = ustToEpoch(f?.state?.id?.ust_id); return e !== null && e >= w0 && e <= w1 && !coversNoSlot(f) && !observedSubject(f, claimWindow.subject); });
  return gap ? 'observation-gap' : 'completeness-backed';                // a blind slot OR one that never observed the subject breaks the no-event guarantee
}

// ─── §S6/F7 — the CONFORMANCE boundary is raw bytes. `verify(JSON.parse(x))` can't satisfy §6 because JSON.parse
//     silently collapses duplicate keys. `verifyJson` scans the raw bytes for duplicate member names BEFORE
//     constructing the object, then verifies. Untrusted transcripts from the network/storage MUST enter here.
export function verifyJson(rawBytes, opts = {}) {
  opts = admitOpts(opts); if (opts === null) return bad('E-MALFORMED', 'opts must be an inert record (round-26 C — a null/hostile opts no longer throws on `opts.maxInputBytes`)');   // round-26 (rev24 C) — totality: opts=null reached `Number(opts.maxInputBytes)` and threw
  // §13 TRANSPORT ADMISSION (rc.12) — distinct from the document verdict. Byte length is read
  // from the buffer BEFORE any decode/materialization (P0-2); an over-budget input is REFUSED as
  // INDETERMINATE('resource_limit') — verification never started, so it is never called INVALID.
  // The default input budget equals the protocol ABS; raw whitespace/base64 padding never flips a
  // verdict because the NORMATIVE size is measured on the canonical signed content inside verify.
  const isStr = typeof rawBytes === 'string';
  // round-25/49/50 P1-02 — TYPED input admission BEFORE any measurement, through the TOTAL intrinsic door ONLY. A UTF-8 string
  // OR a byte container (ArrayBuffer/TypedArray/Buffer/DataView); anything else → structured E-MALFORMED. round-50: NO `instanceof`
  // pre-check on the untrusted input (it runs a Proxy's getPrototypeOf trap → a host throw escaping verifyJson); snapshotBinary is
  // total (all instanceof inside try, intrinsic getters throw without a trap) and returns null for a Proxy / non-binary. Measure +
  // decode the immutable snapshot, never a caller-overridable property (a subclass forged byteLength:1 to bypass maxInputBytes).
  const snap = isStr ? null : snapshotBinary(rawBytes);
  if (!isStr && snap === null) return bad('E-MALFORMED', 'raw input must be a UTF-8 string or a genuine ArrayBuffer/TypedArray/Buffer/DataView (a Proxy/array-like/non-binary returns structured, never a host throw — round-50 P1-02)');
  const byteLen = isStr ? utf8Len(rawBytes) : snap.length;
  const inputBudget = admitBudget(opts.maxInputBytes, BOUNDS.sizeBytes); if (inputBudget === null) return bad('E-MALFORMED', 'maxInputBytes must be a finite positive integer of bytes (round-38 P1-03/R4 — a caller resource scalar may only TIGHTEN the 64 MiB ceiling, never expand or disable it via Infinity/NaN)');
  if (byteLen > inputBudget)
    return { result: 'INDETERMINATE', reason: 'resource_limit', detail: `raw input ${byteLen} B > input budget ${inputBudget} B — transport admission refused, verification not started` };
  // #75 P1-01 — STRICT UTF-8 on the raw path: Buffer.toString('utf8') maps invalid bytes to U+FFFD, so 0xFF and
  // the real 3-byte U+FFFD collapse to ONE string ⇒ distinct byte-strings, one verdict (breaks I4). fatal reject.
  let raw;
  if (isStr) { raw = rawBytes; if (raw.charCodeAt(0) === 0xFEFF) return bad('E-CANON', 'raw input has a leading U+FEFF BOM — rejected (round-19 P1-01)', { obligation: '§6 canonical UTF-8' }); }   // round-19 P1-01 — a pre-decoded string with a leading BOM (same domain as the byte checker's E-BOM)
  else { const a = admitUtf8(snap); if (a.err === 'BOM') return bad('E-CANON', 'raw input has a leading UTF-8 BOM (EF BB BF) — rejected, not stripped (round-19 P1-01)', { obligation: '§6 canonical UTF-8' }); if (a.err) return bad('E-CANON', 'raw input is not valid UTF-8 (invalid byte sequence)', { obligation: '§6 canonical UTF-8' }); raw = a.text; }
  const dup = scanDuplicateKeys(raw);
  if (dup) return bad('E-CANON', dup);
  let obj; try { obj = JSON.parse(raw); } catch { return bad('E-MALFORMED', 'not valid JSON'); }
  if (anyLoneSurrogate(obj)) return bad('E-CANON', 'unpaired UTF-16 surrogate in a parsed string/key — not a Unicode scalar (round-19 P1-01; §6 canonical domain == byte checker)', { obligation: '§6 canonical UTF-8' });   // round-19 P1-01 — same lone-surrogate reject as the byte checker's E-SURROGATE
  return verify(obj, opts);
}

// ─── F.5q-c — COVERAGE AND GAPS ARE MEASURABLE IN THE COMMITMENT CHAIN ALONE. F.5q fixes a closed window and
//     quantifies over the SUBSTRATES (`anchorRollup` above). This asks the other question: WHICH windows are
//     covered at all. Neither expression contains a declaration — each commitment carries `{substrate, from, to}`
//     and the sequence is prev-chained, so «no commitment for this window» and «this publisher commits DAILY and
//     the window sits inside a covered day» differ on a value the observer computes. The granularity is READ.
//
//     Deliberately NOT here: whether a commitment is OWED NOW (F.5q-d). That is present-tense, needs a clock, and
//     a clock reached through an argument is a verdict-flip waiting to happen — it stays out until it can be
//     verifier-owned. A publisher that declared no rhythm owes nothing anyway: nothing is owed where nothing was
//     promised, which is the ordinary case for an event-driven stream and not a deficiency.
export function commitmentCoverage(opts) {
  // NO DESTRUCTURING AT THE SEAM. The argument arrives from a caller that fetched a chain over the network, so
  // reading `.chain` off it is already a hostile property access — a parameter pattern would throw before the
  // first line of the body could refuse.
  let chain, window;
  try { chain = opts == null ? [] : opts.chain; } catch { chain = []; }
  try { window = opts == null ? null : opts.window; } catch { window = null; }
  let list; try { list = Array.isArray(chain) ? chain : []; } catch { list = []; }
  const windows = [];
  for (let i = 0; i < list.length; i++) {
    let a; try { const d = list[i]; a = d?.state?.data?.anchor?.value; } catch { a = undefined; }
    if (!a || typeof a.from !== 'string' || typeof a.to !== 'string') continue;   // not a commitment: skipped, and the count below says so
    windows.push({ from: a.from, to: a.to, substrate: typeof a.substrate === 'string' ? a.substrate : null });
  }
  // INTERVAL ARITHMETIC, NOT STRING ORDER — and the difference is the whole of «the granularity is READ».
  // MEASURED while writing this: comparing `from(next) > to(prev)` as strings reports a gap between EVERY pair of
  // adjacent windows, because `to` names the LAST covered slot and `from` names the next one. A `ust_id` carries
  // its own precision in its own length (§8), so each one denotes the HALF-OPEN interval [start, start + span),
  // and both questions are asked on that scale. Nothing here needs a declared rhythm; the step comes from the
  // coordinate itself.
  const span = (u) => { const m = /^ust:\d{8}\.(\d{2})(\d{2})?(\d{2})?$/.exec(u || ''); return m ? (m[3] ? 1 : m[2] ? 60 : 3600) : null; };
  const startOf = (u) => ustToEpoch(u);
  const endOf = (u) => { const s = ustToEpoch(u), n = span(u); return s === null || n === null ? null : s + n; };
  const sorted = [...windows].filter((x) => startOf(x.from) !== null && endOf(x.to) !== null)
    .sort((x, z) => startOf(x.from) - startOf(z.from));
  const gaps = [];
  for (let i = 1; i < sorted.length; i++) if (startOf(sorted[i].from) > endOf(sorted[i - 1].to)) gaps.push({ after: sorted[i - 1].to, before: sorted[i].from });
  const w = typeof window === 'string' && startOf(window) !== null ? window : null;
  const by = w === null ? null : sorted.find((x) => startOf(x.from) <= startOf(w) && endOf(w) <= endOf(x.to)) ?? null;
  // FOUR states, and the empty one is its own. A chain nobody supplied and a publisher that has committed
  // nothing produce the same emptiness, so `unknown` refuses to answer rather than reporting `uncovered` —
  // the same rule the name sweep applies to an empty target set.
  const status = windows.length === 0 ? 'unknown' : w === null ? 'read' : by ? 'covered' : 'uncovered';
  return deepFreeze({ status, covers: by ? deepFreeze({ ...by }) : null, gaps: deepFreeze(gaps), windows: deepFreeze(sorted), examined: list.length, commitments: windows.length });
}

// ─── F.5t-a — THE NAME OBLIGATION IS A PROPERTY OF A SET, and everything above this line is a function of ONE
//     document. `∀a ∈ Pub(o). name(a) ⇒ doc(a)` quantifies over what an OPERATOR publishes; `verify` never sees
//     that set, so no consumer-side observation establishes it. The predicate therefore has to be callable by the
//     producer, over its own artifacts — which is why it lives HERE and not in a gate. Until now the only
//     implementation was `tools/protocol-name-gate.mjs`, enumerating this repository: the rule bound every
//     operator and shipped its decision procedure to none of them.
//
//     Classification is deliberately NOT verification. An artifact that wears the name and IS a document but
//     fails to verify is a broken document — a correct signal, a different problem, and folding the two together
//     would rebuild the very conflation F.5t is about.
const NAME_IN_BYTES = /"protocol"\s*:\s*"UST"|"ust"\s*:\s*"\d+\.\d+"/;

export function classifyNamed(raw) {
  let doc = null, parsed = false;
  try { doc = JSON.parse(typeof raw === 'string' ? raw : new TextDecoder().decode(raw)); parsed = true; } catch { /* unparseable is a RESULT here, not an error */ }
  // A PARSED artifact is judged from its top level and from nothing else. The byte scan exists only for the
  // case where there IS no top level — an artifact that cannot be parsed, where the bytes are all that can
  // speak. Measured on a live mirror: routing parsed ARRAYS to the byte scan flagged a served key-log, whose
  // top level is a list making no claim at all, on the strength of a marker inside one of its entries. A
  // nested payload that merely mentions the name is not the one making the claim.
  if (!parsed) {
    // FAIL CLOSED: the question is "is this safe to publish under the name", so an artifact that wears it and
    // cannot be read is reported, never skipped — a consumer meeting it gets E-MALFORMED, F.5t's whole subject.
    let named; try { named = NAME_IN_BYTES.test(String(raw)); } catch { named = true; }   // a value whose String() throws is unreadable AND named-by-suspicion
    return named ? { wears: true, status: 'named_non_document', why: 'wears the name and is not parseable JSON' } : { wears: false, status: 'unnamed' };
  }
  const top = doc && typeof doc === 'object' && !Array.isArray(doc) ? doc : null;
  if (!top) return { wears: false, status: 'unnamed' };   // an array or a scalar has no top level to make the claim with
  if (!(top.protocol === 'UST' || typeof top.ust === 'string')) return { wears: false, status: 'unnamed' };
  const missing = ['ust', 'state', 'sig'].filter((k) => !(k in top));
  if (missing.length) return { wears: true, status: 'named_non_document', why: `wears the name and has no ${missing.join('/')} — a consumer applying the verifier gets E-MALFORMED, the signal of a damaged document` };
  return { wears: true, status: 'document' };
}

// The SET roll-up, in the core for the same reason the predicate is: an operator that re-implements the tally is
// one exception list away from the divergence F.5t's corollary describes. FOUR outcomes, because collapsing them
// is how a wrong path reads as a clean mirror — examining nothing is not a pass (F.5p: absence is two facts).
export function nameSetReport(entries) {
  const rows = [];
  let named = 0;
  // TOTAL AT THE WALK SEAM. The caller is a directory traversal, so every access here is hostile-reachable:
  // `Array.isArray` throws on a REVOKED proxy, `length` and `list[i]` throw on a throwing-trap array-like, and
  // `e.id`/`e.raw` are property reads on whatever the filesystem produced. Each is its own guard rather than one
  // outer try, so a single unreadable member costs that member and not the whole report — a sweep that aborts
  // on entry 3 of 900 would report a set nobody enumerated, which is the failure this export exists to prevent.
  let list; try { list = Array.isArray(entries) ? entries : []; } catch { list = []; }
  let examined; try { examined = Number(list.length) || 0; } catch { examined = 0; }
  for (let i = 0; i < examined; i++) {
    let e; try { e = list[i]; } catch { e = undefined; }
    let id; try { id = (e && typeof e === 'object' && typeof e.id === 'string') ? e.id : `#${i}`; } catch { id = `#${i}`; }
    let payload; try { payload = e && typeof e === 'object' ? e.raw : e; } catch { payload = undefined; }
    const c = classifyNamed(payload);
    if (!c.wears) continue;
    named++;
    if (c.status === 'named_non_document') rows.push({ id, why: c.why });
  }
  const outcome = examined === 0 ? 'NOTHING_EXAMINED'
    : rows.length ? 'VIOLATIONS'
      : named === 0 ? 'NONE_WEAR_THE_NAME'
        : 'COMPLIANT';
  return { outcome, examined, named, documents: named - rows.length, violations: rows };
}

// ─── DISCOVERY-DRIVEN RESOLUTION (rc.13) — the SINGLE resolver every surface (web/cli/mcp) calls, so the
// "fetch the publisher's own genesis+keylog and re-verify with the grant" flow exists in ONE place, not
// three copies that drift. A verifier that resolves BY NAME must first decide the name is safe to reach:
// the domain_shard comes from an UNTRUSTED document, so an SSRF guard runs BEFORE any network call.

// SSRF guard: a discovery target MUST be a public DNS name. Rejects IPs (v4/v6), localhost, non-public
// TLDs/suffixes, ports, userinfo, and anything that is not label.label…public-tld. This is the boundary
// between "resolve by name" and "let an attacker's document point my verifier at an internal address".
export function isPublicDnsShard(shard) {
  if (typeof shard !== 'string' || !shard || shard.length > 253) return false;
  if (/[:/@\s]/.test(shard)) return false;                                  // no port/path/userinfo/space
  if (/^\d{1,3}(\.\d{1,3}){3}$/.test(shard)) return false;                  // IPv4
  if (/^[0-9a-f]*:[0-9a-f:]*$/i.test(shard)) return false;                  // IPv6-ish
  const lower = shard.toLowerCase();
  if (lower === 'localhost' || lower.endsWith('.localhost') || lower.endsWith('.local') ||
      lower.endsWith('.internal') || lower.endsWith('.home.arpa') || lower.endsWith('.onion')) return false;
  const labels = lower.split('.');
  if (labels.length < 2) return false;                                      // must have a TLD
  if (!labels.every((l) => /^[a-z0-9](?:[a-z0-9-]{0,61}[a-z0-9])?$/.test(l))) return false;  // RFC1123 labels
  if (!/^[a-z]{2,}$/.test(labels[labels.length - 1])) return false;         // alphabetic public TLD
  return true;
}

// Resolve a document's authority by fetching its OWN §20.1 discovery pair, then re-verify with the grant.
// Returns { verdict, resolution } — verdict is what a caller should surface; resolution carries publisher /
// capacity / noFork status / source (or an error/skip). Honest by construction: without an explicit
// noForkConfirmed the name stays provisional (never silently authoritative). `offline` forbids the network.
// fetchImpl injected for tests. This function performs the ONLY network egress in the verify path.
// Witness auto-query (§12.1 M2, #68) — fetch the publisher's genesis-log and turn no-fork from a manual
// assertion into EVIDENCE. Every listed genesis's anchor is CROSS-CHECKED against its substrate (Bitcoin
// via opts.substrateVerify — the endpoint is only an index, the anchor is the independent truth). The
// honesty ladder is §12.1 exactly: one anchored active genesis (== the one we resolved) ⇒ confirmed;
// ≥2 anchored active ⇒ fork; unreachable / unanchored ⇒ pending (W1 — never a forged HIGH, never silence).
// A genesis is "anchored" iff AT LEAST ONE of its anchors verifies: inclusion (sync Merkle) AND substrate
// finality (async — Bitcoin/Rekor, so it MUST be awaited; the earlier sync path silently dropped every
// real async plugin). `anchors[]` (several substrates) is the norm; a single `anchor` is accepted too.
// round-21 — anchored iff ANY of the given (already unioned + budget-checked) proofs both includes and is substrate-final.
// The witness caller does the grouping/union/budget so this is a pure per-root check. `proofs` is the FULL evidence set
// for one content_hash (P0-01/P0-02: no first-wins, no anchor/anchors shadow, no truncation upstream).
// round-23 P1-05 — a connector must SETTLE within a bound: a never-resolving promise cannot block the verifier forever
// (I4 totality / F.9). Race each plugin call against a deadline; a timeout is that plugin's unavailability, and the timer
// is unref'd + cleared so it neither keeps the process alive nor leaks. Matches the discovery fetch AbortSignal.timeout.
const SUBSTRATE_DEADLINE_MS = 10000, WITNESS_OP_DEADLINE_MS = 30000;   // round-24 P1-04 — per-leaf AND a whole-operation budget
const withDeadline = (p, ms = SUBSTRATE_DEADLINE_MS) => { let t; const to = new Promise((_, rej) => { t = setTimeout(() => rej(new Error('substrate plugin deadline exceeded')), ms); }); return Promise.race([Promise.resolve(p).finally(() => clearTimeout(t)), to]); };
// round-24 P1-04 — a whole-operation deadline, not merely per-leaf: a legal witness (≤16 roots × ≤8 proofs) could burn
// 16×8×10s ≈ 21 min of SEQUENTIAL per-leaf timeouts. `opDeadline` bounds the whole resolution; before each connector call
// the remaining budget also caps that leaf. Exhaustion returns the string 'resource_limit' (the caller maps it to F.9).
async function anchoredByProofs(content_hash, proofs, substrateVerify, opDeadline) {
  // F.9.5-c.5 (round 201) — this loop's `false` means "no proof anchors this root". A proof whose construction this
  // build cannot compute does not support that statement: it supports no statement at all. `!incl.inclusion` is a
  // LOOSE read and a withheld answer is falsy, so without the tracking below a witness root would be reported
  // UNANCHORED because we could not read its tree — the refusal-becomes-verdict class, on the axis that decides
  // `authoritative` vs `consumer-override`. The third channel already exists here (`'resource_limit'`), so this
  // reuses it rather than inventing a second shape.
  let withheld = false;
  for (const proof of proofs) {
    if (opDeadline && witnessNow() >= opDeadline) return 'resource_limit';
    const incl = verifyAnchorCore(content_hash, proof);   // internal (witness log is JSON-parsed, getter-free); inclusion only
    if (!('inclusion' in incl)) { withheld = true; continue; }
    if (!incl.inclusion || !substrateVerify) continue;
    const leafMs = opDeadline ? Math.max(1, Math.min(SUBSTRATE_DEADLINE_MS, opDeadline - witnessNow())) : SUBSTRATE_DEADLINE_MS;
    let sub; try { sub = await withDeadline(substrateVerify(proof.anchor, proof.root, { deadline: opDeadline }), leafMs); } catch { if (opDeadline && witnessNow() >= opDeadline) return 'resource_limit'; continue; }   // round-21 P1-01 / round-23 P1-05 — a connector throw OR a hang is UNAVAILABLE evidence; round-27 P1-01 — but a timeout that EXHAUSTED the whole-op budget is resource_limit, not a mere unavailable leaf
    if (opDeadline && witnessNow() >= opDeadline) return 'resource_limit';   // round-27 P1-01 — the budget can expire DURING the await (incl. the final leaf); check after every awaited leaf, not only before
    if (substrateFinal(sub)) return true;               // round-18 P0-02 — same closed decoder as verifyAnchor (a bare truthy `final` no longer confirms the witness genesis)
  }
  // Nothing anchored it. Whether that is a FACT or an INABILITY depends on whether a proof went unread.
  return withheld ? 'unsupported_construction' : false;
}

// round-17 P1-03 — a BYTE CEILING for authority discovery (F.9/§13). A §13 key-log is ≤256 entries and genesis/witness
// are small, so a 2 MiB cap bounds every discovery body. Reject a declared oversize BEFORE accumulating; when the body
// is a real stream, read with a hard cap and abort past it (no full accumulation); for a mocked response, cap after read.
const DISCOVERY_MAX_BYTES = 1 << 21;                              // 2 MiB — a single genesis/witness doc
// §11.3 — the cadence log is bounded at 256 ENTRIES by `resolveCadenceBytes`, and an entry is one small key-form doc
// (~700 B), so 256 of them is ~180 KiB. 1 MiB is ~5× headroom and deliberately far tighter than the key-log's ceiling:
// a smaller surface for the same 256-entry bound means less to read before the bound itself rejects.
const CADENCE_MAX_BYTES = 1 << 20;
const KEYLOG_MAX_BYTES = 1 << 23;                             // 8 MiB — a ≤256-entry key-log of ordinary small key docs (round-18 P0-03: rev14's 2 MiB was below a valid K≤256 log). This is a RESOURCE BUDGET, not the K bound: a genuinely huge valid log honestly hits INDETERMINATE(resource_limit), never keylog=[].
// round-18 P1-02 — authority-discovery bytes are STRICT UTF-8: an invalid sequence is REJECTED (via the existing
// module strictUtf8 fatal decoder → null), never replacement-decoded to U+FFFD (I4/M-BYTE: an FF byte and a genuine
// U+FFFD string must not collapse to one transcript — that is a cross-implementation split at the authority byte boundary).
const strictUtf8OrThrow = (buf) => { const a = admitUtf8(buf); if (a.err === 'BOM') throw new Error('discovery body has a leading UTF-8 BOM (EF BB BF) — rejected, not stripped (round-19 P1-01; §6 canonical Unicode domain == byte checker)'); if (a.err) throw new Error('discovery body is not valid UTF-8 (invalid byte sequence) — §6 canonical UTF-8'); return a.text; };
const readBounded = async (r, cap = DISCOVERY_MAX_BYTES) => {
  const cl = Number(r.headers?.get?.('content-length'));
  if (Number.isFinite(cl) && cl > cap) throw new Error(`discovery body ${cl} B exceeds the ${cap} B ceiling (§13)`);
  if (r.body && typeof r.body.getReader === 'function') {
    const reader = r.body.getReader(); let total = 0; const chunks = [];
    for (;;) { const { done, value } = await reader.read(); if (done) break; total += value.length; if (total > cap) { try { await reader.cancel(); } catch { /* already closed */ } throw new Error(`discovery body exceeds the ${cap} B ceiling (§13)`); } chunks.push(value); }
    return strictUtf8OrThrow(concatBytes(chunks));
  }
  if (typeof r.arrayBuffer === 'function') {                      // real fetch without a stream: strict-decode the raw bytes (never Response.text()'s replacement decode)
    const buf = new Uint8Array(await r.arrayBuffer());
    if (buf.byteLength > cap) throw new Error(`discovery body ${buf.byteLength} B exceeds the ${cap} B ceiling (§13)`);
    return strictUtf8OrThrow(buf);
  }
  const body = await r.text();                                   // test-mock path: text() returns an already-decoded JS string (no raw bytes to mis-decode)
  if (utf8Len(body) > cap) throw new Error(`discovery body exceeds the ${cap} B ceiling (§13)`);
  return body;
};
// ── §12.1 supersession, the WRITE side ───────────────────────────────────────────────────────────────────────────
//
// The protocol already owns the READ side: witnessNoFork treats an entry without `superseded_by` as ACTIVE and fails
// closed when one hash is listed both active and superseded. Nothing owned the WRITE side, so the only tool that
// mints an identity rebuilt the log from the new genesis alone — a single entry, superseded_by null. Re-running a
// ceremony would therefore have DELETED the previous identity from the served log, anchors and all, which §12.1
// forbids in one sentence: "supersession is expressed by ADDING `superseded_by` and a successor entry, never by
// removal." Two roots with no relation between them is not a supersession; it is an orphaning, and a consumer
// holding the old hash has no path forward.
//
// These two functions are the rule, owned once. A publisher's mirror applies the same no-shrink test on the way in;
// a ceremony must apply it to itself on the way out, or it emits what the mirror will refuse.

/** The §12.1 monotonicity rule. Returns null when `next` legitimately extends `prior`, else the reason it does not. */
export function witnessNoShrink(prior, next) {
  // TOTAL at the door: the roster requires every export to survive an input whose every property access throws, and
  // reading `.genesis_log` off such an object is exactly the host throw that check exists to forbid. Unreadable is a
  // VERDICT here, and the safest one — a log you cannot read is a log you must never overwrite.
  try { return noShrinkInner(prior, next); } catch { return 'witness log unreadable (hostile or malformed input) — refusing'; }
}

function noShrinkInner(prior, next) {
  if (!prior || !Array.isArray(prior.genesis_log)) return 'prior log is not readable — never overwrite history you cannot read';
  if (!next || !Array.isArray(next.genesis_log)) return 'successor log is not readable';
  if (prior.domain_shard !== next.domain_shard) return `domain_shard changed ${prior.domain_shard} → ${next.domain_shard}`;
  for (const o of prior.genesis_log) {
    const n = next.genesis_log.find((e) => e.content_hash === o.content_hash);
    if (!n) return `entry ${String(o.content_hash).slice(0, 15)}… DROPPED — supersession adds, never removes`;
    const oa = o.anchors?.length ?? 0, na = n.anchors?.length ?? 0;
    if (na < oa) return `entry ${String(o.content_hash).slice(0, 15)}… lost anchors (${oa}→${na})`;
    const os = o.superseded_by ?? undefined, ns = n.superseded_by ?? undefined;   // null ≡ absent
    if (os !== undefined && ns !== os) return `entry ${String(o.content_hash).slice(0, 15)}… supersession REWRITTEN — it is set once`;
    // the SIGNED half is set once too, and may never be dropped: a log that loses it silently downgrades a proven
    // continuity to an unproven one, which is the shrink this rule exists to refuse.
    if (o.supersession !== undefined && n.supersession === undefined) return `entry ${String(o.content_hash).slice(0, 15)}… LOST its signed supersession — the proof of continuity may not be dropped`;
    if (o.supersession !== undefined && n.supersession !== undefined && canon(o.supersession) !== canon(n.supersession)) return `entry ${String(o.content_hash).slice(0, 15)}… signed supersession REWRITTEN — it is set once`;
  }
  return null;
}

/**
 * Build the successor witness log: every prior entry survives verbatim, the single ACTIVE prior entry gains
 * `superseded_by = content_hash`, and the new genesis is appended as the new active. `prior` may be null (a first
 * ceremony). Idempotent: re-running against a log whose active is already `content_hash` returns it unchanged.
 * Returns { log } or { error } — never throws, and never silently drops an entry it could not understand.
 */
export function witnessSuccessor(prior, next) {
  // TOTAL across the whole escape battery, not just the obvious shapes. Three separate failures were caught here by
  // the roster rather than by me: destructuring threw on primitives, `Array.isArray` throws on a REVOKED proxy
  // before any trap runs, and a plain object with a throwing getter escapes both. A builder that throws instead of
  // returning a verdict hands the decision to a caller who may not be looking — so the door is a try, and every
  // read of untrusted input happens behind it.
  try { return successorInner(prior, next); } catch { return { error: 'witnessSuccessor: the successor descriptor is unreadable (hostile or malformed)' }; }
}

function successorInner(prior, next) {
  if (next === null || typeof next !== 'object' || Array.isArray(next)) return { error: 'witnessSuccessor: the successor descriptor must be an object' };
  const { domain_shard, content_hash, anchors = null, supersession = null } = next;
  if (typeof domain_shard !== 'string' || !domain_shard) return { error: 'witnessSuccessor: domain_shard must be a non-empty string' };
  if (typeof content_hash !== 'string' || !/^sha256:[0-9a-f]{64}$/.test(content_hash)) return { error: 'witnessSuccessor: content_hash must be a sha256: digest' };
  const list = anchors == null ? [] : (Array.isArray(anchors) ? anchors : [anchors]);
  const entry = { content_hash, superseded_by: null, ...(list.length ? { anchors: list } : {}) };
  if (prior == null) return { log: { domain_shard, active: content_hash, genesis_log: [entry] } };
  if (typeof prior !== 'object' || !Array.isArray(prior.genesis_log)) return { error: 'prior witness log is unreadable — refusing to replace a history that cannot be read' };
  if (prior.domain_shard !== domain_shard) return { error: `prior log is for ${prior.domain_shard}, not ${domain_shard}` };
  // ALREADY ACTIVE — but that is not the same as nothing to do. Anchoring re-runs this with the SAME genesis and a
  // NEW anchor: returning the prior unchanged silently discarded exactly what the caller came to add, so a witness
  // exchange logged its entry in Rekor and the served log still read zero anchors. Idempotence must be about the
  // ENTRY, not about the call. Anchors accumulate — substrates add evidence, they never replace each other — and the
  // no-shrink rule below still holds because a union can only grow.
  if (prior.active === content_hash) {
    if (!list.length) return { log: prior, unchanged: true };
    const key = (a) => JSON.stringify(a);
    const log = {
      ...prior,
      genesis_log: prior.genesis_log.map((e) => {
        if (e.content_hash !== content_hash) return { ...e };
        const have = new Map((e.anchors ?? []).map((a) => [key(a), a]));
        for (const a of list) if (!have.has(key(a))) have.set(key(a), a);
        return { ...e, anchors: [...have.values()] };
      }),
    };
    const shrank = witnessNoShrink(prior, log);
    return shrank ? { error: `anchor merge would not survive the no-shrink rule: ${shrank}` } : { log, merged: true };
  }

  const actives = prior.genesis_log.filter((e) => (e.superseded_by ?? undefined) === undefined);
  if (actives.length === 0) return { error: 'prior log has NO active entry — it is already fully superseded, so there is nothing to supersede' };
  if (actives.length > 1) return { error: `prior log has ${actives.length} active entries — a fork, not a chain; resolve it before superseding` };
  if (prior.genesis_log.some((e) => e.content_hash === content_hash)) return { error: 'the new genesis hash is ALREADY in the log — that is a rewind, not a supersession' };

  // F.5z.5 — the log becomes the COURIER of the signed half. `supersession` is the root-signed `reroot` transcript
  // from the OUTGOING epoch's key log, placed on the entry it closes. It grants this endpoint no authority: the
  // transcript verifies against the OLD genesis's own root key, which a consumer holding that genesis already has,
  // so the log can OMIT but never FORGE — the same standing an anchor proof has here. Omission is not a hole to
  // plug: by F.5z.4 a consumer that cannot reach the proof correctly REFUSES to follow.
  const closed = supersession === undefined || supersession === null ? {} : { supersession };
  const log = {
    domain_shard,
    active: content_hash,
    genesis_log: [...prior.genesis_log.map((e) => (e === actives[0] ? { ...e, superseded_by: content_hash, ...closed } : { ...e })), entry],
  };
  const shrank = witnessNoShrink(prior, log);            // the ceremony checks ITSELF with the rule the mirror will apply
  return shrank ? { error: `successor would not survive the no-shrink rule: ${shrank}` } : { log };
}


/**
 * F.5z.5 READ side — given a genesis and the served witness log, decide whether this identity was SUPERSEDED and
 * whether the claim is PROVEN. Returns one of:
 *   { superseded: false }                              — no entry claims it
 *   { superseded: true, proven: false, detail }        — `superseded_by` present, signed half missing or bad
 *   { superseded: true, proven: true, to }             — a root-signed terminal `reroot` naming exactly that successor
 *
 * The verification is `resolveKeys` itself, not a second implementation: a `reroot` is valid iff the OLD genesis
 * plus that single entry resolve, which is already where root-signing, prev-chaining, the cycle refusal and
 * terminality live. One rule, one place — a second copy here is how the two would drift.
 *
 * `proven: false` is a REFUSAL, not a hole (F.5z.4): a consumer that cannot reach the proof does not follow, and
 * that is the correct behaviour rather than a stranding to be engineered away.
 */
export function resolveSupersession(genesis, witnessLog) {
  // TOTAL at the door, like every consumer surface here: a hostile log must yield a verdict, never a host throw.
  try {
    const g = admitDeep(genesis); if (g === ADMIT_REJECT || !g || typeof g !== 'object') return { superseded: false, detail: 'genesis is not an inert record' };
    const w = admitDeep(witnessLog); if (w === ADMIT_REJECT || !w || typeof w !== 'object' || !Array.isArray(w.genesis_log)) return { superseded: false, detail: 'witness log unreadable — nothing is claimed about this genesis' };
    const mine = contentHash(g);
    const entry = w.genesis_log.find((e) => e && e.content_hash === mine);
    if (!entry || (entry.superseded_by ?? undefined) === undefined) return { superseded: false };
    const claimed = entry.superseded_by;
    if (!isHashStr(claimed)) return { superseded: true, proven: false, detail: 'superseded_by is not a content_hash' };
    if (entry.supersession === undefined || entry.supersession === null)
      return { superseded: true, proven: false, to: claimed, detail: 'the witness log claims a supersession and carries no SIGNED half — §12.1 P2 requires both, so this claim is ignored (F.5z.4)' };
    // The carried value is the WHOLE CLOSED KEY LOG, not the terminal entry alone. MEASURED: with only the entry,
    // any identity whose log had prior entries fails `E-PREV — entry 0 prev not chained`, because the transcript
    // chains from its predecessor and not from the genesis. Carrying the log is also the stronger choice on its
    // merits: `resolveKeys` then verifies the chain, the root-signing and the TERMINALITY as well as the successor
    // claim — one rule, fully exercised — where a lifted entry would have needed a second, weaker argument about
    // what a signature proves out of position. A key log is bounded at 256 entries (§13), so the cost is small.
    const carried = Array.isArray(entry.supersession) ? entry.supersession : [entry.supersession];
    const r = resolveKeys(g, carried);
    if (r.error) return { superseded: true, proven: false, to: claimed, detail: `the signed half does not resolve against this genesis (${r.error}: ${r.detail ?? ''})` };
    if (r.supersededBy === null) return { superseded: true, proven: false, to: claimed, detail: 'the carried entry is a key-log entry but not a `reroot` — it names no successor' };
    // the corollary: the two halves must AGREE, or the log is contradictory and fails closed
    if (r.supersededBy !== claimed) return { superseded: true, proven: false, to: claimed, detail: `the signed half names ${String(r.supersededBy).slice(0, 20)}… and the index says ${String(claimed).slice(0, 20)}… — contradictory, fail closed` };
    return { superseded: true, proven: true, to: claimed };
  } catch { return { superseded: false, detail: 'supersession resolution refused a hostile input' }; }
}

export async function witnessNoFork(shard, genesisHash, opts) {
  // rev34 R1 (round-29 P1-01 / div1) — witnessNoFork is CONSUMER SURFACE: it is exported, takes an untrusted endpoint body,
  // and its verdict gates whether resolveByDiscovery mints a served-list basis. So it must be TOTAL on EVERY argument — a
  // hostile shard/genesisHash/opts yields a STRUCTURED result, never a host throw. Admit the opts at the door (admitOpts
  // preserves the fetchImpl/substrateVerify capabilities, rejects a hostile Proxy → null); a non-string shard/hash is not
  // a witnessable target.
  const o = admitOpts(opts);
  if (o === null || typeof shard !== 'string' || typeof genesisHash !== 'string')
    return { status: 'indeterminate', reason: 'unavailable', detail: 'witnessNoFork requires a string shard + genesisHash and an inert opts record (round-29 P1-01 totality)' };
  const { fetchImpl = ustFetch, substrateVerify, maxWitnessOpMs } = o;
  let log;
  try {
    const r = await fetchImpl(`https://${shard}/.well-known/ust-witness`, { signal: AbortSignal.timeout(10000), redirect: 'error' });
    if (!r.ok) throw new Error(`HTTP ${r.status}`);
    const raw = await readBounded(r);
    // round-20 P0-01 — the witness is AUTHORITY input like genesis and the key-log, so it crosses the SAME raw-byte
    // duplicate-member boundary BEFORE JSON.parse. A duplicate `genesis_log` (or member) collapses under JS last-wins
    // parsing, and a fork-bearing occurrence can be hidden behind an innocent one → false `corroborated`. Fail closed.
    if (scanDuplicateKeys(raw)) throw new Error('witness log fails the raw-byte duplicate-member check (§6) — a duplicate member can hide a fork under last-wins parsing (round-20 P0-01)');
    log = JSON.parse(raw);
  } catch (e) { return { status: 'unreachable', detail: 'witness endpoint unreachable: ' + (e && e.message || e) }; }
  if (anyLoneSurrogate(log)) return { status: 'unreachable', detail: 'witness log has an unpaired UTF-16 surrogate — outside the §6 canonical Unicode domain (round-19 P1-01)' };   // round-19 P1-01 — same Unicode domain as genesis/key-log/byte checker
  if (!log || log.domain_shard !== shard || !Array.isArray(log.genesis_log)) return { status: 'unreachable', detail: 'witness log malformed' };
  // round-21 P0-01/P0-02 — the served list is a MEASURABLE input (F.5a corroborated: "the served witness list shows
  // exactly one active anchored binding"), NOT a lossy projection of it. rev17 dedup'd first-wins and slice()'d anchors
  // — both DISCARD rival evidence and then mint HIGH, which the model forbids: "authority is denied, never forged", and a
  // resource cap must LOWER decisibility, never RAISE assurance. So: (1) GROUP entries by content_hash and UNION their
  // anchor evidence (`anchors[]` + `anchor`) — no first-wins, no anchor/anchors shadow; (2) a structural over-budget is a
  // REFUSAL (INDETERMINATE resource_limit), NEVER a truncation that could delete the one rival proof.
  const OVER = (detail) => ({ status: 'indeterminate', reason: 'resource_limit', detail });   // round-21 P2-01 — machine-readable, not 'unreachable'
  if (log.genesis_log.length > BOUNDS.witnessEntries) return OVER(`witness genesis_log has ${log.genesis_log.length} entries > ${BOUNDS.witnessEntries} (§F.9 — structural fan-out refused, never truncated; round-21 P0-02)`);
  // round-22 P0-01 — GROUP the full equivalence class by content_hash BEFORE any status projection. rev18/rev19 filtered
  // `superseded_by` FIRST, so an active record + a superseded record for the SAME rival hash let the superseded record's
  // valid anchor be dropped → false corroborated. Now: group ALL entries; record BOTH observed statuses; SET-union proofs
  // (round-22 P2-01 — dedupe byte-identical proofs by canon, so duplicate serialization can't consume the structural cap).
  const byHash = new Map();   // content_hash → { active, superseded, proofs: Map<canon, proof> }
  for (const g of log.genesis_log) {
    if (!g || typeof g !== 'object' || !/^sha256:[0-9a-f]{64}$/.test(g.content_hash || '')) continue;
    let e = byHash.get(g.content_hash);
    if (!e) { e = { active: false, superseded: false, proofs: new Map() }; byHash.set(g.content_hash, e); }
    if (g.superseded_by) e.superseded = true; else e.active = true;                                      // record BOTH — a hash seen active AND superseded is a contradiction
    for (const p of [...(Array.isArray(g.anchors) ? g.anchors : []), ...(g.anchor ? [g.anchor] : [])]) { let k; try { k = canon(p); } catch { k = JSON.stringify(p); } if (!e.proofs.has(k)) e.proofs.set(k, p); }
  }
  // round-22 P0-01 — reconcile status AFTER grouping: a content_hash listed BOTH active and superseded is a contradictory
  // witness (a rival cannot be quietly 'superseded' on one record to erase its anchor from the active count) → fail closed.
  const active = [];
  for (const [content_hash, e] of byHash) {
    if (e.active && e.superseded) return { status: 'unreachable', detail: `witness lists ${content_hash.slice(0, 20)}… as BOTH active and superseded — contradictory, fail closed (round-22 P0-01)` };
    if (e.active) active.push({ content_hash, proofs: [...e.proofs.values()] });   // purely-superseded roots are not active
  }
  if (active.length > BOUNDS.witnessActive) return OVER(`witness has ${active.length} distinct active roots > ${BOUNDS.witnessActive} (§F.9 — refused, never truncated; round-21 P0-02)`);
  for (const a of active) if (a.proofs.length > BOUNDS.anchorsPerGenesis) return OVER(`active genesis ${a.content_hash.slice(0, 20)}… carries ${a.proofs.length} UNIQUE anchors > ${BOUNDS.anchorsPerGenesis} (§F.9 — refused, never truncated; round-21 P0-02)`);
  const anchoredActive = [];
  // round-25 Div2 (F.9 — ρ_v belongs to the VERIFIER): the reference 30 s is a DEFAULT ceiling, never a universal
  // protocol constant. A consumer with a tighter deadline caps the operation → effective budget = min(reference, consumer).
  // round-27 P2-01 — typed policy admission: ONLY an ABSENT budget selects the reference default. A SUPPLIED value that
  // is not a finite positive integer (0, -1, NaN, Infinity, fractional, non-number) is a malformed policy → refuse, never
  // silently expand to the 30 s default (which let 0/-1/NaN/Infinity all admit a 20 ms connector and return 'confirmed').
  if (maxWitnessOpMs !== undefined && !(Number.isInteger(maxWitnessOpMs) && maxWitnessOpMs > 0))
    return OVER(`maxWitnessOpMs must be a finite positive integer of milliseconds (got ${typeof maxWitnessOpMs === 'number' ? maxWitnessOpMs : typeof maxWitnessOpMs}); an invalid verifier budget is refused, never expanded to the reference default (round-27 P2-01)`);
  const opBudget = maxWitnessOpMs === undefined ? WITNESS_OP_DEADLINE_MS : Math.min(WITNESS_OP_DEADLINE_MS, maxWitnessOpMs);
  const opDeadline = witnessNow() + opBudget;   // round-24 P1-04 — bound the WHOLE witness resolution, not just each leaf
  let unread = false;
  for (const a of active) {
    const r = await anchoredByProofs(a.content_hash, a.proofs, substrateVerify, opDeadline);
    if (r === 'resource_limit') return OVER(`witness anchor verification exceeded the ${opBudget} ms whole-operation budget (§F.9 — a legal fan-out cannot monopolize a verification; the verifier's ρ_v is min(reference ${WITNESS_OP_DEADLINE_MS} ms, consumer deadline); round-24 P1-04 / round-25 Div2)`);
    // F.9.5-c.5 (round 201) — a root whose proof this build could not READ is neither anchored nor unanchored, and
    // `if (r)` would have pushed the reason STRING as an anchor: a non-empty string is truthy, so withholding would
    // have minted the very confirmation it exists to prevent. Written out because the first cut of this round did
    // exactly that, one line after fixing the same shape elsewhere.
    if (r === 'unsupported_construction') { unread = true; continue; }
    if (r) anchoredActive.push({ content_hash: a.content_hash });
  }
  if (anchoredActive.length >= 2) return { status: 'fork', detail: `${anchoredActive.length} anchored active genesis roots for ${shard} — a rival name-binding root exists` };
  // A FORK is positive evidence and survives an unread sibling; NO-FORK is a claim about ABSENCE and does not. An
  // unread proof could be the rival, so `confirmed` is withheld while `fork` still stands on what was read.
  if (anchoredActive.length === 1 && !unread) {
    if (anchoredActive[0].content_hash !== genesisHash) return { status: 'fork', detail: `the anchored active genesis (${anchoredActive[0].content_hash.slice(0, 20)}…) differs from the one served at /.well-known/ust-genesis` };
    return { status: 'confirmed', detail: 'a single anchored active genesis, cross-checked against its substrate — no rival root' };
  }
  if (unread) return { status: 'pending', detail: 'a witness anchor proof declares an inclusion construction this build cannot compute — no-fork is UNPROVEN rather than disproven, because the unread root could be the rival (F.9.5-c.5)' };
  return { status: 'pending', detail: active.length ? 'genesis present in the witness log but no anchor is final yet (substrate) — no-fork not yet evidence' : 'no active genesis in the witness log' };
}

// Multi-substrate router (#68): a verifier may understand SEVERAL anchor substrates (Bitcoin-OTS, Rekor,
// …) via injected plugins. Each plugin returns null for an anchor whose `substrate` it does not handle;
// combineSubstrates tries them in order and returns the first non-null verdict. This is how a heterogeneous
// witness world stays coherent — not one substrate, but one QUESTION ("is this root committed & final?")
// answered by whichever plugin speaks that substrate. §17 registry is the shared vocabulary.
// ─── §11.3/#95 INCLUSION ROUTER — the same shape as `combineSubstrates`, deliberately not a second pattern.
// A wire cannot carry a function, so a caller composes the connectors it has INSTALLED and passes the result as
// `opts.inclusionVerify`; the selection is then by the substrate NAME already inside the proof, which means no surface
// (MCP, CLI) needs a new input to reach a foreign tree. Same isolation discipline: one plugin that throws or never
// settles must not shadow a later one that CAN verify this proof, and a plugin that declines (null) is not a refusal.
// Returning null from every plugin means NO connector claimed the proof — the caller falls back to the bundled walk.
export function combineInclusion(verifiers) {
  let list;
  try { list = (Array.isArray(verifiers) ? verifiers : [verifiers]).filter(Boolean); } catch { list = []; }
  return (contentHash, proof) => {
    for (const v of list) {
      let r; try { r = v(contentHash, proof); } catch { continue; }        // per-plugin hostility is that plugin's problem
      if (r === true || r === false) return r;                            // a CLAIM: strict Boolean only, same typed leaf as the seam
    }
    return null;                                                          // nobody claimed it → the seam applies its default
  };
}

export function combineSubstrates(verifiers) {
  // round-46 self-audit (totality) — normalize the plugin list DEFENSIVELY: a hostile `verifiers` Proxy (a throwing get/ownKeys trap)
  // makes `Array.isArray(...) ? ... : [...]` + `.filter(Boolean)` throw a HOST exception at this public door — the lone door the
  // hand-maintained totality sweep (round-17/18/19/24/38/39) never listed. Fail CLOSED to an empty list → the combinator returns a
  // function that claims no substrate → verifyAnchor reports 'unavailable' (honest floor), never a host throw. Per-plugin hostility is
  // already isolated below (the try/catch around each v()); this closes the ARRAY-normalization surface.
  let list;
  try { list = (Array.isArray(verifiers) ? verifiers : [verifiers]).filter(Boolean); } catch { list = []; }
  return async (anchor, root, ctx) => {
    // round-22 P1-02 + round-23 P1-05 — ISOLATE each plugin AND bound it: a throwing, unreachable, OR never-settling
    // connector must not shadow a later independent one that CAN verify this substrate. A rejection/timeout from one
    // plugin is that plugin's unavailability, not the seam's. round-27 P1-01 — STOP starting new plugins once the whole-op
    // deadline has passed (was: the loop kept firing plugins after the caller returned — detached work). A plugin may
    // also honor the AbortSignal to cancel its own in-flight work.
    const ac = typeof AbortController === 'function' ? new AbortController() : null;
    for (const v of list) {
      if (ctx?.deadline && witnessNow() >= ctx.deadline) { ac?.abort(); return null; }   // budget spent → no new plugin, cancel any signal-honoring work (rev36 — the deadline is on the witnessNow() monotonic scale, NOT Date.now())
      let r; try { r = await withDeadline(v(anchor, root, { deadline: ctx?.deadline, signal: ac?.signal })); } catch { continue; }
      if (r != null) return r;
    }
    return null;   // no plugin claimed this substrate → verifyAnchor reports 'unavailable' (honest, not INVALID)
  };
}

export async function resolveByDiscovery(doc, opts = {}, transport = {}) {
  opts = admitOpts(opts); const T = admitOpts(transport);   // round-19 P1-02 — inert snapshots; a throwing accessor/Proxy trap on opts OR transport → null → structured reject (not a host throw)
  if (opts === null || T === null) return { verdict: { result: 'INVALID', error: 'E-MALFORMED', detail: 'opts and transport must be inert records (round-19 P1-02 totality)' }, resolution: null };
  const D = admitDeep(doc);   // round-27 (3, self-audit) — admit the doc ONCE at THIS door: the discovery `shard` (the fetch URL) and the verify verdict must read the SAME bytes, else a getter fetches one domain's genesis/witness while the verdict is over another.
  if (D === ADMIT_REJECT) return { verdict: { result: 'INVALID', error: 'E-MALFORMED', detail: 'document is not an inert record (round-27: the ONE input boundary)' }, resolution: null };
  doc = D;
  const { fetchImpl = ustFetch, substrateVerify } = T;
  // #144 one level up: discovery was gated on the SYNCHRONOUS verdict, which in a browser is
  // INDETERMINATE(unsupported_alg) - so `worth` was false, discovery never ran, and the page received the
  // refusal instead of a resolution. Measured in the real page 2026-08-15; the async door carries the faculty.
  const base = await verifyAsync(doc, opts);
  const shard = doc?.state?.id?.domain_shard || '';
  const worth = !opts.offline && !opts.genesis &&
    (base.result === 'VALID:LIGHT' || (base.result === 'INDETERMINATE' && base.reason === 'unavailable'));
  if (!worth) return { verdict: base, resolution: null };
  if (!isPublicDnsShard(shard)) return { verdict: base, resolution: { skipped: 'domain_shard is not a public DNS name — discovery refused (SSRF guard)' } };
  let genesis, keylog = [], cadenceLog = [], genesisHash, gRaw, kRaw, cRaw, cadenceUnknown;
  try {
    const get = async (p, cap) => { const r = await fetchImpl(`https://${shard}${p}`, { signal: AbortSignal.timeout(10000), redirect: 'error' }); if (!r.ok) { const e = new Error(`HTTP ${r.status} at ${p}`); e.httpStatus = r.status; throw e; } return readBounded(r, cap); };   // round-17 P1-03 — bounded read (byte ceiling before accumulate/scan/parse)
    gRaw = await get('/.well-known/ust-genesis');
    try { cRaw = await get('/.well-known/ust-cadence', CADENCE_MAX_BYTES); }
    catch (e) {
      // §11.3 — ABSENT (404/410) means the publisher declares no change and the genesis value stands: the honest common
      // case, since a publisher with one fixed cadence never writes a log, and an event-driven publisher declares none at
      // all. PRESENT-but-UNREADABLE must NEVER collapse to `[]`, because that silently ERASES a cadence change and the
      // range is then judged against the OLD grid — a finer new cadence reads as holes, a coarser one reads as `complete`
      // while frames are missing: a completeness verdict manufactured by a transport failure.
      // round-233 (#169) — but that cost is SCOPED to the range. `c_n(t)` is an input to `verifyStream` and to NO
      // single-document verdict (`ustGrid` has exactly one call site, inside verifyStream; `resolveAuthority` takes no
      // cadence), so an unknown cadence says nothing about the identity carrier. Returning early here withheld an EARNED
      // authority verdict — measured live: the same document read VALID:HIGH in Node and INDETERMINATE in a browser,
      // where a CORS-blocked 404 arrives with no status to read. NOT symmetric with the key-log branch below, which is
      // correct precisely because a hidden retirement lands on identity itself.
      if (e.httpStatus !== 404 && e.httpStatus !== 410)
        cadenceUnknown = { reason: /ceiling|§13/.test(e.message || '') ? 'resource_limit' : 'unavailable', detail: 'cadence-log present but unreadable: ' + (e && e.message || e) };
    }
    try { kRaw = await get('/.well-known/ust-keylog', KEYLOG_MAX_BYTES); }
    catch (e) {
      // round-18 P0-03 — an ABSENT key-log (404/410) is genuinely not served (fail-safe: only genesis-key docs stay
      // authoritative). A PRESENT-but-UNREADABLE one (oversize / transport error) must NEVER become keylog=[] — that
      // erases a real retirement and false-accepts a post-retirement doc. It is INDETERMINATE(resource_limit), F.9.
      if (e.httpStatus !== 404 && e.httpStatus !== 410)
        return { verdict: base, resolution: { status: 'INDETERMINATE', reason: /ceiling|§13/.test(e.message || '') ? 'resource_limit' : 'unavailable', error: 'key-log present but unreadable (' + (e.message || e) + ') — authority NOT computed (round-18 P0-03; never substituted with an empty log)' } };
    }
  } catch (e) { return { verdict: base, resolution: { error: 'discovery fetch failed: ' + (e && e.message || e) } }; }
  // #69 Theme D — genesis AND key-log are AUTHORITY input; both MUST cross the SAME raw-byte boundary as any
  // untrusted transcript (I4). JSON.parse silently collapses duplicate members, so the genesis goes through
  // verifyJson and the key-log's raw bytes go through the SAME scanner (scanDuplicateKeys, which descends into
  // every entry) BEFORE parse. A dup-key authority surface is E-CANON — never a silent downgrade to LIGHT.
  // round-239 (UST-mbso) — ONE memo for the whole resolution: genesis, cadence and authority ask about overlapping
  // signatures, and a signature resolved for one is the same fact for the next. Threading it also bounds the cost to
  // the number of DISTINCT signatures the resolution touches, not to the number of pure calls it makes.
  const sigMemo = new Map();
  const gv = await withSignatureFaculty(() => verifyJson(gRaw, {}), cannotDecide, sigMemo);
  if (cannotDecide(gv)) return { verdict: base, resolution: { error: 'E-UNSUPPORTED: published genesis not decidable by this build — ' + gv.detail } };   // #144 — reached now only when the async faculty is absent TOO
  if (!isValid(gv)) return { verdict: base, resolution: { error: 'published genesis does not VERIFY: ' + (gv.error || gv.result) } };
  genesis = JSON.parse(gRaw); genesisHash = contentHash(genesis);
  if (kRaw !== undefined) {
    const kdup = scanDuplicateKeys(kRaw);
    if (kdup) return { verdict: base, resolution: { error: 'E-CANON: published key-log fails the raw-byte check (' + kdup + ')' } };
    let k; try { k = JSON.parse(kRaw); } catch { return { verdict: base, resolution: { error: 'E-MALFORMED: published key-log is not valid JSON' } }; }
    if (anyLoneSurrogate(k)) return { verdict: base, resolution: { error: 'E-CANON: published key-log has an unpaired UTF-16 surrogate — outside the §6 canonical Unicode domain (round-19 P1-01)' } };   // round-19 P1-01 — same Unicode domain as the byte checker + genesis
    if (!Array.isArray(k)) return { verdict: base, resolution: { error: 'E-MALFORMED: published key-log is not a JSON array' } };
    keylog = k;
  }
  // Same four checks in the same order, because the cadence log crosses the same untrusted-byte boundary (I4) and moves
  // the grid: dup-key scan on the RAW bytes (JSON.parse collapses duplicates silently), then parse, then the §6 Unicode
  // domain, then the shape. A broken authority surface is reported, never downgraded to "no cadence declared".
  if (cRaw !== undefined) {
    const cdup = scanDuplicateKeys(cRaw);
    if (cdup) return { verdict: base, resolution: { error: 'E-CANON: published cadence-log fails the raw-byte check (' + cdup + ')' } };
    let cl; try { cl = JSON.parse(cRaw); } catch { return { verdict: base, resolution: { error: 'E-MALFORMED: published cadence-log is not valid JSON' } }; }
    if (anyLoneSurrogate(cl)) return { verdict: base, resolution: { error: 'E-CANON: published cadence-log has an unpaired UTF-16 surrogate — outside the §6 canonical Unicode domain' } };
    if (!Array.isArray(cl)) return { verdict: base, resolution: { error: 'E-MALFORMED: published cadence-log is not a JSON array' } };
    cadenceLog = cl;
  }

  // no-fork EVIDENCE (default): query the witness UNLESS the caller air-gap-asserts it or forbids the network.
  // #69 B / F.5a — the publisher's OWN served witness list is CORROBORATION, not independent non-membership: a
  // confirmed served list ⇒ `corroborated` (HIGH, honest), NOT `authoritative`. Only a caller air-gap assertion
  // (out-of-band responsibility) or a future anchored map-inclusion reaches `authoritative`. A fork ⇒ E-GENESIS.
  // round-237 (#171) — the probe is suppressed by `offline` ALONE. It used to be skipped whenever the caller had
  // spoken (`noForkEvidence !== undefined || noForkConfirmed`), which confused two different relations to `ℐ`:
  // adjoining an axiom enlarges the CONSUMER's view and says nothing about `σ(observables)`, while declining to look
  // SHRINKS it. Measured by counting fetched surfaces: with no assertion discovery read genesis/cadence/keylog/WITNESS
  // and resolved `corroborated`; with `noForkConfirmed: true` it read the first three and resolved `consumer-override`,
  // so adjoining a TRUE axiom LOWERED the tier and `resolveAuthority`'s served-before-override ordering never applied,
  // because the observation it ranks was never made. Worse than the tier: `w.status === 'fork' ⇒ E-GENESIS` lives on
  // this branch and the served list is its only source, so asserting *no fork* switched off the search for a rival
  // genesis — the exact condition the coordinate exists to detect. What a caller believes changes what is CONCLUDED,
  // never what is looked at. The served genesis-log still only ever CORROBORATES (never independent).
  let witnessConfirmed = false, noFork = 'unconfirmed', witnessReason;
  const callerNoFork = opts.noForkEvidence !== undefined || opts.noForkConfirmed;   // still decides how the RESULT is labelled below, no longer whether it is obtained
  if (!opts.offline) {
    const w = await witnessNoFork(shard, genesisHash, { fetchImpl, substrateVerify, maxWitnessOpMs: opts.maxWitnessOpMs });   // round-26 P1-03 (rev27 E) — thread the verifier's ρ_v.time policy through the PUBLIC entry; the leaf-only budget was unreachable (F.9); round-31 — also the injectable budget clock (resource-limit only)
    if (w.status === 'fork') return { verdict: bad('E-GENESIS', w.detail), resolution: { publisher: shard, fork: true, detail: w.detail } };
    witnessConfirmed = w.status === 'confirmed';
    if (w.reason) witnessReason = w.reason;   // round-21 P2-01 — preserve the machine-readable witness reason (resource_limit) through the resolution, not just a 'HIGH pending' string
    noFork = witnessConfirmed ? 'served-list (corroborated)' : 'HIGH pending — ' + w.detail;
  }
  // VERIFIED served-list evidence (bound to THIS genesis) — the ONLY way the stateless core earns `corroborated`; a
  // bare boolean would be a mere assertion (self-audit rc.35). Present only when witnessNoFork actually confirmed.
  let servedNoFork; if (witnessConfirmed) { servedNoFork = { confirmed: true, active_genesis: genesisHash }; VERIFIED_SERVED.add(servedNoFork); }   // P0-1: mint the trusted token ONLY after witnessNoFork confirmed
  // round-16 P0-02 — key-log freshness is EARNED here: we JUST fetched /.well-known/ust-keylog for THIS domain+genesis,
  // so mint an unforgeable observation token (observed_at = the fetch instant). A raw caller string can never do this.
  let keylogFreshAsOf; if (kRaw !== undefined) { keylogFreshAsOf = { observed_at: new Date().toISOString().slice(0, 19) + 'Z', domain: shard, active_genesis: genesisHash }; VERIFIED_FRESH.add(keylogFreshAsOf); }
  // Resolve the grid the publisher has SIGNED for this document's own moment, and hand the log back. Without this the
  // fetch buys the consumer nothing: a completeness claim is checked by `verifyStream`, which needs the log, and making
  // it fetch the same bytes again is both a wasted round-trip and a second chance to read a DIFFERENT log. A broken log
  // is an error here, not a silent `null` — "no cadence declared" and "your cadence log does not verify" are different
  // facts, and only the first one is benign.
  // round-233 — with the coordinate UNKNOWN there is nothing to resolve: `resolveCadence(genesis, [], …)` would hand back
  // the GENESIS value as though it were in force, which is precisely the substitution the F.4 closure forbids. Not
  // resolving is the whole point; the unknown is reported instead, below.
  const cadRes = cadenceUnknown ? null : await withSignatureFaculty(() => resolveCadence(genesis, cadenceLog, doc?.state?.id?.ust_id, { keylog }), resolverCannotDecide, sigMemo);
  if (cadRes?.error) return { verdict: base, resolution: { error: cadRes.error + ': published cadence-log does not resolve — ' + (cadRes.detail || '') } };
  const authOpts = { genesis, keylog, noForkConfirmed: opts.noForkConfirmed, noForkEvidence: opts.noForkEvidence, trustRoots: opts.trustRoots, servedNoFork, keylogFreshAsOf };
  const auth = await withSignatureFaculty(() => resolveAuthority(doc, authOpts), resolverCannotDecide, sigMemo);
  if (auth.error) return { verdict: base, resolution: { error: auth.error + (auth.detail ? ' — ' + auth.detail : '') } };
  if (callerNoFork) noFork = auth.independently_verified ? 'accepted-external-witness (authoritative)' : 'caller-asserted (consumer-override)';
  const verdict = await verifyAsync(doc, { ...opts, genesis, keylog, noForkConfirmed: opts.noForkConfirmed, servedNoFork, keylogFreshAsOf, capacity: auth.capacity, substrateVerify });   // #69 E1 — await the doc's own anchor substrate (TOP); carry the EARNED freshness token (round-16 P0-02) into the final verdict
  return { verdict, resolution: { publisher: auth.publisher ?? shard, strength: auth.strength, capacity: auth.capacity, noFork, ...(witnessReason ? { witness_reason: witnessReason } : {}),
    // The cadence coordinate is THREE-valued and each state is reported POSITIVELY, never inferred from a missing field.
    // `cadence: "<n>"` — resolved. `cadence: null` — this publisher DECLARES no grid, so it can never earn more than
    // `chain-consistent` (no-deletion) and can never claim no-omission. `cadence_unknown` (round-233) — the surface exists
    // and could not be read, so the range ceiling is the same `chain-consistent` while the REASON differs: the first is
    // the publisher's standing declaration, the third is this verifier's transport, transient and not the publisher's
    // fault. Collapsing the two would report a permanent property where there is a retry.
    ...(cadenceUnknown ? { cadence_unknown: cadenceUnknown } : { cadence: cadRes.cadence === null ? null : String(cadRes.cadence) }),
    ...(cadenceLog.length ? { cadence_log: cadenceLog } : {}),
    source: `https://${shard}/.well-known/ (§20.1 discovery + §12.1a witness)` } };
}

// minimal duplicate-key-detecting JSON scanner (zero-dep). Returns an error string or null. Keys are parsed
// (via JSON.parse of the token) so `a` and `a` collide as the SAME member name.
function scanDuplicateKeys(s) {
  let i = 0; const n = s.length;
  const ws = () => { while (i < n && ' \t\n\r'.includes(s[i])) i++; };
  const str = () => { const a = i; i++; while (i < n) { if (s[i] === '\\') i += 2; else if (s[i] === '"') { i++; return JSON.parse(s.slice(a, i)); } else i++; } throw 'unterminated string'; };
  function value() {
    ws(); const c = s[i];
    if (c === '{') {
      i++; const keys = new Set(); ws();
      if (s[i] === '}') { i++; return; }
      for (;;) { ws(); if (s[i] !== '"') throw 'expected key'; const k = str(); if (keys.has(k)) throw 'duplicate member name: ' + k; keys.add(k); ws(); if (s[i] !== ':') throw 'expected colon'; i++; value(); ws(); if (s[i] === ',') { i++; continue; } if (s[i] === '}') { i++; return; } throw 'bad object'; }
    } else if (c === '[') {
      i++; ws(); if (s[i] === ']') { i++; return; }
      for (;;) { value(); ws(); if (s[i] === ',') { i++; continue; } if (s[i] === ']') { i++; return; } throw 'bad array'; }
    } else if (c === '"') { str(); }
    else { while (i < n && !',}] \t\n\r'.includes(s[i])) i++; }        // number / true / false / null
  }
  try { value(); ws(); return i >= n ? null : 'trailing bytes'; } catch (e) { return typeof e === 'string' ? e : 'malformed JSON'; }
}

function err(code, detail) { const e = new Error(code); e.code = code; e.detail = detail; return e; }
// §14 INVALID constructor. `fields` (optional) carries MACHINE-STRUCTURED context so a program branches WITHOUT
// parsing `detail` (audit #44 §2 — verification fatigue): `obligation` names the exact broken spec rule, and a
// recompute mismatch adds `expected`/`actual` (and `partition` where it applies). `error`+`detail` stay for humans.
// §3.1/F.5.0 — a refused DOCUMENT has assurance ⊥, and the projection is total over it: Π(⊥) = NONE. So an INVALID
// document verdict carries `tier: 'NONE'` — the tier of "there was nothing to rank", never of "measured and failed".
// INDETERMINATE deliberately gets NOTHING: its assurance is PARTIAL, not ⊥ (integrity passed, a dependency was
// unreachable), so NONE there would UNDER-claim as badly as an over-claim and would blur the one line the protocol
// keeps sharp — cannot decide is not earned nothing.
// Stamped at the document DOORS, not inside bad(): bad() also serves verifyEvidenceReceipt, whose subject is a receipt
// and has no document assurance state to rank.
const floorTier = (v) => (v && v.result === 'INVALID' && v.tier === undefined ? { ...v, tier: 'NONE' } : v);
// §6 identifier rule / model F.7a.2 identifier-as-structure corollary. A verdict is READ — by a person on a
// terminal, an agent parsing a report, a page composing a line — and its `detail` quotes strings taken from the
// document at 37 call sites. Measured 2026-09-01 (#177): a signed document whose `enc.key_id` carried a newline
// made the reference CLI print a fabricated `tier : [TOP] anchored in Bitcoin` line above the real `[LIGHT]` one,
// out of its own report. CLOSED 2026-09-01 here rather than at those 37 sites: editing each would leave the
// thirty-eighth exposed the day it is written, and the quotation is the VERIFIER's act, so the escaping is the
// verifier's duty. A rejected value never passed admission and cannot be constrained — only quoted safely.
// TWO constants, and the split is load-bearing: a `/g` regex carries `lastIndex` ACROSS calls, so `.test()` on the
// same string returns true, true, FALSE. A security predicate that answers differently on its third invocation is
// worse than none — it passes exactly when it has been used most. The tester has no `g`; only the replacer does.
const FORGES_STRUCTURE = /[\u0000-\u001f\u007f-\u009f\u2028\u2029\u202a-\u202e\u2066-\u2069]/;
const FORGES_STRUCTURE_G = new RegExp(FORGES_STRUCTURE.source, 'g');
const quoteSafe = (s) => (typeof s === 'string' ? s.replace(FORGES_STRUCTURE_G, (c) => '\\u' + c.charCodeAt(0).toString(16).padStart(4, '0')) : s);
function bad(code, detail, fields) { return { result: 'INVALID', error: code, detail: quoteSafe(detail), ...(fields || null) }; }
// F.5.1d — a SUPPLY remedy is a PROMISE: provide `input` and the coordinate rises. A call that already HOLDS it
// has produced the verdict it yields, so the clause is refuted by the very call that printed it. The helper is
// given `opts` rather than trusting a branch condition, because a literal obeys the invariant only when the guard
// happens to imply the absence its sentence asserts — correctness by coincidence, drifting silently. A branch that
// does not imply it degrades to SILENCE here rather than to a false promise; `explainLadder` is the one place that
// computes what a call did not supply, and F.5.1b forbids a second one.
// REPLACE clauses (re-fetch a stale log, rotate, re-anchor) are NOT this: the slot is filled and the ARTEFACT is
// not, so their precondition is a value the branch already computed. They do not pass through here.
// Hostile `opts` (P1-02 — a throwing accessor) resolves to silence: on the "is it safe to print" question the
// direction is closed, and an unprintable remedy costs a reader nothing a false one does not cost more.
// One decision point for "did this call hand in `input`", so a branch and a sentence cannot disagree about it.
function suppliedInput(opts, input) {
  try { return opts != null && opts[input] != null; } catch { return true; }   // unreadable ⇒ treat as supplied ⇒ silence
}
function supplyRemedy(opts, input, clause) { return suppliedInput(opts, input) ? '' : clause; }
