@kentwynn/kgraph 0.1.27 → 0.2.0

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.
@@ -1,4 +1,4 @@
1
- import type { Command } from "commander";
2
- import type { ContextResponse } from "../../types/cognition.js";
1
+ import type { Command } from 'commander';
2
+ import type { ContextResponse } from '../../types/cognition.js';
3
3
  export declare function registerContextCommand(program: Command): void;
4
4
  export declare function renderContextMarkdown(response: ContextResponse): string;
@@ -1,43 +1,102 @@
1
- import { loadConfig } from "../../config/config.js";
2
- import { queryContext } from "../../context/context-query.js";
3
- import { assertWorkspace } from "../../storage/kgraph-paths.js";
4
- import { mapsExist, readMaps } from "../../storage/map-store.js";
5
- import { KGraphError, runCommand } from "../errors.js";
1
+ import { loadConfig } from '../../config/config.js';
2
+ import { queryContext } from '../../context/context-query.js';
3
+ import { assertWorkspace } from '../../storage/kgraph-paths.js';
4
+ import { mapsExist, readMaps } from '../../storage/map-store.js';
5
+ import { KGraphError, runCommand } from '../errors.js';
6
6
  export function registerContextCommand(program) {
7
7
  program
8
- .command("context <query>")
9
- .description("Return compact repo context for a query")
10
- .option("--json", "Print JSON output")
8
+ .command('context <query>')
9
+ .description('Return compact repo context for a query')
10
+ .option('--json', 'Print JSON output')
11
11
  .action((query, options) => runCommand(async () => {
12
12
  if (!query.trim()) {
13
- throw new KGraphError("Query cannot be empty.");
13
+ throw new KGraphError('Query cannot be empty.');
14
14
  }
15
15
  const workspace = await assertWorkspace(process.cwd());
16
16
  if (!(await mapsExist(workspace))) {
17
- throw new KGraphError("KGraph maps are missing. Run `kgraph scan` first.");
17
+ throw new KGraphError('KGraph maps are missing. Run `kgraph scan` first.');
18
18
  }
19
19
  const config = await loadConfig(workspace);
20
20
  const maps = await readMaps(workspace);
21
21
  const response = await queryContext(workspace, config, maps, query);
22
- console.log(options.json ? JSON.stringify(response, null, 2) : renderContextMarkdown(response));
22
+ console.log(options.json
23
+ ? JSON.stringify(response, null, 2)
24
+ : renderContextMarkdown(response));
23
25
  }));
24
26
  }
25
27
  export function renderContextMarkdown(response) {
26
28
  const lines = [`# KGraph Context`, ``, `Query: ${response.query}`, ``];
27
- lines.push("## Matched Domains", "");
28
- lines.push(...formatList(response.matchedDomains.map((item) => `- ${item.item.name} (${item.reasons.join(", ")})`)));
29
- lines.push("", "## Relevant Files", "");
30
- lines.push(...formatList(response.relevantFiles.map((item) => `- ${item.item.path} (${item.reasons.join(", ")})`)));
31
- lines.push("", "## Relevant Symbols", "");
32
- lines.push(...formatList(response.relevantSymbols.map((item) => `- ${item.item.name} in ${item.item.filePath}`)));
33
- lines.push("", "## Relevant Cognition", "");
29
+ lines.push('## Matched Domains', '');
30
+ lines.push(...formatList(response.matchedDomains.map((item) => `- ${item.item.name} (${item.reasons.join(', ')})`)));
31
+ lines.push('', '## Relevant Files', '');
32
+ lines.push(...formatList(response.relevantFiles.map((item) => {
33
+ const f = item.item;
34
+ const meta = [
35
+ f.language,
36
+ f.tokenEstimate ? `~${f.tokenEstimate} tokens` : '',
37
+ ]
38
+ .filter(Boolean)
39
+ .join(', ');
40
+ return `- ${f.path}${meta ? ` [${meta}]` : ''}`;
41
+ })));
42
+ lines.push('', '## Relevant Symbols', '');
43
+ lines.push(...formatList(response.relevantSymbols.map((item) => {
44
+ const s = item.item;
45
+ const kindInfo = [s.kind, s.parentName].filter(Boolean).join(', ');
46
+ const lineRange = s.startLine != null && s.endLine != null
47
+ ? `:${s.startLine}-${s.endLine}`
48
+ : '';
49
+ return `- ${s.name} (${kindInfo}) in ${s.filePath}${lineRange}`;
50
+ })));
51
+ lines.push('', '## Relevant Cognition', '');
34
52
  lines.push(...formatList(response.relevantCognition.map((item) => `- ${item.item.title} [${item.item.referencesStatus}]`)));
35
- lines.push("", "## Relationships", "");
36
- lines.push(...formatList(response.relationships.map((relationship) => `- ${relationship.sourceId} ${relationship.relationshipType} ${relationship.targetId} (${relationship.confidence})`)));
37
- lines.push("", "## Stale References", "");
53
+ lines.push('', '## Relationships', '');
54
+ lines.push(...formatGroupedRelationships(response.relationships));
55
+ lines.push('', '## Nearby Symbols (1-hop imports)', '');
56
+ lines.push(...formatList((response.nearbySymbols ?? []).map((s) => {
57
+ const kindInfo = [s.kind, s.parentName].filter(Boolean).join(', ');
58
+ const lineRange = s.startLine != null && s.endLine != null
59
+ ? `:${s.startLine}-${s.endLine}`
60
+ : '';
61
+ return `- ${s.name} (${kindInfo}) in ${s.filePath}${lineRange}`;
62
+ })));
63
+ lines.push('', '## Stale References', '');
38
64
  lines.push(...formatList(response.staleReferences.map((ref) => `- ${ref}`)));
39
- return lines.join("\n");
65
+ return lines.join('\n');
66
+ }
67
+ function formatGroupedRelationships(relationships) {
68
+ const imports = relationships.filter((r) => r.relationshipType === 'import');
69
+ const calls = relationships.filter((r) => r.relationshipType === 'calls');
70
+ const contains = relationships.filter((r) => r.relationshipType === 'symbol-contains');
71
+ const other = relationships.filter((r) => r.relationshipType !== 'import' &&
72
+ r.relationshipType !== 'calls' &&
73
+ r.relationshipType !== 'symbol-contains' &&
74
+ r.relationshipType !== 'mentions' &&
75
+ r.relationshipType !== 'belongs-to-domain' &&
76
+ r.relationshipType !== 'stale-reference');
77
+ const lines = [];
78
+ if (imports.length > 0) {
79
+ lines.push('Imports:');
80
+ for (const r of imports)
81
+ lines.push(` ${r.sourceId} → ${r.targetId}`);
82
+ }
83
+ if (calls.length > 0) {
84
+ lines.push('Calls:');
85
+ for (const r of calls)
86
+ lines.push(` ${r.sourceId} → ${r.targetId}`);
87
+ }
88
+ if (contains.length > 0) {
89
+ lines.push('Contains:');
90
+ for (const r of contains)
91
+ lines.push(` ${r.sourceId} contains ${r.targetId}`);
92
+ }
93
+ if (other.length > 0) {
94
+ lines.push('Other:');
95
+ for (const r of other)
96
+ lines.push(` ${r.sourceId} ${r.relationshipType} ${r.targetId}`);
97
+ }
98
+ return lines.length > 0 ? lines : ['- None'];
40
99
  }
41
100
  function formatList(items) {
42
- return items.length > 0 ? items : ["- None"];
101
+ return items.length > 0 ? items : ['- None'];
43
102
  }
@@ -1,5 +1,4 @@
1
- import { updateCognition } from '../../cognition/cognition-updater.js';
2
- import { refreshCognitionReferenceStatuses } from '../../cognition/cognition-updater.js';
1
+ import { refreshCognitionReferenceStatuses, updateCognition, } from '../../cognition/cognition-updater.js';
3
2
  import { loadConfig } from '../../config/config.js';
4
3
  import { queryContext } from '../../context/context-query.js';
5
4
  import { scanRepository } from '../../scanner/repo-scanner.js';
@@ -35,6 +34,7 @@ export async function runDefaultWorkflow(query) {
35
34
  console.log(renderWorkflowBanner({
36
35
  files: scan.files.length,
37
36
  symbols: scan.symbols.length,
37
+ skippedFiles: scan.skippedFiles,
38
38
  cognitionNotes: update.processed.length,
39
39
  integrations: config.integrations.map((integration) => ({
40
40
  name: integration.name,
@@ -3,6 +3,7 @@ interface WorkflowBannerStats {
3
3
  files: number;
4
4
  symbols: number;
5
5
  cognitionNotes: number;
6
+ skippedFiles?: number;
6
7
  integrations?: WorkflowBannerIntegration[];
7
8
  }
8
9
  interface WorkflowBannerIntegration {
package/dist/cli/help.js CHANGED
@@ -77,7 +77,10 @@ export function renderWorkflowBanner(stats, useColor = supportsColor()) {
77
77
  ` ${theme.bold('KGraph')} ${theme.dim('repo intelligence refreshed')}`,
78
78
  '',
79
79
  theme.bold('Refresh Complete'),
80
- command('files', String(stats.files)),
80
+ command('files', String(stats.files) +
81
+ (stats.skippedFiles
82
+ ? ` (${stats.skippedFiles} unchanged, skipped)`
83
+ : '')),
81
84
  command('symbols', String(stats.symbols)),
82
85
  command('cognition notes processed', String(stats.cognitionNotes)),
83
86
  command('integration modes', integrationLine),
@@ -73,6 +73,28 @@ export async function queryContext(workspace, config, maps, query) {
73
73
  .filter((s) => !symbolNames.has(s))
74
74
  .map((ref) => `${note.title}: ${ref}`),
75
75
  ]);
76
+ // Collect nearby symbols: exported symbols from files 1-hop imported by matched files
77
+ const matchedFilePaths = new Set([
78
+ ...relevantFiles.map((f) => f.item.path),
79
+ ...relevantSymbols.map((s) => s.item.filePath),
80
+ ]);
81
+ const matchedSymbolIds = new Set(relevantSymbols.map((s) => s.item.id));
82
+ const importedFilePaths = new Set();
83
+ for (const dep of maps.dependencyMap.dependencies) {
84
+ if (dep.kind === 'local' &&
85
+ dep.resolvedFile &&
86
+ matchedFilePaths.has(dep.fromFile)) {
87
+ importedFilePaths.add(dep.resolvedFile);
88
+ }
89
+ }
90
+ // Remove files already in the matched set
91
+ for (const p of matchedFilePaths)
92
+ importedFilePaths.delete(p);
93
+ const nearbySymbols = maps.symbolMap.symbols
94
+ .filter((s) => s.exported &&
95
+ importedFilePaths.has(s.filePath) &&
96
+ !matchedSymbolIds.has(s.id))
97
+ .slice(0, max);
76
98
  return {
77
99
  query,
78
100
  matchedDomains,
@@ -80,6 +102,7 @@ export async function queryContext(workspace, config, maps, query) {
80
102
  relevantSymbols,
81
103
  relevantCognition,
82
104
  relationships: relationships.slice(0, max),
105
+ nearbySymbols,
83
106
  staleReferences,
84
107
  warnings: [],
85
108
  };
@@ -46,21 +46,71 @@ export async function scanRepository(rootPath, config, previous) {
46
46
  unique: true,
47
47
  ignore: buildFastGlobIgnore(allExcludes),
48
48
  });
49
+ // Build lookup maps from previous scan for incremental skip
50
+ const prevFileByPath = new Map((previous?.files ?? []).map((f) => [f.path, f]));
51
+ const prevSymbolsByFile = new Map();
52
+ const prevDepsByFile = new Map();
53
+ const prevRelsBySource = new Map();
54
+ if (previous) {
55
+ for (const sym of previous.symbols) {
56
+ const arr = prevSymbolsByFile.get(sym.filePath) ?? [];
57
+ arr.push(sym);
58
+ prevSymbolsByFile.set(sym.filePath, arr);
59
+ }
60
+ for (const dep of previous.dependencies) {
61
+ const arr = prevDepsByFile.get(dep.fromFile) ?? [];
62
+ arr.push(dep);
63
+ prevDepsByFile.set(dep.fromFile, arr);
64
+ }
65
+ for (const rel of previous.relationships) {
66
+ if (rel.relationshipType !== 'import' &&
67
+ rel.relationshipType !== 'moved-from') {
68
+ const arr = prevRelsBySource.get(rel.sourceId) ?? [];
69
+ arr.push(rel);
70
+ prevRelsBySource.set(rel.sourceId, arr);
71
+ }
72
+ }
73
+ }
49
74
  const files = [];
50
75
  const symbols = [];
51
76
  const dependencies = [];
52
77
  const relationships = [];
53
78
  const warnings = [];
79
+ let skippedFiles = 0;
54
80
  for (const repoPath of entries.sort()) {
55
81
  if (shouldExclude(repoPath, mergedConfig)) {
56
82
  continue;
57
83
  }
58
84
  const absolutePath = path.join(rootPath, repoPath);
59
85
  try {
60
- const [info, content] = await Promise.all([
61
- stat(absolutePath),
62
- readFile(absolutePath),
63
- ]);
86
+ const info = await stat(absolutePath);
87
+ // Incremental skip: if mtime and size match previous, carry forward
88
+ const prevFile = prevFileByPath.get(repoPath);
89
+ if (prevFile &&
90
+ prevFile.sizeBytes === info.size &&
91
+ prevFile.modifiedAt === info.mtime.toISOString()) {
92
+ files.push({ ...prevFile, modifiedAt: info.mtime.toISOString() });
93
+ const prevSyms = prevSymbolsByFile.get(repoPath);
94
+ if (prevSyms)
95
+ symbols.push(...prevSyms);
96
+ const prevDeps = prevDepsByFile.get(repoPath);
97
+ if (prevDeps)
98
+ dependencies.push(...prevDeps);
99
+ const prevRels = prevRelsBySource.get(repoPath);
100
+ if (prevRels)
101
+ relationships.push(...prevRels);
102
+ // Also carry forward symbol-sourced relationships
103
+ if (prevSyms) {
104
+ for (const sym of prevSyms) {
105
+ const symRels = prevRelsBySource.get(sym.id);
106
+ if (symRels)
107
+ relationships.push(...symRels);
108
+ }
109
+ }
110
+ skippedFiles++;
111
+ continue;
112
+ }
113
+ const content = await readFile(absolutePath);
64
114
  const text = content.toString('utf8');
65
115
  const contentHash = crypto
66
116
  .createHash('sha256')
@@ -105,7 +155,14 @@ export async function scanRepository(rootPath, config, previous) {
105
155
  resolveLocalDependencies(dependencies, files);
106
156
  relationships.push(...buildImportRelationships(dependencies));
107
157
  relationships.push(...detectMovedFiles(previous?.files ?? [], files));
108
- return { files, symbols, dependencies, relationships, warnings };
158
+ return {
159
+ files,
160
+ symbols,
161
+ dependencies,
162
+ relationships,
163
+ warnings,
164
+ skippedFiles,
165
+ };
109
166
  }
110
167
  const SOURCE_EXTENSIONS = [
111
168
  '.ts',
@@ -1,5 +1,5 @@
1
- import type { CodeSymbol, Relationship, RepositoryFile } from "./maps.js";
2
- export type ReferenceStatus = "current" | "stale" | "unresolved" | "mixed";
1
+ import type { CodeSymbol, Relationship, RepositoryFile } from './maps.js';
2
+ export type ReferenceStatus = 'current' | 'stale' | 'unresolved' | 'mixed';
3
3
  export interface ParsedCognitionNote {
4
4
  title: string;
5
5
  domain?: string;
@@ -38,6 +38,7 @@ export interface ContextResponse {
38
38
  relevantSymbols: RankedItem<CodeSymbol>[];
39
39
  relevantCognition: RankedItem<CognitionNote>[];
40
40
  relationships: Relationship[];
41
+ nearbySymbols?: CodeSymbol[];
41
42
  staleReferences: string[];
42
43
  warnings: string[];
43
44
  }
@@ -1,7 +1,7 @@
1
- export type ScanStatus = "mapped" | "generic" | "failed";
2
- export type DependencyKind = "local" | "package" | "unknown";
3
- export type SymbolKind = "function" | "class" | "method" | "type" | "interface" | "export" | "import";
4
- export type RelationshipType = "import" | "contains" | "symbol-contains" | "calls" | "mentions" | "belongs-to-domain" | "stale-reference" | "moved-from";
1
+ export type ScanStatus = 'mapped' | 'generic' | 'failed';
2
+ export type DependencyKind = 'local' | 'package' | 'unknown';
3
+ export type SymbolKind = 'function' | 'class' | 'method' | 'type' | 'interface' | 'export' | 'import';
4
+ export type RelationshipType = 'import' | 'contains' | 'symbol-contains' | 'calls' | 'mentions' | 'belongs-to-domain' | 'stale-reference' | 'moved-from';
5
5
  export interface RepositoryFile {
6
6
  id: string;
7
7
  path: string;
@@ -36,7 +36,7 @@ export interface Relationship {
36
36
  targetType: string;
37
37
  targetId: string;
38
38
  relationshipType: RelationshipType;
39
- confidence: "high" | "medium" | "low";
39
+ confidence: 'high' | 'medium' | 'low';
40
40
  }
41
41
  export interface FileMap {
42
42
  generatedAt: string;
@@ -60,4 +60,5 @@ export interface ScanResult {
60
60
  dependencies: Dependency[];
61
61
  relationships: Relationship[];
62
62
  warnings: string[];
63
+ skippedFiles?: number;
63
64
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@kentwynn/kgraph",
3
- "version": "0.1.27",
3
+ "version": "0.2.0",
4
4
  "description": "Persistent repo intelligence for AI coding assistants.",
5
5
  "type": "module",
6
6
  "bin": {