Skip to content
Merged
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
6 changes: 5 additions & 1 deletion migrations/0001_extensions.sql
Original file line number Diff line number Diff line change
@@ -1,4 +1,8 @@
-- Own Postgres schema for every knowledge-engine table. Host control-plane
-- tables stay in public; this package never collides with or adopts them.
CREATE SCHEMA IF NOT EXISTS "knowledge";

-- pgvector powers the dense retrieval channel. Per-model
-- "knowledge_embedding_<key>" vector tables are created at runtime by the
-- "knowledge"."embedding_<key>" vector tables are created at runtime by the
-- embed-model activation path (dimensionality varies by model), not here.
CREATE EXTENSION IF NOT EXISTS vector;
9 changes: 5 additions & 4 deletions migrations/0002_knowledge_document.sql
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,8 @@
--
-- Identity/ACL lives here: tenant_id scopes every query; visibility_mode +
-- the two visibility_* columns are the self-contained ACL the caller passes.
CREATE TABLE IF NOT EXISTS "knowledge_document" (
-- tenant_id is plain text (no FK into control-plane tables).
CREATE TABLE IF NOT EXISTS "knowledge"."document" (
"id" text PRIMARY KEY,
"tenant_id" text NOT NULL,
"kind" text NOT NULL,
Expand All @@ -18,13 +19,13 @@ CREATE TABLE IF NOT EXISTS "knowledge_document" (
"attributes" jsonb NOT NULL DEFAULT '{}',
"created_at" timestamp NOT NULL DEFAULT now(),
"last_seen_at" timestamp NOT NULL DEFAULT now(),
CONSTRAINT "knowledge_document_visibility_mode_check" CHECK (
CONSTRAINT "document_visibility_mode_check" CHECK (
"visibility_mode" IN ('tenant', 'principals', 'source_acl', 'private')
)
);

CREATE UNIQUE INDEX IF NOT EXISTS "knowledge_document_tenant_adapter_external_ref_uniq"
ON "knowledge_document" (
CREATE UNIQUE INDEX IF NOT EXISTS "document_tenant_adapter_external_ref_uniq"
ON "knowledge"."document" (
"tenant_id",
"adapter",
"external_ref"
Expand Down
20 changes: 10 additions & 10 deletions migrations/0003_knowledge_version.sql
Original file line number Diff line number Diff line change
@@ -1,15 +1,15 @@
-- The versioned body of a knowledge_document. version is a monotonic int per
-- The versioned body of a knowledge.document. version is a monotonic int per
-- document; status tracks the version/supersede/deprecate lifecycle. Chunks
-- belong to a version_id and are never reused across versions.
--
-- Attribution ("who") lives here: created_by_principal_id + created_by_kind
-- (human/agent/system/adapter). The authority_* columns are a per-version
-- snapshot of the corroboration signals computed at capture time (never
-- recomputed retroactively) and consumed as a rank prior at search time.
CREATE TABLE IF NOT EXISTS "knowledge_version" (
CREATE TABLE IF NOT EXISTS "knowledge"."version" (
"id" text PRIMARY KEY,
"tenant_id" text NOT NULL,
"document_id" text NOT NULL REFERENCES "knowledge_document" ("id") ON DELETE CASCADE,
"document_id" text NOT NULL REFERENCES "knowledge"."document" ("id") ON DELETE CASCADE,
"version" integer NOT NULL,
"supersedes_version_id" text,
"status" text NOT NULL DEFAULT 'active',
Expand All @@ -25,25 +25,25 @@ CREATE TABLE IF NOT EXISTS "knowledge_version" (
"actor_count" integer NOT NULL DEFAULT 1,
"has_social_signal" boolean NOT NULL DEFAULT false,
"source_class" text NOT NULL DEFAULT 'native',
CONSTRAINT "knowledge_version_status_check" CHECK (
CONSTRAINT "version_status_check" CHECK (
"status" IN ('active', 'superseded', 'deprecated', 'archived', 'tombstoned')
),
CONSTRAINT "knowledge_version_created_by_kind_check" CHECK (
CONSTRAINT "version_created_by_kind_check" CHECK (
"created_by_kind" IN ('human', 'agent', 'system', 'adapter')
),
CONSTRAINT "knowledge_version_source_class_check" CHECK (
CONSTRAINT "version_source_class_check" CHECK (
"source_class" IN ('native', 'thread', 'channel', 'call', 'record')
)
);

CREATE UNIQUE INDEX IF NOT EXISTS "knowledge_version_document_version_uniq"
ON "knowledge_version" (
CREATE UNIQUE INDEX IF NOT EXISTS "version_document_version_uniq"
ON "knowledge"."version" (
"document_id",
"version"
);

CREATE INDEX IF NOT EXISTS "knowledge_version_document_status_idx"
ON "knowledge_version" (
CREATE INDEX IF NOT EXISTS "version_document_status_idx"
ON "knowledge"."version" (
"document_id",
"status"
);
16 changes: 8 additions & 8 deletions migrations/0004_knowledge_chunk.sql
Original file line number Diff line number Diff line change
@@ -1,24 +1,24 @@
-- An ordered slice of a knowledge_version's text, keyed by (version_id,
-- An ordered slice of a knowledge.version's text, keyed by (version_id,
-- ordinal). text_fts is a generated tsvector for the lexical search channel —
-- no vector column here (per-model embedding tables are created separately,
-- since dimensionality varies by model).
CREATE TABLE IF NOT EXISTS "knowledge_chunk" (
CREATE TABLE IF NOT EXISTS "knowledge"."chunk" (
"id" text PRIMARY KEY,
"tenant_id" text NOT NULL,
"version_id" text NOT NULL REFERENCES "knowledge_version" ("id") ON DELETE CASCADE,
"document_id" text NOT NULL REFERENCES "knowledge_document" ("id") ON DELETE CASCADE,
"version_id" text NOT NULL REFERENCES "knowledge"."version" ("id") ON DELETE CASCADE,
"document_id" text NOT NULL REFERENCES "knowledge"."document" ("id") ON DELETE CASCADE,
"ordinal" integer NOT NULL,
"text" text NOT NULL,
"role" text,
"created_at" timestamp NOT NULL DEFAULT now(),
"text_fts" tsvector GENERATED ALWAYS AS (to_tsvector('{{FTS_LANGUAGE}}', "text")) STORED
);

CREATE UNIQUE INDEX IF NOT EXISTS "knowledge_chunk_version_ordinal_uniq"
ON "knowledge_chunk" (
CREATE UNIQUE INDEX IF NOT EXISTS "chunk_version_ordinal_uniq"
ON "knowledge"."chunk" (
"version_id",
"ordinal"
);

CREATE INDEX IF NOT EXISTS "knowledge_chunk_text_fts_idx"
ON "knowledge_chunk" USING GIN ("text_fts");
CREATE INDEX IF NOT EXISTS "chunk_text_fts_idx"
ON "knowledge"."chunk" USING GIN ("text_fts");
6 changes: 3 additions & 3 deletions migrations/0005_knowledge_entity.sql
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
-- A real-world thing (person, org, deal, ...) a document or chunk mentions.
-- Identity keys only (email, domain, ...) — not another copy of chunk text.
CREATE TABLE IF NOT EXISTS "knowledge_entity" (
CREATE TABLE IF NOT EXISTS "knowledge"."entity" (
"id" text PRIMARY KEY,
"tenant_id" text NOT NULL,
"kind" text NOT NULL,
Expand All @@ -9,8 +9,8 @@ CREATE TABLE IF NOT EXISTS "knowledge_entity" (
"updated_at" timestamp NOT NULL DEFAULT now()
);

CREATE INDEX IF NOT EXISTS "knowledge_entity_tenant_kind_idx"
ON "knowledge_entity" (
CREATE INDEX IF NOT EXISTS "entity_tenant_kind_idx"
ON "knowledge"."entity" (
"tenant_id",
"kind"
);
16 changes: 8 additions & 8 deletions migrations/0006_knowledge_edge.sql
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
-- (e.g. a principal) — never another copy of chunk text. This is the "series
-- of relations" every record can carry; relation-following ingestion writes
-- `mentions` edges here.
CREATE TABLE IF NOT EXISTS "knowledge_edge" (
CREATE TABLE IF NOT EXISTS "knowledge"."edge" (
"id" text PRIMARY KEY,
"tenant_id" text NOT NULL,
"rel" text NOT NULL,
Expand All @@ -11,26 +11,26 @@ CREATE TABLE IF NOT EXISTS "knowledge_edge" (
"to_type" text NOT NULL,
"to_ref" text NOT NULL,
"created_at" timestamp NOT NULL DEFAULT now(),
CONSTRAINT "knowledge_edge_rel_check" CHECK (
CONSTRAINT "edge_rel_check" CHECK (
"rel" IN ('about', 'produced_by', 'links', 'parent', 'mentions', 'waiting_on')
),
CONSTRAINT "knowledge_edge_from_type_check" CHECK (
CONSTRAINT "edge_from_type_check" CHECK (
"from_type" IN ('document', 'entity', 'native')
),
CONSTRAINT "knowledge_edge_to_type_check" CHECK (
CONSTRAINT "edge_to_type_check" CHECK (
"to_type" IN ('document', 'entity', 'native')
)
);

CREATE INDEX IF NOT EXISTS "knowledge_edge_from_idx"
ON "knowledge_edge" (
CREATE INDEX IF NOT EXISTS "edge_from_idx"
ON "knowledge"."edge" (
"tenant_id",
"from_type",
"from_ref"
);

CREATE INDEX IF NOT EXISTS "knowledge_edge_to_idx"
ON "knowledge_edge" (
CREATE INDEX IF NOT EXISTS "edge_to_idx"
ON "knowledge"."edge" (
"tenant_id",
"to_type",
"to_ref"
Expand Down
6 changes: 3 additions & 3 deletions migrations/0007_knowledge_embed_model.sql
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
-- Which embedding model is active per tenant, and the dims it was discovered
-- at (never hard-coded). The per-model "knowledge_embedding_<key>" vector
-- at (never hard-coded). The per-model "knowledge"."embedding_<key>" vector
-- tables are runtime-managed by the single guarded activation path, not here.
CREATE TABLE IF NOT EXISTS "knowledge_embed_model" (
CREATE TABLE IF NOT EXISTS "knowledge"."embed_model" (
"id" text PRIMARY KEY,
"tenant_id" text NOT NULL,
"model_key" text NOT NULL,
Expand All @@ -10,5 +10,5 @@ CREATE TABLE IF NOT EXISTS "knowledge_embed_model" (
"status" text NOT NULL DEFAULT 'active',
"created_at" timestamp NOT NULL DEFAULT now(),
"updated_at" timestamp NOT NULL DEFAULT now(),
CONSTRAINT "knowledge_embed_model_tenant_model_key_uniq" UNIQUE ("tenant_id", "model_key")
CONSTRAINT "embed_model_tenant_model_key_uniq" UNIQUE ("tenant_id", "model_key")
);
10 changes: 5 additions & 5 deletions migrations/0008_raw_capture.sql
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
-- later replay can re-derive under a different config without re-fetching
-- source. Append-only: rows are never updated or deleted by ingestion; dedupe
-- on (tenant_id, source_hash) reuses the existing row.
CREATE TABLE IF NOT EXISTS "raw_capture" (
CREATE TABLE IF NOT EXISTS "knowledge"."raw_capture" (
"id" text PRIMARY KEY,
"tenant_id" text NOT NULL,
"adapter" text NOT NULL,
Expand All @@ -18,10 +18,10 @@ CREATE TABLE IF NOT EXISTS "raw_capture" (
);

CREATE UNIQUE INDEX IF NOT EXISTS "raw_capture_tenant_source_hash_uniq"
ON "raw_capture" ("tenant_id", "source_hash");
ON "knowledge"."raw_capture" ("tenant_id", "source_hash");

CREATE INDEX IF NOT EXISTS "raw_capture_tenant_adapter_external_ref_idx"
ON "raw_capture" ("tenant_id", "adapter", "external_ref");
ON "knowledge"."raw_capture" ("tenant_id", "adapter", "external_ref");

ALTER TABLE "knowledge_version"
ADD COLUMN IF NOT EXISTS "raw_capture_id" text REFERENCES "raw_capture"("id");
ALTER TABLE "knowledge"."version"
ADD COLUMN IF NOT EXISTS "raw_capture_id" text REFERENCES "knowledge"."raw_capture"("id");
18 changes: 9 additions & 9 deletions migrations/0009_transform_pipeline.sql
Original file line number Diff line number Diff line change
Expand Up @@ -3,18 +3,18 @@
-- the immutable `raw_capture` rows under a NEW `generation` tag, without ever
-- re-fetching source or touching the 'live' generation's versions.

ALTER TABLE "knowledge_version"
ALTER TABLE "knowledge"."version"
ADD COLUMN IF NOT EXISTS "generation" text NOT NULL DEFAULT 'live';

-- Version numbering moves from per-document to per-(document, generation),
-- so a replay generation can mint its own v1 alongside the live document's
-- existing versions instead of colliding with them.
DROP INDEX IF EXISTS "knowledge_version_document_version_uniq";
DROP INDEX IF EXISTS "knowledge"."version_document_version_uniq";

CREATE UNIQUE INDEX IF NOT EXISTS "knowledge_version_document_generation_version_uniq"
ON "knowledge_version" ("document_id", "generation", "version");
CREATE UNIQUE INDEX IF NOT EXISTS "version_document_generation_version_uniq"
ON "knowledge"."version" ("document_id", "generation", "version");

CREATE TABLE IF NOT EXISTS "transform_config" (
CREATE TABLE IF NOT EXISTS "knowledge"."transform_config" (
"id" text PRIMARY KEY,
"tenant_id" text NOT NULL,
"name" text NOT NULL,
Expand All @@ -24,10 +24,10 @@ CREATE TABLE IF NOT EXISTS "transform_config" (
CONSTRAINT "transform_config_tenant_name_version_uniq" UNIQUE ("tenant_id", "name", "version")
);

CREATE TABLE IF NOT EXISTS "transform_run" (
CREATE TABLE IF NOT EXISTS "knowledge"."transform_run" (
"id" text PRIMARY KEY,
"tenant_id" text NOT NULL,
"config_id" text NOT NULL REFERENCES "transform_config" ("id"),
"config_id" text NOT NULL REFERENCES "knowledge"."transform_config" ("id"),
"scope" jsonb NOT NULL DEFAULT '{}',
"generation" text NOT NULL,
"status" text NOT NULL DEFAULT 'running',
Expand All @@ -42,7 +42,7 @@ CREATE TABLE IF NOT EXISTS "transform_run" (
-- One run mints exactly one generation; resolving a generation's tuning
-- config at search time is a lookup on this uniqueness.
CREATE UNIQUE INDEX IF NOT EXISTS "transform_run_generation_uniq"
ON "transform_run" ("generation");
ON "knowledge"."transform_run" ("generation");

CREATE INDEX IF NOT EXISTS "transform_run_tenant_config_idx"
ON "transform_run" ("tenant_id", "config_id");
ON "knowledge"."transform_run" ("tenant_id", "config_id");
17 changes: 9 additions & 8 deletions src/core/embed-model-registry.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -64,7 +64,7 @@ describe("computeModelKey / embeddingTableName", () => {
expect(key).toMatch(/^[a-f0-9]{16}$/);
const tableName = embeddingTableName(key);
expect(tableName).toMatch(EMBED_TABLE_NAME_PATTERN);
expect(tableName).toBe(`knowledge_embedding_${key}`);
expect(tableName).toBe(`"knowledge"."embedding_${key}"`);
});

it("rejects a key that would produce an invalid identifier", () => {
Expand Down Expand Up @@ -133,24 +133,25 @@ describe("activateEmbedModel", () => {
expect(result.dims).toBe(768);
expect(result.tableName).toMatch(EMBED_TABLE_NAME_PATTERN);

const insertQuery = queries.find((q) => q.sql.includes("INSERT INTO knowledge_embed_model"));
const insertQuery = queries.find((q) =>
q.sql.includes('INSERT INTO "knowledge"."embed_model"'),
);
expect(insertQuery).toBeDefined();
expect(insertQuery?.params).toContain("tenant-1");
expect(insertQuery?.params).toContain(768);

const createTableQuery = queries.find((q) => q.sql.includes("CREATE TABLE IF NOT EXISTS"));
expect(createTableQuery?.sql).toContain(result.tableName);
expect(createTableQuery?.sql).toContain("vector(768)");
const bare = result.tableName.replace(/^"knowledge"\."|"$/g, "");
expect(createTableQuery?.sql).toContain(`CONSTRAINT ${bare}_chunk_fk`);
expect(createTableQuery?.sql).toContain(
`CONSTRAINT ${result.tableName}_chunk_fk`,
);
expect(createTableQuery?.sql).toContain(
"FOREIGN KEY (chunk_id) REFERENCES knowledge_chunk (id) ON DELETE CASCADE",
'FOREIGN KEY (chunk_id) REFERENCES "knowledge"."chunk" (id) ON DELETE CASCADE',
);

const tenantIndexQuery = queries.find((q) => q.sql.includes("_tenant_chunk_idx"));
expect(tenantIndexQuery?.sql).toBe(
`CREATE INDEX IF NOT EXISTS ${result.tableName}_tenant_chunk_idx ON ${result.tableName} (tenant_id, chunk_id)`,
`CREATE INDEX IF NOT EXISTS ${bare}_tenant_chunk_idx ON ${result.tableName} (tenant_id, chunk_id)`,
);

const indexQuery = queries.find((q) => q.sql.includes("USING hnsw"));
Expand Down Expand Up @@ -268,7 +269,7 @@ describe("resolveActiveEmbedTable", () => {
};
const result = await resolveActiveEmbedTable(client, "tenant-1");
expect(result).toEqual({
tableName: `knowledge_embedding_${modelKey}`,
tableName: `"knowledge"."embedding_${modelKey}"`,
dims: 768,
modelId: baseConfig.modelId,
});
Expand Down
Loading
Loading