@kentwynn/kgraph 0.2.13 → 0.2.15
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/cli/commands/doctor.js +4 -0
- package/dist/cli/commands/pack.js +1 -1
- package/dist/cognition/cognition-quality.d.ts +1 -0
- package/dist/cognition/cognition-quality.js +5 -0
- package/dist/cognition/conclusion.js +19 -7
- package/dist/context/context-pack.d.ts +1 -1
- package/dist/context/context-pack.js +136 -2
- package/dist/context/context-query.js +97 -58
- package/dist/types/knowledge.d.ts +1 -1
- package/package.json +1 -1
|
@@ -179,6 +179,7 @@ export function printQualityReport(report) {
|
|
|
179
179
|
console.log(`Duplicate compatibility note titles: ${report.duplicateTitleCount}`);
|
|
180
180
|
console.log(`Generated files scanned: ${report.generatedFileScanCount}`);
|
|
181
181
|
console.log(`Expensive files: ${report.expensiveFileCount}`);
|
|
182
|
+
console.log(`High-confidence atoms without evidence: ${report.highConfidenceMissingEvidenceCount}`);
|
|
182
183
|
console.log(`Session repeated reads: ${report.sessionRepeatedReadCount}`);
|
|
183
184
|
console.log(`Session estimated read tokens: ${report.sessionEstimatedReadTokens}`);
|
|
184
185
|
console.log(`Session repeated-read tokens: ${report.sessionEstimatedRepeatedReadTokens}`);
|
|
@@ -214,6 +215,9 @@ function summarizeQualityFindings(report) {
|
|
|
214
215
|
if (report.generatedFileScanCount > 0) {
|
|
215
216
|
findings.push(`${report.generatedFileScanCount} generated/integration file(s) scanned; update excludes`);
|
|
216
217
|
}
|
|
218
|
+
if (report.highConfidenceMissingEvidenceCount > 0) {
|
|
219
|
+
findings.push(`${report.highConfidenceMissingEvidenceCount} high-confidence atom(s) without evidence; add file/symbol refs or supersede`);
|
|
220
|
+
}
|
|
217
221
|
return findings;
|
|
218
222
|
}
|
|
219
223
|
function summarizeCoverageNotes(report) {
|
|
@@ -26,7 +26,7 @@ export function registerPackCommand(program) {
|
|
|
26
26
|
readMaps(workspace),
|
|
27
27
|
]);
|
|
28
28
|
const response = await queryContext(workspace, config, maps, task);
|
|
29
|
-
const pack = buildContextPack(response, budget);
|
|
29
|
+
const pack = buildContextPack(response, budget, workspace.rootPath);
|
|
30
30
|
if (options.json) {
|
|
31
31
|
console.log(JSON.stringify(pack, null, 2));
|
|
32
32
|
return;
|
|
@@ -23,6 +23,7 @@ export interface CognitionQualityReport {
|
|
|
23
23
|
duplicateTitleCount: number;
|
|
24
24
|
generatedFileScanCount: number;
|
|
25
25
|
expensiveFileCount: number;
|
|
26
|
+
highConfidenceMissingEvidenceCount: number;
|
|
26
27
|
sessionRepeatedReadCount: number;
|
|
27
28
|
sessionEstimatedReadTokens: number;
|
|
28
29
|
sessionEstimatedRepeatedReadTokens: number;
|
|
@@ -29,6 +29,7 @@ export async function analyzeCognitionQuality(workspace, maps) {
|
|
|
29
29
|
duplicateTitleCount: countDuplicateAtomTopics(activeAtoms),
|
|
30
30
|
generatedFileScanCount: countGeneratedScannedFiles(maps.fileMap),
|
|
31
31
|
expensiveFileCount: countExpensiveFiles(maps.fileMap),
|
|
32
|
+
highConfidenceMissingEvidenceCount: countHighConfidenceMissingEvidence(activeAtoms),
|
|
32
33
|
sessionRepeatedReadCount: session.repeatedReadCount,
|
|
33
34
|
sessionEstimatedReadTokens: session.estimatedReadTokens,
|
|
34
35
|
sessionEstimatedRepeatedReadTokens: session.estimatedRepeatedReadTokens,
|
|
@@ -36,6 +37,9 @@ export async function analyzeCognitionQuality(workspace, maps) {
|
|
|
36
37
|
changes,
|
|
37
38
|
};
|
|
38
39
|
}
|
|
40
|
+
function countHighConfidenceMissingEvidence(atoms) {
|
|
41
|
+
return atoms.filter((atom) => atom.confidence === 'high' && atom.evidenceRefs.length === 0).length;
|
|
42
|
+
}
|
|
39
43
|
export async function repairCognition(workspace, maps, dryRun = false) {
|
|
40
44
|
const refreshed = await refreshKnowledgeAtomStatuses(workspace, { fileMap: maps.fileMap, symbolMap: maps.symbolMap }, dryRun);
|
|
41
45
|
const atoms = refreshed.atoms;
|
|
@@ -135,6 +139,7 @@ export async function repairCognition(workspace, maps, dryRun = false) {
|
|
|
135
139
|
duplicateTitleCount: countDuplicateTitles(nextNotes),
|
|
136
140
|
generatedFileScanCount: countGeneratedScannedFiles(maps.fileMap),
|
|
137
141
|
expensiveFileCount: countExpensiveFiles(maps.fileMap),
|
|
142
|
+
highConfidenceMissingEvidenceCount: countHighConfidenceMissingEvidence(atoms),
|
|
138
143
|
sessionRepeatedReadCount: session.repeatedReadCount,
|
|
139
144
|
sessionEstimatedReadTokens: session.estimatedReadTokens,
|
|
140
145
|
sessionEstimatedRepeatedReadTokens: session.estimatedRepeatedReadTokens,
|
|
@@ -2,6 +2,7 @@ import { readMaps } from '../storage/map-store.js';
|
|
|
2
2
|
import { slugify, writeCognitionNote, writeDomainRecord } from '../storage/cognition-store.js';
|
|
3
3
|
import { KGraphError } from '../cli/errors.js';
|
|
4
4
|
import { createKnowledgeAtom } from '../knowledge/atom-store.js';
|
|
5
|
+
import { getWorkingTreeChanges } from '../scanner/git-utils.js';
|
|
5
6
|
import { evaluateReferenceStatus } from './cognition-updater.js';
|
|
6
7
|
import { readSessionState } from '../session/session-store.js';
|
|
7
8
|
export async function concludeTopic(workspace, input) {
|
|
@@ -10,6 +11,10 @@ export async function concludeTopic(workspace, input) {
|
|
|
10
11
|
const timestamp = now.replace(/[:.]/g, '-');
|
|
11
12
|
const title = input.topic.trim();
|
|
12
13
|
const summary = normalizeBody(input.body) ?? title;
|
|
14
|
+
const relatedFiles = input.relatedFiles && input.relatedFiles.length > 0
|
|
15
|
+
? input.relatedFiles
|
|
16
|
+
: await inferChangedFiles(workspace, maps);
|
|
17
|
+
const relatedSymbols = input.relatedSymbols ?? [];
|
|
13
18
|
const note = {
|
|
14
19
|
title,
|
|
15
20
|
kind: input.kind ?? 'summary',
|
|
@@ -19,18 +24,18 @@ export async function concludeTopic(workspace, input) {
|
|
|
19
24
|
summary,
|
|
20
25
|
sections: {
|
|
21
26
|
Summary: summary,
|
|
22
|
-
...(
|
|
23
|
-
...(
|
|
27
|
+
...(relatedFiles.length ? { 'Related Files': relatedFiles.map((file) => `- ${file}`).join('\n') } : {}),
|
|
28
|
+
...(relatedSymbols.length ? { 'Key Symbols': relatedSymbols.map((symbol) => `- \`${symbol}\``).join('\n') } : {}),
|
|
24
29
|
},
|
|
25
|
-
relatedFiles
|
|
26
|
-
relatedSymbols
|
|
30
|
+
relatedFiles,
|
|
31
|
+
relatedSymbols,
|
|
27
32
|
warnings: [],
|
|
28
33
|
id: `${timestamp}-${slugify(title) || 'conclusion'}`,
|
|
29
34
|
sourceInboxPath: '',
|
|
30
35
|
processedPath: `.kgraph/cognition/${timestamp}-${slugify(title) || 'conclusion'}.md`,
|
|
31
36
|
createdAt: now,
|
|
32
37
|
source: input.source,
|
|
33
|
-
referencesStatus: evaluateReferenceStatus(
|
|
38
|
+
referencesStatus: evaluateReferenceStatus(relatedFiles, relatedSymbols, { files: maps.fileMap.files, symbols: maps.symbolMap.symbols }),
|
|
34
39
|
};
|
|
35
40
|
await writeCognitionNote(workspace, note);
|
|
36
41
|
await writeDomainRecord(workspace, toDomainRecord(note, {
|
|
@@ -43,8 +48,8 @@ export async function concludeTopic(workspace, input) {
|
|
|
43
48
|
claim: note.summary ?? note.title,
|
|
44
49
|
summary: note.summary,
|
|
45
50
|
confidence: note.confidence,
|
|
46
|
-
files:
|
|
47
|
-
symbols:
|
|
51
|
+
files: relatedFiles,
|
|
52
|
+
symbols: relatedSymbols,
|
|
48
53
|
domains: note.domain ? [note.domain] : [],
|
|
49
54
|
sourceCommand: input.source === 'session-conclude'
|
|
50
55
|
? 'session-conclude'
|
|
@@ -58,6 +63,13 @@ export async function concludeTopic(workspace, input) {
|
|
|
58
63
|
}, maps);
|
|
59
64
|
return note;
|
|
60
65
|
}
|
|
66
|
+
async function inferChangedFiles(workspace, maps) {
|
|
67
|
+
const changed = await getWorkingTreeChanges(workspace.rootPath);
|
|
68
|
+
if (changed.length === 0)
|
|
69
|
+
return [];
|
|
70
|
+
const currentFiles = new Set(maps.fileMap.files.map((file) => file.path));
|
|
71
|
+
return changed.filter((file) => currentFiles.has(file));
|
|
72
|
+
}
|
|
61
73
|
export async function concludeActiveSession(workspace, agent, input) {
|
|
62
74
|
return concludeTopic(workspace, await buildActiveSessionConclusion(workspace, agent, input));
|
|
63
75
|
}
|
|
@@ -1,3 +1,3 @@
|
|
|
1
1
|
import type { ContextResponse } from '../types/cognition.js';
|
|
2
2
|
import type { ContextPack } from '../types/knowledge.js';
|
|
3
|
-
export declare function buildContextPack(response: ContextResponse, budget: number): ContextPack;
|
|
3
|
+
export declare function buildContextPack(response: ContextResponse, budget: number, rootPath?: string): ContextPack;
|
|
@@ -1,5 +1,8 @@
|
|
|
1
|
+
import { existsSync, readFileSync } from 'node:fs';
|
|
2
|
+
import path from 'node:path';
|
|
1
3
|
import { estimateTokens } from '../session/token-estimator.js';
|
|
2
|
-
|
|
4
|
+
import { tokenize } from './ranking.js';
|
|
5
|
+
export function buildContextPack(response, budget, rootPath) {
|
|
3
6
|
const candidates = [
|
|
4
7
|
...response.relevantFiles.map((ranked) => ({
|
|
5
8
|
kind: 'file',
|
|
@@ -9,6 +12,7 @@ export function buildContextPack(response, budget) {
|
|
|
9
12
|
reasons: ranked.reasons,
|
|
10
13
|
data: ranked.item,
|
|
11
14
|
})),
|
|
15
|
+
...buildFileRangeCandidates(response, budget, rootPath),
|
|
12
16
|
...response.relevantSymbols.map((ranked) => ({
|
|
13
17
|
kind: 'symbol',
|
|
14
18
|
id: ranked.item.id,
|
|
@@ -48,10 +52,11 @@ export function buildContextPack(response, budget) {
|
|
|
48
52
|
data: change,
|
|
49
53
|
})),
|
|
50
54
|
];
|
|
55
|
+
const orderedCandidates = candidates.sort(comparePackCandidates);
|
|
51
56
|
const items = [];
|
|
52
57
|
const omitted = [];
|
|
53
58
|
let usedTokens = 0;
|
|
54
|
-
for (const candidate of
|
|
59
|
+
for (const candidate of orderedCandidates) {
|
|
55
60
|
if (usedTokens + candidate.tokenEstimate <= budget) {
|
|
56
61
|
items.push(candidate);
|
|
57
62
|
usedTokens += candidate.tokenEstimate;
|
|
@@ -69,3 +74,132 @@ export function buildContextPack(response, budget) {
|
|
|
69
74
|
warnings: response.warnings,
|
|
70
75
|
};
|
|
71
76
|
}
|
|
77
|
+
function comparePackCandidates(left, right) {
|
|
78
|
+
return packPriority(right) - packPriority(left);
|
|
79
|
+
}
|
|
80
|
+
function packPriority(item) {
|
|
81
|
+
let score = 0;
|
|
82
|
+
if (item.kind === 'atom')
|
|
83
|
+
score += 1000;
|
|
84
|
+
if (item.kind === 'git-change')
|
|
85
|
+
score += 900;
|
|
86
|
+
if (item.kind === 'file-range')
|
|
87
|
+
score += 800;
|
|
88
|
+
if (item.kind === 'symbol')
|
|
89
|
+
score += 300;
|
|
90
|
+
if (item.kind === 'file')
|
|
91
|
+
score += 200;
|
|
92
|
+
if (item.kind === 'relationship')
|
|
93
|
+
score += 100;
|
|
94
|
+
if (item.reasons.some((reason) => reason.includes('matched atom')))
|
|
95
|
+
score += 30;
|
|
96
|
+
if (item.reasons.some((reason) => reason.includes('current git change')))
|
|
97
|
+
score += 25;
|
|
98
|
+
if (item.reasons.some((reason) => reason.includes('specific query token')))
|
|
99
|
+
score += 10;
|
|
100
|
+
if (item.reasons.some((reason) => reason.includes('generic path-only match penalty')))
|
|
101
|
+
score -= 20;
|
|
102
|
+
score -= Math.floor(item.tokenEstimate / 2000);
|
|
103
|
+
return score;
|
|
104
|
+
}
|
|
105
|
+
const GENERIC_RANGE_TOKENS = new Set([
|
|
106
|
+
'app',
|
|
107
|
+
'code',
|
|
108
|
+
'component',
|
|
109
|
+
'file',
|
|
110
|
+
'page',
|
|
111
|
+
'repo',
|
|
112
|
+
'work',
|
|
113
|
+
]);
|
|
114
|
+
function buildFileRangeCandidates(response, budget, rootPath) {
|
|
115
|
+
if (!rootPath)
|
|
116
|
+
return [];
|
|
117
|
+
const queryTokens = tokenize(response.query).filter((token) => token.length >= 3 && !GENERIC_RANGE_TOKENS.has(token));
|
|
118
|
+
if (queryTokens.length === 0)
|
|
119
|
+
return [];
|
|
120
|
+
const maxRangeTokens = Math.max(250, Math.min(1200, Math.floor(budget / 3)));
|
|
121
|
+
const candidates = [];
|
|
122
|
+
for (const rankedFile of response.relevantFiles.slice(0, 8)) {
|
|
123
|
+
const file = rankedFile.item;
|
|
124
|
+
const fileTokens = file.tokenEstimate ?? 0;
|
|
125
|
+
if (fileTokens <= Math.max(1000, Math.floor(budget / 2)))
|
|
126
|
+
continue;
|
|
127
|
+
const fullPath = path.join(rootPath, file.path);
|
|
128
|
+
if (!existsSync(fullPath))
|
|
129
|
+
continue;
|
|
130
|
+
let content = '';
|
|
131
|
+
try {
|
|
132
|
+
content = readFileSync(fullPath, 'utf8');
|
|
133
|
+
}
|
|
134
|
+
catch {
|
|
135
|
+
continue;
|
|
136
|
+
}
|
|
137
|
+
const ranges = selectQueryRanges(content, queryTokens, maxRangeTokens, file.path);
|
|
138
|
+
for (const range of ranges) {
|
|
139
|
+
const lines = content.split(/\r?\n/).slice(range.start - 1, range.end);
|
|
140
|
+
const excerpt = lines.join('\n');
|
|
141
|
+
candidates.push({
|
|
142
|
+
kind: 'file-range',
|
|
143
|
+
id: `${file.path}:${range.start}-${range.end}`,
|
|
144
|
+
title: `${file.path}:${range.start}-${range.end}`,
|
|
145
|
+
tokenEstimate: estimateTokens(excerpt, file.path),
|
|
146
|
+
reasons: [
|
|
147
|
+
...rankedFile.reasons,
|
|
148
|
+
`range selected from oversized file`,
|
|
149
|
+
`line text matched ${range.tokens.map((token) => `"${token}"`).join(', ')}`,
|
|
150
|
+
],
|
|
151
|
+
data: {
|
|
152
|
+
path: file.path,
|
|
153
|
+
startLine: range.start,
|
|
154
|
+
endLine: range.end,
|
|
155
|
+
excerpt,
|
|
156
|
+
},
|
|
157
|
+
});
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
return candidates;
|
|
161
|
+
}
|
|
162
|
+
function selectQueryRanges(content, queryTokens, maxRangeTokens, filePath) {
|
|
163
|
+
const lines = content.split(/\r?\n/);
|
|
164
|
+
const hits = [];
|
|
165
|
+
for (const [index, line] of lines.entries()) {
|
|
166
|
+
const lower = line.toLowerCase();
|
|
167
|
+
const matched = queryTokens.filter((token) => lower.includes(token));
|
|
168
|
+
if (matched.length === 0)
|
|
169
|
+
continue;
|
|
170
|
+
hits.push({
|
|
171
|
+
start: Math.max(1, index + 1 - 8),
|
|
172
|
+
end: Math.min(lines.length, index + 1 + 8),
|
|
173
|
+
tokens: matched,
|
|
174
|
+
});
|
|
175
|
+
}
|
|
176
|
+
const ranges = mergeRanges(hits);
|
|
177
|
+
return ranges
|
|
178
|
+
.sort((left, right) => right.tokens.length - left.tokens.length)
|
|
179
|
+
.slice(0, 3)
|
|
180
|
+
.map((range) => trimRangeToBudget(range, lines, maxRangeTokens, filePath));
|
|
181
|
+
}
|
|
182
|
+
function mergeRanges(ranges) {
|
|
183
|
+
const merged = [];
|
|
184
|
+
for (const range of ranges.sort((left, right) => left.start - right.start)) {
|
|
185
|
+
const current = merged.at(-1);
|
|
186
|
+
if (!current || range.start > current.end + 3) {
|
|
187
|
+
merged.push({ ...range, tokens: [...new Set(range.tokens)] });
|
|
188
|
+
continue;
|
|
189
|
+
}
|
|
190
|
+
current.end = Math.max(current.end, range.end);
|
|
191
|
+
current.tokens = [...new Set([...current.tokens, ...range.tokens])];
|
|
192
|
+
}
|
|
193
|
+
return merged;
|
|
194
|
+
}
|
|
195
|
+
function trimRangeToBudget(range, lines, maxRangeTokens, filePath) {
|
|
196
|
+
let start = range.start;
|
|
197
|
+
let end = Math.min(range.end, start + 79);
|
|
198
|
+
while (end > start + 4) {
|
|
199
|
+
const excerpt = lines.slice(start - 1, end).join('\n');
|
|
200
|
+
if (estimateTokens(excerpt, filePath) <= maxRangeTokens)
|
|
201
|
+
break;
|
|
202
|
+
end -= 5;
|
|
203
|
+
}
|
|
204
|
+
return { ...range, start, end };
|
|
205
|
+
}
|
|
@@ -3,6 +3,7 @@ import { readDomainRecords } from '../storage/cognition-store.js';
|
|
|
3
3
|
import { readSessionState } from '../session/session-store.js';
|
|
4
4
|
import { atomToCognitionNote, refreshKnowledgeAtomStatuses, } from '../knowledge/atom-store.js';
|
|
5
5
|
import { rankByFields } from './ranking.js';
|
|
6
|
+
import { tokenize } from './ranking.js';
|
|
6
7
|
export async function queryContext(workspace, config, maps, query) {
|
|
7
8
|
const refreshedAtoms = await refreshKnowledgeAtomStatuses(workspace, {
|
|
8
9
|
fileMap: maps.fileMap,
|
|
@@ -18,27 +19,43 @@ export async function queryContext(workspace, config, maps, query) {
|
|
|
18
19
|
.map((event) => event.path)
|
|
19
20
|
.filter((path) => Boolean(path)));
|
|
20
21
|
const max = config.maxContextItems;
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
]
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
22
|
+
// Collect git changes before file ranking so dirty files can influence ranking,
|
|
23
|
+
// not just appear later as a low-token pack item.
|
|
24
|
+
const knownFilePaths = new Set(maps.fileMap.files.map((f) => f.path));
|
|
25
|
+
const gitChanges = [];
|
|
26
|
+
if (await isGitRepo(workspace.rootPath)) {
|
|
27
|
+
const workingTreeChanges = await getWorkingTreeChangesDetailed(workspace.rootPath);
|
|
28
|
+
for (const change of workingTreeChanges) {
|
|
29
|
+
if (!knownFilePaths.has(change.path))
|
|
30
|
+
continue;
|
|
31
|
+
const status = change.staged && !change.unstaged
|
|
32
|
+
? 'staged'
|
|
33
|
+
: change.unstaged && !change.staged
|
|
34
|
+
? 'unstaged'
|
|
35
|
+
: 'staged'; // both staged and unstaged -> report as staged
|
|
36
|
+
gitChanges.push({
|
|
37
|
+
path: change.path,
|
|
38
|
+
status,
|
|
39
|
+
reason: change.staged && change.unstaged
|
|
40
|
+
? 'partially staged'
|
|
41
|
+
: status === 'staged'
|
|
42
|
+
? 'staged change'
|
|
43
|
+
: 'unstaged change',
|
|
44
|
+
});
|
|
45
|
+
}
|
|
46
|
+
const committedPaths = new Set(gitChanges.map((c) => c.path));
|
|
47
|
+
const recentCommitted = await getRecentlyCommittedFiles(workspace.rootPath);
|
|
48
|
+
for (const filePath of recentCommitted) {
|
|
49
|
+
if (!knownFilePaths.has(filePath) || committedPaths.has(filePath))
|
|
50
|
+
continue;
|
|
51
|
+
gitChanges.push({
|
|
52
|
+
path: filePath,
|
|
53
|
+
status: 'recent-commit',
|
|
54
|
+
reason: 'changed in recent commits',
|
|
55
|
+
});
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
const gitChangedPaths = new Set(gitChanges.map((change) => change.path));
|
|
42
59
|
const relevantCognition = rankByFields(query, atoms.filter((atom) => atom.status !== 'archived'), [
|
|
43
60
|
{ name: 'topic', value: (atom) => atom.topic },
|
|
44
61
|
{ name: 'claim', value: (atom) => atom.claim },
|
|
@@ -58,11 +75,38 @@ export async function queryContext(workspace, config, maps, query) {
|
|
|
58
75
|
}))
|
|
59
76
|
.sort((a, b) => b.score - a.score)
|
|
60
77
|
.slice(0, max);
|
|
78
|
+
const atomLinkedFiles = new Map();
|
|
79
|
+
for (const ranked of relevantCognition) {
|
|
80
|
+
for (const fp of ranked.item.relatedFiles) {
|
|
81
|
+
atomLinkedFiles.set(fp, [
|
|
82
|
+
...(atomLinkedFiles.get(fp) ?? []),
|
|
83
|
+
`referenced by matched atom "${ranked.item.title}"`,
|
|
84
|
+
]);
|
|
85
|
+
}
|
|
86
|
+
}
|
|
61
87
|
const matchedDomains = rankByFields(query, domains, [
|
|
62
88
|
{ name: 'name', value: (domain) => domain.name },
|
|
63
89
|
{ name: 'tags', value: (domain) => domain.tags },
|
|
64
90
|
{ name: 'path', value: (domain) => domain.pathHints },
|
|
65
91
|
]).slice(0, max);
|
|
92
|
+
let relevantFiles = rankByFields(query, maps.fileMap.files, [
|
|
93
|
+
{ name: 'path', value: (file) => file.path },
|
|
94
|
+
{ name: 'language', value: (file) => file.language },
|
|
95
|
+
])
|
|
96
|
+
.map((ranked) => applyFileRankAdjustments(ranked, {
|
|
97
|
+
query,
|
|
98
|
+
atomLinkedFiles,
|
|
99
|
+
gitChangedPaths,
|
|
100
|
+
sessionTouchedPaths,
|
|
101
|
+
}))
|
|
102
|
+
.sort((a, b) => b.score - a.score)
|
|
103
|
+
.slice(0, max);
|
|
104
|
+
const relevantSymbols = rankByFields(query, maps.symbolMap.symbols, [
|
|
105
|
+
{ name: 'name', value: (symbol) => symbol.name },
|
|
106
|
+
{ name: 'path', value: (symbol) => symbol.filePath },
|
|
107
|
+
{ name: 'kind', value: (symbol) => symbol.kind },
|
|
108
|
+
{ name: 'parent', value: (symbol) => symbol.parentName },
|
|
109
|
+
]).slice(0, max);
|
|
66
110
|
// Inject files linked by matched cognition notes/domains that didn't score on name alone
|
|
67
111
|
const rankedFilePaths = new Set(relevantFiles.map((f) => f.item.path));
|
|
68
112
|
const cognitionLinkedMap = new Map();
|
|
@@ -110,10 +154,10 @@ export async function queryContext(workspace, config, maps, query) {
|
|
|
110
154
|
.filter((f) => cognitionLinkedMap.has(f.path))
|
|
111
155
|
.map((f) => ({
|
|
112
156
|
item: f,
|
|
113
|
-
score:
|
|
157
|
+
score: 12,
|
|
114
158
|
reasons: cognitionLinkedMap.get(f.path),
|
|
115
159
|
})),
|
|
116
|
-
];
|
|
160
|
+
].sort((a, b) => b.score - a.score);
|
|
117
161
|
const relatedIds = new Set([
|
|
118
162
|
...relevantFiles.map((file) => file.item.path),
|
|
119
163
|
...relevantSymbols.map((symbol) => symbol.item.id),
|
|
@@ -207,41 +251,6 @@ export async function queryContext(workspace, config, maps, query) {
|
|
|
207
251
|
...dependenciesForImportedSymbol(symbol, maps.dependencyMap.dependencies),
|
|
208
252
|
],
|
|
209
253
|
}));
|
|
210
|
-
// Collect git changes: working-tree and recently committed files known to KGraph
|
|
211
|
-
const knownFilePaths = new Set(maps.fileMap.files.map((f) => f.path));
|
|
212
|
-
const gitChanges = [];
|
|
213
|
-
if (await isGitRepo(workspace.rootPath)) {
|
|
214
|
-
const workingTreeChanges = await getWorkingTreeChangesDetailed(workspace.rootPath);
|
|
215
|
-
for (const change of workingTreeChanges) {
|
|
216
|
-
if (!knownFilePaths.has(change.path))
|
|
217
|
-
continue;
|
|
218
|
-
const status = change.staged && !change.unstaged
|
|
219
|
-
? 'staged'
|
|
220
|
-
: change.unstaged && !change.staged
|
|
221
|
-
? 'unstaged'
|
|
222
|
-
: 'staged'; // both staged and unstaged → report as staged
|
|
223
|
-
gitChanges.push({
|
|
224
|
-
path: change.path,
|
|
225
|
-
status,
|
|
226
|
-
reason: change.staged && change.unstaged
|
|
227
|
-
? 'partially staged'
|
|
228
|
-
: status === 'staged'
|
|
229
|
-
? 'staged change'
|
|
230
|
-
: 'unstaged change',
|
|
231
|
-
});
|
|
232
|
-
}
|
|
233
|
-
const committedPaths = new Set(gitChanges.map((c) => c.path));
|
|
234
|
-
const recentCommitted = await getRecentlyCommittedFiles(workspace.rootPath);
|
|
235
|
-
for (const filePath of recentCommitted) {
|
|
236
|
-
if (!knownFilePaths.has(filePath) || committedPaths.has(filePath))
|
|
237
|
-
continue;
|
|
238
|
-
gitChanges.push({
|
|
239
|
-
path: filePath,
|
|
240
|
-
status: 'recent-commit',
|
|
241
|
-
reason: 'changed in recent commits',
|
|
242
|
-
});
|
|
243
|
-
}
|
|
244
|
-
}
|
|
245
254
|
return {
|
|
246
255
|
query,
|
|
247
256
|
matchedDomains,
|
|
@@ -257,6 +266,36 @@ export async function queryContext(workspace, config, maps, query) {
|
|
|
257
266
|
warnings: [],
|
|
258
267
|
};
|
|
259
268
|
}
|
|
269
|
+
function applyFileRankAdjustments(ranked, context) {
|
|
270
|
+
const reasons = [...ranked.reasons];
|
|
271
|
+
let score = ranked.score - Math.floor((ranked.item.tokenEstimate ?? 0) / 2000);
|
|
272
|
+
if (context.sessionTouchedPaths.has(ranked.item.path)) {
|
|
273
|
+
score += 3;
|
|
274
|
+
reasons.push('touched in current session');
|
|
275
|
+
}
|
|
276
|
+
if (context.gitChangedPaths.has(ranked.item.path)) {
|
|
277
|
+
score += 10;
|
|
278
|
+
reasons.push('current git change');
|
|
279
|
+
}
|
|
280
|
+
const atomReasons = context.atomLinkedFiles.get(ranked.item.path) ?? [];
|
|
281
|
+
if (atomReasons.length > 0) {
|
|
282
|
+
score += 12;
|
|
283
|
+
reasons.push(...atomReasons);
|
|
284
|
+
}
|
|
285
|
+
const strongTokens = tokenize(context.query).filter((token) => token.length >= 4 &&
|
|
286
|
+
!['page', 'work', 'file', 'component', 'app'].includes(token));
|
|
287
|
+
const pathTokens = new Set(tokenize(ranked.item.path));
|
|
288
|
+
const strongMatches = strongTokens.filter((token) => pathTokens.has(token));
|
|
289
|
+
if (strongMatches.length > 0) {
|
|
290
|
+
score += strongMatches.length * 3;
|
|
291
|
+
reasons.push(`path matched specific query token(s): ${strongMatches.join(', ')}`);
|
|
292
|
+
}
|
|
293
|
+
else if (strongTokens.length > 0) {
|
|
294
|
+
score -= 6;
|
|
295
|
+
reasons.push('generic path-only match penalty');
|
|
296
|
+
}
|
|
297
|
+
return { ...ranked, score, reasons };
|
|
298
|
+
}
|
|
260
299
|
function explainRelationships(relationships, context) {
|
|
261
300
|
const rankedReasons = new Map(context.rankedRelationships.map((ranked) => [
|
|
262
301
|
relationshipKey(ranked.item),
|
|
@@ -75,7 +75,7 @@ export interface KnowledgeValidationIssue {
|
|
|
75
75
|
atomId?: string;
|
|
76
76
|
}
|
|
77
77
|
export interface ContextPackItem {
|
|
78
|
-
kind: 'file' | 'symbol' | 'atom' | 'relationship' | 'git-change';
|
|
78
|
+
kind: 'file' | 'file-range' | 'symbol' | 'atom' | 'relationship' | 'git-change';
|
|
79
79
|
id: string;
|
|
80
80
|
title: string;
|
|
81
81
|
tokenEstimate: number;
|