← back to Newmor Onboard

scripts/launch-products.mjs

117 lines

import assert from 'node:assert/strict';
import fs from 'node:fs';
import {execFileSync} from 'node:child_process';
import {ROOT,gql,product,read,save} from './shopify.mjs';
const plan=read('data/newmor-showroom-plan-2026-09-08.json');
const baseline=read('data/live-2026-09-09/shopify-before.json').products;
const range=read('data/live-2026-09-09/vendor-range.json');
const pub='gid://shopify/Publication/22208643184';
const mode=process.argv[2];assert(['canary','rollout','rollback'].includes(mode));
const approved=process.argv.includes('--approved');
const ticketLog=text=>execFileSync('/Users/macstudio3/Projects/ticket-system/tk',['log',plan.ticket,text],{env:{...process.env,TK_AGENT:'codex-run-10670'},stdio:'pipe'});
const canaryIds=new Set(['gid://shopify/Product/4351291785267','gid://shopify/Product/4351291818035']);
const eligible=new Set(range.candidates.filter(c=>c.found).map(c=>c.id));
const candidates=plan.candidates.filter(c=>eligible.has(c.id)&&(mode==='canary'?canaryIds.has(c.id):!canaryIds.has(c.id)));
assert.equal(plan.candidates.length,125);assert.equal(new Set(plan.candidates.map(c=>c.id)).size,125);
if(mode==='canary')assert.deepEqual(new Set(candidates.map(c=>c.id)),canaryIds,'Both canary IDs must be eligible');
if(mode!=='rollback')assert(Date.now()-Date.parse(range.checked_at)<86400000,'Vendor evidence older than24h');
const markerIds=new Set(plan.candidates.filter(c=>c.proposed.add_tags.length||c.proposed.set_showroom_line).map(c=>c.id));
assert.equal(markerIds.size,7);
const journalPath='data/live-2026-09-09/product-launch-journal.json';
const journal=fs.existsSync(`${ROOT}/${journalPath}`)?read(journalPath):{ticket:plan.ticket,approval:'Steve: yes do it (September9); linked showroom memo',records:[]};
const fingerprint=p=>JSON.stringify({id:p.id,vendor:p.vendor,handle:p.handle,title:p.title,descriptionHtml:p.descriptionHtml,templateSuffix:p.templateSuffix,variants:p.variants.nodes});
const published=p=>p.resourcePublications.nodes.filter(n=>n.isPublished).map(n=>n.publication.id).sort();
const configured=p=>p.configuredPublications.nodes.map(n=>n.publication.id).sort();
async function mutation(q,v,name){const d=await gql(q,v);assert(d[name]);assert.deepEqual(d[name].userErrors,[],`${name} failed`);return d[name];}
const setStatus=(id,status)=>mutation('mutation($p:ProductUpdateInput!){productUpdate(product:$p){product{id status}userErrors{field message}}}',{p:{id,status}},'productUpdate');
const unpublish=(id,ids)=>mutation('mutation($id:ID!,$input:[PublicationInput!]!){publishableUnpublish(id:$id,input:$input){userErrors{field message}}}',{id,input:ids.map(publicationId=>({publicationId}))},'publishableUnpublish');
const publish=(id,ids)=>mutation('mutation($id:ID!,$input:[PublicationInput!]!){publishablePublish(id:$id,input:$input){userErrors{field message}}}',{id,input:ids.map(publicationId=>({publicationId}))},'publishablePublish');
if(mode==='rollout') {
 const proof=read('verification/canary-proof.json');assert.equal(proof.verdict,'PASS');
 assert.deepEqual(new Set(proof.ids),canaryIds);assert(Date.now()-Date.parse(proof.timestamp)<86400000,'Stale canary proof');
 const discovery=read('verification/discovery-proof.json');assert.equal(discovery.verdict,'PASS');
 assert.deepEqual(new Set(discovery.expected_ids.map(id=>'gid://shopify/Product/'+id)),canaryIds);assert(Date.now()-Date.parse(discovery.timestamp)<86400000,'Stale discovery proof');
}
if(mode==='rollback') {
 // Restore only this run's changed status/publications/markers, with concurrent-state checks.
 for(const r of journal.records){
  const p=await product(r.id);assert.equal(p.vendor,'Newmor Wallcoverings');
  assert(['ACTIVE','DRAFT'].includes(p.status),'Externally archived: stop rollback');
  assert.equal(fingerprint(p),fingerprint(r.before),'Content or variants changed: stop rollback');
  assert(published(p).every(x=>x===pub),'Unexpected external channel: stop rollback for review');
  assert(configured(p).every(x=>x===pub||configured(r.before).includes(x)),'Unexpected configured channel: stop rollback for review');
  const added=r.steps.filter(s=>s.action==='tagsAdd').flatMap(s=>s.tags);
  const changedMeta=r.steps.some(s=>s.action==='showroom_line');
  const desiredTags=[...new Set([...r.before.tags,...added])].sort();
  const originalTags=[...r.before.tags].sort();
  assert([JSON.stringify(originalTags),JSON.stringify(desiredTags)].includes(JSON.stringify([...p.tags].sort())),'Concurrent tag edit: stop rollback');
  if(changedMeta){assert(!r.before.showroom);assert(!p.showroom||p.showroom.value==='true');}
  if(approved){
   if(p.status!=='DRAFT')await setStatus(r.id,'DRAFT');
   const drafted=await product(r.id);assert.equal(drafted.status,'DRAFT');assert.deepEqual(published(drafted),[]);
   const latentBefore=configured(r.before);const latentNow=configured(drafted);
   const remove=latentNow.filter(id=>!latentBefore.includes(id));if(remove.length)await unpublish(r.id,remove);
   const restore=latentBefore.filter(id=>!latentNow.includes(id));if(restore.length)await publish(r.id,restore);
   if(r.rollback_status!=='restored'){
    if(added.some(t=>p.tags.includes(t)))await mutation('mutation($id:ID!,$tags:[String!]!){tagsRemove(id:$id,tags:$tags){userErrors{field message}}}',{id:r.id,tags:added},'tagsRemove');
    if(changedMeta&&p.showroom)await mutation('mutation($m:[MetafieldIdentifierInput!]!){metafieldsDelete(metafields:$m){userErrors{field message}}}',{m:[{ownerId:r.id,namespace:'custom',key:'showroom_line'}]},'metafieldsDelete');
   }
   const post=await product(r.id);assert.equal(post.status,r.before.status);assert.deepEqual(published(post),published(r.before));assert.deepEqual(configured(post),configured(r.before));assert.deepEqual([...post.tags].sort(),[...r.before.tags].sort());assert.equal(post.showroom?.value,r.before.showroom?.value);
   r.rollback_evidence={at:new Date().toISOString(),verdict:'PASS',product:post};
   ticketLog(`Verified product rollback ${r.code} ${r.id}: DRAFT, zero visible publications, exact original latent memberships/status/tags/showroom restored. Persisted rollback_evidence in product launch journal; image fills retained as separately approved.`);
  }
  r.rollback_status=approved?'restored':'dry-run';save(journalPath,journal);
 }
 console.log(`Rollback ${approved?'verified':'dry-run'} for ${journal.records.length} touched records; status/publications/markers restored; separately sourced images retained.`);
 process.exit(0);
}
for(const c of candidates){
 const expected=baseline.find(p=>p.id===c.id);assert(expected);assert.equal(expected.status,'DRAFT');
 const pre=await product(c.id);
 assert.equal(fingerprint(pre),fingerprint(expected),`Product/variant drift ${c.id}`);
 assert(pre.images.nodes.length>0,`Missing image ${c.id}`);
 assert(pre.variants.nodes.every(v=>/sample/i.test(v.title)),`Material variant in showroom ${c.id}`);
 assert(published(pre).every(x=>x===pub));
 let record=journal.records.find(r=>r.id===c.id);
 const plannedTags=[...new Set([...expected.tags,'quotes','Showroom Line'])].sort();
 const preTags=[...pre.tags].sort();
 if(!record)assert.deepEqual(preTags,[...expected.tags].sort(),`Tag drift ${c.id}`);
 else assert.deepEqual(preTags,record.rollback_status==='restored'?[...expected.tags].sort():[...new Set([...expected.tags,...record.steps.filter(s=>s.action==='tagsAdd').flatMap(s=>s.tags)])].sort(),`Unexpected tag drift ${c.id}`);
 if(!markerIds.has(c.id)){assert.equal(pre.showroom?.value,'true');assert(pre.tags.includes('quotes')&&pre.tags.includes('Showroom Line'));}
 if(pre.status==='ACTIVE'){
   assert(record,`Active without our journal ${c.id}`);
   if(published(pre).includes(pub)){assert.deepEqual(configured(pre),[pub]);assert.deepEqual(preTags,plannedTags);assert.equal(pre.showroom?.value,'true');record.after=pre;save(journalPath,journal);console.log(`Already live ${c.manufacturer_code}`);continue;}
 } else assert.equal(pre.status,'DRAFT');
 assert(eligible.has(c.id));
 if(!approved){console.log(`DRY RUN ${c.manufacturer_code}`);continue;}
 ticketLog(`Approved ${mode}: begin exact product ${c.manufacturer_code} ${c.id}; fresh content/variant/image/marker guards passed. Will clear latent publications while DRAFT, then activate with zero-channel barrier and publish Online Store only.`);
 if(!record){record={id:c.id,code:c.manufacturer_code,before:pre,steps:[]};journal.records.push(record);save(journalPath,journal);}
 if(record.rollback_status==='restored'){record.rollback_status='reapplied';save(journalPath,journal);}
 if(!record.before.configuredPublications){assert.equal(pre.status,'DRAFT');record.before.configuredPublications=pre.configuredPublications;save(journalPath,journal);}
 if(pre.status==='DRAFT' && configured(pre).length){
   await unpublish(c.id,configured(pre));record.steps.push({action:'clear latent publications',ids:configured(pre)});save(journalPath,journal);
   const cleared=await product(c.id);assert.equal(cleared.status,'DRAFT');assert.deepEqual(configured(cleared),[]);assert.deepEqual(published(cleared),[]);
 }
 const add=['Showroom Line','quotes'].filter(t=>!pre.tags.includes(t));
 if(add.length){assert(markerIds.has(c.id));await mutation('mutation($id:ID!,$tags:[String!]!){tagsAdd(id:$id,tags:$tags){userErrors{field message}}}',{id:c.id,tags:add},'tagsAdd');record.steps.push({action:'tagsAdd',tags:add});save(journalPath,journal);}
 if(pre.showroom?.value!=='true'){
   assert(markerIds.has(c.id));
   assert(!pre.showroom,`Unexpected non-true showroom metafield ${c.id}`);
   await mutation('mutation($m:[MetafieldsSetInput!]!){metafieldsSet(metafields:$m){metafields{id}userErrors{field message}}}',{m:[{ownerId:c.id,namespace:'custom',key:'showroom_line',type:'boolean',value:'true',compareDigest:null}]},'metafieldsSet');
   record.steps.push({action:'showroom_line',value:true});save(journalPath,journal);
 }
 await setStatus(c.id,'ACTIVE');record.steps.push({action:'status',value:'ACTIVE'});save(journalPath,journal);
 const active=await product(c.id);
 if(configured(active).length||published(active).length){
   record.steps.push({action:'unexpected activation channels',configured:configured(active),published:published(active)});save(journalPath,journal);
   await setStatus(c.id,'DRAFT');const safe=await product(c.id);assert.equal(safe.status,'DRAFT');assert.deepEqual(published(safe),[]);
   throw new Error(`Activation barrier failed;${c.id} restored to DRAFT`);
 }
 await publish(c.id,[pub]);
 const post=await product(c.id);assert.equal(post.status,'ACTIVE');assert.deepEqual(published(post),[pub]);assert.deepEqual(configured(post),[pub]);assert.equal(fingerprint(post),fingerprint(expected));assert.equal(post.showroom.value,'true');assert(post.tags.includes('quotes')&&post.tags.includes('Showroom Line'));
 record.steps.push({action:'Online Store publication',verified_at:new Date().toISOString()});record.after=post;save(journalPath,journal);
 ticketLog(`Verified ${c.manufacturer_code} ${c.id} ACTIVE; visible AND configured publications exactly Online Store 22208643184. Original identity/content/variants/prices preserved; showroom markers true. Steps/before/after persisted in product-launch-journal.json.`);
 console.log(`Verified live ${c.manufacturer_code}`);
}
console.log(`${mode}: ${candidates.length} candidates ${approved?'processed':'dry-run checked'}`);