@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 {
|
|
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
|
|
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
|
|
115
|
-
|
|
116
|
-
|
|
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
|
+
}
|
package/dist/config/config.js
CHANGED
|
@@ -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);
|