Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
13 changes: 9 additions & 4 deletions src/recall/projections/operations.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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" &&
Expand All @@ -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) {
Expand Down Expand Up @@ -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) {
Expand Down
32 changes: 32 additions & 0 deletions tests/unit/recall.claim-revision-visibility.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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");
Expand Down
Loading