scalix 1.0.0 → 1.1.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.
@@ -0,0 +1,257 @@
1
+ import fs from 'node:fs/promises';
2
+ import path from 'node:path';
3
+ import { estimateTokens } from './token-counter.js';
4
+ export const RETRIEVAL_DEFAULTS = {
5
+ enabled: true,
6
+ maxSnippets: 5,
7
+ maxTokensPerSnippet: 500,
8
+ maxTotalTokens: 3000,
9
+ previewLines: 30,
10
+ minRelevanceScore: 0.3,
11
+ };
12
+ const STOP_WORDS = new Set([
13
+ 'a', 'an', 'the', 'is', 'are', 'was', 'were', 'be', 'been',
14
+ 'in', 'on', 'at', 'to', 'for', 'of', 'with', 'by', 'from',
15
+ 'and', 'or', 'but', 'not', 'no', 'so', 'if', 'then', 'else',
16
+ 'do', 'does', 'did', 'will', 'would', 'should', 'could', 'can',
17
+ 'has', 'have', 'had', 'this', 'that', 'these', 'those',
18
+ 'it', 'its', 'i', 'me', 'my', 'we', 'our', 'you', 'your',
19
+ 'he', 'she', 'they', 'them', 'what', 'which', 'who', 'how',
20
+ 'when', 'where', 'why', 'all', 'each', 'every', 'any', 'some',
21
+ 'about', 'up', 'out', 'just', 'also', 'than', 'very',
22
+ 'code', 'file', 'function', 'change', 'make', 'use', 'using',
23
+ 'want', 'need', 'like', 'please', 'help', 'show', 'get',
24
+ ]);
25
+ const FILE_HINT_RE = /(?:^|[\s`"'(])([.\w][\w./-]*\/[\w./-]+\.\w{1,10})(?=[\s`"'),.:;]|$)/gm;
26
+ const CAMEL_RE = /\b([A-Z][a-zA-Z]{2,})\b/g;
27
+ const SNAKE_RE = /\b([a-z][a-z0-9]*(?:_[a-z0-9]+){1,})\b/g;
28
+ const INTENT_PATTERNS = {
29
+ debug: /\b(?:fix|debug|error|bug|broken|crash|fail|issue|wrong|problem)\b/i,
30
+ create: /\b(?:create|add|new|implement|build|generate|write|introduce)\b/i,
31
+ modify: /\b(?:change|update|modify|refactor|rename|move|replace|improve|optimize)\b/i,
32
+ explore: /\b(?:read|show|explain|understand|find|search|look|what|where|how|list|describe)\b/i,
33
+ general: /./,
34
+ };
35
+ export function analyzeQuery(query) {
36
+ const words = query.toLowerCase().split(/\s+/).filter(Boolean);
37
+ const keywords = words.filter((w) => !STOP_WORDS.has(w) && w.length > 2);
38
+ const fileHints = [];
39
+ for (const m of query.matchAll(FILE_HINT_RE)) {
40
+ fileHints.push(m[1]);
41
+ }
42
+ const symbolHints = [];
43
+ for (const m of query.matchAll(CAMEL_RE)) {
44
+ symbolHints.push(m[1]);
45
+ }
46
+ for (const m of query.matchAll(SNAKE_RE)) {
47
+ symbolHints.push(m[1]);
48
+ }
49
+ let intentType = 'general';
50
+ for (const [intent, re] of Object.entries(INTENT_PATTERNS)) {
51
+ if (intent === 'general')
52
+ continue;
53
+ if (re.test(query)) {
54
+ intentType = intent;
55
+ break;
56
+ }
57
+ }
58
+ return { keywords, fileHints, symbolHints, intentType };
59
+ }
60
+ function scoreFiles(analysis, index) {
61
+ const symbolsByFile = new Map();
62
+ for (const sym of index.symbols) {
63
+ const existing = symbolsByFile.get(sym.file) ?? [];
64
+ existing.push(sym);
65
+ symbolsByFile.set(sym.file, existing);
66
+ }
67
+ const importGraph = new Map();
68
+ for (const imp of index.imports) {
69
+ const deps = importGraph.get(imp.from) ?? new Set();
70
+ deps.add(imp.to);
71
+ importGraph.set(imp.from, deps);
72
+ }
73
+ const recentFiles = new Set(index.files
74
+ .sort((a, b) => b.modifiedAt - a.modifiedAt)
75
+ .slice(0, 20)
76
+ .map((f) => f.relativePath));
77
+ const directMentionFiles = new Set();
78
+ for (const hint of analysis.fileHints) {
79
+ for (const f of index.files) {
80
+ if (f.relativePath === hint || f.relativePath.endsWith(hint)) {
81
+ directMentionFiles.add(f.relativePath);
82
+ }
83
+ }
84
+ }
85
+ const scores = [];
86
+ for (const file of index.files) {
87
+ let score = 0;
88
+ const reasons = [];
89
+ const matchingSymbols = [];
90
+ // Direct file mention
91
+ if (directMentionFiles.has(file.relativePath)) {
92
+ score += 1.0;
93
+ reasons.push('directly mentioned');
94
+ }
95
+ // Symbol match
96
+ const fileSymbols = symbolsByFile.get(file.relativePath) ?? [];
97
+ for (const sym of fileSymbols) {
98
+ const nameLower = sym.name.toLowerCase();
99
+ if (analysis.symbolHints.some((h) => h.toLowerCase() === nameLower)) {
100
+ score += 0.6;
101
+ matchingSymbols.push(sym.name);
102
+ reasons.push(`symbol "${sym.name}"`);
103
+ }
104
+ else if (analysis.keywords.some((k) => nameLower.includes(k))) {
105
+ score += 0.3;
106
+ matchingSymbols.push(sym.name);
107
+ reasons.push(`keyword in symbol "${sym.name}"`);
108
+ }
109
+ }
110
+ // Import proximity — file is imported by a directly-mentioned file
111
+ for (const mentionedFile of directMentionFiles) {
112
+ const deps = importGraph.get(mentionedFile);
113
+ if (deps?.has(file.relativePath)) {
114
+ score += 0.3;
115
+ reasons.push(`imported by ${mentionedFile}`);
116
+ }
117
+ }
118
+ // Keyword in path
119
+ const pathLower = file.relativePath.toLowerCase();
120
+ for (const kw of analysis.keywords) {
121
+ if (pathLower.includes(kw)) {
122
+ score += 0.2;
123
+ reasons.push(`keyword "${kw}" in path`);
124
+ }
125
+ }
126
+ // Recency bonus
127
+ if (recentFiles.has(file.relativePath)) {
128
+ score += 0.1;
129
+ reasons.push('recently modified');
130
+ }
131
+ if (score > 0) {
132
+ scores.push({
133
+ file: file.relativePath,
134
+ score: Math.min(score, 2.0),
135
+ matchingSymbols,
136
+ reason: reasons.join(', '),
137
+ });
138
+ }
139
+ }
140
+ return scores.sort((a, b) => b.score - a.score);
141
+ }
142
+ async function generatePreview(filePath, projectRoot, matchingSymbols, maxLines) {
143
+ try {
144
+ const absPath = path.join(projectRoot, filePath);
145
+ const content = await fs.readFile(absPath, 'utf-8');
146
+ const lines = content.split('\n');
147
+ if (matchingSymbols.length > 0) {
148
+ // Show lines around matching symbols
149
+ const ranges = [];
150
+ for (const sym of matchingSymbols.slice(0, 3)) {
151
+ const start = Math.max(0, sym.line - 2);
152
+ const end = Math.min(lines.length - 1, sym.line + 8);
153
+ ranges.push([start, end]);
154
+ }
155
+ // Merge overlapping ranges
156
+ ranges.sort((a, b) => a[0] - b[0]);
157
+ const merged = [ranges[0]];
158
+ for (let i = 1; i < ranges.length; i++) {
159
+ const prev = merged[merged.length - 1];
160
+ if (ranges[i][0] <= prev[1] + 2) {
161
+ prev[1] = Math.max(prev[1], ranges[i][1]);
162
+ }
163
+ else {
164
+ merged.push(ranges[i]);
165
+ }
166
+ }
167
+ const parts = [];
168
+ let totalLines = 0;
169
+ for (const [start, end] of merged) {
170
+ if (totalLines >= maxLines)
171
+ break;
172
+ const available = maxLines - totalLines;
173
+ const rangeEnd = Math.min(end, start + available - 1);
174
+ for (let i = start; i <= rangeEnd; i++) {
175
+ parts.push(`${i + 1}\t${lines[i]}`);
176
+ totalLines++;
177
+ }
178
+ if (rangeEnd < end)
179
+ break;
180
+ parts.push('...');
181
+ }
182
+ return parts.join('\n');
183
+ }
184
+ // No matching symbols — show first N lines
185
+ return lines
186
+ .slice(0, maxLines)
187
+ .map((l, i) => `${i + 1}\t${l}`)
188
+ .join('\n');
189
+ }
190
+ catch {
191
+ return '(file could not be read)';
192
+ }
193
+ }
194
+ export async function retrieveContext(query, index, config = RETRIEVAL_DEFAULTS) {
195
+ const analysis = analyzeQuery(query);
196
+ if (analysis.keywords.length === 0 && analysis.fileHints.length === 0 && analysis.symbolHints.length === 0) {
197
+ return { snippets: [], totalTokens: 0, queryTerms: [] };
198
+ }
199
+ const scored = scoreFiles(analysis, index);
200
+ const candidates = scored.filter((s) => s.score >= config.minRelevanceScore).slice(0, config.maxSnippets);
201
+ const symbolsByFile = new Map();
202
+ for (const sym of index.symbols) {
203
+ const existing = symbolsByFile.get(sym.file) ?? [];
204
+ existing.push(sym);
205
+ symbolsByFile.set(sym.file, existing);
206
+ }
207
+ const snippets = [];
208
+ let totalTokens = 0;
209
+ for (const candidate of candidates) {
210
+ if (totalTokens >= config.maxTotalTokens)
211
+ break;
212
+ const matchingSymbols = (symbolsByFile.get(candidate.file) ?? [])
213
+ .filter((s) => candidate.matchingSymbols.includes(s.name));
214
+ const preview = await generatePreview(candidate.file, index.root, matchingSymbols, config.previewLines);
215
+ let tokens = estimateTokens(preview);
216
+ let truncatedPreview = preview;
217
+ if (tokens > config.maxTokensPerSnippet) {
218
+ const lines = preview.split('\n');
219
+ const kept = [];
220
+ let running = 0;
221
+ for (const line of lines) {
222
+ const lt = estimateTokens(line);
223
+ if (running + lt > config.maxTokensPerSnippet)
224
+ break;
225
+ kept.push(line);
226
+ running += lt;
227
+ }
228
+ truncatedPreview = kept.join('\n');
229
+ tokens = running;
230
+ }
231
+ if (totalTokens + tokens > config.maxTotalTokens)
232
+ break;
233
+ snippets.push({
234
+ file: candidate.file,
235
+ relevanceScore: candidate.score,
236
+ symbols: candidate.matchingSymbols,
237
+ preview: truncatedPreview,
238
+ reason: candidate.reason,
239
+ });
240
+ totalTokens += tokens;
241
+ }
242
+ return { snippets, totalTokens, queryTerms: analysis.keywords };
243
+ }
244
+ export function snippetsToContext(snippets) {
245
+ if (snippets.length === 0)
246
+ return '';
247
+ const parts = ['## Relevant Code Context',
248
+ 'The following file excerpts may be relevant. Use this as ground truth rather than guessing.\n'];
249
+ for (const s of snippets) {
250
+ const symbolStr = s.symbols.length > 0 ? `\nSymbols: ${s.symbols.join(', ')}` : '';
251
+ parts.push(`### ${s.file} (score: ${s.relevanceScore.toFixed(1)})${symbolStr}`);
252
+ parts.push('```');
253
+ parts.push(s.preview);
254
+ parts.push('```\n');
255
+ }
256
+ return parts.join('\n');
257
+ }
@@ -1,27 +1,30 @@
1
- import { execSync } from 'node:child_process';
1
+ import { exec } from 'node:child_process';
2
+ import { promisify } from 'node:util';
2
3
  import { TOOL_DEFAULTS, GIT_CONTEXT_DEFAULTS, PROJECT_INDEX_DEFAULTS } from './constants.js';
3
- function run(cmd, cwd) {
4
+ const execAsync = promisify(exec);
5
+ async function run(cmd, cwd) {
4
6
  try {
5
- return execSync(cmd, { cwd, encoding: 'utf-8', timeout: TOOL_DEFAULTS.gitTimeoutMs, stdio: ['pipe', 'pipe', 'pipe'] }).trim();
7
+ const { stdout } = await execAsync(cmd, { cwd, encoding: 'utf-8', timeout: TOOL_DEFAULTS.gitTimeoutMs });
8
+ return stdout.trim();
6
9
  }
7
10
  catch {
8
11
  return '';
9
12
  }
10
13
  }
11
- function isGitRepo(cwd) {
12
- return run('git rev-parse --is-inside-work-tree', cwd) === 'true';
14
+ async function isGitRepo(cwd) {
15
+ return (await run('git rev-parse --is-inside-work-tree', cwd)) === 'true';
13
16
  }
14
- function getBranch(cwd) {
15
- return run('git branch --show-current', cwd) || run('git rev-parse --short HEAD', cwd) || 'unknown';
17
+ async function getBranch(cwd) {
18
+ return (await run('git branch --show-current', cwd)) || (await run('git rev-parse --short HEAD', cwd)) || 'unknown';
16
19
  }
17
- function getRemoteBranch(cwd) {
20
+ async function getRemoteBranch(cwd) {
18
21
  return run('git rev-parse --abbrev-ref @{upstream} 2>/dev/null', cwd);
19
22
  }
20
- function getRemoteUrl(cwd) {
23
+ async function getRemoteUrl(cwd) {
21
24
  return run('git remote get-url origin 2>/dev/null', cwd);
22
25
  }
23
- function getAheadBehind(cwd) {
24
- const result = run('git rev-list --left-right --count @{upstream}...HEAD 2>/dev/null', cwd);
26
+ async function getAheadBehind(cwd) {
27
+ const result = await run('git rev-list --left-right --count @{upstream}...HEAD 2>/dev/null', cwd);
25
28
  if (!result)
26
29
  return { ahead: 0, behind: 0 };
27
30
  const parts = result.split('\t');
@@ -29,12 +32,12 @@ function getAheadBehind(cwd) {
29
32
  return { ahead: 0, behind: 0 };
30
33
  return { ahead: Number(parts[1]) || 0, behind: Number(parts[0]) || 0 };
31
34
  }
32
- function getStashCount(cwd) {
33
- const result = run('git stash list 2>/dev/null', cwd);
35
+ async function getStashCount(cwd) {
36
+ const result = await run('git stash list 2>/dev/null', cwd);
34
37
  return result ? result.split('\n').filter(Boolean).length : 0;
35
38
  }
36
- function getStatus(cwd) {
37
- const result = run('git status --porcelain', cwd);
39
+ async function getStatus(cwd) {
40
+ const result = await run('git status --porcelain', cwd);
38
41
  if (!result)
39
42
  return [];
40
43
  return result.split('\n').filter(Boolean).map((line) => {
@@ -68,10 +71,10 @@ function getStatus(cwd) {
68
71
  return { path: filePath, status, staged };
69
72
  });
70
73
  }
71
- function getRecentCommits(cwd, count = GIT_CONTEXT_DEFAULTS.recentCommitsCount) {
74
+ async function getRecentCommits(cwd, count = GIT_CONTEXT_DEFAULTS.recentCommitsCount) {
72
75
  const SEP = '<%>';
73
76
  const format = `%H${SEP}%h${SEP}%s${SEP}%an${SEP}%ar`;
74
- const result = run(`git log --pretty=format:"${format}" --shortstat -n ${count} 2>/dev/null`, cwd);
77
+ const result = await run(`git log --pretty=format:"${format}" --shortstat -n ${count} 2>/dev/null`, cwd);
75
78
  if (!result)
76
79
  return [];
77
80
  const commits = [];
@@ -103,8 +106,8 @@ function getRecentCommits(cwd, count = GIT_CONTEXT_DEFAULTS.recentCommitsCount)
103
106
  }
104
107
  return commits;
105
108
  }
106
- export function collectGitInfo(cwd) {
107
- if (!isGitRepo(cwd)) {
109
+ export async function collectGitInfo(cwd) {
110
+ if (!(await isGitRepo(cwd))) {
108
111
  return {
109
112
  isRepo: false,
110
113
  branch: '',
@@ -116,16 +119,16 @@ export function collectGitInfo(cwd) {
116
119
  remoteUrl: '',
117
120
  };
118
121
  }
119
- return {
120
- isRepo: true,
121
- branch: getBranch(cwd),
122
- remoteBranch: getRemoteBranch(cwd),
123
- status: getStatus(cwd),
124
- recentCommits: getRecentCommits(cwd, GIT_CONTEXT_DEFAULTS.recentCommitsContextCount),
125
- stashCount: getStashCount(cwd),
126
- aheadBehind: getAheadBehind(cwd),
127
- remoteUrl: getRemoteUrl(cwd),
128
- };
122
+ const [branch, remoteBranch, status, recentCommits, stashCount, aheadBehind, remoteUrl] = await Promise.all([
123
+ getBranch(cwd),
124
+ getRemoteBranch(cwd),
125
+ getStatus(cwd),
126
+ getRecentCommits(cwd, GIT_CONTEXT_DEFAULTS.recentCommitsContextCount),
127
+ getStashCount(cwd),
128
+ getAheadBehind(cwd),
129
+ getRemoteUrl(cwd),
130
+ ]);
131
+ return { isRepo: true, branch, remoteBranch, status, recentCommits, stashCount, aheadBehind, remoteUrl };
129
132
  }
130
133
  export function gitInfoToContext(info) {
131
134
  if (!info.isRepo)
@@ -0,0 +1,154 @@
1
+ export const GROUNDING_DEFAULTS = {
2
+ enabled: true,
3
+ maxNudges: 1,
4
+ ungroundedThreshold: 0.5,
5
+ minClaimsToCheck: 2,
6
+ };
7
+ const TRIVIAL_FILES = new Set([
8
+ 'README.md', 'LICENSE', 'package.json', 'tsconfig.json',
9
+ 'Cargo.toml', 'go.mod', 'Makefile', 'Dockerfile',
10
+ '.gitignore', '.env', '.env.example',
11
+ ]);
12
+ const FILE_PATH_RE = /(?:^|[\s`"'(])([.\w][\w./-]*\/[\w./-]+\.\w{1,10})(?=[\s`"'),.:;]|$)/gm;
13
+ const FUNC_CALL_RE = /\b([a-zA-Z_]\w{2,})\s*\(/g;
14
+ const FUNC_DESC_RE = /\b(?:function|method|def|fn|func|handler|callback)\s+([a-zA-Z_]\w{2,})\b/gi;
15
+ const CLASS_RE = /\b(?:class|interface|type|struct|enum|trait)\s+([A-Z]\w{2,})\b/g;
16
+ const LINE_NUM_RE = /\b(?:line\s+|:)(\d{2,5})\b/gi;
17
+ const IMPORT_RE = /(?:import|from|require)\s*\(?['"]([^'"]+)['"]\)?/g;
18
+ function surroundingContext(text, pos, radius = 60) {
19
+ const start = Math.max(0, pos - radius);
20
+ const end = Math.min(text.length, pos + radius);
21
+ return text.slice(start, end).replace(/\n/g, ' ').trim();
22
+ }
23
+ export function extractCodeClaims(text) {
24
+ const seen = new Set();
25
+ const claims = [];
26
+ function add(type, value, position) {
27
+ const key = `${type}:${value}`;
28
+ if (seen.has(key))
29
+ return;
30
+ seen.add(key);
31
+ claims.push({ type, value, context: surroundingContext(text, position), position });
32
+ }
33
+ // File paths (must contain a / to avoid single-word matches)
34
+ for (const m of text.matchAll(FILE_PATH_RE)) {
35
+ const val = m[1];
36
+ const basename = val.split('/').pop() || val;
37
+ if (TRIVIAL_FILES.has(basename) || TRIVIAL_FILES.has(val))
38
+ continue;
39
+ add('file_path', val, m.index + m[0].indexOf(val));
40
+ }
41
+ // Function calls — foo()
42
+ for (const m of text.matchAll(FUNC_CALL_RE)) {
43
+ const name = m[1];
44
+ if (name.length > 50 || /^(if|for|while|switch|return|new|typeof|catch|throw|await|async|const|let|var|function|class|import|export|from|this|self|true|false|null|undefined|console|process|require|module)$/.test(name))
45
+ continue;
46
+ add('function_name', name, m.index);
47
+ }
48
+ // Function descriptions — "the handleSubmit function"
49
+ for (const m of text.matchAll(FUNC_DESC_RE)) {
50
+ add('function_name', m[1], m.index);
51
+ }
52
+ // Class/interface/type names
53
+ for (const m of text.matchAll(CLASS_RE)) {
54
+ if (m[1].length > 50)
55
+ continue;
56
+ add('class_name', m[1], m.index);
57
+ }
58
+ // Line numbers (only >9 to avoid trivial matches)
59
+ for (const m of text.matchAll(LINE_NUM_RE)) {
60
+ const num = parseInt(m[1], 10);
61
+ if (num < 10)
62
+ continue;
63
+ add('line_number', m[1], m.index);
64
+ }
65
+ // Import references
66
+ for (const m of text.matchAll(IMPORT_RE)) {
67
+ if (m[1].startsWith('.')) {
68
+ add('import', m[1], m.index);
69
+ }
70
+ }
71
+ return claims;
72
+ }
73
+ function buildGroundingCorpus(messages) {
74
+ const parts = [];
75
+ for (const m of messages) {
76
+ if (m.role === 'tool' && m.content) {
77
+ parts.push(typeof m.content === 'string' ? m.content : JSON.stringify(m.content));
78
+ }
79
+ if (m.role === 'assistant' && m.tool_calls) {
80
+ for (const tc of m.tool_calls) {
81
+ parts.push(tc.function.arguments);
82
+ }
83
+ }
84
+ }
85
+ return parts.join('\n');
86
+ }
87
+ export function checkGrounding(claims, messages, threshold = GROUNDING_DEFAULTS.ungroundedThreshold, minClaims = GROUNDING_DEFAULTS.minClaimsToCheck) {
88
+ if (claims.length === 0) {
89
+ return { claims, groundedClaims: [], ungroundedClaims: [], groundingRatio: 1, needsVerification: false };
90
+ }
91
+ const corpus = buildGroundingCorpus(messages);
92
+ const corpusLower = corpus.toLowerCase();
93
+ const grounded = [];
94
+ const ungrounded = [];
95
+ for (const claim of claims) {
96
+ let found = false;
97
+ switch (claim.type) {
98
+ case 'file_path': {
99
+ if (corpus.includes(claim.value)) {
100
+ found = true;
101
+ }
102
+ else {
103
+ const basename = claim.value.split('/').pop();
104
+ if (corpus.includes(basename))
105
+ found = true;
106
+ }
107
+ break;
108
+ }
109
+ case 'function_name':
110
+ case 'class_name':
111
+ case 'variable_name': {
112
+ found = corpus.includes(claim.value);
113
+ break;
114
+ }
115
+ case 'line_number': {
116
+ const escaped = claim.value.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
117
+ const linePattern = new RegExp(`(?:^|\\n)\\s*${escaped}\\t|:${escaped}[:\\s]`, 'm');
118
+ found = linePattern.test(corpus);
119
+ break;
120
+ }
121
+ case 'import': {
122
+ found = corpus.includes(claim.value);
123
+ break;
124
+ }
125
+ default:
126
+ found = corpusLower.includes(claim.value.toLowerCase());
127
+ }
128
+ if (found)
129
+ grounded.push(claim);
130
+ else
131
+ ungrounded.push(claim);
132
+ }
133
+ const ratio = grounded.length / claims.length;
134
+ const needsVerification = ratio < threshold && ungrounded.length >= minClaims;
135
+ return { claims, groundedClaims: grounded, ungroundedClaims: ungrounded, groundingRatio: ratio, needsVerification };
136
+ }
137
+ export function formatGroundingNudge(result) {
138
+ const items = result.ungroundedClaims.slice(0, 5).map((c) => {
139
+ switch (c.type) {
140
+ case 'file_path': return `- File "${c.value}" (not read in this conversation)`;
141
+ case 'function_name': return `- Function "${c.value}" (not seen in any tool output)`;
142
+ case 'class_name': return `- Type/class "${c.value}" (not seen in any tool output)`;
143
+ case 'line_number': return `- Line ${c.value} (not verified by reading the file)`;
144
+ case 'import': return `- Import "${c.value}" (not verified)`;
145
+ default: return `- "${c.value}" (not verified)`;
146
+ }
147
+ });
148
+ return [
149
+ '[GROUNDING CHECK] Your response references code elements that were not verified by reading the actual source:',
150
+ ...items,
151
+ 'Please use read_file, search_files, or find_files to verify these references before presenting them as facts.',
152
+ 'If you are uncertain about a path or name, say so explicitly rather than guessing.',
153
+ ].join('\n');
154
+ }