Skip to content

feat(path): report path confidence as the weakest hop (#4885) - #4892

Closed
SyedFahad7 wants to merge 3 commits into
Graphify-Labs:v8from
SyedFahad7:cursor/4885-path-confidence-b0a3
Closed

SyedFahad7 wants to merge 3 commits into
Graphify-Labs:v8from
SyedFahad7:cursor/4885-path-confidence-b0a3

Conversation

@SyedFahad7

@SyedFahad7 SyedFahad7 commented Oct 30, 2026 •

Copy link
Copy Markdown
Contributor

What does this PR do?

Fixes #4885.

Stacked on PR A (fix/4881-keep-weaker-evidence, #4881 / also_seen). This branch is cut from that branch and targets v8, so until PR A merges the diff also shows PR A's commit 3304de16. This PR's own change is the single commit 6e6e68a2. Review or merge PR A first.

graphify path and the MCP shortest_path tool label each hop separately. A path with one INFERRED or AMBIGUOUS hop among EXTRACTED hops therefore reads as a list of independent tags, and nothing marks the path as a whole as uncertain. Both now end with one summary line.

Repro from the issue (Report.cs: IReport.Build(), PdfReport/CsvReport implementing it, Exporter.Run() calling _report.Build(); graphify update .):

$ graphify path report_exporter_run report_pdfreport_build
Shortest path (2 hops):
  .Run() --calls [INFERRED]--> .Build() --dispatches_to [INFERRED]--> .Build()
Path confidence: INFERRED (weakest hop 2: dispatches_to, score 0.75)

The MCP shortest_path result ends with the same line.

How the summary is computed (_path_confidence_line in graphify/serve.py, also called by the CLI path in graphify/cli.py):

  • A hop's strength is the weakest evidence it carries. That includes all of its parallel edges and, from PR A, its also_seen entries.
  • Hops are compared by _CONFIDENCE_RANK (graphify/build.py) first, then by the lowest confidence_score. A missing score never counts as weaker than a present one. On a tie, the first such hop is named.
  • , score X is printed only when the weakest evidence has a score.
  • A hop with no recognised confidence tag makes the result Path confidence: unknown (hop N: <relation> has no confidence tag). The other hops cannot vouch for it.
  • The shortest_path tool description and the README sentence about graphify path mention the new line.

Type of change

  • Bug fix
  • New feature
  • Documentation
  • Tests or CI
  • Refactor
  • Security fix

Verification & Invariants

Invariant: a path is never reported as more certain than its least certain hop (CONTRIBUTING, "Keep inference visibly separate from extracted fact"). The existing hop segments are byte-identical. The only output change is one extra line after them, so anything matching Shortest path (N hops): or hop substrings is unaffected. Nothing is persisted, so no graph or cache state changes.

  • Read the CONTRIBUTING.md guide.
  • Reproduced the issue and identified the invariant.
  • Made the smallest fix necessary.
  • Added a regression test (if bug fix) or isolated boundary test.
  • Kept the PR description synchronized with the final implementation.
  • Documented any limitations / unsupported cases explicitly.

Limitations:

How was this tested?

tests/test_path_confidence.py (7 tests) covers:

  • the exact repro line through the CLI and through the MCP _shortest_path_text;
  • tier taking precedence over score;
  • the lowest score breaking a tier tie, and a missing score not counting as weaker;
  • all-EXTRACTED paths naming the first hop;
  • parallel edges and also_seen counting toward their hop;
  • a hop with no tag making the path unknown.

On PR A's commit, without this change, the module fails (_path_confidence_line does not exist), and the repro output has no summary line.

uv run pytest tests/test_path_confidence.py tests/test_edge_also_seen.py tests/test_path_cli.py tests/test_serve.py -q   # 215 passed
uv run pytest tests -q                               # 8983 passed, 17 skipped, 1 xfailed
uv run ruff check .                                  # All checks passed
uv run pyright                                       # 667 errors, identical to the upstream v8 baseline (no new ones)
graphify update . && graphify path report_exporter_run report_pdfreport_build   # output above

Graphify-specific checklist

  • I updated generated skill artifacts (uv run python -m tools.skillgen --bless) when changing their source fragments. (No fragments touched.)
  • I confirmed that AST/structural extraction remains deterministic (no ambient state dependencies like ENV variables).
  • I reviewed changes for security implications (no unsafe interpolation into shell/Python).
  • I confirmed no API keys or local-only graph data are included.
  • (If applicable) I disclosed AI authorship in my commit messages.
Open in Web Open in Cursor 

Stacked on #4891 — review/merge that first; this PR includes its commit.

Update (follow-up commit 659e7db)

cursoragent and others added 2 commits October 30, 2026 20:52
…phify-Labs#4881)

A same-relation collision keeps the higher-confidence edge (Graphify-Labs#3711), and the
other copy was discarded, so an AMBIGUOUS or INFERRED duplicate (typically a
semantic-pass edge over an AST edge) left no trace in graph.json. The winner
now lists each other copy's confidence, confidence_score, source_file and
source_location in `also_seen`, weakest first, deduplicated against the
winner and each other so re-feeding graph.json through an update is
idempotent. A malformed `also_seen` read back from disk is sanitized.

When the incoming edge wins, fields only the loser carried are removed from
the stored edge: add_edge merges attribute dicts, so the displaced edge's
confidence_score used to stay on an EXTRACTED winner that had none.

`graphify path` (CLI and MCP shortest_path) unions the also_seen tags into the
hop label, as it already does for parallel edges: [AMBIGUOUS/INFERRED].

Fixes Graphify-Labs#4881

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-authored-by: Syed Fahad <sfahad2904@gmail.com>
…4885)

`graphify path` and the MCP `shortest_path` tool labelled each hop on its own,
so one INFERRED or AMBIGUOUS hop among EXTRACTED ones read as a list of
independent tags rather than marking the whole path. Both now end with one
summary line:

  Path confidence: INFERRED (weakest hop 2: dispatches_to, score 0.75)

A hop's strength is the weakest evidence it carries, its parallel edges and
also_seen entries included; hops are ranked by _CONFIDENCE_RANK, then by the
lowest confidence_score (a missing score never counts as weaker than a present
one), and ties name the first such hop. A hop with no recognised tag makes the
path "unknown" rather than letting the other hops vouch for it. The helper
lives in serve.py and the CLI calls it, so both entry points stay identical.

Fixes Graphify-Labs#4885

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-authored-by: Syed Fahad <sfahad2904@gmail.com>
@github-actions

Copy link
Copy Markdown

Thanks for the pull request, @SyedFahad7. 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.

@github-actions github-actions Bot added the contributor Pull request from a returning community contributor label Oct 30, 2026

@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.

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 4 changed function(s) (4 not verified).

Formal verification. PR-changed functions: 0/4 verified (0 proven, 0 may-equivalent, 0 distinguished) · 4 not verified (4 vacuous).

Not verified on this run: build\_from\_json (vacuous: never exercised), dispatch\_command (vacuous: never exercised), \_build\_server (vacuous: never exercised), \_shortest\_path\_text (vacuous: never exercised).


Graphify review — findings

Adds an overall confidence line to graphify path and the shortest_path MCP tool, reporting the path's weakest hop (e.g. Path confidence: INFERRED (weakest hop 2: calls, score 0.7)). A hop with no recognised confidence tag makes the whole path report as unknown. When two edges with the same relation between the same nodes collapse, the lower-confidence copy is no longer thrown away: the winner keeps the best one and records the others in a new also_seen list (confidence, score, source file and location, weakest first). Re-feeding a written graph.json through the build is idempotent, and graphify path now shows the tags of every copy on a hop, such as [AMBIGUOUS/INFERRED]. README and docs/how-it-works.md describe the new line and the also_seen edge field.

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

Analysis details — impact, health, verification

Impact & health

Graphify review

Impact — 2342 functions depend on the 427 functions this change touches.

Health — this change adds coupling hotspots:

  • worse: build_from_json() — 283 callers, 21 callees
  • worse: dispatch_command() — 5 callers, 137 callees
  • worse: main() — 117 callers, 3 callees
  • worse: _shortest_path_text() — 17 callers, 5 callees

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

Test selection

Test selection

471 of 471 test file(s) selected (100%) via static blast radius.

Escalated to a full run for safety — the selection is not trustworthy on its own (see below). CI should run the whole suite.

  • tests/pi.test.ts — full-run-safety
  • tests/test_affected_cli.py — impact, full-run-safety
  • tests/test_affected_defines.py — full-run-safety
  • tests/test_affected_member_seed.py — full-run-safety
  • tests/test_affected_overrides.py — full-run-safety
  • tests/test_affected_path_file_node.py — impact, full-run-safety
  • tests/test_agents_platform.py — impact, full-run-safety
  • tests/test_alias_folding.py — impact, full-run-safety
  • tests/test_analyze.py — impact, full-run-safety
  • tests/test_anthropic_custom_endpoint.py — full-run-safety
  • tests/test_antigravity_install.py — full-run-safety
  • tests/test_apm_fallback_version.py — full-run-safety
  • tests/test_architecture_doc.py — full-run-safety
  • tests/test_astro_extraction.py — full-run-safety
  • tests/test_astro_import_ids.py — full-run-safety
  • tests/test_atomic_canvas_export.py — impact, full-run-safety
  • tests/test_atomic_version_stamp.py — full-run-safety
  • tests/test_atomic_writes.py — impact, full-run-safety
  • tests/test_backend_env_isolation.py — full-run-safety
  • tests/test_backend_extras.py — full-run-safety
  • tests/test_backend_opt_in.py — impact, full-run-safety
  • tests/test_benchmark.py — impact, full-run-safety
  • tests/test_benchmark_raw_graph.py — impact, full-run-safety
  • tests/test_benchmark_real_query.py — impact, full-run-safety
  • tests/test_binary_media_words.py — full-run-safety
  • tests/test_blade_extractor.py — full-run-safety
  • tests/test_build.py — impact, full-run-safety
  • tests/test_build_located_semantic_identity.py — impact, full-run-safety
  • tests/test_build_merge_dedup_scope.py — impact, full-run-safety
  • tests/test_build_merge_hyperedges_and_prune.py — impact, full-run-safety
  • tests/test_build_merge_shrink_guard.py — impact, full-run-safety
  • tests/test_builtin_global_type_refs.py — full-run-safety
  • tests/test_c_include_resolution.py — impact, full-run-safety
  • tests/test_cache.py — full-run-safety
  • tests/test_cache_long_paths.py — full-run-safety
  • tests/test_cache_stale_import_target.py — full-run-safety
  • tests/test_callflow_html.py — full-run-safety
  • tests/test_cargo_introspect.py — full-run-safety
  • tests/test_cargo_missing_manifest.py — full-run-safety
  • tests/test_carried_hyperedge_remap.py — impact, full-run-safety
  • tests/test_case_sensitive_resolution.py — full-run-safety
  • tests/test_charmap_encoding.py — impact, full-run-safety
  • tests/test_chunking.py — impact, full-run-safety
  • tests/test_cjs_module_extension.py — full-run-safety
  • tests/test_claude_cli_backend.py — impact, full-run-safety
  • tests/test_claude_md.py — full-run-safety
  • tests/test_cli_broken_pipe.py — full-run-safety
  • tests/test_cli_export.py — impact, full-run-safety
  • tests/test_cli_help.py — full-run-safety
  • tests/test_cloud_cta.py — impact, full-run-safety
  • … and 421 more

non-code file(s) changed (README.md, docs/how-it-works.md) → running the full suite for safety (a code graph can't see config/fixture/data deps)

changed code file(s) with no mapped test (README.md, docs/how-it-works.md) — a coverage gap or a missing link — running the full suite rather than only the selected tests

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.

Formal verification

Could not verify: Could not verify build\_from\_json.

The verifier did not have enough to check build\_from\_json, 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 11 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: 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)

Could not verify: Could not verify \_shortest\_path\_text.

The verifier did not have enough to check \_shortest\_path\_text, 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 35 sampled inputs raised on both versions — the function never executed, so 'no divergence' would be vacuous (mostly KeyError — names the real obstacle, not a sampling gap)

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

Comment thread graphify/serve.py
return f"Path confidence: {conf} (weakest hop {hop}: {rel}{score_str})"


def _shortest_path_text(G: nx.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 — _shortest_path_text()

17 callers depend on it (afferent coupling).

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

…bs#4885)

EXTRACTED is 1.0 by definition, and the hop tags (Graphify-Labs#4882) print it bare, but
the path summary printed "score 1" when an EXTRACTED hop carried a stored
confidence_score. Its score is now ignored both when printing and when
ranking, so an all-EXTRACTED path names its first hop instead of whichever
hop happened to store 1.0. A non-finite score is treated as missing, as in
the hop tags.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-authored-by: Syed Fahad <sfahad2904@gmail.com>
safishamsi added a commit that referenced this pull request Oct 30, 2026
…els, path weakest-hop confidence, Type.Member path endpoints

Covers the SyedFahad7 batch: #4891 and #4889 merged on GitHub; #4892 and
#4893 landed via authorship-preserving cherry-pick (their CI matrix was
gated pending first-contributor approval, so brought in through the local
gate instead). Credit by GitHub PR-author login @SyedFahad7.

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

Copy link
Copy Markdown
Member

Landed in v0.9.98, now on PyPI (pip install graphifyy==0.9.98). Landed via cherry-pick (your CI matrix was gated). path reports the overall confidence as the weakest hop. Thanks for the fix, @SyedFahad7.

@safishamsi safishamsi closed this Oct 30, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

contributor Pull request from a returning community contributor

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[Feature]: path: report path-level confidence (weakest hop) so one uncertain hop marks the whole path

3 participants