Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

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

  1. Units. vidi splits every file into small nameable pieces — one function, one struct, one paragraph — instead of treating a file as one reviewable thing.
  2. Fingerprints. Each unit gets a content hash. Same bytes, same fingerprint; one character different, a completely different fingerprint.
  3. Vouches. Reviewing a unit records a signed statement: who you are, which unit, its fingerprint, and your verdict.
  4. 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:

PlatformInstall
Linux / WSLsudo apt install build-essential pkg-config
macOSxcode-select --install
WindowsVisual Studio Build Tools — the rustup installer offers these; accept

Close every terminal and editor after installing. Installers add ~/.cargo/bin to 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 see cargo.

$ 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 build is not enough. It leaves the binary in target/, where your shell will not find it. cargo install is what puts vidi on 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-extension from 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

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 5356 as “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:

VerdictWhen
FAILUREanything stale, orphaned, or failing a requirement — checked first, whether or not a policy exists
PASSa policy is present and every requirement is met
NEUTRALno .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 show does not do this. It accepts either a content_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/53561/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

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/54560/5456 — the unit is no longer covered
  • stale 0stale 1 — and vidi knows why it is uncovered
  • NEUTRALFAILURE — 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 FAILURE rather than NEUTRAL.

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

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:

KeyEffect
default = "advisory"everything outside a [[scope]] is reported, never gate-failing
[axes] human.orderthe 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 empty accept = [] 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 status is 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 $?
0

A CI job running vidi status prints FAILURE on every build and passes anyway. Use vidi verify.

Three verdicts

VerdictExitWhen
FAILURE1anything stale, orphaned, or unmet, evaluated before policy presence
PASS0a policy exists and every requirement is met
NEUTRAL0no 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 runsInstall step
Another repository, gating its own codecargo install --git https://github.com/graze-ai/vidi vidi-cli --locked
This repository, gating itselfcargo 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

ExitVerdictMeaning
0PASSa policy exists and every requirement is met
0NEUTRALno .vidi/policy.toml, and nothing broken
1FAILUREsomething is stale, orphaned, or unmet — or a ledger line would not decode
2the command refused: a broken policy, bad arguments, a failed scan
141stdout 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

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:

CommandScopes to a change?
vidi explain <path>yes — one file or one unit
vidi vouch --commit <ref>yes — a whole commit by git ref
vidi queueno — ranks the whole repo
vidi verifyno — 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>"
RigorMeaning
skimmedread quickly
read-fullyread the whole unit (the default)
ran-testsread 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

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:

ValueWhat 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 pull and move with git 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 pullvidi push
Directionportal → your machineyour machine → portal
What movesreview and revocation recordsa coverage snapshot
Effectunion-merges into your local .vidi/stored server-side for re-rendering
Gates anything?the records it adds donever

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:

LoginattesterIdentity.id recorded
vidi login --localyour git email, e.g. you@example.com
vidi logina 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:

TokenRepo selectorScope of the result
person (vida_ / vidc_, from vidi login)required — sent as ?repo=host/owner/namethat repository
tenant API tokennone; the bearer is already tenant-scopedthe 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

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

ActionVS CodeJetBrainsJetBrains (macOS)
Vouch for the unit at the cursorCtrl+Alt+VCtrl+Alt+VCtrl+Shift+V
Review from here (call-graph walk)Ctrl+Alt+Wcontext menucontext menu
Sync (hosted lane only)Ctrl+Alt+SCtrl+Alt+Shift+UCtrl+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

SettingDefaultWhat it does
vidi.binaryPath(empty)Explicit path to the binary; empty uses the resolution ladder.
vidi.showInlayHintstrueEnd-of-line provenance per unit.
vidi.significancetrueCompute the significance lens (vidi json --significance). Advisory; never gates.
vidi.commitTimePromptfalseOffer 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

CommandWhat it does
scanWalk the repo into addressable units, content-hash each, and harvest references, one deterministic tree descent
reportCoverage report: what is covered, the obligations, and the re-review queue ordered by significance x severity
statusOne-line gate status; add –porcelain for a stable scriptable form
queueThe re-review queue: gating obligations first, then significance x churn severity
verifyThe gate: resolve every obligation against the ledger, emit the three-state check-run and the exit code
explainExplain 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-vouchesVerify 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)
pushPush the coverage artifact to the hosted portal (POST /api/v1/ingest), the paid lane: a resolvable remote (URL + bearer) is required
pullPull the tenant’s reviews and revocations from the hosted portal and union-merge them into the local ledgers (GET /api/v1/reviews)
churnAdvisory churn for one unit: state and severity band (never gates)
significanceAdvisory significance ranking: orders within each re-review queue stratum; never gates
graphBounded call-graph neighborhood around a unit as JSON: {from, callees, callers} (advisory; depth <= 2)
showShow the exact bare statement(s) for a content_id or unit address; add –json for the machine shape
jsonMachine report of every unit; add –significance for the within-repo percentile
initInitialize vidi in this repo: create the in-repo .vidi/ files
reviewReview one unit interactively (rigor + verdict), appending to the in-repo ledger
vouchOne-shot approved review of a unit, or a whole commit with –commit
revokeRevoke a review by content_id or unit address (revoke-wins; never needs the notary)
signSign a review through the notary, a logged-in human; refused for automated agents
whoamiResolve who you are: person-bound, before any tenant
attributeRecord 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
hooksManage the post-commit hook that runs vidi attribute after each commit, the routine authorship-capture cadence
historyForensics 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
loginLog in so writes are authorized, machine-wide, like git config --global
logoutLog 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

KeyRequiredTypeMeaning
schemayes"MAJOR.MINOR"Schema version. This engine enforces "1.0".
defaultyes"advisory" or accept-row listPosture for paths matched by no gating [[scope]].
[axes]notableThe two ranked reviewer ladders.
[reviewers]notableThe reviewer directory: identity → role.
[[cover]]noarrayProactive coverage obligations.
[[scope]]noarrayReactive path-scoped requirements.
[severity]notableAdvisory thresholds. Never gates.
[[exemptions]]noarrayAudited 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:

FileEngineResult
1.01.0loads
1.01.4loads; the file uses a subset of what the engine knows
1.71.0refused; the file may carry a gating field this engine cannot enforce
0.9 / 2.01.0refused; 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" }
FieldRequiredMeaning
roleyesMust name a rung on the human ladder.
namenoDisplay 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:

LoginRecorded 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.emailthat 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:

StampSupplies
a role namemin(stamped, current)
absent (legacy line)the current directory role
unrankedno rank, ever
a name that resolves to no rungno 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" }
AxisCompared against
humanThe reviewer’s human-ladder rank (see above).
machineThe tool review’s machine-ladder rank.
assuranceThe 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:

RungMeaning
self-assertedThe floor. What a receiptless review proves.
session-verifiedStronger.
linkedStronger still.
presence-verifiedThe 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" }
FieldRequiredNotes
nameyesNames the obligation in diagnostics.
pathsyesExplicit globs. An empty list is a load error.
requireyesA 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" } ]
FieldRequiredNotes
pathyesA single glob. An empty string is a load error.
acceptyesA 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:

PatternMatches
**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.

ErrorCause
parse errorMalformed TOML, an unknown key, or a mistyped field.
schema major mismatchMAJOR differs from the engine’s, either direction.
schema minor aheadFile MINOR is ahead of the engine’s.
empty accept-row listA [[scope]].accept or strict default with zero rows.
accept row states no minimumA row with no axis set.
unknown ladder rungA reviewer role or accept-row minimum naming no rung.
reserved ladder rungA rung or alias named unranked.
vacuous policyPresent, but declares no obligation.
unmatchable obligationA [[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

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

StateMeaningDisposition
FreshThe unit’s current hash equals a non-revoked, approved review’s subject digest, under a compatible profile and scope. The only covering state.Covered
StaleThe unit still exists at the reviewed address, but its hash changed.Gate-fail
OrphanA non-revoked review whose subject no longer resolves to any unit — renamed, moved, or deleted.Gate-fail
UnreviewedNo covering review at all.Uncovered
RejectedAn exact-hash-matching review with verdict: rejected. Someone read these bytes and said no.Uncovered
Migration-neededA hash-matching approved review whose profileVersion, scopeModelVersion or node domain is incompatible.Uncovered
AttestedA 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

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

PathContentsCommitted?
.vidi/reviews.jsonlreview claims, approved and rejectedyes
.vidi/revocations.jsonlrevoke recordsyes
.vidi/authorship.jsonladvisory model provenanceyes
.vidi/policy.tomlthe trust policyyes
.vidi/notary.pubcached notary public keyyes, once it exists
~/.vidi/identityyour offline identitynever
~/.vidi/tokenhosted bearer tokennever
~/.vidi/trust-root.jsontrust-root manifestnever
.review-cache/derived scan cachenever, 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:

FieldWhy it matters
subject[].digest.blake3the unit’s fingerprint as it was when read. When the code changes this stops matching and the review goes stale
attesterIdentity.idmatched as a literal string against your [reviewers] keys; see policy.toml
roleAtReviewyour standing at vouch time; the effective rank is the weaker of this and your current one
verdictapproved or rejected
criteriaequivalent is the core review claim; other values are weaker attestations
profileVersion / scopeModelVersionthe 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:

FileContents
identitythe offline identity from vidi login --local, one line, e.g. you@example.com
tokenthe hosted bearer token from vidi login. Revocable, and not a signing key
trust-root.jsonthe 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

API documentation

Stub. Link out to the rustdoc build. missing_docs is 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).

EditRestales?Why
Reflow or re-indentnoWhitespace between tokens is dropped; no token is.
Rename the unit or a parameternoA name bound by the unit becomes its position, so a consistent rename collapses.
Rewrite a commentnoComments are excluded from the hash entirely.
Rename something it callsyesA name the unit does not bind keeps its exact text.
a + ba - byesFor an operator or keyword, the token itself is the identity.
Change a literalyesLiterals 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

Fail-closed by default

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:

CrateWhy it is there
blake3the sole content-addressing hash
serde · serde_jsonthe exact bytes a record is hashed from, and the ledger line format
toml.vidi/policy.toml, with Spanned byte offsets for diagnostics
ed25519-dalek · sha2signature verification and the keyid fingerprint
libcO_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):

RuleEffect
content_id dedupcontent-identical lines collapse to one entry
revoke-winsa revocation dominates its target forever, whichever arrived first
monotonereviews.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

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

ExitMeaning
1A verdict was formed and it failed: something is stale, orphaned, or unmet.
2No 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.
  • NEUTRAL passes with zero coverage. No policy means nothing to enforce, so a repo with no reviews at all exits 0.
  • Volume. vidi explain over 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:

CrateWhat it is
vidi-coreThe trusted core: addressing, digests, records, the ledger, policy, crypto. Depends on no internal crate, and never sees a parser.
vidi-langThe scan engine: one generic tree-sitter descent plus a Markdown profile.
vidi-graphSignificance scoring. Advisory; never feeds a content hash.
vidi-cliThe shipped vidi binary and its filesystem glue.
vidi-pyThe PyO3 bridge over the core verifier.
vidi-interopThe 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

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

Checkpre-commitCI
cargo fmtyesyes
cargo clippyyesyes
cargo testnoyes
cargo deny (bans/licenses/sources)yes
cargo deny check advisoriesneverscheduled 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

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:

TypeFor
featNew behaviour.
fixA defect corrected.
portBehaviour brought over from the reference implementation.
docsDocumentation.
testTests only.
ciWorkflows and gates.
choreEverything else.
reviewFindings 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!