opentuple

module
v0.2.0 Latest Latest
Warning

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

Go to latest
Published: Nov 5, 2026 License: Apache-2.0

README

OpenTuple

Learns your repository's conventions from its code, and holds AI coding agents to them.

CI License Go

Every codebase has rules nobody wrote down: this package always closes rows and checks rows.Err(), that one logs through slog, receivers of Store are always s, tests here use testify. AI agents break them constantly, because they cannot see them.

OpenTuple finds those rules by reading the code — only rules that at least 98% of a directory already follows — and checks every edit an agent makes against them. When an edit breaks one, the agent is told, with the evidence, and fixes it. One Go binary, runs locally, no configuration.

What the agent sees

From a real run (Claude Code, headless, Claude Haiku). The agent was asked to add a summary printer to a CLI package. Its first version printed with a bare fmt.Fprintf. Every other printer in that package styles its output through the theme, so the hook answered:

internal/cli/summary.go:9: PrintIndexSummary calls fmt.Fprintf but never Render
  Rule: In internal/cli, functions that call fmt.Fprintf also call Render. [helper_pairing]
  Evidence: 12 of 12 functions in internal/cli that call fmt.Fprintf also call Render (100%); 4% of the functions that do not call fmt.Fprintf call Render.
  Example: reportWrittenFiles (internal/cli/generate.go:218) follows this rule.

The agent's next edit wrapped the text in the package's style (StyleDim.Render(...)), the check passed, and the session ended. Nobody told it about the theme; the repository did.

Quickstart

go install github.com/CtrlAltDelete-Ops/opentuple/cmd/opentuple@latest
cd your-repo
opentuple init .

Then start a new Claude Code session in the repository. init does three things:

  1. Indexes the repository into ~/.opentuple/repos/ (Kubernetes, 9,711 files: 27 seconds on an Apple M3).
  2. Writes convention files agents read at session start: a CLAUDE.md per directory that has rules, and one flat AGENTS.md for tools that read a single file.
  3. Installs the check hook in .claude/settings.local.json. That is Claude Code's personal settings file: the hook names your binary and your checkout by absolute path, so it must not be committed, and init makes sure git ignores it. Each teammate runs opentuple init . once.

Re-run opentuple init . after large changes; re-indexing is idempotent.

Not on Claude Code? opentuple init . --git-hook adds a pre-commit hook that runs the same check on staged files, and AGENTS.md works with Codex, Cursor and other tools that read one.

Already have a CLAUDE.md? OpenTuple never edits a file it did not write. It puts its rules in CLAUDE.opentuple.md beside yours; add the line @CLAUDE.opentuple.md to your CLAUDE.md to import them.

What it enforces

Rules are mined per directory, with test files as their own scope, so a convention of your tests is never imposed on your library code.

Rule type Example of what gets flagged
helper_pairing calls QueryContext but never Err, where every function here that queries also checks rows.Err()
family_dominance calls log.Printf, but logging in pkg/svc goes through slog
wrapper calls text.Pluralize directly, where 23 callers go through the project's Pluralize
receiver_name names a Client receiver cl, where 63 of 63 methods use c
doc_comment an exported function without a doc comment, or one that doesn't start with its name
error_handling swallows an error where this package wraps or returns every one
deferred_call calls reg.Verify(t) without defer, where every test here defers it
test_helper a test helper that never calls t.Helper()
file_header a new file without the license header nearly every file carries
helper_absence calls panic, console.log, eval, print… where no function here does

Opt-in, because they are useful in some repositories and noisy in others: --io (a function doing I/O in a package that never does), --duplicates (a copy of an existing function, identifiers renamed), and --absences (any helper or import the directory never uses, not only banned builtins).

Silence is a feature. A rule exists only where at least 98% of a directory's functions follow it, with at least 10 of them, and the evidence is clear-cut: a pairing must beat the paired helper's base rate by 30 points, an absence needs enough functions that would be expected to call it. A false alarm costs more than a missed one, because the first false alarm teaches people to turn the check off. On a small or inconsistent repository OpenTuple may find nothing to enforce, and then it says nothing.

Does it work?

It stays quiet on code that follows its own conventions. opentuple check --all audits a repository against the conventions mined from it. By construction up to 2% of a rule's own directory may deviate, so this is the noise floor a user would see on unchanged code:

Repository Files checked Flags
junegunn/fzf 79 0
cli/cli (gh) 860 3
kubernetes v1.29.0 7,845 49
10 JS/TS repos (hono, express, koa, fastify, axios, zod, zustand, immer, preact, trpc) 3,191 0

The 49 in Kubernetes are minority deviations: 31 test functions using a different assertion library from the rest of their package, 11 doc comments, 3 receiver names, 2 pairings, 1 logging call outside klog, 1 error-handling site. A 29-repository sweep across six languages in October left 22 flags in total, each a minority deviation or a deliberate exception.

When an agent slips, the hook catches it. With the hook off, agents broke one of the repository's conventions in 11% of runs. With the hook on, every violation it caught was fixed on the agent's next edit.

Telling agents the rules up front did not. In July we tested handing the agent the mined conventions just before each edit (the design this project started with; 6 blind-scored runs on Kubernetes with Claude Opus): it wrote the same code with or without them. That result is why the check exists. The convention files generate writes are the same kind of up-front guidance and have not been shown to help on their own; they are still written because they cost nothing and some tools have no hook.

Commands

Command What it does
opentuple init <path> index if needed, write convention files, install the Claude Code hook (--no-generate, --no-hook, --git-hook, --dry-run, --reindex)
opentuple index <path> index or re-index (--mode fast|balanced|quiet)
opentuple generate <path> write the convention files only (--format claude|agents|both, --output <dir>, --dry-run)
opentuple check <file>... check files against their directories' conventions (--staged, --all, --hook)
opentuple rules <path> list the rules check enforces on a file or directory, with the evidence
opentuple status <path> show what is indexed

check exits 0 when clean, 1 on violations, 2 on an error. With --hook it exits 2 on violations and 1 on an error, because Claude Code shows a hook's stderr to the agent only on exit 2. Every command writes to stderr and leaves stdout empty, so OpenTuple is safe inside pipes and hooks.

Configuration

Nothing is required. To tune the check, add .opentuple.json at the repository root:

{
  "min_confidence": 0.99,
  "disable": ["doc_comment"],
  "enable": ["io_boundary"],
  "ignore_paths": ["vendor", "*.pb.go"]
}

The convention files follow the same settings, so they never state a rule the check would not hold an agent to. To waive rules for one function, put // opentuple:ignore (all rules) or // opentuple:ignore receiver_name (one type) in its doc comment; // opentuple:ignore-file near the top of a file skips the file.

How it works

A concurrent pipeline — gitignore-aware walker, a tree-sitter parser pool, one batched SQLite writer — extracts every function with its calls, a behavioral fingerprint, and per-file facts (imports, header). Generated files are skipped. A miner then finds, per directory and per test partition, what nearly every function does: which helpers pair, which member of an API family wins, what receivers are named, how errors are handled. check re-parses only the edited file with the same extractor and compares it with the rules of its directory, in tens of milliseconds (0.08 s for a 3,000-line file against the 52,000-function Kubernetes index).

Output is deterministic: the same repository produces byte-identical rules on every run.

Languages and limits

Go, Python, JavaScript, TypeScript, Rust and C, via tree-sitter. Go has the deepest coverage (receivers, test helpers, error wrapping). In JavaScript and TypeScript, functions are found wherever they are declared or bound to a name (const f = () => {}, exports.f = function () {}, class fields); callbacks passed inline are part of the function around them. .tsx files are parsed with the TypeScript grammar, which has no JSX, so JSX-heavy components are only partly seen. Re-indexing is not incremental yet: re-run init or index after large changes.

Everything runs on your machine. OpenTuple makes no network requests; the index lives in ~/.opentuple/repos/ and can be deleted at any time.

License

Apache-2.0

Directories

Path Synopsis
cmd
opentuple command
internal
check
Package check compares the functions in a source file against the mined conventions of the subsystem the file belongs to and reports the ones they break.
Package check compares the functions in a source file against the mined conventions of the subsystem the file belongs to and reports the ones they break.
cli
generate
Package generate curates mined patterns into the small, high-value subset worth placing in agent context files, and renders them as CLAUDE.md / AGENTS.md.
Package generate curates mined patterns into the small, high-value subset worth placing in agent context files, and renders them as CLAUDE.md / AGENTS.md.
indexer
Package indexer is the indexing pipeline that walks repositories, parses sources, and produces fingerprints for the store.
Package indexer is the indexing pipeline that walks repositories, parses sources, and produces fingerprints for the store.
store
Package store owns the SQLite database that holds an opentuple index.
Package store owns the SQLite database that holds an opentuple index.
types
Package types holds value types shared between the indexer and store packages, kept neutral to avoid an import cycle between them.
Package types holds value types shared between the indexer and store packages, kept neutral to avoid an import cycle between them.

Jump to

Keyboard shortcuts

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