diff --git a/src/recall/projections/operations.ts b/src/recall/projections/operations.ts index ec24f926..f5c473fc 100644 --- a/src/recall/projections/operations.ts +++ b/src/recall/projections/operations.ts @@ -423,7 +423,12 @@ export function createRecallProjectionOperations(input: { if (!sameIds(edges, expectedEdges)) { issues.push("entity_adjacency_set_mismatch"); } - const factIds = new Set( + const canonicalFactIds = new Set( + sources + .filter(({ collection }) => collection === "facts") + .map(({ id }) => id), + ); + const activeFactIds = new Set( sources .filter(({ collection, document }) => collection === "facts" && @@ -436,14 +441,14 @@ export function createRecallProjectionOperations(input: { status.sourceMemoryId, status, ])); - for (const sourceMemoryId of factIds) { + for (const sourceMemoryId of activeFactIds) { if (!statusBySource.has(sourceMemoryId)) { issues.push(`missing_claim_status:${sourceMemoryId}`); } } const claimsById = new Map(claims.map((claim) => [claim.id, claim])); for (const status of statuses) { - if (!factIds.has(status.sourceMemoryId)) { + if (!canonicalFactIds.has(status.sourceMemoryId)) { issues.push(`orphan_claim_status:${status.id}`); } if (status.scopeKey !== canonicalScopeKey) { @@ -478,7 +483,7 @@ export function createRecallProjectionOperations(input: { } } for (const claim of claims) { - if (!factIds.has(claim.sourceMemoryId)) { + if (!canonicalFactIds.has(claim.sourceMemoryId)) { issues.push(`orphan_claim:${claim.id}`); } if (claim.scopeKey !== canonicalScopeKey) { diff --git a/tests/unit/recall.claim-revision-visibility.test.ts b/tests/unit/recall.claim-revision-visibility.test.ts index 33ca2c45..36cbc7a0 100644 --- a/tests/unit/recall.claim-revision-visibility.test.ts +++ b/tests/unit/recall.claim-revision-visibility.test.ts @@ -500,6 +500,38 @@ describe("claim projection revision visibility", () => { ); }); + it("accepts retained claim history for a superseded canonical fact", async () => { + const store = createInMemoryDocumentStore(); + const fact = buildFact(); + const runtime = createRecallProjectionRuntime({ + documentStore: store, + now: () => observedAt, + }); + await runtime.documentStore.set("facts", fact.id, fact); + await runtime.appendClaim(claimInput("active", fact.updatedAt)); + const supersededFact = { + ...fact, + isActive: false, + lifecycle: "superseded" as const, + updatedAt: "2026-07-21T10:00:00.000Z", + }; + await runtime.documentStore.set("facts", fact.id, supersededFact); + const operations = createRecallProjectionOperations({ + analyzerFingerprint: null, + documentStore: store, + language: createLanguageService(), + now: () => observedAt, + }); + + expect( + await operations.validateScopeUnsafe( + scope, + [{ collection: "facts", document: supersededFact, id: fact.id }], + new Set(), + ), + ).toEqual({ complete: true, issues: [] }); + }); + it("rejects noncanonical claim and status scope keys", async () => { const store = createInMemoryDocumentStore(); const fact = buildFact("fact-forged-scope-key");