@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.
Files changed (39) hide show
  1. package/README.md +59 -29
  2. package/dist/cli/commands/blame.d.ts +2 -0
  3. package/dist/cli/commands/blame.js +52 -0
  4. package/dist/cli/commands/doctor.js +42 -12
  5. package/dist/cli/commands/impact.js +11 -5
  6. package/dist/cli/commands/init.js +2 -0
  7. package/dist/cli/commands/knowledge.d.ts +2 -0
  8. package/dist/cli/commands/knowledge.js +137 -0
  9. package/dist/cli/commands/pack.d.ts +2 -0
  10. package/dist/cli/commands/pack.js +49 -0
  11. package/dist/cli/commands/repair.js +3 -3
  12. package/dist/cli/commands/stale.d.ts +2 -0
  13. package/dist/cli/commands/stale.js +33 -0
  14. package/dist/cli/commands/visualize.js +7 -6
  15. package/dist/cli/help.js +17 -11
  16. package/dist/cli/index.js +8 -0
  17. package/dist/cognition/cognition-quality.d.ts +5 -0
  18. package/dist/cognition/cognition-quality.js +98 -5
  19. package/dist/cognition/cognition-updater.js +14 -0
  20. package/dist/cognition/compact.js +129 -28
  21. package/dist/cognition/conclusion.d.ts +2 -0
  22. package/dist/cognition/conclusion.js +22 -0
  23. package/dist/context/context-pack.d.ts +3 -0
  24. package/dist/context/context-pack.js +71 -0
  25. package/dist/context/context-query.js +53 -28
  26. package/dist/integrations/adapters/claude-code.js +23 -3
  27. package/dist/integrations/adapters/codex.js +1 -1
  28. package/dist/integrations/adapters/copilot.js +46 -3
  29. package/dist/integrations/workflow-steps.js +17 -8
  30. package/dist/knowledge/atom-store.d.ts +60 -0
  31. package/dist/knowledge/atom-store.js +484 -0
  32. package/dist/storage/kgraph-paths.js +5 -2
  33. package/dist/types/config.d.ts +1 -0
  34. package/dist/types/knowledge.d.ts +92 -0
  35. package/dist/types/knowledge.js +1 -0
  36. package/dist/visualization/graph-builder.d.ts +5 -2
  37. package/dist/visualization/graph-builder.js +43 -18
  38. package/dist/visualization/html-template.js +24 -17
  39. 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 cognition')
10
- .option('--dry-run', 'Show proposed cognition cleanup without writing files')
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 cognition references found.');
24
+ console.log('No noisy atom references found.');
25
25
  }
26
26
  else if (options.dryRun) {
27
27
  console.log('');
@@ -0,0 +1,2 @@
1
+ import type { Command } from 'commander';
2
+ export declare function registerStaleCommand(program: Command): void;
@@ -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 { readCognitionNotes } from '../../storage/cognition-store.js';
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 [maps, cognition] = await Promise.all([
26
- readMaps(workspace),
27
- readCognitionNotes(workspace),
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, cognition);
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 cognition notes'),
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 cognition'),
34
- command('compact', 'Merge duplicate cognition and archive stale noise'),
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, cognition, and risk'),
37
- command('update', 'Optional: process only .kgraph/inbox Markdown cognition notes'),
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 cognition references'),
40
- command('repair --dry-run', 'Preview cognition reference cleanup'),
41
- command('repair', 'Clean noisy stale cognition references'),
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 always mode by default'),
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('cognition notes processed', String(stats.cognitionNotes)),
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 cognition quality'),
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, readCognitionNotes, readDomainRecords, writeCognitionNote, } from '../storage/cognition-store.js';
5
+ import { overwriteDomainRecord, readDomainRecords, writeCognitionNote, } from '../storage/cognition-store.js';
5
6
  export async function analyzeCognitionQuality(workspace, maps) {
6
- const notes = await readCognitionNotes(workspace);
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: countDuplicateTitles(notes),
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 notes = await readCognitionNotes(workspace);
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, nextNotes.filter((n) => n.referencesStatus !== 'stale'), maps);
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 { overwriteDomainRecord, readCognitionNotes, readDomainRecords, slugify, writeCognitionNote, } from '../storage/cognition-store.js';
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 groups = groupDuplicates(notes);
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 mergedNotes = [];
16
+ const mergedAtoms = [];
15
17
  for (const group of groups.filter((items) => items.length > 1)) {
16
- const merged = mergeNotes(group, {
18
+ const merged = mergeAtoms(group, {
17
19
  files: maps.fileMap.files,
18
20
  symbols: maps.symbolMap.symbols,
19
21
  });
20
- const sourceIds = group.map((note) => note.id);
22
+ const sourceIds = group.map((atom) => atom.id);
21
23
  result.merged.push({
22
24
  targetId: merged.id,
23
25
  sourceIds,
24
- title: merged.title,
26
+ title: merged.topic,
25
27
  });
26
28
  sourceIds.forEach((id) => consumed.add(id));
27
- mergedNotes.push(merged);
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 note of notes) {
36
- if (consumed.has(note.id))
31
+ for (const atom of atoms) {
32
+ if (consumed.has(atom.id) || atom.status === 'archived')
37
33
  continue;
38
- if (note.confidence === 'low' &&
39
- (note.referencesStatus === 'stale' || note.referencesStatus === 'unresolved')) {
34
+ if (atom.confidence === 'low' &&
35
+ (atom.status === 'stale' || atom.status === 'needs-review')) {
40
36
  result.archived.push({
41
- id: note.id,
42
- title: note.title,
43
- reason: 'low-confidence stale cognition',
37
+ id: atom.id,
38
+ title: atom.topic,
39
+ reason: `low-confidence ${atom.status} atom`,
44
40
  });
45
- archived.add(note.id);
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 activeNotes = [
53
- ...notes.filter((note) => !consumed.has(note.id) && !archived.has(note.id)),
54
- ...mergedNotes,
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) {
@@ -0,0 +1,3 @@
1
+ import type { ContextResponse } from '../types/cognition.js';
2
+ import type { ContextPack } from '../types/knowledge.js';
3
+ export declare function buildContextPack(response: ContextResponse, budget: number): ContextPack;