Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
The table of contents is too big for display.
Diff view
Diff view
  •  
  •  
  •  
2 changes: 1 addition & 1 deletion .tours/03-debug-and-recovery.tour
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@
},
{
"file": "src/app/styles.css",
"line": 216,
"line": 217,
"anchor": "@media (max-width: 960px)",
"description": "The site of defect D1. This block used to say `.na-rail { display: none }`. CSS owned visibility, React owned mounting and gated on data — so a WebGL renderer was mounted into a 0x0 box, threw, and took the app down. The rail is now a bottom panel instead: one owner, no hidden-but-mounted state."
},
Expand Down
13 changes: 10 additions & 3 deletions HANDOFF.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
# NodeAgent developer handoff

Read this file first, then [the runtime walkthrough](docs/START_HERE.md). This review candidate starts from canonical `4e525fd0`. The separately preserved Pi draft is not integrated. Independent source and installed-consumer review passes for the bounded slice below. The [portable judgment](evidence/current-consumer-20260905/judge/E6e_NODEAGENT_CURRENT_CONSUMER_FINAL_JUDGE.md.txt), manifest and Git history identify the reviewed implementation and its later publication metadata.
Read this file first, then [the runtime walkthrough](docs/START_HERE.md). The current consumer/recovery repair merged in PR4 at canonical `5362505`; the source-header follow-on below starts from that exact revision. The separately preserved Pi draft is not integrated. Independent source and installed-consumer review passes for the bounded slice below. The [portable judgment](evidence/current-consumer-20260905/judge/E6e_NODEAGENT_CURRENT_CONSUMER_FINAL_JUDGE.md.txt), manifest and Git history identify the reviewed implementation and its later publication metadata.

A developer can run the repository's local scenario and generate a separate chat application without credentials. Both chats use scripted local adapters; displayed confidence numbers come from fixtures. Their browser conversations reset on reload. The library's durable/SQLite demonstration is a separate integration and does not persist either browser chat.

Expand Down Expand Up @@ -30,9 +30,16 @@ node e2e/current-consumer-recovery-proof.mjs "<generated app>" "<new recovery ev

Separate offline frame/durable/SQLite runs completed. SQLite closed/reopened the retained database and replayed the same frame with one journal entry; lease exclusion, expiry recovery and fencing passed. Use `nodeagent:frame:smoke`, `nodeagent:durable:smoke` and `nodeagent:sqlite:smoke` for that separate library integration. Their fixture confidence is not an external provider measurement.

Remaining limits are explicit. Source-header text still overflows by 21 pixels in the 390-pixel, doubled-computed-text fixture; the changed recovery/composer regions fit and remain visible. The text fixture is not an operating-system zoom certificate. The source graph still needs its own visual/readability assessment. There is no React render-error boundary or New Thread control. Full visual, responsive, accessibility and performance grades remain null. Source full audit has nine development advisories, including one critical; the locked generated app has four development advisories, three high and one low. Both recorded production-only audits have zero findings. Those full-audit findings remain open.
Remaining limits are explicit. The prior source-header overflow is corrected by the separately judged header follow-on below; existing enlarged empty-state clipping and graph readability remain open. The text fixture is not an operating-system zoom certificate. The source graph still needs its own visual/readability assessment. There is no React render-error boundary or New Thread control. Full visual, responsive, accessibility and performance grades remain null. Source full audit has nine development advisories, including one critical; the locked generated app has four development advisories, three high and one low. Both recorded production-only audits have zero findings. Those full-audit findings remain open.

No provider, Convex, login, external-host hook, deployment or Pi capability was activated. The historical dependency checkpoint and previous promotion records retain their original outcomes. This handoff closes the bounded offline consumer and response-recovery implementation for independent review; it does not claim complete repository readiness.


Independent replay adds130 checks/34 captures, including late Stop after an actual completed tool result and two distinct failed-response Retry identities. Fresh41 tests, typecheck and citations pass. The original publication judgment records60 line-ending mismatches; packet-local attributes preserve those exact raw bytes before commit. Existing header overflow, graph readability, development advisories, browser reset and provider/durability limits remain unchanged. This local source/installed proof does not certify deployment or the whole product.
Independent replay adds130 checks/34 captures, including late Stop after an actual completed tool result and two distinct failed-response Retry identities. Fresh41 tests, typecheck and citations pass. The original publication judgment records60 line-ending mismatches; packet-local attributes preserve those exact raw bytes before commit. Graph readability, development advisories, browser reset and provider/durability limits remain unchanged; the later header repair is reviewed separately below. This local source/installed proof does not certify deployment or the whole product.


## Source header reflow follow-on

The two resource links now stay together when enlarged text requires a second row. The header grows with its content; all six normal header layouts remain unchanged. [The portable packet](evidence/header-reflow-20260905/README.md) contains matched before/after pixels, the independent60-check/14-capture review and the old-layout knockout. Root's72 header assertions,145 existing response-recovery assertions/38 captures,41 tests, typecheck/build and citations pass. No generated consumer or runtime adapter changed. The prior44-criterion assessment still binds5362505; this bounded repair does not automatically replace its historical scores or certify complete grades.

Replay the current header with `node e2e/header-reflow-proof.mjs . "<absolute new output directory>" after` after installing the checked-in dependencies and Chromium. The source-bound record and Git history identify this follow-on's eventual commit; no production deployment is claimed.
2 changes: 1 addition & 1 deletion docs/codebase/TESTING.md
Original file line number Diff line number Diff line change
Expand Up @@ -50,7 +50,7 @@ from `src/`. See `docs/SIMPLIFICATION_REPORT.md`.

For the browser gate, the equivalent is to re-hide the graph rail on narrow
screens (`src/app/styles.css`, the `@media (max-width: 960px)` block at line
181). `e2e/capture-journey-at-width.mjs` asserts the graph canvas is not
217). `e2e/capture-journey-at-width.mjs` asserts the graph canvas is not
zero-width — the *cause* of defect D1, not its symptom — so any mechanism that
re-hides the rail turns the gate red.

Expand Down
87 changes: 87 additions & 0 deletions e2e/header-reflow-proof.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,87 @@
import fs from 'node:fs/promises';
import path from 'node:path';
import assert from 'node:assert/strict';
import {createRequire} from 'node:module';
import {pathToFileURL} from 'node:url';
import {createHash} from 'node:crypto';
import {execFileSync} from 'node:child_process';
const root=path.resolve(process.argv[2]??'');
const output=path.resolve(process.argv[3]??'');
const phase=process.argv[4];
assert.ok(process.argv[2] && process.argv[3] && ['before','after'].includes(phase));
await fs.mkdir(output,{recursive:false});
const require=createRequire(path.join(root,'package.json'));
const {chromium}=require('playwright');
const {createServer}=await import(pathToFileURL(require.resolve('vite')).href);
const sha=b=>createHash('sha256').update(b).digest('hex');
const files=['src/app/styles.css','src/features/node-agent/components/NodeAgentDemoApp.tsx','src/features/node-agent/components/NodeAgentThread.tsx','src/features/node-agent/runtime/nodeAgentChatAdapter.ts','package.json','package-lock.json'];
const hashes=async()=>Object.fromEntries(await Promise.all(files.map(async f=>[f,sha(await fs.readFile(path.join(root,f)))])));
const report={proof:'NODEAGENT-ENLARGED-HEADER-01',phase,at:new Date().toISOString(),head:execFileSync('git',['rev-parse','HEAD'],{cwd:root}).toString().trim(),sourceBefore:await hashes(),checks:[],cells:[],error:null,fullGrades:null};
function check(name,passed){report.checks.push({name,passed:Boolean(passed)});assert.ok(passed,name);}
let server,browser;
try{
server=await createServer({root,envDir:false,server:{host:'127.0.0.1',port:0,strictPort:true,open:false}});
await server.listen();
const address=server.httpServer.address();
const base=`http://127.0.0.1:${address.port}`;
browser=await chromium.launch({headless:true});
report.browser=browser.version();
for(const width of [320,390,768,1024,1440,1920])for(const scale of [1,2]){
const context=await browser.newContext({viewport:{width,height:900},reducedMotion:'reduce'});
const page=await context.newPage();
const errors=[];page.on('pageerror',e=>errors.push(String(e)));
await context.route('**/*',route=>{const u=new URL(route.request().url());return u.origin===base||['fonts.googleapis.com','fonts.gstatic.com'].includes(u.hostname)?route.continue():route.abort();});
await page.goto(base,{waitUntil:'networkidle'});
await page.locator('.na-appbar').waitFor();
await page.evaluate(()=>document.fonts.ready);
const fonts=await page.evaluate(()=>({ready:document.fonts.status,manrope:document.fonts.check('14px Manrope'),mono:document.fonts.check('11px "JetBrains Mono"')}));
if(scale===2)await page.evaluate(()=>{
const list=Array.from(document.querySelectorAll('body,body *')).map(e=>({e,size:parseFloat(getComputedStyle(e).fontSize)}));
for(const {e,size}of list)if(Number.isFinite(size))e.style.fontSize=`${size*2}px`;
});
await page.evaluate(()=>new Promise(r=>requestAnimationFrame(()=>requestAnimationFrame(r))));
const state=await page.evaluate(()=>{
const box=e=>{const r=e.getBoundingClientRect();return{x:r.x,y:r.y,width:r.width,height:r.height,right:r.right,bottom:r.bottom};};
const header=document.querySelector('.na-appbar');
const brand=document.querySelector('.na-brand');
const links=Array.from(header.querySelectorAll('a')).map(e=>({text:e.textContent.trim(),href:e.getAttribute('href'),box:box(e),scrollWidth:e.scrollWidth,clientWidth:e.clientWidth}));
return {viewport:{width:innerWidth,height:innerHeight},header:box(header),brand:box(brand),links,main:box(document.querySelector('.na-main')),documentOverflow:document.documentElement.scrollWidth-innerWidth,headerScrollWidth:header.scrollWidth,headerClientWidth:header.clientWidth,headerCss:{height:getComputedStyle(header).height,flexWrap:getComputedStyle(header).flexWrap,flexShrink:getComputedStyle(header).flexShrink},brandText:brand.textContent.trim()};
});
const name=`${phase}-${width}-text-${scale*100}`;
const html=await page.content();await fs.writeFile(path.join(output,name+'.html'),html);
await page.screenshot({path:path.join(output,name+'.png')});
const sidecar={...state,fonts,errors,resizeMethod:'All original computed element text sizes sampled once then doubled; no browser/OS zoom claim.'};
await fs.writeFile(path.join(output,name+'.json'),JSON.stringify(sidecar,null,2)+'\n');
if(phase==='before'&&((scale===2&&width<=390)||(scale===1&&width===1440))){
await page.evaluate(()=>{const e=document.querySelector('.na-appbar');e.style.outline='3px solid #ff007f';e.style.outlineOffset='-3px';const label=document.createElement('div');label.id='proof-boundary';label.textContent='A · existing header owner · BEFORE';Object.assign(label.style,{position:'fixed',top:`${e.getBoundingClientRect().bottom+4}px`,left:'4px',padding:'2px 5px',background:'#ff007f',color:'white',font:'12px sans-serif',zIndex:'2147483647',pointerEvents:'none'});document.body.append(label);});
await fs.writeFile(path.join(output,name+'-boundary.html'),await page.content());
await page.screenshot({path:path.join(output,name+'-boundary.png')});
}
check(name+' no runtime exception',errors.length===0);
if(phase==='after'){
check(name+' header fits viewport',state.header.x>=-0.5&&state.header.right<=width+0.5&&state.headerScrollWidth<=state.headerClientWidth+1);
check(name+' branding stays inside header',state.brand.x>=state.header.x&&state.brand.right<=state.header.right+0.5&&state.brand.y>=state.header.y&&state.brand.bottom<=state.header.bottom+0.5);
check(name+' links remain visible and legible',state.links.length===2&&state.links.every(l=>l.box.x>=0&&l.box.right<=width+0.5&&l.box.y>=0&&l.box.bottom<=state.header.bottom+0.5&&l.scrollWidth<=l.clientWidth+1));
check(name+' main starts below header',state.main.y>=state.header.bottom-0.5);
const focus=[];
await page.keyboard.press('Tab');
for(let i=0;i<12&&focus.length<2;i++){
const f=await page.evaluate(()=>{const e=document.activeElement;if(!e?.matches('.na-appbar a'))return null;const r=e.getBoundingClientRect();return{text:e.textContent.trim(),left:r.left,right:r.right,top:r.top,bottom:r.bottom,focusVisible:e.matches(':focus-visible')};});
if(f&&!focus.some(x=>x.text===f.text))focus.push(f);
await page.keyboard.press('Tab');
}
check(name+' both native header links reachable by keyboard',focus.length===2&&focus.every(f=>f.focusVisible&&f.left>=0&&f.right<=width+0.5&&f.top>=0&&f.bottom<=900));
sidecar.focus=focus;await fs.writeFile(path.join(output,name+'.json'),JSON.stringify(sidecar,null,2)+'\n');
}
report.cells.push({name,...sidecar,pngSha256:sha(await fs.readFile(path.join(output,name+'.png')))});
await context.close();
}
}catch(e){report.error=String(e.stack??e);process.exitCode=1;}
finally{
await browser?.close();await server?.close();
report.sourceAfter=await hashes();
report.sourceUnchanged=JSON.stringify(report.sourceBefore)===JSON.stringify(report.sourceAfter);
report.passed=!report.error&&report.sourceUnchanged;
await fs.writeFile(path.join(output,'report.json'),JSON.stringify(report,null,2)+'\n');
console.log(JSON.stringify({proof:report.proof,phase,passed:report.passed,cells:report.cells.length,checks:report.checks.length,error:report.error}));
}
1 change: 1 addition & 0 deletions evidence/header-reflow-20260905/.gitattributes
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
* -text
Loading
Loading