Skip to content

Add Claude Code GitHub Actions workflow#2160

Open
Solvaray wants to merge 1237 commits into
Graphify-Labs:mainfrom
Solvaray:add-claude-github-actions
Open

Add Claude Code GitHub Actions workflow#2160
Solvaray wants to merge 1237 commits into
Graphify-Labs:mainfrom
Solvaray:add-claude-github-actions

Conversation

@Solvaray

@Solvaray Solvaray commented Jul 24, 2026

Copy link
Copy Markdown

Summary

  • Adds a .github/workflows/claude.yml workflow that runs Claude Code (anthropics/claude-code-action@v1) when @claude is mentioned in issues, issue comments, or PR review comments.

Setup required

  • Repo maintainers need to add a CLAUDE_CODE_OAUTH_TOKEN secret (generated via claude setup-token in the Claude Code CLI) for the action to run.

Test plan

  • Merge and add the required secret
  • Comment @claude on an issue/PR to confirm the workflow triggers

safishamsi and others added 30 commits July 6, 2026 00:48
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
extract_pascal() already imports tree-sitter-pascal for AST-quality
extraction and falls back to a regex extractor when it is absent (Graphify-Labs#781),
but the grammar was not declared anywhere in the package metadata, so it
was never installed and the AST path never ran out of the box.

Declare a `pascal` extra (and add it to `all`) so users can opt into the
AST extractor with `uv tool install "graphifyy[pascal]"`. tree-sitter-pascal
publishes prebuilt wheels for every platform (win/macOS/Linux), so unlike
the `dm` extra it needs no C toolchain.

On a mid-size Delphi codebase the AST path yields notably more accurate
relationship edges than the regex fallback (calls and inherits both up
~25%). README extras table and uv.lock updated accordingly.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…y-Labs#1603)

Adds a _JAVA_BUILTIN_TYPES skip list so ubiquitous java.lang/util/io/time/etc.
type names (String, List, Map, Optional, ...) are not emitted as references
edges (they never resolve to a project node). Mirrors _GO_PREDECLARED_TYPES /
_PYTHON_ANNOTATION_NOISE. Nested user-type generic args still resolve.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…angelog for Graphify-Labs#1603

Applied the .claudeignore troubleshooting entry from Graphify-Labs#1539 manually (the PR
branched from an older README). Closes Graphify-Labs#1387.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
17 fixes/features since 0.9.6. Highlights:
- Ruby: mixes_in edges for include/extend/prepend (Graphify-Labs#1668) and affected <Class>
  reaching method-bound callers (Graphify-Labs#1669); constant-receiver call resolution
  hardening continues from Graphify-Labs#1634.
- Extensionless shebang CLIs are now extracted (Graphify-Labs#1683); JS/TS rationale + ADR/RFC
  doc refs (Graphify-Labs#1599); Java stdlib types dropped from references noise (Graphify-Labs#1603);
  pascal optional extra (Graphify-Labs#1616).
- Incremental/detect correctness: Office source edits re-enter --update (Graphify-Labs#1649),
  Windows long-path hashing (Graphify-Labs#1655), word-count caching (Graphify-Labs#1656), zero-node
  results no longer cached + warned (Graphify-Labs#1666).
- JS/TS phantom cross-package calls edge killed (Graphify-Labs#1659); Windows skill name +
  OpenCode plugin separator + doc-corpus report noise (Graphify-Labs#1635/Graphify-Labs#1646/Graphify-Labs#1657);
  case-insensitive suffix dispatch (Graphify-Labs#1671); postgres URI on Windows (Graphify-Labs#1672);
  deferred import() not a cycle (Graphify-Labs#1241).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…nest benchmarks table)

Rework the top of the README into a self-contained pitch:
- New logo + Trendshift badge; collapse the 31-language row into a toggle so the
  tagline and hero land in the first screen.
- Hero screenshot of the FastAPI graph, tagline with selective bold.
- 'What it does' capability table and a 'See it in action' section with verbatim
  explain/path output (real relations + confidence tags).
- Benchmarks as a scannable table, honest: shows where competitors lead, only
  bolds rows graphify wins or ties, names the tie.
- Collapse platform picker + optional extras into <details>; typography and
  identifier-formatting polish; fix orphaned paragraphs; reword the confidence-tag
  gloss; drop the redundant callflow CTA from the hero.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…vent CLAUDE.md data loss (Graphify-Labs#1688)

The updater located its managed block by substring (`marker in content`
and `next(... if marker in line)`), so a heading that merely appeared as
a substring of another line, or a duplicate heading, matched the wrong
offset and the rewrite could truncate or drop unrelated content in
CLAUDE.md / AGENTS.md.

It now matches the section heading exactly (`line.strip() == marker`),
appends when the section is absent, and prefers the last exact match
when several exist. Thanks @bdfinst for the report.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…fy-Labs#1685)

`_TOKENIZER.encode(content)` raises ValueError by default when the text
contains a special token such as `<|endoftext|>`, so a doc or corpus that
merely mentions these strings crashed the entire semantic pass. Both
`encode` sites in `_estimate_file_tokens` now pass `disallowed_special=()`
so such text is tokenized as ordinary bytes. Thanks @Kyzcreig for the report.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
… the stall (Graphify-Labs#1686)

A stalled local model wedged for `timeout * (max_retries + 1)`, which with
the default 6 retries turned one long stall into a ~21-minute block with no
progress. `_call_openai_compat` now defaults the Ollama backend to zero
client-side retries (a local model that stalls will not un-stall on retry);
set `GRAPHIFY_MAX_RETRIES` to opt back in. Other backends are unchanged.

This bounds the wait; the underlying stall is driven by the model server
and is non-deterministic, so it is not eliminated. Thanks @Kyzcreig for the report.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…n cost (Graphify-Labs#1690, Graphify-Labs#1694)

Two related fixes in the community-labeling path:

Graphify-Labs#1690 (thanks @vdgbcrypto): a truncated or slightly malformed reply no
longer discards the whole batch with "Expecting value: line 1 column 6".
`_parse_label_response` now salvages the complete `"id": "name"` pairs from
a reply that failed a strict `json.loads` (e.g. one truncated mid-object),
raising only when no pairs can be recovered. The per-batch token budget was
also raised (256 + 48*n, was 64 + 24*n) so models that prepend a short
preamble have headroom to finish the JSON. The exact provider truncation
could not be reproduced without a live key; the parser and budget address
the mechanism.

Graphify-Labs#1694 (thanks @sub4biz): cluster-only mode reported a hardcoded
`0 input * 0 output` token cost because the labeling LLM calls were never
accounted for. `_call_llm` now accumulates per-response usage into an
optional accumulator threaded through the labeling path and surfaced in
GRAPH_REPORT.md. Backends that do not return usage (the Claude Code CLI)
contribute nothing, which is honest rather than estimated.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Adds the Hebrew translation under docs/translations/README.he-IL.md and
links it from the language row (30 -> 31 languages). Merged from PR Graphify-Labs#1639
by @AdirBuskila; the language-row link was adapted to the current README
structure by hand since the English README changed after the PR was opened.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
An animated SVG that mirrors the real `graphify path` output: a terminal
types the query on the left while the answer draws itself hop by hop across
the knowledge graph on the right, ending on "3 hops. Zero files opened."

Pure SMIL inside a self-contained dark card, so it renders inline on both
GitHub themes with no JS and no external fonts. Palette is brand-only:
muted emerald (sampled from the logo) as the single accent, no neon.
Generated by scripts/gen_demo_path.py (re-run to regenerate; STATIC=1 bakes
a still frame for QA).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…r-broad filters (Graphify-Labs#1666)

@krishnateja7 root-caused this precisely: the files were never reaching
extraction, so the 0.9.7 no-cache-on-empty mitigation could not surface them.
Two discovery-layer filters were the cause:

(a) A bare `snapshots/` directory was pruned as a Jest/Vitest artifact, which
killed legitimate code namespaces like a Rails `app/services/snapshots/`. It is
now pruned only when it actually contains `.snap` files or sits directly under a
JS test root (`__tests__`/`__test__`). `__snapshots__` stays unconditionally pruned.

(b) `_is_sensitive` dropped files on a bare name-keyword hit (device_token.rb,
passwords_controller.rb) even when `classify_file` had already resolved them to
source code. A genuine programming-language source file is now exempt from the
weak keyword heuristic, while real secret stores in data/config formats
(credentials.json, secrets.yaml, .env, .pem, ...) are still caught — those route
through the CODE path for manifest parsing but are deliberately not exempted.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…ault (Graphify-Labs#1621)

@sub4biz verified against the live DeepSeek API that deepseek-v4-flash (and
v4-pro) have thinking ENABLED by default, contradicting the built-in config's
stale "non-thinking" comment (now corrected).

The naive fix (mirror the kimi branch and force thinking off) is the wrong
call: @sub4biz's production testing on real corpora found that disabling
thinking removes a rare reasoning-leak failure — which the adaptive
extraction/labeling retry already recovers from — but trades it for far more
frequent benign truncation AND measurably lower extraction quality and file
coverage, confirmed by a blind second reviewer.

So thinking stays ON by default (quality/coverage), with a documented opt-in
`GRAPHIFY_DISABLE_THINKING=1` for users who prefer run-to-run stability. Applies
to reasoning-capable OpenAI-compatible backends at both extra_body sites
(extraction + labeling). An explicit providers.json extra_body still wins, and
the moonshot/kimi branch is unchanged (it must disable thinking or content is empty).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…n Windows (Graphify-Labs#522)

The hooks were inline POSIX bash (case/esac, [ -f ], single-quoted echo),
which Windows cmd.exe/PowerShell cannot parse. On Windows the hook failed
silently, so the "run `graphify query` before grepping/reading raw files"
nudge was never injected and users fell back to manual /graphify.

The detection logic (grep-command match; source/doc extension match; skip if
the target is under the output dir; require graph.json to exist) moved into a
shell-agnostic `graphify hook-guard <search|read>` subcommand, invoked via the
absolute exe path resolved by _resolve_graphify_exe() — the exact pattern the
codex hook already uses. A single console-script invocation has no shell syntax,
so it parses identically under sh, cmd.exe and PowerShell.

Behavior on macOS/Linux is unchanged: the nudge payload is byte-identical
(compact JSON, same additionalContext text), matchers stay "Bash"/"Read|Glob"
so install/uninstall still find and replace old hooks, and the command still
contains "graphify". The graph-exists check now honors GRAPHIFY_OUT instead of
the hardcoded graphify-out/ path. Codex stays a no-op there (hook-check) because
Codex Desktop rejects additionalContext.

Detection is fully unit-tested (ported test_read_hook.py + new test_search_hook.py,
byte-identical output on POSIX); Windows execution itself is not testable in CI
here, but the mechanism is now shell-independent by construction.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…stic too (Graphify-Labs#522)

The Gemini hook was a `python -c "..."` one-liner that (a) depended on a bare
`python` being on PATH (frequently `python`/`py` or absent on Windows) and
(b) embedded backticks + escaped quotes that Windows PowerShell mangles. Same
fix as the Claude/Codebuddy hooks: it now invokes `graphify hook-guard gemini`
via the absolute exe path.

The gemini mode always returns {"decision":"allow"} (never blocks a tool) and
appends the graph nudge as additionalContext only when graph.json exists — the
BeforeTool contract Gemini expects, byte-identical to the old payload. It takes
no stdin, honors GRAPHIFY_OUT, and the matcher stays "read_file|list_directory"
so install/uninstall find and replace old hooks unchanged.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…s#522)

Adds a wide matrix over _run_hook_guard covering the search/read detection
boundaries and the gemini BeforeTool contract:

- search: grep-family variants (grep/pgrep/egrep/fgrep/ripgrep), token matches
  (rg/find/fd/ack/ag with trailing space), piped commands; and silence for
  non-search commands, empty/missing/non-string command, 'find' without a
  trailing space, 'ag' mid-word, top-level vs nested tool_input, non-dict
  tool_input, and no-graph.
- read: source/framework extensions, uppercase and multi-dot names, Windows
  backslash paths, glob patterns; and silence for .json (not .js), lockfiles,
  images, extensionless files, an extension on a directory segment, targets
  under the (default and custom-named) output dir, and no-graph.
- fail-open: malformed/empty/binary stdin and a throwing graph-existence check
  never crash or block.
- gemini: always returns {"decision":"allow"}, nudges only with a graph, and
  stays "allow" even if the check throws.
- dispatch/exit/encoding via real subprocess: missing/unknown mode exits 0
  silently, every mode exits 0 (never blocks), and the read nudge's em dash
  round-trips as valid UTF-8.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Fixes: Windows PreToolUse/BeforeTool hooks for Claude Code, Codebuddy and
Gemini CLI (Graphify-Labs#522); CLAUDE.md/AGENTS.md section-write data loss (Graphify-Labs#1688);
tiktoken special-token crash (Graphify-Labs#1685); Ollama hang retry-multiplication (Graphify-Labs#1686);
truncated community-label reply salvage (Graphify-Labs#1690); cluster-only labeling token/cost
accounting (Graphify-Labs#1694); discovery-layer file drops from snapshots/ and name-keyword
filters (Graphify-Labs#1666); deepseek thinking default + GRAPHIFY_DISABLE_THINKING opt-in (Graphify-Labs#1621).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…sec quickstart

- Badges reranked for click-through and split: lean top strip (PyPI, Downloads,
  Discord, LinkedIn, YC S26) with trust signals leading and social/company links
  at the tail; a "Community and links" footer (Discord, X, Sponsor, Book); CI
  badge dropped, book also linked from "Learn more".
- Hero leads with three differentiator bullets, corrected to match the codebase:
  code is tree-sitter AST (deterministic, no LLM, local), while docs/PDFs/images/
  video use your assistant's model or a configured API key for a semantic pass.
  Removed the inaccurate "local embedder" claim (there is no embedder; dedup is
  MinHash/LSH and there is no vector store) and qualified "zero LLM tokens" to the
  code path. "Not a vector index" is accurate.
- Added a 30-second quickstart (install + graphify install + /graphify .) above
  the fold, moved "See it in action" above "What it does" so differentiation is
  shown before it is enumerated, and compressed the "works in" list to one line.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
… in _find_node (Graphify-Labs#1704)

`_find_node` built its search term with `_search_tokens` (\w+ tokenization), so
"blockStream.ts" became "blockstream ts" (space where the '.' was) while the
node's stored `norm_label` keeps punctuation ("blockstream.ts"). The verbatim
case is already rescued by the `term == label_tokens` tier (the node label
tokenizes the same way), but that is a coincidence: if `label` and `norm_label`
diverge, an exactly-typed punctuated label fails to resolve through `explain`
even though `path`/`query` find it.

Add a punctuation-preserving `norm_query` (`_strip_diacritics(label).lower()`)
matched against `norm_label`/`bare_label` across the exact/prefix/substring tiers
(and fed to the trigram prefilter so candidates are not missed). Purely additive,
symmetric with how norm_label is stored. Regression tests cover the verbatim
file-label case and the label/norm_label divergence case that only norm_query
resolves.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…ilently dropping them (Graphify-Labs#1689)

Extensions like .r/.R (also .ejs, .ets) are in CODE_EXTENSIONS, so those files
are classified as code and counted in the scan, but there is no entry for them
in the extractor dispatch — so they produce zero nodes and are silently absent
from the graph while the CLI still reports success. The Graphify-Labs#1666 zero-node warning
deliberately skips them (it only fires when an extractor exists), so nothing
surfaced the gap.

extract() now emits a warning listing the offending extensions and counts
("N file(s) are classified as code but graphify has no AST extractor ...: .r (2)")
so a primarily-R (or .ejs/.ets) corpus no longer looks fully mapped when it is
not. Grouped by extension, fires only for files actually present with no
extractor (today: .ejs, .ets, .r). Adding real grammars for these remains the
follow-up; this removes the silent-data-loss now.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…aphify-Labs#1693)

Intermediate progress lines count against len(uncached_work) ("X/Y uncached
files"), but the final line switched to total_files ("Y/Y files"), which includes
cached hits and files with no extractor that never entered uncached_work. On a
large corpus with unsupported-language files, the total jumped upward right after
99% with no explanation. Both the parallel and sequential final lines now report
the same uncached_work denominator, so the count no longer appears to change
mid-run.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Graphify-Labs#1712)

GRAPH_REPORT.md rendered the Community Hubs section as Obsidian wikilinks, but
the `_COMMUNITY_*.md` notes they target are only created by the opt-in
`--obsidian` export — and the report is written at build time, before any export
runs. So on a default run every link dangled: inside an Obsidian vault they
spawned phantom `_COMMUNITY_*` nodes in the graph view, and outside Obsidian they
rendered as literal brackets that navigate nowhere.

`generate()` now takes `obsidian: bool = False`; by default the hubs render as a
plain list, and the wikilink form is emitted only when a caller opts in. The
Obsidian export's own community notes already cross-link each other, so the vault
stays navigable without the report's links. Mirrors the Graphify-Labs#1444/Graphify-Labs#1465 portability
fix that was applied to `export wiki`.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…ently (Graphify-Labs#1692)

When classify_file() returned None — an extensionless, non-shebang file
(Dockerfile, Gemfile, Makefile, Rakefile, LICENSE, ...) or an unsupported
extension — the file left no trace at all: not counted, not listed, nothing.
A user had no way to tell from graphify's output that those files were even
considered.

detect() now collects these into an "unclassified" list in its result, and
`graphify extract` prints a one-line summary after the scan counts:
"N file(s) not classified (no supported extension or shebang), skipped:
Dockerfile, Makefile, ...". Real code/docs are unaffected. This is the
visibility half of the issue; wiring up extractors/manifest handling for
Dockerfile/Makefile-style files remains a separate feature.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…an vault by default (Graphify-Labs#1681)

The Usage block's bare `/graphify` comment read "full pipeline on current
directory -> Obsidian vault", contradicting Step 6 (HTML viz always; Obsidian
vault only when --obsidian is explicitly given). The comment predated the
opt-in change and told agents a bare /graphify produces a vault. It now reads
"full pipeline on current directory (HTML viz; add --obsidian for a vault)".

Fixed at the skillgen source (fragments/core/core.md + the aider monolith
override), added a sanctioned-diff predicate so the monolith round-trip guard
allows the change, and re-blessed the expected/ snapshots. Every generated
skill-*.md now carries the corrected comment; the only content change across
all rendered files is this one line.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…mmar (Graphify-Labs#1702)

`.m` is shared by Objective-C implementation files and MATLAB, but the extractor
dispatch routed every `.m` to extract_objc unconditionally. Feeding real MATLAB
to the Objective-C tree-sitter grammar yields root.has_error and garbage
nodes/edges — worse than skipping, because it pollutes the graph with wrong data.

_get_extractor now content-sniffs `.m` the same way `.h` already is: a genuine
Objective-C `.m` carries an ObjC directive (@implementation/@interface/@import/
#import) and still routes to extract_objc; a `.m` without one (MATLAB, Octave)
gets no extractor, so it is surfaced by the no-AST-extractor warning (Graphify-Labs#1689)
instead of mis-parsed. `.mm` is unambiguously Objective-C++ and is left untouched.

This stops the wrong-by-omission garbage; wiring a real tree-sitter-matlab
extractor (the issue's primary ask) remains a follow-up.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Fixes since 0.9.8: explain punctuated-label matching (Graphify-Labs#1704); surface code files
with no AST extractor instead of dropping them silently (Graphify-Labs#1689); consistent
AST-extraction progress denominator (Graphify-Labs#1693); no dangling Obsidian wikilinks in
GRAPH_REPORT.md by default (Graphify-Labs#1712); MATLAB .m no longer force-parsed by the
Objective-C grammar (Graphify-Labs#1702); corrected the /graphify usage comment in the skill
files (Graphify-Labs#1681); surface unclassified files (Dockerfile/Makefile/...) instead of
vanishing (Graphify-Labs#1692).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
SECURITY.md said graphify never runs a network listener and only
communicates over stdio. README.md documents an opt-in --transport http
mode (with --host/--api-key flags) for sharing one server across a team,
which does open a network listener. Update the claim to describe the
default (stdio, no listener) and the documented opt-in exception.
HerenderKumar and others added 30 commits July 21, 2026 21:32
…phify-Labs#1540)

`uv run --with graphifyy python -m graphify` runs the system interpreter, so an
older `graphifyy` in system site-packages loads before the uv-managed one — with
no error. Env overrides like OPENAI_BASE_URL are then ignored and requests 401
against the default endpoint. The existing `skill is from graphify X, package is
Y` warning is the real fingerprint, but users dismiss it as a stale skill.

Add a troubleshooting entry next to the uvx-resolution one: name the symptom,
show how to confirm which module actually loaded, and point at
`uvx --from graphifyy` / uninstalling the stale copy.
…ify-Labs#2071)

The top-level `graphify --help` has listed --code-only since Graphify-Labs#1734, but the
`graphify extract` usage string and the README never mentioned it. A user
evaluating graphify on a "can this run with no network call" constraint sees
the extract usage / README first and can conclude the flag doesn't exist.

Add --code-only to the extract usage line, name it in the Privacy section and
the command reference, and add a test asserting the usage advertises it.
get_neighbors and get_community render every edge/member line unbounded — on a
god node or a large community that floods an MCP client's context window with
100KB+ of text in one tool result. query_graph already solves this with the
~3-chars/token cut in _subgraph_to_text; this applies the same budget rule to
the two line-list tools via a shared helper (_cut_lines_to_budget): cut at a
line boundary, report how many lines were dropped, and point at the narrowing
path (relation_filter / get_node). Default 2000 like query_graph; output under
budget is byte-identical to today.
`graphify query` builds an undirected nx.Graph (so BFS/DFS can explore
both callers and callees of the seed node), but its text renderer
assumed the BFS/DFS visit order (u, v) was always the edge's
(source, target). On an undirected graph that assumption only holds
when the seed happens to be the caller: seeding on the callee makes
BFS/DFS visit the callee first, so a `caller --calls--> callee` edge
was rendered backwards as `callee --calls--> caller`. graph.json's
own source/target fields stay correct on disk; only the query
rendering was wrong.

`graphify path` and `graphify explain` don't have this problem
because they force directed=True on load (Graphify-Labs#849, Graphify-Labs#853), and the MCP
query_graph tool's _load_graph() does the same. Doing that for CLI
`query` too was tried and reverted: forcing a DiGraph makes
G.neighbors() return successors only, so a query seeded on a
leaf/sink node (no outgoing edges) found zero neighbors instead of
its callers — a recall regression, not just a display fix, and it
would make the CLI and MCP query tools diverge in what they discover
even though they'd render direction identically.

Fix instead mirrors the _src/_tgt pattern graphify/build.py already
uses for the same underlying problem (undirected storage loses
direction): the CLI now stashes each link's true source/target on
its edge data as _src/_tgt before constructing the (still undirected)
graph, and _subgraph_to_text renders EDGE lines from _src/_tgt when
present, falling back to (u, v) otherwise. Traversal itself is
unchanged, so recall is unaffected — verified against the unpatched
CLI, the node counts returned for the same seeds are identical before
and after this fix, only the printed edge direction changes.

Adds two regression tests in tests/test_query_cli.py seeding the same
`calls` edge from both endpoints; the callee-seeded case fails on the
prior code with the exact backwards-edge symptom above.
`graphify explain "<node>"` sorts a node's connections by neighbor
degree and shows only the top 20, appending a bare "... and N more"
for the rest. On a high-degree node (a logging/error helper called
from dozens of places is typical) that leaves the exact question
explain is meant to answer - "who calls this, what's the impact?" -
unanswered for 97% of the callers, with nothing pointing at where
they live (Graphify-Labs#2009).

The top-20 list and its ordering are unchanged (no behavior change
for nodes at or under the cutoff). Past it, the cut connections are
now grouped by (direction, source_file) with counts and printed
under a "Grouped by file:" section, sorted by count descending, so
the caller/callee distribution is visible without falling back to a
repo-wide grep. The aggregation itself is capped at 20 files with
its own "... and N more files" line for the pathological case where
the remainder is spread across more files than that.

Full per-caller detail behind a flag (--callers --all / --group-by=dir
as proposed in the issue) is left for a follow-up: it's a larger,
more opinionated surface (flag naming, pagination semantics) than
the literal complaint needs, and the default output no longer hides
the answer either way.

Four regression tests in tests/test_explain_cli.py: the pre-existing
truncation notice on a 30-connection node is unchanged, the grouped-
by-file output carries real counts (3 files, one at 4 and two at 3)
that sum back to exactly the cut total (no silent loss), a
byte-for-byte no-op check for nodes at/under the cutoff, and a
boundary check pinning the cutoff itself at exactly 20 connections
(no section) vs exactly 21 (one grouped entry) — the earlier tests
sit well clear of the edge and wouldn't catch a future off-by-one.
…raphify-Labs#2082)

`from pkg import mod as alias` correctly emits the file-level
`imports_from` edge, but every downstream `alias.func()` call was
dropped: the module arm of the cross-file member-call resolver
(Graphify-Labs#1883, _resolve_python_member_calls in extract.py) matches a call
receiver against the imported module's own file stem, with no
awareness that the local binding in the importing file can be a
different name. `from pkg import mod` / `mod.func()` resolves because
the receiver ("mod") equals the stem; aliasing breaks the match
because the receiver ("alias") never does, and the calls edge
silently disappears while imports_from stays present -- the graph
looks connected, only the symbol-level reverse query comes back
empty. `import pkg.mod as alias` regresses the same way through the
same resolver.

Root cause is a missing propagation, not a missing feature: the local
alias is already parsed correctly in two places (_python_imported_names
in extractors/resolution.py, and the aliased_import branch of
_import_python in extract.py) but discarded before it reaches the
edges the module arm reads.

Fix threads the alias through as a `local_alias` field on the
`imports`/`imports_from` edge (mirroring the existing `target_file`
transient-hint pattern, Graphify-Labs#1814, including its pop-once-consumed
hygiene so the hint never reaches graph.json):
- _import_python now splits the alias off `import pkg.mod as alias`
  and stamps it on the edge instead of only using it to compute the
  bare module_name.
- _SymbolResolutionFacts.module_imports gains a 4th `local_name` slot
  so the `from pkg import submod [as alias]` submodule path (Graphify-Labs#1146)
  carries the binding through to _apply_symbol_resolution_facts,
  which now stamps `local_alias` on the edge whenever it differs from
  the submodule's own stem.
- The module arm's receiver match now accepts the tracked alias in
  addition to the module's real stem, keyed per (importing file,
  target module) so two files aliasing the same module differently
  each match their own binding.
- extract() pops `local_alias` off every edge right after
  run_language_resolvers runs, and build_from_json drops it from edge
  attrs too -- the same two spots target_file is dropped at (Graphify-Labs#1814),
  except the extract()-side pop has to happen AFTER the resolver
  reads the field, not at the earlier point _disambiguate_colliding_
  node_ids already pops target_file: that function runs before
  run_language_resolvers, so popping local_alias there would strip it
  before the resolver ever sees it and silently undo the fix above.

Known limitation, left out of scope: two different aliases bound to
the same module in the same file only resolve the last one
registered, since the match is one alias slot keyed per (importing
file, target module) -- not a regression, since the parent resolved
neither.

Adds five regression tests in tests/test_extract.py: the issue's own
shape (`from pkg import gate as m_gate`), its try/except-guarded
variant (the issue's literal repro, confirming the drop is
independent of try: nesting), the adjacent `import mathlib as m` and
dotted `import pkg.gate as g_alias` forms, and the relative `from .
import gate as r_gate` form -- plus an assertion that `local_alias`
never survives into the returned edges. All fail on the parent commit
with the exact missing-edge symptom and pass after the fix. Full
suite: 3554 passed vs. 3549 on the unfixed parent, a delta of exactly
these five new tests; ruff clean. Graphify-Labs#2080's calls-edge-direction
regression tests (test_serve.py) still pass unchanged.
…hify-Labs#2076)

The claude-cli backend delivers the extraction schema in the user turn and
trusts the model to emit raw JSON. Newer Claude Code releases treat that
prompt as an agentic task and report the result in prose instead ("Knowledge
graph extracted — 21 nodes, 20 edges…"), so the graph parses empty, reads as
truncation, and adaptive-retry bisects without ever converging.

Pass --json-schema (structured output) when the CLI advertises it — probed
once via `claude --help` and cached — so the object shape is constrained
regardless of prompt framing. Older CLIs that predate the flag keep the
user-turn prompt as a fallback. The `result` envelope still carries the JSON
string, so the parse path is unchanged.
…and real source (Graphify-Labs#2106)

The heuristic over-matched and silently dropped legitimate files:
- prose `.md`/`.rst` whose topic slug ends in a keyword (privacy-tokens.md,
  token-economics.md) — only code was exempt, not prose;
- the unbounded Stage-2 `service.account` substring (regex `.` wildcard) matched
  real source (google/oauth2/service_account.py) and prose slugs.
It also MISSED real secrets (.npmrc, .pypirc, secring, .git-credentials, and
case variants on case-insensitive filesystems), which were being indexed.

Fix: move service_account/aws_credentials to the boundary-checked keyword path
(so real source is spared, downloaded key files still drop), add a prose-note
carve-out (multi-word slugs indexed, bare `secrets.md`/`token.md` still dropped),
tighten id_rsa with a left boundary, add the missed secret dotfiles + secring,
lowercase the dir/segment comparisons, and count multi-dot slugs as multi-word.
Net effect is stricter on real secrets and stops the false-positive data loss.

Traceability: `graphify extract` now names the files skipped as sensitive (not
just a count), so a wrongly-flagged file is visible.
_xaml_csharp_class_nodes did `sorted(root.rglob("*.cs"))` where `root` comes from
_xaml_project_root walking up for a .csproj/.sln marker. On a standalone
extract_xaml call (no corpus boundary), a .xaml under a large/shared parent (a
temp dir, or a big monorepo) could resolve `root` to a broad ancestor, and the
rglob then recursively scanned the entire tree — effectively hanging (it stalled
the test suite intermittently once the shared temp dir grew). Replace the rglob
with an os.walk that prunes noise/hidden dirs (node_modules/.venv/.git/...) during
traversal and caps directories visited, so a real project scans fully while a
runaway root degrades to a fast, partial scan instead of a hang. Regression test
asserts a decoy .cs in node_modules is pruned and the real ViewModel still links.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…s#2112)

The .graphifyinclude loader and its two matcher helpers had no consumers:
commit df40e4d (Graphify-Labs#873, index dot dirs) removed the blanket dot-prefix
exclusion and with it the only call sites, leaving detect() parsing the
file on every run and then discarding the result. A .graphifyinclude was
silently a no-op.

Delete _load_graphifyinclude, _is_included, _could_contain_included_path
and the orphaned assignment; add .graphifyinclude to _SKIP_FILES so a
leftover file no longer lands in unclassified; and print a one-time
stderr note when one is present at the scan root, pointing to ! negation
patterns in .graphifyignore. Bump to 0.9.25.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The inline api.star-history.com SVG rate-limits on a repo this large and
serves a 503 instead of a chart, so the image was permanently broken.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Apache 2.0 adds an explicit patent grant, a patent-retaliation clause,
and explicit inbound-contribution terms. MIT's sublicense right permits
relicensing the combined work, so this needs no per-contributor consent;
prior contributions were made under MIT and remain available under those
terms. The original MIT text is retained in LICENSE-MIT and referenced
from NOTICE.

- LICENSE: verbatim Apache License 2.0
- LICENSE-MIT: preserved MIT text for prior contributions
- NOTICE: attribution + pointer to LICENSE-MIT
- pyproject: license = "Apache-2.0" (PEP 639 SPDX), license-files, setuptools>=77

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
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.