@kentwynn/kgraph 0.2.23 → 0.2.24
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/knowledge.js +15 -1
- package/dist/cognition/cognition-quality.js +14 -8
- package/dist/cognition/compact.js +2 -29
- package/dist/cognition/conclusion.js +1 -1
- package/dist/cognition/domain-records.d.ts +4 -0
- package/dist/cognition/domain-records.js +32 -0
- package/dist/config/config.js +4 -0
- package/dist/knowledge/atom-store.js +1 -1
- package/dist/scanner/git-utils.d.ts +5 -0
- package/dist/scanner/git-utils.js +33 -1
- package/dist/scanner/repo-scanner.js +30 -3
- package/package.json +1 -1
|
@@ -1,5 +1,7 @@
|
|
|
1
|
-
import { readKnowledgeAtoms, updateKnowledgeAtom, } from '../../knowledge/atom-store.js';
|
|
1
|
+
import { atomToCognitionNote, readKnowledgeAtoms, updateKnowledgeAtom, } from '../../knowledge/atom-store.js';
|
|
2
|
+
import { rebuildDomainRecords } from '../../cognition/domain-records.js';
|
|
2
3
|
import { assertWorkspace } from '../../storage/kgraph-paths.js';
|
|
4
|
+
import { readMaps } from '../../storage/map-store.js';
|
|
3
5
|
import { KGraphError, runCommand } from '../errors.js';
|
|
4
6
|
export function registerKnowledgeCommand(program) {
|
|
5
7
|
const knowledge = program
|
|
@@ -70,6 +72,7 @@ export function registerKnowledgeCommand(program) {
|
|
|
70
72
|
provenance: { ...current.provenance, updatedAt: now },
|
|
71
73
|
lifecycle: { ...current.lifecycle, archivedAt: now },
|
|
72
74
|
}));
|
|
75
|
+
await rebuildActiveDomainRecords(workspace);
|
|
73
76
|
console.log(options.json
|
|
74
77
|
? JSON.stringify(atom, null, 2)
|
|
75
78
|
: `Archived knowledge atom: ${atom.id}`);
|
|
@@ -99,12 +102,23 @@ export function registerKnowledgeCommand(program) {
|
|
|
99
102
|
supersedes: [...new Set([...current.lifecycle.supersedes, oldId])],
|
|
100
103
|
},
|
|
101
104
|
}));
|
|
105
|
+
await rebuildActiveDomainRecords(workspace);
|
|
102
106
|
const result = { old: oldAtom, new: newAtom };
|
|
103
107
|
console.log(options.json
|
|
104
108
|
? JSON.stringify(result, null, 2)
|
|
105
109
|
: `Superseded ${oldId} with ${newId}`);
|
|
106
110
|
}));
|
|
107
111
|
}
|
|
112
|
+
async function rebuildActiveDomainRecords(workspace) {
|
|
113
|
+
const maps = await readMaps(workspace);
|
|
114
|
+
const activeNotes = (await readKnowledgeAtoms(workspace))
|
|
115
|
+
.filter((atom) => atom.status !== 'archived')
|
|
116
|
+
.map(atomToCognitionNote);
|
|
117
|
+
await rebuildDomainRecords(workspace, activeNotes, {
|
|
118
|
+
files: maps.fileMap.files,
|
|
119
|
+
symbols: maps.symbolMap.symbols,
|
|
120
|
+
});
|
|
121
|
+
}
|
|
108
122
|
async function requireAtom(workspace, atomId) {
|
|
109
123
|
const atom = (await readKnowledgeAtoms(workspace)).find((candidate) => candidate.id === atomId);
|
|
110
124
|
if (!atom)
|
|
@@ -26,7 +26,7 @@ export async function analyzeCognitionQuality(workspace, maps) {
|
|
|
26
26
|
noisySymbolRefCount: changes.reduce((total, change) => total + change.removedSymbolRefs.length, 0),
|
|
27
27
|
unresolvedLocalImportCount: countUnresolvedLocalImports(maps.dependencyMap),
|
|
28
28
|
unresolvedCallCount: countUnresolvedCalls(maps.symbolMap, maps.relationshipMap),
|
|
29
|
-
duplicateTitleCount:
|
|
29
|
+
duplicateTitleCount: countDuplicateTitles(notes),
|
|
30
30
|
generatedFileScanCount: countGeneratedScannedFiles(maps.fileMap),
|
|
31
31
|
expensiveFileCount: countExpensiveFiles(maps.fileMap),
|
|
32
32
|
highConfidenceMissingEvidenceCount: countHighConfidenceMissingEvidence(activeAtoms),
|
|
@@ -148,13 +148,23 @@ export async function repairCognition(workspace, maps, dryRun = false) {
|
|
|
148
148
|
};
|
|
149
149
|
}
|
|
150
150
|
function countUnresolvedLocalImports(dependencyMap) {
|
|
151
|
-
return (dependencyMap?.dependencies.filter((dependency) => dependency.kind === 'local' &&
|
|
151
|
+
return (dependencyMap?.dependencies.filter((dependency) => dependency.kind === 'local' &&
|
|
152
|
+
!dependency.resolvedFile &&
|
|
153
|
+
isActionableUnresolvedLocalImport(dependency)).length ?? 0);
|
|
154
|
+
}
|
|
155
|
+
function isActionableUnresolvedLocalImport(dependency) {
|
|
156
|
+
if (dependency.fromFile.endsWith('/next-env.d.ts') &&
|
|
157
|
+
dependency.specifier.includes('.next/')) {
|
|
158
|
+
return false;
|
|
159
|
+
}
|
|
160
|
+
return true;
|
|
152
161
|
}
|
|
153
162
|
function countUnresolvedCalls(symbolMap, relationshipMap) {
|
|
154
163
|
const symbolIds = new Set(symbolMap.symbols.map((symbol) => symbol.id));
|
|
155
164
|
const symbolNames = new Set(symbolMap.symbols.map((symbol) => symbol.name));
|
|
156
165
|
return (relationshipMap?.relationships.filter((relationship) => relationship.relationshipType === 'calls' &&
|
|
157
166
|
relationship.targetType === 'symbol' &&
|
|
167
|
+
relationship.confidence !== 'low' &&
|
|
158
168
|
!symbolIds.has(relationship.targetId) &&
|
|
159
169
|
!symbolNames.has(relationship.targetId) &&
|
|
160
170
|
![...symbolNames].some((name) => relationship.targetId.endsWith(`#${name}`))).length ?? 0);
|
|
@@ -176,12 +186,8 @@ function countDuplicateAtomTopics(atoms) {
|
|
|
176
186
|
const seen = new Set();
|
|
177
187
|
const duplicates = new Set();
|
|
178
188
|
for (const atom of atoms) {
|
|
179
|
-
const key =
|
|
180
|
-
|
|
181
|
-
atom.topic.trim().toLowerCase(),
|
|
182
|
-
atom.claim.trim().toLowerCase(),
|
|
183
|
-
].join('\0');
|
|
184
|
-
if (!atom.topic.trim())
|
|
189
|
+
const key = atom.topic.trim().toLowerCase();
|
|
190
|
+
if (!key)
|
|
185
191
|
continue;
|
|
186
192
|
if (seen.has(key))
|
|
187
193
|
duplicates.add(key);
|
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
import { mkdir, rename } from 'node:fs/promises';
|
|
2
2
|
import path from 'node:path';
|
|
3
3
|
import { atomToCognitionNote, refreshKnowledgeAtomStatuses, writeKnowledgeAtoms, } from '../knowledge/atom-store.js';
|
|
4
|
-
import {
|
|
4
|
+
import { rebuildDomainRecords } from './domain-records.js';
|
|
5
|
+
import { slugify, writeCognitionNote, } from '../storage/cognition-store.js';
|
|
5
6
|
import { pathExists } from '../storage/kgraph-paths.js';
|
|
6
7
|
import { readMaps } from '../storage/map-store.js';
|
|
7
8
|
import { evaluateReferenceStatus } from './cognition-updater.js';
|
|
@@ -194,34 +195,6 @@ function mergeNotes(notes, currentMaps) {
|
|
|
194
195
|
referencesStatus: evaluateReferenceStatus(relatedFiles, relatedSymbols, currentMaps),
|
|
195
196
|
};
|
|
196
197
|
}
|
|
197
|
-
async function rebuildDomainRecords(workspace, notes, currentMaps) {
|
|
198
|
-
const existingDomains = await readDomainRecords(workspace);
|
|
199
|
-
const existingByName = new Map(existingDomains.map((domain) => [domain.name, domain]));
|
|
200
|
-
const domainNames = new Set([
|
|
201
|
-
...existingDomains.map((domain) => domain.name),
|
|
202
|
-
...notes.map((note) => note.domain ?? 'general'),
|
|
203
|
-
]);
|
|
204
|
-
const fileSet = new Set(currentMaps.files.map((file) => file.path));
|
|
205
|
-
const symbolSet = new Set(currentMaps.symbols.map((symbol) => symbol.name));
|
|
206
|
-
for (const name of domainNames) {
|
|
207
|
-
const relatedNotes = notes.filter((note) => (note.domain ?? 'general') === name);
|
|
208
|
-
const existing = existingByName.get(name);
|
|
209
|
-
const next = {
|
|
210
|
-
name,
|
|
211
|
-
description: existing?.description,
|
|
212
|
-
pathHints: unique(relatedNotes.flatMap((note) => note.relatedFiles)),
|
|
213
|
-
tags: unique(relatedNotes.flatMap((note) => note.tags)),
|
|
214
|
-
files: unique(relatedNotes
|
|
215
|
-
.flatMap((note) => note.relatedFiles)
|
|
216
|
-
.filter((file) => fileSet.has(file))),
|
|
217
|
-
symbols: unique(relatedNotes
|
|
218
|
-
.flatMap((note) => note.relatedSymbols)
|
|
219
|
-
.filter((symbol) => symbolSet.has(symbol))),
|
|
220
|
-
cognitionNotes: relatedNotes.map((note) => note.id),
|
|
221
|
-
};
|
|
222
|
-
await overwriteDomainRecord(workspace, next);
|
|
223
|
-
}
|
|
224
|
-
}
|
|
225
198
|
async function archiveNote(workspace, note, reason) {
|
|
226
199
|
const source = path.join(workspace.cognitionPath, `${note.id}.md`);
|
|
227
200
|
if (!(await pathExists(source)))
|
|
@@ -0,0 +1,4 @@
|
|
|
1
|
+
import type { KGraphWorkspace } from '../types/config.js';
|
|
2
|
+
import type { CognitionNote } from '../types/cognition.js';
|
|
3
|
+
import type { ScanResult } from '../types/maps.js';
|
|
4
|
+
export declare function rebuildDomainRecords(workspace: KGraphWorkspace, notes: CognitionNote[], currentMaps: Pick<ScanResult, 'files' | 'symbols'>): Promise<void>;
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import { overwriteDomainRecord, readDomainRecords, } from '../storage/cognition-store.js';
|
|
2
|
+
export async function rebuildDomainRecords(workspace, notes, currentMaps) {
|
|
3
|
+
const existingDomains = await readDomainRecords(workspace);
|
|
4
|
+
const existingByName = new Map(existingDomains.map((domain) => [domain.name, domain]));
|
|
5
|
+
const domainNames = new Set([
|
|
6
|
+
...existingDomains.map((domain) => domain.name),
|
|
7
|
+
...notes.map((note) => note.domain ?? 'general'),
|
|
8
|
+
]);
|
|
9
|
+
const fileSet = new Set(currentMaps.files.map((file) => file.path));
|
|
10
|
+
const symbolSet = new Set(currentMaps.symbols.map((symbol) => symbol.name));
|
|
11
|
+
for (const name of domainNames) {
|
|
12
|
+
const relatedNotes = notes.filter((note) => (note.domain ?? 'general') === name);
|
|
13
|
+
const existing = existingByName.get(name);
|
|
14
|
+
const next = {
|
|
15
|
+
name,
|
|
16
|
+
description: existing?.description,
|
|
17
|
+
pathHints: unique(relatedNotes.flatMap((note) => note.relatedFiles)),
|
|
18
|
+
tags: unique(relatedNotes.flatMap((note) => note.tags)),
|
|
19
|
+
files: unique(relatedNotes
|
|
20
|
+
.flatMap((note) => note.relatedFiles)
|
|
21
|
+
.filter((file) => fileSet.has(file))),
|
|
22
|
+
symbols: unique(relatedNotes
|
|
23
|
+
.flatMap((note) => note.relatedSymbols)
|
|
24
|
+
.filter((symbol) => symbolSet.has(symbol))),
|
|
25
|
+
cognitionNotes: relatedNotes.map((note) => note.id),
|
|
26
|
+
};
|
|
27
|
+
await overwriteDomainRecord(workspace, next);
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
function unique(items) {
|
|
31
|
+
return [...new Set(items)];
|
|
32
|
+
}
|
package/dist/config/config.js
CHANGED
|
@@ -423,7 +423,7 @@ function buildIndexes(atoms) {
|
|
|
423
423
|
const terms = {};
|
|
424
424
|
const refs = {};
|
|
425
425
|
const topics = {};
|
|
426
|
-
for (const atom of atoms) {
|
|
426
|
+
for (const atom of atoms.filter((item) => item.status !== 'archived')) {
|
|
427
427
|
for (const term of tokenize([atom.topic, atom.claim, atom.summary ?? ''].join(' '))) {
|
|
428
428
|
addIndex(terms, term, atom.id);
|
|
429
429
|
}
|
|
@@ -12,6 +12,11 @@ export declare function getCurrentCommit(rootPath: string): Promise<string | nul
|
|
|
12
12
|
* Returns an empty array if git is unavailable or the ref is unknown.
|
|
13
13
|
*/
|
|
14
14
|
export declare function getChangedFilesSince(rootPath: string, ref: string): Promise<string[]>;
|
|
15
|
+
/**
|
|
16
|
+
* Returns the subset of paths ignored by Git, including nested .gitignore rules.
|
|
17
|
+
* Falls back to an empty set when Git is unavailable or the directory is not a repo.
|
|
18
|
+
*/
|
|
19
|
+
export declare function getGitIgnoredFiles(rootPath: string, paths: string[]): Promise<Set<string>>;
|
|
15
20
|
/**
|
|
16
21
|
* Returns paths of files with uncommitted changes (staged or unstaged)
|
|
17
22
|
* relative to HEAD. Returns an empty array if git is unavailable.
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { execFile } from 'node:child_process';
|
|
1
|
+
import { execFile, spawn } from 'node:child_process';
|
|
2
2
|
import { access } from 'node:fs/promises';
|
|
3
3
|
import path from 'node:path';
|
|
4
4
|
import { promisify } from 'node:util';
|
|
@@ -47,6 +47,38 @@ export async function getChangedFilesSince(rootPath, ref) {
|
|
|
47
47
|
return [];
|
|
48
48
|
}
|
|
49
49
|
}
|
|
50
|
+
/**
|
|
51
|
+
* Returns the subset of paths ignored by Git, including nested .gitignore rules.
|
|
52
|
+
* Falls back to an empty set when Git is unavailable or the directory is not a repo.
|
|
53
|
+
*/
|
|
54
|
+
export async function getGitIgnoredFiles(rootPath, paths) {
|
|
55
|
+
if (paths.length === 0) {
|
|
56
|
+
return new Set();
|
|
57
|
+
}
|
|
58
|
+
return new Promise((resolve) => {
|
|
59
|
+
const child = spawn('git', ['check-ignore', '--stdin'], {
|
|
60
|
+
cwd: rootPath,
|
|
61
|
+
stdio: ['pipe', 'pipe', 'ignore'],
|
|
62
|
+
});
|
|
63
|
+
let stdout = '';
|
|
64
|
+
child.stdout.setEncoding('utf8');
|
|
65
|
+
child.stdout.on('data', (chunk) => {
|
|
66
|
+
stdout += chunk;
|
|
67
|
+
});
|
|
68
|
+
child.on('error', () => resolve(new Set()));
|
|
69
|
+
child.on('close', (code) => {
|
|
70
|
+
if (code !== 0 && code !== 1) {
|
|
71
|
+
resolve(new Set());
|
|
72
|
+
return;
|
|
73
|
+
}
|
|
74
|
+
resolve(new Set(stdout
|
|
75
|
+
.split('\n')
|
|
76
|
+
.map((line) => line.trim())
|
|
77
|
+
.filter(Boolean)));
|
|
78
|
+
});
|
|
79
|
+
child.stdin.end(`${paths.join('\n')}\n`);
|
|
80
|
+
});
|
|
81
|
+
}
|
|
50
82
|
/**
|
|
51
83
|
* Returns paths of files with uncommitted changes (staged or unstaged)
|
|
52
84
|
* relative to HEAD. Returns an empty array if git is unavailable.
|
|
@@ -7,7 +7,7 @@ import { extractBroadSymbols, supportsBroadExtraction, } from './broad-symbol-ex
|
|
|
7
7
|
import { extractCSymbols } from './c-symbol-extractor.js';
|
|
8
8
|
import { extractCSharpSymbols } from './csharp-symbol-extractor.js';
|
|
9
9
|
import { buildFastGlobIgnore, detectLanguage, isPreciseLanguage, readGitignorePatterns, shouldExclude, } from './file-classifier.js';
|
|
10
|
-
import { getChangedFilesSince, isGitRepo } from './git-utils.js';
|
|
10
|
+
import { getChangedFilesSince, getGitIgnoredFiles, isGitRepo, } from './git-utils.js';
|
|
11
11
|
import { extractGoSymbols } from './go-symbol-extractor.js';
|
|
12
12
|
import { extractJvmSymbols } from './jvm-symbol-extractor.js';
|
|
13
13
|
import { extractPhpSymbols } from './php-symbol-extractor.js';
|
|
@@ -65,13 +65,17 @@ export async function scanRepository(rootPath, config, previous) {
|
|
|
65
65
|
const gitignorePatterns = await readGitignorePatterns(rootPath);
|
|
66
66
|
const allExcludes = [...config.exclude, ...gitignorePatterns];
|
|
67
67
|
const mergedConfig = { ...config, exclude: allExcludes };
|
|
68
|
-
const
|
|
68
|
+
const rawEntries = await fg(config.include, {
|
|
69
69
|
cwd: rootPath,
|
|
70
70
|
dot: true,
|
|
71
71
|
onlyFiles: true,
|
|
72
72
|
unique: true,
|
|
73
73
|
ignore: buildFastGlobIgnore(allExcludes),
|
|
74
74
|
});
|
|
75
|
+
const gitIgnored = (await isGitRepo(rootPath))
|
|
76
|
+
? await getGitIgnoredFiles(rootPath, rawEntries)
|
|
77
|
+
: new Set();
|
|
78
|
+
const entries = rawEntries.filter((entry) => !gitIgnored.has(entry));
|
|
75
79
|
// Build lookup maps from previous scan for incremental skip
|
|
76
80
|
const prevFileByPath = new Map((previous?.files ?? []).map((f) => [f.path, f]));
|
|
77
81
|
const prevSymbolsByFile = new Map();
|
|
@@ -215,7 +219,7 @@ export async function scanRepository(rootPath, config, previous) {
|
|
|
215
219
|
}
|
|
216
220
|
resolveLocalDependencies(dependencies, files);
|
|
217
221
|
relationships.push(...buildImportRelationships(dependencies));
|
|
218
|
-
relationships.push(...detectMovedFiles(previous?.files ?? [], files));
|
|
222
|
+
relationships.push(...detectMovedFiles((previous?.files ?? []).filter((file) => !shouldExclude(file.path, mergedConfig)), files));
|
|
219
223
|
return {
|
|
220
224
|
files,
|
|
221
225
|
symbols,
|
|
@@ -271,6 +275,9 @@ function resolveLocalDependencyPath(fromFile, specifier, filePaths) {
|
|
|
271
275
|
if (!specifier.startsWith('.')) {
|
|
272
276
|
return undefined;
|
|
273
277
|
}
|
|
278
|
+
if (path.posix.extname(fromFile) === '.py') {
|
|
279
|
+
return resolvePythonRelativeImportPath(fromFile, specifier, filePaths);
|
|
280
|
+
}
|
|
274
281
|
const base = path.posix.normalize(path.posix.join(path.posix.dirname(fromFile), specifier));
|
|
275
282
|
const candidates = path.posix.extname(base)
|
|
276
283
|
? [base]
|
|
@@ -280,6 +287,26 @@ function resolveLocalDependencyPath(fromFile, specifier, filePaths) {
|
|
|
280
287
|
];
|
|
281
288
|
return candidates.find((candidate) => filePaths.has(candidate));
|
|
282
289
|
}
|
|
290
|
+
function resolvePythonRelativeImportPath(fromFile, specifier, filePaths) {
|
|
291
|
+
const match = specifier.match(/^(\.+)(.*)$/);
|
|
292
|
+
if (!match) {
|
|
293
|
+
return undefined;
|
|
294
|
+
}
|
|
295
|
+
const [, dots, moduleName] = match;
|
|
296
|
+
const packageParts = path.posix.dirname(fromFile).split('/');
|
|
297
|
+
const levelsUp = Math.max(0, dots.length - 1);
|
|
298
|
+
const baseParts = levelsUp > 0 ? packageParts.slice(0, -levelsUp) : packageParts;
|
|
299
|
+
const modulePath = moduleName
|
|
300
|
+
.split('.')
|
|
301
|
+
.filter(Boolean)
|
|
302
|
+
.join('/');
|
|
303
|
+
const base = path.posix.normalize(path.posix.join(...baseParts, modulePath));
|
|
304
|
+
const candidates = [
|
|
305
|
+
`${base}.py`,
|
|
306
|
+
path.posix.join(base, '__init__.py'),
|
|
307
|
+
];
|
|
308
|
+
return candidates.find((candidate) => filePaths.has(candidate));
|
|
309
|
+
}
|
|
283
310
|
function buildImportRelationships(dependencies) {
|
|
284
311
|
return dependencies.map((dependency) => ({
|
|
285
312
|
sourceType: 'file',
|