[object Object]

← back to Draft Viewer

Prove the snapshot actually restores — restore.mjs, rehearsed end to end

584f7f4e0e28e43cc728c3a348159aa47024396a · 2026-09-10 09:03:28 -0700 · Steve Abrams

A snapshot nobody has replayed is a claim, not a backup. Same lesson as
dw-restore-rehearsal, which exists because a pg_dump ran green for 12 days while
producing output that would not restore. Codex flagged the same gap here.

restore.mjs replays a snapshotted body back into Gmail as a NEW draft. It can
only create drafts, so it structurally cannot deliver mail. Dry-run by default.

Rehearsed three times against live Gmail, recipient redirected to an internal
address so no real vendor draft was duplicated. Result: the recipient-visible
message round-trips byte-identical (516 == 516 chars). The only delta is
George's provenance banner carrying the restore timestamp, which is correct.

The rehearsal caught a real defect: restore was NOT idempotent. George prepends
a 'From job:' banner to every draft it creates, so each restore stacked another
one. The first strip attempt failed because the banner is a styled <div>
wrapping <strong>From job:</strong> — matching the bare phrase left the wrapper
behind. Now strips the whole leading div block; verified 1 banner in, 1 out.

All three rehearsal drafts deleted; info@ is back to its exact 41-draft baseline.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

Files touched

Diff

commit 584f7f4e0e28e43cc728c3a348159aa47024396a
Author: Steve Abrams <steve@designerwallcoverings.com>
Date:   Thu Sep 10 09:03:28 2026 -0700

    Prove the snapshot actually restores — restore.mjs, rehearsed end to end
    
    A snapshot nobody has replayed is a claim, not a backup. Same lesson as
    dw-restore-rehearsal, which exists because a pg_dump ran green for 12 days while
    producing output that would not restore. Codex flagged the same gap here.
    
    restore.mjs replays a snapshotted body back into Gmail as a NEW draft. It can
    only create drafts, so it structurally cannot deliver mail. Dry-run by default.
    
    Rehearsed three times against live Gmail, recipient redirected to an internal
    address so no real vendor draft was duplicated. Result: the recipient-visible
    message round-trips byte-identical (516 == 516 chars). The only delta is
    George's provenance banner carrying the restore timestamp, which is correct.
    
    The rehearsal caught a real defect: restore was NOT idempotent. George prepends
    a 'From job:' banner to every draft it creates, so each restore stacked another
    one. The first strip attempt failed because the banner is a styled <div>
    wrapping <strong>From job:</strong> — matching the bare phrase left the wrapper
    behind. Now strips the whole leading div block; verified 1 banner in, 1 out.
    
    All three rehearsal drafts deleted; info@ is back to its exact 41-draft baseline.
    
    Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
---
 restore.mjs | 66 +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
 1 file changed, 66 insertions(+)

diff --git a/restore.mjs b/restore.mjs
new file mode 100644
index 0000000..b67de31
--- /dev/null
+++ b/restore.mjs
@@ -0,0 +1,66 @@
+#!/usr/bin/env node
+// restore.mjs — TK-11231
+// Replays a snapshotted draft body back into Gmail as a NEW draft.
+//
+// This is what makes the snapshot a backup rather than a claim. Same doctrine as
+// dw-restore-rehearsal: a dump nobody has replayed is not proven restorable.
+// It NEVER sends — Gmail drafts.create only. Composing a draft cannot deliver mail.
+//
+//   node restore.mjs <snapshot.json>              # dry run, prints what it would do
+//   node restore.mjs <snapshot.json> --apply      # recreate the draft for real
+//   node restore.mjs <snapshot.json> --apply --test-to <addr>
+//        redirect the recipient — used to REHEARSE the restore without creating a
+//        second real draft addressed at a live vendor.
+
+import fs from 'node:fs';
+import path from 'node:path';
+import os from 'node:os';
+
+const args = process.argv.slice(2);
+const file = args.find((a) => !a.startsWith('--'));
+const APPLY = args.includes('--apply');
+const testTo = args.includes('--test-to') ? args[args.indexOf('--test-to') + 1] : null;
+if (!file) { console.error('usage: node restore.mjs <snapshot.json> [--apply] [--test-to addr]'); process.exit(1); }
+
+function george() {
+  const cfg = JSON.parse(fs.readFileSync(path.join(os.homedir(), '.claude.json'), 'utf8'));
+  const env = cfg?.mcpServers?.george?.env || {};
+  return { url: (env.GEORGE_URL || 'http://127.0.0.1:9850').replace(/\/$/, ''),
+           headers: { Authorization: 'Basic ' + env.GEORGE_BASIC_AUTH, 'content-type': 'application/json' } };
+}
+
+const snap = JSON.parse(fs.readFileSync(path.resolve(file), 'utf8'));
+const m = snap.message, acct = snap.board.account;
+const to = testTo || m.to;
+// George stamps a "From job: …" provenance banner onto every draft it creates.
+// The snapshot captured a draft that already carries one, so restoring as-is
+// would prepend a SECOND banner and each further restore another — the restore
+// would not be idempotent. Strip a leading banner so a snapshot can be replayed
+// any number of times and still match the original.
+const stripBanner = (s) => s.replace(
+  // the whole leading <div …>…From job:…</div> block, not just the text — the
+  // banner is a styled div wrapping <strong>From job:</strong>, so matching on
+  // the bare phrase leaves the wrapper behind and strips nothing.
+  /^\s*<div\b[^>]*>(?:(?!<\/div>)[\s\S])*?From job:(?:(?!<\/div>)[\s\S])*?<\/div>\s*/i, '');
+const body = stripBanner(m.body || '');
+if (!body.trim()) { console.error('REFUSING: snapshot has an empty body — nothing to restore.'); process.exit(2); }
+
+console.log(`restore ${APPLY ? '(APPLY)' : '(dry run)'}`);
+console.log(`  from    : ${file}`);
+console.log(`  account : ${acct}`);
+console.log(`  to      : ${to}${testTo ? '   [REDIRECTED for rehearsal]' : ''}`);
+console.log(`  subject : ${m.subject}`);
+console.log(`  body    : ${body.length} chars`);
+if (!APPLY) { console.log('\ndry run — nothing written. add --apply to recreate.'); process.exit(0); }
+
+const g = george();
+// acknowledge_existing: this is a deliberate restore of a known draft, so the
+// duplicate-refusal added earlier today must not block a recovery.
+const res = await fetch(g.url + '/api/drafts', {
+  method: 'POST', headers: g.headers,
+  body: JSON.stringify({ account: acct, to, subject: m.subject, body, acknowledge_existing: true }),
+});
+const out = await res.json().catch(() => ({}));
+if (!res.ok) { console.error('restore FAILED:', res.status, JSON.stringify(out).slice(0, 400)); process.exit(3); }
+console.log('\nrestored →', JSON.stringify({ draftId: out.draftId || out.id, messageId: out.messageId || out.message?.id }));
+console.log('This created a DRAFT. It was not sent.');

← 3fb2245 TK-11231: archive + delete the 13 stale customer replies per  ·  back to Draft Viewer  ·  TK-11231: archive + clear the 7 superseded vendor drafts 955b609 →