import {readFile,writeFile} from 'node:fs/promises' import {fileURLToPath} from 'node:url' const inputKeys=new Set(['schema','cases']) const caseKeys=new Set(['id','changedFiles','diff','expected','findings']) const expectedKeys=new Set(['id','file','line','category']) const findingKeys=new Set(['id','file','line','category','action','nextStep','stipulatedDisposition']) const categories=new Set(['correctness','test','security','design']) const actions=new Set(['fix','test','question']) const dispositions=new Set(['actionable','not-actionable','needs-discussion']) const idPattern=/^[a-z][a-z0-9-]{0,63}$/ const pathPattern=/^(?!\/)(?!.*(?:^|\/)\.\.(?:\/|$))[a-zA-Z0-9._/-]{1,160}$/ function exact(value,keys,label){ if(!value||typeof value!=='object'||Array.isArray(value)||Object.getPrototypeOf(value)!==Object.prototype)throw Error(`${label} must be a plain object`) for(const key of Reflect.ownKeys(value)){if(typeof key!=='string'||!keys.has(key))throw Error(`${label}.${String(key)} is not allowed`)} for(const key of keys)if(!Object.hasOwn(value,key))throw Error(`${label}.${key} is required`) } function validId(value,label){if(typeof value!=='string'||!idPattern.test(value))throw Error(`${label} is invalid`)} function validPath(value,label){if(typeof value!=='string'||!pathPattern.test(value)||value.includes('\\'))throw Error(`${label} is invalid`)} function validLine(value,label){if(!Number.isSafeInteger(value)||value<1||value>1000000)throw Error(`${label} is invalid`)} function unique(values,label){if(new Set(values).size!==values.length)throw Error(`${label} contains duplicates`)} function tuple(item){return`${item.file}:${item.line}:${item.category}`} export function calibrate(input){ exact(input,inputKeys,'input') if(input.schema!=='martybytes-pr-feedback-calibration/v1')throw Error('unsupported schema') if(!Array.isArray(input.cases)||input.cases.length<1||input.cases.length>20)throw Error('cases must contain 1 to 20 items') unique(input.cases.map(item=>item&&item.id),'case ids') const caseResults=[] for(const [caseIndex,item] of input.cases.entries()){ const label=`cases[${caseIndex}]` exact(item,caseKeys,label);validId(item.id,`${label}.id`) if(!Array.isArray(item.changedFiles)||item.changedFiles.length<1||item.changedFiles.length>50)throw Error(`${label}.changedFiles is invalid`) item.changedFiles.forEach((file,index)=>validPath(file,`${label}.changedFiles[${index}]`));unique(item.changedFiles,`${label}.changedFiles`) if(typeof item.diff!=='string'||!item.diff.trim()||item.diff.length>5000)throw Error(`${label}.diff is invalid`) if(!Array.isArray(item.expected)||item.expected.length>100||!Array.isArray(item.findings)||item.findings.length>200)throw Error(`${label} issue lists are invalid`) unique(item.expected.map(entry=>entry&&entry.id),`${label} expected ids`);unique(item.findings.map(entry=>entry&&entry.id),`${label} finding ids`) const expectedByTuple=new Map() for(const [index,entry] of item.expected.entries()){ exact(entry,expectedKeys,`${label}.expected[${index}]`);validId(entry.id,`${label}.expected[${index}].id`);validPath(entry.file,`${label}.expected[${index}].file`);validLine(entry.line,`${label}.expected[${index}].line`) if(!item.changedFiles.includes(entry.file))throw Error(`${label}.expected[${index}] uses an undeclared file`) if(!categories.has(entry.category))throw Error(`${label}.expected[${index}].category is invalid`) if(expectedByTuple.has(tuple(entry)))throw Error(`${label}.expected contains a duplicate issue tuple`) expectedByTuple.set(tuple(entry),entry) } const seenTuples=new Set(),matchedExpected=new Set(),findingResults=[] for(const [index,entry] of item.findings.entries()){ exact(entry,findingKeys,`${label}.findings[${index}]`);validId(entry.id,`${label}.findings[${index}].id`);validPath(entry.file,`${label}.findings[${index}].file`);validLine(entry.line,`${label}.findings[${index}].line`) if(!categories.has(entry.category))throw Error(`${label}.findings[${index}].category is invalid`) if(!actions.has(entry.action))throw Error(`${label}.findings[${index}].action is invalid`) if(typeof entry.nextStep!=='string'||!entry.nextStep.trim()||entry.nextStep.length>300)throw Error(`${label}.findings[${index}].nextStep is invalid`) if(!dispositions.has(entry.stipulatedDisposition))throw Error(`${label}.findings[${index}].stipulatedDisposition is invalid`) const key=tuple(entry),duplicate=seenTuples.has(key),inChangedFile=item.changedFiles.includes(entry.file),matched=!duplicate&&expectedByTuple.has(key) seenTuples.add(key);if(matched)matchedExpected.add(expectedByTuple.get(key).id) findingResults.push({id:entry.id,referenceMatched:matched,duplicate,inChangedFile,stipulatedDisposition:entry.stipulatedDisposition}) } caseResults.push({id:item.id,stipulatedReferenceIssues:item.expected.length,localizedReferenceMatches:matchedExpected.size,missedReferenceIssues:item.expected.filter(entry=>!matchedExpected.has(entry.id)).map(entry=>entry.id),unmatchedFindings:findingResults.filter(entry=>!entry.referenceMatched&&!entry.duplicate).map(entry=>entry.id),duplicates:findingResults.filter(entry=>entry.duplicate).map(entry=>entry.id),outsideChangedFiles:findingResults.filter(entry=>!entry.inChangedFile).map(entry=>entry.id),stipulatedDispositions:{actionable:findingResults.filter(entry=>entry.stipulatedDisposition==='actionable').length,notActionable:findingResults.filter(entry=>entry.stipulatedDisposition==='not-actionable').length,needsDiscussion:findingResults.filter(entry=>entry.stipulatedDisposition==='needs-discussion').length}}) } const totals=caseResults.reduce((sum,item)=>({stipulatedReferenceIssues:sum.stipulatedReferenceIssues+item.stipulatedReferenceIssues,localizedReferenceMatches:sum.localizedReferenceMatches+item.localizedReferenceMatches,missedReferenceIssues:sum.missedReferenceIssues+item.missedReferenceIssues.length,unmatchedFindings:sum.unmatchedFindings+item.unmatchedFindings.length,duplicates:sum.duplicates+item.duplicates.length,outsideChangedFiles:sum.outsideChangedFiles+item.outsideChangedFiles.length,stipulatedActionable:sum.stipulatedActionable+item.stipulatedDispositions.actionable,stipulatedNotActionable:sum.stipulatedNotActionable+item.stipulatedDispositions.notActionable,stipulatedNeedsDiscussion:sum.stipulatedNeedsDiscussion+item.stipulatedDispositions.needsDiscussion}),{stipulatedReferenceIssues:0,localizedReferenceMatches:0,missedReferenceIssues:0,unmatchedFindings:0,duplicates:0,outsideChangedFiles:0,stipulatedActionable:0,stipulatedNotActionable:0,stipulatedNeedsDiscussion:0}) return{schema:'martybytes-pr-feedback-calibration-result/v1',policy:'exact-file-line-category',cases:caseResults,totals,boundary:'Mechanical checks cover exact localization, duplicates, and changed-file scope. Shipped reference issues and dispositions are explicitly stipulated synthetic labels, not evidence of human adjudication. No correctness or actionability is inferred from free text; no reviewer, model, service, or repository was evaluated.'} } async function main([inputPath,outputPath]){ if(!inputPath||!outputPath)throw Error('Usage: node pr-feedback-calibration.mjs input.json result.json') const result=calibrate(JSON.parse(await readFile(inputPath,'utf8'))) await writeFile(outputPath,JSON.stringify(result,null,2)+'\n',{flag:'wx'}) } if(process.argv[1]===fileURLToPath(import.meta.url))main(process.argv.slice(2)).catch(error=>{console.error(error.message);process.exitCode=1})