@jhizzard/termdeck 1.0.0 → 1.0.2

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.
@@ -0,0 +1,425 @@
1
+ // Sprint 51.5 T1 — schema-introspection audit-upgrade.
2
+ //
3
+ // Brad's 2026-05-02 jizzard-brain report (INSTALLER-PITFALLS.md ledger #13)
4
+ // surfaced Class A — schema drift. The user upgraded npm packages but the
5
+ // database stayed frozen at first-kickstart: graph-inference Edge Function
6
+ // never deployed, vault key never created, Mnestra migrations 009-015 + TD
7
+ // Rumen 003 never applied. Both init wizards correctly apply their bundled
8
+ // migrations on a fresh install, but neither one diffs an existing install
9
+ // against the bundled migration set. After `npm install -g @latest`, the
10
+ // npm packages are current and the database is whatever it was the day the
11
+ // project was first kickstarted.
12
+ //
13
+ // auditUpgrade() runs at the top of `termdeck init --mnestra` and
14
+ // `termdeck init --rumen` re-runs. For each known schema artifact it:
15
+ // 1. Probes for presence via a single information_schema / pg_catalog query.
16
+ // 2. If absent, applies the bundled migration that creates that artifact.
17
+ // 3. Logs every probe + apply result so the wizard can report what changed.
18
+ //
19
+ // `dryRun: true` returns the missing[] list without applying — exposed so
20
+ // `mnestra doctor` (Sprint 51.5 T2) can render the same drift detection
21
+ // without committing changes.
22
+ //
23
+ // What this file IS: a cheap, additive, idempotent diff applier. Every probe
24
+ // is a single SQL statement. Every applied migration is idempotent
25
+ // (`ADD COLUMN IF NOT EXISTS`, `CREATE INDEX IF NOT EXISTS`,
26
+ // `ALTER ... SCHEDULE`, `cron.unschedule + cron.schedule`).
27
+ //
28
+ // What this file is NOT: a migration-tracking-table approach. That's the
29
+ // durable answer (deferred to Sprint 52+) — it self-heals all future drift
30
+ // but requires a backfill pass for existing installs. v1.0.1 takes the
31
+ // cheap path: probe-as-source-of-truth.
32
+ //
33
+ // Out of scope for v1.0.1: Edge Function deploy via Management API, vault
34
+ // secret creation. The bundled `init-rumen.js::deployFunctions` already
35
+ // re-deploys both rumen-tick and graph-inference on every `init --rumen`
36
+ // re-run, so a user who runs the v1.0.1 hotfix instructions
37
+ // (`npm install -g @jhizzard/termdeck@1.0.1 && termdeck init --rumen`)
38
+ // gets the function deploys + vault clone via the existing flow. This
39
+ // module's job is to land the SQL artifacts cheaply, on either re-run path.
40
+
41
+ 'use strict';
42
+
43
+ const path = require('path');
44
+
45
+ const migrations = require('./migrations');
46
+ const { applyTemplating } = require('./migration-templating');
47
+
48
+ // Probe → apply mapping. Order matters: dependencies (e.g., M-013 audit
49
+ // columns) come after the tables they touch. Cron schedule probes go last
50
+ // because they need pg_cron + pg_net which migration 002 takes for granted.
51
+ //
52
+ // Migration 012 (project_tag_re_taxonomy) is intentionally NOT in this set:
53
+ // it is pure DML (UPDATE rows WHERE project='chopin-nashville') with no
54
+ // schema artifact to introspect. Re-applying is safe (idempotent on already-
55
+ // retagged rows) but auto-applying every audit cycle would scan memory_items
56
+ // 8 times for no schema benefit. Migration 012 still ships in the bundled
57
+ // set and is applied by the existing init-mnestra `applyMigrations` loop on
58
+ // any wizard re-run.
59
+ //
60
+ // Migration 011 (project_tag_backfill) is similarly out of scope (DML).
61
+ // Migration 008 (legacy_rag_tables) is opt-in (rag.enabled toggle) and
62
+ // already creates schema with IF NOT EXISTS guards in the fresh-install
63
+ // path — not a drift candidate.
64
+ const PROBES = Object.freeze([
65
+ {
66
+ name: 'memory_relationships.weight',
67
+ kind: 'mnestra',
68
+ migrationFile: '009_memory_relationship_metadata.sql',
69
+ probeSql:
70
+ "select 1 as present from information_schema.columns " +
71
+ "where table_schema = 'public' " +
72
+ " and table_name = 'memory_relationships' " +
73
+ " and column_name = 'weight' limit 1",
74
+ presentWhen: 'rowReturned'
75
+ },
76
+ {
77
+ name: 'memory_recall_graph rpc',
78
+ kind: 'mnestra',
79
+ migrationFile: '010_memory_recall_graph.sql',
80
+ probeSql:
81
+ "select 1 as present from pg_proc " +
82
+ "where proname = 'memory_recall_graph' limit 1",
83
+ presentWhen: 'rowReturned'
84
+ },
85
+ {
86
+ name: 'memory_items.reclassified_by',
87
+ kind: 'mnestra',
88
+ migrationFile: '013_reclassify_uncertain.sql',
89
+ probeSql:
90
+ "select 1 as present from information_schema.columns " +
91
+ "where table_schema = 'public' " +
92
+ " and table_name = 'memory_items' " +
93
+ " and column_name = 'reclassified_by' limit 1",
94
+ presentWhen: 'rowReturned'
95
+ },
96
+ {
97
+ // Brad's 2026-04-28 incident: service_role had no INSERT on memory_items
98
+ // because the project's default-privileges-in-schema-public defaults had
99
+ // been tightened (Supabase auto-grants didn't fire). Migration 014 lays
100
+ // down explicit grants. Re-applying on a project where auto-grants did
101
+ // fire is a no-op.
102
+ name: 'service_role explicit grant on memory_items',
103
+ kind: 'mnestra',
104
+ migrationFile: '014_explicit_grants.sql',
105
+ probeSql:
106
+ "select has_table_privilege('service_role', 'public.memory_items', 'INSERT') as present",
107
+ presentWhen: 'boolColumnTrue'
108
+ },
109
+ {
110
+ name: 'memory_items.source_agent',
111
+ kind: 'mnestra',
112
+ migrationFile: '015_source_agent.sql',
113
+ probeSql:
114
+ "select 1 as present from information_schema.columns " +
115
+ "where table_schema = 'public' " +
116
+ " and table_name = 'memory_items' " +
117
+ " and column_name = 'source_agent' limit 1",
118
+ presentWhen: 'rowReturned'
119
+ },
120
+ {
121
+ // Sprint 51.6 T3 — bundled session-end hook (TermDeck v1.0.2+) writes
122
+ // the rich rag-system column set to memory_sessions; canonical engram
123
+ // mig 001 only ships (id, project, summary, metadata, created_at).
124
+ // Probe for memory_sessions.session_id (the most distinctive of the
125
+ // mig-017 columns) and apply mig 017 if absent. Idempotent on petvetbid
126
+ // where the columns are already present from hand-applied DDL.
127
+ name: 'memory_sessions.session_id',
128
+ kind: 'mnestra',
129
+ migrationFile: '017_memory_sessions_session_metadata.sql',
130
+ probeSql:
131
+ "select 1 as present from information_schema.columns " +
132
+ "where table_schema = 'public' " +
133
+ " and table_name = 'memory_sessions' " +
134
+ " and column_name = 'session_id' limit 1",
135
+ presentWhen: 'rowReturned'
136
+ },
137
+ {
138
+ name: 'rumen-tick cron schedule',
139
+ kind: 'rumen',
140
+ migrationFile: '002_pg_cron_schedule.sql',
141
+ templated: true,
142
+ probeSql:
143
+ "select 1 as present from cron.job where jobname = 'rumen-tick' limit 1",
144
+ presentWhen: 'rowReturned'
145
+ },
146
+ {
147
+ name: 'graph-inference-tick cron schedule',
148
+ kind: 'rumen',
149
+ migrationFile: '003_graph_inference_schedule.sql',
150
+ templated: true,
151
+ probeSql:
152
+ "select 1 as present from cron.job where jobname = 'graph-inference-tick' limit 1",
153
+ presentWhen: 'rowReturned'
154
+ },
155
+ // Sprint 51.6 T3 — Brad's Bug D: function-existence probes (cron schedule
156
+ // checks for jobname presence) are not enough. The deployed Edge Function
157
+ // SOURCE may be stale even when the cron job and function both exist.
158
+ // jizzard-brain on 2026-05-03: deployed rumen-tick was missing the
159
+ // SUPABASE_DB_URL fallback that Sprint 51.5 T1 added; cron probe said
160
+ // "present", source was old. The marker check below detects that drift.
161
+ //
162
+ // probeKind 'functionSource' triggers a Management API fetch instead of a
163
+ // pgClient.query. Bumps to skipped[] (not missing[]) when drift is
164
+ // detected — the corresponding "apply" is a redeploy via init-rumen's
165
+ // deployFunctions, not an SQL migration. The wizard shows skipped[]
166
+ // entries with their probeError, prompting the user to re-run init.
167
+ //
168
+ // Maintenance: bump `requiredMarker` whenever a new feature is added to
169
+ // the bundled function source that is meaningful enough to gate redeploys
170
+ // on. The marker should be a string unique to the post-change version.
171
+ {
172
+ name: 'rumen-tick deployed source has SUPABASE_DB_URL fallback',
173
+ kind: 'rumen',
174
+ probeKind: 'functionSource',
175
+ functionSlug: 'rumen-tick',
176
+ requiredMarker: "Deno.env.get('SUPABASE_DB_URL')",
177
+ presentWhen: 'sourceMatch'
178
+ },
179
+ {
180
+ name: 'graph-inference deployed source has SUPABASE_DB_URL fallback',
181
+ kind: 'rumen',
182
+ probeKind: 'functionSource',
183
+ functionSlug: 'graph-inference',
184
+ requiredMarker: "Deno.env.get('SUPABASE_DB_URL')",
185
+ presentWhen: 'sourceMatch'
186
+ }
187
+ ]);
188
+
189
+ // Find the bundled migration file for a probe target. Returns the absolute
190
+ // path or null. `mnestra` looks under bundled mnestra-migrations; `rumen`
191
+ // looks under bundled rumen/migrations. Both kinds prefer the bundled copy
192
+ // (matches the listMnestraMigrations / listRumenMigrations convention from
193
+ // v0.6.8 — bundled FIRST, then the @jhizzard/<pkg> node_modules fallback).
194
+ function resolveMigrationFile(target, files) {
195
+ const wanted = target.migrationFile;
196
+ return files.find((f) => path.basename(f) === wanted) || null;
197
+ }
198
+
199
+ // Sprint 51.6 T3 — Bug D: Edge Function source-drift detection.
200
+ //
201
+ // Fetches the deployed Edge Function body from Supabase Management API and
202
+ // looks for a marker string the bundled source contains. Returns absent
203
+ // (with probeError) when the marker is missing — meaning the deployed
204
+ // function is older than the bundle and should be redeployed.
205
+ //
206
+ // Requires:
207
+ // - projectRef passed through from auditUpgrade()
208
+ // - SUPABASE_ACCESS_TOKEN in env (a personal access token, format `sbp_*`)
209
+ // Fail-soft when either is missing — recorded as probeError, treated as
210
+ // absent. The audit caller decides whether to surface to the user.
211
+ async function probeFunctionSource(target, { projectRef, fetchImpl }) {
212
+ const fn = fetchImpl || (typeof globalThis !== 'undefined' ? globalThis.fetch : undefined);
213
+ if (typeof fn !== 'function') {
214
+ return { present: false, probeError: 'no fetch implementation available' };
215
+ }
216
+ if (!projectRef) {
217
+ return { present: false, probeError: 'projectRef required for functionSource probe' };
218
+ }
219
+ const accessToken = process.env.SUPABASE_ACCESS_TOKEN;
220
+ if (!accessToken) {
221
+ return {
222
+ present: false,
223
+ probeError: 'SUPABASE_ACCESS_TOKEN not set; cannot fetch deployed function body. Set the personal access token (`supabase login` writes it to ~/.supabase/access-token) to enable function-source drift detection.',
224
+ };
225
+ }
226
+ let res;
227
+ try {
228
+ res = await fn(
229
+ `https://api.supabase.com/v1/projects/${projectRef}/functions/${target.functionSlug}/body`,
230
+ { headers: { 'Authorization': `Bearer ${accessToken}` } }
231
+ );
232
+ } catch (err) {
233
+ return { present: false, probeError: `Management API fetch failed: ${err.message}` };
234
+ }
235
+ if (!res.ok) {
236
+ return {
237
+ present: false,
238
+ probeError: `Management API returned HTTP ${res.status} for ${target.functionSlug}/body — function may not be deployed yet, or access token lacks permission.`
239
+ };
240
+ }
241
+ let body;
242
+ try { body = await res.text(); }
243
+ catch (err) { return { present: false, probeError: `body decode failed: ${err.message}` }; }
244
+
245
+ if (target.requiredMarker && body.includes(target.requiredMarker)) {
246
+ return { present: true };
247
+ }
248
+ return {
249
+ present: false,
250
+ probeError: `deployed ${target.functionSlug} source missing marker (${JSON.stringify(target.requiredMarker)}) — re-run \`termdeck init --rumen\` to redeploy from bundled source.`,
251
+ };
252
+ }
253
+
254
+ // Run a probe and decide present/absent based on the probe's contract.
255
+ // Sprint 51.6 T3: dispatches by target.probeKind. Default is the legacy
256
+ // pgClient.query path; 'functionSource' calls probeFunctionSource (HTTP).
257
+ async function probeOne(pgClient, target, ctx = {}) {
258
+ if (target.probeKind === 'functionSource') {
259
+ return probeFunctionSource(target, ctx);
260
+ }
261
+ let result;
262
+ try {
263
+ result = await pgClient.query(target.probeSql);
264
+ } catch (err) {
265
+ // A probe failure (e.g., schema doesn't exist yet — `cron.job` on a
266
+ // project without pg_cron) means the artifact is absent. Record the
267
+ // raw error so a caller can distinguish "absent because never installed"
268
+ // from "absent because we can't even check." Either way the right
269
+ // response is to attempt apply — which will surface the real error
270
+ // (e.g., "extension pg_cron is not installed") with full context.
271
+ return { present: false, probeError: err.message };
272
+ }
273
+ const rows = (result && result.rows) || [];
274
+ if (target.presentWhen === 'rowReturned') {
275
+ return { present: rows.length > 0 };
276
+ }
277
+ if (target.presentWhen === 'boolColumnTrue') {
278
+ return { present: Boolean(rows[0] && rows[0].present === true) };
279
+ }
280
+ // Defensive default: any returned row counts as present.
281
+ return { present: rows.length > 0 };
282
+ }
283
+
284
+ // Apply a single migration file. Templated migrations route through
285
+ // applyTemplating() so the cron schedule body never sees the raw
286
+ // `<project-ref>` placeholder. Brad 2026-05-03 takeaway #5 (bonus): the
287
+ // fresh-install path at init-rumen.js:472-505 already does this; the
288
+ // audit-upgrade path MUST mirror it. Tests in audit-upgrade.test.js guard
289
+ // against future bypass.
290
+ async function applyOne(pgClient, target, files, { projectRef, readFileImpl }) {
291
+ const file = resolveMigrationFile(target, files);
292
+ if (!file) {
293
+ throw new Error(
294
+ `audit-upgrade: bundled migration file not found for ${target.name} ` +
295
+ `(expected ${target.migrationFile}). The bundled migration set may be ` +
296
+ `out of sync — re-publish the package or run scripts/sync-rumen-functions.sh.`
297
+ );
298
+ }
299
+ const raw = readFileImpl(file);
300
+ const sql = target.templated
301
+ ? applyTemplating(raw, { projectRef })
302
+ : raw;
303
+ await pgClient.query(sql);
304
+ return { file: path.basename(file) };
305
+ }
306
+
307
+ // Public API.
308
+ //
309
+ // Inputs:
310
+ // pgClient — open node-postgres Client (caller owns the lifecycle).
311
+ // projectRef — required when any templated migration is in the probe set
312
+ // (i.e., the rumen cron schedules). The applyTemplating
313
+ // helper will throw if it sees a `<project-ref>` placeholder
314
+ // and projectRef is missing — surfaced via errors[].
315
+ // dryRun — when true, probes only; skips apply. applied stays empty.
316
+ // probes — optional override for the probe set (test injection point).
317
+ // Defaults to PROBES.
318
+ // _migrations — optional override for the migrations module (test
319
+ // injection). Lets tests stub listMnestraMigrations /
320
+ // listRumenMigrations / readFile.
321
+ //
322
+ // Returns:
323
+ // {
324
+ // probed: string[] — every target name we tried to probe
325
+ // present: string[] — targets whose probe came back present
326
+ // missing: string[] — targets whose probe came back absent
327
+ // applied: string[] — targets the audit applied this run
328
+ // (empty when dryRun=true)
329
+ // skipped: string[] — targets we couldn't apply (e.g., missing
330
+ // projectRef on a templated migration)
331
+ // errors: Array<{ name, error }> — apply or probe errors (probe errors
332
+ // only surface here when subsequent
333
+ // apply ALSO fails)
334
+ // }
335
+ //
336
+ // Idempotent: a second run reports `applied=[]` because every probe will
337
+ // come back present. All shipped migrations are themselves idempotent
338
+ // (ADD COLUMN IF NOT EXISTS, CREATE INDEX IF NOT EXISTS,
339
+ // cron.unschedule + cron.schedule, GRANT … TO service_role).
340
+ async function auditUpgrade({
341
+ pgClient,
342
+ projectRef,
343
+ dryRun = false,
344
+ probes,
345
+ _migrations,
346
+ _fetch
347
+ } = {}) {
348
+ if (!pgClient || typeof pgClient.query !== 'function') {
349
+ throw new Error('auditUpgrade: pgClient with .query() is required');
350
+ }
351
+ const targets = probes || PROBES;
352
+ const mig = _migrations || migrations;
353
+
354
+ // Resolve once: the bundled migration sets stay constant for the duration
355
+ // of a single audit run.
356
+ const mnestraFiles = mig.listMnestraMigrations();
357
+ const rumenFiles = mig.listRumenMigrations();
358
+
359
+ const probed = [];
360
+ const present = [];
361
+ const missing = [];
362
+ const applied = [];
363
+ const skipped = [];
364
+ const errors = [];
365
+
366
+ for (const target of targets) {
367
+ probed.push(target.name);
368
+ const probeResult = await probeOne(pgClient, target, { projectRef, fetchImpl: _fetch });
369
+ if (probeResult.present) {
370
+ present.push(target.name);
371
+ continue;
372
+ }
373
+
374
+ // Sprint 51.6 T3 — Bug D: functionSource probes go to skipped[] (not
375
+ // missing[]). The corresponding fix is a re-run of `init --rumen` which
376
+ // calls deployFunctions; audit-upgrade does not auto-redeploy.
377
+ if (target.probeKind === 'functionSource') {
378
+ skipped.push({
379
+ name: target.name,
380
+ reason: probeResult.probeError || 'function source drift — redeploy via init --rumen',
381
+ });
382
+ continue;
383
+ }
384
+
385
+ missing.push(target.name);
386
+
387
+ if (dryRun) continue;
388
+
389
+ const files = target.kind === 'rumen' ? rumenFiles : mnestraFiles;
390
+
391
+ try {
392
+ await applyOne(pgClient, target, files, {
393
+ projectRef,
394
+ readFileImpl: mig.readFile
395
+ });
396
+ applied.push(target.name);
397
+ } catch (err) {
398
+ // Surface but don't abort. One missing artifact failing to apply (e.g.,
399
+ // pg_cron extension not enabled) shouldn't block the rest of the audit
400
+ // from running. The wizard will report the whole audit summary at the
401
+ // end so the user can address each failure individually.
402
+ errors.push({
403
+ name: target.name,
404
+ error: err && err.message ? err.message : String(err)
405
+ });
406
+ }
407
+ }
408
+
409
+ // skipped[] reserved for v1.0.2: targets the audit deliberately doesn't
410
+ // attempt (e.g., when projectRef is missing for a templated migration we
411
+ // currently let applyTemplating throw → errors[]; future versions may
412
+ // pre-skip those into skipped[]).
413
+ return { probed, present, missing, applied, skipped, errors };
414
+ }
415
+
416
+ module.exports = {
417
+ auditUpgrade,
418
+ PROBES,
419
+ // Test surface — kept exported so audit-upgrade.test.js can pin probe
420
+ // selection / apply pathway behavior without needing a live pg client.
421
+ _probeOne: probeOne,
422
+ _probeFunctionSource: probeFunctionSource,
423
+ _applyOne: applyOne,
424
+ _resolveMigrationFile: resolveMigrationFile
425
+ };
@@ -13,5 +13,6 @@ module.exports = {
13
13
  migrationTemplating: require('./migration-templating'),
14
14
  pgRunner: require('./pg-runner'),
15
15
  migrationRunner: require('./migration-runner'),
16
- preconditions: require('./preconditions')
16
+ preconditions: require('./preconditions'),
17
+ auditUpgrade: require('./audit-upgrade')
17
18
  };
@@ -0,0 +1,39 @@
1
+ -- 013_reclassify_uncertain.sql
2
+ --
3
+ -- Sprint 41 (T4) — Audit-trail columns for the LLM-classification pass that
4
+ -- finishes the chopin-nashville taxonomy cleanup.
5
+ --
6
+ -- Background:
7
+ -- Sprint 41 T2's deterministic re-tag (`012_project_tag_re_taxonomy.sql`)
8
+ -- handles every chopin-nashville row whose content has a clear keyword or
9
+ -- path signal. The residue — rows with no clear signal — gets classified
10
+ -- by `scripts/reclassify-chopin-nashville.js` which calls Haiku 4.5 in
11
+ -- batches of 20 and writes back per-row tag decisions.
12
+ --
13
+ -- Some of those LLM decisions will be "this row really IS chopin-nashville
14
+ -- competition work — leave the tag." Without an audit stamp the script
15
+ -- can't distinguish "row the LLM voted to keep" from "row the LLM hasn't
16
+ -- seen yet" — every re-run would re-ask Haiku about the same rows
17
+ -- indefinitely. The stamp also gives a one-line audit trail
18
+ -- (`SELECT count(*) FROM memory_items WHERE reclassified_by = '...'`).
19
+ --
20
+ -- Idempotent: safe to re-run. ADD COLUMN IF NOT EXISTS — no-op if already
21
+ -- applied.
22
+ --
23
+ -- Constraints:
24
+ -- Both columns nullable. Only rows the script touches get stamped; every
25
+ -- other row stays untouched. There is no foreign key, no NOT NULL, no
26
+ -- default — these are pure audit metadata.
27
+
28
+ alter table memory_items
29
+ add column if not exists reclassified_by text,
30
+ add column if not exists reclassified_at timestamptz;
31
+
32
+ -- Lightweight partial index — useful for `count(*) WHERE reclassified_by = ...`
33
+ -- audit queries and for the script's own idempotency filter. Keeps the index
34
+ -- small (only stamped rows are indexed) so it doesn't cost anything on the
35
+ -- vast majority of memory_items rows that stay untouched.
36
+
37
+ create index if not exists memory_items_reclassified_by_idx
38
+ on memory_items(reclassified_by)
39
+ where reclassified_by is not null;
@@ -0,0 +1,46 @@
1
+ -- Mnestra v0.3.2 — explicit GRANTs to make installs deterministic
2
+ --
3
+ -- Prior migrations relied on Supabase's auto-grant default, which
4
+ -- auto-grants public-schema privileges to service_role / authenticated /
5
+ -- anon when (a) the creating role is `postgres` AND (b) the project's
6
+ -- default privileges in schema public haven't been tightened. On any
7
+ -- Supabase project where one of those preconditions failed, every
8
+ -- Mnestra install landed in the same broken state:
9
+ --
10
+ -- memory_remember(...) → "Memory skipped: ..." (silent — see remember.ts)
11
+ -- memory_status → Total active memories: 0
12
+ -- memory_recall(...) → "Search error: permission denied for table memory_items"
13
+ --
14
+ -- Root cause: `service_role` had no SELECT/INSERT/UPDATE/DELETE on
15
+ -- memory_items, memory_sessions, memory_relationships, and no EXECUTE
16
+ -- on match_memories / memory_hybrid_search / expand_memory_neighborhood.
17
+ -- PostgREST checks table-level privileges before evaluating RLS, so
18
+ -- service_role's bypassrls attribute does not help.
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.
23
+ --
24
+ -- This migration is idempotent and safe on greenfield projects where
25
+ -- the auto-grant default already fired (the GRANTs become no-ops).
26
+
27
+ -- ── Tables: service_role is Mnestra's only direct connection role.
28
+
29
+ grant select, insert, update, delete on all tables in schema public
30
+ to service_role;
31
+
32
+ -- ── Functions / RPCs: convention from migrations 006 and 010 is to
33
+ -- grant execute to all three Supabase roles. Apply schema-wide so
34
+ -- future RPCs inherit without another migration.
35
+
36
+ grant execute on all functions in schema public
37
+ to service_role, authenticated, anon;
38
+
39
+ -- ── Default privileges: any future tables/functions created in
40
+ -- schema public automatically inherit the same grants.
41
+
42
+ alter default privileges in schema public
43
+ grant select, insert, update, delete on tables to service_role;
44
+
45
+ alter default privileges in schema public
46
+ grant execute on functions to service_role, authenticated, anon;
@@ -0,0 +1,51 @@
1
+ -- Mnestra v0.4.0 — source_agent provenance column on memory_items
2
+ --
3
+ -- Sprint 50 T2 (TermDeck). Adds an LLM-provenance tag to every memory row
4
+ -- so future memory_recall callers can filter or trust-weight by the agent
5
+ -- that produced the row (Claude / Codex / Gemini / Grok / orchestrator).
6
+ --
7
+ -- Why now:
8
+ -- Sprint 49 (mixed-agent dogfood, 2026-05-02) surfaced a trust-fundamental
9
+ -- gap. Each lane's panel produced real work; only Claude's hook wrote to
10
+ -- Mnestra. Sprint 50 closes both halves of that gap — T1 fires the hook
11
+ -- for every adapter at panel close (write-side); T2 (this migration) adds
12
+ -- the read-side ability to filter by source. Without this column,
13
+ -- memory_recall returns a careful Claude observation alongside (e.g.) a
14
+ -- Gemini-produced timestamp claim, with no way to tell them apart at the
15
+ -- recall consumer. See docs/MULTI-AGENT-MEMORY-ARCHITECTURE.md
16
+ -- § Deliverable 2 in the TermDeck repo for the full design.
17
+ --
18
+ -- Backwards compatibility:
19
+ -- Historical rows stay NULL (no destructive default backfill on archived
20
+ -- data). The recall filter treats NULL as "unknown agent" — rows with
21
+ -- NULL source_agent are excluded from a filtered recall and included in
22
+ -- an unfiltered one.
23
+ --
24
+ -- Exception: pre-Sprint-50 session_summary rows came exclusively from
25
+ -- Claude Code's SessionEnd hook (only Claude shipped a hook system before
26
+ -- Sprint 50 T1 added per-agent triggers). Backfill those to 'claude' so
27
+ -- they remain reachable via source_agents=['claude']. Other source_types
28
+ -- (fact / decision / preference / bug_fix / architecture / code_context)
29
+ -- came from a mix of MCP tools and the rag-system extractor — no clean
30
+ -- single-agent attribution exists for them, so they stay NULL.
31
+ --
32
+ -- Idempotent: ADD COLUMN IF NOT EXISTS, CREATE INDEX IF NOT EXISTS,
33
+ -- and the backfill UPDATE skips rows already populated.
34
+
35
+ alter table memory_items
36
+ add column if not exists source_agent text;
37
+
38
+ create index if not exists idx_memory_items_source_agent
39
+ on memory_items (source_agent)
40
+ where source_agent is not null;
41
+
42
+ comment on column memory_items.source_agent is
43
+ 'Agent that produced this memory: claude|codex|gemini|grok|orchestrator|NULL (historical or unknown). Populated by the SessionEnd hook from Sprint 50 onward; NULL for pre-Sprint-50 rows except session_summary which were always Claude (backfilled).';
44
+
45
+ -- Backfill historical session_summary rows. These came from Claude Code's
46
+ -- SessionEnd hook (only Claude shipped a hook system before Sprint 50 T1).
47
+ -- Idempotent — re-running this UPDATE on already-tagged rows is a no-op.
48
+ update memory_items
49
+ set source_agent = 'claude'
50
+ where source_type = 'session_summary'
51
+ and source_agent is null;