Repository navigation
OpenInvar 0.3.0
OpenInvar builds a deterministic symbol relationship graph for a repository and gates CI on it. No model participates in graph construction or in any gating decision — identical input produces identical output, byte for byte, because you cannot gate CI on an opinion.
This release closes the gap between what the tool claimed and what it could show.
Seven of eight languages now resolve
Java and C# were promoted from Tier 2 to Tier 1. They parse the same as before; what changed is that imports, aliases and declared types now resolve, so gates may act on them.
| Tier 1 — resolved | Tier 2 — structural |
|---|---|
| TypeScript / JavaScript, Python, Rust, Go, C / C++, Java, C# | Ruby |
Ruby is the honest caveat rather than a backlog item: autoloading, monkey-patching and method_missing leave a large share of its references undecidable statically, so it may stay Tier 2 rather than be promoted on a claim the analysis cannot support.
C# needed three rules nothing else here needed — using names a namespace rather than a type, so the ambiguity Java reaches only through wildcards is C#'s ordinary case; enclosing namespaces are in scope with no using at all; and a partial class split across files is one type with the union of its members.
Enforcement
openinvar check reads openinvar.toml and returns one of four verdicts per rule — satisfied, violated, inconclusive, skipped. Only violated fails a gate. An undecided region reports as undecided rather than as a pass.
Ten rule kinds: forbid-dependency, forbid-reference, no-field-removal, max-fan-in, max-fan-out, layers, independence, no-cycles, requires-dependency, no-orphans, requires-test, naming-convention.
openinvar audit detects changes that look made to pass a check rather than to work, with four detectors: test-tampering, contract-erosion, dead-on-arrival and assertion-removal (a test that kept its coverage but stopped checking anything). Three more are held back with their reasons recorded in the source — a detector that cannot be built honestly is named as absent rather than shipped weak.
This repository gates its own CI on its own rules.
Honest measurement
status now reports two figures, because one of them was being read as something it is not:
Resolved 99.8% (6019 of 6031 edge(s))
References bound 69.5% (6019 of 8659 reference(s))
2640 reference(s) named in source bound to nothing in the graph.
The first is the share of recorded edges that resolved. An edge exists only once something bound it, so a reference the analysis could not place leaves no trace — it never enters that denominator, and failing to bind more references makes the percentage go up. The second supplies the missing denominator.
An unbound reference is not automatically a defect: a type from a package whose source was never analysed is indistinguishable, to an adapter, from one it should have found. Read it as an upper bound on what is absent.
There is also now an evaluation against four real repositories nobody here wrote — rust-lang/log, spf13/cobra, pallets/click, google/gson — with thresholds fixed in eval/criteria.md before each run, and the runner and summariser kept in separate files so a threshold cannot move in the same edit that produces the result it judges. Determinism held 32 of 32 revisions byte-identical. Reference coverage came in at 63.2% against a 65% pass line; that is recorded as an investigate verdict with the investigation, not scored as a pass.
Also new
analyze --at <rev>— analyse the tree at a revision instead of the working tree, without disturbing the working graph.openinvar context— a minimal working set for orienting on a symbol or a change.openinvar tests <symbol> | --diff— which tests exercise a symbol, via realtestsedges rather than filename guessing.openinvar impact <file>— what depends on one file.- Two MCP tools,
graph_diffandcheck_rules. - Agent hooks for Claude Code and others, a GitHub Action, and contributor documentation.
Upgrading
The project was renamed from graphyn. The binary is openinvar, the config is openinvar.toml, the crates are openinvar-*. Commands are otherwise unchanged.
The graph store moved to .openinvar/graph.db — a SQLite file, not a directory. There is no migration, deliberately: the graph is derived data that rebuilds in about a quarter of a second, so converting a cache would be wasted work. An old store directory is reported, not deleted — a read command names it and says to run openinvar analyze, and .graphyn/db is recognised too, so an upgrade from before the rename gets the same message rather than a bare "no graph found".
Snapshot format version 5. Older snapshots are read, with fields added since recorded as absent rather than as zero — "this graph does not know" and "this graph counted none" are different answers.
One binary per platform, carrying every language. The per-language Cargo features still work — cargo install openinvar-cli --no-default-features --features python builds a Python-only binary — but the released artifact no longer omits anything the README describes.
Install
# macOS / Linux
curl -fsSL https://raw.githubusercontent.com/JeelGajera/OpenInvar/master/install.sh | bash
# Windows (PowerShell)
irm https://raw.githubusercontent.com/JeelGajera/OpenInvar/master/install.ps1 | iex
# Cargo
cargo install openinvar-cliQuick start
openinvar analyze . --snapshot HEAD # record the graph before the change
openinvar audit . --base HEAD --head worktree # what did the change actually do
openinvar check . # gate on the rules in openinvar.tomlFull detail in CHANGELOG.md, including the fixes and the limits each one left behind.