refactor(evidence): remove session ledger quotas
This commit is contained in:
parent
23e56c517c
commit
dca293a446
|
|
@ -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
|
||||
|
|
|
|||
|
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
});
|
||||
});
|
||||
|
|
|
|||
|
|
@ -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: [] });
|
||||
});
|
||||
|
|
|
|||
|
|
@ -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)}`;
|
||||
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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 };
|
||||
|
|
|
|||
|
|
@ -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";
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
Loading…
Reference in New Issue