Configuration#
Start with one review requirement#
A policy selects files and describes what a reviewer must establish. For
example, put this policy in snob/policies/routes.policy.toml:
# snob/policies/routes.policy.toml
match = ["src/routes/**/*.ts"]
[[must]]
id = "no-privilege-escalation"
description = "A user cannot raise their own privileges."
review = "Trace every write to roles or permissions back to an authorization check."
Each matching file needs a review for routes/no-privilege-escalation.
The policy path supplies routes; the rule's id supplies the rest. The
description states the requirement, and review gives the reviewer a
way to investigate it.
Run snob init after adding the policy to create missing review entries
as "changeme". Then use snob pending for the file you want to review.
Its review file maps the rule id to a claim reference. The claim records
the review and the subject it covered; the review file does not repeat the
policy or hold the review itself. See the walkthrough
and claim format.
If the judgment depends on supporting files, declare a narrow context
glob. snob includes those files in the reviewed subject so their changes
can reopen the review. The policy, an individual rule and a file's review
record can each declare context.
Policy files#
The following example adds an exclusion, supporting context and rules at the other levels:
# snob/policies/routes.policy.toml
match = ["src/routes/**/*.ts"] # required
exclude = ["src/routes/**/*.test.ts"] # optional
context = ["src/auth/**"] # optional; applies to every rule here
[[must]]
id = "no-privilege-escalation" # full id: "routes/no-privilege-escalation"
description = "A user cannot raise their own privileges."
review = "Trace every write to roles or permissions back to an authorization check."
[[should]]
id = "audit-log"
description = "Role changes are written to the audit log."
context = ["src/audit/**"] # optional; this rule only
[[consider]]
id = "loading-states"
description = "Slow requests show a loading state."
Rule levels#
An unsatisfied must rule fails snob check; an unsatisfied should rule
warns, and fails only with --deny-warnings. A consider
rule is guidance: it appears in snob pending and in reports, but needs no
claim, gets no init placeholder, is never review debt and never changes
the exit code. A claim written for one anyway is resolved and shown. A
policy may contain only consider rules. The level is part of the rule's
fingerprint, so promoting a rule needs fresh claims.
Rule ids#
A policy's namespace is its path under the policies directory without
.policy.toml; rule ids are namespace/id. Ids use ASCII letters,
digits, -, _ and ., and are unique within a policy across all levels.
File matching#
Globs are relative to the project root. * does not cross /; **
does. A file matched by several policies needs claims for the rules of each.
Supporting context#
Context globs name supporting files a reviewer has to read to judge the
rule. They are part of the subject (see freshness.md), so
editing, adding or removing a matching file makes the claim stale. Context
is only what is declared: snob does no import or dependency analysis. Keep
it to files the judgement really depends on; a broad glob such as **/*
makes every claim stale on every change. Context is not the same thing as
reviewing what a change affects elsewhere. That is a rule of its own (this
repository's is source/downstream-effects), whose claim records the
affected scope. Files that review relied on can be added to the file's
context so the claim goes stale when they change.
Rule fingerprints#
A rule's fingerprint is a BLAKE3 hash of its id, level, description, review text and effective context patterns. Changing any of these makes claims for that rule stale, and only that rule. TOML formatting and comments do not count. Renaming an id makes a new rule.
Coverage policies#
role = "coverage" marks a catch-all policy, typically one that
matches **/* and asks whether each file is covered by the right
policies. Its rules are ordinary obligations, but a file whose only must
or should rules come from coverage policies is listed by snob check
under "Covered only by coverage policies" and reported with
"coverage": "coverage_only". The default is role = "substantive".
Review files#
The routes/rate-limit entry below illustrates multiple references and
assumes an additional matching rule with that id.
# snob/reviews/src/routes/admin.ts.review.toml
context = ["src/db/users.ts"] # optional; widens every subject of this file
[claims]
"routes/no-privilege-escalation" = "local:admin-esc"
"routes/audit-log" = "changeme"
"routes/rate-limit" = ["local:limit-v1", "local:limit-v2"]
A review file is named after its source file. Each entry in [claims] is
provider:id, where snob does not interpret the part after the first :,
or "changeme" for "not reviewed yet". snob init writes the placeholders.
An entry for a rule that does not apply to the file is an error, which
catches typos and removed rules. So is a review file whose source file is
ignored or gone, unless it is marked deleted = true as the record of a
reviewed deletion (freshness.md).
A rule can list several references. That is needed when the trusted base and the change define the same rule id differently, because each definition needs its own claim (freshness.md). Every listed reference counts, in this order:
- an invalid reference, then an unavailable claim, decides the result;
- then a revoked claim, then a rejected one, whatever subject it is about;
- then a satisfied claim bound to the expected subject satisfies the rule;
- then a pending claim leaves it pending;
- then a satisfied claim about another subject makes it stale;
- then
"changeme"leaves it unclaimed.
So a listed rejection cannot be outvoted by another claim. Remove superseded references instead of leaving them listed.
File layout and validation#
snob reads three kinds of file, all strict TOML: policies, review files and
snob.toml. Unknown fields, wrong types, duplicate ids, empty descriptions,
absolute or .. globs and misplaced files are errors (exit 2), and every
problem found is reported in one run.
snob.toml project settings; marks the project root
snob/policies/routes.policy.toml policy, namespace "routes"
snob/policies/api/public.policy.toml policy, namespace "api/public"
snob/reviews/src/routes/admin.ts.review.toml claim references for src/routes/admin.ts
snob/claims/<id>.claim.toml claims for the built-in local: provider
Without --root, snob uses the nearest ancestor of the current directory
that holds snob.toml; if there is none, it uses the current directory.
snob.toml#
# snob.toml
ignore = ["vendor/**"] # never subjects or context files
[paths] # defaults shown
policies = "snob/policies"
reviews = "snob/reviews"
[local]
claims_dir = "snob/claims"
[providers.review-db] # any prefix except "local"
command = ["review-db", "snob-provider"] # argument list, no shell
timeout_ms = 10000 # default
max_output_bytes = 1048576 # default
[ratchet] # optional; see freshness.md
since = "0123456789abcdef0123456789abcdef01234567"
Directories are relative to snob.toml, must stay inside the project and
must not overlap. Moving the policies directory does not change rule ids
or fingerprints. Provider commands are described in claims.md.
Provider names use ASCII letters, digits, - and _; local is
reserved for the built-in provider. A command must name a program, and
timeout_ms and max_output_bytes must be positive.
Which files are checked#
Every regular file under the root is a candidate, except:
- files matching an
ignoreglob; - evidence storage: the reviews and claims directories, and
.git; - untracked files hidden by an ignore file inside the project:
.gitignore,.ignore, and in a git checkout.git/info/exclude. A file git tracks is always a candidate, even if an ignore-file pattern matches it, so ignoring a file cannot hide it from review once it is committed or staged. Outside git nothing is tracked, so ignore files apply to every file.
Global and parent-directory ignore files are never read. Policy files and
snob.toml are candidates like any other file, so a policy can cover them.
Symlinks are skipped, and a path that is not valid UTF-8 is an error.
With --trusted-rev, the base's ignore list applies, and a file that was
a candidate at the base stays one while it exists, even if the change stops
tracking or ignores it.