@kentwynn/kgraph 0.2.13 → 0.2.14

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.
@@ -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) {
@@ -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
- ...(input.relatedFiles?.length ? { 'Related Files': input.relatedFiles.map((file) => `- ${file}`).join('\n') } : {}),
23
- ...(input.relatedSymbols?.length ? { 'Key Symbols': input.relatedSymbols.map((symbol) => `- \`${symbol}\``).join('\n') } : {}),
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: input.relatedFiles ?? [],
26
- relatedSymbols: input.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(input.relatedFiles ?? [], input.relatedSymbols ?? [], { files: maps.fileMap.files, symbols: maps.symbolMap.symbols }),
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: note.relatedFiles,
47
- symbols: note.relatedSymbols,
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
  }
@@ -48,10 +48,11 @@ export function buildContextPack(response, budget) {
48
48
  data: change,
49
49
  })),
50
50
  ];
51
+ const orderedCandidates = candidates.sort(comparePackCandidates);
51
52
  const items = [];
52
53
  const omitted = [];
53
54
  let usedTokens = 0;
54
- for (const candidate of candidates) {
55
+ for (const candidate of orderedCandidates) {
55
56
  if (usedTokens + candidate.tokenEstimate <= budget) {
56
57
  items.push(candidate);
57
58
  usedTokens += candidate.tokenEstimate;
@@ -69,3 +70,29 @@ export function buildContextPack(response, budget) {
69
70
  warnings: response.warnings,
70
71
  };
71
72
  }
73
+ function comparePackCandidates(left, right) {
74
+ return packPriority(right) - packPriority(left);
75
+ }
76
+ function packPriority(item) {
77
+ let score = 0;
78
+ if (item.kind === 'atom')
79
+ score += 40;
80
+ if (item.kind === 'git-change')
81
+ score += 35;
82
+ if (item.kind === 'symbol')
83
+ score += 25;
84
+ if (item.kind === 'file')
85
+ score += 15;
86
+ if (item.kind === 'relationship')
87
+ score += 5;
88
+ if (item.reasons.some((reason) => reason.includes('matched atom')))
89
+ score += 30;
90
+ if (item.reasons.some((reason) => reason.includes('current git change')))
91
+ score += 25;
92
+ if (item.reasons.some((reason) => reason.includes('specific query token')))
93
+ score += 10;
94
+ if (item.reasons.some((reason) => reason.includes('generic path-only match penalty')))
95
+ score -= 20;
96
+ score -= Math.floor(item.tokenEstimate / 2000);
97
+ return score;
98
+ }
@@ -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
- let relevantFiles = rankByFields(query, maps.fileMap.files, [
22
- { name: 'path', value: (file) => file.path },
23
- { name: 'language', value: (file) => file.language },
24
- ])
25
- .map((ranked) => ({
26
- ...ranked,
27
- score: ranked.score -
28
- Math.floor((ranked.item.tokenEstimate ?? 0) / 2000) +
29
- (sessionTouchedPaths.has(ranked.item.path) ? 3 : 0),
30
- reasons: sessionTouchedPaths.has(ranked.item.path)
31
- ? [...ranked.reasons, 'touched in current session']
32
- : ranked.reasons,
33
- }))
34
- .sort((a, b) => b.score - a.score)
35
- .slice(0, max);
36
- const relevantSymbols = rankByFields(query, maps.symbolMap.symbols, [
37
- { name: 'name', value: (symbol) => symbol.name },
38
- { name: 'path', value: (symbol) => symbol.filePath },
39
- { name: 'kind', value: (symbol) => symbol.kind },
40
- { name: 'parent', value: (symbol) => symbol.parentName },
41
- ]).slice(0, max);
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: 1,
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),
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@kentwynn/kgraph",
3
- "version": "0.2.13",
3
+ "version": "0.2.14",
4
4
  "description": "Persistent repo intelligence for AI coding assistants.",
5
5
  "type": "module",
6
6
  "bin": {