Migrate the faked signal, keep the real one: semantic context that still trusts the graph edge
Phase 1b of ADR-020. workitem_context's scorer mixed a real structural signal (a graph edge between two facts) with a faked one (keyword token overlap standing in for 'related'). The fix moves only the faked half to embeddings and keeps the graph edge as a deterministic booster.
A scorer with two very different halves
After graph_query (phase 1), the next keyword-bypass was context_score — the function behind
workitem_context, which builds the cited "here's what's related to this work item" bundle. But
context_score wasn't pure keyword like graph_query_score. It blended:
- structural signals it actually knows: same startup (+20), same domain (+18), confirmed
provenance (+8), and — biggest — a graph edge (+80) when the candidate is literally linked to
the work item (
relates,blocks,derived_from,answered_by, …). - a faked signal: a keyword loop, +6 for every shared ≥3-character token, standing in for "these two facts are about the same thing."
The keyword half has phase 1's blind spot: a genuinely related fact in another language, or worded differently, scores 0. But the structural half is real — a graph edge isn't a guess about relatedness, it's a relationship the system recorded. A blanket "move it all to the brain" would have thrown that away.
Split the signals
So phase 1b doesn't replace context_score — it splits it. A new brain_rank_scored() returns
(index, cosine) pairs (not just an order) so the semantic score can be blended rather than just
sorted. Then:
final = (cosine * 100) // semantic base — replaces the keyword loop
+ (graph edge present ? 80 : 0) // structural — KEPT, deterministic
+ (confirmed provenance ? 8 : 0) // structural — KEPT
+ (same domain ? 10 : 0) // structural — KEPTIf the brain is down, the whole thing falls back to the original context_score (keyword and all).
Edge-connected facts still surface first — because that's a fact the graph holds, not an inference —
but the ordering among everything is now semantic instead of token-overlap.
Verified
cargo check: 0 errors. The /rank response's score field was confirmed numeric this session
(0.53 / 0.33 / 0.088), which is what brain_rank_scored parses. (Not yet built into the .app,
not pushed.)
The lesson
"Move semantics to the brain" is not "delete every heuristic." When a scorer mixes a real deterministic signal (a graph edge, an explicit tag, a provenance flag) with a faked one (keyword overlap pretending to be relatedness), migrate only the faked half and keep the deterministic half as a booster on top. The audit's job is exactly this discrimination — which of the ~26 sites are faking intelligence, and which are honest structure that should stay in Rust.