(opts: {
cwd: string;
touched: string[];
signal?: AbortSignal;
timeoutMs?: number;
/** Pre-edit diagnostics (whole-project). When given, only NEW errors are reported. */
baseline?: Diagnostic[];
})
| 138 | * treats that as "skip silently, let the model finish". |
| 139 | */ |
| 140 | export async function verifyTouchedFiles(opts: { |
| 141 | cwd: string; |
| 142 | touched: string[]; |
| 143 | signal?: AbortSignal; |
| 144 | timeoutMs?: number; |
| 145 | /** Pre-edit diagnostics (whole-project). When given, only NEW errors are reported. */ |
| 146 | baseline?: Diagnostic[]; |
| 147 | }): Promise<VerifyResult> { |
| 148 | const { cwd, touched } = opts; |
| 149 | if (touched.length === 0) return { ran: false, diagnostics: [], errorCount: 0 }; |
| 150 | |
| 151 | const rootFiles = await detectProjectFiles(cwd); |
| 152 | // One checker per language present in the project. Each runs ONLY if the model |
| 153 | // touched a file in a language it owns, so a Python edit in a TS+Py repo is checked |
| 154 | // by ruff, not silently skipped because tsc happened to be first. |
| 155 | const specs = pickCheckers(rootFiles); |
| 156 | if (specs.length === 0) return { ran: false, diagnostics: [], errorCount: 0 }; |
| 157 | const timeoutMs = opts.timeoutMs ?? 120_000; |
| 158 | |
| 159 | let anyRan = false; |
| 160 | let anyUnavailable = false; |
| 161 | const ids: string[] = []; |
| 162 | const allRelevant: string[] = []; |
| 163 | const collected: Diagnostic[] = []; |
| 164 | |
| 165 | for (const spec of specs) { |
| 166 | const relevant = relevantTouchedFiles(touched, spec); |
| 167 | if (relevant.length === 0) continue; // model didn't touch this language |
| 168 | const { diags, unavailable } = await runCheckerCollect(spec, cwd, relevant, timeoutMs, opts.signal); |
| 169 | if (unavailable) { anyUnavailable = true; continue; } // tool missing for this language — skip, don't block |
| 170 | anyRan = true; |
| 171 | ids.push(spec.id); |
| 172 | allRelevant.push(...relevant); |
| 173 | collected.push(...filterToTouched(diags, relevant, cwd)); |
| 174 | } |
| 175 | |
| 176 | if (!anyRan) { |
| 177 | // No language's checker produced a result: either nothing touched is in a covered |
| 178 | // language, or the only relevant checkers' binaries were missing. |
| 179 | return { ran: false, diagnostics: [], errorCount: 0, ...(anyUnavailable ? { unavailable: true } : {}) }; |
| 180 | } |
| 181 | |
| 182 | let scoped = collected; |
| 183 | // Baseline subtraction: if we captured the pre-edit state, only hold the model |
| 184 | // accountable for errors it actually introduced — not the project's prior debt. |
| 185 | // Signatures are checker-specific, so a flat union baseline subtracts correctly per language. |
| 186 | if (opts.baseline) { |
| 187 | const baseScoped = filterToTouched(opts.baseline, allRelevant, cwd); |
| 188 | scoped = diffDiagnostics(baseScoped, scoped); |
| 189 | } |
| 190 | return { |
| 191 | ran: true, |
| 192 | checker: ids.join('+'), |
| 193 | diagnostics: scoped, |
| 194 | errorCount: scoped.filter(d => d.severity === 'error').length, |
| 195 | }; |
| 196 | } |
| 197 |
no test coverage detected