feat(api): append-only timeline fact corrections #73
Loading…
Add table
Add a link
Reference in a new issue
No description provided.
Delete branch "feat/issue-29-corrections"
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
Human reports are fallible: a duration gets misreported, an outcome is wrong, an interruption is logged against the wrong block. The journal is append-only, so a correction must never be a history rewrite — it has to be a new fact that names the one it corrects, while the original stays readable.
POST /v1/capturesalready acceptedkind: correctionand refused it as not implemented (#28 left that refusal as the fixture),life timelinewas an unimplemented command, andview=correctedwas refused. This implements the correction workflow (docs/technical/workflows.md#lifecycle-and-review, scenario W14) together with its refusals (scenario R13): an unknown target, a cyclic chain, a cross-kind correction, and any attempt to rewrite a confirmed transport fact.Changes
src/domain/timeline/correction.rs(new): the correction vocabulary, pure — the payload, the target's evidence-class and kind validation, the replacement vocabulary per fact kind, the appendedlife_eventsrow (correction_target,before_json,after_json,evidence_class: user_report), and the bounded chain resolution (MAX_CORRECTION_HOPS = 16, the discipline #18 established for supersession).src/api/correction.rs(new) andsrc/api/captures.rs: the correction part of the capture family. It reads the target from the journal, refuses an unknown target, aconfirmed_service/observed_externaltarget, a cross-kind correction and an unresolvable chain asinvalid_inputwith the reason stated, appends one row throughJournalService::record_fact_operation, and replays idempotently. It makes no calendar request of any kind.src/history/timeline.rs:correction_targeton the timeline projection,correction_edges/correction_targetreads, andtimeline_corrected— the folded read over the same query, one entry per chain, with the original carried, the chain listed and the correction that supplied each value named.src/api/timeline.rsandsrc/api/mod.rs:viewparsed with the rest of the filters (raw|corrected), the folded read wired in, the view reported in the response, and the pagination cursor fingerprint bound to the view.src/cli.rs:life timelineimplemented (--from,--to,--occurred-from,--occurred-to,--item-id,--evidence-class,--limit,--cursor,--corrected,--view), with a human-readable table that marks correction linkage and folded provenance.skills/life/references/commands.md:life timelinemoved from the unimplemented table to reads, the capture kinds and the correction payload documented, and the stale "outcome/correction are refused" claim removed.tests/correction_tests.rs(10 integration tests over a mock CalDAV, the real router and the real binary) and 13 unit tests in the domain module;tests/outbox_tests.rs's now-obsolete correction refusal fixture removed (no capture kind is refused any more);tests/replan_tests.rs's corrected-view assertion moved to the implemented behaviour.Design decisions worth a reviewer's eye:
doneis refused, not recorded as an outcome); the link members (item_id,related_task_id,linkage) and the derivedended_atare refused, because a correction corrects the assertion and the link the fact was recorded with stands.user_report. Itsevidencemember is the user's own words, never a claim of service evidence — a payload cannot promote an assertion toconfirmed_service.invalid_input, bound stated) and the folded read refuses to fold rather than spinning or inventing a value.Verification
nix develop --command cargo test— 704 tests across 37 suites, all passing (was 681);cargo clippy --all-targets -- -D warningsandcargo fmt --checkclean.Per acceptance criterion, all observed through the real path (in-process daemon, mock CalDAV, real CLI):
tests/correction_tests.rs::a_correction_appends_a_linked_row_and_leaves_the_corrected_one_untouchedcompares the corrected row's stored bytes (every column, raw JSON included) and its sequence before and after, and exercises the triggers directly: a rawUPDATEand a rawDELETEonlife_eventsboth abort withappend-only events, leaving the row unchanged.invalid_input(HTTP 400, CLI exit 2) with nothing appended; a cyclic chain is refused with the depth bound stated (16 links), asserted by planting a loop directly in SQLite (the service cannot produce one) and checking both that the correction is refused and that the folded read refuses to fold it.confirmed_servicefact (a task created and acknowledged by the mock backend) cannot be corrected: the refusal says the recorded fact stays as recorded and where the disagreement belongs, the original row and the row count are untouched, no projection changed, and the user can record their disagreement as a separate fact beside it.item_projection,sync_stateandoutboxrow counts are unchanged.correction_target; the corrected view folds to one entry that carries the original's identity and assertion, the chain,corrected_byandcorrection_chain_length.state: replayed, no second fact or operation); the same key with a changed payload isidempotency_conflict(HTTP 409) with no row appended; a new key is a new correction and stands.evidence_class: user_report,recorded_at, target linkage and reason.life capture --input -records a correction (exit 0,evt_cor_…),life timeline --view rawshows every fact,life timeline --correctedshows the folded value with its provenance, and a contradictory or unknown view is exit 2 before any request.Documentation
skills/life/references/commands.mdis updated in the same commit (the skill package is validated against the real CLI bytests/skill_package_tests.rs), including a correction payload example. The normative design documents were unchanged by this work.Related Issues
Closes #29
3d7ee54fd2to756c796f6fCoordinator review — APPROVED
Verified
756c796myself:cargo test704 passed / 0 failed (23 new correction tests),cargo clippy --all-targets -- -D warnings0 errors,cargo fmt --checkclean.What I checked beyond the summary:
a_correction_appends_a_linked_row_and_leaves_the_corrected_one_untouchedchecks the original row survives alongside the new one — and the schema'slife_events_no_update/life_events_no_deletetriggers from #4 still stand behind it, so "we chose not to update" is not the only defence.a_confirmed_service_fact_cannot_be_rewritten_by_a_user_assertionproves a recorded service fact cannot be overwritten by a later user assertion. That is the boundary that keepsconfirmed_servicemeaning something, and it is enforced by vocabulary (CONFIRMED_SERVICEin the domain) rather than by convention.MAX_CORRECTION_HOPS = 16with the failure message naming the bound, and tests that construct a chain longer than the bound — the same bounded-chain discipline #18 uses for supersession, applied to a graph that is now user-shapeable.Supporting suites that pinned the older behaviour (
tests/outbox_tests.rs,tests/replan_tests.rs) and the skill's CLI reference were updated in the same commit, so the#17refusal fixture now reflects that onlykind: correctionremained unimplemented — and that is no longer true.Merging.
#30(weekly review analytics) is the last Phase 4 slice.feat(api): append-only timeline fact corrections (#29)to feat(api): append-only timeline fact corrections