prjct-cli 0.44.1 → 0.45.3

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 (207) hide show
  1. package/CHANGELOG.md +114 -0
  2. package/bin/prjct.ts +131 -10
  3. package/core/__tests__/agentic/memory-system.test.ts +39 -26
  4. package/core/__tests__/agentic/plan-mode.test.ts +64 -46
  5. package/core/__tests__/agentic/prompt-builder.test.ts +14 -14
  6. package/core/__tests__/services/project-index.test.ts +353 -0
  7. package/core/__tests__/types/fs.test.ts +3 -3
  8. package/core/__tests__/utils/date-helper.test.ts +10 -10
  9. package/core/__tests__/utils/output.test.ts +9 -6
  10. package/core/__tests__/utils/project-commands.test.ts +5 -6
  11. package/core/agentic/agent-router.ts +9 -10
  12. package/core/agentic/chain-of-thought.ts +16 -4
  13. package/core/agentic/command-executor.ts +66 -40
  14. package/core/agentic/context-builder.ts +8 -5
  15. package/core/agentic/ground-truth.ts +15 -9
  16. package/core/agentic/index.ts +145 -152
  17. package/core/agentic/loop-detector.ts +40 -11
  18. package/core/agentic/memory-system.ts +98 -35
  19. package/core/agentic/orchestrator-executor.ts +135 -71
  20. package/core/agentic/plan-mode.ts +46 -16
  21. package/core/agentic/prompt-builder.ts +108 -42
  22. package/core/agentic/services.ts +10 -9
  23. package/core/agentic/skill-loader.ts +9 -15
  24. package/core/agentic/smart-context.ts +129 -79
  25. package/core/agentic/template-executor.ts +13 -12
  26. package/core/agentic/template-loader.ts +7 -4
  27. package/core/agentic/tool-registry.ts +16 -13
  28. package/core/agents/index.ts +1 -1
  29. package/core/agents/performance.ts +10 -27
  30. package/core/ai-tools/formatters.ts +8 -6
  31. package/core/ai-tools/generator.ts +4 -4
  32. package/core/ai-tools/index.ts +1 -1
  33. package/core/ai-tools/registry.ts +21 -11
  34. package/core/bus/bus.ts +23 -16
  35. package/core/bus/index.ts +2 -2
  36. package/core/cli/linear.ts +3 -5
  37. package/core/cli/start.ts +28 -25
  38. package/core/commands/analysis.ts +287 -29
  39. package/core/commands/analytics.ts +52 -44
  40. package/core/commands/base.ts +15 -13
  41. package/core/commands/cleanup.ts +6 -13
  42. package/core/commands/command-data.ts +49 -8
  43. package/core/commands/commands.ts +60 -23
  44. package/core/commands/context.ts +4 -4
  45. package/core/commands/design.ts +3 -10
  46. package/core/commands/index.ts +5 -8
  47. package/core/commands/maintenance.ts +7 -4
  48. package/core/commands/planning.ts +179 -56
  49. package/core/commands/register.ts +14 -9
  50. package/core/commands/registry.ts +15 -14
  51. package/core/commands/setup.ts +26 -14
  52. package/core/commands/shipping.ts +11 -16
  53. package/core/commands/snapshots.ts +16 -32
  54. package/core/commands/uninstall.ts +541 -0
  55. package/core/commands/workflow.ts +24 -28
  56. package/core/constants/index.ts +10 -22
  57. package/core/context/generator.ts +82 -33
  58. package/core/context-tools/files-tool.ts +583 -0
  59. package/core/context-tools/imports-tool.ts +403 -0
  60. package/core/context-tools/index.ts +433 -0
  61. package/core/context-tools/recent-tool.ts +307 -0
  62. package/core/context-tools/signatures-tool.ts +501 -0
  63. package/core/context-tools/summary-tool.ts +307 -0
  64. package/core/context-tools/token-counter.ts +284 -0
  65. package/core/context-tools/types.ts +253 -0
  66. package/core/domain/agent-generator.ts +7 -5
  67. package/core/domain/agent-loader.ts +2 -2
  68. package/core/domain/analyzer.ts +19 -16
  69. package/core/domain/architecture-generator.ts +6 -3
  70. package/core/domain/context-estimator.ts +3 -4
  71. package/core/domain/snapshot-manager.ts +25 -22
  72. package/core/domain/task-stack.ts +24 -14
  73. package/core/errors.ts +1 -1
  74. package/core/events/events.ts +2 -4
  75. package/core/events/index.ts +1 -2
  76. package/core/index.ts +28 -12
  77. package/core/infrastructure/agent-detector.ts +3 -3
  78. package/core/infrastructure/ai-provider.ts +23 -20
  79. package/core/infrastructure/author-detector.ts +16 -10
  80. package/core/infrastructure/capability-installer.ts +2 -2
  81. package/core/infrastructure/claude-agent.ts +6 -6
  82. package/core/infrastructure/command-installer.ts +22 -17
  83. package/core/infrastructure/config-manager.ts +18 -14
  84. package/core/infrastructure/editors-config.ts +8 -4
  85. package/core/infrastructure/path-manager.ts +8 -6
  86. package/core/infrastructure/permission-manager.ts +20 -17
  87. package/core/infrastructure/setup.ts +42 -38
  88. package/core/infrastructure/update-checker.ts +5 -5
  89. package/core/integrations/issue-tracker/enricher.ts +8 -19
  90. package/core/integrations/issue-tracker/index.ts +2 -2
  91. package/core/integrations/issue-tracker/manager.ts +15 -15
  92. package/core/integrations/issue-tracker/types.ts +5 -22
  93. package/core/integrations/jira/client.ts +67 -59
  94. package/core/integrations/jira/index.ts +11 -14
  95. package/core/integrations/jira/mcp-adapter.ts +5 -10
  96. package/core/integrations/jira/service.ts +10 -10
  97. package/core/integrations/linear/client.ts +27 -18
  98. package/core/integrations/linear/index.ts +9 -12
  99. package/core/integrations/linear/service.ts +11 -11
  100. package/core/integrations/linear/sync.ts +8 -8
  101. package/core/outcomes/analyzer.ts +5 -18
  102. package/core/outcomes/index.ts +2 -2
  103. package/core/outcomes/recorder.ts +3 -3
  104. package/core/plugin/builtin/webhook.ts +19 -15
  105. package/core/plugin/hooks.ts +29 -21
  106. package/core/plugin/index.ts +7 -7
  107. package/core/plugin/loader.ts +19 -19
  108. package/core/plugin/registry.ts +12 -23
  109. package/core/schemas/agents.ts +1 -1
  110. package/core/schemas/analysis.ts +1 -1
  111. package/core/schemas/enriched-task.ts +62 -49
  112. package/core/schemas/ideas.ts +13 -13
  113. package/core/schemas/index.ts +17 -27
  114. package/core/schemas/issues.ts +40 -25
  115. package/core/schemas/metrics.ts +143 -0
  116. package/core/schemas/outcomes.ts +70 -62
  117. package/core/schemas/permissions.ts +15 -12
  118. package/core/schemas/prd.ts +27 -14
  119. package/core/schemas/project.ts +3 -3
  120. package/core/schemas/roadmap.ts +47 -34
  121. package/core/schemas/schemas.ts +3 -4
  122. package/core/schemas/shipped.ts +3 -3
  123. package/core/schemas/state.ts +43 -29
  124. package/core/server/index.ts +5 -6
  125. package/core/server/routes-extended.ts +68 -72
  126. package/core/server/routes.ts +3 -3
  127. package/core/server/server.ts +31 -26
  128. package/core/services/agent-generator.ts +237 -0
  129. package/core/services/agent-service.ts +2 -2
  130. package/core/services/breakdown-service.ts +2 -4
  131. package/core/services/context-generator.ts +299 -0
  132. package/core/services/context-selector.ts +420 -0
  133. package/core/services/doctor-service.ts +426 -0
  134. package/core/services/file-categorizer.ts +448 -0
  135. package/core/services/file-scorer.ts +270 -0
  136. package/core/services/git-analyzer.ts +267 -0
  137. package/core/services/index.ts +27 -10
  138. package/core/services/memory-service.ts +3 -4
  139. package/core/services/project-index.ts +911 -0
  140. package/core/services/project-service.ts +4 -4
  141. package/core/services/skill-installer.ts +14 -17
  142. package/core/services/skill-lock.ts +3 -3
  143. package/core/services/skill-service.ts +12 -6
  144. package/core/services/stack-detector.ts +245 -0
  145. package/core/services/sync-service.ts +170 -329
  146. package/core/services/watch-service.ts +294 -0
  147. package/core/session/compaction.ts +23 -31
  148. package/core/session/index.ts +11 -5
  149. package/core/session/log-migration.ts +3 -3
  150. package/core/session/metrics.ts +19 -14
  151. package/core/session/session-log-manager.ts +12 -17
  152. package/core/session/task-session-manager.ts +25 -25
  153. package/core/session/utils.ts +1 -1
  154. package/core/storage/ideas-storage.ts +41 -57
  155. package/core/storage/index-storage.ts +514 -0
  156. package/core/storage/index.ts +41 -13
  157. package/core/storage/metrics-storage.ts +320 -0
  158. package/core/storage/queue-storage.ts +35 -45
  159. package/core/storage/shipped-storage.ts +17 -20
  160. package/core/storage/state-storage.ts +50 -30
  161. package/core/storage/storage-manager.ts +6 -6
  162. package/core/storage/storage.ts +18 -15
  163. package/core/sync/auth-config.ts +3 -3
  164. package/core/sync/index.ts +13 -19
  165. package/core/sync/oauth-handler.ts +3 -3
  166. package/core/sync/sync-client.ts +4 -9
  167. package/core/sync/sync-manager.ts +12 -14
  168. package/core/types/commands.ts +42 -7
  169. package/core/types/index.ts +284 -302
  170. package/core/types/integrations.ts +3 -3
  171. package/core/types/storage.ts +49 -0
  172. package/core/types/utils.ts +3 -3
  173. package/core/utils/agent-stream.ts +3 -1
  174. package/core/utils/animations.ts +14 -11
  175. package/core/utils/branding.ts +7 -7
  176. package/core/utils/cache.ts +1 -3
  177. package/core/utils/collection-filters.ts +3 -15
  178. package/core/utils/date-helper.ts +2 -7
  179. package/core/utils/file-helper.ts +13 -8
  180. package/core/utils/jsonl-helper.ts +13 -10
  181. package/core/utils/keychain.ts +4 -8
  182. package/core/utils/logger.ts +1 -1
  183. package/core/utils/next-steps.ts +3 -3
  184. package/core/utils/output.ts +58 -11
  185. package/core/utils/project-commands.ts +6 -6
  186. package/core/utils/project-credentials.ts +5 -12
  187. package/core/utils/runtime.ts +2 -2
  188. package/core/utils/session-helper.ts +3 -4
  189. package/core/utils/version.ts +3 -3
  190. package/core/wizard/index.ts +13 -0
  191. package/core/wizard/onboarding.ts +633 -0
  192. package/core/workflow/state-machine.ts +7 -7
  193. package/dist/bin/prjct.mjs +18907 -13189
  194. package/dist/core/infrastructure/command-installer.js +96 -111
  195. package/dist/core/infrastructure/editors-config.js +6 -6
  196. package/dist/core/infrastructure/setup.js +256 -257
  197. package/dist/core/utils/version.js +9 -9
  198. package/package.json +11 -12
  199. package/scripts/build.js +3 -3
  200. package/scripts/postinstall.js +2 -2
  201. package/templates/mcp-config.json +6 -1
  202. package/templates/permissions/permissive.jsonc +1 -1
  203. package/templates/permissions/strict.jsonc +5 -9
  204. package/templates/global/docs/agents.md +0 -88
  205. package/templates/global/docs/architecture.md +0 -103
  206. package/templates/global/docs/commands.md +0 -96
  207. package/templates/global/docs/validation.md +0 -95
@@ -0,0 +1,583 @@
1
+ /**
2
+ * Files Tool - Find relevant files for a task
3
+ *
4
+ * Scoring algorithm:
5
+ * - 60% Keywords in path/filename
6
+ * - 20% Domain patterns (frontend/backend/etc)
7
+ * - 15% Git recency (recently modified files)
8
+ * - 5% Import distance (proximity to entry points)
9
+ *
10
+ * @module context-tools/files-tool
11
+ * @version 1.0.0
12
+ */
13
+
14
+ import { exec as execCallback } from 'node:child_process'
15
+ import fs from 'node:fs/promises'
16
+ import path from 'node:path'
17
+ import { promisify } from 'node:util'
18
+ import { isNotFoundError } from '../types/fs'
19
+ import type { FilesToolOutput, ScoredFile, ScoreReason } from './types'
20
+
21
+ const exec = promisify(execCallback)
22
+
23
+ // =============================================================================
24
+ // Domain Keywords
25
+ // =============================================================================
26
+
27
+ /**
28
+ * Domain keywords for classification
29
+ * Used to match file paths against domain patterns
30
+ */
31
+ const DOMAIN_KEYWORDS: Record<string, string[]> = {
32
+ frontend: [
33
+ 'component',
34
+ 'page',
35
+ 'view',
36
+ 'ui',
37
+ 'layout',
38
+ 'style',
39
+ 'css',
40
+ 'scss',
41
+ 'sass',
42
+ 'hook',
43
+ 'context',
44
+ 'store',
45
+ 'redux',
46
+ 'zustand',
47
+ 'react',
48
+ 'vue',
49
+ 'svelte',
50
+ 'angular',
51
+ 'next',
52
+ 'nuxt',
53
+ 'app',
54
+ 'client',
55
+ ],
56
+ backend: [
57
+ 'api',
58
+ 'route',
59
+ 'controller',
60
+ 'service',
61
+ 'middleware',
62
+ 'handler',
63
+ 'resolver',
64
+ 'schema',
65
+ 'model',
66
+ 'entity',
67
+ 'repository',
68
+ 'server',
69
+ 'socket',
70
+ 'graphql',
71
+ 'rest',
72
+ 'trpc',
73
+ ],
74
+ database: [
75
+ 'migration',
76
+ 'seed',
77
+ 'schema',
78
+ 'model',
79
+ 'entity',
80
+ 'repository',
81
+ 'prisma',
82
+ 'drizzle',
83
+ 'sequelize',
84
+ 'typeorm',
85
+ 'mongoose',
86
+ 'knex',
87
+ 'sql',
88
+ 'db',
89
+ ],
90
+ auth: [
91
+ 'auth',
92
+ 'login',
93
+ 'logout',
94
+ 'session',
95
+ 'token',
96
+ 'jwt',
97
+ 'oauth',
98
+ 'passport',
99
+ 'credential',
100
+ 'permission',
101
+ 'role',
102
+ 'user',
103
+ 'account',
104
+ ],
105
+ testing: [
106
+ 'test',
107
+ 'spec',
108
+ 'e2e',
109
+ 'integration',
110
+ 'unit',
111
+ 'mock',
112
+ 'fixture',
113
+ 'stub',
114
+ 'jest',
115
+ 'vitest',
116
+ 'cypress',
117
+ 'playwright',
118
+ ],
119
+ config: [
120
+ 'config',
121
+ 'env',
122
+ 'setting',
123
+ 'constant',
124
+ 'option',
125
+ 'tsconfig',
126
+ 'eslint',
127
+ 'prettier',
128
+ 'vite',
129
+ 'webpack',
130
+ 'rollup',
131
+ ],
132
+ infra: [
133
+ 'docker',
134
+ 'compose',
135
+ 'kubernetes',
136
+ 'k8s',
137
+ 'ci',
138
+ 'cd',
139
+ 'github',
140
+ 'gitlab',
141
+ 'jenkins',
142
+ 'terraform',
143
+ 'ansible',
144
+ 'deploy',
145
+ ],
146
+ util: ['util', 'helper', 'lib', 'common', 'shared', 'core', 'base', 'abstract'],
147
+ }
148
+
149
+ /**
150
+ * Common code file extensions
151
+ */
152
+ const CODE_EXTENSIONS = new Set([
153
+ '.ts',
154
+ '.tsx',
155
+ '.js',
156
+ '.jsx',
157
+ '.mjs',
158
+ '.cjs',
159
+ '.py',
160
+ '.go',
161
+ '.rs',
162
+ '.java',
163
+ '.kt',
164
+ '.swift',
165
+ '.rb',
166
+ '.php',
167
+ '.c',
168
+ '.cpp',
169
+ '.h',
170
+ '.hpp',
171
+ '.cs',
172
+ '.vue',
173
+ '.svelte',
174
+ ])
175
+
176
+ /**
177
+ * Directories to ignore
178
+ */
179
+ const IGNORE_DIRS = new Set([
180
+ 'node_modules',
181
+ '.git',
182
+ 'dist',
183
+ 'build',
184
+ '.next',
185
+ '.nuxt',
186
+ '.output',
187
+ 'coverage',
188
+ '.cache',
189
+ '__pycache__',
190
+ '.pytest_cache',
191
+ 'vendor',
192
+ 'target',
193
+ '.turbo',
194
+ '.vercel',
195
+ ])
196
+
197
+ // =============================================================================
198
+ // Main Function
199
+ // =============================================================================
200
+
201
+ /**
202
+ * Find files relevant to a task description
203
+ *
204
+ * @param taskDescription - Natural language description of the task
205
+ * @param projectPath - Path to the project root
206
+ * @param options - Configuration options
207
+ * @returns Scored files sorted by relevance
208
+ */
209
+ export async function findRelevantFiles(
210
+ taskDescription: string,
211
+ projectPath: string,
212
+ options: {
213
+ maxFiles?: number
214
+ minScore?: number
215
+ includeTests?: boolean
216
+ } = {}
217
+ ): Promise<FilesToolOutput> {
218
+ const startTime = Date.now()
219
+ const maxFiles = options.maxFiles ?? 30
220
+ const minScore = options.minScore ?? 0.1
221
+ const includeTests = options.includeTests ?? false
222
+
223
+ // Extract keywords from task description
224
+ const keywords = extractKeywords(taskDescription)
225
+
226
+ // Get all code files
227
+ const allFiles = await getAllCodeFiles(projectPath)
228
+
229
+ // Get git recency data
230
+ const gitRecency = await getGitRecency(projectPath)
231
+
232
+ // Score each file
233
+ const scoredFiles: ScoredFile[] = []
234
+
235
+ for (const filePath of allFiles) {
236
+ // Skip test files if not requested
237
+ if (!includeTests && isTestFile(filePath)) {
238
+ continue
239
+ }
240
+
241
+ const score = scoreFile(filePath, keywords, gitRecency)
242
+
243
+ if (score.score >= minScore) {
244
+ scoredFiles.push(score)
245
+ }
246
+ }
247
+
248
+ // Sort by score descending
249
+ scoredFiles.sort((a, b) => b.score - a.score)
250
+
251
+ // Limit results
252
+ const topFiles = scoredFiles.slice(0, maxFiles)
253
+
254
+ return {
255
+ files: topFiles,
256
+ metrics: {
257
+ filesScanned: allFiles.length,
258
+ filesReturned: topFiles.length,
259
+ scanDuration: Date.now() - startTime,
260
+ },
261
+ }
262
+ }
263
+
264
+ // =============================================================================
265
+ // Helper Functions
266
+ // =============================================================================
267
+
268
+ /**
269
+ * Extract keywords from task description
270
+ */
271
+ function extractKeywords(description: string): string[] {
272
+ // Convert to lowercase and split by non-word characters
273
+ const words = description
274
+ .toLowerCase()
275
+ .split(/[^a-z0-9]+/)
276
+ .filter(Boolean)
277
+
278
+ // Remove common stop words
279
+ const stopWords = new Set([
280
+ 'a',
281
+ 'an',
282
+ 'the',
283
+ 'and',
284
+ 'or',
285
+ 'but',
286
+ 'is',
287
+ 'are',
288
+ 'was',
289
+ 'were',
290
+ 'be',
291
+ 'been',
292
+ 'being',
293
+ 'have',
294
+ 'has',
295
+ 'had',
296
+ 'do',
297
+ 'does',
298
+ 'did',
299
+ 'will',
300
+ 'would',
301
+ 'could',
302
+ 'should',
303
+ 'may',
304
+ 'might',
305
+ 'must',
306
+ 'shall',
307
+ 'can',
308
+ 'need',
309
+ 'to',
310
+ 'of',
311
+ 'in',
312
+ 'for',
313
+ 'on',
314
+ 'with',
315
+ 'at',
316
+ 'by',
317
+ 'from',
318
+ 'as',
319
+ 'into',
320
+ 'through',
321
+ 'during',
322
+ 'before',
323
+ 'after',
324
+ 'above',
325
+ 'below',
326
+ 'between',
327
+ 'under',
328
+ 'again',
329
+ 'further',
330
+ 'then',
331
+ 'once',
332
+ 'here',
333
+ 'there',
334
+ 'when',
335
+ 'where',
336
+ 'why',
337
+ 'how',
338
+ 'all',
339
+ 'each',
340
+ 'few',
341
+ 'more',
342
+ 'most',
343
+ 'other',
344
+ 'some',
345
+ 'such',
346
+ 'no',
347
+ 'nor',
348
+ 'not',
349
+ 'only',
350
+ 'own',
351
+ 'same',
352
+ 'so',
353
+ 'than',
354
+ 'too',
355
+ 'very',
356
+ 'just',
357
+ 'add',
358
+ 'create',
359
+ 'make',
360
+ 'implement',
361
+ 'fix',
362
+ 'update',
363
+ 'change',
364
+ 'modify',
365
+ 'remove',
366
+ 'delete',
367
+ 'new',
368
+ ])
369
+
370
+ return words.filter((w) => !stopWords.has(w) && w.length > 2)
371
+ }
372
+
373
+ /**
374
+ * Get all code files in the project
375
+ */
376
+ async function getAllCodeFiles(projectPath: string): Promise<string[]> {
377
+ const files: string[] = []
378
+
379
+ async function walk(dir: string, relativePath: string = ''): Promise<void> {
380
+ try {
381
+ const entries = await fs.readdir(dir, { withFileTypes: true })
382
+
383
+ for (const entry of entries) {
384
+ const fullPath = path.join(dir, entry.name)
385
+ const relPath = path.join(relativePath, entry.name)
386
+
387
+ if (entry.isDirectory()) {
388
+ // Skip ignored directories
389
+ if (IGNORE_DIRS.has(entry.name) || entry.name.startsWith('.')) {
390
+ continue
391
+ }
392
+ await walk(fullPath, relPath)
393
+ } else if (entry.isFile()) {
394
+ const ext = path.extname(entry.name).toLowerCase()
395
+ if (CODE_EXTENSIONS.has(ext)) {
396
+ files.push(relPath)
397
+ }
398
+ }
399
+ }
400
+ } catch (error) {
401
+ if (!isNotFoundError(error)) {
402
+ // Log but continue on permission errors, etc.
403
+ }
404
+ }
405
+ }
406
+
407
+ await walk(projectPath)
408
+ return files
409
+ }
410
+
411
+ /**
412
+ * Get git recency information
413
+ */
414
+ async function getGitRecency(
415
+ projectPath: string
416
+ ): Promise<Map<string, { commits: number; daysAgo: number }>> {
417
+ const recency = new Map<string, { commits: number; daysAgo: number }>()
418
+
419
+ try {
420
+ // Get files changed in last 30 commits with their commit counts
421
+ const { stdout } = await exec(
422
+ `git log -30 --pretty=format:"%H %ct" --name-only | awk '
423
+ /^[a-f0-9]{40}/ { commit=$1; timestamp=$2; next }
424
+ NF { files[$0]++; if (!lastmod[$0]) lastmod[$0]=timestamp }
425
+ END { for (f in files) print files[f], lastmod[f], f }
426
+ '`,
427
+ { cwd: projectPath, maxBuffer: 10 * 1024 * 1024 }
428
+ )
429
+
430
+ const now = Math.floor(Date.now() / 1000)
431
+ const lines = stdout.trim().split('\n').filter(Boolean)
432
+
433
+ for (const line of lines) {
434
+ const match = line.match(/^(\d+)\s+(\d+)\s+(.+)$/)
435
+ if (match) {
436
+ const commits = parseInt(match[1], 10)
437
+ const timestamp = parseInt(match[2], 10)
438
+ const file = match[3]
439
+ const daysAgo = Math.floor((now - timestamp) / 86400)
440
+ recency.set(file, { commits, daysAgo })
441
+ }
442
+ }
443
+ } catch (_error) {
444
+ // Git not available or not a git repo
445
+ }
446
+
447
+ return recency
448
+ }
449
+
450
+ /**
451
+ * Score a file based on relevance
452
+ */
453
+ function scoreFile(
454
+ filePath: string,
455
+ keywords: string[],
456
+ gitRecency: Map<string, { commits: number; daysAgo: number }>
457
+ ): ScoredFile {
458
+ const reasons: ScoreReason[] = []
459
+ let keywordScore = 0
460
+ let domainScore = 0
461
+ let recencyScore = 0
462
+ let importScore = 0
463
+
464
+ const pathLower = filePath.toLowerCase()
465
+ const pathParts = pathLower
466
+ .split('/')
467
+ .join(' ')
468
+ .split(/[^a-z0-9]+/)
469
+
470
+ // Keyword matching (60% weight)
471
+ for (const keyword of keywords) {
472
+ if (pathLower.includes(keyword)) {
473
+ keywordScore += 0.3
474
+ reasons.push(`keyword:${keyword}`)
475
+ }
476
+ // Partial match in path parts
477
+ for (const part of pathParts) {
478
+ if (part.includes(keyword) || keyword.includes(part)) {
479
+ keywordScore += 0.15
480
+ break
481
+ }
482
+ }
483
+ }
484
+ keywordScore = Math.min(1, keywordScore)
485
+
486
+ // Domain matching (20% weight)
487
+ for (const [domain, domainKeywords] of Object.entries(DOMAIN_KEYWORDS)) {
488
+ for (const domainKw of domainKeywords) {
489
+ if (pathLower.includes(domainKw)) {
490
+ // Check if any task keyword relates to this domain
491
+ const taskRelatesToDomain = keywords.some(
492
+ (k) => domainKeywords.includes(k) || k.includes(domain) || domain.includes(k)
493
+ )
494
+ if (taskRelatesToDomain) {
495
+ domainScore += 0.4
496
+ reasons.push(`domain:${domain}`)
497
+ break
498
+ }
499
+ }
500
+ }
501
+ }
502
+ domainScore = Math.min(1, domainScore)
503
+
504
+ // Git recency (15% weight)
505
+ const recencyData = gitRecency.get(filePath)
506
+ if (recencyData) {
507
+ // More recent = higher score
508
+ if (recencyData.daysAgo <= 1) {
509
+ recencyScore = 1.0
510
+ reasons.push('recent:1d')
511
+ } else if (recencyData.daysAgo <= 3) {
512
+ recencyScore = 0.8
513
+ reasons.push('recent:3d')
514
+ } else if (recencyData.daysAgo <= 7) {
515
+ recencyScore = 0.6
516
+ reasons.push('recent:1w')
517
+ } else if (recencyData.daysAgo <= 30) {
518
+ recencyScore = 0.3
519
+ reasons.push('recent:1m')
520
+ }
521
+
522
+ // Bonus for frequently changed files
523
+ if (recencyData.commits >= 5) {
524
+ recencyScore = Math.min(1, recencyScore + 0.2)
525
+ }
526
+ }
527
+
528
+ // Import distance - simplified heuristic (5% weight)
529
+ // Entry points (index, main, app) get bonus
530
+ const filename = path.basename(filePath).toLowerCase()
531
+ if (
532
+ filename.includes('index') ||
533
+ filename.includes('main') ||
534
+ filename.includes('app') ||
535
+ filename.includes('entry')
536
+ ) {
537
+ importScore = 0.5
538
+ reasons.push('import:0')
539
+ }
540
+ // Core/shared files get some bonus
541
+ if (
542
+ pathLower.includes('/core/') ||
543
+ pathLower.includes('/shared/') ||
544
+ pathLower.includes('/lib/')
545
+ ) {
546
+ importScore = Math.max(importScore, 0.3)
547
+ if (!reasons.some((r) => r.startsWith('import:'))) {
548
+ reasons.push('import:1')
549
+ }
550
+ }
551
+
552
+ // Calculate weighted score
553
+ const score = keywordScore * 0.6 + domainScore * 0.2 + recencyScore * 0.15 + importScore * 0.05
554
+
555
+ return {
556
+ path: filePath,
557
+ score: Math.min(1, score),
558
+ reasons: [...new Set(reasons)], // Dedupe
559
+ }
560
+ }
561
+
562
+ /**
563
+ * Check if a file is a test file
564
+ */
565
+ function isTestFile(filePath: string): boolean {
566
+ const lower = filePath.toLowerCase()
567
+ return (
568
+ lower.includes('.test.') ||
569
+ lower.includes('.spec.') ||
570
+ lower.includes('__tests__') ||
571
+ lower.includes('__mocks__') ||
572
+ lower.includes('/tests/') ||
573
+ lower.includes('/test/') ||
574
+ lower.endsWith('_test.go') ||
575
+ lower.endsWith('_test.py')
576
+ )
577
+ }
578
+
579
+ // =============================================================================
580
+ // Exports
581
+ // =============================================================================
582
+
583
+ export default { findRelevantFiles }