prjct-cli 1.16.0 → 1.17.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md CHANGED
@@ -1,12 +1,52 @@
1
1
  # Changelog
2
2
 
3
+ ## [1.17.0] - 2026-02-09
4
+
5
+ ### Features
6
+
7
+ - implement BM25 + import graph + git co-change for zero-cost file selection (PRJ-304) (#159)
8
+
9
+
10
+ ## [1.17.0] - 2026-02-08
11
+
12
+ ### Features
13
+ - **BM25 + import graph + git co-change file selection** (PRJ-304): Zero-cost file selection using three mathematical signals combined into a weighted ranker. Replaces keyword matching in smart-context with precision that matches LLM-based classification — at zero API cost.
14
+
15
+ ### Implementation Details
16
+ - `core/domain/bm25.ts` — BM25 indexer: tokenizes files (exports, functions, imports, comments, path segments), builds inverted index, scores queries using Okapi BM25 (k1=1.2, b=0.75). Stores in SQLite kv_store.
17
+ - `core/domain/import-graph.ts` — Import graph builder: parses TS/JS imports to build directed adjacency list, follows chains 2 levels deep, scores by proximity (1/(depth+1)).
18
+ - `core/domain/git-cochange.ts` — Git co-change analyzer: parses last 100 commits, builds Jaccard similarity matrix for file pairs that change together.
19
+ - `core/domain/file-ranker.ts` — Combined ranker: `BM25 × 0.5 + imports × 0.3 + cochange × 0.2`, normalizes each signal to [0,1], returns top 15 files.
20
+ - `core/agentic/smart-context.ts` — Uses ranker when indexes exist, graceful fallback to regex-based domain filtering.
21
+ - `core/services/sync-service.ts` — Builds all 3 indexes in parallel during `prjct sync`.
22
+
23
+ ### Learnings
24
+ - `tokenizeQuery` must split camelCase BEFORE lowercasing — otherwise "getUserById" becomes "getuserbyid" and doesn't split
25
+ - Jaccard similarity > cosine for co-change because data is binary (file present or not in commit)
26
+ - Batch file reads (50 at a time) needed for indexing performance on large projects
27
+ - Stop words list must include code keywords (import, export, const) to reduce noise in scoring
28
+
29
+ ### Test Plan
30
+
31
+ #### For QA
32
+ 1. Run `prjct sync` — verify BM25, import graph, and co-change indexes build without errors
33
+ 2. Query "Fix auth middleware" — verify auth-related files rank higher than unrelated files
34
+ 3. Query "Build responsive dashboard" — verify frontend files rank higher than backend files
35
+ 4. Verify index rebuild time <5 seconds on 300+ file project
36
+ 5. Verify query time <50ms
37
+ 6. Verify zero API calls during file selection
38
+
39
+ #### For Users
40
+ **What changed:** File selection during task context is now powered by BM25 text search, import graph proximity, and git co-change analysis instead of keyword matching.
41
+ **How to use:** Run `p. sync` to build indexes — file selection is automatic and more accurate.
42
+ **Breaking changes:** None. Falls back to previous filtering if indexes don't exist.
43
+
3
44
  ## [1.16.0] - 2026-02-09
4
45
 
5
46
  ### Features
6
47
 
7
48
  - remove JSON storage redundancy, SQLite-only backend (PRJ-303) (#158)
8
49
 
9
-
10
50
  ## [1.16.0] - 2026-02-08
11
51
 
12
52
  ### Features
@@ -0,0 +1,225 @@
1
+ /**
2
+ * Tests for BM25 Text Search Index
3
+ */
4
+
5
+ import { afterEach, beforeEach, describe, expect, it } from 'bun:test'
6
+ import fs from 'node:fs/promises'
7
+ import os from 'node:os'
8
+ import path from 'node:path'
9
+ import { buildIndex, score, tokenizeFile, tokenizeQuery } from '../../domain/bm25'
10
+
11
+ // =============================================================================
12
+ // Tokenization Tests
13
+ // =============================================================================
14
+
15
+ describe('BM25', () => {
16
+ describe('tokenizeFile', () => {
17
+ it('should extract path segments', () => {
18
+ const tokens = tokenizeFile('', 'core/domain/bm25.ts')
19
+ expect(tokens).toContain('core')
20
+ expect(tokens).toContain('domain')
21
+ expect(tokens).toContain('bm25')
22
+ })
23
+
24
+ it('should extract function names and split camelCase', () => {
25
+ const content = 'export function getUserById(id: string) { return id }'
26
+ const tokens = tokenizeFile(content, 'user-service.ts')
27
+ expect(tokens).toContain('get')
28
+ expect(tokens).toContain('user')
29
+ // 'by' and 'id' are filtered (stop word / too short)
30
+ })
31
+
32
+ it('should extract class names', () => {
33
+ const content = 'export class AuthMiddleware { handle() {} }'
34
+ const tokens = tokenizeFile(content, 'middleware.ts')
35
+ expect(tokens).toContain('auth')
36
+ expect(tokens).toContain('middleware')
37
+ })
38
+
39
+ it('should extract interface names', () => {
40
+ const content = 'export interface JwtPayload { sub: string }'
41
+ const tokens = tokenizeFile(content, 'types.ts')
42
+ expect(tokens).toContain('jwt')
43
+ expect(tokens).toContain('payload')
44
+ })
45
+
46
+ it('should extract import sources', () => {
47
+ const content = `import { Router } from './router'\nimport express from 'express'`
48
+ const tokens = tokenizeFile(content, 'app.ts')
49
+ expect(tokens).toContain('router')
50
+ expect(tokens).toContain('express')
51
+ })
52
+
53
+ it('should extract words from comments', () => {
54
+ const content = '// Handle authentication for JWT tokens'
55
+ const tokens = tokenizeFile(content, 'auth.ts')
56
+ expect(tokens).toContain('handle')
57
+ expect(tokens).toContain('authentication')
58
+ expect(tokens).toContain('jwt')
59
+ expect(tokens).toContain('tokens')
60
+ })
61
+
62
+ it('should extract words from JSDoc comments', () => {
63
+ const content = '/** Validates user session and refreshes token */'
64
+ const tokens = tokenizeFile(content, 'session.ts')
65
+ expect(tokens).toContain('validates')
66
+ expect(tokens).toContain('session')
67
+ expect(tokens).toContain('refreshes')
68
+ expect(tokens).toContain('token')
69
+ })
70
+
71
+ it('should filter out stop words', () => {
72
+ const content = 'export function getTheData() {}'
73
+ const tokens = tokenizeFile(content, 'data.ts')
74
+ expect(tokens).not.toContain('the')
75
+ expect(tokens).not.toContain('export')
76
+ expect(tokens).not.toContain('function')
77
+ })
78
+
79
+ it('should handle empty content', () => {
80
+ const tokens = tokenizeFile('', 'empty.ts')
81
+ // Should still have path segments
82
+ expect(tokens).toContain('empty')
83
+ })
84
+ })
85
+
86
+ describe('tokenizeQuery', () => {
87
+ it('should tokenize a task description', () => {
88
+ const tokens = tokenizeQuery('Fix the auth middleware for JWT validation')
89
+ expect(tokens).toContain('fix')
90
+ expect(tokens).toContain('auth')
91
+ expect(tokens).toContain('middleware')
92
+ expect(tokens).toContain('jwt')
93
+ expect(tokens).toContain('validation')
94
+ })
95
+
96
+ it('should split camelCase in queries', () => {
97
+ const tokens = tokenizeQuery('update getUserById function')
98
+ expect(tokens).toContain('update')
99
+ expect(tokens).toContain('get')
100
+ expect(tokens).toContain('user')
101
+ })
102
+
103
+ it('should remove stop words from queries', () => {
104
+ const tokens = tokenizeQuery('Fix the bug in the login')
105
+ expect(tokens).not.toContain('the')
106
+ expect(tokens).not.toContain('in')
107
+ expect(tokens).toContain('fix')
108
+ expect(tokens).toContain('bug')
109
+ expect(tokens).toContain('login')
110
+ })
111
+ })
112
+
113
+ // =============================================================================
114
+ // Index Building & Scoring Tests
115
+ // =============================================================================
116
+
117
+ describe('buildIndex + score', () => {
118
+ let testDir: string
119
+
120
+ beforeEach(async () => {
121
+ testDir = path.join(os.tmpdir(), `prjct-bm25-test-${Date.now()}`)
122
+ await fs.mkdir(testDir, { recursive: true })
123
+ })
124
+
125
+ afterEach(async () => {
126
+ try {
127
+ await fs.rm(testDir, { recursive: true, force: true })
128
+ } catch {
129
+ // Ignore cleanup errors
130
+ }
131
+ })
132
+
133
+ it('should build an index from project files', async () => {
134
+ // Create test files
135
+ await fs.writeFile(
136
+ path.join(testDir, 'auth.ts'),
137
+ `export class AuthService {\n validateJwt(token: string) {}\n refreshSession() {}\n}`
138
+ )
139
+ await fs.writeFile(
140
+ path.join(testDir, 'middleware.ts'),
141
+ `import { AuthService } from './auth'\nexport function authMiddleware(req: any) {}`
142
+ )
143
+ await fs.writeFile(
144
+ path.join(testDir, 'button.tsx'),
145
+ `export function Button({ label }: { label: string }) {\n return <button>{label}</button>\n}`
146
+ )
147
+
148
+ const index = await buildIndex(testDir)
149
+
150
+ expect(index.totalDocs).toBe(3)
151
+ expect(index.avgDocLength).toBeGreaterThan(0)
152
+ expect(Object.keys(index.documents)).toContain('auth.ts')
153
+ expect(Object.keys(index.documents)).toContain('middleware.ts')
154
+ expect(Object.keys(index.documents)).toContain('button.tsx')
155
+ })
156
+
157
+ it('should rank auth files higher for auth query', async () => {
158
+ await fs.writeFile(
159
+ path.join(testDir, 'auth.ts'),
160
+ `export class AuthService {\n // Authenticate user with JWT\n validateJwt(token: string) {}\n refreshSession() {}\n}`
161
+ )
162
+ await fs.writeFile(
163
+ path.join(testDir, 'middleware.ts'),
164
+ `import { AuthService } from './auth'\n// Auth middleware for JWT validation\nexport function authMiddleware(req: any) {}`
165
+ )
166
+ await fs.writeFile(
167
+ path.join(testDir, 'button.tsx'),
168
+ `// Render a UI button component\nexport function Button({ label }: { label: string }) {\n return <button>{label}</button>\n}`
169
+ )
170
+
171
+ const index = await buildIndex(testDir)
172
+ const results = score('Fix the auth middleware for JWT validation', index)
173
+
174
+ // Auth and middleware should rank higher than button
175
+ expect(results.length).toBeGreaterThan(0)
176
+ const authIndex = results.findIndex((r) => r.path === 'auth.ts')
177
+ const middlewareIndex = results.findIndex((r) => r.path === 'middleware.ts')
178
+ const buttonIndex = results.findIndex((r) => r.path === 'button.tsx')
179
+
180
+ expect(authIndex).toBeLessThan(buttonIndex === -1 ? Infinity : buttonIndex)
181
+ expect(middlewareIndex).toBeLessThan(buttonIndex === -1 ? Infinity : buttonIndex)
182
+ })
183
+
184
+ it('should rank frontend files higher for UI query', async () => {
185
+ await fs.writeFile(
186
+ path.join(testDir, 'dashboard.tsx'),
187
+ `// Responsive dashboard with charts and data grid\nexport function Dashboard() {\n return <div className="dashboard">Charts here</div>\n}`
188
+ )
189
+ await fs.writeFile(
190
+ path.join(testDir, 'api-handler.ts'),
191
+ `// Handle API requests for user data\nexport function handleRequest(req: any) { return {} }`
192
+ )
193
+
194
+ const index = await buildIndex(testDir)
195
+ const results = score('Build responsive dashboard', index)
196
+
197
+ expect(results.length).toBeGreaterThan(0)
198
+ expect(results[0].path).toBe('dashboard.tsx')
199
+ })
200
+
201
+ it('should skip node_modules', async () => {
202
+ await fs.mkdir(path.join(testDir, 'node_modules', 'pkg'), { recursive: true })
203
+ await fs.writeFile(path.join(testDir, 'node_modules', 'pkg', 'index.ts'), 'export default {}')
204
+ await fs.writeFile(path.join(testDir, 'app.ts'), 'export function main() {}')
205
+
206
+ const index = await buildIndex(testDir)
207
+ expect(index.totalDocs).toBe(1)
208
+ expect(Object.keys(index.documents)).toContain('app.ts')
209
+ })
210
+
211
+ it('should handle empty query', async () => {
212
+ await fs.writeFile(path.join(testDir, 'app.ts'), 'export function main() {}')
213
+
214
+ const index = await buildIndex(testDir)
215
+ const results = score('', index)
216
+ expect(results).toEqual([])
217
+ })
218
+
219
+ it('should handle empty project', async () => {
220
+ const index = await buildIndex(testDir)
221
+ expect(index.totalDocs).toBe(0)
222
+ expect(score('anything', index)).toEqual([])
223
+ })
224
+ })
225
+ })
@@ -0,0 +1,169 @@
1
+ /**
2
+ * Tests for Combined File Ranker
3
+ */
4
+
5
+ import { afterEach, beforeEach, describe, expect, it } from 'bun:test'
6
+ import { exec as execCallback } from 'node:child_process'
7
+ import fs from 'node:fs/promises'
8
+ import os from 'node:os'
9
+ import path from 'node:path'
10
+ import { promisify } from 'node:util'
11
+ import { indexProject } from '../../domain/bm25'
12
+ import { hasIndexes, rankFiles } from '../../domain/file-ranker'
13
+ import { indexCoChanges } from '../../domain/git-cochange'
14
+ import { indexImports } from '../../domain/import-graph'
15
+ import pathManager from '../../infrastructure/path-manager'
16
+ import prjctDb from '../../storage/database'
17
+
18
+ const exec = promisify(execCallback)
19
+
20
+ describe('FileRanker', () => {
21
+ let testDir: string
22
+ let testProjectId: string
23
+ const originalGetGlobalProjectPath = pathManager.getGlobalProjectPath.bind(pathManager)
24
+
25
+ beforeEach(async () => {
26
+ testDir = path.join(os.tmpdir(), `prjct-ranker-test-${Date.now()}`)
27
+ testProjectId = `test-ranker-${Date.now()}`
28
+ await fs.mkdir(testDir, { recursive: true })
29
+
30
+ // Mock path manager to use temp dir
31
+ pathManager.getGlobalProjectPath = () => testDir
32
+
33
+ // Initialize git repo
34
+ await exec('git init', { cwd: testDir })
35
+ await exec('git config user.email "test@test.com"', { cwd: testDir })
36
+ await exec('git config user.name "Test"', { cwd: testDir })
37
+ })
38
+
39
+ afterEach(async () => {
40
+ pathManager.getGlobalProjectPath = originalGetGlobalProjectPath
41
+ prjctDb.close()
42
+ try {
43
+ await fs.rm(testDir, { recursive: true, force: true })
44
+ } catch {
45
+ // Ignore
46
+ }
47
+ })
48
+
49
+ describe('hasIndexes', () => {
50
+ it('should return false when no indexes exist', () => {
51
+ const result = hasIndexes(testProjectId)
52
+ expect(result.bm25).toBe(false)
53
+ expect(result.imports).toBe(false)
54
+ expect(result.cochange).toBe(false)
55
+ })
56
+
57
+ it('should return true after building indexes', async () => {
58
+ // Create a test file
59
+ await fs.writeFile(path.join(testDir, 'app.ts'), 'export function main() {}')
60
+ await exec('git add -A && git commit -m "init"', { cwd: testDir })
61
+
62
+ await indexProject(testDir, testProjectId)
63
+ await indexImports(testDir, testProjectId)
64
+ await indexCoChanges(testDir, testProjectId)
65
+
66
+ const result = hasIndexes(testProjectId)
67
+ expect(result.bm25).toBe(true)
68
+ expect(result.imports).toBe(true)
69
+ expect(result.cochange).toBe(true)
70
+ })
71
+ })
72
+
73
+ describe('rankFiles', () => {
74
+ it('should return empty array when no indexes exist', () => {
75
+ const result = rankFiles(testProjectId, 'anything')
76
+ expect(result).toEqual([])
77
+ })
78
+
79
+ it('should rank relevant files higher', async () => {
80
+ // Create auth-related files
81
+ await fs.writeFile(
82
+ path.join(testDir, 'auth.ts'),
83
+ `// Authentication service for JWT handling\nexport class AuthService {\n validateJwt(token: string) { return true }\n}`
84
+ )
85
+ await fs.writeFile(
86
+ path.join(testDir, 'middleware.ts'),
87
+ `import { AuthService } from './auth'\n// Auth middleware\nexport function authMiddleware() {}`
88
+ )
89
+ await fs.writeFile(
90
+ path.join(testDir, 'session.ts'),
91
+ `import { AuthService } from './auth'\n// Session management\nexport function refreshSession() {}`
92
+ )
93
+ // Create unrelated file
94
+ await fs.writeFile(
95
+ path.join(testDir, 'button.tsx'),
96
+ `// UI button component\nexport function Button() { return null }`
97
+ )
98
+
99
+ // Create git history with co-changes
100
+ await exec('git add -A && git commit -m "init"', { cwd: testDir })
101
+ await fs.writeFile(path.join(testDir, 'auth.ts'), `export class AuthService { v2() {} }`)
102
+ await fs.writeFile(
103
+ path.join(testDir, 'middleware.ts'),
104
+ `export function authMiddleware() { v2 }`
105
+ )
106
+ await exec('git add -A && git commit -m "update auth"', { cwd: testDir })
107
+
108
+ // Build all indexes
109
+ await indexProject(testDir, testProjectId)
110
+ await indexImports(testDir, testProjectId)
111
+ await indexCoChanges(testDir, testProjectId)
112
+
113
+ const results = rankFiles(testProjectId, 'Fix auth middleware for JWT validation')
114
+
115
+ expect(results.length).toBeGreaterThan(0)
116
+
117
+ // Auth and middleware should be in results
118
+ const authResult = results.find((r) => r.path === 'auth.ts')
119
+ const middlewareResult = results.find((r) => r.path === 'middleware.ts')
120
+
121
+ expect(authResult).toBeDefined()
122
+ expect(middlewareResult).toBeDefined()
123
+
124
+ // Auth should rank higher than button
125
+ const buttonResult = results.find((r) => r.path === 'button.tsx')
126
+ if (authResult && buttonResult) {
127
+ expect(authResult.finalScore).toBeGreaterThan(buttonResult.finalScore)
128
+ }
129
+ })
130
+
131
+ it('should include signal breakdown', async () => {
132
+ await fs.writeFile(
133
+ path.join(testDir, 'auth.ts'),
134
+ `// Authentication\nexport class AuthService {}`
135
+ )
136
+ await exec('git add -A && git commit -m "init"', { cwd: testDir })
137
+
138
+ await indexProject(testDir, testProjectId)
139
+ await indexImports(testDir, testProjectId)
140
+ await indexCoChanges(testDir, testProjectId)
141
+
142
+ const results = rankFiles(testProjectId, 'authentication')
143
+
144
+ if (results.length > 0) {
145
+ const first = results[0]
146
+ expect(first.signals).toBeDefined()
147
+ expect(typeof first.signals.bm25).toBe('number')
148
+ expect(typeof first.signals.imports).toBe('number')
149
+ expect(typeof first.signals.cochange).toBe('number')
150
+ }
151
+ })
152
+
153
+ it('should respect topN config', async () => {
154
+ // Create many files
155
+ for (let i = 0; i < 20; i++) {
156
+ await fs.writeFile(
157
+ path.join(testDir, `service-${i}.ts`),
158
+ `// Service module ${i}\nexport function service${i}() {}`
159
+ )
160
+ }
161
+ await exec('git add -A && git commit -m "init"', { cwd: testDir })
162
+
163
+ await indexProject(testDir, testProjectId)
164
+
165
+ const results = rankFiles(testProjectId, 'service module', { topN: 5 })
166
+ expect(results.length).toBeLessThanOrEqual(5)
167
+ })
168
+ })
169
+ })
@@ -0,0 +1,121 @@
1
+ /**
2
+ * Tests for Git Co-Change Analyzer
3
+ */
4
+
5
+ import { afterEach, beforeEach, describe, expect, it } from 'bun:test'
6
+ import { exec as execCallback } from 'node:child_process'
7
+ import fs from 'node:fs/promises'
8
+ import os from 'node:os'
9
+ import path from 'node:path'
10
+ import { promisify } from 'node:util'
11
+ import { buildMatrix, scoreFromSeeds } from '../../domain/git-cochange'
12
+
13
+ const exec = promisify(execCallback)
14
+
15
+ describe('GitCoChange', () => {
16
+ let testDir: string
17
+
18
+ beforeEach(async () => {
19
+ testDir = path.join(os.tmpdir(), `prjct-cochange-test-${Date.now()}`)
20
+ await fs.mkdir(testDir, { recursive: true })
21
+
22
+ // Initialize a git repo
23
+ await exec('git init', { cwd: testDir })
24
+ await exec('git config user.email "test@test.com"', { cwd: testDir })
25
+ await exec('git config user.name "Test"', { cwd: testDir })
26
+ })
27
+
28
+ afterEach(async () => {
29
+ try {
30
+ await fs.rm(testDir, { recursive: true, force: true })
31
+ } catch {
32
+ // Ignore cleanup errors
33
+ }
34
+ })
35
+
36
+ describe('buildMatrix', () => {
37
+ it('should detect co-changed files', async () => {
38
+ // Create files that change together
39
+ for (let i = 0; i < 5; i++) {
40
+ await fs.writeFile(path.join(testDir, 'auth.ts'), `export const v${i} = ${i}`)
41
+ await fs.writeFile(path.join(testDir, 'middleware.ts'), `export const v${i} = ${i}`)
42
+ await exec('git add -A', { cwd: testDir })
43
+ await exec(`git commit -m "commit ${i}"`, { cwd: testDir })
44
+ }
45
+
46
+ // Create a file that changes independently
47
+ await fs.writeFile(path.join(testDir, 'unrelated.ts'), 'export const x = 1')
48
+ await exec('git add -A', { cwd: testDir })
49
+ await exec('git commit -m "unrelated"', { cwd: testDir })
50
+
51
+ const index = await buildMatrix(testDir, 100)
52
+
53
+ expect(index.commitsAnalyzed).toBeGreaterThan(0)
54
+ expect(index.matrix['auth.ts']).toBeDefined()
55
+ expect(index.matrix['auth.ts']['middleware.ts']).toBeGreaterThan(0)
56
+
57
+ // Auth and middleware should have high co-change
58
+ const similarity = index.matrix['auth.ts']['middleware.ts']
59
+ expect(similarity).toBeGreaterThan(0.5)
60
+ })
61
+
62
+ it('should be symmetric', async () => {
63
+ for (let i = 0; i < 3; i++) {
64
+ await fs.writeFile(path.join(testDir, 'a.ts'), `const v${i} = ${i}`)
65
+ await fs.writeFile(path.join(testDir, 'b.ts'), `const v${i} = ${i}`)
66
+ await exec('git add -A', { cwd: testDir })
67
+ await exec(`git commit -m "commit ${i}"`, { cwd: testDir })
68
+ }
69
+
70
+ const index = await buildMatrix(testDir, 100)
71
+
72
+ if (index.matrix['a.ts'] && index.matrix['b.ts']) {
73
+ expect(index.matrix['a.ts']['b.ts']).toBe(index.matrix['b.ts']['a.ts'])
74
+ }
75
+ })
76
+
77
+ it('should handle no git history', async () => {
78
+ const emptyDir = path.join(os.tmpdir(), `prjct-cochange-empty-${Date.now()}`)
79
+ await fs.mkdir(emptyDir, { recursive: true })
80
+
81
+ const index = await buildMatrix(emptyDir, 100)
82
+ expect(index.commitsAnalyzed).toBe(0)
83
+ expect(Object.keys(index.matrix)).toHaveLength(0)
84
+
85
+ await fs.rm(emptyDir, { recursive: true, force: true })
86
+ })
87
+ })
88
+
89
+ describe('scoreFromSeeds', () => {
90
+ it('should score co-changed files', async () => {
91
+ // Create co-change history
92
+ for (let i = 0; i < 5; i++) {
93
+ await fs.writeFile(path.join(testDir, 'auth.ts'), `v${i}`)
94
+ await fs.writeFile(path.join(testDir, 'session.ts'), `v${i}`)
95
+ await exec('git add -A', { cwd: testDir })
96
+ await exec(`git commit -m "commit ${i}"`, { cwd: testDir })
97
+ }
98
+
99
+ const index = await buildMatrix(testDir, 100)
100
+ const scores = scoreFromSeeds(['auth.ts'], index)
101
+
102
+ const sessionScore = scores.find((s) => s.path === 'session.ts')
103
+ expect(sessionScore).toBeDefined()
104
+ expect(sessionScore!.score).toBeGreaterThan(0)
105
+ })
106
+
107
+ it('should not include seed files in results', async () => {
108
+ for (let i = 0; i < 3; i++) {
109
+ await fs.writeFile(path.join(testDir, 'a.ts'), `v${i}`)
110
+ await fs.writeFile(path.join(testDir, 'b.ts'), `v${i}`)
111
+ await exec('git add -A', { cwd: testDir })
112
+ await exec(`git commit -m "commit ${i}"`, { cwd: testDir })
113
+ }
114
+
115
+ const index = await buildMatrix(testDir, 100)
116
+ const scores = scoreFromSeeds(['a.ts'], index)
117
+
118
+ expect(scores.find((s) => s.path === 'a.ts')).toBeUndefined()
119
+ })
120
+ })
121
+ })