secondpair 0.1.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.
Files changed (62) hide show
  1. package/README.md +119 -0
  2. package/dist/bitbucket/comments.d.ts +49 -0
  3. package/dist/bitbucket/comments.js +198 -0
  4. package/dist/bitbucket/comments.js.map +1 -0
  5. package/dist/cli.d.ts +2 -0
  6. package/dist/cli.js +208 -0
  7. package/dist/cli.js.map +1 -0
  8. package/dist/config.d.ts +33 -0
  9. package/dist/config.js +87 -0
  10. package/dist/config.js.map +1 -0
  11. package/dist/diff/github.d.ts +14 -0
  12. package/dist/diff/github.js +27 -0
  13. package/dist/diff/github.js.map +1 -0
  14. package/dist/diff/local.d.ts +10 -0
  15. package/dist/diff/local.js +39 -0
  16. package/dist/diff/local.js.map +1 -0
  17. package/dist/diff/parse.d.ts +9 -0
  18. package/dist/diff/parse.js +141 -0
  19. package/dist/diff/parse.js.map +1 -0
  20. package/dist/finding-id.d.ts +28 -0
  21. package/dist/finding-id.js +96 -0
  22. package/dist/finding-id.js.map +1 -0
  23. package/dist/github/comments.d.ts +25 -0
  24. package/dist/github/comments.js +193 -0
  25. package/dist/github/comments.js.map +1 -0
  26. package/dist/gitlab/comments.d.ts +46 -0
  27. package/dist/gitlab/comments.js +213 -0
  28. package/dist/gitlab/comments.js.map +1 -0
  29. package/dist/index.d.ts +10 -0
  30. package/dist/index.js +11 -0
  31. package/dist/index.js.map +1 -0
  32. package/dist/llm/prompt.d.ts +23 -0
  33. package/dist/llm/prompt.js +53 -0
  34. package/dist/llm/prompt.js.map +1 -0
  35. package/dist/llm/schema.d.ts +61 -0
  36. package/dist/llm/schema.js +73 -0
  37. package/dist/llm/schema.js.map +1 -0
  38. package/dist/reconcile.d.ts +20 -0
  39. package/dist/reconcile.js +70 -0
  40. package/dist/reconcile.js.map +1 -0
  41. package/dist/redact.d.ts +3 -0
  42. package/dist/redact.js +25 -0
  43. package/dist/redact.js.map +1 -0
  44. package/dist/report/cli.d.ts +5 -0
  45. package/dist/report/cli.js +63 -0
  46. package/dist/report/cli.js.map +1 -0
  47. package/dist/report/json.d.ts +27 -0
  48. package/dist/report/json.js +53 -0
  49. package/dist/report/json.js.map +1 -0
  50. package/dist/review.d.ts +41 -0
  51. package/dist/review.js +274 -0
  52. package/dist/review.js.map +1 -0
  53. package/dist/suppress-signals.d.ts +7 -0
  54. package/dist/suppress-signals.js +19 -0
  55. package/dist/suppress-signals.js.map +1 -0
  56. package/dist/suppressions.d.ts +6 -0
  57. package/dist/suppressions.js +48 -0
  58. package/dist/suppressions.js.map +1 -0
  59. package/dist/types.d.ts +92 -0
  60. package/dist/types.js +14 -0
  61. package/dist/types.js.map +1 -0
  62. package/package.json +49 -0
package/dist/config.js ADDED
@@ -0,0 +1,87 @@
1
+ import { readFile } from "node:fs/promises";
2
+ import { existsSync } from "node:fs";
3
+ import path from "node:path";
4
+ import YAML from "yaml";
5
+ import { z } from "zod";
6
+ import { isIgnored as matchesIgnorePatterns, matchesGlob } from "codengram";
7
+ import { compileRedactPatterns } from "./redact.js";
8
+ import { CATEGORIES, SEVERITIES } from "./types.js";
9
+ export { matchesGlob };
10
+ const defaultCategories = Object.fromEntries(CATEGORIES.map((c) => [c, true]));
11
+ export const DEFAULT_CONFIG = {
12
+ fail_on: "high",
13
+ min_confidence: 0.5,
14
+ ignore: [],
15
+ context_token_budget: 8000,
16
+ context_snippets: 3,
17
+ custom_instructions: "",
18
+ categories: defaultCategories,
19
+ temperature: null,
20
+ limits: { max_findings_per_file: 5, max_total: 30 },
21
+ self_critique: false,
22
+ redact_secrets: true,
23
+ redact_patterns: [],
24
+ write_suppressions: false,
25
+ };
26
+ const configSchema = z
27
+ .object({
28
+ fail_on: z.enum(SEVERITIES).optional(),
29
+ min_confidence: z.number().min(0).max(1).optional(),
30
+ ignore: z.array(z.string()).optional(),
31
+ context_token_budget: z.number().int().positive().optional(),
32
+ context_snippets: z.number().int().min(0).optional(),
33
+ custom_instructions: z.string().optional(),
34
+ categories: z.partialRecord(z.enum(CATEGORIES), z.boolean()).optional(),
35
+ temperature: z.number().min(0).max(2).optional(),
36
+ limits: z
37
+ .object({
38
+ max_findings_per_file: z.number().int().positive().optional(),
39
+ max_total: z.number().int().positive().optional(),
40
+ })
41
+ .strict()
42
+ .optional(),
43
+ self_critique: z.boolean().optional(),
44
+ redact_secrets: z.boolean().optional(),
45
+ redact_patterns: z.array(z.string()).optional(),
46
+ write_suppressions: z.boolean().optional(),
47
+ })
48
+ .strict();
49
+ export const CONFIG_FILENAME = ".pr-review.yml";
50
+ /** Load .pr-review.yml from the repo root, merged over defaults. */
51
+ export async function loadConfig(cwd, configPath) {
52
+ const file = configPath ?? path.join(cwd, CONFIG_FILENAME);
53
+ if (!existsSync(file)) {
54
+ if (configPath)
55
+ throw new Error(`Config file not found: ${configPath}`);
56
+ return { ...DEFAULT_CONFIG };
57
+ }
58
+ const raw = YAML.parse(await readFile(file, "utf8")) ?? {};
59
+ const parsed = configSchema.safeParse(raw);
60
+ if (!parsed.success) {
61
+ const issues = parsed.error.issues
62
+ .map((i) => ` ${i.path.join(".") || "(root)"}: ${i.message}`)
63
+ .join("\n");
64
+ throw new Error(`Invalid ${path.basename(file)}:\n${issues}`);
65
+ }
66
+ compileRedactPatterns(parsed.data.redact_patterns ?? []);
67
+ return mergeConfig(parsed.data);
68
+ }
69
+ export function mergeConfig(partial) {
70
+ return {
71
+ ...DEFAULT_CONFIG,
72
+ ...partial,
73
+ fail_on: (partial.fail_on ?? DEFAULT_CONFIG.fail_on),
74
+ ignore: partial.ignore ?? [],
75
+ categories: { ...defaultCategories, ...(partial.categories ?? {}) },
76
+ temperature: partial.temperature ?? DEFAULT_CONFIG.temperature,
77
+ limits: { ...DEFAULT_CONFIG.limits, ...(partial.limits ?? {}) },
78
+ redact_secrets: partial.redact_secrets ?? DEFAULT_CONFIG.redact_secrets,
79
+ redact_patterns: partial.redact_patterns ?? DEFAULT_CONFIG.redact_patterns,
80
+ write_suppressions: partial.write_suppressions ?? DEFAULT_CONFIG.write_suppressions,
81
+ };
82
+ }
83
+ /** True when the path matches codengram's built-in ignores or this config's patterns. */
84
+ export function isIgnored(filePath, config) {
85
+ return matchesIgnorePatterns(filePath, config.ignore);
86
+ }
87
+ //# sourceMappingURL=config.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"config.js","sourceRoot":"","sources":["../src/config.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,14 @@
1
+ import { Octokit } from "@octokit/rest";
2
+ export interface PrRef {
3
+ owner: string;
4
+ repo: string;
5
+ pull_number: number;
6
+ }
7
+ export declare function parseRepoSlug(slug: string): {
8
+ owner: string;
9
+ repo: string;
10
+ };
11
+ export declare function makeOctokit(token: string | undefined): Octokit;
12
+ /** Fetch the unified diff of a pull request. */
13
+ export declare function getPrDiff(octokit: Octokit, pr: PrRef): Promise<string>;
14
+ export declare function getPrHeadSha(octokit: Octokit, pr: PrRef): Promise<string>;
@@ -0,0 +1,27 @@
1
+ import { Octokit } from "@octokit/rest";
2
+ export function parseRepoSlug(slug) {
3
+ const [owner, repo] = slug.split("/");
4
+ if (!owner || !repo)
5
+ throw new Error(`Invalid repo slug "${slug}" — expected owner/name.`);
6
+ return { owner, repo };
7
+ }
8
+ export function makeOctokit(token) {
9
+ if (!token) {
10
+ throw new Error("A GitHub token is required (GITHUB_TOKEN env var).");
11
+ }
12
+ return new Octokit({ auth: token });
13
+ }
14
+ /** Fetch the unified diff of a pull request. */
15
+ export async function getPrDiff(octokit, pr) {
16
+ const res = await octokit.pulls.get({
17
+ ...pr,
18
+ mediaType: { format: "diff" },
19
+ });
20
+ // With the diff media type the payload is the raw diff string.
21
+ return res.data;
22
+ }
23
+ export async function getPrHeadSha(octokit, pr) {
24
+ const res = await octokit.pulls.get({ ...pr });
25
+ return res.data.head.sha;
26
+ }
27
+ //# sourceMappingURL=github.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"github.js","sourceRoot":"","sources":["../../src/diff/github.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,OAAO,EAAE,MAAM,eAAe,CAAC;AAQxC,MAAM,UAAU,aAAa,CAAC,IAAY;IACxC,MAAM,CAAC,KAAK,EAAE,IAAI,CAAC,GAAG,IAAI,CAAC,KAAK,CAAC,GAAG,CAAC,CAAC;IACtC,IAAI,CAAC,KAAK,IAAI,CAAC,IAAI;QAAE,MAAM,IAAI,KAAK,CAAC,sBAAsB,IAAI,0BAA0B,CAAC,CAAC;IAC3F,OAAO,EAAE,KAAK,EAAE,IAAI,EAAE,CAAC;AACzB,CAAC;AAED,MAAM,UAAU,WAAW,CAAC,KAAyB;IACnD,IAAI,CAAC,KAAK,EAAE,CAAC;QACX,MAAM,IAAI,KAAK,CAAC,oDAAoD,CAAC,CAAC;IACxE,CAAC;IACD,OAAO,IAAI,OAAO,CAAC,EAAE,IAAI,EAAE,KAAK,EAAE,CAAC,CAAC;AACtC,CAAC;AAED,gDAAgD;AAChD,MAAM,CAAC,KAAK,UAAU,SAAS,CAAC,OAAgB,EAAE,EAAS;IACzD,MAAM,GAAG,GAAG,MAAM,OAAO,CAAC,KAAK,CAAC,GAAG,CAAC;QAClC,GAAG,EAAE;QACL,SAAS,EAAE,EAAE,MAAM,EAAE,MAAM,EAAE;KAC9B,CAAC,CAAC;IACH,+DAA+D;IAC/D,OAAO,GAAG,CAAC,IAAyB,CAAC;AACvC,CAAC;AAED,MAAM,CAAC,KAAK,UAAU,YAAY,CAAC,OAAgB,EAAE,EAAS;IAC5D,MAAM,GAAG,GAAG,MAAM,OAAO,CAAC,KAAK,CAAC,GAAG,CAAC,EAAE,GAAG,EAAE,EAAE,CAAC,CAAC;IAC/C,OAAO,GAAG,CAAC,IAAI,CAAC,IAAI,CAAC,GAAG,CAAC;AAC3B,CAAC"}
@@ -0,0 +1,10 @@
1
+ export declare function detectBaseRef(cwd: string): Promise<string>;
2
+ export interface LocalDiffOptions {
3
+ cwd: string;
4
+ /** Diff staged changes instead of branch-vs-base. */
5
+ staged?: boolean;
6
+ /** Base ref; auto-detected when omitted. */
7
+ base?: string;
8
+ }
9
+ /** Produce a unified diff from the local repository. */
10
+ export declare function getLocalDiff(opts: LocalDiffOptions): Promise<string>;
@@ -0,0 +1,39 @@
1
+ import { execFile } from "node:child_process";
2
+ import { promisify } from "node:util";
3
+ const exec = promisify(execFile);
4
+ const MAX_DIFF_BUFFER = 64 * 1024 * 1024;
5
+ async function git(args, cwd) {
6
+ const { stdout } = await exec("git", args, { cwd, maxBuffer: MAX_DIFF_BUFFER });
7
+ return stdout;
8
+ }
9
+ async function refExists(ref, cwd) {
10
+ try {
11
+ await git(["rev-parse", "--verify", "--quiet", ref], cwd);
12
+ return true;
13
+ }
14
+ catch {
15
+ return false;
16
+ }
17
+ }
18
+ export async function detectBaseRef(cwd) {
19
+ for (const ref of ["origin/main", "origin/master", "main", "master"]) {
20
+ if (await refExists(ref, cwd))
21
+ return ref;
22
+ }
23
+ throw new Error("Could not detect a base branch (tried origin/main, origin/master, main, master). Pass --base <ref>.");
24
+ }
25
+ /** Produce a unified diff from the local repository. */
26
+ export async function getLocalDiff(opts) {
27
+ if (opts.staged) {
28
+ return git(["diff", "--staged", "--no-color"], opts.cwd);
29
+ }
30
+ const base = opts.base ?? (await detectBaseRef(opts.cwd));
31
+ // Triple-dot: diff against the merge-base, matching what a PR would show.
32
+ const diff = await git(["diff", "--no-color", `${base}...HEAD`], opts.cwd);
33
+ if (diff.trim() !== "")
34
+ return diff;
35
+ // Fall back to uncommitted working-tree changes so `pr-review review` is
36
+ // useful before anything is committed.
37
+ return git(["diff", "--no-color", base], opts.cwd);
38
+ }
39
+ //# sourceMappingURL=local.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"local.js","sourceRoot":"","sources":["../../src/diff/local.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,QAAQ,EAAE,MAAM,oBAAoB,CAAC;AAC9C,OAAO,EAAE,SAAS,EAAE,MAAM,WAAW,CAAC;AAEtC,MAAM,IAAI,GAAG,SAAS,CAAC,QAAQ,CAAC,CAAC;AAEjC,MAAM,eAAe,GAAG,EAAE,GAAG,IAAI,GAAG,IAAI,CAAC;AAEzC,KAAK,UAAU,GAAG,CAAC,IAAc,EAAE,GAAW;IAC5C,MAAM,EAAE,MAAM,EAAE,GAAG,MAAM,IAAI,CAAC,KAAK,EAAE,IAAI,EAAE,EAAE,GAAG,EAAE,SAAS,EAAE,eAAe,EAAE,CAAC,CAAC;IAChF,OAAO,MAAM,CAAC;AAChB,CAAC;AAED,KAAK,UAAU,SAAS,CAAC,GAAW,EAAE,GAAW;IAC/C,IAAI,CAAC;QACH,MAAM,GAAG,CAAC,CAAC,WAAW,EAAE,UAAU,EAAE,SAAS,EAAE,GAAG,CAAC,EAAE,GAAG,CAAC,CAAC;QAC1D,OAAO,IAAI,CAAC;IACd,CAAC;IAAC,MAAM,CAAC;QACP,OAAO,KAAK,CAAC;IACf,CAAC;AACH,CAAC;AAED,MAAM,CAAC,KAAK,UAAU,aAAa,CAAC,GAAW;IAC7C,KAAK,MAAM,GAAG,IAAI,CAAC,aAAa,EAAE,eAAe,EAAE,MAAM,EAAE,QAAQ,CAAC,EAAE,CAAC;QACrE,IAAI,MAAM,SAAS,CAAC,GAAG,EAAE,GAAG,CAAC;YAAE,OAAO,GAAG,CAAC;IAC5C,CAAC;IACD,MAAM,IAAI,KAAK,CACb,qGAAqG,CACtG,CAAC;AACJ,CAAC;AAUD,wDAAwD;AACxD,MAAM,CAAC,KAAK,UAAU,YAAY,CAAC,IAAsB;IACvD,IAAI,IAAI,CAAC,MAAM,EAAE,CAAC;QAChB,OAAO,GAAG,CAAC,CAAC,MAAM,EAAE,UAAU,EAAE,YAAY,CAAC,EAAE,IAAI,CAAC,GAAG,CAAC,CAAC;IAC3D,CAAC;IACD,MAAM,IAAI,GAAG,IAAI,CAAC,IAAI,IAAI,CAAC,MAAM,aAAa,CAAC,IAAI,CAAC,GAAG,CAAC,CAAC,CAAC;IAC1D,0EAA0E;IAC1E,MAAM,IAAI,GAAG,MAAM,GAAG,CAAC,CAAC,MAAM,EAAE,YAAY,EAAE,GAAG,IAAI,SAAS,CAAC,EAAE,IAAI,CAAC,GAAG,CAAC,CAAC;IAC3E,IAAI,IAAI,CAAC,IAAI,EAAE,KAAK,EAAE;QAAE,OAAO,IAAI,CAAC;IACpC,yEAAyE;IACzE,uCAAuC;IACvC,OAAO,GAAG,CAAC,CAAC,MAAM,EAAE,YAAY,EAAE,IAAI,CAAC,EAAE,IAAI,CAAC,GAAG,CAAC,CAAC;AACrD,CAAC"}
@@ -0,0 +1,9 @@
1
+ import type { FileDiff } from "../types.js";
2
+ /**
3
+ * Parse a unified diff (git diff / GitHub PR diff) into structured FileDiff entries.
4
+ * Tracks, per file, the set of new-file line numbers that were added — these are
5
+ * the lines the LLM may comment on and the lines GitHub accepts review comments on.
6
+ */
7
+ export declare function parseDiff(diffText: string): FileDiff[];
8
+ /** Render a FileDiff back to annotated text with explicit new-file line numbers for the prompt. */
9
+ export declare function renderDiffForPrompt(file: FileDiff): string;
@@ -0,0 +1,141 @@
1
+ const HUNK_HEADER = /^@@ -(\d+)(?:,(\d+))? \+(\d+)(?:,(\d+))? @@/;
2
+ /**
3
+ * Parse a unified diff (git diff / GitHub PR diff) into structured FileDiff entries.
4
+ * Tracks, per file, the set of new-file line numbers that were added — these are
5
+ * the lines the LLM may comment on and the lines GitHub accepts review comments on.
6
+ */
7
+ export function parseDiff(diffText) {
8
+ const files = [];
9
+ const lines = diffText.split("\n");
10
+ let current = null;
11
+ let currentHunk = null;
12
+ let newLineNo = 0;
13
+ const flushFile = () => {
14
+ if (current)
15
+ files.push(current);
16
+ current = null;
17
+ currentHunk = null;
18
+ };
19
+ for (let i = 0; i < lines.length; i++) {
20
+ const line = lines[i];
21
+ if (line.startsWith("diff --git ")) {
22
+ flushFile();
23
+ const paths = parseDiffGitLine(line);
24
+ current = {
25
+ path: paths.newPath,
26
+ oldPath: paths.oldPath !== paths.newPath ? paths.oldPath : undefined,
27
+ status: "modified",
28
+ hunks: [],
29
+ changedLines: [],
30
+ };
31
+ continue;
32
+ }
33
+ if (!current)
34
+ continue;
35
+ if (line.startsWith("new file mode")) {
36
+ current.status = "added";
37
+ continue;
38
+ }
39
+ if (line.startsWith("deleted file mode")) {
40
+ current.status = "deleted";
41
+ current.path = current.oldPath ?? current.path;
42
+ current.oldPath = undefined;
43
+ continue;
44
+ }
45
+ if (line.startsWith("rename from ")) {
46
+ current.status = "renamed";
47
+ current.oldPath = line.slice("rename from ".length);
48
+ continue;
49
+ }
50
+ if (line.startsWith("rename to ")) {
51
+ current.path = line.slice("rename to ".length);
52
+ continue;
53
+ }
54
+ if (line.startsWith("--- ") || line.startsWith("+++ ")) {
55
+ // Path info already derived from the diff --git / rename lines.
56
+ continue;
57
+ }
58
+ const hunkMatch = HUNK_HEADER.exec(line);
59
+ if (hunkMatch) {
60
+ currentHunk = {
61
+ oldStart: parseInt(hunkMatch[1], 10),
62
+ oldLines: hunkMatch[2] !== undefined ? parseInt(hunkMatch[2], 10) : 1,
63
+ newStart: parseInt(hunkMatch[3], 10),
64
+ newLines: hunkMatch[4] !== undefined ? parseInt(hunkMatch[4], 10) : 1,
65
+ lines: [],
66
+ };
67
+ current.hunks.push(currentHunk);
68
+ newLineNo = currentHunk.newStart;
69
+ continue;
70
+ }
71
+ if (!currentHunk)
72
+ continue;
73
+ if (line.startsWith("+")) {
74
+ currentHunk.lines.push(line);
75
+ current.changedLines.push(newLineNo);
76
+ newLineNo++;
77
+ }
78
+ else if (line.startsWith("-")) {
79
+ currentHunk.lines.push(line);
80
+ }
81
+ else if (line.startsWith(" ") || line === "") {
82
+ currentHunk.lines.push(line);
83
+ newLineNo++;
84
+ }
85
+ else if (line.startsWith("\\")) {
86
+ // ""
87
+ currentHunk.lines.push(line);
88
+ }
89
+ else {
90
+ // Anything else ends the hunk (e.g. next file header handled above).
91
+ currentHunk = null;
92
+ }
93
+ }
94
+ flushFile();
95
+ return files;
96
+ }
97
+ function parseDiffGitLine(line) {
98
+ // diff --git a/src/foo.ts b/src/foo.ts (paths with spaces may be quoted)
99
+ const rest = line.slice("diff --git ".length);
100
+ const quoted = /^"a\/(.+)" "b\/(.+)"$/.exec(rest);
101
+ if (quoted)
102
+ return { oldPath: unescapeGitPath(quoted[1]), newPath: unescapeGitPath(quoted[2]) };
103
+ // Unquoted: split on " b/" — handles the common case; paths containing " b/" must be quoted by git anyway.
104
+ const idx = rest.lastIndexOf(" b/");
105
+ if (idx === -1)
106
+ return { oldPath: rest, newPath: rest };
107
+ const oldPath = rest.slice(0, idx).replace(/^a\//, "");
108
+ const newPath = rest.slice(idx + 3);
109
+ return { oldPath, newPath };
110
+ }
111
+ function unescapeGitPath(p) {
112
+ return p.replace(/\\(.)/g, "$1");
113
+ }
114
+ /** Render a FileDiff back to annotated text with explicit new-file line numbers for the prompt. */
115
+ export function renderDiffForPrompt(file) {
116
+ const out = [`### ${file.status.toUpperCase()}: ${file.path}`];
117
+ if (file.oldPath)
118
+ out.push(`(renamed from ${file.oldPath})`);
119
+ for (const hunk of file.hunks) {
120
+ out.push(`@@ -${hunk.oldStart},${hunk.oldLines} +${hunk.newStart},${hunk.newLines} @@`);
121
+ let newNo = hunk.newStart;
122
+ for (const l of hunk.lines) {
123
+ if (l.startsWith("+")) {
124
+ out.push(`${String(newNo).padStart(5)} + ${l.slice(1)}`);
125
+ newNo++;
126
+ }
127
+ else if (l.startsWith("-")) {
128
+ out.push(` - ${l.slice(1)}`);
129
+ }
130
+ else if (l.startsWith("\\")) {
131
+ out.push(` ${l}`);
132
+ }
133
+ else {
134
+ out.push(`${String(newNo).padStart(5)} ${l.slice(1)}`);
135
+ newNo++;
136
+ }
137
+ }
138
+ }
139
+ return out.join("\n");
140
+ }
141
+ //# sourceMappingURL=parse.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"parse.js","sourceRoot":"","sources":["../../src/diff/parse.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,28 @@
1
+ import type { Finding } from "./types.js";
2
+ export declare const FINDING_ID_MARKER_PREFIX = "<!-- pr-review-id:";
3
+ export declare const FINDING_ID_RE: RegExp;
4
+ /** Normalize title so minor wording churn still often matches. */
5
+ export declare function normalizeTitle(title: string): string;
6
+ /** Significant tokens from a title (lowercase, no stopwords, len ≥ 3). */
7
+ export declare function titleTokens(title: string): string[];
8
+ /**
9
+ * Stable fingerprint: file + category + sorted significant title tokens.
10
+ * Line numbers are excluded (they drift). Token set ignores filler words so
11
+ * "Off-by-one loop reads past end" ≈ "Off-by-one loop includes xs length".
12
+ */
13
+ export declare function fingerprintFinding(f: Pick<Finding, "file" | "category" | "title">): string;
14
+ export declare function withFindingId<T extends Finding>(f: T): T & {
15
+ id: string;
16
+ };
17
+ export declare function embedFindingId(body: string, id: string): string;
18
+ export declare function parseFindingId(body: string): string | null;
19
+ export declare function collectIdsFromBodies(bodies: string[]): Set<string>;
20
+ /** Jaccard similarity of significant title tokens (0..1). */
21
+ export declare function titleSimilarity(a: string, b: string): number;
22
+ /** True when line ranges overlap or sit within `slack` lines of each other. */
23
+ export declare function linesNear(a: Pick<Finding, "start_line" | "end_line">, b: Pick<Finding, "start_line" | "end_line">, slack?: number): boolean;
24
+ /**
25
+ * Soft match for re-runs where the model rephrases the title.
26
+ * Same file + category + nearby lines + enough title-token overlap.
27
+ */
28
+ export declare function findingsSoftMatch(a: Pick<Finding, "file" | "category" | "title" | "start_line" | "end_line">, b: Pick<Finding, "file" | "category" | "title" | "start_line" | "end_line">, minTitleSimilarity?: number): boolean;
@@ -0,0 +1,96 @@
1
+ import { createHash } from "node:crypto";
2
+ export const FINDING_ID_MARKER_PREFIX = "<!-- pr-review-id:";
3
+ export const FINDING_ID_RE = /<!--\s*pr-review-id:\s*([a-f0-9]+)\s*-->/i;
4
+ /** Words that add churn without distinguishing the finding. */
5
+ const STOPWORDS = new Set([
6
+ "a", "an", "the", "to", "of", "in", "on", "for", "and", "or", "is", "are", "be",
7
+ "this", "that", "with", "from", "by", "at", "as", "it", "its", "into", "via",
8
+ "when", "which", "who", "whom", "what", "how", "than", "then", "also", "just",
9
+ "not", "no", "can", "could", "should", "would", "may", "might", "will", "does",
10
+ "do", "did", "has", "have", "had", "been", "being", "was", "were", "using", "use",
11
+ "used", "new", "old", "same", "such", "so", "too", "very", "more", "most",
12
+ ]);
13
+ /** Normalize title so minor wording churn still often matches. */
14
+ export function normalizeTitle(title) {
15
+ return titleTokens(title).join(" ");
16
+ }
17
+ /** Significant tokens from a title (lowercase, no stopwords, len ≥ 3). */
18
+ export function titleTokens(title) {
19
+ return title
20
+ .toLowerCase()
21
+ .replace(/[^a-z0-9\s]/g, " ")
22
+ .split(/\s+/)
23
+ .map((t) => t.trim())
24
+ .filter((t) => t.length >= 3 && !STOPWORDS.has(t));
25
+ }
26
+ /**
27
+ * Stable fingerprint: file + category + sorted significant title tokens.
28
+ * Line numbers are excluded (they drift). Token set ignores filler words so
29
+ * "Off-by-one loop reads past end" ≈ "Off-by-one loop includes xs length".
30
+ */
31
+ export function fingerprintFinding(f) {
32
+ const tokens = [...new Set(titleTokens(f.title))].sort();
33
+ const key = `${f.file}|${f.category}|${tokens.join(" ")}`;
34
+ return createHash("sha1").update(key).digest("hex").slice(0, 16);
35
+ }
36
+ export function withFindingId(f) {
37
+ return { ...f, id: f.id ?? fingerprintFinding(f) };
38
+ }
39
+ export function embedFindingId(body, id) {
40
+ if (FINDING_ID_RE.test(body)) {
41
+ return body.replace(FINDING_ID_RE, `${FINDING_ID_MARKER_PREFIX} ${id} -->`);
42
+ }
43
+ return `${body.trimEnd()}\n\n${FINDING_ID_MARKER_PREFIX} ${id} -->\n`;
44
+ }
45
+ export function parseFindingId(body) {
46
+ return FINDING_ID_RE.exec(body)?.[1] ?? null;
47
+ }
48
+ export function collectIdsFromBodies(bodies) {
49
+ const ids = new Set();
50
+ for (const b of bodies) {
51
+ const id = parseFindingId(b);
52
+ if (id)
53
+ ids.add(id);
54
+ }
55
+ return ids;
56
+ }
57
+ /** Jaccard similarity of significant title tokens (0..1). */
58
+ export function titleSimilarity(a, b) {
59
+ const ta = new Set(titleTokens(a));
60
+ const tb = new Set(titleTokens(b));
61
+ if (ta.size === 0 && tb.size === 0)
62
+ return 1;
63
+ if (ta.size === 0 || tb.size === 0)
64
+ return 0;
65
+ let inter = 0;
66
+ for (const t of ta)
67
+ if (tb.has(t))
68
+ inter++;
69
+ return inter / (ta.size + tb.size - inter);
70
+ }
71
+ /** True when line ranges overlap or sit within `slack` lines of each other. */
72
+ export function linesNear(a, b, slack = 3) {
73
+ const a0 = a.start_line - slack;
74
+ const a1 = a.end_line + slack;
75
+ return a0 <= b.end_line && b.start_line <= a1;
76
+ }
77
+ /**
78
+ * Soft match for re-runs where the model rephrases the title.
79
+ * Same file + category + nearby lines + enough title-token overlap.
80
+ */
81
+ export function findingsSoftMatch(a, b, minTitleSimilarity = 0.3) {
82
+ if (a.file !== b.file || a.category !== b.category)
83
+ return false;
84
+ if (!linesNear(a, b))
85
+ return false;
86
+ const ta = new Set(titleTokens(a.title));
87
+ const tb = new Set(titleTokens(b.title));
88
+ let inter = 0;
89
+ for (const t of ta)
90
+ if (tb.has(t))
91
+ inter++;
92
+ if (inter >= 2)
93
+ return true;
94
+ return titleSimilarity(a.title, b.title) >= minTitleSimilarity;
95
+ }
96
+ //# sourceMappingURL=finding-id.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"finding-id.js","sourceRoot":"","sources":["../src/finding-id.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,25 @@
1
+ import type { Octokit } from "@octokit/rest";
2
+ import type { PrRef } from "../diff/github.js";
3
+ import type { Finding, ReviewResult, Severity } from "../types.js";
4
+ export declare const AGENT_MARKER = "<!-- secondpair -->";
5
+ export declare const SEVERITY_EMOJI: Record<Severity, string>;
6
+ export declare function formatCommentBody(f: Finding): string;
7
+ export declare function formatReviewBody(result: ReviewResult, failed: boolean): string;
8
+ export interface PostReviewOptions {
9
+ octokit: Octokit;
10
+ pr: PrRef;
11
+ headSha: string;
12
+ result: ReviewResult;
13
+ failed: boolean;
14
+ log?: (msg: string) => void;
15
+ }
16
+ /** Collect finding ids already posted on this PR by the agent. */
17
+ export declare function listPostedFindingIds(octokit: Octokit, pr: PrRef): Promise<Set<string>>;
18
+ export declare function listWontFixFindingIds(octokit: Octokit, pr: PrRef): Promise<Set<string>>;
19
+ /** Resolve unresolved review threads whose comments contain one of the finding ids. */
20
+ export declare function resolveThreadsForIds(octokit: Octokit, pr: PrRef, ids: Iterable<string>, log?: (msg: string) => void): Promise<number>;
21
+ /**
22
+ * Post new findings as one PR review with inline comments.
23
+ * Persistent findings (same id already on the PR) are skipped.
24
+ */
25
+ export declare function postReview(opts: PostReviewOptions): Promise<void>;