@kentwynn/kgraph 0.2.0 → 0.2.1

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 CHANGED
@@ -62,6 +62,7 @@ kgraph "blog admin token usage"
62
62
  ```
63
63
 
64
64
  Instead of reading the whole repo, it gets a compact starting point: relevant files, symbols, relationships, domains, prior notes, and stale references to watch.
65
+ Each context item explains why it was returned, such as a path/name match, a matched cognition reference, a domain match, or a nearby import relationship.
65
66
 
66
67
  When you need change impact instead of broad context:
67
68
 
@@ -103,7 +104,7 @@ kgraph integrate add codex copilot cursor claude-code gemini windsurf cline
103
104
  # 3. Run the normal workflow for a topic
104
105
  kgraph "auth token refresh"
105
106
 
106
- # 4. Check health if something feels off
107
+ # 4. Verify the setup and use doctor as the quality gate
107
108
  kgraph doctor
108
109
  ```
109
110
 
@@ -120,7 +121,7 @@ kgraph "topic"
120
121
  kgraph
121
122
  ```
122
123
 
123
- Use `kgraph doctor --quality` and `kgraph repair --dry-run` only when stale or noisy cognition references start making context harder to trust.
124
+ Use `kgraph doctor` after setup and before trusting a repo's saved intelligence. It checks initialization, maps, pending inbox notes, integration targets, and actionable quality problems. Use `kgraph doctor --quality` and `kgraph repair --dry-run` when stale or noisy cognition references start making context harder to trust.
124
125
 
125
126
  Agents can also report session activity so KGraph can estimate token waste:
126
127
 
@@ -167,6 +168,8 @@ kgraph doctor --quality
167
168
 
168
169
  Checks whether the workspace is initialized, maps exist, inbox notes are pending, and configured integrations point to real files. Use `--quality` when context shows stale/noisy cognition references, unresolved local imports, unresolved call edges, duplicate cognition titles, or generated files in the scan.
169
170
 
171
+ The default doctor result is the main quality gate. It fails on actionable hygiene issues such as stale/noisy cognition, duplicate cognition titles, generated integration files leaking into scans, missing maps, or broken integration targets. Scanner coverage counts such as unresolved local imports or unresolved call edges remain visible in `--quality`, but they do not fail the gate by themselves because they often reflect current parser limits.
172
+
170
173
  ```bash
171
174
  kgraph repair --dry-run
172
175
  kgraph repair
@@ -174,6 +177,14 @@ kgraph repair
174
177
 
175
178
  `repair --dry-run` previews cleanup for noisy cognition references, such as framework names recorded as files or local variables recorded as symbols. `repair` applies only the safe noisy-reference cleanup; broader quality findings stay report-only. Run repair intentionally when stale references make context noisy; it is not part of every normal workflow.
176
179
 
180
+ ```bash
181
+ kgraph uninstall
182
+ kgraph uninstall --yes
183
+ kgraph uninstall --keep-integrations --yes
184
+ ```
185
+
186
+ `uninstall` previews repo-local removal and does not delete anything unless `--yes` is passed. `uninstall --yes` removes `.kgraph/` and KGraph-managed integration blocks/files while preserving source files and user-authored text outside managed blocks. Use `--keep-integrations --yes` to remove only `.kgraph/` while leaving AI tool instruction files in place. After uninstalling, `kgraph init` can be run again for a fresh setup.
187
+
177
188
  ```bash
178
189
  kgraph impact "Button"
179
190
  kgraph impact "createSession" --json
@@ -192,6 +203,7 @@ kgraph session end --agent codex
192
203
  ```
193
204
 
194
205
  Track agent-reported read/write activity, repeated reads, and estimated token cost. Supported agents are `codex`, `claude-code`, `copilot`, `cursor`, `gemini`, `windsurf`, and `cline`.
206
+ The text report now includes next actions, such as using `kgraph context "<topic>"` before repeated broad file inspection.
195
207
 
196
208
  ## Optional Step Commands
197
209
 
@@ -209,6 +221,7 @@ kgraph context "auth token refresh" --json
209
221
  ```
210
222
 
211
223
  Return context from existing maps and cognition without scanning or updating first.
224
+ Markdown output includes the reason each file, symbol, cognition note, nearby symbol, or relationship was selected. Use `--json` when an agent or script needs the same explanation data programmatically.
212
225
 
213
226
  ```bash
214
227
  kgraph update
@@ -27,7 +27,7 @@ export function registerContextCommand(program) {
27
27
  export function renderContextMarkdown(response) {
28
28
  const lines = [`# KGraph Context`, ``, `Query: ${response.query}`, ``];
29
29
  lines.push('## Matched Domains', '');
30
- lines.push(...formatList(response.matchedDomains.map((item) => `- ${item.item.name} (${item.reasons.join(', ')})`)));
30
+ lines.push(...formatList(response.matchedDomains.map((item) => `- ${item.item.name} because ${formatReasons(item.reasons)}`)));
31
31
  lines.push('', '## Relevant Files', '');
32
32
  lines.push(...formatList(response.relevantFiles.map((item) => {
33
33
  const f = item.item;
@@ -37,7 +37,7 @@ export function renderContextMarkdown(response) {
37
37
  ]
38
38
  .filter(Boolean)
39
39
  .join(', ');
40
- return `- ${f.path}${meta ? ` [${meta}]` : ''}`;
40
+ return `- ${f.path}${meta ? ` [${meta}]` : ''} because ${formatReasons(item.reasons)}`;
41
41
  })));
42
42
  lines.push('', '## Relevant Symbols', '');
43
43
  lines.push(...formatList(response.relevantSymbols.map((item) => {
@@ -46,25 +46,29 @@ export function renderContextMarkdown(response) {
46
46
  const lineRange = s.startLine != null && s.endLine != null
47
47
  ? `:${s.startLine}-${s.endLine}`
48
48
  : '';
49
- return `- ${s.name} (${kindInfo}) in ${s.filePath}${lineRange}`;
49
+ return `- ${s.name} (${kindInfo}) in ${s.filePath}${lineRange} because ${formatReasons(item.reasons)}`;
50
50
  })));
51
51
  lines.push('', '## Relevant Cognition', '');
52
- lines.push(...formatList(response.relevantCognition.map((item) => `- ${item.item.title} [${item.item.referencesStatus}]`)));
52
+ lines.push(...formatList(response.relevantCognition.map((item) => `- ${item.item.title} [${item.item.referencesStatus}] because ${formatReasons(item.reasons)}`)));
53
53
  lines.push('', '## Relationships', '');
54
- lines.push(...formatGroupedRelationships(response.relationships));
54
+ lines.push(...formatGroupedRelationships(response.relationships, response.relationshipExplanations));
55
55
  lines.push('', '## Nearby Symbols (1-hop imports)', '');
56
- lines.push(...formatList((response.nearbySymbols ?? []).map((s) => {
56
+ lines.push(...formatList(nearbySymbolItems(response).map(({ symbol: s, reasons }) => {
57
57
  const kindInfo = [s.kind, s.parentName].filter(Boolean).join(', ');
58
58
  const lineRange = s.startLine != null && s.endLine != null
59
59
  ? `:${s.startLine}-${s.endLine}`
60
60
  : '';
61
- return `- ${s.name} (${kindInfo}) in ${s.filePath}${lineRange}`;
61
+ return `- ${s.name} (${kindInfo}) in ${s.filePath}${lineRange} because ${formatReasons(reasons)}`;
62
62
  })));
63
63
  lines.push('', '## Stale References', '');
64
64
  lines.push(...formatList(response.staleReferences.map((ref) => `- ${ref}`)));
65
65
  return lines.join('\n');
66
66
  }
67
- function formatGroupedRelationships(relationships) {
67
+ function formatGroupedRelationships(relationships, explanations) {
68
+ const reasonsByRelationship = new Map((explanations ?? []).map((item) => [
69
+ relationshipKey(item.relationship),
70
+ item.reasons,
71
+ ]));
68
72
  const imports = relationships.filter((r) => r.relationshipType === 'import');
69
73
  const calls = relationships.filter((r) => r.relationshipType === 'calls');
70
74
  const contains = relationships.filter((r) => r.relationshipType === 'symbol-contains');
@@ -77,26 +81,62 @@ function formatGroupedRelationships(relationships) {
77
81
  const lines = [];
78
82
  if (imports.length > 0) {
79
83
  lines.push('Imports:');
80
- for (const r of imports)
81
- lines.push(` ${r.sourceId} → ${r.targetId}`);
84
+ for (const r of imports) {
85
+ lines.push(` ${r.sourceId} → ${r.targetId}${formatRelationshipReason(r, reasonsByRelationship)}`);
86
+ }
82
87
  }
83
88
  if (calls.length > 0) {
84
89
  lines.push('Calls:');
85
- for (const r of calls)
86
- lines.push(` ${r.sourceId} → ${r.targetId}`);
90
+ for (const r of calls) {
91
+ lines.push(` ${r.sourceId} → ${r.targetId}${formatRelationshipReason(r, reasonsByRelationship)}`);
92
+ }
87
93
  }
88
94
  if (contains.length > 0) {
89
95
  lines.push('Contains:');
90
- for (const r of contains)
91
- lines.push(` ${r.sourceId} contains ${r.targetId}`);
96
+ for (const r of contains) {
97
+ lines.push(` ${r.sourceId} contains ${r.targetId}${formatRelationshipReason(r, reasonsByRelationship)}`);
98
+ }
92
99
  }
93
100
  if (other.length > 0) {
94
101
  lines.push('Other:');
95
- for (const r of other)
96
- lines.push(` ${r.sourceId} ${r.relationshipType} ${r.targetId}`);
102
+ for (const r of other) {
103
+ lines.push(` ${r.sourceId} ${r.relationshipType} ${r.targetId}${formatRelationshipReason(r, reasonsByRelationship)}`);
104
+ }
97
105
  }
98
106
  return lines.length > 0 ? lines : ['- None'];
99
107
  }
100
108
  function formatList(items) {
101
109
  return items.length > 0 ? items : ['- None'];
102
110
  }
111
+ function formatReasons(reasons) {
112
+ if (reasons.length === 0) {
113
+ return 'it is near the query';
114
+ }
115
+ const visible = reasons.slice(0, 3);
116
+ const remaining = reasons.length - visible.length;
117
+ return remaining > 0
118
+ ? `${visible.join('; ')}; and ${remaining} more`
119
+ : visible.join('; ');
120
+ }
121
+ function nearbySymbolItems(response) {
122
+ if (response.nearbySymbolExplanations) {
123
+ return response.nearbySymbolExplanations;
124
+ }
125
+ return (response.nearbySymbols ?? []).map((symbol) => ({
126
+ symbol,
127
+ reasons: ['exported symbol from 1-hop import'],
128
+ }));
129
+ }
130
+ function formatRelationshipReason(relationship, reasonsByRelationship) {
131
+ const reasons = reasonsByRelationship.get(relationshipKey(relationship));
132
+ return reasons && reasons.length > 0
133
+ ? ` because ${formatReasons(reasons)}`
134
+ : '';
135
+ }
136
+ function relationshipKey(relationship) {
137
+ return [
138
+ relationship.sourceId,
139
+ relationship.targetId,
140
+ relationship.relationshipType,
141
+ ].join('\0');
142
+ }
@@ -80,12 +80,28 @@ export function registerDoctorCommand(program) {
80
80
  : `${integration.name}: missing ${integration.targetPath}`)
81
81
  .join('; '),
82
82
  });
83
+ let qualityReport;
84
+ if (maps) {
85
+ qualityReport = await analyzeCognitionQuality(workspace, maps);
86
+ const qualityFindings = summarizeQualityFindings(qualityReport);
87
+ const coverageNotes = summarizeCoverageNotes(qualityReport);
88
+ checks.push({
89
+ label: 'quality gate',
90
+ ok: qualityFindings.length === 0,
91
+ detail: qualityFindings.length === 0
92
+ ? [
93
+ 'no stale/noisy cognition, generated scan noise, or duplicate titles',
94
+ ...coverageNotes,
95
+ ].join('; ')
96
+ : qualityFindings.join('; '),
97
+ });
98
+ }
83
99
  printChecks(checks);
84
100
  if (options.quality && maps) {
85
101
  console.log('');
86
102
  console.log('KGraph Cognition Quality');
87
103
  console.log('');
88
- printQualityReport(await analyzeCognitionQuality(workspace, maps));
104
+ printQualityReport(qualityReport ?? (await analyzeCognitionQuality(workspace, maps)));
89
105
  }
90
106
  if (checks.some((check) => !check.ok)) {
91
107
  process.exitCode = 1;
@@ -134,3 +150,29 @@ export function printQualityReport(report) {
134
150
  console.log(` next status: ${change.nextStatus}`);
135
151
  }
136
152
  }
153
+ function summarizeQualityFindings(report) {
154
+ const findings = [];
155
+ if (report.mixedOrStaleCount > 0) {
156
+ findings.push(`${report.mixedOrStaleCount} stale/mixed/unresolved note(s)`);
157
+ }
158
+ if (report.noisyFileRefCount > 0 || report.noisySymbolRefCount > 0) {
159
+ findings.push(`${report.noisyFileRefCount + report.noisySymbolRefCount} noisy cognition ref(s); run \`kgraph repair --dry-run\``);
160
+ }
161
+ if (report.duplicateTitleCount > 0) {
162
+ findings.push(`${report.duplicateTitleCount} duplicate cognition title(s)`);
163
+ }
164
+ if (report.generatedFileScanCount > 0) {
165
+ findings.push(`${report.generatedFileScanCount} generated/integration file(s) scanned; update excludes`);
166
+ }
167
+ return findings;
168
+ }
169
+ function summarizeCoverageNotes(report) {
170
+ const notes = [];
171
+ if (report.unresolvedLocalImportCount > 0) {
172
+ notes.push(`${report.unresolvedLocalImportCount} unresolved local import(s) visible in --quality`);
173
+ }
174
+ if (report.unresolvedCallCount > 0) {
175
+ notes.push(`${report.unresolvedCallCount} unresolved call edge(s) visible in --quality`);
176
+ }
177
+ return notes;
178
+ }
@@ -5,7 +5,8 @@ import { KGraphError, runCommand } from '../errors.js';
5
5
  export function registerIntegrateCommand(program) {
6
6
  const integrate = program
7
7
  .command('integrate')
8
- .description('Manage AI tool integrations');
8
+ .description('Manage AI tool integrations')
9
+ .helpOption('-h, --help', 'Show help');
9
10
  integrate
10
11
  .command('list')
11
12
  .description('List configured integrations')
@@ -6,6 +6,7 @@ export function registerSessionCommand(program) {
6
6
  const session = program
7
7
  .command('session')
8
8
  .description('Track agent read/write session activity and token estimates')
9
+ .helpOption('-h, --help', 'Show help')
9
10
  .option('--json', 'Print JSON output')
10
11
  .action((options) => runCommand(async () => {
11
12
  const workspace = await assertWorkspace(process.cwd());
@@ -93,6 +94,8 @@ export function renderSessionReport(report) {
93
94
  lines.push(...formatList(report.recentEvents.map((event) => `- ${event.agent} ${event.type}${event.path ? ` ${event.path}` : ''} [${event.captureSource}]`)));
94
95
  lines.push('', 'Recent Ledger');
95
96
  lines.push(...formatList(report.ledger.map((entry) => `- ${entry.agent} ${entry.readCount} reads, ${entry.writeCount} writes, ${entry.repeatedReadCount} repeated`)));
97
+ lines.push('', 'Next');
98
+ lines.push(...sessionNextActions(report));
96
99
  return lines.join('\n');
97
100
  }
98
101
  function requireAgent(value) {
@@ -110,3 +113,20 @@ function normalizeSource(value) {
110
113
  function formatList(items) {
111
114
  return items.length > 0 ? items : ['- None'];
112
115
  }
116
+ function sessionNextActions(report) {
117
+ if (report.readCount === 0 && report.writeCount === 0) {
118
+ return [
119
+ '- Start tracking with `kgraph session start --agent <name>`.',
120
+ '- Record meaningful reads/writes with `kgraph session read <path> --agent <name>` and `kgraph session write <path> --agent <name>`.',
121
+ ];
122
+ }
123
+ if (report.repeatedReadCount > 0) {
124
+ return [
125
+ '- Repeated reads are present; run `kgraph context "<topic>"` before broad file inspection.',
126
+ '- End the tracked work with `kgraph session end --agent <name>` when the coding session is done.',
127
+ ];
128
+ }
129
+ return [
130
+ '- End the tracked work with `kgraph session end --agent <name>` when the coding session is done.',
131
+ ];
132
+ }
@@ -0,0 +1,2 @@
1
+ import type { Command } from 'commander';
2
+ export declare function registerUninstallCommand(program: Command): void;
@@ -0,0 +1,69 @@
1
+ import { rm } from 'node:fs/promises';
2
+ import { loadConfig } from '../../config/config.js';
3
+ import { removeIntegrations } from '../../integrations/integration-store.js';
4
+ import { pathExists, resolveWorkspace } from '../../storage/kgraph-paths.js';
5
+ import { runCommand } from '../errors.js';
6
+ export function registerUninstallCommand(program) {
7
+ program
8
+ .command('uninstall')
9
+ .description('Remove KGraph from this repository')
10
+ .option('--yes', 'Apply the uninstall after previewing what will be removed')
11
+ .option('--keep-integrations', 'Remove only .kgraph/ and preserve generated AI tool instruction files')
12
+ .action((options) => runCommand(async () => {
13
+ const workspace = resolveWorkspace(process.cwd());
14
+ const initialized = await pathExists(workspace.kgraphPath);
15
+ const configuredIntegrations = initialized
16
+ ? (await loadConfig(workspace)).integrations.map((integration) => integration.name)
17
+ : [];
18
+ printUninstallPreview({
19
+ initialized,
20
+ integrations: configuredIntegrations,
21
+ keepIntegrations: options.keepIntegrations === true,
22
+ applying: options.yes === true,
23
+ });
24
+ if (!options.yes) {
25
+ return;
26
+ }
27
+ if (initialized &&
28
+ !options.keepIntegrations &&
29
+ configuredIntegrations.length > 0) {
30
+ await removeIntegrations(workspace, configuredIntegrations);
31
+ }
32
+ if (initialized) {
33
+ await rm(workspace.kgraphPath, { recursive: true, force: true });
34
+ }
35
+ console.log('');
36
+ console.log('KGraph uninstall complete.');
37
+ console.log('Run `kgraph init` to set up this repository again.');
38
+ }));
39
+ }
40
+ function printUninstallPreview(input) {
41
+ console.log('KGraph Uninstall Preview');
42
+ console.log('');
43
+ console.log('Will remove:');
44
+ if (input.initialized) {
45
+ console.log('- .kgraph/ runtime workspace');
46
+ }
47
+ else {
48
+ console.log('- Nothing: .kgraph/ does not exist in this repository');
49
+ }
50
+ if (!input.keepIntegrations) {
51
+ if (input.integrations.length > 0) {
52
+ console.log(`- KGraph-managed integration blocks/files for: ${input.integrations.join(', ')}`);
53
+ }
54
+ else {
55
+ console.log('- No configured integration blocks/files found');
56
+ }
57
+ }
58
+ console.log('');
59
+ console.log('Will preserve:');
60
+ console.log('- Repository source files');
61
+ console.log('- User-authored content outside KGraph managed blocks');
62
+ if (input.keepIntegrations) {
63
+ console.log('- KGraph-managed integration instruction files');
64
+ }
65
+ if (!input.applying) {
66
+ console.log('');
67
+ console.log('No files were removed. Run `kgraph uninstall --yes` to apply.');
68
+ }
69
+ }
package/dist/cli/help.js CHANGED
@@ -36,6 +36,8 @@ export function renderRootHelp(useColor = supportsColor()) {
36
36
  command('doctor --quality', 'Report stale/noisy cognition references'),
37
37
  command('repair --dry-run', 'Preview cognition reference cleanup'),
38
38
  command('repair', 'Clean noisy stale cognition references'),
39
+ command('uninstall', 'Preview repo-local KGraph removal'),
40
+ command('uninstall --yes', 'Remove .kgraph/ and managed integrations'),
39
41
  command('visualize', 'Interactive dependency graph at http://localhost:4242'),
40
42
  command('history "blog button"', 'Search processed cognition sessions'),
41
43
  '',
@@ -1,3 +1,4 @@
1
1
  #!/usr/bin/env node
2
2
  import { Command } from 'commander';
3
3
  export declare function createProgram(): Command;
4
+ export declare function shouldRenderRootHelpBeforeParse(argv: string[]): boolean;
package/dist/cli/index.js CHANGED
@@ -12,6 +12,7 @@ import { registerIntegrateCommand } from './commands/integrate.js';
12
12
  import { registerRepairCommand } from './commands/repair.js';
13
13
  import { registerScanCommand } from './commands/scan.js';
14
14
  import { registerSessionCommand } from './commands/session.js';
15
+ import { registerUninstallCommand } from './commands/uninstall.js';
15
16
  import { registerUpdateCommand } from './commands/update.js';
16
17
  import { registerVisualizeCommand } from './commands/visualize.js';
17
18
  import { runDefaultWorkflow } from './commands/workflow.js';
@@ -25,7 +26,6 @@ export function createProgram() {
25
26
  .description('Persistent repo intelligence for AI coding assistants')
26
27
  .argument('[topic...]', 'Run the default refresh workflow and optionally return context for a topic')
27
28
  .version(version)
28
- .addHelpText('beforeAll', renderRootHelp())
29
29
  .helpOption(false)
30
30
  .action(async (topicParts = []) => {
31
31
  await runDefaultWorkflow(topicParts.join(' '));
@@ -48,17 +48,44 @@ export function createProgram() {
48
48
  registerHistoryCommand(program);
49
49
  registerDoctorCommand(program);
50
50
  registerRepairCommand(program);
51
+ registerUninstallCommand(program);
51
52
  return program;
52
53
  }
53
54
  if (isCliEntrypoint()) {
54
55
  const program = createProgram();
55
- if (process.argv.includes('-h') || process.argv.includes('--help')) {
56
+ const helpTarget = findExplicitHelpTarget(program, process.argv.slice(2));
57
+ if (helpTarget === program || shouldRenderRootHelpBeforeParse(process.argv)) {
56
58
  console.log(renderRootHelp());
57
59
  }
60
+ else if (helpTarget) {
61
+ helpTarget.outputHelp();
62
+ }
58
63
  else {
59
64
  await program.parseAsync(process.argv);
60
65
  }
61
66
  }
67
+ export function shouldRenderRootHelpBeforeParse(argv) {
68
+ const args = argv.slice(2);
69
+ return args.length === 1 && (args[0] === '-h' || args[0] === '--help');
70
+ }
71
+ function findExplicitHelpTarget(program, args) {
72
+ let command = program;
73
+ let matchedSubcommand = false;
74
+ for (const arg of args) {
75
+ if (arg === '-h' || arg === '--help') {
76
+ return matchedSubcommand ? command : program;
77
+ }
78
+ if (arg.startsWith('-')) {
79
+ continue;
80
+ }
81
+ const next = command.commands.find((candidate) => candidate.name() === arg || candidate.aliases().includes(arg));
82
+ if (next) {
83
+ command = next;
84
+ matchedSubcommand = true;
85
+ }
86
+ }
87
+ return undefined;
88
+ }
62
89
  function isCliEntrypoint() {
63
90
  if (!process.argv[1]) {
64
91
  return false;
@@ -59,6 +59,13 @@ export async function queryContext(workspace, config, maps, query) {
59
59
  ].filter((relationship, index, all) => all.findIndex((candidate) => candidate.sourceId === relationship.sourceId &&
60
60
  candidate.targetId === relationship.targetId &&
61
61
  candidate.relationshipType === relationship.relationshipType) === index);
62
+ const relationshipExplanations = explainRelationships(relationships, {
63
+ rankedRelationships,
64
+ relevantFiles,
65
+ relevantSymbols,
66
+ relevantCognition,
67
+ matchedDomains,
68
+ });
62
69
  const filePaths = new Set(maps.fileMap.files.map((f) => f.path));
63
70
  const symbolNames = new Set(maps.symbolMap.symbols.map((s) => s.name));
64
71
  const staleReferences = cognition
@@ -95,6 +102,13 @@ export async function queryContext(workspace, config, maps, query) {
95
102
  importedFilePaths.has(s.filePath) &&
96
103
  !matchedSymbolIds.has(s.id))
97
104
  .slice(0, max);
105
+ const nearbySymbolExplanations = nearbySymbols.map((symbol) => ({
106
+ symbol,
107
+ reasons: [
108
+ `exported symbol from 1-hop import ${symbol.filePath}`,
109
+ ...dependenciesForImportedSymbol(symbol, maps.dependencyMap.dependencies),
110
+ ],
111
+ }));
98
112
  return {
99
113
  query,
100
114
  matchedDomains,
@@ -102,8 +116,71 @@ export async function queryContext(workspace, config, maps, query) {
102
116
  relevantSymbols,
103
117
  relevantCognition,
104
118
  relationships: relationships.slice(0, max),
119
+ relationshipExplanations: relationshipExplanations.slice(0, max),
105
120
  nearbySymbols,
121
+ nearbySymbolExplanations,
106
122
  staleReferences,
107
123
  warnings: [],
108
124
  };
109
125
  }
126
+ function explainRelationships(relationships, context) {
127
+ const rankedReasons = new Map(context.rankedRelationships.map((ranked) => [
128
+ relationshipKey(ranked.item),
129
+ ranked.reasons,
130
+ ]));
131
+ return relationships.map((relationship) => {
132
+ const reasons = new Set();
133
+ for (const reason of rankedReasons.get(relationshipKey(relationship)) ?? []) {
134
+ reasons.add(reason);
135
+ }
136
+ for (const file of context.relevantFiles) {
137
+ if (relationship.sourceId === file.item.path ||
138
+ relationship.targetId === file.item.path) {
139
+ reasons.add(`connected to matched file ${file.item.path}`);
140
+ }
141
+ }
142
+ for (const symbol of context.relevantSymbols) {
143
+ if (relationship.sourceId === symbol.item.id ||
144
+ relationship.targetId === symbol.item.id ||
145
+ relationship.sourceId === symbol.item.name ||
146
+ relationship.targetId === symbol.item.name ||
147
+ relationship.sourceId === symbol.item.filePath ||
148
+ relationship.targetId === symbol.item.filePath) {
149
+ reasons.add(`connected to matched symbol ${symbol.item.name}`);
150
+ }
151
+ }
152
+ for (const note of context.relevantCognition) {
153
+ if (note.item.relatedFiles.includes(relationship.sourceId) ||
154
+ note.item.relatedFiles.includes(relationship.targetId) ||
155
+ note.item.relatedSymbols.includes(relationship.sourceId) ||
156
+ note.item.relatedSymbols.includes(relationship.targetId)) {
157
+ reasons.add(`referenced by cognition "${note.item.title}"`);
158
+ }
159
+ }
160
+ for (const domain of context.matchedDomains) {
161
+ if (domain.item.files.includes(relationship.sourceId) ||
162
+ domain.item.files.includes(relationship.targetId) ||
163
+ domain.item.symbols.includes(relationship.sourceId) ||
164
+ domain.item.symbols.includes(relationship.targetId)) {
165
+ reasons.add(`inside matched domain ${domain.item.name}`);
166
+ }
167
+ }
168
+ if (reasons.size === 0) {
169
+ reasons.add(`nearby ${relationship.relationshipType} relationship`);
170
+ }
171
+ return { relationship, reasons: [...reasons] };
172
+ });
173
+ }
174
+ function dependenciesForImportedSymbol(symbol, dependencies) {
175
+ return dependencies
176
+ .filter((dependency) => dependency.kind === 'local' &&
177
+ dependency.resolvedFile === symbol.filePath)
178
+ .map((dependency) => `imported by ${dependency.fromFile} via ${dependency.specifier}`);
179
+ }
180
+ function relationshipKey(relationship) {
181
+ return [
182
+ relationship.sourceId,
183
+ relationship.targetId,
184
+ relationship.relationshipType,
185
+ ].join('\0');
186
+ }
@@ -37,7 +37,11 @@ export async function pathExists(targetPath) {
37
37
  export async function assertWorkspace(rootPath = process.cwd()) {
38
38
  const workspace = resolveWorkspace(rootPath);
39
39
  if (!(await pathExists(workspace.kgraphPath))) {
40
- throw new KGraphError("KGraph is not initialized. Run `kgraph init` first.");
40
+ throw new KGraphError([
41
+ "KGraph is not initialized for this repository.",
42
+ "Run `kgraph init` first, or use `kgraph init --integrations codex,copilot,cursor,claude-code` to initialize and connect common AI tools.",
43
+ "After init, run `kgraph doctor` to verify maps, integrations, and cognition quality.",
44
+ ].join("\n"));
41
45
  }
42
46
  const info = await stat(workspace.kgraphPath);
43
47
  if (!info.isDirectory()) {
@@ -38,7 +38,15 @@ export interface ContextResponse {
38
38
  relevantSymbols: RankedItem<CodeSymbol>[];
39
39
  relevantCognition: RankedItem<CognitionNote>[];
40
40
  relationships: Relationship[];
41
+ relationshipExplanations?: Array<{
42
+ relationship: Relationship;
43
+ reasons: string[];
44
+ }>;
41
45
  nearbySymbols?: CodeSymbol[];
46
+ nearbySymbolExplanations?: Array<{
47
+ symbol: CodeSymbol;
48
+ reasons: string[];
49
+ }>;
42
50
  staleReferences: string[];
43
51
  warnings: string[];
44
52
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@kentwynn/kgraph",
3
- "version": "0.2.0",
3
+ "version": "0.2.1",
4
4
  "description": "Persistent repo intelligence for AI coding assistants.",
5
5
  "type": "module",
6
6
  "bin": {
@@ -59,7 +59,7 @@
59
59
  "devDependencies": {
60
60
  "@types/node": "^20.17.10",
61
61
  "tsx": "^4.19.2",
62
- "vitest": "^2.1.8"
62
+ "vitest": "^3.2.4"
63
63
  },
64
64
  "engines": {
65
65
  "node": ">=20"