@contractspec/module.context-storage 0.1.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,274 @@
1
+ // src/storage/index.ts
2
+ class PostgresContextStorage {
3
+ database;
4
+ schema;
5
+ createTablesIfMissing;
6
+ ensured = false;
7
+ constructor(options) {
8
+ this.database = options.database;
9
+ this.schema = options.schema ?? "lssm_context";
10
+ this.createTablesIfMissing = options.createTablesIfMissing ?? true;
11
+ }
12
+ async upsertPack(record) {
13
+ await this.ensureTables();
14
+ const packId = `${record.packKey}:${record.version}`;
15
+ await this.database.execute(`INSERT INTO ${this.table("context_pack")}
16
+ (id, pack_key, version, title, description, owners, tags, sources, created_at, updated_at)
17
+ VALUES ($1, $2, $3, $4, $5, $6::jsonb, $7::jsonb, $8::jsonb, $9, $10)
18
+ ON CONFLICT (pack_key, version)
19
+ DO UPDATE SET
20
+ title = EXCLUDED.title,
21
+ description = EXCLUDED.description,
22
+ owners = EXCLUDED.owners,
23
+ tags = EXCLUDED.tags,
24
+ sources = EXCLUDED.sources,
25
+ updated_at = EXCLUDED.updated_at;`, [
26
+ packId,
27
+ record.packKey,
28
+ record.version,
29
+ record.title,
30
+ record.description ?? null,
31
+ record.owners ? JSON.stringify(record.owners) : null,
32
+ record.tags ? JSON.stringify(record.tags) : null,
33
+ record.sources ? JSON.stringify(record.sources) : null,
34
+ record.createdAt,
35
+ record.updatedAt ?? record.createdAt
36
+ ]);
37
+ return record;
38
+ }
39
+ async getPack(packKey, version) {
40
+ await this.ensureTables();
41
+ const rows = await this.database.query(`SELECT * FROM ${this.table("context_pack")}
42
+ WHERE pack_key = $1
43
+ ${version ? "AND version = $2" : ""}
44
+ ORDER BY version DESC
45
+ LIMIT 1;`, version ? [packKey, version] : [packKey]);
46
+ return rows.rows[0] ? this.mapPack(rows.rows[0]) : null;
47
+ }
48
+ async listPacks(query = {}) {
49
+ await this.ensureTables();
50
+ const { query: q, tag, owner, limit = 50, offset = 0 } = query;
51
+ const filters = [];
52
+ const params = [];
53
+ params.push(limit, offset);
54
+ if (q) {
55
+ params.push(`%${q}%`);
56
+ filters.push(`(pack_key ILIKE $${params.length} OR title ILIKE $${params.length})`);
57
+ }
58
+ if (tag) {
59
+ params.push(tag);
60
+ filters.push(`tags @> to_jsonb(ARRAY[$${params.length}]::text[])`);
61
+ }
62
+ if (owner) {
63
+ params.push(owner);
64
+ filters.push(`owners @> to_jsonb(ARRAY[$${params.length}]::text[])`);
65
+ }
66
+ const where = filters.length ? `WHERE ${filters.join(" AND ")}` : "";
67
+ const rows = await this.database.query(`SELECT * FROM ${this.table("context_pack")}
68
+ ${where}
69
+ ORDER BY updated_at DESC
70
+ LIMIT $1 OFFSET $2;`, params);
71
+ const packs = rows.rows.map((row) => this.mapPack(row));
72
+ const total = rows.rowCount;
73
+ return {
74
+ packs,
75
+ total,
76
+ nextOffset: offset + packs.length < total ? offset + packs.length : undefined
77
+ };
78
+ }
79
+ async createSnapshot(record) {
80
+ await this.ensureTables();
81
+ await this.database.execute(`INSERT INTO ${this.table("context_snapshot")}
82
+ (id, pack_key, pack_version, hash, item_count, created_by, metadata, created_at)
83
+ VALUES ($1, $2, $3, $4, $5, $6, $7::jsonb, $8);`, [
84
+ record.snapshotId,
85
+ record.packKey,
86
+ record.packVersion,
87
+ record.hash,
88
+ record.itemCount ?? null,
89
+ record.createdBy ?? null,
90
+ record.metadata ? JSON.stringify(record.metadata) : null,
91
+ record.createdAt
92
+ ]);
93
+ return record;
94
+ }
95
+ async getSnapshot(snapshotId) {
96
+ await this.ensureTables();
97
+ const rows = await this.database.query(`SELECT * FROM ${this.table("context_snapshot")} WHERE id = $1;`, [snapshotId]);
98
+ return rows.rows[0] ? this.mapSnapshot(rows.rows[0]) : null;
99
+ }
100
+ async listSnapshots(query = {}) {
101
+ await this.ensureTables();
102
+ const { packKey, snapshotId, limit = 50, offset = 0 } = query;
103
+ const filters = [];
104
+ const params = [limit, offset];
105
+ if (packKey) {
106
+ params.push(packKey);
107
+ filters.push(`pack_key = $${params.length}`);
108
+ }
109
+ if (snapshotId) {
110
+ params.push(snapshotId);
111
+ filters.push(`id = $${params.length}`);
112
+ }
113
+ const where = filters.length ? `WHERE ${filters.join(" AND ")}` : "";
114
+ const rows = await this.database.query(`SELECT * FROM ${this.table("context_snapshot")}
115
+ ${where}
116
+ ORDER BY created_at DESC
117
+ LIMIT $1 OFFSET $2;`, params);
118
+ const snapshots = rows.rows.map((row) => this.mapSnapshot(row));
119
+ const total = rows.rowCount;
120
+ return {
121
+ snapshots,
122
+ total,
123
+ nextOffset: offset + snapshots.length < total ? offset + snapshots.length : undefined
124
+ };
125
+ }
126
+ async addSnapshotItems(snapshotId, items) {
127
+ await this.ensureTables();
128
+ const created = [];
129
+ for (const item of items) {
130
+ const createdAt = item.createdAt ?? new Date().toISOString();
131
+ await this.database.execute(`INSERT INTO ${this.table("context_snapshot_item")}
132
+ (id, snapshot_id, kind, source_key, source_version, content, text_content, metadata, created_at)
133
+ VALUES ($1, $2, $3, $4, $5, $6::jsonb, $7, $8::jsonb, $9);`, [
134
+ item.itemId,
135
+ snapshotId,
136
+ item.kind,
137
+ item.sourceKey,
138
+ item.sourceVersion ?? null,
139
+ JSON.stringify(item.content),
140
+ item.textContent ?? null,
141
+ item.metadata ? JSON.stringify(item.metadata) : null,
142
+ createdAt
143
+ ]);
144
+ created.push({ ...item, snapshotId, createdAt });
145
+ }
146
+ return created;
147
+ }
148
+ async listSnapshotItems(snapshotId, options = {}) {
149
+ await this.ensureTables();
150
+ const { limit = 100, offset = 0 } = options;
151
+ const rows = await this.database.query(`SELECT * FROM ${this.table("context_snapshot_item")}
152
+ WHERE snapshot_id = $1
153
+ ORDER BY created_at ASC
154
+ LIMIT $2 OFFSET $3;`, [snapshotId, limit, offset]);
155
+ return rows.rows.map((row) => this.mapItem(row));
156
+ }
157
+ async ensureTables() {
158
+ if (this.ensured || !this.createTablesIfMissing)
159
+ return;
160
+ await this.database.execute(`CREATE SCHEMA IF NOT EXISTS ${this.schema};`);
161
+ await this.database.execute(`CREATE TABLE IF NOT EXISTS ${this.table("context_pack")} (
162
+ id text PRIMARY KEY,
163
+ pack_key text NOT NULL,
164
+ version text NOT NULL,
165
+ title text NOT NULL,
166
+ description text,
167
+ owners jsonb,
168
+ tags jsonb,
169
+ sources jsonb,
170
+ created_at timestamptz NOT NULL,
171
+ updated_at timestamptz NOT NULL,
172
+ UNIQUE (pack_key, version)
173
+ );`);
174
+ await this.database.execute(`CREATE TABLE IF NOT EXISTS ${this.table("context_snapshot")} (
175
+ id text PRIMARY KEY,
176
+ pack_key text NOT NULL,
177
+ pack_version text NOT NULL,
178
+ hash text NOT NULL,
179
+ item_count int,
180
+ created_by text,
181
+ metadata jsonb,
182
+ created_at timestamptz NOT NULL
183
+ );`);
184
+ await this.database.execute(`CREATE TABLE IF NOT EXISTS ${this.table("context_snapshot_item")} (
185
+ id text PRIMARY KEY,
186
+ snapshot_id text NOT NULL,
187
+ kind text NOT NULL,
188
+ source_key text NOT NULL,
189
+ source_version text,
190
+ content jsonb NOT NULL,
191
+ text_content text,
192
+ metadata jsonb,
193
+ created_at timestamptz NOT NULL
194
+ );`);
195
+ await this.database.execute(`CREATE INDEX IF NOT EXISTS context_snapshot_item_snapshot_idx
196
+ ON ${this.table("context_snapshot_item")} (snapshot_id);`);
197
+ this.ensured = true;
198
+ }
199
+ table(name) {
200
+ return `${this.schema}.${name}`;
201
+ }
202
+ mapPack(row) {
203
+ return {
204
+ packKey: String(row.pack_key),
205
+ version: String(row.version),
206
+ title: String(row.title),
207
+ description: row.description ? String(row.description) : undefined,
208
+ owners: arrayFromJson(row.owners),
209
+ tags: arrayFromJson(row.tags),
210
+ sources: arrayFromJson(row.sources),
211
+ createdAt: String(row.created_at),
212
+ updatedAt: row.updated_at ? String(row.updated_at) : undefined
213
+ };
214
+ }
215
+ mapSnapshot(row) {
216
+ return {
217
+ snapshotId: String(row.id),
218
+ packKey: String(row.pack_key),
219
+ packVersion: String(row.pack_version),
220
+ hash: String(row.hash),
221
+ itemCount: row.item_count != null ? Number(row.item_count) : undefined,
222
+ createdBy: row.created_by ? String(row.created_by) : undefined,
223
+ metadata: recordFromJson(row.metadata),
224
+ createdAt: String(row.created_at)
225
+ };
226
+ }
227
+ mapItem(row) {
228
+ return {
229
+ itemId: String(row.id),
230
+ snapshotId: String(row.snapshot_id),
231
+ kind: String(row.kind),
232
+ sourceKey: String(row.source_key),
233
+ sourceVersion: row.source_version ? String(row.source_version) : undefined,
234
+ content: recordFromJson(row.content) ?? String(row.content ?? ""),
235
+ textContent: row.text_content ? String(row.text_content) : undefined,
236
+ metadata: recordFromJson(row.metadata),
237
+ createdAt: String(row.created_at)
238
+ };
239
+ }
240
+ }
241
+ function arrayFromJson(value) {
242
+ if (!value)
243
+ return;
244
+ if (Array.isArray(value))
245
+ return value.map(String);
246
+ if (typeof value === "string") {
247
+ try {
248
+ const parsed = JSON.parse(value);
249
+ return Array.isArray(parsed) ? parsed.map(String) : undefined;
250
+ } catch {
251
+ return;
252
+ }
253
+ }
254
+ return;
255
+ }
256
+ function recordFromJson(value) {
257
+ if (!value)
258
+ return;
259
+ if (typeof value === "object" && !Array.isArray(value)) {
260
+ return value;
261
+ }
262
+ if (typeof value === "string") {
263
+ try {
264
+ const parsed = JSON.parse(value);
265
+ return typeof parsed === "object" && parsed ? parsed : undefined;
266
+ } catch {
267
+ return;
268
+ }
269
+ }
270
+ return;
271
+ }
272
+ export {
273
+ PostgresContextStorage
274
+ };
@@ -0,0 +1,28 @@
1
+ import type { ContextPackRecord, ContextSnapshotItemInput, ContextSnapshotRecord } from '@contractspec/lib.context-storage';
2
+ import type { ContextSnapshotStore } from '@contractspec/lib.context-storage/store';
3
+ import { DocumentProcessor, EmbeddingService, VectorIndexer } from '@contractspec/lib.knowledge/ingestion';
4
+ export interface ContextSnapshotBuildInput {
5
+ pack: ContextPackRecord;
6
+ snapshot: ContextSnapshotRecord;
7
+ items: ContextSnapshotItemInput[];
8
+ index?: boolean;
9
+ }
10
+ export interface ContextSnapshotBuildResult {
11
+ snapshot: ContextSnapshotRecord;
12
+ itemCount: number;
13
+ }
14
+ export interface ContextSnapshotPipelineOptions {
15
+ store: ContextSnapshotStore;
16
+ documentProcessor?: DocumentProcessor;
17
+ embeddingService?: EmbeddingService;
18
+ vectorIndexer?: VectorIndexer;
19
+ }
20
+ export declare class ContextSnapshotPipeline {
21
+ private readonly store;
22
+ private readonly processor;
23
+ private readonly embeddingService?;
24
+ private readonly vectorIndexer?;
25
+ constructor(options: ContextSnapshotPipelineOptions);
26
+ buildSnapshot(input: ContextSnapshotBuildInput): Promise<ContextSnapshotBuildResult>;
27
+ private collectFragments;
28
+ }
@@ -0,0 +1,61 @@
1
+ // @bun
2
+ // src/pipeline/context-snapshot-pipeline.ts
3
+ import { Buffer } from "buffer";
4
+ import {
5
+ DocumentProcessor
6
+ } from "@contractspec/lib.knowledge/ingestion";
7
+
8
+ class ContextSnapshotPipeline {
9
+ store;
10
+ processor;
11
+ embeddingService;
12
+ vectorIndexer;
13
+ constructor(options) {
14
+ this.store = options.store;
15
+ this.processor = options.documentProcessor ?? new DocumentProcessor;
16
+ this.embeddingService = options.embeddingService;
17
+ this.vectorIndexer = options.vectorIndexer;
18
+ }
19
+ async buildSnapshot(input) {
20
+ await this.store.upsertPack(input.pack);
21
+ const snapshot = await this.store.createSnapshot({
22
+ ...input.snapshot,
23
+ itemCount: input.items.length
24
+ });
25
+ await this.store.addSnapshotItems(snapshot.snapshotId, input.items);
26
+ if (input.index !== false && this.embeddingService && this.vectorIndexer) {
27
+ const documents = input.items.map((item) => toRawDocument(item, snapshot.snapshotId));
28
+ const fragments = await this.collectFragments(documents);
29
+ const embeddings = await this.embeddingService.embedFragments(fragments);
30
+ await this.vectorIndexer.upsert(fragments, embeddings);
31
+ }
32
+ return { snapshot, itemCount: input.items.length };
33
+ }
34
+ async collectFragments(documents) {
35
+ const fragments = [];
36
+ for (const document of documents) {
37
+ const next = await this.processor.process(document);
38
+ fragments.push(...next);
39
+ }
40
+ return fragments;
41
+ }
42
+ }
43
+ function toRawDocument(item, snapshotId) {
44
+ const content = typeof item.content === "string" ? item.content : JSON.stringify(item.content);
45
+ const mimeType = typeof item.content === "string" ? "text/plain" : "application/json";
46
+ const metadata = {
47
+ snapshotId,
48
+ sourceKey: item.sourceKey,
49
+ sourceVersion: item.sourceVersion ?? "latest",
50
+ kind: item.kind
51
+ };
52
+ return {
53
+ id: item.itemId,
54
+ mimeType,
55
+ data: Buffer.from(content, "utf-8"),
56
+ metadata
57
+ };
58
+ }
59
+ export {
60
+ ContextSnapshotPipeline
61
+ };
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,31 @@
1
+ import type { ContextPackListResult, ContextPackQuery, ContextPackRecord, ContextSnapshotItem, ContextSnapshotItemInput, ContextSnapshotListResult, ContextSnapshotQuery, ContextSnapshotRecord } from '@contractspec/lib.context-storage';
2
+ import type { ContextSnapshotStore } from '@contractspec/lib.context-storage/store';
3
+ import type { DatabaseProvider } from '@contractspec/lib.contracts-integrations';
4
+ export interface PostgresContextStorageOptions {
5
+ database: DatabaseProvider;
6
+ schema?: string;
7
+ createTablesIfMissing?: boolean;
8
+ }
9
+ export declare class PostgresContextStorage implements ContextSnapshotStore {
10
+ private readonly database;
11
+ private readonly schema;
12
+ private readonly createTablesIfMissing;
13
+ private ensured;
14
+ constructor(options: PostgresContextStorageOptions);
15
+ upsertPack(record: ContextPackRecord): Promise<ContextPackRecord>;
16
+ getPack(packKey: string, version?: string): Promise<ContextPackRecord | null>;
17
+ listPacks(query?: ContextPackQuery): Promise<ContextPackListResult>;
18
+ createSnapshot(record: ContextSnapshotRecord): Promise<ContextSnapshotRecord>;
19
+ getSnapshot(snapshotId: string): Promise<ContextSnapshotRecord | null>;
20
+ listSnapshots(query?: ContextSnapshotQuery): Promise<ContextSnapshotListResult>;
21
+ addSnapshotItems(snapshotId: string, items: ContextSnapshotItemInput[]): Promise<ContextSnapshotItem[]>;
22
+ listSnapshotItems(snapshotId: string, options?: {
23
+ limit?: number;
24
+ offset?: number;
25
+ }): Promise<ContextSnapshotItem[]>;
26
+ private ensureTables;
27
+ private table;
28
+ private mapPack;
29
+ private mapSnapshot;
30
+ private mapItem;
31
+ }
@@ -0,0 +1,275 @@
1
+ // @bun
2
+ // src/storage/index.ts
3
+ class PostgresContextStorage {
4
+ database;
5
+ schema;
6
+ createTablesIfMissing;
7
+ ensured = false;
8
+ constructor(options) {
9
+ this.database = options.database;
10
+ this.schema = options.schema ?? "lssm_context";
11
+ this.createTablesIfMissing = options.createTablesIfMissing ?? true;
12
+ }
13
+ async upsertPack(record) {
14
+ await this.ensureTables();
15
+ const packId = `${record.packKey}:${record.version}`;
16
+ await this.database.execute(`INSERT INTO ${this.table("context_pack")}
17
+ (id, pack_key, version, title, description, owners, tags, sources, created_at, updated_at)
18
+ VALUES ($1, $2, $3, $4, $5, $6::jsonb, $7::jsonb, $8::jsonb, $9, $10)
19
+ ON CONFLICT (pack_key, version)
20
+ DO UPDATE SET
21
+ title = EXCLUDED.title,
22
+ description = EXCLUDED.description,
23
+ owners = EXCLUDED.owners,
24
+ tags = EXCLUDED.tags,
25
+ sources = EXCLUDED.sources,
26
+ updated_at = EXCLUDED.updated_at;`, [
27
+ packId,
28
+ record.packKey,
29
+ record.version,
30
+ record.title,
31
+ record.description ?? null,
32
+ record.owners ? JSON.stringify(record.owners) : null,
33
+ record.tags ? JSON.stringify(record.tags) : null,
34
+ record.sources ? JSON.stringify(record.sources) : null,
35
+ record.createdAt,
36
+ record.updatedAt ?? record.createdAt
37
+ ]);
38
+ return record;
39
+ }
40
+ async getPack(packKey, version) {
41
+ await this.ensureTables();
42
+ const rows = await this.database.query(`SELECT * FROM ${this.table("context_pack")}
43
+ WHERE pack_key = $1
44
+ ${version ? "AND version = $2" : ""}
45
+ ORDER BY version DESC
46
+ LIMIT 1;`, version ? [packKey, version] : [packKey]);
47
+ return rows.rows[0] ? this.mapPack(rows.rows[0]) : null;
48
+ }
49
+ async listPacks(query = {}) {
50
+ await this.ensureTables();
51
+ const { query: q, tag, owner, limit = 50, offset = 0 } = query;
52
+ const filters = [];
53
+ const params = [];
54
+ params.push(limit, offset);
55
+ if (q) {
56
+ params.push(`%${q}%`);
57
+ filters.push(`(pack_key ILIKE $${params.length} OR title ILIKE $${params.length})`);
58
+ }
59
+ if (tag) {
60
+ params.push(tag);
61
+ filters.push(`tags @> to_jsonb(ARRAY[$${params.length}]::text[])`);
62
+ }
63
+ if (owner) {
64
+ params.push(owner);
65
+ filters.push(`owners @> to_jsonb(ARRAY[$${params.length}]::text[])`);
66
+ }
67
+ const where = filters.length ? `WHERE ${filters.join(" AND ")}` : "";
68
+ const rows = await this.database.query(`SELECT * FROM ${this.table("context_pack")}
69
+ ${where}
70
+ ORDER BY updated_at DESC
71
+ LIMIT $1 OFFSET $2;`, params);
72
+ const packs = rows.rows.map((row) => this.mapPack(row));
73
+ const total = rows.rowCount;
74
+ return {
75
+ packs,
76
+ total,
77
+ nextOffset: offset + packs.length < total ? offset + packs.length : undefined
78
+ };
79
+ }
80
+ async createSnapshot(record) {
81
+ await this.ensureTables();
82
+ await this.database.execute(`INSERT INTO ${this.table("context_snapshot")}
83
+ (id, pack_key, pack_version, hash, item_count, created_by, metadata, created_at)
84
+ VALUES ($1, $2, $3, $4, $5, $6, $7::jsonb, $8);`, [
85
+ record.snapshotId,
86
+ record.packKey,
87
+ record.packVersion,
88
+ record.hash,
89
+ record.itemCount ?? null,
90
+ record.createdBy ?? null,
91
+ record.metadata ? JSON.stringify(record.metadata) : null,
92
+ record.createdAt
93
+ ]);
94
+ return record;
95
+ }
96
+ async getSnapshot(snapshotId) {
97
+ await this.ensureTables();
98
+ const rows = await this.database.query(`SELECT * FROM ${this.table("context_snapshot")} WHERE id = $1;`, [snapshotId]);
99
+ return rows.rows[0] ? this.mapSnapshot(rows.rows[0]) : null;
100
+ }
101
+ async listSnapshots(query = {}) {
102
+ await this.ensureTables();
103
+ const { packKey, snapshotId, limit = 50, offset = 0 } = query;
104
+ const filters = [];
105
+ const params = [limit, offset];
106
+ if (packKey) {
107
+ params.push(packKey);
108
+ filters.push(`pack_key = $${params.length}`);
109
+ }
110
+ if (snapshotId) {
111
+ params.push(snapshotId);
112
+ filters.push(`id = $${params.length}`);
113
+ }
114
+ const where = filters.length ? `WHERE ${filters.join(" AND ")}` : "";
115
+ const rows = await this.database.query(`SELECT * FROM ${this.table("context_snapshot")}
116
+ ${where}
117
+ ORDER BY created_at DESC
118
+ LIMIT $1 OFFSET $2;`, params);
119
+ const snapshots = rows.rows.map((row) => this.mapSnapshot(row));
120
+ const total = rows.rowCount;
121
+ return {
122
+ snapshots,
123
+ total,
124
+ nextOffset: offset + snapshots.length < total ? offset + snapshots.length : undefined
125
+ };
126
+ }
127
+ async addSnapshotItems(snapshotId, items) {
128
+ await this.ensureTables();
129
+ const created = [];
130
+ for (const item of items) {
131
+ const createdAt = item.createdAt ?? new Date().toISOString();
132
+ await this.database.execute(`INSERT INTO ${this.table("context_snapshot_item")}
133
+ (id, snapshot_id, kind, source_key, source_version, content, text_content, metadata, created_at)
134
+ VALUES ($1, $2, $3, $4, $5, $6::jsonb, $7, $8::jsonb, $9);`, [
135
+ item.itemId,
136
+ snapshotId,
137
+ item.kind,
138
+ item.sourceKey,
139
+ item.sourceVersion ?? null,
140
+ JSON.stringify(item.content),
141
+ item.textContent ?? null,
142
+ item.metadata ? JSON.stringify(item.metadata) : null,
143
+ createdAt
144
+ ]);
145
+ created.push({ ...item, snapshotId, createdAt });
146
+ }
147
+ return created;
148
+ }
149
+ async listSnapshotItems(snapshotId, options = {}) {
150
+ await this.ensureTables();
151
+ const { limit = 100, offset = 0 } = options;
152
+ const rows = await this.database.query(`SELECT * FROM ${this.table("context_snapshot_item")}
153
+ WHERE snapshot_id = $1
154
+ ORDER BY created_at ASC
155
+ LIMIT $2 OFFSET $3;`, [snapshotId, limit, offset]);
156
+ return rows.rows.map((row) => this.mapItem(row));
157
+ }
158
+ async ensureTables() {
159
+ if (this.ensured || !this.createTablesIfMissing)
160
+ return;
161
+ await this.database.execute(`CREATE SCHEMA IF NOT EXISTS ${this.schema};`);
162
+ await this.database.execute(`CREATE TABLE IF NOT EXISTS ${this.table("context_pack")} (
163
+ id text PRIMARY KEY,
164
+ pack_key text NOT NULL,
165
+ version text NOT NULL,
166
+ title text NOT NULL,
167
+ description text,
168
+ owners jsonb,
169
+ tags jsonb,
170
+ sources jsonb,
171
+ created_at timestamptz NOT NULL,
172
+ updated_at timestamptz NOT NULL,
173
+ UNIQUE (pack_key, version)
174
+ );`);
175
+ await this.database.execute(`CREATE TABLE IF NOT EXISTS ${this.table("context_snapshot")} (
176
+ id text PRIMARY KEY,
177
+ pack_key text NOT NULL,
178
+ pack_version text NOT NULL,
179
+ hash text NOT NULL,
180
+ item_count int,
181
+ created_by text,
182
+ metadata jsonb,
183
+ created_at timestamptz NOT NULL
184
+ );`);
185
+ await this.database.execute(`CREATE TABLE IF NOT EXISTS ${this.table("context_snapshot_item")} (
186
+ id text PRIMARY KEY,
187
+ snapshot_id text NOT NULL,
188
+ kind text NOT NULL,
189
+ source_key text NOT NULL,
190
+ source_version text,
191
+ content jsonb NOT NULL,
192
+ text_content text,
193
+ metadata jsonb,
194
+ created_at timestamptz NOT NULL
195
+ );`);
196
+ await this.database.execute(`CREATE INDEX IF NOT EXISTS context_snapshot_item_snapshot_idx
197
+ ON ${this.table("context_snapshot_item")} (snapshot_id);`);
198
+ this.ensured = true;
199
+ }
200
+ table(name) {
201
+ return `${this.schema}.${name}`;
202
+ }
203
+ mapPack(row) {
204
+ return {
205
+ packKey: String(row.pack_key),
206
+ version: String(row.version),
207
+ title: String(row.title),
208
+ description: row.description ? String(row.description) : undefined,
209
+ owners: arrayFromJson(row.owners),
210
+ tags: arrayFromJson(row.tags),
211
+ sources: arrayFromJson(row.sources),
212
+ createdAt: String(row.created_at),
213
+ updatedAt: row.updated_at ? String(row.updated_at) : undefined
214
+ };
215
+ }
216
+ mapSnapshot(row) {
217
+ return {
218
+ snapshotId: String(row.id),
219
+ packKey: String(row.pack_key),
220
+ packVersion: String(row.pack_version),
221
+ hash: String(row.hash),
222
+ itemCount: row.item_count != null ? Number(row.item_count) : undefined,
223
+ createdBy: row.created_by ? String(row.created_by) : undefined,
224
+ metadata: recordFromJson(row.metadata),
225
+ createdAt: String(row.created_at)
226
+ };
227
+ }
228
+ mapItem(row) {
229
+ return {
230
+ itemId: String(row.id),
231
+ snapshotId: String(row.snapshot_id),
232
+ kind: String(row.kind),
233
+ sourceKey: String(row.source_key),
234
+ sourceVersion: row.source_version ? String(row.source_version) : undefined,
235
+ content: recordFromJson(row.content) ?? String(row.content ?? ""),
236
+ textContent: row.text_content ? String(row.text_content) : undefined,
237
+ metadata: recordFromJson(row.metadata),
238
+ createdAt: String(row.created_at)
239
+ };
240
+ }
241
+ }
242
+ function arrayFromJson(value) {
243
+ if (!value)
244
+ return;
245
+ if (Array.isArray(value))
246
+ return value.map(String);
247
+ if (typeof value === "string") {
248
+ try {
249
+ const parsed = JSON.parse(value);
250
+ return Array.isArray(parsed) ? parsed.map(String) : undefined;
251
+ } catch {
252
+ return;
253
+ }
254
+ }
255
+ return;
256
+ }
257
+ function recordFromJson(value) {
258
+ if (!value)
259
+ return;
260
+ if (typeof value === "object" && !Array.isArray(value)) {
261
+ return value;
262
+ }
263
+ if (typeof value === "string") {
264
+ try {
265
+ const parsed = JSON.parse(value);
266
+ return typeof parsed === "object" && parsed ? parsed : undefined;
267
+ } catch {
268
+ return;
269
+ }
270
+ }
271
+ return;
272
+ }
273
+ export {
274
+ PostgresContextStorage
275
+ };