@codraoss/core 0.9.4 → 0.9.5

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.
Files changed (62) hide show
  1. package/dist/{chunk-XD6CGYHO.js → chunk-2TF4DSRX.js} +65 -42
  2. package/dist/chunk-2TF4DSRX.js.map +1 -0
  3. package/dist/{chunk-ILZUCIZH.js → chunk-754NO4DH.js} +7 -7
  4. package/dist/chunk-754NO4DH.js.map +1 -0
  5. package/dist/{chunk-RQ5S67XB.js → chunk-FKRCBUWZ.js} +8 -2
  6. package/dist/chunk-FKRCBUWZ.js.map +1 -0
  7. package/dist/{chunk-OIQRTDYR.js → chunk-I5S3NBFL.js} +20 -4
  8. package/dist/chunk-I5S3NBFL.js.map +1 -0
  9. package/dist/{chunk-U3VKVDZ7.js → chunk-IKVUW4WC.js} +92 -19
  10. package/dist/chunk-IKVUW4WC.js.map +1 -0
  11. package/dist/{chunk-R2HRWFRH.js → chunk-JAF7JIZQ.js} +1 -1
  12. package/dist/chunk-JAF7JIZQ.js.map +1 -0
  13. package/dist/{chunk-V3RF6GQW.js → chunk-KTDBDKL5.js} +1 -1
  14. package/dist/chunk-KTDBDKL5.js.map +1 -0
  15. package/dist/{chunk-MMVQPGK3.js → chunk-ONJZXUQT.js} +112 -24
  16. package/dist/chunk-ONJZXUQT.js.map +1 -0
  17. package/dist/{chunk-PVXFURDR.js → chunk-QPPLO2YV.js} +2 -2
  18. package/dist/chunk-QPPLO2YV.js.map +1 -0
  19. package/dist/{chunk-FGXA7JNR.js → chunk-W3RY755V.js} +14 -2
  20. package/dist/chunk-W3RY755V.js.map +1 -0
  21. package/dist/{chunk-NSTV4TRP.js → chunk-XPYVLHSX.js} +1 -1
  22. package/dist/chunk-XPYVLHSX.js.map +1 -0
  23. package/dist/{chunk-WSK45HW4.js → chunk-Z5B5X7QP.js} +1 -1
  24. package/dist/chunk-Z5B5X7QP.js.map +1 -0
  25. package/dist/claim-checks.d.ts +8 -13
  26. package/dist/claim-checks.js +2 -2
  27. package/dist/diff/index.d.ts +3 -3
  28. package/dist/diff/index.js +2 -1
  29. package/dist/fingerprint.js +1 -1
  30. package/dist/{index-BTDWXDck.d.ts → index-CrtHz4vN.d.ts} +3 -1
  31. package/dist/index.d.ts +11 -7
  32. package/dist/index.js +240 -61
  33. package/dist/index.js.map +1 -1
  34. package/dist/logger.js +1 -1
  35. package/dist/{model-gRDTk8yk.d.ts → model-BkqfyVh9.d.ts} +27 -6
  36. package/dist/model-output/index.d.ts +2 -2
  37. package/dist/model-output/index.js +9 -9
  38. package/dist/ports/index.d.ts +28 -6
  39. package/dist/{position-h_jdn6ZH.d.ts → position-zZl8KGVu.d.ts} +1 -1
  40. package/dist/prompts/file-review.d.ts +3 -3
  41. package/dist/prompts/file-review.js +11 -5
  42. package/dist/prompts/languages.d.ts +2 -1
  43. package/dist/prompts/languages.js +5 -3
  44. package/dist/prompts/verify.d.ts +1 -1
  45. package/dist/prompts/verify.js +1 -1
  46. package/dist/rules/detect.d.ts +1 -1
  47. package/dist/rules/detect.js +5 -5
  48. package/dist/rules/table.js +1 -1
  49. package/dist/token-tracker.js +2 -2
  50. package/package.json +2 -2
  51. package/dist/chunk-FGXA7JNR.js.map +0 -1
  52. package/dist/chunk-ILZUCIZH.js.map +0 -1
  53. package/dist/chunk-MMVQPGK3.js.map +0 -1
  54. package/dist/chunk-NSTV4TRP.js.map +0 -1
  55. package/dist/chunk-OIQRTDYR.js.map +0 -1
  56. package/dist/chunk-PVXFURDR.js.map +0 -1
  57. package/dist/chunk-R2HRWFRH.js.map +0 -1
  58. package/dist/chunk-RQ5S67XB.js.map +0 -1
  59. package/dist/chunk-U3VKVDZ7.js.map +0 -1
  60. package/dist/chunk-V3RF6GQW.js.map +0 -1
  61. package/dist/chunk-WSK45HW4.js.map +0 -1
  62. package/dist/chunk-XD6CGYHO.js.map +0 -1
package/dist/logger.js CHANGED
@@ -5,7 +5,7 @@ import {
5
5
  redact,
6
6
  scrubString,
7
7
  setLoggerSink
8
- } from "./chunk-WSK45HW4.js";
8
+ } from "./chunk-Z5B5X7QP.js";
9
9
  export {
10
10
  consoleLogger,
11
11
  formatLogRecord,
@@ -1,17 +1,30 @@
1
1
  import { RepoConfig } from '@codraoss/schema';
2
- import { F as FileDiff } from './position-h_jdn6ZH.js';
3
- import { p as parseFileReviewResponse, B as BatchReviewResult } from './index-BTDWXDck.js';
2
+ import { F as FileDiff } from './position-zZl8KGVu.js';
3
+ import { p as parseFileReviewResponse, B as BatchReviewResult } from './index-CrtHz4vN.js';
4
4
  import { VerifyCandidate } from './prompts/verify.js';
5
5
 
6
+ declare function changelogExcerptFromDiff(files: readonly FileDiff[]): string | null;
7
+ declare function wantsFileContext(file: Pick<FileDiff, 'lineCount' | 'isNew' | 'isDeleted' | 'isBinary'> & {
8
+ hunks?: FileDiff['hunks'];
9
+ }, fullFileContext: boolean, gate?: {
10
+ compactPrompt?: boolean;
11
+ }): boolean;
12
+
6
13
  declare function generatorFindingCap(maxComments: number): number;
7
- declare function buildReviewResponseSchema(maxComments: number): ModelResponseSchema;
8
- declare function buildBatchReviewResponseSchema(maxComments: number, fileCount: number): ModelResponseSchema;
14
+ /** Internal candidate cap upstream of posting; falls back for job snapshots queued before this field existed. */
15
+ declare function reviewBreadth(config: Pick<RepoConfig['review'], 'max_comments'> & {
16
+ review_breadth?: number;
17
+ }): number;
18
+ declare function buildReviewResponseSchema(findingCap: number): ModelResponseSchema;
19
+ declare function buildBatchReviewResponseSchema(findingCap: number, fileCount: number): ModelResponseSchema;
9
20
  declare function buildFileReviewSystemPromptBase(opts?: {
10
21
  multiFile?: boolean;
22
+ fileContext?: boolean;
11
23
  }): string;
12
24
  declare const fileReviewSystemPromptBase: string;
13
25
  declare function buildFileReviewSystemPrompt(config: RepoConfig['review'], languagePersona?: string, opts?: {
14
26
  multiFile?: boolean;
27
+ fileContext?: boolean;
15
28
  }): string;
16
29
  type RejectedExemplar = {
17
30
  title: string;
@@ -19,8 +32,10 @@ type RejectedExemplar = {
19
32
  };
20
33
  declare function buildFileReviewPrompts(input: {
21
34
  file: FileDiff;
35
+ fileContext?: string | null;
22
36
  prTitle: string | null;
23
37
  prDescription: string | null;
38
+ changelogExcerpt?: string | null;
24
39
  config: RepoConfig['review'];
25
40
  rejectedExemplars?: readonly RejectedExemplar[];
26
41
  }): {
@@ -31,6 +46,7 @@ declare function buildBatchReviewPrompts(input: {
31
46
  files: readonly FileDiff[];
32
47
  prTitle: string | null;
33
48
  prDescription: string | null;
49
+ changelogExcerpt?: string | null;
34
50
  config: RepoConfig['review'];
35
51
  rejectedExemplars?: readonly RejectedExemplar[];
36
52
  }): {
@@ -46,7 +62,7 @@ type ModelResponse = {
46
62
  outputTokens: number;
47
63
  modelUsed: string;
48
64
  provider: string;
49
- degraded?: 'schema-dropped';
65
+ degraded?: 'schema-dropped' | 'schema-dropped-catchall' | 'truncated';
50
66
  };
51
67
  type ModelResponseSchema = {
52
68
  name: string;
@@ -61,8 +77,10 @@ type FileReviewOutcome = ModelResponse & {
61
77
  interface ReviewModel {
62
78
  reviewFile(params: {
63
79
  file: FileDiff;
80
+ fileContext?: string | null;
64
81
  prTitle: string | null;
65
82
  prDescription: string | null;
83
+ changelogExcerpt?: string | null;
66
84
  config: RepoConfig;
67
85
  totalLineCount: number;
68
86
  compactPrompt?: boolean;
@@ -72,6 +90,7 @@ interface ReviewModel {
72
90
  files: readonly FileDiff[];
73
91
  prTitle: string | null;
74
92
  prDescription: string | null;
93
+ changelogExcerpt?: string | null;
75
94
  config: RepoConfig;
76
95
  totalLineCount: number;
77
96
  rejectedExemplars?: readonly RejectedExemplar[];
@@ -81,8 +100,10 @@ interface ReviewModel {
81
100
  }>;
82
101
  submitReviewBatch(params: {
83
102
  file: FileDiff;
103
+ fileContext?: string | null;
84
104
  prTitle: string | null;
85
105
  prDescription: string | null;
106
+ changelogExcerpt?: string | null;
86
107
  config: RepoConfig;
87
108
  totalLineCount: number;
88
109
  compactPrompt?: boolean;
@@ -114,4 +135,4 @@ interface ModelErrorClassifier {
114
135
  nextChainIndexOf(error: unknown): number | null;
115
136
  }
116
137
 
117
- export { type FileReviewOutcome as F, type ModelErrorClassifier as M, type ReviewModel as R, type ModelResponse as a, type ModelResponseSchema as b, type RejectedExemplar as c, buildBatchReviewPrompts as d, buildBatchReviewResponseSchema as e, buildFileReviewPrompts as f, buildFileReviewSystemPrompt as g, buildFileReviewSystemPromptBase as h, buildReviewResponseSchema as i, fileReviewSystemPromptBase as j, generatorFindingCap as k, renderFileDiff as r };
138
+ export { type FileReviewOutcome as F, type ModelErrorClassifier as M, type ReviewModel as R, type ModelResponse as a, type ModelResponseSchema as b, type RejectedExemplar as c, buildBatchReviewPrompts as d, buildBatchReviewResponseSchema as e, buildFileReviewPrompts as f, buildFileReviewSystemPrompt as g, buildFileReviewSystemPromptBase as h, buildReviewResponseSchema as i, changelogExcerptFromDiff as j, fileReviewSystemPromptBase as k, generatorFindingCap as l, reviewBreadth as m, renderFileDiff as r, wantsFileContext as w };
@@ -1,4 +1,4 @@
1
1
  import '@codraoss/schema';
2
2
  import 'zod';
3
- import '../position-h_jdn6ZH.js';
4
- export { d as BatchParseStats, B as BatchReviewResult, e as BinAmbiguity, f as FileReviewPayload, G as GroundedFileReview, g as GroundingOptions, N as NON_ANSWER_MAX_RESPONSE_CHARS, h as NON_ANSWER_MIN_DIFF_LINES, R as RawBatchPayload, i as dedupeFindings, j as groundParsedFindings, k as isNonAnswerReview, l as parseBatchReviewResponse, p as parseFileReviewResponse, m as parseRawBatchPayload, s as samePath } from '../index-BTDWXDck.js';
3
+ import '../position-zZl8KGVu.js';
4
+ export { d as BatchParseStats, B as BatchReviewResult, e as BinAmbiguity, f as FileReviewPayload, G as GroundedFileReview, g as GroundingOptions, N as NON_ANSWER_MAX_RESPONSE_CHARS, h as NON_ANSWER_MIN_DIFF_LINES, R as RawBatchPayload, i as dedupeFindings, j as groundParsedFindings, k as isNonAnswerReview, l as parseBatchReviewResponse, p as parseFileReviewResponse, m as parseRawBatchPayload, s as samePath } from '../index-CrtHz4vN.js';
@@ -6,18 +6,18 @@ import {
6
6
  parseFileReviewResponse,
7
7
  parseRawBatchPayload,
8
8
  samePath
9
- } from "../chunk-XD6CGYHO.js";
10
- import "../chunk-MMVQPGK3.js";
11
- import "../chunk-RQ5S67XB.js";
12
- import "../chunk-V3RF6GQW.js";
9
+ } from "../chunk-2TF4DSRX.js";
10
+ import "../chunk-ONJZXUQT.js";
11
+ import "../chunk-FKRCBUWZ.js";
12
+ import "../chunk-KTDBDKL5.js";
13
+ import "../chunk-IKVUW4WC.js";
14
+ import "../chunk-XPYVLHSX.js";
15
+ import "../chunk-Z5B5X7QP.js";
16
+ import "../chunk-I5S3NBFL.js";
13
17
  import {
14
18
  NON_ANSWER_MAX_RESPONSE_CHARS,
15
19
  NON_ANSWER_MIN_DIFF_LINES
16
- } from "../chunk-FGXA7JNR.js";
17
- import "../chunk-U3VKVDZ7.js";
18
- import "../chunk-NSTV4TRP.js";
19
- import "../chunk-WSK45HW4.js";
20
- import "../chunk-OIQRTDYR.js";
20
+ } from "../chunk-W3RY755V.js";
21
21
  export {
22
22
  NON_ANSWER_MAX_RESPONSE_CHARS,
23
23
  NON_ANSWER_MIN_DIFF_LINES,
@@ -1,10 +1,10 @@
1
1
  import { JobSummary, RepoConfig, ParsedReviewComment, ClaimType, ReviewSettings, ReviewJobMessage } from '@codraoss/schema';
2
- import { R as ReviewModel, M as ModelErrorClassifier } from '../model-gRDTk8yk.js';
3
- export { F as FileReviewOutcome, a as ModelResponse, b as ModelResponseSchema } from '../model-gRDTk8yk.js';
2
+ import { R as ReviewModel, M as ModelErrorClassifier } from '../model-BkqfyVh9.js';
3
+ export { F as FileReviewOutcome, a as ModelResponse, b as ModelResponseSchema } from '../model-BkqfyVh9.js';
4
4
  import { TokenTracker } from '../token-tracker.js';
5
5
  export { Logger } from '../logger.js';
6
- import '../position-h_jdn6ZH.js';
7
- import '../index-BTDWXDck.js';
6
+ import '../position-zZl8KGVu.js';
7
+ import '../index-CrtHz4vN.js';
8
8
  import 'zod';
9
9
  import '../prompts/verify.js';
10
10
 
@@ -155,6 +155,8 @@ type FileReviewRow = {
155
155
  withheld_counts: {
156
156
  evidence?: number;
157
157
  claimDenied?: number;
158
+ contextOnly?: number;
159
+ absenceRefuted?: number;
158
160
  };
159
161
  batch_size: number | null;
160
162
  };
@@ -183,7 +185,10 @@ type BulkFileReviewInput = {
183
185
  withheldCounts?: {
184
186
  evidence: number;
185
187
  claimDenied: number;
188
+ contextOnly?: number;
189
+ absenceRefuted?: number;
186
190
  } | null;
191
+ degraded?: string | null;
187
192
  batchSize: number;
188
193
  };
189
194
  interface FileReviewStore {
@@ -207,7 +212,10 @@ interface FileReviewStore {
207
212
  withheldCounts?: {
208
213
  evidence: number;
209
214
  claimDenied: number;
215
+ contextOnly?: number;
216
+ absenceRefuted?: number;
210
217
  } | null;
218
+ degraded?: string | null;
211
219
  asyncRequestId?: string | null;
212
220
  asyncModel?: string | null;
213
221
  }): Promise<void>;
@@ -324,6 +332,8 @@ interface ReviewGitProvider {
324
332
  getPullRequest(owner: string, repo: string, prNumber: number): Promise<PullRequestRecord>;
325
333
  getPullRequestDiff(owner: string, repo: string, prNumber: number): Promise<string>;
326
334
  getCompareDiff(owner: string, repo: string, base: string, head: string): Promise<string>;
335
+ /** File content at `ref`, or null if unavailable. Optional: backs opt-in file-context enrichment. */
336
+ getRepoFile?(owner: string, repo: string, path: string, ref?: string): Promise<string | null>;
327
337
  createCheckRun(owner: string, repo: string, params: {
328
338
  headSha: string;
329
339
  title: string;
@@ -351,6 +361,8 @@ interface ReviewGitProvider {
351
361
  } | null>;
352
362
  ensureLabel(owner: string, repo: string, name: string, color: string): Promise<unknown>;
353
363
  addIssueLabels(owner: string, repo: string, prNumber: number, labels: string[]): Promise<unknown>;
364
+ /** Optional: a provider or test double without it simply does not react. */
365
+ addIssueReaction?(owner: string, repo: string, prNumber: number, content: '+1'): Promise<unknown>;
354
366
  removeIssueLabelsIfPresent(owner: string, repo: string, prNumber: number, labels: string[]): Promise<unknown>;
355
367
  }
356
368
  interface GitProviderFactory {
@@ -365,8 +377,18 @@ interface ReviewFormatter {
365
377
  warnings: number;
366
378
  };
367
379
  formatInlineComment(comment: ParsedReviewComment): string;
368
- formatReviewOverview(commitSha: string, botUsername: string): string;
380
+ formatReviewOverview(input: ReviewOverviewInput): string;
369
381
  }
382
+ type ReviewOverviewInput = {
383
+ commitSha: string;
384
+ /** Comments actually posted. Zero means the header must not promise suggestions. */
385
+ postedFindings: number;
386
+ filesReviewed: number;
387
+ linesReviewed: number;
388
+ /** Candidates the gates dropped; on a clean review this is what "nothing to report" cost. */
389
+ withheldFindings: number;
390
+ filesFailed: number;
391
+ };
370
392
 
371
393
  type ReviewTelemetryEvent = {
372
394
  linesReviewed: number;
@@ -509,4 +531,4 @@ declare class InMemorySessionStore implements SessionStore {
509
531
  renewSession(token: string): Promise<void>;
510
532
  }
511
533
 
512
- export { type AuthorizationResult, type BulkFileReviewInput, type Clock, type DashboardSessionUser, type FileReviewRow, type FileReviewStore, type GitProviderFactory, type IdGenerator, type IdentityProvider, InMemoryKV, InMemoryOrchestrator, InMemoryQueue, InMemorySessionStore, type InstanceIdStore, type JobLeaseClaim, type JobOrchestrator, type JobRow, type JobStore, type KeyValueStore, type KvStore, type LearningStore, type ModelConfigReader, ModelErrorClassifier, type PersistedReviewJob, type PullRequestRecord, type QueueProducer, type RepoConfigLoader, type RepoConfigStore, type ReviewComment, type ReviewFormatter, type ReviewGitProvider, ReviewModel, type ReviewRuntime, type ReviewSettingsReader, type ReviewTelemetryEvent, type SecretStore, type SessionStore, type SuppressedFinding, type TelemetrySink, type WebhookDeliveryReader };
534
+ export { type AuthorizationResult, type BulkFileReviewInput, type Clock, type DashboardSessionUser, type FileReviewRow, type FileReviewStore, type GitProviderFactory, type IdGenerator, type IdentityProvider, InMemoryKV, InMemoryOrchestrator, InMemoryQueue, InMemorySessionStore, type InstanceIdStore, type JobLeaseClaim, type JobOrchestrator, type JobRow, type JobStore, type KeyValueStore, type KvStore, type LearningStore, type ModelConfigReader, ModelErrorClassifier, type PersistedReviewJob, type PullRequestRecord, type QueueProducer, type RepoConfigLoader, type RepoConfigStore, type ReviewComment, type ReviewFormatter, type ReviewGitProvider, ReviewModel, type ReviewOverviewInput, type ReviewRuntime, type ReviewSettingsReader, type ReviewTelemetryEvent, type SecretStore, type SessionStore, type SuppressedFinding, type TelemetrySink, type WebhookDeliveryReader };
@@ -27,4 +27,4 @@ declare function findPositionForLine(file: FileDiff, lineNumber: number): number
27
27
  declare function truncateFileDiff(file: FileDiff, maxLines: number): FileDiff;
28
28
  declare function chunkFileDiff(file: FileDiff, maxLinesPerChunk: number): FileDiff[];
29
29
 
30
- export { type DiffLine as D, type FileDiff as F, type DiffHunk as a, type DiffLineKind as b, chunkFileDiff as c, getValidPositions as d, findPositionForLine as f, getValidNewLines as g, truncateFileDiff as t };
30
+ export { type DiffHunk as D, type FileDiff as F, type DiffLine as a, type DiffLineKind as b, chunkFileDiff as c, getValidPositions as d, findPositionForLine as f, getValidNewLines as g, truncateFileDiff as t };
@@ -1,6 +1,6 @@
1
1
  import '@codraoss/schema';
2
- import '../position-h_jdn6ZH.js';
3
- export { c as RejectedExemplar, d as buildBatchReviewPrompts, e as buildBatchReviewResponseSchema, f as buildFileReviewPrompts, g as buildFileReviewSystemPrompt, h as buildFileReviewSystemPromptBase, i as buildReviewResponseSchema, j as fileReviewSystemPromptBase, k as generatorFindingCap, r as renderFileDiff } from '../model-gRDTk8yk.js';
4
- import '../index-BTDWXDck.js';
2
+ import '../position-zZl8KGVu.js';
3
+ export { c as RejectedExemplar, d as buildBatchReviewPrompts, e as buildBatchReviewResponseSchema, f as buildFileReviewPrompts, g as buildFileReviewSystemPrompt, h as buildFileReviewSystemPromptBase, i as buildReviewResponseSchema, j as changelogExcerptFromDiff, k as fileReviewSystemPromptBase, l as generatorFindingCap, r as renderFileDiff, m as reviewBreadth, w as wantsFileContext } from '../model-BkqfyVh9.js';
4
+ import '../index-CrtHz4vN.js';
5
5
  import 'zod';
6
6
  import './verify.js';
@@ -5,12 +5,15 @@ import {
5
5
  buildFileReviewSystemPrompt,
6
6
  buildFileReviewSystemPromptBase,
7
7
  buildReviewResponseSchema,
8
+ changelogExcerptFromDiff,
8
9
  fileReviewSystemPromptBase,
9
10
  generatorFindingCap,
10
- renderFileDiff
11
- } from "../chunk-MMVQPGK3.js";
12
- import "../chunk-RQ5S67XB.js";
13
- import "../chunk-FGXA7JNR.js";
11
+ renderFileDiff,
12
+ reviewBreadth,
13
+ wantsFileContext
14
+ } from "../chunk-ONJZXUQT.js";
15
+ import "../chunk-FKRCBUWZ.js";
16
+ import "../chunk-W3RY755V.js";
14
17
  export {
15
18
  buildBatchReviewPrompts,
16
19
  buildBatchReviewResponseSchema,
@@ -18,8 +21,11 @@ export {
18
21
  buildFileReviewSystemPrompt,
19
22
  buildFileReviewSystemPromptBase,
20
23
  buildReviewResponseSchema,
24
+ changelogExcerptFromDiff,
21
25
  fileReviewSystemPromptBase,
22
26
  generatorFindingCap,
23
- renderFileDiff
27
+ renderFileDiff,
28
+ reviewBreadth,
29
+ wantsFileContext
24
30
  };
25
31
  //# sourceMappingURL=file-review.js.map
@@ -5,5 +5,6 @@ type LanguageGuideline = {
5
5
  persona?: string;
6
6
  };
7
7
  declare function getLanguageForFile(path: string): LanguageGuideline | undefined;
8
+ declare function isChangelogPath(path: string): boolean;
8
9
 
9
- export { type LanguageGuideline, getLanguageForFile };
10
+ export { type LanguageGuideline, getLanguageForFile, isChangelogPath };
@@ -1,7 +1,9 @@
1
1
  import {
2
- getLanguageForFile
3
- } from "../chunk-RQ5S67XB.js";
2
+ getLanguageForFile,
3
+ isChangelogPath
4
+ } from "../chunk-FKRCBUWZ.js";
4
5
  export {
5
- getLanguageForFile
6
+ getLanguageForFile,
7
+ isChangelogPath
6
8
  };
7
9
  //# sourceMappingURL=languages.js.map
@@ -1,5 +1,5 @@
1
1
  import { z } from 'zod';
2
- import { F as FileDiff } from '../position-h_jdn6ZH.js';
2
+ import { F as FileDiff } from '../position-zZl8KGVu.js';
3
3
 
4
4
  type VerifyCandidate = {
5
5
  index: number;
@@ -4,7 +4,7 @@ import {
4
4
  buildVerifyPrompt,
5
5
  parseVerifyResponse,
6
6
  renderDiffSnippet
7
- } from "../chunk-V3RF6GQW.js";
7
+ } from "../chunk-KTDBDKL5.js";
8
8
  export {
9
9
  VERIFY_RESPONSE_SCHEMA,
10
10
  VERIFY_SYSTEM_PROMPT,
@@ -1,5 +1,5 @@
1
1
  import { ClaimType, ParsedReviewComment } from '@codraoss/schema';
2
- import { D as DiffLine, F as FileDiff } from '../position-h_jdn6ZH.js';
2
+ import { a as DiffLine, F as FileDiff } from '../position-zZl8KGVu.js';
3
3
  import { Rule } from './table.js';
4
4
 
5
5
  type RuleHit = {
@@ -1,11 +1,11 @@
1
1
  import {
2
2
  ruleHitsToComments,
3
3
  scanFileForRuleHits
4
- } from "../chunk-ILZUCIZH.js";
5
- import "../chunk-FGXA7JNR.js";
6
- import "../chunk-R2HRWFRH.js";
7
- import "../chunk-U3VKVDZ7.js";
8
- import "../chunk-NSTV4TRP.js";
4
+ } from "../chunk-754NO4DH.js";
5
+ import "../chunk-JAF7JIZQ.js";
6
+ import "../chunk-IKVUW4WC.js";
7
+ import "../chunk-XPYVLHSX.js";
8
+ import "../chunk-W3RY755V.js";
9
9
  export {
10
10
  ruleHitsToComments,
11
11
  scanFileForRuleHits
@@ -1,6 +1,6 @@
1
1
  import {
2
2
  RULES
3
- } from "../chunk-R2HRWFRH.js";
3
+ } from "../chunk-JAF7JIZQ.js";
4
4
  export {
5
5
  RULES
6
6
  };
@@ -1,7 +1,7 @@
1
1
  import {
2
2
  TokenTracker
3
- } from "./chunk-PVXFURDR.js";
4
- import "./chunk-WSK45HW4.js";
3
+ } from "./chunk-QPPLO2YV.js";
4
+ import "./chunk-Z5B5X7QP.js";
5
5
  export {
6
6
  TokenTracker
7
7
  };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@codraoss/core",
3
- "version": "0.9.4",
3
+ "version": "0.9.5",
4
4
  "description": "The Codra review engine: pure, transport- and platform-agnostic code review logic behind ports.",
5
5
  "author": "Devarshi Shimpi",
6
6
  "license": "AGPL-3.0-only",
@@ -166,7 +166,7 @@
166
166
  "postpack": "node ../../scripts/swap-publish-exports.mjs restore"
167
167
  },
168
168
  "dependencies": {
169
- "@codraoss/schema": "^0.9.4",
169
+ "@codraoss/schema": "^0.9.5",
170
170
  "jsonrepair": "^3.15.0",
171
171
  "picomatch": "^4.0.5",
172
172
  "zod": "^4.3.6"
@@ -1 +0,0 @@
1
- {"version":3,"sources":["../src/constants.ts"],"sourcesContent":["// Packing & Diff Limits\nexport const PACKABLE_MAX_DIFF_LINES = 150;\nexport const BIN_TARGET_DIFF_LINES = 300;\nexport const BIN_MAX_FILES = 4;\nexport const BIN_DIFF_CHAR_BUDGET = 24_000;\nexport const DIFF_CACHE_TTL_SECONDS = 6 * 60 * 60;\n\n// Phase Control & Timers\nexport const REVIEW_CHUNK_WALL_CLOCK_MS = 12 * 60 * 1000;\nexport const JOB_LEASE_SECONDS = 15 * 60;\nexport const BUSY_RETRY_SECONDS = 60;\nexport const RETRYABLE_MODEL_FAILURE_RETRY_DELAYS_SECONDS = [30, 2 * 60, 5 * 60];\nexport const FRESH_INVOCATION_YIELD_SECONDS = 8;\nexport const ASYNC_BATCH_POLL_DELAY_SECONDS = 20;\nexport const MAX_RETRYABLE_FILE_REVIEW_FAILURES = 6;\nexport const MAX_JOB_CONTINUATIONS = 20;\nexport const MAX_FINALIZE_CONTINUATIONS = 3;\n\n// Budget & Attempts\nexport const FILE_FIXED_SUBREQUESTS = 2;\nexport const MAX_MODEL_ATTEMPTS_ESTIMATE = 4;\nexport const MISSING_FILE_ERROR = 'Model omitted this file from a batched review; retrying later.';\n\n// Model Output Parsing\nexport const MIN_DISCRIMINATING_EVIDENCE_CHARS = 8;\nexport const NON_ANSWER_MAX_RESPONSE_CHARS = 600;\nexport const NON_ANSWER_MIN_DIFF_LINES = 200;\nexport const MAX_LOGGED_JSON_CHARS = 2_000;\nexport const SEVERITY_ORDER = ['P0', 'P1', 'P2', 'P3', 'nit'] as const;\n\n// Finding Gates\nexport const VERIFY_MIN_ANSWER_RATIO = 0.6;\n\n// Rules\nexport const MAX_RULE_SCAN_ADDED_LINES = 600;\n\n// Prompts\nexport const EXEMPLAR_BLOCK_CHARS = 700;\nexport const PR_DESCRIPTION_CHARS = 2_000;\n"],"mappings":";AACO,IAAM,0BAA0B;AAChC,IAAM,wBAAwB;AAC9B,IAAM,gBAAgB;AACtB,IAAM,uBAAuB;AAC7B,IAAM,yBAAyB,IAAI,KAAK;AAGxC,IAAM,6BAA6B,KAAK,KAAK;AAC7C,IAAM,oBAAoB,KAAK;AAC/B,IAAM,qBAAqB;AAC3B,IAAM,+CAA+C,CAAC,IAAI,IAAI,IAAI,IAAI,EAAE;AACxE,IAAM,iCAAiC;AACvC,IAAM,iCAAiC;AACvC,IAAM,qCAAqC;AAC3C,IAAM,wBAAwB;AAC9B,IAAM,6BAA6B;AAGnC,IAAM,yBAAyB;AAC/B,IAAM,8BAA8B;AACpC,IAAM,qBAAqB;AAG3B,IAAM,oCAAoC;AAC1C,IAAM,gCAAgC;AACtC,IAAM,4BAA4B;AAClC,IAAM,wBAAwB;AAC9B,IAAM,iBAAiB,CAAC,MAAM,MAAM,MAAM,MAAM,KAAK;AAGrD,IAAM,0BAA0B;AAGhC,IAAM,4BAA4B;AAGlC,IAAM,uBAAuB;AAC7B,IAAM,uBAAuB;","names":[]}
@@ -1 +0,0 @@
1
- {"version":3,"sources":["../src/rules/detect.ts"],"sourcesContent":["import type { ClaimType, ParsedReviewComment } from '@codraoss/schema';\r\nimport type { DiffLine, FileDiff } from '../diff';\r\nimport { commentSyntaxFor, stripCommentsAndStrings } from '../claim-checks';\r\nimport { buildAnchorHash, buildFindingFingerprint, buildFindingFingerprintV2, normalizeDiffText } from '../fingerprint';\r\nimport { CLAIM_TYPE_CATEGORY } from '@codraoss/schema';\r\nimport { RULES, type Rule } from './table';\r\n\r\nimport { MAX_RULE_SCAN_ADDED_LINES } from '../constants';\r\n\r\nexport type RuleHit = {\r\n rule: Rule;\r\n line: DiffLine;\r\n shadow: boolean;\r\n};\r\n\r\nexport type RuleScanStats = {\r\n addedLinesScanned: number;\r\n sievePassed: number;\r\n hits: number;\r\n shadowHits: number;\r\n suppressedAsMoved: number;\r\n unstrippable: number;\r\n truncated: boolean;\r\n byRule: Record<string, number>;\r\n};\r\n\r\nexport type RuleScanResult = { hits: RuleHit[]; stats: RuleScanStats };\r\n\r\nexport type RuleScanOptions = {\r\n disabledRuleIds?: readonly string[];\r\n shadowRuleIds?: readonly string[];\r\n deniedClaimTypes?: readonly ClaimType[];\r\n};\r\n\r\nfunction extensionOf(path: string) {\r\n return path.toLowerCase().split('.').pop() ?? '';\r\n}\r\n\r\nfunction ruleApplies(rule: Rule, ext: string) {\r\n return !rule.extensions || rule.extensions.includes(ext);\r\n}\r\n\r\nexport function scanFileForRuleHits(file: FileDiff, options: RuleScanOptions = {}): RuleScanResult {\r\n const stats: RuleScanStats = {\r\n addedLinesScanned: 0,\r\n sievePassed: 0,\r\n hits: 0,\r\n shadowHits: 0,\r\n suppressedAsMoved: 0,\r\n unstrippable: 0,\r\n truncated: false,\r\n byRule: {},\r\n };\r\n const hits: RuleHit[] = [];\r\n\r\n if (file.isDeleted || file.isBinary || !file.path) return { hits, stats };\r\n\r\n const ext = extensionOf(file.path);\r\n const denied = new Set(options.deniedClaimTypes ?? []);\r\n const disabled = new Set(options.disabledRuleIds ?? []);\r\n const shadowIds = new Set(options.shadowRuleIds ?? []);\r\n\r\n const active = RULES.filter((rule) =>\r\n rule.enabled\r\n && !disabled.has(rule.id)\r\n && !denied.has(rule.claimType)\r\n && ruleApplies(rule, ext));\r\n if (active.length === 0) return { hits, stats };\r\n\r\n const triggers = [...new Set(active.flatMap((rule) => rule.triggers))];\r\n const syntax = commentSyntaxFor(file.path);\r\n\r\n for (const hunk of file.hunks) {\r\n const removed = new Set<string>();\r\n for (const l of hunk.lines) {\r\n if (l.kind === 'del') removed.add(normalizeDiffText(l.content));\r\n }\r\n\r\n for (const line of hunk.lines) {\r\n if (line.kind !== 'add') continue;\r\n if (stats.addedLinesScanned >= MAX_RULE_SCAN_ADDED_LINES) {\r\n stats.truncated = true;\r\n break;\r\n }\r\n stats.addedLinesScanned += 1;\r\n\r\n const raw = line.content;\r\n if (!triggers.some((trigger) => raw.includes(trigger))) continue;\r\n stats.sievePassed += 1;\r\n\r\n const stripped = stripCommentsAndStrings(raw, syntax);\r\n if (stripped === null) {\r\n stats.unstrippable += 1;\r\n continue;\r\n }\r\n\r\n for (const rule of active) {\r\n if (!rule.triggers.some((trigger) => raw.includes(trigger))) continue;\r\n if (!rule.pattern.test(stripped)) continue;\r\n if (rule.rejectRaw?.test(raw)) continue;\r\n\r\n if (removed.has(normalizeDiffText(raw))) {\r\n stats.suppressedAsMoved += 1;\r\n continue;\r\n }\r\n\r\n const shadow = shadowIds.has(rule.id);\r\n hits.push({ rule, line, shadow });\r\n stats.byRule[rule.id] = (stats.byRule[rule.id] ?? 0) + 1;\r\n if (shadow) stats.shadowHits += 1;\r\n else stats.hits += 1;\r\n break;\r\n }\r\n }\r\n if (stats.truncated) break;\r\n }\r\n\r\n return { hits, stats };\r\n}\r\n\r\nexport function ruleHitsToComments(file: FileDiff, result: RuleScanResult): ParsedReviewComment[] {\r\n const comments: ParsedReviewComment[] = [];\r\n for (const hit of result.hits) {\r\n if (hit.shadow) continue;\r\n\r\n const { rule, line } = hit;\r\n const anchorHash = buildAnchorHash(line.content);\r\n comments.push({\r\n path: file.path,\r\n line: line.newLineNumber ?? null,\r\n position: line.position ?? null,\r\n severity: rule.severity,\r\n category: CLAIM_TYPE_CATEGORY[rule.claimType] ?? 'quality',\r\n title: rule.title,\r\n body: rule.body,\r\n evidence: line.content,\r\n anchorHash,\r\n claimType: rule.claimType,\r\n fingerprint: buildFindingFingerprint(file.path, `${rule.title} @${anchorHash}`),\r\n fingerprintV2: buildFindingFingerprintV2(file.path, rule.claimType, anchorHash),\r\n source: 'rule' as const,\r\n ruleId: rule.id,\r\n } satisfies ParsedReviewComment);\r\n }\r\n\r\n return comments;\r\n}\r\n"],"mappings":"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","names":[]}
@@ -1 +0,0 @@
1
- {"version":3,"sources":["../src/prompts/file-review.ts"],"sourcesContent":["import { claimTypes, type RepoConfig } from '@codraoss/schema';\r\nimport type { FileDiff } from '../diff';\r\nimport type { ModelResponseSchema } from '../ports/model';\r\nimport { getLanguageForFile } from './languages';\r\nimport {\r\n EXEMPLAR_BLOCK_CHARS,\r\n PR_DESCRIPTION_CHARS,\r\n} from '../constants';\r\n\r\nexport function generatorFindingCap(maxComments: number): number {\r\n return Math.max(1, maxComments * 2);\r\n}\r\n\r\nfunction findingItemSchema() {\r\n return {\r\n type: 'object',\r\n additionalProperties: false,\r\n required: ['evidence', 'code_location', 'claim_type', 'title', 'body', 'priority'],\r\n properties: {\r\n evidence: { type: 'string' },\r\n code_location: {\r\n type: 'object',\r\n additionalProperties: false,\r\n properties: {\r\n absolute_file_path: { type: 'string' },\r\n line: { type: 'integer', minimum: 1 },\r\n line_range: {\r\n type: 'object',\r\n additionalProperties: false,\r\n required: ['start', 'end'],\r\n properties: {\r\n start: { type: 'integer', minimum: 1 },\r\n end: { type: 'integer', minimum: 1 },\r\n },\r\n },\r\n },\r\n anyOf: [\r\n { required: ['line'] },\r\n { required: ['line_range'] },\r\n ],\r\n },\r\n claim_type: { type: 'string', enum: [...claimTypes] },\r\n title: { type: 'string', maxLength: 100 },\r\n body: { type: 'string' },\r\n priority: { type: 'integer', minimum: 0, maximum: 4 },\r\n code_suggestion: { type: 'string' },\r\n },\r\n };\r\n}\r\n\r\nexport function buildReviewResponseSchema(maxComments: number): ModelResponseSchema {\r\n return {\r\n name: 'codra_file_review',\r\n schema: {\r\n type: 'object',\r\n additionalProperties: false,\r\n required: ['findings', 'overall_explanation', 'overall_correctness', 'overall_confidence_score'],\r\n properties: {\r\n findings: {\r\n type: 'array',\r\n maxItems: generatorFindingCap(maxComments),\r\n items: findingItemSchema(),\r\n },\r\n overall_explanation: { type: 'string' },\r\n overall_correctness: { type: 'string', enum: ['patch is correct', 'patch is incorrect'] },\r\n overall_confidence_score: { type: 'number', minimum: 0, maximum: 1 },\r\n },\r\n },\r\n };\r\n}\r\n\r\nexport function buildBatchReviewResponseSchema(maxComments: number, fileCount: number): ModelResponseSchema {\r\n return {\r\n name: 'codra_batch_review',\r\n schema: {\r\n type: 'object',\r\n additionalProperties: false,\r\n required: ['files', 'overall_confidence_score'],\r\n properties: {\r\n files: {\r\n type: 'array',\r\n maxItems: fileCount,\r\n items: {\r\n type: 'object',\r\n additionalProperties: false,\r\n required: ['absolute_file_path', 'findings', 'overall_explanation', 'overall_correctness'],\r\n properties: {\r\n absolute_file_path: { type: 'string' },\r\n findings: { type: 'array', items: findingItemSchema() },\r\n overall_explanation: { type: 'string' },\r\n overall_correctness: { type: 'string', enum: ['patch is correct', 'patch is incorrect'] },\r\n },\r\n },\r\n },\r\n overall_confidence_score: { type: 'number', minimum: 0, maximum: 1 },\r\n },\r\n },\r\n };\r\n}\r\n\r\nconst SINGLE_FILE_SCHEMA_FORMAT = `{\r\n \"findings\": [\r\n {\r\n \"evidence\": \"<the exact line of code from the diff this finding is about>\",\r\n \"code_location\": {\r\n \"line\": number,\r\n \"line_range\": { \"start\": number, \"end\": number }\r\n },\r\n \"claim_type\": \"<one of the claim types listed above>\",\r\n \"title\": \"<Plain title, NO tags/emoji>\",\r\n \"body\": \"<Explanation of the concrete defect and its impact>\",\r\n \"priority\": 0 | 1 | 2 | 3 | 4,\r\n \"code_suggestion\": \"Optional replacement code\"\r\n }\r\n ],\r\n \"overall_explanation\": \"Summary\",\r\n \"overall_correctness\": \"patch is correct\" | \"patch is incorrect\",\r\n \"overall_confidence_score\": number (0 to 1)\r\n}`;\r\n\r\nconst MULTI_FILE_SCHEMA_FORMAT = `{\r\n \"files\": [\r\n {\r\n \"absolute_file_path\": \"<exact path of one of the files listed below, copied character-for-character>\",\r\n \"findings\": [\r\n {\r\n \"evidence\": \"<the exact line of code from THIS file's diff this finding is about>\",\r\n \"code_location\": {\r\n \"absolute_file_path\": \"<the same path as above>\",\r\n \"line\": number,\r\n \"line_range\": { \"start\": number, \"end\": number }\r\n },\r\n \"claim_type\": \"<one of the claim types listed above>\",\r\n \"title\": \"<Plain title, NO tags/emoji>\",\r\n \"body\": \"<Explanation of the concrete defect and its impact>\",\r\n \"priority\": 0 | 1 | 2 | 3 | 4,\r\n \"code_suggestion\": \"Optional replacement code\"\r\n }\r\n ],\r\n \"overall_explanation\": \"Summary for THIS file\",\r\n \"overall_correctness\": \"patch is correct\" | \"patch is incorrect\"\r\n }\r\n ],\r\n \"overall_confidence_score\": number (0 to 1)\r\n}`;\r\n\r\nexport function buildFileReviewSystemPromptBase(opts?: { multiFile?: boolean }): string {\r\n const multi = opts?.multiFile === true;\r\n\r\n const contextScope = multi\r\n ? `- You can see ONLY the diffs below, not the whole files or the rest of the repository.\r\n- Each file below is INDEPENDENT. A finding about one file must be grounded in a line from THAT file's diff, and must be reported inside that file's entry. Never carry a claim from one file to another, and never assume two files interact unless both diffs show it.`\r\n : '- You can see ONLY the diff below, not the whole file or the rest of the repository.';\r\n\r\n const evidenceSource = multi\r\n ? `the single line of code the finding is about, copied VERBATIM from that file's diff below.`\r\n : 'the single line of code the finding is about, copied VERBATIM from the diff below.';\r\n\r\n const capRule = multi\r\n ? '4. Return at most {{MAX_COMMENTS}} findings PER FILE, most severe first. Keep each body under 160 words.'\r\n : '4. Return at most {{MAX_COMMENTS}} findings, most severe first. Keep each body under 160 words.';\r\n\r\n // presupposed clean files in every bin and reintroduced exactly the restraint language the note above\r\n const emptyRule = multi\r\n ? `5. Return exactly one entry per file listed below, in the same order, and never omit a file. Review each file's diff with the same care you would give it if it were the only file in front of you. An empty findings array is a positive claim that this diff introduces no defect, so return one only when that is true. Do not pad, and do not withhold.`\r\n : '5. If the diff genuinely introduces no defect, return an empty findings array and a short explanation. Do not pad, and do not withhold.';\r\n\r\n return `You are a world-class software engineer performing a precise, high-signal code review.\r\nYour goal is to find REAL defects (bugs, security vulnerabilities, and performance problems) introduced by the diff. Every finding must be grounded in a line you can quote from the diff.\r\n\r\n### CONTEXT EXTENDS (read carefully, this prevents false positives):\r\n${contextScope}\r\n- You cannot see which files import this one. Never predict that a change breaks callers, importers, \"other modules\" or \"external files\" -- a removed \\`export\\`, a renamed symbol or a changed signature may have no consumers at all, and you have no way to check. The same applies in reverse to a function whose body is not shown: do not assume what it does with its errors or its return value.\r\n- Assume every third-party package is at the version this project pins, and that its API is whatever that version provides. Never claim a library \"does not expose\", \"does not provide\" or \"does not support\" something; your training data predates the installed version.\r\n- Assume the language, runtime and build target are whatever the project already uses successfully. A syntax or standard-library method appearing in the diff is available in this project by construction -- the code around it already compiles and ships. Do not raise compatibility, polyfill, transpilation, engine-version or server-side-rendering concerns unless the diff itself shows the incompatibility.\r\n- Two async facts that are frequently misread. \\`return somePromise()\\` inside an \\`async\\` function IS awaited by whoever awaits that function; it is equivalent to \\`return await\\` except inside \\`try\\`/\\`finally\\`, so it is not a missing await and not a floating promise. And \\`void someAsyncCall()\\` is deliberate fire-and-forget: if the called function handles its own errors, there is no unhandled rejection to report.\r\n\r\n### WHAT TO REPORT:\r\n- Report anything a senior engineer reviewing this diff would want to investigate: a bug, a security hole, a performance problem, a resource leak, an unhandled failure, a broken invariant.\r\n- You do not need to be certain. A finding you can ground in a quoted line is worth raising; every finding is independently checked against the diff afterwards, and a wrong one is discarded at no cost to you. A defect you decline to mention is simply lost.\r\n\r\n### EVIDENCE (mandatory, a finding without it cannot be posted):\r\n- Every finding MUST include \"evidence\": ${evidenceSource}\r\n- Copy the code exactly as it appears. Do NOT include the two line-number columns or the +/- marker, do NOT paraphrase, reformat, shorten, or invent code.\r\n- If you cannot quote a specific line from the diff that exhibits the problem, you do not have a finding. Omit it.\r\n\r\n### CLAIM TYPE (required, pick the one that fits, or \"other\"):\r\n${claimTypes.join(', ')}\r\n- This is a label for the KIND of defect. It does not license the claim: only report a type if the\r\n diff actually shows it. Picking a type the code cannot exhibit makes the finding easy to discard.\r\n- If nothing fits, use \"other\". Do not stretch a label to fit.\r\n- NEVER claim that a package, action, tag or version \"does not exist\", or that a config key is invalid. You cannot know what was released after your training data, and a step pinned to a commit SHA resolves by that SHA regardless of the version written beside it. Such claims are discarded.\r\n- Label honestly. The type you choose does not affect whether a finding is accepted; an inaccurate label only makes a real defect harder to act on.\r\n\r\n### OUTPUT RULES:\r\n1. Output MUST be valid JSON, EXACTLY ONE object matching the schema below.\r\n2. DO NOT output any conversational text, source code, or diff hunks before or after the JSON.\r\n3. Prioritize by severity: 0 = P0 critical, 1 = P1 high, 2 = P2 medium, 3 = P3 low, 4 = nit (cosmetic/trivial). Set priority honestly; do not inflate. Use 4 for anything a reviewer would prefix with \"nit:\".\r\n A finding that rests on a condition you cannot check from the diff -- \"if this runs on an older engine\", \"if another module imports this\", \"depending on the caller\" -- is at most priority 3, never 0 or 1, however serious the consequence would be if the condition held. Certainty about the consequence is not certainty about the premise.\r\n${capRule}\r\n${emptyRule}\r\n\r\n### SCHEMA FORMAT:\r\n${multi ? MULTI_FILE_SCHEMA_FORMAT : SINGLE_FILE_SCHEMA_FORMAT}\r\n\r\nIdentify security risks such as XSS, SQLi, CSRF, insecure randomness, and data leaks that the diff actually introduces.`;\r\n}\r\n\r\nexport const fileReviewSystemPromptBase = buildFileReviewSystemPromptBase();\r\n\r\nexport function buildFileReviewSystemPrompt(\r\n config: RepoConfig['review'],\r\n languagePersona?: string,\r\n opts?: { multiFile?: boolean },\r\n) {\r\n const persona = languagePersona ? ` as ${languagePersona}` : '';\r\n const prompt = buildFileReviewSystemPromptBase(opts)\r\n .replace('{{MAX_COMMENTS}}', generatorFindingCap(config.max_comments).toString());\r\n return `You are a world-class professional senior code reviewer${persona}. ${prompt}`;\r\n}\r\n\r\nexport type RejectedExemplar = { title: string; claimType?: string | null };\r\n\r\n\r\n\r\nfunction renderExemplars(exemplars: readonly RejectedExemplar[] | undefined): string | null {\r\n if (!exemplars?.length) return null;\r\n\r\n const lines: string[] = [];\r\n let used = 0;\r\n for (const exemplar of exemplars) {\r\n const line = `- ${exemplar.title}${exemplar.claimType ? ` (${exemplar.claimType})` : ''}`;\r\n if (used + line.length > EXEMPLAR_BLOCK_CHARS) break;\r\n lines.push(line);\r\n used += line.length;\r\n }\r\n if (lines.length === 0) return null;\r\n\r\n const heading = 'Findings a reviewer on THIS repository has already rejected. Do not report things like these:';\r\n return [heading, ...lines].join('\\n');\r\n}\r\n\r\n\r\n\r\nfunction renderPrContext(prDescription: string | null): string | null {\r\n const trimmed = prDescription?.trim();\r\n if (!trimmed) return null;\r\n return `PR description (author intent - use to judge whether a change is deliberate):\\n${trimmed.slice(0, PR_DESCRIPTION_CHARS)}${trimmed.length > PR_DESCRIPTION_CHARS ? '…' : ''}`;\r\n}\r\n\r\nfunction renderCustomRules(config: RepoConfig['review']): string {\r\n const rules = config.custom_rules.length > 0 ? config.custom_rules.map((rule) => `- ${rule}`).join('\\n') : '- None';\r\n return `Custom rules:\\n${rules}`;\r\n}\r\n\r\nfunction renderLanguageGuidelines(path: string): string {\r\n const languageInfo = getLanguageForFile(path);\r\n const guidelineHeader = 'Specific Guidelines (check the diff against each of these)';\r\n return languageInfo\r\n ? `Language: ${languageInfo.language}\\n${guidelineHeader}:\\n${languageInfo.guidelines.map(g => `- ${g}`).join('\\n')}`\r\n : 'Language: Generic\\nSpecific Guidelines: Follow general best practices.';\r\n}\r\n\r\nexport function buildFileReviewPrompts(input: {\r\n file: FileDiff;\r\n prTitle: string | null;\r\n prDescription: string | null;\r\n config: RepoConfig['review'];\r\n rejectedExemplars?: readonly RejectedExemplar[];\r\n}) {\r\n const languageInfo = getLanguageForFile(input.file.path);\r\n const rules = input.config.custom_rules.length > 0 ? input.config.custom_rules.map((rule) => `- ${rule}`).join('\\n') : '- None';\r\n const systemPrompt = buildFileReviewSystemPrompt(input.config, languageInfo?.persona);\r\n const languageGuidelines = renderLanguageGuidelines(input.file.path);\r\n\r\n const prContext = renderPrContext(input.prDescription);\r\n\r\n const exemplars = renderExemplars(input.rejectedExemplars);\r\n\r\n const userPrompt = [\r\n `PR title: ${input.prTitle ?? 'Untitled PR'}`,\r\n ...(prContext ? [prContext] : []),\r\n ...(exemplars ? [exemplars] : []),\r\n `File path: ${input.file.path}`,\r\n languageGuidelines,\r\n `Custom rules:\\n${rules}`,\r\n 'Review ONLY the diff shown below. You cannot see the rest of the file or repository - do not report something as undefined, unimported, unused, or missing just because it is not in the diff. If the diff note says it was truncated, do not infer issues from omitted lines.',\r\n 'Line numbers: every diff line below is prefixed with two columns - the OLD file line number, then the NEW file line number. Always report `line` (and `line_range`) using the NEW (second, right-hand) number, and only ever cite a line that appears in the diff. For a removed line, cite the nearest NEW line number shown next to it.',\r\n 'Evidence: every finding must carry an `evidence` string containing the exact code of the line it is about, copied character-for-character from the diff below. Strip the two leading line-number columns and the +/-/space marker - quote only the code itself. A finding whose evidence does not appear in the diff will be discarded.',\r\n 'Prioritize correctness, security, and production-impacting bugs. Raise anything you can ground in a quoted line; avoid subjective style feedback.',\r\n '',\r\n `## Output JSON Schema (STRICTLY REQUIRED)`,\r\n `{\r\n \"findings\": [\r\n {\r\n \"evidence\": \"<exact line of code copied verbatim from the diff below, without the line-number columns or +/- marker>\",\r\n \"code_location\": {\r\n \"absolute_file_path\": \"${input.file.path}\",\r\n \"line\": <int>,\r\n \"line_range\": {\"start\": <int>, \"end\": <int>}\r\n },\r\n \"claim_type\": \"<${claimTypes.join(' | ')}>\",\r\n \"title\": \"<Plain title>\",\r\n \"body\": \"<Technical explanation>\",\r\n \"priority\": <0|1|2|3|4>,\r\n \"code_suggestion\": \"string\"\r\n }\r\n ],\r\n \"overall_correctness\": \"patch is correct\" | \"patch is incorrect\",\r\n \"overall_explanation\": \"Summary\",\r\n \"overall_confidence_score\": <float 0.0-1.0>\r\n}`,\r\n '',\r\n 'Unified diff:',\r\n renderFileDiff(input.file),\r\n ].join('\\n');\r\n\r\n return { systemPrompt, userPrompt };\r\n}\r\n\r\nfunction packFileHeader(file: FileDiff, index: number, total: number): string {\r\n return `===== FILE ${index + 1} of ${total}: ${file.path} =====`;\r\n}\r\n\r\nexport function buildBatchReviewPrompts(input: {\r\n files: readonly FileDiff[];\r\n prTitle: string | null;\r\n prDescription: string | null;\r\n config: RepoConfig['review'];\r\n rejectedExemplars?: readonly RejectedExemplar[];\r\n}) {\r\n const files = input.files;\r\n\r\n const languages = new Set(files.map((file) => getLanguageForFile(file.path)));\r\n const uniformLanguage = languages.size === 1 ? [...languages][0] : undefined;\r\n\r\n const systemPrompt = buildFileReviewSystemPrompt(input.config, uniformLanguage?.persona, { multiFile: true });\r\n\r\n const prContext = renderPrContext(input.prDescription);\r\n const exemplars = renderExemplars(input.rejectedExemplars);\r\n const pathList = files.map((file) => `- ${file.path}`).join('\\n');\r\n\r\n const fileBlocks = files.flatMap((file, index) => [\r\n '',\r\n packFileHeader(file, index, files.length),\r\n ...(uniformLanguage ? [] : [renderLanguageGuidelines(file.path)]),\r\n 'Unified diff:',\r\n renderFileDiff(file),\r\n ]);\r\n\r\n const userPrompt = [\r\n `PR title: ${input.prTitle ?? 'Untitled PR'}`,\r\n ...(prContext ? [prContext] : []),\r\n ...(exemplars ? [exemplars] : []),\r\n `You are reviewing ${files.length} files in ONE response. Return exactly ${files.length} entries in \"files\", one per path, in this order:\\n${pathList}`,\r\n ...(uniformLanguage ? [renderLanguageGuidelines(files[0].path)] : []),\r\n renderCustomRules(input.config),\r\n 'Review ONLY the diffs shown below. You cannot see the rest of any file or the repository - do not report something as undefined, unimported, unused, or missing just because it is not in a diff. If a diff note says it was truncated, do not infer issues from omitted lines.',\r\n 'File scoping: each finding belongs to exactly ONE file. Put it inside that file\\'s entry, set that file\\'s path in `absolute_file_path`, and quote evidence from that file\\'s diff only. Never report a finding about one file inside another file\\'s entry, and never quote a line from a different file.',\r\n 'Line numbers: every diff line below is prefixed with two columns - the OLD file line number, then the NEW file line number. Always report `line` (and `line_range`) using the NEW (second, right-hand) number, and only ever cite a line that appears in that file\\'s diff. For a removed line, cite the nearest NEW line number shown next to it.',\r\n 'Evidence: every finding must carry an `evidence` string containing the exact code of the line it is about, copied character-for-character from its own file\\'s diff below. Strip the two leading line-number columns and the +/-/space marker - quote only the code itself. A finding whose evidence does not appear in that file\\'s diff will be discarded.',\r\n 'Prioritize correctness, security, and production-impacting bugs. Raise anything you can ground in a quoted line; avoid subjective style feedback.',\r\n '',\r\n `## Output JSON Schema (STRICTLY REQUIRED)`,\r\n MULTI_FILE_SCHEMA_FORMAT,\r\n ...fileBlocks,\r\n ].join('\\n');\r\n\r\n return { systemPrompt, userPrompt };\r\n}\r\n\r\nexport function renderFileDiff(file: FileDiff) {\r\n const lines = [`diff --git a/${file.previousPath ?? file.path} b/${file.path}`];\r\n for (const hunk of file.hunks) {\r\n lines.push(hunk.header);\r\n for (const line of hunk.lines) {\r\n const prefix = line.kind === 'add' ? '+' : line.kind === 'del' ? '-' : ' ';\r\n const left = line.oldLineNumber ?? '';\r\n const right = line.newLineNumber ?? '';\r\n lines.push(`${String(left).padStart(4, ' ')} ${String(right).padStart(4, ' ')} ${prefix}${line.content}`);\r\n }\r\n }\r\n\r\n if (file.isTruncated) {\r\n lines.push('');\r\n lines.push(`[NOTE: This diff has been truncated from ${file.originalLineCount} lines to ${file.lineCount} lines for brevity.]`);\r\n }\r\n\r\n return lines.join('\\n');\r\n}\r\n"],"mappings":"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","names":[]}
@@ -1 +0,0 @@
1
- {"version":3,"sources":["../src/fingerprint.ts"],"sourcesContent":["\r\nexport function fnv1a32Hex(input: string): string {\r\n let hash = 0x811c9dc5;\r\n for (let i = 0; i < input.length; i++) {\r\n hash ^= input.charCodeAt(i);\r\n hash = (hash + ((hash << 1) + (hash << 4) + (hash << 7) + (hash << 8) + (hash << 24))) >>> 0;\r\n }\r\n return hash.toString(16).padStart(8, '0');\r\n}\r\n\r\nexport function normalizeDiffText(input: string): string {\r\n return input\r\n .replace(/^\\s*\\d*\\s+\\d*\\s*[+\\- ]?/, '')\r\n .replace(/\\s+/g, ' ')\r\n .trim();\r\n}\r\n\r\nexport function foldEvidenceText(input: string): string {\r\n return normalizeDiffText(input)\r\n .replace(/[‘’‚‛′]/g, \"'\")\r\n .replace(/[“”„‟″]/g, '\"')\r\n .replace(/[‐-―−]/g, '-')\r\n .replace(/…/g, '...');\r\n}\r\n\r\nexport function normalizeFindingTitle(title: string): string {\r\n return title.toLowerCase().replace(/[^a-z0-9]+/g, ' ').trim();\r\n}\r\n\r\nexport function buildFindingFingerprint(path: string, title: string): string {\r\n return fnv1a32Hex(`${path}\\u0000${normalizeFindingTitle(title)}`);\r\n}\r\n\r\nexport function buildFindingFingerprintV2(\r\n path: string,\r\n claimType: string | null | undefined,\r\n anchorHash: string | null | undefined,\r\n): string | null {\r\n if (!anchorHash) return null;\r\n return fnv1a32Hex(`v2 ${path} ${claimType ?? 'other'} ${anchorHash}`);\r\n}\r\n\r\nexport function buildAnchorHash(lineContent: string): string {\r\n return fnv1a32Hex(normalizeDiffText(lineContent));\r\n}\r\n"],"mappings":";AACO,SAAS,WAAW,OAAuB;AAChD,MAAI,OAAO;AACX,WAAS,IAAI,GAAG,IAAI,MAAM,QAAQ,KAAK;AACrC,YAAQ,MAAM,WAAW,CAAC;AAC1B,WAAQ,SAAS,QAAQ,MAAM,QAAQ,MAAM,QAAQ,MAAM,QAAQ,MAAM,QAAQ,SAAU;AAAA,EAC7F;AACA,SAAO,KAAK,SAAS,EAAE,EAAE,SAAS,GAAG,GAAG;AAC1C;AAEO,SAAS,kBAAkB,OAAuB;AACvD,SAAO,MACJ,QAAQ,2BAA2B,EAAE,EACrC,QAAQ,QAAQ,GAAG,EACnB,KAAK;AACV;AAEO,SAAS,iBAAiB,OAAuB;AACtD,SAAO,kBAAkB,KAAK,EAC3B,QAAQ,YAAY,GAAG,EACvB,QAAQ,YAAY,GAAG,EACvB,QAAQ,WAAW,GAAG,EACtB,QAAQ,MAAM,KAAK;AACxB;AAEO,SAAS,sBAAsB,OAAuB;AAC3D,SAAO,MAAM,YAAY,EAAE,QAAQ,eAAe,GAAG,EAAE,KAAK;AAC9D;AAEO,SAAS,wBAAwB,MAAc,OAAuB;AAC3E,SAAO,WAAW,GAAG,IAAI,KAAS,sBAAsB,KAAK,CAAC,EAAE;AAClE;AAEO,SAAS,0BACd,MACA,WACA,YACe;AACf,MAAI,CAAC,WAAY,QAAO;AACxB,SAAO,WAAW,MAAM,IAAI,IAAI,aAAa,OAAO,IAAI,UAAU,EAAE;AACtE;AAEO,SAAS,gBAAgB,aAA6B;AAC3D,SAAO,WAAW,kBAAkB,WAAW,CAAC;AAClD;","names":[]}
@@ -1 +0,0 @@
1
- {"version":3,"sources":["../src/diff/index.ts","../src/diff/position.ts"],"sourcesContent":["import picomatch from 'picomatch';\r\nimport type { RepoConfig } from '@codraoss/schema';\r\nimport {\r\n type DiffLineKind,\r\n type DiffLine,\r\n type DiffHunk,\r\n type FileDiff,\r\n getValidNewLines,\r\n getValidPositions,\r\n findPositionForLine,\r\n truncateFileDiff,\r\n chunkFileDiff,\r\n} from './position';\r\n\r\nexport {\r\n type DiffLineKind,\r\n type DiffLine,\r\n type DiffHunk,\r\n type FileDiff,\r\n getValidNewLines,\r\n getValidPositions,\r\n findPositionForLine,\r\n truncateFileDiff,\r\n chunkFileDiff,\r\n};\r\n\r\nconst defaultSkipMatchers = ['**/*.lock', '**/package-lock.json', '**/pnpm-lock.yaml', '**/yarn.lock', '**/*.min.js'].map((pattern) =>\r\n picomatch(pattern, { dot: true }),\r\n);\r\n\r\nexport function isReviewableFile(path: string, customMatchers: ReturnType<typeof picomatch>[]) {\r\n if (defaultSkipMatchers.some((matcher) => matcher(path))) return false;\r\n if (customMatchers.some((matcher) => matcher(path))) return false;\r\n return true;\r\n}\r\n\r\nexport function parseDiffHeaderPath(line: string) {\r\n const rest = line.slice('diff --git '.length);\r\n\r\n if (rest.startsWith('a/')) {\r\n const n = (rest.length - 5) / 2;\r\n if (Number.isInteger(n) && n > 0 && rest[2 + n] === ' ' && rest.startsWith('b/', 3 + n)) {\r\n const a = rest.slice(2, 2 + n);\r\n if (a === rest.slice(5 + n)) return a;\r\n }\r\n }\r\n\r\n const bStart = rest.indexOf(' b/', rest.startsWith('a/') ? 2 : 0);\r\n const bPath = bStart === -1 ? rest.slice(rest.lastIndexOf(' ') + 1) : rest.slice(bStart + 3);\r\n return bPath.startsWith('b/') ? bPath.slice(2) : bPath;\r\n}\r\n\r\nfunction parseHunkHeader(line: string): { oldLine: number; newLine: number } | null {\r\n const match = /^@@ -(\\d+)(?:,\\d+)? \\+(\\d+)(?:,\\d+)? @@/.exec(line);\r\n if (!match) {\r\n return null;\r\n }\r\n\r\n return {\r\n oldLine: Number.parseInt(match[1], 10),\r\n newLine: Number.parseInt(match[2], 10),\r\n };\r\n}\r\n\r\nfunction classifyDiffLine(prefix: ' ' | '+' | '-', content: string, oldLine: number, newLine: number, position: number): DiffLine {\r\n if (prefix === ' ') {\r\n return { kind: 'context', content, oldLineNumber: oldLine, newLineNumber: newLine, position };\r\n }\r\n\r\n if (prefix === '+') {\r\n return { kind: 'add', content, newLineNumber: newLine, position };\r\n }\r\n\r\n return { kind: 'del', content, oldLineNumber: oldLine, position };\r\n}\r\n\r\nfunction finishFile(files: FileDiff[], currentFile: FileDiff | null) {\r\n if (currentFile) {\r\n files.push(currentFile);\r\n }\r\n}\r\n\r\nexport function parseUnifiedDiff(rawDiff: string, reviewConfig?: RepoConfig['review']): FileDiff[] {\r\n const files: FileDiff[] = [];\r\n const customMatchers = reviewConfig?.skip_files?.map((pattern) => picomatch(pattern, { dot: true })) ?? [];\r\n\r\n let currentFile: FileDiff | null = null;\r\n let currentHunk: DiffHunk | null = null;\r\n let oldLine = 0;\r\n let newLine = 0;\r\n let position = 0;\r\n let isIgnored = false;\r\n\r\n const pushCurrentFile = () => {\r\n finishFile(files, currentFile);\r\n currentFile = null;\r\n currentHunk = null;\r\n oldLine = 0;\r\n newLine = 0;\r\n position = 0;\r\n isIgnored = false;\r\n };\r\n\r\n let startIndex = 0;\r\n const length = rawDiff.length;\r\n\r\n while (startIndex < length) {\r\n let endIndex = rawDiff.indexOf('\\n', startIndex);\r\n if (endIndex === -1) {\r\n endIndex = length;\r\n }\r\n\r\n let line = rawDiff.substring(startIndex, endIndex);\r\n if (line.charCodeAt(line.length - 1) === 13) {\r\n line = line.slice(0, -1);\r\n }\r\n\r\n startIndex = endIndex + 1;\r\n\r\n if (line.startsWith('diff --git ')) {\r\n pushCurrentFile();\r\n const path = parseDiffHeaderPath(line);\r\n\r\n currentFile = {\r\n path,\r\n previousPath: null,\r\n isNew: false,\r\n isDeleted: false,\r\n isBinary: false,\r\n lineCount: 0,\r\n hunks: [],\r\n };\r\n\r\n if (reviewConfig) {\r\n isIgnored = !isReviewableFile(path, customMatchers);\r\n }\r\n continue;\r\n }\r\n\r\n if (!currentFile) {\r\n continue;\r\n }\r\n\r\n if (line.startsWith('rename from ')) {\r\n currentFile.previousPath = line.slice(12);\r\n continue;\r\n }\r\n\r\n if (line.startsWith('rename to ')) {\r\n const nextPath = line.slice(10);\r\n currentFile.path = nextPath.startsWith('b/') ? nextPath.slice(2) : nextPath;\r\n if (reviewConfig) {\r\n isIgnored = !isReviewableFile(currentFile.path, customMatchers);\r\n }\r\n continue;\r\n }\r\n\r\n if (line.startsWith('new file mode ')) {\r\n currentFile.isNew = true;\r\n continue;\r\n }\r\n\r\n if (line.startsWith('deleted file mode ')) {\r\n currentFile.isDeleted = true;\r\n isIgnored = true;\r\n continue;\r\n }\r\n\r\n if (line.startsWith('Binary files ') || line.startsWith('GIT binary patch')) {\r\n currentFile.isBinary = true;\r\n isIgnored = true;\r\n continue;\r\n }\r\n\r\n if (line.startsWith('+++ ')) {\r\n const nextPath = line.slice(4);\r\n currentFile.path = nextPath.startsWith('b/') ? nextPath.slice(2) : nextPath;\r\n if (reviewConfig) {\r\n isIgnored = !isReviewableFile(currentFile.path, customMatchers);\r\n }\r\n continue;\r\n }\r\n\r\n if (isIgnored) {\r\n continue;\r\n }\r\n\r\n if (line.startsWith('--- ')) {\r\n continue;\r\n }\r\n\r\n if (line.startsWith('@@ ')) {\r\n const header = parseHunkHeader(line);\r\n if (!header) {\r\n continue;\r\n }\r\n\r\n oldLine = header.oldLine;\r\n newLine = header.newLine;\r\n currentHunk = { header: line, lines: [] };\r\n currentFile.hunks.push(currentHunk);\r\n continue;\r\n }\r\n\r\n if (!currentHunk) {\r\n continue;\r\n }\r\n\r\n const prefix = line[0];\r\n if (prefix !== ' ' && prefix !== '+' && prefix !== '-') {\r\n continue;\r\n }\r\n\r\n position += 1;\r\n const diffLine = classifyDiffLine(prefix, line.slice(1), oldLine, newLine, position);\r\n currentHunk.lines.push(diffLine);\r\n currentFile.lineCount += 1;\r\n\r\n if (diffLine.kind !== 'del') newLine += 1;\r\n if (diffLine.kind !== 'add') oldLine += 1;\r\n }\r\n\r\n pushCurrentFile();\r\n\r\n return files.filter((file) => file.path);\r\n}\r\n\r\n/** Shape returned by forge compare/files endpoints. Maps to GitHub's pulls/files JSON. */\r\nexport type DiffFileEntry = {\r\n filename: string;\r\n previous_filename?: string | null;\r\n status?: string;\r\n patch?: string | null;\r\n};\r\n\r\nexport function buildUnifiedDiffFromFiles(files: DiffFileEntry[]): string {\r\n const out: string[] = [];\r\n\r\n for (const file of files) {\r\n const newPath = file.filename;\r\n const oldPath = file.previous_filename || file.filename;\r\n const isAdded = file.status === 'added';\r\n const isRemoved = file.status === 'removed';\r\n\r\n out.push(`diff --git a/${oldPath} b/${newPath}`);\r\n if (isAdded) out.push('new file mode 100644');\r\n if (isRemoved) out.push('deleted file mode 100644');\r\n if (file.previous_filename && file.previous_filename !== newPath) {\r\n out.push(`rename from ${file.previous_filename}`);\r\n out.push(`rename to ${newPath}`);\r\n }\r\n\r\n if (!file.patch) {\r\n out.push(`Binary files a/${oldPath} and b/${newPath} differ`);\r\n continue;\r\n }\r\n\r\n out.push(isAdded ? '--- /dev/null' : `--- a/${oldPath}`);\r\n out.push(isRemoved ? '+++ /dev/null' : `+++ b/${newPath}`);\r\n out.push(file.patch);\r\n }\r\n\r\n return out.length > 0 ? `${out.join('\\n')}\\n` : '';\r\n}\r\n\r\nexport function filterReviewableFiles(\r\n files: FileDiff[],\r\n config: RepoConfig['review'],\r\n maxFiles: number,\r\n): { files: FileDiff[]; skipped: number } {\r\n const customMatchers = config.skip_files.map((pattern) => picomatch(pattern, { dot: true }));\r\n\r\n const reviewable: FileDiff[] = [];\r\n for (const file of files) {\r\n if (file.isDeleted || file.isBinary) continue;\r\n if (defaultSkipMatchers.some((matcher) => matcher(file.path))) continue;\r\n if (customMatchers.some((matcher) => matcher(file.path))) continue;\r\n reviewable.push(file);\r\n }\r\n reviewable.sort((left, right) => Number(left.isNew) - Number(right.isNew) || left.path.localeCompare(right.path));\r\n\r\n return {\r\n files: reviewable.slice(0, maxFiles),\r\n skipped: Math.max(0, reviewable.length - maxFiles),\r\n };\r\n}\r\n","export type DiffLineKind = 'context' | 'add' | 'del';\r\n\r\nexport type DiffLine = {\r\n kind: DiffLineKind;\r\n content: string;\r\n oldLineNumber?: number;\r\n newLineNumber?: number;\r\n position: number;\r\n};\r\n\r\nexport type DiffHunk = {\r\n header: string;\r\n lines: DiffLine[];\r\n};\r\n\r\nexport type FileDiff = {\r\n path: string;\r\n previousPath: string | null;\r\n isNew: boolean;\r\n isDeleted: boolean;\r\n isBinary: boolean;\r\n lineCount: number;\r\n hunks: DiffHunk[];\r\n isTruncated?: boolean;\r\n originalLineCount?: number;\r\n};\r\n\r\nexport function getValidNewLines(file: FileDiff) {\r\n const newLines = new Set<number>();\r\n for (const hunk of file.hunks) {\r\n for (const line of hunk.lines) {\r\n if (line.kind !== 'del' && line.newLineNumber !== undefined) {\r\n newLines.add(line.newLineNumber);\r\n }\r\n }\r\n }\r\n\r\n return newLines;\r\n}\r\n\r\nexport function getValidPositions(file: FileDiff) {\r\n const positions = new Set<number>();\r\n for (const hunk of file.hunks) {\r\n for (const line of hunk.lines) {\r\n if (line.kind !== 'del') {\r\n positions.add(line.position);\r\n }\r\n }\r\n }\r\n\r\n return positions;\r\n}\r\n\r\nexport function findPositionForLine(file: FileDiff, lineNumber: number) {\r\n for (const hunk of file.hunks) {\r\n for (const line of hunk.lines) {\r\n if (line.newLineNumber === lineNumber && line.kind !== 'del') {\r\n return line.position;\r\n }\r\n }\r\n }\r\n\r\n return undefined;\r\n}\r\n\r\n\r\nexport function truncateFileDiff(file: FileDiff, maxLines: number): FileDiff {\r\n if (file.lineCount <= maxLines) {\r\n return file;\r\n }\r\n\r\n let currentLines = 0;\r\n const keptHunks: DiffHunk[] = [];\r\n\r\n for (const hunk of file.hunks) {\r\n const remainingLines = maxLines - currentLines;\r\n if (remainingLines <= 0) {\r\n break;\r\n }\r\n\r\n if (hunk.lines.length <= remainingLines) {\r\n keptHunks.push(hunk);\r\n currentLines += hunk.lines.length;\r\n continue;\r\n }\r\n\r\n keptHunks.push({\r\n ...hunk,\r\n lines: hunk.lines.slice(0, remainingLines),\r\n });\r\n currentLines += remainingLines;\r\n break;\r\n }\r\n\r\n return {\r\n ...file,\r\n hunks: keptHunks,\r\n lineCount: currentLines,\r\n isTruncated: true,\r\n originalLineCount: file.lineCount,\r\n };\r\n}\r\n\r\nexport function chunkFileDiff(file: FileDiff, maxLinesPerChunk: number): FileDiff[] {\r\n if (file.lineCount <= maxLinesPerChunk) {\r\n return [file];\r\n }\r\n\r\n const chunks: FileDiff[] = [];\r\n let currentHunks: DiffHunk[] = [];\r\n let currentLines = 0;\r\n\r\n for (const hunk of file.hunks) {\r\n let linesRemainingInHunk = hunk.lines;\r\n\r\n while (linesRemainingInHunk.length > 0) {\r\n const roomInChunk = maxLinesPerChunk - currentLines;\r\n\r\n if (roomInChunk <= 0) {\r\n chunks.push({\r\n ...file,\r\n hunks: currentHunks,\r\n lineCount: currentLines,\r\n isTruncated: true,\r\n originalLineCount: file.lineCount,\r\n });\r\n currentHunks = [];\r\n currentLines = 0;\r\n continue;\r\n }\r\n\r\n if (linesRemainingInHunk.length <= roomInChunk) {\r\n currentHunks.push({\r\n ...hunk,\r\n lines: linesRemainingInHunk,\r\n });\r\n currentLines += linesRemainingInHunk.length;\r\n linesRemainingInHunk = [];\r\n } else {\r\n currentHunks.push({\r\n ...hunk,\r\n lines: linesRemainingInHunk.slice(0, roomInChunk),\r\n });\r\n currentLines += roomInChunk;\r\n linesRemainingInHunk = linesRemainingInHunk.slice(roomInChunk);\r\n }\r\n }\r\n }\r\n\r\n if (currentHunks.length > 0) {\r\n chunks.push({\r\n ...file,\r\n hunks: currentHunks,\r\n lineCount: currentLines,\r\n isTruncated: true,\r\n originalLineCount: file.lineCount,\r\n });\r\n }\r\n\r\n return chunks;\r\n}\r\n"],"mappings":"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","names":[]}
@@ -1 +0,0 @@
1
- {"version":3,"sources":["../src/token-tracker.ts"],"sourcesContent":["import { logger } from './logger';\r\n\r\nexport interface TokenUsage {\r\n input: number;\r\n output: number;\r\n}\r\n\r\nexport interface ModelUsage extends TokenUsage {\r\n model: string;\r\n calls: number;\r\n}\r\n\r\nexport type WastedAttemptReason = 'rate-limited' | 'error';\r\n\r\nexport interface WastedUsage {\r\n attempts: number;\r\n estimatedInput: number;\r\n skips: number;\r\n byReason: Record<string, number>;\r\n}\r\n\r\nexport class TokenTracker {\r\n private usage: Map<string, ModelUsage> = new Map();\r\n private wasted = { attempts: 0, estimatedInput: 0, skips: 0 };\r\n private wastedByReason: Map<string, number> = new Map();\r\n private subrequests = 0;\r\n private readonly MAX_SUBREQUESTS = 50;\r\n private readonly SAFE_MARGIN = 25;\r\n\r\n incrementSubrequests(count = 1) {\r\n this.subrequests += count;\r\n }\r\n\r\n getSubrequestCount() {\r\n return this.subrequests;\r\n }\r\n\r\n hasRemainingSubrequests(needed = 1) {\r\n return this.subrequests + needed <= this.MAX_SUBREQUESTS;\r\n }\r\n\r\n isNearLimit() {\r\n return this.subrequests >= this.MAX_SUBREQUESTS - this.SAFE_MARGIN;\r\n }\r\n\r\n remainingSafeBudget() {\r\n return Math.max(0, this.MAX_SUBREQUESTS - this.SAFE_MARGIN - this.subrequests);\r\n }\r\n\r\n record(model: string, input: number, output: number) {\r\n const existing = this.usage.get(model) || { model, input: 0, output: 0, calls: 0 };\r\n \r\n this.usage.set(model, {\r\n model,\r\n input: existing.input + input,\r\n output: existing.output + output,\r\n calls: existing.calls + 1,\r\n });\r\n\r\n logger.debug(`Token usage recorded for ${model}`, { \r\n input, \r\n output, \r\n totalInput: existing.input + input,\r\n totalOutput: existing.output + output\r\n });\r\n }\r\n\r\n recordFailedAttempt(model: string, estimatedInputTokens: number, reason: WastedAttemptReason) {\r\n this.wasted.attempts += 1;\r\n this.wasted.estimatedInput += estimatedInputTokens;\r\n this.wastedByReason.set(reason, (this.wastedByReason.get(reason) ?? 0) + 1);\r\n\r\n logger.debug(`Wasted model attempt on ${model}`, { estimatedInput: estimatedInputTokens, reason });\r\n }\r\n\r\n recordSkippedCall(model: string, reason: string) {\r\n this.wasted.skips += 1;\r\n\r\n logger.debug(`Skipped model call on ${model}`, { reason });\r\n }\r\n\r\n getWasted(): WastedUsage {\r\n return { ...this.wasted, byReason: Object.fromEntries(this.wastedByReason) };\r\n }\r\n\r\n getTotalUsage(): TokenUsage {\r\n let input = 0;\r\n let output = 0;\r\n for (const modelUsage of this.usage.values()) {\r\n input += modelUsage.input;\r\n output += modelUsage.output;\r\n }\r\n return { input, output };\r\n }\r\n\r\n getBreakdown(): ModelUsage[] {\r\n return Array.from(this.usage.values());\r\n }\r\n\r\n merge(other: TokenTracker) {\r\n for (const usage of other.getBreakdown()) {\r\n this.record(usage.model, usage.input, usage.output);\r\n }\r\n\r\n const otherWasted = other.getWasted();\r\n this.wasted.attempts += otherWasted.attempts;\r\n this.wasted.estimatedInput += otherWasted.estimatedInput;\r\n this.wasted.skips += otherWasted.skips;\r\n for (const [reason, count] of Object.entries(otherWasted.byReason)) {\r\n this.wastedByReason.set(reason, (this.wastedByReason.get(reason) ?? 0) + count);\r\n }\r\n }\r\n\r\n reset() {\r\n this.usage.clear();\r\n this.wasted = { attempts: 0, estimatedInput: 0, skips: 0 };\r\n this.wastedByReason.clear();\r\n }\r\n}\r\n"],"mappings":";;;;;AAqBO,IAAM,eAAN,MAAmB;AAAA,EAChB,QAAiC,oBAAI,IAAI;AAAA,EACzC,SAAS,EAAE,UAAU,GAAG,gBAAgB,GAAG,OAAO,EAAE;AAAA,EACpD,iBAAsC,oBAAI,IAAI;AAAA,EAC9C,cAAc;AAAA,EACL,kBAAkB;AAAA,EAClB,cAAc;AAAA,EAE/B,qBAAqB,QAAQ,GAAG;AAC9B,SAAK,eAAe;AAAA,EACtB;AAAA,EAEA,qBAAqB;AACnB,WAAO,KAAK;AAAA,EACd;AAAA,EAEA,wBAAwB,SAAS,GAAG;AAClC,WAAO,KAAK,cAAc,UAAU,KAAK;AAAA,EAC3C;AAAA,EAEA,cAAc;AACZ,WAAO,KAAK,eAAe,KAAK,kBAAkB,KAAK;AAAA,EACzD;AAAA,EAEA,sBAAsB;AACpB,WAAO,KAAK,IAAI,GAAG,KAAK,kBAAkB,KAAK,cAAc,KAAK,WAAW;AAAA,EAC/E;AAAA,EAEA,OAAO,OAAe,OAAe,QAAgB;AACnD,UAAM,WAAW,KAAK,MAAM,IAAI,KAAK,KAAK,EAAE,OAAO,OAAO,GAAG,QAAQ,GAAG,OAAO,EAAE;AAEjF,SAAK,MAAM,IAAI,OAAO;AAAA,MACpB;AAAA,MACA,OAAO,SAAS,QAAQ;AAAA,MACxB,QAAQ,SAAS,SAAS;AAAA,MAC1B,OAAO,SAAS,QAAQ;AAAA,IAC1B,CAAC;AAED,WAAO,MAAM,4BAA4B,KAAK,IAAI;AAAA,MAChD;AAAA,MACA;AAAA,MACA,YAAY,SAAS,QAAQ;AAAA,MAC7B,aAAa,SAAS,SAAS;AAAA,IACjC,CAAC;AAAA,EACH;AAAA,EAEA,oBAAoB,OAAe,sBAA8B,QAA6B;AAC5F,SAAK,OAAO,YAAY;AACxB,SAAK,OAAO,kBAAkB;AAC9B,SAAK,eAAe,IAAI,SAAS,KAAK,eAAe,IAAI,MAAM,KAAK,KAAK,CAAC;AAE1E,WAAO,MAAM,2BAA2B,KAAK,IAAI,EAAE,gBAAgB,sBAAsB,OAAO,CAAC;AAAA,EACnG;AAAA,EAEA,kBAAkB,OAAe,QAAgB;AAC/C,SAAK,OAAO,SAAS;AAErB,WAAO,MAAM,yBAAyB,KAAK,IAAI,EAAE,OAAO,CAAC;AAAA,EAC3D;AAAA,EAEA,YAAyB;AACvB,WAAO,EAAE,GAAG,KAAK,QAAQ,UAAU,OAAO,YAAY,KAAK,cAAc,EAAE;AAAA,EAC7E;AAAA,EAEA,gBAA4B;AAC1B,QAAI,QAAQ;AACZ,QAAI,SAAS;AACb,eAAW,cAAc,KAAK,MAAM,OAAO,GAAG;AAC5C,eAAS,WAAW;AACpB,gBAAU,WAAW;AAAA,IACvB;AACA,WAAO,EAAE,OAAO,OAAO;AAAA,EACzB;AAAA,EAEA,eAA6B;AAC3B,WAAO,MAAM,KAAK,KAAK,MAAM,OAAO,CAAC;AAAA,EACvC;AAAA,EAEA,MAAM,OAAqB;AACzB,eAAW,SAAS,MAAM,aAAa,GAAG;AACxC,WAAK,OAAO,MAAM,OAAO,MAAM,OAAO,MAAM,MAAM;AAAA,IACpD;AAEA,UAAM,cAAc,MAAM,UAAU;AACpC,SAAK,OAAO,YAAY,YAAY;AACpC,SAAK,OAAO,kBAAkB,YAAY;AAC1C,SAAK,OAAO,SAAS,YAAY;AACjC,eAAW,CAAC,QAAQ,KAAK,KAAK,OAAO,QAAQ,YAAY,QAAQ,GAAG;AAClE,WAAK,eAAe,IAAI,SAAS,KAAK,eAAe,IAAI,MAAM,KAAK,KAAK,KAAK;AAAA,IAChF;AAAA,EACF;AAAA,EAEA,QAAQ;AACN,SAAK,MAAM,MAAM;AACjB,SAAK,SAAS,EAAE,UAAU,GAAG,gBAAgB,GAAG,OAAO,EAAE;AACzD,SAAK,eAAe,MAAM;AAAA,EAC5B;AACF;","names":[]}
@@ -1 +0,0 @@
1
- {"version":3,"sources":["../src/rules/table.ts"],"sourcesContent":["import type { ClaimType, reviewSeverities } from '@codraoss/schema';\r\n\r\ntype ReviewSeverity = typeof reviewSeverities[number];\r\n\r\nexport type Rule = {\r\n id: string;\r\n claimType: ClaimType;\r\n severity: ReviewSeverity;\r\n title: string;\r\n body: string;\r\n triggers: readonly string[];\r\n pattern: RegExp;\r\n rejectRaw?: RegExp;\r\n extensions?: readonly string[];\r\n enabled: boolean;\r\n};\r\n\r\nconst ts = ['ts', 'tsx', 'js', 'jsx', 'mjs', 'cjs'] as const;\r\n\r\nexport const RULES: readonly Rule[] = [\r\n {\r\n id: 'empty-catch',\r\n claimType: 'swallowed_error',\r\n severity: 'P2',\r\n title: 'Empty catch block swallows the error',\r\n body: 'This `catch` has no body, so the error is discarded with no log, no rethrow and no recovery. '\r\n + 'A failure here becomes silent. If the error is genuinely expected, say so in a comment inside the block.',\r\n triggers: ['catch'],\r\n pattern: /\\bcatch\\s*(\\([^)]*\\))?\\s*\\{\\s*\\}/,\r\n rejectRaw: /\\bcatch\\s*(\\([^)]*\\))?\\s*\\{\\s*(?:\\/\\/|\\/\\*)/,\r\n extensions: ts,\r\n enabled: true,\r\n },\r\n {\r\n id: 'debugger-statement',\r\n claimType: 'other',\r\n severity: 'P1',\r\n title: '`debugger` statement left in the diff',\r\n body: 'A `debugger` statement halts execution whenever devtools are open. This is almost always '\r\n + 'a leftover from local debugging.',\r\n triggers: ['debugger'],\r\n pattern: /^\\s*debugger\\s*;?\\s*$/,\r\n extensions: ts,\r\n enabled: true,\r\n },\r\n {\r\n id: 'focused-test',\r\n claimType: 'other',\r\n severity: 'P1',\r\n title: 'Focused test will skip the rest of the suite',\r\n body: 'A focused test (`.only`) silently prevents every other test in the file from running, so '\r\n + 'CI stays green while covering almost nothing.',\r\n triggers: ['.only'],\r\n pattern: /\\b(?:describe|it|test|context|suite)\\s*\\.\\s*only\\s*\\(/,\r\n extensions: ts,\r\n enabled: true,\r\n },\r\n {\r\n id: 'dynamic-code-exec',\r\n claimType: 'unsafe_dynamic_code',\r\n severity: 'P1',\r\n title: 'Dynamic code execution',\r\n body: '`eval` and the `Function` constructor execute arbitrary strings as code. If any part of '\r\n + 'that string can be influenced by input, this is remote code execution.',\r\n triggers: ['eval(', 'Function('],\r\n pattern: /(?:^|[^.\\w])eval\\s*\\(|new\\s+Function\\s*\\(/,\r\n extensions: ts,\r\n enabled: true,\r\n },\r\n {\r\n id: 'dynamic-html-sink',\r\n claimType: 'unsafe_dom_sink',\r\n severity: 'P1',\r\n title: 'Unsanitized value assigned to an HTML sink',\r\n body: 'Assigning a non-literal to `innerHTML`/`outerHTML` (or passing one to `insertAdjacentHTML`) '\r\n + 'executes any markup it contains. If the value can carry user input this is XSS.',\r\n triggers: ['innerHTML', 'outerHTML', 'insertAdjacentHTML'],\r\n pattern: /\\.(?:inner|outer)HTML\\s*=\\s*[A-Za-z_$][\\w$.[\\]()]*|insertAdjacentHTML\\s*\\([^)]*,\\s*[A-Za-z_$]/,\r\n extensions: ts,\r\n enabled: true,\r\n },\r\n {\r\n id: 'mutable-default-arg',\r\n claimType: 'mutable_default_arg',\r\n severity: 'P2',\r\n title: 'Mutable default argument',\r\n body: 'Python evaluates a default argument once, at definition time, so this list/dict/set is '\r\n + 'shared by every call. Mutating it leaks state between invocations. Use `None` and build the '\r\n + 'value inside the function.',\r\n triggers: ['def '],\r\n pattern: /\\bdef\\s+\\w+\\s*\\([^)]*=\\s*(?:\\[\\s*\\]|\\{\\s*\\}|set\\s*\\(\\s*\\)|dict\\s*\\(\\s*\\)|list\\s*\\(\\s*\\))/,\r\n extensions: ['py'],\r\n enabled: true,\r\n },\r\n {\r\n id: 'destructive-migration',\r\n claimType: 'destructive_migration',\r\n severity: 'P1',\r\n title: 'Destructive migration statement',\r\n body: 'This statement discards data irreversibly. On a forward-only migration chain there is no '\r\n + 'rollback: confirm the column/table is genuinely unused and that a backup exists.',\r\n triggers: ['DROP', 'TRUNCATE', 'drop', 'truncate'],\r\n pattern: /\\b(?:drop\\s+(?:column|table)|truncate\\s+table|truncate\\s+\\w)/i,\r\n extensions: ['sql'],\r\n enabled: true,\r\n },\r\n\r\n\r\n {\r\n id: 'hardcoded-secret',\r\n claimType: 'hardcoded_secret',\r\n severity: 'P0',\r\n title: 'Possible hardcoded credential',\r\n body: 'This looks like a literal credential committed to the repository. If it is real, rotate it '\r\n + 'and move it to a secret binding.',\r\n triggers: ['sk-', 'AIza', 'ghp_', 'AKIA'],\r\n pattern: /\\b(?:sk-[A-Za-z0-9]{20,}|AIza[0-9A-Za-z_-]{30,}|gh[pousr]_[A-Za-z0-9]{30,}|AKIA[0-9A-Z]{16})\\b/,\r\n enabled: false,\r\n },\r\n {\r\n id: 'insecure-random',\r\n claimType: 'insecure_randomness',\r\n severity: 'P2',\r\n title: '`Math.random()` used for a security-sensitive value',\r\n body: '`Math.random()` is not cryptographically secure and its output is predictable. Use '\r\n + '`crypto.getRandomValues()` for tokens, ids or anything an attacker should not guess.',\r\n triggers: ['Math.random'],\r\n pattern: /\\b(?:token|secret|key|nonce|salt|password|session|id)\\w*\\s*=[^=]*Math\\.random\\s*\\(/i,\r\n extensions: ts,\r\n enabled: false,\r\n },\r\n];\r\n\r\n"],"mappings":";AAiBA,IAAM,KAAK,CAAC,MAAM,OAAO,MAAM,OAAO,OAAO,KAAK;AAE3C,IAAM,QAAyB;AAAA,EACpC;AAAA,IACE,IAAI;AAAA,IACJ,WAAW;AAAA,IACX,UAAU;AAAA,IACV,OAAO;AAAA,IACP,MAAM;AAAA,IAEN,UAAU,CAAC,OAAO;AAAA,IAClB,SAAS;AAAA,IACT,WAAW;AAAA,IACX,YAAY;AAAA,IACZ,SAAS;AAAA,EACX;AAAA,EACA;AAAA,IACE,IAAI;AAAA,IACJ,WAAW;AAAA,IACX,UAAU;AAAA,IACV,OAAO;AAAA,IACP,MAAM;AAAA,IAEN,UAAU,CAAC,UAAU;AAAA,IACrB,SAAS;AAAA,IACT,YAAY;AAAA,IACZ,SAAS;AAAA,EACX;AAAA,EACA;AAAA,IACE,IAAI;AAAA,IACJ,WAAW;AAAA,IACX,UAAU;AAAA,IACV,OAAO;AAAA,IACP,MAAM;AAAA,IAEN,UAAU,CAAC,OAAO;AAAA,IAClB,SAAS;AAAA,IACT,YAAY;AAAA,IACZ,SAAS;AAAA,EACX;AAAA,EACA;AAAA,IACE,IAAI;AAAA,IACJ,WAAW;AAAA,IACX,UAAU;AAAA,IACV,OAAO;AAAA,IACP,MAAM;AAAA,IAEN,UAAU,CAAC,SAAS,WAAW;AAAA,IAC/B,SAAS;AAAA,IACT,YAAY;AAAA,IACZ,SAAS;AAAA,EACX;AAAA,EACA;AAAA,IACE,IAAI;AAAA,IACJ,WAAW;AAAA,IACX,UAAU;AAAA,IACV,OAAO;AAAA,IACP,MAAM;AAAA,IAEN,UAAU,CAAC,aAAa,aAAa,oBAAoB;AAAA,IACzD,SAAS;AAAA,IACT,YAAY;AAAA,IACZ,SAAS;AAAA,EACX;AAAA,EACA;AAAA,IACE,IAAI;AAAA,IACJ,WAAW;AAAA,IACX,UAAU;AAAA,IACV,OAAO;AAAA,IACP,MAAM;AAAA,IAGN,UAAU,CAAC,MAAM;AAAA,IACjB,SAAS;AAAA,IACT,YAAY,CAAC,IAAI;AAAA,IACjB,SAAS;AAAA,EACX;AAAA,EACA;AAAA,IACE,IAAI;AAAA,IACJ,WAAW;AAAA,IACX,UAAU;AAAA,IACV,OAAO;AAAA,IACP,MAAM;AAAA,IAEN,UAAU,CAAC,QAAQ,YAAY,QAAQ,UAAU;AAAA,IACjD,SAAS;AAAA,IACT,YAAY,CAAC,KAAK;AAAA,IAClB,SAAS;AAAA,EACX;AAAA,EAGA;AAAA,IACE,IAAI;AAAA,IACJ,WAAW;AAAA,IACX,UAAU;AAAA,IACV,OAAO;AAAA,IACP,MAAM;AAAA,IAEN,UAAU,CAAC,OAAO,QAAQ,QAAQ,MAAM;AAAA,IACxC,SAAS;AAAA,IACT,SAAS;AAAA,EACX;AAAA,EACA;AAAA,IACE,IAAI;AAAA,IACJ,WAAW;AAAA,IACX,UAAU;AAAA,IACV,OAAO;AAAA,IACP,MAAM;AAAA,IAEN,UAAU,CAAC,aAAa;AAAA,IACxB,SAAS;AAAA,IACT,YAAY;AAAA,IACZ,SAAS;AAAA,EACX;AACF;","names":[]}
@@ -1 +0,0 @@
1
- {"version":3,"sources":["../src/prompts/languages.ts"],"sourcesContent":["export type LanguageGuideline = {\r\n language: string;\r\n extensions: string[];\r\n guidelines: string[];\r\n persona?: string;\r\n};\r\n\r\nconst LANGUAGE_GUIDELINES: LanguageGuideline[] = [\r\n {\r\n language: 'TypeScript/JavaScript',\r\n persona: 'an expert TypeScript engineer focused on correctness and safe async code',\r\n extensions: ['ts', 'tsx', 'js', 'jsx', 'mjs', 'cjs'],\r\n guidelines: [\r\n 'Flag unhandled promise rejections, missing await, or async errors that can crash or silently drop work.',\r\n 'Flag resource leaks that cause real bugs (uncleared timers/intervals/listeners on a path that runs repeatedly).',\r\n 'Flag security pitfalls such as eval() on untrusted input or ReDoS-prone regexes.',\r\n 'Flag runtime-breaking null/undefined access introduced by the diff.',\r\n ],\r\n },\r\n {\r\n language: 'Python',\r\n persona: 'a Python engineer focused on correctness',\r\n extensions: ['py'],\r\n guidelines: [\r\n 'Flag mutable default arguments that cause shared-state bugs.',\r\n 'Flag bare \"except:\" that swallows errors and hides failures.',\r\n 'Flag incorrect exception handling or resource handling (files/sockets not closed).',\r\n ],\r\n },\r\n {\r\n language: 'CSS/SCSS/Less',\r\n persona: 'a frontend engineer',\r\n extensions: ['css', 'scss', 'sass', 'less'],\r\n guidelines: [\r\n 'Flag only rules that break layout or rendering; do not report stylistic preferences.',\r\n ],\r\n },\r\n {\r\n language: 'SQL',\r\n persona: 'a database engineer focused on query safety and correctness',\r\n extensions: ['sql'],\r\n guidelines: [\r\n 'Flag SQL injection risks (unparameterized/interpolated user input).',\r\n 'Flag destructive or non-atomic migrations that risk data loss.',\r\n ],\r\n },\r\n {\r\n language: 'Markdown',\r\n persona: 'a technical writer',\r\n extensions: ['md', 'mdx'],\r\n guidelines: [\r\n 'Flag only broken links/images or factually incorrect content; do not report style or grammar nits.',\r\n ],\r\n },\r\n {\r\n language: 'HTML',\r\n persona: 'a web engineer',\r\n extensions: ['html', 'htm'],\r\n guidelines: [\r\n 'Flag only markup that is broken or functionally inaccessible; do not report SEO or style preferences.',\r\n ],\r\n },\r\n {\r\n language: 'JSON/Config',\r\n persona: 'a DevOps engineer',\r\n extensions: ['json', 'jsonc', 'yaml', 'yml', 'toml'],\r\n guidelines: [\r\n 'Flag invalid syntax/schema or hardcoded secrets; do not report naming-convention preferences.',\r\n ],\r\n },\r\n];\r\n\r\nexport function getLanguageForFile(path: string): LanguageGuideline | undefined {\r\n const ext = path.split('.').pop()?.toLowerCase();\r\n if (!ext) return undefined;\r\n\r\n const matches = LANGUAGE_GUIDELINES.filter((g) => g.extensions.includes(ext));\r\n \r\n if (matches.length === 0) return undefined;\r\n\r\n if (matches.length > 1) {\r\n return matches.reduce((best, candidate) =>\r\n candidate.extensions.length < best.extensions.length ? candidate : best,\r\n );\r\n }\r\n\r\n return matches[0];\r\n}\r\n"],"mappings":";AAOA,IAAM,sBAA2C;AAAA,EAC/C;AAAA,IACE,UAAU;AAAA,IACV,SAAS;AAAA,IACT,YAAY,CAAC,MAAM,OAAO,MAAM,OAAO,OAAO,KAAK;AAAA,IACnD,YAAY;AAAA,MACV;AAAA,MACA;AAAA,MACA;AAAA,MACA;AAAA,IACF;AAAA,EACF;AAAA,EACA;AAAA,IACE,UAAU;AAAA,IACV,SAAS;AAAA,IACT,YAAY,CAAC,IAAI;AAAA,IACjB,YAAY;AAAA,MACV;AAAA,MACA;AAAA,MACA;AAAA,IACF;AAAA,EACF;AAAA,EACA;AAAA,IACE,UAAU;AAAA,IACV,SAAS;AAAA,IACT,YAAY,CAAC,OAAO,QAAQ,QAAQ,MAAM;AAAA,IAC1C,YAAY;AAAA,MACV;AAAA,IACF;AAAA,EACF;AAAA,EACA;AAAA,IACE,UAAU;AAAA,IACV,SAAS;AAAA,IACT,YAAY,CAAC,KAAK;AAAA,IAClB,YAAY;AAAA,MACV;AAAA,MACA;AAAA,IACF;AAAA,EACF;AAAA,EACA;AAAA,IACE,UAAU;AAAA,IACV,SAAS;AAAA,IACT,YAAY,CAAC,MAAM,KAAK;AAAA,IACxB,YAAY;AAAA,MACV;AAAA,IACF;AAAA,EACF;AAAA,EACA;AAAA,IACE,UAAU;AAAA,IACV,SAAS;AAAA,IACT,YAAY,CAAC,QAAQ,KAAK;AAAA,IAC1B,YAAY;AAAA,MACV;AAAA,IACF;AAAA,EACF;AAAA,EACA;AAAA,IACE,UAAU;AAAA,IACV,SAAS;AAAA,IACT,YAAY,CAAC,QAAQ,SAAS,QAAQ,OAAO,MAAM;AAAA,IACnD,YAAY;AAAA,MACV;AAAA,IACF;AAAA,EACF;AACF;AAEO,SAAS,mBAAmB,MAA6C;AAC9E,QAAM,MAAM,KAAK,MAAM,GAAG,EAAE,IAAI,GAAG,YAAY;AAC/C,MAAI,CAAC,IAAK,QAAO;AAEjB,QAAM,UAAU,oBAAoB,OAAO,CAAC,MAAM,EAAE,WAAW,SAAS,GAAG,CAAC;AAE5E,MAAI,QAAQ,WAAW,EAAG,QAAO;AAEjC,MAAI,QAAQ,SAAS,GAAG;AACtB,WAAO,QAAQ;AAAA,MAAO,CAAC,MAAM,cAC3B,UAAU,WAAW,SAAS,KAAK,WAAW,SAAS,YAAY;AAAA,IACrE;AAAA,EACF;AAEA,SAAO,QAAQ,CAAC;AAClB;","names":[]}