claude-brain 0.5.0 → 0.8.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.
Files changed (46) hide show
  1. package/VERSION +1 -1
  2. package/assets/CLAUDE-unified.md +11 -0
  3. package/package.json +2 -1
  4. package/packs/backend/node.json +173 -0
  5. package/packs/core/javascript.json +176 -0
  6. package/packs/core/typescript.json +222 -0
  7. package/packs/frontend/react.json +254 -0
  8. package/packs/meta/testing.json +172 -0
  9. package/src/cli/bin.ts +14 -0
  10. package/src/cli/commands/chroma.ts +53 -17
  11. package/src/cli/commands/hooks.ts +214 -0
  12. package/src/cli/commands/pack.ts +197 -0
  13. package/src/cli/commands/serve.ts +34 -0
  14. package/src/config/defaults.ts +1 -1
  15. package/src/config/schema.ts +85 -2
  16. package/src/hooks/brain-hook.ts +110 -0
  17. package/src/hooks/capture.ts +161 -0
  18. package/src/hooks/deduplicator.ts +72 -0
  19. package/src/hooks/index.ts +19 -0
  20. package/src/hooks/installer.ts +181 -0
  21. package/src/hooks/passive-classifier.ts +366 -0
  22. package/src/hooks/queue.ts +122 -0
  23. package/src/hooks/session-tracker.ts +199 -0
  24. package/src/hooks/types.ts +47 -0
  25. package/src/memory/chroma/client.ts +1 -1
  26. package/src/memory/chroma/index.ts +1 -1
  27. package/src/memory/chroma/store.ts +29 -9
  28. package/src/memory/index.ts +1 -0
  29. package/src/memory/store.ts +1 -0
  30. package/src/packs/index.ts +9 -0
  31. package/src/packs/loader.ts +134 -0
  32. package/src/packs/manager.ts +204 -0
  33. package/src/packs/ranker.ts +78 -0
  34. package/src/packs/types.ts +81 -0
  35. package/src/routing/entity-extractor.ts +410 -0
  36. package/src/routing/intent-classifier.ts +229 -0
  37. package/src/routing/response-filter.ts +221 -0
  38. package/src/routing/router.ts +671 -0
  39. package/src/server/handlers/call-tool.ts +7 -0
  40. package/src/server/handlers/list-tools.ts +22 -5
  41. package/src/server/handlers/tools/brain.ts +85 -0
  42. package/src/server/handlers/tools/init-project.ts +47 -0
  43. package/src/server/handlers/tools/schemas.ts +12 -0
  44. package/src/server/http-api.ts +188 -0
  45. package/src/tools/registry.ts +9 -0
  46. package/src/tools/schemas.ts +33 -1
@@ -0,0 +1,47 @@
1
+ /**
2
+ * Phase 17: Passive Learning via Hooks — Shared Types
3
+ */
4
+
5
+ /** Claude Code hook stdin JSON format */
6
+ export interface HookInput {
7
+ session_id: string
8
+ hook_event_name: 'PostToolUse' | 'Stop' | 'PreToolUse'
9
+ cwd: string
10
+ tool_name?: string
11
+ tool_input?: Record<string, any>
12
+ tool_response?: {
13
+ content?: string | Array<{ type: string; text?: string }>
14
+ [key: string]: any
15
+ }
16
+ }
17
+
18
+ /** Knowledge type classifications */
19
+ export type KnowledgeType = 'decision' | 'pattern' | 'correction' | 'progress'
20
+
21
+ /** A piece of knowledge captured from a hook event */
22
+ export interface CapturedKnowledge {
23
+ type: KnowledgeType
24
+ confidence: number
25
+ content: string
26
+ project?: string
27
+ technologies: string[]
28
+ metadata: Record<string, any>
29
+ source: 'hook-passive'
30
+ timestamp: string
31
+ }
32
+
33
+ /** What to do with captured knowledge before storage */
34
+ export type StoreAction =
35
+ | { action: 'store_new' }
36
+ | { action: 'merge'; existingId: string; mergedContent: string }
37
+ | { action: 'skip'; reason: string }
38
+
39
+ /** Hook event stats for status reporting */
40
+ export interface HookStats {
41
+ totalCaptured: number
42
+ totalSkipped: number
43
+ totalMerged: number
44
+ sessionsTracked: number
45
+ lastCaptureAt?: string
46
+ queueSize: number
47
+ }
@@ -92,7 +92,7 @@ export class ChromaClientManager {
92
92
  }
93
93
 
94
94
  } catch (error) {
95
- this.logger.error({ error }, 'Failed to initialize ChromaDB client')
95
+ this.logger.warn({ error }, 'ChromaDB not available, will use SQLite fallback')
96
96
  throw error
97
97
  }
98
98
  }
@@ -54,7 +54,7 @@ export class ChromaManager {
54
54
  this.logger.info('ChromaDB initialized successfully')
55
55
 
56
56
  } catch (error) {
57
- this.logger.error({ error }, 'Failed to initialize ChromaDB')
57
+ this.logger.warn({ error }, 'ChromaDB unavailable')
58
58
  throw error
59
59
  }
60
60
  }
@@ -6,6 +6,25 @@ import type { DecisionMetadata, MemoryMetadata } from './schemas'
6
6
  import type { EmbeddingProvider } from './embeddings'
7
7
  import type { SearchResult } from './search'
8
8
 
9
+ /**
10
+ * Sanitize metadata for ChromaDB v3.x compatibility.
11
+ * Strips undefined/null values (ChromaDB only accepts string, number, boolean).
12
+ */
13
+ function sanitizeMetadata(metadata: Record<string, any>): Record<string, string | number | boolean> {
14
+ const clean: Record<string, string | number | boolean> = {}
15
+ for (const [key, value] of Object.entries(metadata)) {
16
+ if (value === undefined || value === null) continue
17
+ if (typeof value === 'string' || typeof value === 'number' || typeof value === 'boolean') {
18
+ clean[key] = value
19
+ } else if (Array.isArray(value)) {
20
+ clean[key] = JSON.stringify(value)
21
+ } else {
22
+ clean[key] = String(value)
23
+ }
24
+ }
25
+ return clean
26
+ }
27
+
9
28
  export interface StoreDecisionInput {
10
29
  project: string
11
30
  context: string
@@ -145,7 +164,7 @@ export class ChromaMemoryStore {
145
164
  await collection.add({
146
165
  ids: [id],
147
166
  documents: [input.decision],
148
- metadatas: [metadata as Record<string, any>],
167
+ metadatas: [sanitizeMetadata(metadata as Record<string, any>)],
149
168
  ...(embeddings ? { embeddings } : {})
150
169
  })
151
170
 
@@ -166,7 +185,7 @@ export class ChromaMemoryStore {
166
185
  await memoriesCollection.add({
167
186
  ids: [id], // Use same ID for cross-reference
168
187
  documents: [memoryContent],
169
- metadatas: [memoryMetadata],
188
+ metadatas: [sanitizeMetadata(memoryMetadata)],
170
189
  ...(embeddings ? { embeddings } : {})
171
190
  })
172
191
 
@@ -204,6 +223,7 @@ export class ChromaMemoryStore {
204
223
  example?: string
205
224
  confidence: number
206
225
  context?: string
226
+ source?: string
207
227
  }): Promise<string> {
208
228
  const id = randomUUID()
209
229
  const now = new Date().toISOString()
@@ -217,7 +237,7 @@ export class ChromaMemoryStore {
217
237
  context: input.context || '',
218
238
  created_at: now,
219
239
  updated_at: now,
220
- source: 'manual'
240
+ source: input.source || 'manual'
221
241
  }
222
242
 
223
243
  try {
@@ -230,7 +250,7 @@ export class ChromaMemoryStore {
230
250
  await collection.add({
231
251
  ids: [id],
232
252
  documents: [input.description],
233
- metadatas: [metadata],
253
+ metadatas: [sanitizeMetadata(metadata)],
234
254
  ...(embeddings ? { embeddings } : {})
235
255
  })
236
256
 
@@ -252,7 +272,7 @@ export class ChromaMemoryStore {
252
272
  await memoriesCollection.add({
253
273
  ids: [id],
254
274
  documents: [memoryContent],
255
- metadatas: [memoryMetadata],
275
+ metadatas: [sanitizeMetadata(memoryMetadata)],
256
276
  ...(embeddings ? { embeddings } : {})
257
277
  })
258
278
 
@@ -304,7 +324,7 @@ export class ChromaMemoryStore {
304
324
  await collection.add({
305
325
  ids: [id],
306
326
  documents: [input.correction],
307
- metadatas: [metadata],
327
+ metadatas: [sanitizeMetadata(metadata)],
308
328
  ...(embeddings ? { embeddings } : {})
309
329
  })
310
330
 
@@ -325,7 +345,7 @@ export class ChromaMemoryStore {
325
345
  await memoriesCollection.add({
326
346
  ids: [id],
327
347
  documents: [memoryContent],
328
- metadatas: [memoryMetadata],
348
+ metadatas: [sanitizeMetadata(memoryMetadata)],
329
349
  ...(embeddings ? { embeddings } : {})
330
350
  })
331
351
 
@@ -367,7 +387,7 @@ export class ChromaMemoryStore {
367
387
  await collection.add({
368
388
  ids: [id],
369
389
  documents: [input.content],
370
- metadatas: [metadata as Record<string, any>],
390
+ metadatas: [sanitizeMetadata(metadata as Record<string, any>)],
371
391
  ...(embeddings ? { embeddings } : {})
372
392
  })
373
393
 
@@ -409,7 +429,7 @@ export class ChromaMemoryStore {
409
429
  await collection.upsert({
410
430
  ids: [id],
411
431
  documents: [input.decision],
412
- metadatas: [metadata as Record<string, any>],
432
+ metadatas: [sanitizeMetadata(metadata as Record<string, any>)],
413
433
  ...(embeddings ? { embeddings } : {})
414
434
  })
415
435
 
@@ -316,6 +316,7 @@ export class MemoryManager {
316
316
  example?: string
317
317
  confidence: number
318
318
  context?: string
319
+ source?: string
319
320
  }): Promise<string> {
320
321
  if (this.useChromaDB) {
321
322
  return this.chroma.store.storePattern(input)
@@ -336,6 +336,7 @@ export class MemoryStore {
336
336
  example?: string
337
337
  confidence: number
338
338
  context?: string
339
+ source?: string
339
340
  }): Promise<string> {
340
341
  try {
341
342
  const content = `Pattern (${input.pattern_type}): ${input.description}${input.context ? `\nContext: ${input.context}` : ''}${input.example ? `\nExample: ${input.example}` : ''}`
@@ -0,0 +1,9 @@
1
+ /**
2
+ * Phase 18: Knowledge Packs
3
+ * Pre-seeded knowledge for zero cold-start experience
4
+ */
5
+
6
+ export * from './types'
7
+ export { PackManager } from './manager'
8
+ export { PackLoader } from './loader'
9
+ export { KnowledgeRanker, type RankedResult } from './ranker'
@@ -0,0 +1,134 @@
1
+ /**
2
+ * Phase 18: Pack Loader
3
+ * Orchestrates loading pack entries into the memory system
4
+ */
5
+
6
+ import type { Logger } from 'pino'
7
+ import type { MemoryManager } from '@/memory/index'
8
+ import type { PackManager } from './manager'
9
+ import type { PacksConfig } from '@/config/schema'
10
+ import { ENTRY_TYPE_TO_PATTERN_TYPE, type PackEntry, type PackLoadResult } from './types'
11
+
12
+ export class PackLoader {
13
+ private logger: Logger
14
+ private memory: MemoryManager
15
+ private packManager: PackManager
16
+ private config: PacksConfig
17
+
18
+ constructor(logger: Logger, memory: MemoryManager, packManager: PackManager, config: PacksConfig) {
19
+ this.logger = logger.child({ component: 'pack-loader' })
20
+ this.memory = memory
21
+ this.packManager = packManager
22
+ this.config = config
23
+ }
24
+
25
+ /** Main entry point: load all relevant packs for a project */
26
+ async loadPacksForProject(project: string, techStack: string[]): Promise<PackLoadResult> {
27
+ const result: PackLoadResult = {
28
+ packsLoaded: 0,
29
+ entriesLoaded: 0,
30
+ packDetails: [],
31
+ skipped: []
32
+ }
33
+
34
+ const relevantPackIds = this.packManager.findRelevantPacks(techStack)
35
+ this.logger.info({ project, techStack, relevantPackIds }, 'Loading packs for project')
36
+
37
+ const manifest = await this.packManager.getManifest(project)
38
+
39
+ for (const packId of relevantPackIds) {
40
+ try {
41
+ const pack = await this.packManager.loadPack(packId)
42
+
43
+ // Idempotency check
44
+ if (this.packManager.isPackLoaded(manifest, packId, pack.version)) {
45
+ result.skipped.push({ packId, reason: `Already loaded (v${pack.version})` })
46
+ this.logger.debug({ packId, version: pack.version }, 'Pack already loaded, skipping')
47
+ continue
48
+ }
49
+
50
+ // Load each entry
51
+ let entriesLoaded = 0
52
+ for (const entry of pack.entries) {
53
+ try {
54
+ await this.storeEntry(project, packId, entry)
55
+ entriesLoaded++
56
+ } catch (error) {
57
+ this.logger.warn({ error, packId, entry: entry.title }, 'Failed to store pack entry')
58
+ }
59
+ }
60
+
61
+ // Update manifest
62
+ manifest.packs.push({
63
+ packId,
64
+ version: pack.version,
65
+ entriesLoaded,
66
+ loadedAt: new Date().toISOString()
67
+ })
68
+
69
+ result.packsLoaded++
70
+ result.entriesLoaded += entriesLoaded
71
+ result.packDetails.push({
72
+ packId,
73
+ name: pack.name,
74
+ entriesLoaded
75
+ })
76
+
77
+ this.logger.info({ packId, entriesLoaded }, 'Pack loaded successfully')
78
+ } catch (error) {
79
+ result.skipped.push({
80
+ packId,
81
+ reason: `Load failed: ${error instanceof Error ? error.message : String(error)}`
82
+ })
83
+ this.logger.warn({ error, packId }, 'Failed to load pack')
84
+ }
85
+ }
86
+
87
+ // Save updated manifest
88
+ await this.packManager.saveManifest(manifest)
89
+
90
+ this.logger.info({
91
+ project,
92
+ packsLoaded: result.packsLoaded,
93
+ entriesLoaded: result.entriesLoaded,
94
+ skipped: result.skipped.length
95
+ }, 'Pack loading complete')
96
+
97
+ return result
98
+ }
99
+
100
+ /** Route a single pack entry to the appropriate storage method */
101
+ private async storeEntry(project: string, packId: string, entry: PackEntry): Promise<void> {
102
+ const dampenedConfidence = entry.confidence * this.config.communityConfidenceMultiplier
103
+
104
+ if (entry.type === 'decision-template') {
105
+ // Decision templates go to rememberDecision
106
+ await this.memory.rememberDecision(
107
+ project,
108
+ `[community:pack:${packId}] ${entry.category}`,
109
+ `${entry.title}: ${entry.content}`,
110
+ `Pre-seeded from knowledge pack: ${packId}`,
111
+ {
112
+ tags: ['pack', `pack:${packId}`, ...entry.tags]
113
+ }
114
+ )
115
+ } else {
116
+ // All other types go to storePattern
117
+ const patternType = ENTRY_TYPE_TO_PATTERN_TYPE[entry.type]
118
+ if (!patternType) {
119
+ this.logger.warn({ type: entry.type, packId }, 'Unknown entry type, skipping')
120
+ return
121
+ }
122
+
123
+ await this.memory.storePattern({
124
+ project,
125
+ pattern_type: patternType,
126
+ description: `${entry.title}: ${entry.content}`,
127
+ example: entry.example,
128
+ confidence: dampenedConfidence,
129
+ context: `[community:pack:${packId}] ${entry.category}`,
130
+ source: `pack:${packId}`
131
+ })
132
+ }
133
+ }
134
+ }
@@ -0,0 +1,204 @@
1
+ /**
2
+ * Phase 18: Pack Manager
3
+ * Handles discovery, loading, and manifest tracking for knowledge packs
4
+ */
5
+
6
+ import fs from 'fs/promises'
7
+ import path from 'path'
8
+ import type { Logger } from 'pino'
9
+ import type { PacksConfig } from '@/config/schema'
10
+ import { KnowledgePackSchema, type KnowledgePack, type PackManifest } from './types'
11
+
12
+ /** Maps tech stack names to relevant pack IDs */
13
+ const STACK_MAP: Record<string, string[]> = {
14
+ // Languages
15
+ typescript: ['core/typescript'],
16
+ javascript: ['core/javascript'],
17
+
18
+ // Frontend frameworks
19
+ react: ['frontend/react'],
20
+ vue: ['frontend/react'], // reuse performance patterns; will add vue pack later
21
+ angular: ['frontend/react'],
22
+ svelte: ['frontend/react'],
23
+ 'next.js': ['frontend/react'],
24
+ nuxt: ['frontend/react'],
25
+ remix: ['frontend/react'],
26
+ gatsby: ['frontend/react'],
27
+
28
+ // Backend
29
+ node: ['backend/node'],
30
+ express: ['backend/node'],
31
+ fastify: ['backend/node'],
32
+ hono: ['backend/node'],
33
+ elysia: ['backend/node'],
34
+ nestjs: ['backend/node'],
35
+ bun: ['backend/node'],
36
+
37
+ // Meta
38
+ jest: ['meta/testing'],
39
+ vitest: ['meta/testing'],
40
+ mocha: ['meta/testing'],
41
+ 'bun:test': ['meta/testing'],
42
+ testing: ['meta/testing']
43
+ }
44
+
45
+ export class PackManager {
46
+ private logger: Logger
47
+ private config: PacksConfig
48
+ private packageRoot: string
49
+ private dataDir: string
50
+
51
+ constructor(logger: Logger, config: PacksConfig, packageRoot: string, dataDir: string) {
52
+ this.logger = logger.child({ component: 'pack-manager' })
53
+ this.config = config
54
+ this.packageRoot = packageRoot
55
+ this.dataDir = dataDir
56
+ }
57
+
58
+ /** Find pack IDs relevant to a given tech stack */
59
+ findRelevantPacks(techStack: string[]): string[] {
60
+ const packIds = new Set<string>()
61
+
62
+ // Always include core and meta packs when configured
63
+ if (this.config.alwaysLoadCore) {
64
+ packIds.add('core/typescript')
65
+ packIds.add('core/javascript')
66
+ }
67
+ if (this.config.alwaysLoadMeta) {
68
+ packIds.add('meta/testing')
69
+ }
70
+
71
+ // Add tech-stack specific packs
72
+ for (const tech of techStack) {
73
+ const normalizedTech = tech.toLowerCase()
74
+ const mapped = STACK_MAP[normalizedTech]
75
+ if (mapped) {
76
+ for (const packId of mapped) {
77
+ packIds.add(packId)
78
+ }
79
+ }
80
+ }
81
+
82
+ return Array.from(packIds)
83
+ }
84
+
85
+ /** Load and validate a pack from the packs directory */
86
+ async loadPack(packId: string): Promise<KnowledgePack> {
87
+ const packPath = path.join(this.packageRoot, this.config.packsDir, `${packId}.json`)
88
+
89
+ try {
90
+ const content = await fs.readFile(packPath, 'utf-8')
91
+ const raw = JSON.parse(content)
92
+ const pack = KnowledgePackSchema.parse(raw)
93
+ this.logger.debug({ packId, entries: pack.entries.length }, 'Pack loaded and validated')
94
+ return pack
95
+ } catch (error) {
96
+ this.logger.error({ error, packId, packPath }, 'Failed to load pack')
97
+ throw new Error(`Failed to load pack "${packId}": ${error instanceof Error ? error.message : String(error)}`)
98
+ }
99
+ }
100
+
101
+ /** Get the manifest for a project (tracks which packs are loaded) */
102
+ async getManifest(project: string): Promise<PackManifest> {
103
+ const manifestPath = this.getManifestPath(project)
104
+
105
+ try {
106
+ const content = await fs.readFile(manifestPath, 'utf-8')
107
+ return JSON.parse(content) as PackManifest
108
+ } catch {
109
+ // No manifest yet — return empty
110
+ return {
111
+ project,
112
+ packs: [],
113
+ lastUpdated: new Date().toISOString()
114
+ }
115
+ }
116
+ }
117
+
118
+ /** Save manifest after loading packs */
119
+ async saveManifest(manifest: PackManifest): Promise<void> {
120
+ const manifestPath = this.getManifestPath(manifest.project)
121
+ const manifestDir = path.dirname(manifestPath)
122
+
123
+ await fs.mkdir(manifestDir, { recursive: true })
124
+ manifest.lastUpdated = new Date().toISOString()
125
+ await fs.writeFile(manifestPath, JSON.stringify(manifest, null, 2), 'utf-8')
126
+
127
+ this.logger.debug({ project: manifest.project, packs: manifest.packs.length }, 'Manifest saved')
128
+ }
129
+
130
+ /** Delete manifest for a project (used by reload) */
131
+ async deleteManifest(project: string): Promise<void> {
132
+ const manifestPath = this.getManifestPath(project)
133
+ try {
134
+ await fs.unlink(manifestPath)
135
+ } catch {
136
+ // File didn't exist, that's fine
137
+ }
138
+ }
139
+
140
+ /** Check if a pack version is already loaded */
141
+ isPackLoaded(manifest: PackManifest, packId: string, version: string): boolean {
142
+ return manifest.packs.some(p => p.packId === packId && p.version === version)
143
+ }
144
+
145
+ /** List all available packs in the packs directory */
146
+ async listAvailablePacks(): Promise<Array<{ id: string; name: string; description: string; entries: number; version: string }>> {
147
+ const packsDir = path.join(this.packageRoot, this.config.packsDir)
148
+ const packs: Array<{ id: string; name: string; description: string; entries: number; version: string }> = []
149
+
150
+ try {
151
+ await this.scanPacksDir(packsDir, '', packs)
152
+ } catch (error) {
153
+ this.logger.warn({ error, packsDir }, 'Failed to scan packs directory')
154
+ }
155
+
156
+ return packs
157
+ }
158
+
159
+ private async scanPacksDir(
160
+ dir: string,
161
+ prefix: string,
162
+ result: Array<{ id: string; name: string; description: string; entries: number; version: string }>
163
+ ): Promise<void> {
164
+ let entries: import('fs').Dirent[]
165
+ try {
166
+ entries = await fs.readdir(dir, { withFileTypes: true })
167
+ } catch {
168
+ return
169
+ }
170
+
171
+ for (const entry of entries) {
172
+ if (entry.isDirectory()) {
173
+ await this.scanPacksDir(
174
+ path.join(dir, entry.name),
175
+ prefix ? `${prefix}/${entry.name}` : entry.name,
176
+ result
177
+ )
178
+ } else if (entry.name.endsWith('.json')) {
179
+ const packId = prefix
180
+ ? `${prefix}/${entry.name.replace('.json', '')}`
181
+ : entry.name.replace('.json', '')
182
+
183
+ try {
184
+ const content = await fs.readFile(path.join(dir, entry.name), 'utf-8')
185
+ const raw = JSON.parse(content)
186
+ const pack = KnowledgePackSchema.parse(raw)
187
+ result.push({
188
+ id: packId,
189
+ name: pack.name,
190
+ description: pack.description,
191
+ entries: pack.entries.length,
192
+ version: pack.version
193
+ })
194
+ } catch (error) {
195
+ this.logger.warn({ error, packId }, 'Skipping invalid pack file')
196
+ }
197
+ }
198
+ }
199
+ }
200
+
201
+ private getManifestPath(project: string): string {
202
+ return path.join(this.dataDir, 'pack-manifests', `${project}.json`)
203
+ }
204
+ }
@@ -0,0 +1,78 @@
1
+ /**
2
+ * Phase 18: Knowledge Ranker
3
+ * Post-processing layer for search results that boosts personal entries
4
+ * over community (pack) entries
5
+ */
6
+
7
+ import type { PacksConfig } from '@/config/schema'
8
+
9
+ export interface RankedResult {
10
+ id: string
11
+ content: string
12
+ metadata: Record<string, any>
13
+ similarity: number
14
+ adjustedScore: number
15
+ badge: 'personal' | 'community'
16
+ }
17
+
18
+ export class KnowledgeRanker {
19
+ private personalBoost: number
20
+ private projectBoost: number
21
+
22
+ constructor(config: PacksConfig) {
23
+ this.personalBoost = config.personalBoost
24
+ this.projectBoost = config.projectBoost
25
+ }
26
+
27
+ /** Adjust similarity scores based on entry source */
28
+ rank(
29
+ results: Array<{ id: string; content: string; metadata: Record<string, any>; similarity: number }>,
30
+ currentProject?: string
31
+ ): RankedResult[] {
32
+ const ranked = results.map(result => {
33
+ const isCommunity = this.isCommunityEntry(result.metadata)
34
+ let adjustedScore = result.similarity
35
+
36
+ if (!isCommunity) {
37
+ // Personal entries get a boost
38
+ adjustedScore *= this.personalBoost
39
+
40
+ // Project-specific entries get an additional boost
41
+ if (currentProject && result.metadata.project === currentProject) {
42
+ adjustedScore *= this.projectBoost
43
+ }
44
+ }
45
+ // Community entries: no boost (effectively ranked lower)
46
+
47
+ // Cap at 1.0
48
+ adjustedScore = Math.min(adjustedScore, 1.0)
49
+
50
+ return {
51
+ id: result.id,
52
+ content: result.content,
53
+ metadata: result.metadata,
54
+ similarity: result.similarity,
55
+ adjustedScore,
56
+ badge: isCommunity ? 'community' as const : 'personal' as const
57
+ }
58
+ })
59
+
60
+ // Sort by adjusted score descending
61
+ return ranked.sort((a, b) => b.adjustedScore - a.adjustedScore)
62
+ }
63
+
64
+ /** Detect if a search result is from a community pack */
65
+ isCommunityEntry(metadata: Record<string, any>): boolean {
66
+ // Check source field
67
+ if (typeof metadata.source === 'string' && metadata.source.startsWith('pack:')) {
68
+ return true
69
+ }
70
+
71
+ // Check context field for community marker
72
+ if (typeof metadata.context === 'string' && metadata.context.includes('[community:pack:')) {
73
+ return true
74
+ }
75
+
76
+ return false
77
+ }
78
+ }