← back to Tk10640 Redirect Prune
TK-10640 Cody fixes: random n=1000 re-probe (still 0 ACTIVE, Wilson<=123), collection-source guard, rollback-recreate.mjs
521f8f23adcad779c57254c9cf5e87de00470c9a · 2026-08-17 23:29:29 -0700 · steve
Files touched
M apply-prune-to-home.mjsA data/status-tally-random.jsonA rollback-recreate.mjsA status-probe-random.mjs
Diff
commit 521f8f23adcad779c57254c9cf5e87de00470c9a
Author: steve <steve@designerwallcoverings.com>
Date: Mon Aug 17 23:29:29 2026 -0700
TK-10640 Cody fixes: random n=1000 re-probe (still 0 ACTIVE, Wilson<=123), collection-source guard, rollback-recreate.mjs
---
apply-prune-to-home.mjs | 9 +++++++--
data/status-tally-random.json | 11 +++++++++++
rollback-recreate.mjs | 24 ++++++++++++++++++++++++
status-probe-random.mjs | 30 ++++++++++++++++++++++++++++++
4 files changed, 72 insertions(+), 2 deletions(-)
diff --git a/apply-prune-to-home.mjs b/apply-prune-to-home.mjs
index 2736f53..c2bb2b9 100644
--- a/apply-prune-to-home.mjs
+++ b/apply-prune-to-home.mjs
@@ -25,12 +25,17 @@ let scanned=0,deleted=0,skipped_live=0,err=0;
for(const r of toHome){
if(done.has(r.id)) continue;
scanned++;
- // belt-and-suspenders live re-check for product sources
+ // belt-and-suspenders live re-check for ANY source that maps to a live resource
+ // (Cody fix: previously only /products/ was checked; /collections/ sources deleted blind).
if(/^\/products\//.test(r.path)){
const h=r.path.replace(/^\/products\//,'').split('?')[0].split('#')[0];
const d=await gql(`query($h:String!){productByHandle(handle:$h){status onlineStoreUrl}}`,{h});
const p=d.productByHandle;
- if(p && p.status==='ACTIVE' && p.onlineStoreUrl){ skipped_live++; appendFileSync(DONE,JSON.stringify({id:r.id,path:r.path,skipped:'LIVE_ACTIVE_PUBLISHED'})+'\n'); continue; }
+ if(p && p.status==='ACTIVE' && p.onlineStoreUrl){ skipped_live++; appendFileSync(DONE,JSON.stringify({id:r.id,path:r.path,skipped:'LIVE_ACTIVE_PUBLISHED_PRODUCT'})+'\n'); continue; }
+ } else if(/^\/collections\//.test(r.path)){
+ const h=r.path.replace(/^\/collections\//,'').split('?')[0].split('#')[0];
+ const d=await gql(`query($h:String!){collectionByHandle(handle:$h){id}}`,{h});
+ if(d.collectionByHandle){ skipped_live++; appendFileSync(DONE,JSON.stringify({id:r.id,path:r.path,skipped:'LIVE_COLLECTION'})+'\n'); continue; }
}
if(APPLY){
const del=await gql(DEL,{id:r.id});
diff --git a/data/status-tally-random.json b/data/status-tally-random.json
new file mode 100644
index 0000000..9549bc4
--- /dev/null
+++ b/data/status-tally-random.json
@@ -0,0 +1,11 @@
+{
+ "sample": "RANDOM Fisher-Yates",
+ "n": 1000,
+ "ACTIVE": 0,
+ "ARCHIVED": 558,
+ "DRAFT": 2,
+ "DEAD": 440,
+ "active_published_examples": [],
+ "wilson95_upper_active_rate": 0.00383,
+ "est_max_active_in_32045": 123
+}
diff --git a/rollback-recreate.mjs b/rollback-recreate.mjs
new file mode 100644
index 0000000..d98d2fd
--- /dev/null
+++ b/rollback-recreate.mjs
@@ -0,0 +1,24 @@
+// TK-10640 — ROLLBACK: re-create redirects that apply-prune-to-home.mjs deleted.
+// Reads data/done-prune-to-home.jsonl, re-creates every {deleted:true} row's path->target
+// from the original dump. DRY-RUN default; --apply re-creates. GATED like the apply script.
+// Shopify assigns a fresh GID on create; path is the functional key so behavior is restored.
+import { readFileSync, existsSync } from 'node:fs';
+const txt=readFileSync(`${process.env.HOME}/Projects/secrets-manager/.env`,'utf8');
+const env={}; for(const l of txt.split('\n')){const m=l.match(/^([A-Z0-9_]+)=(.*)$/); if(m) env[m[1]]=m[2].replace(/^["']|["']$/g,'');}
+const STORE=env.SHOPIFY_STORE_DOMAIN, TOKEN=env.SHOPIFY_FULL_ACCESS_TOKEN, API='2024-10';
+const APPLY=process.argv.includes('--apply');
+const sleep=ms=>new Promise(r=>setTimeout(r,ms));
+async function gql(q,v={}){for(let a=0;a<10;a++){try{const res=await fetch(`https://${STORE}/admin/api/${API}/graphql.json`,{method:'POST',headers:{'Content-Type':'application/json','X-Shopify-Access-Token':TOKEN},body:JSON.stringify({query:q,variables:v})});const tx=await res.text();let j;try{j=JSON.parse(tx);}catch{await sleep(1500*(a+1));continue;}if(j.errors){if(/THROTTLED/i.test(JSON.stringify(j.errors))){await sleep(1500*(a+1));continue;}throw new Error(JSON.stringify(j.errors));}return j.data;}catch(e){if(a<9){await sleep(1500*(a+1));continue;}throw e;}}}
+const dump=readFileSync(`${process.env.HOME}/Projects/tk10640-redirect-prune/data/redirects-dump.jsonl`,'utf8').trim().split('\n').map(JSON.parse);
+const byId=new Map(dump.map(r=>[r.id,r]));
+const DONE=`${process.env.HOME}/Projects/tk10640-redirect-prune/data/done-prune-to-home.jsonl`;
+if(!existsSync(DONE)){ console.log('no done-log yet — nothing deleted, nothing to roll back'); process.exit(0); }
+const deleted=readFileSync(DONE,'utf8').trim().split('\n').filter(Boolean).map(JSON.parse).filter(x=>x.deleted);
+const CRE=`mutation($r:UrlRedirectInput!){ urlRedirectCreate(urlRedirect:$r){ urlRedirect{ id } userErrors{ message } } }`;
+let recreated=0,err=0;
+for(const x of deleted){
+ const orig=byId.get(x.id)||{path:x.path,target:'/'};
+ if(APPLY){ const d=await gql(CRE,{r:{path:orig.path,target:orig.target||'/'}});
+ if(d.urlRedirectCreate.userErrors.length) err++; else recreated++; }
+}
+console.log(JSON.stringify({mode:APPLY?'APPLY':'DRY-RUN',deleted_in_log:deleted.length,recreated,errors:err},null,2));
diff --git a/status-probe-random.mjs b/status-probe-random.mjs
new file mode 100644
index 0000000..f364f8c
--- /dev/null
+++ b/status-probe-random.mjs
@@ -0,0 +1,30 @@
+// TK-10640 — RANDOM stratified status probe (Cody fix: n=400 was sequential/oldest-rows).
+// Fisher-Yates shuffle over ALL 32,045 /products/<h>->/ rows, probe first --n at random.
+import { readFileSync } from 'node:fs';
+const txt=readFileSync(`${process.env.HOME}/Projects/secrets-manager/.env`,'utf8');
+const env={}; for(const l of txt.split('\n')){const m=l.match(/^([A-Z0-9_]+)=(.*)$/); if(m) env[m[1]]=m[2].replace(/^["']|["']$/g,'');}
+const STORE=env.SHOPIFY_STORE_DOMAIN, TOKEN=env.SHOPIFY_FULL_ACCESS_TOKEN, API='2024-10';
+const sleep=ms=>new Promise(r=>setTimeout(r,ms));
+async function gql(q,v={}){for(let a=0;a<10;a++){try{const res=await fetch(`https://${STORE}/admin/api/${API}/graphql.json`,{method:'POST',headers:{'Content-Type':'application/json','X-Shopify-Access-Token':TOKEN},body:JSON.stringify({query:q,variables:v})});const tx=await res.text();let j;try{j=JSON.parse(tx);}catch{await sleep(1500*(a+1));continue;}if(j.errors){if(/THROTTLED/i.test(JSON.stringify(j.errors))){await sleep(1500*(a+1));continue;}throw new Error(JSON.stringify(j.errors));}return j.data;}catch(e){if(a<9){await sleep(1500*(a+1));continue;}throw e;}}}
+const N=Number((process.argv.find(a=>a.startsWith('--n='))||'').split('=')[1]||1000);
+const rows=readFileSync('data/redirects-dump.jsonl','utf8').trim().split('\n').map(JSON.parse);
+const norm=s=>(s||'').split('?')[0].split('#')[0];
+const p2r=rows.filter(r=>{const t=norm(r.target);return (!t||t==='/')&&/^\/products\//.test(r.path);});
+// Fisher-Yates
+for(let i=p2r.length-1;i>0;i--){const j=Math.floor(Math.random()*(i+1));[p2r[i],p2r[j]]=[p2r[j],p2r[i]];}
+let A=0,AR=0,DR=0,DE=0; const activeEx=[];
+for(let i=0;i<N&&i<p2r.length;i++){ const h=p2r[i].path.replace(/^\/products\//,'').split('?')[0];
+ const d=await gql(`query($h:String!){productByHandle(handle:$h){status onlineStoreUrl}}`,{h});
+ const p=d.productByHandle; const st=p?p.status:'DEAD';
+ if(st==='ACTIVE'){ A++; if(p.onlineStoreUrl) activeEx.push({path:p2r[i].path,url:p.onlineStoreUrl}); }
+ else if(st==='ARCHIVED')AR++;else if(st==='DRAFT')DR++;else DE++;
+ if(i%200===199) process.stderr.write(` probed ${i+1}: A=${A} AR=${AR} DR=${DR} DE=${DE}\n`);
+}
+const n=Math.min(N,p2r.length);
+// Wilson 95% upper bound for observed A active
+const z=1.96, ph=A/n, denom=1+z*z/n;
+const upper=(ph+z*z/(2*n)+z*Math.sqrt((ph*(1-ph)+z*z/(4*n))/n))/denom;
+console.log(JSON.stringify({sample:'RANDOM Fisher-Yates', n, ACTIVE:A, ARCHIVED:AR, DRAFT:DR, DEAD:DE,
+ active_published_examples:activeEx.slice(0,10),
+ wilson95_upper_active_rate:+upper.toFixed(5),
+ est_max_active_in_32045:Math.ceil(upper*32045)},null,2));
← 8d40896 TK-10640: read-only redirect-cap analysis + gated prune plan
·
back to Tk10640 Redirect Prune
·
auto-data-snapshot: 2026-08-17T23:34:22 (2 data files) — dat a3a92e1 →