feat(export): add Calls view lens to graph.html, code symbols only#12
Merged
Conversation
Community lens colors by Leiden cluster, which mixes short code-symbol labels with long prose concept/rationale node labels from the LLM extraction pass, making the default view hard to read as "just code". Calls stays at per-symbol granularity (unlike the file-collapsed Dependencies lens) but hides every non-code file_type node and restricts edges to the same real call/dependency relation whitelist Dependencies already used, giving a plain function-to-function view. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01CvDvoAyysaoQiWPmmLmLqR
…h tokens/file paths Search ignored the current lens, so a hidden concept/rationale node in the new Calls lens could surface as a result and clicking it panned to nothing. Also plain substring-on-label only, missing camelCase/snake_case identifier splits and source_file matches. Adds isNodeHidden filtering, a ported tokenize() (same split rule as query.py's _tokenize), file-path matching, and a file_type dropdown filter — tiered ranking, not a BM25 port, since this is a client-side <=20-result filter, not a corpus search. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01CvDvoAyysaoQiWPmmLmLqR
…lenses + upstream Graphify-Labs#1881 Legend/select-all counts for the Calls lens (graphify/export.py) counted every node's source_file regardless of file_type, so a file with only concept/rationale nodes inflated the visible-file count even though nothing from it shows in that lens. Added fileScopedNodes() and used it everywhere the file legend counts/toggles files. README: name the graph.html lenses (Community/File/Dependencies/Calls) in the upstream-diff bullet instead of the generic "3D/lens viewer", and add upstream Graphify-Labs#1881 (v0.9.16, uppercase .TS/.TSX/.MTS/.CTS dispatch) as a second, narrower gap in the same TS-extraction area — found during the 2026-07-18 live re-verification against upstream/v8 fb992ce (v0.9.19) that confirmed every other upstream-diff claim in README still holds. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01CvDvoAyysaoQiWPmmLmLqR
Vit129
marked this pull request as ready for review
July 18, 2026 14:23
…ling-diffie # Conflicts: # CHANGELOG.md
Vit129
added a commit
that referenced
this pull request
Jul 19, 2026
) * docs(plans): add P17 post-competitor-audit roadmap Records the prioritized punch-list from this session's search-quality benchmark (6 real projects) + upstream diff + 3-agent competitor research pass (PKM tools, code-graph MCP tools, AI-agent context approaches), so it survives context compaction: file-watcher auto-sync first (biggest gap vs CodeGraph/GitNexus), then PageRank-style ranking, a 0.18.0 cut for PR #12's already-merged content, the affected --relation prefix-match gap, the Home-Assistant config decision, and content-as-data indexing (still held pending explicit sign-off). Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01CvDvoAyysaoQiWPmmLmLqR * docs(plans): mark P14 done - doc was stale, code already shipped Verified live: --no-open/webbrowser.open/osascript reload in __main__.py, settings-2d/settings-3d slider panels in export.py. The doc's remaining 3 goals landed in e4e4f9c and d79b6fb but the plan doc itself was never updated to reflect it. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01CvDvoAyysaoQiWPmmLmLqR * docs(plans): design file-watcher auto-sync (P17 item 1) Orientation first, per the roadmap's own instruction: graphify watch (watch.py) already exists and works - a real watchdog-based daemon, not a stub. The actual gap is that nothing auto-starts it for an AI agent session; also found _install_claude_hook's printed message already (falsely) claims a post-edit rebuild happens. Decision: add a PostToolUse hook that triggers a debounced background `graphify update`, not an auto-started `watch` daemon - closes the exact gap this session hit (agent edits, forgets to rebuild) with no new dependency (watchdog is optional/not installed by default) and no new process-lifecycle surface (PID files, orphaned daemons across session restarts). `graphify watch` stays as-is for the human-editing- in-an-IDE case, which is a different, unconfirmed need. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01CvDvoAyysaoQiWPmmLmLqR * docs(plans): correct file-watcher-auto-sync design - missed a 3rd existing mechanism graphify/hooks.py installs real git post-commit/post-checkout hooks with a detached background rebuild (hooks.py:230/483) - already installed in this repo (the recurring graphify-out/.last-refresh.log line seen on every commit this whole session was this hook firing). Missed in the design's first orientation pass despite that being its own explicit stated principle. Caught before task-design proceeded on a wrong premise. Reframes the gap correctly: not "no auto-rebuild exists" (two mechanisms already do, watch's filesystem daemon and hooks.py's commit hook) but "neither covers the uncommitted-edit window an agent session actually lives in." The PostToolUse-hook decision (option B) still stands - it's the one missing trigger point, not a replacement for either existing mechanism. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01CvDvoAyysaoQiWPmmLmLqR * docs(plans): task-design for file-watcher auto-sync (P17 item 1) 6 tasks, Server Logic (trigger_background_update in watch.py, a --trigger CLI entry, the _POST_EDIT_HOOK constant) then Integration (wire into _install_claude_hook/_uninstall_claude_hook, full test suite, live validation against a real repo). Confirmed two concrete pre-existing gaps along the way: tests/test_watch.py exists (extend, don't create new), but _install_claude_hook/_uninstall_claude_hook have zero test coverage at all - closed as a side effect of task 4 rather than left as separate cleanup. Not implemented yet - task list only, awaiting go-ahead per this session's established pattern (design/plan first, confirm, then code). Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01CvDvoAyysaoQiWPmmLmLqR * feat(watch): trigger_background_update() - task 1 of file-watcher auto-sync Spawns a detached, non-blocking background rebuild for a project root. Meant to be called once per agent write via a future PostToolUse hook (task 3) - covers the window neither existing auto-rebuild mechanism does: graphify watch's foreground daemon (must be started by hand) and hooks.py's git post-commit hook (fires only on commit) both miss uncommitted, mid-session edits. Deviated from the original task-design plan on implementation: _rebuild_code already has its own non-blocking lock + queue/drain coalescing (watch.py:459-480), so no new debounce-marker file or reimplemented PID check was needed - a burst of rapid triggers already collapses into the fewest rebuilds necessary via that existing mechanism. Detach technique mirrors hooks.py's _LAUNCHER_TEMPLATE flags exactly (proven cross-platform there already) as plain Python, since this call site has no shell layer to work around. Verified: 5 new unit tests (spawn shape, argv-passed paths, log dir creation, spawn-failure survival, _TRIGGER_BODY standalone-parses) + a real non-mocked end-to-end smoke test (detached child produced a correct graph.json from a real 2-function scratch corpus). Full suite: 3003 passed, 0 failed. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01CvDvoAyysaoQiWPmmLmLqR * feat: implement post-edit hook for auto-syncing the knowledge graph * docs(plans): design PageRank-style ranking (P17 item 2) Orientation first, per this session's established discipline: _score_nodes/_pick_seeds read at their current line numbers, god_nodes(by="pagerank") confirmed as real existing precedent (not reimplementing PageRank), scipy confirmed optional and NOT in the `all` extras bundle (pyproject.toml:64) - this cannot become a hard dependency of query/explain/path. Benchmarked nx.pagerank on the largest known real graph (kouen-terminal, 15,251 nodes): 0.297s - cheap once at build time, too slow to redo per query, settling the precompute-vs-query-time question with a real number instead of a guess. Found the exact existing hook point to store it (to_json's node["community"] = ... loop, export.py:1001-1050) and the exact existing config-threading convention to mirror (value_coupling's plain proj_config.get() read, no schema file involved - verified config.py is an unschemad generic TOML loader). Decision: opt-in via graphify.toml's pagerank_ranking key (mirrors value_coupling/P15's exact rollout shape), precomputed once at build time and stored as a node attribute, composed as a small bounded multiplicative boost (max +15%) inside _pick_seeds' existing _seed_penalty step - not a separate re-scoring pass, not additive to raw BM25 scores. Reasoning for each choice fully documented in the design doc's Strategic/Tactical sections. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01CvDvoAyysaoQiWPmmLmLqR * docs(plans): task-design for PageRank ranking (P17 item 2) 7 tasks: shared scipy-missing message constant, _rebuild_code computes+passes PageRank, to_json writes the attribute, _seed_penalty composes a bounded boost (candidate-list max, not full-graph scan - avoids undoing the build-time-precompute performance win), config/CLI threading, full suite, live validation against the named motivating case (kouen-terminal's zoom/fullscreen query). Verified exact test file locations before writing the plan rather than leaving "grep first" placeholders: tests/test_analyze.py + test_watch.py (god_nodes pagerank), tests/test_serve.py:938+ (_pick_seeds, has the exact concept-penalty precedent pattern to mirror), test_watch.py likely for --value-coupling CLI threading (test_value_coupling.py turned out to be the YAML extractor's edge logic, not CLI flag tests). Not implemented yet - task list only, awaiting go-ahead per this session's established pattern. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01CvDvoAyysaoQiWPmmLmLqR * refactor(analyze): shared scipy-missing message constant - task 1 of pagerank ranking Extracted the install-instructions half of god_nodes(by="pagerank")'s ImportError message into _PAGERANK_SCIPY_MISSING_MSG, so the upcoming _rebuild_code pagerank-ranking path (task 2) can reuse the exact same install instructions with its own natural context prefix, instead of drifting into a second wording over time. Message text verified byte-identical to the original. Zero behavior change - pure refactor. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01CvDvoAyysaoQiWPmmLmLqR * feat(watch,export): compute+persist PageRank on graph.json nodes - tasks 2+3 _rebuild_code gets a new pagerank_ranking: bool = False parameter, mirroring value_coupling's exact shape (P15). When enabled, computes nx.pagerank(G) right before the existing to_json call, guarded by try/except ImportError (scipy is an optional dependency, not bundled in the `all` extras) - printing the shared install-instructions message on failure and building normally either way. Forwarded through both recursive acquire_lock=False self-call sites - caught myself missing the second one via grep verification before moving on, the same forwarded-kwargs bug class value_coupling shipped once already (#value_coupling P15 changelog note). to_json gets a new pagerank_scores parameter, writing a "pagerank" node attribute in the same loop that already attaches "community" - omitted entirely (not even null) when not provided, so every existing call site's output is byte-identical to before. Implemented together (task 3's to_json parameter is a hard prereq for task 2's call site to be complete/testable) rather than leaving task 2 in a syntactically-incomplete state. Verified: 5 new tests (3 watch.py + 2 export.py) covering the enabled/ disabled/scipy-missing paths and the default-omits-key regression guard, plus a real non-mocked end-to-end smoke test confirming the computed PageRank values are structurally sensible (hub/shared-leaf nodes ranked above individual branches), not just present. Full suite: 3011 passed, 0 failed, 0 regressions. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01CvDvoAyysaoQiWPmmLmLqR * feat(query): compose bounded PageRank boost into _seed_penalty - task 4 The riskiest task in this feature: touches _pick_seeds, the scoring path every query/explain/path call goes through. Added _PAGERANK_BOOST_MAX = 0.15 and extended _seed_penalty to multiply its existing concept/prose penalty by 1.0 + BOOST_MAX * (node_pagerank / max_pagerank_in_candidates) - max computed once per call, over the already-BM25-narrowed candidate list only, not a full graph scan (would undo task 2's build-time-precompute performance win). Zero regression guarantee for the default case: when no candidate carries a pagerank attribute (every graph built without pagerank_ranking, i.e. today's entire installed base), the boost multiplication is skipped entirely - literally the same code path as before this feature existed, not just numerically a no-op. Verified: 4 new tests (near-tie tiebreak, boundedness - hand-verified a max 15% boost can't close a 10x raw BM25 gap, zero-pagerank regression guard, missing-attribute-on-some-nodes doesn't crash) - all 15 pre-existing _pick_seeds tests (concept/prose penalty behavior) pass unchanged. Full suite: 3015 passed, 0 failed, 0 regressions. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01CvDvoAyysaoQiWPmmLmLqR * feat(__main__): wire pagerank_ranking through config/CLI - tasks 5+6 Config read (proj_config.get("pagerank_ranking", False)) and --pagerank-ranking CLI flag, mirroring value_coupling's exact shape. Scope expanded past the original plan, documented rather than silently absorbed: grepping every to_json/_to_json call site (not just the ones named in the task doc) found two more independent graph-build paths beyond _rebuild_code - the standalone `extract` command (builds from scratch, never calls _rebuild_code) and `cluster-only` (re-clusters an existing graph.json). Both needed a real decision: - `extract` command: genuinely needed wiring - added the same compute-pagerank-before-to_json pattern, config read, and CLI flag. Verified live: `graphify extract . --pagerank-ranking` on a real scratch corpus produced pagerank values matching _rebuild_code's own computation on an equivalent corpus exactly - two independent code paths agreeing is a real correctness signal. - `cluster-only`: verified empirically that NO change was needed - build_from_json generically preserves arbitrary node attributes (including pagerank) from the loaded graph.json, and to_json's conditional-write design never deletes an attribute it wasn't asked to set. Confirmed with a real round-trip (pagerank values byte-identical before/after cluster-only). Also theoretically correct: pagerank depends only on graph structure, which cluster-only never changes. No CLI-level test precedent exists for --value-coupling either (checked before deciding not to add a new mock-heavy CLI test file for --pagerank-ranking specifically) - real live end-to-end checks used instead, matching this session's validate-against-the-real-thing discipline. Full suite: 3015 passed, 0 failed, 0 regressions across all three touched command paths (update, update --all, extract). Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01CvDvoAyysaoQiWPmmLmLqR * docs(plans): task 7 live validation - honest result, feature done Enabled pagerank_ranking on kouen-terminal (real repo), rebuilt (100% of 15393 nodes got a pagerank attribute), re-ran the exact original benchmark query. The correct answer did not surface in the top results - root-caused why rather than stopping at the negative observation: SessionEditor.zoomPane() scores #7 of 1765 real BM25 candidates, ~27% behind a genuine keyword-collision false positive (.addNewTab() matching "add"/"new"/"tab" from the query) - a real relevance gap that exceeds the boost's 15% ceiling by design, not a near-tie the feature was ever built to close. Implementation verdict: correct and shippable as designed (Task 4's test suite already proves this in isolation this feature just wasn't sufficient to fix the specific case the roadmap named as motivation. Documented in both this task-progress file and a follow-up note in p17-post-competitor-audit-roadmap.md, so the roadmap accurately reflects what shipped and what the next real hypothesis should be (P9's synonym/vocabulary territory, not more ranking tweaks) rather than silently declaring victory. Cleaned up kouen-terminal fully after testing - graphify.toml reverted, the 4 git-tracked graphify-out/ files restored via git checkout, none of the repo's own unrelated in-progress work touched. All 7 tasks done. PR #15 ready for review. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01CvDvoAyysaoQiWPmmLmLqR --------- Co-authored-by: Claude Sonnet 5 <noreply@anthropic.com>
Vit129
added a commit
that referenced
this pull request
Jul 19, 2026
Covers everything merged to main since 0.17.0: PR #12 (graph.html Calls lens + lens-aware search), item 1's file-watcher auto-sync (PostToolUse hook, trigger_background_update - landed via #15's squash merge since that branch built on top of #14's tip; #14 itself closed without a separate merge to avoid re-applying already-shipped content), and #15 (opt-in PageRank query ranking, kept default-off after evidence-gathering on 4 real repos found it net negative/neutral as a default). Bumps pyproject.toml, version.json + CURRENT_VERSION, and adds the two CHANGELOG entries that were missing (auto-sync and pagerank ranking never got one during their own PRs). Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01CvDvoAyysaoQiWPmmLmLqR
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
codefile_typenode (concept/rationale/document/paper) and restricts edges to the real call/dependency relation whitelist Dependencies already used — a plain function-to-function/class-to-class view without prose-label clutter. Motivated by the default Community lens mixing short code-symbol labels with long prose concept/decision node labels.isNodeHidden) — previously a hidden concept/rationale node in Calls could surface as a result and clicking it panned to nothing; (2) tokenizes on camelCase/snake_case boundaries (ported from the CLI'squery.py_tokenize) instead of plain substring; (3) also matchessource_file; (4) file_type dropdown filter. Deliberately not a BM25/typo-correction/synonym-expansion port — tiered ranking is enough for a client-side ≤20-result filter.fileScopedNodes()), so the displayed count matches what the lens actually shows..TS/.TSX/.MTS/.CTSdispatch) — found live-diffing againstupstream/v8@fb992ce(v0.9.19, 2026-07-18), which confirmed every other upstream-diff claim in README still holds 13 releases later.Test plan
pytest tests/test_export.py— 33 passed (Calls lens, search fixes, legend-count fix)pytest tests/(full suite) — 2995 passed, 28 skipped, 1 pre-existing unrelated failure (test_collect_files_skips_hidden— fails only because this worktree lives under a.claude/path segment, not caused by this change)python -c "import ast; ast.parse(...)"with-W error::SyntaxWarning— export.py's embedded JS backslash-escapes correctly double-escaped for the Python f-stringnode --checkon the extracted<script>blocks from a generatedgraph.htmlafter each round of edits — JS syntax validtokenize()against the project's own real motivating examples (handleNotification,_query_terms,FooBarService,calcPaperPortfolioValue) by extracting it live from generated output and running it innode🤖 Generated with Claude Code
https://claude.ai/code/session_01CvDvoAyysaoQiWPmmLmLqR