scai 0.1.45 → 0.1.46

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.
@@ -8,7 +8,8 @@ import { buildContextualPrompt } from '../utils/buildContextualPrompt.js';
8
8
  import { generateFileTree } from '../utils/fileTree.js';
9
9
  import { log } from '../utils/log.js';
10
10
  import { PROMPT_LOG_PATH, SCAI_HOME, INDEX_DIR } from '../constants.js';
11
- const MAX_RELATED_FILES = 5;
11
+ const MAX_RELATED_FILES = 3;
12
+ const MAX_SUMMARY_LINES = 12;
12
13
  export async function runAskCommand(query) {
13
14
  if (!query) {
14
15
  query = await promptOnce('🧠 Ask your question:\n> ');
@@ -37,7 +38,6 @@ export async function runAskCommand(query) {
37
38
  // 🟩 STEP 2: Merge results (de-duplicate by full resolved path)
38
39
  const seen = new Set();
39
40
  const combinedResults = [];
40
- // Merging results now ensures all have `id`
41
41
  for (const file of semanticResults) {
42
42
  const resolved = path.resolve(file.path);
43
43
  seen.add(resolved);
@@ -48,7 +48,7 @@ export async function runAskCommand(query) {
48
48
  if (!seen.has(resolved)) {
49
49
  seen.add(resolved);
50
50
  combinedResults.push({
51
- id: file.id, // Ensure the id is included here
51
+ id: file.id,
52
52
  path: file.path,
53
53
  summary: file.summary || '',
54
54
  score: 0.0,
@@ -73,9 +73,12 @@ export async function runAskCommand(query) {
73
73
  let code = '';
74
74
  let topSummary = topFile.summary || '(No summary available)';
75
75
  let topFunctions = [];
76
- // Gather all file IDs from the combined results
76
+ // Truncate summary if needed
77
+ if (topSummary) {
78
+ topSummary = topSummary.split('\n').slice(0, MAX_SUMMARY_LINES).join('\n');
79
+ }
77
80
  const allFileIds = combinedResults
78
- .map(file => file.id) // Now file.id exists
81
+ .map(file => file.id)
79
82
  .filter((id) => typeof id === 'number');
80
83
  const allFunctionsMap = getFunctionsForFiles(allFileIds);
81
84
  try {
@@ -89,9 +92,13 @@ export async function runAskCommand(query) {
89
92
  // 🟩 STEP 5: Build relatedFiles with functions
90
93
  const relatedFiles = combinedResults.slice(0, MAX_RELATED_FILES).map(file => {
91
94
  const fileId = file.id;
95
+ let summary = file.summary || '(No summary available)';
96
+ if (summary) {
97
+ summary = summary.split('\n').slice(0, MAX_SUMMARY_LINES).join('\n');
98
+ }
92
99
  return {
93
100
  path: file.path,
94
- summary: file.summary || '(No summary available)',
101
+ summary,
95
102
  functions: allFunctionsMap[fileId]?.map(fn => fn.name) || [],
96
103
  };
97
104
  });
@@ -4,6 +4,7 @@ import path from 'path';
4
4
  import { generateEmbedding } from '../lib/generateEmbedding.js';
5
5
  import { sanitizeQueryForFts } from '../utils/sanitizeQuery.js';
6
6
  import * as sqlTemplates from './sqlTemplates.js';
7
+ import { stringSimilarity } from 'string-similarity-js';
7
8
  /**
8
9
  * 📄 Index a single file into the database.
9
10
  *
@@ -67,7 +68,6 @@ export async function searchFiles(query, topK = 5) {
67
68
  }
68
69
  const safeQuery = sanitizeQueryForFts(query);
69
70
  console.log(`Executing search query in FTS5: ${safeQuery}`);
70
- // Step 1: Narrow candidate set using fast keyword match
71
71
  const ftsResults = db.prepare(`
72
72
  SELECT fts.rowid AS id, f.path, f.summary, f.type, bm25(files_fts) AS bm25Score, f.embedding
73
73
  FROM files f
@@ -80,30 +80,39 @@ export async function searchFiles(query, topK = 5) {
80
80
  if (ftsResults.length === 0) {
81
81
  return [];
82
82
  }
83
- // Step 2: Compute score based on embedding similarity + BM25 score
84
83
  const bm25Min = Math.min(...ftsResults.map(r => r.bm25Score));
85
84
  const bm25Max = Math.max(...ftsResults.map(r => r.bm25Score));
86
85
  const scored = ftsResults.map(result => {
87
- let finalScore = 0;
88
86
  let sim = 0;
87
+ let finalScore = 0;
88
+ const normalizedBm25 = 1 - ((result.bm25Score - bm25Min) / (bm25Max - bm25Min + 1e-5));
89
89
  if (result.embedding) {
90
90
  try {
91
91
  const vector = JSON.parse(result.embedding);
92
92
  sim = cosineSimilarity(embedding, vector);
93
- const normalizedBm25 = 1 - ((result.bm25Score - bm25Min) / (bm25Max - bm25Min + 1e-5));
94
- finalScore = 0.7 * sim + 0.3 * normalizedBm25;
95
93
  }
96
94
  catch (err) {
97
95
  console.error(`❌ Failed to parse embedding for ${result.path}:`, err);
98
- finalScore = 0;
99
96
  }
100
97
  }
101
- else {
102
- // Fallback to BM25-only score
103
- finalScore = 1 - ((result.bm25Score - bm25Min) / (bm25Max - bm25Min + 1e-5));
98
+ const terms = query.toLowerCase().split(/\s+/);
99
+ const path = result.path.toLowerCase();
100
+ const summary = (result.summary || '').toLowerCase();
101
+ let termMatches = 0;
102
+ for (const term of terms) {
103
+ if (path.includes(term) || summary.includes(term)) {
104
+ termMatches += 1;
105
+ }
104
106
  }
107
+ const matchRatio = termMatches / terms.length;
108
+ const termBoost = matchRatio >= 1 ? 1.0 : matchRatio >= 0.5 ? 0.5 : 0;
109
+ // 🧠 Final score with hybrid weighting (BM25 + Embedding + Term Boost)
110
+ finalScore = 0.4 * normalizedBm25 + 0.4 * sim + 0.2 * termBoost;
111
+ // ✅ Fuzzy score using string-similarity-js
112
+ const fuzzyScore = stringSimilarity(query.toLowerCase(), `${path} ${summary}`);
113
+ finalScore += fuzzyScore * 10;
105
114
  return {
106
- id: result.id, // Ensure the id is included here
115
+ id: result.id,
107
116
  path: result.path,
108
117
  summary: result.summary,
109
118
  score: finalScore,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "scai",
3
- "version": "0.1.45",
3
+ "version": "0.1.46",
4
4
  "type": "module",
5
5
  "bin": {
6
6
  "scai": "./dist/index.js"
@@ -30,7 +30,8 @@
30
30
  "better-sqlite3": "^12.1.1",
31
31
  "commander": "^11.0.0",
32
32
  "fast-glob": "^3.3.3",
33
- "proper-lockfile": "^4.1.2"
33
+ "proper-lockfile": "^4.1.2",
34
+ "string-similarity-js": "^2.1.4"
34
35
  },
35
36
  "devDependencies": {
36
37
  "@types/better-sqlite3": "^7.6.13",