CommonJS (require/module.exports). Node >=18 (global fetch, node:test available).
Do not change any signature below — other modules depend on them. Do not run npm install.
const { cfg, QUEUE_NAME, DLQ_NAME } = require('./config');
// cfg: { role, port, redisUrl, ollamaUrl, model, webhookSecret, githubToken,
// concurrency, llmTimeoutMs, maxAttempts } (frozen)const { log } = require('./logger');
log(level, deliveryId, msg, extra = {}); // level: 'info'|'warn'|'error'const { connection, reviewQueue, deadQueue, processedKey } = require('./queue');
// connection: ioredis client (use .exists(key) -> 0|1, .set(key,'1','EX',secs))
// reviewQueue, deadQueue: BullMQ Queue instances
// processedKey(repo, pr, sha) -> stringconst { verifySignature } = require('./signature');
verifySignature(rawBody /* Buffer */, signatureHeader /* string */, secret) -> booleanmodule.exports = { chunkDiff, chunkHasLine };
// chunkDiff(rawDiff: string) -> Array<{ file: string, diff: string }>
// Split a unified git diff per file, then per @@ hunk. Cap each hunk's `diff` to 4000 chars.
// file name comes from the "+++ b/<name>" line; fall back to 'unknown'.
// chunkHasLine(chunk: {diff}, line: number) -> boolean
// Parse the new-file range from the hunk header "@@ -a,b +c,d @@".
// Return true if `line` is within [c, c+d]. If no header found, return true (don't over-reject).module.exports = { reviewChunk, buildPrompt, callModel, withTimeout, FindingSchema };
// FindingSchema (zod):
// { severity: 'info'|'warning'|'critical', line: int>=0, comment: string(1..500), confidence: number(0..1) }
// buildPrompt(chunk: {file, diff}) -> string
// Instruct the model to return ONE JSON object matching FindingSchema and nothing else.
// callModel(prompt: string, signal: AbortSignal) -> Promise<string>
// POST `${cfg.ollamaUrl}/api/generate` with
// { model: cfg.model, prompt, stream: false, format: 'json', options: { temperature: 0.1 } }
// Pass { signal }. Throw on !res.ok. Return the `.response` field (a JSON string).
// Use global fetch.
// withTimeout(fn: (signal) => Promise<T>, ms: number) -> Promise<T>
// Create an AbortController, abort after ms (Error 'LLM timeout'), clearTimeout in finally.
// reviewChunk(chunk: {file, diff}, deliveryId: string) -> Promise<finding | null>
// Up to 3 attempts: withTimeout(signal => callModel(buildPrompt(chunk), signal), cfg.llmTimeoutMs)
// -> JSON.parse -> FindingSchema.parse.
// Guardrail 1: if !chunkHasLine(chunk, finding.line) -> throw (retryable).
// Guardrail 2: if finding.confidence < 0.6 -> log + return null.
// On success return { ...finding, file: chunk.file }.
// On any thrown error: log('warn', ...); after attempt 3 return null (never throw — keep job alive).module.exports = { getPrHeadSha, fetchDiff, upsertReviewComment, renderReview };
// @octokit/rest v20.1.2 is CommonJS-requireable (verified): const { Octokit } = require('@octokit/rest');
// Create one cached instance from cfg.githubToken:
// const { Octokit } = require('@octokit/rest');
// const octokit = new Octokit({ auth: cfg.githubToken });
// getPrHeadSha({ repoOwner, repoName, prNumber }) -> Promise<string>
// octokit.pulls.get({ owner, repo, pull_number }) -> data.head.sha
// fetchDiff({ repoOwner, repoName, prNumber }) -> Promise<string>
// octokit.pulls.get({ owner, repo, pull_number, mediaType: { format: 'diff' } })
// -> data is the raw diff string.
// upsertReviewComment({ repoOwner, repoName, prNumber, headSha, findings }) -> Promise<void>
// body = `<!-- turtlecode:${headSha} -->\n` + renderReview(findings)
// List issue comments; find one whose body startsWith '<!-- turtlecode:'.
// If found -> issues.updateComment({ comment_id, body }); else issues.createComment({ issue_number, body }).
// (owner=repoOwner, repo=repoName, issue_number=prNumber)
// renderReview(findings: Array<{severity,file,line,comment}>) -> string (pure; export for tests)
// No findings -> 'TurtleCode: no issues found.'
// Else header '**TurtleCode review**' + one bullet per finding, sorted critical>warning>info:
// `- **${severity}** \`${file}:${line}\` — ${comment}`module.exports = { startWeb };
// startWeb() -> http.Server
// express app. Use express.json({ verify: (req,_res,buf) => { req.rawBody = buf; } }).
// POST /webhook:
// deliveryId = req.get('X-GitHub-Delivery') || ''; event = req.get('X-GitHub-Event').
// if !verifySignature(req.rawBody, req.get('X-Hub-Signature-256'), cfg.webhookSecret) -> 401.
// actionable = event==='pull_request' && ['opened','synchronize','reopened'].includes(body.action).
// if not actionable -> 204.
// job = { deliveryId, repoOwner: body.repository.owner.login, repoName: body.repository.name,
// prNumber: body.pull_request.number, headSha: body.pull_request.head.sha }.
// await reviewQueue.add('review', job, { jobId: deliveryId, attempts: cfg.maxAttempts,
// backoff: { type:'exponential', delay:2000 }, removeOnComplete:1000, removeOnFail:false }).
// res.status(202).json({ status:'queued', deliveryId }).
// GET /healthz -> { ok: true }.
// return app.listen(cfg.port, () => log('info','-',`web listening on :${cfg.port}`)).module.exports = { startWorker };
// startWorker() -> BullMQ Worker
// new Worker(QUEUE_NAME, processReview, { connection, concurrency: cfg.concurrency }).
// worker.on('failed', (job, err) => { log('error',...); if attemptsMade >= opts.attempts ->
// deadQueue.add('dead', {...job.data, error: err.message}, { removeOnComplete:false }); log DLQ }).
// return worker.
//
// processReview(job): job.data = { deliveryId, repoOwner, repoName, prNumber, headSha }
// 1. if await connection.exists(processedKey(repoName, prNumber, headSha)) -> log + return (idempotent).
// 2. sha = await getPrHeadSha(...); if sha !== headSha -> log 'superseded' + return (staleness).
// 3. diff = await fetchDiff(...); chunks = chunkDiff(diff).
// 4. findings = []; for each chunk: f = await reviewChunk(chunk, deliveryId); if (f) findings.push(f).
// 5. await upsertReviewComment({ repoOwner, repoName, prNumber, headSha, findings }).
// 6. await connection.set(processedKey(repoName, prNumber, headSha), '1', 'EX', 60*60*24*7).
// 7. log('info', deliveryId, 'review posted', { findings: findings.length }).
// Let errors throw so BullMQ retries; the 'failed' handler dead-letters after max attempts.Use node:test + node:assert. Only unit-test modules that don't import queue.js
(no Redis in tests): chunk, reviewer (mock global fetch), github (test renderReview).
Put tests in test/<name>.test.js.