@jhizzard/termdeck 1.0.14 → 1.1.1
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/package.json +1 -1
- package/packages/cli/src/init-mnestra.js +50 -10
- package/packages/client/public/app.js +57 -0
- package/packages/server/src/index.js +19 -1
- package/packages/server/src/setup/migrations.js +514 -1
- package/packages/server/src/setup/mnestra-migrations/001_mnestra_tables.sql +2 -2
- package/packages/server/src/setup/mnestra-migrations/002_mnestra_search_function.sql +1 -1
- package/packages/server/src/setup/mnestra-migrations/009_memory_relationship_metadata.sql +1 -1
- package/packages/server/src/setup/mnestra-migrations/011_project_tag_backfill.sql +7 -3
- package/packages/server/src/setup/mnestra-migrations/012_project_tag_re_taxonomy.sql +2 -2
- package/packages/server/src/setup/mnestra-migrations/014_explicit_grants.sql +3 -3
- package/packages/server/src/setup/mnestra-migrations/016_mnestra_doctor_probes.sql +3 -3
- package/packages/server/src/setup/mnestra-migrations/017_memory_sessions_session_metadata.sql +5 -5
- package/packages/server/src/setup/mnestra-migrations/018_rumen_processed_at.sql +1 -1
- package/packages/server/src/setup/mnestra-migrations/019_security_hardening.sql +190 -0
- package/packages/server/src/setup/mnestra-migrations/020_migration_tracking.sql +57 -0
- package/packages/server/src/setup/mnestra-migrations/021_project_tag_canonicalize_claimguard.sql +175 -0
- package/packages/server/src/setup/mnestra-migrations/022_source_agent_backfill.sql +182 -0
- package/packages/server/src/setup/rumen/functions/rumen-tick/index.ts +0 -30
|
@@ -26,9 +26,146 @@
|
|
|
26
26
|
|
|
27
27
|
const fs = require('fs');
|
|
28
28
|
const path = require('path');
|
|
29
|
+
const crypto = require('crypto');
|
|
29
30
|
|
|
30
31
|
const SETUP_DIR = __dirname;
|
|
31
32
|
|
|
33
|
+
// Sprint 61 T2 — durable migration tracking table + filename + table name.
|
|
34
|
+
// `mnestra_migrations` is created by bundled migration 020 (RLS-on,
|
|
35
|
+
// service_role-only, no policies). The applyPendingMigrations diff loop and
|
|
36
|
+
// the backfill probe both target this table.
|
|
37
|
+
const TRACKER_TABLE = 'public.mnestra_migrations';
|
|
38
|
+
const TRACKER_FILE = '020_migration_tracking.sql';
|
|
39
|
+
|
|
40
|
+
// Sprint 61 T2 — declarative probe set.
|
|
41
|
+
//
|
|
42
|
+
// One row per bundled mnestra migration 001-019 (020 itself is the tracker
|
|
43
|
+
// and is bootstrap-special-cased; not probed). Each probe is a single
|
|
44
|
+
// presence-style SQL statement: returns ≥1 row when the migration's schema
|
|
45
|
+
// artifact is in place, 0 rows otherwise.
|
|
46
|
+
//
|
|
47
|
+
// Used by applyPendingMigrations() during the backfill pass: when an install
|
|
48
|
+
// is pre-020 (no tracker table yet) and a bundled migration is not in the
|
|
49
|
+
// applied-set, the probe decides whether the migration's effects are already
|
|
50
|
+
// present (→ INSERT a backfill tracker row, skip apply) or genuinely missing
|
|
51
|
+
// (→ run the migration via the normal apply path, INSERT a real tracker row).
|
|
52
|
+
//
|
|
53
|
+
// Probe values:
|
|
54
|
+
// - string: SQL fragment to run via client.query(). Probe is "present"
|
|
55
|
+
// when the result has ≥1 row.
|
|
56
|
+
// - null: no schema artifact to introspect (DML migrations, comments-only
|
|
57
|
+
// placeholders). The first apply runs the migration; the tracker
|
|
58
|
+
// row prevents re-application on subsequent passes. The brief
|
|
59
|
+
// notes 003 (event_webhook placeholder), 011 (project_tag_backfill
|
|
60
|
+
// DML), and 012 (project_tag_re_taxonomy DML) fall here.
|
|
61
|
+
const MIGRATION_PROBES = Object.freeze({
|
|
62
|
+
'001_mnestra_tables.sql':
|
|
63
|
+
"select 1 from information_schema.tables where table_schema='public' and table_name='memory_items'",
|
|
64
|
+
'002_mnestra_search_function.sql':
|
|
65
|
+
"select 1 from pg_proc where proname='memory_hybrid_search'",
|
|
66
|
+
// 003 is a comments-only placeholder migration with no DDL/DML body. The
|
|
67
|
+
// apply path is a no-op on every install. Always-present probe is the
|
|
68
|
+
// honest schema fingerprint — every install for which 001 has run is
|
|
69
|
+
// also "compatible with 003." Post-Sprint-61-T2-audit refinement.
|
|
70
|
+
'003_mnestra_event_webhook.sql':
|
|
71
|
+
"select 1",
|
|
72
|
+
'004_mnestra_match_count_cap_and_explain.sql':
|
|
73
|
+
"select 1 from pg_proc where proname='memory_hybrid_search_explain'",
|
|
74
|
+
'005_v0_1_to_v0_2_upgrade.sql':
|
|
75
|
+
"select 1 from information_schema.columns where table_schema='public' and table_name='memory_items' and column_name='archived'",
|
|
76
|
+
'006_memory_status_rpc.sql':
|
|
77
|
+
"select 1 from pg_proc where proname='memory_status_aggregation'",
|
|
78
|
+
'007_add_source_session_id.sql':
|
|
79
|
+
"select 1 from information_schema.columns where table_schema='public' and table_name='memory_items' and column_name='source_session_id'",
|
|
80
|
+
'008_legacy_rag_tables.sql':
|
|
81
|
+
"select 1 from information_schema.tables where table_schema='public' and table_name='mnestra_session_memory'",
|
|
82
|
+
'009_memory_relationship_metadata.sql':
|
|
83
|
+
"select 1 from information_schema.columns where table_schema='public' and table_name='memory_relationships' and column_name='weight'",
|
|
84
|
+
'010_memory_recall_graph.sql':
|
|
85
|
+
"select 1 from pg_proc where proname='memory_recall_graph'",
|
|
86
|
+
// 011 retags chopin-nashville rows into post-Sprint-41 buckets (termdeck,
|
|
87
|
+
// rumen, podium, pvb, dor). Probe present iff any row carries one of those
|
|
88
|
+
// tags — meaning either 011 has run, or the install legitimately has rows
|
|
89
|
+
// tagged that way through other means. Either way the apply is a no-op
|
|
90
|
+
// (the UPDATEs are gated on `project = 'chopin-nashville'`), so a false-
|
|
91
|
+
// positive backfill costs nothing. Post-Sprint-61-T2-audit refinement.
|
|
92
|
+
'011_project_tag_backfill.sql':
|
|
93
|
+
"select 1 from memory_items where project in ('termdeck', 'rumen', 'podium', 'pvb', 'dor') limit 1",
|
|
94
|
+
// 012 expands 011's taxonomy with chopin-in-bohemia, chopin-scheduler, and
|
|
95
|
+
// claimguard buckets. Probe present iff any row is in those expanded
|
|
96
|
+
// buckets. Same false-positive-is-harmless reasoning as 011.
|
|
97
|
+
// Post-Sprint-61-T2-audit refinement.
|
|
98
|
+
'012_project_tag_re_taxonomy.sql':
|
|
99
|
+
"select 1 from memory_items where project in ('chopin-in-bohemia', 'chopin-scheduler', 'claimguard') limit 1",
|
|
100
|
+
'013_reclassify_uncertain.sql':
|
|
101
|
+
"select 1 from information_schema.columns where table_schema='public' and table_name='memory_items' and column_name='reclassified_by'",
|
|
102
|
+
'014_explicit_grants.sql':
|
|
103
|
+
"select 1 where has_table_privilege('service_role', 'public.memory_items', 'INSERT')",
|
|
104
|
+
'015_source_agent.sql':
|
|
105
|
+
"select 1 from information_schema.columns where table_schema='public' and table_name='memory_items' and column_name='source_agent'",
|
|
106
|
+
'016_mnestra_doctor_probes.sql':
|
|
107
|
+
"select 1 from pg_proc where proname='mnestra_doctor_vault_secret_exists'",
|
|
108
|
+
'017_memory_sessions_session_metadata.sql':
|
|
109
|
+
"select 1 from information_schema.columns where table_schema='public' and table_name='memory_sessions' and column_name='session_id'",
|
|
110
|
+
'018_rumen_processed_at.sql':
|
|
111
|
+
"select 1 from information_schema.columns where table_schema='public' and table_name='memory_sessions' and column_name='rumen_processed_at'",
|
|
112
|
+
'019_security_hardening.sql':
|
|
113
|
+
"select 1 from pg_proc p, unnest(coalesce(p.proconfig,'{}'::text[])) c where p.proname='memory_hybrid_search' and c like 'search_path=%' and c like '%extensions%'",
|
|
114
|
+
// 021 canonicalizes legacy gorgias / gorgias-ticket-monitor project tags to
|
|
115
|
+
// claimguard. Probe is NOT-EXISTS-shaped: returns 1 row when both legacy
|
|
116
|
+
// tags carry zero rows (021's effects are in place OR the install never had
|
|
117
|
+
// legacy data). Returns 0 rows when at least one legacy tag still has rows
|
|
118
|
+
// (021 has not yet run). False-positive backfill costs nothing because the
|
|
119
|
+
// migration's UPDATE is gated on `project IN ('gorgias', 'gorgias-ticket-monitor')`
|
|
120
|
+
// so a re-apply against an already-canonicalized corpus is a 0-row no-op.
|
|
121
|
+
// Sprint 62 T2 added this; 020 is bootstrap-special-cased and intentionally
|
|
122
|
+
// absent from MIGRATION_PROBES.
|
|
123
|
+
'021_project_tag_canonicalize_claimguard.sql':
|
|
124
|
+
"select 1 where not exists (select 1 from memory_items where project in ('gorgias', 'gorgias-ticket-monitor'))",
|
|
125
|
+
// 022 backfills source_agent for the rows where the writer is inferable from
|
|
126
|
+
// row shape (Predicate A: decision/bug_fix/architecture/preference/code_context
|
|
127
|
+
// → 'claude'; Predicate B: fact rows with source_session_id → 'claude';
|
|
128
|
+
// Predicate D: document_chunk → 'orchestrator'). Predicate C (fact rows
|
|
129
|
+
// with no session and no path) is intentionally NOT backfilled — see the
|
|
130
|
+
// migration body for the provenance-preservation rationale. Probe is
|
|
131
|
+
// NOT-EXISTS-shaped over the A/B/D row-set: returns 1 when those targets
|
|
132
|
+
// all have source_agent set (022's effects in place), 0 when any A/B/D
|
|
133
|
+
// target still has NULL (022 has not yet run). Excludes Predicate C from
|
|
134
|
+
// the probe predicate so the residual NULL slice doesn't keep the probe
|
|
135
|
+
// false forever. False-positive backfill costs nothing because the
|
|
136
|
+
// migration body is gated on `source_agent IS NULL` and a re-apply against
|
|
137
|
+
// an already-tagged corpus is a 0-row no-op. Sprint 62 T3.
|
|
138
|
+
'022_source_agent_backfill.sql':
|
|
139
|
+
"select 1 where not exists (select 1 from memory_items where source_agent is null and (source_type in ('decision','bug_fix','architecture','preference','code_context') or (source_type='fact' and source_session_id is not null) or source_type='document_chunk'))"
|
|
140
|
+
});
|
|
141
|
+
|
|
142
|
+
// Sprint 61 T2 — self-transactional detection.
|
|
143
|
+
//
|
|
144
|
+
// Bundled migrations 011 + 012 contain top-level `BEGIN;` and `COMMIT;`
|
|
145
|
+
// statements (011:75/217, 012:76/353). When the diff-apply loop wrapped
|
|
146
|
+
// these in its own outer BEGIN/COMMIT, the inner COMMIT closed the outer
|
|
147
|
+
// transaction prematurely and the subsequent `recordApplied` INSERT ran
|
|
148
|
+
// auto-committed — defeating the per-file atomicity contract. T4-CODEX
|
|
149
|
+
// audit-concern 2026-05-07 18:51 ET surfaced this.
|
|
150
|
+
//
|
|
151
|
+
// Detection: case-sensitive match for a line that is exactly `BEGIN;` or
|
|
152
|
+
// `COMMIT;` (top-level). PL/pgSQL anonymous block delimiters (`begin ... end`
|
|
153
|
+
// inside `do $$ ... $$`) use lowercase without trailing semicolon-on-its-
|
|
154
|
+
// own-line, so they don't match.
|
|
155
|
+
//
|
|
156
|
+
// Behavior for self-transactional migrations: SKIP the outer wrapper.
|
|
157
|
+
// Apply via pgRunner.applyFile (which sends the file as a single batched
|
|
158
|
+
// query, inner BEGIN/COMMIT handled by Postgres). Then INSERT the tracker
|
|
159
|
+
// row in a separate auto-commit. The tracker INSERT is recoverable on
|
|
160
|
+
// failure: 011/012 are idempotent (`WHERE project = 'chopin-nashville'`
|
|
161
|
+
// gates every UPDATE; re-running on an already-retagged install is a no-op),
|
|
162
|
+
// so a missing tracker row from a failed INSERT will be re-applied on the
|
|
163
|
+
// next pass and the INSERT retried. The brief explicitly notes this
|
|
164
|
+
// recovery shape under "out-of-T2 scope: rumen migration tracker."
|
|
165
|
+
function isSelfTransactional(sql) {
|
|
166
|
+
return /^[ \t]*(BEGIN|COMMIT)[ \t]*;[ \t]*$/m.test(sql);
|
|
167
|
+
}
|
|
168
|
+
|
|
32
169
|
function listBundled(subdir) {
|
|
33
170
|
const dir = path.join(SETUP_DIR, subdir);
|
|
34
171
|
if (!fs.existsSync(dir)) return [];
|
|
@@ -127,11 +264,387 @@ function readFile(filepath) {
|
|
|
127
264
|
return fs.readFileSync(filepath, 'utf-8');
|
|
128
265
|
}
|
|
129
266
|
|
|
267
|
+
// ── Sprint 61 T2 — durable migration tracker + diff-and-apply ──────────────
|
|
268
|
+
//
|
|
269
|
+
// applyPendingMigrations(client, opts) replaces the per-wizard
|
|
270
|
+
// "apply every bundled migration" loop with a tracker-aware diff loop:
|
|
271
|
+
//
|
|
272
|
+
// 1. Try `SELECT filename, checksum FROM public.mnestra_migrations`.
|
|
273
|
+
// On 42P01 (relation does not exist), the project is pre-020 — bootstrap
|
|
274
|
+
// by applying 020 directly, then INSERT 020's own tracker row, then
|
|
275
|
+
// re-query.
|
|
276
|
+
// 2. Iterate bundled migrations 001..N in lex-filename order. For each
|
|
277
|
+
// bundled file:
|
|
278
|
+
// - Already in tracker: skip; if tracked checksum != bundled checksum,
|
|
279
|
+
// push to warnings[] (do NOT auto-overwrite).
|
|
280
|
+
// - Not in tracker AND probe says present: INSERT backfill row
|
|
281
|
+
// (applied_at = '1970-01-01T00:00:00Z', schema_version = 'backfill'),
|
|
282
|
+
// skip apply. (As of Sprint 61 T2 audit refinement, every bundled
|
|
283
|
+
// migration 001-019 has a non-null probe in MIGRATION_PROBES; the
|
|
284
|
+
// null-probe branch is preserved for forward-compatibility.)
|
|
285
|
+
// - Not in tracker AND probe absent (or null probe):
|
|
286
|
+
// * Self-transactional file (top-level BEGIN; / COMMIT;, currently
|
|
287
|
+
// 011/012): SKIP the outer wrapper. apply via pgRunner.applyFile
|
|
288
|
+
// (the file's own transaction control runs through Postgres).
|
|
289
|
+
// INSERT tracker row in a separate auto-commit. Tracker INSERT
|
|
290
|
+
// failure is recoverable: re-running applyPendingMigrations
|
|
291
|
+
// re-applies the migration (idempotent — the bundled self-tx
|
|
292
|
+
// files are gated on chopin-nashville rows, no-op on subsequent
|
|
293
|
+
// runs) and retries the tracker INSERT.
|
|
294
|
+
// * Non-self-transactional file: BEGIN, apply via
|
|
295
|
+
// pgRunner.applyFile, INSERT real tracker row, COMMIT. On
|
|
296
|
+
// error, ROLLBACK and halt — record errored summary, do not
|
|
297
|
+
// attempt subsequent migrations.
|
|
298
|
+
//
|
|
299
|
+
// Returns:
|
|
300
|
+
// {
|
|
301
|
+
// applied: string[] // filenames applied this pass
|
|
302
|
+
// skipped: string[] // filenames already in tracker
|
|
303
|
+
// backfilled: string[] // filenames probe-seeded this pass
|
|
304
|
+
// warnings: Array<{ file, trackedChecksum, bundledChecksum }>
|
|
305
|
+
// errored: null | { file, error }
|
|
306
|
+
// }
|
|
307
|
+
//
|
|
308
|
+
// Idempotent: a second invocation against an up-to-date project reports
|
|
309
|
+
// applied=[], backfilled=[], errored=null, and skipped=[...all bundled].
|
|
310
|
+
//
|
|
311
|
+
// Test injection (every dependency is replaceable):
|
|
312
|
+
// opts._migrations — module override for listMnestraMigrations / readFile
|
|
313
|
+
// (defaults to this module's own exports).
|
|
314
|
+
// opts._readFile — file-read shim (defaults to fs.readFileSync utf-8).
|
|
315
|
+
// opts._applyFile — pgRunner.applyFile shim (defaults to lazy-required
|
|
316
|
+
// ./pg-runner.applyFile to avoid pulling node-postgres
|
|
317
|
+
// at module-load time).
|
|
318
|
+
// opts._probes — MIGRATION_PROBES override (defaults to the constant).
|
|
319
|
+
|
|
320
|
+
function computeChecksum(content) {
|
|
321
|
+
return crypto.createHash('sha256').update(content, 'utf-8').digest('hex');
|
|
322
|
+
}
|
|
323
|
+
|
|
324
|
+
// Lazy-resolve pgRunner.applyFile so callers in test environments can avoid
|
|
325
|
+
// the `require('pg')` cost. Tests override via opts._applyFile.
|
|
326
|
+
function defaultApplyFile() {
|
|
327
|
+
const pgRunner = require('./pg-runner');
|
|
328
|
+
return (client, file) => pgRunner.applyFile(client, file);
|
|
329
|
+
}
|
|
330
|
+
|
|
331
|
+
// Returns Map<filename, checksum> when the tracker exists, null when the
|
|
332
|
+
// table is missing (PG error code 42P01 — relation does not exist). Any
|
|
333
|
+
// other error propagates.
|
|
334
|
+
async function loadAppliedSet(client) {
|
|
335
|
+
let res;
|
|
336
|
+
try {
|
|
337
|
+
res = await client.query(
|
|
338
|
+
`SELECT filename, checksum FROM ${TRACKER_TABLE}`
|
|
339
|
+
);
|
|
340
|
+
} catch (err) {
|
|
341
|
+
if (err && err.code === '42P01') return null;
|
|
342
|
+
throw err;
|
|
343
|
+
}
|
|
344
|
+
const map = new Map();
|
|
345
|
+
for (const row of (res && res.rows) || []) {
|
|
346
|
+
map.set(row.filename, row.checksum);
|
|
347
|
+
}
|
|
348
|
+
return map;
|
|
349
|
+
}
|
|
350
|
+
|
|
351
|
+
// Pre-020 bootstrap: the tracker doesn't exist yet, so apply 020 to create
|
|
352
|
+
// it, then INSERT 020's own tracker row. Caller re-queries the tracker
|
|
353
|
+
// afterwards to pick up the seeded row.
|
|
354
|
+
async function bootstrapTracker(client, files, applyFile, readFileImpl) {
|
|
355
|
+
const trackerPath = files.find(
|
|
356
|
+
(f) => path.basename(f) === TRACKER_FILE
|
|
357
|
+
);
|
|
358
|
+
if (!trackerPath) {
|
|
359
|
+
throw new Error(
|
|
360
|
+
`applyPendingMigrations: bundled ${TRACKER_FILE} not found in migration set — ` +
|
|
361
|
+
`the migration tracker cannot bootstrap. Re-publish the package or sync ` +
|
|
362
|
+
`the engram migrations directory into packages/server/src/setup/mnestra-migrations/.`
|
|
363
|
+
);
|
|
364
|
+
}
|
|
365
|
+
const result = await applyFile(client, trackerPath);
|
|
366
|
+
if (!result || result.ok !== true) {
|
|
367
|
+
const detail = (result && result.error) || 'apply returned not-ok with no error message';
|
|
368
|
+
throw new Error(
|
|
369
|
+
`applyPendingMigrations: failed to bootstrap tracker via ${TRACKER_FILE}: ${detail}`
|
|
370
|
+
);
|
|
371
|
+
}
|
|
372
|
+
const sql = readFileImpl(trackerPath);
|
|
373
|
+
const checksum = computeChecksum(sql);
|
|
374
|
+
await client.query(
|
|
375
|
+
`INSERT INTO ${TRACKER_TABLE} (filename, applied_at, checksum, schema_version) ` +
|
|
376
|
+
`VALUES ($1, now(), $2, $3) ` +
|
|
377
|
+
`ON CONFLICT (filename) DO UPDATE SET applied_at = EXCLUDED.applied_at, ` +
|
|
378
|
+
`checksum = EXCLUDED.checksum, schema_version = EXCLUDED.schema_version`,
|
|
379
|
+
[TRACKER_FILE, checksum, null]
|
|
380
|
+
);
|
|
381
|
+
}
|
|
382
|
+
|
|
383
|
+
// Run a probe SQL string and return whether the schema artifact is present.
|
|
384
|
+
// Null probes always return false. Probe-side errors (e.g. relation does
|
|
385
|
+
// not exist when probing into the live schema) are swallowed and degrade
|
|
386
|
+
// to "absent" — same posture as audit-upgrade.js::probeOne. The artifact's
|
|
387
|
+
// real apply path will surface any underlying error with full context.
|
|
388
|
+
async function probePresent(client, probeSql) {
|
|
389
|
+
if (!probeSql) return false;
|
|
390
|
+
try {
|
|
391
|
+
const res = await client.query(probeSql);
|
|
392
|
+
return Array.isArray(res && res.rows) && res.rows.length > 0;
|
|
393
|
+
} catch (_err) {
|
|
394
|
+
return false;
|
|
395
|
+
}
|
|
396
|
+
}
|
|
397
|
+
|
|
398
|
+
// Insert a backfill row for a migration whose probe came back present on
|
|
399
|
+
// a pre-020 install. applied_at is the epoch sentinel so audit queries
|
|
400
|
+
// can distinguish "applied at install" from "tracked from day one." The
|
|
401
|
+
// checksum is recorded too so future bundle drift can still be detected
|
|
402
|
+
// against backfilled rows.
|
|
403
|
+
async function recordBackfill(client, filename, checksum) {
|
|
404
|
+
await client.query(
|
|
405
|
+
`INSERT INTO ${TRACKER_TABLE} (filename, applied_at, checksum, schema_version) ` +
|
|
406
|
+
`VALUES ($1, $2::timestamptz, $3, $4) ` +
|
|
407
|
+
`ON CONFLICT (filename) DO NOTHING`,
|
|
408
|
+
[filename, '1970-01-01T00:00:00Z', checksum, 'backfill']
|
|
409
|
+
);
|
|
410
|
+
}
|
|
411
|
+
|
|
412
|
+
// Insert a real tracker row after a successful apply.
|
|
413
|
+
async function recordApplied(client, filename, checksum) {
|
|
414
|
+
await client.query(
|
|
415
|
+
`INSERT INTO ${TRACKER_TABLE} (filename, applied_at, checksum, schema_version) ` +
|
|
416
|
+
`VALUES ($1, now(), $2, $3) ` +
|
|
417
|
+
`ON CONFLICT (filename) DO UPDATE SET applied_at = EXCLUDED.applied_at, ` +
|
|
418
|
+
`checksum = EXCLUDED.checksum, schema_version = EXCLUDED.schema_version`,
|
|
419
|
+
[filename, checksum, null]
|
|
420
|
+
);
|
|
421
|
+
}
|
|
422
|
+
|
|
423
|
+
async function applyPendingMigrations(client, opts = {}) {
|
|
424
|
+
if (!client || typeof client.query !== 'function') {
|
|
425
|
+
throw new Error('applyPendingMigrations: client with .query() is required');
|
|
426
|
+
}
|
|
427
|
+
|
|
428
|
+
const _migrations = opts._migrations || module.exports;
|
|
429
|
+
const _readFile = opts._readFile || ((p) => fs.readFileSync(p, 'utf-8'));
|
|
430
|
+
const _applyFile = opts._applyFile || defaultApplyFile();
|
|
431
|
+
const _probes = opts._probes || MIGRATION_PROBES;
|
|
432
|
+
|
|
433
|
+
const files = _migrations.listMnestraMigrations();
|
|
434
|
+
if (!files || files.length === 0) {
|
|
435
|
+
throw new Error(
|
|
436
|
+
'applyPendingMigrations: no Mnestra migrations bundled — TermDeck install looks corrupted.'
|
|
437
|
+
);
|
|
438
|
+
}
|
|
439
|
+
|
|
440
|
+
const summary = {
|
|
441
|
+
applied: [],
|
|
442
|
+
skipped: [],
|
|
443
|
+
backfilled: [],
|
|
444
|
+
warnings: [],
|
|
445
|
+
errored: null
|
|
446
|
+
};
|
|
447
|
+
|
|
448
|
+
// Step 1: load applied-set (or bootstrap if missing).
|
|
449
|
+
let applied = await loadAppliedSet(client);
|
|
450
|
+
let bootstrapped = false;
|
|
451
|
+
if (applied === null) {
|
|
452
|
+
// Pre-020 — apply 020 + INSERT row.
|
|
453
|
+
await bootstrapTracker(client, files, _applyFile, _readFile);
|
|
454
|
+
bootstrapped = true;
|
|
455
|
+
summary.applied.push(TRACKER_FILE);
|
|
456
|
+
applied = await loadAppliedSet(client);
|
|
457
|
+
if (applied === null) {
|
|
458
|
+
throw new Error(
|
|
459
|
+
'applyPendingMigrations: tracker still missing after bootstrap — ' +
|
|
460
|
+
'check that 020_migration_tracking.sql actually created public.mnestra_migrations.'
|
|
461
|
+
);
|
|
462
|
+
}
|
|
463
|
+
}
|
|
464
|
+
|
|
465
|
+
// Step 2: iterate bundled files in lex order.
|
|
466
|
+
for (const file of files) {
|
|
467
|
+
const base = path.basename(file);
|
|
468
|
+
|
|
469
|
+
// Tracker file: bootstrap already accounted for it in summary.applied.
|
|
470
|
+
// On a non-bootstrap pass (post-020 install where 020 is in the tracker),
|
|
471
|
+
// record as skipped. On any other state (tracker present but somehow
|
|
472
|
+
// missing 020), fall through to the normal apply path so the diff loop
|
|
473
|
+
// can re-record it.
|
|
474
|
+
if (base === TRACKER_FILE) {
|
|
475
|
+
if (bootstrapped) {
|
|
476
|
+
// Already in summary.applied via bootstrap; do not duplicate.
|
|
477
|
+
continue;
|
|
478
|
+
}
|
|
479
|
+
if (applied.has(TRACKER_FILE)) {
|
|
480
|
+
summary.skipped.push(base);
|
|
481
|
+
continue;
|
|
482
|
+
}
|
|
483
|
+
// Defensive fall-through: tracker exists (loadAppliedSet succeeded) but
|
|
484
|
+
// doesn't have 020's row. Apply path below will re-record it.
|
|
485
|
+
}
|
|
486
|
+
|
|
487
|
+
let sql;
|
|
488
|
+
try {
|
|
489
|
+
sql = _readFile(file);
|
|
490
|
+
} catch (err) {
|
|
491
|
+
summary.errored = {
|
|
492
|
+
file: base,
|
|
493
|
+
error: err && err.message ? err.message : String(err)
|
|
494
|
+
};
|
|
495
|
+
return summary;
|
|
496
|
+
}
|
|
497
|
+
const checksum = computeChecksum(sql);
|
|
498
|
+
|
|
499
|
+
if (applied.has(base)) {
|
|
500
|
+
// Already applied — checksum-drift guard.
|
|
501
|
+
const trackedChecksum = applied.get(base);
|
|
502
|
+
if (trackedChecksum && trackedChecksum !== checksum) {
|
|
503
|
+
summary.warnings.push({
|
|
504
|
+
file: base,
|
|
505
|
+
trackedChecksum,
|
|
506
|
+
bundledChecksum: checksum
|
|
507
|
+
});
|
|
508
|
+
}
|
|
509
|
+
summary.skipped.push(base);
|
|
510
|
+
continue;
|
|
511
|
+
}
|
|
512
|
+
|
|
513
|
+
// Not applied. Try probe-backfill (only meaningful on pre-020 installs
|
|
514
|
+
// that just bootstrapped, but cheap to evaluate on every pass; an
|
|
515
|
+
// already-tracked migration short-circuits above before reaching here).
|
|
516
|
+
const probeSql = Object.prototype.hasOwnProperty.call(_probes, base)
|
|
517
|
+
? _probes[base]
|
|
518
|
+
: null;
|
|
519
|
+
if (probeSql) {
|
|
520
|
+
const present = await probePresent(client, probeSql);
|
|
521
|
+
if (present) {
|
|
522
|
+
try {
|
|
523
|
+
await recordBackfill(client, base, checksum);
|
|
524
|
+
} catch (err) {
|
|
525
|
+
summary.errored = {
|
|
526
|
+
file: base,
|
|
527
|
+
error: `backfill INSERT failed: ${err && err.message ? err.message : String(err)}`
|
|
528
|
+
};
|
|
529
|
+
return summary;
|
|
530
|
+
}
|
|
531
|
+
summary.backfilled.push(base);
|
|
532
|
+
applied.set(base, checksum);
|
|
533
|
+
continue;
|
|
534
|
+
}
|
|
535
|
+
}
|
|
536
|
+
|
|
537
|
+
// Genuinely needs apply. Self-transactional migrations (011, 012) ship
|
|
538
|
+
// top-level BEGIN/COMMIT in their bodies — see isSelfTransactional + the
|
|
539
|
+
// header comment near its definition. For those, skip the outer wrapper
|
|
540
|
+
// and rely on the file's own transaction control + idempotency for
|
|
541
|
+
// recovery on tracker INSERT failure. For everything else, wrap in
|
|
542
|
+
// outer BEGIN/COMMIT for per-file atomicity (apply + tracker row commit
|
|
543
|
+
// or roll back together).
|
|
544
|
+
const selfTx = isSelfTransactional(sql);
|
|
545
|
+
|
|
546
|
+
if (selfTx) {
|
|
547
|
+
let applyResult;
|
|
548
|
+
try {
|
|
549
|
+
applyResult = await _applyFile(client, file);
|
|
550
|
+
} catch (err) {
|
|
551
|
+
summary.errored = {
|
|
552
|
+
file: base,
|
|
553
|
+
error: err && err.message ? err.message : String(err)
|
|
554
|
+
};
|
|
555
|
+
return summary;
|
|
556
|
+
}
|
|
557
|
+
if (!applyResult || applyResult.ok !== true) {
|
|
558
|
+
summary.errored = {
|
|
559
|
+
file: base,
|
|
560
|
+
error: (applyResult && applyResult.error) || 'apply returned not-ok with no error message'
|
|
561
|
+
};
|
|
562
|
+
return summary;
|
|
563
|
+
}
|
|
564
|
+
try {
|
|
565
|
+
await recordApplied(client, base, checksum);
|
|
566
|
+
} catch (err) {
|
|
567
|
+
summary.errored = {
|
|
568
|
+
file: base,
|
|
569
|
+
error: `tracker INSERT failed (self-transactional ${base} applied; re-run will replay it idempotently and retry the tracker insert): ${err && err.message ? err.message : String(err)}`
|
|
570
|
+
};
|
|
571
|
+
return summary;
|
|
572
|
+
}
|
|
573
|
+
summary.applied.push(base);
|
|
574
|
+
applied.set(base, checksum);
|
|
575
|
+
continue;
|
|
576
|
+
}
|
|
577
|
+
|
|
578
|
+
// Non-self-transactional path: outer BEGIN/COMMIT wrapper.
|
|
579
|
+
try {
|
|
580
|
+
await client.query('BEGIN');
|
|
581
|
+
} catch (err) {
|
|
582
|
+
summary.errored = {
|
|
583
|
+
file: base,
|
|
584
|
+
error: `BEGIN failed: ${err && err.message ? err.message : String(err)}`
|
|
585
|
+
};
|
|
586
|
+
return summary;
|
|
587
|
+
}
|
|
588
|
+
|
|
589
|
+
let applyResult;
|
|
590
|
+
try {
|
|
591
|
+
applyResult = await _applyFile(client, file);
|
|
592
|
+
} catch (err) {
|
|
593
|
+
try { await client.query('ROLLBACK'); } catch (_e) { /* best-effort */ }
|
|
594
|
+
summary.errored = {
|
|
595
|
+
file: base,
|
|
596
|
+
error: err && err.message ? err.message : String(err)
|
|
597
|
+
};
|
|
598
|
+
return summary;
|
|
599
|
+
}
|
|
600
|
+
|
|
601
|
+
if (!applyResult || applyResult.ok !== true) {
|
|
602
|
+
try { await client.query('ROLLBACK'); } catch (_e) { /* best-effort */ }
|
|
603
|
+
summary.errored = {
|
|
604
|
+
file: base,
|
|
605
|
+
error: (applyResult && applyResult.error) || 'apply returned not-ok with no error message'
|
|
606
|
+
};
|
|
607
|
+
return summary;
|
|
608
|
+
}
|
|
609
|
+
|
|
610
|
+
try {
|
|
611
|
+
await recordApplied(client, base, checksum);
|
|
612
|
+
await client.query('COMMIT');
|
|
613
|
+
} catch (err) {
|
|
614
|
+
try { await client.query('ROLLBACK'); } catch (_e) { /* best-effort */ }
|
|
615
|
+
summary.errored = {
|
|
616
|
+
file: base,
|
|
617
|
+
error: `tracker INSERT failed: ${err && err.message ? err.message : String(err)}`
|
|
618
|
+
};
|
|
619
|
+
return summary;
|
|
620
|
+
}
|
|
621
|
+
|
|
622
|
+
summary.applied.push(base);
|
|
623
|
+
applied.set(base, checksum);
|
|
624
|
+
}
|
|
625
|
+
|
|
626
|
+
return summary;
|
|
627
|
+
}
|
|
628
|
+
|
|
130
629
|
module.exports = {
|
|
131
630
|
listMnestraMigrations,
|
|
132
631
|
listRumenMigrations,
|
|
133
632
|
rumenFunctionsRoot,
|
|
134
633
|
listRumenFunctions,
|
|
135
634
|
rumenFunctionDir,
|
|
136
|
-
readFile
|
|
635
|
+
readFile,
|
|
636
|
+
// Sprint 61 T2 — migration tracker.
|
|
637
|
+
applyPendingMigrations,
|
|
638
|
+
MIGRATION_PROBES,
|
|
639
|
+
TRACKER_TABLE,
|
|
640
|
+
TRACKER_FILE,
|
|
641
|
+
// Test surface — kept exported so tests/migration-tracker.test.js can pin
|
|
642
|
+
// each helper without a live pg client.
|
|
643
|
+
_computeChecksum: computeChecksum,
|
|
644
|
+
_loadAppliedSet: loadAppliedSet,
|
|
645
|
+
_bootstrapTracker: bootstrapTracker,
|
|
646
|
+
_probePresent: probePresent,
|
|
647
|
+
_recordBackfill: recordBackfill,
|
|
648
|
+
_recordApplied: recordApplied,
|
|
649
|
+
_isSelfTransactional: isSelfTransactional
|
|
137
650
|
};
|
|
@@ -79,8 +79,8 @@ create index if not exists memory_relationships_target_idx on memory_relationshi
|
|
|
79
79
|
-- ── match_memories helper RPC ─────────────────────────────────────────────
|
|
80
80
|
-- Used by remember.ts (dedup) and consolidate.ts (cluster seeding).
|
|
81
81
|
--
|
|
82
|
-
-- Sprint 52.1 — signature-drift guard. On long-lived v0.6.x-era installs
|
|
83
|
-
--
|
|
82
|
+
-- Sprint 52.1 — signature-drift guard. On long-lived v0.6.x-era installs,
|
|
83
|
+
-- match_memories was created by
|
|
84
84
|
-- a prior Mnestra version with a different RETURN-table column shape:
|
|
85
85
|
-- (id, content, metadata, source_type, category, project, created_at, similarity)
|
|
86
86
|
-- vs the canonical:
|
|
@@ -16,7 +16,7 @@
|
|
|
16
16
|
-- Sprint 51.9 — signature-drift guard. Same Class A pattern Sprint 52.1
|
|
17
17
|
-- closed for `match_memories` (mig 001:81-95). Codex T4 surfaced the cousin
|
|
18
18
|
-- 2026-05-04 14:42 ET during Sprint 51.5b dogfood: long-lived v0.6.x-era
|
|
19
|
-
-- installs
|
|
19
|
+
-- installs ALSO have a
|
|
20
20
|
-- 10-arg drift overload of `memory_hybrid_search` coexisting with the
|
|
21
21
|
-- canonical 8-arg signature. The drift overload carries the never-shipped
|
|
22
22
|
-- `recency_weight`/`decay_days` parameters from a pre-canonical Mnestra
|
|
@@ -8,7 +8,7 @@
|
|
|
8
8
|
--
|
|
9
9
|
-- Idempotent: safe to re-run.
|
|
10
10
|
--
|
|
11
|
-
-- Pre-existing state (verified against
|
|
11
|
+
-- Pre-existing state (verified against the reference Mnestra project 2026-04-27 17:25 ET):
|
|
12
12
|
-- memory_relationships has 749 live edges. The migration adds nullable
|
|
13
13
|
-- columns and a wider CHECK; no existing row violates the new constraint.
|
|
14
14
|
--
|
|
@@ -36,7 +36,10 @@
|
|
|
36
36
|
-- 1. termdeck / mnestra — keywords: termdeck, mnestra, "4+1 sprint"
|
|
37
37
|
-- 2. rumen — keyword: rumen
|
|
38
38
|
-- 3. podium — keyword: podium
|
|
39
|
-
-- 4. pvb — keywords: PVB,
|
|
39
|
+
-- 4. pvb — keywords: PVB, pet vet bid (and the
|
|
40
|
+
-- legacy single-word identifier matched
|
|
41
|
+
-- by the load-bearing classifier on
|
|
42
|
+
-- line 156)
|
|
40
43
|
-- 5. dor / openclaw — TIGHTENED:
|
|
41
44
|
-- word-boundary uppercase DOR (rules out
|
|
42
45
|
-- "dormant", "vendored", "indoor", etc.),
|
|
@@ -49,7 +52,8 @@
|
|
|
49
52
|
-- rumen: 92 rows, all 6 sampled were true positives.
|
|
50
53
|
-- podium: 58 rows, all 6 sampled were true positives.
|
|
51
54
|
-- pvb: 7 rows, 1 of those overlaps with mnestra ("Mnestra
|
|
52
|
-
-- repo …
|
|
55
|
+
-- repo … legacy single-word project name") and gets
|
|
56
|
+
-- claimed by bucket 1.
|
|
53
57
|
-- dor (tightened): 3 rows after tightening from 6 — the original
|
|
54
58
|
-- `%dor%` ILIKE pattern caught false positives like
|
|
55
59
|
-- "dormant", "vendored". Final 3 rows are all true
|
|
@@ -143,7 +147,7 @@ BEGIN
|
|
|
143
147
|
END $$;
|
|
144
148
|
|
|
145
149
|
-- ============================================================
|
|
146
|
-
-- BUCKET 4 — PVB (case-insensitive
|
|
150
|
+
-- BUCKET 4 — PVB (case-insensitive content markers — see code below)
|
|
147
151
|
-- ============================================================
|
|
148
152
|
DO $$
|
|
149
153
|
DECLARE
|
|
@@ -49,7 +49,7 @@
|
|
|
49
49
|
-- by Joshua; case-sensitive word-boundary token
|
|
50
50
|
-- avoids matching unrelated "[maestro]" log
|
|
51
51
|
-- prefixes)
|
|
52
|
-
-- 6. pvb — PVB,
|
|
52
|
+
-- 6. pvb — PVB, "pet vet bid"
|
|
53
53
|
-- 7. claimguard — claimguard, gorgias-ticket-monitor,
|
|
54
54
|
-- "gorgias ticket monitor"
|
|
55
55
|
-- 8. dor — \mDOR\M, /DOR/, ~/Documents/DOR, dor.config,
|
|
@@ -234,7 +234,7 @@ BEGIN
|
|
|
234
234
|
END $$;
|
|
235
235
|
|
|
236
236
|
-- ============================================================
|
|
237
|
-
-- BUCKET 6 — pvb (case-insensitive
|
|
237
|
+
-- BUCKET 6 — pvb (case-insensitive content markers — see code below)
|
|
238
238
|
--
|
|
239
239
|
-- Same pattern as 011 bucket 4. PVB is small in the chopin-nashville bucket
|
|
240
240
|
-- (Sprint 39 dry-run found 7 rows; live apply landed 3 because bucket 1
|
|
@@ -17,9 +17,9 @@
|
|
|
17
17
|
-- PostgREST checks table-level privileges before evaluating RLS, so
|
|
18
18
|
-- service_role's bypassrls attribute does not help.
|
|
19
19
|
--
|
|
20
|
-
-- Reported and root-caused by Brad Heath 2026-04-28 against
|
|
21
|
-
--
|
|
22
|
-
--
|
|
20
|
+
-- Reported and root-caused by Brad Heath 2026-04-28 against his Mnestra
|
|
21
|
+
-- project; fix verified end-to-end on his install before being upstreamed
|
|
22
|
+
-- here.
|
|
23
23
|
--
|
|
24
24
|
-- This migration is idempotent and safe on greenfield projects where
|
|
25
25
|
-- the auto-grant default already fired (the GRANTs become no-ops).
|
|
@@ -27,8 +27,8 @@
|
|
|
27
27
|
-- their grants are now wrapped in a do$$ guard that only emits them
|
|
28
28
|
-- when `pg_cron` is enabled. The doctor's cron-related probes return
|
|
29
29
|
-- the existing `unknown` band (Sprint 51.5 T2 already established it)
|
|
30
|
-
-- when the wrappers don't exist — graceful degradation.
|
|
31
|
-
--
|
|
30
|
+
-- when the wrappers don't exist — graceful degradation. Existing
|
|
31
|
+
-- rumen-bearing installs are unaffected because both have rumen installed,
|
|
32
32
|
-- which enables pg_cron via rumen's mig 002. Closes the
|
|
33
33
|
-- mnestra-only-no-rumen fresh-install path.
|
|
34
34
|
|
|
@@ -103,7 +103,7 @@ grant execute on function mnestra_doctor_vault_secret_exists(text) to ser
|
|
|
103
103
|
--
|
|
104
104
|
-- Idempotent: do$$ runs every replay; CREATE OR REPLACE keeps the
|
|
105
105
|
-- function definitions in sync if pg_cron later gets enabled and the
|
|
106
|
-
-- migration re-runs. Existing installs
|
|
106
|
+
-- migration re-runs. Existing installs typically have
|
|
107
107
|
-- pg_cron from Rumen's install path and emit these unconditionally.
|
|
108
108
|
|
|
109
109
|
do $cron_guard$
|