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.
- package/dist/cli-register-proof.d.ts.map +1 -1
- package/dist/cli-register-proof.js +16 -2
- package/dist/commands/check.d.ts.map +1 -1
- package/dist/commands/check.js +5 -1
- package/dist/commands/doctor.d.ts.map +1 -1
- package/dist/commands/doctor.js +83 -4
- package/dist/commands/prove.js +1 -1
- package/dist/engine.d.ts.map +1 -1
- package/dist/engine.js +5 -2
- package/dist/operations/bootstrap/discovery.d.ts.map +1 -1
- package/dist/operations/bootstrap/discovery.js +6 -1
- package/dist/operations/bootstrap/generate.d.ts.map +1 -1
- package/dist/operations/bootstrap/generate.js +1 -0
- package/dist/operations/bootstrap/guidance-actions.d.ts +2 -1
- package/dist/operations/bootstrap/guidance-actions.d.ts.map +1 -1
- package/dist/operations/bootstrap/guidance-actions.js +10 -1
- package/dist/operations/bootstrap/guidance.d.ts +2 -0
- package/dist/operations/bootstrap/guidance.d.ts.map +1 -1
- package/dist/operations/bootstrap/guidance.js +1 -1
- package/dist/operations/bootstrap/plugin-offers.d.ts +16 -0
- package/dist/operations/bootstrap/plugin-offers.d.ts.map +1 -0
- package/dist/operations/bootstrap/plugin-offers.js +66 -0
- package/dist/operations/bootstrap/presentation.d.ts +2 -0
- package/dist/operations/bootstrap/presentation.d.ts.map +1 -1
- package/dist/operations/bootstrap/presentation.js +2 -0
- package/dist/operations/bootstrap/types.d.ts +2 -0
- package/dist/operations/bootstrap/types.d.ts.map +1 -1
- package/dist/operations/modeling/model-requirement.d.ts +5 -0
- package/dist/operations/modeling/model-requirement.d.ts.map +1 -1
- package/dist/operations/modeling/model-requirement.js +17 -5
- package/dist/operations/modeling/predicate-applicability.d.ts.map +1 -1
- package/dist/operations/modeling/predicate-applicability.js +27 -1
- package/dist/operations/modeling/predicate-applyplan.d.ts.map +1 -1
- package/dist/operations/modeling/predicate-applyplan.js +16 -6
- package/dist/operations/modeling/predicate-binding-hints.d.ts +10 -0
- package/dist/operations/modeling/predicate-binding-hints.d.ts.map +1 -1
- package/dist/operations/modeling/predicate-binding-hints.js +74 -0
- package/dist/operations/modeling/predicate-catalog-5.d.ts +2 -2
- package/dist/operations/modeling/predicate-catalog-5.js +2 -2
- package/dist/operations/modeling/predicate-catalog-6.d.ts +9 -0
- package/dist/operations/modeling/predicate-catalog-6.d.ts.map +1 -0
- package/dist/operations/modeling/predicate-catalog-6.js +106 -0
- package/dist/operations/modeling/predicate-catalog.d.ts.map +1 -1
- package/dist/operations/modeling/predicate-catalog.js +2 -0
- package/dist/operations/modeling/predicate-usage-hints-4.d.ts.map +1 -1
- package/dist/operations/modeling/predicate-usage-hints-4.js +32 -0
- package/dist/operations/modeling/suggest-predicates.d.ts.map +1 -1
- package/dist/operations/modeling/suggest-predicates.js +32 -11
- package/dist/operations/mutation/contradictions.d.ts +8 -0
- package/dist/operations/mutation/contradictions.d.ts.map +1 -1
- package/dist/operations/mutation/contradictions.js +13 -0
- package/dist/operations/mutation/source-authoring.d.ts +13 -0
- package/dist/operations/mutation/source-authoring.d.ts.map +1 -1
- package/dist/operations/mutation/source-authoring.js +70 -2
- package/dist/operations/mutation/upsert.d.ts.map +1 -1
- package/dist/operations/mutation/upsert.js +9 -3
- package/dist/operations/mutation/validate-upsert.d.ts.map +1 -1
- package/dist/operations/mutation/validate-upsert.js +30 -4
- package/dist/operations/planning/apply-plan.d.ts.map +1 -1
- package/dist/operations/planning/apply-plan.js +15 -0
- package/dist/operations/semantic-advisor/ingestion-boundary.d.ts.map +1 -1
- package/dist/operations/semantic-advisor/ingestion-boundary.js +20 -1
- package/dist/plugins/approved-source-analyzers.generated.d.ts +5 -5
- package/dist/plugins/approved-source-analyzers.generated.js +5 -5
- package/dist/plugins/check-policies.d.ts +24 -0
- package/dist/plugins/check-policies.d.ts.map +1 -0
- package/dist/plugins/check-policies.js +73 -0
- package/dist/plugins/registry.d.ts.map +1 -1
- package/dist/plugins/registry.js +10 -1
- package/dist/prolog.d.ts.map +1 -1
- package/dist/prolog.js +8 -1
- package/dist/proof/inspect.d.ts +18 -1
- package/dist/proof/inspect.d.ts.map +1 -1
- package/dist/proof/inspect.js +17 -8
- package/dist/proof/integration-plan.d.ts +57 -0
- package/dist/proof/integration-plan.d.ts.map +1 -0
- package/dist/proof/integration-plan.js +331 -0
- package/dist/proof/integrations.js +1 -1
- package/dist/public/operations/catalog.d.ts +2 -2
- package/dist/public/operations/check-executor.d.ts +16 -0
- package/dist/public/operations/check-executor.d.ts.map +1 -1
- package/dist/public/operations/check-executor.js +83 -0
- package/dist/public/operations/check-policy-rules.d.ts +49 -0
- package/dist/public/operations/check-policy-rules.d.ts.map +1 -0
- package/dist/public/operations/check-policy-rules.js +305 -0
- package/dist/public/operations/contracts.d.ts.map +1 -1
- package/dist/public/operations/contracts.js +5 -0
- package/dist/public/operations/discovery-executors.d.ts.map +1 -1
- package/dist/public/operations/discovery-executors.js +13 -3
- package/dist/public/operations/migration-plan.d.ts.map +1 -1
- package/dist/public/operations/migration-plan.js +28 -0
- package/dist/public/operations/source-relationship-parity.d.ts.map +1 -1
- package/dist/public/operations/source-relationship-parity.js +16 -1
- package/dist/public/operations/specs/check-rules.generated.d.ts +1 -1
- package/dist/public/operations/specs/check-rules.generated.d.ts.map +1 -1
- package/dist/public/operations/specs/check-rules.generated.js +4 -1
- package/dist/public/operations/specs/check.d.ts +1 -1
- package/dist/public/operations/specs/composite.d.ts +16 -3
- package/dist/public/operations/specs/composite.d.ts.map +1 -1
- package/dist/public/operations/specs/composite.js +79 -2
- package/dist/public/operations/specs/modeling.d.ts +1 -1
- package/dist/public/operations/specs/modeling.js +1 -1
- package/dist/public/operations/specs/reporting.d.ts +9 -0
- package/dist/public/operations/specs/reporting.d.ts.map +1 -1
- package/dist/public/operations/specs/reporting.js +6 -2
- package/dist/public/skills/kibi-bootstrap/SKILL.md +3 -2
- package/dist/public/skills/kibi-usage/SKILL.md +82 -3
- package/dist/public/skills/kibi-usage/resources/proof.md +15 -3
- package/dist/public/skills/kibi-usage/resources/ui-requirements.md +42 -3
- package/dist/report/html-report.d.ts.map +1 -1
- package/dist/report/html-report.js +13 -2
- package/dist/utils/branch-store.d.ts +32 -0
- package/dist/utils/branch-store.d.ts.map +1 -1
- package/dist/utils/branch-store.js +122 -1
- package/dist/utils/rule-registry.generated.d.ts +15 -0
- package/dist/utils/rule-registry.generated.d.ts.map +1 -1
- package/dist/utils/rule-registry.generated.js +18 -0
- package/dist/utils/strict-modeling.d.ts +12 -0
- package/dist/utils/strict-modeling.d.ts.map +1 -1
- package/dist/utils/strict-modeling.js +12 -1
- package/package.json +4 -4
- package/src/public/operations/check-executor.ts +112 -0
- package/src/public/operations/check-policy-rules.ts +462 -0
- package/src/public/operations/contracts.ts +6 -0
- package/src/public/operations/discovery-executors.ts +19 -2
- package/src/public/operations/migration-plan.ts +35 -0
- package/src/public/operations/source-relationship-parity.ts +17 -2
- package/src/public/operations/specs/check-rules.generated.ts +4 -1
- package/src/public/operations/specs/composite.ts +110 -11
- package/src/public/operations/specs/modeling.ts +1 -1
- package/src/public/operations/specs/reporting.ts +13 -2
- package/src/public/skills/kibi-bootstrap/SKILL.md +3 -2
- package/src/public/skills/kibi-usage/SKILL.md +82 -3
- package/src/public/skills/kibi-usage/resources/proof.md +15 -3
- 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
|
-
|
|
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 ${
|
|
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";
|