← back to Agent Cabinet
proof(front-page): harden ux-primitives activation gate + dedupe
65c6777149cf3425d4c7256865db71c65d3f5bb5 · 2026-09-23 13:31:10 -0700 · Steve Abrams
Addresses the 5 review findings on the pending front-page-audit/proof change set:
- Fold SITE env filter into prove-activation.mjs and drop the byte-identical
prove-activation-one.mjs — one gate, no silent drift between two copies.
- Tear the site server down in try/finally (+ boot-error catch) so a slow boot
or a mid-loop throw no longer orphans a node server / leaves a stuck port.
- slowRun: bounded waitForFunction for real product children instead of the
fixed delay+2s clock the deferred bundle could outrun (kills the false-FAIL).
- Page errors: baseline-subtract against the bundle-absent runs + BUNDLE_RX
filter, so a site's own uncaught exception can't red the gate as a "bundle"
failure; also record slowRun page errors in presentPageErr.
- live-ux-canary: expect marker via UX_MARKER (mirrors deploy $CANON) or a
well-formed v<semver>#<hash> shape, not a pinned content hash that would
red the whole fleet on the next healthy rebuild.
Smoke-tested: SITE=flockedwallpaper -> 1/1 all-engine PASS.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01AWMadYahw5wRTf7KcwBLr1
Files touched
A front-page-audit/proof/live-ux-canary.mjsM front-page-audit/proof/prove-activation.mjs
Diff
commit 65c6777149cf3425d4c7256865db71c65d3f5bb5
Author: Steve Abrams <steve@designerwallcoverings.com>
Date: Wed Sep 23 13:31:10 2026 -0700
proof(front-page): harden ux-primitives activation gate + dedupe
Addresses the 5 review findings on the pending front-page-audit/proof change set:
- Fold SITE env filter into prove-activation.mjs and drop the byte-identical
prove-activation-one.mjs — one gate, no silent drift between two copies.
- Tear the site server down in try/finally (+ boot-error catch) so a slow boot
or a mid-loop throw no longer orphans a node server / leaves a stuck port.
- slowRun: bounded waitForFunction for real product children instead of the
fixed delay+2s clock the deferred bundle could outrun (kills the false-FAIL).
- Page errors: baseline-subtract against the bundle-absent runs + BUNDLE_RX
filter, so a site's own uncaught exception can't red the gate as a "bundle"
failure; also record slowRun page errors in presentPageErr.
- live-ux-canary: expect marker via UX_MARKER (mirrors deploy $CANON) or a
well-formed v<semver>#<hash> shape, not a pinned content hash that would
red the whole fleet on the next healthy rebuild.
Smoke-tested: SITE=flockedwallpaper -> 1/1 all-engine PASS.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01AWMadYahw5wRTf7KcwBLr1
---
front-page-audit/proof/live-ux-canary.mjs | 67 +++++++++++++++++++++++++++++
front-page-audit/proof/prove-activation.mjs | 50 ++++++++++++++++-----
2 files changed, 106 insertions(+), 11 deletions(-)
diff --git a/front-page-audit/proof/live-ux-canary.mjs b/front-page-audit/proof/live-ux-canary.mjs
new file mode 100644
index 0000000..756f917
--- /dev/null
+++ b/front-page-audit/proof/live-ux-canary.mjs
@@ -0,0 +1,67 @@
+// live-ux-canary.mjs — READ-ONLY post-deploy canary for the UX-primitives fleet rollout (TK-12031).
+// For each LIVE site: load in a real browser, sample the grid every 50ms during load to catch the
+// skeleton window, then assert after settle: UXGrid initialised, skeletons=0, products>0, empty=0,
+// sort <select> + density <input type=range> present, no bundle-attributable console/page errors.
+// Usage: node live-ux-canary.mjs <sites-file> [engine=chromium|webkit] [out.json]
+import { createRequire } from 'module';
+const require = createRequire(import.meta.url);
+process.env.NODE_PATH = '/Users/macstudio3/.npm-global/lib/node_modules';
+require('module').Module._initPaths();
+const { chromium, webkit } = require('playwright');
+import fs from 'fs';
+const [,, sitesFile, engineName='chromium', outFile=`live-canary-${new Date().toISOString().replace(/[:.]/g,'-')}.json`] = process.argv;
+const sites = fs.readFileSync(sitesFile,'utf8').split('\n').map(s=>s.trim()).filter(Boolean);
+const BUNDLE_RX = /ux-primitives|UXGrid|ModalRig|Bento|CommandPalette|Toast|Skeleton|EmptyState|copyToClipboard/i;
+// Expected bundle marker. Set UX_MARKER to pin a specific build (mirror the deploy script's $CANON as the
+// single source of truth) when you want exact-match; otherwise accept any well-formed marker so a routine
+// rebuild — which rotates the content hash — doesn't red the whole fleet with a perfectly healthy bundle.
+const EXPECT_MARKER = process.env.UX_MARKER || null;
+const MARKER_RX = /^ux-primitives\.bundle v\d+\.\d+\.\d+#[0-9a-f]{6,}$/;
+const engine = engineName==='webkit'?webkit:chromium;
+const browser = await engine.launch({ headless:true });
+const out=[];
+for (const site of sites){
+ const url = `https://${site}.com/`;
+ const ctx = await browser.newContext({ viewport:{width:1280,height:900}, ignoreHTTPSErrors:true });
+ const page = await ctx.newPage();
+ const consoleErr=[], pageErr=[];
+ page.on('console', m=>{ if(m.type()==='error') consoleErr.push(m.text().slice(0,200)); });
+ page.on('pageerror', e=>pageErr.push(String(e.message||e).slice(0,200)));
+ // block third-party ad/analytics noise so it can't mask or delay the measurement
+ await page.route(/googlesyndication|doubleclick|google-analytics|googletagmanager|ads\.agentabrams|facebook|tiktok/i, r=>r.abort());
+ const rec={ site, url, engine:engineName };
+ try{
+ const t0=Date.now();
+ const nav = page.goto(url,{ waitUntil:'domcontentloaded', timeout:45000 });
+ let maxSkel=0, samples=0;
+ const sampler=(async()=>{ while(Date.now()-t0<12000){ try{ const n=await page.evaluate(()=>{const g=document.querySelector('[data-ux-grid]'); return g?g.querySelectorAll('[data-ux-skel-holder],[data-ux-skel],.ux-skel').length:-1;}); if(n>maxSkel) maxSkel=n; samples++; }catch{} await new Promise(r=>setTimeout(r,50)); } })();
+ await nav;
+ await page.waitForLoadState('load',{timeout:45000}).catch(()=>{});
+ await sampler;
+ await page.waitForTimeout(1500);
+ const st = await page.evaluate(()=>{
+ const g=document.querySelector('[data-ux-grid]');
+ const marker=(document.documentElement.outerHTML.match(/ux-primitives\.bundle v[0-9.]+#[0-9a-f]+/)||[null])[0];
+ const q=s=>g?g.querySelectorAll(s).length:-1;
+ const products=g?[...g.children].filter(c=>c.nodeType===1&&!c.matches('[data-ux-skel-holder],[data-ux-skel],[data-ux-empty-holder]')&&!c.classList.contains('ux-skel')).length:-1;
+ return { hasGrid:!!g, marker, skel:q('[data-ux-skel-holder],[data-ux-skel],.ux-skel'), empty:q('[data-ux-empty-holder]'), products,
+ ux: typeof window.UXGrid, sort: !!document.querySelector('select[id*=sort i],select[name*=sort i],#sort,[data-sort]'), density: !!document.querySelector('input[type=range]'),
+ title: document.title.slice(0,60) };
+ });
+ Object.assign(rec, st, { maxSkelDuringLoad:maxSkel, samples });
+ rec.bundleErrors=[...consoleErr,...pageErr].filter(x=>BUNDLE_RX.test(x));
+ rec.otherErrors=[...consoleErr,...pageErr].filter(x=>!BUNDLE_RX.test(x)).length;
+ rec.pageErrorsAll=pageErr.length;
+ rec.criteria={ uxInit: st.ux==='object', markerCanonical: EXPECT_MARKER ? (st.marker===EXPECT_MARKER) : (typeof st.marker==='string' && MARKER_RX.test(st.marker)), gridWired: st.hasGrid,
+ skeletonSeen: maxSkel>0, skeletonCleared: st.skel===0, productsRender: st.products>0, noEmptyWhenProducts: st.empty===0,
+ controls: st.sort && st.density, noBundleErr: rec.bundleErrors.length===0 };
+ // skeletonSeen is INFORMATIONAL on live (a fast CDN load can legitimately beat the 50ms sampler); it does not gate pass
+ const {skeletonSeen, ...gating}=rec.criteria; rec.pass=Object.values(gating).every(Boolean);
+ }catch(e){ rec.error=String(e.message||e).slice(0,200); rec.pass=false; }
+ await ctx.close();
+ console.log(`${site}: pass=${rec.pass} ux=${rec.ux} marker=${rec.marker} skelMax=${rec.maxSkelDuringLoad} skel=${rec.skel} products=${rec.products} empty=${rec.empty} sort=${rec.sort} density=${rec.density} bundleErr=${JSON.stringify(rec.bundleErrors)} ${rec.error?'ERR='+rec.error:''}`);
+ out.push(rec);
+}
+await browser.close();
+fs.writeFileSync(outFile, JSON.stringify(out,null,1));
+const p=out.filter(r=>r.pass).length; console.log(`\nSUMMARY ${engineName}: ${p}/${out.length} pass -> ${outFile}`);
diff --git a/front-page-audit/proof/prove-activation.mjs b/front-page-audit/proof/prove-activation.mjs
index 511aeaf..3f77c7c 100644
--- a/front-page-audit/proof/prove-activation.mjs
+++ b/front-page-audit/proof/prove-activation.mjs
@@ -11,13 +11,18 @@ const ISO = new Date().toISOString().replace(/[:.]/g, '-');
const EVID = path.join(HOME, 'Projects/agent-cabinet/front-page-audit/proof');
fs.mkdirSync(EVID, { recursive: true });
-const PILOTS = [
+const PILOTS_ALL = [
{ site: 'silkwallpaper', mode: 'server', port: 9841, shape: 'items' },
{ site: 'linenwallpaper', mode: 'server', port: 9842, shape: 'items' },
{ site: 'jutewallpaper', mode: 'server', port: 9843, shape: 'items' },
{ site: 'corkwallcovering', mode: 'server', port: 9844, shape: 'items' },
{ site: 'flockedwallpaper', mode: 'static', port: 9845, shape: 'array' },
+ { site: '1800swallpaper', mode: 'server', port: 9846, shape: 'items', forceMock: true, rep: true },
];
+// SITE=silkwallpaper,flockedwallpaper narrows the run to a subset (folded in from the former
+// prove-activation-one.mjs so there is exactly ONE copy of this gate — no drift between two files).
+const __only = (process.env.SITE || '').split(',').map(s => s.trim()).filter(Boolean);
+const PILOTS = __only.length ? PILOTS_ALL.filter(p => __only.includes(p.site)) : PILOTS_ALL;
const BORROW_NM = path.join(HOME, 'Projects/silkwallpaper/node_modules');
const MIME = { '.html':'text/html','.js':'text/javascript','.css':'text/css','.json':'application/json','.svg':'image/svg+xml','.png':'image/png','.jpg':'image/jpeg','.webp':'image/webp','.ico':'image/x-icon','.woff2':'font/woff2' };
@@ -70,6 +75,9 @@ async function run(engineLauncher, url, shape, opts) {
const errs=[], perrs=[];
const ctx = await browser.newContext();
const page = await ctx.newPage();
+ // Abort third-party ad/tracking noise (404/502 under localhost) that otherwise delays the deferred
+ // bundle so far that products load first and uxInit misreads false. Isolates the bundle fairly.
+ await page.route(/ads\.agentabrams|googletagmanager|google-analytics|doubleclick|facebook\.|fbcdn|fburl|connect\.facebook|hotjar|segment\.|mixpanel|\/embed\.js/, r => r.abort().catch(()=>{}));
if (opts.gridTimeout) await page.addInitScript(t => { window.__uxGridTimeout = t; }, opts.gridTimeout);
if (opts.absent) await page.route(/ux-primitives\.bundle\.(js|css)(\?|$)/, r => r.fulfill({ status:200, contentType: r.request().url().endsWith('.css')?'text/css':'text/javascript', body:'' }));
if (opts.api) await page.route(/\/api\/products(\?|$)/, async r => {
@@ -87,6 +95,15 @@ async function run(engineLauncher, url, shape, opts) {
await page.goto(url, { waitUntil:'domcontentloaded', timeout:20000 });
if (opts.sampleDuringMs) { await page.waitForTimeout(opts.sampleDuringMs); rec.during = await page.evaluate(measure); }
await page.waitForTimeout(opts.settleMs || 2500);
+ // Slow-load cases: don't measure on a fixed clock the DEFERRED bundle can outrun. goto resolves at
+ // domcontentloaded (before the deferred bundle runs), so the fetch start is offset from t0 by an
+ // unbounded amount under load. Wait (bounded) for products to actually render so a late-but-successful
+ // load isn't misread as a strand; on timeout we still measure (a genuine strand stays FAIL).
+ if (opts.awaitProductsMs) await page.waitForFunction(() => {
+ const g = document.querySelector('#grid') || document.querySelector('#productGrid');
+ if (!g) return false;
+ return [...g.children].some(c => c.nodeType===1 && !c.matches('[data-ux-skel-holder],[data-ux-skel],[data-ux-empty-holder]') && !c.classList.contains('ux-skel'));
+ }, { timeout: opts.awaitProductsMs, polling: 100 }).catch(()=>{});
rec.after = await page.evaluate(measure);
if (opts.doSort) {
await page.evaluate(() => { const s=document.querySelector('select'); if(s&&s.options.length>1){ s.selectedIndex=Math.min(1,s.options.length-1); s.dispatchEvent(new Event('change',{bubbles:true})); } const rg=document.querySelector('input[type=range]'); if(rg){ rg.dispatchEvent(new Event('input',{bubbles:true})); } });
@@ -101,12 +118,14 @@ async function run(engineLauncher, url, shape, opts) {
}
const results = [];
+const teardown = (h) => { try { if (h?.kill) h.kill(); else if (h?.close) h.close(); } catch {} };
for (const p of PILOTS) {
let handle;
try { if (p.mode==='server'){ handle=startServer(p); await waitPort(p.port); } else handle=await startStatic(p); }
- catch(e){ results.push({site:p.site, bootError:String(e)}); continue; }
+ catch(e){ teardown(handle); results.push({site:p.site, bootError:String(e)}); continue; }
+ try {
const url = `http://127.0.0.1:${p.port}/`;
- const apiLoad = p.mode==='server' ? { mode:'continue' } : { mode:'mock24' }; // real data vs mock for flocked
+ const apiLoad = p.forceMock ? { mode:'mock24' } : (p.mode==='server' ? { mode:'continue' } : { mode:'mock24' }); // mock for flocked + representative sites (isolate bundle from site data pipeline)
const rec = { site:p.site, mode:p.mode, engines:{} };
for (const [en, la] of [['chromium',chromium],['webkit',webkit]]) {
// error baseline (bundle absent), 2 normal runs
@@ -118,30 +137,39 @@ for (const p of PILOTS) {
const loading = await run(la,url,p.shape,{ api:{ ...apiLoad, delay:1500 }, sampleDuringMs:600, settleMs:3500 });
// present true-zero empty (short grid timeout + zero API)
const emptyRun = await run(la,url,p.shape,{ api:{ mode:'zero' }, gridTimeout:1200, settleMs:2600 });
+ // slow-but-NONEMPTY load resolving PAST the 8s skel timeout — empty-state must NOT strand (Cody v1.1.1 fix)
+ const slowRun = await run(la,url,p.shape,{ api:{ mode:'mock24', delay:10000 }, settleMs:3000, awaitProductsMs:14000 });
- const presentErrs = [normal, loading, emptyRun].flatMap(r=>r.jsErrors.map(norm));
+ const presentErrs = [normal, loading, emptyRun, slowRun].flatMap(r=>r.jsErrors.map(norm));
const newErrs = [...new Set(presentErrs)].filter(x=>!baseSet.has(x));
const BUNDLE_RX=/ux-primitives|UXPrimitives|UXGrid|\bToast\b|\bBento\b|ModalRig|Skeleton|EmptyState|CommandPalette/;
const bundleNew = newErrs.filter(x=>BUNDLE_RX.test(x));
- const anyPageErr = [normal,loading,emptyRun].some(r=>r.pageErrors.length);
+ // Page errors: isolate to the bundle the same way console errors are — subtract the bundle-absent
+ // baseline, then keep only bundle-attributable ones, so a site's OWN uncaught exception can't red the
+ // activation gate under a criterion (noBundlePageErr) whose name asserts the bundle caused it.
+ const basePageSet = new Set(absent.flatMap(r=>r.pageErrors.map(norm)));
+ const newPageErrs = [...new Set([normal,loading,emptyRun,slowRun].flatMap(r=>r.pageErrors.map(norm)))].filter(x=>!basePageSet.has(x));
+ const bundlePageErr = newPageErrs.filter(x=>BUNDLE_RX.test(x));
+ const anyPageErr = bundlePageErr.length>0;
const appearThenClear = (loading.during?.skel>0) && (loading.after?.skel===0) && (loading.after?.products>0);
const productsRender = normal.after?.products>0;
const sortNoGhost = normal.afterSort ? (normal.afterSort.skel===0 && normal.afterSort.empty===0 && normal.afterSort.products>0) : false;
const trueZeroEmpty = (emptyRun.after?.empty===1) && (emptyRun.after?.skel===0) && (emptyRun.after?.products===0);
const noEmptyWhenProducts = normal.after?.empty===0;
+ const slowLoadNoStrand = (slowRun.after?.empty===0) && (slowRun.after?.products>0) && (slowRun.after?.skel===0);
const controls = normal.after?.selects>=1 && normal.after?.ranges>=1;
- const pass = appearThenClear && productsRender && sortNoGhost && trueZeroEmpty && noEmptyWhenProducts && controls && !anyPageErr && bundleNew.length===0 && normal.ux==='object';
- rec.engines[en] = { pass, criteria:{ appearThenClear, productsRender, sortNoGhost, trueZeroEmpty, noEmptyWhenProducts, controls, noBundlePageErr:!anyPageErr, noNewBundleErr:bundleNew.length===0, uxInit:normal.ux==='object' },
- numbers: { during:loading.during, loadingAfter:loading.after, normalAfter:normal.after, afterSort:normal.afterSort, emptyAfter:emptyRun.after },
- newBundleErrors: bundleNew, flakySiteNoise: newErrs.filter(x=>!BUNDLE_RX.test(x)),
- absentErrRuns: absent.map(r=>r.jsErrors.length), presentPageErr: [normal,loading,emptyRun].map(r=>r.pageErrors.length) };
+ const pass = appearThenClear && productsRender && sortNoGhost && trueZeroEmpty && noEmptyWhenProducts && slowLoadNoStrand && controls && !anyPageErr && bundleNew.length===0 && normal.ux==='object';
+ rec.engines[en] = { pass, criteria:{ appearThenClear, productsRender, sortNoGhost, trueZeroEmpty, noEmptyWhenProducts, slowLoadNoStrand, controls, noBundlePageErr:!anyPageErr, noNewBundleErr:bundleNew.length===0, uxInit:normal.ux==='object' },
+ numbers: { during:loading.during, loadingAfter:loading.after, normalAfter:normal.after, afterSort:normal.afterSort, emptyAfter:emptyRun.after, slowAfter:slowRun.after },
+ newBundleErrors: bundleNew, newBundlePageErrors: bundlePageErr, flakySiteNoise: newErrs.filter(x=>!BUNDLE_RX.test(x)),
+ absentErrRuns: absent.map(r=>r.jsErrors.length), presentPageErr: [normal,loading,emptyRun,slowRun].map(r=>r.pageErrors.length) };
}
results.push(rec);
- if (handle.kill) handle.kill(); else if (handle.close) handle.close();
const e = rec.engines;
console.log(`${p.site}: ch.pass=${e.chromium?.pass} wk.pass=${e.webkit?.pass} | during.skel=${e.chromium?.numbers.during?.skel} after.skel=${e.chromium?.numbers.loadingAfter?.skel} products=${e.chromium?.numbers.normalAfter?.products} afterSort.skel=${e.chromium?.numbers.afterSort?.skel} emptyAfter=${JSON.stringify(e.chromium?.numbers.emptyAfter)} bundleErr=${JSON.stringify(e.chromium?.newBundleErrors)}`);
+ } finally { teardown(handle); } // always tear the child down — no orphaned server / stuck port on any path
}
const outfile = path.join(EVID, `pilot-activation-${ISO}.json`);
fs.writeFileSync(outfile, JSON.stringify(results,null,2));
← f4b8079 auto-data-snapshot: 2026-09-23T13:13:50 (1 data files) — sho
·
back to Agent Cabinet
·
proof(live-ux-canary): gate marker against the bundle hash c 56486bd →