Skip to content

fix: render rationale in graph read surfaces - #2830

Open
hopstreax wants to merge 1 commit into
Graphify-Labs:v8from
hopstreax:fix/2821-render-rationale
Open

fix: render rationale in graph read surfaces#2830
hopstreax wants to merge 1 commit into
Graphify-Labs:v8from
hopstreax:fix/2821-render-rationale

Conversation

@hopstreax

Copy link
Copy Markdown
Contributor

Summary

Fixes #2821.

Rationale is already persisted as a node attribute during semantic cleanup, but the existing read surfaces never rendered it. This made the rationale effectively write-only: query could identify the correct node, but the explanation for why that node was relevant was unavailable to the caller.

This PR adds opt-in rationale rendering to the CLI and MCP read surfaces.

Changes

  • Add --rationale to:
    • graphify query
    • graphify explain
  • Add include_rationale to:
    • MCP query_graph
    • MCP get_node
  • Render rationale inline as WHY ... for query/query_graph.
  • Render rationale as a dedicated Rationale: field for get_node/explain.
  • Add shared rationale sanitization and truncation.
  • Cap query rationale at 512 characters per node.
  • Cap detail rationale at 2048 characters.
  • Include query rationale in the existing token budget.
  • Preserve the existing output and behavior when rationale is not requested.

Backward compatibility

Rationale is opt-in and defaults to disabled.

Existing CLI invocations and MCP callers that do not request rationale retain the existing output format.

Tests

Added coverage for:

  • rationale sanitization and whitespace handling
  • rationale truncation
  • query/query_graph rendering
  • existing token-budget behavior
  • get_node and explain rendering
  • CLI --rationale
  • MCP include_rationale
  • missing/empty rationale
  • backward compatibility when include_rationale is omitted

Focused test suites:

  • 237 passed

The full suite also passes all tests relevant to this change; the remaining failures are pre-existing Windows/POSIX-specific tests unrelated to this PR.

@hopstreax
hopstreax force-pushed the fix/2821-render-rationale branch from 7559d61 to 60b9b67 Compare August 17, 2026 19:27

@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 2 advisory finding(s) below merit a look before merge.

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


Graphify review — findings

Adds an optional --rationale/include_rationale flag across the query and explain CLI commands, _query_graph_text/_subgraph_to_text, and the MCP query_graph/get_node tools to surface node rationale (as a compact WHY suffix or a full detail block). Introduces sanitize_rationale in security.py with MAX_QUERY_RATIONALE_CHARS/MAX_DETAIL_RATIONALE_CHARS caps for single-line and multiline modes.

Worth a look

  • String "false" enables rationale disclosuregraphify/serve.py:1738 · Escalate · medium
    • agreed by 2 of 2 members but NOT verified (no proof, no reproducing execution) — consensus is not a verdict; needs human review Execution auto-disposal is off for this run; enable it (with sandbox isolation) to have Graphify try to confirm or refute this automatically.
  • String "false" enables node rationale disclosuregraphify/serve.py:1766 · Escalate · medium
    • agreed by 2 of 2 members but NOT verified (no proof, no reproducing execution) — consensus is not a verdict; needs human review Execution auto-disposal is off for this run; enable it (with sandbox isolation) to have Graphify try to confirm or refute this automatically.
Analysis details — impact, health, verification

Impact & health

Graphify review

Impact — 1571 functions depend on the 534 functions this change touches.

Health — this change adds coupling hotspots:

  • new: _rebuild_code() — 98 callers, 51 callees
  • new: build_merge() — 46 callers, 14 callees
  • new: _extract_generic() — 18 callers, 24 callees
  • new: extract_bash() — 40 callers, 10 callees
  • new: extract_xaml() — 19 callers, 17 callees
  • new: ingest_scip_json() — 76 callers, 4 callees
  • new: to_json() — 49 callers, 6 callees
  • new: dispatch_command() — 2 callers, 118 callees
  • …and 45 more — each is listed as a finding

Verification — 1571 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: 1054 function(s) in the blast radius were not formally verified this run

Formal verification

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 \_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 23 sampled inputs raised on both versions — the function never executed, so 'no divergence' would be vacuous (mostly ImportError — names the real obstacle, not a sampling gap)

No difference found (not proven): No behavior difference found in \_query\_graph\_text (not a proof).

The verifier ran both versions of \_query\_graph\_text 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.

No difference found (not proven): No behavior difference found in \_subgraph\_to\_text (not a proof).

The verifier ran both versions of \_subgraph\_to\_text 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.

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

Comment thread graphify/serve.py


def _subgraph_to_text(G: nx.Graph, nodes: set[str], edges: list[tuple], token_budget: int = 2000, *, seeds: list[str] | None = None) -> str:
def _subgraph_to_text(G: nx.Graph, nodes: set[str], edges: list[tuple], token_budget: int = 2000, *, seeds: list[str] | None = None, include_rationale: bool = False) -> 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_subgraph_to_text()

26 callers depend on it (afferent coupling).

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

Comment thread graphify/serve.py
lines.append(f" Degree: {G.degree(nid)}")
return "\n".join(lines)

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 6 callees (efferent coupling).

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

@hopstreax
hopstreax force-pushed the fix/2821-render-rationale branch from 60b9b67 to 1e5414b Compare August 17, 2026 19:38

@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 1 advisory finding(s) below merit a look before merge.

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


Graphify review — findings

Adds an opt-in --rationale/include_rationale flag across the query and explain CLI commands and the MCP query_graph/get_node tools, threading it through _query_graph_text, _subgraph_to_text, and _tool_get_node to emit node rationale (as a compact WHY suffix in query output, full text in detail views). Introduces sanitize_rationale in security.py with MAX_QUERY_RATIONALE_CHARS/MAX_DETAIL_RATIONALE_CHARS caps and single-line vs. paragraph-preserving modes. Updates usage strings and tool input schemas accordingly.

Worth a look

  • Rationale sanitizer leaves C1 terminal control characters intactgraphify/security.py:411 · Escalate · medium
    • agreed by 2 of 2 members but NOT verified (no proof, no reproducing execution) — consensus is not a verdict; needs human review Execution auto-disposal is off for this run; enable it (with sandbox isolation) to have Graphify try to confirm or refute this automatically.
Analysis details — impact, health, verification

Impact & health

Graphify review

Impact — 1573 functions depend on the 536 functions this change touches.

Health — this change adds coupling hotspots:

  • new: _rebuild_code() — 98 callers, 51 callees
  • new: build_merge() — 46 callers, 14 callees
  • new: _extract_generic() — 18 callers, 24 callees
  • new: extract_bash() — 40 callers, 10 callees
  • new: extract_xaml() — 19 callers, 17 callees
  • new: ingest_scip_json() — 76 callers, 4 callees
  • new: to_json() — 50 callers, 6 callees
  • new: dispatch_command() — 2 callers, 118 callees
  • …and 45 more — each is listed as a finding

Verification — 1573 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: 1056 function(s) in the blast radius were not formally verified this run

Formal verification

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 \_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 23 sampled inputs raised on both versions — the function never executed, so 'no divergence' would be vacuous (mostly ImportError — names the real obstacle, not a sampling gap)

No difference found (not proven): No behavior difference found in \_query\_graph\_text (not a proof).

The verifier ran both versions of \_query\_graph\_text 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.

No difference found (not proven): No behavior difference found in \_subgraph\_to\_text (not a proof).

The verifier ran both versions of \_subgraph\_to\_text 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.

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

Comment thread graphify/serve.py


def _subgraph_to_text(G: nx.Graph, nodes: set[str], edges: list[tuple], token_budget: int = 2000, *, seeds: list[str] | None = None) -> str:
def _subgraph_to_text(G: nx.Graph, nodes: set[str], edges: list[tuple], token_budget: int = 2000, *, seeds: list[str] | None = None, include_rationale: bool = False) -> 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_subgraph_to_text()

26 callers depend on it (afferent coupling).

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

Comment thread graphify/serve.py
lines.append(f" Degree: {G.degree(nid)}")
return "\n".join(lines)

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 6 callees (efferent coupling).

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

@hopstreax
hopstreax force-pushed the fix/2821-render-rationale branch from 1e5414b to cd9fb86 Compare August 18, 2026 05: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.

Looks safe to merge — no coupling regressions and no blocking issues, checked against the code graph (not a self-assessment).

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


Graphify review — findings

Adds an opt-in --rationale flag to the query and explain CLI commands and a matching include_rationale input to the MCP query/node tools, threading it through _query_graph_text and _subgraph_to_text to render a WHY suffix on nodes. Introduces sanitize_rationale in security.py with compact single-line and detail multi-line modes plus char caps (MAX_QUERY_RATIONALE_CHARS/MAX_DETAIL_RATIONALE_CHARS), and extends _CONTROL_CHAR_RE to strip C1 control characters (\x7f-\x9f).

No blocking issues surfaced. 10 lower-confidence candidates did not survive cross-model review.

Analysis details — impact, health, verification

Impact & health

Graphify review

Impact — 1576 functions depend on the 539 functions this change touches.

Health — this change adds coupling hotspots:

  • new: _rebuild_code() — 98 callers, 51 callees
  • new: build_merge() — 46 callers, 14 callees
  • new: _extract_generic() — 18 callers, 24 callees
  • new: extract_bash() — 40 callers, 10 callees
  • new: extract_xaml() — 19 callers, 17 callees
  • new: ingest_scip_json() — 76 callers, 4 callees
  • new: to_json() — 50 callers, 6 callees
  • new: dispatch_command() — 2 callers, 118 callees
  • …and 45 more — each is listed as a finding

Verification — 1576 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: 1059 function(s) in the blast radius were not formally verified this run

Formal verification

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 \_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 23 sampled inputs raised on both versions — the function never executed, so 'no divergence' would be vacuous (mostly ImportError — names the real obstacle, not a sampling gap)

No difference found (not proven): No behavior difference found in \_query\_graph\_text (not a proof).

The verifier ran both versions of \_query\_graph\_text 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.

No difference found (not proven): No behavior difference found in \_subgraph\_to\_text (not a proof).

The verifier ran both versions of \_subgraph\_to\_text 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.

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

Comment thread graphify/serve.py
Comment thread graphify/serve.py

@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 1 advisory finding(s) below merit a look before merge.


Graphify review — findings

Adds an opt-in --rationale/include_rationale path across the query and explain CLI commands, _query_graph_text/_subgraph_to_text, and the MCP query/get_node tools, rendering node rationales as a WHY suffix (compact) or detail line. Introduces sanitize_rationale in security.py with MAX_QUERY_RATIONALE_CHARS/MAX_DETAIL_RATIONALE_CHARS caps, control-char stripping, and single-line vs newline-preserving modes.

Worth a look

  • Rationale sanitizer leaves C1 terminal control characters intactgraphify/security.py:411 · Escalate · medium
    • agreed by 2 of 2 members but NOT verified (no proof, no reproducing execution) — consensus is not a verdict; needs human review Execution auto-disposal is off for this run; enable it (with sandbox isolation) to have Graphify try to confirm or refute this automatically.
Analysis details — impact, health, verification

Impact & health

Graphify review

Impact — 1527 functions depend on the 524 functions this change touches.

Health — this change adds coupling hotspots:

  • new: _rebuild_code() — 98 callers, 51 callees
  • new: build_merge() — 43 callers, 14 callees
  • new: _extract_generic() — 18 callers, 24 callees
  • new: extract_bash() — 40 callers, 10 callees
  • new: extract_xaml() — 19 callers, 17 callees
  • new: ingest_scip_json() — 76 callers, 4 callees
  • new: to_json() — 49 callers, 6 callees
  • new: dispatch_command() — 2 callers, 118 callees
  • …and 43 more — each is listed as a finding

Verification — 1527 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: 1026 function(s) in the blast radius were not formally verified this run

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

Comment thread graphify/serve.py
@@ -1725,20 +1763,30 @@ def _tool_query_graph(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 6 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.

rationale attribute is never rendered by query / get_node — the node that answers is returned label-only

1 participant