@kentwynn/kgraph 0.2.10 → 0.2.12
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/README.md +59 -29
- package/dist/cli/commands/blame.d.ts +2 -0
- package/dist/cli/commands/blame.js +52 -0
- package/dist/cli/commands/doctor.js +42 -12
- package/dist/cli/commands/impact.js +11 -5
- package/dist/cli/commands/init.js +2 -0
- package/dist/cli/commands/knowledge.d.ts +2 -0
- package/dist/cli/commands/knowledge.js +137 -0
- package/dist/cli/commands/pack.d.ts +2 -0
- package/dist/cli/commands/pack.js +49 -0
- package/dist/cli/commands/repair.js +3 -3
- package/dist/cli/commands/stale.d.ts +2 -0
- package/dist/cli/commands/stale.js +33 -0
- package/dist/cli/commands/visualize.js +7 -6
- package/dist/cli/help.js +17 -11
- package/dist/cli/index.js +8 -0
- package/dist/cognition/cognition-quality.d.ts +5 -0
- package/dist/cognition/cognition-quality.js +98 -5
- package/dist/cognition/cognition-updater.js +14 -0
- package/dist/cognition/compact.js +129 -28
- package/dist/cognition/conclusion.d.ts +2 -0
- package/dist/cognition/conclusion.js +22 -0
- package/dist/context/context-pack.d.ts +3 -0
- package/dist/context/context-pack.js +71 -0
- package/dist/context/context-query.js +53 -28
- package/dist/integrations/adapters/claude-code.js +23 -3
- package/dist/integrations/adapters/codex.js +1 -1
- package/dist/integrations/adapters/copilot.js +46 -3
- package/dist/integrations/workflow-steps.js +17 -8
- package/dist/knowledge/atom-store.d.ts +60 -0
- package/dist/knowledge/atom-store.js +484 -0
- package/dist/storage/kgraph-paths.js +5 -2
- package/dist/types/config.d.ts +1 -0
- package/dist/types/knowledge.d.ts +92 -0
- package/dist/types/knowledge.js +1 -0
- package/dist/visualization/graph-builder.d.ts +5 -2
- package/dist/visualization/graph-builder.js +43 -18
- package/dist/visualization/html-template.js +24 -17
- package/package.json +1 -1
|
@@ -6,8 +6,8 @@ import { printQualityReport } from './doctor.js';
|
|
|
6
6
|
export function registerRepairCommand(program) {
|
|
7
7
|
program
|
|
8
8
|
.command('repair')
|
|
9
|
-
.description('Clean noisy stale references from KGraph
|
|
10
|
-
.option('--dry-run', 'Show proposed
|
|
9
|
+
.description('Clean noisy stale references from KGraph knowledge atoms')
|
|
10
|
+
.option('--dry-run', 'Show proposed atom cleanup without writing files')
|
|
11
11
|
.action((options) => runCommand(async () => {
|
|
12
12
|
const workspace = await assertWorkspace(process.cwd());
|
|
13
13
|
if (!(await mapsExist(workspace))) {
|
|
@@ -21,7 +21,7 @@ export function registerRepairCommand(program) {
|
|
|
21
21
|
console.log('');
|
|
22
22
|
printQualityReport(report);
|
|
23
23
|
if (report.changes.length === 0) {
|
|
24
|
-
console.log('No noisy
|
|
24
|
+
console.log('No noisy atom references found.');
|
|
25
25
|
}
|
|
26
26
|
else if (options.dryRun) {
|
|
27
27
|
console.log('');
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import { refreshKnowledgeAtomStatuses } from '../../knowledge/atom-store.js';
|
|
2
|
+
import { readMaps } from '../../storage/map-store.js';
|
|
3
|
+
import { assertWorkspace } from '../../storage/kgraph-paths.js';
|
|
4
|
+
import { runCommand } from '../errors.js';
|
|
5
|
+
export function registerStaleCommand(program) {
|
|
6
|
+
program
|
|
7
|
+
.command('stale')
|
|
8
|
+
.description('Show knowledge atoms invalidated by changed or missing refs')
|
|
9
|
+
.option('--json', 'Print JSON output')
|
|
10
|
+
.action((options) => runCommand(async () => {
|
|
11
|
+
const workspace = await assertWorkspace(process.cwd());
|
|
12
|
+
const maps = await readMaps(workspace);
|
|
13
|
+
const result = await refreshKnowledgeAtomStatuses(workspace, {
|
|
14
|
+
fileMap: maps.fileMap,
|
|
15
|
+
symbolMap: maps.symbolMap,
|
|
16
|
+
});
|
|
17
|
+
const atoms = result.atoms.filter((atom) => atom.status === 'stale' || atom.status === 'needs-review');
|
|
18
|
+
if (options.json) {
|
|
19
|
+
console.log(JSON.stringify({ updated: result.updated, atoms }, null, 2));
|
|
20
|
+
return;
|
|
21
|
+
}
|
|
22
|
+
console.log('KGraph Stale Knowledge');
|
|
23
|
+
console.log('');
|
|
24
|
+
for (const atom of atoms) {
|
|
25
|
+
console.log(`- ${atom.id} [${atom.type}, ${atom.confidence}, ${atom.status}] ${atom.topic}`);
|
|
26
|
+
for (const reason of atom.lifecycle.invalidatedBy ?? []) {
|
|
27
|
+
console.log(` - ${reason}`);
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
if (atoms.length === 0)
|
|
31
|
+
console.log('- None');
|
|
32
|
+
}));
|
|
33
|
+
}
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { exec } from 'node:child_process';
|
|
2
2
|
import { createServer } from 'node:http';
|
|
3
3
|
import { loadConfig } from '../../config/config.js';
|
|
4
|
-
import {
|
|
4
|
+
import { refreshKnowledgeAtomStatuses } from '../../knowledge/atom-store.js';
|
|
5
5
|
import { assertWorkspace } from '../../storage/kgraph-paths.js';
|
|
6
6
|
import { mapsExist, readMaps } from '../../storage/map-store.js';
|
|
7
7
|
import { buildGraph } from '../../visualization/graph-builder.js';
|
|
@@ -22,12 +22,13 @@ export function registerVisualizeCommand(program) {
|
|
|
22
22
|
if (!(await mapsExist(workspace))) {
|
|
23
23
|
throw new KGraphError('KGraph maps are missing. Run `kgraph scan` first.');
|
|
24
24
|
}
|
|
25
|
-
const
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
25
|
+
const maps = await readMaps(workspace);
|
|
26
|
+
const { atoms } = await refreshKnowledgeAtomStatuses(workspace, {
|
|
27
|
+
fileMap: maps.fileMap,
|
|
28
|
+
symbolMap: maps.symbolMap,
|
|
29
|
+
});
|
|
29
30
|
await loadConfig(workspace); // ensure workspace is valid
|
|
30
|
-
const graphData = buildGraph(maps.fileMap, maps.symbolMap, maps.dependencyMap, maps.relationshipMap,
|
|
31
|
+
const graphData = buildGraph(maps.fileMap, maps.symbolMap, maps.dependencyMap, maps.relationshipMap, atoms);
|
|
31
32
|
const html = renderHtml(graphData, workspace.rootPath);
|
|
32
33
|
await serveGraph(html, port, options.open);
|
|
33
34
|
}));
|
package/dist/cli/help.js
CHANGED
|
@@ -22,7 +22,7 @@ export function renderRootHelp(useColor = supportsColor()) {
|
|
|
22
22
|
command('init --integrations codex,gemini', 'Initialize and connect AI tools'),
|
|
23
23
|
'',
|
|
24
24
|
theme.bold('Daily workflow'),
|
|
25
|
-
command('kgraph', 'Refresh scan maps and process pending
|
|
25
|
+
command('kgraph', 'Refresh scan maps and process pending capture notes'),
|
|
26
26
|
command('kgraph "auth token refresh"', 'Refresh everything and return compact context for a topic'),
|
|
27
27
|
'',
|
|
28
28
|
theme.bold('Workflows'),
|
|
@@ -30,15 +30,19 @@ export function renderRootHelp(useColor = supportsColor()) {
|
|
|
30
30
|
command('session', 'Show agent read/write activity and token estimates'),
|
|
31
31
|
command('session read src/auth.ts --agent codex', 'Record an agent file read'),
|
|
32
32
|
command('session end --agent codex --conclude', 'End tracking and store a durable session summary'),
|
|
33
|
-
command('conclude "auth refresh gotcha"', 'Store typed engineering
|
|
34
|
-
command('compact', 'Merge duplicate
|
|
33
|
+
command('conclude "auth refresh gotcha"', 'Store typed engineering knowledge'),
|
|
34
|
+
command('compact', 'Merge duplicate atoms and archive stale noise'),
|
|
35
|
+
command('knowledge list', 'Inspect canonical knowledge atoms'),
|
|
36
|
+
command('pack "auth task" --budget 8000', 'Build a budget-aware context pack'),
|
|
37
|
+
command('stale', 'Show atoms invalidated by changed or missing refs'),
|
|
38
|
+
command('blame <atom-id>', 'Show atom provenance and evidence'),
|
|
35
39
|
command('context "auth token refresh"', 'Optional: return context without scanning or updating'),
|
|
36
|
-
command('impact "Button"', 'Show imports, callers, calls,
|
|
37
|
-
command('update', 'Optional: process only .kgraph/inbox
|
|
40
|
+
command('impact "Button"', 'Show imports, callers, calls, knowledge, and risk'),
|
|
41
|
+
command('update', 'Optional: process only .kgraph/inbox capture notes'),
|
|
38
42
|
command('doctor', 'Check workspace health and next actions'),
|
|
39
|
-
command('doctor --quality', 'Report stale/noisy
|
|
40
|
-
command('repair --dry-run', 'Preview
|
|
41
|
-
command('repair', 'Clean noisy stale
|
|
43
|
+
command('doctor --quality', 'Report stale/noisy atom references'),
|
|
44
|
+
command('repair --dry-run', 'Preview atom reference cleanup'),
|
|
45
|
+
command('repair', 'Clean noisy stale atom references'),
|
|
42
46
|
command('uninstall', 'Preview repo-local KGraph removal'),
|
|
43
47
|
command('uninstall --yes', 'Remove .kgraph/ and managed integrations'),
|
|
44
48
|
command('visualize', 'Interactive dependency graph at http://localhost:4242'),
|
|
@@ -46,7 +50,7 @@ export function renderRootHelp(useColor = supportsColor()) {
|
|
|
46
50
|
'',
|
|
47
51
|
theme.bold('Integrations'),
|
|
48
52
|
command('integrate list', 'Show configured AI tool integrations'),
|
|
49
|
-
command('integrate add gemini windsurf cline', 'Write KGraph instructions using
|
|
53
|
+
command('integrate add gemini windsurf cline', 'Write KGraph instructions using smart mode by default'),
|
|
50
54
|
command('integrate add copilot --mode always', 'Every Copilot chat starts with kgraph "<topic>"'),
|
|
51
55
|
command('integrate set copilot --mode manual', 'Only run KGraph when explicitly requested'),
|
|
52
56
|
command('integrate remove cursor', 'Remove KGraph-managed instruction blocks'),
|
|
@@ -87,13 +91,15 @@ export function renderWorkflowBanner(stats, useColor = supportsColor()) {
|
|
|
87
91
|
? ` (${stats.skippedFiles} unchanged, skipped)`
|
|
88
92
|
: '')),
|
|
89
93
|
command('symbols', String(stats.symbols)),
|
|
90
|
-
command('
|
|
94
|
+
command('capture notes processed', String(stats.cognitionNotes)),
|
|
91
95
|
command('integration modes', integrationLine),
|
|
92
96
|
'',
|
|
93
97
|
theme.bold('Next'),
|
|
94
98
|
command('kgraph "auth token refresh"', 'Return compact context for a topic'),
|
|
95
99
|
command('kgraph doctor', 'Check workspace health'),
|
|
96
|
-
command('kgraph doctor --quality', 'Check
|
|
100
|
+
command('kgraph doctor --quality', 'Check atom quality'),
|
|
101
|
+
command('kgraph knowledge list', 'Inspect knowledge atoms'),
|
|
102
|
+
command('kgraph pack "auth task"', 'Build budget-aware context'),
|
|
97
103
|
command('kgraph session', 'Check session token waste'),
|
|
98
104
|
command('kgraph --help', 'Show all commands'),
|
|
99
105
|
].join('\n');
|
package/dist/cli/index.js
CHANGED
|
@@ -3,6 +3,7 @@ import { Command } from 'commander';
|
|
|
3
3
|
import { realpathSync } from 'node:fs';
|
|
4
4
|
import { createRequire } from 'node:module';
|
|
5
5
|
import { fileURLToPath } from 'node:url';
|
|
6
|
+
import { registerBlameCommand } from './commands/blame.js';
|
|
6
7
|
import { registerCompactCommand } from './commands/compact.js';
|
|
7
8
|
import { registerConcludeCommand } from './commands/conclude.js';
|
|
8
9
|
import { registerContextCommand } from './commands/context.js';
|
|
@@ -11,9 +12,12 @@ import { registerHistoryCommand } from './commands/history.js';
|
|
|
11
12
|
import { registerImpactCommand } from './commands/impact.js';
|
|
12
13
|
import { registerInitCommand } from './commands/init.js';
|
|
13
14
|
import { registerIntegrateCommand } from './commands/integrate.js';
|
|
15
|
+
import { registerKnowledgeCommand } from './commands/knowledge.js';
|
|
16
|
+
import { registerPackCommand } from './commands/pack.js';
|
|
14
17
|
import { registerRepairCommand } from './commands/repair.js';
|
|
15
18
|
import { registerScanCommand } from './commands/scan.js';
|
|
16
19
|
import { registerSessionCommand } from './commands/session.js';
|
|
20
|
+
import { registerStaleCommand } from './commands/stale.js';
|
|
17
21
|
import { registerUninstallCommand } from './commands/uninstall.js';
|
|
18
22
|
import { registerUpdateCommand } from './commands/update.js';
|
|
19
23
|
import { registerVisualizeCommand } from './commands/visualize.js';
|
|
@@ -46,6 +50,10 @@ export function createProgram() {
|
|
|
46
50
|
registerCompactCommand(program);
|
|
47
51
|
registerUpdateCommand(program);
|
|
48
52
|
registerContextCommand(program);
|
|
53
|
+
registerPackCommand(program);
|
|
54
|
+
registerKnowledgeCommand(program);
|
|
55
|
+
registerStaleCommand(program);
|
|
56
|
+
registerBlameCommand(program);
|
|
49
57
|
registerImpactCommand(program);
|
|
50
58
|
registerIntegrateCommand(program);
|
|
51
59
|
registerVisualizeCommand(program);
|
|
@@ -9,6 +9,11 @@ export interface CognitionRepairChange {
|
|
|
9
9
|
nextStatus: ReferenceStatus;
|
|
10
10
|
}
|
|
11
11
|
export interface CognitionQualityReport {
|
|
12
|
+
atomCount: number;
|
|
13
|
+
staleAtomCount: number;
|
|
14
|
+
needsReviewAtomCount: number;
|
|
15
|
+
archivedAtomCount: number;
|
|
16
|
+
duplicateAtomTopicCount: number;
|
|
12
17
|
noteCount: number;
|
|
13
18
|
mixedOrStaleCount: number;
|
|
14
19
|
noisyFileRefCount: number;
|
|
@@ -1,9 +1,13 @@
|
|
|
1
1
|
import { mkdir, rename } from 'node:fs/promises';
|
|
2
2
|
import path from 'node:path';
|
|
3
|
+
import { atomToCognitionNote, refreshKnowledgeAtomStatuses, writeKnowledgeAtoms, } from '../knowledge/atom-store.js';
|
|
3
4
|
import { buildSessionReport } from '../session/session-store.js';
|
|
4
|
-
import { overwriteDomainRecord,
|
|
5
|
+
import { overwriteDomainRecord, readDomainRecords, writeCognitionNote, } from '../storage/cognition-store.js';
|
|
5
6
|
export async function analyzeCognitionQuality(workspace, maps) {
|
|
6
|
-
const
|
|
7
|
+
const refreshed = await refreshKnowledgeAtomStatuses(workspace, { fileMap: maps.fileMap, symbolMap: maps.symbolMap }, true);
|
|
8
|
+
const atoms = refreshed.atoms;
|
|
9
|
+
const activeAtoms = atoms.filter((atom) => atom.status !== 'archived');
|
|
10
|
+
const notes = activeAtoms.map(atomToCognitionNote);
|
|
7
11
|
const session = await buildSessionReport(workspace);
|
|
8
12
|
const changes = notes
|
|
9
13
|
.map((note) => analyzeNote(note, maps))
|
|
@@ -11,13 +15,18 @@ export async function analyzeCognitionQuality(workspace, maps) {
|
|
|
11
15
|
change.removedSymbolRefs.length > 0);
|
|
12
16
|
const orphanedNoteCount = notes.filter((note) => note.referencesStatus === 'stale').length;
|
|
13
17
|
return {
|
|
18
|
+
atomCount: activeAtoms.length,
|
|
19
|
+
staleAtomCount: activeAtoms.filter((atom) => atom.status === 'stale').length,
|
|
20
|
+
needsReviewAtomCount: activeAtoms.filter((atom) => atom.status === 'needs-review').length,
|
|
21
|
+
archivedAtomCount: atoms.filter((atom) => atom.status === 'archived').length,
|
|
22
|
+
duplicateAtomTopicCount: countDuplicateAtomTopics(activeAtoms),
|
|
14
23
|
noteCount: notes.length,
|
|
15
24
|
mixedOrStaleCount: notes.filter((note) => ['mixed', 'stale', 'unresolved'].includes(note.referencesStatus)).length,
|
|
16
25
|
noisyFileRefCount: changes.reduce((total, change) => total + change.removedFileRefs.length, 0),
|
|
17
26
|
noisySymbolRefCount: changes.reduce((total, change) => total + change.removedSymbolRefs.length, 0),
|
|
18
27
|
unresolvedLocalImportCount: countUnresolvedLocalImports(maps.dependencyMap),
|
|
19
28
|
unresolvedCallCount: countUnresolvedCalls(maps.symbolMap, maps.relationshipMap),
|
|
20
|
-
duplicateTitleCount:
|
|
29
|
+
duplicateTitleCount: countDuplicateAtomTopics(activeAtoms),
|
|
21
30
|
generatedFileScanCount: countGeneratedScannedFiles(maps.fileMap),
|
|
22
31
|
expensiveFileCount: countExpensiveFiles(maps.fileMap),
|
|
23
32
|
sessionRepeatedReadCount: session.repeatedReadCount,
|
|
@@ -28,10 +37,14 @@ export async function analyzeCognitionQuality(workspace, maps) {
|
|
|
28
37
|
};
|
|
29
38
|
}
|
|
30
39
|
export async function repairCognition(workspace, maps, dryRun = false) {
|
|
31
|
-
const
|
|
40
|
+
const refreshed = await refreshKnowledgeAtomStatuses(workspace, { fileMap: maps.fileMap, symbolMap: maps.symbolMap }, dryRun);
|
|
41
|
+
const atoms = refreshed.atoms;
|
|
42
|
+
const activeAtoms = atoms.filter((atom) => atom.status !== 'archived');
|
|
43
|
+
const notes = activeAtoms.map(atomToCognitionNote);
|
|
32
44
|
const session = await buildSessionReport(workspace);
|
|
33
45
|
const nextNotes = [];
|
|
34
46
|
const changes = [];
|
|
47
|
+
const changesById = new Map();
|
|
35
48
|
for (const note of notes) {
|
|
36
49
|
const change = analyzeNote(note, maps);
|
|
37
50
|
const nextNote = applyChange(note, change);
|
|
@@ -39,6 +52,7 @@ export async function repairCognition(workspace, maps, dryRun = false) {
|
|
|
39
52
|
if (change.removedFileRefs.length > 0 ||
|
|
40
53
|
change.removedSymbolRefs.length > 0) {
|
|
41
54
|
changes.push(change);
|
|
55
|
+
changesById.set(change.noteId, change);
|
|
42
56
|
if (!dryRun) {
|
|
43
57
|
await writeCognitionNote(workspace, nextNote);
|
|
44
58
|
}
|
|
@@ -47,8 +61,58 @@ export async function repairCognition(workspace, maps, dryRun = false) {
|
|
|
47
61
|
// Archive fully-orphaned notes (all refs dead) so they no longer appear in context
|
|
48
62
|
const orphanedNotes = nextNotes.filter((note) => note.referencesStatus === 'stale');
|
|
49
63
|
if (!dryRun && (changes.length > 0 || orphanedNotes.length > 0)) {
|
|
64
|
+
const now = new Date().toISOString();
|
|
65
|
+
const nextAtoms = atoms.map((atom) => {
|
|
66
|
+
if (atom.status === 'archived')
|
|
67
|
+
return atom;
|
|
68
|
+
const change = changesById.get(atom.id);
|
|
69
|
+
if (!change && !orphanedNotes.some((note) => note.id === atom.id)) {
|
|
70
|
+
return atom;
|
|
71
|
+
}
|
|
72
|
+
if (orphanedNotes.some((note) => note.id === atom.id)) {
|
|
73
|
+
return {
|
|
74
|
+
...atom,
|
|
75
|
+
status: 'archived',
|
|
76
|
+
confidence: 'low',
|
|
77
|
+
lifecycle: { ...atom.lifecycle, archivedAt: now },
|
|
78
|
+
provenance: { ...atom.provenance, updatedAt: now },
|
|
79
|
+
};
|
|
80
|
+
}
|
|
81
|
+
const removedFiles = new Set(change?.removedFileRefs ?? []);
|
|
82
|
+
const removedSymbols = new Set(change?.removedSymbolRefs ?? []);
|
|
83
|
+
const nextStatus = atomStatusFromReferenceStatus(change?.nextStatus ?? 'current');
|
|
84
|
+
return {
|
|
85
|
+
...atom,
|
|
86
|
+
status: nextStatus,
|
|
87
|
+
confidence: atom.confidence === 'low' && atom.status === 'stale' && nextStatus !== 'stale'
|
|
88
|
+
? 'medium'
|
|
89
|
+
: atom.confidence,
|
|
90
|
+
scopeRefs: {
|
|
91
|
+
...atom.scopeRefs,
|
|
92
|
+
files: atom.scopeRefs.files.filter((file) => !removedFiles.has(file)),
|
|
93
|
+
symbols: atom.scopeRefs.symbols.filter((symbol) => !removedSymbols.has(symbol)),
|
|
94
|
+
},
|
|
95
|
+
evidenceRefs: atom.evidenceRefs.filter((ref) => {
|
|
96
|
+
if (ref.type === 'file')
|
|
97
|
+
return !removedFiles.has(ref.path);
|
|
98
|
+
if (ref.type === 'symbol')
|
|
99
|
+
return !removedSymbols.has(ref.name);
|
|
100
|
+
return true;
|
|
101
|
+
}),
|
|
102
|
+
lifecycle: {
|
|
103
|
+
...atom.lifecycle,
|
|
104
|
+
invalidatedBy: change?.nextStatus === 'current'
|
|
105
|
+
? undefined
|
|
106
|
+
: atom.lifecycle.invalidatedBy,
|
|
107
|
+
},
|
|
108
|
+
provenance: { ...atom.provenance, updatedAt: now },
|
|
109
|
+
};
|
|
110
|
+
});
|
|
111
|
+
await writeKnowledgeAtoms(workspace, nextAtoms);
|
|
50
112
|
// Exclude fully-orphaned notes from domain records — they are being archived
|
|
51
|
-
await repairDomainRecords(workspace,
|
|
113
|
+
await repairDomainRecords(workspace, nextAtoms
|
|
114
|
+
.filter((atom) => atom.status !== 'archived')
|
|
115
|
+
.map(atomToCognitionNote), maps);
|
|
52
116
|
}
|
|
53
117
|
if (!dryRun) {
|
|
54
118
|
for (const note of orphanedNotes) {
|
|
@@ -57,6 +121,11 @@ export async function repairCognition(workspace, maps, dryRun = false) {
|
|
|
57
121
|
}
|
|
58
122
|
const orphanedNoteCount = orphanedNotes.length;
|
|
59
123
|
return {
|
|
124
|
+
atomCount: activeAtoms.length,
|
|
125
|
+
staleAtomCount: nextNotes.filter((note) => note.referencesStatus === 'stale').length,
|
|
126
|
+
needsReviewAtomCount: nextNotes.filter((note) => note.referencesStatus === 'mixed').length,
|
|
127
|
+
archivedAtomCount: atoms.filter((atom) => atom.status === 'archived').length + orphanedNoteCount,
|
|
128
|
+
duplicateAtomTopicCount: countDuplicateTitles(nextNotes),
|
|
60
129
|
noteCount: notes.length,
|
|
61
130
|
mixedOrStaleCount: nextNotes.filter((note) => ['mixed', 'stale', 'unresolved'].includes(note.referencesStatus)).length,
|
|
62
131
|
noisyFileRefCount: changes.reduce((total, change) => total + change.removedFileRefs.length, 0),
|
|
@@ -98,6 +167,23 @@ function countDuplicateTitles(notes) {
|
|
|
98
167
|
}
|
|
99
168
|
return duplicates.size;
|
|
100
169
|
}
|
|
170
|
+
function countDuplicateAtomTopics(atoms) {
|
|
171
|
+
const seen = new Set();
|
|
172
|
+
const duplicates = new Set();
|
|
173
|
+
for (const atom of atoms) {
|
|
174
|
+
const key = [
|
|
175
|
+
atom.type,
|
|
176
|
+
atom.topic.trim().toLowerCase(),
|
|
177
|
+
atom.claim.trim().toLowerCase(),
|
|
178
|
+
].join('\0');
|
|
179
|
+
if (!atom.topic.trim())
|
|
180
|
+
continue;
|
|
181
|
+
if (seen.has(key))
|
|
182
|
+
duplicates.add(key);
|
|
183
|
+
seen.add(key);
|
|
184
|
+
}
|
|
185
|
+
return duplicates.size;
|
|
186
|
+
}
|
|
101
187
|
function countGeneratedScannedFiles(fileMap) {
|
|
102
188
|
return fileMap.files.filter((file) => [
|
|
103
189
|
'.agents/',
|
|
@@ -175,6 +261,13 @@ function evaluateReferenceStatus(relatedFiles, relatedSymbols, maps) {
|
|
|
175
261
|
return 'stale';
|
|
176
262
|
return 'mixed';
|
|
177
263
|
}
|
|
264
|
+
function atomStatusFromReferenceStatus(status) {
|
|
265
|
+
if (status === 'current')
|
|
266
|
+
return 'active';
|
|
267
|
+
if (status === 'mixed')
|
|
268
|
+
return 'needs-review';
|
|
269
|
+
return 'stale';
|
|
270
|
+
}
|
|
178
271
|
function isNoisyFileRef(ref) {
|
|
179
272
|
return (!ref.includes('/') && /^[A-Z][A-Za-z0-9_-]*\.[A-Za-z0-9_-]+$/.test(ref));
|
|
180
273
|
}
|
|
@@ -2,6 +2,7 @@ import { readFile } from 'node:fs/promises';
|
|
|
2
2
|
import path from 'node:path';
|
|
3
3
|
import { archiveInboxNote, listInboxNotes, readCognitionNotes, slugify, writeCognitionNote, writeDomainRecord, } from '../storage/cognition-store.js';
|
|
4
4
|
import { parseMarkdownNote } from './markdown-note-parser.js';
|
|
5
|
+
import { createKnowledgeAtom } from '../knowledge/atom-store.js';
|
|
5
6
|
export async function updateCognition(workspace, currentMaps, dryRun = false) {
|
|
6
7
|
const inboxNotes = await listInboxNotes(workspace);
|
|
7
8
|
const processed = [];
|
|
@@ -38,6 +39,19 @@ export async function updateCognition(workspace, currentMaps, dryRun = false) {
|
|
|
38
39
|
await archiveInboxNote(workspace, inboxPath, timestamp);
|
|
39
40
|
await writeCognitionNote(workspace, note);
|
|
40
41
|
await writeDomainRecord(workspace, toDomainRecord(note, currentMaps));
|
|
42
|
+
await createKnowledgeAtom(workspace, {
|
|
43
|
+
type: note.kind,
|
|
44
|
+
topic: note.title,
|
|
45
|
+
claim: note.summary ?? note.title,
|
|
46
|
+
summary: note.summary,
|
|
47
|
+
confidence: note.confidence,
|
|
48
|
+
files: note.relatedFiles,
|
|
49
|
+
symbols: note.relatedSymbols,
|
|
50
|
+
domains: note.domain ? [note.domain] : [],
|
|
51
|
+
sourceCommand: 'update',
|
|
52
|
+
createdAt: note.createdAt,
|
|
53
|
+
idSeed: note.id,
|
|
54
|
+
});
|
|
41
55
|
}
|
|
42
56
|
}
|
|
43
57
|
catch (error) {
|
|
@@ -1,58 +1,89 @@
|
|
|
1
1
|
import { mkdir, rename } from 'node:fs/promises';
|
|
2
2
|
import path from 'node:path';
|
|
3
|
-
import {
|
|
3
|
+
import { atomToCognitionNote, refreshKnowledgeAtomStatuses, writeKnowledgeAtoms, } from '../knowledge/atom-store.js';
|
|
4
|
+
import { overwriteDomainRecord, readDomainRecords, slugify, writeCognitionNote, } from '../storage/cognition-store.js';
|
|
4
5
|
import { pathExists } from '../storage/kgraph-paths.js';
|
|
5
6
|
import { readMaps } from '../storage/map-store.js';
|
|
6
7
|
import { evaluateReferenceStatus } from './cognition-updater.js';
|
|
7
8
|
export async function compactCognition(workspace, dryRun = false) {
|
|
8
|
-
const notes = await readCognitionNotes(workspace);
|
|
9
9
|
const maps = await readMaps(workspace);
|
|
10
|
-
const
|
|
10
|
+
const refreshed = await refreshKnowledgeAtomStatuses(workspace, { fileMap: maps.fileMap, symbolMap: maps.symbolMap }, dryRun);
|
|
11
|
+
const atoms = refreshed.atoms;
|
|
12
|
+
const groups = groupDuplicateAtoms(atoms);
|
|
11
13
|
const result = { merged: [], archived: [] };
|
|
12
14
|
const consumed = new Set();
|
|
13
15
|
const archived = new Set();
|
|
14
|
-
const
|
|
16
|
+
const mergedAtoms = [];
|
|
15
17
|
for (const group of groups.filter((items) => items.length > 1)) {
|
|
16
|
-
const merged =
|
|
18
|
+
const merged = mergeAtoms(group, {
|
|
17
19
|
files: maps.fileMap.files,
|
|
18
20
|
symbols: maps.symbolMap.symbols,
|
|
19
21
|
});
|
|
20
|
-
const sourceIds = group.map((
|
|
22
|
+
const sourceIds = group.map((atom) => atom.id);
|
|
21
23
|
result.merged.push({
|
|
22
24
|
targetId: merged.id,
|
|
23
25
|
sourceIds,
|
|
24
|
-
title: merged.
|
|
26
|
+
title: merged.topic,
|
|
25
27
|
});
|
|
26
28
|
sourceIds.forEach((id) => consumed.add(id));
|
|
27
|
-
|
|
28
|
-
if (!dryRun) {
|
|
29
|
-
await writeCognitionNote(workspace, merged);
|
|
30
|
-
for (const note of group) {
|
|
31
|
-
await archiveNote(workspace, note, `superseded-by-${merged.id}`);
|
|
32
|
-
}
|
|
33
|
-
}
|
|
29
|
+
mergedAtoms.push(merged);
|
|
34
30
|
}
|
|
35
|
-
for (const
|
|
36
|
-
if (consumed.has(
|
|
31
|
+
for (const atom of atoms) {
|
|
32
|
+
if (consumed.has(atom.id) || atom.status === 'archived')
|
|
37
33
|
continue;
|
|
38
|
-
if (
|
|
39
|
-
(
|
|
34
|
+
if (atom.confidence === 'low' &&
|
|
35
|
+
(atom.status === 'stale' || atom.status === 'needs-review')) {
|
|
40
36
|
result.archived.push({
|
|
41
|
-
id:
|
|
42
|
-
title:
|
|
43
|
-
reason:
|
|
37
|
+
id: atom.id,
|
|
38
|
+
title: atom.topic,
|
|
39
|
+
reason: `low-confidence ${atom.status} atom`,
|
|
44
40
|
});
|
|
45
|
-
archived.add(
|
|
46
|
-
if (!dryRun) {
|
|
47
|
-
await archiveNote(workspace, note, 'low-confidence-stale');
|
|
48
|
-
}
|
|
41
|
+
archived.add(atom.id);
|
|
49
42
|
}
|
|
50
43
|
}
|
|
51
44
|
if (!dryRun && (consumed.size > 0 || archived.size > 0)) {
|
|
52
|
-
const
|
|
53
|
-
|
|
54
|
-
...
|
|
45
|
+
const now = new Date().toISOString();
|
|
46
|
+
const nextAtoms = [
|
|
47
|
+
...atoms.map((atom) => {
|
|
48
|
+
if (consumed.has(atom.id)) {
|
|
49
|
+
const merged = mergedAtoms.find((candidate) => candidate.lifecycle.supersedes.includes(atom.id));
|
|
50
|
+
return {
|
|
51
|
+
...atom,
|
|
52
|
+
status: 'archived',
|
|
53
|
+
confidence: 'low',
|
|
54
|
+
lifecycle: {
|
|
55
|
+
...atom.lifecycle,
|
|
56
|
+
supersededBy: merged?.id,
|
|
57
|
+
archivedAt: now,
|
|
58
|
+
},
|
|
59
|
+
provenance: { ...atom.provenance, updatedAt: now },
|
|
60
|
+
};
|
|
61
|
+
}
|
|
62
|
+
if (archived.has(atom.id)) {
|
|
63
|
+
return {
|
|
64
|
+
...atom,
|
|
65
|
+
status: 'archived',
|
|
66
|
+
lifecycle: { ...atom.lifecycle, archivedAt: now },
|
|
67
|
+
provenance: { ...atom.provenance, updatedAt: now },
|
|
68
|
+
};
|
|
69
|
+
}
|
|
70
|
+
return atom;
|
|
71
|
+
}),
|
|
72
|
+
...mergedAtoms,
|
|
55
73
|
];
|
|
74
|
+
await writeKnowledgeAtoms(workspace, nextAtoms);
|
|
75
|
+
for (const atom of [...atoms.filter((item) => consumed.has(item.id) || archived.has(item.id)), ...mergedAtoms]) {
|
|
76
|
+
const note = atomToCognitionNote(atom);
|
|
77
|
+
if (mergedAtoms.some((merged) => merged.id === atom.id)) {
|
|
78
|
+
await writeCognitionNote(workspace, note);
|
|
79
|
+
}
|
|
80
|
+
else {
|
|
81
|
+
await archiveNote(workspace, note, consumed.has(atom.id) ? `superseded-by-${note.supersededBy ?? 'compact'}` : 'low-confidence-stale');
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
const activeNotes = nextAtoms
|
|
85
|
+
.filter((atom) => atom.status !== 'archived')
|
|
86
|
+
.map(atomToCognitionNote);
|
|
56
87
|
await rebuildDomainRecords(workspace, activeNotes, {
|
|
57
88
|
files: maps.fileMap.files,
|
|
58
89
|
symbols: maps.symbolMap.symbols,
|
|
@@ -60,6 +91,59 @@ export async function compactCognition(workspace, dryRun = false) {
|
|
|
60
91
|
}
|
|
61
92
|
return result;
|
|
62
93
|
}
|
|
94
|
+
function groupDuplicateAtoms(atoms) {
|
|
95
|
+
const byKey = new Map();
|
|
96
|
+
for (const atom of atoms) {
|
|
97
|
+
if (atom.status === 'archived' || atom.lifecycle.supersededBy)
|
|
98
|
+
continue;
|
|
99
|
+
const key = [
|
|
100
|
+
atom.type,
|
|
101
|
+
atom.scopeRefs.domains[0] ?? 'general',
|
|
102
|
+
normalizeText(atom.topic),
|
|
103
|
+
normalizeText(atom.claim),
|
|
104
|
+
stableList(atom.scopeRefs.files),
|
|
105
|
+
stableList(atom.scopeRefs.symbols),
|
|
106
|
+
].join('\0');
|
|
107
|
+
const group = byKey.get(key) ?? [];
|
|
108
|
+
group.push(atom);
|
|
109
|
+
byKey.set(key, group);
|
|
110
|
+
}
|
|
111
|
+
return [...byKey.values()];
|
|
112
|
+
}
|
|
113
|
+
function mergeAtoms(atoms, currentMaps) {
|
|
114
|
+
const sorted = [...atoms].sort((left, right) => left.provenance.createdAt.localeCompare(right.provenance.createdAt));
|
|
115
|
+
const base = sorted[sorted.length - 1];
|
|
116
|
+
const now = new Date().toISOString();
|
|
117
|
+
const id = `${now.replace(/[:.]/g, '-')}-${slugify(base.topic) || 'compacted'}`;
|
|
118
|
+
const summaries = unique(sorted
|
|
119
|
+
.map((atom) => atom.summary ?? atom.claim)
|
|
120
|
+
.filter((summary) => Boolean(summary?.trim())));
|
|
121
|
+
const files = unique(sorted.flatMap((atom) => atom.scopeRefs.files));
|
|
122
|
+
const symbols = unique(sorted.flatMap((atom) => atom.scopeRefs.symbols));
|
|
123
|
+
const domains = unique(sorted.flatMap((atom) => atom.scopeRefs.domains));
|
|
124
|
+
const packages = unique(sorted.flatMap((atom) => atom.scopeRefs.packages));
|
|
125
|
+
const status = atomStatusFromReferenceStatus(evaluateReferenceStatus(files, symbols, currentMaps));
|
|
126
|
+
return {
|
|
127
|
+
...base,
|
|
128
|
+
id,
|
|
129
|
+
claim: summaries[0] ?? base.claim,
|
|
130
|
+
summary: summaries.join('\n'),
|
|
131
|
+
confidence: highestConfidence(sorted.map((atom) => atom.confidence)),
|
|
132
|
+
status,
|
|
133
|
+
evidenceRefs: uniqueEvidence(sorted.flatMap((atom) => atom.evidenceRefs)),
|
|
134
|
+
scopeRefs: { files, symbols, domains, packages },
|
|
135
|
+
provenance: {
|
|
136
|
+
sourceCommand: 'compact',
|
|
137
|
+
agent: base.provenance.agent,
|
|
138
|
+
sessionId: base.provenance.sessionId,
|
|
139
|
+
commit: base.provenance.commit,
|
|
140
|
+
createdAt: now,
|
|
141
|
+
},
|
|
142
|
+
lifecycle: {
|
|
143
|
+
supersedes: sorted.map((atom) => atom.id),
|
|
144
|
+
},
|
|
145
|
+
};
|
|
146
|
+
}
|
|
63
147
|
function groupDuplicates(notes) {
|
|
64
148
|
const byKey = new Map();
|
|
65
149
|
for (const note of notes) {
|
|
@@ -153,6 +237,23 @@ function highestConfidence(values) {
|
|
|
153
237
|
return 'medium';
|
|
154
238
|
return 'low';
|
|
155
239
|
}
|
|
240
|
+
function atomStatusFromReferenceStatus(status) {
|
|
241
|
+
if (status === 'current')
|
|
242
|
+
return 'active';
|
|
243
|
+
if (status === 'mixed')
|
|
244
|
+
return 'needs-review';
|
|
245
|
+
return 'stale';
|
|
246
|
+
}
|
|
247
|
+
function uniqueEvidence(items) {
|
|
248
|
+
const seen = new Set();
|
|
249
|
+
return items.filter((item) => {
|
|
250
|
+
const key = JSON.stringify(item);
|
|
251
|
+
if (seen.has(key))
|
|
252
|
+
return false;
|
|
253
|
+
seen.add(key);
|
|
254
|
+
return true;
|
|
255
|
+
});
|
|
256
|
+
}
|
|
156
257
|
function normalizeTitle(value) {
|
|
157
258
|
return normalizeText(value);
|
|
158
259
|
}
|
|
@@ -10,6 +10,8 @@ export interface ConclusionInput {
|
|
|
10
10
|
relatedFiles?: string[];
|
|
11
11
|
relatedSymbols?: string[];
|
|
12
12
|
source: CognitionSource;
|
|
13
|
+
agent?: string;
|
|
14
|
+
sessionId?: string;
|
|
13
15
|
}
|
|
14
16
|
export declare function concludeTopic(workspace: KGraphWorkspace, input: ConclusionInput): Promise<CognitionNote>;
|
|
15
17
|
export declare function concludeActiveSession(workspace: KGraphWorkspace, agent: string, input: Omit<ConclusionInput, 'source' | 'relatedFiles' | 'relatedSymbols'>): Promise<CognitionNote>;
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
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
|
+
import { createKnowledgeAtom } from '../knowledge/atom-store.js';
|
|
4
5
|
import { evaluateReferenceStatus } from './cognition-updater.js';
|
|
5
6
|
import { readSessionState } from '../session/session-store.js';
|
|
6
7
|
export async function concludeTopic(workspace, input) {
|
|
@@ -36,6 +37,25 @@ export async function concludeTopic(workspace, input) {
|
|
|
36
37
|
files: maps.fileMap.files,
|
|
37
38
|
symbols: maps.symbolMap.symbols,
|
|
38
39
|
}));
|
|
40
|
+
await createKnowledgeAtom(workspace, {
|
|
41
|
+
type: note.kind,
|
|
42
|
+
topic: note.title,
|
|
43
|
+
claim: note.summary ?? note.title,
|
|
44
|
+
summary: note.summary,
|
|
45
|
+
confidence: note.confidence,
|
|
46
|
+
files: note.relatedFiles,
|
|
47
|
+
symbols: note.relatedSymbols,
|
|
48
|
+
domains: note.domain ? [note.domain] : [],
|
|
49
|
+
sourceCommand: input.source === 'session-conclude'
|
|
50
|
+
? 'session-conclude'
|
|
51
|
+
: input.source === 'compact'
|
|
52
|
+
? 'compact'
|
|
53
|
+
: 'conclude',
|
|
54
|
+
agent: input.agent,
|
|
55
|
+
sessionId: input.sessionId,
|
|
56
|
+
createdAt: note.createdAt,
|
|
57
|
+
idSeed: note.id,
|
|
58
|
+
}, maps);
|
|
39
59
|
return note;
|
|
40
60
|
}
|
|
41
61
|
export async function concludeActiveSession(workspace, agent, input) {
|
|
@@ -76,6 +96,8 @@ export async function buildActiveSessionConclusion(workspace, agent, input) {
|
|
|
76
96
|
body,
|
|
77
97
|
source: 'session-conclude',
|
|
78
98
|
relatedFiles: touchedFiles,
|
|
99
|
+
agent,
|
|
100
|
+
sessionId: active.sessionId,
|
|
79
101
|
};
|
|
80
102
|
}
|
|
81
103
|
function normalizeBody(value) {
|