@persql/context 0.1.0 → 1.0.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 CHANGED
@@ -25,62 +25,59 @@ __export(core_exports, {
25
25
  SCHEMA_SQL: () => SCHEMA_SQL,
26
26
  SCHEMA_VERSION: () => SCHEMA_VERSION,
27
27
  TABLE_PREFIX: () => TABLE_PREFIX,
28
- buildByTag: () => buildByTag,
28
+ buildDetectLegacySchema: () => buildDetectLegacySchema,
29
+ buildDropLegacy: () => buildDropLegacy,
29
30
  buildEdgesAmong: () => buildEdgesAmong,
30
31
  buildExtractionMessages: () => buildExtractionMessages,
31
32
  buildForget: () => buildForget,
32
33
  buildGet: () => buildGet,
34
+ buildIndex: () => buildIndex,
33
35
  buildInit: () => buildInit,
34
36
  buildNeighborEntities: () => buildNeighborEntities,
35
37
  buildRecall: () => buildRecall,
36
- buildRecent: () => buildRecent,
37
38
  buildRemember: () => buildRemember,
38
39
  buildStats: () => buildStats,
39
- buildSupersede: () => buildSupersede,
40
40
  buildUpsertEdge: () => buildUpsertEdge,
41
41
  buildUpsertEntity: () => buildUpsertEntity,
42
42
  entityId: () => entityId,
43
- normalizeTags: () => normalizeTags,
44
43
  toFtsQuery: () => toFtsQuery
45
44
  });
46
45
  module.exports = __toCommonJS(core_exports);
47
- var SCHEMA_VERSION = 1;
46
+ var SCHEMA_VERSION = 2;
48
47
  var TABLE_PREFIX = "ctx_";
49
48
  var T = TABLE_PREFIX;
50
49
  var SCHEMA_SQL = [
51
50
  `CREATE TABLE IF NOT EXISTS ${T}memory (
52
51
  id TEXT PRIMARY KEY,
53
- kind TEXT NOT NULL DEFAULT 'fact',
54
- topic TEXT,
52
+ name TEXT NOT NULL,
53
+ description TEXT NOT NULL,
54
+ type TEXT NOT NULL DEFAULT 'project',
55
55
  body TEXT NOT NULL,
56
- tags TEXT,
57
56
  source TEXT,
58
57
  created_at INTEGER NOT NULL,
59
- superseded_by TEXT
58
+ updated_at INTEGER NOT NULL
60
59
  )`,
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.
60
+ `CREATE UNIQUE INDEX IF NOT EXISTS ${T}memory_name ON ${T}memory(name)`,
61
+ // External-content FTS on name+description+body. Porter stemmer so
62
+ // "invoice" recalls "invoicing" without embeddings.
66
63
  `CREATE VIRTUAL TABLE IF NOT EXISTS ${T}memory_fts USING fts5(
67
- topic, body, tags,
64
+ name, description, body,
68
65
  content='${T}memory', content_rowid='rowid',
69
66
  tokenize='porter unicode61'
70
67
  )`,
71
68
  `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);
69
+ INSERT INTO ${T}memory_fts(rowid, name, description, body)
70
+ VALUES (new.rowid, new.name, new.description, new.body);
74
71
  END`,
75
72
  `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);
73
+ INSERT INTO ${T}memory_fts(${T}memory_fts, rowid, name, description, body)
74
+ VALUES ('delete', old.rowid, old.name, old.description, old.body);
78
75
  END`,
79
76
  `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);
77
+ INSERT INTO ${T}memory_fts(${T}memory_fts, rowid, name, description, body)
78
+ VALUES ('delete', old.rowid, old.name, old.description, old.body);
79
+ INSERT INTO ${T}memory_fts(rowid, name, description, body)
80
+ VALUES (new.rowid, new.name, new.description, new.body);
84
81
  END`,
85
82
  `CREATE TABLE IF NOT EXISTS ${T}entity (
86
83
  id TEXT PRIMARY KEY,
@@ -103,15 +100,21 @@ var SCHEMA_SQL = [
103
100
  function buildInit() {
104
101
  return SCHEMA_SQL.map((sql) => ({ sql, params: [] }));
105
102
  }
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;
103
+ function buildDetectLegacySchema() {
104
+ return {
105
+ sql: `SELECT count(*) AS n FROM pragma_table_info('${T}memory') WHERE name='topic'`,
106
+ params: []
107
+ };
114
108
  }
109
+ function buildDropLegacy() {
110
+ return [
111
+ { sql: `DROP TABLE IF EXISTS ${T}memory_fts`, params: [] },
112
+ { sql: `DROP TABLE IF EXISTS ${T}memory`, params: [] },
113
+ { sql: `DROP TABLE IF EXISTS ${T}entity`, params: [] },
114
+ { sql: `DROP TABLE IF EXISTS ${T}edge`, params: [] }
115
+ ];
116
+ }
117
+ var FTS_OPERATORS = /* @__PURE__ */ new Set(["and", "or", "not", "near"]);
115
118
  function toFtsQuery(input, opts = {}) {
116
119
  if (opts.mode === "raw") return input.trim() || null;
117
120
  const tokens = (input.match(/[\p{L}\p{N}_]+/gu) ?? []).filter(
@@ -134,103 +137,71 @@ function resolveEntityRef(ref) {
134
137
  function edgeId(srcId, rel, dstId) {
135
138
  return slugId("x_", `${srcId}|${rel}|${dstId}`);
136
139
  }
137
- var MEMORY_COLS = "id, kind, topic, body, tags, source, created_at AS createdAt, superseded_by AS supersededBy";
140
+ var MEMORY_COLS = "id, name, description, type, body, source, created_at AS createdAt, updated_at AS updatedAt";
138
141
  function buildRemember(input, now, id) {
139
142
  return {
140
- sql: `INSERT INTO ${T}memory (id, kind, topic, body, tags, source, created_at, superseded_by)
141
- VALUES (?, ?, ?, ?, ?, ?, ?, NULL)`,
143
+ sql: `INSERT INTO ${T}memory (id, name, description, type, body, source, created_at, updated_at)
144
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?)
145
+ ON CONFLICT(name) DO UPDATE SET
146
+ description = excluded.description,
147
+ body = excluded.body,
148
+ type = excluded.type,
149
+ source = excluded.source,
150
+ updated_at = excluded.updated_at`,
142
151
  params: [
143
152
  id,
144
- input.kind ?? "fact",
145
- input.topic ?? null,
153
+ input.name,
154
+ input.description,
155
+ input.type ?? "project",
146
156
  input.body,
147
- normalizeTags(input.tags),
148
157
  input.source ?? null,
158
+ now,
149
159
  now
150
160
  ]
151
161
  };
152
162
  }
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] };
163
+ function buildForget(name) {
164
+ return { sql: `DELETE FROM ${T}memory WHERE name = ?`, params: [name] };
161
165
  }
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);
166
+ function buildGet(name) {
183
167
  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
168
+ sql: `SELECT ${MEMORY_COLS} FROM ${T}memory WHERE name = ?`,
169
+ params: [name]
192
170
  };
193
171
  }
194
- function buildRecent(opts = {}) {
172
+ function buildIndex(opts = {}) {
195
173
  const where = [];
196
174
  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);
175
+ if (opts.type) {
176
+ where.push("type = ?");
177
+ params.push(opts.type);
201
178
  }
202
- params.push(opts.limit ?? 20);
179
+ params.push(opts.limit ?? 50);
203
180
  return {
204
- sql: `SELECT ${MEMORY_COLS} FROM ${T}memory
181
+ sql: `SELECT id, name, description, type, updated_at AS updatedAt FROM ${T}memory
205
182
  ${where.length ? "WHERE " + where.join(" AND ") : ""}
206
- ORDER BY created_at DESC
207
- LIMIT ?`,
183
+ ORDER BY updated_at DESC LIMIT ?`,
208
184
  params
209
185
  };
210
186
  }
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);
187
+ function buildRecall(query, opts = {}) {
188
+ const match = toFtsQuery(query, { operator: opts.operator, mode: opts.mode });
189
+ if (!match) return null;
221
190
  return {
222
- sql: `SELECT ${MEMORY_COLS} FROM ${T}memory
223
- WHERE ${where.join(" AND ")}
224
- ORDER BY created_at DESC
191
+ sql: `SELECT m.id, m.name, m.description, m.type, m.body, m.source,
192
+ m.created_at AS createdAt, m.updated_at AS updatedAt
193
+ FROM ${T}memory_fts
194
+ JOIN ${T}memory m ON m.rowid = ${T}memory_fts.rowid
195
+ WHERE ${T}memory_fts MATCH ?
196
+ ORDER BY ${T}memory_fts.rank
225
197
  LIMIT ?`,
226
- params
198
+ params: [match, opts.limit ?? 8]
227
199
  };
228
200
  }
229
201
  function buildStats() {
230
202
  return {
231
203
  sql: `SELECT
232
- (SELECT count(*) FROM ${T}memory WHERE superseded_by IS NULL) AS facts,
233
- (SELECT count(*) FROM ${T}memory) AS facts_total,
204
+ (SELECT count(*) FROM ${T}memory) AS memories,
234
205
  (SELECT count(*) FROM ${T}entity) AS entities,
235
206
  (SELECT count(*) FROM ${T}edge) AS edges`,
236
207
  params: []
@@ -279,8 +250,7 @@ function buildNeighborEntities(seedId, depth, limit) {
279
250
  GROUP BY en.id
280
251
  ORDER BY depth, en.name
281
252
  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.
253
+ // The seed appears at depth >0 on undirected round-trips exclude it.
284
254
  params: [seedId, depth, seedId, limit]
285
255
  };
286
256
  }
@@ -296,14 +266,21 @@ function buildEdgesAmong(ids) {
296
266
  params: [...ids, ...ids]
297
267
  };
298
268
  }
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.
269
+ var EXTRACTION_SYSTEM_PROMPT = `You extract durable, structured memories from a conversation or document for an AI agent that will recall them across sessions.
300
270
 
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 }.
271
+ Return JSON: { "memories": [...], "entities": [...], "edges": [...] }.
272
+ - memories: facts worth remembering across sessions \u2014 decisions, conventions, constraints, identifiers, preferences, schema details. Each:
273
+ {
274
+ "name": string (kebab-case slug, unique key \u2014 e.g. "auth-provider-choice", "todos-table-schema", "user-prefers-metrics"),
275
+ "description": string (one-liner shown in the always-loaded index),
276
+ "type": "user"|"feedback"|"project"|"reference",
277
+ "body": string (full markdown content with all relevant detail)
278
+ }
279
+ Types: user=who they are/preferences; feedback=guidance for agents; project=ongoing work/decisions; reference=where to find things.
280
+ - entities: named things \u2014 services, people, tables, repos. Each: { "name": string, "kind"?: string, "body"?: string }
281
+ - edges: relationships between entities. Each: { "src": name, "rel": string, "dst": name }
305
282
 
306
- Be conservative: omit anything ephemeral or low-value. Use lowercase tags. If nothing is worth keeping, return empty arrays.`;
283
+ Be conservative. Use unique, meaningful names. If nothing durable is found, return empty arrays.`;
307
284
  function buildExtractionMessages(raw, opts = {}) {
308
285
  return [
309
286
  { role: "system", content: EXTRACTION_SYSTEM_PROMPT },
@@ -318,16 +295,17 @@ function buildExtractionMessages(raw, opts = {}) {
318
295
  var EXTRACTION_JSON_SCHEMA = {
319
296
  type: "object",
320
297
  properties: {
321
- facts: {
298
+ memories: {
322
299
  type: "array",
323
300
  items: {
324
301
  type: "object",
325
302
  properties: {
326
- body: { type: "string" },
327
- topic: { type: "string" },
328
- tags: { type: "array", items: { type: "string" } }
303
+ name: { type: "string" },
304
+ description: { type: "string" },
305
+ type: { type: "string", enum: ["user", "feedback", "project", "reference"] },
306
+ body: { type: "string" }
329
307
  },
330
- required: ["body"]
308
+ required: ["name", "description", "body"]
331
309
  }
332
310
  },
333
311
  entities: {
@@ -363,22 +341,21 @@ var EXTRACTION_JSON_SCHEMA = {
363
341
  SCHEMA_SQL,
364
342
  SCHEMA_VERSION,
365
343
  TABLE_PREFIX,
366
- buildByTag,
344
+ buildDetectLegacySchema,
345
+ buildDropLegacy,
367
346
  buildEdgesAmong,
368
347
  buildExtractionMessages,
369
348
  buildForget,
370
349
  buildGet,
350
+ buildIndex,
371
351
  buildInit,
372
352
  buildNeighborEntities,
373
353
  buildRecall,
374
- buildRecent,
375
354
  buildRemember,
376
355
  buildStats,
377
- buildSupersede,
378
356
  buildUpsertEdge,
379
357
  buildUpsertEntity,
380
358
  entityId,
381
- normalizeTags,
382
359
  toFtsQuery
383
360
  });
384
361
  //# sourceMappingURL=core.cjs.map
package/dist/core.cjs.map CHANGED
@@ -1 +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":[]}
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 contract shared by the\n * `@persql/context` SDK (client-side, over `@persql/sdk`) and the hosted\n * PerSQL Context worker (server-side, over `@persql/ai`). Structured memories\n * with a name-keyed UPSERT + always-loaded index replace the old episode model.\n *\n * Retrieval is lexical: FTS5 with the porter stemmer, BM25-ranked. No vectors.\n */\n\nexport const SCHEMA_VERSION = 2;\nexport const TABLE_PREFIX = \"ctx_\";\nconst T = TABLE_PREFIX;\n\nexport type MemoryType = \"user\" | \"feedback\" | \"project\" | \"reference\";\n\nexport interface MemoryRow {\n id: string;\n name: string;\n description: string;\n type: MemoryType;\n body: string;\n source: string | null;\n createdAt: number;\n updatedAt: number;\n}\n\nexport interface IndexRow {\n id: string;\n name: string;\n description: string;\n type: MemoryType;\n updatedAt: number;\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 name: string;\n description: string;\n type?: MemoryType;\n body: string;\n source?: 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 operator?: \"or\" | \"and\";\n mode?: \"terms\" | \"raw\";\n}\n\nexport interface ListOptions {\n limit?: number;\n type?: MemoryType;\n}\n\n/** Structured memories extracted by an LLM from raw text. */\nexport interface ExtractedContext {\n memories?: Array<{\n name: string;\n description: string;\n type?: MemoryType;\n body: string;\n }>;\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 name TEXT NOT NULL,\n description TEXT NOT NULL,\n type TEXT NOT NULL DEFAULT 'project',\n body TEXT NOT NULL,\n source TEXT,\n created_at INTEGER NOT NULL,\n updated_at INTEGER NOT NULL\n )`,\n `CREATE UNIQUE INDEX IF NOT EXISTS ${T}memory_name ON ${T}memory(name)`,\n // External-content FTS on name+description+body. Porter stemmer so\n // \"invoice\" recalls \"invoicing\" without embeddings.\n `CREATE VIRTUAL TABLE IF NOT EXISTS ${T}memory_fts USING fts5(\n name, description, body,\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, name, description, body)\n VALUES (new.rowid, new.name, new.description, new.body);\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, name, description, body)\n VALUES ('delete', old.rowid, old.name, old.description, old.body);\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, name, description, body)\n VALUES ('delete', old.rowid, old.name, old.description, old.body);\n INSERT INTO ${T}memory_fts(rowid, name, description, body)\n VALUES (new.rowid, new.name, new.description, new.body);\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// Legacy migration helpers\n// ---------------------------------------------------------------------------\n\n/** Returns a non-zero count when the old (topic-based) schema is present. */\nexport function buildDetectLegacySchema(): SqlStatement {\n return {\n sql: `SELECT count(*) AS n FROM pragma_table_info('${T}memory') WHERE name='topic'`,\n params: [],\n };\n}\n\n/**\n * Drop all ctx_ tables + triggers so buildInit() can recreate them cleanly.\n * Run in order — FTS must go first (it references the content table).\n */\nexport function buildDropLegacy(): SqlStatement[] {\n return [\n { sql: `DROP TABLE IF EXISTS ${T}memory_fts`, params: [] },\n { sql: `DROP TABLE IF EXISTS ${T}memory`, params: [] },\n { sql: `DROP TABLE IF EXISTS ${T}entity`, params: [] },\n { sql: `DROP TABLE IF EXISTS ${T}edge`, params: [] },\n ];\n}\n\n// ---------------------------------------------------------------------------\n// Helpers\n// ---------------------------------------------------------------------------\n\nconst FTS_OPERATORS = new Set([\"and\", \"or\", \"not\", \"near\"]);\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. `raw` mode passes a hand-written FTS expression through.\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\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, name, description, type, body, source, created_at AS createdAt, updated_at AS updatedAt\";\n\n// ---------------------------------------------------------------------------\n// Memory builders\n// ---------------------------------------------------------------------------\n\n/** UPSERT by name — same name overwrites description/body/type/source. */\nexport function buildRemember(\n input: RememberInput,\n now: number,\n id: string\n): SqlStatement {\n return {\n sql: `INSERT INTO ${T}memory (id, name, description, type, body, source, created_at, updated_at)\n VALUES (?, ?, ?, ?, ?, ?, ?, ?)\n ON CONFLICT(name) DO UPDATE SET\n description = excluded.description,\n body = excluded.body,\n type = excluded.type,\n source = excluded.source,\n updated_at = excluded.updated_at`,\n params: [\n id,\n input.name,\n input.description,\n input.type ?? \"project\",\n input.body,\n input.source ?? null,\n now,\n now,\n ],\n };\n}\n\n/** Delete a memory by its name slug. */\nexport function buildForget(name: string): SqlStatement {\n return { sql: `DELETE FROM ${T}memory WHERE name = ?`, params: [name] };\n}\n\n/** Fetch one memory row by name, or null if absent. */\nexport function buildGet(name: string): SqlStatement {\n return {\n sql: `SELECT ${MEMORY_COLS} FROM ${T}memory WHERE name = ?`,\n params: [name],\n };\n}\n\n/** Always-loaded index: name+description+type, newest first. */\nexport function buildIndex(opts: ListOptions = {}): SqlStatement {\n const where: string[] = [];\n const params: unknown[] = [];\n if (opts.type) {\n where.push(\"type = ?\");\n params.push(opts.type);\n }\n params.push(opts.limit ?? 50);\n return {\n sql: `SELECT id, name, description, type, updated_at AS updatedAt FROM ${T}memory\n ${where.length ? \"WHERE \" + where.join(\" AND \") : \"\"}\n ORDER BY updated_at DESC LIMIT ?`,\n params,\n };\n}\n\n/** BM25 recall across name+description+body. */\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 return {\n sql: `SELECT m.id, m.name, m.description, m.type, m.body, m.source,\n m.created_at AS createdAt, m.updated_at AS updatedAt\n FROM ${T}memory_fts\n JOIN ${T}memory m ON m.rowid = ${T}memory_fts.rowid\n WHERE ${T}memory_fts MATCH ?\n ORDER BY ${T}memory_fts.rank\n LIMIT ?`,\n params: [match, opts.limit ?? 8],\n };\n}\n\nexport function buildStats(): SqlStatement {\n return {\n sql: `SELECT\n (SELECT count(*) FROM ${T}memory) AS memories,\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 (unchanged from v1)\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\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 appears at depth >0 on undirected round-trips — exclude it.\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 contract (versioned here so client and server stay in sync)\n// ---------------------------------------------------------------------------\n\nexport const EXTRACTION_SYSTEM_PROMPT = `You extract durable, structured memories from a conversation or document for an AI agent that will recall them across sessions.\n\nReturn JSON: { \"memories\": [...], \"entities\": [...], \"edges\": [...] }.\n- memories: facts worth remembering across sessions — decisions, conventions, constraints, identifiers, preferences, schema details. Each:\n {\n \"name\": string (kebab-case slug, unique key — e.g. \"auth-provider-choice\", \"todos-table-schema\", \"user-prefers-metrics\"),\n \"description\": string (one-liner shown in the always-loaded index),\n \"type\": \"user\"|\"feedback\"|\"project\"|\"reference\",\n \"body\": string (full markdown content with all relevant detail)\n }\n Types: user=who they are/preferences; feedback=guidance for agents; project=ongoing work/decisions; reference=where to find things.\n- entities: named things — services, people, tables, repos. Each: { \"name\": string, \"kind\"?: string, \"body\"?: string }\n- edges: relationships between entities. Each: { \"src\": name, \"rel\": string, \"dst\": name }\n\nBe conservative. Use unique, meaningful names. If nothing durable is found, 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 memories: {\n type: \"array\",\n items: {\n type: \"object\",\n properties: {\n name: { type: \"string\" },\n description: { type: \"string\" },\n type: { type: \"string\", enum: [\"user\", \"feedback\", \"project\", \"reference\"] },\n body: { type: \"string\" },\n },\n required: [\"name\", \"description\", \"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":[]}
package/dist/core.d.cts CHANGED
@@ -1,25 +1,32 @@
1
1
  /**
2
2
  * @persql/context/core — zero-runtime-dependency engine.
3
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.
4
+ * Schema DDL, SQL builders, and the extraction contract shared by the
5
+ * `@persql/context` SDK (client-side, over `@persql/sdk`) and the hosted
6
+ * PerSQL Context worker (server-side, over `@persql/ai`). Structured memories
7
+ * with a name-keyed UPSERT + always-loaded index replace the old episode model.
8
8
  *
9
9
  * Retrieval is lexical: FTS5 with the porter stemmer, BM25-ranked. No vectors.
10
10
  */
11
- declare const SCHEMA_VERSION = 1;
11
+ declare const SCHEMA_VERSION = 2;
12
12
  declare const TABLE_PREFIX = "ctx_";
13
- type MemoryKind = "fact" | "episode" | "artifact";
13
+ type MemoryType = "user" | "feedback" | "project" | "reference";
14
14
  interface MemoryRow {
15
15
  id: string;
16
- kind: MemoryKind;
17
- topic: string | null;
16
+ name: string;
17
+ description: string;
18
+ type: MemoryType;
18
19
  body: string;
19
- tags: string | null;
20
20
  source: string | null;
21
21
  createdAt: number;
22
- supersededBy: string | null;
22
+ updatedAt: number;
23
+ }
24
+ interface IndexRow {
25
+ id: string;
26
+ name: string;
27
+ description: string;
28
+ type: MemoryType;
29
+ updatedAt: number;
23
30
  }
24
31
  interface Entity {
25
32
  id: string;
@@ -37,13 +44,11 @@ interface Edge {
37
44
  createdAt: number;
38
45
  }
39
46
  interface RememberInput {
47
+ name: string;
48
+ description: string;
49
+ type?: MemoryType;
40
50
  body: string;
41
- topic?: string;
42
- kind?: MemoryKind;
43
- tags?: string[] | string;
44
51
  source?: string;
45
- supersedes?: string | string[];
46
- id?: string;
47
52
  }
48
53
  interface EntityInput {
49
54
  name: string;
@@ -59,25 +64,20 @@ interface EdgeInput {
59
64
  }
60
65
  interface RecallOptions {
61
66
  limit?: number;
62
- kind?: MemoryKind;
63
67
  operator?: "or" | "and";
64
68
  mode?: "terms" | "raw";
65
- sinceMs?: number;
66
- withinDays?: number;
67
- includeSuperseded?: boolean;
68
69
  }
69
70
  interface ListOptions {
70
71
  limit?: number;
71
- kind?: MemoryKind;
72
- includeSuperseded?: boolean;
72
+ type?: MemoryType;
73
73
  }
74
- /** The shape an extractor (LLM or the hosted worker) returns from raw text. */
74
+ /** Structured memories extracted by an LLM from raw text. */
75
75
  interface ExtractedContext {
76
- facts?: Array<{
76
+ memories?: Array<{
77
+ name: string;
78
+ description: string;
79
+ type?: MemoryType;
77
80
  body: string;
78
- topic?: string;
79
- tags?: string[];
80
- kind?: MemoryKind;
81
81
  }>;
82
82
  entities?: EntityInput[];
83
83
  edges?: EdgeInput[];
@@ -88,34 +88,39 @@ interface SqlStatement {
88
88
  }
89
89
  declare const SCHEMA_SQL: string[];
90
90
  declare function buildInit(): SqlStatement[];
91
- declare function normalizeTags(tags?: string[] | string | null): string | null;
91
+ /** Returns a non-zero count when the old (topic-based) schema is present. */
92
+ declare function buildDetectLegacySchema(): SqlStatement;
93
+ /**
94
+ * Drop all ctx_ tables + triggers so buildInit() can recreate them cleanly.
95
+ * Run in order — FTS must go first (it references the content table).
96
+ */
97
+ declare function buildDropLegacy(): SqlStatement[];
92
98
  /**
93
99
  * Turn arbitrary user text into a safe FTS5 MATCH expression. `terms` mode
94
100
  * (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.
101
+ * rest as quoted terms. `raw` mode passes a hand-written FTS expression through.
98
102
  */
99
103
  declare function toFtsQuery(input: string, opts?: {
100
104
  operator?: "or" | "and";
101
105
  mode?: "terms" | "raw";
102
106
  }): string | null;
103
- /** Deterministic id for an entity, keyed on its name (case-insensitive). */
104
107
  declare function entityId(name: string): string;
108
+ /** UPSERT by name — same name overwrites description/body/type/source. */
105
109
  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;
110
+ /** Delete a memory by its name slug. */
111
+ declare function buildForget(name: string): SqlStatement;
112
+ /** Fetch one memory row by name, or null if absent. */
113
+ declare function buildGet(name: string): SqlStatement;
114
+ /** Always-loaded index: name+description+type, newest first. */
115
+ declare function buildIndex(opts?: ListOptions): SqlStatement;
116
+ /** BM25 recall across name+description+body. */
109
117
  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
118
  declare function buildStats(): SqlStatement;
113
119
  declare function buildUpsertEntity(e: EntityInput, now: number): SqlStatement;
114
120
  declare function buildUpsertEdge(e: EdgeInput, now: number): SqlStatement;
115
- /** Entities reachable from a seed within `depth` hops (seed excluded). */
116
121
  declare function buildNeighborEntities(seedId: string, depth: number, limit: number): SqlStatement;
117
122
  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.";
123
+ declare const EXTRACTION_SYSTEM_PROMPT = "You extract durable, structured memories from a conversation or document for an AI agent that will recall them across sessions.\n\nReturn JSON: { \"memories\": [...], \"entities\": [...], \"edges\": [...] }.\n- memories: facts worth remembering across sessions \u2014 decisions, conventions, constraints, identifiers, preferences, schema details. Each:\n {\n \"name\": string (kebab-case slug, unique key \u2014 e.g. \"auth-provider-choice\", \"todos-table-schema\", \"user-prefers-metrics\"),\n \"description\": string (one-liner shown in the always-loaded index),\n \"type\": \"user\"|\"feedback\"|\"project\"|\"reference\",\n \"body\": string (full markdown content with all relevant detail)\n }\n Types: user=who they are/preferences; feedback=guidance for agents; project=ongoing work/decisions; reference=where to find things.\n- entities: named things \u2014 services, people, tables, repos. Each: { \"name\": string, \"kind\"?: string, \"body\"?: string }\n- edges: relationships between entities. Each: { \"src\": name, \"rel\": string, \"dst\": name }\n\nBe conservative. Use unique, meaningful names. If nothing durable is found, return empty arrays.";
119
124
  declare function buildExtractionMessages(raw: string, opts?: {
120
125
  hint?: string;
121
126
  }): Array<{
@@ -125,25 +130,26 @@ declare function buildExtractionMessages(raw: string, opts?: {
125
130
  declare const EXTRACTION_JSON_SCHEMA: {
126
131
  readonly type: "object";
127
132
  readonly properties: {
128
- readonly facts: {
133
+ readonly memories: {
129
134
  readonly type: "array";
130
135
  readonly items: {
131
136
  readonly type: "object";
132
137
  readonly properties: {
133
- readonly body: {
138
+ readonly name: {
134
139
  readonly type: "string";
135
140
  };
136
- readonly topic: {
141
+ readonly description: {
137
142
  readonly type: "string";
138
143
  };
139
- readonly tags: {
140
- readonly type: "array";
141
- readonly items: {
142
- readonly type: "string";
143
- };
144
+ readonly type: {
145
+ readonly type: "string";
146
+ readonly enum: readonly ["user", "feedback", "project", "reference"];
147
+ };
148
+ readonly body: {
149
+ readonly type: "string";
144
150
  };
145
151
  };
146
- readonly required: readonly ["body"];
152
+ readonly required: readonly ["name", "description", "body"];
147
153
  };
148
154
  };
149
155
  readonly entities: {
@@ -185,4 +191,4 @@ declare const EXTRACTION_JSON_SCHEMA: {
185
191
  };
186
192
  };
187
193
 
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 };
194
+ export { EXTRACTION_JSON_SCHEMA, EXTRACTION_SYSTEM_PROMPT, type Edge, type EdgeInput, type Entity, type EntityInput, type ExtractedContext, type IndexRow, type ListOptions, type MemoryRow, type MemoryType, type RecallOptions, type RememberInput, SCHEMA_SQL, SCHEMA_VERSION, type SqlStatement, TABLE_PREFIX, buildDetectLegacySchema, buildDropLegacy, buildEdgesAmong, buildExtractionMessages, buildForget, buildGet, buildIndex, buildInit, buildNeighborEntities, buildRecall, buildRemember, buildStats, buildUpsertEdge, buildUpsertEntity, entityId, toFtsQuery };