jouleclaw: an answer carries its verifier's backing, so a cached answer is not refused as ungrounded #276

Merged
dcharlot merged 1 commit from fix/answer-carries-entailment-backing into main 2026-10-16 09:41:29 -04:00
Owner

What was wrong

An answer's Provenance stored the entailment results the verifier consulted, but not the backing the verifier declared for them (VerificationReport::grounding_backing).

jouleclaw-edge-cli rebuilds a VerificationReport from a cached answer. Without a stored backing, the rebuild could only declare the floor, Backing::Unstated. As a result, require_substantive_grounding refused every cached answer as ungrounded, including answers a real NLI model had grounded. The rebuild's own comment named the fix: persist the backing beside the entailments.

The fix

  • Provenance.entailment_backing: Option<Backing>. It is omitted from the wire when absent, so answers and caches written before it keep their bytes and deserialize as before. Absent reads as unstated.
  • compose_verified_answer copies the report's backing into the answer.
  • The cache rebuild restores that backing. It never derives one from the cached model_ids, because that would let a stand-in pass as a real model. require_substantive_grounding still checks the backing against the cached results:
    • a fixture stays a fixture;
    • a backing that names a different model from the one whose results are attached is refused.

AnswerOrRefusal gains allow(clippy::large_enum_variant), with the reason in a comment. The new field pushed the Answer variant past clippy's size threshold. There is one value per query, and boxing it would change a public enum for no measurable gain.

Checked

Tests run on the build host:

  • jouleclaw-schema, jouleclaw-compose and jouleclaw-edge-cli pass, including the new tests:
    • the_answer_carries_the_verifiers_backing
    • a_cached_answer_keeps_the_backing_its_verifier_declared
    • a_cached_backing_is_never_promoted_or_trusted_past_its_results
    • the answer round trip, which checks the field is absent from the wire when None and round-trips when Some
  • Clippy reports nothing new on lines this change touches.

Each of these mutations fails a test:

  • the composer dropping the backing;
  • the rebuild going back to the floor;
  • the rebuild deriving SingleModel from the cached model_id, which is the stand-in-as-model path.

🤖 Generated with Claude Code

## What was wrong An answer's `Provenance` stored the entailment results the verifier consulted, but not the backing the verifier declared for them (`VerificationReport::grounding_backing`). jouleclaw-edge-cli rebuilds a `VerificationReport` from a cached answer. Without a stored backing, the rebuild could only declare the floor, `Backing::Unstated`. As a result, `require_substantive_grounding` refused every cached answer as ungrounded, including answers a real NLI model had grounded. The rebuild's own comment named the fix: persist the backing beside the entailments. ## The fix - **`Provenance.entailment_backing: Option<Backing>`.** It is omitted from the wire when absent, so answers and caches written before it keep their bytes and deserialize as before. Absent reads as unstated. - **`compose_verified_answer`** copies the report's backing into the answer. - **The cache rebuild** restores that backing. It never derives one from the cached `model_id`s, because that would let a stand-in pass as a real model. `require_substantive_grounding` still checks the backing against the cached results: - a fixture stays a fixture; - a backing that names a different model from the one whose results are attached is refused. `AnswerOrRefusal` gains `allow(clippy::large_enum_variant)`, with the reason in a comment. The new field pushed the `Answer` variant past clippy's size threshold. There is one value per query, and boxing it would change a public enum for no measurable gain. ## Checked Tests run on the build host: - `jouleclaw-schema`, `jouleclaw-compose` and `jouleclaw-edge-cli` pass, including the new tests: - `the_answer_carries_the_verifiers_backing` - `a_cached_answer_keeps_the_backing_its_verifier_declared` - `a_cached_backing_is_never_promoted_or_trusted_past_its_results` - the answer round trip, which checks the field is absent from the wire when `None` and round-trips when `Some` - Clippy reports nothing new on lines this change touches. Each of these mutations fails a test: - the composer dropping the backing; - the rebuild going back to the floor; - the rebuild deriving `SingleModel` from the cached `model_id`, which is the stand-in-as-model path. 🤖 Generated with [Claude Code](https://claude.com/claude-code)
jouleclaw: an answer carries its verifier's backing, so a cached answer is not refused as ungrounded
All checks were successful
jouleclaw public surface must stay documented / Public-surface audit (jouleclaw-rs) (pull_request) Successful in 1m6s
repository checks / lockfiles, tests in CI, workflow hygiene, spec and section references, public text (pull_request) Successful in 52s
standards workspaces build + test / which workspaces this change touches (pull_request) Successful in 4s
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 1m7s
standards workspaces build + test / macOS energy backends (pull_request) Successful in 6m2s
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 3m35s
standards workspaces build + test / jouledesk-rs (jouledesk/jouledesk-rs, jouledesk-rs, macos) (pull_request) Successful in 1m9s
07756a6730
An answer's `Provenance` stored the entailment results the verifier consulted,
but not the backing the verifier declared for them
(`VerificationReport::grounding_backing`). So jouleclaw-edge-cli's cache
rebuild could only declare the floor, `Backing::Unstated`, and
`require_substantive_grounding` refused every cached answer, including those
a single real NLI model had grounded. The rebuild's own comment named the
fix: persist the backing beside the entailments.

- `Provenance.entailment_backing: Option<Backing>`. It is omitted from the
  wire when absent, so answers and caches written before it keep their bytes
  and deserialize as before; absent reads as unstated.
- `compose_verified_answer` carries the report's backing into the answer.
- The cache rebuild restores that backing and never derives one from the
  cached `model_id`s. `require_substantive_grounding` still cross-checks it
  against the cached results: a fixture stays a fixture, and a backing that
  names a model other than the one whose results are attached is refused.

Tests:
- `the_answer_carries_the_verifiers_backing`: compose carries any backing,
  the floor included, unchanged.
- `a_cached_answer_keeps_the_backing_its_verifier_declared`: a single model's
  labels are grounded after the round trip.
- `a_cached_backing_is_never_promoted_or_trusted_past_its_results`: fixture
  stays refused, a mismatched model is refused, and an old entry is unstated.
- The answer round trip: the field is absent from the wire when None and
  round-trips when Some.

`AnswerOrRefusal` gains `allow(clippy::large_enum_variant)` with its reason.
There is one per query, and boxing would change a public enum for no
measurable gain.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
Sign in to join this conversation.
No reviewers
No labels
No milestone
No project
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set

Reference
Transaction-Science/open-standards!276
No description provided.