@persql/context 0.1.0

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/dist/core.cjs ADDED
@@ -0,0 +1,384 @@
1
+ "use strict";
2
+ var __defProp = Object.defineProperty;
3
+ var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
4
+ var __getOwnPropNames = Object.getOwnPropertyNames;
5
+ var __hasOwnProp = Object.prototype.hasOwnProperty;
6
+ var __export = (target, all) => {
7
+ for (var name in all)
8
+ __defProp(target, name, { get: all[name], enumerable: true });
9
+ };
10
+ var __copyProps = (to, from, except, desc) => {
11
+ if (from && typeof from === "object" || typeof from === "function") {
12
+ for (let key of __getOwnPropNames(from))
13
+ if (!__hasOwnProp.call(to, key) && key !== except)
14
+ __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
15
+ }
16
+ return to;
17
+ };
18
+ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
19
+
20
+ // src/core.ts
21
+ var core_exports = {};
22
+ __export(core_exports, {
23
+ EXTRACTION_JSON_SCHEMA: () => EXTRACTION_JSON_SCHEMA,
24
+ EXTRACTION_SYSTEM_PROMPT: () => EXTRACTION_SYSTEM_PROMPT,
25
+ SCHEMA_SQL: () => SCHEMA_SQL,
26
+ SCHEMA_VERSION: () => SCHEMA_VERSION,
27
+ TABLE_PREFIX: () => TABLE_PREFIX,
28
+ buildByTag: () => buildByTag,
29
+ buildEdgesAmong: () => buildEdgesAmong,
30
+ buildExtractionMessages: () => buildExtractionMessages,
31
+ buildForget: () => buildForget,
32
+ buildGet: () => buildGet,
33
+ buildInit: () => buildInit,
34
+ buildNeighborEntities: () => buildNeighborEntities,
35
+ buildRecall: () => buildRecall,
36
+ buildRecent: () => buildRecent,
37
+ buildRemember: () => buildRemember,
38
+ buildStats: () => buildStats,
39
+ buildSupersede: () => buildSupersede,
40
+ buildUpsertEdge: () => buildUpsertEdge,
41
+ buildUpsertEntity: () => buildUpsertEntity,
42
+ entityId: () => entityId,
43
+ normalizeTags: () => normalizeTags,
44
+ toFtsQuery: () => toFtsQuery
45
+ });
46
+ module.exports = __toCommonJS(core_exports);
47
+ var SCHEMA_VERSION = 1;
48
+ var TABLE_PREFIX = "ctx_";
49
+ var T = TABLE_PREFIX;
50
+ var SCHEMA_SQL = [
51
+ `CREATE TABLE IF NOT EXISTS ${T}memory (
52
+ id TEXT PRIMARY KEY,
53
+ kind TEXT NOT NULL DEFAULT 'fact',
54
+ topic TEXT,
55
+ body TEXT NOT NULL,
56
+ tags TEXT,
57
+ source TEXT,
58
+ created_at INTEGER NOT NULL,
59
+ superseded_by TEXT
60
+ )`,
61
+ `CREATE INDEX IF NOT EXISTS ${T}memory_kind ON ${T}memory(kind)`,
62
+ `CREATE INDEX IF NOT EXISTS ${T}memory_created ON ${T}memory(created_at)`,
63
+ `CREATE INDEX IF NOT EXISTS ${T}memory_superseded ON ${T}memory(superseded_by)`,
64
+ // External-content FTS index: body text is not duplicated, only indexed.
65
+ // Porter stemmer so "invoice" recalls "invoicing" without embeddings.
66
+ `CREATE VIRTUAL TABLE IF NOT EXISTS ${T}memory_fts USING fts5(
67
+ topic, body, tags,
68
+ content='${T}memory', content_rowid='rowid',
69
+ tokenize='porter unicode61'
70
+ )`,
71
+ `CREATE TRIGGER IF NOT EXISTS ${T}memory_ai AFTER INSERT ON ${T}memory BEGIN
72
+ INSERT INTO ${T}memory_fts(rowid, topic, body, tags)
73
+ VALUES (new.rowid, new.topic, new.body, new.tags);
74
+ END`,
75
+ `CREATE TRIGGER IF NOT EXISTS ${T}memory_ad AFTER DELETE ON ${T}memory BEGIN
76
+ INSERT INTO ${T}memory_fts(${T}memory_fts, rowid, topic, body, tags)
77
+ VALUES ('delete', old.rowid, old.topic, old.body, old.tags);
78
+ END`,
79
+ `CREATE TRIGGER IF NOT EXISTS ${T}memory_au AFTER UPDATE ON ${T}memory BEGIN
80
+ INSERT INTO ${T}memory_fts(${T}memory_fts, rowid, topic, body, tags)
81
+ VALUES ('delete', old.rowid, old.topic, old.body, old.tags);
82
+ INSERT INTO ${T}memory_fts(rowid, topic, body, tags)
83
+ VALUES (new.rowid, new.topic, new.body, new.tags);
84
+ END`,
85
+ `CREATE TABLE IF NOT EXISTS ${T}entity (
86
+ id TEXT PRIMARY KEY,
87
+ name TEXT NOT NULL,
88
+ kind TEXT,
89
+ body TEXT,
90
+ created_at INTEGER NOT NULL
91
+ )`,
92
+ `CREATE TABLE IF NOT EXISTS ${T}edge (
93
+ id TEXT PRIMARY KEY,
94
+ src TEXT NOT NULL,
95
+ rel TEXT NOT NULL,
96
+ dst TEXT NOT NULL,
97
+ source TEXT,
98
+ created_at INTEGER NOT NULL
99
+ )`,
100
+ `CREATE INDEX IF NOT EXISTS ${T}edge_src ON ${T}edge(src)`,
101
+ `CREATE INDEX IF NOT EXISTS ${T}edge_dst ON ${T}edge(dst)`
102
+ ];
103
+ function buildInit() {
104
+ return SCHEMA_SQL.map((sql) => ({ sql, params: [] }));
105
+ }
106
+ var FTS_OPERATORS = /* @__PURE__ */ new Set(["and", "or", "not", "near"]);
107
+ function normalizeTags(tags) {
108
+ if (!tags) return null;
109
+ const arr = Array.isArray(tags) ? tags : String(tags).split(/[,\s]+/);
110
+ const norm = Array.from(
111
+ new Set(arr.map((t) => t.trim().toLowerCase()).filter(Boolean))
112
+ );
113
+ return norm.length ? norm.join(" ") : null;
114
+ }
115
+ function toFtsQuery(input, opts = {}) {
116
+ if (opts.mode === "raw") return input.trim() || null;
117
+ const tokens = (input.match(/[\p{L}\p{N}_]+/gu) ?? []).filter(
118
+ (t) => !FTS_OPERATORS.has(t.toLowerCase())
119
+ );
120
+ if (!tokens.length) return null;
121
+ const joiner = opts.operator === "and" ? " AND " : " OR ";
122
+ return tokens.map((t) => `"${t}"`).join(joiner);
123
+ }
124
+ function slugId(prefix, s) {
125
+ const base = s.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "").slice(0, 96);
126
+ return prefix + (base || "x");
127
+ }
128
+ function entityId(name) {
129
+ return slugId("e_", name);
130
+ }
131
+ function resolveEntityRef(ref) {
132
+ return ref.startsWith("e_") ? ref : entityId(ref);
133
+ }
134
+ function edgeId(srcId, rel, dstId) {
135
+ return slugId("x_", `${srcId}|${rel}|${dstId}`);
136
+ }
137
+ var MEMORY_COLS = "id, kind, topic, body, tags, source, created_at AS createdAt, superseded_by AS supersededBy";
138
+ function buildRemember(input, now, id) {
139
+ return {
140
+ sql: `INSERT INTO ${T}memory (id, kind, topic, body, tags, source, created_at, superseded_by)
141
+ VALUES (?, ?, ?, ?, ?, ?, ?, NULL)`,
142
+ params: [
143
+ id,
144
+ input.kind ?? "fact",
145
+ input.topic ?? null,
146
+ input.body,
147
+ normalizeTags(input.tags),
148
+ input.source ?? null,
149
+ now
150
+ ]
151
+ };
152
+ }
153
+ function buildSupersede(oldId, newId) {
154
+ return {
155
+ sql: `UPDATE ${T}memory SET superseded_by = ? WHERE id = ? AND superseded_by IS NULL`,
156
+ params: [newId, oldId]
157
+ };
158
+ }
159
+ function buildForget(id) {
160
+ return { sql: `DELETE FROM ${T}memory WHERE id = ?`, params: [id] };
161
+ }
162
+ function buildGet(id) {
163
+ return {
164
+ sql: `SELECT ${MEMORY_COLS} FROM ${T}memory WHERE id = ?`,
165
+ params: [id]
166
+ };
167
+ }
168
+ function buildRecall(query, opts = {}) {
169
+ const match = toFtsQuery(query, { operator: opts.operator, mode: opts.mode });
170
+ if (!match) return null;
171
+ const where = [`${T}memory_fts MATCH ?`];
172
+ const params = [match];
173
+ if (!opts.includeSuperseded) where.push("m.superseded_by IS NULL");
174
+ if (opts.kind) {
175
+ where.push("m.kind = ?");
176
+ params.push(opts.kind);
177
+ }
178
+ if (opts.sinceMs != null) {
179
+ where.push("m.created_at >= ?");
180
+ params.push(opts.sinceMs);
181
+ }
182
+ params.push(opts.limit ?? 8);
183
+ return {
184
+ sql: `SELECT m.id, m.kind, m.topic, m.body, m.tags, m.source,
185
+ m.created_at AS createdAt, m.superseded_by AS supersededBy
186
+ FROM ${T}memory_fts
187
+ JOIN ${T}memory m ON m.rowid = ${T}memory_fts.rowid
188
+ WHERE ${where.join(" AND ")}
189
+ ORDER BY ${T}memory_fts.rank
190
+ LIMIT ?`,
191
+ params
192
+ };
193
+ }
194
+ function buildRecent(opts = {}) {
195
+ const where = [];
196
+ const params = [];
197
+ if (!opts.includeSuperseded) where.push("superseded_by IS NULL");
198
+ if (opts.kind) {
199
+ where.push("kind = ?");
200
+ params.push(opts.kind);
201
+ }
202
+ params.push(opts.limit ?? 20);
203
+ return {
204
+ sql: `SELECT ${MEMORY_COLS} FROM ${T}memory
205
+ ${where.length ? "WHERE " + where.join(" AND ") : ""}
206
+ ORDER BY created_at DESC
207
+ LIMIT ?`,
208
+ params
209
+ };
210
+ }
211
+ function buildByTag(tag, opts = {}) {
212
+ const t = tag.trim().toLowerCase();
213
+ const where = ["(' ' || COALESCE(tags, '') || ' ') LIKE ?"];
214
+ const params = [`% ${t} %`];
215
+ if (!opts.includeSuperseded) where.push("superseded_by IS NULL");
216
+ if (opts.kind) {
217
+ where.push("kind = ?");
218
+ params.push(opts.kind);
219
+ }
220
+ params.push(opts.limit ?? 20);
221
+ return {
222
+ sql: `SELECT ${MEMORY_COLS} FROM ${T}memory
223
+ WHERE ${where.join(" AND ")}
224
+ ORDER BY created_at DESC
225
+ LIMIT ?`,
226
+ params
227
+ };
228
+ }
229
+ function buildStats() {
230
+ return {
231
+ sql: `SELECT
232
+ (SELECT count(*) FROM ${T}memory WHERE superseded_by IS NULL) AS facts,
233
+ (SELECT count(*) FROM ${T}memory) AS facts_total,
234
+ (SELECT count(*) FROM ${T}entity) AS entities,
235
+ (SELECT count(*) FROM ${T}edge) AS edges`,
236
+ params: []
237
+ };
238
+ }
239
+ function buildUpsertEntity(e, now) {
240
+ const id = e.id ?? entityId(e.name);
241
+ return {
242
+ sql: `INSERT INTO ${T}entity (id, name, kind, body, created_at)
243
+ VALUES (?, ?, ?, ?, ?)
244
+ ON CONFLICT(id) DO UPDATE SET
245
+ name = excluded.name,
246
+ kind = COALESCE(excluded.kind, ${T}entity.kind),
247
+ body = COALESCE(excluded.body, ${T}entity.body)`,
248
+ params: [id, e.name, e.kind ?? null, e.body ?? null, now]
249
+ };
250
+ }
251
+ function buildUpsertEdge(e, now) {
252
+ const srcId = resolveEntityRef(e.src);
253
+ const dstId = resolveEntityRef(e.dst);
254
+ const id = edgeId(srcId, e.rel, dstId);
255
+ return {
256
+ sql: `INSERT INTO ${T}edge (id, src, rel, dst, source, created_at)
257
+ VALUES (?, ?, ?, ?, ?, ?)
258
+ ON CONFLICT(id) DO UPDATE SET
259
+ source = COALESCE(excluded.source, ${T}edge.source)`,
260
+ params: [id, srcId, e.rel, dstId, e.source ?? null, now]
261
+ };
262
+ }
263
+ function buildNeighborEntities(seedId, depth, limit) {
264
+ return {
265
+ sql: `WITH RECURSIVE reach(id, depth) AS (
266
+ SELECT ?, 0
267
+ UNION
268
+ SELECT CASE WHEN e.src = reach.id THEN e.dst ELSE e.src END,
269
+ reach.depth + 1
270
+ FROM reach
271
+ JOIN ${T}edge e ON (e.src = reach.id OR e.dst = reach.id)
272
+ WHERE reach.depth < ?
273
+ )
274
+ SELECT en.id, en.name, en.kind, en.body,
275
+ en.created_at AS createdAt, MIN(reach.depth) AS depth
276
+ FROM reach
277
+ JOIN ${T}entity en ON en.id = reach.id
278
+ WHERE reach.depth > 0 AND en.id <> ?
279
+ GROUP BY en.id
280
+ ORDER BY depth, en.name
281
+ LIMIT ?`,
282
+ // The seed reappears at depth >0 via a round-trip on undirected edges;
283
+ // exclude it explicitly so it isn't listed as its own neighbor.
284
+ params: [seedId, depth, seedId, limit]
285
+ };
286
+ }
287
+ function buildEdgesAmong(ids) {
288
+ if (!ids.length) {
289
+ return { sql: `SELECT id, src, rel, dst, source, created_at AS createdAt FROM ${T}edge WHERE 0`, params: [] };
290
+ }
291
+ const ph = ids.map(() => "?").join(", ");
292
+ return {
293
+ sql: `SELECT id, src, rel, dst, source, created_at AS createdAt
294
+ FROM ${T}edge
295
+ WHERE src IN (${ph}) AND dst IN (${ph})`,
296
+ params: [...ids, ...ids]
297
+ };
298
+ }
299
+ var EXTRACTION_SYSTEM_PROMPT = `You extract durable, shareable context from raw text for a team of AI agents that will read it later by keyword.
300
+
301
+ Return JSON: { "facts": [...], "entities": [...], "edges": [...] }.
302
+ - facts: atomic, self-contained statements worth remembering across sessions \u2014 decisions, conventions, constraints, identifiers, preferences. Each: { "body": string, "topic"?: string, "tags"?: string[] }. Prefer specific and stable over transient chatter.
303
+ - entities: named things \u2014 services, people, tables, repos, concepts. Each: { "name": string, "kind"?: string, "body"?: short description }.
304
+ - edges: relationships between entities, referencing entity names. Each: { "src": name, "rel": string, "dst": name }.
305
+
306
+ Be conservative: omit anything ephemeral or low-value. Use lowercase tags. If nothing is worth keeping, return empty arrays.`;
307
+ function buildExtractionMessages(raw, opts = {}) {
308
+ return [
309
+ { role: "system", content: EXTRACTION_SYSTEM_PROMPT },
310
+ {
311
+ role: "user",
312
+ content: (opts.hint ? `Context: ${opts.hint}
313
+
314
+ ` : "") + raw
315
+ }
316
+ ];
317
+ }
318
+ var EXTRACTION_JSON_SCHEMA = {
319
+ type: "object",
320
+ properties: {
321
+ facts: {
322
+ type: "array",
323
+ items: {
324
+ type: "object",
325
+ properties: {
326
+ body: { type: "string" },
327
+ topic: { type: "string" },
328
+ tags: { type: "array", items: { type: "string" } }
329
+ },
330
+ required: ["body"]
331
+ }
332
+ },
333
+ entities: {
334
+ type: "array",
335
+ items: {
336
+ type: "object",
337
+ properties: {
338
+ name: { type: "string" },
339
+ kind: { type: "string" },
340
+ body: { type: "string" }
341
+ },
342
+ required: ["name"]
343
+ }
344
+ },
345
+ edges: {
346
+ type: "array",
347
+ items: {
348
+ type: "object",
349
+ properties: {
350
+ src: { type: "string" },
351
+ rel: { type: "string" },
352
+ dst: { type: "string" }
353
+ },
354
+ required: ["src", "rel", "dst"]
355
+ }
356
+ }
357
+ }
358
+ };
359
+ // Annotate the CommonJS export names for ESM import in node:
360
+ 0 && (module.exports = {
361
+ EXTRACTION_JSON_SCHEMA,
362
+ EXTRACTION_SYSTEM_PROMPT,
363
+ SCHEMA_SQL,
364
+ SCHEMA_VERSION,
365
+ TABLE_PREFIX,
366
+ buildByTag,
367
+ buildEdgesAmong,
368
+ buildExtractionMessages,
369
+ buildForget,
370
+ buildGet,
371
+ buildInit,
372
+ buildNeighborEntities,
373
+ buildRecall,
374
+ buildRecent,
375
+ buildRemember,
376
+ buildStats,
377
+ buildSupersede,
378
+ buildUpsertEdge,
379
+ buildUpsertEntity,
380
+ entityId,
381
+ normalizeTags,
382
+ toFtsQuery
383
+ });
384
+ //# sourceMappingURL=core.cjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/core.ts"],"sourcesContent":["/**\n * @persql/context/core — zero-runtime-dependency engine.\n *\n * Schema DDL, SQL builders, and the extraction/compaction contracts shared by\n * the `@persql/context` SDK (client-side, over `@persql/sdk`) and the hosted\n * PerSQL Context worker (server-side, over `@persql/ai`). Keeping both sides on\n * the same builders means client-recall and server-recall can never drift.\n *\n * Retrieval is lexical: FTS5 with the porter stemmer, BM25-ranked. No vectors.\n */\n\nexport const SCHEMA_VERSION = 1;\nexport const TABLE_PREFIX = \"ctx_\";\nconst T = TABLE_PREFIX;\n\nexport type MemoryKind = \"fact\" | \"episode\" | \"artifact\";\n\nexport interface MemoryRow {\n id: string;\n kind: MemoryKind;\n topic: string | null;\n body: string;\n tags: string | null;\n source: string | null;\n createdAt: number;\n supersededBy: string | null;\n}\n\nexport interface Entity {\n id: string;\n name: string;\n kind: string | null;\n body: string | null;\n createdAt: number;\n}\n\nexport interface Edge {\n id: string;\n src: string;\n rel: string;\n dst: string;\n source: string | null;\n createdAt: number;\n}\n\nexport interface RememberInput {\n body: string;\n topic?: string;\n kind?: MemoryKind;\n tags?: string[] | string;\n source?: string;\n supersedes?: string | string[];\n id?: string;\n}\n\nexport interface EntityInput {\n name: string;\n kind?: string;\n body?: string;\n id?: string;\n}\n\nexport interface EdgeInput {\n src: string;\n rel: string;\n dst: string;\n source?: string;\n}\n\nexport interface RecallOptions {\n limit?: number;\n kind?: MemoryKind;\n operator?: \"or\" | \"and\";\n mode?: \"terms\" | \"raw\";\n sinceMs?: number;\n withinDays?: number;\n includeSuperseded?: boolean;\n}\n\nexport interface ListOptions {\n limit?: number;\n kind?: MemoryKind;\n includeSuperseded?: boolean;\n}\n\n/** The shape an extractor (LLM or the hosted worker) returns from raw text. */\nexport interface ExtractedContext {\n facts?: Array<{ body: string; topic?: string; tags?: string[]; kind?: MemoryKind }>;\n entities?: EntityInput[];\n edges?: EdgeInput[];\n}\n\nexport interface SqlStatement {\n sql: string;\n params: unknown[];\n}\n\n// ---------------------------------------------------------------------------\n// Schema\n// ---------------------------------------------------------------------------\n\nexport const SCHEMA_SQL: string[] = [\n `CREATE TABLE IF NOT EXISTS ${T}memory (\n id TEXT PRIMARY KEY,\n kind TEXT NOT NULL DEFAULT 'fact',\n topic TEXT,\n body TEXT NOT NULL,\n tags TEXT,\n source TEXT,\n created_at INTEGER NOT NULL,\n superseded_by TEXT\n )`,\n `CREATE INDEX IF NOT EXISTS ${T}memory_kind ON ${T}memory(kind)`,\n `CREATE INDEX IF NOT EXISTS ${T}memory_created ON ${T}memory(created_at)`,\n `CREATE INDEX IF NOT EXISTS ${T}memory_superseded ON ${T}memory(superseded_by)`,\n // External-content FTS index: body text is not duplicated, only indexed.\n // Porter stemmer so \"invoice\" recalls \"invoicing\" without embeddings.\n `CREATE VIRTUAL TABLE IF NOT EXISTS ${T}memory_fts USING fts5(\n topic, body, tags,\n content='${T}memory', content_rowid='rowid',\n tokenize='porter unicode61'\n )`,\n `CREATE TRIGGER IF NOT EXISTS ${T}memory_ai AFTER INSERT ON ${T}memory BEGIN\n INSERT INTO ${T}memory_fts(rowid, topic, body, tags)\n VALUES (new.rowid, new.topic, new.body, new.tags);\n END`,\n `CREATE TRIGGER IF NOT EXISTS ${T}memory_ad AFTER DELETE ON ${T}memory BEGIN\n INSERT INTO ${T}memory_fts(${T}memory_fts, rowid, topic, body, tags)\n VALUES ('delete', old.rowid, old.topic, old.body, old.tags);\n END`,\n `CREATE TRIGGER IF NOT EXISTS ${T}memory_au AFTER UPDATE ON ${T}memory BEGIN\n INSERT INTO ${T}memory_fts(${T}memory_fts, rowid, topic, body, tags)\n VALUES ('delete', old.rowid, old.topic, old.body, old.tags);\n INSERT INTO ${T}memory_fts(rowid, topic, body, tags)\n VALUES (new.rowid, new.topic, new.body, new.tags);\n END`,\n `CREATE TABLE IF NOT EXISTS ${T}entity (\n id TEXT PRIMARY KEY,\n name TEXT NOT NULL,\n kind TEXT,\n body TEXT,\n created_at INTEGER NOT NULL\n )`,\n `CREATE TABLE IF NOT EXISTS ${T}edge (\n id TEXT PRIMARY KEY,\n src TEXT NOT NULL,\n rel TEXT NOT NULL,\n dst TEXT NOT NULL,\n source TEXT,\n created_at INTEGER NOT NULL\n )`,\n `CREATE INDEX IF NOT EXISTS ${T}edge_src ON ${T}edge(src)`,\n `CREATE INDEX IF NOT EXISTS ${T}edge_dst ON ${T}edge(dst)`,\n];\n\nexport function buildInit(): SqlStatement[] {\n return SCHEMA_SQL.map((sql) => ({ sql, params: [] }));\n}\n\n// ---------------------------------------------------------------------------\n// Helpers\n// ---------------------------------------------------------------------------\n\nconst FTS_OPERATORS = new Set([\"and\", \"or\", \"not\", \"near\"]);\n\nexport function normalizeTags(tags?: string[] | string | null): string | null {\n if (!tags) return null;\n const arr = Array.isArray(tags) ? tags : String(tags).split(/[,\\s]+/);\n const norm = Array.from(\n new Set(arr.map((t) => t.trim().toLowerCase()).filter(Boolean))\n );\n return norm.length ? norm.join(\" \") : null;\n}\n\n/**\n * Turn arbitrary user text into a safe FTS5 MATCH expression. `terms` mode\n * (default) extracts word tokens, drops bare boolean operators, and ORs the\n * rest as quoted terms — so a caller can paste \"invoice OR billing\" or just\n * \"invoice billing\" and neither crashes the parser nor injects FTS syntax.\n * `raw` mode passes a hand-written FTS expression through untouched.\n */\nexport function toFtsQuery(\n input: string,\n opts: { operator?: \"or\" | \"and\"; mode?: \"terms\" | \"raw\" } = {}\n): string | null {\n if (opts.mode === \"raw\") return input.trim() || null;\n const tokens = (input.match(/[\\p{L}\\p{N}_]+/gu) ?? []).filter(\n (t) => !FTS_OPERATORS.has(t.toLowerCase())\n );\n if (!tokens.length) return null;\n const joiner = opts.operator === \"and\" ? \" AND \" : \" OR \";\n return tokens.map((t) => `\"${t}\"`).join(joiner);\n}\n\nfunction slugId(prefix: string, s: string): string {\n const base = s\n .toLowerCase()\n .replace(/[^a-z0-9]+/g, \"-\")\n .replace(/^-+|-+$/g, \"\")\n .slice(0, 96);\n return prefix + (base || \"x\");\n}\n\n/** Deterministic id for an entity, keyed on its name (case-insensitive). */\nexport function entityId(name: string): string {\n return slugId(\"e_\", name);\n}\n\nfunction resolveEntityRef(ref: string): string {\n return ref.startsWith(\"e_\") ? ref : entityId(ref);\n}\n\nfunction edgeId(srcId: string, rel: string, dstId: string): string {\n return slugId(\"x_\", `${srcId}|${rel}|${dstId}`);\n}\n\nconst MEMORY_COLS =\n \"id, kind, topic, body, tags, source, created_at AS createdAt, superseded_by AS supersededBy\";\n\n// ---------------------------------------------------------------------------\n// Memory builders\n// ---------------------------------------------------------------------------\n\nexport function buildRemember(\n input: RememberInput,\n now: number,\n id: string\n): SqlStatement {\n return {\n sql: `INSERT INTO ${T}memory (id, kind, topic, body, tags, source, created_at, superseded_by)\n VALUES (?, ?, ?, ?, ?, ?, ?, NULL)`,\n params: [\n id,\n input.kind ?? \"fact\",\n input.topic ?? null,\n input.body,\n normalizeTags(input.tags),\n input.source ?? null,\n now,\n ],\n };\n}\n\nexport function buildSupersede(oldId: string, newId: string): SqlStatement {\n return {\n sql: `UPDATE ${T}memory SET superseded_by = ? WHERE id = ? AND superseded_by IS NULL`,\n params: [newId, oldId],\n };\n}\n\nexport function buildForget(id: string): SqlStatement {\n return { sql: `DELETE FROM ${T}memory WHERE id = ?`, params: [id] };\n}\n\nexport function buildGet(id: string): SqlStatement {\n return {\n sql: `SELECT ${MEMORY_COLS} FROM ${T}memory WHERE id = ?`,\n params: [id],\n };\n}\n\nexport function buildRecall(\n query: string,\n opts: RecallOptions = {}\n): SqlStatement | null {\n const match = toFtsQuery(query, { operator: opts.operator, mode: opts.mode });\n if (!match) return null;\n const where: string[] = [`${T}memory_fts MATCH ?`];\n const params: unknown[] = [match];\n if (!opts.includeSuperseded) where.push(\"m.superseded_by IS NULL\");\n if (opts.kind) {\n where.push(\"m.kind = ?\");\n params.push(opts.kind);\n }\n if (opts.sinceMs != null) {\n where.push(\"m.created_at >= ?\");\n params.push(opts.sinceMs);\n }\n params.push(opts.limit ?? 8);\n return {\n sql: `SELECT m.id, m.kind, m.topic, m.body, m.tags, m.source,\n m.created_at AS createdAt, m.superseded_by AS supersededBy\n FROM ${T}memory_fts\n JOIN ${T}memory m ON m.rowid = ${T}memory_fts.rowid\n WHERE ${where.join(\" AND \")}\n ORDER BY ${T}memory_fts.rank\n LIMIT ?`,\n params,\n };\n}\n\nexport function buildRecent(opts: ListOptions = {}): SqlStatement {\n const where: string[] = [];\n const params: unknown[] = [];\n if (!opts.includeSuperseded) where.push(\"superseded_by IS NULL\");\n if (opts.kind) {\n where.push(\"kind = ?\");\n params.push(opts.kind);\n }\n params.push(opts.limit ?? 20);\n return {\n sql: `SELECT ${MEMORY_COLS} FROM ${T}memory\n ${where.length ? \"WHERE \" + where.join(\" AND \") : \"\"}\n ORDER BY created_at DESC\n LIMIT ?`,\n params,\n };\n}\n\nexport function buildByTag(tag: string, opts: ListOptions = {}): SqlStatement {\n const t = tag.trim().toLowerCase();\n const where: string[] = [\"(' ' || COALESCE(tags, '') || ' ') LIKE ?\"];\n const params: unknown[] = [`% ${t} %`];\n if (!opts.includeSuperseded) where.push(\"superseded_by IS NULL\");\n if (opts.kind) {\n where.push(\"kind = ?\");\n params.push(opts.kind);\n }\n params.push(opts.limit ?? 20);\n return {\n sql: `SELECT ${MEMORY_COLS} FROM ${T}memory\n WHERE ${where.join(\" AND \")}\n ORDER BY created_at DESC\n LIMIT ?`,\n params,\n };\n}\n\nexport function buildStats(): SqlStatement {\n return {\n sql: `SELECT\n (SELECT count(*) FROM ${T}memory WHERE superseded_by IS NULL) AS facts,\n (SELECT count(*) FROM ${T}memory) AS facts_total,\n (SELECT count(*) FROM ${T}entity) AS entities,\n (SELECT count(*) FROM ${T}edge) AS edges`,\n params: [],\n };\n}\n\n// ---------------------------------------------------------------------------\n// Graph builders\n// ---------------------------------------------------------------------------\n\nexport function buildUpsertEntity(e: EntityInput, now: number): SqlStatement {\n const id = e.id ?? entityId(e.name);\n return {\n sql: `INSERT INTO ${T}entity (id, name, kind, body, created_at)\n VALUES (?, ?, ?, ?, ?)\n ON CONFLICT(id) DO UPDATE SET\n name = excluded.name,\n kind = COALESCE(excluded.kind, ${T}entity.kind),\n body = COALESCE(excluded.body, ${T}entity.body)`,\n params: [id, e.name, e.kind ?? null, e.body ?? null, now],\n };\n}\n\nexport function buildUpsertEdge(e: EdgeInput, now: number): SqlStatement {\n const srcId = resolveEntityRef(e.src);\n const dstId = resolveEntityRef(e.dst);\n const id = edgeId(srcId, e.rel, dstId);\n return {\n sql: `INSERT INTO ${T}edge (id, src, rel, dst, source, created_at)\n VALUES (?, ?, ?, ?, ?, ?)\n ON CONFLICT(id) DO UPDATE SET\n source = COALESCE(excluded.source, ${T}edge.source)`,\n params: [id, srcId, e.rel, dstId, e.source ?? null, now],\n };\n}\n\n/** Entities reachable from a seed within `depth` hops (seed excluded). */\nexport function buildNeighborEntities(\n seedId: string,\n depth: number,\n limit: number\n): SqlStatement {\n return {\n sql: `WITH RECURSIVE reach(id, depth) AS (\n SELECT ?, 0\n UNION\n SELECT CASE WHEN e.src = reach.id THEN e.dst ELSE e.src END,\n reach.depth + 1\n FROM reach\n JOIN ${T}edge e ON (e.src = reach.id OR e.dst = reach.id)\n WHERE reach.depth < ?\n )\n SELECT en.id, en.name, en.kind, en.body,\n en.created_at AS createdAt, MIN(reach.depth) AS depth\n FROM reach\n JOIN ${T}entity en ON en.id = reach.id\n WHERE reach.depth > 0 AND en.id <> ?\n GROUP BY en.id\n ORDER BY depth, en.name\n LIMIT ?`,\n // The seed reappears at depth >0 via a round-trip on undirected edges;\n // exclude it explicitly so it isn't listed as its own neighbor.\n params: [seedId, depth, seedId, limit],\n };\n}\n\nexport function buildEdgesAmong(ids: string[]): SqlStatement {\n if (!ids.length) {\n return { sql: `SELECT id, src, rel, dst, source, created_at AS createdAt FROM ${T}edge WHERE 0`, params: [] };\n }\n const ph = ids.map(() => \"?\").join(\", \");\n return {\n sql: `SELECT id, src, rel, dst, source, created_at AS createdAt\n FROM ${T}edge\n WHERE src IN (${ph}) AND dst IN (${ph})`,\n params: [...ids, ...ids],\n };\n}\n\n// ---------------------------------------------------------------------------\n// Extraction / compaction contracts (used by the hosted worker + documented\n// for bring-your-own-LLM callers; kept here so the prompt is versioned with\n// the schema it has to populate).\n// ---------------------------------------------------------------------------\n\nexport const EXTRACTION_SYSTEM_PROMPT = `You extract durable, shareable context from raw text for a team of AI agents that will read it later by keyword.\n\nReturn JSON: { \"facts\": [...], \"entities\": [...], \"edges\": [...] }.\n- facts: atomic, self-contained statements worth remembering across sessions — decisions, conventions, constraints, identifiers, preferences. Each: { \"body\": string, \"topic\"?: string, \"tags\"?: string[] }. Prefer specific and stable over transient chatter.\n- entities: named things — services, people, tables, repos, concepts. Each: { \"name\": string, \"kind\"?: string, \"body\"?: short description }.\n- edges: relationships between entities, referencing entity names. Each: { \"src\": name, \"rel\": string, \"dst\": name }.\n\nBe conservative: omit anything ephemeral or low-value. Use lowercase tags. If nothing is worth keeping, return empty arrays.`;\n\nexport function buildExtractionMessages(\n raw: string,\n opts: { hint?: string } = {}\n): Array<{ role: \"system\" | \"user\"; content: string }> {\n return [\n { role: \"system\", content: EXTRACTION_SYSTEM_PROMPT },\n {\n role: \"user\",\n content: (opts.hint ? `Context: ${opts.hint}\\n\\n` : \"\") + raw,\n },\n ];\n}\n\nexport const EXTRACTION_JSON_SCHEMA = {\n type: \"object\",\n properties: {\n facts: {\n type: \"array\",\n items: {\n type: \"object\",\n properties: {\n body: { type: \"string\" },\n topic: { type: \"string\" },\n tags: { type: \"array\", items: { type: \"string\" } },\n },\n required: [\"body\"],\n },\n },\n entities: {\n type: \"array\",\n items: {\n type: \"object\",\n properties: {\n name: { type: \"string\" },\n kind: { type: \"string\" },\n body: { type: \"string\" },\n },\n required: [\"name\"],\n },\n },\n edges: {\n type: \"array\",\n items: {\n type: \"object\",\n properties: {\n src: { type: \"string\" },\n rel: { type: \"string\" },\n dst: { type: \"string\" },\n },\n required: [\"src\", \"rel\", \"dst\"],\n },\n },\n },\n} as const;\n"],"mappings":"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","names":[]}
@@ -0,0 +1,188 @@
1
+ /**
2
+ * @persql/context/core — zero-runtime-dependency engine.
3
+ *
4
+ * Schema DDL, SQL builders, and the extraction/compaction contracts shared by
5
+ * the `@persql/context` SDK (client-side, over `@persql/sdk`) and the hosted
6
+ * PerSQL Context worker (server-side, over `@persql/ai`). Keeping both sides on
7
+ * the same builders means client-recall and server-recall can never drift.
8
+ *
9
+ * Retrieval is lexical: FTS5 with the porter stemmer, BM25-ranked. No vectors.
10
+ */
11
+ declare const SCHEMA_VERSION = 1;
12
+ declare const TABLE_PREFIX = "ctx_";
13
+ type MemoryKind = "fact" | "episode" | "artifact";
14
+ interface MemoryRow {
15
+ id: string;
16
+ kind: MemoryKind;
17
+ topic: string | null;
18
+ body: string;
19
+ tags: string | null;
20
+ source: string | null;
21
+ createdAt: number;
22
+ supersededBy: string | null;
23
+ }
24
+ interface Entity {
25
+ id: string;
26
+ name: string;
27
+ kind: string | null;
28
+ body: string | null;
29
+ createdAt: number;
30
+ }
31
+ interface Edge {
32
+ id: string;
33
+ src: string;
34
+ rel: string;
35
+ dst: string;
36
+ source: string | null;
37
+ createdAt: number;
38
+ }
39
+ interface RememberInput {
40
+ body: string;
41
+ topic?: string;
42
+ kind?: MemoryKind;
43
+ tags?: string[] | string;
44
+ source?: string;
45
+ supersedes?: string | string[];
46
+ id?: string;
47
+ }
48
+ interface EntityInput {
49
+ name: string;
50
+ kind?: string;
51
+ body?: string;
52
+ id?: string;
53
+ }
54
+ interface EdgeInput {
55
+ src: string;
56
+ rel: string;
57
+ dst: string;
58
+ source?: string;
59
+ }
60
+ interface RecallOptions {
61
+ limit?: number;
62
+ kind?: MemoryKind;
63
+ operator?: "or" | "and";
64
+ mode?: "terms" | "raw";
65
+ sinceMs?: number;
66
+ withinDays?: number;
67
+ includeSuperseded?: boolean;
68
+ }
69
+ interface ListOptions {
70
+ limit?: number;
71
+ kind?: MemoryKind;
72
+ includeSuperseded?: boolean;
73
+ }
74
+ /** The shape an extractor (LLM or the hosted worker) returns from raw text. */
75
+ interface ExtractedContext {
76
+ facts?: Array<{
77
+ body: string;
78
+ topic?: string;
79
+ tags?: string[];
80
+ kind?: MemoryKind;
81
+ }>;
82
+ entities?: EntityInput[];
83
+ edges?: EdgeInput[];
84
+ }
85
+ interface SqlStatement {
86
+ sql: string;
87
+ params: unknown[];
88
+ }
89
+ declare const SCHEMA_SQL: string[];
90
+ declare function buildInit(): SqlStatement[];
91
+ declare function normalizeTags(tags?: string[] | string | null): string | null;
92
+ /**
93
+ * Turn arbitrary user text into a safe FTS5 MATCH expression. `terms` mode
94
+ * (default) extracts word tokens, drops bare boolean operators, and ORs the
95
+ * rest as quoted terms — so a caller can paste "invoice OR billing" or just
96
+ * "invoice billing" and neither crashes the parser nor injects FTS syntax.
97
+ * `raw` mode passes a hand-written FTS expression through untouched.
98
+ */
99
+ declare function toFtsQuery(input: string, opts?: {
100
+ operator?: "or" | "and";
101
+ mode?: "terms" | "raw";
102
+ }): string | null;
103
+ /** Deterministic id for an entity, keyed on its name (case-insensitive). */
104
+ declare function entityId(name: string): string;
105
+ declare function buildRemember(input: RememberInput, now: number, id: string): SqlStatement;
106
+ declare function buildSupersede(oldId: string, newId: string): SqlStatement;
107
+ declare function buildForget(id: string): SqlStatement;
108
+ declare function buildGet(id: string): SqlStatement;
109
+ declare function buildRecall(query: string, opts?: RecallOptions): SqlStatement | null;
110
+ declare function buildRecent(opts?: ListOptions): SqlStatement;
111
+ declare function buildByTag(tag: string, opts?: ListOptions): SqlStatement;
112
+ declare function buildStats(): SqlStatement;
113
+ declare function buildUpsertEntity(e: EntityInput, now: number): SqlStatement;
114
+ declare function buildUpsertEdge(e: EdgeInput, now: number): SqlStatement;
115
+ /** Entities reachable from a seed within `depth` hops (seed excluded). */
116
+ declare function buildNeighborEntities(seedId: string, depth: number, limit: number): SqlStatement;
117
+ declare function buildEdgesAmong(ids: string[]): SqlStatement;
118
+ declare const EXTRACTION_SYSTEM_PROMPT = "You extract durable, shareable context from raw text for a team of AI agents that will read it later by keyword.\n\nReturn JSON: { \"facts\": [...], \"entities\": [...], \"edges\": [...] }.\n- facts: atomic, self-contained statements worth remembering across sessions \u2014 decisions, conventions, constraints, identifiers, preferences. Each: { \"body\": string, \"topic\"?: string, \"tags\"?: string[] }. Prefer specific and stable over transient chatter.\n- entities: named things \u2014 services, people, tables, repos, concepts. Each: { \"name\": string, \"kind\"?: string, \"body\"?: short description }.\n- edges: relationships between entities, referencing entity names. Each: { \"src\": name, \"rel\": string, \"dst\": name }.\n\nBe conservative: omit anything ephemeral or low-value. Use lowercase tags. If nothing is worth keeping, return empty arrays.";
119
+ declare function buildExtractionMessages(raw: string, opts?: {
120
+ hint?: string;
121
+ }): Array<{
122
+ role: "system" | "user";
123
+ content: string;
124
+ }>;
125
+ declare const EXTRACTION_JSON_SCHEMA: {
126
+ readonly type: "object";
127
+ readonly properties: {
128
+ readonly facts: {
129
+ readonly type: "array";
130
+ readonly items: {
131
+ readonly type: "object";
132
+ readonly properties: {
133
+ readonly body: {
134
+ readonly type: "string";
135
+ };
136
+ readonly topic: {
137
+ readonly type: "string";
138
+ };
139
+ readonly tags: {
140
+ readonly type: "array";
141
+ readonly items: {
142
+ readonly type: "string";
143
+ };
144
+ };
145
+ };
146
+ readonly required: readonly ["body"];
147
+ };
148
+ };
149
+ readonly entities: {
150
+ readonly type: "array";
151
+ readonly items: {
152
+ readonly type: "object";
153
+ readonly properties: {
154
+ readonly name: {
155
+ readonly type: "string";
156
+ };
157
+ readonly kind: {
158
+ readonly type: "string";
159
+ };
160
+ readonly body: {
161
+ readonly type: "string";
162
+ };
163
+ };
164
+ readonly required: readonly ["name"];
165
+ };
166
+ };
167
+ readonly edges: {
168
+ readonly type: "array";
169
+ readonly items: {
170
+ readonly type: "object";
171
+ readonly properties: {
172
+ readonly src: {
173
+ readonly type: "string";
174
+ };
175
+ readonly rel: {
176
+ readonly type: "string";
177
+ };
178
+ readonly dst: {
179
+ readonly type: "string";
180
+ };
181
+ };
182
+ readonly required: readonly ["src", "rel", "dst"];
183
+ };
184
+ };
185
+ };
186
+ };
187
+
188
+ export { EXTRACTION_JSON_SCHEMA, EXTRACTION_SYSTEM_PROMPT, type Edge, type EdgeInput, type Entity, type EntityInput, type ExtractedContext, type ListOptions, type MemoryKind, type MemoryRow, type RecallOptions, type RememberInput, SCHEMA_SQL, SCHEMA_VERSION, type SqlStatement, TABLE_PREFIX, buildByTag, buildEdgesAmong, buildExtractionMessages, buildForget, buildGet, buildInit, buildNeighborEntities, buildRecall, buildRecent, buildRemember, buildStats, buildSupersede, buildUpsertEdge, buildUpsertEntity, entityId, normalizeTags, toFtsQuery };