threadlines 0.2.25 → 0.4.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/git/diff.js CHANGED
@@ -1,7 +1,4 @@
1
1
  "use strict";
2
- var __importDefault = (this && this.__importDefault) || function (mod) {
3
- return (mod && mod.__esModule) ? mod : { "default": mod };
4
- };
5
2
  Object.defineProperty(exports, "__esModule", { value: true });
6
3
  exports.getRepoUrl = getRepoUrl;
7
4
  exports.getHeadCommitSha = getHeadCommitSha;
@@ -9,7 +6,6 @@ exports.getCommitMessage = getCommitMessage;
9
6
  exports.getCommitAuthor = getCommitAuthor;
10
7
  exports.getPRDiff = getPRDiff;
11
8
  exports.getCommitDiff = getCommitDiff;
12
- const simple_git_1 = __importDefault(require("simple-git"));
13
9
  const child_process_1 = require("child_process");
14
10
  const logger_1 = require("../utils/logger");
15
11
  // =============================================================================
@@ -88,18 +84,33 @@ async function getHeadCommitSha(repoRoot) {
88
84
  }
89
85
  /**
90
86
  * Get commit message for a specific commit SHA
91
- * Returns full commit message (subject + body) or null if commit not found
87
+ *
88
+ * Fails loudly if commit cannot be retrieved (commit not found, git error, etc.).
89
+ * This function is only called when a commit is expected to exist:
90
+ * - In CI environments (always has HEAD commit)
91
+ * - In local environment with --commit flag (user explicitly provided SHA)
92
+ *
93
+ * @param repoRoot - Path to the repository root
94
+ * @param sha - Commit SHA to get message for
95
+ * @returns Full commit message (subject + body)
96
+ * @throws Error if commit cannot be retrieved
92
97
  */
93
98
  async function getCommitMessage(repoRoot, sha) {
94
- const git = (0, simple_git_1.default)(repoRoot);
95
99
  try {
96
100
  // Get full commit message (subject + body)
97
- const message = await git.show([sha, '--format=%B', '--no-patch']);
98
- return message.trim() || null;
101
+ const message = (0, child_process_1.execSync)(`git show --format=%B --no-patch ${sha}`, {
102
+ encoding: 'utf-8',
103
+ cwd: repoRoot
104
+ }).trim();
105
+ if (!message) {
106
+ throw new Error(`Commit ${sha} exists but has no message`);
107
+ }
108
+ return message;
99
109
  }
100
- catch {
101
- // Commit not found or invalid
102
- return null;
110
+ catch (error) {
111
+ const errorMessage = error instanceof Error ? error.message : 'Unknown error';
112
+ throw new Error(`Failed to get commit message for ${sha}: ${errorMessage}\n` +
113
+ `This commit should exist (called from CI or with --commit flag).`);
103
114
  }
104
115
  }
105
116
  /**
@@ -180,16 +191,19 @@ async function getCommitAuthor(repoRoot, sha) {
180
191
  * @param logger - Optional logger for debug output
181
192
  */
182
193
  async function getPRDiff(repoRoot, targetBranch, logger) {
183
- const git = (0, simple_git_1.default)(repoRoot);
184
194
  // Fetch target branch on-demand (works with shallow clones)
185
195
  logger?.debug(`Fetching target branch: origin/${targetBranch}`);
186
196
  try {
187
- await git.fetch(['origin', `${targetBranch}:refs/remotes/origin/${targetBranch}`, '--depth=1']);
197
+ (0, child_process_1.execSync)(`git fetch origin ${targetBranch}:refs/remotes/origin/${targetBranch} --depth=1`, {
198
+ cwd: repoRoot,
199
+ stdio: 'pipe' // Suppress fetch output
200
+ });
188
201
  }
189
202
  catch (fetchError) {
203
+ const errorMessage = fetchError instanceof Error ? fetchError.message : String(fetchError);
190
204
  throw new Error(`Failed to fetch target branch origin/${targetBranch}. ` +
191
205
  `This is required for PR/MR diff comparison. ` +
192
- `Error: ${fetchError instanceof Error ? fetchError.message : 'Unknown error'}`);
206
+ `Error: ${errorMessage}`);
193
207
  }
194
208
  // Try three dots (merge base) first - shows only developer's changes
195
209
  // Falls back to two dots (direct comparison) if shallow clone prevents merge base calculation
@@ -200,9 +214,16 @@ async function getPRDiff(repoRoot, targetBranch, logger) {
200
214
  // This isolates developer changes by comparing against merge base
201
215
  // Works when we have enough history (full clones or GitHub's merge commits)
202
216
  logger?.debug(`Attempting three-dots diff (merge base): origin/${targetBranch}...HEAD`);
203
- diff = await git.diff([`origin/${targetBranch}...HEAD`, '-U200']);
204
- const diffSummary = await git.diffSummary([`origin/${targetBranch}...HEAD`]);
205
- changedFiles = diffSummary.files.map(f => f.file);
217
+ diff = (0, child_process_1.execSync)(`git diff origin/${targetBranch}...HEAD -U200`, {
218
+ encoding: 'utf-8',
219
+ cwd: repoRoot
220
+ });
221
+ // Get changed files using git diff --name-only
222
+ const changedFilesOutput = (0, child_process_1.execSync)(`git diff --name-only origin/${targetBranch}...HEAD`, {
223
+ encoding: 'utf-8',
224
+ cwd: repoRoot
225
+ }).trim();
226
+ changedFiles = changedFilesOutput ? changedFilesOutput.split('\n') : [];
206
227
  }
207
228
  catch (error) {
208
229
  // Step 2: Fallback to "Risky" Diff (Two Dots)
@@ -215,9 +236,15 @@ async function getPRDiff(repoRoot, targetBranch, logger) {
215
236
  logger?.debug(`Fallback error: ${errorMessage}`);
216
237
  // Use two dots (direct comparison) - shows all differences between tips
217
238
  logger?.debug(`Using two-dots diff (direct comparison): origin/${targetBranch}..HEAD`);
218
- diff = await git.diff([`origin/${targetBranch}..HEAD`, '-U200']);
219
- const diffSummary = await git.diffSummary([`origin/${targetBranch}..HEAD`]);
220
- changedFiles = diffSummary.files.map(f => f.file);
239
+ diff = (0, child_process_1.execSync)(`git diff origin/${targetBranch}..HEAD -U200`, {
240
+ encoding: 'utf-8',
241
+ cwd: repoRoot
242
+ });
243
+ const changedFilesOutput = (0, child_process_1.execSync)(`git diff --name-only origin/${targetBranch}..HEAD`, {
244
+ encoding: 'utf-8',
245
+ cwd: repoRoot
246
+ }).trim();
247
+ changedFiles = changedFilesOutput ? changedFilesOutput.split('\n') : [];
221
248
  }
222
249
  return {
223
250
  diff: diff || '',
@@ -250,7 +277,6 @@ async function getPRDiff(repoRoot, targetBranch, logger) {
250
277
  * @param sha - Commit SHA to get diff for (defaults to HEAD)
251
278
  */
252
279
  async function getCommitDiff(repoRoot, sha = 'HEAD') {
253
- const git = (0, simple_git_1.default)(repoRoot);
254
280
  // Fetch parent commit on-demand to ensure git show can generate a proper diff
255
281
  // This works regardless of CI checkout depth settings (depth=1 or depth=2)
256
282
  // If parent is already available, fetch is fast/no-op; if not, we fetch it
@@ -295,7 +321,10 @@ async function getCommitDiff(repoRoot, sha = 'HEAD') {
295
321
  // If we get here, parentSha is guaranteed to be a valid 40-character SHA
296
322
  try {
297
323
  // Fetch just this one commit (depth=1 is fine, we only need the parent)
298
- await git.fetch(['origin', parentSha, '--depth=1']);
324
+ (0, child_process_1.execSync)(`git fetch origin ${parentSha} --depth=1`, {
325
+ cwd: repoRoot,
326
+ stdio: 'pipe' // Suppress fetch output
327
+ });
299
328
  }
300
329
  catch (error) {
301
330
  const errorMessage = error instanceof Error ? error.message : String(error);
@@ -312,10 +341,16 @@ async function getCommitDiff(repoRoot, sha = 'HEAD') {
312
341
  let changedFiles;
313
342
  try {
314
343
  // Use git diff to compare parent against HEAD (plumbing command, ignores shallow boundaries)
315
- diff = await git.diff([`${parentSha}..${sha}`, '-U200']);
344
+ diff = (0, child_process_1.execSync)(`git diff ${parentSha}..${sha} -U200`, {
345
+ encoding: 'utf-8',
346
+ cwd: repoRoot
347
+ });
316
348
  // Get changed files using git diff --name-only
317
- const diffSummary = await git.diffSummary([`${parentSha}..${sha}`]);
318
- changedFiles = diffSummary.files.map(f => f.file);
349
+ const changedFilesOutput = (0, child_process_1.execSync)(`git diff --name-only ${parentSha}..${sha}`, {
350
+ encoding: 'utf-8',
351
+ cwd: repoRoot
352
+ }).trim();
353
+ changedFiles = changedFilesOutput ? changedFilesOutput.split('\n') : [];
319
354
  }
320
355
  catch (error) {
321
356
  const errorMessage = error instanceof Error ? error.message : 'Unknown error';
package/dist/git/local.js CHANGED
@@ -11,21 +11,55 @@
11
11
  * - branchName: string
12
12
  * - commitAuthor: { name: string; email: string }
13
13
  */
14
- var __importDefault = (this && this.__importDefault) || function (mod) {
15
- return (mod && mod.__esModule) ? mod : { "default": mod };
16
- };
14
+ var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
15
+ if (k2 === undefined) k2 = k;
16
+ var desc = Object.getOwnPropertyDescriptor(m, k);
17
+ if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
18
+ desc = { enumerable: true, get: function() { return m[k]; } };
19
+ }
20
+ Object.defineProperty(o, k2, desc);
21
+ }) : (function(o, m, k, k2) {
22
+ if (k2 === undefined) k2 = k;
23
+ o[k2] = m[k];
24
+ }));
25
+ var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
26
+ Object.defineProperty(o, "default", { enumerable: true, value: v });
27
+ }) : function(o, v) {
28
+ o["default"] = v;
29
+ });
30
+ var __importStar = (this && this.__importStar) || (function () {
31
+ var ownKeys = function(o) {
32
+ ownKeys = Object.getOwnPropertyNames || function (o) {
33
+ var ar = [];
34
+ for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
35
+ return ar;
36
+ };
37
+ return ownKeys(o);
38
+ };
39
+ return function (mod) {
40
+ if (mod && mod.__esModule) return mod;
41
+ var result = {};
42
+ if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
43
+ __setModuleDefault(result, mod);
44
+ return result;
45
+ };
46
+ })();
17
47
  Object.defineProperty(exports, "__esModule", { value: true });
18
48
  exports.getLocalContext = getLocalContext;
19
- const simple_git_1 = __importDefault(require("simple-git"));
49
+ const child_process_1 = require("child_process");
50
+ const fs = __importStar(require("fs"));
51
+ const path = __importStar(require("path"));
20
52
  const diff_1 = require("./diff");
53
+ const logger_1 = require("../utils/logger");
21
54
  /**
22
55
  * Gets all Local context
23
56
  */
24
57
  async function getLocalContext(repoRoot, commitSha) {
25
- const git = (0, simple_git_1.default)(repoRoot);
26
58
  // Check if we're in a git repo
27
- const isRepo = await git.checkIsRepo();
28
- if (!isRepo) {
59
+ try {
60
+ (0, child_process_1.execSync)('git rev-parse --git-dir', { cwd: repoRoot, stdio: 'ignore' });
61
+ }
62
+ catch {
29
63
  throw new Error('Not a git repository. Threadline requires a git repository.');
30
64
  }
31
65
  // Get all Local context
@@ -37,13 +71,10 @@ async function getLocalContext(repoRoot, commitSha) {
37
71
  const commitAuthor = commitSha
38
72
  ? await getCommitAuthorFromGit(repoRoot, commitSha)
39
73
  : await getCommitAuthorFromConfig(repoRoot);
40
- // Get commit message if we have a SHA
74
+ // Get commit message if we have a SHA (fails loudly if commit doesn't exist)
41
75
  let commitMessage;
42
76
  if (commitSha) {
43
- const message = await (0, diff_1.getCommitMessage)(repoRoot, commitSha);
44
- if (message) {
45
- commitMessage = message;
46
- }
77
+ commitMessage = await (0, diff_1.getCommitMessage)(repoRoot, commitSha);
47
78
  }
48
79
  return {
49
80
  diff,
@@ -64,45 +95,162 @@ async function getLocalContext(repoRoot, commitSha) {
64
95
  * or review unstaged changes if nothing is staged.
65
96
  */
66
97
  async function getDiff(repoRoot) {
67
- const git = (0, simple_git_1.default)(repoRoot);
68
- // Get git status to determine what changes exist
69
- const status = await git.status();
98
+ // Get git status in porcelain format to determine what changes exist
99
+ // Porcelain format: XY filename
100
+ // X = staged status, Y = unstaged status
101
+ // ' ' = no change, 'M' = modified, 'A' = added, 'D' = deleted, etc.
102
+ // '?' = untracked (only in Y position, X is always '?' too)
103
+ const statusOutput = (0, child_process_1.execSync)('git status --porcelain', {
104
+ encoding: 'utf-8',
105
+ cwd: repoRoot
106
+ }).trim();
107
+ const lines = statusOutput ? statusOutput.split('\n') : [];
108
+ const staged = [];
109
+ const unstaged = [];
110
+ const untracked = [];
111
+ for (const line of lines) {
112
+ const stagedStatus = line[0];
113
+ const unstagedStatus = line[1];
114
+ // Collect untracked files separately (they need special handling)
115
+ if (stagedStatus === '?' && unstagedStatus === '?') {
116
+ // Format: "?? filename" - skip 3 characters
117
+ const file = line.slice(3);
118
+ untracked.push(file);
119
+ continue;
120
+ }
121
+ // For tracked files, the format can be:
122
+ // - "M filename" (staged, no leading space) - skip 2 characters
123
+ // - " M filename" (unstaged, leading space) - skip 3 characters
124
+ // - "MM filename" (both staged and unstaged) - skip 3 characters
125
+ let file;
126
+ if (stagedStatus !== ' ' && unstagedStatus === ' ') {
127
+ // Staged only: "M filename" - skip 2 characters (M + space)
128
+ file = line.slice(2);
129
+ }
130
+ else {
131
+ // Unstaged or both: " M filename" or "MM filename" - skip 3 characters
132
+ file = line.slice(3);
133
+ }
134
+ if (stagedStatus !== ' ') {
135
+ staged.push(file);
136
+ }
137
+ if (unstagedStatus !== ' ' && unstagedStatus !== '?') {
138
+ unstaged.push(file);
139
+ }
140
+ }
70
141
  let diff;
71
142
  let changedFiles;
72
- // Priority 1: Use staged changes if available
73
- if (status.staged.length > 0) {
74
- diff = await git.diff(['--cached', '-U200']);
75
- // status.staged is an array of strings (file paths)
76
- changedFiles = status.staged;
143
+ // Check if there are actually staged files (use git diff as source of truth)
144
+ // git status parsing can be inconsistent, so we verify with git diff
145
+ const stagedFilesOutput = (0, child_process_1.execSync)('git diff --cached --name-only', {
146
+ encoding: 'utf-8',
147
+ cwd: repoRoot
148
+ }).trim();
149
+ const actualStagedFiles = stagedFilesOutput ? stagedFilesOutput.split('\n') : [];
150
+ // Workflow A: Developer has staged files - check ONLY staged files
151
+ // (Ignore unstaged and untracked - developer explicitly chose to check staged)
152
+ if (actualStagedFiles.length > 0) {
153
+ diff = (0, child_process_1.execSync)('git diff --cached -U200', {
154
+ encoding: 'utf-8',
155
+ cwd: repoRoot
156
+ });
157
+ changedFiles = actualStagedFiles;
158
+ // If staged files exist but diff is empty, something is wrong
159
+ if (!diff || diff.trim() === '') {
160
+ throw new Error(`Staged files exist but diff is empty. ` +
161
+ `This may indicate binary files, whitespace-only changes, or a git issue. ` +
162
+ `Staged files: ${actualStagedFiles.join(', ')}`);
163
+ }
164
+ logger_1.logger.info(`Checking STAGED changes (${changedFiles.length} file(s))`);
165
+ return {
166
+ diff: diff || '',
167
+ changedFiles
168
+ };
77
169
  }
78
- // Priority 2: Use unstaged changes if no staged changes
79
- else if (status.files.length > 0) {
80
- diff = await git.diff(['-U200']);
81
- changedFiles = status.files
82
- .filter(f => f.working_dir !== ' ' || f.index !== ' ')
83
- .map(f => f.path);
170
+ // No staged files - log clearly and continue to unstaged/untracked
171
+ if (staged.length > 0) {
172
+ // git status showed staged files but git diff doesn't - they were likely unstaged
173
+ logger_1.logger.info(`No staged files detected (files may have been unstaged), checking unstaged/untracked files instead.`);
84
174
  }
85
- // No changes at all
86
175
  else {
176
+ logger_1.logger.info(`No staged files, checking unstaged/untracked files.`);
177
+ }
178
+ // Workflow B: Developer hasn't staged files - check unstaged + untracked files
179
+ // (Untracked files are conceptually "unstaged" - files being worked on but not committed)
180
+ if (unstaged.length > 0 || untracked.length > 0) {
181
+ // Get unstaged diff if there are unstaged files
182
+ if (unstaged.length > 0) {
183
+ diff = (0, child_process_1.execSync)('git diff -U200', {
184
+ encoding: 'utf-8',
185
+ cwd: repoRoot
186
+ });
187
+ const changedFilesOutput = (0, child_process_1.execSync)('git diff --name-only', {
188
+ encoding: 'utf-8',
189
+ cwd: repoRoot
190
+ }).trim();
191
+ changedFiles = changedFilesOutput ? changedFilesOutput.split('\n') : [];
192
+ }
193
+ else {
194
+ diff = '';
195
+ changedFiles = [];
196
+ }
197
+ // Handle untracked files: read their content and create artificial diffs
198
+ // Fails loudly if any untracked file cannot be read (permissions, filesystem errors, etc.)
199
+ const untrackedDiffs = [];
200
+ const untrackedFileList = [];
201
+ for (const file of untracked) {
202
+ const fullPath = path.resolve(repoRoot, file);
203
+ // Skip if it's a directory (git status can show directories)
204
+ const stats = fs.statSync(fullPath);
205
+ if (!stats.isFile()) {
206
+ continue;
207
+ }
208
+ // Read file content - fails loudly on any error (permissions, encoding, etc.)
209
+ const content = fs.readFileSync(fullPath, 'utf-8');
210
+ // Normalize path to forward slashes for cross-platform consistency
211
+ const normalizedPath = file.replace(/\\/g, '/');
212
+ // Create artificial diff (all lines as additions, similar to getFileContent)
213
+ const lines = content.split('\n');
214
+ const fileDiff = lines.map((line) => `+${line}`).join('\n');
215
+ // Add git diff header (matches format expected by server's filterDiffByFiles)
216
+ const diffHeader = `diff --git a/${normalizedPath} b/${normalizedPath}\n--- /dev/null\n+++ b/${normalizedPath}\n@@ -0,0 +1,${lines.length} @@\n`;
217
+ untrackedDiffs.push(diffHeader + fileDiff);
218
+ untrackedFileList.push(normalizedPath);
219
+ }
220
+ // Combine unstaged changes with untracked files
221
+ const combinedDiff = untrackedDiffs.length > 0
222
+ ? (diff ? diff + '\n' : '') + untrackedDiffs.join('\n')
223
+ : diff;
224
+ const allChangedFiles = [...changedFiles, ...untrackedFileList];
225
+ const unstagedCount = changedFiles.length;
226
+ const untrackedCount = untrackedFileList.length;
227
+ if (unstagedCount > 0 && untrackedCount > 0) {
228
+ logger_1.logger.info(`Checking UNSTAGED changes (${unstagedCount} file(s)) + ${untrackedCount} untracked file(s)`);
229
+ }
230
+ else if (unstagedCount > 0) {
231
+ logger_1.logger.info(`Checking UNSTAGED changes (${unstagedCount} file(s))`);
232
+ }
233
+ else {
234
+ logger_1.logger.info(`Checking UNTRACKED files (${untrackedCount} file(s))`);
235
+ }
87
236
  return {
88
- diff: '',
89
- changedFiles: []
237
+ diff: combinedDiff || '',
238
+ changedFiles: allChangedFiles
90
239
  };
91
240
  }
92
- return {
93
- diff: diff || '',
94
- changedFiles
95
- };
241
+ // No changes at all - fail loudly
242
+ throw new Error('No changes detected. Stage files with "git add" or modify files to run threadlines.');
96
243
  }
97
244
  /**
98
245
  * Gets branch name for local environment
99
246
  * (Uses git command directly - works in local because not in detached HEAD state)
100
247
  */
101
248
  async function getBranchName(repoRoot) {
102
- const git = (0, simple_git_1.default)(repoRoot);
103
249
  try {
104
- const branchSummary = await git.branchLocal();
105
- const currentBranch = branchSummary.current;
250
+ const currentBranch = (0, child_process_1.execSync)('git branch --show-current', {
251
+ encoding: 'utf-8',
252
+ cwd: repoRoot
253
+ }).trim();
106
254
  if (!currentBranch) {
107
255
  throw new Error('Could not determine current branch. Are you in a git repository?');
108
256
  }
@@ -120,17 +268,22 @@ async function getBranchName(repoRoot) {
120
268
  * No fallbacks - if git config is not set or fails, throws an error.
121
269
  */
122
270
  async function getCommitAuthorFromConfig(repoRoot) {
123
- const git = (0, simple_git_1.default)(repoRoot);
124
271
  try {
125
- const name = await git.getConfig('user.name');
126
- const email = await git.getConfig('user.email');
127
- if (!name.value || !email.value) {
272
+ const name = (0, child_process_1.execSync)('git config --get user.name', {
273
+ encoding: 'utf-8',
274
+ cwd: repoRoot
275
+ }).trim();
276
+ const email = (0, child_process_1.execSync)('git config --get user.email', {
277
+ encoding: 'utf-8',
278
+ cwd: repoRoot
279
+ }).trim();
280
+ if (!name || !email) {
128
281
  throw new Error('Git config user.name or user.email is not set. ' +
129
282
  'Run: git config user.name "Your Name" && git config user.email "your.email@example.com"');
130
283
  }
131
284
  return {
132
- name: name.value.trim(),
133
- email: email.value.trim()
285
+ name: name,
286
+ email: email
134
287
  };
135
288
  }
136
289
  catch (error) {
@@ -0,0 +1,72 @@
1
+ "use strict";
2
+ /**
3
+ * Prompt Builder for LLM Threadline Checks
4
+ *
5
+ * Builds prompts for OpenAI API calls to check code changes against threadline guidelines.
6
+ */
7
+ Object.defineProperty(exports, "__esModule", { value: true });
8
+ exports.buildPrompt = buildPrompt;
9
+ function buildPrompt(threadline, diff, matchingFiles) {
10
+ // Build context files section if available
11
+ const contextFilesSection = threadline.contextContent && Object.keys(threadline.contextContent).length > 0
12
+ ? `Context Files:\n${Object.entries(threadline.contextContent)
13
+ .map(([file, content]) => `\n--- ${file} ---\n${content}`)
14
+ .join('\n')}\n\n`
15
+ : '';
16
+ return `You are a code quality checker focused EXCLUSIVELY on: ${threadline.id}
17
+
18
+ CRITICAL: You must ONLY check for violations of THIS SPECIFIC threadline. Do NOT flag other code quality issues, style problems, or unrelated concerns.
19
+ If the code does not violate THIS threadline's specific rules, return "compliant" even if other issues exist.
20
+
21
+ Threadline Guidelines:
22
+ ${threadline.content}
23
+
24
+ ${contextFilesSection}Code Changes (Git Diff Format):
25
+ ${diff}
26
+
27
+ Changed Files:
28
+ ${matchingFiles.join('\n')}
29
+
30
+ Review the code changes AGAINST ONLY THE THREADLINE GUIDELINES ABOVE.
31
+
32
+ YOUR OBJECTIVES:
33
+ 1. Detect new violations being introduced in the code changes
34
+ 2. Review whether engineers have successfully addressed earlier violations
35
+
36
+ This is why it's important to look very carefully at the diff structure. You'll come across diffs that introduce new violations. You will also come across some that address earlier violations. The diff structure should allow you to tell which is which, because lines starting with '-' are removed in favour of lines with '+'.
37
+
38
+ CRITICAL CHECK BEFORE FLAGGING VIOLATIONS:
39
+ Before commenting on or flagging a violation in any line, look at the FIRST CHARACTER of that line:
40
+ * If it's a "-", the code is deleted.
41
+ → Only flag violations in lines starting with "+" (new code being added)
42
+ * If the first character is "+", this is NEW code being added - flag violations here if they violate the threadline
43
+ * If the line doesn't start with "+" or "-" (context lines), these are UNCHANGED - do NOT flag violations here
44
+ * Some violations may not be line-specific (e.g., file-level patterns, overall structure) - include those in your reasoning as well
45
+
46
+
47
+ IMPORTANT:
48
+ - Only flag violations of the specific rules defined in this threadline
49
+ - Ignore all other code quality issues, style problems, or unrelated concerns
50
+ - Focus on understanding the diff structure to distinguish between new violations and fixes
51
+
52
+ Return JSON only with this exact structure:
53
+ {
54
+ "status": "compliant" | "attention" | "not_relevant",
55
+ "reasoning": "explanation with file paths and line numbers embedded in the text (e.g., 'app/api/checks/route.ts:8 - The addition of...')",
56
+ "file_references": [file paths where violations occur - MUST match files from the diff, include ONLY files with violations]
57
+ }
58
+
59
+ CRITICAL: For each violation, you MUST:
60
+ 1. Embed the file path and line number(s) directly in your reasoning text (e.g., "app/api/checks/route.ts:8 - The addition of 'c.files_changed_counts' violates...")
61
+ 2. For line-specific violations, include the line number (e.g., "file.ts:42")
62
+ 3. For file-level or pattern violations, just include the file path (e.g., "file.ts")
63
+ 4. Include ONLY files that actually contain violations in "file_references" array
64
+ 5. Do NOT include files that don't have violations, even if they appear in the diff
65
+ 6. The "file_references" array should be a simple list of file paths - no line numbers needed there since they're in the reasoning
66
+
67
+ Status meanings:
68
+ - "compliant": Code follows THIS threadline's guidelines, no violations found (even if other issues exist)
69
+ - "attention": Code DIRECTLY violates THIS threadline's specific guidelines
70
+ - "not_relevant": This threadline doesn't apply to these files/changes (e.g., wrong file type, no matching code patterns)
71
+ `;
72
+ }
@@ -0,0 +1,120 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.processThreadlines = processThreadlines;
4
+ const single_expert_1 = require("./single-expert");
5
+ const logger_1 = require("../utils/logger");
6
+ const EXPERT_TIMEOUT = 40000; // 40 seconds
7
+ async function processThreadlines(request) {
8
+ const { threadlines, diff, files, apiKey, model, serviceTier, contextLinesForLLM } = request;
9
+ // Determine LLM model (same for all threadlines in this check)
10
+ const llmModel = `${model} ${serviceTier}`;
11
+ // Create promises with timeout
12
+ const promises = threadlines.map(threadline => {
13
+ let timeoutId = null;
14
+ let resolved = false;
15
+ const timeoutPromise = new Promise((resolve) => {
16
+ timeoutId = setTimeout(() => {
17
+ // Only log and resolve if we haven't already resolved
18
+ if (!resolved) {
19
+ logger_1.logger.error(`Request timed out after ${EXPERT_TIMEOUT / 1000}s for threadline: ${threadline.id}`);
20
+ resolved = true;
21
+ resolve({
22
+ expertId: threadline.id,
23
+ status: 'error',
24
+ reasoning: `Error: Request timed out after ${EXPERT_TIMEOUT / 1000}s`,
25
+ error: {
26
+ message: `Request timed out after ${EXPERT_TIMEOUT / 1000}s`,
27
+ type: 'timeout'
28
+ },
29
+ fileReferences: [],
30
+ relevantFiles: [],
31
+ filteredDiff: '',
32
+ filesInFilteredDiff: [],
33
+ actualModel: undefined
34
+ });
35
+ }
36
+ }, EXPERT_TIMEOUT);
37
+ });
38
+ const actualPromise = (0, single_expert_1.processThreadline)(threadline, diff, files, apiKey, model, serviceTier, contextLinesForLLM).then(result => {
39
+ // Mark as resolved and clear timeout if it hasn't fired yet
40
+ resolved = true;
41
+ if (timeoutId) {
42
+ clearTimeout(timeoutId);
43
+ }
44
+ return result;
45
+ });
46
+ return Promise.race([actualPromise, timeoutPromise]);
47
+ });
48
+ // Wait for all (some may timeout)
49
+ const results = await Promise.allSettled(promises);
50
+ // Process results
51
+ const expertResults = [];
52
+ let completed = 0;
53
+ let timedOut = 0;
54
+ let errors = 0;
55
+ let actualModelFromResponse;
56
+ for (let i = 0; i < results.length; i++) {
57
+ const result = results[i];
58
+ const threadline = threadlines[i];
59
+ if (result.status === 'fulfilled') {
60
+ const expertResult = result.value;
61
+ // Check status directly - errors and timeouts are now 'error' status
62
+ if (expertResult.status === 'error') {
63
+ // Check if it's a timeout (has error.type === 'timeout')
64
+ if ('error' in expertResult && expertResult.error?.type === 'timeout') {
65
+ timedOut++;
66
+ }
67
+ else {
68
+ errors++;
69
+ }
70
+ }
71
+ else {
72
+ completed++;
73
+ }
74
+ expertResults.push(expertResult);
75
+ // Capture actual model from first successful result (all threadlines use same model)
76
+ if (!actualModelFromResponse && 'actualModel' in expertResult && expertResult.actualModel) {
77
+ actualModelFromResponse = expertResult.actualModel;
78
+ }
79
+ }
80
+ else {
81
+ errors++;
82
+ expertResults.push({
83
+ expertId: threadline.id,
84
+ status: 'error',
85
+ reasoning: `Error: ${result.reason?.message || 'Unknown error'}`,
86
+ error: {
87
+ message: result.reason?.message || 'Unknown error',
88
+ rawResponse: result.reason
89
+ },
90
+ fileReferences: [],
91
+ relevantFiles: [],
92
+ filteredDiff: '',
93
+ filesInFilteredDiff: []
94
+ });
95
+ }
96
+ }
97
+ // Use actual model from OpenAI response, append service tier
98
+ let modelToStore;
99
+ if (actualModelFromResponse) {
100
+ modelToStore = `${actualModelFromResponse} ${serviceTier}`;
101
+ }
102
+ else {
103
+ // All calls failed - log prominently and preserve requested model for debugging
104
+ logger_1.logger.error(`No successful LLM responses received. Requested model: ${llmModel}`);
105
+ logger_1.logger.error(`Completed: ${completed}, Timed out: ${timedOut}, Errors: ${errors}`);
106
+ // Store requested model so we can debug what was attempted
107
+ modelToStore = `${llmModel} (no successful responses)`;
108
+ }
109
+ // Return all results - CLI will handle filtering/display
110
+ return {
111
+ results: expertResults,
112
+ metadata: {
113
+ totalThreadlines: threadlines.length,
114
+ completed,
115
+ timedOut,
116
+ errors,
117
+ llmModel: modelToStore
118
+ }
119
+ };
120
+ }