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