kibi-cli 2.19.0 → 2.20.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/cli-operation-metadata.d.ts +1 -1
- package/dist/cli-operation-metadata.js +1 -1
- package/dist/cli-register-maintenance.js +1 -1
- package/dist/commands/engine.d.ts +2 -1
- package/dist/commands/engine.d.ts.map +1 -1
- package/dist/commands/engine.js +6 -1
- package/dist/engine.d.ts.map +1 -1
- package/dist/engine.js +24 -6
- package/dist/intent-search.d.ts +7 -0
- package/dist/intent-search.d.ts.map +1 -1
- package/dist/intent-search.js +66 -4
- package/dist/operations/modeling/predicate-applicability.d.ts.map +1 -1
- package/dist/operations/modeling/predicate-applicability.js +10 -4
- package/dist/operations/modeling/predicate-applyplan.d.ts.map +1 -1
- package/dist/operations/modeling/predicate-applyplan.js +16 -5
- package/dist/operations/modeling/predicate-catalog-4.d.ts.map +1 -1
- package/dist/operations/modeling/predicate-catalog-4.js +3 -0
- package/dist/operations/modeling/predicate-inference-1.d.ts.map +1 -1
- package/dist/operations/modeling/predicate-inference-1.js +34 -7
- package/dist/operations/modeling/predicate-score-3.js +1 -1
- package/dist/operations/modeling/predicate-types.d.ts +6 -0
- package/dist/operations/modeling/predicate-types.d.ts.map +1 -1
- package/dist/operations/modeling/suggest-predicates.d.ts.map +1 -1
- package/dist/operations/modeling/suggest-predicates.js +111 -0
- package/dist/operations/mutation/types.d.ts +26 -0
- package/dist/operations/mutation/types.d.ts.map +1 -1
- package/dist/operations/mutation/upsert.d.ts.map +1 -1
- package/dist/operations/mutation/upsert.js +8 -2
- package/dist/operations/mutation/validate-upsert.d.ts.map +1 -1
- package/dist/operations/mutation/validate-upsert.js +116 -4
- package/dist/operations/mutation/warnings.d.ts +10 -0
- package/dist/operations/mutation/warnings.d.ts.map +1 -1
- package/dist/operations/mutation/warnings.js +26 -0
- package/dist/operations/mutation/what-if.d.ts +25 -0
- package/dist/operations/mutation/what-if.d.ts.map +1 -0
- package/dist/operations/mutation/what-if.js +59 -0
- package/dist/operations/planning/compile-intent.d.ts +15 -20
- package/dist/operations/planning/compile-intent.d.ts.map +1 -1
- package/dist/operations/planning/compile-intent.js +64 -53
- package/dist/operations/semantic-advisor/clauses.d.ts.map +1 -1
- package/dist/operations/semantic-advisor/clauses.js +8 -7
- package/dist/prolog/janitor.d.ts +26 -2
- package/dist/prolog/janitor.d.ts.map +1 -1
- package/dist/prolog/janitor.js +55 -3
- package/dist/prolog/store-lock.d.ts +28 -3
- package/dist/prolog/store-lock.d.ts.map +1 -1
- package/dist/prolog/store-lock.js +103 -12
- package/dist/public/operations/catalog.d.ts +47 -3
- package/dist/public/operations/catalog.d.ts.map +1 -1
- package/dist/public/operations/check-executor.d.ts +2 -2
- package/dist/public/operations/check-executor.d.ts.map +1 -1
- package/dist/public/operations/check-executor.js +35 -0
- package/dist/public/operations/check-format-shared.d.ts +16 -0
- package/dist/public/operations/check-format-shared.d.ts.map +1 -1
- package/dist/public/operations/check-format-shared.js +19 -0
- package/dist/public/operations/contracts.d.ts.map +1 -1
- package/dist/public/operations/contracts.js +10 -0
- package/dist/public/operations/discovery-executors.d.ts +3 -0
- package/dist/public/operations/discovery-executors.d.ts.map +1 -1
- package/dist/public/operations/discovery-executors.js +7 -2
- package/dist/public/operations/specs/composite.d.ts +1 -1
- package/dist/public/operations/specs/composite.js +1 -1
- package/dist/public/operations/specs/discovery.d.ts +2 -2
- package/dist/public/operations/specs/discovery.d.ts.map +1 -1
- package/dist/public/operations/specs/discovery.js +2 -2
- package/dist/public/operations/specs/upsert.d.ts +44 -0
- package/dist/public/operations/specs/upsert.d.ts.map +1 -1
- package/dist/public/operations/specs/upsert.js +14 -0
- package/dist/public/skills/kibi-usage/SKILL.md +36 -1
- package/dist/public/skills/kibi-usage/resources/fact-lanes.md +4 -2
- package/dist/search-answer.d.ts +13 -0
- package/dist/search-answer.d.ts.map +1 -1
- package/dist/search-answer.js +45 -4
- package/dist/utils/branch-store.d.ts.map +1 -1
- package/dist/utils/branch-store.js +4 -2
- package/package.json +3 -3
- package/src/public/operations/check-executor.ts +44 -1
- package/src/public/operations/check-format-shared.ts +35 -1
- package/src/public/operations/contracts.ts +11 -0
- package/src/public/operations/discovery-executors.ts +12 -2
- package/src/public/operations/specs/composite.ts +1 -1
- package/src/public/operations/specs/discovery.ts +3 -2
- package/src/public/operations/specs/upsert.ts +15 -0
- package/src/public/skills/kibi-usage/SKILL.md +36 -1
- package/src/public/skills/kibi-usage/resources/fact-lanes.md +4 -2
package/dist/search-answer.js
CHANGED
|
@@ -65,6 +65,7 @@ export const SEARCH_ANSWER_LIMITS = {
|
|
|
65
65
|
rationale: 3,
|
|
66
66
|
notGoverning: 5,
|
|
67
67
|
observations: 3,
|
|
68
|
+
ungoverned: 5,
|
|
68
69
|
/** Entity rows loaded per answer, the matched seeds included. */
|
|
69
70
|
entityLoads: 70,
|
|
70
71
|
maxBytes: 16_384,
|
|
@@ -513,11 +514,12 @@ export async function buildSearchAnswer(prolog, matches, options = {}) {
|
|
|
513
514
|
candidates.set(owner, { score, via });
|
|
514
515
|
};
|
|
515
516
|
const observations = [];
|
|
517
|
+
const ungoverned = [];
|
|
516
518
|
const rationaleRows = new Map();
|
|
517
519
|
// A matched test may verify a scenario rather than a requirement; its
|
|
518
520
|
// targets are resolved to their governing requirements after the loop.
|
|
519
521
|
const testTargets = [];
|
|
520
|
-
for (const match of seeds) {
|
|
522
|
+
for (const [index, match] of seeds.entries()) {
|
|
521
523
|
const id = text(match.entity.id);
|
|
522
524
|
const entity = graph.row(id) ?? match.entity;
|
|
523
525
|
const type = text(entity.type || match.entity.type);
|
|
@@ -548,6 +550,18 @@ export async function buildSearchAnswer(prolog, matches, options = {}) {
|
|
|
548
550
|
testTargets.push({ id, score: match.score, targets: owners });
|
|
549
551
|
continue;
|
|
550
552
|
}
|
|
553
|
+
if (owners.length === 0 && ["fact", "scenario", "symbol"].includes(type)) {
|
|
554
|
+
if (ungoverned.length < limits.ungoverned)
|
|
555
|
+
ungoverned.push({
|
|
556
|
+
...brief(entity),
|
|
557
|
+
...(type === "fact" ? factDetail(entity) : {}),
|
|
558
|
+
type,
|
|
559
|
+
rank: index + 1,
|
|
560
|
+
});
|
|
561
|
+
else
|
|
562
|
+
truncated = true;
|
|
563
|
+
continue;
|
|
564
|
+
}
|
|
551
565
|
for (const owner of owners)
|
|
552
566
|
propose(owner, match.score * 0.9, `${type} ${id}`);
|
|
553
567
|
}
|
|
@@ -562,6 +576,15 @@ export async function buildSearchAnswer(prolog, matches, options = {}) {
|
|
|
562
576
|
});
|
|
563
577
|
const targetEdges = graph.edgesOf(targetIds);
|
|
564
578
|
for (const test of testTargets) {
|
|
579
|
+
if (test.targets.length === 0) {
|
|
580
|
+
const row = graph.row(test.id);
|
|
581
|
+
const rank = seedIds.indexOf(test.id) + 1;
|
|
582
|
+
if (row !== null && ungoverned.length < limits.ungoverned)
|
|
583
|
+
ungoverned.push({ ...brief(row), type: "test", rank });
|
|
584
|
+
else if (row !== null)
|
|
585
|
+
truncated = true;
|
|
586
|
+
continue;
|
|
587
|
+
}
|
|
565
588
|
for (const target of test.targets) {
|
|
566
589
|
// A target that some requirement specifies is a scenario: lift the
|
|
567
590
|
// test to that requirement. Anything else is a directly verified
|
|
@@ -746,20 +769,36 @@ export async function buildSearchAnswer(prolog, matches, options = {}) {
|
|
|
746
769
|
? verdicts.scope.syncedAt
|
|
747
770
|
: null,
|
|
748
771
|
};
|
|
772
|
+
ungoverned.sort((left, right) => left.rank - right.rank);
|
|
749
773
|
const answer = {
|
|
750
774
|
version: "kibi.search-answer.v1",
|
|
751
775
|
governing,
|
|
752
776
|
rationale,
|
|
753
777
|
notGoverning,
|
|
754
778
|
observations: observations.slice(0, limits.observations),
|
|
779
|
+
ungoverned,
|
|
755
780
|
scope,
|
|
756
781
|
truncated: truncated || graph.truncated,
|
|
757
|
-
note: governing.length
|
|
758
|
-
? "No current requirement matched or owns a matched entity. Absence here is not evidence that nothing governs the change; refine the query or pass sourceLocations."
|
|
759
|
-
: "Governing requirements are current requirements matched by the query or linked to a matched entity. Links are discovery, not proof. A verdict lists the contradiction and scenario-feasibility witnesses that name the requirement; none means no check named it, not that it is proven. Use kb_check and kb_coverage for consistency and proof status.",
|
|
782
|
+
note: answerNote(governing.length, ungoverned),
|
|
760
783
|
};
|
|
761
784
|
return fitSearchAnswer(answer, limits.maxBytes);
|
|
762
785
|
}
|
|
786
|
+
// implements REQ-search-answer-hygiene
|
|
787
|
+
/**
|
|
788
|
+
* How to read the layer. When the top-ranked match is owned by no
|
|
789
|
+
* requirement, the note says so first: the governing list, if any, came from
|
|
790
|
+
* lower-ranked matches and is not the top match's requirement.
|
|
791
|
+
*/
|
|
792
|
+
function answerNote(governingCount, ungoverned) {
|
|
793
|
+
const top = ungoverned.find((entry) => entry.rank === 1);
|
|
794
|
+
const lead = top === undefined
|
|
795
|
+
? ""
|
|
796
|
+
: `The top-ranked match ${top.id} (${top.type}) is linked to no requirement${governingCount === 0 ? "" : "; the governing requirements below were reached through lower-ranked matches"}. `;
|
|
797
|
+
return (lead +
|
|
798
|
+
(governingCount === 0
|
|
799
|
+
? "No current requirement matched or owns a matched entity. Absence here is not evidence that nothing governs the change; refine the query or pass sourceLocations."
|
|
800
|
+
: "Governing requirements are current requirements matched by the query or linked to a matched entity. Links are discovery, not proof. A verdict lists the contradiction and scenario-feasibility witnesses that name the requirement; none means no check named it, not that it is proven. Use kb_check and kb_coverage for consistency and proof status."));
|
|
801
|
+
}
|
|
763
802
|
function byteSize(value) {
|
|
764
803
|
return Buffer.byteLength(JSON.stringify(value), "utf8");
|
|
765
804
|
}
|
|
@@ -784,6 +823,7 @@ function withoutExcerpt(entity) {
|
|
|
784
823
|
const ANSWER_TRIM_ORDER = [
|
|
785
824
|
"observations",
|
|
786
825
|
"notGoverning",
|
|
826
|
+
"ungoverned",
|
|
787
827
|
"rationale",
|
|
788
828
|
];
|
|
789
829
|
const REQUIREMENT_TRIM_ORDER = [
|
|
@@ -838,6 +878,7 @@ export function fitSearchAnswer(answer, maxBytes = SEARCH_ANSWER_LIMITS.maxBytes
|
|
|
838
878
|
rationale: current.rationale.map((adr) => clipRationale(adr, titleChars)),
|
|
839
879
|
notGoverning: clipAll(current.notGoverning),
|
|
840
880
|
observations: clipAll(current.observations),
|
|
881
|
+
ungoverned: clipAll(current.ungoverned),
|
|
841
882
|
};
|
|
842
883
|
// Excerpts are the most expendable text: drop them before any entry.
|
|
843
884
|
for (let index = current.rationale.length - 1; index >= 0 && over(); index -= 1) {
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"branch-store.d.ts","sourceRoot":"","sources":["../../src/utils/branch-store.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"branch-store.d.ts","sourceRoot":"","sources":["../../src/utils/branch-store.ts"],"names":[],"mappings":"AA2BA,mEAAmE;AACnE,eAAO,MAAM,yBAAyB,8BAA8B,CAAC;AACrE,yCAAyC;AACzC,eAAO,MAAM,8BAA8B,8BAA8B,CAAC;AAE1E,MAAM,MAAM,gBAAgB,GACxB,SAAS,GACT,SAAS,GACT,YAAY,GACZ,YAAY,CAAC;AAEjB,MAAM,MAAM,qBAAqB,GAAG,QAAQ,CAAC;IAC3C,KAAK,EAAE,gBAAgB,CAAC;IACxB,IAAI,EAAE,MAAM,CAAC;IACb,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,gBAAgB,EAAE,OAAO,CAAC;CAC3B,CAAC,CAAC;AAEH;;;GAGG;AACH,wBAAgB,kBAAkB,CAChC,aAAa,EAAE,MAAM,EACrB,MAAM,EAAE,MAAM,GACb,qBAAqB,CA0GvB;AAED;;;;GAIG;AAEH,wBAAgB,sBAAsB,CACpC,SAAS,EAAE,MAAM,GAChB,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAAG,IAAI,CAiBhC;AAED;;;;;;GAMG;AAEH,wBAAgB,uBAAuB,CACrC,SAAS,EAAE,MAAM,EACjB,GAAG,GAAE,IAAiB,GACrB,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAAG,IAAI,CAmBhC;AAED,wBAAgB,iBAAiB,CAC/B,UAAU,EAAE,qBAAqB,GAChC,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAAG,IAAI,CAWhC;AA6BD;;;;GAIG;AAEH,wBAAgB,sBAAsB,CAAC,aAAa,EAAE,MAAM,GAAG,MAAM,CAOpE;AAED,MAAM,MAAM,sBAAsB,GAAG,QAAQ,CAAC;IAC5C,oEAAoE;IACpE,QAAQ,EAAE,OAAO,CAAC;IAClB,0EAA0E;IAC1E,UAAU,EAAE,MAAM,GAAG,IAAI,CAAC;CAC3B,CAAC,CAAC;AAEH;;;;;;GAMG;AAEH,wBAAgB,sBAAsB,CACpC,UAAU,EAAE,qBAAqB,GAChC,sBAAsB,CAwBxB;AAkBD;;;;;;GAMG;AAEH,wBAAgB,2BAA2B,CACzC,aAAa,EAAE,MAAM,EACrB,UAAU,EAAE,qBAAqB,EACjC,MAAM,EAAE,MAAM,GACb,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAAG,IAAI,CAwBhC"}
|
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
*/
|
|
5
5
|
import { existsSync, readFileSync, readdirSync, statSync, } from "node:fs";
|
|
6
6
|
import * as path from "node:path";
|
|
7
|
-
import { STORE_LOCK_TAKEOVER_VISIBLE_MS, classifyStoreLockHolder, readStoreLockOwner, readStoreLockTakeover, } from "../prolog/store-lock.js";
|
|
7
|
+
import { FOREIGN_STORE_LOCK_REASON_PREFIX, STORE_LOCK_TAKEOVER_VISIBLE_MS, classifyStoreLockHolder, readStoreLockOwner, readStoreLockTakeover, } from "../prolog/store-lock.js";
|
|
8
8
|
import { branchStoreManifestMatches, branchStorePath, legacyBranchStorePath, } from "./branch-store-locator.js";
|
|
9
9
|
import { ENTITY_LANES, KB_ROOT } from "./kb-paths.js";
|
|
10
10
|
/** Stale-reason code of a branch store that was never compiled. */
|
|
@@ -170,7 +170,9 @@ export function storeLockTakeoverReason(storePath, now = new Date()) {
|
|
|
170
170
|
brokenPid: record.brokenPid,
|
|
171
171
|
reason: record.reason,
|
|
172
172
|
at: record.at,
|
|
173
|
-
detail: `Kibi broke a stale branch-store lock at ${record.at} (${record.reason || "holder no longer running"}) and reattached. Nothing needs repair;
|
|
173
|
+
detail: `Kibi broke a stale branch-store lock at ${record.at} (${record.reason || "holder no longer running"}) and reattached. Nothing needs repair; ${record.reason.startsWith(FOREIGN_STORE_LOCK_REASON_PREFIX)
|
|
174
|
+
? "the lock files were copied with the .kb directory (for example by cp -a) while another workspace's engine held its own store."
|
|
175
|
+
: "a holder that died while it held the store usually means an engine or kibi prove run crashed or was killed."}`,
|
|
174
176
|
};
|
|
175
177
|
}
|
|
176
178
|
export function branchStoreReason(inspection) {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "kibi-cli",
|
|
3
|
-
"version": "2.
|
|
3
|
+
"version": "2.20.0",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"description": "Kibi CLI for knowledge base management",
|
|
6
6
|
"engines": {
|
|
@@ -235,8 +235,8 @@
|
|
|
235
235
|
"fast-glob": "^3.2.12",
|
|
236
236
|
"gray-matter": "^4.0.3",
|
|
237
237
|
"js-yaml": "^4.1.0",
|
|
238
|
-
"kibi-core": "^0.
|
|
239
|
-
"kibi-plugin-builtin": "^0.5.
|
|
238
|
+
"kibi-core": "^0.21.0",
|
|
239
|
+
"kibi-plugin-builtin": "^0.5.1",
|
|
240
240
|
"kibi-plugin-sdk": "^0.5.0",
|
|
241
241
|
"kibi-swipl": "^1.0.1",
|
|
242
242
|
"saxes": "^6.0.0",
|
|
@@ -26,6 +26,7 @@ import type { Violation } from "../check-types.js";
|
|
|
26
26
|
import { collectFullKbQualityDiagnostics } from "../impact-diagnostics.js";
|
|
27
27
|
import {
|
|
28
28
|
type CheckDiagnostic,
|
|
29
|
+
type CheckEvaluated,
|
|
29
30
|
type CheckStructuredContent,
|
|
30
31
|
buildStructuredContent,
|
|
31
32
|
buildSummary,
|
|
@@ -70,7 +71,7 @@ export type CheckInput = {
|
|
|
70
71
|
};
|
|
71
72
|
|
|
72
73
|
export type CheckPayload = CheckStructuredContent;
|
|
73
|
-
export type { CheckDiagnostic, CheckStructuredContent };
|
|
74
|
+
export type { CheckDiagnostic, CheckEvaluated, CheckStructuredContent };
|
|
74
75
|
export {
|
|
75
76
|
buildStructuredContent,
|
|
76
77
|
buildSummary,
|
|
@@ -90,6 +91,38 @@ export type CheckExecutionOptions = {
|
|
|
90
91
|
readonly branchStoreAttached?: boolean;
|
|
91
92
|
};
|
|
92
93
|
|
|
94
|
+
// implements REQ-search-answer-hygiene
|
|
95
|
+
/**
|
|
96
|
+
* What this run evaluated: the rules that ran, in registry order, and for
|
|
97
|
+
* `domain-contradictions` the number of requirement fact pairs the rule
|
|
98
|
+
* compared, read from the engine after the rules ran on the same store. A
|
|
99
|
+
* count the engine cannot give (an older engine) is left out rather than
|
|
100
|
+
* reported as zero.
|
|
101
|
+
*/
|
|
102
|
+
async function describeEvaluation(
|
|
103
|
+
prolog: PrologPort,
|
|
104
|
+
rules: ReadonlySet<string>,
|
|
105
|
+
): Promise<CheckEvaluated> {
|
|
106
|
+
const evaluated: {
|
|
107
|
+
rules: string[];
|
|
108
|
+
domainContradictions?: { factPairsExamined: number };
|
|
109
|
+
} = {
|
|
110
|
+
rules: [...rules],
|
|
111
|
+
};
|
|
112
|
+
if (!rules.has("domain-contradictions")) return evaluated;
|
|
113
|
+
try {
|
|
114
|
+
const result = await prolog.query(
|
|
115
|
+
"kb_domain_contradiction_pairs_examined(Count)",
|
|
116
|
+
);
|
|
117
|
+
const count = Number(result.bindings.Count);
|
|
118
|
+
if (result.success && Number.isInteger(count) && count >= 0)
|
|
119
|
+
evaluated.domainContradictions = { factPairsExamined: count };
|
|
120
|
+
} catch {
|
|
121
|
+
// An engine without the predicate leaves the count out.
|
|
122
|
+
}
|
|
123
|
+
return evaluated;
|
|
124
|
+
}
|
|
125
|
+
|
|
93
126
|
function requireProlog(context: OperationContext): PrologPort {
|
|
94
127
|
if (context.prolog === undefined) {
|
|
95
128
|
throw new Error("Check operation requires a Prolog runtime");
|
|
@@ -226,12 +259,15 @@ export async function executeCheck(
|
|
|
226
259
|
[],
|
|
227
260
|
statusPlan,
|
|
228
261
|
);
|
|
262
|
+
// Every store-backed rule was skipped: only this one was evaluated.
|
|
263
|
+
const evaluated = { rules: [BRANCH_STORE_NOT_COMPILED_RULE] };
|
|
229
264
|
return {
|
|
230
265
|
content: [
|
|
231
266
|
{
|
|
232
267
|
type: "text",
|
|
233
268
|
text: buildSummary({
|
|
234
269
|
violations,
|
|
270
|
+
evaluated,
|
|
235
271
|
impactResult,
|
|
236
272
|
qualityDiagnostics: [],
|
|
237
273
|
}),
|
|
@@ -239,6 +275,7 @@ export async function executeCheck(
|
|
|
239
275
|
],
|
|
240
276
|
structuredContent: buildStructuredContent({
|
|
241
277
|
violations,
|
|
278
|
+
evaluated,
|
|
242
279
|
diagnostics: [
|
|
243
280
|
{
|
|
244
281
|
category: "SYNC_ERROR",
|
|
@@ -283,12 +320,14 @@ export async function executeCheck(
|
|
|
283
320
|
>[],
|
|
284
321
|
statusPlan,
|
|
285
322
|
);
|
|
323
|
+
const evaluated = { rules: [] };
|
|
286
324
|
return {
|
|
287
325
|
content: [
|
|
288
326
|
{
|
|
289
327
|
type: "text",
|
|
290
328
|
text: buildSummary({
|
|
291
329
|
violations: [],
|
|
330
|
+
evaluated,
|
|
292
331
|
impactResult,
|
|
293
332
|
qualityDiagnostics,
|
|
294
333
|
}),
|
|
@@ -296,6 +335,7 @@ export async function executeCheck(
|
|
|
296
335
|
],
|
|
297
336
|
structuredContent: buildStructuredContent({
|
|
298
337
|
violations: [],
|
|
338
|
+
evaluated,
|
|
299
339
|
diagnostics: [],
|
|
300
340
|
qualityDiagnostics,
|
|
301
341
|
impactResult,
|
|
@@ -351,6 +391,7 @@ export async function executeCheck(
|
|
|
351
391
|
...checkPolicyFindings,
|
|
352
392
|
]);
|
|
353
393
|
const violations: Violation[] = partitioned.violations;
|
|
394
|
+
const evaluated = await describeEvaluation(prolog, rulesAllowlist);
|
|
354
395
|
|
|
355
396
|
const diagnostics: CheckDiagnostic[] = violations.map((v) => ({
|
|
356
397
|
category: "SYNC_ERROR",
|
|
@@ -396,6 +437,7 @@ export async function executeCheck(
|
|
|
396
437
|
type: "text",
|
|
397
438
|
text: buildSummary({
|
|
398
439
|
violations,
|
|
440
|
+
evaluated,
|
|
399
441
|
impactResult,
|
|
400
442
|
qualityDiagnostics,
|
|
401
443
|
}),
|
|
@@ -403,6 +445,7 @@ export async function executeCheck(
|
|
|
403
445
|
],
|
|
404
446
|
structuredContent: buildStructuredContent({
|
|
405
447
|
violations,
|
|
448
|
+
evaluated,
|
|
406
449
|
diagnostics,
|
|
407
450
|
qualityDiagnostics,
|
|
408
451
|
impactResult,
|
|
@@ -29,10 +29,22 @@ export interface CheckDiagnostic {
|
|
|
29
29
|
readonly suggestion?: string;
|
|
30
30
|
}
|
|
31
31
|
|
|
32
|
-
|
|
32
|
+
/**
|
|
33
|
+
* What a check run evaluated, so "no violations" can be told apart from
|
|
34
|
+
* "nothing evaluated": the rules that ran, and for `domain-contradictions`
|
|
35
|
+
* how many pairs of requirement facts it compared.
|
|
36
|
+
*/
|
|
37
|
+
// implements REQ-search-answer-hygiene
|
|
38
|
+
export interface CheckEvaluated {
|
|
39
|
+
readonly rules: readonly string[];
|
|
40
|
+
readonly domainContradictions?: Readonly<{ factPairsExamined: number }>;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
// implements REQ-mcp-tool-check, REQ-search-answer-hygiene
|
|
33
44
|
export interface CheckStructuredContent {
|
|
34
45
|
readonly violations: readonly Violation[];
|
|
35
46
|
readonly count: number;
|
|
47
|
+
readonly evaluated: CheckEvaluated;
|
|
36
48
|
readonly diagnostics: readonly CheckDiagnostic[];
|
|
37
49
|
readonly qualityDiagnostics?: readonly QualityDiagnostic[];
|
|
38
50
|
readonly impactDiagnostics?: ChangedFileImpactResult["impactDiagnostics"];
|
|
@@ -139,6 +151,7 @@ export function formatViolationText(violations: readonly Violation[]): string {
|
|
|
139
151
|
// implements REQ-mcp-tool-check
|
|
140
152
|
export function buildStructuredContent(input: {
|
|
141
153
|
violations: readonly Violation[];
|
|
154
|
+
evaluated: CheckEvaluated;
|
|
142
155
|
diagnostics: readonly CheckDiagnostic[];
|
|
143
156
|
qualityDiagnostics?: readonly QualityDiagnostic[];
|
|
144
157
|
impactResult: ChangedFileImpactResult | undefined;
|
|
@@ -148,6 +161,12 @@ export function buildStructuredContent(input: {
|
|
|
148
161
|
return {
|
|
149
162
|
violations: [...input.violations],
|
|
150
163
|
count: input.violations.length,
|
|
164
|
+
evaluated: {
|
|
165
|
+
rules: [...input.evaluated.rules],
|
|
166
|
+
...(input.evaluated.domainContradictions !== undefined
|
|
167
|
+
? { domainContradictions: input.evaluated.domainContradictions }
|
|
168
|
+
: {}),
|
|
169
|
+
},
|
|
151
170
|
diagnostics: formatDiagnostics(input.diagnostics),
|
|
152
171
|
...(qualityDiagnostics.length > 0
|
|
153
172
|
? { qualityDiagnostics: [...qualityDiagnostics] }
|
|
@@ -168,12 +187,27 @@ export function buildStructuredContent(input: {
|
|
|
168
187
|
}
|
|
169
188
|
|
|
170
189
|
// implements REQ-mcp-tool-check
|
|
190
|
+
// implements REQ-search-answer-hygiene
|
|
191
|
+
/** One line saying what the check evaluated, so a clean result is readable. */
|
|
192
|
+
export function formatEvaluatedText(evaluated: CheckEvaluated): string {
|
|
193
|
+
const rules =
|
|
194
|
+
evaluated.rules.length === 0
|
|
195
|
+
? "Evaluated no rules"
|
|
196
|
+
: `Evaluated ${evaluated.rules.length} rule${evaluated.rules.length === 1 ? "" : "s"}`;
|
|
197
|
+
const pairs = evaluated.domainContradictions;
|
|
198
|
+
if (pairs === undefined) return `${rules}.`;
|
|
199
|
+
return `${rules}; domain-contradictions compared ${pairs.factPairsExamined} fact pair${pairs.factPairsExamined === 1 ? "" : "s"}${pairs.factPairsExamined === 0 ? " (no two current requirements constrain the same subject or predicate case)" : ""}.`;
|
|
200
|
+
}
|
|
201
|
+
|
|
171
202
|
export function buildSummary(input: {
|
|
172
203
|
readonly violations: readonly Violation[];
|
|
204
|
+
readonly evaluated?: CheckEvaluated;
|
|
173
205
|
readonly impactResult: ChangedFileImpactResult | undefined;
|
|
174
206
|
readonly qualityDiagnostics: readonly QualityDiagnostic[];
|
|
175
207
|
}): string {
|
|
176
208
|
const sections = [formatViolationText(input.violations)];
|
|
209
|
+
if (input.evaluated !== undefined)
|
|
210
|
+
sections.push(formatEvaluatedText(input.evaluated));
|
|
177
211
|
if (input.impactResult !== undefined) {
|
|
178
212
|
sections.push(formatImpactText(input.impactResult));
|
|
179
213
|
}
|
|
@@ -449,6 +449,7 @@ const BASE_DATA_SCHEMAS: Readonly<
|
|
|
449
449
|
{
|
|
450
450
|
results: valueArray,
|
|
451
451
|
count: integerValue,
|
|
452
|
+
returned: integerValue,
|
|
452
453
|
truncated: booleanValue,
|
|
453
454
|
queryAnalysis: recordValue,
|
|
454
455
|
answer: recordValue,
|
|
@@ -573,6 +574,11 @@ const BASE_DATA_SCHEMAS: Readonly<
|
|
|
573
574
|
recommendedPredicateSchema: { type: ["object", "null"] },
|
|
574
575
|
applyPlan: recordArray,
|
|
575
576
|
relationshipPlan: { type: ["object", "null"] },
|
|
577
|
+
plan: {
|
|
578
|
+
type: ["object", "null"],
|
|
579
|
+
description:
|
|
580
|
+
"With requirementId and a complete candidate: the hash-bound kibi.compile-plan.v1 that writes the predicate fact and links requires_predicate in one kb_apply_plan call (approvedPlanHash = plan.planHash). Its contradictionAnalysis is the what-if over the current KB plus the plan; a conflict leaves the plan blocked. Null when no plan applies.",
|
|
581
|
+
},
|
|
576
582
|
relationshipTarget: {
|
|
577
583
|
type: ["string", "null"],
|
|
578
584
|
description:
|
|
@@ -629,6 +635,8 @@ const BASE_DATA_SCHEMAS: Readonly<
|
|
|
629
635
|
warnings: valueArray,
|
|
630
636
|
semanticAdvisor: nullableJsonSchema(recordValue),
|
|
631
637
|
normalizedPreview: nullableJsonSchema(recordValue),
|
|
638
|
+
stagedFacts: stringArray,
|
|
639
|
+
whatIf: recordValue,
|
|
632
640
|
}),
|
|
633
641
|
kb_upsert: {
|
|
634
642
|
description:
|
|
@@ -653,6 +661,8 @@ const BASE_DATA_SCHEMAS: Readonly<
|
|
|
653
661
|
warnings: valueArray,
|
|
654
662
|
semanticAdvisor: nullableJsonSchema(recordValue),
|
|
655
663
|
normalizedPreview: nullableJsonSchema(recordValue),
|
|
664
|
+
stagedFacts: stringArray,
|
|
665
|
+
whatIf: recordValue,
|
|
656
666
|
dryRun: { const: true },
|
|
657
667
|
skippedEffects: stringArray,
|
|
658
668
|
},
|
|
@@ -683,6 +693,7 @@ const BASE_DATA_SCHEMAS: Readonly<
|
|
|
683
693
|
objectData({
|
|
684
694
|
violations: valueArray,
|
|
685
695
|
count: integerValue,
|
|
696
|
+
evaluated: recordValue,
|
|
686
697
|
diagnostics: recordArray,
|
|
687
698
|
qualityDiagnostics: recordArray,
|
|
688
699
|
impactDiagnostics: recordArray,
|
|
@@ -151,7 +151,10 @@ export type SearchInput = {
|
|
|
151
151
|
|
|
152
152
|
export type SearchPayload = {
|
|
153
153
|
readonly results: readonly (SearchMatch | IntentSearchMatch)[];
|
|
154
|
+
/** Matches in the whole ranking, before `limit` and `offset`. */
|
|
154
155
|
readonly count: number;
|
|
156
|
+
/** Rows in `results`: the page this call returned. */
|
|
157
|
+
readonly returned: number;
|
|
155
158
|
readonly truncated?: boolean;
|
|
156
159
|
readonly queryAnalysis?: IntentSearchAnalysis;
|
|
157
160
|
readonly answer?: SearchAnswer;
|
|
@@ -772,7 +775,7 @@ export async function executeSearch(
|
|
|
772
775
|
const {
|
|
773
776
|
query,
|
|
774
777
|
type,
|
|
775
|
-
limit =
|
|
778
|
+
limit = 10,
|
|
776
779
|
offset = 0,
|
|
777
780
|
rankingMode = "intent-v1",
|
|
778
781
|
semanticFacets,
|
|
@@ -829,6 +832,11 @@ export async function executeSearch(
|
|
|
829
832
|
: undefined;
|
|
830
833
|
// The structured payload carries every row; the text is a short
|
|
831
834
|
// human-readable digest instead of a second copy of the results.
|
|
835
|
+
// A top match no requirement owns is said in the digest too, so the
|
|
836
|
+
// governing list is never read as the top match's requirement.
|
|
837
|
+
const topUngoverned = answerLayer?.ungoverned.find(
|
|
838
|
+
(entry) => entry.rank === 1,
|
|
839
|
+
);
|
|
832
840
|
const text = `${
|
|
833
841
|
intentResult.matches.length === 0
|
|
834
842
|
? `No intent search results for '${trimmedQuery}' (abstained).`
|
|
@@ -837,13 +845,14 @@ export async function executeSearch(
|
|
|
837
845
|
.map((match) => String(match.entity.id ?? ""))
|
|
838
846
|
.join(
|
|
839
847
|
", ",
|
|
840
|
-
)}${answerLayer ? `. Governing: ${answerLayer.governing.map((req) => req.id).join(", ") || "none found"}` : ""}.`
|
|
848
|
+
)}${answerLayer ? `. Governing: ${answerLayer.governing.map((req) => req.id).join(", ") || "none found"}` : ""}${topUngoverned ? ` (the top match ${topUngoverned.id} is linked to no requirement)` : ""}.`
|
|
841
849
|
}${storeNote}`;
|
|
842
850
|
return {
|
|
843
851
|
content: [{ type: "text", text }],
|
|
844
852
|
structuredContent: {
|
|
845
853
|
results: await projectMatches(prolog, paginated, fields),
|
|
846
854
|
count: intentResult.matches.length,
|
|
855
|
+
returned: paginated.length,
|
|
847
856
|
truncated: offset + paginated.length < intentResult.matches.length,
|
|
848
857
|
queryAnalysis: intentResult.analysis,
|
|
849
858
|
...(answerLayer ? { answer: answerLayer } : {}),
|
|
@@ -883,6 +892,7 @@ export async function executeSearch(
|
|
|
883
892
|
structuredContent: {
|
|
884
893
|
results: await projectMatches(prolog, paginated, fields),
|
|
885
894
|
count: matches.length,
|
|
895
|
+
returned: paginated.length,
|
|
886
896
|
truncated: offset + paginated.length < matches.length,
|
|
887
897
|
...branchFields,
|
|
888
898
|
},
|
|
@@ -217,7 +217,7 @@ export const modelSpec = {
|
|
|
217
217
|
name: "kb_model",
|
|
218
218
|
cliName: "model",
|
|
219
219
|
description:
|
|
220
|
-
"Turn requirement prose into checkable structure without writing the KB. mode:analyze returns the semantic advisor receipt: the clause ledger, what is grounded, what is ambiguous and suggested modeling (pass clauses for compound prose and optional typed kibi.logic.v1 interpretations). mode:requirement converts one claim (subjectKey, propertyKey, operator, value, or typed logic) into a strict-lane write set and applyPlan. mode:predicates ranks ontology predicate schemas and returns an applicable predicate-fact plan only for reviewed bindings, otherwise an ontology-gap observation.
|
|
220
|
+
"Turn requirement prose into checkable structure without writing the KB. mode:analyze returns the semantic advisor receipt: the clause ledger, what is grounded, what is ambiguous and suggested modeling (pass clauses for compound prose and optional typed kibi.logic.v1 interpretations). mode:requirement converts one claim (subjectKey, propertyKey, operator, value, or typed logic) into a strict-lane write set and applyPlan. mode:predicates ranks ontology predicate schemas and returns an applicable predicate-fact plan only for reviewed bindings, otherwise an ontology-gap observation. With requirementId, mode:predicates also returns a hash-bound plan (fact write plus requires_predicate link) for kb_apply_plan, whose contradictionAnalysis is the what-if over the current KB; other returned plans apply with kb_upsert, and kb_upsert dryRun with plannedFacts previews a requirement before its facts exist.",
|
|
221
221
|
businessInputSchema: {
|
|
222
222
|
type: "object",
|
|
223
223
|
required: ["mode", "text"],
|
|
@@ -165,8 +165,9 @@ export const searchSpec = {
|
|
|
165
165
|
limit: {
|
|
166
166
|
type: "integer",
|
|
167
167
|
minimum: 0,
|
|
168
|
-
default:
|
|
169
|
-
description:
|
|
168
|
+
default: 10,
|
|
169
|
+
description:
|
|
170
|
+
"Optional max rows to return after ranking. Default: 10; raise it or page with offset when the answer layer is not enough.",
|
|
170
171
|
},
|
|
171
172
|
offset: {
|
|
172
173
|
type: "integer",
|
|
@@ -38,6 +38,21 @@ export const upsertSpec = {
|
|
|
38
38
|
description:
|
|
39
39
|
"Validate the payload and return schema/modeling errors, a normalized preview and the semantic advisor receipt without writing the KB or workspace. Use it as the preflight before the real write.",
|
|
40
40
|
},
|
|
41
|
+
plannedFacts: {
|
|
42
|
+
type: "array",
|
|
43
|
+
description:
|
|
44
|
+
"dryRun only. Fact upsert payloads (type, id, properties, optional relationships) that will be written before this payload, typically the applyPlan entries kb_model returned. They are validated in order and staged with the payload, so a requires_predicate or requires_property target that does not exist yet resolves, its claim_key is read from the plan, and the contradiction preview runs against the KB as it would be after the whole batch (result: stagedFacts, whatIf). A conflict the batch would introduce, such as an allow and a deny permission_rule on the same key, is refused as the commit would refuse it. Rejected without dryRun: write the facts first.",
|
|
45
|
+
items: {
|
|
46
|
+
type: "object",
|
|
47
|
+
required: ["type", "id", "properties"],
|
|
48
|
+
properties: {
|
|
49
|
+
type: { type: "string", const: "fact" },
|
|
50
|
+
id: { type: "string", minLength: 1 },
|
|
51
|
+
properties: { type: "object" },
|
|
52
|
+
relationships: RELATIONSHIPS_SCHEMA,
|
|
53
|
+
},
|
|
54
|
+
},
|
|
55
|
+
},
|
|
41
56
|
document: {
|
|
42
57
|
type: "object",
|
|
43
58
|
additionalProperties: false,
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
id: kibi-usage
|
|
3
3
|
name: Kibi Usage
|
|
4
4
|
description: Use Kibi's source-first, exact-Git, migration-aware, proof-aware operations safely across MCP or the trusted local CLI, including partial completion repair.
|
|
5
|
-
version: 2.15.
|
|
5
|
+
version: 2.15.2
|
|
6
6
|
kibiCompatibility: ">=1.0.0"
|
|
7
7
|
tags:
|
|
8
8
|
- kibi
|
|
@@ -82,6 +82,41 @@ different workspace than the one you asked for; its `detail.reason` says why.
|
|
|
82
82
|
Use the CLI from your working directory instead: `npx --no-install kibi ...`
|
|
83
83
|
runs where you are and needs no `workspaceRoot`.
|
|
84
84
|
|
|
85
|
+
## Reading a hook snippet
|
|
86
|
+
|
|
87
|
+
On hosts where Kibi hooks are installed, a short block
|
|
88
|
+
arrives before you read or edit a file whose symbols are linked to
|
|
89
|
+
requirements. It is projected from `.kb/symbols.yaml`, not from the store, so
|
|
90
|
+
it is a pointer to the knowledge, never proof. Each line answers one question:
|
|
91
|
+
|
|
92
|
+
- `Kibi knowledge for <path> (symbol manifest):` and the `- REQ-… — <symbol
|
|
93
|
+
titles>` rows: the requirements the file's symbols implement, lead
|
|
94
|
+
requirement first. Read the lead requirement with `kb_query({id})` before
|
|
95
|
+
changing behavior.
|
|
96
|
+
- `REQ-… must keep true: FACT-…` (edit only): the ground facts the lead
|
|
97
|
+
requirement requires. An edit that breaks one of them is a requirement
|
|
98
|
+
change: supersede and remodel the requirement, do not code around the fact.
|
|
99
|
+
- `Decision: ADR-…` (edit only): the decision behind the lead requirement.
|
|
100
|
+
- `Covered by: TEST-…` and `Test code for: …`: the tests that prove the
|
|
101
|
+
file's symbols; run or update them with the edit.
|
|
102
|
+
- `The edit is inside <symbol>, which implements REQ-…`: the symbol the edit
|
|
103
|
+
lands in.
|
|
104
|
+
- `Next layer: …`: the exact `kb_query` and `kb_search(... sourceLocations)`
|
|
105
|
+
calls that return the full detail.
|
|
106
|
+
- The `kb_check({sourceFiles:[…], includeImpactDiagnostics:true,
|
|
107
|
+
includeWorkingTreeDiff:true})` line (edit only): run it after the edit. The
|
|
108
|
+
Stop hook reminds you once per session of files edited without it.
|
|
109
|
+
|
|
110
|
+
A file already shown this session gets only the one-line `Kibi: this edit to
|
|
111
|
+
<path> is inside <symbol>` update; the facts and tests from its first snippet
|
|
112
|
+
still apply. `Kibi: no symbol in <path> is linked to a requirement yet` means
|
|
113
|
+
the file is unowned: find the governing requirement with `kb_search` and
|
|
114
|
+
`sourceLocations`, then link a symbol to it with `implements` through
|
|
115
|
+
`kb_upsert`. Without a readable manifest (`Kibi: the hook could not read
|
|
116
|
+
.kb/symbols.yaml`) snippets are off for this session; `kb_query({sourceFile})`
|
|
117
|
+
still answers and `kibi sync` regenerates the manifest. The search tip and the
|
|
118
|
+
`.kb/` note appear once per session and need no action.
|
|
119
|
+
|
|
85
120
|
## Safe workflow
|
|
86
121
|
|
|
87
122
|
1. Always discover before you mutate: start with `kb_search`, then exact-filter with `kb_query`. Use
|
|
@@ -68,14 +68,16 @@ id: FACT-SUSPENDED-PUBLISH-DENIED
|
|
|
68
68
|
status: active
|
|
69
69
|
fact_kind: predicate
|
|
70
70
|
predicate_name: permission_rule
|
|
71
|
-
predicate_args: [suspended_user, publish,
|
|
71
|
+
predicate_args: [suspended_user, publish, article, deny]
|
|
72
72
|
polarity: deny
|
|
73
|
-
canonical_key: permission_rule(suspended_user,publish,
|
|
73
|
+
canonical_key: permission_rule(suspended_user,publish,article,deny)
|
|
74
74
|
claim_key: <advisor-issued key for this exact clause>
|
|
75
75
|
claim_text: Suspended users must not publish articles.
|
|
76
76
|
relationship: { type: requires_predicate, from: REQ-SUSPENDED-PUBLISH-DENIED, to: FACT-SUSPENDED-PUBLISH-DENIED }
|
|
77
77
|
```
|
|
78
78
|
|
|
79
|
+
Inferred bindings drop the article and singularise the resource (`articles` becomes `article`, `any analysis request` and `analysis requests` both become `analysis_request`), so an allow and a deny on the same actor, action and resource share one key. Grants read the same as `allowed`: `may`, `can`, `is allowed to`, `are permitted to`, `must be able to`, `are able to`, `have permission to` and `are entitled to` all select `permission_rule` with `polarity: assert`.
|
|
80
|
+
|
|
79
81
|
If another current requirement links an `assert` fact with the same predicate namespace, name, and ordered arguments, `domain-contradictions` reports the pair. More complex semantic conflicts require a shared canonical schema and arguments; do not assume Kibi can prove arbitrary equivalence between differently shaped predicates.
|
|
80
82
|
|
|
81
83
|
The full logical representation of a requirement is the conjunction of all of its linked ground property and predicate terms. Conditions, exceptions, permissions, and ordering constraints remain explicit arguments of their declared schemas. Contradiction detection therefore depends on equivalent prose reusing the same predicate name, argument roles, ordered canonical values, and opposing polarity rather than inventing synonymous terms.
|