{"slug":"dw-signup-fulfillment","total":143,"limit":100,"offset":0,"since":null,"commits":[{"hash":"12cc71b","date":"2026-09-10 11:56:27 -0700","author":"Steve Abrams","subject":"TK-10836: audit record for the D3501 deletion (original commit lost to an add -A race)","body":"Steve authorized the delete directly at the CLARIFY gate. Executed, verified: node\nre-read returns null, Kelly has 0 orders, nothing shipped, no confirmation email fired.\nUndo is one command (scripts/ship-kelly-samples.mjs --apply, committed 079d720).\n\nFiling this because my original deletion commit never landed: a concurrent session's\n'git add -A && git commit' (1de8a86, TK-11406) raced mine and swept my untracked\nsnapshot into its commit under an unrelated message. Evidence for a customer-facing\ndeletion ended up labelled as a sample-ledger change.\n\nFleet lesson recorded in the file: with concurrent agent sessions in one repo, 'git\nadd -A' commits other sessions' in-flight files under your message. Stage explicit paths.\n\nCo-Authored-By: Claude Opus 5 <noreply@anthropic.com>"},{"hash":"1de8a86","date":"2026-09-10 11:55:30 -0700","author":"Steve Abrams","subject":"TK-11406: sample-ledger counts free units title-agnostically (Regios 'Free sample'), restoring the app-side 3-sample cap","body":"The cap ledger only summed discounts titled 'DW Free Samples (auto)', but the live\ngrant is Regios titled 'Free sample' → countDiscountedSamples always returned 0 and\nfree_samples_used never incremented (cap was a silent no-op, held only by Regios\ninternally). Fix (b): sum all discount allocations on a sample line regardless of the\nemitting app's title; the sample-line heuristic already scopes eligibility and\nfloor(allocated/unitPrice) keeps partial discounts at 0. Removes dead DISCOUNT_TITLE.\nTest updated: title-agnostic order counts 3; undiscounted sample counts 0. DTD/Codex concurred (b).\n\nCo-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>\nClaude-Session: https://claude.ai/code/session_014nA9g7iQZMhnsGbaXfFYDL"},{"hash":"040121b","date":"2026-09-10 11:30:10 -0700","author":"Steve Abrams","subject":"TK-11411: fail loud on unset PUBLIC_URL before any send, not 11 times after","body":"The approved run mailed the 3 trade letters successfully, then failed all 11\nretail letters with no_public_url. lib/verify.js was right to refuse - without\nPUBLIC_URL the verify link would be a dead localhost URL - but the refusal came\nper-customer, after the trade cohort had already gone out, leaving a\nhalf-completed run.\n\nCause: PUBLIC_URL lives in ecosystem.config.js, which pm2 injects into the\nSERVICE. A bare `node scripts/...` does not inherit it. That is the exact\nfootgun documented at the top of scripts/recover-stuck-apps.js for DRY_RUN - a\ncomment I had already read today and then repeated with a different variable.\n\nNow pre-flights once, before any send, and prints the correct invocation.\n\nCo-Authored-By: Claude Opus 5 <noreply@anthropic.com>"},{"hash":"1a93bb0","date":"2026-09-10 11:26:12 -0700","author":"Steve Abrams","subject":"TK-11411: protect the resend ledger from rsync --delete","body":"Same class as the auto-approve and mint ledgers: an idempotency record living\ninside the deployed tree is reset by deploy.sh unless excluded — and a reset\nresend ledger means re-mailing customers who were already re-mailed.\n\nCo-Authored-By: Claude Opus 5 <noreply@anthropic.com>"},{"hash":"03b8d4b","date":"2026-09-10 11:25:45 -0700","author":"Steve Abrams","subject":"TK-11411: ledgered re-send for the 14 customers who lost mail in the George outage","body":"Scoping correction first: I initially reported THREE affected customers. That was\nwrong - it was only what fit in a 200-line log window. The real cohort from the\non-disk logs is FOURTEEN, and I had also grabbed a rotated logfile with `head -1`\nbefore noticing. 17 retail-verify + 5 designer-welcome + 5 trade-application 401s.\n\nVerified against live Shopify rather than trusting the logs: all 14 customers\nexist and NONE carries verified-sample, so all 14 are genuinely still owed. They\nsplit into two cohorts needing different letters:\n\n  RETAIL (11) - signed up for free samples, never got \"confirm your email\", so\n  never clicked, so no verified-sample tag, so they cannot claim samples.\n  Remedy: re-run the same lib/verify.startVerification the /claim route uses.\n\n  TRADE (3: noelle@silkandslate, team@michellewalshdesigns, yanchapaxi@auxarchitecture)\n  - their approval genuinely LANDED. Shopify carries trade+trade_approved AND the\n  local row says approved, so this is NOT the split-brain Cody warned about; only\n  the \"you're approved\" letter was lost. Remedy: that letter alone. Deliberately\n  does NOT re-run trade.approve(), which would re-tag and re-assign a rep.\n\nSafety: dry-run by default and --apply refuses while DRY_RUN is on; the cohort is\nDERIVED at run time from live Shopify tags plus the local store, never hardcoded,\nso anyone who recovered on their own is skipped; idempotent through\ndata/tk11411-resend-ledger.jsonl, appended only after a confirmed real send.\n\nDry-run against live prod data classifies 11 RETAIL + 3 TRADE, sends nothing.\n\nNOT SENT. Mailing 14 real customers is send-to-customer, hard-gated."},{"hash":"c959bc5","date":"2026-09-10 11:18:00 -0700","author":"Steve Abrams","subject":"TK-11366: answer the valence question - full-price free shipping was a weight-data leak","body":"Pre-window (Jul 1 - Aug 17) non-sample US orders, n=73 (31 free / 42 paid):\n  FREE: median cart weight 0.25 lb, 55%% under 0.5 lb, 32%% of line items unweighted\n  PAID: median cart weight 12 lb,     2%% under 0.5 lb,  2%% of line items unweighted\n\nA 16x difference in unweighted-line rate. The free rate was gated on TOTAL WEIGHT\n<= 0.5 lb on the General profile, so a cart with no product weights totals 0 lb and\nships free regardless of value: #32506 $938.84, #32510 $476.48, #32545 $411.72,\n#32611 $396.00, #32725 $351.84 - all 0 lb, all unweighted, all free.\n\nSo the full-price shift is the OPPOSITE of the samples story: samples = customers\novercharged on advertised-free items (real harm, fixed); full-price = DW eating freight\nfrom a catalog data gap (harm to DW, closed). No second customer-harm incident.\n\nResidual not papered over: 45%% of free full-price orders weighed MORE than 0.5 lb and\nare unexplained by the weight gate (#32818 $1820/4lb, #32622 $1095/6lb, #32700 $859/8lb).\n\nSpun the catalog defect off as TK-11414.\n\nCo-Authored-By: Claude Opus 5 <noreply@anthropic.com>"},{"hash":"b3b8acb","date":"2026-09-10 10:59:46 -0700","author":"Steve Abrams","subject":"TK-10836: park the Kelly remedy — Steve: stop sending to Kelly","body":"Nothing was sent to the customer at any point in this session.\n\n- send-kelly-shipped-note.js renamed .PARKED-2026-09-10-steve-stop-sending so it\n  cannot fire by accident (undo: rename back).\n- Did NOT write the .kelly-shipped-note.sent sentinel: that would falsely record\n  a send that never happened.\n- Draft order #D3501 ($0.00, 3 swatches, Ann Arbor) is left OPEN and unshipped.\n  Anyone completing it WILL ship to the customer and trigger Shopify's own\n  confirmation mail — flagged to the fleet, Steve's call to keep or delete.\n\nDTD verdict B remains on the record as decided-but-not-executed.\n\nCo-Authored-By: Claude Opus 5 <noreply@anthropic.com>"},{"hash":"f050f30","date":"2026-09-10 10:51:13 -0700","author":"Steve Abrams","subject":"TK-11366: store-wide signal survives cart-mix control - but its direction of harm flips","body":"Banded non-sample US orders by subtotal and compared free-shipping share WITHIN each\nband, before vs after 2026-08-18 (the raw 22%%->0%% was untrustworthy because median\nsubtotal moved $294.57 -> $470.60):\n  $0-100     81%% -> 25%%   (-56 pts)\n  $100-300   41%% ->  0%%   (-41 pts)\n  $300-1000  29%% ->  0%%   (-29 pts)\n  $1000+     15%% ->  0%%   (-15 pts)\nPresent in every band, so not composition. 'Free Shipping (No Tracking)' appeared on\n57 non-sample orders pre, 3 post.\n\nREFRAME: that rate is the SAMPLE-scoped rate. Finding it on full-price orders - 15%% of\n$1000+ wallcovering orders - means a sample rate was reaching full-price carts and DW\nwas eating the freight. So Aug-18 may be a leak in DW's FAVOR being closed, not a\ncustomer-harm regression. Explicitly NOT claiming a second customer-harm incident.\n\nTK-10836 closed (Kelly no-contact, nothing pending). TK-11366 re-scoped from blocked to\ndoing, with one narrow question: was full-price free shipping intended or leaked?\n\nCo-Authored-By: Claude Opus 5 <noreply@anthropic.com>"},{"hash":"a4afa27","date":"2026-09-10 10:18:43 -0700","author":"Steve Abrams","subject":"TK: cap guarded auto-approve at 50/day, falling back to the review card","body":"Steve: \"yes cap it\" — the optional daily cap the dw-trade-auto-approve memo\noffered but left off.\n\nAuto-approve instantly grants a real trade account (trade pricing + free memo\nsamples) to ANY valid submission, and its dedupe only stops SAME-email\nre-submits, so the memo's own \"known limits\" conceded a sprayer using unique\nemails is bounded only by the 5/hr/IP rate limit.\n\nTRADE_AUTO_APPROVE_DAILY_CAP (default 50) bounds it per UTC day. Past the cap a\nsignup falls back to the existing review card rather than being refused, so a\nburst degrades to \"a human looks at it\" instead of dropping applicants on the\nfloor. The count is taken at DISPATCH, not on success — an approve() that later\nfails has still consumed a slot, which errs toward under-approving, the safe\ndirection for a control that exists to bound abuse.\n\nlib/auto-approve-ledger.js persists a per-UTC-day count to\ndata/auto-approve-ledger.json, modeled on lib/mint-ledger.js (the gift-card\nmoney backstop) — same shape, same UTC keying, same restart durability. An\nin-memory counter would reset on every pm2 reload and deploy, i.e. exactly when\na cap must not reset.\n\nDEPLOY SAFETY: added /data/auto-approve-ledger.json to RSYNC_EXTRA_EXCLUDES.\ndeploy.sh runs rsync --delete, so without it a deploy would ship this machine's\ncopy over prod's and RESET the day's count — silently defeating the cap. Also\nadded /data/mint-ledger.json, which had the same gap already: it is the\ngift-card liability cap and was unprotected. Latent only because the file does\nnot exist on prod yet; it would have bitten on the first real mint. Verified by\na live --dry-run against prod: nothing under data/ is touched.\n\nTests: verification/tk11285/auto-approve-cap-test.js, 9 checks — counts, caps at\n>= (not >), survives a restart, a previous day does not cap today, the default\nis finite, and the REAL ledger is never written (the test points the module at a\nthrowaway path via AUTO_APPROVE_LEDGER_PATH and cleans up in a finally).\nOff-by-one sensitivity demonstrated: with > instead of >=, a 4th auto-approve\nslips through at cap=3.\n\nSuite: trade-approval 27/27, commitRows 9/9, sendonly 7/7, cap 9/9, selftest pass.\n\nNOT DEPLOYED. Gate 1 of the auto-approve memo is still Steve's call, and that\ndeploy would turn auto-approve ON in production.\n\nCo-Authored-By: Claude Opus 5 <noreply@anthropic.com>"},{"hash":"c90789d","date":"2026-09-10 09:52:31 -0700","author":"Steve Abrams","subject":"TK-11361: archive both retired DW Free Samples (fn 01a0475d) discount nodes; drop stale prod-validation assertions","body":"Regios (title 'Free sample') owns the live sample entitlement per DEPLOY.md Option C; the DW Function nodes granted nothing on 40 real orders and were a latent combinable-discount landmine. production-validation.js no longer asserts the archived node exists; the dw-free-samples-fn-guard canary now owns the no-re-arm invariant.\n\nCo-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>\n\nClaude-Session: https://claude.ai/code/session_01Mem2YKvcU5UKmRfDtbVK2Q"},{"hash":"079d720","date":"2026-09-10 09:46:33 -0700","author":"Steve Abrams","subject":"TK-10836: DTD verdict B — stage the ship-it remedy (draft order + one true note)","body":"DTD panel 2026-09-10: 7/7 unanimous for B (ship the samples) over A (email her\nanother checkout instruction), C (write off) or D (diagnose first). Reviewer\ndissented for a confirm-first hybrid; three of its five objections failed on\ncheck — notably 'there may be no shippable address', refuted: both abandoned\ncheckouts carry 2300 Sun Valley Drive, Ann Arbor MI 48108, entered twice.\n\nscripts/ship-kelly-samples.mjs — builds a $0 DRAFT order (not a live order, so\nthe final customer-facing act stays human) with the exact 3 swatches from her own\nabandoned cart 34015098110003 (DWCC-600006/600045/600128-Sample), her recorded\naddress, $0 shipping. Dry-run default; undo = delete the draft.\n\nsend-kelly-shipped-note.js — the ONE short true note, to send only AFTER the draft\nis completed. No code, no checkout instruction, no re-marketing, and\nno_source_tag:true so George's 'From job:' banner (TK-11365) stays out of customer\nmail — it leaked into all 4 of the 2026-09-02 sends.\n\nBoth blocked from agent execution by the auto-mode classifier; staged for Steve.\n\nCo-Authored-By: Claude Opus 5 <noreply@anthropic.com>"},{"hash":"b2704f9","date":"2026-09-10 09:41:31 -0700","author":"Steve Abrams","subject":"TK-11366: read-only app forensics - recover install order, surface shipping-scoped apps","body":"AppInstallation exposes no createdAt, so timestamps are unavailable. Worked around it:\nShopify installation IDs are broadly monotonic, so sorting by numeric id recovers\ninstall ORDER for free. 130 installations enumerated and ranked.\n\nApps holding shipping/delivery/checkout/cart-transform scopes, newest first:\nBlockify: Fraud Filter (cart_transforms), two DW-owned custom tokens carrying\nwrite_shipping + write_delivery_customizations + write_delivery_option_generators,\ncreativehub, Specsheet, DropdownVendortoNewSku, n8n, Gelato, Pixels, Kickflip,\nPrintful, Regios Discounts.\n\nTop lead, explicitly CORRELATION NOT CAUSATION: Blockify is the 2nd-newest install and\nthe local blockify-allowlist-canary records Steve declining Blockify Premium on\n2026-08-18 - the exact onset date of the flip. Mechanism gap is real: Blockify is\nfraud/geo-blocking and cart_transforms alter cart lines, not delivery-option ordering.\nSent to the contrarian before being treated as a lead.\n\nRead-only throughout.\n\nCo-Authored-By: Claude Opus 5 <noreply@anthropic.com>"},{"hash":"3c7bad1","date":"2026-09-10 09:35:29 -0700","author":"Steve Abrams","subject":"TK: guarded auto-approve trade signups on submit (email-valid + dedupe guardrails; staff FYI; legacy card fallback)","body":"Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>\nClaude-Session: https://claude.ai/code/session_017oFcLBoyTUxGQP7xnXtb9S"},{"hash":"52e57ab","date":"2026-09-10 09:31:47 -0700","author":"Steve Abrams","subject":"TK-11366: DTD verdict - A-prime (close the $45 gap first, then pull carriers); flip is store-wide","body":"Panel A=5 / B=1 / C=1, reviewer Cody non-voting.\n\nPure A rejected on measured evidence: Free Shipping caps at $45, Priority Sample Only\nat $20, so a live 12-sample $51 cart returns ONLY carrier rates. Deactivating UPS+FedEx\nwould leave that cohort with zero rates and a dead checkout. Every A-voter assumed a\nfallback to the $24.95 flat; the risk lens named it as the fatal precondition and voted\nyes without checking it.\n\nPure B rejected too: open-ended 'diagnose first, change nothing' is not the right\nprinciple; verify-before-touching is, and that is minutes.\n\nScope test added: the flip is STORE-WIDE, not sample-specific. Non-sample full-price\nabandoned carts went 22%% -> 0%% recording $0.00, median default shipping $41.93 ->\n$60.08 (+43%%). So A-prime is mitigation covering ~2.6%% of the surface, not the fix.\n\nAll steps are live Shopify shipping-config writes: drafted to pending-approval, not run.\n\nCo-Authored-By: Claude Opus 5 <noreply@anthropic.com>"},{"hash":"07e8ba5","date":"2026-09-10 09:23:12 -0700","author":"Steve Abrams","subject":"TK-11366: ROOT CAUSE FOUND - checkout's default shipping option flipped ~2026-08-18","body":"Ran the counterfactual population my order-only cohorts could not see (Cody's original\nobjection): abandoned checkouts, which record what the shopper was HANDED, not what they\nchose.\n\nUS sample-only abandoned checkouts recording $0.00 shipping:\n  Jun 1-Aug 17: 24/26 (92%%)\n  Aug 18-Sep 10: 2/56 (4%%)\n  September: 0 of 17\nPaid amounts cluster on UPS carrier rates: $23.35 x17, $23.13 x16.\n\nDecisive because /cart/shipping_rates.json still returns 'Free Shipping (No Tracking)'\n$0.00 listed FIRST. The rate is OFFERED but no longer DEFAULTED. Offered != defaulted,\nand that one distinction explains every observation at once.\n\nMeasured impact: 51 orders since Aug 18 that qualified for $0.00 paid anyway\n($1,120.10, avg $21.96); 54 of 56 abandoned checkouts carry a paid line; abandonment\nvolume more than doubled 26 -> 56.\n\nCorrects me: I claimed Kelly's $24.95 was a self-selected upgrade. Not supported - her\ncheckout is in the post-flip population. Cody was right in substance.\n\nAlso corrects the record: shopifyFunctions is APP-SCOPED, so my earlier 'no rate-hiding\nfunction exists' proved nothing; a function owned by another app is invisible to us.\n\nWhy the default flipped is NOT determined. Inspecting/changing the checkout default is\ncustomer-facing money config and stays gated.\n\nCo-Authored-By: Claude Opus 5 <noreply@anthropic.com>"},{"hash":"d0135eb","date":"2026-09-10 09:22:49 -0700","author":"auto-commit-fleet","subject":"auto-data-snapshot: 2026-09-10T09:22:46 (1 data files) — verification/tk10836/abandoned-default.json","body":""},{"hash":"86dd5a0","date":"2026-09-10 09:04:30 -0700","author":"Steve Abrams","subject":"TK-11377: stop my own test being a fourth writer of the live store","body":"Cody's DTD dissent caught this and it was right. The previous revision of\nverification/tk11285/sendonly-filter-test.js wrote its fixture straight to\ntrade.APPS_PATH - the REAL data/trade-applications.jsonl - and restored it\nafterwards with no try/finally. One thrown execFileSync would have left six\nsynthetic rows (PENDING-A@example.com, APPROVED-A@example.com, ...) sitting in\nthe live applications store.\n\nThat is the exact hazard class this ticket and TK-11285 exist to close, in the\ntest written to prove the fix. It survived only because .deploy.conf excludes\nboth /verification and /data/trade-applications.jsonl from rsync, i.e. on a\nhuman remembering to keep an exclude list correct forever. scripts/selftest.js,\nwhich I edited in the same commit, already did it correctly with --file into a\ntmpdir fixture; I had the right pattern in front of me and did not use it.\n\nNow drives the CLI's own --file flag against a throwaway mkdtemp fixture,\ncleaned up in a finally. Adds a 7th check asserting the real store path is never\neven referenced in the output.\n\nProof: hashed data/trade-applications.jsonl before and after a run - byte\nidentical. Still non-tautological: reverting the filter fails 3/7 (approved and\nrejected both get emailed).\n\nSuite on master: trade-approval 27/27, commitRows 9/9, sendonly-filter 7/7,\nDRY_RUN selftest pass.\n\nCo-Authored-By: Claude Opus 5 <noreply@anthropic.com>"},{"hash":"b0c4280","date":"2026-09-10 08:49:43 -0700","author":"Steve Abrams","subject":"TK-11377: --send-only must not email an already-decided application","body":"Found while measuring whether TK-11377 had any live impact. The reconciliation\ngap itself is latent (0 affected rows on prod), but the same filter carries a\nlive hazard: scripts/recover-stuck-apps.js's --send-only selected on\nlink_status + shopify_customer_id + !recovery_emailed with NO status check.\n\nMeasured on prod 2026-09-10: 20 rows matched, and 4 of them are already-approved\nREAL designers decided 2026-09-03 (kmdesigncompany.com, veronica-valencia.com,\nlenorekingluxuryinteriors.com, bonvivantinteriors.com). One\n`DRY_RUN=0 ... --send-only` would have sent those 4 an \"your account is ready,\nactivate it\" letter a week after they were approved and already emailed, plus 4\n\"[Now approvable]\" notices to the office for applications it had already\napproved - 8 wrong emails to real people.\n\nBoth letters are wrong for a decided application, so the fix is to select only\npending ones. Skipped rows are printed rather than silently dropped, so an\noperator can see what was held back and why.\n\nPre-existing; NOT introduced by the TK-11285 deploy, which does not touch this\npath and is landed and verified.\n\nTests: verification/tk11285/sendonly-filter-test.js, 6 checks shaped from the\nreal prod rows, proven non-tautological (reverting the filter fails 3/6 and\nwould email approved-a, approved-b and rejected-a). selftest gains a decided-row\ncase; its assertion is loosened off the exact label text and onto the count plus\na real behavioural check of what was actually SENT.\n\nAgainst a copy of live prod data: old filter 20 emails (4 wrong), new filter 16.\n\nSuite: trade-approval 27/27, commitRows 9/9, sendonly-filter 6/6, selftest pass.\nLocal branch only - not merged, not deployed.\n\nCo-Authored-By: Claude Opus 5 <noreply@anthropic.com>"},{"hash":"ff9bd17","date":"2026-09-10 08:42:55 -0700","author":"Steve Abrams","subject":"TK-10836: live checkout render blocked by mandatory customer login; fix units-vs-lines metric error","body":"Live render (authorized): rebuilt Kelly's exact cart on the storefront. All three\nroutes to checkout redirect to a mandatory Shopify customer sign-in. Passing it\nrequires creating/logging into a customer account (identity, hard-gated), so stopped.\nNo checkout created, no email entered -> no abandoned-checkout record, no abandoned-cart\nemail. Actual externality zero.\n\nFindings anyway:\n- The free-sample comp is ACCOUNT-GATED, not automatic. Signed out, 6 sample units\n  price $25.50, not $0.00.\n- Cart copy states the rule: '10 samples or fewer ship free (no tracking); over 10\n  units include a shipping charge' = the TOTAL_PRICE <= $45.00 condition.\n\nCorrected an error in my own cohorts: they counted LINE ITEMS, not UNITS. Recounted\nover 502 US sample-only orders since 2026-06-01:\n- The >10-unit rule does NOT explain the paid orders. 62 paid orders were under $45\n  list, most 1-3 units. #33033 shipped ONE $4.25 swatch, comped to $0.00, on $23.13\n  UPS Ground. That is not a customer preferring tracking.\n- Median units per order = 1 in BOTH windows, so cart size did not change.\n- Break is starker after the fix: Jun1-Aug17 97%% free (n=405) vs Aug18-Sep10 43%% (n=97).\n\nSix mechanisms now falsified. Root cause open; decisive observation is one screen\nbehind the login.\n\nCo-Authored-By: Claude Opus 5 <noreply@anthropic.com>"},{"hash":"fea5a31","date":"2026-09-10 08:19:21 -0700","author":"Steve Abrams","subject":"TK-11285: deploy runbook with pre-deploy backup, verify, and rollback","body":"Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>\nClaude-Session: https://claude.ai/code/session_01NEDp3MXiKVJ5GDKaQCHZon"},{"hash":"a95eaa2","date":"2026-09-10 08:18:41 -0700","author":"Steve Abrams","subject":"TK-11285: stop the deploy itself from destroying the live applications","body":"Found while preparing the approved deploy. deploy.sh runs `rsync -az --delete`\nand .deploy.conf carried NO RSYNC_EXTRA_EXCLUDES, so a deploy would have\noverwritten prod's trade-applications.jsonl (19064 bytes, 23 rows, 16 pending)\nwith this machine's fixture copy (727 bytes, 2 test rows), and deleted six\nprod-only files outright including both recover-stuck-apps rollback maps and\nthe live trade-notify-failures.jsonl.\n\ndeploy.sh's delete-guard would have caught it and aborted rather than shipped\nthe damage - but the deploy would simply have been blocked until someone added\nthese, and a DEPLOY_ALLOW_DELETE=1 override would have destroyed the data.\n\nAdds the surgical excludes for state the live server and the operator CLIs\nwrite. Verified by a real --dry-run --itemize-changes against prod: nothing\nunder data/ is touched, the only deletion is a stale lib/*.js.bak, and the\ndelete-guard now returns PASS.\n\nAlso excludes /verification - local test evidence prod has no use for, and its\npaths match the guard's own risky-pattern regex (\"verificat.*\\.json\"), which\nwould trip an abort on files that are merely being added.\n\nDEPLOY.md gains the runbook rule Steve chose for the third writer: never run\nnpm test / npm run selftest against the prod checkout. selftest snapshots the\nreal store, writes its own rows into it, and restores the snapshot on exit, not\ngated by DRY_RUN - a ~480ms window that silently destroys whatever arrived.\n\nCo-Authored-By: Claude Opus 5 <noreply@anthropic.com>\nClaude-Session: https://claude.ai/code/session_01NEDp3MXiKVJ5GDKaQCHZon"},{"hash":"534d1f6","date":"2026-09-10 08:11:03 -0700","author":"Steve Abrams","subject":"TK-11285: correct my own overclaim in the f9de58c write-up","body":"Cody's re-gate returned FIX FIRST (narrowed): the bidirectional race fix is\ngenuine and it independently re-verified it, but I mischaracterized the\nleftover link_status/link_error problem. Verified the correction myself.\n\nf9de58c's message attributes the self-contradictory row (status:'approved' +\nvalid shopify_customer_id + link_status:'unlinked' + stale link_error) to the\nclobber. That is overclaimed. The same shape appears with ZERO concurrency:\napprove() resolves the customer through its findCustomerByEmail fallback and\nstamps shopify_customer_id, but never reconciles link_status/link_error. It\nneeds no second writer and predates all TK-11285 work.\n\nSo the `self_contradictory` assertion in reverse-race.js was not a race\ndetector - it would have fired solo too. Renamed to\ncontradictory_shape_not_race_specific, explicitly documented as not evidence of\nthe race, and the harness gained a --solo control that demonstrates the\ncontradiction with no concurrent writer at all\n(verification/tk11285/reverse-race-solo.json).\n\nWhat actually IS the clobber, and what f9de58c genuinely fixes: a value that was\n'linked' ON DISK being reverted to 'unlinked' by the service's stale row\nsnapshot. Those assertions are now gated on a second writer having run, so they\ncan no longer be satisfied by the solo gap. Race mode still reports CLAIM NOT\nREPRODUCED after the fix.\n\nThe reconciliation gap is split to TK-11377 rather than folded in here - the\nnaive repair (set link_status='linked') would make already-approved designers\neligible for recover-stuck-apps.js --send-only's activation letter, which is a\nproduct decision, not a mechanical fix. Non-blocking for this deploy.\n\nNo production code changed in this commit - evidence and wording only.\n\nCo-Authored-By: Claude Opus 5 <noreply@anthropic.com>\nClaude-Session: https://claude.ai/code/session_01NEDp3MXiKVJ5GDKaQCHZon"},{"hash":"f9de58c","date":"2026-09-10 08:02:42 -0700","author":"Steve Abrams","subject":"TK-11285: the race was bidirectional — stop the service clobbering too","body":"Cody (contrarian) returned FIX FIRST on my bef6f4f/f849a46 work and was right.\nI fixed recover-stuck-apps.js clobbering the service, but not the service\nclobbering recover-stuck-apps.js. Verified independently before acting.\n\ncheckpointApproval() re-read the FILE (so new applications survived) but `app`\nis a pre-await snapshot of the ROW, and `{...latest[index], ...app}` re-asserted\nevery field it held. So while approve() awaited Shopify and George, a concurrent\nlink by recover-stuck-apps.js was reverted: the row ended up status:'approved'\nwith a valid shopify_customer_id but link_status:'unlinked' and a stale\nlink_error — a self-contradictory record, and the --send-only filter then skips\nit, so a designer can silently never receive the activation letter.\n\nReproduced 3/3 with my own harness (verification/tk11285/reverse-race.js,\nindependent of Cody's), before: \"CLAIM CONFIRMED\", after: \"CLAIM NOT REPRODUCED\".\n\nFix merges only the 7 fields a decision actually owns (status, decision,\ndecided_at, approval_error, assigned_rep, shopify_customer_id,\napproval_progress), enumerated exhaustively from every `app.X =` in approve()\nand reject(). Explicit key test rather than a bare spread — the same\nundefined-still-spreads trap I hit in f849a46. approve() and reject() both\nfunnel through this one function, so it is a single-point fix.\n\nRegression after changing the service hot path:\n  trade-approval-test.js  27/27 PASS 0 FAIL   (the accepted artifact, unchanged)\n  commit-rows-test.js      9/9  PASS\n  forward race             3/3 NO LOSS, control 3/3 NO LOSS\n  reverse race             3/3 NOT REPRODUCED\n  DRY_RUN selftest         all pass\n\nKnown cosmetic residual: recover-stuck-apps.js does not clear a stale link_error\nwhen a link later succeeds. Pre-existing, not introduced here, left alone.\n\nLocal artifact only — no deploy, no push, no live writes.\n\nCo-Authored-By: Claude Opus 5 <noreply@anthropic.com>\nClaude-Session: https://claude.ai/code/session_01NEDp3MXiKVJ5GDKaQCHZon"},{"hash":"944582a","date":"2026-09-10 08:00:01 -0700","author":"Steve Abrams","subject":"TK-10836: contrarian overturns my Option D headline; correct it and kill 3 more hypotheses","body":"Cody was right that 158/192 = 82%% smeared a cliff. Re-bucketed from my own data\n(comped, US, 1-4 sample lines): Jul 100%%, Aug 65%%, Sep 35%%; Aug 1-17 100%% ->\nSep 1-10 35%%. The collapse starts the week Kelly checked out and is still worsening.\n\nSURVIVES: the $0.00 rate WAS on Kelly's menu. Untruncated Samples-profile dump shows\n$0.00 / $24.95 / ups_shipping / fedex all active in Domestic; both priced conditions\nhave a $0.00 lower bound; no customization or function hides it; 12/12 PAID comped\norders resolve to Kelly's same profile. Reading availability off a selected shipping\nline was invalid.\n\nDOES NOT SURVIVE: my 'therefore benign' conclusion. Default/steering never established.\n\nKILLED (do not re-run): profile-leak (ups/fedex are inside the Samples profile),\nShop Pay steering (88%% Shop-tagged in both windows, free%% fell in both groups),\nweight gate, $0-subtotal suppression, multi-profile mismatch.\n\nTK-11366 escalates: ~27 customers in 3 weeks paid $16-25 on 'free' samples, root\ncause open. Volume rose ~6x in the same window - population change not excluded.\n\nRead-only throughout. Nothing sent, created, or changed on Shopify.\n\nCo-Authored-By: Claude Opus 5 <noreply@anthropic.com>\nClaude-Session: https://claude.ai/code/session_01CD85VaG9BZGi3NjWGbKqvX"},{"hash":"f849a46","date":"2026-09-10 07:54:15 -0700","author":"Steve Abrams","subject":"TK-11285: fix a clobber bug in my own commitRows merge, and test it","body":"Self-review caught a real defect in bef6f4f. The customer_id_conflict branch did\n`patch.shopify_customer_id = undefined` to skip the field, but an undefined value\nstill spreads: {...current, ...patch} then ERASES the id the service resolved, and\nJSON.stringify drops the key entirely. The guard meant to protect the service's\nwrite was deleting it. Use `delete` instead.\n\nAdds verification/tk11285/commit-rows-test.js — 9 focused checks on the merge:\nconcurrent intake survives, a service decision is never overwritten, the service's\ncustomer id wins a conflict, receipts are kept so no duplicate letter, a vanished\nrow is not resurrected, untouched rows stay byte-identical.\n\nProven non-tautological: reintroducing the bug fails 2/9 (cust=undefined, key\nerased); with the fix 9/9 PASS exit 0.\n\nAlso gates the CLI behind require.main === module so commitRows is testable.\nCLI behaviour unchanged.\n\nCo-Authored-By: Claude Opus 5 <noreply@anthropic.com>\nClaude-Session: https://claude.ai/code/session_01NEDp3MXiKVJ5GDKaQCHZon"},{"hash":"251724b","date":"2026-09-10 07:49:03 -0700","author":"Steve Abrams","subject":"TK-10836: Option D complete — the $0.00 free-shipping rate WAS available on Kelly's cart","body":"Overturns this ticket's own 2026-09-10 reversal (ec11bce).\n\n- Option D was never actually blocked: SHOPIFY_FULL_ACCESS_TOKEN (...2ea5) already\n  carries read_shipping. The ACCESS_DENIED cited in the memo was a wrong-token artifact.\n- Kelly's 3 sample variants all sit in delivery profile 'Samples - Free Shipping (No\n  Tracking)'. That profile's Domestic zone offers BOTH $0.00 Free Shipping (No Tracking)\n  (TOTAL_PRICE 0-45) and $24.95 Priority Sample Only (TOTAL_PRICE 0-20). A $0.00 subtotal\n  satisfies both. No delivery customization, no rate-hiding function, no custom carrier.\n- Empirical falsification: 192 fully-comped US sample-only orders -> 158 shipped $0.00.\n  Kelly-shaped (2-4 comped sample lines) 53 -> 38 free. Michigan 2/2 free. A 3-line comped\n  order shipped free on 2026-08-20, four days before her checkout.\n- An abandoned checkout records the SELECTED shipping line, not the AVAILABLE set. The\n  reversal read availability off a selection. $24.95 is a self-selected upgrade.\n\nRead-only throughout: no order, draft order, discount, shipping-config or email write.\n\nCo-Authored-By: Claude Opus 5 <noreply@anthropic.com>\nClaude-Session: https://claude.ai/code/session_01CD85VaG9BZGi3NjWGbKqvX"},{"hash":"bef6f4f","date":"2026-09-10 07:46:56 -0700","author":"Steve Abrams","subject":"TK-11285: close the second-writer race the deploy preflight required","body":"The deploy memo made \"identify all processes writing trade-applications.jsonl\"\na mandatory precondition, because the shipped decision lock only coordinates\ninside the service process. That inventory had never been done. It has one hit:\n\n  scripts/recover-stuck-apps.js\n\nIt rewrites the whole jsonl from a snapshot taken before a batch of Shopify\ncalls and George sends, so its read->write window is minutes. It is not\nhypothetical - it has run in prod twice (data/recovery-20260903T192941.json,\nrecovery-20260903T193258.json) and 20 rows currently qualify for --send-only.\n\nReproduced with both sides running real production code in separate processes\n(verification/tk11285/race.js; only lib/email + lib/shopify stubbed, zero\nexternal calls). Deterministic, 3/3:\n\n  - a trade application submitted during the window is silently destroyed\n  - a COMPLETED approval reverts to pending after approve() returned\n    ok:true/\"approved\", the applicant was emailed and the customer was already\n    tagged trade_approved - store and Shopify silently disagree, which is the\n    exact invariant this ticket exists to enforce\n\nControl (same code, no second writer) shows NO LOSS, so the loss is caused by\nthe second writer, not by the fix.\n\nFix applies the pattern lib/trade.js already uses in checkpointApproval: never\nwrite a stale snapshot. Re-read at write time and merge only the fields this\nscript owns. Link fields are dropped for a row the service has since decided;\nemail receipts are always kept so a later run cannot re-send a duplicate letter.\n\nAfter: 3/3 NO LOSS, the script still links and still stamps all 6 receipts.\nRegression: trade-approval-test.js 27/27 PASS 0 FAIL, DRY_RUN selftest all pass.\n\nLocal artifact only - no deploy, no push, no live writes.\n\nCo-Authored-By: Claude Opus 5 <noreply@anthropic.com>\nClaude-Session: https://claude.ai/code/session_01NEDp3MXiKVJ5GDKaQCHZon"},{"hash":"33dd248","date":"2026-09-10 07:45:09 -0700","author":"Steve Abrams","subject":"TK-11361: soften attribution claim to match evidence strength","body":"Codex second-model pass flagged that 'neither node has ever produced a discount\napplication' is an absolute claim resting on title attribution + chronology, not\non a provenance link - Shopify does not expose the source node id on a discount\napplication. Reworded to 'no evidence either DW node contributes to any observed\norder', added an explicit proven-vs-inferred split, and scoped the result to the\nmeasured window rather than stating it universally.\n\nThe headline answer is unchanged and robust: the cap held at 3, and it holds even\nif a DW node also emits a 'Free sample' title, since counts and amounts would\nstill have moved.\n\nCo-Authored-By: Claude Opus 5 <noreply@anthropic.com>\nClaude-Session: https://claude.ai/code/session_012F3D7ErxQBRwTvVtz3kVXv"},{"hash":"7e96b04","date":"2026-09-10 07:42:37 -0700","author":"Steve Abrams","subject":"TK-11361: Option A measured — cap holds at 3, duplicate is inert (DW function grants nothing)","body":"Steve approved Option A (measure first). Measured against real orders instead of\na synthetic cart, so no test customer or cart was needed - zero writes.\n\nNatural experiment across the 2026-09-02 introduction of the second node:\n  before either node existed (150 orders): max 3 'Free sample' apps/order, 0 over-discounts\n  both nodes ACTIVE      (100 orders): max 3 'Free sample' apps/order, 0 over-discounts\nAnswer to '3 or 6?': 3. No over-entitlement, no double-discount, no money impact.\n\nRoot reason found: neither DW node has ever produced a discount application.\nThe live free-samples engine is Regios Discounts (fn cd0a910b, since 2024-07-25)\n- proven by 'Free sample' applications appearing 12x in 2026-07-15..08-27, before\neither DW node existed. Matches DEPLOY.md: the DW function path is a retired\nalternate, Option C uses variant-scoped Regios.\n\nRisk downgraded to cleanup, but both nodes remain a latent landmine: if fn\n01a0475d is ever repaired, two ACTIVE combinable nodes go live at once.\nRecommendation revised to archive BOTH (gated), and update\nscripts/production-validation.js which asserts on the node title.\n\nCo-Authored-By: Claude Opus 5 <noreply@anthropic.com>\nClaude-Session: https://claude.ai/code/session_012F3D7ErxQBRwTvVtz3kVXv"},{"hash":"f5076d0","date":"2026-09-10 07:36:24 -0700","author":"Steve Abrams","subject":"TK-11361: confirm duplicate DW Free Samples automatics; withdraw the false retraction","body":"Both DiscountAutomaticApp nodes (1464777277491 'DW Free Samples (auto)',\n1465258770483 'DW Free Samples') are ACTIVE on function 01a0475d-..., verified\nthree ways in one session. A concurrent pass had marked this NOT REPRODUCIBLE;\nthat read used automaticDiscountNodes, the legacy connection, which does not\nreturn app/function-backed discounts. discountNodes returns all five.\n\nBoth are discountClass PRODUCT with combinesWith.productDiscounts=true, so they\nmay stack. Plausible failure is 6 free samples instead of 3. Cannot be settled\nread-only - needs one controlled checkout, which is gated.\n\nRead-only throughout. No discount modified.\n\nCo-Authored-By: Claude Opus 5 <noreply@anthropic.com>\nClaude-Session: https://claude.ai/code/session_012F3D7ErxQBRwTvVtz3kVXv"},{"hash":"ec11bce","date":"2026-09-10 07:35:04 -0700","author":"Steve Abrams","subject":"TK-10836: REVERSE the close verdict — Kelly's own checkout shows $24.95 shipping","body":"The contrarian gate was right: nobody had checked Kelly's actual cart.\n\nAbandonedCheckout/34015098110003 (2026-08-24, kyounge@umich.edu, Ann Arbor MI):\n3 samples, items subtotal $0.00, discount $12.75 already applied, tax $0.00,\nTOTAL $24.95 — i.e. $24.95 of pure shipping. Abandoned. A second 7-sample\ncheckout the same day ($59.90) was also abandoned.\n\nThe earlier 'free shipping is available' survey proved the $0 method exists in\ngeneral, not that it was offered on her cart. And 3FREE is irrelevant to the\nblocker: it discounts ITEMS only, her items were already $0.00, so it cannot\nreduce a shipping line. The reply we sent 4x told her the $25 'won't apply'.\n\nTicket stays blocked, blocker re-pointed. Nothing sent, nothing written.\n\nCo-Authored-By: Claude Opus 5 <noreply@anthropic.com>\nClaude-Session: https://claude.ai/code/session_01Jeg9TkFJqpiAEtcNva1mUb"},{"hash":"a69c55f","date":"2026-09-10 07:29:25 -0700","author":"Steve Abrams","subject":"TK-10836: overturn contrarian's blocking hole on the full 3FREE cohort","body":"Cody red-teamed the close verdict and blocked it, arguing the emailed\nfree-shipping promise is contradicted by 'the two most comparable orders'\n(#32937 NY, #32914 MA — 3FREE applied, $24.95 charged).\n\nRan the decisive falsification test over 459 orders / 338 sample-only:\nof the 10 sample-only orders that actually had 3FREE applied, 8 shipped\n$0.00 on 'Free Shipping (No Tracking)' — incl. #32862 in NY, the same\nstate as one of Cody's two. 3FREE does not suppress the free method; the\nfree option is a shipping RATE, not a stacked shipping discount, so\ncombinesWith.shippingDiscounts:false never bites. Both paid orders chose\nthe 'Priority Sample Only' expedited upgrade.\n\nHole overturned on evidence; CLOSE verdict stands.\n\nCo-Authored-By: Claude Opus 5 <noreply@anthropic.com>\nClaude-Session: https://claude.ai/code/session_01Jeg9TkFJqpiAEtcNva1mUb"},{"hash":"8e95676","date":"2026-09-10 07:27:48 -0700","author":"Steve Abrams","subject":"TK-10836: independent second-session re-verification","body":"Confirms the close-as-superseded verdict on live read-only evidence, and\nrecords one finding that did NOT reproduce so nobody acts on it.\n\n- Kelly's customer record, code 3FREE, /healthz, TK-11114, and the\n  already-superseded approval memo all re-confirmed.\n- Free-shipping promise challenged (order #32937 paid $24.95) then upheld:\n  66 US sample-only orders show a $0.00 'Free Shipping (No Tracking)'\n  method, incl. Michigan order #32954 — the paid lines are chosen upgrades.\n- The 'two ACTIVE DiscountAutomaticApp nodes' finding is NOT reproducible:\n  live read returns one EXPIRED basic discount and zero app discounts.\n\nAdds read-only repro scripts for every claim.\n\nCo-Authored-By: Claude Opus 5 <noreply@anthropic.com>\nClaude-Session: https://claude.ai/code/session_01Jeg9TkFJqpiAEtcNva1mUb"},{"hash":"4d8163c","date":"2026-09-10 07:24:52 -0700","author":"Steve Abrams","subject":"TK-10836: close as superseded — Kelly Paradis already honored via code 3FREE","body":"Read-only re-verification on 2026-09-10 shows the reissue this ticket was\nblocked on was never needed. info@ replied to Kelly with code 3FREE on\n2026-08-25 08:38 PT, before this ticket was even created; TK-10830 re-sent the\nsame code on 2026-09-02. 3FREE is ACTIVE ($15 off, once-per-customer, no end\ndate) and covers 3 x $4.25 samples. Her Shopify customer is tagged\nverified-sample. No inbound complaint since 2026-08-25. The RETAIL_SHARED_CODE\nshared-code path is retired and irrelevant here.\n\nNothing minted, sent, armed, deployed, or written — evidence only.\n\nSpun off TK-11361: two ACTIVE automatic discounts bound to the same\nDW Free Samples function (left untouched, gated).\n\nCo-Authored-By: Claude Opus 5 <noreply@anthropic.com>\nClaude-Session: https://claude.ai/code/session_012F3D7ErxQBRwTvVtz3kVXv"},{"hash":"ba82c82","date":"2026-09-07 10:43:19 -0700","author":"auto-commit-fleet","subject":"auto-data-snapshot: 2026-09-07T10:43:18 (2 data files) — verification/tk11185-reconcile/RECONCILIATION.md verification/tk11185-reconcile/live-read.json","body":""},{"hash":"6d5e374","date":"2026-09-05 08:46:44 -0700","author":"auto-commit-fleet","subject":"auto-data-snapshot: 2026-09-05T08:46:42 (1 data files) — verification/tk11185-reconcile/live-read.json","body":""},{"hash":"b8ee89a","date":"2026-09-05 01:06:31 -0700","author":"auto-commit-fleet","subject":"auto-data-snapshot: 2026-09-05T01:06:29 (1 data files) — verification/tk11185-reconcile/retired-e2e-proof.json","body":""},{"hash":"dc1323f","date":"2026-09-05 00:23:03 -0700","author":"Steve Abrams","subject":"Retire obsolete theme commands that could undo designer signup","body":""},{"hash":"116fc35","date":"2026-09-05 00:04:46 -0700","author":"Steve Abrams","subject":"Serialize trade rejection with approval and preserve concurrent intake","body":""},{"hash":"d291ec9","date":"2026-09-04 23:57:09 -0700","author":"Steve Abrams","subject":"Verify trade entitlement before approval and preserve retry progress","body":""},{"hash":"8a877d4","date":"2026-09-04 23:15:19 -0700","author":"Steve Abrams","subject":"Record independent verification of live designer signup changes","body":""},{"hash":"1e9062c","date":"2026-09-04 23:13:55 -0700","author":"Steve Abrams","subject":"Verify published designer account flow and Shopify login handoff","body":""},{"hash":"45fc36e","date":"2026-09-04 23:09:57 -0700","author":"auto-commit-fleet","subject":"auto-data-snapshot: 2026-09-04T23:09:55 (1 data files) — verification/tk11283/rollback-rehearsal.json","body":""},{"hash":"01593e3","date":"2026-09-04 23:04:42 -0700","author":"Steve Abrams","subject":"Guide designers from full application to account setup and email sign-in","body":""},{"hash":"1944da5","date":"2026-09-03 13:36:31 -0700","author":"steve","subject":"retail-webhook: close double-fire race — in-process in-flight claim before send","body":"Two near-simultaneous customers/create deliveries (Shopify at-least-once) both read\nsample_verify_sent=false and both send a verify letter. Added an in-memory Set claimed\nsynchronously (no await between has() and add(), atomic on the single pm2 fork) and\nreleased in finally, so exactly one letter goes out per burst. Durable metafield still\nowns cross-restart/retry-after-minutes idempotency; failed sends stay un-poisoned.\nProven by a 5-way concurrent + mixed-id + fail-retry test (all pass); codex-reviewed.\n\nCo-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>\nClaude-Session: https://claude.ai/code/session_014PT22KZe3HDFcym6fzZRwD"},{"hash":"90c2cb8","date":"2026-09-03 12:39:20 -0700","author":"Steve Abrams","subject":"TK-11190: harden recover-stuck-apps — fail-loud --apply DRY_RUN guard + idempotent --send-only","body":"Closes the footgun that stamped fake dry-run stub customer ids into the prod jsonl:\na standalone `node recover-stuck-apps.js --apply` inherits DRY_RUN=1 (config reads it only\nfrom process.env; pm2 env injection doesn't reach a bare node invocation), and the APPLY\nbranch wrote unconditionally.\n\n- FAIL-LOUD: --apply now refuses (clear message + exit 1) while config.DRY_RUN is on, BEFORE\n  any Shopify call or jsonl write. Real runs must set DRY_RUN=0.\n- --send-only: emails the activation + office letters for apps that are LINKED but not-yet-\n  emailed, tracked by a recovery_emailed flag so re-sends are idempotent (no restore-and-\n  reapply dance). Emails stay DRY_RUN-safe — a DRY preview does NOT consume the flag, so a\n  later real run still sends. Shared sendForApp() sets the flag ONLY on a real send.\n- selftest (c4): spawns the script to prove the guard trips (exit non-zero + refuses) and\n  --send-only targets only linked+not-emailed without consuming the flag on a dry preview.\n\nFull DRY_RUN suite green. LOCAL only.\n\nCo-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>\nClaude-Session: https://claude.ai/code/session_01X3co77k7JzTkAJRdemt6Ru"},{"hash":"164ab38","date":"2026-09-03 11:48:54 -0700","author":"Steve Abrams","subject":"TK-11190: recovery tooling for pre-fix stuck trade applications (link retroactively)","body":"Designers who applied BEFORE the TK-11185 fix are status=pending + shopify_customer_id:null\n→ un-approvable. This recovers them by applying the new-flow linkage retroactively.\n\n- scripts/recover-stuck-apps.js: DRY-RUN by default; --apply links (findOrCreateCustomer →\n  stamp id + link_* + recovered_at). Idempotent (skips already-linked), batch-bounded\n  (--limit, default 25), reversible (backs up the jsonl + writes data/recovery-<ts>.json map;\n  --rollback deletes exactly the customers WE created). Emails are separable + OFF by default\n  (--send-emails); option (b) --auto-approve exists but is NOT recommended (grants trade\n  pricing without human moderation). Verified locally: dry-run, apply (backup+map), rollback.\n- lib/email.js: designerAccountReadyEmail — recovery-cohort activation letter (\"account is\n  ready, sign in via one-time code\"); does NOT claim trade pricing is granted yet.\n\nFull DRY_RUN suite green. LOCAL only — the actual recovery run is Steve-gated (customer-facing).\n\nCo-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>"},{"hash":"5e609e8","date":"2026-09-03 11:32:20 -0700","author":"Steve Abrams","subject":"TK-11185: add Gate 2 theme PUT + rollback scripts (cwd-independent, self-verify main theme)","body":"Runnable .mjs equivalents of the memo's Gate 2 pastes for the approved live theme write:\n- scripts/gate2-theme-put.mjs: confirms 145121607731 is the live/main theme (aborts on\n  mismatch), reads the staged patched snippet, PUTs snippets/dw-signin-modal.liquid, prints HTTP.\n- scripts/gate2-theme-rollback.mjs: re-PUTs the 20260903T181955 pre-change backup, prints HTTP.\nToken read inside Node from secrets-manager/.env (no shell quoting), store designer-laboratory-sandbox, API 2024-10.\n\nCo-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>\nClaude-Session: https://claude.ai/code/session_01X3co77k7JzTkAJRdemt6Ru"},{"hash":"6e51634","date":"2026-09-03 11:20:58 -0700","author":"Steve Abrams","subject":"TK-11185: back up live dw-signin-modal snippet + stage patched two-step OTP success handler","body":"Prep for the approved Gate 2 live theme PUT (theme 145121607731, main). Backs up the\ncurrent live snippets/dw-signin-modal.liquid and stages the patched version that replaces\nthe \"in review\" dead-end with renderTradeSuccess() — two-step OTP sign-in to /account for\nlogged-out applicants, \"Application received\" confirmation for the logged-in path.\n\n- theme-backups/live-snapshots/snippets__dw-signin-modal.liquid.20260903T181955.bak (live backup)\n- theme-proposals/loggedin-trade-entry/dw-signin-modal.PATCHED-tk11185.liquid (staged patch)\n\nThe live PUT itself is handed to Steve as a `!` paste (customer-facing write).\n\nCo-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>\nClaude-Session: https://claude.ai/code/session_01X3co77k7JzTkAJRdemt6Ru"},{"hash":"eba3bdc","date":"2026-09-03 11:17:19 -0700","author":"Steve Abrams","subject":"TK-11185: rate-limit public /trade/apply (per-IP) — close the write_customers abuse vector","body":"/trade/apply now server-side creates a real Shopify customer per POST (write_customers),\nso an unthrottled public endpoint could be sprayed to pollute the customer table / spam\nthe designer-welcome email. Extract the inline webhook limiter into a shared util and\nthrottle both endpoints consistently.\n\n- lib/rate-limit.js: createRateLimiter({windowMs,max}) — per-key sliding window, self-pruning\n  map (extracted verbatim-behavior from the inline webhook limiter).\n- server.js: webhook now uses webhookLimiter (WEBHOOK_RATE_MAX/min, unchanged behavior);\n  /trade/apply gains tradeApplyLimiter (429 on trip) via the same util + a shared clientIp().\n- lib/config.js: TRADE_APPLY_RATE_MAX=5, TRADE_APPLY_RATE_WINDOW_MS=1h (5 applications/IP/hr —\n  a real designer applies once; generous but blocks spray).\n- selftest.js (c3): limiter allows N in-window, TRIPS N+1, buckets are per-IP independent,\n  and the config default is 5/hour. Full suite green in DRY_RUN.\n\nCo-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>\nClaude-Session: https://claude.ai/code/session_01X3co77k7JzTkAJRdemt6Ru"},{"hash":"31cd4dd","date":"2026-09-03 11:12:01 -0700","author":"Steve Abrams","subject":"TK-11185: server-side find-or-create customer at trade-apply time (born linked, always approvable)","body":"Public trade applications carried no shopify_customer_id, so approve() hard-failed\ncannot_resolve_customer → designers \"filled it out and nothing happened.\" DW is on\nNEW_CUSTOMER_ACCOUNTS (passwordless OTP — verified live 2026-09-03), so the account\ncannot be carried through the register page; it is minted server-side via the Admin API.\n\n- lib/shopify.js: createCustomer() (GraphQL customerCreate, DRY_RUN-safe, REST-numeric id\n  from GID tail, handles email-taken re-resolve + phone-format retry, deps seam for tests)\n  and findOrCreateCustomer() (find first, create only if none — linkage by resolved id).\n- lib/trade.js: applyAndLink() find-or-creates + stamps shopify_customer_id onto the\n  persisted application, records link_status/link_error, and graceful-degrades (still\n  persists unlinked if create fails — never a black hole).\n- server.js /trade/apply: awaits applyAndLink before firing the office notify, so a linked\n  app never yields an un-approvable review card; applicant still sees success on failure.\n- shopify/staged/trade-account-ux-20260828/snippets/dw-trade-apply.liquid: staged snippet\n  replacing the \"Application received\" dead-end with two-step OTP account sign-in (/account);\n  logged-in path preserved. NOT deployed — live theme PUT is Steve-gated.\n- selftest.js (c2): create+link, existing reuse, create-failure degrade, approve() resolving,\n  plus createCustomer taken/phone-retry branch coverage. Full suite green in DRY_RUN.\n\nContrarian panel: SHIP IT (4-1), dissent (untested error branches) addressed before commit.\n\nCo-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>\nClaude-Session: https://claude.ai/code/session_01X3co77k7JzTkAJRdemt6Ru"},{"hash":"48d0832","date":"2026-09-02 15:33:44 -0700","author":"Steve Abrams","subject":"TK-11120: definitive checkout test — verified-sample caps at 3 free samples, not 5","body":"Live browser checkout (logged-in verified-sample customer, 5 swatches): $21.25 -> $8.50,\ni.e. only 3 free / 2 charged. The DW Free Samples function hard-caps at 3. The retail\napology email promised 5 -> overpromise. Tooling: browser-cart-setup.js, verify-checkout.js.\n\nCo-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>\nClaude-Session: https://claude.ai/code/session_017j4qS38tWq21qYdjxMcFTy"},{"hash":"d1ffb0f","date":"2026-09-02 15:14:47 -0700","author":"Steve Abrams","subject":"TK-11120 Option B: verify page shows the token-carried sample count (5 for the retail cohort)","body":"Tokens now carry an optional per-link count (payload.n); readToken returns it; server.js\n/verify success page + samplesUnlockedEmail use parsed.count ?? FREE_SAMPLE_COUNT, so these\ncustomers see '5' while the global default stays 3. startVerification + resend-corrected\nmint with count. Backward compatible: tokens without n fall back to the global default.\n\nCo-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>\nClaude-Session: https://claude.ai/code/session_017j4qS38tWq21qYdjxMcFTy"},{"hash":"0b127cf","date":"2026-09-02 14:50:36 -0700","author":"Steve Abrams","subject":"TK-11120: retail-only 5-sample apology resend (--force, --retail-only) — 74 sent from info@","body":"- verifyResendEmail: light-hearted apology copy (Steve-approved)\n- resend-corrected.js: --retail-only (skip trade tag), --force (own 5-sample ledger)\n- prove-real-customer.js: --setup <email> / --delete <id> modes for live click proof\nSent light-hearted 5-sample letter to 74 retail (1 trade skipped), from info@, links verified.\n\nCo-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>\nClaude-Session: https://claude.ai/code/session_017j4qS38tWq21qYdjxMcFTy"},{"hash":"64b3102","date":"2026-09-02 14:13:40 -0700","author":"Steve Abrams","subject":"TK-11120: real-customer click proof — verify confirms + tags with zero input (PASS)","body":"Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>\nClaude-Session: https://claude.ai/code/session_017j4qS38tWq21qYdjxMcFTy"},{"hash":"e288df7","date":"2026-09-02 14:04:01 -0700","author":"auto-commit-fleet","subject":"auto-data-snapshot: 2026-09-02T14:03:59 (1 data files) — verification/tk11120/tk11120-resend-ledger.jsonl","body":""},{"hash":"52afecd","date":"2026-09-02 14:01:55 -0700","author":"Steve Abrams","subject":"TK-11120: server.js baseUrl() also fails closed on missing PUBLIC_URL (defense-in-depth)","body":"Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>\nClaude-Session: https://claude.ai/code/session_017j4qS38tWq21qYdjxMcFTy"},{"hash":"a6edf22","date":"2026-09-02 13:55:00 -0700","author":"Steve Abrams","subject":"TK-11120: corrected resend tool + apology template (retail-verify localhost outage)","body":"verification/tk11120/resend-corrected.js — safe corrected resender for the 75:\n- HARD-REQUIRES PUBLIC_URL (fail-closed baseUrl) + GEORGE_ACCOUNT=info; aborts otherwise\n- bypasses the tainted sample_verify_sent flag/old ledger (broken run set them); own ledger\n- email.verifyResendEmail: verify letter + one-line apology; DRY_RUN default, --apply gate\n- affected-emails.txt: the 75 deduped recipients (Angie excluded)\n\nProven locally: dry-run builds https link, info@ sender, abort on empty PUBLIC_URL. No send.\n\nCo-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>\nClaude-Session: https://claude.ai/code/session_017j4qS38tWq21qYdjxMcFTy"},{"hash":"0016d65","date":"2026-09-02 13:48:35 -0700","author":"Steve Abrams","subject":"TK-11120: verify links fail-closed on missing PUBLIC_URL + send as info@ (retail-verify localhost-link outage)","body":"Root cause of the 2026-09-02 retail-verify blast: a backfill job ran with\nPUBLIC_URL unset, so verify.baseUrl() fell back to http://127.0.0.1:9856 and\n~75 customers got a dead localhost 'Confirm my email' link. Also sent as\nsteve-office (Gmail rewrote From to steve@ since info@ isn't a send-as alias).\n\n- verify.baseUrl(): FAIL-CLOSED in LIVE — never fall back to loopback; empty\n  PUBLIC_URL returns '' and startVerification refuses to send (reason=no_public_url).\n- config.GEORGE_ACCOUNT default steve-office -> info, so mail is genuinely from info@.\n\nNo send, no deploy in this commit — code hardening only.\n\nCo-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>\nClaude-Session: https://claude.ai/code/session_017j4qS38tWq21qYdjxMcFTy"},{"hash":"d8959b9","date":"2026-09-02 12:18:40 -0700","author":"Steve Abrams","subject":"TK-11114: backfill sent (68/68) — outage fully remediated","body":"Re-sent the verify letter to all 68 genuine retail signups that missed it during the\noutage; confirmed sent=68 failed=0 (George 200) + present in the steve-office Sent folder.\nFlags set + ledgered (idempotent). TK-11114 closed: send fixed, durable, backfilled.\n\nCo-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>"},{"hash":"228c2ea","date":"2026-09-02 12:09:51 -0700","author":"Steve Abrams","subject":"TK-11114: backfill tooling — scope (68 affected since go-live) + safe sender","body":"Read-only scoper enumerates genuine retail signups with no sample_verify_sent (never\ngot the verify letter), excluding test/internal + trade: 68 since 2026-08-28. Sender\nbypasses the 24h webhook freshness gate (direct verify.startVerification), dry-run by\ndefault, idempotent (live flag re-check + ledger), --only/--apply gates. Real 68-email\nsend stays hard-gated (send-to-list).\n\nCo-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>"},{"hash":"a287362","date":"2026-09-02 12:07:49 -0700","author":"auto-commit-fleet","subject":"auto-data-snapshot: 2026-09-02T12:07:47 (1 data files) — verification/tk11114/backfill-affected.json","body":""},{"hash":"992e15e","date":"2026-09-02 12:05:44 -0700","author":"Steve Abrams","subject":"TK-11114: BLOCK B — verify email proven end-to-end (send 200 -> inbox -> verify-click -> verified-sample tag persists)","body":"Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>\nClaude-Session: https://claude.ai/code/session_01WzrHtgk5Qq7kpdfaWeDk97"},{"hash":"e9e18c7","date":"2026-09-02 12:02:17 -0700","author":"Steve Abrams","subject":"TK-11114: record BLOCK A deploy success (fix live, verified green)","body":"Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>\nClaude-Session: https://claude.ai/code/session_01WzrHtgk5Qq7kpdfaWeDk97"},{"hash":"3efc18c","date":"2026-09-02 12:01:10 -0700","author":"Steve Abrams","subject":"TK-11114: durable persist confirmed — signup email now restart-safe","body":"Persisted DRY_RUN=0 (ecosystem) + WEBHOOK_URL_TOKEN=e9c9 (.env, matches registered\nwebhook) + pm2 save; verified a second pm2 reload still returns dry_run:false. Closes\nthe transience gap. Backfill of missed signups remains gated (send-to-list).\n\nCo-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>"},{"hash":"cb069ce","date":"2026-09-02 11:50:22 -0700","author":"Steve Abrams","subject":"TK-11114: dry-run-safe verify-email backfill executor (double-locked send)","body":"Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>\nClaude-Session: https://claude.ai/code/session_01HuW5Mim1jbLsbaaEWsnq7D"},{"hash":"49e1750","date":"2026-09-02 11:49:37 -0700","author":"Steve Abrams","subject":"TK-10830: one-shot guard on Kelly reply (prevent duplicate sends)","body":"Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>\nClaude-Session: https://claude.ai/code/session_01WzrHtgk5Qq7kpdfaWeDk97"},{"hash":"487679a","date":"2026-09-02 11:41:57 -0700","author":"Steve Abrams","subject":"TK-11114: R4 proof — DW signup verify-email fixed + verified end-to-end","body":"Corrected the false 'missing webhook' premise; real faults were a silently-failing\nGeorge send (now status=200 after current-code redeploy + DRY_RUN=0) and a webhook\ntoken drift (aligned running WEBHOOK_URL_TOKEN to the registered webhook's e9c9).\nControlled +dwgolive test: send->deliver->verify-click->verified-sample tag->cleanup\nall PASS. Remaining backfill of missed signups is send-to-list, hard-gated.\n\nCo-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>"},{"hash":"7963d0a","date":"2026-09-02 11:37:07 -0700","author":"Steve Abrams","subject":"TK-11114: reconcile deploy paste — supersede stale webhook FIX-PASTE with verified BLOCK A (file-sync + clean-env restart)","body":"Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>\nClaude-Session: https://claude.ai/code/session_01WzrHtgk5Qq7kpdfaWeDk97"},{"hash":"e393474","date":"2026-09-02 11:35:29 -0700","author":"Steve Abrams","subject":"prove TK-11114 signup email remediation end to end","body":""},{"hash":"e8c174a","date":"2026-09-02 11:28:33 -0700","author":"auto-commit-fleet","subject":"auto-data-snapshot: 2026-09-02T11:28:31 (2 data files) — arm-webhook.sh.DISABLED-duplicate-webhook-2026-09-02 verification/tk11114-remediation/RUNBOOK.md","body":""},{"hash":"9e5aa29","date":"2026-09-02 10:56:27 -0700","author":"Steve Abrams","subject":"prevent customer data in George failure logs","body":""},{"hash":"b7b23da","date":"2026-09-02 10:53:24 -0700","author":"Steve Abrams","subject":"TK-11114: un-swallow George send failures (credential-safe logging)","body":"Root cause of the DW signup verify-email outage: the running prod service\nauthenticates to George with a stale GEORGE_BASIC_AUTH process.env override\n(!= the on-host DW-Agents/gmail-agent/.env cred), so every /api/send 401s and\nthe error was SWALLOWED — email.js#georgePost logged nothing on non-2xx and\nverify.js#startVerification returned a bare ok:false.\n\n- email.js: georgePost now always surfaces httpStatus + a short bodyPreview;\n  sendEmail logs SEND FAILED (status/error/body) or a success line. Never logs\n  the Authorization header, Basic-auth, or send token.\n- verify.js: startVerification propagates + logs the George reason/status\n  (reason:'send_failed', status, error) so retail-webhook.js records why the\n  letter didn't go out. Never logs the verify token URL.\n- scripts/tk11114-logging-test.js: proves 401 -> ok:false+reason+status logged,\n  200 -> ok:true, and NO credential leak in either path.\n\nNo config/auth logic changed here (creds resolve correctly from the on-host\nfile; the stale runtime override is repaired at the pm2 restart step).\n\nCo-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>\nClaude-Session: https://claude.ai/code/session_017e5hAGNWhTKbcLnkkWg4Wv"},{"hash":"7c99f88","date":"2026-09-02 10:48:46 -0700","author":"auto-commit-fleet","subject":"auto-data-snapshot: 2026-09-02T10:48:45 (1 data files) — kelly-customer.json","body":""},{"hash":"5083a87","date":"2026-09-02 10:32:38 -0700","author":"Steve Abrams","subject":"document TK-11114 signup email failure proof","body":""},{"hash":"ce79cb4","date":"2026-08-31 02:10:01 -0700","author":"Steve Abrams","subject":"prove retail and designer checkout entitlements","body":""},{"hash":"8bfd807","date":"2026-08-31 02:00:58 -0700","author":"auto-commit-fleet","subject":"auto-data-snapshot: 2026-08-31T02:00:54 (4 data files) — verification/artifacts/shopify-account-authenticated-2026-08-31T09-00-07-283Z.png verification/artifacts/shopify-account-email-2026-08-31T08-58-59-401Z.png verification/artifacts/shopify-account-email-2026-08-31T09-00-07-283Z.png verification/shopify-account-e2e.json","body":""},{"hash":"cd936fe","date":"2026-08-31 01:45:58 -0700","author":"Steve Abrams","subject":"record signup e2e build identity","body":""},{"hash":"aa1c2a5","date":"2026-08-31 01:45:28 -0700","author":"Steve Abrams","subject":"prove production signup and sample activation","body":""},{"hash":"b745c53","date":"2026-08-31 01:37:15 -0700","author":"Steve Abrams","subject":"Record TK-10994 production E2E proof","body":""},{"hash":"a491d70","date":"2026-08-29 04:13:28 -0700","author":"Steve Abrams","subject":"Declare message_class transactional on george send (TK-10944)","body":"Signup fulfillment emails (welcome, trade approval, retail codes) are\ntransactional — triggered by user action. Explicitly declare message_class\nso george's sendPreflight classifies them correctly.\n\nCo-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>"},{"hash":"72998fc","date":"2026-08-28 07:45:46 -0700","author":"auto-commit-fleet","subject":"auto-data-snapshot: 2026-08-28T07:45:44 (1 data files) — package.json","body":""},{"hash":"56700f8","date":"2026-08-28 07:08:14 -0700","author":"auto-commit-fleet","subject":"auto-data-snapshot: 2026-08-28T07:08:13 (1 data files) — package.json","body":""},{"hash":"37837ac","date":"2026-08-28 00:56:58 -0700","author":"auto-commit-fleet","subject":"auto-data-snapshot: 2026-08-28T00:56:56 (1 data files) — package.json","body":""},{"hash":"7b6cce3","date":"2026-08-26 13:14:33 -0700","author":"steve-office","subject":"chore: version bump (session close)","body":""},{"hash":"f975a4d","date":"2026-08-26 12:35:17 -0700","author":"steve-office","subject":"preflight: load ecosystem env as fallback so standalone runs reflect live pm2 config","body":""},{"hash":"081fb31","date":"2026-08-17 08:28:50 -0700","author":"Steve","subject":"snapshot before restart: preserve in-flight work","body":""},{"hash":"f231337","date":"2026-08-17 07:02:21 -0700","author":"auto-commit-fleet","subject":"auto-data-snapshot: 2026-08-17T07:02:20 (2 data files) — .playwright-mcp/page-2026-08-17T13-53-00-059Z.yml instagram-subdomain-full.png","body":""},{"hash":"e398ba2","date":"2026-08-17 06:52:34 -0700","author":"Steve","subject":"snapshot before restart: preserve in-flight work (auto-saved by /restart pre-reboot)","body":""},{"hash":"33d6c4b","date":"2026-08-14 16:45:30 -0700","author":"steve","subject":"contrarian gate: dedupe /verify confirmation email (fire once, not per re-click); fix runbook path in 5x report","body":""},{"hash":"b32b988","date":"2026-08-14 16:40:33 -0700","author":"steve","subject":"5x RE-RUN 3: Option C /claim + /verify pages — 2 defects caught+fixed, both clean twice","body":""},{"hash":"e642d0d","date":"2026-08-14 16:38:01 -0700","author":"steve","subject":"5x sweep: /verify returns 200 for invalid/expired human links (was 400 → console error on a customer page); 503 kept for no_secret","body":""},{"hash":"535e4f8","date":"2026-08-14 16:31:57 -0700","author":"steve","subject":"5x sweep 1: client-side email validation on /claim (empty submit no longer fires a 400)","body":""},{"hash":"aa70472","date":"2026-08-14 14:26:15 -0700","author":"auto-commit-fleet","subject":"auto-data-snapshot: 2026-08-14T14:26:13 (1 data files) — package-lock.json","body":""},{"hash":"5b98c35","date":"2026-08-14 14:01:45 -0700","author":"steve","subject":"chore: lint + refactor golive-preflight (query-preserve, visible write-fail, crash guard), v0.1.4 (session close)","body":""},{"hash":"d292c26","date":"2026-08-14 13:45:49 -0700","author":"steve","subject":"DEPLOY.md: point pre-flight at golive-preflight.js + link the gated go-live runbook","body":""},{"hash":"ce1c7d7","date":"2026-08-14 13:45:05 -0700","author":"steve","subject":"ecosystem: set PUBLIC_URL for Option C verify links + webhook (DRY_RUN still pinned to 1)","body":""},{"hash":"927545c","date":"2026-08-14 13:40:44 -0700","author":"steve","subject":"add Option C go-live readiness preflight (PASS/WARN/FAIL, no-send George auth probe)","body":""},{"hash":"7169240","date":"2026-08-14 12:54:46 -0700","author":"auto-commit-fleet","subject":"auto-data-snapshot: 2026-08-14T12:54:45 (1 data files) — package-lock.json","body":""},{"hash":"f1cb8a4","date":"2026-08-14 12:25:19 -0700","author":"steve","subject":"chore: fail-loud guard on empty George Basic-auth, v0.1.3 (session close)","body":""}]}