prjct-cli 1.14.0 → 1.15.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 +41 -0
- package/core/__tests__/agentic/semantic-matching.test.ts +131 -0
- package/core/__tests__/agentic/tech-normalizer.test.ts +136 -0
- package/core/agentic/anti-hallucination.ts +11 -16
- package/core/agentic/memory-system.ts +224 -23
- package/core/agentic/prompt-builder.ts +9 -1
- package/core/agentic/tech-normalizer.ts +167 -0
- package/core/types/memory.ts +19 -9
- package/dist/bin/prjct.mjs +258 -39
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,46 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## [1.15.0] - 2026-02-09
|
|
4
|
+
|
|
5
|
+
### Features
|
|
6
|
+
|
|
7
|
+
- replace hardcoded memory domain tags with semantic matching (PRJ-300) (#157)
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
## [1.14.1] - 2026-02-09
|
|
11
|
+
|
|
12
|
+
### Improved
|
|
13
|
+
- **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.
|
|
14
|
+
- **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).
|
|
15
|
+
- **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".
|
|
16
|
+
- **Sealed analysis priority**: Prompt builder now uses sealed analysis frameworks as primary tech stack source, falling back to repo conventions.
|
|
17
|
+
|
|
18
|
+
### Implementation Details
|
|
19
|
+
- Expanded `TaskDomain` type from fixed union to `KnownDomain | (string & {})` for forward compatibility while preserving autocomplete
|
|
20
|
+
- `DOMAIN_TAG_MAP` now includes TECH_STACK for frontend/database domains (previously missing)
|
|
21
|
+
- `SEMANTIC_DOMAIN_KEYWORDS` maps 80+ keywords across 7 domains for fuzzy domain resolution
|
|
22
|
+
- `resolveCanonicalDomains()` exported for testability — resolves arbitrary strings to known domains
|
|
23
|
+
- `normalizeFrameworkName()` handles 15 aliases; `FRAMEWORK_FAMILIES` maps 12 meta-frameworks to base frameworks
|
|
24
|
+
- `extractTechNames()` splits compound tech strings on +, /, commas, parentheses, "with", "and"
|
|
25
|
+
|
|
26
|
+
### Learnings
|
|
27
|
+
- Two-pass scoring (exact 10pts + semantic 5pts) gives gradual relevance instead of binary match/no-match
|
|
28
|
+
- TypeScript's `KnownDomain | (string & {})` pattern preserves autocomplete for known values while accepting any string
|
|
29
|
+
- Parentheses in compound names need comma replacement (not space) — otherwise adjacent words merge into a single token
|
|
30
|
+
|
|
31
|
+
### Test Plan
|
|
32
|
+
|
|
33
|
+
#### For QA
|
|
34
|
+
1. `bun test core/__tests__/agentic/semantic-matching.test.ts` — domain resolution (uxui→frontend, api→backend, infra→devops)
|
|
35
|
+
2. `bun test core/__tests__/agentic/tech-normalizer.test.ts` — normalization, compound names, framework families, dedup
|
|
36
|
+
3. `bun test core/__tests__/agentic/prompt-assembly.test.ts` — anti-hallucination block still renders correctly
|
|
37
|
+
4. `bun test` — 848 tests pass, 0 fail
|
|
38
|
+
|
|
39
|
+
#### For Users
|
|
40
|
+
- Memory retrieval automatically surfaces related memories across domains
|
|
41
|
+
- No user action needed — improvements are automatic
|
|
42
|
+
- No breaking changes
|
|
43
|
+
|
|
3
44
|
## [1.14.0] - 2026-02-09
|
|
4
45
|
|
|
5
46
|
### 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
|
+
})
|
|
@@ -19,6 +19,7 @@
|
|
|
19
19
|
*/
|
|
20
20
|
|
|
21
21
|
import { z } from 'zod'
|
|
22
|
+
import { deduplicateTechStack } from './tech-normalizer'
|
|
22
23
|
|
|
23
24
|
// =============================================================================
|
|
24
25
|
// Schema
|
|
@@ -85,25 +86,19 @@ export function buildAntiHallucinationBlock(truth: ProjectGroundTruth): string {
|
|
|
85
86
|
|
|
86
87
|
parts.push('## CONSTRAINTS (Read Before Acting)\n')
|
|
87
88
|
|
|
88
|
-
// 1. Explicit availability (enriched by sealed analysis — PRJ-260)
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
89
|
+
// 1. Explicit availability (enriched by sealed analysis — PRJ-260, PRJ-300)
|
|
90
|
+
// Use normalized deduplication to prevent "React" and "react" appearing twice,
|
|
91
|
+
// and to handle aliases like "Next.js" vs "nextjs".
|
|
92
|
+
const rawAvailable: string[] = []
|
|
93
|
+
if (truth.language) rawAvailable.push(truth.language)
|
|
94
|
+
if (truth.framework) rawAvailable.push(truth.framework)
|
|
92
95
|
const techStack = truth.techStack ?? []
|
|
93
|
-
|
|
94
|
-
// Merge languages/frameworks from sealed analysis
|
|
96
|
+
rawAvailable.push(...techStack)
|
|
97
|
+
// Merge languages/frameworks from sealed analysis
|
|
95
98
|
const analysisLangs = truth.analysisLanguages ?? []
|
|
96
99
|
const analysisFrameworks = truth.analysisFrameworks ?? []
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
available.push(lang)
|
|
100
|
-
}
|
|
101
|
-
}
|
|
102
|
-
for (const fw of analysisFrameworks) {
|
|
103
|
-
if (!available.some((a) => a.toLowerCase() === fw.toLowerCase())) {
|
|
104
|
-
available.push(fw)
|
|
105
|
-
}
|
|
106
|
-
}
|
|
100
|
+
rawAvailable.push(...analysisLangs, ...analysisFrameworks)
|
|
101
|
+
const available = deduplicateTechStack(rawAvailable)
|
|
107
102
|
|
|
108
103
|
if (available.length > 0) {
|
|
109
104
|
parts.push(`AVAILABLE in this project: ${available.join(', ')}`)
|
|
@@ -26,6 +26,7 @@ export type {
|
|
|
26
26
|
Decision,
|
|
27
27
|
HistoryEntry,
|
|
28
28
|
HistoryEventType,
|
|
29
|
+
KnownDomain,
|
|
29
30
|
Memory,
|
|
30
31
|
MemoryContext,
|
|
31
32
|
MemoryContextParams,
|
|
@@ -40,11 +41,12 @@ export type {
|
|
|
40
41
|
Workflow,
|
|
41
42
|
} from '../types/memory'
|
|
42
43
|
|
|
43
|
-
export { calculateConfidence, MEMORY_TAGS } from '../types/memory'
|
|
44
|
+
export { calculateConfidence, KNOWN_DOMAINS, MEMORY_TAGS } from '../types/memory'
|
|
44
45
|
|
|
45
46
|
import type {
|
|
46
47
|
HistoryEntry,
|
|
47
48
|
HistoryEventType,
|
|
49
|
+
KnownDomain,
|
|
48
50
|
Memory,
|
|
49
51
|
MemoryContext,
|
|
50
52
|
MemoryDatabase,
|
|
@@ -58,7 +60,167 @@ import type {
|
|
|
58
60
|
Workflow,
|
|
59
61
|
} from '../types/memory'
|
|
60
62
|
|
|
61
|
-
import { calculateConfidence, MEMORY_TAGS } from '../types/memory'
|
|
63
|
+
import { calculateConfidence, KNOWN_DOMAINS, MEMORY_TAGS } from '../types/memory'
|
|
64
|
+
|
|
65
|
+
// =============================================================================
|
|
66
|
+
// Semantic Domain Mapping (PRJ-300)
|
|
67
|
+
// =============================================================================
|
|
68
|
+
|
|
69
|
+
/**
|
|
70
|
+
* Map each known domain to its relevant MEMORY_TAGS.
|
|
71
|
+
* More comprehensive than the previous mapping — includes TECH_STACK and
|
|
72
|
+
* DEPENDENCIES where they apply.
|
|
73
|
+
*/
|
|
74
|
+
export const DOMAIN_TAG_MAP: Record<string, MemoryTag[]> = {
|
|
75
|
+
frontend: [
|
|
76
|
+
MEMORY_TAGS.CODE_STYLE,
|
|
77
|
+
MEMORY_TAGS.FILE_STRUCTURE,
|
|
78
|
+
MEMORY_TAGS.ARCHITECTURE,
|
|
79
|
+
MEMORY_TAGS.TECH_STACK,
|
|
80
|
+
],
|
|
81
|
+
backend: [
|
|
82
|
+
MEMORY_TAGS.CODE_STYLE,
|
|
83
|
+
MEMORY_TAGS.ARCHITECTURE,
|
|
84
|
+
MEMORY_TAGS.DEPENDENCIES,
|
|
85
|
+
MEMORY_TAGS.TECH_STACK,
|
|
86
|
+
],
|
|
87
|
+
devops: [
|
|
88
|
+
MEMORY_TAGS.SHIP_WORKFLOW,
|
|
89
|
+
MEMORY_TAGS.TEST_BEHAVIOR,
|
|
90
|
+
MEMORY_TAGS.DEPENDENCIES,
|
|
91
|
+
MEMORY_TAGS.ARCHITECTURE,
|
|
92
|
+
],
|
|
93
|
+
docs: [MEMORY_TAGS.CODE_STYLE, MEMORY_TAGS.NAMING_CONVENTION, MEMORY_TAGS.FILE_STRUCTURE],
|
|
94
|
+
testing: [MEMORY_TAGS.TEST_BEHAVIOR, MEMORY_TAGS.CODE_STYLE, MEMORY_TAGS.DEPENDENCIES],
|
|
95
|
+
database: [
|
|
96
|
+
MEMORY_TAGS.ARCHITECTURE,
|
|
97
|
+
MEMORY_TAGS.NAMING_CONVENTION,
|
|
98
|
+
MEMORY_TAGS.TECH_STACK,
|
|
99
|
+
MEMORY_TAGS.DEPENDENCIES,
|
|
100
|
+
],
|
|
101
|
+
general: Object.values(MEMORY_TAGS) as MemoryTag[],
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
/**
|
|
105
|
+
* Semantic keywords for each domain.
|
|
106
|
+
* Used to resolve unknown domain strings (e.g., "uxui" → frontend)
|
|
107
|
+
* and for partial scoring when memory tags relate semantically.
|
|
108
|
+
* @see PRJ-300
|
|
109
|
+
*/
|
|
110
|
+
export const SEMANTIC_DOMAIN_KEYWORDS: Record<string, string[]> = {
|
|
111
|
+
frontend: [
|
|
112
|
+
'ui',
|
|
113
|
+
'ux',
|
|
114
|
+
'uxui',
|
|
115
|
+
'css',
|
|
116
|
+
'styling',
|
|
117
|
+
'component',
|
|
118
|
+
'layout',
|
|
119
|
+
'design',
|
|
120
|
+
'responsive',
|
|
121
|
+
'react',
|
|
122
|
+
'vue',
|
|
123
|
+
'svelte',
|
|
124
|
+
'angular',
|
|
125
|
+
'html',
|
|
126
|
+
'tailwind',
|
|
127
|
+
'sass',
|
|
128
|
+
'web',
|
|
129
|
+
'accessibility',
|
|
130
|
+
'a11y',
|
|
131
|
+
],
|
|
132
|
+
backend: [
|
|
133
|
+
'api',
|
|
134
|
+
'server',
|
|
135
|
+
'route',
|
|
136
|
+
'endpoint',
|
|
137
|
+
'rest',
|
|
138
|
+
'graphql',
|
|
139
|
+
'middleware',
|
|
140
|
+
'worker',
|
|
141
|
+
'queue',
|
|
142
|
+
'auth',
|
|
143
|
+
'hono',
|
|
144
|
+
'express',
|
|
145
|
+
'service',
|
|
146
|
+
'microservice',
|
|
147
|
+
],
|
|
148
|
+
devops: [
|
|
149
|
+
'ci',
|
|
150
|
+
'cd',
|
|
151
|
+
'docker',
|
|
152
|
+
'kubernetes',
|
|
153
|
+
'deploy',
|
|
154
|
+
'infra',
|
|
155
|
+
'infrastructure',
|
|
156
|
+
'monitoring',
|
|
157
|
+
'cloud',
|
|
158
|
+
'aws',
|
|
159
|
+
'gcp',
|
|
160
|
+
'azure',
|
|
161
|
+
'pipeline',
|
|
162
|
+
'helm',
|
|
163
|
+
'terraform',
|
|
164
|
+
],
|
|
165
|
+
docs: ['documentation', 'readme', 'guide', 'tutorial', 'wiki', 'changelog', 'jsdoc', 'typedoc'],
|
|
166
|
+
testing: [
|
|
167
|
+
'test',
|
|
168
|
+
'spec',
|
|
169
|
+
'e2e',
|
|
170
|
+
'unit',
|
|
171
|
+
'integration',
|
|
172
|
+
'coverage',
|
|
173
|
+
'mock',
|
|
174
|
+
'vitest',
|
|
175
|
+
'jest',
|
|
176
|
+
'playwright',
|
|
177
|
+
'cypress',
|
|
178
|
+
],
|
|
179
|
+
database: [
|
|
180
|
+
'db',
|
|
181
|
+
'sql',
|
|
182
|
+
'schema',
|
|
183
|
+
'migration',
|
|
184
|
+
'query',
|
|
185
|
+
'orm',
|
|
186
|
+
'prisma',
|
|
187
|
+
'mongo',
|
|
188
|
+
'postgres',
|
|
189
|
+
'redis',
|
|
190
|
+
'drizzle',
|
|
191
|
+
'sqlite',
|
|
192
|
+
],
|
|
193
|
+
general: [],
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
/**
|
|
197
|
+
* Resolve a domain string to canonical known domain(s).
|
|
198
|
+
* Known domains pass through; unknown domains are matched via semantic keywords.
|
|
199
|
+
* Exported for testing.
|
|
200
|
+
* @see PRJ-300
|
|
201
|
+
*/
|
|
202
|
+
export function resolveCanonicalDomains(domain: string): KnownDomain[] {
|
|
203
|
+
// Exact match
|
|
204
|
+
if ((KNOWN_DOMAINS as readonly string[]).includes(domain)) {
|
|
205
|
+
return [domain as KnownDomain]
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
// Semantic resolution — find canonical domains whose keywords match
|
|
209
|
+
const normalized = domain.toLowerCase().replace(/[-_\s]/g, '')
|
|
210
|
+
const matches: KnownDomain[] = []
|
|
211
|
+
|
|
212
|
+
for (const [canonical, keywords] of Object.entries(SEMANTIC_DOMAIN_KEYWORDS)) {
|
|
213
|
+
if (canonical === 'general') continue
|
|
214
|
+
for (const kw of keywords) {
|
|
215
|
+
if (normalized.includes(kw) || kw.includes(normalized)) {
|
|
216
|
+
matches.push(canonical as KnownDomain)
|
|
217
|
+
break
|
|
218
|
+
}
|
|
219
|
+
}
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
return matches.length > 0 ? matches : ['general']
|
|
223
|
+
}
|
|
62
224
|
|
|
63
225
|
// =============================================================================
|
|
64
226
|
// Base Store
|
|
@@ -858,11 +1020,9 @@ export class SemanticMemories extends CachedStore<MemoryDatabase> {
|
|
|
858
1020
|
userTriggered: 0,
|
|
859
1021
|
}
|
|
860
1022
|
|
|
861
|
-
// Domain match scoring (0-25 points)
|
|
1023
|
+
// Domain match scoring (0-25 points) — semantic matching (PRJ-300)
|
|
862
1024
|
if (query.taskDomain) {
|
|
863
|
-
|
|
864
|
-
const matchingTags = (memory.tags || []).filter((tag) => domainTags.includes(tag))
|
|
865
|
-
breakdown.domainMatch = Math.min(25, matchingTags.length * 10)
|
|
1025
|
+
breakdown.domainMatch = this._getSemanticDomainScore(query.taskDomain, memory.tags || [])
|
|
866
1026
|
}
|
|
867
1027
|
|
|
868
1028
|
// Tag match from command context (0-20 points)
|
|
@@ -943,25 +1103,66 @@ export class SemanticMemories extends CachedStore<MemoryDatabase> {
|
|
|
943
1103
|
}
|
|
944
1104
|
|
|
945
1105
|
/**
|
|
946
|
-
*
|
|
947
|
-
*
|
|
1106
|
+
* Compute semantic domain match score (0-25 points).
|
|
1107
|
+
*
|
|
1108
|
+
* Two-pass scoring:
|
|
1109
|
+
* 1. Exact MEMORY_TAG match: memory tag in domain's tag list → 10 pts each
|
|
1110
|
+
* 2. Semantic match: memory tag relates to domain via keywords → 5 pts each
|
|
1111
|
+
*
|
|
1112
|
+
* Unknown domains are resolved to canonical domain(s) via SEMANTIC_DOMAIN_KEYWORDS.
|
|
1113
|
+
* @see PRJ-107, PRJ-300
|
|
948
1114
|
*/
|
|
949
|
-
private
|
|
950
|
-
|
|
951
|
-
|
|
952
|
-
|
|
953
|
-
|
|
954
|
-
|
|
955
|
-
|
|
956
|
-
|
|
957
|
-
]
|
|
958
|
-
|
|
959
|
-
|
|
960
|
-
|
|
961
|
-
|
|
962
|
-
|
|
1115
|
+
private _getSemanticDomainScore(domain: TaskDomain, memoryTags: string[]): number {
|
|
1116
|
+
// Resolve to canonical domain(s)
|
|
1117
|
+
const canonicals = this._resolveCanonicalDomains(domain)
|
|
1118
|
+
if (canonicals.length === 0) return 0
|
|
1119
|
+
|
|
1120
|
+
// Collect all relevant MEMORY_TAGS for the canonical domains
|
|
1121
|
+
const relevantTags = new Set<string>()
|
|
1122
|
+
for (const canonical of canonicals) {
|
|
1123
|
+
const tags = DOMAIN_TAG_MAP[canonical]
|
|
1124
|
+
if (tags) {
|
|
1125
|
+
for (const tag of tags) relevantTags.add(tag)
|
|
1126
|
+
}
|
|
1127
|
+
}
|
|
1128
|
+
|
|
1129
|
+
// Collect semantic keywords for the canonical domains
|
|
1130
|
+
const domainKeywords = new Set<string>()
|
|
1131
|
+
for (const canonical of canonicals) {
|
|
1132
|
+
const keywords = SEMANTIC_DOMAIN_KEYWORDS[canonical]
|
|
1133
|
+
if (keywords) {
|
|
1134
|
+
for (const kw of keywords) domainKeywords.add(kw)
|
|
1135
|
+
}
|
|
1136
|
+
}
|
|
1137
|
+
|
|
1138
|
+
let score = 0
|
|
1139
|
+
|
|
1140
|
+
for (const tag of memoryTags) {
|
|
1141
|
+
// Pass 1: exact MEMORY_TAG match (10 pts)
|
|
1142
|
+
if (relevantTags.has(tag)) {
|
|
1143
|
+
score += 10
|
|
1144
|
+
continue
|
|
1145
|
+
}
|
|
1146
|
+
// Pass 2: semantic keyword match (5 pts)
|
|
1147
|
+
const normalized = tag.toLowerCase().replace(/[-_\s]/g, '')
|
|
1148
|
+
for (const kw of domainKeywords) {
|
|
1149
|
+
if (normalized.includes(kw) || kw.includes(normalized)) {
|
|
1150
|
+
score += 5
|
|
1151
|
+
break
|
|
1152
|
+
}
|
|
1153
|
+
}
|
|
963
1154
|
}
|
|
964
|
-
|
|
1155
|
+
|
|
1156
|
+
return Math.min(25, score)
|
|
1157
|
+
}
|
|
1158
|
+
|
|
1159
|
+
/**
|
|
1160
|
+
* Resolve a domain string to canonical known domain(s).
|
|
1161
|
+
* Delegates to module-level resolveCanonicalDomains().
|
|
1162
|
+
* @see PRJ-300
|
|
1163
|
+
*/
|
|
1164
|
+
private _resolveCanonicalDomains(domain: TaskDomain): KnownDomain[] {
|
|
1165
|
+
return resolveCanonicalDomains(domain)
|
|
965
1166
|
}
|
|
966
1167
|
|
|
967
1168
|
/**
|
|
@@ -39,6 +39,7 @@ import {
|
|
|
39
39
|
InjectionBudgetTracker,
|
|
40
40
|
truncateToTokenBudget,
|
|
41
41
|
} from './injection-validator'
|
|
42
|
+
import { deduplicateTechStack } from './tech-normalizer'
|
|
42
43
|
import type { TokenBudgetCoordinator } from './token-budget'
|
|
43
44
|
|
|
44
45
|
// =============================================================================
|
|
@@ -689,10 +690,17 @@ class PromptBuilder {
|
|
|
689
690
|
|
|
690
691
|
if (projectPath) {
|
|
691
692
|
const sa = orchestratorContext?.sealedAnalysis
|
|
693
|
+
// PRJ-300: prefer sealed analysis frameworks as primary tech stack,
|
|
694
|
+
// falling back to repo conventions. Deduplicate with normalized matching.
|
|
695
|
+
const rawStack = [
|
|
696
|
+
...(sa?.frameworks || []),
|
|
697
|
+
...(orchestratorContext?.project?.conventions || []),
|
|
698
|
+
]
|
|
692
699
|
const groundTruth: ProjectGroundTruth = {
|
|
693
700
|
projectPath,
|
|
694
701
|
language: orchestratorContext?.project?.ecosystem,
|
|
695
|
-
|
|
702
|
+
framework: sa?.frameworks?.[0],
|
|
703
|
+
techStack: deduplicateTechStack(rawStack),
|
|
696
704
|
domains: this.extractDomains(state),
|
|
697
705
|
fileCount: context.files?.length || context.filteredSize || 0,
|
|
698
706
|
availableAgents: orchestratorContext?.agents?.map((a) => a.name) || [],
|
|
@@ -0,0 +1,167 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Tech Normalizer (PRJ-300)
|
|
3
|
+
*
|
|
4
|
+
* Normalized matching for tech stack / framework names.
|
|
5
|
+
* Handles:
|
|
6
|
+
* - Compound names: "React + TypeScript" → ["react", "typescript"]
|
|
7
|
+
* - Framework aliases: "nextjs" → "next.js"
|
|
8
|
+
* - Framework families: "Next.js" → react family
|
|
9
|
+
* - Case-insensitive, whitespace-insensitive matching
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
// =============================================================================
|
|
13
|
+
// Framework Families
|
|
14
|
+
// =============================================================================
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* Map framework names to their family (meta-framework → base framework).
|
|
18
|
+
* Used to verify that "Next.js" matches expected "react".
|
|
19
|
+
*/
|
|
20
|
+
const FRAMEWORK_FAMILIES: Record<string, string> = {
|
|
21
|
+
'next.js': 'react',
|
|
22
|
+
nextjs: 'react',
|
|
23
|
+
remix: 'react',
|
|
24
|
+
gatsby: 'react',
|
|
25
|
+
'react native': 'react',
|
|
26
|
+
expo: 'react',
|
|
27
|
+
nuxt: 'vue',
|
|
28
|
+
'nuxt.js': 'vue',
|
|
29
|
+
nuxtjs: 'vue',
|
|
30
|
+
sveltekit: 'svelte',
|
|
31
|
+
analog: 'angular',
|
|
32
|
+
astro: 'multi',
|
|
33
|
+
vite: 'multi',
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* Aliases that normalize to a canonical name.
|
|
38
|
+
*/
|
|
39
|
+
const FRAMEWORK_ALIASES: Record<string, string> = {
|
|
40
|
+
nextjs: 'next.js',
|
|
41
|
+
nuxtjs: 'nuxt.js',
|
|
42
|
+
expressjs: 'express',
|
|
43
|
+
fastifyjs: 'fastify',
|
|
44
|
+
'react.js': 'react',
|
|
45
|
+
'vue.js': 'vue',
|
|
46
|
+
'svelte.js': 'svelte',
|
|
47
|
+
'angular.js': 'angular',
|
|
48
|
+
angularjs: 'angular',
|
|
49
|
+
'node.js': 'node',
|
|
50
|
+
nodejs: 'node',
|
|
51
|
+
ts: 'typescript',
|
|
52
|
+
js: 'javascript',
|
|
53
|
+
pg: 'postgres',
|
|
54
|
+
postgresql: 'postgres',
|
|
55
|
+
mongo: 'mongodb',
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
// =============================================================================
|
|
59
|
+
// Core Functions
|
|
60
|
+
// =============================================================================
|
|
61
|
+
|
|
62
|
+
/**
|
|
63
|
+
* Normalize a single framework/tech name.
|
|
64
|
+
* Strips whitespace, lowercases, resolves aliases.
|
|
65
|
+
*
|
|
66
|
+
* @example
|
|
67
|
+
* normalizeFrameworkName("Next.js") → "next.js"
|
|
68
|
+
* normalizeFrameworkName(" TypeScript ") → "typescript"
|
|
69
|
+
* normalizeFrameworkName("NodeJS") → "node"
|
|
70
|
+
*/
|
|
71
|
+
export function normalizeFrameworkName(name: string): string {
|
|
72
|
+
const trimmed = name.trim().toLowerCase()
|
|
73
|
+
|
|
74
|
+
// Check aliases
|
|
75
|
+
const aliasKey = trimmed.replace(/[.\s-]/g, '')
|
|
76
|
+
if (FRAMEWORK_ALIASES[aliasKey]) {
|
|
77
|
+
return FRAMEWORK_ALIASES[aliasKey]
|
|
78
|
+
}
|
|
79
|
+
if (FRAMEWORK_ALIASES[trimmed]) {
|
|
80
|
+
return FRAMEWORK_ALIASES[trimmed]
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
return trimmed
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
/**
|
|
87
|
+
* Extract individual tech names from a compound string.
|
|
88
|
+
* Handles separators: +, /, comma, parentheses, "with", "and".
|
|
89
|
+
*
|
|
90
|
+
* @example
|
|
91
|
+
* extractTechNames("React + TypeScript") → ["react", "typescript"]
|
|
92
|
+
* extractTechNames("Next.js (React)") → ["next.js", "react"]
|
|
93
|
+
* extractTechNames("Hono with Zod") → ["hono", "zod"]
|
|
94
|
+
*/
|
|
95
|
+
export function extractTechNames(compound: string): string[] {
|
|
96
|
+
// Replace parentheses with comma separators to split them out
|
|
97
|
+
const parts = compound
|
|
98
|
+
.replace(/[()]/g, ',')
|
|
99
|
+
.split(/[+/,]|\bwith\b|\band\b/i)
|
|
100
|
+
.map((s) => s.trim())
|
|
101
|
+
.filter((s) => s.length > 0)
|
|
102
|
+
|
|
103
|
+
return parts.map(normalizeFrameworkName)
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
/**
|
|
107
|
+
* Get the framework family for a tech name.
|
|
108
|
+
* Returns the base framework or the name itself if no family exists.
|
|
109
|
+
*
|
|
110
|
+
* @example
|
|
111
|
+
* getFrameworkFamily("next.js") → "react"
|
|
112
|
+
* getFrameworkFamily("nuxt") → "vue"
|
|
113
|
+
* getFrameworkFamily("express") → "express"
|
|
114
|
+
*/
|
|
115
|
+
export function getFrameworkFamily(name: string): string {
|
|
116
|
+
const normalized = normalizeFrameworkName(name)
|
|
117
|
+
return FRAMEWORK_FAMILIES[normalized] || normalized
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
/**
|
|
121
|
+
* Check if two tech names match (direct or via family).
|
|
122
|
+
*
|
|
123
|
+
* @example
|
|
124
|
+
* matchesTech("Next.js", "react") → true (Next.js is React family)
|
|
125
|
+
* matchesTech("React + TypeScript", "react") → true (contains react)
|
|
126
|
+
* matchesTech("Vue", "react") → false
|
|
127
|
+
*/
|
|
128
|
+
export function matchesTech(actual: string, expected: string): boolean {
|
|
129
|
+
const expectedNorm = normalizeFrameworkName(expected)
|
|
130
|
+
|
|
131
|
+
// Extract all tech names from actual (handles compound names)
|
|
132
|
+
const actualNames = extractTechNames(actual)
|
|
133
|
+
|
|
134
|
+
for (const name of actualNames) {
|
|
135
|
+
// Direct match
|
|
136
|
+
if (name === expectedNorm) return true
|
|
137
|
+
// Family match
|
|
138
|
+
if (getFrameworkFamily(name) === expectedNorm) return true
|
|
139
|
+
// Reverse family match
|
|
140
|
+
if (getFrameworkFamily(expectedNorm) === name) return true
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
return false
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
/**
|
|
147
|
+
* Deduplicate a tech stack list using normalized matching.
|
|
148
|
+
* Preserves the first occurrence's original casing.
|
|
149
|
+
*
|
|
150
|
+
* @example
|
|
151
|
+
* deduplicateTechStack(["React", "react", "Next.js"]) → ["React", "Next.js"]
|
|
152
|
+
* deduplicateTechStack(["TypeScript", "ts"]) → ["TypeScript"]
|
|
153
|
+
*/
|
|
154
|
+
export function deduplicateTechStack(stack: string[]): string[] {
|
|
155
|
+
const seen = new Set<string>()
|
|
156
|
+
const result: string[] = []
|
|
157
|
+
|
|
158
|
+
for (const name of stack) {
|
|
159
|
+
const normalized = normalizeFrameworkName(name)
|
|
160
|
+
if (!seen.has(normalized)) {
|
|
161
|
+
seen.add(normalized)
|
|
162
|
+
result.push(name)
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
return result
|
|
167
|
+
}
|
package/core/types/memory.ts
CHANGED
|
@@ -86,17 +86,27 @@ export interface MemoryQuery {
|
|
|
86
86
|
}
|
|
87
87
|
|
|
88
88
|
/**
|
|
89
|
-
*
|
|
89
|
+
* Known domain types for task context.
|
|
90
90
|
* @see PRJ-107
|
|
91
91
|
*/
|
|
92
|
-
export
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
92
|
+
export const KNOWN_DOMAINS = [
|
|
93
|
+
'frontend',
|
|
94
|
+
'backend',
|
|
95
|
+
'devops',
|
|
96
|
+
'docs',
|
|
97
|
+
'testing',
|
|
98
|
+
'database',
|
|
99
|
+
'general',
|
|
100
|
+
] as const
|
|
101
|
+
export type KnownDomain = (typeof KNOWN_DOMAINS)[number]
|
|
102
|
+
|
|
103
|
+
/**
|
|
104
|
+
* Task domain — accepts known domains with autocomplete + any string for
|
|
105
|
+
* semantic resolution. Unknown domains are resolved to the closest canonical
|
|
106
|
+
* domain via SEMANTIC_DOMAIN_KEYWORDS.
|
|
107
|
+
* @see PRJ-107, PRJ-300
|
|
108
|
+
*/
|
|
109
|
+
export type TaskDomain = KnownDomain | (string & {})
|
|
100
110
|
|
|
101
111
|
/**
|
|
102
112
|
* Enhanced query parameters for selective memory retrieval.
|
package/dist/bin/prjct.mjs
CHANGED
|
@@ -10482,7 +10482,7 @@ function calculateConfidence(count, userConfirmed = false) {
|
|
|
10482
10482
|
if (count >= 3) return "medium";
|
|
10483
10483
|
return "low";
|
|
10484
10484
|
}
|
|
10485
|
-
var MEMORY_TAGS;
|
|
10485
|
+
var MEMORY_TAGS, KNOWN_DOMAINS;
|
|
10486
10486
|
var init_memory = __esm({
|
|
10487
10487
|
"core/types/memory.ts"() {
|
|
10488
10488
|
"use strict";
|
|
@@ -10505,6 +10505,15 @@ var init_memory = __esm({
|
|
|
10505
10505
|
CONFIRMATION_LEVEL: "confirmation_level",
|
|
10506
10506
|
AGENT_PREFERENCE: "agent_preference"
|
|
10507
10507
|
};
|
|
10508
|
+
KNOWN_DOMAINS = [
|
|
10509
|
+
"frontend",
|
|
10510
|
+
"backend",
|
|
10511
|
+
"devops",
|
|
10512
|
+
"docs",
|
|
10513
|
+
"testing",
|
|
10514
|
+
"database",
|
|
10515
|
+
"general"
|
|
10516
|
+
];
|
|
10508
10517
|
__name(calculateConfidence, "calculateConfidence");
|
|
10509
10518
|
}
|
|
10510
10519
|
});
|
|
@@ -10512,7 +10521,24 @@ var init_memory = __esm({
|
|
|
10512
10521
|
// core/agentic/memory-system.ts
|
|
10513
10522
|
import fs25 from "node:fs/promises";
|
|
10514
10523
|
import path23 from "node:path";
|
|
10515
|
-
|
|
10524
|
+
function resolveCanonicalDomains(domain) {
|
|
10525
|
+
if (KNOWN_DOMAINS.includes(domain)) {
|
|
10526
|
+
return [domain];
|
|
10527
|
+
}
|
|
10528
|
+
const normalized = domain.toLowerCase().replace(/[-_\s]/g, "");
|
|
10529
|
+
const matches = [];
|
|
10530
|
+
for (const [canonical, keywords] of Object.entries(SEMANTIC_DOMAIN_KEYWORDS)) {
|
|
10531
|
+
if (canonical === "general") continue;
|
|
10532
|
+
for (const kw of keywords) {
|
|
10533
|
+
if (normalized.includes(kw) || kw.includes(normalized)) {
|
|
10534
|
+
matches.push(canonical);
|
|
10535
|
+
break;
|
|
10536
|
+
}
|
|
10537
|
+
}
|
|
10538
|
+
}
|
|
10539
|
+
return matches.length > 0 ? matches : ["general"];
|
|
10540
|
+
}
|
|
10541
|
+
var DOMAIN_TAG_MAP, SEMANTIC_DOMAIN_KEYWORDS, CachedStore, SessionStore, HistoryStore, PatternStore, SemanticMemories, MemorySystem, memorySystem, memory_system_default;
|
|
10516
10542
|
var init_memory_system = __esm({
|
|
10517
10543
|
"core/agentic/memory-system.ts"() {
|
|
10518
10544
|
"use strict";
|
|
@@ -10524,6 +10550,121 @@ var init_memory_system = __esm({
|
|
|
10524
10550
|
init_jsonl_helper();
|
|
10525
10551
|
init_memory();
|
|
10526
10552
|
init_memory();
|
|
10553
|
+
DOMAIN_TAG_MAP = {
|
|
10554
|
+
frontend: [
|
|
10555
|
+
MEMORY_TAGS.CODE_STYLE,
|
|
10556
|
+
MEMORY_TAGS.FILE_STRUCTURE,
|
|
10557
|
+
MEMORY_TAGS.ARCHITECTURE,
|
|
10558
|
+
MEMORY_TAGS.TECH_STACK
|
|
10559
|
+
],
|
|
10560
|
+
backend: [
|
|
10561
|
+
MEMORY_TAGS.CODE_STYLE,
|
|
10562
|
+
MEMORY_TAGS.ARCHITECTURE,
|
|
10563
|
+
MEMORY_TAGS.DEPENDENCIES,
|
|
10564
|
+
MEMORY_TAGS.TECH_STACK
|
|
10565
|
+
],
|
|
10566
|
+
devops: [
|
|
10567
|
+
MEMORY_TAGS.SHIP_WORKFLOW,
|
|
10568
|
+
MEMORY_TAGS.TEST_BEHAVIOR,
|
|
10569
|
+
MEMORY_TAGS.DEPENDENCIES,
|
|
10570
|
+
MEMORY_TAGS.ARCHITECTURE
|
|
10571
|
+
],
|
|
10572
|
+
docs: [MEMORY_TAGS.CODE_STYLE, MEMORY_TAGS.NAMING_CONVENTION, MEMORY_TAGS.FILE_STRUCTURE],
|
|
10573
|
+
testing: [MEMORY_TAGS.TEST_BEHAVIOR, MEMORY_TAGS.CODE_STYLE, MEMORY_TAGS.DEPENDENCIES],
|
|
10574
|
+
database: [
|
|
10575
|
+
MEMORY_TAGS.ARCHITECTURE,
|
|
10576
|
+
MEMORY_TAGS.NAMING_CONVENTION,
|
|
10577
|
+
MEMORY_TAGS.TECH_STACK,
|
|
10578
|
+
MEMORY_TAGS.DEPENDENCIES
|
|
10579
|
+
],
|
|
10580
|
+
general: Object.values(MEMORY_TAGS)
|
|
10581
|
+
};
|
|
10582
|
+
SEMANTIC_DOMAIN_KEYWORDS = {
|
|
10583
|
+
frontend: [
|
|
10584
|
+
"ui",
|
|
10585
|
+
"ux",
|
|
10586
|
+
"uxui",
|
|
10587
|
+
"css",
|
|
10588
|
+
"styling",
|
|
10589
|
+
"component",
|
|
10590
|
+
"layout",
|
|
10591
|
+
"design",
|
|
10592
|
+
"responsive",
|
|
10593
|
+
"react",
|
|
10594
|
+
"vue",
|
|
10595
|
+
"svelte",
|
|
10596
|
+
"angular",
|
|
10597
|
+
"html",
|
|
10598
|
+
"tailwind",
|
|
10599
|
+
"sass",
|
|
10600
|
+
"web",
|
|
10601
|
+
"accessibility",
|
|
10602
|
+
"a11y"
|
|
10603
|
+
],
|
|
10604
|
+
backend: [
|
|
10605
|
+
"api",
|
|
10606
|
+
"server",
|
|
10607
|
+
"route",
|
|
10608
|
+
"endpoint",
|
|
10609
|
+
"rest",
|
|
10610
|
+
"graphql",
|
|
10611
|
+
"middleware",
|
|
10612
|
+
"worker",
|
|
10613
|
+
"queue",
|
|
10614
|
+
"auth",
|
|
10615
|
+
"hono",
|
|
10616
|
+
"express",
|
|
10617
|
+
"service",
|
|
10618
|
+
"microservice"
|
|
10619
|
+
],
|
|
10620
|
+
devops: [
|
|
10621
|
+
"ci",
|
|
10622
|
+
"cd",
|
|
10623
|
+
"docker",
|
|
10624
|
+
"kubernetes",
|
|
10625
|
+
"deploy",
|
|
10626
|
+
"infra",
|
|
10627
|
+
"infrastructure",
|
|
10628
|
+
"monitoring",
|
|
10629
|
+
"cloud",
|
|
10630
|
+
"aws",
|
|
10631
|
+
"gcp",
|
|
10632
|
+
"azure",
|
|
10633
|
+
"pipeline",
|
|
10634
|
+
"helm",
|
|
10635
|
+
"terraform"
|
|
10636
|
+
],
|
|
10637
|
+
docs: ["documentation", "readme", "guide", "tutorial", "wiki", "changelog", "jsdoc", "typedoc"],
|
|
10638
|
+
testing: [
|
|
10639
|
+
"test",
|
|
10640
|
+
"spec",
|
|
10641
|
+
"e2e",
|
|
10642
|
+
"unit",
|
|
10643
|
+
"integration",
|
|
10644
|
+
"coverage",
|
|
10645
|
+
"mock",
|
|
10646
|
+
"vitest",
|
|
10647
|
+
"jest",
|
|
10648
|
+
"playwright",
|
|
10649
|
+
"cypress"
|
|
10650
|
+
],
|
|
10651
|
+
database: [
|
|
10652
|
+
"db",
|
|
10653
|
+
"sql",
|
|
10654
|
+
"schema",
|
|
10655
|
+
"migration",
|
|
10656
|
+
"query",
|
|
10657
|
+
"orm",
|
|
10658
|
+
"prisma",
|
|
10659
|
+
"mongo",
|
|
10660
|
+
"postgres",
|
|
10661
|
+
"redis",
|
|
10662
|
+
"drizzle",
|
|
10663
|
+
"sqlite"
|
|
10664
|
+
],
|
|
10665
|
+
general: []
|
|
10666
|
+
};
|
|
10667
|
+
__name(resolveCanonicalDomains, "resolveCanonicalDomains");
|
|
10527
10668
|
CachedStore = class {
|
|
10528
10669
|
static {
|
|
10529
10670
|
__name(this, "CachedStore");
|
|
@@ -11101,9 +11242,7 @@ var init_memory_system = __esm({
|
|
|
11101
11242
|
userTriggered: 0
|
|
11102
11243
|
};
|
|
11103
11244
|
if (query.taskDomain) {
|
|
11104
|
-
|
|
11105
|
-
const matchingTags = (memory.tags || []).filter((tag) => domainTags.includes(tag));
|
|
11106
|
-
breakdown.domainMatch = Math.min(25, matchingTags.length * 10);
|
|
11245
|
+
breakdown.domainMatch = this._getSemanticDomainScore(query.taskDomain, memory.tags || []);
|
|
11107
11246
|
}
|
|
11108
11247
|
if (query.commandName) {
|
|
11109
11248
|
const commandTags = this._getCommandTags(query.commandName);
|
|
@@ -11153,25 +11292,55 @@ var init_memory_system = __esm({
|
|
|
11153
11292
|
};
|
|
11154
11293
|
}
|
|
11155
11294
|
/**
|
|
11156
|
-
*
|
|
11157
|
-
*
|
|
11295
|
+
* Compute semantic domain match score (0-25 points).
|
|
11296
|
+
*
|
|
11297
|
+
* Two-pass scoring:
|
|
11298
|
+
* 1. Exact MEMORY_TAG match: memory tag in domain's tag list → 10 pts each
|
|
11299
|
+
* 2. Semantic match: memory tag relates to domain via keywords → 5 pts each
|
|
11300
|
+
*
|
|
11301
|
+
* Unknown domains are resolved to canonical domain(s) via SEMANTIC_DOMAIN_KEYWORDS.
|
|
11302
|
+
* @see PRJ-107, PRJ-300
|
|
11303
|
+
*/
|
|
11304
|
+
_getSemanticDomainScore(domain, memoryTags) {
|
|
11305
|
+
const canonicals = this._resolveCanonicalDomains(domain);
|
|
11306
|
+
if (canonicals.length === 0) return 0;
|
|
11307
|
+
const relevantTags = /* @__PURE__ */ new Set();
|
|
11308
|
+
for (const canonical of canonicals) {
|
|
11309
|
+
const tags = DOMAIN_TAG_MAP[canonical];
|
|
11310
|
+
if (tags) {
|
|
11311
|
+
for (const tag of tags) relevantTags.add(tag);
|
|
11312
|
+
}
|
|
11313
|
+
}
|
|
11314
|
+
const domainKeywords = /* @__PURE__ */ new Set();
|
|
11315
|
+
for (const canonical of canonicals) {
|
|
11316
|
+
const keywords = SEMANTIC_DOMAIN_KEYWORDS[canonical];
|
|
11317
|
+
if (keywords) {
|
|
11318
|
+
for (const kw of keywords) domainKeywords.add(kw);
|
|
11319
|
+
}
|
|
11320
|
+
}
|
|
11321
|
+
let score = 0;
|
|
11322
|
+
for (const tag of memoryTags) {
|
|
11323
|
+
if (relevantTags.has(tag)) {
|
|
11324
|
+
score += 10;
|
|
11325
|
+
continue;
|
|
11326
|
+
}
|
|
11327
|
+
const normalized = tag.toLowerCase().replace(/[-_\s]/g, "");
|
|
11328
|
+
for (const kw of domainKeywords) {
|
|
11329
|
+
if (normalized.includes(kw) || kw.includes(normalized)) {
|
|
11330
|
+
score += 5;
|
|
11331
|
+
break;
|
|
11332
|
+
}
|
|
11333
|
+
}
|
|
11334
|
+
}
|
|
11335
|
+
return Math.min(25, score);
|
|
11336
|
+
}
|
|
11337
|
+
/**
|
|
11338
|
+
* Resolve a domain string to canonical known domain(s).
|
|
11339
|
+
* Delegates to module-level resolveCanonicalDomains().
|
|
11340
|
+
* @see PRJ-300
|
|
11158
11341
|
*/
|
|
11159
|
-
|
|
11160
|
-
|
|
11161
|
-
frontend: [MEMORY_TAGS.CODE_STYLE, MEMORY_TAGS.FILE_STRUCTURE, MEMORY_TAGS.ARCHITECTURE],
|
|
11162
|
-
backend: [
|
|
11163
|
-
MEMORY_TAGS.CODE_STYLE,
|
|
11164
|
-
MEMORY_TAGS.ARCHITECTURE,
|
|
11165
|
-
MEMORY_TAGS.DEPENDENCIES,
|
|
11166
|
-
MEMORY_TAGS.TECH_STACK
|
|
11167
|
-
],
|
|
11168
|
-
devops: [MEMORY_TAGS.SHIP_WORKFLOW, MEMORY_TAGS.TEST_BEHAVIOR, MEMORY_TAGS.DEPENDENCIES],
|
|
11169
|
-
docs: [MEMORY_TAGS.CODE_STYLE, MEMORY_TAGS.NAMING_CONVENTION],
|
|
11170
|
-
testing: [MEMORY_TAGS.TEST_BEHAVIOR, MEMORY_TAGS.CODE_STYLE],
|
|
11171
|
-
database: [MEMORY_TAGS.ARCHITECTURE, MEMORY_TAGS.NAMING_CONVENTION],
|
|
11172
|
-
general: Object.values(MEMORY_TAGS)
|
|
11173
|
-
};
|
|
11174
|
-
return domainTagMap[domain] || [];
|
|
11342
|
+
_resolveCanonicalDomains(domain) {
|
|
11343
|
+
return resolveCanonicalDomains(domain);
|
|
11175
11344
|
}
|
|
11176
11345
|
/**
|
|
11177
11346
|
* Map command to relevant memory tags.
|
|
@@ -15894,28 +16063,71 @@ var init_outcomes2 = __esm({
|
|
|
15894
16063
|
}
|
|
15895
16064
|
});
|
|
15896
16065
|
|
|
16066
|
+
// core/agentic/tech-normalizer.ts
|
|
16067
|
+
function normalizeFrameworkName(name) {
|
|
16068
|
+
const trimmed = name.trim().toLowerCase();
|
|
16069
|
+
const aliasKey = trimmed.replace(/[.\s-]/g, "");
|
|
16070
|
+
if (FRAMEWORK_ALIASES[aliasKey]) {
|
|
16071
|
+
return FRAMEWORK_ALIASES[aliasKey];
|
|
16072
|
+
}
|
|
16073
|
+
if (FRAMEWORK_ALIASES[trimmed]) {
|
|
16074
|
+
return FRAMEWORK_ALIASES[trimmed];
|
|
16075
|
+
}
|
|
16076
|
+
return trimmed;
|
|
16077
|
+
}
|
|
16078
|
+
function deduplicateTechStack(stack) {
|
|
16079
|
+
const seen = /* @__PURE__ */ new Set();
|
|
16080
|
+
const result = [];
|
|
16081
|
+
for (const name of stack) {
|
|
16082
|
+
const normalized = normalizeFrameworkName(name);
|
|
16083
|
+
if (!seen.has(normalized)) {
|
|
16084
|
+
seen.add(normalized);
|
|
16085
|
+
result.push(name);
|
|
16086
|
+
}
|
|
16087
|
+
}
|
|
16088
|
+
return result;
|
|
16089
|
+
}
|
|
16090
|
+
var FRAMEWORK_ALIASES;
|
|
16091
|
+
var init_tech_normalizer = __esm({
|
|
16092
|
+
"core/agentic/tech-normalizer.ts"() {
|
|
16093
|
+
"use strict";
|
|
16094
|
+
FRAMEWORK_ALIASES = {
|
|
16095
|
+
nextjs: "next.js",
|
|
16096
|
+
nuxtjs: "nuxt.js",
|
|
16097
|
+
expressjs: "express",
|
|
16098
|
+
fastifyjs: "fastify",
|
|
16099
|
+
"react.js": "react",
|
|
16100
|
+
"vue.js": "vue",
|
|
16101
|
+
"svelte.js": "svelte",
|
|
16102
|
+
"angular.js": "angular",
|
|
16103
|
+
angularjs: "angular",
|
|
16104
|
+
"node.js": "node",
|
|
16105
|
+
nodejs: "node",
|
|
16106
|
+
ts: "typescript",
|
|
16107
|
+
js: "javascript",
|
|
16108
|
+
pg: "postgres",
|
|
16109
|
+
postgresql: "postgres",
|
|
16110
|
+
mongo: "mongodb"
|
|
16111
|
+
};
|
|
16112
|
+
__name(normalizeFrameworkName, "normalizeFrameworkName");
|
|
16113
|
+
__name(deduplicateTechStack, "deduplicateTechStack");
|
|
16114
|
+
}
|
|
16115
|
+
});
|
|
16116
|
+
|
|
15897
16117
|
// core/agentic/anti-hallucination.ts
|
|
15898
16118
|
import { z as z16 } from "zod";
|
|
15899
16119
|
function buildAntiHallucinationBlock(truth) {
|
|
15900
16120
|
const parts = [];
|
|
15901
16121
|
parts.push("## CONSTRAINTS (Read Before Acting)\n");
|
|
15902
|
-
const
|
|
15903
|
-
if (truth.language)
|
|
15904
|
-
if (truth.framework)
|
|
16122
|
+
const rawAvailable = [];
|
|
16123
|
+
if (truth.language) rawAvailable.push(truth.language);
|
|
16124
|
+
if (truth.framework) rawAvailable.push(truth.framework);
|
|
15905
16125
|
const techStack = truth.techStack ?? [];
|
|
15906
|
-
|
|
16126
|
+
rawAvailable.push(...techStack);
|
|
15907
16127
|
const analysisLangs = truth.analysisLanguages ?? [];
|
|
15908
16128
|
const analysisFrameworks = truth.analysisFrameworks ?? [];
|
|
15909
|
-
|
|
15910
|
-
|
|
15911
|
-
available.push(lang);
|
|
15912
|
-
}
|
|
15913
|
-
}
|
|
15914
|
-
for (const fw of analysisFrameworks) {
|
|
15915
|
-
if (!available.some((a) => a.toLowerCase() === fw.toLowerCase())) {
|
|
15916
|
-
available.push(fw);
|
|
15917
|
-
}
|
|
15918
|
-
}
|
|
16129
|
+
rawAvailable.push(...analysisLangs, ...analysisFrameworks);
|
|
16130
|
+
const available = deduplicateTechStack(rawAvailable);
|
|
15919
16131
|
if (available.length > 0) {
|
|
15920
16132
|
parts.push(`AVAILABLE in this project: ${available.join(", ")}`);
|
|
15921
16133
|
}
|
|
@@ -15948,6 +16160,7 @@ var ProjectGroundTruthSchema, DOMAIN_LABELS;
|
|
|
15948
16160
|
var init_anti_hallucination = __esm({
|
|
15949
16161
|
"core/agentic/anti-hallucination.ts"() {
|
|
15950
16162
|
"use strict";
|
|
16163
|
+
init_tech_normalizer();
|
|
15951
16164
|
ProjectGroundTruthSchema = z16.object({
|
|
15952
16165
|
/** Project root path */
|
|
15953
16166
|
projectPath: z16.string(),
|
|
@@ -16375,6 +16588,7 @@ var init_prompt_builder = __esm({
|
|
|
16375
16588
|
init_command_context2();
|
|
16376
16589
|
init_environment_block();
|
|
16377
16590
|
init_injection_validator();
|
|
16591
|
+
init_tech_normalizer();
|
|
16378
16592
|
PromptBuilder = class {
|
|
16379
16593
|
static {
|
|
16380
16594
|
__name(this, "PromptBuilder");
|
|
@@ -16870,10 +17084,15 @@ Show changes, list affected files, ask for confirmation.
|
|
|
16870
17084
|
}
|
|
16871
17085
|
if (projectPath) {
|
|
16872
17086
|
const sa = orchestratorContext?.sealedAnalysis;
|
|
17087
|
+
const rawStack = [
|
|
17088
|
+
...sa?.frameworks || [],
|
|
17089
|
+
...orchestratorContext?.project?.conventions || []
|
|
17090
|
+
];
|
|
16873
17091
|
const groundTruth2 = {
|
|
16874
17092
|
projectPath,
|
|
16875
17093
|
language: orchestratorContext?.project?.ecosystem,
|
|
16876
|
-
|
|
17094
|
+
framework: sa?.frameworks?.[0],
|
|
17095
|
+
techStack: deduplicateTechStack(rawStack),
|
|
16877
17096
|
domains: this.extractDomains(state),
|
|
16878
17097
|
fileCount: context2.files?.length || context2.filteredSize || 0,
|
|
16879
17098
|
availableAgents: orchestratorContext?.agents?.map((a) => a.name) || [],
|
|
@@ -31842,7 +32061,7 @@ var require_package = __commonJS({
|
|
|
31842
32061
|
"package.json"(exports, module) {
|
|
31843
32062
|
module.exports = {
|
|
31844
32063
|
name: "prjct-cli",
|
|
31845
|
-
version: "1.
|
|
32064
|
+
version: "1.15.0",
|
|
31846
32065
|
description: "Context layer for AI agents. Project context for Claude Code, Gemini CLI, and more.",
|
|
31847
32066
|
main: "core/index.ts",
|
|
31848
32067
|
bin: {
|