diff --git a/extensions/memory-wiki/src/claim-health.test.ts b/extensions/memory-wiki/src/claim-health.test.ts index 6b871f3f9256..dd0b627bb8b1 100644 --- a/extensions/memory-wiki/src/claim-health.test.ts +++ b/extensions/memory-wiki/src/claim-health.test.ts @@ -1,7 +1,7 @@ // Memory Wiki tests cover claim health plugin behavior. import { describe, expect, it } from "vitest"; -import { buildPageContradictionClusters } from "./claim-health.js"; -import type { WikiPageSummary } from "./markdown.js"; +import { assessClaimFreshness, buildPageContradictionClusters } from "./claim-health.js"; +import type { WikiClaim, WikiPageSummary } from "./markdown.js"; function createPage(params: { relativePath: string; @@ -64,3 +64,103 @@ describe("buildPageContradictionClusters", () => { expect(clusters.every((cluster) => cluster.entries)).toBe(true); }); }); + +describe("assessClaimFreshness", () => { + it("uses the latest claim evidence timestamp without relying on page freshness", () => { + const page = createPage({ + relativePath: "entities/alpha.md", + title: "Alpha", + contradictions: [], + }); + page.updatedAt = "2026-01-01T00:00:00.000Z"; + const claim: WikiClaim = { + text: "Alpha prefers current evidence.", + updatedAt: "2026-01-05T00:00:00.000Z", + evidence: [ + { updatedAt: "2026-01-03T00:00:00.000Z" }, + { updatedAt: "2026-01-20T00:00:00.000Z" }, + ], + }; + + const freshness = assessClaimFreshness({ + page, + claim, + now: new Date("2026-01-25T00:00:00.000Z"), + }); + + expect(freshness.level).toBe("fresh"); + expect(freshness.lastTouchedAt).toBe("2026-01-20T00:00:00.000Z"); + expect(freshness.daysSinceTouch).toBe(5); + }); + + it("does not let a newer page timestamp make stale claim evidence fresh", () => { + const page = createPage({ + relativePath: "entities/beta.md", + title: "Beta", + contradictions: [], + }); + page.updatedAt = "2026-04-20T00:00:00.000Z"; + const claim: WikiClaim = { + text: "Beta still needs old evidence checked.", + updatedAt: "2026-01-01T00:00:00.000Z", + evidence: [{ updatedAt: "2026-01-05T00:00:00.000Z" }], + }; + + const freshness = assessClaimFreshness({ + page, + claim, + now: new Date("2026-04-20T00:00:00.000Z"), + }); + + expect(freshness.level).toBe("stale"); + expect(freshness.lastTouchedAt).toBe("2026-01-05T00:00:00.000Z"); + expect(freshness.daysSinceTouch).toBe(105); + }); + + it("falls back to page freshness when claim and evidence timestamps are absent", () => { + const page = createPage({ + relativePath: "entities/gamma.md", + title: "Gamma", + contradictions: [], + }); + page.updatedAt = "2026-04-20T00:00:00.000Z"; + const claim: WikiClaim = { + text: "Gamma was written through wiki_apply without per-claim timestamps.", + evidence: [{ kind: "session", sourceId: "session-1" }], + }; + + const freshness = assessClaimFreshness({ + page, + claim, + now: new Date("2026-04-25T00:00:00.000Z"), + }); + + expect(freshness.level).toBe("fresh"); + expect(freshness.lastTouchedAt).toBe("2026-04-20T00:00:00.000Z"); + expect(freshness.daysSinceTouch).toBe(5); + }); + + it("keeps malformed claim timestamps unknown instead of using page freshness", () => { + const page = createPage({ + relativePath: "entities/delta.md", + title: "Delta", + contradictions: [], + }); + page.updatedAt = "2026-04-20T00:00:00.000Z"; + const claim: WikiClaim = { + text: "Delta has malformed claim freshness metadata.", + updatedAt: "not-a-date", + evidence: [], + }; + + const freshness = assessClaimFreshness({ + page, + claim, + now: new Date("2026-04-25T00:00:00.000Z"), + }); + + expect(freshness.level).toBe("unknown"); + expect(freshness.lastTouchedAt).toBeUndefined(); + expect(freshness.reason).toBe("missing updatedAt"); + }); +}); diff --git a/extensions/memory-wiki/src/claim-health.ts b/extensions/memory-wiki/src/claim-health.ts index 7be5cb7662f2..8b36ff5b0ac7 100644 --- a/extensions/memory-wiki/src/claim-health.ts +++ b/extensions/memory-wiki/src/claim-health.ts @@ -136,12 +136,25 @@ export function assessClaimFreshness(params: { claim: WikiClaim; now?: Date; }): WikiFreshness { - const latestTimestamp = resolveLatestTimestamp([ - params.claim.updatedAt, - params.page.updatedAt, - ...params.claim.evidence.map((evidence) => evidence.updatedAt), - ]); - return buildFreshnessFromTimestamp({ timestamp: latestTimestamp, now: params.now }); + let hasClaimTimestamp = typeof params.claim.updatedAt === "string" && + params.claim.updatedAt.trim().length > 0; + let latestTimestamp = resolveLatestTimestamp([params.claim.updatedAt]); + let latestMs = parseTimestamp(latestTimestamp) ?? -1; + for (const evidence of params.claim.evidence) { + if (typeof evidence.updatedAt === "string" && evidence.updatedAt.trim().length > 0) { + hasClaimTimestamp = true; + } + const evidenceMs = parseTimestamp(evidence.updatedAt); + if (evidenceMs === null || !evidence.updatedAt || evidenceMs <= latestMs) { + continue; + } + latestMs = evidenceMs; + latestTimestamp = evidence.updatedAt; + } + return buildFreshnessFromTimestamp({ + timestamp: latestTimestamp ?? (hasClaimTimestamp ? undefined : params.page.updatedAt), + now: params.now, + }); } function buildWikiClaimHealth(params: {