refactor(evidence): remove session ledger quotas

This commit is contained in:
morluto 2026-09-29 10:53:17 +08:00
parent 23e56c517c
commit dca293a446
11 changed files with 61 additions and 189 deletions

View File

@ -17,7 +17,6 @@ harm_rank,location,trigger,current_copy,user_risk,proposed_replacement,implement
16,src/domain/errors.ts:AnalysisTimeoutError,Analysis deadline expires,"Analysis operation timed out after {timeoutMs}ms: {operation}",Exposes internal deadline and operation; weak recovery,"Analysis took too long. Try a smaller request, then run `rea doctor` if it continues.",implemented,focused assertions passed; full `npm run check` passed
17,src/domain/errors.ts:HopperTimeoutError,Provider deadline expires,"Hopper did not respond within {timeoutMs}ms",Exposes provider and internal deadline,"Analysis took too long. Try a smaller request, then run `rea doctor` if it continues.",implemented,focused assertions passed; full `npm run check` passed
18,src/domain/errors.ts:ProviderSelectionError,Deep-provider selection fails,"Analysis provider selection failed ({reason}) for {requestedProviderId}; candidates: {candidateIds}",Retains non-secret local selection context that needs a safe structured projection,Reason-specific guidance with stable candidate IDs and actionable rejection diagnostics,implemented,focused registry/CLI/MCP assertions passed; full `npm run check` passed
19,src/domain/errors.ts:EvidenceLimitError,Evidence ledger limit reached,"Evidence ledger {limit} limit exceeded ({maximum})",Exposes internal storage model,"Evidence is too large for this session. Reduce the evidence set and try again.",implemented,focused assertions passed; full `npm run check` passed
20,src/domain/errors.ts:AnalysisInputError,Parsed analysis input invalid,"Invalid analysis input for {operation}",Exposes operation identifier and lacks correction,"Analysis input is invalid. Check the arguments and try again.",implemented,focused assertions passed; full `npm run check` passed
21,src/domain/errors.ts:AnalysisCancelledError,Caller cancels analysis,"Analysis operation was cancelled: {operation}",Exposes operation identifier,"Analysis was cancelled. Start it again when ready.",implemented,focused assertions passed; full `npm run check` passed
22,src/domain/errors.ts:HopperCancelledError,Caller cancels provider request,Hopper operation was cancelled,Exposes provider name,"Analysis was cancelled. Start it again when ready.",implemented,focused assertions passed; full `npm run check` passed

1 harm_rank location trigger current_copy user_risk proposed_replacement implementation_status verification_result
17 16 src/domain/errors.ts:AnalysisTimeoutError Analysis deadline expires Analysis operation timed out after {timeoutMs}ms: {operation} Exposes internal deadline and operation; weak recovery Analysis took too long. Try a smaller request, then run `rea doctor` if it continues. implemented focused assertions passed; full `npm run check` passed
18 17 src/domain/errors.ts:HopperTimeoutError Provider deadline expires Hopper did not respond within {timeoutMs}ms Exposes provider and internal deadline Analysis took too long. Try a smaller request, then run `rea doctor` if it continues. implemented focused assertions passed; full `npm run check` passed
19 18 src/domain/errors.ts:ProviderSelectionError Deep-provider selection fails Analysis provider selection failed ({reason}) for {requestedProviderId}; candidates: {candidateIds} Retains non-secret local selection context that needs a safe structured projection Reason-specific guidance with stable candidate IDs and actionable rejection diagnostics implemented focused registry/CLI/MCP assertions passed; full `npm run check` passed
19 src/domain/errors.ts:EvidenceLimitError Evidence ledger limit reached Evidence ledger {limit} limit exceeded ({maximum}) Exposes internal storage model Evidence is too large for this session. Reduce the evidence set and try again. implemented focused assertions passed; full `npm run check` passed
20 20 src/domain/errors.ts:AnalysisInputError Parsed analysis input invalid Invalid analysis input for {operation} Exposes operation identifier and lacks correction Analysis input is invalid. Check the arguments and try again. implemented focused assertions passed; full `npm run check` passed
21 21 src/domain/errors.ts:AnalysisCancelledError Caller cancels analysis Analysis operation was cancelled: {operation} Exposes operation identifier Analysis was cancelled. Start it again when ready. implemented focused assertions passed; full `npm run check` passed
22 22 src/domain/errors.ts:HopperCancelledError Caller cancels provider request Hopper operation was cancelled Exposes provider name Analysis was cancelled. Start it again when ready. implemented focused assertions passed; full `npm run check` passed

View File

@ -8,7 +8,6 @@ import type { AnalysisProviderSelector } from "../contracts/providerSelection.js
import type {
AnalysisError,
EvidenceIntegrityError,
EvidenceLimitError,
UnknownRegistryError,
} from "../domain/errors.js";
import type { Result } from "../domain/result.js";
@ -45,13 +44,11 @@ export interface BinarySessionPort extends AnalysisOperationPort {
activeTarget(): BinaryTarget | undefined;
recordEvidence(
evidence: Evidence,
): Result<"added" | "duplicate", EvidenceIntegrityError | EvidenceLimitError>;
): Result<"added" | "duplicate", EvidenceIntegrityError>;
hasEvidence(evidenceId: string): boolean;
evidenceById(evidenceId: string): Evidence | undefined;
exportEvidenceBundle(): EvidenceBundle;
importEvidenceBundle(
bundle: unknown,
): Result<number, EvidenceIntegrityError | EvidenceLimitError>;
importEvidenceBundle(bundle: unknown): Result<number, EvidenceIntegrityError>;
exportAnalysisSnapshot(): Result<AnalysisSnapshot, AnalysisError>;
importAnalysisSnapshot(
snapshot: AnalysisSnapshot,

View File

@ -6,7 +6,6 @@ import type { EvidenceBundle } from "../domain/evidenceBundle.js";
import { evidenceBundleForTarget } from "../domain/evidenceBundle.js";
import {
EvidenceIntegrityError,
EvidenceLimitError,
type AnalysisError,
type UnknownRegistryError,
} from "../domain/errors.js";
@ -36,10 +35,7 @@ export interface ActiveAnalysisBinding {
/** Owns session evidence, snapshots, and residual unknowns. */
export abstract class BinarySessionRecords {
readonly #evidence = new EvidenceLedger({
maxRecords: 10_000,
maxBytes: 64 * 1024 * 1024,
});
readonly #evidence = new EvidenceLedger();
readonly #snapshot = new AnalysisSnapshotCache();
#snapshotInvalidated = false;
readonly #snapshotListeners = new Set<() => void | Promise<void>>();
@ -52,10 +48,7 @@ export abstract class BinarySessionRecords {
recordEvidence(
evidence: Evidence,
): Result<
"added" | "duplicate",
EvidenceIntegrityError | EvidenceLimitError
> {
): Result<"added" | "duplicate", EvidenceIntegrityError> {
const recorded = this.#evidence.record(evidence);
if (recorded.ok && recorded.value === "added") this.#emitSnapshotChanged();
return recorded;
@ -75,7 +68,7 @@ export abstract class BinarySessionRecords {
importEvidenceBundle(
bundle: unknown,
): Result<number, EvidenceIntegrityError | EvidenceLimitError> {
): Result<number, EvidenceIntegrityError> {
const imported = this.#evidence.import(bundle);
if (!imported.ok) return imported;
if (imported.value.changed) this.#emitSnapshotChanged();

View File

@ -16,7 +16,7 @@ import {
writeEvidenceBundle,
} from "./EvidenceBundleFiles.js";
/** Validate and merge one bundle using the same bounded ledger as MCP. */
/** Validate and merge one bundle using the session Evidence ledger. */
export const importEvidenceBundleCommand = async (
path: string,
policy: EvidenceFilePolicy,
@ -72,8 +72,7 @@ export const compareEvidenceBundlesCommand = async (input: {
}
};
const createLedger = (): EvidenceLedger =>
new EvidenceLedger({ maxRecords: 10_000, maxBytes: 64 * 1024 * 1024 });
const createLedger = (): EvidenceLedger => new EvidenceLedger();
const projectWrite = (
bundle: EvidenceBundle,

View File

@ -16,8 +16,7 @@ import {
} from "../domain/residualUnknown.js";
const provider = { id: "fixture", name: "Fixture", version: "1" };
const ledger = (): EvidenceLedger =>
new EvidenceLedger({ maxRecords: 1_000, maxBytes: 4 * 1024 * 1024 });
const ledger = (): EvidenceLedger => new EvidenceLedger();
const evidence = (
label: string,
confidence: "observed" | "derived" | "inferred" = "derived",
@ -110,13 +109,10 @@ describe("residual unknown registry reads and identity", () => {
).toMatchObject({ ok: true, value: { valid: false } });
});
it("requires approval, derives stable IDs, filters heads, and rejects duplicate creation", () => {
expect(() =>
recordUnknownInputSchema.parse({
...input("Is branch live?"),
approved: false,
}),
).toThrow();
it("derives stable IDs, filters heads, and rejects duplicate creation", () => {
expect(recordUnknownInputSchema.parse(input("Is branch live?"))).toEqual(
input("Is branch live?"),
);
const store = ledger();
const first = store.recordUnknown(
input(" Is branch live? "),
@ -311,10 +307,10 @@ describe("residual unknown registry resolution and history", () => {
expect(base.import(branch.export()).ok).toBe(false);
expect(serializeEvidenceBundle(base.export())).toBe(before);
const bounded = new EvidenceLedger({ maxRecords: 1, maxBytes: 1_000_000 });
const unbounded = new EvidenceLedger();
expect(
bounded.recordUnknown(input("Too many?"), mutation("bounded")).ok,
).toBe(false);
expect(bounded.export()).toMatchObject({ records: [], unknowns: [] });
unbounded.recordUnknown(input("No quota"), mutation("unbounded")).ok,
).toBe(true);
expect(unbounded.export().unknowns).toHaveLength(1);
});
});

View File

@ -17,13 +17,13 @@ const TARGET: BinaryTarget = {
const PROVIDER = { id: "fixture", name: "Fixture provider", version: "1" };
describe("evidence ledger recording", () => {
it("deduplicates and atomically imports bounded bundles", () => {
it("deduplicates and atomically imports bundles", () => {
const evidence = createEvidence(TARGET, PROVIDER, {
operation: "health",
parameters: {},
result: true,
});
const ledger = new EvidenceLedger({ maxRecords: 1, maxBytes: 1_000_000 });
const ledger = new EvidenceLedger();
expect(ledger.record(evidence)).toEqual({ ok: true, value: "added" });
expect(ledger.record(evidence)).toEqual({ ok: true, value: "duplicate" });
const relocated = createEvidence(
@ -56,7 +56,7 @@ describe("evidence ledger recording", () => {
parameters: { beta: 2, alpha: 1 },
normalized_result: { beta: 2, alpha: 1 },
});
const ledger = new EvidenceLedger({ maxRecords: 1, maxBytes: 1_000_000 });
const ledger = new EvidenceLedger();
expect(ledger.record(evidence)).toEqual({ ok: true, value: "added" });
expect(ledger.record(reordered)).toEqual({
ok: true,
@ -64,41 +64,32 @@ describe("evidence ledger recording", () => {
});
});
it("returns typed record and byte limit failures without eviction", () => {
const first = createEvidence(TARGET, PROVIDER, {
operation: "health",
parameters: {},
result: true,
});
const second = createEvidence(TARGET, PROVIDER, {
operation: "health",
parameters: { changed: true },
result: true,
});
const recordBound = new EvidenceLedger({
maxRecords: 1,
maxBytes: 1_000_000,
});
expect(recordBound.record(first).ok).toBe(true);
expect(recordBound.record(second)).toMatchObject({
ok: false,
error: { _tag: "EvidenceLimitError", limit: "records", maximum: 1 },
});
expect(recordBound.export().records).toEqual([first]);
const byteBound = new EvidenceLedger({ maxRecords: 10, maxBytes: 1 });
expect(byteBound.record(first)).toMatchObject({
ok: false,
error: { _tag: "EvidenceLimitError", limit: "bytes", maximum: 1 },
});
expect(byteBound.export().records).toEqual([]);
it("retains more than ten thousand inline evidence records", () => {
const ledger = new EvidenceLedger();
for (let index = 0; index < 10_001; index += 1) {
const record = createEvidence(TARGET, PROVIDER, {
operation: "health",
parameters: { index },
result: true,
});
expect(ledger.record(record)).toEqual({ ok: true, value: "added" });
}
expect(ledger.export().records).toHaveLength(10_001);
});
it("counts unknown revisions when enforcing direct record limits", () => {
const ledger = new EvidenceLedger({
maxRecords: 2,
maxBytes: 1_000_000,
it("retains inline evidence larger than the former session byte quota", () => {
const ledger = new EvidenceLedger();
const record = createEvidence(TARGET, PROVIDER, {
operation: "health",
parameters: {},
result: "x".repeat(64 * 1024 * 1024 + 1),
});
expect(ledger.record(record)).toEqual({ ok: true, value: "added" });
expect(ledger.get(record.evidence_id)).toEqual(record);
});
it("retains Evidence alongside unknown revisions without a record quota", () => {
const ledger = new EvidenceLedger();
const mutation = createEvidence(undefined, PROVIDER, {
predicateType: "rea.residual-unknown-mutation/v1",
operation: "record_unknown",
@ -125,12 +116,9 @@ describe("evidence ledger recording", () => {
parameters: {},
result: true,
});
expect(ledger.record(direct)).toMatchObject({
ok: false,
error: { _tag: "EvidenceLimitError", limit: "records", maximum: 2 },
});
expect(ledger.record(direct)).toEqual({ ok: true, value: "added" });
expect(ledger.export()).toMatchObject({
records: [mutation],
records: expect.arrayContaining([mutation, direct]),
unknowns: [expect.objectContaining({ domain: "record-limit-test" })],
});
});
@ -138,7 +126,7 @@ describe("evidence ledger recording", () => {
describe("evidence bundle imports", () => {
it("rejects conflicting duplicate IDs without mutating the ledger", () => {
const ledger = new EvidenceLedger({ maxRecords: 2, maxBytes: 1_000_000 });
const ledger = new EvidenceLedger();
const first = createEvidence(TARGET, PROVIDER, {
operation: "health",
parameters: {},
@ -159,8 +147,8 @@ describe("evidence bundle imports", () => {
expect(ledger.export().records).toEqual([first]);
});
it("atomically rejects Evidence plus unknown when the batch exceeds limits", () => {
const ledger = new EvidenceLedger({ maxRecords: 2, maxBytes: 1_000_000 });
it("atomically rejects Evidence plus an invalid unknown", () => {
const ledger = new EvidenceLedger();
const output = createEvidence(undefined, PROVIDER, {
operation: "derived",
parameters: {},
@ -177,7 +165,7 @@ describe("evidence bundle imports", () => {
question: "What remains unresolved?",
severity: "high",
domain: "atomic-test",
supporting_evidence_ids: [],
supporting_evidence_ids: [`ev_${"f".repeat(64)}`],
contradicting_evidence_ids: [],
required_authority: "shipped-artifact",
required_confidence: "observed",
@ -187,7 +175,7 @@ describe("evidence bundle imports", () => {
});
expect(ledger.recordWithUnknown(output, unknown, mutation)).toMatchObject({
ok: false,
error: { _tag: "EvidenceLimitError" },
error: { _tag: "UnknownRegistryError" },
});
expect(ledger.export()).toMatchObject({ records: [], unknowns: [] });
});

View File

@ -8,7 +8,6 @@ import {
} from "../domain/evidenceBundle.js";
import {
EvidenceIntegrityError,
EvidenceLimitError,
UnknownRegistryError,
} from "../domain/errors.js";
import { parseEvidence, type Evidence } from "../domain/evidence.js";
@ -22,12 +21,7 @@ import {
} from "../domain/residualUnknown.js";
import { err, ok, type Result } from "../domain/result.js";
export interface EvidenceLedgerLimits {
readonly maxRecords: number;
readonly maxBytes: number;
}
type EvidenceLedgerFailure = EvidenceIntegrityError | EvidenceLimitError;
type EvidenceLedgerFailure = EvidenceIntegrityError;
type RecordResult = Result<"added" | "duplicate", EvidenceLedgerFailure>;
/** Describes the state changes made by one atomic bundle import. */
@ -39,20 +33,11 @@ export interface EvidenceImportDelta {
type ImportResult = Result<EvidenceImportDelta, EvidenceLedgerFailure>;
/** Bounded, session-owned set of immutable evidence records. */
/** Session-owned set of immutable evidence records. */
export class EvidenceLedger {
readonly #records = new Map<string, Evidence>();
readonly #unknownRevisions = new Map<string, ResidualUnknown>();
readonly #unknownHeads = new Map<string, ResidualUnknown>();
#bytes = 0;
constructor(private readonly limits: EvidenceLedgerLimits) {
if (!Number.isSafeInteger(limits.maxRecords) || limits.maxRecords < 1)
throw new RangeError("maxRecords must be a positive safe integer");
if (!Number.isSafeInteger(limits.maxBytes) || limits.maxBytes < 1)
throw new RangeError("maxBytes must be a positive safe integer");
}
/** Record evidence idempotently; conflicting content is rejected. */
record(input: Evidence): RecordResult {
let evidence: Evidence;
@ -71,18 +56,7 @@ export class EvidenceLedger {
? ok("duplicate")
: err(new EvidenceIntegrityError("Conflicting evidence record"));
}
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)
return err(new EvidenceLimitError("bytes", this.limits.maxBytes));
this.#records.set(evidence.evidence_id, evidence);
this.#bytes += bytes;
return ok("added");
}
@ -132,7 +106,7 @@ export class EvidenceLedger {
return err(checked.error);
}
const added = pending.size - this.#records.size;
this.#commit(pending, pendingUnknowns, checked.value);
this.#commit(pending, pendingUnknowns);
return ok({
recordsAdded: added,
unknownsAdded,
@ -165,10 +139,7 @@ export class EvidenceLedger {
recordUnknown(
input: RecordUnknownInput,
mutationEvidence: Evidence,
): Result<
ResidualUnknown,
EvidenceIntegrityError | EvidenceLimitError | UnknownRegistryError
> {
): Result<ResidualUnknown, EvidenceIntegrityError | UnknownRegistryError> {
let parsedMutation: Evidence;
try {
parsedMutation = parseEvidence(mutationEvidence);
@ -201,7 +172,7 @@ export class EvidenceLedger {
mutationInput: Evidence,
): Result<
ResidualUnknown | null,
EvidenceIntegrityError | EvidenceLimitError | UnknownRegistryError
EvidenceIntegrityError | UnknownRegistryError
> {
let evidence: Evidence;
let mutation: Evidence;
@ -251,10 +222,7 @@ export class EvidenceLedger {
updateUnknown(
input: UpdateUnknownInput,
mutationEvidence: Evidence,
): Result<
ResidualUnknown,
EvidenceIntegrityError | EvidenceLimitError | UnknownRegistryError
> {
): Result<ResidualUnknown, EvidenceIntegrityError | UnknownRegistryError> {
const current = this.#unknownHeads.get(input.unknown_id);
if (current === undefined)
return err(new UnknownRegistryError("not-found"));
@ -317,16 +285,12 @@ export class EvidenceLedger {
this.#records.clear();
this.#unknownRevisions.clear();
this.#unknownHeads.clear();
this.#bytes = 0;
}
#appendUnknown(
unknown: ResidualUnknown,
mutationEvidenceInput: Evidence,
): Result<
ResidualUnknown,
EvidenceIntegrityError | EvidenceLimitError | UnknownRegistryError
> {
): Result<ResidualUnknown, EvidenceIntegrityError | UnknownRegistryError> {
let mutationEvidence: Evidence;
try {
mutationEvidence = parseEvidence(mutationEvidenceInput);
@ -345,12 +309,8 @@ export class EvidenceLedger {
#appendIncremental(
records: readonly Evidence[],
unknown?: ResidualUnknown,
): Result<
void,
EvidenceIntegrityError | EvidenceLimitError | UnknownRegistryError
> {
): Result<void, EvidenceIntegrityError | UnknownRegistryError> {
const additions = new Map<string, Evidence>();
let addedBytes = 0;
for (const evidence of records) {
const existing =
additions.get(evidence.evidence_id) ??
@ -361,22 +321,11 @@ export class EvidenceLedger {
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(
@ -395,30 +344,13 @@ export class EvidenceLedger {
this.#unknownRevisions.set(unknownKey, unknown);
this.#unknownHeads.set(unknown.unknown_id, unknown);
}
this.#bytes += addedBytes + unknownBytes;
return ok(undefined);
}
#validateCandidate(
records: ReadonlyMap<string, Evidence>,
unknowns: ReadonlyMap<string, ResidualUnknown>,
): Result<
number,
EvidenceIntegrityError | EvidenceLimitError | UnknownRegistryError
> {
if (this.#exceedsRecordLimit(records.size, unknowns.size))
return err(new EvidenceLimitError("records", this.limits.maxRecords));
const bytes =
[...records.values()].reduce(
(total, evidence) => total + serializedBytes(evidence),
0,
) +
[...unknowns.values()].reduce(
(total, unknown) => total + serializedBytes(unknown),
0,
);
if (bytes > this.limits.maxBytes)
return err(new EvidenceLimitError("bytes", this.limits.maxBytes));
): Result<void, EvidenceIntegrityError | UnknownRegistryError> {
try {
parseEvidenceBundle(
createEvidenceBundle([...records.values()], [...unknowns.values()]),
@ -426,17 +358,12 @@ export class EvidenceLedger {
} catch (cause: unknown) {
return err(new UnknownRegistryError("integrity", { cause }));
}
return ok(bytes);
}
#exceedsRecordLimit(recordCount: number, unknownCount: number): boolean {
return recordCount + unknownCount > this.limits.maxRecords;
return ok(undefined);
}
#commit(
records: ReadonlyMap<string, Evidence>,
unknowns: ReadonlyMap<string, ResidualUnknown>,
bytes: number,
): void {
this.#records.clear();
for (const [id, evidence] of records) this.#records.set(id, evidence);
@ -448,13 +375,9 @@ export class EvidenceLedger {
if (head === undefined || head.revision < unknown.revision)
this.#unknownHeads.set(unknown.unknown_id, unknown);
}
this.#bytes = bytes;
}
}
const serializedBytes = (value: Evidence | ResidualUnknown): number =>
Buffer.byteLength(JSON.stringify(value), "utf8");
const unknownRevisionKey = (unknown: ResidualUnknown): string =>
`${unknown.unknown_id}:${String(unknown.revision)}`;

View File

@ -6,7 +6,6 @@ import {
ConfigurationError,
EvidenceFileError,
EvidenceIntegrityError,
EvidenceLimitError,
HopperRemoteError,
HopperProcessError,
HopperStartError,
@ -133,7 +132,6 @@ const STATIC_ERROR_CATEGORIES: Readonly<
AnalysisCapabilityUnavailableError: "unsupported_provider",
ProviderSelectionError: "unsupported_provider",
EvidenceIntegrityError: "integrity_mismatch",
EvidenceLimitError: "truncated",
AnalysisCancelledError: "cancelled",
HopperCancelledError: "cancelled",
AnalysisTimeoutError: "timeout",
@ -158,8 +156,6 @@ export const analysisErrorUserMessage = (error: AnalysisError): string => {
return artifactMessage(error.reason);
if (error instanceof EvidenceIntegrityError)
return "Evidence is invalid or has changed. Recreate or re-import it, then try again.";
if (error instanceof EvidenceLimitError)
return "Evidence is too large for this session. Reduce the evidence set and try again.";
if (error instanceof EvidenceFileError)
return evidenceFileMessage(error.reason);
if (error instanceof WorkspaceStorageError)
@ -307,7 +303,6 @@ const KNOWN_ERROR_TAGS = {
ArtifactOperationError: true,
ProcessCaptureError: true,
EvidenceIntegrityError: true,
EvidenceLimitError: true,
EvidenceFileError: true,
WorkspaceStorageError: true,
UnknownRegistryError: true,

View File

@ -12,7 +12,6 @@ import {
BinaryTargetError,
BrowserObservationError,
EvidenceFileError,
EvidenceLimitError,
EvidenceReferenceError,
HopperCancelledError,
HopperProcessError,
@ -175,7 +174,6 @@ const STATIC_ERROR_CODES = {
NoBinaryOpenError: "target_unavailable",
BinaryTargetError: "target_unavailable",
EvidenceIntegrityError: "evidence_integrity_mismatch",
EvidenceLimitError: "truncated",
ProviderAdapterError: "execution_failure",
HopperRemoteError: "execution_failure",
} as const satisfies Readonly<
@ -266,8 +264,6 @@ const artifactStateErrorDetails = (
reason: error.reason,
...(error.reason === "limit" ? { truncated: true } : {}),
};
if (error instanceof EvidenceLimitError)
return { limit: error.limit, maximum: error.maximum, truncated: true };
if (error instanceof WorkspaceStorageError)
return { operation: error.operation, reason: error.reason };
if (error instanceof UnknownRegistryError) return { reason: error.reason };

View File

@ -38,7 +38,6 @@ const ANALYSIS_ERROR_TAGS = [
"ArtifactOperationError",
"ProcessCaptureError",
"EvidenceIntegrityError",
"EvidenceLimitError",
"EvidenceFileError",
"WorkspaceStorageError",
"UnknownRegistryError",
@ -319,18 +318,6 @@ export class EvidenceReferenceError extends EvidenceIntegrityError {
}
}
/** A bounded evidence ledger cannot accept more records or serialized bytes. */
export class EvidenceLimitError extends AnalysisError {
readonly _tag = "EvidenceLimitError";
constructor(
readonly limit: "records" | "bytes",
readonly maximum: number,
) {
super(`Evidence ledger ${limit} limit exceeded (${String(maximum)})`);
}
}
/** Evidence bundle filesystem access failed within the configured policy. */
export class EvidenceFileError extends AnalysisError {
readonly _tag = "EvidenceFileError";

View File

@ -2,14 +2,13 @@ import type { BinarySessionPort } from "../application/BinarySession.js";
import {
EvidenceIntegrityError,
EvidenceReferenceError,
type EvidenceLimitError,
} from "../domain/errors.js";
import type { Evidence } from "../domain/evidence.js";
import { err, ok, type Result } from "../domain/result.js";
type EvidenceAuthorityResult = Result<Evidence[], EvidenceIntegrityError>;
/** Resolve bounded IDs to authoritative records with exact semantic identity. */
/** Resolve session IDs to authoritative records with exact semantic identity. */
export const resolveSessionEvidenceIds = (
session: BinarySessionPort,
evidenceIds: readonly string[],
@ -61,7 +60,7 @@ export const resolveSessionEvidenceIds = (
export const recordSessionEvidenceSources = (
recordEvidence: BinarySessionPort["recordEvidence"] | undefined,
sources: readonly Evidence[],
): Result<null, EvidenceIntegrityError | EvidenceLimitError> => {
): Result<null, EvidenceIntegrityError> => {
for (const source of sources) {
const recorded = recordEvidence?.(source);
if (recorded !== undefined && !recorded.ok) return recorded;