Repository navigation
Detect duplicate/divergent implementations: span hash can't tell a shared import from a local copy #23
Description
Summary
An anchored span's hash is location-blind to symbol resolution: a function that calls foo(...) hashes identically whether foo is imported from a shared module or re-declared as a byte-identical top-level function in the same file. As a result, an anchor on the caller cannot express or guard the very common architectural invariant "this uses the shared implementation, not a local duplicate."
Duplicate-implementation drift (two copies of the same logic silently diverging over time) is one of the most common ways documented behavior goes stale — and it's exactly the case surface currently can't see.
Environment
surf 0.1.0, prebuilt binarysurf-aarch64-apple-darwin(curl installer)- macOS (Apple Silicon, arm64)
- Target: TypeScript (tree-sitter TS grammar)
Concrete repro (observed on a real codebase)
A server action claimEntry derives a value via findCurrentEvent(bootstrap.events). We anchored the caller:
- claim: "... derives the current gameweek via findCurrentEvent ..."
at: "src/lib/invite/actions.ts > claimEntry"- Source uses
import { findCurrentEvent } from "@/lib/fpl/utils";→surf verifystamps hash H. - Replace the import with a byte-identical top-level copy in the same file:
function findCurrentEvent(events: {...}[]): number { /* same body */ }
surf check→ GREEN (the caller's span hash is still H — the import line and the top-level
function definition both sit outsideclaimEntry's span, and the call expression is unchanged). - Only when the duplicate is declared inside
claimEntry's body does the span change →DIVERGED.
So surface neither flags introducing a top-level duplicate nor removing one — both directions stay green.
Why it matters
- "Uses the shared util / delegates to X / no copy-paste of this logic" is a frequent, high-value
architectural claim, and it's silently unenforceable today. - It's the inverse of the gate's purpose: the prose can assert a dependency that the hash can't back up,
which is the kind of false confidence the README's honesty section warns about.
Proposed directions (any one)
- Per-anchor dependency assertion — e.g.
calls:/references:on an anchor;lint/check
fails if the named symbol isn't imported or called within the span's resolved scope. - Repo-wide duplicate-implementation lint — surface already computes subtree hashes; flag when two
function/method subtrees in different locations share an (identical or near-identical) hash, so
copy-paste forks surface as a warning. - Resolution-aware span hashing — when hashing a span, resolve called identifiers to their
definition sites and fold the definition's hash (or its module path) in, so import-vs-local-copy
yields different hashes.
Related
- Ties into Non-logic edits (string literals) trip the gate — consider magnitude-aware policy or literal scoping #21 (span granularity / signal-to-noise) and the resolution rules surfaced in Anchors can't resolve const-bound function symbols (only function decls & type aliases resolve) #20
(function/typeresolve,const-bound don't).
Found while dogfooding surface on a real TypeScript codebase (OffsideFPL) — specifically while
refactoring a duplicated private function onto a shared util and discovering the gate stayed green
through the whole refactor.
Activity