feat(api): next action command with advisory provenance #66
Loading…
Add table
Add a link
Reference in a new issue
No description provided.
Delete branch "feat/issue-24-next"
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
life nextis the primary entry point for a user deciding what to work on right now, and it is the surface where three easy mistakes become user-visible lies: presenting a model's opinion as a fact, dropping an attribute the item never stated, and letting a recommendation look like an action. #22 (policy filter) and #23 (lexicographic ranker) already own the deterministic constraints and the order; this change adds the missing user-facing surface —POST /v1/nextandlife next— and nothing else, so those invariants are published rather than re-implemented.The documented contract (
docs/technical/cli-api.md#command-to-route-table,#command-payloads-and-results;docs/technical/jev.md#next-task-policysteps 1–4;docs/technical/workflows.md; scenario W07) fixes the shape: read scope, at most three candidates withitem_id,factual_reasons,semantic_reasons,unknownsand judgment provenance, zero calendar writes, and a choice that is never task completion.Changes
src/api/next.rs(new) —POST /v1/next, a thin read over #22 + #23:windowis a positive duration (90m,2h) bounded to 1 minute–24 hours (the bound is stated in the module and enforced with the reason),energyis one of the three levels that exist. A malformed request isinvalid_input(HTTP 400 / exit 2) before any backend is contacted.tasks::todo_reportrather than duplicating the read. An unread collection is disclosed asincomplete_calendar; all collections unread isdependency_unavailable(exit 5), never an empty calendar.history::timeline::day_outcomes), where an unreadable or absent journal ishistory_incomplete+CareerFloor::Unknown— an observation gap, never an unmet floor.data:basis(semantic/deterministic),considered(how many candidates the policy ordered, so the three-candidate bound is stated rather than hidden),candidates(documented fields;unknownspropagated verbatim from #22;judgment_provenance= pinned model + the versioned questions), andexcluded(one stable reason code and sentence per dropped task, so an empty answer explains itself).src/api/mod.rs— registersnextand the read-scopenext_handler(noIdempotency-Key; mirrors the existing/v1/resolveread-scope POST).src/api/tasks.rs—todo_reportbecomespub(crate)so the next-action read shares the one live VTODO read implementation.src/cli.rs—life next --window <duration> --energy <low|medium|high> [--area <area>] [--json]: validates both flags locally (exit 2 without a round trip), posts throughpost_read_envelope(no operation key), prints exactly one envelope on stdout with--json, and otherwise renders a terminal view that marks[Fact],[Jev]and[UNKNOWN]apart, states the basis, and says plainly that choosing a candidate is not task completion.tests/next_command_tests.rs(new) — 19 end-to-end tests over real HTTP: mock CalDAV + mock System One over TCP, the daemon's own router and SQLite worker, and the real binary for the CLI assertions.Explicitly not in scope: planning/proposals (#25–#27), outcome capture (#28), auth (#35), and any change to #22/#23 semantics.
Verification
nix develop --command cargo test— 537 passed, 0 failed (was 514; +19 integration, +4 unit).cargo clippy --all-targets -- -D warningsandcargo fmt --checkare clean.Scenarios covered by
tests/next_command_tests.rs:considered: 4, every documented field present, and no factual reason is a score while every semantic reason is a model answer naming the model and question versions.--window 90mexcludes a known-120m task withwindow_exceeded, retains the unestimated task, and that task carriesunknowns: [estimated_minutes, energy]with a factual reason that says fit is not claimed; the same task becomes eligible (and is claimed to fit) at--window 2h.{},90,0m,25h,1h30m,turbo, missing flags) → HTTP 400 / CLI exit 2, asserted with zero calendar requests and zero judgment dispatches.basis: deterministicwithjudgment_unavailable/judgment_invalidand every candidate retained; a semantic run and a deterministic run are shown to state identical factual reasons for the same candidate.candidates: [],considered: 0and the filter's exclusion diagnostics — never a 404, never an invented candidate.incomplete_calendar(naming the collection) with candidates retained and no free-capacity claim; an unreachable calendar isdependency_unavailable/ exit 5 with no data.REPORT(explicitPUT/POST/DELETE/PATCH/PROPPATCH/MKCOLcounts of 0), no journal row written,operationsrows 0, nooperation_id/operation_statein any answer, and the completed task is never offered.[Fact]/[UNKNOWN]markers, the degraded-basis note and the "not task completion" line with diagnostics on stderr.Related Issues
Closes #24
Coordinator review — APPROVED
Verified
9fe1eebon a tree based on currentmain(772f0f2, so #35's auth layer and scope table are present and/v1/nextis already mapped toread). Independent verification:cargo test557 passed / 0 failed (19 new next-command tests),cargo clippy --all-targets -- -D warnings0 errors,cargo fmt --checkclean.What I checked beyond the summary:
src/api/next.rsfor every write primitive —put_new_resource,put_with_precondition,prepare_operation,commit_operation_result,lifecycle::— and got zero matches. There is no write path to reach, which is stronger than an assertion that none was taken; your zero-write test then adds the empirical layer (every CalDAV request is aREPORT, all journal tables byte-identical before/after,operations == 0, and no answer carries anoperation_id).consideredvs the ≤3 returned candidates makes truncation visible, andexcludedcarries the single deciding reason per dropped task — so criterion 6's "empty is explained, never a 404 or an invented candidate" is satisfied by the shape itself rather than by a special case.unknownsis copied verbatim from #22 with the doc comment spelling out the rule ("an unknown estimate is not a fit and an unknown energy is not low"), and the test pair is the good kind — the same 120-minute task is excluded at90mwith a "not claimed to fit" reason and is claimed to fit at2h. That asymmetry is what proves the claim is window-relative rather than hard-coded.semantic_reasonsis empty on the deterministic path with the reason stated ("a score that decided nothing is not a reason").MIN_WINDOW_MINUTES = 1,MAX_WINDOW_MINUTES = 1440), with malformed input rejected asinvalid_input/exit 2 across nine bodies and seven CLI flag combinations, all with zero calendar requests and zero judgment dispatches.todo_reportwas widened topub(crate)so this reads through the single existing live VTODO read rather than adding a second one.Merging. This closes the #22 → #23 → #24 chain: the deterministic filter, the proven lexicographic order, and the advisory command that separates facts from judgments.
feat(api): next action command with advisory provenance (#24)to feat(api): next action command with advisory provenance