jouleclaw-compose: attribute a claim to its most authoritative citation #344

Merged
dcharlot merged 1 commit from fix/cited-item-authority into main 2026-10-19 22:51:54 -04:00

2026-10-19

Transaction Science
jouleclaw-compose: attribute a claim to its most authoritative citation
All checks were successful
standards workspaces build + test / which workspaces this change touches (pull_request) Successful in 7s
repository checks / lockfiles, tests in CI, workflow hygiene, spec and section references, public text (pull_request) Successful in 59s
standards workspaces build + test / mesh-rs (mesh/mesh-rs, -p mesh-engine-gguf -p mesh-openai --features mesh-engine-gguf/gguf,mesh-openai/server, mesh-rs, linux-arm64) (pull_request) Successful in 1m31s
standards workspaces build + test / jouleclaw-rs (-nodebug, jouleclaw/jouleclaw-rs, -p jouleclaw-energy --features rapl,nvml, jouleclaw-rs, linux-arm64, -C target-feature=+fp16) (pull_request) Successful in 4m11s
jouleclaw public surface must stay documented / Public-surface audit (jouleclaw-rs) (pull_request) Successful in 1m15s
standards workspaces build + test / macOS energy backends (pull_request) Successful in 13m14s
standards workspaces build + test / jouledesk-rs (jouledesk/jouledesk-rs, jouledesk-rs, macos) (pull_request) Successful in 4m23s
`compose_verified_answer` built each segment's `ClaimAttribution` from

    ds.cited_item_ids.iter().filter_map(resolve).next()

under a comment reading "Source URI from the most authoritative cited item".
`.next()` is the first cited id that happens to resolve, in whatever order the
ids were listed — a stated selection criterion the code did not perform.

Authority tier 1 is `AuthorityTier::Primary` and 4 is `Community`, so the most
authoritative cited item is the **minimum** tier. A segment citing a forum post
before a peer-reviewed source was therefore attributed to the forum post, and
the whole attribution rides on that one choice: `retrieval_source_uri`,
`retrieval_timestamp` and `retrieval_axes` all come from it. Selection is now
`min_by_key` on the tier, with ties broken on `item_id` so the result is
reproducible rather than a function of citation order.

A second problem sat in the same expression. `filter_map` drops a cited id that
matches no retrieved item **silently**, so a segment whose citations all dangle
came out with the ids still attached and an all-`None` attribution, with
nothing anywhere stating why. A reader sees citations and no source. Dangling
citations are now counted and reported as a caveat, using the mechanism the
function already uses for dropped segments. This is the honest shape of the
problem the sweep called "citation laundering": the citations are not read back
as provenance, and the gap was invisible.

Three tests, each pinning one property, every one of them discriminating on
*order* rather than on a single arrangement:

- the same two items, tier 1 and tier 4, cited both ways round, must attribute
  to the tier-1 source both times;
- two items of **equal** authority, cited both ways round, must resolve the
  same way — the tie-break is the only thing that makes this true, and it is
  the only test that catches its removal;
- a segment with one real and one dangling citation reports the dangling one
  and still attributes from the real one; a segment where every citation
  dangles has no attribution at all, which is when saying so matters most.

The sweep's own description of this finding was wrong in its specifics —
`cited_item_ids` is read in three places, and there is no `jouleclaw-diagnose`
crate — but reading the code to check surfaced the real defect beside it.

Co-Authored-By: Claude Opus 5 <[email protected]>
2026-10-19 20:41:38 -04:00