@ai-devkit/memory 0.16.0 → 0.17.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.
@@ -1 +1 @@
1
- {"version":3,"file":"connection.d.ts","sourceRoot":"","sources":["../../src/database/connection.ts"],"names":[],"mappings":"AAAA,OAAO,QAAQ,MAAM,gBAAgB,CAAC;AAMtC;;GAEG;AACH,eAAO,MAAM,eAAe,QAA6C,CAAC;AAE1E,MAAM,WAAW,eAAe;IAC5B,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,OAAO,CAAC,EAAE,OAAO,CAAC;IAClB,QAAQ,CAAC,EAAE,OAAO,CAAC;CACtB;AAED,qBAAa,kBAAkB;IAC3B,OAAO,CAAC,EAAE,CAAoB;IAC9B,OAAO,CAAC,QAAQ,CAAC,MAAM,CAAS;gBAEpB,OAAO,GAAE,eAAoB;IAczC,OAAO,CAAC,SAAS;IAQjB,IAAI,QAAQ,IAAI,QAAQ,CAAC,QAAQ,CAEhC;IAED,IAAI,IAAI,IAAI,MAAM,CAEjB;IAED,IAAI,MAAM,IAAI,OAAO,CAEpB;IAED,KAAK,CAAC,CAAC,EAAE,GAAG,EAAE,MAAM,EAAE,MAAM,GAAE,OAAO,EAAO,GAAG,CAAC,EAAE;IAIlD,QAAQ,CAAC,CAAC,EAAE,GAAG,EAAE,MAAM,EAAE,MAAM,GAAE,OAAO,EAAO,GAAG,CAAC,GAAG,SAAS;IAI/D,OAAO,CAAC,GAAG,EAAE,MAAM,EAAE,MAAM,GAAE,OAAO,EAAO,GAAG,QAAQ,CAAC,SAAS;IAGhE,WAAW,CAAC,CAAC,EAAE,EAAE,EAAE,MAAM,CAAC,GAAG,CAAC;IAI9B,KAAK,IAAI,IAAI;CAKhB;AAKD,wBAAgB,WAAW,CAAC,OAAO,CAAC,EAAE,eAAe,GAAG,kBAAkB,CAWzE;AAED,wBAAgB,aAAa,IAAI,IAAI,CAMpC"}
1
+ {"version":3,"file":"connection.d.ts","sourceRoot":"","sources":["../../src/database/connection.ts"],"names":[],"mappings":"AAAA,OAAO,QAAQ,MAAM,gBAAgB,CAAC;AAMtC;;GAEG;AACH,eAAO,MAAM,eAAe,QAA6C,CAAC;AAE1E,MAAM,WAAW,eAAe;IAC5B,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,OAAO,CAAC,EAAE,OAAO,CAAC;IAClB,QAAQ,CAAC,EAAE,OAAO,CAAC;CACtB;AAED,qBAAa,kBAAkB;IAC3B,OAAO,CAAC,EAAE,CAAoB;IAC9B,OAAO,CAAC,QAAQ,CAAC,MAAM,CAAS;gBAEpB,OAAO,GAAE,eAAoB;IAezC,OAAO,CAAC,SAAS;IASjB,IAAI,QAAQ,IAAI,QAAQ,CAAC,QAAQ,CAEhC;IAED,IAAI,IAAI,IAAI,MAAM,CAEjB;IAED,IAAI,MAAM,IAAI,OAAO,CAEpB;IAED,KAAK,CAAC,CAAC,EAAE,GAAG,EAAE,MAAM,EAAE,MAAM,GAAE,OAAO,EAAO,GAAG,CAAC,EAAE;IAIlD,QAAQ,CAAC,CAAC,EAAE,GAAG,EAAE,MAAM,EAAE,MAAM,GAAE,OAAO,EAAO,GAAG,CAAC,GAAG,SAAS;IAI/D,OAAO,CAAC,GAAG,EAAE,MAAM,EAAE,MAAM,GAAE,OAAO,EAAO,GAAG,QAAQ,CAAC,SAAS;IAGhE,WAAW,CAAC,CAAC,EAAE,EAAE,EAAE,MAAM,CAAC,GAAG,CAAC;IAI9B,KAAK,IAAI,IAAI;CAKhB;AAKD,wBAAgB,WAAW,CAAC,OAAO,CAAC,EAAE,eAAe,GAAG,kBAAkB,CAWzE;AAED,wBAAgB,aAAa,IAAI,IAAI,CAMpC"}
@@ -17,12 +17,16 @@ export class DatabaseConnection {
17
17
  });
18
18
  this.db = new Database(this.dbPath, {
19
19
  readonly: options.readonly ?? false,
20
+ timeout: 5000,
20
21
  verbose: options.verbose ? console.log : undefined
21
22
  });
22
23
  this.configure();
23
24
  }
24
25
  configure() {
25
- this.db.pragma('journal_mode = WAL');
26
+ const journalMode = this.db.pragma('journal_mode', {
27
+ simple: true
28
+ });
29
+ if (journalMode.toLowerCase() !== 'wal') this.db.pragma('journal_mode = WAL');
26
30
  this.db.pragma('foreign_keys = ON');
27
31
  this.db.pragma('synchronous = NORMAL');
28
32
  this.db.pragma('busy_timeout = 5000');
@@ -1 +1 @@
1
- {"version":3,"sources":["../../src/database/connection.ts"],"sourcesContent":["import Database from 'better-sqlite3';\nimport { mkdirSync } from 'fs';\nimport { dirname, join } from 'path';\nimport { homedir } from 'os';\nimport { initializeSchema } from './schema.js';\n\n/**\n * Default database path: ~/.ai-devkit/memory.db\n */\nexport const DEFAULT_DB_PATH = join(homedir(), '.ai-devkit', 'memory.db');\n\nexport interface DatabaseOptions {\n dbPath?: string;\n verbose?: boolean;\n readonly?: boolean;\n}\n\nexport class DatabaseConnection {\n private db: Database.Database;\n private readonly dbPath: string;\n\n constructor(options: DatabaseOptions = {}) {\n this.dbPath = options.dbPath ?? DEFAULT_DB_PATH;\n\n const dir = dirname(this.dbPath);\n mkdirSync(dir, { recursive: true });\n\n this.db = new Database(this.dbPath, {\n readonly: options.readonly ?? false,\n verbose: options.verbose ? console.log : undefined,\n });\n\n this.configure();\n }\n\n private configure(): void {\n this.db.pragma('journal_mode = WAL');\n this.db.pragma('foreign_keys = ON');\n this.db.pragma('synchronous = NORMAL');\n this.db.pragma('busy_timeout = 5000');\n this.db.pragma('mmap_size = 268435456');\n }\n\n get instance(): Database.Database {\n return this.db;\n }\n\n get path(): string {\n return this.dbPath;\n }\n\n get isOpen(): boolean {\n return this.db.open;\n }\n\n query<T>(sql: string, params: unknown[] = []): T[] {\n return this.db.prepare(sql).all(...params) as T[];\n }\n\n queryOne<T>(sql: string, params: unknown[] = []): T | undefined {\n return this.db.prepare(sql).get(...params) as T | undefined;\n }\n\n execute(sql: string, params: unknown[] = []): Database.RunResult {\n return this.db.prepare(sql).run(...params);\n }\n transaction<T>(fn: () => T): T {\n return this.db.transaction(fn)();\n }\n\n close(): void {\n if (this.db.open) {\n this.db.close();\n }\n }\n}\n\nlet instance: DatabaseConnection | null = null;\nlet schemaInitialized = false;\n\nexport function getDatabase(options?: DatabaseOptions): DatabaseConnection {\n if (!instance) {\n instance = new DatabaseConnection(options);\n }\n\n if (!schemaInitialized) {\n initializeSchema(instance);\n schemaInitialized = true;\n }\n\n return instance;\n}\n\nexport function closeDatabase(): void {\n if (instance) {\n instance.close();\n instance = null;\n schemaInitialized = false;\n }\n}"],"names":["Database","mkdirSync","dirname","join","homedir","initializeSchema","DEFAULT_DB_PATH","DatabaseConnection","db","dbPath","options","dir","recursive","readonly","verbose","console","log","undefined","configure","pragma","instance","path","isOpen","open","query","sql","params","prepare","all","queryOne","get","execute","run","transaction","fn","close","schemaInitialized","getDatabase","closeDatabase"],"mappings":"AAAA,OAAOA,cAAc,iBAAiB;AACtC,SAASC,SAAS,QAAQ,KAAK;AAC/B,SAASC,OAAO,EAAEC,IAAI,QAAQ,OAAO;AACrC,SAASC,OAAO,QAAQ,KAAK;AAC7B,SAASC,gBAAgB,QAAQ,cAAc;AAE/C;;CAEC,GACD,OAAO,MAAMC,kBAAkBH,KAAKC,WAAW,cAAc,aAAa;AAQ1E,OAAO,MAAMG;IACDC,GAAsB;IACbC,OAAe;IAEhC,YAAYC,UAA2B,CAAC,CAAC,CAAE;QACvC,IAAI,CAACD,MAAM,GAAGC,QAAQD,MAAM,IAAIH;QAEhC,MAAMK,MAAMT,QAAQ,IAAI,CAACO,MAAM;QAC/BR,UAAUU,KAAK;YAAEC,WAAW;QAAK;QAEjC,IAAI,CAACJ,EAAE,GAAG,IAAIR,SAAS,IAAI,CAACS,MAAM,EAAE;YAChCI,UAAUH,QAAQG,QAAQ,IAAI;YAC9BC,SAASJ,QAAQI,OAAO,GAAGC,QAAQC,GAAG,GAAGC;QAC7C;QAEA,IAAI,CAACC,SAAS;IAClB;IAEQA,YAAkB;QACtB,IAAI,CAACV,EAAE,CAACW,MAAM,CAAC;QACf,IAAI,CAACX,EAAE,CAACW,MAAM,CAAC;QACf,IAAI,CAACX,EAAE,CAACW,MAAM,CAAC;QACf,IAAI,CAACX,EAAE,CAACW,MAAM,CAAC;QACf,IAAI,CAACX,EAAE,CAACW,MAAM,CAAC;IACnB;IAEA,IAAIC,WAA8B;QAC9B,OAAO,IAAI,CAACZ,EAAE;IAClB;IAEA,IAAIa,OAAe;QACf,OAAO,IAAI,CAACZ,MAAM;IACtB;IAEA,IAAIa,SAAkB;QAClB,OAAO,IAAI,CAACd,EAAE,CAACe,IAAI;IACvB;IAEAC,MAASC,GAAW,EAAEC,SAAoB,EAAE,EAAO;QAC/C,OAAO,IAAI,CAAClB,EAAE,CAACmB,OAAO,CAACF,KAAKG,GAAG,IAAIF;IACvC;IAEAG,SAAYJ,GAAW,EAAEC,SAAoB,EAAE,EAAiB;QAC5D,OAAO,IAAI,CAAClB,EAAE,CAACmB,OAAO,CAACF,KAAKK,GAAG,IAAIJ;IACvC;IAEAK,QAAQN,GAAW,EAAEC,SAAoB,EAAE,EAAsB;QAC7D,OAAO,IAAI,CAAClB,EAAE,CAACmB,OAAO,CAACF,KAAKO,GAAG,IAAIN;IACvC;IACAO,YAAeC,EAAW,EAAK;QAC3B,OAAO,IAAI,CAAC1B,EAAE,CAACyB,WAAW,CAACC;IAC/B;IAEAC,QAAc;QACV,IAAI,IAAI,CAAC3B,EAAE,CAACe,IAAI,EAAE;YACd,IAAI,CAACf,EAAE,CAAC2B,KAAK;QACjB;IACJ;AACJ;AAEA,IAAIf,WAAsC;AAC1C,IAAIgB,oBAAoB;AAExB,OAAO,SAASC,YAAY3B,OAAyB;IACjD,IAAI,CAACU,UAAU;QACXA,WAAW,IAAIb,mBAAmBG;IACtC;IAEA,IAAI,CAAC0B,mBAAmB;QACpB/B,iBAAiBe;QACjBgB,oBAAoB;IACxB;IAEA,OAAOhB;AACX;AAEA,OAAO,SAASkB;IACZ,IAAIlB,UAAU;QACVA,SAASe,KAAK;QACdf,WAAW;QACXgB,oBAAoB;IACxB;AACJ"}
1
+ {"version":3,"sources":["../../src/database/connection.ts"],"sourcesContent":["import Database from 'better-sqlite3';\nimport { mkdirSync } from 'fs';\nimport { dirname, join } from 'path';\nimport { homedir } from 'os';\nimport { initializeSchema } from './schema.js';\n\n/**\n * Default database path: ~/.ai-devkit/memory.db\n */\nexport const DEFAULT_DB_PATH = join(homedir(), '.ai-devkit', 'memory.db');\n\nexport interface DatabaseOptions {\n dbPath?: string;\n verbose?: boolean;\n readonly?: boolean;\n}\n\nexport class DatabaseConnection {\n private db: Database.Database;\n private readonly dbPath: string;\n\n constructor(options: DatabaseOptions = {}) {\n this.dbPath = options.dbPath ?? DEFAULT_DB_PATH;\n\n const dir = dirname(this.dbPath);\n mkdirSync(dir, { recursive: true });\n\n this.db = new Database(this.dbPath, {\n readonly: options.readonly ?? false,\n timeout: 5000,\n verbose: options.verbose ? console.log : undefined,\n });\n\n this.configure();\n }\n\n private configure(): void {\n const journalMode = this.db.pragma('journal_mode', { simple: true }) as string;\n if (journalMode.toLowerCase() !== 'wal') this.db.pragma('journal_mode = WAL');\n this.db.pragma('foreign_keys = ON');\n this.db.pragma('synchronous = NORMAL');\n this.db.pragma('busy_timeout = 5000');\n this.db.pragma('mmap_size = 268435456');\n }\n\n get instance(): Database.Database {\n return this.db;\n }\n\n get path(): string {\n return this.dbPath;\n }\n\n get isOpen(): boolean {\n return this.db.open;\n }\n\n query<T>(sql: string, params: unknown[] = []): T[] {\n return this.db.prepare(sql).all(...params) as T[];\n }\n\n queryOne<T>(sql: string, params: unknown[] = []): T | undefined {\n return this.db.prepare(sql).get(...params) as T | undefined;\n }\n\n execute(sql: string, params: unknown[] = []): Database.RunResult {\n return this.db.prepare(sql).run(...params);\n }\n transaction<T>(fn: () => T): T {\n return this.db.transaction(fn)();\n }\n\n close(): void {\n if (this.db.open) {\n this.db.close();\n }\n }\n}\n\nlet instance: DatabaseConnection | null = null;\nlet schemaInitialized = false;\n\nexport function getDatabase(options?: DatabaseOptions): DatabaseConnection {\n if (!instance) {\n instance = new DatabaseConnection(options);\n }\n\n if (!schemaInitialized) {\n initializeSchema(instance);\n schemaInitialized = true;\n }\n\n return instance;\n}\n\nexport function closeDatabase(): void {\n if (instance) {\n instance.close();\n instance = null;\n schemaInitialized = false;\n }\n}\n"],"names":["Database","mkdirSync","dirname","join","homedir","initializeSchema","DEFAULT_DB_PATH","DatabaseConnection","db","dbPath","options","dir","recursive","readonly","timeout","verbose","console","log","undefined","configure","journalMode","pragma","simple","toLowerCase","instance","path","isOpen","open","query","sql","params","prepare","all","queryOne","get","execute","run","transaction","fn","close","schemaInitialized","getDatabase","closeDatabase"],"mappings":"AAAA,OAAOA,cAAc,iBAAiB;AACtC,SAASC,SAAS,QAAQ,KAAK;AAC/B,SAASC,OAAO,EAAEC,IAAI,QAAQ,OAAO;AACrC,SAASC,OAAO,QAAQ,KAAK;AAC7B,SAASC,gBAAgB,QAAQ,cAAc;AAE/C;;CAEC,GACD,OAAO,MAAMC,kBAAkBH,KAAKC,WAAW,cAAc,aAAa;AAQ1E,OAAO,MAAMG;IACDC,GAAsB;IACbC,OAAe;IAEhC,YAAYC,UAA2B,CAAC,CAAC,CAAE;QACvC,IAAI,CAACD,MAAM,GAAGC,QAAQD,MAAM,IAAIH;QAEhC,MAAMK,MAAMT,QAAQ,IAAI,CAACO,MAAM;QAC/BR,UAAUU,KAAK;YAAEC,WAAW;QAAK;QAEjC,IAAI,CAACJ,EAAE,GAAG,IAAIR,SAAS,IAAI,CAACS,MAAM,EAAE;YAChCI,UAAUH,QAAQG,QAAQ,IAAI;YAC9BC,SAAS;YACTC,SAASL,QAAQK,OAAO,GAAGC,QAAQC,GAAG,GAAGC;QAC7C;QAEA,IAAI,CAACC,SAAS;IAClB;IAEQA,YAAkB;QACtB,MAAMC,cAAc,IAAI,CAACZ,EAAE,CAACa,MAAM,CAAC,gBAAgB;YAAEC,QAAQ;QAAK;QAClE,IAAIF,YAAYG,WAAW,OAAO,OAAO,IAAI,CAACf,EAAE,CAACa,MAAM,CAAC;QACxD,IAAI,CAACb,EAAE,CAACa,MAAM,CAAC;QACf,IAAI,CAACb,EAAE,CAACa,MAAM,CAAC;QACf,IAAI,CAACb,EAAE,CAACa,MAAM,CAAC;QACf,IAAI,CAACb,EAAE,CAACa,MAAM,CAAC;IACnB;IAEA,IAAIG,WAA8B;QAC9B,OAAO,IAAI,CAAChB,EAAE;IAClB;IAEA,IAAIiB,OAAe;QACf,OAAO,IAAI,CAAChB,MAAM;IACtB;IAEA,IAAIiB,SAAkB;QAClB,OAAO,IAAI,CAAClB,EAAE,CAACmB,IAAI;IACvB;IAEAC,MAASC,GAAW,EAAEC,SAAoB,EAAE,EAAO;QAC/C,OAAO,IAAI,CAACtB,EAAE,CAACuB,OAAO,CAACF,KAAKG,GAAG,IAAIF;IACvC;IAEAG,SAAYJ,GAAW,EAAEC,SAAoB,EAAE,EAAiB;QAC5D,OAAO,IAAI,CAACtB,EAAE,CAACuB,OAAO,CAACF,KAAKK,GAAG,IAAIJ;IACvC;IAEAK,QAAQN,GAAW,EAAEC,SAAoB,EAAE,EAAsB;QAC7D,OAAO,IAAI,CAACtB,EAAE,CAACuB,OAAO,CAACF,KAAKO,GAAG,IAAIN;IACvC;IACAO,YAAeC,EAAW,EAAK;QAC3B,OAAO,IAAI,CAAC9B,EAAE,CAAC6B,WAAW,CAACC;IAC/B;IAEAC,QAAc;QACV,IAAI,IAAI,CAAC/B,EAAE,CAACmB,IAAI,EAAE;YACd,IAAI,CAACnB,EAAE,CAAC+B,KAAK;QACjB;IACJ;AACJ;AAEA,IAAIf,WAAsC;AAC1C,IAAIgB,oBAAoB;AAExB,OAAO,SAASC,YAAY/B,OAAyB;IACjD,IAAI,CAACc,UAAU;QACXA,WAAW,IAAIjB,mBAAmBG;IACtC;IAEA,IAAI,CAAC8B,mBAAmB;QACpBnC,iBAAiBmB;QACjBgB,oBAAoB;IACxB;IAEA,OAAOhB;AACX;AAEA,OAAO,SAASkB;IACZ,IAAIlB,UAAU;QACVA,SAASe,KAAK;QACdf,WAAW;QACXgB,oBAAoB;IACxB;AACJ"}
@@ -1 +1 @@
1
- {"version":3,"file":"search.d.ts","sourceRoot":"","sources":["../../src/handlers/search.ts"],"names":[],"mappings":"AAIA,OAAO,KAAK,EAAE,oBAAoB,EAAE,qBAAqB,EAAE,MAAM,mBAAmB,CAAC;AAgBrF,wBAAgB,eAAe,CAAC,KAAK,EAAE,oBAAoB,GAAG,qBAAqB,CAwClF"}
1
+ {"version":3,"file":"search.d.ts","sourceRoot":"","sources":["../../src/handlers/search.ts"],"names":[],"mappings":"AAWA,OAAO,KAAK,EAAE,oBAAoB,EAAE,qBAAqB,EAAE,MAAM,mBAAmB,CAAC;AAkBrF,wBAAgB,eAAe,CAAC,KAAK,EAAE,oBAAoB,GAAG,qBAAqB,CAmDlF"}
@@ -1,30 +1,35 @@
1
1
  import { getDatabase } from '../database/index.js';
2
- import { buildFtsQuery, buildSearchQuery, buildSimpleQuery } from '../services/search.js';
2
+ import { buildBroadFtsQuery, buildBroadSearchQuery, buildFtsQuery, buildSearchQuery, buildSimpleQuery, normalizeSearchTokens } from '../services/search.js';
3
3
  import { rankResults } from '../services/ranker.js';
4
4
  import { ValidationError } from '../utils/errors.js';
5
5
  const DEFAULT_LIMIT = 5;
6
6
  const MAX_LIMIT = 20;
7
7
  const MIN_QUERY_LENGTH = 3;
8
8
  const MAX_QUERY_LENGTH = 500;
9
+ const BROAD_CANDIDATE_MULTIPLIER = 4;
9
10
  export function searchKnowledge(input) {
10
11
  validateSearchInput(input);
11
12
  const db = getDatabase();
12
13
  const limit = Math.min(Math.max(input.limit ?? DEFAULT_LIMIT, 1), MAX_LIMIT);
13
14
  const ftsQuery = buildFtsQuery(input.query);
15
+ const tokens = normalizeSearchTokens(input.query);
14
16
  let rows;
17
+ let strategy;
15
18
  if (ftsQuery === '') {
16
- // Empty or invalid query - return recent items
19
+ // A query containing no searchable terms returns recent items.
17
20
  const { sql, params } = buildSimpleQuery(input.scope, limit);
18
21
  rows = db.query(sql, params);
22
+ strategy = 'recent';
19
23
  } else {
20
24
  // Full-text search with BM25
21
25
  const { sql, params } = buildSearchQuery(ftsQuery, input.scope, limit * 2);
22
- try {
23
- rows = db.query(sql, params);
24
- } catch (error) {
25
- // FTS query syntax error - fallback to simple query
26
- const { sql: fallbackSql, params: fallbackParams } = buildSimpleQuery(input.scope, limit);
27
- rows = db.query(fallbackSql, fallbackParams);
26
+ rows = db.query(sql, params);
27
+ strategy = 'strict';
28
+ if (rows.length === 0 && tokens.length >= 2) {
29
+ const broadQuery = buildBroadFtsQuery(input.query);
30
+ const broad = buildBroadSearchQuery(broadQuery, tokens, input.scope, limit * BROAD_CANDIDATE_MULTIPLIER);
31
+ rows = db.query(broad.sql, broad.params);
32
+ strategy = 'broad';
28
33
  }
29
34
  }
30
35
  // Apply ranking with tag and scope boosts
@@ -37,7 +42,8 @@ export function searchKnowledge(input) {
37
42
  return {
38
43
  results,
39
44
  totalMatches: ranked.length,
40
- query: input.query
45
+ query: input.query,
46
+ strategy
41
47
  };
42
48
  }
43
49
  function validateSearchInput(input) {
@@ -1 +1 @@
1
- {"version":3,"sources":["../../src/handlers/search.ts"],"sourcesContent":["import { getDatabase } from '../database/index.js';\nimport { buildFtsQuery, buildSearchQuery, buildSimpleQuery } from '../services/search.js';\nimport { rankResults } from '../services/ranker.js';\nimport { ValidationError } from '../utils/errors.js';\nimport type { SearchKnowledgeInput, SearchKnowledgeResult } from '../types/index.js';\n\nconst DEFAULT_LIMIT = 5;\nconst MAX_LIMIT = 20;\nconst MIN_QUERY_LENGTH = 3;\nconst MAX_QUERY_LENGTH = 500;\n\ninterface RawSearchRow {\n id: string;\n title: string;\n content: string;\n tags: string;\n scope: string;\n bm25_score: number;\n}\n\nexport function searchKnowledge(input: SearchKnowledgeInput): SearchKnowledgeResult {\n validateSearchInput(input);\n\n const db = getDatabase();\n const limit = Math.min(Math.max(input.limit ?? DEFAULT_LIMIT, 1), MAX_LIMIT);\n const ftsQuery = buildFtsQuery(input.query);\n\n let rows: RawSearchRow[];\n\n if (ftsQuery === '') {\n // Empty or invalid query - return recent items\n const { sql, params } = buildSimpleQuery(input.scope, limit);\n rows = db.query<RawSearchRow>(sql, params);\n } else {\n // Full-text search with BM25\n const { sql, params } = buildSearchQuery(ftsQuery, input.scope, limit * 2);\n\n try {\n rows = db.query<RawSearchRow>(sql, params);\n } catch (error) {\n // FTS query syntax error - fallback to simple query\n const { sql: fallbackSql, params: fallbackParams } = buildSimpleQuery(input.scope, limit);\n rows = db.query<RawSearchRow>(fallbackSql, fallbackParams);\n }\n }\n\n // Apply ranking with tag and scope boosts\n const ranked = rankResults(rows, {\n contextTags: input.contextTags,\n queryScope: input.scope,\n });\n\n // Limit to requested count\n const results = ranked.slice(0, limit);\n\n return {\n results,\n totalMatches: ranked.length,\n query: input.query,\n };\n}\n\nfunction validateSearchInput(input: SearchKnowledgeInput): void {\n const errors: string[] = [];\n\n if (!input.query || typeof input.query !== 'string') {\n errors.push('Query is required');\n } else {\n const trimmed = input.query.trim();\n if (trimmed.length < MIN_QUERY_LENGTH) {\n errors.push(`Query must be at least ${MIN_QUERY_LENGTH} characters`);\n }\n if (trimmed.length > MAX_QUERY_LENGTH) {\n errors.push(`Query must be at most ${MAX_QUERY_LENGTH} characters`);\n }\n }\n\n if (input.limit !== undefined) {\n if (typeof input.limit !== 'number' || input.limit < 1) {\n errors.push('Limit must be a positive number');\n }\n }\n\n if (errors.length > 0) {\n throw new ValidationError(errors.join('; '), { errors });\n }\n}\n"],"names":["getDatabase","buildFtsQuery","buildSearchQuery","buildSimpleQuery","rankResults","ValidationError","DEFAULT_LIMIT","MAX_LIMIT","MIN_QUERY_LENGTH","MAX_QUERY_LENGTH","searchKnowledge","input","validateSearchInput","db","limit","Math","min","max","ftsQuery","query","rows","sql","params","scope","error","fallbackSql","fallbackParams","ranked","contextTags","queryScope","results","slice","totalMatches","length","errors","push","trimmed","trim","undefined","join"],"mappings":"AAAA,SAASA,WAAW,QAAQ,uBAAuB;AACnD,SAASC,aAAa,EAAEC,gBAAgB,EAAEC,gBAAgB,QAAQ,wBAAwB;AAC1F,SAASC,WAAW,QAAQ,wBAAwB;AACpD,SAASC,eAAe,QAAQ,qBAAqB;AAGrD,MAAMC,gBAAgB;AACtB,MAAMC,YAAY;AAClB,MAAMC,mBAAmB;AACzB,MAAMC,mBAAmB;AAWzB,OAAO,SAASC,gBAAgBC,KAA2B;IACvDC,oBAAoBD;IAEpB,MAAME,KAAKb;IACX,MAAMc,QAAQC,KAAKC,GAAG,CAACD,KAAKE,GAAG,CAACN,MAAMG,KAAK,IAAIR,eAAe,IAAIC;IAClE,MAAMW,WAAWjB,cAAcU,MAAMQ,KAAK;IAE1C,IAAIC;IAEJ,IAAIF,aAAa,IAAI;QACjB,+CAA+C;QAC/C,MAAM,EAAEG,GAAG,EAAEC,MAAM,EAAE,GAAGnB,iBAAiBQ,MAAMY,KAAK,EAAET;QACtDM,OAAOP,GAAGM,KAAK,CAAeE,KAAKC;IACvC,OAAO;QACH,6BAA6B;QAC7B,MAAM,EAAED,GAAG,EAAEC,MAAM,EAAE,GAAGpB,iBAAiBgB,UAAUP,MAAMY,KAAK,EAAET,QAAQ;QAExE,IAAI;YACAM,OAAOP,GAAGM,KAAK,CAAeE,KAAKC;QACvC,EAAE,OAAOE,OAAO;YACZ,oDAAoD;YACpD,MAAM,EAAEH,KAAKI,WAAW,EAAEH,QAAQI,cAAc,EAAE,GAAGvB,iBAAiBQ,MAAMY,KAAK,EAAET;YACnFM,OAAOP,GAAGM,KAAK,CAAeM,aAAaC;QAC/C;IACJ;IAEA,0CAA0C;IAC1C,MAAMC,SAASvB,YAAYgB,MAAM;QAC7BQ,aAAajB,MAAMiB,WAAW;QAC9BC,YAAYlB,MAAMY,KAAK;IAC3B;IAEA,2BAA2B;IAC3B,MAAMO,UAAUH,OAAOI,KAAK,CAAC,GAAGjB;IAEhC,OAAO;QACHgB;QACAE,cAAcL,OAAOM,MAAM;QAC3Bd,OAAOR,MAAMQ,KAAK;IACtB;AACJ;AAEA,SAASP,oBAAoBD,KAA2B;IACpD,MAAMuB,SAAmB,EAAE;IAE3B,IAAI,CAACvB,MAAMQ,KAAK,IAAI,OAAOR,MAAMQ,KAAK,KAAK,UAAU;QACjDe,OAAOC,IAAI,CAAC;IAChB,OAAO;QACH,MAAMC,UAAUzB,MAAMQ,KAAK,CAACkB,IAAI;QAChC,IAAID,QAAQH,MAAM,GAAGzB,kBAAkB;YACnC0B,OAAOC,IAAI,CAAC,CAAC,uBAAuB,EAAE3B,iBAAiB,WAAW,CAAC;QACvE;QACA,IAAI4B,QAAQH,MAAM,GAAGxB,kBAAkB;YACnCyB,OAAOC,IAAI,CAAC,CAAC,sBAAsB,EAAE1B,iBAAiB,WAAW,CAAC;QACtE;IACJ;IAEA,IAAIE,MAAMG,KAAK,KAAKwB,WAAW;QAC3B,IAAI,OAAO3B,MAAMG,KAAK,KAAK,YAAYH,MAAMG,KAAK,GAAG,GAAG;YACpDoB,OAAOC,IAAI,CAAC;QAChB;IACJ;IAEA,IAAID,OAAOD,MAAM,GAAG,GAAG;QACnB,MAAM,IAAI5B,gBAAgB6B,OAAOK,IAAI,CAAC,OAAO;YAAEL;QAAO;IAC1D;AACJ"}
1
+ {"version":3,"sources":["../../src/handlers/search.ts"],"sourcesContent":["import { getDatabase } from '../database/index.js';\nimport {\n buildBroadFtsQuery,\n buildBroadSearchQuery,\n buildFtsQuery,\n buildSearchQuery,\n buildSimpleQuery,\n normalizeSearchTokens,\n} from '../services/search.js';\nimport { rankResults } from '../services/ranker.js';\nimport { ValidationError } from '../utils/errors.js';\nimport type { SearchKnowledgeInput, SearchKnowledgeResult } from '../types/index.js';\n\nconst DEFAULT_LIMIT = 5;\nconst MAX_LIMIT = 20;\nconst MIN_QUERY_LENGTH = 3;\nconst MAX_QUERY_LENGTH = 500;\nconst BROAD_CANDIDATE_MULTIPLIER = 4;\n\ninterface RawSearchRow {\n id: string;\n title: string;\n content: string;\n tags: string;\n scope: string;\n bm25_score: number;\n token_coverage?: number;\n}\n\nexport function searchKnowledge(input: SearchKnowledgeInput): SearchKnowledgeResult {\n validateSearchInput(input);\n\n const db = getDatabase();\n const limit = Math.min(Math.max(input.limit ?? DEFAULT_LIMIT, 1), MAX_LIMIT);\n const ftsQuery = buildFtsQuery(input.query);\n const tokens = normalizeSearchTokens(input.query);\n\n let rows: RawSearchRow[];\n let strategy: SearchKnowledgeResult['strategy'];\n\n if (ftsQuery === '') {\n // A query containing no searchable terms returns recent items.\n const { sql, params } = buildSimpleQuery(input.scope, limit);\n rows = db.query<RawSearchRow>(sql, params);\n strategy = 'recent';\n } else {\n // Full-text search with BM25\n const { sql, params } = buildSearchQuery(ftsQuery, input.scope, limit * 2);\n\n rows = db.query<RawSearchRow>(sql, params);\n strategy = 'strict';\n\n if (rows.length === 0 && tokens.length >= 2) {\n const broadQuery = buildBroadFtsQuery(input.query);\n const broad = buildBroadSearchQuery(\n broadQuery,\n tokens,\n input.scope,\n limit * BROAD_CANDIDATE_MULTIPLIER,\n );\n rows = db.query<RawSearchRow>(broad.sql, broad.params);\n strategy = 'broad';\n }\n }\n\n // Apply ranking with tag and scope boosts\n const ranked = rankResults(rows, {\n contextTags: input.contextTags,\n queryScope: input.scope,\n });\n\n // Limit to requested count\n const results = ranked.slice(0, limit);\n\n return {\n results,\n totalMatches: ranked.length,\n query: input.query,\n strategy,\n };\n}\n\nfunction validateSearchInput(input: SearchKnowledgeInput): void {\n const errors: string[] = [];\n\n if (!input.query || typeof input.query !== 'string') {\n errors.push('Query is required');\n } else {\n const trimmed = input.query.trim();\n if (trimmed.length < MIN_QUERY_LENGTH) {\n errors.push(`Query must be at least ${MIN_QUERY_LENGTH} characters`);\n }\n if (trimmed.length > MAX_QUERY_LENGTH) {\n errors.push(`Query must be at most ${MAX_QUERY_LENGTH} characters`);\n }\n }\n\n if (input.limit !== undefined) {\n if (typeof input.limit !== 'number' || input.limit < 1) {\n errors.push('Limit must be a positive number');\n }\n }\n\n if (errors.length > 0) {\n throw new ValidationError(errors.join('; '), { errors });\n }\n}\n"],"names":["getDatabase","buildBroadFtsQuery","buildBroadSearchQuery","buildFtsQuery","buildSearchQuery","buildSimpleQuery","normalizeSearchTokens","rankResults","ValidationError","DEFAULT_LIMIT","MAX_LIMIT","MIN_QUERY_LENGTH","MAX_QUERY_LENGTH","BROAD_CANDIDATE_MULTIPLIER","searchKnowledge","input","validateSearchInput","db","limit","Math","min","max","ftsQuery","query","tokens","rows","strategy","sql","params","scope","length","broadQuery","broad","ranked","contextTags","queryScope","results","slice","totalMatches","errors","push","trimmed","trim","undefined","join"],"mappings":"AAAA,SAASA,WAAW,QAAQ,uBAAuB;AACnD,SACIC,kBAAkB,EAClBC,qBAAqB,EACrBC,aAAa,EACbC,gBAAgB,EAChBC,gBAAgB,EAChBC,qBAAqB,QAClB,wBAAwB;AAC/B,SAASC,WAAW,QAAQ,wBAAwB;AACpD,SAASC,eAAe,QAAQ,qBAAqB;AAGrD,MAAMC,gBAAgB;AACtB,MAAMC,YAAY;AAClB,MAAMC,mBAAmB;AACzB,MAAMC,mBAAmB;AACzB,MAAMC,6BAA6B;AAYnC,OAAO,SAASC,gBAAgBC,KAA2B;IACvDC,oBAAoBD;IAEpB,MAAME,KAAKjB;IACX,MAAMkB,QAAQC,KAAKC,GAAG,CAACD,KAAKE,GAAG,CAACN,MAAMG,KAAK,IAAIT,eAAe,IAAIC;IAClE,MAAMY,WAAWnB,cAAcY,MAAMQ,KAAK;IAC1C,MAAMC,SAASlB,sBAAsBS,MAAMQ,KAAK;IAEhD,IAAIE;IACJ,IAAIC;IAEJ,IAAIJ,aAAa,IAAI;QACjB,+DAA+D;QAC/D,MAAM,EAAEK,GAAG,EAAEC,MAAM,EAAE,GAAGvB,iBAAiBU,MAAMc,KAAK,EAAEX;QACtDO,OAAOR,GAAGM,KAAK,CAAeI,KAAKC;QACnCF,WAAW;IACf,OAAO;QACH,6BAA6B;QAC7B,MAAM,EAAEC,GAAG,EAAEC,MAAM,EAAE,GAAGxB,iBAAiBkB,UAAUP,MAAMc,KAAK,EAAEX,QAAQ;QAExEO,OAAOR,GAAGM,KAAK,CAAeI,KAAKC;QACnCF,WAAW;QAEX,IAAID,KAAKK,MAAM,KAAK,KAAKN,OAAOM,MAAM,IAAI,GAAG;YACzC,MAAMC,aAAa9B,mBAAmBc,MAAMQ,KAAK;YACjD,MAAMS,QAAQ9B,sBACV6B,YACAP,QACAT,MAAMc,KAAK,EACXX,QAAQL;YAEZY,OAAOR,GAAGM,KAAK,CAAeS,MAAML,GAAG,EAAEK,MAAMJ,MAAM;YACrDF,WAAW;QACf;IACJ;IAEA,0CAA0C;IAC1C,MAAMO,SAAS1B,YAAYkB,MAAM;QAC7BS,aAAanB,MAAMmB,WAAW;QAC9BC,YAAYpB,MAAMc,KAAK;IAC3B;IAEA,2BAA2B;IAC3B,MAAMO,UAAUH,OAAOI,KAAK,CAAC,GAAGnB;IAEhC,OAAO;QACHkB;QACAE,cAAcL,OAAOH,MAAM;QAC3BP,OAAOR,MAAMQ,KAAK;QAClBG;IACJ;AACJ;AAEA,SAASV,oBAAoBD,KAA2B;IACpD,MAAMwB,SAAmB,EAAE;IAE3B,IAAI,CAACxB,MAAMQ,KAAK,IAAI,OAAOR,MAAMQ,KAAK,KAAK,UAAU;QACjDgB,OAAOC,IAAI,CAAC;IAChB,OAAO;QACH,MAAMC,UAAU1B,MAAMQ,KAAK,CAACmB,IAAI;QAChC,IAAID,QAAQX,MAAM,GAAGnB,kBAAkB;YACnC4B,OAAOC,IAAI,CAAC,CAAC,uBAAuB,EAAE7B,iBAAiB,WAAW,CAAC;QACvE;QACA,IAAI8B,QAAQX,MAAM,GAAGlB,kBAAkB;YACnC2B,OAAOC,IAAI,CAAC,CAAC,sBAAsB,EAAE5B,iBAAiB,WAAW,CAAC;QACtE;IACJ;IAEA,IAAIG,MAAMG,KAAK,KAAKyB,WAAW;QAC3B,IAAI,OAAO5B,MAAMG,KAAK,KAAK,YAAYH,MAAMG,KAAK,GAAG,GAAG;YACpDqB,OAAOC,IAAI,CAAC;QAChB;IACJ;IAEA,IAAID,OAAOT,MAAM,GAAG,GAAG;QACnB,MAAM,IAAItB,gBAAgB+B,OAAOK,IAAI,CAAC,OAAO;YAAEL;QAAO;IAC1D;AACJ"}
@@ -6,6 +6,7 @@ interface RawSearchResult {
6
6
  tags: string;
7
7
  scope: string;
8
8
  bm25_score: number;
9
+ token_coverage?: number;
9
10
  }
10
11
  interface RankingContext {
11
12
  contextTags?: string[];
@@ -14,7 +15,7 @@ interface RankingContext {
14
15
  /**
15
16
  * Apply ranking formula to search results
16
17
  *
17
- * Formula: final_score = bm25_score × tag_boost + scope_boost
18
+ * Formula: final_score = bm25_score × token_coverage × tag_boost + scope_boost
18
19
  */
19
20
  export declare function rankResults(results: RawSearchResult[], context: RankingContext): SearchResultItem[];
20
21
  export {};
@@ -1 +1 @@
1
- {"version":3,"file":"ranker.d.ts","sourceRoot":"","sources":["../../src/services/ranker.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,mBAAmB,CAAC;AAE1D,UAAU,eAAe;IACrB,EAAE,EAAE,MAAM,CAAC;IACX,KAAK,EAAE,MAAM,CAAC;IACd,OAAO,EAAE,MAAM,CAAC;IAChB,IAAI,EAAE,MAAM,CAAC;IACb,KAAK,EAAE,MAAM,CAAC;IACd,UAAU,EAAE,MAAM,CAAC;CACtB;AAED,UAAU,cAAc;IACpB,WAAW,CAAC,EAAE,MAAM,EAAE,CAAC;IACvB,UAAU,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;CAC9B;AAwCD;;;;GAIG;AACH,wBAAgB,WAAW,CACvB,OAAO,EAAE,eAAe,EAAE,EAC1B,OAAO,EAAE,cAAc,GACxB,gBAAgB,EAAE,CAgCpB"}
1
+ {"version":3,"file":"ranker.d.ts","sourceRoot":"","sources":["../../src/services/ranker.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,mBAAmB,CAAC;AAE1D,UAAU,eAAe;IACrB,EAAE,EAAE,MAAM,CAAC;IACX,KAAK,EAAE,MAAM,CAAC;IACd,OAAO,EAAE,MAAM,CAAC;IAChB,IAAI,EAAE,MAAM,CAAC;IACb,KAAK,EAAE,MAAM,CAAC;IACd,UAAU,EAAE,MAAM,CAAC;IACnB,cAAc,CAAC,EAAE,MAAM,CAAC;CAC3B;AAED,UAAU,cAAc;IACpB,WAAW,CAAC,EAAE,MAAM,EAAE,CAAC;IACvB,UAAU,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;CAC9B;AAwCD;;;;GAIG;AACH,wBAAgB,WAAW,CACvB,OAAO,EAAE,eAAe,EAAE,EAC1B,OAAO,EAAE,cAAc,GACxB,gBAAgB,EAAE,CAgCpB"}
@@ -32,7 +32,7 @@
32
32
  /**
33
33
  * Apply ranking formula to search results
34
34
  *
35
- * Formula: final_score = bm25_score × tag_boost + scope_boost
35
+ * Formula: final_score = bm25_score × token_coverage × tag_boost + scope_boost
36
36
  */ export function rankResults(results, context) {
37
37
  const ranked = results.map((result)=>{
38
38
  let tags;
@@ -46,7 +46,7 @@
46
46
  // BM25 returns negative values (closer to 0 = better match)
47
47
  // We negate it to make higher values better
48
48
  const normalizedBm25 = -result.bm25_score;
49
- const finalScore = normalizedBm25 * tagBoost + scopeBoost;
49
+ const finalScore = normalizedBm25 * (result.token_coverage ?? 1) * tagBoost + scopeBoost;
50
50
  return {
51
51
  id: result.id,
52
52
  title: result.title,
@@ -1 +1 @@
1
- {"version":3,"sources":["../../src/services/ranker.ts"],"sourcesContent":["import type { SearchResultItem } from '../types/index.js';\n\ninterface RawSearchResult {\n id: string;\n title: string;\n content: string;\n tags: string;\n scope: string;\n bm25_score: number;\n}\n\ninterface RankingContext {\n contextTags?: string[];\n queryScope?: string | null;\n}\n\n/**\n * Calculate tag boost based on matching contextTags\n * +10% per matching tag\n */\nfunction calculateTagBoost(itemTags: string[], contextTags: string[]): number {\n if (!contextTags || contextTags.length === 0) {\n return 1.0;\n }\n\n const itemTagsLower = itemTags.map(t => t.toLowerCase());\n const contextTagsLower = contextTags.map(t => t.toLowerCase());\n\n let matchCount = 0;\n for (const tag of contextTagsLower) {\n if (itemTagsLower.includes(tag)) {\n matchCount++;\n }\n }\n\n return 1 + (matchCount * 0.1);\n}\n\n/**\n * Calculate scope boost\n * +0.5 if scope matches query scope\n * +0.2 if global\n * 0 otherwise\n */\nfunction calculateScopeBoost(itemScope: string, queryScope?: string | null): number {\n if (queryScope && itemScope === queryScope) {\n return 0.5;\n }\n if (itemScope === 'global') {\n return 0.2;\n }\n return 0;\n}\n\n/**\n * Apply ranking formula to search results\n * \n * Formula: final_score = bm25_score × tag_boost + scope_boost\n */\nexport function rankResults(\n results: RawSearchResult[],\n context: RankingContext\n): SearchResultItem[] {\n const ranked = results.map(result => {\n let tags: string[];\n try {\n tags = JSON.parse(result.tags) as string[];\n } catch {\n tags = [];\n }\n\n const tagBoost = calculateTagBoost(tags, context.contextTags ?? []);\n const scopeBoost = calculateScopeBoost(result.scope, context.queryScope);\n\n // BM25 returns negative values (closer to 0 = better match)\n // We negate it to make higher values better\n const normalizedBm25 = -result.bm25_score;\n\n const finalScore = (normalizedBm25 * tagBoost) + scopeBoost;\n\n return {\n id: result.id,\n title: result.title,\n content: result.content,\n tags,\n scope: result.scope,\n score: Math.round(finalScore * 1000) / 1000,\n };\n });\n\n // Sort by score descending\n ranked.sort((a, b) => b.score - a.score);\n\n return ranked;\n}\n"],"names":["calculateTagBoost","itemTags","contextTags","length","itemTagsLower","map","t","toLowerCase","contextTagsLower","matchCount","tag","includes","calculateScopeBoost","itemScope","queryScope","rankResults","results","context","ranked","result","tags","JSON","parse","tagBoost","scopeBoost","scope","normalizedBm25","bm25_score","finalScore","id","title","content","score","Math","round","sort","a","b"],"mappings":"AAgBA;;;CAGC,GACD,SAASA,kBAAkBC,QAAkB,EAAEC,WAAqB;IAChE,IAAI,CAACA,eAAeA,YAAYC,MAAM,KAAK,GAAG;QAC1C,OAAO;IACX;IAEA,MAAMC,gBAAgBH,SAASI,GAAG,CAACC,CAAAA,IAAKA,EAAEC,WAAW;IACrD,MAAMC,mBAAmBN,YAAYG,GAAG,CAACC,CAAAA,IAAKA,EAAEC,WAAW;IAE3D,IAAIE,aAAa;IACjB,KAAK,MAAMC,OAAOF,iBAAkB;QAChC,IAAIJ,cAAcO,QAAQ,CAACD,MAAM;YAC7BD;QACJ;IACJ;IAEA,OAAO,IAAKA,aAAa;AAC7B;AAEA;;;;;CAKC,GACD,SAASG,oBAAoBC,SAAiB,EAAEC,UAA0B;IACtE,IAAIA,cAAcD,cAAcC,YAAY;QACxC,OAAO;IACX;IACA,IAAID,cAAc,UAAU;QACxB,OAAO;IACX;IACA,OAAO;AACX;AAEA;;;;CAIC,GACD,OAAO,SAASE,YACZC,OAA0B,EAC1BC,OAAuB;IAEvB,MAAMC,SAASF,QAAQX,GAAG,CAACc,CAAAA;QACvB,IAAIC;QACJ,IAAI;YACAA,OAAOC,KAAKC,KAAK,CAACH,OAAOC,IAAI;QACjC,EAAE,OAAM;YACJA,OAAO,EAAE;QACb;QAEA,MAAMG,WAAWvB,kBAAkBoB,MAAMH,QAAQf,WAAW,IAAI,EAAE;QAClE,MAAMsB,aAAaZ,oBAAoBO,OAAOM,KAAK,EAAER,QAAQH,UAAU;QAEvE,4DAA4D;QAC5D,4CAA4C;QAC5C,MAAMY,iBAAiB,CAACP,OAAOQ,UAAU;QAEzC,MAAMC,aAAa,AAACF,iBAAiBH,WAAYC;QAEjD,OAAO;YACHK,IAAIV,OAAOU,EAAE;YACbC,OAAOX,OAAOW,KAAK;YACnBC,SAASZ,OAAOY,OAAO;YACvBX;YACAK,OAAON,OAAOM,KAAK;YACnBO,OAAOC,KAAKC,KAAK,CAACN,aAAa,QAAQ;QAC3C;IACJ;IAEA,2BAA2B;IAC3BV,OAAOiB,IAAI,CAAC,CAACC,GAAGC,IAAMA,EAAEL,KAAK,GAAGI,EAAEJ,KAAK;IAEvC,OAAOd;AACX"}
1
+ {"version":3,"sources":["../../src/services/ranker.ts"],"sourcesContent":["import type { SearchResultItem } from '../types/index.js';\n\ninterface RawSearchResult {\n id: string;\n title: string;\n content: string;\n tags: string;\n scope: string;\n bm25_score: number;\n token_coverage?: number;\n}\n\ninterface RankingContext {\n contextTags?: string[];\n queryScope?: string | null;\n}\n\n/**\n * Calculate tag boost based on matching contextTags\n * +10% per matching tag\n */\nfunction calculateTagBoost(itemTags: string[], contextTags: string[]): number {\n if (!contextTags || contextTags.length === 0) {\n return 1.0;\n }\n\n const itemTagsLower = itemTags.map(t => t.toLowerCase());\n const contextTagsLower = contextTags.map(t => t.toLowerCase());\n\n let matchCount = 0;\n for (const tag of contextTagsLower) {\n if (itemTagsLower.includes(tag)) {\n matchCount++;\n }\n }\n\n return 1 + (matchCount * 0.1);\n}\n\n/**\n * Calculate scope boost\n * +0.5 if scope matches query scope\n * +0.2 if global\n * 0 otherwise\n */\nfunction calculateScopeBoost(itemScope: string, queryScope?: string | null): number {\n if (queryScope && itemScope === queryScope) {\n return 0.5;\n }\n if (itemScope === 'global') {\n return 0.2;\n }\n return 0;\n}\n\n/**\n * Apply ranking formula to search results\n * \n * Formula: final_score = bm25_score × token_coverage × tag_boost + scope_boost\n */\nexport function rankResults(\n results: RawSearchResult[],\n context: RankingContext\n): SearchResultItem[] {\n const ranked = results.map(result => {\n let tags: string[];\n try {\n tags = JSON.parse(result.tags) as string[];\n } catch {\n tags = [];\n }\n\n const tagBoost = calculateTagBoost(tags, context.contextTags ?? []);\n const scopeBoost = calculateScopeBoost(result.scope, context.queryScope);\n\n // BM25 returns negative values (closer to 0 = better match)\n // We negate it to make higher values better\n const normalizedBm25 = -result.bm25_score;\n\n const finalScore = (normalizedBm25 * (result.token_coverage ?? 1) * tagBoost) + scopeBoost;\n\n return {\n id: result.id,\n title: result.title,\n content: result.content,\n tags,\n scope: result.scope,\n score: Math.round(finalScore * 1000) / 1000,\n };\n });\n\n // Sort by score descending\n ranked.sort((a, b) => b.score - a.score);\n\n return ranked;\n}\n"],"names":["calculateTagBoost","itemTags","contextTags","length","itemTagsLower","map","t","toLowerCase","contextTagsLower","matchCount","tag","includes","calculateScopeBoost","itemScope","queryScope","rankResults","results","context","ranked","result","tags","JSON","parse","tagBoost","scopeBoost","scope","normalizedBm25","bm25_score","finalScore","token_coverage","id","title","content","score","Math","round","sort","a","b"],"mappings":"AAiBA;;;CAGC,GACD,SAASA,kBAAkBC,QAAkB,EAAEC,WAAqB;IAChE,IAAI,CAACA,eAAeA,YAAYC,MAAM,KAAK,GAAG;QAC1C,OAAO;IACX;IAEA,MAAMC,gBAAgBH,SAASI,GAAG,CAACC,CAAAA,IAAKA,EAAEC,WAAW;IACrD,MAAMC,mBAAmBN,YAAYG,GAAG,CAACC,CAAAA,IAAKA,EAAEC,WAAW;IAE3D,IAAIE,aAAa;IACjB,KAAK,MAAMC,OAAOF,iBAAkB;QAChC,IAAIJ,cAAcO,QAAQ,CAACD,MAAM;YAC7BD;QACJ;IACJ;IAEA,OAAO,IAAKA,aAAa;AAC7B;AAEA;;;;;CAKC,GACD,SAASG,oBAAoBC,SAAiB,EAAEC,UAA0B;IACtE,IAAIA,cAAcD,cAAcC,YAAY;QACxC,OAAO;IACX;IACA,IAAID,cAAc,UAAU;QACxB,OAAO;IACX;IACA,OAAO;AACX;AAEA;;;;CAIC,GACD,OAAO,SAASE,YACZC,OAA0B,EAC1BC,OAAuB;IAEvB,MAAMC,SAASF,QAAQX,GAAG,CAACc,CAAAA;QACvB,IAAIC;QACJ,IAAI;YACAA,OAAOC,KAAKC,KAAK,CAACH,OAAOC,IAAI;QACjC,EAAE,OAAM;YACJA,OAAO,EAAE;QACb;QAEA,MAAMG,WAAWvB,kBAAkBoB,MAAMH,QAAQf,WAAW,IAAI,EAAE;QAClE,MAAMsB,aAAaZ,oBAAoBO,OAAOM,KAAK,EAAER,QAAQH,UAAU;QAEvE,4DAA4D;QAC5D,4CAA4C;QAC5C,MAAMY,iBAAiB,CAACP,OAAOQ,UAAU;QAEzC,MAAMC,aAAa,AAACF,iBAAkBP,CAAAA,OAAOU,cAAc,IAAI,CAAA,IAAKN,WAAYC;QAEhF,OAAO;YACHM,IAAIX,OAAOW,EAAE;YACbC,OAAOZ,OAAOY,KAAK;YACnBC,SAASb,OAAOa,OAAO;YACvBZ;YACAK,OAAON,OAAOM,KAAK;YACnBQ,OAAOC,KAAKC,KAAK,CAACP,aAAa,QAAQ;QAC3C;IACJ;IAEA,2BAA2B;IAC3BV,OAAOkB,IAAI,CAAC,CAACC,GAAGC,IAAMA,EAAEL,KAAK,GAAGI,EAAEJ,KAAK;IAEvC,OAAOf;AACX"}
@@ -2,6 +2,7 @@
2
2
  * FTS5 Query Builder
3
3
  * Converts natural language queries to FTS5 match expressions
4
4
  */
5
+ export declare function normalizeSearchTokens(query: string): string[];
5
6
  /**
6
7
  * Build FTS5 query from natural language input
7
8
  *
@@ -11,6 +12,7 @@
11
12
  * - Escape special characters
12
13
  */
13
14
  export declare function buildFtsQuery(query: string): string;
15
+ export declare function buildBroadFtsQuery(query: string): string;
14
16
  /**
15
17
  * Build FTS5 query with column boosting
16
18
  * Uses bm25() with weights: title=10, content=5, tags=1
@@ -19,6 +21,10 @@ export declare function buildSearchQuery(ftsQuery: string, scope?: string | null
19
21
  sql: string;
20
22
  params: unknown[];
21
23
  };
24
+ export declare function buildBroadSearchQuery(ftsQuery: string, tokens: string[], scope?: string | null, limit?: number): {
25
+ sql: string;
26
+ params: unknown[];
27
+ };
22
28
  /**
23
29
  * Build simple search query without FTS (fallback for empty queries)
24
30
  */
@@ -1 +1 @@
1
- {"version":3,"file":"search.d.ts","sourceRoot":"","sources":["../../src/services/search.ts"],"names":[],"mappings":"AAAA;;;GAGG;AAeH;;;;;;;GAOG;AACH,wBAAgB,aAAa,CAAC,KAAK,EAAE,MAAM,GAAG,MAAM,CAWnD;AAED;;;GAGG;AACH,wBAAgB,gBAAgB,CAC5B,QAAQ,EAAE,MAAM,EAChB,KAAK,CAAC,EAAE,MAAM,GAAG,IAAI,EACrB,KAAK,SAAI,GACV;IAAE,GAAG,EAAE,MAAM,CAAC;IAAC,MAAM,EAAE,OAAO,EAAE,CAAA;CAAE,CA4BpC;AAED;;GAEG;AACH,wBAAgB,gBAAgB,CAC5B,KAAK,CAAC,EAAE,MAAM,GAAG,IAAI,EACrB,KAAK,SAAI,GACV;IAAE,GAAG,EAAE,MAAM,CAAC;IAAC,MAAM,EAAE,OAAO,EAAE,CAAA;CAAE,CAmBpC"}
1
+ {"version":3,"file":"search.d.ts","sourceRoot":"","sources":["../../src/services/search.ts"],"names":[],"mappings":"AAAA;;;GAGG;AASH,wBAAgB,qBAAqB,CAAC,KAAK,EAAE,MAAM,GAAG,MAAM,EAAE,CAY7D;AAMD;;;;;;;GAOG;AACH,wBAAgB,aAAa,CAAC,KAAK,EAAE,MAAM,GAAG,MAAM,CAEnD;AAED,wBAAgB,kBAAkB,CAAC,KAAK,EAAE,MAAM,GAAG,MAAM,CAExD;AAED;;;GAGG;AACH,wBAAgB,gBAAgB,CAC5B,QAAQ,EAAE,MAAM,EAChB,KAAK,CAAC,EAAE,MAAM,GAAG,IAAI,EACrB,KAAK,SAAI,GACV;IAAE,GAAG,EAAE,MAAM,CAAC;IAAC,MAAM,EAAE,OAAO,EAAE,CAAA;CAAE,CA6BpC;AAED,wBAAgB,qBAAqB,CACjC,QAAQ,EAAE,MAAM,EAChB,MAAM,EAAE,MAAM,EAAE,EAChB,KAAK,CAAC,EAAE,MAAM,GAAG,IAAI,EACrB,KAAK,SAAI,GACV;IAAE,GAAG,EAAE,MAAM,CAAC;IAAC,MAAM,EAAE,OAAO,EAAE,CAAA;CAAE,CAwBpC;AAED;;GAEG;AACH,wBAAgB,gBAAgB,CAC5B,KAAK,CAAC,EAAE,MAAM,GAAG,IAAI,EACrB,KAAK,SAAI,GACV;IAAE,GAAG,EAAE,MAAM,CAAC;IAAC,MAAM,EAAE,OAAO,EAAE,CAAA;CAAE,CAoBpC"}
@@ -1,14 +1,67 @@
1
1
  /**
2
2
  * FTS5 Query Builder
3
3
  * Converts natural language queries to FTS5 match expressions
4
- */ /**
5
- * Escape special FTS5 characters to prevent query syntax errors
6
- */ function escapeFtsSpecialChars(text) {
7
- // FTS5 special characters: " * ^ - : OR AND NOT ( )
8
- return text.replace(/"/g, '""') // Escape quotes by doubling
9
- .replace(/[*^():-]/g, ' ') // Replace operators with space (including hyphen)
10
- .replace(/\b(AND|OR|NOT)\b/gi, '') // Remove boolean operators
11
- .trim().replace(/\s+/g, ' '); // Collapse multiple spaces
4
+ */ const STOPWORDS = new Set([
5
+ 'a',
6
+ 'an',
7
+ 'and',
8
+ 'are',
9
+ 'as',
10
+ 'at',
11
+ 'be',
12
+ 'been',
13
+ 'but',
14
+ 'by',
15
+ 'do',
16
+ 'does',
17
+ 'for',
18
+ 'from',
19
+ 'had',
20
+ 'has',
21
+ 'have',
22
+ 'how',
23
+ 'i',
24
+ 'if',
25
+ 'in',
26
+ 'is',
27
+ 'it',
28
+ 'of',
29
+ 'on',
30
+ 'or',
31
+ 'should',
32
+ 'that',
33
+ 'the',
34
+ 'their',
35
+ 'this',
36
+ 'to',
37
+ 'was',
38
+ 'what',
39
+ 'when',
40
+ 'where',
41
+ 'which',
42
+ 'who',
43
+ 'why',
44
+ 'will',
45
+ 'with',
46
+ 'you',
47
+ 'your'
48
+ ]);
49
+ export function normalizeSearchTokens(query) {
50
+ const matches = query.normalize('NFKC').match(/[\p{L}\p{N}]+/gu) ?? [];
51
+ const seen = new Set();
52
+ return matches.flatMap((token)=>{
53
+ const normalized = token.toLowerCase();
54
+ if (normalized.length === 1 || STOPWORDS.has(normalized) || seen.has(normalized)) {
55
+ return [];
56
+ }
57
+ seen.add(normalized);
58
+ return [
59
+ normalized
60
+ ];
61
+ });
62
+ }
63
+ function buildPrefixTerms(query) {
64
+ return normalizeSearchTokens(query).map((token)=>`"${token}"*`);
12
65
  }
13
66
  /**
14
67
  * Build FTS5 query from natural language input
@@ -18,14 +71,10 @@
18
71
  * - Use prefix matching (*) for partial matches
19
72
  * - Escape special characters
20
73
  */ export function buildFtsQuery(query) {
21
- const escaped = escapeFtsSpecialChars(query);
22
- const words = escaped.split(/\s+/).filter((w)=>w.length > 0);
23
- if (words.length === 0) {
24
- return '';
25
- }
26
- // Use prefix matching for each word
27
- // This allows "api design" to match "API", "designing", etc.
28
- return words.map((word)=>`${word}*`).join(' ');
74
+ return buildPrefixTerms(query).join(' ');
75
+ }
76
+ export function buildBroadFtsQuery(query) {
77
+ return buildPrefixTerms(query).join(' OR ');
29
78
  }
30
79
  /**
31
80
  * Build FTS5 query with column boosting
@@ -41,7 +90,8 @@
41
90
  k.scope,
42
91
  k.created_at,
43
92
  k.updated_at,
44
- bm25(knowledge_fts, 10.0, 5.0, 1.0) as bm25_score
93
+ bm25(knowledge_fts, 10.0, 5.0, 1.0) as bm25_score,
94
+ 1.0 as token_coverage
45
95
  FROM knowledge k
46
96
  JOIN knowledge_fts fts ON k.rowid = fts.rowid
47
97
  WHERE knowledge_fts MATCH ?
@@ -58,6 +108,30 @@
58
108
  params
59
109
  };
60
110
  }
111
+ export function buildBroadSearchQuery(ftsQuery, tokens, scope, limit = 5) {
112
+ const tokenMatches = tokens.map(()=>'(CASE WHEN k.rowid IN (SELECT rowid FROM knowledge_fts WHERE knowledge_fts MATCH ?) THEN 1 ELSE 0 END)').join(' + ');
113
+ const params = tokens.map((token)=>`"${token}"*`);
114
+ let sql = `
115
+ SELECT
116
+ k.id, k.title, k.content, k.tags, k.scope, k.created_at, k.updated_at,
117
+ bm25(knowledge_fts, 10.0, 5.0, 1.0) as bm25_score,
118
+ CAST((${tokenMatches}) AS REAL) / ${tokens.length} as token_coverage
119
+ FROM knowledge k
120
+ JOIN knowledge_fts fts ON k.rowid = fts.rowid
121
+ WHERE knowledge_fts MATCH ?
122
+ `;
123
+ params.push(ftsQuery);
124
+ if (scope) {
125
+ sql += ` AND (k.scope = ? OR k.scope = 'global')`;
126
+ params.push(scope);
127
+ }
128
+ sql += ` ORDER BY token_coverage DESC, bm25_score LIMIT ?`;
129
+ params.push(limit);
130
+ return {
131
+ sql,
132
+ params
133
+ };
134
+ }
61
135
  /**
62
136
  * Build simple search query without FTS (fallback for empty queries)
63
137
  */ export function buildSimpleQuery(scope, limit = 5) {
@@ -65,7 +139,8 @@
65
139
  let sql = `
66
140
  SELECT
67
141
  id, title, content, tags, scope, created_at, updated_at,
68
- 0 as bm25_score
142
+ 0 as bm25_score,
143
+ 1.0 as token_coverage
69
144
  FROM knowledge
70
145
  `;
71
146
  if (scope) {
@@ -1 +1 @@
1
- {"version":3,"sources":["../../src/services/search.ts"],"sourcesContent":["/**\n * FTS5 Query Builder\n * Converts natural language queries to FTS5 match expressions\n */\n\n/**\n * Escape special FTS5 characters to prevent query syntax errors\n */\nfunction escapeFtsSpecialChars(text: string): string {\n // FTS5 special characters: \" * ^ - : OR AND NOT ( )\n return text\n .replace(/\"/g, '\"\"') // Escape quotes by doubling\n .replace(/[*^():-]/g, ' ') // Replace operators with space (including hyphen)\n .replace(/\\b(AND|OR|NOT)\\b/gi, '') // Remove boolean operators\n .trim()\n .replace(/\\s+/g, ' '); // Collapse multiple spaces\n}\n\n/**\n * Build FTS5 query from natural language input\n * \n * Strategy:\n * - Split query into words\n * - Use prefix matching (*) for partial matches\n * - Escape special characters\n */\nexport function buildFtsQuery(query: string): string {\n const escaped = escapeFtsSpecialChars(query);\n const words = escaped.split(/\\s+/).filter(w => w.length > 0);\n\n if (words.length === 0) {\n return '';\n }\n\n // Use prefix matching for each word\n // This allows \"api design\" to match \"API\", \"designing\", etc.\n return words.map(word => `${word}*`).join(' ');\n}\n\n/**\n * Build FTS5 query with column boosting\n * Uses bm25() with weights: title=10, content=5, tags=1\n */\nexport function buildSearchQuery(\n ftsQuery: string,\n scope?: string | null,\n limit = 5\n): { sql: string; params: unknown[] } {\n const params: unknown[] = [];\n\n let sql = `\n SELECT \n k.id,\n k.title,\n k.content,\n k.tags,\n k.scope,\n k.created_at,\n k.updated_at,\n bm25(knowledge_fts, 10.0, 5.0, 1.0) as bm25_score\n FROM knowledge k\n JOIN knowledge_fts fts ON k.rowid = fts.rowid\n WHERE knowledge_fts MATCH ?\n `;\n params.push(ftsQuery);\n\n if (scope) {\n sql += ` AND (k.scope = ? OR k.scope = 'global')`;\n params.push(scope);\n }\n\n sql += ` ORDER BY bm25_score LIMIT ?`;\n params.push(limit);\n\n return { sql, params };\n}\n\n/**\n * Build simple search query without FTS (fallback for empty queries)\n */\nexport function buildSimpleQuery(\n scope?: string | null,\n limit = 5\n): { sql: string; params: unknown[] } {\n const params: unknown[] = [];\n\n let sql = `\n SELECT \n id, title, content, tags, scope, created_at, updated_at,\n 0 as bm25_score\n FROM knowledge\n `;\n\n if (scope) {\n sql += ` WHERE scope = ? OR scope = 'global'`;\n params.push(scope);\n }\n\n sql += ` ORDER BY created_at DESC LIMIT ?`;\n params.push(limit);\n\n return { sql, params };\n}\n"],"names":["escapeFtsSpecialChars","text","replace","trim","buildFtsQuery","query","escaped","words","split","filter","w","length","map","word","join","buildSearchQuery","ftsQuery","scope","limit","params","sql","push","buildSimpleQuery"],"mappings":"AAAA;;;CAGC,GAED;;CAEC,GACD,SAASA,sBAAsBC,IAAY;IACvC,oDAAoD;IACpD,OAAOA,KACFC,OAAO,CAAC,MAAM,MAAO,4BAA4B;KACjDA,OAAO,CAAC,aAAa,KAAM,kDAAkD;KAC7EA,OAAO,CAAC,sBAAsB,IAAK,2BAA2B;KAC9DC,IAAI,GACJD,OAAO,CAAC,QAAQ,MAAO,2BAA2B;AAC3D;AAEA;;;;;;;CAOC,GACD,OAAO,SAASE,cAAcC,KAAa;IACvC,MAAMC,UAAUN,sBAAsBK;IACtC,MAAME,QAAQD,QAAQE,KAAK,CAAC,OAAOC,MAAM,CAACC,CAAAA,IAAKA,EAAEC,MAAM,GAAG;IAE1D,IAAIJ,MAAMI,MAAM,KAAK,GAAG;QACpB,OAAO;IACX;IAEA,oCAAoC;IACpC,6DAA6D;IAC7D,OAAOJ,MAAMK,GAAG,CAACC,CAAAA,OAAQ,GAAGA,KAAK,CAAC,CAAC,EAAEC,IAAI,CAAC;AAC9C;AAEA;;;CAGC,GACD,OAAO,SAASC,iBACZC,QAAgB,EAChBC,KAAqB,EACrBC,QAAQ,CAAC;IAET,MAAMC,SAAoB,EAAE;IAE5B,IAAIC,MAAM,CAAC;;;;;;;;;;;;;EAab,CAAC;IACCD,OAAOE,IAAI,CAACL;IAEZ,IAAIC,OAAO;QACPG,OAAO,CAAC,wCAAwC,CAAC;QACjDD,OAAOE,IAAI,CAACJ;IAChB;IAEAG,OAAO,CAAC,4BAA4B,CAAC;IACrCD,OAAOE,IAAI,CAACH;IAEZ,OAAO;QAAEE;QAAKD;IAAO;AACzB;AAEA;;CAEC,GACD,OAAO,SAASG,iBACZL,KAAqB,EACrBC,QAAQ,CAAC;IAET,MAAMC,SAAoB,EAAE;IAE5B,IAAIC,MAAM,CAAC;;;;;EAKb,CAAC;IAEC,IAAIH,OAAO;QACPG,OAAO,CAAC,oCAAoC,CAAC;QAC7CD,OAAOE,IAAI,CAACJ;IAChB;IAEAG,OAAO,CAAC,iCAAiC,CAAC;IAC1CD,OAAOE,IAAI,CAACH;IAEZ,OAAO;QAAEE;QAAKD;IAAO;AACzB"}
1
+ {"version":3,"sources":["../../src/services/search.ts"],"sourcesContent":["/**\n * FTS5 Query Builder\n * Converts natural language queries to FTS5 match expressions\n */\n\nconst STOPWORDS = new Set([\n 'a', 'an', 'and', 'are', 'as', 'at', 'be', 'been', 'but', 'by', 'do', 'does',\n 'for', 'from', 'had', 'has', 'have', 'how', 'i', 'if', 'in', 'is', 'it', 'of',\n 'on', 'or', 'should', 'that', 'the', 'their', 'this', 'to', 'was', 'what',\n 'when', 'where', 'which', 'who', 'why', 'will', 'with', 'you', 'your',\n]);\n\nexport function normalizeSearchTokens(query: string): string[] {\n const matches = query.normalize('NFKC').match(/[\\p{L}\\p{N}]+/gu) ?? [];\n const seen = new Set<string>();\n\n return matches.flatMap(token => {\n const normalized = token.toLowerCase();\n if (normalized.length === 1 || STOPWORDS.has(normalized) || seen.has(normalized)) {\n return [];\n }\n seen.add(normalized);\n return [normalized];\n });\n}\n\nfunction buildPrefixTerms(query: string): string[] {\n return normalizeSearchTokens(query).map(token => `\"${token}\"*`);\n}\n\n/**\n * Build FTS5 query from natural language input\n * \n * Strategy:\n * - Split query into words\n * - Use prefix matching (*) for partial matches\n * - Escape special characters\n */\nexport function buildFtsQuery(query: string): string {\n return buildPrefixTerms(query).join(' ');\n}\n\nexport function buildBroadFtsQuery(query: string): string {\n return buildPrefixTerms(query).join(' OR ');\n}\n\n/**\n * Build FTS5 query with column boosting\n * Uses bm25() with weights: title=10, content=5, tags=1\n */\nexport function buildSearchQuery(\n ftsQuery: string,\n scope?: string | null,\n limit = 5\n): { sql: string; params: unknown[] } {\n const params: unknown[] = [];\n\n let sql = `\n SELECT \n k.id,\n k.title,\n k.content,\n k.tags,\n k.scope,\n k.created_at,\n k.updated_at,\n bm25(knowledge_fts, 10.0, 5.0, 1.0) as bm25_score,\n 1.0 as token_coverage\n FROM knowledge k\n JOIN knowledge_fts fts ON k.rowid = fts.rowid\n WHERE knowledge_fts MATCH ?\n `;\n params.push(ftsQuery);\n\n if (scope) {\n sql += ` AND (k.scope = ? OR k.scope = 'global')`;\n params.push(scope);\n }\n\n sql += ` ORDER BY bm25_score LIMIT ?`;\n params.push(limit);\n\n return { sql, params };\n}\n\nexport function buildBroadSearchQuery(\n ftsQuery: string,\n tokens: string[],\n scope?: string | null,\n limit = 5\n): { sql: string; params: unknown[] } {\n const tokenMatches = tokens.map(() =>\n '(CASE WHEN k.rowid IN (SELECT rowid FROM knowledge_fts WHERE knowledge_fts MATCH ?) THEN 1 ELSE 0 END)'\n ).join(' + ');\n const params: unknown[] = tokens.map(token => `\"${token}\"*`);\n let sql = `\n SELECT\n k.id, k.title, k.content, k.tags, k.scope, k.created_at, k.updated_at,\n bm25(knowledge_fts, 10.0, 5.0, 1.0) as bm25_score,\n CAST((${tokenMatches}) AS REAL) / ${tokens.length} as token_coverage\n FROM knowledge k\n JOIN knowledge_fts fts ON k.rowid = fts.rowid\n WHERE knowledge_fts MATCH ?\n `;\n params.push(ftsQuery);\n\n if (scope) {\n sql += ` AND (k.scope = ? OR k.scope = 'global')`;\n params.push(scope);\n }\n\n sql += ` ORDER BY token_coverage DESC, bm25_score LIMIT ?`;\n params.push(limit);\n return { sql, params };\n}\n\n/**\n * Build simple search query without FTS (fallback for empty queries)\n */\nexport function buildSimpleQuery(\n scope?: string | null,\n limit = 5\n): { sql: string; params: unknown[] } {\n const params: unknown[] = [];\n\n let sql = `\n SELECT \n id, title, content, tags, scope, created_at, updated_at,\n 0 as bm25_score,\n 1.0 as token_coverage\n FROM knowledge\n `;\n\n if (scope) {\n sql += ` WHERE scope = ? OR scope = 'global'`;\n params.push(scope);\n }\n\n sql += ` ORDER BY created_at DESC LIMIT ?`;\n params.push(limit);\n\n return { sql, params };\n}\n"],"names":["STOPWORDS","Set","normalizeSearchTokens","query","matches","normalize","match","seen","flatMap","token","normalized","toLowerCase","length","has","add","buildPrefixTerms","map","buildFtsQuery","join","buildBroadFtsQuery","buildSearchQuery","ftsQuery","scope","limit","params","sql","push","buildBroadSearchQuery","tokens","tokenMatches","buildSimpleQuery"],"mappings":"AAAA;;;CAGC,GAED,MAAMA,YAAY,IAAIC,IAAI;IACtB;IAAK;IAAM;IAAO;IAAO;IAAM;IAAM;IAAM;IAAQ;IAAO;IAAM;IAAM;IACtE;IAAO;IAAQ;IAAO;IAAO;IAAQ;IAAO;IAAK;IAAM;IAAM;IAAM;IAAM;IACzE;IAAM;IAAM;IAAU;IAAQ;IAAO;IAAS;IAAQ;IAAM;IAAO;IACnE;IAAQ;IAAS;IAAS;IAAO;IAAO;IAAQ;IAAQ;IAAO;CAClE;AAED,OAAO,SAASC,sBAAsBC,KAAa;IAC/C,MAAMC,UAAUD,MAAME,SAAS,CAAC,QAAQC,KAAK,CAAC,sBAAsB,EAAE;IACtE,MAAMC,OAAO,IAAIN;IAEjB,OAAOG,QAAQI,OAAO,CAACC,CAAAA;QACnB,MAAMC,aAAaD,MAAME,WAAW;QACpC,IAAID,WAAWE,MAAM,KAAK,KAAKZ,UAAUa,GAAG,CAACH,eAAeH,KAAKM,GAAG,CAACH,aAAa;YAC9E,OAAO,EAAE;QACb;QACAH,KAAKO,GAAG,CAACJ;QACT,OAAO;YAACA;SAAW;IACvB;AACJ;AAEA,SAASK,iBAAiBZ,KAAa;IACnC,OAAOD,sBAAsBC,OAAOa,GAAG,CAACP,CAAAA,QAAS,CAAC,CAAC,EAAEA,MAAM,EAAE,CAAC;AAClE;AAEA;;;;;;;CAOC,GACD,OAAO,SAASQ,cAAcd,KAAa;IACvC,OAAOY,iBAAiBZ,OAAOe,IAAI,CAAC;AACxC;AAEA,OAAO,SAASC,mBAAmBhB,KAAa;IAC5C,OAAOY,iBAAiBZ,OAAOe,IAAI,CAAC;AACxC;AAEA;;;CAGC,GACD,OAAO,SAASE,iBACZC,QAAgB,EAChBC,KAAqB,EACrBC,QAAQ,CAAC;IAET,MAAMC,SAAoB,EAAE;IAE5B,IAAIC,MAAM,CAAC;;;;;;;;;;;;;;EAcb,CAAC;IACCD,OAAOE,IAAI,CAACL;IAEZ,IAAIC,OAAO;QACPG,OAAO,CAAC,wCAAwC,CAAC;QACjDD,OAAOE,IAAI,CAACJ;IAChB;IAEAG,OAAO,CAAC,4BAA4B,CAAC;IACrCD,OAAOE,IAAI,CAACH;IAEZ,OAAO;QAAEE;QAAKD;IAAO;AACzB;AAEA,OAAO,SAASG,sBACZN,QAAgB,EAChBO,MAAgB,EAChBN,KAAqB,EACrBC,QAAQ,CAAC;IAET,MAAMM,eAAeD,OAAOZ,GAAG,CAAC,IAC5B,0GACFE,IAAI,CAAC;IACP,MAAMM,SAAoBI,OAAOZ,GAAG,CAACP,CAAAA,QAAS,CAAC,CAAC,EAAEA,MAAM,EAAE,CAAC;IAC3D,IAAIgB,MAAM,CAAC;;;;YAIH,EAAEI,aAAa,aAAa,EAAED,OAAOhB,MAAM,CAAC;;;;EAItD,CAAC;IACCY,OAAOE,IAAI,CAACL;IAEZ,IAAIC,OAAO;QACPG,OAAO,CAAC,wCAAwC,CAAC;QACjDD,OAAOE,IAAI,CAACJ;IAChB;IAEAG,OAAO,CAAC,iDAAiD,CAAC;IAC1DD,OAAOE,IAAI,CAACH;IACZ,OAAO;QAAEE;QAAKD;IAAO;AACzB;AAEA;;CAEC,GACD,OAAO,SAASM,iBACZR,KAAqB,EACrBC,QAAQ,CAAC;IAET,MAAMC,SAAoB,EAAE;IAE5B,IAAIC,MAAM,CAAC;;;;;;EAMb,CAAC;IAEC,IAAIH,OAAO;QACPG,OAAO,CAAC,oCAAoC,CAAC;QAC7CD,OAAOE,IAAI,CAACJ;IAChB;IAEAG,OAAO,CAAC,iCAAiC,CAAC;IAC1CD,OAAOE,IAAI,CAACH;IAEZ,OAAO;QAAEE;QAAKD;IAAO;AACzB"}
@@ -51,6 +51,7 @@ export interface SearchKnowledgeResult {
51
51
  results: SearchResultItem[];
52
52
  totalMatches: number;
53
53
  query: string;
54
+ strategy: 'strict' | 'broad' | 'recent';
54
55
  }
55
56
  export type ListKnowledgeSort = 'updated-desc' | 'created-desc' | 'title-asc';
56
57
  export interface ListKnowledgeInput {
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/types/index.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/types/index.ts"],"names":[],"mappings":"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"}
@@ -1 +1 @@
1
- {"version":3,"sources":["../../src/types/index.ts"],"sourcesContent":["export interface KnowledgeItem {\n id: string;\n title: string;\n content: string;\n tags: string[];\n scope: string;\n normalizedTitle: string;\n contentHash: string;\n createdAt: string;\n updatedAt: string;\n}\n\nexport type KnowledgeScope = 'global' | `project:${string}` | `repo:${string}`;\n\nexport interface StoreKnowledgeInput {\n title: string;\n content: string;\n tags?: string[];\n scope?: string;\n}\n\nexport interface StoreKnowledgeResult {\n success: boolean;\n id?: string;\n message: string;\n}\n\nexport interface UpdateKnowledgeInput {\n id: string;\n title?: string;\n content?: string;\n tags?: string[];\n scope?: string;\n}\n\nexport interface UpdateKnowledgeResult {\n success: boolean;\n id: string;\n message: string;\n}\n\nexport interface SearchKnowledgeInput {\n query: string;\n contextTags?: string[];\n scope?: string;\n limit?: number;\n}\n\nexport interface SearchResultItem {\n id: string;\n title: string;\n content: string;\n tags: string[];\n scope: string;\n score: number;\n}\n\nexport interface SearchKnowledgeResult {\n results: SearchResultItem[];\n totalMatches: number;\n query: string;\n}\n\nexport type ListKnowledgeSort = 'updated-desc' | 'created-desc' | 'title-asc';\n\nexport interface ListKnowledgeInput {\n query?: string;\n tags?: string[];\n scope?: string;\n limit?: number;\n offset?: number;\n sort?: ListKnowledgeSort;\n}\n\nexport interface ListKnowledgeResult {\n items: KnowledgeItem[];\n total: number;\n}\n\nexport interface KnowledgeSummaryResult {\n totalItems: number;\n scopes: Array<{ scope: string; count: number }>;\n tags: Array<{ tag: string; count: number }>;\n recency: Array<{ bucket: string; count: number }>;\n}\n\nexport interface KnowledgeRow {\n id: string;\n title: string;\n content: string;\n tags: string;\n scope: string;\n normalized_title: string;\n content_hash: string;\n created_at: string;\n updated_at: string;\n}\n\nexport interface MetaRow {\n key: string;\n value: string;\n}\n"],"names":[],"mappings":"AAkGA,WAGC"}
1
+ {"version":3,"sources":["../../src/types/index.ts"],"sourcesContent":["export interface KnowledgeItem {\n id: string;\n title: string;\n content: string;\n tags: string[];\n scope: string;\n normalizedTitle: string;\n contentHash: string;\n createdAt: string;\n updatedAt: string;\n}\n\nexport type KnowledgeScope = 'global' | `project:${string}` | `repo:${string}`;\n\nexport interface StoreKnowledgeInput {\n title: string;\n content: string;\n tags?: string[];\n scope?: string;\n}\n\nexport interface StoreKnowledgeResult {\n success: boolean;\n id?: string;\n message: string;\n}\n\nexport interface UpdateKnowledgeInput {\n id: string;\n title?: string;\n content?: string;\n tags?: string[];\n scope?: string;\n}\n\nexport interface UpdateKnowledgeResult {\n success: boolean;\n id: string;\n message: string;\n}\n\nexport interface SearchKnowledgeInput {\n query: string;\n contextTags?: string[];\n scope?: string;\n limit?: number;\n}\n\nexport interface SearchResultItem {\n id: string;\n title: string;\n content: string;\n tags: string[];\n scope: string;\n score: number;\n}\n\nexport interface SearchKnowledgeResult {\n results: SearchResultItem[];\n totalMatches: number;\n query: string;\n strategy: 'strict' | 'broad' | 'recent';\n}\n\nexport type ListKnowledgeSort = 'updated-desc' | 'created-desc' | 'title-asc';\n\nexport interface ListKnowledgeInput {\n query?: string;\n tags?: string[];\n scope?: string;\n limit?: number;\n offset?: number;\n sort?: ListKnowledgeSort;\n}\n\nexport interface ListKnowledgeResult {\n items: KnowledgeItem[];\n total: number;\n}\n\nexport interface KnowledgeSummaryResult {\n totalItems: number;\n scopes: Array<{ scope: string; count: number }>;\n tags: Array<{ tag: string; count: number }>;\n recency: Array<{ bucket: string; count: number }>;\n}\n\nexport interface KnowledgeRow {\n id: string;\n title: string;\n content: string;\n tags: string;\n scope: string;\n normalized_title: string;\n content_hash: string;\n created_at: string;\n updated_at: string;\n}\n\nexport interface MetaRow {\n key: string;\n value: string;\n}\n"],"names":[],"mappings":"AAmGA,WAGC"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ai-devkit/memory",
3
- "version": "0.16.0",
3
+ "version": "0.17.0",
4
4
  "type": "module",
5
5
  "description": "Local MCP memory for AI coding agents to reuse project decisions, conventions, and fixes across sessions",
6
6
  "main": "dist/index.js",