kibi-mcp 0.17.4 → 0.19.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.
@@ -0,0 +1,118 @@
1
+ import { escapeAtom, toPrologAtom } from "kibi-cli/prolog/codec";
2
+ function getStringField(record, field) {
3
+ const value = record[field];
4
+ if (typeof value !== "string" || value.trim() === "") {
5
+ throw new Error(`Relationship ${field} must be a non-empty string`);
6
+ }
7
+ return value;
8
+ }
9
+ function normalizePrologAtom(value) {
10
+ return value.replace(/^['"]|['"]$/g, "");
11
+ }
12
+ async function resolveEndpointType(input) {
13
+ if (input.endpointId === input.entity.id) {
14
+ const type = input.entity.type;
15
+ if (typeof type === "string" && type.trim() !== "") {
16
+ return type;
17
+ }
18
+ }
19
+ let result;
20
+ try {
21
+ result = await input.prolog.query(`kb_entity('${escapeAtom(input.endpointId)}', Type, _)`);
22
+ }
23
+ catch (error) {
24
+ if (error instanceof Error) {
25
+ return null;
26
+ }
27
+ throw error;
28
+ }
29
+ if (!result.success) {
30
+ return null;
31
+ }
32
+ const rawType = result.bindings?.Type;
33
+ if (typeof rawType !== "string" || rawType.trim() === "") {
34
+ return null;
35
+ }
36
+ return normalizePrologAtom(rawType);
37
+ }
38
+ function relationshipRecipe(tuple) {
39
+ if (tuple.relType === "verified_by" &&
40
+ tuple.fromType === "fact" &&
41
+ tuple.toType === "test") {
42
+ return [
43
+ "Facts are not directly verified by tests.",
44
+ "Create or update a requirement and link REQ -> TEST with verified_by.",
45
+ "Link the requirement to the fact with constrains or requires_property.",
46
+ ].join(" ");
47
+ }
48
+ if (tuple.relType === "validates" &&
49
+ tuple.fromType === "test" &&
50
+ tuple.toType === "fact") {
51
+ return [
52
+ "Tests validate requirements or scenarios, not facts directly.",
53
+ "Create or update a requirement and link TEST -> REQ with validates.",
54
+ "Link the requirement to the fact with constrains or requires_property.",
55
+ ].join(" ");
56
+ }
57
+ if (tuple.relType === "verified_by") {
58
+ return "verified_by is only valid as req/scenario -> test.";
59
+ }
60
+ if (tuple.relType === "validates") {
61
+ return "validates is only valid as test -> req/scenario.";
62
+ }
63
+ return "Use a typed relationship from docs/entity-schema.md, or relates_to only as a reviewed escape hatch.";
64
+ }
65
+ export function formatInvalidRelationshipTuple(tuple) {
66
+ return `Invalid relationship: ${tuple.relType} from ${tuple.fromType} to ${tuple.toType}. ${relationshipRecipe(tuple)}`;
67
+ }
68
+ export function formatInvalidRelationshipError(rawError) {
69
+ const placeholderMatch = rawError.match(/Invalid relationship:\s*~w from ~w to ~w-\[([^,\]]+),([^,\]]+),([^\]]+)\]/);
70
+ if (placeholderMatch) {
71
+ const [, relType, fromType, toType] = placeholderMatch;
72
+ if (relType && fromType && toType) {
73
+ return formatInvalidRelationshipTuple({ relType, fromType, toType });
74
+ }
75
+ }
76
+ const readableMatch = rawError.match(/Invalid relationship:\s*([^\s]+) from ([^\s]+) to ([^\s.]+)/);
77
+ if (readableMatch) {
78
+ const [, relType, fromType, toType] = readableMatch;
79
+ if (relType && fromType && toType) {
80
+ return formatInvalidRelationshipTuple({ relType, fromType, toType });
81
+ }
82
+ }
83
+ return null;
84
+ }
85
+ export function formatRelationshipSourceMismatch(entityId, relationship) {
86
+ const from = getStringField(relationship, "from");
87
+ const to = getStringField(relationship, "to");
88
+ return [
89
+ `Relationship source must match the upserted entity ${entityId}; received from=${from}.`,
90
+ `To add ${from} -> ${to}, upsert ${from} instead and include the relationship in that call.`,
91
+ ].join(" ");
92
+ }
93
+ export async function validateLiveRelationshipTargets(prolog, entity, relationships) {
94
+ for (const relationship of relationships) {
95
+ const fromType = await resolveEndpointType({
96
+ prolog,
97
+ entity,
98
+ endpointId: getStringField(relationship, "from"),
99
+ });
100
+ const toType = await resolveEndpointType({
101
+ prolog,
102
+ entity,
103
+ endpointId: getStringField(relationship, "to"),
104
+ });
105
+ if (fromType === null || toType === null) {
106
+ continue;
107
+ }
108
+ const tuple = {
109
+ relType: getStringField(relationship, "type"),
110
+ fromType,
111
+ toType,
112
+ };
113
+ const result = await prolog.query(`once(kb:validate_relationship(${toPrologAtom(tuple.relType)}, ${toPrologAtom(tuple.fromType)}, ${toPrologAtom(tuple.toType)}))`);
114
+ if (!result.success) {
115
+ throw new Error(formatInvalidRelationshipTuple(tuple));
116
+ }
117
+ }
118
+ }
@@ -36,7 +36,7 @@ export async function handleKbSkillsLoad(args) {
36
36
  content: [
37
37
  {
38
38
  type: "text",
39
- text: `Loaded bundled skill ${bundle.manifest.id} with ${resources.length} resources`,
39
+ text: `Loaded bundled skill ${bundle.manifest.id} with ${resources.length} resources: ${formatResourceList(resources)}`,
40
40
  },
41
41
  ],
42
42
  structuredContent: payload,
@@ -65,7 +65,25 @@ export async function handleKbSkillsRead(args) {
65
65
  }
66
66
  catch (error) {
67
67
  const message = error instanceof Error ? error.message : String(error);
68
- throw new Error(`Skills read failed: ${message}`);
68
+ throw new Error(`Skills read failed: ${message}${resourceListHint(args.id)}`);
69
+ }
70
+ }
71
+ function formatResourceList(resources) {
72
+ return resources.length === 0 ? "none" : resources.join(", ");
73
+ }
74
+ function resourceListHint(id) {
75
+ if (typeof id !== "string" || id.trim() === "") {
76
+ return "";
77
+ }
78
+ try {
79
+ const bundle = loadBundledSkill(id);
80
+ return `. Declared resources: ${formatResourceList(bundle.manifest.resources ?? [])}`;
81
+ }
82
+ catch (error) {
83
+ if (error instanceof Error) {
84
+ return "";
85
+ }
86
+ throw error;
69
87
  }
70
88
  }
71
89
  function assertNonEmptyString(value, field) {
@@ -27,6 +27,7 @@ import { isMcpDebugEnabled } from "../env.js";
27
27
  import { analyzeSemanticAdvisorInput } from "../semantic-advisor/analyze-prose.js";
28
28
  import { readBranchKbStamp } from "../server/kb-freshness.js";
29
29
  import { attachedBranchKbPath, updateAttachedBranchStamp, } from "../server/session.js";
30
+ import { formatInvalidRelationshipError, formatRelationshipSourceMismatch, validateLiveRelationshipTargets, } from "./relationship-validation.js";
30
31
  import { refreshCoordinatesForSymbolId } from "./symbols.js";
31
32
  let refreshCoordinatesForSymbolIdImpl = refreshCoordinatesForSymbolId;
32
33
  const ajv = new Ajv({ strict: false });
@@ -209,6 +210,7 @@ export async function handleKbUpsert(prolog, args) {
209
210
  // Validate strict-lane fact_kind pairing for constrains/requires_property
210
211
  // implements REQ-011
211
212
  await validateStrictLanePairing(prolog, effectiveRelationships);
213
+ await validateLiveRelationshipTargets(prolog, entity, effectiveRelationships);
212
214
  // Process entities
213
215
  for (const entity of entities) {
214
216
  const id = entity.id;
@@ -318,6 +320,19 @@ export async function handleKbUpsert(prolog, args) {
318
320
  relationships_created: relationshipsCreated,
319
321
  warnings: [...semanticAdvisor.warnings, ...coverageWarnings],
320
322
  semanticAdvisor: semanticAdvisor.receipt,
323
+ ...(type === "req"
324
+ ? {
325
+ contradictionCheck: {
326
+ outcome: args._skipContradictionCheck
327
+ ? "skipped"
328
+ : "no-conflict",
329
+ checked_req_id: entity.id,
330
+ strict_readiness: semanticAdvisor.receipt.logic_readiness === "modeled"
331
+ ? "modeled"
332
+ : semanticAdvisor.receipt.candidate_lane,
333
+ },
334
+ }
335
+ : {}),
321
336
  },
322
337
  };
323
338
  }
@@ -508,6 +523,9 @@ function buildPropertyList(entity) {
508
523
  "priority",
509
524
  "severity",
510
525
  "symbol_role",
526
+ "granularity_reason",
527
+ "verification_scope",
528
+ "verification_perspective",
511
529
  // Typed fact enum fields must be atoms for Prolog validation
512
530
  "fact_kind",
513
531
  "operator",
@@ -587,7 +605,7 @@ function buildRelationshipMetadata(rel) {
587
605
  function validateRelationshipSources(entityId, relationships) {
588
606
  for (const rel of relationships) {
589
607
  if (rel.from !== entityId) {
590
- throw new Error(`Relationship source must match the upserted entity ${entityId}; received from=${String(rel.from)}`);
608
+ throw new Error(formatRelationshipSourceMismatch(entityId, rel));
591
609
  }
592
610
  }
593
611
  }
@@ -766,6 +784,10 @@ function formatUpsertError(entityId, rawError) {
766
784
  if (!rawError) {
767
785
  return `Failed to upsert entity ${entityId}: Unknown error`;
768
786
  }
787
+ const invalidRelationshipError = formatInvalidRelationshipError(rawError);
788
+ if (invalidRelationshipError !== null) {
789
+ return `Failed to upsert entity ${entityId}: ${invalidRelationshipError}`;
790
+ }
769
791
  // Check for contradiction error - Prolog returns kb_contradiction([...]) term
770
792
  // Try to extract readable details from the term
771
793
  const contradictionMatch = rawError.match(/kb_contradiction\(\s*\[([^\]]+)\]\s*\)/);
@@ -1,8 +1,20 @@
1
1
  import { analyzeSemanticAdvisorInput } from "../semantic-advisor/analyze-prose.js";
2
+ import { validateLiveRelationshipTargets } from "./relationship-validation.js";
2
3
  import { validateKbUpsertArgs } from "./upsert.js";
3
- export async function handleKbValidateUpsert(args) {
4
+ function isUpsertArgs(value) {
5
+ return "type" in value && "id" in value && "properties" in value;
6
+ }
7
+ export async function handleKbValidateUpsert(prologOrArgs, maybeArgs) {
4
8
  try {
9
+ const args = maybeArgs ?? (isUpsertArgs(prologOrArgs) ? prologOrArgs : null);
10
+ if (args === null) {
11
+ throw new Error("kb_validate_upsert requires an upsert payload");
12
+ }
13
+ const prolog = maybeArgs === undefined ? null : prologOrArgs;
5
14
  const { entity } = validateKbUpsertArgs(args);
15
+ if (prolog !== null && !isUpsertArgs(prolog)) {
16
+ await validateLiveRelationshipTargets(prolog, entity, args.relationships ?? []);
17
+ }
6
18
  const semanticAdvisor = analyzeSemanticAdvisorInput({
7
19
  payload: { ...args },
8
20
  });
@@ -421,6 +421,16 @@ const BASE_TOOLS = [
421
421
  ],
422
422
  description: "Optional role classification for symbol entities. Example: 'behavioral'.",
423
423
  },
424
+ verification_scope: {
425
+ type: "string",
426
+ enum: ["unit", "integration", "end_to_end"],
427
+ description: "Optional typed verification scope for test entities. Example: 'end_to_end'.",
428
+ },
429
+ verification_perspective: {
430
+ type: "string",
431
+ enum: ["internal", "consumer"],
432
+ description: "Optional typed verification perspective for test entities. Example: 'consumer'.",
433
+ },
424
434
  fact_kind: {
425
435
  type: "string",
426
436
  enum: [
@@ -592,7 +602,7 @@ const BASE_TOOLS = [
592
602
  },
593
603
  {
594
604
  name: "kb_check",
595
- description: "Run KB validation rules and return violations. Use before or after mutations. Do not use for point lookups. No write side effects. Prefer explicit rules for faster iteration.",
605
+ description: "Run KB validation rules and return violations. Use before or after mutations, and after meaningful source edits with impact options to surface symbol granularity and semantic-review diagnostics. Do not use for point lookups. No write side effects. Prefer explicit rules for faster iteration; omit rules for final full validation plus full-KB qualityDiagnostics review.",
596
606
  inputSchema: {
597
607
  type: "object",
598
608
  properties: {
@@ -611,9 +621,37 @@ const BASE_TOOLS = [
611
621
  "domain-contradictions",
612
622
  "strict-fact-shape",
613
623
  "strict-req-fact-pairing",
624
+ "predicate-verifiability",
625
+ "query-plan-safety",
614
626
  ],
615
627
  },
616
- description: "Optional rule subset. Allowed: must-priority-coverage, symbol-coverage, symbol-traceability, no-dangling-refs, no-cycles, required-fields, deprecated-adr-no-successor, domain-contradictions, strict-fact-shape, strict-req-fact-pairing. If omitted, server runs all.",
628
+ description: "Optional rule subset. Allowed: must-priority-coverage, symbol-coverage, symbol-traceability, no-dangling-refs, no-cycles, required-fields, deprecated-adr-no-successor, domain-contradictions, strict-fact-shape, strict-req-fact-pairing, predicate-verifiability, query-plan-safety. If omitted, server runs all rules plus the full-KB qualityDiagnostics audit scan; if supplied, server preserves scoped validation and skips the full-KB advisory scan.",
629
+ },
630
+ sourceFiles: {
631
+ type: "array",
632
+ items: { type: "string" },
633
+ description: "Optional repo-relative source files to inspect for early impact diagnostics. Use with includeImpactDiagnostics after meaningful source edits.",
634
+ },
635
+ staged: {
636
+ type: "boolean",
637
+ description: "When true, inspect staged source changes for impact diagnostics using the shared CLI impact analyzer without shelling out to kibi check.",
638
+ },
639
+ includeWorkingTreeDiff: {
640
+ type: "boolean",
641
+ description: "When true, inspect current unstaged working-tree diffs for impact diagnostics. Pair with sourceFiles to scope the analysis.",
642
+ },
643
+ includeImpactDiagnostics: {
644
+ type: "boolean",
645
+ description: "When true, include changed-file impact diagnostics such as symbol_granularity_violation and symbol_semantic_review_needed in structured output.",
646
+ },
647
+ maxDiagnostics: {
648
+ type: "integer",
649
+ minimum: 0,
650
+ description: "Optional maximum number of impact diagnostics to return. Graph validation violations are not capped by this value.",
651
+ },
652
+ workspaceRoot: {
653
+ type: "string",
654
+ description: "Optional workspace root for impact diagnostics and .kb/config.json lookup. Defaults to the MCP server workspace.",
617
655
  },
618
656
  },
619
657
  },
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "kibi-mcp",
3
- "version": "0.17.4",
3
+ "version": "0.19.0",
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.7",
13
- "kibi-core": "^0.6.4",
12
+ "kibi-cli": "^0.14.0",
13
+ "kibi-core": "^0.7.0",
14
14
  "mcpcat": "^0.1.12",
15
15
  "ts-morph": "^23.0.0",
16
16
  "zod": "^4.3.6"