From 9d66d6ca9ecf13d8a8209283e611d0fce652b6f4 Mon Sep 17 00:00:00 2001 From: Garry Tan Date: Thu, 10 Sep 2026 10:38:16 +0000 Subject: [PATCH] test: recognize completed review decisions and queued permissions --- test/autoplan-edit-queue-am.test.ts | 188 ++++++++++++++ test/ceo-assertion-header-am.test.ts | 89 +++++++ test/ceo-section-loading-fixture.test.ts | 43 ++++ test/design-variant-choice-am.test.ts | 122 ++++++++++ test/devex-seed-coverage.test.ts | 151 ++++++++++++ test/eng-cache-brief-am.test.ts | 58 +++++ test/eng-legacy-contract-am.test.ts | 64 +++++ test/eng-retry-contract-am.test.ts | 45 ++++ test/fixtures/autoplan-edit-queue-am.json | 211 ++++++++++++++++ .../ceo-assertion-header-am-calls.json | 229 ++++++++++++++++++ .../design-variant-choice-am-retry.json | 60 +++++ test/fixtures/design-variant-choice-am.json | 60 +++++ test/fixtures/dx-declarative-choices-am.json | 170 +++++++++++++ test/fixtures/eng-cache-brief-am.json | 110 +++++++++ test/fixtures/eng-legacy-contract-am.json | 138 +++++++++++ test/fixtures/eng-retry-contract-am.json | 138 +++++++++++ test/helpers/autoplan-artifact-permission.ts | 105 ++++++-- test/helpers/autoplan-artifact-recorder.ts | 11 +- test/helpers/ceo-section-loading-fixture.ts | 13 +- test/helpers/claude-pty-runner.ts | 57 ++++- test/helpers/design-count-review.ts | 41 +++- test/helpers/devex-seed-coverage.ts | 48 +++- test/helpers/eng-seeded-coverage.ts | 92 ++++++- test/helpers/plan-count-transcript.ts | 8 +- test/helpers/touchfiles-data.ts | 14 ++ test/skill-e2e-autoplan-chain.test.ts | 7 +- test/skill-e2e-plan-ceo-finding-count.test.ts | 23 ++ 27 files changed, 2250 insertions(+), 45 deletions(-) create mode 100644 test/autoplan-edit-queue-am.test.ts create mode 100644 test/ceo-assertion-header-am.test.ts create mode 100644 test/design-variant-choice-am.test.ts create mode 100644 test/eng-cache-brief-am.test.ts create mode 100644 test/eng-legacy-contract-am.test.ts create mode 100644 test/eng-retry-contract-am.test.ts create mode 100644 test/fixtures/autoplan-edit-queue-am.json create mode 100644 test/fixtures/ceo-assertion-header-am-calls.json create mode 100644 test/fixtures/design-variant-choice-am-retry.json create mode 100644 test/fixtures/design-variant-choice-am.json create mode 100644 test/fixtures/dx-declarative-choices-am.json create mode 100644 test/fixtures/eng-cache-brief-am.json create mode 100644 test/fixtures/eng-legacy-contract-am.json create mode 100644 test/fixtures/eng-retry-contract-am.json diff --git a/test/autoplan-edit-queue-am.test.ts b/test/autoplan-edit-queue-am.test.ts new file mode 100644 index 000000000..29b8fb162 --- /dev/null +++ b/test/autoplan-edit-queue-am.test.ts @@ -0,0 +1,188 @@ +import {expect,test} from 'bun:test'; +import fs from 'node:fs'; +import os from 'node:os'; +import path from 'node:path'; +import fixture from './fixtures/autoplan-edit-queue-am.json'; +import * as permission from './helpers/autoplan-artifact-permission'; +import {readPendingAutoplanArtifact} from './helpers/autoplan-artifact-recorder'; +import {readPlanCountTranscript,type NativePublicToolEvent} from './helpers/plan-count-transcript'; +import {E2E_TOUCHFILES,selectTests} from './helpers/touchfiles'; + +function setup(changeRecords?:(records:any[])=>void){ + const dir=fs.mkdtempSync(path.join(os.tmpdir(),'gstack-ap-queue-')); + const cwd=path.join(dir,path.basename(fixture.cwd)),config=path.join(dir,'config'); + const stateRoot=path.join(dir,'gstack-hermetic-2101964-HvDZyN/skill-home-zgCNxG/.gstack'); + const replace=(text:string)=>text.replaceAll(fixture.stateRoot,stateRoot).replaceAll(fixture.cwd,cwd).replaceAll(fixture.config,config); + const hook=JSON.parse(replace(JSON.stringify(fixture.hook))); + const file=hook.pending.file,nativeFile=path.join(config,'projects','owned',hook.sessionId+'.jsonl');hook.pending.transcriptPath=nativeFile; + fs.mkdirSync(cwd,{recursive:true});fs.mkdirSync(path.dirname(file),{recursive:true});fs.mkdirSync(path.dirname(nativeFile),{recursive:true}); + fs.writeFileSync(file,fixture.before);fs.utimesSync(file,new Date(fixture.now-1000000),new Date(Date.parse(hook.pending.timestamp)-1000)); + const events=JSON.parse(replace(JSON.stringify(fixture.publicTools))) as (NativePublicToolEvent & {messageId?:string;requestId?:string})[]; + const records=events.map(e=>({sessionId:e.sessionId,cwd,isSidechain:false,timestamp:e.timestamp,requestId:e.requestId,message:{id:e.messageId,role:e.kind==='use'?'assistant':'user',content:e.kind==='use'?[{type:'tool_use',id:e.toolUseId,name:e.name,input:e.input}]:[{type:'tool_result',tool_use_id:e.toolUseId,content:'',is_error:e.isError}]}})); + changeRecords?.(records); + fs.writeFileSync(nativeFile,records.map(r=>JSON.stringify(r)).join('\n')+'\n'); + const hookFile=path.join(dir,'hook.json');fs.writeFileSync(hookFile,JSON.stringify(hook)+'\n'); + const publicTools:NativePublicToolEvent[]=[];const native=readPlanCountTranscript(config,cwd,e=>publicTools.push(e)); + const pending=(readPendingAutoplanArtifact as any)(hookFile,cwd,config,stateRoot,fixture.commandStartedAt,publicTools,fixture.now,true); + const context={cwd,ownedStateRoot:stateRoot,commandStartedAt:fixture.commandStartedAt,now:fixture.now,viewportCapturedAt:fixture.now,transcriptStatus:native.status,publicTools,pending}; + const invoke=(screen=fixture.viewport,ctx:any=context,seen=new Set())=>(permission as any).publishedAutoplanArtifactPermissionInput?.(screen,ctx,seen)??null; + return {dir,cwd,config,stateRoot,hook,hookFile,file,nativeFile,publicTools,context,invoke,dispose:()=>fs.rmSync(dir,{recursive:true,force:true})}; +} + +test('the actual active hook binds its published request amid later queued edits and both native prefix forms',()=>{ + const s=setup();try{ + expect(permission.autoplanArtifactPermissionInput(fixture.viewport,s.context,new Set())).toBeNull(); + expect(permission.pendingAutoplanArtifactPermissionInput(fixture.viewport,s.context,new Set())).toBeNull(); + expect(s.context.pending?.toolUseId).toBe(fixture.hook.pending.toolUseId); + expect(s.invoke()?.signature).toBe(`${fixture.hook.sessionId}:${fixture.hook.pending.toolUseId}`); + expect(s.invoke()?.file).toBe(s.file); + expect(s.invoke()?.input).toBe('1\r'); + }finally{s.dispose()} +}); + +test('the default metadata-only reader continues excluding a published request',()=>{ + const s=setup();try{expect(readPendingAutoplanArtifact(s.hookFile,s.cwd,s.config,s.stateRoot,fixture.commandStartedAt,s.publicTools,fixture.now)).toBeUndefined();}finally{s.dispose()} +}); + +type Replay=ReturnType; +const current=(s:Replay)=>s.context.publicTools.find(e=>e.kind==='use'&&e.toolUseId===fixture.hook.pending.toolUseId)!; +const queued=(s:Replay)=>s.context.publicTools.find(e=>e.kind==='use'&&e.toolUseId==='toolu_01SYiANcdq3hLqGxEhDQVNJf')!; +function rejects(cases:Array<[string,(s:Replay)=>void]>){ + for(const [name,change] of cases){const s=setup();try{change(s);expect(s.invoke(),name).toBeNull()}finally{s.dispose()}} +} + +test('only exact native message and request identifiers establish queued membership',()=>{ + const s=setup();try{ + expect(current(s).messageId).toBe('msg_011CeuYDnRH9L1Qoom8gBVdc'); + expect(current(s).requestId).toBe('req_011CeuYDk9cAd6Yozh8QnH62'); + expect(queued(s).messageId).toBe(current(s).messageId); + }finally{s.dispose()} + for(const change of [ + (r:any)=>{delete r.message.id},(r:any)=>{delete r.requestId}, + (r:any)=>{r.message.id='quoted msg_example'},(r:any)=>{r.requestId='req_'+ 'a'.repeat(161)}, + ]){const s=setup(records=>{for(const r of records)if(r.message.content[0]?.id===fixture.hook.pending.toolUseId)change(r)});try{ + expect(current(s).messageId).toBeUndefined();expect(current(s).requestId).toBeUndefined();expect(s.invoke()).toBeNull(); + }finally{s.dispose()}} +}); + +test.each(['one native record','equal timestamps'])('ordered later blocks in %s remain queued behind the current hook',shape=>{ + const ids=['toolu_01SYiANcdq3hLqGxEhDQVNJf','toolu_01LgaibBToDfuxGNFBKew9PS','toolu_01VqJFXfD5cfdjiar1gAjpkV']; + const s=setup(records=>{ + const active=records.find(r=>r.message.content[0]?.id===fixture.hook.pending.toolUseId)!; + for(let i=records.length-1;i>=0;i--){const r=records[i];if(!ids.includes(r.message.content[0]?.id))continue; + if(shape==='one native record'){active.message.content.splice(1,0,r.message.content[0]);records.splice(i,1)} + else r.timestamp=active.timestamp; + } + });try{ + const active=current(s),remaining=s.publicTools.filter(e=>e.kind==='use'&&ids.includes(e.toolUseId)); + expect(remaining.map(e=>e.toolUseId)).toEqual(ids); + expect(remaining.every(e=>e.timestamp===active.timestamp&&e.messageId===active.messageId&&e.requestId===active.requestId)).toBe(true); + expect(s.invoke()?.signature).toBe(s.hook.sessionId+':'+s.hook.pending.toolUseId); + // Moving a same-time unresolved block ahead of the current request is not a queued successor. + const earlier=remaining[0]!,events=s.context.publicTools;events.splice(events.indexOf(earlier),1);events.splice(events.indexOf(active),0,earlier); + expect(s.invoke()).toBeNull(); + }finally{s.dispose()} +}); + +test('another batch, session, path, tool, malformed edit or already hooked successor cannot be ignored',()=>{ + rejects([ + ['foreign message',s=>{queued(s).messageId='msg_other'}], + ['foreign request',s=>{queued(s).requestId='req_other'}], + ['missing message',s=>{delete queued(s).messageId}], + ['foreign session',s=>{queued(s).sessionId='foreign'}], + ['foreign file',s=>{queued(s).input!.file_path=s.file+'.other'}], + ['queued Write',s=>{queued(s).name='Write'}], + ['empty old request',s=>{queued(s).input!.old_string=''}], + ['missing replacement',s=>{delete queued(s).input!.new_string}], + ['replace all',s=>{queued(s).input!.replace_all=true}], + ['already hooked',s=>{s.context.pending!.hookSeenIds!.push(queued(s).toolUseId)}], + ['older unresolved',s=>{s.context.publicTools=s.context.publicTools.filter(e=>!(e.kind==='result'&&e.toolUseId==='toolu_01BbKwZ7JFFdm2FLFdcNQXPq'))}], + ]); +}); + +test('current hook identity, completed or failed requests and ordering cannot be overridden',()=>{ + rejects([ + ['foreign pending',s=>{s.context.pending!.sessionId='foreign'}], + ['wrong current hook',s=>{s.context.pending!.toolUseId=queued(s).toolUseId}], + ['missing hook',s=>{s.context.pending=undefined}], + ['missing tombstones',s=>{delete s.context.pending!.hookSeenIds}], + ['duplicate tombstone',s=>{s.context.pending!.hookSeenIds!.push(fixture.hook.pending.toolUseId)}], + ['unseen current',s=>{s.context.pending!.hookSeenIds=[]}], + ['duplicate current',s=>{const at=s.context.publicTools.indexOf(current(s));s.context.publicTools.splice(at,0,structuredClone(current(s)))}], + ['completion',s=>{s.context.publicTools.push({kind:'result',sessionId:s.hook.sessionId,toolUseId:current(s).toolUseId,timestamp:s.hook.pending.timestamp,isError:false})}], + ['failure',s=>{s.context.publicTools.push({kind:'result',sessionId:s.hook.sessionId,toolUseId:current(s).toolUseId,timestamp:s.hook.pending.timestamp,isError:true})}], + ['completed queued',s=>{s.context.publicTools.push({kind:'result',sessionId:s.hook.sessionId,toolUseId:queued(s).toolUseId,timestamp:s.hook.pending.timestamp,isError:false})}], + ['failed queued',s=>{s.context.publicTools.push({kind:'result',sessionId:s.hook.sessionId,toolUseId:queued(s).toolUseId,timestamp:s.hook.pending.timestamp,isError:true})}], + ['late predecessor completion',s=>{s.context.publicTools.at(-1)!.timestamp=new Date(Date.parse(s.hook.pending.timestamp)+1).toISOString()}], + ['no successful predecessor',s=>{for(const e of s.context.publicTools)if(e.kind==='result')e.isError=true}], + ['out of order',s=>{s.context.publicTools.reverse()}], + ['future publication',s=>{queued(s).timestamp=new Date(fixture.now+1).toISOString()}], + ]); +}); + +test('exact digest and current before file are required independently of the visible subset',()=>{ + rejects([ + ['missing digest',s=>{delete s.context.pending!.editDigest}], + ['malformed digest',s=>{s.context.pending!.editDigest.version=2}], + ['different request hash',s=>{s.context.pending!.editDigest.requestSHA256='0'.repeat(64)}], + ['different before hash',s=>{s.context.pending!.editDigest.beforeSHA256='0'.repeat(64)}], + ['different old lines',s=>{s.context.pending!.editDigest.oldLineHashes=['0'.repeat(64)]}], + ['different new lines',s=>{s.context.pending!.editDigest.newLineHashes=['0'.repeat(64)]}], + ['changed old request',s=>{current(s).input!.old_string+=' '}], + ['changed replacement',s=>{current(s).input!.new_string+=' '}], + ['missing current file',s=>{fs.unlinkSync(s.file)}], + ['changed current file with old mtime',s=>{fs.writeFileSync(s.file,fixture.before+'\nChanged.');fs.utimesSync(s.file,new Date(0),new Date(0))}], + ['file updated after hook',s=>{fs.utimesSync(s.file,new Date(fixture.now),new Date(fixture.now))}], + ['stale viewport',s=>{s.context.viewportCapturedAt=Date.parse(s.hook.pending.timestamp)-1}], + ['stale hook',s=>{s.context.pending!.timestamp=new Date(fixture.commandStartedAt-1).toISOString()}], + ['future viewport',s=>{s.context.viewportCapturedAt=fixture.now+1}], + ['unavailable native',s=>{s.context.transcriptStatus='missing'}], + ]); + const s=setup();try{ + expect(s.invoke(fixture.viewport,s.context,new Set([s.hook.sessionId+':'+s.hook.pending.toolUseId]))).toBeNull(); + expect(s.invoke(fixture.viewport,s.context,new Set([permission.autoplanArtifactMenuKey(fixture.viewport)]))).toBeNull(); + }finally{s.dispose()} +}); + +test('invalid, busy, foreign or ambiguous persisted hook state supplies no current authority',()=>{ + for(const change of [ + (s:Replay)=>{fs.writeFileSync(s.hookFile+'.invalid','{"reason":"conflicting_replay"}')}, + (s:Replay)=>{fs.writeFileSync(s.hookFile+'.lock','')}, + (s:Replay)=>{s.hook.pending.transcriptPath=path.join(s.dir,'foreign.jsonl');fs.writeFileSync(s.hookFile,JSON.stringify(s.hook))}, + (s:Replay)=>{s.hook.pending.hookSeenIds=[];fs.writeFileSync(s.hookFile,JSON.stringify(s.hook))}, + ]){const s=setup();try{change(s);expect(readPendingAutoplanArtifact(s.hookFile,s.cwd,s.config,s.stateRoot,fixture.commandStartedAt,s.publicTools,fixture.now,true)).toBeUndefined()}finally{s.dispose()}} +}); + +test('existing prefix forms compose but cannot hide a competing title, source or malformed current panel',()=>{ + const s=setup();try{ + const first=fixture.viewport.indexOf('● Update('),screen=fixture.viewport.slice(first); + const titles=screen.match(/^● Update\([^\n]+\)\n/gm)!; + expect(titles).toHaveLength(4); + expect(s.invoke(screen)?.input).toBe('1\r'); + expect(s.invoke('\n\n'+screen)?.input).toBe('1\r'); + expect(s.invoke(fixture.viewport.replaceAll(titles[0]!,''))).toBeNull(); // A completed prefix still needs its current tool boundary. + expect(s.invoke(screen.slice(screen.indexOf('────────────────')))?.input).toBe('1\r'); + let one=screen;for(let n=0;n<3;n++)one=one.replace(titles[0]!,''); + expect(s.invoke(one.trimStart())?.input).toBe('1\r'); + for(const [name,changed] of [ + ['foreign first title',fixture.viewport.replace(titles[0]!,titles[0]!.replace('user-dashboard.md','foreign.md'))], + ['quoted whole pane',fixture.viewport.split('\n').map(row=>'> '+row).join('\n')], + ['source prefix','Example:\n'+fixture.viewport], + ['arbitrary indented prose',' This is an example.\n'+fixture.viewport], + ['competing completed panel','● Update(/tmp/foreign.md)\n'+fixture.viewport], + ['broken wrap kind',fixture.viewport.replace(/^ \+/m,' -')], + ['foreign displayed path',fixture.viewport.replace('…2101964-HvDZyN','…foreign')], + ['wrong menu target',fixture.viewport.replace('user-dashboard.md?','foreign.md?')], + ['persistent edit mode',fixture.viewport.replace('❯ 1. Yes','❯ 2. Yes')], + ['malformed no',fixture.viewport.replace('3. No','3. Maybe')], + ['changed addition',fixture.viewport.replace(/^( {0,3}\d+ \+).*/m,'$1A different current edit')], + ])expect(s.invoke(changed),name).toBeNull(); + }finally{s.dispose()} +}); + +test('the new queue regression files select only the Autoplan owner with dense registration',()=>{ + const owner=E2E_TOUCHFILES['autoplan-chain-pty']!; + for(let i=0;i['questions'][number]) => void) { + const call = structuredClone(fp.nativeCall!); + const selected = call.questions[0]!.options.findIndex(o => o.label === call.answers?.[call.questions[0]!.question]); + edit(call.questions[0]!); + call.answers = { [call.questions[0]!.question]: call.questions[0]!.options[selected]!.label }; + return nativePlanCallFingerprint(call, fp.observedAtMs, fp.preReview); +} + +for (const [i, fp] of findings.entries()) { + test(`the actual completed assertion finding ${i + 1} starts review with its descriptive header`, () => { + expect(ceoFirstReviewAUQ(fp)).toBe(true); + }); +} +test('routing and implementation layout remain setup', () => { + for (const fp of calls.slice(0, 2)) expect(ceoFirstReviewAUQ(fp)).toBe(false); +}); +test('the same current issue is already recognized with an explicit numbered header', () => { + findings.forEach((fp, i) => expect(ceoFirstReviewAUQ(change(fp, q => { q.header = `Issue ${i + 1}`; }))).toBe(true)); +}); +test('a competing numbered header cannot borrow the title issue', () => { + findings.forEach((fp, i) => expect(ceoFirstReviewAUQ(change(fp, q => { q.header = `Issue ${i + 2}`; }))).toBe(false)); +}); +test('only the completed owned native decision supplies the finding', () => { + for (const fp of findings) { + for (const mutate of [ + (x: AskUserQuestionFingerprint) => { x.nativeCall!.answered = false; }, + (x: AskUserQuestionFingerprint) => { x.nativeCall!.failed = true; }, + (x: AskUserQuestionFingerprint) => { x.nativeCall!.unansweredQuestionIndices = [0]; }, + (x: AskUserQuestionFingerprint) => { x.signature = 'foreign:call'; }, + (x: AskUserQuestionFingerprint) => { x.nativeCall!.answers = {}; }, + (x: AskUserQuestionFingerprint) => { x.options[0]!.label = 'different menu'; }, + ]) { + const modified = structuredClone(fp); mutate(modified); + expect(ceoFirstReviewAUQ(modified)).toBe(false); + } + } +}); +test('descriptive headers and decision ordinals do not replace the issue identity', () => { + for (const fp of findings) { + for (const titlePrefix of ['D19', 'd4']) { + expect(ceoFirstReviewAUQ(change(fp, q => { q.question = q.question.replace(/^D\d+/, titlePrefix); }))).toBe(true); + } + expect(ceoFirstReviewAUQ(change(fp, q => { q.header = 'Test contract'; }))).toBe(true); + expect(ceoFirstReviewAUQ(change(fp, q => { q.question = q.question.replace(/^Recommendation: \d+/m, 'Recommendation: 9'); }))).toBe(false); + } +}); +test('source, earlier and conditional framing cannot own the current assessment', () => { + for (const fp of findings) { + for (const prefix of ['Source excerpt:', 'The following assessment is hypothetical.', 'Earlier review assessment:', 'If approved:', 'Source:', 'Example:', 'Historical review:']) { + expect(ceoFirstReviewAUQ(change(fp, q => { q.question = q.question.replace('\nELI10:', `\n${prefix}\nELI10:`); }))).toBe(false); + } + for (const prefix of ['Source excerpt. ', 'Previously, ', 'If approved, ']) { + expect(ceoFirstReviewAUQ(change(fp, q => { q.question = q.question.replace('ELI10: ', `ELI10: ${prefix}`); }))).toBe(false); + } + expect(ceoFirstReviewAUQ(change(fp, q => { q.question = q.question.replace('\nELI10:', '\nArchived wording: "Source excerpt."\nELI10:'); }))).toBe(true); + } +}); +test('the assertion gap and offered remedy must still be current', () => { + for (const fp of findings) { + for (const correction of ['This finding is withdrawn.', 'No current defect remains.']) { + expect(ceoFirstReviewAUQ(change(fp, q => { q.question += '\n' + correction; }))).toBe(false); + } + expect(ceoFirstReviewAUQ(change(fp, q => { + q.question = q.question.replace(/^\d+[A-Z]\)[\s\S]*?(?=^Net:)/m, ''); + const issue = q.options[0]!.label.match(/^\d+/)![0]; + q.options.forEach((option, i) => { + option.label = `${issue}${String.fromCharCode(65 + i)}: Keep the current assertion`; + option.description = 'Leave the assertion unchanged.'; + }); + }))).toBe(false); + } +}); +test('regression inputs belong only to the existing CEO finding owner without sparse paths', () => { + for (const input of ['test/ceo-assertion-header-am.test.ts', 'test/fixtures/ceo-assertion-header-am-calls.json']) { + expect(Object.entries(E2E_TOUCHFILES).filter(([, paths]) => paths.includes(input)).map(([owner]) => owner)).toEqual(['plan-ceo-finding-count']); + } + const paths = E2E_TOUCHFILES['plan-ceo-finding-count']!; + for (let i = 0; i < paths.length; i++) { + expect(Object.hasOwn(paths, i)).toBe(true); + expect(typeof paths[i]).toBe('string'); + } +}); diff --git a/test/ceo-section-loading-fixture.test.ts b/test/ceo-section-loading-fixture.test.ts index dfb6a611f..c51c9541a 100644 --- a/test/ceo-section-loading-fixture.test.ts +++ b/test/ceo-section-loading-fixture.test.ts @@ -433,3 +433,46 @@ describe('coordination findings require directly asserted premises and conclusio ['mismatched fence', '~~~text\n' + premise + claim + '\n```\n' + premise + claim], ])('rejects %s', (_name, text) => expect(hasStaleFillRaceFinding(text)).toBe(false)); }); + +// AM first SDK report, exact public Write53 acknowledged by native result54. +// The original report remains in run evidence; this is its complete asserted paragraph. +describe('reported original coordination violation with an owned finding citation', () => { + const finding = "`[Amended: F1, F2, F3, F6]` The original sketch stated that no coordination\nbetween a cache fill and a write was proposed. Review showed that sketch\nviolates the retained read-after-write invariant (see F1). The ordering rules\nbelow replace it. They are the complete new read/write ordering rules."; + test('accepts the exact owned paragraph without requiring its separate amended diagram', () => { + expect(hasStaleFillRaceFinding(finding)).toBe(true); + expect(hasStaleFillRaceFinding('## Proposed wrapper integration\n\n' + finding)).toBe(true); + }); + test('binds the named violation to its original subject and finding identity', () => { + expect(hasStaleFillRaceFinding(finding.replaceAll('F1', 'F7'))).toBe(true); + expect(hasStaleFillRaceFinding(finding.replaceAll('sketch', 'wrapper'))).toBe(true); + expect(hasStaleFillRaceFinding('## Historical example\n\nA copied example.\n\n## Current review\n\n' + finding)).toBe(true); + expect(hasStaleFillRaceFinding(finding + '\n\n## Assessment of F2\nF2 is rejected.')).toBe(true); + expect(hasStaleFillRaceFinding(finding + '\n\n## Assessment of F1\n> F1 is rejected.')).toBe(true); + }); + test.each([ + ['negated missing coordination', finding.replace('no coordination', 'coordination')], + ['unrelated operations', finding.replace('cache fill and a write', 'cache hit and a read')], + ['uncertain absence', finding.replace('stated that no', 'might have stated that no')], + ['unproven violation', finding.replace('Review showed', 'Review may show')], + ['negated violation', finding.replace('sketch\nviolates', 'sketch\ndoes not violate')], + ['hypothetical violation', finding.replace('sketch\nviolates', 'sketch\nmight violate')], + ['wrong contract', finding.replace('read-after-write', 'read-before-write')], + ['different subject', finding.replace('Review showed that sketch', 'Review showed that wrapper')], + ['missing finding identity', finding.replace(' (see F1)', '')], + ['question rather than conclusion', finding.replace('(see F1).', '(see F1)?')], + ['conditional finding', 'If approved: ' + finding], + ['historical owner', '## Historical example\n\n' + finding], + ['source owner', '## Quoted source\n\n' + finding], + ['hypothetical owner', '## Hypothetical example\n\n' + finding], + ['explicit source preface', 'The following is a quoted source excerpt.\n\n' + finding], + ['quoted paragraph', '"' + finding + '"'], + ['block quote', finding.split('\n').map(line => '> ' + line).join('\n')], + ['fenced source', '````text\n' + finding + '\n````'], + ['literal assertion', finding.replace('The original sketch', '`The original sketch').replace('(see F1).', '(see F1).`')], + ['split unrelated section', finding.replace('Review showed', '\n\n## Another finding\nReview showed')], + ['direct same-finding withdrawal', finding + '\n\nF1 is rejected.'], + ['later named same-finding withdrawal', finding + '\n\n## Assessment of F1\nF1 is withdrawn.'], + ['dismissed defect', finding + '\n\nThis is not a defect; no fix is required.'], + ['accepted stale consequence', finding + '\n\nA subsequent stale read is permitted by the amended contract.'], + ])('rejects %s', (_name, text) => expect(hasStaleFillRaceFinding(text)).toBe(false)); +}); diff --git a/test/design-variant-choice-am.test.ts b/test/design-variant-choice-am.test.ts new file mode 100644 index 000000000..386443dff --- /dev/null +++ b/test/design-variant-choice-am.test.ts @@ -0,0 +1,122 @@ +import {expect, test} from 'bun:test'; +import fixture from './fixtures/design-variant-choice-am.json'; +import retry from './fixtures/design-variant-choice-am-retry.json'; +import {isDesignCountFirstReview} from './helpers/design-count-review'; +import type {AskUserQuestionFingerprint as FP} from './helpers/claude-pty-runner'; +type Q=NonNullable['questions'][number]; +const original=()=>structuredClone(fixture.fingerprint) as unknown as FP; +function edit(change:(q:Q,fp:FP)=>void):FP { + const fp=original(),c=fp.nativeCall!,q=c.questions[0]!,chosen=q.options.findIndex(o=>o.label===c.answers![q.question]); + change(q,fp);c.answers={[q.question]:q.options[chosen]!.label};fp.options=q.options.map((o,i)=>({index:i+1,label:o.label}));return fp; +} +test('exact completed primary choice binds the current token contract to existing component variants',()=>expect(isDesignCountFirstReview(original())).toBe(true)); +const yes:Array<[string,(q:Q,fp:FP)=>void]>=[ + ['renamed primary',q=>{q.question=q.question.replaceAll('Save','Submit');q.options=q.options.map(o=>({...o,label:o.label.replaceAll('Save','Submit'),description:o.description?.replaceAll('Save','Submit')}));}], + ['another prescribed color and foreground',q=>{q.question=q.question.replace('#1d4ed8 with white text, about 6.7:1 contrast','#ffcc22 with black text');}], + ['unqualified action position',q=>{q.question=q.question.replace('single primary action in the header?','single primary action?');}], + ['one benefit sufficient',q=>{q.options[0]!.description=q.options[0]!.description!.split('\n').slice(1).join('\n');}], + ['an existing open-gap deferral',q=>{q.options[2]!.description='Leaves a documented DESIGN.md violation in place.';}], + ['quoted historical note does not cancel current choice',q=>{q.options[0]!.description+=' Prior note: "This amendment is withdrawn."';}], +]; +test.each(yes)('%s preserves current owned review',(_,change)=>expect(isDesignCountFirstReview(edit(change))).toBe(true)); +const no:Array<[string,(q:Q,fp:FP)=>void]>=[ + ['proposed token contract only',q=>{q.question=q.question.replace('DESIGN.md already says','A proposed example follows. DESIGN.md already says');}], + ['withdrawn token requirement',q=>{q.question=q.question.replace('This is Design Principle 2:','Correction: this DESIGN.md requirement is withdrawn. This is Design Principle 2:');}], + ['superseded token contract',q=>{q.question=q.question.replace('This is Design Principle 2:','That token contract is no longer current. This is Design Principle 2:');}], + ['variant amendment cancelled directly',q=>{q.options[0]!.description+=' Correction: do not use the primary and ghost variants.';}], + ['variant amendment contradicts its remedy',q=>{q.options[0]!.description+=' The current amendment keeps all four buttons identical.';}], + ['failed call',(_,f)=>{f.nativeCall!.failed=true;}], + ['unanswered call',(_,f)=>{f.nativeCall!.answered=false;}], + ['unbound call',(_,f)=>{f.signature='other:call';}], + ['missing completion time',(_,f)=>{delete f.nativeCall!.answeredAt;}], + ['wrong question index',(_,f)=>{f.nativeQuestionIndex=1;}], + ['unanswered member',(_,f)=>{f.nativeCall!.unansweredQuestionIndices=[0];}], + ['wrong header identity',q=>{q.header='Issue 2';}], + ['wrong choice identity',q=>{q.options[0]!.label=q.options[0]!.label.replace('1A','2A');}], + ['setup framing',q=>{q.header='Routing';}], + ['source-framed question',q=>{q.question='Historical example:\n'+q.question;}], + ['historical assessment',q=>{q.question=q.question.replace('ELI10: Right now','ELI10: Previously');}], + ['conditional assessment',q=>{q.question=q.question.replace('ELI10: Right now','ELI10: If right now');}], + ['quoted assessment',q=>{q.question=q.question.replace('ELI10:','> ELI10:');}], + ['negated equality',q=>{q.question=q.question.replace('all look identical','do not look identical');}], + ['archived-only equality',q=>{q.question=q.question.replace('all look identical.','all look identical only in an archived screenshot.');}], + ['absent token contract',q=>{q.question=q.question.replace('DESIGN.md already says','Archived notes say');}], + ['wrong primary contract',q=>{q.question=q.question.replace('says Save is','says Export is');}], + ['no fill contract',q=>{q.question=q.question.replace('only filled button','outlined button');}], + ['no foreground contract',q=>{q.question=q.question.replace('with white text','with unknown text');}], + ['no ghost contract',q=>{q.question=q.question.replace('neutral ghost buttons','also filled buttons');}], + ['quoted token contract',q=>{q.question=q.question.replace('DESIGN.md already says','"DESIGN.md already says').replace('neutral ghost buttons.','neutral ghost buttons."');}], + ['conditional token contract',q=>{q.question=q.question.replace('DESIGN.md already says','If DESIGN.md already says');}], + ['resolved current gap',q=>{q.question+='\nCorrection: the gap is already resolved.';}], + ['wrong proposed primary',q=>{q.options[0]!.label=q.options[0]!.label.replace('Filled Save','Filled Reset');}], + ['wrong proposed ghost role',q=>{q.options[0]!.label=q.options[0]!.label.replace('ghost others','filled others');}], + ['no variant authority',q=>{q.options[0]!.description=q.options[0]!.description!.replace('from DESIGN.md','from an archived example');}], + ['no variant amendment',q=>{q.options[0]!.description=q.options[0]!.description!.replace('Uses the existing','Mentions the existing');}], + ['quoted variant amendment',q=>{q.options[0]!.description=q.options[0]!.description!.replace('✅ Uses','> ✅ Uses');}], + ['conditional variant amendment',q=>{q.options[0]!.description='If approved later:\n'+q.options[0]!.description;}], + ['conditional benefit prefix',q=>{q.options[0]!.description=q.options[0]!.description!.replace('✅ Save reads','✅ If Save reads');}], + ['historical variant amendment',q=>{q.options[0]!.description+=' This is a historical example, not the current amendment.';}], + ['withdrawn amendment',q=>{q.options[0]!.description+=' This amendment is withdrawn.';}], + ['cancelled style',q=>{q.options[0]!.description+=' Correction: do not apply these styles.';}], + ['no opposed choice',q=>{q.options[2]!.label='1C Export preferences';}], + ['deferral no longer retains violation',q=>{q.options[2]!.description=q.options[2]!.description!.replace('Violates DESIGN.md','Matches DESIGN.md');}], + ['historical deferral',q=>{q.options[2]!.description='Historical source excerpt:\n'+q.options[2]!.description;}], + ['conditional deferral',q=>{q.options[2]!.description='If accepted later:\n'+q.options[2]!.description;}], + ['closed deferral',q=>{q.options[2]!.description+=' Correction: the violation is now closed.';}], +]; +test.each(no)('%s is not current completed review evidence',(_,change)=>expect(isDesignCountFirstReview(edit(change))).toBe(false)); + +function retryEdit(change:(q:Q,fp:FP)=>void):FP { + const fp=structuredClone(retry.fingerprint) as unknown as FP,c=fp.nativeCall!,q=c.questions[0]!; + const chosen=q.options.findIndex(o=>o.label===c.answers![q.question]); + change(q,fp);c.answers={[q.question]:q.options[chosen]!.label}; + fp.options=q.options.map((o,i)=>({index:i+1,label:o.label}));return fp; +} +test('retry first decision supplies concrete tokens in the offered label and DESIGN.md authority in its description',()=>{ + expect(isDesignCountFirstReview(retryEdit(()=>{}))).toBe(true); + expect(retry.provenance.historicalOutcome).toBe('no_review_questions'); +}); +test('current labelled token choice permits any offered alternate and harmless historical quotes',()=>{ + for(const index of [0,1,2]){ + const fp=retryEdit(q=>{q.question+='\nArchived note: "This requirement was withdrawn."';}); + const c=fp.nativeCall!,q=c.questions[0]!;c.answers={[q.question]:q.options[index]!.label}; + expect(isDesignCountFirstReview(fp)).toBe(true); + } + expect(isDesignCountFirstReview(retryEdit(q=>{ + q.question=q.question.replaceAll('Save','Submit'); + q.options=q.options.map(o=>({...o,label:o.label.replaceAll('Save','Submit'),description:o.description?.replaceAll('Save','Submit')})); + }))).toBe(true); +}); +const retryNo:Array<[string,(q:Q,fp:FP)=>void]>=[ + ['wrong offered secondary count',q=>{q.options[0]!.description=q.options[0]!.description!.replace('three neutral ghosts','two neutral ghosts');}], + ['wrong stated secondary count',q=>{q.question=q.question.replace('other three','other five');}], + ['consistent but wrong secondary counts',q=>{q.question=q.question.replace('other three','other five');q.options[0]!.description=q.options[0]!.description!.replace('three neutral ghosts','five neutral ghosts');}], + ['token authority withdrawn',q=>{q.options[0]!.description+=' Correction: these tokens do not match DESIGN.md.';}], + ['unanswered',(_,f)=>{f.nativeCall!.answered=false;}], + ['failed',(_,f)=>{f.nativeCall!.failed=true;}], + ['wrong owner',(_,f)=>{f.signature='foreign:use';}], + ['missing completion time',(_,f)=>{delete f.nativeCall!.answeredAt;}], + ['unanswered member',(_,f)=>{f.nativeCall!.unansweredQuestionIndices=[0];}], + ['wrong issue',q=>{q.header='Issue 2';}], + ['wrong option issue',q=>{q.options[0]!.label=q.options[0]!.label.replace('1A','2A');}], + ['non-design pass',q=>{q.question=q.question.replace('Visual Hierarchy','Routing');}], + ['invalid pass',q=>{q.question=q.question.replace('Pass 1,','Pass 9,');}], + ['source assessment',q=>{q.question=q.question.replace('\nELI10:','\nSource excerpt:\nELI10:');}], + ['proposed contract',q=>{q.question=q.question.replace('DESIGN.md already says','A proposed example follows. DESIGN.md already says');}], + ['withdrawn contract',q=>{q.question+=' Correction: this DESIGN.md requirement is withdrawn.';}], + ['superseded contract',q=>{q.question+=' That token contract is no longer current.';}], + ['wrong contract control',q=>{q.question=q.question.replace('says Save is','says Reset is');}], + ['not an exclusive primary',q=>{q.question=q.question.replace('only filled primary','outlined');}], + ['not ghost secondaries',q=>{q.question=q.question.replace('neutral ghost buttons','filled buttons');}], + ['wrong labelled control',q=>{q.options[0]!.label=q.options[0]!.label.replace('Save filled','Reset filled');}], + ['missing concrete color',q=>{q.options[0]!.label=q.options[0]!.label.replace('#1d4ed8','blue');}], + ['missing foreground',q=>{q.options[0]!.label=q.options[0]!.label.replace('/white','');}], + ['missing style authority',q=>{q.options[0]!.description=q.options[0]!.description!.replace('Matches DESIGN.md exactly','Matches a historical example');}], + ['quoted remedy',q=>{q.options[0]!.description='> '+q.options[0]!.description;}], + ['conditional remedy',q=>{q.options[0]!.description='If approved: '+q.options[0]!.description;}], + ['cancelled variants',q=>{q.options[0]!.description+=' Correction: do not use the primary and ghost variants.';}], + ['contradictory remedy',q=>{q.options[0]!.description+=' The current amendment keeps all four buttons identical.';}], + ['resolved deferral',q=>{q.options[2]!.description+=' This violation is now resolved.';}], + ['no remaining violation',q=>{q.options[2]!.description=q.options[2]!.description!.replace('Documented DESIGN.md violation ships','No documented violation ships');}], +]; +test.each(retryNo)('retry %s is not positive review evidence',(_,change)=>expect(isDesignCountFirstReview(retryEdit(change))).toBe(false)); diff --git a/test/devex-seed-coverage.test.ts b/test/devex-seed-coverage.test.ts index 8d327574a..81962ac42 100644 --- a/test/devex-seed-coverage.test.ts +++ b/test/devex-seed-coverage.test.ts @@ -2,6 +2,7 @@ import { describe, expect, test } from 'bun:test'; import { DEVEX_SEEDED_GAPS, devexSeedCoverage } from './helpers/devex-seed-coverage'; import type { PlanCountTranscript, NativePlanQuestionCall } from './helpers/plan-count-transcript'; import fixture from './fixtures/devex-seed-coverage-ad-v3.json'; +import declarativeFixture from './fixtures/dx-declarative-choices-am.json'; import { E2E_TOUCHFILES, matchGlob } from './helpers/touchfiles'; function transcript(attempt = 0): PlanCountTranscript { @@ -115,3 +116,153 @@ describe('DX seeded-gap coverage', () => { } }); }); + +function declarativeTranscript(): PlanCountTranscript { + return { status: 'ready', calls: structuredClone(declarativeFixture.calls) as NativePlanQuestionCall[], assistantMessages: [] }; +} +function changeDeclaration(t: PlanCountTranscript, index: number, change: (text: string) => string) { + const call = t.calls[index]!, question = call.questions[0]!, answer = call.answers![question.question]!; + question.question = change(question.question); + call.answers = { [question.question]: answer }; +} + +describe('DX current declarative choices', () => { + test('captured declarative titles retain five distinct completed decisions', () => { + const t = declarativeTranscript(), result = devexSeedCoverage(t); + expect(result.complete).toBe(true); + expect(result.missing).toEqual([]); + expect(Object.values(result.decisions).flat().sort()).toEqual(t.calls.map(c => `${c.sessionId}:${c.toolUseId}`).sort()); + expect(declarativeFixture.provenance.paidOutcomesReclassified).toBe(false); + expect(declarativeFixture.provenance.historicalOutcome).toBe('plan_ready; seeded-gap assertion failed'); + }); + test('renumbering, inline subject code, singular codes and final punctuation keep the same current decisions', () => { + const changes = [ + (text: string) => text.replace(/^D\d+ — /, 'D27: '), + (text: string) => text.replace(/^(.*)\n/, '$1.\n'), + (text: string) => text.replace(/^(.*)\n/, '$1?\n'), + (text: string) => text.replace('run_eval and run_batch take', '`run_eval` and `run_batch` take'), + ]; + for (const change of changes) { + const t = declarativeTranscript(); t.calls.forEach((_, i) => changeDeclaration(t, i, change)); + expect(devexSeedCoverage(t).complete).toBe(true); + } + const t = declarativeTranscript(); + t.calls[3]!.questions[0]!.options[0]!.description = t.calls[3]!.questions[0]!.options[0]!.description!.replace('Codes for', 'Code for'); + expect(devexSeedCoverage(t).complete).toBe(true); + }); + test('each legitimate alternate or deferral remains a decision', () => { + for (let index = 0; index < 5; index++) { + const t = declarativeTranscript(), c = t.calls[index]!, q = c.questions[0]!; + for (const option of q.options) { c.answers = { [q.question]: option.label }; expect(devexSeedCoverage(t).complete).toBe(true); } + } + }); + test('current titles cannot be borrowed from examples, hypotheses, literal quotes or reported history', () => { + for (const prefix of ['Historical example: ', 'Quoted source: ', 'If approved, ', 'Suppose ', 'The old report states: ', '> ', '"', '`']) { + for (let i = 0; i < 5; i++) { + const t = declarativeTranscript(); + changeDeclaration(t, i, text => text.replace(/^(D\d+ — )(.*)\n/, (_, id, title) => `${id}${prefix}${title}${prefix === '"' || prefix === '`' ? prefix : ''}\n`)); + expect(devexSeedCoverage(t).complete).toBe(false); + } + } + }); + test('source or conditional ownership before the explanation is not a current finding', () => { + for (const prefix of ['Source excerpt:\n', 'If approved:\n', 'Historical example only:\n', 'The following is a quoted source excerpt.\n', 'The following is a hypothetical example.\n', '```\n']) { + for (let i = 0; i < 5; i++) { + const t = declarativeTranscript(); changeDeclaration(t, i, text => text.replace('\nELI10:', `\n${prefix}ELI10:`)); + expect(devexSeedCoverage(t).complete).toBe(false); + } + } + for (const prefix of ['Source excerpt: ', 'If approved: ', 'Historical example: ', 'The following is a hypothetical example. ']) { + const t = declarativeTranscript(); changeDeclaration(t, 0, text => text.replace('ELI10: ', `ELI10: ${prefix}`)); + expect(devexSeedCoverage(t).complete).toBe(false); + } + const metadata = declarativeTranscript(); changeDeclaration(metadata, 0, text => text.replace('Project/branch/task: ', 'Project/branch/task: copied source example; the following is not a current finding; ')); + expect(devexSeedCoverage(metadata).complete).toBe(false); + for (let i = 0; i < 5; i++) { + const t = declarativeTranscript(); changeDeclaration(t, i, text => text.replace('Project/branch/task: ', 'Project/branch/task: If approved, ')); + expect(devexSeedCoverage(t).complete).toBe(false); + } + }); + test('same-finding current withdrawals override titles and proposed remedies', () => { + for (const tail of ['Correction: this finding is withdrawn.', 'Correction: this finding is "withdrawn".', 'This issue is already resolved.', 'The defect is historical, not current.', 'There is no current defect.', 'Correction: this explanation is a source example, not a current finding.']) { + for (let i = 0; i < 5; i++) { + const t = declarativeTranscript(); changeDeclaration(t, i, text => `${text}\n${tail}`); + expect(devexSeedCoverage(t).complete).toBe(false); + } + } + }); + test('attributed quoted history and conditional future outcomes do not withdraw a current decision', () => { + for (const tail of ['> This finding is withdrawn.', 'Old note: "The issue is already resolved."', '```\nSource excerpt:\nThis finding is withdrawn.\n```', 'If the fix is accepted, this defect is resolved in the proposed API.']) { + for (let i = 0; i < 5; i++) { + const t = declarativeTranscript(); changeDeclaration(t, i, text => `${text}\n${tail}`); + expect(devexSeedCoverage(t).complete).toBe(true); + } + } + }); + test('affirmatively healthy titles, missing subjects and nominal headers do not assert a defect', () => { + const titles = [ + 'Quickstart points at the shipped README example and the file is available', + 'First local evaluation runs immediately without any remote CI check', + 'run_eval and run_batch take the same arguments in the same positional order', + 'Invalid API key raises AuthError with a clear cause, code and fix', + 'v2 removes Client.evaluate() with a compatibility alias and migration warning', + ]; + for (let i = 0; i < 5; i++) { + for (const title of [titles[i]!, 'Current issue', 'The draft describes the relevant interface']) { + const t = declarativeTranscript(); changeDeclaration(t, i, text => text.replace(/^.*\n/, `D1 — ${title}\n`)); + expect(devexSeedCoverage(t).complete).toBe(false); + } + } + const otherFunctions = declarativeTranscript(); changeDeclaration(otherFunctions, 2, text => text.replace(/^.*\n/, 'D3 — run_score and run_many take arguments in reversed positional order\n')); + expect(devexSeedCoverage(otherFunctions).complete).toBe(false); + }); + test('offered current remedies are required; navigation, source or withdrawn actions cannot supply them', () => { + for (let i = 0; i < 5; i++) { + for (const change of [ + (s: string) => `Quoted source: ${s}`, + (s: string) => `If approved: ${s}`, + (s: string) => `${s} Correction: this option is withdrawn.`, + ]) { + const t = declarativeTranscript(), c = t.calls[i]!, q = c.questions[0]!; + q.options = q.options.map(o => ({ label: change(o.label), description: change(o.description ?? '') })); + c.answers = { [q.question]: q.options[0]!.label }; + expect(devexSeedCoverage(t).complete).toBe(false); + } + const t = declarativeTranscript(), c = t.calls[i]!, q = c.questions[0]!; + q.options = [{ label: 'Continue', description: 'Next section.' }, { label: 'Stop', description: 'End the review.' }]; + c.answers = { [q.question]: 'Continue' }; + expect(devexSeedCoverage(t).complete).toBe(false); + } + }); + test('the new title form preserves completion, exact answer, native identity and batching requirements', () => { + const mutations: Array<(t: PlanCountTranscript) => void> = [ + t => { t.calls[0]!.answered = false; }, t => { t.calls[0]!.failed = true; }, + t => { t.calls[0]!.unansweredQuestionIndices = [0]; }, t => { t.calls[0]!.answeredAt = 'invalid'; }, + t => { t.calls[0]!.answers = { 'A foreign question': t.calls[0]!.questions[0]!.options[0]!.label }; }, + t => { t.calls[0]!.answers = { [t.calls[0]!.questions[0]!.question]: 'Not offered' }; }, + t => { t.calls[0]!.sessionId = 'foreign'; }, t => { t.calls[0]!.toolUseId = t.calls[1]!.toolUseId; }, + t => { t.calls[0]!.questions[0]!.multiSelect = true; }, + t => { t.calls[0]!.questions.push(structuredClone(t.calls[1]!.questions[0]!)); }, + ]; + for (const mutate of mutations) { const t = declarativeTranscript(); mutate(t); expect(devexSeedCoverage(t).complete).toBe(false); } + }); + test('only asserted option prose supplies actions, while inline API identifiers remain usable', () => { + for (const wrap of [(s: string) => `> ${s}`, (s: string) => `~~~\n${s}\n~~~`, (s: string) => `"${s}"`]) { + const t = declarativeTranscript(); + t.calls[3]!.questions[0]!.options[0]!.description = wrap(t.calls[3]!.questions[0]!.options[0]!.description!); + expect(devexSeedCoverage(t).complete).toBe(false); + } + const t = declarativeTranscript(), c = t.calls[4]!, q = c.questions[0]!; + q.options[0]!.label = 'A: Keep `Client.evaluate` as an `alias` with `DeprecationWarning`'; + q.options[0]!.description = 'Preserve compatibility for existing callers.'; + q.options[1]!.description = 'Leave the API unchanged.'; q.options[1]!.label = 'B: Keep the plan'; + c.answers = { [q.question]: q.options[0]!.label }; + expect(devexSeedCoverage(t).complete).toBe(true); + }); + test('the captured fixture selects only DX and its complete dependency array stays dense', () => { + const file = 'test/fixtures/dx-declarative-choices-am.json'; + expect(Object.entries(E2E_TOUCHFILES).filter(([, files]) => files.some(pattern => matchGlob(file, pattern))).map(([name]) => name)).toEqual(['plan-devex-finding-count']); + const deps = E2E_TOUCHFILES['plan-devex-finding-count']!; + for (let i = 0; i < deps.length; i++) { expect(Object.hasOwn(deps, i)).toBe(true); expect(typeof deps[i]).toBe('string'); } + }); +}); diff --git a/test/eng-cache-brief-am.test.ts b/test/eng-cache-brief-am.test.ts new file mode 100644 index 000000000..68dd8afe2 --- /dev/null +++ b/test/eng-cache-brief-am.test.ts @@ -0,0 +1,58 @@ +import {expect,test} from 'bun:test'; +import {engFirstReviewAUQ,nativePlanCallFingerprint,type AskUserQuestionFingerprint as FP} from './helpers/claude-pty-runner'; +import fixture from './fixtures/eng-cache-brief-am.json'; +const calls=fixture.calls as FP[]; +function edit(change:(q:any,f:FP)=>void):FP { const f=structuredClone(calls[1]!),c=f.nativeCall!,q=c.questions[0]!,selected=q.options.findIndex(o=>o.label===c.answers?.[q.question]);change(q,f);c.answers={[q.question]:q.options[selected]!.label};return nativePlanCallFingerprint(c,f.observedAtMs,f.preReview); } +test('the completed current cache ownership brief starts the engineering review',()=>expect(engFirstReviewAUQ(calls[1]!)).toBe(true)); +test('the earlier whole-plan scope choice does not become a finding',()=>expect(engFirstReviewAUQ(calls[0]!)).toBe(false)); +test('equivalent decision ordinal and current wording retain the owned finding',()=>{ + expect(engFirstReviewAUQ(edit(q=>{q.question=q.question.replace(/^D2/,'D17');q.header='D17 DI';}))).toBe(true); + expect(engFirstReviewAUQ(edit(q=>{q.question=q.question.replace('Right now both services grab','Today both services import').replace('and both write to it.','and both mutate it.');}))).toBe(true); +}); +test('unrelated current decisions remain outside this dependency branch',()=>{ + expect(engFirstReviewAUQ(edit(q=>{q.header='D3 DI';}))).toBe(false); + expect(engFirstReviewAUQ(edit(q=>{q.question=q.question.replace('Architecture finding A1','Architecture finding A2');}))).toBe(false); +}); +const negative:Array<[string,(q:any,f:FP)=>void]>=[ + ['source preface',q=>q.question=q.question.replace('\nELI10:','\nSource excerpt:\nELI10:')], + ['historical current clause',q=>q.question=q.question.replace('ELI10: Right now','ELI10: Previously')], + ['hypothetical current clause',q=>q.question=q.question.replace('ELI10: Right now','ELI10: If approved, right now')], + ['negated current writes',q=>q.question=q.question.replace('and both write to it.','and neither writes to it.')], + ['quoted current assessment',q=>q.question=q.question.replace('ELI10: Right now','ELI10: "Right now').replace('it. Nobody','it." Nobody')], + ['withdrawn finding',q=>q.question+='\nCorrection: this finding is withdrawn.'], + ['resolved current finding',q=>q.question+='\nNo current gap remains.'], + ['foreign cache title',q=>q.question=q.question.replace('Module-level AuthCache','Module-level OtherCache')], + ['source remedy preface',q=>q.options[0].description='Source excerpt:\n'+q.options[0].description], + ['conditional writer ownership',q=>q.options[0].description=q.options[0].description.replace('✅ SessionMint','✅ If SessionMint')], + ['quoted writer ownership',q=>q.options[0].description=q.options[0].description.replace('✅ SessionMint','✅ "SessionMint').replace('not convention.','not convention."')], + ['read-only claim only in con',q=>q.options[0].description=q.options[0].description.replace('✅ SessionMint','❌ SessionMint')], + ['same writable and read-only actor',q=>q.options[0].description=q.options[0].description.replace('AuthBroker gets','SessionMint gets')], + ['withdrawn remedy',q=>q.options[0].description+=' This remedy is withdrawn.'], + ['foreign opposing finding',q=>q.options[2].description=q.options[2].description.replace('Both A1','Both A9')], + ['opposed gap resolved',q=>q.options[2].description+=' No current gap remains.'], + ['conditional opposed gap',q=>q.options[2].description=q.options[2].description.replace('❌ Both A1','❌ If Both A1')], + ['unoffered recommendation',q=>q.question=q.question.replace('Recommendation: A','Recommendation: D')], + ['multiple recommendations',q=>q.options[1].label+=' (recommended)'], + ['unlettered choice',q=>q.options[1].label=q.options[1].label.slice(3)], + ['unanswered',(_,f)=>f.nativeCall!.answered=false], + ['failed',(_,f)=>f.nativeCall!.failed=true], + ['incomplete member',(_,f)=>f.nativeCall!.unansweredQuestionIndices=[0]], + ['invalid completion time',(_,f)=>f.nativeCall!.answeredAt='invalid'], +]; +test.each(negative)('%s cannot supply current owned engineering review',(_,change)=>expect(engFirstReviewAUQ(edit(change))).toBe(false)); +test('whole quoted history and consistent identifiers preserve the current decision',()=>{ + expect(engFirstReviewAUQ(edit(q=>q.question+='\nPrior note: "This finding is withdrawn."'))).toBe(true); + expect(engFirstReviewAUQ(edit(q=>{q.question=q.question.replaceAll('AuthCache','SessionCache').replaceAll('A1','A9');q.options.forEach((o:any)=>o.description=o.description.replaceAll('A1','A9'));}))).toBe(true); + expect(engFirstReviewAUQ(edit(q=>{q.question=q.question.replace(/^D2/,'d2');q.header='d2 DI';}))).toBe(true); +}); + +import { E2E_TOUCHFILES } from './helpers/touchfiles-data'; +test('new inputs have only the engineering finding owner and dense paths',()=>{ + for(const file of ['test/eng-cache-brief-am.test.ts','test/fixtures/eng-cache-brief-am.json']) expect(Object.entries(E2E_TOUCHFILES).filter(([,paths])=>paths.includes(file)).map(([owner])=>owner)).toEqual(['plan-eng-finding-count']); + const paths=E2E_TOUCHFILES['plan-eng-finding-count']!; + for(let i=0;i{ + for(const text of ['Correction: this finding is rejected.','Correction: this remedy is cancelled.','Correction: this finding is "withdrawn".','Correction: this explanation is not current.']) expect(engFirstReviewAUQ(edit(q=>q.question+='\n'+text))).toBe(false); + expect(engFirstReviewAUQ(edit(q=>q.question=q.question.replace('Architecture finding A1','If approved, Architecture finding A1')))).toBe(false); +}); diff --git a/test/eng-legacy-contract-am.test.ts b/test/eng-legacy-contract-am.test.ts new file mode 100644 index 000000000..2bcdf3bec --- /dev/null +++ b/test/eng-legacy-contract-am.test.ts @@ -0,0 +1,64 @@ +import {test,expect} from 'bun:test'; +import {evaluateEngSeedCoverage} from './helpers/eng-seeded-coverage'; +import fixture from './fixtures/eng-legacy-contract-am.json'; +const transcript:any={status:'ready',calls:fixture.calls,assistantMessages:[],planReadyRequests:[]}; +const times=fixture.calls.map(c=>Date.parse(c.answeredAt)); +const check=(plan=fixture.compact,calls=transcript.calls)=>evaluateEngSeedCoverage({...transcript,calls},plan,Math.min(...times)-1,Math.max(...times)+1); +test('the actual class inventory decision is a distinct complexity seed',()=>expect(check().missing).toEqual([])); +test('the actual mandatory current-output suite and linked before-rewrite task establish legacy parity',()=>expect(check().problems).toEqual([])); +test('the unchanged legacy function and exact output oracle remain required',()=>{ + expect(check(fixture.compact.replaceAll('legacyAuthFlow','anotherFlow')).regression).toBeUndefined(); + expect(check(fixture.compact.replace('record current outputs','record proposed outputs')).regression).toBeUndefined(); + expect(check(fixture.compact.replace('produces identical decisions and equivalent error surfaces','may produce different decisions and error surfaces')).regression).toBeUndefined(); +}); +const no:Array<[string,(s:string)=>string]>=[ + ['historical source ancestor',s=>'# Source\n'+s], + ['quoted declaration',s=>s.replace(fixture.declaration,fixture.declaration.split('\n').map(l=>'> '+l).join('\n'))], + ['fenced declaration',s=>s.replace(fixture.declaration,'```text\n'+fixture.declaration+'```\n')], + ['optional declaration',s=>s.replace('REGRESSION (mandatory,','REGRESSION (optional,')], + ['source declaration prefix',s=>s.replace('`legacyAuthFlow()` is existing','The following is a quoted source excerpt.\n`legacyAuthFlow()` is existing')], + ['hypothetical declaration prefix',s=>s.replace('`legacyAuthFlow()` is existing','If approved:\n`legacyAuthFlow()` is existing')], + ['after-rewrite capture',s=>s.replace('written BEFORE any rewrite','written AFTER any rewrite')], + ['foreign declared task',s=>s.replace('rewrite (T1)','rewrite (T9)')], + ['foreign task file',s=>s.replace('Files: `auth/legacyAuthFlow.characterization.test.ts`','Files: `auth/other.characterization.test.ts`')], + ['proposed-only task owner',s=>s.replace('## Implementation Tasks','## Proposed Implementation Tasks')], + ['task after rewrite',s=>s.replace('for `legacyAuthFlow()` before any rewrite','for `legacyAuthFlow()` after any rewrite')], + ['missing current baseline',s=>s.replace('suite green on current main','suite green on the new implementation')], + ['missing later rerun',s=>s.replace('; re-run after each later task','; no later runs needed')], + ['conditional verification',s=>s.replace(' - Verify:',' If approved:\n - Verify:')], + ['source verification',s=>s.replace(' - Verify:',' Source excerpt:\n - Verify:')], + ['withdrawn task',s=>s+'\n## Final assessment\nT1 is withdrawn.\n'], + ['withdrawn rerun',s=>s+'\n## Final assessment\nT1 rerun is cancelled.\n'], + ['withdrawn suite',s=>s+'\n## Final assessment\nThe legacy regression suite is withdrawn.\n'], + ['withdrawn baseline verification',s=>s.replace(' - Verify:',' Correction: this baseline verification is withdrawn.\n - Verify:')], +]; +test.each(no)('%s cannot supply the required unchanged legacy oracle',(_,change)=>expect(check(change(fixture.compact)).regression).toBeUndefined()); +test('same file/task identity and harmless unrelated context are preserved',()=>{ + expect(check(fixture.compact.replaceAll('T1','T9').replaceAll('legacyAuthFlow.characterization.test.ts','legacy-behavior.test.ts')).ok).toBe(true); + expect(check(fixture.compact+'\n## Payment regression suite\nThis regression suite is withdrawn.\n').ok).toBe(true); + expect(check(fixture.compact.replace(' - Verify:',' Literal UI label: "This is a hypothetical example."\n - Verify:')).ok).toBe(true); +}); +test('a class-name or historical example cannot replace the class-inventory scope decision',()=>{ + for(const title of ['D1 — Rename the class before building?','Historical example: Reduce the class inventory before building?','D1 — A hypothetical example: reduce the class inventory before building?']){ + const calls=structuredClone(transcript.calls);const q=calls[0].questions[0];const selected=calls[0].answers[q.question];q.question=q.question.replace(/^.*\n/,title+'\n');calls[0].answers={[q.question]:selected};expect(check(fixture.compact,calls).missing).toContain('complexity'); + } +}); + +test('explicit current baseline changes and named verification withdrawal cancel this oracle',()=>{ + for(const suffix of [ + '## Current baseline correction\nlegacyAuthFlow() is modified before T1 records the baseline.', + '## Final verification assessment\nT1 verification is withdrawn.', + '## Final verification assessment\nT1 verification is "withdrawn".', + ])expect(check(fixture.compact+'\n'+suffix).regression).toBeUndefined(); + expect(check(fixture.compact+'\n## History\nOld note: "legacyAuthFlow() is modified before T1 records the baseline."').regression).toBeDefined(); +}); +test('current class inventory ownership excludes literal, source-only and withdrawn actions',()=>{ + const changes=[ + (q:any)=>{q.question=q.question.replace(/^(D1 — )(.*)\n/,'$1`$2`\n')}, + (q:any)=>{q.options=q.options.map((o:any)=>({label:'Quoted source: '+o.label,description:'Source excerpt: '+o.description}))}, + (q:any)=>{q.question+='\nCorrection: this class-inventory decision is withdrawn.'}, + (q:any)=>{q.question+='\nCorrection: this class-inventory decision is "withdrawn".'}, + ]; + for(const change of changes){const calls=structuredClone(transcript.calls),c=calls[0],q=c.questions[0],selected=q.options.findIndex((o:any)=>o.label===c.answers[q.question]);change(q);c.answers={[q.question]:q.options[selected].label};expect(check(fixture.compact,calls).missing).toContain('complexity');} + const calls=structuredClone(transcript.calls),c=calls[0],q=c.questions[0],selected=c.answers[q.question];q.question+='\nOld note: "This class-inventory decision is withdrawn."';c.answers={[q.question]:selected};expect(check(fixture.compact,calls).missing).not.toContain('complexity'); +}); diff --git a/test/eng-retry-contract-am.test.ts b/test/eng-retry-contract-am.test.ts new file mode 100644 index 000000000..f791188a3 --- /dev/null +++ b/test/eng-retry-contract-am.test.ts @@ -0,0 +1,45 @@ +import {test,expect} from 'bun:test'; +import {evaluateEngSeedCoverage} from './helpers/eng-seeded-coverage'; +import fixture from './fixtures/eng-retry-contract-am.json'; +const times=fixture.calls.map(c=>Date.parse(c.answeredAt)); +const check=(text=fixture.compact)=>evaluateEngSeedCoverage({status:'ready',calls:fixture.calls,assistantMessages:[]},text,Math.min(...times)-1,Math.max(...times)+1); +test('the exact retry binds mandatory characterization before extraction to the unchanged baseline and same-task rerun',()=>expect(check().ok).toBe(true)); +const negatives:Array<[string,(s:string)=>string]>=[ + ['source ancestor',s=>'# Source excerpt\n'+s], + ['quoted declaration',s=>s.replace(fixture.declaration,'> '+fixture.declaration)], + ['conditional declaration',s=>s.replace(fixture.declaration,'If approved:\n'+fixture.declaration)], + ['source declaration prefix',s=>s.replace(fixture.declaration,'Source excerpt:\n'+fixture.declaration)], + ['optional rule',s=>s.replace('mandatory, not a decision','optional, not a decision')], + ['changed baseline subject',s=>s.replaceAll('legacyAuthFlow','anotherFlow')], + ['foreign declaration task',s=>s.replace('T1 adds','T8 adds')], + ['foreign extraction identity',s=>s.replace('before* the 4A','before* the 9A')], + ['baseline on modified code',s=>s.replace('against unmodified legacy','against modified legacy')], + ['baseline after other commits',s=>s.replace('before any other commit','after the other commits')], + ['different extraction behavior',s=>s.replace('behavior unchanged','behavior changed')], + ['unlinked rerun',s=>s.replace('Verify: T1 still green','Verify: T8 still green')], + ['omitted rerun',s=>s.replace('T1 still green','no rerun needed')], + ['source baseline verification',s=>s.replace(' - Verify: test passes',' Source excerpt:\n - Verify: test passes')], + ['conditional rerun verification',s=>s.replace(' - Verify: T1',' If approved:\n - Verify: T1')], + ['withdrawn baseline task',s=>s+'\n## Final assessment\nT1 is withdrawn.\n'], + ['withdrawn extraction rerun',s=>s+'\n## Final assessment\nT2 verification is withdrawn.\n'], + ['quoted current withdrawal',s=>s+'\n## Final assessment\nT1 verification is "withdrawn".\n'], + ['current legacy changed before baseline',s=>s+'\n## Current correction\nlegacyAuthFlow() is modified before T1 records the baseline.\n'], + ['withdrawn legacy suite',s=>s+'\n## Final assessment\nThe legacy regression suite is withdrawn.\n'], +]; +test.each(negatives)('%s does not provide a current unchanged oracle',(_,change)=>expect(check(change(fixture.compact)).regression).toBeUndefined()); +test('consistent task/extraction renaming and attributed historical/foreign context preserve the oracle',()=>{ + expect(check(fixture.compact.replaceAll('T1','T8').replaceAll('T2','T9').replaceAll('4A','6B').replaceAll('validate()','checkToken()')).ok).toBe(true); + expect(check(fixture.compact+'\n## Payment regression suite\nThe regression suite is withdrawn.\n').ok).toBe(true); + expect(check(fixture.compact+'\n## Notes\nOld note: "T1 verification is withdrawn."\n').ok).toBe(true); +}); + +test('the owned regression-test and extraction-rerun obligation remain current',()=>{ + for(const value of ['T1 regression test is withdrawn.','Correction: T2 no longer reruns T1.'])expect(check(fixture.compact+'\n## Final assessment\n'+value).regression).toBeUndefined(); + expect(check(fixture.compact+'\n## History\nOld note: "T1 regression test is withdrawn."').regression).toBeDefined(); + expect(check(fixture.compact+'\n## Payment task\nT8 no longer reruns T7.').regression).toBeDefined(); +}); + +test('standalone source and prior-review frames cannot own the current retry declaration',()=>{ + for(const prefix of ['Source:','Earlier review assessment:'])expect(check(fixture.compact.replace(fixture.declaration,prefix+'\n'+fixture.declaration)).regression).toBeUndefined(); + expect(check(fixture.compact.replace(fixture.declaration,'Old note: "Source:"\n'+fixture.declaration)).regression).toBeDefined(); +}); diff --git a/test/fixtures/autoplan-edit-queue-am.json b/test/fixtures/autoplan-edit-queue-am.json new file mode 100644 index 000000000..4d0198129 --- /dev/null +++ b/test/fixtures/autoplan-edit-queue-am.json @@ -0,0 +1,211 @@ +{ + "sourceCaptureSHA256": "1988a0af219e86a020ec09bd320f8ff02c7d3685f010927ccf725b79f105c058", + "projection": "Exact public file-mutation inputs and identity/timestamp/success metadata; result bodies and unrelated tools omitted. Current before and pane are direct retained bytes.", + "cwd": "/tmp/gstack-paid-shard-uaCq3n/tmp/gstack-autoplan-chain-RnwL2i", + "config": "/tmp/gstack-paid-shard-uaCq3n/tmp/gstack-hermetic-2101964-HvDZyN/with-skills/.claude", + "stateRoot": "/tmp/gstack-paid-shard-uaCq3n/tmp/gstack-hermetic-2101964-HvDZyN/skill-home-zgCNxG/.gstack", + "commandStartedAt": 1789032380903, + "now": 1789033507687, + "viewport": " +o CC figure given). Deferred: needs a ranking service and push infrastructure that do not exist. This p\n +lan lays the substrate it would build on: the per-panel result envelope, the per-panel state machine (l\n +oading \u2192 ok / empty / error \u2192 retry), and the instrumentation; all three are in Accepted Scope below. \n 24 \n 25 ## Scope Decisions\n 26 \n\n\u25cf Update(~/.gstack/projects/gstack-autoplan-chain-RnwL2i/ceo-plans/2026-09-10-user-dashboard.md)\n\n\u25cf Update(~/.gstack/projects/gstack-autoplan-chain-RnwL2i/ceo-plans/2026-09-10-user-dashboard.md)\n\n\u25cf Update(~/.gstack/projects/gstack-autoplan-chain-RnwL2i/ceo-plans/2026-09-10-user-dashboard.md)\n\n\u25cf Update(~/.gstack/projects/gstack-autoplan-chain-RnwL2i/ceo-plans/2026-09-10-user-dashboard.md)\n\n\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n Edit file\n \u20262101964-HvDZyN/skill-home-zgCNxG/.gstack/projects/gstack-autoplan-chain-RnwL2i/ceo-plans/2026-09-10-user-dashboard.md\n\u254c\u254c\u254c\u254c\u254c\u254c\u254c\u254c\u254c\u254c\u254c\u254c\u254c\u254c\u254c\u254c\u254c\u254c\u254c\u254c\u254c\u254c\u254c\u254c\u254c\u254c\u254c\u254c\u254c\u254c\u254c\u254c\u254c\u254c\u254c\u254c\u254c\u254c\u254c\u254c\u254c\u254c\u254c\u254c\u254c\u254c\u254c\u254c\u254c\u254c\u254c\u254c\u254c\u254c\u254c\u254c\u254c\u254c\u254c\u254c\u254c\u254c\u254c\u254c\u254c\u254c\u254c\u254c\u254c\u254c\u254c\u254c\u254c\u254c\u254c\u254c\u254c\u254c\u254c\u254c\u254c\u254c\u254c\u254c\u254c\u254c\u254c\u254c\u254c\u254c\u254c\u254c\u254c\u254c\u254c\u254c\u254c\u254c\u254c\u254c\u254c\u254c\u254c\u254c\u254c\u254c\u254c\u254c\u254c\u254c\u254c\u254c\u254c\u254c\u254c\u254c\u254c\u254c\u254c\u254c\n 26 \n 27 | # | Proposal | Effort | Decision | Reasoning | Revisit when |\n 28 |---|----------|--------|----------|-----------|--------------|\n 29 -| 1 | Pull real login-to-first-task baseline (median/p90) and find-vs-do split from existing analytics before locki\n -ng 45s | S | ACCEPTED | Data already exists; the 75s walkthrough number is a stand-in | \u2014 | \n 29 +| 1 | Pull real login-to-first-task baseline (median/p90) and find-vs-do split from existing analytics before locki\n +ng 45s | S | ACCEPTED | Data already exists; the 75s walkthrough number is a stand-in | Dashboard owner re-locks th\n +e target in this document after the pull | \n 30 | 2 | Numeric rollback triggers defined before rollout | S | ACCEPTED | Rollout criteria were \"to be specified\"; de\n pends on #1 | \u2014 |\n 31 | 3 | Relative timestamps with absolute on hover/focus in ActivityFeed | S | ACCEPTED | 1 file, under an hour, in b\n last radius | \u2014 |\n 32 | 4 | Unread count badge and document.title mirror | S | ACCEPTED | 1 file, under an hour | \u2014 |\n\u254c\u254c\u254c\u254c\u254c\u254c\u254c\u254c\u254c\u254c\u254c\u254c\u254c\u254c\u254c\u254c\u254c\u254c\u254c\u254c\u254c\u254c\u254c\u254c\u254c\u254c\u254c\u254c\u254c\u254c\u254c\u254c\u254c\u254c\u254c\u254c\u254c\u254c\u254c\u254c\u254c\u254c\u254c\u254c\u254c\u254c\u254c\u254c\u254c\u254c\u254c\u254c\u254c\u254c\u254c\u254c\u254c\u254c\u254c\u254c\u254c\u254c\u254c\u254c\u254c\u254c\u254c\u254c\u254c\u254c\u254c\u254c\u254c\u254c\u254c\u254c\u254c\u254c\u254c\u254c\u254c\u254c\u254c\u254c\u254c\u254c\u254c\u254c\u254c\u254c\u254c\u254c\u254c\u254c\u254c\u254c\u254c\u254c\u254c\u254c\u254c\u254c\u254c\u254c\u254c\u254c\u254c\u254c\u254c\u254c\u254c\u254c\u254c\u254c\u254c\u254c\u254c\u254c\u254c\u254c\n Do you want to make this edit to 2026-09-10-user-dashboard.md?\n \u276f 1. Yes\n 2. Yes, and switch to accept edits (auto-approve file edits and common file commands) for this session (shift+tab)\n 3. No\n\n Esc to cancel \u00b7 Tab to amend\n", + "before": "---\nstatus: ACTIVE\n---\n# CEO Plan: User Dashboard Page\nGenerated by /plan-ceo-review (via /autoplan) on 2026-09-10\nBranch: main | Mode: SELECTIVE EXPANSION (hold the plan's scope as baseline; cherry-pick expansions individually)\nRepo: gstack-autoplan-chain-RnwL2i (no remote)\nSource plan: `.claude/plans/ui-heavy-feature.md` (reviewed copy with full review record: `.claude/plans/starry-riding-otter.md`)\n\n**Primary metric:** median login-to-first-completed-task \u2264 45s (provisional until the baseline in #1 is pulled). Guardrails: completed-task rate and permission-error rate must not regress.\n\n**Glossary.** *Blast radius*: the files this plan creates or modifies plus their direct importers. *Find vs. do*: time from login to starting an action, versus time from starting to completing it. *CC*: Claude Code implementation time, as opposed to human-team time. *Effort scale* (human team): S under 1 day, M 1 to 5 days, L 1 to 3 weeks, XL over 3 weeks. *Previous landing page*: the post-login destination in use before this plan (the existing default route members see today; name it in the flag config when implementing).\n\n**Acceptance principle.** In SELECTIVE EXPANSION, an expansion inside the blast radius that costs under an hour is accepted on cost alone, whether or not it moves the metric (#3, #4, #6). Items outside the blast radius, or that need an audit or new infrastructure, are deferred even when cheap (#7, #8).\n\n**Assumptions.** The feature-flag framework, request/error metrics, and analytics events named in the source plan exist (this repository contains no source, so they are unverified). Item #1 (baseline pull) precedes item #2 (numeric rollback triggers), because the triggers are expressed against the baseline. **Fallback if the analytics events do not exist:** instrument login, action start, and action completion first, collect at least 7 days of data before any cohort rollout, and keep the 75s walkthrough figure as the stand-in with the target widened to \"at least 30% faster than measured baseline\" until the pull succeeds.\n\n**Carried from the source plan (not additions):** the per-panel state machine (loading \u2192 ok / empty / error \u2192 retry) and the instrumentation set (metrics, alerts, structured logs) are already required by the source plan's accepted CEO obligations; this document ratifies them without a proposal row.\n\n## Vision\n\n### 10x Check\nA post-login home that tells the member what to do next instead of showing three things to scan. A ranked \"next up\" card sits above the panels, computed server-side from eligible actions and unread alerts, and updates live over a push channel. The member arrives, sees one thing, and does it. Effort: XL, infrastructure-bound rather than implementation-bound (ranking service and push channel must exist first; no CC figure given). Deferred: needs a ranking service and push infrastructure that do not exist. This plan lays the substrate it would build on: the per-panel result envelope, the per-panel state machine (loading \u2192 ok / empty / error \u2192 retry), and the instrumentation; all three are in Accepted Scope below.\n\n## Scope Decisions\n\n| # | Proposal | Effort | Decision | Reasoning | Revisit when |\n|---|----------|--------|----------|-----------|--------------|\n| 1 | Pull real login-to-first-task baseline (median/p90) and find-vs-do split from existing analytics before locking 45s | S | ACCEPTED | Data already exists; the 75s walkthrough number is a stand-in | \u2014 |\n| 2 | Numeric rollback triggers defined before rollout | S | ACCEPTED | Rollout criteria were \"to be specified\"; depends on #1 | \u2014 |\n| 3 | Relative timestamps with absolute on hover/focus in ActivityFeed | S | ACCEPTED | 1 file, under an hour, in blast radius | \u2014 |\n| 4 | Unread count badge and document.title mirror | S | ACCEPTED | 1 file, under an hour | \u2014 |\n| 5 | \"Back to previous landing page\" link during rollout | S | ACCEPTED | Per-member escape hatch and bounce-back signal | Remove at 100% rollout |\n| 6 | Empty-state copy pointing at the primary action | S | ACCEPTED | Copy only | \u2014 |\n| 7 | Keyboard shortcuts for quick actions | S | DEFERRED | Shortcut conflict audit needed; not on the metric path | Dashboard owner runs the conflict audit after 100% rollout |\n| 8 | Prefetch /api/dashboard during login redirect | S | DEFERRED | Touches login flow, outside blast radius | If client TTFB p95 > 800ms at 100% |\n| 9 | Post-login redirect-to-resume experiment arm | M | DEFERRED, provisional (taste T1) | Skips alerts the plan says members need; touches login flow; attribution needs its own arm | Final Approval Gate may flip to \"run concurrently\" |\n| 10 | Ranked \"next up\" card with live updates | XL | DEFERRED | New ranking + push infrastructure | After dashboard metric data at 100% |\n| 11 | ETag / short TTL cache on the endpoint | S | DEFERRED | Wait for p95 at 100% rollout | Dashboard owner checks endpoint p95 one week after 100% |\n| 12 | Token-only styling: PR review checklist item now, lint rule later | S | ACCEPTED (checklist) / DEFERRED (lint) | Keeps dark mode viable; lint needs design-system owner | Design-system owner adds lint rule |\n| 13 | Approach C: aggregate `GET /api/dashboard` with per-panel `PanelResult` envelope and `serverTime` | M | ACCEPTED, provisional (taste T2) | Only approach that gives per-panel error states in one round trip and a server clock for the read snapshot | Gate may choose B (client composes existing endpoints) |\n| 14 | Shared `PanelFrame` (state chrome) and shared `Toast` primitive | S | ACCEPTED, provisional (taste T4) | Three panels share one state switch; a11y policy requires a live region and no toast exists; shared placement is the same code in a reusable folder | Gate may choose inline status text over toast |\n| 15 | QuickActions as visual primary; fixed column order at sm/md/lg | S | ACCEPTED | Only panel that drives the metric; three equal cards is the generic pattern | \u2014 |\n| 16 | Keep confirmation modal for \"Mark all as read\" | S | ACCEPTED, provisional (taste T3) | No undo/restore API exists and the plan forbids new mutation APIs; confirm is the honest safety net | Gate may choose direct action + undo (needs a new mutation API, breaks a plan constraint) |\n\n## Accepted Scope (added to this plan)\n- Analytics baseline pull and find/do split before target lock (#1)\n- Numeric rollback triggers (#2)\n- Relative timestamps in ActivityFeed (#3)\n- Unread badge + title mirror (#4)\n- Back-to-previous-landing link during rollout (#5)\n- Action-pointing empty-state copy (#6)\n- Token-only styling as a PR review checklist item (#12)\n- Approach C: aggregate endpoint with per-panel `PanelResult` envelope and `serverTime` (#13)\n- Shared `PanelFrame` and shared `Toast` primitive (#14)\n- QuickActions as visual primary; fixed column order at sm/md/lg (#15)\n- Confirmation modal on the existing dialog primitive (#16)\n- Per-panel state machine and instrumentation (metrics, alerts, structured logs) as specified in the source plan's accepted CEO obligations\n\n## Deferred to TODOS.md\n- Keyboard shortcuts for quick actions (#7)\n- Prefetch dashboard payload during login redirect (#8)\n- Redirect-to-resume experiment arm under the same flag framework (#9, taste T1)\n- Ranked \"next up\" card + real-time push (#10)\n- Endpoint caching once p95 data exists (#11)\n- Token-only styling lint enforcement (#12)\n\n## Taste decisions (provisional; confirmed or overridden at the /autoplan Final Approval Gate)\n- T1: **Provisional decision: dashboard is the sole arm; experiment deferred (#9).** Alternative: run a redirect-to-resume arm concurrently.\n- T2: **Provisional decision: Approach C (#13).** Alternative: B, client composition of existing endpoints plus a new quick-actions endpoint.\n- T3: **Provisional decision: keep the confirmation modal (#16).** Alternative: direct action with undo, which needs a new mutation API.\n- T4: **Provisional decision: shared toast primitive (#14).** Alternative: inline status text per panel.\n", + "hook": { + "version": 1, + "cwd": "/tmp/gstack-paid-shard-uaCq3n/tmp/gstack-autoplan-chain-RnwL2i", + "config": "/tmp/gstack-paid-shard-uaCq3n/tmp/gstack-hermetic-2101964-HvDZyN/with-skills/.claude", + "stateRoot": "/tmp/gstack-paid-shard-uaCq3n/tmp/gstack-hermetic-2101964-HvDZyN/skill-home-zgCNxG/.gstack", + "seenIds": [ + "toolu_01BbKwZ7JFFdm2FLFdcNQXPq", + "toolu_01W9d5F6LgadaG9o9maU8kaT", + "toolu_01YXZEmM6gthBvGshzGE4zYb" + ], + "pending": { + "source": "pre_tool_use", + "sessionId": "9b7779e9-18f9-4f8d-84ac-9e8bf4458a8e", + "toolUseId": "toolu_01YXZEmM6gthBvGshzGE4zYb", + "tool": "Edit", + "file": "/tmp/gstack-paid-shard-uaCq3n/tmp/gstack-hermetic-2101964-HvDZyN/skill-home-zgCNxG/.gstack/projects/gstack-autoplan-chain-RnwL2i/ceo-plans/2026-09-10-user-dashboard.md", + "timestamp": "2026-09-10T09:42:01.132Z", + "transcriptPath": "/tmp/gstack-paid-shard-uaCq3n/tmp/gstack-hermetic-2101964-HvDZyN/with-skills/.claude/projects/-tmp-gstack-paid-shard-uaCq3n-tmp-gstack-autoplan-chain-RnwL2i/9b7779e9-18f9-4f8d-84ac-9e8bf4458a8e.jsonl", + "editDigest": { + "version": 1, + "beforeSHA256": "87ba6c184db192a85eb4fa0771dc59d6f3e12311f73a5bccc3497fd3138899ec", + "requestSHA256": "0b2f9bd8a462279cfd3845b2200c448072a8efb68287b75aa95dcf0b38eb0efd", + "oldLineHashes": [ + "0e0dffc87c4190d31ee08629994526b6d12fb120ffb7a88ff1011d4f385f3923" + ], + "newLineHashes": [ + "36e18b0e5881093948120902c19482cf1eafbf4716500ed739cabaa8064287e3" + ] + } + }, + "sessionId": "9b7779e9-18f9-4f8d-84ac-9e8bf4458a8e" + }, + "publicTools": [ + { + "sessionId": "9b7779e9-18f9-4f8d-84ac-9e8bf4458a8e", + "timestamp": "2026-09-10T09:36:51.205Z", + "toolUseId": "toolu_01EMZc2Hum5xqWxGHi3dddtJ", + "kind": "use", + "name": "Edit", + "input": { + "replace_all": false, + "file_path": "/tmp/gstack-paid-shard-uaCq3n/tmp/gstack-hermetic-2101964-HvDZyN/with-skills/.claude/plans/starry-riding-otter.md", + "old_string": "## Review record\n", + "new_string": "## Review record\n\n\n\n### Phase 0: Intake\n\n- SOURCE_PLAN: `.claude/plans/ui-heavy-feature.md` (sha256 2fdf0ece\u2026fddbc, 4607 bytes). Restore point written by `init`.\n- Context read: no CLAUDE.md, no TODOS.md, no design doc, one commit (`28730e6 init UI-heavy fixture`), no diff vs main, no stash, no TODO/FIXME markers.\n- **System audit key finding:** the repository contains only `README.md` and the plan file. None of the \"existing contracts\" the plan cites (repository methods, dialog primitive, action registry, HTTP clients, Vitest/RTL/Playwright, feature flags) exist in this repo. Every reuse claim is unverifiable here and is treated as a stated premise, not a checked fact.\n- Prerequisite /office-hours offer: auto-decided **skip** (P6, one-gate rule). Cross-project learnings config prompt: left unset (user preference, not a plan decision; LEARNINGS: 0 so no effect this run).\n- CLAUDE.md routing rules: user accepted (D1). Deferred until plan mode exits: write CLAUDE.md routing section and commit.\n- CEO methodology read log: `methodology.md` (2260 lines, sha256 cbb64d50\u20269a28) read at offsets 1/601/1201/1801, all four ranges successful through EOF.\n\n### Phase 1: CEO Review (SELECTIVE EXPANSION)\n\n**Mode selection (0F):** SELECTIVE EXPANSION per /autoplan override. Context default agrees: this is a feature on an existing system (consolidates three existing pages), not greenfield.\n\n**Landscape check:** Aside not installed, WebSearch not used in plan mode for this fixture. Proceeding with in-distribution knowledge. Layer 1 (tried and true): post-login \"home\" dashboards with a primary action rail, an alerts panel, and a recent-activity feed are the standard shape (Linear, GitHub, Notion, Asana home). Layer 2: the current trend is \"next up\" surfaces that rank one action above the fold rather than three equal panels. Layer 3 (first principles): the plan's metric is login-to-first-completed-task. Only QuickActions directly drives that metric; notifications and activity are context. Hierarchy should follow the metric.\n\n#### 0A. Premise Challenge\n\n| # | Premise | Stated or assumed | Assessment | Decision |\n|---|---------|-------------------|------------|----------|\n| P1 | Members spend a median 75s finding the next item after login | Stated, sourced from a team walkthrough, not the analytics the plan says already record login/action start/completion | Reasonable but weakly sourced. Real member data exists and is cheaper than a walkthrough. | Accept the problem; **add requirement**: pull real median/p90 login-to-first-task from existing analytics before locking the 45s target (auto-approved, in blast radius, <1h). |\n| P2 | A three-panel dashboard is the right shape to hit 45s | Assumed | Consolidation of three pages does move navigation time. But only one panel (QuickActions) drives task completion. A redirect-to-resume experiment could bank part of the win cheaper, though it skips alerts, which the plan says members need. | Accept dashboard shape. **Amend**: QuickActions is the visual primary. Redirect-to-resume experiment \u2192 **TASTE DECISION T1** (surfaced at gate) and deferred to TODOS.md. |\n| P3 | One aggregate `GET /api/dashboard` is better than the client calling existing endpoints | Assumed | No justification in plan. Quick actions have no existing list endpoint (registry + server predicates), so a new endpoint exists either way. | Resolved in 0C-bis: aggregate with per-panel result envelope (Approach C). B vs C close \u2192 **TASTE DECISION T2**. |\n| P4 | \"Mark all as read\" needs a confirmation modal | Stated | The bulk-read API is idempotent and snapshot-bounded, but there is no restore/undo API and the plan forbids new mutation APIs. Without undo, a confirm is the honest safety net. | Keep modal. Direct-action+undo would need a new mutation API (plan constraint). **TASTE DECISION T3** (recommend keep). |\n| P5 | A toast system is needed for action feedback | Stated | Accessibility policy requires a live region for nonblocking feedback; nothing exists. Building it as a one-page component would be regretted; building it as a shared primitive is the same code in a different folder. | Build toast as a shared UI primitive (P1, P4). Inline-text alternative \u2192 **TASTE DECISION T4** (recommend shared toast). |\n| P6 | No schema changes needed | Stated | Consistent with read composition + existing bulk-read API. | Accept. |\n| P7 | Existing fixtures/flags/metrics exist and are reusable | Stated | Cannot verify in this repo (see system audit). | Accept as premise; **flag at gate** as an unverified dependency, not a challenge. |\n\nNo premise is clearly wrong. No User Challenge queued from 0A.\n\n#### 0B. Existing Code Leverage Map\n\n| Sub-problem | Existing code (per plan) | Reused? |\n|---|---|---|\n| Auth + workspace scoping | Cookie sessions, membership middleware, request context member/workspace IDs | Yes: handler reads IDs from request context only |\n| Activity list | Repository list method (latest 20 + cursor, indexed) | Yes: called by aggregate handler |\n| Notifications list | Repository list method (latest 20 + cursor, indexed) | Yes |\n| Mark all as read | Member-scoped idempotent bulk-read API, snapshot-time bounded, CSRF | Yes: modal confirm calls it with server-issued snapshot time |\n| Quick actions | Action registry (3 actions, IDs, labels, routes, server eligibility predicates) | Yes: handler evaluates predicates, returns eligible set |\n| Typed client errors | Existing HTTP clients (unauthenticated/forbidden/validation/retryable/network) | Yes: each panel maps typed errors to states |\n| Dialog | Dialog primitive (focus trap, Escape, focus return) | Yes: modal composes it |\n| Toast | None | **New shared primitive** |\n| Layout tokens | Tailwind tokens, responsive page shell, buttons, links | Yes; **constraint added**: token-only values in dashboard components |\n| Tests | Vitest, RTL, Playwright, fixtures (member, other workspace, empty, failures) | Yes; dashboard specs new |\n| Rollout | Feature flags, request/error metrics | Yes; flag `dashboard_landing` |\n| Analytics | login, action start/completion, permission errors | Yes; add `dashboard_view`, `dashboard_panel_state`, `quick_action_click`, `mark_all_read` |\n\nNothing is rebuilt that already exists.\n\n#### 0C. Dream State\n\n```\n CURRENT STATE THIS PLAN 12-MONTH IDEAL\n Login \u2192 generic landing Login \u2192 /dashboard (flagged Login \u2192 \"next up\" home that\n Member visits 3 pages to cohort). One round trip, three ranks the single best action,\n resume / check alerts / panels, QuickActions primary, streams notification updates,\n inspect changes. ~75s (walk- per-panel failure isolation, personal layout, dark mode,\n through) to first task. ---> shared toast + modal, a11y, ---> cross-workspace view.\n No dashboard telemetry. telemetry on every state, Dashboard is the platform\n rollback = flag off. surface other teams add to.\n```\n**Dream state delta:** this plan lands the substrate (aggregate envelope, panel state machine, shared toast, instrumentation) that the ideal builds on. It does not do ranking, streaming, personalization, or dark mode. It moves toward the ideal; nothing here blocks it.\n\n#### 0C-bis. Implementation Alternatives\n\n```\nAPPROACH A: Minimal aggregate (all-or-nothing)\n Summary: GET /api/dashboard runs three repository calls; any failure \u2192 5xx; client shows one page-level error. Dialog primitive for confirm; inline status text instead of toast.\n Effort: S (human ~3 days / CC ~1h) Risk: Med (one slow panel blanks the page)\n Pros: fewest files; no new primitive; simplest handler\n Cons: violates \"error state for each panel\"; one repo timeout hides everything; no live region for feedback\n Reuses: repos, middleware, dialog, tokens\n Completeness: 5/10\n\nAPPROACH B: Client composes existing endpoints\n Summary: Page calls existing activity + notifications list endpoints in parallel plus a NEW /api/quick-actions endpoint. Panel isolation for free. Shared toast primitive.\n Effort: M (human ~5 days / CC ~1.5h) Risk: Low-Med (3 round trips on mobile; still one new endpoint)\n Pros: reuses two endpoints verbatim; per-panel failure isolation is structural; no partial-failure design\n Cons: 3 requests per landing on mobile; snapshot time for mark-all-read has no single server clock source; new endpoint needed anyway\n Reuses: existing list endpoints, clients, dialog\n Completeness: 8/10\n\nAPPROACH C: Aggregate with per-panel result envelope (RECOMMENDED)\n Summary: GET /api/dashboard runs three repository calls concurrently, returns\n { serverTime, activity: PanelResult, notifications: PanelResult, quickActions: PanelResult }\n where PanelResult = { status:\"ok\", data, cursor? } | { status:\"error\", code }.\n HTTP 200 whenever auth passes; per-panel errors are data. Shared toast primitive.\n Effort: M (human ~6 days / CC ~2h) Risk: Low\n Pros: one round trip; per-panel isolation matches the plan's own state requirement; serverTime gives the mark-all-read snapshot; envelope pattern is reusable\n Cons: new composition + partial-failure code; 200-with-errors needs its own metric (not HTTP status alerts)\n Reuses: repos, middleware, action registry, dialog, tokens, clients\n Completeness: 9/10\n```\n**RECOMMENDATION:** C, because it is the only approach that satisfies the plan's stated per-panel state requirement in one round trip and yields a server clock for the read snapshot (P1 completeness, P5 explicit). B is close (8/10) \u2192 **TASTE DECISION T2**.\n\n#### 0D. Mode-Specific Analysis (SELECTIVE EXPANSION)\n\n**Complexity check:** ~12 new files (page, 3 panels, hook, modal, toast primitive + provider, handler, envelope type, 3 test files, Playwright spec). Over the 8-file smell threshold, but each file is a distinct concern with no shared mutable state beyond the dashboard hook. Not reducible without merging panels into one component, which would defeat per-panel states. Accepted.\n\n**Minimum set achieving the goal:** page + QuickActions + endpoint + flag. Notifications and Activity could ship later without blocking the metric. Mechanical decision: do not reduce scope on a complete plan (P1). All three panels stay.\n\n**Expansion scan**\n- 10x: a ranked \"next up\" card above the panels, updated in real time, that turns the dashboard from a place you check into a place that tells you what to do. Concrete shape: server ranking over eligible actions + unread notifications, SSE channel for updates. Effort human ~3 weeks / CC ~1 day. **Deferred**: new infra (ranking service, push), outside blast radius.\n- Delight opportunities (\u22655), each decided by the blast-radius rule (in radius + <1d \u2192 accept; else defer):\n 1. Relative timestamps (\"3 min ago\") with absolute time on hover/focus in ActivityFeed. **ACCEPT** (1 file, <1h).\n 2. Unread count badge in NotificationsPanel header, mirrored into `document.title` while unread > 0. **ACCEPT** (1 file, <1h).\n 3. \"Back to the previous landing page\" link in the dashboard header during rollout, removed when flag reaches 100%. **ACCEPT** (1 file, <1h). Gives per-member escape hatch and a bounce-back metric.\n 4. Empty-state copy that points at the primary action (\"Nothing to resume. Create an item or invite a teammate.\") **ACCEPT** (copy only).\n 5. Keyboard shortcuts for the three quick actions. **DEFER** to TODOS.md: shortcut conflicts with existing pages need an audit; not on the metric path.\n 6. Prefetch `/api/dashboard` during the login redirect. **DEFER**: touches login flow, outside radius.\n 7. Post-login redirect-to-resume experiment arm under the same flag framework. **DEFER** + TASTE T1.\n- Platform potential: the `PanelResult` envelope and the toast primitive are reusable by any future composite page. Accepted as part of C.\n\n**Cherry-pick ceremony (auto-decided, neutral posture, logged in audit trail):** Accepted 1-4; deferred 5-7 and 10x.\n\n**Additional accepted hardening (from premise challenge and outside voice, all in blast radius):**\n- Baseline pull: real median/p90 login-to-first-task and find-vs-do split from existing analytics before locking 45s.\n- Numeric rollback triggers defined before rollout (see Section 9).\n- Token-only styling constraint for dashboard components (review checklist now; lint enforcement \u2192 TODOS.md).\n\n#### 0E. Temporal Interrogation (human hours; CC \u2248 10-20x faster)\n\n```\nHOUR 1 (foundations): PanelResult type + serverTime in the envelope; flag name dashboard_landing;\n route /dashboard gated by flag; toast primitive API (show({kind, message, persistent?})).\nHOUR 2-3 (core logic): Handler: Promise.allSettled over three repo calls; map rejections to {status:\"error\", code}\n by typed error class; never leak internal messages. Client hook maps 401\u2192login redirect,\n 403\u2192forbidden state, validation\u2192error state with log, retryable\u2192auto-retry once then\n error+Retry button, network\u2192error+Retry.\nHOUR 4-5 (integration): Mark-all-read uses envelope.serverTime as snapshot, POST via existing bulk-read API with\n CSRF; button disabled while in flight; on success optimistic clear + toast; on 401/403/CSRF\n failure toast \"Session expired, refresh\"; on retryable error retry once then persistent toast.\n Old landing page stays reachable at its route.\nHOUR 6+ (polish/tests): Skeleton fixed heights (no layout shift); reduced-motion disables skeleton shimmer and toast\n slide; toast max 3 stacked, 5s auto-dismiss, pause on hover/focus, role=\"status\" for\n success and role=\"alert\" for errors (persistent until dismissed); Playwright: flag on/off,\n each panel state, keyboard-only modal flow, screen reader names.\n```\nDecisions above are resolved now and recorded as accepted obligations.\n\n#### Section 1: Architecture Review\n\n```\n Browser (React) Server\n \u250c\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2510 \u250c\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2510\n \u2502 /dashboard (flag: dashboard_landing) \u2502 \u2502 GET /api/dashboard \u2502\n \u2502 UserDashboard.tsx \u2502 1 fetch \u2502 DashboardHandler \u2502\n \u2502 \u251c\u2500 useDashboard() \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u25b6 \u2502 \u251c\u2500 session + membership middleware\u2502\n \u2502 \u251c\u2500 QuickActions (primary) \u2502 \u2502 \u251c\u2500 Promise.allSettled([ \u2502\n \u2502 \u251c\u2500 NotificationsPanel \u2502 \u2502 \u2502 activityRepo.list(ws, 20) \u2502\n \u2502 \u2502 \u2514\u2500 MarkAllReadModal \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500 POST \u2500\u2500\u2500\u25b6 \u2502 \u2502 notificationRepo.list(m,ws,20)\u2502\n \u2502 \u2502 (dialog primitive) \u2502 existing \u2502 \u2502 actionRegistry.eligible(m,ws)])\u2502\n \u2502 \u2514\u2500 ActivityFeed \u2502 bulk-read \u2502 \u2514\u2500 envelope {serverTime, 3\u00d7PanelResult}\n \u2502 ToastProvider (shared ui primitive) \u2502 \u2502 POST /api/notifications/read-all (existing)\n \u2514\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2518 \u2514\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2518\n \u2502 \u2502 \u2502\n PostgreSQL PostgreSQL registry predicates\n```\n- **Data flow, four paths (GET /api/dashboard):** happy \u2192 200 envelope, all `ok`. Nil (no session) \u2192 middleware 401 before handler; client redirects to login. Empty (member with zero activity/notifications/eligible actions) \u2192 200, `ok` with `data: []`, each panel renders its empty state; QuickActions empty state must never occur in practice (create-item is always eligible for a member) but is still rendered and tested. Error (one repo throws) \u2192 that panel `{status:\"error\", code}`, others `ok`; page renders; metric emitted.\n- **State machine (per panel):** `idle \u2192 loading \u2192 (ok|empty|error) ; error \u2192 loading (retry)`. Invalid: `loading \u2192 loading` (prevented by in-flight guard), `ok \u2192 error` without a new request (impossible; state only changes on response).\n- **Mark-all-read state machine:** `closed \u2192 open (button) \u2192 submitting (confirm) \u2192 (closed+success toast | open+error toast)`. Double confirm prevented by disabling the confirm button while submitting. Escape during submitting is ignored (dialog stays until response) to avoid orphaned toasts.\n- **Coupling:** handler now depends on three repositories and the action registry; that fan-in is the point of the endpoint and is bounded by the envelope contract. No new coupling between panels; each consumes its own `PanelResult`.\n- **Scaling:** 10x load \u2192 three indexed LIMIT 20 queries per landing; DB connection use triples per request vs a single-page load. First to break: pool saturation under a login storm. Mitigation: concurrent calls share the request's pool budget; flag-gated cohort rollout observes p95. 100x \u2192 add short TTL cache per member (deferred, Section 7).\n- **SPOF:** PostgreSQL (existing). The endpoint adds none.\n- **Security architecture:** endpoint is read-only, scoped by request-context member/workspace; no IDs from query string. Mutation reuses existing CSRF-protected bulk-read API. See Section 3.\n- **Production failure scenario:** notifications table lock during a bulk job \u2192 `notificationRepo.list` times out \u2192 `notifications: {status:\"error\", code:\"retryable\"}`; page still shows actions and activity; panel shows Retry; `dashboard_panel_error{panel=notifications}` spikes \u2192 alert. Plan (as amended) accounts for it.\n- **Rollback:** flag `dashboard_landing` off \u2192 login lands on the previous page; under 1 minute; no migration; endpoint can stay deployed dark.\n- **Beauty / platform:** `PanelResult` as a typed discriminated union is the one abstraction; a new engineer reads it in 30 seconds. It makes any future composite page a two-line addition.\n\nFindings: (1) HTTP 200 with per-panel errors hides failures from status-code alerts \u2192 **decided**: emit `dashboard_panel_error` metric and alert on it (Section 8). (2) Old/new client mismatch during deploy \u2192 **decided**: page route gated by flag; endpoint deploys first (Section 9). Both auto-decided (mechanical, P5).\n\n#### Section 2: Error & Rescue Map\n\n```\n METHOD/CODEPATH | WHAT CAN GO WRONG | ERROR CLASS\n --------------------------------|-------------------------------------|---------------------------\n membership middleware | no/expired session | UnauthenticatedError (401)\n | member not in workspace | ForbiddenError (403)\n DashboardHandler | activityRepo.list timeout/DB error | RetryableServiceError\n | notificationRepo.list timeout/DB | RetryableServiceError\n | actionRegistry.eligible throws | RegistryEvaluationError\n | envelope serialization bug | TypeError (programmer)\n useDashboard (client) | network down | NetworkError\n | 401 | UnauthenticatedError\n | 403 | ForbiddenError\n | response fails schema check | ValidationError\n | 5xx / retryable | RetryableServiceError\n MarkAllRead submit | CSRF token stale | ForbiddenError (CSRF)\n | 401/403 | Unauthenticated/Forbidden\n | bulk-read API 5xx | RetryableServiceError\n | network | NetworkError\n | double submit | (prevented) in-flight guard\n ToastProvider | toast fires after unmount | (prevented) timer cleared on unmount\n | >3 toasts | (prevented) queue, oldest dropped\n\n ERROR CLASS | RESCUED? | RESCUE ACTION | USER SEES\n ----------------------------|----------|-------------------------------------------------|----------------------------------\n UnauthenticatedError | Y | client redirects to /login?next=/dashboard | login page\n ForbiddenError (endpoint) | Y | forbidden state, log warn with member/ws ids | \"You don't have access to this workspace\"\n RetryableServiceError (panel)| Y | handler maps to PanelResult error; client auto-retries once, then Retry button; log error with panel, ids, duration; metric | panel error state + Retry\n RegistryEvaluationError | Y | quickActions PanelResult error; log error | actions panel error + Retry\n TypeError (envelope) | Y | existing 500 handler; log with request id | page-level error + Retry (rare)\n ValidationError (client) | Y | error state, log to client error reporter | panel/page error + Retry\n NetworkError | Y | error state with Retry; no auto-retry | \"Can't reach the server\" + Retry\n ForbiddenError (CSRF) | Y | persistent error toast; keep modal open | \"Session expired. Refresh and try again.\"\n RetryableServiceError (POST)| Y | retry once, then persistent error toast | \"Couldn't mark all as read. Try again.\"\n NetworkError (POST) | Y | persistent error toast | same as above\n```\nNo catch-all rescues. Every rescued error logs member id, workspace id, request id, panel, and duration. No GAP rows. Finding: none unrescued. \"No issues, moving on\" after mapping 14 error paths.\n\n#### Section 3: Security & Threat Model\n\n| Threat | Likelihood | Impact | Mitigated? |\n|---|---|---|---|\n| IDOR: member reads another workspace's activity via query param | Low | High | Yes: handler takes IDs from request context only; test with \"another workspace\" fixture asserts 403/empty |\n| Ineligible action leaked to client | Med | Med | Yes: eligibility predicates evaluated server-side in handler; client never receives ineligible actions; clicking still hits existing server checks |\n| Open redirect via action route targets | Low | Med | Yes: routes come from the server registry, never from user input; client only renders registry routes |\n| XSS via activity descriptions / notification text | Med | High | Yes: render as text nodes, never `dangerouslySetInnerHTML`; RTL test asserts a `