Skip to content

Resync with graphify-py @ 35665a7#43

Merged
rblaine95 merged 48 commits into
masterfrom
resync/graphify-py/35665a7
Jul 15, 2026
Merged

Resync with graphify-py @ 35665a7#43
rblaine95 merged 48 commits into
masterfrom
resync/graphify-py/35665a7

Conversation

@rblaine95

Copy link
Copy Markdown
Member

Ports every applicable behavioural change from graphify-py since the last
pinned submodule commit up to 35665a7, bumps the submodule pointer, and runs a
full per-crate CodeRabbit review + re-review loop until clean.

Ported changes

  • extract resolvers: case-sensitive cross-file resolution (#1581), cross-language
    family guard, JS/TS import-evidence gate (#1659), ruby containers, pascal, ref-stub
    externals, and the indirect_call feature (#1565/#1566) with the affected member-seed.
  • detect: FK pg_constraint introspection, posix path (#1672), incremental
    cache-root threading (#1747), and inline unit tests migrated to integration suites
    per AGENTS.md.
  • reflect: work-memory learning overlay (#1441) across report/serve/expose/HTML.
  • build / dedup / export / cache / cluster / analyze / semantic / security / watch /
    hooks / cli-root
    : the remaining v8 changes, incl. --code-only, skill-version
    positional matching (#1568), and the #1747 out/cache routing.
  • Workspace version bumped to 0.9.12; README/USAGE reconciled.

Fixes beyond parity (AGENTS.md: fix reference bugs, don't replicate)

  • cache: the stat index is flushed via a scope guard, not a static (Rust never
    drops statics at exit, so the #1656 cache never persisted); keyed by resolved index
    file so an absolute GRAPHIFY_OUT no longer clobbers across roots; incremental runs
    route the cache under --out, keeping the corpus clean (#1747).
  • serve: per-project Arc<Mutex<GraphCtx>> locking so concurrent requests to
    different projects run in parallel and one slow load can't stall others; bounded,
    canonicalised project cache (remote project_path is a memory-exhaustion vector);
    hot reload clears the stale IDF cache; tools/call failures return isError: true
    and resources/read failures a JSON-RPC error; a per-request timeout; non-string
    project_path is a tool error.
  • build / export: hyperedges require a non-empty id and dedup within a batch;
    carried hyperedges with a member from a deleted/re-extracted file are dropped.
  • detect: per-entry / metadata walk failures are recorded, not silently dropped.

Intentional divergences from graphify-py

Documented in USAGE.md: carried-hyperedge dangling-member drop and the malformed-alias
stderr warning (update), and the bounded project cache + structured MCP errors (serve).

Testing

cargo clippy --all-targets --all-features --workspace clean; cargo nextest run --workspace — 2703 passed, 2 skipped; hk check clean. Every touched crate passed a
CodeRabbit review (and a re-review after fixes) with zero remaining findings.

rblaine95 added 30 commits July 12, 2026 10:30
Port every applicable behavioural change from `graphify-py`
v0.9.3 through v0.9.12 into the Rust workspace and bump the
workspace version to `0.9.12` in lockstep with the submodule.

Ports:

- extract: case-sensitive cross-file resolution, cross-language
  family guard, JS/TS import-evidence gate, Ruby module/`Struct`/
  `Class.new` containers, Pascal call-scoping + inherited-call
  resolver, JS/TS rationale + ADR/RFC doc-refs, ref-namespaced
  external imports, and the misc edge-emission batch.
- indirect-call feature: `indirect_call` edges for assignment /
  return / getattr dispatch across Python and JS/TS, with
  `affected` member-seed tracking.
- detect: `is_sensitive` source-file exemption, `snapshots`
  noise-dir gating, cached word-count, and the unclassified-file
  CLI notice.
- reflect work-memory overlay (#1441): `.graphify_learning.json`
  sidecar plus read surfaces in report, `serve`, `explain`, and
  the interactive HTML viz.
- CLI `--code-only` (#1734): local AST-only code graph that skips
  documents, papers, and images.
- CLI out/cache placement (#1747): `--out` keeps all artefacts and
  caches under `<out>/graphify-out/`; `cluster-only` writes beside
  a `--graph` inside a `graphify-out/` dir.
- watch reconciliation (#8d8d2b8): removed and renamed sources are
  reconciled through a root-aware identity resolver, with a
  separate hyperedge eviction set (#1755) and `follow_symlinks`.
- hooks: `claude_uninstall` cleans `CLAUDE.local.md`; skill-version
  stamp writes plus a direction-aware startup mismatch warning
  (#1568).

By the will of the Machine God.
Replace the `GRAPHIFY_TEST_FORCE_SHRINK_REFUSE` env-var fault
injector with a typed, per-call test seam: `check_shrink` is
threaded through the rebuild pipeline as an injectable
`ShrinkChecker`, and `test_support::rebuild_code_forcing_shrink_refusal`
supplies a rejecting checker scoped to a single call. No global
state or environment variable can alter a production binary.

Document two disputed findings inline (CodeRabbit does not read
commit messages):

- `rebase_relative_source_files` iterates `nodes`/`edges`/
  `hyperedges` to match graphify-py; it runs on the fresh
  extraction whose edge bucket is always `edges`, so a `links`
  bucket would never match and would diverge from the reference.
- `stabilize_rebuild_cwd`'s process-wide `chdir` is pre-existing
  `e5044c3` behaviour mirroring the reference; concurrent rebuilds
  are serialised by the per-repo lock.

Glory to the Omnissiah.
Replace `stabilize_rebuild_cwd`'s process-global `set_current_dir`
with `effective_watch_path`, which resolves the rebuild's watch
path without mutating the working directory: a valid CWD keeps the
caller-supplied (often relative) path so the committed
`.graphify_root` marker stays portable (#777), and only a gone CWD
falls back to rooting under `GRAPHIFY_REPO_ROOT`. A concurrent
caller's working directory can no longer be disturbed. Add a
regression test asserting `rebuild_code` leaves the process CWD
unchanged.

The shrink-guard's `had_explicit_deletions` short-circuit is left
as-is: it mirrors graphify-py `_check_shrink`, which bypasses
before the `rebuilt_sources` accounting, and the divergence
suggested by review would make the Rust guard stricter than the
reference.

By the will of the Machine God.
Fix the still-valid findings from the extract-crate review and the
case-insensitivity gaps they surfaced:

- Reuse `HEADER_SUFFIXES` instead of a duplicate constant in
  `postprocess`.
- Gate the JS/TS phantom-edge check case-insensitively (a `.JS`/`.TS`
  file is still JS/TS), via a new allocation-free
  `lang_configs::ends_with_suffix_ci` helper, and lowercase the
  cross-file resolver activation gate so an uppercase `.PAS`/`.CS`
  file's pass runs at all (#1671). Regression tests for uppercase
  `.TS` and `.PAS`.
- Fix C++ receiver typing: visit locals in source order (a nested
  block shadow no longer wins over the outer binding) and key the
  table per function body so a local never types a same-named
  receiver in another function.
- Fix Objective-C receiver typing the same way: per-method tables so a
  local never leaks across methods, source-ordered within a body.
- Check the `_callable` leak assertions against each node's nested
  `metadata` map, where the marker actually rides.
- Rewrite the `clippy::similar_names` justifications to describe the
  Rust field roles rather than referencing the Python source.

Disputed via code comment (kept matching graphify-py):

- `resolve_up_chain` keeps nearest-base-first BFS: Delphi lists the
  parent class before interfaces, so the first base owns the call;
  same-depth aggregation would drop real parent-method calls.
- The C# receiver table stays a file-wide LIFO walk: it mirrors
  `_csharp_member_type_table`, and its reverse order lets a method
  local shadow an earlier class field, which is the C# rule.

Glory to the Omnissiah.
Fix the still-valid findings and dispute the parity false positives
surfaced by the fuller extract-crate review:

- Deduplicate Julia import-symbol nodes (guard the push on the
  `seen_ids` insert that was already computed but ignored).
- Register a Ruby `Struct.new`/`Class.new`/`Data.define` synthesized
  class in `callable_def_nids`, matching every other class def site
  and the reference.
- Skip a C# generic-argument `references` self-edge
  (`class Foo : Base<Foo>`).
- Include `.mjs`/`.cjs`/`.mts`/`.cts` in the TS/JS resolver
  activation suffixes.
- Rewrite the last `clippy::similar_names` justification that
  referenced the Python source.
- Scope `merge_decl_def_classes` self-loop / duplicate-edge cleanup
  to edges touching a merged id, so an unrelated file's edges are no
  longer corpus-dependent on whether a header/impl pair merged.

Disputed via code comment (Rust already mirrors graphify-py):

- `resolve_up_chain` callee is pre-lowercased at emission.
- C# `method_index` last-wins (deterministic edge order) and C#
  `new_type` direct-child scan both port the reference and are
  covered by passing tests.
- PowerShell simple-name-only bases, Rust enum struct-variant field
  typing, and Pascal exact-case qualified impl headers all match the
  reference.

Glory to the Omnissiah.
Add `#[must_use]` to the pure `read_csharp_type_name`, per the
project's pure-function-returns-owned convention.

By the will of the Machine God.
The resolver-activation gate now normalises extension case, so a pass
can fire for an uppercase `.CS`/`.JAVA`/`.RB`/`.TS` file. Route each
language resolver's own `source_file` suffix guard through the shared
`ends_with_suffix_ci` helper so the activated pass actually acts on
those inputs instead of silently skipping them - the C#, Java, Ruby,
and TypeScript member/reference/import guards were case-sensitive.
Add an uppercase `.CS` member-resolution regression.

Also skip a C# generic-argument reference whose name is an in-scope
type parameter (`class Box<T> : Base<T>`), matching the base-name
exclusion, so `T` produces no reference or phantom node.

Ave Deus Mechanicus.
Add uppercase member-resolution regressions for Java (`.JAVA`),
Ruby (`.RB`), and TypeScript (`.TS`), covering each resolver guard
routed through `ends_with_suffix_ci` in the #1671 coherence sweep
(C# `.CS` already covered). TypeScript's distinct four-suffix
member set is exercised too.

Glory to the Omnissiah.
Add a regression proving `merge_decl_def_classes`' edge cleanup is
scoped to merged ids: merge-created self-loops / duplicate edges are
normalised while unrelated ones survive. Add a `relativize_source_files`
test exercising the `scope` gate (#8d8d2b8) - an absolute path outside
the watched subtree is left untouched, one inside is relativised.

By the will of the Machine God.
Rewrite the `.TS` member-call regression as a single-file case so it
isolates the resolver's source-suffix guard instead of relying on
case-insensitive filesystem folding for JS module-path resolution
(which probes lowercase candidates and would miss `SVC.TS` on a
case-sensitive volume).

Glory to the Omnissiah.
Assert a module imported in two statements emits one imported-symbol
node, defending the `seen_ids`-guarded node push against regression.

Ave Deus Mechanicus.
Make `scope` narrower than `root` with a real sibling path inside
`root` but outside the watched subtree: without the gate it would be
relativised against `root`, so the test now actually protects the
#8d8d2b8 scope behavior. Use `tempdir()` fixtures for isolation.

By the will of the Machine God.
A README-only reader could not discover the no-LLM-key local-AST path.
Add a Quick start example for `graphify extract . --code-only` (skips
the semantic pass and doc/paper/image files).

Glory to the Omnissiah.
- Fix `merge-graphs` tag deduplication so an index-suffixed tag
  (`front_src-2` from two `front/src` repos) can no longer collide
  with an input repo literally tagged `front_src-2`: reserve every
  returned tag and advance the suffix until unused. Regression test
  with two `front/src` and one `front/src-2` repo.
- Clarify the stale-verdict note in `query` output.

The token-wide skill-version argv scan is left as-is via a code
comment: it mirrors graphify-py's `sys.argv` scan, and a stray arg
value matching a silent-command name is a harmless warning
false-negative not worth positional parsing.

Glory to the Omnissiah.
Satisfy `clippy::doc_markdown` on the Julia dedup and uppercase-TS
regression doc comments (`raw_calls`, `CodeRabbit`).

By the will of the Machine God.
Revert the `explain` stale-verdict suffix to graphify-py's verbatim
`[code changed since — re-verify]`: `test_explain_cli.py` pins that
exact substring, so the earlier reword broke byte parity. Port the
contested+stale and no-lesson explain cases so the string (and the
no-sidecar contract) are defended in Rust.

By the will of the Machine God.
Fix a provenance divergence: `record_node` recorded a provenance
entry for every non-neutral outcome, including `dead_end`, but
graphify-py `_record_node` records provenance only for `useful` /
`corrected` events (a dead end updates the score, not the trail).
Gate the push on the outcome and add a regression. This is the
opposite of the review's "record even when sign == 0" suggestion,
which would diverge - a neutral doc never reaches `record_node`
(disputed via comment).

The overlay's first-candidate-wins provenance is left as-is: it
mirrors graphify-py's `_add` (`if cid in nodes_out: return`), so a
losing alias contributes no provenance by design (disputed via
comment).

Glory to the Omnissiah.
Describe the semantic rule (useful/corrected-only trail; first-status
-wins canonical collision) rather than narrating the review, per code
-comment style.

By the will of the Machine God.
The stale-skill check scanned all of argv for a silent-command name,
so `graphify query install` (or any free-text arg equal to
`install`/`uninstall`/`hook-check`/`hook-guard`) wrongly suppressed
the warning. Validate command POSITIONS against clap's command tree:
a top-level silent command, or a platform group's `install`/
`uninstall` subcommand. Extend the startup-warning test with
`query install` (must warn) and `claude install` (silent).

By the will of the Machine God.
Fix `claude_uninstall` so the absent-hook path adds no message: the
empty string from `uninstall_claude_hook` was pushed into the joined
output, leaving a trailing blank line. graphify-py's
`_strip_graphify_hook` is silent when no hook is present, so skip the
empty message (not a "nothing to do" line the reference never emits).
Pin it with a no-trailing-newline assertion.

The `graphifyy` package name in the stale-skill warning is left
verbatim (disputed via comment): `graphifyy` is graphify-py's real
PyPI distribution name (`pyproject.toml` `name = "graphifyy"`), so the
upgrade command must name it - "graphify" would point users at the
wrong package.

Glory to the Omnissiah.
- Fail SAFE in the graph.json shrink guard when an existing file
  cannot be READ (permission / transient IO / a directory), instead
  of treating a read error as an empty file and permitting the
  overwrite. A missing file still permits growth. This is the
  fail-open data-loss path #479 exists to prevent; the parse-error
  branch already fails closed. Divergence from graphify-py, whose
  read-error branch sets `raw = ""`.
- Drop canvas communities left with no members after the dangling
  filter, and synthesize the all-nodes community from the FILTERED
  result so an all-dangling input recovers real cards instead of
  emitting an empty group box. Divergence from graphify-py to_canvas,
  which emits an empty group box per fully-dangling community.

Regressions for both: an unreadable existing graph.json is refused,
and an all-dangling community yields one card per real node with a
single synthesized group.

Glory to the Omnissiah.
- Read a `Unit::Slice` from the containment-checked `safe_path` (via a
  new `read_slice_text_from`) instead of re-following the slice's
  stored path at read time, so slice reads honour the corpus-root
  symlink guard exactly like whole-file reads.
- Record Bedrock token usage on every call the sink is configured for
  (zero when the response omits usage), matching the other backends
  instead of silently dropping the call from the tally.
- Rename the internal `tokenizer::estimate_file_tokens(content,
  overhead)` helper to `estimate_content_tokens` and make it
  `pub(crate)`, so it no longer collides by name with the public
  path-based `estimate_file_tokens` wrapper (the source of a review
  false positive).
- Tighten the #1685 special-token test to a token-only fixture and
  assert it encodes as multiple ordinary tokens (not a collapsed
  special id or a drop).

Glory to the Omnissiah.
Pass a `UsageSink` to the Bedrock mock call and assert it records the
fixture's 1/2 token usage, covering the always-record plumbing.

By the will of the Machine God.
Describe caller-capped content plus the `<untrusted_source>` wrapper
overhead, dropping the obsolete `char_cap` / `=== rel ===` wording.

By the will of the Machine God.
Key the process stat index by resolved cache-file root instead of a
single first-seen root. `detect_incremental_with_cache_root` is a
public API promising a per-call cache root, but `ensure_stat_index`
ignored every root after the first, so two sequential detections with
different roots clobbered one shared index. `StatIndex` now holds a
`Map<PathBuf, RootState>`; `ensure_stat_index` returns the resolved
key, each hash/word-count lookup selects its keyed state, and
`flush_stat_index` iterates dirty roots.

Record per-entry read failures and stat failures in the sequential
`walk_dir` (the memory sidecar) instead of dropping them via
`rd.flatten()` and `let Ok(m) else continue`, so a file that cannot be
enumerated or stat'd is surfaced in `walk_errors`.

Extend the snapshot-prune test to assert pruned dirs are absent from
`unclassified`, and add regressions: two `cache_root`s write to
separate indexes (cache + detect layers) and a dangling sidecar
symlink is recorded as a walk error.

Glory to the Omnissiah
AGENTS.md requires all tests to live under `tests/` as integration
tests, but `extensions.rs`, `sensitive.rs`, and `ignore.rs` each wired
a `#[cfg(test)] #[path = "*_tests.rs"] mod` sibling. Remove those three
declarations and files.

`extensions_tests` and `sensitive_tests` were fully redundant: every
case is already covered (identically or by a more granular case) in
`tests/parity_classify.rs` and `tests/parity_sensitive.rs`.

`ignore_tests` uniquely exercised the small parse/glob helpers, so port
those into `tests/parity_ignore.rs`: the four `parse_gitignore_line`
cases call the public function directly, and the `*` / `**` cases run
through the public `is_ignored` (anchored `/*.py` keeps the "star does
not cross a slash" negative; `/**/*.py` covers the recursive wildcard)
so `glob_match` stays crate-private. Both new anchored tests
canonicalise the temp root so the anchor `strip_prefix` survives the
macOS `/var` symlink.

Glory to the Omnissiah
The root-keyed map clobbered itself under an absolute `GRAPHIFY_OUT`:
`out_base` ignores the root when the override is absolute, so two
distinct cache roots resolve to the SAME `stat-index.json`, yet the map
held two competing states that flushed to that one file
nondeterministically.

Key the map by the resolved index FILE path instead. Distinct files
still get isolated states (the relative-`GRAPHIFY_OUT` case), while
roots that collapse to one file now share and merge a single state.
`flush_stat_index` writes each key path directly. Add a serial
env-guarded regression asserting two roots under one absolute
`GRAPHIFY_OUT` merge into a single index holding both files.

Glory to the Omnissiah
Two hyperedge merges diverged from graphify-py `attach_hyperedges`,
which only appends when `h.get("id")` is truthy and records each
accepted id as it goes.

`graphify-build`'s `attach_carried_hyperedges` carried an id-less (or
empty-id) hyperedge forward instead of dropping it. Guard on a
non-empty id before insertion.

`graphify-export`'s `attach_hyperedges` built `seen_ids` once and never
updated it, so two entries sharing an id within a single batch both
appended. Record each id on insertion so an intra-batch duplicate
collapses, first occurrence winning.

Add regressions: a `build_merge` with an id-less carried hyperedge, and
an `attach_hyperedges` batch with a duplicate id.

Glory to the Omnissiah
The absolute-`GRAPHIFY_OUT` regression restored the env only after its
`cached_word_count` / `flush_stat_index` calls, so a panic in any of
those would leak the override into other serial tests. Introduce a
`GraphifyOutGuard` that restores the previous value on drop, covering
unwinding, and drop the manual restore block.

Ave Deus Mechanicus
rblaine95 added 13 commits July 14, 2026 15:55
`ensure_atexit_flush_registered` stored a `FlushSentinel` in a `static
OnceLock`, but Rust never drops `static` items at process exit, so the
sentinel's `Drop` never ran and the stat index was never written to
disk. The #1656 word-count / hash cache therefore never survived a run,
silently recomputing every time.

Replace it with a `StatIndexFlushGuard` whose `Drop` flushes; the CLI
`run` binds one for the whole invocation, so the index persists on a
normal return, an error return, or an unwind, mirroring graphify-py's
`atexit` flush. Replace the idempotence test with a subprocess
regression that mutates the index, exits normally with no explicit
`flush_stat_index`, and asserts the file was written.

By the will of the Machine God
`run` now owns a `StatIndexFlushGuard` rather than registering a static
atexit hook. Update the doc to say so and note that clap's
`--help`/`--version`/parse-error paths call `process::exit` and skip
the guard, which is harmless since they never mutate the index.

Ave Deus Mechanicus
Address CodeRabbit's review of the build port.

Carried hyperedges were filtered only by `source_file`, so a hyperedge
whose own file survived could still reference a member node from a file
that was pruned or re-extracted — a dangling reference graphify-py
keeps verbatim. Prune first, then carry forward only hyperedges whose
every `nodes` member resolves to a live node in the final graph. The
carry logic moves into a `carry_forward_hyperedges` helper (keeping
`build_merge_with_graph_cap` under the line cap).

`normalize_hyperedge_members` silently dropped a non-array member alias
(e.g. `members: "a"`), emptying the hyperedge; warn on stderr instead,
like the array-alias normalisation already does.

Add regressions: a carried hyperedge with a pruned member is dropped
while a local one survives, and a subprocess test asserting the
malformed-alias warning reaches stderr.

By the will of the Machine God
Address CodeRabbit's review of the serve port.

`maybe_reload` now reports whether it replaced the graph, and
`GraphCtx::reload` clears `idf_cache` on a successful reload. Without
this a hot reload kept the previous graph's IDF weights, so queries
after a graph-file change were ranked with stale term weights
(graphify-py sidesteps this by storing `_idf_cache` on the graph).

`call_tool` now validates `project_path`: absent / null / empty routes
to the default graph, a non-empty string routes to that project, and
any other JSON type is a tool error instead of silently falling back
to the default (matches graphify-py, where `Path()` raises on a
non-str).

Tests: an IDF-staleness regression proving a persisted cache returns
the wrong weight for a changed graph, a non-string project_path tool
error, a required-schema assertion that `project_path` stays optional,
and a bad-path test derived from a guaranteed-missing temp child.

Declined with in-code rationale (parity with graphify-py, or perf-only
in the off-by-default HTTP feature): structured `isError` results,
canonicalising/evicting the project cache, and a lock-free load path.

By the will of the Machine God
Follow-up to the serve review: rather than disputing on Python parity,
fix the findings (the resync process requires owner approval to
dispute, and these are genuine contract/robustness bugs).

- MCP wire contract: `tools/call` failures now return a result with
  `isError: true`, and `resources/read` failures a JSON-RPC error, so
  clients can distinguish failures from benign output.
- Project cache: key it by the canonicalised graph path (aliases share
  one load) and bound it at `MAX_PROJECT_CTXS` with FIFO eviction —
  `project_path` is remotely selectable, so an unbounded cache is a
  memory-exhaustion vector.
- Hot reload: `maybe_reload` now takes and clears `idf_cache` on a
  successful reload, making the invalidation a testable part of the
  reload op; the regression drives `maybe_reload` across a file change
  and asserts the cache empties (an unchanged file keeps it).
- HTTP: dispatch the blocking lock+`handle` via `spawn_blocking` so a
  slow graph load never stalls the async executor.

Ave Deus Mechanicus
Address CodeRabbit review of the cluster port: the pure `degree_map`
helper returns an owned map with no side effects, so flag a dropped
result.

Glory to the Omnissiah
Address CodeRabbit review of the semantic port: the fragment validator
took `members`/`node_ids` only when `nodes` was absent, so a hyperedge
with a present-but-non-array `nodes` and a valid alias list was wrongly
rejected. Pick the first of `nodes`/`members`/`node_ids` that is an
array, matching `normalize_hyperedge_members` (which folds the alias
onto `nodes` when `nodes` is not a list). Regression added.

Glory to the Omnissiah
The earlier `spawn_blocking` fix still held one global mutex across the
graph load and the whole tool dispatch, so a single slow project blocked
every other request and could exhaust the blocking pool. Give each
resolved graph its own `Arc<Mutex<GraphCtx>>`: `select_ctx` takes only a
brief lock to look up or insert the handle (loading OUTSIDE that lock,
double-checking on the race), then the caller locks just that project's
context for reload + dispatch.

`McpServerState` now uses interior mutability (`Mutex<Option<_>>` for the
lazily-loaded default, `Mutex<IndexMap<_, _>>` for the bounded project
cache), so `handle` takes `&self` and the HTTP transport drops its outer
mutex — concurrent requests to different projects run in parallel. A
failed load is still not cached, so a later-appearing graph resolves.

Add a multi-threaded HTTP test firing concurrent calls to two distinct
projects and asserting isolated routing.

By the will of the Machine God
Address CodeRabbit review of the dedup port: `warn_cross_chunk_collision`
fires whenever the colliding nodes have different `source_file`s, but its
text asserted they share the same base name in different directories —
which it never checks. Describe the collision as between two different
source files (noting a shared base name is the common cause) so the
message cannot mislead when the paths differ entirely.

Glory to the Omnissiah
Address CodeRabbit review of the affected port: the member-seed tests
only exercised `method` edges and a non-traversable cross-class edge.

Add a `contains`-bound variant asserting a caller of a contains-linked
member is reachable from the class (member seeding covers both edge
relations), and rework the exclusion test so `b` genuinely calls A's
member — proving `b` is reached as a caller while its own member `b_m`
is still not pulled into the walk.

Glory to the Omnissiah
Phase 5 docs pass for this review round's intentional divergences:
- `update`: carried hyperedges with a member from a deleted/re-extracted
  file are dropped (Python keeps them); malformed member aliases warn on
  stderr instead of silently emptying the hyperedge.
- `serve`: project graphs are cached by canonical path and bounded;
  tool-load failures return `isError: true` and resource-load failures a
  JSON-RPC error (Python reports both as plain text).

By the will of the Machine God
Serve re-review: a hung graph load (e.g. a stalled network filesystem)
would tie up the HTTP connection indefinitely. Wrap the blocking
dispatch join in a 2-minute `tokio::time::timeout`; on elapse return
503 Service Unavailable. The blocking task is not cancelled — it runs to
completion and still populates the cache — we simply stop waiting.

Ave Deus Mechanicus
The stat-index flush fix (scope guard) exposed a latent #1747 leak: the
incremental second run of `extract <corpus> --out <dir>` wrote a stray
`graphify-out/cache/stat-index.json` into the corpus.
`detect_incremental_with_manifest` re-walked the corpus via
`walk::detect` with no `cache_root`, so its word-count phase rooted the
stat index at the scanned corpus instead of `--out`. (The prior broken
atexit never flushed, so the leak was invisible.)

Thread `cache_root` through `detect_incremental_with_manifest` to
`walk::detect_with_cache_root`, and pass it from
`detect_incremental_with_cache_root`. The clean-corpus regression now
holds across both runs.

Glory to the Omnissiah
@coderabbitai

coderabbitai Bot commented Jul 14, 2026

Copy link
Copy Markdown

Important

Review skipped

Too many files!

This PR contains 243 files, which is 93 over the limit of 150.

To get a review, narrow the scope:
• coderabbit review --type committed # exclude uncommitted changes
• coderabbit review --dir # limit to a subdirectory
• coderabbit review --base # compare against a closer base

Upgrade to a paid plan to raise the limit.

⚙️ Run configuration

Configuration used: Repository UI

Review profile: CHILL

Plan: Pro

Run ID: 204575a2-e8de-403b-9a0c-8e21d9528d1a

📥 Commits

Reviewing files that changed from the base of the PR and between e0d53a7 and 2624e92.

⛔ Files ignored due to path filters (1)
  • Cargo.lock is excluded by !**/*.lock
📒 Files selected for processing (243)
  • Cargo.toml
  • README.md
  • USAGE.md
  • crates/graphify-affected/src/lib.rs
  • crates/graphify-affected/tests/parity.rs
  • crates/graphify-analyze/src/classify.rs
  • crates/graphify-analyze/src/cycles.rs
  • crates/graphify-analyze/tests/parity.rs
  • crates/graphify-build/src/build_fn.rs
  • crates/graphify-build/src/error.rs
  • crates/graphify-build/src/ingest.rs
  • crates/graphify-build/src/migrate.rs
  • crates/graphify-build/src/normalize.rs
  • crates/graphify-build/tests/parity.rs
  • crates/graphify-build/tests/semantic_id_remap_root.rs
  • crates/graphify-cache/src/hash.rs
  • crates/graphify-cache/src/lib.rs
  • crates/graphify-cache/src/semantic.rs
  • crates/graphify-cache/src/stat_index.rs
  • crates/graphify-cache/src/store.rs
  • crates/graphify-cache/tests/parity.rs
  • crates/graphify-cluster/Cargo.toml
  • crates/graphify-cluster/src/labels.rs
  • crates/graphify-cluster/src/lib.rs
  • crates/graphify-cluster/tests/hub_labels.rs
  • crates/graphify-dedup/src/merge.rs
  • crates/graphify-dedup/tests/parity.rs
  • crates/graphify-detect/Cargo.toml
  • crates/graphify-detect/src/extensions.rs
  • crates/graphify-detect/src/extensions_tests.rs
  • crates/graphify-detect/src/ignore.rs
  • crates/graphify-detect/src/ignore_tests.rs
  • crates/graphify-detect/src/incremental.rs
  • crates/graphify-detect/src/lib.rs
  • crates/graphify-detect/src/manifest.rs
  • crates/graphify-detect/src/office.rs
  • crates/graphify-detect/src/sensitive.rs
  • crates/graphify-detect/src/sensitive_tests.rs
  • crates/graphify-detect/src/walk.rs
  • crates/graphify-detect/tests/office_extractors.rs
  • crates/graphify-detect/tests/parity_classify.rs
  • crates/graphify-detect/tests/parity_detect.rs
  • crates/graphify-detect/tests/parity_ignore.rs
  • crates/graphify-detect/tests/parity_manifest.rs
  • crates/graphify-detect/tests/parity_sensitive.rs
  • crates/graphify-export/src/canvas.rs
  • crates/graphify-export/src/html.rs
  • crates/graphify-export/src/json.rs
  • crates/graphify-export/tests/obsidian_export.rs
  • crates/graphify-export/tests/parity.rs
  • crates/graphify-extract/Cargo.toml
  • crates/graphify-extract/src/extractors/apex.rs
  • crates/graphify-extract/src/extractors/bash.rs
  • crates/graphify-extract/src/extractors/blade.rs
  • crates/graphify-extract/src/extractors/dart.rs
  • crates/graphify-extract/src/extractors/dm/dmf.rs
  • crates/graphify-extract/src/extractors/dm/dmi.rs
  • crates/graphify-extract/src/extractors/dm/dmm.rs
  • crates/graphify-extract/src/extractors/dm/source.rs
  • crates/graphify-extract/src/extractors/dotnet/csproj.rs
  • crates/graphify-extract/src/extractors/dotnet/razor.rs
  • crates/graphify-extract/src/extractors/dotnet/sln.rs
  • crates/graphify-extract/src/extractors/dotnet/slnx.rs
  • crates/graphify-extract/src/extractors/dotnet/xaml.rs
  • crates/graphify-extract/src/extractors/elixir.rs
  • crates/graphify-extract/src/extractors/fortran.rs
  • crates/graphify-extract/src/extractors/go/calls.rs
  • crates/graphify-extract/src/extractors/go/mod.rs
  • crates/graphify-extract/src/extractors/go/refs.rs
  • crates/graphify-extract/src/extractors/go/walk.rs
  • crates/graphify-extract/src/extractors/groovy.rs
  • crates/graphify-extract/src/extractors/js_rationale.rs
  • crates/graphify-extract/src/extractors/json_lang.rs
  • crates/graphify-extract/src/extractors/julia/calls.rs
  • crates/graphify-extract/src/extractors/julia/mod.rs
  • crates/graphify-extract/src/extractors/julia/walk.rs
  • crates/graphify-extract/src/extractors/manifest_ingest.rs
  • crates/graphify-extract/src/extractors/markdown.rs
  • crates/graphify-extract/src/extractors/mcp.rs
  • crates/graphify-extract/src/extractors/mod.rs
  • crates/graphify-extract/src/extractors/multi/cache.rs
  • crates/graphify-extract/src/extractors/multi/cpp.rs
  • crates/graphify-extract/src/extractors/multi/csharp.rs
  • crates/graphify-extract/src/extractors/multi/java.rs
  • crates/graphify-extract/src/extractors/multi/js.rs
  • crates/graphify-extract/src/extractors/multi/mod.rs
  • crates/graphify-extract/src/extractors/multi/objc.rs
  • crates/graphify-extract/src/extractors/multi/pascal_resolution.rs
  • crates/graphify-extract/src/extractors/multi/python.rs
  • crates/graphify-extract/src/extractors/multi/resolvers.rs
  • crates/graphify-extract/src/extractors/multi/ruby.rs
  • crates/graphify-extract/src/extractors/multi/swift.rs
  • crates/graphify-extract/src/extractors/multi/typescript.rs
  • crates/graphify-extract/src/extractors/objc.rs
  • crates/graphify-extract/src/extractors/pascal/forms.rs
  • crates/graphify-extract/src/extractors/pascal/mod.rs
  • crates/graphify-extract/src/extractors/pascal/package.rs
  • crates/graphify-extract/src/extractors/powershell/manifest.rs
  • crates/graphify-extract/src/extractors/powershell/mod.rs
  • crates/graphify-extract/src/extractors/python_rationale.rs
  • crates/graphify-extract/src/extractors/rust_lang/calls.rs
  • crates/graphify-extract/src/extractors/rust_lang/mod.rs
  • crates/graphify-extract/src/extractors/rust_lang/walk.rs
  • crates/graphify-extract/src/extractors/sql/mod.rs
  • crates/graphify-extract/src/extractors/sql/refs.rs
  • crates/graphify-extract/src/extractors/sql/walk.rs
  • crates/graphify-extract/src/extractors/svelte.rs
  • crates/graphify-extract/src/extractors/terraform.rs
  • crates/graphify-extract/src/extractors/verilog.rs
  • crates/graphify-extract/src/extractors/zig.rs
  • crates/graphify-extract/src/generic/calls.rs
  • crates/graphify-extract/src/generic/cpp.rs
  • crates/graphify-extract/src/generic/csharp.rs
  • crates/graphify-extract/src/generic/graph.rs
  • crates/graphify-extract/src/generic/indirect.rs
  • crates/graphify-extract/src/generic/inherit/cpp.rs
  • crates/graphify-extract/src/generic/inherit/csharp.rs
  • crates/graphify-extract/src/generic/inherit/kotlin.rs
  • crates/graphify-extract/src/generic/inherit/mod.rs
  • crates/graphify-extract/src/generic/inherit/ruby.rs
  • crates/graphify-extract/src/generic/java.rs
  • crates/graphify-extract/src/generic/js_extra.rs
  • crates/graphify-extract/src/generic/mod.rs
  • crates/graphify-extract/src/generic/names.rs
  • crates/graphify-extract/src/generic/references/csharp.rs
  • crates/graphify-extract/src/generic/references/java.rs
  • crates/graphify-extract/src/generic/ruby.rs
  • crates/graphify-extract/src/generic/ts.rs
  • crates/graphify-extract/src/generic/walk.rs
  • crates/graphify-extract/src/ids.rs
  • crates/graphify-extract/src/ids_tests.rs
  • crates/graphify-extract/src/import_handlers.rs
  • crates/graphify-extract/src/lang_configs.rs
  • crates/graphify-extract/src/lib.rs
  • crates/graphify-extract/src/pg_introspect.rs
  • crates/graphify-extract/src/postprocess.rs
  • crates/graphify-extract/src/postprocess_tests.rs
  • crates/graphify-extract/src/symbol_resolution.rs
  • crates/graphify-extract/src/symbol_resolution_tests.rs
  • crates/graphify-extract/src/tsconfig.rs
  • crates/graphify-extract/src/types.rs
  • crates/graphify-extract/tests/case_sensitive_resolution.rs
  • crates/graphify-extract/tests/cross_file_multi.rs
  • crates/graphify-extract/tests/cross_language_call_resolution.rs
  • crates/graphify-extract/tests/csharp_type_resolution.rs
  • crates/graphify-extract/tests/fixtures/sample_scoped_calls.pas
  • crates/graphify-extract/tests/indirect_dispatch.rs
  • crates/graphify-extract/tests/java_type_resolution.rs
  • crates/graphify-extract/tests/js_alias_relative_paths.rs
  • crates/graphify-extract/tests/js_reexport_resolution.rs
  • crates/graphify-extract/tests/no_ast_extractor.rs
  • crates/graphify-extract/tests/parity.rs
  • crates/graphify-extract/tests/parity_languages.rs
  • crates/graphify-extract/tests/parity_postprocess.rs
  • crates/graphify-extract/tests/parity_rationale.rs
  • crates/graphify-extract/tests/pascal_call_scoping.rs
  • crates/graphify-extract/tests/pascal_resolution.rs
  • crates/graphify-extract/tests/phantom_cross_package_call.rs
  • crates/graphify-extract/tests/phantom_external_import.rs
  • crates/graphify-extract/tests/ruby_resolution.rs
  • crates/graphify-extract/tests/schema_serialization.rs
  • crates/graphify-extract/tests/ts_import_require.rs
  • crates/graphify-extract/tests/ts_member_calls.rs
  • crates/graphify-extract/tests/zero_node_cache.rs
  • crates/graphify-hooks/src/constants.rs
  • crates/graphify-hooks/src/lib.rs
  • crates/graphify-hooks/src/platform/claude.rs
  • crates/graphify-hooks/src/platform/common/fs.rs
  • crates/graphify-hooks/src/platform/common/mod.rs
  • crates/graphify-hooks/src/platform/common/skill_version.rs
  • crates/graphify-hooks/src/platform/mod.rs
  • crates/graphify-hooks/tests/parity.rs
  • crates/graphify-hooks/tests/skill_version.rs
  • crates/graphify-llm/src/azure.rs
  • crates/graphify-llm/src/bedrock.rs
  • crates/graphify-llm/src/call.rs
  • crates/graphify-llm/src/claude_cli.rs
  • crates/graphify-llm/src/deepseek.rs
  • crates/graphify-llm/src/file_slice.rs
  • crates/graphify-llm/src/gemini.rs
  • crates/graphify-llm/src/kimi.rs
  • crates/graphify-llm/src/labeling.rs
  • crates/graphify-llm/src/lib.rs
  • crates/graphify-llm/src/ollama.rs
  • crates/graphify-llm/src/openai.rs
  • crates/graphify-llm/src/openai_compat.rs
  • crates/graphify-llm/src/parse.rs
  • crates/graphify-llm/src/read.rs
  • crates/graphify-llm/src/tokenizer.rs
  • crates/graphify-llm/src/tokens.rs
  • crates/graphify-llm/src/vision.rs
  • crates/graphify-llm/tests/azure.rs
  • crates/graphify-llm/tests/bedrock_http.rs
  • crates/graphify-llm/tests/labeling.rs
  • crates/graphify-llm/tests/ollama_http.rs
  • crates/graphify-llm/tests/parallel_retry.rs
  • crates/graphify-llm/tests/parity.rs
  • crates/graphify-llm/tests/per_backend_http.rs
  • crates/graphify-llm/tests/small_modules.rs
  • crates/graphify-llm/tests/token_overhead.rs
  • crates/graphify-reflect/Cargo.toml
  • crates/graphify-reflect/src/aggregate.rs
  • crates/graphify-reflect/src/lib.rs
  • crates/graphify-reflect/src/overlay.rs
  • crates/graphify-reflect/tests/parity.rs
  • crates/graphify-report/Cargo.toml
  • crates/graphify-report/src/render.rs
  • crates/graphify-report/src/sections/communities.rs
  • crates/graphify-report/tests/parity.rs
  • crates/graphify-security/src/graph_size.rs
  • crates/graphify-security/tests/parity.rs
  • crates/graphify-semantic/src/lib.rs
  • crates/graphify-semantic/tests/parity.rs
  • crates/graphify-serve/Cargo.toml
  • crates/graphify-serve/src/graph.rs
  • crates/graphify-serve/src/http.rs
  • crates/graphify-serve/src/server.rs
  • crates/graphify-serve/src/tools.rs
  • crates/graphify-serve/tests/http_transport.rs
  • crates/graphify-serve/tests/parity.rs
  • crates/graphify-serve/tests/server_dispatch.rs
  • crates/graphify-watch/src/lib.rs
  • crates/graphify-watch/src/rebuild/helpers.rs
  • crates/graphify-watch/src/rebuild/mod.rs
  • crates/graphify-watch/src/rebuild/pipeline.rs
  • crates/graphify-watch/src/rebuild/pipeline_helpers.rs
  • crates/graphify-watch/src/rebuild/reconcile.rs
  • crates/graphify-watch/src/rebuild/relativize.rs
  • crates/graphify-watch/src/rebuild/shrink.rs
  • crates/graphify-watch/src/test_support.rs
  • crates/graphify-watch/tests/helpers.rs
  • crates/graphify-watch/tests/rebuild_pipeline.rs
  • graphify-py
  • src/cli/args.rs
  • src/cli/cluster_only.rs
  • src/cli/dispatch.rs
  • src/cli/extract.rs
  • src/cli/install.rs
  • src/cli/merge.rs
  • src/cli/mod.rs
  • src/cli/query.rs
  • tests/cli.rs
  • tests/cli_commands.rs

You can disable this status message by setting the reviews.review_status to false in the CodeRabbit configuration file.

Use the checkbox below for a quick retry:

  • 🔍 Trigger review

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

Cache re-review: when the cache dir does not exist yet (`canonicalize`
fails, e.g. a first `--out` run), `ensure_stat_index` fell back to the
raw `base` path, which could be relative or carry redundant `.`/`..`
segments — so equivalent spellings of one dir produced different
`stat-index.json` keys and competing in-memory indexes. Fall back to
`std::path::absolute` (lexical, no existence needed) before the raw
path, so the key is always a normalized absolute path. Regression added.

Glory to the Omnissiah
Cache re-review: the multi-root `flush_stat_index` returned on the first
root's I/O/serialisation error, stranding the remaining dirty roots'
entries (lost on exit). Persist every dirty root via a `persist_root`
helper, keep a failed root dirty for a later retry, and surface the
first error only after the whole sweep. Regression: a healthy root is
still flushed when an earlier root's cache dir is unwritable.

By the will of the Machine God
The `flush_continues_past_a_failing_root` regression relied on the bad
root being flushed first, but `StatIndex::roots` was a `HashMap` with
randomised iteration — under the old early-return bug the good root
could flush first and the test would pass anyway. Switch `roots` to an
`IndexMap` (insertion order), so a root populated first is flushed
first and the failing-first regression is deterministic.

Glory to the Omnissiah
Export re-review: the #1324 synthesized all-nodes group was keyed under
community id 0, so a stale `community_labels[0]` entry (labels present
but every community fully dangled) would mislabel the fallback with a
real community's name. Key it under a sentinel id outside the natural
range; `emit_community_nodes` renders that sentinel as `Community 0` /
`g0` (unchanged output when no label collides) and never consults
`community_labels`. Regression covers a label for community 0.

Glory to the Omnissiah
Semantic re-review: the alias-fold tests only checked `nodes.len() == 2`,
which would pass even if sanitization produced the wrong ids. Assert the
`nodes` array is exactly `["n1", "n2"]` (deduped, in order) in both the
alias-fold and nodes-already-a-list cases, keeping the alias-key-absent
checks.

Glory to the Omnissiah
@rblaine95

Copy link
Copy Markdown
Member Author

CodeRabbit review status

The whole-branch review (coderabbit review --agent --base master --type committed, no --dir) cannot run on this plan: the branch touches 243 files, 93 over CodeRabbit's 150-file limit. Per its own guidance ("limit to a subdirectory"), the review was therefore run per crate.

Every crate touched by this branch was reviewed and, after fixes, re-reviewed to {"type":"complete","status":"review_completed","findings":0}:

  • 0 findings first pass: report, analyze, security
  • reviewed → fixed → re-reviewed clean: extract(src), detect, reflect, hooks, build, llm, cluster, dedup, affected
  • multiple fix/re-review cycles to reach 0: cache (3), serve (2), export (2), semantic (2)

Each fix shipped as its own commit (no amends) so the iterative history is auditable. Full gate: cargo clippy --all-targets --all-features --workspace clean, cargo nextest run --workspace 2706 passed / 2 skipped, hk check clean.

@rblaine95
rblaine95 merged commit c1625ab into master Jul 15, 2026
29 of 38 checks passed
@rblaine95
rblaine95 deleted the resync/graphify-py/35665a7 branch July 15, 2026 05:50
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant