Skip to content

feat: module graph of one package, as Mermaid or JSON - #743

Merged
zaebee merged 4 commits into
mainfrom
feat/741-module-graph
Oct 23, 2026
Merged

zaebee merged 4 commits into
mainfrom
feat/741-module-graph

Conversation

@zaebee

@zaebee zaebee commented Oct 23, 2026

Copy link
Copy Markdown
Owner

Closes #741.

What

cgis_module_graph (MCP) and cgis module-graph (CLI) answer "how is this package built inside?" from graph.db.

  • Nodes. The package's direct children, each sub-package one node ([[sub/]]). expand lists every module below the package instead.
  • Edges by kind.
    • Imports -->, inheritance ==> and calls -.->. A symbol-level edge counts for the files that hold its ends.
    • Imports also read IMPORTS_SYMBOL, because from pkg import mod points IMPORTS at pkg, and only IMPORTS_SYMBOL names pkg.mod.
    • Calls are off by default, since they make the picture much denser.
  • Transitive reduction per kind.
    • An edge stays when it is part of a cycle, so a cycle never vanishes from the picture.
    • It also stays when nothing else reaches its target.
  • Production only. Test modules are left out (exclude_tests, --with-tests), and so are type-only imports, as the cycle tools do.
  • Outside dependencies are a table per module under the diagram, not drawn edges. A package outside is named whole (lib.deep, not lib).
  • Ids. Mermaid ids carry an m_ prefix, so a module named end or graph is safe.
  • JSON: {prefix, nodes, subpackages, edges:[{source,target,kind}], dropped, outside}, so a repository can keep and guard its own snapshot.

Two things the self-parsing gates caught

  • self.* placeholder calls. A small helper class pushed the count from 82±10 to 95. It is now plain functions.
  • Drift of cgis.query. Importing fqn, drift._scc and cohesion from the new module raised query's self-drift from 0.228 to 0.241, against a 0.24 tolerance.

Prefix

Give the prefix as the graph names it (cgis.query; cgis_find_symbol shows it). A directory above the ingest root, such as src., is dropped. The issue's example domains.reservation would need the root, e.g. app.domains.reservation on owner-api.

Example (cgis.query on this repository)

m_analysis --> m_drift ; m_drift --> m_analysis   (a real package-level cycle)
m_context --> m_engine, m_fqn, m_render ; m_drift --> m_engine

Checks

  • tests/unit/test_module_graph.py: 16 tests. They cover the reduction, including cycles, the default layers, tests excluded, expand, the outside table, the Mermaid styles and keywords, the CLI and the MCP tool.
  • self-parsing: 77 passed. mypy and interrogate are clean.
  • MCP_REFERENCE.md is regenerated by autodoc on main, so it is not edited here.

🤖 Generated with Claude Code

…aph, cgis module-graph)

"How is this package built inside?" had no tool: cgis_get_structure gives
containment, trace/impact the graph around one symbol, suggest_packages
communities and bridges. cgis_module_graph and `cgis module-graph` answer it
from graph.db (#741):

- Nodes are the package's direct children, each sub-package one node
  (`expand` for every module below it).
- Edges are imports -->, extends ==> and calls -.->, a symbol edge counting
  for the files holding its ends. Imports read IMPORTS_SYMBOL too, since
  `from pkg import mod` points IMPORTS at the package. Calls are off by
  default; type-only imports and test modules are left out.
- Each kind is transitively reduced. An edge stays when it is part of a
  cycle or when nothing else reaches its target, so cycles never vanish
  from the picture.
- Dependencies on code outside the package are a table per module, not
  edges; a package outside is named whole (lib.deep, not lib).
- Mermaid ids carry an m_ prefix, so `end` or `graph` is safe. The json
  form is {prefix, nodes, subpackages, edges, dropped, outside}.

The prefix is resolved by the surfaces through effective_prefixes (#584),
and the module keeps no import inside cgis.query: with them, query's
self-drift rose from 0.228 to 0.241 against a 0.24 tolerance. Without them
it is 0.229. The tolerance is unchanged.

Closes #741

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

@gemini-code-assist gemini-code-assist Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Code Review

This pull request introduces a new module-graph feature, adding a CLI command and an MCP tool to visualize a package's internal module dependencies as a Mermaid flowchart or JSON data. The implementation includes transitive reduction to simplify the graph and lists outbound dependencies. The review feedback suggests several code quality improvements: avoiding mutable default arguments in the MCP server and CLI options, adding defensive checks for empty or whitespace-only prefixes, optimizing the file index construction to minimize iterations, and deduplicating the layers sequence to prevent redundant rendering.

Comment thread src/cgis/api/mcp_server.py Outdated
Comment thread src/cgis/api/mcp_server.py
Comment thread src/cgis/cli.py
Comment thread src/cgis/cli.py
Comment thread src/cgis/query/analysis/module_graph.py Outdated
Comment thread src/cgis/query/analysis/module_graph.py Outdated
zaebee and others added 2 commits October 23, 2026 07:29
…in stays

Review of #743:
- Blocker: the edge-by-edge rule let parallel edges into one cycle justify
  each other, so x -> a1, x -> a2 with a1 <-> a2 lost both, and two cycles
  joined twice looked unrelated. The reduction now works on the
  condensation: every edge inside a component stays, and the edges joining
  two components go together, only when a third component carries the path.
- With --expand, `from pkg.sub import mod` no longer draws an edge to the
  empty pkg/sub/__init__ as well as to pkg.sub.mod.
- The MCP diagram carries the freshness note the other diagram tools give.
- A layer named twice is one layer; no layer is an error. Test files are
  left out of the outside table too. The Go behaviour of --expand (files,
  not packages) is stated in the module docstring.
- build_module_graph is split into helpers (C901).
- .github/test-count-baseline raised to 4433, as the floor asked.

Each fix has a test that failed first. query's self-drift is 0.231
(tolerance 0.24, unchanged).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…one pass over nodes

From Gemini's review of #743: the MCP and CLI layer defaults are tuples (the
MCP schema is unchanged: an array defaulting to imports, extends); a blank
prefix is refused by both surfaces with a usage message, with a test; the
file index is built in one pass over the nodes.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@zaebee

zaebee commented Oct 23, 2026

Copy link
Copy Markdown
Owner Author

/gemini review

@zaebee

zaebee commented Oct 23, 2026

Copy link
Copy Markdown
Owner Author

/guardian review

@codegraph-brain codegraph-brain Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Summary: Checked the transitive reduction (SCC condensation, implied-edge check, cycle preservation), the file-to-group mapping for collapsed and expanded views, outside-package summarisation, and the ontology edge types used (IMPORTS, IMPORTS_SYMBOL, EXTENDS, CALLS), which all exist in the ontology; the reduction logic and tests for cycles and parallel edges look correct. Flagged the Mermaid node-id collision in expand mode and a broad ValueError handler in the CLI.


🤖 claude-haiku-5-5 · 26,437 prompt + 8,111 completion = 34,548 tokens · graph 4/6 files (67%)

Comment thread src/cgis/query/analysis/module_graph.py Outdated
Comment thread src/cgis/cli.py Outdated
github-actions Bot added a commit that referenced this pull request Oct 23, 2026

@gemini-code-assist gemini-code-assist Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Code Review

This pull request introduces a new module graph feature to visualize and analyze the internal dependencies of a package. It adds a new cgis_module_graph MCP tool, a module-graph CLI command, and the underlying logic in module_graph.py to extract, transitively reduce, and render the module graph in Mermaid or JSON formats. Feedback on the pull request suggests improving the _outside_package helper function to correctly resolve deeply nested outside dependencies by finding the longest matching package prefix.

Comment thread src/cgis/query/analysis/module_graph.py
…re the graph

From guardian's review of #743 (round 2):
- Folding a name to [A-Za-z0-9_] is not injective: pkg.a.b and pkg.a_b both
  gave m_a_b, and Mermaid merged them. Ids are now assigned once per render,
  a collision getting _2, _3... in sorted order.
- The CLI validates --layer (checked_layers, now public) before it opens
  the graph, so its ValueError handler no longer wraps unrelated code.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@sonarqubecloud

Copy link
Copy Markdown

@zaebee
zaebee merged commit 0ea24a4 into main Oct 23, 2026
4 checks passed
@zaebee
zaebee deleted the feat/741-module-graph branch October 23, 2026 07:55
zaebee added a commit that referenced this pull request Oct 23, 2026
* refactor(module-graph): clear the three Sonar issues #743 left

- transitive_reduction (S3776, 18): the condensation is built in _condense,
  and "reachable through a third component" is _implied over a generic
  _reach, instead of a nested search.
- build_module_graph (S3776, 16): the edge loop moves to _collect.
- test_module_graph (S3415): the one assertion with its expected side first
  is turned round.

Behaviour is unchanged: the same 25 tests pass, and query's self-drift is
0.230 (tolerance 0.24).

Closes #746

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* perf(module-graph): one walk per implied check again

Review of #747: _implied ran a separate full _reach from each successor,
where the code it replaced did one DFS from all of them, so the "behaviour-
preserving" refactor made the reduction up to 2.6x slower on a synthetic
2000-node / 12000-edge graph. _reach now takes several starts, and
_implied walks once from all of a's other successors.

Measured with the reviewer's bench against the pre-refactor function: same
output on 3000 random graphs; ratio now 1.0x / 1.5x / 1.2x (cyclic 0.05,
cyclic 0.01, DAG; was 1.0x / 2.6x / 1.9x). The remainder is the old early
exit on reaching b, kept out to stay under Sonar's complexity limit; real
module graphs have tens of nodes.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* test(module-graph): a type-only import draws no edge

From guardian's review of #747: the type_only filter in the edge
collection had no test. One now ingests an import under TYPE_CHECKING and
expects no edge; it fails with the filter removed.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 5.5 <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.

feat: module graph of one package — modules as nodes, imports/calls/extends as edges, transitively reduced, Mermaid

1 participant