How the gate works
The gate runs in four steps.
- Locate. tree-sitter parses the file and resolves the
at:path (a qualifiedfile > A > Bpath, with@Nfor genuine name collisions) to the exact node span. A scope is treated as a set of nodes, so a type and itsimpl/methods — which share a name — disambiguate by path:Typealone is ambiguous,Type > methodis unique. In Python the path also resolves non-callables: module constants, type aliases, and class attributes. - Canonicalize. Walk that span’s syntax tree into a token stream. Whitespace and comments
aren’t in the tree, so they drop out for free; identifiers are alpha-renamed to positional
placeholders (a consistent rename yields the same tokens, swapping two names does not);
operators, keywords, and literal values are kept verbatim. Python decorators are part of the
span, and a decorator’s name is kept verbatim — so swapping
@cachefor@lru_cache, or@staticmethodfor@classmethod, changes the hash. - Hash. SHA-256 of that stream, truncated to 12 hex. A list
at:combines its sites into one hash, so the claim is stale if any listed span changes. - Compare against the hash stored in the frontmatter (written by
surf verify). Equal → pass; different → block.
Quiet on cosmetics, loud on logic — and reproducible, because the parser ships inside the binary and is version-pinned. There is no separate formatter or language server in CI to skew the result.
A claim can opt a narrower scope with ignore_literals: true, which excludes string-literal
content from its hash (a copy edit no longer re-opens the gate; logic still does). The stored
hash is computed in that mode, so the option lives on the claim.
The JSON seam
Section titled “The JSON seam”surf check --format json is the seam every optional layer reads. The payload is a versioned
envelope:
{ "version": 1, "divergences": [ { "hub": "hubs/auth.md", "claim": "refresh rotation is single-use; reuse triggers global logout", "at": "src/auth/refresh.ts > rotateRefreshToken", "kind": "changed", "old_hash": "9b1c33ade8f1", "new_hash": "4d5e6f2a0b7c", "new_code": "function rotateRefreshToken(...) { ... }", "prose": "refresh rotation is single-use; reuse triggers global logout", "magnitude": "small" } ]}Per diverged claim: hub, claim, at, kind (changed | unverified | unresolvable),
old_hash, new_hash, old_code, new_code, prose, magnitude, and a detail string on an
unresolvable claim. magnitude (small / medium / large) is advisory triage only — it helps a
human decide which blocked claim to read first, and it never affects pass/fail.
Stability. version is the contract version. Within a major version the shape is
additive-only: new optional fields may appear, but existing fields are never removed, renamed,
or repurposed. A breaking change bumps version. Consumers should read .divergences and tolerate
unknown fields.