From 834c70cf4b9c8dca000c7a20491215d1acc0db89 Mon Sep 17 00:00:00 2001 From: N0zoM1z0 <1705004398@qq.com> Date: Tue, 14 Jul 2026 20:37:10 +0000 Subject: [PATCH] fix(evidence): enforce the combined record limit --- src/application/EvidenceLedger.ts | 13 ++++++++-- tests/evidence.test.ts | 41 +++++++++++++++++++++++++++++++ 2 files changed, 52 insertions(+), 2 deletions(-) diff --git a/src/application/EvidenceLedger.ts b/src/application/EvidenceLedger.ts index 367b64b77..8248920a5 100644 --- a/src/application/EvidenceLedger.ts +++ b/src/application/EvidenceLedger.ts @@ -62,7 +62,12 @@ export class EvidenceLedger { ? ok("duplicate") : err(new EvidenceIntegrityError("Conflicting evidence record")); } - if (this.#records.size >= this.limits.maxRecords) + if ( + this.#exceedsRecordLimit( + this.#records.size + 1, + this.#unknownRevisions.size, + ) + ) return err(new EvidenceLimitError("records", this.limits.maxRecords)); const bytes = serializedBytes(evidence); if (this.#bytes + bytes > this.limits.maxBytes) @@ -344,7 +349,7 @@ export class EvidenceLedger { number, EvidenceIntegrityError | EvidenceLimitError | UnknownRegistryError > { - if (records.size + unknowns.size > this.limits.maxRecords) + if (this.#exceedsRecordLimit(records.size, unknowns.size)) return err(new EvidenceLimitError("records", this.limits.maxRecords)); const bytes = [...records.values()].reduce( @@ -367,6 +372,10 @@ export class EvidenceLedger { return ok(bytes); } + #exceedsRecordLimit(recordCount: number, unknownCount: number): boolean { + return recordCount + unknownCount > this.limits.maxRecords; + } + #commit( records: ReadonlyMap, unknowns: ReadonlyMap, diff --git a/tests/evidence.test.ts b/tests/evidence.test.ts index fdfd8f7a2..da517b4dc 100644 --- a/tests/evidence.test.ts +++ b/tests/evidence.test.ts @@ -204,6 +204,47 @@ describe("analysis evidence", () => { expect(byteBound.export().records).toEqual([]); }); + it("counts unknown revisions when enforcing direct record limits", () => { + const ledger = new EvidenceLedger({ + maxRecords: 2, + maxBytes: 1_000_000, + }); + const mutation = createEvidence(undefined, PROVIDER, { + predicateType: "rea.residual-unknown-mutation/v1", + operation: "record_unknown", + parameters: {}, + result: { action: "record" }, + }); + const unknown = recordUnknownInputSchema.parse({ + approved: true, + question: "What remains unresolved?", + severity: "high", + domain: "record-limit-test", + supporting_evidence_ids: [], + contradicting_evidence_ids: [], + required_authority: "shipped-artifact", + required_confidence: "observed", + required_environment: null, + recommended_probes: [], + relationships: [], + }); + expect(ledger.recordUnknown(unknown, mutation).ok).toBe(true); + + const direct = createEvidence(TARGET, PROVIDER, { + operation: "health", + parameters: {}, + result: true, + }); + expect(ledger.record(direct)).toMatchObject({ + ok: false, + error: { _tag: "EvidenceLimitError", limit: "records", maximum: 2 }, + }); + expect(ledger.export()).toMatchObject({ + records: [mutation], + unknowns: [expect.objectContaining({ domain: "record-limit-test" })], + }); + }); + it("derives byte-stable manifests independent of record insertion order", () => { const artifactEvidence = createEvidence(TARGET, PROVIDER, { operation: "health",