@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,404 @@
1
+ // src/entities/index.ts
2
+ import { defineEntity, field, index } from "@contractspec/lib.schema";
3
+ var ContextPackEntity = defineEntity({
4
+ name: "ContextPack",
5
+ description: "Context pack definition for snapshots.",
6
+ schema: "lssm_context",
7
+ map: "context_pack",
8
+ fields: {
9
+ id: field.id({ description: "Context pack record ID" }),
10
+ packKey: field.string({ description: "Context pack key" }),
11
+ version: field.string({ description: "Context pack version" }),
12
+ title: field.string({ description: "Pack title" }),
13
+ description: field.string({ isOptional: true }),
14
+ owners: field.json({ isOptional: true }),
15
+ tags: field.json({ isOptional: true }),
16
+ sources: field.json({ isOptional: true }),
17
+ createdAt: field.createdAt(),
18
+ updatedAt: field.updatedAt()
19
+ },
20
+ indexes: [index.unique(["packKey", "version"])]
21
+ });
22
+ var ContextSnapshotEntity = defineEntity({
23
+ name: "ContextSnapshot",
24
+ description: "Immutable snapshot created from a context pack.",
25
+ schema: "lssm_context",
26
+ map: "context_snapshot",
27
+ fields: {
28
+ id: field.id({ description: "Snapshot ID" }),
29
+ packKey: field.string({ description: "Context pack key" }),
30
+ packVersion: field.string({ description: "Context pack version" }),
31
+ hash: field.string({ description: "Snapshot hash" }),
32
+ itemCount: field.int({ isOptional: true }),
33
+ createdBy: field.string({ isOptional: true }),
34
+ metadata: field.json({ isOptional: true }),
35
+ createdAt: field.createdAt()
36
+ },
37
+ indexes: [index.on(["packKey", "packVersion"]), index.on(["createdAt"])]
38
+ });
39
+ var ContextSnapshotItemEntity = defineEntity({
40
+ name: "ContextSnapshotItem",
41
+ description: "Item belonging to a context snapshot.",
42
+ schema: "lssm_context",
43
+ map: "context_snapshot_item",
44
+ fields: {
45
+ id: field.id({ description: "Snapshot item ID" }),
46
+ snapshotId: field.string({ description: "Context snapshot ID" }),
47
+ kind: field.string({ description: "Item kind" }),
48
+ sourceKey: field.string({ description: "Source key" }),
49
+ sourceVersion: field.string({ isOptional: true }),
50
+ content: field.json({ description: "Structured content" }),
51
+ textContent: field.string({ isOptional: true }),
52
+ metadata: field.json({ isOptional: true }),
53
+ createdAt: field.createdAt()
54
+ },
55
+ indexes: [index.on(["snapshotId"]), index.on(["kind"])]
56
+ });
57
+ var contextStorageEntities = [
58
+ ContextPackEntity,
59
+ ContextSnapshotEntity,
60
+ ContextSnapshotItemEntity
61
+ ];
62
+ var contextStorageSchemaContribution = {
63
+ moduleId: "@contractspec/module.context-storage",
64
+ entities: contextStorageEntities
65
+ };
66
+
67
+ // src/storage/index.ts
68
+ class PostgresContextStorage {
69
+ database;
70
+ schema;
71
+ createTablesIfMissing;
72
+ ensured = false;
73
+ constructor(options) {
74
+ this.database = options.database;
75
+ this.schema = options.schema ?? "lssm_context";
76
+ this.createTablesIfMissing = options.createTablesIfMissing ?? true;
77
+ }
78
+ async upsertPack(record) {
79
+ await this.ensureTables();
80
+ const packId = `${record.packKey}:${record.version}`;
81
+ await this.database.execute(`INSERT INTO ${this.table("context_pack")}
82
+ (id, pack_key, version, title, description, owners, tags, sources, created_at, updated_at)
83
+ VALUES ($1, $2, $3, $4, $5, $6::jsonb, $7::jsonb, $8::jsonb, $9, $10)
84
+ ON CONFLICT (pack_key, version)
85
+ DO UPDATE SET
86
+ title = EXCLUDED.title,
87
+ description = EXCLUDED.description,
88
+ owners = EXCLUDED.owners,
89
+ tags = EXCLUDED.tags,
90
+ sources = EXCLUDED.sources,
91
+ updated_at = EXCLUDED.updated_at;`, [
92
+ packId,
93
+ record.packKey,
94
+ record.version,
95
+ record.title,
96
+ record.description ?? null,
97
+ record.owners ? JSON.stringify(record.owners) : null,
98
+ record.tags ? JSON.stringify(record.tags) : null,
99
+ record.sources ? JSON.stringify(record.sources) : null,
100
+ record.createdAt,
101
+ record.updatedAt ?? record.createdAt
102
+ ]);
103
+ return record;
104
+ }
105
+ async getPack(packKey, version) {
106
+ await this.ensureTables();
107
+ const rows = await this.database.query(`SELECT * FROM ${this.table("context_pack")}
108
+ WHERE pack_key = $1
109
+ ${version ? "AND version = $2" : ""}
110
+ ORDER BY version DESC
111
+ LIMIT 1;`, version ? [packKey, version] : [packKey]);
112
+ return rows.rows[0] ? this.mapPack(rows.rows[0]) : null;
113
+ }
114
+ async listPacks(query = {}) {
115
+ await this.ensureTables();
116
+ const { query: q, tag, owner, limit = 50, offset = 0 } = query;
117
+ const filters = [];
118
+ const params = [];
119
+ params.push(limit, offset);
120
+ if (q) {
121
+ params.push(`%${q}%`);
122
+ filters.push(`(pack_key ILIKE $${params.length} OR title ILIKE $${params.length})`);
123
+ }
124
+ if (tag) {
125
+ params.push(tag);
126
+ filters.push(`tags @> to_jsonb(ARRAY[$${params.length}]::text[])`);
127
+ }
128
+ if (owner) {
129
+ params.push(owner);
130
+ filters.push(`owners @> to_jsonb(ARRAY[$${params.length}]::text[])`);
131
+ }
132
+ const where = filters.length ? `WHERE ${filters.join(" AND ")}` : "";
133
+ const rows = await this.database.query(`SELECT * FROM ${this.table("context_pack")}
134
+ ${where}
135
+ ORDER BY updated_at DESC
136
+ LIMIT $1 OFFSET $2;`, params);
137
+ const packs = rows.rows.map((row) => this.mapPack(row));
138
+ const total = rows.rowCount;
139
+ return {
140
+ packs,
141
+ total,
142
+ nextOffset: offset + packs.length < total ? offset + packs.length : undefined
143
+ };
144
+ }
145
+ async createSnapshot(record) {
146
+ await this.ensureTables();
147
+ await this.database.execute(`INSERT INTO ${this.table("context_snapshot")}
148
+ (id, pack_key, pack_version, hash, item_count, created_by, metadata, created_at)
149
+ VALUES ($1, $2, $3, $4, $5, $6, $7::jsonb, $8);`, [
150
+ record.snapshotId,
151
+ record.packKey,
152
+ record.packVersion,
153
+ record.hash,
154
+ record.itemCount ?? null,
155
+ record.createdBy ?? null,
156
+ record.metadata ? JSON.stringify(record.metadata) : null,
157
+ record.createdAt
158
+ ]);
159
+ return record;
160
+ }
161
+ async getSnapshot(snapshotId) {
162
+ await this.ensureTables();
163
+ const rows = await this.database.query(`SELECT * FROM ${this.table("context_snapshot")} WHERE id = $1;`, [snapshotId]);
164
+ return rows.rows[0] ? this.mapSnapshot(rows.rows[0]) : null;
165
+ }
166
+ async listSnapshots(query = {}) {
167
+ await this.ensureTables();
168
+ const { packKey, snapshotId, limit = 50, offset = 0 } = query;
169
+ const filters = [];
170
+ const params = [limit, offset];
171
+ if (packKey) {
172
+ params.push(packKey);
173
+ filters.push(`pack_key = $${params.length}`);
174
+ }
175
+ if (snapshotId) {
176
+ params.push(snapshotId);
177
+ filters.push(`id = $${params.length}`);
178
+ }
179
+ const where = filters.length ? `WHERE ${filters.join(" AND ")}` : "";
180
+ const rows = await this.database.query(`SELECT * FROM ${this.table("context_snapshot")}
181
+ ${where}
182
+ ORDER BY created_at DESC
183
+ LIMIT $1 OFFSET $2;`, params);
184
+ const snapshots = rows.rows.map((row) => this.mapSnapshot(row));
185
+ const total = rows.rowCount;
186
+ return {
187
+ snapshots,
188
+ total,
189
+ nextOffset: offset + snapshots.length < total ? offset + snapshots.length : undefined
190
+ };
191
+ }
192
+ async addSnapshotItems(snapshotId, items) {
193
+ await this.ensureTables();
194
+ const created = [];
195
+ for (const item of items) {
196
+ const createdAt = item.createdAt ?? new Date().toISOString();
197
+ await this.database.execute(`INSERT INTO ${this.table("context_snapshot_item")}
198
+ (id, snapshot_id, kind, source_key, source_version, content, text_content, metadata, created_at)
199
+ VALUES ($1, $2, $3, $4, $5, $6::jsonb, $7, $8::jsonb, $9);`, [
200
+ item.itemId,
201
+ snapshotId,
202
+ item.kind,
203
+ item.sourceKey,
204
+ item.sourceVersion ?? null,
205
+ JSON.stringify(item.content),
206
+ item.textContent ?? null,
207
+ item.metadata ? JSON.stringify(item.metadata) : null,
208
+ createdAt
209
+ ]);
210
+ created.push({ ...item, snapshotId, createdAt });
211
+ }
212
+ return created;
213
+ }
214
+ async listSnapshotItems(snapshotId, options = {}) {
215
+ await this.ensureTables();
216
+ const { limit = 100, offset = 0 } = options;
217
+ const rows = await this.database.query(`SELECT * FROM ${this.table("context_snapshot_item")}
218
+ WHERE snapshot_id = $1
219
+ ORDER BY created_at ASC
220
+ LIMIT $2 OFFSET $3;`, [snapshotId, limit, offset]);
221
+ return rows.rows.map((row) => this.mapItem(row));
222
+ }
223
+ async ensureTables() {
224
+ if (this.ensured || !this.createTablesIfMissing)
225
+ return;
226
+ await this.database.execute(`CREATE SCHEMA IF NOT EXISTS ${this.schema};`);
227
+ await this.database.execute(`CREATE TABLE IF NOT EXISTS ${this.table("context_pack")} (
228
+ id text PRIMARY KEY,
229
+ pack_key text NOT NULL,
230
+ version text NOT NULL,
231
+ title text NOT NULL,
232
+ description text,
233
+ owners jsonb,
234
+ tags jsonb,
235
+ sources jsonb,
236
+ created_at timestamptz NOT NULL,
237
+ updated_at timestamptz NOT NULL,
238
+ UNIQUE (pack_key, version)
239
+ );`);
240
+ await this.database.execute(`CREATE TABLE IF NOT EXISTS ${this.table("context_snapshot")} (
241
+ id text PRIMARY KEY,
242
+ pack_key text NOT NULL,
243
+ pack_version text NOT NULL,
244
+ hash text NOT NULL,
245
+ item_count int,
246
+ created_by text,
247
+ metadata jsonb,
248
+ created_at timestamptz NOT NULL
249
+ );`);
250
+ await this.database.execute(`CREATE TABLE IF NOT EXISTS ${this.table("context_snapshot_item")} (
251
+ id text PRIMARY KEY,
252
+ snapshot_id text NOT NULL,
253
+ kind text NOT NULL,
254
+ source_key text NOT NULL,
255
+ source_version text,
256
+ content jsonb NOT NULL,
257
+ text_content text,
258
+ metadata jsonb,
259
+ created_at timestamptz NOT NULL
260
+ );`);
261
+ await this.database.execute(`CREATE INDEX IF NOT EXISTS context_snapshot_item_snapshot_idx
262
+ ON ${this.table("context_snapshot_item")} (snapshot_id);`);
263
+ this.ensured = true;
264
+ }
265
+ table(name) {
266
+ return `${this.schema}.${name}`;
267
+ }
268
+ mapPack(row) {
269
+ return {
270
+ packKey: String(row.pack_key),
271
+ version: String(row.version),
272
+ title: String(row.title),
273
+ description: row.description ? String(row.description) : undefined,
274
+ owners: arrayFromJson(row.owners),
275
+ tags: arrayFromJson(row.tags),
276
+ sources: arrayFromJson(row.sources),
277
+ createdAt: String(row.created_at),
278
+ updatedAt: row.updated_at ? String(row.updated_at) : undefined
279
+ };
280
+ }
281
+ mapSnapshot(row) {
282
+ return {
283
+ snapshotId: String(row.id),
284
+ packKey: String(row.pack_key),
285
+ packVersion: String(row.pack_version),
286
+ hash: String(row.hash),
287
+ itemCount: row.item_count != null ? Number(row.item_count) : undefined,
288
+ createdBy: row.created_by ? String(row.created_by) : undefined,
289
+ metadata: recordFromJson(row.metadata),
290
+ createdAt: String(row.created_at)
291
+ };
292
+ }
293
+ mapItem(row) {
294
+ return {
295
+ itemId: String(row.id),
296
+ snapshotId: String(row.snapshot_id),
297
+ kind: String(row.kind),
298
+ sourceKey: String(row.source_key),
299
+ sourceVersion: row.source_version ? String(row.source_version) : undefined,
300
+ content: recordFromJson(row.content) ?? String(row.content ?? ""),
301
+ textContent: row.text_content ? String(row.text_content) : undefined,
302
+ metadata: recordFromJson(row.metadata),
303
+ createdAt: String(row.created_at)
304
+ };
305
+ }
306
+ }
307
+ function arrayFromJson(value) {
308
+ if (!value)
309
+ return;
310
+ if (Array.isArray(value))
311
+ return value.map(String);
312
+ if (typeof value === "string") {
313
+ try {
314
+ const parsed = JSON.parse(value);
315
+ return Array.isArray(parsed) ? parsed.map(String) : undefined;
316
+ } catch {
317
+ return;
318
+ }
319
+ }
320
+ return;
321
+ }
322
+ function recordFromJson(value) {
323
+ if (!value)
324
+ return;
325
+ if (typeof value === "object" && !Array.isArray(value)) {
326
+ return value;
327
+ }
328
+ if (typeof value === "string") {
329
+ try {
330
+ const parsed = JSON.parse(value);
331
+ return typeof parsed === "object" && parsed ? parsed : undefined;
332
+ } catch {
333
+ return;
334
+ }
335
+ }
336
+ return;
337
+ }
338
+
339
+ // src/pipeline/context-snapshot-pipeline.ts
340
+ import { Buffer } from "node:buffer";
341
+ import {
342
+ DocumentProcessor
343
+ } from "@contractspec/lib.knowledge/ingestion";
344
+
345
+ class ContextSnapshotPipeline {
346
+ store;
347
+ processor;
348
+ embeddingService;
349
+ vectorIndexer;
350
+ constructor(options) {
351
+ this.store = options.store;
352
+ this.processor = options.documentProcessor ?? new DocumentProcessor;
353
+ this.embeddingService = options.embeddingService;
354
+ this.vectorIndexer = options.vectorIndexer;
355
+ }
356
+ async buildSnapshot(input) {
357
+ await this.store.upsertPack(input.pack);
358
+ const snapshot = await this.store.createSnapshot({
359
+ ...input.snapshot,
360
+ itemCount: input.items.length
361
+ });
362
+ await this.store.addSnapshotItems(snapshot.snapshotId, input.items);
363
+ if (input.index !== false && this.embeddingService && this.vectorIndexer) {
364
+ const documents = input.items.map((item) => toRawDocument(item, snapshot.snapshotId));
365
+ const fragments = await this.collectFragments(documents);
366
+ const embeddings = await this.embeddingService.embedFragments(fragments);
367
+ await this.vectorIndexer.upsert(fragments, embeddings);
368
+ }
369
+ return { snapshot, itemCount: input.items.length };
370
+ }
371
+ async collectFragments(documents) {
372
+ const fragments = [];
373
+ for (const document of documents) {
374
+ const next = await this.processor.process(document);
375
+ fragments.push(...next);
376
+ }
377
+ return fragments;
378
+ }
379
+ }
380
+ function toRawDocument(item, snapshotId) {
381
+ const content = typeof item.content === "string" ? item.content : JSON.stringify(item.content);
382
+ const mimeType = typeof item.content === "string" ? "text/plain" : "application/json";
383
+ const metadata = {
384
+ snapshotId,
385
+ sourceKey: item.sourceKey,
386
+ sourceVersion: item.sourceVersion ?? "latest",
387
+ kind: item.kind
388
+ };
389
+ return {
390
+ id: item.itemId,
391
+ mimeType,
392
+ data: Buffer.from(content, "utf-8"),
393
+ metadata
394
+ };
395
+ }
396
+ export {
397
+ contextStorageSchemaContribution,
398
+ contextStorageEntities,
399
+ PostgresContextStorage,
400
+ ContextSnapshotPipeline,
401
+ ContextSnapshotItemEntity,
402
+ ContextSnapshotEntity,
403
+ ContextPackEntity
404
+ };
@@ -0,0 +1,60 @@
1
+ // src/pipeline/context-snapshot-pipeline.ts
2
+ import { Buffer } from "node:buffer";
3
+ import {
4
+ DocumentProcessor
5
+ } from "@contractspec/lib.knowledge/ingestion";
6
+
7
+ class ContextSnapshotPipeline {
8
+ store;
9
+ processor;
10
+ embeddingService;
11
+ vectorIndexer;
12
+ constructor(options) {
13
+ this.store = options.store;
14
+ this.processor = options.documentProcessor ?? new DocumentProcessor;
15
+ this.embeddingService = options.embeddingService;
16
+ this.vectorIndexer = options.vectorIndexer;
17
+ }
18
+ async buildSnapshot(input) {
19
+ await this.store.upsertPack(input.pack);
20
+ const snapshot = await this.store.createSnapshot({
21
+ ...input.snapshot,
22
+ itemCount: input.items.length
23
+ });
24
+ await this.store.addSnapshotItems(snapshot.snapshotId, input.items);
25
+ if (input.index !== false && this.embeddingService && this.vectorIndexer) {
26
+ const documents = input.items.map((item) => toRawDocument(item, snapshot.snapshotId));
27
+ const fragments = await this.collectFragments(documents);
28
+ const embeddings = await this.embeddingService.embedFragments(fragments);
29
+ await this.vectorIndexer.upsert(fragments, embeddings);
30
+ }
31
+ return { snapshot, itemCount: input.items.length };
32
+ }
33
+ async collectFragments(documents) {
34
+ const fragments = [];
35
+ for (const document of documents) {
36
+ const next = await this.processor.process(document);
37
+ fragments.push(...next);
38
+ }
39
+ return fragments;
40
+ }
41
+ }
42
+ function toRawDocument(item, snapshotId) {
43
+ const content = typeof item.content === "string" ? item.content : JSON.stringify(item.content);
44
+ const mimeType = typeof item.content === "string" ? "text/plain" : "application/json";
45
+ const metadata = {
46
+ snapshotId,
47
+ sourceKey: item.sourceKey,
48
+ sourceVersion: item.sourceVersion ?? "latest",
49
+ kind: item.kind
50
+ };
51
+ return {
52
+ id: item.itemId,
53
+ mimeType,
54
+ data: Buffer.from(content, "utf-8"),
55
+ metadata
56
+ };
57
+ }
58
+ export {
59
+ ContextSnapshotPipeline
60
+ };