kibi-cli 2.21.0 → 2.21.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.
Files changed (105) hide show
  1. package/dist/cli-operation-metadata.d.ts +1 -1
  2. package/dist/cli-operation-metadata.js +1 -1
  3. package/dist/commands/doctor.js +13 -1
  4. package/dist/commands/init.d.ts.map +1 -1
  5. package/dist/commands/init.js +15 -3
  6. package/dist/commands/legacy-storage-migration.d.ts.map +1 -1
  7. package/dist/commands/legacy-storage-migration.js +26 -1
  8. package/dist/commands/migrate.d.ts.map +1 -1
  9. package/dist/commands/migrate.js +123 -5
  10. package/dist/commands/sync/persistence.d.ts.map +1 -1
  11. package/dist/commands/sync/persistence.js +4 -0
  12. package/dist/decision-firmness.d.ts +39 -0
  13. package/dist/decision-firmness.d.ts.map +1 -0
  14. package/dist/decision-firmness.js +115 -0
  15. package/dist/extractors/markdown.d.ts +2 -0
  16. package/dist/extractors/markdown.d.ts.map +1 -1
  17. package/dist/extractors/markdown.js +14 -0
  18. package/dist/operations/migration/schema6.d.ts +2 -0
  19. package/dist/operations/migration/schema6.d.ts.map +1 -1
  20. package/dist/operations/migration/schema6.js +44 -1
  21. package/dist/operations/migration/supersedes-reason.d.ts +26 -0
  22. package/dist/operations/migration/supersedes-reason.d.ts.map +1 -0
  23. package/dist/operations/migration/supersedes-reason.js +69 -0
  24. package/dist/operations/modeling/predicate-applyplan.d.ts.map +1 -1
  25. package/dist/operations/modeling/predicate-applyplan.js +10 -2
  26. package/dist/operations/modeling/predicate-loader.d.ts.map +1 -1
  27. package/dist/operations/modeling/predicate-loader.js +11 -0
  28. package/dist/operations/modeling/predicate-types.d.ts +32 -1
  29. package/dist/operations/modeling/predicate-types.d.ts.map +1 -1
  30. package/dist/operations/modeling/suggest-predicates.d.ts.map +1 -1
  31. package/dist/operations/modeling/suggest-predicates.js +175 -35
  32. package/dist/operations/mutation/delete.d.ts +5 -1
  33. package/dist/operations/mutation/delete.d.ts.map +1 -1
  34. package/dist/operations/mutation/delete.js +223 -17
  35. package/dist/operations/mutation/serialization.d.ts.map +1 -1
  36. package/dist/operations/mutation/serialization.js +1 -0
  37. package/dist/operations/mutation/types.d.ts +37 -0
  38. package/dist/operations/mutation/types.d.ts.map +1 -1
  39. package/dist/operations/mutation/upsert.d.ts.map +1 -1
  40. package/dist/operations/mutation/upsert.js +54 -2
  41. package/dist/operations/planning/apply-plan.d.ts +6 -1
  42. package/dist/operations/planning/apply-plan.d.ts.map +1 -1
  43. package/dist/operations/planning/apply-plan.js +8 -2
  44. package/dist/public/operations/catalog.d.ts +12 -3
  45. package/dist/public/operations/catalog.d.ts.map +1 -1
  46. package/dist/public/operations/check-executor.d.ts.map +1 -1
  47. package/dist/public/operations/check-executor.js +3 -0
  48. package/dist/public/operations/contracts.d.ts.map +1 -1
  49. package/dist/public/operations/contracts.js +29 -1
  50. package/dist/public/operations/decision-firmness.d.ts +34 -0
  51. package/dist/public/operations/decision-firmness.d.ts.map +1 -0
  52. package/dist/public/operations/decision-firmness.js +216 -0
  53. package/dist/public/operations/discovery-executors.d.ts.map +1 -1
  54. package/dist/public/operations/discovery-executors.js +16 -2
  55. package/dist/public/operations/migration-plan.js +1 -1
  56. package/dist/public/operations/specs/check-rules.generated.d.ts +1 -1
  57. package/dist/public/operations/specs/check-rules.generated.d.ts.map +1 -1
  58. package/dist/public/operations/specs/check-rules.generated.js +5 -1
  59. package/dist/public/operations/specs/check.d.ts +1 -1
  60. package/dist/public/operations/specs/mutation-schemas.d.ts +5 -0
  61. package/dist/public/operations/specs/mutation-schemas.d.ts.map +1 -1
  62. package/dist/public/operations/specs/mutation-schemas.js +5 -0
  63. package/dist/public/operations/specs/mutation.d.ts +6 -2
  64. package/dist/public/operations/specs/mutation.d.ts.map +1 -1
  65. package/dist/public/operations/specs/mutation.js +6 -2
  66. package/dist/public/operations/specs/upsert.d.ts +5 -0
  67. package/dist/public/operations/specs/upsert.d.ts.map +1 -1
  68. package/dist/public/schemas/entity.d.ts.map +1 -1
  69. package/dist/public/schemas/entity.js +11 -0
  70. package/dist/public/skills/kibi-traceability/SKILL.md +6 -4
  71. package/dist/public/skills/kibi-usage/SKILL.md +50 -10
  72. package/dist/public/skills/kibi-usage/resources/source-authoring.md +4 -2
  73. package/dist/relationships/shards.d.ts +10 -0
  74. package/dist/relationships/shards.d.ts.map +1 -1
  75. package/dist/relationships/shards.js +27 -11
  76. package/dist/schemas/entity.schema.json +28 -0
  77. package/dist/search-answer.d.ts +31 -2
  78. package/dist/search-answer.d.ts.map +1 -1
  79. package/dist/search-answer.js +115 -17
  80. package/dist/types/entities.d.ts +4 -0
  81. package/dist/types/entities.d.ts.map +1 -1
  82. package/dist/utils/kb-manifest.d.ts +6 -0
  83. package/dist/utils/kb-manifest.d.ts.map +1 -1
  84. package/dist/utils/kb-manifest.js +6 -0
  85. package/dist/utils/rule-registry.generated.d.ts +21 -0
  86. package/dist/utils/rule-registry.generated.d.ts.map +1 -1
  87. package/dist/utils/rule-registry.generated.js +25 -0
  88. package/dist/utils/schema-version.d.ts +1 -1
  89. package/dist/utils/schema-version.d.ts.map +1 -1
  90. package/dist/utils/schema-version.js +3 -1
  91. package/package.json +2 -2
  92. package/schema/entities.pl +6 -0
  93. package/src/public/operations/check-executor.ts +6 -0
  94. package/src/public/operations/contracts.ts +33 -1
  95. package/src/public/operations/decision-firmness.ts +280 -0
  96. package/src/public/operations/discovery-executors.ts +17 -2
  97. package/src/public/operations/migration-plan.ts +1 -1
  98. package/src/public/operations/specs/check-rules.generated.ts +5 -1
  99. package/src/public/operations/specs/mutation-schemas.ts +6 -0
  100. package/src/public/operations/specs/mutation.ts +7 -2
  101. package/src/public/schemas/entity.ts +12 -0
  102. package/src/public/skills/kibi-traceability/SKILL.md +6 -4
  103. package/src/public/skills/kibi-usage/SKILL.md +50 -10
  104. package/src/public/skills/kibi-usage/resources/source-authoring.md +4 -2
  105. package/src/schemas/entity.schema.json +28 -0
@@ -1 +1 @@
1
- {"version":3,"file":"kb-manifest.d.ts","sourceRoot":"","sources":["../../src/utils/kb-manifest.ts"],"names":[],"mappings":"AA8BA;;;;;;;GAOG;AAEH,eAAO,MAAM,mBAAmB,IAAI,CAAC;AAErC,MAAM,MAAM,uBAAuB,GAC/B,SAAS,GACT,WAAW,GACX,gBAAgB,CAAC;AAErB,MAAM,WAAW,UAAU;IACzB,uEAAuE;IACvE,eAAe,EAAE,MAAM,CAAC;IACxB,qEAAqE;IACrE,aAAa,EAAE,MAAM,CAAC;IACtB,0DAA0D;IAC1D,uBAAuB,EAAE,uBAAuB,CAAC;IACjD;;;OAGG;IACH,mBAAmB,CAAC,EAAE,MAAM,EAAE,CAAC;CAChC;AAED,wBAAgB,iBAAiB,IAAI,UAAU,CAM9C;AAED,MAAM,MAAM,gBAAgB,GACxB;IAAE,KAAK,EAAE,IAAI,CAAC;IAAC,QAAQ,EAAE,UAAU,CAAC;IAAC,YAAY,EAAE,MAAM,GAAG,IAAI,CAAA;CAAE,GAClE;IACE,KAAK,EAAE,SAAS,GAAG,SAAS,GAAG,QAAQ,CAAC;IACxC,QAAQ,EAAE,IAAI,CAAC;IACf,YAAY,EAAE,MAAM,GAAG,IAAI,CAAC;IAC5B,OAAO,EAAE,MAAM,CAAC;CACjB,CAAC;AAuDN,wBAAgB,oBAAoB,CAAC,aAAa,EAAE,MAAM,GAAG,gBAAgB,CAyD5E;AAED,wBAAgB,cAAc,CAAC,aAAa,EAAE,MAAM,GAAG,UAAU,GAAG,IAAI,CAGvE;AAED,8EAA8E;AAC9E,wBAAgB,uBAAuB,CAAC,aAAa,EAAE,MAAM,GAAG,UAAU,CAEzE;AASD,wBAAgB,eAAe,CAC7B,aAAa,EAAE,MAAM,EACrB,QAAQ,EAAE,UAAU,GACnB,MAAM,CAIR"}
1
+ {"version":3,"file":"kb-manifest.d.ts","sourceRoot":"","sources":["../../src/utils/kb-manifest.ts"],"names":[],"mappings":"AA8BA;;;;;;;GAOG;AAEH,eAAO,MAAM,mBAAmB,IAAI,CAAC;AAErC,MAAM,MAAM,uBAAuB,GAC/B,SAAS,GACT,WAAW,GACX,gBAAgB,CAAC;AAErB,MAAM,WAAW,UAAU;IACzB,uEAAuE;IACvE,eAAe,EAAE,MAAM,CAAC;IACxB,qEAAqE;IACrE,aAAa,EAAE,MAAM,CAAC;IACtB,0DAA0D;IAC1D,uBAAuB,EAAE,uBAAuB,CAAC;IACjD;;;OAGG;IACH,mBAAmB,CAAC,EAAE,MAAM,EAAE,CAAC;IAC/B;;;;OAIG;IACH,4BAA4B,CAAC,EAAE,MAAM,EAAE,CAAC;CACzC;AAED,wBAAgB,iBAAiB,IAAI,UAAU,CAM9C;AAED,MAAM,MAAM,gBAAgB,GACxB;IAAE,KAAK,EAAE,IAAI,CAAC;IAAC,QAAQ,EAAE,UAAU,CAAC;IAAC,YAAY,EAAE,MAAM,GAAG,IAAI,CAAA;CAAE,GAClE;IACE,KAAK,EAAE,SAAS,GAAG,SAAS,GAAG,QAAQ,CAAC;IACxC,QAAQ,EAAE,IAAI,CAAC;IACf,YAAY,EAAE,MAAM,GAAG,IAAI,CAAC;IAC5B,OAAO,EAAE,MAAM,CAAC;CACjB,CAAC;AA+DN,wBAAgB,oBAAoB,CAAC,aAAa,EAAE,MAAM,GAAG,gBAAgB,CAyD5E;AAED,wBAAgB,cAAc,CAAC,aAAa,EAAE,MAAM,GAAG,UAAU,GAAG,IAAI,CAGvE;AAED,8EAA8E;AAC9E,wBAAgB,uBAAuB,CAAC,aAAa,EAAE,MAAM,GAAG,UAAU,CAEzE;AASD,wBAAgB,eAAe,CAC7B,aAAa,EAAE,MAAM,EACrB,QAAQ,EAAE,UAAU,GACnB,MAAM,CAIR"}
@@ -71,6 +71,7 @@ function validateManifest(parsed) {
71
71
  return null;
72
72
  }
73
73
  const acknowledged = parsed.contextAcknowledged;
74
+ const supersedesAcknowledged = parsed.supersedesReasonAcknowledged;
74
75
  return {
75
76
  manifestVersion,
76
77
  schemaVersion,
@@ -80,6 +81,11 @@ function validateManifest(parsed) {
80
81
  contextAcknowledged: acknowledged.filter((id) => typeof id === "string"),
81
82
  }
82
83
  : {}),
84
+ ...(Array.isArray(supersedesAcknowledged)
85
+ ? {
86
+ supersedesReasonAcknowledged: supersedesAcknowledged.filter((id) => typeof id === "string"),
87
+ }
88
+ : {}),
83
89
  };
84
90
  }
85
91
  export function readKbManifestStatus(workspaceRoot) {
@@ -228,6 +228,27 @@ export declare const GENERATED_RULES: readonly [{
228
228
  readonly enforcementClass: "advisory";
229
229
  readonly category: "lifecycle";
230
230
  readonly diagnosticSeverity: "info";
231
+ }, {
232
+ readonly name: "decision-firm-unreferenced";
233
+ readonly description: "A requirement or ADR marked firmness: firm must name the human words it rests on: origin.ref (or approval_ref on a requirement); Kibi checks that the reference is present, not that it is genuine";
234
+ readonly enforcementClass: "canonical";
235
+ readonly category: "integrity";
236
+ }, {
237
+ readonly name: "decision-firm-superseded";
238
+ readonly description: "A requirement or ADR that supersedes a firm decision, retired or not, must itself be firm (and so carry a ref to the human who changed the rule); a default or exploratory replacement cannot overrule a firm decision";
239
+ readonly enforcementClass: "canonical";
240
+ readonly category: "lifecycle";
241
+ }, {
242
+ readonly name: "supersedes-reason-missing";
243
+ readonly description: "A current requirement or ADR that supersedes another must explain why under a 'Why this changed' heading (at least 12 words of prose naming the evidence and answering the old decision's reasoning); supersede edges recorded before schema 9 are acknowledged in the manifest and exempt";
244
+ readonly enforcementClass: "canonical";
245
+ readonly category: "lifecycle";
246
+ }, {
247
+ readonly name: "supersedes-reason-acknowledged";
248
+ readonly description: "Informational: how many current superseding requirements and ADRs have only supersede edges the schema 9 migration acknowledged without a 'Why this changed' section; one summary finding per check";
249
+ readonly enforcementClass: "advisory";
250
+ readonly category: "lifecycle";
251
+ readonly diagnosticSeverity: "info";
231
252
  }, {
232
253
  readonly name: "policy-ownership";
233
254
  readonly description: "When a plugin activated for kibi.check-policy.v1 declares ownership rules, every production symbol in a rule's file set must implement a current requirement grounded (requires_predicate) in one of the rule's predicates; projects without an activated check policy are unaffected";
@@ -1 +1 @@
1
- {"version":3,"file":"rule-registry.generated.d.ts","sourceRoot":"","sources":["../../src/utils/rule-registry.generated.ts"],"names":[],"mappings":"AAUA,eAAO,MAAM,eAAe;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;EA4UlB,CAAC"}
1
+ {"version":3,"file":"rule-registry.generated.d.ts","sourceRoot":"","sources":["../../src/utils/rule-registry.generated.ts"],"names":[],"mappings":"AAUA,eAAO,MAAM,eAAe;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;EAyWlB,CAAC"}
@@ -283,6 +283,31 @@ export const GENERATED_RULES = [
283
283
  category: "lifecycle",
284
284
  diagnosticSeverity: "info",
285
285
  },
286
+ {
287
+ name: "decision-firm-unreferenced",
288
+ description: "A requirement or ADR marked firmness: firm must name the human words it rests on: origin.ref (or approval_ref on a requirement); Kibi checks that the reference is present, not that it is genuine",
289
+ enforcementClass: "canonical",
290
+ category: "integrity",
291
+ },
292
+ {
293
+ name: "decision-firm-superseded",
294
+ description: "A requirement or ADR that supersedes a firm decision, retired or not, must itself be firm (and so carry a ref to the human who changed the rule); a default or exploratory replacement cannot overrule a firm decision",
295
+ enforcementClass: "canonical",
296
+ category: "lifecycle",
297
+ },
298
+ {
299
+ name: "supersedes-reason-missing",
300
+ description: "A current requirement or ADR that supersedes another must explain why under a 'Why this changed' heading (at least 12 words of prose naming the evidence and answering the old decision's reasoning); supersede edges recorded before schema 9 are acknowledged in the manifest and exempt",
301
+ enforcementClass: "canonical",
302
+ category: "lifecycle",
303
+ },
304
+ {
305
+ name: "supersedes-reason-acknowledged",
306
+ description: "Informational: how many current superseding requirements and ADRs have only supersede edges the schema 9 migration acknowledged without a 'Why this changed' section; one summary finding per check",
307
+ enforcementClass: "advisory",
308
+ category: "lifecycle",
309
+ diagnosticSeverity: "info",
310
+ },
286
311
  {
287
312
  name: "policy-ownership",
288
313
  description: "When a plugin activated for kibi.check-policy.v1 declares ownership rules, every production symbol in a rule's file set must implement a current requirement grounded (requires_predicate) in one of the rule's predicates; projects without an activated check policy are unaffected",
@@ -1,4 +1,4 @@
1
- export declare const LATEST_KB_SCHEMA_VERSION = 8;
1
+ export declare const LATEST_KB_SCHEMA_VERSION = 9;
2
2
  export interface SchemaVersionStatus {
3
3
  status: "missing" | "invalid" | "older" | "current" | "newer";
4
4
  currentVersion: number | null;
@@ -1 +1 @@
1
- {"version":3,"file":"schema-version.d.ts","sourceRoot":"","sources":["../../src/utils/schema-version.ts"],"names":[],"mappings":"AAqBA,eAAO,MAAM,wBAAwB,IAAI,CAAC;AAE1C,MAAM,WAAW,mBAAmB;IAClC,MAAM,EAAE,SAAS,GAAG,SAAS,GAAG,OAAO,GAAG,SAAS,GAAG,OAAO,CAAC;IAC9D,cAAc,EAAE,MAAM,GAAG,IAAI,CAAC;IAC9B,aAAa,EAAE,MAAM,CAAC;IACtB,cAAc,EAAE,OAAO,CAAC;IACxB,OAAO,EAAE,MAAM,GAAG,IAAI,CAAC;CACxB;AAED,MAAM,MAAM,mBAAmB,GAC3B;IAAE,aAAa,EAAE,MAAM,GAAG,MAAM,GAAG,IAAI,GAAG,SAAS,CAAA;CAAE,GACrD,IAAI,GACJ,SAAS,CAAC;AAGd,wBAAgB,sBAAsB,CACpC,aAAa,EAAE,MAAM,GAAG,MAAM,GAAG,IAAI,GAAG,SAAS,GAChD,MAAM,GAAG,IAAI,CAgBf;AAGD,wBAAgB,sBAAsB,CACpC,MAAM,CAAC,EAAE,mBAAmB,GAC3B,mBAAmB,CAgDrB"}
1
+ {"version":3,"file":"schema-version.d.ts","sourceRoot":"","sources":["../../src/utils/schema-version.ts"],"names":[],"mappings":"AAuBA,eAAO,MAAM,wBAAwB,IAAI,CAAC;AAE1C,MAAM,WAAW,mBAAmB;IAClC,MAAM,EAAE,SAAS,GAAG,SAAS,GAAG,OAAO,GAAG,SAAS,GAAG,OAAO,CAAC;IAC9D,cAAc,EAAE,MAAM,GAAG,IAAI,CAAC;IAC9B,aAAa,EAAE,MAAM,CAAC;IACtB,cAAc,EAAE,OAAO,CAAC;IACxB,OAAO,EAAE,MAAM,GAAG,IAAI,CAAC;CACxB;AAED,MAAM,MAAM,mBAAmB,GAC3B;IAAE,aAAa,EAAE,MAAM,GAAG,MAAM,GAAG,IAAI,GAAG,SAAS,CAAA;CAAE,GACrD,IAAI,GACJ,SAAS,CAAC;AAGd,wBAAgB,sBAAsB,CACpC,aAAa,EAAE,MAAM,GAAG,MAAM,GAAG,IAAI,GAAG,SAAS,GAChD,MAAM,GAAG,IAAI,CAgBf;AAGD,wBAAgB,sBAAsB,CACpC,MAAM,CAAC,EAAE,mBAAmB,GAC3B,mBAAmB,CAgDrB"}
@@ -18,7 +18,9 @@
18
18
  // implements REQ-003
19
19
  // Schema 8 requires body context on current entities and pins requirement
20
20
  // semantic_text so context sections never change a requirement's meaning.
21
- export const LATEST_KB_SCHEMA_VERSION = 8;
21
+ // Schema 9 adds decision firmness on requirements and ADRs and requires a
22
+ // "Why this changed" section on new supersedes.
23
+ export const LATEST_KB_SCHEMA_VERSION = 9;
22
24
  // implements REQ-003
23
25
  export function normalizeSchemaVersion(schemaVersion) {
24
26
  if (schemaVersion === undefined || schemaVersion === null) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "kibi-cli",
3
- "version": "2.21.0",
3
+ "version": "2.21.2",
4
4
  "type": "module",
5
5
  "description": "Kibi CLI for knowledge base management",
6
6
  "engines": {
@@ -235,7 +235,7 @@
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.21.0",
238
+ "kibi-core": "^0.21.1",
239
239
  "kibi-plugin-builtin": "^0.5.1",
240
240
  "kibi-plugin-sdk": "^0.5.0",
241
241
  "kibi-swipl": "^1.0.1",
@@ -51,6 +51,10 @@ entity_property(req, approval_ref, string).
51
51
  entity_property(req, rationale, string).
52
52
  % Claim keys of the exempted requirement an exception waives (all when absent).
53
53
  entity_property(req, exempts_claims, list).
54
+ % How firmly a requirement or ADR was decided: firm, default or exploratory.
55
+ % Absent reads as default.
56
+ entity_property(req, firmness, atom).
57
+ entity_property(adr, firmness, atom).
54
58
  % Intended outcome of a scenario: success, rejection or error.
55
59
  entity_property(scenario, expects, atom).
56
60
 
@@ -143,6 +147,8 @@ optional_property(req, approved_by).
143
147
  optional_property(req, approval_ref).
144
148
  optional_property(req, rationale).
145
149
  optional_property(req, exempts_claims).
150
+ optional_property(req, firmness).
151
+ optional_property(adr, firmness).
146
152
  optional_property(test, verification_scope).
147
153
  optional_property(test, verification_perspective).
148
154
  optional_property(test, proof_contract).
@@ -41,6 +41,7 @@ import {
41
41
  stagedEntityIdStyleDiagnostics,
42
42
  } from "./check-helpers.js";
43
43
  import { collectCheckPolicyViolations } from "./check-policy-rules.js";
44
+ import { collectDecisionFirmnessViolations } from "./decision-firmness.js";
44
45
  import { executeStatus } from "./discovery-executors.js";
45
46
  import { collectEntityContextViolations } from "./entity-context.js";
46
47
  import {
@@ -375,6 +376,10 @@ export async function executeCheck(
375
376
  rulesAllowlist,
376
377
  workspaceRoot,
377
378
  );
379
+ const decisionFirmnessFindings = collectDecisionFirmnessViolations(
380
+ rulesAllowlist,
381
+ workspaceRoot,
382
+ );
378
383
  const checkPolicyFindings = await collectCheckPolicyViolations(
379
384
  prolog,
380
385
  rulesAllowlist,
@@ -388,6 +393,7 @@ export async function executeCheck(
388
393
  ...predicateConformanceFindings,
389
394
  ...originReviewFindings,
390
395
  ...entityContextFindings,
396
+ ...decisionFirmnessFindings,
391
397
  ...checkPolicyFindings,
392
398
  ]);
393
399
  const violations: Violation[] = partitioned.violations;
@@ -569,6 +569,8 @@ const BASE_DATA_SCHEMAS: Readonly<
569
569
  "record_ontology_gap",
570
570
  "review_nonlogical",
571
571
  "replace_grounding",
572
+ "extend_requirement",
573
+ "keep_existing_grounding",
572
574
  ],
573
575
  },
574
576
  recommendedPredicateSchema: { type: ["object", "null"] },
@@ -584,7 +586,16 @@ const BASE_DATA_SCHEMAS: Readonly<
584
586
  description:
585
587
  "The planned predicate fact id (FACT-PRED-...) that a requires_predicate link must target. Candidate ids (SUGGEST-...) are never relationship targets.",
586
588
  },
587
- existingGrounding: recordArray,
589
+ existingGrounding: {
590
+ ...recordArray,
591
+ description:
592
+ "The grounding links requirementId already has for this claim: matched by claim_key (the same proposition text) or, for a predicate fact, by the recommended candidate's canonical_key (the same predicate tuple in other words; matchedBy says which).",
593
+ },
594
+ decisionConflicts: {
595
+ ...recordArray,
596
+ description:
597
+ "When no hash-bound plan ran the contradiction analysis: stored decision facts of current requirements that agree with the complete candidate on its key arguments and decide the opposite (requirementId, factId, effect, proposedEffect, canonicalKey). A warning repeats each one.",
598
+ },
588
599
  replacementPlan: { type: ["object", "null"] },
589
600
  bindingHints: {
590
601
  ...recordArray,
@@ -648,6 +659,11 @@ const BASE_DATA_SCHEMAS: Readonly<
648
659
  relationships_created: integerValue,
649
660
  warnings: valueArray,
650
661
  semanticAdvisor: recordValue,
662
+ claimKey: {
663
+ ...stringValue,
664
+ description:
665
+ "For a fact with claim_text: the stored claim_key, derived from claim_text when the payload did not supply it.",
666
+ },
651
667
  status: stringValue,
652
668
  effectFailures: recordArray,
653
669
  nextActions: recordArray,
@@ -685,6 +701,22 @@ const BASE_DATA_SCHEMAS: Readonly<
685
701
  status: stringValue,
686
702
  effectFailures: recordArray,
687
703
  nextActions: recordArray,
704
+ outcome: {
705
+ type: "string",
706
+ enum: [
707
+ "plan_returned",
708
+ "supersession_required",
709
+ "plan_applied",
710
+ "dry_run",
711
+ ],
712
+ description:
713
+ "Absent on a direct delete. plan_returned: deletionPlan was returned and nothing was written; nextActions[0].input is the exact kb_apply_plan input. supersession_required: an authored requirement must be superseded with kb_upsert instead. plan_applied: approvedPlanHash matched and the plan was applied. dry_run: dryRun:true previewed the delete and nothing was written.",
714
+ },
715
+ dryRun: { const: true },
716
+ wouldDelete: recordValue,
717
+ planHash: stringValue,
718
+ sourcePaths: stringArray,
719
+ recoveryJournalId: stringValue,
688
720
  }),
689
721
  kb_check: {
690
722
  description:
@@ -0,0 +1,280 @@
1
+ /*
2
+ Kibi — repo-local, per-branch, queryable long-term memory for software projects
3
+ Copyright (C) 2026 Piotr Franczyk
4
+
5
+ This program is free software: you can redistribute it and/or modify
6
+ it under the terms of the GNU Affero General Public License as published by
7
+ the Free Software Foundation, either version 3 of the License, or
8
+ (at your option) any later version.
9
+
10
+ This program is distributed in the hope that it will be useful,
11
+ but WITHOUT ANY WARRANTY; without even the implied warranty of
12
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
13
+ GNU Affero General Public License for more details.
14
+
15
+ You should have received a copy of the GNU Affero General Public License
16
+ along with this program. If not, see <https://www.gnu.org/licenses/>.
17
+ */
18
+
19
+ import path from "node:path";
20
+ import {
21
+ MIN_WHY_CHANGED_WORDS,
22
+ WHY_THIS_CHANGED_HEADING,
23
+ carriesFirmness,
24
+ effectiveFirmness,
25
+ firmnessRef,
26
+ supersedeAcknowledgement,
27
+ whyThisChangedWords,
28
+ } from "../../decision-firmness.js";
29
+ import {
30
+ listLaneMarkdownFiles,
31
+ readText,
32
+ } from "../../operations/migration/kb-sources.js";
33
+ import { readAllShards } from "../../relationships/shards.js";
34
+ import { readKbManifest } from "../../utils/kb-manifest.js";
35
+ import type { Violation } from "../../utils/rule-registry.js";
36
+ import {
37
+ type AuthoredEntity,
38
+ isCurrentAuthoredEntity,
39
+ readAuthoredEntity,
40
+ } from "./entity-context.js";
41
+
42
+ /** A firm decision must cite the human words it rests on (blocking). */
43
+ // implements REQ-kb-decision-firmness
44
+ export const DECISION_FIRM_UNREFERENCED_RULE = "decision-firm-unreferenced";
45
+ /** Only a firm decision may supersede a firm decision (blocking). */
46
+ // implements REQ-kb-decision-firmness
47
+ export const DECISION_FIRM_SUPERSEDED_RULE = "decision-firm-superseded";
48
+ /** A supersede must say why under "Why this changed" (blocking). */
49
+ // implements REQ-kb-decision-firmness
50
+ export const SUPERSEDES_REASON_MISSING_RULE = "supersedes-reason-missing";
51
+ /** How many legacy supersedes the schema 9 migration acknowledged (info). */
52
+ // implements REQ-kb-decision-firmness
53
+ export const SUPERSEDES_REASON_ACKNOWLEDGED_RULE =
54
+ "supersedes-reason-acknowledged";
55
+
56
+ // implements REQ-kb-decision-firmness
57
+ export const DECISION_FIRMNESS_RULES = [
58
+ DECISION_FIRM_UNREFERENCED_RULE,
59
+ DECISION_FIRM_SUPERSEDED_RULE,
60
+ SUPERSEDES_REASON_MISSING_RULE,
61
+ SUPERSEDES_REASON_ACKNOWLEDGED_RULE,
62
+ ] as const;
63
+
64
+ const DECISION_LANES = ["requirements", "adr"] as const;
65
+
66
+ /** Every authored requirement and ADR. */
67
+ // implements REQ-kb-decision-firmness
68
+ export function listDecisionEntities(workspaceRoot: string): AuthoredEntity[] {
69
+ const entities: AuthoredEntity[] = [];
70
+ for (const file of listLaneMarkdownFiles(workspaceRoot, DECISION_LANES)) {
71
+ const content = readText(file.absolutePath);
72
+ if (content === null) continue;
73
+ const entity = readAuthoredEntity(content, file.relativePath);
74
+ if (entity !== null && carriesFirmness(entity.type)) entities.push(entity);
75
+ }
76
+ return entities;
77
+ }
78
+
79
+ /**
80
+ * Supersedes edges from requirements and ADRs, keyed by the superseding id,
81
+ * read from authored `links` and the relationship shards so the check and the
82
+ * migration agree before a sync. A shard edge whose source entity no longer
83
+ * exists is ignored: it must not make its target look replaced.
84
+ */
85
+ // implements REQ-kb-decision-firmness
86
+ export function authoredSupersedes(
87
+ workspaceRoot: string,
88
+ entities: readonly AuthoredEntity[],
89
+ ): Map<string, Set<string>> {
90
+ const edges = new Map<string, Set<string>>();
91
+ const known = new Set(entities.map((entity) => entity.id));
92
+ const add = (from: string, to: string) => {
93
+ const targets = edges.get(from) ?? new Set<string>();
94
+ targets.add(to);
95
+ edges.set(from, targets);
96
+ };
97
+ for (const entity of entities) {
98
+ const links = entity.data.links;
99
+ if (!Array.isArray(links)) continue;
100
+ for (const link of links) {
101
+ if (
102
+ link !== null &&
103
+ typeof link === "object" &&
104
+ (link as { type?: unknown }).type === "supersedes" &&
105
+ typeof (link as { target?: unknown }).target === "string"
106
+ ) {
107
+ add(entity.id, (link as { target: string }).target);
108
+ }
109
+ }
110
+ }
111
+ try {
112
+ for (const relationship of readAllShards(path.join(workspaceRoot, ".kb"))) {
113
+ if (relationship.type === "supersedes" && known.has(relationship.from))
114
+ add(relationship.from, relationship.to);
115
+ }
116
+ } catch {
117
+ // Malformed shards fail sync with their own diagnostic.
118
+ }
119
+ return edges;
120
+ }
121
+
122
+ function supersededIds(edges: ReadonlyMap<string, Set<string>>): Set<string> {
123
+ const superseded = new Set<string>();
124
+ for (const targets of edges.values())
125
+ for (const target of targets) superseded.add(target);
126
+ return superseded;
127
+ }
128
+
129
+ /**
130
+ * Superseding requirements and ADRs, whatever their status, without a "Why
131
+ * this changed" explanation. The schema 9 migration acknowledges their edges
132
+ * once, retired ones included, so one that becomes current again does not
133
+ * start blocking; read-only.
134
+ */
135
+ // implements REQ-kb-decision-firmness
136
+ export function unexplainedSupersedes(
137
+ entities: readonly AuthoredEntity[],
138
+ edges: ReadonlyMap<string, Set<string>>,
139
+ ): AuthoredEntity[] {
140
+ return entities
141
+ .filter(
142
+ (entity) =>
143
+ edges.has(entity.id) &&
144
+ whyThisChangedWords(entity.body) < MIN_WHY_CHANGED_WORDS,
145
+ )
146
+ .sort((left, right) => left.id.localeCompare(right.id));
147
+ }
148
+
149
+ /** The supersede edges of an entity, as the manifest acknowledges them. */
150
+ // implements REQ-kb-decision-firmness
151
+ export function supersedeAcknowledgements(
152
+ id: string,
153
+ edges: ReadonlyMap<string, Set<string>>,
154
+ ): string[] {
155
+ return [...(edges.get(id) ?? [])]
156
+ .sort()
157
+ .map((target) => supersedeAcknowledgement(id, target));
158
+ }
159
+
160
+ /** Evaluate the firmness rules over authored requirements and ADRs. */
161
+ // implements REQ-kb-decision-firmness
162
+ export function evaluateDecisionFirmness(
163
+ entities: readonly AuthoredEntity[],
164
+ edges: ReadonlyMap<string, Set<string>>,
165
+ rules: ReadonlySet<string> = new Set(DECISION_FIRMNESS_RULES),
166
+ acknowledged: ReadonlySet<string> = new Set(),
167
+ ): Violation[] {
168
+ const findings: Violation[] = [];
169
+ const superseded = supersededIds(edges);
170
+ const byId = new Map(entities.map((entity) => [entity.id, entity]));
171
+ const current = entities
172
+ .filter((entity) => isCurrentAuthoredEntity(entity, superseded))
173
+ .sort((left, right) => left.id.localeCompare(right.id));
174
+ const firmnessOf = (entity: AuthoredEntity) =>
175
+ effectiveFirmness(entity.type, entity.data.firmness);
176
+
177
+ if (rules.has(DECISION_FIRM_UNREFERENCED_RULE)) {
178
+ for (const entity of current) {
179
+ if (firmnessOf(entity) !== "firm") continue;
180
+ if (firmnessRef({ type: entity.type, ...entity.data }) !== undefined)
181
+ continue;
182
+ findings.push({
183
+ rule: DECISION_FIRM_UNREFERENCED_RULE,
184
+ entityId: entity.id,
185
+ description: `${entity.id} is marked firmness: firm but names no human words it rests on (origin.ref${entity.type === "req" ? " or approval_ref" : ""} is missing)`,
186
+ suggestion:
187
+ "Set origin.ref to the message, review or ticket where a human stated this rule; if no human stated it, use firmness: default (or exploratory for a guess)",
188
+ source: entity.path,
189
+ evidence: { type: entity.type, firmness: "firm" },
190
+ });
191
+ }
192
+ }
193
+
194
+ // Every superseder counts, retired ones too: replacing a non-firm
195
+ // superseder must not launder the overruled firm decision.
196
+ if (rules.has(DECISION_FIRM_SUPERSEDED_RULE)) {
197
+ for (const entity of [...entities].sort((left, right) =>
198
+ left.id.localeCompare(right.id),
199
+ )) {
200
+ if (firmnessOf(entity) === "firm") continue;
201
+ const firmTargets = [...(edges.get(entity.id) ?? [])]
202
+ .filter((id) => {
203
+ const target = byId.get(id);
204
+ return (
205
+ target !== undefined &&
206
+ effectiveFirmness(target.type, target.data.firmness) === "firm"
207
+ );
208
+ })
209
+ .sort();
210
+ if (firmTargets.length === 0) continue;
211
+ findings.push({
212
+ rule: DECISION_FIRM_SUPERSEDED_RULE,
213
+ entityId: entity.id,
214
+ description: `${entity.id} (firmness: ${firmnessOf(entity)}) supersedes the firm decision${firmTargets.length === 1 ? "" : "s"} ${firmTargets.join(", ")}; only a human may change a firm decision`,
215
+ suggestion:
216
+ "Ask the person who stated the firm decision; if they agree, mark the replacement firmness: firm with origin.ref to their words, otherwise remove the supersedes link and propose the change instead",
217
+ source: entity.path,
218
+ evidence: { firmness: firmnessOf(entity), firmTargets },
219
+ });
220
+ }
221
+ }
222
+
223
+ const currentIds = new Set(current.map((entity) => entity.id));
224
+ const unexplained = unexplainedSupersedes(entities, edges).filter((entity) =>
225
+ currentIds.has(entity.id),
226
+ );
227
+ const isAcknowledged = (entity: AuthoredEntity) =>
228
+ supersedeAcknowledgements(entity.id, edges).every((edge) =>
229
+ acknowledged.has(edge),
230
+ );
231
+ const exempt = unexplained.filter(isAcknowledged);
232
+ if (rules.has(SUPERSEDES_REASON_MISSING_RULE)) {
233
+ for (const entity of unexplained) {
234
+ if (isAcknowledged(entity)) continue;
235
+ const targets = [...(edges.get(entity.id) ?? [])].sort();
236
+ const unacknowledged = targets.filter(
237
+ (target) =>
238
+ !acknowledged.has(supersedeAcknowledgement(entity.id, target)),
239
+ );
240
+ findings.push({
241
+ rule: SUPERSEDES_REASON_MISSING_RULE,
242
+ entityId: entity.id,
243
+ description: `${entity.id} supersedes ${unacknowledged.join(", ")} without a '## ${WHY_THIS_CHANGED_HEADING}' section of at least ${MIN_WHY_CHANGED_WORDS} words`,
244
+ suggestion: `Add '## ${WHY_THIS_CHANGED_HEADING}' to the body: name the evidence (a failing test, a measured cost, a constraint the old decision missed, a new requirement) and answer the superseded decision's reasoning. A preference is not evidence; propose it instead of superseding.`,
245
+ source: entity.path,
246
+ evidence: { type: entity.type, supersedes: unacknowledged },
247
+ });
248
+ }
249
+ }
250
+ if (rules.has(SUPERSEDES_REASON_ACKNOWLEDGED_RULE) && exempt.length) {
251
+ findings.push({
252
+ rule: SUPERSEDES_REASON_ACKNOWLEDGED_RULE,
253
+ entityId: "workspace",
254
+ description: `${exempt.length} current superseding requirement${exempt.length === 1 ? " or ADR was" : "s and ADRs were"} recorded before schema 9 without a '${WHY_THIS_CHANGED_HEADING}' section`,
255
+ suggestion:
256
+ "When you next touch one, add the reason if the history or a person states it; never invent one",
257
+ evidence: {
258
+ total: exempt.length,
259
+ ids: exempt.slice(0, 25).map((entity) => entity.id),
260
+ },
261
+ });
262
+ }
263
+ return findings;
264
+ }
265
+
266
+ /** Read the authored decisions once and evaluate the selected rules. */
267
+ // implements REQ-kb-decision-firmness
268
+ export function collectDecisionFirmnessViolations(
269
+ rules: ReadonlySet<string>,
270
+ workspaceRoot: string,
271
+ ): Violation[] {
272
+ if (!DECISION_FIRMNESS_RULES.some((rule) => rules.has(rule))) return [];
273
+ const entities = listDecisionEntities(workspaceRoot);
274
+ return evaluateDecisionFirmness(
275
+ entities,
276
+ authoredSupersedes(workspaceRoot, entities),
277
+ rules,
278
+ new Set(readKbManifest(workspaceRoot)?.supersedesReasonAcknowledged ?? []),
279
+ );
280
+ }
@@ -1,5 +1,6 @@
1
1
  import path from "node:path";
2
2
  import { compilerFingerprintReason } from "../../commands/sync/cache.js";
3
+ import { effectiveFirmness } from "../../decision-firmness.js";
3
4
  import { EngineClient } from "../../engine.js";
4
5
  import {
5
6
  type IntentSearchAnalysis,
@@ -514,7 +515,13 @@ function factRowSummary(row: Record<string, unknown>): string {
514
515
  const names = Array.isArray(row.argumentNames)
515
516
  ? row.argumentNames.map(String).join(", ")
516
517
  : "";
517
- return `${row.name}(${names})`;
518
+ // A built-in schema is no stored entity: its id is for kb_model's
519
+ // schemaId, not for a kb_query by id.
520
+ const builtin =
521
+ row.builtin === true
522
+ ? ` [builtin${id ? ` ${id}` : ""}, not a stored entity]`
523
+ : "";
524
+ return `${row.name}(${names})${builtin}`;
518
525
  }
519
526
  return `${id} (${String(row.title ?? "")})`;
520
527
  }
@@ -695,7 +702,15 @@ export async function executeQuery(
695
702
  const indexedPage = prolog.queryEntities
696
703
  ? await prolog.queryEntities(pageInput, signal)
697
704
  : await queryEntitiesViaQuery(prolog, pageInput, signal);
698
- const paginated = indexedPage.entities;
705
+ // A requirement or ADR without authored firmness reads as `default`;
706
+ // the row says so, so a reader never has to know the default.
707
+ // implements REQ-kb-decision-firmness
708
+ const paginated = indexedPage.entities.map((entity) => {
709
+ const firmness = effectiveFirmness(entity.type, entity.firmness);
710
+ return firmness === undefined || entity.firmness === firmness
711
+ ? entity
712
+ : { ...entity, firmness };
713
+ });
699
714
  const text =
700
715
  indexedPage.count === 0
701
716
  ? `No entities found${type ? ` of type '${type}'` : ""}.`
@@ -750,7 +750,7 @@ export function buildActionsFromStatus(input: {
750
750
  invocation: legacyStorageBlocked
751
751
  ? {
752
752
  kind: "review",
753
- instruction: `The legacy .kb/config.json still names knowledge files outside .kb/ and Kibi cannot move them: ${legacyStorage?.blockers.join(" ")} Keep one copy of each conflicting file (the .kb/ one is what Kibi compiles), remove or git-rm the legacy copy, then run 'kibi migrate --yes' to retire .kb/config.json.`,
753
+ instruction: `The legacy .kb/config.json still names knowledge files outside .kb/ and Kibi cannot move them: ${legacyStorage?.blockers.join(" ")} Compare each legacy copy with its .kb/ copy and keep the current one: replace the .kb/ copy when the legacy one is newer, or remove (git rm) the legacy copy when it is a stale duplicate. Then run 'kibi migrate --yes' to retire .kb/config.json.`,
754
754
  }
755
755
  : { kind: "cli", command_argv: ["kibi", "migrate", "--yes"] },
756
756
  affectedFiles: [
@@ -55,6 +55,10 @@ export const SELECTABLE_RULE_NAMES: readonly string[] = [
55
55
  "predicate-schema-conformance",
56
56
  "entity-context-missing",
57
57
  "entity-context-acknowledged",
58
+ "decision-firm-unreferenced",
59
+ "decision-firm-superseded",
60
+ "supersedes-reason-missing",
61
+ "supersedes-reason-acknowledged",
58
62
  "policy-ownership",
59
63
  "policy-markers",
60
64
  ];
@@ -62,4 +66,4 @@ export const SELECTABLE_RULE_NAMES: readonly string[] = [
62
66
  // implements REQ-006
63
67
  /** Comma-separated "Allowed:" fragment for the kb_check rules description. */
64
68
  export const SELECTABLE_RULES_ALLOWED =
65
- "must-priority-coverage, symbol-coverage, symbol-traceability, no-dangling-refs, source-relationship-parity, branch-store-not-compiled, source-path-dangling, no-cycles, required-fields, deprecated-adr-no-successor, superseded-requirement-open, scenario-feasibility, scenario-feasibility-unknown, exception-claim-keys, numeric-string-value, rule-key-arguments-missing, domain-contradictions, logic-coverage, rule-safety, rule-verifiability, query-plan-safety, req-status-vocabulary, strict-fact-shape, strict-req-fact-pairing, predicate-verifiability, strict-readiness, semantic-completeness, related-requirement-unmodeled, proof-contract-symbols, entity-id-style, domain-redundancy, domain-implication, subject-key-identity, subject-key-shape, ontology-quality, exception-unapproved, exception-approval-self-attested, agent-requirement-unapproved, requirement-rationale-missing, symbol-owner-superseded, adr-unlinked, adr-proposed, predicate-schema-conformance, entity-context-missing, entity-context-acknowledged, policy-ownership, policy-markers";
69
+ "must-priority-coverage, symbol-coverage, symbol-traceability, no-dangling-refs, source-relationship-parity, branch-store-not-compiled, source-path-dangling, no-cycles, required-fields, deprecated-adr-no-successor, superseded-requirement-open, scenario-feasibility, scenario-feasibility-unknown, exception-claim-keys, numeric-string-value, rule-key-arguments-missing, domain-contradictions, logic-coverage, rule-safety, rule-verifiability, query-plan-safety, req-status-vocabulary, strict-fact-shape, strict-req-fact-pairing, predicate-verifiability, strict-readiness, semantic-completeness, related-requirement-unmodeled, proof-contract-symbols, entity-id-style, domain-redundancy, domain-implication, subject-key-identity, subject-key-shape, ontology-quality, exception-unapproved, exception-approval-self-attested, agent-requirement-unapproved, requirement-rationale-missing, symbol-owner-superseded, adr-unlinked, adr-proposed, predicate-schema-conformance, entity-context-missing, entity-context-acknowledged, decision-firm-unreferenced, decision-firm-superseded, supersedes-reason-missing, supersedes-reason-acknowledged, policy-ownership, policy-markers";
@@ -125,6 +125,12 @@ export const ENTITY_PROPERTIES_SCHEMA = {
125
125
  description:
126
126
  "Requirement-only. Optional reference to the exception's approval decision record.",
127
127
  },
128
+ firmness: {
129
+ type: "string",
130
+ enum: ["firm", "default", "exploratory"],
131
+ description:
132
+ "req/adr only. firm = a human stated it as a rule (needs origin.ref to their words); default (when absent) = delegated or agent choice; exploratory = flagged guess.",
133
+ },
128
134
  rationale: {
129
135
  type: "string",
130
136
  minLength: 1,
@@ -39,7 +39,7 @@ export const deleteSpec = {
39
39
  name: "kb_delete",
40
40
  cliName: "delete",
41
41
  description:
42
- "Delete entities by ID. Use only for intentional removals after dependency checks. Do not use as a bulk cleanup shortcut. Side effects: mutates and saves KB; skips entities with dependents.",
42
+ "Delete entities by ID, or exact relationship triples. Use only for intentional removals after dependency checks. Do not use as a bulk cleanup shortcut. An entity authored in a Markdown or YAML source is not deleted directly: the call writes nothing and returns outcome plan_returned with a hash-bound deletionPlan and a nextActions entry holding the exact kb_apply_plan input ({plan, approvedPlanHash}); apply that unmodified, or repeat this call with approvedPlanHash = deletionPlan.planHash to apply the freshly computed plan in one step (a stale or different hash fails with DELETE_PLAN_HASH_MISMATCH and writes nothing). Authored requirements are never deleted: outcome supersession_required points to kb_upsert with a supersedes relationship. dryRun:true previews any delete and writes nothing. Side effects: mutates and saves KB and authored sources; skips entities with dependents; dryRun and plan_returned write nothing.",
43
43
  businessInputSchema: {
44
44
  type: "object",
45
45
  oneOf: [
@@ -73,7 +73,12 @@ export const deleteSpec = {
73
73
  type: "string",
74
74
  pattern: "^[a-fA-F0-9]{64}$",
75
75
  description:
76
- "Exact hash returned by a prior authored-entity deletion plan.",
76
+ "With ids only: the deletionPlan.planHash a prior call for the same ids returned. The plan is recomputed and applied through kb_apply_plan when its hash still matches; a mismatch fails with DELETE_PLAN_HASH_MISMATCH and writes nothing. Not allowed with relationships or dryRun.",
77
+ },
78
+ dryRun: {
79
+ type: "boolean",
80
+ description:
81
+ "Preview without writing. Relationship and direct entity deletes run their preflight and return outcome dry_run with wouldDelete counts, the sourceWrites (paths and hashes) a commit would make, and skippedEffects. Authored ids return the same deletion plan as without dryRun.",
77
82
  },
78
83
  },
79
84
  },