From d307141f402a6d821e2f57dca90e78ac882fea72 Mon Sep 17 00:00:00 2001 From: Rudy Celekli Date: Mon, 12 Oct 2026 22:33:57 -0400 Subject: [PATCH 01/16] Add bounded native TypeScript verification oracle --- src/reporters/typecheck-worker.cjs | 52 ++++++++++++++ src/typecheck.js | 107 +++++++++++++++++++++++++++++ test/typecheck.test.js | 88 ++++++++++++++++++++++++ 3 files changed, 247 insertions(+) create mode 100644 src/reporters/typecheck-worker.cjs create mode 100644 src/typecheck.js create mode 100644 test/typecheck.test.js diff --git a/src/reporters/typecheck-worker.cjs b/src/reporters/typecheck-worker.cjs new file mode 100644 index 0000000..62a397f --- /dev/null +++ b/src/reporters/typecheck-worker.cjs @@ -0,0 +1,52 @@ +'use strict'; +const fs=require('node:fs'),path=require('node:path'); +let output={schemaVersion:1,status:'incomplete',complete:false,files:[],diagnostics:[],compiler:{version:'unknown'},programFiles:[],implementationFiles:[],suppressions:[],limitations:[],errorCode:'typecheck-worker-incomplete'}; +const fail=code=>{throw Object.assign(new Error(code),{code});}; +try{ + const request=JSON.parse(fs.readFileSync(0,'utf8')); + if(!request||typeof request.root!=='string'||typeof request.project!=='string'||typeof request.sdk!=='string')fail('typecheck-invalid-request'); + const root=fs.realpathSync(request.root),relative=file=>path.relative(root,fs.realpathSync(file)).split(path.sep).join('/'); + const local=file=>{const real=fs.realpathSync(file);if(real!==root&&!real.startsWith(root+path.sep))fail('typecheck-external-closure');return real;}; + local(request.sdk);const ts=require(request.sdk);output.compiler.version=ts.version; + if(ts.version!==request.version||typeof ts.createProgram!=='function'||typeof ts.getPreEmitDiagnostics!=='function')fail('typecheck-sdk-invalid'); + const readFile=file=>{if(!fs.existsSync(file))return undefined;return ts.sys.readFile(local(file));}; + const host={...ts.sys,readFile,readDirectory:(directory,...args)=>{local(directory);return ts.sys.readDirectory(directory,...args);}}; + const configFile=local(path.join(root,request.project)),read=ts.readConfigFile(configFile,readFile); + const diagnostic=d=>{if(!d||!Number.isSafeInteger(d.category)||d.category<0||d.category>3||!Number.isSafeInteger(d.code)||d.code<0)fail('typecheck-malformed-diagnostic');if(d.start!==undefined&&(!Number.isSafeInteger(d.start)||d.start<0||!d.file||d.start>d.file.text.length))fail('typecheck-malformed-diagnostic');let file=null,line=null,column=null;if(d.file){file=relative(d.file.fileName);if(d.start!==undefined){const pos=d.file.getLineAndCharacterOfPosition(d.start);line=pos.line+1;column=pos.character+1;}}return {code:d.code,category:['warning','error','suggestion','message'][d.category],file,line,column,message:ts.flattenDiagnosticMessageText(d.messageText,'\n').slice(0,4096)};}; + if(read.error){output.diagnostics=[diagnostic(read.error)];fail('typecheck-config-invalid');} + const parsed=ts.parseJsonConfigFileContent(read.config,host,path.dirname(configFile),undefined,configFile); + if(parsed.errors.length){output.diagnostics=parsed.errors.map(diagnostic);fail('typecheck-config-invalid');} + if(parsed.projectReferences?.length)fail('typecheck-project-references-unsupported'); + if(parsed.options.noCheck)fail('typecheck-no-check'); + if(parsed.options.composite)fail('typecheck-composite-unsupported'); + if(parsed.options.plugins?.length)fail('typecheck-plugins-unsupported'); + // Declaration-only/unchecked-JS scopes cannot establish implementation checking. + const implementations=parsed.fileNames.filter(file=>! /\.d\.[cm]?ts$/i.test(file)&&(/\.[cm]?tsx?$/i.test(file)||(parsed.options.allowJs&&parsed.options.checkJs&&/\.[cm]?jsx?$/i.test(file)))); + if(!implementations.length)fail('typecheck-no-checked-inputs'); + parsed.fileNames.forEach(local); + if(parsed.options.skipLibCheck)output.limitations.push('skipLibCheck preserves the owner setting: declaration diagnostics may be omitted.'); + if(parsed.options.skipDefaultLibCheck)output.limitations.push('skipDefaultLibCheck preserves the owner setting: default library diagnostics may be omitted.'); + if(parsed.options.allowJs&&!parsed.options.checkJs)output.limitations.push('JavaScript inputs are not type checked because checkJs is disabled.'); + output.limitations.push('Files excluded by the TypeScript project are outside this check.'); + const options={...parsed.options,noEmit:true,incremental:false,tsBuildInfoFile:undefined}; + const compilerHost=ts.createCompilerHost(options);compilerHost.readFile=readFile;compilerHost.writeFile=()=>fail('typecheck-unexpected-write'); + const program=ts.createProgram({rootNames:parsed.fileNames,options,host:compilerHost}); + const sourceFiles=program.getSourceFiles(); + output.programFiles=sourceFiles.map(file=>relative(file.fileName)).sort(); + const implementationFiles=sourceFiles.filter(file=>!file.isDeclarationFile&&(file.scriptKind===ts.ScriptKind.TS||file.scriptKind===ts.ScriptKind.TSX||options.allowJs&&(options.checkJs||file.checkJsDirective?.enabled)&&[ts.ScriptKind.JS,ts.ScriptKind.JSX].includes(file.scriptKind))); + for(const file of sourceFiles){ + const suppression=(kind,pos)=>{const location=file.getLineAndCharacterOfPosition(pos);output.suppressions.push({file:relative(file.fileName),directive:kind,line:location.line+1,column:location.character+1});}; + if(file.checkJsDirective?.enabled===false)suppression('ts-nocheck',file.checkJsDirective.pos); + for(const comment of file.commentDirectives||[])suppression(comment.type===ts.CommentDirectiveType.ExpectError?'ts-expect-error':'ts-ignore',comment.range.pos); + } + output.files=implementationFiles.filter(file=>file.checkJsDirective?.enabled!==false).map(file=>relative(file.fileName)).sort(); + output.implementationFiles=[...output.files]; + if(!output.files.length)fail('typecheck-no-checked-inputs'); + if(implementationFiles.some(file=>file.checkJsDirective?.enabled===false))fail('typecheck-source-no-check'); + if(output.suppressions.length)output.limitations.push('Native source suppression directives are present; suppressed diagnostics are outside the reported type-error evidence.'); + const seen=new Set();output.diagnostics=ts.getPreEmitDiagnostics(program).map(diagnostic).filter(d=>{const key=JSON.stringify(d);if(seen.has(key))return false;seen.add(key);return true;}); + // Missing inputs/modules or config/global errors mean the declared closure is incomplete. + if(output.diagnostics.some(d=>d.category==='error'&&(d.file===null||[2307,2688,6053,5083].includes(d.code))))fail('typecheck-input-closure-incomplete'); + output.complete=true;output.status=output.diagnostics.some(d=>d.category==='error')?'failed':'passed';delete output.errorCode; +}catch(error){output.status='incomplete';output.complete=false;output.errorCode=typeof error.code==='string'&&error.code.startsWith('typecheck-')?error.code:'typecheck-worker-incomplete';} +process.stdout.write(JSON.stringify(output)); diff --git a/src/typecheck.js b/src/typecheck.js new file mode 100644 index 0000000..e8c684e --- /dev/null +++ b/src/typecheck.js @@ -0,0 +1,107 @@ +import fs from 'node:fs'; +import os from 'node:os'; +import path from 'node:path'; +import {fileURLToPath} from 'node:url'; +import {createHash} from 'node:crypto'; +import {spawnSync} from 'node:child_process'; +import {snapshot, freshness} from './provenance.js'; +import {readConfig} from './files.js'; + +const worker=fileURLToPath(new URL('./reporters/typecheck-worker.cjs',import.meta.url)); +const hash=value=>createHash('sha256').update(typeof value==='string'||Buffer.isBuffer(value)?value:JSON.stringify(value)).digest('hex'); +const canonical=file=>typeof file==='string'&&file.length>0&&!file.includes('\\')&&!file.includes('\0')&&!path.isAbsolute(file)&&file.split('/').every(x=>x&&x!=='.'&&x!=='..'); +const inside=(root,file)=>file.startsWith(root+path.sep); +function failure(code){throw Object.assign(new Error(code),{code});} +function checkDeadline(deadline){if(performance.now()>=deadline)failure('typecheck-timeout');} +function fileHash(file,deadline){const digest=createHash('sha256'),buffer=Buffer.alloc(65536),fd=fs.openSync(file,fs.constants.O_RDONLY|fs.constants.O_NOFOLLOW);try{let n;while((n=fs.readSync(fd,buffer,0,buffer.length,null))){checkDeadline(deadline);digest.update(buffer.subarray(0,n));}}finally{fs.closeSync(fd);}return digest.digest('hex');} +// Include ignored source and directory membership. A public checksum is evidence, +// never admission: each verification captures fresh bytes both before and after. +function inventory(root,deadline){ + const source=[],dependencies=[];let bytes=0,count=0; + function walk(directory,prefix=''){ + for(const name of fs.readdirSync(directory).sort()){ + checkDeadline(deadline);if(!prefix&&['.git','.tddswarm'].includes(name))continue; + const file=prefix+name,target=path.join(directory,name),stat=fs.lstatSync(target); + if(++count>100000)failure('typecheck-inventory-bound'); + const rows=file==='node_modules'||file.startsWith('node_modules/')?dependencies:source; + if(stat.isSymbolicLink()){ + const real=fs.realpathSync(target);if(!inside(root,real))failure('typecheck-external-symlink'); + rows.push({file,kind:'symlink',target:fs.readlinkSync(target),real:path.relative(root,real)}); + }else if(stat.isDirectory()){ + if(prefix&&name==='.git')failure('typecheck-nested-repository'); + rows.push({file,kind:'directory',mode:stat.mode&0o777});walk(target,file+'/'); + }else if(stat.isFile()){ + bytes+=stat.size;if(bytes>1024*1024*1024)failure('typecheck-inventory-bound'); + rows.push({file,kind:'file',size:stat.size,mode:stat.mode&0o777,sha256:fileHash(target,deadline)}); + }else failure('typecheck-unsupported-file'); + } + } + walk(root); + return {source:{sha256:hash(source),files:source},dependencies:{sha256:hash(dependencies),files:dependencies},bytes,count}; +} +function runtime(deadline){const executable=fs.realpathSync(process.execPath);return {executable,version:process.version,platform:process.platform,arch:process.arch,sha256:fileHash(executable,deadline)};} +function producers(deadline){return [fileURLToPath(import.meta.url),worker].map(file=>({file:path.basename(file),sha256:fileHash(file,deadline)}));} +function verifyProtocol(result,root,inventoryBefore){ + if(!result||result.schemaVersion!==1||!['passed','failed','incomplete'].includes(result.status)||!Array.isArray(result.files)||!Array.isArray(result.diagnostics)||!Array.isArray(result.limitations)||typeof result.compiler?.version!=='string'||typeof result.complete!=='boolean')failure('typecheck-malformed-output'); + if(result.files.length>10000||new Set(result.files).size!==result.files.length||result.files.some(file=>!canonical(file)))failure('typecheck-malformed-output'); + if(!Array.isArray(result.implementationFiles)||JSON.stringify(result.implementationFiles)!==JSON.stringify(result.files))failure('typecheck-malformed-output'); + if(!Array.isArray(result.programFiles)||!Array.isArray(result.suppressions)||result.programFiles.length>10000||new Set(result.programFiles).size!==result.programFiles.length)failure('typecheck-malformed-output'); + const known=new Map([...inventoryBefore.source.files,...inventoryBefore.dependencies.files].map(row=>[row.file,row])); + if(result.programFiles.some(file=>!canonical(file)||!known.has(file))||result.files.some(file=>!result.programFiles.includes(file)))failure('typecheck-malformed-output'); + if(result.suppressions.length>10000||result.suppressions.some(item=>!item||!canonical(item.file)||!known.has(item.file)||!['ts-nocheck','ts-ignore','ts-expect-error'].includes(item.directive)||!Number.isSafeInteger(item.line)||item.line<1||!Number.isSafeInteger(item.column)||item.column<1))failure('typecheck-malformed-output'); + if(result.files.some(file=>!known.has(file)))failure('typecheck-external-scope'); + const seen=new Set(); + for(const diagnostic of result.diagnostics){ + if(!Number.isSafeInteger(diagnostic.code)||diagnostic.code<0||!['error','warning','message','suggestion'].includes(diagnostic.category)||typeof diagnostic.message!=='string'||diagnostic.message.length>4096||!(diagnostic.file===null||canonical(diagnostic.file))||!(diagnostic.line===null||Number.isSafeInteger(diagnostic.line)&&diagnostic.line>0)||!(diagnostic.column===null||Number.isSafeInteger(diagnostic.column)&&diagnostic.column>0))failure('typecheck-malformed-output'); + if(diagnostic.file!==null&&!result.files.includes(diagnostic.file)&&!known.has(diagnostic.file))failure('typecheck-external-scope'); + const identity=hash(diagnostic);if(seen.has(identity))failure('typecheck-malformed-output');seen.add(identity); + } + const errors=result.diagnostics.some(x=>x.category==='error'); + if(result.complete&&(!result.files.length||result.status==='incomplete'||(result.status==='passed'&&errors)||(result.status==='failed'&&!errors)))failure('typecheck-malformed-output'); + if(!result.complete&&result.status!=='incomplete')failure('typecheck-malformed-output'); + if(result.status==='incomplete'&&(typeof result.errorCode!=='string'||!result.errorCode))failure('typecheck-malformed-output'); + return result; +} + +/** Complementary type oracle. Trusted native execution, not an OS sandbox. */ +export function verifyTypecheck(projectRoot,{project='tsconfig.json',timeoutMs=30000,expectedProvenance}={}){ + const started=performance.now(),base={schemaVersion:1,kind:'native-typecheck',project,status:'incomplete',complete:false,exitCode:2,files:[],diagnostics:[],programFiles:[],implementationFiles:[],suppressions:[],compiler:null,binding:null,signal:null,errorCode:null,reasons:[],durationMs:0,nextAction:'Resolve the incomplete compiler check and run verification again.',scope:'Project-local unreferenced TypeScript implementation scope',limitations:['Trusted compiler execution is not an OS sandbox.','This type oracle grants no test-routing or publication authority.']}; + let home; + const finish=extra=>({...base,...extra,durationMs:Math.round(performance.now()-started)}); + try{ + if(!Number.isSafeInteger(timeoutMs)||timeoutMs<1||timeoutMs>300000)failure('typecheck-invalid-timeout'); + const deadline=started+timeoutMs,root=fs.realpathSync(projectRoot); + if(!canonical(project))failure('typecheck-invalid-project'); + const configFile=path.join(root,project); + if(!fs.existsSync(configFile))return finish({status:'not-configured',errorCode:'typecheck-config-missing',reasons:['typecheck-config-missing'],nextAction:`Add ${project}, or configure the path to an existing TypeScript project.`}); + if(!inside(root,fs.realpathSync(configFile))||!fs.statSync(configFile).isFile())failure('typecheck-external-config'); + const before=inventory(root,deadline); + const beforeProvenance=snapshot(root,readConfig(root));checkDeadline(deadline); + if(expectedProvenance){const check=freshness(expectedProvenance,beforeProvenance);if(!check.fresh||expectedProvenance.revision!==beforeProvenance.revision)failure('typecheck-provenance-mismatch');} + const nodeBefore=runtime(deadline),producerBefore=producers(deadline); + base.binding={source:before.source,dependencies:before.dependencies,provenance:beforeProvenance,node:nodeBefore,producers:producerBefore}; + const sdk=path.join(root,'node_modules/typescript/lib/typescript.js'),manifest=path.join(root,'node_modules/typescript/package.json'); + if(!fs.existsSync(sdk)||!fs.existsSync(manifest))failure('typecheck-sdk-missing'); + if(!inside(root,fs.realpathSync(sdk))||!inside(root,fs.realpathSync(manifest)))failure('typecheck-external-sdk'); + const packageData=JSON.parse(fs.readFileSync(manifest,'utf8')); + if(packageData.name!=='typescript'||typeof packageData.version!=='string')failure('typecheck-sdk-invalid'); + const compiler={version:packageData.version,file:path.relative(root,fs.realpathSync(sdk)),sha256:fileHash(sdk,deadline),manifestSha256:fileHash(manifest,deadline)}; + base.compiler=compiler; + home=fs.mkdtempSync(path.join(os.tmpdir(),'testlore-typecheck-'));fs.chmodSync(home,0o700); + const remaining=Math.floor(deadline-performance.now());if(remaining<1)failure('typecheck-timeout'); + const child=spawnSync(nodeBefore.executable,['--unhandled-rejections=strict',worker],{cwd:root,input:JSON.stringify({root,project,sdk,version:compiler.version}),encoding:'utf8',timeout:remaining,maxBuffer:4*1024*1024,killSignal:'SIGKILL',env:{HOME:home,USERPROFILE:home,TMPDIR:home,TMP:home,TEMP:home,PATH:path.dirname(nodeBefore.executable)+path.delimiter+'/usr/bin:/bin',LANG:'C',LC_ALL:'C',TZ:'UTC'}}); + base.signal=child.signal||null; + if(child.error){failure(child.error.code==='ETIMEDOUT'?'typecheck-timeout':'typecheck-worker-error');} + if(child.signal||child.status!==0||child.stderr)failure('typecheck-worker-error'); + let parsed;try{parsed=JSON.parse(child.stdout);}catch{failure('typecheck-malformed-output');} + const result=verifyProtocol(parsed,root,before); + if(result.compiler.version!==compiler.version)failure('typecheck-sdk-drift'); + const after=inventory(root,deadline),nodeAfter=runtime(deadline),producerAfter=producers(deadline),afterProvenance=snapshot(root,readConfig(root));checkDeadline(deadline); + if(before.source.sha256!==after.source.sha256)failure('typecheck-source-drift'); + if(before.dependencies.sha256!==after.dependencies.sha256)failure('typecheck-dependency-drift'); + if(hash(nodeBefore)!==hash(nodeAfter)||hash(producerBefore)!==hash(producerAfter))failure('typecheck-runtime-drift'); + if(!freshness(beforeProvenance,afterProvenance).fresh||beforeProvenance.revision!==afterProvenance.revision)failure('typecheck-provenance-drift'); + return finish({status:result.status,complete:result.complete,exitCode:result.complete?(result.status==='failed'?1:0):2,files:result.files,diagnostics:result.diagnostics,programFiles:result.programFiles,implementationFiles:result.implementationFiles,suppressions:result.suppressions,compiler,binding:{source:before.source,dependencies:before.dependencies,provenance:beforeProvenance,node:nodeBefore,producers:producerBefore,projectSha256:fileHash(configFile,deadline)},errorCode:result.errorCode||null,reasons:result.errorCode?[result.errorCode]:[],limitations:[...base.limitations,...result.limitations],nextAction:result.status==='passed'?'Type checking passed; continue with independent runtime tests.':result.status==='failed'?'Repair the reported type diagnostics, then rerun type checking and runtime tests.':'Resolve unsupported compiler configuration or incomplete scope, then rerun.'}); + }catch(error){const errorCode=error.code?.startsWith('typecheck-')?error.code:'typecheck-input-unavailable';return finish({errorCode,reasons:[errorCode]});} + finally{if(home)fs.rmSync(home,{recursive:true,force:true});} +} diff --git a/test/typecheck.test.js b/test/typecheck.test.js new file mode 100644 index 0000000..07f20ca --- /dev/null +++ b/test/typecheck.test.js @@ -0,0 +1,88 @@ +import test from 'node:test'; +import assert from 'node:assert/strict'; +import fs from 'node:fs'; +import path from 'node:path'; +import {fileURLToPath} from 'node:url'; +import {spawnSync} from 'node:child_process'; +import {fixture,write} from './helpers.js'; +import {verifyTypecheck} from '../src/typecheck.js'; +import {snapshot} from '../src/provenance.js'; +import {readConfig} from '../src/files.js'; + +// Real installed SDK; copied into each project's closed native dependency scope. +// Supports both an ordinary checkout and this private development lane. +const here=path.dirname(fileURLToPath(import.meta.url)); +const sdkCandidates=[path.join(here,'../node_modules/typescript'),path.join(here,'../../../../node_modules/typescript')]; +const sdk=sdkCandidates.find(directory=>fs.existsSync(path.join(directory,'lib/typescript.js'))); +function project(t,extra={}){ + assert.ok(sdk,'Actual installed TypeScript required for native controls'); + const root=fixture(t,{'package.json':{type:'module'},'.gitignore':'ignored.txt\nnode_modules/\n', + 'tsconfig.json':{compilerOptions:{strict:true,target:'ES2022',module:'NodeNext',moduleResolution:'NodeNext',skipLibCheck:true},include:['src/**/*.ts']}, + 'src/value.ts':'export const value: number = 42;\n','ignored.txt':'bound ignored input',...extra}); + fs.mkdirSync(path.join(root,'node_modules'),{recursive:true});fs.cpSync(sdk,path.join(root,'node_modules/typescript'),{recursive:true});return root; +} +const check=root=>verifyTypecheck(root,{timeoutMs:20000}); +const hook=(root,code)=>{const file=path.join(root,'node_modules/typescript/lib/typescript.js');fs.writeFileSync(file,code+'\n'+fs.readFileSync(file,'utf8'));}; + +test('native runtime green cannot hide TS2322; healthy inverse is genuinely checked and read-only',t=>{ + const root=project(t,{'src/value.ts':'export const value: string = 42;\n','test/runtime.test.mjs':"import test from 'node:test';import assert from 'node:assert/strict';import {value} from '../src/value.ts';test('runtime value',()=>assert.equal(value,42));"}); + const runtime=spawnSync(process.execPath,['--experimental-strip-types','--test','test/runtime.test.mjs'],{cwd:root,encoding:'utf8'});assert.equal(runtime.status,0,runtime.stderr); + const result=check(root);assert.equal(result.status,'failed',JSON.stringify({status:result.status,errorCode:result.errorCode,diagnostics:result.diagnostics}));assert.equal(result.exitCode,1);assert.equal(result.complete,true);assert.ok(result.diagnostics.some(d=>d.code===2322&&d.file==='src/value.ts'&&d.line===1));assert.ok(result.compiler.version);assert.ok(result.binding.source.files.some(row=>row.file==='ignored.txt'));assert.ok(result.files.includes('src/value.ts')); + write(root,'src/value.ts','export const value: number = 42;\n');const healthy=check(root);assert.equal(healthy.status,'passed',JSON.stringify({status:healthy.status,errorCode:healthy.errorCode,diagnostics:healthy.diagnostics}));assert.equal(healthy.exitCode,0);assert.equal(healthy.complete,true);assert.equal(healthy.diagnostics.length,0);assert.ok(healthy.limitations.some(s=>s.includes('skipLibCheck')));assert.equal(fs.existsSync(path.join(root,'src/value.js')),false);assert.equal(fs.existsSync(path.join(root,'tsconfig.tsbuildinfo')),false); +}); + +test('missing configuration and missing SDK are actionable incomplete checks',t=>{ + const root=fixture(t,{'package.json':{type:'module'}});const missing=check(root);assert.equal(missing.status,'not-configured');assert.equal(missing.complete,false);assert.equal(missing.exitCode,2);assert.match(missing.nextAction,/tsconfig/); + write(root,'tsconfig.json',{compilerOptions:{strict:true},files:['src/value.ts']});write(root,'src/value.ts','export const value=1;');const result=check(root);assert.equal(result.errorCode,'typecheck-sdk-missing');assert.equal(result.exitCode,2); +}); + +test('noCheck, empty, unchecked JS, references and composite profiles never produce partial green',t=>{ + const root=project(t); + for(const config of [{compilerOptions:{noCheck:true},files:['src/value.ts']},{files:[]},{compilerOptions:{allowJs:true,checkJs:false},files:['src/plain.js']},{files:['src/value.ts'],references:[{path:'./other'}]},{compilerOptions:{composite:true},files:['src/value.ts']}]){ + write(root,'src/plain.js','export const value=42;');write(root,'tsconfig.json',config);const result=check(root);assert.equal(result.status,'incomplete',JSON.stringify({status:result.status,errorCode:result.errorCode,diagnostics:result.diagnostics}));assert.equal(result.exitCode,2);assert.equal(result.complete,false); + } +}); + +test('malformed config, missing includes, unresolved modules, and external extends decline complete scope',t=>{ + const root=project(t);for(const config of ['{invalid',JSON.stringify({files:['src/missing.ts']}),JSON.stringify({extends:'../outside.json',files:['src/value.ts']})]){write(root,'tsconfig.json',config);const result=check(root);assert.equal(result.exitCode,2);assert.equal(result.complete,false);} + write(root,'tsconfig.json',{compilerOptions:{strict:true,module:'NodeNext',moduleResolution:'NodeNext',skipLibCheck:true},files:['src/value.ts']});write(root,'src/value.ts',"import {x} from 'not-installed';export const value=x;");assert.equal(check(root).errorCode,'typecheck-input-closure-incomplete'); +}); + +test('provenance mismatches reject before executing the installed compiler',t=>{ + const root=project(t),expected=snapshot(root,readConfig(root));hook(root,"require('node:fs').writeFileSync('compiler-entered.txt','yes');");write(root,'src/value.ts','export const value: number=43;');const result=verifyTypecheck(root,{expectedProvenance:expected,timeoutMs:20000});assert.equal(result.errorCode,'typecheck-provenance-mismatch');assert.equal(fs.existsSync(path.join(root,'compiler-entered.txt')),false); +}); + +test('ignored source and installed dependency mutations during native execution reject green',t=>{ + const root=project(t);hook(root,"require('node:fs').writeFileSync('ignored.txt','drift');");let result=check(root);assert.equal(result.errorCode,'typecheck-source-drift');assert.equal(result.complete,false); + const compiler=path.join(root,'node_modules/typescript/lib/typescript.js');fs.copyFileSync(path.join(sdk,'lib/typescript.js'),compiler);hook(root,"require('node:fs').writeFileSync('node_modules/added.txt','drift');");result=check(root);assert.equal(result.errorCode,'typecheck-dependency-drift');assert.equal(result.exitCode,2); +}); + +test('worker malformed output, native timeout and inherited preloads cannot produce green',t=>{ + const root=project(t);hook(root,"process.stdout.write('non-json');");assert.equal(check(root).errorCode,'typecheck-malformed-output'); + fs.copyFileSync(path.join(sdk,'lib/typescript.js'),path.join(root,'node_modules/typescript/lib/typescript.js'));hook(root,'while(true){}');const timed=verifyTypecheck(root,{timeoutMs:1000});assert.equal(timed.errorCode,'typecheck-timeout');assert.equal(timed.exitCode,2); + fs.copyFileSync(path.join(sdk,'lib/typescript.js'),path.join(root,'node_modules/typescript/lib/typescript.js'));write(root,'preload.cjs',"require('node:fs').writeFileSync('preload-entered.txt','yes');");const previous=process.env.NODE_OPTIONS;try{process.env.NODE_OPTIONS='--require='+path.join(root,'preload.cjs');const result=check(root);assert.equal(result.status,'passed',JSON.stringify({status:result.status,errorCode:result.errorCode,diagnostics:result.diagnostics}));assert.equal(fs.existsSync(path.join(root,'preload-entered.txt')),false);}finally{if(previous===undefined)delete process.env.NODE_OPTIONS;else process.env.NODE_OPTIONS=previous;} +}); + +test('external source or SDK symlinks are unsupported rather than silently unhashed',t=>{ + const root=project(t),external=fixture(t,{'external.ts':'export const value=1;'});fs.symlinkSync(path.join(external,'external.ts'),path.join(root,'src/external.ts'));assert.equal(check(root).errorCode,'typecheck-external-symlink'); +}); + +test('project traversal and invalid total budgets decline safely',t=>{ + const root=project(t);for(const project of ['../tsconfig.json','/tmp/tsconfig.json','src\\config.json'])assert.equal(verifyTypecheck(root,{project}).errorCode,'typecheck-invalid-project');for(const timeoutMs of [0,-1,NaN,300001])assert.equal(verifyTypecheck(root,{timeoutMs}).errorCode,'typecheck-invalid-timeout'); +}); + +test('native directives expose suppressions and reject unchecked implementation rather than library-only green',t=>{ + const root=project(t);write(root,'src/value.ts','// @ts-nocheck\nexport const value: string=42;');let result=check(root);assert.equal(result.status,'incomplete');assert.equal(result.complete,false);assert.equal(result.errorCode,'typecheck-no-checked-inputs');assert.ok(result.suppressions.some(d=>d.directive==='ts-nocheck'&&d.file==='src/value.ts')); + write(root,'src/healthy.ts','export const healthy: number=1;');result=check(root);assert.equal(result.errorCode,'typecheck-source-no-check');assert.equal(result.exitCode,2); + write(root,'src/value.ts','// @ts-ignore\nexport const value: string=42;');result=check(root);assert.equal(result.status,'passed');assert.ok(result.suppressions.some(d=>d.directive==='ts-ignore'));assert.ok(result.limitations.some(d=>d.includes('suppression')));assert.deepEqual(result.files,['src/healthy.ts','src/value.ts']);assert.ok(result.programFiles.length>result.files.length); + write(root,'src/value.ts','// @ts-expect-error\nexport const value: string=42;');result=check(root);assert.equal(result.status,'passed');assert.ok(result.suppressions.some(d=>d.directive==='ts-expect-error')); + write(root,'src/value.ts','export const value: string="@ts-ignore @ts-nocheck @ts-expect-error";');result=check(root);assert.equal(result.status,'passed');assert.deepEqual(result.suppressions,[]); +}); + +test('malformed native diagnostic categories and offsets reject evaluator false greens',t=>{ + const root=project(t),compiler=path.join(root,'node_modules/typescript/lib/typescript.js'),original=fs.readFileSync(compiler,'utf8'); + for(const injected of ["{category:99,code:2322,messageText:'invalid native category'}","{category:1,code:2322,start:NaN,messageText:'invalid native position'}"]){ + const altered=original.replace('function getPreEmitDiagnostics(program, sourceFile, cancellationToken) {',`function getPreEmitDiagnostics(program, sourceFile, cancellationToken) { return [${injected}];`); + assert.notEqual(altered,original);fs.writeFileSync(compiler,altered);const result=check(root);assert.equal(result.status,'incomplete');assert.equal(result.errorCode,'typecheck-malformed-diagnostic');assert.equal(result.exitCode,2); + } +}); From 2455f37b6b5cad776130060befa97bc52009f38e Mon Sep 17 00:00:00 2001 From: Rudy Celekli Date: Mon, 12 Oct 2026 22:37:27 -0400 Subject: [PATCH 02/16] Refuse unbound compiler inputs in reserved state roots --- src/reporters/typecheck-worker.cjs | 8 ++++++-- src/typecheck.js | 5 ++++- test/typecheck.test.js | 8 ++++++++ 3 files changed, 18 insertions(+), 3 deletions(-) diff --git a/src/reporters/typecheck-worker.cjs b/src/reporters/typecheck-worker.cjs index 62a397f..d66e064 100644 --- a/src/reporters/typecheck-worker.cjs +++ b/src/reporters/typecheck-worker.cjs @@ -1,20 +1,23 @@ 'use strict'; const fs=require('node:fs'),path=require('node:path'); let output={schemaVersion:1,status:'incomplete',complete:false,files:[],diagnostics:[],compiler:{version:'unknown'},programFiles:[],implementationFiles:[],suppressions:[],limitations:[],errorCode:'typecheck-worker-incomplete'}; -const fail=code=>{throw Object.assign(new Error(code),{code});}; +let deniedInput=null; +const fail=code=>{if(['typecheck-reserved-input','typecheck-external-closure'].includes(code))deniedInput=code;throw Object.assign(new Error(code),{code});}; try{ const request=JSON.parse(fs.readFileSync(0,'utf8')); if(!request||typeof request.root!=='string'||typeof request.project!=='string'||typeof request.sdk!=='string')fail('typecheck-invalid-request'); const root=fs.realpathSync(request.root),relative=file=>path.relative(root,fs.realpathSync(file)).split(path.sep).join('/'); - const local=file=>{const real=fs.realpathSync(file);if(real!==root&&!real.startsWith(root+path.sep))fail('typecheck-external-closure');return real;}; + const local=file=>{const lexical=path.relative(root,path.resolve(file)).split(path.sep)[0];if(['.git','.tddswarm'].includes(lexical))fail('typecheck-reserved-input');const real=fs.realpathSync(file);if(real!==root&&!real.startsWith(root+path.sep))fail('typecheck-external-closure');if(['.git','.tddswarm'].includes(path.relative(root,real).split(path.sep)[0]))fail('typecheck-reserved-input');return real;}; local(request.sdk);const ts=require(request.sdk);output.compiler.version=ts.version; if(ts.version!==request.version||typeof ts.createProgram!=='function'||typeof ts.getPreEmitDiagnostics!=='function')fail('typecheck-sdk-invalid'); const readFile=file=>{if(!fs.existsSync(file))return undefined;return ts.sys.readFile(local(file));}; const host={...ts.sys,readFile,readDirectory:(directory,...args)=>{local(directory);return ts.sys.readDirectory(directory,...args);}}; const configFile=local(path.join(root,request.project)),read=ts.readConfigFile(configFile,readFile); + if(deniedInput)fail(deniedInput); const diagnostic=d=>{if(!d||!Number.isSafeInteger(d.category)||d.category<0||d.category>3||!Number.isSafeInteger(d.code)||d.code<0)fail('typecheck-malformed-diagnostic');if(d.start!==undefined&&(!Number.isSafeInteger(d.start)||d.start<0||!d.file||d.start>d.file.text.length))fail('typecheck-malformed-diagnostic');let file=null,line=null,column=null;if(d.file){file=relative(d.file.fileName);if(d.start!==undefined){const pos=d.file.getLineAndCharacterOfPosition(d.start);line=pos.line+1;column=pos.character+1;}}return {code:d.code,category:['warning','error','suggestion','message'][d.category],file,line,column,message:ts.flattenDiagnosticMessageText(d.messageText,'\n').slice(0,4096)};}; if(read.error){output.diagnostics=[diagnostic(read.error)];fail('typecheck-config-invalid');} const parsed=ts.parseJsonConfigFileContent(read.config,host,path.dirname(configFile),undefined,configFile); + if(deniedInput)fail(deniedInput); if(parsed.errors.length){output.diagnostics=parsed.errors.map(diagnostic);fail('typecheck-config-invalid');} if(parsed.projectReferences?.length)fail('typecheck-project-references-unsupported'); if(parsed.options.noCheck)fail('typecheck-no-check'); @@ -31,6 +34,7 @@ try{ const options={...parsed.options,noEmit:true,incremental:false,tsBuildInfoFile:undefined}; const compilerHost=ts.createCompilerHost(options);compilerHost.readFile=readFile;compilerHost.writeFile=()=>fail('typecheck-unexpected-write'); const program=ts.createProgram({rootNames:parsed.fileNames,options,host:compilerHost}); + if(deniedInput)fail(deniedInput); const sourceFiles=program.getSourceFiles(); output.programFiles=sourceFiles.map(file=>relative(file.fileName)).sort(); const implementationFiles=sourceFiles.filter(file=>!file.isDeclarationFile&&(file.scriptKind===ts.ScriptKind.TS||file.scriptKind===ts.ScriptKind.TSX||options.allowJs&&(options.checkJs||file.checkJsDirective?.enabled)&&[ts.ScriptKind.JS,ts.ScriptKind.JSX].includes(file.scriptKind))); diff --git a/src/typecheck.js b/src/typecheck.js index e8c684e..eddbded 100644 --- a/src/typecheck.js +++ b/src/typecheck.js @@ -72,9 +72,12 @@ export function verifyTypecheck(projectRoot,{project='tsconfig.json',timeoutMs=3 if(!Number.isSafeInteger(timeoutMs)||timeoutMs<1||timeoutMs>300000)failure('typecheck-invalid-timeout'); const deadline=started+timeoutMs,root=fs.realpathSync(projectRoot); if(!canonical(project))failure('typecheck-invalid-project'); + if(['.git','.tddswarm'].includes(project.split('/')[0]))failure('typecheck-reserved-input'); const configFile=path.join(root,project); if(!fs.existsSync(configFile))return finish({status:'not-configured',errorCode:'typecheck-config-missing',reasons:['typecheck-config-missing'],nextAction:`Add ${project}, or configure the path to an existing TypeScript project.`}); - if(!inside(root,fs.realpathSync(configFile))||!fs.statSync(configFile).isFile())failure('typecheck-external-config'); + const realConfig=fs.realpathSync(configFile); + if(!inside(root,realConfig)||!fs.statSync(configFile).isFile())failure('typecheck-external-config'); + if(['.git','.tddswarm'].includes(path.relative(root,realConfig).split(path.sep)[0]))failure('typecheck-reserved-input'); const before=inventory(root,deadline); const beforeProvenance=snapshot(root,readConfig(root));checkDeadline(deadline); if(expectedProvenance){const check=freshness(expectedProvenance,beforeProvenance);if(!check.fresh||expectedProvenance.revision!==beforeProvenance.revision)failure('typecheck-provenance-mismatch');} diff --git a/test/typecheck.test.js b/test/typecheck.test.js index 07f20ca..d9dc78e 100644 --- a/test/typecheck.test.js +++ b/test/typecheck.test.js @@ -86,3 +86,11 @@ test('malformed native diagnostic categories and offsets reject evaluator false assert.notEqual(altered,original);fs.writeFileSync(compiler,altered);const result=check(root);assert.equal(result.status,'incomplete');assert.equal(result.errorCode,'typecheck-malformed-diagnostic');assert.equal(result.exitCode,2); } }); + +test('reserved state configurations and real-path aliases never become unbound native inputs',t=>{ + const root=project(t);for(const project of ['.tddswarm/tsconfig.json','.git/tsconfig.json']){const result=verifyTypecheck(root,{project,timeoutMs:20000});assert.equal(result.errorCode,'typecheck-reserved-input');assert.equal(result.exitCode,2);} + write(root,'.tddswarm/shared.json',{compilerOptions:{strict:true}});write(root,'tsconfig.json',{extends:'./.tddswarm/shared.json',compilerOptions:{target:'ES2022',module:'NodeNext',moduleResolution:'NodeNext',skipLibCheck:true},include:['src/**/*.ts']});let result=check(root);assert.equal(result.errorCode,'typecheck-reserved-input');assert.equal(result.complete,false); + fs.symlinkSync(path.join(root,'.tddswarm/shared.json'),path.join(root,'shared-alias.json'));write(root,'tsconfig.json',{extends:'./shared-alias.json',files:['src/value.ts']});result=check(root);assert.equal(result.errorCode,'typecheck-reserved-input');assert.equal(result.exitCode,2); + fs.symlinkSync(path.join(root,'.tddswarm/shared.json'),path.join(root,'config-alias.json'));result=verifyTypecheck(root,{project:'config-alias.json',timeoutMs:20000});assert.equal(result.errorCode,'typecheck-reserved-input'); + write(root,'.tddswarm/source.ts','export const hidden=1;');write(root,'tsconfig.json',{files:['.tddswarm/source.ts']});result=check(root);assert.equal(result.errorCode,'typecheck-reserved-input');assert.equal(result.complete,false); +}); From f09990bf7448fa7151ab12ecd31aceb71db75449 Mon Sep 17 00:00:00 2001 From: Rudy Celekli Date: Mon, 12 Oct 2026 22:59:02 -0400 Subject: [PATCH 03/16] Bind type verification to opening inputs and close final capture windows --- src/reporters/typecheck-worker.cjs | 2 +- src/typecheck.js | 73 ++++++++++++++++++++++++------ test/typecheck.test.js | 36 +++++++++++++-- 3 files changed, 92 insertions(+), 19 deletions(-) diff --git a/src/reporters/typecheck-worker.cjs b/src/reporters/typecheck-worker.cjs index d66e064..078e598 100644 --- a/src/reporters/typecheck-worker.cjs +++ b/src/reporters/typecheck-worker.cjs @@ -7,7 +7,7 @@ try{ const request=JSON.parse(fs.readFileSync(0,'utf8')); if(!request||typeof request.root!=='string'||typeof request.project!=='string'||typeof request.sdk!=='string')fail('typecheck-invalid-request'); const root=fs.realpathSync(request.root),relative=file=>path.relative(root,fs.realpathSync(file)).split(path.sep).join('/'); - const local=file=>{const lexical=path.relative(root,path.resolve(file)).split(path.sep)[0];if(['.git','.tddswarm'].includes(lexical))fail('typecheck-reserved-input');const real=fs.realpathSync(file);if(real!==root&&!real.startsWith(root+path.sep))fail('typecheck-external-closure');if(['.git','.tddswarm'].includes(path.relative(root,real).split(path.sep)[0]))fail('typecheck-reserved-input');return real;}; + const local=file=>{if(typeof file!=='string'||/[\x00-\x1f\x7f]/.test(file))fail('typecheck-invalid-input-path');const lexical=path.relative(root,path.resolve(file)).split(path.sep)[0];if(['.git','.tddswarm'].includes(lexical))fail('typecheck-reserved-input');const real=fs.realpathSync(file);if(/[\x00-\x1f\x7f]/.test(path.relative(root,real)))fail('typecheck-invalid-input-path');if(real!==root&&!real.startsWith(root+path.sep))fail('typecheck-external-closure');if(['.git','.tddswarm'].includes(path.relative(root,real).split(path.sep)[0]))fail('typecheck-reserved-input');return real;}; local(request.sdk);const ts=require(request.sdk);output.compiler.version=ts.version; if(ts.version!==request.version||typeof ts.createProgram!=='function'||typeof ts.getPreEmitDiagnostics!=='function')fail('typecheck-sdk-invalid'); const readFile=file=>{if(!fs.existsSync(file))return undefined;return ts.sys.readFile(local(file));}; diff --git a/src/typecheck.js b/src/typecheck.js index eddbded..d661e3e 100644 --- a/src/typecheck.js +++ b/src/typecheck.js @@ -9,7 +9,7 @@ import {readConfig} from './files.js'; const worker=fileURLToPath(new URL('./reporters/typecheck-worker.cjs',import.meta.url)); const hash=value=>createHash('sha256').update(typeof value==='string'||Buffer.isBuffer(value)?value:JSON.stringify(value)).digest('hex'); -const canonical=file=>typeof file==='string'&&file.length>0&&!file.includes('\\')&&!file.includes('\0')&&!path.isAbsolute(file)&&file.split('/').every(x=>x&&x!=='.'&&x!=='..'); +const canonical=file=>typeof file==='string'&&file.length>0&&!file.includes('\\')&&!/[\x00-\x1f\x7f]/.test(file)&&!path.isAbsolute(file)&&file.split('/').every(x=>x&&x!=='.'&&x!=='..'); const inside=(root,file)=>file.startsWith(root+path.sep); function failure(code){throw Object.assign(new Error(code),{code});} function checkDeadline(deadline){if(performance.now()>=deadline)failure('typecheck-timeout');} @@ -21,7 +21,8 @@ function inventory(root,deadline){ function walk(directory,prefix=''){ for(const name of fs.readdirSync(directory).sort()){ checkDeadline(deadline);if(!prefix&&['.git','.tddswarm'].includes(name))continue; - const file=prefix+name,target=path.join(directory,name),stat=fs.lstatSync(target); + const file=prefix+name;if(!canonical(file))failure('typecheck-invalid-input-path'); + const target=path.join(directory,name),stat=fs.lstatSync(target); if(++count>100000)failure('typecheck-inventory-bound'); const rows=file==='node_modules'||file.startsWith('node_modules/')?dependencies:source; if(stat.isSymbolicLink()){ @@ -40,7 +41,51 @@ function inventory(root,deadline){ return {source:{sha256:hash(source),files:source},dependencies:{sha256:hash(dependencies),files:dependencies},bytes,count}; } function runtime(deadline){const executable=fs.realpathSync(process.execPath);return {executable,version:process.version,platform:process.platform,arch:process.arch,sha256:fileHash(executable,deadline)};} -function producers(deadline){return [fileURLToPath(import.meta.url),worker].map(file=>({file:path.basename(file),sha256:fileHash(file,deadline)}));} +const producerFiles=['./typecheck.js','./reporters/typecheck-worker.cjs','./provenance.js','./files.js','./inputs.js','./configuration-inputs.js','./plugin-config.js'].map(file=>fileURLToPath(new URL(file,import.meta.url))); +function producers(deadline){return producerFiles.map(file=>({file:path.relative(path.dirname(fileURLToPath(import.meta.url)),file).split(path.sep).join('/'),sha256:fileHash(file,deadline)}));} +const openingTokens=new WeakMap(); +function environment(home){return {HOME:home,USERPROFILE:home,TMPDIR:home,TMP:home,TEMP:home,PATH:path.dirname(process.execPath)+path.delimiter+'/usr/bin:/bin',LANG:'C',LC_ALL:'C',TZ:'UTC'};} +function captureProvenance(root,deadline,home){ + checkDeadline(deadline);const config=readConfig(root),services={...config.services}; + if(Object.keys(services).length>32)failure('typecheck-service-bound'); + for(const [name,service] of Object.entries(services)){ + if(service?.probe===undefined)continue; + if(!Array.isArray(service.probe)||!service.probe.length||service.probe.length>128||service.probe.some(arg=>typeof arg!=='string'||arg.length>65536))failure('typecheck-invalid-service-probe'); + checkDeadline(deadline);const remaining=Math.floor(deadline-performance.now());if(remaining<1)failure('typecheck-timeout'); + const result=spawnSync(service.probe[0],service.probe.slice(1),{cwd:root,encoding:'utf8',timeout:remaining,maxBuffer:1024*1024,killSignal:'SIGKILL',shell:false,env:environment(home)}); + if(result.error?.code==='ETIMEDOUT')failure('typecheck-timeout'); + if(result.error||result.signal||result.status!==0)failure('typecheck-service-probe-failed'); + services[name]={...service,probe:undefined,version:result.stdout.trim()}; + } + const result=snapshot(root,{...config,services});checkDeadline(deadline);return result; +} +function captureState(root,deadline,home){ + // Bound paths and bytes before executing a configured probe. Its output is + // advisory provenance; final byte capture always follows all probe callbacks. + inventory(root,deadline); + const provenance=captureProvenance(root,deadline,home),inputs=inventory(root,deadline),node=runtime(deadline),producer=producers(deadline); + checkDeadline(deadline);return {root,inputs,provenance,node,producer}; +} +function assertState(expected,current,prefix='typecheck'){ + if(expected.root!==current.root)failure(prefix+'-root-mismatch'); + if(expected.inputs.source.sha256!==current.inputs.source.sha256)failure(prefix+'-source-drift'); + if(expected.inputs.dependencies.sha256!==current.inputs.dependencies.sha256)failure(prefix+'-dependency-drift'); + if(hash(expected.node)!==hash(current.node)||hash(expected.producer)!==hash(current.producer))failure(prefix+'-runtime-drift'); + if(!freshness(expected.provenance,current.provenance).fresh||expected.provenance.revision!==current.provenance.revision)failure(prefix+'-provenance-drift'); +} +/** Opening evidence must be captured before any native config or test executes. + * The exact locally minted object is required; copied/serialized receipts cannot + * carry this freshness binding. It does not authorize routing or publication. */ +export function captureTypecheckInputs(projectRoot,{timeoutMs=30000}={}){ + const started=performance.now();let home; + try{ + if(!Number.isSafeInteger(timeoutMs)||timeoutMs<1||timeoutMs>300000)failure('typecheck-invalid-timeout'); + const deadline=started+timeoutMs,root=fs.realpathSync(projectRoot);home=fs.mkdtempSync(path.join(os.tmpdir(),'testlore-typecheck-opening-'));fs.chmodSync(home,0o700); + const state=captureState(root,deadline,home),token=Object.freeze({schemaVersion:1,kind:'native-typecheck-inputs',status:'captured',complete:true,errorCode:null,exitCode:0,durationMs:Math.round(performance.now()-started),sourceSha256:state.inputs.source.sha256,dependencySha256:state.inputs.dependencies.sha256,provenanceFingerprint:state.provenance.fingerprint,revision:state.provenance.revision}); + openingTokens.set(token,state);return token; + }catch(error){return Object.freeze({schemaVersion:1,kind:'native-typecheck-inputs',status:'incomplete',complete:false,errorCode:error.code?.startsWith('typecheck-')?error.code:'typecheck-input-unavailable',exitCode:2,durationMs:Math.round(performance.now()-started)});} + finally{if(home)fs.rmSync(home,{recursive:true,force:true});} +} function verifyProtocol(result,root,inventoryBefore){ if(!result||result.schemaVersion!==1||!['passed','failed','incomplete'].includes(result.status)||!Array.isArray(result.files)||!Array.isArray(result.diagnostics)||!Array.isArray(result.limitations)||typeof result.compiler?.version!=='string'||typeof result.complete!=='boolean')failure('typecheck-malformed-output'); if(result.files.length>10000||new Set(result.files).size!==result.files.length||result.files.some(file=>!canonical(file)))failure('typecheck-malformed-output'); @@ -64,8 +109,8 @@ function verifyProtocol(result,root,inventoryBefore){ } /** Complementary type oracle. Trusted native execution, not an OS sandbox. */ -export function verifyTypecheck(projectRoot,{project='tsconfig.json',timeoutMs=30000,expectedProvenance}={}){ - const started=performance.now(),base={schemaVersion:1,kind:'native-typecheck',project,status:'incomplete',complete:false,exitCode:2,files:[],diagnostics:[],programFiles:[],implementationFiles:[],suppressions:[],compiler:null,binding:null,signal:null,errorCode:null,reasons:[],durationMs:0,nextAction:'Resolve the incomplete compiler check and run verification again.',scope:'Project-local unreferenced TypeScript implementation scope',limitations:['Trusted compiler execution is not an OS sandbox.','This type oracle grants no test-routing or publication authority.']}; +export function verifyTypecheck(projectRoot,{project='tsconfig.json',timeoutMs=30000,expectedProvenance,openingInputs}={}){ + const started=performance.now(),base={schemaVersion:1,kind:'native-typecheck',project:canonical(project)?project:null,status:'incomplete',complete:false,exitCode:2,files:[],diagnostics:[],programFiles:[],implementationFiles:[],suppressions:[],compiler:null,binding:null,signal:null,errorCode:null,reasons:[],durationMs:0,nextAction:'Resolve the incomplete compiler check and run verification again.',scope:'Project-local unreferenced TypeScript implementation scope',limitations:['Trusted compiler execution is not an OS sandbox.','This type oracle grants no test-routing or publication authority.']}; let home; const finish=extra=>({...base,...extra,durationMs:Math.round(performance.now()-started)}); try{ @@ -78,10 +123,12 @@ export function verifyTypecheck(projectRoot,{project='tsconfig.json',timeoutMs=3 const realConfig=fs.realpathSync(configFile); if(!inside(root,realConfig)||!fs.statSync(configFile).isFile())failure('typecheck-external-config'); if(['.git','.tddswarm'].includes(path.relative(root,realConfig).split(path.sep)[0]))failure('typecheck-reserved-input'); - const before=inventory(root,deadline); - const beforeProvenance=snapshot(root,readConfig(root));checkDeadline(deadline); + if(openingInputs!==undefined&&!openingTokens.has(openingInputs))failure('typecheck-invalid-opening-inputs'); + home=fs.mkdtempSync(path.join(os.tmpdir(),'testlore-typecheck-'));fs.chmodSync(home,0o700); + const beforeState=captureState(root,deadline,home),before=beforeState.inputs,beforeProvenance=beforeState.provenance; + if(openingInputs!==undefined)assertState(openingTokens.get(openingInputs),beforeState,'typecheck-opening'); if(expectedProvenance){const check=freshness(expectedProvenance,beforeProvenance);if(!check.fresh||expectedProvenance.revision!==beforeProvenance.revision)failure('typecheck-provenance-mismatch');} - const nodeBefore=runtime(deadline),producerBefore=producers(deadline); + const nodeBefore=beforeState.node,producerBefore=beforeState.producer; base.binding={source:before.source,dependencies:before.dependencies,provenance:beforeProvenance,node:nodeBefore,producers:producerBefore}; const sdk=path.join(root,'node_modules/typescript/lib/typescript.js'),manifest=path.join(root,'node_modules/typescript/package.json'); if(!fs.existsSync(sdk)||!fs.existsSync(manifest))failure('typecheck-sdk-missing'); @@ -90,20 +137,16 @@ export function verifyTypecheck(projectRoot,{project='tsconfig.json',timeoutMs=3 if(packageData.name!=='typescript'||typeof packageData.version!=='string')failure('typecheck-sdk-invalid'); const compiler={version:packageData.version,file:path.relative(root,fs.realpathSync(sdk)),sha256:fileHash(sdk,deadline),manifestSha256:fileHash(manifest,deadline)}; base.compiler=compiler; - home=fs.mkdtempSync(path.join(os.tmpdir(),'testlore-typecheck-'));fs.chmodSync(home,0o700); const remaining=Math.floor(deadline-performance.now());if(remaining<1)failure('typecheck-timeout'); - const child=spawnSync(nodeBefore.executable,['--unhandled-rejections=strict',worker],{cwd:root,input:JSON.stringify({root,project,sdk,version:compiler.version}),encoding:'utf8',timeout:remaining,maxBuffer:4*1024*1024,killSignal:'SIGKILL',env:{HOME:home,USERPROFILE:home,TMPDIR:home,TMP:home,TEMP:home,PATH:path.dirname(nodeBefore.executable)+path.delimiter+'/usr/bin:/bin',LANG:'C',LC_ALL:'C',TZ:'UTC'}}); + const child=spawnSync(nodeBefore.executable,['--unhandled-rejections=strict',worker],{cwd:root,input:JSON.stringify({root,project,sdk,version:compiler.version}),encoding:'utf8',timeout:remaining,maxBuffer:4*1024*1024,killSignal:'SIGKILL',env:environment(home)}); base.signal=child.signal||null; if(child.error){failure(child.error.code==='ETIMEDOUT'?'typecheck-timeout':'typecheck-worker-error');} if(child.signal||child.status!==0||child.stderr)failure('typecheck-worker-error'); let parsed;try{parsed=JSON.parse(child.stdout);}catch{failure('typecheck-malformed-output');} const result=verifyProtocol(parsed,root,before); if(result.compiler.version!==compiler.version)failure('typecheck-sdk-drift'); - const after=inventory(root,deadline),nodeAfter=runtime(deadline),producerAfter=producers(deadline),afterProvenance=snapshot(root,readConfig(root));checkDeadline(deadline); - if(before.source.sha256!==after.source.sha256)failure('typecheck-source-drift'); - if(before.dependencies.sha256!==after.dependencies.sha256)failure('typecheck-dependency-drift'); - if(hash(nodeBefore)!==hash(nodeAfter)||hash(producerBefore)!==hash(producerAfter))failure('typecheck-runtime-drift'); - if(!freshness(beforeProvenance,afterProvenance).fresh||beforeProvenance.revision!==afterProvenance.revision)failure('typecheck-provenance-drift'); + const afterState=captureState(root,deadline,home);assertState(beforeState,afterState); + if(openingInputs!==undefined)assertState(openingTokens.get(openingInputs),afterState,'typecheck-opening'); return finish({status:result.status,complete:result.complete,exitCode:result.complete?(result.status==='failed'?1:0):2,files:result.files,diagnostics:result.diagnostics,programFiles:result.programFiles,implementationFiles:result.implementationFiles,suppressions:result.suppressions,compiler,binding:{source:before.source,dependencies:before.dependencies,provenance:beforeProvenance,node:nodeBefore,producers:producerBefore,projectSha256:fileHash(configFile,deadline)},errorCode:result.errorCode||null,reasons:result.errorCode?[result.errorCode]:[],limitations:[...base.limitations,...result.limitations],nextAction:result.status==='passed'?'Type checking passed; continue with independent runtime tests.':result.status==='failed'?'Repair the reported type diagnostics, then rerun type checking and runtime tests.':'Resolve unsupported compiler configuration or incomplete scope, then rerun.'}); }catch(error){const errorCode=error.code?.startsWith('typecheck-')?error.code:'typecheck-input-unavailable';return finish({errorCode,reasons:[errorCode]});} finally{if(home)fs.rmSync(home,{recursive:true,force:true});} diff --git a/test/typecheck.test.js b/test/typecheck.test.js index d9dc78e..44e9ff7 100644 --- a/test/typecheck.test.js +++ b/test/typecheck.test.js @@ -4,8 +4,8 @@ import fs from 'node:fs'; import path from 'node:path'; import {fileURLToPath} from 'node:url'; import {spawnSync} from 'node:child_process'; -import {fixture,write} from './helpers.js'; -import {verifyTypecheck} from '../src/typecheck.js'; +import {fixture,write,commit} from './helpers.js'; +import {verifyTypecheck,captureTypecheckInputs} from '../src/typecheck.js'; import {snapshot} from '../src/provenance.js'; import {readConfig} from '../src/files.js'; @@ -26,7 +26,7 @@ const hook=(root,code)=>{const file=path.join(root,'node_modules/typescript/lib/ test('native runtime green cannot hide TS2322; healthy inverse is genuinely checked and read-only',t=>{ const root=project(t,{'src/value.ts':'export const value: string = 42;\n','test/runtime.test.mjs':"import test from 'node:test';import assert from 'node:assert/strict';import {value} from '../src/value.ts';test('runtime value',()=>assert.equal(value,42));"}); - const runtime=spawnSync(process.execPath,['--experimental-strip-types','--test','test/runtime.test.mjs'],{cwd:root,encoding:'utf8'});assert.equal(runtime.status,0,runtime.stderr); + const runtime=spawnSync(process.execPath,['--experimental-strip-types','--test','test/runtime.test.mjs'],{cwd:root,encoding:'utf8',env:{HOME:root,LANG:'C',PATH:path.dirname(process.execPath)+path.delimiter+'/usr/bin:/bin'}});assert.equal(runtime.status,0,runtime.stderr);assert.match(runtime.stdout,/# tests 1/);assert.match(runtime.stdout,/# pass 1/); const result=check(root);assert.equal(result.status,'failed',JSON.stringify({status:result.status,errorCode:result.errorCode,diagnostics:result.diagnostics}));assert.equal(result.exitCode,1);assert.equal(result.complete,true);assert.ok(result.diagnostics.some(d=>d.code===2322&&d.file==='src/value.ts'&&d.line===1));assert.ok(result.compiler.version);assert.ok(result.binding.source.files.some(row=>row.file==='ignored.txt'));assert.ok(result.files.includes('src/value.ts')); write(root,'src/value.ts','export const value: number = 42;\n');const healthy=check(root);assert.equal(healthy.status,'passed',JSON.stringify({status:healthy.status,errorCode:healthy.errorCode,diagnostics:healthy.diagnostics}));assert.equal(healthy.exitCode,0);assert.equal(healthy.complete,true);assert.equal(healthy.diagnostics.length,0);assert.ok(healthy.limitations.some(s=>s.includes('skipLibCheck')));assert.equal(fs.existsSync(path.join(root,'src/value.js')),false);assert.equal(fs.existsSync(path.join(root,'tsconfig.tsbuildinfo')),false); }); @@ -94,3 +94,33 @@ test('reserved state configurations and real-path aliases never become unbound n fs.symlinkSync(path.join(root,'.tddswarm/shared.json'),path.join(root,'config-alias.json'));result=verifyTypecheck(root,{project:'config-alias.json',timeoutMs:20000});assert.equal(result.errorCode,'typecheck-reserved-input'); write(root,'.tddswarm/source.ts','export const hidden=1;');write(root,'tsconfig.json',{files:['.tddswarm/source.ts']});result=check(root);assert.equal(result.errorCode,'typecheck-reserved-input');assert.equal(result.complete,false); }); + +test('final service provenance callbacks cannot mutate ignored compiler input behind successful evidence',t=>{ + const root=project(t);write(root,'.gitignore','src/value.ts\nnode_modules/\n.tddswarm/\n');write(root,'.tddswarm/probe-count','0'); + const code="const fs=require('node:fs');const n=Number(fs.readFileSync('.tddswarm/probe-count'))+1;fs.writeFileSync('.tddswarm/probe-count',String(n));if(n===2)fs.writeFileSync('src/value.ts','export const value: number=\"broken\";');process.stdout.write('stable-service-version');"; + write(root,'tddswarm.config.json',{services:{example:{tests:['test/example.test.js'],probe:[process.execPath,'-e',code]}}});commit(root);const result=check(root);assert.equal(Number(fs.readFileSync(path.join(root,'.tddswarm/probe-count'))),2);assert.equal(result.errorCode,'typecheck-source-drift');assert.equal(result.complete,false);assert.equal(result.exitCode,2); +}); + +test('service probes use a bare environment and share the total deadline',t=>{ + const root=project(t);write(root,'tddswarm.config.json',{services:{example:{tests:['test/example.test.js'],probe:[process.execPath,'-e',"if(process.env.TESTLORE_SECRET_SENTINEL||process.env.NODE_OPTIONS)process.exit(3);process.stdout.write('stable');"]}}}); + const expectedProvenance=snapshot(root,readConfig(root));assert.equal(verifyTypecheck(root,{expectedProvenance,timeoutMs:20000}).status,'passed','Successful service probe binding must match the existing provenance contract'); + const prior=process.env.TESTLORE_SECRET_SENTINEL;try{process.env.TESTLORE_SECRET_SENTINEL='private-test-sentinel';assert.equal(check(root).status,'passed');}finally{if(prior===undefined)delete process.env.TESTLORE_SECRET_SENTINEL;else process.env.TESTLORE_SECRET_SENTINEL=prior;} + write(root,'tddswarm.config.json',{services:{example:{tests:['test/example.test.js'],probe:[process.execPath,'-e','setTimeout(()=>process.stdout.write("stable"),5000)']}}});const started=performance.now(),result=verifyTypecheck(root,{timeoutMs:500});assert.equal(result.errorCode,'typecheck-timeout');assert.equal(result.exitCode,2);assert.ok(performance.now()-started<2500,'Configured probe must respect remaining total budget'); +}); + +test('ASCII control and DEL paths are rejected without returning unsafe path prose',t=>{ + const root=project(t);for(const unsafe of ['src/\x1b[31mbad.ts','src/bad\nname.ts','src/bad\x7fname.ts']){ + write(root,unsafe,'export const value: string=42;');const result=check(root);assert.equal(result.status,'incomplete');assert.equal(result.errorCode,'typecheck-invalid-input-path');assert.equal(result.exitCode,2);assert.equal(/[\x00-\x1f\x7f]/.test(result.errorCode),false);fs.rmSync(path.join(root,unsafe)); + } + for(const project of ['\x1b[31mconfig.json','bad\nconfig.json','bad\x7fconfig.json']){const result=verifyTypecheck(root,{project});assert.equal(result.errorCode,'typecheck-invalid-project');assert.equal(result.project,null);} +}); + +test('private opening inputs bind ignored source and SDK across prior native execution',t=>{ + const root=project(t);write(root,'.gitignore','src/value.ts\nnode_modules/\n');const token=captureTypecheckInputs(root,{timeoutMs:20000});assert.equal(token.status,'captured');assert.equal(token.complete,true);assert.equal(Object.isFrozen(token),true); + write(root,'test/native.test.mjs',"import test from 'node:test';import assert from 'node:assert/strict';import fs from 'node:fs';import {value} from '../src/value.ts';test('native then mutates',()=>{assert.equal(value,42);fs.writeFileSync('src/value.ts','export const value: number=99;');});"); + // Capture after installation of the independent native oracle, before it runs. + commit(root);const opening=captureTypecheckInputs(root,{timeoutMs:20000});const native=spawnSync(process.execPath,['--experimental-strip-types','--test','test/native.test.mjs'],{cwd:root,encoding:'utf8',env:{HOME:root,LANG:'C',PATH:path.dirname(process.execPath)+path.delimiter+'/usr/bin:/bin'}});assert.equal(native.status,0,native.stderr);assert.match(native.stdout,/# tests 1/);assert.match(native.stdout,/# pass 1/);assert.match(fs.readFileSync(path.join(root,'src/value.ts'),'utf8'),/99/); + const changed=verifyTypecheck(root,{openingInputs:opening,timeoutMs:20000});assert.equal(changed.errorCode,'typecheck-opening-source-drift');assert.equal(changed.exitCode,2);assert.equal(changed.complete,false);assert.equal(check(root).status,'passed','Standalone current-state type check alone would not detect prior runtime drift'); + write(root,'src/value.ts','export const value: number=42;');const sdkOpening=captureTypecheckInputs(root,{timeoutMs:20000});hook(root,"require('node:fs').writeFileSync('compiler-entered.txt','yes');");const sdkChanged=verifyTypecheck(root,{openingInputs:sdkOpening,timeoutMs:20000});assert.equal(sdkChanged.errorCode,'typecheck-opening-dependency-drift');assert.equal(fs.existsSync(path.join(root,'compiler-entered.txt')),false); + for(const copied of [{...sdkOpening},JSON.parse(JSON.stringify(sdkOpening)),null])assert.equal(verifyTypecheck(root,{openingInputs:copied,timeoutMs:20000}).errorCode,'typecheck-invalid-opening-inputs'); +}); From 26019c0620687844c11eb4fbb4a7292d482c20c8 Mon Sep 17 00:00:00 2001 From: Rudy Celekli Date: Mon, 12 Oct 2026 23:27:49 -0400 Subject: [PATCH 04/16] Require repeated native TypeScript admission for enabled repairs --- src/repair-candidates.js | 91 ++++++++++++++++++++++++++++++++--- test/repair-typecheck.test.js | 56 +++++++++++++++++++++ 2 files changed, 139 insertions(+), 8 deletions(-) create mode 100644 test/repair-typecheck.test.js diff --git a/src/repair-candidates.js b/src/repair-candidates.js index 64a7477..3b48a55 100644 --- a/src/repair-candidates.js +++ b/src/repair-candidates.js @@ -6,6 +6,8 @@ import {spawnSync} from 'node:child_process'; import {SOURCE, TEST, safePath, readConfig} from './files.js'; import {digest, freshness, snapshot} from './provenance.js'; import {adapterFor, discover, execute, nativeEnvironment} from './execution.js'; +import {captureTypecheckInputs,verifyTypecheck} from './typecheck.js'; +import {fileURLToPath} from 'node:url'; import {withRepairEnvironment} from './repair-environment.js'; import {captureRepairChallenges,captureChallengeRuntime,captureNativeRepairRuntime,runRepairChallenges,assessRepairChallenges} from './repair-challenges.js'; @@ -104,7 +106,7 @@ export function stageRepair(root, {files, requirements, sourcePaths, review} = { for (const file of files) { const target = safePath(directory,`files/${file.path}`); fs.mkdirSync(path.dirname(target),{recursive:true}); fs.writeFileSync(target,file.content,{flag:'wx'}); } const challenges=captureRepairChallenges(root); if(challenges.status==='configured'&&challenges.cases.some(item=>sourcePaths.includes(item.script)))throw Error('Repair source cannot own challenge assertions'); - const manifest = {schemaVersion:1,kind:'source-repair',id,state:before,sourcePaths,files:files.map(file=>({path:file.path,hash:digest(file.content),beforeHash:before.scope.files[file.path].hash})),requirementsPath:'tddswarm.requirements.md',requirements,requirementsHash:digest(requirements),review,challenges,...(challenges.status==='configured'?{challengeRuntime:captureChallengeRuntime()}:{})}; + const manifest = {schemaVersion:1,kind:'source-repair',id,state:before,sourcePaths,files:files.map(file=>({path:file.path,hash:digest(file.content),beforeHash:before.scope.files[file.path].hash})),requirementsPath:'tddswarm.requirements.md',requirements,requirementsHash:digest(requirements),review,challenges,...(repairTypecheckSettings(readConfig(root))?{typecheck:repairTypecheckSettings(readConfig(root))}:{}),...(challenges.status==='configured'?{challengeRuntime:captureChallengeRuntime()}:{})}; const serialized=JSON.stringify(manifest,null,2);if(Buffer.byteLength(serialized)>MAX_REPORT){fs.rmSync(directory,{recursive:true,force:true});throw new Error('Repair manifest evidence exceeds bound');} fs.writeFileSync(path.join(directory,'manifest.json'),serialized); return {id,directory,candidatePath,kind:'source-repair',status:review.accepted?'reviewed-repair':'rejected-repair',applied:false}; @@ -160,11 +162,71 @@ function seal(root,value,{create=false}={}) { if(key.length!==32)throw new Error('Invalid local repair validation key'); return createHmac('sha256',key).update(JSON.stringify(value)).digest('hex'); } -function copied(root, scope, action) { +function repairTypecheckSettings(config){const value=config.plugins?.typescript;return value?.enabled===true?{project:value.project||'tsconfig.json',timeoutMs:value.timeoutMs||20000}:null;} +const typecheckProducerFiles=['./repair-candidates.js','./typecheck.js','./reporters/typecheck-worker.cjs','./provenance.js','./files.js','./inputs.js','./configuration-inputs.js','./plugin-config.js'].map(file=>fileURLToPath(new URL(file,import.meta.url))); +function typecheckProducers(){return typecheckProducerFiles.map(file=>({file:path.relative(path.dirname(fileURLToPath(import.meta.url)),file).split(path.sep).join('/'),sha256:digest(fs.readFileSync(file))}));} +const sha=value=>typeof value==='string'&&/^[a-f0-9]{64}$/.test(value); +function typecheckValid(run,{candidate=false}={}){ + const errors=Array.isArray(run?.diagnostics)&&run.diagnostics.filter(item=>item?.category==='error'); + const paths=value=>Array.isArray(value)&&value.length<=10000&&new Set(value).size===value.length&&value.every(file=>canonical(file)&&!/[\x00-\x1f\x7f]/.test(file)); + if(!paths(run?.files)||!paths(run?.implementationFiles)||!paths(run?.programFiles)||!run.files.every(file=>run.programFiles.includes(file))||!Array.isArray(run?.diagnostics)||run.diagnostics.some(row=>!row||!Number.isSafeInteger(row.code)||row.code<1||!['error','warning','message','suggestion'].includes(row.category)||!(row.file===null||canonical(row.file)&&!/[\x00-\x1f\x7f]/.test(row.file))||!(row.line===null||Number.isSafeInteger(row.line)&&row.line>0)||!(row.column===null||Number.isSafeInteger(row.column)&&row.column>0)))return false; + return run?.schemaVersion===1&&run.kind==='native-typecheck'&&run.complete===true&&run.signal===null&&run.errorCode===null&&Array.isArray(run.reasons)&&run.reasons.length===0&&Array.isArray(run.implementationFiles)&&run.implementationFiles.length>0&&digest(run.implementationFiles)===digest(run.files)&&sha(run.compiler?.sha256)&&sha(run.compiler?.manifestSha256)&&typeof run.compiler.version==='string'&&sha(run.binding?.source?.sha256)&&sha(run.binding?.dependencies?.sha256)&&sha(run.binding?.node?.sha256)&&Array.isArray(run.binding?.source?.files)&&((run.exitCode===0&&run.status==='passed'&&errors?.length===0)||(!candidate&&run.exitCode===1&&run.status==='failed'&&errors?.length>0)); +} +function typecheckOutcome(run){return digest({status:run.status,exitCode:run.exitCode,files:run.implementationFiles,programFiles:run.programFiles,suppressions:run.suppressions,diagnostics:run.diagnostics.map(({code,category,file,line,column})=>({code,category,file,line,column}))});} +function typedRepeatable(baseline,candidate){return baseline.length===2&&candidate.length===2&&typecheckOutcome(baseline[0])===typecheckOutcome(baseline[1])&&typecheckOutcome(candidate[0])===typecheckOutcome(candidate[1]);} +function typedInstalledBinding(root,{deadline=performance.now()+30000}={}){ + const installed=fs.realpathSync(path.join(root,'node_modules')),rows=[];let bytes=0,count=0; + function walk(target,file){ + if(performance.now()>deadline)throw Error('repair-total-time-budget-exceeded');if(++count>100000)throw Error('typed-repair-installed-bound'); + const stat=fs.lstatSync(target); + if(stat.isDirectory()){rows.push({file,kind:'directory',mode:stat.mode&0o777});for(const name of fs.readdirSync(target).sort())walk(path.join(target,name),file+'/'+name);} + else if(stat.isSymbolicLink()){const link=fs.readlinkSync(target),real=fs.realpathSync(target);if(path.isAbsolute(link)||!real.startsWith(installed+path.sep))throw Error('typed-repair-dependency-link-unsupported');rows.push({file,kind:'symlink',target:link,real:'node_modules/'+path.relative(installed,real).split(path.sep).join('/')});} + else if(stat.isFile()){bytes+=stat.size;if(bytes>1024*1024*1024)throw Error('typed-repair-installed-bound');rows.push({file,kind:'file',size:stat.size,mode:stat.mode&0o777,sha256:digest(fs.readFileSync(target))});}else throw Error('typed-repair-dependency-entry-unsupported'); + } + walk(installed,'node_modules'); + const sdk=fs.realpathSync(path.join(root,'node_modules/typescript/lib/typescript.js')),manifest=fs.realpathSync(path.join(root,'node_modules/typescript/package.json')); + if(!sdk.startsWith(installed+path.sep)||!manifest.startsWith(installed+path.sep))throw Error('typed-repair-sdk-closure-unsupported'); + const packageData=JSON.parse(fs.readFileSync(manifest,'utf8'));if(packageData.name!=='typescript')throw Error('typed-repair-sdk-invalid'); + return {rows,compiler:{version:packageData.version,file:'node_modules/'+path.relative(installed,sdk).split(path.sep).join('/'),sha256:digest(fs.readFileSync(sdk)),manifestSha256:digest(fs.readFileSync(manifest))},node:{executable:fs.realpathSync(process.execPath),version:process.version,platform:process.platform,arch:process.arch,sha256:digest(fs.readFileSync(fs.realpathSync(process.execPath)))}}; +} +function assertTypedReceipt(root,manifest,receipt,{deadline=performance.now()+30000}={}){ + const currentSettings=repairTypecheckSettings(readConfig(root)),settings=manifest.typecheck||currentSettings;if(!settings)return; + if(digest(settings)!==digest(currentSettings))throw Error('Repair TypeScript obligation or settings changed'); + const evidence=receipt.typecheck; + if(!evidence||evidence.schemaVersion!==1||evidence.kind!=='repair-typecheck'||evidence.accepted!==true||digest(evidence.settings)!==digest(settings)||digest(manifest.typecheck)!==digest(settings)||!Array.isArray(evidence.baseline)||evidence.baseline.length!==2||!Array.isArray(evidence.candidate)||evidence.candidate.length!==2||evidence.baseline.some(run=>!typecheckValid(run))||evidence.candidate.some(run=>!typecheckValid(run,{candidate:true})))throw Error('Authenticated repeated TypeScript repair validation required'); + if(!typedRepeatable(evidence.baseline,evidence.candidate))throw Error('Repair TypeScript outcomes are not repeatable'); + if(digest(evidence.producers)!==digest(typecheckProducers())||evidence.dependencyFingerprint!==manifest.state.dependencies.fingerprint)throw Error('Repair TypeScript compiler producer or dependency binding changed'); + const installed=typedInstalledBinding(root,{deadline}),verifierProducers=typecheckProducers().filter(row=>row.file!=='repair-candidates.js'); + const compilerIdentity=run=>({compiler:run.compiler,node:run.binding.node,producers:run.binding.producers,dependencies:run.binding.dependencies.sha256,project:run.project}); + const all=[...evidence.baseline,...evidence.candidate];if(all.some(run=>digest(compilerIdentity(run))!==digest(compilerIdentity(all[0]))||run.project!==settings.project))throw Error('Repair TypeScript native identities differ'); + for(const run of all){if(run.binding.source.sha256!==digest(run.binding.source.files)||run.binding.dependencies.sha256!==digest(run.binding.dependencies.files)||digest(run.binding.dependencies.files)!==digest(installed.rows)||digest(run.compiler)!==digest(installed.compiler)||digest(run.binding.node)!==digest(installed.node)||digest(run.binding.producers)!==digest(verifierProducers)||run.binding.projectSha256!==manifest.state.scope.files[settings.project]?.hash)throw Error('Repair TypeScript current compiler or input inventory differs from authenticated evidence');} + for(const [runs,candidate] of [[evidence.baseline,false],[evidence.candidate,true]])for(const run of runs){ + const expected=Object.fromEntries(Object.entries(manifest.state.scope.files).map(([file,value])=>[file,{hash:candidate?(manifest.files.find(item=>item.path===file)?.hash??value.hash):value.hash,mode:value.mode}])); + const actual=Object.fromEntries(run.binding.source.files.filter(row=>row.kind==='file').map(row=>[row.file,{hash:row.sha256,mode:row.mode}])); + const sorted=value=>Object.fromEntries(Object.entries(value).sort(([a],[b])=>ab?1:0)); + if(digest(sorted(actual))!==digest(sorted(expected)))throw Error('Repair TypeScript source evidence differs from authenticated manifest'); + } +} +function copied(root, scope, action,{typed=false,deadline=Infinity,dependencies}={}) { const temp=fs.mkdtempSync(path.join(os.tmpdir(),'testlore-repair-')); try { for(const [file,value] of Object.entries(scope.files)){const target=safePath(temp,file);fs.mkdirSync(path.dirname(target),{recursive:true});fs.copyFileSync(regular(root,file),target);fs.chmodSync(target,value.mode);} - if(fs.existsSync(path.join(root,'node_modules')))fs.symlinkSync(fs.realpathSync(path.join(root,'node_modules')),path.join(temp,'node_modules'),'dir'); + if(fs.existsSync(path.join(root,'node_modules'))){ + if(typed){ + const installed=fs.realpathSync(path.join(root,'node_modules')); + // Absolute/exterior links cannot be relocated without changing native + // origin semantics. Decline rather than weakening the compiler closure. + function copy(from,to){for(const name of fs.readdirSync(from).sort()){ + if(performance.now()>deadline)throw Error('repair-total-time-budget-exceeded'); + const source=path.join(from,name),target=path.join(to,name),stat=fs.lstatSync(source); + if(stat.isDirectory()){fs.mkdirSync(target);copy(source,target);fs.chmodSync(target,stat.mode&0o777);} + else if(stat.isSymbolicLink()){const link=fs.readlinkSync(source);if(path.isAbsolute(link)||!fs.realpathSync(source).startsWith(installed+path.sep))throw Error('typed-repair-dependency-link-unsupported');fs.symlinkSync(link,target);} + else if(stat.isFile()){fs.copyFileSync(source,target);fs.chmodSync(target,stat.mode&0o777);}else throw Error('typed-repair-dependency-entry-unsupported'); + }} + fs.mkdirSync(path.join(temp,'node_modules'));copy(installed,path.join(temp,'node_modules'));fs.chmodSync(path.join(temp,'node_modules'),fs.statSync(installed).mode&0o777); + if(tree(temp,{dependencies:true,deadline}).fingerprint!==dependencies.fingerprint)throw Error('typed-repair-installed-copy-differs'); + }else fs.symlinkSync(fs.realpathSync(path.join(root,'node_modules')),path.join(temp,'node_modules'),'dir'); + } return action(temp); }finally{fs.rmSync(temp,{recursive:true,force:true});} } @@ -172,14 +234,16 @@ function copied(root, scope, action) { export function validateRepair(root, candidatePath, options={}) { const started=performance.now(),total=options.totalTimeoutMs??120000,perRun=options.timeoutMs??20000; if(!Number.isInteger(total)||total<1||total>600000||!Number.isInteger(perRun)||perRun<1||perRun>120000)throw new Error('Invalid bounded repair runtime budget'); - const deadline=started+total,{directory,manifest,hash}=locate(root,candidatePath),baseline=[],candidateRuns=[],assertionRuns=[],challengeBaseline=[],challengeCandidate=[],reasons=[]; + const deadline=started+total,{directory,manifest,hash}=locate(root,candidatePath),baseline=[],candidateRuns=[],assertionRuns=[],challengeBaseline=[],challengeCandidate=[],typecheckBaseline=[],typecheckCandidate=[],reasons=[]; + let typecheckSettings=null,typecheckProducerBinding=null; const remaining=()=>{const value=Math.floor(Math.min(perRun,deadline-performance.now()));if(value<1)throw new Error('repair-total-time-budget-exceeded');return value;}; const nativeFresh=()=>{if(manifest.state.nativeRuntime?.status!=='qualified'||digest(captureNativeRepairRuntime(root))!==digest(manifest.state.nativeRuntime))throw Error('native-execution-mutated-runtime');}; try { assertFresh(manifest,state(root,deadline)); if(manifest.state.nativeRuntime?.status!=='qualified')throw new Error('assertion-evidence-unsupported: '+(manifest.state.nativeRuntime?.reason||'repair-native-runtime-binding-required')); if(!manifest.review.accepted||!manifest.review.oracle?.independent||!Array.isArray(manifest.review.oracle.basis)||!manifest.review.oracle.basis.length||manifest.review.oracle.basis.some(x=>typeof x!=='string'||!x.trim()||x.length>4096))throw new Error('independent-repair-review-required'); - const config=readConfig(root); + const config=readConfig(root);typecheckSettings=repairTypecheckSettings(config); + if(typecheckSettings){if(digest(manifest.typecheck)!==digest(typecheckSettings))throw Error('repair-TypeScript-settings-drift');typecheckProducerBinding=typecheckProducers();} const argv=config.runner||['node','--test','{files}']; if(adapterFor(config)!=='node'||config.integration||Array.isArray(config.discovery)||!/(?:^|[\/\\])node(?:\.exe)?$/.test(argv[0])||!argv.includes('--test')||argv.slice(1).some(arg=>arg!=='{files}'&&!arg.startsWith('-'))||argv.some(arg=>/^--test-(?:name-pattern|skip-pattern|only|force-exit)(?:=|$)/.test(arg)))throw new Error('assertion-evidence-unsupported'); for(const candidate of [false,false,true,true]) { @@ -187,6 +251,8 @@ export function validateRepair(root, candidatePath, options={}) { const result=copied(root,manifest.state.scope,temp=>{ if(candidate)for(const file of manifest.files)fs.writeFileSync(regular(temp,file.path),fs.readFileSync(regular(directory,`files/${file.path}`))); const before=tree(temp,{deadline}),localConfig=readConfig(temp); + const opening=typecheckSettings?captureTypecheckInputs(temp,{timeoutMs:remaining()}):null; + if(typecheckSettings&&opening.complete!==true)throw Error('repair-TypeScript-opening-incomplete:'+(opening.errorCode||'unknown')); return withRepairEnvironment(localConfig,isolatedConfig=>{ const discovery=discover(temp,{...isolatedConfig,discovery:'native',runnerTimeoutMs:remaining()}); nativeFresh(); @@ -209,9 +275,15 @@ export function validateRepair(root, candidatePath, options={}) { (candidate?challengeCandidate:challengeBaseline).push(challenge); if(!challenge.accepted)throw Error(candidate?'independent-challenge-candidate-regression':'independent-challenge-baseline-not-passing'); } + if(typecheckSettings){ + const typed=verifyTypecheck(temp,{project:typecheckSettings.project,timeoutMs:Math.min(typecheckSettings.timeoutMs,remaining()),openingInputs:opening}); + (candidate?typecheckCandidate:typecheckBaseline).push(typed); + if(!typecheckValid(typed,{candidate}))throw Error(candidate?'candidate-TypeScript-not-complete-green':'baseline-TypeScript-incomplete'); + if(digest(typecheckProducerBinding)!==digest(typecheckProducers()))throw Error('repair-TypeScript-producer-drift'); + } return report; }); - }); + },{typed:Boolean(typecheckSettings),deadline,dependencies:manifest.state.dependencies}); if(tree(root,{dependencies:true,deadline}).fingerprint!==manifest.state.dependencies.fingerprint)throw new Error('native-execution-mutated-dependencies'); if(digest(captureNativeRepairRuntime(root))!==digest(manifest.state.nativeRuntime))throw Error('native-execution-mutated-runtime'); if(baseline.length===2&&digest(inventory(baseline[0]))!==digest(inventory(baseline[1])))throw new Error('baseline-case-outcomes-not-repeatable'); @@ -222,14 +294,16 @@ export function validateRepair(root, candidatePath, options={}) { }catch(error){reasons.push(error.message.slice(0,1000));} const challenges=manifest.challenges?.status==='configured'?assessRepairChallenges(challengeBaseline,challengeCandidate):{status:'not-configured',accepted:false,next:['Suite-only validation does not qualify production repair readiness; configure independent behavioral challenges.']}; if(manifest.challenges?.status==='configured'&&!challenges.accepted)reasons.push(...challenges.reasons); - const result={schemaVersion:1,kind:'source-repair',accepted:!reasons.length&&baseline.length===2&&candidateRuns.length===2&&assertionRuns.length===2,reasons,manifestHash:hash,inputFingerprint:digest(manifest.state),baseline,candidateRuns,assertionRuns,challenges,qualification:challenges.accepted?'suite-and-independent-challenges':'original-suite-only',original:baseline[0]??null,candidate:candidateRuns[0]??null,durationMs:Math.round(performance.now()-started),bounds:{totalTimeoutMs:total,perNativeTimeoutMs:perRun,retainedReportBytes:MAX_REPORT,nativeTransportMaxBufferBytes:64*1024*1024},cost:null}; - if(Buffer.byteLength(JSON.stringify(result,null,2))>MAX_REPORT-512){result.accepted=false;result.reasons.push('repair-total-evidence-output-budget-exceeded');result.baseline=[];result.candidateRuns=[];result.assertionRuns=[];result.challenges={status:'rejected',accepted:false,reasons:['repair-total-evidence-output-budget-exceeded']};result.original=null;result.candidate=null;} + const result={schemaVersion:1,kind:'source-repair',accepted:!reasons.length&&baseline.length===2&&candidateRuns.length===2&&assertionRuns.length===2,reasons,manifestHash:hash,inputFingerprint:digest(manifest.state),baseline,candidateRuns,assertionRuns,challenges,qualification:challenges.accepted?'suite-and-independent-challenges':'original-suite-only',original:baseline[0]??null,candidate:candidateRuns[0]??null,durationMs:Math.round(performance.now()-started),bounds:{totalTimeoutMs:total,perNativeTimeoutMs:perRun,retainedReportBytes:MAX_REPORT,nativeTransportMaxBufferBytes:64*1024*1024},cost:null,...(typecheckSettings?{typecheck:{schemaVersion:1,kind:'repair-typecheck',settings:typecheckSettings,producers:typecheckProducerBinding,dependencyFingerprint:manifest.state.dependencies.fingerprint,baseline:typecheckBaseline,candidate:typecheckCandidate,accepted:typecheckBaseline.length===2&&typecheckCandidate.length===2&&typecheckBaseline.every(run=>typecheckValid(run))&&typecheckCandidate.every(run=>typecheckValid(run,{candidate:true}))&&typedRepeatable(typecheckBaseline,typecheckCandidate)}}:{})}; + if(typecheckSettings&&result.typecheck.accepted!==true){result.accepted=false;if(!result.reasons.includes('repair-TypeScript-repeatable-evidence-required'))result.reasons.push('repair-TypeScript-repeatable-evidence-required');} + if(Buffer.byteLength(JSON.stringify(result,null,2))>MAX_REPORT-512){result.accepted=false;result.reasons.push('repair-total-evidence-output-budget-exceeded');result.baseline=[];result.candidateRuns=[];result.assertionRuns=[];result.challenges={status:'rejected',accepted:false,reasons:['repair-total-evidence-output-budget-exceeded']};if(result.typecheck){result.typecheck.accepted=false;result.typecheck.baseline=[];result.typecheck.candidate=[];}result.original=null;result.candidate=null;} result.integrity=seal(root,result,{create:true});fs.writeFileSync(safePath(directory,'validation.json'),JSON.stringify(result,null,2));return result; } /** Apply only an accepted, fresh source repair. No tests or requirements are touched. */ export function applyRepair(root,candidatePath) { const {directory,manifest,hash}=locate(root,candidatePath),receipt=readJson(regular(directory,'validation.json')),{integrity,...body}=receipt; if(integrity!==seal(root,body)||!receipt.accepted||receipt.kind!=='source-repair'||receipt.manifestHash!==hash||receipt.inputFingerprint!==digest(manifest.state)||receipt.baseline?.length!==2||receipt.candidateRuns?.length!==2||receipt.assertionRuns?.length!==2)throw new Error('Accepted intact repair validation required'); + assertTypedReceipt(root,manifest,receipt); if(manifest.challenges?.status==='configured'&&!receipt.challenges?.accepted)throw Error('Accepted independent regression challenges required'); if(manifest.challengeRuntime&&digest(captureChallengeRuntime())!==digest(manifest.challengeRuntime))throw Error('Repair challenge runtime changed before apply'); assertFresh(manifest,state(root,performance.now()+30000)); @@ -249,6 +323,7 @@ export function verifyAppliedRepairValidation(root,candidatePath,{deadline=perfo if(!Number.isFinite(deadline)||deadline<=performance.now())throw Error('Repair publication freshness deadline exhausted'); const {directory,manifest,hash}=locate(root,candidatePath),receipt=readJson(regular(directory,'validation.json')),{integrity,...body}=receipt; if(typeof integrity!=='string'||integrity!==seal(root,body)||receipt.kind!=='source-repair'||receipt.accepted!==true||receipt.manifestHash!==hash||receipt.inputFingerprint!==digest(manifest.state)||receipt.baseline?.length!==2||receipt.candidateRuns?.length!==2||receipt.assertionRuns?.length!==2)throw Error('Authenticated accepted repair validation required for publication'); + assertTypedReceipt(root,manifest,receipt,{deadline}); if(manifest.challenges?.status!=='configured'||receipt.challenges?.accepted!==true||!assessRepairChallenges(receipt.challenges.baseline,receipt.challenges.candidate).accepted)throw Error('Authenticated independent repair challenges required for publication'); if(digest(captureRepairChallenges(root))!==digest(manifest.challenges)||digest(captureChallengeRuntime())!==digest(manifest.challengeRuntime))throw Error('Repair challenge oracle or runtime changed before publication'); const source=applied=>manifest.sourcePaths.map(file=>({file,sha256:applied?(manifest.files.find(item=>item.path===file)?.hash??manifest.state.scope.files[file].hash):manifest.state.scope.files[file].hash,bytes:fs.statSync(regular(root,file)).size})); diff --git a/test/repair-typecheck.test.js b/test/repair-typecheck.test.js new file mode 100644 index 0000000..c1abc7a --- /dev/null +++ b/test/repair-typecheck.test.js @@ -0,0 +1,56 @@ +import test from 'node:test'; +import assert from 'node:assert/strict'; +import fs from 'node:fs'; +import path from 'node:path'; +import {fileURLToPath} from 'node:url'; +import {createHmac} from 'node:crypto'; +import {fixture,write,commit} from './helpers.js'; +import {stageRepair,validateRepair,applyRepair,verifyAppliedRepairValidation} from '../src/repair-candidates.js'; +const here=path.dirname(fileURLToPath(import.meta.url)),sdk=[path.join(here,'../node_modules/typescript'),path.join(here,'../../../../node_modules/typescript')].find(directory=>fs.existsSync(path.join(directory,'lib/typescript.js'))); +const requirements='Addition returns the arithmetic sum. Preserve the independent retained value contract.'; +const baseline='/** @param {number} a @param {number} b @returns {number} */ export const add=(a,b)=>a-b;\n'; +const good=baseline.replace('a-b','a+b'); +const bad=good+'/** @type {string} */ const hiddenTypeDefect=42;\n'; +const review={accepted:true,findings:[],oracle:{independent:true,basis:[requirements]}}; +function project(t,{enabled=true,config={},source=baseline}={}){ + assert.ok(sdk,'Actual installed TypeScript SDK required'); + const root=fixture(t,{'package.json':{type:'module'},'.gitignore':'node_modules/\n.tddswarm/\n','tddswarm.config.json':{runner:[process.execPath,'--test','{files}'],plugins:{typescript:{enabled,timeoutMs:10000}},repair:{challenges:'quality/challenges.json'},...config},'tddswarm.requirements.md':requirements,'src/add.js':source, + 'tsconfig.json':{compilerOptions:{strict:true,allowJs:true,checkJs:true,skipLibCheck:true,noEmit:true,target:'ES2022',module:'NodeNext',moduleResolution:'NodeNext'},files:['src/add.js']}, + 'test/add.test.js':"import test from 'node:test';import assert from 'node:assert/strict';import {add} from '../src/add.js';test('arithmetic sum',()=>assert.equal(add(2,3),5));test('zero identity',()=>assert.equal(add(2,0),2));", + 'quality/challenges.json':{schemaVersion:1,independent:true,basis:'Maintainer authored arithmetic identity remains valid beyond original assertions.',cases:[{id:'retained-identity',script:'quality/retained.cjs'}]}, + 'quality/retained.cjs':"module.exports=async ({assert,root})=>{const {add}=await import(require('url').pathToFileURL(root+'/src/add.js').href);assert.strictEqual(add(7,0),7);};"}); + fs.mkdirSync(path.join(root,'node_modules'));fs.cpSync(sdk,path.join(root,'node_modules/typescript'),{recursive:true});commit(root);return root; +} +const stage=(root,content=good)=>stageRepair(root,{files:[{path:'src/add.js',content}],sourcePaths:['src/add.js'],requirements,review}); +const validate=(root,item)=>validateRepair(root,item.candidatePath,{timeoutMs:10000,totalTimeoutMs:90000}); +function rewriteReceipt(root,item,mutate,{authenticated=false}={}){const file=path.join(item.directory,'validation.json'),value=JSON.parse(fs.readFileSync(file));mutate(value);if(authenticated){delete value.integrity;value.integrity=createHmac('sha256',fs.readFileSync(path.join(root,'.tddswarm/repair-validation.key'))).update(JSON.stringify(value)).digest('hex');}fs.writeFileSync(file,JSON.stringify(value));} + +test('unchanged runtime assertions can all pass yet genuine TS2322 vetoes candidate repair',t=>{ + const root=project(t),item=stage(root,bad),result=validate(root,item); + assert.equal(result.candidateRuns[0].exitCode,0);assert.equal(result.accepted,false);assert.equal(result.typecheck.baseline.length,2);assert.equal(result.typecheck.baseline[0].exitCode,0);assert.equal(result.typecheck.candidate[0].exitCode,1);assert.ok(result.typecheck.candidate[0].diagnostics.some(row=>row.code===2322));assert.match(result.reasons.join(','),/candidate-TypeScript-not-complete-green/);assert.throws(()=>applyRepair(root,item.candidatePath),/Accepted intact/);assert.equal(fs.readFileSync(path.join(root,'src/add.js'),'utf8'),baseline); +}); + +test('correct typed source repair passes repeated native/compiler/challenge evidence and authenticated publication admission',t=>{ + const root=project(t),item=stage(root),result=validate(root,item);assert.equal(result.accepted,true,result.reasons.join(','));assert.equal(result.typecheck.accepted,true);assert.equal(result.typecheck.baseline.length,2);assert.equal(result.typecheck.candidate.length,2);assert.ok(result.typecheck.candidate.every(run=>run.complete&&run.exitCode===0&&run.implementationFiles.includes('src/add.js')));assert.equal(applyRepair(root,item.candidatePath).applied,true);assert.equal(verifyAppliedRepairValidation(root,item.candidatePath).receipt.typecheck.accepted,true); + const original=fs.readFileSync(path.join(item.directory,'validation.json'));rewriteReceipt(root,item,value=>{value.typecheck.candidate[0].implementationFiles=[];});assert.throws(()=>verifyAppliedRepairValidation(root,item.candidatePath),/Authenticated accepted/);fs.writeFileSync(path.join(item.directory,'validation.json'),original); + rewriteReceipt(root,item,value=>{for(const run of [...value.typecheck.baseline,...value.typecheck.candidate])run.compiler.sha256='0'.repeat(64);},{authenticated:true});assert.throws(()=>verifyAppliedRepairValidation(root,item.candidatePath),/current compiler|inventory/);fs.writeFileSync(path.join(item.directory,'validation.json'),original); + // Evaluator control: even a body re-authenticated by the test owner cannot turn + // missing typed evidence into admissible legacy publication. + rewriteReceipt(root,item,value=>{delete value.typecheck;},{authenticated:true});assert.throws(()=>verifyAppliedRepairValidation(root,item.candidatePath),/TypeScript repair validation/);fs.writeFileSync(path.join(item.directory,'validation.json'),original); +}); + +test('authenticated legacy or fabricated typed admission is refused at apply while disabled remains inert',t=>{ + const root=project(t),item=stage(root),result=validate(root,item);assert.equal(result.accepted,true,result.reasons.join(','));const original=fs.readFileSync(path.join(item.directory,'validation.json')); + rewriteReceipt(root,item,value=>{for(const run of [...value.typecheck.baseline,...value.typecheck.candidate])run.compiler.sha256='0'.repeat(64);},{authenticated:true});assert.throws(()=>applyRepair(root,item.candidatePath),/current compiler|inventory/);fs.writeFileSync(path.join(item.directory,'validation.json'),original); + rewriteReceipt(root,item,value=>{delete value.typecheck;},{authenticated:true});assert.throws(()=>applyRepair(root,item.candidatePath),/TypeScript repair validation/);fs.writeFileSync(path.join(item.directory,'validation.json'),original); + rewriteReceipt(root,item,value=>{value.typecheck.candidate[0].complete=false;},{authenticated:true});assert.throws(()=>applyRepair(root,item.candidatePath),/TypeScript repair validation/); + const disabled=project(t,{enabled:false}),untypedItem=stage(disabled,bad),untyped=validate(disabled,untypedItem);assert.equal(untyped.accepted,true,untyped.reasons.join(','));assert.equal(untyped.typecheck,undefined);assert.equal(applyRepair(disabled,untypedItem.candidatePath).applied,true); +}); + +test('missing SDK, noCheck and absolute installed links conservatively reject typed repairs',t=>{ + for(const kind of ['missing','noCheck','link']){const root=project(t);if(kind==='missing')fs.rmSync(path.join(root,'node_modules'),{recursive:true});if(kind==='noCheck')write(root,'tsconfig.json',{compilerOptions:{allowJs:true,checkJs:true,noCheck:true},files:['src/add.js']});if(kind==='link')fs.symlinkSync(path.join(root,'node_modules/typescript/lib/typescript.js'),path.join(root,'node_modules/absolute-sdk-link.js'));const item=stage(root),result=validate(root,item);assert.equal(result.accepted,false);assert.match(result.reasons.join(','),/TypeScript|typed-repair/);assert.throws(()=>applyRepair(root,item.candidatePath));} +}); + +test('repeatable baseline type errors are retained while corrected typed candidates must be green',t=>{ + const root=project(t,{source:baseline.replace('@returns {number}','@returns {string}')}),item=stage(root),result=validate(root,item);assert.equal(result.accepted,true,result.reasons.join(','));assert.ok(result.typecheck.baseline.every(run=>run.complete&&run.exitCode===1&&run.diagnostics.some(row=>row.code===2322)));assert.ok(result.typecheck.candidate.every(run=>run.complete&&run.exitCode===0));assert.equal(applyRepair(root,item.candidatePath).applied,true);assert.equal(verifyAppliedRepairValidation(root,item.candidatePath).receipt.typecheck.accepted,true); +}); From fbfdf498945e3dbc269d30fee603c725f67382be Mon Sep 17 00:00:00 2001 From: Rudy Celekli Date: Mon, 12 Oct 2026 23:30:49 -0400 Subject: [PATCH 05/16] Run complementary type verification through setup, runtime checks and repair gates --- .github/workflows/ci.yml | 23 + README.md | 2 + .../type-detection/development-v1/README.md | 12 + .../fixtures/api-consumer/fault/api.ts | 1 + .../fixtures/api-consumer/fault/consumer.ts | 2 + .../fixtures/api-consumer/fault/package.json | 1 + .../fixtures/api-consumer/fault/probe.mjs | 4 + .../fixtures/api-consumer/fault/tsconfig.json | 16 + .../fixtures/api-consumer/healthy/api.ts | 1 + .../fixtures/api-consumer/healthy/consumer.ts | 2 + .../api-consumer/healthy/package.json | 1 + .../fixtures/api-consumer/healthy/probe.mjs | 4 + .../api-consumer/healthy/tsconfig.json | 16 + .../cross-module-contract/fault/package.json | 1 + .../fault/packages/app/module.ts | 2 + .../fault/packages/contracts/types.ts | 1 + .../cross-module-contract/fault/probe.mjs | 4 + .../cross-module-contract/fault/tsconfig.json | 16 + .../healthy/package.json | 1 + .../healthy/packages/app/module.ts | 2 + .../healthy/packages/contracts/types.ts | 1 + .../cross-module-contract/healthy/probe.mjs | 4 + .../healthy/tsconfig.json | 16 + .../fixtures/nullable-branch/fault/module.ts | 4 + .../nullable-branch/fault/package.json | 1 + .../fixtures/nullable-branch/fault/probe.mjs | 4 + .../nullable-branch/fault/tsconfig.json | 16 + .../nullable-branch/healthy/module.ts | 4 + .../nullable-branch/healthy/package.json | 1 + .../nullable-branch/healthy/probe.mjs | 4 + .../nullable-branch/healthy/tsconfig.json | 16 + .../readonly-mutation/fault/module.ts | 4 + .../readonly-mutation/fault/package.json | 1 + .../readonly-mutation/fault/probe.mjs | 6 + .../readonly-mutation/fault/tsconfig.json | 16 + .../readonly-mutation/healthy/module.ts | 4 + .../readonly-mutation/healthy/package.json | 1 + .../readonly-mutation/healthy/probe.mjs | 6 + .../readonly-mutation/healthy/tsconfig.json | 16 + .../referenced-workspace/fault/package.json | 1 + .../fault/packages/app/module.ts | 5 + .../fault/packages/app/tsconfig.json | 25 + .../fault/packages/contracts/tsconfig.json | 20 + .../fault/packages/contracts/types.ts | 1 + .../referenced-workspace/fault/probe.mjs | 4 + .../referenced-workspace/fault/tsconfig.json | 11 + .../referenced-workspace/healthy/package.json | 1 + .../healthy/packages/app/module.ts | 5 + .../healthy/packages/app/tsconfig.json | 25 + .../healthy/packages/contracts/tsconfig.json | 20 + .../healthy/packages/contracts/types.ts | 1 + .../referenced-workspace/healthy/probe.mjs | 4 + .../healthy/tsconfig.json | 11 + .../fixtures/return-contract/fault/module.ts | 4 + .../return-contract/fault/package.json | 1 + .../fixtures/return-contract/fault/probe.mjs | 4 + .../return-contract/fault/tsconfig.json | 16 + .../return-contract/healthy/module.ts | 4 + .../return-contract/healthy/package.json | 1 + .../return-contract/healthy/probe.mjs | 4 + .../return-contract/healthy/tsconfig.json | 16 + .../development-v1/manifest.json | 583 ++++++++++++++++++ .../011-complementary-native-type-oracle.md | 15 + docs/defect-detection.md | 2 + docs/native-typecheck.md | 50 ++ docs/plugins.md | 4 +- package.json | 3 +- scripts/packed-evidence.js | 27 +- scripts/packed-proof.js | 28 +- scripts/release-candidate.js | 3 +- scripts/type-detection-benchmark.js | 191 ++++++ src/behavioral-contracts.js | 12 +- src/cli.js | 14 +- src/index.js | 1 + src/plugin-config.js | 7 +- src/plugin-recommendations.js | 12 +- src/plugins.js | 8 + src/pull-request.js | 16 + src/repair.js | 22 +- src/run-report.js | 15 +- src/runner.js | 76 ++- src/typecheck-report.js | 31 + src/verification-checks.js | 15 + test/packed-evidence.test.js | 11 + test/plugin-recommendations.test.js | 2 +- test/plugins.test.js | 4 +- test/repair.test.js | 22 + test/typecheck-integration.test.js | 86 +++ test/typecheck-plugins.test.js | 32 + 89 files changed, 1669 insertions(+), 44 deletions(-) create mode 100644 benchmarks/type-detection/development-v1/README.md create mode 100644 benchmarks/type-detection/development-v1/fixtures/api-consumer/fault/api.ts create mode 100644 benchmarks/type-detection/development-v1/fixtures/api-consumer/fault/consumer.ts create mode 100644 benchmarks/type-detection/development-v1/fixtures/api-consumer/fault/package.json create mode 100644 benchmarks/type-detection/development-v1/fixtures/api-consumer/fault/probe.mjs create mode 100644 benchmarks/type-detection/development-v1/fixtures/api-consumer/fault/tsconfig.json create mode 100644 benchmarks/type-detection/development-v1/fixtures/api-consumer/healthy/api.ts create mode 100644 benchmarks/type-detection/development-v1/fixtures/api-consumer/healthy/consumer.ts create mode 100644 benchmarks/type-detection/development-v1/fixtures/api-consumer/healthy/package.json create mode 100644 benchmarks/type-detection/development-v1/fixtures/api-consumer/healthy/probe.mjs create mode 100644 benchmarks/type-detection/development-v1/fixtures/api-consumer/healthy/tsconfig.json create mode 100644 benchmarks/type-detection/development-v1/fixtures/cross-module-contract/fault/package.json create mode 100644 benchmarks/type-detection/development-v1/fixtures/cross-module-contract/fault/packages/app/module.ts create mode 100644 benchmarks/type-detection/development-v1/fixtures/cross-module-contract/fault/packages/contracts/types.ts create mode 100644 benchmarks/type-detection/development-v1/fixtures/cross-module-contract/fault/probe.mjs create mode 100644 benchmarks/type-detection/development-v1/fixtures/cross-module-contract/fault/tsconfig.json create mode 100644 benchmarks/type-detection/development-v1/fixtures/cross-module-contract/healthy/package.json create mode 100644 benchmarks/type-detection/development-v1/fixtures/cross-module-contract/healthy/packages/app/module.ts create mode 100644 benchmarks/type-detection/development-v1/fixtures/cross-module-contract/healthy/packages/contracts/types.ts create mode 100644 benchmarks/type-detection/development-v1/fixtures/cross-module-contract/healthy/probe.mjs create mode 100644 benchmarks/type-detection/development-v1/fixtures/cross-module-contract/healthy/tsconfig.json create mode 100644 benchmarks/type-detection/development-v1/fixtures/nullable-branch/fault/module.ts create mode 100644 benchmarks/type-detection/development-v1/fixtures/nullable-branch/fault/package.json create mode 100644 benchmarks/type-detection/development-v1/fixtures/nullable-branch/fault/probe.mjs create mode 100644 benchmarks/type-detection/development-v1/fixtures/nullable-branch/fault/tsconfig.json create mode 100644 benchmarks/type-detection/development-v1/fixtures/nullable-branch/healthy/module.ts create mode 100644 benchmarks/type-detection/development-v1/fixtures/nullable-branch/healthy/package.json create mode 100644 benchmarks/type-detection/development-v1/fixtures/nullable-branch/healthy/probe.mjs create mode 100644 benchmarks/type-detection/development-v1/fixtures/nullable-branch/healthy/tsconfig.json create mode 100644 benchmarks/type-detection/development-v1/fixtures/readonly-mutation/fault/module.ts create mode 100644 benchmarks/type-detection/development-v1/fixtures/readonly-mutation/fault/package.json create mode 100644 benchmarks/type-detection/development-v1/fixtures/readonly-mutation/fault/probe.mjs create mode 100644 benchmarks/type-detection/development-v1/fixtures/readonly-mutation/fault/tsconfig.json create mode 100644 benchmarks/type-detection/development-v1/fixtures/readonly-mutation/healthy/module.ts create mode 100644 benchmarks/type-detection/development-v1/fixtures/readonly-mutation/healthy/package.json create mode 100644 benchmarks/type-detection/development-v1/fixtures/readonly-mutation/healthy/probe.mjs create mode 100644 benchmarks/type-detection/development-v1/fixtures/readonly-mutation/healthy/tsconfig.json create mode 100644 benchmarks/type-detection/development-v1/fixtures/referenced-workspace/fault/package.json create mode 100644 benchmarks/type-detection/development-v1/fixtures/referenced-workspace/fault/packages/app/module.ts create mode 100644 benchmarks/type-detection/development-v1/fixtures/referenced-workspace/fault/packages/app/tsconfig.json create mode 100644 benchmarks/type-detection/development-v1/fixtures/referenced-workspace/fault/packages/contracts/tsconfig.json create mode 100644 benchmarks/type-detection/development-v1/fixtures/referenced-workspace/fault/packages/contracts/types.ts create mode 100644 benchmarks/type-detection/development-v1/fixtures/referenced-workspace/fault/probe.mjs create mode 100644 benchmarks/type-detection/development-v1/fixtures/referenced-workspace/fault/tsconfig.json create mode 100644 benchmarks/type-detection/development-v1/fixtures/referenced-workspace/healthy/package.json create mode 100644 benchmarks/type-detection/development-v1/fixtures/referenced-workspace/healthy/packages/app/module.ts create mode 100644 benchmarks/type-detection/development-v1/fixtures/referenced-workspace/healthy/packages/app/tsconfig.json create mode 100644 benchmarks/type-detection/development-v1/fixtures/referenced-workspace/healthy/packages/contracts/tsconfig.json create mode 100644 benchmarks/type-detection/development-v1/fixtures/referenced-workspace/healthy/packages/contracts/types.ts create mode 100644 benchmarks/type-detection/development-v1/fixtures/referenced-workspace/healthy/probe.mjs create mode 100644 benchmarks/type-detection/development-v1/fixtures/referenced-workspace/healthy/tsconfig.json create mode 100644 benchmarks/type-detection/development-v1/fixtures/return-contract/fault/module.ts create mode 100644 benchmarks/type-detection/development-v1/fixtures/return-contract/fault/package.json create mode 100644 benchmarks/type-detection/development-v1/fixtures/return-contract/fault/probe.mjs create mode 100644 benchmarks/type-detection/development-v1/fixtures/return-contract/fault/tsconfig.json create mode 100644 benchmarks/type-detection/development-v1/fixtures/return-contract/healthy/module.ts create mode 100644 benchmarks/type-detection/development-v1/fixtures/return-contract/healthy/package.json create mode 100644 benchmarks/type-detection/development-v1/fixtures/return-contract/healthy/probe.mjs create mode 100644 benchmarks/type-detection/development-v1/fixtures/return-contract/healthy/tsconfig.json create mode 100644 benchmarks/type-detection/development-v1/manifest.json create mode 100644 docs/adr/011-complementary-native-type-oracle.md create mode 100644 docs/native-typecheck.md create mode 100644 scripts/type-detection-benchmark.js create mode 100644 src/typecheck-report.js create mode 100644 src/verification-checks.js create mode 100644 test/typecheck-integration.test.js create mode 100644 test/typecheck-plugins.test.js diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 5f78211..b5e08e1 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -9,6 +9,29 @@ concurrency: group: ci-${{ github.ref }} cancel-in-progress: true jobs: + type-detection: + runs-on: ubuntu-latest + timeout-minutes: 6 + steps: + - uses: actions/checkout@v7.0.1 + with: + ref: ${{ github.event.pull_request.head.sha || github.sha }} + - uses: actions/setup-node@v7.0.0 + with: + node-version: '24' + - run: npm ci --ignore-scripts + - name: Reproduce observed type defects, retain unsupported scope, challenge evaluator + run: | + node scripts/type-detection-benchmark.js --output .tddswarm/ci-type-detection + node scripts/type-detection-benchmark.js --verify .tddswarm/ci-type-detection/summary.json + node scripts/type-detection-benchmark.js --challenge-evaluator .tddswarm/ci-type-detection/summary.json + - uses: actions/upload-artifact@v4 + if: always() + with: + name: type-detection-${{ github.event.pull_request.head.sha || github.sha }} + path: .tddswarm/ci-type-detection/ + include-hidden-files: true + if-no-files-found: error gradia-evidence: runs-on: ubuntu-latest timeout-minutes: 5 diff --git a/README.md b/README.md index 1433a61..b01a84b 100644 --- a/README.md +++ b/README.md @@ -38,6 +38,8 @@ TestLore brings **test creation, intelligent routing and reviewed learning** int **Go beyond a green suite:** explicitly configured independent behavioral challenges also run during routine verification—even when no native tests are selected. Reports separate native outcomes, additional failures and incomplete checks. [Configure behavioral checks and compare detector coverage →](docs/defect-detection.md) +**Catch type errors runtime assertions miss:** setup enables the installed project-local TypeScript checker when a root `tsconfig.json` is present. Runtime and compiler results stay separate; ignored-source or dependency drift prevents a combined pass. [Native type verification and reproducible development examples →](docs/native-typecheck.md) + | Your starting point | What TestLore does | | --- | --- | | **No tests yet** | An architect, bounded author team, and independent reviewer propose runnable tests from your behavioral requirements. | diff --git a/benchmarks/type-detection/development-v1/README.md b/benchmarks/type-detection/development-v1/README.md new file mode 100644 index 0000000..1b6a16a --- /dev/null +++ b/benchmarks/type-detection/development-v1/README.md @@ -0,0 +1,12 @@ +# Independent development type-contract examples + +Five seeded healthy/fault pairs show why ordinary runtime checks and static type checking complement each other. Each pair keeps the same native assertions, configuration and shared contracts; one TypeScript source differs. A separate project-reference fixture remains explicitly unsupported/unqualified. + +These are original DEVELOPMENT examples. They are not untouched maintainer specifications, historical upstream bugs, learned repairs, or a gold-standard claim. Public export makes them inspectable; they must not be called future holdouts. + +Use Node22.19.0 and the pinned original TypeScript6.0.3 installation. Every probe.mjs executes real top-level node:assert/strict checks: six positive assertions across five ordinary probes, plus one in the separate reference probe. Each runs unchanged against healthy and fault states. Runtime checks intentionally exercise partial ordinary inputs. + +Read manifest.json for exact source hashes, diagnostic categories, original checker arguments and explicit reference limitations. Capture actual installed SDK closure and complete process costs during reproduction; no SDK is shipped here and the recorded closure checksum grants no authority. + +Derived public manifest SHA256: b4130e918e9d6a91edace4eb46e7a227aff46c88212a07760a85ed90942f0b39 +Original private frozen manifest SHA256: 16b34a7b750805b6c1b2b8b0988a7e2fd204ae5ce285c630f149111bd2b7b09c diff --git a/benchmarks/type-detection/development-v1/fixtures/api-consumer/fault/api.ts b/benchmarks/type-detection/development-v1/fixtures/api-consumer/fault/api.ts new file mode 100644 index 0000000..ed0835f --- /dev/null +++ b/benchmarks/type-detection/development-v1/fixtures/api-consumer/fault/api.ts @@ -0,0 +1 @@ +export function parseCount(value: string): number { return Number(value); } diff --git a/benchmarks/type-detection/development-v1/fixtures/api-consumer/fault/consumer.ts b/benchmarks/type-detection/development-v1/fixtures/api-consumer/fault/consumer.ts new file mode 100644 index 0000000..54bbaa1 --- /dev/null +++ b/benchmarks/type-detection/development-v1/fixtures/api-consumer/fault/consumer.ts @@ -0,0 +1,2 @@ +import {parseCount} from './api.ts'; +export function configuredCount(): number { return parseCount(12); } diff --git a/benchmarks/type-detection/development-v1/fixtures/api-consumer/fault/package.json b/benchmarks/type-detection/development-v1/fixtures/api-consumer/fault/package.json new file mode 100644 index 0000000..af50513 --- /dev/null +++ b/benchmarks/type-detection/development-v1/fixtures/api-consumer/fault/package.json @@ -0,0 +1 @@ +{"name":"testlore-independent-development-spec","private":true,"type":"module"} diff --git a/benchmarks/type-detection/development-v1/fixtures/api-consumer/fault/probe.mjs b/benchmarks/type-detection/development-v1/fixtures/api-consumer/fault/probe.mjs new file mode 100644 index 0000000..7673053 --- /dev/null +++ b/benchmarks/type-detection/development-v1/fixtures/api-consumer/fault/probe.mjs @@ -0,0 +1,4 @@ +import assert from 'node:assert/strict'; +import {configuredCount} from './consumer.ts'; +assert.equal(configuredCount(), 12); +console.log('ordinary-input assertions passed'); diff --git a/benchmarks/type-detection/development-v1/fixtures/api-consumer/fault/tsconfig.json b/benchmarks/type-detection/development-v1/fixtures/api-consumer/fault/tsconfig.json new file mode 100644 index 0000000..ebd9c6d --- /dev/null +++ b/benchmarks/type-detection/development-v1/fixtures/api-consumer/fault/tsconfig.json @@ -0,0 +1,16 @@ +{ + "compilerOptions": { + "target": "ES2022", + "module": "NodeNext", + "moduleResolution": "NodeNext", + "strict": true, + "noEmit": true, + "allowImportingTsExtensions": true, + "erasableSyntaxOnly": true, + "types": [], + "skipLibCheck": false + }, + "include": [ + "**/*.ts" + ] +} diff --git a/benchmarks/type-detection/development-v1/fixtures/api-consumer/healthy/api.ts b/benchmarks/type-detection/development-v1/fixtures/api-consumer/healthy/api.ts new file mode 100644 index 0000000..ed0835f --- /dev/null +++ b/benchmarks/type-detection/development-v1/fixtures/api-consumer/healthy/api.ts @@ -0,0 +1 @@ +export function parseCount(value: string): number { return Number(value); } diff --git a/benchmarks/type-detection/development-v1/fixtures/api-consumer/healthy/consumer.ts b/benchmarks/type-detection/development-v1/fixtures/api-consumer/healthy/consumer.ts new file mode 100644 index 0000000..3a26c2e --- /dev/null +++ b/benchmarks/type-detection/development-v1/fixtures/api-consumer/healthy/consumer.ts @@ -0,0 +1,2 @@ +import {parseCount} from './api.ts'; +export function configuredCount(): number { return parseCount('12'); } diff --git a/benchmarks/type-detection/development-v1/fixtures/api-consumer/healthy/package.json b/benchmarks/type-detection/development-v1/fixtures/api-consumer/healthy/package.json new file mode 100644 index 0000000..af50513 --- /dev/null +++ b/benchmarks/type-detection/development-v1/fixtures/api-consumer/healthy/package.json @@ -0,0 +1 @@ +{"name":"testlore-independent-development-spec","private":true,"type":"module"} diff --git a/benchmarks/type-detection/development-v1/fixtures/api-consumer/healthy/probe.mjs b/benchmarks/type-detection/development-v1/fixtures/api-consumer/healthy/probe.mjs new file mode 100644 index 0000000..7673053 --- /dev/null +++ b/benchmarks/type-detection/development-v1/fixtures/api-consumer/healthy/probe.mjs @@ -0,0 +1,4 @@ +import assert from 'node:assert/strict'; +import {configuredCount} from './consumer.ts'; +assert.equal(configuredCount(), 12); +console.log('ordinary-input assertions passed'); diff --git a/benchmarks/type-detection/development-v1/fixtures/api-consumer/healthy/tsconfig.json b/benchmarks/type-detection/development-v1/fixtures/api-consumer/healthy/tsconfig.json new file mode 100644 index 0000000..ebd9c6d --- /dev/null +++ b/benchmarks/type-detection/development-v1/fixtures/api-consumer/healthy/tsconfig.json @@ -0,0 +1,16 @@ +{ + "compilerOptions": { + "target": "ES2022", + "module": "NodeNext", + "moduleResolution": "NodeNext", + "strict": true, + "noEmit": true, + "allowImportingTsExtensions": true, + "erasableSyntaxOnly": true, + "types": [], + "skipLibCheck": false + }, + "include": [ + "**/*.ts" + ] +} diff --git a/benchmarks/type-detection/development-v1/fixtures/cross-module-contract/fault/package.json b/benchmarks/type-detection/development-v1/fixtures/cross-module-contract/fault/package.json new file mode 100644 index 0000000..af50513 --- /dev/null +++ b/benchmarks/type-detection/development-v1/fixtures/cross-module-contract/fault/package.json @@ -0,0 +1 @@ +{"name":"testlore-independent-development-spec","private":true,"type":"module"} diff --git a/benchmarks/type-detection/development-v1/fixtures/cross-module-contract/fault/packages/app/module.ts b/benchmarks/type-detection/development-v1/fixtures/cross-module-contract/fault/packages/app/module.ts new file mode 100644 index 0000000..3950f6d --- /dev/null +++ b/benchmarks/type-detection/development-v1/fixtures/cross-module-contract/fault/packages/app/module.ts @@ -0,0 +1,2 @@ +import type {Entry} from '../contracts/types.ts'; +export function entry(): Entry { return {id: 'fixed', attempts: '1'}; } diff --git a/benchmarks/type-detection/development-v1/fixtures/cross-module-contract/fault/packages/contracts/types.ts b/benchmarks/type-detection/development-v1/fixtures/cross-module-contract/fault/packages/contracts/types.ts new file mode 100644 index 0000000..e4a5c69 --- /dev/null +++ b/benchmarks/type-detection/development-v1/fixtures/cross-module-contract/fault/packages/contracts/types.ts @@ -0,0 +1 @@ +export interface Entry { id: string; attempts: number } diff --git a/benchmarks/type-detection/development-v1/fixtures/cross-module-contract/fault/probe.mjs b/benchmarks/type-detection/development-v1/fixtures/cross-module-contract/fault/probe.mjs new file mode 100644 index 0000000..a529660 --- /dev/null +++ b/benchmarks/type-detection/development-v1/fixtures/cross-module-contract/fault/probe.mjs @@ -0,0 +1,4 @@ +import assert from 'node:assert/strict'; +import {entry} from './packages/app/module.ts'; +assert.equal(entry().id, 'fixed'); +console.log('ordinary-input assertions passed'); diff --git a/benchmarks/type-detection/development-v1/fixtures/cross-module-contract/fault/tsconfig.json b/benchmarks/type-detection/development-v1/fixtures/cross-module-contract/fault/tsconfig.json new file mode 100644 index 0000000..ebd9c6d --- /dev/null +++ b/benchmarks/type-detection/development-v1/fixtures/cross-module-contract/fault/tsconfig.json @@ -0,0 +1,16 @@ +{ + "compilerOptions": { + "target": "ES2022", + "module": "NodeNext", + "moduleResolution": "NodeNext", + "strict": true, + "noEmit": true, + "allowImportingTsExtensions": true, + "erasableSyntaxOnly": true, + "types": [], + "skipLibCheck": false + }, + "include": [ + "**/*.ts" + ] +} diff --git a/benchmarks/type-detection/development-v1/fixtures/cross-module-contract/healthy/package.json b/benchmarks/type-detection/development-v1/fixtures/cross-module-contract/healthy/package.json new file mode 100644 index 0000000..af50513 --- /dev/null +++ b/benchmarks/type-detection/development-v1/fixtures/cross-module-contract/healthy/package.json @@ -0,0 +1 @@ +{"name":"testlore-independent-development-spec","private":true,"type":"module"} diff --git a/benchmarks/type-detection/development-v1/fixtures/cross-module-contract/healthy/packages/app/module.ts b/benchmarks/type-detection/development-v1/fixtures/cross-module-contract/healthy/packages/app/module.ts new file mode 100644 index 0000000..d941667 --- /dev/null +++ b/benchmarks/type-detection/development-v1/fixtures/cross-module-contract/healthy/packages/app/module.ts @@ -0,0 +1,2 @@ +import type {Entry} from '../contracts/types.ts'; +export function entry(): Entry { return {id: 'fixed', attempts: 1}; } diff --git a/benchmarks/type-detection/development-v1/fixtures/cross-module-contract/healthy/packages/contracts/types.ts b/benchmarks/type-detection/development-v1/fixtures/cross-module-contract/healthy/packages/contracts/types.ts new file mode 100644 index 0000000..e4a5c69 --- /dev/null +++ b/benchmarks/type-detection/development-v1/fixtures/cross-module-contract/healthy/packages/contracts/types.ts @@ -0,0 +1 @@ +export interface Entry { id: string; attempts: number } diff --git a/benchmarks/type-detection/development-v1/fixtures/cross-module-contract/healthy/probe.mjs b/benchmarks/type-detection/development-v1/fixtures/cross-module-contract/healthy/probe.mjs new file mode 100644 index 0000000..a529660 --- /dev/null +++ b/benchmarks/type-detection/development-v1/fixtures/cross-module-contract/healthy/probe.mjs @@ -0,0 +1,4 @@ +import assert from 'node:assert/strict'; +import {entry} from './packages/app/module.ts'; +assert.equal(entry().id, 'fixed'); +console.log('ordinary-input assertions passed'); diff --git a/benchmarks/type-detection/development-v1/fixtures/cross-module-contract/healthy/tsconfig.json b/benchmarks/type-detection/development-v1/fixtures/cross-module-contract/healthy/tsconfig.json new file mode 100644 index 0000000..ebd9c6d --- /dev/null +++ b/benchmarks/type-detection/development-v1/fixtures/cross-module-contract/healthy/tsconfig.json @@ -0,0 +1,16 @@ +{ + "compilerOptions": { + "target": "ES2022", + "module": "NodeNext", + "moduleResolution": "NodeNext", + "strict": true, + "noEmit": true, + "allowImportingTsExtensions": true, + "erasableSyntaxOnly": true, + "types": [], + "skipLibCheck": false + }, + "include": [ + "**/*.ts" + ] +} diff --git a/benchmarks/type-detection/development-v1/fixtures/nullable-branch/fault/module.ts b/benchmarks/type-detection/development-v1/fixtures/nullable-branch/fault/module.ts new file mode 100644 index 0000000..8f0c048 --- /dev/null +++ b/benchmarks/type-detection/development-v1/fixtures/nullable-branch/fault/module.ts @@ -0,0 +1,4 @@ +export function label(value: string | null): string { + if (value === null) return value; + return value.toUpperCase(); +} diff --git a/benchmarks/type-detection/development-v1/fixtures/nullable-branch/fault/package.json b/benchmarks/type-detection/development-v1/fixtures/nullable-branch/fault/package.json new file mode 100644 index 0000000..af50513 --- /dev/null +++ b/benchmarks/type-detection/development-v1/fixtures/nullable-branch/fault/package.json @@ -0,0 +1 @@ +{"name":"testlore-independent-development-spec","private":true,"type":"module"} diff --git a/benchmarks/type-detection/development-v1/fixtures/nullable-branch/fault/probe.mjs b/benchmarks/type-detection/development-v1/fixtures/nullable-branch/fault/probe.mjs new file mode 100644 index 0000000..801e518 --- /dev/null +++ b/benchmarks/type-detection/development-v1/fixtures/nullable-branch/fault/probe.mjs @@ -0,0 +1,4 @@ +import assert from 'node:assert/strict'; +import {label} from './module.ts'; +assert.equal(label('hello'), 'HELLO'); +console.log('ordinary-input assertions passed'); diff --git a/benchmarks/type-detection/development-v1/fixtures/nullable-branch/fault/tsconfig.json b/benchmarks/type-detection/development-v1/fixtures/nullable-branch/fault/tsconfig.json new file mode 100644 index 0000000..ebd9c6d --- /dev/null +++ b/benchmarks/type-detection/development-v1/fixtures/nullable-branch/fault/tsconfig.json @@ -0,0 +1,16 @@ +{ + "compilerOptions": { + "target": "ES2022", + "module": "NodeNext", + "moduleResolution": "NodeNext", + "strict": true, + "noEmit": true, + "allowImportingTsExtensions": true, + "erasableSyntaxOnly": true, + "types": [], + "skipLibCheck": false + }, + "include": [ + "**/*.ts" + ] +} diff --git a/benchmarks/type-detection/development-v1/fixtures/nullable-branch/healthy/module.ts b/benchmarks/type-detection/development-v1/fixtures/nullable-branch/healthy/module.ts new file mode 100644 index 0000000..bde579b --- /dev/null +++ b/benchmarks/type-detection/development-v1/fixtures/nullable-branch/healthy/module.ts @@ -0,0 +1,4 @@ +export function label(value: string | null): string { + if (value === null) return 'missing'; + return value.toUpperCase(); +} diff --git a/benchmarks/type-detection/development-v1/fixtures/nullable-branch/healthy/package.json b/benchmarks/type-detection/development-v1/fixtures/nullable-branch/healthy/package.json new file mode 100644 index 0000000..af50513 --- /dev/null +++ b/benchmarks/type-detection/development-v1/fixtures/nullable-branch/healthy/package.json @@ -0,0 +1 @@ +{"name":"testlore-independent-development-spec","private":true,"type":"module"} diff --git a/benchmarks/type-detection/development-v1/fixtures/nullable-branch/healthy/probe.mjs b/benchmarks/type-detection/development-v1/fixtures/nullable-branch/healthy/probe.mjs new file mode 100644 index 0000000..801e518 --- /dev/null +++ b/benchmarks/type-detection/development-v1/fixtures/nullable-branch/healthy/probe.mjs @@ -0,0 +1,4 @@ +import assert from 'node:assert/strict'; +import {label} from './module.ts'; +assert.equal(label('hello'), 'HELLO'); +console.log('ordinary-input assertions passed'); diff --git a/benchmarks/type-detection/development-v1/fixtures/nullable-branch/healthy/tsconfig.json b/benchmarks/type-detection/development-v1/fixtures/nullable-branch/healthy/tsconfig.json new file mode 100644 index 0000000..ebd9c6d --- /dev/null +++ b/benchmarks/type-detection/development-v1/fixtures/nullable-branch/healthy/tsconfig.json @@ -0,0 +1,16 @@ +{ + "compilerOptions": { + "target": "ES2022", + "module": "NodeNext", + "moduleResolution": "NodeNext", + "strict": true, + "noEmit": true, + "allowImportingTsExtensions": true, + "erasableSyntaxOnly": true, + "types": [], + "skipLibCheck": false + }, + "include": [ + "**/*.ts" + ] +} diff --git a/benchmarks/type-detection/development-v1/fixtures/readonly-mutation/fault/module.ts b/benchmarks/type-detection/development-v1/fixtures/readonly-mutation/fault/module.ts new file mode 100644 index 0000000..1895147 --- /dev/null +++ b/benchmarks/type-detection/development-v1/fixtures/readonly-mutation/fault/module.ts @@ -0,0 +1,4 @@ +export function update(entry: {readonly id: number}, replace: boolean): number { + if (replace) entry.id = 2; + return entry.id; +} diff --git a/benchmarks/type-detection/development-v1/fixtures/readonly-mutation/fault/package.json b/benchmarks/type-detection/development-v1/fixtures/readonly-mutation/fault/package.json new file mode 100644 index 0000000..af50513 --- /dev/null +++ b/benchmarks/type-detection/development-v1/fixtures/readonly-mutation/fault/package.json @@ -0,0 +1 @@ +{"name":"testlore-independent-development-spec","private":true,"type":"module"} diff --git a/benchmarks/type-detection/development-v1/fixtures/readonly-mutation/fault/probe.mjs b/benchmarks/type-detection/development-v1/fixtures/readonly-mutation/fault/probe.mjs new file mode 100644 index 0000000..4f464c0 --- /dev/null +++ b/benchmarks/type-detection/development-v1/fixtures/readonly-mutation/fault/probe.mjs @@ -0,0 +1,6 @@ +import assert from 'node:assert/strict'; +import {update} from './module.ts'; +const entry = {id: 1}; +assert.equal(update(entry, false), 1); +assert.deepEqual(entry, {id: 1}); +console.log('ordinary-input assertions passed'); diff --git a/benchmarks/type-detection/development-v1/fixtures/readonly-mutation/fault/tsconfig.json b/benchmarks/type-detection/development-v1/fixtures/readonly-mutation/fault/tsconfig.json new file mode 100644 index 0000000..ebd9c6d --- /dev/null +++ b/benchmarks/type-detection/development-v1/fixtures/readonly-mutation/fault/tsconfig.json @@ -0,0 +1,16 @@ +{ + "compilerOptions": { + "target": "ES2022", + "module": "NodeNext", + "moduleResolution": "NodeNext", + "strict": true, + "noEmit": true, + "allowImportingTsExtensions": true, + "erasableSyntaxOnly": true, + "types": [], + "skipLibCheck": false + }, + "include": [ + "**/*.ts" + ] +} diff --git a/benchmarks/type-detection/development-v1/fixtures/readonly-mutation/healthy/module.ts b/benchmarks/type-detection/development-v1/fixtures/readonly-mutation/healthy/module.ts new file mode 100644 index 0000000..7d9def4 --- /dev/null +++ b/benchmarks/type-detection/development-v1/fixtures/readonly-mutation/healthy/module.ts @@ -0,0 +1,4 @@ +export function update(entry: {readonly id: number}, replace: boolean): number { + if (replace) return 2; + return entry.id; +} diff --git a/benchmarks/type-detection/development-v1/fixtures/readonly-mutation/healthy/package.json b/benchmarks/type-detection/development-v1/fixtures/readonly-mutation/healthy/package.json new file mode 100644 index 0000000..af50513 --- /dev/null +++ b/benchmarks/type-detection/development-v1/fixtures/readonly-mutation/healthy/package.json @@ -0,0 +1 @@ +{"name":"testlore-independent-development-spec","private":true,"type":"module"} diff --git a/benchmarks/type-detection/development-v1/fixtures/readonly-mutation/healthy/probe.mjs b/benchmarks/type-detection/development-v1/fixtures/readonly-mutation/healthy/probe.mjs new file mode 100644 index 0000000..4f464c0 --- /dev/null +++ b/benchmarks/type-detection/development-v1/fixtures/readonly-mutation/healthy/probe.mjs @@ -0,0 +1,6 @@ +import assert from 'node:assert/strict'; +import {update} from './module.ts'; +const entry = {id: 1}; +assert.equal(update(entry, false), 1); +assert.deepEqual(entry, {id: 1}); +console.log('ordinary-input assertions passed'); diff --git a/benchmarks/type-detection/development-v1/fixtures/readonly-mutation/healthy/tsconfig.json b/benchmarks/type-detection/development-v1/fixtures/readonly-mutation/healthy/tsconfig.json new file mode 100644 index 0000000..ebd9c6d --- /dev/null +++ b/benchmarks/type-detection/development-v1/fixtures/readonly-mutation/healthy/tsconfig.json @@ -0,0 +1,16 @@ +{ + "compilerOptions": { + "target": "ES2022", + "module": "NodeNext", + "moduleResolution": "NodeNext", + "strict": true, + "noEmit": true, + "allowImportingTsExtensions": true, + "erasableSyntaxOnly": true, + "types": [], + "skipLibCheck": false + }, + "include": [ + "**/*.ts" + ] +} diff --git a/benchmarks/type-detection/development-v1/fixtures/referenced-workspace/fault/package.json b/benchmarks/type-detection/development-v1/fixtures/referenced-workspace/fault/package.json new file mode 100644 index 0000000..af50513 --- /dev/null +++ b/benchmarks/type-detection/development-v1/fixtures/referenced-workspace/fault/package.json @@ -0,0 +1 @@ +{"name":"testlore-independent-development-spec","private":true,"type":"module"} diff --git a/benchmarks/type-detection/development-v1/fixtures/referenced-workspace/fault/packages/app/module.ts b/benchmarks/type-detection/development-v1/fixtures/referenced-workspace/fault/packages/app/module.ts new file mode 100644 index 0000000..24fc4aa --- /dev/null +++ b/benchmarks/type-detection/development-v1/fixtures/referenced-workspace/fault/packages/app/module.ts @@ -0,0 +1,5 @@ +import type {Result} from '../contracts/types.js'; +export function next(mode: 'idle' | 'fast'): Result { + if (mode === 'idle') return {value: 0}; + return {value: '1'}; +} diff --git a/benchmarks/type-detection/development-v1/fixtures/referenced-workspace/fault/packages/app/tsconfig.json b/benchmarks/type-detection/development-v1/fixtures/referenced-workspace/fault/packages/app/tsconfig.json new file mode 100644 index 0000000..b6d8157 --- /dev/null +++ b/benchmarks/type-detection/development-v1/fixtures/referenced-workspace/fault/packages/app/tsconfig.json @@ -0,0 +1,25 @@ +{ + "compilerOptions": { + "target": "ES2022", + "module": "NodeNext", + "moduleResolution": "NodeNext", + "strict": true, + "noEmit": false, + "allowImportingTsExtensions": false, + "erasableSyntaxOnly": true, + "types": [], + "skipLibCheck": false, + "composite": true, + "declaration": true, + "rootDir": ".", + "outDir": "dist" + }, + "include": [ + "module.ts" + ], + "references": [ + { + "path": "../contracts" + } + ] +} diff --git a/benchmarks/type-detection/development-v1/fixtures/referenced-workspace/fault/packages/contracts/tsconfig.json b/benchmarks/type-detection/development-v1/fixtures/referenced-workspace/fault/packages/contracts/tsconfig.json new file mode 100644 index 0000000..0446ca3 --- /dev/null +++ b/benchmarks/type-detection/development-v1/fixtures/referenced-workspace/fault/packages/contracts/tsconfig.json @@ -0,0 +1,20 @@ +{ + "compilerOptions": { + "target": "ES2022", + "module": "NodeNext", + "moduleResolution": "NodeNext", + "strict": true, + "noEmit": false, + "allowImportingTsExtensions": false, + "erasableSyntaxOnly": true, + "types": [], + "skipLibCheck": false, + "composite": true, + "declaration": true, + "rootDir": ".", + "outDir": "dist" + }, + "include": [ + "types.ts" + ] +} diff --git a/benchmarks/type-detection/development-v1/fixtures/referenced-workspace/fault/packages/contracts/types.ts b/benchmarks/type-detection/development-v1/fixtures/referenced-workspace/fault/packages/contracts/types.ts new file mode 100644 index 0000000..778715f --- /dev/null +++ b/benchmarks/type-detection/development-v1/fixtures/referenced-workspace/fault/packages/contracts/types.ts @@ -0,0 +1 @@ +export interface Result { value: number } diff --git a/benchmarks/type-detection/development-v1/fixtures/referenced-workspace/fault/probe.mjs b/benchmarks/type-detection/development-v1/fixtures/referenced-workspace/fault/probe.mjs new file mode 100644 index 0000000..9dcdc91 --- /dev/null +++ b/benchmarks/type-detection/development-v1/fixtures/referenced-workspace/fault/probe.mjs @@ -0,0 +1,4 @@ +import assert from 'node:assert/strict'; +import {next} from './packages/app/module.ts'; +assert.deepEqual(next('idle'), {value: 0}); +console.log('ordinary-input assertions passed'); diff --git a/benchmarks/type-detection/development-v1/fixtures/referenced-workspace/fault/tsconfig.json b/benchmarks/type-detection/development-v1/fixtures/referenced-workspace/fault/tsconfig.json new file mode 100644 index 0000000..36ddfe5 --- /dev/null +++ b/benchmarks/type-detection/development-v1/fixtures/referenced-workspace/fault/tsconfig.json @@ -0,0 +1,11 @@ +{ + "files": [], + "references": [ + { + "path": "./packages/contracts" + }, + { + "path": "./packages/app" + } + ] +} diff --git a/benchmarks/type-detection/development-v1/fixtures/referenced-workspace/healthy/package.json b/benchmarks/type-detection/development-v1/fixtures/referenced-workspace/healthy/package.json new file mode 100644 index 0000000..af50513 --- /dev/null +++ b/benchmarks/type-detection/development-v1/fixtures/referenced-workspace/healthy/package.json @@ -0,0 +1 @@ +{"name":"testlore-independent-development-spec","private":true,"type":"module"} diff --git a/benchmarks/type-detection/development-v1/fixtures/referenced-workspace/healthy/packages/app/module.ts b/benchmarks/type-detection/development-v1/fixtures/referenced-workspace/healthy/packages/app/module.ts new file mode 100644 index 0000000..f0300d8 --- /dev/null +++ b/benchmarks/type-detection/development-v1/fixtures/referenced-workspace/healthy/packages/app/module.ts @@ -0,0 +1,5 @@ +import type {Result} from '../contracts/types.js'; +export function next(mode: 'idle' | 'fast'): Result { + if (mode === 'idle') return {value: 0}; + return {value: 1}; +} diff --git a/benchmarks/type-detection/development-v1/fixtures/referenced-workspace/healthy/packages/app/tsconfig.json b/benchmarks/type-detection/development-v1/fixtures/referenced-workspace/healthy/packages/app/tsconfig.json new file mode 100644 index 0000000..b6d8157 --- /dev/null +++ b/benchmarks/type-detection/development-v1/fixtures/referenced-workspace/healthy/packages/app/tsconfig.json @@ -0,0 +1,25 @@ +{ + "compilerOptions": { + "target": "ES2022", + "module": "NodeNext", + "moduleResolution": "NodeNext", + "strict": true, + "noEmit": false, + "allowImportingTsExtensions": false, + "erasableSyntaxOnly": true, + "types": [], + "skipLibCheck": false, + "composite": true, + "declaration": true, + "rootDir": ".", + "outDir": "dist" + }, + "include": [ + "module.ts" + ], + "references": [ + { + "path": "../contracts" + } + ] +} diff --git a/benchmarks/type-detection/development-v1/fixtures/referenced-workspace/healthy/packages/contracts/tsconfig.json b/benchmarks/type-detection/development-v1/fixtures/referenced-workspace/healthy/packages/contracts/tsconfig.json new file mode 100644 index 0000000..0446ca3 --- /dev/null +++ b/benchmarks/type-detection/development-v1/fixtures/referenced-workspace/healthy/packages/contracts/tsconfig.json @@ -0,0 +1,20 @@ +{ + "compilerOptions": { + "target": "ES2022", + "module": "NodeNext", + "moduleResolution": "NodeNext", + "strict": true, + "noEmit": false, + "allowImportingTsExtensions": false, + "erasableSyntaxOnly": true, + "types": [], + "skipLibCheck": false, + "composite": true, + "declaration": true, + "rootDir": ".", + "outDir": "dist" + }, + "include": [ + "types.ts" + ] +} diff --git a/benchmarks/type-detection/development-v1/fixtures/referenced-workspace/healthy/packages/contracts/types.ts b/benchmarks/type-detection/development-v1/fixtures/referenced-workspace/healthy/packages/contracts/types.ts new file mode 100644 index 0000000..778715f --- /dev/null +++ b/benchmarks/type-detection/development-v1/fixtures/referenced-workspace/healthy/packages/contracts/types.ts @@ -0,0 +1 @@ +export interface Result { value: number } diff --git a/benchmarks/type-detection/development-v1/fixtures/referenced-workspace/healthy/probe.mjs b/benchmarks/type-detection/development-v1/fixtures/referenced-workspace/healthy/probe.mjs new file mode 100644 index 0000000..9dcdc91 --- /dev/null +++ b/benchmarks/type-detection/development-v1/fixtures/referenced-workspace/healthy/probe.mjs @@ -0,0 +1,4 @@ +import assert from 'node:assert/strict'; +import {next} from './packages/app/module.ts'; +assert.deepEqual(next('idle'), {value: 0}); +console.log('ordinary-input assertions passed'); diff --git a/benchmarks/type-detection/development-v1/fixtures/referenced-workspace/healthy/tsconfig.json b/benchmarks/type-detection/development-v1/fixtures/referenced-workspace/healthy/tsconfig.json new file mode 100644 index 0000000..36ddfe5 --- /dev/null +++ b/benchmarks/type-detection/development-v1/fixtures/referenced-workspace/healthy/tsconfig.json @@ -0,0 +1,11 @@ +{ + "files": [], + "references": [ + { + "path": "./packages/contracts" + }, + { + "path": "./packages/app" + } + ] +} diff --git a/benchmarks/type-detection/development-v1/fixtures/return-contract/fault/module.ts b/benchmarks/type-detection/development-v1/fixtures/return-contract/fault/module.ts new file mode 100644 index 0000000..03796b5 --- /dev/null +++ b/benchmarks/type-detection/development-v1/fixtures/return-contract/fault/module.ts @@ -0,0 +1,4 @@ +export function scale(value: number): number { + if (value === 0) return 0; + return String(value * 2); +} diff --git a/benchmarks/type-detection/development-v1/fixtures/return-contract/fault/package.json b/benchmarks/type-detection/development-v1/fixtures/return-contract/fault/package.json new file mode 100644 index 0000000..af50513 --- /dev/null +++ b/benchmarks/type-detection/development-v1/fixtures/return-contract/fault/package.json @@ -0,0 +1 @@ +{"name":"testlore-independent-development-spec","private":true,"type":"module"} diff --git a/benchmarks/type-detection/development-v1/fixtures/return-contract/fault/probe.mjs b/benchmarks/type-detection/development-v1/fixtures/return-contract/fault/probe.mjs new file mode 100644 index 0000000..9d84bd1 --- /dev/null +++ b/benchmarks/type-detection/development-v1/fixtures/return-contract/fault/probe.mjs @@ -0,0 +1,4 @@ +import assert from 'node:assert/strict'; +import {scale} from './module.ts'; +assert.equal(scale(0), 0); +console.log('ordinary-input assertions passed'); diff --git a/benchmarks/type-detection/development-v1/fixtures/return-contract/fault/tsconfig.json b/benchmarks/type-detection/development-v1/fixtures/return-contract/fault/tsconfig.json new file mode 100644 index 0000000..ebd9c6d --- /dev/null +++ b/benchmarks/type-detection/development-v1/fixtures/return-contract/fault/tsconfig.json @@ -0,0 +1,16 @@ +{ + "compilerOptions": { + "target": "ES2022", + "module": "NodeNext", + "moduleResolution": "NodeNext", + "strict": true, + "noEmit": true, + "allowImportingTsExtensions": true, + "erasableSyntaxOnly": true, + "types": [], + "skipLibCheck": false + }, + "include": [ + "**/*.ts" + ] +} diff --git a/benchmarks/type-detection/development-v1/fixtures/return-contract/healthy/module.ts b/benchmarks/type-detection/development-v1/fixtures/return-contract/healthy/module.ts new file mode 100644 index 0000000..690d352 --- /dev/null +++ b/benchmarks/type-detection/development-v1/fixtures/return-contract/healthy/module.ts @@ -0,0 +1,4 @@ +export function scale(value: number): number { + if (value === 0) return 0; + return value * 2; +} diff --git a/benchmarks/type-detection/development-v1/fixtures/return-contract/healthy/package.json b/benchmarks/type-detection/development-v1/fixtures/return-contract/healthy/package.json new file mode 100644 index 0000000..af50513 --- /dev/null +++ b/benchmarks/type-detection/development-v1/fixtures/return-contract/healthy/package.json @@ -0,0 +1 @@ +{"name":"testlore-independent-development-spec","private":true,"type":"module"} diff --git a/benchmarks/type-detection/development-v1/fixtures/return-contract/healthy/probe.mjs b/benchmarks/type-detection/development-v1/fixtures/return-contract/healthy/probe.mjs new file mode 100644 index 0000000..9d84bd1 --- /dev/null +++ b/benchmarks/type-detection/development-v1/fixtures/return-contract/healthy/probe.mjs @@ -0,0 +1,4 @@ +import assert from 'node:assert/strict'; +import {scale} from './module.ts'; +assert.equal(scale(0), 0); +console.log('ordinary-input assertions passed'); diff --git a/benchmarks/type-detection/development-v1/fixtures/return-contract/healthy/tsconfig.json b/benchmarks/type-detection/development-v1/fixtures/return-contract/healthy/tsconfig.json new file mode 100644 index 0000000..ebd9c6d --- /dev/null +++ b/benchmarks/type-detection/development-v1/fixtures/return-contract/healthy/tsconfig.json @@ -0,0 +1,16 @@ +{ + "compilerOptions": { + "target": "ES2022", + "module": "NodeNext", + "moduleResolution": "NodeNext", + "strict": true, + "noEmit": true, + "allowImportingTsExtensions": true, + "erasableSyntaxOnly": true, + "types": [], + "skipLibCheck": false + }, + "include": [ + "**/*.ts" + ] +} diff --git a/benchmarks/type-detection/development-v1/manifest.json b/benchmarks/type-detection/development-v1/manifest.json new file mode 100644 index 0000000..ff05fe5 --- /dev/null +++ b/benchmarks/type-detection/development-v1/manifest.json @@ -0,0 +1,583 @@ +{ + "schemaVersion": 1, + "kind": "derived-public-development-type-contract-fixtures", + "originManifestSha256": "16b34a7b750805b6c1b2b8b0988a7e2fd204ae5ce285c630f149111bd2b7b09c", + "derivedDisplayAndReproductionOnly": true, + "admissionAuthority": false, + "priorUnseenClaim": false, + "untouchedMaintainerBugClaim": false, + "upstreamHistoricalGateComplete": false, + "nodeRequirement": { + "version": "v22.19.0", + "runtime": "Native Node22 type stripping; top-level node:assert/strict probes, no assertion-free test wrappers." + }, + "compilerRequirement": { + "package": "typescript", + "version": "6.0.3", + "installation": "Use the pinned TestLore npm lock installation; independently capture actual local SDK bytes before and after verification. No SDK bundled in these fixtures.", + "originClosureSha256": "06d5172989ccb41ed11d848c0dab4adb749905146a472bd38f7c1276ee1a05ca", + "originClosureHashScope": "SHA256 of JSON sorted original package rows {file,sha256,bytes}; this digest is reproduction evidence, never executable trust/admission." + }, + "uniqueScoredDevelopmentDefects": 5, + "separateUnqualifiedReferenceDefects": 1, + "nativePositiveAssertionsOrdinary": 6, + "specs": [ + { + "id": "nullable-branch", + "category": "unsafe-nullable-return", + "uniqueSeededDefects": 1, + "diagnosticCodes": [ + 2322 + ], + "qualificationTier": "ordinary-project", + "nativePositiveAssertions": 1, + "states": { + "healthy": { + "directory": "fixtures/nullable-branch/healthy", + "sourceInputs": [ + { + "file": "module.ts", + "sha256": "c234cd1c5486c707c906780659ef331cd1ed8bc49190ef90352eddb64b66508f", + "bytes": 126 + }, + { + "file": "package.json", + "sha256": "62c9ee3336a5a38c92ec171ea5fcedb621eaf7693c0729a77aefd4b2d68f6ec1", + "bytes": 80 + }, + { + "file": "probe.mjs", + "sha256": "e4b98e1e4d0101f13440ce46bbf0a52b7607c063900bb5389571235582a34405", + "bytes": 164 + }, + { + "file": "tsconfig.json", + "sha256": "b5f075e72b83f9ecb7578d945a13aa955ef98dec6ed5d5e74605d40e6a9950a1", + "bytes": 306 + } + ], + "inputsSha256": "fd2576027f281140a7b3252f697dcab3cdc8e934b7c1b3c849c94f74a9f7278a", + "expectedNativeExit": 0, + "originalCheckerArgs": [ + "--project", + "tsconfig.json", + "--pretty", + "false" + ], + "expectedOriginalCheckerExit": 0 + }, + "fault": { + "directory": "fixtures/nullable-branch/fault", + "sourceInputs": [ + { + "file": "module.ts", + "sha256": "a256bfc805e68a1ea59f1126ff36fa4e417687b6885fcd056836a4b59053f6f6", + "bytes": 122 + }, + { + "file": "package.json", + "sha256": "62c9ee3336a5a38c92ec171ea5fcedb621eaf7693c0729a77aefd4b2d68f6ec1", + "bytes": 80 + }, + { + "file": "probe.mjs", + "sha256": "e4b98e1e4d0101f13440ce46bbf0a52b7607c063900bb5389571235582a34405", + "bytes": 164 + }, + { + "file": "tsconfig.json", + "sha256": "b5f075e72b83f9ecb7578d945a13aa955ef98dec6ed5d5e74605d40e6a9950a1", + "bytes": 306 + } + ], + "inputsSha256": "e3d3171b58578be51cdac2cc2656d605c8f085cdb3e2442004a1a0521c0ea154", + "expectedNativeExit": 0, + "originalCheckerArgs": [ + "--project", + "tsconfig.json", + "--pretty", + "false" + ], + "expectedOriginalCheckerExit": 2 + } + }, + "qualification": { + "scope": "Independent development example only; original runtime assertions pass both states, original checker and frozen component detect the faulty type contract." + } + }, + { + "id": "return-contract", + "category": "declared-return-type-mismatch", + "uniqueSeededDefects": 1, + "diagnosticCodes": [ + 2322 + ], + "qualificationTier": "ordinary-project", + "nativePositiveAssertions": 1, + "states": { + "healthy": { + "directory": "fixtures/return-contract/healthy", + "sourceInputs": [ + { + "file": "module.ts", + "sha256": "b4951787a55719ecc15d23e9d6e7c49053417937201fc4601748b7e02f7fcd94", + "bytes": 98 + }, + { + "file": "package.json", + "sha256": "62c9ee3336a5a38c92ec171ea5fcedb621eaf7693c0729a77aefd4b2d68f6ec1", + "bytes": 80 + }, + { + "file": "probe.mjs", + "sha256": "fe9fcd354082c89cf02ae7a9fa2c83ffe6689903e9e2600f1bfce60798e9550a", + "bytes": 152 + }, + { + "file": "tsconfig.json", + "sha256": "b5f075e72b83f9ecb7578d945a13aa955ef98dec6ed5d5e74605d40e6a9950a1", + "bytes": 306 + } + ], + "inputsSha256": "b2fc5e2a494052b062d06dd3d2ee9c8ad1154e82764e884d1fc993bc3b12b830", + "expectedNativeExit": 0, + "originalCheckerArgs": [ + "--project", + "tsconfig.json", + "--pretty", + "false" + ], + "expectedOriginalCheckerExit": 0 + }, + "fault": { + "directory": "fixtures/return-contract/fault", + "sourceInputs": [ + { + "file": "module.ts", + "sha256": "1d25125a671e45524ebb06654c6257ed72af4faaef58ac910d9fa3bbe4529ad8", + "bytes": 106 + }, + { + "file": "package.json", + "sha256": "62c9ee3336a5a38c92ec171ea5fcedb621eaf7693c0729a77aefd4b2d68f6ec1", + "bytes": 80 + }, + { + "file": "probe.mjs", + "sha256": "fe9fcd354082c89cf02ae7a9fa2c83ffe6689903e9e2600f1bfce60798e9550a", + "bytes": 152 + }, + { + "file": "tsconfig.json", + "sha256": "b5f075e72b83f9ecb7578d945a13aa955ef98dec6ed5d5e74605d40e6a9950a1", + "bytes": 306 + } + ], + "inputsSha256": "ef5b3de2e7271d7ebd25678a473e54aafd31c0e3f46c16e90d9459aa2da6631c", + "expectedNativeExit": 0, + "originalCheckerArgs": [ + "--project", + "tsconfig.json", + "--pretty", + "false" + ], + "expectedOriginalCheckerExit": 2 + } + }, + "qualification": { + "scope": "Independent development example only; original runtime assertions pass both states, original checker and frozen component detect the faulty type contract." + } + }, + { + "id": "cross-module-contract", + "category": "type-only-cross-module-contract-mismatch", + "uniqueSeededDefects": 1, + "diagnosticCodes": [ + 2322 + ], + "qualificationTier": "ordinary-project", + "nativePositiveAssertions": 1, + "states": { + "healthy": { + "directory": "fixtures/cross-module-contract/healthy", + "sourceInputs": [ + { + "file": "package.json", + "sha256": "62c9ee3336a5a38c92ec171ea5fcedb621eaf7693c0729a77aefd4b2d68f6ec1", + "bytes": 80 + }, + { + "file": "packages/app/module.ts", + "sha256": "9a4c573f3fb1eb6667d9bf33ee519969a4910da7d6a966ba5440f87a42ed1de8", + "bytes": 120 + }, + { + "file": "packages/contracts/types.ts", + "sha256": "a408c3f43d24fca3dbe78779ee7991ade6142c718547cc964cf884ebc6ca34b9", + "bytes": 56 + }, + { + "file": "probe.mjs", + "sha256": "46d17f10c397140d2dc6d4e3c7ebc4a81e9dc260429da6e9454ce1e712decdf0", + "bytes": 173 + }, + { + "file": "tsconfig.json", + "sha256": "b5f075e72b83f9ecb7578d945a13aa955ef98dec6ed5d5e74605d40e6a9950a1", + "bytes": 306 + } + ], + "inputsSha256": "54b779193e3485fc1e49d4a165db74152325a47366d2c0c2b114a966f5e75c58", + "expectedNativeExit": 0, + "originalCheckerArgs": [ + "--project", + "tsconfig.json", + "--pretty", + "false" + ], + "expectedOriginalCheckerExit": 0 + }, + "fault": { + "directory": "fixtures/cross-module-contract/fault", + "sourceInputs": [ + { + "file": "package.json", + "sha256": "62c9ee3336a5a38c92ec171ea5fcedb621eaf7693c0729a77aefd4b2d68f6ec1", + "bytes": 80 + }, + { + "file": "packages/app/module.ts", + "sha256": "6f9f5d848af89f4c7b5613f55896695c81e36912dc260a4753bafc35d5524551", + "bytes": 122 + }, + { + "file": "packages/contracts/types.ts", + "sha256": "a408c3f43d24fca3dbe78779ee7991ade6142c718547cc964cf884ebc6ca34b9", + "bytes": 56 + }, + { + "file": "probe.mjs", + "sha256": "46d17f10c397140d2dc6d4e3c7ebc4a81e9dc260429da6e9454ce1e712decdf0", + "bytes": 173 + }, + { + "file": "tsconfig.json", + "sha256": "b5f075e72b83f9ecb7578d945a13aa955ef98dec6ed5d5e74605d40e6a9950a1", + "bytes": 306 + } + ], + "inputsSha256": "84703fb567feb839379d685c34f4f3a75f7f31ec1fd4180eb5b647c21e2bc3cc", + "expectedNativeExit": 0, + "originalCheckerArgs": [ + "--project", + "tsconfig.json", + "--pretty", + "false" + ], + "expectedOriginalCheckerExit": 2 + } + }, + "qualification": { + "scope": "Independent development example only; original runtime assertions pass both states, original checker and frozen component detect the faulty type contract." + } + }, + { + "id": "api-consumer", + "category": "invalid-api-boundary-consumer", + "uniqueSeededDefects": 1, + "diagnosticCodes": [ + 2345 + ], + "qualificationTier": "ordinary-project", + "nativePositiveAssertions": 1, + "states": { + "healthy": { + "directory": "fixtures/api-consumer/healthy", + "sourceInputs": [ + { + "file": "api.ts", + "sha256": "5e252647d3a6ca057b5d8f61fd8a9ba973e2ec4e4aec8d83905cb2fb74cf2aa4", + "bytes": 76 + }, + { + "file": "consumer.ts", + "sha256": "49a7afe7ac455286221f881afad87e11bad64a596c669fa701509a7ec4776622", + "bytes": 108 + }, + { + "file": "package.json", + "sha256": "62c9ee3336a5a38c92ec171ea5fcedb621eaf7693c0729a77aefd4b2d68f6ec1", + "bytes": 80 + }, + { + "file": "probe.mjs", + "sha256": "aec0331ee134be5906375f84a8bff0dd1ef06b8ce7b3e8d2c4180520109ab9d3", + "bytes": 174 + }, + { + "file": "tsconfig.json", + "sha256": "b5f075e72b83f9ecb7578d945a13aa955ef98dec6ed5d5e74605d40e6a9950a1", + "bytes": 306 + } + ], + "inputsSha256": "5247708012109b88d1f5921d322bc7e50529e6e0437a8b59eb79f72009ebe3d8", + "expectedNativeExit": 0, + "originalCheckerArgs": [ + "--project", + "tsconfig.json", + "--pretty", + "false" + ], + "expectedOriginalCheckerExit": 0 + }, + "fault": { + "directory": "fixtures/api-consumer/fault", + "sourceInputs": [ + { + "file": "api.ts", + "sha256": "5e252647d3a6ca057b5d8f61fd8a9ba973e2ec4e4aec8d83905cb2fb74cf2aa4", + "bytes": 76 + }, + { + "file": "consumer.ts", + "sha256": "b604d8d5186eab787db59af0c7b8e9bcb09dcc644c3f8eaac67438c947cf6c92", + "bytes": 106 + }, + { + "file": "package.json", + "sha256": "62c9ee3336a5a38c92ec171ea5fcedb621eaf7693c0729a77aefd4b2d68f6ec1", + "bytes": 80 + }, + { + "file": "probe.mjs", + "sha256": "aec0331ee134be5906375f84a8bff0dd1ef06b8ce7b3e8d2c4180520109ab9d3", + "bytes": 174 + }, + { + "file": "tsconfig.json", + "sha256": "b5f075e72b83f9ecb7578d945a13aa955ef98dec6ed5d5e74605d40e6a9950a1", + "bytes": 306 + } + ], + "inputsSha256": "2edc051d9d14d2f7f22db256a9fb97fff478deabf27a2d4d5a5c2687e8be4806", + "expectedNativeExit": 0, + "originalCheckerArgs": [ + "--project", + "tsconfig.json", + "--pretty", + "false" + ], + "expectedOriginalCheckerExit": 2 + } + }, + "qualification": { + "scope": "Independent development example only; original runtime assertions pass both states, original checker and frozen component detect the faulty type contract." + } + }, + { + "id": "readonly-mutation", + "category": "readonly-contract-mutation", + "uniqueSeededDefects": 1, + "diagnosticCodes": [ + 2540 + ], + "qualificationTier": "ordinary-project", + "nativePositiveAssertions": 2, + "states": { + "healthy": { + "directory": "fixtures/readonly-mutation/healthy", + "sourceInputs": [ + { + "file": "module.ts", + "sha256": "2d8f629a7a359b2a4626c20926a552ded833f6f96a1825cc23cc9871c56e84b7", + "bytes": 127 + }, + { + "file": "package.json", + "sha256": "62c9ee3336a5a38c92ec171ea5fcedb621eaf7693c0729a77aefd4b2d68f6ec1", + "bytes": 80 + }, + { + "file": "probe.mjs", + "sha256": "44c28952651e88bb9f548a7dbe14cfba17b6e63df643230d8782ba6ba5fa992a", + "bytes": 222 + }, + { + "file": "tsconfig.json", + "sha256": "b5f075e72b83f9ecb7578d945a13aa955ef98dec6ed5d5e74605d40e6a9950a1", + "bytes": 306 + } + ], + "inputsSha256": "d002d71fe1e22fa025311fa8e0a5f318f95091dc415da377dd8f6e685df6525f", + "expectedNativeExit": 0, + "originalCheckerArgs": [ + "--project", + "tsconfig.json", + "--pretty", + "false" + ], + "expectedOriginalCheckerExit": 0 + }, + "fault": { + "directory": "fixtures/readonly-mutation/fault", + "sourceInputs": [ + { + "file": "module.ts", + "sha256": "eb88f6428794ade5c98055c4866dddb91604d7906f8fa5f660923919f40b0b44", + "bytes": 131 + }, + { + "file": "package.json", + "sha256": "62c9ee3336a5a38c92ec171ea5fcedb621eaf7693c0729a77aefd4b2d68f6ec1", + "bytes": 80 + }, + { + "file": "probe.mjs", + "sha256": "44c28952651e88bb9f548a7dbe14cfba17b6e63df643230d8782ba6ba5fa992a", + "bytes": 222 + }, + { + "file": "tsconfig.json", + "sha256": "b5f075e72b83f9ecb7578d945a13aa955ef98dec6ed5d5e74605d40e6a9950a1", + "bytes": 306 + } + ], + "inputsSha256": "fa85beadacf2332c34fa5f09021cf745326cf53b3c3b8c17273e5b2bb5b4834c", + "expectedNativeExit": 0, + "originalCheckerArgs": [ + "--project", + "tsconfig.json", + "--pretty", + "false" + ], + "expectedOriginalCheckerExit": 2 + } + }, + "qualification": { + "scope": "Independent development example only; original runtime assertions pass both states, original checker and frozen component detect the faulty type contract." + } + }, + { + "id": "referenced-workspace", + "category": "project-reference-type-contract-mismatch", + "uniqueSeededDefects": 1, + "diagnosticCodes": [ + 2322 + ], + "qualificationTier": "reference-build-required", + "nativePositiveAssertions": 1, + "states": { + "healthy": { + "directory": "fixtures/referenced-workspace/healthy", + "sourceInputs": [ + { + "file": "package.json", + "sha256": "62c9ee3336a5a38c92ec171ea5fcedb621eaf7693c0729a77aefd4b2d68f6ec1", + "bytes": 80 + }, + { + "file": "packages/app/module.ts", + "sha256": "b015373b9ef0bf6e109173b283affe6b7bd087c3f70a7f2e8bf2f1ec92da0ddd", + "bytes": 170 + }, + { + "file": "packages/app/tsconfig.json", + "sha256": "0bb5617b75e11c43113bba576df10745487f288b2cf4a540038b23c06dd7ed70", + "bytes": 464 + }, + { + "file": "packages/contracts/tsconfig.json", + "sha256": "896bdf7669e53048d4ede4a9abe01d29cc50b7db431ad203f55fb64614479008", + "bytes": 399 + }, + { + "file": "packages/contracts/types.ts", + "sha256": "47331a1e4727b3293ffe0fff1f7473fd7659849192b922dcb499a6b15420f69d", + "bytes": 42 + }, + { + "file": "probe.mjs", + "sha256": "d49c59985d9b7a86fb9b4def1e124bef9a830a3cb183b786ba1e34ec0ab1faac", + "bytes": 181 + }, + { + "file": "tsconfig.json", + "sha256": "16f9bcc58e4259fc3f0ec63a9c045381e6ed7ce5bf9f2632111fd60b609d4383", + "bytes": 134 + } + ], + "inputsSha256": "4632ea938374b46a3e4150a734a6012d189e63feab6259787539a2bb2a9961f2", + "expectedNativeExit": 0, + "originalCheckerArgs": [ + "--build", + "tsconfig.json", + "--pretty", + "false" + ], + "expectedOriginalCheckerExit": 0 + }, + "fault": { + "directory": "fixtures/referenced-workspace/fault", + "sourceInputs": [ + { + "file": "package.json", + "sha256": "62c9ee3336a5a38c92ec171ea5fcedb621eaf7693c0729a77aefd4b2d68f6ec1", + "bytes": 80 + }, + { + "file": "packages/app/module.ts", + "sha256": "c452dc22f83efd9abdf5223fe781261821e82617bc7954d99539f52c7866f33d", + "bytes": 172 + }, + { + "file": "packages/app/tsconfig.json", + "sha256": "0bb5617b75e11c43113bba576df10745487f288b2cf4a540038b23c06dd7ed70", + "bytes": 464 + }, + { + "file": "packages/contracts/tsconfig.json", + "sha256": "896bdf7669e53048d4ede4a9abe01d29cc50b7db431ad203f55fb64614479008", + "bytes": 399 + }, + { + "file": "packages/contracts/types.ts", + "sha256": "47331a1e4727b3293ffe0fff1f7473fd7659849192b922dcb499a6b15420f69d", + "bytes": 42 + }, + { + "file": "probe.mjs", + "sha256": "d49c59985d9b7a86fb9b4def1e124bef9a830a3cb183b786ba1e34ec0ab1faac", + "bytes": 181 + }, + { + "file": "tsconfig.json", + "sha256": "16f9bcc58e4259fc3f0ec63a9c045381e6ed7ce5bf9f2632111fd60b609d4383", + "bytes": 134 + } + ], + "inputsSha256": "993ba722b23602900b249c93245c8cad77cee151376e93b95c4383a2f7b329c2", + "expectedNativeExit": 0, + "originalCheckerArgs": [ + "--build", + "tsconfig.json", + "--pretty", + "false" + ], + "expectedOriginalCheckerExit": 2 + } + }, + "qualification": { + "complete": false, + "scoredForDetection": false, + "scoredForSpeed": false, + "reason": "Original reference build repeats produce the expected diagnostic but exits2 then1 versus frozen expected2. The component explicitly refuses project references. Keep this fixture as an unsupported/incomplete boundary, never a green or zero-time win." + } + } + ], + "limits": [ + "Intentionally partial ordinary-input native probes; green execution does not validate every type contract or branch.", + "These sources were independently frozen before the evaluated checker component, but are DEVELOPMENT examples. Once exported they are openly inspectable, not future holdouts.", + "No SDK, private inventory, credentials, user identifiers, machine paths or original private run artifacts included.", + "No speed, learning, upstream acceptance or complete product integration claim." + ] +} diff --git a/docs/adr/011-complementary-native-type-oracle.md b/docs/adr/011-complementary-native-type-oracle.md new file mode 100644 index 0000000..0c5c72a --- /dev/null +++ b/docs/adr/011-complementary-native-type-oracle.md @@ -0,0 +1,15 @@ +# 011: Complementary native type verification + +Accepted for the initial single-project TypeScript profile. + +## Problem and decision + +Runtime-green receipts can hide declared type-contract failures. Routing already reasons about type dependencies, but that analysis does not execute the compiler. Add an installed project-local TypeScript oracle alongside native runtime verification and explicit independent behavioral challenges. Setup recommends it from static project fit and preserves explicit choices. + +Keep native cases/history/comparisons separate. Overall failure or incompleteness includes the requested compiler check. Bind full ignored-source/dependency bytes, declared configuration, actual Node and producer closure before planning/native execution and after all callbacks. Only the original opaque opening object carries the cross-window binding; serialized evidence is display data. The oracle forces no emission and rejects unsupported/incomplete scope rather than claiming broad coverage. + +## Verification and consequences + +Independent development specifications predate the first component evaluation. Their ordinary runtime assertions miss five seeded type defects; the original compiler detects them. Retain healthy controls, all repeat exposures, exact input hashes, unsupported reference evidence and controller cost. Challenge the evaluator with malformed diagnostics, no-check sources, reserved inputs, service callback drift, ignored native-input mutation, copied opening tokens and contradictory persisted reports. + +This adds execution and hashing cost. It supplies no omission authority, universal defect recall, learning promotion or overall competitive ranking. Project-reference builds and external symlinked dependency layouts need separate qualification. Source repair must satisfy enabled checks before authenticated application/publication; a requested but unqualified check cannot be silently ignored. diff --git a/docs/defect-detection.md b/docs/defect-detection.md index 0a96da8..a43f3fb 100644 --- a/docs/defect-detection.md +++ b/docs/defect-detection.md @@ -67,6 +67,8 @@ Primary documentation inspected October 12, 2026. Descriptions below are upstrea TestLore should coordinate complementary oracles and preserve their evidence. Enabling a tool, retrieving a lesson, parsing an advisory report or listing an adapter does not establish that its detectors executed successfully. Unrun detector families remain unknown. +The installed project-local TypeScript checker now participates in routine verification when enabled. Setup recommends it for a root TypeScript project. Five observed development defect pairs demonstrate native-green/type-failed behavior with healthy controls; the genuine TypeScript compiler detects the same defects. [Scope, failure handling and public reproduction](native-typecheck.md). + ## Qualification and next detection gate The new evaluator controls intentionally use development fixtures. They demonstrate native-green/contract-failed behavior, empty selections, continuing independent cases, setup/fallback/cancellation, ignored/dependency drift, timeout/output rejection, and completed asynchronous assertions. They do not demonstrate new upstream defects or out-of-sample model quality. diff --git a/docs/native-typecheck.md b/docs/native-typecheck.md new file mode 100644 index 0000000..0b446cc --- /dev/null +++ b/docs/native-typecheck.md @@ -0,0 +1,50 @@ +# Native TypeScript verification + +A runtime test can pass while a nullable branch, declared return type, readonly mutation or consumer call violates a type contract. TestLore runs the project's installed TypeScript compiler as a complementary oracle. It does not replace the project's tests or make type checking a universal correctness claim. + +`testlore setup --verify` recommends and enables this check when it finds both a root `tsconfig.json` and an installed project-local TypeScript SDK. Explicitly disabled plugins stay disabled. Setup reads bounded package/configuration metadata without importing the compiler, downloading optional tools or calling providers. Execution then qualifies the actual compiler configuration and inputs. + +```json +{ + "plugins": { + "typescript": {"enabled": true, "project": "tsconfig.json", "timeoutMs": 30000} + } +} +``` + +Run through the full shadow workflow with `testlore run --shadow`. To inspect just the compiler oracle: + +```sh +testlore typecheck --project tsconfig.json --json +``` + +The standalone command returns its result directly; it does not claim that a native runtime suite ran. Routine verification persists the combined run report. Native cases, history and shadow comparisons retain their native meaning. The overall exit is **0** when requested checks complete and pass, **1** for demonstrated failures, or **2** for incomplete evidence. A type diagnostic cannot disappear behind green runtime cases. Human summaries show diagnostic codes and bounded locations without echoing raw diagnostic prose. + +## Qualified scope and limitations + +The initial profile supports one project-local, unreferenced TypeScript implementation program. It uses the genuine installed SDK, preserves declared compiler options affecting checking, forces `noEmit`, disables incremental reuse and refuses writes. JSONC configuration is parsed by TypeScript. The checker records checked implementation files separately from SDK/declaration inputs. + +Project references, composite builds, compiler plugins, `noCheck`, `@ts-nocheck` implementation files, empty/declaration-only or unchecked-JavaScript scope, external input/symlink layouts, unresolved configuration, malformed output and exceeded budgets remain **incomplete**. A pnpm workspace with external dependency symlinks may require a different qualified profile. `skipLibCheck`, `skipDefaultLibCheck`, excluded files and `@ts-ignore`/`@ts-expect-error` suppressions remain disclosed limitations of the declared scope. + +The controller captures full source—including ignored files—and installed dependency bytes before runtime planning or execution. Its opening token is local, opaque and required by the combined verification controller; copied JSON cannot mint that binding. After runtime assertions and independent behavioral checks, it checks compiler results against the same inputs. Configuration/service callbacks run before the final byte capture. Source, dependency, Node or producer changes prevent a combined green report. Service probes and compiler execution use bounded, minimal environments. This is trusted native execution, not an OS security sandbox. + +Type verification does not permit test omissions. Routing still needs its existing independently qualified dependency evidence. A pass covers only the declared compiler program and recorded suppressions; browser behavior, service correctness and security remain separate obligations. + +Enabled type checking also gates the supported Node source-repair profile. Disposable repair copies retain the exact admitted installed dependency tree. Two baseline compiler checks may expose existing type errors; both candidate checks must complete and pass. Type evidence joins the authenticated validation receipt, and application/publication refuse missing, changed or failed evidence. Final branch verification checks the compiler again after independent behavioral challenges. Unsupported dependency relocation remains rejected. The initial repair profile still needs a genuine failing native assertion; a type-only failure produces a gap to resolve rather than an invented runtime repair. + +## Reproduce the development evidence + +The [public fixture manifest](../benchmarks/type-detection/development-v1/manifest.json) preserves five independently frozen development defect pairs and one separately unqualified project-reference pair. Each healthy/fault pair keeps identical ordinary-input assertions, configuration and shared contracts, changing exactly one TypeScript source. The runtime probes execute real top-level `node:assert/strict` assertions and print a completion marker. Reproduce from a reviewed source checkout: + +```sh +npm ci +node scripts/type-detection-benchmark.js --output .tddswarm/type-detection-replay +node scripts/type-detection-benchmark.js --verify .tddswarm/type-detection-replay/summary.json +node scripts/type-detection-benchmark.js --challenge-evaluator .tddswarm/type-detection-replay/summary.json +``` + +The output directory must be new. The harness checks the original fixture and TypeScript 6.0.3 SDK hashes, repeats healthy/fault states, retains raw native/compiler output and reports unique defects, false positives, incomplete arms and total time. Source is copied into isolated fresh directories; no model or provider is called. The original reference build's cache-sensitive exit discrepancy is retained in the manifest and excluded from scored detection and speed. Its unsupported TestLore result is exercised and recorded. + +These are now observed development examples, not untouched maintainer specifications, newly discovered upstream bugs, an AQE comparison or evidence of learning improvement. Native TypeScript catches the same type defects. TestLore's contribution is invoking that complementary check in the normal verification workflow, binding the inputs and retaining actionable evidence. + +Recorded local replay on Node 22.19.0 and TypeScript 6.0.3: **5 unique seeded defects, 10/10 fault exposures detected, 0 healthy false positives, 0 ordinary incomplete checks**. All 24 unchanged ordinary-input runtime probes passed; four reference exposures remained explicitly incomplete. Staging, opening capture, native probes, both compiler arms and evidence writes took **79.34 seconds** before the final receipt. This is a development qualification result with uncontrolled host load, not a speed claim. CI reruns the public examples, raw-output replay and eleven evaluator counterexamples. diff --git a/docs/plugins.md b/docs/plugins.md index 8d68dac..6f9a873 100644 --- a/docs/plugins.md +++ b/docs/plugins.md @@ -2,7 +2,7 @@ TestLore is a complementary coordination layer for independently maintained specialist tools. Users access their capabilities through a consistent workflow while those tools retain their native scope and TestLore retains its acceptance rules. Plugins are explicit project configuration, not automatic downloads. Listing plugins does not invoke them. Enabling a tool does not establish that it is installed, compatible, or produces effective tests. -The built-in catalog covers Agentic QE generation, pytest-testmon/Nx/Bazel selection and execution, c8/Stryker measured-report tooling, optional RuVector learning retrieval, and Guardana/iFixAi assessment report imports. Their implementations remain independent upstream projects. Updating one does not automatically qualify a new version: check its contract and run the project's validation after upgrades. +The built-in catalog covers installed TypeScript verification, Agentic QE generation, pytest-testmon/Nx/Bazel selection and execution, c8/Stryker measured-report tooling, optional RuVector learning retrieval, and Guardana/iFixAi assessment report imports. Their implementations remain independent upstream projects. Updating one does not automatically qualify a new version: check its contract and run the project's validation after upgrades. ## Let your project choose its setup @@ -15,6 +15,8 @@ Inspection reads bounded project manifests and local installation evidence witho This is a deterministic project-fit policy, not a performance ranking. Installation evidence does not certify a tool's runtime contract or measured quality: validate the actual operation. RuVector becomes applicable when local learning contains usable history and its optional SDK is present. AQE is chosen separately during generation rather than persistently enabled by setup. +For a root `tsconfig.json` and installed project-local TypeScript SDK, setup enables the complementary `typescript` native check. Runtime tests keep their native engine. Configure a different bounded project path or explicitly disable the check; unsupported references or incomplete compiler scope remain visible. Enabled checking also gates supported source repairs. [Compiler profile and public reproduction](native-typecheck.md). + ## Automatic generation composition Generation records a per-run `generationPlan` explaining the chosen author provider and rejected alternatives. Default automatic routing requires a safely inspected project-local supported AQE distribution, a compatible Vitest/Jest scope, independent requirements, distinct existing production-source tasks and an available architect/reviewer worker. Current automatic AQE admission is restricted to the inspected 3.14.8 distribution; a newer version needs contract qualification. Node assertions and source repair retain their supported JSON-worker path. Explicit disabled plugins, selected workers and manual AQE choices take precedence. diff --git a/package.json b/package.json index 9d17ba4..1225c0e 100644 --- a/package.json +++ b/package.json @@ -31,7 +31,8 @@ "demo": "node src/cli.js demo", "benchmark": "node scripts/benchmark.js", "benchmark:public": "node scripts/public-benchmark.js", - "benchmark:defects": "node scripts/generated-defects.js" + "benchmark:defects": "node scripts/generated-defects.js", + "benchmark:types": "node scripts/type-detection-benchmark.js" }, "dependencies": { "@modelcontextprotocol/server": "2.2.0", diff --git a/scripts/packed-evidence.js b/scripts/packed-evidence.js index 1dfbabb..a86a035 100644 --- a/scripts/packed-evidence.js +++ b/scripts/packed-evidence.js @@ -1,6 +1,29 @@ import {installedRepairEvidenceComplete} from './installed-repair-proof.js'; import {oneCommandEvidenceComplete} from './one-command-proof.js'; import {assessmentImportEvidenceComplete} from './assessment-import-proof.js'; +import fs from 'node:fs'; +import path from 'node:path'; +import {createHash} from 'node:crypto'; +const typePhases=[['setup-fault',1,'failed',true,1,[2322]],['healthy-shadow',0,'passed',true,1,[]],['unsupported-no-check',2,'incomplete',false,0,[]]]; +export function installedTypecheckEvidenceComplete(value){ + const hex=x=>typeof x==='string'&&/^[a-f0-9]{64}$/.test(x); + return value?.verified===true&&value.productionInstalled===true&&value.setupEnabled===true&&value.providerCalls===0&&hex(value.archiveSha256)&&hex(value.installedCliSha256)&& + typeof value.compilerVersion==='string'&&/^\d+\.\d+\.\d+/.test(value.compilerVersion)&&Array.isArray(value.outcomes)&&value.outcomes.length===3&& + typePhases.every(([id,exit,status,complete,count,codes],index)=>{ + const row=value.outcomes[index];return row?.id===id&&row.exitCode===exit&&row.nativeExitCode===0&&row.status===status&&row.complete===complete&&row.implementationCount===count&&JSON.stringify(row.errorCodes)===JSON.stringify(codes)&&hex(row.rawSha256); + }); +} +export function verifyInstalledTypecheckRawEvidence(value,directory){ + if(!installedTypecheckEvidenceComplete(value))return false; + try{return value.outcomes.every(row=>{ + const file=path.join(directory,row.id+'.json'),stat=fs.lstatSync(file);if(!stat.isFile()||stat.isSymbolicLink()||stat.size>16*1024*1024)return false; + const bytes=fs.readFileSync(file);if(createHash('sha256').update(bytes).digest('hex')!==row.rawSha256)return false; + const envelope=JSON.parse(bytes),report=envelope.verification||envelope,check=report.typecheck; + return report.exitCode===row.exitCode&&report.nativeExitCode===0&&check?.status===row.status&&check.complete===row.complete&& + check.implementationFiles?.length===row.implementationCount&&JSON.stringify(check.diagnostics.filter(d=>d.category==='error').map(d=>d.code))===JSON.stringify(row.errorCodes)&& + report.tests?.length===1&&report.tests[0].status==='passed'; + });}catch{return false;} +} /** Validate the independently produced installed-archive proof, without truthy attestations. */ export function packedEvidenceComplete(receipt, evidence, {proofScriptSha256, sourceManifestSha256, installedRepairProofSha256,oneCommandProofSha256,assessmentImportProofSha256}) { const hex = value => typeof value === 'string' && /^[a-f0-9]{64}$/.test(value); @@ -30,5 +53,7 @@ export function packedEvidenceComplete(receipt, evidence, {proofScriptSha256, so evidence.oneCommand.sourceRevision === receipt.sourceRevision && assessmentImportEvidenceComplete(evidence.assessmentImports) && hex(assessmentImportProofSha256) && evidence.assessmentImports.proofScriptSha256 === assessmentImportProofSha256 && evidence.assessmentImports.archiveSha256 === receipt.archiveSha256 && evidence.assessmentImports.installedManifestSha256 === receipt.packedManifestSha256 && - evidence.assessmentImports.sourceRevision === receipt.sourceRevision && evidence.assessmentImports.installedCliSha256 === evidence.oneCommand.installedCliSha256; + evidence.assessmentImports.sourceRevision === receipt.sourceRevision && evidence.assessmentImports.installedCliSha256 === evidence.oneCommand.installedCliSha256&& + installedTypecheckEvidenceComplete(evidence.installedTypecheck)&&evidence.installedTypecheck.archiveSha256===receipt.archiveSha256&& + evidence.installedTypecheck.installedCliSha256===evidence.oneCommand.installedCliSha256; } diff --git a/scripts/packed-proof.js b/scripts/packed-proof.js index 73bc4ca..b5be694 100644 --- a/scripts/packed-proof.js +++ b/scripts/packed-proof.js @@ -37,6 +37,32 @@ try{ invoke(tools,['npm','install','--omit=dev','--ignore-scripts','--no-audit','--no-fund',archive]); const installedPackage=JSON.parse(fs.readFileSync(path.join(tools,'node_modules/testlore/package.json'),'utf8'));assert.equal(installedPackage.name,'testlore');pack.version=installedPackage.version; const cli=path.join(tools,'node_modules/testlore/src/cli.js'); + const typedRoot=path.join(workspace,'type-oracle');fs.mkdirSync(typedRoot); + const typedWrite=(file,value)=>{const target=path.join(typedRoot,file);fs.mkdirSync(path.dirname(target),{recursive:true});fs.writeFileSync(target,typeof value==='string'?value:JSON.stringify(value));}; + typedWrite('package.json',{type:'module'});typedWrite('.gitignore','node_modules/\n.tddswarm/\n'); + typedWrite('src/value.ts','export const value: string = 42;\n'); + typedWrite('test/value.test.mjs',"import test from 'node:test';import assert from 'node:assert/strict';import {value} from '../src/value.ts';test('ordinary value',()=>assert.equal(value,42));"); + typedWrite('tsconfig.json',{compilerOptions:{strict:true,noEmit:true,module:'NodeNext',moduleResolution:'NodeNext',target:'ES2022'},files:['src/value.ts']}); + typedWrite('tddswarm.config.json',{adapter:'node',runner:[process.execPath,'--experimental-strip-types','--no-warnings','--test','{files}'],executionMode:'shadow'}); + fs.mkdirSync(path.join(typedRoot,'node_modules'));fs.cpSync(path.join(tools,'node_modules/typescript'),path.join(typedRoot,'node_modules/typescript'),{recursive:true}); + for(const args of [['init','-b','main'],['config','user.name','Packed Proof'],['config','user.email','proof@example.invalid'],['add','.'],['commit','-m','independent type contract']])invoke(typedRoot,['git',...args]); + const typedEvidenceDirectory=path.join(path.dirname(output),'installed-typecheck-raw');fs.mkdirSync(typedEvidenceDirectory,{recursive:true}); + const typeOutcomes=[]; + const typedCommand=(id,args,exit)=>{ + const stdout=invoke(typedRoot,[process.execPath,cli,...args,'--json'],exit),envelope=JSON.parse(stdout),report=envelope.verification||envelope; + fs.writeFileSync(path.join(typedEvidenceDirectory,id+'.json'),stdout,{flag:'wx'}); + typeOutcomes.push({id,exitCode:report.exitCode,nativeExitCode:report.nativeExitCode,status:report.typecheck.status,complete:report.typecheck.complete,implementationCount:report.typecheck.implementationFiles.length,errorCodes:report.typecheck.diagnostics.filter(row=>row.category==='error').map(row=>row.code),rawSha256:createHash('sha256').update(stdout).digest('hex')});return report; + }; + const typedFault=typedCommand('setup-fault',['setup','--verify','--no-ci'],1); + assert.equal(JSON.parse(fs.readFileSync(path.join(typedRoot,'tddswarm.config.json'))).plugins.typescript.enabled,true); + assert.equal(typedFault.nativeExitCode,0);assert.equal(typedFault.typecheck.status,'failed');assert.ok(typedFault.typecheck.diagnostics.some(row=>row.code===2322)); + typedWrite('src/value.ts','export const value: number = 42;\n'); + const typedHealthy=typedCommand('healthy-shadow',['run','--shadow','--base','HEAD'],0);assert.equal(typedHealthy.typecheck.status,'passed');assert.equal(typedHealthy.nativeExitCode,0); + typedWrite('tsconfig.json',{compilerOptions:{noCheck:true,noEmit:true},files:['src/value.ts']}); + const typedUnsupported=typedCommand('unsupported-no-check',['run','--shadow','--base','HEAD'],2);assert.equal(typedUnsupported.typecheck.complete,false);assert.equal(typedUnsupported.nativeExitCode,0); + const installedTypecheck={verified:true,productionInstalled:true,setupEnabled:true,providerCalls:0,outcomes:typeOutcomes, + archiveSha256:sha256,installedCliSha256:createHash('sha256').update(fs.readFileSync(cli)).digest('hex'),compilerVersion:typedHealthy.typecheck.compiler.version, + scope:'Controlled single-project TypeScript production-install fixture; no upstream defect or universal compatibility claim.'}; const command=(args,exit=0)=>JSON.parse(invoke(project,[process.execPath,cli,...args,'--json'],exit)); const immunityRoot=path.join(workspace,'immunity-inspection');fs.mkdirSync(immunityRoot); const immunity=command(['immunity','--root',immunityRoot]);assert.equal(immunity.backgroundServiceRunning,false);assert.equal(immunity.status,'awaiting-reviewed-probes');assert.equal(immunity.catalog.entries.length,3); @@ -124,7 +150,7 @@ try{ const installedRepair=installedRepairProof({workspace,cli,packageRoot:path.join(tools,'node_modules/testlore'),archiveSha256:sha256,sourceRevision:expectedSource,evidenceDirectory:path.join(path.dirname(output),'installed-repair-raw')}); const oneCommand=await oneCommandProof({workspace,archive,archiveSha256:sha256,sourceRevision:expectedSource||installedPackage.gitHead,installedManifestSha256:createHash('sha256').update(fs.readFileSync(path.join(tools,'node_modules/testlore/package.json'))).digest('hex'),installedCliSha256:createHash('sha256').update(fs.readFileSync(cli)).digest('hex'),evidenceDirectory:path.join(path.dirname(output),'one-command-raw')}); assert.equal(createHash('sha256').update(fs.readFileSync(archive)).digest('hex'),sha256,'Archive changed during qualification'); - const receipt={schemaVersion:1,exactInputArchive:Boolean(inputArchive),proofScriptSha256:createHash('sha256').update(fs.readFileSync(fileURLToPath(import.meta.url))).digest('hex'),date:new Date().toISOString(),archive:pack.filename,sha256,version:pack.version,node:process.version,productionInstall:true,mcpStdioVerified:true,mcpShadowVerified:true,briefInspectionVerified:true,evidenceLoopVerified:true,mappingQualificationVerified:true,packedManifestSha256:createHash('sha256').update(fs.readFileSync(path.join(tools,'node_modules/testlore/package.json'))).digest('hex'),actionReference:installedPackage.gitHead||null,workflowActionReference,actionIdentityVerified:Boolean(expectedSource),shadowSetupVerified:true,adoptionInspectionVerified:true,plugins:{catalogCount:catalog.plugins.length,missingSdkExit:missingVectorSdk.exitCode,lexicalFallbackVerified:true},nativeShadow:{complete:shadow.complete,selected:shadow.plan.selected,executed:shadow.executedFiles.length,detected:true},runtimeCaptureComplete:capture.complete,improvement:{status:improvement.status,cases:improvement.fullRun.tests.length,originalBranch:'main'},installedRepair,oneCommand,assessmentImports,limitations:['Local packed-artifact installation; no registry publication or live GitHub PR created.','Controlled two-module fixture; broad project compatibility remains unqualified.','Repair workers are deterministic transport fixtures; no real model provider quality or cost measured.','One-command npm exec uses the same sealed local archive and isolated cache reused between controlled runs; no cold-start speed claim.']}; + const receipt={schemaVersion:1,exactInputArchive:Boolean(inputArchive),proofScriptSha256:createHash('sha256').update(fs.readFileSync(fileURLToPath(import.meta.url))).digest('hex'),date:new Date().toISOString(),archive:pack.filename,sha256,version:pack.version,node:process.version,productionInstall:true,mcpStdioVerified:true,mcpShadowVerified:true,briefInspectionVerified:true,evidenceLoopVerified:true,mappingQualificationVerified:true,packedManifestSha256:createHash('sha256').update(fs.readFileSync(path.join(tools,'node_modules/testlore/package.json'))).digest('hex'),actionReference:installedPackage.gitHead||null,workflowActionReference,actionIdentityVerified:Boolean(expectedSource),shadowSetupVerified:true,adoptionInspectionVerified:true,plugins:{catalogCount:catalog.plugins.length,missingSdkExit:missingVectorSdk.exitCode,lexicalFallbackVerified:true},nativeShadow:{complete:shadow.complete,selected:shadow.plan.selected,executed:shadow.executedFiles.length,detected:true},runtimeCaptureComplete:capture.complete,improvement:{status:improvement.status,cases:improvement.fullRun.tests.length,originalBranch:'main'},installedRepair,oneCommand,assessmentImports,installedTypecheck,limitations:['Local packed-artifact installation; no registry publication or live GitHub PR created.','Controlled two-module fixture; broad project compatibility remains unqualified.','Repair workers are deterministic transport fixtures; no real model provider quality or cost measured.','One-command npm exec uses the same sealed local archive and isolated cache reused between controlled runs; no cold-start speed claim.']}; fs.mkdirSync(path.dirname(output),{recursive:true});fs.writeFileSync(output,JSON.stringify(receipt,null,2));console.log(JSON.stringify(receipt,null,2)); }finally{ if(improvement?.worktreeRoot)invoke(project,['git','worktree','remove','--force',improvement.worktreeRoot]); diff --git a/scripts/release-candidate.js b/scripts/release-candidate.js index da341d3..75e77ae 100644 --- a/scripts/release-candidate.js +++ b/scripts/release-candidate.js @@ -2,7 +2,7 @@ // Seal once, qualify that exact archive, and verify it again before a protected publish. import fs from 'node:fs'; import {sealSourceArchive} from './archive-source.js'; -import {packedEvidenceComplete} from './packed-evidence.js'; +import {packedEvidenceComplete,verifyInstalledTypecheckRawEvidence} from './packed-evidence.js'; import {verifyInstalledRepairRawEvidence} from './installed-repair-proof.js'; import {verifyOneCommandRawEvidence} from './one-command-proof.js'; import {verifyAssessmentImportRawEvidence} from './assessment-import-proof.js'; @@ -27,6 +27,7 @@ if(options['--verify']){ verifyInstalledRepairRawEvidence(path.join(directory,'installed-repair-raw'),evidence.installedRepair); verifyOneCommandRawEvidence(path.join(directory,'one-command-raw'),evidence.oneCommand); verifyAssessmentImportRawEvidence(path.join(directory,'assessment-import-raw'),evidence.assessmentImports); + if(!verifyInstalledTypecheckRawEvidence(evidence.installedTypecheck,path.join(directory,'installed-typecheck-raw')))throw Error('Installed TypeScript raw evidence incomplete or changed'); if(options.publish&&(!/^\d+\.\d+\.\d+(?:-alpha\.\d+)?$/.test(receipt.version)||process.env.NODE_AUTH_TOKEN||process.env.NPM_TOKEN))throw new Error('Publish requires a valid scoped alpha candidate version and token-free trusted OIDC'); if(options.publish&&(process.env.GITHUB_ACTIONS!=='true'||!process.env.ACTIONS_ID_TOKEN_REQUEST_URL||!process.env.ACTIONS_ID_TOKEN_REQUEST_TOKEN))throw new Error('Publish check requires a GitHub Actions OIDC-enabled job; publisher mapping is still an external gate'); console.log(JSON.stringify({...receipt,verified:true,alphaVersionEligible:/^\d+\.\d+\.\d+(?:-alpha\.\d+)?$/.test(receipt.version)},null,2)); diff --git a/scripts/type-detection-benchmark.js b/scripts/type-detection-benchmark.js new file mode 100644 index 0000000..ac52dda --- /dev/null +++ b/scripts/type-detection-benchmark.js @@ -0,0 +1,191 @@ +#!/usr/bin/env node +// Observed development replay, never an untouched holdout or admission token. +import fs from 'node:fs'; +import os from 'node:os'; +import path from 'node:path'; +import assert from 'node:assert/strict'; +import {createHash} from 'node:crypto'; +import {spawnSync} from 'node:child_process'; +import {fileURLToPath} from 'node:url'; +import {captureTypecheckInputs,verifyTypecheck} from '../src/typecheck.js'; +import {summarizeTypecheck} from '../src/typecheck-report.js'; + +const repository=fileURLToPath(new URL('../',import.meta.url)); +const hash=bytes=>createHash('sha256').update(bytes).digest('hex'); +const base=path.join(repository,'benchmarks/type-detection/development-v1'); +const manifestBytes=fs.readFileSync(path.join(base,'manifest.json')); +const manifest=JSON.parse(manifestBytes); +const MANIFEST_SHA='b4130e918e9d6a91edace4eb46e7a227aff46c88212a07760a85ed90942f0b39'; +const producerPaths=['typecheck.js','reporters/typecheck-worker.cjs','provenance.js','files.js','inputs.js','configuration-inputs.js','plugin-config.js']; +function inventory(directory,prefix='') { + return fs.readdirSync(directory,{withFileTypes:true}).sort((a,b)=>a.nameb.name?1:0).flatMap(entry=>{ + const file=prefix+entry.name,target=path.join(directory,entry.name),stat=fs.lstatSync(target); + if(stat.isDirectory())return inventory(target,file+'/'); + if(!stat.isFile()||stat.size>32*1024*1024)throw Error('Unsupported SDK entry'); + return [{file,sha256:hash(fs.readFileSync(target)),bytes:stat.size}]; + }); +} +function execute(args,cwd,output,role) { + const started=performance.now(),env={PATH:path.dirname(process.execPath),HOME:cwd,TMPDIR:os.tmpdir(),LANG:'C',LC_ALL:'C'}; + const result=spawnSync(process.execPath,args,{cwd,env,encoding:'utf8',timeout:30000,maxBuffer:2*1024*1024}); + const stdout=result.stdout||'',stderr=result.stderr||''; + fs.writeFileSync(path.join(output,role+'.stdout'),stdout,{flag:'wx'}); + fs.writeFileSync(path.join(output,role+'.stderr'),stderr,{flag:'wx'}); + const receipt={command:[process.execPath,...args],exitCode:result.status,signal:result.signal,error:result.error?.code||null, + durationMs:performance.now()-started,stdoutSha256:hash(stdout),stderrSha256:hash(stderr), + diagnosticCodes:[...new Set([...stdout.matchAll(/\bTS([0-9]+):/g)].map(x=>Number(x[1])))].sort((a,b)=>a-b), + assertionCompletionMarker:stdout==='ordinary-input assertions passed\n'}; + fs.writeFileSync(path.join(output,role+'.json'),JSON.stringify(receipt,null,2)+'\n',{flag:'wx'}); + return receipt; +} +export function assessDevelopmentRows(rows) { + const sdk=path.join(repository,'node_modules/typescript'),sdkRows=inventory(sdk); + assert.equal(hash(JSON.stringify(sdkRows)),manifest.compilerRequirement.originClosureSha256); + const producers=producerPaths.map(file=>({file,sha256:hash(fs.readFileSync(path.join(repository,'src',file)))})); + const expected=manifest.specs.flatMap(spec=>Object.keys(spec.states).flatMap(state=>[0,1].map(repeat=>({id:spec.id,state,repeat,tier:spec.qualificationTier})))); + const key=row=>JSON.stringify([row.id,row.state,row.repeat]); + const map=new Map(); + for(const row of rows){if(map.has(key(row)))throw Error('Duplicate development exposure');map.set(key(row),row);} + assert.equal(rows.length,expected.length,'Incomplete development exposure inventory'); + let faults=0,healthyFailures=0,incomplete=0,unsupported=0,nativePassed=0; + const detected=new Set(); + for(const identity of expected){ + const row=map.get(key(identity));assert.ok(row,'Missing development exposure');assert.equal(row.tier,identity.tier); + if(row.native.exitCode===0&&!row.native.error&&!row.native.signal&&row.native.assertionCompletionMarker===true&& + row.native.stdoutSha256===hash('ordinary-input assertions passed\n')&&row.native.stderrSha256===hash(''))nativePassed++; + const spec=manifest.specs.find(item=>item.id===row.id),check=row.typecheck; + const state=spec.states[row.state],binding=check.binding; + assert.ok(binding&&Array.isArray(binding.source?.files)&&Array.isArray(binding.dependencies?.files),'Missing actual input inventory'); + assert.equal(binding.source.sha256,hash(JSON.stringify(binding.source.files)),'Source inventory checksum mismatch'); + assert.equal(binding.dependencies.sha256,hash(JSON.stringify(binding.dependencies.files)),'Dependency inventory checksum mismatch'); + const sourceRows=binding.source.files.filter(item=>item.kind==='file').map(({file,sha256,size})=>({file,sha256,bytes:size})).sort((a,b)=>a.fileb.file?1:0); + assert.deepEqual(sourceRows,state.sourceInputs,'Wrong exposure source binding'); + const dependencyRows=binding.dependencies.files.filter(item=>item.kind==='file').map(({file,sha256,size})=>({file:file.replace(/^node_modules\/typescript\//,''),sha256,bytes:size})); + assert.deepEqual(dependencyRows,sdkRows,'Wrong compiler closure'); + assert.deepEqual(binding.producers,producers,'Producer closure changed'); + assert.equal(binding.node.sha256,hash(fs.readFileSync(process.execPath)),'Wrong actual Node runtime'); + assert.equal(binding.projectSha256,state.sourceInputs.find(input=>input.file==='tsconfig.json').sha256); + assert.equal(check.compiler?.version,manifest.compilerRequirement.version); + assert.equal(check.compiler?.sha256,sdkRows.find(input=>input.file==='lib/typescript.js').sha256); + assert.equal(check.compiler?.manifestSha256,sdkRows.find(input=>input.file==='package.json').sha256); + if(row.tier!=='ordinary-project'){ + assert.equal(check.complete,false);assert.equal(check.status,'incomplete');assert.equal(check.exitCode,2); + assert.equal(check.signal,null);assert.equal(check.errorCode,'typecheck-project-references-unsupported'); + assert.deepEqual(check.reasons,['typecheck-project-references-unsupported']);unsupported++;continue; + } + const original=row.originalChecker; + assert.ok(original&&!original.error&&!original.signal,'Original checker incomplete'); + assert.equal(original.exitCode,spec.states[row.state].expectedOriginalCheckerExit); + assert.deepEqual(original.diagnosticCodes,row.state==='fault'?spec.diagnosticCodes:[]); + if(!summarizeTypecheck(check).complete||![0,1].includes(check.exitCode)){incomplete++;continue;} + if(row.state==='healthy'){if(check.exitCode!==0||check.status!=='passed')healthyFailures++;} + else if(check.status==='failed'&&check.exitCode===1&&spec.diagnosticCodes.every(code=>check.diagnostics?.some(d=>d.category==='error'&&d.code===code))){faults++;detected.add(row.id);} + } + return {qualified:nativePassed===24&&faults===10&&detected.size===5&&healthyFailures===0&&incomplete===0&&unsupported===4, + uniqueSeededDevelopmentDefectsDetected:detected.size,faultExposuresDetected:faults,healthyFalsePositives:healthyFailures, + ordinaryIncomplete:incomplete,unsupportedReferenceExposures:unsupported,nativeProbePasses:nativePassed, + referenceQualified:false,upstreamBugClaim:false,learningImprovementClaim:false,superiorityClaim:false}; +} +export function verifyReplay(file){ + const summary=JSON.parse(fs.readFileSync(file)),output=path.dirname(file); + assert.equal(summary.kind,'observed-development-type-detection-replay');assert.equal(summary.manifestSha256,MANIFEST_SHA); + assert.equal(summary.producerSha256,hash(fs.readFileSync(fileURLToPath(import.meta.url))),'Replay producer changed'); + for(const row of summary.rows){ + const directory=path.join(output,`${row.id}-${row.state}-${row.repeat}`); + for(const [role,receipt]of [['native',row.native],['original-tsc',row.originalChecker]]){ + if(!receipt)continue; + assert.deepEqual(JSON.parse(fs.readFileSync(path.join(directory,role+'.json'))),receipt); + const stdout=fs.readFileSync(path.join(directory,role+'.stdout')),stderr=fs.readFileSync(path.join(directory,role+'.stderr')); + assert.equal(hash(stdout),receipt.stdoutSha256);assert.equal(hash(stderr),receipt.stderrSha256); + if(role==='native')assert.equal(stdout.toString(),'ordinary-input assertions passed\n'); + else assert.deepEqual([...new Set([...stdout.toString().matchAll(/\bTS([0-9]+):/g)].map(x=>Number(x[1])))].sort((a,b)=>a-b),receipt.diagnosticCodes); + } + assert.deepEqual(JSON.parse(fs.readFileSync(path.join(directory,'typecheck.json'))),row.typecheck); + const opening=JSON.parse(fs.readFileSync(path.join(directory,'opening-inputs.json'))); + assert.equal(opening.status,'captured');assert.equal(opening.sourceSha256,row.typecheck.binding.source.sha256); + assert.equal(opening.dependencySha256,row.typecheck.binding.dependencies.sha256); + assert.equal(opening.provenanceFingerprint,row.typecheck.binding.provenance.fingerprint); + } + const result=assessDevelopmentRows(summary.rows);assert.equal(result.qualified,true);return result; +} +export function challengeEvaluator(file){ + const summary=JSON.parse(fs.readFileSync(file));assert.equal(assessDevelopmentRows(summary.rows).qualified,true); + const cases=[ + ['empty-native-output',rows=>{rows[0].native.stdoutSha256=hash('');}], + ['missing-native-marker',rows=>{rows[0].native.assertionCompletionMarker=false;}], + ['zero-type-scope',rows=>{rows[0].typecheck.files=[];}], + ['copied-other-fault',rows=>{const target=rows.find(r=>r.id==='nullable-branch'&&r.state==='fault');target.typecheck=structuredClone(rows.find(r=>r.id==='return-contract'&&r.state==='fault').typecheck);}], + ['wrong-sdk-hash',rows=>{rows[0].typecheck.compiler.sha256='0'.repeat(64);}], + ['reference-timeout',rows=>{rows.find(r=>r.tier!=='ordinary-project').typecheck.errorCode='typecheck-timeout';}], + ['ordinary-incomplete',rows=>{rows[0].typecheck.complete=false;}], + ['healthy-error-hidden',rows=>{rows[0].typecheck.diagnostics.push({code:2322,category:'error',file:'module.ts',line:1,column:1,message:'independent evaluator mutant'});}], + ['duplicate-exposure',rows=>{rows[1]=structuredClone(rows[0]);}], + ['unknown-exposure',rows=>{rows[0].id='unknown';}], + ['missing-exposure',rows=>{rows.pop();}] + ]; + const controls=cases.map(([id,mutate])=>{ + const rows=structuredClone(summary.rows);mutate(rows);let rejected=false; + try{rejected=assessDevelopmentRows(rows).qualified!==true;}catch{rejected=true;} + assert.equal(rejected,true,'Evaluator admitted '+id);return {id,rejected}; + }); + return {kind:'development-evaluator-challenges',qualified:true,controls}; +} +export function main(args=process.argv.slice(2)) { + if(args.length===2&&args[0]==='--verify'){console.log(JSON.stringify(verifyReplay(path.resolve(args[1])),null,2));return 0;} + if(args.length===2&&args[0]==='--challenge-evaluator'){console.log(JSON.stringify(challengeEvaluator(path.resolve(args[1])),null,2));return 0;} + if(args.length!==2||args[0]!=='--output')throw Error('Use --output NEW_DIRECTORY'); + assert.equal(hash(manifestBytes),MANIFEST_SHA,'Frozen public manifest changed'); + const output=path.resolve(args[1]);if(fs.existsSync(output))throw Error('Preserve evidence: output must be new'); + const sdk=path.join(repository,'node_modules/typescript'); + const sdkPackage=JSON.parse(fs.readFileSync(path.join(sdk,'package.json'))); + assert.equal(sdkPackage.name,'typescript');assert.equal(sdkPackage.version,manifest.compilerRequirement.version); + const sdkRows=inventory(sdk),sdkSha=hash(JSON.stringify(sdkRows)); + assert.equal(sdkSha,manifest.compilerRequirement.originClosureSha256,'Original SDK closure differs'); + for(const spec of manifest.specs){ + const healthy=spec.states.healthy.sourceInputs,fault=spec.states.fault.sourceInputs; + assert.equal(healthy.find(x=>x.file==='probe.mjs').sha256,fault.find(x=>x.file==='probe.mjs').sha256); + const differences=healthy.filter(x=>fault.find(y=>y.file===x.file)?.sha256!==x.sha256); + assert.equal(differences.length,1);assert.ok(differences[0].file.endsWith('.ts')); + for(const state of Object.values(spec.states))for(const input of state.sourceInputs){ + const bytes=fs.readFileSync(path.join(base,state.directory,input.file)); + assert.equal(bytes.length,input.bytes);assert.equal(hash(bytes),input.sha256); + } + } + fs.mkdirSync(output,{recursive:true});const started=performance.now(),rows=[],producerSha256=hash(fs.readFileSync(fileURLToPath(import.meta.url))); + for(const spec of manifest.specs)for(const [stateName,state]of Object.entries(spec.states))for(let repeat=0;repeat<2;repeat++){ + const rowStart=performance.now(),scratch=fs.mkdtempSync(path.join(os.tmpdir(),'testlore-type-development-')); + const destination=path.join(output,`${spec.id}-${stateName}-${repeat}`);fs.mkdirSync(destination); + try{ + for(const input of state.sourceInputs){const target=path.join(scratch,input.file);fs.mkdirSync(path.dirname(target),{recursive:true});fs.copyFileSync(path.join(base,state.directory,input.file),target);} + const localSdk=path.join(scratch,'node_modules/typescript');fs.mkdirSync(path.dirname(localSdk),{recursive:true});fs.cpSync(sdk,localSdk,{recursive:true}); + assert.equal(hash(JSON.stringify(inventory(localSdk))),sdkSha); + const openingInputs=captureTypecheckInputs(scratch,{timeoutMs:30000}); + fs.writeFileSync(path.join(destination,'opening-inputs.json'),JSON.stringify(openingInputs,null,2)+'\n',{flag:'wx'}); + const native=execute(['--experimental-strip-types','--no-warnings','probe.mjs'],scratch,destination,'native'); + const typecheck=verifyTypecheck(scratch,{timeoutMs:30000,openingInputs}); + fs.writeFileSync(path.join(destination,'typecheck.json'),JSON.stringify(typecheck,null,2)+'\n',{flag:'wx'}); + // The reference build's original cache-sensitive exit mismatch remains recorded in the manifest. + // Never run an emitting build in the typecheck arm or score its unsupported result as a win. + const originalChecker=spec.qualificationTier==='ordinary-project'? + execute(['node_modules/typescript/lib/tsc.js',...state.originalCheckerArgs],scratch,destination,'original-tsc'):null; + for(const input of state.sourceInputs)assert.equal(hash(fs.readFileSync(path.join(scratch,input.file))),input.sha256,'Source drift'); + assert.equal(hash(JSON.stringify(inventory(localSdk))),sdkSha,'SDK drift'); + rows.push({id:spec.id,state:stateName,repeat,tier:spec.qualificationTier,native,typecheck,originalChecker, + stagingNativeTypecheckOriginalAndEvidenceMs:performance.now()-rowStart}); + }finally{fs.rmSync(scratch,{recursive:true,force:true});} + } + assert.equal(hash(JSON.stringify(inventory(sdk))),sdkSha,'Installed SDK drift'); + assert.equal(hash(fs.readFileSync(path.join(base,'manifest.json'))),MANIFEST_SHA); + assert.equal(hash(fs.readFileSync(fileURLToPath(import.meta.url))),producerSha256,'Producer drift'); + const result={schemaVersion:1,kind:'observed-development-type-detection-replay',manifestSha256:MANIFEST_SHA, + originManifestSha256:manifest.originManifestSha256,producerSha256, + node:process.version,sdk:{version:sdkPackage.version,closureSha256:sdkSha,files:sdkRows.length}, + ...assessDevelopmentRows(rows),rows,totalControllerBeforeFinalReceiptMs:performance.now()-started, + limitations:manifest.limits,protectedHoldout:false,cost:null}; + fs.writeFileSync(path.join(output,'summary.json'),JSON.stringify(result,null,2)+'\n',{flag:'wx'}); + console.log(JSON.stringify({...assessDevelopmentRows(rows),output,totalControllerMs:result.totalControllerBeforeFinalReceiptMs},null,2)); + return result.qualified?0:1; +} +if(process.argv[1]&&path.resolve(process.argv[1])===fileURLToPath(import.meta.url)){ + try{process.exitCode=main();}catch(error){console.error(error.message);process.exitCode=1;} +} diff --git a/src/behavioral-contracts.js b/src/behavioral-contracts.js index a7997f4..3f1e87b 100644 --- a/src/behavioral-contracts.js +++ b/src/behavioral-contracts.js @@ -7,7 +7,7 @@ import {captureRepairChallenges,runRepairChallenges} from './repair-challenges.j /** Explicit repository oracles supplement native tests. They never authorize * omissions, prove authorship, or establish universal defect coverage. */ -export function verifyBehavioralContracts(root,{timeoutMs=20000,totalTimeoutMs=120000,expectedProvenance}={}) { +export function verifyBehavioralContracts(root,{timeoutMs=20000,totalTimeoutMs=120000,expectedProvenance,requestedRepair}={}) { root=path.resolve(root);const started=performance.now(); if(!Number.isSafeInteger(timeoutMs)||timeoutMs<1||timeoutMs>120000||!Number.isSafeInteger(totalTimeoutMs)||totalTimeoutMs<1||totalTimeoutMs>120000)throw Error('behavioral-contract-budget-invalid'); const deadline=started+totalTimeoutMs; @@ -18,7 +18,9 @@ export function verifyBehavioralContracts(root,{timeoutMs=20000,totalTimeoutMs=1 nextAction:'Inspect incomplete contract evidence; preserve the independent assertions and rerun verification.'}; const finish=()=>({...report,durationMs:Math.round(performance.now()-started)}); try { - const config=readConfig(root),definition=captureRepairChallenges(root,config); + const config=readConfig(root); + if(requestedRepair!==undefined&&digest(config.repair)!==digest(requestedRepair))throw Error('behavioral-contract-requested-policy-drift'); + const definition=captureRepairChallenges(root,config); if(definition.status==='not-configured')return {...finish(),status:'not-configured',exitCode:2,reasons:['behavioral-contracts-not-configured'],nextAction:'Commit independently maintained behavioral challenges and configure repair.challenges and repair.sourcePaths. No additional behavioral check ran.'}; const sourcePaths=config.repair?.sourcePaths; if(!Array.isArray(sourcePaths)||!sourcePaths.length||sourcePaths.length>32||new Set(sourcePaths).size!==sourcePaths.length||sourcePaths.some(file=>!isRepairSourcePath(file)))throw Error('behavioral-contract-source-scope-invalid'); @@ -49,10 +51,10 @@ export function verifyBehavioralContracts(root,{timeoutMs=20000,totalTimeoutMs=1 /** Native cases/history retain their meaning; the overall exit includes the * additional dimension. Missing configuration remains an explicit coverage gap. */ -export function supplementWithBehavioralContracts(root,report) { +export function supplementWithBehavioralContracts(root,report,{requestedRepair}={}) { const config=readConfig(root); - if(config.repair?.challenges===undefined)return report; - const contracts=verifyBehavioralContracts(root,{expectedProvenance:report.provenance||report.plan?.provenance}); + if((requestedRepair||config.repair)?.challenges===undefined)return report; + const contracts=verifyBehavioralContracts(root,{expectedProvenance:report.provenance||report.plan?.provenance,requestedRepair}); return {...report,nativeExitCode:report.exitCode,nativeComplete:report.complete,contracts, exitCode:contracts.exitCode===2||report.exitCode===2?2:contracts.exitCode===1?1:report.exitCode, ...(report.timings?{timings:{...report.timings,contractsMs:contracts.durationMs,totalMs:report.timings.totalMs+contracts.durationMs}}:{})}; diff --git a/src/cli.js b/src/cli.js index 6933c44..dfca908 100755 --- a/src/cli.js +++ b/src/cli.js @@ -12,6 +12,7 @@ const commandModules = { setup: ['quality-layer', 'agent-profile', 'files', 'adoption-readiness', 'plugin-recommendations', 'plugins', 'run-report'], report: ['files', 'run-report'], mappings: ['routing-proposals'], contracts: ['behavioral-contracts'], + typecheck: ['typecheck'], 'mapping-qualify': ['files', 'mapping-qualification'], 'browser-build': ['files', 'browser-evidence'], 'browser-instrument': ['browser-evidence'], 'browser-capture': ['browser-evidence'], 'browser-mappings': ['files', 'browser-evidence'], @@ -71,6 +72,7 @@ Usage: testlore [options] plan Explain which tests a Git change can affect run Execute the planned tests and preserve the runner's exit code contracts Execute explicitly configured independent behavioral challenges + typecheck Run the project's installed TypeScript checker without emitting files modules Propose test groups and flag broad dependencies generate Produce an agent work order; --execute stages reviewed candidates snapshot Save source/runner/environment provenance before measurement @@ -133,7 +135,7 @@ need explicit declarations. No AI account required for audit, plan, or run. export function parseArgs(args) { const options = {}; let command = 'help'; - const values = new Set(['root','base','changed','output','report','type','provenance','repeat','id','patch','target','framework','head','action-ref','base-branch','query','name','enable','disable','select','plugin','settings','manifest','mutate','defects','revision','deadline-ms','trusted-key','checkpoint','claim','verdict','reviewer','sources','provider','sha256','contract','exclude-repositories']); + const values = new Set(['root','base','changed','output','report','type','provenance','repeat','id','patch','target','framework','head','action-ref','base-branch','query','name','enable','disable','select','plugin','settings','manifest','mutate','defects','revision','deadline-ms','trusted-key','checkpoint','claim','verdict','reviewer','sources','provider','sha256','contract','exclude-repositories','project']); const flags = new Set(['json', 'full', 'shadow', 'execute', 'help', 'version', 'local', 'no-ci','check','recommend','auto','selective','allow-execution','unified-native','verify','autofix','immunity']); if (args[0] && !args[0].startsWith('-')) command = args.shift(); for (let i = 0; i < args.length; i++) { @@ -200,6 +202,10 @@ function init(root, {safePath, defaultNativeAdapter, defaultNativeRunner, audit} return { config: file, created, report, next: report.testFiles ? ['tddswarm plan --base HEAD', 'tddswarm run --shadow'] : ['tddswarm generate', 'Add tddswarm.requirements.md and configure an agent for generation.'] }; } function human(command, result, implementations = {}) { + if(command==='typecheck'){ + const location=value=>String(value??'').replace(/[\x00-\x1f\x7f]/g,' ').slice(0,512); + return `Type checking: ${result.status}; ${result.files.length} declared source files.\n${result.diagnostics.slice(0,10).map(row=>`TS${row.code}: ${location(row.file||'configuration')}${row.line?':'+row.line:''}${row.column?':'+row.column:''}`).join('\n')}\n${result.nextAction}\nDeclared compiler scope only; no routing omissions or universal correctness authority.`; + } if(command==='contracts')return `Behavioral challenges: ${result.status}; ${result.runs.length} observed.\n${result.runs.filter(row=>!row.passed).map(row=>`Did not pass: ${row.id} (${row.script})`).join('\n')}\n${result.nextAction}\nReceipt: ${result.receipt}`; if(command==='mappings') { const diagnostics=result.processRegistrations,rows=[...(diagnostics?.proposals||[])].sort((a,b)=>Number(b.launches.length>0)-Number(a.launches.length>0)||a.fixture.localeCompare(b.fixture)); @@ -235,6 +241,7 @@ function human(command, result, implementations = {}) { export async function main(args = process.argv.slice(2)) { const { command, options } = parseArgs([...args]); if (options.version) { console.log(JSON.parse(fs.readFileSync(new URL('../package.json',import.meta.url),'utf8')).version); return 0; } + if(options.project!==undefined&&command!=='typecheck')throw Error('--project applies only to typecheck'); if (command === 'help' || options.help) { console.log(help); return 0; } validateLoopOptions(command, options); const investigationOptions={'probe-catalog':['output','execute'],'probe-qualify':['manifest','execute'],investigate:['manifest','execute'],immunity:['manifest','execute'],'investigation-outcome':['report','sha256','id','verdict','reviewer'],'investigation-links':['report','sha256'],'investigation-learn':[],'investigation-repair':['report','sha256','id','local','patch','deadline-ms','base-branch']}; @@ -281,6 +288,10 @@ export async function main(args = process.argv.slice(2)) { pilot, exportPilot, verificationBrief, qualifyRoutingMappings, adoptionReadiness, importAssessments, assessCommunityCampaignFile} = implementations; let result; switch (command) { + case 'typecheck': { + if(Object.keys(options).some(key=>!['root','json','project'].includes(key)))throw Error('typecheck accepts only --root, --json and --project'); + result=implementations.verifyTypecheck(root,{project:options.project||'tsconfig.json'});break; + } case 'contracts': { if(Object.keys(options).some(key=>!['root','json'].includes(key)))throw Error('contracts accepts only --root and --json'); result=implementations.saveBehavioralContractReport(root,implementations.verifyBehavioralContracts(root));break; @@ -508,6 +519,7 @@ export async function main(args = process.argv.slice(2)) { console.log(options.json ? JSON.stringify(result, null, 2) : human(command, result, implementations)); if(command==='knowledge-import')return result.imported?0:2; if(command==='contracts')return result.exitCode; + if(command==='typecheck')return result.exitCode; if(command==='probe-qualify')return result.qualified?0:2; if(['investigate','immunity'].includes(command))return result.exitCode??0; if(command==='investigation-outcome')return result.recorded?0:2; diff --git a/src/index.js b/src/index.js index cd6b347..23d7088 100644 --- a/src/index.js +++ b/src/index.js @@ -4,6 +4,7 @@ export { audit, modules, inspectTest } from './audit.js'; export { workOrder, generate } from './swarm.js'; export { run, runUnifiedNative } from './runner.js'; export {verifyBehavioralContracts} from './behavioral-contracts.js'; +export {verifyTypecheck} from './typecheck.js'; export { discover, execute, resolveNative, adapterFor } from './execution.js'; export { snapshot, freshness, runnerIdentity } from './provenance.js'; export { ingestQuality, qualityEvidence, measureStability, captureRuntime, ingestRuntime } from './evidence.js'; diff --git a/src/plugin-config.js b/src/plugin-config.js index 69fed2c..8582a20 100644 --- a/src/plugin-config.js +++ b/src/plugin-config.js @@ -1,5 +1,6 @@ // Pure configuration resolution: listing/configuration never loads plugin code. export const BUILTIN_PLUGINS = Object.freeze([ + { id: 'typescript', kind: 'native-check', capabilities: ['fresh-project-type-verification'], limitation: 'Declared compiler scope only; no routing omission or universal correctness authority.' }, { id: 'agentic-qe', kind: 'generation', capabilities: ['direct-unreviewed-candidate-generation'], limitation: 'AQE is not the architect/author/reviewer worker protocol.' }, { id: 'pytest-testmon', kind: 'native-backend', capabilities: ['native-selection', 'native-execution'] }, { id: 'nx', kind: 'native-backend', capabilities: ['native-selection', 'native-execution'] }, @@ -37,7 +38,11 @@ function validateEnabled(id, entry) { if (entry.kind !== 'worker' || entry.protocolVersion !== 1) throw new Error(`Unknown enabled plugin ${id}; register kind worker with protocolVersion 1`); argv(entry.command, `${id}.command`); return 'worker'; } - if (builtin.kind === 'native-backend') { + if (id === 'typescript') { + keys(entry, ['enabled','project','timeoutMs'],id); + if(entry.project!==undefined&&(typeof entry.project!=='string'||entry.project.length>512||!entry.project.endsWith('.json')||entry.project.includes('\\')||/[\x00-\x1f\x7f]/.test(entry.project)||entry.project.split('/').some(part=>!part||part==='.'||part==='..'||part.startsWith('.'))))throw Error('typescript.project must be a canonical project-relative JSON path'); + if(entry.timeoutMs!==undefined)integer(entry.timeoutMs,'typescript.timeoutMs',100,120000); + } else if (builtin.kind === 'native-backend') { keys(entry, ['enabled', 'executable', 'args', 'options', 'queryOptions', 'targets', 'fileLabels', 'timeoutMs'], id); if (entry.executable !== undefined && (typeof entry.executable !== 'string' || !entry.executable || entry.executable.startsWith('-'))) throw new Error(`${id}.executable must be a program path`); for (const key of ['args', 'options', 'queryOptions']) if (entry[key] !== undefined) argv(entry[key], `${id}.${key}`, { empty: true }); diff --git a/src/plugin-recommendations.js b/src/plugin-recommendations.js index f05ed53..3206e0f 100644 --- a/src/plugin-recommendations.js +++ b/src/plugin-recommendations.js @@ -89,6 +89,7 @@ export function recommendPlugins(projectRoot) { } function nodeTool(name, bin) { return Boolean(packageInstalled(name)) && executable(`node_modules/.bin/${bin}`, name); } const installed = { + typescript: nodeTool('typescript','tsc'), nx: nodeTool('nx', 'nx'), c8: nodeTool('c8', 'c8'), stryker: nodeTool('@stryker-mutator/core', 'stryker'), 'agentic-qe': nodeTool('agentic-qe', 'aqe'), ruvector: false, bazel: false, 'pytest-testmon': false }; @@ -143,8 +144,10 @@ export function recommendPlugins(projectRoot) { } catch { evidence.push({ path: configured[id].report, state: 'unsupported-assessment' }); warnings.push(`Configured ${id} report is unavailable or unsupported; live evaluation is never invoked.`); } } const nodeProject = own(pkg, 'name') || Object.keys(deps).length > 0 || ['node','jest','vitest'].includes(config.adapter); - const fit = { nx: nx !== null || own(deps, 'nx'), bazel: bazelFiles.length > 0, 'pytest-testmon': python.length > 0, c8: nodeProject && own(deps, 'c8'), stryker: nodeProject && own(deps, '@stryker-mutator/core'), ruvector: learningReady && config.learning?.enabled !== false, 'agentic-qe': nodeProject || python.length > 0, guardana: Boolean(assessmentFiles.guardana), ifixai: Boolean(assessmentFiles.ifixai) }; + const typeProject=read('tsconfig.json',{max:128*1024}); + const fit = { typescript:typeProject!==null, nx: nx !== null || own(deps, 'nx'), bazel: bazelFiles.length > 0, 'pytest-testmon': python.length > 0, c8: nodeProject && own(deps, 'c8'), stryker: nodeProject && own(deps, '@stryker-mutator/core'), ruvector: learningReady && config.learning?.enabled !== false, 'agentic-qe': nodeProject || python.length > 0, guardana: Boolean(assessmentFiles.guardana), ifixai: Boolean(assessmentFiles.ifixai) }; const evidenceFor = { + typescript:['tsconfig.json',...(installed.typescript?['node_modules/typescript/package.json','node_modules/.bin/tsc']:[])], nx: [...(nx ? ['nx.json'] : []), ...(own(deps, 'nx') ? ['package.json'] : []), ...(installed.nx ? ['node_modules/.bin/nx'] : [])], bazel: [...bazelFiles, ...(bazelExecutable ? [bazelExecutable] : []), ...(bazelisk ? ['node_modules/@bazel/bazelisk/package.json'] : [])], 'pytest-testmon': [...python.map(item => item.file), ...(pytestExecutable ? [pytestExecutable, testmonMetadata] : [])], @@ -158,7 +161,8 @@ export function recommendPlugins(projectRoot) { const recommendations = BUILTIN_PLUGINS.map(plugin => { const retained = own(configured, plugin.id), ready = installed[plugin.id] && fit[plugin.id]; const reasons = []; - if (fit[plugin.id]) reasons.push(({nx:'Nx workspace or dependency detected.',bazel:'Bazel workspace detected.','pytest-testmon':'Python project configuration detected.',c8:'Declared c8 dependency complements execution with coverage evidence.',stryker:'Declared Stryker dependency complements coverage with mutation evidence.',ruvector:'Intact persisted learning can support indexed recall.','agentic-qe':'Test generation can complement this project when bounded routing prerequisites are satisfied.',guardana:'Configured security assessment report has a supported native format.',ifixai:'Configured agent behavior assessment report has a supported native format.'})[plugin.id]); + if(plugin.id==='typescript'&&fit.typescript)reasons.push('Project tsconfig detected; its installed TypeScript compiler complements runtime tests.'); + if (fit[plugin.id]&&plugin.id!=='typescript') reasons.push(({nx:'Nx workspace or dependency detected.',bazel:'Bazel workspace detected.','pytest-testmon':'Python project configuration detected.',c8:'Declared c8 dependency complements execution with coverage evidence.',stryker:'Declared Stryker dependency complements coverage with mutation evidence.',ruvector:'Intact persisted learning can support indexed recall.','agentic-qe':'Test generation can complement this project when bounded routing prerequisites are satisfied.',guardana:'Configured security assessment report has a supported native format.',ifixai:'Configured agent behavior assessment report has a supported native format.'})[plugin.id]); if (retained) reasons.push('Existing explicit plugin entry is preserved, including disabled settings.'); if (!fit[plugin.id]) reasons.push('No matching project evidence for automatic activation.'); if (!installed[plugin.id]) reasons.push(plugin.kind === 'assessment-import' ? 'No enabled supported local report is configured; upstream installation is not required for import.' : 'Matching project-local installation is missing or cannot be safely inspected.'); @@ -168,14 +172,14 @@ export function recommendPlugins(projectRoot) { if (plugin.id === 'bazel' && bazelisk && !installed.bazel) reasons.push('Bazelisk can download tools; configure an installed native Bazel binary explicitly.'); if (plugin.id === 'pytest-testmon' && !pythonTestmon) reasons.push('No declared pytest-testmon dependency found; local extension metadata is required.'); if (['c8','stryker'].includes(plugin.id)) reasons.push('Enabling report import does not execute measurement or certify test quality.'); - return { id: plugin.id, role: ({ 'native-backend':'execution','measured-import':plugin.id === 'c8' ? 'coverage' : 'mutation','learning-retrieval':'learning','generation':'generation','assessment-import':plugin.id === 'guardana' ? 'security-assessment' : 'behavior-assessment' })[plugin.kind], status: retained ? 'retained' : ready ? 'recommended' : installed[plugin.id] ? 'available' : 'missing', configured: retained, ...(plugin.kind === 'assessment-import' ? { reportAvailable: installed[plugin.id] } : { installed: installed[plugin.id] }), ready: Boolean(ready), projectFit: Boolean(fit[plugin.id]), evidence: evidenceFor[plugin.id], reasons }; + return { id: plugin.id, role: ({ 'native-check':'type-verification','native-backend':'execution','measured-import':plugin.id === 'c8' ? 'coverage' : 'mutation','learning-retrieval':'learning','generation':'generation','assessment-import':plugin.id === 'guardana' ? 'security-assessment' : 'behavior-assessment' })[plugin.kind], status: retained ? 'retained' : ready ? 'recommended' : installed[plugin.id] ? 'available' : 'missing', configured: retained, ...(plugin.kind === 'assessment-import' ? { reportAvailable: installed[plugin.id] } : { installed: installed[plugin.id] }), ready: Boolean(ready), projectFit: Boolean(fit[plugin.id]), evidence: evidenceFor[plugin.id], reasons }; }); for (const id of Object.keys(configured).filter(id => !BUILTIN_PLUGINS.some(plugin => plugin.id === id))) recommendations.push({ id, role: configured[id]?.kind === 'worker' ? 'generation' : 'custom', status: 'retained', configured: true, ready: false, evidence: ['tddswarm.config.json'], reasons: ['Explicit custom plugin is preserved; automatic configuration never adds competing workers.'] }); const explicitCore = own(raw, 'runner') || own(raw, 'adapter'); const existingBackend = config.integration?.type || raw.executionPlugin; const nativeCandidates = ['nx','bazel','pytest-testmon'].filter(id => fit[id]); const needsChoice = !existingBackend && !explicitCore && nativeCandidates.length > 1; - const applicable = recommendations.filter(item => !item.configured && item.ready && ['coverage','mutation','learning'].includes(item.role)).map(item => ({ id: item.id })); + const applicable = recommendations.filter(item => !item.configured && item.ready && ['coverage','mutation','learning','type-verification'].includes(item.role)).map(item => ({ id: item.id })); let selected = existingBackend || (explicitCore ? 'core' : null); if (!selected && !needsChoice && nativeCandidates.length === 1) { const id = nativeCandidates[0], item = recommendations.find(item => item.id === id); diff --git a/src/plugins.js b/src/plugins.js index 3b7b4fc..0da0260 100644 --- a/src/plugins.js +++ b/src/plugins.js @@ -95,6 +95,14 @@ function executablePath(root, executable) { return candidates.find(candidate => { try { fs.accessSync(candidate, process.platform === 'win32' ? fs.constants.F_OK : fs.constants.X_OK); return fs.statSync(candidate).isFile(); } catch { return false; } }) || null; } function probe(root, id, entry) { + if(id==='typescript') { + try { + const manifest=safePath(root,'node_modules/typescript/package.json'),library=safePath(root,'node_modules/typescript/lib/typescript.js'),project=safePath(root,entry.project||'tsconfig.json'); + if(fs.statSync(manifest).size>65536||fs.statSync(library).size>32*1024*1024||!fs.statSync(project).isFile()||fs.statSync(project).size>128*1024)throw Error('Unsupported compiler presence'); + const metadata=JSON.parse(fs.readFileSync(manifest,'utf8'));if(metadata.name!=='typescript'||typeof metadata.version!=='string'||!fs.statSync(library).isFile())throw Error('Missing compiler'); + return {available:true,installed:true,version:metadata.version,probe:'bounded-project-compiler-presence',measured:false,limitation:'Presence only. Run typecheck for actual diagnostics and unsupported-scope detection.'}; + }catch{return {available:false,installed:false,probe:'bounded-project-compiler-presence',measured:false,error:'Project TypeScript compiler or declared tsconfig is missing, unsafe or unbounded.'};} + } if (ASSESSMENT_PLUGINS.includes(id)) { if (!entry.report) return { available: false, probe: 'local-assessment-report', measured: false, error: 'Configure a project-relative report path; this adapter never invokes an upstream executable.' }; try { diff --git a/src/pull-request.js b/src/pull-request.js index 3dbbb95..f157d39 100644 --- a/src/pull-request.js +++ b/src/pull-request.js @@ -6,6 +6,7 @@ import {git,safePath} from './files.js'; import {digest} from './provenance.js'; import {captureRepairChallenges,captureChallengeRuntime,assessRepairChallenges} from './repair-challenges.js'; import {verifyAppliedRepairValidation} from './repair-candidates.js'; +import {summarizeTypecheck} from './typecheck-report.js'; // Advisory reviewer approval and suite-only receipts cannot qualify source // repair publication. Reconcile actual controller observations, then rebind the @@ -19,6 +20,21 @@ function repairPublicationGuard(worktree,result) { const verified=verifyAppliedRepairValidation(worktree,result.candidate?.candidatePath); if(digest(result.validation)!==digest(verified.receipt)) throw new Error('Public repair validation differs from the authenticated local receipt'); + if(verified.manifest.typecheck){ + const final=summarizeTypecheck(result.fullRun?.typecheck); + if(final.status!=='passed'||!final.complete)throw Error('Source repair publication requires completed final TypeScript verification'); + const typed=result.fullRun.typecheck,source=typed.binding?.source?.files; + if(!Array.isArray(source))throw Error('Final TypeScript source binding required'); + for(const [file,value]of Object.entries(verified.manifest.state.scope.files)){ + const expected=verified.manifest.files.find(item=>item.path===file)?.hash??value.hash; + if(source.find(item=>item.file===file&&item.kind==='file')?.sha256!==expected)throw Error('Final TypeScript source differs from validated repair'); + } + if(digest(typed.compiler)!==digest(verified.receipt.typecheck.candidate[0].compiler)|| + digest(typed.binding.node)!==digest(verified.receipt.typecheck.candidate[0].binding.node)|| + digest(typed.binding.producers)!==digest(verified.receipt.typecheck.candidate[0].binding.producers)|| + typed.binding.dependencies.sha256!==verified.receipt.typecheck.candidate[0].binding.dependencies.sha256) + throw Error('Final TypeScript compiler identities differ from validated repair'); + } const expected=result.finalChallenges; const definition=captureRepairChallenges(worktree); if(definition.status!=='configured'||definition.identity!==expected.identity|| diff --git a/src/repair.js b/src/repair.js index bab70bd..4c1d021 100644 --- a/src/repair.js +++ b/src/repair.js @@ -9,6 +9,7 @@ import {stageRepair,validateRepair,applyRepair,isRepairSourcePath,captureRepairS import {proposeRepair} from './repair-swarm.js'; import {withRepairEnvironment} from './repair-environment.js'; import {captureRepairChallenges,runRepairChallenges,captureChallengeRuntime} from './repair-challenges.js'; +import {captureTypecheckInputs,verifyTypecheck} from './typecheck.js'; function changes(root){ const entries=git(root,['status','--porcelain=v1','-z','--untracked-files=all']).split('\0').filter(Boolean),result=[]; @@ -20,7 +21,9 @@ const clean=(root,prefix)=>changes(root).filter(row=>!metadata(row,prefix)); const inputs=root=>digest(Object.fromEntries(listFiles(root).map(file=>[file,digest(fs.readFileSync(safePath(root,file)))]))); export function repairFullSuite(root,{timeoutMs=120000}={}){ const deadline=performance.now()+boundInteger(timeoutMs,120000,120000,'timeoutMs'); - return withRepairEnvironment(readConfig(root),config=>{ + const declared=readConfig(root),settings=declared.plugins?.typescript; + const opening=settings?.enabled===true?captureTypecheckInputs(root,{timeoutMs:Math.max(1,Math.floor(deadline-performance.now()))}):undefined; + const native=withRepairEnvironment(declared,config=>{ const remaining=()=>Math.floor(deadline-performance.now()); if(remaining()<1)return {exitCode:2,complete:false,tests:[],error:'Full native phase deadline exhausted before discovery'}; const discovery=discover(root,{...config,runnerTimeoutMs:remaining(),discovery:Array.isArray(config.discovery)?config.discovery:'native'}); @@ -28,6 +31,13 @@ export function repairFullSuite(root,{timeoutMs=120000}={}){ if(remaining()<1)return {exitCode:2,complete:false,tests:[],discovery,error:'Full native phase deadline exhausted after discovery'}; return {...execute(root,discovery.files,config,{capture:true,timeoutMs:remaining()}),discovery,credentialPolicy:'fresh-native-home-and-masked-auth-environment'}; }); + return settings?.enabled===true?closeRepairTypecheck(root,native,settings,opening,Math.max(1,Math.floor(deadline-performance.now()))):native; +} +function closeRepairTypecheck(root,native,settings,opening,timeoutMs){ + const typecheck=verifyTypecheck(root,{project:settings.project||'tsconfig.json',timeoutMs:Math.min(settings.timeoutMs||30000,timeoutMs),openingInputs:opening}); + return {...native,nativeExitCode:native.nativeExitCode??native.exitCode,nativeComplete:native.nativeComplete??native.complete,typecheck, + complete:(native.nativeComplete??native.complete)===true&&typecheck.complete===true, + exitCode:typecheck.exitCode===2||native.exitCode===2?2:typecheck.exitCode===1?1:native.exitCode}; } const inventory=tests=>tests.map(test=>JSON.stringify([test.file,test.name,test.project||null,test.status])).sort(); function boundInteger(value,fallback,max,label){const number=value??fallback;if(!Number.isSafeInteger(number)||number<1||number>max)throw new Error(`${label} must be an integer from 1 to ${max}`);return number;} @@ -93,7 +103,10 @@ export async function repair(root,options={}){ branchContributionBinding=contributionState(worktree).binding; result.contributionKnowledge={advisoryOnly:true,callerIndex:originalContribution.binding,branchIndex:branchContributionBinding,branchCopied:originalContribution.bytes!==null};assertContributionFresh(); } - const dependencies=path.join(root,'node_modules');if(fs.existsSync(dependencies)&&!fs.existsSync(path.join(worktree,'node_modules')))fs.symlinkSync(fs.realpathSync(dependencies),path.join(worktree,'node_modules'),'dir'); + const dependencies=path.join(root,'node_modules');if(fs.existsSync(dependencies)&&!fs.existsSync(path.join(worktree,'node_modules'))){ + if(config.plugins?.typescript?.enabled===true)fs.cpSync(fs.realpathSync(dependencies),path.join(worktree,'node_modules'),{recursive:true,verbatimSymlinks:true}); + else fs.symlinkSync(fs.realpathSync(dependencies),path.join(worktree,'node_modules'),'dir'); + } if(!freshness(before,snapshot(worktree,readConfig(worktree))).fresh)throw new Error('Repair branch inputs differ from caller inputs'); const branchState=captureRepairState(worktree,{deadline:performance.now()+remaining()}); if(branchState.scope.fingerprint!==originalState.scope.fingerprint||branchState.dependencies.fingerprint!==originalState.dependencies.fingerprint||digest(branchState.nativeRuntime)!==digest(originalState.nativeRuntime))throw new Error('Repair branch does not contain the caller’s exact runtime inputs, including ignored files'); @@ -122,6 +135,7 @@ export async function repair(root,options={}){ if(!result.validation.accepted||result.validation.challenges?.accepted!==true){result.status=result.validation.reasons.some(reason=>reason==='independent-challenge-candidate-regression')?'regression-challenge-rejected':'validation-rejected';result.next=result.validation.challenges?.next??['Inspect retained native validation reasons.'];return receipt();} result.application=applyRepair(worktree,staged.candidatePath); const tested=snapshot(worktree,readConfig(worktree)),testedInputs=inputs(worktreeRoot),testedState=captureRepairState(worktree,{deadline:performance.now()+remaining()}); + const finalTypecheckOpening=config.plugins?.typescript?.enabled===true?captureTypecheckInputs(worktree,{timeoutMs:Math.min(timeoutMs,remaining())}):undefined; result.fullRun=repairFullSuite(worktree,{timeoutMs:Math.min(timeoutMs,remaining())});assertContributionFresh(); const final=result.fullRun,expected=result.validation.candidate?.tests; if(!final.complete||final.exitCode!==0||!final.tests.some(test=>test.status==='passed')||!expected||JSON.stringify(inventory(expected))!==JSON.stringify(inventory(final.tests))){result.status='full-run-failed';result.error='Final native execution must preserve the validated case identities and pass/skip outcomes';result.next=['Inspect final native evidence for missing, failed or newly skipped assertions.','Resolve unstable outcomes before starting another bounded repair round.'];return receipt();} @@ -129,6 +143,10 @@ export async function repair(root,options={}){ const current=captureRepairState(worktree,{deadline});if(current.scope.fingerprint!==testedState.scope.fingerprint||current.dependencies.fingerprint!==testedState.dependencies.fingerprint||digest(current.nativeRuntime)!==digest(testedState.nativeRuntime))throw Error('repair-final-challenge-input-drift');assertContributionFresh(); }}); if(!result.finalChallenges.accepted){result.status='regression-challenge-rejected';result.next=['Inspect the retained final challenge failure; no repair commit or PR was produced.'];return receipt();} + if(config.plugins?.typescript?.enabled===true){ + result.fullRun=closeRepairTypecheck(worktree,result.fullRun,config.plugins.typescript,finalTypecheckOpening,Math.min(timeoutMs,remaining())); + if(!result.fullRun.complete||result.fullRun.exitCode!==0){result.status='final-typecheck-rejected';result.next=['Resolve the final compiler diagnostic or input drift; no repair commit or PR was produced.'];return receipt();} + } const afterRun=captureRepairState(worktree,{deadline:performance.now()+remaining()}),callerState=captureRepairState(root,{deadline:performance.now()+remaining()}); if(inputs(worktreeRoot)!==testedInputs||!freshness(tested,snapshot(worktree,readConfig(worktree))).fresh||afterRun.scope.fingerprint!==testedState.scope.fingerprint||afterRun.dependencies.fingerprint!==testedState.dependencies.fingerprint||digest(afterRun.nativeRuntime)!==digest(testedState.nativeRuntime))throw new Error('Repository or dependencies changed during final repair validation'); if(git(repository,['rev-parse','HEAD']).trim()!==base||clean(repository,prefix).length||!freshness(before,snapshot(root,readConfig(root))).fresh||callerState.scope.fingerprint!==originalState.scope.fingerprint||callerState.dependencies.fingerprint!==originalState.dependencies.fingerprint||digest(callerState.nativeRuntime)!==digest(originalState.nativeRuntime))throw new Error('Original checkout changed during repair'); diff --git a/src/run-report.js b/src/run-report.js index 530392e..4d8c108 100644 --- a/src/run-report.js +++ b/src/run-report.js @@ -1,4 +1,5 @@ // Render bounded, escaped decision evidence; test output is retained only in JSON. +import {summarizeTypecheck} from './typecheck-report.js'; const escape = value => String(value ?? '').replace(/[\\`*_{}[\]<>|]/g, '\\$&').replace(/[\r\n]+/g, ' ').slice(0, 1000); const array = value => Array.isArray(value) ? value : []; const object = value => value && typeof value === 'object' && !Array.isArray(value) ? value : {}; @@ -103,8 +104,11 @@ export function summarizeFirstRun(input = {}) { const contractPassed=contractShape&&contracts.status==='passed'&&contracts.complete===true&&contracts.exitCode===0&&Array.isArray(contracts.reasons)&&contracts.reasons.length===0&&contractRows.every(row=>row.passed); const contractFailed=contractShape&&contracts.status==='failed'&&contracts.complete===true&&contracts.exitCode===1&&Array.isArray(contracts.reasons)&&contracts.reasons.length>0&&contracts.reasons.every(reason=>typeof reason==='string'&&contractRows.some(row=>!row.passed&&reason===`repair-challenge-failed:${row.id}`))&&contractRows.some(row=>!row.passed); const contractIncomplete=contractsRequested&&!contractPassed&&!contractFailed; - const verdict=incomplete||contractIncomplete?'incomplete':omittedFailures.length?'omitted-failures-observed':(report.exitCode!==0||failures.length||contractFailed)?'failed':delegated?'native-result-reported':'passed-in-observed-scope'; - const nextAction=contractIncomplete?'Inspect incomplete behavioral contract evidence, preserve its independent assertions, and rerun verification. Native case outcomes remain separate.': + const typecheck=summarizeTypecheck(report.typecheck,{expectedProvenance:report.provenance||report.plan?.provenance}),typeIncomplete=typecheck.requested&&!typecheck.complete,typeFailed=typecheck.status==='failed'; + const verdict=incomplete||contractIncomplete||typeIncomplete?'incomplete':omittedFailures.length?'omitted-failures-observed':(report.exitCode!==0||failures.length||contractFailed||typeFailed)?'failed':delegated?'native-result-reported':'passed-in-observed-scope'; + const nextAction=typeIncomplete?'Inspect incomplete TypeScript scope or compiler evidence and rerun typecheck. Native case outcomes remain separate.': + typeFailed?'Inspect the type diagnostic codes and locations, repair the declared contract, and rerun both type checking and native tests.': + contractIncomplete?'Inspect incomplete behavioral contract evidence, preserve its independent assertions, and rerun verification. Native case outcomes remain separate.': contractFailed?'Inspect the named behavioral challenge failures, even when native tests passed. Repair independently established defects and preserve the challenge assertions.': verdict==='incomplete'&&report.executed!==true&&!delegated&&declaredTotal===0&&selection.discovery?.complete!==false? 'No native test files were discovered. Run testlore doctor --json to confirm your runner; if the project has no tests, add independent behavioral requirements in tddswarm.requirements.md before requesting generation.': @@ -123,7 +127,7 @@ export function summarizeFirstRun(input = {}) { const fallback=fallbackCauses(selection); const timing=object(report.timings),validTiming=value=>typeof value==='number'&&Number.isFinite(value)&&value>=0?value:null; return { - schemaVersion:1,advisory:true,verdict,complete:!incomplete&&!contractIncomplete,delegated, + schemaVersion:1,advisory:true,verdict,complete:!incomplete&&!contractIncomplete&&!typeIncomplete,delegated,typecheck, behavioralContracts:{requested:contractsRequested,status:contractsRequested?contractPassed?'passed':contractFailed?'failed':'incomplete':'not-configured', nativeExitCode:contractsRequested?(Number.isInteger(report.nativeExitCode)?report.nativeExitCode:null):report.exitCode, observed:array(contracts.runs).length,failed:array(contracts.runs).filter(row=>row?.passed===false).slice(0,32).map(row=>({id:label(row.id),script:label(row.script)})), @@ -171,6 +175,9 @@ export function renderFirstRunSummary(report = {}) { text.push('',`Additional behavioral challenges: ${escape(behavioral.status)}; ${behavioral.observed} observed. ${escape(behavioral.scope)}`); if(behavioral.requested)text.push(`Native runner exit: ${behavioral.nativeExitCode??'unknown'}. Overall exit: ${Number.isInteger(report.exitCode)?report.exitCode:'unknown'}. Contract span: ${behavioral.durationMs===null?'unknown':behavioral.durationMs+' ms'}.`); for(const failure of behavioral.failed)text.push(`- Behavioral check did not pass: ${escape(failure.id)} (${escape(failure.script)}). Preserve its assertions; inspect the JSON receipt.`); + const typecheck=summary.typecheck; + text.push('',`TypeScript checking: ${escape(typecheck.status)}; ${typecheck.implementationCount??'unknown'} checked implementation inputs. ${escape(typecheck.scope)}`); + if(typecheck.requested){text.push(`Native runner exit: ${Number.isInteger(report.nativeExitCode)?report.nativeExitCode:'unknown'}. Overall exit: ${Number.isInteger(report.exitCode)?report.exitCode:'unknown'}. Compiler: ${escape(typecheck.compilerVersion||'unavailable')}. Suppression directives: ${typecheck.suppressionCount??'unknown'}.`);for(const row of typecheck.diagnostics)text.push(`- TS${row.code}: ${escape(row.file||'configuration')}${row.line?':'+row.line:''}${row.column?':'+row.column:''}.`);} if(summary.failedCasesTruncated)text.push(`Showing ${summary.failedCases.length}/${summary.failedCaseCount} failures; remaining identities are in the JSON receipt.`); if(summary.omittedFailureCount)text.push('',`Shadow detected ${summary.omittedFailureCount} failing case records that the proposed selection would omit. Keep shadow mode enabled.`,''); for(const failure of summary.omittedFailures)text.push(`- Proposed omission failure: ${escape(failure.file||'file unavailable')} — ${escape(failure.name||'name unavailable')} (identity: ${escape(failure.id||'unavailable')}).`); @@ -215,7 +222,7 @@ export function renderRunReport(input = {}) { if(report.error)rows.push('',`Execution issue: ${escape(report.error)}`); rows.push('','Omitted targets and their dependency decisions are owned by the native engine; inspect the JSON command and native report. This report makes no individual-case safety claim.','',`Next action: ${firstSummary.nextAction}`);return renderFirstRunSummary(report)+'\n'+rows.join('\n')+'\n'; } - const nativeSummary=report.contracts===undefined?firstSummary:summarizeFirstRun({...report,exitCode:report.nativeExitCode,contracts:undefined}); + const nativeSummary=report.contracts===undefined&&report.typecheck===undefined?firstSummary:summarizeFirstRun({...report,exitCode:report.nativeExitCode,contracts:undefined,typecheck:undefined}); const unverified=report.actualExecutedFilesUnverified===true||!nativeSummary.complete; const fileEvidence=executionFileEvidence(unverified?{...report,actualExecutedFilesUnverified:true}:report); const ran = new Set(fileEvidence.executedFiles),requested=new Set(fileEvidence.requestedFiles); diff --git a/src/runner.js b/src/runner.js index 8435068..6ba38ac 100644 --- a/src/runner.js +++ b/src/runner.js @@ -1,5 +1,6 @@ import fs from 'node:fs'; import path from 'node:path'; +import {randomUUID} from 'node:crypto'; import { plan, planWithinStartupPhase } from './selector.js'; import { readConfig, safePath } from './files.js'; import { execute, adapterFor } from './execution.js'; @@ -11,7 +12,36 @@ import { externalRun } from './integrations.js'; import { captureVitestWorkspaceRoot } from './native-workspace-root.js'; import { captureNativeVitestCases } from './native-vitest-cases.js'; import {captureReaObservation} from './rea-observation.js'; -import {supplementWithBehavioralContracts} from './behavioral-contracts.js'; +import {supplementWithChecks} from './verification-checks.js'; +import {captureTypecheckInputs} from './typecheck.js'; +const typecheckOpening=Symbol('typecheck-opening-inputs'); +const checkRequest=Symbol('opening-requested-checks'); + +function writeReceipt(root,file,value) { + const target=safePath(root,file); + if(fs.existsSync(target)){const stat=fs.lstatSync(target);if(!stat.isFile()||stat.nlink!==1)throw Error('Receipt must be an unlinked regular file');} + const temporary=safePath(root,file+'.'+randomUUID()+'.tmp');let fd; + try{ + fd=fs.openSync(temporary,fs.constants.O_WRONLY|fs.constants.O_CREAT|fs.constants.O_EXCL|fs.constants.O_NOFOLLOW,0o600); + const stat=fs.fstatSync(fd);if(!stat.isFile()||stat.nlink!==1)throw Error('Receipt must be an unlinked regular file'); + fs.writeFileSync(fd,value);fs.closeSync(fd);fd=undefined; + safePath(root,file);fs.renameSync(temporary,target); + }finally{if(fd!==undefined)fs.closeSync(fd);fs.rmSync(temporary,{force:true});} +} + +function openCheckWindow(root,options,config) { + if(options[checkRequest])return options; + const requested=Object.freeze({typescript:Object.freeze({...config.plugins?.typescript}),repair:structuredClone(config.repair)}); + const opened={...options,[checkRequest]:requested}; + return config.plugins?.typescript?.enabled!==true?opened:{...opened,[typecheckOpening]:captureTypecheckInputs(root,{timeoutMs:config.plugins.typescript.timeoutMs})}; +} +function openingRejected(options) { + const opening=options[typecheckOpening]; + if(!opening||opening.status==='captured')return null; + return {executed:false,complete:false,exitCode:2,nativeExitCode:2,nativeComplete:false, + typecheck:{schemaVersion:1,kind:'native-typecheck',status:'incomplete',complete:false,exitCode:2,files:[],diagnostics:[],reasons:[opening.errorCode||'typecheck-opening-capture-incomplete'],errorCode:opening.errorCode||'typecheck-opening-capture-incomplete'}, + error:'TypeScript input-window capture is incomplete; repair the declared scope before native execution.'}; +} export function compareShadow(selection, execution) { const proposed = new Set(selection.selected); @@ -36,8 +66,13 @@ export function compareSubsetCases(full, subset, files) { } export function run(root,options={}) { - const prepared=prepareRun(root,options); - if(prepared.terminal)return finishTerminal(root,prepared.terminal); + const started=performance.now(),config=readConfig(root); + if(options.shadow&&options.selective)throw Error('Choose shadow or selective execution'); + if(options.signal?.aborted)return declineContracts(config,{exitCode:2,executed:false,complete:false,error:'Native execution cancelled'}); + options=openCheckWindow(root,options,config); + const rejected=openingRejected(options);if(rejected)return rejected; + const prepared=prepareRun(root,options,undefined,started); + if(prepared.terminal)return finishTerminal(root,prepared.terminal,options); return finishRun(prepared,execute(root,prepared.executedTests,prepared.config,prepared.options)); } @@ -47,12 +82,13 @@ export async function runUnifiedNative(root,options={}) { if(options.changed)throw new Error('--changed is diagnostic only. Unified execution requires Git change discovery.'); if(options.shadow&&options.selective)throw new Error('Choose shadow or selective execution'); if(options.signal?.aborted)return declineContracts(config,{exitCode:2,executed:false,complete:false,error:'Unified execution cancelled'}); + options=openCheckWindow(root,options,config);const rejected=openingRejected(options);if(rejected)return rejected; if(config.reaObservation!==undefined)try{captureReaObservation(root,config,config.runner||[]);if(options.selective)throw Error('REA observation is shadow-only; selective execution is not admitted');}catch(error){return declineContracts(config,{exitCode:2,executed:false,complete:false,tests:[],requestedFiles:[],error:error.message,unifiedNative:{prototype:true,used:false,reason:'Explicit REA shadow observation rejected before native startup'}});} // An explicit workspace profile cannot escape rejection through legacy fallback. if(adapterFor(config)==='vitest')try{captureVitestWorkspaceRoot(root,config.runner||[],{nativeCwd:config.nativeCwd});captureNativeVitestCases(root,config,config.runner||[]);}catch(error){return declineContracts(config,{exitCode:2,executed:false,complete:false,tests:[],requestedFiles:[],error:error.message,unifiedNative:{prototype:true,used:false,reason:'Explicit workspace/case profile rejected before native startup'}});} const unsupported=unifiedNativeEligibility(root,config); if(unsupported){if(config.nativeCwd!==undefined)return declineContracts(config,{exitCode:2,executed:false,complete:false,tests:[],requestedFiles:[],error:'Explicit native cwd cannot use legacy fallback: '+unsupported,unifiedNative:{prototype:true,used:false,fallbackReason:unsupported}});const result=run(root,options);return {...result,unifiedNative:{prototype:true,used:false,fallbackReason:unsupported}};} - const initial=snapshot(root,config);let session,prepared,executionAttempted=false; + const initial=snapshot(root,config);let session,prepared,executionAttempted=false,sessionClosed=false; try { session=await openNativeSession(root,config,options,{root,configurationDigest:digest(config),files:Object.keys(initial.files)}); if(session.unsupported)throw new Error('Native command eligibility changed before startup; rerun after inputs stabilize'); @@ -60,19 +96,21 @@ export async function runUnifiedNative(root,options={}) { const startupCheck=freshness(initial,startupCurrent); if(!startupCheck.fresh)throw new Error('Inputs changed during native context startup: '+startupCheck.reasons.join(', ')); prepared=prepareRun(root,options,session.token,started,{root,configurationDigest:digest(config),provenance:startupCurrent}); - if(prepared.terminal)return finishTerminal(root,{...prepared.terminal,unifiedNative:{prototype:true,used:true,contexts:1}}); + if(prepared.terminal){await session.close();sessionClosed=true;return finishTerminal(root,{...prepared.terminal,unifiedNative:{prototype:true,used:true,contexts:1}},options);} executionAttempted=true; const execution=await session.execute(prepared.executedTests); if(!options.capture){if(execution.stdout)process.stdout.write(execution.stdout);if(execution.stderr)process.stderr.write(execution.stderr);} + await session.close();sessionClosed=true; return finishRun(prepared,execution); } catch(error) { if(prepared&&executionAttempted)return finishRun(prepared,{adapter:'vitest',exitCode:2,complete:false,tests:[],collectionFiles:[],requestedFiles:prepared.executedTests,executedFiles:prepared.executedTests,actualExecutedFilesUnverified:true,durationMs:Math.round(performance.now()-started-prepared.planningMs),...session?.failureEvidence?.(),error:error.message,unifiedNative:{prototype:true,used:true,contexts:1,partial:true}}); return declineContracts(config,{exitCode:2,executed:executionAttempted,complete:false,tests:[],requestedFiles:prepared?.executedTests||[],...(prepared?{plan:prepared.selection}:{}),...session?.failureEvidence?.(),...error.nativeFailureEvidence,error:error.message,unifiedNative:{prototype:true,used:true,reason:'Unified context rejected; no automatic replan after configuration execution'}}); - } finally {await session?.close?.();} + } finally {if(!sessionClosed)await session?.close?.();} } function declineContracts(config,report) { - return config.repair?.challenges===undefined?report:{...report,nativeExitCode:report.exitCode,nativeComplete:report.complete, - contracts:{schemaVersion:1,status:'incomplete',complete:false,exitCode:2,runs:[],reasons:['native-startup-rejected-contracts-not-run']}}; + const contracts=config.repair?.challenges===undefined?{}:{contracts:{schemaVersion:1,status:'incomplete',complete:false,exitCode:2,runs:[],reasons:['native-startup-rejected-contracts-not-run']}}; + const typecheck=config.plugins?.typescript?.enabled===true?{typecheck:{schemaVersion:1,status:'incomplete',complete:false,exitCode:2,files:[],diagnostics:[],reasons:['native-startup-rejected-typecheck-not-run']}}:{}; + return !Object.keys(contracts).length&&!Object.keys(typecheck).length?report:{...report,nativeExitCode:report.exitCode,nativeComplete:report.complete,...contracts,...typecheck}; } function prepareRun(root, options = {}, nativeSession, started = performance.now(), startupPhase) { @@ -111,7 +149,7 @@ function finishRun({root,options,config,selection,before,serviceBefore,executedT const directory = safePath(root, '.tddswarm'); fs.mkdirSync(directory, { recursive: true }); const runner = runnerIdentity(root, config); - const historyFile = path.join(directory, `history-${runner}.json`); + const historyFile = safePath(root, `.tddswarm/history-${runner}.json`); let history = { count: 0, failed: [], failedCases: [] }; try { history = JSON.parse(fs.readFileSync(historyFile, 'utf8')); } catch {} if (!Number.isInteger(history.count) || history.count < 0) history.count = 0; @@ -128,26 +166,26 @@ function finishRun({root,options,config,selection,before,serviceBefore,executedT failed: [...new Set([...history.failed.filter(f => !actualFiles.includes(f)), ...failed])].sort(), failedCases: [...history.failedCases.filter(t => !actualFiles.includes(t.file)), ...failedCases], complete: execution.complete, provenance: report.provenance }; - fs.writeFileSync(historyFile, JSON.stringify(next, null, 2)); + writeReceipt(root, `.tddswarm/history-${runner}.json`, JSON.stringify(next, null, 2)); // Compatibility receipt only; selection treats namespaced history as authority. - fs.writeFileSync(path.join(directory, 'history.json'), JSON.stringify(next, null, 2)); + writeReceipt(root, '.tddswarm/history.json', JSON.stringify(next, null, 2)); // Timings cover completed selection/execution/provenance/history work. Final // report sealing cannot include its own write; pilots measure an outer span. report.timings={planningMs,executionMs:execution.durationMs||0,totalMs:Math.round(performance.now()-started),finalReportSealingExcluded:true}; report.timings.otherMs=Math.max(0,report.timings.totalMs-planningMs-report.timings.executionMs); - const combined=supplementWithBehavioralContracts(root,report); + const combined=supplementWithChecks(root,report,{openingInputs:options[typecheckOpening],requested:options[checkRequest]}); combined.summary=summarizeFirstRun(combined); - fs.writeFileSync(path.join(directory, 'last-run.md'), renderRunReport(combined)); - fs.writeFileSync(path.join(directory, 'last-run.json'), JSON.stringify(combined, null, 2)); + writeReceipt(root, '.tddswarm/last-run.md', renderRunReport(combined)); + writeReceipt(root, '.tddswarm/last-run.json', JSON.stringify(combined, null, 2)); return combined; } -function finishTerminal(root,report) { - const combined=supplementWithBehavioralContracts(root,report); - if(combined.contracts){ +function finishTerminal(root,report,options={}) { + const combined=supplementWithChecks(root,report,{openingInputs:options[typecheckOpening],requested:options[checkRequest]}); + if(combined.contracts||combined.typecheck){ const directory=safePath(root,'.tddswarm');fs.mkdirSync(directory,{recursive:true}); combined.summary=summarizeFirstRun(combined); - fs.writeFileSync(path.join(directory,'last-run.md'),renderRunReport(combined)); - fs.writeFileSync(path.join(directory,'last-run.json'),JSON.stringify(combined,null,2)); + writeReceipt(root,'.tddswarm/last-run.md',renderRunReport(combined)); + writeReceipt(root,'.tddswarm/last-run.json',JSON.stringify(combined,null,2)); } return combined; } diff --git a/src/typecheck-report.js b/src/typecheck-report.js new file mode 100644 index 0000000..643c623 --- /dev/null +++ b/src/typecheck-report.js @@ -0,0 +1,31 @@ +const object=value=>value!==null&&typeof value==='object'&&!Array.isArray(value); +const sha=value=>typeof value==='string'&&/^[a-f0-9]{64}$/.test(value); +const canonical=value=>typeof value==='string'&&value.length>0&&value.length<=1024&&!value.includes('\\')&&!/[\x00-\x1f\x7f]/.test(value)&&value.split('/').every(part=>part&&part!=='.'&&part!=='..'); +const paths=value=>Array.isArray(value)&&value.length<=10000&&new Set(value).size===value.length&&value.every(canonical); +/** Display admission only. Retained JSON cannot mint native execution authority. */ +export function summarizeTypecheck(input,{expectedProvenance}={}) { + const requested=input!==undefined,check=object(input)?input:{}; + const diagnostics=Array.isArray(check.diagnostics)?check.diagnostics:[]; + const diagnosticValid=row=>object(row)&&Number.isSafeInteger(row.code)&&row.code>0&&['error','warning','message','suggestion'].includes(row.category)&& + (row.file===null||canonical(row.file))&&(row.line===null||Number.isSafeInteger(row.line)&&row.line>0)&&(row.column===null||Number.isSafeInteger(row.column)&&row.column>0)&&typeof row.message==='string'&&row.message.length<=4096; + const sharedWindow=expectedProvenance===undefined||(object(expectedProvenance)&&sha(expectedProvenance.fingerprint)&&check.binding?.provenance?.fingerprint===expectedProvenance.fingerprint&&check.binding?.provenance?.revision===expectedProvenance.revision); + const shape=sharedWindow&&check.schemaVersion===1&&check.kind==='native-typecheck'&&check.complete===true&&check.signal===null&&check.errorCode===null&& + paths(check.files)&&check.files.length>0&&paths(check.implementationFiles)&&JSON.stringify(check.files)===JSON.stringify(check.implementationFiles)&& + paths(check.programFiles)&&check.files.every(file=>check.programFiles.includes(file))&& + Array.isArray(check.diagnostics)&&diagnostics.length<=10000&&diagnostics.every(diagnosticValid)&&new Set(diagnostics.map(row=>JSON.stringify(row))).size===diagnostics.length&& + typeof check.compiler?.version==='string'&&/^\d+\.\d+\.\d+(?:[-+][a-zA-Z0-9.-]+)?$/.test(check.compiler.version)&&sha(check.compiler.sha256)&&sha(check.compiler.manifestSha256)&& + sha(check.binding?.source?.sha256)&&sha(check.binding?.dependencies?.sha256)&&sha(check.binding?.provenance?.fingerprint)&&sha(check.binding?.node?.sha256)&&sha(check.binding?.projectSha256)&& + Array.isArray(check.reasons)&&check.reasons.length===0&&Number.isFinite(check.durationMs)&&check.durationMs>=0&& + Array.isArray(check.suppressions)&&check.suppressions.length<=10000&&check.suppressions.every(row=>object(row)&&canonical(row.file)&&['ts-ignore','ts-expect-error'].includes(row.directive)&&Number.isSafeInteger(row.line)&&row.line>0&&Number.isSafeInteger(row.column)&&row.column>0); + const errors=diagnostics.filter(row=>row?.category==='error'); + const passed=shape&&check.status==='passed'&&check.exitCode===0&&errors.length===0; + const failed=shape&&check.status==='failed'&&check.exitCode===1&&errors.length>0; + return {requested,status:!requested?'not-configured':passed?'passed':failed?'failed':'incomplete',complete:passed||failed, + implementationCount:paths(check.implementationFiles)?check.implementationFiles.length:null, + programFileCount:paths(check.programFiles)?check.programFiles.length:null,errorCount:errors.length, + diagnostics:errors.filter(diagnosticValid).slice(0,10).map(({code,file,line,column})=>({code,file,line,column})), + suppressionCount:Array.isArray(check.suppressions)?check.suppressions.length:null, + compilerVersion:typeof check.compiler?.version==='string'?check.compiler.version.slice(0,80):null, + limitations:Array.isArray(check.limitations)?check.limitations.filter(item=>typeof item==='string').slice(0,10).map(item=>item.slice(0,512)):[], + scope:'Declared TypeScript implementation scope only; excluded files, suppression directives and owner library-check settings remain coverage gaps.'}; +} diff --git a/src/verification-checks.js b/src/verification-checks.js new file mode 100644 index 0000000..39cf1d9 --- /dev/null +++ b/src/verification-checks.js @@ -0,0 +1,15 @@ +import {readConfig} from './files.js'; +import {supplementWithBehavioralContracts} from './behavioral-contracts.js'; +import {verifyTypecheck} from './typecheck.js'; + +/** Additional native oracles supplement runtime cases. They never narrow the + * native selection, erase failed-case history or grant publication authority. */ +export function supplementWithChecks(root,report,{openingInputs,requested}={}) { + const config=readConfig(root),combined=supplementWithBehavioralContracts(root,report,{requestedRepair:requested?.repair}),settings=requested?.typescript||config.plugins?.typescript; + if(settings?.enabled!==true)return combined; + const typecheck=verifyTypecheck(root,{project:settings.project||'tsconfig.json',timeoutMs:settings.timeoutMs, + expectedProvenance:report.provenance||report.plan?.provenance,openingInputs}); + return {...combined,nativeExitCode:combined.nativeExitCode??report.exitCode,nativeComplete:combined.nativeComplete??report.complete,typecheck, + exitCode:typecheck.exitCode===2||combined.exitCode===2?2:typecheck.exitCode===1?1:combined.exitCode, + ...(combined.timings?{timings:{...combined.timings,typecheckOpeningMs:openingInputs?.durationMs??0,typecheckMs:typecheck.durationMs,totalMs:combined.timings.totalMs+typecheck.durationMs}}:{})}; +} diff --git a/test/packed-evidence.test.js b/test/packed-evidence.test.js index 9ff8451..0125746 100644 --- a/test/packed-evidence.test.js +++ b/test/packed-evidence.test.js @@ -14,6 +14,11 @@ const evidence = {schemaVersion: 1, sha256: archive, version: '0.1.0', exactInpu const oneCommandProof='3'.repeat(64);evidence.oneCommand=oneCommandFixture({archive,manifest,producer:oneCommandProof,revision:identity}); evidence.assessmentImports={verified:true,mode:'installed-cli-synthetic-report-import',liveEvaluation:false,providerCalls:0,archiveSha256:archive,installedManifestSha256:manifest,installedCliSha256:'1'.repeat(64),sourceRevision:identity,proofScriptSha256:proof,outcomes:[{id:'guardana',exitCode:0,outcome:'reported-clean'},{id:'ifixai',exitCode:2,outcome:'incomplete'},{id:'unsupported',exitCode:2,outcome:'incomplete'}],rawEvidence:['guardana','ifixai','unsupported'].flatMap(id=>['input.json','stdout.json','stderr.txt','process.json','receipt.json'].map(suffix=>({file:`${id}.${suffix}`,bytes:3,sha256:source})))}; const context = {proofScriptSha256: proof, sourceManifestSha256: source,installedRepairProofSha256:repairProof,oneCommandProofSha256:oneCommandProof,assessmentImportProofSha256:proof}; +evidence.installedTypecheck={verified:true,productionInstalled:true,setupEnabled:true,providerCalls:0,archiveSha256:archive,installedCliSha256:'1'.repeat(64),compilerVersion:'6.0.3',outcomes:[ + {id:'setup-fault',exitCode:1,nativeExitCode:0,status:'failed',complete:true,implementationCount:1,errorCodes:[2322],rawSha256:source}, + {id:'healthy-shadow',exitCode:0,nativeExitCode:0,status:'passed',complete:true,implementationCount:1,errorCodes:[],rawSha256:source}, + {id:'unsupported-no-check',exitCode:2,nativeExitCode:0,status:'incomplete',complete:false,implementationCount:0,errorCodes:[],rawSha256:source} +]}; test('installed qualification requires explicit evidence for persisted shadow and generated Action identity', () => { assert.equal(packedEvidenceComplete(receipt, evidence, context), true); @@ -23,6 +28,12 @@ test('installed qualification requires explicit evidence for persisted shadow an assert.equal(packedEvidenceComplete({...receipt, qualified: 'true'}, evidence, context), false); for (const field of ['complete', 'detected']) for (const value of [undefined, false, 'true']) assert.equal(packedEvidenceComplete(receipt, {...evidence, nativeShadow: {...evidence.nativeShadow, [field]: value}}, context), false); }); +test('packed admission requires native-green/type-failed, healthy and incomplete installed controls',()=>{ + for(const mutation of [undefined,{...evidence.installedTypecheck,verified:'true'},{...evidence.installedTypecheck,providerCalls:1},{...evidence.installedTypecheck,outcomes:[]}, + {...evidence.installedTypecheck,outcomes:evidence.installedTypecheck.outcomes.map(row=>({...row,nativeExitCode:1}))}, + {...evidence.installedTypecheck,outcomes:evidence.installedTypecheck.outcomes.map(row=>({...row,exitCode:0}))}]) + assert.equal(packedEvidenceComplete(receipt,{...evidence,installedTypecheck:mutation},context),false); +}); test('installed qualification rejects lineage, manifest, fixture scope and proof producer drift', () => { for (const field of ['sha256', 'packedManifestSha256', 'proofScriptSha256', 'actionReference', 'workflowActionReference']) assert.equal(packedEvidenceComplete(receipt, {...evidence, [field]: '0'.repeat(64)}, context), false, field); diff --git a/test/plugin-recommendations.test.js b/test/plugin-recommendations.test.js index f2cbb4b..f734891 100644 --- a/test/plugin-recommendations.test.js +++ b/test/plugin-recommendations.test.js @@ -124,7 +124,7 @@ test('RuVector requires intact nonempty local learning and preserves disabled le }); test('all enabled builtin entries and custom worker remain retained without competing generation', t => { - const plugins = Object.fromEntries(['nx','bazel','pytest-testmon','agentic-qe','c8','stryker','ruvector','guardana','ifixai'].map(id => [id, { enabled: true }])); + const plugins = Object.fromEntries(['typescript','nx','bazel','pytest-testmon','agentic-qe','c8','stryker','ruvector','guardana','ifixai'].map(id => [id, { enabled: true }])); plugins['my-worker'] = { enabled: true, kind: 'worker', protocolVersion: 1, command: ['manual-worker'] }; const root = fixture(t, { 'package.json': { name: 'sample' }, 'tddswarm.config.json': { executionPlugin: 'bazel', plugins, agent: ['manual-worker'] }, 'nx.json': {}, 'MODULE.bazel': 'module(name="sample")' }); const report = recommendPlugins(root); diff --git a/test/plugins.test.js b/test/plugins.test.js index 63a44ab..a435046 100644 --- a/test/plugins.test.js +++ b/test/plugins.test.js @@ -30,7 +30,7 @@ test('enabled native plugin resolves existing integration without mutating raw c }); test('disabled unknown and malformed executable settings remain inert in catalog and checks', t => { const root = fixture(t, { plugins: { 'not-installed': { enabled: false, command: 'invalid', kind: 'future' }, nx: { enabled: false, executable: [] } } }); - const catalog = pluginCatalog(root); assert.equal(catalog.plugins.length, 10); + const catalog = pluginCatalog(root); assert.equal(catalog.plugins.length, 11); assert.ok(catalog.plugins.every(plugin => plugin.measured === false && plugin.availability === 'unchecked')); const check = checkPlugins(root); assert.equal(check.exitCode, 0); assert.ok(check.plugins.every(plugin => plugin.probe === 'skipped-disabled')); @@ -86,7 +86,7 @@ test('custom workers require explicit protocol version and reject ambiguous agen }); test('all builtins can be available while one explicit backend owns execution', t => { const root = fixture(t); - for (const id of ['nx', 'bazel', 'pytest-testmon', 'agentic-qe', 'c8', 'stryker', 'ruvector', 'guardana', 'ifixai']) configurePlugin(root, id, { enabled: true }); + for (const id of ['typescript','nx', 'bazel', 'pytest-testmon', 'agentic-qe', 'c8', 'stryker', 'ruvector', 'guardana', 'ifixai']) configurePlugin(root, id, { enabled: true }); const raw = readRaw(root); assert.equal(raw.executionPlugin, 'nx'); assert.equal(raw.integration, undefined); // Configuration persists opt-ins, not derived adapter settings. assert.equal(readConfig(root).integration.type, 'nx'); diff --git a/test/repair.test.js b/test/repair.test.js index 0a6462a..0bcc608 100644 --- a/test/repair.test.js +++ b/test/repair.test.js @@ -7,6 +7,7 @@ import {fileURLToPath} from 'node:url'; import {repair,repairFullSuite} from '../src/repair.js'; import {fixture,commit,git,twoModules,write} from './helpers.js'; import {challengeFiles} from './repair-challenge-fixtures.js'; +import {publishImprovement} from '../src/pull-request.js'; const requirements='Module a must export the integer 1. Module b must continue exporting 2. Preserve all public behavior and unchanged tests.'; const review={accepted:true,findings:[],oracle:{independent:true,basis:[requirements]}}; function project(t,files={}){const root=fixture(t,{...twoModules,...challengeFiles,'src/a.js':'export const a=9;','tddswarm.requirements.md':requirements,...files,'tddswarm.config.json':{...files['tddswarm.config.json'],repair:{...files['tddswarm.config.json']?.repair,challenges:'quality/challenges.json'}}});commit(root);return root;} @@ -29,6 +30,27 @@ test('source repair validates repeated unchanged assertions and commits an isola assert.equal(git(root,'rev-parse','HEAD').trim(),head);assert.equal(git(root,'status','--porcelain').trim(),'');assert.equal(fs.readFileSync(path.join(root,'src/a.js'),'utf8'),'export const a=9;');assert.equal(fs.readFileSync(path.join(root,'test/a.test.js'),'utf8'),fs.readFileSync(path.join(result.worktree,'test/a.test.js'),'utf8')); assert.equal(git(result.worktreeRoot,'show','--format=','--name-only','HEAD').trim(),'src/a.js'); }); +test('enabled native compiler vetoes a runtime-green repair and qualifies the corrected complete branch',async t=>{ + const root=project(t,{ + '.gitignore':'node_modules/\n.tddswarm/\n', + 'src/a.js':'/** @type {number} */ export const a=9;', + 'test/a.test.js':"import test from 'node:test';import assert from 'node:assert/strict';import {a} from '../src/a.js';test('numeric value',()=>assert.equal(Number(a),1));", + 'tsconfig.json':{compilerOptions:{allowJs:true,checkJs:true,noEmit:true,strict:true,module:'NodeNext',moduleResolution:'NodeNext',target:'ES2022'},include:['src/*.js']}, + 'tddswarm.config.json':{adapter:'node',runner:[process.execPath,'--test','{files}'],plugins:{typescript:{enabled:true,timeoutMs:20000}}} + }); + fs.mkdirSync(path.join(root,'node_modules'));fs.cpSync(path.resolve('node_modules/typescript'),path.join(root,'node_modules/typescript'),{recursive:true}); + const bad=await repair(root,{patch:patch("/** @type {number} */ export const a='1';"),sourcePaths:['src/a.js'],deadlineMs:120000});cleanup(t,root,bad); + assert.equal(bad.status,'validation-rejected',JSON.stringify(bad));assert.equal(bad.sha,null);assert.equal(bad.validation.candidate.exitCode,0); + assert.equal(bad.validation.typecheck.candidate[0].status,'failed');assert.ok(bad.validation.typecheck.candidate[0].diagnostics.some(row=>row.code===2322)); + assert.equal(fs.readFileSync(path.join(root,'src/a.js'),'utf8'),'/** @type {number} */ export const a=9;'); + const good=await repair(root,{patch:patch('/** @type {number} */ export const a=1;'),sourcePaths:['src/a.js'],deadlineMs:120000});cleanup(t,root,good); + assert.equal(good.status,'ready-for-review',JSON.stringify(good));assert.equal(good.fullRun.typecheck.status,'passed');assert.equal(good.validation.typecheck.accepted,true); + assert.equal(fs.lstatSync(path.join(good.worktree,'node_modules')).isSymbolicLink(),false); + const missing=structuredClone(good);delete missing.fullRun.typecheck; + assert.throws(()=>publishImprovement(root,missing),/final TypeScript verification/); + const corrupt=structuredClone(good);corrupt.fullRun.typecheck.compiler.sha256='0'.repeat(64); + assert.throws(()=>publishImprovement(root,corrupt),/compiler identities differ/); +}); test('wrong source repair never commits, and test edits cannot replace a repair',async t=>{ const root=project(t),wrong=await repair(root,{patch:patch('export const a=99;'),sourcePaths:['src/a.js']});cleanup(t,root,wrong);assert.equal(wrong.status,'validation-rejected');assert.equal(wrong.sha,null); const oracle=await repair(root,{patch:{files:[{path:'test/a.test.js',content:"import test from 'node:test';test('a',()=>{});"}],review},sourcePaths:['src/a.js']});cleanup(t,root,oracle);assert.equal(oracle.status,'failed');assert.equal(oracle.sha,null);assert.match(oracle.error,/source|path|scope|file/i); diff --git a/test/typecheck-integration.test.js b/test/typecheck-integration.test.js new file mode 100644 index 0000000..eef937c --- /dev/null +++ b/test/typecheck-integration.test.js @@ -0,0 +1,86 @@ +import test from 'node:test'; +import assert from 'node:assert/strict'; +import fs from 'node:fs'; +import path from 'node:path'; +import {spawnSync} from 'node:child_process'; +import {fixture,commit,write} from './helpers.js'; +import {run,runUnifiedNative} from '../src/runner.js'; +import {verifyTypecheck} from '../src/typecheck.js'; +import {summarizeFirstRun,renderFirstRunSummary,renderRunReport} from '../src/run-report.js'; +import {summarizeTypecheck} from '../src/typecheck-report.js'; + +function project(t,extra={}) { + const root=fixture(t,{'package.json':{type:'module',name:'type-contract'},'.gitignore':'.tddswarm/\nnode_modules/\n', + 'tsconfig.json':{compilerOptions:{strict:true,noEmit:true,target:'ES2022',module:'NodeNext',moduleResolution:'NodeNext',skipLibCheck:true},include:['src/**/*.ts']}, + 'tddswarm.config.json':{adapter:'node',runner:[process.execPath,'--experimental-strip-types','--no-warnings','--test','{files}'],plugins:{typescript:{enabled:true}}}, + 'src/value.ts':'export const value: string = 42;\n', + 'test/runtime.test.mjs':"import test from 'node:test';import assert from 'node:assert/strict';import {value} from '../src/value.ts';test('ordinary runtime value',()=>assert.equal(value,42));",...extra}); + fs.mkdirSync(path.join(root,'node_modules'));fs.cpSync(path.resolve('node_modules/typescript'),path.join(root,'node_modules/typescript'),{recursive:true}); + fs.mkdirSync(path.join(root,'node_modules/.bin'));fs.symlinkSync('../typescript/bin/tsc',path.join(root,'node_modules/.bin/tsc'));commit(root);return root; +} +test('runtime green plus compiler failure blocks overall green and preserves native outcomes/history',t=>{ + const root=project(t),result=run(root,{full:true,capture:true});assert.equal(result.nativeExitCode,0);assert.equal(result.exitCode,1);assert.equal(result.nativeComplete,true);assert.equal(result.typecheck.status,'failed'); + assert.equal(result.tests[0].status,'passed');assert.equal(result.summary.verdict,'failed');assert.equal(result.summary.typecheck.errorCount,1);assert.equal(result.timings.typecheckMs,result.typecheck.durationMs); + assert.deepEqual(JSON.parse(fs.readFileSync(path.join(root,'.tddswarm/history.json'))).failed,[]); + assert.match(renderFirstRunSummary(result),/TS2322: src\/value.ts:1:14/);assert.match(renderRunReport(result),/Native runner exit: 0. Overall exit: 1/); + const saved=JSON.parse(fs.readFileSync(path.join(root,'.tddswarm/last-run.json')));assert.equal(saved.typecheck.exitCode,1);assert.equal(saved.summary.verdict,'failed'); +}); +test('zero native selection and unsupported unified fallback still run enabled type checks',async t=>{ + const root=project(t),empty=run(root,{capture:true});assert.equal(empty.executed,false);assert.equal(empty.exitCode,1);assert.equal(empty.typecheck.status,'failed'); + const fallback=await runUnifiedNative(root,{full:true,capture:true});assert.equal(fallback.exitCode,1);assert.equal(fallback.nativeExitCode,0);assert.equal(fallback.typecheck.status,'failed'); + const controller=new AbortController();controller.abort();const cancelled=await runUnifiedNative(root,{signal:controller.signal});assert.equal(cancelled.exitCode,2);assert.equal(cancelled.typecheck.status,'incomplete'); +}); +test('healthy type evidence cannot hide a native failure, and disabled checks remain inert',t=>{ + const root=project(t,{'src/value.ts':'export const value: number = 99;\n'}),result=run(root,{full:true,capture:true});assert.equal(result.nativeExitCode,1);assert.equal(result.typecheck.exitCode,0);assert.equal(result.exitCode,1); + write(root,'src/value.ts','export const value: string = 42;\n');write(root,'tddswarm.config.json',{adapter:'node',runner:[process.execPath,'--experimental-strip-types','--no-warnings','--test','{files}'],plugins:{typescript:{enabled:false}}}); + const disabled=run(root,{full:true,capture:true});assert.equal(disabled.exitCode,0);assert.equal(disabled.typecheck,undefined); +}); +test('missing scope, suppressed implementations and corrupt config remain incomplete despite native green',t=>{ + const root=project(t,{'src/value.ts':'// @ts-nocheck\nexport const value: string = 42;\n'}),result=run(root,{full:true,capture:true});assert.equal(result.nativeExitCode,0);assert.equal(result.exitCode,2);assert.equal(result.summary.verdict,'incomplete'); + const text=renderRunReport(result);assert.match(text,/TypeScript checking: incomplete/);assert.match(text,/Executed 1 files/); + write(root,'tsconfig.json','{"compilerOptions":{"noCheck":true},"files":["src/value.ts"]}');assert.equal(run(root,{full:true,capture:true}).exitCode,2); +}); +test('one-command setup enables available complementary TypeScript and reports its actual failure',t=>{ + const root=project(t);write(root,'tddswarm.config.json',{adapter:'node',runner:[process.execPath,'--experimental-strip-types','--no-warnings','--test','{files}'],executionMode:'shadow'}); + const native=spawnSync(process.execPath,[path.resolve('src/cli.js'),'setup','--root',root,'--verify','--no-ci','--json'],{encoding:'utf8',timeout:30000,maxBuffer:8*1024*1024});assert.equal(native.status,1,native.stderr); + const result=JSON.parse(native.stdout);assert.equal(result.verification.nativeExitCode,0);assert.equal(result.verification.typecheck.status,'failed');assert.equal(JSON.parse(fs.readFileSync(path.join(root,'tddswarm.config.json'))).plugins.typescript.enabled,true); + const standalone=spawnSync(process.execPath,[path.resolve('src/cli.js'),'typecheck','--root',root,'--json'],{encoding:'utf8',timeout:30000,maxBuffer:8*1024*1024});assert.equal(standalone.status,1);assert.equal(JSON.parse(standalone.stdout).diagnostics[0].code,2322); +}); +test('contradictory persisted type reports cannot create a passing advisory headline or leak raw diagnostic prose',t=>{ + const root=project(t,{'src/value.ts':'export const value: number = 42;\n'}),actual=verifyTypecheck(root),base={executed:true,complete:true,exitCode:0,nativeExitCode:0,adapter:'node',executedTests:['test/runtime.test.mjs'],tests:[{id:'ordinary',name:'ordinary',file:'test/runtime.test.mjs',status:'passed'}],plan:{total:1,selected:['test/runtime.test.mjs'],decisions:[{test:'test/runtime.test.mjs'}]}}; + assert.equal(summarizeFirstRun({...base,typecheck:actual}).verdict,'passed-in-observed-scope'); + for(const mutation of [{signal:'SIGKILL'},{errorCode:'ETIMEDOUT'},{files:[]},{implementationFiles:[]},{compiler:{version:'made-up'}},{reasons:['drift']},{programFiles:[]},{complete:false},{exitCode:1},{diagnostics:[{code:2322,category:'error',file:'src/value.ts',line:1,column:1,message:'SECRET_DO_NOT_DISPLAY'}]}]){ + const corrupt={...structuredClone(actual),...mutation};assert.equal(summarizeFirstRun({...base,typecheck:corrupt}).verdict,'incomplete',JSON.stringify(mutation));assert.doesNotMatch(renderFirstRunSummary({...base,typecheck:corrupt}),/SECRET_DO_NOT_DISPLAY/); + } + assert.equal(summarizeTypecheck(undefined).status,'not-configured');assert.equal(summarizeTypecheck(null).status,'incomplete'); + const wrong=structuredClone(actual);wrong.binding.provenance.fingerprint='0'.repeat(64); + assert.equal(summarizeFirstRun({...base,provenance:actual.binding.provenance,typecheck:wrong}).verdict,'incomplete'); + assert.equal(summarizeFirstRun({...base,provenance:actual.binding.provenance,typecheck:actual}).verdict,'passed-in-observed-scope'); +}); +test('ignored source changed by a passing runtime test cannot mix with fresh compiler evidence',t=>{ + const root=project(t,{ + '.gitignore':'.tddswarm/\nnode_modules/\nignored/\n', + 'tsconfig.json':{compilerOptions:{strict:true,noEmit:true,target:'ES2022',module:'NodeNext',moduleResolution:'NodeNext',allowImportingTsExtensions:true},files:['ignored/subject.ts']}, + 'ignored/subject.ts':'export const value: number = 42;\n', + 'test/runtime.test.mjs':"import test from 'node:test';import assert from 'node:assert/strict';import fs from 'node:fs';import {value} from '../ignored/subject.ts';test('original ignored runtime value',()=>{assert.equal(value,42);fs.writeFileSync('ignored/subject.ts','export const value: number = 99;\\n');});"}); + const result=run(root,{full:true,capture:true});assert.equal(result.nativeExitCode,0);assert.equal(result.tests[0].status,'passed');assert.equal(result.exitCode,2);assert.equal(result.typecheck.status,'incomplete');assert.notEqual(result.summary.verdict,'passed-in-observed-scope'); + const native=spawnSync(process.execPath,['--experimental-strip-types','--no-warnings','--test','test/runtime.test.mjs'],{cwd:root,encoding:'utf8',env:{PATH:process.env.PATH,HOME:root}});assert.equal(native.status,1);assert.match(native.stdout,/# fail 1/); +}); +test('native execution cannot erase the requested checker by disabling ignored configuration',t=>{ + const root=project(t,{ + '.gitignore':'.tddswarm/\nnode_modules/\nignored/\ntddswarm.config.json\n', + 'tsconfig.json':{compilerOptions:{strict:true,noEmit:true,target:'ES2022',module:'NodeNext',moduleResolution:'NodeNext'},files:['ignored/subject.ts']}, + 'ignored/subject.ts':'export const value: number = 42;\n', + 'test/runtime.test.mjs':"import test from 'node:test';import assert from 'node:assert/strict';import fs from 'node:fs';import {value} from '../ignored/subject.ts';test('original value before disabling checker',()=>{assert.equal(value,42);fs.writeFileSync('ignored/subject.ts','export const value: number = 99;\\n');const config=JSON.parse(fs.readFileSync('tddswarm.config.json'));config.plugins.typescript.enabled=false;fs.writeFileSync('tddswarm.config.json',JSON.stringify(config));});"}); + const result=run(root,{full:true,capture:true});assert.equal(result.nativeExitCode,0);assert.equal(result.exitCode,2);assert.equal(result.typecheck.status,'incomplete');assert.notEqual(result.summary.verdict,'passed-in-observed-scope'); +}); +test('post-check report receipts cannot overwrite checked source through symbolic or hard links',t=>{ + const root=project(t,{'src/value.ts':'export const value: number = 42;\n'}); + fs.mkdirSync(path.join(root,'.tddswarm'));const source=path.join(root,'src/value.ts'),original=fs.readFileSync(source); + for(const [receipt,link]of [['last-run.json','symbolic'],['last-run.md','hard'],['history.json','symbolic']]){ + const destination=path.join(root,'.tddswarm',receipt);fs.rmSync(destination,{force:true}); + if(link==='symbolic')fs.symlinkSync('../src/value.ts',destination);else fs.linkSync(source,destination); + assert.throws(()=>run(root,{full:true,capture:true}),/Symlink path is unsupported|Receipt must be an unlinked regular file/); + assert.deepEqual(fs.readFileSync(source),original);fs.rmSync(destination); + } +}); diff --git a/test/typecheck-plugins.test.js b/test/typecheck-plugins.test.js new file mode 100644 index 0000000..e4d0f50 --- /dev/null +++ b/test/typecheck-plugins.test.js @@ -0,0 +1,32 @@ +import test from 'node:test'; +import assert from 'node:assert/strict'; +import fs from 'node:fs'; +import path from 'node:path'; +import {fixture,write} from './helpers.js'; +import {recommendPlugins} from '../src/plugin-recommendations.js'; +import {configurePluginsAutomatically,configurePlugin,checkPlugins} from '../src/plugins.js'; +import {readConfig} from '../src/files.js'; + +function presence(root){ + write(root,'node_modules/typescript/package.json',{name:'typescript',version:'6.0.3'}); + write(root,'node_modules/typescript/lib/typescript.js','throw Error("Static inspection must not import the SDK");'); + write(root,'node_modules/typescript/bin/tsc','#!/usr/bin/env node\nrequire("fs").writeFileSync("FORBIDDEN_EXECUTION","yes");'); + fs.chmodSync(path.join(root,'node_modules/typescript/bin/tsc'),0o755);fs.mkdirSync(path.join(root,'node_modules/.bin'),{recursive:true});fs.symlinkSync('../typescript/bin/tsc',path.join(root,'node_modules/.bin/tsc')); +} +test('setup recommends local type verification alongside runtime execution without running compiler code',t=>{ + const root=fixture(t,{'package.json':{name:'example',devDependencies:{typescript:'6.0.3'}},'tsconfig.json':'{"compilerOptions":{"strict":true}}','tddswarm.config.json':{adapter:'vitest',runner:['node','vitest.mjs','run','{files}'],executionMode:'shadow'}});presence(root); + const recommendation=recommendPlugins(root);assert.deepEqual(recommendation.applicable,[{id:'typescript'}]);assert.equal(recommendation.execution.selected,'core'); + configurePluginsAutomatically(root,recommendation);const config=readConfig(root);assert.deepEqual(config.plugins.typescript,{enabled:true});assert.equal(config.adapter,'vitest');assert.equal(config.executionMode,'shadow'); + assert.equal(checkPlugins(root,{id:'typescript'}).plugins[0].probe,'bounded-project-compiler-presence');assert.equal(fs.existsSync(path.join(root,'FORBIDDEN_EXECUTION')),false); +}); +test('explicitly disabled type checks remain disabled and absent project contracts are not activated',t=>{ + const root=fixture(t,{'package.json':{name:'example'},'tsconfig.json':'{}','tddswarm.config.json':{plugins:{typescript:{enabled:false}}}});presence(root); + assert.deepEqual(recommendPlugins(root).applicable,[]);configurePluginsAutomatically(root,recommendPlugins(root));assert.equal(readConfig(root).plugins.typescript.enabled,false); + const missing=fixture(t,{'package.json':{name:'example'}});presence(missing);assert.deepEqual(recommendPlugins(missing).applicable,[]); +}); +test('type-check plugin cannot replace a native backend or execute a custom compiler wrapper',t=>{ + const root=fixture(t,{'tddswarm.config.json':{executionPlugin:'nx',plugins:{nx:{enabled:true}}}}); + configurePlugin(root,'typescript',{enabled:true,settings:{project:'packages/app/tsconfig.json',timeoutMs:1000}});assert.equal(readConfig(root).integration.type,'nx'); + for(const settings of [{command:['external']},{project:'../tsconfig.json'},{project:'/tsconfig.json'},{timeoutMs:0}])assert.throws(()=>configurePlugin(root,'typescript',{enabled:true,settings})); + assert.throws(()=>configurePlugin(root,'typescript',{enabled:true,select:true}),/native backend/); +}); From efd272cf4b6e5c971fb3de8bafc1559199c75dce Mon Sep 17 00:00:00 2001 From: Rudy Celekli Date: Mon, 12 Oct 2026 23:38:28 -0400 Subject: [PATCH 06/16] Challenge archive evidence and preserve physical typed repair installations --- scripts/packed-evidence.js | 9 ++++++ scripts/packed-proof.js | 1 + src/pull-request.js | 4 +++ test/installed-typecheck-evidence.test.js | 38 +++++++++++++++++++++++ test/repair.test.js | 4 ++- 5 files changed, 55 insertions(+), 1 deletion(-) create mode 100644 test/installed-typecheck-evidence.test.js diff --git a/scripts/packed-evidence.js b/scripts/packed-evidence.js index a86a035..65bf387 100644 --- a/scripts/packed-evidence.js +++ b/scripts/packed-evidence.js @@ -4,6 +4,7 @@ import {assessmentImportEvidenceComplete} from './assessment-import-proof.js'; import fs from 'node:fs'; import path from 'node:path'; import {createHash} from 'node:crypto'; +import {summarizeTypecheck} from '../src/typecheck-report.js'; const typePhases=[['setup-fault',1,'failed',true,1,[2322]],['healthy-shadow',0,'passed',true,1,[]],['unsupported-no-check',2,'incomplete',false,0,[]]]; export function installedTypecheckEvidenceComplete(value){ const hex=x=>typeof x==='string'&&/^[a-f0-9]{64}$/.test(x); @@ -19,6 +20,14 @@ export function verifyInstalledTypecheckRawEvidence(value,directory){ const file=path.join(directory,row.id+'.json'),stat=fs.lstatSync(file);if(!stat.isFile()||stat.isSymbolicLink()||stat.size>16*1024*1024)return false; const bytes=fs.readFileSync(file);if(createHash('sha256').update(bytes).digest('hex')!==row.rawSha256)return false; const envelope=JSON.parse(bytes),report=envelope.verification||envelope,check=report.typecheck; + const native=report.tests?.[0],typed=summarizeTypecheck(check,{expectedProvenance:report.provenance||report.plan?.provenance}); + if(report.complete!==true||report.nativeComplete!==true||report.signal||report.error||report.reportErrors?.length|| + report.missingFiles?.length||report.unknownFiles?.length||report.tests?.length!==1|| + typeof native?.id!=='string'||!native.id||native.file!=='test/value.test.mjs'||native.name!=='ordinary value'||native.status!=='passed'|| + JSON.stringify(report.collectionFiles)!==JSON.stringify(['test/value.test.mjs']))return false; + if(row.id==='unsupported-no-check'){ + if(check?.exitCode!==2||check.signal!==null||check.errorCode!=='typecheck-no-check'||JSON.stringify(check.reasons)!==JSON.stringify(['typecheck-no-check']))return false; + }else if(!typed.complete||typed.status!==row.status||check.exitCode!==row.exitCode)return false; return report.exitCode===row.exitCode&&report.nativeExitCode===0&&check?.status===row.status&&check.complete===row.complete&& check.implementationFiles?.length===row.implementationCount&&JSON.stringify(check.diagnostics.filter(d=>d.category==='error').map(d=>d.code))===JSON.stringify(row.errorCodes)&& report.tests?.length===1&&report.tests[0].status==='passed'; diff --git a/scripts/packed-proof.js b/scripts/packed-proof.js index b5be694..4789ee7 100644 --- a/scripts/packed-proof.js +++ b/scripts/packed-proof.js @@ -45,6 +45,7 @@ try{ typedWrite('tsconfig.json',{compilerOptions:{strict:true,noEmit:true,module:'NodeNext',moduleResolution:'NodeNext',target:'ES2022'},files:['src/value.ts']}); typedWrite('tddswarm.config.json',{adapter:'node',runner:[process.execPath,'--experimental-strip-types','--no-warnings','--test','{files}'],executionMode:'shadow'}); fs.mkdirSync(path.join(typedRoot,'node_modules'));fs.cpSync(path.join(tools,'node_modules/typescript'),path.join(typedRoot,'node_modules/typescript'),{recursive:true}); + fs.mkdirSync(path.join(typedRoot,'node_modules/.bin'));fs.symlinkSync('../typescript/bin/tsc',path.join(typedRoot,'node_modules/.bin/tsc')); for(const args of [['init','-b','main'],['config','user.name','Packed Proof'],['config','user.email','proof@example.invalid'],['add','.'],['commit','-m','independent type contract']])invoke(typedRoot,['git',...args]); const typedEvidenceDirectory=path.join(path.dirname(output),'installed-typecheck-raw');fs.mkdirSync(typedEvidenceDirectory,{recursive:true}); const typeOutcomes=[]; diff --git a/src/pull-request.js b/src/pull-request.js index f157d39..48b1c71 100644 --- a/src/pull-request.js +++ b/src/pull-request.js @@ -67,6 +67,10 @@ export function publishImprovement(root, result, options = {}) { const prefix=git(worktree,['rev-parse','--show-prefix']).trim(); const metadata=line=>{if(line.slice(0,2)!=='??')return false;const file=line.slice(3);const local=prefix&&file.startsWith(prefix)?file.slice(prefix.length):file; if(local==='.tddswarm'||local.startsWith('.tddswarm/'))return true; + // Repair admission above authenticates the full installed tree at every + // boundary. Physical typed-repair copies need the same metadata treatment + // as shared installations without hiding a changed dependency byte. + if(repair&&(local==='node_modules'||local.startsWith('node_modules/'))){try{if(fs.lstatSync(path.join(worktree,'node_modules')).isDirectory())return true;}catch{}} if(local==='node_modules'){try{return fs.lstatSync(path.join(worktree,'node_modules')).isSymbolicLink()&&fs.realpathSync(path.join(worktree,'node_modules'))===fs.realpathSync(path.join(root,'node_modules'));}catch{}}return false;}; function exactState(){ if(repair)repairPublicationGuard(worktree,result); diff --git a/test/installed-typecheck-evidence.test.js b/test/installed-typecheck-evidence.test.js new file mode 100644 index 0000000..74399a7 --- /dev/null +++ b/test/installed-typecheck-evidence.test.js @@ -0,0 +1,38 @@ +import test from 'node:test'; +import assert from 'node:assert/strict'; +import fs from 'node:fs'; +import path from 'node:path'; +import {createHash} from 'node:crypto'; +import {fixture,commit,write} from './helpers.js'; +import {run} from '../src/runner.js'; +import {verifyInstalledTypecheckRawEvidence} from '../scripts/packed-evidence.js'; +const hash=bytes=>createHash('sha256').update(bytes).digest('hex'); + +test('native-backed raw compiler evidence rejects contradictory archive result claims',t=>{ + const root=fixture(t,{'package.json':{type:'module'},'.gitignore':'node_modules/\n.tddswarm/\n', + 'src/value.ts':'export const value: string = 42;\n', + 'tsconfig.json':{compilerOptions:{strict:true,noEmit:true,module:'NodeNext',moduleResolution:'NodeNext',target:'ES2022'},files:['src/value.ts']}, + 'test/value.test.mjs':"import test from 'node:test';import assert from 'node:assert/strict';import {value} from '../src/value.ts';test('ordinary value',()=>assert.equal(value,42));", + 'tddswarm.config.json':{adapter:'node',runner:[process.execPath,'--experimental-strip-types','--no-warnings','--test','{files}'],plugins:{typescript:{enabled:true}}}}); + fs.mkdirSync(path.join(root,'node_modules'));fs.cpSync(path.resolve('node_modules/typescript'),path.join(root,'node_modules/typescript'),{recursive:true});commit(root); + const reports={};reports['setup-fault']=run(root,{full:true,capture:true}); + write(root,'src/value.ts','export const value: number = 42;\n');reports['healthy-shadow']=run(root,{full:true,capture:true}); + write(root,'tsconfig.json',{compilerOptions:{noCheck:true,noEmit:true},files:['src/value.ts']});reports['unsupported-no-check']=run(root,{full:true,capture:true}); + // Synthetic installation envelope only. These real source executions test + // parser admission; packed-proof independently exercises the installed CLI. + const directory=path.join(root,'.tddswarm/raw-type-evidence');fs.mkdirSync(directory); + const envelope={verified:true,productionInstalled:true,setupEnabled:true,providerCalls:0,archiveSha256:'a'.repeat(64),installedCliSha256:'b'.repeat(64),compilerVersion:'6.0.3',outcomes:[]}; + for(const [id,report]of Object.entries(reports)){ + const check=report.typecheck,bytes=JSON.stringify(report);fs.writeFileSync(path.join(directory,id+'.json'),bytes); + envelope.outcomes.push({id,exitCode:report.exitCode,nativeExitCode:report.nativeExitCode,status:check.status,complete:check.complete, + implementationCount:check.implementationFiles.length,errorCodes:check.diagnostics.filter(row=>row.category==='error').map(row=>row.code),rawSha256:hash(bytes)}); + } + assert.equal(verifyInstalledTypecheckRawEvidence(envelope,directory),true); + const healthy=reports['healthy-shadow']; + for(const mutate of [r=>{r.typecheck.signal='SIGKILL';},r=>{r.typecheck.errorCode='ETIMEDOUT';},r=>{r.typecheck.exitCode=2;}, + r=>{r.typecheck.binding=null;},r=>{r.complete=false;},r=>{r.nativeComplete=false;},r=>{r.tests=[{status:'passed'}];},r=>{r.typecheck.binding.provenance.fingerprint='0'.repeat(64);}]){ + const changed=structuredClone(healthy);mutate(changed);const bytes=JSON.stringify(changed),receipt=structuredClone(envelope); + fs.writeFileSync(path.join(directory,'healthy-shadow.json'),bytes);receipt.outcomes[1].rawSha256=hash(bytes); + assert.equal(verifyInstalledTypecheckRawEvidence(receipt,directory),false); + } +}); diff --git a/test/repair.test.js b/test/repair.test.js index 0bcc608..c9bb15e 100644 --- a/test/repair.test.js +++ b/test/repair.test.js @@ -32,7 +32,7 @@ test('source repair validates repeated unchanged assertions and commits an isola }); test('enabled native compiler vetoes a runtime-green repair and qualifies the corrected complete branch',async t=>{ const root=project(t,{ - '.gitignore':'node_modules/\n.tddswarm/\n', + '.gitignore':'.tddswarm/\n', 'src/a.js':'/** @type {number} */ export const a=9;', 'test/a.test.js':"import test from 'node:test';import assert from 'node:assert/strict';import {a} from '../src/a.js';test('numeric value',()=>assert.equal(Number(a),1));", 'tsconfig.json':{compilerOptions:{allowJs:true,checkJs:true,noEmit:true,strict:true,module:'NodeNext',moduleResolution:'NodeNext',target:'ES2022'},include:['src/*.js']}, @@ -50,6 +50,8 @@ test('enabled native compiler vetoes a runtime-green repair and qualifies the co assert.throws(()=>publishImprovement(root,missing),/final TypeScript verification/); const corrupt=structuredClone(good);corrupt.fullRun.typecheck.compiler.sha256='0'.repeat(64); assert.throws(()=>publishImprovement(root,corrupt),/compiler identities differ/); + git(root,'remote','add','origin','/private/tmp/testlore-no-network'); + assert.throws(()=>publishImprovement(root,good),/Automatic pull requests require a configured GitHub remote/); }); test('wrong source repair never commits, and test edits cannot replace a repair',async t=>{ const root=project(t),wrong=await repair(root,{patch:patch('export const a=99;'),sourcePaths:['src/a.js']});cleanup(t,root,wrong);assert.equal(wrong.status,'validation-rejected');assert.equal(wrong.sha,null); From f31b22b4a076f2da6c4648e2a5ef6bc7a7ddf085 Mon Sep 17 00:00:00 2001 From: Rudy Celekli Date: Mon, 12 Oct 2026 23:49:23 -0400 Subject: [PATCH 07/16] Make contribution timeout control independent of scheduler timing --- test/public-contribution-snapshot.test.js | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/test/public-contribution-snapshot.test.js b/test/public-contribution-snapshot.test.js index 4cf141f..59463ed 100644 --- a/test/public-contribution-snapshot.test.js +++ b/test/public-contribution-snapshot.test.js @@ -116,12 +116,16 @@ test('repeated cursors, changed counts and transport errors produce explicit inc }); }); -test('API, time and byte ceilings retain partial public evidence without false completeness', async () => { +test('API, time and byte ceilings retain partial public evidence without false completeness', async t => { const result = await collectPublicContributions({ ...config, maxCalls: 1, fetch: async () => page([pr(1)], 2, true, 'next') }); assert.equal(result.coverage.complete, false); assert.equal(result.pullRequests.length, 1); assert.equal(result.coverage.apiCalls, 1); const tooLarge = await collectPublicContributions({ ...config, maxPersistedBytes: 1, fetch: async () => page([pr(1)]) }); assert.equal(tooLarge.coverage.complete, false); assert.equal(tooLarge.sourceHashes.pages.length, 0); - const timed = await collectPublicContributions({ ...config, maxDurationMs: 1, fetch: async (_,__,bounds) => { assert.ok(bounds.timeoutMs <= 1); await new Promise(resolve => setTimeout(resolve, 10)); return page([pr(1)]); } }); + // Freeze startup time, then consume the budget inside the actual fetch seam. + // A real one-millisecond clock can expire before any query starts, correctly + // yielding zero calls; scheduler speed must not determine this assertion. + t.mock.timers.enable({apis:['Date'],now:0}); + const timed = await collectPublicContributions({ ...config, maxDurationMs: 1, fetch: async (_,__,bounds) => { assert.ok(bounds.timeoutMs <= 1); t.mock.timers.tick(10); return page([pr(1)]); } }); assert.equal(timed.coverage.complete, false); assert.equal(timed.coverage.apiCalls, 1); assert.equal(timed.pullRequests.length, 0); assert.match(timed.coverage.limits[0], /wall-clock-budget/); }); From e8d9590ef586b014e07b87cd707b53157568901f Mon Sep 17 00:00:00 2001 From: Rudy Celekli Date: Tue, 13 Oct 2026 00:01:17 -0400 Subject: [PATCH 08/16] Request explicit TAP for nested type verification controls --- test/typecheck-integration.test.js | 2 +- test/typecheck.test.js | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/test/typecheck-integration.test.js b/test/typecheck-integration.test.js index eef937c..42d2e51 100644 --- a/test/typecheck-integration.test.js +++ b/test/typecheck-integration.test.js @@ -64,7 +64,7 @@ test('ignored source changed by a passing runtime test cannot mix with fresh com 'ignored/subject.ts':'export const value: number = 42;\n', 'test/runtime.test.mjs':"import test from 'node:test';import assert from 'node:assert/strict';import fs from 'node:fs';import {value} from '../ignored/subject.ts';test('original ignored runtime value',()=>{assert.equal(value,42);fs.writeFileSync('ignored/subject.ts','export const value: number = 99;\\n');});"}); const result=run(root,{full:true,capture:true});assert.equal(result.nativeExitCode,0);assert.equal(result.tests[0].status,'passed');assert.equal(result.exitCode,2);assert.equal(result.typecheck.status,'incomplete');assert.notEqual(result.summary.verdict,'passed-in-observed-scope'); - const native=spawnSync(process.execPath,['--experimental-strip-types','--no-warnings','--test','test/runtime.test.mjs'],{cwd:root,encoding:'utf8',env:{PATH:process.env.PATH,HOME:root}});assert.equal(native.status,1);assert.match(native.stdout,/# fail 1/); + const native=spawnSync(process.execPath,['--experimental-strip-types','--no-warnings','--test','--test-reporter=tap','test/runtime.test.mjs'],{cwd:root,encoding:'utf8',env:{PATH:process.env.PATH,HOME:root}});assert.equal(native.status,1);assert.match(native.stdout,/# fail 1/); }); test('native execution cannot erase the requested checker by disabling ignored configuration',t=>{ const root=project(t,{ diff --git a/test/typecheck.test.js b/test/typecheck.test.js index 44e9ff7..ddada0b 100644 --- a/test/typecheck.test.js +++ b/test/typecheck.test.js @@ -26,7 +26,7 @@ const hook=(root,code)=>{const file=path.join(root,'node_modules/typescript/lib/ test('native runtime green cannot hide TS2322; healthy inverse is genuinely checked and read-only',t=>{ const root=project(t,{'src/value.ts':'export const value: string = 42;\n','test/runtime.test.mjs':"import test from 'node:test';import assert from 'node:assert/strict';import {value} from '../src/value.ts';test('runtime value',()=>assert.equal(value,42));"}); - const runtime=spawnSync(process.execPath,['--experimental-strip-types','--test','test/runtime.test.mjs'],{cwd:root,encoding:'utf8',env:{HOME:root,LANG:'C',PATH:path.dirname(process.execPath)+path.delimiter+'/usr/bin:/bin'}});assert.equal(runtime.status,0,runtime.stderr);assert.match(runtime.stdout,/# tests 1/);assert.match(runtime.stdout,/# pass 1/); + const runtime=spawnSync(process.execPath,['--experimental-strip-types','--test','--test-reporter=tap','test/runtime.test.mjs'],{cwd:root,encoding:'utf8',env:{HOME:root,LANG:'C',PATH:path.dirname(process.execPath)+path.delimiter+'/usr/bin:/bin'}});assert.equal(runtime.status,0,runtime.stderr);assert.match(runtime.stdout,/# tests 1/);assert.match(runtime.stdout,/# pass 1/); const result=check(root);assert.equal(result.status,'failed',JSON.stringify({status:result.status,errorCode:result.errorCode,diagnostics:result.diagnostics}));assert.equal(result.exitCode,1);assert.equal(result.complete,true);assert.ok(result.diagnostics.some(d=>d.code===2322&&d.file==='src/value.ts'&&d.line===1));assert.ok(result.compiler.version);assert.ok(result.binding.source.files.some(row=>row.file==='ignored.txt'));assert.ok(result.files.includes('src/value.ts')); write(root,'src/value.ts','export const value: number = 42;\n');const healthy=check(root);assert.equal(healthy.status,'passed',JSON.stringify({status:healthy.status,errorCode:healthy.errorCode,diagnostics:healthy.diagnostics}));assert.equal(healthy.exitCode,0);assert.equal(healthy.complete,true);assert.equal(healthy.diagnostics.length,0);assert.ok(healthy.limitations.some(s=>s.includes('skipLibCheck')));assert.equal(fs.existsSync(path.join(root,'src/value.js')),false);assert.equal(fs.existsSync(path.join(root,'tsconfig.tsbuildinfo')),false); }); @@ -119,7 +119,7 @@ test('private opening inputs bind ignored source and SDK across prior native exe const root=project(t);write(root,'.gitignore','src/value.ts\nnode_modules/\n');const token=captureTypecheckInputs(root,{timeoutMs:20000});assert.equal(token.status,'captured');assert.equal(token.complete,true);assert.equal(Object.isFrozen(token),true); write(root,'test/native.test.mjs',"import test from 'node:test';import assert from 'node:assert/strict';import fs from 'node:fs';import {value} from '../src/value.ts';test('native then mutates',()=>{assert.equal(value,42);fs.writeFileSync('src/value.ts','export const value: number=99;');});"); // Capture after installation of the independent native oracle, before it runs. - commit(root);const opening=captureTypecheckInputs(root,{timeoutMs:20000});const native=spawnSync(process.execPath,['--experimental-strip-types','--test','test/native.test.mjs'],{cwd:root,encoding:'utf8',env:{HOME:root,LANG:'C',PATH:path.dirname(process.execPath)+path.delimiter+'/usr/bin:/bin'}});assert.equal(native.status,0,native.stderr);assert.match(native.stdout,/# tests 1/);assert.match(native.stdout,/# pass 1/);assert.match(fs.readFileSync(path.join(root,'src/value.ts'),'utf8'),/99/); + commit(root);const opening=captureTypecheckInputs(root,{timeoutMs:20000});const native=spawnSync(process.execPath,['--experimental-strip-types','--test','--test-reporter=tap','test/native.test.mjs'],{cwd:root,encoding:'utf8',env:{HOME:root,LANG:'C',PATH:path.dirname(process.execPath)+path.delimiter+'/usr/bin:/bin'}});assert.equal(native.status,0,native.stderr);assert.match(native.stdout,/# tests 1/);assert.match(native.stdout,/# pass 1/);assert.match(fs.readFileSync(path.join(root,'src/value.ts'),'utf8'),/99/); const changed=verifyTypecheck(root,{openingInputs:opening,timeoutMs:20000});assert.equal(changed.errorCode,'typecheck-opening-source-drift');assert.equal(changed.exitCode,2);assert.equal(changed.complete,false);assert.equal(check(root).status,'passed','Standalone current-state type check alone would not detect prior runtime drift'); write(root,'src/value.ts','export const value: number=42;');const sdkOpening=captureTypecheckInputs(root,{timeoutMs:20000});hook(root,"require('node:fs').writeFileSync('compiler-entered.txt','yes');");const sdkChanged=verifyTypecheck(root,{openingInputs:sdkOpening,timeoutMs:20000});assert.equal(sdkChanged.errorCode,'typecheck-opening-dependency-drift');assert.equal(fs.existsSync(path.join(root,'compiler-entered.txt')),false); for(const copied of [{...sdkOpening},JSON.parse(JSON.stringify(sdkOpening)),null])assert.equal(verifyTypecheck(root,{openingInputs:copied,timeoutMs:20000}).errorCode,'typecheck-invalid-opening-inputs'); From 6227c360187bd41becfef7d34263d390c55c8539 Mon Sep 17 00:00:00 2001 From: Rudy Celekli Date: Tue, 13 Oct 2026 00:31:40 -0400 Subject: [PATCH 09/16] Gate test improvement readiness on native TypeScript and safe receipts --- src/improvement.js | 125 ++++++++++++++++++++++++++--- test/improvement-typecheck.test.js | 39 +++++++++ 2 files changed, 152 insertions(+), 12 deletions(-) create mode 100644 test/improvement-typecheck.test.js diff --git a/src/improvement.js b/src/improvement.js index b8e2156..d70195c 100644 --- a/src/improvement.js +++ b/src/improvement.js @@ -8,6 +8,8 @@ import { git, readConfig, safePath, listFiles } from './files.js'; import { snapshot, freshness, digest } from './provenance.js'; import { stagePatch, validateCandidates, applyPatch } from './candidates.js'; import { generate } from './swarm.js'; +import {captureTypecheckInputs,verifyTypecheck} from './typecheck.js'; +import {summarizeTypecheck} from './typecheck-report.js'; import { discover, execute } from './execution.js'; const candidateId = id => typeof id === 'string' && /^[a-zA-Z0-9][a-zA-Z0-9_-]{0,127}$/.test(id); @@ -59,10 +61,11 @@ function copyCandidate(root, destination, id) { } const repositoryInputs=root=>digest(Object.fromEntries(listFiles(root).map(file=>[file,digest(fs.readFileSync(safePath(root,file)))]))); function fullSuite(root, options) { - const config = readConfig(root); - const discovery = discover(root, { ...config, discovery: Array.isArray(config.discovery) ? config.discovery : 'native' }); + const config = readConfig(root),provenance=options.typecheckRequested?snapshot(root,config):undefined; + const discovery = discover(root, { ...config, discovery: Array.isArray(config.discovery) ? config.discovery : 'native',...(options.typecheckRequested?{runnerTimeoutMs:Math.min(options.timeoutMs||20000,Math.max(1,options.deadline-performance.now()))}:{}) }); if (!discovery.complete || !discovery.files.length) return { exitCode: 2, complete: false, discovery, tests: [], error: 'Full native test scope is incomplete or empty' }; - return { ...execute(root, discovery.files, config, { capture: true, timeoutMs: options.timeoutMs }), discovery }; + if(options.typecheckRequested&&performance.now()>=options.deadline)return {exitCode:2,complete:false,discovery,tests:[],error:'typed-improvement-total-timeout',provenance}; + return { ...execute(root, discovery.files, config, { capture: true, timeoutMs: options.typecheckRequested?Math.min(options.timeoutMs||20000,Math.max(1,options.deadline-performance.now())):options.timeoutMs }), discovery,...(provenance?{provenance}:{}) }; } function missingPassingCases(expected, actual) { const counts = new Map(); @@ -75,10 +78,71 @@ function missingPassingCases(expected, actual) { return missing; } +const typeSettings=config=>config.plugins?.typescript?.enabled===true?{enabled:true,project:config.plugins.typescript.project||'tsconfig.json',timeoutMs:config.plugins.typescript.timeoutMs||30000}:null; +function dependencyInventory(root,deadline){ + const base=fs.realpathSync(path.join(root,'node_modules')),rows=[];let bytes=0,count=0; + function walk(directory,prefix=''){for(const name of fs.readdirSync(directory).sort()){ + if(performance.now()>deadline)throw Error('typed-improvement-total-timeout'); + const file=prefix+name,target=path.join(directory,name),stat=fs.lstatSync(target); + if(++count>100000||/[\x00-\x1f\x7f]/.test(file))throw Error('typed-improvement-dependency-profile-unsupported'); + if(stat.isDirectory()){rows.push({file,kind:'directory',mode:stat.mode&0o777});walk(target,file+'/');} + else if(stat.isSymbolicLink()){const link=fs.readlinkSync(target);if(path.isAbsolute(link)||!fs.realpathSync(target).startsWith(base+path.sep))throw Error('typed-improvement-external-dependency-link');rows.push({file,kind:'symlink',target:link});} + else if(stat.isFile()){bytes+=stat.size;if(bytes>1024*1024*1024)throw Error('typed-improvement-dependency-bound');rows.push({file,kind:'file',size:stat.size,mode:stat.mode&0o777,sha256:digest(fs.readFileSync(target))});} + else throw Error('typed-improvement-dependency-profile-unsupported'); + }} + walk(base);return {base,rows,sha256:digest(rows),bytes,mode:fs.statSync(base).mode&0o777}; +} +function copyTypedDependencies(root,worktree,deadline){ + if(!fs.existsSync(path.join(root,'node_modules/typescript/lib/typescript.js')))throw Error('typed-improvement-installed-SDK-required'); + const before=dependencyInventory(root,deadline),destination=path.join(worktree,'node_modules'); + if(fs.existsSync(destination))throw Error('typed-improvement-tracked-dependencies-unsupported'); + const space=fs.statfsSync(worktree);if(Number(space.bavail)*Number(space.bsize)deadline)throw Error('typed-improvement-total-timeout');const source=path.join(from,name),target=path.join(to,name),stat=fs.lstatSync(source); + if(stat.isDirectory()){fs.mkdirSync(target);copy(source,target);fs.chmodSync(target,stat.mode&0o777);} + else if(stat.isSymbolicLink())fs.symlinkSync(fs.readlinkSync(source),target); + else if(stat.isFile()){fs.copyFileSync(source,target);fs.chmodSync(target,stat.mode&0o777);}else throw Error('typed-improvement-dependency-profile-unsupported'); + }} + fs.mkdirSync(destination);copy(before.base,destination);fs.chmodSync(destination,before.mode); + if(dependencyInventory(root,deadline).sha256!==before.sha256||dependencyInventory(worktree,deadline).sha256!==before.sha256)throw Error('typed-improvement-installed-copy-drift'); + return before; +} +function assertObligation(root,expected){if(digest(typeSettings(readConfig(root)))!==digest(expected))throw Error('typed-improvement-enabled-obligation-changed');} +function sameTypedInputs(left,right){return left.binding?.source?.sha256===right.binding?.source?.sha256&&left.binding?.dependencies?.sha256===right.binding?.dependencies?.sha256&&digest(left.compiler)===digest(right.compiler)&&digest(left.binding?.node)===digest(right.binding?.node)&&digest(left.binding?.producers)===digest(right.binding?.producers);} +function attachTypecheck(result,report,check){ + result.typecheck=check;report.nativeExitCode=report.exitCode;report.nativeComplete=report.complete;report.typecheck=check; + const summary=summarizeTypecheck(check);report.complete=report.nativeComplete&&summary.complete;report.exitCode=summary.status==='incomplete'?2:summary.status==='failed'?1:report.nativeExitCode; + return summary; +} +function safeImprovementMetadata(root){ + const directory=path.join(root,'.tddswarm');if(!fs.existsSync(directory))return; + let count=0;function walk(target){const stat=fs.lstatSync(target);if(++count>10000||stat.isSymbolicLink()||!stat.isDirectory()&&(!stat.isFile()||stat.nlink!==1))throw Error('improvement-receipt-unsafe-metadata');if(stat.isDirectory())for(const name of fs.readdirSync(target))walk(path.join(target,name));} + walk(directory); +} +function writeTypedCandidateValidation(root,directory,value){ + safeImprovementMetadata(root);const target=safePath(directory,'validation.json'),temporary=path.join(directory,'.typed-validation-'+randomUUID()+'.tmp'); + try{const fd=fs.openSync(temporary,fs.constants.O_WRONLY|fs.constants.O_CREAT|fs.constants.O_EXCL|(fs.constants.O_NOFOLLOW||0),0o600);try{fs.writeFileSync(fd,JSON.stringify(value,null,2));}finally{fs.closeSync(fd);}fs.renameSync(temporary,target);} + finally{fs.rmSync(temporary,{force:true});} +} +function writeImprovementReceipt(root,result){ + let directory=root; + for(const part of ['.tddswarm','improvement']){directory=path.join(directory,part);if(!fs.existsSync(directory))fs.mkdirSync(directory,{mode:0o700});const stat=fs.lstatSync(directory);if(!stat.isDirectory()||stat.isSymbolicLink())throw Error('improvement-receipt-unsafe-directory');} + const target=path.join(directory,'result.json'); + if(fs.existsSync(target)||fs.lstatSync(directory).isSymbolicLink()){ + const stat=fs.lstatSync(target);if(!stat.isFile()||stat.isSymbolicLink()||stat.nlink!==1)throw Error('improvement-receipt-unsafe-output'); + } + const temporary=path.join(directory,'.result-'+randomUUID()+'.tmp'); + try{const text=JSON.stringify(result,null,2);if(Buffer.byteLength(text)>32*1024*1024)throw Error('improvement-receipt-output-bound');const fd=fs.openSync(temporary,fs.constants.O_WRONLY|fs.constants.O_CREAT|fs.constants.O_EXCL|(fs.constants.O_NOFOLLOW||0),0o600);try{fs.writeFileSync(fd,text);}finally{fs.closeSync(fd);}fs.renameSync(temporary,target);return target;} + finally{fs.rmSync(temporary,{force:true});} +} /** Improve in a new branch. Never check out, apply into, or merge the caller's checkout. */ export async function improve(root, options = {}) { root = fs.realpathSync(path.resolve(root)); - if (readConfig(root).integration) throw new Error('Reviewed improvements currently require Node/Jest/Vitest/Playwright individual-case validation. Use plan/run for your native ecosystem backend; a native-backend candidate validator is required before improving that project.'); + const originalConfig=readConfig(root),obligation=typeSettings(originalConfig); + const typedBudget=options.totalTimeoutMs??120000; + if(obligation&&(!Number.isSafeInteger(typedBudget)||typedBudget<1||typedBudget>600000))throw Error('Invalid bounded typed improvement totalTimeoutMs'); + const deadline=performance.now()+typedBudget,remaining=()=>{const time=Math.floor(deadline-performance.now());if(time<1)throw Error('typed-improvement-total-timeout');return Math.min(obligation?.timeoutMs||20000,time);}; + if (originalConfig.integration) throw new Error('Reviewed improvements currently require Node/Jest/Vitest/Playwright individual-case validation. Use plan/run for your native ecosystem backend; a native-backend candidate validator is required before improving that project.'); if (options.agent !== undefined && (!Array.isArray(options.agent) || !options.agent.length || options.agent.some(argument => typeof argument !== 'string' || !argument))) throw new Error('agent must be a nonempty executable and argv array'); if (options.id && options.patch) throw new Error('Use a candidate id or a patch, not both'); if (options.id && !candidateId(options.id)) throw new Error('Invalid candidate id'); @@ -98,17 +162,25 @@ export async function improve(root, options = {}) { git(repository, ['check-ref-format', '--branch', branch]); const worktreeRoot = fs.mkdtempSync(path.join(os.tmpdir(), 'tddswarm-improve-')); const worktree = prefix ? path.join(worktreeRoot, prefix) : worktreeRoot; - const result = { schemaVersion: 1, id, branch, worktree, worktreeRoot, base, sourceHead: base, baseBranch, sha: null, status: 'preparing', merged: false, originalCheckout: root }; + const result = { schemaVersion: 1, id, branch, worktree, worktreeRoot, base, sourceHead: base, baseBranch, sha: null, status: 'preparing', merged: false, originalCheckout: root,...(obligation?{typecheckObligation:obligation,typecheckVerified:false}: {}) }; + let installedTypedDependencies=null; const receipt = () => { - result.learning=mergeLearning(worktree,root); - const directory = safePath(worktree, '.tddswarm/improvement'); fs.mkdirSync(directory, { recursive: true }); - result.receipt = path.join(directory, 'result.json'); fs.writeFileSync(result.receipt, JSON.stringify(result, null, 2)); return result; + try{ + safeImprovementMetadata(worktree);safeImprovementMetadata(root); + result.learning=mergeLearning(worktree,root); + safeImprovementMetadata(worktree);safeImprovementMetadata(root); + result.receipt=path.join(worktree,'.tddswarm/improvement/result.json');writeImprovementReceipt(worktree,result);return result; + }catch(error){result.status='receipt-failed';result.error=error.message;result.receipt=null;result.typecheckVerified=false;if(result.validation)result.validation.qualityReady=false;return result;} }; try { git(repository, ['worktree', 'add', '-b', branch, worktreeRoot, base]); const dependencies = path.join(root, 'node_modules'); - const dependencyMetadata=fs.existsSync(dependencies)&&fs.lstatSync(dependencies).isDirectory()?{root:fs.realpathSync(root),nodeModules:fs.realpathSync(dependencies),sourceHead:base}:undefined; - if (fs.existsSync(dependencies) && !fs.existsSync(path.join(worktree, 'node_modules'))) fs.symlinkSync(fs.realpathSync(dependencies), path.join(worktree, 'node_modules'), 'dir'); + const dependencyMetadata=!obligation&&fs.existsSync(dependencies)&&fs.lstatSync(dependencies).isDirectory()?{root:fs.realpathSync(root),nodeModules:fs.realpathSync(dependencies),sourceHead:base}:undefined; + if(obligation){ + try{installedTypedDependencies=copyTypedDependencies(root,worktree,deadline);result.typecheckPreflight=verifyTypecheck(worktree,{project:obligation.project,timeoutMs:remaining()});const admitted=summarizeTypecheck(result.typecheckPreflight);if(!admitted.complete)throw Error(result.typecheckPreflight.errorCode||'typed-improvement-profile-incomplete');} + catch(error){result.status='typecheck-incomplete';result.error=error.message;return receipt();} + } + if (!obligation&&fs.existsSync(dependencies) && !fs.existsSync(path.join(worktree, 'node_modules'))) fs.symlinkSync(fs.realpathSync(dependencies), path.join(worktree, 'node_modules'), 'dir'); result.agentProfile=ensureQualityAgent(worktree,{name:qualityAgent(root).name}).profile; result.learningSeed=copyLearning(root,worktree); // Bound original input must match the checked-out branch before any agent runs. @@ -121,6 +193,7 @@ export async function improve(root, options = {}) { if (!Array.isArray(files) || files.some(file => typeof file !== 'string')) throw new Error('initialize must return changed project paths'); initialized.push(...files.map(file => changedPath(worktree, file))); } + if(obligation)assertObligation(worktree,obligation); let staged; if (options.id) staged = copyCandidate(root, worktree, options.id); else if (options.patch) staged = stagePatch(worktree, options.patch); @@ -137,6 +210,7 @@ export async function improve(root, options = {}) { if(staged.generation)result.generation=staged.generation; const validation = validateCandidates(worktree, staged.id, options); result.validation = validation; + if(obligation){validation.typecheckFinalRequired=true;validation.qualityReady=false;const value={...validation};delete value.integrity;validation.integrity=digest(value);writeTypedCandidateValidation(worktree,staged.directory,validation);} if (!validation.accepted) { result.status = 'validation-rejected'; return receipt(); } result.application = applyPatch(worktree, staged.id, { execute: true }); const candidate = manifest(worktree, staged.id).value; @@ -157,9 +231,12 @@ export async function improve(root, options = {}) { const paths = [...new Set([...candidate.files.map(file => file.path), ...candidate.delete, ...initialized, ...prepared])]; for (const file of candidate.files) if (digest(fs.readFileSync(safePath(worktree, file.path))) !== file.hash) throw new Error(`Preparation altered reviewed candidate: ${file.path}`); for (const file of candidate.delete) if (fs.existsSync(safePath(worktree, file))) throw new Error(`Preparation restored a reviewed deletion: ${file}`); + if(obligation){assertObligation(worktree,obligation);if(dependencyInventory(worktree,deadline).sha256!==installedTypedDependencies.sha256||dependencyInventory(root,deadline).sha256!==installedTypedDependencies.sha256)throw Error('typed-improvement-dependencies-mutated-before-final-run');} const installed = snapshot(worktree, readConfig(worktree)); const testedRepository=repositoryInputs(worktreeRoot); - const report = fullSuite(worktree, options); result.fullRun = report; + const opening=obligation?captureTypecheckInputs(worktree,{timeoutMs:remaining()}):null; + if(obligation&&opening.complete!==true){result.status='typecheck-incomplete';result.error=opening.errorCode;return receipt();} + const report = fullSuite(worktree, obligation?{...options,typecheckRequested:true,deadline,timeoutMs:Math.min(options.timeoutMs||20000,remaining())}:options); result.fullRun = report;if(obligation){report.nativeExitCode=report.exitCode;report.nativeComplete=report.complete;} result.missingCases = missingPassingCases(validation.candidate?.tests, report.tests); if (!report.complete || report.exitCode !== 0 || !report.tests.some(test => test.status === 'passed') || result.missingCases.length) { result.status = 'full-run-failed'; return receipt(); @@ -169,6 +246,12 @@ export async function improve(root, options = {}) { if (!branchFresh.fresh) throw new Error(`Branch changed during full execution: ${branchFresh.reasons.join(', ')}`); const originalFresh = freshness(before, snapshot(root, readConfig(root))); if (!originalFresh.fresh || git(repository, ['rev-parse', 'HEAD']).trim() !== base || clean(repository, prefix).length) throw new Error('Original checkout changed while improvement was running; branch retained without a commit'); + if(obligation){ + assertObligation(worktree,obligation);assertObligation(root,obligation); + if(dependencyInventory(root,deadline).sha256!==installedTypedDependencies.sha256)throw Error('typed-improvement-original-dependencies-mutated'); + const check=verifyTypecheck(worktree,{project:obligation.project,timeoutMs:remaining(),openingInputs:opening}); + const typed=attachTypecheck(result,report,check);if(typed.status!=='passed'){result.status=typed.status==='failed'?'typecheck-failed':'typecheck-incomplete';return receipt();} + } const allowed = new Set([...paths.map(file => prefix ? `${prefix}/${file}` : file),...repositoryPrepared]); const unexpected = changes(worktreeRoot).filter(change => !metadata(change, prefix) && !allowed.has(change.file)); if (unexpected.length) throw new Error(`Unexpected changes outside the reviewed patch: ${unexpected.map(change => change.file).join(', ')}`); @@ -180,10 +263,28 @@ export async function improve(root, options = {}) { const afterCommit = freshness(installed, snapshot(worktree, readConfig(worktree))); const committedMatches = !git(worktreeRoot, ['diff', 'HEAD', '--name-only']).trim() && !changes(worktreeRoot).some(change => !metadata(change, prefix)); const treeMatches=git(worktreeRoot,['rev-parse','HEAD^{tree}']).trim()===intendedTree&&repositoryInputs(worktreeRoot)===testedRepository; - result.status = afterCommit.fresh && committedMatches && treeMatches ? 'ready-for-review' : 'commit-requires-review'; + const preCommitTypecheck=result.typecheck; + let postCommitTyped=true; + if(obligation){ + assertObligation(worktree,obligation);assertObligation(root,obligation); + const postOpening=captureTypecheckInputs(worktree,{timeoutMs:remaining()}); + const postNative=fullSuite(worktree,{...options,typecheckRequested:true,deadline,timeoutMs:Math.min(options.timeoutMs||20000,remaining())}); + // Capture every potentially executable config/probe callback before the + // closing compiler; no copied revision can stand in for actual execution. + const postOriginalFresh=freshness(before,snapshot(root,readConfig(root))); + assertObligation(worktree,obligation);assertObligation(root,obligation); + const check=verifyTypecheck(worktree,{project:obligation.project,timeoutMs:remaining(),openingInputs:postOpening,expectedProvenance:postNative.provenance}); + const typed=attachTypecheck(result,postNative,check); + const cases=tests=>digest((tests||[]).map(({file,name,status})=>[file,name,status]).sort((a,b)=>JSON.stringify(a).localeCompare(JSON.stringify(b)))); + postCommitTyped=postOpening.complete===true&&typed.status==='passed'&&postNative.nativeComplete===true&&postNative.nativeExitCode===0&&cases(report.tests)===cases(postNative.tests)&&sameTypedInputs(preCommitTypecheck,check)&&postOriginalFresh.fresh&&git(repository,['rev-parse','HEAD']).trim()===base&&repositoryInputs(worktreeRoot)===testedRepository&&dependencyInventory(root,deadline).sha256===installedTypedDependencies.sha256; + result.fullRunPreCommit=report;result.fullRun=postNative;result.typecheckPreCommit=preCommitTypecheck;result.typecheck=check;result.typecheckVerified=postCommitTyped; + if(!postCommitTyped){postNative.exitCode=2;postNative.complete=false;result.error='Post-commit native or TypeScript inputs differ from the exact previously tested scope';} + } + result.status = postCommitTyped&&afterCommit.fresh && committedMatches && treeMatches ? 'ready-for-review' : 'commit-requires-review'; if(!treeMatches)result.error='Committed repository tree differs from the exact tested and staged inputs'; if (!committedMatches) result.error = 'Committed tree differs from the inputs that passed the full suite'; if (!afterCommit.fresh) result.error = `Git hooks changed tested inputs: ${afterCommit.reasons.join(', ')}`; + if(obligation){result.validation.qualityReady=result.status==='ready-for-review';result.validation.typecheckFinalRequired=true;result.validation.typecheckVerified=result.typecheckVerified;const value={...result.validation};delete value.integrity;value.integrity=digest(value);result.validation=value;writeTypedCandidateValidation(worktree,result.candidate.directory,value);} result.preparedPaths = [...initialized, ...prepared];result.repositoryPreparedPaths=repositoryPrepared; result.next = ['Review the branch diff and retained original/candidate/full-run evidence.', 'Open a pull request when ready; merge is explicit.', 'Keep configured quality workflows enabled after merge.']; return receipt(); diff --git a/test/improvement-typecheck.test.js b/test/improvement-typecheck.test.js new file mode 100644 index 0000000..b38b817 --- /dev/null +++ b/test/improvement-typecheck.test.js @@ -0,0 +1,39 @@ +import test from 'node:test';import assert from 'node:assert/strict';import fs from 'node:fs';import path from 'node:path';import {fileURLToPath} from 'node:url'; +import {improve} from '../src/improvement.js';import {fixture,write,commit,git} from './helpers.js';import {digest} from '../src/provenance.js'; +const here=path.dirname(fileURLToPath(import.meta.url)),dependencies=[path.join(here,'../node_modules'),path.join(here,'../../../../node_modules')].find(directory=>fs.existsSync(path.join(directory,'typescript/lib/typescript.js'))); +const testSource="import test from 'node:test';import assert from 'node:assert/strict';import {value} from '../src/value.js';test('additional contract',()=>assert.equal(value,42));\n"; +const review={accepted:true,findings:[],oracle:{independent:true,basis:['Independent contract: value remains42.']}},patch=content=>({files:[{path:'test/additional.test.js',content}],requirements:'Independent contract: value remains42.',review}); +function project(t,{enabled=true}={}){assert.ok(dependencies);const root=fixture(t,{'package.json':{type:'module'},'.gitignore':'node_modules/\n.tddswarm/\n','tddswarm.config.json':{runner:[process.execPath,'--test','{files}'],plugins:{typescript:{enabled}}},'tsconfig.json':{compilerOptions:{strict:true,allowJs:true,checkJs:true,types:['node'],skipLibCheck:true,noEmit:true,target:'ES2022',module:'NodeNext',moduleResolution:'NodeNext'},include:['src/**/*.js','test/**/*.js']},'src/value.js':'export const value=42;','test/original.test.js':"import test from 'node:test';import assert from 'node:assert/strict';import {value} from '../src/value.js';test('original contract',()=>assert.equal(value,42));"});fs.mkdirSync(path.join(root,'node_modules/@types'),{recursive:true});for(const file of ['typescript','@types/node','undici-types'])fs.cpSync(path.join(dependencies,file),path.join(root,'node_modules',file),{recursive:true});commit(root);return root;} +function cleanup(t,root,result){t.after(()=>{try{git(root,'worktree','remove','--force',result.worktreeRoot);}catch{}try{git(root,'branch','-D',result.branch);}catch{}fs.rmSync(result.worktreeRoot,{recursive:true,force:true});});} +const run=(root,content=testSource,extra={})=>improve(root,{patch:patch(content),timeoutMs:10000,totalTimeoutMs:120000,...extra}); + +test('native-green test-only improvement with TS2322 cannot commit or become quality-ready',async t=>{ + const root=project(t),sdk=fs.readFileSync(path.join(root,'node_modules/typescript/lib/typescript.js')),before=git(root,'rev-parse','HEAD').trim();const result=await run(root,testSource+'/** @type {string} */ const hiddenTypeDefect=42;');cleanup(t,root,result); + assert.equal(result.validation.accepted,true);assert.equal(result.validation.candidate.exitCode,0);assert.equal(result.fullRun.nativeExitCode,0);assert.equal(result.fullRun.nativeComplete,true);assert.equal(result.status,'typecheck-failed',result.error);assert.equal(result.typecheck.exitCode,1);assert.ok(result.typecheck.diagnostics.some(d=>d.code===2322));assert.equal(result.fullRun.exitCode,1);assert.equal(result.sha,null);assert.equal(result.validation.qualityReady,false);assert.equal(git(root,'rev-parse','HEAD').trim(),before);assert.deepEqual(fs.readFileSync(path.join(root,'node_modules/typescript/lib/typescript.js')),sdk); +}); + +test('healthy typed improvement qualifies postcommit native/compiler replay with copied immutable SDK',async t=>{ + const root=project(t),sdk=fs.readFileSync(path.join(root,'node_modules/typescript/lib/typescript.js')),result=await run(root);cleanup(t,root,result); + assert.equal(result.status,'ready-for-review',JSON.stringify({error:result.error,diagnostics:result.typecheck?.diagnostics}));assert.ok(result.sha);assert.equal(result.typecheckVerified,true);assert.equal(result.typecheckObligation.timeoutMs,30000);assert.equal(result.validation.qualityReady,true);assert.equal(result.fullRun.nativeExitCode,0);assert.equal(result.fullRun.typecheck.status,'passed');assert.equal(result.fullRun.provenance.revision,result.sha);assert.equal(result.typecheck.binding.provenance.revision,result.sha);assert.equal(result.typecheck.binding.source.sha256,result.typecheckPreCommit.binding.source.sha256);assert.equal(fs.lstatSync(path.join(result.worktree,'node_modules')).isSymbolicLink(),false);assert.deepEqual(fs.readFileSync(path.join(root,'node_modules/typescript/lib/typescript.js')),sdk);assert.equal(fs.existsSync(path.join(root,'test/additional.test.js')),false);assert.equal(result.merged,false); +}); + +test('missing SDK and noCheck stop before initializer or worker while disabled remains native-only',async t=>{ + for(const cause of ['missing','noCheck']){const root=project(t);if(cause==='missing')fs.rmSync(path.join(root,'node_modules'),{recursive:true});else{write(root,'tsconfig.json',{compilerOptions:{allowJs:true,checkJs:true,noCheck:true},files:['src/value.js']});git(root,'add','tsconfig.json');git(root,'commit','-m','unsupported native typecheck profile');}let initialized=false;const result=await improve(root,{initialize:()=>{initialized=true;return [];},timeoutMs:10000,totalTimeoutMs:120000});cleanup(t,root,result);assert.equal(result.status,'typecheck-incomplete');assert.equal(initialized,false);assert.equal(result.sha,null);} + const root=project(t,{enabled:false}),result=await run(root,testSource+'/** @type {string} */ const hiddenTypeDefect=42;');cleanup(t,root,result);assert.equal(result.status,'ready-for-review',result.error);assert.equal(result.typecheckObligation,undefined);assert.equal(result.typecheck,undefined);assert.equal(fs.lstatSync(path.join(result.worktree,'node_modules')).isSymbolicLink(),true); +}); + +test('preparation cannot disable an initially requested typecheck obligation',async t=>{ + const root=project(t),result=await run(root,testSource,{prepare:branch=>{const config=JSON.parse(fs.readFileSync(path.join(branch,'tddswarm.config.json')));config.plugins.typescript.enabled=false;write(branch,'tddswarm.config.json',config);return ['tddswarm.config.json'];}});cleanup(t,root,result);assert.equal(result.status,'failed');assert.match(result.error,/enabled-obligation-changed/);assert.equal(result.sha,null); +}); + +test('postcommit hook drift cannot reuse precommit compiler success',async t=>{ + const root=project(t);fs.writeFileSync(path.join(root,'.git/hooks/pre-commit'),'#!/bin/sh\nprintf "export const value=99;" > src/value.js\ngit add src/value.js\n',{mode:0o755});const result=await run(root);cleanup(t,root,result);assert.equal(result.status,'commit-requires-review',result.error);assert.ok(result.sha);assert.equal(result.typecheckVerified,false);assert.equal(result.validation.qualityReady,false);assert.equal(result.fullRun.nativeExitCode,1);assert.equal(result.fullRun.exitCode,2);assert.equal(fs.readFileSync(path.join(root,'src/value.js'),'utf8'),'export const value=42;'); +}); + +test('receipt symlinks and hardlinks never overwrite source after healthy native runs',async t=>{ + for(const link of ['symlink','hardlink']){const root=project(t,{enabled:false});const content=testSource+`import fs from 'node:fs';fs.mkdirSync('.tddswarm/improvement',{recursive:true});if(!fs.existsSync('.tddswarm/improvement/result.json'))fs.${link==='symlink'?'symlinkSync':'linkSync'}(${link==='symlink'?"'../../src/value.js'":"'src/value.js'"},'.tddswarm/improvement/result.json');`;const result=await run(root,content);cleanup(t,root,result);assert.equal(result.status,'receipt-failed');assert.match(result.error,/unsafe-metadata|unsafe-output/);assert.equal(fs.readFileSync(path.join(result.worktree,'src/value.js'),'utf8'),'export const value=42;');assert.equal(result.receipt,null);assert.equal(fs.readFileSync(path.join(root,'src/value.js'),'utf8'),'export const value=42;');} +}); + +test('late typed candidate receipt alias cannot overwrite compiled source during final annotation',async t=>{ + const root=project(t);const content=testSource+"import fs from 'node:fs';if(fs.existsSync('.tddswarm/candidates')){const id=fs.readdirSync('.tddswarm/candidates')[0];const target='.tddswarm/candidates/'+id+'/validation.json';if(fs.existsSync(target))fs.unlinkSync(target);fs.symlinkSync('../../../src/value.js',target);}";const result=await run(root,content);cleanup(t,root,result);assert.equal(result.status,'receipt-failed');assert.match(result.error,/unsafe-metadata/);assert.equal(result.validation.qualityReady,false);assert.equal(fs.readFileSync(path.join(result.worktree,'src/value.js'),'utf8'),'export const value=42;');assert.equal(fs.readFileSync(path.join(root,'src/value.js'),'utf8'),'export const value=42;'); +}); From 370cf5ce10b6c1c239d03eda5d7f3851f48c97e6 Mon Sep 17 00:00:00 2001 From: Rudy Celekli Date: Tue, 13 Oct 2026 00:39:10 -0400 Subject: [PATCH 10/16] Recheck typed improvements at publication and in installed archive proof --- .../011-complementary-native-type-oracle.md | 2 +- docs/native-typecheck.md | 4 ++ scripts/packed-evidence.js | 20 ++++++++- scripts/packed-proof.js | 21 ++++++++- src/pull-request.js | 45 +++++++++++++++++-- .../improvement-publication-typecheck.test.js | 37 +++++++++++++++ test/installed-typecheck-evidence.test.js | 17 ++++++- test/packed-evidence.test.js | 5 ++- test/pull-request.test.js | 9 ++++ 9 files changed, 147 insertions(+), 13 deletions(-) create mode 100644 test/improvement-publication-typecheck.test.js diff --git a/docs/adr/011-complementary-native-type-oracle.md b/docs/adr/011-complementary-native-type-oracle.md index 0c5c72a..86a6213 100644 --- a/docs/adr/011-complementary-native-type-oracle.md +++ b/docs/adr/011-complementary-native-type-oracle.md @@ -12,4 +12,4 @@ Keep native cases/history/comparisons separate. Overall failure or incompletenes Independent development specifications predate the first component evaluation. Their ordinary runtime assertions miss five seeded type defects; the original compiler detects them. Retain healthy controls, all repeat exposures, exact input hashes, unsupported reference evidence and controller cost. Challenge the evaluator with malformed diagnostics, no-check sources, reserved inputs, service callback drift, ignored native-input mutation, copied opening tokens and contradictory persisted reports. -This adds execution and hashing cost. It supplies no omission authority, universal defect recall, learning promotion or overall competitive ranking. Project-reference builds and external symlinked dependency layouts need separate qualification. Source repair must satisfy enabled checks before authenticated application/publication; a requested but unqualified check cannot be silently ignored. +This adds execution and hashing cost. It supplies no omission authority, universal defect recall, learning promotion or overall competitive ranking. Project-reference builds and external symlinked dependency layouts need separate qualification. Source repair and test-only improvements must satisfy initially enabled checks before readiness/publication; a requested but unqualified check cannot be silently ignored. Typed improvements copy admitted dependencies and execute native/compiler verification both before and after commit, with current-input checks again at publication. Native-only candidate acceptance remains distinct from final typed readiness. Atomic unlinked receipt writes prevent validation, result and PR-body metadata from overwriting source. diff --git a/docs/native-typecheck.md b/docs/native-typecheck.md index 0b446cc..07cd9b2 100644 --- a/docs/native-typecheck.md +++ b/docs/native-typecheck.md @@ -32,6 +32,10 @@ Type verification does not permit test omissions. Routing still needs its existi Enabled type checking also gates the supported Node source-repair profile. Disposable repair copies retain the exact admitted installed dependency tree. Two baseline compiler checks may expose existing type errors; both candidate checks must complete and pass. Type evidence joins the authenticated validation receipt, and application/publication refuse missing, changed or failed evidence. Final branch verification checks the compiler again after independent behavioral challenges. Unsupported dependency relocation remains rejected. The initial repair profile still needs a genuine failing native assertion; a type-only failure produces a gap to resolve rather than an invented runtime repair. +Test-only improvements preserve the same initially requested compiler obligation. They copy the admitted dependency tree, require a complete compiler preflight before initialization or generation, and veto runtime-green candidates whose final compiler check fails. Native candidate acceptance alone is not typed quality readiness. After a commit, the controller actually reruns the full native suite and compiler at the new revision; it compares case identities and input bindings rather than copying a revision into old evidence. Commit-hook drift cannot become ready. Publication checks the compiler and bound inputs again before authentication and at subsequent publication boundaries. Linked candidate, result and PR-body receipts are refused without overwriting source. + +Dependency copying is bounded to 100,000 entries and 1 GiB, with an additional 1 GiB free-space reserve. Absolute or external dependency links and incomplete compiler profiles remain reviewable gaps. A passed program covers the files admitted by its configuration; new JavaScript tests outside that program still rely on the native runtime checks. These controls do not establish provider-generated test quality or catch every possible defect. + ## Reproduce the development evidence The [public fixture manifest](../benchmarks/type-detection/development-v1/manifest.json) preserves five independently frozen development defect pairs and one separately unqualified project-reference pair. Each healthy/fault pair keeps identical ordinary-input assertions, configuration and shared contracts, changing exactly one TypeScript source. The runtime probes execute real top-level `node:assert/strict` assertions and print a completion marker. Reproduce from a reviewed source checkout: diff --git a/scripts/packed-evidence.js b/scripts/packed-evidence.js index 65bf387..26f8fee 100644 --- a/scripts/packed-evidence.js +++ b/scripts/packed-evidence.js @@ -12,7 +12,23 @@ export function installedTypecheckEvidenceComplete(value){ typeof value.compilerVersion==='string'&&/^\d+\.\d+\.\d+/.test(value.compilerVersion)&&Array.isArray(value.outcomes)&&value.outcomes.length===3&& typePhases.every(([id,exit,status,complete,count,codes],index)=>{ const row=value.outcomes[index];return row?.id===id&&row.exitCode===exit&&row.nativeExitCode===0&&row.status===status&&row.complete===complete&&row.implementationCount===count&&JSON.stringify(row.errorCodes)===JSON.stringify(codes)&&hex(row.rawSha256); - }); + })&&value.improvement?.status==='ready-for-review'&&value.improvement.verified===true&&value.improvement.nativeCases===2&& + typeof value.improvement.sha==='string'&&/^[a-f0-9]{40}$/.test(value.improvement.sha)&&typeof value.improvement.sourceHead==='string'&&/^[a-f0-9]{40}$/.test(value.improvement.sourceHead)&&value.improvement.sha!==value.improvement.sourceHead&& + value.improvement.originalCallerUnchanged===true&&value.improvement.originalAssertionsUnchanged===true&&value.improvement.physicalDependencies===true&&hex(value.improvement.rawSha256); +} +function verifyInstalledTypedImprovement(value,directory){ + const row=value.improvement,file=path.join(directory,'improvement-ready.json'),stat=fs.lstatSync(file); + if(!stat.isFile()||stat.isSymbolicLink()||stat.size>16*1024*1024)return false; + const bytes=fs.readFileSync(file);if(createHash('sha256').update(bytes).digest('hex')!==row.rawSha256)return false; + const result=JSON.parse(bytes),report=result.fullRun,check=report?.typecheck,typed=summarizeTypecheck(check,{expectedProvenance:report?.provenance}); + const expected=[['test/boundary.test.mjs','independent numeric boundary'],['test/value.test.mjs','ordinary value']]; + const cases=report?.tests?.map(test=>[test.file,test.name]).sort((a,b)=>a[0].localeCompare(b[0])); + return result.status==='ready-for-review'&&result.typecheckVerified===true&&result.sha===row.sha&&result.sourceHead===row.sourceHead&&result.base===row.sourceHead&&result.merged===false&& + result.typecheckObligation?.enabled===true&&result.typecheckObligation.project==='tsconfig.json'&&result.typecheckObligation.timeoutMs===30000&&result.validation?.accepted===true&&result.validation.qualityReady===true&& + report?.provenance?.revision===row.sha&&report.complete===true&&report.nativeComplete===true&&report.exitCode===0&&report.nativeExitCode===0&&!report.signal&&!report.error&&!report.reportErrors?.length&&!report.missingFiles?.length&&!report.unknownFiles?.length&& + typed.complete&&typed.status==='passed'&&check.exitCode===0&&check.compiler?.version===value.compilerVersion&&check.implementationFiles?.length===1&&JSON.stringify(result.typecheck)===JSON.stringify(check)&& + JSON.stringify(cases)===JSON.stringify(expected)&&report.tests.every(test=>typeof test.id==='string'&&test.id&&test.status==='passed')&& + report.discovery?.complete===true&&JSON.stringify([...report.discovery.files].sort())===JSON.stringify(expected.map(([file])=>file)); } export function verifyInstalledTypecheckRawEvidence(value,directory){ if(!installedTypecheckEvidenceComplete(value))return false; @@ -31,7 +47,7 @@ export function verifyInstalledTypecheckRawEvidence(value,directory){ return report.exitCode===row.exitCode&&report.nativeExitCode===0&&check?.status===row.status&&check.complete===row.complete&& check.implementationFiles?.length===row.implementationCount&&JSON.stringify(check.diagnostics.filter(d=>d.category==='error').map(d=>d.code))===JSON.stringify(row.errorCodes)&& report.tests?.length===1&&report.tests[0].status==='passed'; - });}catch{return false;} + })&&verifyInstalledTypedImprovement(value,directory);}catch{return false;} } /** Validate the independently produced installed-archive proof, without truthy attestations. */ export function packedEvidenceComplete(receipt, evidence, {proofScriptSha256, sourceManifestSha256, installedRepairProofSha256,oneCommandProofSha256,assessmentImportProofSha256}) { diff --git a/scripts/packed-proof.js b/scripts/packed-proof.js index 4789ee7..f7ac67b 100644 --- a/scripts/packed-proof.js +++ b/scripts/packed-proof.js @@ -26,7 +26,7 @@ const sourceIndex=process.argv.indexOf('--expected-source-sha'),expectedSource=s if(expectedSource&&!/^[a-f0-9]{40}$/.test(expectedSource))throw new Error('Expected source SHA must be 40 lowercase hex characters'); const expectedSha=expectedIndex<0?null:process.argv[expectedIndex+1]; if(expectedSha&&!/^[a-f0-9]{64}$/.test(expectedSha))throw new Error('Expected SHA-256 must be 64 lowercase hex characters'); -let improvement; +let improvement,typedImprovement; try{ const pack=inputArchive?{filename:path.basename(inputArchive)}:JSON.parse(invoke(source,['npm','pack','--ignore-scripts','--json','--pack-destination',workspace]))[0]; const archive=inputArchive||path.join(workspace,pack.filename); @@ -59,11 +59,27 @@ try{ assert.equal(typedFault.nativeExitCode,0);assert.equal(typedFault.typecheck.status,'failed');assert.ok(typedFault.typecheck.diagnostics.some(row=>row.code===2322)); typedWrite('src/value.ts','export const value: number = 42;\n'); const typedHealthy=typedCommand('healthy-shadow',['run','--shadow','--base','HEAD'],0);assert.equal(typedHealthy.typecheck.status,'passed');assert.equal(typedHealthy.nativeExitCode,0); + for(const args of [['add','.'],['commit','-m','healthy typed baseline']])invoke(typedRoot,['git',...args]); + const typedBase=invoke(typedRoot,['git','rev-parse','HEAD']).trim(); + const typedOriginal=fs.readFileSync(path.join(typedRoot,'test/value.test.mjs')); + typedWrite('.tddswarm/typed-proposal.json',{files:[{path:'test/boundary.test.mjs',content:"import test from 'node:test';import assert from 'node:assert/strict';import {value} from '../src/value.ts';test('independent numeric boundary',()=>assert.equal(value+1,43));"}],requirements:'Independent numeric contract: value is 42 and adding one yields 43.',review:{accepted:true,findings:[],oracle:{independent:true,basis:['Independent public numeric contract.']}}}); + const typedImprovementRaw=invoke(typedRoot,[process.execPath,cli,'improve','--local','--patch','.tddswarm/typed-proposal.json','--json']); + typedImprovement=JSON.parse(typedImprovementRaw); + assert.equal(typedImprovement.status,'ready-for-review');assert.equal(typedImprovement.typecheckVerified,true); + assert.equal(typedImprovement.fullRun.nativeExitCode,0);assert.equal(typedImprovement.fullRun.exitCode,0);assert.equal(typedImprovement.fullRun.complete,true); + assert.equal(typedImprovement.fullRun.typecheck.status,'passed');assert.equal(typedImprovement.fullRun.provenance.revision,typedImprovement.sha); + assert.equal(typedImprovement.fullRun.tests.length,2);assert.ok(typedImprovement.fullRun.tests.every(row=>row.status==='passed')); + assert.equal(invoke(typedImprovement.worktree,['git','rev-parse','HEAD']).trim(),typedImprovement.sha); + assert.equal(fs.lstatSync(path.join(typedImprovement.worktree,'node_modules')).isDirectory(),true); + assert.deepEqual(fs.readFileSync(path.join(typedImprovement.worktree,'test/value.test.mjs')),typedOriginal); + assert.equal(invoke(typedRoot,['git','rev-parse','HEAD']).trim(),typedBase);assert.equal(invoke(typedRoot,['git','status','--porcelain']).trim(),''); + fs.writeFileSync(path.join(typedEvidenceDirectory,'improvement-ready.json'),typedImprovementRaw,{flag:'wx'}); + const typedImprovementEvidence={status:typedImprovement.status,verified:typedImprovement.typecheckVerified,nativeCases:2,sha:typedImprovement.sha,sourceHead:typedBase,originalCallerUnchanged:true,originalAssertionsUnchanged:true,physicalDependencies:true,rawSha256:createHash('sha256').update(typedImprovementRaw).digest('hex')}; typedWrite('tsconfig.json',{compilerOptions:{noCheck:true,noEmit:true},files:['src/value.ts']}); const typedUnsupported=typedCommand('unsupported-no-check',['run','--shadow','--base','HEAD'],2);assert.equal(typedUnsupported.typecheck.complete,false);assert.equal(typedUnsupported.nativeExitCode,0); const installedTypecheck={verified:true,productionInstalled:true,setupEnabled:true,providerCalls:0,outcomes:typeOutcomes, archiveSha256:sha256,installedCliSha256:createHash('sha256').update(fs.readFileSync(cli)).digest('hex'),compilerVersion:typedHealthy.typecheck.compiler.version, - scope:'Controlled single-project TypeScript production-install fixture; no upstream defect or universal compatibility claim.'}; + improvement:typedImprovementEvidence,scope:'Controlled single-project TypeScript production-install fixture with supplied reviewed test patch; no model, upstream defect or universal compatibility claim.'}; const command=(args,exit=0)=>JSON.parse(invoke(project,[process.execPath,cli,...args,'--json'],exit)); const immunityRoot=path.join(workspace,'immunity-inspection');fs.mkdirSync(immunityRoot); const immunity=command(['immunity','--root',immunityRoot]);assert.equal(immunity.backgroundServiceRunning,false);assert.equal(immunity.status,'awaiting-reviewed-probes');assert.equal(immunity.catalog.entries.length,3); @@ -154,6 +170,7 @@ try{ const receipt={schemaVersion:1,exactInputArchive:Boolean(inputArchive),proofScriptSha256:createHash('sha256').update(fs.readFileSync(fileURLToPath(import.meta.url))).digest('hex'),date:new Date().toISOString(),archive:pack.filename,sha256,version:pack.version,node:process.version,productionInstall:true,mcpStdioVerified:true,mcpShadowVerified:true,briefInspectionVerified:true,evidenceLoopVerified:true,mappingQualificationVerified:true,packedManifestSha256:createHash('sha256').update(fs.readFileSync(path.join(tools,'node_modules/testlore/package.json'))).digest('hex'),actionReference:installedPackage.gitHead||null,workflowActionReference,actionIdentityVerified:Boolean(expectedSource),shadowSetupVerified:true,adoptionInspectionVerified:true,plugins:{catalogCount:catalog.plugins.length,missingSdkExit:missingVectorSdk.exitCode,lexicalFallbackVerified:true},nativeShadow:{complete:shadow.complete,selected:shadow.plan.selected,executed:shadow.executedFiles.length,detected:true},runtimeCaptureComplete:capture.complete,improvement:{status:improvement.status,cases:improvement.fullRun.tests.length,originalBranch:'main'},installedRepair,oneCommand,assessmentImports,installedTypecheck,limitations:['Local packed-artifact installation; no registry publication or live GitHub PR created.','Controlled two-module fixture; broad project compatibility remains unqualified.','Repair workers are deterministic transport fixtures; no real model provider quality or cost measured.','One-command npm exec uses the same sealed local archive and isolated cache reused between controlled runs; no cold-start speed claim.']}; fs.mkdirSync(path.dirname(output),{recursive:true});fs.writeFileSync(output,JSON.stringify(receipt,null,2));console.log(JSON.stringify(receipt,null,2)); }finally{ + if(typedImprovement?.worktreeRoot)invoke(path.join(workspace,'type-oracle'),['git','worktree','remove','--force',typedImprovement.worktreeRoot]); if(improvement?.worktreeRoot)invoke(project,['git','worktree','remove','--force',improvement.worktreeRoot]); fs.rmSync(workspace,{recursive:true,force:true}); } diff --git a/src/pull-request.js b/src/pull-request.js index 48b1c71..c5f6939 100644 --- a/src/pull-request.js +++ b/src/pull-request.js @@ -2,11 +2,44 @@ import fs from 'node:fs'; import path from 'node:path'; import {spawnSync} from 'node:child_process'; import {renderRunReport} from './run-report.js'; -import {git,safePath} from './files.js'; -import {digest} from './provenance.js'; +import {git,safePath,readConfig} from './files.js'; +import {digest,freshness} from './provenance.js'; import {captureRepairChallenges,captureChallengeRuntime,assessRepairChallenges} from './repair-challenges.js'; import {verifyAppliedRepairValidation} from './repair-candidates.js'; import {summarizeTypecheck} from './typecheck-report.js'; +import {verifyTypecheck} from './typecheck.js'; +import {randomUUID} from 'node:crypto'; + +function savePullRequestBody(root,value){ + const file='.tddswarm/pull-request.md',target=safePath(root,file); + if(fs.existsSync(target)){const stat=fs.lstatSync(target);if(!stat.isFile()||stat.nlink!==1)throw Error('Pull request body must be an unlinked regular file');} + const temporary=safePath(root,file+'.'+randomUUID()+'.tmp');let fd; + try{ + fd=fs.openSync(temporary,fs.constants.O_WRONLY|fs.constants.O_CREAT|fs.constants.O_EXCL|fs.constants.O_NOFOLLOW,0o600); + if(!fs.fstatSync(fd).isFile()||fs.fstatSync(fd).nlink!==1)throw Error('Pull request body must be an unlinked regular file'); + fs.writeFileSync(fd,value);fs.closeSync(fd);fd=undefined;safePath(root,file);fs.renameSync(temporary,target);return target; + }finally{if(fd!==undefined)fs.closeSync(fd);fs.rmSync(temporary,{force:true});} +} + +function improvementTypecheckGuard(worktree,result){ + const settings=readConfig(worktree).plugins?.typescript,retained=result.fullRun?.typecheck||result.typecheck; + if(settings?.enabled!==true&&!retained&&!result.typecheckObligation)return false; + if(settings?.enabled!==true)throw Error('Test improvement TypeScript obligation changed before publication'); + const project=settings.project||'tsconfig.json',obligation=result.typecheckObligation; + if(obligation&&(obligation.enabled!==true||obligation.project!==project||obligation.timeoutMs!==(settings.timeoutMs||30000)))throw Error('Test improvement TypeScript obligation changed before publication'); + if(!summarizeTypecheck(retained).complete||retained.status!=='passed'||retained.project!==project)throw Error('Test improvement publication requires completed final TypeScript verification'); + // Re-execute the genuine installed oracle at publication boundaries. Advisory + // result JSON alone cannot authorize the current compiler check. + const current=verifyTypecheck(worktree,{project,timeoutMs:settings.timeoutMs||30000}); + if(!summarizeTypecheck(current).complete||current.status!=='passed')throw Error('Test improvement current TypeScript verification failed or is incomplete'); + if(!freshness(retained.binding.provenance,current.binding.provenance).fresh)throw Error('Test improvement native contract changed before publication'); + for(const key of ['source','dependencies','node','producers']){ + const previous=retained.binding?.[key],fresh=current.binding?.[key]; + if(digest(previous)!==digest(fresh))throw Error('Test improvement TypeScript inputs changed before publication'); + } + if(digest(current.compiler)!==digest(retained.compiler))throw Error('Test improvement TypeScript compiler changed before publication'); + return true; +} // Advisory reviewer approval and suite-only receipts cannot qualify source // repair publication. Reconcile actual controller observations, then rebind the @@ -62,6 +95,9 @@ export function publishImprovement(root, result, options = {}) { const worktree=result.worktree; const repair=result.kind==='source-repair'; if(repair)repairPublicationGuard(worktree,result); + else improvementTypecheckGuard(worktree,result); + const bodyTarget=safePath(worktree,'.tddswarm/pull-request.md'); + if(fs.existsSync(bodyTarget)){const stat=fs.lstatSync(bodyTarget);if(!stat.isFile()||stat.nlink!==1)throw Error('Pull request body must be an unlinked regular file');} if(!new RegExp(`^tddswarm/${repair?'repair':'improve'}-[a-zA-Z0-9][a-zA-Z0-9_-]{0,100}$`).test(result.branch))throw new Error('Invalid improvement branch'); if(result.sourceHead!==undefined&&!/^(?:[a-f0-9]{40}|[a-f0-9]{64})$/.test(result.sourceHead))throw new Error('Invalid original source head for pull request receipt'); const prefix=git(worktree,['rev-parse','--show-prefix']).trim(); @@ -70,10 +106,11 @@ export function publishImprovement(root, result, options = {}) { // Repair admission above authenticates the full installed tree at every // boundary. Physical typed-repair copies need the same metadata treatment // as shared installations without hiding a changed dependency byte. - if(repair&&(local==='node_modules'||local.startsWith('node_modules/'))){try{if(fs.lstatSync(path.join(worktree,'node_modules')).isDirectory())return true;}catch{}} + if((repair||result.typecheckObligation)&&(local==='node_modules'||local.startsWith('node_modules/'))){try{if(fs.lstatSync(path.join(worktree,'node_modules')).isDirectory())return true;}catch{}} if(local==='node_modules'){try{return fs.lstatSync(path.join(worktree,'node_modules')).isSymbolicLink()&&fs.realpathSync(path.join(worktree,'node_modules'))===fs.realpathSync(path.join(root,'node_modules'));}catch{}}return false;}; function exactState(){ if(repair)repairPublicationGuard(worktree,result); + else improvementTypecheckGuard(worktree,result); if(git(worktree,['rev-parse',`refs/heads/${result.branch}`]).trim()!==result.sha)throw new Error('Improvement branch changed after validation'); if(git(worktree,['rev-parse','HEAD']).trim()!==result.sha)throw new Error('Improvement commit changed after validation'); const status=git(worktree,['status','--porcelain=v1','-z','--untracked-files=all']).split('\0').filter(Boolean); @@ -119,7 +156,7 @@ export function publishImprovement(root, result, options = {}) { const introduction=repair?`TestLore proposes a bounded source repair against the original assertions.${investigation}\n\nThe original assertions were retained unchanged. Repeated baseline and candidate runs in disposable copies evaluated whether the source change repaired the observed failure; the repair branch then ran the full discovered suite before this commit. Independently maintained behavioral challenges passed twice on unchanged source, twice on the candidate, and again on the final branch. Their oracle, runtime and source bindings were rechecked before publication.\n\nSource changes: ${(result.files||result.changedFiles||result.patch?.files||[]).map(file=>typeof file==='string'?file:file.path).filter(Boolean).join(', ')||'see retained source-repair receipt'}. Baseline runs: ${(result.validation.baselineRuns||result.validation.baseline||[]).length||'see receipt'}; candidate runs: ${(result.validation.candidateRuns||[]).length||'see receipt'}. Challenges: ${result.finalChallenges.runs.map(item=>item.id).join(', ')}. The receipt binds the tested source and unchanged original assertions. A passing candidate does not establish behavior outside those observations.`:`TestLore proposes reviewed test improvements and an ongoing quality layer.\n\nThe original and candidate suites were validated in disposable copies; the improvement branch then ran the full discovered suite before this commit.`; const policy=repair?'The existing test and deployment policy is preserved. Review the source change and retained failure evidence before merging; unobserved dependencies and outcomes remain uncertain.':'New workflows run a scoped audit and shadow validation on pull requests, and full tests on default-branch pushes. Existing project policy is preserved. Static grades and exercised traces do not certify all possible behavior.'; const dedupeMarker=result.sourceHead?`\n\n`:''; - fs.writeFileSync(body,`${dedupeMarker}${introduction}\n\nValidation: ${summary} Commit ${result.sha}. See the retained local ${repair?'source-repair':'improvement'} receipt for exact scope, held-out checks and limitations.\n\n${policy}\n\n${renderRunReport({...full,executedFiles:full.executedFiles||full.collectionFiles,plan:full.plan||{mode:'full',total:full.executedFiles?.length||0,selected:full.executedFiles||full.collectionFiles||[],decisions:(full.executedFiles||full.collectionFiles||[]).map(test=>({test,reasons:[repair?'full source-repair validation':'full improvement validation']})),warnings:[]}})}\n`); + savePullRequestBody(worktree,`${dedupeMarker}${introduction}\n\nValidation: ${summary} Commit ${result.sha}. See the retained local ${repair?'source-repair':'improvement'} receipt for exact scope, held-out checks and limitations.\n\n${policy}\n\n${renderRunReport({...full,executedFiles:full.executedFiles||full.collectionFiles,plan:full.plan||{mode:'full',total:full.executedFiles?.length||0,selected:full.executedFiles||full.collectionFiles||[],decisions:(full.executedFiles||full.collectionFiles||[]).map(test=>({test,reasons:[repair?'full source-repair validation':'full improvement validation']})),warnings:[]}})}\n`); const output=existing||invoke(['pr','create','--repo',match[1],'--head',result.branch,'--base',base,'--title',options.title||(repair?'Repair the verified source failure with TestLore':'Improve test quality with TestLore'),'--body-file',body]); const pullRequest=output.split(/\s+/).find(s=>/^https:\/\/github\.com\/[^/]+\/[^/]+\/pull\/[1-9]\d*$/.test(s)); if(!pullRequest)throw new Error('GitHub CLI did not return a pull request URL; inspect the published branch before retrying'); diff --git a/test/improvement-publication-typecheck.test.js b/test/improvement-publication-typecheck.test.js new file mode 100644 index 0000000..7563557 --- /dev/null +++ b/test/improvement-publication-typecheck.test.js @@ -0,0 +1,37 @@ +import test from 'node:test'; +import assert from 'node:assert/strict'; +import fs from 'node:fs'; +import path from 'node:path'; +import {improve} from '../src/improvement.js'; +import {publishImprovement} from '../src/pull-request.js'; +import {fixture,write,commit,git} from './helpers.js'; + +test('typed test improvements use fresh publication evidence and preserve physical dependencies',async t=>{ + const root=fixture(t,{'package.json':{type:'module'},'.gitignore':'.tddswarm/\n', + 'src/value.ts':'export const value: number = 42;\n', + 'tsconfig.json':{compilerOptions:{strict:true,noEmit:true,module:'NodeNext',moduleResolution:'NodeNext',target:'ES2022'},files:['src/value.ts']}, + 'test/original.test.mjs':"import test from 'node:test';import assert from 'node:assert/strict';import {value} from '../src/value.ts';test('original contract',()=>assert.equal(value,42));", + 'tddswarm.config.json':{adapter:'node',runner:[process.execPath,'--experimental-strip-types','--no-warnings','--test','{files}'],plugins:{typescript:{enabled:true}}}}); + commit(root);fs.mkdirSync(path.join(root,'node_modules'));fs.cpSync(path.resolve('node_modules/typescript'),path.join(root,'node_modules/typescript'),{recursive:true}); + const content="import test from 'node:test';import assert from 'node:assert/strict';import {value} from '../src/value.ts';test('independent boundary',()=>assert.equal(value+1,43));"; + const result=await improve(root,{patch:{files:[{path:'test/additional.test.mjs',content}],requirements:'Independent contract: value is42 and adding1 yields43.',review:{accepted:true,findings:[],oracle:{independent:true,basis:['Independent public numeric contract.']}}},timeoutMs:10000,totalTimeoutMs:120000}); + t.after(()=>{try{git(root,'worktree','remove','--force',result.worktreeRoot);}catch{}try{git(root,'branch','-D',result.branch);}catch{}fs.rmSync(result.worktreeRoot,{recursive:true,force:true});}); + assert.equal(result.status,'ready-for-review',JSON.stringify({status:result.status,error:result.error,type:result.typecheck})); + assert.equal(result.fullRun.typecheck.status,'passed');assert.equal(result.fullRun.provenance.revision,result.sha); + const missing=structuredClone(result);delete missing.fullRun.typecheck;delete missing.typecheck; + assert.throws(()=>publishImprovement(root,missing),/completed final TypeScript verification/); + const wrong=structuredClone(result);wrong.fullRun.typecheck.compiler.sha256='0'.repeat(64);wrong.typecheck=wrong.fullRun.typecheck; + assert.throws(()=>publishImprovement(root,wrong),/compiler changed/); + const bare=fixture(t);git(bare,'init','--bare');git(root,'remote','add','origin','https://github.com/type-proof/project.git');git(root,'config',`url.${bare}.pushInsteadOf`,'https://github.com/type-proof/project.git'); + const requests=path.join(result.worktree,'.tddswarm/publication-requests.jsonl'),script=path.join(result.worktree,'.tddswarm/github.cjs'); + const metadata={url:'https://github.com/type-proof/project/pull/123',headRefOid:result.sha,headRefName:result.branch,baseRefName:result.baseBranch,state:'OPEN',isDraft:false}; + write(result.worktree,'.tddswarm/github.cjs',`const fs=require('fs');const a=process.argv.slice(2);fs.appendFileSync(${JSON.stringify(requests)},JSON.stringify(a)+'\\n');if(a[0]==='repo')console.log('{}');else if(a[1]==='create')console.log(${JSON.stringify(metadata.url)});else console.log(${JSON.stringify(JSON.stringify(metadata))});`); + const sdk=path.join(result.worktree,'node_modules/typescript/lib/typescript.js'),original=fs.readFileSync(sdk);fs.appendFileSync(sdk,'\n// changed installed compiler\n'); + assert.throws(()=>publishImprovement(root,result,{command:[process.execPath,script]}),/inputs changed|compiler changed/);assert.equal(fs.existsSync(requests),false);fs.writeFileSync(sdk,original); + const published=publishImprovement(root,result,{command:[process.execPath,script]}); + assert.equal(published.published,true);assert.equal(git(bare,'rev-parse',`refs/heads/${result.branch}`).trim(),result.sha); + assert.equal(fs.lstatSync(path.join(result.worktree,'node_modules')).isDirectory(),true);assert.equal(fs.readFileSync(path.join(root,'src/value.ts'),'utf8'),'export const value: number = 42;\n'); + assert.match(fs.readFileSync(path.join(result.worktree,'.tddswarm/pull-request.md'),'utf8'),/TypeScript checking: passed/); + // The Git push targets a temporary bare repository; the CLI above is a + // deterministic protocol fixture, not a live GitHub/model qualification. +}); diff --git a/test/installed-typecheck-evidence.test.js b/test/installed-typecheck-evidence.test.js index 74399a7..c6a852b 100644 --- a/test/installed-typecheck-evidence.test.js +++ b/test/installed-typecheck-evidence.test.js @@ -3,12 +3,13 @@ import assert from 'node:assert/strict'; import fs from 'node:fs'; import path from 'node:path'; import {createHash} from 'node:crypto'; -import {fixture,commit,write} from './helpers.js'; +import {fixture,commit,write,git} from './helpers.js'; import {run} from '../src/runner.js'; +import {improve} from '../src/improvement.js'; import {verifyInstalledTypecheckRawEvidence} from '../scripts/packed-evidence.js'; const hash=bytes=>createHash('sha256').update(bytes).digest('hex'); -test('native-backed raw compiler evidence rejects contradictory archive result claims',t=>{ +test('native-backed raw compiler evidence rejects contradictory archive result claims',async t=>{ const root=fixture(t,{'package.json':{type:'module'},'.gitignore':'node_modules/\n.tddswarm/\n', 'src/value.ts':'export const value: string = 42;\n', 'tsconfig.json':{compilerOptions:{strict:true,noEmit:true,module:'NodeNext',moduleResolution:'NodeNext',target:'ES2022'},files:['src/value.ts']}, @@ -17,6 +18,10 @@ test('native-backed raw compiler evidence rejects contradictory archive result c fs.mkdirSync(path.join(root,'node_modules'));fs.cpSync(path.resolve('node_modules/typescript'),path.join(root,'node_modules/typescript'),{recursive:true});commit(root); const reports={};reports['setup-fault']=run(root,{full:true,capture:true}); write(root,'src/value.ts','export const value: number = 42;\n');reports['healthy-shadow']=run(root,{full:true,capture:true}); + commit(root); + const improved=await improve(root,{patch:{files:[{path:'test/boundary.test.mjs',content:"import test from 'node:test';import assert from 'node:assert/strict';import {value} from '../src/value.ts';test('independent numeric boundary',()=>assert.equal(value+1,43));"}],requirements:'Independent numeric contract: value is42 and addingone yields43.',review:{accepted:true,findings:[],oracle:{independent:true,basis:['Independent public numeric contract.']}}}}); + t.after(()=>{try{git(root,'worktree','remove','--force',improved.worktreeRoot);}catch{}fs.rmSync(improved.worktreeRoot,{recursive:true,force:true});}); + assert.equal(improved.status,'ready-for-review',improved.error); write(root,'tsconfig.json',{compilerOptions:{noCheck:true,noEmit:true},files:['src/value.ts']});reports['unsupported-no-check']=run(root,{full:true,capture:true}); // Synthetic installation envelope only. These real source executions test // parser admission; packed-proof independently exercises the installed CLI. @@ -27,6 +32,8 @@ test('native-backed raw compiler evidence rejects contradictory archive result c envelope.outcomes.push({id,exitCode:report.exitCode,nativeExitCode:report.nativeExitCode,status:check.status,complete:check.complete, implementationCount:check.implementationFiles.length,errorCodes:check.diagnostics.filter(row=>row.category==='error').map(row=>row.code),rawSha256:hash(bytes)}); } + const improvedBytes=JSON.stringify(improved);fs.writeFileSync(path.join(directory,'improvement-ready.json'),improvedBytes); + envelope.improvement={status:improved.status,verified:improved.typecheckVerified,nativeCases:2,sha:improved.sha,sourceHead:improved.sourceHead,originalCallerUnchanged:true,originalAssertionsUnchanged:true,physicalDependencies:true,rawSha256:hash(improvedBytes)}; assert.equal(verifyInstalledTypecheckRawEvidence(envelope,directory),true); const healthy=reports['healthy-shadow']; for(const mutate of [r=>{r.typecheck.signal='SIGKILL';},r=>{r.typecheck.errorCode='ETIMEDOUT';},r=>{r.typecheck.exitCode=2;}, @@ -35,4 +42,10 @@ test('native-backed raw compiler evidence rejects contradictory archive result c fs.writeFileSync(path.join(directory,'healthy-shadow.json'),bytes);receipt.outcomes[1].rawSha256=hash(bytes); assert.equal(verifyInstalledTypecheckRawEvidence(receipt,directory),false); } + fs.writeFileSync(path.join(directory,'healthy-shadow.json'),JSON.stringify(healthy)); + for(const mutate of [r=>{r.fullRun.provenance.revision=r.sourceHead;},r=>{r.fullRun.typecheck.signal='SIGKILL';},r=>{r.typecheckVerified=false;},r=>{r.validation.qualityReady=false;},r=>{r.fullRun.tests.pop();},r=>{r.fullRun.tests[0].status='failed';},r=>{r.typecheckObligation.enabled=false;}]){ + const changed=structuredClone(improved);mutate(changed);const bytes=JSON.stringify(changed),receipt=structuredClone(envelope); + fs.writeFileSync(path.join(directory,'improvement-ready.json'),bytes);receipt.improvement.rawSha256=hash(bytes); + assert.equal(verifyInstalledTypecheckRawEvidence(receipt,directory),false); + } }); diff --git a/test/packed-evidence.test.js b/test/packed-evidence.test.js index 0125746..ba8f351 100644 --- a/test/packed-evidence.test.js +++ b/test/packed-evidence.test.js @@ -18,7 +18,7 @@ evidence.installedTypecheck={verified:true,productionInstalled:true,setupEnabled {id:'setup-fault',exitCode:1,nativeExitCode:0,status:'failed',complete:true,implementationCount:1,errorCodes:[2322],rawSha256:source}, {id:'healthy-shadow',exitCode:0,nativeExitCode:0,status:'passed',complete:true,implementationCount:1,errorCodes:[],rawSha256:source}, {id:'unsupported-no-check',exitCode:2,nativeExitCode:0,status:'incomplete',complete:false,implementationCount:0,errorCodes:[],rawSha256:source} -]}; +],improvement:{status:'ready-for-review',verified:true,nativeCases:2,sha:'4'.repeat(40),sourceHead:'5'.repeat(40),originalCallerUnchanged:true,originalAssertionsUnchanged:true,physicalDependencies:true,rawSha256:source}}; test('installed qualification requires explicit evidence for persisted shadow and generated Action identity', () => { assert.equal(packedEvidenceComplete(receipt, evidence, context), true); @@ -31,7 +31,8 @@ test('installed qualification requires explicit evidence for persisted shadow an test('packed admission requires native-green/type-failed, healthy and incomplete installed controls',()=>{ for(const mutation of [undefined,{...evidence.installedTypecheck,verified:'true'},{...evidence.installedTypecheck,providerCalls:1},{...evidence.installedTypecheck,outcomes:[]}, {...evidence.installedTypecheck,outcomes:evidence.installedTypecheck.outcomes.map(row=>({...row,nativeExitCode:1}))}, - {...evidence.installedTypecheck,outcomes:evidence.installedTypecheck.outcomes.map(row=>({...row,exitCode:0}))}]) + {...evidence.installedTypecheck,outcomes:evidence.installedTypecheck.outcomes.map(row=>({...row,exitCode:0}))}, + ...[undefined,{...evidence.installedTypecheck.improvement,verified:'true'},{...evidence.installedTypecheck.improvement,nativeCases:0},{...evidence.installedTypecheck.improvement,originalAssertionsUnchanged:false},{...evidence.installedTypecheck.improvement,sha:evidence.installedTypecheck.improvement.sourceHead}].map(improvement=>({...evidence.installedTypecheck,improvement}))]) assert.equal(packedEvidenceComplete(receipt,{...evidence,installedTypecheck:mutation},context),false); }); diff --git a/test/pull-request.test.js b/test/pull-request.test.js index 45a127d..cbc7906 100644 --- a/test/pull-request.test.js +++ b/test/pull-request.test.js @@ -27,6 +27,15 @@ test('automatic PR publication uses actual Git push and structured GitHub CLI ar const requests=fs.readFileSync(path.join(root,'.tddswarm/requests.jsonl'),'utf8').trim().split('\n').map(JSON.parse);assert.deepEqual(requests[1].slice(0,8),['pr','create','--repo','test-owner/project','--head',result.branch,'--base','main']); assert.deepEqual(requests[2],['pr','view',published.pullRequest,'--repo','test-owner/project','--json','url,headRefOid,headRefName,baseRefName,state,isDraft']); }); +test('linked publication bodies cannot overwrite source or start external publication',t=>{ + const {root,bare,result}=publicationFixture(t),cli=fakeGithub(root,result),source=path.join(root,'src/a.js'),original=fs.readFileSync(source),body=path.join(root,'.tddswarm/pull-request.md'); + for(const link of ['symbolic','hard']){ + fs.rmSync(body,{force:true});if(link==='symbolic')fs.symlinkSync('../src/a.js',body);else fs.linkSync(source,body); + assert.throws(()=>publishImprovement(root,result,{command:[process.execPath,cli]}),/Symlink path is unsupported|unlinked regular file/); + assert.deepEqual(fs.readFileSync(source),original);assert.equal(fs.existsSync(path.join(root,'.tddswarm/requests.jsonl')),false); + assert.throws(()=>git(bare,'rev-parse',`refs/heads/${result.branch}`));fs.rmSync(body); + } +}); test('verified shared dependency metadata does not block an otherwise clean improvement publication',async t=>{ From b80e34a612b2ac921c1dee45a15a8d09090ad884 Mon Sep 17 00:00:00 2001 From: Rudy Celekli Date: Tue, 13 Oct 2026 00:41:26 -0400 Subject: [PATCH 11/16] Require actual collection scope in installed typed improvement evidence --- scripts/packed-evidence.js | 6 ++++-- test/installed-typecheck-evidence.test.js | 2 +- 2 files changed, 5 insertions(+), 3 deletions(-) diff --git a/scripts/packed-evidence.js b/scripts/packed-evidence.js index 26f8fee..606f1d6 100644 --- a/scripts/packed-evidence.js +++ b/scripts/packed-evidence.js @@ -22,13 +22,15 @@ function verifyInstalledTypedImprovement(value,directory){ const bytes=fs.readFileSync(file);if(createHash('sha256').update(bytes).digest('hex')!==row.rawSha256)return false; const result=JSON.parse(bytes),report=result.fullRun,check=report?.typecheck,typed=summarizeTypecheck(check,{expectedProvenance:report?.provenance}); const expected=[['test/boundary.test.mjs','independent numeric boundary'],['test/value.test.mjs','ordinary value']]; + const files=expected.map(([file])=>file),exactFiles=value=>Array.isArray(value)&&JSON.stringify([...value].sort())===JSON.stringify(files); const cases=report?.tests?.map(test=>[test.file,test.name]).sort((a,b)=>a[0].localeCompare(b[0])); return result.status==='ready-for-review'&&result.typecheckVerified===true&&result.sha===row.sha&&result.sourceHead===row.sourceHead&&result.base===row.sourceHead&&result.merged===false&& result.typecheckObligation?.enabled===true&&result.typecheckObligation.project==='tsconfig.json'&&result.typecheckObligation.timeoutMs===30000&&result.validation?.accepted===true&&result.validation.qualityReady===true&& report?.provenance?.revision===row.sha&&report.complete===true&&report.nativeComplete===true&&report.exitCode===0&&report.nativeExitCode===0&&!report.signal&&!report.error&&!report.reportErrors?.length&&!report.missingFiles?.length&&!report.unknownFiles?.length&& typed.complete&&typed.status==='passed'&&check.exitCode===0&&check.compiler?.version===value.compilerVersion&&check.implementationFiles?.length===1&&JSON.stringify(result.typecheck)===JSON.stringify(check)&& - JSON.stringify(cases)===JSON.stringify(expected)&&report.tests.every(test=>typeof test.id==='string'&&test.id&&test.status==='passed')&& - report.discovery?.complete===true&&JSON.stringify([...report.discovery.files].sort())===JSON.stringify(expected.map(([file])=>file)); + JSON.stringify(cases)===JSON.stringify(expected)&&report.tests.every(test=>typeof test.id==='string'&&test.id&&test.status==='passed')&&new Set(report.tests.map(test=>test.id)).size===2&& + !report.actualExecutedFilesUnverified&&exactFiles(report.collectionFiles)&&exactFiles(report.requestedFiles)&&exactFiles(report.executedFiles)&& + Array.isArray(result.missingCases)&&result.missingCases.length===0&&report.discovery?.complete===true&&exactFiles(report.discovery.files); } export function verifyInstalledTypecheckRawEvidence(value,directory){ if(!installedTypecheckEvidenceComplete(value))return false; diff --git a/test/installed-typecheck-evidence.test.js b/test/installed-typecheck-evidence.test.js index c6a852b..fd12ddd 100644 --- a/test/installed-typecheck-evidence.test.js +++ b/test/installed-typecheck-evidence.test.js @@ -43,7 +43,7 @@ test('native-backed raw compiler evidence rejects contradictory archive result c assert.equal(verifyInstalledTypecheckRawEvidence(receipt,directory),false); } fs.writeFileSync(path.join(directory,'healthy-shadow.json'),JSON.stringify(healthy)); - for(const mutate of [r=>{r.fullRun.provenance.revision=r.sourceHead;},r=>{r.fullRun.typecheck.signal='SIGKILL';},r=>{r.typecheckVerified=false;},r=>{r.validation.qualityReady=false;},r=>{r.fullRun.tests.pop();},r=>{r.fullRun.tests[0].status='failed';},r=>{r.typecheckObligation.enabled=false;}]){ + for(const mutate of [r=>{r.fullRun.provenance.revision=r.sourceHead;},r=>{r.fullRun.typecheck.signal='SIGKILL';},r=>{r.typecheckVerified=false;},r=>{r.validation.qualityReady=false;},r=>{r.fullRun.tests.pop();},r=>{r.fullRun.tests[0].status='failed';},r=>{r.typecheckObligation.enabled=false;},r=>{r.fullRun.collectionFiles=[];},r=>{r.fullRun.requestedFiles=['test/value.test.mjs'];},r=>{r.fullRun.executedFiles=[];},r=>{r.fullRun.actualExecutedFilesUnverified=true;},r=>{r.fullRun.tests[1].id=r.fullRun.tests[0].id;},r=>{r.missingCases=['ordinary value'];}]){ const changed=structuredClone(improved);mutate(changed);const bytes=JSON.stringify(changed),receipt=structuredClone(envelope); fs.writeFileSync(path.join(directory,'improvement-ready.json'),bytes);receipt.improvement.rawSha256=hash(bytes); assert.equal(verifyInstalledTypecheckRawEvidence(receipt,directory),false); From 08761fe175be97292bb5ee0f227202e3d6a6f949 Mon Sep 17 00:00:00 2001 From: Rudy Celekli Date: Tue, 13 Oct 2026 00:58:31 -0400 Subject: [PATCH 12/16] Synchronize descendant cleanup control with actual OS observation --- test/vite-react-native-profile.test.js | 13 ++++++++++++- 1 file changed, 12 insertions(+), 1 deletion(-) diff --git a/test/vite-react-native-profile.test.js b/test/vite-react-native-profile.test.js index 9d15a73..ab90c30 100644 --- a/test/vite-react-native-profile.test.js +++ b/test/vite-react-native-profile.test.js @@ -11,4 +11,15 @@ test('all pending build assertions cannot establish execution, even with matchin test('private npm configurations are distinct; credential-bearing environment entries are excluded',t=>{const root=directory(t),env=profileEnvironment(root,{GH_TOKEN:'secret',NPM_TOKEN:'secret',NODE_OPTIONS:'--require bad',LANG:'C'});assert.notEqual(env.NPM_CONFIG_USERCONFIG,env.NPM_CONFIG_GLOBALCONFIG);assert.equal(env.GH_TOKEN,undefined);assert.equal(env.NODE_OPTIONS,undefined);assert.equal(env.LANG,'C');assert.throws(()=>parseProfileArguments(['--run']));assert.throws(()=>parseProfileArguments(['--run','--output',root,'--project','/tmp/other']));}); function context(output){const manifest=profileManifest();return {output,project:output,env:{PATH:path.dirname(process.execPath)+':/usr/bin:/bin'},manifest:{...manifest,resources:{...manifest.resources,stopFreeBytes:0,maximumWorkspaceBytes:1024**3,maximumPhaseOutputBytes:16,resourcePollMs:100}},report:{phases:[]}};} test('a fast native zero exit cannot bypass output or final deadline checks',async t=>{const output=directory(t),c=context(output);await assert.rejects(()=>phase(c,'overflow',[process.execPath,'-e','process.stdout.write("01234567890123456789")']),/phase-output-limit/);await assert.rejects(()=>phase(c,'deadline',[process.execPath,'-e',''],1),/phase-deadline/);assert.equal(c.report.phases.length,2);assert.ok(c.report.phases.every(r=>r.status==='failed'));}); -test('an observed surviving descendant rejects a successful parent and cleanup is retained',async t=>{const output=directory(t),c=context(output);c.manifest.resources.maximumPhaseOutputBytes=1024;const script='const {spawn}=require("child_process");const child=spawn(process.execPath,["-e","setInterval(()=>{},1000)"],{stdio:"ignore",detached:true});console.log(child.pid);setTimeout(()=>process.exit(0),350)';await assert.rejects(()=>phase(c,'survivor',[process.execPath,'-e',script]),/surviving-descendants/);const record=c.report.phases[0];assert.equal(record.exitCode,0);assert.ok(record.descendantCleanup.observed>0);assert.ok(record.descendantCleanup.survivingAtClose>0);}); +test('an observed surviving descendant rejects a successful parent and cleanup is retained',async t=>{ + const output=directory(t),c=context(output);c.manifest.resources.maximumPhaseOutputBytes=1024; + const identity=path.join(output,'descendant.json'),observed=path.join(output,'observed'),commands=path.join(output,'commands');fs.mkdirSync(commands); + // Hold the real parent until a real native ps sample contains its child. + // The shim forwards the untouched OS rows; it fabricates no observation. + fs.writeFileSync(path.join(commands,'ps'),`#!${process.execPath}\nconst fs=require('fs'),{spawnSync}=require('child_process');const r=spawnSync('ps',process.argv.slice(2),{env:{PATH:'/usr/bin:/bin'},encoding:'utf8',timeout:800});if(r.status===0&&fs.existsSync(${JSON.stringify(identity)})){const p=JSON.parse(fs.readFileSync(${JSON.stringify(identity)}));if(r.stdout.split('\\n').some(line=>{const f=line.trim().split(/\\s+/);return Number(f[0])===p.child&&Number(f[1])===p.parent;}))fs.writeFileSync(${JSON.stringify(observed)},'observed');}process.stdout.write(r.stdout||'');process.stderr.write(r.stderr||'');process.exit(r.status??2);`,{mode:0o755}); + c.env.PATH=commands+':'+c.env.PATH; + const script=`const fs=require('fs'),{spawn}=require('child_process');const child=spawn(process.execPath,['-e','setInterval(()=>{},1000);setTimeout(()=>process.exit(0),20000).unref();'],{stdio:'ignore',detached:true});fs.writeFileSync(${JSON.stringify(identity)},JSON.stringify({parent:process.pid,child:child.pid}));console.log(child.pid);const timer=setInterval(()=>{if(fs.existsSync(${JSON.stringify(observed)})){clearInterval(timer);process.exit(0);}},25);setTimeout(()=>{child.kill('SIGKILL');process.exit(2);},10000).unref();`; + try{await assert.rejects(()=>phase(c,'survivor',[process.execPath,'-e',script],15000),/surviving-descendants/);} + finally{if(fs.existsSync(identity)){const {child}=JSON.parse(fs.readFileSync(identity));try{process.kill(-child,'SIGKILL');}catch{}}} + const record=c.report.phases[0];assert.equal(record.exitCode,0);assert.ok(record.descendantCleanup.observed>0);assert.ok(record.descendantCleanup.survivingAtClose>0);assert.equal(record.descendantCleanup.requested,true); +}); From cd7bba56873b98f8f70faefb26ba4e430efbb708 Mon Sep 17 00:00:00 2001 From: Rudy Celekli Date: Tue, 13 Oct 2026 01:17:21 -0400 Subject: [PATCH 13/16] Bound publication forgery setup independently from deadline controls --- test/repair-publication-validation.test.js | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/test/repair-publication-validation.test.js b/test/repair-publication-validation.test.js index 50d5949..de61275 100644 --- a/test/repair-publication-validation.test.js +++ b/test/repair-publication-validation.test.js @@ -32,7 +32,9 @@ test('legacy direct suite-only validation cannot qualify applied production publ const supplied={kind:'source-repair',status:'ready-for-review',candidate,validation:result,fullRun:{complete:true,exitCode:0,tests:[{status:'passed'}]},sha:git(root,'rev-parse','HEAD').trim(),branch:'tddswarm/repair-forged',worktree:root};assert.throws(()=>publishImprovement(root,supplied),/repair challenges|regression challenges|protected project/); }); test('a forged public validation object never reaches remote authentication or branch push',async t=>{ - const root=project(t),result=await repair(root,{patch:{files:[{path:'src/a.js',content:'export const a=1;'}],review},sourcePaths:['src/a.js'],deadlineMs:30000});t.after(()=>fs.rmSync(result.worktreeRoot,{recursive:true,force:true}));assert.equal(result.status,'ready-for-review',JSON.stringify(result)); + // This positive setup qualifies publication authentication, not campaign + // timeout behavior. Keep its budget bounded while allowing full-suite load. + const root=project(t),result=await repair(root,{patch:{files:[{path:'src/a.js',content:'export const a=1;'}],review},sourcePaths:['src/a.js'],deadlineMs:60000});t.after(()=>fs.rmSync(result.worktreeRoot,{recursive:true,force:true}));assert.equal(result.status,'ready-for-review',JSON.stringify(result)); const fake=structuredClone(result);fake.validation.challenges.candidate[0].runs[0].observed.assertions++; assert.throws(()=>publishImprovement(root,fake),/authenticated|Authenticated|validation.*differ|receipt.*differ/); }); From 82c9048a43fdc7bf39e395e0879fc5e98fec0b48 Mon Sep 17 00:00:00 2001 From: Rudy Celekli Date: Tue, 13 Oct 2026 01:39:38 -0400 Subject: [PATCH 14/16] Prevent result receipt aliases from clobbering verified source --- src/cli.js | 16 +++++------ src/investigation-repair.js | 5 ++-- src/repair.js | 3 ++- src/result-receipt.js | 50 ++++++++++++++++++++++++++++++++++ test/result-receipt.test.js | 54 +++++++++++++++++++++++++++++++++++++ 5 files changed, 117 insertions(+), 11 deletions(-) create mode 100644 src/result-receipt.js create mode 100644 test/result-receipt.test.js diff --git a/src/cli.js b/src/cli.js index dfca908..9be6303 100755 --- a/src/cli.js +++ b/src/cli.js @@ -19,9 +19,9 @@ const commandModules = { pilot: ['pilot'], 'pilot-export': ['pilot'], plugins: ['files', 'plugins', 'plugin-recommendations'], assess: ['assessments'], campaign: ['community-campaign'], - improve: ['files', 'agent-profile', 'improvement', 'quality-layer', 'pull-request'], - repair: ['files', 'repair', 'pull-request'], - autopilot: ['files', 'repair', 'improvement', 'pull-request'], + improve: ['files', 'agent-profile', 'improvement', 'quality-layer', 'pull-request', 'result-receipt'], + repair: ['files', 'repair', 'pull-request', 'result-receipt'], + autopilot: ['files', 'repair', 'improvement', 'pull-request', 'result-receipt'], agent: ['agent-profile'], learn: ['learning'], recall: ['learning'], 'learning-export': ['learning'], 'knowledge-import': ['contribution-knowledge'], knowledge: ['contribution-knowledge', 'files'], 'knowledge-reflect': ['contribution-knowledge'], @@ -29,7 +29,7 @@ const commandModules = { investigate: ['bug-investigation'], immunity: ['bug-investigation'], 'investigation-outcome': ['investigation-outcomes'], 'investigation-links': ['investigation-outcomes'], 'investigation-learn': ['investigation-outcomes'], - 'investigation-repair': ['investigation-repair','files','pull-request'], + 'investigation-repair': ['investigation-repair','files','pull-request','result-receipt'], 'probe-catalog': ['bug-catalog'], snapshot: ['files', 'provenance'], evidence: ['evidence'], mutation: ['files', 'quality-measurement', 'evidence'], effectiveness: ['files', 'quality-measurement'], @@ -230,7 +230,7 @@ function human(command, result, implementations = {}) { if (command === 'init') return `${result.created ? 'Created' : 'Kept'} ${result.config}\n${human('audit', result.report)}\nNext: ${result.next.join(' → ')}`; if (command === 'audit') return `${result.testFiles} test files · static triage grade ${result.grade}${result.score === null ? '' : ` (${result.score}/100)`}\n${Object.entries(result.measured).map(([key,value])=>`${key}: ${value?'measured (see JSON scope)':'not measured'}`).join(' · ')}\n${result.files.flatMap(f => f.findings.map(x => ` ${f.file}:${x.line} — ${x.message}`)).join('\n')}\n${result.sourcesWithoutImportingTests.length} source files have no importing tests (not a coverage result).`; if (command === 'plan') return `${result.mode.toUpperCase()} · ${result.selected.length}/${result.total} test files selected\n${result.reasons.length ? `Reasons: ${result.reasons.join(', ')}\n` : ''}${result.decisions.map(d => `${d.selected ? 'RUN ' : 'SKIP'} ${d.test} — ${d.reasons.join(', ')}${d.paths.length ? `\n ${d.paths.map(p => p.join(' → ')).join('\n ')}` : ''}`).join('\n')}\nStatic evidence; runtime dependencies require declarations.`; - if(['improve','repair','autopilot','investigation-repair'].includes(command))return `${result.status} · ${result.branch||result.kind||command}\n${result.worktree||''}\n${result.pullRequest||result.publicationError||result.error||result.next?.join('\n')||'Review the retained improvement receipt.'}`; + if(['improve','repair','autopilot','investigation-repair'].includes(command))return `${result.status} · ${result.branch||result.kind||command}\n${result.worktree||''}\n${result.status==='receipt-failed'?[result.pullRequest&&`Confirmed pull request: ${result.pullRequest}`,result.receiptError||result.error].filter(Boolean).join('\n'):result.pullRequest||result.publicationError||result.error||result.next?.join('\n')||'Review the retained improvement receipt.'}`; if(command==='probe-catalog')return `${result.entries.length} qualified historical recipes · ${result.directory||'read-only catalog'}\n${result.entries.map(row=>`${row.id} — ${row.contract}`).join('\n')}\n${result.nextAction}`; if(command==='investigate'||command==='immunity')return `${result.kind} · ${result.status||'observation'}\n${result.summary?Object.entries(result.summary).map(([key,value])=>`${key}: ${value}`).join(' · '):result.coverage?`${result.coverage.approvedInvestigations} approved probes · ${result.coverage.unresolvedInvestigations} unresolved`:result.candidates?`${result.candidates.filter(row=>row.applicable).length}/${result.candidates.length} approved probes`:'No reviewed probe scope yet.'}\n${result.receipt||''}\n${result.nextAction}\nNo universal coverage or automatic learning improvement is established.`; if (command === 'modules') return `${result.groups.map(g => `${g.name}: ${g.tests.join(', ')}`).join('\n')}\nProposal only; no tests rewritten.`; @@ -315,7 +315,7 @@ export async function main(args = process.argv.slice(2)) { let patch;if(options.patch){const {readInvestigationInput}=await import('./bug-investigation.js');patch=readInvestigationInput(root,options.patch,{maxBytes:131072}).value;} result=await implementations.repairInvestigation(root,{report:options.report,reportSha256:options.sha256,findingId:options.id,agent,patch,deadlineMs:options['deadline-ms']===undefined?undefined:Number(options['deadline-ms'])}); if(result.status==='ready-for-review'&&!options.local){try{result=implementations.publishImprovement(root,result,{baseBranch:options['base-branch']});}catch(error){result={...result,published:false,publicationError:error.message};}} - if(result.receipt)fs.writeFileSync(result.receipt,JSON.stringify(result,null,2));break; + if(result.receipt)result=implementations.persistResultReceipt(result.worktree||root,result);break; } case 'knowledge-import': result=implementations.importContributionKnowledge(root,options.report,{expectedSha256:options.sha256});break; case 'knowledge-reflect': result=implementations.reflectContributionKnowledge(root);break; @@ -406,7 +406,7 @@ export async function main(args = process.argv.slice(2)) { } if(options.provider&&result.kind==='source-repair')result.generationProviderRequest={provider:options.provider,applied:false,reason:'Author-provider selection applies to test generation. Source repair retains dedicated JSON-worker roles.'}; if(result.status==='ready-for-review'&&!options.local){try{result=publishImprovement(root,result,{baseBranch:options['base-branch']});}catch(error){result={...result,published:false,publicationError:error.message};}} - if(result.receipt)fs.writeFileSync(result.receipt,JSON.stringify(result,null,2)); + if(result.receipt)result=implementations.persistResultReceipt(result.worktree||root,result); break; } case 'report': result=JSON.parse(fs.readFileSync(safePath(root,options.report||'.tddswarm/last-run.json'),'utf8'));if(!options.json){console.log(renderRunReport(result));return result.exitCode||0;}break; @@ -460,7 +460,7 @@ export async function main(args = process.argv.slice(2)) { try{result=publishImprovement(root,result,{baseBranch:options['base-branch']});} catch(error){result={...result,published:false,publicationError:error.message};} } - if(result.receipt)fs.writeFileSync(result.receipt,JSON.stringify(result,null,2)); + if(result.receipt)result=implementations.persistResultReceipt(result.worktree||root,result); break; } case 'agent': result=ensureQualityAgent(root,{name:options.name});break; diff --git a/src/investigation-repair.js b/src/investigation-repair.js index ed2229c..c861e7a 100644 --- a/src/investigation-repair.js +++ b/src/investigation-repair.js @@ -1,3 +1,4 @@ +import {persistResultReceipt} from './result-receipt.js'; import fs from 'node:fs'; import os from 'node:os'; import path from 'node:path'; @@ -102,7 +103,7 @@ export async function repairInvestigation(root, options = {}) { }; const outer = fs.mkdtempSync(path.join(os.tmpdir(),'testlore-investigation-repair-')); const id = randomUUID(); let result = {schemaVersion:1,kind:'source-repair',id,originalCheckout:root,sourceHead:base,base,baseBranch,worktree:outer,worktreeRoot:outer,sha:null,status:'preparing',published:false,merged:false,sourcePaths:[finding.module],investigation:{report:options.report,reportSha256:options.reportSha256,findingId:finding.id,recipeId:finding.recipeId,recipeSha256:finding.recipeSha256,regressionTest:added.path,regressionSha256:digest(added.content),originalGreenRuns:[],regressionFailureRuns:[],deadlineMs,scope:'standalone-node-approved-module-probe-to-native-regression',limitations:['Target approval is a reviewed declaration; this controller does not independently authenticate the reviewer.','A new passing regression does not certify behavior outside the observed native and module scopes.']}}; - const save = () => { remaining(); const directory = safePath(result.worktree,'.tddswarm/investigation-repair'); fs.mkdirSync(directory,{recursive:true,mode:0o700}); result.receipt = path.join(directory,'result.json'); fs.writeFileSync(result.receipt,JSON.stringify(result,null,2)+'\n',{mode:0o600}); return result; }; + const save = () => { remaining(); return persistResultReceipt(result.worktree,result,'.tddswarm/investigation-repair/result.json'); }; try { assertCaller(); git(root,['worktree','add','--detach',outer,base]); const dependencies = path.join(root,'node_modules'); if (fs.existsSync(dependencies) && !fs.existsSync(path.join(outer,'node_modules'))) fs.symlinkSync(fs.realpathSync(dependencies),path.join(outer,'node_modules'),'dir'); @@ -144,7 +145,7 @@ export async function repairInvestigation(root, options = {}) { assertCaller(); return save(); } catch (error) { result.status='failed'; result.error=error.message; result.published=false; result.merged=false; - if (fs.existsSync(path.join(result.worktree,'.git'))) { const directory=safePath(result.worktree,'.tddswarm/investigation-repair');fs.mkdirSync(directory,{recursive:true,mode:0o700});result.receipt=path.join(directory,'result.json');fs.writeFileSync(result.receipt,JSON.stringify(result,null,2)+'\n',{mode:0o600}); } + if (fs.existsSync(path.join(result.worktree,'.git'))) persistResultReceipt(result.worktree,result,'.tddswarm/investigation-repair/result.json'); else fs.rmSync(outer,{recursive:true,force:true}); return result; } diff --git a/src/repair.js b/src/repair.js index 4c1d021..f37d6f0 100644 --- a/src/repair.js +++ b/src/repair.js @@ -10,6 +10,7 @@ import {proposeRepair} from './repair-swarm.js'; import {withRepairEnvironment} from './repair-environment.js'; import {captureRepairChallenges,runRepairChallenges,captureChallengeRuntime} from './repair-challenges.js'; import {captureTypecheckInputs,verifyTypecheck} from './typecheck.js'; +import {persistResultReceipt} from './result-receipt.js'; function changes(root){ const entries=git(root,['status','--porcelain=v1','-z','--untracked-files=all']).split('\0').filter(Boolean),result=[]; @@ -90,7 +91,7 @@ export async function repair(root,options={}){ let baseBranch=null;try{baseBranch=git(repository,['symbolic-ref','--short','HEAD']).trim();}catch{} const id=randomUUID(),branch=`tddswarm/repair-${id}`,worktreeRoot=fs.mkdtempSync(path.join(os.tmpdir(),'testlore-repair-')),worktree=prefix?path.join(worktreeRoot,prefix):worktreeRoot; const result={schemaVersion:1,kind:'source-repair',id,branch,worktreeRoot,worktree,originalCheckout:root,sourceHead:base,base,baseBranch,sha:null,status:'preparing',published:false,merged:false,budget:{deadlineMs,timeoutMs,maxTasks:3,maxFiles:3,maxPatchBytes:65536,maxRounds:1},sourcePaths}; - const receipt=()=>{const directory=safePath(worktree,'.tddswarm/repair');fs.mkdirSync(directory,{recursive:true});result.receipt=path.join(directory,'result.json');fs.writeFileSync(result.receipt,JSON.stringify(result,null,2));return result;}; + const receipt=()=>persistResultReceipt(worktree,result,'.tddswarm/repair/result.json'); try{ git(repository,['worktree','add','-b',branch,worktreeRoot,base]); if(originalContribution){ diff --git a/src/result-receipt.js b/src/result-receipt.js new file mode 100644 index 0000000..676de0b --- /dev/null +++ b/src/result-receipt.js @@ -0,0 +1,50 @@ +import fs from 'node:fs'; +import path from 'node:path'; +import {randomUUID} from 'node:crypto'; + +// Metadata is never permission to follow a link into verified source. This is +// an atomic local writer, not an OS sandbox against concurrent hostile actors. +function ancestors(root,target,{create=false}={}){ + const rows=[];let current=root; + const rootStat=fs.lstatSync(root); + if(!rootStat.isDirectory()||rootStat.isSymbolicLink())throw Error('Receipt root must be a real directory'); + rows.push([root,rootStat.dev,rootStat.ino]); + for(const part of path.relative(root,path.dirname(target)).split(path.sep).filter(Boolean)){ + current=path.join(current,part); + let stat;try{stat=fs.lstatSync(current);}catch(error){if(error.code!=='ENOENT'||!create)throw error;fs.mkdirSync(current,{mode:0o700});stat=fs.lstatSync(current);} + if(!stat.isDirectory()||stat.isSymbolicLink())throw Error('Receipt ancestors must be real directories'); + rows.push([current,stat.dev,stat.ino]); + } + return rows; +} +function leaf(target){ + try{const stat=fs.lstatSync(target);if(!stat.isFile()||stat.isSymbolicLink()||stat.nlink!==1)throw Error('Receipt must be an unlinked regular file');return [stat.dev,stat.ino,stat.size,stat.mtimeMs,stat.ctimeMs];} + catch(error){if(error.code==='ENOENT')return null;throw error;} +} +function identical(left,right){return JSON.stringify(left)===JSON.stringify(right);} +export function writeResultReceipt(root,result,relative){ + const declaredRoot=path.resolve(root);root=fs.realpathSync(declaredRoot); + const lexical=relative??path.relative(declaredRoot,path.resolve(result.receipt||'')); + const declared=relative??(lexical.startsWith('.tddswarm/')?lexical:path.relative(root,path.resolve(result.receipt||''))); + if(typeof declared!=='string'||/[\x00-\x1f\x7f\\]/.test(declared)||!declared.startsWith('.tddswarm/')||declared.split('/').some(part=>!part||part==='.'||part==='..')||!declared.endsWith('.json')||declared.length>1024)throw Error('Receipt must identify a bounded local metadata JSON path'); + const target=path.join(root,declared),chain=ancestors(root,target,{create:true}),previous=leaf(target); + const bytes=JSON.stringify({...result,receipt:target},null,2)+'\n'; + if(Buffer.byteLength(bytes)>32*1024*1024)throw Error('Receipt exceeds the 32 MiB bound'); + const temporary=path.join(path.dirname(target),`.result-${randomUUID()}.tmp`);let fd; + try{ + fd=fs.openSync(temporary,fs.constants.O_WRONLY|fs.constants.O_CREAT|fs.constants.O_EXCL|fs.constants.O_NOFOLLOW,0o600); + const stat=fs.fstatSync(fd);if(!stat.isFile()||stat.nlink!==1)throw Error('Receipt temporary must be an unlinked regular file'); + fs.writeFileSync(fd,bytes);fs.closeSync(fd);fd=undefined; + if(!identical(chain,ancestors(root,target))||!identical(previous,leaf(target)))throw Error('Receipt path changed during storage'); + const named=fs.lstatSync(temporary);if(!named.isFile()||named.nlink!==1||named.dev!==stat.dev||named.ino!==stat.ino)throw Error('Receipt temporary changed during storage'); + fs.renameSync(temporary,target);result.receipt=target;return result; + }finally{if(fd!==undefined)fs.closeSync(fd);fs.rmSync(temporary,{force:true});} +} +export function persistResultReceipt(root,result,relative){ + try{return writeResultReceipt(root,result,relative);} + catch{ + // Keep actual publication facts: storage failure does not undo a created PR. + result.status='receipt-failed';result.receipt=null;result.receiptError='Result receipt could not be stored safely; inspect metadata links and retain the returned result before retrying.'; + result.error=result.receiptError;result.nextAction=result.receiptError;result.exitCode=2;result.qualityReady=false;result.typecheckVerified=false;return result; + } +} diff --git a/test/result-receipt.test.js b/test/result-receipt.test.js new file mode 100644 index 0000000..917d22e --- /dev/null +++ b/test/result-receipt.test.js @@ -0,0 +1,54 @@ +import test from 'node:test'; +import assert from 'node:assert/strict'; +import fs from 'node:fs'; +import path from 'node:path'; +import {spawnSync} from 'node:child_process'; +import {fileURLToPath} from 'node:url'; +import {persistResultReceipt,writeResultReceipt} from '../src/result-receipt.js'; +import {repair} from '../src/repair.js'; +import {fixture,write,commit,git,twoModules} from './helpers.js'; +import {challengeFiles} from './repair-challenge-fixtures.js'; +const cli=fileURLToPath(new URL('../src/cli.js',import.meta.url)); +const requirements='Module a exports1; independent module b remains2.'; +function cleanWorktree(t,root,result){if(result.worktreeRoot)t.after(()=>{try{git(root,'worktree','remove','--force',result.worktreeRoot);}catch{}fs.rmSync(result.worktreeRoot,{recursive:true,force:true});});} +const nativeEnv=()=>{const env={...process.env};delete env.NODE_TEST_CONTEXT;delete env.NODE_OPTIONS;return env;}; +test('atomic result receipts replace healthy metadata and preserve confirmed publication facts on refusal',t=>{ + const root=fixture(t,{'src/value.js':'verified source'}),result={status:'ready-for-review',published:true,pullRequest:'https://github.com/test-owner/project/pull/123',pullRequestVerification:{headRefOid:'a'.repeat(40)}}; + persistResultReceipt(root,result,'.tddswarm/improvement/result.json');const receipt=result.receipt; + assert.equal(JSON.parse(fs.readFileSync(receipt)).published,true);result.receipt=path.join(root,'.tddswarm/improvement/result.json');persistResultReceipt(root,result);assert.equal(result.status,'ready-for-review'); + fs.unlinkSync(receipt);fs.symlinkSync('../../src/value.js',receipt);persistResultReceipt(root,result); + assert.equal(result.status,'receipt-failed');assert.equal(result.receipt,null);assert.equal(result.published,true);assert.match(result.pullRequest,/pull\/123$/);assert.equal(result.pullRequestVerification.headRefOid,'a'.repeat(40));assert.equal(result.exitCode,2);assert.equal(fs.readFileSync(path.join(root,'src/value.js'),'utf8'),'verified source'); +}); +test('receipt paths refuse hardlinks, linked ancestors, special leaves, escapes, control paths and oversized metadata',t=>{ + const root=fixture(t,{'src/value.js':'verified source'}),source=path.join(root,'src/value.js'); + write(root,'.tddswarm/receipt/result.json','old');const leaf=path.join(root,'.tddswarm/receipt/result.json');fs.unlinkSync(leaf);fs.linkSync(source,leaf); + assert.throws(()=>writeResultReceipt(root,{},'.tddswarm/receipt/result.json'),/unlinked regular/);fs.unlinkSync(leaf);fs.mkdirSync(leaf);assert.throws(()=>writeResultReceipt(root,{},'.tddswarm/receipt/result.json'),/unlinked regular/);fs.rmSync(leaf,{recursive:true}); + fs.symlinkSync('receipt',path.join(root,'.tddswarm/linked'));assert.throws(()=>writeResultReceipt(root,{},'.tddswarm/linked/result.json'),/ancestors/); + for(const target of ['src/value.js','.tddswarm/../src/value.json','.tddswarm/\x1b[31m.json'])assert.throws(()=>writeResultReceipt(root,{},target),/bounded local/); + assert.throws(()=>writeResultReceipt(root,{data:'x'.repeat(32*1024*1024)},'.tddswarm/receipt/result.json'),/32 MiB/); + assert.equal(fs.readFileSync(source,'utf8'),'verified source');assert.equal(fs.readdirSync(path.dirname(leaf)).some(x=>x.endsWith('.tmp')),false); +}); +for(const link of ['symbolic','hard'])test(`actual repaired native test cannot clobber source through a ${link} result alias`,async t=>{ + const root=fixture(t,{...twoModules,...challengeFiles,'src/a.js':'export const a=9;','tddswarm.requirements.md':requirements,'tddswarm.config.json':{adapter:'node',runner:[process.execPath,'--test','{files}'],repair:{challenges:'quality/challenges.json'}}, + 'test/a.test.js':`import {a} from '../src/a.js';import test from 'node:test';import assert from 'node:assert/strict';import fs from 'node:fs';test('a',()=>{assert.equal(a,1);fs.mkdirSync('.tddswarm/repair',{recursive:true});try{${link==='symbolic'?"fs.symlinkSync('../../src/a.js','.tddswarm/repair/result.json')":"fs.linkSync('src/a.js','.tddswarm/repair/result.json')"};}catch(e){if(e.code!=='EEXIST')throw e;}});`});commit(root); + const fixed='export const a=1;',result=await repair(root,{patch:{files:[{path:'src/a.js',content:fixed}],review:{accepted:true,findings:[],oracle:{independent:true,basis:[requirements]}}},sourcePaths:['src/a.js'],deadlineMs:60000});cleanWorktree(t,root,result); + assert.equal(result.status,'receipt-failed',JSON.stringify(result));assert.equal(result.validation.accepted,true);assert.equal(result.fullRun.exitCode,0);assert.ok(result.sha);assert.equal(result.receipt,null);assert.equal(result.published,false); + assert.equal(fs.readFileSync(path.join(result.worktree,'src/a.js'),'utf8'),fixed);assert.equal(fs.readFileSync(path.join(root,'src/a.js'),'utf8'),'export const a=9;'); + const repeat=spawnSync(process.execPath,['--test','test/a.test.js','test/b.test.js'],{cwd:result.worktree,encoding:'utf8',env:nativeEnv(),timeout:10000});assert.equal(repeat.status,0,repeat.stderr+repeat.stdout);assert.match(repeat.stdout,/# tests 2/); +}); +test('actual CLI retains verified local PR metadata while refusing a late service-probe receipt alias',t=>{ + const original='/** @type {number} */ export const value=42;'; + const probe="const fs=require('fs'),p='.tddswarm/improvement/result.json';if(fs.existsSync(p)&&!fs.lstatSync(p).isSymbolicLink()){fs.unlinkSync(p);fs.symlinkSync('../../src/value.js',p);fs.appendFileSync('.tddswarm/alias-observed','alias-created');}process.stdout.write('stable-service-version');"; + const root=fixture(t,{'.gitignore':'.tddswarm/\nnode_modules/\n','package.json':{type:'module'},'src/value.js':original,'test/value.test.js':"import test from 'node:test';import assert from 'node:assert/strict';import {value} from '../src/value.js';test('value',()=>assert.equal(value,42));",'tsconfig.json':{compilerOptions:{strict:true,allowJs:true,checkJs:true,noEmit:true,skipLibCheck:true,module:'NodeNext',moduleResolution:'NodeNext',target:'ES2022'},files:['src/value.js']},'tddswarm.config.json':{adapter:'node',runner:[process.execPath,'--test','{files}'],plugins:{typescript:{enabled:true,timeoutMs:10000}},services:{example:{tests:['test/value.test.js'],probe:[process.execPath,'-e',probe]}}},'tddswarm.requirements.md':'Value42 remains unchanged; successor43 is an independent boundary.'}); + fs.mkdirSync(path.join(root,'node_modules'));fs.cpSync(path.resolve('node_modules/typescript'),path.join(root,'node_modules/typescript'),{recursive:true});commit(root); + const bare=fixture(t);git(bare,'init','--bare');const url='https://github.com/type-proof/project.git';git(root,'remote','add','origin',url);git(root,'config',`url.${bare}.pushInsteadOf`,url); + const bin=path.join(root,'.tddswarm/bin');fs.mkdirSync(bin,{recursive:true}); + write(root,'.tddswarm/bin/gh',`#!${process.execPath}\nimport fs from 'node:fs';import cp from 'node:child_process';const args=process.argv.slice(2);fs.appendFileSync(${JSON.stringify(path.join(root,'.tddswarm/gh-requests'))},JSON.stringify(args)+'\\n');if(args[0]==='repo')console.log('{}');else if(args[1]==='create')console.log('https://github.com/type-proof/project/pull/123');else {const git=a=>cp.execFileSync('git',a,{encoding:'utf8'}).trim();console.log(JSON.stringify({url:'https://github.com/type-proof/project/pull/123',headRefOid:git(['rev-parse','HEAD']),headRefName:git(['symbolic-ref','--short','HEAD']),baseRefName:'main',state:'OPEN',isDraft:false}));}`);fs.chmodSync(path.join(bin,'gh'),0o700); + write(root,'.tddswarm/proposal.json',{files:[{path:'test/boundary.test.js',content:"import test from 'node:test';import assert from 'node:assert/strict';import {value} from '../src/value.js';test('boundary',()=>assert.equal(value+1,43));"}],requirements:'Value42 and successor43.',review:{accepted:true,findings:[],oracle:{independent:true,basis:['Maintainer value42 and successor43 contract.']}}}); + const response=spawnSync(process.execPath,[cli,'improve','--root',root,'--patch','.tddswarm/proposal.json','--no-ci','--json'],{encoding:'utf8',timeout:120000,maxBuffer:16*1024*1024,env:{...nativeEnv(),PATH:bin+path.delimiter+process.env.PATH}}); + assert.equal(response.status,2,response.stderr+response.stdout);const result=JSON.parse(response.stdout);cleanWorktree(t,root,result); + assert.equal(result.status,'receipt-failed',JSON.stringify(result));assert.equal(result.published,true,result.publicationError||result.receiptError);assert.equal(result.pullRequest,'https://github.com/type-proof/project/pull/123');assert.equal(result.pullRequestVerification.headRefOid,result.sha);assert.equal(result.receipt,null);assert.equal(result.fullRun.typecheck.status,'passed');assert.equal(fs.readFileSync(path.join(result.worktree,'src/value.js'),'utf8'),original);assert.equal(fs.readFileSync(path.join(root,'src/value.js'),'utf8'),original);assert.ok(fs.existsSync(path.join(result.worktree,'.tddswarm/alias-observed'))); + assert.equal(git(bare,'rev-parse',`refs/heads/${result.branch}`).trim(),result.sha); + const repeat=spawnSync(process.execPath,['--test','test/value.test.js','test/boundary.test.js'],{cwd:result.worktree,encoding:'utf8',timeout:10000,env:nativeEnv()});assert.equal(repeat.status,0,repeat.stderr+repeat.stdout);assert.match(repeat.stdout,/# tests 2/); + // Real local Git, Node and SDK; deterministic gh only, no external publication. +}); From c09866d68243d24b41abf28289deb78a6b18f730 Mon Sep 17 00:00:00 2001 From: Rudy Celekli Date: Tue, 13 Oct 2026 01:54:16 -0400 Subject: [PATCH 15/16] Document source-preserving late result storage failures --- docs/native-typecheck.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/docs/native-typecheck.md b/docs/native-typecheck.md index 07cd9b2..9787795 100644 --- a/docs/native-typecheck.md +++ b/docs/native-typecheck.md @@ -34,6 +34,8 @@ Enabled type checking also gates the supported Node source-repair profile. Dispo Test-only improvements preserve the same initially requested compiler obligation. They copy the admitted dependency tree, require a complete compiler preflight before initialization or generation, and veto runtime-green candidates whose final compiler check fails. Native candidate acceptance alone is not typed quality readiness. After a commit, the controller actually reruns the full native suite and compiler at the new revision; it compares case identities and input bindings rather than copying a revision into old evidence. Commit-hook drift cannot become ready. Publication checks the compiler and bound inputs again before authentication and at subsequent publication boundaries. Linked candidate, result and PR-body receipts are refused without overwriting source. +Repair, investigation-repair and final CLI result updates use bounded atomic metadata storage as well. A linked result cannot clobber verified source after the last native/compiler check. Storage refusal returns `receipt-failed`, a null receipt and exit 2. If a PR was already verified before a late storage failure, its confirmed publication details remain in the returned result; a local failure does not undo that external action. The writer checks real directories, regular unlinked leaves and temporary-file identity. This is best-effort local concurrency protection within trusted native execution, not an OS sandbox. + Dependency copying is bounded to 100,000 entries and 1 GiB, with an additional 1 GiB free-space reserve. Absolute or external dependency links and incomplete compiler profiles remain reviewable gaps. A passed program covers the files admitted by its configuration; new JavaScript tests outside that program still rely on the native runtime checks. These controls do not establish provider-generated test quality or catch every possible defect. ## Reproduce the development evidence From eb5a883b420e6335558ff40c1ac0e1bc02d1cb55 Mon Sep 17 00:00:00 2001 From: Rudy Celekli Date: Tue, 13 Oct 2026 02:10:18 -0400 Subject: [PATCH 16/16] Request explicit TAP for source-preserving receipt replays --- test/result-receipt.test.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/test/result-receipt.test.js b/test/result-receipt.test.js index 917d22e..92bbd5d 100644 --- a/test/result-receipt.test.js +++ b/test/result-receipt.test.js @@ -34,7 +34,7 @@ for(const link of ['symbolic','hard'])test(`actual repaired native test cannot c const fixed='export const a=1;',result=await repair(root,{patch:{files:[{path:'src/a.js',content:fixed}],review:{accepted:true,findings:[],oracle:{independent:true,basis:[requirements]}}},sourcePaths:['src/a.js'],deadlineMs:60000});cleanWorktree(t,root,result); assert.equal(result.status,'receipt-failed',JSON.stringify(result));assert.equal(result.validation.accepted,true);assert.equal(result.fullRun.exitCode,0);assert.ok(result.sha);assert.equal(result.receipt,null);assert.equal(result.published,false); assert.equal(fs.readFileSync(path.join(result.worktree,'src/a.js'),'utf8'),fixed);assert.equal(fs.readFileSync(path.join(root,'src/a.js'),'utf8'),'export const a=9;'); - const repeat=spawnSync(process.execPath,['--test','test/a.test.js','test/b.test.js'],{cwd:result.worktree,encoding:'utf8',env:nativeEnv(),timeout:10000});assert.equal(repeat.status,0,repeat.stderr+repeat.stdout);assert.match(repeat.stdout,/# tests 2/); + const repeat=spawnSync(process.execPath,['--test','--test-reporter=tap','test/a.test.js','test/b.test.js'],{cwd:result.worktree,encoding:'utf8',env:nativeEnv(),timeout:10000});assert.equal(repeat.status,0,repeat.stderr+repeat.stdout);assert.match(repeat.stdout,/# tests 2/); }); test('actual CLI retains verified local PR metadata while refusing a late service-probe receipt alias',t=>{ const original='/** @type {number} */ export const value=42;'; @@ -49,6 +49,6 @@ test('actual CLI retains verified local PR metadata while refusing a late servic assert.equal(response.status,2,response.stderr+response.stdout);const result=JSON.parse(response.stdout);cleanWorktree(t,root,result); assert.equal(result.status,'receipt-failed',JSON.stringify(result));assert.equal(result.published,true,result.publicationError||result.receiptError);assert.equal(result.pullRequest,'https://github.com/type-proof/project/pull/123');assert.equal(result.pullRequestVerification.headRefOid,result.sha);assert.equal(result.receipt,null);assert.equal(result.fullRun.typecheck.status,'passed');assert.equal(fs.readFileSync(path.join(result.worktree,'src/value.js'),'utf8'),original);assert.equal(fs.readFileSync(path.join(root,'src/value.js'),'utf8'),original);assert.ok(fs.existsSync(path.join(result.worktree,'.tddswarm/alias-observed'))); assert.equal(git(bare,'rev-parse',`refs/heads/${result.branch}`).trim(),result.sha); - const repeat=spawnSync(process.execPath,['--test','test/value.test.js','test/boundary.test.js'],{cwd:result.worktree,encoding:'utf8',timeout:10000,env:nativeEnv()});assert.equal(repeat.status,0,repeat.stderr+repeat.stdout);assert.match(repeat.stdout,/# tests 2/); + const repeat=spawnSync(process.execPath,['--test','--test-reporter=tap','test/value.test.js','test/boundary.test.js'],{cwd:result.worktree,encoding:'utf8',timeout:10000,env:nativeEnv()});assert.equal(repeat.status,0,repeat.stderr+repeat.stdout);assert.match(repeat.stdout,/# tests 2/); // Real local Git, Node and SDK; deterministic gh only, no external publication. });