Interruption Fact Logging & Schedule Replanning #26

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

Summary

Implement the interruption logging and replanning subsystem (POST /v1/replans and life replan --input -), recording explicit user-reported interruption events as append-only timeline facts in SQLite life_events (evidence_class: "user_report"), calculating remaining working capacity from the interruption timestamp, evaluating impact on scheduled tasks, and generating an updated schedule proposal in SQLite without modifying any calendar events in Radicale CalDAV.

Background & Technical Rationale

Real-world execution rarely proceeds without unexpected events. When an interruption occurs (e.g., an urgent production incident, an impromptu meeting, or an urgent family obligation), systems typically fail in one of two ways:

  1. They ignore the disruption, leaving scheduled calendar blocks in the past as if they occurred normally (fabricating attendance).
  2. They automatically shift, move, or truncate calendar blocks on remote servers without human oversight, creating calendar thrash and race conditions.

Life resolves this via strict structural separation between timeline facts and remedial proposals:

  • Interruption as Timeline Fact: An interruption is an explicit user-reported observation of reality. When reported, it must be durably recorded immediately in life_events with evidence_class: "user_report".
  • Decoupled Failure Paths: Recording the interruption fact must succeed even if generating a replan proposal fails (for instance, due to calendar fetch timeouts, conflicting constraints, or zero remaining capacity). The fact is never discarded just because replanning is infeasible.
  • Handling Unknown Duration/End: In many live situations, a user reports an interruption when it begins or while it is underway, without knowing when it will end (ended_at: null). The engine records the open interruption fact, treats remaining capacity as uncertain, discloses unknown_estimate and capacity uncertainty in warnings, and refuses to schedule tight work blocks until the end time is clarified or bounded.
  • Zero Automated Calendar Mutation: Replanning generates a new reviewable proposal in SQLite with an immutable SHA-256 digest. It makes ZERO writes to Radicale CalDAV. The user must review the proposed shifts and explicitly approve them via life proposal apply (Issue #027) before any calendar changes take effect.

Exact Technical References & Specifications

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

Section: ## Planning and interruption (lines 24–28):

| Workflow | Input and reads | Decision and approval | Writes and observable result | Failure path |
|---|---|---|---|---|
| Interruption/replanning | Explicit interruption fact, affected item IDs and actual times if known; live remaining schedule | Record user-reported interruption separately; compute available remainder; review changed commitments | Replan stores interruption fact and new proposal; only approved apply changes calendar | Recording may succeed while proposal generation fails; report both, do not erase fact or claim calendar changed |

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

Policy exceptions are explicit, scoped and recorded with reason if supplied. No guilt-driven messaging, inferred completion, silent deletion or automatic idea promotion. Life-area categories are not diagnoses or rankings of human worth.

2. Domain Entities Specification (docs/technical/domain-model.md)

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

| Entity | Meaning and authoritative home |
|---|---|
| Interruption | User-reported timeline fact: actual start/end if known, affected items and reason if supplied |
| Plan | SQLite proposal artifact: intended outcomes, capacity observations and proposed allocations |
| Proposal | Immutable SQLite review artifact: baseline versions, ordered steps, reasons, expiry and review digest |
| Timeline record | Append-only fact with event kind, evidence class, recording time, nullable actual time and correction linkage |

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

Dates and instants are distinct tagged values: `{"kind":"date","value":"2030-04-12"}` or `{"kind":"instant","value":"2030-04-12T05:00:00Z"}`. Examples are fictional. A date-only deadline is displayed as that local date, not silently converted to midnight UTC. Its planning cutoff is the following local midnight, with inclusive human meaning documented in Calendar.

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.

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

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

| `life replan --input -` | POST /v1/replans | write: fact/proposal, not calendar |

Section: ## Command payloads and results (line 91):

| Replan | interruption `{item_ids,occurred_at?,ended_at?,reason?}`, remaining intended outcomes, optional policy exceptions | Interruption event ID, proposal or generation error; no automatic calendar mutation |

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

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

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;

CREATE TABLE proposals (
  proposal_id TEXT PRIMARY KEY,
  schema_version INTEGER NOT NULL,
  state TEXT NOT NULL CHECK(state IN
    ('pending','applied','rejected','expired','partial')),
  digest TEXT NOT NULL,
  created_at TEXT NOT NULL,
  expires_at TEXT NOT NULL,
  steps_json TEXT NOT NULL,
  policy_json TEXT NOT NULL,
  operation_id TEXT REFERENCES operations(operation_id),
  approval_json TEXT
);

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

Section: ## Workflow scenarios (line 22):

| ID | Requirement and specification | Input / action | Observable acceptance |
|---|---|---|---|
| W08 | Interruption (workflows.md#planning-and-interruption) | Explicit interruption with unknown end; later corrected actual end | User fact recorded; uncertain capacity disclosed; calendar moves require a new reviewed proposal |

What to build

1. Interruption Event Recorder (src/domain/timeline/interruption.rs)

  • Parse the incoming interruption payload:
    • item_ids: Array of affected scheduled work block IDs or task IDs.
    • occurred_at: Optional tagged instant or ISO-8601 timestamp marking the start of the interruption (left null if omitted; do not invent an actual time, per docs/technical/timeline-consistency.md:119; recorded_at captures reception time).
    • ended_at: Optional tagged instant or ISO-8601 timestamp marking the end of the interruption. May be null if the interruption is ongoing or the end is unknown.
    • reason: Optional human-supplied explanation string (e.g., "Urgent production outage").
  • Construct an append-only timeline event:
    • event_id: Unique prefixed ID (evt_int_...).
    • dedupe_key: Derived from operation ID and event index (op_replan_...:fact:0).
    • schema_version: 1.
    • kind: "interruption_logged".
    • evidence_class: "user_report".
    • recorded_at: Current UTC timestamp.
    • occurred_at: The supplied occurred_at timestamp.
    • actor: "user".
    • source: "cli_api".
    • after_json: Serialized JSON payload containing {affected_item_ids, occurred_at, ended_at, reason}.
    • provenance_json: Metadata capturing client request ID and user reporting channel.
  • Execute SQL insert into life_events. This insert occurs inside a dedicated transaction that is committed regardless of whether subsequent replan calculation succeeds.

2. Remaining Capacity Evaluator (src/domain/policy/replan_capacity.rs)

  • Determine effective planning anchor:
    • If ended_at is provided, the anchor is \max(\text{ended\_at}, \text{current\_time}).
    • If ended_at is null, the anchor is \text{current\_time}. Calculate remaining time until the end of the configured working window, but mark capacity confidence as uncertain.
  • Query live Radicale calendar for the remainder of the day (from anchor to configured working window end):
    • Identify scheduled work blocks that were truncated or overrun by the interruption.
    • Identify scheduled work blocks starting strictly after the anchor.
    • Subtract remaining opaque busy events.
    • Apply the 40% unscheduled reserve rule to the remaining available window:
      \text{Remaining Gross Window} = \text{Window End} - \text{Anchor}
      \text{Remaining Available} = \max(0, \text{Remaining Gross Window} - \text{Remaining Opaque Busy})
      \text{Remaining Reserve} = 0.40 \times \text{Remaining Available}
      \text{Remaining Allocatable} = 0.60 \times \text{Remaining Available}
  • If ended_at is unknown, generate warning unknown_estimate and incomplete_calendar to explicitly disclose that capacity cannot be guaranteed.

3. Schedule Re-Sequencer & Proposal Builder (src/domain/replan.rs)

  • Take remaining intended outcomes:
    • For work blocks overrun by the interruption: propose marking the work block as interrupted/truncated or shifting it.
    • Propose moving unstarted work blocks to open slots in the remaining allocatable capacity window.
    • If remaining allocatable capacity is insufficient for all remaining outcomes:
      • Retain high-priority career floor items.
      • Propose dropping or unscheduling lower-priority blocks back to open task status (NEEDS-ACTION) with planned_start: null.
      • Do NOT silently delete any tasks.
  • Compile proposal mutation steps:
    • Each step specifies step_id, action (update_event, cancel_event, update_task), item_id, collection_id, baseline_etag, and payload.
  • Compute canonical SHA-256 digest over the ordered steps, baseline versions, and scoped policy exceptions (docs/technical/cli-api.md:104).
  • Store proposal in SQLite proposals table with state: "pending".
  • Under NO circumstance write or patch any VEVENT or VTODO in Radicale.

4. Axum Route & CLI Interface (src/api/replans.rs, src/cli/replan.rs)

  • Implement POST /v1/replans:
    • Request body:
      {
        "interruption": {
          "item_ids": ["evt_block_1"],
          "occurred_at": "2026-09-23T10:15:00Z",
          "ended_at": null,
          "reason": "Production database incident"
        },
        "remaining_intended_outcomes": ["itm_task2"],
        "policy_exceptions": []
      }
      
    • Response (HTTP 200 OK):
      {
        "schema_version": 1,
        "request_id": "req_replan_123",
        "status": "ok",
        "data": {
          "interruption_event_id": "evt_int_987",
          "proposal": {
            "proposal_id": "prop_replan_456",
            "digest": "sha256:4a3b8...",
            "state": "pending",
            "created_at": "2026-09-23T10:16:00Z",
            "expires_at": "2026-09-24T10:16:00Z",
            "steps": [
              {
                "step_id": "step_1",
                "action": "update_event",
                "item_id": "evt_block_2",
                "collection_id": "personal",
                "baseline_etag": "\"etag-block2\"",
                "payload": {
                  "start": {"kind": "instant", "value": "2026-09-23T14:00:00Z"},
                  "end": {"kind": "instant", "value": "2026-09-23T15:30:00Z"}
                },
                "reason": "Shift focus block 2 hours forward due to production interruption"
              }
            ]
          },
          "coverage": [{"collection_id": "personal", "state": "complete", "as_of": "2026-09-23T10:15:00Z"}]
        },
        "warnings": [
          {
            "code": "unknown_estimate",
            "message": "Interruption has no recorded end time; remaining capacity is uncertain"
          }
        ],
        "error": null
      }
      
    • If proposal generation fails (e.g., CalDAV read error), return HTTP 200 with status: "partial", data containing interruption_event_id, and error explaining proposal generation failure.
  • Implement CLI command:
    life replan --input - [--json]
    • Stdin takes the replan JSON payload.

Acceptance Criteria

  • Calling life replan inserts a row into life_events with kind: "interruption_logged" and evidence_class: "user_report" (verifying Scenario W08).
  • Supplying an interruption with ended_at: null records the event with ended_at as null and returns an unknown_estimate warning regarding capacity uncertainty.
  • If calendar read or proposal generation fails, the interruption event is confirmed committed in SQLite and is NOT rolled back.
  • Replan proposal generation calculates remaining capacity from the interruption anchor and applies the 40% unscheduled reserve.
  • Replan generation performs ZERO writes or updates to Radicale CalDAV storage.
  • Returned proposal includes a unique proposal_id, state "pending", and canonical SHA-256 digest covering ordered steps, baseline ETags, and policy exceptions (docs/technical/cli-api.md:104).
  • An omitted occurred_at remains null in the recorded interruption event without inventing an actual time (docs/technical/timeline-consistency.md:119).

Notes

  • The interruption fact is recorded through the new reusable journal::record_fact_operation (operations row + life_events append in one transaction, with fingerprint replay detection) — available for #28/#29.
  • Fact durability when generation fails is proven for two different failure modes, and the response reports both outcomes separately with status: partial (exit 6).
  • Replan reuses plans::generate with the anchor's remaining day and the same ceil(remaining × 40%) reserve; the prior proposal row is asserted byte-identical.
  • Zero-write property proven both by an all-REPORT request assertion and by per-table deltas.
  • No dependency added.

Blocked by

  • #025: Planning Proposal Engine & Capacity Reserve Calculations
## Summary Implement the interruption logging and replanning subsystem (`POST /v1/replans` and `life replan --input -`), recording explicit user-reported interruption events as append-only timeline facts in SQLite `life_events` (`evidence_class: "user_report"`), calculating remaining working capacity from the interruption timestamp, evaluating impact on scheduled tasks, and generating an updated schedule proposal in SQLite without modifying any calendar events in Radicale CalDAV. ## Background & Technical Rationale Real-world execution rarely proceeds without unexpected events. When an interruption occurs (e.g., an urgent production incident, an impromptu meeting, or an urgent family obligation), systems typically fail in one of two ways: 1. They ignore the disruption, leaving scheduled calendar blocks in the past as if they occurred normally (fabricating attendance). 2. They automatically shift, move, or truncate calendar blocks on remote servers without human oversight, creating calendar thrash and race conditions. Life resolves this via strict structural separation between **timeline facts** and **remedial proposals**: - **Interruption as Timeline Fact:** An interruption is an explicit user-reported observation of reality. When reported, it must be durably recorded immediately in `life_events` with `evidence_class: "user_report"`. - **Decoupled Failure Paths:** Recording the interruption fact must succeed even if generating a replan proposal fails (for instance, due to calendar fetch timeouts, conflicting constraints, or zero remaining capacity). The fact is never discarded just because replanning is infeasible. - **Handling Unknown Duration/End:** In many live situations, a user reports an interruption when it begins or while it is underway, without knowing when it will end (`ended_at: null`). The engine records the open interruption fact, treats remaining capacity as uncertain, discloses `unknown_estimate` and capacity uncertainty in warnings, and refuses to schedule tight work blocks until the end time is clarified or bounded. - **Zero Automated Calendar Mutation:** Replanning generates a new reviewable proposal in SQLite with an immutable SHA-256 digest. It makes ZERO writes to Radicale CalDAV. The user must review the proposed shifts and explicitly approve them via `life proposal apply` (Issue #027) before any calendar changes take effect. ## Exact Technical References & Specifications ### 1. Workflows Specification (`docs/technical/workflows.md`) Section: `## Planning and interruption` (lines 24–28): ```markdown | Workflow | Input and reads | Decision and approval | Writes and observable result | Failure path | |---|---|---|---|---| | Interruption/replanning | Explicit interruption fact, affected item IDs and actual times if known; live remaining schedule | Record user-reported interruption separately; compute available remainder; review changed commitments | Replan stores interruption fact and new proposal; only approved apply changes calendar | Recording may succeed while proposal generation fails; report both, do not erase fact or claim calendar changed | ``` Section: `## Personal policy defaults` (line 50): ```markdown Policy exceptions are explicit, scoped and recorded with reason if supplied. No guilt-driven messaging, inferred completion, silent deletion or automatic idea promotion. Life-area categories are not diagnoses or rankings of human worth. ``` ### 2. Domain Entities Specification (`docs/technical/domain-model.md`) Section: `## Entities` (lines 7–18): ```markdown | Entity | Meaning and authoritative home | |---|---| | Interruption | User-reported timeline fact: actual start/end if known, affected items and reason if supplied | | Plan | SQLite proposal artifact: intended outcomes, capacity observations and proposed allocations | | Proposal | Immutable SQLite review artifact: baseline versions, ordered steps, reasons, expiry and review digest | | Timeline record | Append-only fact with event kind, evidence class, recording time, nullable actual time and correction linkage | ``` Section: `## Time and work outcomes` (lines 39–42): ```markdown Dates and instants are distinct tagged values: `{"kind":"date","value":"2030-04-12"}` or `{"kind":"instant","value":"2030-04-12T05:00:00Z"}`. Examples are fictional. A date-only deadline is displayed as that local date, not silently converted to midnight UTC. Its planning cutoff is the following local midnight, with inclusive human meaning documented in Calendar. 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. ``` ### 3. CLI and API Route & Payloads (`docs/technical/cli-api.md`) Section: `## Command-to-route table` (line 44): ```markdown | `life replan --input -` | POST /v1/replans | write: fact/proposal, not calendar | ``` Section: `## Command payloads and results` (line 91): ```markdown | Replan | interruption `{item_ids,occurred_at?,ended_at?,reason?}`, remaining intended outcomes, optional policy exceptions | Interruption event ID, proposal or generation error; no automatic calendar mutation | ``` ### 4. Database Schema & Triggers (`docs/technical/timeline-consistency.md`) Section: `## Illustrative schema` (lines 37–75): ```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; CREATE TABLE proposals ( proposal_id TEXT PRIMARY KEY, schema_version INTEGER NOT NULL, state TEXT NOT NULL CHECK(state IN ('pending','applied','rejected','expired','partial')), digest TEXT NOT NULL, created_at TEXT NOT NULL, expires_at TEXT NOT NULL, steps_json TEXT NOT NULL, policy_json TEXT NOT NULL, operation_id TEXT REFERENCES operations(operation_id), approval_json TEXT ); ``` ### 5. Verification Scenario W08 (`docs/technical/verification.md`) Section: `## Workflow scenarios` (line 22): ```markdown | ID | Requirement and specification | Input / action | Observable acceptance | |---|---|---|---| | W08 | Interruption (workflows.md#planning-and-interruption) | Explicit interruption with unknown end; later corrected actual end | User fact recorded; uncertain capacity disclosed; calendar moves require a new reviewed proposal | ``` ## What to build ### 1. Interruption Event Recorder (`src/domain/timeline/interruption.rs`) - Parse the incoming interruption payload: - `item_ids`: Array of affected scheduled work block IDs or task IDs. - `occurred_at`: Optional tagged instant or ISO-8601 timestamp marking the start of the interruption (left null if omitted; do not invent an actual time, per `docs/technical/timeline-consistency.md:119`; `recorded_at` captures reception time). - `ended_at`: Optional tagged instant or ISO-8601 timestamp marking the end of the interruption. May be `null` if the interruption is ongoing or the end is unknown. - `reason`: Optional human-supplied explanation string (e.g., "Urgent production outage"). - Construct an append-only timeline event: - `event_id`: Unique prefixed ID (`evt_int_...`). - `dedupe_key`: Derived from operation ID and event index (`op_replan_...:fact:0`). - `schema_version`: `1`. - `kind`: `"interruption_logged"`. - `evidence_class`: `"user_report"`. - `recorded_at`: Current UTC timestamp. - `occurred_at`: The supplied `occurred_at` timestamp. - `actor`: `"user"`. - `source`: `"cli_api"`. - `after_json`: Serialized JSON payload containing `{affected_item_ids, occurred_at, ended_at, reason}`. - `provenance_json`: Metadata capturing client request ID and user reporting channel. - Execute SQL insert into `life_events`. This insert occurs inside a dedicated transaction that is committed regardless of whether subsequent replan calculation succeeds. ### 2. Remaining Capacity Evaluator (`src/domain/policy/replan_capacity.rs`) - Determine effective planning anchor: - If `ended_at` is provided, the anchor is $\max(\text{ended\_at}, \text{current\_time})$. - If `ended_at` is `null`, the anchor is $\text{current\_time}$. Calculate remaining time until the end of the configured working window, but mark capacity confidence as uncertain. - Query live Radicale calendar for the remainder of the day (from anchor to configured working window end): - Identify scheduled work blocks that were truncated or overrun by the interruption. - Identify scheduled work blocks starting strictly after the anchor. - Subtract remaining opaque busy events. - Apply the 40% unscheduled reserve rule to the remaining available window: $$\text{Remaining Gross Window} = \text{Window End} - \text{Anchor}$$ $$\text{Remaining Available} = \max(0, \text{Remaining Gross Window} - \text{Remaining Opaque Busy})$$ $$\text{Remaining Reserve} = 0.40 \times \text{Remaining Available}$$ $$\text{Remaining Allocatable} = 0.60 \times \text{Remaining Available}$$ - If `ended_at` is unknown, generate warning `unknown_estimate` and `incomplete_calendar` to explicitly disclose that capacity cannot be guaranteed. ### 3. Schedule Re-Sequencer & Proposal Builder (`src/domain/replan.rs`) - Take remaining intended outcomes: - For work blocks overrun by the interruption: propose marking the work block as interrupted/truncated or shifting it. - Propose moving unstarted work blocks to open slots in the remaining allocatable capacity window. - If remaining allocatable capacity is insufficient for all remaining outcomes: - Retain high-priority career floor items. - Propose dropping or unscheduling lower-priority blocks back to open task status (`NEEDS-ACTION`) with `planned_start: null`. - Do NOT silently delete any tasks. - Compile proposal mutation steps: - Each step specifies `step_id`, `action` (`update_event`, `cancel_event`, `update_task`), `item_id`, `collection_id`, `baseline_etag`, and `payload`. - Compute canonical SHA-256 digest over the ordered steps, baseline versions, and scoped policy exceptions (`docs/technical/cli-api.md:104`). - Store proposal in SQLite `proposals` table with `state: "pending"`. - Under NO circumstance write or patch any VEVENT or VTODO in Radicale. ### 4. Axum Route & CLI Interface (`src/api/replans.rs`, `src/cli/replan.rs`) - Implement `POST /v1/replans`: - Request body: ```json { "interruption": { "item_ids": ["evt_block_1"], "occurred_at": "2026-09-23T10:15:00Z", "ended_at": null, "reason": "Production database incident" }, "remaining_intended_outcomes": ["itm_task2"], "policy_exceptions": [] } ``` - Response (HTTP 200 OK): ```json { "schema_version": 1, "request_id": "req_replan_123", "status": "ok", "data": { "interruption_event_id": "evt_int_987", "proposal": { "proposal_id": "prop_replan_456", "digest": "sha256:4a3b8...", "state": "pending", "created_at": "2026-09-23T10:16:00Z", "expires_at": "2026-09-24T10:16:00Z", "steps": [ { "step_id": "step_1", "action": "update_event", "item_id": "evt_block_2", "collection_id": "personal", "baseline_etag": "\"etag-block2\"", "payload": { "start": {"kind": "instant", "value": "2026-09-23T14:00:00Z"}, "end": {"kind": "instant", "value": "2026-09-23T15:30:00Z"} }, "reason": "Shift focus block 2 hours forward due to production interruption" } ] }, "coverage": [{"collection_id": "personal", "state": "complete", "as_of": "2026-09-23T10:15:00Z"}] }, "warnings": [ { "code": "unknown_estimate", "message": "Interruption has no recorded end time; remaining capacity is uncertain" } ], "error": null } ``` - If proposal generation fails (e.g., CalDAV read error), return HTTP 200 with `status: "partial"`, data containing `interruption_event_id`, and `error` explaining proposal generation failure. - Implement CLI command: `life replan --input - [--json]` - Stdin takes the replan JSON payload. ## Acceptance Criteria - [x] Calling `life replan` inserts a row into `life_events` with `kind: "interruption_logged"` and `evidence_class: "user_report"` (verifying Scenario W08). - [x] Supplying an interruption with `ended_at: null` records the event with `ended_at` as null and returns an `unknown_estimate` warning regarding capacity uncertainty. - [x] If calendar read or proposal generation fails, the interruption event is confirmed committed in SQLite and is NOT rolled back. - [x] Replan proposal generation calculates remaining capacity from the interruption anchor and applies the 40% unscheduled reserve. - [x] Replan generation performs ZERO writes or updates to Radicale CalDAV storage. - [x] Returned proposal includes a unique `proposal_id`, state `"pending"`, and canonical SHA-256 digest covering ordered steps, baseline ETags, and policy exceptions (`docs/technical/cli-api.md:104`). - [x] An omitted `occurred_at` remains null in the recorded interruption event without inventing an actual time (`docs/technical/timeline-consistency.md:119`). ## Notes - The interruption fact is recorded through the new reusable `journal::record_fact_operation` (operations row + `life_events` append in one transaction, with fingerprint replay detection) — available for #28/#29. - Fact durability when generation fails is proven for two different failure modes, and the response reports both outcomes separately with `status: partial` (exit 6). - Replan reuses `plans::generate` with the anchor's remaining day and the same `ceil(remaining × 40%)` reserve; the prior proposal row is asserted byte-identical. - Zero-write property proven both by an all-`REPORT` request assertion and by per-table deltas. - No dependency added. ## Blocked by - #025: Planning Proposal Engine & Capacity Reserve Calculations
yuris changed title from Issue #026: Interruption Fact Logging & Schedule Replanning to Interruption Fact Logging & Schedule Replanning 2026-09-22 20:02:35 +00:00
yuris closed this issue 2026-09-23 02:19:47 +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#26
No description provided.