@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
- export function buildContextPack(response, budget) {
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 += 40;
83
+ score += 1000;
80
84
  if (item.kind === 'git-change')
81
- score += 35;
85
+ score += 900;
86
+ if (item.kind === 'file-range')
87
+ score += 800;
82
88
  if (item.kind === 'symbol')
83
- score += 25;
89
+ score += 300;
84
90
  if (item.kind === 'file')
85
- score += 15;
91
+ score += 200;
86
92
  if (item.kind === 'relationship')
87
- score += 5;
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;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@kentwynn/kgraph",
3
- "version": "0.2.14",
3
+ "version": "0.2.15",
4
4
  "description": "Persistent repo intelligence for AI coding assistants.",
5
5
  "type": "module",
6
6
  "bin": {