Skip to content

Latest commit

 

History

History
69 lines (60 loc) · 3.67 KB

File metadata and controls

69 lines (60 loc) · 3.67 KB
title How the gate works
description Locate, canonicalize, hash, compare - the four steps behind surf check, and the versioned JSON seam every plugin reads.

The gate runs in four steps.

  1. Locate. tree-sitter parses the file and resolves the at: path (a qualified file > A > B path, with @N for genuine name collisions) to the exact node span. A scope is treated as a set of nodes, so a type and its impl/methods - which share a name - disambiguate by path: Type alone is ambiguous, Type > method is unique. In Python the path also resolves non-callables: module constants, type aliases, and class attributes.
  2. 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; operators, keywords, and literal values are kept verbatim. Identifiers split into two kinds: a bound name (the symbol's own name, parameters, locals, loop/destructuring binders) is alpha-renamed to a positional placeholder, so a consistent local rename yields the same tokens; a free name (external members, call targets, types, enum/constant references, object keys, decorators) is kept verbatim, so re-pointing a span at a different symbol - PointsTier.TIER_1TIER_2, getHighestgetLowest, @cache@lru_cache - changes the hash even when the name occurs once. (This bound/free split is the v2 recipe; see Hash recipes.)
  3. 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.
  4. Compare against the stamp stored in the frontmatter (written by surf verify). The stamp carries its recipe - a v2 stamp is prefixed 2:, a bare hex stamp is an older v1 - and is verified under its own recipe, so existing v1 stamps keep passing until surf verify upgrades them. 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

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.