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.
- package/dist/semantic-advisor/analyze-prose.js +1367 -0
- package/dist/semantic-advisor/prose-coverage-evaluator.js +66 -0
- package/dist/semantic-advisor/types.js +1 -0
- package/dist/server/kb-freshness.js +88 -0
- package/dist/server/session.js +65 -4
- package/dist/server/tools.js +5 -0
- package/dist/tools/check.js +2 -0
- package/dist/tools/semantic-advisor.js +42 -0
- package/dist/tools/suggest-predicates.js +1902 -9
- package/dist/tools/upsert.js +153 -8
- package/dist/tools/validate-upsert.js +7 -1
- package/dist/tools-config.js +38 -2
- package/package.json +7 -4
package/dist/tools/upsert.js
CHANGED
|
@@ -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
|
-
//
|
|
181
|
-
//
|
|
182
|
-
|
|
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
|
|
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
|
|
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 +=
|
|
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: ${
|
|
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): ${
|
|
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
|
};
|
package/dist/tools-config.js
CHANGED
|
@@ -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
|
|
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.
|
|
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.
|
|
13
|
-
"kibi-core": "^0.6.
|
|
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": [
|
|
30
|
+
"files": [
|
|
31
|
+
"dist",
|
|
32
|
+
"bin"
|
|
33
|
+
],
|
|
31
34
|
"engines": {
|
|
32
35
|
"node": ">=18",
|
|
33
36
|
"bun": ">=1.0"
|