← back to Stack Map Viewer
/5x REVISE fixes (TK-12030): persistent drawer (Esc+X only) + push-layout so chips are never occluded; heartbeat LAST RUN parses in-content ts, mtime only as flagged-UNVERIFIED fallback (kills the db-touch lie in the honesty feature itself)
09c69a9af08e19b921b6fa57aac970f9e568b146 · 2026-09-22 13:53:30 -0700 · Steve Abrams
Cody-gate findings #1 (over-aggressive dismiss) + #2 (mtime lie) fixed; #3 replayable probe saved to 5x/verify.mjs. Verified 12/12.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_018b1cDQdM4DFs1BGsqimvy3
Files touched
A 5x/verify.mjsM index.htmlM server.js
Diff
commit 09c69a9af08e19b921b6fa57aac970f9e568b146
Author: Steve Abrams <steve@designerwallcoverings.com>
Date: Tue Sep 22 13:53:30 2026 -0700
/5x REVISE fixes (TK-12030): persistent drawer (Esc+X only) + push-layout so chips are never occluded; heartbeat LAST RUN parses in-content ts, mtime only as flagged-UNVERIFIED fallback (kills the db-touch lie in the honesty feature itself)
Cody-gate findings #1 (over-aggressive dismiss) + #2 (mtime lie) fixed; #3 replayable probe saved to 5x/verify.mjs. Verified 12/12.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_018b1cDQdM4DFs1BGsqimvy3
---
5x/verify.mjs | 48 ++++++++++++++++++++++++++++++++++++++++++++++++
index.html | 13 ++++++++-----
server.js | 15 ++++++++++++++-
3 files changed, 70 insertions(+), 6 deletions(-)
diff --git a/5x/verify.mjs b/5x/verify.mjs
new file mode 100644
index 0000000..8d204c1
--- /dev/null
+++ b/5x/verify.mjs
@@ -0,0 +1,48 @@
+// /5x replayable verification probe — stack-map-viewer :9768
+// Run: node 5x/verify.mjs (needs playwright at ~/Projects/Designer-Wallcoverings/node_modules)
+import pkg from '/Users/macstudio3/Projects/Designer-Wallcoverings/node_modules/playwright/index.js';
+const { chromium } = pkg;
+const CHROME='/Applications/Google Chrome.app/Contents/MacOS/Google Chrome';
+const b=await chromium.launch({executablePath:CHROME});
+const ctx=await b.newContext({httpCredentials:{username:'admin',password:'DW2024!'},viewport:{width:1280,height:800}});
+const p=await ctx.newPage();
+const errs=[]; p.on('console',m=>{if(m.type()==='error')errs.push(m.text());}); p.on('pageerror',e=>errs.push('PAGEERR '+e.message));
+const R=[]; const rec=(n,ok,x='')=>{R.push((ok?'PASS':'FAIL')+' '+n+(x?' — '+x:''));};
+await p.goto('http://127.0.0.1:9768/',{waitUntil:'networkidle'}); await p.waitForTimeout(700);
+const chips=await p.$$('#folders .chip, #folders button');
+const openFirst=async()=>{await chips[3].click(); await p.waitForTimeout(300);};
+const isOpen=()=>p.$eval('#drawer',d=>d.classList.contains('open'));
+// A: push-layout — no chip covered while open
+await openFirst();
+let covered=0; for(const c of chips){const v=await c.evaluate(el=>{const r=el.getBoundingClientRect();const t=document.elementFromPoint(r.x+r.width/2,r.y+r.height/2);return !!(t&&t.closest('#drawer'));}); if(v)covered++;}
+rec('A push-layout: 0 chips covered while open', covered===0, covered+' covered');
+// B: formerly-covered chip reachable + opens
+let clicked=false; for(const c of chips){const t=(await c.innerText()).replace(/\s+/g,' ').trim(); if(/pricing \/ cost/.test(t)){try{await c.click({timeout:4000});clicked=true;}catch{clicked=false;}break;}}
+await p.waitForTimeout(250); rec('B formerly-covered chip clickable+opens', clicked&&await isOpen());
+// C: PERSISTENT — clicking a view button does NOT close (Cody #1)
+await p.$$eval('.vbtn',bs=>{const t=bs.find(x=>x.dataset.view==='tree'); if(t)t.click();}); await p.waitForTimeout(300);
+rec('C persistent: view-switch keeps drawer open', await isOpen());
+// back to map for reset test
+await p.$$eval('.vbtn',bs=>{const m=bs.find(x=>x.dataset.view==='map'); if(m)m.click();}); await p.waitForTimeout(300);
+// D: PERSISTENT — clicking Reset layout does NOT close (Cody #1)
+const rl=await p.$('#resetLayout');
+if(rl){const vis=await rl.isVisible(); if(vis){await rl.click(); await p.waitForTimeout(200); rec('D persistent: Reset-layout keeps drawer open', await isOpen());} else rec('D persistent: Reset-layout keeps drawer open', true, 'skip(not visible)');}
+else rec('D persistent Reset-layout', true, 'skip(no button)');
+// E: Esc closes
+await p.keyboard.press('Escape'); await p.waitForTimeout(200); rec('E Esc closes', !(await isOpen()));
+// F: ✕ closes
+await openFirst(); await p.click('#close'); await p.waitForTimeout(200); rec('F ✕ closes', !(await isOpen()));
+// G: node drag while open works + stays open (dispatched pointer events — map uses pointerdown/move/up, NOT mouse events)
+await openFirst();
+const drag=await p.evaluate(()=>{const g=document.querySelector('g.dnode');if(!g)return{moved:false,skip:true};const before=g.getAttribute('transform');const r=g.getBoundingClientRect();const cx=r.x+8,cy=r.y+8;const pe=(t,x,y)=>g.dispatchEvent(new PointerEvent(t,{bubbles:true,cancelable:true,clientX:x,clientY:y,pointerId:1,pointerType:'mouse',isPrimary:true}));pe('pointerdown',cx,cy);pe('pointermove',cx+70,cy+50);pe('pointerup',cx+70,cy+50);return{before,after:g.getAttribute('transform'),moved:before!==g.getAttribute('transform')};});
+rec('G node-drag moves + drawer persists', (drag.skip||(drag.moved&&await isOpen())), drag.skip?'skip(no node)':(drag.before+'→'+drag.after));
+// H: LAST RUN honest across tiers (API)
+const tiers=await p.evaluate(async()=>{const g=async(t,i)=>{const r=await fetch('/api/drill?type='+t+'&id='+i);const j=await r.json();return (j.info&&j.info.lastRun)||'';};return {officer:await g('officer','vp-engineering'),skillHB:await g('skill','fleet-health-rollup'),skillHBbad:await g('skill','creds-guard-canary'),idle:await g('agent','mcp-expert')};});
+rec('H1 officer LAST RUN via signal', /via ledger|via ticket/.test(tiers.officer), tiers.officer.slice(0,30));
+rec('H2 heartbeat uses in-content ts', /from "ts"/.test(tiers.skillHB), tiers.skillHB.slice(0,40));
+rec('H3 no-ts heartbeat flagged UNVERIFIED', /UNVERIFIED/.test(tiers.skillHBbad), tiers.skillHBbad.slice(0,40));
+rec('H4 idle agent never-recorded', /never recorded/.test(tiers.idle));
+rec('I zero console errors', errs.length===0, errs.slice(0,3).join(' | '));
+await b.close();
+console.log(R.join('\n'));
+console.log('\nSUMMARY: '+R.filter(x=>x.startsWith('PASS')).length+'/'+R.length+' PASS');
diff --git a/index.html b/index.html
index d8236c2..a07a754 100644
--- a/index.html
+++ b/index.html
@@ -62,6 +62,8 @@
#drawer{position:fixed;top:0;right:-460px;width:440px;height:100%;background:var(--panel);border-left:1px solid #2a3247;
transition:right .18s;overflow:auto;padding:18px;box-shadow:-8px 0 30px rgba(0,0,0,.5);z-index:50}
#drawer.open{right:0}
+ /* when the drill drawer is open, push the category chips out from under it so none are buried/unclickable */
+ @media(min-width:900px){body.drawer-open #folders{padding-right:456px;transition:padding-right .18s}}
#crumbs{font-size:12px;margin:0 0 8px;color:#9fc4ff;line-height:1.7}
#crumbs a{cursor:pointer;color:#9fc4ff;text-decoration:none}
#crumbs a:hover{text-decoration:underline}
@@ -314,7 +316,7 @@ function renderInfo(info){
}
async function renderDrill(){
const c=CRUMBS[CRUMBS.length-1];
- const dw=document.getElementById('drawer');dw.classList.add('open');
+ const dw=document.getElementById('drawer');dw.classList.add('open');document.body.classList.add('drawer-open');
document.getElementById('dtitle').textContent='…';document.getElementById('dlist').innerHTML='';document.getElementById('dcnt').textContent='';document.getElementById('dinfo').innerHTML='';
renderCrumbs();
const j=await fetchDrill(c.type,c.id);
@@ -330,12 +332,13 @@ function renderCrumbs(){
document.getElementById('crumbs').innerHTML=CRUMBS.map((c,i)=>'<a data-i="'+i+'">'+c.label+'</a>').join('<span class="sep">›</span>');
document.querySelectorAll('#crumbs a').forEach(a=>a.addEventListener('click',()=>{CRUMBS=CRUMBS.slice(0,+a.dataset.i+1);renderDrill();}));
}
-function closeDrawer(){document.getElementById('drawer').classList.remove('open');CRUMBS=[];}
+function closeDrawer(){document.getElementById('drawer').classList.remove('open');document.body.classList.remove('drawer-open');CRUMBS=[];}
document.getElementById('close').addEventListener('click',closeDrawer);
-// dismiss the drill card via Esc or a click outside it — an open fixed drawer otherwise
-// buries the right-column category chips with no affordance to reach them (only the ✕ closed it).
+// Persistent panel, NOT a modal: dismiss only via ✕ or Esc. An earlier "any click outside closes"
+// handler killed the cross-reference workflow (open a 5W card, then flip Map→Tree→Scrapers, or hit
+// Reset layout) by wiping the drawer on unrelated clicks. Reachability of the chips underneath is
+// handled by push-layout (body.drawer-open shifts #folders), so no outside-click dismiss is needed.
document.addEventListener('keydown',e=>{if(e.key==='Escape'&&document.getElementById('drawer').classList.contains('open'))closeDrawer();});
-document.addEventListener('mousedown',e=>{const dw=document.getElementById('drawer');if(dw.classList.contains('open')&&!e.target.closest('#drawer'))closeDrawer();},true);
// ── Tree view (recursive collapsible, same /api/drill) ───────────────────────
function treeRoots(d){return [
diff --git a/server.js b/server.js
index aaa159b..6d161bf 100644
--- a/server.js
+++ b/server.js
@@ -365,7 +365,20 @@ function skillInfo(id) {
} else {
// no vendor table → next-strongest TRUE run signal, else say so honestly (never the dir mtime)
const hb = path.join(dir, 'data/latest.json');
- if (existsP(hb)) { const m = new Date(mtimeP(hb)).toISOString(); lastRun = 'heartbeat ' + fmtWhen(m) + ' (' + ageOf(m) + ') · rewritten every run → data/latest.json'; }
+ if (existsP(hb)) {
+ // A file mtime is a db-touch lie (git pull / rsync -a / tar -x all rewrite it without the skill running).
+ // Prefer a TRUE timestamp field written INSIDE the heartbeat; fall back to mtime ONLY when none exists,
+ // and label that fallback as unverified so it is never mistaken for a real run signal. (Cody TK-12015; [[liveness-artifact-must-follow-the-side-effect]])
+ let ts = '', hbKey = '';
+ try {
+ const j = JSON.parse(readText(hb));
+ for (const k of ['ts', 'timestamp', 'generated_at', 'generatedAt', 'checked_at', 'checkedAt', 'run_at', 'runAt', 'last_run', 'lastRun', 'ranAt', 'asof', 'date', 'completed_at']) {
+ if (j && j[k] != null && !isNaN(Date.parse(String(j[k])))) { ts = new Date(j[k]).toISOString(); hbKey = k; break; }
+ }
+ } catch {}
+ if (ts) { lastRun = 'heartbeat ' + fmtWhen(ts) + ' (' + ageOf(ts) + ') · from "' + hbKey + '" in data/latest.json'; }
+ else { const m = new Date(mtimeP(hb)).toISOString(); lastRun = 'data/latest.json present · file mtime ' + fmtWhen(m) + ' (' + ageOf(m) + ') · ⚠ mtime-only, UNVERIFIED — no timestamp field inside the heartbeat (a git/rsync touch would move this)'; }
+ }
else if (refP.length) { lastRun = 'driven by cron ' + refP[0].replace(/\.plist$/, '') + ' — no per-run timestamp recorded'; }
else { lastRun = 'on-demand — no local run record kept'; }
}
← dc60ca8 Scrapers view: add Health column from live scraper-status.js
·
back to Stack Map Viewer
·
auto-data-snapshot: 2026-09-22T13:54:25 (1 data files) — 5x/ 4a01f55 →