@sentry/warden 0.0.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 (199) hide show
  1. package/.agents/skills/find-bugs/SKILL.md +75 -0
  2. package/.agents/skills/vercel-react-best-practices/AGENTS.md +2934 -0
  3. package/.agents/skills/vercel-react-best-practices/SKILL.md +136 -0
  4. package/.agents/skills/vercel-react-best-practices/rules/advanced-event-handler-refs.md +55 -0
  5. package/.agents/skills/vercel-react-best-practices/rules/advanced-init-once.md +42 -0
  6. package/.agents/skills/vercel-react-best-practices/rules/advanced-use-latest.md +39 -0
  7. package/.agents/skills/vercel-react-best-practices/rules/async-api-routes.md +38 -0
  8. package/.agents/skills/vercel-react-best-practices/rules/async-defer-await.md +80 -0
  9. package/.agents/skills/vercel-react-best-practices/rules/async-dependencies.md +51 -0
  10. package/.agents/skills/vercel-react-best-practices/rules/async-parallel.md +28 -0
  11. package/.agents/skills/vercel-react-best-practices/rules/async-suspense-boundaries.md +99 -0
  12. package/.agents/skills/vercel-react-best-practices/rules/bundle-barrel-imports.md +59 -0
  13. package/.agents/skills/vercel-react-best-practices/rules/bundle-conditional.md +31 -0
  14. package/.agents/skills/vercel-react-best-practices/rules/bundle-defer-third-party.md +49 -0
  15. package/.agents/skills/vercel-react-best-practices/rules/bundle-dynamic-imports.md +35 -0
  16. package/.agents/skills/vercel-react-best-practices/rules/bundle-preload.md +50 -0
  17. package/.agents/skills/vercel-react-best-practices/rules/client-event-listeners.md +74 -0
  18. package/.agents/skills/vercel-react-best-practices/rules/client-localstorage-schema.md +71 -0
  19. package/.agents/skills/vercel-react-best-practices/rules/client-passive-event-listeners.md +48 -0
  20. package/.agents/skills/vercel-react-best-practices/rules/client-swr-dedup.md +56 -0
  21. package/.agents/skills/vercel-react-best-practices/rules/js-batch-dom-css.md +107 -0
  22. package/.agents/skills/vercel-react-best-practices/rules/js-cache-function-results.md +80 -0
  23. package/.agents/skills/vercel-react-best-practices/rules/js-cache-property-access.md +28 -0
  24. package/.agents/skills/vercel-react-best-practices/rules/js-cache-storage.md +70 -0
  25. package/.agents/skills/vercel-react-best-practices/rules/js-combine-iterations.md +32 -0
  26. package/.agents/skills/vercel-react-best-practices/rules/js-early-exit.md +50 -0
  27. package/.agents/skills/vercel-react-best-practices/rules/js-hoist-regexp.md +45 -0
  28. package/.agents/skills/vercel-react-best-practices/rules/js-index-maps.md +37 -0
  29. package/.agents/skills/vercel-react-best-practices/rules/js-length-check-first.md +49 -0
  30. package/.agents/skills/vercel-react-best-practices/rules/js-min-max-loop.md +82 -0
  31. package/.agents/skills/vercel-react-best-practices/rules/js-set-map-lookups.md +24 -0
  32. package/.agents/skills/vercel-react-best-practices/rules/js-tosorted-immutable.md +57 -0
  33. package/.agents/skills/vercel-react-best-practices/rules/rendering-activity.md +26 -0
  34. package/.agents/skills/vercel-react-best-practices/rules/rendering-animate-svg-wrapper.md +47 -0
  35. package/.agents/skills/vercel-react-best-practices/rules/rendering-conditional-render.md +40 -0
  36. package/.agents/skills/vercel-react-best-practices/rules/rendering-content-visibility.md +38 -0
  37. package/.agents/skills/vercel-react-best-practices/rules/rendering-hoist-jsx.md +46 -0
  38. package/.agents/skills/vercel-react-best-practices/rules/rendering-hydration-no-flicker.md +82 -0
  39. package/.agents/skills/vercel-react-best-practices/rules/rendering-hydration-suppress-warning.md +30 -0
  40. package/.agents/skills/vercel-react-best-practices/rules/rendering-svg-precision.md +28 -0
  41. package/.agents/skills/vercel-react-best-practices/rules/rendering-usetransition-loading.md +75 -0
  42. package/.agents/skills/vercel-react-best-practices/rules/rerender-defer-reads.md +39 -0
  43. package/.agents/skills/vercel-react-best-practices/rules/rerender-dependencies.md +45 -0
  44. package/.agents/skills/vercel-react-best-practices/rules/rerender-derived-state-no-effect.md +40 -0
  45. package/.agents/skills/vercel-react-best-practices/rules/rerender-derived-state.md +29 -0
  46. package/.agents/skills/vercel-react-best-practices/rules/rerender-functional-setstate.md +74 -0
  47. package/.agents/skills/vercel-react-best-practices/rules/rerender-lazy-state-init.md +58 -0
  48. package/.agents/skills/vercel-react-best-practices/rules/rerender-memo-with-default-value.md +38 -0
  49. package/.agents/skills/vercel-react-best-practices/rules/rerender-memo.md +44 -0
  50. package/.agents/skills/vercel-react-best-practices/rules/rerender-move-effect-to-event.md +45 -0
  51. package/.agents/skills/vercel-react-best-practices/rules/rerender-simple-expression-in-memo.md +35 -0
  52. package/.agents/skills/vercel-react-best-practices/rules/rerender-transitions.md +40 -0
  53. package/.agents/skills/vercel-react-best-practices/rules/rerender-use-ref-transient-values.md +73 -0
  54. package/.agents/skills/vercel-react-best-practices/rules/server-after-nonblocking.md +73 -0
  55. package/.agents/skills/vercel-react-best-practices/rules/server-auth-actions.md +96 -0
  56. package/.agents/skills/vercel-react-best-practices/rules/server-cache-lru.md +41 -0
  57. package/.agents/skills/vercel-react-best-practices/rules/server-cache-react.md +76 -0
  58. package/.agents/skills/vercel-react-best-practices/rules/server-dedup-props.md +65 -0
  59. package/.agents/skills/vercel-react-best-practices/rules/server-parallel-fetching.md +83 -0
  60. package/.agents/skills/vercel-react-best-practices/rules/server-serialization.md +38 -0
  61. package/.claude/settings.json +57 -0
  62. package/.claude/settings.local.json +88 -0
  63. package/.claude/skills/agent-prompt/SKILL.md +54 -0
  64. package/.claude/skills/agent-prompt/references/agentic-patterns.md +94 -0
  65. package/.claude/skills/agent-prompt/references/anti-patterns.md +140 -0
  66. package/.claude/skills/agent-prompt/references/context-design.md +124 -0
  67. package/.claude/skills/agent-prompt/references/core-principles.md +75 -0
  68. package/.claude/skills/agent-prompt/references/model-guidance.md +118 -0
  69. package/.claude/skills/agent-prompt/references/output-formats.md +98 -0
  70. package/.claude/skills/agent-prompt/references/skill-structure.md +115 -0
  71. package/.claude/skills/agent-prompt/references/system-prompts.md +115 -0
  72. package/.claude/skills/notseer/SKILL.md +131 -0
  73. package/.claude/skills/skill-writer/SKILL.md +140 -0
  74. package/.claude/skills/testing-guidelines/SKILL.md +132 -0
  75. package/.claude/skills/warden-skill/SKILL.md +250 -0
  76. package/.claude/skills/warden-skill/references/config-schema.md +133 -0
  77. package/.dex/config.toml +2 -0
  78. package/.github/workflows/ci.yml +33 -0
  79. package/.github/workflows/release.yml +54 -0
  80. package/.github/workflows/warden.yml +40 -0
  81. package/AGENTS.md +89 -0
  82. package/CONTRIBUTING.md +60 -0
  83. package/LICENSE +105 -0
  84. package/README.md +43 -0
  85. package/SPEC.md +263 -0
  86. package/action.yml +87 -0
  87. package/assets/favicon.png +0 -0
  88. package/assets/warden-icon-bw.svg +5 -0
  89. package/assets/warden-icon-purple.png +0 -0
  90. package/assets/warden-icon-purple.svg +5 -0
  91. package/docs/.claude/settings.local.json +11 -0
  92. package/docs/astro.config.mjs +43 -0
  93. package/docs/package.json +19 -0
  94. package/docs/pnpm-lock.yaml +4000 -0
  95. package/docs/public/favicon.svg +5 -0
  96. package/docs/src/components/Code.astro +141 -0
  97. package/docs/src/components/PackageManagerTabs.astro +183 -0
  98. package/docs/src/components/Terminal.astro +212 -0
  99. package/docs/src/layouts/Base.astro +380 -0
  100. package/docs/src/pages/cli.astro +167 -0
  101. package/docs/src/pages/config.astro +394 -0
  102. package/docs/src/pages/guide.astro +449 -0
  103. package/docs/src/pages/index.astro +490 -0
  104. package/docs/src/styles/global.css +551 -0
  105. package/docs/tsconfig.json +3 -0
  106. package/docs/vercel.json +5 -0
  107. package/eslint.config.js +33 -0
  108. package/package.json +73 -0
  109. package/src/action/index.ts +1 -0
  110. package/src/action/main.ts +868 -0
  111. package/src/cli/args.test.ts +477 -0
  112. package/src/cli/args.ts +415 -0
  113. package/src/cli/commands/add.ts +447 -0
  114. package/src/cli/commands/init.test.ts +136 -0
  115. package/src/cli/commands/init.ts +132 -0
  116. package/src/cli/commands/setup-app/browser.ts +38 -0
  117. package/src/cli/commands/setup-app/credentials.ts +45 -0
  118. package/src/cli/commands/setup-app/manifest.ts +48 -0
  119. package/src/cli/commands/setup-app/server.ts +172 -0
  120. package/src/cli/commands/setup-app.ts +156 -0
  121. package/src/cli/commands/sync.ts +114 -0
  122. package/src/cli/context.ts +131 -0
  123. package/src/cli/files.test.ts +155 -0
  124. package/src/cli/files.ts +89 -0
  125. package/src/cli/fix.test.ts +310 -0
  126. package/src/cli/fix.ts +387 -0
  127. package/src/cli/git.test.ts +119 -0
  128. package/src/cli/git.ts +318 -0
  129. package/src/cli/index.ts +14 -0
  130. package/src/cli/main.ts +672 -0
  131. package/src/cli/output/box.ts +235 -0
  132. package/src/cli/output/formatters.test.ts +187 -0
  133. package/src/cli/output/formatters.ts +269 -0
  134. package/src/cli/output/icons.ts +13 -0
  135. package/src/cli/output/index.ts +44 -0
  136. package/src/cli/output/ink-runner.tsx +337 -0
  137. package/src/cli/output/jsonl.test.ts +347 -0
  138. package/src/cli/output/jsonl.ts +126 -0
  139. package/src/cli/output/reporter.ts +435 -0
  140. package/src/cli/output/tasks.ts +374 -0
  141. package/src/cli/output/tty.test.ts +117 -0
  142. package/src/cli/output/tty.ts +60 -0
  143. package/src/cli/output/verbosity.test.ts +40 -0
  144. package/src/cli/output/verbosity.ts +31 -0
  145. package/src/cli/terminal.test.ts +148 -0
  146. package/src/cli/terminal.ts +301 -0
  147. package/src/config/index.ts +3 -0
  148. package/src/config/loader.test.ts +313 -0
  149. package/src/config/loader.ts +103 -0
  150. package/src/config/schema.ts +168 -0
  151. package/src/config/writer.test.ts +119 -0
  152. package/src/config/writer.ts +84 -0
  153. package/src/diff/classify.test.ts +162 -0
  154. package/src/diff/classify.ts +92 -0
  155. package/src/diff/coalesce.test.ts +208 -0
  156. package/src/diff/coalesce.ts +133 -0
  157. package/src/diff/context.test.ts +226 -0
  158. package/src/diff/context.ts +201 -0
  159. package/src/diff/index.ts +4 -0
  160. package/src/diff/parser.test.ts +212 -0
  161. package/src/diff/parser.ts +149 -0
  162. package/src/event/context.ts +132 -0
  163. package/src/event/index.ts +2 -0
  164. package/src/event/schedule-context.ts +101 -0
  165. package/src/examples/examples.integration.test.ts +66 -0
  166. package/src/examples/index.test.ts +101 -0
  167. package/src/examples/index.ts +122 -0
  168. package/src/examples/setup.ts +25 -0
  169. package/src/index.ts +115 -0
  170. package/src/output/dedup.test.ts +419 -0
  171. package/src/output/dedup.ts +607 -0
  172. package/src/output/github-checks.test.ts +300 -0
  173. package/src/output/github-checks.ts +476 -0
  174. package/src/output/github-issues.ts +329 -0
  175. package/src/output/index.ts +5 -0
  176. package/src/output/issue-renderer.ts +197 -0
  177. package/src/output/renderer.test.ts +727 -0
  178. package/src/output/renderer.ts +217 -0
  179. package/src/output/stale.test.ts +375 -0
  180. package/src/output/stale.ts +155 -0
  181. package/src/output/types.ts +34 -0
  182. package/src/sdk/index.ts +1 -0
  183. package/src/sdk/runner.test.ts +806 -0
  184. package/src/sdk/runner.ts +1232 -0
  185. package/src/skills/index.ts +36 -0
  186. package/src/skills/loader.test.ts +300 -0
  187. package/src/skills/loader.ts +423 -0
  188. package/src/skills/remote.test.ts +704 -0
  189. package/src/skills/remote.ts +604 -0
  190. package/src/triggers/matcher.test.ts +277 -0
  191. package/src/triggers/matcher.ts +152 -0
  192. package/src/types/index.ts +194 -0
  193. package/src/utils/async.ts +18 -0
  194. package/src/utils/index.test.ts +84 -0
  195. package/src/utils/index.ts +50 -0
  196. package/tsconfig.json +25 -0
  197. package/vitest.config.ts +8 -0
  198. package/vitest.integration.config.ts +11 -0
  199. package/warden.toml +19 -0
@@ -0,0 +1,226 @@
1
+ import { describe, it, expect, beforeEach, afterEach } from 'vitest';
2
+ import { mkdirSync, writeFileSync, rmSync } from 'node:fs';
3
+ import { join } from 'node:path';
4
+ import { tmpdir } from 'node:os';
5
+ import {
6
+ expandHunkContext,
7
+ expandDiffContext,
8
+ formatHunkForAnalysis,
9
+ } from './context.js';
10
+ import type { DiffHunk, ParsedDiff } from './parser.js';
11
+
12
+ describe('expandHunkContext', () => {
13
+ let tempDir: string;
14
+
15
+ beforeEach(() => {
16
+ tempDir = join(tmpdir(), `warden-test-${Date.now()}-${Math.random().toString(36).slice(2)}`);
17
+ mkdirSync(tempDir, { recursive: true });
18
+ });
19
+
20
+ afterEach(() => {
21
+ rmSync(tempDir, { recursive: true, force: true });
22
+ });
23
+
24
+ it('expands hunk with surrounding file context', () => {
25
+ const fileContent = Array.from({ length: 50 }, (_, i) => `line ${i + 1}`).join('\n');
26
+ writeFileSync(join(tempDir, 'test.ts'), fileContent);
27
+
28
+ const hunk: DiffHunk = {
29
+ oldStart: 20,
30
+ oldCount: 3,
31
+ newStart: 20,
32
+ newCount: 4,
33
+ content: '@@ -20,3 +20,4 @@\n line 20\n+added\n line 21\n line 22',
34
+ lines: [' line 20', '+added', ' line 21', ' line 22'],
35
+ };
36
+
37
+ const result = expandHunkContext(tempDir, 'test.ts', hunk, 5);
38
+
39
+ expect(result.filename).toBe('test.ts');
40
+ expect(result.hunk).toBe(hunk);
41
+ expect(result.language).toBe('typescript');
42
+ expect(result.contextBefore).toHaveLength(5); // lines 15-19
43
+ expect(result.contextBefore[0]).toBe('line 15');
44
+ expect(result.contextAfter).toHaveLength(5); // lines 24-28
45
+ expect(result.contextAfter[0]).toBe('line 24');
46
+ expect(result.contextStartLine).toBe(15);
47
+ });
48
+
49
+ it('handles missing file gracefully', () => {
50
+ const hunk: DiffHunk = {
51
+ oldStart: 1,
52
+ oldCount: 2,
53
+ newStart: 1,
54
+ newCount: 2,
55
+ content: '@@ -1,2 +1,2 @@',
56
+ lines: [],
57
+ };
58
+
59
+ const result = expandHunkContext(tempDir, 'nonexistent.ts', hunk);
60
+
61
+ expect(result.contextBefore).toEqual([]);
62
+ expect(result.contextAfter).toEqual([]);
63
+ });
64
+
65
+ it('detects language from file extension', () => {
66
+ writeFileSync(join(tempDir, 'test.py'), 'print("hello")');
67
+
68
+ const hunk: DiffHunk = {
69
+ oldStart: 1,
70
+ oldCount: 1,
71
+ newStart: 1,
72
+ newCount: 1,
73
+ content: '@@ -1 +1 @@',
74
+ lines: [],
75
+ };
76
+
77
+ const result = expandHunkContext(tempDir, 'test.py', hunk);
78
+ expect(result.language).toBe('python');
79
+ });
80
+
81
+ it('handles files at start with limited context before', () => {
82
+ const fileContent = 'line 1\nline 2\nline 3\nline 4\nline 5';
83
+ writeFileSync(join(tempDir, 'start.ts'), fileContent);
84
+
85
+ const hunk: DiffHunk = {
86
+ oldStart: 1,
87
+ oldCount: 2,
88
+ newStart: 1,
89
+ newCount: 3,
90
+ content: '@@ -1,2 +1,3 @@',
91
+ lines: [],
92
+ };
93
+
94
+ const result = expandHunkContext(tempDir, 'start.ts', hunk, 10);
95
+ expect(result.contextBefore).toEqual([]);
96
+ expect(result.contextStartLine).toBe(1);
97
+ });
98
+ });
99
+
100
+ describe('expandDiffContext', () => {
101
+ let tempDir: string;
102
+
103
+ beforeEach(() => {
104
+ tempDir = join(tmpdir(), `warden-test-${Date.now()}-${Math.random().toString(36).slice(2)}`);
105
+ mkdirSync(tempDir, { recursive: true });
106
+ });
107
+
108
+ afterEach(() => {
109
+ rmSync(tempDir, { recursive: true, force: true });
110
+ });
111
+
112
+ it('expands all hunks in a parsed diff', () => {
113
+ const fileContent = Array.from({ length: 100 }, (_, i) => `line ${i + 1}`).join('\n');
114
+ writeFileSync(join(tempDir, 'multi.ts'), fileContent);
115
+
116
+ const diff: ParsedDiff = {
117
+ filename: 'multi.ts',
118
+ status: 'modified',
119
+ hunks: [
120
+ {
121
+ oldStart: 10,
122
+ oldCount: 2,
123
+ newStart: 10,
124
+ newCount: 3,
125
+ content: '@@ -10,2 +10,3 @@',
126
+ lines: [],
127
+ },
128
+ {
129
+ oldStart: 50,
130
+ oldCount: 2,
131
+ newStart: 51,
132
+ newCount: 3,
133
+ content: '@@ -50,2 +51,3 @@',
134
+ lines: [],
135
+ },
136
+ ],
137
+ rawPatch: '',
138
+ };
139
+
140
+ const results = expandDiffContext(tempDir, diff, 3);
141
+
142
+ expect(results).toHaveLength(2);
143
+ expect(results[0]!.hunk.newStart).toBe(10);
144
+ expect(results[1]!.hunk.newStart).toBe(51);
145
+ });
146
+ });
147
+
148
+ describe('formatHunkForAnalysis', () => {
149
+ it('formats hunk with all sections', () => {
150
+ const hunkCtx = {
151
+ filename: 'src/index.ts',
152
+ hunk: {
153
+ oldStart: 10,
154
+ oldCount: 2,
155
+ newStart: 10,
156
+ newCount: 3,
157
+ header: 'function example()',
158
+ content: '@@ -10,2 +10,3 @@ function example()\n const x = 1;\n+const y = 2;\n return x;',
159
+ lines: [' const x = 1;', '+const y = 2;', ' return x;'],
160
+ },
161
+ contextBefore: ['// before line 1', '// before line 2'],
162
+ contextAfter: ['// after line 1'],
163
+ contextStartLine: 8,
164
+ language: 'typescript',
165
+ };
166
+
167
+ const output = formatHunkForAnalysis(hunkCtx);
168
+
169
+ expect(output).toContain('## File: src/index.ts');
170
+ expect(output).toContain('## Language: typescript');
171
+ expect(output).toContain('## Hunk: lines 10-12');
172
+ expect(output).toContain('## Scope: function example()');
173
+ expect(output).toContain('### Context Before (lines 8-9)');
174
+ expect(output).toContain('```typescript');
175
+ expect(output).toContain('// before line 1');
176
+ expect(output).toContain('### Changes');
177
+ expect(output).toContain('```diff');
178
+ expect(output).toContain('### Context After (lines 13-13)');
179
+ });
180
+
181
+ it('omits context sections when empty', () => {
182
+ const hunkCtx = {
183
+ filename: 'new.ts',
184
+ hunk: {
185
+ oldStart: 0,
186
+ oldCount: 0,
187
+ newStart: 1,
188
+ newCount: 2,
189
+ content: '@@ -0,0 +1,2 @@\n+line 1\n+line 2',
190
+ lines: ['+line 1', '+line 2'],
191
+ },
192
+ contextBefore: [],
193
+ contextAfter: [],
194
+ contextStartLine: 1,
195
+ language: 'typescript',
196
+ };
197
+
198
+ const output = formatHunkForAnalysis(hunkCtx);
199
+
200
+ expect(output).not.toContain('### Context Before');
201
+ expect(output).not.toContain('### Context After');
202
+ expect(output).toContain('### Changes');
203
+ });
204
+
205
+ it('omits scope when no header', () => {
206
+ const hunkCtx = {
207
+ filename: 'test.ts',
208
+ hunk: {
209
+ oldStart: 1,
210
+ oldCount: 1,
211
+ newStart: 1,
212
+ newCount: 1,
213
+ content: '@@ -1 +1 @@',
214
+ lines: [],
215
+ },
216
+ contextBefore: [],
217
+ contextAfter: [],
218
+ contextStartLine: 1,
219
+ language: 'typescript',
220
+ };
221
+
222
+ const output = formatHunkForAnalysis(hunkCtx);
223
+
224
+ expect(output).not.toContain('## Scope:');
225
+ });
226
+ });
@@ -0,0 +1,201 @@
1
+ import { readFileSync, existsSync } from 'node:fs';
2
+ import { join } from 'node:path';
3
+ import type { DiffHunk, ParsedDiff } from './parser.js';
4
+ import { getExpandedLineRange } from './parser.js';
5
+
6
+ /** Cache for file contents to avoid repeated reads */
7
+ const fileCache = new Map<string, string[] | null>();
8
+
9
+ /** Clear the file cache (useful for testing or long-running processes) */
10
+ export function clearFileCache(): void {
11
+ fileCache.clear();
12
+ }
13
+
14
+ /** Get cached file lines or read and cache them */
15
+ function getCachedFileLines(filePath: string): string[] | null {
16
+ if (fileCache.has(filePath)) {
17
+ return fileCache.get(filePath) ?? null;
18
+ }
19
+
20
+ if (!existsSync(filePath)) {
21
+ fileCache.set(filePath, null);
22
+ return null;
23
+ }
24
+
25
+ try {
26
+ const content = readFileSync(filePath, 'utf-8');
27
+ const lines = content.split('\n');
28
+ fileCache.set(filePath, lines);
29
+ return lines;
30
+ } catch {
31
+ // Binary file or read error
32
+ fileCache.set(filePath, null);
33
+ return null;
34
+ }
35
+ }
36
+
37
+ export interface HunkWithContext {
38
+ /** File path */
39
+ filename: string;
40
+ /** The hunk being analyzed */
41
+ hunk: DiffHunk;
42
+ /** Lines before the hunk (from actual file) */
43
+ contextBefore: string[];
44
+ /** Lines after the hunk (from actual file) */
45
+ contextAfter: string[];
46
+ /** Start line of contextBefore */
47
+ contextStartLine: number;
48
+ /** Detected language from file extension */
49
+ language: string;
50
+ }
51
+
52
+ /**
53
+ * Detect language from filename.
54
+ */
55
+ function detectLanguage(filename: string): string {
56
+ const ext = filename.split('.').pop()?.toLowerCase() ?? '';
57
+ const languageMap: Record<string, string> = {
58
+ ts: 'typescript',
59
+ tsx: 'typescript',
60
+ js: 'javascript',
61
+ jsx: 'javascript',
62
+ py: 'python',
63
+ rb: 'ruby',
64
+ go: 'go',
65
+ rs: 'rust',
66
+ java: 'java',
67
+ kt: 'kotlin',
68
+ cs: 'csharp',
69
+ cpp: 'cpp',
70
+ c: 'c',
71
+ h: 'c',
72
+ hpp: 'cpp',
73
+ swift: 'swift',
74
+ php: 'php',
75
+ sh: 'bash',
76
+ bash: 'bash',
77
+ zsh: 'bash',
78
+ yml: 'yaml',
79
+ yaml: 'yaml',
80
+ json: 'json',
81
+ toml: 'toml',
82
+ md: 'markdown',
83
+ sql: 'sql',
84
+ html: 'html',
85
+ css: 'css',
86
+ scss: 'scss',
87
+ less: 'less',
88
+ };
89
+ return languageMap[ext] ?? ext;
90
+ }
91
+
92
+ /**
93
+ * Read specific lines from a file using the cache.
94
+ * Returns empty array if file doesn't exist or is binary.
95
+ */
96
+ function readFileLines(
97
+ filePath: string,
98
+ startLine: number,
99
+ endLine: number
100
+ ): string[] {
101
+ const lines = getCachedFileLines(filePath);
102
+ if (!lines) {
103
+ return [];
104
+ }
105
+ // Lines are 1-indexed, arrays are 0-indexed
106
+ return lines.slice(startLine - 1, endLine);
107
+ }
108
+
109
+ /**
110
+ * Expand a hunk with surrounding context from the actual file.
111
+ */
112
+ export function expandHunkContext(
113
+ repoPath: string,
114
+ filename: string,
115
+ hunk: DiffHunk,
116
+ contextLines = 20
117
+ ): HunkWithContext {
118
+ const filePath = join(repoPath, filename);
119
+ const expandedRange = getExpandedLineRange(hunk, contextLines);
120
+
121
+ // Read context before the hunk
122
+ const contextBefore = readFileLines(
123
+ filePath,
124
+ expandedRange.start,
125
+ hunk.newStart - 1
126
+ );
127
+
128
+ // Read context after the hunk
129
+ const contextAfter = readFileLines(
130
+ filePath,
131
+ hunk.newStart + hunk.newCount,
132
+ expandedRange.end
133
+ );
134
+
135
+ return {
136
+ filename,
137
+ hunk,
138
+ contextBefore,
139
+ contextAfter,
140
+ contextStartLine: expandedRange.start,
141
+ language: detectLanguage(filename),
142
+ };
143
+ }
144
+
145
+ /**
146
+ * Expand all hunks in a parsed diff with context.
147
+ */
148
+ export function expandDiffContext(
149
+ repoPath: string,
150
+ diff: ParsedDiff,
151
+ contextLines = 20
152
+ ): HunkWithContext[] {
153
+ return diff.hunks.map((hunk) =>
154
+ expandHunkContext(repoPath, diff.filename, hunk, contextLines)
155
+ );
156
+ }
157
+
158
+ /**
159
+ * Format a hunk with context for LLM analysis.
160
+ */
161
+ export function formatHunkForAnalysis(hunkCtx: HunkWithContext): string {
162
+ const lines: string[] = [];
163
+
164
+ lines.push(`## File: ${hunkCtx.filename}`);
165
+ lines.push(`## Language: ${hunkCtx.language}`);
166
+ lines.push(`## Hunk: lines ${hunkCtx.hunk.newStart}-${hunkCtx.hunk.newStart + hunkCtx.hunk.newCount - 1}`);
167
+
168
+ if (hunkCtx.hunk.header) {
169
+ lines.push(`## Scope: ${hunkCtx.hunk.header}`);
170
+ }
171
+
172
+ lines.push('');
173
+
174
+ // Context before
175
+ if (hunkCtx.contextBefore.length > 0) {
176
+ lines.push(`### Context Before (lines ${hunkCtx.contextStartLine}-${hunkCtx.hunk.newStart - 1})`);
177
+ lines.push('```' + hunkCtx.language);
178
+ lines.push(hunkCtx.contextBefore.join('\n'));
179
+ lines.push('```');
180
+ lines.push('');
181
+ }
182
+
183
+ // The actual changes
184
+ lines.push(`### Changes`);
185
+ lines.push('```diff');
186
+ lines.push(hunkCtx.hunk.content);
187
+ lines.push('```');
188
+ lines.push('');
189
+
190
+ // Context after
191
+ if (hunkCtx.contextAfter.length > 0) {
192
+ const afterStart = hunkCtx.hunk.newStart + hunkCtx.hunk.newCount;
193
+ const afterEnd = afterStart + hunkCtx.contextAfter.length - 1;
194
+ lines.push(`### Context After (lines ${afterStart}-${afterEnd})`);
195
+ lines.push('```' + hunkCtx.language);
196
+ lines.push(hunkCtx.contextAfter.join('\n'));
197
+ lines.push('```');
198
+ }
199
+
200
+ return lines.join('\n');
201
+ }
@@ -0,0 +1,4 @@
1
+ export * from './parser.js';
2
+ export * from './context.js';
3
+ export * from './classify.js';
4
+ export * from './coalesce.js';
@@ -0,0 +1,212 @@
1
+ import { describe, it, expect } from 'vitest';
2
+ import {
3
+ parsePatch,
4
+ parseFileDiff,
5
+ getHunkLineRange,
6
+ getExpandedLineRange,
7
+ type DiffHunk,
8
+ } from './parser.js';
9
+
10
+ describe('parsePatch', () => {
11
+ it('parses a simple hunk', () => {
12
+ const patch = `@@ -1,3 +1,4 @@
13
+ line 1
14
+ +added line
15
+ line 2
16
+ line 3`;
17
+
18
+ const hunks = parsePatch(patch);
19
+ expect(hunks).toHaveLength(1);
20
+ const hunk = hunks[0]!;
21
+ expect(hunk.oldStart).toBe(1);
22
+ expect(hunk.oldCount).toBe(3);
23
+ expect(hunk.newStart).toBe(1);
24
+ expect(hunk.newCount).toBe(4);
25
+ expect(hunk.lines).toEqual([' line 1', '+added line', ' line 2', ' line 3']);
26
+ });
27
+
28
+ it('parses multiple hunks', () => {
29
+ const patch = `@@ -1,3 +1,4 @@
30
+ line 1
31
+ +added line
32
+ line 2
33
+ @@ -10,2 +11,3 @@
34
+ line 10
35
+ +another added
36
+ line 11`;
37
+
38
+ const hunks = parsePatch(patch);
39
+ expect(hunks).toHaveLength(2);
40
+ expect(hunks[0]!.newStart).toBe(1);
41
+ expect(hunks[1]!.newStart).toBe(11);
42
+ });
43
+
44
+ it('parses hunk with function header', () => {
45
+ const patch = `@@ -5,3 +5,4 @@ function example()
46
+ const x = 1;
47
+ +const y = 2;
48
+ return x;`;
49
+
50
+ const hunks = parsePatch(patch);
51
+ expect(hunks).toHaveLength(1);
52
+ expect(hunks[0]!.header).toBe('function example()');
53
+ });
54
+
55
+ it('handles hunk without count (single line)', () => {
56
+ const patch = `@@ -1 +1,2 @@
57
+ existing
58
+ +added`;
59
+
60
+ const hunks = parsePatch(patch);
61
+ expect(hunks).toHaveLength(1);
62
+ const hunk = hunks[0]!;
63
+ expect(hunk.oldCount).toBe(1);
64
+ expect(hunk.newCount).toBe(2);
65
+ });
66
+
67
+ it('skips diff metadata lines', () => {
68
+ const patch = `diff --git a/file.ts b/file.ts
69
+ index abc123..def456 100644
70
+ --- a/file.ts
71
+ +++ b/file.ts
72
+ @@ -1,2 +1,3 @@
73
+ line 1
74
+ +added
75
+ line 2`;
76
+
77
+ const hunks = parsePatch(patch);
78
+ expect(hunks).toHaveLength(1);
79
+ const hunk = hunks[0]!;
80
+ expect(hunk.lines).toEqual([' line 1', '+added', ' line 2']);
81
+ expect(hunk.content).not.toContain('diff --git');
82
+ expect(hunk.content).not.toContain('index ');
83
+ });
84
+
85
+ it('handles "No newline at end of file" marker', () => {
86
+ const patch = `@@ -1,2 +1,2 @@
87
+ line 1
88
+ -old line
89
+ +new line
90
+ \`;
91
+
92
+ const hunks = parsePatch(patch);
93
+ expect(hunks).toHaveLength(1);
94
+ expect(hunks[0]!.lines).not.toContain('\');
95
+ });
96
+
97
+ it('returns empty array for empty patch', () => {
98
+ expect(parsePatch('')).toEqual([]);
99
+ });
100
+
101
+ it('returns empty array for patch with only metadata', () => {
102
+ const patch = `diff --git a/file.ts b/file.ts
103
+ index abc123..def456 100644
104
+ --- a/file.ts
105
+ +++ b/file.ts`;
106
+
107
+ expect(parsePatch(patch)).toEqual([]);
108
+ });
109
+ });
110
+
111
+ describe('parseFileDiff', () => {
112
+ it('creates parsed diff with correct structure', () => {
113
+ const patch = `@@ -1,2 +1,3 @@
114
+ line 1
115
+ +added
116
+ line 2`;
117
+
118
+ const diff = parseFileDiff('src/index.ts', patch);
119
+ expect(diff.filename).toBe('src/index.ts');
120
+ expect(diff.status).toBe('modified');
121
+ expect(diff.hunks).toHaveLength(1);
122
+ expect(diff.rawPatch).toBe(patch);
123
+ });
124
+
125
+ it('accepts different status values', () => {
126
+ const patch = `@@ -0,0 +1,2 @@
127
+ +new file
128
+ +content`;
129
+
130
+ expect(parseFileDiff('new.ts', patch, 'added').status).toBe('added');
131
+ expect(parseFileDiff('old.ts', patch, 'removed').status).toBe('removed');
132
+ expect(parseFileDiff('moved.ts', patch, 'renamed').status).toBe('renamed');
133
+ });
134
+ });
135
+
136
+ describe('getHunkLineRange', () => {
137
+ it('returns correct range for hunk', () => {
138
+ const hunk: DiffHunk = {
139
+ oldStart: 1,
140
+ oldCount: 3,
141
+ newStart: 5,
142
+ newCount: 10,
143
+ content: '',
144
+ lines: [],
145
+ };
146
+
147
+ const range = getHunkLineRange(hunk);
148
+ expect(range.start).toBe(5);
149
+ expect(range.end).toBe(14); // 5 + 10 - 1
150
+ });
151
+
152
+ it('handles single line hunk', () => {
153
+ const hunk: DiffHunk = {
154
+ oldStart: 1,
155
+ oldCount: 1,
156
+ newStart: 1,
157
+ newCount: 1,
158
+ content: '',
159
+ lines: [],
160
+ };
161
+
162
+ const range = getHunkLineRange(hunk);
163
+ expect(range.start).toBe(1);
164
+ expect(range.end).toBe(1);
165
+ });
166
+ });
167
+
168
+ describe('getExpandedLineRange', () => {
169
+ it('expands range with default context', () => {
170
+ const hunk: DiffHunk = {
171
+ oldStart: 1,
172
+ oldCount: 3,
173
+ newStart: 50,
174
+ newCount: 5,
175
+ content: '',
176
+ lines: [],
177
+ };
178
+
179
+ const range = getExpandedLineRange(hunk);
180
+ expect(range.start).toBe(30); // 50 - 20
181
+ expect(range.end).toBe(74); // 54 + 20
182
+ });
183
+
184
+ it('does not go below line 1', () => {
185
+ const hunk: DiffHunk = {
186
+ oldStart: 1,
187
+ oldCount: 2,
188
+ newStart: 5,
189
+ newCount: 3,
190
+ content: '',
191
+ lines: [],
192
+ };
193
+
194
+ const range = getExpandedLineRange(hunk);
195
+ expect(range.start).toBe(1); // Math.max(1, 5 - 20)
196
+ });
197
+
198
+ it('accepts custom context lines', () => {
199
+ const hunk: DiffHunk = {
200
+ oldStart: 1,
201
+ oldCount: 2,
202
+ newStart: 100,
203
+ newCount: 5,
204
+ content: '',
205
+ lines: [],
206
+ };
207
+
208
+ const range = getExpandedLineRange(hunk, 50);
209
+ expect(range.start).toBe(50); // 100 - 50
210
+ expect(range.end).toBe(154); // 104 + 50
211
+ });
212
+ });