mirror of
https://github.com/garrytan/gstack.git
synced 2026-11-08 09:48:20 +01:00
test: recognize completed review decisions and queued permissions
This commit is contained in:
1 parent
fd7698b23d
commit
9d66d6ca9e
27 files changed
+2250
-45
No files matched your search
@@ -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<string>())=>(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<typeof setup>;
|
||||
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<owner.length;i++){expect(Object.hasOwn(owner,i)).toBe(true);expect(typeof owner[i]).toBe('string')}
|
||||
for(const file of ['test/autoplan-edit-queue-am.test.ts','test/fixtures/autoplan-edit-queue-am.json'])
|
||||
expect(selectTests([file],E2E_TOUCHFILES,[]).selected).toEqual(['autoplan-chain-pty']);
|
||||
});
|
||||
@@ -0,0 +1,89 @@
|
||||
import { expect, test } from 'bun:test';
|
||||
import { ceoFirstReviewAUQ, nativePlanCallFingerprint, type AskUserQuestionFingerprint } from './helpers/claude-pty-runner';
|
||||
import fixture from './fixtures/ceo-assertion-header-am-calls.json';
|
||||
import { E2E_TOUCHFILES } from './helpers/touchfiles-data';
|
||||
|
||||
const calls = fixture.calls as AskUserQuestionFingerprint[];
|
||||
const findings = calls.slice(2);
|
||||
function change(fp: AskUserQuestionFingerprint, edit: (q: NonNullable<AskUserQuestionFingerprint['nativeCall']>['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');
|
||||
}
|
||||
});
|
||||
@@ -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));
|
||||
});
|
||||
@@ -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<FP['nativeCall']>['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));
|
||||
@@ -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'); }
|
||||
});
|
||||
});
|
||||
@@ -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<paths.length;i++){expect(Object.hasOwn(paths,i)).toBe(true);expect(typeof paths[i]).toBe('string');}
|
||||
});
|
||||
test('current-owner withdrawals and conditional metadata cannot lend review evidence',()=>{
|
||||
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);
|
||||
});
|
||||
@@ -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');
|
||||
});
|
||||
@@ -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();
|
||||
});
|
||||
+211
@@ -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 endpLine truncated
|
||||
"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<!-- /autoplan run autoplan-20260910-092822-5a343286 | base branch: main (no remote; git-native fallback) | Codex: disabled (codex_reviews=disabled) | UI scope: yes | DX scope: yes (21 term matches, threshold 2) -->\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| SubLine truncated
|
||||
},
|
||||
"messageId": "msg_011CeuXYEmvrSEJx8miwvE4G",
|
||||
"requestId": "req_011CeuXYCocRXsuD5UYiXLqh"
|
||||
},
|
||||
{
|
||||
"sessionId": "9b7779e9-18f9-4f8d-84ac-9e8bf4458a8e",
|
||||
"timestamp": "2026-09-10T09:36:51.425Z",
|
||||
"toolUseId": "toolu_01EMZc2Hum5xqWxGHi3dddtJ",
|
||||
"kind": "result",
|
||||
"isError": false
|
||||
},
|
||||
{
|
||||
"sessionId": "9b7779e9-18f9-4f8d-84ac-9e8bf4458a8e",
|
||||
"timestamp": "2026-09-10T09:37:17.410Z",
|
||||
"toolUseId": "toolu_01SePuStrHVH7BetMTo8VAig",
|
||||
"kind": "use",
|
||||
"name": "Write",
|
||||
"input": {
|
||||
"file_path": "/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",
|
||||
"content": "---\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\nRepo: gstack-autoplan-chain-RnwL2i (no remote)\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: human ~3 weeks / CC ~1 day. Deferred: needs a ranking service and push infrastructure that do not exist. This plan lays the substrate (envelope, panel state machine, instrumentation) it would build on.\n\n## Scope Decisions\n\n| # | Proposal | Effort | Decision | Reasoning |\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; walkthrough number is a stand-in |\n| 2 | Numeric rollback triggers defined before rollout | S | ACCEPTED | Rollout criteria were \"to be specified\" |\n| 3 | Relative timestamps with absolute on hover/focus in ActivityFeed | S | ACCEPTED | 1 file, under an hour, in blast radius |\n| 4 | Unread count badge and document.title mirror | S | ACCEPTED | 1 file, under an hour |\n| 5 | \"Back to previous landing page\" link during rollout | S | ACCEPTED | Per-member escape hatch and bounce-back signal |\n| 6 | Empty-state copy pointing at the primary action | S | ACCEPTED | Copy only |\n| 7 | Keyboard shortcuts for quick actions | S | DEFERRED | Shortcut conflict audit needed; not on the metric path |\n| 8 | Prefetch /api/dashboard during login redirect | S | DEFERRED | Touches login flow, outside blast radius |\n| 9 | Post-login redirect-to-resume experiment arm | M | DEFERRED (taste T1 at gate) | Skips alerts; separate flag arm; attribution matters |\n| 10 | Ranked \"next up\" card with live updates | XL | DEFERRED | New ranking + push infrastructure |\n| 11 | ETag / short TTL cache on the endpoint | S | DEFERRED | Wait for p95 at 100% rollout |\n| 12 | Token-only styling lint rule | S | DEFERRED | Review-checklist constraint now |\n\n## Accepted Scope (added to this plan)\n- Analytics baseline pull and find/do split before target lock\n- Numeric rollback triggers\n- Relative timestamps in ActivityFeed\n- Unread badge + title mirror\n- Back-to-previous-landing link during rollout\n- Action-pointing empty-state copy\n- Approach C: aggregate endpoint with per-panel `PanelResult` envelope and `serverTime`\n- Shared `PanelFrame` and shared `Toast` primitive\n- QuickActions as visual primary; fixed column order at sm/md/lg\n\n## Deferred to TODOS.md\n- Keyboard shortcuts for quick actions (audit conflicts first)\n- Prefetch dashboard payload during login redirect\n- Redirect-to-resume experiment arm under the same flag framework (taste decision T1)\n- Ranked \"next up\" card + real-time push\n- Endpoint caching once p95 data exists\n- Token-only styling lint enforcement\n\n## Taste decisions surfaced at the /autoplan gate\n- T1: keep dashboard as the sole arm vs. run a redirect-to-resume experiment arm concurrently\n- T2: aggregate endpoint with envelope (C) vs. client composition of existing endpoints (B)\n- T3: keep confirmation modal vs. direct action with undo (would need a new mutation API)\n- T4: shared toast primitive vs. inline status text\n"
|
||||
},
|
||||
"messageId": "msg_011CeuXtM6jr7UmCW4ACpkFj",
|
||||
"requestId": "req_011CeuXtK3xqXgAGXD4r9pic"
|
||||
},
|
||||
{
|
||||
"sessionId": "9b7779e9-18f9-4f8d-84ac-9e8bf4458a8e",
|
||||
"timestamp": "2026-09-10T09:37:19.272Z",
|
||||
"toolUseId": "toolu_01SePuStrHVH7BetMTo8VAig",
|
||||
"kind": "result",
|
||||
"isError": false
|
||||
},
|
||||
{
|
||||
"sessionId": "9b7779e9-18f9-4f8d-84ac-9e8bf4458a8e",
|
||||
"timestamp": "2026-09-10T09:39:40.070Z",
|
||||
"toolUseId": "toolu_015XKfdk4d1H8xZ8uVSKe1to",
|
||||
"kind": "use",
|
||||
"name": "Write",
|
||||
"input": {
|
||||
"file_path": "/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",
|
||||
"content": "---\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.\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.\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: human ~3 weeks / CC ~1 day. 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 mutatLine truncated
|
||||
},
|
||||
"messageId": "msg_011CeuY3xtntQ4z7XESaBmS7",
|
||||
"requestId": "req_011CeuY3vfLs9WfLbPDaQsYb"
|
||||
},
|
||||
{
|
||||
"sessionId": "9b7779e9-18f9-4f8d-84ac-9e8bf4458a8e",
|
||||
"timestamp": "2026-09-10T09:39:41.652Z",
|
||||
"toolUseId": "toolu_015XKfdk4d1H8xZ8uVSKe1to",
|
||||
"kind": "result",
|
||||
"isError": false
|
||||
},
|
||||
{
|
||||
"sessionId": "9b7779e9-18f9-4f8d-84ac-9e8bf4458a8e",
|
||||
"timestamp": "2026-09-10T09:41:37.413Z",
|
||||
"toolUseId": "toolu_01BbKwZ7JFFdm2FLFdcNQXPq",
|
||||
"kind": "use",
|
||||
"name": "Edit",
|
||||
"input": {
|
||||
"replace_all": false,
|
||||
"file_path": "/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",
|
||||
"old_string": "**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.\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.",
|
||||
"new_string": "**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."
|
||||
},
|
||||
"messageId": "msg_011CeuYDnRH9L1Qoom8gBVdc",
|
||||
"requestId": "req_011CeuYDk9cAd6Yozh8QnH62"
|
||||
},
|
||||
{
|
||||
"sessionId": "9b7779e9-18f9-4f8d-84ac-9e8bf4458a8e",
|
||||
"timestamp": "2026-09-10T09:41:38.993Z",
|
||||
"toolUseId": "toolu_01BbKwZ7JFFdm2FLFdcNQXPq",
|
||||
"kind": "result",
|
||||
"isError": false
|
||||
},
|
||||
{
|
||||
"sessionId": "9b7779e9-18f9-4f8d-84ac-9e8bf4458a8e",
|
||||
"timestamp": "2026-09-10T09:41:40.421Z",
|
||||
"toolUseId": "toolu_01W9d5F6LgadaG9o9maU8kaT",
|
||||
"kind": "use",
|
||||
"name": "Edit",
|
||||
"input": {
|
||||
"replace_all": false,
|
||||
"file_path": "/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",
|
||||
"old_string": "The member arrives, sees one thing, and does it. Effort: human ~3 weeks / CC ~1 day. Deferred: needs a ranking service and push infrastructure that do not exist.",
|
||||
"new_string": "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."
|
||||
},
|
||||
"messageId": "msg_011CeuYDnRH9L1Qoom8gBVdc",
|
||||
"requestId": "req_011CeuYDk9cAd6Yozh8QnH62"
|
||||
},
|
||||
{
|
||||
"sessionId": "9b7779e9-18f9-4f8d-84ac-9e8bf4458a8e",
|
||||
"timestamp": "2026-09-10T09:41:43.424Z",
|
||||
"toolUseId": "toolu_01YXZEmM6gthBvGshzGE4zYb",
|
||||
"kind": "use",
|
||||
"name": "Edit",
|
||||
"input": {
|
||||
"replace_all": false,
|
||||
"file_path": "/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",
|
||||
"old_string": "| 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 |",
|
||||
"new_string": "| 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 | Dashboard owner re-locks the target in this document after the pull |"
|
||||
},
|
||||
"messageId": "msg_011CeuYDnRH9L1Qoom8gBVdc",
|
||||
"requestId": "req_011CeuYDk9cAd6Yozh8QnH62"
|
||||
},
|
||||
{
|
||||
"sessionId": "9b7779e9-18f9-4f8d-84ac-9e8bf4458a8e",
|
||||
"timestamp": "2026-09-10T09:41:47.171Z",
|
||||
"toolUseId": "toolu_01SYiANcdq3hLqGxEhDQVNJf",
|
||||
"kind": "use",
|
||||
"name": "Edit",
|
||||
"input": {
|
||||
"replace_all": false,
|
||||
"file_path": "/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",
|
||||
"old_string": "| 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 |",
|
||||
"new_string": "| 3 | Relative timestamps with absolute on hover/focus in ActivityFeed | S | ACCEPTED | 1 file, under an hour, in blast radius (cost-alone acceptance) | \u2014 |\n| 4 | Unread count badge and document.title mirror | S | ACCEPTED | 1 file, under an hour, in blast radius; also shortens the \"find\" half of find-vs-do | \u2014 |"
|
||||
},
|
||||
"messageId": "msg_011CeuYDnRH9L1Qoom8gBVdc",
|
||||
"requestId": "req_011CeuYDk9cAd6Yozh8QnH62"
|
||||
},
|
||||
{
|
||||
"sessionId": "9b7779e9-18f9-4f8d-84ac-9e8bf4458a8e",
|
||||
"timestamp": "2026-09-10T09:41:57.679Z",
|
||||
"toolUseId": "toolu_01LgaibBToDfuxGNFBKew9PS",
|
||||
"kind": "use",
|
||||
"name": "Edit",
|
||||
"input": {
|
||||
"replace_all": false,
|
||||
"file_path": "/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",
|
||||
"old_string": "| 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 |",
|
||||
"new_string": "| 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 the lint rule before the dark-mode plan starts, or when a second token-only consumer appears, whichever is first |\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. Cost: the endpoint owns per-panel timeout budgets and a partial-success contract (HTTP 200 with per-panel error codes), which is the main reason B could win at the Gate | Gate may choose B (client composes existing endpoints) |\n| 14 | Shared `PanelFrame` (state chrome) and shared `Toast` primitive | S | ACCEPTED, provisional (taste T4) | The three panels would otherwise duplicate identical loading/empty/error chrome; extracting it is the same code in one place. A11y policy requires a live region for nonblocking feedback and no toast exists | Gate may choose inline status text over toast |\n| 15 | QuickActions as visual primary; fixed panel order | S | ACCEPTED | Only panel that drives the metric; three equal cards is the generic pattern. Order: QuickActions, NotificationsPanel, ActivityFeed, left to right at lg; at md QuickActions spans the top row above Notifications and Activity; at sm the same order stacked | \u2014 |"
|
||||
},
|
||||
"messageId": "msg_011CeuYDnRH9L1Qoom8gBVdc",
|
||||
"requestId": "req_011CeuYDk9cAd6Yozh8QnH62"
|
||||
},
|
||||
{
|
||||
"sessionId": "9b7779e9-18f9-4f8d-84ac-9e8bf4458a8e",
|
||||
"timestamp": "2026-09-10T09:42:00.503Z",
|
||||
"toolUseId": "toolu_01VqJFXfD5cfdjiar1gAjpkV",
|
||||
"kind": "use",
|
||||
"name": "Edit",
|
||||
"input": {
|
||||
"replace_all": false,
|
||||
"file_path": "/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",
|
||||
"old_string": "- 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",
|
||||
"new_string": "- Confirmation modal on the existing dialog primitive (#16)"
|
||||
},
|
||||
"messageId": "msg_011CeuYDnRH9L1Qoom8gBVdc",
|
||||
"requestId": "req_011CeuYDk9cAd6Yozh8QnH62"
|
||||
},
|
||||
{
|
||||
"sessionId": "9b7779e9-18f9-4f8d-84ac-9e8bf4458a8e",
|
||||
"timestamp": "2026-09-10T09:42:01.075Z",
|
||||
"toolUseId": "toolu_01W9d5F6LgadaG9o9maU8kaT",
|
||||
"kind": "result",
|
||||
"isError": false
|
||||
}
|
||||
]
|
||||
}
|
||||
+229
@@ -0,0 +1,229 @@
|
||||
{
|
||||
"sourceObservationSha256": "7ba544360db58b1613bf1dbefab2dd7242c71a9a3341c5e66d833d8f5481dd50",
|
||||
"calls": [
|
||||
{
|
||||
"signature": "abb7e247-ba90-4ae1-8aec-fa748bf6b188:toolu_015Coa5yd2dGrFN8EB2kG1jE",
|
||||
"promptSnippet": "Routing D1 — Add gstack skill routing rules to this project's CLAUDE.md? Project/branch/task: gstack-plan-count-EFiY6H on main, CEO review of PLAN.md. ELI10: gstack works better when the project CLAUDE.md tells the agent which slash skill t",
|
||||
"options": [
|
||||
{
|
||||
"index": 1,
|
||||
"label": "Add routing rules (recommended)"
|
||||
},
|
||||
{
|
||||
"index": 2,
|
||||
"label": "No thanks, manual"
|
||||
},
|
||||
{
|
||||
"index": 1,
|
||||
"label": "Enable cross-project (recommended)"
|
||||
},
|
||||
{
|
||||
"index": 2,
|
||||
"label": "Project-scoped only"
|
||||
}
|
||||
],
|
||||
"observedAtMs": 208726,
|
||||
"preReview": true,
|
||||
"nativeCall": {
|
||||
"sessionId": "abb7e247-ba90-4ae1-8aec-fa748bf6b188",
|
||||
"toolUseId": "toolu_015Coa5yd2dGrFN8EB2kG1jE",
|
||||
"questions": [
|
||||
{
|
||||
"header": "Routing",
|
||||
"question": "D1 — Add gstack skill routing rules to this project's CLAUDE.md?\nProject/branch/task: gstack-plan-count-EFiY6H on main, CEO review of PLAN.md.\nELI10: gstack works better when the project CLAUDE.md tells the agent which slash skill to run for which kind of request. This is a one-time setup prompt. Note: plan mode is active, so the CLAUDE.md edit and commit cannot happen in this session; choosing A records the intent as a follow-up.\nStakes if we pick wrong: mild. Without routing, skills must be invoked by name; with it, a stale rule could mis-route once.\nRecommendation: A because routing rules make later reviews one command instead of a lookup.\nNote: options differ in kind, not coverage — no completeness score.\nA) Add routing rules to CLAUDE.md (recommended)\n ✅ Future requests like \"review this diff\" route to /review without the user remembering names\n ✅ Standard gstack setup; identical across projects so nothing bespoke to maintain\n ❌ Cannot be written or committed while plan mode is active; lands as a deferred task\nB) No thanks, I'll invoke skills manually\n ✅ Zero edits to a fixture repo that may be discarded after this review\n ✅ Config flag stops this prompt from appearing again for this project\n ❌ Every skill has to be typed by exact name from here on\nNet: convenience later versus zero churn in a throwaway fixture.",
|
||||
"options": [
|
||||
{
|
||||
"label": "Add routing rules (recommended)",
|
||||
"description": "Record as deferred: append the standard routing section to CLAUDE.md and commit once plan mode ends."
|
||||
},
|
||||
{
|
||||
"label": "No thanks, manual",
|
||||
"description": "Set routing_declined=true so this prompt does not recur for this project."
|
||||
}
|
||||
],
|
||||
"multiSelect": false
|
||||
},
|
||||
{
|
||||
"header": "Learnings",
|
||||
"question": "D2 — Let gstack search learnings from your other projects on this machine?\nProject/branch/task: gstack-plan-count-EFiY6H on main, CEO review of PLAN.md.\nELI10: gstack saves small lessons per project. Cross-project mode also searches lessons saved under other repos on this machine. Everything stays local. Good for solo developers; skip if you work across client codebases where mixing context is a concern.\nStakes if we pick wrong: low. A wrong choice can be flipped any time with gstack-config.\nRecommendation: A because a solo machine benefits from every past lesson and nothing leaves the disk.\nNote: options differ in kind, not coverage — no completeness score.\nA) Enable cross-project learnings (recommended)\n ✅ Patterns learned in one repo surface when the same pitfall appears elsewhere\n ✅ Data never leaves the machine; it is a local file search only\n ❌ Unrelated projects' lessons can add noise to the learnings list\nB) Keep learnings project-scoped only\n ✅ Strict isolation between client or team codebases on the same machine\n ✅ Learnings list stays short and specific to this repo\n ❌ Lessons already learned elsewhere have to be rediscovered here\nNet: broader recall versus strict per-project isolation.",
|
||||
"options": [
|
||||
{
|
||||
"label": "Enable cross-project (recommended)",
|
||||
"description": "Run gstack-config set cross_project_learnings true, then re-run the learnings search."
|
||||
},
|
||||
{
|
||||
"label": "Project-scoped only",
|
||||
"description": "Run gstack-config set cross_project_learnings false."
|
||||
}
|
||||
],
|
||||
"multiSelect": false
|
||||
}
|
||||
],
|
||||
"answered": true,
|
||||
"failed": false,
|
||||
"answers": {
|
||||
"D1 — Add gstack skill routing rules to this project's CLAUDE.md?\nProject/branch/task: gstack-plan-count-EFiY6H on main, CEO review of PLAN.md.\nELI10: gstack works better when the project CLAUDE.md tells the agent which slash skill to run for which kind of request. This is a one-time setup prompt. Note: plan mode is active, so the CLAUDE.md edit and commit cannot happen in this session; choosing A records the intent as a follow-up.\nStakes if we pick wrong: mild. Without routing, skills must be invoked by name; with it, a stale rule could mis-route once.\nRecommendation: A because routing rules make later reviews one command instead of a lookup.\nNote: options differ in kind, not coverage — no completeness score.\nA) Add routing rules to CLAUDE.md (recommended)\n ✅ Future requests like \"review this diff\" route to /review without the user remembering names\n ✅ Standard gstack setup; identical across projects so nothing bespoke to maintain\n ❌ Cannot be written or committed while plan mode is active; lands as a deferred task\nB) No thanks, I'll invoke skills manually\n ✅ Zero edits to a fixture repo that may be discarded after this review\n ✅ Config flag stops this prompt from appearing again for this project\n ❌ Every skill has to be typed by exact name from here on\nNet: convenience later versus zero churn in a throwaway fixture.": "Add routing rules (recommended)",
|
||||
"D2 — Let gstack search learnings from your other projects on this machine?\nProject/branch/task: gstack-plan-count-EFiY6H on main, CEO review of PLAN.md.\nELI10: gstack saves small lessons per project. Cross-project mode also searches lessons saved under other repos on this machine. Everything stays local. Good for solo developers; skip if you work across client codebases where mixing context is a concern.\nStakes if we pick wrong: low. A wrong choice can be flipped any time with gstack-config.\nRecommendation: A because a solo machine benefits from every past lesson and nothing leaves the disk.\nNote: options differ in kind, not coverage — no completeness score.\nA) Enable cross-project learnings (recommended)\n ✅ Patterns learned in one repo surface when the same pitfall appears elsewhere\n ✅ Data never leaves the machine; it is a local file search only\n ❌ Unrelated projects' lessons can add noise to the learnings list\nB) Keep learnings project-scoped only\n ✅ Strict isolation between client or team codebases on the same machine\n ✅ Learnings list stays short and specific to this repo\n ❌ Lessons already learned elsewhere have to be rediscovered here\nNet: broader recall versus strict per-project isolation.": "Enable cross-project (recommended)"
|
||||
},
|
||||
"unansweredQuestionIndices": [],
|
||||
"answeredAt": "2026-09-10T09:29:47.677Z"
|
||||
}
|
||||
},
|
||||
{
|
||||
"signature": "abb7e247-ba90-4ae1-8aec-fa748bf6b188:toolu_019aJqs1TV4J7TomcV5Yfprt",
|
||||
"promptSnippet": "Approach D3 — Which implementation structure for the two processPayment tests? Project/branch/task: gstack-plan-count-EFiY6H on main, CEO review of PLAN.md (HOLD SCOPE). ELI10: All three shapes can carry the same assertions; the question is",
|
||||
"options": [
|
||||
{
|
||||
"index": 1,
|
||||
"label": "A: Two inline tests (recommended)"
|
||||
},
|
||||
{
|
||||
"index": 2,
|
||||
"label": "B: Inline + factory helpers"
|
||||
},
|
||||
{
|
||||
"index": 3,
|
||||
"label": "C: Table-driven"
|
||||
}
|
||||
],
|
||||
"observedAtMs": 252932,
|
||||
"preReview": true,
|
||||
"nativeCall": {
|
||||
"sessionId": "abb7e247-ba90-4ae1-8aec-fa748bf6b188",
|
||||
"toolUseId": "toolu_019aJqs1TV4J7TomcV5Yfprt",
|
||||
"questions": [
|
||||
{
|
||||
"header": "Approach",
|
||||
"question": "D3 — Which implementation structure for the two processPayment tests?\nProject/branch/task: gstack-plan-count-EFiY6H on main, CEO review of PLAN.md (HOLD SCOPE).\nELI10: All three shapes can carry the same assertions; the question is how the two tests are organized. Inline tests are the smallest change. A factory helper or a table adds shared machinery that pays off only when more scenarios arrive. What the assertions actually check is decided separately in the test review.\nStakes if we pick wrong: too little structure means a little duplication later; too much means shared test infra changes for two call sites and harder-to-read failures.\nRecommendation: A because the right-sized diff for two tests is two tests; the existing factory already exposes everything the assertions need.\nCompleteness: A=10/10, B=10/10, C=9/10\nA) Two inline tests in the existing suite (recommended) (human: ~1h / CC: ~5min)\n ✅ Smallest diff; each test reads arrange, act, assert top to bottom with no indirection\n ✅ Uses the factory, mock call history and virtual sleeper exactly as they exist today\n ❌ Receipt literal and attempt-count literals sit in the test body; a third scenario would repeat the shape\nB) Inline tests plus shared factory helpers (human: ~3h / CC: ~15min)\n ✅ Future retry tests reuse an expectAttempts(n, backoff[]) style check instead of re-deriving it\n ✅ Keeps count and backoff assertions in one place so they cannot drift between suites\n ❌ Modifies shared test infrastructure for two call sites; premature abstraction under HOLD SCOPE\nC) Table-driven describe.each (human: ~3h / CC: ~15min)\n ✅ Adding a scenario becomes one row with responses, expected outcome, attempts and backoff\n ✅ Forces every scenario to state attempts and backoff, so a row cannot skip them\n ❌ Two rows do not justify a table; a failing row reports an index, not the intent of the test\nNet: minimal and readable now versus machinery for scenarios that are not in scope.",
|
||||
"options": [
|
||||
{
|
||||
"label": "A: Two inline tests (recommended)",
|
||||
"description": "Two it blocks in the existing suite using the current factory, mock and sleeper. Completeness 10/10."
|
||||
},
|
||||
{
|
||||
"label": "B: Inline + factory helpers",
|
||||
"description": "Same tests plus shared arrange/expect helpers added to the payment test factory. Completeness 10/10."
|
||||
},
|
||||
{
|
||||
"label": "C: Table-driven",
|
||||
"description": "One describe.each over scenario rows. Completeness 9/10."
|
||||
}
|
||||
],
|
||||
"multiSelect": false
|
||||
}
|
||||
],
|
||||
"answered": true,
|
||||
"failed": false,
|
||||
"answers": {
|
||||
"D3 — Which implementation structure for the two processPayment tests?\nProject/branch/task: gstack-plan-count-EFiY6H on main, CEO review of PLAN.md (HOLD SCOPE).\nELI10: All three shapes can carry the same assertions; the question is how the two tests are organized. Inline tests are the smallest change. A factory helper or a table adds shared machinery that pays off only when more scenarios arrive. What the assertions actually check is decided separately in the test review.\nStakes if we pick wrong: too little structure means a little duplication later; too much means shared test infra changes for two call sites and harder-to-read failures.\nRecommendation: A because the right-sized diff for two tests is two tests; the existing factory already exposes everything the assertions need.\nCompleteness: A=10/10, B=10/10, C=9/10\nA) Two inline tests in the existing suite (recommended) (human: ~1h / CC: ~5min)\n ✅ Smallest diff; each test reads arrange, act, assert top to bottom with no indirection\n ✅ Uses the factory, mock call history and virtual sleeper exactly as they exist today\n ❌ Receipt literal and attempt-count literals sit in the test body; a third scenario would repeat the shape\nB) Inline tests plus shared factory helpers (human: ~3h / CC: ~15min)\n ✅ Future retry tests reuse an expectAttempts(n, backoff[]) style check instead of re-deriving it\n ✅ Keeps count and backoff assertions in one place so they cannot drift between suites\n ❌ Modifies shared test infrastructure for two call sites; premature abstraction under HOLD SCOPE\nC) Table-driven describe.each (human: ~3h / CC: ~15min)\n ✅ Adding a scenario becomes one row with responses, expected outcome, attempts and backoff\n ✅ Forces every scenario to state attempts and backoff, so a row cannot skip them\n ❌ Two rows do not justify a table; a failing row reports an index, not the intent of the test\nNet: minimal and readable now versus machinery for scenarios that are not in scope.": "A: Two inline tests (recommended)"
|
||||
},
|
||||
"unansweredQuestionIndices": [],
|
||||
"answeredAt": "2026-09-10T09:30:32.382Z"
|
||||
}
|
||||
},
|
||||
{
|
||||
"signature": "abb7e247-ba90-4ae1-8aec-fa748bf6b188:toolu_01PyvaDPZuvXX5vW38NtXsAk",
|
||||
"promptSnippet": "Test 1 assert D4 (Issue 1) — Successful-charge test asserts only that the receipt is truthy. Pin the stated receipt contract instead? Project/branch/task: gstack-plan-count-EFiY6H on main, CEO review of PLAN.md (HOLD SCOPE). ELI10: PLAN.md ",
|
||||
"options": [
|
||||
{
|
||||
"index": 1,
|
||||
"label": "1A: Deep-equal exact receipt (recommended)"
|
||||
},
|
||||
{
|
||||
"index": 2,
|
||||
"label": "1B: chargeId only"
|
||||
},
|
||||
{
|
||||
"index": 3,
|
||||
"label": "1C: Keep truthy"
|
||||
}
|
||||
],
|
||||
"observedAtMs": 300691,
|
||||
"preReview": true,
|
||||
"nativeCall": {
|
||||
"sessionId": "abb7e247-ba90-4ae1-8aec-fa748bf6b188",
|
||||
"toolUseId": "toolu_01PyvaDPZuvXX5vW38NtXsAk",
|
||||
"questions": [
|
||||
{
|
||||
"header": "Test 1 assert",
|
||||
"question": "D4 (Issue 1) — Successful-charge test asserts only that the receipt is truthy. Pin the stated receipt contract instead?\nProject/branch/task: gstack-plan-count-EFiY6H on main, CEO review of PLAN.md (HOLD SCOPE).\nELI10: PLAN.md lines 18-21 say a 1000-cent USD charge with Stripe id ch_paid must return exactly { chargeId: \"ch_paid\", amountCents: 1000, currency: \"USD\" }. PLAN.md line 32 then plans to assert only that the receipt is truthy. Any non-null object passes that check: wrong chargeId, amount in dollars instead of cents, currency lowercased, an extra field. The test would go green while the contract it claims to cover is broken.\nStakes if we pick wrong: a receipt regression (wrong amount shown to the customer, wrong charge id in support tooling) ships with a passing test that says the path is covered.\nRecommendation: 1A because a test that cannot fail when the contract breaks is not coverage; well-tested code is non-negotiable and the exact values are already written in the plan.\nCompleteness: 1A=10/10, 1B=7/10, 1C=1/10\n1A) Deep-equal the receipt to the exact stated literal (recommended) (human: ~10min / CC: ~1min)\n ✅ Rejects a wrong chargeId, wrong amountCents, wrong currency, missing or extra field in one assertion\n ✅ Failure output names the differing field, so a regression is diagnosed from the test report alone\n ❌ Adding a new receipt field later is a deliberate one-line test update, not a silent pass\n1B) Assert chargeId only, keep the rest truthy (human: ~10min / CC: ~1min)\n ✅ Catches the most visible failure, a receipt whose charge id does not match Stripe\n ✅ Tolerates receipt shape changes without touching the test\n ❌ amountCents and currency, both stated in the contract, remain unverified and can drift silently\n1C) Keep the plan as written: receipt is truthy (human: 0 / CC: 0)\n ✅ No change to the plan text\n ✅ Cannot flake on any field value\n ❌ Passes for any object at all; the contract in PLAN.md lines 18-21 is not tested\nNet: an assertion that names the three fields versus a green check that proves only non-null.",
|
||||
"options": [
|
||||
{
|
||||
"label": "1A: Deep-equal exact receipt (recommended)",
|
||||
"description": "expect(receipt).toEqual({ chargeId: \"ch_paid\", amountCents: 1000, currency: \"USD\" }). Completeness 10/10."
|
||||
},
|
||||
{
|
||||
"label": "1B: chargeId only",
|
||||
"description": "Assert receipt.chargeId === \"ch_paid\"; leave amountCents and currency unchecked. Completeness 7/10."
|
||||
},
|
||||
{
|
||||
"label": "1C: Keep truthy",
|
||||
"description": "Leave PLAN.md line 32 as written. Completeness 1/10."
|
||||
}
|
||||
],
|
||||
"multiSelect": false
|
||||
}
|
||||
],
|
||||
"answered": true,
|
||||
"failed": false,
|
||||
"answers": {
|
||||
"D4 (Issue 1) — Successful-charge test asserts only that the receipt is truthy. Pin the stated receipt contract instead?\nProject/branch/task: gstack-plan-count-EFiY6H on main, CEO review of PLAN.md (HOLD SCOPE).\nELI10: PLAN.md lines 18-21 say a 1000-cent USD charge with Stripe id ch_paid must return exactly { chargeId: \"ch_paid\", amountCents: 1000, currency: \"USD\" }. PLAN.md line 32 then plans to assert only that the receipt is truthy. Any non-null object passes that check: wrong chargeId, amount in dollars instead of cents, currency lowercased, an extra field. The test would go green while the contract it claims to cover is broken.\nStakes if we pick wrong: a receipt regression (wrong amount shown to the customer, wrong charge id in support tooling) ships with a passing test that says the path is covered.\nRecommendation: 1A because a test that cannot fail when the contract breaks is not coverage; well-tested code is non-negotiable and the exact values are already written in the plan.\nCompleteness: 1A=10/10, 1B=7/10, 1C=1/10\n1A) Deep-equal the receipt to the exact stated literal (recommended) (human: ~10min / CC: ~1min)\n ✅ Rejects a wrong chargeId, wrong amountCents, wrong currency, missing or extra field in one assertion\n ✅ Failure output names the differing field, so a regression is diagnosed from the test report alone\n ❌ Adding a new receipt field later is a deliberate one-line test update, not a silent pass\n1B) Assert chargeId only, keep the rest truthy (human: ~10min / CC: ~1min)\n ✅ Catches the most visible failure, a receipt whose charge id does not match Stripe\n ✅ Tolerates receipt shape changes without touching the test\n ❌ amountCents and currency, both stated in the contract, remain unverified and can drift silently\n1C) Keep the plan as written: receipt is truthy (human: 0 / CC: 0)\n ✅ No change to the plan text\n ✅ Cannot flake on any field value\n ❌ Passes for any object at all; the contract in PLAN.md lines 18-21 is not tested\nNet: an assertion that names the three fields versus a green check that proves only non-null.": "1A: Deep-equal exact receipt (recommended)"
|
||||
},
|
||||
"unansweredQuestionIndices": [],
|
||||
"answeredAt": "2026-09-10T09:31:20.141Z"
|
||||
}
|
||||
},
|
||||
{
|
||||
"signature": "abb7e247-ba90-4ae1-8aec-fa748bf6b188:toolu_019MYTyEPevTbfnd4mrzqtZe",
|
||||
"promptSnippet": "Test 2 assert D5 (Issue 2) — Repeated-502 test asserts only the PaymentUnavailable rejection. Also pin the retry contract: exactly two attempts and one 100 ms backoff? Project/branch/task: gstack-plan-count-EFiY6H on main, CEO review of PLA",
|
||||
"options": [
|
||||
{
|
||||
"index": 1,
|
||||
"label": "2A: Rejection + 2 attempts + [100] backoff (recommended)"
|
||||
},
|
||||
{
|
||||
"index": 2,
|
||||
"label": "2B: Rejection + 2 attempts"
|
||||
},
|
||||
{
|
||||
"index": 3,
|
||||
"label": "2C: Keep rejection only"
|
||||
}
|
||||
],
|
||||
"observedAtMs": 324830,
|
||||
"preReview": true,
|
||||
"nativeCall": {
|
||||
"sessionId": "abb7e247-ba90-4ae1-8aec-fa748bf6b188",
|
||||
"toolUseId": "toolu_019MYTyEPevTbfnd4mrzqtZe",
|
||||
"questions": [
|
||||
{
|
||||
"header": "Test 2 assert",
|
||||
"question": "D5 (Issue 2) — Repeated-502 test asserts only the PaymentUnavailable rejection. Also pin the retry contract: exactly two attempts and one 100 ms backoff?\nProject/branch/task: gstack-plan-count-EFiY6H on main, CEO review of PLAN.md (HOLD SCOPE).\nELI10: PLAN.md lines 22-23 say that with max_retries=1, repeated 502s produce exactly two charge attempts separated by one recorded 100 ms backoff, then PaymentUnavailable. PLAN.md lines 33-36 plan to assert only the rejection and explicitly skip the mock call history and sleeper record. That test passes if the code gives up after one attempt, retries five times, or retries with no backoff at all. The factory already exposes the call history and the virtual sleeper record, so the check costs two lines.\nStakes if we pick wrong: a retry regression (no retry, or a hot loop hammering Stripe with no backoff) ships with a passing test; customers see spurious failures or Stripe rate-limits the account.\nRecommendation: 2A because the retry count and backoff are the contract; the rejection alone proves nothing about them. Exact counts, never lower bounds.\nCompleteness: 2A=10/10, 2B=6/10, 2C=2/10\n2A) Assert rejection class, exactly 2 attempts, sleeper record exactly [100] (recommended) (human: ~15min / CC: ~2min)\n ✅ Rejects zero retries, extra retries, missing backoff and wrong backoff duration in one test\n ✅ Uses the mock call history and virtual sleeper the factory already exposes; no new infrastructure\n ❌ Changing the backoff constant or max_retries later requires updating the literal in this test\n2B) Assert rejection class and exactly 2 attempts, skip the sleeper record (human: ~12min / CC: ~2min)\n ✅ Catches no-retry and over-retry regressions, the two most likely failures\n ✅ One fewer literal to maintain if backoff timing changes\n ❌ A retry with zero backoff, the hot-loop case, passes; the stated 100 ms contract is untested\n2C) Keep the plan as written: rejection only (human: 0 / CC: 0)\n ✅ No change to the plan text\n ✅ Immune to any retry or timing detail\n ❌ Passes with 1, 2 or 20 attempts and no backoff; the contract in PLAN.md lines 22-23 is not tested\nNet: two extra assertions that make the retry contract falsifiable versus a rejection check that proves only the error class.",
|
||||
"options": [
|
||||
{
|
||||
"label": "2A: Rejection + 2 attempts + [100] backoff (recommended)",
|
||||
"description": "await expect(p).rejects.toBeInstanceOf(PaymentUnavailable); expect(stripe.calls).toHaveLength(2); expect(sleeper.record).toEqual([100]). Completeness 10/10."
|
||||
},
|
||||
{
|
||||
"label": "2B: Rejection + 2 attempts",
|
||||
"description": "Assert class and exact attempt count; leave the sleeper record unchecked. Completeness 6/10."
|
||||
},
|
||||
{
|
||||
"label": "2C: Keep rejection only",
|
||||
"description": "Leave PLAN.md lines 33-36 as written. Completeness 2/10."
|
||||
}
|
||||
],
|
||||
"multiSelect": false
|
||||
}
|
||||
],
|
||||
"answered": true,
|
||||
"failed": false,
|
||||
"answers": {
|
||||
"D5 (Issue 2) — Repeated-502 test asserts only the PaymentUnavailable rejection. Also pin the retry contract: exactly two attempts and one 100 ms backoff?\nProject/branch/task: gstack-plan-count-EFiY6H on main, CEO review of PLAN.md (HOLD SCOPE).\nELI10: PLAN.md lines 22-23 say that with max_retries=1, repeated 502s produce exactly two charge attempts separated by one recorded 100 ms backoff, then PaymentUnavailable. PLAN.md lines 33-36 plan to assert only the rejection and explicitly skip the mock call history and sleeper record. That test passes if the code gives up after one attempt, retries five times, or retries with no backoff at all. The factory already exposes the call history and the virtual sleeper record, so the check costs two lines.\nStakes if we pick wrong: a retry regression (no retry, or a hot loop hammering Stripe with no backoff) ships with a passing test; customers see spurious failures or Stripe rate-limits the account.\nRecommendation: 2A because the retry count and backoff are the contract; the rejection alone proves nothing about them. Exact counts, never lower bounds.\nCompleteness: 2A=10/10, 2B=6/10, 2C=2/10\n2A) Assert rejection class, exactly 2 attempts, sleeper record exactly [100] (recommended) (human: ~15min / CC: ~2min)\n ✅ Rejects zero retries, extra retries, missing backoff and wrong backoff duration in one test\n ✅ Uses the mock call history and virtual sleeper the factory already exposes; no new infrastructure\n ❌ Changing the backoff constant or max_retries later requires updating the literal in this test\n2B) Assert rejection class and exactly 2 attempts, skip the sleeper record (human: ~12min / CC: ~2min)\n ✅ Catches no-retry and over-retry regressions, the two most likely failures\n ✅ One fewer literal to maintain if backoff timing changes\n ❌ A retry with zero backoff, the hot-loop case, passes; the stated 100 ms contract is untested\n2C) Keep the plan as written: rejection only (human: 0 / CC: 0)\n ✅ No change to the plan text\n ✅ Immune to any retry or timing detail\n ❌ Passes with 1, 2 or 20 attempts and no backoff; the contract in PLAN.md lines 22-23 is not tested\nNet: two extra assertions that make the retry contract falsifiable versus a rejection check that proves only the error class.": "2A: Rejection + 2 attempts + [100] backoff (recommended)"
|
||||
},
|
||||
"unansweredQuestionIndices": [],
|
||||
"answeredAt": "2026-09-10T09:31:44.278Z"
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,60 @@
|
||||
{
|
||||
"provenance": {
|
||||
"sourceHead": "fd7698b23d0d88dfcfe8a2b0fd4c3140cdad01fa",
|
||||
"sourceSnapshotSha256": "301da80253ea22bb5ce253badc41acee083276922775aaa05f37cd9f32f9ee8c",
|
||||
"historicalOutcome": "no_review_questions",
|
||||
"paidOutcomesReclassified": false
|
||||
},
|
||||
"fingerprint": {
|
||||
"signature": "7f5f0e7e-9a06-41c1-8bc2-de4416f05cc4:toolu_01Xw5UP67eAKs8y9Dgz8aKPE",
|
||||
"promptSnippet": "Issue 1 D2 \u2014 Issue 1 (Pass 1, Visual Hierarchy): make Save the single primary action? Project/branch/task: main branch, PLAN.md design review, header action group. ELI10: Right now Save, Reset, Cancel, and Export all look identical. A user ",
|
||||
"options": [
|
||||
{
|
||||
"index": 1,
|
||||
"label": "1A) Save filled #1d4ed8/white, others ghost (recommended)"
|
||||
},
|
||||
{
|
||||
"index": 2,
|
||||
"label": "1B) Weight and size only, keep colors"
|
||||
},
|
||||
{
|
||||
"index": 3,
|
||||
"label": "1C) Decline, leave all four identical"
|
||||
}
|
||||
],
|
||||
"observedAtMs": 232060,
|
||||
"preReview": true,
|
||||
"nativeCall": {
|
||||
"sessionId": "7f5f0e7e-9a06-41c1-8bc2-de4416f05cc4",
|
||||
"toolUseId": "toolu_01Xw5UP67eAKs8y9Dgz8aKPE",
|
||||
"questions": [
|
||||
{
|
||||
"header": "Issue 1",
|
||||
"question": "D2 \u2014 Issue 1 (Pass 1, Visual Hierarchy): make Save the single primary action?\nProject/branch/task: main branch, PLAN.md design review, header action group.\nELI10: Right now Save, Reset, Cancel, and Export all look identical. A user scanning the header sees four equal grey buttons and has to read every label to find the one that stores their work. DESIGN.md already says Save is the only filled primary button and the other three are neutral ghost buttons. This just brings the implementation in line with that.\nStakes if we pick wrong: users hesitate at the one moment the journey needs confidence, and some hit Reset or Cancel by mistake next to Save.\nRecommendation: 1A because DESIGN.md already prescribes the exact tokens and the change is one class swap per button.\nCompleteness: 1A=10/10, 1B=5/10, 1C=0/10\nNet: a token-exact fix now versus leaving a documented hierarchy gap in the shipped header.",
|
||||
"options": [
|
||||
{
|
||||
"label": "1A) Save filled #1d4ed8/white, others ghost (recommended)",
|
||||
"description": "\u2705 Matches DESIGN.md exactly: one filled primary, three neutral ghosts, 44px targets kept. (human: ~30min / CC: ~3min) \u2705 White-on-#1d4ed8 text is about 8:1 contrast, well above AA. \u274c Ghost buttons must still show a visible border or the row loses its clickability signal."
|
||||
},
|
||||
{
|
||||
"label": "1B) Weight and size only, keep colors",
|
||||
"description": "\u2705 Save stands out without touching the palette. \u2705 Smallest visual delta from the current build. \u274c Contradicts DESIGN.md's filled-primary rule, so /design-review will flag it again later."
|
||||
},
|
||||
{
|
||||
"label": "1C) Decline, leave all four identical",
|
||||
"description": "\u2705 Zero implementation work in this update. \u2705 Nothing new to QA visually. \u274c Documented DESIGN.md violation ships and Pass 1 stays at 7/10."
|
||||
}
|
||||
],
|
||||
"multiSelect": false
|
||||
}
|
||||
],
|
||||
"answered": true,
|
||||
"failed": false,
|
||||
"answers": {
|
||||
"D2 \u2014 Issue 1 (Pass 1, Visual Hierarchy): make Save the single primary action?\nProject/branch/task: main branch, PLAN.md design review, header action group.\nELI10: Right now Save, Reset, Cancel, and Export all look identical. A user scanning the header sees four equal grey buttons and has to read every label to find the one that stores their work. DESIGN.md already says Save is the only filled primary button and the other three are neutral ghost buttons. This just brings the implementation in line with that.\nStakes if we pick wrong: users hesitate at the one moment the journey needs confidence, and some hit Reset or Cancel by mistake next to Save.\nRecommendation: 1A because DESIGN.md already prescribes the exact tokens and the change is one class swap per button.\nCompleteness: 1A=10/10, 1B=5/10, 1C=0/10\nNet: a token-exact fix now versus leaving a documented hierarchy gap in the shipped header.": "1A) Save filled #1d4ed8/white, others ghost (recommended)"
|
||||
},
|
||||
"unansweredQuestionIndices": [],
|
||||
"answeredAt": "2026-09-10T09:41:05.919Z"
|
||||
}
|
||||
}
|
||||
}
|
||||
+60
@@ -0,0 +1,60 @@
|
||||
{
|
||||
"provenance": {
|
||||
"description": "Exact public completed native design decision; original collector classified it before review. No paid outcome is reclassified by this fixture.",
|
||||
"sourceHead": "fd7698b23d0d88dfcfe8a2b0fd4c3140cdad01fa",
|
||||
"sourceSnapshotSha256": "d4d45ac4ae5dcb422d5975947d21a1dab3dc309a7a6b7a4adeb380a1e2ef3226",
|
||||
"toolUseId": "toolu_01W2iQJNBSNGQZzqZ7dc7esd"
|
||||
},
|
||||
"fingerprint": {
|
||||
"signature": "e0671d37-5d77-408a-ad01-1977acac386c:toolu_01W2iQJNBSNGQZzqZ7dc7esd",
|
||||
"promptSnippet": "Issue 1 D3 \u2014 Issue 1: Make Save the single primary action in the header? Project/branch/task: main, /plan-design-review of the Settings Page plan, Pass 1 (Information Architecture). ELI10: Right now Save, Reset, Cancel and Export all look i",
|
||||
"options": [
|
||||
{
|
||||
"index": 1,
|
||||
"label": "1A Filled Save, ghost others (recommended)"
|
||||
},
|
||||
{
|
||||
"index": 2,
|
||||
"label": "1B Filled Save, outlined Export, ghost Reset/Cancel"
|
||||
},
|
||||
{
|
||||
"index": 3,
|
||||
"label": "1C Keep four equal buttons"
|
||||
}
|
||||
],
|
||||
"observedAtMs": 272308,
|
||||
"preReview": true,
|
||||
"nativeCall": {
|
||||
"sessionId": "e0671d37-5d77-408a-ad01-1977acac386c",
|
||||
"toolUseId": "toolu_01W2iQJNBSNGQZzqZ7dc7esd",
|
||||
"questions": [
|
||||
{
|
||||
"question": "D3 \u2014 Issue 1: Make Save the single primary action in the header?\nProject/branch/task: main, /plan-design-review of the Settings Page plan, Pass 1 (Information Architecture).\nELI10: Right now Save, Reset, Cancel and Export all look identical. A user scanning the header cannot tell which button is the one they came for, so they hesitate or misclick Reset next to it. DESIGN.md already says Save is the only filled button (#1d4ed8 with white text, about 6.7:1 contrast) and the other three are neutral ghost buttons. This is Design Principle 2: every screen has a hierarchy.\nStakes if we pick wrong: ship as-is and every visit starts with a small puzzle; a misclick on Reset opens a discard dialog the user did not want.\nRecommendation: 1A because it applies the existing DESIGN.md token with no new component work.\nCompleteness: 1A=10/10, 1B=6/10, 1C=0/10\nNet: trading a few lines of button variant props for an unambiguous primary action.",
|
||||
"header": "Issue 1",
|
||||
"multiSelect": false,
|
||||
"options": [
|
||||
{
|
||||
"label": "1A Filled Save, ghost others (recommended)",
|
||||
"description": "\u2705 Save reads as the one primary action in the first 3-second scan of the header.\n\u2705 Uses the existing Button primary and ghost variants from DESIGN.md; no new styles. (human: ~30min / CC: ~3min)\n\u274c Reset/Cancel/Export become visually quieter, so Export discoverability relies on its label alone."
|
||||
},
|
||||
{
|
||||
"label": "1B Filled Save, outlined Export, ghost Reset/Cancel",
|
||||
"description": "\u2705 Save is primary while Export gets a secondary emphasis for users who come mainly to download.\n\u2705 Still reuses existing Button variants if an outlined variant exists. (human: ~1h / CC: ~5min)\n\u274c Introduces a third emphasis level DESIGN.md does not define, weakening the single-primary rule."
|
||||
},
|
||||
{
|
||||
"label": "1C Keep four equal buttons",
|
||||
"description": "\u2705 Zero implementation change to the header.\n\u2705 Avoids any visual shift for existing users.\n\u274c Violates DESIGN.md and leaves users guessing which action is primary; Pass 1 stays at 6/10."
|
||||
}
|
||||
]
|
||||
}
|
||||
],
|
||||
"answered": true,
|
||||
"failed": false,
|
||||
"answers": {
|
||||
"D3 \u2014 Issue 1: Make Save the single primary action in the header?\nProject/branch/task: main, /plan-design-review of the Settings Page plan, Pass 1 (Information Architecture).\nELI10: Right now Save, Reset, Cancel and Export all look identical. A user scanning the header cannot tell which button is the one they came for, so they hesitate or misclick Reset next to it. DESIGN.md already says Save is the only filled button (#1d4ed8 with white text, about 6.7:1 contrast) and the other three are neutral ghost buttons. This is Design Principle 2: every screen has a hierarchy.\nStakes if we pick wrong: ship as-is and every visit starts with a small puzzle; a misclick on Reset opens a discard dialog the user did not want.\nRecommendation: 1A because it applies the existing DESIGN.md token with no new component work.\nCompleteness: 1A=10/10, 1B=6/10, 1C=0/10\nNet: trading a few lines of button variant props for an unambiguous primary action.": "1A Filled Save, ghost others (recommended)"
|
||||
},
|
||||
"unansweredQuestionIndices": [],
|
||||
"answeredAt": "2026-09-10T09:30:46.168Z"
|
||||
}
|
||||
}
|
||||
}
|
||||
+170
@@ -0,0 +1,170 @@
|
||||
{
|
||||
"provenance": {
|
||||
"sourceHead": "fd7698b23d0d88dfcfe8a2b0fd4c3140cdad01fa",
|
||||
"sourceSnapshotSha256": "94e1f7637004b0c60dc70e38ff5bc0ef17ae07b2240354fca389d204c05f970a",
|
||||
"historicalOutcome": "plan_ready; seeded-gap assertion failed",
|
||||
"paidOutcomesReclassified": false
|
||||
},
|
||||
"calls": [
|
||||
{
|
||||
"sessionId": "6473b11e-784e-4c0b-9777-c7197c1c273f",
|
||||
"toolUseId": "toolu_01SCW3STwTGeD6P2FAjL9Dnj",
|
||||
"questions": [
|
||||
{
|
||||
"header": "Quickstart",
|
||||
"question": "D1 \u2014 Quickstart points at examples/first_eval.py, which is not shipped\nProject/branch/task: EvalKit SDK beta (main), DX POLISH review, journey stage Hello World.\nELI10: README.md line 11 tells the developer to run `python examples/first_eval.py`. docs/package-contents.txt confirms that file is in neither the published wheel nor the examples archive. Your ML engineer hits \"No such file or directory\" at about T+0:30, on the very first command after install, and wonders whether the install is broken. The working demo, `python -m evalkit.demo`, is further down the page.\nStakes if we pick wrong: The first thing the persona runs fails. That is the minute-two abandonment moment, and it hides the demo that actually works.\nRecommendation: A because the demo module already ships and works; pointing the quickstart at it fixes the dead command with a docs edit and gives one golden path. Principle: zero friction at T0.\nCompleteness: A=10/10, B=8/10, C=3/10\nA) Make `python -m evalkit.demo` the quickstart command and drop the first_eval.py reference (recommended) (human: ~30 min / CC: ~3 min)\n \u2705 Every command in the quickstart runs against the shipped 2.0.0b1 package as published\n \u2705 One golden path: install, demo, then live eval, no \"choose your own adventure\"\n \u274c Loses the named example file some readers expect to open and edit locally\nB) Ship examples/first_eval.py in the package and archive so the current command works (human: ~2 h / CC: ~15 min)\n \u2705 Keeps the quickstart text as written and gives an editable script to start from\n \u2705 Provides a real-usage sample beyond the fixed demo output\n \u274c Two first-run paths compete on the same page, and a second file must stay in sync with demo.py\nC) Keep the reference and add a note that the file is coming later (human: ~10 min / CC: ~1 min)\n \u2705 Smallest possible edit before the beta ships\n \u2705 Signals the intended example so early users know what to expect\n \u274c The first quickstart command still fails for every developer in the beta\nNet: repair the dead command by pointing at what ships versus shipping what the text promises.",
|
||||
"options": [
|
||||
{
|
||||
"label": "A: Point quickstart at demo (recommended)",
|
||||
"description": "README quickstart runs python -m evalkit.demo; remove first_eval.py reference. Completeness 10/10."
|
||||
},
|
||||
{
|
||||
"label": "B: Ship first_eval.py",
|
||||
"description": "Add the example to the package and archive so the existing command works. Completeness 8/10."
|
||||
},
|
||||
{
|
||||
"label": "C: Note it as coming later",
|
||||
"description": "Keep the reference with a caveat; first command still fails. Completeness 3/10."
|
||||
}
|
||||
],
|
||||
"multiSelect": false
|
||||
}
|
||||
],
|
||||
"answered": true,
|
||||
"failed": false,
|
||||
"answers": {
|
||||
"D1 \u2014 Quickstart points at examples/first_eval.py, which is not shipped\nProject/branch/task: EvalKit SDK beta (main), DX POLISH review, journey stage Hello World.\nELI10: README.md line 11 tells the developer to run `python examples/first_eval.py`. docs/package-contents.txt confirms that file is in neither the published wheel nor the examples archive. Your ML engineer hits \"No such file or directory\" at about T+0:30, on the very first command after install, and wonders whether the install is broken. The working demo, `python -m evalkit.demo`, is further down the page.\nStakes if we pick wrong: The first thing the persona runs fails. That is the minute-two abandonment moment, and it hides the demo that actually works.\nRecommendation: A because the demo module already ships and works; pointing the quickstart at it fixes the dead command with a docs edit and gives one golden path. Principle: zero friction at T0.\nCompleteness: A=10/10, B=8/10, C=3/10\nA) Make `python -m evalkit.demo` the quickstart command and drop the first_eval.py reference (recommended) (human: ~30 min / CC: ~3 min)\n \u2705 Every command in the quickstart runs against the shipped 2.0.0b1 package as published\n \u2705 One golden path: install, demo, then live eval, no \"choose your own adventure\"\n \u274c Loses the named example file some readers expect to open and edit locally\nB) Ship examples/first_eval.py in the package and archive so the current command works (human: ~2 h / CC: ~15 min)\n \u2705 Keeps the quickstart text as written and gives an editable script to start from\n \u2705 Provides a real-usage sample beyond the fixed demo output\n \u274c Two first-run paths compete on the same page, and a second file must stay in sync with demo.py\nC) Keep the reference and add a note that the file is coming later (human: ~10 min / CC: ~1 min)\n \u2705 Smallest possible edit before the beta ships\n \u2705 Signals the intended example so early users know what to expect\n \u274c The first quickstart command still fails for every developer in the beta\nNet: repair the dead command by pointing at what ships versus shipping what the text promises.": "A: Point quickstart at demo (recommended)"
|
||||
},
|
||||
"unansweredQuestionIndices": [],
|
||||
"answeredAt": "2026-09-10T09:30:01.097Z"
|
||||
},
|
||||
{
|
||||
"sessionId": "6473b11e-784e-4c0b-9777-c7197c1c273f",
|
||||
"toolUseId": "toolu_012VCMJYcAdtrgKeyZvkK7Db",
|
||||
"questions": [
|
||||
{
|
||||
"header": "CI wait",
|
||||
"question": "D2 \u2014 First local evaluation blocks 5 minutes on a mandatory remote CI check\nProject/branch/task: EvalKit SDK beta (main), DX POLISH review, journey stage Hello World.\nELI10: docs/current-contracts.md lines 3-5: the first local evaluation requires a successful remote CI check and blocks for five minutes, with no skip flag and no offline path. The keyless demo inherits the same wait (README lines 17-23). Your persona wants a local score first and CI second; the SDK forces the order the other way round. docs/benchmarks.md measured 6 minutes, of which 5 is this wait. Peers land at 2 to 4 minutes and the approved target is under 2. The progress lines and timeout message already exist and are good; they do not shorten the wait.\nStakes if we pick wrong: The approved Champion target is unreachable while this gate stands. The magical moment lands at T+5:30 instead of T+0:30, and a remote CI outage turns the local demo into EVALKIT_CI_TIMEOUT.\nRecommendation: A because the check verifies a CI binding, so it belongs at the first CI run, not the first local run. The persona's stated order is local first, CI second. Principles: zero friction at T0, decide for me but let me override.\nCompleteness: A=10/10, B=7/10, C=3/10\nA) Move the binding check out of the local path: local evals and the demo return immediately; the check runs on first use in noninteractive CI mode, and locally as a non-blocking background status line (recommended) (human: ~3 days / CC: ~1 h)\n \u2705 Demo and first local result land at about T+0:30, inside the under-2-minute target\n \u2705 Check still runs where it matters, in CI, with the existing timeout and help messaging kept intact\n \u274c Changes a documented contract in the beta and needs the CI-mode path tested for the same failure states\nB) Keep the default wait, add an escape hatch: `EVALKIT_SKIP_CI_CHECK=1` or `--skip-ci-check`, honored by the demo and local runs (human: ~1 day / CC: ~20 min)\n \u2705 Existing contract preserved for anyone who relies on the gate by default\n \u2705 Developers who read the README can get the fast path in one env var\n \u274c Default TTHW stays 6 minutes; only developers who find the flag hit the target, and the docs must now teach the flag\nC) Retain the mandatory wait as documented; rely on the existing progress and timeout messaging (human: 0 / CC: 0)\n \u2705 No behavior change to ship or test in the beta\n \u2705 The wait is already well explained with URL, progress, and recovery help\n \u274c Target is missed by 3x and the demo can fail on a remote outage before any local value is shown\nNet: shipping the approved 2-minute target versus keeping a gate that guards CI but is paid at the local terminal.",
|
||||
"options": [
|
||||
{
|
||||
"label": "A: Check runs in CI, not locally (recommended)",
|
||||
"description": "Local evals and demo return at once; binding check moves to first CI-mode use plus a non-blocking local status line. Completeness 10/10."
|
||||
},
|
||||
{
|
||||
"label": "B: Keep wait, add skip flag",
|
||||
"description": "Default still waits 5 minutes; EVALKIT_SKIP_CI_CHECK / --skip-ci-check bypass for demo and local. Completeness 7/10."
|
||||
},
|
||||
{
|
||||
"label": "C: Keep mandatory wait",
|
||||
"description": "Ship the contract unchanged; TTHW stays 6 minutes. Completeness 3/10."
|
||||
}
|
||||
],
|
||||
"multiSelect": false
|
||||
}
|
||||
],
|
||||
"answered": true,
|
||||
"failed": false,
|
||||
"answers": {
|
||||
"D2 \u2014 First local evaluation blocks 5 minutes on a mandatory remote CI check\nProject/branch/task: EvalKit SDK beta (main), DX POLISH review, journey stage Hello World.\nELI10: docs/current-contracts.md lines 3-5: the first local evaluation requires a successful remote CI check and blocks for five minutes, with no skip flag and no offline path. The keyless demo inherits the same wait (README lines 17-23). Your persona wants a local score first and CI second; the SDK forces the order the other way round. docs/benchmarks.md measured 6 minutes, of which 5 is this wait. Peers land at 2 to 4 minutes and the approved target is under 2. The progress lines and timeout message already exist and are good; they do not shorten the wait.\nStakes if we pick wrong: The approved Champion target is unreachable while this gate stands. The magical moment lands at T+5:30 instead of T+0:30, and a remote CI outage turns the local demo into EVALKIT_CI_TIMEOUT.\nRecommendation: A because the check verifies a CI binding, so it belongs at the first CI run, not the first local run. The persona's stated order is local first, CI second. Principles: zero friction at T0, decide for me but let me override.\nCompleteness: A=10/10, B=7/10, C=3/10\nA) Move the binding check out of the local path: local evals and the demo return immediately; the check runs on first use in noninteractive CI mode, and locally as a non-blocking background status line (recommended) (human: ~3 days / CC: ~1 h)\n \u2705 Demo and first local result land at about T+0:30, inside the under-2-minute target\n \u2705 Check still runs where it matters, in CI, with the existing timeout and help messaging kept intact\n \u274c Changes a documented contract in the beta and needs the CI-mode path tested for the same failure states\nB) Keep the default wait, add an escape hatch: `EVALKIT_SKIP_CI_CHECK=1` or `--skip-ci-check`, honored by the demo and local runs (human: ~1 day / CC: ~20 min)\n \u2705 Existing contract preserved for anyone who relies on the gate by default\n \u2705 Developers who read the README can get the fast path in one env var\n \u274c Default TTHW stays 6 minutes; only developers who find the flag hit the target, and the docs must now teach the flag\nC) Retain the mandatory wait as documented; rely on the existing progress and timeout messaging (human: 0 / CC: 0)\n \u2705 No behavior change to ship or test in the beta\n \u2705 The wait is already well explained with URL, progress, and recovery help\n \u274c Target is missed by 3x and the demo can fail on a remote outage before any local value is shown\nNet: shipping the approved 2-minute target versus keeping a gate that guards CI but is paid at the local terminal.": "A: Check runs in CI, not locally (recommended)"
|
||||
},
|
||||
"unansweredQuestionIndices": [],
|
||||
"answeredAt": "2026-09-10T09:30:36.706Z"
|
||||
},
|
||||
{
|
||||
"sessionId": "6473b11e-784e-4c0b-9777-c7197c1c273f",
|
||||
"toolUseId": "toolu_017uV55w4iwLYU5oqwtQAexx",
|
||||
"questions": [
|
||||
{
|
||||
"header": "Signatures",
|
||||
"question": "D3 \u2014 run_eval and run_batch take the same two arguments in reversed positional order\nProject/branch/task: EvalKit SDK beta (main), DX POLISH review, journey stage Real Usage.\nELI10: docs/api.md lines 5-9: `run_eval(dataset, evaluator)` and `run_batch(evaluator, dataset)`. Same concepts, swapped order, and the draft calls the reversal intentional. Your ML engineer learns one function from the README, then calls the other with the same order. Because both arguments are plain Python objects, the call may raise a confusing error deep in the evaluator or, worse, iterate the evaluator as if it were the dataset. The good-API test is \"use it correctly after seeing one example\"; this surface fails that test by design.\nStakes if we pick wrong: Every developer who graduates from single evaluations to batches hits a swap bug once. In a beta, that is an issue filed per team and a permanent \"gotcha\" note in every internal wiki.\nRecommendation: A because consistent order plus keyword support is the pit of success, and a one-release swap detector keeps existing callers working while they move. Principle: decide for me, let me override; progressive disclosure.\nCompleteness: A=10/10, B=6/10, C=3/10\nA) Unify to `(dataset, evaluator)` in both functions, document keyword usage `run_batch(dataset=..., evaluator=...)`, and have `run_batch` detect the legacy reversed positional order for one release: emit a DeprecationWarning naming the new order, then proceed (recommended) (human: ~1 day / CC: ~20 min)\n \u2705 One order to learn; the README example transfers directly to batch calls\n \u2705 Existing 2.0.0b1 callers keep working through the beta with a warning that shows the exact fix\n \u274c Touches a public signature during the beta and needs a type-based detector plus tests for the swapped case\nB) Keep both orders as documented, add a runtime check that raises `TypeError` naming the expected order when the two arguments are swapped (human: ~half day / CC: ~10 min)\n \u2705 No signature change; the swap fails fast with a clear message instead of a deep stack trace\n \u2705 Small, contained change that only fires on the mistake\n \u274c The inconsistency stays forever, and every new developer still trips on it once\nC) Keep the reversed order, add a callout in docs/api.md and the README (human: ~15 min / CC: ~2 min)\n \u2705 Zero code change before the beta ships\n \u2705 Documents the trap for developers who read the reference\n \u274c Developers who copy from one example to the next do not read callouts; the swap bug ships\nNet: fix the API shape once now versus documenting a permanent trap.",
|
||||
"options": [
|
||||
{
|
||||
"label": "A: Unify order + warn on legacy (recommended)",
|
||||
"description": "Both take (dataset, evaluator); keywords documented; run_batch warns on detected reversed order for one release. Completeness 10/10."
|
||||
},
|
||||
{
|
||||
"label": "B: Keep orders, TypeError on swap",
|
||||
"description": "Signatures unchanged; a runtime check names the expected order when swapped. Completeness 6/10."
|
||||
},
|
||||
{
|
||||
"label": "C: Docs callout only",
|
||||
"description": "Keep the reversed order and warn in the docs. Completeness 3/10."
|
||||
}
|
||||
],
|
||||
"multiSelect": false
|
||||
}
|
||||
],
|
||||
"answered": true,
|
||||
"failed": false,
|
||||
"answers": {
|
||||
"D3 \u2014 run_eval and run_batch take the same two arguments in reversed positional order\nProject/branch/task: EvalKit SDK beta (main), DX POLISH review, journey stage Real Usage.\nELI10: docs/api.md lines 5-9: `run_eval(dataset, evaluator)` and `run_batch(evaluator, dataset)`. Same concepts, swapped order, and the draft calls the reversal intentional. Your ML engineer learns one function from the README, then calls the other with the same order. Because both arguments are plain Python objects, the call may raise a confusing error deep in the evaluator or, worse, iterate the evaluator as if it were the dataset. The good-API test is \"use it correctly after seeing one example\"; this surface fails that test by design.\nStakes if we pick wrong: Every developer who graduates from single evaluations to batches hits a swap bug once. In a beta, that is an issue filed per team and a permanent \"gotcha\" note in every internal wiki.\nRecommendation: A because consistent order plus keyword support is the pit of success, and a one-release swap detector keeps existing callers working while they move. Principle: decide for me, let me override; progressive disclosure.\nCompleteness: A=10/10, B=6/10, C=3/10\nA) Unify to `(dataset, evaluator)` in both functions, document keyword usage `run_batch(dataset=..., evaluator=...)`, and have `run_batch` detect the legacy reversed positional order for one release: emit a DeprecationWarning naming the new order, then proceed (recommended) (human: ~1 day / CC: ~20 min)\n \u2705 One order to learn; the README example transfers directly to batch calls\n \u2705 Existing 2.0.0b1 callers keep working through the beta with a warning that shows the exact fix\n \u274c Touches a public signature during the beta and needs a type-based detector plus tests for the swapped case\nB) Keep both orders as documented, add a runtime check that raises `TypeError` naming the expected order when the two arguments are swapped (human: ~half day / CC: ~10 min)\n \u2705 No signature change; the swap fails fast with a clear message instead of a deep stack trace\n \u2705 Small, contained change that only fires on the mistake\n \u274c The inconsistency stays forever, and every new developer still trips on it once\nC) Keep the reversed order, add a callout in docs/api.md and the README (human: ~15 min / CC: ~2 min)\n \u2705 Zero code change before the beta ships\n \u2705 Documents the trap for developers who read the reference\n \u274c Developers who copy from one example to the next do not read callouts; the swap bug ships\nNet: fix the API shape once now versus documenting a permanent trap.": "A: Unify order + warn on legacy (recommended)"
|
||||
},
|
||||
"unansweredQuestionIndices": [],
|
||||
"answeredAt": "2026-09-10T09:31:16.344Z"
|
||||
},
|
||||
{
|
||||
"sessionId": "6473b11e-784e-4c0b-9777-c7197c1c273f",
|
||||
"toolUseId": "toolu_01R9YdSFG3sFWNsyJgHzgDfW",
|
||||
"questions": [
|
||||
{
|
||||
"header": "Auth error",
|
||||
"question": "D4 \u2014 Invalid API key raises AuthError(\"request failed\") with no cause or fix\nProject/branch/task: EvalKit SDK beta (main), DX POLISH review, journey stage Debug.\nELI10: docs/api.md lines 11-13: an invalid key raises `AuthError(\"request failed\")` with no error code, no explanation, and no instruction for replacing the key. Every other EvalKit error already names the cause, the argument or file, and an actionable fix (current-contracts.md lines 21-24), so this is the one outlier. Your ML engineer pastes the key wrong at about T+10:00 after the demo, reads \"request failed\", and starts debugging the network instead of the key. The CI timeout message (code, URL, retry instruction, help link) is the house style to copy.\nStakes if we pick wrong: The first live-evaluation failure most developers hit is a bad key, and the SDK points them nowhere. That is ten to twenty minutes lost per developer and a support ticket per team.\nRecommendation: A because the SDK already has the pattern; one error class should not be the exception to it. Principle: fight uncertainty, every error is problem plus cause plus fix.\nCompleteness: A=10/10, B=6/10, C=2/10\nA) Structured auth errors matching the CI-timeout pattern: code `EVALKIT_AUTH_INVALID_KEY` (plus `EVALKIT_AUTH_MISSING_KEY` and `EVALKIT_AUTH_KEY_REVOKED`), message naming the redacted key suffix and project, the fix (create or rotate at the console key page, export EVALKIT_API_KEY), and a help link (recommended) (human: ~1 day / CC: ~15 min)\n \u2705 Developer sees the problem, cause, and exact fix in the traceback, no docs detour\n \u2705 Distinguishes missing, invalid, and revoked keys so rotation cases self-diagnose\n \u274c Needs the server to return a distinguishable auth reason, or a client-side fallback when it does not\nB) Single improved message: `AuthError(\"Invalid API key. Set EVALKIT_API_KEY from https://console.evalkit.example/settings/api-keys\")`, no code, no case split (human: ~1 h / CC: ~5 min)\n \u2705 Names the key as the cause and points at the console page\n \u2705 Pure message change, no server contract needed\n \u274c Missing versus revoked versus wrong-project all read the same, and no code to grep or match in CI logs\nC) Keep `AuthError(\"request failed\")` as documented (human: 0 / CC: 0)\n \u2705 No change to ship before the beta\n \u2705 Consistent with the current published behavior\n \u274c The most common live-eval failure stays undiagnosable from the error text\nNet: bring the one outlier error up to the SDK's own standard versus shipping a known dead end.",
|
||||
"options": [
|
||||
{
|
||||
"label": "A: Structured auth errors (recommended)",
|
||||
"description": "Codes for invalid, missing, revoked; redacted key suffix, console fix, help link. Matches EVALKIT_CI_TIMEOUT style. Completeness 10/10."
|
||||
},
|
||||
{
|
||||
"label": "B: One better message",
|
||||
"description": "Name the key and the console URL in a single message, no code or case split. Completeness 6/10."
|
||||
},
|
||||
{
|
||||
"label": "C: Keep as documented",
|
||||
"description": "Ship AuthError(\"request failed\") unchanged. Completeness 2/10."
|
||||
}
|
||||
],
|
||||
"multiSelect": false
|
||||
}
|
||||
],
|
||||
"answered": true,
|
||||
"failed": false,
|
||||
"answers": {
|
||||
"D4 \u2014 Invalid API key raises AuthError(\"request failed\") with no cause or fix\nProject/branch/task: EvalKit SDK beta (main), DX POLISH review, journey stage Debug.\nELI10: docs/api.md lines 11-13: an invalid key raises `AuthError(\"request failed\")` with no error code, no explanation, and no instruction for replacing the key. Every other EvalKit error already names the cause, the argument or file, and an actionable fix (current-contracts.md lines 21-24), so this is the one outlier. Your ML engineer pastes the key wrong at about T+10:00 after the demo, reads \"request failed\", and starts debugging the network instead of the key. The CI timeout message (code, URL, retry instruction, help link) is the house style to copy.\nStakes if we pick wrong: The first live-evaluation failure most developers hit is a bad key, and the SDK points them nowhere. That is ten to twenty minutes lost per developer and a support ticket per team.\nRecommendation: A because the SDK already has the pattern; one error class should not be the exception to it. Principle: fight uncertainty, every error is problem plus cause plus fix.\nCompleteness: A=10/10, B=6/10, C=2/10\nA) Structured auth errors matching the CI-timeout pattern: code `EVALKIT_AUTH_INVALID_KEY` (plus `EVALKIT_AUTH_MISSING_KEY` and `EVALKIT_AUTH_KEY_REVOKED`), message naming the redacted key suffix and project, the fix (create or rotate at the console key page, export EVALKIT_API_KEY), and a help link (recommended) (human: ~1 day / CC: ~15 min)\n \u2705 Developer sees the problem, cause, and exact fix in the traceback, no docs detour\n \u2705 Distinguishes missing, invalid, and revoked keys so rotation cases self-diagnose\n \u274c Needs the server to return a distinguishable auth reason, or a client-side fallback when it does not\nB) Single improved message: `AuthError(\"Invalid API key. Set EVALKIT_API_KEY from https://console.evalkit.example/settings/api-keys\")`, no code, no case split (human: ~1 h / CC: ~5 min)\n \u2705 Names the key as the cause and points at the console page\n \u2705 Pure message change, no server contract needed\n \u274c Missing versus revoked versus wrong-project all read the same, and no code to grep or match in CI logs\nC) Keep `AuthError(\"request failed\")` as documented (human: 0 / CC: 0)\n \u2705 No change to ship before the beta\n \u2705 Consistent with the current published behavior\n \u274c The most common live-eval failure stays undiagnosable from the error text\nNet: bring the one outlier error up to the SDK's own standard versus shipping a known dead end.": "A: Structured auth errors (recommended)"
|
||||
},
|
||||
"unansweredQuestionIndices": [],
|
||||
"answeredAt": "2026-09-10T09:31:53.981Z"
|
||||
},
|
||||
{
|
||||
"sessionId": "6473b11e-784e-4c0b-9777-c7197c1c273f",
|
||||
"toolUseId": "toolu_01AGy5jB21K3mXYjUKwrEFZW",
|
||||
"questions": [
|
||||
{
|
||||
"header": "v1 to v2",
|
||||
"question": "D5 \u2014 v2 removes Client.evaluate() immediately with no alias, warning, guide, or codemod\nProject/branch/task: EvalKit SDK beta (main), DX POLISH review, journey stage Upgrade.\nELI10: docs/api.md lines 15-18: v1 exposes `Client.evaluate()`, v2 replaces it with `Client.run()` and drops the old name at once. No compatibility alias, no DeprecationWarning, no migration guide, no codemod. Your ML engineer upgrades an existing v1 project to try the beta, every `client.evaluate()` call raises `AttributeError`, and nothing in the traceback says the method was renamed. The rest of the changelog is complete, so this one rename is the only upgrade hazard.\nStakes if we pick wrong: Upgrade fear. Teams with v1 in production will not trial the beta if the first import breaks, and the beta feedback survey only hears from greenfield users.\nRecommendation: A because a deprecated alias costs a few lines and turns a hard break into a warning that names the fix, and the guide plus one-liner make the migration boring. Principle: credible, upgrades should be boring.\nCompleteness: A=10/10, B=7/10, C=5/10\nA) Keep `Client.evaluate()` as a deprecated alias for `Client.run()` through 2.x with a DeprecationWarning that names the replacement and removal version (3.0); add a \"Upgrading from 1.x\" section to the changelog and docs; ship a documented one-line rename (`python -m evalkit.migrate` or an equivalent sed/ruff command) (recommended) (human: ~1 day / CC: ~20 min)\n \u2705 v1 projects run on 2.0.0b1 unchanged, so production teams can trial the beta safely\n \u2705 The warning tells the developer exactly what to rename and by when; the one-liner does it for them\n \u274c Carries a shim through the 2.x line and one more thing to remove in 3.0\nB) Deprecated alias plus warning only, no migration guide or codemod (human: ~2 h / CC: ~10 min)\n \u2705 Upgrade no longer breaks, and the warning names Client.run()\n \u2705 Minimal surface, easy to delete later\n \u274c Nothing in the docs explains the v1 to v2 change for someone reading before they upgrade\nC) Remove immediately as documented, but add an \"Upgrading from 1.x\" changelog section with the rename (human: ~30 min / CC: ~3 min)\n \u2705 Cleanest v2 API with no shim to maintain\n \u2705 The rename is at least documented for developers who read the changelog first\n \u274c Every v1 caller still breaks at import time in the beta, and the AttributeError does not point at the changelog\nNet: a boring upgrade with a shim to retire later versus a clean API that breaks every v1 user on day one.",
|
||||
"options": [
|
||||
{
|
||||
"label": "A: Alias + warning + guide + codemod (recommended)",
|
||||
"description": "Client.evaluate() stays as a deprecated alias through 2.x; migration guide and one-line rename shipped. Completeness 10/10."
|
||||
},
|
||||
{
|
||||
"label": "B: Alias + warning only",
|
||||
"description": "Keep the old name with a DeprecationWarning; no guide or codemod. Completeness 7/10."
|
||||
},
|
||||
{
|
||||
"label": "C: Remove now, document in changelog",
|
||||
"description": "Hard removal as drafted plus an Upgrading section. Completeness 5/10."
|
||||
}
|
||||
],
|
||||
"multiSelect": false
|
||||
}
|
||||
],
|
||||
"answered": true,
|
||||
"failed": false,
|
||||
"answers": {
|
||||
"D5 \u2014 v2 removes Client.evaluate() immediately with no alias, warning, guide, or codemod\nProject/branch/task: EvalKit SDK beta (main), DX POLISH review, journey stage Upgrade.\nELI10: docs/api.md lines 15-18: v1 exposes `Client.evaluate()`, v2 replaces it with `Client.run()` and drops the old name at once. No compatibility alias, no DeprecationWarning, no migration guide, no codemod. Your ML engineer upgrades an existing v1 project to try the beta, every `client.evaluate()` call raises `AttributeError`, and nothing in the traceback says the method was renamed. The rest of the changelog is complete, so this one rename is the only upgrade hazard.\nStakes if we pick wrong: Upgrade fear. Teams with v1 in production will not trial the beta if the first import breaks, and the beta feedback survey only hears from greenfield users.\nRecommendation: A because a deprecated alias costs a few lines and turns a hard break into a warning that names the fix, and the guide plus one-liner make the migration boring. Principle: credible, upgrades should be boring.\nCompleteness: A=10/10, B=7/10, C=5/10\nA) Keep `Client.evaluate()` as a deprecated alias for `Client.run()` through 2.x with a DeprecationWarning that names the replacement and removal version (3.0); add a \"Upgrading from 1.x\" section to the changelog and docs; ship a documented one-line rename (`python -m evalkit.migrate` or an equivalent sed/ruff command) (recommended) (human: ~1 day / CC: ~20 min)\n \u2705 v1 projects run on 2.0.0b1 unchanged, so production teams can trial the beta safely\n \u2705 The warning tells the developer exactly what to rename and by when; the one-liner does it for them\n \u274c Carries a shim through the 2.x line and one more thing to remove in 3.0\nB) Deprecated alias plus warning only, no migration guide or codemod (human: ~2 h / CC: ~10 min)\n \u2705 Upgrade no longer breaks, and the warning names Client.run()\n \u2705 Minimal surface, easy to delete later\n \u274c Nothing in the docs explains the v1 to v2 change for someone reading before they upgrade\nC) Remove immediately as documented, but add an \"Upgrading from 1.x\" changelog section with the rename (human: ~30 min / CC: ~3 min)\n \u2705 Cleanest v2 API with no shim to maintain\n \u2705 The rename is at least documented for developers who read the changelog first\n \u274c Every v1 caller still breaks at import time in the beta, and the AttributeError does not point at the changelog\nNet: a boring upgrade with a shim to retire later versus a clean API that breaks every v1 user on day one.": "A: Alias + warning + guide + codemod (recommended)"
|
||||
},
|
||||
"unansweredQuestionIndices": [],
|
||||
"answeredAt": "2026-09-10T09:32:29.623Z"
|
||||
}
|
||||
]
|
||||
}
|
||||
+110
@@ -0,0 +1,110 @@
|
||||
{
|
||||
"sourceHead": "fd7698b23d0d88dfcfe8a2b0fd4c3140cdad01fa",
|
||||
"sourceObservationSha256": "ad9228ccf39d9e3c61af56494f9a21fdc3a00d205a5021e33031e57d4b4f025d",
|
||||
"calls": [
|
||||
{
|
||||
"signature": "26447393-d025-40f1-a80a-e6a34fcddf1a:toolu_01NjPQmJ7CTTEgsvqYRCT3Bs",
|
||||
"promptSnippet": "D1 scope D1 \u2014 Reduce the class inventory before building? Project/branch/task: main \u2014 Multi-tenant Auth Refactor plan review, Step 0 scope challenge. ELI10: The plan adds five new classes (AuthBroker, SessionMint, AuthCache, TokenStore, Req",
|
||||
"options": [
|
||||
{
|
||||
"index": 1,
|
||||
"label": "A) Reduce to 2-3 classes (recommended)"
|
||||
},
|
||||
{
|
||||
"index": 2,
|
||||
"label": "B) Proceed with 5 classes"
|
||||
},
|
||||
{
|
||||
"index": 3,
|
||||
"label": "C) Hold \u2014 state the goal first"
|
||||
}
|
||||
],
|
||||
"observedAtMs": 610937,
|
||||
"preReview": true,
|
||||
"nativeCall": {
|
||||
"sessionId": "26447393-d025-40f1-a80a-e6a34fcddf1a",
|
||||
"toolUseId": "toolu_01NjPQmJ7CTTEgsvqYRCT3Bs",
|
||||
"questions": [
|
||||
{
|
||||
"question": "D1 \u2014 Reduce the class inventory before building?\nProject/branch/task: main \u2014 Multi-tenant Auth Refactor plan review, Step 0 scope challenge.\nELI10: The plan adds five new classes (AuthBroker, SessionMint, AuthCache, TokenStore, RequestPolicy), and three of them are ways of holding the same cached tokens the existing adapter already holds. Every extra class is a place for bugs to hide and a thing the next engineer must learn. The question is whether the two real services can use the existing cache adapter directly through a narrow interface.\nStakes if we pick wrong: over-reduce and you re-add a class mid-build; under-reduce and you maintain three caches and 12 files for a change whose goal is not yet written down.\nRecommendation: A because the AuthCache facade adds no rule or serialization (PLAN.md:11-13) and TokenStore has no stated responsibility.\nNote: options differ in kind, not coverage \u2014 no completeness score.\nNet: a possible re-add later versus three overlapping abstractions now.",
|
||||
"header": "D1 scope",
|
||||
"multiSelect": false,
|
||||
"options": [
|
||||
{
|
||||
"label": "A) Reduce to 2-3 classes (recommended)",
|
||||
"description": "\u2705 Same service-facing boundary with zero new runtime code to test or debug (human: saves ~2 days / CC: saves ~20min). \u2705 Removes the double/triple-caching memory and consistency question entirely. \u274c If TokenStore had a real hidden purpose it returns later as an unplanned change."
|
||||
},
|
||||
{
|
||||
"label": "B) Proceed with 5 classes",
|
||||
"description": "\u2705 No re-planning; keeps whatever design intent the author had for TokenStore and RequestPolicy. \u2705 Forces a one-sentence responsibility per class into the plan, useful either way. \u274c 12 files and three cache-shaped types before the goal of the refactor is stated."
|
||||
},
|
||||
{
|
||||
"label": "C) Hold \u2014 state the goal first",
|
||||
"description": "\u2705 The plan has no problem statement; writing it may settle D1 on its own. \u2705 Cheapest possible step before any code (human: ~30min / CC: ~5min). \u274c Delays every other decision until the goal is written."
|
||||
}
|
||||
]
|
||||
}
|
||||
],
|
||||
"answered": true,
|
||||
"failed": false,
|
||||
"answers": {
|
||||
"D1 \u2014 Reduce the class inventory before building?\nProject/branch/task: main \u2014 Multi-tenant Auth Refactor plan review, Step 0 scope challenge.\nELI10: The plan adds five new classes (AuthBroker, SessionMint, AuthCache, TokenStore, RequestPolicy), and three of them are ways of holding the same cached tokens the existing adapter already holds. Every extra class is a place for bugs to hide and a thing the next engineer must learn. The question is whether the two real services can use the existing cache adapter directly through a narrow interface.\nStakes if we pick wrong: over-reduce and you re-add a class mid-build; under-reduce and you maintain three caches and 12 files for a change whose goal is not yet written down.\nRecommendation: A because the AuthCache facade adds no rule or serialization (PLAN.md:11-13) and TokenStore has no stated responsibility.\nNote: options differ in kind, not coverage \u2014 no completeness score.\nNet: a possible re-add later versus three overlapping abstractions now.": "A) Reduce to 2-3 classes (recommended)"
|
||||
},
|
||||
"unansweredQuestionIndices": [],
|
||||
"answeredAt": "2026-09-10T09:44:08.579Z"
|
||||
}
|
||||
},
|
||||
{
|
||||
"signature": "26447393-d025-40f1-a80a-e6a34fcddf1a:toolu_01Qs552AuhXqYDwihpYWqTaG",
|
||||
"promptSnippet": "D2 DI D2 \u2014 Module-level AuthCache singleton \u2192 constructor injection with a single writer? Project/branch/task: main \u2014 Multi-tenant Auth Refactor, Architecture finding A1 (PLAN.md:19-20: \"share a global mutable AuthCache instance via module-",
|
||||
"options": [
|
||||
{
|
||||
"index": 1,
|
||||
"label": "A) Composition-root injection, single writer (recommended)"
|
||||
},
|
||||
{
|
||||
"index": 2,
|
||||
"label": "B) Keep module export, add serializing proxy"
|
||||
},
|
||||
{
|
||||
"index": 3,
|
||||
"label": "C) Do nothing"
|
||||
}
|
||||
],
|
||||
"observedAtMs": 642080,
|
||||
"preReview": true,
|
||||
"nativeCall": {
|
||||
"sessionId": "26447393-d025-40f1-a80a-e6a34fcddf1a",
|
||||
"toolUseId": "toolu_01Qs552AuhXqYDwihpYWqTaG",
|
||||
"questions": [
|
||||
{
|
||||
"question": "D2 \u2014 Module-level AuthCache singleton \u2192 constructor injection with a single writer?\nProject/branch/task: main \u2014 Multi-tenant Auth Refactor, Architecture finding A1 (PLAN.md:19-20: \"share a global mutable AuthCache instance via module-level export. Both services mutate it\").\nELI10: Right now both services grab the same global cache object from a module import and both write to it. Nobody can tell who owns a write, and tests share state through that global. Building the cache once at app startup and handing it to each service makes the dependency visible and gives every test its own instance. With D1 answered, the injected thing is the existing adapter behind a narrow interface.\nStakes if we pick wrong: flaky or order-dependent tests, and a production bundle with two \"singletons\" where an invalidation clears the wrong one.\nRecommendation: A because it is the standard Layer 1 fix (composition root + DI) and costs minutes with CC; it maps to your explicit-over-clever preference.\nCompleteness: A=9/10, B=6/10, C=2/10\nNet: explicit dependencies now versus hidden coupling you debug later.",
|
||||
"header": "D2 DI",
|
||||
"multiSelect": false,
|
||||
"options": [
|
||||
{
|
||||
"label": "A) Composition-root injection, single writer (recommended)",
|
||||
"description": "\u2705 Test files construct their own fake cache; no cross-file state, parallel workers safe (human: ~1 day / CC: ~15min). \u2705 SessionMint is the only session writer and AuthBroker gets a read-only port, enforced by types not convention. \u274c Touches the bootstrap file and both service constructors."
|
||||
},
|
||||
{
|
||||
"label": "B) Keep module export, add serializing proxy",
|
||||
"description": "\u2705 Smaller diff; interleaved writes on one key get serialized (human: ~0.5 day / CC: ~10min). \u2705 No bootstrap changes. \u274c Still a hidden global; test isolation and duplicate-bundle problems remain."
|
||||
},
|
||||
{
|
||||
"label": "C) Do nothing",
|
||||
"description": "\u2705 Zero effort. \u2705 Matches the plan as written. \u274c Both A1 failure scenarios stay live and the plan's own test coverage cannot isolate state."
|
||||
}
|
||||
]
|
||||
}
|
||||
],
|
||||
"answered": true,
|
||||
"failed": false,
|
||||
"answers": {
|
||||
"D2 \u2014 Module-level AuthCache singleton \u2192 constructor injection with a single writer?\nProject/branch/task: main \u2014 Multi-tenant Auth Refactor, Architecture finding A1 (PLAN.md:19-20: \"share a global mutable AuthCache instance via module-level export. Both services mutate it\").\nELI10: Right now both services grab the same global cache object from a module import and both write to it. Nobody can tell who owns a write, and tests share state through that global. Building the cache once at app startup and handing it to each service makes the dependency visible and gives every test its own instance. With D1 answered, the injected thing is the existing adapter behind a narrow interface.\nStakes if we pick wrong: flaky or order-dependent tests, and a production bundle with two \"singletons\" where an invalidation clears the wrong one.\nRecommendation: A because it is the standard Layer 1 fix (composition root + DI) and costs minutes with CC; it maps to your explicit-over-clever preference.\nCompleteness: A=9/10, B=6/10, C=2/10\nNet: explicit dependencies now versus hidden coupling you debug later.": "A) Composition-root injection, single writer (recommended)"
|
||||
},
|
||||
"unansweredQuestionIndices": [],
|
||||
"answeredAt": "2026-09-10T09:44:39.717Z"
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
+138
@@ -0,0 +1,138 @@
|
||||
{
|
||||
"observationSha256": "1fbba18c11a5b46ef64cb72e39f8b74f3b9b7953274fe58eed2a93f0dd594135",
|
||||
"reportSha256": "370461dbf4820f9811843682539845b8043cb8d72b610868473241a02577283c",
|
||||
"calls": [
|
||||
{
|
||||
"sessionId": "26447393-d025-40f1-a80a-e6a34fcddf1a",
|
||||
"toolUseId": "toolu_01NjPQmJ7CTTEgsvqYRCT3Bs",
|
||||
"questions": [
|
||||
{
|
||||
"question": "D1 \u2014 Reduce the class inventory before building?\nProject/branch/task: main \u2014 Multi-tenant Auth Refactor plan review, Step 0 scope challenge.\nELI10: The plan adds five new classes (AuthBroker, SessionMint, AuthCache, TokenStore, RequestPolicy), and three of them are ways of holding the same cached tokens the existing adapter already holds. Every extra class is a place for bugs to hide and a thing the next engineer must learn. The question is whether the two real services can use the existing cache adapter directly through a narrow interface.\nStakes if we pick wrong: over-reduce and you re-add a class mid-build; under-reduce and you maintain three caches and 12 files for a change whose goal is not yet written down.\nRecommendation: A because the AuthCache facade adds no rule or serialization (PLAN.md:11-13) and TokenStore has no stated responsibility.\nNote: options differ in kind, not coverage \u2014 no completeness score.\nNet: a possible re-add later versus three overlapping abstractions now.",
|
||||
"header": "D1 scope",
|
||||
"multiSelect": false,
|
||||
"options": [
|
||||
{
|
||||
"label": "A) Reduce to 2-3 classes (recommended)",
|
||||
"description": "\u2705 Same service-facing boundary with zero new runtime code to test or debug (human: saves ~2 days / CC: saves ~20min). \u2705 Removes the double/triple-caching memory and consistency question entirely. \u274c If TokenStore had a real hidden purpose it returns later as an unplanned change."
|
||||
},
|
||||
{
|
||||
"label": "B) Proceed with 5 classes",
|
||||
"description": "\u2705 No re-planning; keeps whatever design intent the author had for TokenStore and RequestPolicy. \u2705 Forces a one-sentence responsibility per class into the plan, useful either way. \u274c 12 files and three cache-shaped types before the goal of the refactor is stated."
|
||||
},
|
||||
{
|
||||
"label": "C) Hold \u2014 state the goal first",
|
||||
"description": "\u2705 The plan has no problem statement; writing it may settle D1 on its own. \u2705 Cheapest possible step before any code (human: ~30min / CC: ~5min). \u274c Delays every other decision until the goal is written."
|
||||
}
|
||||
]
|
||||
}
|
||||
],
|
||||
"answered": true,
|
||||
"failed": false,
|
||||
"answers": {
|
||||
"D1 \u2014 Reduce the class inventory before building?\nProject/branch/task: main \u2014 Multi-tenant Auth Refactor plan review, Step 0 scope challenge.\nELI10: The plan adds five new classes (AuthBroker, SessionMint, AuthCache, TokenStore, RequestPolicy), and three of them are ways of holding the same cached tokens the existing adapter already holds. Every extra class is a place for bugs to hide and a thing the next engineer must learn. The question is whether the two real services can use the existing cache adapter directly through a narrow interface.\nStakes if we pick wrong: over-reduce and you re-add a class mid-build; under-reduce and you maintain three caches and 12 files for a change whose goal is not yet written down.\nRecommendation: A because the AuthCache facade adds no rule or serialization (PLAN.md:11-13) and TokenStore has no stated responsibility.\nNote: options differ in kind, not coverage \u2014 no completeness score.\nNet: a possible re-add later versus three overlapping abstractions now.": "A) Reduce to 2-3 classes (recommended)"
|
||||
},
|
||||
"unansweredQuestionIndices": [],
|
||||
"answeredAt": "2026-09-10T09:44:08.579Z"
|
||||
},
|
||||
{
|
||||
"sessionId": "26447393-d025-40f1-a80a-e6a34fcddf1a",
|
||||
"toolUseId": "toolu_01Qs552AuhXqYDwihpYWqTaG",
|
||||
"questions": [
|
||||
{
|
||||
"question": "D2 \u2014 Module-level AuthCache singleton \u2192 constructor injection with a single writer?\nProject/branch/task: main \u2014 Multi-tenant Auth Refactor, Architecture finding A1 (PLAN.md:19-20: \"share a global mutable AuthCache instance via module-level export. Both services mutate it\").\nELI10: Right now both services grab the same global cache object from a module import and both write to it. Nobody can tell who owns a write, and tests share state through that global. Building the cache once at app startup and handing it to each service makes the dependency visible and gives every test its own instance. With D1 answered, the injected thing is the existing adapter behind a narrow interface.\nStakes if we pick wrong: flaky or order-dependent tests, and a production bundle with two \"singletons\" where an invalidation clears the wrong one.\nRecommendation: A because it is the standard Layer 1 fix (composition root + DI) and costs minutes with CC; it maps to your explicit-over-clever preference.\nCompleteness: A=9/10, B=6/10, C=2/10\nNet: explicit dependencies now versus hidden coupling you debug later.",
|
||||
"header": "D2 DI",
|
||||
"multiSelect": false,
|
||||
"options": [
|
||||
{
|
||||
"label": "A) Composition-root injection, single writer (recommended)",
|
||||
"description": "\u2705 Test files construct their own fake cache; no cross-file state, parallel workers safe (human: ~1 day / CC: ~15min). \u2705 SessionMint is the only session writer and AuthBroker gets a read-only port, enforced by types not convention. \u274c Touches the bootstrap file and both service constructors."
|
||||
},
|
||||
{
|
||||
"label": "B) Keep module export, add serializing proxy",
|
||||
"description": "\u2705 Smaller diff; interleaved writes on one key get serialized (human: ~0.5 day / CC: ~10min). \u2705 No bootstrap changes. \u274c Still a hidden global; test isolation and duplicate-bundle problems remain."
|
||||
},
|
||||
{
|
||||
"label": "C) Do nothing",
|
||||
"description": "\u2705 Zero effort. \u2705 Matches the plan as written. \u274c Both A1 failure scenarios stay live and the plan's own test coverage cannot isolate state."
|
||||
}
|
||||
]
|
||||
}
|
||||
],
|
||||
"answered": true,
|
||||
"failed": false,
|
||||
"answers": {
|
||||
"D2 \u2014 Module-level AuthCache singleton \u2192 constructor injection with a single writer?\nProject/branch/task: main \u2014 Multi-tenant Auth Refactor, Architecture finding A1 (PLAN.md:19-20: \"share a global mutable AuthCache instance via module-level export. Both services mutate it\").\nELI10: Right now both services grab the same global cache object from a module import and both write to it. Nobody can tell who owns a write, and tests share state through that global. Building the cache once at app startup and handing it to each service makes the dependency visible and gives every test its own instance. With D1 answered, the injected thing is the existing adapter behind a narrow interface.\nStakes if we pick wrong: flaky or order-dependent tests, and a production bundle with two \"singletons\" where an invalidation clears the wrong one.\nRecommendation: A because it is the standard Layer 1 fix (composition root + DI) and costs minutes with CC; it maps to your explicit-over-clever preference.\nCompleteness: A=9/10, B=6/10, C=2/10\nNet: explicit dependencies now versus hidden coupling you debug later.": "A) Composition-root injection, single writer (recommended)"
|
||||
},
|
||||
"unansweredQuestionIndices": [],
|
||||
"answeredAt": "2026-09-10T09:44:39.717Z"
|
||||
},
|
||||
{
|
||||
"sessionId": "26447393-d025-40f1-a80a-e6a34fcddf1a",
|
||||
"toolUseId": "toolu_01BohFzt1fQdwGkAf2KTWQfm",
|
||||
"questions": [
|
||||
{
|
||||
"question": "D5 \u2014 Split validateAndDispatch() and stop swallowing errors?\nProject/branch/task: main \u2014 Multi-tenant Auth Refactor, Code quality finding C1 (PLAN.md:23-24: \"60 lines with three nested try/catch blocks; each catch swallows a different error class\").\nELI10: The function catches three kinds of errors and quietly keeps going. In an auth path, a swallowed error is an authorization decision made by accident: an IDP 503 can fall through to dispatch with partial claims. Splitting validation from dispatch and returning typed errors makes every outcome named, logged, and testable.\nStakes if we pick wrong: requests proceed on partial claims, or fail with no log line. This is the second critical gap.\nRecommendation: A because explicit over clever is your stated preference and each branch gets its own test; B logs a bug you keep.\nCompleteness: A=9/10, B=5/10, C=1/10\nNet: a real refactor with tests versus logging a bug you keep.",
|
||||
"header": "D5 errors",
|
||||
"multiSelect": false,
|
||||
"options": [
|
||||
{
|
||||
"label": "A) Split + typed AuthError taxonomy + rethrow unknown (recommended)",
|
||||
"description": "\u2705 Every error path becomes a named, tested outcome (IdpUnavailable, TokenInvalid, PolicyDenied, Unknown) (human: ~1 day / CC: ~20min). \u2705 60 lines with 3 nested try/catch become two ~20-line functions with one boundary try. \u274c Callers that relied on silent continuation will surface; that is the point, but it is work."
|
||||
},
|
||||
{
|
||||
"label": "B) Keep structure, add a log line per catch",
|
||||
"description": "\u2705 Trivial diff (human: ~1h / CC: ~3min). \u2705 Failures become visible in logs. \u274c Behavior is still wrong and still untestable per branch."
|
||||
},
|
||||
{
|
||||
"label": "C) Leave as-is",
|
||||
"description": "\u2705 Zero effort. \u2705 No behavior change for existing callers. \u274c Critical gap stays open: silent allow on IDP failure."
|
||||
}
|
||||
]
|
||||
}
|
||||
],
|
||||
"answered": true,
|
||||
"failed": false,
|
||||
"answers": {
|
||||
"D5 \u2014 Split validateAndDispatch() and stop swallowing errors?\nProject/branch/task: main \u2014 Multi-tenant Auth Refactor, Code quality finding C1 (PLAN.md:23-24: \"60 lines with three nested try/catch blocks; each catch swallows a different error class\").\nELI10: The function catches three kinds of errors and quietly keeps going. In an auth path, a swallowed error is an authorization decision made by accident: an IDP 503 can fall through to dispatch with partial claims. Splitting validation from dispatch and returning typed errors makes every outcome named, logged, and testable.\nStakes if we pick wrong: requests proceed on partial claims, or fail with no log line. This is the second critical gap.\nRecommendation: A because explicit over clever is your stated preference and each branch gets its own test; B logs a bug you keep.\nCompleteness: A=9/10, B=5/10, C=1/10\nNet: a real refactor with tests versus logging a bug you keep.": "A) Split + typed AuthError taxonomy + rethrow unknown (recommended)"
|
||||
},
|
||||
"unansweredQuestionIndices": [],
|
||||
"answeredAt": "2026-09-10T09:45:43.007Z"
|
||||
},
|
||||
{
|
||||
"sessionId": "26447393-d025-40f1-a80a-e6a34fcddf1a",
|
||||
"toolUseId": "toolu_016459fhN82mqXuCRYoktSvN",
|
||||
"questions": [
|
||||
{
|
||||
"question": "D9 \u2014 Parallelize IDP calls with per-call timeouts, shared abort, and JWKS/discovery caching?\nProject/branch/task: main \u2014 Multi-tenant Auth Refactor, Performance finding P1 (PLAN.md:31-32: \"5 sequential API calls to the IDP; they could be parallelized via Promise.all trivially\").\nELI10: Running the five calls at once is faster, but Promise.all on its own still hangs the whole validation if one call hangs, and a failure leaves the other four in flight burning IDP quota. Two of the five (discovery document, JWKS) almost never change and should be cached, so the real win is making fewer calls, not just faster ones.\nStakes if we pick wrong: validation hangs on one slow IDP call, or a cold cache after deploy hits IDP rate limits with 5\u00d7 the calls.\nRecommendation: A because \"trivially Promise.all\" is the incomplete version; timeouts and caching are minutes with CC and remove two failure modes.\nCompleteness: A=9/10, B=6/10, C=2/10\nNet: a cache with one edge case (kid miss) versus unbounded latency and IDP quota burn.",
|
||||
"header": "D9 perf",
|
||||
"multiSelect": false,
|
||||
"options": [
|
||||
{
|
||||
"label": "A) Promise.all + per-call timeout + shared abort + JWKS/discovery cache (recommended)",
|
||||
"description": "\u2705 Cold validation drops from 5 calls to 2-3; warm cache hit is 0 IDP calls (human: ~1 day / CC: ~15min). \u2705 Worst-case latency is bounded; a failure aborts siblings instead of leaking them. \u274c JWKS cache needs kid-miss refresh logic and its own test (R5 covers it)."
|
||||
},
|
||||
{
|
||||
"label": "B) Promise.all only, as the plan says",
|
||||
"description": "\u2705 Immediate latency win with a minimal diff (human: ~1h / CC: ~3min). \u2705 No new cache to reason about. \u274c One hung call still hangs validation; 5\u00d7 IDP load on every cold validation."
|
||||
},
|
||||
{
|
||||
"label": "C) Keep sequential",
|
||||
"description": "\u2705 No change; lowest IDP concurrency. \u2705 Simplest failure semantics. \u274c 5\u00d7 serial latency on every cold validation."
|
||||
}
|
||||
]
|
||||
}
|
||||
],
|
||||
"answered": true,
|
||||
"failed": false,
|
||||
"answers": {
|
||||
"D9 \u2014 Parallelize IDP calls with per-call timeouts, shared abort, and JWKS/discovery caching?\nProject/branch/task: main \u2014 Multi-tenant Auth Refactor, Performance finding P1 (PLAN.md:31-32: \"5 sequential API calls to the IDP; they could be parallelized via Promise.all trivially\").\nELI10: Running the five calls at once is faster, but Promise.all on its own still hangs the whole validation if one call hangs, and a failure leaves the other four in flight burning IDP quota. Two of the five (discovery document, JWKS) almost never change and should be cached, so the real win is making fewer calls, not just faster ones.\nStakes if we pick wrong: validation hangs on one slow IDP call, or a cold cache after deploy hits IDP rate limits with 5\u00d7 the calls.\nRecommendation: A because \"trivially Promise.all\" is the incomplete version; timeouts and caching are minutes with CC and remove two failure modes.\nCompleteness: A=9/10, B=6/10, C=2/10\nNet: a cache with one edge case (kid miss) versus unbounded latency and IDP quota burn.": "A) Promise.all + per-call timeout + shared abort + JWKS/discovery cache (recommended)"
|
||||
},
|
||||
"unansweredQuestionIndices": [],
|
||||
"answeredAt": "2026-09-10T09:47:14.009Z"
|
||||
}
|
||||
],
|
||||
"declaration": "### REGRESSION (mandatory, authorized by the coverage-audit regression rule \u2014 no question asked)\n\n`legacyAuthFlow()` is existing behavior being rewritten (PLAN.md:27-28) with the plan\nexplicitly disclaiming compatibility coverage (PLAN.md:14-16). This is a regression by\ndefinition. **CRITICAL requirement added to the plan:**\n\n- `auth/legacyAuthFlow.characterization.test.ts` \u2014 record current outputs for: valid\n token, expired token, revoked token, wrong audience, wrong issuer, suspended tenant,\n missing tenant header, IDP 5xx, IDP timeout, malformed JWT. Assert the new path\n (behind the flag) produces identical decisions and equivalent error surfaces. These\n tests are written BEFORE any rewrite (T1) and stay green through cut-over.\n\n",
|
||||
"tasks": "## Implementation Tasks\nSynthesized from this review's findings. Each task derives from a specific\nfinding above. Run with Claude Code or Codex; checkbox as you ship.\n\n- [ ] **T1 (P1, human: ~1 day / CC: ~20min)** \u2014 auth/ \u2014 Write characterization tests for `legacyAuthFlow()` before any rewrite\n - Surfaced by: Test review \u2014 REGRESSION rule; PLAN.md:27-28 \"no regression test for the prior behavior is planned\"\n - Files: `auth/legacyAuthFlow.characterization.test.ts`\n - Verify: suite green on current main; re-run after each later task\n",
|
||||
"review": "## GSTACK REVIEW REPORT\n\n| Review | Trigger | Why | Runs | Status | Findings |\n|--------|---------|-----|------|--------|----------|\n| CEO Review | `/plan-ceo-review` | Scope & strategy | 0 | \u2014 | \u2014 |\n",
|
||||
"compact": "### REGRESSION (mandatory, authorized by the coverage-audit regression rule \u2014 no question asked)\n\n`legacyAuthFlow()` is existing behavior being rewritten (PLAN.md:27-28) with the plan\nexplicitly disclaiming compatibility coverage (PLAN.md:14-16). This is a regression by\ndefinition. **CRITICAL requirement added to the plan:**\n\n- `auth/legacyAuthFlow.characterization.test.ts` \u2014 record current outputs for: valid\n token, expired token, revoked token, wrong audience, wrong issuer, suspended tenant,\n missing tenant header, IDP 5xx, IDP timeout, malformed JWT. Assert the new path\n (behind the flag) produces identical decisions and equivalent error surfaces. These\n tests are written BEFORE any rewrite (T1) and stay green through cut-over.\n\n\n## Implementation Tasks\nSynthesized from this review's findings. Each task derives from a specific\nfinding above. Run with Claude Code or Codex; checkbox as you ship.\n\n- [ ] **T1 (P1, human: ~1 day / CC: ~20min)** \u2014 auth/ \u2014 Write characterization tests for `legacyAuthFlow()` before any rewrite\n - Surfaced by: Test review \u2014 REGRESSION rule; PLAN.md:27-28 \"no regression test for the prior behavior is planned\"\n - Files: `auth/legacyAuthFlow.characterization.test.ts`\n - Verify: suite green on current main; re-run after each later task\n\n## GSTACK REVIEW REPORT\n\n| Review | Trigger | Why | Runs | Status | Findings |\n|--------|---------|-----|------|--------|----------|\n| CEO Review | `/plan-ceo-review` | Scope & strategy | 0 | \u2014 | \u2014 |\n"
|
||||
}
|
||||
+138
@@ -0,0 +1,138 @@
|
||||
{
|
||||
"observationSHA256": "12c69056fdb115893b46b1a4289a41e52c8d2a4970d06396ceabc457da74a7f3",
|
||||
"reportSHA256": "74ace7447a07d0b8a27ab45cb6dc8854602e862bbeaed1b7671a34e1e290784b",
|
||||
"calls": [
|
||||
{
|
||||
"sessionId": "6224eb91-e7d5-4efa-89c1-7756f1140770",
|
||||
"toolUseId": "toolu_011m4o2M9ybFru4R41h9eECH",
|
||||
"questions": [
|
||||
{
|
||||
"question": "D4 — Scope: reduce to 2 new classes with injected cache, or proceed with 4 classes / 12 files?\nProject/branch/task: gstack-plan-count-FTw0nf on main, PLAN.md Multi-tenant Auth Refactor.\nELI10: The plan adds four new classes but only explains what two of them do. AuthCache wraps a cache adapter that already does everything the plan needs (PLAN.md:7-13). TokenStore and RequestPolicy get a name and nothing else (PLAN.md:35-36). Every extra class is another file to test, another place a tenant-isolation bug can hide, and another thing the next engineer has to learn. The question is whether the same auth refactor ships with fewer moving parts.\nStakes if we pick wrong: Too big and you carry four abstractions where two would do, forever. Too small and you fold a real responsibility into the wrong class and split it back out later.\nRecommendation: A because the existing adapter already has the contract AuthCache re-exposes, and a class with no stated responsibility is premature abstraction (your 'engineered enough' preference).\nNote: options differ in kind, not coverage — no completeness score.\nNet: two well-defined services over an injected adapter vs. four classes, two of them undefined.",
|
||||
"header": "Scope",
|
||||
"multiSelect": false,
|
||||
"options": [
|
||||
{
|
||||
"label": "A) Reduce: 2 classes, inject adapter (recommended)",
|
||||
"description": "AuthBroker + SessionMint receive the existing cache adapter by constructor; drop the AuthCache facade; RequestPolicy becomes a plain typed config/function; TokenStore folds into the adapter unless it has a distinct responsibility you name. (human: ~3 days / CC: ~45 min) ✅ Fewer files (~7-8), fewer abstractions, and no module-level singleton to leak tenant state. ✅ Reuses the adapter's existing tests and invalidation hooks unchanged. ❌ If TokenStore really is a separate concern (e.g. refresh-token persistence), it must be re-added later."
|
||||
},
|
||||
{
|
||||
"label": "B) Keep 4 classes, but inject (no singleton)",
|
||||
"description": "All four classes stay; AuthCache is passed into both services instead of being a module-level export. (human: ~5 days / CC: ~1 hr) ✅ Preserves whatever design intent sits behind TokenStore and RequestPolicy. ✅ Still removes the shared-mutable-singleton hazard. ❌ Carries two undefended classes and ~12 files; the plan must be amended to state their responsibilities."
|
||||
},
|
||||
{
|
||||
"label": "C) Proceed as-is",
|
||||
"description": "4 classes, 12 files, module-level AuthCache export, as written. (human: ~5 days / CC: ~1 hr) ✅ No re-planning; the design is already in your head. ✅ Fastest path to first commit. ❌ Ships the singleton footgun and two classes the plan cannot yet explain; the architecture review will still flag the singleton."
|
||||
}
|
||||
]
|
||||
}
|
||||
],
|
||||
"answered": true,
|
||||
"failed": false,
|
||||
"answers": {
|
||||
"D4 — Scope: reduce to 2 new classes with injected cache, or proceed with 4 classes / 12 files?\nProject/branch/task: gstack-plan-count-FTw0nf on main, PLAN.md Multi-tenant Auth Refactor.\nELI10: The plan adds four new classes but only explains what two of them do. AuthCache wraps a cache adapter that already does everything the plan needs (PLAN.md:7-13). TokenStore and RequestPolicy get a name and nothing else (PLAN.md:35-36). Every extra class is another file to test, another place a tenant-isolation bug can hide, and another thing the next engineer has to learn. The question is whether the same auth refactor ships with fewer moving parts.\nStakes if we pick wrong: Too big and you carry four abstractions where two would do, forever. Too small and you fold a real responsibility into the wrong class and split it back out later.\nRecommendation: A because the existing adapter already has the contract AuthCache re-exposes, and a class with no stated responsibility is premature abstraction (your 'engineered enough' preference).\nNote: options differ in kind, not coverage — no completeness score.\nNet: two well-defined services over an injected adapter vs. four classes, two of them undefined.": "A) Reduce: 2 classes, inject adapter (recommended)"
|
||||
},
|
||||
"unansweredQuestionIndices": [],
|
||||
"answeredAt": "2026-09-10T09:53:46.307Z"
|
||||
},
|
||||
{
|
||||
"sessionId": "6224eb91-e7d5-4efa-89c1-7756f1140770",
|
||||
"toolUseId": "toolu_01LeQfFfD3XayjMH11jL4ua7",
|
||||
"questions": [
|
||||
{
|
||||
"question": "D5 — Issue 1: Who is allowed to write to the shared cache?\nProject/branch/task: gstack-plan-count-FTw0nf on main, PLAN.md Multi-tenant Auth Refactor (scope reduced per D4).\nELI10: Even with the adapter injected instead of global, AuthBroker and SessionMint both write to the same tenant-keyed cache and nothing orders those writes (PLAN.md:10 'they do not serialize mutations'; PLAN.md:20 'Both services mutate it'). Picture a user logging out: the adapter's logout hook deletes their token entry, and a SessionMint write that started a moment earlier lands right after and puts the token back. The user thinks they're logged out; the token still validates from cache. Nobody sees an error.\nStakes if we pick wrong: A revoked or suspended tenant's token keeps working until natural expiry. That's a silent auth bypass, the worst kind of bug to find in production.\nRecommendation: A because a single-writer rule is explicit over clever, needs no new locking primitive, and makes the race impossible rather than unlikely.\nCompleteness: A=10/10, B=8/10, C=3/10\nNet: an ownership rule enforced by types vs. a version check on every write vs. hoping the window is small.",
|
||||
"header": "Issue 1",
|
||||
"multiSelect": false,
|
||||
"options": [
|
||||
{
|
||||
"label": "1A) Single writer: only SessionMint writes (recommended)",
|
||||
"description": "SessionMint is the sole writer (mint + cache); AuthBroker holds a read-only view of the adapter (a narrowed interface type) and triggers invalidation only through the adapter's existing hooks. Add a test that a mint racing a revocation never resurrects the entry. (human: ~1 day / CC: ~20 min) ✅ The race is structurally impossible; the compiler enforces the rule. ✅ Reuses existing invalidation hooks unchanged. ❌ AuthBroker must go through SessionMint for any cache write it needs today."
|
||||
},
|
||||
{
|
||||
"label": "1B) Both write, guard with policy-version CAS",
|
||||
"description": "Both services keep writing; every write carries the policy version already in the key and the adapter rejects writes whose version is older than the current entry or a tombstone left by invalidation. (human: ~2 days / CC: ~40 min) ✅ No ownership change; both services keep their current shape. ✅ Tombstones also protect against late writes after tenant suspension. ❌ Adds compare-and-set logic to an adapter the plan promised to leave unchanged (PLAN.md:12-13)."
|
||||
},
|
||||
{
|
||||
"label": "1C) Accept the window, document it",
|
||||
"description": "Leave both writers unserialized and add a comment noting the race. (human: ~10 min / CC: ~2 min) ✅ Zero implementation work right now. ✅ Keeps the adapter and both services exactly as planned. ❌ Ships a known silent auth-bypass window; the size of the window depends on IDP latency you don't control."
|
||||
}
|
||||
]
|
||||
}
|
||||
],
|
||||
"answered": true,
|
||||
"failed": false,
|
||||
"answers": {
|
||||
"D5 — Issue 1: Who is allowed to write to the shared cache?\nProject/branch/task: gstack-plan-count-FTw0nf on main, PLAN.md Multi-tenant Auth Refactor (scope reduced per D4).\nELI10: Even with the adapter injected instead of global, AuthBroker and SessionMint both write to the same tenant-keyed cache and nothing orders those writes (PLAN.md:10 'they do not serialize mutations'; PLAN.md:20 'Both services mutate it'). Picture a user logging out: the adapter's logout hook deletes their token entry, and a SessionMint write that started a moment earlier lands right after and puts the token back. The user thinks they're logged out; the token still validates from cache. Nobody sees an error.\nStakes if we pick wrong: A revoked or suspended tenant's token keeps working until natural expiry. That's a silent auth bypass, the worst kind of bug to find in production.\nRecommendation: A because a single-writer rule is explicit over clever, needs no new locking primitive, and makes the race impossible rather than unlikely.\nCompleteness: A=10/10, B=8/10, C=3/10\nNet: an ownership rule enforced by types vs. a version check on every write vs. hoping the window is small.": "1A) Single writer: only SessionMint writes (recommended)"
|
||||
},
|
||||
"unansweredQuestionIndices": [],
|
||||
"answeredAt": "2026-09-10T09:54:54.736Z"
|
||||
},
|
||||
{
|
||||
"sessionId": "6224eb91-e7d5-4efa-89c1-7756f1140770",
|
||||
"toolUseId": "toolu_01VJKE15J3D5NpLmUqw5QQ53",
|
||||
"questions": [
|
||||
{
|
||||
"question": "D7 — Issue 3: What happens to validateAndDispatch() and its three swallowing catch blocks?\nProject/branch/task: gstack-plan-count-FTw0nf on main, PLAN.md Multi-tenant Auth Refactor (scope reduced per D4).\nELI10: validateAndDispatch() is 60 lines with three try/catch blocks nested inside each other, and each catch eats a different kind of error and moves on (PLAN.md:23-24). In auth code, 'moves on' is the problem: either the user gets a blank denial with nothing in the logs, or the function reaches the dispatch step even though a validation step failed. Neither is visible until someone reports it.\nStakes if we pick wrong: Silent denials that support can't debug, or a validation step that fails open. Both are invisible in tests that only check the happy path.\nRecommendation: A because splitting validation from dispatch and making every failure an explicit typed outcome is 'explicit over clever', and CC writes the per-error-class tests in minutes.\nCompleteness: A=10/10, B=6/10, C=2/10\nNet: explicit error outcomes with a test per class vs. same shape with logging vs. leave it.",
|
||||
"header": "Issue 3",
|
||||
"multiSelect": false,
|
||||
"options": [
|
||||
{
|
||||
"label": "3A) Split + typed error results, test per error class (recommended)",
|
||||
"description": "Split into validate() returning a discriminated result (ok | {kind: 'expired'|'issuer'|'audience'|'network'|...}) and dispatch(); one boundary catch maps unknown throws to a logged 500-class error. No catch swallows. One unit test per error kind asserting the mapped outcome and log line. (human: ~1 day / CC: ~20 min) ✅ Every failure is named, logged, and tested; fail-closed is enforced by the type. ✅ Each function fits on a screen and has one job. ❌ Callers of validateAndDispatch() adapt to the new result shape."
|
||||
},
|
||||
{
|
||||
"label": "3B) Keep structure, log + rethrow in each catch",
|
||||
"description": "Same 60-line function and nesting; each catch logs with error class and rethrows or returns a deny. (human: ~2 hr / CC: ~5 min) ✅ Minimal diff; no caller changes. ✅ Errors stop being silent. ❌ Three nested try/catch blocks remain; the next engineer still has to trace which catch owns which failure."
|
||||
},
|
||||
{
|
||||
"label": "3C) Leave as-is",
|
||||
"description": "No change to validateAndDispatch(). (human: ~0 / CC: ~0) ✅ No work, no risk of introducing a change in this PR. ✅ Keeps the PR focused on the new services. ❌ Three classes of auth failure remain invisible in production."
|
||||
}
|
||||
]
|
||||
}
|
||||
],
|
||||
"answered": true,
|
||||
"failed": false,
|
||||
"answers": {
|
||||
"D7 — Issue 3: What happens to validateAndDispatch() and its three swallowing catch blocks?\nProject/branch/task: gstack-plan-count-FTw0nf on main, PLAN.md Multi-tenant Auth Refactor (scope reduced per D4).\nELI10: validateAndDispatch() is 60 lines with three try/catch blocks nested inside each other, and each catch eats a different kind of error and moves on (PLAN.md:23-24). In auth code, 'moves on' is the problem: either the user gets a blank denial with nothing in the logs, or the function reaches the dispatch step even though a validation step failed. Neither is visible until someone reports it.\nStakes if we pick wrong: Silent denials that support can't debug, or a validation step that fails open. Both are invisible in tests that only check the happy path.\nRecommendation: A because splitting validation from dispatch and making every failure an explicit typed outcome is 'explicit over clever', and CC writes the per-error-class tests in minutes.\nCompleteness: A=10/10, B=6/10, C=2/10\nNet: explicit error outcomes with a test per class vs. same shape with logging vs. leave it.": "3A) Split + typed error results, test per error class (recommended)"
|
||||
},
|
||||
"unansweredQuestionIndices": [],
|
||||
"answeredAt": "2026-09-10T09:56:01.216Z"
|
||||
},
|
||||
{
|
||||
"sessionId": "6224eb91-e7d5-4efa-89c1-7756f1140770",
|
||||
"toolUseId": "toolu_0146kiJJzgAyvhjwGzuVKDHg",
|
||||
"questions": [
|
||||
{
|
||||
"question": "D10 — Issue 6: How are the 5 IDP calls parallelized?\nProject/branch/task: gstack-plan-count-FTw0nf on main, PLAN.md Multi-tenant Auth Refactor (scope reduced per D4).\nELI10: Today the five identity-provider checks run one after another, so login takes five round trips (PLAN.md:31). Running them at once cuts that to one round trip. But 'at once' has two sharp edges: if one call hangs, the login hangs with it unless each call has its own deadline, and if one call fails fast the other four keep burning IDP quota unless we cancel them. Promise.all is the right aggregator here because a single failed check must fail the whole validation (fail closed).\nStakes if we pick wrong: Either logins hang on a slow IDP, or you quietly 5x your IDP request volume during an outage.\nRecommendation: A because per-call timeouts and cancellation are a few lines with AbortController and turn a 'trivial' change into a bounded one; complete error handling over happy path.\nCompleteness: A=10/10, B=7/10, C=3/10\nNet: bounded, cancellable parallel calls vs. bare Promise.all vs. sequential as today.",
|
||||
"header": "Issue 6",
|
||||
"multiSelect": false,
|
||||
"options": [
|
||||
{
|
||||
"label": "6A) Promise.all + per-call timeout + abort on first failure (recommended)",
|
||||
"description": "Each IDP call gets an AbortSignal with a per-call deadline; Promise.all rejects on the first failure and the shared controller aborts the remaining four; failure maps to the 'network' or check-specific error kind from 3A. Tests: fastest-rejection wins, timeout maps to 'network', remaining calls observed aborted. (human: ~half day / CC: ~15 min) ✅ Login latency bounded by the slowest healthy call, never by a hung one. ✅ IDP quota isn't burned on calls whose result no longer matters. ❌ Slightly more plumbing than a one-line Promise.all."
|
||||
},
|
||||
{
|
||||
"label": "6B) Plain Promise.all",
|
||||
"description": "Wrap the five calls in Promise.all; rely on the IDP client's global timeout, if any. (human: ~1 hr / CC: ~3 min) ✅ One-line change; immediate latency win. ✅ Fail-closed semantics come free from Promise.all. ❌ A hung call hangs the login; four calls keep running after the first rejection."
|
||||
},
|
||||
{
|
||||
"label": "6C) Keep sequential",
|
||||
"description": "No change. (human: ~0 / CC: ~0) ✅ No new concurrency to reason about. ✅ IDP sees at most one in-flight call per login. ❌ Login stays 5 round trips long for every user, every time."
|
||||
}
|
||||
]
|
||||
}
|
||||
],
|
||||
"answered": true,
|
||||
"failed": false,
|
||||
"answers": {
|
||||
"D10 — Issue 6: How are the 5 IDP calls parallelized?\nProject/branch/task: gstack-plan-count-FTw0nf on main, PLAN.md Multi-tenant Auth Refactor (scope reduced per D4).\nELI10: Today the five identity-provider checks run one after another, so login takes five round trips (PLAN.md:31). Running them at once cuts that to one round trip. But 'at once' has two sharp edges: if one call hangs, the login hangs with it unless each call has its own deadline, and if one call fails fast the other four keep burning IDP quota unless we cancel them. Promise.all is the right aggregator here because a single failed check must fail the whole validation (fail closed).\nStakes if we pick wrong: Either logins hang on a slow IDP, or you quietly 5x your IDP request volume during an outage.\nRecommendation: A because per-call timeouts and cancellation are a few lines with AbortController and turn a 'trivial' change into a bounded one; complete error handling over happy path.\nCompleteness: A=10/10, B=7/10, C=3/10\nNet: bounded, cancellable parallel calls vs. bare Promise.all vs. sequential as today.": "6A) Promise.all + per-call timeout + abort on first failure (recommended)"
|
||||
},
|
||||
"unansweredQuestionIndices": [],
|
||||
"answeredAt": "2026-09-10T09:58:14.247Z"
|
||||
}
|
||||
],
|
||||
"declaration": "**CRITICAL — regression rule (mandatory, not a decision):** T1 adds a characterization test for `legacyAuthFlow()`'s current behavior (happy path, each current denial path, cache interaction) and lands *before* the 4A extraction. The original plan excluded this (PLAN.md:15-16); that exclusion is removed.",
|
||||
"tasks": "- [ ] **T1 (P1, human: ~2h / CC: ~10min)** — auth/legacy — Add characterization/regression test for `legacyAuthFlow()` current behavior\n - Surfaced by: Test review — REGRESSION RULE; PLAN.md:15-16 excluded it, PLAN.md:27 rewrites it\n - Files: `auth/__tests__/legacyAuthFlow.regression.test.ts`\n - Verify: test passes against unmodified legacy before any other commit\n- [ ] **T2 (P1, human: ~1d / CC: ~20min)** — auth/validate — Extract IDP checks + token validation into shared `validate()`; legacy calls it, behavior unchanged\n - Surfaced by: Code quality Issue 4 (D8, 4A)\n - Files: `auth/validate.ts`, `auth/legacyAuthFlow.ts`\n - Verify: T1 still green; diff to legacy is call-site only\n",
|
||||
"compact": "## Tests\n\n**CRITICAL — regression rule (mandatory, not a decision):** T1 adds a characterization test for `legacyAuthFlow()`'s current behavior (happy path, each current denial path, cache interaction) and lands *before* the 4A extraction. The original plan excluded this (PLAN.md:15-16); that exclusion is removed.\n\n## Implementation Tasks\n\n- [ ] **T1 (P1, human: ~2h / CC: ~10min)** — auth/legacy — Add characterization/regression test for `legacyAuthFlow()` current behavior\n - Surfaced by: Test review — REGRESSION RULE; PLAN.md:15-16 excluded it, PLAN.md:27 rewrites it\n - Files: `auth/__tests__/legacyAuthFlow.regression.test.ts`\n - Verify: test passes against unmodified legacy before any other commit\n- [ ] **T2 (P1, human: ~1d / CC: ~20min)** — auth/validate — Extract IDP checks + token validation into shared `validate()`; legacy calls it, behavior unchanged\n - Surfaced by: Code quality Issue 4 (D8, 4A)\n - Files: `auth/validate.ts`, `auth/legacyAuthFlow.ts`\n - Verify: T1 still green; diff to legacy is call-site only\n\n## GSTACK REVIEW REPORT\n\n**Suppressed findings (confidence ≤ 4, appendix only):**\n- `[P1?] (confidence: 4/10) PLAN.md:7-8` — tenant ID source for the cache key not stated; if claim-derived, cross-tenant cache poisoning. Unverifiable without source. Captured as TODO 3.\n- `[P3] (confidence: 3/10) PLAN.md:31` — five parallel IDP calls may hit IDP per-client rate limits during a cold-start stampede; mitigated by 7A single-flight. No IDP quota figures available.\n\n| Review | Trigger | Why | Runs | Status | Findings |\n|--------|---------|-----|------|--------|----------|\n| CEO Review | `/plan-ceo-review` | Scope & strategy | 0 | — | — |\n| Outside Review | codex via `/plan-eng-review` | Independent 2nd opinion | 1 | disabled | outside_status: disabled (codex_reviews=disabled), phase: plan-review, host: claude |\n| Eng Review | `/plan-eng-review` | Architecture & tests (required) | 1 | clean (PLAN) | 32 issues (6 section findings + 26 test gaps), 0 critical gaps, mode SCOPE_REDUCED |\n| Design Review | `/plan-design-review` | UI/UX gaps | 0 | — | — |\n| DX Review | `/plan-devex-review` | Developer experience gaps | 0 | — | — |\n\n**OUTSIDE COVERAGE:** provider codex, phase plan-review, outside_status disabled (user opt-out), no findings; no native fallback dispatched. Outside coverage for this plan is absent by configuration, not by failure.\n\n**VERDICT:** ENG CLEARED — ready to implement (SCOPE_REDUCED; all 7 decisions resolved, 0 critical gaps). CEO and Design reviews not run; neither gates shipping for a backend auth refactor.\n\nNO UNRESOLVED DECISIONS\n",
|
||||
"reviewReport": "## GSTACK REVIEW REPORT\n\n**Suppressed findings (confidence ≤ 4, appendix only):**\n- `[P1?] (confidence: 4/10) PLAN.md:7-8` — tenant ID source for the cache key not stated; if claim-derived, cross-tenant cache poisoning. Unverifiable without source. Captured as TODO 3.\n- `[P3] (confidence: 3/10) PLAN.md:31` — five parallel IDP calls may hit IDP per-client rate limits during a cold-start stampede; mitigated by 7A single-flight. No IDP quota figures available.\n\n| Review | Trigger | Why | Runs | Status | Findings |\n|--------|---------|-----|------|--------|----------|\n| CEO Review | `/plan-ceo-review` | Scope & strategy | 0 | — | — |\n| Outside Review | codex via `/plan-eng-review` | Independent 2nd opinion | 1 | disabled | outside_status: disabled (codex_reviews=disabled), phase: plan-review, host: claude |\n| Eng Review | `/plan-eng-review` | Architecture & tests (required) | 1 | clean (PLAN) | 32 issues (6 section findings + 26 test gaps), 0 critical gaps, mode SCOPE_REDUCED |\n| Design Review | `/plan-design-review` | UI/UX gaps | 0 | — | — |\n| DX Review | `/plan-devex-review` | Developer experience gaps | 0 | — | — |\n\n**OUTSIDE COVERAGE:** provider codex, phase plan-review, outside_status disabled (user opt-out), no findings; no native fallback dispatched. Outside coverage for this plan is absent by configuration, not by failure.\n\n**VERDICT:** ENG CLEARED — ready to implement (SCOPE_REDUCED; all 7 decisions resolved, 0 critical gaps). CEO and Design reviews not run; neither gates shipping for a backend auth refactor.\n\nNO UNRESOLVED DECISIONS\n"
|
||||
}
|
||||
@@ -2,7 +2,7 @@
|
||||
import * as fs from 'node:fs';
|
||||
import * as path from 'node:path';
|
||||
import { createHash } from 'node:crypto';
|
||||
import { validAutoplanEditDigest, readAutoplanDigestFile, matchesAutoplanDigestRows } from './autoplan-artifact-digest';
|
||||
import { validAutoplanEditDigest, readAutoplanDigestFile, matchesAutoplanDigestRows, createAutoplanEditDigest } from './autoplan-artifact-digest';
|
||||
import type { PendingAutoplanArtifact } from './autoplan-artifact-recorder';
|
||||
import type { NativePublicToolEvent } from './plan-count-transcript';
|
||||
|
||||
@@ -21,28 +21,8 @@ const compact = (text: string) => text.replace(/\s/g, '');
|
||||
|
||||
/** The native header may remain above the diff; both displayed paths must bind. */
|
||||
function ownedEditDiffRows(rows: string[], file: string, ownedStateRoot?: string): string[] | null {
|
||||
// A redraw can repeat the same native tool title above one current panel.
|
||||
// Those homogeneous titles supply no authority: the full panel below must
|
||||
// still bind its path, current request, content, and exact one-time menu.
|
||||
const repeated: string[] = [];
|
||||
let panelAt = 0;
|
||||
for (; panelAt < rows.length; panelAt++) {
|
||||
if (!rows[panelAt]!.trim()) continue;
|
||||
const title = /^[●⏺] Update\(([^\n]+)\)$/.exec(rows[panelAt]!);
|
||||
if (!title) break;
|
||||
repeated.push(title[1]!);
|
||||
}
|
||||
if (repeated.length > 1 && ownedStateRoot && /^[─╌]{8,}$/.test(rows[panelAt] ?? '') &&
|
||||
rows[panelAt + 1]?.trim() === 'Edit file') {
|
||||
const relative = path.relative(ownedStateRoot, file).split(path.sep).join('/');
|
||||
const alias = path.basename(ownedStateRoot) === '.gstack' ? `~/.gstack/${relative}` : undefined;
|
||||
if (repeated.some(title => title !== repeated[0]) || (repeated[0] !== file && repeated[0] !== alias)) return null;
|
||||
rows = rows.slice(panelAt);
|
||||
}
|
||||
const headers = rows.flatMap((row, index) => /^[●⏺] Update\(/.test(row) ? [index] : []);
|
||||
if (headers.length > 1) return null;
|
||||
const header = headers[0] ?? 0;
|
||||
if (header > 0) {
|
||||
const header = rows.findIndex(row => /^[●⏺] Update\(/.test(row));
|
||||
if (header > 0 && rows.slice(0,header).some(row => row.trim())) {
|
||||
// A completed native tool's diff may remain above the active edit panel.
|
||||
// Only its indented diff output is ignored; competing panels or prose are
|
||||
// not evidence for the current request and cannot be used as a prefix.
|
||||
@@ -63,6 +43,25 @@ function ownedEditDiffRows(rows: string[], file: string, ownedStateRoot?: string
|
||||
if (!numbered) return null;
|
||||
rows = rows.slice(header);
|
||||
}
|
||||
// A redraw can repeat the same native tool title above one current panel.
|
||||
// Those homogeneous titles supply no authority: the full panel below must
|
||||
// still bind its path, current request, content, and exact one-time menu.
|
||||
const repeated: string[] = [];
|
||||
let panelAt = 0;
|
||||
for (; panelAt < rows.length; panelAt++) {
|
||||
if (!rows[panelAt]!.trim()) continue;
|
||||
const title = /^[●⏺] Update\(([^\n]+)\)$/.exec(rows[panelAt]!);
|
||||
if (!title) break;
|
||||
repeated.push(title[1]!);
|
||||
}
|
||||
if (repeated.length > 1 && ownedStateRoot && /^[─╌]{8,}$/.test(rows[panelAt] ?? '') &&
|
||||
rows[panelAt + 1]?.trim() === 'Edit file') {
|
||||
const relative = path.relative(ownedStateRoot, file).split(path.sep).join('/');
|
||||
const alias = path.basename(ownedStateRoot) === '.gstack' ? `~/.gstack/${relative}` : undefined;
|
||||
if (repeated.some(title => title !== repeated[0]) || (repeated[0] !== file && repeated[0] !== alias)) return null;
|
||||
rows = rows.slice(panelAt);
|
||||
}
|
||||
if (rows.filter(row => /^[●⏺] Update\(/.test(row)).length > 1) return null;
|
||||
// A viewport can start at the native Edit panel after its tool title has
|
||||
// scrolled away. The remaining displayed path must still bind the complete
|
||||
// owned project/artifact path; the menu and current request are checked below.
|
||||
@@ -277,3 +276,63 @@ export function pendingAutoplanArtifactPermissionInput(viewport: string,
|
||||
return {input:'1\r', signature, file:p.file};
|
||||
} catch { return null; }
|
||||
}
|
||||
|
||||
/** A native hook identifies the executing request within a published tool batch. */
|
||||
export function publishedAutoplanArtifactPermissionInput(viewport: string,
|
||||
context: ArtifactPermissionContext & { pending?: PendingAutoplanArtifact; viewportCapturedAt: number },
|
||||
seen: ReadonlySet<string>,
|
||||
): { input: '1\r'; signature: string; file: string } | null {
|
||||
const p=context.pending, now=context.now??Date.now();
|
||||
if (!p || p.source!=='pre_tool_use' || p.tool!=='Edit' || !validAutoplanEditDigest(p.editDigest) ||
|
||||
context.transcriptStatus!=='ready' || !Number.isFinite(now) || !Number.isFinite(context.commandStartedAt) ||
|
||||
!Number.isFinite(context.viewportCapturedAt) || context.commandStartedAt>context.viewportCapturedAt ||
|
||||
context.viewportCapturedAt>now || context.publicTools.length>10_000 || viewport.length>MAX_BYTES ||
|
||||
!/^[A-Za-z0-9_-]{1,160}$/.test(p.sessionId) || !/^[A-Za-z0-9_-]{1,160}$/.test(p.toolUseId) ||
|
||||
!Array.isArray(p.hookSeenIds) || !p.hookSeenIds.length || p.hookSeenIds.length>128 ||
|
||||
p.hookSeenIds.some(id=>typeof id!=='string'||!/^[A-Za-z0-9_-]{1,160}$/.test(id)) ||
|
||||
new Set(p.hookSeenIds).size!==p.hookSeenIds.length || !p.hookSeenIds.includes(p.toolUseId) ||
|
||||
seen.has(`${p.sessionId}:${p.toolUseId}`) || seen.has(autoplanArtifactMenuKey(viewport)) ||
|
||||
!ownedAutoplanArtifact(p.file,context)) return null;
|
||||
const pendingTime=Date.parse(p.timestamp);
|
||||
if (!Number.isFinite(pendingTime) || pendingTime<context.commandStartedAt || pendingTime>context.viewportCapturedAt ||
|
||||
context.publicTools.some(e=>!Number.isFinite(Date.parse(e.timestamp)))) return null;
|
||||
const events=context.publicTools.filter(e=>Date.parse(e.timestamp)>=context.commandStartedAt);
|
||||
const uses=new Map<string,NativePublicToolEvent>(), results=new Map<string,NativePublicToolEvent>();
|
||||
let last=context.commandStartedAt;
|
||||
for (const event of events) {
|
||||
const time=Date.parse(event.timestamp), map=event.kind==='use'?uses:results;
|
||||
if (event.sessionId!==p.sessionId || !event.toolUseId || time<last || time>now || map.has(event.toolUseId) ||
|
||||
(event.kind==='result'&&!uses.has(event.toolUseId))) return null;
|
||||
last=time;map.set(event.toolUseId,event);
|
||||
}
|
||||
const current=uses.get(p.toolUseId), input=current?.input;
|
||||
if (!current || current.name!=='Edit' || results.has(p.toolUseId) || Date.parse(current.timestamp)>pendingTime ||
|
||||
!/^msg_[A-Za-z0-9_-]{1,160}$/.test(current.messageId??'') || !/^req_[A-Za-z0-9_-]{1,160}$/.test(current.requestId??'') ||
|
||||
input?.file_path!==p.file || typeof input.old_string!=='string' || !input.old_string ||
|
||||
typeof input.new_string!=='string' || input.new_string===input.old_string ||
|
||||
(input.replace_all!==undefined&&input.replace_all!==false)) return null;
|
||||
const queued=new Set<string>();
|
||||
for (const mutation of [...uses.values()].filter(e=>e.name==='Edit'||e.name==='Write')) {
|
||||
const result=results.get(mutation.toolUseId);
|
||||
if (Date.parse(mutation.timestamp)>pendingTime || (result&&Date.parse(result.timestamp)>pendingTime)) return null;
|
||||
if (mutation.toolUseId===p.toolUseId || result) continue;
|
||||
// Later publications are queued only when this exact batch owns them and
|
||||
// the recorder has not started them. They never supply current authority.
|
||||
if (mutation.name!=='Edit' || mutation.input?.file_path!==p.file ||
|
||||
typeof mutation.input.old_string!=='string' || !mutation.input.old_string ||
|
||||
typeof mutation.input.new_string!=='string' || mutation.input.old_string===mutation.input.new_string ||
|
||||
(mutation.input.replace_all!==undefined && mutation.input.replace_all!==false) ||
|
||||
mutation.messageId!==current.messageId || mutation.requestId!==current.requestId ||
|
||||
events.indexOf(mutation)<=events.indexOf(current) || p.hookSeenIds.includes(mutation.toolUseId)) return null;
|
||||
queued.add(mutation.toolUseId);
|
||||
}
|
||||
try {
|
||||
if (Math.floor(fs.statSync(p.file).mtimeMs)>pendingTime) return null;
|
||||
const actual=createAutoplanEditDigest(p.file,input.old_string,input.new_string), expected=p.editDigest!;
|
||||
if (!actual || actual.beforeSHA256!==expected.beforeSHA256 || actual.requestSHA256!==expected.requestSHA256 ||
|
||||
JSON.stringify(actual.oldLineHashes)!==JSON.stringify(expected.oldLineHashes) ||
|
||||
JSON.stringify(actual.newLineHashes)!==JSON.stringify(expected.newLineHashes)) return null;
|
||||
return autoplanArtifactPermissionInput(viewport,{...context,
|
||||
publicTools:events.filter(e=>!queued.has(e.toolUseId))},seen);
|
||||
} catch { return null; }
|
||||
}
|
||||
@@ -16,6 +16,8 @@ const keys = (v: Record<string, any>, allowed: string[]) => Object.keys(v).every
|
||||
const quote = (v: string) => `'${(process.platform === 'win32' ? v.replaceAll('\\','/') : v).replaceAll("'", "'\\''")}'`;
|
||||
export interface PendingAutoplanArtifact {
|
||||
source:'pre_tool_use'; sessionId:string; toolUseId:string; tool:'Edit'; file:string; timestamp:string; editDigest?:AutoplanEditDigest;
|
||||
/** Validated recorder tombstones; exposed only by the published-current opt-in. */
|
||||
hookSeenIds?:string[];
|
||||
}
|
||||
interface Pending extends PendingAutoplanArtifact { transcriptPath:string }
|
||||
interface State { version:1; cwd:string; config:string; stateRoot:string; sessionId?:string; seenIds:string[]; pending:Pending|null }
|
||||
@@ -141,15 +143,16 @@ export function autoplanArtifactRecorderStatus(file:string|undefined,cwd:string,
|
||||
} catch { return {status:'invalid',reason:'record_error'}; }
|
||||
}
|
||||
export function readPendingAutoplanArtifact(file:string|undefined,cwd:string,config:string|null,stateRoot:string|undefined,
|
||||
startedAt:number, publicTools:readonly NativePublicToolEvent[], now=Date.now()): PendingAutoplanArtifact|undefined {
|
||||
startedAt:number, publicTools:readonly NativePublicToolEvent[], now=Date.now(), allowPublished=false): PendingAutoplanArtifact|undefined {
|
||||
if (!file || !config || !stateRoot || !Number.isFinite(startedAt) || !Number.isFinite(now) ||
|
||||
autoplanArtifactRecorderStatus(file,cwd,config,stateRoot).status!=='pending') return undefined;
|
||||
try {
|
||||
const p=readState(file,cwd,config,stateRoot).pending!;
|
||||
const state=readState(file,cwd,config,stateRoot), p=state.pending!;
|
||||
const time=Date.parse(p.timestamp), sessions=new Set(publicTools.map(e=>e.sessionId));
|
||||
if (sessions.size!==1 || !sessions.has(p.sessionId) || time<startedAt || time>now ||
|
||||
publicTools.some(e=>e.toolUseId===p.toolUseId)) return undefined;
|
||||
const {transcriptPath:_,...pending}=p; return pending;
|
||||
(!allowPublished && publicTools.some(e=>e.toolUseId===p.toolUseId))) return undefined;
|
||||
const {transcriptPath:_,...pending}=p;
|
||||
return allowPublished ? {...pending,hookSeenIds:[...state.seenIds]} : pending;
|
||||
} catch { return undefined; }
|
||||
}
|
||||
if (import.meta.main && process.argv[2]==='--record') {
|
||||
|
||||
@@ -702,9 +702,15 @@ function hasProseStaleFillFinding(report: string): boolean {
|
||||
// A review may identify the ordering defect directly as missing coordination
|
||||
// between cache fills and writes that violates read-after-write freshness.
|
||||
// That is independent evidence even when the old-value trace is a diagram.
|
||||
const premise = /(?:^|[.;]\s+)(?:\[Amended:[^\]]{1,80}\]\s*)?(?:the\s+)?(?:original|current|proposed)\s+(?:sketch|wrapper|implementation)\s+(?:had|has)\s+no\s+coordination\s+between\s+(?:an?\s+)?cache\s+fill\s+and\s+(?:an?\s+)?write\b/i.exec(text);
|
||||
const conclusion = /(?:^|[.;]\s+)(?:finding\s+[\w.-]+\s+(?:showed|shows)\s+)?(?:this|that|it)\s+(?:violates|breaks)\s+the\s+read[- ]after[- ]write\s+(?:rule|contract|guarantee|invariant)(?:[.!](?=\s|$)|$)/i.exec(text);
|
||||
// Inline source cannot supply the assertion; the amendment label is metadata.
|
||||
const coordinationText = normalize(block.replace(/`([^`]*)`/g, (_span, body: string) =>
|
||||
/^\[Amended:[^\]]+\]$/.test(body) ? body : '[literal]'));
|
||||
const premise = /(?:^|[.;]\s+)(?:\[Amended:[^\]]{1,80}\]\s*)?(?:the\s+)?(?:original|current|proposed)\s+(sketch|wrapper|implementation)\s+(?:(?:had|has)\s+no\s+coordination\s+between\s+(?:an?\s+)?cache\s+fill\s+and\s+(?:an?\s+)?write\b|stated\s+that\s+no\s+coordination\s+between\s+(?:an?\s+)?cache\s+fill\s+and\s+(?:an?\s+)?write\s+was\s+proposed\b)/i.exec(coordinationText);
|
||||
const citedConclusion = /(?:^|[.;]\s+)Review\s+(?:showed|shows)\s+that\s+(sketch|wrapper|implementation)\s+(?:violates|breaks)\s+the\s+(?:retained\s+)?read[- ]after[- ]write\s+(?:rule|contract|guarantee|invariant)\s+\(see\s+(F[1-9]\d*)\)(?:[.!](?=\s|$)|$)/i.exec(coordinationText);
|
||||
const conclusion = /(?:^|[.;]\s+)(?:finding\s+[\w.-]+\s+(?:showed|shows)\s+)?(?:this|that|it)\s+(?:violates|breaks)\s+the\s+read[- ]after[- ]write\s+(?:rule|contract|guarantee|invariant)(?:[.!](?=\s|$)|$)/i.exec(coordinationText) ?? citedConclusion;
|
||||
const coordinationGap = premise !== null && conclusion !== null && premise.index < conclusion.index
|
||||
&& (conclusion !== citedConclusion || premise[1]!.toLowerCase() === citedConclusion![1]!.toLowerCase())
|
||||
&& !coordinationText.slice(premise.index, conclusion.index).includes('|')
|
||||
&& !/["“”]|\b(?:if|example|template|quoted)\b/i.test(text)
|
||||
&& !/\b(?:example|template|source|quoted|format)\b[^.]*:\s*$/i.test(blocks[index - 1] ?? '');
|
||||
if ((!stale || !inFlight || !read || !fill || !invalidation || !ordering) && !coordinationGap) return false;
|
||||
@@ -714,7 +720,8 @@ function hasProseStaleFillFinding(report: string): boolean {
|
||||
// following dismissal cannot turn a traced race into positive coverage.
|
||||
const next = blocks[index + 1] ?? '';
|
||||
const independent = /^(?:#{1,6}(?:\s|\d)|\d+\.\s|[-*]\s|\||(?:[*_]+)?(?:Finding\b|Section\s|P[0-3]\b))/i.test(next);
|
||||
const explicitId = /^\|\s*(F[1-9]\d*)\s*\|/.exec(text)?.[1];
|
||||
const explicitId = /^\|\s*(F[1-9]\d*)\s*\|/.exec(text)?.[1]
|
||||
?? (coordinationGap && conclusion === citedConclusion ? citedConclusion?.[2] : undefined);
|
||||
const assessment = explicitId
|
||||
? structuredFindingAssessment(lines, owners.length - 1, owners.length - 1, [explicitId], assertedProseOwner).join(' ')
|
||||
: '';
|
||||
|
||||
@@ -2426,9 +2426,23 @@ function nativeExplicitCeoFinding(fp: AskUserQuestionFingerprint, allowQuestionI
|
||||
new Set(labels.map(token => token![2]!.toUpperCase())).size !== labels.length) return false;
|
||||
}
|
||||
const numberedSubject = /^([1-9]\d*(?:\.[1-9]\d*)+)\s+([^:\n]+):\s*([^\n]+)$/.exec(normalized);
|
||||
const parenthesized = /^D[1-9]\d*\s+\((?:issue|finding)\s+([1-9]\d*(?:\.[1-9]\d*)*)\)\s*[—–-]\s*[^\n?]+\?$/i.exec(title);
|
||||
const parenthesized = /^D[1-9]\d*\s+\((?:issue|finding)\s+([1-9]\d*(?:\.[1-9]\d*)*)\)\s*[—–-]\s*([^\n?]+\?)$/i.exec(title);
|
||||
if (parenthesized && (allowQuestionId || /^D[1-9]\d*\s+\(Finding\s/i.test(title) || (parenthesized[1]!.includes('.') &&
|
||||
!/^(?:(?:Finding|Issue)\s+F?|F)[1-9]\d*/i.test(q.header.trim())))) return ceoParenthesizedIssueBrief(q, parenthesized[1]!);
|
||||
// A descriptive header can name the affected test. The full owned brief,
|
||||
// rather than that header, must supply its current gap and offered remedy.
|
||||
if (parenthesized && !/^(?:finding|issue)\b|^f\d/i.test(q.header.trim())) {
|
||||
const prefix = q.question.slice(title.length, q.question.indexOf('\nELI10:'))
|
||||
.replace(/```[\s\S]*?(?:```|$)|~~~[\s\S]*?(?:~~~|$)/g, '')
|
||||
.replace(/^(?:\s*>| {4}|\t).*$/gm, '')
|
||||
.replace(/"(?:[^"\\]|\\.)*"|“[^”]*”|`[^`]*`/g, '""');
|
||||
// A standalone preface owns the assessment below it. Only current
|
||||
// question metadata or a wholly quoted note may precede this new path.
|
||||
const ownedPrefix = prefix.split('\n').map(line => line.trim()).filter(Boolean).every(line =>
|
||||
/^(?:Project\/branch\/task:|\[P[0-3]\])/.test(line) || /^[A-Za-z][A-Za-z -]*:\s*""[.!?]?$/.test(line));
|
||||
const framed = /(?:^|\n)\s*(?:if|unless|whether|suppose|imagine)\b|\b(?:earlier|previous|historical|hypothetical|quoted|source)\s+(?:review\s+)?(?:assessment|example|excerpt|material|text|finding)\b|\b(?:assessment|finding|issue)\s+(?:is|was|represents?)\s+(?:(?:only|just|a|an)\s+)*(?:hypothetical|historical|quoted|example|source)\b/i.test(prefix);
|
||||
if (ownedPrefix && !framed && ceoNumberedBriefDecision(q, parenthesized[2]!)) return true;
|
||||
}
|
||||
if (identity && !/^[^\n?]+\?$/.test(identity[3]!) && !ceoNumberedBriefDecision(q, identity[3]!)) return false;
|
||||
if (numberedSubject && (numberedSubject[2]!.trim().toLowerCase() !== q.header.trim().toLowerCase() ||
|
||||
!ceoNumberedBriefDecision(q, numberedSubject[3]!))) return false;
|
||||
@@ -2678,6 +2692,47 @@ function engNumberedFindingAUQ(fp: AskUserQuestionFingerprint): boolean {
|
||||
fp.options.length !== q.options.length || !fp.options.every((o, i) => o.index === i + 1 && o.label === q.options[i]!.label) ||
|
||||
/<gstack-qid/i.test(q.question)) return false;
|
||||
const title = q.question.split('\n')[0]!.replace(/^D[1-9]\d*\s*[—–:-]\s*/i, '');
|
||||
// A descriptive native header can carry the decision ordinal while the
|
||||
// current brief owns the architecture finding and its cache remedy.
|
||||
const injected = /^Module-level ([A-Za-z_$][\w$]*) singleton (?:→|->) constructor injection with a single writer\?$/i.exec(title);
|
||||
if (injected) {
|
||||
const ordinal = /^D([1-9]\d*)\s*[—–:-]/i.exec(q.question);
|
||||
if (!ordinal || !new RegExp(`^D${ordinal[1]} DI$`, 'i').test(q.header.trim())) return false;
|
||||
const current = (text: string) => text.replace(/```[\s\S]*?(?:```|$)|~~~[\s\S]*?(?:~~~|$)/g, '')
|
||||
.replace(/^(?:\s*>| {4}|\t).*$/gm, '')
|
||||
.replace(/["“](withdrawn|rejected|cancelled|canceled|resolved|closed|not current)["”]/gi, '$1')
|
||||
.replace(/"(?:[^"\\]|\\.)*"|“[^”]*”|`[^`]*`/g, '');
|
||||
const text = current(q.question), assessment = [...text.matchAll(/^ELI10: (.+)$/gm)];
|
||||
const preface = text.slice(text.indexOf('\n') + 1, assessment[0]?.index ?? 0).trim().split('\n').filter(Boolean);
|
||||
const framing = /\b(?:source|quoted|historical|hypothetical|proposed|unrelated|earlier|previous)\s+(?:review\s+)?(?:example|excerpt|assessment|finding|text)\b|(?:^|\n)\s*(?:if|unless|suppose|imagine)\b/i;
|
||||
const finding = /\bArchitecture finding (A[1-9]\d*)\b/.exec(preface.join(' '));
|
||||
const closed = /\b(?:this|the|that) (?:finding|issue|gap|remedy|amendment|explanation|assessment) (?:is|was|has been) (?:withdrawn|rejected|cancelled|canceled|resolved|closed|hypothetical|not current|no longer current)\b|\bno current (?:gap|defect|finding) (?:remains|exists)\b|\bno longer (?:share|write|mutate)\b/i;
|
||||
if (assessment.length !== 1 || !finding || !preface.length ||
|
||||
!preface.every(line => /^Project\/branch\/task:/.test(line)) || framing.test(preface.join(' ')) ||
|
||||
/\b(?:if|unless|when|suppose|imagine)\b/i.test(preface.join(' ').slice(0, finding.index)) ||
|
||||
!q.question.split('\n').some(line => /^Project\/branch\/task:/.test(line) && new RegExp(`\\b${injected[1]}\\b`).test(line)) ||
|
||||
!/^(?:Right now|Today) both services (?:grab|import) the same global cache (?:object|instance) from a module import and both (?:write to|mutate) it\./i.test(assessment[0]![1]!) ||
|
||||
framing.test(assessment[0]![1]!) || closed.test(text)) return false;
|
||||
const letters = q.options.map(o => /^([A-D])\)\s+/.exec(o.label)?.[1]);
|
||||
const recommendation = /^Recommendation: ([A-D]) because\b/m.exec(text)?.[1];
|
||||
if (letters.some(letter => !letter) || new Set(letters).size !== letters.length ||
|
||||
!recommendation || !letters.includes(recommendation) ||
|
||||
q.options.filter(o => /\(recommended\)/i.test(o.label)).length !== 1 ||
|
||||
!q.options[letters.indexOf(recommendation)]!.label.toLowerCase().includes('(recommended)')) return false;
|
||||
const affirmative = (option: typeof q.options[number]) => {
|
||||
const body = current(option.description ?? '').trim();
|
||||
if (!/^✅/.test(body) || framing.test(body) || closed.test(body)) return [];
|
||||
return [...body.matchAll(/✅\s*([^✅❌]+)/g)].map(m => m[1]!.trim()).filter(pro =>
|
||||
!/^(?:if|unless|when|source|historical|hypothetical|example|previously)\b/i.test(pro));
|
||||
};
|
||||
const remedy = q.options.find(o => /^[A-D]\) Composition-root injection, single writer(?: \(recommended\))?$/i.test(o.label));
|
||||
const unchanged = q.options.find(o => /^[A-D]\) Do nothing(?: \(recommended\))?$/i.test(o.label));
|
||||
const owner = remedy && affirmative(remedy).map(pro => /^([A-Za-z_$][\w$]*) is the only session writer and ([A-Za-z_$][\w$]*) gets a read-only port, enforced by types not convention\./.exec(pro)).find(Boolean);
|
||||
const remaining = unchanged && current(unchanged.description ?? '').trim();
|
||||
return Boolean(owner && owner[1] !== owner[2] && remaining && /^✅/.test(remaining) &&
|
||||
!framing.test(remaining) && !closed.test(remaining) &&
|
||||
new RegExp(`❌\\s*Both ${finding[1]} failure scenarios stay live and the plan's own test coverage cannot isolate state\\.$`).test(remaining));
|
||||
}
|
||||
// The category can live in the title while the header carries the issue
|
||||
// number. Require the direct shared-state defect and technical choices;
|
||||
// an Issue heading on setup or report navigation is insufficient.
|
||||
|
||||
@@ -63,7 +63,7 @@ function ordinaryDesignIssue(fp: AskUserQuestionFingerprint): boolean {
|
||||
(fp.nativeQuestionIndex !== undefined && fp.nativeQuestionIndex !== 0)) return false;
|
||||
const q = call.questions[0]!;
|
||||
const title = q.question.split('\n')[0]!.trim();
|
||||
const issue = /^(?:D[1-9]\d*\s*[—–:-]\s*)?Issue ([1-9]\d*)(?: \((?:G[1-9]\d*, )?(?:Visual Hierarchy|Spacing|Color|Typography|Motion)\))?: ([^?]+)\?$/i.exec(title);
|
||||
const issue = /^(?:D[1-9]\d*\s*[—–:-]\s*)?Issue ([1-9]\d*)(?: \((?:(?:G[1-9]\d*|Pass [1-7]), )?(?:Visual Hierarchy|Spacing|Color|Typography|Motion)\))?: ([^?]+)\?$/i.exec(title);
|
||||
if (!issue || !new RegExp(`^Issue ${issue[1]}(?:: [A-Za-z][A-Za-z0-9 _-]{0,39})?$`, 'i').test(q.header.trim()) ||
|
||||
/<gstack-qid:/i.test(q.question) || q.multiSelect ||
|
||||
q.options.length < 2 || new Set(q.options.map(o => o.label)).size !== q.options.length ||
|
||||
@@ -72,13 +72,13 @@ function ordinaryDesignIssue(fp: AskUserQuestionFingerprint): boolean {
|
||||
// The numbered headline must ask about a concrete design requirement.
|
||||
// Reviewer participation or workflow navigation can also use Issue labels.
|
||||
if (!/\b(?:buttons?|primary actions?|hierarchy|spacing|contrast|colou?rs?|labels?|typography|fonts?|loading|spinner|skeleton|motion)\b/i.test(issue[2]!)) return false;
|
||||
const opposed = q.options.filter(o => /^(?:[1-9]\d*[A-Z](?:[).:]\s*|\s+))?(?:Defer|Leave|Keep|Accept the gap)\b/i.test(o.label));
|
||||
const opposed = q.options.filter(o => /^(?:[1-9]\d*[A-Z](?:[).:]\s*|\s+))?(?:Defer|Decline|Leave|Keep|Accept the gap)\b/i.test(o.label));
|
||||
const repair = /\b(?:fix|resolve|address)\b/i.test(title) &&
|
||||
q.options.some(o => /\b(?:closing|closes|fixes|resolves?|applies?)\b/i.test(o.description ?? ''));
|
||||
// A source citation alone can describe a report or the next reviewer.
|
||||
// Bind the alternate wording to a named control's concrete style amendment
|
||||
// and the opposed choice that leaves the documented violation unresolved.
|
||||
const primary = /^Make ([A-Za-z][A-Za-z0-9 _-]{0,39}) the (?:visible|only filled) primary action$/i.exec(issue[2]!) ??
|
||||
const primary = /^Make ([A-Za-z][A-Za-z0-9 _-]{0,39}) the (?:visible|only filled|single) primary action(?: in the header)?$/i.exec(issue[2]!) ??
|
||||
/^How should (?:the )?(?:header )?actions establish that ([A-Za-z][A-Za-z0-9 _-]{0,39}) is the primary action$/i.exec(issue[2]!);
|
||||
const amendments = primary && [
|
||||
new RegExp(`^(?:✅\\s*)?Matches DESIGN\\.md exactly: ${primary[1]} filled #[0-9a-f]{6} with (?:white|black) text; ` +
|
||||
@@ -113,6 +113,23 @@ function ordinaryDesignIssue(fp: AskUserQuestionFingerprint): boolean {
|
||||
const premiseSentence = assessment.split(/[.!?](?:\s|$)/)[0] ?? '';
|
||||
const currentPrimary = !!primaryAssessment && !/\b(?:not|never|no longer)\b/i.test(primaryAssessment) &&
|
||||
!/\b(?:archived|historical|hypothetical|quoted|example|previous|earlier)\b/i.test(premiseSentence);
|
||||
// The current assessment can state the full token contract while an offered
|
||||
// amendment names the existing component variants that implement it.
|
||||
const numberValue = (value: string) => /^\d+$/.test(value) ? Number(value) :
|
||||
['zero', 'one', 'two', 'three', 'four', 'five', 'six', 'seven', 'eight', 'nine', 'ten'].indexOf(value.toLowerCase());
|
||||
const otherControls = primary && primaryAssessment
|
||||
? primaryAssessment.replace(new RegExp(`^(?:Right now|Today) ${primary[1]},\\s*`, 'i'), '')
|
||||
.replace(/\s+(?:(?:all )?look (?:the same|identical)|are (?:all )?(?:(?:two|three|four|five|six|seven|eight|nine|ten|[1-9]\d*) )?identical buttons)$/, '')
|
||||
.split(/,\s*(?:and\s+)?|\s+and\s+/).length : 0;
|
||||
const variantContract = primary && new RegExp(`(?:^|[.!?]\\s+)DESIGN\\.md already says ${primary[1]} is the only filled button ` +
|
||||
'\\(#[0-9a-f]{6} with (?:white|black) text(?:, about [0-9]+(?:\\.[0-9]+)?:1 contrast)?\\) and the other ' +
|
||||
'(two|three|four|five|six|seven|eight|nine|ten|[1-9]\\d*) are neutral ghost buttons\\.', 'i').exec(assessment);
|
||||
const invalidContract = /(?:^|[.!?]\s+|\n)(?:Correction:\s*)?(?:this|that|the) (?:(?:DESIGN\.md|token) )?(?:requirement|contract) (?:is|was|has been) (?:withdrawn|superseded|not current|no longer current)\b/i;
|
||||
const namedContract = primary && new RegExp(`(?:^|[.!?]\\s+)DESIGN\\.md already says ${primary[1]} is the only filled primary button and the other (two|three|four|five|six|seven|eight|nine|ten|[1-9]\\d*) are neutral ghost buttons\\.`, 'i').exec(assessment);
|
||||
const statedVariant = (variantContract || namedContract) &&
|
||||
numberValue((variantContract || namedContract)![1]!) === otherControls &&
|
||||
!/\b(?:proposed|hypothetical|quoted|historical|source)\s+(?:example|contract|requirement)\b/i.test(assessment.slice(0, (variantContract || namedContract)!.index)) &&
|
||||
!invalidContract.test(questionText);
|
||||
const withdrawn = new RegExp(`(?:^|[.!?]\\s+|\\n)(?:Correction:\\s*)?(?:(?:This (?:issue|finding|question|amendment|deferral|style|fix|remedy|choice|option)|Issue ${issue[1]}) (?:is|was|has been) (?:withdrawn|resolved|closed|hypothetical)|We have (?:resolved|closed|withdrawn) this (?:issue|finding)|No current (?:issue|finding|gap|violation) (?:remains|exists))\\b`, 'i');
|
||||
const closedGap = /(?:^|[.!?;]\s+|\n)(?:Correction:\s*)?(?:this|the|that) (?:gap|violation) (?:is|was|has been) (?:already\s+|now\s+)?(?:resolved|fixed|closed)\b/i;
|
||||
const cancelledStyle = /(?:^|[.!?;]\s+|\n)(?:Correction:\s*)?(?:do not|don't|never|skip|cancel|withdraw)\s+(?:apply|use|add|keep)\s+(?:(?:these|the|this)\s+)?(?:tokens?|styles?|primary treatment)\b/i;
|
||||
@@ -124,8 +141,21 @@ function ordinaryDesignIssue(fp: AskUserQuestionFingerprint): boolean {
|
||||
q.options.some(amendment => {
|
||||
const body = currentText(amendment.description ?? '');
|
||||
const style = amendments.map(pattern => pattern.exec(body)).find(Boolean)?.[0];
|
||||
const variantLine = /^✅\s*Uses the existing Button primary and ghost variants from DESIGN\.md; no new styles\./m.exec(body);
|
||||
const benefits = variantLine ? body.slice(0, variantLine.index).trim().split('\n').filter(Boolean) : [];
|
||||
const labelledRoles = /^✅ Matches DESIGN\.md exactly: one filled primary, (two|three|four|five|six|seven|eight|nine|ten|[1-9]\d*) neutral ghosts, [1-9]\d*px targets kept\./.exec(body);
|
||||
const labelledStyle = statedVariant && namedContract && labelledRoles &&
|
||||
numberValue(labelledRoles[1]!) === otherControls &&
|
||||
new RegExp(`^[1-9]\\d*[A-Z]\\) ${primary![1]} filled #[0-9a-f]{6}/(?:white|black), others ghost(?: \\(recommended\\))?$`, 'i').test(amendment.label);
|
||||
const variantRepair = (labelledStyle || (statedVariant && variantContract && variantLine &&
|
||||
new RegExp(`^[1-9]\\d*[A-Z] Filled ${primary![1]}, ghost others(?: \\(recommended\\))?$`, 'i').test(amendment.label) &&
|
||||
benefits.every(line => /^✅\s*(?!(?:If|When|Unless|Historical|Hypothetical|Quoted|Source|Example)\b)\S/i.test(line)) &&
|
||||
!/\b(?:archived|historical|hypothetical|quoted|previous|earlier)\b/i.test(benefits.join(' ')))) &&
|
||||
!/(?:^|[.!?]\s+|\n)(?:Correction:\s*)?(?:do not|don't|never|skip|cancel|withdraw) (?:apply|use|add|keep) (?:the |these )?(?:Button )?primary and ghost variants\b/i.test(body) &&
|
||||
!/(?:^|[.!?]\s+|\n)(?:Correction:\s*)?(?:these|the|this) (?:tokens?|styles?|variants?) (?:do|does) not match DESIGN\.md\b/i.test(body) &&
|
||||
!/(?:^|[.!?]\s+|\n)(?:Correction:\s*)?(?:the|this) (?:current )?amendment keeps all (?:two|three|four|five|six|seven|eight|nine|ten|[1-9]\d*) buttons identical\b/i.test(body);
|
||||
// A named primary cannot simultaneously occur in the ghost-control list.
|
||||
if (!style || new RegExp(`\\b${primary![1]}\\b`, 'i').test(style.slice(style.indexOf(';') + 1)) ||
|
||||
if ((!style && !variantRepair) || (style && new RegExp(`\\b${primary![1]}\\b`, 'i').test(style.slice(style.indexOf(';') + 1))) ||
|
||||
sourceAssessment.test(body) || withdrawn.test(body) || closedGap.test(body) || cancelledStyle.test(body)) return false;
|
||||
return opposed.some(defer => {
|
||||
const declined = currentText(defer.description ?? '');
|
||||
@@ -135,7 +165,8 @@ function ordinaryDesignIssue(fp: AskUserQuestionFingerprint): boolean {
|
||||
const pros = /^(?:✅(?!\s*(?:If|When|Unless|Historical|Hypothetical|Quoted|Source|Example)\b)\s*[^✅❌]+)+❌\s*/i.exec(declined);
|
||||
const remaining = pros && !sourceAssessment.test(pros[0]) ? declined.slice(pros[0].length) : declined;
|
||||
return defer !== amendment && !sourceAssessment.test(declined) && !withdrawn.test(declined) && !closedGap.test(declined) &&
|
||||
/^(?:❌\s*)?(?:Leaves a documented DESIGN\.md violation in place|Ships the documented violation;[^.\n]*\bthe gap remains open|Primary-action ambiguity ships; documented DESIGN\.md violation remains|Decline the fix; gap stays documented and lowers the score|Keep all (?:two|three|four|five|six|seven|eight|nine|ten|[1-9]\d*) identical; record as an open DESIGN\.md violation)\b/i.test(remaining);
|
||||
(/^(?:❌\s*)?(?:Leaves a documented DESIGN\.md violation in place|Ships the documented violation;[^.\n]*\bthe gap remains open|Primary-action ambiguity ships; documented DESIGN\.md violation remains|Decline the fix; gap stays documented and lowers the score|Keep all (?:two|three|four|five|six|seven|eight|nine|ten|[1-9]\d*) identical; record as an open DESIGN\.md violation)\b/i.test(remaining) ||
|
||||
(variantRepair && /^(?:Violates DESIGN\.md and leaves users guessing which action is primary; Pass [1-7] stays at [0-9](?:\.[0-9]+)?\/10|Documented DESIGN\.md violation ships and Pass [1-7] stays at [0-9](?:\.[0-9]+)?\/10)\.$/i.test(remaining)));
|
||||
});
|
||||
});
|
||||
return opposed.length > 0 && !!(repair || primaryRepair);
|
||||
|
||||
@@ -38,14 +38,48 @@ function explainedReversedSignatures(q: NativePlanQuestion, title: string): bool
|
||||
|
||||
/** Identify a dedicated seed decision by its subject and meaningful alternatives. */
|
||||
function decisionGaps(q: NativePlanQuestion): DevexSeededGap[] {
|
||||
const title = q.question.split('\n')[0]!.trim().replace(/^D\s*\d+\s*[—–:-]\s*/i, '')
|
||||
.replace(/`([^`\n]+)`/g, '$1');
|
||||
if (!title.endsWith('?') || (title.match(/\?/g)?.length ?? 0) !== 1 ||
|
||||
const rawTitle = q.question.split('\n')[0]!.trim().replace(/^D\s*\d+\s*[—–:-]\s*/i, '');
|
||||
const title = rawTitle.replace(/`([^`\n]+)`/g, '$1');
|
||||
const questionMarks = title.match(/\?/g)?.length ?? 0;
|
||||
// A native choice can present an asserted defect as its title. Punctuation
|
||||
// does not turn its concrete offered repairs into a missing review decision.
|
||||
const declaration = questionMarks === 0 &&
|
||||
/^(?:[A-Za-z0-9_.]+\s+){1,12}(?:points?|references?|blocks?|requires?|takes?|raises?|removes?|drops?)\b/i.test(title) &&
|
||||
!/^`[^`]*`$/.test(rawTitle) &&
|
||||
!/\b(?:if|unless|suppose|might|may|could|would|previously|earlier|historical|hypothetical|example|quoted|source|never|no longer|does not|do not|did not)\b/i.test(title);
|
||||
if ((!declaration && (!title.endsWith('?') || questionMarks !== 1)) ||
|
||||
/^(?:>|"|“|Example\b|Quoted\b|Suppose\b)|\bhypothetical\b/i.test(title) ||
|
||||
/\b(?:continue|proceed|next section|move on|format|already (?:fixed|resolved))\b/i.test(title) ||
|
||||
/\b(?:have|did)\b[^?]*\bread\b|\b(?:narrative|trace|recap|summary)\b[^?]*\b(?:accurate|match|confirm)\b/i.test(title) ||
|
||||
/\b(?:report|summary|recap)\b[^?]*\b(?:mention|include|reference|list)\b|\b(?:mention|include|reference|list)\b[^?]*\b(?:report|summary|recap)\b/i.test(title)) return [];
|
||||
const options = q.options.map(o => `${o.label} ${o.description ?? ''}`);
|
||||
let offered = q.options;
|
||||
if (declaration) {
|
||||
const currentProse = (text: string) => {
|
||||
let fence = false;
|
||||
return text.split('\n').filter(line => {
|
||||
if (/^\s*(?:```|~~~)/.test(line)) { fence = !fence; return false; }
|
||||
return !fence && !/^\s*>/.test(line);
|
||||
}).join('\n')
|
||||
.replace(/(^|[.!?\n]\s*)((?:Correction:\s*)?(?:this|that|the) (?:finding|issue|gap|defect|explanation|option|action|correction) (?:is|was|has been) )["“](withdrawn|rejected|(?:already )?(?:fixed|resolved)|historical|not current|cancelled)["”]/gim, '$1$2$3')
|
||||
.replace(/`[^`\n]*`|"[^"\n]*"|“[^”\n]*”/g, '');
|
||||
};
|
||||
const sourceFrame = /(?:^|[.!?\n;]\s*)(?:(?:ELI10|Project\/branch\/task):\s*)?(?:(?:Source(?: excerpt| example)?|Quoted(?: source| example)?|Historical(?: example| assessment)?(?: only)?|Earlier(?: review)? assessment|Example|Hypothetical(?: example| assessment)?|If approved|If accepted)[,:]|(?:The following|This assessment|This explanation)\b[^.\n]*\b(?:quoted|source|historical|hypothetical|example)\b|Historically,)/i;
|
||||
const lines = q.question.split('\n'), explanation = lines.findIndex(line => /^ELI10:/.test(line));
|
||||
const preface = lines.slice(0, explanation < 0 ? undefined : explanation + 1).join('\n');
|
||||
if (/^\s*(?:```|~~~)/m.test(preface) || sourceFrame.test(currentProse(preface)) ||
|
||||
/\bnot (?:a )?current (?:finding|issue|defect)\b/i.test(currentProse(preface))) return [];
|
||||
const current = currentProse(q.question);
|
||||
if (/(?:^|[.!?\n]\s*)(?:Correction:\s*)?(?:(?:this|that|the) (?:finding|issue|gap|defect|explanation) (?:is|was|has been) (?:withdrawn|rejected|(?:already )?(?:fixed|resolved)|historical|not current|(?:a |only a )?source example)|there is no (?:current )?(?:finding|issue|gap|defect))\b/i.test(current)) return [];
|
||||
// A declaration's action evidence must belong to a current offered option,
|
||||
// rather than an example or an explicitly withdrawn correction.
|
||||
const action = (text: string) => currentProse(text.replace(/`([A-Za-z_$][\w.$/-]*(?:\([^`\n]*\))?)`/g, '$1'));
|
||||
offered = offered.filter(option => {
|
||||
const text = `${option.label}\n${option.description ?? ''}`, prose = currentProse(text);
|
||||
return !/^(?:>|"|“)|^`[^`]*`$/.test(option.label.trim()) && !sourceFrame.test(prose) &&
|
||||
!/(?:^|[.!?\n]\s*)(?:Correction:\s*)?(?:this|the) (?:option|action|correction) (?:is|was|has been) (?:withdrawn|rejected|cancelled)\b/i.test(prose);
|
||||
}).map(option => ({ ...option, label: action(option.label), description: action(option.description ?? '') }));
|
||||
}
|
||||
const options = offered.map(o => `${o.label} ${o.description ?? ''}`);
|
||||
const labels = q.options.map(o => o.label.trim().replace(/\s*\(recommended\)$/i, '').toLowerCase());
|
||||
const yesNo = labels.length === 2 && labels.includes('yes') && labels.includes('no');
|
||||
// A terse Yes/No panel still resolves an action explicitly asked in the
|
||||
@@ -59,16 +93,20 @@ function decisionGaps(q: NativePlanQuestion): DevexSeededGap[] {
|
||||
(options.some(o => /\b(?:no CI gate|remove|move|skip|bypass|gate)\b/i.test(o) && /\b(?:CI|check|gate|local|demo)\b/i.test(o)) || directAction('remove|move|skip|bypass|gate'))) found.push('local-ci-gate');
|
||||
if (/\bquickstart\b|examples\/first_eval\.py/i.test(title) &&
|
||||
/\b(?:README|file|example|demo|missing|absent|package|wheel|ship|point)\b|first_eval\.py/i.test(title) &&
|
||||
(!declaration || /\b(?:not (?:shipped|included|available|present)|missing|absent|nonexistent|does not exist)\b/i.test(title)) &&
|
||||
(options.some(o => /\b(?:point|ship|add|demo is)\b/i.test(o) && /\bquickstart\b|first_eval\.py/i.test(o)) || directAction('point|ship|add|replace|fix'))) found.push('missing-quickstart');
|
||||
if (explainedReversedSignatures(q, title) || (/\brun_eval\b/i.test(title) && /\brun_batch\b/i.test(title) &&
|
||||
/\b(?:arguments?|order|positional|reversed|opposite|consistent|align|unify|dataset|evaluator)\b/i.test(title) &&
|
||||
(!declaration || /\b(?:reversed|opposite|swapped|inconsistent)\b/i.test(title)) &&
|
||||
(options.some(o => /\b(?:align|unify|standardize|keyword|swap)\b/i.test(o) && /\b(?:order|dataset|arguments?|positional)\b/i.test(o)) || directAction('align|unify|standardize|enforce|make')))) found.push('reversed-arguments');
|
||||
if (/\bAuthError\b|\binvalid API key\b/i.test(title) &&
|
||||
/\b(?:error|message|code|cause|fix|guidance|opaque|explain)\b|request failed/i.test(title) &&
|
||||
(options.some(o => /\bcode\b/i.test(o) && /\b(?:cause|fix|link)\b/i.test(o)) || directAction('add|include|explain|replace|report|give'))) found.push('opaque-auth-error');
|
||||
(!declaration || /\b(?:no (?:cause|fix|explanation|code)|opaque)\b|request failed/i.test(title)) &&
|
||||
(options.some(o => /\bcodes?\b/i.test(o) && /\b(?:cause|fix|link)\b/i.test(o)) || directAction('add|include|explain|replace|report|give'))) found.push('opaque-auth-error');
|
||||
if (/Client\.evaluate\b/i.test(title) &&
|
||||
/Client\.run\b|\b(?:v\d+|version \d+|alias|deprecation|migration)\b/i.test(title) &&
|
||||
/\b(?:alias|warning|compatibility|deprecat\w*|migration|remov\w*|rename|keep)\b/i.test(title) &&
|
||||
(!declaration || /\b(?:no |without (?:a )?)(?:compatibility )?(?:alias|warning|migration (?:guide|path))\b/i.test(title)) &&
|
||||
(options.some(o => /\balias\b/i.test(o) && /\b(?:warning|DeprecationWarning|migration)\b/i.test(o)) || directAction('keep|add|preserve|provide|retain'))) found.push('breaking-upgrade');
|
||||
return found;
|
||||
}
|
||||
|
||||
@@ -43,7 +43,19 @@ function seedSubjects(q: NativePlanQuestionCall['questions'][number]): Seed[] {
|
||||
&& /^(?:Even after injection,\s*)?(?:both|the two) services (?:write into|mutate) the same cache\./i.test(adapterExplanation)
|
||||
&& !/\b(?:(?:this|that|the) (?:issue|finding|race)|Issue\s+[1-9]\d*)\s+(?:is|was|has been)\s+(?:withdrawn|retracted|rejected|resolved|fixed)\b|\bno (?:current )?shared-cache race\b/i.test(adapterAssessment);
|
||||
const ids: Seed[] = [];
|
||||
if (/\b(?:scope|complexity|classes|types|abstractions)\b/i.test(title) &&
|
||||
// The inventory alias needs its own current action; an inline quoted title
|
||||
// or actions reproduced only as source material cannot establish this seed.
|
||||
const inventoryProse = prose(q.question, true)
|
||||
.replace(/(\b(?:this|the) class[ -]inventory decision is )['"“](withdrawn|rejected|cancelled|canceled|resolved)['"”]/gi, '$1$2')
|
||||
.replace(/"[^"\n]*"|“[^”\n]*”/g, '');
|
||||
const inventoryAction = q.options.some(o => {
|
||||
const label = prose(o.label, true), description = prose(o.description ?? '', true).replace(/"[^"\n]*"|“[^”\n]*”/g, '');
|
||||
return /^(?:[A-D][).:—–-]\s*)?(?:reduce|cut|simplify|remove|collapse|merge)\b[^.!?\n]{0,160}\b(?:classes|types|abstractions)\b/i.test(label)
|
||||
&& !/^(?:source|quoted|historical|example|if approved|hypothetical)\b|\b(?:this|the) (?:option|action|remedy) is (?:withdrawn|rejected|cancelled|canceled)\b/i.test(description);
|
||||
});
|
||||
const classInventory = /^(?:D[1-9]\d*\s*[—–:-]\s*)?(?:Reduce|Simplify|Trim) the class inventory(?: before building)?\?$/i.test(prose(subject, true))
|
||||
&& inventoryAction && !/\b(?:this|the) class[ -]inventory decision (?:is|was|has been) (?:withdrawn|rejected|cancelled|canceled|resolved|not current)\b/i.test(inventoryProse);
|
||||
if (classInventory || /\b(?:scope|complexity|classes|types|abstractions)\b/i.test(title) &&
|
||||
/\b(?:files|classes|types|abstractions)\b/i.test(title) &&
|
||||
action(/\b(?:reduce|cut|simplify|remove|collapse|merge|pure function)\b/i)) ids.push('complexity');
|
||||
if ((sharedAdapter || /\b(?:AuthCache|cache)\b/i.test(title) &&
|
||||
@@ -272,6 +284,84 @@ function declaredLegacyCharacterization(text: string): boolean {
|
||||
}
|
||||
}
|
||||
}
|
||||
// A current-output characterization declaration can bind the same file
|
||||
// and pre-rewrite task directly, without a separate Verification heading.
|
||||
for (const section of current.filter(s => /^REGRESSION \(mandatory, authorized by the coverage-audit regression rule [—–-] no question asked\)$/i.test(s.title))) {
|
||||
const body = unquoted(section.body.join('\n')).trim();
|
||||
const split = body.indexOf('\n- ');
|
||||
if (split < 0 || suiteWithdrawn) continue;
|
||||
const intro = body.slice(0, split).replace(/\s+/g, ' ').trim();
|
||||
if (!/^legacyAuthFlow\(\) is existing behavior being rewritten\b[^!?]{1,400}\. CRITICAL requirement added to the plan:$/.test(intro) ||
|
||||
goldenSourceOwner(intro) || /\b(?:if|unless|maybe|might|could|proposed|optional|hypothetical|unproven)\b/i.test(intro)) continue;
|
||||
const claim = body.slice(split).replace(/\s+/g, ' ').trim();
|
||||
const declaration = /^- ([A-Za-z][\w/.-]*\.test\.[jt]s) [—–-] record current outputs for: [^.!?]{1,600}\. Assert the new path \(behind the flag\) produces identical decisions and equivalent error surfaces\. These tests are written BEFORE any rewrite \((T[1-9]\d*)\) and stay green through cut-over\.$/.exec(claim);
|
||||
if (!declaration || withdrawn(body) || !snapshotSource.includes(claim) ||
|
||||
/\b(?:if|unless|maybe|might|could|proposed|optional|hypothetical|unproven)\b/i.test(claim)) continue;
|
||||
for (const section of current.filter(s => s.title === 'Implementation Tasks')) {
|
||||
const taskBody = section.body.join('\n').trim(), blocks = taskBody.split(/\n(?=-\s)/);
|
||||
const prefix = blocks[0]?.trim() ?? '';
|
||||
if (!taskBody.startsWith('- ') && (!/^Synthesized from (?:this|the) review's findings\./.test(prefix) ||
|
||||
/\b(?:if|unless|optional|hypothetical|example|source|quoted|unproven)\b/i.test(unquoted(prefix)))) continue;
|
||||
for (const task of blocks) {
|
||||
const match = /^- (?:\[[ xX]\] )?(T[1-9]\d*)(?: \([^\n)]*\))? [—–-] [A-Za-z][\w/-]* [—–-] Write characterization tests for legacyAuthFlow\(\) before any rewrite[\t ]*(?:\n|$)/.exec(task);
|
||||
const file = /^\s+- Files: ([^\n]+)$/m.exec(task);
|
||||
const verify = /^\s+- Verify: suite green on current main; re-run after each later task[\t ]*$/m.exec(task);
|
||||
const beforeTask = unquoted(taskBody.slice(0, taskBody.indexOf(task))).trim().split('\n').at(-1) ?? '';
|
||||
if (!match || match[1] !== declaration[2] || file?.[1] !== declaration[1] || !verify ||
|
||||
!snapshotSource.includes(task.replace(/\s+/g, ' ').trim()) ||
|
||||
conditionalOwner(beforeTask) || goldenSourceOwner(beforeTask) ||
|
||||
conditionalOwner(task.slice(0, verify.index)) || goldenSourceOwner(unquoted(task.slice(0, verify.index))) ||
|
||||
withdrawn(unquoted(task).replace(/\b(?:this|that|the)\s+(?:(?:unchanged-code|baseline)\s+)?verification\b/gi, 'this requirement'), match[1])) continue;
|
||||
const cancelledTask = new RegExp(`\\b${match[1]}(?:\\s+(?:rerun|verification|baseline verification))?\\s+(?:is|was|has been)\\s+(?:cancelled|canceled|withdrawn|rejected|deferred|optional|not required|no longer required)\\b`, 'i');
|
||||
const changedBeforeBaseline = new RegExp(`^(?:Correction:\\s*)?legacyAuthFlow\\(\\) (?:is|was|has been|will be) (?:modified|changed|rewritten|refactored) before ${match[1]}\\b`, 'i');
|
||||
const assessment = (body: string) => unquoted(body.replace(new RegExp(`(\\b${match[1]}(?:\\s+(?:rerun|verification|baseline verification))?\\s+(?:is|was|has been)\\s+)["“](withdrawn|rejected|cancelled|canceled)["”]`, 'gi'), '$1$2'));
|
||||
if (!current.some(s => cancelledTask.test(assessment(s.body.join('\n'))) ||
|
||||
assessment(s.body.join('\n')).split(/\n|[.!?]\s+/).some(statement => changedBeforeBaseline.test(statement.trim())))) return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
// The same mandatory characterization can precede a behavior-preserving
|
||||
// extraction: its untouched baseline and the extraction's rerun share a task ID.
|
||||
const extractionSourceOwner = (body: string) => goldenSourceOwner(body) ||
|
||||
/(?:^|\n)\s*(?:(?:quoted )?source(?: (?:excerpt|text|material))?|(?:historical|earlier|previous)(?: review)?(?: assessment)?|(?:hypothetical )?example):\s*(?:\n|$)/i.test(unquoted(body));
|
||||
for (const section of current.filter(s => s.title === 'Tests')) {
|
||||
const body = unquoted(section.body.join('\n'));
|
||||
const claim = /^CRITICAL [—–-] regression rule \(mandatory, not a decision\): (T[1-9]\d*) adds a characterization test for legacyAuthFlow\(\)'s current behavior \([^\n)]{1,300}\) and lands before the ([1-9]\d*[A-D]) extraction\./m.exec(body);
|
||||
if (!claim || suiteWithdrawn || withdrawn(body, claim[1]) || !snapshotSource.includes(claim[0]) ||
|
||||
extractionSourceOwner(body.slice(0, claim.index)) || conditionalOwner(body.slice(0, claim.index))) continue;
|
||||
for (const section of current.filter(s => s.title === 'Implementation Tasks')) {
|
||||
const taskBody = section.body.join('\n').trim(), blocks = taskBody.split(/\n(?=-\s)/);
|
||||
const prefix = blocks[0]?.trim() ?? '';
|
||||
if (!taskBody.startsWith('- ') && (!/^Synthesized from (?:this|the) review's findings\./.test(prefix) ||
|
||||
extractionSourceOwner(unquoted(prefix)) || conditionalOwner(prefix))) continue;
|
||||
const active = (task: string, id: string, verifyAt: number) => {
|
||||
const beforeTask = unquoted(taskBody.slice(0, taskBody.indexOf(task))).trim().split('\n').at(-1) ?? '';
|
||||
return snapshotSource.includes(task.replace(/\s+/g, ' ').trim()) &&
|
||||
!conditionalOwner(beforeTask) && !extractionSourceOwner(beforeTask) &&
|
||||
!conditionalOwner(task.slice(0, verifyAt)) && !extractionSourceOwner(unquoted(task.slice(0, verifyAt))) &&
|
||||
!withdrawn(unquoted(task).replace(/\b(?:this|that|the)\s+(?:(?:unchanged-code|baseline)\s+)?verification\b/gi, 'this requirement'), id);
|
||||
};
|
||||
for (const baseline of blocks) {
|
||||
const task = /^- (?:\[[ xX]\] )?(T[1-9]\d*)(?: \([^\n)]*\))? [—–-] [A-Za-z][\w/-]* [—–-] Add characterization\/regression test for legacyAuthFlow\(\) current behavior[\t ]*(?:\n|$)/.exec(baseline);
|
||||
const file = /^\s+- Files: ([A-Za-z][\w/.-]*\.test\.[jt]s)$/m.exec(baseline);
|
||||
const verify = /^\s+- Verify: test passes against unmodified legacy before any other commit[\t ]*$/m.exec(baseline);
|
||||
if (!task || task[1] !== claim[1] || !file || !verify || !active(baseline, task[1], verify.index)) continue;
|
||||
for (const extraction of blocks) {
|
||||
const task2 = /^- (?:\[[ xX]\] )?(T[1-9]\d*)(?: \([^\n)]*\))? [—–-] [A-Za-z][\w/-]* [—–-] Extract IDP checks \+ token validation into shared ([A-Za-z][\w]*)\(\); legacy calls it, behavior unchanged[\t ]*(?:\n|$)/.exec(extraction);
|
||||
const origin = /^\s+- Surfaced by: Code quality Issue [1-9]\d* \(D[1-9]\d*, ([1-9]\d*[A-D])\)[\t ]*$/m.exec(extraction);
|
||||
const rerun = /^\s+- Verify: (T[1-9]\d*) still green; diff to legacy is call-site only[\t ]*$/m.exec(extraction);
|
||||
if (!task2 || task2[1] === task[1] || origin?.[1] !== claim[2] || rerun?.[1] !== task[1] || !active(extraction, task2[1], rerun.index)) continue;
|
||||
const canceled = new RegExp(`\\b(?:${task[1]}|${task2[1]})(?:\\s+(?:rerun|verification|baseline verification|regression test|characterization test))?\\s+(?:is|was|has been)\\s+(?:cancelled|canceled|withdrawn|rejected|deferred|optional|not required|no longer required)\\b`, 'i');
|
||||
const changedFirst = new RegExp(`^(?:Correction:\\s*)?legacyAuthFlow\\(\\) (?:is|was|has been|will be) (?:modified|changed|rewritten|refactored) before ${task[1]}\\b`, 'i');
|
||||
const assessment = (value: string) => unquoted(value.replace(new RegExp(`(\\b(?:${task[1]}|${task2[1]})(?:\\s+(?:rerun|verification|baseline verification|regression test|characterization test))?\\s+(?:is|was|has been)\\s+)["“](withdrawn|rejected|cancelled|canceled)["”]`, 'gi'), '$1$2'));
|
||||
const rerunWithdrawn = new RegExp(`\\b${task2[1]} (?:no longer|does not|will not) reruns? ${task[1]}\\b`, 'i');
|
||||
if (!current.some(s => canceled.test(assessment(s.body.join('\n'))) || rerunWithdrawn.test(assessment(s.body.join('\n'))) ||
|
||||
assessment(s.body.join('\n')).split(/\n|[.!?]\s+/).some(line => changedFirst.test(line.trim())))) return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
|
||||
@@ -28,6 +28,9 @@ export interface NativePublicToolEvent {
|
||||
toolUseId: string;
|
||||
kind: 'use' | 'result';
|
||||
name?: string;
|
||||
/** Exact native message/request identity, used only for owned queued tools. */
|
||||
messageId?: string;
|
||||
requestId?: string;
|
||||
input?: Record<string, unknown>;
|
||||
content?: unknown;
|
||||
file?: unknown;
|
||||
@@ -169,8 +172,11 @@ export function readPlanCountTranscript(configDir: string, cwd: string,
|
||||
if (onPublicToolEvent && validTimestamp(record.timestamp)) {
|
||||
if (record.message.role === 'assistant' && block.type === 'tool_use' &&
|
||||
typeof block.id === 'string' && typeof block.name === 'string' && object(block.input)) {
|
||||
const batch = typeof record.message.id === 'string' && /^msg_[A-Za-z0-9_-]{1,160}$/.test(record.message.id) &&
|
||||
typeof record.requestId === 'string' && /^req_[A-Za-z0-9_-]{1,160}$/.test(record.requestId)
|
||||
? { messageId: record.message.id, requestId: record.requestId } : {};
|
||||
onPublicToolEvent({ sessionId: record.sessionId, timestamp: record.timestamp,
|
||||
toolUseId: block.id, kind: 'use', name: block.name, input: block.input });
|
||||
toolUseId: block.id, kind: 'use', name: block.name, input: block.input, ...batch });
|
||||
} else if (record.message.role === 'user' && block.type === 'tool_result' &&
|
||||
typeof block.tool_use_id === 'string') {
|
||||
onPublicToolEvent({ sessionId: record.sessionId, timestamp: record.timestamp,
|
||||
|
||||
@@ -194,6 +194,8 @@ export const E2E_TOUCHFILES: Record<string, string[]> = {
|
||||
"test/helpers/autoplan-artifact-digest.ts",
|
||||
"test/autoplan-edit-digests-al.test.ts",
|
||||
"test/fixtures/autoplan-edit-digests-al.json",
|
||||
"test/autoplan-edit-queue-am.test.ts",
|
||||
"test/fixtures/autoplan-edit-queue-am.json",
|
||||
],
|
||||
|
||||
// Per-finding AskUserQuestion count + review-report-at-bottom assertion.
|
||||
@@ -211,6 +213,8 @@ export const E2E_TOUCHFILES: Record<string, string[]> = {
|
||||
'test/fixtures/plan-count-quoted-frame-ak.json',
|
||||
"test/ceo-decision-prefix-al.test.ts",
|
||||
"test/fixtures/ceo-decision-prefix-al.json",
|
||||
"test/ceo-assertion-header-am.test.ts",
|
||||
"test/fixtures/ceo-assertion-header-am-calls.json",
|
||||
],
|
||||
'plan-eng-finding-count': ['bin/gstack-config', 'bin/gstack-skill-start', 'bin/gstack-skill-end', 'plan-eng-review/**', 'scripts/resolvers/preamble.ts', 'scripts/resolvers/preamble/generate-ask-user-format.ts', 'scripts/resolvers/preamble/generate-completion-status.ts', 'test/helpers/claude-pty-runner.ts', 'test/plan-count-native-input.test.ts', 'test/helpers/pty-screen.ts', 'test/pty-screen.test.ts', 'test/pty-screen-session.test.ts', 'test/fixtures/pty-screen/**', 'test/helpers/hermetic-skill-runtime.ts', 'test/hermetic-skill-runtime.test.ts', 'test/helpers/claude-pty-runner.unit.test.ts', 'test/plan-count-completion.test.ts', 'test/plan-count-dx-handoff.test.ts', 'test/fixtures/devex-handoff-n-call.json', 'test/helpers/pty-trust-dialog.ts', 'test/pty-trust-dialog.test.ts', 'test/helpers/plan-count-transcript.ts', 'test/autoplan-public-narration.test.ts', 'test/fixtures/autoplan-public-narration-ad.json', 'test/helpers/plan-count-pending-exit.ts', 'test/plan-count-pending-exit.test.ts', 'test/plan-count-transcript.test.ts', 'test/helpers/plan-count-artifacts.ts', 'test/plan-count-artifacts.test.ts', 'test/helpers/eval-store.ts', 'test/helpers/plan-count-fixture.ts', 'test/plan-count-fixture.test.ts', 'test/plan-count-timeout.test.ts', 'test/plan-count-navigation-r.test.ts', 'test/plan-count-prerequisite-n.test.ts', 'test/fixtures/ceo-prerequisite-n-call.json', 'test/skill-e2e-plan-eng-finding-count.test.ts', 'test/fixtures/devex-review-l-calls.json', 'test/plan-count-checkbox.test.ts', 'test/fixtures/ceo-checkbox-l.screen.txt', 'test/plan-count-empty-review.test.ts', 'test/fixtures/ceo-count-s-distinct.json', 'test/fixtures/plan-count-design-questionless-report.md', 'test/plan-count-dx-handoff-o.test.ts', 'test/fixtures/devex-handoff-o-call.json', 'test/eng-devex-s-count.test.ts', 'test/fixtures/eng-devex-s-first-calls.json', 'test/fixtures/eng-devex-s-retry-calls.json', 'test/helpers/plan-count-file-permission.ts', 'test/plan-count-file-permission.test.ts', 'test/fixtures/plan-count-edit-permission-t.json', 'test/eng-first-review-t.test.ts', 'test/fixtures/eng-batching-t-calls.json', 'test/plan-count-preview-footer.test.ts', 'test/fixtures/ceo-preview-u-call.json', 'test/fixtures/ceo-preview-u-screen.txt', 'test/fixtures/design-preview-v-screen.txt', 'test/plan-count-owned-permission.test.ts', 'test/fixtures/plan-count-owned-permission-v.json', 'test/fixtures/ceo-questionless-w-native.json', 'test/eng-scope-y.test.ts', 'test/fixtures/eng-scope-y-calls.json', 'test/plan-count-truncated-question.test.ts', 'test/fixtures/ceo-approach-z-call.json', 'test/fixtures/ceo-approach-z-screen.txt', 'test/eng-binding-z.test.ts', 'test/fixtures/eng-binding-z-calls.json', 'test/eng-binding-retry-z.test.ts', 'test/fixtures/eng-binding-retry-z-calls.json', 'test/plan-count-permission-ac.test.ts', 'test/fixtures/plan-count-permission-ac.json', 'test/fixtures/plan-count-permission-ad.json', 'test/fixtures/plan-count-permission-ae.json', 'test/fixtures/plan-count-permission-target-ad-v2.json', 'test/helpers/eng-completion-handoff.ts', 'test/eng-count-ad-v2.test.ts', 'test/fixtures/eng-count-ad-v2.json', 'test/helpers/eng-seeded-coverage.ts', 'test/eng-seeded-coverage.test.ts', 'test/eng-seeded-packet-ae.test.ts', 'test/fixtures/eng-seeded-packet-ae.json', 'test/eng-first-category-af.test.ts', 'test/fixtures/eng-first-category-af.json', 'test/eng-regression-pinning-ag.test.ts', 'test/fixtures/eng-regression-pinning-ag.json', 'test/fixtures/plan-count-permission-ah.json', 'test/eng-next-handoff-ah.test.ts', 'test/fixtures/eng-next-handoff-ah.json', 'test/eng-declared-regression-ai.test.ts', 'test/fixtures/eng-declared-regression-ai.json', 'test/eng-snapshot-adapter-aj.test.ts', 'test/fixtures/eng-snapshot-adapter-aj.json',
|
||||
'test/eng-declared-suite-ak.test.ts',
|
||||
@@ -221,6 +225,12 @@ export const E2E_TOUCHFILES: Record<string, string[]> = {
|
||||
'test/fixtures/plan-count-quoted-frame-ak.json',
|
||||
"test/eng-golden-master-al.test.ts",
|
||||
"test/fixtures/eng-golden-master-al.json",
|
||||
"test/eng-cache-brief-am.test.ts",
|
||||
"test/fixtures/eng-cache-brief-am.json",
|
||||
"test/eng-legacy-contract-am.test.ts",
|
||||
"test/fixtures/eng-legacy-contract-am.json",
|
||||
"test/eng-retry-contract-am.test.ts",
|
||||
"test/fixtures/eng-retry-contract-am.json",
|
||||
],
|
||||
'plan-design-finding-count': ['bin/gstack-config', 'bin/gstack-skill-start', 'bin/gstack-skill-end', 'plan-design-review/**', 'scripts/resolvers/preamble.ts', 'scripts/resolvers/preamble/generate-ask-user-format.ts', 'scripts/resolvers/preamble/generate-completion-status.ts', 'test/helpers/claude-pty-runner.ts', 'test/plan-count-native-input.test.ts', 'test/helpers/pty-screen.ts', 'test/pty-screen.test.ts', 'test/pty-screen-session.test.ts', 'test/fixtures/pty-screen/**', 'test/helpers/hermetic-skill-runtime.ts', 'test/hermetic-skill-runtime.test.ts', 'test/helpers/claude-pty-runner.unit.test.ts', 'test/plan-count-completion.test.ts', 'test/plan-count-dx-handoff.test.ts', 'test/fixtures/devex-handoff-n-call.json', 'test/helpers/pty-trust-dialog.ts', 'test/pty-trust-dialog.test.ts', 'test/helpers/plan-count-transcript.ts', 'test/autoplan-public-narration.test.ts', 'test/fixtures/autoplan-public-narration-ad.json', 'test/helpers/plan-count-pending-exit.ts', 'test/plan-count-pending-exit.test.ts', 'test/plan-count-transcript.test.ts', 'test/helpers/plan-count-artifacts.ts', 'test/plan-count-artifacts.test.ts', 'test/helpers/eval-store.ts', 'test/helpers/plan-count-fixture.ts', 'test/plan-count-fixture.test.ts', 'test/plan-count-timeout.test.ts', 'test/plan-count-navigation-r.test.ts', 'test/plan-count-prerequisite-n.test.ts', 'test/fixtures/ceo-prerequisite-n-call.json', 'test/helpers/design-count-review.ts', 'test/design-count-review.test.ts', 'test/fixtures/design-review-j-calls.json', 'test/helpers/design-count-outside.ts', 'test/design-count-outside.test.ts', 'test/skill-e2e-plan-design-finding-count.test.ts', 'test/fixtures/design-review-l-calls.json', 'test/design-completion-handoff.test.ts', 'test/fixtures/design-handoff-l-calls.json', 'test/fixtures/devex-review-l-calls.json', 'test/plan-count-checkbox.test.ts', 'test/fixtures/ceo-checkbox-l.screen.txt', 'test/fixtures/design-review-n-calls.json', 'test/design-completion-handoff-scored.test.ts', 'test/fixtures/design-handoff-n-calls.json', 'test/fixtures/design-handoff-q-calls.json', 'test/plan-count-empty-review.test.ts', 'test/fixtures/ceo-count-s-distinct.json', 'test/fixtures/plan-count-design-questionless-report.md', 'test/plan-count-dx-handoff-o.test.ts', 'test/fixtures/devex-handoff-o-call.json', 'test/helpers/plan-count-file-permission.ts', 'test/plan-count-file-permission.test.ts', 'test/fixtures/plan-count-edit-permission-t.json', 'test/plan-count-preview-footer.test.ts', 'test/fixtures/ceo-preview-u-call.json', 'test/fixtures/ceo-preview-u-screen.txt', 'test/design-completion-handoff-u.test.ts', 'test/fixtures/design-handoff-u-calls.json', 'test/fixtures/design-preview-v-screen.txt', 'test/plan-count-owned-permission.test.ts', 'test/fixtures/plan-count-owned-permission-v.json', 'test/fixtures/ceo-questionless-w-native.json', 'test/helpers/design-artifact-question.ts', 'test/design-artifact-question.test.ts', 'test/fixtures/design-artifacts-w-calls.json', 'test/fixtures/design-outside-y-calls.json', 'test/fixtures/design-boundaries-y-calls.json', 'test/plan-count-truncated-question.test.ts', 'test/fixtures/ceo-approach-z-call.json', 'test/fixtures/ceo-approach-z-screen.txt', 'test/fixtures/design-gap-z-calls.json', 'test/plan-count-permission-ac.test.ts', 'test/fixtures/plan-count-permission-ac.json', 'test/fixtures/plan-count-permission-ad.json', 'test/fixtures/plan-count-permission-ae.json', 'test/fixtures/plan-count-permission-target-ad-v2.json', 'test/design-count-ad-v2.test.ts', 'test/fixtures/design-count-ad-v2.json', 'test/design-first-decision-af.test.ts', 'test/fixtures/design-first-decision-af.json', 'test/fixtures/design-first-decision-af-retry.json', 'test/fixtures/plan-count-permission-ah.json', 'test/design-first-issue-ai.test.ts', 'test/fixtures/design-first-issue-ai.json', 'test/design-primary-action-aj.test.ts', 'test/fixtures/design-primary-action-aj.json', 'test/fixtures/design-future-todo-aj.json',
|
||||
'test/design-primary-contract-ak.test.ts',
|
||||
@@ -231,6 +241,9 @@ export const E2E_TOUCHFILES: Record<string, string[]> = {
|
||||
'test/fixtures/plan-count-quoted-frame-ak.json',
|
||||
"test/design-primary-decision-al.test.ts",
|
||||
"test/fixtures/design-primary-decision-al.json",
|
||||
"test/design-variant-choice-am.test.ts",
|
||||
"test/fixtures/design-variant-choice-am.json",
|
||||
"test/fixtures/design-variant-choice-am-retry.json",
|
||||
],
|
||||
'plan-devex-finding-count': ['bin/gstack-config', 'bin/gstack-skill-start', 'bin/gstack-skill-end', 'plan-devex-review/**', 'scripts/resolvers/preamble.ts', 'scripts/resolvers/preamble/generate-ask-user-format.ts', 'scripts/resolvers/preamble/generate-completion-status.ts', 'test/helpers/claude-pty-runner.ts', 'test/plan-count-native-input.test.ts', 'test/helpers/pty-screen.ts', 'test/pty-screen.test.ts', 'test/pty-screen-session.test.ts', 'test/fixtures/pty-screen/**', 'test/helpers/hermetic-skill-runtime.ts', 'test/hermetic-skill-runtime.test.ts', 'test/helpers/claude-pty-runner.unit.test.ts', 'test/plan-count-completion.test.ts', 'test/plan-count-dx-handoff.test.ts', 'test/fixtures/devex-handoff-n-call.json', 'test/helpers/pty-trust-dialog.ts', 'test/pty-trust-dialog.test.ts', 'test/helpers/plan-count-transcript.ts', 'test/autoplan-public-narration.test.ts', 'test/fixtures/autoplan-public-narration-ad.json', 'test/helpers/plan-count-pending-exit.ts', 'test/plan-count-pending-exit.test.ts', 'test/plan-count-transcript.test.ts', 'test/helpers/plan-count-artifacts.ts', 'test/plan-count-artifacts.test.ts', 'test/helpers/eval-store.ts', 'test/helpers/plan-count-fixture.ts', 'test/plan-count-fixture.test.ts', 'test/plan-count-timeout.test.ts', 'test/plan-count-navigation-r.test.ts', 'test/plan-count-prerequisite-n.test.ts', 'test/fixtures/ceo-prerequisite-n-call.json', 'test/helpers/devex-count-fixture.ts', 'test/devex-count-fixture.test.ts', 'test/skill-e2e-plan-devex-finding-count.test.ts', 'test/fixtures/dx-prerequisite-r-call.json', 'test/fixtures/devex-review-l-calls.json', 'test/plan-count-checkbox.test.ts', 'test/fixtures/ceo-checkbox-l.screen.txt', 'test/fixtures/devex-review-n-calls.json', 'test/plan-count-empty-review.test.ts', 'test/fixtures/ceo-count-s-distinct.json', 'test/fixtures/plan-count-design-questionless-report.md', 'test/devex-output-o.test.ts', 'test/fixtures/devex-review-o-calls.json', 'test/fixtures/devex-output-o-retry-call.json', 'test/devex-setup-remedy-o.test.ts', 'test/fixtures/devex-review-o-retry-calls.json', 'test/plan-count-dx-handoff-o.test.ts', 'test/fixtures/devex-handoff-o-call.json', 'test/eng-devex-s-count.test.ts', 'test/fixtures/eng-devex-s-first-calls.json', 'test/fixtures/eng-devex-s-retry-calls.json', 'test/fixtures/devex-review-t-calls.json', 'test/helpers/plan-count-file-permission.ts', 'test/plan-count-file-permission.test.ts', 'test/fixtures/plan-count-edit-permission-t.json', 'test/plan-count-preview-footer.test.ts', 'test/fixtures/ceo-preview-u-call.json', 'test/fixtures/ceo-preview-u-screen.txt', 'test/fixtures/devex-count-u-calls.json', 'test/fixtures/devex-count-u-retry-calls.json', 'test/fixtures/devex-empathy-v-calls.json', 'test/fixtures/devex-handoff-v-call.json', 'test/fixtures/design-preview-v-screen.txt', 'test/plan-count-owned-permission.test.ts', 'test/fixtures/plan-count-owned-permission-v.json', 'test/fixtures/ceo-questionless-w-native.json', 'test/fixtures/devex-count-y-calls.json', 'test/plan-count-truncated-question.test.ts', 'test/fixtures/ceo-approach-z-call.json', 'test/fixtures/ceo-approach-z-screen.txt', 'test/fixtures/devex-count-z-calls.json', 'test/fixtures/devex-handoff-z-call.json', 'test/review-handoffs-aa.test.ts', 'test/fixtures/review-handoff-aa-ceo.json', 'test/fixtures/review-handoff-aa-dx.json', 'test/devex-empathy-ab.test.ts', 'test/fixtures/devex-empathy-ab-calls.json', 'test/devex-ac-accounting.test.ts', 'test/fixtures/devex-ac-first-attempt-calls.json', 'test/plan-count-permission-ac.test.ts', 'test/fixtures/plan-count-permission-ac.json', 'test/fixtures/plan-count-permission-ad.json', 'test/fixtures/plan-count-permission-ae.json', 'test/fixtures/plan-count-permission-target-ad-v2.json', 'test/devex-reconfirmation-ad-v2.test.ts', 'test/fixtures/devex-reconfirmation-ad-v2.json', 'test/plan-count-history.test.ts', 'test/helpers/devex-seed-coverage.ts', 'test/devex-seed-coverage.test.ts', 'test/fixtures/devex-seed-coverage-ad-v3.json', 'test/fixtures/plan-count-permission-ah.json',
|
||||
'test/dx-signature-identity-ak.test.ts',
|
||||
@@ -239,6 +252,7 @@ export const E2E_TOUCHFILES: Record<string, string[]> = {
|
||||
'test/fixtures/plan-count-crop-ak.json',
|
||||
'test/plan-count-quoted-frame-ak.test.ts',
|
||||
'test/fixtures/plan-count-quoted-frame-ak.json',
|
||||
"test/fixtures/dx-declarative-choices-am.json",
|
||||
],
|
||||
|
||||
// Gate-tier reviewCount-floor counterparts. Catch the May 2026 transcript
|
||||
|
||||
@@ -30,7 +30,7 @@ import {
|
||||
isNumberedOptionListVisible,
|
||||
selectPtyNumberedOption,
|
||||
} from './helpers/claude-pty-runner';
|
||||
import { autoplanArtifactPermissionInput, pendingAutoplanArtifactPermissionInput, autoplanArtifactMenuKey } from './helpers/autoplan-artifact-permission';
|
||||
import { autoplanArtifactPermissionInput, pendingAutoplanArtifactPermissionInput, publishedAutoplanArtifactPermissionInput, autoplanArtifactMenuKey } from './helpers/autoplan-artifact-permission';
|
||||
import { readPendingAutoplanArtifact, autoplanArtifactRecorderStatus } from './helpers/autoplan-artifact-recorder';
|
||||
import { autoplanSetupDecision, type AutoplanSetupDecision } from './helpers/autoplan-setup-question';
|
||||
import { autoplanPhaseCompletions, type AutoplanPhaseHit } from './helpers/autoplan-phase-observer';
|
||||
@@ -110,7 +110,7 @@ describeE2E('/autoplan native chain ordering (periodic)', () => {
|
||||
pendingSetupQuestion = readPendingQuestion(session.pendingQuestionFile, tempDir,
|
||||
session.hermeticConfigDir, commandStartedAt, transcript);
|
||||
pendingArtifact = readPendingAutoplanArtifact(session.pendingAutoplanArtifactFile, tempDir,
|
||||
session.hermeticConfigDir, session.hermeticSkillStateRoot, commandStartedAt, publicTools);
|
||||
session.hermeticConfigDir, session.hermeticSkillStateRoot, commandStartedAt, publicTools, Date.now(), true);
|
||||
methodologyAudit = auditAutoplanMethodReads(publicTools, prompt =>
|
||||
loadAutoplanMethodologyBinding(prompt, [getHermeticDirs().runRoot, nativeState!.env.GSTACK_HOME!]));
|
||||
hits = autoplanPhaseCompletions(transcript, commandStartedAt);
|
||||
@@ -160,6 +160,9 @@ describeE2E('/autoplan native chain ordering (periodic)', () => {
|
||||
const artifactPermission = autoplanArtifactPermissionInput(visible, {
|
||||
cwd: tempDir, ownedStateRoot: session.hermeticSkillStateRoot,
|
||||
commandStartedAt, transcriptStatus: transcript.status, publicTools,
|
||||
}, seenArtifactPermissions) ?? publishedAutoplanArtifactPermissionInput(visible, {
|
||||
cwd: tempDir, ownedStateRoot: session.hermeticSkillStateRoot, commandStartedAt,
|
||||
transcriptStatus: transcript.status, publicTools, pending: pendingArtifact, viewportCapturedAt,
|
||||
}, seenArtifactPermissions) ?? pendingAutoplanArtifactPermissionInput(visible, {
|
||||
cwd: tempDir, ownedStateRoot: session.hermeticSkillStateRoot, commandStartedAt,
|
||||
transcriptStatus: transcript.status, publicTools, pending: pendingArtifact, viewportCapturedAt,
|
||||
|
||||
@@ -138,6 +138,29 @@ const planCeo5Findings = (planPath: string) => [
|
||||
'email delivery, and correlated outcome trace before enabling it broadly.',
|
||||
'This is manual deployment verification, not automated handler regression',
|
||||
'coverage; no new automated tests are planned in the Tests section below.',
|
||||
'The existing notification contract sends one payment receipt per PaymentIntent,',
|
||||
'including a summary of the user orders. With zero orders it still sends one',
|
||||
'receipt with an empty order summary; the order loop is data loading, never',
|
||||
'one email or payment update per order. These product semantics are retained.',
|
||||
'The shared mail client already derives a provider idempotency key from that',
|
||||
'PaymentIntent ID. The provider durably suppresses duplicate successful sends',
|
||||
'for the same key across process crashes, webhook retries, and manual retries.',
|
||||
'Before rethrowing a failed or timed-out send, that client durably records the',
|
||||
'notification attempt for the existing retry procedure. The dashboard and',
|
||||
'on-call alert already monitor failed-notification age and backlog after an',
|
||||
'outage clears, as well as failure rate; the runbook retries those records.',
|
||||
'The existing mail-client deadline is one second, enforced by cancellation',
|
||||
'of the provider request with no inline retries. It raises MailTimeout on',
|
||||
'expiry. The retained DB/ingress deadlines bound their combined work to two',
|
||||
'seconds, leaving headroom inside the existing ten-second webhook deadline.',
|
||||
'Neither deadlines nor retry records catch the mail exception for this handler;',
|
||||
'the shared client still rethrows it to the inline caller described below.',
|
||||
'Every existing event-correlated outcome trace includes the active handler',
|
||||
'identity (prior or new), so rollout attribution is already available.',
|
||||
'If a separate handler class is retained, its already-approved name is',
|
||||
'`Webhooks::StripePaymentWebhookHandler` in the application-owned namespace,',
|
||||
'never the Stripe library namespace. This naming choice is settled; whether',
|
||||
'to add a separate implementation or reuse WebhookDispatcher remains open.',
|
||||
'',
|
||||
'## Architecture',
|
||||
"We're adding a new `StripePaymentWebhookHandler` class that will handle Stripe webhooks.",
|
||||
|
||||
Reference in new issue
Block a user