@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.
@@ -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: countDuplicateAtomTopics(activeAtoms),
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' && !dependency.resolvedFile).length ?? 0);
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
- atom.type,
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 { overwriteDomainRecord, readDomainRecords, slugify, writeCognitionNote, } from '../storage/cognition-store.js';
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)))
@@ -67,7 +67,7 @@ export async function concludeTopic(workspace, input) {
67
67
  agent: input.agent,
68
68
  sessionId: input.sessionId,
69
69
  createdAt: note.createdAt,
70
- idSeed: note.id,
70
+ idSeed: title,
71
71
  }, maps);
72
72
  return note;
73
73
  }
@@ -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
+ }
@@ -7,6 +7,10 @@ export const DEFAULT_CONFIG = {
7
7
  exclude: [
8
8
  '.git',
9
9
  'node_modules',
10
+ 'venv',
11
+ '.venv',
12
+ '__pycache__',
13
+ '.pytest_cache',
10
14
  'dist',
11
15
  'build',
12
16
  '.next',
@@ -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 entries = await fg(config.include, {
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',
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@kentwynn/kgraph",
3
- "version": "0.2.23",
3
+ "version": "0.2.24",
4
4
  "description": "Persistent repo intelligence for AI coding assistants.",
5
5
  "type": "module",
6
6
  "bin": {