@kentwynn/kgraph 0.2.30 → 0.2.31

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,5 @@
1
- import { atomToCognitionNote, readKnowledgeAtoms, updateKnowledgeAtom, updateKnowledgeAtoms, } from '../../knowledge/atom-store.js';
2
1
  import { rebuildDomainRecords } from '../../cognition/domain-records.js';
2
+ import { atomToCognitionNote, readKnowledgeAtoms, updateKnowledgeAtom, updateKnowledgeAtoms, } from '../../knowledge/atom-store.js';
3
3
  import { assertWorkspace } from '../../storage/kgraph-paths.js';
4
4
  import { readMaps } from '../../storage/map-store.js';
5
5
  import { KGraphError, runCommand } from '../errors.js';
@@ -109,7 +109,10 @@ export function registerKnowledgeCommand(program) {
109
109
  lifecycle: {
110
110
  ...nextAtoms[newIndex].lifecycle,
111
111
  supersedes: [
112
- ...new Set([...nextAtoms[newIndex].lifecycle.supersedes, oldId]),
112
+ ...new Set([
113
+ ...nextAtoms[newIndex].lifecycle.supersedes,
114
+ oldId,
115
+ ]),
113
116
  ],
114
117
  },
115
118
  };
@@ -144,13 +147,18 @@ function filterAtoms(atoms, options) {
144
147
  return atoms.filter((atom) => {
145
148
  if (options.type && atom.type !== options.type)
146
149
  return false;
147
- if (options.status &&
148
- atom.status !== normalizeStatus(options.status)) {
150
+ if (options.status && atom.status !== normalizeStatus(options.status)) {
149
151
  return false;
150
152
  }
151
- if (options.topic &&
152
- !atom.topic.toLowerCase().includes(options.topic.toLowerCase())) {
153
- return false;
153
+ if (options.topic) {
154
+ const q = options.topic.toLowerCase();
155
+ const matches = atom.topic.toLowerCase().includes(q) ||
156
+ atom.claim.toLowerCase().includes(q) ||
157
+ (atom.summary?.toLowerCase().includes(q) ?? false) ||
158
+ atom.scopeRefs.files.some((f) => f.toLowerCase().includes(q)) ||
159
+ atom.scopeRefs.symbols.some((s) => s.toLowerCase().includes(q));
160
+ if (!matches)
161
+ return false;
154
162
  }
155
163
  return true;
156
164
  });
@@ -1,10 +1,10 @@
1
1
  import path from 'node:path';
2
+ import { loadConfig } from '../../config/config.js';
2
3
  import { buildContextPack } from '../../context/context-pack.js';
3
4
  import { queryContext } from '../../context/context-query.js';
4
- import { loadConfig } from '../../config/config.js';
5
5
  import { assertSessionAgent, recordSessionEvent, } from '../../session/session-store.js';
6
- import { assertWorkspace } from '../../storage/kgraph-paths.js';
7
6
  import { listInboxNotes } from '../../storage/cognition-store.js';
7
+ import { assertWorkspace } from '../../storage/kgraph-paths.js';
8
8
  import { mapsExist, readMaps } from '../../storage/map-store.js';
9
9
  import { KGraphError, runCommand } from '../errors.js';
10
10
  export function registerPackCommand(program) {
@@ -41,7 +41,7 @@ export function registerPackCommand(program) {
41
41
  ]);
42
42
  const response = await queryContext(workspace, config, maps, task);
43
43
  const pack = buildContextPack(response, budget, workspace.rootPath);
44
- const pendingInboxFiles = (await listInboxNotes(workspace)).map((file) => path.relative(workspace.rootPath, file));
44
+ const pendingInboxFiles = (await listInboxNotes(workspace)).map((file) => path.relative(workspace.rootPath, file).split(path.sep).join('/'));
45
45
  if (pendingInboxFiles.length > 0) {
46
46
  pack.pendingInbox = {
47
47
  count: pendingInboxFiles.length,
@@ -122,6 +122,12 @@ function appendGroup(lines, title, items) {
122
122
  lines.push(` range ${data.path}:${data.startLine}-${data.endLine}`);
123
123
  }
124
124
  }
125
+ if (item.kind === 'symbol') {
126
+ const data = item.data;
127
+ if (data.filePath && data.startLine != null && data.endLine != null) {
128
+ lines.push(` ${data.filePath}:${data.startLine}-${data.endLine}`);
129
+ }
130
+ }
125
131
  }
126
132
  if (items.length > 6)
127
133
  lines.push(` ◌ ${items.length - 6} more ${title.toLowerCase()} omitted from display`);
@@ -132,5 +138,7 @@ function formatReasons(reasons) {
132
138
  return 'included by pack ranking';
133
139
  const shown = reasons.slice(0, 3);
134
140
  const remaining = reasons.length - shown.length;
135
- return remaining > 0 ? `${shown.join('; ')}; and ${remaining} more` : shown.join('; ');
141
+ return remaining > 0
142
+ ? `${shown.join('; ')}; and ${remaining} more`
143
+ : shown.join('; ');
136
144
  }
@@ -13,14 +13,7 @@ export function buildContextPack(response, budget, rootPath) {
13
13
  data: ranked.item,
14
14
  })),
15
15
  ...buildFileRangeCandidates(response, budget, rootPath),
16
- ...response.relevantSymbols.map((ranked) => ({
17
- kind: 'symbol',
18
- id: ranked.item.id,
19
- title: ranked.item.name,
20
- tokenEstimate: 20,
21
- reasons: ranked.reasons,
22
- data: ranked.item,
23
- })),
16
+ ...response.relevantSymbols.map((ranked) => buildSymbolCandidate(ranked, rootPath)),
24
17
  ...response.relevantCognition.map((ranked) => ({
25
18
  kind: 'atom',
26
19
  id: ranked.item.id,
@@ -131,14 +124,18 @@ function strongPackPaths(candidates) {
131
124
  function isLowSignalCandidate(candidate, strongPaths) {
132
125
  if (strongPaths.size === 0)
133
126
  return false;
134
- if (candidate.kind === 'atom' || candidate.kind === 'git-change' || candidate.kind === 'file-range') {
127
+ if (candidate.kind === 'atom' ||
128
+ candidate.kind === 'git-change' ||
129
+ candidate.kind === 'file-range') {
135
130
  return false;
136
131
  }
137
132
  if (hasStrongReason(candidate))
138
133
  return false;
139
134
  if (candidateTouchesStrongPath(candidate, strongPaths))
140
135
  return false;
141
- return candidate.kind === 'file' || candidate.kind === 'symbol' || candidate.kind === 'relationship';
136
+ return (candidate.kind === 'file' ||
137
+ candidate.kind === 'symbol' ||
138
+ candidate.kind === 'relationship');
142
139
  }
143
140
  function hasStrongReason(candidate) {
144
141
  return candidate.reasons.some((reason) => reason.includes('matched atom') ||
@@ -170,6 +167,39 @@ const GENERIC_RANGE_TOKENS = new Set([
170
167
  'repo',
171
168
  'work',
172
169
  ]);
170
+ const MAX_SYMBOL_EXCERPT_LINES = 40;
171
+ function buildSymbolCandidate(ranked, rootPath) {
172
+ const symbol = ranked.item;
173
+ let excerpt;
174
+ let tokenEstimate = 20;
175
+ if (rootPath &&
176
+ symbol.startLine != null &&
177
+ symbol.endLine != null &&
178
+ symbol.endLine - symbol.startLine + 1 <= MAX_SYMBOL_EXCERPT_LINES) {
179
+ const fullPath = path.join(rootPath, symbol.filePath);
180
+ if (existsSync(fullPath)) {
181
+ try {
182
+ const content = readFileSync(fullPath, 'utf8');
183
+ const allLines = content.split(/\r?\n/);
184
+ const from = symbol.startLine - 1; // 0-based
185
+ const to = symbol.endLine; // exclusive
186
+ excerpt = allLines.slice(from, to).join('\n');
187
+ tokenEstimate = estimateTokens(excerpt, symbol.filePath);
188
+ }
189
+ catch {
190
+ // best-effort; fall back to default estimate
191
+ }
192
+ }
193
+ }
194
+ return {
195
+ kind: 'symbol',
196
+ id: symbol.id,
197
+ title: symbol.name,
198
+ tokenEstimate,
199
+ reasons: ranked.reasons,
200
+ data: excerpt != null ? { ...symbol, excerpt } : symbol,
201
+ };
202
+ }
173
203
  function buildFileRangeCandidates(response, budget, rootPath) {
174
204
  if (!rootPath)
175
205
  return [];
@@ -1,5 +1,5 @@
1
1
  import { randomUUID } from 'node:crypto';
2
- import { mkdir, readFile, rename, rm, writeFile } from 'node:fs/promises';
2
+ import { mkdir, readFile, rename, rm, unlink, writeFile, } from 'node:fs/promises';
3
3
  import path from 'node:path';
4
4
  import { setTimeout as delay } from 'node:timers/promises';
5
5
  import { KGraphError } from '../cli/errors.js';
@@ -120,8 +120,7 @@ export async function migrateLegacyCognitionToAtoms(workspace) {
120
120
  const current = await readAtomsFile(workspace);
121
121
  const currentIds = new Set(current.map((atom) => atom.id));
122
122
  const nextMigrated = migrated.filter((atom) => !currentIds.has(atom.id) &&
123
- !current.some((existing) => existing.topic === atom.topic &&
124
- existing.claim === atom.claim));
123
+ !current.some((existing) => existing.topic === atom.topic && existing.claim === atom.claim));
125
124
  if (nextMigrated.length > 0) {
126
125
  await writeKnowledgeAtomsUnlocked(workspace, [
127
126
  ...current,
@@ -206,7 +205,10 @@ export async function refreshKnowledgeAtomStatuses(workspace, maps, dryRun = fal
206
205
  export async function validateKnowledgeStore(workspace, maps) {
207
206
  const issues = [];
208
207
  if (!(await pathExists(schemaPath(workspace)))) {
209
- issues.push({ code: 'missing-schema', message: 'missing knowledge/schema.json' });
208
+ issues.push({
209
+ code: 'missing-schema',
210
+ message: 'missing knowledge/schema.json',
211
+ });
210
212
  }
211
213
  else {
212
214
  try {
@@ -220,7 +222,10 @@ export async function validateKnowledgeStore(workspace, maps) {
220
222
  }
221
223
  catch (error) {
222
224
  const message = error instanceof Error ? error.message : String(error);
223
- issues.push({ code: 'missing-schema', message: `invalid knowledge schema: ${message}` });
225
+ issues.push({
226
+ code: 'missing-schema',
227
+ message: `invalid knowledge schema: ${message}`,
228
+ });
224
229
  }
225
230
  }
226
231
  let atoms = [];
@@ -252,11 +257,15 @@ export async function validateKnowledgeStore(workspace, maps) {
252
257
  });
253
258
  }
254
259
  else if (ref.contentHash && ref.contentHash !== file.contentHash) {
255
- issues.push({
256
- code: 'stale-file-hash',
257
- atomId: atom.id,
258
- message: `${atom.id} references changed file ${ref.path}`,
259
- });
260
+ // Only report if the atom hasn't already been moved to needs-review;
261
+ // needs-review atoms are already caught by the quality gate.
262
+ if (atom.status !== 'needs-review') {
263
+ issues.push({
264
+ code: 'stale-file-hash',
265
+ atomId: atom.id,
266
+ message: `${atom.id} references changed file ${ref.path}; run \`kgraph stale\` to update atom status`,
267
+ });
268
+ }
260
269
  }
261
270
  }
262
271
  if (ref.type === 'symbol') {
@@ -344,7 +353,11 @@ function buildEvidenceRefs(input, maps) {
344
353
  refs.push({ type: 'git', commit: input.commit });
345
354
  }
346
355
  if (input.sessionId || input.agent) {
347
- refs.push({ type: 'session', sessionId: input.sessionId, agent: input.agent });
356
+ refs.push({
357
+ type: 'session',
358
+ sessionId: input.sessionId,
359
+ agent: input.agent,
360
+ });
348
361
  }
349
362
  return refs;
350
363
  }
@@ -373,7 +386,8 @@ function evaluateAtomHealth(refs, maps) {
373
386
  }
374
387
  }
375
388
  if (ref.type === 'symbol') {
376
- const exists = (ref.symbolId && symbolIds.has(ref.symbolId)) || symbolNames.has(ref.name);
389
+ const exists = (ref.symbolId && symbolIds.has(ref.symbolId)) ||
390
+ symbolNames.has(ref.name);
377
391
  if (!exists) {
378
392
  stale = true;
379
393
  reasons.push(`missing symbol:${ref.name}`);
@@ -393,7 +407,8 @@ function computeConfidence(initial, status, atom) {
393
407
  return 'low';
394
408
  if (status === 'needs-review' && initial === 'high')
395
409
  return 'medium';
396
- if (atom?.provenance.sourceCommand === 'legacy-migration' && initial === 'high') {
410
+ if (atom?.provenance.sourceCommand === 'legacy-migration' &&
411
+ initial === 'high') {
397
412
  return 'medium';
398
413
  }
399
414
  return initial;
@@ -518,7 +533,37 @@ async function atomicWriteFile(targetPath, content) {
518
533
  await mkdir(path.dirname(targetPath), { recursive: true });
519
534
  const tempPath = path.join(path.dirname(targetPath), `.${path.basename(targetPath)}.${process.pid}.${randomUUID()}.tmp`);
520
535
  await writeFile(tempPath, content, 'utf8');
521
- await rename(tempPath, targetPath);
536
+ // On Windows, rename over an existing file can transiently fail with EPERM
537
+ // when another handle was recently released. Retry with short delays.
538
+ let lastError;
539
+ for (let attempt = 0; attempt < 3; attempt++) {
540
+ try {
541
+ await rename(tempPath, targetPath);
542
+ return;
543
+ }
544
+ catch (error) {
545
+ const code = error.code;
546
+ if ((code === 'EPERM' || code === 'EACCES') && attempt < 2) {
547
+ lastError = error;
548
+ await delay(20 * (attempt + 1));
549
+ continue;
550
+ }
551
+ try {
552
+ await unlink(tempPath);
553
+ }
554
+ catch {
555
+ /* best-effort cleanup */
556
+ }
557
+ throw error;
558
+ }
559
+ }
560
+ try {
561
+ await unlink(tempPath);
562
+ }
563
+ catch {
564
+ /* best-effort cleanup */
565
+ }
566
+ throw lastError;
522
567
  }
523
568
  async function withKnowledgeWriteLock(workspace, operation) {
524
569
  await mkdir(workspace.knowledgePath, { recursive: true });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@kentwynn/kgraph",
3
- "version": "0.2.30",
3
+ "version": "0.2.31",
4
4
  "description": "Persistent repo intelligence for AI coding assistants.",
5
5
  "type": "module",
6
6
  "bin": {