prjct-cli 1.14.0 → 1.16.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,5 +1,83 @@
1
1
  # Changelog
2
2
 
3
+ ## [1.16.0] - 2026-02-09
4
+
5
+ ### Features
6
+
7
+ - remove JSON storage redundancy, SQLite-only backend (PRJ-303) (#158)
8
+
9
+
10
+ ## [1.16.0] - 2026-02-08
11
+
12
+ ### Features
13
+ - **Remove JSON storage redundancy** (PRJ-303): SQLite is now the sole storage backend. JSON dual-write removed from StorageManager and IndexStorage. Auto-migration runs during `prjct sync`, deletes source JSON files after backup.
14
+
15
+ ### Implementation Details
16
+ - `StorageManager.write()` — removed JSON file write and temp file pattern; writes only to SQLite kv_store + regenerates context MD
17
+ - `StorageManager.read()` — removed JSON fallback; reads from cache → SQLite → default
18
+ - `StorageManager.exists()` — removed JSON file check; checks SQLite only
19
+ - `IndexStorage` — all 5 read methods (readIndex, readChecksums, readScores, readDomains, readCategories) no longer fall back to JSON; all 5 write methods no longer create JSON files
20
+ - `migrateJsonToSqlite()` — new cleanup step deletes `storage/*.json`, `index/*.json`, `memory/*.jsonl` after successful migration; keeps `storage/backup/`
21
+ - `SyncService.sync()` — auto-runs `migrateJsonToSqlite()` after directory setup (idempotent)
22
+ - `PrjctDatabase.getDb()` — ensures parent directory exists before creating SQLite DB
23
+ - `database.ts` — fixed all TS errors: `db.exec()` → `db.run()` (deprecated), `unknown[]` → `SQLQueryBindings[]`, `require()` → `fs.existsSync()`
24
+
25
+ ### Test Plan
26
+
27
+ #### For QA
28
+ 1. Run `bun test` — all 879 tests must pass
29
+ 2. Run `prjct sync` on existing project — verify migration runs, JSON files deleted, `prjct.db` has data
30
+ 3. Run `prjct sync` again — verify idempotent (no errors, migration skips)
31
+ 4. Verify `storage/backup/` contains pre-migration JSON files
32
+ 5. Verify `context/*.md` files still generated after writes
33
+ 6. Run `prjct status` — verify state reads from SQLite
34
+
35
+ #### For Users
36
+ **What changed:** Storage is now fully SQLite-backed. JSON files are auto-migrated and removed during sync.
37
+ **How to use:** Run `prjct sync` — migration happens automatically.
38
+ **Breaking changes:** None — all public APIs unchanged. JSON backups preserved in `storage/backup/`.
39
+
40
+ ## [1.15.0] - 2026-02-09
41
+
42
+ ### Features
43
+
44
+ - replace hardcoded memory domain tags with semantic matching (PRJ-300) (#157)
45
+
46
+
47
+ ## [1.14.1] - 2026-02-09
48
+
49
+ ### Improved
50
+ - **Semantic memory domain matching** (PRJ-300): Memory retrieval now uses two-pass scoring (exact MEMORY_TAG match + semantic keyword match) instead of exact-only tag matching. Unknown domains like "uxui" are resolved to canonical domains ("frontend") via SEMANTIC_DOMAIN_KEYWORDS map.
51
+ - **Tech stack normalizer**: New `tech-normalizer.ts` module handles framework name normalization, compound names ("React + TypeScript"), framework families (Next.js → React), and alias resolution (nextjs → next.js).
52
+ - **Anti-hallucination dedup**: Tech stack entries in the anti-hallucination block are now deduplicated using normalized matching, preventing duplicates like "React" and "react" or "Next.js" and "nextjs".
53
+ - **Sealed analysis priority**: Prompt builder now uses sealed analysis frameworks as primary tech stack source, falling back to repo conventions.
54
+
55
+ ### Implementation Details
56
+ - Expanded `TaskDomain` type from fixed union to `KnownDomain | (string & {})` for forward compatibility while preserving autocomplete
57
+ - `DOMAIN_TAG_MAP` now includes TECH_STACK for frontend/database domains (previously missing)
58
+ - `SEMANTIC_DOMAIN_KEYWORDS` maps 80+ keywords across 7 domains for fuzzy domain resolution
59
+ - `resolveCanonicalDomains()` exported for testability — resolves arbitrary strings to known domains
60
+ - `normalizeFrameworkName()` handles 15 aliases; `FRAMEWORK_FAMILIES` maps 12 meta-frameworks to base frameworks
61
+ - `extractTechNames()` splits compound tech strings on +, /, commas, parentheses, "with", "and"
62
+
63
+ ### Learnings
64
+ - Two-pass scoring (exact 10pts + semantic 5pts) gives gradual relevance instead of binary match/no-match
65
+ - TypeScript's `KnownDomain | (string & {})` pattern preserves autocomplete for known values while accepting any string
66
+ - Parentheses in compound names need comma replacement (not space) — otherwise adjacent words merge into a single token
67
+
68
+ ### Test Plan
69
+
70
+ #### For QA
71
+ 1. `bun test core/__tests__/agentic/semantic-matching.test.ts` — domain resolution (uxui→frontend, api→backend, infra→devops)
72
+ 2. `bun test core/__tests__/agentic/tech-normalizer.test.ts` — normalization, compound names, framework families, dedup
73
+ 3. `bun test core/__tests__/agentic/prompt-assembly.test.ts` — anti-hallucination block still renders correctly
74
+ 4. `bun test` — 848 tests pass, 0 fail
75
+
76
+ #### For Users
77
+ - Memory retrieval automatically surfaces related memories across domains
78
+ - No user action needed — improvements are automatic
79
+ - No breaking changes
80
+
3
81
  ## [1.14.0] - 2026-02-09
4
82
 
5
83
  ### Features
@@ -0,0 +1,131 @@
1
+ import { describe, expect, it } from 'bun:test'
2
+ import {
3
+ DOMAIN_TAG_MAP,
4
+ resolveCanonicalDomains,
5
+ SEMANTIC_DOMAIN_KEYWORDS,
6
+ } from '../../agentic/memory-system'
7
+ import { KNOWN_DOMAINS, MEMORY_TAGS } from '../../types/memory'
8
+
9
+ describe('semantic domain matching (PRJ-300)', () => {
10
+ describe('resolveCanonicalDomains', () => {
11
+ it('should pass through known domains unchanged', () => {
12
+ expect(resolveCanonicalDomains('frontend')).toEqual(['frontend'])
13
+ expect(resolveCanonicalDomains('backend')).toEqual(['backend'])
14
+ expect(resolveCanonicalDomains('devops')).toEqual(['devops'])
15
+ expect(resolveCanonicalDomains('testing')).toEqual(['testing'])
16
+ expect(resolveCanonicalDomains('database')).toEqual(['database'])
17
+ expect(resolveCanonicalDomains('general')).toEqual(['general'])
18
+ })
19
+
20
+ it('should resolve "uxui" to frontend', () => {
21
+ const result = resolveCanonicalDomains('uxui')
22
+ expect(result).toContain('frontend')
23
+ })
24
+
25
+ it('should resolve "ui" to frontend', () => {
26
+ const result = resolveCanonicalDomains('ui')
27
+ expect(result).toContain('frontend')
28
+ })
29
+
30
+ it('should resolve "api" to backend', () => {
31
+ const result = resolveCanonicalDomains('api')
32
+ expect(result).toContain('backend')
33
+ })
34
+
35
+ it('should resolve "ml-pipeline" to backend', () => {
36
+ const result = resolveCanonicalDomains('ml-pipeline')
37
+ // "pipeline" matches devops, so it should at least resolve
38
+ expect(result.length).toBeGreaterThan(0)
39
+ expect(result).not.toEqual(['general'])
40
+ })
41
+
42
+ it('should resolve "infra" to devops', () => {
43
+ const result = resolveCanonicalDomains('infra')
44
+ expect(result).toContain('devops')
45
+ })
46
+
47
+ it('should resolve "docker" to devops', () => {
48
+ const result = resolveCanonicalDomains('docker')
49
+ expect(result).toContain('devops')
50
+ })
51
+
52
+ it('should resolve "schema" to database', () => {
53
+ const result = resolveCanonicalDomains('schema')
54
+ expect(result).toContain('database')
55
+ })
56
+
57
+ it('should resolve "css" to frontend', () => {
58
+ const result = resolveCanonicalDomains('css')
59
+ expect(result).toContain('frontend')
60
+ })
61
+
62
+ it('should resolve "e2e" to testing', () => {
63
+ const result = resolveCanonicalDomains('e2e')
64
+ expect(result).toContain('testing')
65
+ })
66
+
67
+ it('should fallback to general for truly unknown domains', () => {
68
+ expect(resolveCanonicalDomains('quantum-computing')).toEqual(['general'])
69
+ expect(resolveCanonicalDomains('astrology')).toEqual(['general'])
70
+ })
71
+
72
+ it('should handle case-insensitive and separator-insensitive', () => {
73
+ expect(resolveCanonicalDomains('UI')).toContain('frontend')
74
+ expect(resolveCanonicalDomains('API')).toContain('backend')
75
+ expect(resolveCanonicalDomains('e_2_e')).toContain('testing')
76
+ })
77
+ })
78
+
79
+ describe('DOMAIN_TAG_MAP', () => {
80
+ it('should include TECH_STACK for frontend (improved from PRJ-107)', () => {
81
+ expect(DOMAIN_TAG_MAP.frontend).toContain(MEMORY_TAGS.TECH_STACK)
82
+ })
83
+
84
+ it('should include TECH_STACK for database', () => {
85
+ expect(DOMAIN_TAG_MAP.database).toContain(MEMORY_TAGS.TECH_STACK)
86
+ })
87
+
88
+ it('should include DEPENDENCIES for testing', () => {
89
+ expect(DOMAIN_TAG_MAP.testing).toContain(MEMORY_TAGS.DEPENDENCIES)
90
+ })
91
+
92
+ it('should include all MEMORY_TAGS for general', () => {
93
+ const allTags = Object.values(MEMORY_TAGS)
94
+ for (const tag of allTags) {
95
+ expect(DOMAIN_TAG_MAP.general).toContain(tag)
96
+ }
97
+ })
98
+
99
+ it('should have mappings for all known domains', () => {
100
+ for (const domain of KNOWN_DOMAINS) {
101
+ expect(DOMAIN_TAG_MAP[domain]).toBeDefined()
102
+ expect(DOMAIN_TAG_MAP[domain].length).toBeGreaterThan(0)
103
+ }
104
+ })
105
+ })
106
+
107
+ describe('SEMANTIC_DOMAIN_KEYWORDS', () => {
108
+ it('should map uxui-related keywords to frontend', () => {
109
+ expect(SEMANTIC_DOMAIN_KEYWORDS.frontend).toContain('uxui')
110
+ expect(SEMANTIC_DOMAIN_KEYWORDS.frontend).toContain('ui')
111
+ expect(SEMANTIC_DOMAIN_KEYWORDS.frontend).toContain('ux')
112
+ expect(SEMANTIC_DOMAIN_KEYWORDS.frontend).toContain('css')
113
+ })
114
+
115
+ it('should map CI/CD keywords to devops', () => {
116
+ expect(SEMANTIC_DOMAIN_KEYWORDS.devops).toContain('ci')
117
+ expect(SEMANTIC_DOMAIN_KEYWORDS.devops).toContain('cd')
118
+ expect(SEMANTIC_DOMAIN_KEYWORDS.devops).toContain('docker')
119
+ expect(SEMANTIC_DOMAIN_KEYWORDS.devops).toContain('kubernetes')
120
+ })
121
+
122
+ it('should have keywords for all known domains except general', () => {
123
+ for (const domain of KNOWN_DOMAINS) {
124
+ expect(SEMANTIC_DOMAIN_KEYWORDS[domain]).toBeDefined()
125
+ if (domain !== 'general') {
126
+ expect(SEMANTIC_DOMAIN_KEYWORDS[domain].length).toBeGreaterThan(0)
127
+ }
128
+ }
129
+ })
130
+ })
131
+ })
@@ -0,0 +1,136 @@
1
+ import { describe, expect, it } from 'bun:test'
2
+ import {
3
+ deduplicateTechStack,
4
+ extractTechNames,
5
+ getFrameworkFamily,
6
+ matchesTech,
7
+ normalizeFrameworkName,
8
+ } from '../../agentic/tech-normalizer'
9
+
10
+ describe('tech-normalizer', () => {
11
+ describe('normalizeFrameworkName', () => {
12
+ it('should lowercase and trim', () => {
13
+ expect(normalizeFrameworkName(' TypeScript ')).toBe('typescript')
14
+ expect(normalizeFrameworkName('React')).toBe('react')
15
+ })
16
+
17
+ it('should resolve aliases', () => {
18
+ expect(normalizeFrameworkName('NodeJS')).toBe('node')
19
+ expect(normalizeFrameworkName('ts')).toBe('typescript')
20
+ expect(normalizeFrameworkName('js')).toBe('javascript')
21
+ expect(normalizeFrameworkName('pg')).toBe('postgres')
22
+ expect(normalizeFrameworkName('postgresql')).toBe('postgres')
23
+ })
24
+
25
+ it('should preserve dotted names', () => {
26
+ expect(normalizeFrameworkName('Next.js')).toBe('next.js')
27
+ expect(normalizeFrameworkName('Vue.js')).toBe('vue')
28
+ })
29
+
30
+ it('should handle nextjs alias', () => {
31
+ expect(normalizeFrameworkName('nextjs')).toBe('next.js')
32
+ expect(normalizeFrameworkName('nuxtjs')).toBe('nuxt.js')
33
+ })
34
+ })
35
+
36
+ describe('extractTechNames', () => {
37
+ it('should split on + separator', () => {
38
+ expect(extractTechNames('React + TypeScript')).toEqual(['react', 'typescript'])
39
+ })
40
+
41
+ it('should extract from parentheses', () => {
42
+ expect(extractTechNames('Next.js (React)')).toEqual(['next.js', 'react'])
43
+ })
44
+
45
+ it('should split on "with"', () => {
46
+ expect(extractTechNames('Hono with Zod')).toEqual(['hono', 'zod'])
47
+ })
48
+
49
+ it('should split on commas', () => {
50
+ expect(extractTechNames('React, Vue, Angular')).toEqual(['react', 'vue', 'angular'])
51
+ })
52
+
53
+ it('should handle single name', () => {
54
+ expect(extractTechNames('React')).toEqual(['react'])
55
+ })
56
+ })
57
+
58
+ describe('getFrameworkFamily', () => {
59
+ it('should resolve meta-frameworks to base', () => {
60
+ expect(getFrameworkFamily('next.js')).toBe('react')
61
+ expect(getFrameworkFamily('Next.js')).toBe('react')
62
+ expect(getFrameworkFamily('Remix')).toBe('react')
63
+ expect(getFrameworkFamily('Gatsby')).toBe('react')
64
+ })
65
+
66
+ it('should resolve Vue meta-frameworks', () => {
67
+ expect(getFrameworkFamily('nuxt')).toBe('vue')
68
+ expect(getFrameworkFamily('Nuxt.js')).toBe('vue')
69
+ })
70
+
71
+ it('should resolve Svelte meta-framework', () => {
72
+ expect(getFrameworkFamily('SvelteKit')).toBe('svelte')
73
+ })
74
+
75
+ it('should return name itself for base frameworks', () => {
76
+ expect(getFrameworkFamily('React')).toBe('react')
77
+ expect(getFrameworkFamily('Express')).toBe('express')
78
+ expect(getFrameworkFamily('Hono')).toBe('hono')
79
+ })
80
+ })
81
+
82
+ describe('matchesTech', () => {
83
+ it('should match exact names (case-insensitive)', () => {
84
+ expect(matchesTech('React', 'react')).toBe(true)
85
+ expect(matchesTech('react', 'React')).toBe(true)
86
+ })
87
+
88
+ it('should match via framework family', () => {
89
+ expect(matchesTech('Next.js', 'react')).toBe(true)
90
+ expect(matchesTech('Nuxt', 'vue')).toBe(true)
91
+ expect(matchesTech('SvelteKit', 'svelte')).toBe(true)
92
+ })
93
+
94
+ it('should match compound names', () => {
95
+ expect(matchesTech('React + TypeScript', 'react')).toBe(true)
96
+ expect(matchesTech('React + TypeScript', 'typescript')).toBe(true)
97
+ })
98
+
99
+ it('should match parenthesized names', () => {
100
+ expect(matchesTech('Next.js (React)', 'react')).toBe(true)
101
+ expect(matchesTech('Next.js (React)', 'next.js')).toBe(true)
102
+ })
103
+
104
+ it('should not match unrelated frameworks', () => {
105
+ expect(matchesTech('Vue', 'react')).toBe(false)
106
+ expect(matchesTech('Angular', 'react')).toBe(false)
107
+ })
108
+ })
109
+
110
+ describe('deduplicateTechStack', () => {
111
+ it('should remove case-insensitive duplicates', () => {
112
+ expect(deduplicateTechStack(['React', 'react', 'REACT'])).toEqual(['React'])
113
+ })
114
+
115
+ it('should remove alias duplicates', () => {
116
+ expect(deduplicateTechStack(['TypeScript', 'ts'])).toEqual(['TypeScript'])
117
+ expect(deduplicateTechStack(['Node.js', 'nodejs'])).toEqual(['Node.js'])
118
+ })
119
+
120
+ it('should preserve unique entries', () => {
121
+ expect(deduplicateTechStack(['React', 'Next.js', 'TypeScript'])).toEqual([
122
+ 'React',
123
+ 'Next.js',
124
+ 'TypeScript',
125
+ ])
126
+ })
127
+
128
+ it('should preserve first occurrence casing', () => {
129
+ expect(deduplicateTechStack(['Next.js', 'nextjs', 'NEXT.JS'])).toEqual(['Next.js'])
130
+ })
131
+
132
+ it('should handle empty array', () => {
133
+ expect(deduplicateTechStack([])).toEqual([])
134
+ })
135
+ })
136
+ })