kibi-mcp 0.15.0 → 0.15.1

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.
@@ -16,6 +16,9 @@
16
16
  along with this program. If not, see <https://www.gnu.org/licenses/>.
17
17
  */
18
18
  import Ajv from "ajv";
19
+ import { existsSync, readFileSync } from "node:fs";
20
+ import path from "node:path";
21
+ import { Project, ScriptKind } from "ts-morph";
19
22
  import { escapeAtom, toPrologAtom, toPrologString, } from "kibi-cli/prolog/codec";
20
23
  import entitySchema from "kibi-cli/schemas/entity";
21
24
  import relationshipSchema from "kibi-cli/schemas/relationship";
@@ -23,8 +26,40 @@ import { isMcpDebugEnabled } from "../env.js";
23
26
  import { refreshCoordinatesForSymbolId } from "./symbols.js";
24
27
  let refreshCoordinatesForSymbolIdImpl = refreshCoordinatesForSymbolId;
25
28
  const ajv = new Ajv({ strict: false });
26
- const validateEntity = ajv.compile(entitySchema);
29
+ const entitySchemaRecord = entitySchema;
30
+ const entitySchemaProperties = entitySchemaRecord.properties;
31
+ const normalizedEntitySchemaProperties = entitySchemaProperties !== null &&
32
+ typeof entitySchemaProperties === "object" &&
33
+ !Array.isArray(entitySchemaProperties)
34
+ ? entitySchemaProperties
35
+ : {};
36
+ const validateEntity = ajv.compile({
37
+ ...entitySchemaRecord,
38
+ properties: {
39
+ ...normalizedEntitySchemaProperties,
40
+ granularity_reason: {
41
+ type: "string",
42
+ enum: [
43
+ "config-artifact",
44
+ "module-level-behavior",
45
+ "extractor-miss",
46
+ "legacy-link",
47
+ ],
48
+ },
49
+ },
50
+ });
27
51
  const validateRelationship = ajv.compile(relationshipSchema);
52
+ const TRACEABILITY_RELATIONSHIP_TYPES = new Set([
53
+ "implements",
54
+ "covered_by",
55
+ "executable_for",
56
+ ]);
57
+ const ALLOWED_GRANULARITY_REASONS = new Set([
58
+ "config-artifact",
59
+ "module-level-behavior",
60
+ "extractor-miss",
61
+ "legacy-link",
62
+ ]);
28
63
  /**
29
64
  * Handle kb.upsert tool calls
30
65
  * Accepts { type, id, properties } — the flat format matching the tool schema.
@@ -76,6 +111,7 @@ export async function handleKbUpsert(prolog, args) {
76
111
  }
77
112
  }
78
113
  validateRelationshipSources(id, relationships);
114
+ validateSymbolGranularity(entity, relationships);
79
115
  // Validate strict-lane fact_kind pairing for constrains/requires_property
80
116
  // implements REQ-011
81
117
  await validateStrictLanePairing(prolog, relationships);
@@ -185,6 +221,83 @@ export async function handleKbUpsert(prolog, args) {
185
221
  throw new Error(`Upsert execution failed: ${message}`);
186
222
  }
187
223
  }
224
+ function chooseScriptKind(filePath) {
225
+ const lower = filePath.toLowerCase();
226
+ if (lower.endsWith(".tsx"))
227
+ return ScriptKind.TSX;
228
+ if (lower.endsWith(".ts") || lower.endsWith(".mts") || lower.endsWith(".cts")) {
229
+ return ScriptKind.TS;
230
+ }
231
+ if (lower.endsWith(".jsx"))
232
+ return ScriptKind.JSX;
233
+ return ScriptKind.JS;
234
+ }
235
+ function hasTraceabilityRelationship(relationships) {
236
+ return relationships.some((relationship) => typeof relationship.type === "string" &&
237
+ TRACEABILITY_RELATIONSHIP_TYPES.has(relationship.type));
238
+ }
239
+ function hasAllowedGranularityReason(entity) {
240
+ const reason = entity.granularity_reason;
241
+ return (typeof reason === "string" && ALLOWED_GRANULARITY_REASONS.has(reason));
242
+ }
243
+ function collectNarrowExportNames(filePath, content) {
244
+ const project = new Project({ skipAddingFilesFromTsConfig: true });
245
+ const sourceFile = project.createSourceFile(`${filePath}::granularity`, content, {
246
+ overwrite: true,
247
+ scriptKind: chooseScriptKind(filePath),
248
+ });
249
+ const names = new Set();
250
+ for (const fn of sourceFile.getFunctions()) {
251
+ if (fn.isExported()) {
252
+ const name = fn.getName();
253
+ if (name)
254
+ names.add(name);
255
+ }
256
+ }
257
+ for (const cls of sourceFile.getClasses()) {
258
+ if (cls.isExported()) {
259
+ const name = cls.getName();
260
+ if (name)
261
+ names.add(name);
262
+ }
263
+ }
264
+ for (const iface of sourceFile.getInterfaces()) {
265
+ if (iface.isExported())
266
+ names.add(iface.getName());
267
+ }
268
+ for (const alias of sourceFile.getTypeAliases()) {
269
+ if (alias.isExported())
270
+ names.add(alias.getName());
271
+ }
272
+ for (const en of sourceFile.getEnums()) {
273
+ if (en.isExported())
274
+ names.add(en.getName());
275
+ }
276
+ return [...names].sort();
277
+ }
278
+ function validateSymbolGranularity(entity, relationships) {
279
+ if (entity.type !== "symbol")
280
+ return;
281
+ if (!hasTraceabilityRelationship(relationships))
282
+ return;
283
+ if (hasAllowedGranularityReason(entity))
284
+ return;
285
+ if (typeof entity.sourceFile !== "string")
286
+ return;
287
+ if (typeof entity.title !== "string")
288
+ return;
289
+ const sourcePath = path.isAbsolute(entity.sourceFile)
290
+ ? entity.sourceFile
291
+ : path.resolve(process.cwd(), entity.sourceFile);
292
+ if (!existsSync(sourcePath))
293
+ return;
294
+ const narrowNames = collectNarrowExportNames(entity.sourceFile, readFileSync(sourcePath, "utf8"));
295
+ if (narrowNames.length === 0)
296
+ return;
297
+ if (narrowNames.includes(entity.title))
298
+ return;
299
+ throw new Error(`Symbol ${String(entity.id)} links ${entity.sourceFile} coarsely while granular symbols are available: ${narrowNames.join(", ")}. Move relationships to the narrow symbol or set granularity_reason to config-artifact, module-level-behavior, extractor-miss, or legacy-link.`);
300
+ }
188
301
  export const __test__ = {
189
302
  // implements REQ-vscode-traceability
190
303
  setRefreshCoordinatesForSymbolIdForTests(fn) {
@@ -337,6 +337,20 @@ const BASE_TOOLS = [
337
337
  type: "string",
338
338
  description: "Optional text anchor/reference. Example: 'requirements.md#L40'.",
339
339
  },
340
+ sourceFile: {
341
+ type: "string",
342
+ description: "Optional code source file for symbol entities. Example: 'src/auth/login.ts'.",
343
+ },
344
+ granularity_reason: {
345
+ type: "string",
346
+ enum: [
347
+ "config-artifact",
348
+ "module-level-behavior",
349
+ "extractor-miss",
350
+ "legacy-link",
351
+ ],
352
+ description: "Optional justification for a coarse file/module-level symbol traceability relationship when narrower function/class/type symbols exist.",
353
+ },
340
354
  },
341
355
  required: ["title", "status"],
342
356
  },
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "kibi-mcp",
3
- "version": "0.15.0",
3
+ "version": "0.15.1",
4
4
  "dependencies": {
5
5
  "@modelcontextprotocol/sdk": "^1.26.0",
6
6
  "ajv": "^8.18.0",
@@ -9,7 +9,7 @@
9
9
  "fast-glob": "^3.2.12",
10
10
  "gray-matter": "^4.0.3",
11
11
  "js-yaml": "^4.1.0",
12
- "kibi-cli": "^0.12.0",
12
+ "kibi-cli": "^0.12.1",
13
13
  "kibi-core": "^0.6.0",
14
14
  "mcpcat": "^0.1.12",
15
15
  "ts-morph": "^23.0.0",
@@ -27,7 +27,10 @@
27
27
  "build": "tsc -p tsconfig.json",
28
28
  "prepack": "npm run build"
29
29
  },
30
- "files": ["dist", "bin"],
30
+ "files": [
31
+ "dist",
32
+ "bin"
33
+ ],
31
34
  "engines": {
32
35
  "node": ">=18",
33
36
  "bun": ">=1.0"