kibi-cli 2.14.0 → 2.16.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 (135) hide show
  1. package/dist/cli-register-proof.d.ts.map +1 -1
  2. package/dist/cli-register-proof.js +16 -2
  3. package/dist/commands/check.d.ts.map +1 -1
  4. package/dist/commands/check.js +5 -1
  5. package/dist/commands/doctor.d.ts.map +1 -1
  6. package/dist/commands/doctor.js +83 -4
  7. package/dist/commands/prove.js +1 -1
  8. package/dist/engine.d.ts.map +1 -1
  9. package/dist/engine.js +5 -2
  10. package/dist/operations/bootstrap/discovery.d.ts.map +1 -1
  11. package/dist/operations/bootstrap/discovery.js +6 -1
  12. package/dist/operations/bootstrap/generate.d.ts.map +1 -1
  13. package/dist/operations/bootstrap/generate.js +1 -0
  14. package/dist/operations/bootstrap/guidance-actions.d.ts +2 -1
  15. package/dist/operations/bootstrap/guidance-actions.d.ts.map +1 -1
  16. package/dist/operations/bootstrap/guidance-actions.js +10 -1
  17. package/dist/operations/bootstrap/guidance.d.ts +2 -0
  18. package/dist/operations/bootstrap/guidance.d.ts.map +1 -1
  19. package/dist/operations/bootstrap/guidance.js +1 -1
  20. package/dist/operations/bootstrap/plugin-offers.d.ts +16 -0
  21. package/dist/operations/bootstrap/plugin-offers.d.ts.map +1 -0
  22. package/dist/operations/bootstrap/plugin-offers.js +66 -0
  23. package/dist/operations/bootstrap/presentation.d.ts +2 -0
  24. package/dist/operations/bootstrap/presentation.d.ts.map +1 -1
  25. package/dist/operations/bootstrap/presentation.js +2 -0
  26. package/dist/operations/bootstrap/types.d.ts +2 -0
  27. package/dist/operations/bootstrap/types.d.ts.map +1 -1
  28. package/dist/operations/modeling/model-requirement.d.ts +5 -0
  29. package/dist/operations/modeling/model-requirement.d.ts.map +1 -1
  30. package/dist/operations/modeling/model-requirement.js +17 -5
  31. package/dist/operations/modeling/predicate-applicability.d.ts.map +1 -1
  32. package/dist/operations/modeling/predicate-applicability.js +27 -1
  33. package/dist/operations/modeling/predicate-applyplan.d.ts.map +1 -1
  34. package/dist/operations/modeling/predicate-applyplan.js +16 -6
  35. package/dist/operations/modeling/predicate-binding-hints.d.ts +10 -0
  36. package/dist/operations/modeling/predicate-binding-hints.d.ts.map +1 -1
  37. package/dist/operations/modeling/predicate-binding-hints.js +74 -0
  38. package/dist/operations/modeling/predicate-catalog-5.d.ts +2 -2
  39. package/dist/operations/modeling/predicate-catalog-5.js +2 -2
  40. package/dist/operations/modeling/predicate-catalog-6.d.ts +9 -0
  41. package/dist/operations/modeling/predicate-catalog-6.d.ts.map +1 -0
  42. package/dist/operations/modeling/predicate-catalog-6.js +106 -0
  43. package/dist/operations/modeling/predicate-catalog.d.ts.map +1 -1
  44. package/dist/operations/modeling/predicate-catalog.js +2 -0
  45. package/dist/operations/modeling/predicate-usage-hints-4.d.ts.map +1 -1
  46. package/dist/operations/modeling/predicate-usage-hints-4.js +32 -0
  47. package/dist/operations/modeling/suggest-predicates.d.ts.map +1 -1
  48. package/dist/operations/modeling/suggest-predicates.js +32 -11
  49. package/dist/operations/mutation/contradictions.d.ts +8 -0
  50. package/dist/operations/mutation/contradictions.d.ts.map +1 -1
  51. package/dist/operations/mutation/contradictions.js +13 -0
  52. package/dist/operations/mutation/source-authoring.d.ts +13 -0
  53. package/dist/operations/mutation/source-authoring.d.ts.map +1 -1
  54. package/dist/operations/mutation/source-authoring.js +70 -2
  55. package/dist/operations/mutation/upsert.d.ts.map +1 -1
  56. package/dist/operations/mutation/upsert.js +9 -3
  57. package/dist/operations/mutation/validate-upsert.d.ts.map +1 -1
  58. package/dist/operations/mutation/validate-upsert.js +30 -4
  59. package/dist/operations/planning/apply-plan.d.ts.map +1 -1
  60. package/dist/operations/planning/apply-plan.js +15 -0
  61. package/dist/operations/semantic-advisor/ingestion-boundary.d.ts.map +1 -1
  62. package/dist/operations/semantic-advisor/ingestion-boundary.js +20 -1
  63. package/dist/plugins/approved-source-analyzers.generated.d.ts +5 -5
  64. package/dist/plugins/approved-source-analyzers.generated.js +5 -5
  65. package/dist/plugins/check-policies.d.ts +24 -0
  66. package/dist/plugins/check-policies.d.ts.map +1 -0
  67. package/dist/plugins/check-policies.js +73 -0
  68. package/dist/plugins/registry.d.ts.map +1 -1
  69. package/dist/plugins/registry.js +10 -1
  70. package/dist/prolog.d.ts.map +1 -1
  71. package/dist/prolog.js +8 -1
  72. package/dist/proof/inspect.d.ts +18 -1
  73. package/dist/proof/inspect.d.ts.map +1 -1
  74. package/dist/proof/inspect.js +17 -8
  75. package/dist/proof/integration-plan.d.ts +57 -0
  76. package/dist/proof/integration-plan.d.ts.map +1 -0
  77. package/dist/proof/integration-plan.js +331 -0
  78. package/dist/proof/integrations.js +1 -1
  79. package/dist/public/operations/catalog.d.ts +2 -2
  80. package/dist/public/operations/check-executor.d.ts +16 -0
  81. package/dist/public/operations/check-executor.d.ts.map +1 -1
  82. package/dist/public/operations/check-executor.js +83 -0
  83. package/dist/public/operations/check-policy-rules.d.ts +49 -0
  84. package/dist/public/operations/check-policy-rules.d.ts.map +1 -0
  85. package/dist/public/operations/check-policy-rules.js +305 -0
  86. package/dist/public/operations/contracts.d.ts.map +1 -1
  87. package/dist/public/operations/contracts.js +5 -0
  88. package/dist/public/operations/discovery-executors.d.ts.map +1 -1
  89. package/dist/public/operations/discovery-executors.js +13 -3
  90. package/dist/public/operations/migration-plan.d.ts.map +1 -1
  91. package/dist/public/operations/migration-plan.js +28 -0
  92. package/dist/public/operations/source-relationship-parity.d.ts.map +1 -1
  93. package/dist/public/operations/source-relationship-parity.js +16 -1
  94. package/dist/public/operations/specs/check-rules.generated.d.ts +1 -1
  95. package/dist/public/operations/specs/check-rules.generated.d.ts.map +1 -1
  96. package/dist/public/operations/specs/check-rules.generated.js +4 -1
  97. package/dist/public/operations/specs/check.d.ts +1 -1
  98. package/dist/public/operations/specs/composite.d.ts +16 -3
  99. package/dist/public/operations/specs/composite.d.ts.map +1 -1
  100. package/dist/public/operations/specs/composite.js +79 -2
  101. package/dist/public/operations/specs/modeling.d.ts +1 -1
  102. package/dist/public/operations/specs/modeling.js +1 -1
  103. package/dist/public/operations/specs/reporting.d.ts +9 -0
  104. package/dist/public/operations/specs/reporting.d.ts.map +1 -1
  105. package/dist/public/operations/specs/reporting.js +6 -2
  106. package/dist/public/skills/kibi-bootstrap/SKILL.md +3 -2
  107. package/dist/public/skills/kibi-usage/SKILL.md +82 -3
  108. package/dist/public/skills/kibi-usage/resources/proof.md +15 -3
  109. package/dist/public/skills/kibi-usage/resources/ui-requirements.md +42 -3
  110. package/dist/report/html-report.d.ts.map +1 -1
  111. package/dist/report/html-report.js +13 -2
  112. package/dist/utils/branch-store.d.ts +32 -0
  113. package/dist/utils/branch-store.d.ts.map +1 -1
  114. package/dist/utils/branch-store.js +122 -1
  115. package/dist/utils/rule-registry.generated.d.ts +15 -0
  116. package/dist/utils/rule-registry.generated.d.ts.map +1 -1
  117. package/dist/utils/rule-registry.generated.js +18 -0
  118. package/dist/utils/strict-modeling.d.ts +12 -0
  119. package/dist/utils/strict-modeling.d.ts.map +1 -1
  120. package/dist/utils/strict-modeling.js +12 -1
  121. package/package.json +4 -4
  122. package/src/public/operations/check-executor.ts +112 -0
  123. package/src/public/operations/check-policy-rules.ts +462 -0
  124. package/src/public/operations/contracts.ts +6 -0
  125. package/src/public/operations/discovery-executors.ts +19 -2
  126. package/src/public/operations/migration-plan.ts +35 -0
  127. package/src/public/operations/source-relationship-parity.ts +17 -2
  128. package/src/public/operations/specs/check-rules.generated.ts +4 -1
  129. package/src/public/operations/specs/composite.ts +110 -11
  130. package/src/public/operations/specs/modeling.ts +1 -1
  131. package/src/public/operations/specs/reporting.ts +13 -2
  132. package/src/public/skills/kibi-bootstrap/SKILL.md +3 -2
  133. package/src/public/skills/kibi-usage/SKILL.md +82 -3
  134. package/src/public/skills/kibi-usage/resources/proof.md +15 -3
  135. package/src/public/skills/kibi-usage/resources/ui-requirements.md +42 -3
@@ -0,0 +1,462 @@
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 * as fs from "node:fs";
20
+ import {
21
+ basename,
22
+ dirname,
23
+ extname,
24
+ isAbsolute,
25
+ join,
26
+ posix,
27
+ relative,
28
+ } from "node:path";
29
+ import ignore from "ignore";
30
+ import type {
31
+ CheckPolicyMarkerRule,
32
+ CheckPolicyOwnershipRule,
33
+ } from "kibi-plugin-sdk";
34
+
35
+ import {
36
+ type ActiveCheckPolicy,
37
+ type CheckPolicyLoadError,
38
+ readActiveCheckPolicies,
39
+ } from "../../plugins/check-policies.js";
40
+ import { parseListOfLists, parsePrologValue } from "../../prolog/codec.js";
41
+ import type { Violation } from "../../utils/rule-registry.js";
42
+ import { loadEntitiesPaged } from "./discovery-entities.js";
43
+ import type { PrologPort } from "./runtime-types.js";
44
+
45
+ /** Symbols in a policy's file set must implement a qualifying requirement. */
46
+ // implements REQ-capability-check-policy
47
+ export const POLICY_OWNERSHIP_RULE = "policy-ownership";
48
+ /** Files implementing a pattern must keep the pattern's declared markers. */
49
+ // implements REQ-capability-check-policy
50
+ export const POLICY_MARKERS_RULE = "policy-markers";
51
+
52
+ const POLICY_RULES = [POLICY_OWNERSHIP_RULE, POLICY_MARKERS_RULE] as const;
53
+
54
+ // implements REQ-capability-check-policy
55
+ export type PolicySymbol = Readonly<{
56
+ id: string;
57
+ title: string;
58
+ sourceFile: string;
59
+ tags: readonly string[];
60
+ }>;
61
+
62
+ // implements REQ-capability-check-policy
63
+ export type PolicyPredicateFact = Readonly<{
64
+ id: string;
65
+ name: string;
66
+ args: readonly string[];
67
+ polarity: string;
68
+ }>;
69
+
70
+ // implements REQ-capability-check-policy
71
+ export type CheckPolicyEvaluationInput = Readonly<{
72
+ policies: readonly ActiveCheckPolicy[];
73
+ loadErrors: readonly CheckPolicyLoadError[];
74
+ /** Production symbols (no `executable_for`) that carry a sourceFile. */
75
+ symbols: readonly PolicySymbol[];
76
+ /** `[symbol, requirement]` implements edges. */
77
+ implementsEdges: readonly (readonly [string, string])[];
78
+ currentRequirements: ReadonlySet<string>;
79
+ /** `[requirement, fact]` requires_predicate edges. */
80
+ requiresPredicateEdges: readonly (readonly [string, string])[];
81
+ predicateFacts: readonly PolicyPredicateFact[];
82
+ /** Workspace-relative file contents, or undefined when unreadable. */
83
+ readFile: (path: string) => string | undefined;
84
+ }>;
85
+
86
+ function toPosix(path: string): string {
87
+ return path.replaceAll("\\", "/").replace(/^\.\//, "");
88
+ }
89
+
90
+ /** True for a workspace-relative path that stays inside the workspace. */
91
+ function insideWorkspace(path: string): boolean {
92
+ return (
93
+ path !== "" &&
94
+ !path.startsWith("/") &&
95
+ !/^[A-Za-z]:\//.test(path) &&
96
+ path !== ".." &&
97
+ !path.startsWith("../")
98
+ );
99
+ }
100
+
101
+ /**
102
+ * Workspace-relative POSIX path of a symbol sourceFile, or undefined when it
103
+ * points outside the workspace. Policies only govern workspace files.
104
+ */
105
+ // implements REQ-capability-check-policy
106
+ export function workspaceSourcePath(
107
+ workspaceRoot: string,
108
+ sourceFile: string,
109
+ ): string | undefined {
110
+ const path = posix.normalize(
111
+ toPosix(
112
+ isAbsolute(sourceFile) ? relative(workspaceRoot, sourceFile) : sourceFile,
113
+ ),
114
+ );
115
+ return insideWorkspace(path) ? path : undefined;
116
+ }
117
+
118
+ function matcher(patterns: readonly string[]): (path: string) => boolean {
119
+ const matcher = ignore().add([...patterns]);
120
+ return (path) => {
121
+ const relative = toPosix(path);
122
+ // ignore() throws on absolute and ../ paths; neither is in a policy's scope.
123
+ return insideWorkspace(relative) && matcher.ignores(relative);
124
+ };
125
+ }
126
+
127
+ function groupBy<T>(
128
+ pairs: readonly (readonly [string, T])[],
129
+ ): Map<string, T[]> {
130
+ const grouped = new Map<string, T[]>();
131
+ for (const [key, value] of pairs) {
132
+ grouped.set(key, [...(grouped.get(key) ?? []), value]);
133
+ }
134
+ return grouped;
135
+ }
136
+
137
+ /** Asserted predicate names each current requirement is grounded in. */
138
+ function requirementPredicates(
139
+ input: CheckPolicyEvaluationInput,
140
+ ): Map<string, PolicyPredicateFact[]> {
141
+ const facts = new Map(input.predicateFacts.map((fact) => [fact.id, fact]));
142
+ const grounded = new Map<string, PolicyPredicateFact[]>();
143
+ for (const [req, factId] of input.requiresPredicateEdges) {
144
+ const fact = facts.get(factId);
145
+ if (!input.currentRequirements.has(req) || fact === undefined) continue;
146
+ if (fact.polarity !== "assert") continue;
147
+ grounded.set(req, [...(grounded.get(req) ?? []), fact]);
148
+ }
149
+ return grounded;
150
+ }
151
+
152
+ function ownershipViolations(
153
+ policy: ActiveCheckPolicy,
154
+ rule: CheckPolicyOwnershipRule,
155
+ input: CheckPolicyEvaluationInput,
156
+ grounded: Map<string, PolicyPredicateFact[]>,
157
+ owners: Map<string, string[]>,
158
+ ): Violation[] {
159
+ const included = matcher(rule.include);
160
+ const excluded = matcher(rule.exclude ?? []);
161
+ const title =
162
+ rule.symbolTitlePattern !== undefined
163
+ ? new RegExp(rule.symbolTitlePattern)
164
+ : undefined;
165
+ const qualifying = new Set(rule.requirePredicates);
166
+ const violations: Violation[] = [];
167
+ for (const symbol of input.symbols) {
168
+ if (!included(symbol.sourceFile) || excluded(symbol.sourceFile)) continue;
169
+ if (title !== undefined && !title.test(symbol.title)) continue;
170
+ if (rule.exemptTag !== undefined && symbol.tags.includes(rule.exemptTag))
171
+ continue;
172
+ const requirements = owners.get(symbol.id) ?? [];
173
+ const owned = requirements.some((req) =>
174
+ (grounded.get(req) ?? []).some((fact) => qualifying.has(fact.name)),
175
+ );
176
+ if (owned) continue;
177
+ violations.push({
178
+ rule: POLICY_OWNERSHIP_RULE,
179
+ entityId: symbol.id,
180
+ description: `${symbol.id} (${symbol.sourceFile}) is in scope of ${policy.packageName} rule '${rule.id}' but implements no current requirement grounded in ${rule.requirePredicates.join(", ")}`,
181
+ suggestion: `${rule.description}. Link the symbol with implements to a requirement that carries one of these predicates via requires_predicate, or create that requirement first${rule.exemptTag !== undefined ? `; tag the symbol '${rule.exemptTag}' only when it is intentionally unconstrained` : ""}.`,
182
+ source: symbol.sourceFile,
183
+ evidence: {
184
+ policy: policy.document.id,
185
+ package: policy.packageName,
186
+ policyRule: rule.id,
187
+ requirePredicates: [...rule.requirePredicates],
188
+ implements: requirements,
189
+ },
190
+ });
191
+ }
192
+ return violations;
193
+ }
194
+
195
+ /** Marker text as declared, plus its kebab-case spelling for snake_case atoms. */
196
+ function markerSpellings(marker: string): string[] {
197
+ return [...new Set([marker, marker.replaceAll("_", "-")])];
198
+ }
199
+
200
+ function searchedFiles(
201
+ sourceFile: string,
202
+ rule: CheckPolicyMarkerRule,
203
+ ): string[] {
204
+ const source = toPosix(sourceFile);
205
+ const directory = dirname(source);
206
+ const stem = basename(source, extname(source));
207
+ const siblings = (rule.siblingExtensions ?? []).map((extension) =>
208
+ toPosix(join(directory, `${stem}${extension}`)),
209
+ );
210
+ return [source, ...siblings.filter((path) => path !== source)];
211
+ }
212
+
213
+ function markerViolations(
214
+ policy: ActiveCheckPolicy,
215
+ rule: CheckPolicyMarkerRule,
216
+ input: CheckPolicyEvaluationInput,
217
+ grounded: Map<string, PolicyPredicateFact[]>,
218
+ owners: Map<string, string[]>,
219
+ ): Violation[] {
220
+ const markersByPattern = new Map<string, string[]>();
221
+ for (const fact of input.predicateFacts) {
222
+ if (fact.name !== rule.markerPredicate || fact.polarity !== "assert")
223
+ continue;
224
+ const pattern = fact.args[rule.markerPatternArgument];
225
+ const marker = fact.args[rule.markerArgument];
226
+ if (pattern === undefined || marker === undefined) continue;
227
+ markersByPattern.set(pattern, [
228
+ ...(markersByPattern.get(pattern) ?? []),
229
+ marker,
230
+ ]);
231
+ }
232
+ const violations: Violation[] = [];
233
+ // Several symbols in one file (component, props, helpers) share its markers;
234
+ // report each missing marker once per file and requirement.
235
+ const reported = new Set<string>();
236
+ for (const symbol of input.symbols) {
237
+ for (const req of owners.get(symbol.id) ?? []) {
238
+ for (const fact of grounded.get(req) ?? []) {
239
+ if (fact.name !== rule.patternPredicate) continue;
240
+ const pattern = fact.args[rule.patternArgument];
241
+ if (pattern === undefined) continue;
242
+ const files = searchedFiles(symbol.sourceFile, rule);
243
+ const contents = files
244
+ .map((path) => input.readFile(path))
245
+ .filter((content): content is string => content !== undefined);
246
+ for (const marker of [
247
+ ...new Set(markersByPattern.get(pattern) ?? []),
248
+ ]) {
249
+ const present = markerSpellings(marker).some((spelling) =>
250
+ contents.some((content) => content.includes(spelling)),
251
+ );
252
+ const key = JSON.stringify([files[0], req, marker]);
253
+ if (present || reported.has(key)) continue;
254
+ reported.add(key);
255
+ violations.push({
256
+ rule: POLICY_MARKERS_RULE,
257
+ entityId: symbol.id,
258
+ description: `${symbol.id} implements ${req}, which uses pattern '${pattern}', but none of ${files.join(", ")} contains its marker '${marker}'`,
259
+ suggestion: `${rule.description}. Restore the pattern in the implementation, or if the design changed on purpose, supersede ${req} with a requirement naming the new pattern.`,
260
+ source: symbol.sourceFile,
261
+ evidence: {
262
+ policy: policy.document.id,
263
+ package: policy.packageName,
264
+ policyRule: rule.id,
265
+ requirement: req,
266
+ pattern,
267
+ marker,
268
+ searchedFiles: files,
269
+ },
270
+ });
271
+ }
272
+ }
273
+ }
274
+ }
275
+ return violations;
276
+ }
277
+
278
+ function loadErrorViolations(
279
+ errors: readonly CheckPolicyLoadError[],
280
+ rules: ReadonlySet<string>,
281
+ ): Violation[] {
282
+ const rule = POLICY_RULES.find((name) => rules.has(name));
283
+ if (rule === undefined) return [];
284
+ return errors.map((error) => ({
285
+ rule,
286
+ entityId: error.packageName,
287
+ description: `Check policy from '${error.packageName}' could not be read: ${error.message}`,
288
+ suggestion:
289
+ "Install the activated plugin package in this workspace, or remove its kibi.check-policy.v1 entry from package.json#kibi.plugins. Kibi does not skip an activated policy it cannot read.",
290
+ evidence: { package: error.packageName },
291
+ }));
292
+ }
293
+
294
+ /**
295
+ * Evaluate active check policies against the KB. Pure: all KB data and file
296
+ * reads come in through `input`.
297
+ */
298
+ // implements REQ-capability-check-policy
299
+ export function evaluateCheckPolicies(
300
+ input: CheckPolicyEvaluationInput,
301
+ rules: ReadonlySet<string>,
302
+ ): Violation[] {
303
+ const grounded = requirementPredicates(input);
304
+ const owners = groupBy(
305
+ input.implementsEdges.filter(([, req]) =>
306
+ input.currentRequirements.has(req),
307
+ ),
308
+ );
309
+ const violations = loadErrorViolations(input.loadErrors, rules);
310
+ for (const policy of input.policies) {
311
+ if (rules.has(POLICY_OWNERSHIP_RULE)) {
312
+ for (const rule of policy.document.ownership ?? []) {
313
+ violations.push(
314
+ ...ownershipViolations(policy, rule, input, grounded, owners),
315
+ );
316
+ }
317
+ }
318
+ if (rules.has(POLICY_MARKERS_RULE)) {
319
+ for (const rule of policy.document.markers ?? []) {
320
+ violations.push(
321
+ ...markerViolations(policy, rule, input, grounded, owners),
322
+ );
323
+ }
324
+ }
325
+ }
326
+ return violations;
327
+ }
328
+
329
+ async function rows(
330
+ prolog: Pick<PrologPort, "query">,
331
+ goal: string,
332
+ what: string,
333
+ ): Promise<string[][]> {
334
+ const result = await prolog.query(goal);
335
+ if (!result.success) {
336
+ throw new Error(
337
+ `Unable to read ${what} for check policies: ${result.error ?? "query failed"}`,
338
+ );
339
+ }
340
+ return parseListOfLists(result.bindings.Rows ?? "[]");
341
+ }
342
+
343
+ function atom(value: string | undefined): string {
344
+ return String(parsePrologValue(value ?? ""));
345
+ }
346
+
347
+ function stringList(value: unknown): string[] {
348
+ if (Array.isArray(value)) return value.map(String);
349
+ if (typeof value === "string" && value !== "") return [value];
350
+ return [];
351
+ }
352
+
353
+ function fileReader(
354
+ workspaceRoot: string,
355
+ ): (path: string) => string | undefined {
356
+ const cache = new Map<string, string | undefined>();
357
+ return (path) => {
358
+ if (!insideWorkspace(toPosix(path))) return undefined;
359
+ if (!cache.has(path)) {
360
+ try {
361
+ cache.set(path, fs.readFileSync(join(workspaceRoot, path), "utf8"));
362
+ } catch {
363
+ cache.set(path, undefined);
364
+ }
365
+ }
366
+ return cache.get(path);
367
+ };
368
+ }
369
+
370
+ /**
371
+ * Collect policy findings for the selected rules. With no activated check
372
+ * policy this returns at once, so projects without one pay nothing.
373
+ */
374
+ // implements REQ-capability-check-policy
375
+ export async function collectCheckPolicyViolations(
376
+ prolog: Pick<PrologPort, "query">,
377
+ rules: ReadonlySet<string>,
378
+ workspaceRoot: string,
379
+ ): Promise<Violation[]> {
380
+ if (!POLICY_RULES.some((rule) => rules.has(rule))) return [];
381
+ const { policies, errors } = readActiveCheckPolicies(workspaceRoot);
382
+ if (policies.length === 0) {
383
+ return loadErrorViolations(errors, rules);
384
+ }
385
+ // One Prolog port serves queries in order; read sequentially.
386
+ const symbolEntities = await loadEntitiesPaged(prolog, "symbol", 200);
387
+ const testSymbols = new Set(
388
+ (
389
+ await rows(
390
+ prolog,
391
+ "findall([S], kb:executable_test_symbol(S), Rows0), sort(Rows0, Rows)",
392
+ "executable test symbols",
393
+ )
394
+ ).map(([id]) => atom(id)),
395
+ );
396
+ const implementsEdges = (
397
+ await rows(
398
+ prolog,
399
+ "findall([S,R], kb_relationship(implements, S, R), Rows)",
400
+ "implements relationships",
401
+ )
402
+ ).map(([from, to]) => [atom(from), atom(to)] as const);
403
+ const requiresPredicateEdges = (
404
+ await rows(
405
+ prolog,
406
+ "findall([R,F], kb_relationship(requires_predicate, R, F), Rows)",
407
+ "requires_predicate relationships",
408
+ )
409
+ ).map(([from, to]) => [atom(from), atom(to)] as const);
410
+ const currentRequirements = new Set(
411
+ (
412
+ await rows(
413
+ prolog,
414
+ "findall([R], kb:current_req(R), Rows)",
415
+ "current requirements",
416
+ )
417
+ ).map(([id]) => atom(id)),
418
+ );
419
+ const predicateFacts = (
420
+ await rows(
421
+ prolog,
422
+ "findall([F,Name,Args,Polarity], kb:predicate_fact(F, _, Name, Args, Polarity), Rows)",
423
+ "predicate facts",
424
+ )
425
+ ).map(([id, name, args, polarity]) => ({
426
+ id: atom(id),
427
+ name: atom(name),
428
+ args: stringList(parsePrologValue(args ?? "[]")),
429
+ polarity: atom(polarity),
430
+ }));
431
+
432
+ const symbols: PolicySymbol[] = symbolEntities.flatMap((entity) => {
433
+ const id = String(entity.id ?? "");
434
+ const sourceFile =
435
+ typeof entity.sourceFile === "string"
436
+ ? workspaceSourcePath(workspaceRoot, entity.sourceFile)
437
+ : undefined;
438
+ if (id === "" || testSymbols.has(id) || sourceFile === undefined) return [];
439
+ return [
440
+ {
441
+ id,
442
+ title: String(entity.title ?? id),
443
+ sourceFile,
444
+ tags: stringList(entity.tags),
445
+ },
446
+ ];
447
+ });
448
+
449
+ return evaluateCheckPolicies(
450
+ {
451
+ policies,
452
+ loadErrors: errors,
453
+ symbols,
454
+ implementsEdges,
455
+ currentRequirements,
456
+ requiresPredicateEdges,
457
+ predicateFacts,
458
+ readFile: fileReader(workspaceRoot),
459
+ },
460
+ rules,
461
+ );
462
+ }
@@ -489,6 +489,7 @@ const BASE_DATA_SCHEMAS: Readonly<
489
489
  kb_find_gaps: objectData({
490
490
  rows: recordArray,
491
491
  count: integerValue,
492
+ summary: recordValue,
492
493
  meta: recordValue,
493
494
  }),
494
495
  kb_coverage: objectData({
@@ -603,6 +604,11 @@ const BASE_DATA_SCHEMAS: Readonly<
603
604
  tldr: stringValue,
604
605
  promptBlock: stringValue,
605
606
  recommendedActions: recordArray,
607
+ pluginOffers: {
608
+ ...recordArray,
609
+ description:
610
+ "Optional plugins that fit this workspace and that the project has neither activated nor declined. Ask the operator; on yes install and activate the package, on no add it to package.json kibi.declinedPlugins.",
611
+ },
606
612
  declaredContext: recordValue,
607
613
  discoverySummary: recordValue,
608
614
  candidates: recordArray,
@@ -16,10 +16,12 @@ import { rankEntities } from "../../search-ranking.js";
16
16
  import type { SearchMatch } from "../../search-ranking.js";
17
17
  import { resolveBranchAttachment } from "../../utils/branch-resolver.js";
18
18
  import {
19
+ BRANCH_STORE_NOT_COMPILED,
19
20
  type BranchStoreInspection,
20
21
  branchStoreReason,
21
22
  inspectBranchStore,
22
23
  storeLockJournalReason,
24
+ uncompiledBranchStoreReason,
23
25
  } from "../../utils/branch-store.js";
24
26
  import {
25
27
  loadEntities,
@@ -459,7 +461,17 @@ export async function executeStatus(
459
461
  }
460
462
  const snapshotEvidence = await readWorkspaceSnapshot(context);
461
463
  const existingReasons = payload.staleReasons ?? [];
462
- const storeReason = branchStoreReason(store);
464
+ // implements REQ-cli-status-pre-first-sync
465
+ // A missing or empty store beside authored sources gets the one reason
466
+ // whose remediation works (kibi sync); `kibi branch ensure` alone would
467
+ // leave the store empty.
468
+ const storeReason =
469
+ uncompiledBranchStoreReason(
470
+ context.workspaceRoot,
471
+ store,
472
+ attachment.kbBranch,
473
+ ) ?? branchStoreReason(store);
474
+ const storeUncompiled = storeReason?.code === BRANCH_STORE_NOT_COMPILED;
463
475
  const lockReason = storeLockJournalReason(store.path);
464
476
  const compilerReason = compilerFingerprintReason(store.path);
465
477
  const engineReason = engineStatus
@@ -495,6 +507,11 @@ export async function executeStatus(
495
507
  // Source hashes can all match while the compilation itself is lossy, so
496
508
  // a compiler contract change alone makes the store stale.
497
509
  ...(compilerReason ? { syncState: "stale", dirty: true } : {}),
510
+ // An empty store has no stale file to report, yet nothing in it is
511
+ // current: the authored sources were never compiled.
512
+ ...(storeUncompiled && store.state === "healthy"
513
+ ? { syncState: "stale", dirty: true }
514
+ : {}),
498
515
  branchAttachment: attachment,
499
516
  branchStore: store,
500
517
  ...(engineStatus ? { engineStatus } : {}),
@@ -644,7 +661,7 @@ export async function executeStatus(
644
661
  content: [
645
662
  {
646
663
  type: "text",
647
- text: `Branch ${payload.branch} is ${payload.syncState} (snapshot ${payload.snapshotId}, dirty=${payload.dirty}, proofSnapshot=${enrichedPayload.proofSnapshot})`,
664
+ text: `Branch ${payload.branch} is ${enrichedPayload.syncState} (snapshot ${payload.snapshotId}, dirty=${enrichedPayload.dirty}, proofSnapshot=${enrichedPayload.proofSnapshot})${storeUncompiled ? `. Blocking: ${String(storeReason?.detail)} Run kibi sync.` : ""}`,
648
665
  },
649
666
  ],
650
667
  structuredContent: statusWithPlan,
@@ -1,6 +1,7 @@
1
1
  import { createHash } from "node:crypto";
2
2
  import { existsSync, readFileSync } from "node:fs";
3
3
  import * as path from "node:path";
4
+ import { BRANCH_STORE_NOT_COMPILED } from "../../utils/branch-store.js";
4
5
  import { legacyConfigExists } from "../../utils/config.js";
5
6
  import { readKbManifestStatus } from "../../utils/kb-manifest.js";
6
7
  import {
@@ -744,8 +745,42 @@ export function buildActionsFromStatus(input: {
744
745
  );
745
746
  }
746
747
 
748
+ // implements REQ-cli-status-pre-first-sync
749
+ // A branch store that was never compiled while .kb/ holds authored sources
750
+ // is fixed only by compiling it: `kibi branch ensure` alone leaves an empty
751
+ // manifest. The compile step runs after the store exists and after any
752
+ // schema migration, so it compiles the migrated sources.
753
+ const uncompiled = (input.staleReasons ?? []).find(
754
+ (reason) => reason.code === BRANCH_STORE_NOT_COMPILED,
755
+ );
756
+ if (uncompiled !== undefined) {
757
+ actions.push(
758
+ migrationAction({
759
+ id: "branch-store-compile",
760
+ code: BRANCH_STORE_NOT_COMPILED,
761
+ category: "storage",
762
+ safety: "automatic",
763
+ invocation: { kind: "cli", command_argv: ["kibi", "sync"] },
764
+ affectedFiles: [
765
+ typeof store?.path === "string" ? store.path : ".kb/branches",
766
+ ],
767
+ evidence: { reason: uncompiled },
768
+ dependsOn: actions
769
+ .map((action) => action.id)
770
+ .filter(
771
+ (id) =>
772
+ id === "branch-store-ensure" || id === "schema-config-upgrade",
773
+ ),
774
+ postconditions: [{ branchStore: "compiled" }],
775
+ autoApplicable: true,
776
+ }),
777
+ );
778
+ }
779
+
747
780
  for (const reason of input.staleReasons ?? []) {
748
781
  const code = typeof reason.code === "string" ? reason.code : "stale_source";
782
+ // Planned above as an automatic compile step, not a review.
783
+ if (code === BRANCH_STORE_NOT_COMPILED) continue;
749
784
  const file = typeof reason.path === "string" ? reason.path : "";
750
785
  actions.push(
751
786
  migrationAction({
@@ -160,6 +160,22 @@ function authoredRelationship(
160
160
  };
161
161
  }
162
162
 
163
+ /**
164
+ * The repair that works for a source-discovery failure. There is no separate
165
+ * receipt repair tool: a rewrite through kb_upsert republishes a receipt,
166
+ * and `kibi branch recover` retires receipts of files removed on purpose.
167
+ */
168
+ // implements REQ-kibi-source-relationship-parity
169
+ function sourceDiscoverySuggestion(message: string): string {
170
+ if (/Pending source hash drift/.test(message)) {
171
+ return "A pending-source receipt no longer matches its untracked file. Rewrite that entity with kb_upsert (its current title and status are enough) so the receipt is republished against the file's current bytes, or 'git add' the file to retire the receipt; then rerun kb_check. Do not ignore missing authored inputs.";
172
+ }
173
+ if (/Pending source is missing/.test(message)) {
174
+ return "A pending-source receipt binds an untracked file that no longer exists. If the file was deleted on purpose, run 'kibi branch recover --apply' to rebuild the branch KB from the sources that remain; otherwise restore the file (or rewrite the entity with kb_upsert); then rerun kb_check.";
175
+ }
176
+ return "Repair the authored sources named in the message (restore or rewrite the entity with kb_upsert, or run 'kibi branch recover --apply' for sources removed on purpose), then rerun kb_check; do not ignore missing authored inputs.";
177
+ }
178
+
163
179
  // implements REQ-kibi-source-relationship-parity
164
180
  export async function collectSourceRelationshipParityViolations(
165
181
  workspaceRoot: string,
@@ -179,8 +195,7 @@ export async function collectSourceRelationshipParityViolations(
179
195
  rule: "source-relationship-parity",
180
196
  entityId: "source-discovery",
181
197
  description: `Authored relationship parity could not establish a complete source view: ${message}`,
182
- suggestion:
183
- "Repair the pending-source receipt through Kibi recovery, then rerun kb_check; do not ignore missing authored inputs.",
198
+ suggestion: sourceDiscoverySuggestion(message),
184
199
  evidence: {
185
200
  direction: "source_discovery",
186
201
  message,
@@ -15,6 +15,7 @@ export const SELECTABLE_RULE_NAMES: readonly string[] = [
15
15
  "symbol-traceability",
16
16
  "no-dangling-refs",
17
17
  "source-relationship-parity",
18
+ "branch-store-not-compiled",
18
19
  "source-path-dangling",
19
20
  "no-cycles",
20
21
  "required-fields",
@@ -54,9 +55,11 @@ export const SELECTABLE_RULE_NAMES: readonly string[] = [
54
55
  "predicate-schema-conformance",
55
56
  "entity-context-missing",
56
57
  "entity-context-acknowledged",
58
+ "policy-ownership",
59
+ "policy-markers",
57
60
  ];
58
61
 
59
62
  // implements REQ-006
60
63
  /** Comma-separated "Allowed:" fragment for the kb_check rules description. */
61
64
  export const SELECTABLE_RULES_ALLOWED =
62
- "must-priority-coverage, symbol-coverage, symbol-traceability, no-dangling-refs, source-relationship-parity, 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";
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";