feat(domain): multi-criteria lexicographic next-task ranking #64
Loading…
Add table
Add a link
Reference in a new issue
No description provided.
Delete branch "feat/issue-23-ranking"
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
Step 3 of the next-task policy (
docs/technical/jev.md#next-task-policy) fixes an explicit lexicographic order over the eligible candidates that issue #22 produces, and step 4 fixes the shape of what is returned; scenario W07 requires at most three choices in that documented order with factual and semantic reasons kept apart. Nothing implemented it yet: #22 deliberately stops at "eligible, with facts and unknowns" and states that it never orders. This change adds that engine and nothing above it — no route, no CLI, no presentation, which #24 owns.Changes
src/domain/policy/next_task_ranker.rs(new) — the ranking engine over #22'sEligibleCandidates:overdue(earlier first), then a deadline inside the known window (earlier first), then a known later deadline (earlier first), then no deadline. An overdue task outranks any semantic score. A date-only deadline is compared at the end of its local day and is reported as the date it states: the two are never converted into each other, so no midnight appointment is invented, and with no known window a future deadline is not claimed to fall inside one.career_floor_prioritysignal #22 already decided (trueonly when the floor isUnmet). It is applied after the deadline, so it only reorders candidates the deadline already tied;Unknownis never read as unmet.alignment.v1, thenconsequence.v1, thenreadiness.v1, each descending, fromJevService::candidate_advice.item.id.cmp(&other.id).compare_candidatesis that order as a public function, so the order the sort uses is exactly the order that is tested for antisymmetry, transitivity and permutation invariance.factual_reasons; the three answers aresemantic_reasons. No score can enter a factual reason, and no score is read as a deadline, duration, energy, status or window fit.MAX_SCORED_CANDIDATES = 20(=MAX_CHOICE_CANDIDATES, the reviewed pool cap the resolution path already applies and discloses) because every judged candidate costs three dispatches. When a larger set is considered, the result carriescandidate_narrowednaming how many were judged and that the rest are ordered by the deadline and career facts plus the item id. No candidate is dropped: the whole order is returned.Deterministic— the scores are discarded rather than deciding part of the order — withjudgment_unavailable/judgment_invalidattached and every candidate still ranked.src/domain/policy/mod.rs— registers and re-exports the module; its module doc now says the ranking lives beside the filter instead of being future work.The typed surface #24 will consume
POST /v1/nextmaps these onto the documentedcandidatesentries (item_id,factual_reasons,semantic_reasons,unknowns, judgment provenance), merges #22's filter warnings and exclusion diagnostics into the envelope, and maps theErr(a date-only deadline whose local midnight this zone cannot resolve) tocalendar_unsupported, as the tasks and agenda reads already do.Verification
nix develop --command cargo test— 514 tests, every suite green (28 suites; 499 before this change, all still passing).nix develop --command cargo fmt --checkandnix develop --command cargo clippy --all-targets -- -D warnings— clean.tests/next_ranking_tests.rs(15 tests), driving the real path end to end: live tasks come from a mock CalDAV server, the candidate set from the #22 filter itself, and the semantic families from a mock System One that answers exactly what the adapter validates.Met/Unknown/NotApplicableleaving the item id to decide); alignment, then consequence, then readiness separating identical facts; identical answers falling through to the item id.candidate_narrowednaming it, 21 candidates still returned, the unjudged one left unjudged and disclosed).unknownspropagated (estimated_minutes,energy), and the factual reasons identical with and without a judgment.judgment_unavailable), disabled provider (dispatches nothing), drifted model refused (judgment_invalid) — each still ranking every candidate deterministically.PUT/POST/DELETEagainst the mock CalDAV (only its reads), with a semantic ranking actually produced.Related Issues
Closes #23
7423467ae9toae9abf0f57Coordinator review — APPROVED
Verified
ae9abf0myself:cargo test514 passed / 0 failed (15 new ranking tests),cargo clippy --all-targets -- -D warnings0 errors,cargo fmt --checkclean.What I checked beyond the summary:
jev.mdstep 3 —tier → deadline instant → career floor (descending) → alignment/consequence/readiness (descending) → item id— andDeadlineUrgencyTierderivesOrdin the documented sequence, so the tier order cannot drift out of the spec by a later reordering. That is the right way to make a policy order hard to break.#22's signal and never reinterpretsUnknownas unmet, and it only reorders already-tied candidates — a career task with a later deadline still sits behind a non-career earlier deadline.MAX_SCORED_CANDIDATES = 20applied to the factual order (so the choice is deterministic), withcandidate_narrowed— the identifier#20already established — naming the bound, and all 21 candidates still returned with the unjudged one marked unjudged. Nothing is silently dropped. The 21-candidate test asserting exactly 60 dispatches (20 × 3 families) is precise evidence rather than a vibe.RankingBasis::Deterministicwith the scores dropped and the right warning — which is stronger than "rank what we have", and matches "never present a deterministic order as a semantic ranking".Merging.
#24now consumesrank_candidates(...) -> NextTaskRanking.feat(domain): multi-criteria lexicographic next-task ranking (#23)to feat(domain): multi-criteria lexicographic next-task ranking