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.
Files changed (85) hide show
  1. package/dist/cli-operation-metadata.d.ts +1 -1
  2. package/dist/cli-operation-metadata.js +1 -1
  3. package/dist/cli-register-maintenance.js +1 -1
  4. package/dist/commands/engine.d.ts +2 -1
  5. package/dist/commands/engine.d.ts.map +1 -1
  6. package/dist/commands/engine.js +6 -1
  7. package/dist/engine.d.ts.map +1 -1
  8. package/dist/engine.js +24 -6
  9. package/dist/intent-search.d.ts +7 -0
  10. package/dist/intent-search.d.ts.map +1 -1
  11. package/dist/intent-search.js +66 -4
  12. package/dist/operations/modeling/predicate-applicability.d.ts.map +1 -1
  13. package/dist/operations/modeling/predicate-applicability.js +10 -4
  14. package/dist/operations/modeling/predicate-applyplan.d.ts.map +1 -1
  15. package/dist/operations/modeling/predicate-applyplan.js +16 -5
  16. package/dist/operations/modeling/predicate-catalog-4.d.ts.map +1 -1
  17. package/dist/operations/modeling/predicate-catalog-4.js +3 -0
  18. package/dist/operations/modeling/predicate-inference-1.d.ts.map +1 -1
  19. package/dist/operations/modeling/predicate-inference-1.js +34 -7
  20. package/dist/operations/modeling/predicate-score-3.js +1 -1
  21. package/dist/operations/modeling/predicate-types.d.ts +6 -0
  22. package/dist/operations/modeling/predicate-types.d.ts.map +1 -1
  23. package/dist/operations/modeling/suggest-predicates.d.ts.map +1 -1
  24. package/dist/operations/modeling/suggest-predicates.js +111 -0
  25. package/dist/operations/mutation/types.d.ts +26 -0
  26. package/dist/operations/mutation/types.d.ts.map +1 -1
  27. package/dist/operations/mutation/upsert.d.ts.map +1 -1
  28. package/dist/operations/mutation/upsert.js +8 -2
  29. package/dist/operations/mutation/validate-upsert.d.ts.map +1 -1
  30. package/dist/operations/mutation/validate-upsert.js +116 -4
  31. package/dist/operations/mutation/warnings.d.ts +10 -0
  32. package/dist/operations/mutation/warnings.d.ts.map +1 -1
  33. package/dist/operations/mutation/warnings.js +26 -0
  34. package/dist/operations/mutation/what-if.d.ts +25 -0
  35. package/dist/operations/mutation/what-if.d.ts.map +1 -0
  36. package/dist/operations/mutation/what-if.js +59 -0
  37. package/dist/operations/planning/compile-intent.d.ts +15 -20
  38. package/dist/operations/planning/compile-intent.d.ts.map +1 -1
  39. package/dist/operations/planning/compile-intent.js +64 -53
  40. package/dist/operations/semantic-advisor/clauses.d.ts.map +1 -1
  41. package/dist/operations/semantic-advisor/clauses.js +8 -7
  42. package/dist/prolog/janitor.d.ts +26 -2
  43. package/dist/prolog/janitor.d.ts.map +1 -1
  44. package/dist/prolog/janitor.js +55 -3
  45. package/dist/prolog/store-lock.d.ts +28 -3
  46. package/dist/prolog/store-lock.d.ts.map +1 -1
  47. package/dist/prolog/store-lock.js +103 -12
  48. package/dist/public/operations/catalog.d.ts +47 -3
  49. package/dist/public/operations/catalog.d.ts.map +1 -1
  50. package/dist/public/operations/check-executor.d.ts +2 -2
  51. package/dist/public/operations/check-executor.d.ts.map +1 -1
  52. package/dist/public/operations/check-executor.js +35 -0
  53. package/dist/public/operations/check-format-shared.d.ts +16 -0
  54. package/dist/public/operations/check-format-shared.d.ts.map +1 -1
  55. package/dist/public/operations/check-format-shared.js +19 -0
  56. package/dist/public/operations/contracts.d.ts.map +1 -1
  57. package/dist/public/operations/contracts.js +10 -0
  58. package/dist/public/operations/discovery-executors.d.ts +3 -0
  59. package/dist/public/operations/discovery-executors.d.ts.map +1 -1
  60. package/dist/public/operations/discovery-executors.js +7 -2
  61. package/dist/public/operations/specs/composite.d.ts +1 -1
  62. package/dist/public/operations/specs/composite.js +1 -1
  63. package/dist/public/operations/specs/discovery.d.ts +2 -2
  64. package/dist/public/operations/specs/discovery.d.ts.map +1 -1
  65. package/dist/public/operations/specs/discovery.js +2 -2
  66. package/dist/public/operations/specs/upsert.d.ts +44 -0
  67. package/dist/public/operations/specs/upsert.d.ts.map +1 -1
  68. package/dist/public/operations/specs/upsert.js +14 -0
  69. package/dist/public/skills/kibi-usage/SKILL.md +36 -1
  70. package/dist/public/skills/kibi-usage/resources/fact-lanes.md +4 -2
  71. package/dist/search-answer.d.ts +13 -0
  72. package/dist/search-answer.d.ts.map +1 -1
  73. package/dist/search-answer.js +45 -4
  74. package/dist/utils/branch-store.d.ts.map +1 -1
  75. package/dist/utils/branch-store.js +4 -2
  76. package/package.json +3 -3
  77. package/src/public/operations/check-executor.ts +44 -1
  78. package/src/public/operations/check-format-shared.ts +35 -1
  79. package/src/public/operations/contracts.ts +11 -0
  80. package/src/public/operations/discovery-executors.ts +12 -2
  81. package/src/public/operations/specs/composite.ts +1 -1
  82. package/src/public/operations/specs/discovery.ts +3 -2
  83. package/src/public/operations/specs/upsert.ts +15 -0
  84. package/src/public/skills/kibi-usage/SKILL.md +36 -1
  85. package/src/public/skills/kibi-usage/resources/fact-lanes.md +4 -2
@@ -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 === 0
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":"AA0BA,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,CAehC;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"}
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; a holder that died while it held the store usually means an engine or kibi prove run crashed or was killed.`,
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.19.0",
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.20.0",
239
- "kibi-plugin-builtin": "^0.5.0",
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
- // implements REQ-mcp-tool-check
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 = 20,
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. Apply returned plans with kb_upsert.",
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: 20,
169
- description: "Optional max rows to return after ranking. Default: 20.",
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.0
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, articles, deny]
71
+ predicate_args: [suspended_user, publish, article, deny]
72
72
  polarity: deny
73
- canonical_key: permission_rule(suspended_user,publish,articles,deny)
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.