prjct-cli 0.9.1 → 0.9.2

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
@@ -7,6 +7,60 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
7
7
 
8
8
  ## [Unreleased]
9
9
 
10
+ ## [0.9.2] - 2025-11-22
11
+
12
+ ### Fixed
13
+
14
+ - **Critical: Missing `glob` dependency** - Fixed "Cannot find module 'glob'" error
15
+ - Added `glob@^10.3.10` to dependencies in `package.json`
16
+ - Resolves installation failures on fresh npm installs
17
+ - Compatible with Node.js v18+ (including v24.x LTS)
18
+
19
+ - **Critical: glob API compatibility** - Fixed TypeError with modern glob versions
20
+ - Updated `context-filter.js` to use modern glob API (v10+)
21
+ - Removed deprecated `promisify(glob)` pattern
22
+ - Changed to `const { glob } = require('glob')` with native promise support
23
+ - Added defensive array validation for glob results
24
+ - Resolves "ERR_INVALID_ARG_TYPE: The 'original' argument must be of type function" error
25
+
26
+ ### Added
27
+
28
+ - **Comprehensive Test Coverage** - Added 140+ tests for critical modules
29
+ - ✅ `core/agentic/agent-router.js` - 20 tests (agent assignment, task analysis, context filtering)
30
+ - ✅ `core/infrastructure/config-manager.js` - 30+ tests (config read/write, author management, validation)
31
+ - ✅ `core/infrastructure/path-manager.js` - 35 tests (path generation, session management, project structure)
32
+ - ✅ `core/utils/jsonl-helper.js` - 20+ tests (JSONL parsing, file operations, rotation)
33
+ - ✅ `core/domain/analyzer.js` - 15+ tests (project analysis, file detection, git integration)
34
+ - ✅ `core/infrastructure/author-detector.js` - 10+ tests (author detection, git integration)
35
+ - ✅ `core/agentic/context-filter.js` - 31 tests (already existed, now includes glob API tests)
36
+ - **Coverage improvement**: ~15% → ~40-50% overall coverage
37
+ - **Test files created**: 6 new test suites covering critical infrastructure
38
+
39
+ ### Changed
40
+
41
+ - **Test Infrastructure** - Enhanced test suite with comprehensive coverage
42
+ - All critical infrastructure modules now have full test coverage
43
+ - Tests detect dependency issues and API compatibility problems
44
+ - Better error detection and prevention for future changes
45
+
46
+ ### Technical Details
47
+
48
+ - **Dependency Updates**:
49
+ - Added `glob@^10.3.10` to production dependencies
50
+ - Compatible with Node.js v18.0.0+ (tested with v24.x)
51
+
52
+ - **Code Changes**:
53
+ - `core/agentic/context-filter.js`: Updated glob usage to modern API
54
+ - Added array validation for glob results (defensive programming)
55
+
56
+ - **Test Files Added**:
57
+ - `core/__tests__/agentic/agent-router.test.js`
58
+ - `core/__tests__/infrastructure/config-manager.test.js`
59
+ - `core/__tests__/infrastructure/path-manager.test.js`
60
+ - `core/__tests__/utils/jsonl-helper.test.js`
61
+ - `core/__tests__/domain/analyzer.test.js`
62
+ - `core/__tests__/infrastructure/author-detector.test.js`
63
+
10
64
  ## [0.9.1] - 2024-11-22
11
65
 
12
66
  ### 🎯 Context Optimization & Prompt Efficiency
@@ -0,0 +1,398 @@
1
+ import { describe, it, expect, beforeEach, vi } from 'vitest'
2
+ import { createRequire } from 'module'
3
+ import path from 'path'
4
+ import fs from 'fs/promises'
5
+ import os from 'os'
6
+
7
+ const require = createRequire(import.meta.url)
8
+
9
+ describe('Mandatory Agent Router', () => {
10
+ let MandatoryAgentRouter
11
+ let AgentGenerator
12
+ let router
13
+ let mockAgentGenerator
14
+
15
+ beforeEach(() => {
16
+ MandatoryAgentRouter = require('../../agentic/agent-router.js')
17
+ AgentGenerator = require('../../domain/agent-generator.js')
18
+
19
+ // Mock agent generator
20
+ mockAgentGenerator = {
21
+ generateDynamicAgent: vi.fn().mockResolvedValue({
22
+ name: 'test-agent',
23
+ type: 'frontend-specialist',
24
+ domain: 'frontend',
25
+ confidence: 0.9
26
+ })
27
+ }
28
+
29
+ router = new MandatoryAgentRouter()
30
+ router.agentGenerator = mockAgentGenerator
31
+ })
32
+
33
+ describe('Constructor', () => {
34
+ it('should initialize with agent generator', () => {
35
+ expect(router.agentGenerator).toBeDefined()
36
+ expect(router.agentCache).toBeInstanceOf(Map)
37
+ expect(Array.isArray(router.usageLog)).toBe(true)
38
+ })
39
+ })
40
+
41
+ describe('executeTask()', () => {
42
+ it('should assign agent to task', async () => {
43
+ const task = { description: 'create login component', type: 'ui' }
44
+ const context = { projectPath: '/test', files: [] }
45
+ const projectPath = '/test'
46
+
47
+ const result = await router.executeTask(task, context, projectPath)
48
+
49
+ expect(result).toBeDefined()
50
+ expect(result.agent).toBeDefined()
51
+ expect(result.agent.name).toBe('test-agent')
52
+ expect(result.context).toBeDefined()
53
+ expect(result.taskAnalysis).toBeDefined()
54
+ expect(result.routing).toBeDefined()
55
+ })
56
+
57
+ it('should throw error if no agent can be assigned', async () => {
58
+ router.agentGenerator.generateDynamicAgent = vi.fn().mockResolvedValue(null)
59
+
60
+ const task = { description: 'unknown task' }
61
+ const context = { projectPath: '/test' }
62
+ const projectPath = '/test'
63
+
64
+ await expect(router.executeTask(task, context, projectPath)).rejects.toThrow('CRITICAL: No agent assigned')
65
+ })
66
+
67
+ it('should cache agents for reuse', async () => {
68
+ const task = { description: 'create component', type: 'ui' }
69
+ const context = { projectPath: '/test' }
70
+ const projectPath = '/test'
71
+
72
+ await router.executeTask(task, context, projectPath)
73
+ await router.executeTask(task, context, projectPath)
74
+
75
+ // Should only generate once, second call uses cache
76
+ expect(mockAgentGenerator.generateDynamicAgent).toHaveBeenCalledTimes(1)
77
+ })
78
+
79
+ it('should log agent usage', async () => {
80
+ const task = { description: 'test task' }
81
+ const context = { projectPath: '/test', files: ['file1.js', 'file2.js'] }
82
+ const projectPath = '/test'
83
+
84
+ await router.executeTask(task, context, projectPath)
85
+
86
+ expect(router.usageLog.length).toBeGreaterThan(0)
87
+ expect(router.usageLog[0].task).toBe('test task')
88
+ expect(router.usageLog[0].agent).toBe('test-agent')
89
+ })
90
+ })
91
+
92
+ describe('analyzeTask()', () => {
93
+ it('should detect frontend tasks', () => {
94
+ const task = { description: 'create react component with styles' }
95
+ const analysis = router.analyzeTask(task)
96
+
97
+ expect(analysis.domain).toBe('frontend')
98
+ expect(analysis.confidence).toBeGreaterThan(0)
99
+ expect(analysis.matchedKeywords.length).toBeGreaterThan(0)
100
+ })
101
+
102
+ it('should detect backend tasks', () => {
103
+ const task = { description: 'create API endpoint with authentication' }
104
+ const analysis = router.analyzeTask(task)
105
+
106
+ expect(analysis.domain).toBe('backend')
107
+ expect(analysis.matchedKeywords).toContain('api')
108
+ })
109
+
110
+ it('should detect database tasks', () => {
111
+ const task = { description: 'create migration schema for postgres' }
112
+ const analysis = router.analyzeTask(task)
113
+
114
+ expect(analysis.domain).toBe('database')
115
+ expect(analysis.matchedKeywords).toContain('migration')
116
+ })
117
+
118
+ it('should detect devops tasks', () => {
119
+ const task = { description: 'deploy to docker kubernetes' }
120
+ const analysis = router.analyzeTask(task)
121
+
122
+ expect(analysis.domain).toBe('devops')
123
+ })
124
+
125
+ it('should detect qa tasks', () => {
126
+ const task = { description: 'write unit tests and fix bugs' }
127
+ const analysis = router.analyzeTask(task)
128
+
129
+ expect(analysis.domain).toBe('qa')
130
+ })
131
+
132
+ it('should detect architecture tasks', () => {
133
+ const task = { description: 'design architecture pattern and refactor' }
134
+ const analysis = router.analyzeTask(task)
135
+
136
+ expect(analysis.domain).toBe('architecture')
137
+ })
138
+
139
+ it('should return generalist for unknown tasks', () => {
140
+ const task = { description: 'random task description' }
141
+ const analysis = router.analyzeTask(task)
142
+
143
+ expect(analysis.domain).toBe('generalist')
144
+ expect(analysis.confidence).toBeLessThan(1)
145
+ })
146
+
147
+ it('should include technology stack in analysis', () => {
148
+ const task = { description: 'create react component with typescript' }
149
+ const analysis = router.analyzeTask(task)
150
+
151
+ expect(analysis.techStack).toBeDefined()
152
+ expect(analysis.techStack.languages).toContain('typescript')
153
+ expect(analysis.techStack.frameworks).toContain('react')
154
+ })
155
+ })
156
+
157
+ describe('detectTechnology()', () => {
158
+ it('should detect languages', () => {
159
+ const task = { description: 'write python script' }
160
+ const tech = router.detectTechnology(task, task.description)
161
+
162
+ expect(tech.languages).toContain('python')
163
+ })
164
+
165
+ it('should detect frameworks', () => {
166
+ const task = { description: 'build express api' }
167
+ const tech = router.detectTechnology(task, task.description)
168
+
169
+ expect(tech.frameworks).toContain('express')
170
+ })
171
+
172
+ it('should detect databases', () => {
173
+ const task = { description: 'query mongodb database' }
174
+ const tech = router.detectTechnology(task, task.description)
175
+
176
+ expect(tech.databases).toContain('mongodb')
177
+ })
178
+
179
+ it('should return empty arrays for unknown tech', () => {
180
+ const task = { description: 'random task' }
181
+ const tech = router.detectTechnology(task, task.description)
182
+
183
+ expect(tech.languages).toEqual([])
184
+ expect(tech.frameworks).toEqual([])
185
+ expect(tech.databases).toEqual([])
186
+ })
187
+ })
188
+
189
+ describe('assignAgent()', () => {
190
+ it('should generate agent for task', async () => {
191
+ const taskAnalysis = {
192
+ domain: 'frontend',
193
+ techStack: { languages: ['typescript'], frameworks: ['react'] }
194
+ }
195
+ const context = { projectPath: '/test' }
196
+
197
+ const agent = await router.assignAgent(taskAnalysis, context)
198
+
199
+ expect(agent).toBeDefined()
200
+ expect(mockAgentGenerator.generateDynamicAgent).toHaveBeenCalled()
201
+ })
202
+
203
+ it('should use cached agent if available', async () => {
204
+ const taskAnalysis = {
205
+ domain: 'frontend',
206
+ techStack: { languages: ['typescript'] }
207
+ }
208
+ const context = { projectPath: '/test' }
209
+
210
+ const agent1 = await router.assignAgent(taskAnalysis, context)
211
+ const agent2 = await router.assignAgent(taskAnalysis, context)
212
+
213
+ expect(agent1).toBe(agent2) // Same instance from cache
214
+ expect(mockAgentGenerator.generateDynamicAgent).toHaveBeenCalledTimes(1)
215
+ })
216
+ })
217
+
218
+ describe('filterContextForAgent()', () => {
219
+ it('should filter context for frontend agent', async () => {
220
+ const agent = { name: 'frontend-agent', domain: 'frontend' }
221
+ const fullContext = {
222
+ files: [
223
+ 'src/components/Button.jsx',
224
+ 'src/api/users.js',
225
+ 'src/styles/main.css',
226
+ 'migrations/001_users.sql'
227
+ ]
228
+ }
229
+ const taskAnalysis = { domain: 'frontend' }
230
+
231
+ const filtered = await router.filterContextForAgent(agent, fullContext, taskAnalysis)
232
+
233
+ expect(filtered.files).toContain('src/components/Button.jsx')
234
+ expect(filtered.files).toContain('src/styles/main.css')
235
+ expect(filtered.files).not.toContain('migrations/001_users.sql')
236
+ expect(filtered.relevantOnly).toBe(true)
237
+ })
238
+
239
+ it('should filter context for backend agent', async () => {
240
+ const agent = { name: 'backend-agent', domain: 'backend' }
241
+ const fullContext = {
242
+ files: [
243
+ 'src/components/Button.jsx',
244
+ 'src/api/users.js',
245
+ 'src/routes/auth.js',
246
+ 'src/styles/main.css'
247
+ ]
248
+ }
249
+ const taskAnalysis = { domain: 'backend' }
250
+
251
+ const filtered = await router.filterContextForAgent(agent, fullContext, taskAnalysis)
252
+
253
+ expect(filtered.files).toContain('src/api/users.js')
254
+ expect(filtered.files).toContain('src/routes/auth.js')
255
+ expect(filtered.files).not.toContain('src/styles/main.css')
256
+ })
257
+
258
+ it('should filter context for database agent', async () => {
259
+ const agent = { name: 'database-agent', domain: 'database' }
260
+ const fullContext = {
261
+ files: [
262
+ 'src/models/User.js',
263
+ 'migrations/001_users.sql',
264
+ 'src/components/Button.jsx'
265
+ ]
266
+ }
267
+ const taskAnalysis = { domain: 'database' }
268
+
269
+ const filtered = await router.filterContextForAgent(agent, fullContext, taskAnalysis)
270
+
271
+ expect(filtered.files).toContain('src/models/User.js')
272
+ expect(filtered.files).toContain('migrations/001_users.sql')
273
+ expect(filtered.files).not.toContain('src/components/Button.jsx')
274
+ })
275
+ })
276
+
277
+ describe('filterFiles()', () => {
278
+ it('should exclude files matching exclude patterns', () => {
279
+ const files = ['src/app.js', 'node_modules/lib.js', 'dist/bundle.js']
280
+ const pattern = {
281
+ include: [],
282
+ exclude: ['node_modules', 'dist'],
283
+ extensions: []
284
+ }
285
+
286
+ const filtered = router.filterFiles(files, pattern)
287
+
288
+ expect(filtered).not.toContain('node_modules/lib.js')
289
+ expect(filtered).not.toContain('dist/bundle.js')
290
+ expect(filtered).toContain('src/app.js')
291
+ })
292
+
293
+ it('should include only files matching include patterns', () => {
294
+ const files = ['src/components/Button.jsx', 'src/api/users.js', 'tests/test.js']
295
+ const pattern = {
296
+ include: ['components'],
297
+ exclude: [],
298
+ extensions: []
299
+ }
300
+
301
+ const filtered = router.filterFiles(files, pattern)
302
+
303
+ expect(filtered).toContain('src/components/Button.jsx')
304
+ expect(filtered).not.toContain('src/api/users.js')
305
+ })
306
+
307
+ it('should filter by extensions', () => {
308
+ const files = ['src/app.js', 'src/app.ts', 'src/app.py']
309
+ const pattern = {
310
+ include: [],
311
+ exclude: [],
312
+ extensions: ['.js', '.ts']
313
+ }
314
+
315
+ const filtered = router.filterFiles(files, pattern)
316
+
317
+ expect(filtered).toContain('src/app.js')
318
+ expect(filtered).toContain('src/app.ts')
319
+ expect(filtered).not.toContain('src/app.py')
320
+ })
321
+ })
322
+
323
+ describe('getBestPractices()', () => {
324
+ it('should return domain-specific practices', async () => {
325
+ const practices = await router.getBestPractices('frontend', { languages: [], frameworks: [] })
326
+
327
+ expect(practices.length).toBeGreaterThan(0)
328
+ expect(practices).toContain('Component composition over inheritance')
329
+ })
330
+
331
+ it('should include tech-specific practices', async () => {
332
+ const practices = await router.getBestPractices('frontend', {
333
+ languages: ['typescript'],
334
+ frameworks: ['react']
335
+ })
336
+
337
+ expect(practices.some(p => p.includes('Hooks') || p.includes('TypeScript'))).toBe(true)
338
+ })
339
+ })
340
+
341
+ describe('getSimilarDomains()', () => {
342
+ it('should return similar domains for frontend', () => {
343
+ const similar = router.getSimilarDomains('frontend')
344
+ expect(similar).toContain('fullstack')
345
+ })
346
+
347
+ it('should return similar domains for backend', () => {
348
+ const similar = router.getSimilarDomains('backend')
349
+ expect(similar).toContain('fullstack')
350
+ })
351
+
352
+ it('should return default for unknown domain', () => {
353
+ const similar = router.getSimilarDomains('unknown')
354
+ expect(similar).toEqual(['generalist'])
355
+ })
356
+ })
357
+
358
+ describe('getUsageStats()', () => {
359
+ it('should return usage statistics', async () => {
360
+ const task = { description: 'test task' }
361
+ const context = { projectPath: '/test' }
362
+ const projectPath = '/test'
363
+
364
+ await router.executeTask(task, context, projectPath)
365
+ await router.executeTask(task, context, projectPath)
366
+
367
+ const stats = router.getUsageStats()
368
+
369
+ expect(stats.totalTasks).toBe(2)
370
+ expect(stats.byAgent).toBeDefined()
371
+ expect(stats.mostUsedAgent).toBe('test-agent')
372
+ })
373
+
374
+ it('should handle empty usage log', () => {
375
+ const stats = router.getUsageStats()
376
+
377
+ expect(stats.totalTasks).toBe(0)
378
+ expect(stats.mostUsedAgent).toBeNull()
379
+ })
380
+ })
381
+
382
+ describe('calculateContextReduction()', () => {
383
+ it('should calculate reduction for filtered context', () => {
384
+ const filteredContext = { relevantOnly: true }
385
+ const reduction = router.calculateContextReduction(filteredContext)
386
+
387
+ expect(reduction).toBe('70-90%')
388
+ })
389
+
390
+ it('should return 0% for unfiltered context', () => {
391
+ const context = { relevantOnly: false }
392
+ const reduction = router.calculateContextReduction(context)
393
+
394
+ expect(reduction).toBe('0%')
395
+ })
396
+ })
397
+ })
398
+