kibi-cli 2.21.0 → 2.22.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 (164) 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-foundation.d.ts.map +1 -1
  4. package/dist/cli-register-foundation.js +1 -0
  5. package/dist/commands/check.d.ts +0 -1
  6. package/dist/commands/check.d.ts.map +1 -1
  7. package/dist/commands/check.js +47 -111
  8. package/dist/commands/doctor.d.ts.map +1 -1
  9. package/dist/commands/doctor.js +107 -5
  10. package/dist/commands/init-helpers.d.ts +14 -1
  11. package/dist/commands/init-helpers.d.ts.map +1 -1
  12. package/dist/commands/init-helpers.js +49 -10
  13. package/dist/commands/init.d.ts +1 -0
  14. package/dist/commands/init.d.ts.map +1 -1
  15. package/dist/commands/init.js +27 -4
  16. package/dist/commands/legacy-storage-migration.d.ts +46 -1
  17. package/dist/commands/legacy-storage-migration.d.ts.map +1 -1
  18. package/dist/commands/legacy-storage-migration.js +244 -9
  19. package/dist/commands/migrate.d.ts.map +1 -1
  20. package/dist/commands/migrate.js +150 -166
  21. package/dist/commands/sync/manifest.d.ts.map +1 -1
  22. package/dist/commands/sync/manifest.js +9 -0
  23. package/dist/commands/sync/persistence.d.ts.map +1 -1
  24. package/dist/commands/sync/persistence.js +4 -0
  25. package/dist/decision-firmness.d.ts +39 -0
  26. package/dist/decision-firmness.d.ts.map +1 -0
  27. package/dist/decision-firmness.js +115 -0
  28. package/dist/extractors/markdown.d.ts +2 -0
  29. package/dist/extractors/markdown.d.ts.map +1 -1
  30. package/dist/extractors/markdown.js +14 -0
  31. package/dist/extractors/symbols-coordinator.js +1 -0
  32. package/dist/intent-search.d.ts.map +1 -1
  33. package/dist/intent-search.js +28 -6
  34. package/dist/operations/migration/inventory-rederive.d.ts +40 -1
  35. package/dist/operations/migration/inventory-rederive.d.ts.map +1 -1
  36. package/dist/operations/migration/inventory-rederive.js +99 -8
  37. package/dist/operations/migration/requirement-status.d.ts +25 -0
  38. package/dist/operations/migration/requirement-status.d.ts.map +1 -0
  39. package/dist/operations/migration/requirement-status.js +105 -0
  40. package/dist/operations/migration/schema6.d.ts +27 -0
  41. package/dist/operations/migration/schema6.d.ts.map +1 -1
  42. package/dist/operations/migration/schema6.js +308 -7
  43. package/dist/operations/migration/supersedes-reason.d.ts +26 -0
  44. package/dist/operations/migration/supersedes-reason.d.ts.map +1 -0
  45. package/dist/operations/migration/supersedes-reason.js +69 -0
  46. package/dist/operations/migration/symbol-legacy-links.d.ts +51 -0
  47. package/dist/operations/migration/symbol-legacy-links.d.ts.map +1 -0
  48. package/dist/operations/migration/symbol-legacy-links.js +319 -0
  49. package/dist/operations/modeling/predicate-applyplan.d.ts.map +1 -1
  50. package/dist/operations/modeling/predicate-applyplan.js +10 -2
  51. package/dist/operations/modeling/predicate-loader.d.ts.map +1 -1
  52. package/dist/operations/modeling/predicate-loader.js +11 -0
  53. package/dist/operations/modeling/predicate-types.d.ts +32 -1
  54. package/dist/operations/modeling/predicate-types.d.ts.map +1 -1
  55. package/dist/operations/modeling/suggest-predicates.d.ts.map +1 -1
  56. package/dist/operations/modeling/suggest-predicates.js +175 -35
  57. package/dist/operations/mutation/context-warning.d.ts +12 -2
  58. package/dist/operations/mutation/context-warning.d.ts.map +1 -1
  59. package/dist/operations/mutation/context-warning.js +6 -6
  60. package/dist/operations/mutation/delete.d.ts +5 -1
  61. package/dist/operations/mutation/delete.d.ts.map +1 -1
  62. package/dist/operations/mutation/delete.js +223 -17
  63. package/dist/operations/mutation/serialization.d.ts.map +1 -1
  64. package/dist/operations/mutation/serialization.js +1 -0
  65. package/dist/operations/mutation/source-authoring.d.ts.map +1 -1
  66. package/dist/operations/mutation/source-authoring.js +130 -35
  67. package/dist/operations/mutation/types.d.ts +39 -1
  68. package/dist/operations/mutation/types.d.ts.map +1 -1
  69. package/dist/operations/mutation/upsert.d.ts +17 -2
  70. package/dist/operations/mutation/upsert.d.ts.map +1 -1
  71. package/dist/operations/mutation/upsert.js +82 -13
  72. package/dist/operations/mutation/validate-upsert.d.ts.map +1 -1
  73. package/dist/operations/mutation/validate-upsert.js +40 -8
  74. package/dist/operations/planning/apply-plan.d.ts +6 -1
  75. package/dist/operations/planning/apply-plan.d.ts.map +1 -1
  76. package/dist/operations/planning/apply-plan.js +26 -5
  77. package/dist/public/impact/diagnostics.d.ts.map +1 -1
  78. package/dist/public/impact/diagnostics.js +24 -14
  79. package/dist/public/operations/catalog.d.ts +14 -5
  80. package/dist/public/operations/catalog.d.ts.map +1 -1
  81. package/dist/public/operations/check-executor.d.ts.map +1 -1
  82. package/dist/public/operations/check-executor.js +6 -0
  83. package/dist/public/operations/contracts.d.ts.map +1 -1
  84. package/dist/public/operations/contracts.js +35 -2
  85. package/dist/public/operations/decision-firmness.d.ts +34 -0
  86. package/dist/public/operations/decision-firmness.d.ts.map +1 -0
  87. package/dist/public/operations/decision-firmness.js +216 -0
  88. package/dist/public/operations/discovery-executors.d.ts +2 -0
  89. package/dist/public/operations/discovery-executors.d.ts.map +1 -1
  90. package/dist/public/operations/discovery-executors.js +25 -4
  91. package/dist/public/operations/migration-plan.d.ts +2 -0
  92. package/dist/public/operations/migration-plan.d.ts.map +1 -1
  93. package/dist/public/operations/migration-plan.js +101 -16
  94. package/dist/public/operations/result-envelope.d.ts.map +1 -1
  95. package/dist/public/operations/result-envelope.js +27 -1
  96. package/dist/public/operations/specs/check-rules.generated.d.ts +1 -1
  97. package/dist/public/operations/specs/check-rules.generated.d.ts.map +1 -1
  98. package/dist/public/operations/specs/check-rules.generated.js +6 -1
  99. package/dist/public/operations/specs/check.d.ts +1 -1
  100. package/dist/public/operations/specs/mutation-schemas.d.ts +5 -0
  101. package/dist/public/operations/specs/mutation-schemas.d.ts.map +1 -1
  102. package/dist/public/operations/specs/mutation-schemas.js +5 -0
  103. package/dist/public/operations/specs/mutation.d.ts +6 -2
  104. package/dist/public/operations/specs/mutation.d.ts.map +1 -1
  105. package/dist/public/operations/specs/mutation.js +6 -2
  106. package/dist/public/operations/specs/planning.d.ts +2 -2
  107. package/dist/public/operations/specs/planning.js +2 -2
  108. package/dist/public/operations/specs/upsert.d.ts +5 -0
  109. package/dist/public/operations/specs/upsert.d.ts.map +1 -1
  110. package/dist/public/operations/symbol-granularity-check.d.ts +18 -0
  111. package/dist/public/operations/symbol-granularity-check.d.ts.map +1 -0
  112. package/dist/public/operations/symbol-granularity-check.js +175 -0
  113. package/dist/public/schemas/entity.d.ts.map +1 -1
  114. package/dist/public/schemas/entity.js +11 -0
  115. package/dist/public/skills/kibi-traceability/SKILL.md +6 -4
  116. package/dist/public/skills/kibi-usage/SKILL.md +50 -10
  117. package/dist/public/skills/kibi-usage/resources/source-authoring.md +4 -2
  118. package/dist/public/symbol-granularity.d.ts +17 -0
  119. package/dist/public/symbol-granularity.d.ts.map +1 -1
  120. package/dist/public/symbol-granularity.js +21 -0
  121. package/dist/relationships/shards.d.ts +10 -0
  122. package/dist/relationships/shards.d.ts.map +1 -1
  123. package/dist/relationships/shards.js +27 -11
  124. package/dist/schemas/entity.schema.json +28 -0
  125. package/dist/search-answer.d.ts +31 -2
  126. package/dist/search-answer.d.ts.map +1 -1
  127. package/dist/search-answer.js +115 -17
  128. package/dist/traceability/evidence-model.d.ts +7 -3
  129. package/dist/traceability/evidence-model.d.ts.map +1 -1
  130. package/dist/traceability/staged-symbols-manifest.js +6 -5
  131. package/dist/traceability/validate.d.ts +14 -1
  132. package/dist/traceability/validate.d.ts.map +1 -1
  133. package/dist/traceability/validate.js +41 -0
  134. package/dist/types/entities.d.ts +4 -0
  135. package/dist/types/entities.d.ts.map +1 -1
  136. package/dist/utils/kb-manifest.d.ts +6 -0
  137. package/dist/utils/kb-manifest.d.ts.map +1 -1
  138. package/dist/utils/kb-manifest.js +6 -0
  139. package/dist/utils/rule-registry.generated.d.ts +26 -0
  140. package/dist/utils/rule-registry.generated.d.ts.map +1 -1
  141. package/dist/utils/rule-registry.generated.js +31 -0
  142. package/dist/utils/schema-version.d.ts +1 -1
  143. package/dist/utils/schema-version.d.ts.map +1 -1
  144. package/dist/utils/schema-version.js +3 -1
  145. package/package.json +3 -3
  146. package/schema/entities.pl +6 -0
  147. package/src/public/impact/diagnostics.ts +29 -17
  148. package/src/public/operations/check-executor.ts +13 -0
  149. package/src/public/operations/contracts.ts +39 -2
  150. package/src/public/operations/decision-firmness.ts +280 -0
  151. package/src/public/operations/discovery-executors.ts +29 -4
  152. package/src/public/operations/migration-plan.ts +100 -19
  153. package/src/public/operations/result-envelope.ts +34 -1
  154. package/src/public/operations/specs/check-rules.generated.ts +6 -1
  155. package/src/public/operations/specs/mutation-schemas.ts +6 -0
  156. package/src/public/operations/specs/mutation.ts +7 -2
  157. package/src/public/operations/specs/planning.ts +2 -2
  158. package/src/public/operations/symbol-granularity-check.ts +233 -0
  159. package/src/public/schemas/entity.ts +12 -0
  160. package/src/public/skills/kibi-traceability/SKILL.md +6 -4
  161. package/src/public/skills/kibi-usage/SKILL.md +50 -10
  162. package/src/public/skills/kibi-usage/resources/source-authoring.md +4 -2
  163. package/src/public/symbol-granularity.ts +37 -0
  164. package/src/schemas/entity.schema.json +28 -0
@@ -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,
@@ -94,6 +95,8 @@ export type QueryPayload = {
94
95
  readonly entities: readonly Record<string, unknown>[];
95
96
  readonly count: number;
96
97
  readonly factQuery?: FactQueryPayload;
98
+ /** Present when an empty answer came from a store never compiled for this branch. */
99
+ readonly branchStore?: SearchBranchStoreNotice;
97
100
  };
98
101
 
99
102
  export const FACT_KINDS = [
@@ -514,7 +517,13 @@ function factRowSummary(row: Record<string, unknown>): string {
514
517
  const names = Array.isArray(row.argumentNames)
515
518
  ? row.argumentNames.map(String).join(", ")
516
519
  : "";
517
- return `${row.name}(${names})`;
520
+ // A built-in schema is no stored entity: its id is for kb_model's
521
+ // schemaId, not for a kb_query by id.
522
+ const builtin =
523
+ row.builtin === true
524
+ ? ` [builtin${id ? ` ${id}` : ""}, not a stored entity]`
525
+ : "";
526
+ return `${row.name}(${names})${builtin}`;
518
527
  }
519
528
  return `${id} (${String(row.title ?? "")})`;
520
529
  }
@@ -695,10 +704,22 @@ export async function executeQuery(
695
704
  const indexedPage = prolog.queryEntities
696
705
  ? await prolog.queryEntities(pageInput, signal)
697
706
  : await queryEntitiesViaQuery(prolog, pageInput, signal);
698
- const paginated = indexedPage.entities;
707
+ // A requirement or ADR without authored firmness reads as `default`;
708
+ // the row says so, so a reader never has to know the default.
709
+ // implements REQ-kb-decision-firmness
710
+ const paginated = indexedPage.entities.map((entity) => {
711
+ const firmness = effectiveFirmness(entity.type, entity.firmness);
712
+ return firmness === undefined || entity.firmness === firmness
713
+ ? entity
714
+ : { ...entity, firmness };
715
+ });
716
+ // An empty page from a never-compiled store says nothing about the
717
+ // authored entities, so the answer names the store state.
718
+ const storeNotice =
719
+ indexedPage.count === 0 ? uncompiledSearchNotice(context) : null;
699
720
  const text =
700
721
  indexedPage.count === 0
701
- ? `No entities found${type ? ` of type '${type}'` : ""}.`
722
+ ? `No entities found${type ? ` of type '${type}'` : ""}.${storeNotice ? ` Note: ${storeNotice.detail} Run kibi sync before relying on query results.` : ""}`
702
723
  : `Found ${indexedPage.count} entities${type ? ` of type '${type}'` : ""}. Showing ${paginated.length} (offset ${offset}, limit ${limit}): ${paginated
703
724
  .map((entity) => {
704
725
  const entityId = String(entity.id ?? "").replace(
@@ -710,7 +731,11 @@ export async function executeQuery(
710
731
  .join(", ")}`;
711
732
  return {
712
733
  content: [{ type: "text", text }],
713
- structuredContent: { entities: paginated, count: indexedPage.count },
734
+ structuredContent: {
735
+ entities: paginated,
736
+ count: indexedPage.count,
737
+ ...(storeNotice ? { branchStore: storeNotice } : {}),
738
+ },
714
739
  };
715
740
  } catch (error) {
716
741
  const message = error instanceof Error ? error.message : String(error);
@@ -396,6 +396,66 @@ function predicateSchemaAlignmentAction(
396
396
  /** Migration code for mechanical predicate namespace/alias repairs. */
397
397
  export const PREDICATE_SCHEMA_ALIGNMENT_CODE = "predicate_schema_alignment";
398
398
 
399
+ /** Migration code of the grouped untracked-source parity findings. */
400
+ export const UNTRACKED_KNOWLEDGE_SOURCES_CODE = "untracked_knowledge_sources";
401
+
402
+ /**
403
+ * Every source-relationship-parity finding explained by untracked files
404
+ * (evidence `untrackedSources`) has one cause and one fix: the files Git
405
+ * does not track and no pending-source receipt covers. After a storage move
406
+ * that can be thousands of relationships, so they become one review action
407
+ * naming the files instead of one per relationship.
408
+ */
409
+ // implements REQ-agent-guided-migration-orchestration, REQ-kibi-source-relationship-parity
410
+ function untrackedSourceActionFromViolations(
411
+ violations: readonly Readonly<Record<string, unknown>>[],
412
+ ): { action: MigrationAction; consumed: ReadonlySet<number> } | null {
413
+ const consumed = new Set<number>();
414
+ const files = new Set<string>();
415
+ const entityIds = new Set<string>();
416
+ const relationships: unknown[] = [];
417
+ for (const [index, violation] of violations.entries()) {
418
+ if (violation.rule !== "source-relationship-parity") continue;
419
+ const evidence = isRecord(violation.evidence) ? violation.evidence : {};
420
+ const sources = Array.isArray(evidence.untrackedSources)
421
+ ? evidence.untrackedSources.filter(isRecord)
422
+ : [];
423
+ if (sources.length === 0) continue;
424
+ consumed.add(index);
425
+ for (const source of sources) {
426
+ if (typeof source.file === "string") files.add(source.file);
427
+ if (typeof source.entityId === "string") entityIds.add(source.entityId);
428
+ }
429
+ if (evidence.relationship !== undefined)
430
+ relationships.push(evidence.relationship);
431
+ }
432
+ if (consumed.size === 0) return null;
433
+ const listed = [...files].sort();
434
+ return {
435
+ consumed,
436
+ action: migrationAction({
437
+ id: "untracked-knowledge-sources",
438
+ code: UNTRACKED_KNOWLEDGE_SOURCES_CODE,
439
+ category: "relationship",
440
+ safety: "review",
441
+ invocation: {
442
+ kind: "review",
443
+ instruction: `${consumed.size} authored relationship(s) are missing from the compiled KB because ${listed.length} file(s) declaring their endpoints are not tracked by Git and no pending-source receipt covers them: ${listed.join(", ")}. Stage them (git add; staging is enough, Kibi never stages files itself) or rewrite the entities with kb_upsert, then run kibi sync and rerun kibi migrate.`,
444
+ },
445
+ affectedEntityIds: [...entityIds],
446
+ affectedFiles: listed,
447
+ postconditions: [{ rule: "source-relationship-parity", findings: 0 }],
448
+ evidence: {
449
+ relationshipCount: consumed.size,
450
+ files: listed,
451
+ // A sample; the count above is the full number.
452
+ relationships: relationships.slice(0, 20),
453
+ },
454
+ dispositionRequired: true,
455
+ }),
456
+ };
457
+ }
458
+
399
459
  // implements REQ-agent-guided-migration-orchestration, REQ-kibi-predicate-vocabulary-migration, REQ-cli-schema-migration
400
460
  export function buildActionsFromCheck(input: {
401
461
  violations?: readonly Readonly<Record<string, unknown>>[];
@@ -404,8 +464,11 @@ export function buildActionsFromCheck(input: {
404
464
  const actions: MigrationAction[] = [];
405
465
  const lifecycle = lifecycleActionsFromViolations(input.violations ?? []);
406
466
  actions.push(...lifecycle.actions.map(migrationAction));
467
+ const untracked = untrackedSourceActionFromViolations(input.violations ?? []);
468
+ if (untracked !== null) actions.push(untracked.action);
407
469
  for (const [index, violation] of (input.violations ?? []).entries()) {
408
470
  if (lifecycle.consumed.has(index)) continue;
471
+ if (untracked?.consumed.has(index)) continue;
409
472
  const rule = typeof violation.rule === "string" ? violation.rule : "check";
410
473
  const entityId =
411
474
  typeof violation.entityId === "string" ? violation.entityId : "";
@@ -621,6 +684,18 @@ function readLegacySchemaVersion(
621
684
  /** Migration action code of the automatic recompile of changed sources. */
622
685
  export const FRESHNESS_RESYNC = "freshness_resync";
623
686
 
687
+ // implements REQ-cli-status-pre-first-sync
688
+ /**
689
+ * Stale reasons one `kibi sync` clears: sources whose content changed since
690
+ * the last compilation, and a store compiled under another compiler contract
691
+ * (the sync discards that cache, re-imports every source and stamps the
692
+ * current contract).
693
+ */
694
+ const RESYNC_REASON_CODES: ReadonlySet<string> = new Set([
695
+ ...MTIME_SOURCE_REASON_CODES,
696
+ "compiler_changed",
697
+ ]);
698
+
624
699
  export function buildActionsFromStatus(input: {
625
700
  workspaceRoot: string;
626
701
  branchAttachment?: Readonly<Record<string, unknown>>;
@@ -686,17 +761,20 @@ export function buildActionsFromStatus(input: {
686
761
  }
687
762
 
688
763
  const storeState = typeof store?.state === "string" ? store.state : null;
689
- const legacyStorageMigrationRequired = legacyConfigExists(
690
- input.workspaceRoot,
691
- );
764
+ // Legacy storage is evaluated whether or not the legacy config survives:
765
+ // legacy copies left beside a migrated .kb/ (moved, identical or stale)
766
+ // are the same finding 'kibi doctor' and 'kibi migrate' report.
767
+ // implements REQ-cli-schema-migration
768
+ const legacyStorage = planLegacyStorageMigration(input.workspaceRoot);
769
+ const legacyStorageMigrationRequired =
770
+ legacyConfigExists(input.workspaceRoot) ||
771
+ legacyStorage.moves.length > 0 ||
772
+ legacyStorage.removals.length > 0 ||
773
+ legacyStorage.blockers.length > 0;
692
774
  // A legacy cutover that cannot move its files (a destination already
693
775
  // exists, a malformed config) is an operator decision, not an automatic
694
776
  // step that fails when applied and takes the compile with it.
695
- const legacyStorage = legacyStorageMigrationRequired
696
- ? planLegacyStorageMigration(input.workspaceRoot)
697
- : null;
698
- const legacyStorageBlocked =
699
- legacyStorage !== null && legacyStorage.blockers.length > 0;
777
+ const legacyStorageBlocked = legacyStorage.blockers.length > 0;
700
778
  if (storeState === "missing") {
701
779
  actions.push(
702
780
  migrationAction({
@@ -750,26 +828,29 @@ export function buildActionsFromStatus(input: {
750
828
  invocation: legacyStorageBlocked
751
829
  ? {
752
830
  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.`,
831
+ instruction: `Legacy knowledge files outside .kb/ cannot be migrated automatically: ${legacyStorage.blockers.join(" ")} Then run 'kibi migrate --yes'.`,
754
832
  }
755
833
  : { kind: "cli", command_argv: ["kibi", "migrate", "--yes"] },
756
834
  affectedFiles: [
757
835
  ".kb/manifest.json",
758
836
  ".kb/migrations",
759
837
  ".kb/config.json",
760
- ...(legacyStorage?.moves.map((move) => move.from) ?? []),
838
+ ...legacyStorage.moves.map((move) => move.from),
839
+ ...legacyStorage.removals.map((removal) => removal.path),
761
840
  ],
762
841
  evidence: {
763
842
  configStatus,
764
- ...(legacyStorage === null
765
- ? {}
766
- : {
843
+ ...(legacyStorageMigrationRequired
844
+ ? {
767
845
  legacyStorage: {
768
846
  moves: legacyStorage.moves,
847
+ removals: legacyStorage.removals,
848
+ conflicts: legacyStorage.conflicts,
769
849
  blockers: legacyStorage.blockers,
770
850
  legacyConfig: legacyStorage.legacyConfig,
771
851
  },
772
- }),
852
+ }
853
+ : {}),
773
854
  },
774
855
  dependsOn: storeState === "missing" ? ["branch-store-ensure"] : [],
775
856
  autoApplicable: !operatorOnly,
@@ -816,12 +897,12 @@ export function buildActionsFromStatus(input: {
816
897
  // implements REQ-cli-status-pre-first-sync
817
898
  // Sources whose content changed since the last compilation (kb_status has
818
899
  // already dropped mtime-only reasons) are fixed by recompiling them: one
819
- // automatic `kibi sync`, not a review per file. An uncompiled store is
820
- // compiled by the step above, which also covers these files.
900
+ // automatic `kibi sync`, not a review per file. A store compiled under
901
+ // another compiler contract is fixed by the same sync. An uncompiled store
902
+ // is compiled by the step above, which also covers these reasons.
821
903
  const resyncReasons = (input.staleReasons ?? []).filter(
822
904
  (reason) =>
823
- typeof reason.code === "string" &&
824
- MTIME_SOURCE_REASON_CODES.has(reason.code),
905
+ typeof reason.code === "string" && RESYNC_REASON_CODES.has(reason.code),
825
906
  );
826
907
  if (resyncReasons.length > 0 && uncompiled === undefined) {
827
908
  actions.push(
@@ -856,7 +937,7 @@ export function buildActionsFromStatus(input: {
856
937
  // Planned above as an automatic compile step, not a review.
857
938
  if (code === BRANCH_STORE_NOT_COMPILED) continue;
858
939
  // Planned above as one automatic resync (or covered by the compile).
859
- if (MTIME_SOURCE_REASON_CODES.has(code)) continue;
940
+ if (RESYNC_REASON_CODES.has(code)) continue;
860
941
  const file = typeof reason.path === "string" ? reason.path : "";
861
942
  actions.push(
862
943
  migrationAction({
@@ -17,6 +17,36 @@ function diagnostics(value: unknown): KibiResult["diagnostics"] {
17
17
  });
18
18
  }
19
19
 
20
+ // Discovery operations whose empty answer may come from an uncompiled store.
21
+ const STORE_READ_OPERATIONS = new Set(["kb_search", "kb_query"]);
22
+
23
+ /**
24
+ * A search or query answered from a branch store that was never compiled
25
+ * reads an empty KB. The payload already names the store state; the envelope
26
+ * also raises it as a warning, so a caller that reads only `status` and
27
+ * `diagnostics` does not take the empty answer as "nothing governs this".
28
+ */
29
+ // implements REQ-cli-status-pre-first-sync, REQ-mcp-search-discovery
30
+ function uncompiledStoreDiagnostics(
31
+ operation: string,
32
+ row: Record<string, unknown> | undefined,
33
+ ): KibiResult["diagnostics"] {
34
+ const store = row?.branchStore;
35
+ if (
36
+ !STORE_READ_OPERATIONS.has(operation) ||
37
+ !record(store) ||
38
+ store.state !== "not_compiled"
39
+ )
40
+ return [];
41
+ return [
42
+ {
43
+ ...(typeof store.code === "string" ? { code: store.code } : {}),
44
+ severity: "warning",
45
+ message: `${typeof store.detail === "string" ? `${store.detail} ` : ""}An empty answer here does not mean no requirement governs this; run \`kibi sync\` and ask again.`,
46
+ },
47
+ ];
48
+ }
49
+
20
50
  function nextActions(value: unknown): KibiResult["nextActions"] {
21
51
  if (!Array.isArray(value)) return [];
22
52
  return value.flatMap((entry) => {
@@ -214,7 +244,10 @@ export function toKibiResult<T>(
214
244
  ...spec.effects.map((effect) => effectStatus(effect, row, outcome)),
215
245
  ...extraFailedEffects(spec.effects, row),
216
246
  ],
217
- diagnostics: options.diagnostics ?? diagnostics(row?.diagnostics),
247
+ diagnostics: options.diagnostics ?? [
248
+ ...diagnostics(row?.diagnostics),
249
+ ...uncompiledStoreDiagnostics(String(spec.name), row),
250
+ ],
218
251
  nextActions: options.nextActions ?? nextActions(row?.nextActions),
219
252
  ...(resultError ? { error: resultError } : {}),
220
253
  };
@@ -13,6 +13,7 @@ export const SELECTABLE_RULE_NAMES: readonly string[] = [
13
13
  "must-priority-coverage",
14
14
  "symbol-coverage",
15
15
  "symbol-traceability",
16
+ "symbol-granularity",
16
17
  "no-dangling-refs",
17
18
  "source-relationship-parity",
18
19
  "branch-store-not-compiled",
@@ -55,6 +56,10 @@ export const SELECTABLE_RULE_NAMES: readonly string[] = [
55
56
  "predicate-schema-conformance",
56
57
  "entity-context-missing",
57
58
  "entity-context-acknowledged",
59
+ "decision-firm-unreferenced",
60
+ "decision-firm-superseded",
61
+ "supersedes-reason-missing",
62
+ "supersedes-reason-acknowledged",
58
63
  "policy-ownership",
59
64
  "policy-markers",
60
65
  ];
@@ -62,4 +67,4 @@ export const SELECTABLE_RULE_NAMES: readonly string[] = [
62
67
  // implements REQ-006
63
68
  /** Comma-separated "Allowed:" fragment for the kb_check rules description. */
64
69
  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";
70
+ "must-priority-coverage, symbol-coverage, symbol-traceability, symbol-granularity, 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
  },
@@ -236,13 +236,13 @@ export const applyPlanSpec = {
236
236
  type: "object",
237
237
  required: ["version", "planHash"],
238
238
  description:
239
- "The complete kibi.bootstrap-plan.v1, kibi.compile-plan.v1, kibi.migration-plan.v2, or hash-bound kibi.entity-deletion-plan.v1 object. Migration plans require approvedActionIds and reject blocked/non-automatic actions.",
239
+ "The complete kibi.bootstrap-plan.v1, kibi.compile-plan.v1, kibi.migration-plan.v2, or hash-bound kibi.entity-deletion-plan.v1 object. Migration plans require approvedActionIds and reject blocked actions and review actions Kibi cannot execute.",
240
240
  },
241
241
  approvedActionIds: {
242
242
  type: "array",
243
243
  items: { type: "string", minLength: 1 },
244
244
  description:
245
- "Required for migration-plan.v2. Exact automatic action IDs explicitly approved for this application.",
245
+ "Required for migration-plan.v2. Exact action IDs explicitly approved for this application: automatic actions, or review actions with a cli invocation that Kibi executes only when approved by id (for example semantic_inventory_rederive_drop_grounding).",
246
246
  },
247
247
  recoveryJournalId: {
248
248
  type: "string",