feat(jev): advisory question families and graceful degradation #57

Merged
yuris merged 1 commit from feat/issue-21-advisory into main 2026-09-22 23:21:19 +00:00
Owner

Motivation

life keeps Jev strictly advisory: calendar and task commands must never depend on a semantic model, and a model answer must never authorize anything. #19 delivered the typed HTTP adapter — wire validation and the pinned-model requirement — but nothing that asked a question. This change adds the code-owned, versioned question families that the next-task policy (#22–#24), review (#30) and memory-offer paths will consume, together with the graceful degradation that keeps every deterministic path working when Jev is disabled, unreachable, slow, rate limited, or answering wrongly.

Changes

Five families, each code-owned and versioned independently of the model version, all dispatching through the #19 adapter so its strict wire validation and pinned-model requirement are inherited rather than re-implemented:

  • src/jev/questions/alignment.rs — alignment_v1, score over 5 ordered criteria; the rubric forbids inventing a fact the caller supplied as unknown (missing blockers or energy requirements stay unknown).
  • src/jev/questions/consequence.rs — consequence_v1, score over 5 levels of cost of delay.
  • src/jev/questions/readiness.rs — readiness_v1, score over 4 levels; a prerequisite the state does not state stays unknown and is never scored as ready.
  • src/jev/questions/review_flag.rs — review_flag_v1, noul; the counts and their coverage come from the deterministic side, and the answer is an interpretation to offer for discussion, never a diagnosis.
  • src/jev/questions/meaningful_memory.rs — meaningful_memory_v1, noul, matching the documented wire example exactly; the answer only ever offers retention.

Supporting changes:

  • src/jev/advisory.rs — JevService is the single dispatch point, and JevDegradation the explicit classification. Disabled ([jev].enabled = false, or no [jev] section) is a named capability that dispatches nothing and reports judgment_unavailable with a configuration reason — never an outage, never an empty success. Transport, timeout, credential, rate-limit and unusable-configuration failures are judgment_unavailable; a wrong model, drifted probabilities, a mismatched legend, a different question id or a malformed primitive is judgment_invalid. Every dispatch is bounded by the service budget and cancellable, and no judgment path writes anything: no journal row, no calendar request, and no confidence threshold anywhere in the layer.
  • candidate_advice() returns the three per-candidate scores alongside the degradations that replaced any of them; is_semantic() is false unless all three answered, so a partially judged candidate can never be presented as a semantic ranking. JevDegradation::warning() produces the envelope warning a caller attaches.
  • Internal API for #22–#24 and #30: life::jev::advisory::{JevService, JevDegradation, CandidateAdvice} and life::jev::questions::{alignment, consequence, readiness, review_flag, meaningful_memory}; ScoreAnswer/NouLAnswer are re-exported from life::jev. No route, no candidate filtering/ranking, no life next, no life resolve, no planning, no new dependency.

Out of scope for this change (as agreed): reference_match.v1 and the resolve route belong to #20; interruption_impact.v1 and capture_class.v1 are not part of this issue's scope boundary.

Verification

  • nix develop --command cargo test — all suites green (393 tests, including the 19 new ones in tests/jev_advisory_tests.rs); cargo clippy --all-targets -- -D warnings and cargo fmt --check clean.
  • Each family is asserted at the wire against a real in-process daemon and a mock System One over TCP: question id, primitive shape (ordered criteria for the scores, true/false criteria for the nouL), the static instructions, one family per dispatch, and the caller's state sent verbatim as data.
  • Refused answers: wrong model, drifted score, probabilities that do not sum to 1, mismatched legend, unexpected question id, and a noul carrying a confidence field are each judgment_invalid.
  • No judgment obtained: disabled (zero dispatches), unreachable, HTTP 500/503, exhausted 429 retries (bounded at three attempts) and a slow provider cut off by the service budget are each judgment_unavailable; a cancelled call leaves the backend immediately usable.
  • Determinism: a daemon with [jev] enabled against a dead port produces a calendar write identical — modulo the generated UID and creation timestamps — to one with no [jev] section, carrying no warning; GET /v1/tasks, GET /v1/agenda and POST /v1/tasks make zero Jev calls.
  • Non-authority: the strongest possible answers (score 4.0, noul 0.99) leave operations, life_events, item_projection and outbox empty and issue no calendar request; the same counters move on an explicit deterministic write, so the zero is not vacuous.
  • Privacy: with a private marker inside state, the captured log across the accepted, unavailable, invalid and disabled paths is non-empty, contains the request fingerprint and both failure codes, and never contains the marker — while the mock confirms the state really was sent.

Closes #21

## Motivation `life` keeps Jev strictly advisory: calendar and task commands must never depend on a semantic model, and a model answer must never authorize anything. #19 delivered the typed HTTP adapter — wire validation and the pinned-model requirement — but nothing that asked a question. This change adds the code-owned, versioned question families that the next-task policy (#22–#24), review (#30) and memory-offer paths will consume, together with the graceful degradation that keeps every deterministic path working when Jev is disabled, unreachable, slow, rate limited, or answering wrongly. ## Changes Five families, each code-owned and versioned independently of the model version, all dispatching through the #19 adapter so its strict wire validation and pinned-model requirement are inherited rather than re-implemented: - `src/jev/questions/alignment.rs` — `alignment_v1`, `score` over 5 ordered criteria; the rubric forbids inventing a fact the caller supplied as unknown (missing blockers or energy requirements stay unknown). - `src/jev/questions/consequence.rs` — `consequence_v1`, `score` over 5 levels of cost of delay. - `src/jev/questions/readiness.rs` — `readiness_v1`, `score` over 4 levels; a prerequisite the state does not state stays unknown and is never scored as ready. - `src/jev/questions/review_flag.rs` — `review_flag_v1`, `noul`; the counts and their coverage come from the deterministic side, and the answer is an interpretation to offer for discussion, never a diagnosis. - `src/jev/questions/meaningful_memory.rs` — `meaningful_memory_v1`, `noul`, matching the documented wire example exactly; the answer only ever *offers* retention. Supporting changes: - `src/jev/advisory.rs` — `JevService` is the single dispatch point, and `JevDegradation` the explicit classification. Disabled (`[jev].enabled = false`, or no `[jev]` section) is a named capability that dispatches nothing and reports `judgment_unavailable` with a configuration reason — never an outage, never an empty success. Transport, timeout, credential, rate-limit and unusable-configuration failures are `judgment_unavailable`; a wrong model, drifted probabilities, a mismatched legend, a different question id or a malformed primitive is `judgment_invalid`. Every dispatch is bounded by the service budget and cancellable, and no judgment path writes anything: no journal row, no calendar request, and no confidence threshold anywhere in the layer. - `candidate_advice()` returns the three per-candidate scores alongside the degradations that replaced any of them; `is_semantic()` is false unless all three answered, so a partially judged candidate can never be presented as a semantic ranking. `JevDegradation::warning()` produces the envelope warning a caller attaches. - Internal API for #22–#24 and #30: `life::jev::advisory::{JevService, JevDegradation, CandidateAdvice}` and `life::jev::questions::{alignment, consequence, readiness, review_flag, meaningful_memory}`; `ScoreAnswer`/`NouLAnswer` are re-exported from `life::jev`. No route, no candidate filtering/ranking, no `life next`, no `life resolve`, no planning, no new dependency. Out of scope for this change (as agreed): `reference_match.v1` and the resolve route belong to #20; `interruption_impact.v1` and `capture_class.v1` are not part of this issue's scope boundary. ## Verification - `nix develop --command cargo test` — all suites green (393 tests, including the 19 new ones in `tests/jev_advisory_tests.rs`); `cargo clippy --all-targets -- -D warnings` and `cargo fmt --check` clean. - Each family is asserted at the wire against a real in-process daemon and a mock System One over TCP: question id, primitive shape (ordered criteria for the scores, true/false criteria for the nouL), the static instructions, one family per dispatch, and the caller's state sent verbatim as data. - Refused answers: wrong model, drifted score, probabilities that do not sum to 1, mismatched legend, unexpected question id, and a `noul` carrying a `confidence` field are each `judgment_invalid`. - No judgment obtained: disabled (zero dispatches), unreachable, HTTP 500/503, exhausted 429 retries (bounded at three attempts) and a slow provider cut off by the service budget are each `judgment_unavailable`; a cancelled call leaves the backend immediately usable. - Determinism: a daemon with `[jev]` enabled against a dead port produces a calendar write identical — modulo the generated `UID` and creation timestamps — to one with no `[jev]` section, carrying no warning; `GET /v1/tasks`, `GET /v1/agenda` and `POST /v1/tasks` make zero Jev calls. - Non-authority: the strongest possible answers (score 4.0, `noul` 0.99) leave `operations`, `life_events`, `item_projection` and `outbox` empty and issue no calendar request; the same counters move on an explicit deterministic write, so the zero is not vacuous. - Privacy: with a private marker inside `state`, the captured log across the accepted, unavailable, invalid and disabled paths is non-empty, contains the request fingerprint and both failure codes, and never contains the marker — while the mock confirms the state really was sent. ## Related Issues Closes #21
Five code-owned, independently versioned question families — alignment.v1,
consequence.v1, readiness.v1 (Score) and review_flag.v1, meaningful_memory.v1
(nouL) — dispatching through the #19 adapter so every answer inherits its
strict wire validation and pinned-model requirement rather than
re-implementing either. JevService is the single dispatch point: it short
circuits a disabled or unconfigured section without contacting anything, and
reports every failure as an explicit JevDegradation — judgment_unavailable
when no judgment was obtained (disabled, unreachable, timed out, rate
limited, unusable configuration), judgment_invalid when one arrived and was
refused. Each dispatch is bounded by the service budget and cancellable; a
partially judged candidate is never a semantic ranking. No score, however
high, writes anything: no journal row, no calendar request, no threshold.
Author
Owner

Coordinator review — APPROVED

Verified 928f581 myself (read the module and the rubrics, ran the suite on a tree based on current main).

What I checked beyond the summary:

  • alignment.v1 forbids the exact thing an advisory answer must never do. Its INSTRUCTIONS state that a fact marked unknown stays unknown and that no blocker, dependency, deadline, estimate or energy requirement may be invented or reported as satisfied, and it treats the state as data. That is the rule docs/technical/jev.md sets, enforced at the rubric rather than in a comment.
  • No threshold grants authority. The module doc says the confidence on the wire is validated because the provider sends it, and nothing in the layer turns any value into authority; nested CandidateAdvice fields are Option<ScoreAnswer> with is_semantic() requiring all three scores — a conservative choice I agree with, since two of three is not a ranking.
  • The zero-is-not-vacuous test. judgment_answers_are_never_turned_into_rows_or_calendar_requests proves strongest-possible answers produce zero CalDAV calls and zero operations/life_events/item_projection rows, then issues an explicit POST /v1/tasks to show the same counters do move. That is the right way to assert an absence.
  • Outage independence is compared, not assumed. a_jev_outage_never_changes_a_calendar_result runs a Jev-enabled daemon against a dead port and a Jev-disabled daemon and asserts identical items and stable ICS bodies (modulo UID/DTSTAMP) — a real differential, not a single-path smoke test.
  • Bounded and cancellable: one shared service budget with a slow-provider test returning in <1s under a 150 ms budget, plus a cancelled judgment that does not wedge the backend, and zero calls asserted for commands that need no judgment.
  • Redaction across all four terminal paths (accepted / 500 / wrong-model / disabled) with a non-empty-log guard and a mock-side assertion that the state really was sent.

Independent verification on 928f581: cargo test 393 passed / 0 failed (19 new advisory tests); cargo clippy --all-targets -- -D warnings 0 errors; cargo fmt --check clean. Diff is 9 files, additive, and touches no route — #22–#24 own the filtering, ordering and life next. Cargo.toml untouched, so the deferred authors chore stays pending.

Merging.

## Coordinator review — APPROVED Verified `928f581` myself (read the module and the rubrics, ran the suite on a tree based on current `main`). What I checked beyond the summary: - **`alignment.v1` forbids the exact thing an advisory answer must never do.** Its `INSTRUCTIONS` state that a fact marked unknown stays unknown and that no blocker, dependency, deadline, estimate or energy requirement may be invented or reported as satisfied, and it treats the state as data. That is the rule `docs/technical/jev.md` sets, enforced at the rubric rather than in a comment. - **No threshold grants authority.** The module doc says the confidence on the wire is validated because the provider sends it, and nothing in the layer turns any value into authority; nested `CandidateAdvice` fields are `Option<ScoreAnswer>` with `is_semantic()` requiring *all three* scores — a conservative choice I agree with, since two of three is not a ranking. - **The zero-is-not-vacuous test.** `judgment_answers_are_never_turned_into_rows_or_calendar_requests` proves strongest-possible answers produce zero CalDAV calls and zero `operations`/`life_events`/`item_projection` rows, then issues an explicit `POST /v1/tasks` to show the same counters *do* move. That is the right way to assert an absence. - **Outage independence is compared, not assumed.** `a_jev_outage_never_changes_a_calendar_result` runs a Jev-enabled daemon against a dead port and a Jev-disabled daemon and asserts identical items and stable ICS bodies (modulo UID/DTSTAMP) — a real differential, not a single-path smoke test. - **Bounded and cancellable:** one shared service budget with a slow-provider test returning in <1s under a 150 ms budget, plus a cancelled judgment that does not wedge the backend, and zero calls asserted for commands that need no judgment. - **Redaction across all four terminal paths** (accepted / 500 / wrong-model / disabled) with a non-empty-log guard and a mock-side assertion that the state really was sent. Independent verification on `928f581`: `cargo test` **393 passed / 0 failed** (19 new advisory tests); `cargo clippy --all-targets -- -D warnings` 0 errors; `cargo fmt --check` clean. Diff is 9 files, additive, and touches no route — #22–#24 own the filtering, ordering and `life next`. `Cargo.toml` untouched, so the deferred authors chore stays pending. Merging.
yuris merged commit d0713c0360 into main 2026-09-22 23:21:19 +00:00
yuris changed title from feat(jev): advisory question families and graceful degradation (#21) to feat(jev): advisory question families and graceful degradation 2026-09-23 05:12:05 +00:00
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
yuris/life-service!57
No description provided.