From 86cc52adb8bbfba166a9f4e5b836711b3e00fe41 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Nathan=20=F0=9F=94=B6=20Tarbert?= <66887028+NathanTarbert@users.noreply.github.com> Date: Fri, 21 Aug 2026 19:53:15 -0400 Subject: [PATCH] fix: remove the knowledge modules #118 left unreachable (#118) #118 asked for the knowledge/ modules and the three tables to be dropped in a change that says so. This is the half that can be done now. server/src/knowledge/acl.ts, repository.ts and types.ts have no importer outside their own two test files. InMemoryKnowledgeRepository holds documents, chunks and ACLs in a Map in the process, which is the shape #21 took back, and canRead(actor, entries[]) filters ACL rows already pulled into memory rather than in SQL. Neither is a starting point for anything #119 describes. The three tables stay for now. PR #97 has not merged, so connectors/sync-persistence.ts still imports documents, chunks, document_acls, connector_cursors and sync_runs and writes to all of them; dropping the tables breaks typecheck there. It is already orphaned at runtime, its only caller being its own integration test, so this is a typecheck dependency rather than a live one, but it is #97's to remove. Left alone deliberately: connector_instances, which server/src/connectors.ts writes to from production code; connector_cursors and sync_runs, which are connector-side and go with #97; and webhook_subscriptions, which is referenced nowhere at all and wants its own change. Docs that describe pgvector holding knowledge records stay accurate while the tables exist and belong in the commit that drops them. --- server/src/knowledge/acl.ts | 22 --------- server/src/knowledge/repository.ts | 57 ----------------------- server/src/knowledge/types.ts | 9 ---- server/tests/knowledge-acl.test.ts | 30 ------------ server/tests/knowledge-repository.test.ts | 51 -------------------- 5 files changed, 169 deletions(-) delete mode 100644 server/src/knowledge/acl.ts delete mode 100644 server/src/knowledge/repository.ts delete mode 100644 server/src/knowledge/types.ts delete mode 100644 server/tests/knowledge-acl.test.ts delete mode 100644 server/tests/knowledge-repository.test.ts diff --git a/server/src/knowledge/acl.ts b/server/src/knowledge/acl.ts deleted file mode 100644 index d9dc3e97..00000000 --- a/server/src/knowledge/acl.ts +++ /dev/null @@ -1,22 +0,0 @@ -import type { KnowledgeAclEntry, KnowledgeActor } from "./types"; - -export function canRead( - actor: KnowledgeActor, - entries: KnowledgeAclEntry[], -): boolean { - let allowed = false; - - for (const entry of entries) { - if (!matchesPrincipal(actor, entry.principal)) continue; - if (entry.effect === "deny") return false; - allowed = true; - } - - return allowed; -} - -function matchesPrincipal(actor: KnowledgeActor, principal: string): boolean { - if (principal === `user:${actor.userId}`) return true; - if (!principal.startsWith("group:")) return false; - return actor.groups.includes(principal.slice("group:".length)); -} diff --git a/server/src/knowledge/repository.ts b/server/src/knowledge/repository.ts deleted file mode 100644 index 883cf9e3..00000000 --- a/server/src/knowledge/repository.ts +++ /dev/null @@ -1,57 +0,0 @@ -import { canRead } from "./acl"; -import type { KnowledgeAclEntry, KnowledgeActor } from "./types"; - -type SourceChange = { - connectorInstanceId: string; - sourceId: string; - title: string; - canonicalUrl: string; - contentHash: string; - chunks: { position: number; content: string }[]; - acls: KnowledgeAclEntry[]; -}; - -type Citation = { - documentId: string; - title: string; - canonicalUrl: string; - chunkId: string; - content: string; -}; - -export class InMemoryKnowledgeRepository { - #sources = new Map(); - #deleted = new Set(); - - apply(change: SourceChange) { - const key = sourceKey(change.connectorInstanceId, change.sourceId); - this.#sources.set(key, structuredClone(change)); - this.#deleted.delete(key); - } - - delete(connectorInstanceId: string, sourceId: string) { - this.#deleted.add(sourceKey(connectorInstanceId, sourceId)); - } - - documents(): SourceChange[] { - return [...this.#sources.values()].map((source) => structuredClone(source)); - } - - search(actor: KnowledgeActor): Citation[] { - return [...this.#sources.entries()].flatMap(([key, source]) => { - if (this.#deleted.has(key) || !canRead(actor, source.acls)) return []; - const documentId = key; - return source.chunks.map((chunk) => ({ - documentId, - title: source.title, - canonicalUrl: source.canonicalUrl, - chunkId: `${documentId}:${chunk.position}`, - content: chunk.content, - })); - }); - } -} - -function sourceKey(connectorInstanceId: string, sourceId: string) { - return `${connectorInstanceId}:${sourceId}`; -} diff --git a/server/src/knowledge/types.ts b/server/src/knowledge/types.ts deleted file mode 100644 index 5ef9b202..00000000 --- a/server/src/knowledge/types.ts +++ /dev/null @@ -1,9 +0,0 @@ -export type KnowledgeActor = { - userId: string; - groups: string[]; -}; - -export type KnowledgeAclEntry = { - principal: string; - effect: "allow" | "deny"; -}; diff --git a/server/tests/knowledge-acl.test.ts b/server/tests/knowledge-acl.test.ts deleted file mode 100644 index e7608922..00000000 --- a/server/tests/knowledge-acl.test.ts +++ /dev/null @@ -1,30 +0,0 @@ -import { describe, expect, test } from "bun:test"; -import { canRead } from "../src/knowledge/acl"; - -describe("knowledge ACL evaluation", () => { - test("allows a matching user principal", () => { - expect( - canRead({ userId: "u1", groups: [] }, [ - { principal: "user:u1", effect: "allow" }, - ]), - ).toBe(true); - }); - - test("fails closed for an unmatched or empty ACL", () => { - expect( - canRead({ userId: "u1", groups: ["finance"] }, [ - { principal: "group:engineering", effect: "allow" }, - ]), - ).toBe(false); - expect(canRead({ userId: "u1", groups: [] }, [])).toBe(false); - }); - - test("makes a matching deny override a matching allow", () => { - expect( - canRead({ userId: "u1", groups: ["finance"] }, [ - { principal: "group:finance", effect: "allow" }, - { principal: "user:u1", effect: "deny" }, - ]), - ).toBe(false); - }); -}); diff --git a/server/tests/knowledge-repository.test.ts b/server/tests/knowledge-repository.test.ts deleted file mode 100644 index d3863a1c..00000000 --- a/server/tests/knowledge-repository.test.ts +++ /dev/null @@ -1,51 +0,0 @@ -import { describe, expect, test } from "bun:test"; -import { InMemoryKnowledgeRepository } from "../src/knowledge/repository"; - -const change = { - connectorInstanceId: "connector-1", - sourceId: "source-1", - title: "Finance policy", - canonicalUrl: "https://example.test/policy", - contentHash: "hash-1", - chunks: [{ position: 0, content: "finance content" }], - acls: [{ principal: "group:finance", effect: "allow" as const }], -}; - -describe("knowledge repository", () => { - test("replaces source content idempotently", () => { - const repository = new InMemoryKnowledgeRepository(); - repository.apply(change); - repository.apply({ - ...change, - contentHash: "hash-2", - chunks: [{ position: 0, content: "updated" }], - }); - - expect(repository.documents()).toEqual([ - { - ...change, - contentHash: "hash-2", - chunks: [{ position: 0, content: "updated" }], - }, - ]); - }); - - test("returns citations only to authorized actors and hides deleted sources", () => { - const repository = new InMemoryKnowledgeRepository(); - repository.apply(change); - expect(repository.search({ userId: "u1", groups: ["finance"] })).toEqual([ - { - documentId: "connector-1:source-1", - title: "Finance policy", - canonicalUrl: "https://example.test/policy", - chunkId: "connector-1:source-1:0", - content: "finance content", - }, - ]); - expect(repository.search({ userId: "u2", groups: [] })).toEqual([]); - repository.delete("connector-1", "source-1"); - expect(repository.search({ userId: "u1", groups: ["finance"] })).toEqual( - [], - ); - }); -});