(node: TaskNode, result: WorkerResult, signal?: AbortSignal)
| 51 | constructor(private hooks: QaHooks = {}) {} |
| 52 | |
| 53 | async review(node: TaskNode, result: WorkerResult, signal?: AbortSignal): Promise<QaVerdict> { |
| 54 | const blockers: string[] = []; |
| 55 | const warnings: string[] = []; |
| 56 | |
| 57 | // 0. Did the worker produce the files it was supposed to? |
| 58 | const wrote = new Set(result.fileEdits.map(e => e.path)); |
| 59 | for (const expected of node.targetFiles) { |
| 60 | if (!wrote.has(expected)) { |
| 61 | blockers.push(`Expected file not produced: ${expected}`); |
| 62 | } |
| 63 | } |
| 64 | // Did it write outside its allowed set? |
| 65 | for (const e of result.fileEdits) { |
| 66 | if (node.targetFiles.length > 0 && !node.targetFiles.includes(e.path)) { |
| 67 | warnings.push(`Wrote file outside assigned scope: ${e.path}`); |
| 68 | } |
| 69 | } |
| 70 | |
| 71 | // 1. STATIC parse check. |
| 72 | for (const edit of result.fileEdits) { |
| 73 | const parseErr = await this.parseCheck(edit.path, edit.content); |
| 74 | if (parseErr) blockers.push(parseErr); |
| 75 | } |
| 76 | |
| 77 | // 1b. Optional typecheck (scoped). |
| 78 | if (this.hooks.typecheck && blockers.length === 0) { |
| 79 | try { |
| 80 | const errs = await this.hooks.typecheck(result.fileEdits.map(e => ({ path: e.path, content: e.content })), signal); |
| 81 | for (const err of errs) blockers.push(err); |
| 82 | } catch (e: any) { |
| 83 | logger.debug('Typecheck hook threw (non-fatal)', { err: e?.message }); |
| 84 | warnings.push(`Typecheck could not run: ${e?.message ?? String(e)}`); |
| 85 | } |
| 86 | } |
| 87 | |
| 88 | // 2. DESIGN audit for visual kinds. |
| 89 | if ((node.kind === 'component' || node.kind === 'style') && this.hooks.designAudit && blockers.length === 0) { |
| 90 | try { |
| 91 | const issues = await this.hooks.designAudit(result.fileEdits.map(e => ({ path: e.path, content: e.content }))); |
| 92 | for (const issue of issues) { |
| 93 | if (issue.severity === 'high') blockers.push(`Design: ${issue.message}`); |
| 94 | else warnings.push(`Design: ${issue.message}`); |
| 95 | } |
| 96 | } catch (e: any) { |
| 97 | logger.debug('Design audit hook threw (non-fatal)', { err: e?.message }); |
| 98 | warnings.push(`Design audit could not run: ${e?.message ?? String(e)}`); |
| 99 | } |
| 100 | } |
| 101 | |
| 102 | // 3. VISION review when requested and everything else passed. |
| 103 | let visual: QaVerdict['visual']; |
| 104 | if (node.visualReview && this.hooks.visualReview && blockers.length === 0) { |
| 105 | try { |
| 106 | const v = await this.hooks.visualReview(node, signal); |
| 107 | visual = { screenshotPath: v.screenshotPath, notes: v.notes }; |
| 108 | if (!v.ok) blockers.push(`Visual review failed: ${v.notes}`); |
| 109 | } catch (e: any) { |
| 110 | logger.debug('Visual review hook threw (non-fatal)', { err: e?.message }); |
no test coverage detected