Next-Task Policy Filter & Candidate Window Slicing #22

Closed
opened 2026-09-22 17:58:57 +00:00 by yuris · 0 comments
Owner

Summary

Implement the deterministic candidate selection and policy filtering engine for next-task recommendations, enforcing strict lifecycle status, commitment strength, explicit context blockers, duration window capacity, energy levels, and weekday career floor invariants before any semantic ranking or Jev evaluation occurs.

Background & Technical Rationale

When a user or agent requests recommendations for what to work on next (life next --window 90m --energy medium), the system must not immediately delegate task selection to an LLM or semantic ranker. Doing so would risk hallucinating task properties, ignoring hard calendar deadlines, violating energy limits, or scheduling blocked work.

Instead, Life applies a deterministic-first policy pipeline that filters the active candidate pool down to eligible tasks based on verifiable facts:

  1. Lifecycle Status & Commitment Strength Invariants:
    • Candidate items are read from live VTODOs in Radicale.
    • Standard iCalendar STATUS must be NEEDS-ACTION or IN-PROCESS. Tasks that are COMPLETED or CANCELLED are strictly excluded.
    • Commitment strength is tracked via custom categories: life-active, life-queued, and life-someday.
    • Candidates MUST have life-active. Tasks tagged life-queued or life-someday are strictly excluded from next-task consideration (docs/technical/jev.md:36).
    • Multiple strength categories are an invalid-policy observation requiring correction (docs/technical/domain-model.md:24).
    • External tasks or items lacking any life-* strength category have unknown strength and must NOT be automatically activated or scheduled without user review.
  2. Explicit Blocker Resolution:
    • In accordance with the domain model, blockers are never represented as a fabricated iCalendar status like BLOCKED.
    • Blockers are explicit, structured user facts recorded in linked context (stored in SQLite timeline/outbox).
    • If an open task has an active, unreleased blocker recorded in its linked context facts, it is strictly excluded from the eligible candidate pool.
  3. Window Capacity Slicing:
    • The user provides an available time window duration (e.g., 90m, 2h, 45m).
    • Task estimated duration is read from the preservation-safe property X-LIFE-ESTIMATED-MINUTES.
    • If estimated_minutes is known and estimated_minutes > window_minutes, the task is strictly excluded.
    • If estimated_minutes is known and estimated_minutes <= window_minutes, the task is eligible.
    • If estimated_minutes is null or unknown, the task is RETAINED as eligible, but must be explicitly tagged with unknowns: ["estimated_minutes"] and must NEVER be described as fitting the window with certainty (Scenario W07).
  4. Energy Level Slicing:
    • The user provides their current energy state: low, medium, or high.
    • Task required energy is read from X-LIFE-ENERGY (low < medium < high).
    • If the task requires higher energy than available (e.g. task requires high, user has medium), the task is excluded.
    • If task energy is null or unknown, the task is RETAINED, but tagged with unknowns: ["energy"].
  5. Weekday Career Floor Evaluation:
    • Personal policy default: One meaningful career action per weekday. Success requires an explicit qualifying outcome linked to the action and day; allocating a calendar block is not success.
    • The filter queries the local timeline/journal in SQLite for today's date: has an approved qualifying outcome been recorded for today?
    • If today is a weekday (Monday–Friday) and no qualifying outcome has been recorded, the candidate filter records career_floor_met = false, which flags tasks categorized with area-career for priority in subsequent ranking.

Exact Technical References & Specifications

Normative Next-Task Policy Specification

From docs/technical/jev.md (lines 34–37):

"1. Read live tasks and relevant schedule, plus recorded outcomes establishing whether the career floor is met. Filter completed/cancelled, queued/someday and explicit blockers; honor user policy exceptions. Do not equate unknown strength with active.
2. Enforce known window/capacity, hard timing and explicit energy constraints. Unknown estimates or energy requirements remain marked unknown and are not described as fitting with certainty; do not fabricate blockers or readiness facts."

Workflow & Policy Defaults Contract

From docs/technical/workflows.md (lines 27, 44–47):

Workflow Input and reads Decision and approval Writes and observable result Failure path
Next task Available window and energy; live eligible tasks, deadlines and recorded qualifying outcomes Apply deterministic constraints before semantic ranking; policy order owned by Jev page At most three advisory choices, factual versus semantic reasons, no calendar writes Unknown duration/energy shown, not fabricated; Jev unavailable leaves explicit deterministic candidates, not claimed semantic ranking

"One meaningful career action per weekday. Success requires an explicit qualifying outcome linked to the action and day; allocating a calendar block is not success. Qualification must be user-approved or satisfy a reviewed deterministic rubric, never a model's unreviewed inference."

"Reserve 40% of declared available capacity unscheduled. Compute availability from configured working windows minus opaque busy time; reserve is applied to that remaining capacity before new blocks. No working windows or incomplete busy data means unknown capacity."

Domain Model Contract

From docs/technical/domain-model.md (lines 22–25, 35):

"VTODO STATUS is exactly NEEDS-ACTION, IN-PROCESS, COMPLETED or CANCELLED. A missing external STATUS normalizes to unspecified, not inferred completed. New Life tasks default to NEEDS-ACTION. Blockers are explicit structured user facts in linked context, never a fabricated standard STATUS named BLOCKED."

"Commitment strength is separate: at most one of life-active, life-queued, life-someday in CATEGORIES. New actionable tasks require an explicit strength; agent capture requests clarification when unclear. External objects without these categories have unknown strength and are not automatically activated."

"Task-only optional properties estimated_minutes, energy and project_id map to preservation-safe X-LIFE-ESTIMATED-MINUTES, X-LIFE-ENERGY, X-LIFE-PROJECT-ID; energy enum is low/medium/high. These are user assertions, not model-filled facts."

Verification Scenario W07

From docs/technical/verification.md (line 21):

ID Requirement and specification Input / action Observable acceptance
W07 Next action 90m, medium energy, deadlines and unmet career floor At most three eligible choices in documented order; factual/semantic reasons separated; unknown estimate not claimed to fit

What to Build

1. src/domain/policy/next_task_filter.rs

  • Policy filter module implementing candidate eligibility rules:
    #[derive(Debug, Clone)]
    pub struct NextTaskQuery {
        pub window_minutes: u32,
        pub energy: EnergyLevel,
        pub area_filter: Option<String>,
    }
    
    #[derive(Debug, Clone)]
    pub struct FilterContext {
        pub now: DateTime<Utc>,
        pub today: NaiveDate,
        pub is_weekday: bool,
        pub career_floor_met: bool,
        pub calendar_complete: bool,
        pub active_blockers: HashSet<String>, // Canonical item IDs with active blockers
    }
    
    #[derive(Debug, Clone)]
    pub struct EligibleCandidate {
        pub item: Item,
        pub factual_reasons: Vec<String>,
        pub unknowns: Vec<String>,
        pub is_career_task: bool,
        pub urgency_tier: DeadlineUrgencyTier,
    }
    
    pub fn filter_candidate_tasks(
        tasks: Vec<Item>,
        query: &NextTaskQuery,
        context: &FilterContext,
    ) -> (Vec<EligibleCandidate>, Vec<FilterDiagnostic>)
    
  • Filter Evaluation Steps:
    1. Lifecycle check: Exclude if status == "COMPLETED" or status == "CANCELLED".
    2. Strength check: Exclude if categories contains life-someday or life-queued, lacks life-active, or contains multiple conflicting strength tags (docs/technical/jev.md:36, domain-model.md:24).
    3. Hard-timing check: Exclude if item.planned_start is present and in the future (item.planned_start > context.now) (docs/technical/domain-model.md:35).
    4. Blocker check: Exclude if context.active_blockers.contains(&item.id). Record diagnostic reason "Task is blocked by linked context".
    5. Window capacity check:
      • If item.estimated_minutes > Some(query.window_minutes): exclude task. Diagnostic: "Estimated duration (Xm) exceeds available window (Ym)".
      • If item.estimated_minutes.is_none(): retain task; record "estimated_minutes" in unknowns; record factual reason "Estimated duration unknown; not claimed to fit with certainty".
      • If item.estimated_minutes <= Some(query.window_minutes): retain task; record factual reason "Estimated duration (Xm) fits within available window (Ym)".
    6. Energy level check:
      • If item.energy > Some(query.energy): exclude task. Diagnostic: "Required energy (high) exceeds available energy (medium)".
      • If item.energy.is_none(): retain task; record "energy" in unknowns.
      • If item.energy <= Some(query.energy): retain task; record factual reason "Required energy matches available level".
    7. Area filter check (if requested):
      • If query.area_filter is provided, ensure item.categories contains format!("area-{}", area).
    8. Career floor check:
      • Check if item.categories contains "area-career". If context.is_weekday && !context.career_floor_met && is_career, record factual reason "Matches unmet weekday career floor".

2. Timeline Career Floor Query (src/storage/timeline.rs)

  • Add query to SQLite timeline storage using canonical schema (docs/technical/timeline-consistency.md:37–58):
    pub fn check_career_floor_attained(
        conn: &Connection,
        date: NaiveDate,
    ) -> Result<bool, StorageError>;
    
  • Checks for any event in life_events occurring on date where:
    • evidence_class == "user_report"
    • kind == "work_block_outcome"
    • json_extract(after_json, '$.qualifies_career_floor') == 1 or true
    • json_extract(after_json, '$.outcome') == 'completed'

3. Unit & Integration Tests (tests/policy_filter_tests.rs)

  • Test 1: Completed and cancelled tasks are strictly excluded.
  • Test 2: Task with life-someday or unknown strength is excluded.
  • Test 3: Task requiring 120 minutes is excluded when query specifies --window 90m.
  • Test 4: Task with unknown duration (estimated_minutes: null) is retained, has "estimated_minutes" in unknowns, and factual reasons do not assert guaranteed fit (Scenario W07).
  • Test 5: Task requiring high energy is excluded when query specifies --energy medium.
  • Test 6: Active blocker in context excludes task even if window and energy fit perfectly.
  • Test 7: Weekday career floor detection flags area-career tasks as high priority when no qualifying outcome has been recorded for today.

Acceptance Criteria

  • Completed (COMPLETED) and cancelled (CANCELLED) tasks are strictly excluded from candidate selection.
  • Tasks tagged life-someday, life-queued, or lacking life-active are strictly excluded (docs/technical/jev.md:36).
  • Tasks with future planned start times (planned_start > now) are excluded from immediate next-task eligibility.
  • Tasks with active blockers recorded in linked context are matched by canonical composite item ID and strictly excluded.
  • A task with estimated duration of 120 minutes is excluded when query specifies --window 90m.
  • A task with unknown duration or energy is retained as a candidate, labeled with unknown attributes, and not claimed to fit with certainty (Scenario W07).
  • A task with high energy requirement is filtered out when query specifies --energy low or --energy medium.
  • Unmet weekday career floor correctly identifies qualifying area-career tasks for priority ranking.
  • Diagnostic logs or filter output explain why ineligible tasks were excluded.

Notes

  • Criterion 4's matching rule is implemented and tested; the source (deriving active_blockers from recorded explicit blocker facts) does not exist yet and is tracked as a separate follow-up, so today the set is supplied by the caller (#24) or empty.
  • Journal corrections (#29) are not folded into day_outcomes; the 40% unscheduled reserve belongs to #25–#27.
  • No dependency added, so the deferred Cargo.toml authors chore lands with #35.

Blocked by

  • #012: Task Lifecycle Transitions, Replays & Progress Tracking
  • #018: Strict Context Search & Supersession Provenance
  • #021: Advisory Jev Integration & Graceful Degradation
## Summary Implement the deterministic candidate selection and policy filtering engine for next-task recommendations, enforcing strict lifecycle status, commitment strength, explicit context blockers, duration window capacity, energy levels, and weekday career floor invariants before any semantic ranking or Jev evaluation occurs. ## Background & Technical Rationale When a user or agent requests recommendations for what to work on next (`life next --window 90m --energy medium`), the system must not immediately delegate task selection to an LLM or semantic ranker. Doing so would risk hallucinating task properties, ignoring hard calendar deadlines, violating energy limits, or scheduling blocked work. Instead, Life applies a **deterministic-first policy pipeline** that filters the active candidate pool down to eligible tasks based on verifiable facts: 1. **Lifecycle Status & Commitment Strength Invariants:** - Candidate items are read from live VTODOs in Radicale. - Standard iCalendar `STATUS` must be `NEEDS-ACTION` or `IN-PROCESS`. Tasks that are `COMPLETED` or `CANCELLED` are strictly excluded. - Commitment strength is tracked via custom categories: `life-active`, `life-queued`, and `life-someday`. - Candidates MUST have `life-active`. Tasks tagged `life-queued` or `life-someday` are strictly excluded from next-task consideration (`docs/technical/jev.md:36`). - Multiple strength categories are an invalid-policy observation requiring correction (`docs/technical/domain-model.md:24`). - External tasks or items lacking any `life-*` strength category have **unknown strength** and must NOT be automatically activated or scheduled without user review. 2. **Explicit Blocker Resolution:** - In accordance with the domain model, blockers are never represented as a fabricated iCalendar status like `BLOCKED`. - Blockers are explicit, structured user facts recorded in linked context (stored in SQLite timeline/outbox). - If an open task has an active, unreleased blocker recorded in its linked context facts, it is strictly excluded from the eligible candidate pool. 3. **Window Capacity Slicing:** - The user provides an available time window duration (e.g., `90m`, `2h`, `45m`). - Task estimated duration is read from the preservation-safe property `X-LIFE-ESTIMATED-MINUTES`. - If `estimated_minutes` is known and `estimated_minutes > window_minutes`, the task is strictly excluded. - If `estimated_minutes` is known and `estimated_minutes <= window_minutes`, the task is eligible. - If `estimated_minutes` is **null or unknown**, the task is **RETAINED** as eligible, but must be explicitly tagged with `unknowns: ["estimated_minutes"]` and must NEVER be described as fitting the window with certainty (Scenario W07). 4. **Energy Level Slicing:** - The user provides their current energy state: `low`, `medium`, or `high`. - Task required energy is read from `X-LIFE-ENERGY` (`low` < `medium` < `high`). - If the task requires higher energy than available (e.g. task requires `high`, user has `medium`), the task is excluded. - If task energy is **null or unknown**, the task is **RETAINED**, but tagged with `unknowns: ["energy"]`. 5. **Weekday Career Floor Evaluation:** - Personal policy default: One meaningful career action per weekday. Success requires an explicit qualifying outcome linked to the action and day; allocating a calendar block is not success. - The filter queries the local timeline/journal in SQLite for today's date: has an approved qualifying outcome been recorded for today? - If today is a weekday (Monday–Friday) and no qualifying outcome has been recorded, the candidate filter records `career_floor_met = false`, which flags tasks categorized with `area-career` for priority in subsequent ranking. ## Exact Technical References & Specifications ### Normative Next-Task Policy Specification From `docs/technical/jev.md` (lines 34–37): > "1. Read live tasks and relevant schedule, plus recorded outcomes establishing whether the career floor is met. Filter completed/cancelled, queued/someday and explicit blockers; honor user policy exceptions. Do not equate unknown strength with active. > 2. Enforce known window/capacity, hard timing and explicit energy constraints. Unknown estimates or energy requirements remain marked unknown and are not described as fitting with certainty; do not fabricate blockers or readiness facts." ### Workflow & Policy Defaults Contract From `docs/technical/workflows.md` (lines 27, 44–47): | Workflow | Input and reads | Decision and approval | Writes and observable result | Failure path | |---|---|---|---|---| | Next task | Available window and energy; live eligible tasks, deadlines and recorded qualifying outcomes | Apply deterministic constraints before semantic ranking; policy order owned by Jev page | At most three advisory choices, factual versus semantic reasons, no calendar writes | Unknown duration/energy shown, not fabricated; Jev unavailable leaves explicit deterministic candidates, not claimed semantic ranking | > "One meaningful career action per weekday. Success requires an explicit qualifying outcome linked to the action and day; allocating a calendar block is not success. Qualification must be user-approved or satisfy a reviewed deterministic rubric, never a model's unreviewed inference." > > "Reserve 40% of declared available capacity unscheduled. Compute availability from configured working windows minus opaque busy time; reserve is applied to that remaining capacity before new blocks. No working windows or incomplete busy data means unknown capacity." ### Domain Model Contract From `docs/technical/domain-model.md` (lines 22–25, 35): > "VTODO `STATUS` is exactly `NEEDS-ACTION`, `IN-PROCESS`, `COMPLETED` or `CANCELLED`. A missing external STATUS normalizes to unspecified, not inferred completed. New Life tasks default to NEEDS-ACTION. Blockers are explicit structured user facts in linked context, never a fabricated standard STATUS named BLOCKED." > > "Commitment strength is separate: at most one of `life-active`, `life-queued`, `life-someday` in CATEGORIES. New actionable tasks require an explicit strength; agent capture requests clarification when unclear. External objects without these categories have unknown strength and are not automatically activated." > > "Task-only optional properties estimated_minutes, energy and project_id map to preservation-safe `X-LIFE-ESTIMATED-MINUTES`, `X-LIFE-ENERGY`, `X-LIFE-PROJECT-ID`; energy enum is low/medium/high. These are user assertions, not model-filled facts." ### Verification Scenario W07 From `docs/technical/verification.md` (line 21): | ID | Requirement and specification | Input / action | Observable acceptance | |---|---|---|---| | W07 | [Next action](jev.md#next-task-policy) | 90m, medium energy, deadlines and unmet career floor | At most three eligible choices in documented order; factual/semantic reasons separated; unknown estimate not claimed to fit | ## What to Build ### 1. `src/domain/policy/next_task_filter.rs` - Policy filter module implementing candidate eligibility rules: ```rust #[derive(Debug, Clone)] pub struct NextTaskQuery { pub window_minutes: u32, pub energy: EnergyLevel, pub area_filter: Option<String>, } #[derive(Debug, Clone)] pub struct FilterContext { pub now: DateTime<Utc>, pub today: NaiveDate, pub is_weekday: bool, pub career_floor_met: bool, pub calendar_complete: bool, pub active_blockers: HashSet<String>, // Canonical item IDs with active blockers } #[derive(Debug, Clone)] pub struct EligibleCandidate { pub item: Item, pub factual_reasons: Vec<String>, pub unknowns: Vec<String>, pub is_career_task: bool, pub urgency_tier: DeadlineUrgencyTier, } pub fn filter_candidate_tasks( tasks: Vec<Item>, query: &NextTaskQuery, context: &FilterContext, ) -> (Vec<EligibleCandidate>, Vec<FilterDiagnostic>) ``` - **Filter Evaluation Steps:** 1. **Lifecycle check:** Exclude if `status == "COMPLETED"` or `status == "CANCELLED"`. 2. **Strength check:** Exclude if `categories` contains `life-someday` or `life-queued`, lacks `life-active`, or contains multiple conflicting strength tags (`docs/technical/jev.md:36`, `domain-model.md:24`). 3. **Hard-timing check:** Exclude if `item.planned_start` is present and in the future (`item.planned_start > context.now`) (`docs/technical/domain-model.md:35`). 4. **Blocker check:** Exclude if `context.active_blockers.contains(&item.id)`. Record diagnostic reason `"Task is blocked by linked context"`. 5. **Window capacity check:** - If `item.estimated_minutes > Some(query.window_minutes)`: exclude task. Diagnostic: `"Estimated duration (Xm) exceeds available window (Ym)"`. - If `item.estimated_minutes.is_none()`: retain task; record `"estimated_minutes"` in `unknowns`; record factual reason `"Estimated duration unknown; not claimed to fit with certainty"`. - If `item.estimated_minutes <= Some(query.window_minutes)`: retain task; record factual reason `"Estimated duration (Xm) fits within available window (Ym)"`. 5. **Energy level check:** - If `item.energy > Some(query.energy)`: exclude task. Diagnostic: `"Required energy (high) exceeds available energy (medium)"`. - If `item.energy.is_none()`: retain task; record `"energy"` in `unknowns`. - If `item.energy <= Some(query.energy)`: retain task; record factual reason `"Required energy matches available level"`. 6. **Area filter check (if requested):** - If `query.area_filter` is provided, ensure `item.categories` contains `format!("area-{}", area)`. 7. **Career floor check:** - Check if `item.categories` contains `"area-career"`. If `context.is_weekday && !context.career_floor_met && is_career`, record factual reason `"Matches unmet weekday career floor"`. ### 2. Timeline Career Floor Query (`src/storage/timeline.rs`) - Add query to SQLite timeline storage using canonical schema (`docs/technical/timeline-consistency.md:37–58`): ```rust pub fn check_career_floor_attained( conn: &Connection, date: NaiveDate, ) -> Result<bool, StorageError>; ``` - Checks for any event in `life_events` occurring on `date` where: - `evidence_class == "user_report"` - `kind == "work_block_outcome"` - `json_extract(after_json, '$.qualifies_career_floor') == 1` or `true` - `json_extract(after_json, '$.outcome') == 'completed'` ### 3. Unit & Integration Tests (`tests/policy_filter_tests.rs`) - Test 1: Completed and cancelled tasks are strictly excluded. - Test 2: Task with `life-someday` or unknown strength is excluded. - Test 3: Task requiring 120 minutes is excluded when query specifies `--window 90m`. - Test 4: Task with unknown duration (`estimated_minutes: null`) is retained, has `"estimated_minutes"` in `unknowns`, and factual reasons do not assert guaranteed fit (Scenario W07). - Test 5: Task requiring `high` energy is excluded when query specifies `--energy medium`. - Test 6: Active blocker in context excludes task even if window and energy fit perfectly. - Test 7: Weekday career floor detection flags `area-career` tasks as high priority when no qualifying outcome has been recorded for today. ## Acceptance Criteria - [x] Completed (`COMPLETED`) and cancelled (`CANCELLED`) tasks are strictly excluded from candidate selection. - [x] Tasks tagged `life-someday`, `life-queued`, or lacking `life-active` are strictly excluded (`docs/technical/jev.md:36`). - [x] Tasks with future planned start times (`planned_start > now`) are excluded from immediate next-task eligibility. - [x] Tasks with active blockers recorded in linked context are matched by canonical composite item ID and strictly excluded. - [x] A task with estimated duration of 120 minutes is excluded when query specifies `--window 90m`. - [x] A task with unknown duration or energy is retained as a candidate, labeled with `unknown` attributes, and not claimed to fit with certainty (Scenario W07). - [x] A task with `high` energy requirement is filtered out when query specifies `--energy low` or `--energy medium`. - [x] Unmet weekday career floor correctly identifies qualifying `area-career` tasks for priority ranking. - [x] Diagnostic logs or filter output explain why ineligible tasks were excluded. ## Notes - Criterion 4's **matching** rule is implemented and tested; the **source** (deriving `active_blockers` from recorded explicit blocker facts) does not exist yet and is tracked as a separate follow-up, so today the set is supplied by the caller (#24) or empty. - Journal corrections (#29) are not folded into `day_outcomes`; the 40% unscheduled reserve belongs to #25–#27. - No dependency added, so the deferred `Cargo.toml` authors chore lands with #35. ## Blocked by - #012: Task Lifecycle Transitions, Replays & Progress Tracking - #018: Strict Context Search & Supersession Provenance - #021: Advisory Jev Integration & Graceful Degradation
yuris changed title from Issue #022: Next-Task Policy Filter & Candidate Window Slicing to Next-Task Policy Filter & Candidate Window Slicing 2026-09-22 20:02:36 +00:00
yuris closed this issue 2026-09-23 00:16:43 +00:00
Sign in to join this conversation.
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#22
No description provided.