prjct-cli 0.45.0 → 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.
- package/CHANGELOG.md +75 -0
- package/bin/prjct.ts +117 -10
- package/core/__tests__/agentic/memory-system.test.ts +39 -26
- package/core/__tests__/agentic/plan-mode.test.ts +64 -46
- package/core/__tests__/agentic/prompt-builder.test.ts +14 -14
- package/core/__tests__/services/project-index.test.ts +353 -0
- package/core/__tests__/types/fs.test.ts +3 -3
- package/core/__tests__/utils/date-helper.test.ts +10 -10
- package/core/__tests__/utils/output.test.ts +9 -6
- package/core/__tests__/utils/project-commands.test.ts +5 -6
- package/core/agentic/agent-router.ts +9 -10
- package/core/agentic/chain-of-thought.ts +16 -4
- package/core/agentic/command-executor.ts +66 -40
- package/core/agentic/context-builder.ts +8 -5
- package/core/agentic/ground-truth.ts +15 -9
- package/core/agentic/index.ts +145 -152
- package/core/agentic/loop-detector.ts +40 -11
- package/core/agentic/memory-system.ts +98 -35
- package/core/agentic/orchestrator-executor.ts +135 -71
- package/core/agentic/plan-mode.ts +46 -16
- package/core/agentic/prompt-builder.ts +108 -42
- package/core/agentic/services.ts +10 -9
- package/core/agentic/skill-loader.ts +9 -15
- package/core/agentic/smart-context.ts +129 -79
- package/core/agentic/template-executor.ts +13 -12
- package/core/agentic/template-loader.ts +7 -4
- package/core/agentic/tool-registry.ts +16 -13
- package/core/agents/index.ts +1 -1
- package/core/agents/performance.ts +10 -27
- package/core/ai-tools/formatters.ts +8 -6
- package/core/ai-tools/generator.ts +4 -4
- package/core/ai-tools/index.ts +1 -1
- package/core/ai-tools/registry.ts +21 -11
- package/core/bus/bus.ts +23 -16
- package/core/bus/index.ts +2 -2
- package/core/cli/linear.ts +3 -5
- package/core/cli/start.ts +28 -25
- package/core/commands/analysis.ts +58 -39
- package/core/commands/analytics.ts +52 -44
- package/core/commands/base.ts +15 -13
- package/core/commands/cleanup.ts +6 -13
- package/core/commands/command-data.ts +28 -4
- package/core/commands/commands.ts +57 -24
- package/core/commands/context.ts +4 -4
- package/core/commands/design.ts +3 -10
- package/core/commands/index.ts +5 -8
- package/core/commands/maintenance.ts +7 -4
- package/core/commands/planning.ts +179 -56
- package/core/commands/register.ts +13 -9
- package/core/commands/registry.ts +15 -14
- package/core/commands/setup.ts +26 -14
- package/core/commands/shipping.ts +11 -16
- package/core/commands/snapshots.ts +16 -32
- package/core/commands/uninstall.ts +541 -0
- package/core/commands/workflow.ts +24 -28
- package/core/constants/index.ts +10 -22
- package/core/context/generator.ts +82 -33
- package/core/context-tools/files-tool.ts +18 -19
- package/core/context-tools/imports-tool.ts +13 -33
- package/core/context-tools/index.ts +29 -54
- package/core/context-tools/recent-tool.ts +16 -22
- package/core/context-tools/signatures-tool.ts +17 -26
- package/core/context-tools/summary-tool.ts +20 -22
- package/core/context-tools/token-counter.ts +25 -20
- package/core/context-tools/types.ts +5 -5
- package/core/domain/agent-generator.ts +7 -5
- package/core/domain/agent-loader.ts +2 -2
- package/core/domain/analyzer.ts +19 -16
- package/core/domain/architecture-generator.ts +6 -3
- package/core/domain/context-estimator.ts +3 -4
- package/core/domain/snapshot-manager.ts +25 -22
- package/core/domain/task-stack.ts +24 -14
- package/core/errors.ts +1 -1
- package/core/events/events.ts +2 -4
- package/core/events/index.ts +1 -2
- package/core/index.ts +28 -16
- package/core/infrastructure/agent-detector.ts +3 -3
- package/core/infrastructure/ai-provider.ts +23 -20
- package/core/infrastructure/author-detector.ts +16 -10
- package/core/infrastructure/capability-installer.ts +2 -2
- package/core/infrastructure/claude-agent.ts +6 -6
- package/core/infrastructure/command-installer.ts +22 -17
- package/core/infrastructure/config-manager.ts +18 -14
- package/core/infrastructure/editors-config.ts +8 -4
- package/core/infrastructure/path-manager.ts +8 -6
- package/core/infrastructure/permission-manager.ts +20 -17
- package/core/infrastructure/setup.ts +42 -38
- package/core/infrastructure/update-checker.ts +5 -5
- package/core/integrations/issue-tracker/enricher.ts +8 -19
- package/core/integrations/issue-tracker/index.ts +2 -2
- package/core/integrations/issue-tracker/manager.ts +15 -15
- package/core/integrations/issue-tracker/types.ts +5 -22
- package/core/integrations/jira/client.ts +67 -59
- package/core/integrations/jira/index.ts +11 -14
- package/core/integrations/jira/mcp-adapter.ts +5 -10
- package/core/integrations/jira/service.ts +10 -10
- package/core/integrations/linear/client.ts +27 -18
- package/core/integrations/linear/index.ts +9 -12
- package/core/integrations/linear/service.ts +11 -11
- package/core/integrations/linear/sync.ts +8 -8
- package/core/outcomes/analyzer.ts +5 -18
- package/core/outcomes/index.ts +2 -2
- package/core/outcomes/recorder.ts +3 -3
- package/core/plugin/builtin/webhook.ts +19 -15
- package/core/plugin/hooks.ts +29 -21
- package/core/plugin/index.ts +7 -7
- package/core/plugin/loader.ts +19 -19
- package/core/plugin/registry.ts +12 -23
- package/core/schemas/agents.ts +1 -1
- package/core/schemas/analysis.ts +1 -1
- package/core/schemas/enriched-task.ts +62 -49
- package/core/schemas/ideas.ts +13 -13
- package/core/schemas/index.ts +17 -27
- package/core/schemas/issues.ts +40 -25
- package/core/schemas/metrics.ts +25 -25
- package/core/schemas/outcomes.ts +70 -62
- package/core/schemas/permissions.ts +15 -12
- package/core/schemas/prd.ts +27 -14
- package/core/schemas/project.ts +3 -3
- package/core/schemas/roadmap.ts +47 -34
- package/core/schemas/schemas.ts +3 -4
- package/core/schemas/shipped.ts +3 -3
- package/core/schemas/state.ts +43 -29
- package/core/server/index.ts +5 -6
- package/core/server/routes-extended.ts +68 -72
- package/core/server/routes.ts +3 -3
- package/core/server/server.ts +31 -26
- package/core/services/agent-generator.ts +237 -0
- package/core/services/agent-service.ts +2 -2
- package/core/services/breakdown-service.ts +2 -4
- package/core/services/context-generator.ts +299 -0
- package/core/services/context-selector.ts +420 -0
- package/core/services/doctor-service.ts +426 -0
- package/core/services/file-categorizer.ts +448 -0
- package/core/services/file-scorer.ts +270 -0
- package/core/services/git-analyzer.ts +267 -0
- package/core/services/index.ts +27 -10
- package/core/services/memory-service.ts +3 -4
- package/core/services/project-index.ts +911 -0
- package/core/services/project-service.ts +4 -4
- package/core/services/skill-installer.ts +14 -17
- package/core/services/skill-lock.ts +3 -3
- package/core/services/skill-service.ts +12 -6
- package/core/services/stack-detector.ts +245 -0
- package/core/services/sync-service.ts +87 -345
- package/core/services/watch-service.ts +294 -0
- package/core/session/compaction.ts +23 -31
- package/core/session/index.ts +11 -5
- package/core/session/log-migration.ts +3 -3
- package/core/session/metrics.ts +19 -14
- package/core/session/session-log-manager.ts +12 -17
- package/core/session/task-session-manager.ts +25 -25
- package/core/session/utils.ts +1 -1
- package/core/storage/ideas-storage.ts +41 -57
- package/core/storage/index-storage.ts +514 -0
- package/core/storage/index.ts +41 -17
- package/core/storage/metrics-storage.ts +39 -34
- package/core/storage/queue-storage.ts +35 -45
- package/core/storage/shipped-storage.ts +17 -20
- package/core/storage/state-storage.ts +50 -30
- package/core/storage/storage-manager.ts +6 -6
- package/core/storage/storage.ts +18 -15
- package/core/sync/auth-config.ts +3 -3
- package/core/sync/index.ts +13 -19
- package/core/sync/oauth-handler.ts +3 -3
- package/core/sync/sync-client.ts +4 -9
- package/core/sync/sync-manager.ts +12 -14
- package/core/types/commands.ts +42 -7
- package/core/types/index.ts +284 -305
- package/core/types/integrations.ts +3 -3
- package/core/types/storage.ts +14 -14
- package/core/types/utils.ts +3 -3
- package/core/utils/agent-stream.ts +3 -1
- package/core/utils/animations.ts +14 -11
- package/core/utils/branding.ts +7 -7
- package/core/utils/cache.ts +1 -3
- package/core/utils/collection-filters.ts +3 -15
- package/core/utils/date-helper.ts +2 -7
- package/core/utils/file-helper.ts +13 -8
- package/core/utils/jsonl-helper.ts +13 -10
- package/core/utils/keychain.ts +4 -8
- package/core/utils/logger.ts +1 -1
- package/core/utils/next-steps.ts +3 -3
- package/core/utils/output.ts +58 -11
- package/core/utils/project-commands.ts +6 -6
- package/core/utils/project-credentials.ts +5 -12
- package/core/utils/runtime.ts +2 -2
- package/core/utils/session-helper.ts +3 -4
- package/core/utils/version.ts +3 -3
- package/core/wizard/index.ts +13 -0
- package/core/wizard/onboarding.ts +633 -0
- package/core/workflow/state-machine.ts +7 -7
- package/dist/bin/prjct.mjs +18755 -15574
- package/dist/core/infrastructure/command-installer.js +86 -79
- package/dist/core/infrastructure/editors-config.js +6 -6
- package/dist/core/infrastructure/setup.js +246 -225
- package/dist/core/utils/version.js +9 -9
- package/package.json +11 -12
- package/scripts/build.js +3 -3
- package/scripts/postinstall.js +2 -2
- package/templates/mcp-config.json +6 -1
- package/templates/permissions/permissive.jsonc +1 -1
- package/templates/permissions/strict.jsonc +5 -9
- package/templates/global/docs/agents.md +0 -88
- package/templates/global/docs/architecture.md +0 -103
- package/templates/global/docs/commands.md +0 -96
- package/templates/global/docs/validation.md +0 -95
|
@@ -0,0 +1,911 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* ProjectIndex - Persistent Project Scanner with Scoring
|
|
3
|
+
*
|
|
4
|
+
* Features:
|
|
5
|
+
* - Full scan: Analyzes entire project, caches results
|
|
6
|
+
* - Incremental update: Only re-scans changed files
|
|
7
|
+
* - Relevance scoring: Prioritizes important files
|
|
8
|
+
* - Pattern detection: Identifies project architecture
|
|
9
|
+
*
|
|
10
|
+
* Usage:
|
|
11
|
+
* - sync-service calls fullScan() on first sync
|
|
12
|
+
* - Subsequent syncs use incrementalUpdate() or cached index
|
|
13
|
+
* - watch-service uses incrementalUpdate() for changed files
|
|
14
|
+
*
|
|
15
|
+
* Storage location: ~/.prjct-cli/projects/{projectId}/index/
|
|
16
|
+
*/
|
|
17
|
+
|
|
18
|
+
import { exec } from 'node:child_process'
|
|
19
|
+
import fs from 'node:fs/promises'
|
|
20
|
+
import path from 'node:path'
|
|
21
|
+
import { promisify } from 'node:util'
|
|
22
|
+
import {
|
|
23
|
+
type ConfigFileEntry,
|
|
24
|
+
type DetectedPattern,
|
|
25
|
+
type DetectedStack,
|
|
26
|
+
type DirectoryEntry,
|
|
27
|
+
getDefaultIndex,
|
|
28
|
+
INDEX_VERSION,
|
|
29
|
+
indexStorage,
|
|
30
|
+
type LanguageStats,
|
|
31
|
+
type ProjectIndex,
|
|
32
|
+
type ScoredFile,
|
|
33
|
+
} from '../storage/index-storage'
|
|
34
|
+
import { getTimestamp } from '../utils/date-helper'
|
|
35
|
+
import { type FileStats, fileScorer, RELEVANCE_THRESHOLD, type ScoringContext } from './file-scorer'
|
|
36
|
+
|
|
37
|
+
const execAsync = promisify(exec)
|
|
38
|
+
|
|
39
|
+
// ============================================================================
|
|
40
|
+
// TYPES
|
|
41
|
+
// ============================================================================
|
|
42
|
+
|
|
43
|
+
export interface IndexOptions {
|
|
44
|
+
forceFullScan?: boolean // Force full scan even if index exists
|
|
45
|
+
maxFiles?: number // Limit number of files to scan (for large repos)
|
|
46
|
+
excludePatterns?: string[] // Additional patterns to exclude
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
export interface ScanResult {
|
|
50
|
+
index: ProjectIndex
|
|
51
|
+
fromCache: boolean
|
|
52
|
+
changedFiles: number
|
|
53
|
+
scanDuration: number
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
export interface RelevantContext {
|
|
57
|
+
files: ScoredFile[]
|
|
58
|
+
estimatedTokens: number
|
|
59
|
+
originalTokens: number
|
|
60
|
+
compressionRate: number
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
// ============================================================================
|
|
64
|
+
// CONSTANTS
|
|
65
|
+
// ============================================================================
|
|
66
|
+
|
|
67
|
+
// Source file extensions to scan
|
|
68
|
+
const SOURCE_EXTENSIONS = new Set([
|
|
69
|
+
'.ts',
|
|
70
|
+
'.tsx',
|
|
71
|
+
'.js',
|
|
72
|
+
'.jsx',
|
|
73
|
+
'.mjs',
|
|
74
|
+
'.cjs', // JavaScript/TypeScript
|
|
75
|
+
'.py',
|
|
76
|
+
'.pyw', // Python
|
|
77
|
+
'.go', // Go
|
|
78
|
+
'.rs', // Rust
|
|
79
|
+
'.java',
|
|
80
|
+
'.kt',
|
|
81
|
+
'.scala', // JVM
|
|
82
|
+
'.c',
|
|
83
|
+
'.cpp',
|
|
84
|
+
'.h',
|
|
85
|
+
'.hpp', // C/C++
|
|
86
|
+
'.rb', // Ruby
|
|
87
|
+
'.php', // PHP
|
|
88
|
+
'.swift', // Swift
|
|
89
|
+
'.cs', // C#
|
|
90
|
+
'.vue',
|
|
91
|
+
'.svelte', // Frontend frameworks
|
|
92
|
+
])
|
|
93
|
+
|
|
94
|
+
// Config file names to track
|
|
95
|
+
const CONFIG_FILES = new Set([
|
|
96
|
+
'package.json',
|
|
97
|
+
'tsconfig.json',
|
|
98
|
+
'vite.config.ts',
|
|
99
|
+
'vite.config.js',
|
|
100
|
+
'next.config.js',
|
|
101
|
+
'next.config.mjs',
|
|
102
|
+
'next.config.ts',
|
|
103
|
+
'webpack.config.js',
|
|
104
|
+
'rollup.config.js',
|
|
105
|
+
'esbuild.config.js',
|
|
106
|
+
'jest.config.js',
|
|
107
|
+
'jest.config.ts',
|
|
108
|
+
'vitest.config.ts',
|
|
109
|
+
'vitest.config.js',
|
|
110
|
+
'tailwind.config.js',
|
|
111
|
+
'tailwind.config.ts',
|
|
112
|
+
'postcss.config.js',
|
|
113
|
+
'.eslintrc',
|
|
114
|
+
'.eslintrc.js',
|
|
115
|
+
'.eslintrc.json',
|
|
116
|
+
'.prettierrc',
|
|
117
|
+
'.prettierrc.js',
|
|
118
|
+
'.prettierrc.json',
|
|
119
|
+
'Cargo.toml',
|
|
120
|
+
'go.mod',
|
|
121
|
+
'pyproject.toml',
|
|
122
|
+
'requirements.txt',
|
|
123
|
+
'setup.py',
|
|
124
|
+
'Dockerfile',
|
|
125
|
+
'docker-compose.yml',
|
|
126
|
+
'docker-compose.yaml',
|
|
127
|
+
'.env',
|
|
128
|
+
'.env.local',
|
|
129
|
+
'.env.development',
|
|
130
|
+
'.env.production',
|
|
131
|
+
])
|
|
132
|
+
|
|
133
|
+
// Directories to ignore
|
|
134
|
+
const IGNORE_DIRS = new Set([
|
|
135
|
+
'node_modules',
|
|
136
|
+
'.git',
|
|
137
|
+
'.next',
|
|
138
|
+
'.nuxt',
|
|
139
|
+
'dist',
|
|
140
|
+
'build',
|
|
141
|
+
'out',
|
|
142
|
+
'coverage',
|
|
143
|
+
'.turbo',
|
|
144
|
+
'.cache',
|
|
145
|
+
'.parcel-cache',
|
|
146
|
+
'__pycache__',
|
|
147
|
+
'.pytest_cache',
|
|
148
|
+
'target', // Rust
|
|
149
|
+
'vendor', // Go/PHP
|
|
150
|
+
'.venv',
|
|
151
|
+
'venv', // Python
|
|
152
|
+
'eggs',
|
|
153
|
+
'*.egg-info',
|
|
154
|
+
])
|
|
155
|
+
|
|
156
|
+
// Directory type detection patterns
|
|
157
|
+
const DIR_TYPE_PATTERNS: { type: DirectoryEntry['type']; patterns: RegExp[] }[] = [
|
|
158
|
+
{ type: 'test', patterns: [/^tests?$/i, /^__tests__$/i, /^spec$/i, /^e2e$/i] },
|
|
159
|
+
{
|
|
160
|
+
type: 'source',
|
|
161
|
+
patterns: [
|
|
162
|
+
/^src$/i,
|
|
163
|
+
/^lib$/i,
|
|
164
|
+
/^core$/i,
|
|
165
|
+
/^app$/i,
|
|
166
|
+
/^pages$/i,
|
|
167
|
+
/^components$/i,
|
|
168
|
+
/^services$/i,
|
|
169
|
+
/^utils$/i,
|
|
170
|
+
],
|
|
171
|
+
},
|
|
172
|
+
{ type: 'config', patterns: [/^config$/i, /^\.config$/i, /^settings$/i] },
|
|
173
|
+
{ type: 'build', patterns: [/^dist$/i, /^build$/i, /^out$/i, /^\.next$/i] },
|
|
174
|
+
{ type: 'vendor', patterns: [/^node_modules$/i, /^vendor$/i, /^packages$/i] },
|
|
175
|
+
{ type: 'docs', patterns: [/^docs?$/i, /^documentation$/i] },
|
|
176
|
+
]
|
|
177
|
+
|
|
178
|
+
// Pattern detection rules
|
|
179
|
+
const PATTERN_DETECTORS: {
|
|
180
|
+
name: string
|
|
181
|
+
detect: (index: ProjectIndex) => number
|
|
182
|
+
evidence: (index: ProjectIndex) => string[]
|
|
183
|
+
}[] = [
|
|
184
|
+
{
|
|
185
|
+
name: 'monorepo',
|
|
186
|
+
detect: (idx) => {
|
|
187
|
+
const hasWorkspaces = idx.configFiles.some(
|
|
188
|
+
(cf) => cf.type === 'package.json' && cf.parsed?.workspaces
|
|
189
|
+
)
|
|
190
|
+
const hasPackages = idx.directories.some((d) => d.path === 'packages' || d.path === 'apps')
|
|
191
|
+
return hasWorkspaces ? 0.9 : hasPackages ? 0.7 : 0
|
|
192
|
+
},
|
|
193
|
+
evidence: (idx) => {
|
|
194
|
+
const ev: string[] = []
|
|
195
|
+
if (idx.directories.some((d) => d.path === 'packages')) ev.push('packages/')
|
|
196
|
+
if (idx.directories.some((d) => d.path === 'apps')) ev.push('apps/')
|
|
197
|
+
return ev
|
|
198
|
+
},
|
|
199
|
+
},
|
|
200
|
+
{
|
|
201
|
+
name: 'api-first',
|
|
202
|
+
detect: (idx) => {
|
|
203
|
+
const hasApiDir = idx.directories.some(
|
|
204
|
+
(d) => d.path.includes('api') || d.path.includes('routes')
|
|
205
|
+
)
|
|
206
|
+
const hasOpenApi = idx.configFiles.some(
|
|
207
|
+
(cf) => cf.path.includes('openapi') || cf.path.includes('swagger')
|
|
208
|
+
)
|
|
209
|
+
return hasOpenApi ? 0.9 : hasApiDir ? 0.6 : 0
|
|
210
|
+
},
|
|
211
|
+
evidence: (idx) =>
|
|
212
|
+
idx.directories
|
|
213
|
+
.filter((d) => d.path.includes('api') || d.path.includes('routes'))
|
|
214
|
+
.map((d) => `${d.path}/`),
|
|
215
|
+
},
|
|
216
|
+
{
|
|
217
|
+
name: 'component-based',
|
|
218
|
+
detect: (idx) => {
|
|
219
|
+
const hasComponents = idx.directories.some((d) => d.path.includes('components'))
|
|
220
|
+
const hasReact = idx.detectedStack.frameworks.includes('React')
|
|
221
|
+
const hasVue = idx.detectedStack.frameworks.includes('Vue')
|
|
222
|
+
return hasComponents && (hasReact || hasVue) ? 0.8 : hasComponents ? 0.5 : 0
|
|
223
|
+
},
|
|
224
|
+
evidence: (idx) =>
|
|
225
|
+
idx.directories.filter((d) => d.path.includes('components')).map((d) => `${d.path}/`),
|
|
226
|
+
},
|
|
227
|
+
{
|
|
228
|
+
name: 'serverless',
|
|
229
|
+
detect: (idx) => {
|
|
230
|
+
const hasServerless = idx.configFiles.some(
|
|
231
|
+
(cf) =>
|
|
232
|
+
cf.path.includes('serverless') ||
|
|
233
|
+
cf.path.includes('netlify') ||
|
|
234
|
+
cf.path.includes('vercel')
|
|
235
|
+
)
|
|
236
|
+
const hasLambda = idx.directories.some(
|
|
237
|
+
(d) => d.path.includes('functions') || d.path.includes('lambda')
|
|
238
|
+
)
|
|
239
|
+
return hasServerless ? 0.9 : hasLambda ? 0.6 : 0
|
|
240
|
+
},
|
|
241
|
+
evidence: (idx) =>
|
|
242
|
+
idx.configFiles
|
|
243
|
+
.filter((cf) => cf.path.includes('serverless') || cf.path.includes('vercel'))
|
|
244
|
+
.map((cf) => cf.path),
|
|
245
|
+
},
|
|
246
|
+
]
|
|
247
|
+
|
|
248
|
+
// ============================================================================
|
|
249
|
+
// PROJECT INDEXER CLASS
|
|
250
|
+
// ============================================================================
|
|
251
|
+
|
|
252
|
+
export class ProjectIndexer {
|
|
253
|
+
private projectPath: string
|
|
254
|
+
private projectId: string
|
|
255
|
+
|
|
256
|
+
constructor(projectPath: string, projectId: string) {
|
|
257
|
+
this.projectPath = projectPath
|
|
258
|
+
this.projectId = projectId
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
// ==========================================================================
|
|
262
|
+
// MAIN METHODS
|
|
263
|
+
// ==========================================================================
|
|
264
|
+
|
|
265
|
+
/**
|
|
266
|
+
* Perform a full project scan
|
|
267
|
+
* Creates fresh index from scratch
|
|
268
|
+
*/
|
|
269
|
+
async fullScan(options: IndexOptions = {}): Promise<ScanResult> {
|
|
270
|
+
const startTime = Date.now()
|
|
271
|
+
|
|
272
|
+
// Create fresh index
|
|
273
|
+
const index = getDefaultIndex(this.projectPath)
|
|
274
|
+
|
|
275
|
+
// Scan all files
|
|
276
|
+
const allFiles = await this.scanAllFiles(options)
|
|
277
|
+
const filesArray = Array.from(allFiles.values())
|
|
278
|
+
|
|
279
|
+
// Build language stats
|
|
280
|
+
index.languages = this.buildLanguageStats(filesArray)
|
|
281
|
+
|
|
282
|
+
// Find and parse config files
|
|
283
|
+
index.configFiles = await this.findConfigFiles()
|
|
284
|
+
|
|
285
|
+
// Analyze directory structure
|
|
286
|
+
index.directories = await this.analyzeDirectories()
|
|
287
|
+
|
|
288
|
+
// Detect stack
|
|
289
|
+
index.detectedStack = await this.detectStack(index.configFiles)
|
|
290
|
+
|
|
291
|
+
// Calculate scores
|
|
292
|
+
const context = this.buildScoringContext(allFiles)
|
|
293
|
+
const scores = fileScorer.getRelevantFiles(context, RELEVANCE_THRESHOLD)
|
|
294
|
+
|
|
295
|
+
index.relevantFiles = scores.map((s) => ({
|
|
296
|
+
path: s.path,
|
|
297
|
+
score: s.score,
|
|
298
|
+
size: allFiles.get(s.path)?.size || 0,
|
|
299
|
+
mtime: allFiles.get(s.path)?.mtime.toISOString() || '',
|
|
300
|
+
}))
|
|
301
|
+
|
|
302
|
+
// Detect patterns
|
|
303
|
+
index.patterns = this.detectPatterns(index)
|
|
304
|
+
|
|
305
|
+
// Set metrics
|
|
306
|
+
index.totalFiles = allFiles.size
|
|
307
|
+
index.totalSize = filesArray.reduce((sum, f) => sum + f.size, 0)
|
|
308
|
+
index.totalLines = filesArray.reduce((sum, f) => sum + (f.lines || 0), 0)
|
|
309
|
+
index.scanDuration = Date.now() - startTime
|
|
310
|
+
|
|
311
|
+
// Set timestamps
|
|
312
|
+
const now = getTimestamp()
|
|
313
|
+
index.lastFullScan = now
|
|
314
|
+
index.lastIncrementalUpdate = now
|
|
315
|
+
|
|
316
|
+
// Persist
|
|
317
|
+
await indexStorage.writeIndex(this.projectId, index)
|
|
318
|
+
await this.saveChecksums(allFiles)
|
|
319
|
+
await indexStorage.writeScores(this.projectId, index.relevantFiles)
|
|
320
|
+
|
|
321
|
+
return {
|
|
322
|
+
index,
|
|
323
|
+
fromCache: false,
|
|
324
|
+
changedFiles: allFiles.size,
|
|
325
|
+
scanDuration: index.scanDuration,
|
|
326
|
+
}
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
/**
|
|
330
|
+
* Incremental update - only re-scan changed files
|
|
331
|
+
*/
|
|
332
|
+
async incrementalUpdate(changedPaths?: string[]): Promise<ScanResult> {
|
|
333
|
+
const startTime = Date.now()
|
|
334
|
+
|
|
335
|
+
// Load existing index
|
|
336
|
+
const index = await indexStorage.readIndex(this.projectId)
|
|
337
|
+
if (!index) {
|
|
338
|
+
// No index exists, do full scan
|
|
339
|
+
return this.fullScan()
|
|
340
|
+
}
|
|
341
|
+
|
|
342
|
+
// If specific paths provided, use those; otherwise detect changes
|
|
343
|
+
let filesToUpdate: string[]
|
|
344
|
+
if (changedPaths && changedPaths.length > 0) {
|
|
345
|
+
filesToUpdate = changedPaths
|
|
346
|
+
} else {
|
|
347
|
+
const changes = await this.detectFileChanges()
|
|
348
|
+
filesToUpdate = [...changes.added, ...changes.modified]
|
|
349
|
+
|
|
350
|
+
// Remove deleted files from index
|
|
351
|
+
if (changes.deleted.length > 0) {
|
|
352
|
+
index.relevantFiles = index.relevantFiles.filter((f) => !changes.deleted.includes(f.path))
|
|
353
|
+
}
|
|
354
|
+
}
|
|
355
|
+
|
|
356
|
+
// If no changes, return cached
|
|
357
|
+
if (filesToUpdate.length === 0) {
|
|
358
|
+
return {
|
|
359
|
+
index,
|
|
360
|
+
fromCache: true,
|
|
361
|
+
changedFiles: 0,
|
|
362
|
+
scanDuration: Date.now() - startTime,
|
|
363
|
+
}
|
|
364
|
+
}
|
|
365
|
+
|
|
366
|
+
// Scan only changed files
|
|
367
|
+
const updatedFiles = await this.scanFiles(filesToUpdate)
|
|
368
|
+
|
|
369
|
+
// Rebuild scoring context with updated files
|
|
370
|
+
const existingFiles = await this.loadExistingFileStats(index)
|
|
371
|
+
for (const [path, stats] of updatedFiles) {
|
|
372
|
+
existingFiles.set(path, stats)
|
|
373
|
+
}
|
|
374
|
+
|
|
375
|
+
const context = this.buildScoringContext(existingFiles)
|
|
376
|
+
const scores = fileScorer.getRelevantFiles(context, RELEVANCE_THRESHOLD)
|
|
377
|
+
|
|
378
|
+
index.relevantFiles = scores.map((s) => ({
|
|
379
|
+
path: s.path,
|
|
380
|
+
score: s.score,
|
|
381
|
+
size: existingFiles.get(s.path)?.size || 0,
|
|
382
|
+
mtime: existingFiles.get(s.path)?.mtime.toISOString() || '',
|
|
383
|
+
}))
|
|
384
|
+
|
|
385
|
+
// Update timestamps
|
|
386
|
+
index.lastIncrementalUpdate = getTimestamp()
|
|
387
|
+
index.scanDuration = Date.now() - startTime
|
|
388
|
+
|
|
389
|
+
// Persist
|
|
390
|
+
await indexStorage.writeIndex(this.projectId, index)
|
|
391
|
+
await indexStorage.writeScores(this.projectId, index.relevantFiles)
|
|
392
|
+
|
|
393
|
+
return {
|
|
394
|
+
index,
|
|
395
|
+
fromCache: false,
|
|
396
|
+
changedFiles: filesToUpdate.length,
|
|
397
|
+
scanDuration: index.scanDuration,
|
|
398
|
+
}
|
|
399
|
+
}
|
|
400
|
+
|
|
401
|
+
/**
|
|
402
|
+
* Load index from cache if valid, otherwise full scan
|
|
403
|
+
*/
|
|
404
|
+
async loadOrScan(options: IndexOptions = {}): Promise<ScanResult> {
|
|
405
|
+
if (options.forceFullScan) {
|
|
406
|
+
return this.fullScan(options)
|
|
407
|
+
}
|
|
408
|
+
|
|
409
|
+
const index = await indexStorage.readIndex(this.projectId)
|
|
410
|
+
if (index?.lastFullScan) {
|
|
411
|
+
// Check if index is fresh enough (< 24 hours old)
|
|
412
|
+
const age = await indexStorage.getIndexAge(this.projectId)
|
|
413
|
+
if (age < 24) {
|
|
414
|
+
return {
|
|
415
|
+
index,
|
|
416
|
+
fromCache: true,
|
|
417
|
+
changedFiles: 0,
|
|
418
|
+
scanDuration: 0,
|
|
419
|
+
}
|
|
420
|
+
}
|
|
421
|
+
}
|
|
422
|
+
|
|
423
|
+
return this.fullScan(options)
|
|
424
|
+
}
|
|
425
|
+
|
|
426
|
+
/**
|
|
427
|
+
* Get relevant context for LLM with token estimation
|
|
428
|
+
*/
|
|
429
|
+
async getRelevantContext(maxTokens: number = 50000): Promise<RelevantContext> {
|
|
430
|
+
const index = await indexStorage.readIndex(this.projectId)
|
|
431
|
+
if (!index) {
|
|
432
|
+
return {
|
|
433
|
+
files: [],
|
|
434
|
+
estimatedTokens: 0,
|
|
435
|
+
originalTokens: 0,
|
|
436
|
+
compressionRate: 0,
|
|
437
|
+
}
|
|
438
|
+
}
|
|
439
|
+
|
|
440
|
+
const CHARS_PER_TOKEN = 4
|
|
441
|
+
let estimatedTokens = 0
|
|
442
|
+
const selectedFiles: ScoredFile[] = []
|
|
443
|
+
|
|
444
|
+
// Select files by score until we hit token limit
|
|
445
|
+
for (const file of index.relevantFiles) {
|
|
446
|
+
const fileTokens = Math.ceil(file.size / CHARS_PER_TOKEN)
|
|
447
|
+
if (estimatedTokens + fileTokens > maxTokens) {
|
|
448
|
+
break
|
|
449
|
+
}
|
|
450
|
+
selectedFiles.push(file)
|
|
451
|
+
estimatedTokens += fileTokens
|
|
452
|
+
}
|
|
453
|
+
|
|
454
|
+
// Original tokens = total project size
|
|
455
|
+
const originalTokens = Math.ceil(index.totalSize / CHARS_PER_TOKEN)
|
|
456
|
+
const compressionRate =
|
|
457
|
+
originalTokens > 0 ? (originalTokens - estimatedTokens) / originalTokens : 0
|
|
458
|
+
|
|
459
|
+
return {
|
|
460
|
+
files: selectedFiles,
|
|
461
|
+
estimatedTokens,
|
|
462
|
+
originalTokens,
|
|
463
|
+
compressionRate,
|
|
464
|
+
}
|
|
465
|
+
}
|
|
466
|
+
|
|
467
|
+
// ==========================================================================
|
|
468
|
+
// SCANNING METHODS
|
|
469
|
+
// ==========================================================================
|
|
470
|
+
|
|
471
|
+
/**
|
|
472
|
+
* Scan all source files in the project
|
|
473
|
+
*/
|
|
474
|
+
private async scanAllFiles(options: IndexOptions = {}): Promise<Map<string, FileStats>> {
|
|
475
|
+
const files = new Map<string, FileStats>()
|
|
476
|
+
const maxFiles = options.maxFiles || 10000
|
|
477
|
+
|
|
478
|
+
// Use find command for speed
|
|
479
|
+
try {
|
|
480
|
+
const excludeDirs = Array.from(IGNORE_DIRS)
|
|
481
|
+
.map((d) => `-not -path "*/${d}/*"`)
|
|
482
|
+
.join(' ')
|
|
483
|
+
const extensions = Array.from(SOURCE_EXTENSIONS)
|
|
484
|
+
.map((e) => `-name "*${e}"`)
|
|
485
|
+
.join(' -o ')
|
|
486
|
+
|
|
487
|
+
const { stdout } = await execAsync(
|
|
488
|
+
`find . -type f \\( ${extensions} \\) ${excludeDirs} | head -n ${maxFiles}`,
|
|
489
|
+
{ cwd: this.projectPath, maxBuffer: 10 * 1024 * 1024 }
|
|
490
|
+
)
|
|
491
|
+
|
|
492
|
+
const paths = stdout.trim().split('\n').filter(Boolean)
|
|
493
|
+
|
|
494
|
+
// Process files in parallel batches
|
|
495
|
+
const batchSize = 100
|
|
496
|
+
for (let i = 0; i < paths.length; i += batchSize) {
|
|
497
|
+
const batch = paths.slice(i, i + batchSize)
|
|
498
|
+
const results = await Promise.all(
|
|
499
|
+
batch.map((p) => this.getFileStats(p.replace(/^\.\//, '')))
|
|
500
|
+
)
|
|
501
|
+
for (const stats of results) {
|
|
502
|
+
if (stats) {
|
|
503
|
+
files.set(stats.path, stats)
|
|
504
|
+
}
|
|
505
|
+
}
|
|
506
|
+
}
|
|
507
|
+
} catch {
|
|
508
|
+
// Fallback to recursive directory walk
|
|
509
|
+
await this.walkDirectory('.', files, maxFiles)
|
|
510
|
+
}
|
|
511
|
+
|
|
512
|
+
return files
|
|
513
|
+
}
|
|
514
|
+
|
|
515
|
+
/**
|
|
516
|
+
* Scan specific files
|
|
517
|
+
*/
|
|
518
|
+
private async scanFiles(paths: string[]): Promise<Map<string, FileStats>> {
|
|
519
|
+
const files = new Map<string, FileStats>()
|
|
520
|
+
|
|
521
|
+
const results = await Promise.all(paths.map((p) => this.getFileStats(p)))
|
|
522
|
+
|
|
523
|
+
for (const stats of results) {
|
|
524
|
+
if (stats) {
|
|
525
|
+
files.set(stats.path, stats)
|
|
526
|
+
}
|
|
527
|
+
}
|
|
528
|
+
|
|
529
|
+
return files
|
|
530
|
+
}
|
|
531
|
+
|
|
532
|
+
/**
|
|
533
|
+
* Get stats for a single file
|
|
534
|
+
*/
|
|
535
|
+
private async getFileStats(relativePath: string): Promise<FileStats | null> {
|
|
536
|
+
const fullPath = path.join(this.projectPath, relativePath)
|
|
537
|
+
|
|
538
|
+
try {
|
|
539
|
+
const stat = await fs.stat(fullPath)
|
|
540
|
+
const content = await fs.readFile(fullPath, 'utf-8')
|
|
541
|
+
const lines = content.split('\n').length
|
|
542
|
+
|
|
543
|
+
return {
|
|
544
|
+
path: relativePath,
|
|
545
|
+
size: stat.size,
|
|
546
|
+
mtime: stat.mtime,
|
|
547
|
+
lines,
|
|
548
|
+
}
|
|
549
|
+
} catch {
|
|
550
|
+
return null
|
|
551
|
+
}
|
|
552
|
+
}
|
|
553
|
+
|
|
554
|
+
/**
|
|
555
|
+
* Recursive directory walk (fallback)
|
|
556
|
+
*/
|
|
557
|
+
private async walkDirectory(
|
|
558
|
+
dir: string,
|
|
559
|
+
files: Map<string, FileStats>,
|
|
560
|
+
maxFiles: number
|
|
561
|
+
): Promise<void> {
|
|
562
|
+
if (files.size >= maxFiles) return
|
|
563
|
+
|
|
564
|
+
const fullDir = path.join(this.projectPath, dir)
|
|
565
|
+
|
|
566
|
+
try {
|
|
567
|
+
const entries = await fs.readdir(fullDir, { withFileTypes: true })
|
|
568
|
+
|
|
569
|
+
for (const entry of entries) {
|
|
570
|
+
if (files.size >= maxFiles) break
|
|
571
|
+
|
|
572
|
+
const relativePath = path.join(dir, entry.name).replace(/^\.\//, '')
|
|
573
|
+
|
|
574
|
+
if (entry.isDirectory()) {
|
|
575
|
+
if (!IGNORE_DIRS.has(entry.name)) {
|
|
576
|
+
await this.walkDirectory(relativePath, files, maxFiles)
|
|
577
|
+
}
|
|
578
|
+
} else if (entry.isFile()) {
|
|
579
|
+
const ext = path.extname(entry.name)
|
|
580
|
+
if (SOURCE_EXTENSIONS.has(ext)) {
|
|
581
|
+
const stats = await this.getFileStats(relativePath)
|
|
582
|
+
if (stats) {
|
|
583
|
+
files.set(relativePath, stats)
|
|
584
|
+
}
|
|
585
|
+
}
|
|
586
|
+
}
|
|
587
|
+
}
|
|
588
|
+
} catch {
|
|
589
|
+
// Directory may not be accessible
|
|
590
|
+
}
|
|
591
|
+
}
|
|
592
|
+
|
|
593
|
+
// ==========================================================================
|
|
594
|
+
// CONFIG & DIRECTORY ANALYSIS
|
|
595
|
+
// ==========================================================================
|
|
596
|
+
|
|
597
|
+
/**
|
|
598
|
+
* Find and parse config files
|
|
599
|
+
*/
|
|
600
|
+
private async findConfigFiles(): Promise<ConfigFileEntry[]> {
|
|
601
|
+
const configs: ConfigFileEntry[] = []
|
|
602
|
+
|
|
603
|
+
for (const configName of CONFIG_FILES) {
|
|
604
|
+
const configPath = path.join(this.projectPath, configName)
|
|
605
|
+
|
|
606
|
+
try {
|
|
607
|
+
await fs.access(configPath)
|
|
608
|
+
const checksum = await indexStorage.calculateChecksum(configPath)
|
|
609
|
+
|
|
610
|
+
const entry: ConfigFileEntry = {
|
|
611
|
+
path: configName,
|
|
612
|
+
type: configName,
|
|
613
|
+
checksum,
|
|
614
|
+
}
|
|
615
|
+
|
|
616
|
+
// Parse JSON config files
|
|
617
|
+
if (configName.endsWith('.json')) {
|
|
618
|
+
try {
|
|
619
|
+
const content = await fs.readFile(configPath, 'utf-8')
|
|
620
|
+
entry.parsed = JSON.parse(content)
|
|
621
|
+
} catch {
|
|
622
|
+
// Invalid JSON
|
|
623
|
+
}
|
|
624
|
+
}
|
|
625
|
+
|
|
626
|
+
configs.push(entry)
|
|
627
|
+
} catch {
|
|
628
|
+
// Config file doesn't exist
|
|
629
|
+
}
|
|
630
|
+
}
|
|
631
|
+
|
|
632
|
+
return configs
|
|
633
|
+
}
|
|
634
|
+
|
|
635
|
+
/**
|
|
636
|
+
* Analyze top-level directory structure
|
|
637
|
+
*/
|
|
638
|
+
private async analyzeDirectories(): Promise<DirectoryEntry[]> {
|
|
639
|
+
const directories: DirectoryEntry[] = []
|
|
640
|
+
|
|
641
|
+
try {
|
|
642
|
+
const entries = await fs.readdir(this.projectPath, { withFileTypes: true })
|
|
643
|
+
|
|
644
|
+
for (const entry of entries) {
|
|
645
|
+
if (!entry.isDirectory()) continue
|
|
646
|
+
if (IGNORE_DIRS.has(entry.name)) continue
|
|
647
|
+
if (entry.name.startsWith('.') && entry.name !== '.github') continue
|
|
648
|
+
|
|
649
|
+
const dirPath = entry.name
|
|
650
|
+
const type = this.classifyDirectory(dirPath)
|
|
651
|
+
const fileCount = await this.countFilesInDir(dirPath)
|
|
652
|
+
|
|
653
|
+
directories.push({
|
|
654
|
+
path: dirPath,
|
|
655
|
+
type,
|
|
656
|
+
fileCount,
|
|
657
|
+
})
|
|
658
|
+
}
|
|
659
|
+
} catch {
|
|
660
|
+
// Project path may not be accessible
|
|
661
|
+
}
|
|
662
|
+
|
|
663
|
+
return directories
|
|
664
|
+
}
|
|
665
|
+
|
|
666
|
+
/**
|
|
667
|
+
* Classify directory type
|
|
668
|
+
*/
|
|
669
|
+
private classifyDirectory(dirName: string): DirectoryEntry['type'] {
|
|
670
|
+
for (const { type, patterns } of DIR_TYPE_PATTERNS) {
|
|
671
|
+
if (patterns.some((p) => p.test(dirName))) {
|
|
672
|
+
return type
|
|
673
|
+
}
|
|
674
|
+
}
|
|
675
|
+
return 'unknown'
|
|
676
|
+
}
|
|
677
|
+
|
|
678
|
+
/**
|
|
679
|
+
* Count files in a directory
|
|
680
|
+
*/
|
|
681
|
+
private async countFilesInDir(relativePath: string): Promise<number> {
|
|
682
|
+
const fullPath = path.join(this.projectPath, relativePath)
|
|
683
|
+
|
|
684
|
+
try {
|
|
685
|
+
const { stdout } = await execAsync(`find . -type f | wc -l`, { cwd: fullPath })
|
|
686
|
+
return parseInt(stdout.trim(), 10) || 0
|
|
687
|
+
} catch {
|
|
688
|
+
return 0
|
|
689
|
+
}
|
|
690
|
+
}
|
|
691
|
+
|
|
692
|
+
// ==========================================================================
|
|
693
|
+
// STACK DETECTION
|
|
694
|
+
// ==========================================================================
|
|
695
|
+
|
|
696
|
+
/**
|
|
697
|
+
* Detect technology stack from config files
|
|
698
|
+
*/
|
|
699
|
+
private async detectStack(configFiles: ConfigFileEntry[]): Promise<DetectedStack> {
|
|
700
|
+
const stack: DetectedStack = {
|
|
701
|
+
ecosystem: 'unknown',
|
|
702
|
+
frameworks: [],
|
|
703
|
+
hasTests: false,
|
|
704
|
+
hasDocker: false,
|
|
705
|
+
hasCi: false,
|
|
706
|
+
buildTool: null,
|
|
707
|
+
}
|
|
708
|
+
|
|
709
|
+
// Find package.json for JS/TS projects
|
|
710
|
+
const packageJson = configFiles.find((cf) => cf.type === 'package.json')
|
|
711
|
+
if (packageJson?.parsed) {
|
|
712
|
+
stack.ecosystem = 'JavaScript'
|
|
713
|
+
|
|
714
|
+
const deps = {
|
|
715
|
+
...(((packageJson.parsed as Record<string, unknown>).dependencies as Record<
|
|
716
|
+
string,
|
|
717
|
+
string
|
|
718
|
+
>) || {}),
|
|
719
|
+
...(((packageJson.parsed as Record<string, unknown>).devDependencies as Record<
|
|
720
|
+
string,
|
|
721
|
+
string
|
|
722
|
+
>) || {}),
|
|
723
|
+
}
|
|
724
|
+
|
|
725
|
+
// Detect frameworks
|
|
726
|
+
if (deps.react) stack.frameworks.push('React')
|
|
727
|
+
if (deps.next) stack.frameworks.push('Next.js')
|
|
728
|
+
if (deps.vue) stack.frameworks.push('Vue')
|
|
729
|
+
if (deps.nuxt) stack.frameworks.push('Nuxt')
|
|
730
|
+
if (deps.svelte) stack.frameworks.push('Svelte')
|
|
731
|
+
if (deps['@angular/core']) stack.frameworks.push('Angular')
|
|
732
|
+
if (deps.express) stack.frameworks.push('Express')
|
|
733
|
+
if (deps.fastify) stack.frameworks.push('Fastify')
|
|
734
|
+
if (deps.hono) stack.frameworks.push('Hono')
|
|
735
|
+
if (deps['@nestjs/core']) stack.frameworks.push('NestJS')
|
|
736
|
+
|
|
737
|
+
// Detect testing
|
|
738
|
+
if (deps.jest || deps.vitest || deps.mocha) stack.hasTests = true
|
|
739
|
+
|
|
740
|
+
// Detect build tool
|
|
741
|
+
if (deps.vite) stack.buildTool = 'vite'
|
|
742
|
+
else if (deps.webpack) stack.buildTool = 'webpack'
|
|
743
|
+
else if (deps.esbuild) stack.buildTool = 'esbuild'
|
|
744
|
+
else if (deps.rollup) stack.buildTool = 'rollup'
|
|
745
|
+
}
|
|
746
|
+
|
|
747
|
+
// Other ecosystems
|
|
748
|
+
if (configFiles.some((cf) => cf.type === 'Cargo.toml')) {
|
|
749
|
+
stack.ecosystem = 'Rust'
|
|
750
|
+
}
|
|
751
|
+
if (configFiles.some((cf) => cf.type === 'go.mod')) {
|
|
752
|
+
stack.ecosystem = 'Go'
|
|
753
|
+
}
|
|
754
|
+
if (configFiles.some((cf) => cf.type === 'pyproject.toml' || cf.type === 'requirements.txt')) {
|
|
755
|
+
stack.ecosystem = 'Python'
|
|
756
|
+
}
|
|
757
|
+
|
|
758
|
+
// Docker & CI
|
|
759
|
+
stack.hasDocker = configFiles.some(
|
|
760
|
+
(cf) => cf.type === 'Dockerfile' || cf.type.includes('docker-compose')
|
|
761
|
+
)
|
|
762
|
+
|
|
763
|
+
// Check for CI configs
|
|
764
|
+
try {
|
|
765
|
+
await fs.access(path.join(this.projectPath, '.github', 'workflows'))
|
|
766
|
+
stack.hasCi = true
|
|
767
|
+
} catch {
|
|
768
|
+
// No GitHub Actions
|
|
769
|
+
}
|
|
770
|
+
|
|
771
|
+
return stack
|
|
772
|
+
}
|
|
773
|
+
|
|
774
|
+
// ==========================================================================
|
|
775
|
+
// PATTERN DETECTION
|
|
776
|
+
// ==========================================================================
|
|
777
|
+
|
|
778
|
+
/**
|
|
779
|
+
* Detect architectural patterns
|
|
780
|
+
*/
|
|
781
|
+
private detectPatterns(index: ProjectIndex): DetectedPattern[] {
|
|
782
|
+
const patterns: DetectedPattern[] = []
|
|
783
|
+
|
|
784
|
+
for (const detector of PATTERN_DETECTORS) {
|
|
785
|
+
const confidence = detector.detect(index)
|
|
786
|
+
if (confidence > 0.3) {
|
|
787
|
+
patterns.push({
|
|
788
|
+
name: detector.name,
|
|
789
|
+
confidence,
|
|
790
|
+
evidence: detector.evidence(index),
|
|
791
|
+
})
|
|
792
|
+
}
|
|
793
|
+
}
|
|
794
|
+
|
|
795
|
+
return patterns.sort((a, b) => b.confidence - a.confidence)
|
|
796
|
+
}
|
|
797
|
+
|
|
798
|
+
// ==========================================================================
|
|
799
|
+
// HELPER METHODS
|
|
800
|
+
// ==========================================================================
|
|
801
|
+
|
|
802
|
+
/**
|
|
803
|
+
* Build language statistics
|
|
804
|
+
*/
|
|
805
|
+
private buildLanguageStats(files: FileStats[]): Record<string, LanguageStats> {
|
|
806
|
+
const stats: Record<string, LanguageStats> = {}
|
|
807
|
+
|
|
808
|
+
for (const file of files) {
|
|
809
|
+
const ext = path.extname(file.path)
|
|
810
|
+
if (!ext) continue
|
|
811
|
+
|
|
812
|
+
if (!stats[ext]) {
|
|
813
|
+
stats[ext] = { count: 0, totalLines: 0, totalSize: 0 }
|
|
814
|
+
}
|
|
815
|
+
|
|
816
|
+
stats[ext].count++
|
|
817
|
+
stats[ext].totalLines += file.lines || 0
|
|
818
|
+
stats[ext].totalSize += file.size
|
|
819
|
+
}
|
|
820
|
+
|
|
821
|
+
return stats
|
|
822
|
+
}
|
|
823
|
+
|
|
824
|
+
/**
|
|
825
|
+
* Build scoring context for all files
|
|
826
|
+
*/
|
|
827
|
+
private buildScoringContext(files: Map<string, FileStats>): ScoringContext {
|
|
828
|
+
const configFiles = new Set<string>()
|
|
829
|
+
let maxRecentCommits = 0
|
|
830
|
+
|
|
831
|
+
for (const file of files.values()) {
|
|
832
|
+
if (CONFIG_FILES.has(path.basename(file.path))) {
|
|
833
|
+
configFiles.add(file.path)
|
|
834
|
+
}
|
|
835
|
+
if (file.recentCommits && file.recentCommits > maxRecentCommits) {
|
|
836
|
+
maxRecentCommits = file.recentCommits
|
|
837
|
+
}
|
|
838
|
+
}
|
|
839
|
+
|
|
840
|
+
return {
|
|
841
|
+
allFiles: files,
|
|
842
|
+
configFiles,
|
|
843
|
+
maxFileSize: Math.max(...Array.from(files.values()).map((f) => f.size)),
|
|
844
|
+
maxRecentCommits,
|
|
845
|
+
now: new Date(),
|
|
846
|
+
}
|
|
847
|
+
}
|
|
848
|
+
|
|
849
|
+
/**
|
|
850
|
+
* Load existing file stats from index
|
|
851
|
+
*/
|
|
852
|
+
private async loadExistingFileStats(index: ProjectIndex): Promise<Map<string, FileStats>> {
|
|
853
|
+
const files = new Map<string, FileStats>()
|
|
854
|
+
|
|
855
|
+
for (const file of index.relevantFiles) {
|
|
856
|
+
files.set(file.path, {
|
|
857
|
+
path: file.path,
|
|
858
|
+
size: file.size,
|
|
859
|
+
mtime: new Date(file.mtime),
|
|
860
|
+
})
|
|
861
|
+
}
|
|
862
|
+
|
|
863
|
+
return files
|
|
864
|
+
}
|
|
865
|
+
|
|
866
|
+
/**
|
|
867
|
+
* Detect changed files using checksums
|
|
868
|
+
*/
|
|
869
|
+
private async detectFileChanges(): Promise<{
|
|
870
|
+
added: string[]
|
|
871
|
+
modified: string[]
|
|
872
|
+
deleted: string[]
|
|
873
|
+
}> {
|
|
874
|
+
// Scan current files and calculate checksums
|
|
875
|
+
const currentFiles = new Map<string, string>()
|
|
876
|
+
|
|
877
|
+
const allFiles = await this.scanAllFiles()
|
|
878
|
+
for (const [filePath] of allFiles) {
|
|
879
|
+
const fullPath = path.join(this.projectPath, filePath)
|
|
880
|
+
const checksum = await indexStorage.calculateChecksum(fullPath)
|
|
881
|
+
currentFiles.set(filePath, checksum)
|
|
882
|
+
}
|
|
883
|
+
|
|
884
|
+
return indexStorage.detectChangedFiles(this.projectId, currentFiles)
|
|
885
|
+
}
|
|
886
|
+
|
|
887
|
+
/**
|
|
888
|
+
* Save checksums for all scanned files
|
|
889
|
+
*/
|
|
890
|
+
private async saveChecksums(files: Map<string, FileStats>): Promise<void> {
|
|
891
|
+
const checksums: Record<string, string> = {}
|
|
892
|
+
|
|
893
|
+
for (const [filePath] of files) {
|
|
894
|
+
const fullPath = path.join(this.projectPath, filePath)
|
|
895
|
+
checksums[filePath] = await indexStorage.calculateChecksum(fullPath)
|
|
896
|
+
}
|
|
897
|
+
|
|
898
|
+
await indexStorage.writeChecksums(this.projectId, {
|
|
899
|
+
version: INDEX_VERSION,
|
|
900
|
+
lastUpdated: getTimestamp(),
|
|
901
|
+
checksums,
|
|
902
|
+
})
|
|
903
|
+
}
|
|
904
|
+
}
|
|
905
|
+
|
|
906
|
+
// Factory function for convenience
|
|
907
|
+
export function createProjectIndexer(projectPath: string, projectId: string): ProjectIndexer {
|
|
908
|
+
return new ProjectIndexer(projectPath, projectId)
|
|
909
|
+
}
|
|
910
|
+
|
|
911
|
+
export { RELEVANCE_THRESHOLD }
|