fix(query): score the rationale attribute as its own tier (#2293) - #3288
fix(query): score the rationale attribute as its own tier (#2293)#3288andytsai821201-spec wants to merge 1 commit into
rationale attribute as its own tier (#2293)#3288Conversation
…-Labs#2293) The extraction spec stores the WHY of a concept as a `rationale` attribute on the node — deliberately not as a node of its own. `_score_query` matched a question against `norm_label`, `label_tokens`, `source_file` and the node id only, and `_node_search_text` indexed the same fields, so for a "why does X …" question the node holding the answer was neither a trigram candidate nor a seed unless the asker already knew its label (Graphify-Labs#2293: 0/5 answers reached on a 5.5k-node corpus although every answer node existed and stated the answer in its rationale). Add a rationale tier, following the fix the issue proposes: - `_RATIONALE_MATCH_BONUS = 0.75`: below the label substring tier (1.0), above the source-path tier (0.5). A term found in the rationale adds to the score and to the per-term singleton used for seed seating, but — like the source tier — never to term coverage, so a long rationale adds recall without winning back an exact-label tier it did not earn. - `_node_rationale_text`: one normalizer (diacritics folded, lower-cased), tolerant of the list an extractor may emit; "" when absent. - `_node_search_text` appends the same text as a trailing field, only when present, so the trigram prefilter stays a complete candidate generator and every existing field position (Graphify-Labs#2467) is unchanged. Nodes without a rationale score exactly as before. Design credit: @nuboxworld-byte (issue Graphify-Labs#2293 and Graphify-Labs#2294). Graphify-Labs#2294 implements the same idea but its diff carries the whole repository (+252k lines, 766 files) from a base mismatch and cannot be reviewed or merged as-is; this is a fresh minimal implementation against v8. Tests: rationale-only match ranks; tier order label-substring > rationale > source; no coverage credit; list-valued attribute; search text carries the field (and is unchanged without it); end to end a "why" question phrased from the rationale seats the node as a seed. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
There was a problem hiding this comment.
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. 1 change(s) tested, no difference found (not proven).
Graphify review — findings
Adds a rationale scoring tier so "why" questions match the answer text stored on a node's rationale attribute rather than its label: _node_rationale_text folds diacritics and joins list-valued rationales, and _score_query awards _RATIONALE_MATCH_BONUS (0.75) per matching term — ranked below a label-substring hit, above a source-path hit, and excluded from term coverage so a long rationale boosts recall without reclaiming the exact-label tier. _node_search_text appends the rationale last and only when present, keeping the trigram prefilter a complete candidate generator while leaving every existing field position unchanged.
No blocking issues surfaced.
Analysis details — impact, health, verification
Impact & health
Graphify review
Impact — 582 functions depend on the 340 functions this change touches.
Health — this change adds coupling hotspots:
- new:
dispatch_command()— 2 callers, 123 callees - new:
_query_graph_text()— 21 callers, 9 callees - new:
_score_query()— 15 callers, 6 callees - new:
_query_terms()— 20 callers, 3 callees - new:
run_benchmark()— 16 callers, 3 callees - new:
_build_server()— 2 callers, 16 callees - new:
_load_graph()— 9 callers, 3 callees - new:
_query_subgraph_tokens()— 7 callers, 3 callees - …and 9 more — each is listed as a finding
Verification — 582 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: 407 function(s) in the blast radius were not formally verified this run
Formal verification
No difference found (not proven): No behavior difference found in \_node\_search\_text (not a proof).
The verifier ran both versions of \_node\_search\_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 \_score\_query.
The verifier did not have enough to check \_score\_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: not verifiable: all 45 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)
· 1 grounded finding(s) anchored inline below; 16 more finding(s) on lines outside this diff (see the check run).
| return _strip_diacritics(str(raw)).lower() | ||
|
|
||
|
|
||
| def _node_search_text(data: dict, nid: str) -> str: |
There was a problem hiding this comment.
_node_search_text()
high coupling complexity (Ca·Ce = 15).
Grounded coupling-delta finding (deterministic), not an LLM guess.
Fixes #2293.
Summary
The extraction spec stores the WHY of a concept as a
rationaleattribute on the node, not as a node._score_querynever read it and_node_search_textnever indexed it, so for a "why does X …" question the node that literally states the answer was neither a trigram candidate nor a seed unless the asker already knew its label. #2293 measured 0 / 5 answers reached on a 5.5k-node corpus where all five answer nodes existed.This implements the fix the issue proposes, minimally (+~40 lines of code): a rationale scoring tier and the matching search-text field.
Fix
_RATIONALE_MATCH_BONUS = 0.75— below the label substring tier (1.0), above the source-path tier (0.5). A term found in the rationale adds to the node's score and to the per-term singleton that seats seeds, but — exactly like the source tier — never to term coverage, so a long rationale adds recall without restoring a coverage-scaled exact tier it did not earn._node_rationale_text(data)— one normalizer (diacritics folded, lower-cased); tolerates the list an extractor may emit;""when absent._node_search_text— appends that text as a trailing NUL-separated field, only when present, so the trigram prefilter remains a complete candidate generator for the scorer (the docstring's contract) and every existing field position — including the explain/_find_node cannot resolve node IDs containing punctuation or non-ASCII text (concept:*, merge-graphs ::, Hangul) #2467 folded-id slot — is unchanged.Nodes without a
rationalescore exactly as before. No new flag, no LLM, one extra dict lookup per candidate node.Before / after (the #2293 shape)
Node
FAB visibility rulewith rationale "Hidden when the mini card is dismissed and while the geolocation popover is open, because that popover opens upward into the DirectionsFAB's space."; neighborsDirectionsFAB,GeolocationButton,MiniCard.query "why is the directions button hidden when the geolocation popover opens"Tests (all watched failing first)
agains a sub-unit nudge overb, not ¾ of an exact tier)rationaletolerated_node_search_textcarries the field; without a rationale the 5-field layout is unchangedtests/test_serve.py: 152 passed. Full suite: 5,170 passed; the 15 failures present (test_ollama_retry_cap.pymissingopenai,test_skillgen.pyaudit baselines) reproduce identically on pristinev8in this environment.Relation to existing work
rationaleattribute — so "why" questions cannot reach the node that answers them #2293's "Suggested fix". Their PR fix(query): score therationaleattribute, not just labels (#2293) #2294 implements the same idea, but its diff carries the whole repository (+252,795 / −5,138 across 766 files) from a base mismatch and cannot be reviewed or merged as-is; this is a fresh minimal implementation againstv8. Happy to close this in favour of fix(query): score therationaleattribute, not just labels (#2293) #2294 if it gets rebased.rationaleattribute — so "why" questions cannot reach the node that answers them #2293 itself notes:_pick_seeds' gap cutoff can still drop a rationale-scored node that ranks 2nd–4th. That is a separate, more opinionated change.🤖 Generated with Claude Code