Skip to content

Releases: RexBytes/stitchgraph

stitchgraph v3.27.1 — release notes

Choose a tag to compare

@RexBytes RexBytes released this 03 Jul 12:58
a085345

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_tree was dead on arrival. The D2 stage-2 patch added the shared
    walk-guard helper but the transformation script never wired the nine _walk functions to call
    it, and ruff --fix then removed the unused imports — hiding the slip from every
    output-equivalence gate, because dead code has no outputs. find_stale caught it statically;
    find_gaps corroborated at runtime (untested_dead = exactly this + the one known advisory).
    It now guards all nine walk entries as intended, and the seven remaining _walk-local text
    helpers delegate to the shared node_text (closing the residual clone hubs orient still
    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, scan findings 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

Choose a tag to compare

@RexBytes RexBytes released this 03 Jul 12:04
a9a8076

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-mcp launch line, a copy-paste Claude
    Desktop / Claude Code config block, and rules of engagement distilled from AGENTS.md
    query-before-grep, respect needs_review, never delete on find_stale alone, prefer
    select_tests when 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)

Choose a tag to compare

@RexBytes RexBytes released this 03 Jul 10:00
958c650

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,
    singleton def self.top keyed M.top, bare top-level free_fn — matching the extractor.
  • Expression-oriented (a trailing expression is an implicit return, like Rust). Compound assignment
    (x += e), if/elsif/unless and 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 opaque NESTED leaves (closures).

core/structure_php.py — one walker for PHP

Same _VFG, same kernel. Specifics:

  • Qualname = the class chain (the namespace is NOT part of the key): Calc.compute,
    constructor C.__construct, bare top-level free_fn — matching the extractor.
  • Statement-oriented. Call/method/scoped-call arguments are unwrapped from their argument wrappers;
    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 NESTED leaves.

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); a variable_assignment binds (copy propagation); $x/
    ${x} are variable reads; a string carries flow through its $(…)/$x holes; $(( … ))
    arithmetic, [[ … ]]/[ … ] tests, pipelines, and if/for/while/case/c_style_for are
    walked for control + data flow.
  • Functions are keyed by their bare name (shell functions are flat) — matching the extractor.
  • Nested function definitions are opaque NESTED leaves.

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 PHP function/class
    snippet, and the C/C++ grammar parses a bare Bash/PHP name() { … } snippet — so Bash and PHP are
    tried before C/C++. This affects only the advisory snippet auto-detect — never the extension-keyed
    graph_diff body 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): a helper()/$(helper) (a CALL)
    vs 0 (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)]=x LHS) 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 the anonymous_class node, not as a direct arguments child.
    • PHP, panel (round-1 confirm): heredoc interpolation holes (<<<E…{$o->m(helper())}…E) were
      dropped — heredoc was bucketed with non-interpolating nowdoc as 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_initializer sibling 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 through bind() as an element_binding_expression that 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 opaque NESTED leaf 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.
  • Cross-cutting fix — default parameter-value expressions are walked. A helper() CALL vs a 0
    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 JS function f({a = helper()}), AND JS/TS destructuring defaults in a
    declaration/assignment target — const {x = helper()} = a — which route through bind(), a
    separate path), pinned by a cross-language oracle.
  • Invariant fix — Python lambdas are opaque. A lambda in expression position leaked its body's
    value flow into the enclosing fingerprint (ev had no ast.Lambda branch → generic fallback
    recursed into the body), breaking the documented "closures are opaque NESTED" invariant. Python
    was the lone diverging frontend — all 11 tree-sitter frontends already return a single NESTED leaf
    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 a 0
    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 ...
Read more

stitchgraph v3.1.0 — semantic-path test hardening + a clearer README

Choose a tag to compare

@RexBytes RexBytes released this 29 Jun 19:39
91b760f

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 a reverse=True flip 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 > 0 filter observable.
  • Test isolation. The dense backend is module-global (_EMBEDDER + the _M2V_TRIED once-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_cos norm guards (… or 1.0) are now pinned against the
    and flip that would raise ZeroDivisionError.
  • model2vec auto-load, tested offline. A fake model2vec module + 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_TRIED latch); and
    an import failure returns cleanly and stays on the token path (never calling _dense with 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 orand flip absorbed by a
downstream if dot == 0, a defensive zip(strict=False) that only differs on contract-violating
ragged vectors, and an orand 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.py 15/15, graphdiff 9/9, similar.py 29/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)

Choose a tag to compare

@RexBytes RexBytes released this 29 Jun 16:19
fdd6d97

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.py core 15/15 (kill-signal pytest tests/test_structure.py tests/oracles/test_structure_completeness.py) and the graphdiff core 9/9 (kill-signal pytest 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 default

stitchgraph v2.3.0 — shared Tarjan SCC core

Choose a tag to compare

@RexBytes RexBytes released this 29 Jun 11:20
b49b960

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.pystrongly_connected_components (call / import cycles, behind scan)
  • 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 (no RecursionError), defaultdict non-mutation, and
    recursion-limit restoration on both normal return and an exception mid-walk. Stdlib-only, so it
    runs in the core-only CI 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 by tests/test_scc.py alone.
  • 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_stale liveness 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 (the on_stack cross-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

Choose a tag to compare

@RexBytes RexBytes released this 29 Jun 09:33
aaee94f

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 prompt

A 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_COMMAND variable (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; $var indirection 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:

  • #18risk --path defaults to the indexed root recorded in the DB.
  • #19stitchgraph --version callback; orient/scan scope reconciled in design §9.
  • #20find_holes first-index scope documented in LIMITATIONS.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

Choose a tag to compare

@RexBytes RexBytes released this 29 Jun 08:41
4ca70bb

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/set accessors, toJSON/toString/valueOf.
  • Goinit() runtime entry, method value/expression selectors.
  • Rust#[no_mangle]/#[export_name], test attributes, #[ctor]/#[dtor], const-item
    initializer calls, trait impl/inherits.
  • C / C++EXPORT_SYMBOL, export-attribute declarations, #define macro-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_for symbol dispatch.
  • Bash — top-level body roots, trap / complete -F / time / export -f callbacks.

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 / satisfies forms.
  • #76 / #78 — TS #private method called via this.#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 -fx function 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

Choose a tag to compare

@RexBytes RexBytes released this 26 Jun 16:34
072818e

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. reindex now decides for you: it streams
    large on-disk repos and keeps the slightly faster in-memory path for small ones. Force it
    either way with streaming=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 pins streaming == full byte-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.extract still 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 new streaming knob) 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

Choose a tag to compare

@RexBytes RexBytes released this 26 Jun 12:15
684d480

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 alongside pyproject.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 (not src.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__.py required). 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's shell_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_added and more implicit interpreter hooks.
  • C/C++ EXPORT_SYMBOL(...) (and _GPL / _NS variants) roots the named function as
    public kernel/module ABI — the C analogue of __all__ / module.exports.
  • JS/TS member-assigned functions and classesapp.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 the exported role 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: reindex builds 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) — see LIMITATIONS.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_stale 1 advisory (no false-dead) · find_holes 0
  • 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.