From 809dfbfe4868478bf62be40e5f9db35d1b52f6be Mon Sep 17 00:00:00 2001 From: morluto <76467478+morluto@users.noreply.github.com> Date: Sat, 25 Jul 2026 09:11:07 +0000 Subject: [PATCH] perf(evidence): validate ledger mutations incrementally --- src/application/EvidenceLedger.ts | 79 ++++++++++++++++++++++++------- src/domain/evidenceBundle.ts | 74 ++++++++++++++++++++++++++++- 2 files changed, 134 insertions(+), 19 deletions(-) diff --git a/src/application/EvidenceLedger.ts b/src/application/EvidenceLedger.ts index 8248920a5..92981cde4 100644 --- a/src/application/EvidenceLedger.ts +++ b/src/application/EvidenceLedger.ts @@ -3,6 +3,7 @@ import canonicalize from "canonicalize"; import { createEvidenceBundle, parseEvidenceBundle, + validateResidualUnknownAddition, type EvidenceBundle, } from "../domain/evidenceBundle.js"; import { @@ -216,8 +217,6 @@ export class EvidenceLedger { } catch (cause: unknown) { return err(new UnknownRegistryError("invalid-transition", { cause })); } - const pendingRecords = new Map(this.#records); - pendingRecords.set(evidence.evidence_id, evidence); const existingUnknown = this.#unknownHeads.get(unknown.unknown_id); if (existingUnknown !== undefined) { if ( @@ -225,20 +224,12 @@ export class EvidenceLedger { existingUnknown.revision_digest !== unknown.revision_digest ) return err(new UnknownRegistryError("already-exists")); - const checked = this.#validateCandidate( - pendingRecords, - this.#unknownRevisions, - ); + const checked = this.#appendIncremental([evidence]); if (!checked.ok) return checked; - this.#commit(pendingRecords, this.#unknownRevisions, checked.value); return ok(null); } - pendingRecords.set(mutation.evidence_id, mutation); - const pendingUnknowns = new Map(this.#unknownRevisions); - pendingUnknowns.set(unknownRevisionKey(unknown), unknown); - const checked = this.#validateCandidate(pendingRecords, pendingUnknowns); + const checked = this.#appendIncremental([evidence, mutation], unknown); if (!checked.ok) return checked; - this.#commit(pendingRecords, pendingUnknowns, checked.value); return ok(structuredClone(unknown)); } @@ -332,16 +323,68 @@ export class EvidenceLedger { }), ); } - const pendingRecords = new Map(this.#records); - pendingRecords.set(mutationEvidence.evidence_id, mutationEvidence); - const pendingUnknowns = new Map(this.#unknownRevisions); - pendingUnknowns.set(unknownRevisionKey(unknown), unknown); - const checked = this.#validateCandidate(pendingRecords, pendingUnknowns); + const checked = this.#appendIncremental([mutationEvidence], unknown); if (!checked.ok) return checked; - this.#commit(pendingRecords, pendingUnknowns, checked.value); return ok(structuredClone(unknown)); } + #appendIncremental( + records: readonly Evidence[], + unknown?: ResidualUnknown, + ): Result< + void, + EvidenceIntegrityError | EvidenceLimitError | UnknownRegistryError + > { + const additions = new Map(); + let addedBytes = 0; + for (const evidence of records) { + const existing = + additions.get(evidence.evidence_id) ?? + this.#records.get(evidence.evidence_id); + if (existing !== undefined) { + if (!recordsAgree(existing, evidence)) + return err(new EvidenceIntegrityError("Conflicting evidence record")); + continue; + } + additions.set(evidence.evidence_id, evidence); + addedBytes += serializedBytes(evidence); + } + const unknownKey = + unknown === undefined ? undefined : unknownRevisionKey(unknown); + if (unknownKey !== undefined && this.#unknownRevisions.has(unknownKey)) + return err(new UnknownRegistryError("integrity")); + const unknownBytes = unknown === undefined ? 0 : serializedBytes(unknown); + if ( + this.#exceedsRecordLimit( + this.#records.size + additions.size, + this.#unknownRevisions.size + (unknown === undefined ? 0 : 1), + ) + ) + return err(new EvidenceLimitError("records", this.limits.maxRecords)); + if (this.#bytes + addedBytes + unknownBytes > this.limits.maxBytes) + return err(new EvidenceLimitError("bytes", this.limits.maxBytes)); + if (unknown !== undefined) + try { + validateResidualUnknownAddition( + unknown, + { + get: (id) => additions.get(id) ?? this.#records.get(id), + has: (id) => additions.has(id) || this.#records.has(id), + }, + this.#unknownHeads, + ); + } catch (cause: unknown) { + return err(new UnknownRegistryError("integrity", { cause })); + } + for (const [id, evidence] of additions) this.#records.set(id, evidence); + if (unknown !== undefined && unknownKey !== undefined) { + this.#unknownRevisions.set(unknownKey, unknown); + this.#unknownHeads.set(unknown.unknown_id, unknown); + } + this.#bytes += addedBytes + unknownBytes; + return ok(undefined); + } + #validateCandidate( records: ReadonlyMap, unknowns: ReadonlyMap, diff --git a/src/domain/evidenceBundle.ts b/src/domain/evidenceBundle.ts index 0048d53eb..e50505328 100644 --- a/src/domain/evidenceBundle.ts +++ b/src/domain/evidenceBundle.ts @@ -239,7 +239,7 @@ const validateUnknownGraph = ( const validateVerifiedResolution = ( unknown: ResidualUnknown, - evidenceById: ReadonlyMap, + evidenceById: Pick, "get">, ): void => { if (unknown.contradicting_evidence_ids.length > 0) throw new TypeError( @@ -260,6 +260,78 @@ const validateVerifiedResolution = ( ); }; +/** + * Validate one new residual-unknown revision against an already valid ledger. + * This keeps ordinary mutations proportional to the affected revision while + * full bundle imports continue to validate the complete graph. + */ +export const validateResidualUnknownAddition = ( + unknown: ResidualUnknown, + evidenceById: Pick, "get" | "has">, + currentHeads: ReadonlyMap, +): void => { + const current = currentHeads.get(unknown.unknown_id); + if ( + current === undefined + ? unknown.revision !== 1 || unknown.previous_revision_digest !== null + : unknown.revision !== current.revision + 1 || + unknown.previous_revision_digest !== current.revision_digest + ) + throw new TypeError( + `Residual unknown ${unknown.unknown_id} revision chain is broken`, + ); + for (const evidenceId of referencedEvidenceIds(unknown)) + if (!evidenceById.has(evidenceId)) + throw new TypeError( + `Residual unknown references missing evidence ${evidenceId}`, + ); + for (const relationship of unknown.relationships) + if ( + relationship.unknown_id !== unknown.unknown_id && + !currentHeads.has(relationship.unknown_id) + ) + throw new TypeError( + `Residual unknown references missing unknown ${relationship.unknown_id}`, + ); + if (unknown.resolution?.disposition === "verified") + validateVerifiedResolution(unknown, evidenceById); + for (const relationship of unknown.relationships) + if ( + relationship.type === "depends-on" && + reachesUnknown( + relationship.unknown_id, + unknown.unknown_id, + currentHeads, + unknown, + ) + ) + throw new TypeError("Residual unknown dependency graph contains a cycle"); +}; + +const reachesUnknown = ( + start: string, + goal: string, + currentHeads: ReadonlyMap, + candidate: ResidualUnknown, +): boolean => { + const pending = [start]; + const visited = new Set(); + while (pending.length > 0) { + const id = pending.pop(); + if (id === undefined) break; + if (id === goal) return true; + if (visited.has(id)) continue; + visited.add(id); + const unknown = + id === candidate.unknown_id ? candidate : currentHeads.get(id); + if (unknown === undefined) continue; + for (const relationship of unknown.relationships) + if (relationship.type === "depends-on") + pending.push(relationship.unknown_id); + } + return false; +}; + const evidenceQualifies = ( unknown: ResidualUnknown, evidence: Evidence,