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.
- package/dist/diagnostics-helpers.js +159 -0
- package/dist/diagnostics.js +117 -100
- package/dist/semantic-advisor/analyze-prose.js +25 -0
- package/dist/server/diagnostic-usage.js +74 -0
- package/dist/server/docs.js +7 -6
- package/dist/server/json-schema-to-zod.js +104 -0
- package/dist/server/tool-registration.js +131 -0
- package/dist/server/tool-types.js +1 -0
- package/dist/server/tools-runtime.js +76 -0
- package/dist/server/tools.js +24 -287
- package/dist/tools/check-config.js +67 -0
- package/dist/tools/check-format.js +92 -0
- package/dist/tools/check-impact.js +45 -0
- package/dist/tools/check-prolog.js +51 -0
- package/dist/tools/check-types.js +1 -0
- package/dist/tools/check.js +64 -148
- package/dist/tools/query-plan-safety.js +58 -0
- package/dist/tools/relationship-validation.js +118 -0
- package/dist/tools/skills.js +20 -2
- package/dist/tools/upsert.js +23 -1
- package/dist/tools/validate-upsert.js +13 -1
- package/dist/tools-config.js +40 -2
- package/package.json +3 -3
|
@@ -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
|
+
}
|
package/dist/tools/skills.js
CHANGED
|
@@ -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) {
|
package/dist/tools/upsert.js
CHANGED
|
@@ -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(
|
|
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
|
-
|
|
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
|
});
|
package/dist/tools-config.js
CHANGED
|
@@ -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.
|
|
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.
|
|
13
|
-
"kibi-core": "^0.
|
|
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"
|