Releases: RexBytes/stitchgraph
Release list
stitchgraph v3.27.1 — release notes
stitchgraph v3.27.1 — release notes
The second dogfood patch. v3.27.0 was run on itself with the full battery
(research/15-dogfood-v3.27.md) — a controlled before/after of the D2 dedup, since
research/14 measured the same codebase pre-refactor.
Fixed
structure_common.parse_treewas dead on arrival. The D2 stage-2 patch added the shared
walk-guard helper but the transformation script never wired the nine_walkfunctions to call
it, andruff --fixthen removed the unused imports — hiding the slip from every
output-equivalence gate, because dead code has no outputs.find_stalecaught it statically;
find_gapscorroborated at runtime (untested_dead= exactly this + the one known advisory).
It now guards all nine walk entries as intended, and the seven remaining_walk-localtext
helpers delegate to the sharednode_text(closing the residual clone hubsorientstill
ranked at fan_in 97).
What the run verified (the interesting part)
- Behaviour-preservation, measured at runtime: intrinsic dimensionality 27 → 27 across a
~400-line refactor — the strongest runtime statement of "behaviour-preserving" available,
complementing the byte-identical output oracles. coverage_drift's first real cross-release use narrated the dedup from coverage alone:
lost = the nine deleted per-language iterator/closure copies; gained =structure_common.*.
A behavioural changelog derived without reading the diff.- The refactor is visible in the static graph exactly where intended: nodes 899 → 866, the
frontend subsystem cluster 329 → 320,scanfindings 192 → 167 (evaporated name-ambiguity
artifacts), the same three verified-deliberate oranges.
Gate
Byte-identical fingerprint/VFG/PDG baseline over the 9-language corpus; the full 1,618-test
oracle battery; ruff + mypy. Post-patch find_stale is back to the single known advisory.
stitchgraph v3.27.0 — release notes
stitchgraph v3.27.0 — release notes
The front-door release. No code change — this release makes the project legible to someone
(human or agent) who didn't build it.
README, rewritten
The old README had grown into a development log: version archaeology in the status header,
§-references into the design doc, and panel jargon a newcomer can't parse. The new one leads with
an introduction and is organised by reader:
- For humans: install matrix (what each extra unlocks), a five-minute quickstart — every
command in it verified against a live index before shipping — the operations as ask→operation
tables, the behavioural-toolkit workflow (scaffold → run the kit in your own sandbox →
find_modes), exit-code semantics for CI gating, and the trust model in plain words. - For agents: an MCP section with the
stitchgraph-mcplaunch line, a copy-paste Claude
Desktop / Claude Code config block, and rules of engagement distilled fromAGENTS.md—
query-before-grep, respectneeds_review, never delete onfind_stalealone, prefer
select_testswhen a coverage artifact exists.
The version-history narrative moved where it belongs: docs/STATUS.md, CHANGELOG.md, and the
per-version docs/RELEASE_NOTES_v*.md files.
Also in this release
docs/RELEASE_NOTES_v3.26.0.md— the D2 dedup release notes, missed when v3.26.0 shipped.
Tag hygiene note (for the release history)
The tag v3.7.0 created on 2026-07-03 pointed at the v3.25.0 merge commit; v3.7.0 is a
documented historical release (the Ruby/PHP/Bash body-matrix release). The current line is
v3.25.0 → v3.25.1 → v3.26.0 → v3.27.0.
stitchgraph v3.7.0 — the body matrix completes the language sweep (Ruby, PHP, Bash)
stitchgraph v3.7.0 — the body matrix completes the language sweep (Ruby, PHP, Bash)
v3.0.0 added the intra-procedural body matrix for Python; v3.2.0 ported it to the JavaScript
family, v3.3.0 to Go, v3.4.0 to Rust, v3.5.0 to C and C++, v3.6.0 to Java and C#. v3.7.0 adds the
final three — Ruby, PHP, and Bash — so the body matrix now spans all 12 languages the
extractor indexes (docs/IDEAS.md §5b). Bash is the outlier that closes the sweep: a
command-oriented, not expression-oriented, grammar.
A new language for an existing representation earns the MINOR bump, but it is backward-compatible:
schema, on-disk indexes, and every existing operation are unchanged, and the new behavior is opt-in
and advisory.
Added
core/structure_ruby.py — one walker for Ruby
Emits the same _VFG vocabulary as the other frontends and reuses the WL kernel, so a Ruby clone
with renamed locals or reordered statements fingerprints as the same shape. Specifics:
- Qualname = the dotted module/class chain (modules ARE part of the key):
M.Calc.compute,
singletondef self.topkeyedM.top, bare top-levelfree_fn— matching the extractor. - Expression-oriented (a trailing expression is an implicit return, like Rust). Compound assignment
(x += e),if/elsif/unlessand their statement-modifier forms,case/when,while/until/
for(+ modifiers),?:, ranges, array/hash literals, string#{…}interpolation holes,
index- and attribute-assignment,return/yield. - Blocks (
{ … }/do … end) are opaqueNESTEDleaves (closures).
core/structure_php.py — one walker for PHP
Same _VFG, same kernel. Specifics:
- Qualname = the class chain (the
namespaceis NOT part of the key):Calc.compute,
constructorC.__construct, bare top-levelfree_fn— matching the extractor. - Statement-oriented. Call/method/scoped-call arguments are unwrapped from their
argumentwrappers;
member access, subscript, compound assignment,?:, casts,new, array literals,match,
encapsed(interpolated) strings,for/foreach/while/do,switch,try/catch/finally
carry flow. - Closures and arrow functions are opaque
NESTEDleaves.
core/structure_bash.py — one walker for Bash (the command-oriented outlier)
Same _VFG, same kernel, but a different evaluation model — Bash has no expressions, only commands:
- A
command(name arg…) is a CALL — the command name is the callee, its arguments flow as
data;$(…)/`…`command substitution carries the value of the command it runs (including
in callee position —$(get_cmd) arg); avariable_assignmentbinds (copy propagation);$x/
${x}are variable reads; a string carries flow through its$(…)/$xholes;$(( … ))
arithmetic,[[ … ]]/[ … ]tests, pipelines, andif/for/while/case/c_style_forare
walked for control + data flow. - Functions are keyed by their bare name (shell functions are flat) — matching the extractor.
- Nested function definitions are opaque
NESTEDleaves.
find_similar(mode="structure") and graph_diff — now detect Ruby, PHP and Bash
Auto-detects the snippet's language (Python → JS/TS family → Go → Rust → Java → C# → Ruby → PHP →
Bash → C/C++) and ranks it only against stored functions of the same language; graph_diff's
body layer reports a diverged Ruby/PHP/Bash body present in both indexes. Same-language by
construction (a node id maps to exactly one file, hence one language).
Scope & caveats
- Advisory and read-only — never feeds
find_stale, so the cardinal rule (live code is never
confidently flagged dead) is structurally unaffected. - The Ruby/PHP/Bash layer needs the optional tree-sitter extra; without it those paths return
nothing (advisory degrade). The Python body matrix remains stdlib-only. - Cross-language body comparison stays oracle-only — topology tracks the extractor; the features
rank/diff within one language. - Some grammars are permissive supersets of others, so the advisory snippet auto-detect can mis-sniff
one bare snippet for a related language: the JS/TS grammar parses a bare PHPfunction/class
snippet, and the C/C++ grammar parses a bare Bash/PHPname() { … }snippet — so Bash and PHP are
tried before C/C++. This affects only the advisory snippet auto-detect — never the extension-keyed
graph_diffbody layer, which maps each file to exactly one language. - Same structural-approximation limits as the other frontends: no alias analysis, constants are
collapsed, Bash word-splitting/alias/exit-code semantics are not modeled. The method is in
docs/BODY_MATRIX_LESSONS.md.
Quality gate
- ruff + mypy clean; full suite passing; differential oracle suite passing.
- Three new body-matrix completeness oracles — Ruby
(tests/oracles/test_structure_ruby_completeness.py, 45 cases), PHP
(tests/oracles/test_structure_php_completeness.py, 49 cases) and Bash
(tests/oracles/test_structure_bash_completeness.py, 36 cases): ahelper()/$(helper)(a CALL)
vs0(a CONST) in every value-bearing position must change the fingerprint, plus dedicated
invariants (compound-assign rebind, module/namespace keying, constructor keyed, opaque
block/closure/nested-function, Bash dynamic-callee walked). All use the hardened exact-equality
predicate introduced in v3.6.0 (dodging the cosine float-rounding blind spot). - The adversarial panel earned its keep — 10 dropped value-flow positions found and fixed, none
caught by the generic fallback (only the value-bearing metamorphic probe surfaces these), all now
oracle-pinned:- Bash, building the frontend: a dynamic-callee drop — a command whose name is a
$(…)
substitution ($(resolve) arg) was collapsed to an opaque free word, dropping the inner CALL. - Bash, panel: a command substitution in an array-subscript index (
${arr[$(helper)]}read,
arr[$(helper)]=xLHS) was dropped on both the expansion-read and assignment-LHS paths. - Ruby, panel: a
begin/rescue/**else**clause body was never walked, and a
parenthesized multi-statement group ((sink(helper()); 0)) kept only its trailing statement. - PHP, panel: anonymous-class constructor arguments (
new class(helper()) {}) were dropped —
the args live inside theanonymous_classnode, not as a directargumentschild. - PHP, panel (round-1 confirm): heredoc interpolation holes (
<<<E…{$o->m(helper())}…E) were
dropped —heredocwas bucketed with non-interpolatingnowdocas a CONST, even though heredoc
interpolates exactly like a double-quoted string (which was already walked). Now walked;nowdoc
stays opaque. - C#, panel (certification round): a constructor initializer (
: this(helper())/
: base(helper())) had its arguments dropped — they run before the body but live in a
constructor_initializersibling of the body that the walker never visited (the C# analogue of
the C++ member-initializer-list, already handled there). Now walked. - C#, panel (certification round): an indexed/dictionary-initializer key (
new D { [Key()] = v })
dropped the key — it routes throughbind()as anelement_binding_expressionthat had no branch.
Now walked. - JS/TS, panel (certification round): a computed method key in an object literal
({ [helper()]() {} }) dropped the key — it is evaluated in the enclosing scope but the
method-definition fell straight to its opaqueNESTEDleaf without walking the computed key first
(the data-property form{ [helper()]: 1 }was always walked). Now walked; the body stays opaque. - This is language diversity as an adversarial probe again: the Bash outlier exercised a
callee/subscript position the seven prior expression-oriented frontends never could.
- Bash, building the frontend: a dynamic-callee drop — a command whose name is a
- Cross-cutting fix — default parameter-value expressions are walked. A
helper()CALL vs a0
CONST in a parameter's default value (def f(b = helper())) produced an identical fingerprint — the
parameter-seeding loop registered only the parameter name and never walked its default-value child.
Found across every language with default-argument syntax: latent in C++, C# (shipped) and
Python, JS/TS (shipped — the original frontends) plus the new Ruby/PHP (Go/Rust/Java have no
default arguments). A genuine CALL-vs-CONST completeness violation — it survived in every body
position yet vanished in the default-value slot. All now walk the default (incl. destructured
defaults like JSfunction f({a = helper()}), AND JS/TS destructuring defaults in a
declaration/assignment target —const {x = helper()} = a— which route throughbind(), a
separate path), pinned by a cross-language oracle. - Invariant fix — Python lambdas are opaque. A
lambdain expression position leaked its body's
value flow into the enclosing fingerprint (evhad noast.Lambdabranch → generic fallback
recursed into the body), breaking the documented "closures are opaqueNESTED" invariant. Python
was the lone diverging frontend — all 11 tree-sitter frontends already return a singleNESTEDleaf
for an expression-position closure. Now Python matches (the lambda's default-arg values still carry
flow). Behaviorally pinned in the Python completeness oracle (which previously only classified
Lambda as opaque without testing it). - Cross-cutting fix — assignment-target subscript index is walked. A
helper()CALL vs a0
CONST in the index of an assignment target (d[helper()] = v) produced an identical fingerprint:
the read path always walked the index, but the write (bind) path linked only the written value and
the container, never the index. Latent in ...
stitchgraph v3.1.0 — semantic-path test hardening + a clearer README
stitchgraph v3.1.0 — semantic-path test hardening + a clearer README
A small, safe release on top of v3.0.0. No source, API, or schema change — every operation
behaves exactly as in 3.0.0, and indexes don't need rebuilding. v3.1.0 closes a mutation-coverage
gap in find_similar's optional dense path and makes the README say plainly what the package
delivers.
Why
v3.0.0 shipped the body matrix with its own core mutation-pinned (structure.py 15/15, graphdiff
9/9). The release-readiness work flagged that core/similar.py's optional semantic/dense
(model2vec) retrieval path still had ~15 surviving mutants — un-pinned behaviour in the dense
ranking and the offline model-load — and parked it as a follow-up (docs/IDEAS.md §5d). This is
that follow-up.
Hardened — core/similar.py mutation coverage
The in-house differential mutation meta-oracle (scripts/mutate.py) injects one operator/boolean
mutation at a time and checks the suite kills it. New unit tests pin the previously-unpinned
behaviour:
- Ranking is strict (token + dense). The existing fixtures tied at the top (the dense test's
two nodes both scored 1.0; the token query tied at 0.447), so areverse=Trueflip left the
winner unchanged and the sort-direction mutants survived. New strict, tie-free fixtures (a 3-axis
fake embedder for the dense path; a distinct-overlap pair for the token path) make sort direction
and the drop-non-positive> 0filter observable. - Test isolation. The dense backend is module-global (
_EMBEDDER+ the_M2V_TRIEDonce-latch).
An autouse fixture now snapshots and resets it around every test, so leaked state can't change
which retrieval path a later test takes — which had made both the suite and the mutation kills
order-dependent (one mutant was killed only by a leaked embedder, masking a real gap). - Zero-norm embeddings degrade, never divide-by-zero. A zero-magnitude query or node
embedding must score 0.0; the two_dot_cosnorm guards (… or 1.0) are now pinned against the
andflip that would raiseZeroDivisionError. model2vecauto-load, tested offline. A fakemodel2vecmodule + fake config exercise the
auto-load without a network: the success path wires an embedder and selects the
configured-or-default model; the load is attempted at most once (the_M2V_TRIEDlatch); and
an import failure returns cleanly and stays on the token path (never calling_densewith no
embedder).
Result: 29/32 mutants killed. The 3 survivors are documented as justified-equivalent (not
test gaps) in the tests/test_similar.py module docstring — an or→and flip absorbed by a
downstream if dot == 0, a defensive zip(strict=False) that only differs on contract-violating
ragged vectors, and an or→and whose extra-permissive branch yields nothing downstream.
Docs
- README now opens with what stitchgraph delivers — the question each operation answers —
before the internals, with the v3.0.0 body matrix as the headline and a consistent language count.
Cardinal-safety & scope
Unchanged from 3.0.0: the body matrix and find_similar are advisory and read-only; they never
feed find_stale, so the cardinal rule (live code is never confidently flagged dead) is
unaffected. This release adds tests only — no runtime code changed.
Quality gate
- ruff + mypy clean; full suite 703 passing; differential oracle suite 85.
- Mutation meta-oracle:
structure.py15/15,graphdiff9/9,similar.py29/32 (3 justified
equivalent) — kill-signals named in the CHANGELOG. - Two-round full-diversity adversarial panel (opus / sonnet / haiku), clean.
Upgrading
Nothing to do — no behaviour, API, or schema change from 3.0.0.
stitchgraph v3.0.0 — the intra-procedural body matrix (Python)
stitchgraph v3.0.0 — the intra-procedural body matrix (Python)
stitchgraph has always modelled code between definitions: a graph of functions/classes/modules
linked by CALLS / REFERENCES / INHERITS / IMPORTS. v3.0.0 adds the level below that — a matrix
of what happens inside a function — and two advisory features built on it.
A new representation earns the MAJOR bump, but it is backward-compatible: every existing
operation, the schema, and on-disk indexes are unchanged. The new capabilities are opt-in.
Why
The matrix-as-oracle research (in research/, not shipped) showed the call graph is blind to a
whole class of signal: two functions can call the same helpers yet do completely different things,
and two functions can do the same thing while calling nothing in common. The redundancy and
fidelity signal lives inside the function. That research independently re-found — and drove — the
v2.3.0 tarjan_scc de-duplication; v3.0.0 turns the approach into real features.
Added
core/structure.py — a Python function-body fingerprint
A per-function value-flow graph (operations + control points; data + control edges) built from
the body AST with copy propagation, fingerprinted order- and name-invariantly via a
Weisfeiler-Lehman kernel. Consequence: renamed locals, reordered independent statements, and
temp-variable factoring all fingerprint as the same shape.
find_similar(mode="structure")
Rank stored Python functions by body shape instead of name/docstring/callees. Finds
renamed/reordered/temp-var clones (Type-2/Type-3) a token differ misses. The default
mode="semantic" is unchanged.
graph_diff — a new operation
Structurally diff this index against another built index (a stitchgraph .db path):
- call-level deltas — located node/edge differences.
mode="id"is exact (same codebase: did a
refactor change the graph? does the actual match the plan?);mode="leaf"reduces names to their
tail so two different codebases (e.g. a translation) can be compared (advisory — cross-language
topology tracks the extractor). - body-level changes — for Python functions present in both, those whose body shape diverged.
The headline: a data-flow bug that leaves the call graph identical is invisible to the
call-level diff but caught here.
Exposed on the library API (sg.graph_diff), the CLI (graph-diff OTHER_DB --mode / --body / --body-threshold), and the MCP tool schema.
Cardinal-safety & scope
All three features are advisory and read-only — they never feed find_stale / liveness rooting,
so the cardinal rule (live code is never confidently flagged dead) is structurally unaffected by
this release. They are Python-only (built on the deep stdlib ast); extending the body matrix to
the other languages is future work (docs/IDEAS.md §5b, with a layered/Code-Property-Graph design
in §5c). The fingerprint is a structural approximation, not sound data flow — copy propagation
but no SSA φ-nodes, loop fixpoint, or alias analysis, and constants are collapsed (so
algorithmically-equivalent-but-differently-structured code can still escape). Limits are documented
in the module and research/04-expr-dfg/FINDINGS.md.
Quality gate
- ruff + mypy clean; full suite 698 passing.
- Differential oracle suite 85, including a new graph_diff dogfood oracle: stitchgraph's own
source, indexed twice, self-diffs to equivalent (id and leaf, no body changes) — a real-code
determinism guard. - Mutation meta-oracle: the
structure.pycore 15/15 (kill-signalpytest tests/test_structure.py tests/oracles/test_structure_completeness.py) and thegraphdiffcore 9/9 (kill-signalpytest tests/test_graph_diff.py) mutants killed by their own unit suites. - Two-round full-diversity adversarial panel (opus / sonnet / haiku), clean.
Upgrading
Nothing to do — indexes don't need rebuilding and existing calls are unchanged. To try the new
features:
import stitchgraph as sg
with sg.Store("stitchgraph.db") as store:
sg.reindex(store, "src")
print(sg.find_similar(store, open("some_func.py").read(), mode="structure"))
print(sg.graph_diff(store, "other_index.db", mode="id")) # body-aware by defaultstitchgraph v2.3.0 — shared Tarjan SCC core
stitchgraph v2.3.0 — shared Tarjan SCC core
A small, internal de-duplication release. No API, schema, or behaviour change: indexes,
scan cycle detection, and find_data_loops produce byte-identical results. Indexes do not need
rebuilding.
Changed
Shared tarjan_scc helper
The strongly-connected-components algorithm was duplicated verbatim in two places:
core/reach.py→strongly_connected_components(call / import cycles, behindscan)core/dataloop.py→_tarjan(data-feedback loops)
Both copies carried an identical strongconnect implementation and recursion-limit handling. They
are now a single core/_scc.py:tarjan_scc(adj, seeds, node_count). Each call site keeps its own
adjacency construction, seed set (reach: all node ids; dataloop: adjacency keys), and
post-filter — so behaviour is preserved exactly, including the temporary recursion-limit raise that
is always restored in a finally (never leaked to the host).
This was the one piece of genuine redundancy surfaced by the matrix-as-oracle research line
(research/, not part of the package): the body-level structural-clone detector found the
byte-identical duplication the call-graph detector was blind to, and an independent
program-dependence-graph pass re-found the same pair.
Added
tests/test_scc.py— 15 direct unit tests pinning the shared primitive: component identity
(empty / single / self-loop / chain / 3-cycle / two independent cycles / cross-edge into an
already-finished SCC), destination-only and out-of-adjacency seeds, reverse-topological
ordering, a 5 000-node chain (noRecursionError),defaultdictnon-mutation, and
recursion-limit restoration on both normal return and an exception mid-walk. Stdlib-only, so it
runs in thecore-onlyCI job.
Quality gate
- ruff + mypy clean; full suite 590 passing; differential oracle suite (27) green.
- Mutation meta-oracle on
tarjan_scc: 6/6 mutants killed bytests/test_scc.pyalone. - Two-round full-diversity adversarial panel (opus / sonnet / haiku), clean. The panel
confirmed component / ordering / recursion-limit equivalence to the old inline copies — including
a 650-graph random differential with zero divergences between old and new — and verified that
find_staleliveness is structurally independent of SCC results (SCC feeds only advisory
cycle / data-loop findings, never stale rooting), so the cardinal rule is insulated from this
change. Round 2 surfaced a unit-test coverage gap (theon_stackcross-edge guard), which was
closed before release; the two confirming rounds were clean.
Notes
The companion research (research/SYNTHESIS.md) and the larger deferred direction — an
intra-procedural (body / CFG / DFG / PDG) matrix, a candidate v3.0.0 feature — are documented in
docs/IDEAS.md §5. This release ships only the safe, validated de-duplication.
stitchgraph v2.2.1 — Bash `PROMPT_COMMAND` recall + contributor methodology
stitchgraph v2.2.1 — Bash PROMPT_COMMAND recall + contributor methodology
A small patch on top of the v2.2.0 milestone. No API or schema change; indexes rebuild cleanly.
Fixed
#95 — Bash PROMPT_COMMAND=fn runtime hook
__prompt() { history -a; }
PROMPT_COMMAND=__prompt # run by the interactive shell before each promptA function registered via PROMPT_COMMAND (also PROMPT_COMMAND="fn1; fn2" and
export PROMPT_COMMAND=fn) is invoked by the shell with no textual call site, so it was
false-flagged dead. _bash_callback_refs now roots the function name(s) in a PROMPT_COMMAND
assignment.
- Scoped to the well-known
PROMPT_COMMANDvariable (high precision). - Cardinal-safe: only a name that resolves to a project function is rooted; a genuinely-unused
function still flags dead (verified). - The generic
var=fn; $varindirection remains a documented, deferred dynamic-dispatch gap.
Docs
CONTRIBUTING.md→ "The cardinal-hardening loop (dogfood + docs)" — the repeatable method
behind the entire v2.1.x → v2.2.0 line, written down so it's reusable: dogfood real repos to
surface false-deads, read the language/runtime docs to find the exact form that's actually
invoked, fix additively (an added root can't introduce a cardinal — it can only over-root),
gate and pin both directions (the live symbol is live and a dead sibling still flags), and
document over-rooting boundaries rather than risk the invariant by tightening them.
Issue triage
GitHub issues #18–#22 (filed against v1.0.4) were verified already fixed in the shipped code and can
be closed:
- #18 —
risk --pathdefaults to the indexed root recorded in the DB. - #19 —
stitchgraph --versioncallback;orient/scanscope reconciled in design §9. - #20 —
find_holesfirst-index scope documented inLIMITATIONS.md. - #21 —
[project.scripts]/[project.entry-points]parsed as roots. - #22 — bash top-level body seeded as a root and its top-level calls scanned.
Quality gate
Full suite — 575 tests (PROMPT_COMMAND bare / quoted-multi / export forms root the hook; a
non-PROMPT_COMMAND var assignment does not; a genuinely-unused function still flags) + ruff +
mypy clean; differential oracle suite (27) green; mutation meta-oracle on _bash_prompt_command_ref
(4/4 killed). CI green across all four jobs (test 3.11 / 3.12 / lint / core-only).
Two-round full-diversity multi-model adversarial review (opus / sonnet / haiku), clean in both
rounds (readiness R147–R148 → RELEASABLE): end-to-end additivity proof (rooting is append-only and
no-ops on unresolved names, so it cannot under-root or abort), PROMPT_COMMAND-only scoping,
crash-free on array / empty / non-UTF-8 / ${…} / heredoc / comment / append values, real
prompt-framework idioms (starship-style dispatcher, ${PROMPT_COMMAND:+…} preserve-existing),
cross-language isolation, streaming==in-memory + dogfood parity, prior bash fixes intact, no metric
inflation. One cardinal-safe note: the PROMPT_COMMAND+=fn append form is not yet rooted (a rare
under-rooting, filed as a deferred recall follow-up).
stitchgraph v2.2.0 — the cardinal sweep is complete
stitchgraph v2.2.0 — the cardinal sweep is complete
This is a milestone release. The per-language cardinal sweep is complete across all ten supported
languages, and the post-sweep precision/recall follow-up backlog (#70–#89) is closed. It
consolidates the entire 2.1.1–2.1.31 hardening line into one minor release.
No API or schema change. Indexes rebuild cleanly. find_stale is strictly more precise than
2.1.0 — fewer false-positive dead-code reports, nothing new to migrate.
The invariant this release is built around
Live code is never confidently flagged dead.
Reporting a live symbol as dead ("a cardinal") is the one failure that destroys trust in a dead-code
tool — once it happens, you stop believing any of its findings. Every change in this line either
removes a way that could happen or improves dead-code recall without ever risking it. Over-rooting
(occasionally keeping genuinely-dead code alive) is the deliberate, safe direction.
Each fix shipped behind a hard gate — ruff + mypy + the full test suite + a differential
streaming oracle (the streamed graph must be byte-identical to the in-memory one) + a mutation
meta-oracle — and two consecutive clean full-diversity multi-model adversarial review rounds
(independent Opus, Sonnet, and Haiku reviewers, each trying to find a live symbol flagged dead).
What's in it
Per-language cardinal sweep (2.1.1 – 2.1.26)
One gated cardinal fix per language, so a live symbol reached only by a language-specific or
framework idiom is no longer reported dead:
- Python — framework/Protocol/ABC classes, enum hooks, pytest/conftest discovery, parameter
defaults & annotations, nested/local defs. - JS / TS — re-exports and CommonJS/
export default, object-literal method shorthand, class
expressions, well-known-Symbol methods,get/setaccessors,toJSON/toString/valueOf. - Go —
init()runtime entry, method value/expression selectors. - Rust —
#[no_mangle]/#[export_name], test attributes,#[ctor]/#[dtor], const-item
initializer calls, traitimpl/inherits. - C / C++ —
EXPORT_SYMBOL, export-attribute declarations,#definemacro-body call sites,
global function-pointer / vtable tables, ISR/interrupt attributes, range-for customization points. - C# — attribute classes, explicit interface implementations, native/FFI entry points.
- Java — native (JNI) methods, same-name overload role unions, anonymous-inner-class overrides.
- PHP — bare-string callables, transitive framework inheritance.
- Ruby — operator methods, implicit conversion/coercion & Enumerable/Comparable protocols,
&:symbol/enum_forsymbol dispatch. - Bash — top-level body roots,
trap/complete -F/time/export -fcallbacks.
Post-sweep follow-up backlog #70–#89 (2.1.27 – 2.1.31)
- #74 — JS/TS function referenced via object-literal shorthand in an exported object
(export const handlers = { onClick }), including the canonical… as const/satisfiesforms. - #76 / #78 — TS
#privatemethod called viathis.#m(); string / computed / numeric-keyed
class methods. - #70 / #86 — Python subscripted
Protocol[T]/ABC, Generic[T]recognition, and bodyless
abstract / Protocol interface methods. - #89 — C/C++ struct/union/enum used only as a type (
struct Config g;). - #73 — Bash
declare -fx/declare -f -x/typeset -fxfunction exports and
time { fn; }targets.
The remaining backlog items were resolved without a code change — confirmed by the review panels as
deliberate cardinal-safe boundaries (where tightening would risk the invariant) — or are
coverage-only.
Compatibility
- No public API change; no index-schema change.
- Existing indexes rebuild cleanly; the streaming and in-memory indexers remain byte-identical.
- The only observable difference is fewer false-positive dead-code reports from
find_stale.
Known limitations (unchanged)
Genuinely dynamic dispatch the static graph can't see remains out of scope and documented in
LIMITATIONS.md (e.g. Ruby send/public_send, Salt-style string-keyed loaders). One newly
filed pre-existing recall gap is deferred: Bash PROMPT_COMMAND=fn / var=fn; $var indirect
invocation.
Quality gate
571 tests + 27 differential oracles + ruff + mypy, all green; mutation meta-oracle on each new
helper; readiness RELEASABLE (two consecutive clean full-diversity panels per fix). Dogfood on
stitchgraph's own source: find_stale advisory-only (no false-dead), 0 holes, streaming ==
in-memory parity.
The GitHub Actions CI matrix — test (py3.11), test (py3.12), lint + type-check, and
core-only (no extras) — is green. (The core-only job runs the suite with no optional extras
installed to prove the stdlib-only core; 14 tree-sitter-dependent tests were guarded with
pytest.importorskip so they skip cleanly there.)
stitchgraph v2.0.0 — constant-memory streaming indexer
stitchgraph v2.0.0 — constant-memory streaming indexer
The major feature of v2: reindex can stream the graph straight to SQLite instead of
building it all in Python first, so peak memory tracks one file's working set — not the
size of the whole repo. Tens-of-thousands-of-file monorepos (Magento, 24k PHP files) that used
to need >12 GB and OOM now index on a laptop. The streamed index is byte-identical to the
in-memory one, pinned by a differential oracle across Python + JS/TS + Go + Ruby + C/C++ +
Rust + PHP.
Measured on the Magento Framework core (4,304 PHP files → 30,412 nodes, ~15.5M raw edges):
| peak RSS | output | |
|---|---|---|
in-memory (streaming=False) |
3,183 MB | 30,412 nodes / 3,926,345 edges |
streaming (streaming=True) |
269 MB | 30,412 nodes / 3,926,345 edges |
≈12× less memory, byte-identical output (verified row-for-row — including weight,
provenance, and the internal name_based flag), ~40% slower.
Highlights
- Monorepo-scale indexing on modest hardware — the former top limitation ("very large
monorepos are indexed as one in-memory graph") is resolved. Peak memory is bounded by symbol
count + one file's working set, not by the millions of edges a big polyglot repo produces. - Automatic — you don't have to think about it.
reindexnow decides for you: it streams
large on-disk repos and keeps the slightly faster in-memory path for small ones. Force it
either way withstreaming=True/streaming=False(CLI:--streaming/--no-streaming;
also on the MCP tool). - Identical results, guaranteed. Streaming changes how the graph is built, never what
it contains. A differential oracle pinsstreaming == fullbyte-for-byte on a polyglot
corpus plus heavy-fan-out / cross-group stress fixtures, so the low-memory path can never
silently diverge.
How to use it
import stitchgraph as sg
with sg.Store("stitchgraph.db") as store: # an on-disk DB realises the memory win
sg.reindex(store, "/path/to/huge/monorepo") # AUTO: streams when the tree is large
print(sg.find_stale(store))streaming is tri-state:
None(default) — AUTO: stream when the store is on-disk and the tree is large
(≥ 2,000 indexable source files). Small repos use the faster in-memory path.True/False— force streaming / in-memory.
Streaming saves memory only with an on-disk
Store— a:memory:database holds the
rows in RAM regardless — so AUTO never picks it for:memory:.
How it works
- Parse trees + source are dropped after pass 1. Each file's Python AST / tree-sitter
parse tree and its source bytes are freed once definitions are collected; only a tiny
per-definition record survives into the edge-resolution pass. Neither all the trees nor all
the source are ever resident at once. - Edges stream to SQLite, deduplicated per source on the fly. The dominant cost on a big
repo is the edge set — name-based ambiguous fan-out yields ~15.5M edges on a single Magento
module. Because every dedup key is scoped to an edge's source, each source's fan-out is
collapsed in memory the moment it's complete, and only the survivors are written (in
committed batches). The raw millions never materialise in Python or on disk. A final
global dedup pass in the store reconciles the rare cross-group / resolver overlap.
Full design: docs/V2_STREAMING_DESIGN.md.
Notes & trade-offs
- Streaming reindex commits in batches rather than as one transaction, so a crash mid-rebuild
can leave a partial index; a re-run rebuilds cleanly (it clears first). The default
in-memory path remains crash-atomic. AUTO only engages streaming for large on-disk repos —
exactly where the in-memory alternative is an out-of-memory failure. - No public API break:
extract_project/treesitter.extractstill return(nodes, edges);
the streaming machinery is internal. The major version reflects the new default behaviour
(AUTO streaming) and the scale milestone.
Compatibility
- Existing indexes and the on-disk schema are unchanged; no migration needed.
- All v1 operations, the CLI, and the MCP server behave exactly as before — only
reindex's
memory profile (and its newstreamingknob) changed.
Quality gate
Shipped under stitchgraph's standard three-layer release gate: the full test suite, the
differential oracle suite (incremental == full, streaming == full, GraphBLAS ==
pure-Python), and the mutation meta-oracle, plus multi-model adversarial review panels
(opus + sonnet + haiku) driven to convergence. ruff + mypy clean. The streaming path is
specifically pinned by the streaming differential oracle (now comparing every load-bearing
edge field, name_based included) and a mutation run over its correctness core.
stitchgraph v1.0.7 — multi-repo / multi-language precision hardening
stitchgraph v1.0.7 — multi-repo / multi-language precision hardening
A precision release driven by a multi-repo, multi-language false-positive hunt:
stitchgraph was run against ~47 real-world projects across 9 languages — including code
designed to break parsers (IOCCC obfuscated C) and large/messy corpora (Linux kernel core,
WordPress, Magento, PrestaShop, Symfony, flake8, Flask, NestJS, TypeORM) — to ground-truth
find_stale against actual liveness. The hunt surfaced a family of cardinal-class
false-deads (live code flagged dead) from entry-point and liveness signals stitchgraph did
not yet model. Every fix only ever adds roots (precision-safe), and each is owned by a
regression test.
Robustness held: 0 crashes across every corpus — the 1.0.6 RecursionError / FIFO /
large-file / non-UTF-8 guards survive obfuscated and machine-generated C.
Highlights
- Far fewer false-deads on real codebases — entry points, framework callbacks, and
packaging conventions across Python, JS/TS, C/C++, Java, C#, Ruby, PHP, Rust, Go are now
recognized as live roots. - src-layout just works — a PyPA
src/project's absolute imports resolve (including
PEP 420 namespace packages with no__init__.py). - Modern framework code is understood — NestJS/Angular/TypeORM decorators, Java/C#
reflection annotations, and the CommonJS prototype/exports.X = …idiom. - Less noise — dependency/vendored/build directories (
node_modules,vendor,
third_party,target, …) are skipped by default.
What changed
Added — new entry-point / liveness signals (all cardinal-safe)
setup.cfg [options.entry_points]is parsed alongsidepyproject.toml—
console_scripts,gui_scripts, and plugin groups (e.g.flake8.extension/
flake8.report). Class targets also root their public methods.- src-layout absolute-import resolution — modules under a top-level
src/are imported as
pkg.mod(notsrc.pkg.mod); absolute imports now resolve so module-load-only-live code
(registries, dispatch tables instantiated at import) isn't flagged dead. Works for PEP 420
namespace packages too (no__init__.pyrequired). Node ids are unchanged — the module
lookup gains a src-stripped alias. - Inherited public methods of an exported class are rooted — a base-class method such as
Flask'sshell_context_processor, called on an instance, is public API. - Framework callbacks across more languages:
- Java/C# reflection annotations/attributes —
@PostConstruct,@EventListener, JPA
@PrePersist,[OnSerializing],[ModuleInitializer], BenchmarkDotNet[Benchmark], … - JS/TS decorators — NestJS/Angular/TypeORM:
@Controller/@Get/@Injectable/
@Component/@Entity/… root the decorated class or handler method. - Transitive and self-named external-base callback classes —
FlaskGroup → AppGroup → click.Group;EnvironBuilder(werkzeug.test.EnvironBuilder). - Ruby
const_missing/const_addedand more implicit interpreter hooks.
- Java/C# reflection annotations/attributes —
- C/C++
EXPORT_SYMBOL(...)(and_GPL/_NSvariants) roots the named function as
public kernel/module ABI — the C analogue of__all__/module.exports. - JS/TS member-assigned functions and classes —
app.render = function(){…},
Foo.prototype.m = …,module.exports.x = …,exports.Parser = class {…}are modeled and
their bodies walked, so helpers they call aren't flagged dead. Module-scope ones are rooted
(a member-assigned class takes theexportedrole so its public methods are rescued);
function-nested ones stay reachability-gated.
Changed
- Dependency/vendored/build directories are skipped when indexing — one shared set across
both extractors:node_modules,vendor,third_party,third-party,bower_components,
target,.gradle, plus the existing.venv/build/dist/__pycache__/.git/
.tox/.svn/.hg/.mypy_cache/.pytest_cache/.ruff_cache. Conservative —
only names reserved by convention for non-first-party content. - A bodyless C/C++ struct/enum/union (a type reference like
struct timeval tv, a forward
declaration) is no longer extracted as a phantom dead class.
Notes & limitations
- Scalability:
reindexbuilds a single in-memory graph, so peak RAM scales with total
nodes + edges — a tens-of-thousands-of-file monorepo (Magento, 24k files) exceeds ~12 GB.
Index self-contained sub-trees separately, or provision RAM (~0.7–0.9 GB per 1,000 dense PHP
files). A streaming / constant-memory indexer is the next planned enhancement. - New documented, cardinal-safe over-rooting tradeoffs (flat-name export-name collisions;
member-assigned methods) — seeLIMITATIONS.md.
How it was verified
Hardened by the standard three-layer release gate (full-diversity adversarial panel +
deterministic oracle suite + in-house mutation meta-oracle) over a 9-round fix-panel campaign
(R40–R48). The panels caught and fixed 6 over-rooting / recall defects the new features
themselves introduced — R40A (script-class over-root), R40B + R41A (a comment between a
decorator/attribute and its def dropping the marker, across JS/TS and Rust), R40C
(member-assignment inside a dead function rooted), R42A (namespace-package src-layout), and
R46A (member-assigned-class methods flagged dead) — none ever shipped, each cardinal-safe and
owned by a regression test. The src-layout incremental defect class is now owned by the
differential oracle (a new src/-layout incremental==full fixture).
Released on two consecutive full-diversity (opus + sonnet + haiku) clean panels (R47–R48);
scripts/readiness.py reports RELEASABLE.
- Tests: 374 passing (full extras), incl. 23 oracle tests
- Mutation meta-oracle: 17/17 killed (envelope)
ruff✅ ·mypy✅- Dogfood (self-index):
find_stale1 advisory (no false-dead) ·find_holes0 - Robustness: ~47 real repos across 9 languages, 0 crashes
Upgrade notes
Drop-in. No API or schema changes. On a src/-layout repo or one with vendored directories,
expect fewer find_stale candidates (live code previously mis-flagged is now correctly
rooted; dependency dirs are no longer indexed). Re-run reindex to pick up the improved
resolution.
The maintainer applies the v1.0.7 tag.