@kentwynn/kgraph 0.2.14 → 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.
|
@@ -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;
|
|
@@ -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,
|
|
@@ -76,15 +80,17 @@ function comparePackCandidates(left, right) {
|
|
|
76
80
|
function packPriority(item) {
|
|
77
81
|
let score = 0;
|
|
78
82
|
if (item.kind === 'atom')
|
|
79
|
-
score +=
|
|
83
|
+
score += 1000;
|
|
80
84
|
if (item.kind === 'git-change')
|
|
81
|
-
score +=
|
|
85
|
+
score += 900;
|
|
86
|
+
if (item.kind === 'file-range')
|
|
87
|
+
score += 800;
|
|
82
88
|
if (item.kind === 'symbol')
|
|
83
|
-
score +=
|
|
89
|
+
score += 300;
|
|
84
90
|
if (item.kind === 'file')
|
|
85
|
-
score +=
|
|
91
|
+
score += 200;
|
|
86
92
|
if (item.kind === 'relationship')
|
|
87
|
-
score +=
|
|
93
|
+
score += 100;
|
|
88
94
|
if (item.reasons.some((reason) => reason.includes('matched atom')))
|
|
89
95
|
score += 30;
|
|
90
96
|
if (item.reasons.some((reason) => reason.includes('current git change')))
|
|
@@ -96,3 +102,104 @@ function packPriority(item) {
|
|
|
96
102
|
score -= Math.floor(item.tokenEstimate / 2000);
|
|
97
103
|
return score;
|
|
98
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
|
+
}
|
|
@@ -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;
|