@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([
|
|
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
|
-
|
|
153
|
-
|
|
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
|
|
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' ||
|
|
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' ||
|
|
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({
|
|
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({
|
|
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
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
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({
|
|
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)) ||
|
|
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' &&
|
|
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
|
-
|
|
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 });
|