doppel

command module
v0.1.1 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Aug 21, 2026 License: MIT Imports: 3 Imported by: 0

README

doppel

A CLI tool that detects structurally similar functions across a Go codebase. It helps identify duplicate logic and refactoring opportunities by fingerprinting each function from its AST and cross-checking matches against call-graph context — rather than by text matching.

Everything runs locally and offline: no models, no network, no cache. The same source always produces the same report.

For a detailed breakdown of the pipeline internals, see How Doppel Works.

Quick Start

Prerequisites: Go 1.25+

go run . analyze .

This scans the current directory, fingerprints every Go function, and prints the most similar pairs with the evidence behind each match. Add --output report.md to also write a Markdown report.

Real-world examples

examples/ holds doppel's actual output on seven pinned public Go projects, ordered old-and-complex to new-and-narrow — from moby (8003 functions, a decade of accretion) down to conc (81 functions, one idea, written recently). Each report is regenerated by task examples, never hand-edited, and every pair it names can be opened in the corresponding pinned tree.

The same directory carries the per-stage performance numbers across that ladder and a committed golden labels file: a human review of cobra's ranking that task golden scores the current pipeline against.

Installation

Download a prebuilt binary from the latest releaselinux, darwin and windows, on amd64 and arm64. Extract it and put doppel on your PATH:

tar -xzf doppel_*_linux_amd64.tar.gz
sudo install doppel /usr/local/bin/

Every release also ships checksums.txt:

sha256sum -c checksums.txt --ignore-missing

Or install with Go:

go install github.com/LukasSelin/doppel@latest

Or build from source:

git clone https://github.com/LukasSelin/doppel
cd doppel
go build -o doppel .

doppel version prints the build identity. That string is recorded in every snapshot, and a session baseline is discarded when it changes — so upgrading mid-session correctly invalidates the baseline instead of comparing measurements across a scoring change.

Usage

doppel analyze <path> [flags]
Examples
# Analyze current directory with defaults
doppel analyze .

# Lower the threshold to catch more subtle similarities
doppel analyze ./src --threshold 0.50

# Keep only pairs that also share architectural context, and save a report
doppel analyze . --struct-min 0.4 --output report.md

# Print the vocabulary scoring is based on, and check it is consistent
doppel ontology --defs

# Before writing a function, ask whether the repo already has one like it
doppel query --near billing . < draft.go
Querying before you write

doppel query reads a Go snippet — the function you are about to write — and reports the corpus functions most related to it by structure, concept tags and calls:

query: cmd.validateHookSetup — tags: validation
  role: orchestrator   resolved calls: 3

Corpus: 304 functions. 5 related functions:

#1  cmd.hookParams  cmd/config.go:130
    evidence: 69.7 nats (shape 59.1, concept 1.4, call 9.2)  code-shape: 0.49  locality: 1.00
    tags: validation   role: orchestrator

Matches are ranked by evidence boosted with locality — the fraction of the snippet's resolved call neighborhood the match inhabits — so architecturally near code outranks equally-similar code from elsewhere. --near names the package the function will live in: a bare snippet is wrapped in it, and its bare-name calls resolve to that package's functions, which is what locality is built from. Include the snippet's imports — calls into imported packages only count as evidence when the import that binds them is present.

Two scores per pair

Every reported pair carries two independent numbers:

  • Code similarity (Score, gated by --threshold) — how alike the two bodies are, from the AST fingerprint. The report breaks it into its components: ast (3-gram shingle overlap), flow (control-flow shape), sig (parameter and result types), and size (relative body size, shown for context but not scored).
  • Structural overlap (gated by --struct-min) — how much architectural context the two share: callees, callers, intent patterns, role, package, and what their own callers and callees do. Intent patterns, roles and receiver types are matched through a concept hierarchy rather than compared as strings, so two functions doing related work — one hitting a database, the other a cache — score partial credit instead of zero. Every graded match comes with an evidence line saying which ancestor relates the two and how strongly.

A high code score with low structural overlap means two lookalike bodies in unrelated parts of the system. High on both is the real merge candidate.

Flags
Flag Default Description
-t, --threshold 0.60 Minimum code similarity score to report (0.0–1.0)
-n, --top 20 Maximum number of pairs to show (0 for no limit)
--struct-min 0.0 Minimum structural overlap score (0.0–1.0) to keep a pair
--min-nodes 12 Skip functions whose body has fewer than this many AST nodes. Guards against one-line accessors, which match each other perfectly and would otherwise flood the report
-o, --output (disabled) Write report as Markdown to this file. The stdout report is still printed
--format text Stdout format: text or json. The JSON form is a deterministic snapshot of the whole run — every function, its concept tags and role, and every reported pair
--config .doppel.json if present Path to a JSON config file
Configuration

Any flag above except --config can be set in a .doppel.json at the repo root. Keys are kebab-case, mirroring the flag names, and an explicit CLI flag always wins over the file:

{
  "threshold": 0.65,
  "top": 10,
  "struct-min": 0.4,
  "output": "doppel-report.md"
}

A missing config file is not an error; malformed JSON is.

One key has no flag behind it: hook-notify (agent | user | off) decides who the plugin's Stop hook reports to. See plugin/README.md — reaching the agent costs an extra turn, so it is worth understanding before leaving it on the default.

Use as a Claude Code plugin

The same analysis can run automatically around a coding session, answering questions it is otherwise easy to skip: does this codebase already have a concept for what I am about to write, does the file I am about to edit have twins, and what did I just do to its duplication surface.

The plugin shells out to the doppel binary and does not bundle one, so install it first — a release download or:

go install github.com/LukasSelin/doppel@latest
claude plugin marketplace add LukasSelin/doppel
claude plugin install doppel@doppel

Four hooks, placed by when a fact can still change what gets written:

  • SessionStart — the corpus inventory: which concept tags the repo carries, which it has none of, the role distribution.
  • UserPromptSubmit — the duplication facts for the packages your message mentions, and nothing else. Silent when it recognises none.
  • PreToolUse on Edit/Write — immediately before a file changes, the merge-worthy twins of the functions in it. Advisory only; it never blocks an edit.
  • Stop — what the session has done to the duplication surface, leading with the pairs it can trace to a function you actually edited. Prints nothing on turns that changed nothing.

Each is driven by a doppel hook <name> subcommand reading a Claude Code hook payload on stdin and writing a hook response on stdout. None ever exits non-zero: a measurement must not be able to break a session.

See plugin/README.md for what the output means and how to read it honestly, and Hooks and the Causal Window for why each hook fires where it does.

Documentation

The Go Gopher

There is no documentation for this package.

Directories

Path Synopsis
internal
bench
Package bench is the measurement harness: a golden-ranking scorer, a ladder of pinned public Go corpora, per-stage performance benchmarks over them, and the generator behind the reports in examples/.
Package bench is the measurement harness: a golden-ranking scorer, a ladder of pinned public Go corpora, per-stage performance benchmarks over them, and the generator behind the reports in examples/.
culture
Package culture models the repository's local conceptual practice from counts alone: which concepts, roles, and calls co-occur beyond chance (ecology), and how each concept is normally realized here (prototypes and typicality).
Package culture models the repository's local conceptual practice from counts alone: which concepts, roles, and calls co-occur beyond chance (ecology), and how each concept is normally realized here (prototypes and typicality).
fingerprint
Package fingerprint builds deterministic static summaries of Go function bodies and scores how similar two of them are.
Package fingerprint builds deterministic static summaries of Go function bodies and scores how similar two of them are.
ontology
Package ontology is Doppel's formal vocabulary: the entity kinds, typed relations, intent concepts and structural roles the rest of the pipeline reasons about, declared once with definitions and machine-checkable axioms.
Package ontology is Doppel's formal vocabulary: the entity kinds, typed relations, intent concepts and structural roles the rest of the pipeline reasons about, declared once with definitions and machine-checkable axioms.
retriever
Package retriever generates candidate pairs for the expensive structural comparison stage.
Package retriever generates candidate pairs for the expensive structural comparison stage.
snapshot
Package snapshot captures one complete doppel analysis run as plain data, so that two runs can be compared.
Package snapshot captures one complete doppel analysis run as plain data, so that two runs can be compared.

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL