Skip to content
Writing anchors

Writing anchors

An anchor is a content-based claim about the code: “in that repo, in those files, something matches this”. Lines move on the first commit; content survives. Write anchors so that a verify failure means the claim is actually in doubt.

Grammar

One leg per line, an HTML comment directly under the claim’s row in .multivac/invariants.md — invisible when rendered, greppable, no parallel file to drift. Verify reads them from the brain’s root *.md, .multivac/*.md and .multivac/changes/*.md; an anchor inside a code fence, indented four spaces or in a file in any other directory is skipped without a word, so its claim just reads as unanchored and a tombstone there blocks nothing:

<!-- @anchor <CLAIM-ID> <repo-key>:<glob> [![<repo-key>:]<glob> ...] /<regex>/[flags] [mode] -->
  • CLAIM-ID is explicit, never inferred from proximity. It is the join key for reporting and for change close.

  • repo-key is the registry key from .multivac/config.yml (api), never the directory name (acme-api). * = every declared repo plus the brain itself.

  • glob is a picomatch pattern over repo-relative, /-separated paths (what git ls-files prints): ** crosses directories, {a,b} alternates, dotfiles match. Not shell, not regex — src/*.ts misses src/lib/git.ts, so write src/**/*.ts.

  • !<glob> excludes, applied after the include. The surviving file set is what gets matched — and what counts toward vacuity.

  • !<repo-key>:<glob> excludes in that repo only. A bare exclusion is repo-relative and bites in every repo the leg evaluates, so under * it exempts the path’s namesake everywhere; qualify it to exempt one repo:

    <!-- @anchor INV-77 *:**.md !brain:07-rules.md /PIN/ absent -->

    “no PIN in any markdown, anywhere — except the page in the brain that carries the tombstone”. A 07-rules.md in another repo is still checked. An exclusion naming an undeclared repo is a parse error naming the key; qualifying an exclusion in a single-repo leg is legal and redundant.

  • Flags: i only.

  • mode: present (default), absent, unique, count=N, each, each!.

  • One include glob per leg. Alternate paths with braces — api:{src,lib}/**/*.ts — never with a second glob; only exclusions repeat.

  • The whole grammar is one screen away: mvac help anchor. Before pinning a count=N, dry-run the leg with mvac count '<repo>:<glob> /<regex>/' — it prints the per-file breakdown through verify’s own matcher, so the ratchet is right the first time.

A claim may carry several legs; legs AND together. On failure the claim inherits the worst failing leg’s severity, and verify reports per leg.

Dialect: POSIX ERE, enforced

The one dialect every engine on every machine executes the same way. \s \b \d \w are rejected at parse time — macOS git grep drops them silently, which turns a tombstone into a vacuous pass. The error prints above the claim count, and a parse error exits 1 in every mode:

$ mvac verify
  parse     .multivac/invariants.md:10 — \s is not POSIX ERE — use [[:space:]]

3 claims · 2 anchored (67%)
  ...

The summary line reads 0 blocking broken · exit 1 · 1 anchor parse errors.

Translate:

PCREPOSIX
\s[[:space:]]
\d[[:digit:]]
\w[[:alnum:]_]
\b(^|[^[:alnum:]_]) … ([^[:alnum:]_]|$)

Four more constructs are refused for the same reason — they mean something in JavaScript and something else, or nothing, to git grep:

WrittenWhy it is refused
(?= (?! (?:POSIX ERE has no lookaround and no non-capturing groups
*? +? ??POSIX ERE quantifiers are greedy; there is no lazy form
\1 … \9POSIX ERE has no backreferences
\t \n, any alphabetic escapePOSIX ERE escapes punctuation only

And the mistake that used to compile: a character class needs BOTH pairs of brackets. [[:digit:]] is the class; [:digit:] is a bracket expression whose members are :, d, i, g, t — so PIN[:digit:] used to become PIN0-9, matching that literal text and never PIN4. It is refused now, in git grep’s own words: character class syntax is [[:digit:]], not [:digit:].

Matching rules you must know

  • .sql files match per statement, not per line. Comments stripped, whitespace collapsed, split on ;. Real DDL splits one GRANT across lines; a per-line tombstone over DDL has an escape by construction. Other files match per line — keep each leg’s pattern on one physical line of the target.

  • Append-only surfaces (migrations) lie to present. A match proves “was built this way”, never “still is”; unique/count conflate history with HEAD. Over migrations either target the latest definition of the object, or use count=N as the ratchet.

  • count=N is a deletion ratchet, never a universal. It counts matches across ALL files the glob matches: it catches removal, not a new file that omits the pattern. Measurement 2 proved it by injection — a privileged: true rogue container added to a default k8s manifest left fifteen anchors green at exit 0. “For every file, P” is each; “for no file, P” is each! — quantified per file, failing files named. The count dry-run nudges you here on purpose: every summary that offers a ratchet ends by naming each/each!, so a universal-shaped rule is never pinned as a ratchet by accident.

  • Vacuous globs fail loudly. A glob matching zero tracked files is vacuous for absent, unique, count and each — and for present when its regex has nowhere to move to either (see moved). absent/count/each block — a directory rename must not silently green a tombstone, and a universal over nothing proves nothing. present/unique are reported, and block only under --strict:

    vacuous   INV-01 [absent] .multivac/invariants.md:7 · glob matched no tracked files — a rename greens this tombstone silently; fix the glob · blocking
  • moved self-heals. A present leg with no match inside its glob, whose matches elsewhere fall in exactly one file — of the include’s own kind (the same trailing extension), never inside .multivac/ — gets its glob rewritten in place, exit 0. verify --check and a run from a consumer repo report the move and leave the glob alone:

    moved     INV-01 [present] .multivac/invariants.md:6 · glob rewritten to sql/migrations/001_accounts.sql — review the diff

    Review the diff like any other edit and let it ride the same branch. With no such file the leg stays broken — vacuous, if its glob is empty. With several files it is broken too, and the first three are named.

Choosing the mode

Anchor to contracts, not implementations — migrations, schemas, route tables, config keys, GRANTs. A contract site is simultaneously the cheapest thing to read and the most stable thing to anchor to; a widget body breaks on Tuesday. If you find yourself anchoring an implementation, first ask whether a contract site exists for the claim; anchor the implementation only when there is none, and expect churn.

you are pinningmode
the rule was enacted (the revoke, the constraint, the check exists)present
a dead mechanism stays dead — the tombstoneabsent
a single source of a valueunique
“never again” over append-only history; a sanctioned exception stays the only onecount=N
every matched file satisfies the rule (“every manifest declares limits”)each
no matched file carries the pattern, and the offender is named per fileeach!

absent, count and each block by default; present and unique report, and block only under --strict. Put the teeth in the blocking modes and let present document the enactment. Without that asymmetry every refactor turns the check red and someone disables the tool in week three — lint-family tools die of noise, not of bugs.

The universal: each and each!

each holds iff every file the glob matches (after exclusions) contains at least one match; each! iff every such file contains none. A violation is a hard failure and the failing files are named, first few + count:

broken    INV-90 [each] .multivac/invariants.md:9 · each: 1 of 4 files lack the pattern (api:k8s/rogue.yaml) — add it there, or exclude the file with !<glob> · blocking

Exempt a sanctioned file with an exclusion (!api:k8s/debug.yaml). Inside matched .sql files the per-statement normalization applies as everywhere else. Plain absent also says “nowhere in the glob”; reach for each! when you want the report per file — each offender once, as “N of M files” — rather than the first three matches. What each cannot say — deliberately — is a cross-file relation (“the vendored copy equals the root copy”, “the env var matches the containerPort”). That is a different primitive, not a quantifier; leave such claims unanchored rather than fake them.

| INV-90 | Every deployment is confined: limits declared, never privileged. | published | active | 2026-08-14 | map |
<!-- @anchor INV-90 api:k8s/*.yaml /limits:/ each -->
<!-- @anchor INV-90 api:k8s/*.yaml /privileged:[[:space:]]*true/ each! -->

The legs pattern

One claim, several legs, all must hold. The canonical shape — enactment, tombstone, ratchet:

| INV-01 | Nobody has UPDATE on `accounts`, not even `admin_role`. | published | active | 2026-08-13 | map |
<!-- @anchor INV-01 api:db/migrations/*.sql /revoke[[:space:]]+update[[:space:]]+on[[:space:]]+[^[:space:]]*accounts/i -->
<!-- @anchor INV-01 api:db/migrations/*.sql /grant[[:space:]]+update[[:space:]]+on[[:space:]]+[^[:space:]]*accounts/i absent -->
<!-- @anchor INV-01 api:db/migrations/*.sql /update[[:space:]]+accounts/i count=1 -->

The present proves the rule was enacted; the absent kills the re-grant; the count=1 pins the one sanctioned update accounts in history — any new occurrence breaks it. A tombstone can cross repos:

<!-- @anchor INV-42 *:AGENTS.md /(^|[^[:alnum:]_])legacy_token([^[:alnum:]_]|$)/ absent -->

The dead-terms dictionary is not a separate feature: it is these absent legs, accumulated on retired rows, blocking forever.

Before committing an anchor: two self-checks

Run both. An anchor that fails either is not ready.

  1. Misfire check — name a refactor that breaks it while the claim stays true. File rename? The glob self-heals only for present with one matching file — an absent leg’s glob renames to vacuity and blocks. Statement rewritten in equivalent SQL? Widen the pattern to the invariant part ([^[:space:]]*accounts survives schema-qualification; a literal public.accounts does not).
  2. False-green check — name a violation that passes it. A grant via GRANT ALL? Your absent on grant update misses it — add a leg. A bypass that never says the guarded word (an upsert instead of an update)? Kill it with its own absent leg. A directory rename? Vacuity catches the glob, but only if the glob is tight enough to go empty.

If you cannot name either, you don’t understand the claim yet — reread the contract site before anchoring it.

Not everything anchors

A meta-rule or a process rule anchors to nothing. Leave it unanchored: it is legal and counted, and the report never pretends to have verified it. Do not invent a decorative anchor to move a coverage number.