From 2bbcbee1dde4b9509c6ffe4c38749cf819567a5c Mon Sep 17 00:00:00 2001 From: Tanish Valesha Date: Wed, 22 Jul 2026 23:39:10 +0530 Subject: [PATCH 1/3] add a new command for auditing dedupe claims --- apps/cli/main.ts | 26 ++++++++ libs/knowledge-graph/local-provider.ts | 4 ++ libs/knowledge-graph/managed-client.ts | 6 +- libs/knowledge-graph/provider.ts | 2 + libs/knowledge-graph/service.ts | 92 +++++++++++++++++++++++++- 5 files changed, 128 insertions(+), 2 deletions(-) diff --git a/apps/cli/main.ts b/apps/cli/main.ts index f21ea34..651ceed 100644 --- a/apps/cli/main.ts +++ b/apps/cli/main.ts @@ -191,6 +191,13 @@ const cliCommands = [ handler: withCommandContext(runGraphAuditAnchorsCommand), showInTopLevelHelp: true, }, + { + key: "graphAuditDuplicates", + path: ["graph", "audit", "duplicates"], + usage: "graph audit duplicates", + handler: withCommandContext(runGraphAuditDuplicatesCommand), + showInTopLevelHelp: true, + }, { key: "graphExport", path: ["graph", "export"], @@ -416,6 +423,25 @@ async function runGraphAuditAnchorsCommand(_args: string[], getContext: CommandC if (anchorAuditIssueCount(result) > 0) process.exitCode = 1; } +async function runGraphAuditDuplicatesCommand(_args: string[], getContext: CommandContextProvider): Promise { + const { service } = getContext(); + const result = await service.auditDuplicateClaims(); + console.log("Duplicate claims audit:"); + console.log(`Total active claims checked: ${result.total_claims}`); + if (result.groups.length === 0) { + console.log("- No duplicate claim groups found."); + return; + } + for (const group of result.groups) { + const truncated = group.claim_text.length > 80 ? group.claim_text.slice(0, 80) + "..." : group.claim_text; + console.log(`- "${group.claim_id}" ("${truncated}")`); + for (const dup of group.duplicates) { + const dupTruncated = dup.claim_text.length > 80 ? dup.claim_text.slice(0, 80) + "..." : dup.claim_text; + console.log(` → "${dup.claim_id}" (${dup.similarity.toFixed(4)}) "${dupTruncated}"`); + } + } +} + async function runGraphExportCommand(args: string[], getContext: CommandContextProvider): Promise { const outputDir = requireFile(args[0], usage("graphExport")); const { service } = getContext(); diff --git a/libs/knowledge-graph/local-provider.ts b/libs/knowledge-graph/local-provider.ts index d2631b4..d90a92e 100644 --- a/libs/knowledge-graph/local-provider.ts +++ b/libs/knowledge-graph/local-provider.ts @@ -38,6 +38,10 @@ export class LocalGraphMemoryProvider implements GraphMemoryProvider { return this.service.auditCodeAnchors(this.repo); } + async auditDuplicateClaims() { + return this.service.auditDuplicateClaims(this.repo); + } + async reviewProposal(proposal: unknown) { return this.service.validateProposal(this.repo, proposal); } diff --git a/libs/knowledge-graph/managed-client.ts b/libs/knowledge-graph/managed-client.ts index 823c40e..6c3f580 100644 --- a/libs/knowledge-graph/managed-client.ts +++ b/libs/knowledge-graph/managed-client.ts @@ -15,7 +15,7 @@ import { openDatabase } from "../storage/sqlite/db.js"; import { buildGraphViewHtmlFromData, type GraphViewData } from "./graph-view/build-graph-view.js"; import { normalizeProposal } from "./proposal.js"; import type { GraphMemoryProvider, ManagedProposalReviewResult } from "./provider.js"; -import type { ApplyProposalResult, GraphReadResult, RepoRef } from "./service.js"; +import type { ApplyProposalResult, DuplicateAuditResult, GraphReadResult, RepoRef } from "./service.js"; import type { GraphContextResult } from "./graph-context/types.js"; export interface ManagedGraphClientOptions { @@ -101,6 +101,10 @@ export class ManagedGraphMemoryClient implements GraphMemoryProvider { ); } + async auditDuplicateClaims(): Promise { + return this.request("/graph/audit-duplicates", { method: "GET" }); + } + async reviewProposal(proposal: unknown): Promise { const anchorAudit = await this.proposalAnchorAudit(proposal); if (anchorAudit.result.missing_anchors.length > 0 || diff --git a/libs/knowledge-graph/provider.ts b/libs/knowledge-graph/provider.ts index 5d135c3..ce130b5 100644 --- a/libs/knowledge-graph/provider.ts +++ b/libs/knowledge-graph/provider.ts @@ -4,6 +4,7 @@ import type { GraphContextResult } from "./graph-context/types.js"; import type { GraphViewData } from "./graph-view/build-graph-view.js"; import type { ApplyProposalResult, + DuplicateAuditResult, GraphReadResult, ProposalReviewResult, } from "./service.js"; @@ -23,6 +24,7 @@ export interface GraphMemoryProvider { viewData(): Promise; buildGraphView(): Promise; auditCodeAnchors(): Promise; + auditDuplicateClaims(): Promise; reviewProposal(proposal: unknown): Promise; applyProposal(proposal: unknown): Promise; close(): void; diff --git a/libs/knowledge-graph/service.ts b/libs/knowledge-graph/service.ts index 29bb794..02c627a 100644 --- a/libs/knowledge-graph/service.ts +++ b/libs/knowledge-graph/service.ts @@ -17,7 +17,7 @@ import { SqliteRepository as SqliteKnowledgeGraphRepository } from "../storage/s import { bufferToFloat32Array } from "./graph-context/vector.js"; import { createEmbedder } from "./graph-context/embedder.js"; import { buildClaimDocuments } from "./graph-context/documents.js"; -import { findSimilarClaims, type SimilarClaimMatch } from "./dedupe/find-similar-claims.js"; +import { findSimilarClaims, type SimilarClaimMatch, type ClaimEmbeddingCandidate } from "./dedupe/find-similar-claims.js"; export type { GraphContextResult } from "./graph-context/types.js"; export type { ClaimAnchorAuditResult } from "./code-anchors/types.js"; @@ -62,6 +62,17 @@ export interface ProposalReviewResult extends ProposalValidationResult { duplicate_warnings: Record; } +export interface DuplicateAuditGroup { + claim_id: string; + claim_text: string; + duplicates: Array<{ claim_id: string; claim_text: string; similarity: number }>; +} + +export interface DuplicateAuditResult { + total_claims: number; + groups: DuplicateAuditGroup[]; +} + export class KnowledgeGraphService { constructor( private readonly repository: SqliteRepository, @@ -152,6 +163,85 @@ export class KnowledgeGraphService { return auditClaimCodeAnchors(input.repo_root, claims, new CodeAnchorResolver(), baselineFingerprints); } + async auditDuplicateClaims(input: RepoRef): Promise { + const initialized = this.requireRepo(input); + const graph = this.repository.readGraphView(initialized.repo_id); + if (graph.claims.length === 0) { + return { total_claims: 0, groups: [] }; + } + + const supersededIds = new Set( + this.repository.readSupersededClaims(initialized.repo_id).map((claim) => claim.id), + ); + const activeClaims = graph.claims.filter((claim) => !supersededIds.has(claim.id)); + if (activeClaims.length === 0) { + return { total_claims: graph.claims.length, groups: [] }; + } + + const storedVectors = new Map( + this.repository + .listGraphObjectEmbeddings({ + repo_id: initialized.repo_id, + provider: this.contextConfig.embedding.provider, + model: this.contextConfig.embedding.model, + dimensions: this.contextConfig.embedding.dimensions, + }) + .filter((record) => record.object_type === "claim") + .map((record) => [record.object_id, bufferToFloat32Array(record.embedding)]), + ); + + const activeDocuments = buildClaimDocuments({ ...graph, claims: activeClaims }); + const existingVectors: ClaimEmbeddingCandidate[] = []; + const missingDocs: typeof activeDocuments = []; + for (const doc of activeDocuments) { + const vector = storedVectors.get(doc.id); + if (vector === undefined) { + missingDocs.push(doc); + } else { + existingVectors.push({ claim_id: doc.id, vector }); + } + } + + if (missingDocs.length > 0) { + const embedder = createEmbedder(this.contextConfig.embedding); + const generated = await embedder.embedBatch(missingDocs.map((d) => d.text)); + for (const [index, doc] of missingDocs.entries()) { + const vector = new Float32Array(generated[index]); + existingVectors.push({ claim_id: doc.id, vector }); + } + } + + const seen = new Set(); + const claimText = new Map(activeClaims.map((c) => [c.id, c.text])); + const groups: DuplicateAuditGroup[] = []; + + for (const claim of activeClaims) { + if (seen.has(claim.id)) continue; + const vector = existingVectors.find((v) => v.claim_id === claim.id)?.vector; + if (vector === undefined) continue; + const others = existingVectors.filter((v) => v.claim_id !== claim.id && !seen.has(v.claim_id)); + const matches = findSimilarClaims(vector, others, this.contextConfig.dedupe.similarityThreshold) + .filter((m) => m.claim_id !== claim.id) + .map((m) => ({ + claim_id: m.claim_id, + claim_text: claimText.get(m.claim_id) ?? "", + similarity: m.similarity, + })); + if (matches.length > 0) { + groups.push({ + claim_id: claim.id, + claim_text: claim.text, + duplicates: matches, + }); + seen.add(claim.id); + for (const m of matches) seen.add(m.claim_id); + } + } + + groups.sort((a, b) => b.duplicates[0].similarity - a.duplicates[0].similarity); + return { total_claims: activeClaims.length, groups }; + } + async validateProposal(input: RepoRef, proposal: unknown): Promise { const initialized = this.requireRepo(input); const normalizedProposal = normalizeProposal(proposal, this.subjectLookup(initialized.repo_id)); From ada12c1d7b21f16b4beda8383d108fb33acc9f1e Mon Sep 17 00:00:00 2001 From: Tanish Valesha Date: Thu, 23 Jul 2026 00:29:07 +0530 Subject: [PATCH 2/3] print the total duplicates found --- apps/cli/main.ts | 2 ++ 1 file changed, 2 insertions(+) diff --git a/apps/cli/main.ts b/apps/cli/main.ts index 651ceed..79200a4 100644 --- a/apps/cli/main.ts +++ b/apps/cli/main.ts @@ -432,6 +432,8 @@ async function runGraphAuditDuplicatesCommand(_args: string[], getContext: Comma console.log("- No duplicate claim groups found."); return; } + const totalDuplicates = result.groups.reduce((sum, group) => sum + group.duplicates.length, 0); + console.log(`Total duplicate pairs found: ${totalDuplicates}`); for (const group of result.groups) { const truncated = group.claim_text.length > 80 ? group.claim_text.slice(0, 80) + "..." : group.claim_text; console.log(`- "${group.claim_id}" ("${truncated}")`); From 7158d2c2455a8c9c132b9a23f09a536cfcfc7305 Mon Sep 17 00:00:00 2001 From: Kushal Date: Tue, 28 Jul 2026 16:31:03 -0700 Subject: [PATCH 3/3] fix: complete duplicate audit contracts --- libs/knowledge-graph/managed-client.ts | 5 +- libs/knowledge-graph/service.ts | 25 ++- libs/managed/protocol.ts | 17 ++ package.json | 2 +- scripts/check-duplicate-audit.js | 225 +++++++++++++++++++++++++ 5 files changed, 268 insertions(+), 6 deletions(-) create mode 100644 scripts/check-duplicate-audit.js diff --git a/libs/knowledge-graph/managed-client.ts b/libs/knowledge-graph/managed-client.ts index 6c3f580..cf97c6a 100644 --- a/libs/knowledge-graph/managed-client.ts +++ b/libs/knowledge-graph/managed-client.ts @@ -15,8 +15,9 @@ import { openDatabase } from "../storage/sqlite/db.js"; import { buildGraphViewHtmlFromData, type GraphViewData } from "./graph-view/build-graph-view.js"; import { normalizeProposal } from "./proposal.js"; import type { GraphMemoryProvider, ManagedProposalReviewResult } from "./provider.js"; -import type { ApplyProposalResult, DuplicateAuditResult, GraphReadResult, RepoRef } from "./service.js"; +import type { ApplyProposalResult, GraphReadResult, RepoRef } from "./service.js"; import type { GraphContextResult } from "./graph-context/types.js"; +import type { ManagedDuplicateAudit } from "../managed/protocol.js"; export interface ManagedGraphClientOptions { apiUrl: string; @@ -101,7 +102,7 @@ export class ManagedGraphMemoryClient implements GraphMemoryProvider { ); } - async auditDuplicateClaims(): Promise { + async auditDuplicateClaims(): Promise { return this.request("/graph/audit-duplicates", { method: "GET" }); } diff --git a/libs/knowledge-graph/service.ts b/libs/knowledge-graph/service.ts index 02c627a..316e7b7 100644 --- a/libs/knowledge-graph/service.ts +++ b/libs/knowledge-graph/service.ts @@ -175,7 +175,7 @@ export class KnowledgeGraphService { ); const activeClaims = graph.claims.filter((claim) => !supersededIds.has(claim.id)); if (activeClaims.length === 0) { - return { total_claims: graph.claims.length, groups: [] }; + return { total_claims: 0, groups: [] }; } const storedVectors = new Map( @@ -187,7 +187,15 @@ export class KnowledgeGraphService { dimensions: this.contextConfig.embedding.dimensions, }) .filter((record) => record.object_type === "claim") - .map((record) => [record.object_id, bufferToFloat32Array(record.embedding)]), + .map((record) => { + const vector = bufferToFloat32Array(record.embedding); + if (vector.length !== this.contextConfig.embedding.dimensions) { + throw new Error( + `Stored embedding for claim ${record.object_id} has ${vector.length} dimensions; expected ${this.contextConfig.embedding.dimensions}.`, + ); + } + return [record.object_id, vector] as const; + }), ); const activeDocuments = buildClaimDocuments({ ...graph, claims: activeClaims }); @@ -205,8 +213,19 @@ export class KnowledgeGraphService { if (missingDocs.length > 0) { const embedder = createEmbedder(this.contextConfig.embedding); const generated = await embedder.embedBatch(missingDocs.map((d) => d.text)); + if (generated.length !== missingDocs.length) { + throw new Error( + `Embedding provider returned ${generated.length} vectors for ${missingDocs.length} missing claims.`, + ); + } for (const [index, doc] of missingDocs.entries()) { - const vector = new Float32Array(generated[index]); + const values = generated[index]; + if (values === undefined || values.length !== this.contextConfig.embedding.dimensions) { + throw new Error( + `Embedding for claim ${doc.id} has ${values?.length ?? 0} dimensions; expected ${this.contextConfig.embedding.dimensions}.`, + ); + } + const vector = new Float32Array(values); existingVectors.push({ claim_id: doc.id, vector }); } } diff --git a/libs/managed/protocol.ts b/libs/managed/protocol.ts index 915079f..f54a996 100644 --- a/libs/managed/protocol.ts +++ b/libs/managed/protocol.ts @@ -221,6 +221,21 @@ export const ProposalAnchorAuditSchema = Type.Object({ fingerprints: Type.Record(Type.String(), Type.Record(Type.String(), Type.String())), }); +export const DuplicateAuditMatchSchema = Type.Object({ + claim_id: Type.String(), + claim_text: Type.String(), + similarity: Type.Number(), +}); +export const DuplicateAuditGroupSchema = Type.Object({ + claim_id: Type.String(), + claim_text: Type.String(), + duplicates: Type.Array(DuplicateAuditMatchSchema), +}); +export const DuplicateAuditSchema = Type.Object({ + total_claims: Type.Integer({ minimum: 0 }), + groups: Type.Array(DuplicateAuditGroupSchema), +}); + export const ProposalReviewSchema = Type.Object({ valid: Type.Boolean(), errors: Type.Array(Type.String()), @@ -481,6 +496,7 @@ export const routeSchemas = { claims: Type.Array(ClaimSchema), fingerprints: Type.Record(Type.String(), Type.Record(Type.String(), Type.String())), })), + graphAuditDuplicates: route(Type.Object({}), DuplicateAuditSchema), proposalReview: route(Type.Object({ proposal: MemoryProposalSchema, anchor_audit: ProposalAnchorAuditSchema }), ProposalReviewSchema), proposalApply: route(Type.Object({ proposal: MemoryProposalSchema, @@ -507,6 +523,7 @@ export type ManagedAccessRequest = Static; export type ManagedGraphRead = Static; export type ManagedGraphContext = Static; export type ManagedGraphViewData = Static; +export type ManagedDuplicateAudit = Static; export type ManagedProposalReview = Static; function route(request: TRequest, response: TResponse) { diff --git a/package.json b/package.json index 171aaab..4647c20 100644 --- a/package.json +++ b/package.json @@ -24,7 +24,7 @@ "smoke:copilot": "npm run build && node scripts/smoke-copilot-install.mjs", "smoke:opencode": "npm run build && node scripts/smoke-opencode-install.mjs", "smoke:cursor": "npm run build && node scripts/smoke-cursor-install.mjs", - "test": "npm run build && node scripts/check-transcript-bundle.js && node scripts/check-repo-context.js && node scripts/check-install-options.js && node scripts/check-repo-installations.js && node scripts/check-managed-cli.js && node scripts/check-graph-view.js && node scripts/check-graph-view-offline-browser.js && node scripts/check-source-memberships.js && node scripts/check-proposal-validate.js && node scripts/check-bm25-tokenizer.js && node scripts/check-anchor-drift.js && node scripts/check-find-similar-claims.js && node scripts/check-apply-proposal-dedupe.js && node scripts/check-opencode-sqlite-transcript.js", + "test": "npm run build && node scripts/check-transcript-bundle.js && node scripts/check-repo-context.js && node scripts/check-install-options.js && node scripts/check-repo-installations.js && node scripts/check-managed-cli.js && node scripts/check-graph-view.js && node scripts/check-graph-view-offline-browser.js && node scripts/check-source-memberships.js && node scripts/check-proposal-validate.js && node scripts/check-bm25-tokenizer.js && node scripts/check-anchor-drift.js && node scripts/check-find-similar-claims.js && node scripts/check-apply-proposal-dedupe.js && node scripts/check-opencode-sqlite-transcript.js && node scripts/check-duplicate-audit.js", "test:repo-installations": "npm run build && node scripts/check-repo-installations.js", "test:transcript-bundle": "npm run build && node scripts/check-transcript-bundle.js", "test:repo-context": "npm run build && node scripts/check-repo-context.js", diff --git a/scripts/check-duplicate-audit.js b/scripts/check-duplicate-audit.js new file mode 100644 index 0000000..2ec73a7 --- /dev/null +++ b/scripts/check-duplicate-audit.js @@ -0,0 +1,225 @@ +import assert from "node:assert/strict"; +import { execFileSync } from "node:child_process"; +import { mkdirSync, mkdtempSync } from "node:fs"; +import { tmpdir } from "node:os"; +import { join } from "node:path"; +import { fileURLToPath } from "node:url"; +import { Value } from "@sinclair/typebox/value"; + +const temporary = mkdtempSync(join(tmpdir(), "greplica-duplicate-audit-")); +process.env.GREPLICA_HOME = join(temporary, "greplica-home"); + +const root = new URL("..", import.meta.url); +const cliPath = fileURLToPath(new URL("dist/apps/cli/main.js", root)); +const { graphContextConfig } = await import( + new URL("dist/libs/knowledge-graph/graph-context/config.js", root) +); +const { ManagedGraphMemoryClient } = await import( + new URL("dist/libs/knowledge-graph/managed-client.js", root) +); +const { KnowledgeGraphService } = await import( + new URL("dist/libs/knowledge-graph/service.js", root) +); +const { routeSchemas } = await import(new URL("dist/libs/managed/protocol.js", root)); +const { openDatabase } = await import(new URL("dist/libs/storage/sqlite/db.js", root)); +const { SqliteRepository } = await import( + new URL("dist/libs/storage/sqlite/repository.js", root) +); +const { RepoInstallationStore } = await import( + new URL("dist/libs/install/repo-installation-store.js", root) +); + +const embeddingConfig = { + ...graphContextConfig, + dedupe: { similarityThreshold: 0.9 }, +}; + +const repoRoot = join(temporary, "repo"); +mkdirSync(repoRoot, { recursive: true }); +execFileSync("git", ["init", "--quiet", "--initial-branch=main"], { cwd: repoRoot }); +const repo = { + repo_root: repoRoot, + repo_name: "repo", + default_branch: "main", +}; +const db = openDatabase(); +const repository = new SqliteRepository(db); +const installation = new RepoInstallationStore(db).activateLocal(repo, { + hooksEnabled: false, + autoMemoryUpdates: false, +}); +const service = new KnowledgeGraphService(repository, embeddingConfig); +const initialized = service.initRepo(repo); +const memoryCommit = repository.createMemoryCommit({ + scope_id: initialized.working_scope_id, + title: "Seed duplicate audit", +}); +repository.createProposalRecords(initialized.working_scope_id, memoryCommit.id, { + title: "Seed duplicate audit", + creates: { + claims: [ + claim("claim.alpha", "The worker closes its transcript before cleanup."), + claim("claim.alpha-copy", "The worker closes the transcript before cleanup."), + claim("claim.distinct", "The graph view renders a static HTML file."), + claim("claim.superseded", "An obsolete duplicate claim."), + ], + edges: [{ + id: "edge.supersede-obsolete", + from_id: "claim.distinct", + from_type: "claim", + to_id: "claim.superseded", + to_type: "claim", + kind: "supersedes", + }], + }, +}); +repository.insertGraphObjectEmbeddings([ + embedding(initialized.repo_id, "claim.alpha", vector(1, 0, 0)), + embedding(initialized.repo_id, "claim.alpha-copy", vector(0.99, 0.05, 0)), + embedding(initialized.repo_id, "claim.distinct", vector(0, 1, 0)), + embedding(initialized.repo_id, "claim.superseded", vector(1, 0, 0)), +]); + +const result = await service.auditDuplicateClaims(repo); +assert.equal(result.total_claims, 3, "superseded claims must not count as active"); +assert.equal(result.groups.length, 1); +assert.equal(result.groups[0].claim_id, "claim.alpha"); +assert.deepEqual( + result.groups[0].duplicates.map(({ claim_id }) => claim_id), + ["claim.alpha-copy"], +); +assert.equal( + result.groups.some((group) => + group.claim_id === "claim.superseded" || + group.duplicates.some(({ claim_id }) => claim_id === "claim.superseded")), + false, +); +db.close(); + +const output = execFileSync(process.execPath, [cliPath, "graph", "audit", "duplicates"], { + cwd: repoRoot, + encoding: "utf8", + env: { ...process.env, GREPLICA_HOME: process.env.GREPLICA_HOME }, +}); +assert.match(output, /Duplicate claims audit:/); +assert.match(output, /Total active claims checked: 3/); +assert.match(output, /Total duplicate pairs found: 1/); +assert.match(output, /claim\.alpha-copy/); +assert.doesNotMatch(output, /claim\.superseded/); + +const managedResult = { + total_claims: 2, + groups: [{ + claim_id: "claim.one", + claim_text: "One", + duplicates: [{ claim_id: "claim.two", claim_text: "Two", similarity: 0.95 }], + }], +}; +assert.equal(Value.Check(routeSchemas.graphAuditDuplicates.response, managedResult), true); +let managedRequest; +const managedClient = new ManagedGraphMemoryClient( + { + ...installation, + activeMode: "managed", + managedRepoId: "11111111-1111-4111-8111-111111111111", + }, + repo, + { + apiUrl: "https://managed.example.test", + token: "test-token", + fetchImpl: async (url, input) => { + managedRequest = { url: String(url), method: input?.method }; + return new Response(JSON.stringify(managedResult), { + status: 200, + headers: { "content-type": "application/json" }, + }); + }, + }, +); +assert.deepEqual(await managedClient.auditDuplicateClaims(), managedResult); +assert.deepEqual(managedRequest, { + url: "https://managed.example.test/v1/repos/11111111-1111-4111-8111-111111111111/graph/audit-duplicates", + method: "GET", +}); + +await assertEmbeddingIntegrity(); + +console.log("Duplicate audit checks passed."); + +async function assertEmbeddingIntegrity() { + const integrityDb = openDatabase(join(temporary, "integrity.db")); + try { + const integrityRepository = new SqliteRepository(integrityDb); + const integrityConfig = { + ...embeddingConfig, + embedding: { + provider: "openai", + apiKey: "test-key", + model: "test-model", + dimensions: 3, + batchSize: 16, + }, + }; + const integrityService = new KnowledgeGraphService(integrityRepository, integrityConfig); + const integrityRepo = { + repo_root: join(temporary, "integrity-repo"), + repo_name: "integrity-repo", + default_branch: "main", + }; + const integrityInitialized = integrityService.initRepo(integrityRepo); + const commit = integrityRepository.createMemoryCommit({ + scope_id: integrityInitialized.working_scope_id, + title: "Seed missing embedding", + }); + integrityRepository.createProposalRecords(integrityInitialized.working_scope_id, commit.id, { + title: "Seed missing embedding", + creates: { claims: [claim("claim.missing", "This claim has no stored embedding.")] }, + }); + + const originalFetch = globalThis.fetch; + globalThis.fetch = async () => new Response(JSON.stringify({ data: [] }), { + status: 200, + headers: { "content-type": "application/json" }, + }); + try { + await assert.rejects( + integrityService.auditDuplicateClaims(integrityRepo), + /returned 0 vectors for 1 missing claims/, + ); + } finally { + globalThis.fetch = originalFetch; + } + } finally { + integrityDb.close(); + } +} + +function claim(id, text) { + return { + id, + kind: "fact", + text, + truth: "source_verified", + intent: "intended", + }; +} + +function embedding(repoId, claimId, vector) { + return { + repo_id: repoId, + object_type: "claim", + object_id: claimId, + provider: embeddingConfig.embedding.provider, + model: embeddingConfig.embedding.model, + dimensions: embeddingConfig.embedding.dimensions, + embedding: Buffer.from(new Float32Array(vector).buffer), + }; +} + +function vector(first, second, third) { + const result = Array(embeddingConfig.embedding.dimensions).fill(0); + result[0] = first; + result[1] = second; + result[2] = third; + return result; +}