kibi-cursor 2.10.1 → 2.12.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/.cursor-plugin/plugin.json +1 -1
- package/CHANGELOG.md +122 -0
- package/README.md +22 -4
- package/agent-plugin/plugin.json +1 -1
- package/agent-plugin/skills/kibi-bootstrap/SKILL.md +2 -2
- package/agent-plugin/skills/kibi-provable-code/SKILL.md +203 -0
- package/agent-plugin/skills/kibi-provable-code/resources/provable-patterns.md +172 -0
- package/agent-plugin/skills/kibi-usage/SKILL.md +9 -3
- package/agent-plugin/skills/kibi-usage/resources/prolog-proofs.md +207 -0
- package/agent-plugin/skills/kibi-usage/resources/proof.md +65 -3
- package/bin/hook-runner.mjs +3236 -0
- package/bin/launch-kibi-mcp.mjs +2 -1
- package/dist/.cursor-plugin/plugin.json +1 -1
- package/dist/bin/hook-runner.mjs +3236 -0
- package/dist/bin/launch-kibi-mcp.mjs +2 -1
- package/dist/hook-runner.js +35 -3
- package/dist/hooks/hooks.json +6 -6
- package/dist/skills/.canon-hash.json +6 -3
- package/dist/skills/kibi-bootstrap/SKILL.md +2 -2
- package/dist/skills/kibi-provable-code/SKILL.md +203 -0
- package/dist/skills/kibi-provable-code/resources/provable-patterns.md +172 -0
- package/dist/skills/kibi-usage/SKILL.md +9 -3
- package/dist/skills/kibi-usage/resources/prolog-proofs.md +207 -0
- package/dist/skills/kibi-usage/resources/proof.md +65 -3
- package/hooks/hooks.json +6 -6
- package/package.json +5 -3
- package/skills/.canon-hash.json +6 -3
- package/skills/kibi-bootstrap/SKILL.md +2 -2
- package/skills/kibi-provable-code/SKILL.md +203 -0
- package/skills/kibi-provable-code/resources/provable-patterns.md +172 -0
- package/skills/kibi-usage/SKILL.md +9 -3
- package/skills/kibi-usage/resources/prolog-proofs.md +207 -0
- package/skills/kibi-usage/resources/proof.md +65 -3
|
@@ -0,0 +1,172 @@
|
|
|
1
|
+
# Provable Patterns
|
|
2
|
+
|
|
3
|
+
Before-and-after examples for the practices in `kibi-provable-code`. Each
|
|
4
|
+
"after" is a change worth making without Kibi; the note says what Kibi can
|
|
5
|
+
then read. Keep the "before" when the dynamic form is the design (plugins,
|
|
6
|
+
serialization, registries, generated code).
|
|
7
|
+
|
|
8
|
+
## One source of truth for a decision
|
|
9
|
+
|
|
10
|
+
```ts
|
|
11
|
+
// Before: the limit lives in two conditions.
|
|
12
|
+
if (files.length > 20) throw new Error("too many files");
|
|
13
|
+
// ...elsewhere
|
|
14
|
+
const remaining = 20 - files.length;
|
|
15
|
+
|
|
16
|
+
// After: one named constant that both read.
|
|
17
|
+
export const UPLOAD_POLICY = { maxFiles: 20 } as const;
|
|
18
|
+
if (files.length > UPLOAD_POLICY.maxFiles) throw new TooManyFilesError();
|
|
19
|
+
```
|
|
20
|
+
|
|
21
|
+
Kibi reads `UPLOAD_POLICY.maxFiles` as a `value_source` with a dotted export.
|
|
22
|
+
Do not add a second `MAX_FILES` export just for the proof.
|
|
23
|
+
|
|
24
|
+
```py
|
|
25
|
+
# Before: allowed roles spread over checks.
|
|
26
|
+
if role == "owner" or role == "admin": ...
|
|
27
|
+
|
|
28
|
+
# After: one set, read by the check.
|
|
29
|
+
INVITER_ROLES = frozenset({"owner", "admin"})
|
|
30
|
+
if role in INVITER_ROLES: ...
|
|
31
|
+
```
|
|
32
|
+
|
|
33
|
+
## Dispatch through a table, not a computed name
|
|
34
|
+
|
|
35
|
+
```py
|
|
36
|
+
# Before: any attribute name can be called.
|
|
37
|
+
handler = getattr(self, f"handle_{event.kind}")
|
|
38
|
+
# or: globals()[f"handle_{event.kind}"]
|
|
39
|
+
|
|
40
|
+
# After: the handled kinds are listed.
|
|
41
|
+
HANDLERS = {"created": handle_created, "deleted": handle_deleted}
|
|
42
|
+
handler = HANDLERS[event.kind]
|
|
43
|
+
```
|
|
44
|
+
|
|
45
|
+
```php
|
|
46
|
+
// Before
|
|
47
|
+
$$name = $value; // variable variables
|
|
48
|
+
$this->$method($arg); // computed method name
|
|
49
|
+
|
|
50
|
+
// After
|
|
51
|
+
$settings[$name] = $value;
|
|
52
|
+
match ($method) { 'archive' => $this->archive($arg), 'restore' => $this->restore($arg) };
|
|
53
|
+
```
|
|
54
|
+
|
|
55
|
+
```ts
|
|
56
|
+
// Before: the loaded module depends on run-time text.
|
|
57
|
+
const exporter = await import(`./exporters/${format}.js`);
|
|
58
|
+
|
|
59
|
+
// After: the supported formats are a literal map.
|
|
60
|
+
const EXPORTERS = {
|
|
61
|
+
csv: () => import("./exporters/csv.js"),
|
|
62
|
+
pdf: () => import("./exporters/pdf.js"),
|
|
63
|
+
} as const;
|
|
64
|
+
const exporter = await EXPORTERS[format]();
|
|
65
|
+
```
|
|
66
|
+
|
|
67
|
+
The table is also the list of supported values: Kibi's `exhaustive` kind or
|
|
68
|
+
an `Object.keys(EXPORTERS)` domain proves "only CSV and PDF are supported".
|
|
69
|
+
|
|
70
|
+
## Configuration at one boundary
|
|
71
|
+
|
|
72
|
+
```ts
|
|
73
|
+
// Before: environment reads spread through the code, some computed.
|
|
74
|
+
const url = process.env[`${service.toUpperCase()}_URL`];
|
|
75
|
+
|
|
76
|
+
// After: config.ts reads every variable by name and exports a typed object.
|
|
77
|
+
export const config = {
|
|
78
|
+
billingUrl: process.env.BILLING_URL,
|
|
79
|
+
searchUrl: process.env.SEARCH_URL,
|
|
80
|
+
};
|
|
81
|
+
```
|
|
82
|
+
|
|
83
|
+
```go
|
|
84
|
+
// After: one function reads the environment; the rest receives Config.
|
|
85
|
+
func LoadConfig() Config {
|
|
86
|
+
return Config{Port: os.Getenv("PORT"), DatabaseURL: os.Getenv("DATABASE_URL")}
|
|
87
|
+
}
|
|
88
|
+
```
|
|
89
|
+
|
|
90
|
+
`extract: env` over `config.ts` (or the Go file) proves "the service reads
|
|
91
|
+
only these variables". Code elsewhere that reads the environment directly is
|
|
92
|
+
then a review finding in its own right.
|
|
93
|
+
|
|
94
|
+
## Pure deciders, effects at the edge
|
|
95
|
+
|
|
96
|
+
```ts
|
|
97
|
+
// Before: the decision reads the database and the session itself.
|
|
98
|
+
async function invite(req) {
|
|
99
|
+
const me = await db.members.find(req.user);
|
|
100
|
+
if (me.role !== "owner" && !(me.role === "admin" && req.body.role !== "owner")) throw ...
|
|
101
|
+
await db.invites.insert(...);
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
// After: the decision is a pure function over finite inputs.
|
|
105
|
+
export function canInvite(inviter: Role, invitee: Role): boolean {
|
|
106
|
+
return inviter === "owner" || (inviter === "admin" && invitee !== "owner");
|
|
107
|
+
}
|
|
108
|
+
async function invite(req) {
|
|
109
|
+
const me = await db.members.find(req.user);
|
|
110
|
+
if (!canInvite(me.role, req.body.role)) throw new ForbiddenError();
|
|
111
|
+
await db.invites.insert(...);
|
|
112
|
+
}
|
|
113
|
+
```
|
|
114
|
+
|
|
115
|
+
Kibi's `rule` kind calls `canInvite` on every pair of roles.
|
|
116
|
+
|
|
117
|
+
## Explicit state
|
|
118
|
+
|
|
119
|
+
```ts
|
|
120
|
+
// Before: several flags imply a state, and impossible pairs are possible.
|
|
121
|
+
{ sent: true, paid: false, cancelled: true }
|
|
122
|
+
|
|
123
|
+
// After: one status and its transitions.
|
|
124
|
+
export const INVOICE_TRANSITIONS = {
|
|
125
|
+
draft: ["sent", "cancelled"],
|
|
126
|
+
sent: ["paid", "cancelled"],
|
|
127
|
+
paid: [],
|
|
128
|
+
cancelled: [],
|
|
129
|
+
} as const;
|
|
130
|
+
```
|
|
131
|
+
|
|
132
|
+
`state_machine` proves "a cancelled invoice can never become paid".
|
|
133
|
+
|
|
134
|
+
## Named failures
|
|
135
|
+
|
|
136
|
+
```java
|
|
137
|
+
// Before
|
|
138
|
+
throw new RuntimeException("quota " + kind + " exceeded");
|
|
139
|
+
// After
|
|
140
|
+
throw new QuotaExceededException(kind);
|
|
141
|
+
```
|
|
142
|
+
|
|
143
|
+
The `thrown` read lists `QuotaExceededException`; callers catch it by type.
|
|
144
|
+
|
|
145
|
+
## Accepted dynamic code
|
|
146
|
+
|
|
147
|
+
Keep reflection, open registries and dynamic loading where they are the
|
|
148
|
+
design, and fence them:
|
|
149
|
+
|
|
150
|
+
- One loader module discovers plugins; the rest of the code calls the
|
|
151
|
+
plugin interface statically.
|
|
152
|
+
- An ORM or serializer maps fields by reflection; claims about the fields
|
|
153
|
+
are proved over the schema or validator, not over the mapper.
|
|
154
|
+
- A dependency-injection container wires by key; the bindings sit in one
|
|
155
|
+
module with literal keys where the container allows it.
|
|
156
|
+
- Built-in entries of an open registry live in a literal table, so a claim
|
|
157
|
+
about the built-ins is provable while the open part stays `tested`.
|
|
158
|
+
|
|
159
|
+
## Generated and vendored code
|
|
160
|
+
|
|
161
|
+
- Keep the generator header and list the paths in `.gitattributes`:
|
|
162
|
+
|
|
163
|
+
```gitattributes
|
|
164
|
+
src/gen/** linguist-generated
|
|
165
|
+
third_party/** linguist-vendored
|
|
166
|
+
```
|
|
167
|
+
|
|
168
|
+
- Change the generator input and regenerate; never hand-edit the output.
|
|
169
|
+
- Prove claims over the input (`openapi`, `schema`, `json`/`yaml`,
|
|
170
|
+
`validator` readers) and over the thin hand-written wrapper.
|
|
171
|
+
- Cover behaviour of the output with tests or snapshots, plus a CI step
|
|
172
|
+
that regenerates and fails on a diff.
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
id: kibi-usage
|
|
3
3
|
name: Kibi Usage
|
|
4
4
|
description: Use Kibi's source-first, exact-Git, migration-aware, proof-aware operations safely across MCP or the trusted local CLI, including partial completion repair.
|
|
5
|
-
version: 2.
|
|
5
|
+
version: 2.42.0
|
|
6
6
|
kibiCompatibility: ">=1.0.0"
|
|
7
7
|
tags:
|
|
8
8
|
- kibi
|
|
@@ -20,6 +20,7 @@ resources:
|
|
|
20
20
|
- resources/fact-lanes.md
|
|
21
21
|
- resources/workflows.md
|
|
22
22
|
- resources/proof.md
|
|
23
|
+
- resources/prolog-proofs.md
|
|
23
24
|
- resources/ui-requirements.md
|
|
24
25
|
- resources/logic-ir.md
|
|
25
26
|
- resources/kb-improvement.md
|
|
@@ -94,8 +95,10 @@ it is a pointer to the knowledge, never proof. Each line answers one question:
|
|
|
94
95
|
requirement first. Read the lead requirement with `kb_query({id})` before
|
|
95
96
|
changing behavior; add `includeBody: true` to see its authored prose, and
|
|
96
97
|
fix a stale sentence there with `kb_upsert` `document.body`.
|
|
97
|
-
- `REQ-… must keep true:
|
|
98
|
-
|
|
98
|
+
- `REQ-… must keep true: <claim>` (edit only): what the lead requirement
|
|
99
|
+
requires, as the claims its facts model (with the operator and members,
|
|
100
|
+
such as "The greet function must not call console.log") or, without
|
|
101
|
+
them, its own statement. An edit that breaks one of them is a requirement
|
|
99
102
|
change: ask before changing the rule (see Decision firmness), and never
|
|
100
103
|
code around the fact.
|
|
101
104
|
- `Decision: ADR-…` (edit only): the decision behind the lead requirement.
|
|
@@ -453,6 +456,9 @@ arithmetic, smt, rule, state_machine, regex, source) reach `proved` for the clai
|
|
|
453
456
|
predicate bound with `harness_for` (scope `harness`) also counts toward
|
|
454
457
|
`proved`; an intent-scope binding stays `tested`. When the `evidence-floor` rule fails, follow Debugging
|
|
455
458
|
proof regressions in `resources/proof.md` and use `kibi proof explain`.
|
|
459
|
+
Load `kibi-provable-code` before writing or refactoring code that implements
|
|
460
|
+
a requirement, when a source read is refused for a computed name, dynamic
|
|
461
|
+
import, reflection or eval, or when deciding how to treat generated code.
|
|
456
462
|
|
|
457
463
|
## Quantified claims: model the dimension
|
|
458
464
|
|
|
@@ -0,0 +1,207 @@
|
|
|
1
|
+
# SWI-Prolog Code Proofs
|
|
2
|
+
|
|
3
|
+
Kibi reads code models from SWI-Prolog files (`.pl`, `.plt`) with the
|
|
4
|
+
SWI-Prolog it runs on (KIBI_SWIPL, the bundled runtime, then `swipl` on
|
|
5
|
+
PATH). It loads the file a symbol names (a module, or a plain file into
|
|
6
|
+
`user`) and reads a predicate named by its indicator, the symbol's title
|
|
7
|
+
(`operator/2`, `lists:append/3`, `greeting//1`) or `export`.
|
|
8
|
+
|
|
9
|
+
- A domain (`exhaustive`, `domains`, `value_source` with `measure: count`)
|
|
10
|
+
is the distinct solutions of one argument. A one-argument predicate needs
|
|
11
|
+
nothing more; for more arguments set `arg` (1-based) to the one that names
|
|
12
|
+
each member: `domain_source: {symbol_id: SYM-operator, arg: 1}` lists every
|
|
13
|
+
`Name` of `operator(Name, Description)`. A solution that leaves that
|
|
14
|
+
argument unbound refuses the read.
|
|
15
|
+
- A value (`value_source`) is the single number a constant holds:
|
|
16
|
+
`hard_limit_seconds(10).` Zero or several solutions refuse the read.
|
|
17
|
+
- A `rule` predicate is called once per row of its argument domains; success
|
|
18
|
+
is true, failure false, an exception an error row (which rejects).
|
|
19
|
+
- A `state_machine` transition table is a `Name/2` (from, to) or `Name/3`
|
|
20
|
+
(from, event, to) fact table: every solution is an edge. With `domains:
|
|
21
|
+
{state, event}` a `Name/3` next-state predicate is called on every state
|
|
22
|
+
and event. `initial_source` names an `initial/1`-style fact.
|
|
23
|
+
- A `regex` reads an atom or string constant as a `type: string` pattern;
|
|
24
|
+
Kibi decides string facts on it. Patterns for `library(pcre)` are not read.
|
|
25
|
+
|
|
26
|
+
Loading a module runs its directives, the same trust a JavaScript import
|
|
27
|
+
gives. The proof file must name the predicate and load its file
|
|
28
|
+
(`use_module`, `ensure_loaded`, `consult`, a `:- [..]` load list, or
|
|
29
|
+
`library(X)` for a pack's `prolog/X.pl`), or be the plunit `.plt` file
|
|
30
|
+
beside it; otherwise Kibi treats the domain as hand-listed. `arg` on a
|
|
31
|
+
TypeScript, JavaScript or Python export is refused, and so is `field` on a
|
|
32
|
+
Prolog predicate.
|
|
33
|
+
|
|
34
|
+
## Writing the certificate
|
|
35
|
+
|
|
36
|
+
A Prolog test writes the `kibi.code-certificate.v1` JSON into the directory
|
|
37
|
+
`kibi prove` sets in `KIBI_PROOF_CERTIFICATES`. Copy this module into the
|
|
38
|
+
project's test directory (for example `test/kibi_certificate.pl`) and call it
|
|
39
|
+
from the proof. Read members from the code with `findall/3` over the domain
|
|
40
|
+
predicate; never list them by hand.
|
|
41
|
+
|
|
42
|
+
```prolog
|
|
43
|
+
:- module(kibi_certificate,
|
|
44
|
+
[ kibi_exhaustive_certificate/5, % +TestId, +SymbolId, +Members, :Check, -Certificate
|
|
45
|
+
kibi_rule_certificate/4, % +TestId, +Domains, +Predicates, -Certificate
|
|
46
|
+
kibi_machine_certificate/5, % +TestId, +SymbolId, +Edges, +Initial, -Certificate
|
|
47
|
+
kibi_pattern_certificate/4, % +TestId, +SymbolId, +Text, -Certificate
|
|
48
|
+
kibi_write_certificate/1 % +Certificate
|
|
49
|
+
]).
|
|
50
|
+
:- use_module(library(http/json)).
|
|
51
|
+
:- use_module(library(apply)).
|
|
52
|
+
:- use_module(library(lists)).
|
|
53
|
+
|
|
54
|
+
:- meta_predicate
|
|
55
|
+
kibi_exhaustive_certificate(+, +, +, 1, -).
|
|
56
|
+
|
|
57
|
+
% Members as Kibi reads them: atoms and strings as text, numbers as written,
|
|
58
|
+
% other terms quoted.
|
|
59
|
+
kibi_text(Value, Text) :- string(Value), !, Text = Value.
|
|
60
|
+
kibi_text(Value, Text) :- atom(Value), !, atom_string(Value, Text).
|
|
61
|
+
kibi_text(Value, Text) :- number(Value), !, number_string(Value, Text).
|
|
62
|
+
kibi_text(Value, Text) :- format(string(Text), "~q", [Value]).
|
|
63
|
+
|
|
64
|
+
%! kibi_exhaustive_certificate(+TestId, +SymbolId, +Members, :Check, -Certificate)
|
|
65
|
+
%
|
|
66
|
+
% Run Check on every member of the domain and record each outcome. Read
|
|
67
|
+
% Members from the code (findall over the domain predicate), never list
|
|
68
|
+
% them by hand: Kibi re-reads the predicate and rejects a different set.
|
|
69
|
+
kibi_exhaustive_certificate(TestId, SymbolId, Members, Check, Certificate) :-
|
|
70
|
+
maplist(kibi_text, Members, Texts),
|
|
71
|
+
maplist(exhaustive_result(Check), Members, Texts, Results),
|
|
72
|
+
Certificate = _{version: "kibi.code-certificate.v1", kind: "exhaustive",
|
|
73
|
+
test_id: TestId,
|
|
74
|
+
domain: _{symbol_id: SymbolId, members: Texts},
|
|
75
|
+
results: Results}.
|
|
76
|
+
|
|
77
|
+
exhaustive_result(Check, Member, Text, Result) :-
|
|
78
|
+
catch(( call(Check, Member)
|
|
79
|
+
-> Result = _{member: Text, outcome: "passed"}
|
|
80
|
+
; Result = _{member: Text, outcome: "failed", message: "the check failed"}
|
|
81
|
+
),
|
|
82
|
+
Error,
|
|
83
|
+
( message_to_text(Error, Message),
|
|
84
|
+
Result = _{member: Text, outcome: "failed", message: Message} )).
|
|
85
|
+
|
|
86
|
+
%! kibi_rule_certificate(+TestId, +Domains, +Predicates, -Certificate)
|
|
87
|
+
%
|
|
88
|
+
% Domains is a list of domain(Type, SymbolId, Members); Predicates a list
|
|
89
|
+
% of predicate(Name, SymbolId, ArgTypes, Goal), where Goal is called with
|
|
90
|
+
% one argument per type appended and decides the row by success or
|
|
91
|
+
% failure.
|
|
92
|
+
kibi_rule_certificate(TestId, Domains, Predicates, Certificate) :-
|
|
93
|
+
findall(Type-_{symbol_id: SymbolId, members: Texts},
|
|
94
|
+
( member(domain(Type, SymbolId, Members), Domains), maplist(kibi_text, Members, Texts) ),
|
|
95
|
+
DomainPairs),
|
|
96
|
+
dict_pairs(DomainDict, _, DomainPairs),
|
|
97
|
+
findall(Name-_{symbol_id: SymbolId, arg_types: ArgTypes, rows: Rows},
|
|
98
|
+
( member(predicate(Name, SymbolId, ArgTypes, Goal), Predicates),
|
|
99
|
+
maplist(domain_members(Domains), ArgTypes, Lists),
|
|
100
|
+
findall(Row, (maplist(member, Tuple, Lists), rule_row(Goal, Tuple, Row)), Rows)
|
|
101
|
+
),
|
|
102
|
+
PredicatePairs),
|
|
103
|
+
dict_pairs(PredicateDict, _, PredicatePairs),
|
|
104
|
+
Certificate = _{version: "kibi.code-certificate.v1", kind: "rule",
|
|
105
|
+
test_id: TestId, domains: DomainDict, predicates: PredicateDict}.
|
|
106
|
+
|
|
107
|
+
domain_members(Domains, Type, Members) :-
|
|
108
|
+
memberchk(domain(Type, _, Members), Domains).
|
|
109
|
+
|
|
110
|
+
rule_row(Goal, Tuple, Row) :-
|
|
111
|
+
maplist(kibi_text, Tuple, Args),
|
|
112
|
+
Call =.. [call, Goal|Tuple],
|
|
113
|
+
catch(( once(Call) -> Row = _{args: Args, value: true} ; Row = _{args: Args, value: false} ),
|
|
114
|
+
Error,
|
|
115
|
+
( message_to_text(Error, Message), Row = _{args: Args, error: Message} )).
|
|
116
|
+
|
|
117
|
+
%! kibi_machine_certificate(+TestId, +SymbolId, +Edges, +Initial, -Certificate)
|
|
118
|
+
%
|
|
119
|
+
% Edges is the list of From-To pairs of the transition table (findall over
|
|
120
|
+
% it); Initial is the list of initial states, or [] when the contract names
|
|
121
|
+
% no initial_source.
|
|
122
|
+
kibi_machine_certificate(TestId, SymbolId, Edges, Initial, Certificate) :-
|
|
123
|
+
findall([F, T], (member(From-To, Edges), kibi_text(From, F), kibi_text(To, T)), Pairs0),
|
|
124
|
+
sort(Pairs0, Pairs),
|
|
125
|
+
findall(S, (member([A, B], Pairs), member(S, [A, B])), Ends),
|
|
126
|
+
sort(Ends, States),
|
|
127
|
+
Machine0 = _{symbol_id: SymbolId, states: States, transitions: Pairs},
|
|
128
|
+
( Initial == []
|
|
129
|
+
-> Machine = Machine0
|
|
130
|
+
; maplist(kibi_text, Initial, InitialTexts0),
|
|
131
|
+
sort(InitialTexts0, InitialTexts),
|
|
132
|
+
Machine = Machine0.put(initial, InitialTexts)
|
|
133
|
+
),
|
|
134
|
+
Certificate = _{version: "kibi.code-certificate.v1", kind: "state_machine",
|
|
135
|
+
test_id: TestId, machine: Machine}.
|
|
136
|
+
|
|
137
|
+
%! kibi_pattern_certificate(+TestId, +SymbolId, +Text, -Certificate)
|
|
138
|
+
%
|
|
139
|
+
% The string or atom constant a string claim is about.
|
|
140
|
+
kibi_pattern_certificate(TestId, SymbolId, Value, Certificate) :-
|
|
141
|
+
kibi_text(Value, Text),
|
|
142
|
+
Certificate = _{version: "kibi.code-certificate.v1", kind: "regex", test_id: TestId,
|
|
143
|
+
pattern: _{symbol_id: SymbolId, type: "string", value: Text}}.
|
|
144
|
+
|
|
145
|
+
%! kibi_write_certificate(+Certificate)
|
|
146
|
+
%
|
|
147
|
+
% Leave the certificate where kibi prove reads it (the directory in
|
|
148
|
+
% KIBI_PROOF_CERTIFICATES). Does nothing outside a kibi prove run.
|
|
149
|
+
kibi_write_certificate(Certificate) :-
|
|
150
|
+
( getenv('KIBI_PROOF_CERTIFICATES', Directory), Directory \== ''
|
|
151
|
+
-> make_directory_path(Directory),
|
|
152
|
+
format(atom(Name), "~w.json", [Certificate.test_id]),
|
|
153
|
+
directory_file_path(Directory, Name, File),
|
|
154
|
+
setup_call_cleanup(open(File, write, Out, [encoding(utf8)]),
|
|
155
|
+
json_write_dict(Out, Certificate, [width(0)]),
|
|
156
|
+
close(Out))
|
|
157
|
+
; true
|
|
158
|
+
).
|
|
159
|
+
|
|
160
|
+
message_to_text(Error, Text) :-
|
|
161
|
+
catch(message_to_codes(Error, Text), _, fail), !.
|
|
162
|
+
message_to_text(Error, Text) :- format(string(Text), "~q", [Error]).
|
|
163
|
+
|
|
164
|
+
message_to_codes(Error, Text) :-
|
|
165
|
+
'$messages':translate_message(Error, Lines, []),
|
|
166
|
+
with_output_to(string(Raw), print_message_lines(current_output, '', Lines)),
|
|
167
|
+
split_string(Raw, "", "\n ", [Text]).
|
|
168
|
+
```
|
|
169
|
+
|
|
170
|
+
A proof that the operators are exactly the claimed set:
|
|
171
|
+
|
|
172
|
+
```prolog
|
|
173
|
+
:- use_module('../prolog/operators').
|
|
174
|
+
:- use_module(kibi_certificate).
|
|
175
|
+
:- initialization(main, main).
|
|
176
|
+
|
|
177
|
+
main :-
|
|
178
|
+
findall(Name, operator(Name, _), Names0),
|
|
179
|
+
list_to_set(Names0, Names),
|
|
180
|
+
kibi_exhaustive_certificate('TEST-operator-set', 'SYM-operator', Names,
|
|
181
|
+
[Name]>>operator(Name, _), Certificate),
|
|
182
|
+
kibi_write_certificate(Certificate).
|
|
183
|
+
```
|
|
184
|
+
|
|
185
|
+
Its contract: `kind: exhaustive`, `domain_source: {symbol_id: SYM-operator,
|
|
186
|
+
arg: 1}`, `proves: [CLAIM-...]`. A rule proof passes
|
|
187
|
+
`[domain(status, 'SYM-status', Statuses)]` and
|
|
188
|
+
`[predicate(detected, 'SYM-detected', [status], detected)]`; its contract
|
|
189
|
+
names `predicate_source: {detected: {symbol_id: SYM-detected}}` and
|
|
190
|
+
`domains: {status: {symbol_id: SYM-status}}`.
|
|
191
|
+
|
|
192
|
+
## Source proofs over Prolog code
|
|
193
|
+
|
|
194
|
+
A `source` proof needs no certificate: Kibi reads the Prolog files itself,
|
|
195
|
+
term by term, and never runs them. Use the `references` reader on `.pl` and
|
|
196
|
+
`.plt` files for what modules load (`extract: imports`, members as written:
|
|
197
|
+
`library(lists)`, `helpers`), call (`calls`, `Name/Arity` or
|
|
198
|
+
`Module:Name/Arity`), read from the environment (`env`, `getenv/2`), pass as
|
|
199
|
+
a first argument (`string_args` with `callee`) and throw (`thrown`: the error
|
|
200
|
+
of `error(type_error(...), _)` is `type_error`). Use the `prolog` reader for
|
|
201
|
+
facts: `{reader: prolog, file: pack.pl, functor: requires/1}` lists a pack's
|
|
202
|
+
requirements, and `arg` picks the argument of a fact with several. A goal or
|
|
203
|
+
closure held in a variable (`call(Goal)`, `maplist(G, L)`), a goal with a
|
|
204
|
+
variable module, a clause asserted from a variable and `:- if(...)` fail the
|
|
205
|
+
read with their place: name the predicate instead. The claim cannot be
|
|
206
|
+
decided for a file the read refuses; never drop a file the claim is about
|
|
207
|
+
from `files`.
|
|
@@ -262,11 +262,16 @@ left:
|
|
|
262
262
|
each list item, such as the `run` of each workflow step), `glob` (`pattern`, `capture`),
|
|
263
263
|
`openapi` (`file`, `select: operations|unsecured_operations|...`),
|
|
264
264
|
`references` (`files`, `extract: env|imports|calls|string_args|thrown|
|
|
265
|
-
property
|
|
265
|
+
property`; TypeScript, JavaScript, Python and SWI-Prolog files, and Go,
|
|
266
|
+
Java, C#, Rust, PHP, C, Bash and Terraform/HCL files through
|
|
267
|
+
`kibi-plugin-treesitter`), `schema` (`format: drizzle|prisma|sql`, `select:
|
|
266
268
|
tables|columns|required_columns|nullable_columns|foreign_keys`), `routes`
|
|
267
269
|
(Hono or Express app export, `path_prefix`, `names_from`, `select:
|
|
268
270
|
routes|middleware`) or `validator` (zod, valibot or pydantic, `select:
|
|
269
|
-
fields|required_fields|optional_fields`)
|
|
271
|
+
fields|required_fields|optional_fields`) or `prolog` (`file`, `functor:
|
|
272
|
+
requires/1`, `arg`: one argument of each fact, such as a pack's
|
|
273
|
+
requirements in `pack.pl`). Prolog files are read term by term and never
|
|
274
|
+
run; see `resources/prolog-proofs.md`. A member Kibi cannot read
|
|
270
275
|
statically (a computed env name, a dynamic import, an env object handed
|
|
271
276
|
to a variable) fails the read with its location, and so does an empty
|
|
272
277
|
read (a glob matching no file, migrations with no `CREATE TABLE`), so a
|
|
@@ -288,6 +293,60 @@ left:
|
|
|
288
293
|
The builders need kibi-cli 2.33.0 or newer in the project. The env
|
|
289
294
|
extractor reads `process.env`, `import.meta.env`, `Bun.env`,
|
|
290
295
|
`Deno.env.get`, `os.environ` and `os.getenv`, through casts.
|
|
296
|
+
- `references` over Go, Java, C#, Rust, PHP, C, Bash or Terraform/HCL
|
|
297
|
+
needs `kibi-plugin-treesitter` and the language's grammar pack
|
|
298
|
+
(`kibi-lang-go`, `kibi-lang-java`, `kibi-lang-csharp`, `kibi-lang-rust`,
|
|
299
|
+
`kibi-lang-php`, `kibi-lang-c`, `kibi-lang-bash`, `kibi-lang-hcl`) in
|
|
300
|
+
`devDependencies`, with the
|
|
301
|
+
plugin activated for proofs:
|
|
302
|
+
`"kibi.reference-extractor.v1": { "mode": "augment" }` in its
|
|
303
|
+
`kibi.plugins` entry (next to `"kibi.symbol-extractor.v2"`). A project
|
|
304
|
+
with no `package.json` creates one that only holds this list. Without it
|
|
305
|
+
the read is refused with what to install, so ask the operator before
|
|
306
|
+
installing; `kibi doctor` names what the project needs. Members are
|
|
307
|
+
written as the code names them (`os.Getenv`, `System.out.println`,
|
|
308
|
+
`Console.WriteLine`, `std::env::var`, Go import paths, `java.util.*`,
|
|
309
|
+
`std::io::Write`); a method on an expression Kibi cannot name is
|
|
310
|
+
`*.name`. Rust `#[cfg(...)]` items are read as if every build compiled
|
|
311
|
+
them (`#[cfg(test)]` items are skipped): a name two gated `use` items
|
|
312
|
+
bind is listed under both, and a `cfg_attr` counts as the attributes it
|
|
313
|
+
applies, so `excludes` holds in every build while `includes` and `eq`
|
|
314
|
+
may count a member only some builds compile. Reflection, a name
|
|
315
|
+
computed at run time, the whole environment, C# `dynamic` and `#if`, a
|
|
316
|
+
`cfg_attr` Kibi cannot split, and any macro for Rust `calls` and
|
|
317
|
+
`thrown` fail the read with their place: the claim cannot be decided
|
|
318
|
+
for that file, so model a claim the code can show (Rust `calls` over a file with
|
|
319
|
+
`println!` is always refused). PHP class names are written in full with
|
|
320
|
+
backslashes (`App\Mail\Mailer::send`) and functions in lower case;
|
|
321
|
+
PHP names ignore letter case, so a PHP `excludes` refuses a member that
|
|
322
|
+
differs from an item only in case (`Exec` for `exec`);
|
|
323
|
+
Terraform/HCL reads only `property`; C and Bash have no `thrown`, and
|
|
324
|
+
Bash no `property`. A Bash variable the script expands but never sets is
|
|
325
|
+
an environment read, and one it sets only on some paths is refused. A
|
|
326
|
+
Terraform `override.tf`, `*_override.tf` or `.tofu` file in the folder
|
|
327
|
+
refuses an HCL read, so a value claim there cannot be proved. C
|
|
328
|
+
`property` reads only initializers (a later `cfg.field = v` is not
|
|
329
|
+
seen), Bash does not read code handed to another interpreter
|
|
330
|
+
(`python -c`, `awk`), and the commands a sourced Bash file or an
|
|
331
|
+
included C file runs are that file's: model such claims over the code
|
|
332
|
+
that holds them, listing every file it runs.
|
|
333
|
+
Never drop a file the claim is about
|
|
334
|
+
from `files`: the read decides a claim only for the files it covers,
|
|
335
|
+
and a proof is rejected when a symbol the test covers or that
|
|
336
|
+
implements the claim's requirement lies outside them.
|
|
337
|
+
- Reference members match by name segments (`.`, `/`, `::`). `excludes`
|
|
338
|
+
fails on anything that is or may be an excluded item: `java.sql`
|
|
339
|
+
excludes `java.sql.Connection`, `github.com/labstack/echo/v4` excludes
|
|
340
|
+
`.../echo/v4/middleware`, a trailing `*` is a prefix (`java.sql.*`), a
|
|
341
|
+
wildcard import or a shorter name (`File.delete`) may be the item, and
|
|
342
|
+
`*.delete` (a call on a receiver Kibi cannot name) may be
|
|
343
|
+
`java.io.File.delete`, so the claim is refused naming it. `includes` and
|
|
344
|
+
`subset_of` count only members that certainly are an item (the item, a
|
|
345
|
+
member under `X.*`, or `a.b.m` for `*.m`); `eq` compares spellings.
|
|
346
|
+
- For a claim about what the code reads from the environment use
|
|
347
|
+
`extract: env`: `string_args` on a getter (`os.Getenv`, `env::var`,
|
|
348
|
+
`System.getenv`, `getenv/2`, ...) sees only that getter, so Kibi refuses
|
|
349
|
+
it for `eq`, `subset_of` and `excludes`.
|
|
291
350
|
|
|
292
351
|
`arithmetic` and `smt` from `constraint_source` alone prove that the
|
|
293
352
|
knowledge base's facts entail the claim, not that code follows the facts:
|
|
@@ -318,7 +377,10 @@ read for a test that wants to assert on it), fail the test when it records a fai
|
|
|
318
377
|
(`failedMembers`, `failedCalls`), and call `writeCodeCertificate`; it writes to the
|
|
319
378
|
directory `kibi prove` sets in `KIBI_PROOF_CERTIFICATES` and does nothing
|
|
320
379
|
elsewhere. A Python test writes the same `kibi.code-certificate.v1` JSON
|
|
321
|
-
to that directory itself.
|
|
380
|
+
to that directory itself. Kibi also reads SWI-Prolog domains, constants,
|
|
381
|
+
rule predicates, transition tables and string constants (`arg` picks a
|
|
382
|
+
predicate's argument); `resources/prolog-proofs.md` has the rules and a
|
|
383
|
+
certificate module to copy. Wire the KB: the proof file symbol is
|
|
322
384
|
`executable_for` the test and named in `required_proofs`; the domain symbol
|
|
323
385
|
(and every symbol a rule, machine or pattern contract names) `implements`
|
|
324
386
|
the requirement and is `covered_by` the test; the test
|