Repository navigation
Conversation
…haracters (Graphify-Labs#2821) Review follow-ups on the rebased Graphify-Labs#2830: - indent continuation lines in explain/get_node so a rationale cannot pose as a sibling field - strip bidi/zero-width characters; treat U+2028, U+2029 and NEL as line breaks - keep the shared control-char regex unchanged; the C1 range applies to rationale only - ignore non-string rationale values; clamp a negative max_chars - list --rationale in the main help text Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
|
Thanks for the pull request, @yotamleo. A maintainer will review it soon. Want to talk it through while it is in review? Come join us on our Discord server. For longer-form discussion there is also GitHub Discussions. A couple of things that speed up review: make sure the test suite passes on Python 3.10 and 3.13, and that the change keeps extraction deterministic. |
There was a problem hiding this comment.
Graphify reviewed this change.
No issues found by static checks — no coupling regressions or blocking issues in the code graph. That is not the same as safe to merge: see what was not checked.
Not checked: tests were not run; formal verification proved 0 of 6 changed function(s) (3 sampled, not proven, 3 not verified).
Formal verification. PR-changed functions: 3/6 verified (0 proven, 3 may-equivalent, 0 distinguished) · 3 not verified (3 vacuous).
Not verified on this run: dispatch\_command (vacuous: never exercised), \_build\_server (vacuous: never exercised), \_render\_query (vacuous: never exercised).
Graphify review — findings
Adds an opt-in --rationale flag to graphify query and graphify explain that shows a node's stored rationale. Query appends it as a single-line WHY suffix capped at 512 chars, and explain prints an indented Rationale: block that keeps paragraphs up to 2048 chars; without the flag, output is unchanged. Introduces sanitize_rationale and indent_continuation, which strip control, bidi and zero-width characters, mark truncation with ..., and indent continuation lines so multi-line text can't pose as a sibling field, while sanitize_label keeps its existing behaviour. Threads include_rationale through _subgraph_to_text and _query_graph_text into the rendered output and documents the flag in the help text.
Review partial — this diff was larger than one review pass covers, so later files were not reviewed; some findings may be missing.
Analysis details — impact, health, verification
Impact & health
Graphify review
Impact — 2377 functions depend on the 689 functions this change touches.
Health — this change adds coupling hotspots:
- worse:
to_json()— 87 callers, 11 callees - worse:
dispatch_command()— 5 callers, 136 callees - worse:
main()— 111 callers, 3 callees - worse:
_query_graph_text()— 49 callers, 5 callees - worse:
_subgraph_to_text()— 31 callers, 4 callees - worse:
_build_server()— 6 callers, 19 callees
Verification — 2377 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: 1719 function(s) in the blast radius were not formally verified this run
Test selection
Test selection
96 of 432 test file(s) selected (22%) via static blast radius.
tests/test_affected_cli.py— impacttests/test_agents_platform.py— impacttests/test_atomic_writes.py— impacttests/test_benchmark.py— impacttests/test_benchmark_raw_graph.py— impacttests/test_benchmark_real_query.py— impacttests/test_build.py— impacttests/test_build_located_semantic_identity.py— impacttests/test_build_merge_dedup_scope.py— impacttests/test_build_merge_hyperedges_and_prune.py— impacttests/test_build_merge_shrink_guard.py— impacttests/test_callflow_html.py— impacttests/test_carried_hyperedge_remap.py— impacttests/test_chunking.py— impacttests/test_cli_export.py— impacttests/test_cloud_cta.py— impacttests/test_codebuddy.py— impacttests/test_community_labels_skill.py— impacttests/test_compact_graph_json.py— impacttests/test_confidence.py— impacttests/test_corrupt_graph_json.py— impacttests/test_cross_extension_reexport_self_cycle.py— impacttests/test_dedup.py— impacttests/test_dedup_shrink_refuses_force_write.py— impacttests/test_devin.py— impacttests/test_dotnet.py— impacttests/test_explain_cli.py— impact, changed-testtests/test_explain_same_file_tie.py— impacttests/test_export.py— impacttests/test_export_control_characters.py— impacttests/test_export_direction.py— impacttests/test_extract.py— impacttests/test_extract_cli.py— impacttests/test_file_label_disambiguation.py— impacttests/test_global_add_tag_inference.py— impacttests/test_global_graph.py— impacttests/test_go_qualified_resolution.py— impacttests/test_god_nodes_cli.py— impacttests/test_god_nodes_exclude_hubs.py— impacttests/test_hollow_chunks_arm_shrink_guard.py— impacttests/test_hook_guard_token_match.py— impacttests/test_hook_out_of_project_paths.py— impacttests/test_hook_strict.py— impacttests/test_html_script_breakout.py— impacttests/test_html_theme.py— impacttests/test_hyperedge_roundtrip.py— impacttests/test_hypergraph.py— impacttests/test_incomplete_build_guard.py— impacttests/test_ingest.py— impacttests/test_ingest_url_type.py— impact- … and 46 more
Selection is safe under the controlled-regression assumption; always-run tests + a periodic full run are the backstops. Advisory — it never changes the check verdict.
Docs that may be stale (advisory)
docs/superpowers/plans/2026-05-04-incremental-updates-dedup.md§ Task 4: Incremental updates — semantic cache + manifest in __main__.py (lines 603-911): references changed symbols_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 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: no capturable inputs from the test suite; property tier: not verifiable: all 40 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: no capturable inputs from the test suite; property tier: not verifiable: all 40 sampled inputs raised on both versions — the function never executed, so 'no divergence' would be vacuous (mostly ValueError — 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.
Could not verify: Could not verify \_render\_query.
The verifier did not have enough to check \_render\_query, 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: no capturable inputs from the test suite; property tier: not verifiable: all 223 sampled inputs raised on both versions — the function never executed, so 'no divergence' would be vacuous (mostly TypeError — names the real obstacle, not a sampling gap)
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.
· 1 grounded finding(s) anchored inline below; 5 more finding(s) on lines outside this diff (see the check run).
|
|
||
|
|
||
| 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: |
There was a problem hiding this comment.
_subgraph_to_text()
31 callers depend on it (afferent coupling).
Grounded coupling-delta finding (deterministic), not an LLM guess.
There was a problem hiding this comment.
Acknowledged, no change: the 31 callers are pre-existing. This PR only adds an optional keyword argument (include_rationale, default False) to _subgraph_to_text and forwards it, so no caller changes and default output is identical.
Fixes #2821. Supersedes #2830, which has been conflicting with
v8since 2026-08-20; this is that change rebased onto currentv8, with the original commit and author kept.What
graph.jsonstores arationaleon semantic nodes, butquery,get_nodeandexplainnever printed it. This adds it, opt-in:graphify query ... --rationaleandgraphify explain ... --rationaleinclude_rationaleonquery_graphandget_node(strict boolean)queryappendsWHY <text>after the NODE line;explainandget_nodeprintRationale: <text>The text is untrusted corpus/LLM output, so it goes through
sanitize_rationale()ingraphify/security.py:\x80-\x9f), bidi overrides and zero-width characters strippedquery(cap 512), multi-line forexplain/get_node(cap 2048), explicit...marker when cappedDegree:,Node:)Changes on top of #2830
The first commit is #2830 rebased onto
v8(conflicts incli.py,serve.py,tests/test_explain_cli.py,tests/test_serve.py; the new--seed,_resolve_single_nodeandattrssuffix are preserved). The second commit is review hardening:_CONTROL_CHAR_REis left as onv8; the C1 range applies to rationale only, sosanitize_labelbehaviour does not change--rationalelisted in the main help textBefore / after
Fixture: a small graph with rationale on two semantic nodes.
Before (
v8),graphify query "halt narrowing token" --graph g.json:graphify explain "completes-ticket line and Ticket coverage" --graph g.json --rationaleprints no Rationale line (the flag is ignored).After, with
--rationale:Without the flag the output is identical to
v8. A node whose rationale contains ESC, BEL and\x9bbytes, a newline and a ~1200-char tail prints with the control bytes stripped; thequeryWHY is collapsed to one line.Tests
tests/test_security.py,tests/test_query_cli.py,tests/test_explain_cli.py,tests/test_serve.py; the flag/kwarg does not exist onv8, so they fail there.pytest tests/: 8418 passed, 14 skipped, 1 deselected.tests/test_extract.py::test_collect_files_from_dir, is an environment artifact, not related to this diff: it fails only when the checkout sits under a hidden directory (here.worktrees/), becausecollect_filesskips hidden path components. It passes on a cleanv8copy outside a hidden path, and this PR does not touchextract.py.ruff check .clean.pyright graphifyreports 498 errors on bothv8and this branch (no new ones).