fix(confidence): stop an unreadable confidence from reading as 0.0

strtod answers 0.0 for text it cannot read, and 0.0 is a real confidence
in both places that call it. So a property blob carrying a malformed
value, such as "confidence":null, came back as a recorded confidence of
zero. Two things went wrong with that.

In src/graph_buffer/graph_buffer.c, edge_props_confidence answers
CBM_EDGE_CONF_ABSENT (-1) when an edge carries no confidence, so any real
confidence outranks it. A malformed value answered 0.0, which beats -1 in
the merge comparison, so the malformed blob displaced a clean stored one.
The function's own comment already promised that "absent/unparseable
reads as -1". Only the absent half was true.

In src/mcp/mcp.c, bfs_edge_evidence_for_hop sets the confidence to -1 and
the emitter publishes any value of 0 or more as a recorded number. A
malformed value printed as 0.00, which reads as "the resolver was certain
this is wrong" rather than "nobody wrote a number here".

Both sites now pass an end pointer to strtod and keep the absent sentinel
when the pointer never moved, which is the shape src/store/store.c:402
already uses.

Two tests come with the change, and both were seen failing before the fix
and passing after:

  gbuf_edge_props_unreadable_confidence_does_not_displace_absent
  tool_trace_path_unreadable_confidence_reports_not_recorded

Red: 258 passed, 2 failed. Green: 260 passed, 0 failed.

The full suite reports 7633 passed, 2 failed. Both failures are in
tests/test_cli.c (lines 1749 and 6725) and reproduce on a clean tree
without this change. They depend on the coding agents installed on the
machine, not on this change. make -f Makefile.cbm lint-ci passes.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Signed-off-by: Joshua Richter <jrichter5781@gmail.com>
This commit is contained in:
Joshua Richter
2026-09-01 19:35:12 -04:00
co-authored by Claude Opus 5
parent 5fbab7bb73
commit 62b44519c6
4 changed files with 115 additions and 2 deletions
+12 -1
View File
@@ -1045,7 +1045,18 @@ static double edge_props_confidence(const char *props_json) {
if (!p) {
return CBM_EDGE_CONF_ABSENT;
}
return strtod(p + sizeof(conf_key) - SKIP_ONE, NULL);
/* strtod answers 0.0 for text it cannot read, and 0.0 is a real
* confidence that beats CBM_EDGE_CONF_ABSENT. So a blob carrying
* "confidence":null used to outrank a clean blob that carries no
* confidence at all, and displace it. Ask strtod where it stopped: an
* end pointer that never moved means it read nothing. */
const char *value = p + sizeof(conf_key) - SKIP_ONE;
char *end = NULL;
double conf = strtod(value, &end);
if (end == value) {
return CBM_EDGE_CONF_ABSENT;
}
return conf;
}
/* Decide whether an incoming property blob replaces the stored one on a