Skip to content

v0.5.0

Latest

Choose a tag to compare

@github-actions github-actions released this 08 Sep 00:35
· 123 commits to main since this release

[0.5.0] — 2026-09-07

The first release carrying outside contributions. Three people who do not work on this project
wrote fixes that are in this binary — Michael Freeman, PollyBot13 and Andriy Tyurnikov — and three
more found things it got wrong: Cort Fritz, stalep and Jan Mangs. All six are named below, beside
what they found. That is what this number is for.

Added — Elixir, as a first-class indexed language

A vendored tree-sitter grammar and call-graph extraction, with protocol implementations indexed as
their own definitions. Contributed by Michael Freeman
(#43), rebased onto main's tip — the extraction
identity is 78, not the 83 the fork carried — and extended with two gaps the fork could not see from
where it sat. The import edges are described in their own section below.

Added — <recent> answers "what changed", not "what churns"

--rank-by=churn-decay emits a file-level <recent> block ordered by newest commit first rather
than by heaviest weight. A question about what changed recently was being answered with what changes
most often, which is a different question. The MCP instructions carry the deferral hint.

Added — tests-to-run rows in evidence order

A changed test file comes first, then its stem partner, then graph hops, and each row says why it
is there. A test file that is itself in the diff is an obligation on its own evidence. The silent
zero on that surface is fixed: an empty result now says so.

Fixed — named JavaScript and TypeScript import aliases

import { a as b } resolved to the wrong symbol, and the refusal path deleted edges that were
correct. Contributed by PollyBot13
(#45). The refusal now reports which of the three
things it knew rather than collapsing them into one message.

Fixed — five languages were invisible to the unanalyzed-language disclosure

filesByLang was sized with a hardcoded 16 while the Lang enum had grown to 21, so TOML, YAML,
PHP, Lua and Elixir were dropped from the count silently — and two of them were named in
kUnanalyzedLangs, meaning the lens promised to declare them and could not. Found by Cort Fritz
on his own fork. The array is now sized by kLangCount, with a static_assert that fails the build
if the enum outgrows it again. A disclosure surface that under-reports is worse than one that is
absent
, which is why this is the fix in this release that mattered most.

Fixed — the MCP tool schema a strict client refuses

One tool declared a union type that stricter MCP clients reject outright, taking the whole server
down with it (#48, reported by stalep against
opencode with @ai-sdk/google-vertex). Gated by test/mcpstrictschemacheck.sh, written red against
the binary that had the bug.

Fixed — twenty first-run defects a stranger hits and a maintainer never does

PATH not printed on install, a borrowed query in the quickstart, shallow clones mishandled, two
inverted --doctor verdicts, lock-file litter, sidecars that did not say what they dropped, an empty
map that read as an answer, and an upgrade path that left a binary which could not run and reported
success. Found by auditing the install as somebody who had never run it.

Changed — the README leads with the proof

The ten-moments token table and the graphs now sit under the install block: 300 words to the first
piece of evidence instead of 1,009
. Twenty-six prose blocks moved behind <details>, each with a
summary carrying its own number, so the page makes the same case whether or not anything is clicked.
Nothing was removed — the full read is longer than before, because the summaries are additive.

Changed — Graft folded into the lineage ledger as the 42nd repository

A registered head-to-head against Graft 0.17.0 ran, its losses were converted into code, and it was
re-run: 14 of 30, with the placebo arm at 13-12-5. The stop condition fired, so no ranking claim is
published from that round.
What shipped is the two fixes it produced — tests-to-run in evidence
order, and <recent>.

Changed — CI shards its gate suite across runners

Each release leg's gates split across runner jobs, so the workflow's wall clock is one shard rather
than one suite. Main runs are no longer cancelled by the next push.

Changed — every skill description rewritten under the client budget, and one skill folded away

Reported and measured by @jmangs in #49: Codex silently shortens skill
descriptions to fit its context budget, keeping the first ~350 characters of each. All eighteen ripwire
descriptions were over that budget — 18,455 characters authored, 6,300 retained, 12,155 discarded,
fifteen of them cut mid-token. What the truncation removed was the routing boundaries: the "NOT for X,
that's skill Y" clauses, the secondary triggers, the misuse warnings. His diagnosis is the one this round
acted on, and it is worth quoting: the shortened descriptions "do not become literally identical — the
problem is semantic: related skills lose the clauses that distinguish them."

That reframed the task. Eighteen skills whose boundaries need a thousand characters each to explain are
eighteen skills whose boundaries are not carrying their own weight; the budget did not create the routing
problem, it exposed it by deleting the prose that was compensating. So the round asked what set of skills
has boundaries an agent can tell apart in 350 characters, rather than how to compress the existing ones.

Pre-registered before any description was touched (docs/EVALS.md), with a truncation-aware A/B whose
baseline was today's descriptions truncated — the thing users actually have — not today's full text.
The registered set-total ceiling was amended 4,800 → 5,400 and a third blind rater added, both before
measurement rather than after. Three LLM raters, sealed held-out set. The result was a REJECT on the
registered band
, published as such; the parts that stood were kept, including folding
ripwire-efficient into ripwire-orient — the one boundary all three raters independently could not
distinguish. test/skilldescbudgetcheck.sh now pins every description under the budget, with a
binary-backed arm reading the binary's own skill discovery, so this cannot drift back silently.

Added — Bash, Lua, Ruby and Elixir get import/dependency edges (parser version 81)

Four languages that emitted no dependency record on any tree now emit one per directive. Each spells a
real file dependency, and each spells it as an ordinary CALL rather than a reserved statement — which is
why directiveTargetOf had no branch for any of them, and why lintrules.h::dependencyCapable called all
four incapable. That was a true statement about this extractor and a false one about the languages.

Language Directives captured Resolution rule
Bash source FILE, . FILE the argument IS the path — no convention to model. A $VAR/$( … ) anchor is reduced to its literal tail and probed against the includer's directory and every ancestor, unique-or-degrade
Lua require "a.b" package.path's dotted convention (a.ba/b.lua, or the package form a/b/init.lua), probed from the requiring file upward and under the src/ and lua/ source roots
Ruby require_relative, require, load a leading dot means file-relative (the extractor normalizes require_relative "x" to ./x); a bare specifier is searched against the crawl root plus lib/, app/, test/, spec/
Elixir alias, import, require, use the corpus's OWN defmodule index, not a MyApp.Foolib/my_app/foo.ex path convention — so umbrella layouts and generated paths resolve, and a module two files define resolves to neither

Every rule is unique-or-degrade: two candidate files answering one specifier resolve to neither. There
is no basename fallback anywhere in this, which is the one shortcut that would have made all four look
better on a benchmark and been wrong invisibly.

Measured on this repository (ripwire . --deps): 29 source directives across 28 gate scripts, 26 of
them resolved. The 3 that do not are . /dev/stdin <<EOF, an absolute path outside the crawl — shown as a
target row with no edge, never dropped. All 29 specifiers in this tree are $ROOT/…, so a literal-only
resolver would have resolved zero of them.

Disclosed floors. A shell specifier whose FILENAME is variable ("$1", "$d/$n.sh") cannot be
resolved by anything short of running the script: it is captured, displayed, and produces no edge. Ruby's
autoload :Foo, "path" is not captured (its path is argument two). An Elixir alias A.B.C also binds the
local name C, so a later C.f() means A.B.C.f — the FILE edge lands, the NAME alias does not narrow
call resolution, because the call's receiver is not kept by queries/elixir/tags.scm. Quoted Elixir AST
(quote do … end) is descended into, so an alias inside a macro template is captured: the same
union-over-arms posture the preprocessor tables take, a spurious edge rather than a missing one.

Changed — the dependency denominator moved, and now says so

Making four languages dependency-capable changes five denominators and one predicate: --deps's
dep_files=/ccd/acd/nccd, --arch's propagation_cost, and --cochange's pair filter. Any number
recorded against an older build on a corpus holding Bash, Ruby, Lua or Elixir has moved. On this repository
dep_files went 758 → 1392 and nccd 0.68 → 0.39.

--deps therefore publishes <health dep_langs=> — the capable language set, derived from the
predicate itself so it cannot drift from what it documents. A dep_files= number is only comparable across
builds when dep_langs= matches, and until now the set behind it existed only in a source comment.

--cochange's surprising= is now a PAIR question. It was "both sides dependency-capable", which
agreed with the truth only while .sh was incapable. The moment a shell script became capable, that form
would have declared test/foo.shsrc/bar.h a pair whose missing static dependency is evidence — and
no source can name a header. Measured before the change: of 153 dep_capable="0" rows in this repo's top
400, the per-file form would have turned 88 capable and 75 of those are cross-dialect pairs that would
have read as hidden architectural debt. The predicate now also requires a shared dependency dialect, which
additionally fixes 22 pre-existing over-claims of the same shape (.js.h, .py.h, .py.cpp).

Markdown stays excluded, now on the record. Markdown does mint doc→doc link edges — [B](b.md) is a
real edge and the map shows it — so "no import syntax" was never the reason. It is excluded because --deps
and --arch measure change amplification, and a README linking twelve design docs is not twelve files of
it: docs are read, not compiled. JSON/TOML/YAML have no file-level import at all.

Fixed while building this: the root-relative probe every unknown-anchor rule needs was anchored at an
empty base, which is the crawl root only when the root was written as .. The same tree scanned as
ripwire /abs/path resolved 13 of 29 source directives where ripwire . resolved 26. Probing the
includer's ancestor chain instead is root-spelling independent, and each new gate asserts the two spellings
produce identical edges.

Five new gates: test/bashsourcecheck.sh, test/luarequirecheck.sh, test/rubyrequirecheck.sh,
test/eliximportcheck.sh, test/deplangscheck.sh (550 → 555). test/luacheck.sh §2 was inverted — it
used to assert <deps files="0"> on a Lua corpus, which is the assertion that would have kept this defect.

Fixed — Ruby: definitions carry their enclosing class/module, and def name= is indexed and called

Landed from PR #47 (Andriy Tyurnikov), rebased onto the Elixir and ES-import work. Two Ruby extraction
defects, plus one resolver defect that turned out not to be Ruby's at all.

A Ruby def had no scope. src/ingest_sidecap.h set a definition's scope for C++, Python and Rust
only, so no Ruby row ever carried an id=: a Scope::name selector (--expand=B::initialize,
--callers=A::helper) could not address a Ruby method, same-named methods in different classes of one file
folded into a single overloads=N row, and --edit-check=Widget::resize answered "symbol not found".
(PR #47 also listed editcheck.h's implicit-receiver exemption as a dead branch the scope revives. It does
revive it, and it is still inert: Ruby has no implicit receiver parameter to exempt, and the caller test
short-circuits on arityExact == 0 — which cc_paramArityExact gives every Ruby definition, since its
language gate does not list Ruby. Measured incompatible="0" scoped and unscoped, before and after. The
comment there now says so rather than implying a recovered signal.) rubyEnclosingScopeOf (src/ingest_names.h) records
the nearest enclosing class/module: it walks THROUGH class << self, skips the definition's own node
(a class Widget inside module Outer scopes to Outer, never to itself) and takes the last segment of a
class Foo::Bar name, matching the contract C++'s qualifierOf already keeps. A top-level def still has
no scope and no id= — a file is not a scope.

def name=(v) was never indexed, and obj.name = v called the getter. tree-sitter-ruby names a setter
with a (setter) node, which queries/ruby/tags.scm's method pattern did not accept. And obj.name = v
parses as (assignment left: (call method: (identifier))) — the same (call) shape as the read
obj.name — so the call rule captured a reference to name and the resolver handed a WRITE to the getter:
a false edge, not a floor. The setter is now named name= on both sides: the definition from the
(setter) node's own text, the call site by reading the assignment parent (rubyCallIsAssignmentTarget).
self.name = v inside the class pins to Class::name= through Rule 1, and --callers=name= answers.

Three resolver-side changes ride along so the new scope adds precision without losing edges: Ruby call
receivers are classified (src/ingest_binds.hself.m is ThisObj, x.m is NamedVar, a receiver-less
m(args) stays bare); Rule 1 (src/resolve.h) treats a bare Ruby paren call as the implicit-self send it
is, so helper(2) inside class A pins to A::helper as a FACT rather than as a disclosed locality guess
(lpin=); and the S6-C locality tie-break (src/graph.h) no longer lets the caller's own definition win.

The tie-break fix is not Ruby's, and is disclosed as such. A candidate that IS the caller matched itself
on every locality segment, won alone, and was then dropped at emission as a self-loop — the site produced no
edge at all, silently. Ruby's facade idiom surfaced it, but the shape is language-agnostic: the same fixture
in PYTHON goes from edges=0 to an honest 2-way split with amb="1" (test/lpincheck.sh arm (I), the
language-agnostic pin — revert that one line and it goes red before any Ruby gate does). Measured across
eight Ruby-FREE corpora (rocksdb, duckdb, ugrep, django, ccxt, mlflow, cpython and one large
ObjC++ tree —
--no-cache --top-k=100000, every row and every edge compared): the change is edge-ADDITIVE, 0 edges lost
anywhere
, symbols=, unresolved= and external= unchanged, edges= +0.02% (ccxt) to +0.33% (rocksdb),
and locality_pinned= up where an edge that used to vanish is now emitted as a disclosed guess (rocksdb
141 → 587, duckdb 171 → 575, cpython 556 → 709, the ObjC++ tree 100 → 237). A control binary carrying
other change with only that line reverted is BYTE-IDENTICAL to the pre-merge tip on all eleven Ruby-free
corpora — so nothing else in this change moves any other language.

Measured on Ruby 2.6's own stdlib (833 .rb files, --no-cache --top-k=100000, byte-identical across runs,
xmllint --noout clean): symbols= 14220 → 14476 (250 setter rows, 253 definitions, where none were
indexed before); rows carrying id= 97 → 13191; rows carrying overloads= 649 → 194; call edges naming a
setter 0 → 534; ambiguous= 5872 → 5544; edges= 29077 → 28840 — a NET DROP, because a write against a
setter the tree does not define no longer invents an edge to the getter. The id= attribute is what a
scoped row costs
: the full map's est_tokens rose 504107 → 816226 on that corpus and the default 200-row
map's 8796 → 12313 (+40%), the same price Python already pays. Non-Ruby corpora pay ~1% (rocksdb 14652 →
14826).

Stated floors, each pinned by a gate arm so it stays a decision. rubyCallIsAssignmentTarget reads a
plain (assignment) only, so a compound w.count += 1 and a conditional w.count ||= 1 (both
operator_assignment: they read AND write, and one capture carries one name) and a multiple assignment
a.count, b.count = 1, 2 (a left_assignment_list, one level deeper) all keep the getter edge only.
attr_accessor / attr_writer / attr_reader generate their methods at load time and define nothing in
the source text, so they are not symbols and a write against one resolves to an honest NOTHING — unchanged
by this work, and now asserted. And the tie-break fix is the tie-break only: one layer up, tier 1 admits
same-FILE candidates and stops if any exist, so a caller that is the only same-file candidate is still
selected alone and still dropped to nothing (def prerelease=; set.prerelease = v; end in one file with the
real prerelease= in another). Widening tier 1 past the caller would mint a cross-file edge the same-file
tier already outranked, so the honest nothing stands.

kParserVer 79 → 80 with quality.h's kIngestParserVerMirror in the same commit (the fork carried 79,
which the Elixir and ES-import bumps had already taken — re-bumped to the next free number over the merged
tip, per the rule in src/ingest_cache.h); kCacheVersion stays 16, no record shape moved. Gates:
test/rubyscopecheck.sh (scope shapes, the Scope::name selector, the overload split, facade delegation,
Rule 1 pins, a hoist mutation, determinism), test/rubysettercheck.sh (definitions, write vs read edges,
the explicit w.name=(4) and chained w.inner.name = 5 spellings, four stated floors, --callers on both
names, a write-to-read mutation, determinism) and test/lpincheck.sh arm (I). Both new gates were run
against the PRE-fix binary and fail there (18 and 12 failing assertions), which is what makes them evidence.

What's Changed

  • ci: shard each release leg's gate suite across runner jobs by @joyful-ii-V-I in #52
  • Head-to-head vs Graft: losses converted to code, tail fix, lineage by @joyful-ii-V-I in #50
  • fix: resolve named JavaScript and TypeScript import aliases by @PollyBot13 in #45
  • fix(ruby): definition scope, and setter methods by @andriytyurnikov in #47
  • ci: macOS plain leg skips four platform-independent gates; main runs are not cancelled by the next push by @joyful-ii-V-I in #53

New Contributors

Full Changelog: v0.4.0...v0.5.0