Proving requirements
Proof contracts, the kibi prove workflow, and the kibi.proof-run.v1 producer artifact contract.
This guide explains how a requirement goes from "implemented" to "tested": fresh evidence from any proof producer, bound to the current code snapshot, evaluated against explicit proof obligations, and recorded in an ordered proof history that only ever drops receipts that can no longer decide proof.
Kibi does not support test runners. Kibi supports proof evidence.
Playwright, pytest, JUnit, TAP, Go tests, shell commands, database harnesses,
device farms, and proprietary systems are all just producers of
kibi.proof-run.v1 artifacts. Producers report what happened; Kibi
evaluates proof.
The proof chain
REQ-* --specified_by--> SCEN-* --verified_by--> TEST-* <--executable_for-- SYM-*
^----covered_by---- SYM-*
TEST-* --proof_contract--> obligations --integration--> configured producerA requirement counts as tested (proofStatus: tested, evidence level
tested) only when every required proof obligation
on its contracted test has fresh, passing evidence at the required
verification_scope and verification_perspective:
- bound to the current workspace snapshot,
- bound to the current proof contract hash and execution fingerprint,
- derived from a validated
kibi.proof-run.v1artifact (never caller-authored), - the newest valid receipt within the 7-day freshness window.
The three integration levels
Bootstrap picks the strongest available mechanism automatically; every project has a floor.
| Level | Mechanism | Fidelity | Example |
|---|---|---|---|
| 1 | Native producer | Complete attempt history, native IDs, per-target results | Playwright with kibi-cli/playwright-reporter |
| 2 | Standard-format adapter | Native-case outcomes; retry history per source format | pytest/JUnit XML, Go/TAP |
| 3 | Command proof | Aggregate process outcome bound to contracted obligations | cargo test, custom scripts |
Level 3 is the universal fallback: if the project can run a command and observe its exit code, the project can prove requirements with Kibi.
The canonical command: kibi prove
kibi prove --all # prove every proof-bearing test
kibi prove --test TEST-mcp-search-discovery # one test
kibi prove --requirement REQ-cli-check
kibi prove --integration self-proof
kibi prove --stale # only tests without a current passing receipt
kibi prove --all --resume # continue an interrupted run--stale narrows any selector (all tests by default) to the tests whose
receipt is missing, stale, failed or bound to an older contract, judged the
way coverage judges them, so re-proving after a change only reruns what the
change invalidated.
Receipts are written as each integration finishes, and kibi prove prints
each integration it starts and ingests on stderr. When a long run is stopped,
rerun the same selector with --resume: integrations already ingested at the
same workspace snapshot, for every test the run selects, are skipped (the
record lives in .kb/proof/runs/prove-progress.json). After the workspace
changes, --resume runs everything again.
When --test, --requirement or --stale selects a subset and the
integration's producer is playwright, a direct playwright test command is
narrowed to the selected tests' proof_bindings[].source_file spec files, so
proving one test runs its specs instead of the whole suite. Narrowing needs a
source_file on every selected test's bindings; otherwise, or when the
command is a wrapper such as npm run e2e, the whole integration runs and
kibi prove says why. Every producer gets the selected tests' bound files in
KIBI_PROOF_SOURCE_FILES (a JSON array), so a wrapper can narrow itself.
For each configured integration, kibi prove:
- captures the workspace snapshot,
- runs the integration's producer once,
- revalidates the snapshot (proof execution must not change tracked state),
- validates the complete
kibi.proof-run.v1artifact, - evaluates it independently against every selected test's proof contract,
- derives idempotent
kibi.proof-receipt.v2receipts and appends them (re-checking any certificate the run left, see Certificate proof kinds), - reports gaps with expected versus received values.
One suite run can satisfy many TEST contracts; unrelated results are ignored per contract.
CI proof reuse for pull requests
The canonical kibi.workspace-snapshot.v2 verification snapshot, rather than a
Git commit or branch name, identifies the source state proven by Kibi. On each
successful develop push, the strict proof workflow uploads a
kibi-proof-attestation artifact with the snapshot version and hash, source
commit, outcome, and workflow run provenance. The artifact is emitted only
after full proof, baseline enforcement, and report generation succeed.
For a PR targeting master, the same workflow checks out GitHub's merge
commit and computes its snapshot through Kibi's production snapshot code. It
reuses proof only for a same-repository develop PR whose head still equals
the current develop branch, whose checkout is the expected merge commit,
and whose exact head has one successful develop push proof run with a matching
attestation. If the canonical snapshots match, the workflow records a reused
proof decision and skips the full proof steps. If lookup, provenance, version,
or snapshot comparison is uncertain, it runs the full proof on the merge
checkout using the master KB branch identity.
For a same-repository PR targeting develop, the workflow compares the merge
commit's snapshot with the attestation of the PR's base commit instead. The
snapshot leaves out general docs/, release changesets and the derived .kb/
trees, so a PR that only changes those paths has the same snapshot as its
already proven base and skips the full proof. Any change to code, tests,
requirements, symbol manifests, README.md or the docs site changes the
snapshot and runs the full proof. Scheduled, manual, fork, and push proof runs
remain full runs.
Reuse is a CI gate decision only. It does not execute tests again, create new
proof receipts, or attach master to the develop KB branch. The decision
artifact and job summary say whether proof ran or was reused. Attestation
artifacts expire after seven days; expiration simply causes a full proof run.
Proof contracts
A TEST-* entity declares its semantic obligations:
verification_scope: end_to_end # unit | integration | end_to_end
verification_perspective: consumer # internal | consumer
proof_contract:
version: kibi.proof-contract.v1
integration: self-proof # id in .kb/proof/integrations.json
required_proofs:
- symbol_id: SYM-e2e-packed-cli-check
target: default # browser, runtime, db, device, or default
success_policy: all_required_first_attempt
proof_bindings: # optional provenance metadata
- symbol_id: SYM-e2e-packed-cli-check
target: default
native_id: documentation/tests/e2e/packed/cli-workflows.test.ts::checkContracts are semantic. Native test IDs, aliases, and source coordinates live
in proof_bindings, not in the contract. Bindings never replace obligations;
they help adapters map native results to symbols.
Explicit obligations, not Cartesian matrices
Each required_proofs entry is an explicit (symbol_id, target) pair. A
suite must demonstrate exactly what the contract declares — no implicit
cases × projects combinations.
Certificate proof kinds
A test samples cases. When a requirement is about a finite domain (every
entity type, every evidence level, every built-in schema), a test can cover
every case, and Kibi can then report the requirement as proved instead of
tested. The contract says so with kind:
| Kind | Checker | Evidence it supports |
|---|---|---|
e2e (default), example, property |
the test runner | tested |
exhaustive |
the test checks every member of a domain it imports from the code | proved |
typecheck |
Kibi compiles a witness that fails to compile when a member goes unhandled | proved |
arithmetic |
Kibi decides a one-variable claim exactly from the typed facts in constraint_source or the code constants in value_source |
proved |
smt |
Kibi re-runs z3 or cvc5 on a query that asserts those assumptions and needs unsat |
proved |
rule |
Kibi calls the code's predicate functions on every combination of the finite domains the code exports and model checks a rule, predicate or every-member claim | proved |
state_machine |
Kibi reads the code's transition table or next-state function and checks the transitions, initial, terminal or reachable states a claim names | proved |
regex |
Kibi decides whether the code's pattern (or string constant) accepts exactly the strings a string fact describes | proved |
source |
Kibi reads a project source itself (a JSON or YAML file, a glob, an OpenAPI document, the references the code makes, a data schema, a route table, a validator) and decides a set, field or middleware claim against it | proved |
"Proved" here is what the certificate supports; a requirement is proved only for the claims a certificate binds to (see "Binding a certificate to claims").
proof_contract:
version: kibi.proof-contract.v1
integration: self-proof
required_proofs:
- symbol_id: SYM-test-evidence-levels-exhaustive
target: default
success_policy: all_required_first_attempt
kind: exhaustive
domain_source:
symbol_id: SYM-cli-evidence-levels # the symbol that defines the domainexhaustive and typecheck need domain_source; typecheck also needs
witness: {path}. arithmetic and smt need constraint_source: {fact_ids: [...]}, the typed numeric property_value facts (one subject and property,
value_type: int or number) the claim rests on:
kind: arithmetic
constraint_source:
fact_ids: [FACT-RETRY-MIN, FACT-RETRY-MAX] # retry_count >= 0, retry_count < 4The arithmetic certificate's variable is the facts' property_key, its
assumptions are exactly the facts' constraints and its claim is what the
test proves from them; an SMT query declares the property as a constant and
asserts each fact constraint ((assert (>= retry_count 0))) before the
negated claim. These kinds prove that the knowledge base's facts entail the
claim; they say nothing about code that does not follow those facts, so pair
them with a test of the code when the code is what matters. A certificate-kind test leaves a kibi.code-certificate.v1
JSON file in the directory kibi prove names in KIBI_PROOF_CERTIFICATES
(outside kibi prove the variable is unset and nothing is written). The
kibi-cli/proof-certificates module builds and writes them:
import {
exhaustiveCertificate,
failedMembers,
writeCodeCertificate,
} from "kibi-cli/proof-certificates";
import { EVIDENCE_LEVELS } from "../src/evidence-levels.js";
const certificate = await exhaustiveCertificate({
testId: "TEST-evidence-levels-exhaustive",
domain: { symbolId: "SYM-cli-evidence-levels", members: EVIDENCE_LEVELS },
check(level) {
// throw when the claim fails for this member
},
});
writeCodeCertificate(certificate);
expect(failedMembers(certificate)).toEqual([]);Where the domain comes from
domain_source.symbol_id names the symbol that defines the domain. Kibi
loads that symbol's file and reads one export from it: the symbol's title by
default, or domain_source.export when the export has another name (a symbol
titled ENTITY_LANES whose export is entityLanes, or a module-level
anchor):
domain_source:
symbol_id: SYM-order-status
export: OrderStatusWhen the export is a list of records, such as an operation catalog,
domain_source.field names the field of each record that is the domain
member, and the certificate's members are that field's values
(key: (operation) => operation.name in exhaustiveCertificate). A record
without the field fails the read. field applies only to exhaustive.
domain_source:
symbol_id: SYM-operation-catalog
field: name- TypeScript and JavaScript: Kibi tries bun, then the workspace's own
tsx(node_modules/.bin/tsx), then node with type stripping and a resolve hook that maps extensionless and.jsrelative imports to their.tssources. A domain whose module imports runtime-only dependencies needs one of these runners to resolve them. - Compiled output: when the proof file imports the domain from build
output (
dist/status.js), Kibi maps the import back to the source through the nearesttsconfig.json(outDirtorootDir) before checking that the proof file imports the domain. - Python: a
.pydomain is loaded with the first interpreter found amongKIBI_PYTHON, the active virtualenv (VIRTUAL_ENV), a.venvorvenvdirectory from the domain file's directory up to the workspace root, thenpython3(orpython) onPATH, so the domain's own dependencies import. It is imported under its package name so absolute and relative imports inside it resolve. When loading fails, the rejection quotes the exception line of the traceback (ModuleNotFoundError: No module named 'x'). AnEnumcontributes its member values, adictits keys, and a list, tuple, set or frozenset its items. The proof file must import the module (from app.status import OrderStatus,from .status import OrderStatusorimport app.status). - SWI-Prolog: a
.plor.pltdomain is loaded with the same SWI-Prolog Kibi runs on (KIBI_SWIPL, then the bundled runtime, thenswiplonPATH). A module file loads as its module and a plain file intouser.exportnames a predicate indicator (status/1,phrase//1oroperators:operator/2), and the domain is the distinct solutions of its argument. For a predicate with more than one argument,argnames the 1-based argument that holds the members (argandfieldare not both allowed). Kibi refuses a read whose solutions leave that argument unbound, a predicate with more than 100,000 solutions, and a predicate with several arguments and noarg. Members are atoms and strings as text, numbers as written. The proof file must name the predicate and load its file (:- use_module('../prolog/operators'),library(operators)for a pack'sprolog/directory,consult/1or a:- [...]list), or be the.pltfile beside it. A value_source reads a predicate with one solution that is a number, and the rule, state_machine and regex kinds below read Prolog predicates too.
A Python test has no helper module: it writes the certificate JSON itself to
a file in the directory named by KIBI_PROOF_CERTIFICATES (skip writing when
the variable is unset):
import json, os
from app.status import OrderStatus
members = [m.value for m in OrderStatus]
results = [{"member": str(m), "outcome": "passed"} for m in members] # check each
directory = os.environ.get("KIBI_PROOF_CERTIFICATES")
if directory:
os.makedirs(directory, exist_ok=True)
with open(os.path.join(directory, "TEST-order-status-exhaustive.json"), "w") as f:
json.dump({
"version": "kibi.code-certificate.v1",
"kind": "exhaustive",
"test_id": "TEST-order-status-exhaustive",
"domain": {"symbol_id": "SYM-order-status", "members": [str(m) for m in members]},
"results": results,
}, f)A SWI-Prolog test writes its certificate with the kibi_certificate module
the kibi-usage skill ships in resources/prolog-proofs.md (copy it into the
project's test directory). It reads the domain with findall/3 over the
predicate, never a hand-written list:
:- use_module('../prolog/operators').
:- use_module(kibi_certificate).
:- initialization(main, main).
operator_name(Name) :- operator(Name, _).
main :-
findall(Name, operator_name(Name), All), list_to_set(All, Names),
kibi_exhaustive_certificate('TEST-operators', 'SYM-operator', Names,
[N]>>atom(N), Certificate),
kibi_write_certificate(Certificate). domain_source:
symbol_id: SYM-operator
export: operator/2
arg: 1Wiring a certificate test into the KB
A certificate only counts when the KB links it to the requirement it proves:
- The proof file is an executable test symbol:
required_proofsnames it and it linksexecutable_forto the test. - The domain symbol links
implementsto the requirement andcovered_byto the test (and to the requirement's end-to-end test). - The test
validatesa scenario that the requirement isspecified_by; that scenario's end-to-end test still has to pass. - The test has a
proof_contractwithkindanddomain_source, and the integration's command runs the proof file. - The contract names the requirement claims the certificate proves in
proves(see below).
kibi proof explain REQ-... shows each link it found, and
certificateRejections says why a certificate did not count.
Binding a certificate to claims
A requirement's prose is split into claims (its semantic_inventory), and
each modeled claim is grounded by one typed fact. A certificate proves claims,
not requirements: the contract names them in proves, and Kibi derives from
each claim's fact what the certificate has to establish.
proof_contract:
kind: exhaustive
domain_source:
symbol_id: SYM-cli-evidence-levels
proves:
- CLAIM-3F2A9C0D11E4B7A8 # "The evidence levels are proved, tested, asserted and unverified"| The claim's fact | Kind | What the certificate must establish |
|---|---|---|
a set fact (value_type: set, value_set: [...]) |
exhaustive |
the domain Kibi loads is exactly the fact's set |
| a set fact | typecheck |
the domain export, as a type, is exactly the fact's set (Kibi compiles a set-equality witness itself) |
a numeric fact (value_type: int or number, a comparison) |
arithmetic |
the certificate's claim is the fact's constraint (negated for polarity: forbid) |
| a numeric fact | smt |
the query asserts the negation of the fact's constraint |
a set fact with property_key: required_fields |
typecheck |
every member is a required, non-nullable field of the domain type (Kibi compiles that witness itself) |
a set fact whose property_key names a predicate (eq or includes) |
rule |
the code decides the predicate true for every member |
a rule fact (kibi.logic.v1, from kibi model-requirement with logic) |
rule, state_machine |
the rule holds for every assignment of its variables over the domains the code exports |
a predicate fact (a ground atom, or a decision predicate such as permission_rule) |
rule, state_machine |
the code decides the atom as the fact says (for a decision predicate: the key arguments are allowed exactly when the effective decision is allow) |
a set fact with property_key transitions, states, initial_states, terminal_states or reachable_states |
state_machine |
that projection of the machine is exactly the set (transitions members are spelled from->to) |
a string fact (eq, neq or matches) |
regex |
the code's string meets the constraint, or the code's pattern accepts exactly the fact's string (eq), rejects it (neq) or accepts exactly the strings the fact's pattern matches (matches); polarity: forbid negates it, and a forbidden match means no string matches both |
A legacy property_key: allowed_values fact whose value_string joins the
members with | reads as a set fact. Each claim gets one binding per test,
recorded in the receipt's claim_bindings:
- derived: Kibi checked the certificate against the claim's own fact. Only a derived binding proves a claim.
- attested: the fact has no derivation for the kind (a boolean
placeholder, a rule fact under
exhaustive). The author says the certificate expresses the claim; Kibi reports it and counts it as tested, never proved. - rejected: the key is not a modeled claim of a current requirement the
test reaches through its scenarios, its fact is the wrong shape for the
kind (a set fact under
arithmetic), or the certificate does not establish the obligation. The reason says which.
Exhaustive and typecheck bindings also need the domain_source symbol linked
covered_by the test, so a change to the domain stales the proof. An
arithmetic or SMT certificate that reads its assumptions from facts proves
only that the facts entail the claim; its binding has scope intent and the
claim stays tested. To prove that the code satisfies the claim, read the
value from the code with value_source (instead of or with
constraint_source), link those symbols covered_by the test, and the
binding has scope code:
kind: arithmetic
value_source:
- symbol_id: SYM-retry-config
export: MAX_RETRIES # a number export; Kibi reads it itself
proves:
- CLAIM-0A1B2C3D4E5F6071 # "Clients retry at most 3 times"A count claim over a collection reads the collection itself: with
measure: count, Kibi evaluates a list, set, map or object export and uses
its size, so "exactly 32 tools" needs no separate number kept beside the list
for the proof:
kind: arithmetic
value_source:
- symbol_id: SYM-tool-names
export: TOOL_NAMES
measure: count # the number of members of TOOL_NAMES
proves:
- CLAIM-5D6E7F8091A2B3C4 # "There are exactly 32 tools"A claim that every member of a closed set has a property ("Every currency in
GBP, ILS and ZAR must fail closed") is modeled as a set fact named for the
property: the semantic advisor proposes property_key: fail_closed,
operator: includes and value_set: [GBP, ILS, ZAR], so the property stays in
the requirement instead of living only in the test. includes says every
listed member has the property and leaves the others open, so "every role in
owner and admin can invite" and "every role in owner, admin and member can
invite" are both valid facts; the code decides which holds. eq on a set
means exactly those members, and an includes fact contradicts an eq fact
on the same property only when it names a member the eq set leaves out.
A rule certificate derives an includes fact by checking the property on
each listed member, and a typecheck certificate derives one on
required_fields; the kinds that compare the whole set (exhaustive, a
set-equality typecheck, state_machine) reject an includes fact, because
they would read "at least these" as "exactly these".
Kibi judges bindings at ingest and judges them again whenever it reports
evidence: a binding whose fact changed since ingest (another set, another
bound), whose claim is no longer modeled, or whose certificate no longer
re-checks against the obligation is rejected. kibi prove prints each
binding as it ingests a receipt.
From schema 12 the blocking proof-contract-claims rule reports a
certificate-kind contract without proves and a named claim no certificate of
its kind could prove. kibi migrate pins proves where the test reaches
exactly one claim its kind can derive. For every other contract its review
lists each modeled claim the test reaches through its scenarios with the
binding a certificate of its kind would get (provable, attested only, or the
reason its kind cannot express the fact), so the person binding it knows which
keys to name and which facts to remodel.
Rules, state machines and patterns
These kinds prove claims that are not numbers. In each one Kibi reads the
model from the code itself at ingest, with the same runners as an exhaustive
domain (TypeScript, JavaScript, Python or SWI-Prolog), needs the certificate to be that
model, and decides the claim exactly: nothing is sampled. Every symbol a
contract names must link covered_by the test.
A rule contract names the function behind each predicate the claims use
in predicate_source, and the export listing each argument type's values in
domains. Each function takes the predicate's arguments in order and returns
a boolean; Kibi calls it on every combination of its argument domains (up to
100,000 rows) and records the table. A rule holds when, for every assignment
of its variables, its head has the value the modality requires (true for
assert, oblige and permit, false for deny and forbid) wherever its
body holds and no exception applies. A domain's type name is also a
predicate of one argument that holds for its members (role(R)), so every
variable can be ranged over a domain. A failure names the assignment:
kind: rule
predicate_source:
can_share:
symbol_id: SYM-can-share # canShare(status, role) -> boolean
archived:
symbol_id: SYM-is-archived
export: isArchived # when the export is not the symbol's title
domains:
document_status:
symbol_id: SYM-document-statuses # ["draft", "active", "archived"]
role:
symbol_id: SYM-roles
proves:
- CLAIM-15C388CF1E89C595 # "Archived documents must never be shared"rejected: the rule fails for V1=archived, V2=owner: the code decides can_share(archived, owner) is true, but the rule requires falseRules with existential quantifiers, counts, temporal expressions or durations are refused with the reason.
A state_machine contract names the transition table in domain_source:
an object or Map from each state to its next states (a list, a set, one
state, or an event map), or a list of [from, to] pairs. A next-state
function next(state, event) works too, with domains: {state, event} naming
the exports it is called on (a throw or null means no transition).
initial_source names the initial state, which reachable states need. A
statement such as "Invoice status transitions must be one of draft -> sent,
sent -> paid" is modeled as a transitions set fact; terminal, initial and
reachable states the same way. A rule over a machine can use transition,
reachable (one or more steps), initial, terminal and state.
A regex contract names the pattern in pattern_source: a RegExp or a
Python re.compile(...) (its flags included), or a string constant. A
dotted export (BRAND.name) reads one field of an object, dict or class
export, and the proof file counts as importing it when it imports BRAND. "Invoice
numbers must match INV-[0-9]{6}" is modeled as a string fact with
operator: matches, whose value is a JavaScript pattern (with the u flag)
that must match the whole string. Kibi compares the two languages exactly
(over well-formed UTF-16 strings, with each engine's own rules for $, .,
\d and case folding) and, when they differ, reports the shortest string
only one of them accepts:
rejected: the code's pattern /INV-\d{6}/ does not accept exactly the strings matching /INV-[0-9]{6}/: "0INV-000000" is in the code's pattern onlyOnly the regular subset is decided: backreferences, lookaround, \b,
Unicode property escapes, the g, y, m and v flags (Python
MULTILINE, VERBOSE and LOCALE), Python \d and \w without
re.ASCII, and non-ASCII letters under case-insensitive matching are refused
with the reason. The certificate builders are ruleCertificate,
stateMachineCertificate and regexCertificate in
kibi-cli/proof-certificates; failedCalls lists predicate calls that
threw.
In SWI-Prolog each symbol names a predicate indicator. A rule predicate is
called once per row with one argument per argument type and its success or
failure is the row's value; an arity that differs from the argument types is
refused. A transition table is a predicate of arity 2 (From, To) or 3
(From, Event, To) whose solutions are the edges; a next-state predicate
next(State, Event, Next) is driven over its domains instead. A pattern is
a predicate with one solution that is an atom or string, read like a string
constant. The kibi_certificate module has
kibi_rule_certificate/4, kibi_machine_certificate/5 and
kibi_pattern_certificate/4 for these kinds. One certificate reads one
language: a contract that mixes a Prolog domain with a TypeScript predicate
is refused.
Project sources: routes, schemas, configuration and references
Many claims in a real product are about sets the project already declares:
every route under /api runs the auth middleware, every table has
created_at, the app reads only the environment variables in the deploy
docs, a module never imports node:fs, every validator field the API
accepts is a column of the table. A source proof reads that set from the
project itself and decides the claim; the test does not have to compute or
list anything. The contract names the source:
kind: source
source:
reader: routes
file: src/app.ts
export: app # a Hono or Express app, or a function returning one
path_prefix: /api
names_from: [src/middleware.ts] # name middleware by the export they are (or that makes them)
proves:
- CLAIM-... # "Every route under /api must use requireAuth"| Reader | Reads | select |
|---|---|---|
json, yaml |
file at path (dotted, or a JSON pointer) |
keys, values, items |
glob |
the files pattern matches (capture: true reads what each * and ** matched, joined with /: i18n/**/*.json reads finance/de from i18n/finance/de.json) |
|
openapi |
file's operations (GET /invoices) |
operations, unsecured_operations, security_schemes, responses |
references |
the code in files (TypeScript, JavaScript, Python, SWI-Prolog; Go, Java, C#, Rust, PHP, C, Bash and Terraform/HCL through kibi-plugin-treesitter): extract env, imports, calls, string_args (with callee), thrown (the class of throw new X(), or the factory of throw createX()) or property (with key; not in Prolog) |
|
schema |
format: drizzle or prisma (file), or sql migrations replayed in order (files); table filters by name or glob |
tables, columns, required_columns, nullable_columns, foreign_keys |
routes |
a Hono or Express app's route table, with the middleware that runs before each route (mounted routers included); path_prefix keeps the routes under a prefix |
routes, middleware |
validator |
a zod or valibot object (or one wrapped in refine, transform or pipe; any field that is a Standard Schema), or a pydantic model | fields, required_fields, optional_fields |
prolog |
file's facts with functor (requires/1 in pack.pl): argument arg (or the only one) of each, a list contributing its items, in file order |
Details that decide what a reader sees:
- Environment variables.
extract: envreadsprocess.env,import.meta.env(without Vite's built-insMODE,BASE_URL,PROD,DEV,SSR),Bun.envandDeno.env.getin TypeScript and JavaScript, andos.environandos.getenvin Python, through casts ((import.meta as X).env),!and parentheses. An environment object, orimport.metaitself, handed to a variable or a function fails the read, because its later reads could use any name. A framework's own runtime object (globalThis._NGX_ENV_) is not an environment object Kibi reads. - Names in JavaScript and TypeScript. What a file imports is read under
the name it is bound to:
import { execSync as run } from "child_process"makesrun()a call ofchild_process.execSync,import * as cpandconst cp = require(...)makecp.execSync()one, andimport { env } from "node:process"orconst { env } = processmakeenv.Xan environment read.node:names of Node's built-in modules are listed under both spellings,globalThis.processandglobal.processareprocess, andf.call(...),f.apply(...)andf.bind(...)callf. An imported function used as a value (const f = cp.execSync,[execSync].forEach(...)) is listed as called, and a built-in module handed on is listed asmodule.*(any of its members may be called). A module loaded in place (process.getBuiltinModule,createRequire(url)(...),import()) is read only when a member of it is called directly; otherwise an environment read through it, a non-literal specifier, and code built from text (eval,new Function,vm) fail the read. - Symbolic links. A link to a file that
filesmatches is read as that file, as git and compilers read it. Links to folders are not walked. - SQL migrations. The replay reads
CREATE TABLE(quoted and schema-qualified names such as"public"."messages"included),DROP TABLEand theALTER TABLEactions that change columns, nullability or foreign keys, one or several per statement. Actions that change none of them (row level security, triggers, ownership, defaults, column types, statistics, replica identity, other constraints) are skipped. Function bodies in$$ ... $$stay whole. Dropping a foreign key by a name the replay never saw fails the read, unless the statement saysIF EXISTS. A set of files with noCREATE TABLEfails the read, as does a glob that matches no file, so a mistyped pattern never proves a negative. - Middleware names.
names_frommodules name middleware by the export it is, or by the export that makes it: a zero-argument factory such asrequireAuthentication()that returns a fresh function per route is called once at read time and its result is recognised by its source text, so routes guarded byrequireAuthentication()listrequireAuthentication. Async functions, generators and classes are never called. Withoutnames_froma middleware is its function's name, or(anonymous). - Python. The references reader parses
.pyfiles with Python'sastand runs none of them. Names the file imports are followed:import subprocess as sp,from subprocess import Popenandfrom os import getenv as gread assubprocess,subprocess.Popenandos.getenv.envreadsos.getenv,os.environ[...],os.environ.get,setdefaultandpop, and"X" in os.environ.callslists each call as written and, through an alias, under what it names too (sp.callandsubprocess.call); an imported function used as a value (f = sp.check_output,fn=Popen) is listed as called.importslistsimport a.basaanda.b, andfrom urllib import requestasurllibandurllib.request.posixandntare the modulesosre-exports, so a name through them is listed as theosname too:posix.system("x")andfrom posix import systemare the callos.system(as well asposix.system) forcallsandstring_args, andimport posixis listed asosandposixbyimports. The whole environment handed on, copied or iterated (os.environ.copy(),dict(os.environ),**os.environ,env=os.environ), a getter or theosmodule used as a value, a name computed at run time,getattrorvarson an imported module,exec,eval,compile,__import__andimportlib.import_module(whichimportslists when the name is a literal),pkgutil.resolve_name,runpy,importlib.utilandimportlib.machinery,sys.modules,os.path.expandvars, and a star import (fromosforenv, any forcalls) fail the read with their place.posix.environandnt.environread likeos.environ. A module reached through another name also fails the read, because Kibi does not follow what another module binds:os,sys,subprocessand the like as an attribute of an imported module (shutil.os.getenv,path.os,dotenv.main.os) or imported from one (from .main import os, whichimportsrefuses too), a private name of a module (from tempfile import _os,threading._os.popen, refused byimportstoo; the private names ofosandsysthemselves, such asos._exit, are read as written),environorgetenvon any imported name other thanos(from .main import o; o.environ, and a framework'srequest.environalike), a module's namespace (os.__dict__,f.__globals__,globals(),locals(),vars()andvars(module), sogetattr(tempfile, "_os"),vars(tempfile)["_os"]andtempfile.__dict__["_os"]are refused byimportstoo), and an imported module handed on: a module used as a value rather than called or read through (_t = tempfile,f(shutil),return tf,[tempfile][0],p = os.path), and a name imported from a project or third-party module assigned to another name (x = h, sincehmay be a module), failimports,callsandenv. A standard module's member is checked to be a module by importing that standard module; a class or function used as a value (@dataclass,isinstance(x, Path)) is read. Forcalls, a name imported from a project or third-party module that is, or is used through, a name of anos,subprocessorsysfunction (from .cli import Popen,from .main import o; o.system()) fails the read, since it may be that function re-exported; a re-export under an unrelated name is not detected. Forenvandcalls, any use ofctypesorcffi(ctypes.CDLL(None).getenv(b"X"),ffi.dlopen) fails the read: a function of a native library it loads can read the environment or run a program. - SWI-Prolog. The references reader and the
prologreader read a.plor.pltfile term by term with SWI-Prolog and run none of it: no directive,initialization/1or clause of the project executes. The file's ownop/3directives, its module export list, the export lists of project modules it loads and the SWI-Prolog libraries it loads give the operators it is read with.importslists each loaded file as written (library(lists),helpers,foreign(fastmath)) fromuse_module,ensure_loaded,consult,reexport,load_files,include,use_foreign_library,autoloadand:- [...], in directives and in clause bodies, with string paths written as atoms (library("utf8")islibrary(utf8)), pluslibrary(L)for each predicate the files call that SWI-Prolog would autoload fromL(maplist/2islibrary(apply)) because no library or project module they load gives it.callslistsName/Arity(Module:Name/Aritywhen qualified) of every goal in clause bodies and directives, walking control constructs, grammar rules (a non-terminaln//0is the calln/2), lambdas and the arguments of meta-predicates (the files' ownmeta_predicatedeclarations, those of the project modules they load and SWI-Prolog's),apply/2as the goal it calls, the arguments of~@informat/2,3,debug/3,ansi_format/3,sformat/2,3,print_message(_, format(...))and message lines (print_message_lines(_, _, ['~@'-[Goal]]), and the list elements ofprolog:message//1rules and of any grammar rule whose element is such a line), a{Goal}grammar body handed tophrase/2,3orcall_dcg/3, the goal lists offirst_solution/3andconcurrent/3, and the body of a clause asserted from a literal term (assertz((s(V) :- getenv('X', V)))).envreadsgetenv/2;string_argsthe first argument of calls tocallee(formatorformat/2);thrownthe error ofthrow(error(type_error(...), _))(type_error), the functor of any other thrown term, and thelibrary(error)helpers such asdomain_error/2, while a rethrow of the ballcatch/3caught is not a new error. A variable called as a goal or closure (call(Goal),maplist(G, L)), a goal qualified by a variable module, a clause asserted from a variable, conditional compilation (:- if(...)), aterm_expansionorgoal_expansionclause (asserted ones too) in the files or in a project file they load, since expansion can produce any clause, anincludeof a file that is not amongfiles, aconsult,ensure_loaded,load_filesor:- [...]of a file without a module header that is not amongfiles(its clauses join the loading module), a format string Kibi cannot read whose arguments could be goals, message lines Kibi cannot see to their end (a variable list or element, or in aprolog:message//1rule a format that is not written out), and any other module-sensitive (:) meta argument that is not a clause, predicate, file or operator spec or[](prolog_listen/2,on_signal/3) fail the read with their place. An include or load through a project alias (bson(include/common)) resolves only through thefile_search_path/2declarations of the files read: a fact or clause forfile_search_path/2(withuser:or not) or a directive that asserts one, where Kibi evaluatesprolog_load_context(directory, D)(the declaring file's directory),directory_file_path/3andatom_concat/3on known atoms, and the directory is absolute orAlias(Sub)of another alias the files declare. The alias resolves when those directories hold exactly one existing file for the aliased path. A file that only ends with the aliased path (a vendored copy, while the real target sits undernode_modules/or outside the project) is never taken for it. No declaration among the files, one Kibi cannot evaluate (a relative directory, which depends on the working directory at run time, or one computed otherwise), one that goes through an alias SWI-Prolog defines (library,pack), or several matching files fail the read with its place. The refusal of an include names a file to add tofilesonly when it is the file the declarations resolve to; one undernode_modules,.gitor.kbor outside the project is named as out of reach. - Go, Java, C# and Rust. The references reader hands these files to
kibi-plugin-treesitter, which parses them in its bounded workers and compiles or runs nothing. Activate it for proofs inpackage.json:{"package": "kibi-plugin-treesitter", "capabilities": {"kibi.reference-extractor.v1": {"mode": "augment"}, "kibi.symbol-extractor.v2": {"mode": "augment"}}}, with the package and the language's grammar pack (kibi-lang-go,kibi-lang-java,kibi-lang-csharporkibi-lang-rust) indevDependencies; a missing pack refuses the read with its install line. A project without apackage.jsoncreates one that only holds this list. Proofs use the plugin only when its installed files match the release-qualified closure Kibi ships, so a modified or unqualified provider is never imported. Without it the read is refused with what to install, andkibi init,kibi doctorandkb_plan_bootstrapsuggest it when such sources are tracked. Members are written as the code names them:importslists Go import paths, Java imports (java.util.*for a wildcard), C#usingtargets and Rustusepaths expanded from their groups (std::io,std::io::Write) plusextern cratenames.callslistsos.Getenv,System.out.println(Java method references such asSystem.out::printlntoo),Console.WriteLineandstd::env::var; a method called on an expression Kibi cannot name, such as the result of another call, is*.name(*.toList).envreadsos.Getenvandos.LookupEnv,System.getenv("X"),Environment.GetEnvironmentVariable("X"), and Rust'senv::var,env::var_os,env!andoption_env!(and Go'ssyscallandgolang.org/x/sys/unixandwindowsgetters).string_argsreads the first argument of calls tocallee.thrownlists the type of Gopanic(&ConfigError{})(or the factory ofpanic(errors.New(...))) and of Java and C#throw new X(); a rethrow of the caught exception is not a new error.propertyreads keyed Go composite literals and C# object object, indexer and collection initializers (new Options { Mode = "strict" },{ ["Mode"] = "strict" },{ { "Mode", "strict" } }); Java and Rust have none. Names the file gives itself are followed: Go import aliases (o "os"), Java single static imports (import static java.lang.System.getenv), C#using static System.Environmentandusing E = System.Environment, and Rustusebindings (use std::env::var as getvar) read as the canonical name, and the fully qualified forms match too, socallscan list bothenv::varandstd::env::var. Rust paths resolve through theuse,modandextern crateitems in their scope: a path whose root the scope does not bind (env::varwith nouse std::env, or under a function-localmod env) is not the standard library, whilemod m { pub use std::env; }thenm::env::var,self::env,extern crate std as sanduse Command as C(also forstring_argswithcallee: Command::new) are followed. Calls on Java'sjava.langtypes and C#'sSystemtypes are also listed under their long spelling (java.lang.System.getenv,System.Console.WriteLine), so a "never calls" claim holds whichever spelling it uses. C#importsalso holds the project'sglobal usinglines from any tracked.csfile, the<Using Include>items of.csproj/.propsfiles and the usingsImplicitUsingsadds, andglobal::prefixes are dropped. A Go identifier that shadows a package the file does not import (a localos) is not that package, and neither is a localosin a block, parameter, closure,rangeor type switch of a file that imports it; a package imported asosthat is not the standard library's is listed by its path ("github.com/joho/godotenv".Getenv). A Go method on a value is*.Method, and also the package's (logrus.Error) when its receiver visibly comes from that package (logrus.StandardLogger().Error,l := logrus.New(), a parameter or struct field of that type), so a never-call claim on the package does not pass over its logger. A Go qualifier that is no import (a package-level logger of another file) or a call result (logger().Error) has an unknown type (*.Error), and a method value (g := l.Error) is listed like a call. A Java receiver is named by its declared type, cast ornew(Runtime r; r.exit()and((System) null).getenvareRuntime.exitandSystem.getenv), andimport other.Systemor aSystemclass in the file shadowsjava.lang.System; a call on an imported type is also listed under its import (java.io.File.delete). A JavaSystemthat a supertype or a static import may name is unknown, and a method the class declares shadows a static import. Since another file of the package may declare its ownSystemorRuntime, a call on a bare one also gets a row that Kibi drops only after finding no such type in the.javafiles of the file's folder; a bare Rust standard macro likewise gets a row that Kibi drops only after finding nomacro_rules!of that name (nor#[macro_use] extern crate) in the crate. For Goenv, a name the file uses but does not declare gets a row that Kibi drops only when no other file of the package (its test files only for a test file) can read the environment or none names it, and every import other thanos,syscall, x/sys and cgo gets a row that Kibi drops only when the path is no package of the project, or that package cannot read the environment: a package of the project is a relative path, a path under the module path of anygo.modin the project or under a localreplace, or, with nogo.mod, a folder of that path above the file; it can read the environment when one of its files (not its tests) spells a getter, cgo or//go:linkname, or imports a package of the project that can. Soconfig.Get("X")over a package that wrapsos.Getenv,var Get = os.Getenvthere, or a package-level value it reads at init is refused; third-party packages are not followed. A C# bare call is listed under a type only when the file declares the method, or oneusing staticSystem type has it;@verbatim identifiers are read without the@. A C# member access on a local, parameter, field or property named like a System type (Environment), on a type parameter (class C<Environment>,void M<Environment>()), on a pattern,out, deconstruction or query variable declared in the same member (anywhere in the file for top-level statements), or on a type the file declares, is on a value of unknown type (*.Exit), and so is a method group on an expression Kibi cannot name (Process.GetCurrentProcess().Kill,new Process().Kill,ps[0].Killare*.Kill). Rust paths are read without comments between segments, and a plaintypealias is followed. A function or method used as a value (logf := logrus.Error,apply(logrus.Error), a C# method group, also cast or in a conditional, a Rust path such asenv::varpassed on) is listed incalls. Rust#[cfg(...)]items are read as if every build compiled them (#[cfg(test)]items are skipped): a name that gated items bind in several ways (#[cfg(unix)] use std::process as p;next to#[cfg(not(unix))] use mycrate::process as p;) resolves to each of them, a gated binding may also leave the outer scope's binding in force, and acfg_attrcounts as the attributes it applies (derive(Debug)is read as usual,tokio::mainrefusescalls).excludesandsubset_ofthen hold in every build;includesandeqmay count a member only some builds compile, as they do for a call in anif. A name computed at run time (os.Getenv(name)), the whole environment (os.Environ(),System.getenv(),os.ExpandEnv,Environment.ExpandEnvironmentVariables), a getter handed on as a value (g := os.Getenv,System::getenv,let f = env::var), a Go dot import, a Java static wildcard or a Rust glob import that could bring a getter or call into scope, a call with no static name (fs[0](),<T as Tr>::m()), reflection (Goreflectcalls such asMethodByNameandplugin.Open, JavaClass.forNameandMethod.invoke, also fully qualified, C#Type.GetType,Assembly.LoadandCreateInstance), C#dynamicanywhere (forcallsandenvalike, since a reflective call can read the environment), a C##ifblock, a Rustcfg_attrwhose attributes cannot be split, for Rustthrownany macro invocation (println!included), for Rustcallsa macro other than a standard one or a known crate's formatting macro, also nested in one (the calls in a standard macro's arguments are read:println!("{}", f())listsf), and for both an attribute macro (#[tokio::main]) or derive outside the standard library, for Rustenva macro whose arguments mention an environment getter (eprintln!("{}", env::var("X")?)), and forstring_argsa macro whose arguments mention the callee fail the read with their place. So do, since the call may be a claimed callee or a getter under a name the file does not show: Go cgo (import "C"),//go:linknameand anyEnviron()method (exec.Cmd.Environ) forenv; a Java method named like a JDK callee (getenv,exit,halt,exec,load,loadLibrary) on a receiver of unknown type (Runtime.getRuntime()is known, avarlocal or an inherited field is not), forcallsand, forgetenv,env; a C# bare call the file cannot place (a base type's, or one of ausing staticin this file or a project-wide one such asglobal using static System.Environmentor<Using Include="System.Environment" Static="true" />) forcalls, an environment getter's name it cannot place (bare, as a method group, or on a project-wide alias) forenv; a Rust getter's name on a path that cannot be followed (crate::,super::out of the file, a module file, another crate'svar) forenvandcalls; and for Rustenv,importsandstring_argsany macro other than a standard one or a known crate's formatting macro (anyhow!,log::info!), such as the file's ownmacro_rules!orinclude!, anenv!nested in another macro, or (forenvandstring_args) any attribute or derive macro outside the standard library (clap's#[arg(env = "X")],#[derive(FromEnv)]); a Rust glob of a project module or another crate (use inner::*) for a bare getter name and the standard macros; and a Rust alias Kibi cannot follow or a qualified-self callee (<T>::new) forstring_args. A file that does not parse is refused with its line. - PHP, C, Bash and Terraform/HCL. These go through the same plugin and
need their packs (
kibi-lang-php,kibi-lang-c,kibi-lang-bash,kibi-lang-hcl). Files are matched by extension:.php;.cand.h(a header is read as C);.shand.bash;.tfand.hcl.- PHP reads every extract;
propertyreads literal array keys ('acl' => 'private'), named arguments, property and constant defaults, enum case values,define()and writes such as$config['acl'] = 'private'; a promoted constructor parameter holds what eachnewpasses, so it refuses. Numbers are compared by value (0x10is16). In a merge the last literal value wins, and an operand Kibi cannot read after it refuses; a duplicate key,compact(),array_combine,array_fill_keys,array_flip, a negative implicit key, and a write through a reference (also a reference element or destructuring['acl' => &$a], a slot passed to a constructor or any function that is not a by-value builtin),unset,foreachby reference,++,ReflectionProperty::setValue,ArrayObject::offsetSetand a seconddefine()of a name refuse. Class names resolve through the namespace anduseimports (group uses too, each from the line it appears on) and are listed in full (App\Mail\Mailer::send,*.sendfor an unknown receiver); function names are listed in lower case, anddieandexitare both listed for either. PHP names ignore letter case, soexcludesrefuses a member that differs from an excluded item only in case. An unqualified call inside a namespace may reach the namespace's own function, so it is refused unless no project file that opens PHP code (any extension, dot folders andvendorincluded) declares that function there.envreadsgetenv,apache_getenv,env(),Env::getandfilter_input(INPUT_ENV|INPUT_SERVER, ...), and literal subscripts of$_ENVand$_SERVER; a call on a class that may extendIlluminate\Support\Envis refused while a project orvendorfile names that class (group uses too), and so is one onnew staticor on a trait's$this. Callables handed toarray_map,call_user_funcand the like are followed; a caught exception passed on as an argument is not a callable when it goes to a PHP exception's constructor or a by-value builtin (error_log,get_class); any other function or method may replace it through a reference, and so may anincludein the catch body, so the read refuses. Only a builtin'scallback:named argument is a callable. A dynamic call,eval,create_function, a stringassert, a getter's name in a string,$$name,$GLOBALS,get_defined_vars(), Reflection and itsinvoke-like methods (on any object),class_alias,FILTER_CALLBACK,PDO::FETCH_FUNC, a SOAPaddFunction, anewof a computed class with arguments, an include through a stream wrapper (data://,phar://), a caught exception whose variable may be replaced (&$e,extract,$$), and an environment getter called on an object Kibi cannot name in a file that namesEnvrefuse, and so do a*.mmember, a named first argument and a first-class callable of the callee (exec(...)) forstring_args. In a closure that the file rebinds (->call,bindTo,Closure::bind),selfandparentare unknown. Backticks are listed asshell_exec. Forproperty, a slot of the key passed to a function or method other than a by-value builtin (it may take it by reference), aforeachthat writes the key, a regex named group of the key, and an array rewrite (str_replace,array_map,array_change_key_caseand the like) in a file that names the key refuse. Strings are bytes read as UTF-8 ("\xc3\xa9"isé); a float joined into a string refuses, since PHP writes it with its own precision. - C reads
env,imports,calls,string_argsandproperty(the designated fields of an initializer,.field = value, GNUfield:labels too; a field set twice refuses); C has no thrown errors. Only initializers are read: a latercfg.field = valueis not, and a value is read as written, not converted to the field's type (a bit-field or a narrower integer may hold another value). An initializer written in a macro (TLS_DEFAULTS), a union in the file or an included header, and an array designator that is not a number refuse; an array of designated structs is read. Numbers are compared by value (0x10,1'000and16uare16), and a character constant refuses. Digraphs are read; trigraphs, a line splice inside an identifier and a backslash followed by blanks refuse. Macros the file defines are expanded, respecting#undef, and the names in a macro's body are listed; a header guard with a body is a macro. A function-like macro expands only where(follows it; elsewhere its name is the function of that name. A macro defined or undefined under a condition,#pragma push_macroorredefine_extname(also through_Pragma), one that uses#or##, and an#ifblock that adds members refuse; a macro whose body is a literal does not.#include,#importand#include_nextare read. An include may bring macros or function pointers, so it is refused unless the header (a quoted one next to the file, an angle one when any project file has that path) defines no macro the file uses, other than upper-case ones whose body calls nothing (pure builtins such as__builtin_expectaside), pastes nothing and names only their parameters, keywords and other such macros (one that ends with a name it does not define, such as#define HASH SHA256, counts where the file calls it), and no function pointer it calls (in any spelling, a function-typetypedeftoo), and holds noasm,alias,weakref,__REDIRECT,typeofor symbol-renaming pragma. A call through a variable, a function pointer or a struct field (ops->exec),environ, a call to an unknown function whose name looks like an environment getter, a/proc/.../environpath, a three-parametermainor constructor (K&R and macro-written ones too), inlineasm, a function declared inside a block, a getter handed on as a value, a string with an embedded\0, a positional struct initializer and a field set twice (also through a nested designator) refuse. Code in an included file (a header'sstatic inlinefunction, an#included.cfile) is that file's: list it infilestoo. - Bash reads
env,imports(sourceand., with the$(dirname "$0")script-folder idioms resolved, also through a variable set once at the top,DIR="$(cd "$(dirname "$0")" && pwd)"),callsandstring_args. A variable the script expands but never sets is an environment read, and so are~(HOME),test -v NAMEand arithmetic in an array index; one it sets only on some paths, or in an assignment with a redirection or in a background job, is refused as a read the environment may supply. Commands run throughsudo,env,timeoutand similar wrappers are followed; a runner whose command Kibi cannot name refuses, and so do the first argumentsxargsandfind -exec ... {}supply.eval,sh -c, a shell reading its script from input or/dev/stdin,trapcode,alias, any mention ofBASH_ALIASES,BASH_CMDSorBASH_ARGV0, a prompt variable set to anything but plain text,${x@P}, a command name Kibi cannot read ($cmd,"${cmds[0]}"), arithmetic on a value that is not certainly a number (a value such asa[$(rm -rf x)]runs code), a quoted subscript holding$(...),coproc, a/procenvironment path in any form (a glob, a variable,cd /proc/self), BSDps e, a word splice across lines, a here-document whose end line has trailing blanks, and asourceof a computed path (forenv) refuse, and so does a backtick or$[ ]in an unquoted here-document. Asourcerelative to$0resolves to the script's folder, and is refused when another project script may source this one (by name, a computed path or a glob:$0then names that script) or after the script changes directory. Commands a sourced file runs are that file's, as with a C include: list it infilestoo (an alias or function it defines is read there). Code handed to another interpreter (python -c,awk,sed e) is not read, except that anenvread refuses when the script holds text that reads the environment anywhere (ENVIRON,os.environ,%ENV,ENV[...],getenv(, jq'senv), or hands an interpreter a program from a variable.unsetof a name some function declareslocal(ordeclares),local -Iandshopt localvar_inheritrefuse forenv. - Terraform/HCL reads
propertyonly: the literal values of an attribute or object key anywhere in the file (strings with their blanks, numbers as exact decimals, bools,null, tuples, plain heredocs with their line ends). Keys are compared after Unicode NFC, as Terraform does, identifiers included. A value that needs evaluation (variables, functions, templates), a computed key (one with~strip markers or that NFC may compose with an interpolation too), a duplicate key (also of an object holding it, where the later one wins), aforobject,jsondecodeand similar functions, a value evaluation may drop (a conditional branch, an indexed or selected collection, aforbody, a splat, acoalesceortryargument, an operand of==,!,&&and other operators, a template%{ if }or%{ for }), amergewhose later argument Kibi cannot read or may replace the key's top-level object, a list expanded intomerge(...), a typed variabledefault(oroptionaldefault) the type converts, a decomposed (non-NFC) attribute name in a type, a number longer than Terraform's 512-bit precision, and a Terragruntincludeblock refuse, and so does any Terraformoverride.tf,*_override.tfor.tofufile in the same folder, whether or not the read includes it, since it may replace the value, and aterragrunt.hclthere with agenerateorincludeblock, which may write one. Values are read as written: a block thatcount,for_eachordynamicmay create zero or many times still shows its values. A folder Kibi cannot list refuses for the same reason. A computed key is allowed only in thefor_eachof a block that takes one (resource,data,module,dynamic,import,provider,ephemeral), where keys name instances.
- PHP reads every extract;
- Importing the app. The
routes,validatorand drizzle readers import the module, so it must load without side effects (nolisten, no database connection at import). Export a factory or a wrapper when the app needs dependencies.
Every reader is exact or refuses: a member Kibi cannot read statically
(process.env[name], a dynamic import(), an ALTER TABLE form it does not
know, a router mounted on a pattern) fails the read and names the file and
line, so a set claim is never decided over a set Kibi only partly saw.
The test leaves no certificate (or sourceCertificate({testId}) from
kibi-cli/proof-certificates): Kibi reads the source at ingest and stores
its own read as the certificate. What the set fact means depends on its
property and operator:
| Fact | The source must |
|---|---|
operator eq, includes, subset_of or excludes |
have exactly, at least, only, or none of the members |
property required_fields |
have each member as a required, non-null field of every selected table or object; a bare name (id) applies to every table, including tables added later, while table.field names one table. Only eq and includes |
property nullable_fields |
have each member nullable. Only eq and includes |
property middleware |
run the members (by the operator) before every selected route |
The references reader's members are names, so for it Kibi matches them by
segment (., /, ::) in the direction that can never prove a claim the
code does not hold:
excludesfails on every member that is, or may be, an excluded item: the item itself, a member under it (java.sqlexcludesjava.sql.Connection;github.com/labstack/echo/v4excludesgithub.com/labstack/echo/v4/middleware), a member under the prefix of an item ending in*(java.sql.*,os.*), a wildcard import that may bring the item in (java.sql.*forjava.sql.Connection), a shorter name the item ends with (File.deleteforjava.io.File.delete), a longer name ending in the item at a segment boundary (checkNotNullexcludesPreconditions.checkNotNull), and a call on a receiver Kibi cannot name (*.deletemay bejava.io.File.delete). The last four are refused with the member, the item it may be and the files read: Kibi cannot decide the claim for them.includesholds only on a member that certainly is the item: the item, a member underX.*, or a member ending in.mfor*.m. A*.mnever satisfiesX.m, andjava.sql.Connectiondoes not satisfyjava.sql.subset_ofallows a member only when it certainly is an allowed item, soos/execis not allowed byos(writeos/*for anything under it), and*.monly by*.mitself.eqcompares spellings, as for every other reader.
PHP function and class names ignore letter case, so for a PHP read
excludes also refuses a member that matches an excluded item only when
letter case is ignored (Exec for exec). includes, subset_of and
eq stay exact.
A references read decides a claim only for the files it reads. A proof is
rejected when a symbol the test covers (covered_by) or a symbol that
implements the claim's requirement lies in a file outside files (after
ignore), naming the symbol and its file: a refused file cannot be dropped
from files to prove a claim about it on other code. A claim whose subject
key or claim text names a project file (src/match.c, a file name such as
utils.sh that Kibi finds in the project, a path with a repository prefix
such as fzy/src/match.c, another letter case, a slashed name without its
extension such as src/match, or a quoted path with spaces) is also rejected
when none of the files read is that file, so a claim about one file cannot be decided on
another. A string_args read
whose callee is an environment getter (os.Getenv, os.getenv,
System.getenv, env::var, env::var_os,
Environment.GetEnvironmentVariable, getenv/2 and the like) sees only that
getter's first arguments, so it cannot decide an eq, subset_of or
excludes claim about what the code reads; Kibi refuses it and names
extract: env, which reads every getter.
kibi prove counts a test whose run passed but whose certificate or claim
binding Kibi rejected as refused, not passed. A derived binding on a
receipt whose verification_scope is not end_to_end says that the claim
stays unproved until the requirement is tested end to end, and
kibi proof explain lists each claim's bindings and rejections, and why a
derived claim is not yet proved, whatever the requirement's proof status;
signoff shows the same rejections.
The receipt's binding covers what the read depends on: each file it parses,
the list of files a glob matches, and the modules a drizzle schema, route
table or validator imports (through relative imports, tsconfig paths
including those inherited through extends, and workspace packages linked
into node_modules). Change any of them and the receipt is stale. A claim a
source proof derives is bound with scope source, so coverage and signoff
show that the proof rests on project files Kibi read.
The certificate builders (sourceCertificate, readProjectSource) are in
kibi-cli/proof-certificates from kibi-cli 2.33.0; a project that pins an
older kibi-cli has to upgrade before its tests can import them.
Sources also feed the other kinds. An arithmetic or smt value_source entry
{source: {...}, measure: count} counts the members ("at most 40 routes"),
and a rule domain {source: {...}} ranges a predicate over the members
("every route Kibi reads is guarded"); readProjectSource in
kibi-cli/proof-certificates gives the test the same members. A rule
predicate whose function is a test harness that drives the code (it calls
the handler for each role) says so with harness_for: [SYM-...]; its
claims are bound with scope harness.
The semantic advisor models these statements for you: "Every route under
/api must use requireAuth" (middleware, includes), "Every table must have id
and created_at" or "Every table in schema.ts must have a required org_id
column" (required_fields), "The deleted_at column of organizations must be
nullable" (nullable_fields), "Every environment variable the app reads must
be one of A, B or C" (subset_of), "The web app must never import node:fs" or
"... @supabase/supabase-js" (imports, excludes), "The top-level keys of
scenarios.json must be exactly groups and scenarios" (eq), and "The server
must expose at least 30 routes under /api/orgs" (count). A field claim needs
a schema word (table, object, schema, row, record, model, entity, type,
payload, DTO, or field, column or property next to the names): "Every
organization must have an owner" is about membership and stays a behavioural
claim. kb_model in requirement mode writes the same set facts when you pass
operator (eq, includes, subset_of, excludes) and a list value.
Temporal claims on state machines
A state_machine set fact can name reachable_pairs (from->to pairs joined
by a path of one or more transitions) and required_waypoints
(from->to via state: the target is reachable and every path to it passes
through the state), and any machine projection can use includes,
subset_of or excludes. "A cancelled invoice can never become paid" is a
reachable_pairs fact with excludes (and "A member can never become
owner", with no machine named, one on subject workflow); "Every path from draft to paid must
pass through approved" is a required_waypoints fact. A failure names the
path or the state:
rejected: a path from draft reaches paid without passing through approvedWhat proved covers
A requirement is proved when it is tested end to end and every applicable
claim (every normative claim of its inventory; nonlogical context spans are
not claims) has a derived, code-scope binding from a fresh, re-checked
certificate. Coverage reports claimSummary per requirement (applicable,
proved, attested, tested and unresolved claims), lists unprovedClaims, and
kibi proof explain shows each claim with its bindings. A requirement whose
claims are only partly proved is tested, and the table and the HTML report
show n/m claims proved. A claim that is not yet modeled keeps the
requirement below proved until it is.
arithmetic and smt certificates pay off only when the requirement rests on
typed numeric facts (bounds, limits, counts). For an enumerable domain use
exhaustive or typecheck; for decisions, lifecycles and formats use
rule, state_machine or regex; for routes, schemas, configuration and
the references the code makes use source.
Kibi never takes a certificate's word for it. At ingest it re-checks the
certificate (every member listed once and passed; the arithmetic decided by
the same interval reasoning as contradiction checks; the solver re-run), and
it checks the workspace. For an exhaustive proof Kibi loads the
domain_source symbol's export itself (with bun when it is on PATH, else
node with type stripping) and needs the certificate to cover exactly its
members (or, for non-primitive members, as many of them), so a hand-listed
subset is rejected; the proof file must also import the symbol. A typecheck
witness must import the domain and be the file in the workspace (its sha256),
and Kibi compiles it itself with the workspace's TypeScript under fixed strict
options, so a test cannot report a pass the compiler would not give. Imports
resolve the way the nearest tsconfig.json says (module,
moduleResolution, paths, ...), and the declaration files that tsconfig
includes (a reset.d.ts, for example) are part of the witness program;
without one, NodeNext. When the test's own compile failed, the rejection adds
the first error Kibi's compile of the witness reports, or that Kibi's compile
accepts it. TypeScript 7 ships
without the compiler API, so Kibi falls back to an older typescript further
up the workspace and otherwise asks for typescript@6 as a dev dependency. A
contract without proves rests on the witness alone, so the witness must also
contain an exhaustiveness check, a never type or a satisfies map, read
from its syntax (the word in a comment does not count). With proves, Kibi
checks each claim with a witness it writes itself, and a rejection names the
fields that are missing, optional or nullable. Arithmetic and SMT certificates must match their
constraint_source facts as described above. The receipt
(kibi.proof-receipt.v2) records kind,
evidence_level, the certificate and its digest (the sha256 of its canonical
JSON, computed by code_certificates.pl), or certificate_rejection with the
reason.
Whenever it reports evidence, Kibi re-checks the stored certificate's digest
and structure (and re-decides an arithmetic claim); the solver and workspace
checks run at ingest only. The digest is an unsigned hash: it catches edited
or corrupted receipts, not deliberate forgery, which review of the receipt
diff is for. A receipt that claimed proved but no longer re-checks counts as
tested, and coverage and kibi proof explain name the test and the reason
(certificateRejections).
A certificate strengthens end-to-end evidence; it never replaces the
journey test. required_evidence: proved makes the evidence-floor rule demand
proved, and its finding lists the claims no certificate derives
(gapDetails.unprovedClaims).
A bun test proof step that runs a *.proof.test.* file is never satisfied by
reused unit results, because reuse would leave no certificate.
Integration configuration
Evidence production is configured in tracked, Kibi-managed
.kb/proof/integrations.json:
{
"version": "kibi.proof-integration.v1",
"integrations": [
{
"id": "self-proof",
"producer": "command",
"command": ["node", "scripts/run-proof-producer.mjs"],
"artifact": ".kb/proof/runs/self-proof.json",
"targets": ["default"],
"description": "Packed end-to-end step commands"
},
{
"id": "web-e2e",
"producer": "playwright",
"command": ["npx", "playwright", "test"]
},
{
"id": "api-tests",
"producer": "junit",
"command": ["pytest", "--junitxml=.kb/proof/runs/api-junit.xml"],
"artifact": ".kb/proof/runs/api-junit.xml"
}
]
}commandis executed withshell: false, exactly as configured.- The child environment always includes
KIBI_PROOF_RUN=1. Runner configurations that must behave differently under a proof run (for example, Playwrightretries: 0) should branch on this stable marker instead of guessing which output-path variable implies a proof run. producer: commandlets Kibi synthesize the envelope from the process outcome (aggregate-run provenance). One process runs the steps of every selected test, so by default one failing step fails every test in the run. To have each test judged by its own steps, the command writes akibi.proof-test-report.v1to the path inKIBI_PROOF_TEST_REPORT(see per-test attribution).producer: playwright(or a custom id) expects the child to emitkibi.proof-run.v1atKIBI_PROOF_OUTPUT.producer: junit/tapmake Kibi convert the native report atartifactinto bound proof results using each test'sproof_bindings.description, labels, and comments are cosmetic: editing them never invalidates proof. Execution-relevant edits (command, artifact, targets, options, bindings, contract) change the effective execution fingerprint and stale prior evidence.
kibi init never creates this file, and agents never hand-write it.
kibi proof inspect --json proposes a command integration for the
detected test runner (the package.json test script when there is one)
and returns integrationPlan, a hash-bound kibi.migration-plan.v2 with one
proof_integration_configure action, plus the contractDefaults a
proof-bearing test's proof_contract uses. Review it, then apply it through
Kibi:
kibi proof inspect --json > inspection.json
jq '{plan: .integrationPlan, approvedPlanHash: .integrationPlan.planHash,
approvedActionIds: [.integrationPlan.actions[].id]}' inspection.json \
| kibi apply-plan --input -MCP hosts pass the same three fields to kb_apply_plan. The plan binds the
hashes of the files the proposal came from (package.json, lockfiles,
runner configs) and is refused once one changed. A create plan is refused
once the file exists; kibi proof inspect --update <id> --json plans adding
or replacing one named integration and is bound to the file's current hash.
--command "<command>" (space-separated words or a JSON argv array)
proposes that command instead of the detected one, on a create or an update
plan. A command integration judges each selected test by the exit code of
the whole run, so when it runs the package's whole test script, any failing
test anywhere fails every proof obligation that names it and a suite with
unrelated failures proves nothing; integrationPlanReason says so. Narrow the
command to the proof-bearing tests (kibi proof inspect --update <id> --command "<command>" --json) or have it write a per-test report (below). Greenfield repositories without a harness record proof integration
as deferred — Kibi does not install test frameworks.
The canonical artifact: kibi.proof-run.v1
Most consumers never hand-write artifacts — kibi prove and its producers
do. Direct kb_ingest_proof calls are an integration path for custom
producers and agents.
{
"version": "kibi.proof-run.v1",
"producer": { "name": "kibi-playwright-producer", "version": "1.0.1" },
"executor": { "name": "playwright", "version": "1.57.0" },
"integration": "web-e2e",
"command_argv": ["npx", "playwright", "test", "--project=chromium"],
"code_snapshot": "<64-hex snapshot captured before the run>",
"environment": {
"os": "linux",
"arch": "x86_64",
"runtime": { "name": "node", "version": "v24.15.0" },
"artifacts": { "lockfile_digest": "<…>" }
},
"run": {
"outcome": "passed",
"exit_code": 0,
"started_at": "2026-08-30T09:00:00.000Z",
"finished_at": "2026-08-30T09:02:03.000Z"
},
"proof_results": [
{
"symbol_id": "SYM-PW-4F2A9C61B7D03E85",
"target": "chromium",
"outcome": "passed",
"binding": "native_case",
"native_id": "tests/checkout.spec.ts:4:5 › checkout › accepts a card",
"attempts": {
"status": "complete",
"entries": [{ "outcome": "passed", "duration_ms": 2415 }]
}
}
]
}Field contract:
| Field | Requirement |
|---|---|
version |
Literal kibi.proof-run.v1 |
producer |
{name, version?} — the component that produced the artifact |
executor |
Optional {name, version?} — the underlying runner |
integration |
Configured integration id this run belongs to |
command_argv |
Non-empty; must equal the integration's configured command |
code_snapshot |
64-hex; must equal the live snapshot captured before the run |
environment |
Typed JSON object; Kibi canonicalizes and hashes it |
run |
Run-level outcome, process exit code, ISO timestamps, optional failure_phase (setup, collection, execution, teardown, infrastructure) |
proof_results |
1–1000 results, each shaped as below |
Each proof result:
| Field | Requirement |
|---|---|
symbol_id |
Non-empty stable proof symbol (SYM-*) |
target |
Ecosystem-neutral execution target |
outcome |
passed, failed, timed_out, skipped, interrupted, or errored |
binding |
native_case (individually observed) or aggregate_run (bound process outcome) |
native_id |
Optional native runner identity (provenance only) |
attempts |
{status: "complete", entries: [{outcome, duration_ms?}]} or {status: "unavailable"} |
Attempt history is factual
Adapters report facts, never improve them. When a source format cannot prove
retry history, report attempts: {status: "unavailable"}. The strict
all_required_first_attempt policy:
- accepts complete history whose first attempt passed,
- rejects any known non-passing first attempt,
- fails closed on unavailable native history,
- accepts
aggregate_runresults only through the single Kibi-launched process invocation itself (the process attempt), which is known first-attempt evidence when the run passed with exit code 0.
Run-level outcomes are authoritative
run.outcome — passed, failed, errored, cancelled, timed_out,
interrupted, no_results — is evaluated independently of individual
results. Under all_required_first_attempt a failed run never proves
anything, no matter how many results passed before the failure.
own_results_first_attempt: one failing case does not erase a suite
A large end-to-end suite almost always has some failing or flaky case, and
under the strict policy that one case fails every receipt of the run. A
contract can opt into success_policy: own_results_first_attempt instead:
- a run whose outcome is
failedin theexecutionphase (or with no phase) is judged per obligation, so an obligation whose ownnative_caseresult passed on its first attempt is proved even though other cases failed; - the first-attempt rules above still apply to the obligation's own result;
aggregate_runobligations still need the whole run to pass, because they have no result of their own;errored,cancelled,timed_out,interruptedandno_resultsruns, and failures insetup,collection,teardownorinfrastructure, still refuse every obligation.
kibi-cli/playwright-reporter marks a run that only failed test cases as
failure_phase: execution; an error outside the test cases (config, global
setup, a spec that fails to load) is setup, a global timeout is timed_out
and an interrupted run is interrupted. Use the policy where the per-case
results are native, as with the Playwright reporter; keep the strict policy
where a run-level failure should block everything.
Environments and fingerprints
Producers provide typed environment dimensions (OS, architecture, runtime,
container digest, lockfile digest, database image, device, deployment).
Kibi canonicalizes the object and derives environment_hash itself.
Each receipt stores the effective execution fingerprint and its components (contract, integration, command, bindings, producer). A receipt whose components differ from the test's current execution is stale, and the drifted components are named:
kibi proveprintsexecution fingerprint changed since its previous receipt (integration, command)for each re-proven test, and the JSON summary carriesfingerprintDrifton that test's row.kibi coverageandkb_coveragelist the same names asfingerprintDrifton the test'sreceiptEvidencerow, beside thestale_proof_receiptgap.kibi proof explainprintsfingerprint drift for TEST-x: integration, commandand names the tests in the stale gap's reason.
A test whose contract, bindings or integration cannot be resolved is not
checked for drift; other rules report those. With
KIBI_PROOF_BINDING_MODE=strict-snapshot only kibi prove names the drift,
because every tracked change already stales a receipt by snapshot there.
After upgrading, a receipt recorded before an integration edit reports
stale_proof_receipt; run kibi prove --stale to refresh it.
Receipt freshness scope
By default a receipt stays fresh until something it depends on changes, not
until any file in the workspace changes. Each receipt records a
binding_hash over:
- the test's
proof_contract, - the test document itself (excluding its
proof_receiptshistory and itslinks, so adding or reordering a link does not invalidate the receipt; receipts written before links were excluded stay current), and - the code scope: the current source-file content of every symbol in
- the contract's
required_proofs(the test's own executable code), - the test's
proof_bindings, and - every production symbol linked
covered_bythis test.
- the contract's
Editing any of those files (or deleting one) changes the binding, so that
test's receipts go stale until kibi prove runs again. Edits elsewhere leave
them fresh. Keep covered_by links accurate: they decide which production
code a receipt vouches for. Set KIBI_PROOF_BINDING_MODE=strict-snapshot to
instead bind every receipt to the whole-workspace snapshot it was proven on.
Both the binding and the snapshot are computed from repository-relative paths and file contents only, so a CI runner and a developer checkout of the same commit agree:
- the workspace snapshot names each file by its repository-relative path in Unicode NFC, so a checkout whose filesystem reports decomposed names hashes the same as one that reports composed names, wherever the repository lives;
- a test document and each symbol's
sourceFileare located through their normalized repository-relative path (./a.ts,a\b.tsand an absolute path inside the checkout all name the same file); - a
sourceFilethat leaves the repository (a..escape or another machine's absolute path) contributes the fixed markeroutside-workspaceinstead of whatever that path holds on the current machine.
Receipt history compaction
Every kibi prove run appends one receipt per test, so a long-lived branch
would otherwise accumulate receipts that can no longer change any decision.
Ingest compacts each history as it appends
(kibi.proof-receipt-compaction.v1), keeping only:
- the newest receipt, and the newest passing receipt;
- for the current binding and for the live snapshot, the newest receipt per verification scope and contract hash — exactly the receipts coverage selects from.
Compaction is deterministic and only removes receipts: the kept receipts are
the originals, in their original order, so every coverage decision is the
same before and after. Stores written before compaction can be compacted
once with kibi proof compact (--dry-run reports what would be removed).
Per-test attribution for command integrations
A command integration is one process. When it writes a
kibi.proof-test-report.v1 to the path Kibi passes in
KIBI_PROOF_TEST_REPORT, each selected test is evaluated against its own
steps:
{
"version": "kibi.proof-test-report.v1",
"tests": [
{
"test_id": "TEST-checkout",
"outcome": "failed",
"steps": [
{ "step_index": 1, "command": ["bun", "test", "checkout"], "outcome": "failed", "exit_code": 1 }
]
}
]
}Tests whose steps all passed receive passing receipts even when the process
exited non-zero; only tests with a failing step receive failing receipts. The
kibi prove summary lists attribution: "per_test" and the failedSteps
(test, step, command, outcome, exit code) for that integration. Attribution
fails closed: with no report, a report that omits or duplicates a selected
test, a test reported passed over a failing step, or a failing process whose
report blames no test, the whole run is evaluated as one unit as before
(attribution: "aggregate" with an attributionReason). A valid report in
which every test passed is also per_test. Kibi's own
scripts/run-proof-producer.mjs writes this report.
kibi proof run-steps
Projects do not need their own producer script. Declare each proof-bearing
test's steps (argv arrays, no shell) in proof/steps.json:
[
{ "test_id": "TEST-checkout", "steps": [["bun", "test", "./tests/checkout.test.ts"]] },
{ "test_id": "TEST-cart", "steps": [["node", "--test", "tests/cart.test.js"]] }
]and make kibi proof run-steps the integration's command:
{ "id": "unit", "producer": "command", "command": ["npx", "kibi", "proof", "run-steps"], "targets": ["default"] }It runs the steps of the tests kibi prove selected, each distinct argv once
(in its own process group, with a timeout from KIBI_PROOF_STEP_TIMEOUT_MS),
and writes the per-test report, so each test gets its own receipt. Ctrl-C
during kibi prove stops the whole process group of the producer, so no test
worker is left running.
Trust boundary
Local proof evidence is trusted as part of the local execution environment. An out-of-process adapter can lie just as an in-process reporter can lie. Kibi's guarantees are about binding, policy, freshness, and history — not independent attestation. The envelope is designed so CI identities, signatures, or attestations can be added later.
Representative recipes
- Web UI (Playwright, optional native producer): register
kibi-cli/playwright-reporterinplaywright.config.ts; add aplaywrightintegration; the reporter emitskibi.proof-run.v1with producer namekibi-playwright-producer. Obligations map toSYM-PW-*case symbols. Kibi does not depend on Playwright. - API (pytest via JUnit XML): run
pytest --junitxml=…with ajunitintegration; authorproof_bindingsmapping native test ids to symbols; attempt history isunavailable(standard JUnit XML has no retry data). - JVM/.NET libraries: Maven/Gradle/dotnet produce JUnit XML; same adapter path as pytest.
- Go/Rust:
go test -jsonor TAP-consuming harnesses via thetapintegration; TAP has no retry history, so attempts areunavailable. - Database products: a command integration that starts a cluster,
migrates, writes, replicates, and reads; obligations bound with
aggregate_runprovenance. - CLI tools: a command integration running the installed binary and asserting observed behavior.
- Custom/proprietary runners: emit
kibi.proof-run.v1directly and callkb_ingest_proof, or wrap the run in a command integration.
Examples live in docs/examples/proof and are validated against the schema in CI.
Common failures and fixes
| Error | Cause | Fix |
|---|---|---|
No proof integration configuration at .kb/proof/integrations.json |
Proof is not configured for this repository yet | Run kibi proof inspect --json and apply its integrationPlan with kb_apply_plan / kibi apply-plan |
integration '<id>' … is not configured in .kb/proof/integrations.json |
A test names an integration the file lacks | Run kibi proof inspect --update <id> --json and apply the plan |
artifact command_argv does not match the configured command |
Command drift | Run through kibi prove so the configured command executes |
captured snapshot is not the live workspace snapshot |
Tracked files changed between capture and ingest | Re-run kibi prove; do not edit the tree mid-proof |
run did not pass (outcome: …) |
Run-level failure despite passing results | Fix the run (setup/teardown/infrastructure); rerun |
attempt history unavailable |
Source format carries no retry data | Accept aggregate provenance, or use a producer with complete history |
missing proof result |
Producer never reported a required obligation | Ensure the obligation ran in the configured integration/target |
proof_receipts is append-only |
History was rewritten by hand | Update via the engine (kibi prove, kibi proof compact); never edit history |
Every test of a command integration failed for one step |
The command wrote no kibi.proof-test-report.v1 |
Write the report to KIBI_PROOF_TEST_REPORT; the summary's attributionReason says why it was not used |
| Receipts stale after code changes | The edit touched code in a test's freshness scope | Re-run kibi prove for the affected tests |
For agent workflows
Bundled skill guidance is in kibi-usage → resources/proof.md
(kb_skills with action: "load" and id: "kibi-usage", then action: "read").
Deterministic discovery: kibi proof inspect --json.