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.
Files changed (47) hide show
  1. package/bin/kibi-mcp +12 -11
  2. package/dist/diagnostics.js +68 -8
  3. package/dist/env.js +5 -1
  4. package/dist/runtime/mcp-runtime.js +39 -3
  5. package/dist/server/docs.js +31 -42
  6. package/dist/server/icons.js +27 -0
  7. package/dist/server/jobs.js +107 -0
  8. package/dist/server/json-schema-to-zod.js +33 -9
  9. package/dist/server/session.js +129 -133
  10. package/dist/server/tool-registration.js +46 -9
  11. package/dist/server/tools-runtime.js +10 -10
  12. package/dist/server/tools.js +143 -7
  13. package/dist/server.js +16 -1
  14. package/dist/startup-resolution.js +15 -17
  15. package/dist/tools/check-impact.js +1 -1
  16. package/dist/tools/check.js +1 -1
  17. package/dist/tools/core-module.js +19 -8
  18. package/dist/tools/coverage.js +1 -1
  19. package/dist/tools/delete.js +1 -1
  20. package/dist/tools/discovery-adapter.js +5 -0
  21. package/dist/tools/entity-query.js +1 -1
  22. package/dist/tools/find-gaps.js +9 -1
  23. package/dist/tools/graph.js +9 -1
  24. package/dist/tools/model-requirement.js +2 -2
  25. package/dist/tools/mutation-context.js +15 -9
  26. package/dist/tools/plan-bootstrap.js +21 -0
  27. package/dist/tools/prolog-list.js +1 -1
  28. package/dist/tools/query.js +1 -1
  29. package/dist/tools/relationship-validation.js +1 -1
  30. package/dist/tools/search.js +1 -1
  31. package/dist/tools/semantic-advisor.js +1 -1
  32. package/dist/tools/skills.js +2 -2
  33. package/dist/tools/sparql.js +1 -1
  34. package/dist/tools/status.js +1 -1
  35. package/dist/tools/suggest-predicates.js +1 -1
  36. package/dist/tools/symbols.js +310 -68
  37. package/dist/tools/upsert.js +3 -3
  38. package/dist/tools/validate-upsert.js +1 -1
  39. package/dist/tools-config.js +77 -3
  40. package/dist/workspace.js +10 -8
  41. package/package.json +15 -9
  42. package/dist/tools/autopilot-candidates.js +0 -554
  43. package/dist/tools/autopilot-discovery.js +0 -898
  44. package/dist/tools/autopilot-generate.js +0 -47
  45. package/dist/tools/autopilot-test-compat.js +0 -88
  46. package/dist/tools/autopilot-test-parsers.js +0 -120
  47. package/dist/tools/check-config.js +0 -67
@@ -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 { access, readFile, writeFile } from "node:fs/promises";
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-cli/extractors/symbols-coordinator";
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 writeFile(coordinatesPath, nextCoordinates, "utf8");
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
- nextCoordinates[symbolId] = coordinatesRecord;
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
- if (coordinatesRecord || Object.keys(nextCoordinates).length > 0) {
148
- const nextContent = writeCoordinateArtifact(nextCoordinates);
149
- const currentContent = await readOptionalTextFile(coordinatesPath);
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 writeFile(coordinatesPath, nextContent, "utf8");
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-002, REQ-013
165
- const configPath = path.join(workspaceRoot, ".kb", "config.json");
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 (typeof entry.sourceLine === "number" &&
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
- return after;
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 || !title) {
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
- ...after,
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 after;
367
+ return null;
263
368
  }
264
- return after;
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
- typeof sourceEndColumn !== "number") {
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 = normalizeCoordinateRecord(coordinates[symbolId]);
436
+ const record = normalizeBoundCoordinateRecord(coordinates[symbolId]);
293
437
  if (!record) {
294
- continue;
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
- const parsed = parseYAML(content);
302
- if (!isRecord(parsed) || !isRecord(parsed.coordinates)) {
303
- return { coordinates: {} };
445
+ let parsed;
446
+ try {
447
+ parsed = parseYAML(content);
304
448
  }
305
- const coordinates = {};
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 = normalizeCoordinateRecord(record);
478
+ const normalizedRecord = normalizeBoundCoordinateRecord(record);
308
479
  if (!normalizedRecord) {
309
- continue;
480
+ throw new Error(`Coordinate artifact record '${symbolId}' is missing a valid identity/source binding or span`);
310
481
  }
311
- coordinates[symbolId] = normalizedRecord;
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({ coordinates: sortCoordinates(coordinates) }, {
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 buildCoordinatesMap(entries) {
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
- coordinates[id] = record;
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
- return null;
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
  }
@@ -1,6 +1,6 @@
1
- import { upsertSpec } from "kibi-cli/operations";
2
- import { setSymbolRefreshForTests } from "kibi-cli/operations/mutation/symbol-refresh";
3
- import { validateUpsertInput } from "kibi-cli/operations/mutation/validation";
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-cli/operations";
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;
@@ -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-cli/operations";
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
- "kb_autopilot_generate",
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
- ...(TOOL_ANNOTATIONS[name] ? { annotations: TOOL_ANNOTATIONS[name] } : {}),
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
  /**