Skip to content

feat(serve): add multi-graph support to MCP server (#581) - #2099

Open
YanisGuerault wants to merge 2 commits into
Graphify-Labs:v8from
YanisGuerault:feat/add-multi-graph-support
Open

feat(serve): add multi-graph support to MCP server (#581)#2099
YanisGuerault wants to merge 2 commits into
Graphify-Labs:v8from
YanisGuerault:feat/add-multi-graph-support

Conversation

@YanisGuerault

@YanisGuerault YanisGuerault commented Jul 22, 2026

Copy link
Copy Markdown

Summary

  • Enable serving multiple knowledge graphs from a single MCP endpoint via --graphs-dir flag or GRAPHS_DIR env var
  • Always registry-based architecture: single graph = 1-entry registry, multi-graph = directory scan — no separate code path
  • Resolve issue MCP server must work with multiple repos #581

Changes

  • Add GraphContext dataclass + GraphRegistry (from_path, from_directory, hot-reload via rescan)
  • Refactor _build_server to accept GraphRegistry with per-call context resolution (_get_ctx)
  • Tool visibility keyed on registry size: list_graphs/use_graph when >1, PR tools when ==1
  • New graph param on all tool schemas (replaces project_path)
  • --graphs-dir CLI flag forces HTTP transport with daemon rescan thread

🤖 Generated with Claude Code

@YanisGuerault
YanisGuerault force-pushed the feat/add-multi-graph-support branch from 12f436c to 6d020bd Compare July 22, 2026 10:24
@YanisGuerault YanisGuerault changed the title feat(serve): add multi-graph support to MCP server feat(serve): add multi-graph support to MCP server (#581) Jul 22, 2026
@YanisGuerault
YanisGuerault force-pushed the feat/add-multi-graph-support branch from 6d020bd to 2e39ab0 Compare August 25, 2026 13:56

@graphify-labs graphify-labs Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Graphify reviewed this change.

Worth a look — the grounded gate found no coupling regressions or blocking issues, but 5 advisory finding(s) below merit a look before merge.

Formal verification. No changes could be formally verified in this run.


Graphify review — findings

Adds a multi-graph MCP server mode alongside the existing single-graph one: a new GraphRegistry loads either a single graph.json or every <name>/graph.json under a directory, rescans on an interval to pick up file changes (dropping graphs whose files vanish or fail to load), and _resolve_graph selects a target by explicit graph param, session default, or the sole loaded graph. Reworks _build_server to route all tool handlers through the registry and expose list_graphs/use_graph, and returns its handler dict for testing. Ships a multi Docker target driven by GRAPHS_DIR/SCAN_INTERVAL/PORT env vars plus a docker-compose.multi.yml and README docs, leaving the default single-graph image and entrypoint unchanged.

Worth a look

  • Default Docker build now produces multi-graph image instead of existing single-graph imageDockerfile:28 · Escalate · high
    • agreed by 2 of 2 members but NOT verified (no proof, no reproducing execution) — consensus is not a verdict; needs human review
  • Multi-graph compose publishes unauthenticated MCP service by defaultdocker-compose.multi.yml:11 · Escalate · medium
    • agreed by 2 of 2 members but NOT verified (no proof, no reproducing execution) — consensus is not a verdict; needs human review
  • except (OSError, SystemExit, Exception) swallows SystemExit and all errors, deleting graphgraphify/serve.py · Escalate · medium
    • agreed by 2 of 2 members but NOT verified (no proof, no reproducing execution) — consensus is not a verdict; needs human review
  • GraphRegistry.get/names read shared _graphs without lockgraphify/serve.py · Escalate · medium
    • agreed by 2 of 2 members but NOT verified (no proof, no reproducing execution) — consensus is not a verdict; needs human review
  • Non-atomic single-graph resolution can return None during concurrent rescangraphify/serve.py:146 · Escalate · medium
    • agreed by 2 of 2 members but NOT verified (no proof, no reproducing execution) — consensus is not a verdict; needs human review
Analysis details — impact, health, verification

Impact & health

Graphify review

Impact — 1850 functions depend on the 1647 functions this change touches.

Health — this change adds coupling hotspots:

  • new: dispatch_command() — 2 callers, 122 callees
  • new: _build_server() — 12 callers, 17 callees
  • new: _query_graph_text() — 20 callers, 10 callees
  • new: _score_query() — 15 callers, 6 callees
  • new: _build_http_app() — 19 callers, 4 callees
  • new: _query_terms() — 20 callers, 3 callees
  • new: _subgraph_to_text() — 20 callers, 3 callees
  • new: _main() — 8 callers, 7 callees
  • …and 14 more — each is listed as a finding

Verification — 1850 functions in the blast radius were not formally verified this run (proofs are advisory here).

Gate & verification

graphify gate

PASS — objectively clean (no health regressions, tests not run — proofs not run this pass (advisory)). Grounded, not self-assessed.

Advisory (not blocking):

  • verification_scope: 1689 function(s) in the blast radius were not formally verified this run

Formal verification

Could not verify: Could not verify \_\_init\_\_.

The verifier did not have enough to check \_\_init\_\_, so it is saying so rather than guessing. No false assurance is the whole point.

Guarantee: No guarantee either way, this is an honest abstention, not a pass.

Note: Reason: signature changed: no shared positional arity

Could not verify: Could not verify \_build\_http\_app.

The verifier did not have enough to check \_build\_http\_app, so it is saying so rather than guessing. No false assurance is the whole point.

Guarantee: No guarantee either way, this is an honest abstention, not a pass.

Note: Reason: not verifiable: all 24 sampled inputs raised on both versions — the function never executed, so 'no divergence' would be vacuous (mostly SystemExit — names the real obstacle, not a sampling gap)

Could not verify: Could not verify \_build\_server.

The verifier did not have enough to check \_build\_server, so it is saying so rather than guessing. No false assurance is the whole point.

Guarantee: No guarantee either way, this is an honest abstention, not a pass.

Note: Reason: not verifiable: all 54 sampled inputs raised on both versions — the function never executed, so 'no divergence' would be vacuous (mostly AttributeError — names the real obstacle, not a sampling gap)

Could not verify: Could not verify \_\_init\_\_.

The verifier did not have enough to check \_\_init\_\_, so it is saying so rather than guessing. No false assurance is the whole point.

Guarantee: No guarantee either way, this is an honest abstention, not a pass.

Note: Reason: signature changed: no shared positional arity

Could not verify: Could not verify \_\_init\_\_.

The verifier did not have enough to check \_\_init\_\_, so it is saying so rather than guessing. No false assurance is the whole point.

Guarantee: No guarantee either way, this is an honest abstention, not a pass.

Note: Reason: signature changed: no shared positional arity

Verification did not run: Verification did not run for \_main.

The verification could not execute (an environment/toolchain issue, not a statement about the code).

Guarantee: No guarantee, the check itself did not complete.

Note: Detail: harness produced no verdict (rc=1): error: Graph path must be a .json file, got: '/workspace/job-a1e2a6c2baa84ce596d72365c42bb445/head/a' error: Graph path

Could not verify: Could not verify \_\_init\_\_.

The verifier did not have enough to check \_\_init\_\_, so it is saying so rather than guessing. No false assurance is the whole point.

Guarantee: No guarantee either way, this is an honest abstention, not a pass.

Note: Reason: signature changed: no shared positional arity

Verification did not run: Verification did not run for serve.

The verification could not execute (an environment/toolchain issue, not a statement about the code).

Guarantee: No guarantee, the check itself did not complete.

Note: Detail: harness produced no verdict (rc=1): error: Graph path must be a .json file, got: '/workspace/job-a1e2a6c2baa84ce596d72365c42bb445/head/a longer string' erro

Verification did not run: Verification did not run for serve\_http.

The verification could not execute (an environment/toolchain issue, not a statement about the code).

Guarantee: No guarantee, the check itself did not complete.

Note: Detail: harness produced no verdict (rc=124): timeout after 30s

· 9 grounded finding(s) anchored inline below; 13 more finding(s) on lines outside this diff (see the check run).

Comment thread graphify/serve.py Outdated
)


def _load_graph(graph_path: str) -> nx.Graph:

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Health regression_load_graph()

12 callers depend on it (afferent coupling).

Grounded coupling-delta finding (deterministic), not an LLM guess.

Comment thread graphify/serve.py Outdated


def _build_server(graph_path: str):
def _build_server(registry: GraphRegistry, *, session_state: dict | None = None):

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Health regression_build_server()

fans out to 17 callees (efferent coupling); 12 callers depend on it (afferent coupling).

Grounded coupling-delta finding (deterministic), not an LLM guess.

Comment thread graphify/serve.py
path = _resolve_graph_path(project_path)
G, communities = _load_ctx(path)
active_graph_path = str(Path(path).resolve())
def _get_ctx(arguments: dict) -> GraphContext:

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Health regression_get_ctx()

9 callers depend on it (afferent coupling).

Grounded coupling-delta finding (deterministic), not an LLM guess.

Comment thread graphify/serve.py
@@ -1781,6 +1919,8 @@ def _tool_get_node(arguments: dict) -> str:
])

def _tool_get_neighbors(arguments: dict) -> str:

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Health regression_tool_get_neighbors()

fans out to 8 callees (efferent coupling).

Grounded coupling-delta finding (deterministic), not an LLM guess.

Comment thread graphify/serve.py
@@ -1884,6 +2035,8 @@ def _tool_list_prs(arguments: dict) -> str:
return format_prs_text(prs, base)

def _tool_get_pr_impact(arguments: dict) -> str:

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Health regression_tool_get_pr_impact()

fans out to 6 callees (efferent coupling).

Grounded coupling-delta finding (deterministic), not an LLM guess.

Comment thread graphify/serve.py
@@ -1990,17 +2174,33 @@ async def list_resources() -> list[types.Resource]:
]

async def read_resource(uri: AnyUrl) -> str:

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Health regressionread_resource()

fans out to 6 callees (efferent coupling).

Grounded coupling-delta finding (deterministic), not an LLM guess.

Comment thread graphify/serve.py Outdated
return server, _handlers


def serve(graph_path: str | None = None) -> None:

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Health regressionserve()

fans out to 6 callees (efferent coupling).

Grounded coupling-delta finding (deterministic), not an LLM guess.

Comment thread graphify/serve.py
@@ -2175,8 +2374,9 @@ async def __call__(self, scope, receive, send) -> None:


def _build_http_app(

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Health regression_build_http_app()

19 callers depend on it (afferent coupling).

Grounded coupling-delta finding (deterministic), not an LLM guess.

Comment thread tests/test_serve_http.py
assert "Nodes: 2" in _call_tool(client, headers, "graph_stats", {}, rid=3)


def test_corrupt_project_graph_is_a_tool_error_without_killing_server(tmp_path):

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Health regressiontest_corrupt_project_graph_is_a_tool_error_without_killing_server()

fans out to 7 callees (efferent coupling).

Grounded coupling-delta finding (deterministic), not an LLM guess.

@YanisGuerault
YanisGuerault force-pushed the feat/add-multi-graph-support branch from 3e02e0a to 70194e1 Compare August 25, 2026 15:02

@graphify-labs graphify-labs Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Graphify reviewed this change.

Worth a look — the grounded gate found no coupling regressions or blocking issues, but 5 advisory finding(s) below merit a look before merge.

Formal verification. 2 change(s) tested, no difference found (not proven).


Graphify review — findings

Adds a public graphify <repo>... --mcp CLI path that serves one or more existing repository graphs over MCP, parsed by _run_mcp_cli and dispatched to stdio via serve or HTTP via serve_http, rejecting duplicate repo basenames, missing graphify-out/graph.json, and malformed transport/host/port/api-key options. Introduces GraphRegistry and GraphContext to hold multiple named graphs, with from_paths skipping the learning overlay and project-path access, and exposes list_graphs/use_graph tools plus an optional per-tool graph parameter so callers can target a specific graph or set a session default. Retools the Dockerfile and adds docker-compose.multi.yml to run the public CLI against a mounted repo tree instead of baking a graph into the image.

Worth a look

  • HTTP transport binds to 0.0.0.0 without requiring API keygraphify/__main__.py:519 · Escalate · high
    • agreed by 2 of 2 members but NOT verified (no proof, no reproducing execution) — consensus is not a verdict; needs human review
  • CLI allows unauthenticated non-loopback HTTP MCP bindinggraphify/__main__.py:584 · Escalate · high
    • agreed by 2 of 2 members but NOT verified (no proof, no reproducing execution) — consensus is not a verdict; needs human review
  • GraphRegistry.get/names read shared dict without holding the lock while rescan mutates itgraphify/serve.py · Escalate · high
    • agreed by 2 of 2 members but NOT verified (no proof, no reproducing execution) — consensus is not a verdict; needs human review
  • Per-call project_path graph selection removed from tool dispatchgraphify/serve.py · Escalate · high
    • agreed by 2 of 2 members but NOT verified (no proof, no reproducing execution) — consensus is not a verdict; needs human review
  • MCP tool calls no longer honor project_path routinggraphify/serve.py:2080 · Escalate · high
    • agreed by 2 of 2 members but NOT verified (no proof, no reproducing execution) — consensus is not a verdict; needs human review
Analysis details — impact, health, verification

Impact & health

Graphify review

Impact — 839 functions depend on the 570 functions this change touches.

Health — this change adds coupling hotspots:

  • new: dispatch_command() — 2 callers, 122 callees
  • new: _build_server() — 14 callers, 17 callees
  • new: _query_graph_text() — 20 callers, 10 callees
  • new: _score_query() — 15 callers, 6 callees
  • new: _build_http_app() — 20 callers, 4 callees
  • new: render() — 13 callers, 5 callees
  • new: _get_ctx() — 9 callers, 7 callees
  • new: _query_terms() — 20 callers, 3 callees
  • …and 21 more — each is listed as a finding

Verification — 839 functions in the blast radius were not formally verified this run (proofs are advisory here).

Gate & verification

graphify gate

PASS — objectively clean (no health regressions, tests not run — proofs not run this pass (advisory)). Grounded, not self-assessed.

Advisory (not blocking):

  • verification_scope: 827 function(s) in the blast radius were not formally verified this run

Formal verification

No difference found (not proven): No behavior difference found in \_run\_cli (not a proof).

The verifier ran both versions of \_run\_cli on many inputs and saw identical behavior every time. Strong evidence the change is safe, but evidence, not a proof.

Guarantee: Empirical: differential testing (both versions run on many generated inputs). A divergence on an untested input remains possible, so this is 'no counterexample found', not 'proven equivalent'.

Note: An input the sampler did not try could still differ.

Could not verify: Could not verify \_\_init\_\_.

The verifier did not have enough to check \_\_init\_\_, so it is saying so rather than guessing. No false assurance is the whole point.

Guarantee: No guarantee either way, this is an honest abstention, not a pass.

Note: Reason: signature changed: no shared positional arity

Could not verify: Could not verify \_build\_http\_app.

The verifier did not have enough to check \_build\_http\_app, so it is saying so rather than guessing. No false assurance is the whole point.

Guarantee: No guarantee either way, this is an honest abstention, not a pass.

Note: Reason: not verifiable: all 24 sampled inputs raised on both versions — the function never executed, so 'no divergence' would be vacuous (mostly SystemExit — names the real obstacle, not a sampling gap)

Could not verify: Could not verify \_build\_server.

The verifier did not have enough to check \_build\_server, so it is saying so rather than guessing. No false assurance is the whole point.

Guarantee: No guarantee either way, this is an honest abstention, not a pass.

Note: Reason: not verifiable: all 54 sampled inputs raised on both versions — the function never executed, so 'no divergence' would be vacuous (mostly AttributeError — names the real obstacle, not a sampling gap)

Could not verify: Could not verify \_\_init\_\_.

The verifier did not have enough to check \_\_init\_\_, so it is saying so rather than guessing. No false assurance is the whole point.

Guarantee: No guarantee either way, this is an honest abstention, not a pass.

Note: Reason: signature changed: no shared positional arity

Could not verify: Could not verify \_\_init\_\_.

The verifier did not have enough to check \_\_init\_\_, so it is saying so rather than guessing. No false assurance is the whole point.

Guarantee: No guarantee either way, this is an honest abstention, not a pass.

Note: Reason: signature changed: no shared positional arity

Could not verify: Could not verify \_load\_graph.

The verifier did not have enough to check \_load\_graph, so it is saying so rather than guessing. No false assurance is the whole point.

Guarantee: No guarantee either way, this is an honest abstention, not a pass.

Note: Reason: not verifiable: all 23 sampled inputs raised on both versions — the function never executed, so 'no divergence' would be vacuous (mostly SystemExit — names the real obstacle, not a sampling gap)

Verification did not run: Verification did not run for \_main.

The verification could not execute (an environment/toolchain issue, not a statement about the code).

Guarantee: No guarantee, the check itself did not complete.

Note: Detail: harness produced no verdict (rc=1): error: Graph path must be a .json file, got: '/workspace/job-7af6d2a6766b4d54bd60987fc9155ca7/head/a' error: Graph path

Could not verify: Could not verify \_\_init\_\_.

The verifier did not have enough to check \_\_init\_\_, so it is saying so rather than guessing. No false assurance is the whole point.

Guarantee: No guarantee either way, this is an honest abstention, not a pass.

Note: Reason: signature changed: no shared positional arity

Verification did not run: Verification did not run for serve.

The verification could not execute (an environment/toolchain issue, not a statement about the code).

Guarantee: No guarantee, the check itself did not complete.

Note: Detail: harness produced no verdict (rc=1): error: Graph path must be a .json file, got: '/workspace/job-7af6d2a6766b4d54bd60987fc9155ca7/head/a longer string' erro

Verification did not run: Verification did not run for serve\_http.

The verification could not execute (an environment/toolchain issue, not a statement about the code).

Guarantee: No guarantee, the check itself did not complete.

Note: Detail: harness produced no verdict (rc=124): timeout after 30s

No difference found (not proven): No behavior difference found in monolith\_roundtrip (not a proof).

The verifier ran both versions of monolith\_roundtrip on many inputs and saw identical behavior every time. Strong evidence the change is safe, but evidence, not a proof.

Guarantee: Empirical: differential testing (both versions run on many generated inputs). A divergence on an untested input remains possible, so this is 'no counterexample found', not 'proven equivalent'.

Note: An input the sampler did not try could still differ.

· 11 grounded finding(s) anchored inline below; 18 more finding(s) on lines outside this diff (see the check run).

Comment thread graphify/__main__.py Outdated
serve(registry=registry)


def _serve_mcp_repositories(

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Health regression_serve_mcp_repositories()

6 callers depend on it (afferent coupling).

Grounded coupling-delta finding (deterministic), not an LLM guess.

Comment thread graphify/__main__.py
return True


def _run_cli() -> None:

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Health regression_run_cli()

high coupling complexity (Ca·Ce = 15).

Grounded coupling-delta finding (deterministic), not an LLM guess.

Comment thread graphify/serve.py Outdated
)


def _load_graph(graph_path: str, *, load_learning_overlay: bool = True) -> nx.Graph:

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Health regression_load_graph()

12 callers depend on it (afferent coupling).

Grounded coupling-delta finding (deterministic), not an LLM guess.

Comment thread graphify/serve.py Outdated


def _build_server(graph_path: str):
def _build_server(registry: GraphRegistry, *, session_state: dict | None = None):

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Health regression_build_server()

fans out to 17 callees (efferent coupling); 14 callers depend on it (afferent coupling).

Grounded coupling-delta finding (deterministic), not an LLM guess.

Comment thread graphify/serve.py
return fallback_session_state
return session_states.setdefault(session, {})

def _get_ctx(arguments: dict) -> GraphContext:

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Health regression_get_ctx()

fans out to 7 callees (efferent coupling); 9 callers depend on it (afferent coupling).

Grounded coupling-delta finding (deterministic), not an LLM guess.

Comment thread graphify/serve.py
@@ -1884,6 +2023,8 @@ def _tool_list_prs(arguments: dict) -> str:
return format_prs_text(prs, base)

def _tool_get_pr_impact(arguments: dict) -> str:

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Health regression_tool_get_pr_impact()

fans out to 6 callees (efferent coupling).

Grounded coupling-delta finding (deterministic), not an LLM guess.

Comment thread graphify/serve.py
@@ -1990,17 +2164,34 @@ async def list_resources() -> list[types.Resource]:
]

async def read_resource(uri: AnyUrl) -> str:

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Health regressionread_resource()

fans out to 8 callees (efferent coupling).

Grounded coupling-delta finding (deterministic), not an LLM guess.

Comment thread graphify/serve.py


def serve(graph_path: str | None = None) -> None:
def serve(graph_path: str | None = None, *, registry: GraphRegistry | None = None) -> None:

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Health regressionserve()

fans out to 6 callees (efferent coupling).

Grounded coupling-delta finding (deterministic), not an LLM guess.

Comment thread graphify/serve.py
@@ -2175,8 +2366,9 @@ async def __call__(self, scope, receive, send) -> None:


def _build_http_app(

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Health regression_build_http_app()

20 callers depend on it (afferent coupling).

Grounded coupling-delta finding (deterministic), not an LLM guess.

Comment thread tests/test_serve_http.py
assert "Nodes: 2" in _call_tool(client, headers, "graph_stats", {}, rid=3)


def test_corrupt_project_graph_is_a_tool_error_without_killing_server(tmp_path):

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Health regressiontest_corrupt_project_graph_is_a_tool_error_without_killing_server()

fans out to 7 callees (efferent coupling).

Grounded coupling-delta finding (deterministic), not an LLM guess.

@YanisGuerault
YanisGuerault force-pushed the feat/add-multi-graph-support branch from 9d2e6ac to 91c928e Compare August 26, 2026 12:37

@graphify-labs graphify-labs Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Graphify reviewed this change.

Worth a look — the grounded gate found no coupling regressions or blocking issues, but 5 advisory finding(s) below merit a look before merge.

Formal verification. 2 change(s) tested, no difference found (not proven).


Graphify review — findings

Adds a multi-graph MCP mode: graphify <repo>... --mcp serves the existing graphify-out/graph.json of one or more repositories over stdio or HTTP, exposing the usual tools plus list_graphs/use_graph and an optional per-tool graph selector with a session default. Introduces GraphRegistry/GraphContext to hold named graphs (basenames must be unique), lazily build trigram/community data, and rescan reloads only graphs whose graph.json mtime changed. HTTP binds still go through _validate_http_bind, so remote (--host 0.0.0.0) requires a nonblank API key, and the Docker image now defaults to graphify /data --mcp --transport http with the repo mounted read-only.

Worth a look

  • call_tool dropped per-call graph selection via project_pathgraphify/serve.py · Escalate · high
    • agreed by 2 of 2 members but NOT verified (no proof, no reproducing execution) — consensus is not a verdict; needs human review
  • Removal of per-request _select_graph makes multi-graph tool dispatch share mutable global stategraphify/serve.py · Escalate · high
    • agreed by 2 of 2 members but NOT verified (no proof, no reproducing execution) — consensus is not a verdict; needs human review
  • Duplicate repository graph names are silently overwrittengraphify/serve.py:56 · Escalate · high
    • agreed by 2 of 2 members but NOT verified (no proof, no reproducing execution) — consensus is not a verdict; needs human review
  • from_paths uses parent.parent.name causing name collisions/empty namesgraphify/serve.py · Escalate · medium
    • agreed by 2 of 2 members but NOT verified (no proof, no reproducing execution) — consensus is not a verdict; needs human review
  • rescan() except clause deletes graph on any exception, not just file errorsgraphify/serve.py · Escalate · medium
    • agreed by 2 of 2 members but NOT verified (no proof, no reproducing execution) — consensus is not a verdict; needs human review
Analysis details — impact, health, verification

Impact & health

Graphify review

Impact — 671 functions depend on the 466 functions this change touches.

Health — this change adds coupling hotspots:

  • new: dispatch_command() — 2 callers, 122 callees
  • new: _build_server() — 14 callers, 17 callees
  • new: _query_graph_text() — 20 callers, 10 callees
  • new: _score_query() — 15 callers, 6 callees
  • new: _build_http_app() — 20 callers, 4 callees
  • new: _get_ctx() — 9 callers, 7 callees
  • new: _query_terms() — 20 callers, 3 callees
  • new: _subgraph_to_text() — 20 callers, 3 callees
  • …and 17 more — each is listed as a finding

Verification — 671 functions in the blast radius were not formally verified this run (proofs are advisory here).

Gate & verification

graphify gate

PASS — objectively clean (no health regressions, tests not run — proofs not run this pass (advisory)). Grounded, not self-assessed.

Advisory (not blocking):

  • verification_scope: 659 function(s) in the blast radius were not formally verified this run

Formal verification

No difference found (not proven): No behavior difference found in \_run\_cli (not a proof).

The verifier ran both versions of \_run\_cli on many inputs and saw identical behavior every time. Strong evidence the change is safe, but evidence, not a proof.

Guarantee: Empirical: differential testing (both versions run on many generated inputs). A divergence on an untested input remains possible, so this is 'no counterexample found', not 'proven equivalent'.

Note: An input the sampler did not try could still differ.

Could not verify: Could not verify \_infer\_merge\_root.

The verifier did not have enough to check \_infer\_merge\_root, so it is saying so rather than guessing. No false assurance is the whole point.

Guarantee: No guarantee either way, this is an honest abstention, not a pass.

Note: Reason: parameter `graph_path` is annotated `Path` — outside the synthesizable primitive/collection set

Could not verify: Could not verify prefix\_graph\_for\_global.

The verifier did not have enough to check prefix\_graph\_for\_global, so it is saying so rather than guessing. No false assurance is the whole point.

Guarantee: No guarantee either way, this is an honest abstention, not a pass.

Note: Reason: not verifiable: all 115 sampled inputs raised on both versions — the function never executed, so 'no divergence' would be vacuous

Could not verify: Could not verify dispatch\_command.

The verifier did not have enough to check dispatch\_command, so it is saying so rather than guessing. No false assurance is the whole point.

Guarantee: No guarantee either way, this is an honest abstention, not a pass.

Note: Reason: not verifiable: all 23 sampled inputs raised on both versions — the function never executed, so 'no divergence' would be vacuous (mostly SystemExit — names the real obstacle, not a sampling gap)

Could not verify: Could not verify \_is\_ignored.

The verifier did not have enough to check \_is\_ignored, so it is saying so rather than guessing. No false assurance is the whole point.

Guarantee: No guarantee either way, this is an honest abstention, not a pass.

Note: Reason: parameter `path` is annotated `Path` — outside the synthesizable primitive/collection set

No difference found (not proven): No behavior difference found in \_match\_anchored\_ignore\_pattern (not a proof).

The verifier ran both versions of \_match\_anchored\_ignore\_pattern on many inputs and saw identical behavior every time. Strong evidence the change is safe, but evidence, not a proof.

Guarantee: Empirical: differential testing (both versions run on many generated inputs). A divergence on an untested input remains possible, so this is 'no counterexample found', not 'proven equivalent'.

Note: An input the sampler did not try could still differ.

Could not verify: Could not verify detect.

The verifier did not have enough to check detect, so it is saying so rather than guessing. No false assurance is the whole point.

Guarantee: No guarantee either way, this is an honest abstention, not a pass.

Note: Reason: parameter `root` is annotated `Path` — outside the synthesizable primitive/collection set

Could not verify: Could not verify \_csharp\_extra\_walk.

The verifier did not have enough to check \_csharp\_extra\_walk, so it is saying so rather than guessing. No false assurance is the whole point.

Guarantee: No guarantee either way, this is an honest abstention, not a pass.

Note: Reason: not verifiable: all 200 sampled inputs raised on both versions — the function never executed, so 'no divergence' would be vacuous (mostly AttributeError — names the real obstacle, not a sampling gap)

Could not verify: Could not verify \_extract\_generic.

The verifier did not have enough to check \_extract\_generic, so it is saying so rather than guessing. No false assurance is the whole point.

Guarantee: No guarantee either way, this is an honest abstention, not a pass.

Note: Reason: parameter `path` is annotated `Path` — outside the synthesizable primitive/collection set

Could not verify: Could not verify \_js\_extra\_walk.

The verifier did not have enough to check \_js\_extra\_walk, so it is saying so rather than guessing. No false assurance is the whole point.

Guarantee: No guarantee either way, this is an honest abstention, not a pass.

Note: Reason: not verifiable: all 200 sampled inputs raised on both versions — the function never executed, so 'no divergence' would be vacuous (mostly AttributeError — names the real obstacle, not a sampling gap)

Could not verify: Could not verify \_ts\_extra\_walk.

The verifier did not have enough to check \_ts\_extra\_walk, so it is saying so rather than guessing. No false assurance is the whole point.

Guarantee: No guarantee either way, this is an honest abstention, not a pass.

Note: Reason: not verifiable: all 200 sampled inputs raised on both versions — the function never executed, so 'no divergence' would be vacuous (mostly AttributeError — names the real obstacle, not a sampling gap)

Could not verify: Could not verify introspect\_postgres.

The verifier did not have enough to check introspect\_postgres, so it is saying so rather than guessing. No false assurance is the whole point.

Guarantee: No guarantee either way, this is an honest abstention, not a pass.

Note: Reason: not verifiable: all 24 sampled inputs raised on both versions — the function never executed, so 'no divergence' would be vacuous (mostly ProgrammingError — names the real obstacle, not a sampling gap)

Could not verify: Could not verify \_resolve\_source\_path.

The verifier did not have enough to check \_resolve\_source\_path, so it is saying so rather than guessing. No false assurance is the whole point.

Guarantee: No guarantee either way, this is an honest abstention, not a pass.

Note: Reason: parameter `graph_path` is annotated `Path` — outside the synthesizable primitive/collection set

Could not verify: Could not verify resolve\_ruby\_member\_calls.

The verifier did not have enough to check resolve\_ruby\_member\_calls, so it is saying so rather than guessing. No false assurance is the whole point.

Guarantee: No guarantee either way, this is an honest abstention, not a pass.

Note: Reason: non-vacuity: domain too small (only 1 distinct inputs exercised, need 3) — 'no divergence' would be near-vacuous

Could not verify: Could not verify \_\_init\_\_.

The verifier did not have enough to check \_\_init\_\_, so it is saying so rather than guessing. No false assurance is the whole point.

Guarantee: No guarantee either way, this is an honest abstention, not a pass.

Note: Reason: signature changed: no shared positional arity

Could not verify: Could not verify \_build\_http\_app.

The verifier did not have enough to check \_build\_http\_app, so it is saying so rather than guessing. No false assurance is the whole point.

Guarantee: No guarantee either way, this is an honest abstention, not a pass.

Note: Reason: not verifiable: all 24 sampled inputs raised on both versions — the function never executed, so 'no divergence' would be vacuous (mostly SystemExit — names the real obstacle, not a sampling gap)

Could not verify: Could not verify \_build\_server.

The verifier did not have enough to check \_build\_server, so it is saying so rather than guessing. No false assurance is the whole point.

Guarantee: No guarantee either way, this is an honest abstention, not a pass.

Note: Reason: not verifiable: all 54 sampled inputs raised on both versions — the function never executed, so 'no divergence' would be vacuous (mostly AttributeError — names the real obstacle, not a sampling gap)

Could not verify: Could not verify \_load\_graph.

The verifier did not have enough to check \_load\_graph, so it is saying so rather than guessing. No false assurance is the whole point.

Guarantee: No guarantee either way, this is an honest abstention, not a pass.

Note: Reason: not verifiable: all 23 sampled inputs raised on both versions — the function never executed, so 'no divergence' would be vacuous (mostly SystemExit — names the real obstacle, not a sampling gap)

Verification did not run: Verification did not run for \_main.

The verification could not execute (an environment/toolchain issue, not a statement about the code).

Guarantee: No guarantee, the check itself did not complete.

Note: Detail: harness produced no verdict (rc=1): error: Graph path must be a .json file, got: '/workspace/job-590480ab84794e8caddd656cbf949421/head/a' error: Graph path

Verification did not run: Verification did not run for serve.

The verification could not execute (an environment/toolchain issue, not a statement about the code).

Guarantee: No guarantee, the check itself did not complete.

Note: Detail: harness produced no verdict (rc=1): error: Graph path must be a .json file, got: '/workspace/job-590480ab84794e8caddd656cbf949421/head/a longer string' erro

Verification did not run: Verification did not run for serve\_http.

The verification could not execute (an environment/toolchain issue, not a statement about the code).

Guarantee: No guarantee, the check itself did not complete.

Note: Detail: harness produced no verdict (rc=124): timeout after 30s

Could not verify: Could not verify \_\_init\_\_.

The verifier did not have enough to check \_\_init\_\_, so it is saying so rather than guessing. No false assurance is the whole point.

Guarantee: No guarantee either way, this is an honest abstention, not a pass.

Note: Reason: method — first parameter is `self`, which needs a constructed instance (not synthesizable)

Could not verify: Could not verify on\_any\_event.

The verifier did not have enough to check on\_any\_event, so it is saying so rather than guessing. No false assurance is the whole point.

Guarantee: No guarantee either way, this is an honest abstention, not a pass.

Note: Reason: method — first parameter is `self`, which needs a constructed instance (not synthesizable)

Could not verify: Could not verify watch.

The verifier did not have enough to check watch, so it is saying so rather than guessing. No false assurance is the whole point.

Guarantee: No guarantee either way, this is an honest abstention, not a pass.

Note: Reason: parameter `watch_path` is annotated `Path` — outside the synthesizable primitive/collection set

· 12 grounded finding(s) anchored inline below; 13 more finding(s) on lines outside this diff (see the check run).

Comment thread graphify/__main__.py Outdated
serve(registry=registry)


def _serve_mcp_repositories(

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Health regression_serve_mcp_repositories()

6 callers depend on it (afferent coupling).

Grounded coupling-delta finding (deterministic), not an LLM guess.

Comment thread graphify/__main__.py
return True


def _run_cli() -> None:

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Health regression_run_cli()

high coupling complexity (Ca·Ce = 15).

Grounded coupling-delta finding (deterministic), not an LLM guess.

Comment thread graphify/serve.py Outdated
)


def _load_graph(graph_path: str, *, load_learning_overlay: bool = True) -> nx.Graph:

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Health regression_load_graph()

12 callers depend on it (afferent coupling).

Grounded coupling-delta finding (deterministic), not an LLM guess.

Comment thread graphify/serve.py Outdated


def _build_server(graph_path: str):
def _build_server(registry: GraphRegistry, *, session_state: dict | None = None):

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Health regression_build_server()

fans out to 17 callees (efferent coupling); 14 callers depend on it (afferent coupling).

Grounded coupling-delta finding (deterministic), not an LLM guess.

Comment thread graphify/serve.py
return fallback_session_state
return session_states.setdefault(session, {})

def _get_ctx(arguments: dict) -> GraphContext:

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Health regression_get_ctx()

fans out to 7 callees (efferent coupling); 9 callers depend on it (afferent coupling).

Grounded coupling-delta finding (deterministic), not an LLM guess.

Comment thread graphify/serve.py
@@ -1990,17 +2166,34 @@ async def list_resources() -> list[types.Resource]:
]

async def read_resource(uri: AnyUrl) -> str:

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Health regressionread_resource()

fans out to 8 callees (efferent coupling).

Grounded coupling-delta finding (deterministic), not an LLM guess.

Comment thread graphify/serve.py


def serve(graph_path: str | None = None) -> None:
def serve(graph_path: str | None = None, *, registry: GraphRegistry | None = None) -> None:

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Health regressionserve()

fans out to 6 callees (efferent coupling).

Grounded coupling-delta finding (deterministic), not an LLM guess.

Comment thread graphify/serve.py
@@ -2175,8 +2368,9 @@ async def __call__(self, scope, receive, send) -> None:


def _build_http_app(

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Health regression_build_http_app()

20 callers depend on it (afferent coupling).

Grounded coupling-delta finding (deterministic), not an LLM guess.

Comment thread graphify/serve.py
raise ValueError("HTTP binding outside loopback requires --api-key")


def serve_http(

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Health regressionserve_http()

high coupling complexity (Ca·Ce = 12).

Grounded coupling-delta finding (deterministic), not an LLM guess.

Comment thread tests/test_serve_http.py
assert "Nodes: 2" in _call_tool(client, headers, "graph_stats", {}, rid=3)


def test_corrupt_project_graph_is_a_tool_error_without_killing_server(tmp_path):

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Health regressiontest_corrupt_project_graph_is_a_tool_error_without_killing_server()

fans out to 7 callees (efferent coupling).

Grounded coupling-delta finding (deterministic), not an LLM guess.

@YanisGuerault
YanisGuerault force-pushed the feat/add-multi-graph-support branch from 91c928e to 2906f3f Compare August 26, 2026 12:52

@graphify-labs graphify-labs Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Graphify reviewed this change.

Worth a look — the grounded gate found no coupling regressions or blocking issues, but 5 advisory finding(s) below merit a look before merge.

Formal verification. 1 change(s) tested, no difference found (not proven).


Graphify review — findings

Adds a multi-graph MCP mode: graphify <repo>... --mcp resolves each repo's graphify-out/graph.json, builds a GraphRegistry, and serves them over stdio (default) or HTTP with --transport/--host/--port/--api-key, rejecting duplicate repo basenames and enforcing the HTTP bind/API-key validation. Each MCP tool gains an optional graph parameter to target a specific graph, backed by new list_graphs and use_graph tools and a session default, while registry-loaded graphs skip project-path resolution and the learning overlay and refresh on mtime change via rescan. Repoints the Dockerfile to the graphify entrypoint serving a mounted /data repo over HTTP and documents the multi-graph server in the README.

Worth a look

  • from_paths overwrites graphs sharing the same grandparent directory namegraphify/serve.py · Escalate · high
    • agreed by 2 of 2 members but NOT verified (no proof, no reproducing execution) — consensus is not a verdict; needs human review
  • use_graph tool omits 'graph' from its input schema but requires itgraphify/serve.py · Escalate · high
    • agreed by 2 of 2 members but NOT verified (no proof, no reproducing execution) — consensus is not a verdict; needs human review
  • Multi-graph MCP server drops PR tool handlersgraphify/serve.py:2134 · Escalate · high
    • agreed by 2 of 2 members but NOT verified (no proof, no reproducing execution) — consensus is not a verdict; needs human review
  • Docker default command fails unauthenticated HTTP bind validationDockerfile:15 · Escalate · medium
    • agreed by 2 of 2 members but NOT verified (no proof, no reproducing execution) — consensus is not a verdict; needs human review
  • Invalid graph parse errors are not converted to CLI exitsgraphify/__main__.py:538 · Escalate · medium
    • agreed by 2 of 2 members but NOT verified (no proof, no reproducing execution) — consensus is not a verdict; needs human review
Analysis details — impact, health, verification

Impact & health

Graphify review

Impact — 671 functions depend on the 466 functions this change touches.

Health — this change adds coupling hotspots:

  • new: dispatch_command() — 2 callers, 122 callees
  • new: _build_server() — 14 callers, 17 callees
  • new: _query_graph_text() — 20 callers, 10 callees
  • new: _score_query() — 15 callers, 6 callees
  • new: _build_http_app() — 20 callers, 4 callees
  • new: _get_ctx() — 9 callers, 7 callees
  • new: _query_terms() — 20 callers, 3 callees
  • new: _subgraph_to_text() — 20 callers, 3 callees
  • …and 17 more — each is listed as a finding

Verification — 671 functions in the blast radius were not formally verified this run (proofs are advisory here).

Gate & verification

graphify gate

PASS — objectively clean (no health regressions, tests not run — proofs not run this pass (advisory)). Grounded, not self-assessed.

Advisory (not blocking):

  • verification_scope: 659 function(s) in the blast radius were not formally verified this run

Formal verification

No difference found (not proven): No behavior difference found in \_run\_cli (not a proof).

The verifier ran both versions of \_run\_cli on many inputs and saw identical behavior every time. Strong evidence the change is safe, but evidence, not a proof.

Guarantee: Empirical: differential testing (both versions run on many generated inputs). A divergence on an untested input remains possible, so this is 'no counterexample found', not 'proven equivalent'.

Note: An input the sampler did not try could still differ.

Could not verify: Could not verify \_\_init\_\_.

The verifier did not have enough to check \_\_init\_\_, so it is saying so rather than guessing. No false assurance is the whole point.

Guarantee: No guarantee either way, this is an honest abstention, not a pass.

Note: Reason: signature changed: no shared positional arity

Could not verify: Could not verify \_build\_http\_app.

The verifier did not have enough to check \_build\_http\_app, so it is saying so rather than guessing. No false assurance is the whole point.

Guarantee: No guarantee either way, this is an honest abstention, not a pass.

Note: Reason: not verifiable: all 24 sampled inputs raised on both versions — the function never executed, so 'no divergence' would be vacuous (mostly SystemExit — names the real obstacle, not a sampling gap)

Could not verify: Could not verify \_build\_server.

The verifier did not have enough to check \_build\_server, so it is saying so rather than guessing. No false assurance is the whole point.

Guarantee: No guarantee either way, this is an honest abstention, not a pass.

Note: Reason: not verifiable: all 54 sampled inputs raised on both versions — the function never executed, so 'no divergence' would be vacuous (mostly AttributeError — names the real obstacle, not a sampling gap)

Could not verify: Could not verify \_\_init\_\_.

The verifier did not have enough to check \_\_init\_\_, so it is saying so rather than guessing. No false assurance is the whole point.

Guarantee: No guarantee either way, this is an honest abstention, not a pass.

Note: Reason: signature changed: no shared positional arity

Could not verify: Could not verify \_\_init\_\_.

The verifier did not have enough to check \_\_init\_\_, so it is saying so rather than guessing. No false assurance is the whole point.

Guarantee: No guarantee either way, this is an honest abstention, not a pass.

Note: Reason: signature changed: no shared positional arity

Could not verify: Could not verify \_load\_graph.

The verifier did not have enough to check \_load\_graph, so it is saying so rather than guessing. No false assurance is the whole point.

Guarantee: No guarantee either way, this is an honest abstention, not a pass.

Note: Reason: not verifiable: all 23 sampled inputs raised on both versions — the function never executed, so 'no divergence' would be vacuous (mostly SystemExit — names the real obstacle, not a sampling gap)

Verification did not run: Verification did not run for \_main.

The verification could not execute (an environment/toolchain issue, not a statement about the code).

Guarantee: No guarantee, the check itself did not complete.

Note: Detail: harness produced no verdict (rc=1): error: Graph path must be a .json file, got: '/workspace/job-6d70e3f451d34b69b47183ef1d4c0ad5/head/a' error: Graph path

Could not verify: Could not verify \_\_init\_\_.

The verifier did not have enough to check \_\_init\_\_, so it is saying so rather than guessing. No false assurance is the whole point.

Guarantee: No guarantee either way, this is an honest abstention, not a pass.

Note: Reason: signature changed: no shared positional arity

Verification did not run: Verification did not run for serve.

The verification could not execute (an environment/toolchain issue, not a statement about the code).

Guarantee: No guarantee, the check itself did not complete.

Note: Detail: harness produced no verdict (rc=1): error: Graph path must be a .json file, got: '/workspace/job-6d70e3f451d34b69b47183ef1d4c0ad5/head/a longer string' erro

Verification did not run: Verification did not run for serve\_http.

The verification could not execute (an environment/toolchain issue, not a statement about the code).

Guarantee: No guarantee, the check itself did not complete.

Note: Detail: harness produced no verdict (rc=124): timeout after 30s

· 12 grounded finding(s) anchored inline below; 13 more finding(s) on lines outside this diff (see the check run).

Comment thread graphify/__main__.py Outdated
serve(registry=registry)


def _serve_mcp_repositories(

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Health regression_serve_mcp_repositories()

6 callers depend on it (afferent coupling).

Grounded coupling-delta finding (deterministic), not an LLM guess.

Comment thread graphify/__main__.py
return True


def _run_cli() -> None:

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Health regression_run_cli()

high coupling complexity (Ca·Ce = 15).

Grounded coupling-delta finding (deterministic), not an LLM guess.

Comment thread graphify/serve.py Outdated
)


def _load_graph(graph_path: str, *, load_learning_overlay: bool = True) -> nx.Graph:

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Health regression_load_graph()

12 callers depend on it (afferent coupling).

Grounded coupling-delta finding (deterministic), not an LLM guess.

Comment thread graphify/serve.py Outdated


def _build_server(graph_path: str):
def _build_server(registry: GraphRegistry, *, session_state: dict | None = None):

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Health regression_build_server()

fans out to 17 callees (efferent coupling); 14 callers depend on it (afferent coupling).

Grounded coupling-delta finding (deterministic), not an LLM guess.

Comment thread graphify/serve.py
return fallback_session_state
return session_states.setdefault(session, {})

def _get_ctx(arguments: dict) -> GraphContext:

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Health regression_get_ctx()

fans out to 7 callees (efferent coupling); 9 callers depend on it (afferent coupling).

Grounded coupling-delta finding (deterministic), not an LLM guess.

Comment thread graphify/serve.py
@@ -1990,17 +2166,34 @@ async def list_resources() -> list[types.Resource]:
]

async def read_resource(uri: AnyUrl) -> str:

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Health regressionread_resource()

fans out to 8 callees (efferent coupling).

Grounded coupling-delta finding (deterministic), not an LLM guess.

Comment thread graphify/serve.py


def serve(graph_path: str | None = None) -> None:
def serve(graph_path: str | None = None, *, registry: GraphRegistry | None = None) -> None:

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Health regressionserve()

fans out to 6 callees (efferent coupling).

Grounded coupling-delta finding (deterministic), not an LLM guess.

Comment thread graphify/serve.py
@@ -2175,8 +2368,9 @@ async def __call__(self, scope, receive, send) -> None:


def _build_http_app(

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Health regression_build_http_app()

20 callers depend on it (afferent coupling).

Grounded coupling-delta finding (deterministic), not an LLM guess.

Comment thread graphify/serve.py
raise ValueError("HTTP binding outside loopback requires --api-key")


def serve_http(

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Health regressionserve_http()

high coupling complexity (Ca·Ce = 12).

Grounded coupling-delta finding (deterministic), not an LLM guess.

Comment thread tests/test_serve_http.py
assert "Nodes: 2" in _call_tool(client, headers, "graph_stats", {}, rid=3)


def test_corrupt_project_graph_is_a_tool_error_without_killing_server(tmp_path):

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Health regressiontest_corrupt_project_graph_is_a_tool_error_without_killing_server()

fans out to 7 callees (efferent coupling).

Grounded coupling-delta finding (deterministic), not an LLM guess.

@graphify-labs graphify-labs Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Graphify reviewed this change.

Worth a look — the grounded gate found no coupling regressions or blocking issues, but 5 advisory finding(s) below merit a look before merge.

Formal verification. 1 change(s) tested, no difference found (not proven).


Graphify review — findings

Adds a multi-graph MCP server mode: graphify <repo>... --mcp serves one or more repositories' existing graphify-out/graph.json files over stdio or HTTP, exposing the usual tools plus list_graphs/use_graph and an optional per-call graph parameter, with a session-scoped default graph. Introduces GraphRegistry and GraphContext to hold each named graph, its communities, and mtime, with rescan reloading only changed graphs before requests and rejecting duplicate repository basenames. Enforces a nonblank API key for remote HTTP binds via _validate_http_bind, and repoints the Dockerfile to graphify /data --mcp mounting the repo read-only instead of running graphify.serve on a baked-in graph path.

Worth a look

  • Single default graph is named after .graphify directorygraphify/serve.py:52 · Escalate · medium
    • agreed by 2 of 2 members but NOT verified (no proof, no reproducing execution) — consensus is not a verdict; needs human review
  • Duplicate multi-graph repository names are silently overwrittengraphify/serve.py:74 · Escalate · medium
    • agreed by 2 of 2 members but NOT verified (no proof, no reproducing execution) — consensus is not a verdict; needs human review
  • except clause with (OSError, SystemExit, Exception) swallows SystemExitgraphify/serve.py:87 · Escalate · medium
    • agreed by 2 of 2 members but NOT verified (no proof, no reproducing execution) — consensus is not a verdict; needs human review
  • Transient reload failure permanently unregisters graphgraphify/serve.py:96 · Escalate · medium
    • agreed by 2 of 2 members but NOT verified (no proof, no reproducing execution) — consensus is not a verdict; needs human review
  • Failed hot reload permanently removes graphgraphify/serve.py:99 · Escalate · medium
    • agreed by 2 of 2 members but NOT verified (no proof, no reproducing execution) — consensus is not a verdict; needs human review
Analysis details — impact, health, verification

Impact & health

Graphify review

Impact — 672 functions depend on the 467 functions this change touches.

Health — this change adds coupling hotspots:

  • new: _build_server() — 15 callers, 17 callees
  • new: dispatch_command() — 2 callers, 122 callees
  • new: _query_graph_text() — 20 callers, 10 callees
  • new: _score_query() — 15 callers, 6 callees
  • new: _build_http_app() — 20 callers, 4 callees
  • new: _get_ctx() — 9 callers, 7 callees
  • new: _query_terms() — 20 callers, 3 callees
  • new: _subgraph_to_text() — 20 callers, 3 callees
  • …and 17 more — each is listed as a finding

Verification — 672 functions in the blast radius were not formally verified this run (proofs are advisory here).

Gate & verification

graphify gate

PASS — objectively clean (no health regressions, tests not run — proofs not run this pass (advisory)). Grounded, not self-assessed.

Advisory (not blocking):

  • verification_scope: 660 function(s) in the blast radius were not formally verified this run

Formal verification

No difference found (not proven): No behavior difference found in \_run\_cli (not a proof).

The verifier ran both versions of \_run\_cli on many inputs and saw identical behavior every time. Strong evidence the change is safe, but evidence, not a proof.

Guarantee: Empirical: differential testing (both versions run on many generated inputs). A divergence on an untested input remains possible, so this is 'no counterexample found', not 'proven equivalent'.

Note: An input the sampler did not try could still differ.

Could not verify: Could not verify \_\_init\_\_.

The verifier did not have enough to check \_\_init\_\_, so it is saying so rather than guessing. No false assurance is the whole point.

Guarantee: No guarantee either way, this is an honest abstention, not a pass.

Note: Reason: signature changed: no shared positional arity

Could not verify: Could not verify \_build\_http\_app.

The verifier did not have enough to check \_build\_http\_app, so it is saying so rather than guessing. No false assurance is the whole point.

Guarantee: No guarantee either way, this is an honest abstention, not a pass.

Note: Reason: not verifiable: all 24 sampled inputs raised on both versions — the function never executed, so 'no divergence' would be vacuous (mostly SystemExit — names the real obstacle, not a sampling gap)

Could not verify: Could not verify \_build\_server.

The verifier did not have enough to check \_build\_server, so it is saying so rather than guessing. No false assurance is the whole point.

Guarantee: No guarantee either way, this is an honest abstention, not a pass.

Note: Reason: parameter `graph_path_or_registry` is annotated `str | GraphRegistry` — outside the synthesizable primitive/collection set

Could not verify: Could not verify \_\_init\_\_.

The verifier did not have enough to check \_\_init\_\_, so it is saying so rather than guessing. No false assurance is the whole point.

Guarantee: No guarantee either way, this is an honest abstention, not a pass.

Note: Reason: signature changed: no shared positional arity

Could not verify: Could not verify \_\_init\_\_.

The verifier did not have enough to check \_\_init\_\_, so it is saying so rather than guessing. No false assurance is the whole point.

Guarantee: No guarantee either way, this is an honest abstention, not a pass.

Note: Reason: signature changed: no shared positional arity

Could not verify: Could not verify \_load\_graph.

The verifier did not have enough to check \_load\_graph, so it is saying so rather than guessing. No false assurance is the whole point.

Guarantee: No guarantee either way, this is an honest abstention, not a pass.

Note: Reason: not verifiable: all 23 sampled inputs raised on both versions — the function never executed, so 'no divergence' would be vacuous (mostly SystemExit — names the real obstacle, not a sampling gap)

Verification did not run: Verification did not run for \_main.

The verification could not execute (an environment/toolchain issue, not a statement about the code).

Guarantee: No guarantee, the check itself did not complete.

Note: Detail: harness produced no verdict (rc=1): error: Graph path must be a .json file, got: '/workspace/job-08723558d04f4c899ffe1ebd611f275a/head/a' error: Graph path

Could not verify: Could not verify \_\_init\_\_.

The verifier did not have enough to check \_\_init\_\_, so it is saying so rather than guessing. No false assurance is the whole point.

Guarantee: No guarantee either way, this is an honest abstention, not a pass.

Note: Reason: signature changed: no shared positional arity

Verification did not run: Verification did not run for serve.

The verification could not execute (an environment/toolchain issue, not a statement about the code).

Guarantee: No guarantee, the check itself did not complete.

Note: Detail: harness produced no verdict (rc=1): error: Graph path must be a .json file, got: '/workspace/job-08723558d04f4c899ffe1ebd611f275a/head/a longer string' erro

Verification did not run: Verification did not run for serve\_http.

The verification could not execute (an environment/toolchain issue, not a statement about the code).

Guarantee: No guarantee, the check itself did not complete.

Note: Detail: harness produced no verdict (rc=124): timeout after 30s

· 12 grounded finding(s) anchored inline below; 13 more finding(s) on lines outside this diff (see the check run).

Comment thread graphify/__main__.py Outdated
serve(registry=registry)


def _serve_mcp_repositories(

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Health regression_serve_mcp_repositories()

6 callers depend on it (afferent coupling).

Grounded coupling-delta finding (deterministic), not an LLM guess.

Comment thread graphify/__main__.py
return True


def _run_cli() -> None:

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Health regression_run_cli()

high coupling complexity (Ca·Ce = 15).

Grounded coupling-delta finding (deterministic), not an LLM guess.

Comment thread graphify/serve.py Outdated
)


def _load_graph(graph_path: str, *, load_learning_overlay: bool = True) -> nx.Graph:

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Health regression_load_graph()

12 callers depend on it (afferent coupling).

Grounded coupling-delta finding (deterministic), not an LLM guess.

Comment thread graphify/serve.py


def _build_server(graph_path: str):
def _build_server(

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Health regression_build_server()

fans out to 17 callees (efferent coupling); 15 callers depend on it (afferent coupling).

Grounded coupling-delta finding (deterministic), not an LLM guess.

Comment thread graphify/serve.py
return fallback_session_state
return session_states.setdefault(session, {})

def _get_ctx(arguments: dict) -> GraphContext:

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Health regression_get_ctx()

fans out to 7 callees (efferent coupling); 9 callers depend on it (afferent coupling).

Grounded coupling-delta finding (deterministic), not an LLM guess.

Comment thread graphify/serve.py
@@ -1990,17 +2175,34 @@ async def list_resources() -> list[types.Resource]:
]

async def read_resource(uri: AnyUrl) -> str:

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Health regressionread_resource()

fans out to 8 callees (efferent coupling).

Grounded coupling-delta finding (deterministic), not an LLM guess.

Comment thread graphify/serve.py


def serve(graph_path: str | None = None) -> None:
def serve(graph_path: str | None = None, *, registry: GraphRegistry | None = None) -> None:

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Health regressionserve()

fans out to 6 callees (efferent coupling).

Grounded coupling-delta finding (deterministic), not an LLM guess.

Comment thread graphify/serve.py
@@ -2175,8 +2377,9 @@ async def __call__(self, scope, receive, send) -> None:


def _build_http_app(

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Health regression_build_http_app()

20 callers depend on it (afferent coupling).

Grounded coupling-delta finding (deterministic), not an LLM guess.

Comment thread graphify/serve.py
raise ValueError("HTTP binding outside loopback requires --api-key")


def serve_http(

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Health regressionserve_http()

high coupling complexity (Ca·Ce = 12).

Grounded coupling-delta finding (deterministic), not an LLM guess.

Comment thread tests/test_serve_http.py
assert "Nodes: 2" in _call_tool(client, headers, "graph_stats", {}, rid=3)


def test_corrupt_project_graph_is_a_tool_error_without_killing_server(tmp_path):

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Health regressiontest_corrupt_project_graph_is_a_tool_error_without_killing_server()

fans out to 7 callees (efferent coupling).

Grounded coupling-delta finding (deterministic), not an LLM guess.

@YanisGuerault
YanisGuerault force-pushed the feat/add-multi-graph-support branch from fe29dd7 to ac3b845 Compare September 1, 2026 08:15

@graphify-labs graphify-labs Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Graphify reviewed this change.

Worth a look — the grounded gate found no coupling regressions or blocking issues, but 5 advisory finding(s) below merit a look before merge.

Formal verification. 2 change(s) tested, no difference found (not proven).


Graphify review — findings

Adds a multi-graph MCP server mode driven by graphify <repo>... --mcp, resolving each repo's graphify-out/graph.json, rejecting duplicate basenames and missing graphs, and serving over stdio or HTTP (with HTTP bind validation requiring a nonblank API key for remote hosts) via a new GraphRegistry that loads and caches each graph without the learning overlay. Exposes list_graphs and use_graph tools and threads an optional graph parameter through every existing tool so callers can target a specific graph or set a session default. Rewrites the Dockerfile to use the graphify entrypoint mounting a repo at /data instead of baking a graph.json path, and documents the multi-graph setup in the README.

Worth a look

  • GraphRegistry.rescan mutates dict while iterating and deletes on any Exceptiongraphify/serve.py · Escalate · high
    • agreed by 2 of 2 members but NOT verified (no proof, no reproducing execution) — consensus is not a verdict; needs human review
  • Session state 'current_graph' shared across concurrent MCP clients without isolationgraphify/serve.py · Escalate · high
    • agreed by 2 of 2 members but NOT verified (no proof, no reproducing execution) — consensus is not a verdict; needs human review
  • Docker default command violates HTTP auth contractDockerfile:15 · Escalate · medium
    • agreed by 2 of 2 members but NOT verified (no proof, no reproducing execution) — consensus is not a verdict; needs human review
  • from_paths uses grandparent dir name for graph key while from_path uses parent, risking key collisions/mismatchgraphify/serve.py · Escalate · medium
    • agreed by 2 of 2 members but NOT verified (no proof, no reproducing execution) — consensus is not a verdict; needs human review
  • rescan() exception handler catches SystemExit and swallows all errors, deleting graph on transient stat failuregraphify/serve.py · Escalate · medium
    • agreed by 2 of 2 members but NOT verified (no proof, no reproducing execution) — consensus is not a verdict; needs human review
Analysis details — impact, health, verification

Impact & health

Graphify review

Impact — 704 functions depend on the 482 functions this change touches.

Health — this change adds coupling hotspots:

  • new: _build_server() — 15 callers, 17 callees
  • new: dispatch_command() — 2 callers, 123 callees
  • new: _query_graph_text() — 20 callers, 10 callees
  • new: _build_http_app() — 23 callers, 4 callees
  • new: _score_query() — 15 callers, 6 callees
  • new: _get_ctx() — 9 callers, 7 callees
  • new: _query_terms() — 20 callers, 3 callees
  • new: _subgraph_to_text() — 20 callers, 3 callees
  • …and 18 more — each is listed as a finding

Verification — 704 functions in the blast radius were not formally verified this run (proofs are advisory here).

Gate & verification

graphify gate

PASS — objectively clean (no health regressions, tests not run — proofs not run this pass (advisory)). Grounded, not self-assessed.

Advisory (not blocking):

  • verification_scope: 692 function(s) in the blast radius were not formally verified this run

Formal verification

No difference found (not proven): No behavior difference found in \_run\_cli (not a proof).

The verifier ran both versions of \_run\_cli on many inputs and saw identical behavior every time. Strong evidence the change is safe, but evidence, not a proof.

Guarantee: Empirical: differential testing (both versions run on many generated inputs). A divergence on an untested input remains possible, so this is 'no counterexample found', not 'proven equivalent'.

Note: An input the sampler did not try could still differ.

Could not verify: Could not verify \_\_init\_\_.

The verifier did not have enough to check \_\_init\_\_, so it is saying so rather than guessing. No false assurance is the whole point.

Guarantee: No guarantee either way, this is an honest abstention, not a pass.

Note: Reason: signature changed: no shared positional arity

Could not verify: Could not verify \_build\_http\_app.

The verifier did not have enough to check \_build\_http\_app, so it is saying so rather than guessing. No false assurance is the whole point.

Guarantee: No guarantee either way, this is an honest abstention, not a pass.

Note: Reason: not verifiable: all 24 sampled inputs raised on both versions — the function never executed, so 'no divergence' would be vacuous (mostly SystemExit — names the real obstacle, not a sampling gap)

Could not verify: Could not verify \_build\_server.

The verifier did not have enough to check \_build\_server, so it is saying so rather than guessing. No false assurance is the whole point.

Guarantee: No guarantee either way, this is an honest abstention, not a pass.

Note: Reason: parameter `graph_path_or_registry` is annotated `str | GraphRegistry` — outside the synthesizable primitive/collection set

Could not verify: Could not verify \_\_init\_\_.

The verifier did not have enough to check \_\_init\_\_, so it is saying so rather than guessing. No false assurance is the whole point.

Guarantee: No guarantee either way, this is an honest abstention, not a pass.

Note: Reason: signature changed: no shared positional arity

Could not verify: Could not verify \_\_init\_\_.

The verifier did not have enough to check \_\_init\_\_, so it is saying so rather than guessing. No false assurance is the whole point.

Guarantee: No guarantee either way, this is an honest abstention, not a pass.

Note: Reason: signature changed: no shared positional arity

Could not verify: Could not verify \_load\_graph.

The verifier did not have enough to check \_load\_graph, so it is saying so rather than guessing. No false assurance is the whole point.

Guarantee: No guarantee either way, this is an honest abstention, not a pass.

Note: Reason: not verifiable: all 23 sampled inputs raised on both versions — the function never executed, so 'no divergence' would be vacuous (mostly SystemExit — names the real obstacle, not a sampling gap)

Verification did not run: Verification did not run for \_main.

The verification could not execute (an environment/toolchain issue, not a statement about the code).

Guarantee: No guarantee, the check itself did not complete.

Note: Detail: harness produced no verdict (rc=1): error: Graph path must be a .json file, got: '/workspace/job-bf00da44b5554b70b72e3885ddb42fad/head/a' error: Graph path

Could not verify: Could not verify \_\_init\_\_.

The verifier did not have enough to check \_\_init\_\_, so it is saying so rather than guessing. No false assurance is the whole point.

Guarantee: No guarantee either way, this is an honest abstention, not a pass.

Note: Reason: signature changed: no shared positional arity

No difference found (not proven): No behavior difference found in \_resolve\_single\_node (not a proof).

The verifier ran both versions of \_resolve\_single\_node on many inputs and saw identical behavior every time. Strong evidence the change is safe, but evidence, not a proof.

Guarantee: Empirical: differential testing (both versions run on many generated inputs). A divergence on an untested input remains possible, so this is 'no counterexample found', not 'proven equivalent'.

Note: An input the sampler did not try could still differ.

Verification did not run: Verification did not run for serve.

The verification could not execute (an environment/toolchain issue, not a statement about the code).

Guarantee: No guarantee, the check itself did not complete.

Note: Detail: harness produced no verdict (rc=1): error: Graph path must be a .json file, got: '/workspace/job-bf00da44b5554b70b72e3885ddb42fad/head/a longer string' erro

Verification did not run: Verification did not run for serve\_http.

The verification could not execute (an environment/toolchain issue, not a statement about the code).

Guarantee: No guarantee, the check itself did not complete.

Note: Detail: harness produced no verdict (rc=124): timeout after 30s

· 13 grounded finding(s) anchored inline below; 13 more finding(s) on lines outside this diff (see the check run).

Comment thread graphify/__main__.py Outdated
serve(registry=registry)


def _serve_mcp_repositories(

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Health regression_serve_mcp_repositories()

6 callers depend on it (afferent coupling).

Grounded coupling-delta finding (deterministic), not an LLM guess.

Comment thread graphify/__main__.py
return True


def _run_cli() -> None:

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Health regression_run_cli()

high coupling complexity (Ca·Ce = 15).

Grounded coupling-delta finding (deterministic), not an LLM guess.

Comment thread graphify/serve.py Outdated
)


def _load_graph(graph_path: str, *, load_learning_overlay: bool = True) -> nx.Graph:

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Health regression_load_graph()

12 callers depend on it (afferent coupling).

Grounded coupling-delta finding (deterministic), not an LLM guess.

Comment thread graphify/serve.py


def _build_server(graph_path: str):
def _build_server(

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Health regression_build_server()

fans out to 17 callees (efferent coupling); 15 callers depend on it (afferent coupling).

Grounded coupling-delta finding (deterministic), not an LLM guess.

Comment thread graphify/serve.py
return fallback_session_state
return session_states.setdefault(session, {})

def _get_ctx(arguments: dict) -> GraphContext:

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Health regression_get_ctx()

fans out to 7 callees (efferent coupling); 9 callers depend on it (afferent coupling).

Grounded coupling-delta finding (deterministic), not an LLM guess.

Comment thread graphify/serve.py
@@ -2013,17 +2195,34 @@ async def list_resources() -> list[types.Resource]:
]

async def read_resource(uri: AnyUrl) -> str:

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Health regressionread_resource()

fans out to 8 callees (efferent coupling).

Grounded coupling-delta finding (deterministic), not an LLM guess.

Comment thread graphify/serve.py


def serve(graph_path: str | None = None) -> None:
def serve(graph_path: str | None = None, *, registry: GraphRegistry | None = None) -> None:

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Health regressionserve()

fans out to 6 callees (efferent coupling).

Grounded coupling-delta finding (deterministic), not an LLM guess.

Comment thread graphify/serve.py
@@ -2209,8 +2405,9 @@ async def __call__(self, scope, receive, send) -> None:


def _build_http_app(

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Health regression_build_http_app()

23 callers depend on it (afferent coupling).

Grounded coupling-delta finding (deterministic), not an LLM guess.

Comment thread graphify/serve.py
raise ValueError("HTTP binding outside loopback requires --api-key")


def serve_http(

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Health regressionserve_http()

high coupling complexity (Ca·Ce = 12).

Grounded coupling-delta finding (deterministic), not an LLM guess.

Comment thread tests/test_serve_http.py
assert "Nodes: 2" in _call_tool(client, headers, "graph_stats", {}, rid=3)


def test_corrupt_project_graph_is_a_tool_error_without_killing_server(tmp_path):

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Health regressiontest_corrupt_project_graph_is_a_tool_error_without_killing_server()

fans out to 7 callees (efferent coupling).

Grounded coupling-delta finding (deterministic), not an LLM guess.

@YanisGuerault
YanisGuerault force-pushed the feat/add-multi-graph-support branch from ac3b845 to f1dc8c0 Compare September 1, 2026 09:22

@graphify-labs graphify-labs Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Graphify reviewed this change.

Worth a look — the grounded gate found no coupling regressions or blocking issues, but 5 advisory finding(s) below merit a look before merge.

Formal verification. 2 change(s) tested, no difference found (not proven).


Graphify review — findings

Adds a multi-graph MCP serving mode: graphify <repo>... --mcp (and --graphs-dir for recursive discovery via _discover_mcp_graphs) resolves each repo's graphify-out/graph.json, builds a GraphRegistry, and serves over stdio or HTTP, exposing list_graphs/use_graph plus a per-tool graph argument. HTTP binds enforce a nonblank API key on non-loopback hosts, duplicate repo basenames are rejected, and symlinked/hidden directories are skipped during discovery. Reworks the Dockerfile to use the graphify entrypoint against a mounted /data repo, adds docker-compose.multi.yml for two read-only repo graphs, and documents the multi-graph and Compose workflows.

Worth a look

  • _build_http_app default graph_path=None can call Path(None)graphify/serve.py:2468 · Escalate · high
    • agreed by 2 of 2 members but NOT verified (no proof, no reproducing execution) — consensus is not a verdict; needs human review
  • Docker image defaults to unauthenticated HTTP on all interfacesDockerfile:15 · Escalate · medium
    • agreed by 2 of 2 members but NOT verified (no proof, no reproducing execution) — consensus is not a verdict; needs human review
  • from_path uses parent.name while from_paths uses parent.parent.name for the same graph.json layoutgraphify/serve.py · Escalate · medium
    • agreed by 2 of 2 members but NOT verified (no proof, no reproducing execution) — consensus is not a verdict; needs human review
  • MCP project_path allows clients to load arbitrary filesystem graphsgraphify/serve.py · Escalate · medium
    • agreed by 2 of 2 members but NOT verified (no proof, no reproducing execution) — consensus is not a verdict; needs human review
  • project_path branch computes graph.json path but does not honor session current_graph or validate existence before statgraphify/serve.py · Escalate · medium
    • agreed by 2 of 2 members but NOT verified (no proof, no reproducing execution) — consensus is not a verdict; needs human review
Analysis details — impact, health, verification

Impact & health

Graphify review

Impact — 721 functions depend on the 499 functions this change touches.

Health — this change adds coupling hotspots:

  • new: _build_server() — 16 callers, 17 callees
  • new: dispatch_command() — 2 callers, 123 callees
  • new: _query_graph_text() — 20 callers, 10 callees
  • new: _build_http_app() — 23 callers, 4 callees
  • new: _score_query() — 15 callers, 6 callees
  • new: _run_mcp_cli() — 9 callers, 7 callees
  • new: _get_ctx() — 9 callers, 7 callees
  • new: _query_terms() — 20 callers, 3 callees
  • …and 19 more — each is listed as a finding

Verification — 721 functions in the blast radius were not formally verified this run (proofs are advisory here).

Gate & verification

graphify gate

PASS — objectively clean (no health regressions, tests not run — proofs not run this pass (advisory)). Grounded, not self-assessed.

Advisory (not blocking):

  • verification_scope: 709 function(s) in the blast radius were not formally verified this run

Formal verification

No difference found (not proven): No behavior difference found in \_run\_cli (not a proof).

The verifier ran both versions of \_run\_cli on many inputs and saw identical behavior every time. Strong evidence the change is safe, but evidence, not a proof.

Guarantee: Empirical: differential testing (both versions run on many generated inputs). A divergence on an untested input remains possible, so this is 'no counterexample found', not 'proven equivalent'.

Note: An input the sampler did not try could still differ.

Could not verify: Could not verify \_\_init\_\_.

The verifier did not have enough to check \_\_init\_\_, so it is saying so rather than guessing. No false assurance is the whole point.

Guarantee: No guarantee either way, this is an honest abstention, not a pass.

Note: Reason: signature changed: no shared positional arity

Could not verify: Could not verify \_build\_http\_app.

The verifier did not have enough to check \_build\_http\_app, so it is saying so rather than guessing. No false assurance is the whole point.

Guarantee: No guarantee either way, this is an honest abstention, not a pass.

Note: Reason: not verifiable: all 24 sampled inputs raised on both versions — the function never executed, so 'no divergence' would be vacuous (mostly SystemExit — names the real obstacle, not a sampling gap)

Could not verify: Could not verify \_build\_server.

The verifier did not have enough to check \_build\_server, so it is saying so rather than guessing. No false assurance is the whole point.

Guarantee: No guarantee either way, this is an honest abstention, not a pass.

Note: Reason: parameter `graph_path_or_registry` is annotated `str | GraphRegistry` — outside the synthesizable primitive/collection set

Could not verify: Could not verify \_\_init\_\_.

The verifier did not have enough to check \_\_init\_\_, so it is saying so rather than guessing. No false assurance is the whole point.

Guarantee: No guarantee either way, this is an honest abstention, not a pass.

Note: Reason: signature changed: no shared positional arity

Could not verify: Could not verify \_\_init\_\_.

The verifier did not have enough to check \_\_init\_\_, so it is saying so rather than guessing. No false assurance is the whole point.

Guarantee: No guarantee either way, this is an honest abstention, not a pass.

Note: Reason: signature changed: no shared positional arity

Could not verify: Could not verify \_load\_graph.

The verifier did not have enough to check \_load\_graph, so it is saying so rather than guessing. No false assurance is the whole point.

Guarantee: No guarantee either way, this is an honest abstention, not a pass.

Note: Reason: not verifiable: all 23 sampled inputs raised on both versions — the function never executed, so 'no divergence' would be vacuous (mostly SystemExit — names the real obstacle, not a sampling gap)

Verification did not run: Verification did not run for \_main.

The verification could not execute (an environment/toolchain issue, not a statement about the code).

Guarantee: No guarantee, the check itself did not complete.

Note: Detail: harness produced no verdict (rc=1): error: Graph path must be a .json file, got: '/workspace/job-753d71641c074f319b10dba73f9e4761/head/a' error: Graph path

Could not verify: Could not verify \_\_init\_\_.

The verifier did not have enough to check \_\_init\_\_, so it is saying so rather than guessing. No false assurance is the whole point.

Guarantee: No guarantee either way, this is an honest abstention, not a pass.

Note: Reason: signature changed: no shared positional arity

No difference found (not proven): No behavior difference found in \_resolve\_single\_node (not a proof).

The verifier ran both versions of \_resolve\_single\_node on many inputs and saw identical behavior every time. Strong evidence the change is safe, but evidence, not a proof.

Guarantee: Empirical: differential testing (both versions run on many generated inputs). A divergence on an untested input remains possible, so this is 'no counterexample found', not 'proven equivalent'.

Note: An input the sampler did not try could still differ.

Verification did not run: Verification did not run for serve.

The verification could not execute (an environment/toolchain issue, not a statement about the code).

Guarantee: No guarantee, the check itself did not complete.

Note: Detail: harness produced no verdict (rc=1): error: Graph path must be a .json file, got: '/workspace/job-753d71641c074f319b10dba73f9e4761/head/a longer string' erro

Verification did not run: Verification did not run for serve\_http.

The verification could not execute (an environment/toolchain issue, not a statement about the code).

Guarantee: No guarantee, the check itself did not complete.

Note: Detail: harness produced no verdict (rc=124): timeout after 30s

· 14 grounded finding(s) anchored inline below; 13 more finding(s) on lines outside this diff (see the check run).

Comment thread graphify/__main__.py Outdated
serve(registry=registry)


def _serve_mcp_repositories(

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Health regression_serve_mcp_repositories()

6 callers depend on it (afferent coupling).

Grounded coupling-delta finding (deterministic), not an LLM guess.

Comment thread graphify/__main__.py Outdated
return dict(sorted(graphs.items()))


def _run_mcp_cli(args: list[str]) -> bool:

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Health regression_run_mcp_cli()

fans out to 7 callees (efferent coupling); 9 callers depend on it (afferent coupling).

Grounded coupling-delta finding (deterministic), not an LLM guess.

Comment thread graphify/__main__.py
return True


def _run_cli() -> None:

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Health regression_run_cli()

high coupling complexity (Ca·Ce = 15).

Grounded coupling-delta finding (deterministic), not an LLM guess.

Comment thread graphify/serve.py Outdated
)


def _load_graph(graph_path: str, *, load_learning_overlay: bool = True) -> nx.Graph:

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Health regression_load_graph()

13 callers depend on it (afferent coupling).

Grounded coupling-delta finding (deterministic), not an LLM guess.

Comment thread graphify/serve.py


def _build_server(graph_path: str):
def _build_server(

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Health regression_build_server()

fans out to 17 callees (efferent coupling); 16 callers depend on it (afferent coupling).

Grounded coupling-delta finding (deterministic), not an LLM guess.

Comment thread graphify/serve.py
@@ -2013,17 +2218,34 @@ async def list_resources() -> list[types.Resource]:
]

async def read_resource(uri: AnyUrl) -> str:

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Health regressionread_resource()

fans out to 8 callees (efferent coupling).

Grounded coupling-delta finding (deterministic), not an LLM guess.

Comment thread graphify/serve.py


def serve(graph_path: str | None = None) -> None:
def serve(graph_path: str | None = None, *, registry: GraphRegistry | None = None) -> None:

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Health regressionserve()

fans out to 6 callees (efferent coupling).

Grounded coupling-delta finding (deterministic), not an LLM guess.

Comment thread graphify/serve.py
@@ -2209,8 +2428,9 @@ async def __call__(self, scope, receive, send) -> None:


def _build_http_app(

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Health regression_build_http_app()

23 callers depend on it (afferent coupling).

Grounded coupling-delta finding (deterministic), not an LLM guess.

Comment thread graphify/serve.py
raise ValueError("HTTP binding outside loopback requires --api-key")


def serve_http(

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Health regressionserve_http()

high coupling complexity (Ca·Ce = 15).

Grounded coupling-delta finding (deterministic), not an LLM guess.

Comment thread tests/test_serve_http.py
assert "Nodes: 2" in _call_tool(client, headers, "graph_stats", {}, rid=3)


def test_corrupt_project_graph_is_a_tool_error_without_killing_server(tmp_path):

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Health regressiontest_corrupt_project_graph_is_a_tool_error_without_killing_server()

fans out to 7 callees (efferent coupling).

Grounded coupling-delta finding (deterministic), not an LLM guess.

@YanisGuerault
YanisGuerault force-pushed the feat/add-multi-graph-support branch from f1dc8c0 to 20ef370 Compare September 1, 2026 15:28

@graphify-labs graphify-labs Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Graphify reviewed this change.

Worth a look — the grounded gate found no coupling regressions or blocking issues, but 5 advisory finding(s) below merit a look before merge.

Formal verification. 1 change(s) tested, no difference found (not proven).


Graphify review — findings

Adds a multi-graph MCP serving mode: GraphRegistry loads one or many repository graphs (by path, paths, or named paths), resolves the target per-request via a graph param or session default, and rescan hot-reloads graphs whose graph.json mtime changed. Introduces list_graphs/use_graph tools alongside the existing single-graph tools, isolates the active-graph selection per HTTP session, and keeps the learning overlay off for multi-graph loads. Reworks graph load failures to raise GraphLoadError for CLI diagnostics instead of exiting mid-load, and reshapes the Docker/Compose entrypoint to run graphify /data --mcp against mounted read-only repos with GRAPHIFY_API_KEY.

Worth a look

  • os module imported locally in _main but used at module scope by _discover_graphsgraphify/serve.py · Escalate · high
    • agreed by 2 of 2 members but NOT verified (no proof, no reproducing execution) — consensus is not a verdict; needs human review
  • project_path allows arbitrary path traversal to load any graph.json on diskgraphify/serve.py:1721 · Escalate · high
    • agreed by 2 of 2 members but NOT verified (no proof, no reproducing execution) — consensus is not a verdict; needs human review
  • _discover_graphs uses os without a module-level importgraphify/serve.py:2607 · Escalate · high
    • agreed by 2 of 2 members but NOT verified (no proof, no reproducing execution) — consensus is not a verdict; needs human review
  • from_path/from_paths derive graph name from different path levels causing inconsistent naminggraphify/serve.py · Escalate · medium
    • agreed by 2 of 2 members but NOT verified (no proof, no reproducing execution) — consensus is not a verdict; needs human review
  • session_states keyed by session object grows unbounded (leak) and setdefault is non-atomic across concurrent requestsgraphify/serve.py · Escalate · medium
    • agreed by 2 of 2 members but NOT verified (no proof, no reproducing execution) — consensus is not a verdict; needs human review
Analysis details — impact, health, verification

Impact & health

Graphify review

Impact — 716 functions depend on the 499 functions this change touches.

Health — this change adds coupling hotspots:

  • new: _build_server() — 16 callers, 17 callees
  • new: dispatch_command() — 2 callers, 123 callees
  • new: _query_graph_text() — 20 callers, 10 callees
  • new: _build_http_app() — 23 callers, 4 callees
  • new: _score_query() — 15 callers, 6 callees
  • new: _main() — 11 callers, 7 callees
  • new: _get_ctx() — 9 callers, 7 callees
  • new: _query_terms() — 20 callers, 3 callees
  • …and 15 more — each is listed as a finding

Verification — 716 functions in the blast radius were not formally verified this run (proofs are advisory here).

Gate & verification

graphify gate

PASS — objectively clean (no health regressions, tests not run — proofs not run this pass (advisory)). Grounded, not self-assessed.

Advisory (not blocking):

  • verification_scope: 541 function(s) in the blast radius were not formally verified this run

Formal verification

Could not verify: Could not verify \_\_init\_\_.

The verifier did not have enough to check \_\_init\_\_, so it is saying so rather than guessing. No false assurance is the whole point.

Guarantee: No guarantee either way, this is an honest abstention, not a pass.

Note: Reason: signature changed: no shared positional arity

Could not verify: Could not verify \_build\_http\_app.

The verifier did not have enough to check \_build\_http\_app, so it is saying so rather than guessing. No false assurance is the whole point.

Guarantee: No guarantee either way, this is an honest abstention, not a pass.

Note: Reason: not verifiable: all 24 sampled inputs raised on both versions — the function never executed, so 'no divergence' would be vacuous (mostly SystemExit — names the real obstacle, not a sampling gap)

Could not verify: Could not verify \_build\_server.

The verifier did not have enough to check \_build\_server, so it is saying so rather than guessing. No false assurance is the whole point.

Guarantee: No guarantee either way, this is an honest abstention, not a pass.

Note: Reason: parameter `graph_path_or_registry` is annotated `str | GraphRegistry` — outside the synthesizable primitive/collection set

Could not verify: Could not verify \_\_init\_\_.

The verifier did not have enough to check \_\_init\_\_, so it is saying so rather than guessing. No false assurance is the whole point.

Guarantee: No guarantee either way, this is an honest abstention, not a pass.

Note: Reason: signature changed: no shared positional arity

Could not verify: Could not verify \_\_init\_\_.

The verifier did not have enough to check \_\_init\_\_, so it is saying so rather than guessing. No false assurance is the whole point.

Guarantee: No guarantee either way, this is an honest abstention, not a pass.

Note: Reason: signature changed: no shared positional arity

Could not verify: Could not verify \_load\_graph.

The verifier did not have enough to check \_load\_graph, so it is saying so rather than guessing. No false assurance is the whole point.

Guarantee: No guarantee either way, this is an honest abstention, not a pass.

Note: Reason: not verifiable: all 23 sampled inputs raised on both versions — the function never executed, so 'no divergence' would be vacuous (mostly SystemExit — names the real obstacle, not a sampling gap)

Verification did not run: Verification did not run for \_main.

The verification could not execute (an environment/toolchain issue, not a statement about the code).

Guarantee: No guarantee, the check itself did not complete.

Note: Detail: harness produced no verdict (rc=1): error: Graph path must be a .json file, got: '/workspace/job-2fa8e6636c0742869241232e881b9b99/head/a' error: Graph path

Could not verify: Could not verify \_\_init\_\_.

The verifier did not have enough to check \_\_init\_\_, so it is saying so rather than guessing. No false assurance is the whole point.

Guarantee: No guarantee either way, this is an honest abstention, not a pass.

Note: Reason: signature changed: no shared positional arity

No difference found (not proven): No behavior difference found in \_resolve\_single\_node (not a proof).

The verifier ran both versions of \_resolve\_single\_node on many inputs and saw identical behavior every time. Strong evidence the change is safe, but evidence, not a proof.

Guarantee: Empirical: differential testing (both versions run on many generated inputs). A divergence on an untested input remains possible, so this is 'no counterexample found', not 'proven equivalent'.

Note: An input the sampler did not try could still differ.

Verification did not run: Verification did not run for serve.

The verification could not execute (an environment/toolchain issue, not a statement about the code).

Guarantee: No guarantee, the check itself did not complete.

Note: Detail: harness produced no verdict (rc=1): error: Graph path must be a .json file, got: '/workspace/job-2fa8e6636c0742869241232e881b9b99/head/a longer string' erro

Verification did not run: Verification did not run for serve\_http.

The verification could not execute (an environment/toolchain issue, not a statement about the code).

Guarantee: No guarantee, the check itself did not complete.

Note: Detail: harness produced no verdict (rc=124): timeout after 30s

· 10 grounded finding(s) anchored inline below; 13 more finding(s) on lines outside this diff (see the check run).

Comment thread graphify/serve.py


def _build_server(graph_path: str):
def _build_server(

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Health regression_build_server()

fans out to 17 callees (efferent coupling); 16 callers depend on it (afferent coupling).

Grounded coupling-delta finding (deterministic), not an LLM guess.

Comment thread graphify/serve.py
return fallback_session_state
return session_states.setdefault(session, {})

def _get_ctx(arguments: dict) -> GraphContext:

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Health regression_get_ctx()

fans out to 7 callees (efferent coupling); 9 callers depend on it (afferent coupling).

Grounded coupling-delta finding (deterministic), not an LLM guess.

Comment thread graphify/serve.py
@@ -1815,11 +1974,24 @@ def _tool_get_node(arguments: dict) -> str:
])

def _tool_get_neighbors(arguments: dict) -> str:

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Health regression_tool_get_neighbors()

fans out to 8 callees (efferent coupling).

Grounded coupling-delta finding (deterministic), not an LLM guess.

Comment thread graphify/serve.py
@@ -1907,6 +2090,8 @@ def _tool_list_prs(arguments: dict) -> str:
return format_prs_text(prs, base)

def _tool_get_pr_impact(arguments: dict) -> str:

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Health regression_tool_get_pr_impact()

fans out to 7 callees (efferent coupling).

Grounded coupling-delta finding (deterministic), not an LLM guess.

Comment thread graphify/serve.py
@@ -1937,6 +2122,8 @@ def _tool_get_pr_impact(arguments: dict) -> str:
return "\n".join(lines)

def _tool_triage_prs(arguments: dict) -> str:

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Health regression_tool_triage_prs()

fans out to 6 callees (efferent coupling).

Grounded coupling-delta finding (deterministic), not an LLM guess.

Comment thread graphify/serve.py
@@ -2013,17 +2231,34 @@ async def list_resources() -> list[types.Resource]:
]

async def read_resource(uri: AnyUrl) -> str:

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Health regressionread_resource()

fans out to 8 callees (efferent coupling).

Grounded coupling-delta finding (deterministic), not an LLM guess.

Comment thread graphify/serve.py


def serve(graph_path: str | None = None) -> None:
def serve(graph_path: str | None = None, *, registry: GraphRegistry | None = None) -> None:

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Health regressionserve()

fans out to 6 callees (efferent coupling).

Grounded coupling-delta finding (deterministic), not an LLM guess.

Comment thread graphify/serve.py
@@ -2209,8 +2441,9 @@ async def __call__(self, scope, receive, send) -> None:


def _build_http_app(

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Health regression_build_http_app()

23 callers depend on it (afferent coupling).

Grounded coupling-delta finding (deterministic), not an LLM guess.

Comment thread graphify/serve.py
return dict(sorted(graphs.items()))


def _main(argv: list[str] | None = None) -> None:

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Health regression_main()

fans out to 7 callees (efferent coupling); 11 callers depend on it (afferent coupling).

Grounded coupling-delta finding (deterministic), not an LLM guess.

Comment thread tests/test_serve_http.py
assert "Nodes: 2" in _call_tool(client, headers, "graph_stats", {}, rid=3)


def test_corrupt_project_graph_is_a_tool_error_without_killing_server(tmp_path):

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Health regressiontest_corrupt_project_graph_is_a_tool_error_without_killing_server()

fans out to 7 callees (efferent coupling).

Grounded coupling-delta finding (deterministic), not an LLM guess.

@YanisGuerault
YanisGuerault force-pushed the feat/add-multi-graph-support branch from 20ef370 to 3acbaaf Compare September 2, 2026 07:55

@graphify-labs graphify-labs Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Graphify reviewed this change.

Worth a look — the grounded gate found no coupling regressions or blocking issues, but 5 advisory finding(s) below merit a look before merge.

Formal verification. 1 change(s) tested, no difference found (not proven).


Graphify review — findings

Adds a multi-graph MCP serving mode: a GraphRegistry loads one or many repository graph.json files (via from_path/from_paths/from_named_paths), resolves a target per request through _resolve_graph with an error listing available names when ambiguous, and rescan hot-reloads changed graphs while evicting ones whose file disappeared. Exposes list_graphs and use_graph tools plus an optional graph parameter on existing tools, and switches the CLI/build_server to accept --graphs-dir; multi-graph mounts skip the learning overlay and project-path fallbacks. Reworks the Docker story around directory mounts — the image entrypoint and docker-compose.multi.yml now serve read-only --graphs-dir mounts keyed off GRAPHIFY_API_KEY, and .dockerignore/.gitignore exclude .env* and .worktrees/.

Worth a look

  • _discover_graphs references os without a module-level importgraphify/serve.py:2611 · Escalate · high
    • agreed by 2 of 2 members but NOT verified (no proof, no reproducing execution) — consensus is not a verdict; needs human review
  • Docker image default command no longer accepts the documented legacy /data/graph.json mountDockerfile:15 · Escalate · medium
    • agreed by 2 of 2 members but NOT verified (no proof, no reproducing execution) — consensus is not a verdict; needs human review
  • from_paths uses grandparent dir as name, causing collisions and wrong naming vs from_pathgraphify/serve.py · Escalate · medium
    • agreed by 2 of 2 members but NOT verified (no proof, no reproducing execution) — consensus is not a verdict; needs human review
  • rescan keeps stale graph if file disappears during reloadgraphify/serve.py:116 · Escalate · medium
    • agreed by 2 of 2 members but NOT verified (no proof, no reproducing execution) — consensus is not a verdict; needs human review
  • Single-graph resolution can return None if registry changes between names() and get()graphify/serve.py:154 · Escalate · medium
    • agreed by 2 of 2 members but NOT verified (no proof, no reproducing execution) — consensus is not a verdict; needs human review
Analysis details — impact, health, verification

Impact & health

Graphify review

Impact — 699 functions depend on the 482 functions this change touches.

Health — this change adds coupling hotspots:

  • new: _build_server() — 16 callers, 17 callees
  • new: dispatch_command() — 2 callers, 123 callees
  • new: _query_graph_text() — 20 callers, 10 callees
  • new: _build_http_app() — 23 callers, 4 callees
  • new: _score_query() — 15 callers, 6 callees
  • new: _main() — 11 callers, 7 callees
  • new: _get_ctx() — 9 callers, 7 callees
  • new: _query_terms() — 20 callers, 3 callees
  • …and 14 more — each is listed as a finding

Verification — 699 functions in the blast radius were not formally verified this run (proofs are advisory here).

Gate & verification

graphify gate

PASS — objectively clean (no health regressions, tests not run — proofs not run this pass (advisory)). Grounded, not self-assessed.

Advisory (not blocking):

  • verification_scope: 524 function(s) in the blast radius were not formally verified this run

Formal verification

Could not verify: Could not verify \_\_init\_\_.

The verifier did not have enough to check \_\_init\_\_, so it is saying so rather than guessing. No false assurance is the whole point.

Guarantee: No guarantee either way, this is an honest abstention, not a pass.

Note: Reason: signature changed: no shared positional arity

Could not verify: Could not verify \_build\_http\_app.

The verifier did not have enough to check \_build\_http\_app, so it is saying so rather than guessing. No false assurance is the whole point.

Guarantee: No guarantee either way, this is an honest abstention, not a pass.

Note: Reason: not verifiable: all 24 sampled inputs raised on both versions — the function never executed, so 'no divergence' would be vacuous (mostly SystemExit — names the real obstacle, not a sampling gap)

Could not verify: Could not verify \_build\_server.

The verifier did not have enough to check \_build\_server, so it is saying so rather than guessing. No false assurance is the whole point.

Guarantee: No guarantee either way, this is an honest abstention, not a pass.

Note: Reason: parameter `graph_path_or_registry` is annotated `str | GraphRegistry` — outside the synthesizable primitive/collection set

Could not verify: Could not verify \_\_init\_\_.

The verifier did not have enough to check \_\_init\_\_, so it is saying so rather than guessing. No false assurance is the whole point.

Guarantee: No guarantee either way, this is an honest abstention, not a pass.

Note: Reason: signature changed: no shared positional arity

Could not verify: Could not verify \_\_init\_\_.

The verifier did not have enough to check \_\_init\_\_, so it is saying so rather than guessing. No false assurance is the whole point.

Guarantee: No guarantee either way, this is an honest abstention, not a pass.

Note: Reason: signature changed: no shared positional arity

Could not verify: Could not verify \_load\_graph.

The verifier did not have enough to check \_load\_graph, so it is saying so rather than guessing. No false assurance is the whole point.

Guarantee: No guarantee either way, this is an honest abstention, not a pass.

Note: Reason: not verifiable: all 23 sampled inputs raised on both versions — the function never executed, so 'no divergence' would be vacuous (mostly SystemExit — names the real obstacle, not a sampling gap)

Verification did not run: Verification did not run for \_main.

The verification could not execute (an environment/toolchain issue, not a statement about the code).

Guarantee: No guarantee, the check itself did not complete.

Note: Detail: harness produced no verdict (rc=1): error: Graph path must be a .json file, got: '/workspace/job-846033fd262544cf862e69d009fbdb57/head/a' error: Graph path

Could not verify: Could not verify \_\_init\_\_.

The verifier did not have enough to check \_\_init\_\_, so it is saying so rather than guessing. No false assurance is the whole point.

Guarantee: No guarantee either way, this is an honest abstention, not a pass.

Note: Reason: signature changed: no shared positional arity

No difference found (not proven): No behavior difference found in \_resolve\_single\_node (not a proof).

The verifier ran both versions of \_resolve\_single\_node on many inputs and saw identical behavior every time. Strong evidence the change is safe, but evidence, not a proof.

Guarantee: Empirical: differential testing (both versions run on many generated inputs). A divergence on an untested input remains possible, so this is 'no counterexample found', not 'proven equivalent'.

Note: An input the sampler did not try could still differ.

Verification did not run: Verification did not run for serve.

The verification could not execute (an environment/toolchain issue, not a statement about the code).

Guarantee: No guarantee, the check itself did not complete.

Note: Detail: harness produced no verdict (rc=1): error: Graph path must be a .json file, got: '/workspace/job-846033fd262544cf862e69d009fbdb57/head/a longer string' erro

Verification did not run: Verification did not run for serve\_http.

The verification could not execute (an environment/toolchain issue, not a statement about the code).

Guarantee: No guarantee, the check itself did not complete.

Note: Detail: harness produced no verdict (rc=124): timeout after 30s

· 10 grounded finding(s) anchored inline below; 12 more finding(s) on lines outside this diff (see the check run).

Comment thread graphify/serve.py


def _build_server(graph_path: str):
def _build_server(

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Health regression_build_server()

fans out to 17 callees (efferent coupling); 16 callers depend on it (afferent coupling).

Grounded coupling-delta finding (deterministic), not an LLM guess.

Comment thread graphify/serve.py
return fallback_session_state
return session_states.setdefault(session, {})

def _get_ctx(arguments: dict) -> GraphContext:

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Health regression_get_ctx()

fans out to 7 callees (efferent coupling); 9 callers depend on it (afferent coupling).

Grounded coupling-delta finding (deterministic), not an LLM guess.

Comment thread graphify/serve.py
@@ -1815,11 +1980,24 @@ def _tool_get_node(arguments: dict) -> str:
])

def _tool_get_neighbors(arguments: dict) -> str:

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Health regression_tool_get_neighbors()

fans out to 8 callees (efferent coupling).

Grounded coupling-delta finding (deterministic), not an LLM guess.

Comment thread graphify/serve.py
@@ -1907,6 +2096,8 @@ def _tool_list_prs(arguments: dict) -> str:
return format_prs_text(prs, base)

def _tool_get_pr_impact(arguments: dict) -> str:

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Health regression_tool_get_pr_impact()

fans out to 7 callees (efferent coupling).

Grounded coupling-delta finding (deterministic), not an LLM guess.

Comment thread graphify/serve.py
@@ -1937,6 +2128,8 @@ def _tool_get_pr_impact(arguments: dict) -> str:
return "\n".join(lines)

def _tool_triage_prs(arguments: dict) -> str:

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Health regression_tool_triage_prs()

fans out to 6 callees (efferent coupling).

Grounded coupling-delta finding (deterministic), not an LLM guess.

Comment thread graphify/serve.py
@@ -2013,17 +2237,34 @@ async def list_resources() -> list[types.Resource]:
]

async def read_resource(uri: AnyUrl) -> str:

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Health regressionread_resource()

fans out to 8 callees (efferent coupling).

Grounded coupling-delta finding (deterministic), not an LLM guess.

Comment thread graphify/serve.py


def serve(graph_path: str | None = None) -> None:
def serve(graph_path: str | None = None, *, registry: GraphRegistry | None = None) -> None:

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Health regressionserve()

fans out to 6 callees (efferent coupling).

Grounded coupling-delta finding (deterministic), not an LLM guess.

Comment thread graphify/serve.py
@@ -2209,8 +2447,9 @@ async def __call__(self, scope, receive, send) -> None:


def _build_http_app(

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Health regression_build_http_app()

23 callers depend on it (afferent coupling).

Grounded coupling-delta finding (deterministic), not an LLM guess.

Comment thread graphify/serve.py
return dict(sorted(graphs.items()))


def _main(argv: list[str] | None = None) -> None:

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Health regression_main()

fans out to 7 callees (efferent coupling); 11 callers depend on it (afferent coupling).

Grounded coupling-delta finding (deterministic), not an LLM guess.

Comment thread tests/test_serve_http.py
assert "Nodes: 2" in _call_tool(client, headers, "graph_stats", {}, rid=3)


def test_corrupt_project_graph_is_a_tool_error_without_killing_server(tmp_path):

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Health regressiontest_corrupt_project_graph_is_a_tool_error_without_killing_server()

fans out to 7 callees (efferent coupling).

Grounded coupling-delta finding (deterministic), not an LLM guess.

Enable serving multiple knowledge graphs from a single MCP endpoint.
Always registry-based: single graph = 1-entry registry, multi-graph =
directory scan via --graphs-dir flag or GRAPHS_DIR env var.

- Add GraphContext dataclass + GraphRegistry (from_path, from_directory)
- Refactor _build_server to accept GraphRegistry with per-call resolution
- Tool visibility keyed on registry size: list_graphs/use_graph when >1,
  PR tools when ==1. New graph param on all tool schemas.
- Add --graphs-dir CLI flag forcing HTTP transport with auto-rescan
- Remove multi_serve.py and graphify-multi-mcp entry point (consolidated)

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
@YanisGuerault
YanisGuerault force-pushed the feat/add-multi-graph-support branch from 3acbaaf to 069d4cf Compare September 2, 2026 09:23

@graphify-labs graphify-labs Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Graphify reviewed this change.

Worth a look — the grounded gate found no coupling regressions or blocking issues, but 5 advisory finding(s) below merit a look before merge.

Formal verification. 1 change(s) tested, no difference found (not proven).


Graphify review — findings

Adds a multi-graph MCP serving mode: GraphRegistry loads several graphify-out/graph.json files (via from_paths/from_named_paths/--graphs-dir), exposes list_graphs and use_graph plus a per-call graph parameter, and _resolve_graph picks the target from an explicit arg, a session default, or the sole loaded graph, erroring when the choice is ambiguous. Registry loads skip the learning overlay and reject duplicate repository names, and rescan reloads graphs on mtime change and drops ones whose files disappear, swallowing load failures rather than crashing the server. Reworks the Docker/Compose story around directory mounts and API-key env vars — the image now defaults to --graphs-dir /data, adds docker-compose.multi.yml for two read-only repo mounts over localhost HTTP, and documents that remote HTTP requires a nonblank API key.

Worth a look

  • project_path allows arbitrary path traversal to load any graph.json on diskgraphify/serve.py:1740 · Escalate · high
    • agreed by 2 of 2 members but NOT verified (no proof, no reproducing execution) — consensus is not a verdict; needs human review
  • Docker image default command no longer supports the previously documented /data/graph.json mount contractDockerfile:15 · Escalate · medium
    • agreed by 2 of 2 members but NOT verified (no proof, no reproducing execution) — consensus is not a verdict; needs human review
  • from_paths uses parent.parent.name causing collisions and wrong names vs from_pathgraphify/serve.py · Escalate · medium
    • agreed by 2 of 2 members but NOT verified (no proof, no reproducing execution) — consensus is not a verdict; needs human review
  • request-context session state is keyed inconsistentlygraphify/serve.py · Escalate · medium
    • agreed by 2 of 2 members but NOT verified (no proof, no reproducing execution) — consensus is not a verdict; needs human review
  • session_states dict grows unboundedly and is mutated without a lockgraphify/serve.py · Escalate · medium
    • agreed by 2 of 2 members but NOT verified (no proof, no reproducing execution) — consensus is not a verdict; needs human review
Analysis details — impact, health, verification

Impact & health

Graphify review

Impact — 701 functions depend on the 484 functions this change touches.

Health — this change adds coupling hotspots:

  • new: _build_server() — 16 callers, 17 callees
  • new: dispatch_command() — 2 callers, 123 callees
  • new: _query_graph_text() — 20 callers, 10 callees
  • new: _build_http_app() — 24 callers, 4 callees
  • new: _score_query() — 15 callers, 6 callees
  • new: _main() — 11 callers, 7 callees
  • new: _get_ctx() — 9 callers, 7 callees
  • new: _query_terms() — 20 callers, 3 callees
  • …and 14 more — each is listed as a finding

Verification — 701 functions in the blast radius were not formally verified this run (proofs are advisory here).

Gate & verification

graphify gate

PASS — objectively clean (no health regressions, tests not run — proofs not run this pass (advisory)). Grounded, not self-assessed.

Advisory (not blocking):

  • verification_scope: 526 function(s) in the blast radius were not formally verified this run

Formal verification

Could not verify: Could not verify \_\_init\_\_.

The verifier did not have enough to check \_\_init\_\_, so it is saying so rather than guessing. No false assurance is the whole point.

Guarantee: No guarantee either way, this is an honest abstention, not a pass.

Note: Reason: signature changed: no shared positional arity

Could not verify: Could not verify \_build\_http\_app.

The verifier did not have enough to check \_build\_http\_app, so it is saying so rather than guessing. No false assurance is the whole point.

Guarantee: No guarantee either way, this is an honest abstention, not a pass.

Note: Reason: not verifiable: all 24 sampled inputs raised on both versions — the function never executed, so 'no divergence' would be vacuous (mostly SystemExit — names the real obstacle, not a sampling gap)

Could not verify: Could not verify \_build\_server.

The verifier did not have enough to check \_build\_server, so it is saying so rather than guessing. No false assurance is the whole point.

Guarantee: No guarantee either way, this is an honest abstention, not a pass.

Note: Reason: parameter `graph_path_or_registry` is annotated `str | GraphRegistry` — outside the synthesizable primitive/collection set

Could not verify: Could not verify \_\_init\_\_.

The verifier did not have enough to check \_\_init\_\_, so it is saying so rather than guessing. No false assurance is the whole point.

Guarantee: No guarantee either way, this is an honest abstention, not a pass.

Note: Reason: signature changed: no shared positional arity

Could not verify: Could not verify \_\_init\_\_.

The verifier did not have enough to check \_\_init\_\_, so it is saying so rather than guessing. No false assurance is the whole point.

Guarantee: No guarantee either way, this is an honest abstention, not a pass.

Note: Reason: signature changed: no shared positional arity

Could not verify: Could not verify \_load\_graph.

The verifier did not have enough to check \_load\_graph, so it is saying so rather than guessing. No false assurance is the whole point.

Guarantee: No guarantee either way, this is an honest abstention, not a pass.

Note: Reason: not verifiable: all 23 sampled inputs raised on both versions — the function never executed, so 'no divergence' would be vacuous (mostly SystemExit — names the real obstacle, not a sampling gap)

Verification did not run: Verification did not run for \_main.

The verification could not execute (an environment/toolchain issue, not a statement about the code).

Guarantee: No guarantee, the check itself did not complete.

Note: Detail: harness produced no verdict (rc=1): error: Graph path must be a .json file, got: '/workspace/job-05490e4bcaef4e028b5380e0966f06f6/head/a' error: Graph path

Could not verify: Could not verify \_\_init\_\_.

The verifier did not have enough to check \_\_init\_\_, so it is saying so rather than guessing. No false assurance is the whole point.

Guarantee: No guarantee either way, this is an honest abstention, not a pass.

Note: Reason: signature changed: no shared positional arity

No difference found (not proven): No behavior difference found in \_resolve\_single\_node (not a proof).

The verifier ran both versions of \_resolve\_single\_node on many inputs and saw identical behavior every time. Strong evidence the change is safe, but evidence, not a proof.

Guarantee: Empirical: differential testing (both versions run on many generated inputs). A divergence on an untested input remains possible, so this is 'no counterexample found', not 'proven equivalent'.

Note: An input the sampler did not try could still differ.

Verification did not run: Verification did not run for serve.

The verification could not execute (an environment/toolchain issue, not a statement about the code).

Guarantee: No guarantee, the check itself did not complete.

Note: Detail: harness produced no verdict (rc=1): error: Graph path must be a .json file, got: '/workspace/job-05490e4bcaef4e028b5380e0966f06f6/head/a longer string' erro

Verification did not run: Verification did not run for serve\_http.

The verification could not execute (an environment/toolchain issue, not a statement about the code).

Guarantee: No guarantee, the check itself did not complete.

Note: Detail: harness produced no verdict (rc=124): timeout after 30s

· 10 grounded finding(s) anchored inline below; 12 more finding(s) on lines outside this diff (see the check run).

Comment thread graphify/serve.py


def _build_server(graph_path: str):
def _build_server(

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Health regression_build_server()

fans out to 17 callees (efferent coupling); 16 callers depend on it (afferent coupling).

Grounded coupling-delta finding (deterministic), not an LLM guess.

Comment thread graphify/serve.py
return fallback_session_state
return session_states.setdefault(session, {})

def _get_ctx(arguments: dict) -> GraphContext:

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Health regression_get_ctx()

fans out to 7 callees (efferent coupling); 9 callers depend on it (afferent coupling).

Grounded coupling-delta finding (deterministic), not an LLM guess.

Comment thread graphify/serve.py
@@ -1815,11 +1987,24 @@ def _tool_get_node(arguments: dict) -> str:
])

def _tool_get_neighbors(arguments: dict) -> str:

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Health regression_tool_get_neighbors()

fans out to 8 callees (efferent coupling).

Grounded coupling-delta finding (deterministic), not an LLM guess.

Comment thread graphify/serve.py
@@ -1907,6 +2103,8 @@ def _tool_list_prs(arguments: dict) -> str:
return format_prs_text(prs, base)

def _tool_get_pr_impact(arguments: dict) -> str:

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Health regression_tool_get_pr_impact()

fans out to 7 callees (efferent coupling).

Grounded coupling-delta finding (deterministic), not an LLM guess.

Comment thread graphify/serve.py
@@ -1937,6 +2135,8 @@ def _tool_get_pr_impact(arguments: dict) -> str:
return "\n".join(lines)

def _tool_triage_prs(arguments: dict) -> str:

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Health regression_tool_triage_prs()

fans out to 6 callees (efferent coupling).

Grounded coupling-delta finding (deterministic), not an LLM guess.

Comment thread graphify/serve.py
@@ -2013,17 +2244,34 @@ async def list_resources() -> list[types.Resource]:
]

async def read_resource(uri: AnyUrl) -> str:

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Health regressionread_resource()

fans out to 8 callees (efferent coupling).

Grounded coupling-delta finding (deterministic), not an LLM guess.

Comment thread graphify/serve.py


def serve(graph_path: str | None = None) -> None:
def serve(graph_path: str | None = None, *, registry: GraphRegistry | None = None) -> None:

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Health regressionserve()

fans out to 6 callees (efferent coupling).

Grounded coupling-delta finding (deterministic), not an LLM guess.

Comment thread graphify/serve.py
@@ -2209,8 +2461,9 @@ async def __call__(self, scope, receive, send) -> None:


def _build_http_app(

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Health regression_build_http_app()

24 callers depend on it (afferent coupling).

Grounded coupling-delta finding (deterministic), not an LLM guess.

Comment thread graphify/serve.py
return dict(sorted(graphs.items()))


def _main(argv: list[str] | None = None) -> None:

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Health regression_main()

fans out to 7 callees (efferent coupling); 11 callers depend on it (afferent coupling).

Grounded coupling-delta finding (deterministic), not an LLM guess.

Comment thread tests/test_serve_http.py
assert "Nodes: 2" in _call_tool(client, headers, "graph_stats", {}, rid=3)


def test_corrupt_project_graph_is_a_tool_error_without_killing_server(tmp_path):

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Health regressiontest_corrupt_project_graph_is_a_tool_error_without_killing_server()

fans out to 7 callees (efferent coupling).

Grounded coupling-delta finding (deterministic), not an LLM guess.

@graphify-labs graphify-labs Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Graphify reviewed this change.

Worth a look — the grounded gate found no coupling regressions or blocking issues, but 5 advisory finding(s) below merit a look before merge.

Formal verification. 1 change(s) tested, no difference found (not proven).


Graphify review — findings

Adds a multi-graph MCP server: serve can now be pointed at a directory with --graphs-dir (or multiple graph paths) and exposes every repository's graphify-out/graph.json from one endpoint, adding list_graphs/use_graph tools and an optional graph parameter on each existing tool. Introduces a GraphRegistry that loads, per-request-reloads on mtime change, and periodically rediscovers graphs on a configurable --graph-scan-interval (default 30s, 0 to rescan every request), dropping graphs whose files disappear and skipping ones that fail to load. Reworks the Docker image and adds docker-compose.multi.yml to serve read-only-mounted repos over localhost HTTP with a required GRAPHIFY_API_KEY, and updates the README/Dockerfile invocation accordingly.

Worth a look

  • _discover_graphs uses os without a module-level importgraphify/serve.py:2671 · Escalate · high
    • agreed by 2 of 2 members but NOT verified (no proof, no reproducing execution) — consensus is not a verdict; needs human review
  • Docker image default command no longer serves the documented mounted graph fileDockerfile:15 · Escalate · medium
    • agreed by 2 of 2 members but NOT verified (no proof, no reproducing execution) — consensus is not a verdict; needs human review
  • rescan swallows SystemExit from _load_graph, but from_named_paths/discovery path can still exit process on legacy loadergraphify/serve.py · Escalate · medium
    • agreed by 2 of 2 members but NOT verified (no proof, no reproducing execution) — consensus is not a verdict; needs human review
  • rescan() re-loads graph while holding lock, blocking all get()/names() callersgraphify/serve.py · Escalate · medium
    • agreed by 2 of 2 members but NOT verified (no proof, no reproducing execution) — consensus is not a verdict; needs human review
  • os imported at module scope may shadow local import in _maingraphify/serve.py · Escalate · medium
    • agreed by 2 of 2 members but NOT verified (no proof, no reproducing execution) — consensus is not a verdict; needs human review
Analysis details — impact, health, verification

Impact & health

Graphify review

Impact — 716 functions depend on the 499 functions this change touches.

Health — this change adds coupling hotspots:

  • new: _build_server() — 17 callers, 17 callees
  • new: dispatch_command() — 2 callers, 123 callees
  • new: _query_graph_text() — 20 callers, 10 callees
  • new: _main() — 16 callers, 6 callees
  • new: _build_http_app() — 24 callers, 4 callees
  • new: _score_query() — 15 callers, 6 callees
  • new: _get_ctx() — 9 callers, 7 callees
  • new: _query_terms() — 20 callers, 3 callees
  • …and 15 more — each is listed as a finding

Verification — 716 functions in the blast radius were not formally verified this run (proofs are advisory here).

Gate & verification

graphify gate

PASS — objectively clean (no health regressions, tests not run — proofs not run this pass (advisory)). Grounded, not self-assessed.

Advisory (not blocking):

  • verification_scope: 541 function(s) in the blast radius were not formally verified this run

Formal verification

Could not verify: Could not verify \_\_init\_\_.

The verifier did not have enough to check \_\_init\_\_, so it is saying so rather than guessing. No false assurance is the whole point.

Guarantee: No guarantee either way, this is an honest abstention, not a pass.

Note: Reason: signature changed: no shared positional arity

Could not verify: Could not verify \_build\_http\_app.

The verifier did not have enough to check \_build\_http\_app, so it is saying so rather than guessing. No false assurance is the whole point.

Guarantee: No guarantee either way, this is an honest abstention, not a pass.

Note: Reason: not verifiable: all 24 sampled inputs raised on both versions — the function never executed, so 'no divergence' would be vacuous (mostly SystemExit — names the real obstacle, not a sampling gap)

Could not verify: Could not verify \_build\_server.

The verifier did not have enough to check \_build\_server, so it is saying so rather than guessing. No false assurance is the whole point.

Guarantee: No guarantee either way, this is an honest abstention, not a pass.

Note: Reason: parameter `graph_path_or_registry` is annotated `str | GraphRegistry` — outside the synthesizable primitive/collection set

Could not verify: Could not verify \_\_init\_\_.

The verifier did not have enough to check \_\_init\_\_, so it is saying so rather than guessing. No false assurance is the whole point.

Guarantee: No guarantee either way, this is an honest abstention, not a pass.

Note: Reason: signature changed: no shared positional arity

Could not verify: Could not verify \_\_init\_\_.

The verifier did not have enough to check \_\_init\_\_, so it is saying so rather than guessing. No false assurance is the whole point.

Guarantee: No guarantee either way, this is an honest abstention, not a pass.

Note: Reason: signature changed: no shared positional arity

Could not verify: Could not verify \_load\_graph.

The verifier did not have enough to check \_load\_graph, so it is saying so rather than guessing. No false assurance is the whole point.

Guarantee: No guarantee either way, this is an honest abstention, not a pass.

Note: Reason: not verifiable: all 23 sampled inputs raised on both versions — the function never executed, so 'no divergence' would be vacuous (mostly SystemExit — names the real obstacle, not a sampling gap)

Verification did not run: Verification did not run for \_main.

The verification could not execute (an environment/toolchain issue, not a statement about the code).

Guarantee: No guarantee, the check itself did not complete.

Note: Detail: harness produced no verdict (rc=1): error: Graph path must be a .json file, got: '/workspace/job-0d7a6f4dce554d389f9dc90e9fc305f7/head/a' error: Graph path

Could not verify: Could not verify \_\_init\_\_.

The verifier did not have enough to check \_\_init\_\_, so it is saying so rather than guessing. No false assurance is the whole point.

Guarantee: No guarantee either way, this is an honest abstention, not a pass.

Note: Reason: signature changed: no shared positional arity

No difference found (not proven): No behavior difference found in \_resolve\_single\_node (not a proof).

The verifier ran both versions of \_resolve\_single\_node on many inputs and saw identical behavior every time. Strong evidence the change is safe, but evidence, not a proof.

Guarantee: Empirical: differential testing (both versions run on many generated inputs). A divergence on an untested input remains possible, so this is 'no counterexample found', not 'proven equivalent'.

Note: An input the sampler did not try could still differ.

Verification did not run: Verification did not run for serve.

The verification could not execute (an environment/toolchain issue, not a statement about the code).

Guarantee: No guarantee, the check itself did not complete.

Note: Detail: harness produced no verdict (rc=1): error: Graph path must be a .json file, got: '/workspace/job-0d7a6f4dce554d389f9dc90e9fc305f7/head/a longer string' erro

Verification did not run: Verification did not run for serve\_http.

The verification could not execute (an environment/toolchain issue, not a statement about the code).

Guarantee: No guarantee, the check itself did not complete.

Note: Detail: harness produced no verdict (rc=124): timeout after 30s

· 11 grounded finding(s) anchored inline below; 12 more finding(s) on lines outside this diff (see the check run).

Comment thread graphify/serve.py
)


def _load_graph_or_raise(graph_path: str, *, load_learning_overlay: bool = True) -> nx.Graph:

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Health regression_load_graph_or_raise()

high coupling complexity (Ca·Ce = 15).

Grounded coupling-delta finding (deterministic), not an LLM guess.

Comment thread graphify/serve.py


def _build_server(graph_path: str):
def _build_server(

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Health regression_build_server()

fans out to 17 callees (efferent coupling); 17 callers depend on it (afferent coupling).

Grounded coupling-delta finding (deterministic), not an LLM guess.

Comment thread graphify/serve.py
return fallback_session_state
return session_states.setdefault(session, {})

def _get_ctx(arguments: dict) -> GraphContext:

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Health regression_get_ctx()

fans out to 7 callees (efferent coupling); 9 callers depend on it (afferent coupling).

Grounded coupling-delta finding (deterministic), not an LLM guess.

Comment thread graphify/serve.py
@@ -1815,11 +2030,24 @@ def _tool_get_node(arguments: dict) -> str:
])

def _tool_get_neighbors(arguments: dict) -> str:

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Health regression_tool_get_neighbors()

fans out to 8 callees (efferent coupling).

Grounded coupling-delta finding (deterministic), not an LLM guess.

Comment thread graphify/serve.py
@@ -1907,6 +2146,8 @@ def _tool_list_prs(arguments: dict) -> str:
return format_prs_text(prs, base)

def _tool_get_pr_impact(arguments: dict) -> str:

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Health regression_tool_get_pr_impact()

fans out to 7 callees (efferent coupling).

Grounded coupling-delta finding (deterministic), not an LLM guess.

Comment thread graphify/serve.py
@@ -2013,17 +2287,34 @@ async def list_resources() -> list[types.Resource]:
]

async def read_resource(uri: AnyUrl) -> str:

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Health regressionread_resource()

fans out to 8 callees (efferent coupling).

Grounded coupling-delta finding (deterministic), not an LLM guess.

Comment thread graphify/serve.py


def serve(graph_path: str | None = None) -> None:
def serve(graph_path: str | None = None, *, registry: GraphRegistry | None = None) -> None:

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Health regressionserve()

fans out to 6 callees (efferent coupling).

Grounded coupling-delta finding (deterministic), not an LLM guess.

Comment thread graphify/serve.py
@@ -2209,8 +2504,9 @@ async def __call__(self, scope, receive, send) -> None:


def _build_http_app(

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Health regression_build_http_app()

24 callers depend on it (afferent coupling).

Grounded coupling-delta finding (deterministic), not an LLM guess.

Comment thread graphify/serve.py
return dict(sorted(graphs.items()))


def _main(argv: list[str] | None = None) -> None:

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Health regression_main()

fans out to 6 callees (efferent coupling); 16 callers depend on it (afferent coupling).

Grounded coupling-delta finding (deterministic), not an LLM guess.

Comment thread tests/test_serve_http.py
assert "Nodes: 2" in _call_tool(client, headers, "graph_stats", {}, rid=3)


def test_corrupt_project_graph_is_a_tool_error_without_killing_server(tmp_path):

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Health regressiontest_corrupt_project_graph_is_a_tool_error_without_killing_server()

fans out to 7 callees (efferent coupling).

Grounded coupling-delta finding (deterministic), not an LLM guess.

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.

2 participants