kibi-mcp 0.22.0 → 2.0.0
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.
- package/bin/kibi-mcp +12 -11
- package/dist/diagnostics.js +68 -8
- package/dist/env.js +5 -1
- package/dist/runtime/mcp-runtime.js +39 -3
- package/dist/server/docs.js +31 -42
- package/dist/server/icons.js +27 -0
- package/dist/server/jobs.js +107 -0
- package/dist/server/json-schema-to-zod.js +33 -9
- package/dist/server/session.js +129 -133
- package/dist/server/tool-registration.js +46 -9
- package/dist/server/tools-runtime.js +10 -10
- package/dist/server/tools.js +143 -7
- package/dist/server.js +16 -1
- package/dist/startup-resolution.js +15 -17
- package/dist/tools/check-impact.js +1 -1
- package/dist/tools/check.js +1 -1
- package/dist/tools/core-module.js +19 -8
- package/dist/tools/coverage.js +1 -1
- package/dist/tools/delete.js +1 -1
- package/dist/tools/discovery-adapter.js +5 -0
- package/dist/tools/entity-query.js +1 -1
- package/dist/tools/find-gaps.js +9 -1
- package/dist/tools/graph.js +9 -1
- package/dist/tools/model-requirement.js +2 -2
- package/dist/tools/mutation-context.js +15 -9
- package/dist/tools/plan-bootstrap.js +21 -0
- package/dist/tools/prolog-list.js +1 -1
- package/dist/tools/query.js +1 -1
- package/dist/tools/relationship-validation.js +1 -1
- package/dist/tools/search.js +1 -1
- package/dist/tools/semantic-advisor.js +1 -1
- package/dist/tools/skills.js +2 -2
- package/dist/tools/sparql.js +1 -1
- package/dist/tools/status.js +1 -1
- package/dist/tools/suggest-predicates.js +1 -1
- package/dist/tools/symbols.js +310 -68
- package/dist/tools/upsert.js +3 -3
- package/dist/tools/validate-upsert.js +1 -1
- package/dist/tools-config.js +77 -3
- package/dist/workspace.js +10 -8
- package/package.json +15 -9
- package/dist/tools/autopilot-candidates.js +0 -554
- package/dist/tools/autopilot-discovery.js +0 -898
- package/dist/tools/autopilot-generate.js +0 -47
- package/dist/tools/autopilot-test-compat.js +0 -88
- package/dist/tools/autopilot-test-parsers.js +0 -120
- package/dist/tools/check-config.js +0 -67
package/dist/tools/symbols.js
CHANGED
|
@@ -15,11 +15,13 @@
|
|
|
15
15
|
You should have received a copy of the GNU Affero General Public License
|
|
16
16
|
along with this program. If not, see <https://www.gnu.org/licenses/>.
|
|
17
17
|
*/
|
|
18
|
-
import {
|
|
18
|
+
import { createHash } from "node:crypto";
|
|
19
|
+
import { access, readFile, rename, unlink, writeFile } from "node:fs/promises";
|
|
19
20
|
import path from "node:path";
|
|
20
21
|
import { dump as dumpYAML, load as parseYAML } from "js-yaml";
|
|
21
|
-
import { enrichSymbolCoordinates, } from "kibi-
|
|
22
|
+
import { enrichSymbolCoordinates, withSymbolCompilerLock, } from "kibi-runtime";
|
|
22
23
|
import { resolveWorkspaceRoot } from "../workspace.js";
|
|
24
|
+
const SYMBOL_COORDINATES_FORMAT_VERSION = 2;
|
|
23
25
|
const SYMBOL_COORDINATES_COMMENT_BLOCK = `# symbol-coordinates.yaml
|
|
24
26
|
# GENERATED coordinate artifact — do not edit manually.
|
|
25
27
|
# Run \`kibi sync --refresh-symbol-coordinates\` to refresh.
|
|
@@ -41,11 +43,24 @@ const SOURCE_EXTENSIONS = new Set([
|
|
|
41
43
|
".mjs",
|
|
42
44
|
".cjs",
|
|
43
45
|
]);
|
|
46
|
+
const COARSE_GRANULARITY_REASONS = new Set([
|
|
47
|
+
"test-suite",
|
|
48
|
+
"extractor-miss",
|
|
49
|
+
"module-level-behavior",
|
|
50
|
+
"config-artifact",
|
|
51
|
+
]);
|
|
44
52
|
export async function handleKbSymbolsRefresh(args) {
|
|
53
|
+
const workspaceRoot = args.workspaceRoot ?? resolveWorkspaceRoot();
|
|
54
|
+
return withSymbolCompilerLock(workspaceRoot, () => handleKbSymbolsRefreshUnlocked(args, workspaceRoot));
|
|
55
|
+
}
|
|
56
|
+
async function handleKbSymbolsRefreshUnlocked(args, workspaceRoot) {
|
|
45
57
|
// implements REQ-vscode-traceability
|
|
46
58
|
const dryRun = args.dryRun === true;
|
|
47
|
-
const workspaceRoot = args.workspaceRoot ?? resolveWorkspaceRoot();
|
|
48
59
|
const { coordinatesPath, manifestPath } = await resolveManifestPaths(workspaceRoot);
|
|
60
|
+
const currentCoordinates = await readOptionalTextFile(coordinatesPath);
|
|
61
|
+
if (currentCoordinates !== null) {
|
|
62
|
+
readCoordinateArtifact(currentCoordinates);
|
|
63
|
+
}
|
|
49
64
|
const rawContent = await readFile(manifestPath, "utf8");
|
|
50
65
|
const parsed = parseYAML(rawContent);
|
|
51
66
|
if (!isRecord(parsed) || !Array.isArray(parsed.symbols)) {
|
|
@@ -86,10 +101,9 @@ export async function handleKbSymbolsRefresh(args) {
|
|
|
86
101
|
unchanged++;
|
|
87
102
|
}
|
|
88
103
|
}
|
|
89
|
-
const nextCoordinates = writeCoordinateArtifact(buildCoordinatesMap(finalized));
|
|
90
|
-
const currentCoordinates = await readOptionalTextFile(coordinatesPath);
|
|
104
|
+
const nextCoordinates = writeCoordinateArtifact(await buildCoordinatesMap(finalized, workspaceRoot));
|
|
91
105
|
if (!dryRun && currentCoordinates !== nextCoordinates) {
|
|
92
|
-
await
|
|
106
|
+
await publishCoordinateArtifact(coordinatesPath, nextCoordinates);
|
|
93
107
|
}
|
|
94
108
|
return {
|
|
95
109
|
content: [
|
|
@@ -107,6 +121,9 @@ export async function handleKbSymbolsRefresh(args) {
|
|
|
107
121
|
};
|
|
108
122
|
}
|
|
109
123
|
export async function refreshCoordinatesForSymbolId(symbolId, workspaceRoot = resolveWorkspaceRoot()) {
|
|
124
|
+
return withSymbolCompilerLock(workspaceRoot, () => refreshCoordinatesForSymbolIdUnlocked(symbolId, workspaceRoot));
|
|
125
|
+
}
|
|
126
|
+
async function refreshCoordinatesForSymbolIdUnlocked(symbolId, workspaceRoot) {
|
|
110
127
|
// implements REQ-vscode-traceability
|
|
111
128
|
const { coordinatesPath, manifestPath } = await resolveManifestPaths(workspaceRoot);
|
|
112
129
|
const rawContent = await readFile(manifestPath, "utf8");
|
|
@@ -137,18 +154,45 @@ export async function refreshCoordinatesForSymbolId(symbolId, workspaceRoot = re
|
|
|
137
154
|
parsed.symbols = symbols;
|
|
138
155
|
const refreshed = GENERATED_COORD_FIELDS.some((field) => original[field] !== finalized[field]);
|
|
139
156
|
const artifact = await readCoordinateArtifactFromPath(coordinatesPath);
|
|
157
|
+
const currentContent = await readOptionalTextFile(coordinatesPath);
|
|
140
158
|
const nextCoordinates = {
|
|
141
159
|
...artifact.coordinates,
|
|
142
160
|
};
|
|
161
|
+
if (artifact.status === "legacy") {
|
|
162
|
+
for (const [existingId, existingRecord] of Object.entries(nextCoordinates)) {
|
|
163
|
+
const existingManifestEntry = symbols.find((entry) => entry.id === existingId);
|
|
164
|
+
if (!existingManifestEntry ||
|
|
165
|
+
!(await legacyRecordMatchesCurrentExtraction(existingManifestEntry, existingRecord, workspaceRoot))) {
|
|
166
|
+
delete nextCoordinates[existingId];
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
}
|
|
143
170
|
const coordinatesRecord = toCoordinateRecord(finalized);
|
|
144
171
|
if (coordinatesRecord) {
|
|
145
|
-
|
|
172
|
+
const sourceText = await readSourceText(coordinatesRecord.sourceFile, workspaceRoot);
|
|
173
|
+
if (sourceText !== null) {
|
|
174
|
+
nextCoordinates[symbolId] =
|
|
175
|
+
artifact.status === "legacy" && currentContent !== null
|
|
176
|
+
? coordinatesRecord
|
|
177
|
+
: {
|
|
178
|
+
...coordinatesRecord,
|
|
179
|
+
identityHash: coordinateIdentityHash(finalized),
|
|
180
|
+
sourceHash: coordinateSourceHash(sourceText),
|
|
181
|
+
};
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
else if (nextCoordinates[symbolId] !== undefined) {
|
|
185
|
+
delete nextCoordinates[symbolId];
|
|
146
186
|
}
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
187
|
+
const removedExistingRecord = coordinatesRecord === null && artifact.coordinates[symbolId] !== undefined;
|
|
188
|
+
if (coordinatesRecord ||
|
|
189
|
+
Object.keys(nextCoordinates).length > 0 ||
|
|
190
|
+
removedExistingRecord) {
|
|
191
|
+
const nextContent = artifact.status === "legacy" && currentContent !== null
|
|
192
|
+
? writeLegacyCoordinateArtifact(nextCoordinates)
|
|
193
|
+
: writeCoordinateArtifact(nextCoordinates);
|
|
150
194
|
if (currentContent !== nextContent) {
|
|
151
|
-
await
|
|
195
|
+
await publishCoordinateArtifact(coordinatesPath, nextContent);
|
|
152
196
|
}
|
|
153
197
|
}
|
|
154
198
|
return { refreshed, found: true };
|
|
@@ -161,42 +205,11 @@ async function resolveManifestPaths(workspaceRoot) {
|
|
|
161
205
|
};
|
|
162
206
|
}
|
|
163
207
|
export async function resolveManifestPath(workspaceRoot) {
|
|
164
|
-
// implements REQ-
|
|
165
|
-
|
|
166
|
-
try {
|
|
167
|
-
const config = JSON.parse(await readFile(configPath, "utf8"));
|
|
168
|
-
// Prefer paths.symbols (new standard) over symbolsManifest (legacy)
|
|
169
|
-
if (config.paths?.symbols) {
|
|
170
|
-
return path.isAbsolute(config.paths.symbols)
|
|
171
|
-
? config.paths.symbols
|
|
172
|
-
: path.resolve(workspaceRoot, config.paths.symbols);
|
|
173
|
-
}
|
|
174
|
-
// Backward compatibility: check legacy symbolsManifest field
|
|
175
|
-
if (config.symbolsManifest) {
|
|
176
|
-
return path.isAbsolute(config.symbolsManifest)
|
|
177
|
-
? config.symbolsManifest
|
|
178
|
-
: path.resolve(workspaceRoot, config.symbolsManifest);
|
|
179
|
-
}
|
|
180
|
-
}
|
|
181
|
-
catch {
|
|
182
|
-
// config file missing or malformed; fall through to defaults
|
|
183
|
-
}
|
|
184
|
-
const candidates = [
|
|
185
|
-
path.join(workspaceRoot, "symbols.yaml"),
|
|
186
|
-
path.join(workspaceRoot, "symbols.yml"),
|
|
187
|
-
];
|
|
188
|
-
for (const candidate of candidates) {
|
|
189
|
-
if (await fileExists(candidate)) {
|
|
190
|
-
return candidate;
|
|
191
|
-
}
|
|
192
|
-
}
|
|
193
|
-
return candidates[0] ?? path.join(workspaceRoot, "symbols.yaml");
|
|
208
|
+
// implements REQ-cli-canonical-runtime, REQ-vscode-traceability
|
|
209
|
+
return path.join(workspaceRoot, ".kb", "symbols.yaml");
|
|
194
210
|
}
|
|
195
211
|
function hasGeneratedCoordinates(entry) {
|
|
196
|
-
return (
|
|
197
|
-
typeof entry.sourceColumn === "number" &&
|
|
198
|
-
typeof entry.sourceEndLine === "number" &&
|
|
199
|
-
typeof entry.sourceEndColumn === "number");
|
|
212
|
+
return normalizeCoordinateRecord(entry) !== null;
|
|
200
213
|
}
|
|
201
214
|
async function isEligible(sourceFile, workspaceRoot) {
|
|
202
215
|
if (!sourceFile)
|
|
@@ -219,7 +232,13 @@ async function fileExists(filePath) {
|
|
|
219
232
|
}
|
|
220
233
|
async function fillMissingCoordinates(before, after, workspaceRoot) {
|
|
221
234
|
if (hasGeneratedCoordinates(after)) {
|
|
222
|
-
|
|
235
|
+
const coordinate = toCoordinateRecord(after);
|
|
236
|
+
if (coordinate &&
|
|
237
|
+
(isCoarseAnchor(after) ||
|
|
238
|
+
(await coordinateMatchesCurrentDeclaration(after, coordinate, workspaceRoot)))) {
|
|
239
|
+
return after;
|
|
240
|
+
}
|
|
241
|
+
return withoutGeneratedCoordinates(after);
|
|
223
242
|
}
|
|
224
243
|
const sourceFile = typeof after.sourceFile === "string"
|
|
225
244
|
? after.sourceFile
|
|
@@ -231,7 +250,7 @@ async function fillMissingCoordinates(before, after, workspaceRoot) {
|
|
|
231
250
|
: typeof before.title === "string"
|
|
232
251
|
? before.title
|
|
233
252
|
: undefined;
|
|
234
|
-
if (!sourceFile
|
|
253
|
+
if (!sourceFile) {
|
|
235
254
|
return after;
|
|
236
255
|
}
|
|
237
256
|
const absolutePath = path.isAbsolute(sourceFile)
|
|
@@ -239,6 +258,40 @@ async function fillMissingCoordinates(before, after, workspaceRoot) {
|
|
|
239
258
|
: path.resolve(workspaceRoot, sourceFile);
|
|
240
259
|
try {
|
|
241
260
|
const content = await readFile(absolutePath, "utf8");
|
|
261
|
+
const lines = content.split(/\r?\n/);
|
|
262
|
+
if (isCoarseAnchor(after)) {
|
|
263
|
+
return {
|
|
264
|
+
...after,
|
|
265
|
+
...coarseCoordinateSpan(sourceFile, title ?? "", content),
|
|
266
|
+
};
|
|
267
|
+
}
|
|
268
|
+
if (title) {
|
|
269
|
+
const escapedTitle = title.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
270
|
+
const pattern = new RegExp(`\\b${escapedTitle}\\b`);
|
|
271
|
+
for (let index = 0; index < lines.length; index++) {
|
|
272
|
+
const line = lines[index];
|
|
273
|
+
if (!line)
|
|
274
|
+
continue;
|
|
275
|
+
const match = pattern.exec(line);
|
|
276
|
+
if (!match || match.index < 0)
|
|
277
|
+
continue;
|
|
278
|
+
return {
|
|
279
|
+
...after,
|
|
280
|
+
sourceLine: index + 1,
|
|
281
|
+
sourceColumn: match.index,
|
|
282
|
+
sourceEndLine: index + 1,
|
|
283
|
+
sourceEndColumn: match.index + title.length,
|
|
284
|
+
};
|
|
285
|
+
}
|
|
286
|
+
}
|
|
287
|
+
}
|
|
288
|
+
catch {
|
|
289
|
+
return after;
|
|
290
|
+
}
|
|
291
|
+
return after;
|
|
292
|
+
}
|
|
293
|
+
function coarseCoordinateSpan(sourceFile, title, content) {
|
|
294
|
+
if (title.length > 0) {
|
|
242
295
|
const escapedTitle = title.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
243
296
|
const pattern = new RegExp(`\\b${escapedTitle}\\b`);
|
|
244
297
|
const lines = content.split(/\r?\n/);
|
|
@@ -250,7 +303,7 @@ async function fillMissingCoordinates(before, after, workspaceRoot) {
|
|
|
250
303
|
if (!match || match.index < 0)
|
|
251
304
|
continue;
|
|
252
305
|
return {
|
|
253
|
-
|
|
306
|
+
sourceFile,
|
|
254
307
|
sourceLine: index + 1,
|
|
255
308
|
sourceColumn: match.index,
|
|
256
309
|
sourceEndLine: index + 1,
|
|
@@ -258,10 +311,77 @@ async function fillMissingCoordinates(before, after, workspaceRoot) {
|
|
|
258
311
|
};
|
|
259
312
|
}
|
|
260
313
|
}
|
|
314
|
+
const lines = content.split(/\r?\n/);
|
|
315
|
+
const lastLine = lines[lines.length - 1] ?? "";
|
|
316
|
+
return {
|
|
317
|
+
sourceFile,
|
|
318
|
+
sourceLine: 1,
|
|
319
|
+
sourceColumn: 0,
|
|
320
|
+
sourceEndLine: Math.max(1, lines.length),
|
|
321
|
+
sourceEndColumn: lastLine.length,
|
|
322
|
+
};
|
|
323
|
+
}
|
|
324
|
+
function isCoarseAnchor(entry) {
|
|
325
|
+
return (typeof entry.granularity_reason === "string" &&
|
|
326
|
+
COARSE_GRANULARITY_REASONS.has(entry.granularity_reason));
|
|
327
|
+
}
|
|
328
|
+
function withoutGeneratedCoordinates(entry) {
|
|
329
|
+
const next = { ...entry };
|
|
330
|
+
for (const field of [
|
|
331
|
+
"sourceLine",
|
|
332
|
+
"sourceColumn",
|
|
333
|
+
"sourceEndLine",
|
|
334
|
+
"sourceEndColumn",
|
|
335
|
+
"coordinatesGeneratedAt",
|
|
336
|
+
]) {
|
|
337
|
+
delete next[field];
|
|
338
|
+
}
|
|
339
|
+
return next;
|
|
340
|
+
}
|
|
341
|
+
function coordinateIdentityKey(entry) {
|
|
342
|
+
return [
|
|
343
|
+
typeof entry.id === "string" ? entry.id : "",
|
|
344
|
+
typeof entry.title === "string" ? entry.title : "",
|
|
345
|
+
typeof entry.sourceFile === "string" ? entry.sourceFile : "",
|
|
346
|
+
typeof entry.granularity_reason === "string"
|
|
347
|
+
? entry.granularity_reason
|
|
348
|
+
: "",
|
|
349
|
+
].join("\u0000");
|
|
350
|
+
}
|
|
351
|
+
function coordinateIdentityHash(entry) {
|
|
352
|
+
return createHash("sha256")
|
|
353
|
+
.update(coordinateIdentityKey(entry))
|
|
354
|
+
.digest("hex");
|
|
355
|
+
}
|
|
356
|
+
function coordinateSourceHash(sourceText) {
|
|
357
|
+
return createHash("sha256").update(sourceText).digest("hex");
|
|
358
|
+
}
|
|
359
|
+
async function readSourceText(sourceFile, workspaceRoot) {
|
|
360
|
+
const absolutePath = path.isAbsolute(sourceFile)
|
|
361
|
+
? sourceFile
|
|
362
|
+
: path.resolve(workspaceRoot, sourceFile);
|
|
363
|
+
try {
|
|
364
|
+
return await readFile(absolutePath, "utf8");
|
|
365
|
+
}
|
|
261
366
|
catch {
|
|
262
|
-
return
|
|
367
|
+
return null;
|
|
263
368
|
}
|
|
264
|
-
|
|
369
|
+
}
|
|
370
|
+
async function coordinateMatchesCurrentDeclaration(entry, coordinate, workspaceRoot) {
|
|
371
|
+
const sourceFile = typeof entry.sourceFile === "string" ? entry.sourceFile : undefined;
|
|
372
|
+
const title = typeof entry.title === "string" ? entry.title : "";
|
|
373
|
+
if (!sourceFile || sourceFile !== coordinate.sourceFile || !title) {
|
|
374
|
+
return false;
|
|
375
|
+
}
|
|
376
|
+
const content = await readSourceText(coordinate.sourceFile, workspaceRoot);
|
|
377
|
+
if (content === null)
|
|
378
|
+
return false;
|
|
379
|
+
const declarationName = title.slice(title.lastIndexOf(".") + 1);
|
|
380
|
+
const line = content.split(/\r?\n/)[coordinate.sourceLine - 1];
|
|
381
|
+
return (declarationName.length > 0 &&
|
|
382
|
+
line !== undefined &&
|
|
383
|
+
coordinate.sourceColumn <= line.length - declarationName.length &&
|
|
384
|
+
line.slice(coordinate.sourceColumn, coordinate.sourceColumn + declarationName.length) === declarationName);
|
|
265
385
|
}
|
|
266
386
|
function isRecord(value) {
|
|
267
387
|
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
@@ -272,10 +392,19 @@ function normalizeCoordinateRecord(value) {
|
|
|
272
392
|
}
|
|
273
393
|
const { sourceColumn, sourceEndColumn, sourceEndLine, sourceFile, sourceLine, } = value;
|
|
274
394
|
if (typeof sourceFile !== "string" ||
|
|
395
|
+
sourceFile.length === 0 ||
|
|
275
396
|
typeof sourceLine !== "number" ||
|
|
397
|
+
!Number.isInteger(sourceLine) ||
|
|
398
|
+
sourceLine < 1 ||
|
|
276
399
|
typeof sourceColumn !== "number" ||
|
|
400
|
+
!Number.isInteger(sourceColumn) ||
|
|
401
|
+
sourceColumn < 0 ||
|
|
277
402
|
typeof sourceEndLine !== "number" ||
|
|
278
|
-
|
|
403
|
+
!Number.isInteger(sourceEndLine) ||
|
|
404
|
+
sourceEndLine < sourceLine ||
|
|
405
|
+
typeof sourceEndColumn !== "number" ||
|
|
406
|
+
!Number.isInteger(sourceEndColumn) ||
|
|
407
|
+
sourceEndColumn < 0) {
|
|
279
408
|
return null;
|
|
280
409
|
}
|
|
281
410
|
return {
|
|
@@ -286,43 +415,127 @@ function normalizeCoordinateRecord(value) {
|
|
|
286
415
|
sourceEndColumn,
|
|
287
416
|
};
|
|
288
417
|
}
|
|
418
|
+
function isSha256Hash(value) {
|
|
419
|
+
return typeof value === "string" && /^[a-f0-9]{64}$/.test(value);
|
|
420
|
+
}
|
|
421
|
+
function normalizeBoundCoordinateRecord(value) {
|
|
422
|
+
if (!isRecord(value))
|
|
423
|
+
return null;
|
|
424
|
+
const record = normalizeCoordinateRecord(value);
|
|
425
|
+
const { identityHash, sourceHash } = value;
|
|
426
|
+
if (record === null ||
|
|
427
|
+
!isSha256Hash(identityHash) ||
|
|
428
|
+
!isSha256Hash(sourceHash)) {
|
|
429
|
+
return null;
|
|
430
|
+
}
|
|
431
|
+
return { ...record, identityHash, sourceHash };
|
|
432
|
+
}
|
|
289
433
|
function sortCoordinates(coordinates) {
|
|
290
434
|
const sortedCoordinates = {};
|
|
291
435
|
for (const symbolId of Object.keys(coordinates).sort((left, right) => left.localeCompare(right))) {
|
|
292
|
-
const record =
|
|
436
|
+
const record = normalizeBoundCoordinateRecord(coordinates[symbolId]);
|
|
293
437
|
if (!record) {
|
|
294
|
-
|
|
438
|
+
throw new Error(`Invalid bound coordinate record for ${symbolId}; refusing to publish`);
|
|
295
439
|
}
|
|
296
440
|
sortedCoordinates[symbolId] = record;
|
|
297
441
|
}
|
|
298
442
|
return sortedCoordinates;
|
|
299
443
|
}
|
|
300
444
|
function readCoordinateArtifact(content) {
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
445
|
+
let parsed;
|
|
446
|
+
try {
|
|
447
|
+
parsed = parseYAML(content);
|
|
304
448
|
}
|
|
305
|
-
|
|
449
|
+
catch (error) {
|
|
450
|
+
throw new Error(`Failed to parse coordinate artifact: ${error instanceof Error ? error.message : String(error)}`);
|
|
451
|
+
}
|
|
452
|
+
if (parsed === null || parsed === undefined) {
|
|
453
|
+
return { status: "legacy", coordinates: {} };
|
|
454
|
+
}
|
|
455
|
+
if (!isRecord(parsed)) {
|
|
456
|
+
throw new Error("Coordinate artifact root must be a mapping");
|
|
457
|
+
}
|
|
458
|
+
if (!isRecord(parsed.coordinates)) {
|
|
459
|
+
throw new Error("Coordinate artifact coordinates must be a mapping");
|
|
460
|
+
}
|
|
461
|
+
const declaredVersion = parsed.version;
|
|
462
|
+
if (declaredVersion === undefined) {
|
|
463
|
+
const coordinates = {};
|
|
464
|
+
for (const [symbolId, record] of Object.entries(parsed.coordinates)) {
|
|
465
|
+
const normalizedRecord = normalizeCoordinateRecord(record);
|
|
466
|
+
if (!normalizedRecord) {
|
|
467
|
+
throw new Error(`Coordinate artifact record '${symbolId}' has an invalid coordinate span`);
|
|
468
|
+
}
|
|
469
|
+
coordinates[symbolId] = normalizedRecord;
|
|
470
|
+
}
|
|
471
|
+
return { status: "legacy", coordinates };
|
|
472
|
+
}
|
|
473
|
+
if (declaredVersion !== SYMBOL_COORDINATES_FORMAT_VERSION) {
|
|
474
|
+
throw new Error(`Unsupported coordinate artifact version: ${String(declaredVersion)}`);
|
|
475
|
+
}
|
|
476
|
+
const boundCoordinates = {};
|
|
306
477
|
for (const [symbolId, record] of Object.entries(parsed.coordinates)) {
|
|
307
|
-
const normalizedRecord =
|
|
478
|
+
const normalizedRecord = normalizeBoundCoordinateRecord(record);
|
|
308
479
|
if (!normalizedRecord) {
|
|
309
|
-
|
|
480
|
+
throw new Error(`Coordinate artifact record '${symbolId}' is missing a valid identity/source binding or span`);
|
|
310
481
|
}
|
|
311
|
-
|
|
482
|
+
boundCoordinates[symbolId] = normalizedRecord;
|
|
312
483
|
}
|
|
313
|
-
return { coordinates };
|
|
484
|
+
return { status: "parsed", coordinates: boundCoordinates };
|
|
314
485
|
}
|
|
315
486
|
function writeCoordinateArtifact(coordinates) {
|
|
316
|
-
return `${SYMBOL_COORDINATES_COMMENT_BLOCK}${dumpYAML({
|
|
487
|
+
return `${SYMBOL_COORDINATES_COMMENT_BLOCK}${dumpYAML({
|
|
488
|
+
version: SYMBOL_COORDINATES_FORMAT_VERSION,
|
|
489
|
+
coordinates: sortCoordinates(coordinates),
|
|
490
|
+
}, {
|
|
317
491
|
lineWidth: -1,
|
|
318
492
|
noRefs: true,
|
|
319
|
-
sortKeys: true,
|
|
320
493
|
})}`;
|
|
321
494
|
}
|
|
495
|
+
function writeLegacyCoordinateArtifact(coordinates) {
|
|
496
|
+
const sortedCoordinates = {};
|
|
497
|
+
for (const symbolId of Object.keys(coordinates).sort((left, right) => left.localeCompare(right))) {
|
|
498
|
+
const record = normalizeCoordinateRecord(coordinates[symbolId]);
|
|
499
|
+
if (!record) {
|
|
500
|
+
throw new Error(`Invalid legacy coordinate record for ${symbolId}; refusing to publish`);
|
|
501
|
+
}
|
|
502
|
+
sortedCoordinates[symbolId] = record;
|
|
503
|
+
}
|
|
504
|
+
return `${SYMBOL_COORDINATES_COMMENT_BLOCK}${dumpYAML({ coordinates: sortedCoordinates }, { lineWidth: -1, noRefs: true })}`;
|
|
505
|
+
}
|
|
322
506
|
function toCoordinateRecord(entry) {
|
|
323
507
|
return normalizeCoordinateRecord(entry);
|
|
324
508
|
}
|
|
325
|
-
function
|
|
509
|
+
async function legacyRecordMatchesCurrentExtraction(record, coordinate, workspaceRoot) {
|
|
510
|
+
if (!normalizeCoordinateRecord(coordinate))
|
|
511
|
+
return false;
|
|
512
|
+
const sourceFile = typeof record.sourceFile === "string" ? record.sourceFile : undefined;
|
|
513
|
+
const title = typeof record.title === "string" ? record.title : "";
|
|
514
|
+
if (!sourceFile)
|
|
515
|
+
return false;
|
|
516
|
+
const content = await readSourceText(coordinate.sourceFile, workspaceRoot);
|
|
517
|
+
if (content === null || coordinate.sourceFile !== sourceFile)
|
|
518
|
+
return false;
|
|
519
|
+
if (isCoarseAnchor(record)) {
|
|
520
|
+
return coordinatesEqual(coordinate, coarseCoordinateSpan(sourceFile, title, content));
|
|
521
|
+
}
|
|
522
|
+
if (!title)
|
|
523
|
+
return false;
|
|
524
|
+
const declarationName = title.slice(title.lastIndexOf(".") + 1);
|
|
525
|
+
const line = content.split(/\r?\n/)[coordinate.sourceLine - 1];
|
|
526
|
+
return (declarationName.length > 0 &&
|
|
527
|
+
line !== undefined &&
|
|
528
|
+
coordinate.sourceColumn <= line.length - declarationName.length &&
|
|
529
|
+
line.slice(coordinate.sourceColumn, coordinate.sourceColumn + declarationName.length) === declarationName);
|
|
530
|
+
}
|
|
531
|
+
function coordinatesEqual(left, right) {
|
|
532
|
+
return (left.sourceFile === right.sourceFile &&
|
|
533
|
+
left.sourceLine === right.sourceLine &&
|
|
534
|
+
left.sourceColumn === right.sourceColumn &&
|
|
535
|
+
left.sourceEndLine === right.sourceEndLine &&
|
|
536
|
+
left.sourceEndColumn === right.sourceEndColumn);
|
|
537
|
+
}
|
|
538
|
+
async function buildCoordinatesMap(entries, workspaceRoot) {
|
|
326
539
|
const coordinates = {};
|
|
327
540
|
for (const entry of entries) {
|
|
328
541
|
const id = typeof entry.id === "string" ? entry.id : undefined;
|
|
@@ -330,14 +543,21 @@ function buildCoordinatesMap(entries) {
|
|
|
330
543
|
if (!id || !record) {
|
|
331
544
|
continue;
|
|
332
545
|
}
|
|
333
|
-
|
|
546
|
+
const sourceText = await readSourceText(record.sourceFile, workspaceRoot);
|
|
547
|
+
if (sourceText === null)
|
|
548
|
+
continue;
|
|
549
|
+
coordinates[id] = {
|
|
550
|
+
...record,
|
|
551
|
+
identityHash: coordinateIdentityHash(entry),
|
|
552
|
+
sourceHash: coordinateSourceHash(sourceText),
|
|
553
|
+
};
|
|
334
554
|
}
|
|
335
555
|
return coordinates;
|
|
336
556
|
}
|
|
337
557
|
async function readCoordinateArtifactFromPath(coordinatesPath) {
|
|
338
558
|
const content = await readOptionalTextFile(coordinatesPath);
|
|
339
559
|
if (content === null) {
|
|
340
|
-
return { coordinates: {} };
|
|
560
|
+
return { status: "legacy", coordinates: {} };
|
|
341
561
|
}
|
|
342
562
|
return readCoordinateArtifact(content);
|
|
343
563
|
}
|
|
@@ -345,7 +565,29 @@ async function readOptionalTextFile(filePath) {
|
|
|
345
565
|
try {
|
|
346
566
|
return await readFile(filePath, "utf8");
|
|
347
567
|
}
|
|
348
|
-
catch {
|
|
349
|
-
|
|
568
|
+
catch (error) {
|
|
569
|
+
if (error !== null &&
|
|
570
|
+
typeof error === "object" &&
|
|
571
|
+
"code" in error &&
|
|
572
|
+
error.code === "ENOENT") {
|
|
573
|
+
return null;
|
|
574
|
+
}
|
|
575
|
+
throw error;
|
|
576
|
+
}
|
|
577
|
+
}
|
|
578
|
+
async function publishCoordinateArtifact(targetPath, content) {
|
|
579
|
+
const temporary = `${targetPath}.kibi-tmp-${process.pid}`;
|
|
580
|
+
try {
|
|
581
|
+
await writeFile(temporary, content, "utf8");
|
|
582
|
+
await rename(temporary, targetPath);
|
|
583
|
+
}
|
|
584
|
+
catch (error) {
|
|
585
|
+
try {
|
|
586
|
+
await unlink(temporary);
|
|
587
|
+
}
|
|
588
|
+
catch {
|
|
589
|
+
// Preserve the publication failure; cleanup is best effort.
|
|
590
|
+
}
|
|
591
|
+
throw error;
|
|
350
592
|
}
|
|
351
593
|
}
|
package/dist/tools/upsert.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
import { upsertSpec } from "kibi-
|
|
2
|
-
import { setSymbolRefreshForTests } from "kibi-
|
|
3
|
-
import { validateUpsertInput } from "kibi-
|
|
1
|
+
import { upsertSpec } from "kibi-runtime";
|
|
2
|
+
import { setSymbolRefreshForTests } from "kibi-runtime";
|
|
3
|
+
import { validateUpsertInput } from "kibi-runtime";
|
|
4
4
|
import { isMcpDebugEnabled } from "../env.js";
|
|
5
5
|
import { createMutationContext } from "./mutation-context.js";
|
|
6
6
|
export async function handleKbUpsert(prolog, args) {
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { validateUpsertSpec } from "kibi-
|
|
1
|
+
import { validateUpsertSpec } from "kibi-runtime";
|
|
2
2
|
import { createMutationContext } from "./mutation-context.js";
|
|
3
3
|
function isUpsertArgs(value) {
|
|
4
4
|
return "type" in value && "id" in value && "properties" in value;
|
package/dist/tools-config.js
CHANGED
|
@@ -15,7 +15,7 @@
|
|
|
15
15
|
You should have received a copy of the GNU Affero General Public License
|
|
16
16
|
along with this program. If not, see <https://www.gnu.org/licenses/>.
|
|
17
17
|
*/
|
|
18
|
-
import { getSpec } from "kibi-
|
|
18
|
+
import { getSpec } from "kibi-runtime";
|
|
19
19
|
import { DIAGNOSTIC_MODE_ENABLED, DIAGNOSTIC_TELEMETRY_SCHEMA, } from "./diagnostics.js";
|
|
20
20
|
const MCP_TOOL_ORDER = [
|
|
21
21
|
"kb_query",
|
|
@@ -35,10 +35,19 @@ const MCP_TOOL_ORDER = [
|
|
|
35
35
|
"kb_check",
|
|
36
36
|
"kb_model_requirement",
|
|
37
37
|
"kb_suggest_predicates",
|
|
38
|
-
"
|
|
38
|
+
"kb_plan_bootstrap",
|
|
39
|
+
"kb_compile_intent",
|
|
40
|
+
"kb_apply_plan",
|
|
41
|
+
"kb_ingest_proof",
|
|
39
42
|
];
|
|
40
43
|
// implements REQ-002
|
|
41
44
|
const TOOL_ANNOTATIONS = {
|
|
45
|
+
kb_query: {
|
|
46
|
+
title: "Query Kibi entities",
|
|
47
|
+
},
|
|
48
|
+
kb_search: {
|
|
49
|
+
title: "Search Kibi knowledge base",
|
|
50
|
+
},
|
|
42
51
|
kb_status: {
|
|
43
52
|
title: "Inspect Kibi branch status",
|
|
44
53
|
readOnlyHint: true,
|
|
@@ -67,6 +76,18 @@ const TOOL_ANNOTATIONS = {
|
|
|
67
76
|
idempotentHint: true,
|
|
68
77
|
openWorldHint: false,
|
|
69
78
|
},
|
|
79
|
+
kb_find_gaps: {
|
|
80
|
+
title: "Find missing Kibi relationships",
|
|
81
|
+
},
|
|
82
|
+
kb_coverage: {
|
|
83
|
+
title: "Report Kibi coverage and proofs",
|
|
84
|
+
},
|
|
85
|
+
kb_graph: {
|
|
86
|
+
title: "Traverse Kibi graph",
|
|
87
|
+
},
|
|
88
|
+
kb_sparql_remote: {
|
|
89
|
+
title: "Run a remote SPARQL query",
|
|
90
|
+
},
|
|
70
91
|
kb_semantic_advisor: {
|
|
71
92
|
title: "Advise on Kibi requirement modeling",
|
|
72
93
|
readOnlyHint: true,
|
|
@@ -81,14 +102,67 @@ const TOOL_ANNOTATIONS = {
|
|
|
81
102
|
idempotentHint: true,
|
|
82
103
|
openWorldHint: false,
|
|
83
104
|
},
|
|
105
|
+
kb_upsert: {
|
|
106
|
+
title: "Create or update Kibi entities",
|
|
107
|
+
},
|
|
108
|
+
kb_validate_upsert: {
|
|
109
|
+
title: "Validate a Kibi upsert payload",
|
|
110
|
+
},
|
|
111
|
+
kb_delete: {
|
|
112
|
+
title: "Delete a Kibi entity",
|
|
113
|
+
},
|
|
114
|
+
kb_check: {
|
|
115
|
+
title: "Validate Kibi knowledge base",
|
|
116
|
+
},
|
|
117
|
+
kb_model_requirement: {
|
|
118
|
+
title: "Model a Kibi requirement",
|
|
119
|
+
},
|
|
120
|
+
kb_compile_intent: {
|
|
121
|
+
title: "Compile Kibi change intent",
|
|
122
|
+
readOnlyHint: true,
|
|
123
|
+
destructiveHint: false,
|
|
124
|
+
idempotentHint: true,
|
|
125
|
+
openWorldHint: false,
|
|
126
|
+
},
|
|
127
|
+
kb_plan_bootstrap: {
|
|
128
|
+
title: "Plan Kibi bootstrap",
|
|
129
|
+
},
|
|
130
|
+
kb_apply_plan: {
|
|
131
|
+
title: "Apply an approved Kibi plan",
|
|
132
|
+
readOnlyHint: false,
|
|
133
|
+
destructiveHint: false,
|
|
134
|
+
idempotentHint: false,
|
|
135
|
+
openWorldHint: false,
|
|
136
|
+
},
|
|
137
|
+
kb_ingest_proof: {
|
|
138
|
+
title: "Ingest proof-run evidence",
|
|
139
|
+
readOnlyHint: false,
|
|
140
|
+
destructiveHint: false,
|
|
141
|
+
idempotentHint: false,
|
|
142
|
+
openWorldHint: false,
|
|
143
|
+
},
|
|
84
144
|
};
|
|
85
145
|
const BASE_TOOLS = MCP_TOOL_ORDER.map((name) => {
|
|
86
146
|
const spec = getSpec(name);
|
|
147
|
+
const effects = spec.declaredEffects;
|
|
148
|
+
const derived = {
|
|
149
|
+
readOnlyHint: effects.every((effect) => effect.mutability === "read"),
|
|
150
|
+
destructiveHint: effects.some((effect) => effect.destructive),
|
|
151
|
+
idempotentHint: effects.every((effect) => effect.retrySafety === "safe"),
|
|
152
|
+
openWorldHint: effects.some((effect) => effect.openWorld),
|
|
153
|
+
};
|
|
87
154
|
return {
|
|
88
155
|
name: spec.name,
|
|
89
156
|
description: spec.description,
|
|
90
157
|
inputSchema: spec.businessInputSchema,
|
|
91
|
-
...(
|
|
158
|
+
...(spec.outputSchema ? { outputSchema: spec.outputSchema } : {}),
|
|
159
|
+
annotations: {
|
|
160
|
+
...(TOOL_ANNOTATIONS[name] ?? {}),
|
|
161
|
+
// Mutability and world-model hints are generated from the authoritative
|
|
162
|
+
// operation effects. Hand-authored entries may supply presentation-only
|
|
163
|
+
// metadata such as a title, but cannot contradict the catalog contract.
|
|
164
|
+
...derived,
|
|
165
|
+
},
|
|
92
166
|
};
|
|
93
167
|
});
|
|
94
168
|
/**
|