vidi
vidi records who — and which AI model — authored each piece of a codebase, lets humans vouch for the pieces they have reviewed, and gates a merge on review coverage.
The problem
A pull-request approval says “Ada approved this change.” It does not say which functions Ada actually read, and it stays true forever — including after the code is rewritten. It ages into a claim nothing checks.
vidi records something narrower and more useful: which small pieces of code a person read, and exactly what those pieces looked like at the time. When the code changes, the claim stops applying — automatically, for that piece only.
How it works, in four steps
- Units. vidi splits every file into small nameable pieces — one function, one struct, one paragraph — instead of treating a file as one reviewable thing.
- Fingerprints. Each unit gets a content hash. Same bytes, same fingerprint; one character different, a completely different fingerprint.
- Vouches. Reviewing a unit records a signed statement: who you are, which unit, its fingerprint, and your verdict.
- Expiry. Later, vidi recomputes each fingerprint and compares. Match means the review still describes the code. Different means it does not.
Nobody has to remember to invalidate anything. A review expires exactly when the code it described stops existing.
Where to go
Get started — install it, review one unit, watch a review go stale, and turn on the gate. Start here if you have never run vidi.
Guides — task-shaped instructions: blocking merges in CI, reviewing a pull request, revoking an approval, sharing reviews across a team.
Reference — every command, every policy key, every unit state, and the on-disk file formats. The CLI reference is generated from the binary, so it cannot drift from the code.
Explanation — why it is built this way: units rather than files, fingerprints as an expiry mechanism, the parser-free core, and why review records never cause merge conflicts.
Contributing — building vidi itself.
Status
These docs are being written. Pages that do not exist yet are listed in the sidebar so the gaps are visible rather than silently missing; each carries a note about what belongs in it.
Install
Prerequisites
Rust, via rustup. The repo’s rust-toolchain.toml
pins the channel and components; rustup reads it on your first cargo
command and installs what is missing.
A linker. Since Rust does not ship one:
| Platform | Install |
|---|---|
| Linux / WSL | sudo apt install build-essential pkg-config |
| macOS | xcode-select --install |
| Windows | Visual Studio Build Tools — the rustup installer offers these; accept |
Close every terminal and editor after installing. Installers add
~/.cargo/binto your PATH, but a running process keeps the environment it started with. A new tab in an app that was already open still will not seecargo.
$ cargo --version
cargo 1.8x.0
Build and install
$ git clone https://github.com/graze-ai/vidi.git
$ cd vidi
$ cargo install --path crates/vidi-cli
The first build compiles 21 tree-sitter grammars from C and takes a few minutes. Later builds are incremental.
cargo buildis not enough. It leaves the binary intarget/, where your shell will not find it.cargo installis what putsvidion your PATH.
$ vidi --version
vidi 0.1.2
Editor extensions
Optional, as the CLI is complete without them.
VS Code
$ cd editors/vscode
$ npm ci
$ npm run build
$ npx vsce package
$ code --install-extension vidi-vscode-*.vsix
Requires Node 22.
On WSL, run
code --install-extensionfrom your WSL shell. Extensions install per-context; installing on the Windows side leaves the extension invisible to a WSL window.
JetBrains
$ cd editors/jetbrains
$ ./gradlew buildPlugin
Requires JDK 21. The wrapper fetches Gradle and the IDE SDK on first run.
Updating
$ git pull
$ cargo install --path crates/vidi-cli
Rebuild and reinstall the editor extension separately if editors/ changed.
Next
Your first vouch
By the end of this page you will have reviewed one unit and seen the coverage number move.
Numbers will not match. Every count here depends on how many files your repo has, and it changes as you edit, including when you edit documentation. Read
5356as “some number”, and not a value to compare against.
Where you’re starting
vidi status is read-only. It scans the repo, splits every file into units, and
reports how many carry a current review.
$ vidi status
NEUTRAL: 0/5356 reviewed · stale 0 · orphans 0 · requirement failures 0 · default shortfalls 0
Nothing is reviewed yet, so 0/5356.
NEUTRAL is one of three verdicts, and it means no policy file and nothing
structurally broken:
| Verdict | When |
|---|---|
FAILURE | anything stale, orphaned, or failing a requirement — checked first, whether or not a policy exists |
PASS | a policy is present and every requirement is met |
NEUTRAL | no .vidi/policy.toml, and nothing broken. Counts as passing for branch protection |
Staleness is not a policy rule, it is an integrity property. A review claiming to cover bytes that no longer exist is broken regardless of what your policy says, so it fails even in a repo with no policy at all. See Turn on the gate.
Set the repo up
vidi init is the visible opt-in. Until you run it, every command that would
write to .vidi/ refuses.
$ vidi init
initialized: this repo can now track reviews in .vidi/
created .vidi/: commit these files so reviews travel with the repo
updated .gitattributes + .gitignore: combine review records across branches; keep the scan cache out of git
stage git add .vidi/ .gitattributes .gitignore
next: run vidi scan to address the repo, then vidi status to see current review coverage.
when ready: add .vidi/policy.toml to turn review coverage into a CI gate.
Three empty ledger files appear, and one line is added to .gitattributes:
$ ls .vidi/
authorship.jsonl reviews.jsonl revocations.jsonl
$ git diff .gitattributes
+.vidi/*.jsonl merge=union
That merge=union line is what stops review records from ever causing a merge
conflict, git keeps every line from both sides. See
Why review files never conflict.
Commit these. Review history is meant to travel with the code.
Log in
Writes have to be attributed to somebody, so they require a login. The offline form needs no account and no network:
$ vidi login --local
Logged in locally as you@example.com (self-asserted, offline)
Writes (vouch / review / revoke) are now authorized on this machine.
The identity defaults to your git config user.email. Check that it is the
address you want. It is written into every record you create, and the ledger
is append-only. To choose explicitly:
$ vidi login --local you@example.com
Confirm at any time:
$ vidi whoami
you@example.com (local, self-asserted)
Find something to review
vidi queue ranks every unreviewed unit by significance and shows the top 20.
Pass --top N to widen the cut, or --all for the full list.
$ vidi queue --top 8
re-review queue: top 8 of 5356 below the bar · gating first, then significance × severity
1. advisory · sig 100 · unreviewed crates/vidi-core/src/crypto/assurance.rs::enum:AssuranceScalar
2. advisory · sig 100 · unreviewed crates/vidi-core/src/crypto/entrypoint.rs::enum:SignRefusal
3. advisory · sig 100 · unreviewed crates/vidi-core/src/crypto/error.rs::enum:VerifyError
4. advisory · sig 100 · unreviewed crates/vidi-core/src/crypto/keyid.rs::struct:KeyId
5. advisory · sig 100 · unreviewed crates/vidi-core/src/digest.rs::struct:Blake3Digest
6. advisory · sig 100 · unreviewed crates/vidi-core/src/error.rs::enum:DecodeError
7. advisory · sig 100 · unreviewed crates/vidi-core/src/ledger/codec.rs::fn:check_no_dup_keys::struct:NoDupKeys
8. advisory · sig 100 · unreviewed crates/vidi-core/src/policy/diagnostics.rs::enum:PolicyError
Each line is a unit address, the file, then ::, then what’s inside it:
crates/vidi-core/src/digest.rs::struct:Blake3Digest
└─────────── file ───────────┘ └── what's inside ──┘
Read it
Open the file and find the declaration named in the address. There is currently no vidi command that prints a unit’s source, so use your editor.
The machine report knows the exact lines:
$ vidi json | python -c "import sys,json; [print(u['file'], u['lineStart'], u['lineEnd']) for u in json.load(sys.stdin)['units'] if u['address'].endswith('struct:Blake3Digest')]"
crates/vidi-core/src/digest.rs 55 55
vidi showdoes not do this. It accepts either acontent_id(32 hex characters) or a unit address, but what it prints is a ledger record. A review someone already made, not source code.
Vouch for it
$ vidi vouch "crates/vidi-core/src/digest.rs::struct:Blake3Digest"
vouched crates/vidi-core/src/digest.rs::struct:Blake3Digest as you@example.com
content_id 28d7043ed80ea10df8a78cd4b6f32f3f · appended to .vidi/ (bare line, diff-visible)
note: this repo has no .vidi/policy.toml: this vouch is recorded without a standing and will never satisfy a rank requirement. To count toward one: add a policy with a [reviewers] entry, then vouch again.
The content_id is the record’s own identity. That is the value vidi show
and vidi revoke accept.
The note means that without a policy file the vouch is recorded but cannot satisfy any requirement, because none exist yet. Expected at this stage.
The number moved
$ vidi status
NEUTRAL: 1/5356 reviewed · stale 0 · orphans 0 · requirement failures 0 · default shortfalls 0
0/5356 → 1/5356. One unit now carries a current review.
What actually got written
One line in .vidi/reviews.jsonl, an in-toto Statement:
{
"_type": "https://in-toto.io/Statement/v1",
"subject": [{
"name": "crates/vidi-core/src/digest.rs::struct:Blake3Digest",
"digest": { "blake3": "76b53b0c9cfd57d07d2a4802263e6856" },
"repo": { "host": "github.com", "owner": "graze-ai", "name": "vidi" }
}],
"predicateType": "https://vidivouch.com/vidi/human-review/v1",
"predicate": {
"criteria": ["equivalent"],
"rigor": "read-fully",
"attesterIdentity": { "kind": "person", "id": "you@example.com" },
"verdict": "approved",
"claimTimestamp": "2026-07-30T20:52:58Z",
"profileVersion": "rust-1",
"scopeModelVersion": "vidi-scope-4"
}
}
The blake3 value is the fingerprint of the unit as it was when you read
it. That one field is what makes the next page work.
Next
Watch a review go stale
This is the idea the whole tool is built upon: a review expires when the code it described no longer exists.
Where you are
One reviewed unit, and nothing stale:
$ vidi status
NEUTRAL: 1/5456 reviewed · stale 0 · orphans 0 · requirement failures 0 · default shortfalls 0
Change the code you vouched for
Open crates/vidi-core/src/digest.rs and edit line 55 — the unit you approved:
#![allow(unused)]
fn main() {
pub struct Blake3Digest([u8; 16]); // before
pub struct Blake3Digest([u8; 32]); // after
}
Save the file.
Run status again
$ vidi status
FAILURE: 0/5456 reviewed · stale 1 · orphans 0 · requirement failures 0 · default shortfalls 0
Three things changed on their own:
1/5456→0/5456— the unit is no longer coveredstale 0→stale 1— and vidi knows why it is uncoveredNEUTRAL→FAILURE— the gate now fails
Vidi recomputed the unit’s fingerprint, compared it to the one stored in your review, and they no longer matched, leading it to go stale.
fingerprint stored in the vouch 76b53b0c9cfd57d07d2a4802263e6856
fingerprint of the code now (different, so the bytes changed)
─────────────────────────────────
this review no longer describes
the code that is there
The gate agrees
$ vidi verify
FAIL: stale 1 · orphans 0 · requirement failures 0 · default shortfalls 0 · rejected ledger lines 0
▲ stale crates/vidi-core/src/digest.rs::struct:Blake3Digest
verify exits non-zero, which is what blocks a merge in CI. It also names the
exact unit, so nobody has to guess what went wrong.
This happened with no policy file. Staleness is not a rule you configure, it is an integrity property. A review claiming to cover bytes that no longer exist is broken regardless of your policy, so it fails even in an unconfigured repo. That is why the verdict is
FAILURErather thanNEUTRAL.
Put it back
#![allow(unused)]
fn main() {
pub struct Blake3Digest([u8; 16]);
}
$ vidi status
NEUTRAL: 1/5456 reviewed · stale 0 · orphans 0 · requirement failures 0 · default shortfalls 0
Restoring the original bytes restores the original fingerprint.
Why this matters
A normal pull-request approval says “Ada approved this change” and stays true forever, even after the code is rewritten. It ages into a lie that nothing detects.
A vouch says “Ada read exactly these bytes”, and the moment those bytes change the claim stops applying. Every other reviewed unit in the file is untouched.
That is the entire design. Everything else: addressing, the ledger, the policy, all exist to make this one comparison possible.
Next
Turn on the gate
So far vidi has only reported. This page makes review coverage a merge requirement.
Before: nothing is enforced
$ vidi verify
no policy file: .vidi/policy.toml absent
NEUTRAL: unconfigured (counts as passing for branch protection)
opt in with `vidi init`, then add a [[cover]] or [[scope]] block.
NEUTRAL means no rules exist, so nothing can fail. A repository opts into
gating by adding exactly one file: .vidi/policy.toml.
Write a policy
Start narrow. A policy that gates the whole repository on day one just fails every build.
# .vidi/policy.toml
schema = "1.0"
# Paths matched by no gating [[scope]] are report-only.
default = "advisory"
# The reviewer ladder, lowest rank first.
[axes]
human.order = ["contributor", "reviewer", "maintainer"]
# The reviewer directory. Only people listed here have a standing that can
# satisfy a rank requirement.
[reviewers]
"you@example.com" = { role = "maintainer", name = "Your Name" }
# One gated path: the crypto core needs a maintainer's review.
[[scope]]
path = "crates/vidi-core/src/crypto/**"
accept = [ { human = "maintainer" } ]
Four things are doing work here:
| Key | Effect |
|---|---|
default = "advisory" | everything outside a [[scope]] is reported, never gate-failing |
[axes] human.order | the rank ladder; later entries outrank earlier ones |
[reviewers] | who has a standing. A vouch from someone not listed cannot satisfy a requirement |
[[scope]] | a path glob plus the accept rows a review must meet |
A
[[scope]]with an emptyaccept = []is a load error, not a silent no-op. A rule that could never be satisfied is rejected when the policy loads rather than quietly passing everything.
After: the gate bites
$ vidi verify
FAIL: stale 0 · orphans 0 · requirement failures 175 · default shortfalls 0 · rejected ledger lines 0
unmet crates/vidi-core/src/crypto/assurance.rs::enum:AssuranceScalar
unmet crates/vidi-core/src/crypto/assurance.rs::enum:Identity
unmet crates/vidi-core/src/crypto/assurance.rs::enum:Presence
unmet crates/vidi-core/src/crypto/entrypoint.rs::enum:SignRefusal
...
175 units under crypto/** now require a maintainer’s review and have none.
Everything outside that glob is untouched, that is default = "advisory"
working.
The exit code is the gate
$ vidi verify > /dev/null; echo $?
1
That non-zero exit is the entire enforcement mechanism. CI sees a failing command and blocks the merge. See Block merges in CI.
vidi statusis not a gate. It prints the same verdict word but always exits 0:$ vidi status FAILURE: 0/5489 reviewed · stale 0 · orphans 0 · requirement failures 175 · default shortfalls 0 $ echo $? 0A CI job running
vidi statusprintsFAILUREon every build and passes anyway. Usevidi verify.
Three verdicts
| Verdict | Exit | When |
|---|---|---|
FAILURE | 1 | anything stale, orphaned, or unmet, evaluated before policy presence |
PASS | 0 | a policy exists and every requirement is met |
NEUTRAL | 0 | no policy, and nothing broken |
Staleness fails even with no policy at all, because it is an integrity property rather than a rule you configure. See Watch a review go stale.
Standing
A vouch recorded before a policy existed does not retroactively satisfy one:
note: this repo has no .vidi/policy.toml: this vouch is recorded without a
standing and will never satisfy a rank requirement. To count toward one: add a
policy with a [reviewers] entry, then vouch again.
The same applies to anyone missing from [reviewers]. Their reviews are
recorded and visible, but carry no rank, so they cannot clear a requirement.
Why start narrow
Turning on a repo-wide gate against an unreviewed codebase means every build fails until the whole backlog is cleared, and the usual outcome is that somebody disables the check.
Gate one directory that genuinely matters, clear it, then widen. Everything
outside your scopes stays visible in vidi status and vidi queue the whole
time — you lose no information by gating gradually.
Block merges in CI
vidi verify exits non-zero when the gate fails. That exit code is the
enforcement. CI needs nothing else from vidi.
The job
# .github/workflows/vidi.yml
name: Review coverage
on:
pull_request:
push:
branches: [main]
permissions:
contents: read
jobs:
verify:
runs-on: ubuntu-latest
timeout-minutes: 20
steps:
- uses: actions/checkout@v7
with:
# The gate reads .vidi/*.jsonl out of the checkout. A shallow clone is
# fine, the ledger is committed files, not git history.
fetch-depth: 1
- name: Install Rust (stable)
uses: actions-rust-lang/setup-rust-toolchain@v1
with:
toolchain: stable
- name: Install vidi
run: cargo install --git https://github.com/graze-ai/vidi vidi-cli --locked
- name: Verify review coverage
run: vidi verify
Nothing here needs a token, a login, or a network call. verify is offline and
read-only: it scans the working tree, reads the committed ledger, and compares.
Getting the binary
vidi-cli is not published to crates.io and there are no prebuilt release
binaries yet, so CI has to build it from source. Which source depends on where
the workflow lives:
| Where the workflow runs | Install step |
|---|---|
| Another repository, gating its own code | cargo install --git https://github.com/graze-ai/vidi vidi-cli --locked |
| This repository, gating itself | cargo install --path crates/vidi-cli --locked |
vidi-cli is the package name; the binary it installs is vidi. Both forms
put it on PATH, which a bare cargo build would not.
Pin the version you gate on — --git … --tag v0.1.0 — rather than tracking the
default branch, or a change upstream can turn a green build red without a commit
of yours. --locked uses the lockfile committed to vidi rather than resolving
fresh dependencies, so the build is reproducible.
Expect this step to dominate the run: it compiles the tree-sitter grammars from
C, which takes minutes. actions-rust-lang/setup-rust-toolchain caches the
cargo registry and build artifacts between runs, so only the first one pays full
price.
Exit codes
| Exit | Verdict | Meaning |
|---|---|---|
0 | PASS | a policy exists and every requirement is met |
0 | NEUTRAL | no .vidi/policy.toml, and nothing broken |
1 | FAILURE | something is stale, orphaned, or unmet — or a ledger line would not decode |
2 | — | the command refused: a broken policy, bad arguments, a failed scan |
141 | — | stdout closed early (128+SIGPIPE, e.g. vidi queue | head) |
Only 1 is a gate failure. A 2 means vidi never got as far as forming a
verdict, so nothing was checked. Do not read it as a review backlog.
NEUTRAL passes: an unconfigured repository does not fail its own builds. See
Turn on the gate.
Use verify, not status
vidi status prints the same verdict word and always exits 0. A CI step
running vidi status prints FAILURE on every build and passes anyway.
Explaining a failure in the log
verify names the failing units but not why they fail. Running vidi explain
on failure turns the log into something a contributor can act on without
reproducing anything locally:
- name: Verify review coverage
run: vidi verify
- name: Explain the failure
if: failure()
run: vidi explain
explain is read-only, and renders each failure as category → what failed → why
→ what to do.
Annotating the diff
vidi report --sarif emits SARIF 2.1.0, which GitHub renders as inline
annotations on the changed lines:
- name: Review-coverage annotations
if: always()
run: vidi report --sarif > vidi.sarif
- uses: github/codeql-action/upload-sarif@v3
if: always()
with:
sarif_file: vidi.sarif
Use if: always() on both steps, so the annotations still appear when verify
failed, which is exactly when they are useful.
Three rules can appear: vidi/requirement-unmet, vidi/stale, and
vidi/orphan. A passing repository emits a valid document with an empty
results array, which is the healthy case, not a broken one:
$ vidi report --sarif
{"$schema":"https://json.schemastore.org/sarif-2.1.0.json","runs":[{"results":[],"tool":{"driver":{"informationUri":"https://vidivouch.com","name":"vidi","version":"0.1.2"}}}],"version":"2.1.0"}
There is no vidi/rejected-ledger-line result. A partially-decoded ledger
refuses before any report is assembled, so it exits 1 with no SARIF artifact at
all. The gate still fails closed; it just fails earlier and without
annotations.
Machine-readable results
For a custom check-run or a dashboard, vidi verify --json emits the gate
object directly:
$ vidi verify --json
{ "vidi": "0.1.2", "schema": "vidi.gate/v1", "gate": { "checkRun": "failure", "exit": 1, "policyPresent": true, "stale": 1, "orphan": 0, "requirementFailures": 0, "defaultShortfalls": 0, "rejectedLedgerLines": 0, "reviewed": 0, "total": 5456 } }
checkRun is success / failure / neutral — the GitHub check-run
vocabulary, so it can be forwarded to the Checks API unchanged.
This is a different schema from vidi json (the full report,
vidi.json/v1.0). The gate object carries its own schema id precisely so a
consumer never validates one against the other’s schema.
Check the exit code before reading the stream. A ledger line that will not
decode refuses before any report is assembled, so nothing is emitted: --json
exits 1 with empty stdout and the rejected lines on stderr, and --sarif
produces no artifact. The gate still fails closed, just earlier and without
output.
Branch protection
Make the verify job a required status check in your branch-protection rules.
Because an unconfigured repo returns NEUTRAL and exit 0, you can require the
check before writing a policy and it will simply pass — then start gating by
adding .vidi/policy.toml, with no CI change.
Deleting or corrupting the ledger does not quietly disable the gate, every ambiguity resolves toward failure. See Fail-closed by default.
Other CI systems
Nothing above is GitHub-specific except the SARIF upload. The whole gate is one command:
vidi verify
vidi push reads $GITHUB_REPOSITORY, $GITHUB_SHA, $GITHUB_REF_NAME and
$CI_COMMIT_REF_NAME when they are set, and falls back to the git remote and
HEAD otherwise, so the hosted lane works anywhere too. See Share reviews across
a team.
Next
Review a pull request
Your first vouch walks one unit end to end. This is the repeat loop: find the work, read it, record a verdict, and confirm the gate agrees.
The loop
$ git diff --name-only main...HEAD # what the PR touched
$ vidi explain <file> # what vidi wants there
$ vidi vouch "<unit address>" # record your verdict
$ vidi verify # confirm the gate agrees
Counts in the examples below move as the repo changes, including when someone edits documentation, so read them as shapes, not values to match.
What scopes to a change, and what doesn’t
Vidi’s readers are repo-wide. queue ranks every unit in the repository and
verify gates all of them; neither takes a branch, a commit range, or a diff.
Two of the four pieces scope to a change:
| Command | Scopes to a change? |
|---|---|
vidi explain <path> | yes — one file or one unit |
vidi vouch --commit <ref> | yes — a whole commit by git ref |
vidi queue | no — ranks the whole repo |
vidi verify | no — gates the whole repo |
The gap is on the finding side: there is no vidi queue --since main. Bridge it
with git, as the next section does.
Finding what the PR touched
Ask git for the changed files, then ask vidi about each one:
$ git diff --name-only main...HEAD
crates/vidi-core/src/crypto/keyid.rs
crates/vidi-core/src/policy/schema.rs
$ vidi explain crates/vidi-core/src/crypto/keyid.rs
explain: gate FAILS · stale 0 · orphans 0 · requirement failures 174 · default shortfalls 0 · rejected ledger lines 0 · crates/vidi-core/src/crypto/keyid.rs
gating requirement-unmet crates/vidi-core/src/crypto/keyid.rs::impl:KeyId::fn:parse_hex
what: the unit is fresh but falls short of an explicit `[[cover]]`/`[[scope]]` review requirement (e.g. a required reviewer standing or a second axis). This fails the gate.
why:
- The matching policy row demands a stronger review than is present (a higher reviewer rank, or a second review axis).
fix:
→ Add the missing review(s) at the required standing; `vidi report` lists the exact gap for the unit.
→ If the requirement is wrong, adjust the `[[cover]]`/`[[scope]]` block in `.vidi/policy.toml`.
Scoping explain to a path is what keeps this readable. Unscoped, it narrates
every failing unit in the repository.
Read requirement-unmet carefully. The wording says “the unit is fresh but
falls short”. It says that whether the unit carries a review that is not
strong enough or no review at all. If you are hunting a rank problem that does
not seem to exist, check whether anything has reviewed the unit first:
vidi show <unit address> lists every statement against it, and prints nothing
when there are none.
Working the queue
When you are not reviewing a specific PR, take the queue from the top. It shows the top 20 by default:
$ vidi queue
re-review queue: top 20 of 5752 below the bar · gating first, then significance × severity
1. gating · sig 100 · unreviewed crates/vidi-core/src/crypto/assurance.rs::enum:AssuranceScalar
2. gating · sig 100 · unreviewed crates/vidi-core/src/crypto/assurance.rs::struct:Assurance
3. gating · sig 100 · unreviewed crates/vidi-core/src/crypto/entrypoint.rs::enum:SignRefusal
…
20. gating · sig 96 · unreviewed crates/vidi-core/src/crypto/entrypoint.rs::const:AGENT_ENV_MARKERS
… 5732 more · `vidi queue --all` shows everything, `--top N` widens the cut
The ordering is two-tier, and the first column names the tier. gating means a
[[cover]] or [[scope]] requires review there, those fail the build and sort
first. advisory is reported but not gating. sig orders within each tier and
never affects whether a unit passes; see
Unit states.
So the top of the queue is what is breaking the gate, most significant first. Everything below the gating block is backlog.
Reading the unit
vidi show does not print source code. It prints the ledger statements recorded
against a unit. The review history, not the thing under review:
$ vidi show "crates/vidi-core/src/crypto/keyid.rs::impl:KeyId::fn:of_public_key"
content_id 8384bb1382675eb3dd0b7d3e9d858c5f
subject crates/vidi-core/src/crypto/keyid.rs::impl:KeyId::fn:of_public_key
kind review · by you@example.com · at 2026-08-06T19:59:01Z
add --json for the exact bare-statement bytes.
That is worth running before you review as it answers “has anyone looked at this already, and who?” However, it is not the code. To read the code, open the file in your editor. No vidi command displays a unit’s source.
The machine report knows the exact lines if you want them:
$ vidi json | python -c "import sys,json; [print(u['file'], u['lineStart'], u['lineEnd']) for u in json.load(sys.stdin)['units'] if u['address'].endswith('fn:of_public_key')]"
crates/vidi-core/src/crypto/keyid.rs 30 32
Recording a verdict
For a straightforward approval, vouch:
$ vidi vouch "crates/vidi-core/src/crypto/keyid.rs::impl:KeyId::fn:of_public_key"
vouched crates/vidi-core/src/crypto/keyid.rs::impl:KeyId::fn:of_public_key as you@example.com (maintainer)
content_id 8384bb1382675eb3dd0b7d3e9d858c5f · appended to .vidi/ (bare line, diff-visible)
The (maintainer) is your standing at this moment, read from [reviewers] and
stamped into the record as roleAtReview. It is not decoration: a later
promotion will not retroactively strengthen this vouch, and a later demotion
weakens it immediately. See the
policy.toml reference.
If no role appears, your identity is not in [reviewers] — read the last
section of this page before going further.
Rigor
--rigor records how hard you looked:
$ vidi vouch --rigor ran-tests "<unit>"
| Rigor | Meaning |
|---|---|
skimmed | read quickly |
read-fully | read the whole unit (the default) |
ran-tests | read it and exercised it |
Rejecting, and other verdicts
vouch is approval only. For a verdict you have to think about, including
rejection, use the interactive form which prompts for rigor and verdict:
$ vidi review "crates/vidi-core/src/crypto/keyid.rs::impl:KeyId::fn:parse_hex"
A rejection is a real, recorded position: someone read exactly these bytes and
declined them. It does not gate on its own, but it is visible in show, and it
is not the same as silence.
Approving a whole commit
$ vidi vouch --commit <ref>
This vouches every unit the commit touches. Convenient for a small, focused PR. Consider what you are claiming on a large one. It records the same rigor claim against every unit in the change.
Confirming
$ vidi status
FAILURE: 2/5752 reviewed · stale 0 · orphans 0 · requirement failures 173 · default shortfalls 0
requirement failures dropped by one, from the 174 the explain above reported:
that unit’s obligation is now satisfied.
vidi verify is the command that actually gates, and it exits non-zero while
any requirement is unmet.
Your reviews are now lines in .vidi/reviews.jsonl. Commit them with the code,
they are meant to travel with the branch, and they cannot cause a merge conflict
(vidi init sets merge=union, so git keeps every line from both sides). See
Why review files never conflict.
When your reviews do not count
Your [reviewers] key is compared as a literal string against the identity
stored in each review. If they differ by even one character, every vouch you
make is silently ignored, and nothing warns you.
This is easy to hit because the two logins write different identities:
vidi login --local writes your git email, the hosted login writes a
vidi:p:<guid>. Key the policy one way, write the ledger the other, and
coverage you know you recorded reads as missing.
Those reviews are not lost, they land in an unverified bucket that only the
machine report mentions. If vidi json shows a non-zero summary.unverified,
this is what happened. The by … line in vidi show <unit address> prints the
string your [reviewers] key has to match.
Next
Revoke an approval
Sometimes you approve something and later decide you shouldn’t have.
vidi revoke withdraws a review.
Revoking
Pass either the unit address or the content_id of the review:
$ vidi revoke "crates/vidi-core/src/digest.rs::struct:Blake3Digest"
revoked 28d7043ed80ea10df8a78cd4b6f32f3f as you@example.com
content_id c6045ac7293ac85f4e5aef54b6d0fbb0 · appended to .vidi/ (bare line, diff-visible)
Two content_ids appear, and they are different things:
| Value | What it is |
|---|---|
28d7043e… | the review being withdrawn |
c6045ac7… | the revocation record itself |
Coverage drops immediately:
$ vidi status
NEUTRAL: 0/5356 reviewed · stale 0 · orphans 0 · requirement failures 0 · default shortfalls 0
Nothing is deleted
The original approval is still on disk. Revoking adds a record; it never edits or removes one:
$ wc -l .vidi/reviews.jsonl .vidi/revocations.jsonl
1 .vidi/reviews.jsonl
1 .vidi/revocations.jsonl
The vouch is still in reviews.jsonl. The revocation sits in
revocations.jsonl, and when the two are resolved the revocation wins, so the
unit reads as unreviewed.
The revocation is a much smaller record than a review:
{
"subject": [{ "name": "revoke" }],
"predicateType": "https://vidivouch.com/vidi/revoke/v1",
"predicate": {
"reason": "...",
"attesterIdentity": { "kind": "person", "id": "you@example.com" },
"claimTimestamp": "..."
}
}
Note the subject name is the reserved word revoke rather than a unit address —
the record it cancels is identified by content_id, not by location.
Why append instead of edit
Three reasons, all of which matter for a record you want to trust:
History survives. “This was approved on Tuesday and withdrawn on Thursday” is a fact worth keeping. Deleting the approval would erase it.
Merges cannot conflict. Both files are append-only lists where order does
not matter, so merging two branches means keeping every line from both. That is
what the merge=union rule added by vidi init does. If revoking edited a
line, two people revoking on different branches would collide.
The result cannot depend on merge order. A revocation beats an approval no matter which arrived first, so everyone who merges the same set of records computes the same answer. See Why review files never conflict.
Re-approving
Nothing stops you vouching again after a revocation. It works for a subtler reason than “the newer record wins”.
A revocation targets one specific content_id, and it dominates that record
forever — arrival order is irrelevant, and no later record can un-revoke it.
But vouching again produces a review with a different content_id, which
the revocation simply does not name. The old record stays dead; the new one is
live.
This is why revoke-wins can be permanent without ever locking a unit out of review: revocation kills a claim, not an address.
No notary needed
Revocation never requires the hosted notary, even for repos that use signed reviews. Withdrawing a claim you made is always available offline.
Share reviews across a team
A review is a line in a file in your repository. So the question “how does my teammate see my review?” has the same answer as “how does my teammate see my code?” You commit it and they pull.
There is a second, optional path through the hosted portal. It is worth understanding what it does and does not do, because the names mislead.
The default: commit .vidi/
vidi init creates the ledger files and tells you to commit them:
$ vidi init
initialized: this repo can now track reviews in .vidi/
created .vidi/: commit these files so reviews travel with the repo
updated .gitattributes + .gitignore: combine review records across branches; keep the scan cache out of git
stage git add .vidi/ .gitattributes .gitignore
That is the entire sharing mechanism. Vouch, commit, push:
$ vidi vouch "crates/vidi-core/src/crypto/keyid.rs::impl:KeyId::fn:of_public_key"
vouched crates/vidi-core/src/crypto/keyid.rs::impl:KeyId::fn:of_public_key as you@example.com (maintainer)
content_id 8384bb1382675eb3dd0b7d3e9d858c5f · appended to .vidi/ (bare line, diff-visible)
$ git add .vidi/reviews.jsonl && git commit -m "review: vouch KeyId::of_public_key"
Your teammate pulls the branch and runs vidi status. Your review is theirs
now, and it counts toward coverage on their machine exactly as it did on yours.
No account, no server, no token. This is the free tier, and it is the recommended path.
Why two people’s reviews never collide
Review files are append-only JSONL — one claim per line, and nothing is ever
rewritten in place. vidi init adds one line to .gitattributes:
.vidi/*.jsonl merge=union
That tells git to resolve these files by keeping every line from both sides rather than asking you to pick. Two people reviewing different units on different branches produce two sets of lines, and the merge is their union.
Order does not matter either, because the merge algebra is monotone: union,
dedupe by content_id, and revoke-wins. Everyone who merges the same set of
records computes the same coverage, whatever sequence the merges happened in.
Commit that .gitattributes line along with .vidi/, the guarantee only holds
if everyone has the rule. See
Why review files never conflict.
Reviews travel with the branch
Because they are ordinary tracked files, review records follow the same rules as code:
- they arrive with
git pulland move withgit push - they are visible in a pull request diff, one added line per review
- a branch that is behind on reviews is behind in the ordinary git sense
- rebasing replays review commits like any other commit
If your team rebases rather than merges, set it once per repo:
$ git config pull.rebase true
Nothing about the ledger objects to being rebased. The merge=union rule exists
for the case where two histories genuinely diverge, which rebasing avoids by
construction.
What the hosted lane actually is
The paid portal adds two commands. They sound like a matched pair and are not:
vidi pull | vidi push | |
|---|---|---|
| Direction | portal → your machine | your machine → portal |
| What moves | review and revocation records | a coverage snapshot |
| Effect | union-merges into your local .vidi/ | stored server-side for re-rendering |
| Gates anything? | the records it adds do | never |
vidi push does not share your reviews. It uploads an already-computed
coverage snapshot, the numbers behind vidi json, so the portal can render a
dashboard. The server stores it verbatim and it never gates anything. The half
of push that would upload an authoritative review store is not built.
Pushing reviews outward stays git’s job. The portal is a read-and-display surface over a git-native ledger, plus the hydrate path below.
Logging in
Two logins, and they are for different things:
$ vidi login --local # offline identity, no account, free tier
$ vidi login # vidivouch.com device flow, the paid lane
--local records a self-asserted identity enough to authorize writes, which
is all the free tier needs. The hosted form runs a device flow and stores a
revocable bearer token, never a signing key, at ~/.vidi/token.
The two record different identity strings, which is the one thing here that will silently cost a team coverage:
| Login | attesterIdentity.id recorded |
|---|---|
vidi login --local | your git email, e.g. you@example.com |
vidi login | a person GUID, vidi:p:<uuid> |
Your policy’s [reviewers] keys are matched literally against that string. If
your team switches from local to hosted logins, list both forms for each
person, or the reviews made under the other one stop counting silently. See
policy.toml.
What vidi pull fetches
vidi pull fetches the tenant’s stored review and revocation lines and splits them
by kind. Revocations into .vidi/revocations.jsonl, everything else into
.vidi/reviews.jsonl, then union-merges each into the local file.
$ vidi pull
pulled github.com/graze-ai/vidi → 0 review + 0 revocation line(s) fetched · 0 new review + 0 new revocation merged · 0 already present
The three counts are worth reading separately: what the server served, what was new to you, and what you already had. A healthy repeat pull shows lines fetched and nothing new, which means you are in sync rather than that nothing happened.
The pull above returned nothing on a repository whose local ledger holds several
reviews, which is the push/pull asymmetry made concrete. Those reviews live in
git and have never been in the portal’s review store, because no command
uploads them there. vidi push uploads coverage, not reviews.
What comes back depends on which bearer you hold:
| Token | Repo selector | Scope of the result |
|---|---|---|
person (vida_ / vidc_, from vidi login) | required — sent as ?repo=host/owner/name | that repository |
| tenant API token | none; the bearer is already tenant-scoped | the whole tenant |
A person token can belong to several tenants, which is why it must name the repo; a tenant token already implies one, so a tenant-wide pull works even where no git remote resolves.
The merge is the same algebra as the git path, plus two rules: a review that
arrives notary-signed where you held only a self-asserted copy is reported as an
upgrade, and an undecodable remote line is refused rather than appended. A
refusal means something is wrong in transport, so pull exits non-zero if any
line was refused, even though the lines that did decode merged normally.
Configuring the remote
Both commands resolve a URL and a bearer, each by its own ladder:
| Order | |
|---|---|
| URL | --url > $VIDI_REMOTE_URL > $VIDI_NOTARY_URL > the production host |
| Token | --token > $VIDI_REMOTE_TOKEN > the stored vidi login token |
URL resolution never fails, a missing URL falls back to production rather than
erroring. The repo slug resolves as --repo > $GITHUB_REPOSITORY > the
origin remote, taking the host from $GITHUB_SERVER_URL so GitHub Enterprise
works without assuming github.com.
$VIDI_REMOTE_TOKEN is the CI path: it passes through untouched with no refresh
half, so a build authenticates without an interactive login. Keep it in your CI
secret store, not in the repository.
Pointing at a self-hosted portal, vidi will not send a bearer over plain
HTTP to a non-loopback host: use https://, or loopback for local testing.
Which path to use
Commit .vidi/ regardless, it is the source of truth and it costs nothing.
Add the portal when you want coverage dashboards across repos, or when you want fresh clones and CI to hydrate reviews without a full checkout of every branch that produced them.
Next
Editor extensions
Install
Both packages are attached to each GitHub release. Download them from the releases page. Neither extension is on a marketplace yet.
VS Code (requires VS Code 1.90 or newer):
$ code --install-extension vidi-vscode-0.1.1.vsix
On WSL, run that from your WSL shell.
JetBrains — Settings → Plugins → gear icon → Install Plugin from Disk,
then select vidi-jetbrains-0.1.1.zip. Loads in IDEA, PyCharm and the other
IntelliJ-platform IDEs; it depends only on com.intellij.modules.platform.
To build from source instead, see Install.
Requirements
vidi must be on your PATH.
VS Code resolves it through a ladder: the vidi.binaryPath setting, then
$VIDI_BINARY, then ~/.cargo, ~/.local, /opt/homebrew and /usr/local,
then PATH. That last-resort search exists because an editor launched from the
Dock or a launcher often inherits a minimal PATH that omits ~/.cargo/bin.
If coverage is blank and the binary works in a terminal, that is the first thing to check.
Two surprises
Both are places the extension refuses to be convenient.
Revoke behaves differently once your repo is on the hosted lane. In a
plain repo it runs vidi revoke, which appends a revocation line. That
revocation wins from then on. In a hosted-bound repo the extension revokes
nothing locally; it opens the portal in your browser and you finish there.
Revoking only ever removes coverage, so nobody does it to sneak code through. The risk is the opposite: a hijacked session or a runaway agent revoking in bulk, and turning a green repo red. Making a human prove they are present, to a server, is the one check an automated caller cannot satisfy, so for a shared ledger that step is required, and the editor cannot skip it on your behalf.
The before-commit prompt pre-selects nothing, and in VS Code it is off by
default (vidi.commitTimePrompt).
Ticking every box by default would make the fastest path through the dialog
“press OK.” Recording approved · read-fully against code you never opened.
That record is indistinguishable from a real review. Staging a file means you
want it in the commit; it is not evidence you read it, and the two coinciding so
often is exactly what makes conflating them dangerous.
Keybindings
| Action | VS Code | JetBrains | JetBrains (macOS) |
|---|---|---|---|
| Vouch for the unit at the cursor | Ctrl+Alt+V | Ctrl+Alt+V | Ctrl+Shift+V |
| Review from here (call-graph walk) | Ctrl+Alt+W | context menu | context menu |
| Sync (hosted lane only) | Ctrl+Alt+S | Ctrl+Alt+Shift+U | Ctrl+Shift+S |
The JetBrains bindings differ from VS Code’s on purpose: Ctrl+Alt+S is the stock Settings shortcut in IntelliJ keymaps, and macOS treats Option+V as a dead key.
VS Code settings
| Setting | Default | What it does |
|---|---|---|
vidi.binaryPath | (empty) | Explicit path to the binary; empty uses the resolution ladder. |
vidi.showInlayHints | true | End-of-line provenance per unit. |
vidi.significance | true | Compute the significance lens (vidi json --significance). Advisory; never gates. |
vidi.commitTimePrompt | false | Offer a before-commit vouch prompt. |
CLI reference
Every command vidi accepts, generated from the binary itself.
Commands that only read are safe to run at any time. Commands that write
require vidi init and a login.
Commands at a glance
| Command | What it does |
|---|---|
scan | Walk the repo into addressable units, content-hash each, and harvest references, one deterministic tree descent |
report | Coverage report: what is covered, the obligations, and the re-review queue ordered by significance x severity |
status | One-line gate status; add –porcelain for a stable scriptable form |
queue | The re-review queue: gating obligations first, then significance x churn severity |
verify | The gate: resolve every obligation against the ledger, emit the three-state check-run and the exit code |
explain | Explain WHY the gate reads the way it does: render every failure (stale, orphan, unreviewed, migration-needed, requirement shortfalls, and rejected ledger lines) into plain-English category -> what-failed -> why -> what-to-do diagnostics |
verify-vouches | Verify the SIGNATURES on the committed vouches against the notary key: every signed DSSE envelope in .vidi/reviews.jsonl must verify under the notary’s Ed25519 key (cached at .vidi/notary.pub, so this is offline once the key is known); an unsigned vouch verifies as self-asserted (the free baseline, never a failure) |
push | Push the coverage artifact to the hosted portal (POST /api/v1/ingest), the paid lane: a resolvable remote (URL + bearer) is required |
pull | Pull the tenant’s reviews and revocations from the hosted portal and union-merge them into the local ledgers (GET /api/v1/reviews) |
churn | Advisory churn for one unit: state and severity band (never gates) |
significance | Advisory significance ranking: orders within each re-review queue stratum; never gates |
graph | Bounded call-graph neighborhood around a unit as JSON: {from, callees, callers} (advisory; depth <= 2) |
show | Show the exact bare statement(s) for a content_id or unit address; add –json for the machine shape |
json | Machine report of every unit; add –significance for the within-repo percentile |
init | Initialize vidi in this repo: create the in-repo .vidi/ files |
review | Review one unit interactively (rigor + verdict), appending to the in-repo ledger |
vouch | One-shot approved review of a unit, or a whole commit with –commit |
revoke | Revoke a review by content_id or unit address (revoke-wins; never needs the notary) |
sign | Sign a review through the notary, a logged-in human; refused for automated agents |
whoami | Resolve who you are: person-bound, before any tenant |
attribute | Record model authorship (advisory; never gates): import git-ai’s refs/notes/ai attestation for HEAD into .vidi/authorship.jsonl, AI lane only, fail-toward-unknown |
hooks | Manage the post-commit hook that runs vidi attribute after each commit, the routine authorship-capture cadence |
history | Forensics over git history: when could AI coding tools first have touched this repo? vidi history audit sweeps every ref for AI-tool commit signatures and config-file first-adds and derives the worst-case AI-era start date |
login | Log in so writes are authorized, machine-wide, like git config --global |
logout | Log out on this machine: best-effort revoke the hosted refresh token, then always clear the locally stored hosted and offline credentials |
vidi scan
vidi scan [OPTIONS]
Walk the repo into addressable units, content-hash each, and harvest references, one deterministic tree descent. Read-only; never writes .vidi/
Usage: vidi scan [OPTIONS]
Options:
--color <COLOR>
When to colourise output: `auto` (a terminal, honouring NO_COLOR), `always`, or `never` (byte-identical to the plain human-output contract). Machine surfaces (--json / --porcelain / SARIF) are never coloured regardless
Possible values:
- auto: Colourise when stdout is a terminal
- always: Always colourise
- never: Never colourise (the scriptable form)
[default: auto]
-h, --help
Print help (see a summary with '-h')
vidi report
vidi report [OPTIONS]
Coverage report: what is covered, the obligations, and the re-review queue ordered by significance x severity
Usage: vidi report [OPTIONS]
Options:
--sarif
Emit SARIF 2.1.0 (files-changed annotations for CI) instead of the human report
--color <COLOR>
When to colourise output: `auto` (a terminal, honouring NO_COLOR), `always`, or `never` (byte-identical to the plain human-output contract). Machine surfaces (--json / --porcelain / SARIF) are never coloured regardless
Possible values:
- auto: Colourise when stdout is a terminal
- always: Always colourise
- never: Never colourise (the scriptable form)
[default: auto]
-h, --help
Print help (see a summary with '-h')
vidi status
vidi status [OPTIONS]
One-line gate status; add --porcelain for a stable scriptable form
Usage: vidi status [OPTIONS]
Options:
--porcelain
Stable, line-oriented output for scripts
--color <COLOR>
When to colourise output: `auto` (a terminal, honouring NO_COLOR), `always`, or `never` (byte-identical to the plain human-output contract). Machine surfaces (--json / --porcelain / SARIF) are never coloured regardless
Possible values:
- auto: Colourise when stdout is a terminal
- always: Always colourise
- never: Never colourise (the scriptable form)
[default: auto]
-h, --help
Print help (see a summary with '-h')
vidi queue
vidi queue [OPTIONS]
The re-review queue: gating obligations first, then significance x churn severity
Usage: vidi queue [OPTIONS]
Options:
--top <N>
Show only the top N entries (default: 20)
--all
Show every entry instead of the top 20
--color <COLOR>
When to colourise output: `auto` (a terminal, honouring NO_COLOR), `always`, or `never` (byte-identical to the plain human-output contract). Machine surfaces (--json / --porcelain / SARIF) are never coloured regardless
Possible values:
- auto: Colourise when stdout is a terminal
- always: Always colourise
- never: Never colourise (the scriptable form)
[default: auto]
-h, --help
Print help (see a summary with '-h')
vidi verify
vidi verify [OPTIONS]
The gate: resolve every obligation against the ledger, emit the three-state check-run and the exit code. Fails closed on any ambiguity
Usage: vidi verify [OPTIONS]
Options:
--json
Emit the machine gate object (vidi.gate/v1) instead of the human gate output. The full report is `vidi json` (vidi.json/v1.0); the gate object has its own schema id so consumers never validate it against vidi-json-v1.schema.json
--color <COLOR>
When to colourise output: `auto` (a terminal, honouring NO_COLOR), `always`, or `never` (byte-identical to the plain human-output contract). Machine surfaces (--json / --porcelain / SARIF) are never coloured regardless
Possible values:
- auto: Colourise when stdout is a terminal
- always: Always colourise
- never: Never colourise (the scriptable form)
[default: auto]
-h, --help
Print help (see a summary with '-h')
vidi explain
vidi explain [OPTIONS] [PATH]
Explain WHY the gate reads the way it does: render every failure (stale, orphan, unreviewed, migration-needed, requirement shortfalls, and rejected ledger lines) into plain-English category -> what-failed -> why -> what-to-do diagnostics. Read-only; runs the same reader/gate path as `verify` and never writes .vidi/. Add --json for the structured form; pass a path to scope the explanation to one file or unit
Usage: vidi explain [OPTIONS] [PATH]
Arguments:
[PATH]
Scope the explanation to one file or unit address (prefix match on a segment boundary, e.g. `src/auth.rs` or `src/auth.rs::fn:verify`). Rejected ledger lines are repo-wide and always shown
Options:
--json
Emit the machine object (vidi.explain/v1) instead of the human diagnostics
--color <COLOR>
When to colourise output: `auto` (a terminal, honouring NO_COLOR), `always`, or `never` (byte-identical to the plain human-output contract). Machine surfaces (--json / --porcelain / SARIF) are never coloured regardless
Possible values:
- auto: Colourise when stdout is a terminal
- always: Always colourise
- never: Never colourise (the scriptable form)
[default: auto]
-h, --help
Print help (see a summary with '-h')
vidi verify-vouches
vidi verify-vouches [OPTIONS]
Verify the SIGNATURES on the committed vouches against the notary key: every signed DSSE envelope in `.vidi/reviews.jsonl` must verify under the notary's Ed25519 key (cached at `.vidi/notary.pub`, so this is offline once the key is known); an unsigned vouch verifies as self-asserted (the free baseline, never a failure). Exits non-zero if any signed line fails to verify or any line is undecodable (fail-closed). Distinct from `verify`, the orphan/stale/scope coverage gate
Usage: vidi verify-vouches [OPTIONS]
Options:
--url <URL>
The notary base URL (else `$VIDI_REMOTE_URL`). Optional: with a cached `.vidi/notary.pub`, verification is fully offline and needs neither this nor a token
--token <TOKEN>
The notary bearer token (else `$VIDI_REMOTE_TOKEN`). Only consulted for an online key fetch when no key is cached; unused on the offline path
--color <COLOR>
When to colourise output: `auto` (a terminal, honouring NO_COLOR), `always`, or `never` (byte-identical to the plain human-output contract). Machine surfaces (--json / --porcelain / SARIF) are never coloured regardless
Possible values:
- auto: Colourise when stdout is a terminal
- always: Always colourise
- never: Never colourise (the scriptable form)
[default: auto]
-h, --help
Print help (see a summary with '-h')
vidi push
vidi push [OPTIONS]
Push the coverage artifact to the hosted portal (`POST /api/v1/ingest`), the paid lane: a resolvable remote (URL + bearer) is required. Read/transport verb; stays open to agents (CI is its home; see the push-gate note in push.rs)
Usage: vidi push [OPTIONS]
Options:
--repo <REPO>
Host-qualified repo slug, e.g. `github.com/vidivouch/vid`. Defaults to `$GITHUB_REPOSITORY`, else the repo's `origin` remote
--commit <COMMIT>
The commit the snapshot is for (default: `$GITHUB_SHA`, else `git rev-parse HEAD`)
--branch <BRANCH>
The branch the snapshot is for (the portal's grouping dimension). Defaults to `$GITHUB_REF_NAME` / `$CI_COMMIT_REF_NAME`, else the current HEAD branch, else its upstream; a detached checkout lands the snapshot unbranched
--file <FILE>
Push a pre-generated `vidi.json` instead of recomputing (`-` reads stdin)
--url <URL>
The remote base URL (else `$VIDI_REMOTE_URL`, else the production host)
--token <TOKEN>
The bearer token (else `$VIDI_REMOTE_TOKEN`, else the stored `vidi login` token)
--color <COLOR>
When to colourise output: `auto` (a terminal, honouring NO_COLOR), `always`, or `never` (byte-identical to the plain human-output contract). Machine surfaces (--json / --porcelain / SARIF) are never coloured regardless
Possible values:
- auto: Colourise when stdout is a terminal
- always: Always colourise
- never: Never colourise (the scriptable form)
[default: auto]
-h, --help
Print help (see a summary with '-h')
vidi pull
vidi pull [OPTIONS]
Pull the tenant's reviews and revocations from the hosted portal and union-merge them into the local ledgers (`GET /api/v1/reviews`). The paid hydrate lane: a resolvable remote is required. Read/hydrate verb; open to agents
Usage: vidi pull [OPTIONS]
Options:
--repo <REPO>
Host-qualified repo slug, e.g. `github.com/vidivouch/vid`. Defaults to `$GITHUB_REPOSITORY`, else the repo's `origin` remote (same ladder as `push`). Person logins send it to select the repo; tenant API tokens remain tenant-scoped
--url <URL>
The remote base URL (else `$VIDI_REMOTE_URL`, else the production host)
--token <TOKEN>
The bearer token (else `$VIDI_REMOTE_TOKEN`, else the stored `vidi login` token)
--color <COLOR>
When to colourise output: `auto` (a terminal, honouring NO_COLOR), `always`, or `never` (byte-identical to the plain human-output contract). Machine surfaces (--json / --porcelain / SARIF) are never coloured regardless
Possible values:
- auto: Colourise when stdout is a terminal
- always: Always colourise
- never: Never colourise (the scriptable form)
[default: auto]
-h, --help
Print help (see a summary with '-h')
vidi churn
vidi churn [OPTIONS] <UNIT>
Advisory churn for one unit: state and severity band (never gates)
Usage: vidi churn [OPTIONS] <UNIT>
Arguments:
<UNIT>
The unit address to describe
Options:
--color <COLOR>
When to colourise output: `auto` (a terminal, honouring NO_COLOR), `always`, or `never` (byte-identical to the plain human-output contract). Machine surfaces (--json / --porcelain / SARIF) are never coloured regardless
Possible values:
- auto: Colourise when stdout is a terminal
- always: Always colourise
- never: Never colourise (the scriptable form)
[default: auto]
-h, --help
Print help (see a summary with '-h')
vidi significance
vidi significance [OPTIONS]
Advisory significance ranking: orders within each re-review queue stratum; never gates
Usage: vidi significance [OPTIONS]
Options:
--top <N>
Show only the top N units
--color <COLOR>
When to colourise output: `auto` (a terminal, honouring NO_COLOR), `always`, or `never` (byte-identical to the plain human-output contract). Machine surfaces (--json / --porcelain / SARIF) are never coloured regardless
Possible values:
- auto: Colourise when stdout is a terminal
- always: Always colourise
- never: Never colourise (the scriptable form)
[default: auto]
-h, --help
Print help (see a summary with '-h')
vidi graph
vidi graph [OPTIONS] --from <UNIT>
Bounded call-graph neighborhood around a unit as JSON: {from, callees, callers} (advisory; depth <= 2). The editor plugins parse this shape
Usage: vidi graph [OPTIONS] --from <UNIT>
Options:
--from <UNIT>
The unit address to center the neighborhood on
--depth <N>
Neighborhood depth (bounded at 2)
[default: 2]
--color <COLOR>
When to colourise output: `auto` (a terminal, honouring NO_COLOR), `always`, or `never` (byte-identical to the plain human-output contract). Machine surfaces (--json / --porcelain / SARIF) are never coloured regardless
Possible values:
- auto: Colourise when stdout is a terminal
- always: Always colourise
- never: Never colourise (the scriptable form)
[default: auto]
-h, --help
Print help (see a summary with '-h')
vidi show
vidi show [OPTIONS] <CONTENT_ID|UNIT>
Show the exact bare statement(s) for a content_id or unit address; add --json for the machine shape
Usage: vidi show [OPTIONS] <CONTENT_ID|UNIT>
Arguments:
<CONTENT_ID|UNIT>
The statement(s) to show: a content_id (32 lowercase hex, printed when a vouch or review is recorded), or a unit address (`<file>::<qualpath>` from `vidi json`) showing every ledger statement for that unit
Options:
--json
Emit the exact bare-statement JSON (matches contracts/D2-provenance-core.json; one document per line when several statements match)
--color <COLOR>
When to colourise output: `auto` (a terminal, honouring NO_COLOR), `always`, or `never` (byte-identical to the plain human-output contract). Machine surfaces (--json / --porcelain / SARIF) are never coloured regardless
Possible values:
- auto: Colourise when stdout is a terminal
- always: Always colourise
- never: Never colourise (the scriptable form)
[default: auto]
-h, --help
Print help (see a summary with '-h')
vidi json
vidi json [OPTIONS]
Machine report of every unit; add --significance for the within-repo percentile
Usage: vidi json [OPTIONS]
Options:
--significance
Add one optional int per unit: the within-repo significance percentile (0-100)
--color <COLOR>
When to colourise output: `auto` (a terminal, honouring NO_COLOR), `always`, or `never` (byte-identical to the plain human-output contract). Machine surfaces (--json / --porcelain / SARIF) are never coloured regardless
Possible values:
- auto: Colourise when stdout is a terminal
- always: Always colourise
- never: Never colourise (the scriptable form)
[default: auto]
-h, --help
Print help (see a summary with '-h')
vidi init
vidi init [OPTIONS]
Initialize vidi in this repo: create the in-repo .vidi/ files. The visible opt-in act: vidi's state-writing verbs refuse a repo that has not run it
Usage: vidi init [OPTIONS]
Options:
--color <COLOR>
When to colourise output: `auto` (a terminal, honouring NO_COLOR), `always`, or `never` (byte-identical to the plain human-output contract). Machine surfaces (--json / --porcelain / SARIF) are never coloured regardless
Possible values:
- auto: Colourise when stdout is a terminal
- always: Always colourise
- never: Never colourise (the scriptable form)
[default: auto]
-h, --help
Print help (see a summary with '-h')
vidi review
vidi review [OPTIONS] <UNIT>
Review one unit interactively (rigor + verdict), appending to the in-repo ledger
Usage: vidi review [OPTIONS] <UNIT>
Arguments:
<UNIT>
The unit address, e.g. src/auth.rs::fn:verify_token
Options:
--color <COLOR>
When to colourise output: `auto` (a terminal, honouring NO_COLOR), `always`, or `never` (byte-identical to the plain human-output contract). Machine surfaces (--json / --porcelain / SARIF) are never coloured regardless
Possible values:
- auto: Colourise when stdout is a terminal
- always: Always colourise
- never: Never colourise (the scriptable form)
[default: auto]
-h, --help
Print help (see a summary with '-h')
vidi vouch
vidi vouch [OPTIONS] [UNIT]
One-shot approved review of a unit, or a whole commit with --commit
Usage: vidi vouch [OPTIONS] [UNIT]
Arguments:
[UNIT]
The unit address to vouch (omit when using --commit)
Options:
--rigor <RIGOR>
Review rigor: skimmed | read-fully | ran-tests
--commit <REF>
Vouch a whole commit by git ref instead of a unit
--color <COLOR>
When to colourise output: `auto` (a terminal, honouring NO_COLOR), `always`, or `never` (byte-identical to the plain human-output contract). Machine surfaces (--json / --porcelain / SARIF) are never coloured regardless
Possible values:
- auto: Colourise when stdout is a terminal
- always: Always colourise
- never: Never colourise (the scriptable form)
[default: auto]
-h, --help
Print help (see a summary with '-h')
vidi revoke
vidi revoke [OPTIONS] <CONTENT_ID|UNIT>
Revoke a review by content_id or unit address (revoke-wins; never needs the notary)
Usage: vidi revoke [OPTIONS] <CONTENT_ID|UNIT>
Arguments:
<CONTENT_ID|UNIT>
The review to revoke: a content_id (32 lowercase hex), or a unit address (`<file>::<qualpath>`) resolving to exactly one active review
Options:
--color <COLOR>
When to colourise output: `auto` (a terminal, honouring NO_COLOR), `always`, or `never` (byte-identical to the plain human-output contract). Machine surfaces (--json / --porcelain / SARIF) are never coloured regardless
Possible values:
- auto: Colourise when stdout is a terminal
- always: Always colourise
- never: Never colourise (the scriptable form)
[default: auto]
-h, --help
Print help (see a summary with '-h')
vidi sign
vidi sign [OPTIONS] <UNIT>
Sign a review through the notary, a logged-in human; refused for automated agents
Usage: vidi sign [OPTIONS] <UNIT>
Arguments:
<UNIT>
The unit address to sign
Options:
--color <COLOR>
When to colourise output: `auto` (a terminal, honouring NO_COLOR), `always`, or `never` (byte-identical to the plain human-output contract). Machine surfaces (--json / --porcelain / SARIF) are never coloured regardless
Possible values:
- auto: Colourise when stdout is a terminal
- always: Always colourise
- never: Never colourise (the scriptable form)
[default: auto]
-h, --help
Print help (see a summary with '-h')
vidi whoami
vidi whoami [OPTIONS]
Resolve who you are: person-bound, before any tenant
Usage: vidi whoami [OPTIONS]
Options:
--color <COLOR>
When to colourise output: `auto` (a terminal, honouring NO_COLOR), `always`, or `never` (byte-identical to the plain human-output contract). Machine surfaces (--json / --porcelain / SARIF) are never coloured regardless
Possible values:
- auto: Colourise when stdout is a terminal
- always: Always colourise
- never: Never colourise (the scriptable form)
[default: auto]
-h, --help
Print help (see a summary with '-h')
vidi attribute
vidi attribute [OPTIONS]
Record model authorship (advisory; never gates): import git-ai's refs/notes/ai attestation for HEAD into .vidi/authorship.jsonl, AI lane only, fail-toward-unknown
Usage: vidi attribute [OPTIONS]
Options:
--hook <HARNESS>
The agent harness that fired this invocation as a hook (e.g. post-tool-use). Optional context only: bare `vidi attribute` runs the same import
--color <COLOR>
When to colourise output: `auto` (a terminal, honouring NO_COLOR), `always`, or `never` (byte-identical to the plain human-output contract). Machine surfaces (--json / --porcelain / SARIF) are never coloured regardless
Possible values:
- auto: Colourise when stdout is a terminal
- always: Always colourise
- never: Never colourise (the scriptable form)
[default: auto]
-h, --help
Print help (see a summary with '-h')
vidi hooks
vidi hooks [OPTIONS] <COMMAND>
Manage the post-commit hook that runs `vidi attribute` after each commit, the routine authorship-capture cadence. The hook is advisory and fail-open: it runs in the background and can never block or fail a commit
Usage: vidi hooks [OPTIONS] <COMMAND>
Commands:
install Install the post-commit authorship hook. An existing post-commit hook is appended to, never replaced; installing twice is a no-op
status Show whether the hook is installed, whether other post-commit content coexists, and whether the hook is currently disabled
uninstall Remove vidi's hook block. Any other post-commit content is preserved byte-exact
help Print this message or the help of the given subcommand(s)
Options:
--color <COLOR>
When to colourise output: `auto` (a terminal, honouring NO_COLOR), `always`, or `never` (byte-identical to the plain human-output contract). Machine surfaces (--json / --porcelain / SARIF) are never coloured regardless
Possible values:
- auto: Colourise when stdout is a terminal
- always: Always colourise
- never: Never colourise (the scriptable form)
[default: auto]
-h, --help
Print help (see a summary with '-h')
vidi history
vidi history [OPTIONS] <COMMAND>
Forensics over git history: when could AI coding tools first have touched this repo? `vidi history audit` sweeps every ref for AI-tool commit signatures and config-file first-adds and derives the worst-case AI-era start date. Read-only, works before `vidi init` on any git repo; advisory only (never gates)
Usage: vidi history [OPTIONS] <COMMAND>
Commands:
audit Sweep every ref for AI-tool commit signatures and config-file first-adds, derive the worst-case AI-era start date, and report. Signature dates are earliest-evidence lower bounds; absence of signature proves nothing. Read-only: report + cache, nothing recorded
help Print this message or the help of the given subcommand(s)
Options:
--color <COLOR>
When to colourise output: `auto` (a terminal, honouring NO_COLOR), `always`, or `never` (byte-identical to the plain human-output contract). Machine surfaces (--json / --porcelain / SARIF) are never coloured regardless
Possible values:
- auto: Colourise when stdout is a terminal
- always: Always colourise
- never: Never colourise (the scriptable form)
[default: auto]
-h, --help
Print help (see a summary with '-h')
vidi login
vidi login [OPTIONS] [IDENTITY]
Log in so writes are authorized, machine-wide, like `git config --global`. Two ways: `vidi login` connects your vidivouch.com account (device flow; stores only a revocable bearer token, no signing key); `vidi login --local` records an offline self-asserted identity: enough for the free tier
Usage: vidi login [OPTIONS] [IDENTITY]
Arguments:
[IDENTITY]
The self-asserted identity to record with --local (e.g. your email or a handle). Defaults to `git config user.email` when omitted. Only valid with --local
Options:
--local
Establish a LOCAL self-asserted identity (offline; no hosted account, no network) instead of the device flow. The free-tier write login: writes require being logged in, and this is the login that needs no server. Identity assurance stays self-asserted; the paid lane is the notary presence ceremony, never this
--color <COLOR>
When to colourise output: `auto` (a terminal, honouring NO_COLOR), `always`, or `never` (byte-identical to the plain human-output contract). Machine surfaces (--json / --porcelain / SARIF) are never coloured regardless
Possible values:
- auto: Colourise when stdout is a terminal
- always: Always colourise
- never: Never colourise (the scriptable form)
[default: auto]
-h, --help
Print help (see a summary with '-h')
vidi logout
vidi logout [OPTIONS]
Log out on this machine: best-effort revoke the hosted refresh token, then always clear the locally stored hosted and offline credentials
Usage: vidi logout [OPTIONS]
Options:
--color <COLOR>
When to colourise output: `auto` (a terminal, honouring NO_COLOR), `always`, or `never` (byte-identical to the plain human-output contract). Machine surfaces (--json / --porcelain / SARIF) are never coloured regardless
Possible values:
- auto: Colourise when stdout is a terminal
- always: Always colourise
- never: Never colourise (the scriptable form)
[default: auto]
-h, --help
Print help (see a summary with '-h')
policy.toml reference
A repository opts into gating by placing exactly one file at
.vidi/policy.toml. No policy file means no gating at all: the verdict is
NEUTRAL and the gate exits 0, except for the two integrity failures that are
not policy questions (see Unit states).
Source of truth: crates/vidi-core/src/policy/.
Discovery
The path is fixed: .vidi/policy.toml, relative to the repository root.
Discovery does not consult home directories, environment variables, ancestor
folders, or legacy filenames. There is exactly one place a policy can live, so
no repository can be gated by a file you cannot see in its diff.
A .vidi/policy.toml that resolves outside the repository root (a symlink
escape) is refused rather than followed.
The file is a trust boundary
Two consequences worth internalising before the key tables:
Unknown keys are a hard error. Every table is deny_unknown_fields. A typo
in a security key fails the load; it is never parsed and ignored.
A policy that gates nothing will not load. A file with no [[cover]], no
[[scope]], and default = "advisory" is rejected as vacuous. Either declare
an obligation or delete the file; “present but inert” is not a state you can
reach by accident.
Load failures are refusals, not warnings, and each points at the offending bytes.
Top-level keys
| Key | Required | Type | Meaning |
|---|---|---|---|
schema | yes | "MAJOR.MINOR" | Schema version. This engine enforces "1.0". |
default | yes | "advisory" or accept-row list | Posture for paths matched by no gating [[scope]]. |
[axes] | no | table | The two ranked reviewer ladders. |
[reviewers] | no | table | The reviewer directory: identity → role. |
[[cover]] | no | array | Proactive coverage obligations. |
[[scope]] | no | array | Reactive path-scoped requirements. |
[severity] | no | table | Advisory thresholds. Never gates. |
[[exemptions]] | no | array | Audited exclusions from the unreviewed set. |
schema
Exactly two dotted integers. No patch component, no zero-fill, no partial parse:
"1", "1.2.3" and "1.x" are all rejected.
Version skew fails closed in three of four directions:
| File | Engine | Result |
|---|---|---|
1.0 | 1.0 | loads |
1.0 | 1.4 | loads; the file uses a subset of what the engine knows |
1.7 | 1.0 | refused; the file may carry a gating field this engine cannot enforce |
0.9 / 2.0 | 1.0 | refused; a major mismatch in either direction |
A newer minor is refused rather than best-effort honoured, because the failure mode of guessing is a build that passes while a rule goes unenforced.
default
Either the string "advisory" or a list of accept rows.
default = "advisory" # unmatched paths are report-only
default = [ { human = "reviewer" } ] # unmatched paths must meet this
default = "advisory" is the recommended starting posture: paths matched by no
gating [[scope]] are reported but never fail the build. A strict default gates
the entire repository, including files nobody has thought about yet.
A default = [] with zero rows is a load error, not “allow everything”.
[axes]: the ranked ladders
Two ladders, human and machine. Each is a weakest-first list; a name’s
index is its rank.
[axes]
human.order = ["contributor", "reviewer", "maintainer"]
machine.order = ["coderabbit", "cubic", "claude-sonnet-5", "claude-opus-5"]
The names are yours. Vidi attaches no meaning to "maintainer" beyond “index 2,
therefore outranks index 1”. A requirement of human = "reviewer" is satisfied
by anyone at that rung or above.
The two ladders never substitute for each other: a tool review supplies a machine rank and never pads the human axis, however capable the tool. This is lane discipline, and it is the point of having two ladders rather than one.
The machine rank resolves against the review’s model first, then its tool,
so you can rank a specific model or fall back to ranking the vendor.
aliases: renaming a rung
[axes]
human.order = ["contributor", "reviewer", "maintainer"]
human.aliases = { "owner" = "maintainer" }
An alias lets a renamed rung keep old ledger stamps and directory entries resolvable. Three rules:
- One hop, never chained. An alias pointing at another alias resolves nothing. You get a load-time warning, and the review supplies no rank.
- A real rung wins over a same-named alias.
- Only role names are alias-resolved. An accept-row minimum must name a
rung that actually exists in
order.
unranked is reserved on both ladders and as an alias name. It is the
record-layer sentinel for “vouched with no directory standing”, so it can never
double as a real rank.
[reviewers]: the directory
[reviewers]
"vidi:p:018f3c2e-9d41-7c3a-b1f2-4a5d6e7f8a9b" = { role = "maintainer", name = "Ada" }
| Field | Required | Meaning |
|---|---|---|
role | yes | Must name a rung on the human ladder. |
name | no | Display only. Never load-bearing for a gate. |
Only people listed here have a standing that can satisfy a rank
requirement. A review from an identity absent from [reviewers] supplies no
human rank at all. It is not an error; it simply does not count. That is
deliberate: departure fully weakens, with no ledger rewrite required.
The key must match the recorded identity exactly
This is the detail that most often makes a correct-looking policy gate nothing.
The key is matched as a literal string against the attesterIdentity.id
stored in the review record. Which string that is depends on how you logged in:
| Login | Recorded identity | [reviewers] key to use |
|---|---|---|
Hosted (vidi login) | a person GUID | "vidi:p:<guid>" |
Local (vidi login --local) | the identity you established, by default your git config user.email | that exact string, e.g. "ada@example.com" |
Check what your ledger actually recorded before writing the key: the
attesterIdentity.id field of any line in .vidi/reviews.jsonl is the string to
copy.
A mismatch is silent by design: an unknown identity is a stranger, and strangers supply no rank. If a vouch you know exists is not satisfying a requirement, this is the first thing to check.
roleAtReview: rank at vouch time
A review stamps the reviewer’s standing at the moment of the ceremony. The rank it supplies afterwards is the weaker of the stamped and the current standing:
| Stamp | Supplies |
|---|---|
| a role name | min(stamped, current) |
| absent (legacy line) | the current directory role |
unranked | no rank, ever |
| a name that resolves to no rung | no rank (fail-closed) |
So a promotion never retroactively upgrades old vouches, a demotion weakens them immediately, and a vouch made with no standing cannot be converted into coverage later by adding that person to the directory. They re-vouch at the new standing to count.
Accept rows
An accept row is a conjunction of minima, one per axis. Every field is optional individually, but a row with no minimum on any axis is a load error; it would pass with zero review.
{ human = "maintainer", machine = "claude-opus-5", assurance = "session-verified" }
| Axis | Compared against |
|---|---|
human | The reviewer’s human-ladder rank (see above). |
machine | The tool review’s machine-ladder rank. |
assurance | The grade a verified receipt proves. Not a policy ladder; the vocabulary is fixed. |
Rows in a list are OR’d; minima within a row are AND’d. So accept reads as
“any one of these combinations”.
The grammar admits only >= minima: no negation, no maxima. Policy is
therefore monotone by construction: more review, or stronger review, can only
ever flip a unit from fail to pass. You cannot write a rule that a further vouch
breaks.
The assurance vocabulary
Fixed and server-set, because a client cannot attest to its own assurance:
| Rung | Meaning |
|---|---|
self-asserted | The floor. What a receiptless review proves. |
session-verified | Stronger. |
linked | Stronger still. |
presence-verified | The strongest. |
In self-hosted use with no notary, every review sits at self-asserted. An
accept row demanding more is unmeetable. It fails every unit rather than
passing them, which is the correct direction, but it is not a useful gate until
receipts are in play.
[[cover]] vs [[scope]]
The two obligation types differ in a way the names understate.
[[cover]] is proactive. Every unit under paths must carry a fresh,
approved review meeting require. Absence is a failure. Use it for a pinned
trusted set you intend to hold at full coverage.
[[cover]]
name = "crypto-tcb"
paths = ["crates/vidi-core/src/crypto/**"]
require = { human = "maintainer" }
| Field | Required | Notes |
|---|---|---|
name | yes | Names the obligation in diagnostics. |
paths | yes | Explicit globs. An empty list is a load error. |
require | yes | A single accept row, not a list. |
[[scope]] is reactive. Units under path that are present must meet one
of the accept rows.
[[scope]]
path = "crates/vidi-core/src/crypto/**"
accept = [ { human = "maintainer" }, { human = "reviewer", machine = "claude-opus-5" } ]
| Field | Required | Notes |
|---|---|---|
path | yes | A single glob. An empty string is a load error. |
accept | yes | A list of rows, OR’d. An empty list is a load error. |
Shortfalls from the two surface under different words in the summary line:
[[cover]] and [[scope]] produce requirement failures, while paths falling
through to a strict default produce default shortfalls.
An obligation that can never match any path (an empty paths list, an
empty-string glob) is refused at load. It would otherwise satisfy the
“policy present ⇒ gates something” check from the inside while gating nothing.
Path globs
Matching is segment-wise over /, byte-exact and case-sensitive:
| Pattern | Matches |
|---|---|
** | zero or more whole path segments |
* | any run of bytes within a single segment |
? [ { | literal characters, not metacharacters |
crates/**/billing/** matches at any depth, including crates/billing/mod.rs
(** spans zero segments). src/*.rs matches src/lib.rs but not
src/a/b.rs.
There is no brace expansion and no character classes. This is a deliberately small grammar; a policy glob should not be able to surprise its author.
[severity] and [[exemptions]]
[severity]
major = 0.7
[[exemptions]]
unit = "crates/vidi-core/src/legacy.rs::fn:shim"
reason = "scheduled for deletion, tracked in VIDI-214"
[severity] sets advisory band thresholds. It never gates: significance
and severity order the re-review queue and nothing else.
[[exemptions]] deliberately excludes one unit from the unreviewed set; it is
removed from the denominator and reads as Exempt. Both fields are required;
the mandatory reason gives every exemption its own audit trail in the diff.
A minimal starting policy
Everything advisory except one directory that genuinely requires review:
schema = "1.0"
default = "advisory"
[axes]
human.order = ["contributor", "reviewer", "maintainer"]
[reviewers]
"ada@example.com" = { role = "maintainer", name = "Ada" }
[[scope]]
path = "crates/vidi-core/src/crypto/**"
accept = [ { human = "maintainer" } ]
Expect the requirement-failure count to jump the moment you add that scope: every unit under it is unreviewed until someone vouches. That is the policy working, not a misconfiguration. Widen scopes as coverage grows; a policy that gates the whole repository on day one just fails every build, and a gate that always fails teaches people to ignore it.
Load errors
Every one of these refuses the load outright, and a refusal is exit 2, not the exit 1 of a failed gate. The distinction matters in CI: exit 1 means “the code needs review”, exit 2 means “your policy is broken and nothing was checked”. A job that treats them alike will read a malformed policy as a review backlog.
| Error | Cause |
|---|---|
| parse error | Malformed TOML, an unknown key, or a mistyped field. |
| schema major mismatch | MAJOR differs from the engine’s, either direction. |
| schema minor ahead | File MINOR is ahead of the engine’s. |
| empty accept-row list | A [[scope]].accept or strict default with zero rows. |
| accept row states no minimum | A row with no axis set. |
| unknown ladder rung | A reviewer role or accept-row minimum naming no rung. |
| reserved ladder rung | A rung or alias named unranked. |
| vacuous policy | Present, but declares no obligation. |
| unmatchable obligation | A [[cover]]/[[scope]] glob that can match nothing. |
| policy path escapes the repository | .vidi/policy.toml resolves outside the root. |
The one non-fatal case is an alias pointing at a non-rung: a warning, and the alias resolves nothing.
Next
Unit states
Every unit resolves to exactly one coverage state. The state is what
vidi status, vidi report and vidi queue display; the disposition is
the pass/fail signal the gate reads.
Source of truth: GateState in crates/vidi-core/src/ledger/staleness.rs.
The states
| State | Meaning | Disposition |
|---|---|---|
| Fresh | The unit’s current hash equals a non-revoked, approved review’s subject digest, under a compatible profile and scope. The only covering state. | Covered |
| Stale | The unit still exists at the reviewed address, but its hash changed. | Gate-fail |
| Orphan | A non-revoked review whose subject no longer resolves to any unit — renamed, moved, or deleted. | Gate-fail |
| Unreviewed | No covering review at all. | Uncovered |
| Rejected | An exact-hash-matching review with verdict: rejected. Someone read these bytes and said no. | Uncovered |
| Migration-needed | A hash-matching approved review whose profileVersion, scopeModelVersion or node domain is incompatible. | Uncovered |
| Attested | A hash-matching approved review that does not claim the core equivalent criterion — for example a gh:viewed attestation. Surfaces as under-reviewed. | Uncovered |
The three dispositions
Covered: the unit passes. Only Fresh qualifies.
Gate-fail: the unit fails the gate on its own, regardless of policy.
Only Stale and Orphan. Both mean a review claims to describe code that is
no longer there, which is broken independently of any rule you configured. This
is why editing a vouched unit fails the gate in a repo with no policy.toml at
all.
Uncovered: not covered, but not an independent failure either. The trust
policy decides whether it matters. A default = "advisory" policy tolerates
these; a [[scope]] requiring a rank does not.
Two rules that are load-bearing
Migration-needed dominates Fresh. It is evaluated first, so a
profileVersion or scopeModelVersion mismatch never resolves to Fresh even
when the stored hash equals the current recompute. A review made under different
analysis rules is not silently honoured.
Over-report, never false-fresh. Every ambiguity resolves toward Stale,
Orphan or uncovered. The tool would rather say something needs review when it
does not than the reverse. See
Fail-closed by default.
Rejected is narrower than it looks
Rejected is reached only when a review inspected the current bytes:
- a rejected review on an older hash is
Stale, nothing has inspected what is there now - a rejected review whose address resolves to nothing is
Orphan
So Rejected means “someone looked at exactly this and declined it”. It is not
coverage, can never read Fresh, and never gate-fails on its own. The unit
falls to Unreviewed for gating purposes.
A malformed ledger line is a different thing entirely, and does exit non-zero.
Severity is advisory
vidi churn and the severity bands rank how much a change matters. They order
the re-review queue and never affect any state above. The gate reads only the
binary signal: exact hash, verdict, and profile compatibility.
Next
File formats
Vidi keeps state in exactly two places: .vidi/ inside the repository, and
~/.vidi/ on your machine. The split is the important part: the repo
directory is meant to be committed, the home directory never is.
Source of truth: crates/vidi-core/src/ledger/layout.rs, which declares every
repo-relative path as a constant.
What lives where
| Path | Contents | Committed? |
|---|---|---|
.vidi/reviews.jsonl | review claims, approved and rejected | yes |
.vidi/revocations.jsonl | revoke records | yes |
.vidi/authorship.jsonl | advisory model provenance | yes |
.vidi/policy.toml | the trust policy | yes |
.vidi/notary.pub | cached notary public key | yes, once it exists |
~/.vidi/identity | your offline identity | never |
~/.vidi/token | hosted bearer token | never |
~/.vidi/trust-root.json | trust-root manifest | never |
.review-cache/ | derived scan cache | never, git-ignored |
The rule of thumb: anything that is a claim is committed, anything that is a credential or derived is not.
.vidi/reviews.jsonl
One in-toto Statement per line, a review claim about one unit. Both verdicts live here; a rejection is a review, not a separate file.
{
"_type": "https://in-toto.io/Statement/v1",
"subject": [{
"name": "crates/vidi-core/src/digest.rs::struct:Blake3Digest",
"digest": { "blake3": "76b53b0c9cfd57d07d2a4802263e6856" },
"repo": { "host": "github.com", "owner": "graze-ai", "name": "vidi" }
}],
"predicateType": "https://vidivouch.com/vidi/human-review/v1",
"predicate": {
"criteria": ["equivalent"],
"rigor": "read-fully",
"attesterIdentity": { "kind": "person", "id": "you@example.com" },
"roleAtReview": "maintainer",
"verdict": "approved",
"claimTimestamp": "2026-07-30T20:52:58Z",
"profileVersion": "rust-1",
"scopeModelVersion": "vidi-scope-4"
}
}
The fields that carry weight:
| Field | Why it matters |
|---|---|
subject[].digest.blake3 | the unit’s fingerprint as it was when read. When the code changes this stops matching and the review goes stale |
attesterIdentity.id | matched as a literal string against your [reviewers] keys; see policy.toml |
roleAtReview | your standing at vouch time; the effective rank is the weaker of this and your current one |
verdict | approved or rejected |
criteria | equivalent is the core review claim; other values are weaker attestations |
profileVersion / scopeModelVersion | the analysis rules in force. A mismatch reads as migration-needed rather than fresh |
Two records for the same unit are two independent claims. Nothing is overwritten.
.vidi/revocations.jsonl
Revoke records, and a much smaller shape:
{
"_type": "https://in-toto.io/Statement/v1",
"subject": [{ "name": "revoke", "digest": { "blake3": "28d7043ed80ea10df8a78cd4b6f32f3f" } }],
"predicateType": "https://vidivouch.com/vidi/revoke/v1",
"predicate": {
"reason": "revoked via vidi revoke",
"attesterIdentity": { "kind": "person", "id": "you@example.com" },
"claimTimestamp": "2026-07-31T15:26:24Z"
}
}
The subject name is the reserved word revoke, not a unit address. The digest
names the content_id of the review being withdrawn. A revocation targets a
record, not a location, which is why re-vouching afterwards works: the new review
has a different content_id the revocation does not name. See
Revoke an approval.
The file lane is itself a constraint: reviews.jsonl accepts review records
only, revocations.jsonl revocations only. A record in the wrong file is
rejected rather than honoured.
.vidi/authorship.jsonl
Advisory model provenance (which model or models wrote a unit) recorded by
vidi attribute from git-ai’s refs/notes/ai attestations.
{
"address": "crates/vidi-core/src/digest.rs::struct:Blake3Digest",
"contentId": "76b53b0c9cfd57d07d2a4802263e6856",
"contributions": [{ "modelId": "claude-opus-4-8", "lineCount": 12 }],
"agentContext": true
}
This file is never read by the gate, and that guarantee is structural rather
than documentary: the gate path does not import the authorship module, the
ledger exposes no method accepting an authorship record, and no type there can
be converted into a review or a gate state. A malformed line here is dropped as
advisory and can never affect an exit code, unlike a malformed line in
reviews.jsonl, which fails closed.
It stays empty until you run vidi attribute.
.vidi/policy.toml
The trust policy: reviewer ladders, the reviewer directory, and the coverage
obligations that turn review into a gate. It is the one file in .vidi/ that is
not append-only, and the ledger never reads it.
Every key is documented in the policy.toml reference.
.vidi/notary.pub
A cached copy of the notary’s Ed25519 public key (standard-base64, raw 32
bytes). Its only job is to make vidi verify-vouches work offline: once
the key is cached, signature verification needs neither network nor token.
It does not exist until you verify signed vouches, and repos using only self-asserted reviews never grow one.
~/.vidi/: machine-level, never committed
Credentials, scoped to your machine rather than to a repository, the way
git config --global is:
| File | Contents |
|---|---|
identity | the offline identity from vidi login --local, one line, e.g. you@example.com |
token | the hosted bearer token from vidi login. Revocable, and not a signing key |
trust-root.json | the trust-root manifest mapping key ids to epochs |
Two overrides exist for non-default setups: $VIDI_IDENTITY_PATH relocates the
identity file, and $VIDI_REMOTE_TOKEN supplies a bearer directly, which is
how CI authenticates without an interactive login.
.review-cache/: derived, git-ignored
Per-file scan facts, cached so repeated scans do not re-hash unchanged files.
Never a source of truth: delete it and the next command rebuilds it. vidi init
adds .review-cache/ to .gitignore for you.
Why the ledgers are JSONL
One claim per line, append-only, and never rewritten in place. Three properties follow from that, and they are the reason the format is what it is:
Diffs are readable. A new review is one added line in a pull request, not an opaque binary delta.
Merges cannot conflict. vidi init writes .vidi/*.jsonl merge=union into
.gitattributes, so git resolves two branches by keeping every line from both
sides. Order does not matter because the merge algebra is monotone:
union, dedupe by content_id, revoke-wins.
History survives. Revoking appends a record rather than deleting one, so “approved Tuesday, withdrawn Thursday” stays visible. See Why review files never conflict.
This is also why writers append rather than rewrite the file: a whole-file rewrite would clobber lines contributed on another branch, breaking the union algebra that makes the conflict-freedom work.
The two lines vidi init writes
.gitattributes: .vidi/*.jsonl merge=union
.gitignore: .review-cache/
Both are declared as constants in layout.rs. Commit them along with .vidi/:
the merge rule is what stops two people’s reviews from colliding, and it only
works if everyone has it.
Next
API documentation
Stub. Link out to the rustdoc build.
missing_docsis already enforced workspace-wide, so the API reference is complete — it just needs publishing alongside this book.
Why units, and not files?
A file is a container, not a thing. Reviewing one is reviewing whatever happens to live in it, which is a moving target. Vidi attaches review to the smallest chunk of code that has its own identity: a unit.
The problem
Suppose reviews are attached to files. A file can have 1000 lines, you can change one line in it, and every other line in that file loses its coverage.
This makes coverage useless on exactly the files that matter most, because the biggest files would spend their lives uncovered. Per unit, an edit expires the function you edited. The rest of the file keeps the coverage it earned.
What a unit is
A function, a struct, an impl block, a class method, a Markdown paragraph. The boundaries come from parsing the file, not from counting lines, so they follow the shape the language actually has.
One generic tree-sitter descent produces them for every language
(crates/vidi-lang/src/engine.rs), driven entirely by per-language
configuration rather than per-language code. That descent is what the
tree-sitter grammars are for, and why building vidi takes as long as it does.
The address
Every unit has an address: the file, then the path to the unit inside it.
crates/vidi-core/src/digest.rs::struct:Blake3Digest
└──────────── file ───────────┘ └── what, inside it ──┘
Addresses and hashes are independent
This is the part that is easy to get backwards. The address says where a unit is. The hash says what it contains. They are computed separately and neither feeds the other.
hash_unit takes no address at all. The addressing suffix never enters the hash.
Two identical sibling functions therefore produce the same hash, which is
correct: they are the same code, and reviewing one is reviewing the other.
When two siblings would otherwise share an address, vidi appends a #{n}
suffix to tell them apart. That suffix stays strictly on the addressing side; it
cannot leak into the content hash, because a naming detail changing what a unit
“is” would restale code that nobody edited.
Another issue
Raw HTML inside Markdown splits badly. CommonMark ends an HTML block at a blank
line, so a wrapper element becomes two units. One holding the opening tag,
one holding the closing tag, each containing nothing else. In this repo’s own
docs, 23 <div> wrappers produced 46 reviewable units of pure markup.
This is specific to Markdown that embeds raw HTML. It does not affect code: a
TSX component with four nested <div>s is one unit, the function. Plain .html
files are not scanned at all. If a Markdown file is generating junk units, the
fix is usually to use Markdown constructs instead of HTML ones.
Next
How fingerprints expire reviews
How fingerprints expire reviews
A review records a hash, not a name. Therefore, when the code changes, the hash changes, and the review no longer describes anything that exists.
What counts as a change
Not every edit counts. If reformatting expired existing reviews, nobody would trust it.
As such, the source is normalized first, and only what survives normalization is
hashed. (crates/vidi-lang/src/normalize.rs).
| Edit | Restales? | Why |
|---|---|---|
| Reflow or re-indent | no | Whitespace between tokens is dropped; no token is. |
| Rename the unit or a parameter | no | A name bound by the unit becomes its position, so a consistent rename collapses. |
| Rewrite a comment | no | Comments are excluded from the hash entirely. |
| Rename something it calls | yes | A name the unit does not bind keeps its exact text. |
a + b → a - b | yes | For an operator or keyword, the token itself is the identity. |
| Change a literal | yes | Literals keep their exact text. |
The dividing line is the behaviour: an edit that cannot change what the code does leaves the hash alone, and an edit that could, changes it.
Block structure counts
Moving a statement into or out of a loop body is a real change, and vidi has to see it even where nothing visible marks the block.
In a brace language this is free since { and } are tokens, so the nesting is
already part of the identity. Python has no such token, and flattening the tree
would erase the nesting, making the move hash-invariant. That would be a way to
change behaviour without expiring the review, so those blocks get synthetic
enter and exit markers instead. A whole-unit re-indent still hashes the same.
Nested code restales its parent
A unit’s hash folds in the hashes of its children, so editing a nested function changes both its own hash and its parent’s, so the parent’s identity includes what it contains.
Where child order carries no meaning, children fold order-independently: reordering independent methods is not a change.
Why the rules are part of the hash
The hash covers profileVersion and scopeModelVersion which are the identities of the
normalization rules themselves. A fingerprint means these bytes, under these
rules, and comparing hashes computed under different rules would be
meaningless.
It is also why a review whose hash matches but whose profile does not never reads as fresh. Vidi will not re-hash under new rules and call the result covered. See Fail-closed by default.
What this does not cover
Comments are stripped before hashing, so a comment can be made wrong without anything going stale. A function whose doc comment describes behaviour it no longer has stays fresh, and vidi will not tell you.
This is the right default, since expiring every review in a file because someone fixed a typo would make coverage too noisy to read. But it narrows the guarantee, and the narrowed version is worth stating plainly: vidi certifies that behaviour was reviewed, not that every byte was.
Next
The parser-free core
Reading source code is the most dangerous thing vidi does. Nineteen tree-sitter grammars, all of them generated C, run over whatever happens to be in your repository.
So the code that decides whether you pass never does it.
The one-way edge
The workspace manifest states the doctrine in its own header: six crates whose dependency edges point one way, into a parser-free trusted core. Nothing the core trusts ever sees a parser.
vidi-lang 19 tree-sitter grammars + a Markdown profile ─┐
vidi-graph significance scoring, advisory only ─┼─→ vidi-core
vidi-cli filesystem and CLI glue ─┘ decides
vidi-core holds everything that produces a verdict: addressing and digests,
in-toto records, the ledger, trust policy, signature and notary verification. It depends
on no internal crate, not one path dependency in its manifest.
The mechanism is that simple, and Cargo checks it on every build. Doctrine in a comment is a wish; doctrine as a dependency edge is enforced.
Why parsers specifically
A parser’s input is untrusted by definition. The scan reads whatever is
committed, and a generated, minified, or deliberately hostile file can nest
arbitrarily deep: a 20,000-level parenthesis chain, a 50,000-segment qualified
name. Every recursive descent in vidi-lang carries a depth cap for exactly
that reason.
Keeping the grammars out of the deciding path bounds what a bad file can do. It can crash the scanner, produce nonsense units, or refuse to parse. It cannot forge a verdict, because nothing it touches is trusted by the code that computes one.
What the name overstates
vidi-core is not literally free of parsing. It depends on seven external
crates, and two of them decode formats:
| Crate | Why it is there |
|---|---|
blake3 | the sole content-addressing hash |
serde · serde_json | the exact bytes a record is hashed from, and the ledger line format |
toml | .vidi/policy.toml, with Spanned byte offsets for diagnostics |
ed25519-dalek · sha2 | signature verification and the keyid fingerprint |
libc | O_NOFOLLOW for the ledger custody seam, Unix targets only |
So the core does parse, but only its own small, controlled formats. What it never parses is a programming language. That claim is narrower than the name suggests.
The cost, in hand-written code
Adding a dependency to the core is expensive, so small things get written rather
than imported. The policy glob matcher is 186 hand-rolled lines, and its module
docs say why plainly: a vendored glob engine would be far more attack surface
than these few lines. Errors are hand-rolled too, with no thiserror.
Every dependency in the core’s manifest carries a comment arguing for its existence, and the workspace catalog is grown at first use, never ahead of a consumer, so the tree never carries an unused edge.
What sits outside, and why
Two crates are excluded on purpose, and neither is a parser:
vidi-graph computes significance, meaning how much a change matters. It is
advisory and never feeds a content hash. Ranking is allowed to be clever,
heuristic, and wrong; it orders a queue rather than deciding an outcome, so it
does not need to be trusted.
vidi-interop binds the engine’s output for renderers and never re-derives
a verdict. Clients display what the core decided; they do not recompute it.
The line is not “parsers outside, everything else inside”. It is: anything that could be wrong without being dangerous stays out.
No unsafe anywhere
All six crates carry #![forbid(unsafe_code)], the core included. The compiler
rejects hand-written unsafe, so memory-safety review has no surface to cover.
The tree-sitter grammars are still C, linked in. The forbid applies to vidi’s
own Rust, not to what it links.
Where it frays
The core still decodes untrusted input. Ledger lines, policy files, and signature envelopes all arrive from outside and all go through a decoder. Parsing is reduced to small controlled formats, not eliminated, which is why those paths fail closed on any doubt rather than trusting a successful parse.
Hand-rolled means fewer eyes. A bespoke glob matcher has bespoke bugs, and 186 lines maintained here get far less scrutiny than a widely used crate. The trade buys a smaller, auditable surface at the price of shared maintenance. That is a trade rather than a free win.
The grammars still ship. They are excluded from the deciding path, not from the product. A parser bug is still a bug you experience; the guarantee is only that it cannot become a forged verdict.
Next
Why review files never conflict
Why review files never conflict
Two people review different code on different branches. Both append to
.vidi/reviews.jsonl. That should collide on every merge, and if reviewing
meant resolving a conflict each time, people would quietly stop reviewing.
It does not collide, and the reason is four properties stacked on top of each other.
One claim per line, never rewritten
The ledgers are JSONL: one review per line, appended. Nothing is ever edited in place, and nothing is deleted. Revoking adds a record rather than removing one.
That already removes most of the problem. Two people appending different lines are not editing the same text, so there is nothing for git to arbitrate.
The git half: merge=union
vidi init writes one line into .gitattributes:
.vidi/*.jsonl merge=union
This tells git to resolve these files by keeping every line from both sides instead of asking you to choose. No conflict markers, ever.
Writers append rather than rewrite for exactly this reason. A whole-file rewrite would clobber lines contributed on another branch, which is what union merge exists to prevent.
Why union alone would be wrong
Keeping every line says nothing about what the lines mean together. Union on its own leaves two holes:
- the same claim can arrive twice: a replay, or one review present in both its bare and its signed form
- a revocation and the review it kills can both be present
So the reader applies an algebra over the union
(crates/vidi-core/src/ledger/store.rs):
| Rule | Effect |
|---|---|
content_id dedup | content-identical lines collapse to one entry |
| revoke-wins | a revocation dominates its target forever, whichever arrived first |
| monotone | reviews.jsonl only ever adds claims; revocations.jsonl only ever tightens |
Order cannot change the answer
None of those rules depend on order. Two people who merge the same set of records compute the same coverage, no matter what sequence the merges happened in, and no matter where in the file a line sits.
No coordination is needed, because there is no ordering to agree on. (The formal name for this property is a monotone CRDT, if you want to look it up.)
It is also what makes re-approving after a revocation work. A revocation names
one content_id and dominates it permanently. But vouching again produces a
record with a different content_id, which the revocation does not name. The
old claim stays dead, the new one is live, and neither outcome depends on
arrival order.
Choosing which copy survives
Dedup has to keep one entry, and choosing which one is harder than it looks.
The same review can be written down three ways: plainly, wrapped in a signature,
or wrapped in a signature plus a notary receipt. Same claim, different envelopes,
and all three share one content_id.
So when several of them arrive, something must decide which to keep. Last-write-wins would make the survivor depend on merge order, and the whole guarantee above would collapse.
Instead the store ranks the forms (plain, then signed, then signed with a receipt) and always keeps the highest, breaking ties on the bytes themselves. The form it retains therefore depends only on which forms turned up, never on the order they arrived in.
That tie-break is also deliberately inert. The gate never reads a record’s form, receipt, or signatures, so which representative survives can never move a gate outcome. It exists only to make the answer deterministic.
A set, not a log
There is no repo-side hash chain and no sequence number. The ledger is an unordered set of claims, and any chain integrity lives notary-side only.
This is a deliberate trade. A chain would give tamper-evident ordering, but ordering is the one thing that cannot survive two people merging branches in different sequences. Vidi gives up the chain in the repository to keep the convergence.
Whitespace cannot dodge a revocation
There is a hard edge here. A content_id is computed after the surrounding
whitespace is stripped, so two records that differ only in spacing get the same
id (crates/vidi-core/src/ledger/codec.rs).
Without that, a whitespace twin of a revoked review would hash to a different
content_id, which the revocation does not name, and the resurrected claim
would read fresh. Stripping the whitespace first closes that door.
What this does not cover
The guarantee is about merging, not about agreement. Two people can hold contradictory positions on the same code, one approving and one rejecting, and the merge keeps both lines without complaint. Resolving that disagreement is a human problem; the ledger only promises that it will not lose either claim and that everyone computes the same answer from the pair.
It also depends on everyone having the .gitattributes rule. Commit that line
with .vidi/. A clone missing it gets ordinary conflicting-file behavior on
the one file that should never conflict.
Next
Fail-closed by default
One comment in crates/vidi-core/src/ledger/staleness.rs states the rule:
over-report, never false-fresh: every ambiguity resolves toward STALE / ORPHAN / uncovered.
Wherever vidi cannot be certain, it resolves toward less coverage, not more.
Why
The two ways to be wrong are not symmetric. A false red costs someone twenty minutes re-reading a unit that was probably fine. A false green merges unreviewed code wearing a check mark that says otherwise, which is worse than no gate at all, because the check mark is what people trust.
There is no threshold where that trade reverses, so ambiguity always resolves the same direction. What follows is the four places it could have gone the other way.
When the code changed
Of the seven unit states, exactly one is
coverage: Fresh. Two of the others exist to stop a near-miss reading as a hit.
Migration-needed beats fresh. A review whose hash matches exactly, but whose
profileVersion or scopeModelVersion does not, never resolves to Fresh. A
matching hash is not enough on its own. Vidi also has to believe both hashes
were computed under the same rules, and it will not re-hash under new rules and
call the result covered.
Rejected is not coverage. Someone read the current bytes and said no. It
never reads Fresh, though it does not gate-fail on its own either.
When the ledger is corrupt
A line in .vidi/reviews.jsonl that will not decode is not skipped. It refuses
the whole report:
vidi: refusing to assemble a report over a partially-decoded ledger: 1 rejected line(s)
Skipping the bad line is the reasonable-looking choice, and it is exactly the bug: a truncated ledger would read as fewer reviews recorded, which is indistinguishable from nothing is stale, everything is fine. Corruption would present as a pass.
Because this refusal happens before any verdict exists, vidi verify --json
exits 1 with empty stdout and vidi report --sarif emits no file at all. There
is nothing to serialize.
When the policy will not parse
A broken .vidi/policy.toml is a load error. It never degrades quietly to
advisory and never falls back to a default.
Unknown keys are refused rather than ignored. An ignored key is a rule you believe you wrote and do not have, which is a policy stricter on disk than in effect.
When a signature cannot be checked
With no trust root, or a stale one, every review floors to the weakest assurance rung rather than keeping the standing it claimed. A signature that cannot be verified grades as unverified, never as valid.
Two exit codes that mean opposite things
| Exit | Meaning |
|---|---|
1 | A verdict was formed and it failed: something is stale, orphaned, or unmet. |
2 | No verdict could be formed. Vidi refused — unloadable policy, unreadable repo, bad arguments. |
Exit 1 says your code needs review. Exit 2 says vidi could not run. Both block the merge; that does not make them the same event.
What deliberately does not fail closed
Churn, significance, vidi graph and vidi attribute never gate. Severity
bands are advisory by contract, and authorship import fails toward unknown
rather than toward a guess.
The line is between what vidi computes exactly and what it only estimates. Hashes, decode success and signature validity are exact, so they fail closed. How risky or significant a change is are estimates, and an estimate that gates is noise with a moral posture. Advisory signals order your queue; they never decide it.
What it costs
- Prose counts. Markdown becomes units, so writing documentation raises your unreviewed count while you write it.
- Counts drift. Totals move as files change. A different number than last run is normal, not evidence you broke something.
NEUTRALpasses with zero coverage. No policy means nothing to enforce, so a repo with no reviews at all exits 0.- Volume.
vidi explainover thousands of uncovered units prints thousands of diagnostics. Scope it to a path.
None of these argue against the posture. They are its price, and hiding them would be the same mistake in miniature.
Set up your environment
Prerequisites
Rust stable via rustup, with rustfmt and clippy:
rustup component add rustfmt clippy
rust-toolchain.toml pins the channel and components, so rustup installs the
right ones the first time you build.
pre-commit for the git hooks. Install it however you install Python tools
(pipx install pre-commit, brew install pre-commit, uv tool install pre-commit).
Get the workspace building
git clone https://github.com/graze-ai/vidi
cd vidi
cargo build # build the workspace
pre-commit install # wire the git hooks
The first build takes a while. Most of it is the 19 tree-sitter grammars compiling their generated C. Later builds only recompile what changed.
Confirm it works
cargo run -- status
Run inside the repo, that prints a coverage line for vidi’s own code. If you get a verdict rather than an error, the workspace is set up.
Two binaries live in vidi-cli, so cargo run needs default-run = "vidi" to
know which one you mean. The other, fake-vidi, exists only for the hooks test suite.
Run the tests
cargo test --all --all-features --locked
--all-features matters. The hooks suite is gated behind vidi-cli’s private
_fake-vidi feature, which is what keeps the test helper out of cargo install.
A bare cargo test silently skips that suite, meaning it passes, and it did not run
what you thought it ran.
The crates
Six, with dependency edges pointing one way into a parser-free core:
| Crate | What it is |
|---|---|
vidi-core | The trusted core: addressing, digests, records, the ledger, policy, crypto. Depends on no internal crate, and never sees a parser. |
vidi-lang | The scan engine: one generic tree-sitter descent plus a Markdown profile. |
vidi-graph | Significance scoring. Advisory; never feeds a content hash. |
vidi-cli | The shipped vidi binary and its filesystem glue. |
vidi-py | The PyO3 bridge over the core verifier. |
vidi-interop | The renderer-side client contract. Binds the engine’s output, never re-derives a verdict. |
Editors
.idea/ and .vscode/ carry shared settings and load automatically. In VS Code,
install the recommended extensions when prompted; rust-analyzer is configured to
run clippy on check.
Next
Gates and checks
Four checks gate the workspace:
cargo fmt --all -- --check # formatting
cargo clippy --all-targets --all-features --locked -- -D warnings # lints, warnings denied
cargo test --all --all-features --locked # tests
cargo deny check bans licenses sources # dependency policy
No single hook or workflow runs all four. Enforcement is split, and running those four commands yourself is the only place they are checked together. Do it before finishing a unit of work.
What runs where
| Check | pre-commit | CI |
|---|---|---|
cargo fmt | yes | yes |
cargo clippy | yes | yes |
cargo test | no | yes |
cargo deny (bans/licenses/sources) | yes | — |
cargo deny check advisories | never | scheduled only |
The tests never run on commit because they take too long. The cost is that you have to run them yourself. Do it before you finish a unit of work, not before every commit.
Each pre-commit hook is a whole-workspace check (pass_filenames: false) gated
by a files matcher, so the matcher only decides whether it runs this commit,
never what it checks. Staging a .rs file runs fmt and clippy over everything;
staging only a Markdown file runs neither.
The advisories rule
Never run cargo deny check advisories as a local gate, and never bare
cargo deny check, which includes it.
Advisories query the live RustSec database, so a new advisory can fail your build without a single change on your side.
Advisories belong to the scheduled audit.yml workflow, which runs them daily,
on pushes touching dependency manifests, and on manual dispatch. If one does
appear, do not let it block or derail what you are doing, just flag it and carry on.
The fix is a routine cargo update -p <crate> handled separately.
Lint posture
Strict by default, declared in Cargo.toml under [workspace.lints]: clippy
pedantic and nursery enabled, unsafe_code forbidden.
Those groups are set to warn in the manifest, but both the pre-commit hook and
CI run -D warnings. A warning is a hard failure at the gate. The manifest
level only controls what you see mid-edit.
Any unsafe requires a // SAFETY: justification plus an explicit lint allow.
Every crate carries #![forbid(unsafe_code)] except vidi-py, whose PyO3 macros
expand to unsafe and so cannot inherit the forbid, as it writes none of its own.
CI
Two workflows in .github/workflows/:
lint.yml — every push to main and every pull request. Format check and
clippy, then a build-and-test matrix across Linux, macOS and Windows.
It also checks that docs/src/reference/cli.md is current. That page is
generated from the binary’s own --help, so a renamed flag silently invalidates
it. If the check fails, run python docs/generate-cli-reference.py and commit
the result.
audit.yml — the live advisory scan, on a schedule.
Hooks that block skipping
Agent hooks under .claude/hooks/ refuse git commit, push and merge
invocations that bypass verification — --no-verify, the -n shorthand, and
core.hooksPath overrides.
The point is not that skipping a hook is forbidden. However, skipping one should be a decision you make out loud rather than a flag that becomes a habit.
Next
Commit conventions
The existing history is the spec. git log --oneline is the best reference
available; this page only names the patterns it already follows.
Shape
type(scope): what changed, specifically
The scope is optional and names the crate or area — lang, core, graph,
policy, cli, prose, meta. Types in use:
| Type | For |
|---|---|
feat | New behaviour. |
fix | A defect corrected. |
port | Behaviour brought over from the reference implementation. |
docs | Documentation. |
test | Tests only. |
ci | Workflows and gates. |
chore | Everything else. |
review | Findings closed from a review pass. |
Commit descriptions
This is the rule that matters. A commit should say what is now true that was not true before. Specifically, it should be enough that someone scanning the log can tell whether this commit is the one they are looking for.
Compare:
fix(graph): harden qualpath resolution. package-index dirs, keyword roots,
drop-on-miss, no file-stem inflation
against fix(graph): fix bugs. Both are accurate, though only one of them is findable.
More from the log:
port(crypto): bind DSSE signature to exact wire payload bytes, not the trimmed form
fix(cli): exit 141 on a closed stdout pipe instead of panicking
feat(show): resolve a unit address, not just a content_id
fix(init): report only the dotfiles it actually rewrote
Each names a specific behaviour and, where it helps, the thing it replaced. The
X, not Y construction does a lot of work. It tells you the old behaviour too,
which is what you actually want when bisecting.
Write bodies if necessary
Skip it for anything self-evident; but write one when the change has a reason that is not visible in the diff.
ci: fail the build when the generated CLI reference is stale
docs/src/reference/cli.md is generated from the binary's own --help, so a
renamed flag or a new subcommand silently invalidates it. It had drifted
13 commits before anyone noticed.
Runs in the existing test job after the build, reusing target/debug/vidi
rather than paying the tree-sitter compile again, and only on Linux since
the page is byte-identical on every host.
The first paragraph is the reason; while the second is the choice and its justification.
Wrap bodies at 72 characters.
Version and profile rotations
A change that rotates profileVersion, SCOPE_MODEL_VERSION or a grammar pin
must say so in the subject, because it restales every review under the old
rules:
port(lang): vidi-scope-4 — out-of-node modifier/decorator/trait-header hashing
(SCOPE_MODEL 3->4, goldens re-blessed)
The 3->4 and the note that goldens were re-blessed are not decoration. They
are how someone reading the log later knows why every hash moved.
Next
Start contributing!