kibi-cli 2.14.0 → 2.15.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/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/predicate-applicability.d.ts.map +1 -1
- package/dist/operations/modeling/predicate-applicability.js +27 -1
- 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/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/public/operations/catalog.d.ts +1 -1
- package/dist/public/operations/check-executor.d.ts.map +1 -1
- package/dist/public/operations/check-executor.js +3 -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/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 +3 -1
- package/dist/public/operations/specs/check.d.ts +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 +2 -2
- package/dist/public/skills/kibi-usage/SKILL.md +74 -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/rule-registry.generated.d.ts +10 -0
- package/dist/utils/rule-registry.generated.d.ts.map +1 -1
- package/dist/utils/rule-registry.generated.js +12 -0
- package/package.json +4 -4
- package/src/public/operations/check-executor.ts +7 -0
- package/src/public/operations/check-policy-rules.ts +462 -0
- package/src/public/operations/contracts.ts +6 -0
- package/src/public/operations/source-relationship-parity.ts +17 -2
- package/src/public/operations/specs/check-rules.generated.ts +3 -1
- package/src/public/operations/specs/reporting.ts +13 -2
- package/src/public/skills/kibi-bootstrap/SKILL.md +2 -2
- package/src/public/skills/kibi-usage/SKILL.md +74 -3
- package/src/public/skills/kibi-usage/resources/ui-requirements.md +42 -3
|
@@ -1,7 +1,10 @@
|
|
|
1
1
|
# UI / Visual Requirement Modeling
|
|
2
2
|
|
|
3
3
|
Optional, per-project lane for recording what the screen should look like so UI edits cannot
|
|
4
|
-
silently drift from the spec. Non-UI projects skip it entirely
|
|
4
|
+
silently drift from the spec. Non-UI projects skip it entirely. Without the experimental
|
|
5
|
+
`kibi-plugin-ui` no check rule requires it; with that plugin active, `kb_check` blocks
|
|
6
|
+
components that lack a design requirement (`policy-ownership`) and implementations that
|
|
7
|
+
lose their pattern markers (`policy-markers`).
|
|
5
8
|
|
|
6
9
|
## Three layers
|
|
7
10
|
|
|
@@ -13,12 +16,48 @@ silently drift from the spec. Non-UI projects skip it entirely; no check rule re
|
|
|
13
16
|
from two current requirements are rejected on write unless `supersedes`.
|
|
14
17
|
3. **Relational predicates** — ground `fact_kind: predicate` linked with `requires_predicate`.
|
|
15
18
|
The built-in `visual_layout_rule(subject, relation, target)` covers "X must remain visually
|
|
16
|
-
aligned with Y" (relation `aligned_with`)
|
|
17
|
-
|
|
19
|
+
aligned with Y" (relation `aligned_with`); `ui_pattern`, `same_pattern` and
|
|
20
|
+
`pattern_marker` cover patterns (below); `ui_container(subject, size, overflow)` covers
|
|
21
|
+
"a half-page panel that scrolls". Other relational layout claims need a project-local
|
|
22
|
+
`predicate_schema` when the task authorizes ontology extension.
|
|
18
23
|
|
|
19
24
|
Kibi enforces stated facts and links, not pixels. Freeform spatial prose is stored and
|
|
20
25
|
searchable but is not machine-checked.
|
|
21
26
|
|
|
27
|
+
## Patterns and variants
|
|
28
|
+
|
|
29
|
+
Name the visual pattern a concept uses, and which variants must share it, so a rewrite to a
|
|
30
|
+
different pattern (a line-and-dots timeline redone as cards) is a visible contradiction
|
|
31
|
+
instead of a silent drift. All three predicates are built in:
|
|
32
|
+
|
|
33
|
+
- `ui_pattern(subject, pattern)`: `ui_pattern(activity_feed, line_dots_timeline)`.
|
|
34
|
+
- `same_pattern(subject, variant, other_variant)`: `same_pattern(activity_feed, editor_view, read_only_view)`.
|
|
35
|
+
- `pattern_marker(pattern, marker)`: `pattern_marker(line_dots_timeline, timeline_dot)`; the
|
|
36
|
+
marker is a literal class name or test id every implementation of the pattern contains.
|
|
37
|
+
|
|
38
|
+
Link the first two from the requirement with `requires_predicate`. Marker facts belong to
|
|
39
|
+
the pattern, not to one requirement, so link them from the requirement that introduced the
|
|
40
|
+
pattern or leave them standalone. With `kibi-plugin-ui` active, every component that
|
|
41
|
+
implements a requirement naming a `ui_pattern` must contain each marker of that pattern in
|
|
42
|
+
its source or its `.html` template. A component with no design constraint (a
|
|
43
|
+
layout-free wrapper) carries the tag `review:ui-unconstrained` instead of a requirement.
|
|
44
|
+
|
|
45
|
+
A requirement grounded only in `ui_pattern`, `same_pattern` and `pattern_marker` is proved
|
|
46
|
+
by component tests: a test with `verification_scope: unit` or `integration` and a fresh
|
|
47
|
+
passing receipt satisfies its scenarios. Write one component test per variant named in
|
|
48
|
+
`same_pattern`; that is what catches a divergent branch inside one template, which the
|
|
49
|
+
static marker check cannot see. Adding any other fact (`ui_container`, a property) brings
|
|
50
|
+
back the end-to-end requirement.
|
|
51
|
+
|
|
52
|
+
### Capturing the inventory
|
|
53
|
+
|
|
54
|
+
When the plugin is first enabled on an existing UI, list the patterns the code already
|
|
55
|
+
uses per screen, ask the human which is intended wherever two variants of one concept
|
|
56
|
+
differ, and record each answer as a requirement. Record a divergence the human has not
|
|
57
|
+
decided as an `observation` fact tagged `review:ui-divergence`, citing both files, rather
|
|
58
|
+
than guessing which variant is right. Colours, spacing and component-library choices
|
|
59
|
+
(which scrollbar component to use) belong in a linter, not here.
|
|
60
|
+
|
|
22
61
|
## Button position
|
|
23
62
|
|
|
24
63
|
```yaml
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"html-report.d.ts","sourceRoot":"","sources":["../../src/report/html-report.ts"],"names":[],"mappings":"AAUA,OAAO,EACL,KAAK,YAAY,EAGlB,MAAM,kBAAkB,CAAC;AAC1B,OAAO,EACL,KAAK,gBAAgB,EAMtB,MAAM,iBAAiB,CAAC;AAGzB,MAAM,MAAM,aAAa,GAAG,QAAQ,CAAC,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC,CAAC;AAuB9D,eAAO,MAAM,UAAU,6FACqE,CAAC;AAG7F,eAAO,MAAM,wBAAwB,gEAC0B,CAAC;AAEhE,MAAM,MAAM,kBAAkB,GAAG,QAAQ,CAAC;IACxC,OAAO,EAAE,aAAa,CAAC;IACvB,IAAI,EAAE,SAAS,aAAa,EAAE,CAAC;IAC/B,IAAI,CAAC,EAAE,aAAa,CAAC;CACtB,CAAC,CAAC;AAEH,MAAM,MAAM,eAAe,GAAG,QAAQ,CAAC;IACrC,YAAY,EAAE,kBAAkB,CAAC;IACjC,OAAO,EAAE,kBAAkB,CAAC;IAC5B,MAAM,EAAE,MAAM,CAAC;IACf,WAAW,EAAE,IAAI,CAAC;IAClB,UAAU,CAAC,EAAE,gBAAgB,CAAC;CAC/B,CAAC,CAAC;
|
|
1
|
+
{"version":3,"file":"html-report.d.ts","sourceRoot":"","sources":["../../src/report/html-report.ts"],"names":[],"mappings":"AAUA,OAAO,EACL,KAAK,YAAY,EAGlB,MAAM,kBAAkB,CAAC;AAC1B,OAAO,EACL,KAAK,gBAAgB,EAMtB,MAAM,iBAAiB,CAAC;AAGzB,MAAM,MAAM,aAAa,GAAG,QAAQ,CAAC,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC,CAAC;AAuB9D,eAAO,MAAM,UAAU,6FACqE,CAAC;AAG7F,eAAO,MAAM,wBAAwB,gEAC0B,CAAC;AAEhE,MAAM,MAAM,kBAAkB,GAAG,QAAQ,CAAC;IACxC,OAAO,EAAE,aAAa,CAAC;IACvB,IAAI,EAAE,SAAS,aAAa,EAAE,CAAC;IAC/B,IAAI,CAAC,EAAE,aAAa,CAAC;CACtB,CAAC,CAAC;AAEH,MAAM,MAAM,eAAe,GAAG,QAAQ,CAAC;IACrC,YAAY,EAAE,kBAAkB,CAAC;IACjC,OAAO,EAAE,kBAAkB,CAAC;IAC5B,MAAM,EAAE,MAAM,CAAC;IACf,WAAW,EAAE,IAAI,CAAC;IAClB,UAAU,CAAC,EAAE,gBAAgB,CAAC;CAC/B,CAAC,CAAC;AAkNH,wBAAgB,qBAAqB,CACnC,KAAK,EAAE,SAAS,YAAY,EAAE,EAC9B,MAAM,EAAE,CAAC,IAAI,EAAE,YAAY,KAAK,OAAO,GACtC,YAAY,GAAG,QAAQ,CAKzB;AAID,wBAAgB,iBAAiB,CAAC,GAAG,EAAE,aAAa,GAAG,YAAY,GAAG,QAAQ,CAM7E;AA8YD,wBAAgB,gBAAgB,CAAC,KAAK,EAAE,eAAe,GAAG,MAAM,CAuM/D"}
|
|
@@ -117,9 +117,20 @@ function combinedStageState(...statuses) {
|
|
|
117
117
|
function stagePassed(stages, name) {
|
|
118
118
|
return stageStatus(stages[name]) === "passed";
|
|
119
119
|
}
|
|
120
|
+
/**
|
|
121
|
+
* A test whose scope its scenario accepts: end-to-end everywhere, plus unit and
|
|
122
|
+
* integration tests for component-scope UI pattern scenarios. Other tests carry
|
|
123
|
+
* the `not_end_to_end` state.
|
|
124
|
+
*/
|
|
125
|
+
// implements REQ-ui-pattern-component-proof
|
|
126
|
+
function isProofScopeEvidence(item) {
|
|
127
|
+
return item.state !== undefined
|
|
128
|
+
? item.state !== "not_end_to_end"
|
|
129
|
+
: item.scope === "end_to_end";
|
|
130
|
+
}
|
|
120
131
|
function hasEndToEndTest(stages) {
|
|
121
132
|
const evidence = records(record(stages.passingE2e).receiptEvidence);
|
|
122
|
-
return evidence.some(
|
|
133
|
+
return evidence.some(isProofScopeEvidence);
|
|
123
134
|
}
|
|
124
135
|
function implementationOwned(stages) {
|
|
125
136
|
const productionSymbols = strings(record(stages.productionSymbols).symbols);
|
|
@@ -268,7 +279,7 @@ function requirementStages(row, repository) {
|
|
|
268
279
|
const implementationSymbols = strings(productionSymbols.symbols);
|
|
269
280
|
const missingCoordinates = strings(sourceCoordinatesStage.missingSymbols);
|
|
270
281
|
const receiptEvidence = records(passingE2e.receiptEvidence);
|
|
271
|
-
const hasE2e = receiptEvidence.some(
|
|
282
|
+
const hasE2e = receiptEvidence.some(isProofScopeEvidence);
|
|
272
283
|
const factSources = sourceCoordinates(record(stages.logicGrounding).sources);
|
|
273
284
|
const scenarioSources = sourceCoordinates(record(stages.scenarios).sources);
|
|
274
285
|
const testSources = sourceCoordinates(record(stages.scenarioTests).sources);
|
|
@@ -223,5 +223,15 @@ export declare const GENERATED_RULES: readonly [{
|
|
|
223
223
|
readonly enforcementClass: "advisory";
|
|
224
224
|
readonly category: "lifecycle";
|
|
225
225
|
readonly diagnosticSeverity: "info";
|
|
226
|
+
}, {
|
|
227
|
+
readonly name: "policy-ownership";
|
|
228
|
+
readonly description: "When a plugin activated for kibi.check-policy.v1 declares ownership rules, every production symbol in a rule's file set must implement a current requirement grounded (requires_predicate) in one of the rule's predicates; projects without an activated check policy are unaffected";
|
|
229
|
+
readonly enforcementClass: "canonical";
|
|
230
|
+
readonly category: "traceability";
|
|
231
|
+
}, {
|
|
232
|
+
readonly name: "policy-markers";
|
|
233
|
+
readonly description: "When a plugin activated for kibi.check-policy.v1 declares marker rules, every file implementing a requirement grounded in the rule's pattern predicate (with its declared sibling files) must contain each marker the KB declares for that pattern";
|
|
234
|
+
readonly enforcementClass: "canonical";
|
|
235
|
+
readonly category: "traceability";
|
|
226
236
|
}];
|
|
227
237
|
//# sourceMappingURL=rule-registry.generated.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"rule-registry.generated.d.ts","sourceRoot":"","sources":["../../src/utils/rule-registry.generated.ts"],"names":[],"mappings":"AAUA,eAAO,MAAM,eAAe
|
|
1
|
+
{"version":3,"file":"rule-registry.generated.d.ts","sourceRoot":"","sources":["../../src/utils/rule-registry.generated.ts"],"names":[],"mappings":"AAUA,eAAO,MAAM,eAAe;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;EAqUlB,CAAC"}
|
|
@@ -277,4 +277,16 @@ export const GENERATED_RULES = [
|
|
|
277
277
|
category: "lifecycle",
|
|
278
278
|
diagnosticSeverity: "info",
|
|
279
279
|
},
|
|
280
|
+
{
|
|
281
|
+
name: "policy-ownership",
|
|
282
|
+
description: "When a plugin activated for kibi.check-policy.v1 declares ownership rules, every production symbol in a rule's file set must implement a current requirement grounded (requires_predicate) in one of the rule's predicates; projects without an activated check policy are unaffected",
|
|
283
|
+
enforcementClass: "canonical",
|
|
284
|
+
category: "traceability",
|
|
285
|
+
},
|
|
286
|
+
{
|
|
287
|
+
name: "policy-markers",
|
|
288
|
+
description: "When a plugin activated for kibi.check-policy.v1 declares marker rules, every file implementing a requirement grounded in the rule's pattern predicate (with its declared sibling files) must contain each marker the KB declares for that pattern",
|
|
289
|
+
enforcementClass: "canonical",
|
|
290
|
+
category: "traceability",
|
|
291
|
+
},
|
|
280
292
|
];
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "kibi-cli",
|
|
3
|
-
"version": "2.
|
|
3
|
+
"version": "2.15.0",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"description": "Kibi CLI for knowledge base management",
|
|
6
6
|
"engines": {
|
|
@@ -235,9 +235,9 @@
|
|
|
235
235
|
"fast-glob": "^3.2.12",
|
|
236
236
|
"gray-matter": "^4.0.3",
|
|
237
237
|
"js-yaml": "^4.1.0",
|
|
238
|
-
"kibi-core": "^0.
|
|
239
|
-
"kibi-plugin-builtin": "^0.
|
|
240
|
-
"kibi-plugin-sdk": "^0.
|
|
238
|
+
"kibi-core": "^0.18.0",
|
|
239
|
+
"kibi-plugin-builtin": "^0.5.0",
|
|
240
|
+
"kibi-plugin-sdk": "^0.5.0",
|
|
241
241
|
"kibi-swipl": "^1.0.1",
|
|
242
242
|
"saxes": "^6.0.0",
|
|
243
243
|
"ignore": "^5.3.0",
|
|
@@ -33,6 +33,7 @@ import {
|
|
|
33
33
|
resolveCheckRules,
|
|
34
34
|
stagedEntityIdStyleDiagnostics,
|
|
35
35
|
} from "./check-helpers.js";
|
|
36
|
+
import { collectCheckPolicyViolations } from "./check-policy-rules.js";
|
|
36
37
|
import { executeStatus } from "./discovery-executors.js";
|
|
37
38
|
import { collectEntityContextViolations } from "./entity-context.js";
|
|
38
39
|
import {
|
|
@@ -229,6 +230,11 @@ export async function executeCheck(
|
|
|
229
230
|
rulesAllowlist,
|
|
230
231
|
workspaceRoot,
|
|
231
232
|
);
|
|
233
|
+
const checkPolicyFindings = await collectCheckPolicyViolations(
|
|
234
|
+
prolog,
|
|
235
|
+
rulesAllowlist,
|
|
236
|
+
workspaceRoot,
|
|
237
|
+
);
|
|
232
238
|
const partitioned = partitionCheckFindings([
|
|
233
239
|
...aggregatedFindings,
|
|
234
240
|
...queryPlanViolations,
|
|
@@ -237,6 +243,7 @@ export async function executeCheck(
|
|
|
237
243
|
...predicateConformanceFindings,
|
|
238
244
|
...originReviewFindings,
|
|
239
245
|
...entityContextFindings,
|
|
246
|
+
...checkPolicyFindings,
|
|
240
247
|
]);
|
|
241
248
|
const violations: Violation[] = partitioned.violations;
|
|
242
249
|
|
|
@@ -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,
|