Work-Block Attendance & Outcome Fact Capture #28

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

Summary

Implement explicit work-block outcome logging (POST /v1/captures with kind: "outcome" and life capture --input -), capturing user-reported attendance and execution facts (attended, completed, missed, partial, unknown) into SQLite life_events (evidence_class: "user_report") without mutating Radicale CalDAV VEVENT components or inferring task completion.

Background & Technical Rationale

Calendar systems and productivity applications frequently commit a critical epistemic error: they conflate the passage of time on a clock with human action. When a scheduled 90-minute focus block expires, naive systems assume the user worked on the task, or worse, automatically mark the linked task as finished.

Life strictly enforces an empirical timeline contract:

  1. Zero Clock Inference: The passage of time proves neither attendance nor task completion. When a scheduled calendar event ends, Life does nothing. No outcome is created, no task status is toggled, and no assumptions are recorded (Scenario W13).
  2. VEVENT Standards Limitation: Under RFC 5545, VEVENT components support only STATUS: TENTATIVE, STATUS: CONFIRMED, and STATUS: CANCELLED. There is no standard iCalendar COMPLETED status for calendar events. Any attempt to write a completion status to a VEVENT either corrupts the CalDAV resource or introduces proprietary, non-interoperable fields.
  3. Outcomes as User-Reported Timeline Facts: Real execution outcomes exist exclusively as append-only facts in SQLite life_events with evidence_class: "user_report".
  4. Decoupled Task Lifecycle: A work block can end with its associated task still incomplete (e.g., an attended 90-minute coding session on a multi-day project). Conversely, a task can be completed without any scheduled work block (e.g., an ad-hoc action). Task completion remains an independent domain action on a VTODO component. Logging an outcome for a work block NEVER automatically alters the linked task's VTODO lifecycle status.
  5. Career Floor Attribution: Qualification toward personal career floor metrics requires an explicit user assertion (qualifies_career_floor: true), never an unreviewed model inference.

Exact Technical References & Specifications

1. Domain Entities & Time/Work Outcomes (docs/technical/domain-model.md)

Section: ## Entities (lines 7–18):

| Entity | Meaning and authoritative home |
|---|---|
| Calendar event | Radicale VEVENT: scheduled interval, potentially linked to a task as a work block |
| Timeline record | Append-only fact with event kind, evidence class, recording time, nullable actual time and correction linkage |

Section: ## Time and work outcomes (lines 41–42):

VEVENT supports TENTATIVE, CONFIRMED and CANCELLED; there is no standard COMPLETED status. Work-block outcomes attended, completed, missed, partial and unknown are explicit user-reported timeline facts. An ended calendar interval proves neither attendance nor task completion. A task completion can occur without a scheduled block; a finished block can leave its task incomplete.

2. Workflows Specification (docs/technical/workflows.md)

Section: ## Lifecycle and review (line 36):

| Workflow | Input and reads | Decision and approval | Writes and observable result | Failure path |
|---|---|---|---|---|
| Work-block outcome | Explicit user report with event/item identity, outcome and actual time if known | Never infer from elapsed clock; task completion remains separate | Structured capture appends user-reported timeline fact; no invented VEVENT completion status | Reject unsupported outcome; event availability failure disclosed if linkage cannot be verified |

Section: ## Personal policy defaults (line 44):

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.

3. CLI and API Route & Payloads (docs/technical/cli-api.md)

Section: ## Command-to-route table (line 40):

| `life capture --input -` | POST /v1/captures | write |

Section: ## Command payloads and results (lines 87, 102):

| Capture | kind task/event/context/outcome/correction; payload with corresponding fields; optional context for actionable capture | operation_id, per-part results and delivery state; partial parts remain explicit |

Outcome capture payload: item_id, outcome (domain enum), optional occurred_at, actual_minutes, qualifies_career_floor (explicit user assertion), reason. Correction payload: target_event_id, replacement (same assertion type as target), reason. Only user assertions and derived interpretations may be corrected through capture; confirmed transport facts cannot be rewritten into different remote results. Fixing item state uses a separate conditional item mutation.

4. Database Schema & Append-Only Triggers (docs/technical/timeline-consistency.md)

Section: ## Illustrative schema (lines 37–62):

CREATE TABLE life_events (
  sequence INTEGER PRIMARY KEY AUTOINCREMENT,
  event_id TEXT NOT NULL UNIQUE,
  dedupe_key TEXT NOT NULL UNIQUE,
  schema_version INTEGER NOT NULL,
  kind TEXT NOT NULL,
  evidence_class TEXT NOT NULL CHECK(evidence_class IN
    ('confirmed_service','observed_external','user_report','derived')),
  recorded_at TEXT NOT NULL,
  occurred_at TEXT,
  actor TEXT,
  source TEXT NOT NULL,
  collection_id TEXT,
  uid TEXT,
  recurrence_key TEXT,
  before_json TEXT,
  after_json TEXT,
  reason TEXT,
  provenance_json TEXT NOT NULL,
  operation_id TEXT REFERENCES operations(operation_id),
  correction_target TEXT REFERENCES life_events(event_id)
);
CREATE TRIGGER life_events_no_update BEFORE UPDATE ON life_events
BEGIN SELECT RAISE(ABORT, 'append-only events'); END;
CREATE TRIGGER life_events_no_delete BEFORE DELETE ON life_events
BEGIN SELECT RAISE(ABORT, 'append-only events'); END;

5. Verification Scenario W13 (docs/technical/verification.md)

Section: ## Workflow scenarios (line 27):

| ID | Requirement and specification | Input / action | Observable acceptance |
|---|---|---|---|
| W13 | Outcomes (domain-model.md#time-and-work-outcomes) | Event ends with no report, then explicit missed outcome | No inferred completion; explicit timeline outcome, no VEVENT completion status |

What to build

1. Domain Outcome Data Types (src/domain/timeline/outcome.rs)

  • Define domain enum WorkBlockOutcome:
    #[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
    #[serde(rename_all = "snake_case")]
    pub enum WorkBlockOutcome {
        Attended,
        Completed,
        Missed,
        Partial,
        Unknown,
    }
    
  • Reject any unsupported string value with 400 Bad Request (code: "invalid_input").
  • Define OutcomePayload:
    • item_id: String (references scheduled VEVENT item or related task).
    • outcome: WorkBlockOutcome.
    • occurred_at: Optional ISO-8601 instant string (when the block took place).
    • actual_minutes: Optional non-negative integer (self-reported actual duration in minutes; 0 represents explicitly observed zero effort, while null denotes unknown duration).
    • qualifies_career_floor: Optional boolean (explicit user assertion; default false).
    • reason: Optional explanatory string (e.g., "Interrupted by incident triage").

2. Event Linkage Validator (src/domain/timeline/linkage.rs)

  • Check target item_id in local projection or live Radicale store:
    • If the item exists, extract its collection_id, uid, and optional recurrence_id to establish persistent linkage.
    • If the item cannot be verified due to backend unavailability, record the outcome with unverified linkage and disclose an event_linkage_unverified warning in the response.

3. Timeline Event Insertion (src/storage/timeline_repo.rs)

  • Construct life_events row:
    • event_id: Unique prefixed ID (evt_out_...).
    • dedupe_key: Derived from operation_id (op_cap_...:fact:outcome).
    • schema_version: 1.
    • kind: "work_block_outcome".
    • evidence_class: "user_report".
    • recorded_at: Current UTC ISO-8601 timestamp.
    • occurred_at: The user-reported occurred_at or null.
    • actor: "user".
    • source: "cli_api".
    • collection_id: Target collection ID.
    • uid: Target item UID.
    • after_json: Serialized JSON payload containing:
      {
        "outcome": "missed",
        "actual_minutes": 0,
        "qualifies_career_floor": false,
        "related_task_id": "itm_task_123"
      }
      
    • reason: The user-provided reason string.
    • provenance_json: Client identity and request context.
    • operation_id: Current operation ID.
  • Execute SQL insert into life_events.
  • Perform ZERO writes to Radicale CalDAV (VEVENT remains untouched; VTODO remains untouched).

4. Axum Capture Route & CLI Command (src/api/captures.rs, src/cli/capture.rs)

  • Integrate into POST /v1/captures:
    • Request body:
      {
        "kind": "outcome",
        "payload": {
          "item_id": "evt_block_456",
          "outcome": "missed",
          "occurred_at": "2026-09-23T11:30:00Z",
          "actual_minutes": 0,
          "qualifies_career_floor": false,
          "reason": "Emergency meeting called"
        }
      }
      
    • Response (HTTP 200 OK):
      {
        "schema_version": 1,
        "request_id": "req_capture_789",
        "status": "ok",
        "data": {
          "operation_id": "op_capture_789",
          "operation_state": "applied",
          "changed": true,
          "event_id": "evt_out_01HN...",
          "kind": "outcome",
          "context_delivery": "not_requested"
        },
        "warnings": [],
        "error": null
      }
      
  • CLI Command:
    life capture --input - [--operation-id KEY] [--json]

Acceptance Criteria

  • Passing a scheduled calendar block's end time without any user capture creates ZERO entries in life_events and modifies nothing (verifying Scenario W13).
  • Logging an outcome (e.g. missed) inserts a new row into life_events with kind: "work_block_outcome" and evidence_class: "user_report" (Scenario W13).
  • The associated Radicale CalDAV VEVENT object is completely untouched; no proprietary completion status is written to CalDAV (Scenario W13).
  • Submitting an outcome of completed for a work block linked to a VTODO task leaves the task status unchanged (NEEDS-ACTION), proving decoupled task lifecycle.
  • Supplying an invalid outcome enum (e.g. "done", "failed") returns 400 Bad Request with code invalid_input.
  • The output JSON envelope contains the newly created event_id and durable operation_id.
  • actual_minutes accepts 0 to record explicitly reported zero effort (e.g. for missed blocks), while null denotes unknown duration.

Notes

  • Outcome facts are recorded through record_fact_operation (one operations row + one life_events append in one transaction); the route performs reads only, and byte-level non-mutation is asserted for both the VEVENT and its linked VTODO.
  • An ended block with no report creates nothing and triggers zero CalDAV requests — nothing is inferred from the clock.
  • The scheduled DTEND can never enter after_json as an actual end; actual_minutes: 0 is an explicit zero.
  • #17's refusal fixture was narrowed to correction (still covered; that is #29's kind).
  • No dependency added.

Blocked by

  • #005: Two-Phase Commit Journaling & Idempotency Fingerprinting
  • #027: Multi-Step Proposal Approval Verification & Application
## Summary Implement explicit work-block outcome logging (`POST /v1/captures` with `kind: "outcome"` and `life capture --input -`), capturing user-reported attendance and execution facts (`attended`, `completed`, `missed`, `partial`, `unknown`) into SQLite `life_events` (`evidence_class: "user_report"`) without mutating Radicale CalDAV VEVENT components or inferring task completion. ## Background & Technical Rationale Calendar systems and productivity applications frequently commit a critical epistemic error: they conflate the passage of time on a clock with human action. When a scheduled 90-minute focus block expires, naive systems assume the user worked on the task, or worse, automatically mark the linked task as finished. Life strictly enforces an empirical timeline contract: 1. **Zero Clock Inference:** The passage of time proves neither attendance nor task completion. When a scheduled calendar event ends, Life does nothing. No outcome is created, no task status is toggled, and no assumptions are recorded (Scenario W13). 2. **VEVENT Standards Limitation:** Under RFC 5545, `VEVENT` components support only `STATUS: TENTATIVE`, `STATUS: CONFIRMED`, and `STATUS: CANCELLED`. There is no standard iCalendar `COMPLETED` status for calendar events. Any attempt to write a completion status to a VEVENT either corrupts the CalDAV resource or introduces proprietary, non-interoperable fields. 3. **Outcomes as User-Reported Timeline Facts:** Real execution outcomes exist exclusively as append-only facts in SQLite `life_events` with `evidence_class: "user_report"`. 4. **Decoupled Task Lifecycle:** A work block can end with its associated task still incomplete (e.g., an attended 90-minute coding session on a multi-day project). Conversely, a task can be completed without any scheduled work block (e.g., an ad-hoc action). Task completion remains an independent domain action on a VTODO component. Logging an outcome for a work block NEVER automatically alters the linked task's VTODO lifecycle status. 5. **Career Floor Attribution:** Qualification toward personal career floor metrics requires an explicit user assertion (`qualifies_career_floor: true`), never an unreviewed model inference. ## Exact Technical References & Specifications ### 1. Domain Entities & Time/Work Outcomes (`docs/technical/domain-model.md`) Section: `## Entities` (lines 7–18): ```markdown | Entity | Meaning and authoritative home | |---|---| | Calendar event | Radicale VEVENT: scheduled interval, potentially linked to a task as a work block | | Timeline record | Append-only fact with event kind, evidence class, recording time, nullable actual time and correction linkage | ``` Section: `## Time and work outcomes` (lines 41–42): ```markdown VEVENT supports TENTATIVE, CONFIRMED and CANCELLED; there is no standard COMPLETED status. Work-block outcomes attended, completed, missed, partial and unknown are explicit user-reported timeline facts. An ended calendar interval proves neither attendance nor task completion. A task completion can occur without a scheduled block; a finished block can leave its task incomplete. ``` ### 2. Workflows Specification (`docs/technical/workflows.md`) Section: `## Lifecycle and review` (line 36): ```markdown | Workflow | Input and reads | Decision and approval | Writes and observable result | Failure path | |---|---|---|---|---| | Work-block outcome | Explicit user report with event/item identity, outcome and actual time if known | Never infer from elapsed clock; task completion remains separate | Structured capture appends user-reported timeline fact; no invented VEVENT completion status | Reject unsupported outcome; event availability failure disclosed if linkage cannot be verified | ``` Section: `## Personal policy defaults` (line 44): ```markdown 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. ``` ### 3. CLI and API Route & Payloads (`docs/technical/cli-api.md`) Section: `## Command-to-route table` (line 40): ```markdown | `life capture --input -` | POST /v1/captures | write | ``` Section: `## Command payloads and results` (lines 87, 102): ```markdown | Capture | kind task/event/context/outcome/correction; payload with corresponding fields; optional context for actionable capture | operation_id, per-part results and delivery state; partial parts remain explicit | Outcome capture payload: item_id, outcome (domain enum), optional occurred_at, actual_minutes, qualifies_career_floor (explicit user assertion), reason. Correction payload: target_event_id, replacement (same assertion type as target), reason. Only user assertions and derived interpretations may be corrected through capture; confirmed transport facts cannot be rewritten into different remote results. Fixing item state uses a separate conditional item mutation. ``` ### 4. Database Schema & Append-Only Triggers (`docs/technical/timeline-consistency.md`) Section: `## Illustrative schema` (lines 37–62): ```sql CREATE TABLE life_events ( sequence INTEGER PRIMARY KEY AUTOINCREMENT, event_id TEXT NOT NULL UNIQUE, dedupe_key TEXT NOT NULL UNIQUE, schema_version INTEGER NOT NULL, kind TEXT NOT NULL, evidence_class TEXT NOT NULL CHECK(evidence_class IN ('confirmed_service','observed_external','user_report','derived')), recorded_at TEXT NOT NULL, occurred_at TEXT, actor TEXT, source TEXT NOT NULL, collection_id TEXT, uid TEXT, recurrence_key TEXT, before_json TEXT, after_json TEXT, reason TEXT, provenance_json TEXT NOT NULL, operation_id TEXT REFERENCES operations(operation_id), correction_target TEXT REFERENCES life_events(event_id) ); CREATE TRIGGER life_events_no_update BEFORE UPDATE ON life_events BEGIN SELECT RAISE(ABORT, 'append-only events'); END; CREATE TRIGGER life_events_no_delete BEFORE DELETE ON life_events BEGIN SELECT RAISE(ABORT, 'append-only events'); END; ``` ### 5. Verification Scenario W13 (`docs/technical/verification.md`) Section: `## Workflow scenarios` (line 27): ```markdown | ID | Requirement and specification | Input / action | Observable acceptance | |---|---|---|---| | W13 | Outcomes (domain-model.md#time-and-work-outcomes) | Event ends with no report, then explicit missed outcome | No inferred completion; explicit timeline outcome, no VEVENT completion status | ``` ## What to build ### 1. Domain Outcome Data Types (`src/domain/timeline/outcome.rs`) - Define domain enum `WorkBlockOutcome`: ```rust #[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)] #[serde(rename_all = "snake_case")] pub enum WorkBlockOutcome { Attended, Completed, Missed, Partial, Unknown, } ``` - Reject any unsupported string value with `400 Bad Request` (`code: "invalid_input"`). - Define `OutcomePayload`: - `item_id`: String (references scheduled VEVENT item or related task). - `outcome`: `WorkBlockOutcome`. - `occurred_at`: Optional ISO-8601 instant string (when the block took place). - `actual_minutes`: Optional non-negative integer (self-reported actual duration in minutes; 0 represents explicitly observed zero effort, while null denotes unknown duration). - `qualifies_career_floor`: Optional boolean (explicit user assertion; default false). - `reason`: Optional explanatory string (e.g., "Interrupted by incident triage"). ### 2. Event Linkage Validator (`src/domain/timeline/linkage.rs`) - Check target `item_id` in local projection or live Radicale store: - If the item exists, extract its `collection_id`, `uid`, and optional `recurrence_id` to establish persistent linkage. - If the item cannot be verified due to backend unavailability, record the outcome with unverified linkage and disclose an `event_linkage_unverified` warning in the response. ### 3. Timeline Event Insertion (`src/storage/timeline_repo.rs`) - Construct `life_events` row: - `event_id`: Unique prefixed ID (`evt_out_...`). - `dedupe_key`: Derived from `operation_id` (`op_cap_...:fact:outcome`). - `schema_version`: `1`. - `kind`: `"work_block_outcome"`. - `evidence_class`: `"user_report"`. - `recorded_at`: Current UTC ISO-8601 timestamp. - `occurred_at`: The user-reported `occurred_at` or null. - `actor`: `"user"`. - `source`: `"cli_api"`. - `collection_id`: Target collection ID. - `uid`: Target item UID. - `after_json`: Serialized JSON payload containing: ```json { "outcome": "missed", "actual_minutes": 0, "qualifies_career_floor": false, "related_task_id": "itm_task_123" } ``` - `reason`: The user-provided reason string. - `provenance_json`: Client identity and request context. - `operation_id`: Current operation ID. - Execute SQL insert into `life_events`. - Perform ZERO writes to Radicale CalDAV (`VEVENT` remains untouched; `VTODO` remains untouched). ### 4. Axum Capture Route & CLI Command (`src/api/captures.rs`, `src/cli/capture.rs`) - Integrate into `POST /v1/captures`: - Request body: ```json { "kind": "outcome", "payload": { "item_id": "evt_block_456", "outcome": "missed", "occurred_at": "2026-09-23T11:30:00Z", "actual_minutes": 0, "qualifies_career_floor": false, "reason": "Emergency meeting called" } } ``` - Response (HTTP 200 OK): ```json { "schema_version": 1, "request_id": "req_capture_789", "status": "ok", "data": { "operation_id": "op_capture_789", "operation_state": "applied", "changed": true, "event_id": "evt_out_01HN...", "kind": "outcome", "context_delivery": "not_requested" }, "warnings": [], "error": null } ``` - CLI Command: `life capture --input - [--operation-id KEY] [--json]` ## Acceptance Criteria - [x] Passing a scheduled calendar block's end time without any user capture creates ZERO entries in `life_events` and modifies nothing (verifying Scenario W13). - [x] Logging an outcome (e.g. `missed`) inserts a new row into `life_events` with `kind: "work_block_outcome"` and `evidence_class: "user_report"` (Scenario W13). - [x] The associated Radicale CalDAV `VEVENT` object is completely untouched; no proprietary completion status is written to CalDAV (Scenario W13). - [x] Submitting an outcome of `completed` for a work block linked to a `VTODO` task leaves the task status unchanged (`NEEDS-ACTION`), proving decoupled task lifecycle. - [x] Supplying an invalid outcome enum (e.g. `"done"`, `"failed"`) returns `400 Bad Request` with code `invalid_input`. - [x] The output JSON envelope contains the newly created `event_id` and durable `operation_id`. - [x] `actual_minutes` accepts 0 to record explicitly reported zero effort (e.g. for missed blocks), while null denotes unknown duration. ## Notes - Outcome facts are recorded through `record_fact_operation` (one `operations` row + one `life_events` append in one transaction); the route performs reads only, and byte-level non-mutation is asserted for both the VEVENT and its linked VTODO. - An ended block with no report creates nothing and triggers **zero** CalDAV requests — nothing is inferred from the clock. - The scheduled `DTEND` can never enter `after_json` as an actual end; `actual_minutes: 0` is an explicit zero. - `#17`'s refusal fixture was narrowed to `correction` (still covered; that is #29's kind). - No dependency added. ## Blocked by - #005: Two-Phase Commit Journaling & Idempotency Fingerprinting - #027: Multi-Step Proposal Approval Verification & Application
yuris changed title from Issue #028: Work-Block Attendance & Outcome Fact Capture to Work-Block Attendance & Outcome Fact Capture 2026-09-22 20:02:34 +00:00
yuris closed this issue 2026-09-23 03:25:21 +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#28
No description provided.