@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.
- package/dist/cli/commands/doctor.js +4 -0
- 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.js +28 -1
- package/dist/context/context-query.js +97 -58
- 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) {
|
|
@@ -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
|
}
|
|
@@ -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
|
|
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
|
-
|
|
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),
|