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:

  1. an invalid reference, then an unavailable claim, decides the result;
  2. then a revoked claim, then a rejected one, whatever subject it is about;
  3. then a satisfied claim bound to the expected subject satisfies the rule;
  4. then a pending claim leaves it pending;
  5. then a satisfied claim about another subject makes it stale;
  6. 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 ignore glob;
  • 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.