mirror of
https://github.com/morluto/rea
synced 2026-10-27 22:45:19 -04:00
fix(snapshot): export only target-bound observations
This commit is contained in:
@@ -491,6 +491,11 @@ Dispatching a provider metadata write invalidates the active session's snapshot
|
||||
even if its reply fails: the provider may already have applied the write.
|
||||
Close and reopen the target before importing or saving another snapshot.
|
||||
|
||||
Saved snapshots include only cache entries whose subject identifies the selected
|
||||
target. Unbound observations and observations of child artifacts remain available
|
||||
in the open session and its Evidence bundle; they are excluded from the target's
|
||||
saved replay entries without changing their subjects.
|
||||
|
||||
Snapshots retain eligible target-scoped question histories in full, including
|
||||
mutation Evidence recorded while another target was active. Observations and
|
||||
related questions must still belong to the saved target. If those dependencies
|
||||
|
||||
@@ -8,7 +8,10 @@ import {
|
||||
ANALYSIS_SNAPSHOT_TARGET,
|
||||
} from "../../domain/analysisSnapshot.fixture.js";
|
||||
import { createEvidence } from "../../domain/evidence.js";
|
||||
import { createEvidenceBundle } from "../../domain/evidenceBundle.js";
|
||||
import {
|
||||
createEvidenceBundle,
|
||||
evidenceBundleForTarget,
|
||||
} from "../../domain/evidenceBundle.js";
|
||||
import { ok } from "../../domain/result.js";
|
||||
import { createAnalysisExecution } from "../AnalysisProvider.js";
|
||||
import {
|
||||
@@ -291,6 +294,90 @@ it.each(["provider", "workflow"] as const)(
|
||||
},
|
||||
);
|
||||
|
||||
it.each(["provider", "workflow"] as const)(
|
||||
"exports target-scoped %s rows without changing retained observations",
|
||||
(kind) => {
|
||||
const cache = new AnalysisSnapshotCache();
|
||||
const ledger = new EvidenceLedger();
|
||||
const target = ANALYSIS_SNAPSHOT_TARGET;
|
||||
const profile = ANALYSIS_SNAPSHOT_PROFILE;
|
||||
const operation = kind === "provider" ? "procedure_info" : "trace_feature";
|
||||
const provider =
|
||||
kind === "provider" ? ANALYSIS_SNAPSHOT_PROVIDER : REA_WORKFLOW_PROVIDER;
|
||||
const analysisProfile =
|
||||
kind === "provider"
|
||||
? profile
|
||||
: workflowAnalysisProfile(profile, operation);
|
||||
const child = { ...target, path: "/tmp/child", sha256: "b".repeat(64) };
|
||||
for (const [index, subject] of [target, null, child].entries()) {
|
||||
const parameters = { address: `0x${index}000` };
|
||||
const result = { observation: index };
|
||||
const execution = createAnalysisExecution(result, provider, {
|
||||
subject,
|
||||
analysisProfile,
|
||||
});
|
||||
const input = {
|
||||
target,
|
||||
profile,
|
||||
operation,
|
||||
parameters,
|
||||
execution: { ...execution, subject, analysisProfile },
|
||||
};
|
||||
if (kind === "provider")
|
||||
cache.record({ ...input, operation: "procedure_info" });
|
||||
else cache.recordWorkflow(input);
|
||||
expect(
|
||||
ledger.record(
|
||||
createEvidence(subject ?? undefined, provider, {
|
||||
operation,
|
||||
parameters,
|
||||
result,
|
||||
analysisProfile,
|
||||
confidence: kind === "provider" ? "observed" : "derived",
|
||||
}),
|
||||
).ok,
|
||||
).toBe(true);
|
||||
}
|
||||
const before =
|
||||
kind === "provider" ? cache.entries() : cache.workflowEntries();
|
||||
expect(before).toHaveLength(3);
|
||||
const evidence = ledger.export();
|
||||
const exported = cache.export(
|
||||
target,
|
||||
profile,
|
||||
evidenceBundleForTarget(evidence, target.sha256),
|
||||
);
|
||||
if (!exported.ok) throw exported.error;
|
||||
const snapshot = parseAnalysisSnapshot(
|
||||
JSON.parse(serializeAnalysisSnapshot(exported.value)),
|
||||
);
|
||||
const saved =
|
||||
kind === "provider" ? snapshot.entries : snapshot.workflow_entries;
|
||||
expect(saved).toHaveLength(1);
|
||||
expect(saved[0]?.execution.subject?.sha256).toBe(target.sha256);
|
||||
expect(snapshot.evidence_bundle.records).toHaveLength(1);
|
||||
expect(
|
||||
kind === "provider" ? cache.entries() : cache.workflowEntries(),
|
||||
).toEqual(before);
|
||||
expect(ledger.export()).toEqual(evidence);
|
||||
const restored = new AnalysisSnapshotCache();
|
||||
expect(restored.stage(snapshot)).toBe(1);
|
||||
expect(
|
||||
kind === "provider" ? restored.entries() : restored.workflowEntries(),
|
||||
).toEqual(saved);
|
||||
if (kind === "provider") {
|
||||
expect(
|
||||
cache.lookup(target, profile, "procedure_info", { address: "0x1000" })
|
||||
?.subject,
|
||||
).toBeNull();
|
||||
expect(
|
||||
cache.lookup(target, profile, "procedure_info", { address: "0x2000" })
|
||||
?.subject?.sha256,
|
||||
).toBe(child.sha256);
|
||||
}
|
||||
},
|
||||
);
|
||||
|
||||
it("counts newly retained direct and workflow bindings when staging", () => {
|
||||
const source = new AnalysisSnapshotCache();
|
||||
const target = ANALYSIS_SNAPSHOT_TARGET;
|
||||
|
||||
@@ -291,13 +291,19 @@ export class AnalysisSnapshotCache {
|
||||
try {
|
||||
const snapshotTargetIdentity = snapshotTarget(target);
|
||||
const binding = snapshotBinding(profile);
|
||||
const entries = [...this.#entries.values()].sort((left, right) =>
|
||||
compareUnicodeCodePoints(left.query_id, right.query_id),
|
||||
);
|
||||
const workflows = [...this.#workflowEntries.values()].sort(
|
||||
(left, right) =>
|
||||
// In-memory replay can preserve unbound and child-artifact observations.
|
||||
// A persisted target snapshot can authenticate only rows whose Evidence
|
||||
// has that target subject; the record owner applies the same scope to its bundle.
|
||||
const entries = [...this.#entries.values()]
|
||||
.filter((entry) => entry.execution.subject?.sha256 === target.sha256)
|
||||
.sort((left, right) =>
|
||||
compareUnicodeCodePoints(left.query_id, right.query_id),
|
||||
);
|
||||
);
|
||||
const workflows = [...this.#workflowEntries.values()]
|
||||
.filter((entry) => entry.execution.subject?.sha256 === target.sha256)
|
||||
.sort((left, right) =>
|
||||
compareUnicodeCodePoints(left.query_id, right.query_id),
|
||||
);
|
||||
// Parsing owns the returned JSON and metadata; pre-cloning the same
|
||||
// payloads here only adds another full materialization.
|
||||
return ok(
|
||||
|
||||
@@ -73,6 +73,81 @@ it("preserves an explicitly unbound provider observation", async () => {
|
||||
}
|
||||
});
|
||||
|
||||
it("keeps unbound observations out of persisted target snapshots", async () => {
|
||||
const { session, deep } = openSession();
|
||||
const directory = await createTestTempDirectory("rea-unbound-snapshot-");
|
||||
const path = join(directory, "snapshot.json");
|
||||
const parameters = { document: "0", address: "0x1000" };
|
||||
try {
|
||||
expect((await session.open(process.execPath)).ok).toBe(true);
|
||||
const bound = await session.execute("address_name", parameters);
|
||||
expect(bound.ok).toBe(true);
|
||||
const child = deep.created[0];
|
||||
if (child === undefined || !bound.ok)
|
||||
throw new Error("Expected bound read");
|
||||
child.execute = () =>
|
||||
Promise.resolve(
|
||||
ok(
|
||||
createAnalysisExecution("unbound observation", deepIdentity, {
|
||||
subject: null,
|
||||
}),
|
||||
),
|
||||
);
|
||||
const execution = await session.execute("address_name", {
|
||||
...parameters,
|
||||
address: "0x2000",
|
||||
});
|
||||
expect(execution.ok).toBe(true);
|
||||
if (!execution.ok) return;
|
||||
expect(execution.value.subject).toBeNull();
|
||||
expect(session.exportEvidenceBundle().records).toEqual(
|
||||
expect.arrayContaining([
|
||||
expect.objectContaining({
|
||||
subject: null,
|
||||
normalized_result: "unbound observation",
|
||||
}),
|
||||
]),
|
||||
);
|
||||
const snapshot = session.exportAnalysisSnapshot();
|
||||
expect(snapshot.ok, snapshot.ok ? undefined : snapshot.error.message).toBe(
|
||||
true,
|
||||
);
|
||||
if (!snapshot.ok) return;
|
||||
expect(snapshot.value.entries).toHaveLength(1);
|
||||
expect(snapshot.value.entries[0]?.parameters).toEqual(parameters);
|
||||
const unboundReplay = await session.execute("address_name", {
|
||||
...parameters,
|
||||
address: "0x2000",
|
||||
});
|
||||
expect(unboundReplay.ok).toBe(true);
|
||||
if (unboundReplay.ok) {
|
||||
expect(unboundReplay.value.subject).toBeNull();
|
||||
expect(unboundReplay.value.evidenceId).toBe(execution.value.evidenceId);
|
||||
}
|
||||
expect(
|
||||
(await session.writeAnalysisSnapshot(snapshot.value, path, false)).ok,
|
||||
).toBe(true);
|
||||
expect((await session.close()).ok).toBe(true);
|
||||
expect(
|
||||
(
|
||||
await session.open(process.execPath, {
|
||||
loadSnapshot: () => session.readAnalysisSnapshot(path),
|
||||
})
|
||||
).ok,
|
||||
).toBe(true);
|
||||
const replay = await session.execute("address_name", parameters);
|
||||
expect(replay.ok).toBe(true);
|
||||
if (replay.ok) {
|
||||
expect(replay.value.evidenceId).toBe(bound.value.evidenceId);
|
||||
expect(replay.value.limitations).toContain(
|
||||
"Loaded from a local REA analysis snapshot; this call did not re-run the provider.",
|
||||
);
|
||||
}
|
||||
} finally {
|
||||
await session.close();
|
||||
}
|
||||
});
|
||||
|
||||
describe("BinarySession open liveness", () => {
|
||||
it("retires a locally blocked owner even when its native health is idle", async () => {
|
||||
const { session, deep } = openSession();
|
||||
|
||||
@@ -213,8 +213,12 @@ const makeProvider = (
|
||||
},
|
||||
],
|
||||
list_documents: ["fixture"],
|
||||
list_procedures: ["0x1000"],
|
||||
list_strings: { "0x1000": "fixture" },
|
||||
list_procedures: [
|
||||
{ address: "0x1000", value: "fixture" },
|
||||
] satisfies z.infer<typeof nativeInventoryRowsSchemas.names>,
|
||||
list_strings: [
|
||||
{ address: "0x1000", value: "fixture" },
|
||||
] satisfies z.infer<typeof nativeInventoryRowsSchemas.strings>,
|
||||
analyze_function: functionDossier,
|
||||
list_names: [
|
||||
{ address: "0x1000", value: "fixture" },
|
||||
|
||||
Reference in New Issue
Block a user