@kentwynn/kgraph 0.2.19 → 0.2.21

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.
@@ -2,8 +2,9 @@ import { refreshCognitionReferenceStatuses, updateCognition, } from '../../cogni
2
2
  import { concludeTopic } from '../../cognition/conclusion.js';
3
3
  import { loadConfig } from '../../config/config.js';
4
4
  import { queryContext } from '../../context/context-query.js';
5
- import { readKnowledgeAtoms } from '../../knowledge/atom-store.js';
5
+ import { refreshKnowledgeAtomStatuses } from '../../knowledge/atom-store.js';
6
6
  import { getWorkingTreeChanges } from '../../scanner/git-utils.js';
7
+ import { shouldExclude } from '../../scanner/file-classifier.js';
7
8
  import { scanRepository } from '../../scanner/repo-scanner.js';
8
9
  import { listInboxNotes } from '../../storage/cognition-store.js';
9
10
  import { assertWorkspace, pathExists, resolveWorkspace, } from '../../storage/kgraph-paths.js';
@@ -59,10 +60,22 @@ export async function runDefaultWorkflow(query, options = {}) {
59
60
  console.error(`Warning: ${warning}`);
60
61
  }
61
62
  }
62
- const atoms = await readKnowledgeAtoms(workspace);
63
+ const refreshedAtoms = await refreshKnowledgeAtomStatuses(workspace, {
64
+ fileMap: {
65
+ generatedAt: new Date().toISOString(),
66
+ files: scan.files,
67
+ },
68
+ symbolMap: {
69
+ generatedAt: new Date().toISOString(),
70
+ symbols: scan.symbols,
71
+ },
72
+ });
73
+ const atoms = refreshedAtoms.atoms;
63
74
  const pendingInbox = await listInboxNotes(workspace);
64
75
  const activeAtoms = atoms.filter((atom) => atom.status === 'active');
65
76
  const captureCheck = await buildCaptureCheck(workspace.rootPath, {
77
+ topic,
78
+ previousFiles: previousMaps.fileMap.files.filter((file) => !shouldExclude(file.path, config)),
66
79
  files: scan.files,
67
80
  atoms,
68
81
  });
@@ -95,7 +108,7 @@ export async function runDefaultWorkflow(query, options = {}) {
95
108
  if (options.final) {
96
109
  console.log('');
97
110
  renderFinalCaptureCheck(captureCheck, topic);
98
- if (captureCheck.required) {
111
+ if (captureCheck.required || captureCheck.unresolvedAtoms.length > 0) {
99
112
  process.exitCode = 1;
100
113
  }
101
114
  return;
@@ -111,10 +124,21 @@ export async function runDefaultWorkflow(query, options = {}) {
111
124
  }
112
125
  async function buildCaptureCheck(rootPath, input) {
113
126
  const knownFiles = new Set(input.files.map((file) => file.path));
114
- const changedFiles = (await getWorkingTreeChanges(rootPath)).filter((file) => knownFiles.has(file));
115
- if (changedFiles.length === 0) {
116
- return { required: false, changedFiles, coveredFiles: [] };
117
- }
127
+ const previousByPath = new Map(input.previousFiles.map((file) => [file.path, file]));
128
+ const currentPaths = new Set(input.files.map((file) => file.path));
129
+ const mapChangedFiles = input.files
130
+ .filter((file) => {
131
+ const previous = previousByPath.get(file.path);
132
+ return !previous || previous.contentHash !== file.contentHash;
133
+ })
134
+ .map((file) => file.path);
135
+ const deletedFiles = input.previousFiles
136
+ .filter((file) => !currentPaths.has(file.path))
137
+ .map((file) => file.path);
138
+ const gitChangedFiles = (await getWorkingTreeChanges(rootPath)).filter((file) => knownFiles.has(file) || previousByPath.has(file));
139
+ const changedFiles = [
140
+ ...new Set([...mapChangedFiles, ...deletedFiles, ...gitChangedFiles]),
141
+ ];
118
142
  const recentCutoff = Date.now() - 24 * 60 * 60 * 1000;
119
143
  const recentActiveAtoms = input.atoms.filter((atom) => {
120
144
  if (atom.status !== 'active')
@@ -122,6 +146,12 @@ async function buildCaptureCheck(rootPath, input) {
122
146
  const createdAt = Date.parse(atom.provenance.createdAt);
123
147
  return Number.isFinite(createdAt) && createdAt >= recentCutoff;
124
148
  });
149
+ const invalidatedAtoms = matchingInvalidatedAtoms(input.atoms, input.topic).filter((atom) => !isInvalidatedAtomCovered(atom, recentActiveAtoms));
150
+ const unresolvedAtoms = input.atoms.filter((atom) => atom.status === 'needs-review' || atom.status === 'stale');
151
+ const reviewItems = unresolvedAtoms.map((atom) => ({
152
+ atom,
153
+ replacement: findReplacementAtom(atom, recentActiveAtoms),
154
+ }));
125
155
  const covered = new Set();
126
156
  for (const atom of recentActiveAtoms) {
127
157
  for (const ref of atom.evidenceRefs) {
@@ -136,16 +166,136 @@ async function buildCaptureCheck(rootPath, input) {
136
166
  }
137
167
  }
138
168
  return {
139
- required: changedFiles.some((file) => !covered.has(file)),
169
+ required: changedFiles.some((file) => !covered.has(file)) ||
170
+ invalidatedAtoms.length > 0,
140
171
  changedFiles,
141
172
  coveredFiles: [...covered],
173
+ invalidatedAtoms,
174
+ unresolvedAtoms,
175
+ reviewItems,
142
176
  };
143
177
  }
178
+ function isInvalidatedAtomCovered(invalidated, recentActiveAtoms) {
179
+ return recentActiveAtoms.some((atom) => atomsOverlap(invalidated, atom));
180
+ }
181
+ function findReplacementAtom(invalidated, recentActiveAtoms) {
182
+ return recentActiveAtoms.find((atom) => atomsHaveReplacementSignal(invalidated, atom));
183
+ }
184
+ function atomsOverlap(a, b) {
185
+ const invalidatedFiles = new Set(a.scopeRefs.files);
186
+ const invalidatedSymbols = new Set(a.scopeRefs.symbols);
187
+ for (const ref of a.evidenceRefs) {
188
+ if (ref.type === 'file')
189
+ invalidatedFiles.add(ref.path);
190
+ if (ref.type === 'symbol')
191
+ invalidatedSymbols.add(ref.name);
192
+ }
193
+ const atomFiles = new Set(b.scopeRefs.files);
194
+ const atomSymbols = new Set(b.scopeRefs.symbols);
195
+ for (const ref of b.evidenceRefs) {
196
+ if (ref.type === 'file')
197
+ atomFiles.add(ref.path);
198
+ if (ref.type === 'symbol')
199
+ atomSymbols.add(ref.name);
200
+ }
201
+ const fileOverlap = [...invalidatedFiles].some((file) => atomFiles.has(file));
202
+ const symbolOverlap = [...invalidatedSymbols].some((symbol) => atomSymbols.has(symbol));
203
+ if (fileOverlap || symbolOverlap)
204
+ return true;
205
+ return tokenOverlap(a.topic, b.topic);
206
+ }
207
+ function atomsHaveReplacementSignal(a, b) {
208
+ const aSymbols = new Set(a.scopeRefs.symbols);
209
+ for (const ref of a.evidenceRefs) {
210
+ if (ref.type === 'symbol')
211
+ aSymbols.add(ref.name);
212
+ }
213
+ const bSymbols = new Set(b.scopeRefs.symbols);
214
+ for (const ref of b.evidenceRefs) {
215
+ if (ref.type === 'symbol')
216
+ bSymbols.add(ref.name);
217
+ }
218
+ const symbolOverlap = [...aSymbols].some((symbol) => bSymbols.has(symbol));
219
+ return symbolOverlap || meaningfulTopicOverlap(a.topic, b.topic);
220
+ }
221
+ function meaningfulTopicOverlap(a, b) {
222
+ const weakTokens = new Set([
223
+ 'add',
224
+ 'after',
225
+ 'behavior',
226
+ 'change',
227
+ 'changed',
228
+ 'new',
229
+ 'old',
230
+ 'review',
231
+ 'update',
232
+ 'with',
233
+ ]);
234
+ const aTokens = new Set(a
235
+ .toLowerCase()
236
+ .split(/[^a-z0-9]+/)
237
+ .filter((token) => token.length > 2 && !weakTokens.has(token)));
238
+ return b
239
+ .toLowerCase()
240
+ .split(/[^a-z0-9]+/)
241
+ .filter((token) => token.length > 2 && !weakTokens.has(token))
242
+ .some((token) => aTokens.has(token));
243
+ }
244
+ function tokenOverlap(a, b) {
245
+ const aTokens = new Set(a
246
+ .toLowerCase()
247
+ .split(/[^a-z0-9]+/)
248
+ .filter(Boolean));
249
+ return b
250
+ .toLowerCase()
251
+ .split(/[^a-z0-9]+/)
252
+ .filter(Boolean)
253
+ .some((token) => aTokens.has(token));
254
+ }
255
+ function matchingInvalidatedAtoms(atoms, topic) {
256
+ const tokens = new Set((topic ?? '')
257
+ .toLowerCase()
258
+ .split(/[^a-z0-9]+/)
259
+ .filter(Boolean));
260
+ return atoms.filter((atom) => {
261
+ if (atom.status !== 'needs-review' && atom.status !== 'stale')
262
+ return false;
263
+ if (tokens.size === 0)
264
+ return true;
265
+ const haystack = [
266
+ atom.topic,
267
+ atom.claim,
268
+ atom.summary,
269
+ ...atom.scopeRefs.files,
270
+ ...atom.scopeRefs.symbols,
271
+ ]
272
+ .filter(Boolean)
273
+ .join(' ')
274
+ .toLowerCase();
275
+ return [...tokens].some((token) => haystack.includes(token));
276
+ });
277
+ }
144
278
  function renderFinalCaptureCheck(check, topic) {
145
279
  console.log('KGraph Final Check');
280
+ if (!check.required && check.unresolvedAtoms.length > 0) {
281
+ console.log(' status memory-review-required');
282
+ console.log(` unresolved ${check.unresolvedAtoms.length}`);
283
+ console.log(' conclusion stale or needs-review atoms remain');
284
+ renderMemoryReviewItems(check.reviewItems);
285
+ return;
286
+ }
146
287
  if (check.changedFiles.length === 0) {
288
+ if (check.required) {
289
+ console.log(' status capture-required');
290
+ console.log(' changed files 0');
291
+ console.log(` invalid atoms ${check.invalidatedAtoms.length}`);
292
+ renderMemoryReviewItems(check.reviewItems.filter((item) => check.invalidatedAtoms.some((atom) => atom.id === item.atom.id)));
293
+ console.log(' conclusion missing for needs-review or stale knowledge');
294
+ console.log(` next kgraph "${topic || '<topic>'}" --capture "<durable conclusion>" --capture-file <path>`);
295
+ return;
296
+ }
147
297
  console.log(' status clean');
148
- console.log(' reason no mapped repo files changed');
298
+ console.log(' reason no mapped repo files changed or invalidated matching atoms');
149
299
  return;
150
300
  }
151
301
  if (!check.required) {
@@ -156,6 +306,26 @@ function renderFinalCaptureCheck(check, topic) {
156
306
  }
157
307
  console.log(' status capture-required');
158
308
  console.log(` changed files ${check.changedFiles.length}`);
309
+ if (check.invalidatedAtoms.length > 0) {
310
+ console.log(` invalid atoms ${check.invalidatedAtoms.length}`);
311
+ renderMemoryReviewItems(check.reviewItems.filter((item) => check.invalidatedAtoms.some((atom) => atom.id === item.atom.id)));
312
+ }
159
313
  console.log(' conclusion missing for one or more changed files');
160
314
  console.log(` next kgraph "${topic || '<topic>'}" --capture "<durable conclusion>" --capture-file <path>`);
161
315
  }
316
+ function renderMemoryReviewItems(items) {
317
+ const visible = items.slice(0, 3);
318
+ for (const item of visible) {
319
+ console.log(` review atom ${item.atom.id}`);
320
+ console.log(` review topic ${item.atom.status}: ${item.atom.topic}`);
321
+ if (item.replacement) {
322
+ console.log(` supersede kgraph knowledge supersede ${item.atom.id} ${item.replacement.id}`);
323
+ }
324
+ else {
325
+ console.log(` inspect kgraph knowledge get ${item.atom.id}`);
326
+ }
327
+ }
328
+ if (items.length > visible.length) {
329
+ console.log(` review more ${items.length - visible.length} more atom(s)`);
330
+ }
331
+ }
@@ -23,6 +23,7 @@ export const DEFAULT_CONFIG = {
23
23
  '.agents',
24
24
  '.specify',
25
25
  'specs',
26
+ 'tmp',
26
27
  '.cursor',
27
28
  '.claude',
28
29
  '.windsurf',
@@ -209,6 +209,8 @@ export async function validateKnowledgeStore(workspace, maps) {
209
209
  const symbolNames = new Set(maps.symbolMap.symbols.map((symbol) => symbol.name));
210
210
  const symbolIds = new Set(maps.symbolMap.symbols.map((symbol) => symbol.id));
211
211
  for (const atom of atoms) {
212
+ if (atom.status === 'archived')
213
+ continue;
212
214
  for (const ref of atom.evidenceRefs) {
213
215
  if (ref.type === 'file') {
214
216
  const file = fileByPath.get(ref.path);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@kentwynn/kgraph",
3
- "version": "0.2.19",
3
+ "version": "0.2.21",
4
4
  "description": "Persistent repo intelligence for AI coding assistants.",
5
5
  "type": "module",
6
6
  "bin": {