@jhizzard/termdeck 1.0.0 → 1.0.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 +40 -1
- package/packages/cli/src/init-rumen.js +337 -29
- package/packages/server/src/setup/audit-upgrade.js +302 -0
- package/packages/server/src/setup/index.js +2 -1
- package/packages/server/src/setup/mnestra-migrations/013_reclassify_uncertain.sql +39 -0
- package/packages/server/src/setup/mnestra-migrations/014_explicit_grants.sql +46 -0
- package/packages/server/src/setup/mnestra-migrations/015_source_agent.sql +51 -0
- package/packages/server/src/setup/mnestra-migrations/016_mnestra_doctor_probes.sql +117 -0
- package/packages/server/src/setup/preconditions.js +36 -4
- package/packages/server/src/setup/rumen/functions/graph-inference/index.ts +6 -2
- package/packages/server/src/setup/rumen/functions/rumen-tick/index.ts +6 -2
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@jhizzard/termdeck",
|
|
3
|
-
"version": "1.0.
|
|
3
|
+
"version": "1.0.1",
|
|
4
4
|
"description": "Browser-based terminal multiplexer with metadata overlays, panel flashback memory recall, and AI-aware session management",
|
|
5
5
|
"bin": {
|
|
6
6
|
"termdeck": "./packages/cli/src/index.js"
|
|
@@ -45,7 +45,8 @@ const {
|
|
|
45
45
|
supabaseUrl: urlHelper,
|
|
46
46
|
migrations,
|
|
47
47
|
pgRunner,
|
|
48
|
-
preconditions
|
|
48
|
+
preconditions,
|
|
49
|
+
auditUpgrade: auditUpgradeMod
|
|
49
50
|
} = require(SETUP_DIR);
|
|
50
51
|
|
|
51
52
|
const HELP = [
|
|
@@ -339,6 +340,43 @@ async function applyMigrations(client, dryRun) {
|
|
|
339
340
|
}
|
|
340
341
|
}
|
|
341
342
|
|
|
343
|
+
// Sprint 51.5 T1 — schema-introspection audit-upgrade. Runs AFTER the
|
|
344
|
+
// fresh-install applyMigrations() loop completes, but reports separately.
|
|
345
|
+
// On a Sprint-37-era project that pre-dated several mnestra migrations,
|
|
346
|
+
// applyMigrations now has the full bundled set (Sprint 51.5 synced 013-015
|
|
347
|
+
// from canonical engram), so this audit is mostly a belt-and-suspenders
|
|
348
|
+
// confirmation. It still surfaces drift if, e.g., the user manually
|
|
349
|
+
// dropped a column post-install. Mnestra-kind only — rumen cron probes
|
|
350
|
+
// reference vault secrets the user hasn't set up yet at this point. Run
|
|
351
|
+
// init-rumen for the rumen-side audit.
|
|
352
|
+
async function runMnestraAudit(client, projectRef, dryRun) {
|
|
353
|
+
step('Audit-upgrade: probing for missing mnestra schema artifacts...');
|
|
354
|
+
if (dryRun) { ok('(dry-run)'); return; }
|
|
355
|
+
const probes = auditUpgradeMod.PROBES.filter((p) => p.kind === 'mnestra');
|
|
356
|
+
let result;
|
|
357
|
+
try {
|
|
358
|
+
result = await auditUpgradeMod.auditUpgrade({
|
|
359
|
+
pgClient: client,
|
|
360
|
+
projectRef,
|
|
361
|
+
probes
|
|
362
|
+
});
|
|
363
|
+
} catch (err) {
|
|
364
|
+
fail(err.message);
|
|
365
|
+
return;
|
|
366
|
+
}
|
|
367
|
+
if (result.applied.length === 0 && result.errors.length === 0) {
|
|
368
|
+
ok(`(install up to date — ${result.probed.length} probes all present)`);
|
|
369
|
+
return;
|
|
370
|
+
}
|
|
371
|
+
ok(`(probed ${result.probed.length}, applied ${result.applied.length})`);
|
|
372
|
+
for (const name of result.applied) {
|
|
373
|
+
process.stdout.write(` ✓ applied ${name}\n`);
|
|
374
|
+
}
|
|
375
|
+
for (const e of result.errors) {
|
|
376
|
+
process.stdout.write(` ! ${e.name}: ${e.error}\n`);
|
|
377
|
+
}
|
|
378
|
+
}
|
|
379
|
+
|
|
342
380
|
async function checkExistingStore(client) {
|
|
343
381
|
step('Checking for existing memory_items table...');
|
|
344
382
|
try {
|
|
@@ -538,6 +576,7 @@ async function main(argv) {
|
|
|
538
576
|
try {
|
|
539
577
|
await checkExistingStore(client);
|
|
540
578
|
await applyMigrations(client, false);
|
|
579
|
+
await runMnestraAudit(client, inputs.projectUrl.projectRef, false);
|
|
541
580
|
writeYamlConfig(false);
|
|
542
581
|
// v0.6.9: post-write outcome verification. Confirms each migration's
|
|
543
582
|
// expected schema bits actually landed — including memory_items.
|
|
@@ -20,7 +20,9 @@
|
|
|
20
20
|
// 4. Apply rumen migration 001 via pg
|
|
21
21
|
// 5. supabase functions deploy rumen-tick AND graph-inference (Sprint 43 T3)
|
|
22
22
|
// from a single staging dir with multi-function supabase/config.toml
|
|
23
|
-
// 6. supabase secrets set DATABASE_URL
|
|
23
|
+
// 6. supabase secrets set, ONE call per key (DATABASE_URL, ANTHROPIC_API_KEY,
|
|
24
|
+
// [OPENAI_API_KEY], [GRAPH_LLM_CLASSIFY=1]) — per-secret to avoid the
|
|
25
|
+
// v2.90.0 multi-arg drop documented in INSTALLER-PITFALLS.md Class J.
|
|
24
26
|
// 7. Test rumen-tick with a manual POST (graph-inference is cron-only)
|
|
25
27
|
// 8. Apply pg_cron schedule migrations 002 (rumen-tick) AND 003 (graph-inference)
|
|
26
28
|
// with project ref substituted
|
|
@@ -38,7 +40,8 @@ const {
|
|
|
38
40
|
migrations,
|
|
39
41
|
migrationTemplating,
|
|
40
42
|
pgRunner,
|
|
41
|
-
preconditions
|
|
43
|
+
preconditions,
|
|
44
|
+
auditUpgrade: auditUpgradeMod
|
|
42
45
|
} = require(SETUP_DIR);
|
|
43
46
|
|
|
44
47
|
const {
|
|
@@ -303,6 +306,57 @@ async function applyRumenTables(secrets, dryRun) {
|
|
|
303
306
|
}
|
|
304
307
|
}
|
|
305
308
|
|
|
309
|
+
// Sprint 51.5 T1 — schema-introspection audit-upgrade. Probes for missing
|
|
310
|
+
// mnestra schema artifacts AND missing rumen cron schedules. Runs before
|
|
311
|
+
// the existing init-rumen flow so the user sees what's about to be applied
|
|
312
|
+
// up front. Idempotent: a re-run on an up-to-date project reports
|
|
313
|
+
// "install up to date" and applies nothing.
|
|
314
|
+
//
|
|
315
|
+
// Brad's 2026-05-02 jizzard-brain report (INSTALLER-PITFALLS.md ledger #13)
|
|
316
|
+
// is the originating motivation: he upgraded npm packages but his database
|
|
317
|
+
// stayed frozen at first-kickstart because no installer code path diffed an
|
|
318
|
+
// existing install against the bundled migration set. After v1.0.1 ships,
|
|
319
|
+
// `npm install -g @jhizzard/termdeck@1.0.1 && termdeck init --rumen` will
|
|
320
|
+
// surface and apply every missing artifact in one pass.
|
|
321
|
+
//
|
|
322
|
+
// Errors are surfaced inline but do NOT abort the wizard — a single
|
|
323
|
+
// failing probe (e.g., pg_cron not enabled when probing for cron.job)
|
|
324
|
+
// shouldn't block the rest of the audit or the rest of the init flow.
|
|
325
|
+
async function runRumenAudit(projectRef, secrets, dryRun) {
|
|
326
|
+
step('Audit-upgrade: probing for missing schema + cron artifacts...');
|
|
327
|
+
if (dryRun) { ok('(dry-run)'); return true; }
|
|
328
|
+
let client;
|
|
329
|
+
try {
|
|
330
|
+
client = await pgRunner.connect(secrets.DATABASE_URL);
|
|
331
|
+
} catch (err) {
|
|
332
|
+
fail(err.message);
|
|
333
|
+
return false;
|
|
334
|
+
}
|
|
335
|
+
try {
|
|
336
|
+
const result = await auditUpgradeMod.auditUpgrade({
|
|
337
|
+
pgClient: client,
|
|
338
|
+
projectRef
|
|
339
|
+
});
|
|
340
|
+
if (result.applied.length === 0 && result.errors.length === 0) {
|
|
341
|
+
ok(`(install up to date — ${result.probed.length} probes all present)`);
|
|
342
|
+
return true;
|
|
343
|
+
}
|
|
344
|
+
ok(`(probed ${result.probed.length}, applied ${result.applied.length})`);
|
|
345
|
+
for (const name of result.applied) {
|
|
346
|
+
process.stdout.write(` ✓ applied ${name}\n`);
|
|
347
|
+
}
|
|
348
|
+
for (const e of result.errors) {
|
|
349
|
+
process.stdout.write(` ! ${e.name}: ${e.error}\n`);
|
|
350
|
+
}
|
|
351
|
+
return true;
|
|
352
|
+
} catch (err) {
|
|
353
|
+
fail(err.message);
|
|
354
|
+
return true; // non-blocking
|
|
355
|
+
} finally {
|
|
356
|
+
try { await client.end(); } catch (_err) { /* ignore */ }
|
|
357
|
+
}
|
|
358
|
+
}
|
|
359
|
+
|
|
306
360
|
// Sprint 43 T3: rumen-tick is the only function with a `__RUMEN_VERSION__`
|
|
307
361
|
// placeholder (its `npm:@jhizzard/rumen@<ver>` import is rewritten at deploy
|
|
308
362
|
// time). graph-inference pins its own deps (`npm:postgres@3.4.4`) and is
|
|
@@ -400,29 +454,238 @@ ${fnBlocks}`;
|
|
|
400
454
|
return stage;
|
|
401
455
|
}
|
|
402
456
|
|
|
403
|
-
|
|
457
|
+
// Sprint 51.5 T3: per-secret CLI loop. Pre-Sprint-51.5 this issued a single
|
|
458
|
+
// `supabase secrets set KEY1=VAL1 KEY2=VAL2 ...` call with all keys as
|
|
459
|
+
// positional args. Brad's 2026-05-03 4-project install pass observed
|
|
460
|
+
// supabase CLI v2.90.0 silently dropping some args from a multi-arg call —
|
|
461
|
+
// even materializing stray entries from misparsed argv (his email landed as
|
|
462
|
+
// a secret name). Documented as INSTALLER-PITFALLS.md Class J. The
|
|
463
|
+
// deterministic fix is one CLI invocation per secret, exit code checked per
|
|
464
|
+
// call, stderr surfaced with the failing key name. Order is preserved for
|
|
465
|
+
// log readability (DATABASE_URL → ANTHROPIC_API_KEY → optional → optional).
|
|
466
|
+
function setFunctionSecrets(secrets, dryRun, opts = {}) {
|
|
467
|
+
const orderedKeys = ['DATABASE_URL', 'ANTHROPIC_API_KEY'];
|
|
468
|
+
if (secrets.OPENAI_API_KEY) orderedKeys.push('OPENAI_API_KEY');
|
|
469
|
+
if (secrets.GRAPH_LLM_CLASSIFY === '1') orderedKeys.push('GRAPH_LLM_CLASSIFY');
|
|
470
|
+
|
|
404
471
|
const haveOpenAI = Boolean(secrets.OPENAI_API_KEY);
|
|
405
|
-
|
|
406
|
-
? 'DATABASE_URL, ANTHROPIC_API_KEY, OPENAI_API_KEY'
|
|
407
|
-
: 'DATABASE_URL, ANTHROPIC_API_KEY';
|
|
408
|
-
step(`Setting function secrets (${label})...`);
|
|
472
|
+
step(`Setting function secrets per-call (${orderedKeys.join(', ')})...`);
|
|
409
473
|
if (dryRun) { ok('(dry-run)'); return true; }
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
474
|
+
|
|
475
|
+
// Test surface: opts.runner is an optional injected function that mimics
|
|
476
|
+
// runShellCaptured((bin, args) => { ok, code, stdout, stderr }). Production
|
|
477
|
+
// path passes nothing and shells out to the real supabase CLI.
|
|
478
|
+
const runner = (typeof opts.runner === 'function') ? opts.runner : runShellCaptured;
|
|
479
|
+
|
|
480
|
+
for (const key of orderedKeys) {
|
|
481
|
+
const value = secrets[key];
|
|
482
|
+
if (value === undefined || value === null || value === '') {
|
|
483
|
+
fail(`secret ${key} missing from in-memory secrets map — wizard wiring bug`);
|
|
484
|
+
return false;
|
|
485
|
+
}
|
|
486
|
+
const r = runner('supabase', ['secrets', 'set', `${key}=${value}`]);
|
|
487
|
+
if (!r || !r.ok) {
|
|
488
|
+
fail(`supabase secrets set ${key} failed (exit ${r ? r.code : 'no-result'})`);
|
|
489
|
+
if (r && r.stderr) process.stderr.write(r.stderr + '\n');
|
|
490
|
+
return false;
|
|
491
|
+
}
|
|
492
|
+
}
|
|
493
|
+
const llmTag = secrets.GRAPH_LLM_CLASSIFY === '1' ? ', graph LLM classify on' : '';
|
|
494
|
+
ok(`${haveOpenAI ? '(hybrid mode' : '(keyword-only mode — OPENAI_API_KEY not set'}${llmTag})`);
|
|
495
|
+
return true;
|
|
496
|
+
}
|
|
497
|
+
|
|
498
|
+
// Sprint 51.5 T3: Build a Supabase SQL-Editor deeplink that pre-fills the
|
|
499
|
+
// vault.create_secret() call for one secret. Used as the fallback when
|
|
500
|
+
// auto-apply via pgRunner can't write to vault.secrets (permission denied,
|
|
501
|
+
// missing extension, etc.) AND as the manual-fix surface in any wizard text
|
|
502
|
+
// that previously instructed users to "click Vault in the dashboard" — the
|
|
503
|
+
// Vault dashboard panel was quietly removed/relocated in current Supabase
|
|
504
|
+
// UIs (Brad 2026-05-03 takeaway #2; INSTALLER-PITFALLS.md Class B).
|
|
505
|
+
//
|
|
506
|
+
// vault.create_secret signature is `(secret text, name text [, description text])`
|
|
507
|
+
// — value-then-name. Both arguments are escaped as Postgres string literals
|
|
508
|
+
// (single-quote doubling). The full URL is roughly:
|
|
509
|
+
// https://supabase.com/dashboard/project/<ref>/sql/new?content=<encoded SQL>
|
|
510
|
+
// Click → SQL Editor opens with the call pre-filled → user clicks Run.
|
|
511
|
+
function vaultSqlEditorUrl(projectRef, secretName, secretValue) {
|
|
512
|
+
if (!projectRef || typeof projectRef !== 'string') {
|
|
513
|
+
throw new Error('vaultSqlEditorUrl: projectRef is required');
|
|
514
|
+
}
|
|
515
|
+
const value = String(secretValue == null ? '' : secretValue).replace(/'/g, "''");
|
|
516
|
+
const name = String(secretName == null ? '' : secretName).replace(/'/g, "''");
|
|
517
|
+
const sql = `select vault.create_secret('${value}', '${name}');`;
|
|
518
|
+
return `https://supabase.com/dashboard/project/${projectRef}/sql/new?content=${encodeURIComponent(sql)}`;
|
|
519
|
+
}
|
|
520
|
+
|
|
521
|
+
// Sprint 51.5 T3: Ensure the two Vault secrets the cron schedules need are
|
|
522
|
+
// present, auto-creating them via the user's pg connection when possible.
|
|
523
|
+
//
|
|
524
|
+
// Required:
|
|
525
|
+
// - rumen_service_role_key (used by 002_pg_cron_schedule.sql)
|
|
526
|
+
// - graph_inference_service_role_key (used by 003_graph_inference_schedule.sql)
|
|
527
|
+
//
|
|
528
|
+
// Both keys hold the same value (`secrets.SUPABASE_SERVICE_ROLE_KEY`). Brad's
|
|
529
|
+
// 2026-05-02 recovery on jizzard-brain literally cloned rumen → graph_inference
|
|
530
|
+
// in vault.
|
|
531
|
+
//
|
|
532
|
+
// Strategy:
|
|
533
|
+
// 1. Open a pg connection to DATABASE_URL (same path as applyRumenTables).
|
|
534
|
+
// 2. Probe vault.secrets for both names. (Reads vault.secrets, NOT
|
|
535
|
+
// vault.decrypted_secrets — we don't need the decrypted value, just
|
|
536
|
+
// presence.)
|
|
537
|
+
// 3. For any missing name, call `vault.create_secret($value, $name)`.
|
|
538
|
+
// 4. On per-secret failure (permission denied, etc.), surface a SQL-Editor
|
|
539
|
+
// deeplink the user can click; do not fail the wizard hard — the
|
|
540
|
+
// preconditions audit will catch a still-missing rumen_service_role_key
|
|
541
|
+
// with its own hint, and the user has the actionable URL in front of
|
|
542
|
+
// them either way.
|
|
543
|
+
//
|
|
544
|
+
// Returns `{ ok, created: [...], deeplinks: [{ name, url, error }] }`.
|
|
545
|
+
async function ensureVaultSecrets({ projectRef, secrets, dryRun, _pgClient }) {
|
|
546
|
+
const required = [
|
|
547
|
+
{ name: 'rumen_service_role_key', value: secrets.SUPABASE_SERVICE_ROLE_KEY },
|
|
548
|
+
{ name: 'graph_inference_service_role_key', value: secrets.SUPABASE_SERVICE_ROLE_KEY }
|
|
414
549
|
];
|
|
415
|
-
|
|
416
|
-
|
|
550
|
+
|
|
551
|
+
step('Ensuring Vault secrets (rumen_service_role_key, graph_inference_service_role_key)...');
|
|
552
|
+
if (dryRun) { ok('(dry-run)'); return { ok: true, created: [], deeplinks: [] }; }
|
|
553
|
+
|
|
554
|
+
if (!secrets.SUPABASE_SERVICE_ROLE_KEY) {
|
|
555
|
+
fail('SUPABASE_SERVICE_ROLE_KEY missing from in-memory secrets — preflight should have rejected');
|
|
556
|
+
return { ok: false, created: [], deeplinks: [], error: 'service-role-key-missing' };
|
|
417
557
|
}
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
558
|
+
|
|
559
|
+
let client = _pgClient;
|
|
560
|
+
let ownsClient = false;
|
|
561
|
+
if (!client) {
|
|
562
|
+
try {
|
|
563
|
+
client = await pgRunner.connect(secrets.DATABASE_URL);
|
|
564
|
+
ownsClient = true;
|
|
565
|
+
} catch (err) {
|
|
566
|
+
fail(err.message);
|
|
567
|
+
return { ok: false, created: [], deeplinks: [], error: 'pg-connect-failed' };
|
|
568
|
+
}
|
|
569
|
+
}
|
|
570
|
+
|
|
571
|
+
let existing = new Set();
|
|
572
|
+
try {
|
|
573
|
+
const probe = await client.query(
|
|
574
|
+
'SELECT name FROM vault.secrets WHERE name = ANY($1::text[])',
|
|
575
|
+
[required.map((x) => x.name)]
|
|
576
|
+
);
|
|
577
|
+
existing = new Set((probe.rows || []).map((r) => r.name));
|
|
578
|
+
} catch (err) {
|
|
579
|
+
fail(`vault.secrets probe failed: ${err.message}`);
|
|
580
|
+
if (ownsClient) { try { await client.end(); } catch (_e) { /* ignore */ } }
|
|
581
|
+
// Emit deeplinks for both — user can click through manually.
|
|
582
|
+
const deeplinks = required.map(({ name, value }) => ({
|
|
583
|
+
name,
|
|
584
|
+
url: vaultSqlEditorUrl(projectRef, name, value),
|
|
585
|
+
error: err.message
|
|
586
|
+
}));
|
|
587
|
+
printVaultDeeplinks(deeplinks);
|
|
588
|
+
return { ok: false, created: [], deeplinks, error: 'vault-probe-failed' };
|
|
589
|
+
}
|
|
590
|
+
|
|
591
|
+
const missing = required.filter((x) => !existing.has(x.name));
|
|
592
|
+
if (missing.length === 0) {
|
|
593
|
+
if (ownsClient) { try { await client.end(); } catch (_e) { /* ignore */ } }
|
|
594
|
+
ok('(both already present)');
|
|
595
|
+
return { ok: true, created: [], deeplinks: [] };
|
|
596
|
+
}
|
|
597
|
+
|
|
598
|
+
const created = [];
|
|
599
|
+
const deeplinks = [];
|
|
600
|
+
for (const { name, value } of missing) {
|
|
601
|
+
try {
|
|
602
|
+
await client.query('SELECT vault.create_secret($1, $2)', [value, name]);
|
|
603
|
+
created.push(name);
|
|
604
|
+
} catch (err) {
|
|
605
|
+
deeplinks.push({
|
|
606
|
+
name,
|
|
607
|
+
url: vaultSqlEditorUrl(projectRef, name, value),
|
|
608
|
+
error: err && err.message ? err.message : String(err)
|
|
609
|
+
});
|
|
610
|
+
}
|
|
611
|
+
}
|
|
612
|
+
if (ownsClient) { try { await client.end(); } catch (_e) { /* ignore */ } }
|
|
613
|
+
|
|
614
|
+
if (deeplinks.length > 0) {
|
|
615
|
+
fail(`auto-created ${created.length} of ${missing.length}; ${deeplinks.length} need a manual SQL Editor click`);
|
|
616
|
+
printVaultDeeplinks(deeplinks);
|
|
617
|
+
return { ok: false, created, deeplinks };
|
|
618
|
+
}
|
|
619
|
+
|
|
620
|
+
ok(`(created ${created.length}: ${created.map((n) => n).join(', ')})`);
|
|
621
|
+
return { ok: true, created, deeplinks: [] };
|
|
622
|
+
}
|
|
623
|
+
|
|
624
|
+
function printVaultDeeplinks(deeplinks) {
|
|
625
|
+
process.stderr.write(
|
|
626
|
+
'\nThe Supabase Vault dashboard panel has been removed in current Supabase UIs.\n' +
|
|
627
|
+
'Open each link below and click Run in SQL Editor to create the secret:\n\n'
|
|
628
|
+
);
|
|
629
|
+
for (const d of deeplinks) {
|
|
630
|
+
process.stderr.write(` ${d.name}\n ${d.url}\n`);
|
|
631
|
+
if (d.error) process.stderr.write(` (auto-apply error: ${d.error})\n`);
|
|
632
|
+
process.stderr.write('\n');
|
|
423
633
|
}
|
|
424
|
-
|
|
425
|
-
|
|
634
|
+
}
|
|
635
|
+
|
|
636
|
+
// Sprint 51.5 T3: Install-time prompt for the GRAPH_LLM_CLASSIFY toggle.
|
|
637
|
+
//
|
|
638
|
+
// graph-inference (the daily cron Edge Function) defaults every new edge to
|
|
639
|
+
// `relates_to` unless GRAPH_LLM_CLASSIFY=1 AND ANTHROPIC_API_KEY are set as
|
|
640
|
+
// Edge Function secrets. Pre-Sprint-51.5, no install path covered this — the
|
|
641
|
+
// wizard set ANTHROPIC_API_KEY (already required) but never set
|
|
642
|
+
// GRAPH_LLM_CLASSIFY, leaving the LLM classifier off by default. This is
|
|
643
|
+
// INSTALLER-PITFALLS.md Class F (default-vs-runtime asymmetry — Joshua may
|
|
644
|
+
// or may not have set the flag manually; new installs definitely don't).
|
|
645
|
+
//
|
|
646
|
+
// Side-effect: mutates `secrets.GRAPH_LLM_CLASSIFY` to '1' on Y-path. The
|
|
647
|
+
// per-secret loop in setFunctionSecrets reads that key and pushes it into
|
|
648
|
+
// the supabase secrets set sequence. On N-path the key is left undefined
|
|
649
|
+
// and the loop skips it; the wizard prints the manual flip command so the
|
|
650
|
+
// user can opt in later without re-running the whole wizard.
|
|
651
|
+
//
|
|
652
|
+
// --yes accepts the default (Y). --dry-run reports the plan and assumes Y.
|
|
653
|
+
async function promptGraphLlmClassify({ secrets, flags }) {
|
|
654
|
+
const explainer =
|
|
655
|
+
'\nGraph edge classification\n' +
|
|
656
|
+
'─────────────────────────\n' +
|
|
657
|
+
'When enabled, the daily graph-inference cron uses Claude Haiku 4.5 to label\n' +
|
|
658
|
+
'each new edge with a relationship type (supersedes, contradicts, elaborates,\n' +
|
|
659
|
+
'caused_by, blocks, inspired_by, cross_project_link, relates_to).\n' +
|
|
660
|
+
'\n' +
|
|
661
|
+
'Cost: ~$0.003 per 1k edges classified (a typical project sees a few hundred\n' +
|
|
662
|
+
'new edges per day). Disabled = every edge is typed "relates_to".\n\n';
|
|
663
|
+
|
|
664
|
+
if (flags.dryRun) {
|
|
665
|
+
process.stdout.write(explainer);
|
|
666
|
+
process.stdout.write('? Enable AI-classified graph edges? [Y/n] (dry-run, defaulting Y)\n');
|
|
667
|
+
secrets.GRAPH_LLM_CLASSIFY = '1';
|
|
668
|
+
return { enabled: true, source: 'dry-run' };
|
|
669
|
+
}
|
|
670
|
+
if (flags.yes) {
|
|
671
|
+
process.stdout.write(explainer);
|
|
672
|
+
process.stdout.write('? Enable AI-classified graph edges? [Y/n] (--yes, defaulting Y)\n');
|
|
673
|
+
secrets.GRAPH_LLM_CLASSIFY = '1';
|
|
674
|
+
return { enabled: true, source: '--yes' };
|
|
675
|
+
}
|
|
676
|
+
|
|
677
|
+
process.stdout.write(explainer);
|
|
678
|
+
const yes = await prompts.confirm('? Enable AI-classified graph edges?', { defaultYes: true });
|
|
679
|
+
if (yes) {
|
|
680
|
+
secrets.GRAPH_LLM_CLASSIFY = '1';
|
|
681
|
+
process.stdout.write(' → Will set GRAPH_LLM_CLASSIFY=1 + ANTHROPIC_API_KEY in Edge Function secrets.\n\n');
|
|
682
|
+
return { enabled: true, source: 'prompt' };
|
|
683
|
+
}
|
|
684
|
+
process.stdout.write(
|
|
685
|
+
' → GRAPH_LLM_CLASSIFY left unset. Edges will default to "relates_to".\n' +
|
|
686
|
+
' To enable later: supabase secrets set GRAPH_LLM_CLASSIFY=1\n\n'
|
|
687
|
+
);
|
|
688
|
+
return { enabled: false, source: 'prompt' };
|
|
426
689
|
}
|
|
427
690
|
|
|
428
691
|
async function testFunction(projectRef, secrets, dryRun) {
|
|
@@ -601,13 +864,22 @@ function wireAccessTokenInMcpJson({ token, mcpJsonPath, _testFs } = {}) {
|
|
|
601
864
|
return { status: 'updated', path: targetPath };
|
|
602
865
|
}
|
|
603
866
|
|
|
604
|
-
function printNextSteps(projectRef) {
|
|
867
|
+
function printNextSteps(projectRef, vaultResult, llmResult) {
|
|
605
868
|
const rumenTickUrl = `https://${projectRef}.supabase.co/functions/v1/rumen-tick`;
|
|
606
869
|
const graphInferenceUrl = `https://${projectRef}.supabase.co/functions/v1/graph-inference`;
|
|
607
870
|
const now = new Date();
|
|
608
871
|
// Round up to the next 15-minute mark so the hint is accurate.
|
|
609
872
|
const next = new Date(now.getTime());
|
|
610
873
|
next.setUTCMinutes(Math.ceil((now.getUTCMinutes() + 1) / 15) * 15, 0, 0);
|
|
874
|
+
|
|
875
|
+
const vaultLine = (vaultResult && vaultResult.ok)
|
|
876
|
+
? ' Vault secrets: rumen_service_role_key + graph_inference_service_role_key in place.'
|
|
877
|
+
: ' Vault secrets: open the SQL Editor URLs above and click Run for any deeplinks shown.';
|
|
878
|
+
|
|
879
|
+
const llmLine = llmResult && llmResult.enabled
|
|
880
|
+
? ' Graph edges: classified by Claude Haiku 4.5 (GRAPH_LLM_CLASSIFY=1).'
|
|
881
|
+
: ' Graph edges: untyped (relates_to). To enable: supabase secrets set GRAPH_LLM_CLASSIFY=1';
|
|
882
|
+
|
|
611
883
|
process.stdout.write(`
|
|
612
884
|
Rumen is deployed.
|
|
613
885
|
|
|
@@ -618,13 +890,12 @@ Edge Functions:
|
|
|
618
890
|
${graphInferenceUrl}
|
|
619
891
|
|
|
620
892
|
Next steps:
|
|
621
|
-
|
|
622
|
-
|
|
623
|
-
|
|
624
|
-
|
|
625
|
-
|
|
626
|
-
|
|
627
|
-
5. TermDeck's Flashback will surface cross-project patterns automatically.
|
|
893
|
+
${vaultLine}
|
|
894
|
+
${llmLine}
|
|
895
|
+
Monitor rumen jobs: psql "$DATABASE_URL" -c "SELECT * FROM rumen_jobs ORDER BY started_at DESC LIMIT 5"
|
|
896
|
+
Rumen insights flow back into Mnestra's memory_items via rumen_insights.
|
|
897
|
+
graph-inference fills memory_relationships edges nightly (cosine similarity ≥ 0.85).
|
|
898
|
+
TermDeck's Flashback will surface cross-project patterns automatically.
|
|
628
899
|
`);
|
|
629
900
|
}
|
|
630
901
|
|
|
@@ -658,6 +929,22 @@ async function main(argv) {
|
|
|
658
929
|
}
|
|
659
930
|
}
|
|
660
931
|
|
|
932
|
+
// Sprint 51.5 T3: ensure both Vault secrets are present BEFORE the
|
|
933
|
+
// precondition audit (which checks vault.decrypted_secrets for
|
|
934
|
+
// rumen_service_role_key). Auto-applies via pgRunner; on permission
|
|
935
|
+
// failure prints SQL-Editor deeplinks and lets the audit's own hint
|
|
936
|
+
// catch the still-missing secret. The Vault dashboard panel was quietly
|
|
937
|
+
// removed in current Supabase UIs (Brad 2026-05-03 takeaway #2;
|
|
938
|
+
// INSTALLER-PITFALLS.md Class B), which is why we no longer instruct
|
|
939
|
+
// users to "click Vault."
|
|
940
|
+
let vaultResult = { ok: true, created: [], deeplinks: [] };
|
|
941
|
+
if (!flags.dryRun) {
|
|
942
|
+
vaultResult = await ensureVaultSecrets({ projectRef, secrets, dryRun: false });
|
|
943
|
+
// Continue regardless — preconditions audit will catch a still-missing
|
|
944
|
+
// secret with its own hint, and the user already has the deeplinks if
|
|
945
|
+
// any auto-apply failed.
|
|
946
|
+
}
|
|
947
|
+
|
|
661
948
|
// v0.6.9: front-loaded precondition audit. Runs BEFORE link so we don't
|
|
662
949
|
// create state (function deploy, function secrets, schedule SQL) that the
|
|
663
950
|
// user would have to manually clean up if a precondition is missing. Every
|
|
@@ -691,6 +978,14 @@ async function main(argv) {
|
|
|
691
978
|
// already-set) are silent — they're all expected paths.
|
|
692
979
|
}
|
|
693
980
|
|
|
981
|
+
// Sprint 51.5 T1 — audit-upgrade BEFORE the rest of the flow. Surfaces
|
|
982
|
+
// and applies any drift between the bundled artifact set and what's
|
|
983
|
+
// actually live on the user's project. Non-blocking on failure (probe
|
|
984
|
+
// errors get logged inline; main flow continues).
|
|
985
|
+
if (!flags.dryRun) {
|
|
986
|
+
await runRumenAudit(projectRef, secrets, flags.dryRun);
|
|
987
|
+
}
|
|
988
|
+
|
|
694
989
|
if (!(await applyRumenTables(secrets, flags.dryRun))) return 5;
|
|
695
990
|
|
|
696
991
|
step('Resolving @jhizzard/rumen version from npm registry...');
|
|
@@ -703,6 +998,12 @@ async function main(argv) {
|
|
|
703
998
|
}
|
|
704
999
|
|
|
705
1000
|
if (!deployFunctions(resolved.version, flags.dryRun)) return 6;
|
|
1001
|
+
|
|
1002
|
+
// Sprint 51.5 T3: install-time prompt for AI edge classification. Sets
|
|
1003
|
+
// secrets.GRAPH_LLM_CLASSIFY in-memory; the per-secret loop below picks
|
|
1004
|
+
// it up. On --yes / --dry-run defaults to enabled (Y).
|
|
1005
|
+
const llmResult = await promptGraphLlmClassify({ secrets, flags });
|
|
1006
|
+
|
|
706
1007
|
if (!setFunctionSecrets(secrets, flags.dryRun)) return 7;
|
|
707
1008
|
if (!(await testFunction(projectRef, secrets, flags.dryRun))) return 8;
|
|
708
1009
|
if (!flags.skipSchedule) {
|
|
@@ -720,7 +1021,7 @@ async function main(argv) {
|
|
|
720
1021
|
process.stdout.write('→ Skipping pg_cron schedule (per --skip-schedule) ✓\n');
|
|
721
1022
|
}
|
|
722
1023
|
|
|
723
|
-
printNextSteps(projectRef);
|
|
1024
|
+
printNextSteps(projectRef, vaultResult, llmResult);
|
|
724
1025
|
return 0;
|
|
725
1026
|
}
|
|
726
1027
|
|
|
@@ -741,3 +1042,10 @@ module.exports._wireAccessTokenInMcpJson = wireAccessTokenInMcpJson;
|
|
|
741
1042
|
// Sprint 43 T3: stage helper exposed so init-rumen-deploy.test.js can pin
|
|
742
1043
|
// the multi-function staging contract without shelling out to `supabase`.
|
|
743
1044
|
module.exports._stageRumenFunctions = stageRumenFunctions;
|
|
1045
|
+
// Sprint 51.5 T3: per-secret CLI loop, Vault SQL-Editor URL builder, and
|
|
1046
|
+
// Vault-secret ensure helper exposed for tests/init-rumen-secrets-per-call,
|
|
1047
|
+
// init-rumen-graph-llm, and init-rumen-vault-deeplinks.
|
|
1048
|
+
module.exports._setFunctionSecrets = setFunctionSecrets;
|
|
1049
|
+
module.exports._vaultSqlEditorUrl = vaultSqlEditorUrl;
|
|
1050
|
+
module.exports._ensureVaultSecrets = ensureVaultSecrets;
|
|
1051
|
+
module.exports._promptGraphLlmClassify = promptGraphLlmClassify;
|
|
@@ -0,0 +1,302 @@
|
|
|
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
|
+
name: 'rumen-tick cron schedule',
|
|
122
|
+
kind: 'rumen',
|
|
123
|
+
migrationFile: '002_pg_cron_schedule.sql',
|
|
124
|
+
templated: true,
|
|
125
|
+
probeSql:
|
|
126
|
+
"select 1 as present from cron.job where jobname = 'rumen-tick' limit 1",
|
|
127
|
+
presentWhen: 'rowReturned'
|
|
128
|
+
},
|
|
129
|
+
{
|
|
130
|
+
name: 'graph-inference-tick cron schedule',
|
|
131
|
+
kind: 'rumen',
|
|
132
|
+
migrationFile: '003_graph_inference_schedule.sql',
|
|
133
|
+
templated: true,
|
|
134
|
+
probeSql:
|
|
135
|
+
"select 1 as present from cron.job where jobname = 'graph-inference-tick' limit 1",
|
|
136
|
+
presentWhen: 'rowReturned'
|
|
137
|
+
}
|
|
138
|
+
]);
|
|
139
|
+
|
|
140
|
+
// Find the bundled migration file for a probe target. Returns the absolute
|
|
141
|
+
// path or null. `mnestra` looks under bundled mnestra-migrations; `rumen`
|
|
142
|
+
// looks under bundled rumen/migrations. Both kinds prefer the bundled copy
|
|
143
|
+
// (matches the listMnestraMigrations / listRumenMigrations convention from
|
|
144
|
+
// v0.6.8 — bundled FIRST, then the @jhizzard/<pkg> node_modules fallback).
|
|
145
|
+
function resolveMigrationFile(target, files) {
|
|
146
|
+
const wanted = target.migrationFile;
|
|
147
|
+
return files.find((f) => path.basename(f) === wanted) || null;
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
// Run a probe and decide present/absent based on the probe's contract.
|
|
151
|
+
async function probeOne(pgClient, target) {
|
|
152
|
+
let result;
|
|
153
|
+
try {
|
|
154
|
+
result = await pgClient.query(target.probeSql);
|
|
155
|
+
} catch (err) {
|
|
156
|
+
// A probe failure (e.g., schema doesn't exist yet — `cron.job` on a
|
|
157
|
+
// project without pg_cron) means the artifact is absent. Record the
|
|
158
|
+
// raw error so a caller can distinguish "absent because never installed"
|
|
159
|
+
// from "absent because we can't even check." Either way the right
|
|
160
|
+
// response is to attempt apply — which will surface the real error
|
|
161
|
+
// (e.g., "extension pg_cron is not installed") with full context.
|
|
162
|
+
return { present: false, probeError: err.message };
|
|
163
|
+
}
|
|
164
|
+
const rows = (result && result.rows) || [];
|
|
165
|
+
if (target.presentWhen === 'rowReturned') {
|
|
166
|
+
return { present: rows.length > 0 };
|
|
167
|
+
}
|
|
168
|
+
if (target.presentWhen === 'boolColumnTrue') {
|
|
169
|
+
return { present: Boolean(rows[0] && rows[0].present === true) };
|
|
170
|
+
}
|
|
171
|
+
// Defensive default: any returned row counts as present.
|
|
172
|
+
return { present: rows.length > 0 };
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
// Apply a single migration file. Templated migrations route through
|
|
176
|
+
// applyTemplating() so the cron schedule body never sees the raw
|
|
177
|
+
// `<project-ref>` placeholder. Brad 2026-05-03 takeaway #5 (bonus): the
|
|
178
|
+
// fresh-install path at init-rumen.js:472-505 already does this; the
|
|
179
|
+
// audit-upgrade path MUST mirror it. Tests in audit-upgrade.test.js guard
|
|
180
|
+
// against future bypass.
|
|
181
|
+
async function applyOne(pgClient, target, files, { projectRef, readFileImpl }) {
|
|
182
|
+
const file = resolveMigrationFile(target, files);
|
|
183
|
+
if (!file) {
|
|
184
|
+
throw new Error(
|
|
185
|
+
`audit-upgrade: bundled migration file not found for ${target.name} ` +
|
|
186
|
+
`(expected ${target.migrationFile}). The bundled migration set may be ` +
|
|
187
|
+
`out of sync — re-publish the package or run scripts/sync-rumen-functions.sh.`
|
|
188
|
+
);
|
|
189
|
+
}
|
|
190
|
+
const raw = readFileImpl(file);
|
|
191
|
+
const sql = target.templated
|
|
192
|
+
? applyTemplating(raw, { projectRef })
|
|
193
|
+
: raw;
|
|
194
|
+
await pgClient.query(sql);
|
|
195
|
+
return { file: path.basename(file) };
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
// Public API.
|
|
199
|
+
//
|
|
200
|
+
// Inputs:
|
|
201
|
+
// pgClient — open node-postgres Client (caller owns the lifecycle).
|
|
202
|
+
// projectRef — required when any templated migration is in the probe set
|
|
203
|
+
// (i.e., the rumen cron schedules). The applyTemplating
|
|
204
|
+
// helper will throw if it sees a `<project-ref>` placeholder
|
|
205
|
+
// and projectRef is missing — surfaced via errors[].
|
|
206
|
+
// dryRun — when true, probes only; skips apply. applied stays empty.
|
|
207
|
+
// probes — optional override for the probe set (test injection point).
|
|
208
|
+
// Defaults to PROBES.
|
|
209
|
+
// _migrations — optional override for the migrations module (test
|
|
210
|
+
// injection). Lets tests stub listMnestraMigrations /
|
|
211
|
+
// listRumenMigrations / readFile.
|
|
212
|
+
//
|
|
213
|
+
// Returns:
|
|
214
|
+
// {
|
|
215
|
+
// probed: string[] — every target name we tried to probe
|
|
216
|
+
// present: string[] — targets whose probe came back present
|
|
217
|
+
// missing: string[] — targets whose probe came back absent
|
|
218
|
+
// applied: string[] — targets the audit applied this run
|
|
219
|
+
// (empty when dryRun=true)
|
|
220
|
+
// skipped: string[] — targets we couldn't apply (e.g., missing
|
|
221
|
+
// projectRef on a templated migration)
|
|
222
|
+
// errors: Array<{ name, error }> — apply or probe errors (probe errors
|
|
223
|
+
// only surface here when subsequent
|
|
224
|
+
// apply ALSO fails)
|
|
225
|
+
// }
|
|
226
|
+
//
|
|
227
|
+
// Idempotent: a second run reports `applied=[]` because every probe will
|
|
228
|
+
// come back present. All shipped migrations are themselves idempotent
|
|
229
|
+
// (ADD COLUMN IF NOT EXISTS, CREATE INDEX IF NOT EXISTS,
|
|
230
|
+
// cron.unschedule + cron.schedule, GRANT … TO service_role).
|
|
231
|
+
async function auditUpgrade({
|
|
232
|
+
pgClient,
|
|
233
|
+
projectRef,
|
|
234
|
+
dryRun = false,
|
|
235
|
+
probes,
|
|
236
|
+
_migrations
|
|
237
|
+
} = {}) {
|
|
238
|
+
if (!pgClient || typeof pgClient.query !== 'function') {
|
|
239
|
+
throw new Error('auditUpgrade: pgClient with .query() is required');
|
|
240
|
+
}
|
|
241
|
+
const targets = probes || PROBES;
|
|
242
|
+
const mig = _migrations || migrations;
|
|
243
|
+
|
|
244
|
+
// Resolve once: the bundled migration sets stay constant for the duration
|
|
245
|
+
// of a single audit run.
|
|
246
|
+
const mnestraFiles = mig.listMnestraMigrations();
|
|
247
|
+
const rumenFiles = mig.listRumenMigrations();
|
|
248
|
+
|
|
249
|
+
const probed = [];
|
|
250
|
+
const present = [];
|
|
251
|
+
const missing = [];
|
|
252
|
+
const applied = [];
|
|
253
|
+
const skipped = [];
|
|
254
|
+
const errors = [];
|
|
255
|
+
|
|
256
|
+
for (const target of targets) {
|
|
257
|
+
probed.push(target.name);
|
|
258
|
+
const probeResult = await probeOne(pgClient, target);
|
|
259
|
+
if (probeResult.present) {
|
|
260
|
+
present.push(target.name);
|
|
261
|
+
continue;
|
|
262
|
+
}
|
|
263
|
+
missing.push(target.name);
|
|
264
|
+
|
|
265
|
+
if (dryRun) continue;
|
|
266
|
+
|
|
267
|
+
const files = target.kind === 'rumen' ? rumenFiles : mnestraFiles;
|
|
268
|
+
|
|
269
|
+
try {
|
|
270
|
+
await applyOne(pgClient, target, files, {
|
|
271
|
+
projectRef,
|
|
272
|
+
readFileImpl: mig.readFile
|
|
273
|
+
});
|
|
274
|
+
applied.push(target.name);
|
|
275
|
+
} catch (err) {
|
|
276
|
+
// Surface but don't abort. One missing artifact failing to apply (e.g.,
|
|
277
|
+
// pg_cron extension not enabled) shouldn't block the rest of the audit
|
|
278
|
+
// from running. The wizard will report the whole audit summary at the
|
|
279
|
+
// end so the user can address each failure individually.
|
|
280
|
+
errors.push({
|
|
281
|
+
name: target.name,
|
|
282
|
+
error: err && err.message ? err.message : String(err)
|
|
283
|
+
});
|
|
284
|
+
}
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
// skipped[] reserved for v1.0.2: targets the audit deliberately doesn't
|
|
288
|
+
// attempt (e.g., when projectRef is missing for a templated migration we
|
|
289
|
+
// currently let applyTemplating throw → errors[]; future versions may
|
|
290
|
+
// pre-skip those into skipped[]).
|
|
291
|
+
return { probed, present, missing, applied, skipped, errors };
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
module.exports = {
|
|
295
|
+
auditUpgrade,
|
|
296
|
+
PROBES,
|
|
297
|
+
// Test surface — kept exported so audit-upgrade.test.js can pin probe
|
|
298
|
+
// selection / apply pathway behavior without needing a live pg client.
|
|
299
|
+
_probeOne: probeOne,
|
|
300
|
+
_applyOne: applyOne,
|
|
301
|
+
_resolveMigrationFile: resolveMigrationFile
|
|
302
|
+
};
|
|
@@ -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;
|
|
@@ -0,0 +1,117 @@
|
|
|
1
|
+
-- Mnestra v0.4.1 — `mnestra doctor` SECURITY DEFINER probe wrappers
|
|
2
|
+
--
|
|
3
|
+
-- Sprint 51.5 T2 (TermDeck). Adds five SECURITY DEFINER helper functions
|
|
4
|
+
-- so the `mnestra doctor` subcommand (running under the supabase
|
|
5
|
+
-- service_role) can probe `cron.job_run_details`, `cron.job`, `vault.secrets`,
|
|
6
|
+
-- `information_schema.columns`, and `pg_proc` without granting raw schema
|
|
7
|
+
-- access to service_role.
|
|
8
|
+
--
|
|
9
|
+
-- Why SECURITY DEFINER: by default `cron.*` and `vault.*` are owned by
|
|
10
|
+
-- the `postgres` role and unreadable from service_role. The classical
|
|
11
|
+
-- alternative is to `grant usage on schema cron to service_role; grant
|
|
12
|
+
-- select on cron.job_run_details to service_role; …` — broader privilege
|
|
13
|
+
-- expansion than this lane needs. SECURITY DEFINER lets us read exactly
|
|
14
|
+
-- what the doctor needs without exposing the entire cron/vault surface.
|
|
15
|
+
--
|
|
16
|
+
-- Idempotent: every function uses CREATE OR REPLACE; every GRANT is
|
|
17
|
+
-- safe to re-run. No data mutation. Safe to re-apply on every install.
|
|
18
|
+
|
|
19
|
+
-- ── 1. cron.job_run_details lookup ───────────────────────────────────────
|
|
20
|
+
--
|
|
21
|
+
-- Returns the most recent N runs of a named cron job, projecting only
|
|
22
|
+
-- the columns the doctor needs (status, start/end timestamps, return
|
|
23
|
+
-- message). The doctor parses `return_message` for the rumen-tick /
|
|
24
|
+
-- graph-inference all-zeros pattern.
|
|
25
|
+
|
|
26
|
+
create or replace function mnestra_doctor_cron_runs(
|
|
27
|
+
p_jobname text,
|
|
28
|
+
p_limit int default 10
|
|
29
|
+
)
|
|
30
|
+
returns table (
|
|
31
|
+
jobname text,
|
|
32
|
+
status text,
|
|
33
|
+
start_time timestamptz,
|
|
34
|
+
end_time timestamptz,
|
|
35
|
+
return_message text
|
|
36
|
+
)
|
|
37
|
+
language sql
|
|
38
|
+
security definer
|
|
39
|
+
set search_path = cron, public
|
|
40
|
+
as $$
|
|
41
|
+
select j.jobname, d.status, d.start_time, d.end_time, d.return_message
|
|
42
|
+
from cron.job_run_details d
|
|
43
|
+
join cron.job j on j.jobid = d.jobid
|
|
44
|
+
where j.jobname = p_jobname
|
|
45
|
+
order by d.start_time desc
|
|
46
|
+
limit greatest(coalesce(p_limit, 10), 1);
|
|
47
|
+
$$;
|
|
48
|
+
|
|
49
|
+
-- ── 2. column existence probe (public schema only) ───────────────────────
|
|
50
|
+
|
|
51
|
+
create or replace function mnestra_doctor_column_exists(
|
|
52
|
+
p_table text,
|
|
53
|
+
p_column text
|
|
54
|
+
)
|
|
55
|
+
returns boolean
|
|
56
|
+
language sql
|
|
57
|
+
security definer
|
|
58
|
+
set search_path = public
|
|
59
|
+
as $$
|
|
60
|
+
select exists (
|
|
61
|
+
select 1
|
|
62
|
+
from information_schema.columns
|
|
63
|
+
where table_schema = 'public'
|
|
64
|
+
and table_name = p_table
|
|
65
|
+
and column_name = p_column
|
|
66
|
+
);
|
|
67
|
+
$$;
|
|
68
|
+
|
|
69
|
+
-- ── 3. RPC / function existence probe ────────────────────────────────────
|
|
70
|
+
|
|
71
|
+
create or replace function mnestra_doctor_rpc_exists(p_name text)
|
|
72
|
+
returns boolean
|
|
73
|
+
language sql
|
|
74
|
+
security definer
|
|
75
|
+
set search_path = public
|
|
76
|
+
as $$
|
|
77
|
+
select exists (
|
|
78
|
+
select 1
|
|
79
|
+
from pg_proc p
|
|
80
|
+
join pg_namespace n on n.oid = p.pronamespace
|
|
81
|
+
where p.proname = p_name
|
|
82
|
+
and n.nspname = 'public'
|
|
83
|
+
);
|
|
84
|
+
$$;
|
|
85
|
+
|
|
86
|
+
-- ── 4. cron.job existence probe (does the named job exist at all) ───────
|
|
87
|
+
|
|
88
|
+
create or replace function mnestra_doctor_cron_job_exists(p_jobname text)
|
|
89
|
+
returns boolean
|
|
90
|
+
language sql
|
|
91
|
+
security definer
|
|
92
|
+
set search_path = cron, public
|
|
93
|
+
as $$
|
|
94
|
+
select exists (select 1 from cron.job where jobname = p_jobname);
|
|
95
|
+
$$;
|
|
96
|
+
|
|
97
|
+
-- ── 5. vault.secrets existence probe (no value disclosure) ──────────────
|
|
98
|
+
--
|
|
99
|
+
-- Existence-only — never returns the secret value. The doctor only needs
|
|
100
|
+
-- to know whether the named vault entry was created during stack install.
|
|
101
|
+
|
|
102
|
+
create or replace function mnestra_doctor_vault_secret_exists(p_name text)
|
|
103
|
+
returns boolean
|
|
104
|
+
language sql
|
|
105
|
+
security definer
|
|
106
|
+
set search_path = vault, public
|
|
107
|
+
as $$
|
|
108
|
+
select exists (select 1 from vault.secrets where name = p_name);
|
|
109
|
+
$$;
|
|
110
|
+
|
|
111
|
+
-- ── 6. Grants ────────────────────────────────────────────────────────────
|
|
112
|
+
|
|
113
|
+
grant execute on function mnestra_doctor_cron_runs(text, int) to service_role;
|
|
114
|
+
grant execute on function mnestra_doctor_column_exists(text, text) to service_role;
|
|
115
|
+
grant execute on function mnestra_doctor_rpc_exists(text) to service_role;
|
|
116
|
+
grant execute on function mnestra_doctor_cron_job_exists(text) to service_role;
|
|
117
|
+
grant execute on function mnestra_doctor_vault_secret_exists(text) to service_role;
|
|
@@ -52,6 +52,23 @@ function extensionsDashboardUrl(secrets) {
|
|
|
52
52
|
return `https://supabase.com/dashboard/project/${parsed.projectRef}/database/extensions`;
|
|
53
53
|
}
|
|
54
54
|
|
|
55
|
+
// Sprint 51.5 T3: Build a SQL-Editor deeplink that pre-fills a
|
|
56
|
+
// vault.create_secret() call. Used when the audit's vault probe finds the
|
|
57
|
+
// secret missing and the wizard's auto-apply step (init-rumen.js
|
|
58
|
+
// `ensureVaultSecrets`) was unable to create it via pgRunner. The Vault
|
|
59
|
+
// dashboard panel was quietly removed/relocated in current Supabase UIs
|
|
60
|
+
// (Brad 2026-05-03 takeaway #2; INSTALLER-PITFALLS.md Class B), so SQL
|
|
61
|
+
// Editor is the working manual surface.
|
|
62
|
+
function vaultSqlEditorUrl(secrets, secretName, secretValue) {
|
|
63
|
+
if (!secrets || !secrets.SUPABASE_URL) return null;
|
|
64
|
+
const parsed = supabaseUrlHelper.parseProjectUrl(secrets.SUPABASE_URL);
|
|
65
|
+
if (!parsed.ok) return null;
|
|
66
|
+
const value = String(secretValue == null ? '' : secretValue).replace(/'/g, "''");
|
|
67
|
+
const name = String(secretName == null ? '' : secretName).replace(/'/g, "''");
|
|
68
|
+
const sql = `select vault.create_secret('${value}', '${name}');`;
|
|
69
|
+
return `https://supabase.com/dashboard/project/${parsed.projectRef}/sql/new?content=${encodeURIComponent(sql)}`;
|
|
70
|
+
}
|
|
71
|
+
|
|
55
72
|
// Render a single gap into 2-3 lines of CLI output (one indented hint per
|
|
56
73
|
// non-empty `hint` line). Format aligned with the rest of the wizard's
|
|
57
74
|
// step lines.
|
|
@@ -206,14 +223,28 @@ async function auditRumenPreconditions({ secrets, env, _pgClient } = {}) {
|
|
|
206
223
|
'If permission is denied, the Vault is not accessible to this connection — double-check secrets.env.'
|
|
207
224
|
});
|
|
208
225
|
} else if (!vault.ok) {
|
|
226
|
+
// Sprint 51.5 T3: the Supabase Vault dashboard panel was quietly removed
|
|
227
|
+
// / relocated in current Supabase UIs (Brad 2026-05-03 takeaway #2;
|
|
228
|
+
// INSTALLER-PITFALLS.md Class B). The wizard's `ensureVaultSecrets`
|
|
229
|
+
// step (init-rumen.js) auto-creates this key via pgRunner when the
|
|
230
|
+
// user's connection has vault.create_secret privileges; this hint only
|
|
231
|
+
// fires when auto-apply also failed, in which case the SQL Editor is
|
|
232
|
+
// the working manual surface. Build a project-specific deeplink when
|
|
233
|
+
// we can derive the project ref so the user gets one click instead of
|
|
234
|
+
// a "find the SQL Editor yourself" instruction.
|
|
235
|
+
const sqlEditorUrl = vaultSqlEditorUrl(secrets, 'rumen_service_role_key',
|
|
236
|
+
'<paste your service_role JWT from Project Settings → API>');
|
|
237
|
+
const sqlEditorLine = sqlEditorUrl
|
|
238
|
+
? ` Open: ${sqlEditorUrl}\n Replace the placeholder with your service_role key from Project Settings → API, then click Run.`
|
|
239
|
+
: ' Open the Supabase SQL Editor and run:\n' +
|
|
240
|
+
" select vault.create_secret('<service_role JWT>', 'rumen_service_role_key');";
|
|
209
241
|
gaps.push({
|
|
210
242
|
key: 'rumen_service_role_key',
|
|
211
243
|
message: 'Vault secret "rumen_service_role_key" is missing',
|
|
212
244
|
hint:
|
|
213
|
-
|
|
214
|
-
'
|
|
215
|
-
|
|
216
|
-
' Value: your service_role key from Project Settings → API\n' +
|
|
245
|
+
"The wizard tries to auto-create this via vault.create_secret() and only surfaces this hint when auto-apply also failed.\n" +
|
|
246
|
+
'Create it manually via the SQL Editor (the Vault dashboard panel was removed in current Supabase UIs):\n' +
|
|
247
|
+
sqlEditorLine + '\n' +
|
|
217
248
|
'(The pg_cron schedule calls the Edge Function with this key as the bearer token.)'
|
|
218
249
|
});
|
|
219
250
|
}
|
|
@@ -384,6 +415,7 @@ module.exports = {
|
|
|
384
415
|
printAuditReport,
|
|
385
416
|
printVerifyReport,
|
|
386
417
|
extensionsDashboardUrl,
|
|
418
|
+
vaultSqlEditorUrl,
|
|
387
419
|
// Test surface
|
|
388
420
|
_probeSupabaseAuth: probeSupabaseAuth,
|
|
389
421
|
_safeQuery: safeQuery
|
|
@@ -343,9 +343,13 @@ export async function runGraphInference(sql: Sql): Promise<InferenceSummary> {
|
|
|
343
343
|
}
|
|
344
344
|
|
|
345
345
|
serve(async (_req: Request) => {
|
|
346
|
-
|
|
346
|
+
// Supabase Edge Runtime auto-injects SUPABASE_DB_URL as a built-in env var.
|
|
347
|
+
// Falling back to it removes one whole category of "where do I get the DB
|
|
348
|
+
// connection string" from the install wizard. Brad surfaced this 2026-05-03
|
|
349
|
+
// after hand-patching all four of his deployed copies.
|
|
350
|
+
const url = Deno.env.get('DATABASE_URL') ?? Deno.env.get('SUPABASE_DB_URL');
|
|
347
351
|
if (!url) {
|
|
348
|
-
console.error('[graph-inference] DATABASE_URL not set in Edge Function secrets');
|
|
352
|
+
console.error('[graph-inference] DATABASE_URL / SUPABASE_DB_URL not set in Edge Function secrets');
|
|
349
353
|
return new Response(
|
|
350
354
|
JSON.stringify({ ok: false, error: 'DATABASE_URL not set' }),
|
|
351
355
|
{ status: 500, headers: { 'Content-Type': 'application/json' } },
|
|
@@ -31,9 +31,13 @@ import { runRumenJob, createPoolFromUrl } from 'npm:@jhizzard/rumen@__RUMEN_VERS
|
|
|
31
31
|
declare const Deno: { env: { get: (k: string) => string | undefined } };
|
|
32
32
|
|
|
33
33
|
serve(async (_req: Request) => {
|
|
34
|
-
|
|
34
|
+
// Supabase Edge Runtime auto-injects SUPABASE_DB_URL as a built-in env var.
|
|
35
|
+
// Falling back to it removes one whole category of "where do I get the DB
|
|
36
|
+
// connection string" from the install wizard. Brad surfaced this 2026-05-03
|
|
37
|
+
// after hand-patching all four of his deployed copies.
|
|
38
|
+
const url = Deno.env.get('DATABASE_URL') ?? Deno.env.get('SUPABASE_DB_URL');
|
|
35
39
|
if (!url) {
|
|
36
|
-
console.error('[rumen] DATABASE_URL not set in Edge Function secrets');
|
|
40
|
+
console.error('[rumen] DATABASE_URL / SUPABASE_DB_URL not set in Edge Function secrets');
|
|
37
41
|
return new Response(
|
|
38
42
|
JSON.stringify({
|
|
39
43
|
ok: false,
|