mirror of
https://github.com/morluto/rea
synced 2026-10-27 22:45:19 -04:00
refactor(evidence): return import counts from ledger owner
This commit is contained in:
@@ -21,6 +21,7 @@ import type {
|
||||
UpdateUnknownInput,
|
||||
} from "../../domain/residualUnknown.js";
|
||||
import type {
|
||||
EvidenceImportReceipt,
|
||||
UnknownPage,
|
||||
UnknownQuery,
|
||||
} from "../investigation/InvestigationRecordPort.js";
|
||||
@@ -167,12 +168,19 @@ export abstract class BinarySessionRecords {
|
||||
|
||||
importEvidenceBundle(
|
||||
bundle: unknown,
|
||||
): Result<number, EvidenceIntegrityError | AnalysisInputError> {
|
||||
): Result<
|
||||
EvidenceImportReceipt,
|
||||
EvidenceIntegrityError | AnalysisInputError
|
||||
> {
|
||||
const imported = this.#records.mergeEvidenceBundle(bundle);
|
||||
if (!imported.ok) return imported;
|
||||
this.#snapshot.refreshEvidenceMetadata(imported.value.replacements);
|
||||
if (imported.value.changed) this.#emitSnapshotChanged();
|
||||
return ok(imported.value.recordsAdded);
|
||||
return ok({
|
||||
recordsAdded: imported.value.recordsAdded,
|
||||
unknownsAdded: imported.value.unknownsAdded,
|
||||
totalRecords: this.#records.immutableEvidenceBundle().records.length,
|
||||
});
|
||||
}
|
||||
|
||||
protected abstract activeAnalysisBinding(): ActiveAnalysisBinding | undefined;
|
||||
|
||||
@@ -77,6 +77,13 @@ export interface UnknownRegistryPort {
|
||||
>;
|
||||
}
|
||||
|
||||
/** Counts from the committed merge, before adapter response projection. */
|
||||
export interface EvidenceImportReceipt {
|
||||
readonly recordsAdded: number;
|
||||
readonly unknownsAdded: number;
|
||||
readonly totalRecords: number;
|
||||
}
|
||||
|
||||
/** Investigation record surface; binary execution and snapshots remain separate. */
|
||||
export interface InvestigationRecordPort
|
||||
extends
|
||||
@@ -86,5 +93,5 @@ export interface InvestigationRecordPort
|
||||
UnknownRegistryPort {
|
||||
importEvidenceBundle(
|
||||
bundle: unknown,
|
||||
): Result<number, EvidenceIntegrityError | AnalysisInputError>;
|
||||
): Result<EvidenceImportReceipt, EvidenceIntegrityError | AnalysisInputError>;
|
||||
}
|
||||
|
||||
@@ -8,7 +8,6 @@ import {
|
||||
writeEvidenceBundle,
|
||||
} from "../application/EvidenceBundleFiles.js";
|
||||
import { toolContract } from "../contracts/toolContracts.js";
|
||||
import type { EvidenceBundle } from "../domain/evidenceBundle.js";
|
||||
import type { ArtifactResourceScope } from "../artifacts/ArtifactResourceScope.js";
|
||||
import { err, ok } from "../domain/result.js";
|
||||
import { AnalysisCancelledError } from "../domain/analysisErrorCore.js";
|
||||
@@ -117,11 +116,6 @@ const registerImportEvidenceTool = ({
|
||||
);
|
||||
if (!loaded.ok)
|
||||
return server.delivery.toCallToolResult(loaded, importContract);
|
||||
const retainedUnknownRevisions = new Set(
|
||||
session
|
||||
.immutableEvidenceBundle()
|
||||
.unknowns.map((unknown) => unknownRevisionKey(unknown)),
|
||||
);
|
||||
if (signal.aborted)
|
||||
return server.delivery.toCallToolResult(
|
||||
err(
|
||||
@@ -135,14 +129,9 @@ const registerImportEvidenceTool = ({
|
||||
return imported.ok
|
||||
? server.delivery.toCallToolResult(
|
||||
ok({
|
||||
imported: imported.value,
|
||||
unknowns_added: loaded.value.unknowns.filter(
|
||||
(unknown) =>
|
||||
!retainedUnknownRevisions.has(
|
||||
unknownRevisionKey(unknown),
|
||||
),
|
||||
).length,
|
||||
total: session.immutableEvidenceBundle().records.length,
|
||||
imported: imported.value.recordsAdded,
|
||||
unknowns_added: imported.value.unknownsAdded,
|
||||
total: imported.value.totalRecords,
|
||||
}),
|
||||
importContract,
|
||||
)
|
||||
@@ -152,10 +141,6 @@ const registerImportEvidenceTool = ({
|
||||
);
|
||||
};
|
||||
|
||||
const unknownRevisionKey = (
|
||||
unknown: EvidenceBundle["unknowns"][number],
|
||||
): string => `${unknown.unknown_id}:${String(unknown.revision)}`;
|
||||
|
||||
interface UnknownToolRegistration {
|
||||
readonly server: EvidenceMcpServer;
|
||||
readonly session: BinarySessionPort;
|
||||
|
||||
@@ -10,6 +10,7 @@ import { toolContract } from "../../../src/contracts/toolContracts.js";
|
||||
import { createServer } from "../../../src/server/createServer.js";
|
||||
import { EvidenceMcpServer } from "../../../src/server/EvidenceMcpServer.js";
|
||||
import { ToolResultDelivery } from "../../../src/server/ToolResultDelivery.js";
|
||||
import * as evidenceBundleFiles from "../../../src/application/EvidenceBundleFiles.js";
|
||||
import { OwnedFileHandle } from "../../../src/filesystem/OwnedFileHandle.js";
|
||||
import { createTestBinarySession } from "../../fixtures/binarySession.js";
|
||||
import { createTestTempDirectory } from "../../fixtures/temporaryDirectory.js";
|
||||
@@ -51,13 +52,13 @@ it("preserves the exact reason when import is cancelled after reading the bundle
|
||||
const reason = { request: "cancel after authenticated bundle read" };
|
||||
const controller = new AbortController();
|
||||
const before = session.exportEvidenceBundle();
|
||||
const snapshot = session.immutableEvidenceBundle.bind(session);
|
||||
const snapshotRead = vi
|
||||
.spyOn(session, "immutableEvidenceBundle")
|
||||
.mockImplementationOnce(() => {
|
||||
const bundle = snapshot();
|
||||
const read = evidenceBundleFiles.readImmutableEvidenceBundle;
|
||||
const authenticatedRead = vi
|
||||
.spyOn(evidenceBundleFiles, "readImmutableEvidenceBundle")
|
||||
.mockImplementationOnce(async (...args) => {
|
||||
const loaded = await read(...args);
|
||||
controller.abort(reason);
|
||||
return bundle;
|
||||
return loaded;
|
||||
});
|
||||
const imported = vi.spyOn(session, "importEvidenceBundle");
|
||||
const delivery = vi.spyOn(ToolResultDelivery.prototype, "toCallToolResult");
|
||||
@@ -73,7 +74,7 @@ it("preserves the exact reason when import is cancelled after reading the bundle
|
||||
expect(response.isError).toBe(true);
|
||||
expect(delivered.error._tag).toBe("AnalysisCancelledError");
|
||||
expect(delivered.error.cause).toBe(reason);
|
||||
expect(snapshotRead).toHaveBeenCalledOnce();
|
||||
expect(authenticatedRead).toHaveBeenCalledOnce();
|
||||
expect(imported).not.toHaveBeenCalled();
|
||||
expect(session.exportEvidenceBundle()).toEqual(before);
|
||||
});
|
||||
|
||||
@@ -153,3 +153,61 @@ it("admits a later import after close_binary without retaining the earlier bundl
|
||||
).toEqual(["after_close"]);
|
||||
await fixture.client.ping();
|
||||
});
|
||||
|
||||
it("reports newly committed Unknown revisions and counts a repeated import once", async () => {
|
||||
const root = await createTestTempDirectory("rea-mcp-import-counts-");
|
||||
const source = createTestBinarySession(() => {
|
||||
throw new Error("Evidence import must not start a binary provider");
|
||||
});
|
||||
const unknown = source.recordUnknown({
|
||||
question: "Does this artifact have an unresolved dependency?",
|
||||
severity: "medium",
|
||||
domain: "investigation",
|
||||
supporting_evidence_ids: [],
|
||||
contradicting_evidence_ids: [],
|
||||
required_authority: "analyst-inference",
|
||||
required_confidence: "derived",
|
||||
required_environment: null,
|
||||
recommended_probes: [],
|
||||
relationships: [],
|
||||
});
|
||||
if (!unknown.ok) throw unknown.error;
|
||||
const bundle = source.exportEvidenceBundle();
|
||||
const path = join(root, "unknowns.json");
|
||||
await writeFile(path, JSON.stringify(bundle));
|
||||
const session = createTestBinarySession(() => {
|
||||
throw new Error("Evidence import must not start a binary provider");
|
||||
});
|
||||
const server = createServer({ kind: "session", session });
|
||||
const client = new Client({ name: "import-counts", version: "1" });
|
||||
const [clientTransport, serverTransport] =
|
||||
InMemoryTransport.createLinkedPair();
|
||||
onTestFinished(async () => {
|
||||
await client.close();
|
||||
await server.close();
|
||||
await session.close();
|
||||
await source.close();
|
||||
});
|
||||
await server.connect(serverTransport);
|
||||
await client.connect(clientTransport);
|
||||
const first = await client.callTool({
|
||||
name: "import_evidence_bundle",
|
||||
arguments: { path },
|
||||
});
|
||||
expect(first.isError).not.toBe(true);
|
||||
expect(first.structuredContent).toEqual({
|
||||
result: {
|
||||
imported: bundle.records.length,
|
||||
unknowns_added: 1,
|
||||
total: bundle.records.length,
|
||||
},
|
||||
});
|
||||
const repeated = await client.callTool({
|
||||
name: "import_evidence_bundle",
|
||||
arguments: { path },
|
||||
});
|
||||
expect(repeated.isError).not.toBe(true);
|
||||
expect(repeated.structuredContent).toEqual({
|
||||
result: { imported: 0, unknowns_added: 0, total: bundle.records.length },
|
||||
});
|
||||
});
|
||||
|
||||
@@ -606,10 +606,24 @@ describe("evidence metadata imports and snapshot cache", () => {
|
||||
);
|
||||
expect(
|
||||
session.importEvidenceBundle(createEvidenceBundle([addition])),
|
||||
).toEqual({ ok: true, value: 1 });
|
||||
).toEqual({
|
||||
ok: true,
|
||||
value: {
|
||||
recordsAdded: 1,
|
||||
unknownsAdded: 0,
|
||||
totalRecords: before.value.evidence_bundle.records.length + 1,
|
||||
},
|
||||
});
|
||||
expect(
|
||||
session.importEvidenceBundle(createEvidenceBundle([addition])),
|
||||
).toEqual({ ok: true, value: 0 });
|
||||
).toEqual({
|
||||
ok: true,
|
||||
value: {
|
||||
recordsAdded: 0,
|
||||
unknownsAdded: 0,
|
||||
totalRecords: before.value.evidence_bundle.records.length + 1,
|
||||
},
|
||||
});
|
||||
expect(session.exportAnalysisSnapshot()).toMatchObject({
|
||||
ok: true,
|
||||
value: {
|
||||
@@ -630,7 +644,9 @@ describe("evidence metadata imports and snapshot cache", () => {
|
||||
expect(session.exportAnalysisSnapshot().ok).toBe(true);
|
||||
await session.close();
|
||||
});
|
||||
});
|
||||
|
||||
describe("Evidence display metadata refresh", () => {
|
||||
it.each(["active", "unrelated"] as const)(
|
||||
"refreshes %s Evidence display metadata without invalidating provider observations",
|
||||
async (scope) => {
|
||||
@@ -671,7 +687,14 @@ describe("evidence metadata imports and snapshot cache", () => {
|
||||
const unsubscribe = session.onAnalysisSnapshotChanged(changed);
|
||||
expect(
|
||||
session.importEvidenceBundle(createEvidenceBundle([relocated])),
|
||||
).toEqual({ ok: true, value: 0 });
|
||||
).toEqual({
|
||||
ok: true,
|
||||
value: {
|
||||
recordsAdded: 0,
|
||||
unknownsAdded: 0,
|
||||
totalRecords: session.immutableEvidenceBundle().records.length,
|
||||
},
|
||||
});
|
||||
expect(changed).toHaveBeenCalledTimes(1);
|
||||
unsubscribe();
|
||||
expect(session.evidenceById(evidence.evidence_id)).toEqual(relocated);
|
||||
|
||||
@@ -138,12 +138,20 @@ it("notifies an Unknown-only bundle change even when no new Evidence is added",
|
||||
});
|
||||
expect(recipient.importEvidenceBundle(bundle)).toEqual({
|
||||
ok: true,
|
||||
value: 0,
|
||||
value: {
|
||||
recordsAdded: 0,
|
||||
unknownsAdded: 1,
|
||||
totalRecords: bundle.records.length,
|
||||
},
|
||||
});
|
||||
expect(revisions).toEqual([1]);
|
||||
expect(recipient.importEvidenceBundle(bundle)).toEqual({
|
||||
ok: true,
|
||||
value: 0,
|
||||
value: {
|
||||
recordsAdded: 0,
|
||||
unknownsAdded: 0,
|
||||
totalRecords: bundle.records.length,
|
||||
},
|
||||
});
|
||||
expect(revisions).toEqual([1]);
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user