hippo-memory 1.15.0 → 1.17.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/README.md +862 -861
- package/dist/audit.d.ts +1 -1
- package/dist/audit.d.ts.map +1 -1
- package/dist/audit.js.map +1 -1
- package/dist/cli.d.ts.map +1 -1
- package/dist/cli.js +1244 -3
- package/dist/cli.js.map +1 -1
- package/dist/customer-notes.d.ts +95 -0
- package/dist/customer-notes.d.ts.map +1 -0
- package/dist/customer-notes.js +296 -0
- package/dist/customer-notes.js.map +1 -0
- package/dist/db.d.ts.map +1 -1
- package/dist/db.js +731 -1
- package/dist/db.js.map +1 -1
- package/dist/graph-extract.d.ts +55 -0
- package/dist/graph-extract.d.ts.map +1 -0
- package/dist/graph-extract.js +259 -0
- package/dist/graph-extract.js.map +1 -0
- package/dist/graph-recall.d.ts +41 -0
- package/dist/graph-recall.d.ts.map +1 -0
- package/dist/graph-recall.js +246 -0
- package/dist/graph-recall.js.map +1 -0
- package/dist/graph.d.ts +137 -0
- package/dist/graph.d.ts.map +1 -0
- package/dist/graph.js +433 -0
- package/dist/graph.js.map +1 -0
- package/dist/incidents.d.ts +100 -0
- package/dist/incidents.d.ts.map +1 -0
- package/dist/incidents.js +322 -0
- package/dist/incidents.js.map +1 -0
- package/dist/index.d.ts +1 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1 -0
- package/dist/index.js.map +1 -1
- package/dist/memory.d.ts +6 -0
- package/dist/memory.d.ts.map +1 -1
- package/dist/memory.js +6 -0
- package/dist/memory.js.map +1 -1
- package/dist/policies.d.ts +149 -0
- package/dist/policies.d.ts.map +1 -0
- package/dist/policies.js +380 -0
- package/dist/policies.js.map +1 -0
- package/dist/processes.d.ts +104 -0
- package/dist/processes.d.ts.map +1 -0
- package/dist/processes.js +330 -0
- package/dist/processes.js.map +1 -0
- package/dist/project-briefs.d.ts +126 -0
- package/dist/project-briefs.d.ts.map +1 -0
- package/dist/project-briefs.js +453 -0
- package/dist/project-briefs.js.map +1 -0
- package/dist/search.d.ts +7 -0
- package/dist/search.d.ts.map +1 -1
- package/dist/search.js.map +1 -1
- package/dist/server.d.ts.map +1 -1
- package/dist/server.js +1028 -16
- package/dist/server.js.map +1 -1
- package/dist/skills.d.ts +98 -0
- package/dist/skills.d.ts.map +1 -0
- package/dist/skills.js +339 -0
- package/dist/skills.js.map +1 -0
- package/dist/src/audit.js.map +1 -1
- package/dist/src/cli.js +1244 -3
- package/dist/src/cli.js.map +1 -1
- package/dist/src/customer-notes.js +296 -0
- package/dist/src/customer-notes.js.map +1 -0
- package/dist/src/db.js +731 -1
- package/dist/src/db.js.map +1 -1
- package/dist/src/graph-extract.js +259 -0
- package/dist/src/graph-extract.js.map +1 -0
- package/dist/src/graph-recall.js +246 -0
- package/dist/src/graph-recall.js.map +1 -0
- package/dist/src/graph.js +433 -0
- package/dist/src/graph.js.map +1 -0
- package/dist/src/incidents.js +322 -0
- package/dist/src/incidents.js.map +1 -0
- package/dist/src/index.js +1 -0
- package/dist/src/index.js.map +1 -1
- package/dist/src/memory.js +6 -0
- package/dist/src/memory.js.map +1 -1
- package/dist/src/policies.js +380 -0
- package/dist/src/policies.js.map +1 -0
- package/dist/src/processes.js +330 -0
- package/dist/src/processes.js.map +1 -0
- package/dist/src/project-briefs.js +453 -0
- package/dist/src/project-briefs.js.map +1 -0
- package/dist/src/search.js.map +1 -1
- package/dist/src/server.js +1028 -16
- package/dist/src/server.js.map +1 -1
- package/dist/src/skills.js +339 -0
- package/dist/src/skills.js.map +1 -0
- package/dist/src/version.js +1 -1
- package/dist/version.d.ts +1 -1
- package/dist/version.js +1 -1
- package/extensions/openclaw-plugin/openclaw.plugin.json +1 -1
- package/extensions/openclaw-plugin/package.json +1 -1
- package/openclaw.plugin.json +1 -1
- package/package.json +2 -2
package/dist/src/db.js
CHANGED
|
@@ -6,7 +6,7 @@ import { cleanupArchivedMirrors } from './raw-archive-mirror-cleanup.js';
|
|
|
6
6
|
import { PACKAGE_VERSION, compareSemver } from './version.js';
|
|
7
7
|
const require = createRequire(import.meta.url);
|
|
8
8
|
const { DatabaseSync } = require('node:sqlite');
|
|
9
|
-
const CURRENT_SCHEMA_VERSION =
|
|
9
|
+
const CURRENT_SCHEMA_VERSION = 37;
|
|
10
10
|
const MIGRATIONS = [
|
|
11
11
|
{
|
|
12
12
|
version: 1,
|
|
@@ -1092,6 +1092,736 @@ const MIGRATIONS = [
|
|
|
1092
1092
|
}
|
|
1093
1093
|
},
|
|
1094
1094
|
},
|
|
1095
|
+
{
|
|
1096
|
+
version: 31,
|
|
1097
|
+
up: (db) => {
|
|
1098
|
+
// E2 incident first-class object (docs/plans/2026-05-29-e2-incident-object.md).
|
|
1099
|
+
// Mirrors the v30 decisions block but for an open -> resolved -> closed
|
|
1100
|
+
// lifecycle (NOT supersede): there is no superseded_by self-FK and no
|
|
1101
|
+
// supersede trigger. An incident is a postmortem capsule: a recorded
|
|
1102
|
+
// operational event with a lifecycle and optional linked receipts (the
|
|
1103
|
+
// memories that are its evidence, stored as a JSON array of ids in
|
|
1104
|
+
// linked_memory_ids). The memory mirror is kept for recall but is not
|
|
1105
|
+
// authoritative; memory_id is NULLABLE with ON DELETE SET NULL so
|
|
1106
|
+
// forget/consolidate/archive does not lose an incident.
|
|
1107
|
+
//
|
|
1108
|
+
// status (open|resolved|closed): resolved records a resolution_text +
|
|
1109
|
+
// resolved_at and stays on record; closed is a terminal retire reachable
|
|
1110
|
+
// from open or resolved. Cross-tenant safety: BEFORE INSERT + BEFORE
|
|
1111
|
+
// UPDATE triggers enforce incidents.tenant_id == the referenced memory's
|
|
1112
|
+
// tenant_id (verbatim mirror of the v30 decisions tenant-match triggers).
|
|
1113
|
+
if (!tableExists(db, 'incidents')) {
|
|
1114
|
+
db.exec(`
|
|
1115
|
+
CREATE TABLE incidents (
|
|
1116
|
+
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
1117
|
+
memory_id TEXT,
|
|
1118
|
+
tenant_id TEXT NOT NULL,
|
|
1119
|
+
incident_text TEXT NOT NULL,
|
|
1120
|
+
context TEXT,
|
|
1121
|
+
status TEXT NOT NULL DEFAULT 'open'
|
|
1122
|
+
CHECK (status IN ('open', 'resolved', 'closed')),
|
|
1123
|
+
resolution_text TEXT,
|
|
1124
|
+
resolved_at TEXT,
|
|
1125
|
+
closed_at TEXT,
|
|
1126
|
+
linked_memory_ids TEXT NOT NULL DEFAULT '[]',
|
|
1127
|
+
created_at TEXT NOT NULL,
|
|
1128
|
+
FOREIGN KEY (memory_id) REFERENCES memories(id) ON DELETE SET NULL
|
|
1129
|
+
)
|
|
1130
|
+
`);
|
|
1131
|
+
db.exec(`
|
|
1132
|
+
CREATE INDEX IF NOT EXISTS idx_incidents_tenant_status
|
|
1133
|
+
ON incidents(tenant_id, status)
|
|
1134
|
+
`);
|
|
1135
|
+
db.exec(`
|
|
1136
|
+
CREATE INDEX IF NOT EXISTS idx_incidents_memory
|
|
1137
|
+
ON incidents(memory_id) WHERE memory_id IS NOT NULL
|
|
1138
|
+
`);
|
|
1139
|
+
// Cross-tenant safety vs the referenced memory (verbatim mirror of the
|
|
1140
|
+
// v30 decisions tenant-match triggers; no supersede trigger).
|
|
1141
|
+
db.exec(`
|
|
1142
|
+
CREATE TRIGGER IF NOT EXISTS trg_incidents_tenant_match_insert
|
|
1143
|
+
BEFORE INSERT ON incidents
|
|
1144
|
+
WHEN NEW.memory_id IS NOT NULL
|
|
1145
|
+
BEGIN
|
|
1146
|
+
SELECT CASE
|
|
1147
|
+
WHEN NEW.tenant_id != (SELECT tenant_id FROM memories WHERE id = NEW.memory_id)
|
|
1148
|
+
THEN RAISE(ABORT, 'incidents.tenant_id must match memories.tenant_id for the referenced memory')
|
|
1149
|
+
END;
|
|
1150
|
+
END
|
|
1151
|
+
`);
|
|
1152
|
+
db.exec(`
|
|
1153
|
+
CREATE TRIGGER IF NOT EXISTS trg_incidents_tenant_match_update
|
|
1154
|
+
BEFORE UPDATE ON incidents
|
|
1155
|
+
WHEN NEW.memory_id IS NOT NULL
|
|
1156
|
+
AND (NEW.memory_id IS NOT OLD.memory_id OR NEW.tenant_id IS NOT OLD.tenant_id)
|
|
1157
|
+
BEGIN
|
|
1158
|
+
SELECT CASE
|
|
1159
|
+
WHEN NEW.tenant_id != (SELECT tenant_id FROM memories WHERE id = NEW.memory_id)
|
|
1160
|
+
THEN RAISE(ABORT, 'incidents.tenant_id must match memories.tenant_id for the referenced memory')
|
|
1161
|
+
END;
|
|
1162
|
+
END
|
|
1163
|
+
`);
|
|
1164
|
+
}
|
|
1165
|
+
},
|
|
1166
|
+
},
|
|
1167
|
+
{
|
|
1168
|
+
version: 32,
|
|
1169
|
+
up: (db) => {
|
|
1170
|
+
// E2 process first-class object (docs/plans/2026-05-29-e2-process-object.md).
|
|
1171
|
+
// A process is a "living process map": a named, ordered list of steps that
|
|
1172
|
+
// evolves. Unlike incident (open->resolved->closed, no supersede), process
|
|
1173
|
+
// REUSES the v30 decisions supersede path as its delta mechanism: a process
|
|
1174
|
+
// evolves by being superseded by a NEW VERSION that records what changed
|
|
1175
|
+
// (change_summary) and the full new state (steps), carrying a derived
|
|
1176
|
+
// version counter. So this table combines the v31 incidents tenant-match
|
|
1177
|
+
// trigger pair (vs the referenced memory) WITH the v30 decisions
|
|
1178
|
+
// superseded_by self-FK + supersede tenant-match trigger.
|
|
1179
|
+
//
|
|
1180
|
+
// status (active|superseded|closed): superseded carries a self-FK
|
|
1181
|
+
// superseded_by to the successor version; closed is a terminal
|
|
1182
|
+
// retire-without-successor (only an active head closes). The memory mirror
|
|
1183
|
+
// is kept for recall but is not authoritative; memory_id is NULLABLE with
|
|
1184
|
+
// ON DELETE SET NULL so forget/consolidate/archive does not lose a process.
|
|
1185
|
+
// steps is a JSON-encoded array of step strings (scoped v1; a normalized
|
|
1186
|
+
// process_steps table is deferred). version is server-derived
|
|
1187
|
+
// (predecessor.version + 1); change_summary is set on a successor row only.
|
|
1188
|
+
if (!tableExists(db, 'processes')) {
|
|
1189
|
+
db.exec(`
|
|
1190
|
+
CREATE TABLE processes (
|
|
1191
|
+
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
1192
|
+
memory_id TEXT,
|
|
1193
|
+
tenant_id TEXT NOT NULL,
|
|
1194
|
+
process_name TEXT NOT NULL,
|
|
1195
|
+
description TEXT,
|
|
1196
|
+
steps TEXT NOT NULL DEFAULT '[]',
|
|
1197
|
+
version INTEGER NOT NULL DEFAULT 1,
|
|
1198
|
+
status TEXT NOT NULL DEFAULT 'active'
|
|
1199
|
+
CHECK (status IN ('active', 'superseded', 'closed')),
|
|
1200
|
+
superseded_by INTEGER,
|
|
1201
|
+
superseded_at TEXT,
|
|
1202
|
+
change_summary TEXT,
|
|
1203
|
+
closed_at TEXT,
|
|
1204
|
+
created_at TEXT NOT NULL,
|
|
1205
|
+
FOREIGN KEY (memory_id) REFERENCES memories(id) ON DELETE SET NULL,
|
|
1206
|
+
FOREIGN KEY (superseded_by) REFERENCES processes(id) ON DELETE SET NULL
|
|
1207
|
+
)
|
|
1208
|
+
`);
|
|
1209
|
+
db.exec(`
|
|
1210
|
+
CREATE INDEX IF NOT EXISTS idx_processes_tenant_status
|
|
1211
|
+
ON processes(tenant_id, status)
|
|
1212
|
+
`);
|
|
1213
|
+
db.exec(`
|
|
1214
|
+
CREATE INDEX IF NOT EXISTS idx_processes_memory
|
|
1215
|
+
ON processes(memory_id) WHERE memory_id IS NOT NULL
|
|
1216
|
+
`);
|
|
1217
|
+
// Cross-tenant safety vs the referenced memory (verbatim mirror of the
|
|
1218
|
+
// v31 incidents / v30 decisions tenant-match triggers).
|
|
1219
|
+
db.exec(`
|
|
1220
|
+
CREATE TRIGGER IF NOT EXISTS trg_processes_tenant_match_insert
|
|
1221
|
+
BEFORE INSERT ON processes
|
|
1222
|
+
WHEN NEW.memory_id IS NOT NULL
|
|
1223
|
+
BEGIN
|
|
1224
|
+
SELECT CASE
|
|
1225
|
+
WHEN NEW.tenant_id != (SELECT tenant_id FROM memories WHERE id = NEW.memory_id)
|
|
1226
|
+
THEN RAISE(ABORT, 'processes.tenant_id must match memories.tenant_id for the referenced memory')
|
|
1227
|
+
END;
|
|
1228
|
+
END
|
|
1229
|
+
`);
|
|
1230
|
+
db.exec(`
|
|
1231
|
+
CREATE TRIGGER IF NOT EXISTS trg_processes_tenant_match_update
|
|
1232
|
+
BEFORE UPDATE ON processes
|
|
1233
|
+
WHEN NEW.memory_id IS NOT NULL
|
|
1234
|
+
AND (NEW.memory_id IS NOT OLD.memory_id OR NEW.tenant_id IS NOT OLD.tenant_id)
|
|
1235
|
+
BEGIN
|
|
1236
|
+
SELECT CASE
|
|
1237
|
+
WHEN NEW.tenant_id != (SELECT tenant_id FROM memories WHERE id = NEW.memory_id)
|
|
1238
|
+
THEN RAISE(ABORT, 'processes.tenant_id must match memories.tenant_id for the referenced memory')
|
|
1239
|
+
END;
|
|
1240
|
+
END
|
|
1241
|
+
`);
|
|
1242
|
+
// Cross-tenant safety vs the successor process (self-FK; verbatim mirror
|
|
1243
|
+
// of the v30 decisions supersede trigger). superseded_by is set only via
|
|
1244
|
+
// the supersede UPDATE; the successor must share the tenant. The successor
|
|
1245
|
+
// row already exists in the same transaction when this fires.
|
|
1246
|
+
db.exec(`
|
|
1247
|
+
CREATE TRIGGER IF NOT EXISTS trg_processes_supersede_tenant_match_update
|
|
1248
|
+
BEFORE UPDATE ON processes
|
|
1249
|
+
WHEN NEW.superseded_by IS NOT NULL
|
|
1250
|
+
AND NEW.superseded_by IS NOT OLD.superseded_by
|
|
1251
|
+
BEGIN
|
|
1252
|
+
SELECT CASE
|
|
1253
|
+
WHEN NEW.tenant_id != (SELECT tenant_id FROM processes WHERE id = NEW.superseded_by)
|
|
1254
|
+
THEN RAISE(ABORT, 'processes.superseded_by must reference a process in the same tenant')
|
|
1255
|
+
END;
|
|
1256
|
+
END
|
|
1257
|
+
`);
|
|
1258
|
+
}
|
|
1259
|
+
},
|
|
1260
|
+
},
|
|
1261
|
+
{
|
|
1262
|
+
version: 33,
|
|
1263
|
+
up: (db) => {
|
|
1264
|
+
// E2 policy first-class object (docs/plans/2026-05-30-e2-policy-object.md).
|
|
1265
|
+
// The "bi-temporal-first" object type: a named rule/statement that is in
|
|
1266
|
+
// force over an EFFECTIVE-TIME range (valid_from required, valid_to nullable
|
|
1267
|
+
// = open-ended) and evolves via the v32 processes supersede machinery
|
|
1268
|
+
// (superseded_by self-FK + supersede tenant-match trigger + version +
|
|
1269
|
+
// change_summary). This table = the v32 processes table MINUS `steps`
|
|
1270
|
+
// (a policy has policy_text, not an ordered step list) PLUS the first-class
|
|
1271
|
+
// effective-time columns valid_from/valid_to. Valid-time is the queryable
|
|
1272
|
+
// axis (the as-of query loadPoliciesAsOf); transaction-time is present via
|
|
1273
|
+
// created_at + the supersede chain's superseded_at (time-travel deferred).
|
|
1274
|
+
//
|
|
1275
|
+
// All date inputs are normalized to canonical ISO-8601 datetime
|
|
1276
|
+
// (toISOString) at the store boundary before persist/compare, so the
|
|
1277
|
+
// fixed-width values sort lexically and the half-open [valid_from, valid_to)
|
|
1278
|
+
// as-of comparison is correct (plan-eng-critic round-1 CRIT fix).
|
|
1279
|
+
if (!tableExists(db, 'policies')) {
|
|
1280
|
+
db.exec(`
|
|
1281
|
+
CREATE TABLE policies (
|
|
1282
|
+
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
1283
|
+
memory_id TEXT,
|
|
1284
|
+
tenant_id TEXT NOT NULL,
|
|
1285
|
+
policy_name TEXT NOT NULL,
|
|
1286
|
+
policy_text TEXT NOT NULL,
|
|
1287
|
+
valid_from TEXT NOT NULL,
|
|
1288
|
+
valid_to TEXT,
|
|
1289
|
+
version INTEGER NOT NULL DEFAULT 1,
|
|
1290
|
+
status TEXT NOT NULL DEFAULT 'active'
|
|
1291
|
+
CHECK (status IN ('active', 'superseded', 'closed')),
|
|
1292
|
+
superseded_by INTEGER,
|
|
1293
|
+
superseded_at TEXT,
|
|
1294
|
+
change_summary TEXT,
|
|
1295
|
+
closed_at TEXT,
|
|
1296
|
+
created_at TEXT NOT NULL,
|
|
1297
|
+
FOREIGN KEY (memory_id) REFERENCES memories(id) ON DELETE SET NULL,
|
|
1298
|
+
FOREIGN KEY (superseded_by) REFERENCES policies(id) ON DELETE SET NULL
|
|
1299
|
+
)
|
|
1300
|
+
`);
|
|
1301
|
+
db.exec(`
|
|
1302
|
+
CREATE INDEX IF NOT EXISTS idx_policies_tenant_status
|
|
1303
|
+
ON policies(tenant_id, status)
|
|
1304
|
+
`);
|
|
1305
|
+
db.exec(`
|
|
1306
|
+
CREATE INDEX IF NOT EXISTS idx_policies_memory
|
|
1307
|
+
ON policies(memory_id) WHERE memory_id IS NOT NULL
|
|
1308
|
+
`);
|
|
1309
|
+
// Supports the as-of query (active policies in force at a valid-time).
|
|
1310
|
+
db.exec(`
|
|
1311
|
+
CREATE INDEX IF NOT EXISTS idx_policies_asof
|
|
1312
|
+
ON policies(tenant_id, valid_from)
|
|
1313
|
+
`);
|
|
1314
|
+
// Cross-tenant safety vs the referenced memory (verbatim mirror of the
|
|
1315
|
+
// v32 processes tenant-match triggers).
|
|
1316
|
+
db.exec(`
|
|
1317
|
+
CREATE TRIGGER IF NOT EXISTS trg_policies_tenant_match_insert
|
|
1318
|
+
BEFORE INSERT ON policies
|
|
1319
|
+
WHEN NEW.memory_id IS NOT NULL
|
|
1320
|
+
BEGIN
|
|
1321
|
+
SELECT CASE
|
|
1322
|
+
WHEN NEW.tenant_id != (SELECT tenant_id FROM memories WHERE id = NEW.memory_id)
|
|
1323
|
+
THEN RAISE(ABORT, 'policies.tenant_id must match memories.tenant_id for the referenced memory')
|
|
1324
|
+
END;
|
|
1325
|
+
END
|
|
1326
|
+
`);
|
|
1327
|
+
db.exec(`
|
|
1328
|
+
CREATE TRIGGER IF NOT EXISTS trg_policies_tenant_match_update
|
|
1329
|
+
BEFORE UPDATE ON policies
|
|
1330
|
+
WHEN NEW.memory_id IS NOT NULL
|
|
1331
|
+
AND (NEW.memory_id IS NOT OLD.memory_id OR NEW.tenant_id IS NOT OLD.tenant_id)
|
|
1332
|
+
BEGIN
|
|
1333
|
+
SELECT CASE
|
|
1334
|
+
WHEN NEW.tenant_id != (SELECT tenant_id FROM memories WHERE id = NEW.memory_id)
|
|
1335
|
+
THEN RAISE(ABORT, 'policies.tenant_id must match memories.tenant_id for the referenced memory')
|
|
1336
|
+
END;
|
|
1337
|
+
END
|
|
1338
|
+
`);
|
|
1339
|
+
// Cross-tenant safety vs the successor policy (self-FK; verbatim mirror of
|
|
1340
|
+
// the v32 processes supersede trigger).
|
|
1341
|
+
db.exec(`
|
|
1342
|
+
CREATE TRIGGER IF NOT EXISTS trg_policies_supersede_tenant_match_update
|
|
1343
|
+
BEFORE UPDATE ON policies
|
|
1344
|
+
WHEN NEW.superseded_by IS NOT NULL
|
|
1345
|
+
AND NEW.superseded_by IS NOT OLD.superseded_by
|
|
1346
|
+
BEGIN
|
|
1347
|
+
SELECT CASE
|
|
1348
|
+
WHEN NEW.tenant_id != (SELECT tenant_id FROM policies WHERE id = NEW.superseded_by)
|
|
1349
|
+
THEN RAISE(ABORT, 'policies.superseded_by must reference a policy in the same tenant')
|
|
1350
|
+
END;
|
|
1351
|
+
END
|
|
1352
|
+
`);
|
|
1353
|
+
}
|
|
1354
|
+
},
|
|
1355
|
+
},
|
|
1356
|
+
{
|
|
1357
|
+
version: 34,
|
|
1358
|
+
up: (db) => {
|
|
1359
|
+
// E2 skill first-class object (docs/plans/2026-05-30-e2-skill-object.md).
|
|
1360
|
+
// A skill is a reusable, agent-followable capability: an `instructions` body
|
|
1361
|
+
// + an optional `trigger_text` (when to apply), evolving via the v32
|
|
1362
|
+
// processes supersede machinery (superseded_by self-FK + supersede
|
|
1363
|
+
// tenant-match trigger + version + change_summary). This table = the v32
|
|
1364
|
+
// processes table MINUS `steps` (a skill's content is a single instructions
|
|
1365
|
+
// body) PLUS `instructions` (NOT NULL) and `trigger_text`. "Executable" is
|
|
1366
|
+
// scoped to an agent-followable instruction that EXPORTS into the agent's
|
|
1367
|
+
// in-force rules (AGENTS.md / CLAUDE.md) via exportSkills; literal code
|
|
1368
|
+
// execution is deferred. NOTE: the column is `trigger_text`, NOT `trigger`,
|
|
1369
|
+
// because TRIGGER is a SQLite reserved keyword.
|
|
1370
|
+
if (!tableExists(db, 'skills')) {
|
|
1371
|
+
db.exec(`
|
|
1372
|
+
CREATE TABLE skills (
|
|
1373
|
+
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
1374
|
+
memory_id TEXT,
|
|
1375
|
+
tenant_id TEXT NOT NULL,
|
|
1376
|
+
skill_name TEXT NOT NULL,
|
|
1377
|
+
instructions TEXT NOT NULL,
|
|
1378
|
+
trigger_text TEXT,
|
|
1379
|
+
version INTEGER NOT NULL DEFAULT 1,
|
|
1380
|
+
status TEXT NOT NULL DEFAULT 'active'
|
|
1381
|
+
CHECK (status IN ('active', 'superseded', 'closed')),
|
|
1382
|
+
superseded_by INTEGER,
|
|
1383
|
+
superseded_at TEXT,
|
|
1384
|
+
change_summary TEXT,
|
|
1385
|
+
closed_at TEXT,
|
|
1386
|
+
created_at TEXT NOT NULL,
|
|
1387
|
+
FOREIGN KEY (memory_id) REFERENCES memories(id) ON DELETE SET NULL,
|
|
1388
|
+
FOREIGN KEY (superseded_by) REFERENCES skills(id) ON DELETE SET NULL
|
|
1389
|
+
)
|
|
1390
|
+
`);
|
|
1391
|
+
db.exec(`
|
|
1392
|
+
CREATE INDEX IF NOT EXISTS idx_skills_tenant_status
|
|
1393
|
+
ON skills(tenant_id, status)
|
|
1394
|
+
`);
|
|
1395
|
+
db.exec(`
|
|
1396
|
+
CREATE INDEX IF NOT EXISTS idx_skills_memory
|
|
1397
|
+
ON skills(memory_id) WHERE memory_id IS NOT NULL
|
|
1398
|
+
`);
|
|
1399
|
+
// Cross-tenant safety vs the referenced memory (verbatim mirror of the
|
|
1400
|
+
// v32 processes tenant-match triggers).
|
|
1401
|
+
db.exec(`
|
|
1402
|
+
CREATE TRIGGER IF NOT EXISTS trg_skills_tenant_match_insert
|
|
1403
|
+
BEFORE INSERT ON skills
|
|
1404
|
+
WHEN NEW.memory_id IS NOT NULL
|
|
1405
|
+
BEGIN
|
|
1406
|
+
SELECT CASE
|
|
1407
|
+
WHEN NEW.tenant_id != (SELECT tenant_id FROM memories WHERE id = NEW.memory_id)
|
|
1408
|
+
THEN RAISE(ABORT, 'skills.tenant_id must match memories.tenant_id for the referenced memory')
|
|
1409
|
+
END;
|
|
1410
|
+
END
|
|
1411
|
+
`);
|
|
1412
|
+
db.exec(`
|
|
1413
|
+
CREATE TRIGGER IF NOT EXISTS trg_skills_tenant_match_update
|
|
1414
|
+
BEFORE UPDATE ON skills
|
|
1415
|
+
WHEN NEW.memory_id IS NOT NULL
|
|
1416
|
+
AND (NEW.memory_id IS NOT OLD.memory_id OR NEW.tenant_id IS NOT OLD.tenant_id)
|
|
1417
|
+
BEGIN
|
|
1418
|
+
SELECT CASE
|
|
1419
|
+
WHEN NEW.tenant_id != (SELECT tenant_id FROM memories WHERE id = NEW.memory_id)
|
|
1420
|
+
THEN RAISE(ABORT, 'skills.tenant_id must match memories.tenant_id for the referenced memory')
|
|
1421
|
+
END;
|
|
1422
|
+
END
|
|
1423
|
+
`);
|
|
1424
|
+
// Cross-tenant safety vs the successor skill (self-FK; verbatim mirror of
|
|
1425
|
+
// the v32 processes supersede trigger).
|
|
1426
|
+
db.exec(`
|
|
1427
|
+
CREATE TRIGGER IF NOT EXISTS trg_skills_supersede_tenant_match_update
|
|
1428
|
+
BEFORE UPDATE ON skills
|
|
1429
|
+
WHEN NEW.superseded_by IS NOT NULL
|
|
1430
|
+
AND NEW.superseded_by IS NOT OLD.superseded_by
|
|
1431
|
+
BEGIN
|
|
1432
|
+
SELECT CASE
|
|
1433
|
+
WHEN NEW.tenant_id != (SELECT tenant_id FROM skills WHERE id = NEW.superseded_by)
|
|
1434
|
+
THEN RAISE(ABORT, 'skills.superseded_by must reference a skill in the same tenant')
|
|
1435
|
+
END;
|
|
1436
|
+
END
|
|
1437
|
+
`);
|
|
1438
|
+
}
|
|
1439
|
+
},
|
|
1440
|
+
},
|
|
1441
|
+
{
|
|
1442
|
+
version: 35,
|
|
1443
|
+
up: (db) => {
|
|
1444
|
+
// E2 project_brief first-class object
|
|
1445
|
+
// (docs/plans/2026-05-30-e2-project-brief-object.md). A project_brief is the
|
|
1446
|
+
// living, repo-scoped summary of a repository's state: a `summary` body
|
|
1447
|
+
// scoped to a `repo`, evolving via the v34 skills supersede machinery
|
|
1448
|
+
// (superseded_by self-FK + supersede tenant-match trigger + version +
|
|
1449
|
+
// change_summary). This table = the v34 skills table with
|
|
1450
|
+
// skill_name/trigger_text replaced by `repo` (the repo-scoping dimension)
|
|
1451
|
+
// PLUS `summary` (the brief body). The distinguishing op (refreshBrief, in
|
|
1452
|
+
// src/project-briefs.ts) auto-assembles the summary from the repo's receipts
|
|
1453
|
+
// (memory rows tagged path:<repo>); it needs no schema support beyond `repo`.
|
|
1454
|
+
// All column names were checked against SQLite reserved words (skill-episode
|
|
1455
|
+
// lesson re: `trigger`): repo/summary/version/status/etc. are non-reserved.
|
|
1456
|
+
if (!tableExists(db, 'project_briefs')) {
|
|
1457
|
+
db.exec(`
|
|
1458
|
+
CREATE TABLE project_briefs (
|
|
1459
|
+
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
1460
|
+
memory_id TEXT,
|
|
1461
|
+
tenant_id TEXT NOT NULL,
|
|
1462
|
+
repo TEXT NOT NULL,
|
|
1463
|
+
summary TEXT NOT NULL,
|
|
1464
|
+
version INTEGER NOT NULL DEFAULT 1,
|
|
1465
|
+
status TEXT NOT NULL DEFAULT 'active'
|
|
1466
|
+
CHECK (status IN ('active', 'superseded', 'closed')),
|
|
1467
|
+
superseded_by INTEGER,
|
|
1468
|
+
superseded_at TEXT,
|
|
1469
|
+
change_summary TEXT,
|
|
1470
|
+
closed_at TEXT,
|
|
1471
|
+
created_at TEXT NOT NULL,
|
|
1472
|
+
FOREIGN KEY (memory_id) REFERENCES memories(id) ON DELETE SET NULL,
|
|
1473
|
+
FOREIGN KEY (superseded_by) REFERENCES project_briefs(id) ON DELETE SET NULL
|
|
1474
|
+
)
|
|
1475
|
+
`);
|
|
1476
|
+
db.exec(`
|
|
1477
|
+
CREATE INDEX IF NOT EXISTS idx_project_briefs_tenant_status
|
|
1478
|
+
ON project_briefs(tenant_id, status)
|
|
1479
|
+
`);
|
|
1480
|
+
db.exec(`
|
|
1481
|
+
CREATE INDEX IF NOT EXISTS idx_project_briefs_memory
|
|
1482
|
+
ON project_briefs(memory_id) WHERE memory_id IS NOT NULL
|
|
1483
|
+
`);
|
|
1484
|
+
db.exec(`
|
|
1485
|
+
CREATE INDEX IF NOT EXISTS idx_project_briefs_repo
|
|
1486
|
+
ON project_briefs(tenant_id, repo, status)
|
|
1487
|
+
`);
|
|
1488
|
+
// Cross-tenant safety vs the referenced memory (verbatim mirror of the
|
|
1489
|
+
// v34 skills tenant-match triggers).
|
|
1490
|
+
db.exec(`
|
|
1491
|
+
CREATE TRIGGER IF NOT EXISTS trg_project_briefs_tenant_match_insert
|
|
1492
|
+
BEFORE INSERT ON project_briefs
|
|
1493
|
+
WHEN NEW.memory_id IS NOT NULL
|
|
1494
|
+
BEGIN
|
|
1495
|
+
SELECT CASE
|
|
1496
|
+
WHEN NEW.tenant_id != (SELECT tenant_id FROM memories WHERE id = NEW.memory_id)
|
|
1497
|
+
THEN RAISE(ABORT, 'project_briefs.tenant_id must match memories.tenant_id for the referenced memory')
|
|
1498
|
+
END;
|
|
1499
|
+
END
|
|
1500
|
+
`);
|
|
1501
|
+
db.exec(`
|
|
1502
|
+
CREATE TRIGGER IF NOT EXISTS trg_project_briefs_tenant_match_update
|
|
1503
|
+
BEFORE UPDATE ON project_briefs
|
|
1504
|
+
WHEN NEW.memory_id IS NOT NULL
|
|
1505
|
+
AND (NEW.memory_id IS NOT OLD.memory_id OR NEW.tenant_id IS NOT OLD.tenant_id)
|
|
1506
|
+
BEGIN
|
|
1507
|
+
SELECT CASE
|
|
1508
|
+
WHEN NEW.tenant_id != (SELECT tenant_id FROM memories WHERE id = NEW.memory_id)
|
|
1509
|
+
THEN RAISE(ABORT, 'project_briefs.tenant_id must match memories.tenant_id for the referenced memory')
|
|
1510
|
+
END;
|
|
1511
|
+
END
|
|
1512
|
+
`);
|
|
1513
|
+
// Cross-tenant safety vs the successor brief (self-FK; verbatim mirror of
|
|
1514
|
+
// the v34 skills supersede trigger).
|
|
1515
|
+
db.exec(`
|
|
1516
|
+
CREATE TRIGGER IF NOT EXISTS trg_project_briefs_supersede_tenant_match_update
|
|
1517
|
+
BEFORE UPDATE ON project_briefs
|
|
1518
|
+
WHEN NEW.superseded_by IS NOT NULL
|
|
1519
|
+
AND NEW.superseded_by IS NOT OLD.superseded_by
|
|
1520
|
+
BEGIN
|
|
1521
|
+
SELECT CASE
|
|
1522
|
+
WHEN NEW.tenant_id != (SELECT tenant_id FROM project_briefs WHERE id = NEW.superseded_by)
|
|
1523
|
+
THEN RAISE(ABORT, 'project_briefs.superseded_by must reference a project_brief in the same tenant')
|
|
1524
|
+
END;
|
|
1525
|
+
END
|
|
1526
|
+
`);
|
|
1527
|
+
}
|
|
1528
|
+
},
|
|
1529
|
+
},
|
|
1530
|
+
{
|
|
1531
|
+
version: 36,
|
|
1532
|
+
up: (db) => {
|
|
1533
|
+
// E2 customer_note first-class object (the LAST E2 object)
|
|
1534
|
+
// (docs/plans/2026-06-01-e2-customer-note-object.md). A customer_note is a
|
|
1535
|
+
// discrete note recorded against an account/customer entity, evolving via the
|
|
1536
|
+
// v35 project_briefs supersede machinery (superseded_by self-FK + supersede
|
|
1537
|
+
// tenant-match trigger + version + change_summary). This table = the v35
|
|
1538
|
+
// project_briefs table with repo/summary replaced by `customer` (the
|
|
1539
|
+
// entity-scoping dimension; a free-form account/customer id - the entities
|
|
1540
|
+
// table is unbuilt E3.1, so a FK is deferred) PLUS `note` (the note body).
|
|
1541
|
+
// MANY notes per customer (each its own supersede chain), unlike the
|
|
1542
|
+
// one-summary-per-repo project_brief. All column names checked against SQLite
|
|
1543
|
+
// reserved words (skill-episode lesson, codebase-audit rule 10): customer/note/
|
|
1544
|
+
// version/status/etc. are non-reserved.
|
|
1545
|
+
if (!tableExists(db, 'customer_notes')) {
|
|
1546
|
+
db.exec(`
|
|
1547
|
+
CREATE TABLE customer_notes (
|
|
1548
|
+
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
1549
|
+
memory_id TEXT,
|
|
1550
|
+
tenant_id TEXT NOT NULL,
|
|
1551
|
+
customer TEXT NOT NULL,
|
|
1552
|
+
note TEXT NOT NULL,
|
|
1553
|
+
version INTEGER NOT NULL DEFAULT 1,
|
|
1554
|
+
status TEXT NOT NULL DEFAULT 'active'
|
|
1555
|
+
CHECK (status IN ('active', 'superseded', 'closed')),
|
|
1556
|
+
superseded_by INTEGER,
|
|
1557
|
+
superseded_at TEXT,
|
|
1558
|
+
change_summary TEXT,
|
|
1559
|
+
closed_at TEXT,
|
|
1560
|
+
created_at TEXT NOT NULL,
|
|
1561
|
+
FOREIGN KEY (memory_id) REFERENCES memories(id) ON DELETE SET NULL,
|
|
1562
|
+
FOREIGN KEY (superseded_by) REFERENCES customer_notes(id) ON DELETE SET NULL
|
|
1563
|
+
)
|
|
1564
|
+
`);
|
|
1565
|
+
db.exec(`
|
|
1566
|
+
CREATE INDEX IF NOT EXISTS idx_customer_notes_tenant_status
|
|
1567
|
+
ON customer_notes(tenant_id, status)
|
|
1568
|
+
`);
|
|
1569
|
+
db.exec(`
|
|
1570
|
+
CREATE INDEX IF NOT EXISTS idx_customer_notes_memory
|
|
1571
|
+
ON customer_notes(memory_id) WHERE memory_id IS NOT NULL
|
|
1572
|
+
`);
|
|
1573
|
+
db.exec(`
|
|
1574
|
+
CREATE INDEX IF NOT EXISTS idx_customer_notes_customer
|
|
1575
|
+
ON customer_notes(tenant_id, customer, status)
|
|
1576
|
+
`);
|
|
1577
|
+
// Cross-tenant safety vs the referenced memory (verbatim mirror of the
|
|
1578
|
+
// v35 project_briefs tenant-match triggers).
|
|
1579
|
+
db.exec(`
|
|
1580
|
+
CREATE TRIGGER IF NOT EXISTS trg_customer_notes_tenant_match_insert
|
|
1581
|
+
BEFORE INSERT ON customer_notes
|
|
1582
|
+
WHEN NEW.memory_id IS NOT NULL
|
|
1583
|
+
BEGIN
|
|
1584
|
+
SELECT CASE
|
|
1585
|
+
WHEN NEW.tenant_id != (SELECT tenant_id FROM memories WHERE id = NEW.memory_id)
|
|
1586
|
+
THEN RAISE(ABORT, 'customer_notes.tenant_id must match memories.tenant_id for the referenced memory')
|
|
1587
|
+
END;
|
|
1588
|
+
END
|
|
1589
|
+
`);
|
|
1590
|
+
db.exec(`
|
|
1591
|
+
CREATE TRIGGER IF NOT EXISTS trg_customer_notes_tenant_match_update
|
|
1592
|
+
BEFORE UPDATE ON customer_notes
|
|
1593
|
+
WHEN NEW.memory_id IS NOT NULL
|
|
1594
|
+
AND (NEW.memory_id IS NOT OLD.memory_id OR NEW.tenant_id IS NOT OLD.tenant_id)
|
|
1595
|
+
BEGIN
|
|
1596
|
+
SELECT CASE
|
|
1597
|
+
WHEN NEW.tenant_id != (SELECT tenant_id FROM memories WHERE id = NEW.memory_id)
|
|
1598
|
+
THEN RAISE(ABORT, 'customer_notes.tenant_id must match memories.tenant_id for the referenced memory')
|
|
1599
|
+
END;
|
|
1600
|
+
END
|
|
1601
|
+
`);
|
|
1602
|
+
// Cross-tenant safety vs the successor note (self-FK; verbatim mirror of the
|
|
1603
|
+
// v35 project_briefs supersede trigger).
|
|
1604
|
+
db.exec(`
|
|
1605
|
+
CREATE TRIGGER IF NOT EXISTS trg_customer_notes_supersede_tenant_match_update
|
|
1606
|
+
BEFORE UPDATE ON customer_notes
|
|
1607
|
+
WHEN NEW.superseded_by IS NOT NULL
|
|
1608
|
+
AND NEW.superseded_by IS NOT OLD.superseded_by
|
|
1609
|
+
BEGIN
|
|
1610
|
+
SELECT CASE
|
|
1611
|
+
WHEN NEW.tenant_id != (SELECT tenant_id FROM customer_notes WHERE id = NEW.superseded_by)
|
|
1612
|
+
THEN RAISE(ABORT, 'customer_notes.superseded_by must reference a customer_note in the same tenant')
|
|
1613
|
+
END;
|
|
1614
|
+
END
|
|
1615
|
+
`);
|
|
1616
|
+
}
|
|
1617
|
+
},
|
|
1618
|
+
},
|
|
1619
|
+
{
|
|
1620
|
+
version: 37,
|
|
1621
|
+
up: (db) => {
|
|
1622
|
+
// E3.3 graph-on-consolidated guard (docs/plans/2026-06-01-e3-graph-guard.md).
|
|
1623
|
+
// The graph layer (entities + relations) sits ON TOP OF consolidated state and
|
|
1624
|
+
// must NEVER index the raw layer. The substrate: entities + relations +
|
|
1625
|
+
// graph_extraction_queue, each FK-ing to memories and guarded so they can only
|
|
1626
|
+
// reference CONSOLIDATED memories (kind IN ('distilled','superseded')), never
|
|
1627
|
+
// kind='raw'. New tables -> real CHECK constraints (unlike the ALTER'd memories,
|
|
1628
|
+
// whose kind CHECK lives in triggers). The kind/source MATCH (source_kind ==
|
|
1629
|
+
// the FK'd memory's actual kind) cannot be a CHECK (CHECK can't subquery), so it
|
|
1630
|
+
// is a BEFORE INSERT *and* BEFORE UPDATE trigger (the subquery-capable pattern
|
|
1631
|
+
// from the v30 decisions / predictions tenant-match triggers). Both INSERT and
|
|
1632
|
+
// UPDATE are guarded: an INSERT-only guard is bypassable via a raw SQL UPDATE
|
|
1633
|
+
// that moves a row onto a raw memory (plan-eng-critic 2026-06-01). All column
|
|
1634
|
+
// names checked vs SQL reserved words (rule 10): rel_type avoids REFERENCES.
|
|
1635
|
+
if (!tableExists(db, 'entities')) {
|
|
1636
|
+
db.exec(`
|
|
1637
|
+
CREATE TABLE entities (
|
|
1638
|
+
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
1639
|
+
tenant_id TEXT NOT NULL,
|
|
1640
|
+
entity_type TEXT NOT NULL
|
|
1641
|
+
CHECK (entity_type IN ('person', 'project', 'customer', 'system', 'policy', 'decision')),
|
|
1642
|
+
name TEXT NOT NULL,
|
|
1643
|
+
memory_id TEXT NOT NULL,
|
|
1644
|
+
source_kind TEXT NOT NULL CHECK (source_kind IN ('distilled', 'superseded')),
|
|
1645
|
+
created_at TEXT NOT NULL,
|
|
1646
|
+
FOREIGN KEY (memory_id) REFERENCES memories(id) ON DELETE CASCADE
|
|
1647
|
+
)
|
|
1648
|
+
`);
|
|
1649
|
+
db.exec(`CREATE INDEX IF NOT EXISTS idx_entities_tenant ON entities(tenant_id)`);
|
|
1650
|
+
db.exec(`CREATE INDEX IF NOT EXISTS idx_entities_memory ON entities(memory_id)`);
|
|
1651
|
+
db.exec(`
|
|
1652
|
+
CREATE TABLE relations (
|
|
1653
|
+
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
1654
|
+
tenant_id TEXT NOT NULL,
|
|
1655
|
+
from_entity_id INTEGER NOT NULL,
|
|
1656
|
+
to_entity_id INTEGER NOT NULL,
|
|
1657
|
+
rel_type TEXT NOT NULL
|
|
1658
|
+
CHECK (rel_type IN ('owns', 'supersedes', 'depends-on', 'blocked-by', 'references')),
|
|
1659
|
+
memory_id TEXT NOT NULL,
|
|
1660
|
+
source_kind TEXT NOT NULL CHECK (source_kind IN ('distilled', 'superseded')),
|
|
1661
|
+
created_at TEXT NOT NULL,
|
|
1662
|
+
FOREIGN KEY (from_entity_id) REFERENCES entities(id) ON DELETE CASCADE,
|
|
1663
|
+
FOREIGN KEY (to_entity_id) REFERENCES entities(id) ON DELETE CASCADE,
|
|
1664
|
+
FOREIGN KEY (memory_id) REFERENCES memories(id) ON DELETE CASCADE
|
|
1665
|
+
)
|
|
1666
|
+
`);
|
|
1667
|
+
db.exec(`CREATE INDEX IF NOT EXISTS idx_relations_tenant ON relations(tenant_id)`);
|
|
1668
|
+
db.exec(`CREATE INDEX IF NOT EXISTS idx_relations_from ON relations(from_entity_id)`);
|
|
1669
|
+
db.exec(`CREATE INDEX IF NOT EXISTS idx_relations_to ON relations(to_entity_id)`);
|
|
1670
|
+
db.exec(`CREATE INDEX IF NOT EXISTS idx_relations_memory ON relations(memory_id)`);
|
|
1671
|
+
db.exec(`
|
|
1672
|
+
CREATE TABLE graph_extraction_queue (
|
|
1673
|
+
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
1674
|
+
tenant_id TEXT NOT NULL,
|
|
1675
|
+
memory_id TEXT NOT NULL,
|
|
1676
|
+
kind TEXT NOT NULL CHECK (kind IN ('distilled', 'superseded')),
|
|
1677
|
+
status TEXT NOT NULL DEFAULT 'pending'
|
|
1678
|
+
CHECK (status IN ('pending', 'processed', 'skipped')),
|
|
1679
|
+
enqueued_at TEXT NOT NULL,
|
|
1680
|
+
processed_at TEXT,
|
|
1681
|
+
FOREIGN KEY (memory_id) REFERENCES memories(id) ON DELETE CASCADE
|
|
1682
|
+
)
|
|
1683
|
+
`);
|
|
1684
|
+
db.exec(`CREATE INDEX IF NOT EXISTS idx_graph_queue_status ON graph_extraction_queue(tenant_id, status)`);
|
|
1685
|
+
// entities guard: source_kind must equal the FK'd memory's actual kind (so a
|
|
1686
|
+
// raw memory or a lying source_kind both ABORT), and tenant must match. Both
|
|
1687
|
+
// INSERT and UPDATE (UPDATE fires when memory_id/source_kind/tenant_id change).
|
|
1688
|
+
db.exec(`
|
|
1689
|
+
CREATE TRIGGER IF NOT EXISTS trg_entities_consolidated_only_insert
|
|
1690
|
+
BEFORE INSERT ON entities
|
|
1691
|
+
BEGIN
|
|
1692
|
+
SELECT CASE
|
|
1693
|
+
WHEN NEW.source_kind != (SELECT kind FROM memories WHERE id = NEW.memory_id)
|
|
1694
|
+
THEN RAISE(ABORT, 'entities.source_kind must equal the referenced memory kind; the graph indexes consolidated state only (no raw)')
|
|
1695
|
+
WHEN NEW.tenant_id != (SELECT tenant_id FROM memories WHERE id = NEW.memory_id)
|
|
1696
|
+
THEN RAISE(ABORT, 'entities.tenant_id must match memories.tenant_id for the referenced memory')
|
|
1697
|
+
END;
|
|
1698
|
+
END
|
|
1699
|
+
`);
|
|
1700
|
+
db.exec(`
|
|
1701
|
+
CREATE TRIGGER IF NOT EXISTS trg_entities_consolidated_only_update
|
|
1702
|
+
BEFORE UPDATE ON entities
|
|
1703
|
+
WHEN NEW.memory_id IS NOT OLD.memory_id
|
|
1704
|
+
OR NEW.source_kind IS NOT OLD.source_kind
|
|
1705
|
+
OR NEW.tenant_id IS NOT OLD.tenant_id
|
|
1706
|
+
BEGIN
|
|
1707
|
+
SELECT CASE
|
|
1708
|
+
WHEN NEW.source_kind != (SELECT kind FROM memories WHERE id = NEW.memory_id)
|
|
1709
|
+
THEN RAISE(ABORT, 'entities.source_kind must equal the referenced memory kind; the graph indexes consolidated state only (no raw)')
|
|
1710
|
+
WHEN NEW.tenant_id != (SELECT tenant_id FROM memories WHERE id = NEW.memory_id)
|
|
1711
|
+
THEN RAISE(ABORT, 'entities.tenant_id must match memories.tenant_id for the referenced memory')
|
|
1712
|
+
END;
|
|
1713
|
+
END
|
|
1714
|
+
`);
|
|
1715
|
+
// relations guard: source_kind must equal the FK'd memory's kind; tenant must
|
|
1716
|
+
// match the memory AND both endpoint entities (no cross-tenant edges).
|
|
1717
|
+
db.exec(`
|
|
1718
|
+
CREATE TRIGGER IF NOT EXISTS trg_relations_consolidated_only_insert
|
|
1719
|
+
BEFORE INSERT ON relations
|
|
1720
|
+
BEGIN
|
|
1721
|
+
SELECT CASE
|
|
1722
|
+
WHEN NEW.source_kind != (SELECT kind FROM memories WHERE id = NEW.memory_id)
|
|
1723
|
+
THEN RAISE(ABORT, 'relations.source_kind must equal the referenced memory kind; the graph indexes consolidated state only (no raw)')
|
|
1724
|
+
WHEN NEW.tenant_id != (SELECT tenant_id FROM memories WHERE id = NEW.memory_id)
|
|
1725
|
+
THEN RAISE(ABORT, 'relations.tenant_id must match memories.tenant_id for the referenced memory')
|
|
1726
|
+
WHEN NEW.tenant_id != (SELECT tenant_id FROM entities WHERE id = NEW.from_entity_id)
|
|
1727
|
+
THEN RAISE(ABORT, 'relations.tenant_id must match the from_entity tenant (no cross-tenant edges)')
|
|
1728
|
+
WHEN NEW.tenant_id != (SELECT tenant_id FROM entities WHERE id = NEW.to_entity_id)
|
|
1729
|
+
THEN RAISE(ABORT, 'relations.tenant_id must match the to_entity tenant (no cross-tenant edges)')
|
|
1730
|
+
END;
|
|
1731
|
+
END
|
|
1732
|
+
`);
|
|
1733
|
+
db.exec(`
|
|
1734
|
+
CREATE TRIGGER IF NOT EXISTS trg_relations_consolidated_only_update
|
|
1735
|
+
BEFORE UPDATE ON relations
|
|
1736
|
+
WHEN NEW.memory_id IS NOT OLD.memory_id
|
|
1737
|
+
OR NEW.source_kind IS NOT OLD.source_kind
|
|
1738
|
+
OR NEW.tenant_id IS NOT OLD.tenant_id
|
|
1739
|
+
OR NEW.from_entity_id IS NOT OLD.from_entity_id
|
|
1740
|
+
OR NEW.to_entity_id IS NOT OLD.to_entity_id
|
|
1741
|
+
BEGIN
|
|
1742
|
+
SELECT CASE
|
|
1743
|
+
WHEN NEW.source_kind != (SELECT kind FROM memories WHERE id = NEW.memory_id)
|
|
1744
|
+
THEN RAISE(ABORT, 'relations.source_kind must equal the referenced memory kind; the graph indexes consolidated state only (no raw)')
|
|
1745
|
+
WHEN NEW.tenant_id != (SELECT tenant_id FROM memories WHERE id = NEW.memory_id)
|
|
1746
|
+
THEN RAISE(ABORT, 'relations.tenant_id must match memories.tenant_id for the referenced memory')
|
|
1747
|
+
WHEN NEW.tenant_id != (SELECT tenant_id FROM entities WHERE id = NEW.from_entity_id)
|
|
1748
|
+
THEN RAISE(ABORT, 'relations.tenant_id must match the from_entity tenant (no cross-tenant edges)')
|
|
1749
|
+
WHEN NEW.tenant_id != (SELECT tenant_id FROM entities WHERE id = NEW.to_entity_id)
|
|
1750
|
+
THEN RAISE(ABORT, 'relations.tenant_id must match the to_entity tenant (no cross-tenant edges)')
|
|
1751
|
+
END;
|
|
1752
|
+
END
|
|
1753
|
+
`);
|
|
1754
|
+
// graph_extraction_queue guard: kind must equal the FK'd memory's actual kind
|
|
1755
|
+
// (so a raw memory ABORTs), and tenant must match. INSERT and UPDATE.
|
|
1756
|
+
db.exec(`
|
|
1757
|
+
CREATE TRIGGER IF NOT EXISTS trg_graph_queue_consolidated_only_insert
|
|
1758
|
+
BEFORE INSERT ON graph_extraction_queue
|
|
1759
|
+
BEGIN
|
|
1760
|
+
SELECT CASE
|
|
1761
|
+
WHEN NEW.kind != (SELECT kind FROM memories WHERE id = NEW.memory_id)
|
|
1762
|
+
THEN RAISE(ABORT, 'graph_extraction_queue.kind must equal the referenced memory kind; only consolidated memories are queued (no raw)')
|
|
1763
|
+
WHEN NEW.tenant_id != (SELECT tenant_id FROM memories WHERE id = NEW.memory_id)
|
|
1764
|
+
THEN RAISE(ABORT, 'graph_extraction_queue.tenant_id must match memories.tenant_id for the referenced memory')
|
|
1765
|
+
END;
|
|
1766
|
+
END
|
|
1767
|
+
`);
|
|
1768
|
+
db.exec(`
|
|
1769
|
+
CREATE TRIGGER IF NOT EXISTS trg_graph_queue_consolidated_only_update
|
|
1770
|
+
BEFORE UPDATE ON graph_extraction_queue
|
|
1771
|
+
WHEN NEW.memory_id IS NOT OLD.memory_id
|
|
1772
|
+
OR NEW.kind IS NOT OLD.kind
|
|
1773
|
+
OR NEW.tenant_id IS NOT OLD.tenant_id
|
|
1774
|
+
BEGIN
|
|
1775
|
+
SELECT CASE
|
|
1776
|
+
WHEN NEW.kind != (SELECT kind FROM memories WHERE id = NEW.memory_id)
|
|
1777
|
+
THEN RAISE(ABORT, 'graph_extraction_queue.kind must equal the referenced memory kind; only consolidated memories are queued (no raw)')
|
|
1778
|
+
WHEN NEW.tenant_id != (SELECT tenant_id FROM memories WHERE id = NEW.memory_id)
|
|
1779
|
+
THEN RAISE(ABORT, 'graph_extraction_queue.tenant_id must match memories.tenant_id for the referenced memory')
|
|
1780
|
+
END;
|
|
1781
|
+
END
|
|
1782
|
+
`);
|
|
1783
|
+
// Reverse guard (codex-review-critic 2026-06-01, P1): the graph-table triggers
|
|
1784
|
+
// only fire on writes to the GRAPH tables. They do NOT fire when an
|
|
1785
|
+
// already-indexed memory is later mutated. So 'UPDATE memories SET kind=raw'
|
|
1786
|
+
// (or a tenant change) on a memory the graph references would silently leave
|
|
1787
|
+
// entity/relation/queue rows pointing at a raw / cross-tenant memory while
|
|
1788
|
+
// their source_kind stays 'distilled' - bypassing the central 'graph never
|
|
1789
|
+
// indexes raw' invariant after insertion. This trigger closes that direction:
|
|
1790
|
+
// a memory cannot be reclassified to raw, nor moved cross-tenant, WHILE the
|
|
1791
|
+
// graph references it (rebuild/remove the graph rows first). Cheap: the EXISTS
|
|
1792
|
+
// checks are only evaluated when kind actually becomes raw or tenant changes.
|
|
1793
|
+
db.exec(`
|
|
1794
|
+
CREATE TRIGGER IF NOT EXISTS trg_memories_graph_referenced_guard
|
|
1795
|
+
BEFORE UPDATE ON memories
|
|
1796
|
+
WHEN (NEW.kind IS NOT OLD.kind OR NEW.tenant_id IS NOT OLD.tenant_id)
|
|
1797
|
+
AND (
|
|
1798
|
+
EXISTS (SELECT 1 FROM entities WHERE memory_id = OLD.id)
|
|
1799
|
+
OR EXISTS (SELECT 1 FROM relations WHERE memory_id = OLD.id)
|
|
1800
|
+
OR EXISTS (SELECT 1 FROM graph_extraction_queue WHERE memory_id = OLD.id)
|
|
1801
|
+
)
|
|
1802
|
+
BEGIN
|
|
1803
|
+
SELECT RAISE(ABORT, 'cannot change the kind or tenant of a memory while the graph references it (E3.3 graph-on-consolidated guard); a graph-referenced memory is immutable in kind/tenant - rebuild/remove the graph rows first, or rebuild them after supersession');
|
|
1804
|
+
END
|
|
1805
|
+
`);
|
|
1806
|
+
// Reverse guard #2 (codex-review-critic 2026-06-01 retry, P2): an entity that is
|
|
1807
|
+
// a relation endpoint cannot be moved cross-tenant. The entity UPDATE trigger
|
|
1808
|
+
// validates the entity against its source memory, but an existing relation
|
|
1809
|
+
// pointing at the entity is NOT re-validated, so a raw 'UPDATE entities SET
|
|
1810
|
+
// tenant_id=?, memory_id=?' to another tenant would leave a tenant-A relation
|
|
1811
|
+
// pointing at a tenant-B entity. Block the tenant move while the entity is
|
|
1812
|
+
// referenced by any relation (rebuild the relations first).
|
|
1813
|
+
db.exec(`
|
|
1814
|
+
CREATE TRIGGER IF NOT EXISTS trg_entities_no_tenant_move_when_referenced
|
|
1815
|
+
BEFORE UPDATE ON entities
|
|
1816
|
+
WHEN NEW.tenant_id IS NOT OLD.tenant_id
|
|
1817
|
+
AND EXISTS (SELECT 1 FROM relations WHERE from_entity_id = OLD.id OR to_entity_id = OLD.id)
|
|
1818
|
+
BEGIN
|
|
1819
|
+
SELECT RAISE(ABORT, 'cannot move an entity cross-tenant while a relation references it as an endpoint (E3.3 graph-on-consolidated guard); rebuild/remove the relations first');
|
|
1820
|
+
END
|
|
1821
|
+
`);
|
|
1822
|
+
}
|
|
1823
|
+
},
|
|
1824
|
+
},
|
|
1095
1825
|
];
|
|
1096
1826
|
function tableHasColumn(db, tableName, columnName) {
|
|
1097
1827
|
if (!/^[a-z_]+$/i.test(tableName))
|