Skip to content

fix(build): keep a collapsed edge's weaker evidence in also_seen (#4881) - #4891

Merged
safishamsi merged 1 commit into
Graphify-Labs:v8from
SyedFahad7:fix/4881-keep-weaker-evidence
Oct 30, 2026
Merged

safishamsi merged 1 commit into
Graphify-Labs:v8from
SyedFahad7:fix/4881-keep-weaker-evidence

Conversation

@SyedFahad7

Copy link
Copy Markdown
Contributor

What does this PR do?

Fixes #4881.

When two edges with the same relation land on the same node pair, build_from_json keeps the higher-confidence one (#3711, graphify/build.py at the _CONFIDENCE_RANK comparison) and throws the other away. The usual case is a semantic-pass edge duplicating an AST edge. An AMBIGUOUS or INFERRED duplicate therefore left no trace in graph.json, even though path already shows tag unions such as [EXTRACTED/INFERRED] for parallel edges.

This change keeps the winner exactly as before and adds the loser's provenance to it:

  • also_seen: the surviving edge lists every other copy's confidence, confidence_score, source_file and source_location, weakest first. An entry that only repeats the winner's own provenance, or another entry, is dropped. That keeps a rebuild that re-feeds graph.json (the update path) idempotent. A malformed also_seen read back from disk is reduced to its valid scalar fields.
  • No leaked fields from the loser: G.add_edge(**attrs) merges attribute dicts. When a stronger edge without a score replaced a scored INFERRED one, the loser's confidence_score stayed on the EXTRACTED winner. Provenance fields that only the displaced edge carried are now removed from the stored edge, and that edge's own values go into also_seen instead.
  • The weakest tag is shown: graphify path and the MCP shortest_path tool include also_seen tags in the hop label, as they already do for parallel edges. For example: --dispatches_to [AMBIGUOUS/INFERRED]-->. This is a one-line change at each of the two hop-formatting sites.
  • docs/how-it-works.md documents the new optional edge field.

Repro from the issue: build Report.cs (IReport.Build(), PdfReport/CsvReport implementing it, Exporter.Run() calling _report.Build()) together with a weaker same-relation duplicate of the report_ireport_build -> report_pdfreport_build dispatches_to edge, for example AMBIGUOUS 0.4 from a notes file. Before: the stored edge is INFERRED 0.75 L2 and the AMBIGUOUS copy is gone. After: the same winner, plus also_seen: [{confidence: AMBIGUOUS, confidence_score: 0.4, source_file: notes.md, source_location: L12}], in either arrival order.

#4885's path-confidence summary is stacked on this branch as PR B (branch cursor/4885-path-confidence-b0a3).

Overlap with #4882 / PR #4888: that PR rewrites the same confs = sorted(...) hop-tag line in serve.py and cli.py. Whichever lands second needs a one-line merge per file: sorted({_confidence_tag(e) for d in datas for e in edge_evidence(d) if e.get("confidence")}).

Type of change

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

Verification & Invariants

Invariant: collapsing same-relation duplicates must not erase provenance (CONTRIBUTING, "Preserve provenance" and "Keep inference visibly separate from extracted fact"). The winner's relation, confidence, confidence_score, source_file and source_location are decided exactly as before. Only the loser's record is added. A field the winner does not carry no longer survives from the loser.

Persisted state: graph.json edges may gain an optional also_seen list. Old graphs load unchanged. A rebuild over a graph that already has also_seen does not duplicate entries (covered by a test). Neo4j/FalkorDB pushes already skip non-scalar properties, GraphML JSON-encodes them, and Cypher export only reads confidence. Extraction output is unchanged, so no AST cache bump is needed.

  • 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_edge_also_seen.py (10 tests) covers:

  • the weaker duplicate recorded in both arrival orders;
  • a displaced winner's score not leaking onto the new winner;
  • an equal-confidence second call site being recorded;
  • an identical duplicate adding nothing;
  • idempotent rebuild from written graph.json;
  • malformed on-disk also_seen being sanitized;
  • CLI and MCP path showing [AMBIGUOUS/INFERRED];
  • an end-to-end run of the real C# extractor on the issue's Report.cs.

With the source change stashed, 9 of the 10 fail; the identical-duplicate case is a boundary test.

uv sync --all-extras
uv run pytest tests/test_edge_also_seen.py -q        # 10 passed; 9 failed with graphify/ stashed
uv run pytest tests -q                               # 8976 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   # on the Report.cs repro

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 

…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>
@github-actions github-actions Bot added the contributor Pull request from a returning community contributor label Oct 30, 2026
@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.

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

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

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


Graphify review — findings

Adds an optional also_seen list to edges, so when two edges with the same relation between the same nodes collapse in build_from_json, the winner keeps the loser's confidence, score, file and line instead of discarding them. _fold_also_seen dedupes against the winner's own provenance and clears fields the loser alone carried, which keeps re-feeding a written graph.json idempotent. graphify path in the CLI and _shortest_path_text in the server now union the tags from every also_seen copy into the hop label (e.g. [AMBIGUOUS/INFERRED]), and those code paths read also_seen values straight from graph.json, so what they accept as a confidence string matters.

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

Analysis details — impact, health, verification

Impact & health

Graphify review

Impact — 2299 functions depend on the 384 functions this change touches.

Health — this change adds coupling hotspots:

  • worse: build_from_json() — 281 callers, 21 callees
  • worse: dispatch_command() — 5 callers, 136 callees
  • worse: main() — 116 callers, 3 callees
  • worse: _shortest_path_text() — 15 callers, 4 callees

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

Test selection

Test selection

470 of 470 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 420 more

non-code file(s) changed (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 (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 \_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)

· 4 more finding(s) on lines outside this diff (see the check run).

@safishamsi
safishamsi merged commit 3801e52 into Graphify-Labs:v8 Oct 30, 2026
12 checks passed
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 added a commit that referenced this pull request Oct 30, 2026
…4888)

#4888 was built on 0.9.97, before #4881 (path weakest-hop refactor),
#4889 (qualified C# method labels) and #4891 (also_seen) landed. Its
cherry-pick dropped the merge commit that reconciled expectations. Update
the per-hop confidence assertions to carry scores ([INFERRED 0.85]) and
the C# path/explain/neighbor labels to their owner-qualified form
(Exporter.Run, IReport.Build) - the correct composed output of all four
features. Resolved the serve.py conflict by keeping #4881's _path_weakest/
_path_confidence_line and adding only #4888's _confidence_tag, applied over
edge_evidence so parallel/also_seen evidence keeps its scores.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
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]: don't erase weaker-confidence evidence when edges or paths merge

3 participants