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.
Files changed (92) hide show
  1. package/dist/engine.d.ts.map +1 -1
  2. package/dist/engine.js +5 -2
  3. package/dist/operations/bootstrap/discovery.d.ts.map +1 -1
  4. package/dist/operations/bootstrap/discovery.js +6 -1
  5. package/dist/operations/bootstrap/generate.d.ts.map +1 -1
  6. package/dist/operations/bootstrap/generate.js +1 -0
  7. package/dist/operations/bootstrap/guidance-actions.d.ts +2 -1
  8. package/dist/operations/bootstrap/guidance-actions.d.ts.map +1 -1
  9. package/dist/operations/bootstrap/guidance-actions.js +10 -1
  10. package/dist/operations/bootstrap/guidance.d.ts +2 -0
  11. package/dist/operations/bootstrap/guidance.d.ts.map +1 -1
  12. package/dist/operations/bootstrap/guidance.js +1 -1
  13. package/dist/operations/bootstrap/plugin-offers.d.ts +16 -0
  14. package/dist/operations/bootstrap/plugin-offers.d.ts.map +1 -0
  15. package/dist/operations/bootstrap/plugin-offers.js +66 -0
  16. package/dist/operations/bootstrap/presentation.d.ts +2 -0
  17. package/dist/operations/bootstrap/presentation.d.ts.map +1 -1
  18. package/dist/operations/bootstrap/presentation.js +2 -0
  19. package/dist/operations/bootstrap/types.d.ts +2 -0
  20. package/dist/operations/bootstrap/types.d.ts.map +1 -1
  21. package/dist/operations/modeling/predicate-applicability.d.ts.map +1 -1
  22. package/dist/operations/modeling/predicate-applicability.js +27 -1
  23. package/dist/operations/modeling/predicate-binding-hints.d.ts +10 -0
  24. package/dist/operations/modeling/predicate-binding-hints.d.ts.map +1 -1
  25. package/dist/operations/modeling/predicate-binding-hints.js +74 -0
  26. package/dist/operations/modeling/predicate-catalog-5.d.ts +2 -2
  27. package/dist/operations/modeling/predicate-catalog-5.js +2 -2
  28. package/dist/operations/modeling/predicate-catalog-6.d.ts +9 -0
  29. package/dist/operations/modeling/predicate-catalog-6.d.ts.map +1 -0
  30. package/dist/operations/modeling/predicate-catalog-6.js +106 -0
  31. package/dist/operations/modeling/predicate-catalog.d.ts.map +1 -1
  32. package/dist/operations/modeling/predicate-catalog.js +2 -0
  33. package/dist/operations/modeling/predicate-usage-hints-4.d.ts.map +1 -1
  34. package/dist/operations/modeling/predicate-usage-hints-4.js +32 -0
  35. package/dist/operations/modeling/suggest-predicates.d.ts.map +1 -1
  36. package/dist/operations/modeling/suggest-predicates.js +32 -11
  37. package/dist/operations/mutation/contradictions.d.ts +8 -0
  38. package/dist/operations/mutation/contradictions.d.ts.map +1 -1
  39. package/dist/operations/mutation/contradictions.js +13 -0
  40. package/dist/operations/mutation/source-authoring.d.ts +13 -0
  41. package/dist/operations/mutation/source-authoring.d.ts.map +1 -1
  42. package/dist/operations/mutation/source-authoring.js +70 -2
  43. package/dist/operations/mutation/upsert.d.ts.map +1 -1
  44. package/dist/operations/mutation/upsert.js +9 -3
  45. package/dist/operations/mutation/validate-upsert.d.ts.map +1 -1
  46. package/dist/operations/mutation/validate-upsert.js +30 -4
  47. package/dist/operations/semantic-advisor/ingestion-boundary.d.ts.map +1 -1
  48. package/dist/operations/semantic-advisor/ingestion-boundary.js +20 -1
  49. package/dist/plugins/approved-source-analyzers.generated.d.ts +5 -5
  50. package/dist/plugins/approved-source-analyzers.generated.js +5 -5
  51. package/dist/plugins/check-policies.d.ts +24 -0
  52. package/dist/plugins/check-policies.d.ts.map +1 -0
  53. package/dist/plugins/check-policies.js +73 -0
  54. package/dist/plugins/registry.d.ts.map +1 -1
  55. package/dist/plugins/registry.js +10 -1
  56. package/dist/prolog.d.ts.map +1 -1
  57. package/dist/prolog.js +8 -1
  58. package/dist/public/operations/catalog.d.ts +1 -1
  59. package/dist/public/operations/check-executor.d.ts.map +1 -1
  60. package/dist/public/operations/check-executor.js +3 -0
  61. package/dist/public/operations/check-policy-rules.d.ts +49 -0
  62. package/dist/public/operations/check-policy-rules.d.ts.map +1 -0
  63. package/dist/public/operations/check-policy-rules.js +305 -0
  64. package/dist/public/operations/contracts.d.ts.map +1 -1
  65. package/dist/public/operations/contracts.js +5 -0
  66. package/dist/public/operations/source-relationship-parity.d.ts.map +1 -1
  67. package/dist/public/operations/source-relationship-parity.js +16 -1
  68. package/dist/public/operations/specs/check-rules.generated.d.ts +1 -1
  69. package/dist/public/operations/specs/check-rules.generated.d.ts.map +1 -1
  70. package/dist/public/operations/specs/check-rules.generated.js +3 -1
  71. package/dist/public/operations/specs/check.d.ts +1 -1
  72. package/dist/public/operations/specs/reporting.d.ts +9 -0
  73. package/dist/public/operations/specs/reporting.d.ts.map +1 -1
  74. package/dist/public/operations/specs/reporting.js +6 -2
  75. package/dist/public/skills/kibi-bootstrap/SKILL.md +2 -2
  76. package/dist/public/skills/kibi-usage/SKILL.md +74 -3
  77. package/dist/public/skills/kibi-usage/resources/ui-requirements.md +42 -3
  78. package/dist/report/html-report.d.ts.map +1 -1
  79. package/dist/report/html-report.js +13 -2
  80. package/dist/utils/rule-registry.generated.d.ts +10 -0
  81. package/dist/utils/rule-registry.generated.d.ts.map +1 -1
  82. package/dist/utils/rule-registry.generated.js +12 -0
  83. package/package.json +4 -4
  84. package/src/public/operations/check-executor.ts +7 -0
  85. package/src/public/operations/check-policy-rules.ts +462 -0
  86. package/src/public/operations/contracts.ts +6 -0
  87. package/src/public/operations/source-relationship-parity.ts +17 -2
  88. package/src/public/operations/specs/check-rules.generated.ts +3 -1
  89. package/src/public/operations/specs/reporting.ts +13 -2
  90. package/src/public/skills/kibi-bootstrap/SKILL.md +2 -2
  91. package/src/public/skills/kibi-usage/SKILL.md +74 -3
  92. 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; no check rule requires it.
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`). Other relational layout claims need a
17
- project-local `predicate_schema` when the task authorizes ontology extension.
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;AAsMH,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"}
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((item) => item.scope === "end_to_end");
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((item) => item.scope === "end_to_end");
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;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;EAuTlB,CAAC"}
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.14.0",
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.17.3",
239
- "kibi-plugin-builtin": "^0.4.2",
240
- "kibi-plugin-sdk": "^0.4.1",
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,