feat(jev): candidate reference resolution with deterministic first #58
Loading…
Add table
Add a link
Reference in a new issue
No description provided.
Delete branch "feat/issue-20-resolve"
Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?
Motivation
Users and agents refer to tasks and events informally ("finish portfolio outline", "weekly sync"), but every safe operation needs an authoritative
itm_...id and its current ETag. Issue #20 asks for that mapping to be deterministic first, to consult a semantic judgment only when the reference is genuinely ambiguous, to disclose when the candidate pool was narrowed, and to never let a model's confidence authorize anything.The route is read scope, so it is also the place where the architectural boundary is easiest to state: resolution returns an identity, and nothing it returns can write.
Changes
src/domain/resolve.rs(new): the resolution policy as pure functions over a live candidate snapshot.exact_matchresolves an item id, UID, or case-folded equal title only when exactly one live candidate matches (a shared title is ambiguous, not deterministic).select_candidatesbuilds a bounded, deterministically ordered heuristic pool and reportsnarrowed(). Prompts are fixed text; the caller's reference is never echoed.src/jev/questions/reference_match.rs(new): the code-ownedreference_match_v1Choice question — options are exactly the offered live item ids plus the fixedno_match, rubrics are fixed strings, and the candidate titles, statuses and the user's phrase travel only inside the untrustedstateobject.evaluate_reference_matchreads a validated answer: a decisive selection yields that live id,no_matchis abstention, and a tie at the maximum is ambiguous. Theconfidencefield is deliberately not consulted.src/api/resolve.rs(new):POST /v1/resolve. Reads the live inventory of the requested kind (open tasks; events that are not cancelled) across the selected collections, then: exact match →method: "deterministic"with zero Jev calls; otherwise the bounded pool goes toreference_match_v1through the #19 adapter, whosevalidated_answersenforces exact question id, candidate membership and probability rules. Degradations arejudgment_unavailable(disabled/unreachable) orjudgment_invalid(refused payload), both HTTP 503 / exit 5 with the candidates still carried; the deterministic path keeps working through either.src/api/mod.rs,src/domain/mod.rs,src/jev/mod.rs: registration, plus the route and handler.src/cli.rs,src/client.rs:life resolve --text TEXT --kind task|event [--collection ID]... [--json], the documented--input -stdin alternative for sensitive text, a read-scope POST transport (no operation key), andjudgment_unavailable/judgment_invalidmapped to exit 5.src/api/agenda.rs:select_collectionswidened topub(crate)so resolve reuses the same collection validation instead of a second convention.Notes on two judgement calls:
no_match, because the wire contract caps a Choice question at 20 options in total (docs/technical/jev.mdallows 20 candidates plusno_match;MAX_CHOICE_CANDIDATEScounts options). Anything the cap excludes is disclosed ascandidate_narrowed, naming how many matched.needs_clarification(HTTP 200 / exit 3) or the degraded error, always with the candidate choices and never with an invented id. No confidence threshold is applied anywhere, andlife resolvehas no write path at all: it issues live REPORT reads only.Verification
nix develop --command cargo test— 430 passed, 0 failed (all suites; 23 new integration tests plus 14 new unit tests).cargo clippy --all-targets -- -D warningsandcargo fmt --checkare clean undernix develop.tests/resolve_tests.rsdrives the daemon over real HTTP against a mock Radicale and a mock System One, and drives the real CLI binary against a daemon serving the daemon's own router:method: "deterministic"and asserts zero captured Jev requests; a title shared by two live items is not deterministic and does ask.no_matchwith fixed rubrics, the candidate titles appear only instate, and the resolved id and live ETag are the selected candidate's.judgment_invalidwith no id invented and the candidates still carried.judgment_unavailable; the exact-match request in the same test still resolves deterministically.no_matchanswer returnneeds_clarificationwith the canonical questions/candidates payload andjudgment_provenance; an unmatched reference returns an empty candidate list and is asserted to make zero Jev calls (never widened).candidate_narrowednaming the count, a 20-option offer, and a resolved id that is one of the offered options.incomplete_calendar; all collections unreadable isdependency_unavailable, never an empty calendar. A cancelled event is not a candidate.{text,kind}from stdin (asserted to arrive as untrusted state) instead of argv.Documentation
No doc changes:
docs/technical/cli-api.md(POST /v1/resolve, read scope,Resolvepayload row,--input -,clarification_required,candidate_narrowed,judgment_unavailable/judgment_invalid) anddocs/technical/jev.md(#versioned-questions, thereference_match.v1row, the deterministic-boundary rule) already specify this behaviour.Related Issues
Closes #20
Coordinator review — APPROVED
Verified
c361a98on a tree based on currentmain(d0713c0, so #21's five families are present andsrc/jev/questions/mod.rscarries both sides).What I checked beyond the summary:
an_exact_title_uid_or_item_id_resolves_deterministically_with_zero_jev_callsasserts the mock captured zero provider requests across four exact references — the only form of evidence that actually distinguishes "did not need the model" from "asked and happened to agree". And a title shared by two live items correctly stops being deterministic and does ask.put|post_conditional|prepare_operation|commit_operationinsrc/api/resolve.rsare the substring insideinvalid_input. There is no journal or calendar-write path to reach, which is stronger than a test asserting none was taken.MAX_CANDIDATE_POOL = MAX_CHOICE_CANDIDATES.saturating_sub(1)with the fixedno_matchtaking the last slot, and the adapter independently rejects >20 options (TooManyCandidates).MAX_CHOICE_CANDIDATESis documented insrc/jev/primitives.rsas Life's cap ("far below the provider's 255-option maximum"), so treating it as the whole offered budget is the stricter reading and is coherent. Exclusions are disclosed ascandidate_narrowedrather than passed off as exhaustive.needs_clarification(200 / exit 3) with the candidate choices; a tie at the maximum is treated as ambiguous;no_matchyields an empty candidate list with a rephrase prompt, no provenance and zero Jev calls rather than a widened search.judgment_unavailable, refused payload →judgment_invalid, a moving alias is never believed, and the exact-match path keeps resolving with zero Jev calls in the same test.Independent verification on
c361a98:cargo test430 passed / 0 failed (37 new: 23 integration + 14 unit);cargo clippy --all-targets -- -D warnings0 errors;cargo fmt --checkclean. The #19 adapter (src/jev/{client,primitives}.rs) is untouched, and the diff adds no dependency.Merging. With this, Phase 3's judgment half is complete; #17 is the last in-flight item.
feat(jev): candidate reference resolution with deterministic first (#20)to feat(jev): candidate reference resolution with deterministic first