Skip to content

feat(export): add Calls view lens to graph.html, code symbols only#12

Merged
Vit129 merged 5 commits into
mainfrom
worktree-ethereal-rolling-diffie
Jul 18, 2026
Merged

feat(export): add Calls view lens to graph.html, code symbols only#12
Vit129 merged 5 commits into
mainfrom
worktree-ethereal-rolling-diffie

Conversation

@Vit129

@Vit129 Vit129 commented Jul 18, 2026

Copy link
Copy Markdown
Owner

Summary

  • graph.html gets a 4th view lens, "Calls" — alongside Community/File/Dependencies. Per-symbol granularity (unlike Dependencies' file collapse), but hides every non-code file_type node (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.
  • Search fixes: (1) now respects the active lens (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's query.py _tokenize) instead of plain substring; (3) also matches source_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.
  • Legend fix: Calls-lens file legend/select-all counts now come from code-type nodes only (fileScopedNodes()), so the displayed count matches what the lens actually shows.
  • Docs: README's upstream-diff bullet now names the 4 lenses instead of the generic "3D/lens viewer", and records upstream fix(extract): recognize uppercase TypeScript extensions Graphify-Labs/graphify#1881 (v0.9.16, uppercase .TS/.TSX/.MTS/.CTS dispatch) — found live-diffing against upstream/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-string
  • node --check on the extracted <script> blocks from a generated graph.html after each round of edits — JS syntax valid
  • Runtime-verified the ported tokenize() against the project's own real motivating examples (handleNotification, _query_terms, FooBarService, calcPaperPortfolioValue) by extracting it live from generated output and running it in node

🤖 Generated with Claude Code

https://claude.ai/code/session_01CvDvoAyysaoQiWPmmLmLqR

Vit129 and others added 3 commits July 18, 2026 20:20
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
Vit129 marked this pull request as ready for review July 18, 2026 14:23
@Vit129
Vit129 merged commit e95104b into main Jul 18, 2026
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
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