@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.
- package/dist/cli/commands/context.d.ts +2 -2
- package/dist/cli/commands/context.js +82 -23
- package/dist/cli/commands/workflow.js +2 -2
- package/dist/cli/help.d.ts +1 -0
- package/dist/cli/help.js +4 -1
- package/dist/context/context-query.js +23 -0
- package/dist/scanner/repo-scanner.js +62 -5
- package/dist/types/cognition.d.ts +3 -2
- package/dist/types/maps.d.ts +6 -5
- package/package.json +1 -1
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type { Command } from
|
|
2
|
-
import type { ContextResponse } from
|
|
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
|
|
2
|
-
import { queryContext } from
|
|
3
|
-
import { assertWorkspace } from
|
|
4
|
-
import { mapsExist, readMaps } from
|
|
5
|
-
import { KGraphError, runCommand } from
|
|
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(
|
|
9
|
-
.description(
|
|
10
|
-
.option(
|
|
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(
|
|
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(
|
|
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
|
|
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(
|
|
28
|
-
lines.push(...formatList(response.matchedDomains.map((item) => `- ${item.item.name} (${item.reasons.join(
|
|
29
|
-
lines.push(
|
|
30
|
-
lines.push(...formatList(response.relevantFiles.map((item) =>
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
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(
|
|
36
|
-
lines.push(...
|
|
37
|
-
lines.push(
|
|
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(
|
|
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 : [
|
|
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,
|
package/dist/cli/help.d.ts
CHANGED
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
|
|
61
|
-
|
|
62
|
-
|
|
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 {
|
|
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
|
|
2
|
-
export type ReferenceStatus =
|
|
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
|
}
|
package/dist/types/maps.d.ts
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
|
-
export type ScanStatus =
|
|
2
|
-
export type DependencyKind =
|
|
3
|
-
export type SymbolKind =
|
|
4
|
-
export type RelationshipType =
|
|
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:
|
|
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
|
}
|