harness-auto-docs 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 (67) hide show
  1. package/.nvmrc +1 -0
  2. package/AGENTS.md +69 -0
  3. package/ARCHITECTURE.md +123 -0
  4. package/README.md +52 -0
  5. package/dist/ai/anthropic.d.ts +7 -0
  6. package/dist/ai/anthropic.js +20 -0
  7. package/dist/ai/interface.d.ts +3 -0
  8. package/dist/ai/interface.js +1 -0
  9. package/dist/ai/minimax.d.ts +7 -0
  10. package/dist/ai/minimax.js +21 -0
  11. package/dist/ai/openai.d.ts +7 -0
  12. package/dist/ai/openai.js +16 -0
  13. package/dist/cli.d.ts +2 -0
  14. package/dist/cli.js +103 -0
  15. package/dist/core/diff.d.ts +17 -0
  16. package/dist/core/diff.js +46 -0
  17. package/dist/core/generator.d.ts +10 -0
  18. package/dist/core/generator.js +238 -0
  19. package/dist/core/relevance.d.ts +3 -0
  20. package/dist/core/relevance.js +29 -0
  21. package/dist/core/writer.d.ts +2 -0
  22. package/dist/core/writer.js +23 -0
  23. package/dist/providers/github.d.ts +13 -0
  24. package/dist/providers/github.js +43 -0
  25. package/dist/providers/gitlab.d.ts +9 -0
  26. package/dist/providers/gitlab.js +6 -0
  27. package/dist/providers/interface.d.ts +8 -0
  28. package/dist/providers/interface.js +1 -0
  29. package/docs/DESIGN.md +94 -0
  30. package/docs/QUALITY_SCORE.md +74 -0
  31. package/docs/design-docs/core-beliefs.md +71 -0
  32. package/docs/design-docs/index.md +32 -0
  33. package/docs/exec-plans/tech-debt-tracker.md +26 -0
  34. package/docs/product-specs/index.md +39 -0
  35. package/docs/references/anthropic-sdk-llms.txt +40 -0
  36. package/docs/references/octokit-rest-llms.txt +44 -0
  37. package/docs/references/openai-sdk-llms.txt +38 -0
  38. package/docs/superpowers/plans/2026-04-03-harness-engineering-auto-docs.md +1863 -0
  39. package/docs/superpowers/specs/2026-04-03-harness-engineering-auto-docs-design.md +169 -0
  40. package/examples/github-workflow.yml +32 -0
  41. package/markdown/harness-engineering-codex-agent-first-world.md +215 -0
  42. package/package.json +30 -0
  43. package/src/ai/anthropic.ts +23 -0
  44. package/src/ai/interface.ts +3 -0
  45. package/src/ai/minimax.ts +25 -0
  46. package/src/ai/openai.ts +20 -0
  47. package/src/cli.ts +122 -0
  48. package/src/core/diff.ts +77 -0
  49. package/src/core/generator.ts +294 -0
  50. package/src/core/relevance.ts +53 -0
  51. package/src/core/writer.ts +25 -0
  52. package/src/providers/github.ts +53 -0
  53. package/src/providers/gitlab.ts +16 -0
  54. package/src/providers/interface.ts +9 -0
  55. package/tests/core/anthropic.test.ts +33 -0
  56. package/tests/core/diff.test.ts +49 -0
  57. package/tests/core/generator.test.ts +93 -0
  58. package/tests/core/openai.test.ts +38 -0
  59. package/tests/core/relevance.test.ts +62 -0
  60. package/tests/core/writer.test.ts +56 -0
  61. package/tests/fixtures/diff-frontend.txt +11 -0
  62. package/tests/fixtures/diff-schema.txt +12 -0
  63. package/tests/fixtures/diff-small.txt +16 -0
  64. package/tests/integration/generate.test.ts +49 -0
  65. package/tests/providers/github.test.ts +69 -0
  66. package/tsconfig.json +15 -0
  67. package/vitest.config.ts +7 -0
@@ -0,0 +1,77 @@
1
+ // src/core/diff.ts
2
+ import { execSync } from 'child_process';
3
+
4
+ export interface FileGroups {
5
+ frontend: string[];
6
+ schema: string[];
7
+ auth: string[];
8
+ infra: string[];
9
+ other: string[];
10
+ }
11
+
12
+ export interface DiffResult {
13
+ raw: string;
14
+ prevTag: string;
15
+ currentTag: string;
16
+ changedFiles: string[];
17
+ fileGroups: FileGroups;
18
+ }
19
+
20
+ export function extractDiff(): DiffResult {
21
+ const tags = execSync('git tag --sort=version:refname')
22
+ .toString()
23
+ .trim()
24
+ .split('\n')
25
+ .filter(Boolean);
26
+
27
+ if (tags.length === 0) throw new Error('No tags found in repository');
28
+
29
+ const currentTag = tags[tags.length - 1];
30
+ const prevTag = tags.length >= 2 ? tags[tags.length - 2] : null;
31
+
32
+ const raw = prevTag
33
+ ? execSync(`git diff ${prevTag} ${currentTag}`).toString()
34
+ : execSync(`git show ${currentTag} --format='' -p`).toString();
35
+
36
+ const changedFiles = parseChangedFiles(raw);
37
+ const fileGroups = groupFiles(changedFiles);
38
+
39
+ return {
40
+ raw,
41
+ prevTag: prevTag ?? '(initial)',
42
+ currentTag,
43
+ changedFiles,
44
+ fileGroups,
45
+ };
46
+ }
47
+
48
+ export function parseChangedFiles(diff: string): string[] {
49
+ const matches = diff.match(/^diff --git a\/.+ b\/(.+)$/gm) ?? [];
50
+ return matches.map(line => {
51
+ const match = line.match(/^diff --git a\/.+ b\/(.+)$/);
52
+ return match ? match[1] : '';
53
+ }).filter(Boolean);
54
+ }
55
+
56
+ export function groupFiles(files: string[]): FileGroups {
57
+ const isFrontend = (f: string) =>
58
+ /\.(tsx?|jsx?|css|scss|html|vue|svelte)$/.test(f) ||
59
+ /\b(ui|frontend|components|pages|views)\b/.test(f);
60
+
61
+ const isSchema = (f: string) =>
62
+ /\.sql$/.test(f) || /\b(schema|migration|migrate)\b/i.test(f);
63
+
64
+ const isAuth = (f: string) =>
65
+ /\b(auth|permission|security|oauth|jwt|session|token)\b/i.test(f);
66
+
67
+ const isInfra = (f: string) =>
68
+ /\b(infra|deploy|k8s|docker|compose|helm|terraform|service|gateway)\b/i.test(f);
69
+
70
+ return {
71
+ frontend: files.filter(isFrontend),
72
+ schema: files.filter(isSchema),
73
+ auth: files.filter(isAuth),
74
+ infra: files.filter(isInfra),
75
+ other: files.filter(f => !isFrontend(f) && !isSchema(f) && !isAuth(f) && !isInfra(f)),
76
+ };
77
+ }
@@ -0,0 +1,294 @@
1
+ // src/core/generator.ts
2
+ import type { AIProvider } from '../ai/interface.js';
3
+ import type { DocTarget } from './relevance.js';
4
+ import type { DiffResult } from './diff.js';
5
+ import { appendSection, createFile } from './writer.js';
6
+
7
+ type PromptFn = (diff: DiffResult) => string;
8
+
9
+ const PROMPTS: Record<DocTarget, PromptFn> = {
10
+ 'AGENTS.md': (diff) =>
11
+ `You are a technical writer following Harness Engineering documentation style.
12
+
13
+ Based on the git diff between ${diff.prevTag} and ${diff.currentTag}, write a section for AGENTS.md.
14
+ Describe what AI coding agents need to know: new modules, interfaces, APIs, navigation patterns, new conventions.
15
+ Write in present tense. Be specific and actionable. 2-4 paragraphs max.
16
+
17
+ Git diff:
18
+ ${diff.raw.slice(0, 8000)}`,
19
+
20
+ 'ARCHITECTURE.md': (diff) =>
21
+ `You are a technical writer following Harness Engineering documentation style.
22
+
23
+ Based on the git diff between ${diff.prevTag} and ${diff.currentTag}, write a section for ARCHITECTURE.md.
24
+ Focus on: new layers, modules, dependency changes, new abstractions, removed or restructured components.
25
+ Write in present tense. 2-3 paragraphs max.
26
+
27
+ Git diff:
28
+ ${diff.raw.slice(0, 8000)}`,
29
+
30
+ 'DESIGN.md': (diff) =>
31
+ `You are a technical writer following Harness Engineering documentation style.
32
+
33
+ Based on the git diff between ${diff.prevTag} and ${diff.currentTag}, write a section for docs/DESIGN.md.
34
+ Focus on key design decisions, why certain approaches were chosen, trade-offs made.
35
+ Write in present tense. 2-3 paragraphs max.
36
+
37
+ Git diff:
38
+ ${diff.raw.slice(0, 8000)}`,
39
+
40
+ 'FRONTEND.md': (diff) =>
41
+ `You are a technical writer following Harness Engineering documentation style.
42
+
43
+ Based on the git diff between ${diff.prevTag} and ${diff.currentTag}, write a section for FRONTEND.md.
44
+ Focus on new components, UI patterns, styling changes, frontend architecture changes.
45
+ Write in present tense. 2-3 paragraphs max.
46
+
47
+ Git diff:
48
+ ${diff.raw.slice(0, 8000)}`,
49
+
50
+ 'SECURITY.md': (diff) =>
51
+ `You are a technical writer following Harness Engineering documentation style.
52
+
53
+ Based on the git diff between ${diff.prevTag} and ${diff.currentTag}, write a section for SECURITY.md.
54
+ Focus on auth changes, permission model updates, new security boundaries, data handling changes.
55
+ Write in present tense. 2-3 paragraphs max.
56
+
57
+ Git diff:
58
+ ${diff.raw.slice(0, 8000)}`,
59
+
60
+ 'RELIABILITY.md': (diff) =>
61
+ `You are a technical writer following Harness Engineering documentation style.
62
+
63
+ Based on the git diff between ${diff.prevTag} and ${diff.currentTag}, write a section for RELIABILITY.md.
64
+ Focus on error handling improvements, retry logic, circuit breakers, observability changes.
65
+ Write in present tense. 2-3 paragraphs max.
66
+
67
+ Git diff:
68
+ ${diff.raw.slice(0, 8000)}`,
69
+
70
+ 'QUALITY_SCORE.md': (diff) =>
71
+ `You are a technical writer following Harness Engineering documentation style.
72
+
73
+ Based on the git diff between ${diff.prevTag} and ${diff.currentTag}, write a quality assessment section.
74
+ Assess test coverage changes, code complexity, technical debt introduced or resolved.
75
+ Write in present tense. 1-2 paragraphs max.
76
+
77
+ Git diff:
78
+ ${diff.raw.slice(0, 8000)}`,
79
+
80
+ 'changelog': (diff) =>
81
+ `Write a changelog entry in Markdown for changes from ${diff.prevTag} to ${diff.currentTag}.
82
+
83
+ Format:
84
+ # Changelog: ${diff.currentTag}
85
+
86
+ ## Added
87
+ - ...
88
+
89
+ ## Changed
90
+ - ...
91
+
92
+ ## Fixed
93
+ - ...
94
+
95
+ ## Removed
96
+ - ...
97
+
98
+ Be specific and engineer-focused. Only include sections with actual content.
99
+
100
+ Git diff:
101
+ ${diff.raw.slice(0, 8000)}`,
102
+
103
+ 'design-doc': (diff) =>
104
+ `You are a technical writer following Harness Engineering documentation style.
105
+
106
+ Write a design document for changes from ${diff.prevTag} to ${diff.currentTag}.
107
+
108
+ Format:
109
+ # Design: ${diff.currentTag}
110
+
111
+ ## Summary
112
+ [What changed and why]
113
+
114
+ ## Design Decisions
115
+ [Key decisions with rationale]
116
+
117
+ ## Agent Legibility Notes
118
+ [What an AI coding agent needs to know to work in the updated codebase]
119
+
120
+ ## Technical Debt
121
+ [Any shortcuts taken, what should be cleaned up later — or "None identified"]
122
+
123
+ Git diff:
124
+ ${diff.raw.slice(0, 8000)}`,
125
+
126
+ 'design-doc-index': (diff) =>
127
+ `Return only a single Markdown list item for a docs index. Format:
128
+ - [${diff.currentTag}](${diff.currentTag}.md) — One-sentence summary of changes.
129
+
130
+ Changed files: ${diff.changedFiles.slice(0, 20).join(', ')}
131
+
132
+ Return only the list item, nothing else.`,
133
+
134
+ 'tech-debt-tracker': (diff) =>
135
+ `You are a technical writer following Harness Engineering documentation style.
136
+
137
+ Based on the git diff between ${diff.prevTag} and ${diff.currentTag}, identify technical debt.
138
+
139
+ Write this section:
140
+ ## ${diff.currentTag}
141
+
142
+ ### New debt
143
+ - [Shortcuts, hacks, or deferred work visible in the diff — or "None identified"]
144
+
145
+ ### Resolved debt
146
+ - [Cleanup or refactoring that resolves known issues — or "None identified"]
147
+
148
+ Git diff:
149
+ ${diff.raw.slice(0, 8000)}`,
150
+
151
+ 'db-schema': (diff) =>
152
+ `You are a technical writer.
153
+
154
+ Based on the git diff between ${diff.prevTag} and ${diff.currentTag}, document database schema changes.
155
+ Focus only on schema changes visible in the diff: new tables, columns, indexes, constraints.
156
+ Format as Markdown with clear table descriptions.
157
+ If no schema changes: write "No schema changes in this release."
158
+
159
+ Git diff:
160
+ ${diff.raw.slice(0, 8000)}`,
161
+
162
+ 'product-specs-index': (diff) =>
163
+ `Return only a single Markdown list item for a product specs index, or an empty string if no clear new feature.
164
+ Format (if applicable): - [Feature Name](feature-name.md) — One-sentence description.
165
+
166
+ Changed files: ${diff.changedFiles.slice(0, 20).join(', ')}
167
+
168
+ Return only the list item or empty string, nothing else.`,
169
+
170
+ 'references': (diff) =>
171
+ `You are a technical writer.
172
+
173
+ Based on the git diff between ${diff.prevTag} and ${diff.currentTag}, identify new external libraries introduced.
174
+ For each new library write:
175
+
176
+ # library-name
177
+
178
+ One paragraph: what it does and how it is used in this codebase.
179
+
180
+ If no new external libraries: return an empty string.
181
+
182
+ Git diff:
183
+ ${diff.raw.slice(0, 8000)}`,
184
+ };
185
+
186
+ export interface GenerationResult {
187
+ target: DocTarget;
188
+ content: string;
189
+ error?: string;
190
+ }
191
+
192
+ export async function generateDocs(
193
+ ai: AIProvider,
194
+ diff: DiffResult,
195
+ targets: DocTarget[]
196
+ ): Promise<GenerationResult[]> {
197
+ return Promise.all(
198
+ targets.map(async (target): Promise<GenerationResult> => {
199
+ try {
200
+ const prompt = PROMPTS[target](diff);
201
+ const content = await ai.generate(prompt);
202
+ return { target, content };
203
+ } catch (err) {
204
+ return { target, content: '', error: String(err) };
205
+ }
206
+ })
207
+ );
208
+ }
209
+
210
+ export function writeResults(
211
+ results: GenerationResult[],
212
+ diff: DiffResult,
213
+ cwd: string
214
+ ): string[] {
215
+ const written: string[] = [];
216
+ for (const result of results) {
217
+ if (result.error || !result.content.trim()) continue;
218
+ const path = writeResult(result, diff, cwd);
219
+ if (path) written.push(path);
220
+ }
221
+ return written;
222
+ }
223
+
224
+ function writeResult(
225
+ result: GenerationResult,
226
+ diff: DiffResult,
227
+ cwd: string
228
+ ): string | null {
229
+ const tag = diff.currentTag;
230
+ const heading = `Changes in ${tag}`;
231
+
232
+ const appendTargets: DocTarget[] = [
233
+ 'AGENTS.md', 'ARCHITECTURE.md', 'DESIGN.md', 'FRONTEND.md',
234
+ 'SECURITY.md', 'RELIABILITY.md', 'QUALITY_SCORE.md',
235
+ ];
236
+
237
+ const rootTargets: DocTarget[] = ['AGENTS.md', 'ARCHITECTURE.md'];
238
+
239
+ if ((appendTargets as string[]).includes(result.target)) {
240
+ const dir = (rootTargets as string[]).includes(result.target) ? '' : 'docs/';
241
+ const path = `${cwd}/${dir}${result.target}`;
242
+ appendSection(path, heading, result.content);
243
+ return path;
244
+ }
245
+
246
+ switch (result.target) {
247
+ case 'changelog': {
248
+ const path = `${cwd}/changelog/${tag}.md`;
249
+ createFile(path, result.content);
250
+ return path;
251
+ }
252
+ case 'design-doc': {
253
+ const path = `${cwd}/docs/design-docs/${tag}.md`;
254
+ createFile(path, result.content);
255
+ return path;
256
+ }
257
+ case 'design-doc-index': {
258
+ const path = `${cwd}/docs/design-docs/index.md`;
259
+ appendSection(path, heading, result.content);
260
+ return path;
261
+ }
262
+ case 'tech-debt-tracker': {
263
+ const path = `${cwd}/docs/exec-plans/tech-debt-tracker.md`;
264
+ appendSection(path, heading, result.content);
265
+ return path;
266
+ }
267
+ case 'db-schema': {
268
+ const path = `${cwd}/docs/generated/db-schema.md`;
269
+ appendSection(path, heading, result.content);
270
+ return path;
271
+ }
272
+ case 'product-specs-index': {
273
+ if (!result.content.trim()) return null;
274
+ const path = `${cwd}/docs/product-specs/index.md`;
275
+ appendSection(path, heading, result.content);
276
+ return path;
277
+ }
278
+ case 'references': {
279
+ if (!result.content.trim()) return null;
280
+ const libName = extractLibName(result.content);
281
+ if (!libName) return null;
282
+ const path = `${cwd}/docs/references/${libName}-llms.txt`;
283
+ createFile(path, result.content);
284
+ return path;
285
+ }
286
+ default:
287
+ return null;
288
+ }
289
+ }
290
+
291
+ function extractLibName(content: string): string | null {
292
+ const match = content.match(/^#\s+(.+)$/m);
293
+ return match ? match[1].toLowerCase().replace(/[^a-z0-9-]/g, '-') : null;
294
+ }
@@ -0,0 +1,53 @@
1
+ // src/core/relevance.ts
2
+ import type { FileGroups } from './diff.js';
3
+
4
+ export type DocTarget =
5
+ | 'AGENTS.md'
6
+ | 'ARCHITECTURE.md'
7
+ | 'DESIGN.md'
8
+ | 'FRONTEND.md'
9
+ | 'SECURITY.md'
10
+ | 'RELIABILITY.md'
11
+ | 'QUALITY_SCORE.md'
12
+ | 'changelog'
13
+ | 'design-doc'
14
+ | 'design-doc-index'
15
+ | 'tech-debt-tracker'
16
+ | 'db-schema'
17
+ | 'product-specs-index'
18
+ | 'references';
19
+
20
+ const CORE_TARGETS: DocTarget[] = [
21
+ 'AGENTS.md',
22
+ 'ARCHITECTURE.md',
23
+ 'DESIGN.md',
24
+ 'QUALITY_SCORE.md',
25
+ 'changelog',
26
+ 'design-doc',
27
+ 'design-doc-index',
28
+ 'tech-debt-tracker',
29
+ ];
30
+
31
+ export function selectTargets(
32
+ fileGroups: FileGroups,
33
+ changedFiles: string[]
34
+ ): DocTarget[] {
35
+ const targets: DocTarget[] = [...CORE_TARGETS];
36
+
37
+ if (fileGroups.frontend.length > 0) targets.push('FRONTEND.md');
38
+ if (fileGroups.auth.length > 0) targets.push('SECURITY.md');
39
+ if (fileGroups.infra.length > 0) targets.push('RELIABILITY.md');
40
+ if (fileGroups.schema.length > 0) targets.push('db-schema');
41
+
42
+ const packageFiles = ['package.json', 'package-lock.json', 'yarn.lock', 'pnpm-lock.yaml'];
43
+ if (changedFiles.some(f => packageFiles.includes(f))) {
44
+ targets.push('references');
45
+ }
46
+
47
+ const looksLikeNewFeature = changedFiles.some(f =>
48
+ /\b(feature|feat|new)\b/i.test(f) || f.startsWith('src/features/')
49
+ );
50
+ if (looksLikeNewFeature) targets.push('product-specs-index');
51
+
52
+ return [...new Set(targets)];
53
+ }
@@ -0,0 +1,25 @@
1
+ import { writeFileSync, readFileSync, existsSync, mkdirSync } from 'fs';
2
+ import { dirname } from 'path';
3
+
4
+ export function appendSection(filePath: string, heading: string, content: string): void {
5
+ ensureDir(filePath);
6
+ const section = `\n## ${heading}\n\n${content}\n`;
7
+ if (existsSync(filePath)) {
8
+ const existing = readFileSync(filePath, 'utf-8');
9
+ writeFileSync(filePath, existing + section, 'utf-8');
10
+ } else {
11
+ writeFileSync(filePath, section.trimStart(), 'utf-8');
12
+ }
13
+ }
14
+
15
+ export function createFile(filePath: string, content: string): void {
16
+ ensureDir(filePath);
17
+ writeFileSync(filePath, content, 'utf-8');
18
+ }
19
+
20
+ function ensureDir(filePath: string): void {
21
+ const dir = dirname(filePath);
22
+ if (!existsSync(dir)) {
23
+ mkdirSync(dir, { recursive: true });
24
+ }
25
+ }
@@ -0,0 +1,53 @@
1
+ import { Octokit } from '@octokit/rest';
2
+ import { execSync } from 'child_process';
3
+ import type { PlatformProvider } from './interface.js';
4
+
5
+ export class GitHubProvider implements PlatformProvider {
6
+ private octokit: Octokit;
7
+ private owner: string;
8
+ private repo: string;
9
+
10
+ constructor(token: string) {
11
+ this.octokit = new Octokit({ auth: token });
12
+ const remoteUrl = execSync('git remote get-url origin').toString().trim();
13
+ const match = remoteUrl.match(/github\.com[/:](.+?)\/(.+?)(?:\.git)?$/);
14
+ if (!match) throw new Error(`Cannot parse GitHub remote URL: ${remoteUrl}`);
15
+ this.owner = match[1];
16
+ this.repo = match[2];
17
+ }
18
+
19
+ async createOrUpdatePR(opts: {
20
+ branch: string;
21
+ title: string;
22
+ body: string;
23
+ baseBranch: string;
24
+ }): Promise<string> {
25
+ const { data: existingPRs } = await this.octokit.pulls.list({
26
+ owner: this.owner,
27
+ repo: this.repo,
28
+ head: `${this.owner}:${opts.branch}`,
29
+ state: 'open',
30
+ });
31
+
32
+ if (existingPRs.length > 0) {
33
+ const { data: pr } = await this.octokit.pulls.update({
34
+ owner: this.owner,
35
+ repo: this.repo,
36
+ pull_number: existingPRs[0].number,
37
+ title: opts.title,
38
+ body: opts.body,
39
+ });
40
+ return pr.html_url;
41
+ }
42
+
43
+ const { data: pr } = await this.octokit.pulls.create({
44
+ owner: this.owner,
45
+ repo: this.repo,
46
+ title: opts.title,
47
+ body: opts.body,
48
+ head: opts.branch,
49
+ base: opts.baseBranch,
50
+ });
51
+ return pr.html_url;
52
+ }
53
+ }
@@ -0,0 +1,16 @@
1
+ // src/providers/gitlab.ts
2
+ import type { PlatformProvider } from './interface.js';
3
+
4
+ export class GitLabProvider implements PlatformProvider {
5
+ async createOrUpdatePR(_opts: {
6
+ branch: string;
7
+ title: string;
8
+ body: string;
9
+ baseBranch: string;
10
+ }): Promise<string> {
11
+ throw new Error(
12
+ 'GitLab provider is not yet implemented. ' +
13
+ 'Contributions welcome: https://github.com/your-org/harness-engineering-auto-docs'
14
+ );
15
+ }
16
+ }
@@ -0,0 +1,9 @@
1
+ // src/providers/interface.ts
2
+ export interface PlatformProvider {
3
+ createOrUpdatePR(opts: {
4
+ branch: string;
5
+ title: string;
6
+ body: string;
7
+ baseBranch: string;
8
+ }): Promise<string>;
9
+ }
@@ -0,0 +1,33 @@
1
+ import { describe, it, expect, vi } from 'vitest';
2
+ import { AnthropicProvider } from '../../src/ai/anthropic.js';
3
+
4
+ vi.mock('@anthropic-ai/sdk', () => ({
5
+ default: vi.fn().mockImplementation(() => ({
6
+ messages: {
7
+ create: vi.fn().mockResolvedValue({
8
+ content: [{ type: 'text', text: 'generated content' }],
9
+ }),
10
+ },
11
+ })),
12
+ }));
13
+
14
+ describe('AnthropicProvider', () => {
15
+ it('returns text content from the API response', async () => {
16
+ const provider = new AnthropicProvider('test-key', 'claude-sonnet-4-6');
17
+ const result = await provider.generate('write docs for this diff');
18
+ expect(result).toBe('generated content');
19
+ });
20
+
21
+ it('throws if response content is not text', async () => {
22
+ const { default: Anthropic } = await import('@anthropic-ai/sdk');
23
+ vi.mocked(Anthropic).mockImplementationOnce(() => ({
24
+ messages: {
25
+ create: vi.fn().mockResolvedValue({
26
+ content: [{ type: 'image', source: {} }],
27
+ }),
28
+ },
29
+ }) as any);
30
+ const provider = new AnthropicProvider('test-key', 'claude-sonnet-4-6');
31
+ await expect(provider.generate('prompt')).rejects.toThrow('Unexpected response type');
32
+ });
33
+ });
@@ -0,0 +1,49 @@
1
+ // tests/core/diff.test.ts
2
+ import { describe, it, expect } from 'vitest';
3
+ import { readFileSync } from 'fs';
4
+ import { join } from 'path';
5
+ import { parseChangedFiles, groupFiles } from '../../src/core/diff.js';
6
+
7
+ const smallDiff = readFileSync(
8
+ join(import.meta.dirname, '../fixtures/diff-small.txt'), 'utf-8'
9
+ );
10
+ const schemaDiff = readFileSync(
11
+ join(import.meta.dirname, '../fixtures/diff-schema.txt'), 'utf-8'
12
+ );
13
+ const frontendDiff = readFileSync(
14
+ join(import.meta.dirname, '../fixtures/diff-frontend.txt'), 'utf-8'
15
+ );
16
+
17
+ describe('parseChangedFiles', () => {
18
+ it('extracts file paths from diff output', () => {
19
+ const files = parseChangedFiles(smallDiff);
20
+ expect(files).toContain('src/auth/login.ts');
21
+ expect(files).toContain('src/utils/helpers.ts');
22
+ expect(files).toHaveLength(2);
23
+ });
24
+
25
+ it('returns empty array for empty diff', () => {
26
+ expect(parseChangedFiles('')).toEqual([]);
27
+ });
28
+ });
29
+
30
+ describe('groupFiles', () => {
31
+ it('puts auth files in auth group', () => {
32
+ const groups = groupFiles(['src/auth/login.ts', 'src/utils/helpers.ts']);
33
+ expect(groups.auth).toContain('src/auth/login.ts');
34
+ expect(groups.auth).not.toContain('src/utils/helpers.ts');
35
+ });
36
+
37
+ it('puts sql files in schema group', () => {
38
+ const files = parseChangedFiles(schemaDiff);
39
+ const groups = groupFiles(files);
40
+ expect(groups.schema.length).toBeGreaterThan(0);
41
+ });
42
+
43
+ it('puts tsx files in frontend group', () => {
44
+ const files = parseChangedFiles(frontendDiff);
45
+ const groups = groupFiles(files);
46
+ expect(groups.frontend).toContain('src/ui/Button.tsx');
47
+ expect(groups.frontend).toContain('src/ui/styles.css');
48
+ });
49
+ });