kibi-mcp 0.16.1 → 0.17.2

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.
@@ -24,7 +24,10 @@ import entitySchema from "kibi-cli/schemas/entity";
24
24
  import relationshipSchema from "kibi-cli/schemas/relationship";
25
25
  import { Project, ScriptKind } from "ts-morph";
26
26
  import { isMcpDebugEnabled } from "../env.js";
27
+ import { analyzeSemanticAdvisorInput } from "../semantic-advisor/analyze-prose.js";
27
28
  import { refreshCoordinatesForSymbolId } from "./symbols.js";
29
+ import { attachedBranchKbPath, updateAttachedBranchStamp, } from "../server/session.js";
30
+ import { readBranchKbStamp } from "../server/kb-freshness.js";
28
31
  let refreshCoordinatesForSymbolIdImpl = refreshCoordinatesForSymbolId;
29
32
  const ajv = new Ajv({ strict: false });
30
33
  const entitySchemaRecord = entitySchema;
@@ -175,15 +178,36 @@ function validateFactModelingShape(entity) {
175
178
  */
176
179
  export async function handleKbUpsert(prolog, args) {
177
180
  const { entity, relationships } = validateKbUpsertArgs(args);
181
+ const semanticAdvisor = analyzeSemanticAdvisorInput({
182
+ payload: { ...args },
183
+ });
178
184
  const type = entity.type;
179
185
  const entities = [entity];
180
- // Validate strict-lane fact_kind pairing for constrains/requires_property
181
- // implements REQ-011
182
- await validateStrictLanePairing(prolog, relationships);
186
+ // If relationships are not explicitly provided, preserve existing ones.
187
+ // This prevents accidental relationship loss when updating only properties.
188
+ let effectiveRelationships = relationships;
183
189
  let created = 0;
184
190
  let updated = 0;
185
191
  let relationshipsCreated = 0;
186
192
  try {
193
+ if ((args.relationships === undefined ||
194
+ (Array.isArray(args.relationships) && args.relationships.length === 0)) &&
195
+ args.id) {
196
+ // Preserve relationships on updates when the request omits relationships
197
+ // OR provides an empty array. This avoids accidental edge deletion by
198
+ // clients that always serialize `relationships: []` for partial updates.
199
+ // For creates, this remains cheap because existence is checked first.
200
+ const existsResult = await prolog.query(`once(kb_entity('${escapeAtom(args.id)}', _, _))`);
201
+ if (existsResult.success) {
202
+ const existing = await fetchExistingRelationships(prolog, args.id);
203
+ if (existing.length > 0) {
204
+ effectiveRelationships = existing;
205
+ }
206
+ }
207
+ }
208
+ // Validate strict-lane fact_kind pairing for constrains/requires_property
209
+ // implements REQ-011
210
+ await validateStrictLanePairing(prolog, effectiveRelationships);
187
211
  // Process entities
188
212
  for (const entity of entities) {
189
213
  const id = entity.id;
@@ -196,7 +220,7 @@ export async function handleKbUpsert(prolog, args) {
196
220
  const props = buildPropertyList(entity);
197
221
  // Build relationship goals
198
222
  const relationshipGoals = [];
199
- for (const rel of relationships) {
223
+ for (const rel of effectiveRelationships) {
200
224
  const relType = rel.type;
201
225
  const from = rel.from;
202
226
  const to = rel.to;
@@ -237,7 +261,7 @@ export async function handleKbUpsert(prolog, args) {
237
261
  throw new Error(formattedError);
238
262
  }
239
263
  await recordEntityAudit(prolog, isUpdate ? "updated" : "created", type, entity);
240
- for (const rel of relationships) {
264
+ for (const rel of effectiveRelationships) {
241
265
  await recordRelationshipAudit(prolog, rel);
242
266
  }
243
267
  // Update counters
@@ -247,7 +271,7 @@ export async function handleKbUpsert(prolog, args) {
247
271
  else {
248
272
  created++;
249
273
  }
250
- relationshipsCreated += relationships.length;
274
+ relationshipsCreated += effectiveRelationships.length;
251
275
  }
252
276
  // Save KB to disk after all entities/relationships are written to ensure
253
277
  // durability across process restarts.
@@ -256,6 +280,17 @@ export async function handleKbUpsert(prolog, args) {
256
280
  if (!saveResult.success) {
257
281
  throw new Error(`Failed to save KB after upsert: ${saveResult.error || "Unknown error"}`);
258
282
  }
283
+ // Update session stamp so kb_check (and other tools) don't trigger a
284
+ // stale-detach refresh that would unload the just-saved runtime state.
285
+ if (attachedBranchKbPath) {
286
+ try {
287
+ const freshStamp = await readBranchKbStamp(attachedBranchKbPath);
288
+ updateAttachedBranchStamp(freshStamp);
289
+ }
290
+ catch {
291
+ // Non-fatal: stamp update is best-effort to avoid spurious refresh.
292
+ }
293
+ }
259
294
  if (type === "symbol") {
260
295
  try {
261
296
  await refreshCoordinatesForSymbolIdImpl(entity.id);
@@ -267,6 +302,8 @@ export async function handleKbUpsert(prolog, args) {
267
302
  }
268
303
  }
269
304
  }
305
+ // Check for scenario-coverage guidance
306
+ const coverageWarnings = await checkScenarioCoverageGuidance(prolog, relationships, type, entity.id);
270
307
  return {
271
308
  content: [
272
309
  {
@@ -278,6 +315,8 @@ export async function handleKbUpsert(prolog, args) {
278
315
  created,
279
316
  updated,
280
317
  relationships_created: relationshipsCreated,
318
+ warnings: [...semanticAdvisor.warnings, ...coverageWarnings],
319
+ semanticAdvisor: semanticAdvisor.receipt,
281
320
  },
282
321
  };
283
322
  }
@@ -429,10 +468,19 @@ function validateSymbolGranularity(entity, relationships) {
429
468
  if (behavioralNames.length === 0)
430
469
  return;
431
470
  const nonBehavioralNames = getNonBehavioralSymbolNames(candidates);
471
+ const maxNamesInMessage = 10;
472
+ const shownBehavioral = behavioralNames.slice(0, maxNamesInMessage);
473
+ const hiddenBehavioralCount = behavioralNames.length - shownBehavioral.length;
474
+ const behavioralList = shownBehavioral.join(", ") +
475
+ (hiddenBehavioralCount > 0 ? `, and ${hiddenBehavioralCount} more` : "");
476
+ const shownNonBehavioral = nonBehavioralNames.slice(0, maxNamesInMessage);
477
+ const hiddenNonBehavioralCount = nonBehavioralNames.length - shownNonBehavioral.length;
478
+ const nonBehavioralList = shownNonBehavioral.join(", ") +
479
+ (hiddenNonBehavioralCount > 0 ? `, and ${hiddenNonBehavioralCount} more` : "");
432
480
  const ignoredSymbolsMessage = nonBehavioralNames.length > 0
433
- ? ` Non-behavioral symbols in the file were ignored for this decision: ${nonBehavioralNames.join(", ")}.`
481
+ ? ` Non-behavioral symbols in the file were ignored for this decision: ${nonBehavioralList}.`
434
482
  : "";
435
- throw new Error(`Symbol ${String(entity.id)} links ${entity.sourceFile} coarsely while granular symbols are available (behavioral only): ${behavioralNames.join(", ")}. Move relationships to a behavioral symbol, add a manifest behavioral anchor, or set granularity_reason to config-artifact, module-level-behavior, extractor-miss, or legacy-link.${ignoredSymbolsMessage}`);
483
+ throw new Error(`Symbol ${String(entity.id)} links ${entity.sourceFile} coarsely while granular symbols are available (behavioral only): ${behavioralList}. Move relationships to a behavioral symbol, add a manifest behavioral anchor, or set granularity_reason to config-artifact, module-level-behavior, extractor-miss, or legacy-link.${ignoredSymbolsMessage}`);
436
484
  }
437
485
  export const __test__ = {
438
486
  // implements REQ-vscode-traceability
@@ -570,6 +618,103 @@ async function validateStrictLanePairing(prolog, relationships) {
570
618
  }
571
619
  }
572
620
  }
621
+ /**
622
+ * Check for scenario-coverage guidance when verified_by is added to a
623
+ * requirement that already has scenarios (specified_by relationships).
624
+ * Returns non-blocking warnings; never throws.
625
+ */
626
+ async function checkScenarioCoverageGuidance(prolog, relationships, entityType, entityId) {
627
+ const warnings = [];
628
+ if (entityType !== "req")
629
+ return warnings;
630
+ try {
631
+ for (const rel of relationships) {
632
+ const relType = rel.type;
633
+ if (relType !== "verified_by")
634
+ continue;
635
+ // Check if this requirement has scenarios
636
+ const scenarioQuery = `kb_relationship(specified_by, '${escapeAtom(entityId)}', ScenarioId)`;
637
+ const scenarioResult = await prolog.query(`once(${scenarioQuery})`);
638
+ if (scenarioResult.success) {
639
+ warnings.push(`Scenario-backed coverage: verified_by(${entityId},test) is valid but will not satisfy symbol-coverage because ${entityId} has specified_by a scenario. Use verified_by(scenario,test) or validates(test,scenario) instead.`);
640
+ break; // One warning per entity is enough
641
+ }
642
+ }
643
+ }
644
+ catch {
645
+ // Non-blocking: never fail the upsert
646
+ }
647
+ return warnings;
648
+ }
649
+ /**
650
+ * Query existing relationships for an entity from the live KB.
651
+ * Used to preserve relationships when the upsert request omits the
652
+ * relationships field (entity-only property update).
653
+ */
654
+ async function fetchExistingRelationships(prolog, entityId) {
655
+ const relTypes = [
656
+ "depends_on", "specified_by", "verified_by", "validates",
657
+ "implements", "covered_by", "executable_for", "constrained_by",
658
+ "constrains", "requires_property", "requires_predicate",
659
+ "guards", "publishes", "consumes", "supersedes", "relates_to",
660
+ ];
661
+ const existing = [];
662
+ try {
663
+ for (const relType of relTypes) {
664
+ const forwardGoal = `findall(To, kb_relationship(${relType}, '${escapeAtom(entityId)}', To), Targets)`;
665
+ const forwardResult = await prolog.query(forwardGoal);
666
+ if (forwardResult.success && forwardResult.bindings.Targets) {
667
+ const targetsStr = forwardResult.bindings.Targets;
668
+ const targets = parsePrologList(targetsStr);
669
+ for (const to of targets) {
670
+ existing.push({ type: relType, from: entityId, to });
671
+ }
672
+ }
673
+ const reverseGoal = `findall(From, kb_relationship(${relType}, From, '${escapeAtom(entityId)}'), Sources)`;
674
+ const reverseResult = await prolog.query(reverseGoal);
675
+ if (reverseResult.success && reverseResult.bindings.Sources) {
676
+ const sourcesStr = reverseResult.bindings.Sources;
677
+ const sources = parsePrologList(sourcesStr);
678
+ for (const from of sources) {
679
+ existing.push({ type: relType, from, to: entityId });
680
+ }
681
+ }
682
+ }
683
+ }
684
+ catch (e) {
685
+ // Best-effort: if we can't read existing relationships, proceed without them.
686
+ // This preserves backward compatibility and avoids breaking mocked tests.
687
+ }
688
+ return existing;
689
+ }
690
+ function parsePrologList(listStr) {
691
+ const trimmed = listStr.trim();
692
+ if (trimmed === "[]")
693
+ return [];
694
+ const match = trimmed.match(/^\[(.*)\]$/s);
695
+ if (!match || !match[1])
696
+ return [];
697
+ const content = match[1];
698
+ const items = [];
699
+ let depth = 0;
700
+ let current = "";
701
+ for (const char of content) {
702
+ if (char === "[")
703
+ depth++;
704
+ if (char === "]")
705
+ depth--;
706
+ if (char === "," && depth === 0) {
707
+ items.push(current.trim().replace(/^'|'$/g, ""));
708
+ current = "";
709
+ continue;
710
+ }
711
+ current += char;
712
+ }
713
+ if (current.trim()) {
714
+ items.push(current.trim().replace(/^'|'$/g, ""));
715
+ }
716
+ return items;
717
+ }
573
718
  /**
574
719
  * Record audit entry for a successfully committed entity mutation.
575
720
  * Called only after the RDF transaction succeeds.
@@ -1,7 +1,11 @@
1
+ import { analyzeSemanticAdvisorInput } from "../semantic-advisor/analyze-prose.js";
1
2
  import { validateKbUpsertArgs } from "./upsert.js";
2
3
  export async function handleKbValidateUpsert(args) {
3
4
  try {
4
5
  const { entity } = validateKbUpsertArgs(args);
6
+ const semanticAdvisor = analyzeSemanticAdvisorInput({
7
+ payload: { ...args },
8
+ });
5
9
  return {
6
10
  content: [
7
11
  {
@@ -12,7 +16,8 @@ export async function handleKbValidateUpsert(args) {
12
16
  structuredContent: {
13
17
  valid: true,
14
18
  errors: [],
15
- warnings: [],
19
+ warnings: semanticAdvisor.warnings,
20
+ semanticAdvisor: semanticAdvisor.receipt,
16
21
  normalizedPreview: entity,
17
22
  },
18
23
  };
@@ -30,6 +35,7 @@ export async function handleKbValidateUpsert(args) {
30
35
  valid: false,
31
36
  errors: [message],
32
37
  warnings: [],
38
+ semanticAdvisor: null,
33
39
  normalizedPreview: null,
34
40
  },
35
41
  };
@@ -292,9 +292,45 @@ const BASE_TOOLS = [
292
292
  },
293
293
  },
294
294
  },
295
+ {
296
+ name: "kb_semantic_advisor",
297
+ description: "Analyze requirement prose without mutating the KB and return semantic advisor receipts with modeling suggestions. Use before constructing kb_upsert payloads when prose may contain machine-checkable logic. Suggestions can include strict-property facts, predicate facts, ambiguity observations, or ontology-gap observations; all suggestions are advisory and reviewable.",
298
+ inputSchema: {
299
+ type: "object",
300
+ required: ["text"],
301
+ properties: {
302
+ text: {
303
+ type: "string",
304
+ description: "Requirement prose to inspect for machine-checkable modeling suggestions.",
305
+ },
306
+ type: {
307
+ type: "string",
308
+ enum: ["req"],
309
+ default: "req",
310
+ description: "Entity type context for analysis. Currently requirement prose is supported.",
311
+ },
312
+ id: {
313
+ type: "string",
314
+ description: "Optional requirement ID used for deterministic draft relationship guidance.",
315
+ },
316
+ title: {
317
+ type: "string",
318
+ description: "Optional requirement title for draft apply plans.",
319
+ },
320
+ source: {
321
+ type: "string",
322
+ description: "Optional provenance for draft suggestions.",
323
+ },
324
+ status: {
325
+ type: "string",
326
+ description: "Optional requirement status for draft suggestions.",
327
+ },
328
+ },
329
+ },
330
+ },
295
331
  {
296
332
  name: "kb_upsert",
297
- description: "Create or update one entity and optional relationships. Use for KB mutations after validating intent. Use kb_model_requirement before hand-writing strict property facts from prose, and kb_suggest_predicates before hand-writing ontology predicate facts. Use the `relationships` array for batch creation of multiple links in a single call (e.g., linking a requirement to multiple tests or facts). Prefer modeling requirements as reusable fact links (`constrains`, `requires_property`, or `requires_predicate`) so consistency and contradiction checks remain queryable. Relationship endpoints must already exist in KB. For requirements, the write will be rejected if it contradicts existing current requirements that constrain the same subject with incompatible properties. To replace a conflicting requirement, include a `supersedes` relationship from the new requirement to the old one in the same request. Do not use for read-only inspection. Side effects: writes KB, may refresh symbol coordinates.",
333
+ description: "Create or update one entity and optional relationships. Use for KB mutations after validating intent; prefer kb_validate_upsert first because it returns semantic advisor receipts for prose-heavy requirements. Use kb_model_requirement before hand-writing strict property facts from prose, and kb_suggest_predicates before hand-writing ontology predicate facts. Use the `relationships` array for batch creation of multiple links in a single call (e.g., linking a requirement to multiple tests or facts). Prefer modeling requirements as reusable fact links (`constrains`, `requires_property`, or `requires_predicate`) so consistency and contradiction checks remain queryable. Relationship endpoints must already exist in KB. For requirements, the write will be rejected if it contradicts existing current requirements that constrain the same subject with incompatible properties. To replace a conflicting requirement, include a `supersedes` relationship from the new requirement to the old one in the same request. Successful writes may return non-blocking semantic advisor warnings; inspect and repair those warnings before treating prose as contradiction-checkable. Do not use for read-only inspection. Side effects: writes KB, may refresh symbol coordinates.",
298
334
  inputSchema: {
299
335
  type: "object",
300
336
  required: ["type", "id", "properties"],
@@ -509,7 +545,7 @@ const BASE_TOOLS = [
509
545
  },
510
546
  {
511
547
  name: "kb_validate_upsert",
512
- description: "Validate a kb_upsert payload without mutating the KB. Use this read-only preflight when modeling strict facts or predicates and you want actionable schema/modeling errors before calling kb_upsert.",
548
+ description: "Validate a kb_upsert payload without mutating the KB. Use this read-only preflight before kb_upsert, especially for requirements, because it returns schema/modeling errors plus semantic advisor receipts that identify prose likely needing kb_model_requirement, kb_suggest_predicates, ambiguity review, or an ontology-gap observation.",
513
549
  inputSchema: {
514
550
  type: "object",
515
551
  required: ["type", "id", "properties"],
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "kibi-mcp",
3
- "version": "0.16.1",
3
+ "version": "0.17.2",
4
4
  "dependencies": {
5
5
  "@modelcontextprotocol/sdk": "^1.26.0",
6
6
  "ajv": "^8.18.0",
@@ -9,8 +9,8 @@
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.5",
13
- "kibi-core": "^0.6.1",
12
+ "kibi-cli": "^0.12.7",
13
+ "kibi-core": "^0.6.2",
14
14
  "mcpcat": "^0.1.12",
15
15
  "ts-morph": "^23.0.0",
16
16
  "zod": "^4.3.6"
@@ -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"