@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.
Files changed (19) hide show
  1. package/package.json +1 -1
  2. package/packages/cli/src/init-mnestra.js +50 -10
  3. package/packages/client/public/app.js +57 -0
  4. package/packages/server/src/index.js +19 -1
  5. package/packages/server/src/setup/migrations.js +514 -1
  6. package/packages/server/src/setup/mnestra-migrations/001_mnestra_tables.sql +2 -2
  7. package/packages/server/src/setup/mnestra-migrations/002_mnestra_search_function.sql +1 -1
  8. package/packages/server/src/setup/mnestra-migrations/009_memory_relationship_metadata.sql +1 -1
  9. package/packages/server/src/setup/mnestra-migrations/011_project_tag_backfill.sql +7 -3
  10. package/packages/server/src/setup/mnestra-migrations/012_project_tag_re_taxonomy.sql +2 -2
  11. package/packages/server/src/setup/mnestra-migrations/014_explicit_grants.sql +3 -3
  12. package/packages/server/src/setup/mnestra-migrations/016_mnestra_doctor_probes.sql +3 -3
  13. package/packages/server/src/setup/mnestra-migrations/017_memory_sessions_session_metadata.sql +5 -5
  14. package/packages/server/src/setup/mnestra-migrations/018_rumen_processed_at.sql +1 -1
  15. package/packages/server/src/setup/mnestra-migrations/019_security_hardening.sql +190 -0
  16. package/packages/server/src/setup/mnestra-migrations/020_migration_tracking.sql +57 -0
  17. package/packages/server/src/setup/mnestra-migrations/021_project_tag_canonicalize_claimguard.sql +175 -0
  18. package/packages/server/src/setup/mnestra-migrations/022_source_agent_backfill.sql +182 -0
  19. 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
- -- (Joshua's petvetbid, Brad's jizzard-brain), match_memories was created by
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 (Joshua's petvetbid, likely Brad's jizzard-brain) ALSO have a
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 petvetbid 2026-04-27 17:25 ET):
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, petvetbid, pet vet bid
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 … petvetbid project") and gets claimed by bucket 1.
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 PVB / petvetbid markers)
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, petvetbid, "pet vet bid"
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 PVB / petvetbid markers)
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 project
21
- -- ref rrzkceirgciiqgeefvbe; fix verified end-to-end on his install
22
- -- before being upstreamed here.
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. Petvetbid +
31
- -- jizzard-brain are unaffected because both have rumen installed,
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 (petvetbid, jizzard-brain) have
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$