feat(tasks): delete, promote and duplicate (B3) #798

Closed
kayg wants to merge 1 commit from feat/tasks-delete-promote-duplicate into dev
Owner

round 2 final fix

Shared actor byte/kind validation guards task splices and Trash; structural child markers cannot hide owners, and promotion scans the full block extent before claiming a task. Both reported P1 probes are permanent regressions. Final gates pass: 598 package tests, 23 task-operation tests, fmt, clippy, contract and action types. Never merged; ready for final independent review.

Full status, exact commands, decisions and limits: round 2 fix report.

Tasks B3 implementation plan

Base: origin/dev; no B1/B2 stack required. Backend only.

  1. Add typed delete, promote and duplicate actions following tasks.create job keys and notes bulk plans.
  2. Reuse backend Markdown parsing through notes; use the notes job/room boundary for durable multi-file changes. Promotion preserves parent prose and children, writes a child link and quoted origin. Resolve an existing owner before allocating another.
  3. Delete owning checkbox lines everywhere and only task relationship bullets from sessions. Persist exact removed spans, source identity/version, surrounding anchors and target bindings in the plan and Trash replay record. Restore by identity, then surviving anchors, then today's daily note. Undo uses the same restore path.
  4. Duplicate with fresh note/block identities and origin; clear outcome/source authority and hidden blocks. Preserve ordinary shared references.
  5. Add action integration tests for byte preservation, exact Undo, moved anchors, replay/retry, isolation and duplicate identities. Run all gates on a free dev VM using Nix and the build slot wrapper.

Expected size: L, at most 1,500 application lines plus tests. Planning scans backlink candidate notes; current bound is O(items × candidate bytes + items²) because ownership checks reuse the existing link reader. Restore searches parsed block identities Home-wide, O(removed spans × Home bytes). No performance result is claimed. Each job holds one Home job lock and uses existing room/version and step receipts.

Failure Effect Detection Recovery Test
Crash after plan or room write Partial file graph Stored running job and step receipt Replay same identities/steps Crash boundaries and repeat action
Retry or concurrent Track Duplicate owner risk Resolve current checkbox ownership under job lock Return original owner Two request keys
Source edited or renamed Stale position/path Note UUID, version and anchors Surviving anchor placement; retain other bytes Edited/renamed source
Block moved before Restore Duplicate line risk Home-wide marker search Reuse moved identity Moved anchor/line
Source deleted No insertion destination Missing source UUID Daily fallback plus report Deleted source
Malformed/unsupported metadata Unsafe rewrite risk Validate before durable plan Refuse without changes Preservation fixtures
Duplicate carries source authority Second upstream identity Explicit cleared property set and fresh markers New local task only Duplicate source/log/IDs

Gates (dev-01, slot, Nix): cargo fmt --all -- --check; cargo clippy -j4 -p calternal-tasks -p calternal-notes -p calternal --all-targets -- -D warnings; cargo test -j4 -p calternal-tasks -p calternal-notes -p calternal; contract tests from the repository runner. No merge.

Implementation decisions:

  • Reuse the existing backend pulldown-cmark parser used by the task index, and the notes::props writer. The web editor uses Lezer; no JavaScript parser runtime is added to the Rust backend.
  • Refuse unusual frontmatter before promotion/duplication: comments, flow containers, anchors, block scalars, nested maps, and quotes within plain scalars. Accept simple scalars and writer-compatible block lists. No source normalization is attempted.
  • Refuse ambiguous literal backticks reported by the Markdown parser, including a multiline opener split by a list boundary. Fences, inline code, HTML and reference comments are preservation corpus cases.
  • Keep unrelated child text and session prose byte-identical. Delete removes only owning checkbox lines and relationship bullets; its Trash record pins exact source spans/version, anchors and target UUID bindings.
  • Match Undo to the deletion generation, so undoing an older delete cannot restore a later one. Release the lifecycle lock before line restoration may create its fallback daily note.
  • A duplicate with no owning line receives a new line in today's daily note. Ordinary references remain shared, outcome/source authority is cleared, and original block markers are omitted from the copied body. Its owning line gets a new marker.

Validation on dev-01 (tasks-b3-gates3, exit 0):

  • cargo clippy -j4 -p calternal-tasks -p calternal-notes -p calternal --all-targets -- -D warnings passed.
  • cargo test -j4 -p calternal-tasks -p calternal-notes -p calternal passed: 556 tests, 10 existing ignored tests, no failures.
  • cargo test -j4 -p calternal contract passed: one contract test.
  • node apps/web/scripts/action-types.mjs --check passed.
  • cargo fmt --all -- --check passed.

The corpus covers accepted metadata preservation, refused unusual YAML, code spans/fences/comments, BOM/CRLF and no final newline. Job crash tests kill and restart at each planned step; restore tests cover moved anchors, renamed/deleted sources and deletion generations. No latency measurement or independent review is claimed.

Result: registers tasks.delete, tasks.promote and tasks.duplicate with durable job receipts and notes.undo. Promotion retains parent prose and person links, appends a block marker when needed for its backlink, and adds a child link. Delete sends the note to Trash with exact removed-line records. Duplicate creates a fresh note and owning-line identity, stripping copied block IDs and upstream authority.

Limits: YAML support is intentionally narrower than valid YAML, including refusal of nested maps. Markdown handling reuses the existing Rust task-index parser (pulldown-cmark); it does not embed the web editor's Lezer parser. Ambiguous unmatched backticks refuse the action. Planning and restoration scan existing notes as described above; no performance measurement was run. Review pending; never merge as part of this task.

## round 2 final fix Shared actor byte/kind validation guards task splices and Trash; structural child markers cannot hide owners, and promotion scans the full block extent before claiming a task. Both reported P1 probes are permanent regressions. Final gates pass: 598 package tests, 23 task-operation tests, fmt, clippy, contract and action types. Never merged; ready for final independent review. Full status, exact commands, decisions and limits: [round 2 fix report](https://git.kayg.org/kayg/calternal/pulls/798#issuecomment-71378). # Tasks B3 implementation plan Base: `origin/dev`; no B1/B2 stack required. Backend only. 1. Add typed delete, promote and duplicate actions following tasks.create job keys and notes bulk plans. 2. Reuse backend Markdown parsing through notes; use the notes job/room boundary for durable multi-file changes. Promotion preserves parent prose and children, writes a child link and quoted origin. Resolve an existing owner before allocating another. 3. Delete owning checkbox lines everywhere and only task relationship bullets from sessions. Persist exact removed spans, source identity/version, surrounding anchors and target bindings in the plan and Trash replay record. Restore by identity, then surviving anchors, then today's daily note. Undo uses the same restore path. 4. Duplicate with fresh note/block identities and origin; clear outcome/source authority and hidden blocks. Preserve ordinary shared references. 5. Add action integration tests for byte preservation, exact Undo, moved anchors, replay/retry, isolation and duplicate identities. Run all gates on a free dev VM using Nix and the build slot wrapper. Expected size: L, at most 1,500 application lines plus tests. Planning scans backlink candidate notes; current bound is O(items × candidate bytes + items²) because ownership checks reuse the existing link reader. Restore searches parsed block identities Home-wide, O(removed spans × Home bytes). No performance result is claimed. Each job holds one Home job lock and uses existing room/version and step receipts. | Failure | Effect | Detection | Recovery | Test | | --- | --- | --- | --- | --- | | Crash after plan or room write | Partial file graph | Stored running job and step receipt | Replay same identities/steps | Crash boundaries and repeat action | | Retry or concurrent Track | Duplicate owner risk | Resolve current checkbox ownership under job lock | Return original owner | Two request keys | | Source edited or renamed | Stale position/path | Note UUID, version and anchors | Surviving anchor placement; retain other bytes | Edited/renamed source | | Block moved before Restore | Duplicate line risk | Home-wide marker search | Reuse moved identity | Moved anchor/line | | Source deleted | No insertion destination | Missing source UUID | Daily fallback plus report | Deleted source | | Malformed/unsupported metadata | Unsafe rewrite risk | Validate before durable plan | Refuse without changes | Preservation fixtures | | Duplicate carries source authority | Second upstream identity | Explicit cleared property set and fresh markers | New local task only | Duplicate source/log/IDs | Gates (dev-01, slot, Nix): `cargo fmt --all -- --check`; `cargo clippy -j4 -p calternal-tasks -p calternal-notes -p calternal --all-targets -- -D warnings`; `cargo test -j4 -p calternal-tasks -p calternal-notes -p calternal`; contract tests from the repository runner. No merge. Implementation decisions: - Reuse the existing backend `pulldown-cmark` parser used by the task index, and the `notes::props` writer. The web editor uses Lezer; no JavaScript parser runtime is added to the Rust backend. - Refuse unusual frontmatter before promotion/duplication: comments, flow containers, anchors, block scalars, nested maps, and quotes within plain scalars. Accept simple scalars and writer-compatible block lists. No source normalization is attempted. - Refuse ambiguous literal backticks reported by the Markdown parser, including a multiline opener split by a list boundary. Fences, inline code, HTML and reference comments are preservation corpus cases. - Keep unrelated child text and session prose byte-identical. Delete removes only owning checkbox lines and relationship bullets; its Trash record pins exact source spans/version, anchors and target UUID bindings. - Match Undo to the deletion generation, so undoing an older delete cannot restore a later one. Release the lifecycle lock before line restoration may create its fallback daily note. - A duplicate with no owning line receives a new line in today's daily note. Ordinary references remain shared, outcome/source authority is cleared, and original block markers are omitted from the copied body. Its owning line gets a new marker. Validation on dev-01 (`tasks-b3-gates3`, exit 0): - `cargo clippy -j4 -p calternal-tasks -p calternal-notes -p calternal --all-targets -- -D warnings` passed. - `cargo test -j4 -p calternal-tasks -p calternal-notes -p calternal` passed: 556 tests, 10 existing ignored tests, no failures. - `cargo test -j4 -p calternal contract` passed: one contract test. - `node apps/web/scripts/action-types.mjs --check` passed. - `cargo fmt --all -- --check` passed. The corpus covers accepted metadata preservation, refused unusual YAML, code spans/fences/comments, BOM/CRLF and no final newline. Job crash tests kill and restart at each planned step; restore tests cover moved anchors, renamed/deleted sources and deletion generations. No latency measurement or independent review is claimed. Result: registers `tasks.delete`, `tasks.promote` and `tasks.duplicate` with durable job receipts and `notes.undo`. Promotion retains parent prose and person links, appends a block marker when needed for its backlink, and adds a child link. Delete sends the note to Trash with exact removed-line records. Duplicate creates a fresh note and owning-line identity, stripping copied block IDs and upstream authority. Limits: YAML support is intentionally narrower than valid YAML, including refusal of nested maps. Markdown handling reuses the existing Rust task-index parser (`pulldown-cmark`); it does not embed the web editor's Lezer parser. Ambiguous unmatched backticks refuse the action. Planning and restoration scan existing notes as described above; no performance measurement was run. Review pending; never merge as part of this task.
docs: plan tasks delete promote and duplicate
Some checks failed
CI / server (pull_request) Has been cancelled
CI / rust (pull_request) Has been cancelled
CI / web (pull_request) Has been cancelled
3890fa905d
kayg changed title from WIP: tasks delete, promote and duplicate (B3) to feat(tasks): delete, promote and duplicate (B3) 2026-10-31 18:03:59 +00:00
Author
Owner

Independent review, round 1

Verdict: fix first. Reviewed head 7e063cf02afd61a1f3d762752bd3b6559d60b64c against the PR plan, ADRs 0066/0056/0068/0067 (including the → amendment), and the tasks decision memory. No P0 findings. Four P1 findings and one P2 finding below are reproduced by independent probes.

  1. P1: delete removes ordinary reference bullets, not just owners and session relationships. crates/notes/src/task_ops.rs:173–177. A note containing # Reference reading followed by a top-level - [Task](<Task.md>) loses that bullet when Task is deleted. There is no owning checkbox or event/session parent. The non-checkbox branch checks only the link target, so unrelated lists are edited. Fix: classify the parent/container and remove standalone relationship bullets only under an owning checkbox or a supported session; preserve ordinary references. Probe: review_r1_delete_keeps_ordinary_reference_bullets.

  2. P1: duplicate reuses block IDs on paragraph continuation lines. crates/notes/src/task_blocks.rs:111–121, consumed by crates/notes/src/task_ops.rs:516–530. A task body first line\nlast line ^bbbbbbbbbbbbbbbb\n is copied with that exact ID still present. marker_blocks examines only each parsed block's first physical line, so the copy filter misses this marker and creates a second Home-wide identity. The same omission affects hidden-block filtering. Fix: recognize marker positions throughout supported block ranges with the shared marker rules, and remove/re-key copied identities; otherwise refuse unsupported marker layouts before creating anything. Probe: review_r1_duplicate_multiline_block_id_is_fresh.

  3. P1: a stale promotion still mutates the source and reports success. crates/notes/src/task_ops.rs:100–105,122–129; crates/notes/src/bulk/steps.rs:108–125. Pause at splice-before-turn, change - [ ] Task ^bbbbbbbbbbbbbbbb to - [ ] Changed ^bbbbbbbbbbbbbbbb, then resume. The job returns state: done and inserts the old [Task] relationship beneath Changed. For an existing marker the splice is insertion-only, so the destructive-undo guard has no deleted span to validate; the original source hash/version is never rechecked at mutation time. Fix: enforce the source block/version precondition inside the actor turn for insertion-only promotion too, propagate refusal rather than a successful kept result, and ensure a failed source claim cannot publish an orphan owner. Probe: review_r1_promote_stale_source_must_not_report_success.

  4. P1: delete Undo is not byte-exact even without intervening edits. crates/notes/src/task_ops/restore.rs:202–222. Starting with - [Task](Task.md), delete then notes.undo restores - [Task](<Task.md>). Every bound link is unconditionally regenerated despite unchanged source/target paths, so the stored exact span is not actually restored. Fix: reuse the saved raw link spelling when it still resolves to the retained UUID; rewrite only destinations that must change after relocation/path reuse, preserving other bytes. Probe: review_r1_delete_undo_preserves_link_spelling.

  5. P2: the YAML refusal guard still has the plain-list interior-quote bypass. crates/notes/src/task_ops.rs:575–578. custom:\n - has "quote" within\n passes simple_frontmatter; the scalar branch rejects interior quotes, but the list branch merely compares the existing writer's rendering. That writer also emits interior quotes bare, so round-trip equality is not a safety proof. This contradicts the plan's refusal policy and leaves the specifically requested calendar-regression bypass open. Fix: apply lexical checks to each raw plain list item too, including quote/comment interactions; accept properly quoted supported scalars or refuse before planning. Probe: review_r1_yaml_list_quotes_must_refuse.

Validation

All commands ran on dev-02 in an isolated, uncommitted worktree, using Nix and ~/bin/slot; no code was pushed.

  • cargo test -j4 -p calternal-notes task_ops::tests -- --skip review_r1: 4 passed, including kill-9/replay, deletion generations, renamed/deleted sources and moved anchors.
  • cargo test -j4 -p calternal-tasks mutation_tests: 7 passed, including the existing YAML/code/comment corpus and BOM/CRLF/exact-undo fixture.
  • cargo test -j4 -p calternal-notes review_r1 -- --nocapture: 2 passed, 5 failed. The passing independent cases cover exact restore around surviving children and promotion with an existing descriptive → child, including a second request returning the same owning task.

Probe source: cua@calternal-dev-02-std:~/Developer/Worktrees/review-798-r1/crates/notes/src/task_ops/tests.rs (uncommitted). Logs: ~/review-798-r1-finalprobes.log, ~/review-798-r1-existing.log, ~/review-798-r1-actions.log on that VM. Run from the worktree with export PATH=/nix/var/nix/profiles/default/bin:$PATH; nix develop -c bash -c '~/bin/slot cargo test -j4 -p calternal-notes review_r1 -- --nocapture'. Full lint/workspace gates were not rerun for this review.

## Independent review, round 1 **Verdict: fix first.** Reviewed head `7e063cf02afd61a1f3d762752bd3b6559d60b64c` against the PR plan, ADRs 0066/0056/0068/0067 (including the → amendment), and the tasks decision memory. No P0 findings. Four P1 findings and one P2 finding below are reproduced by independent probes. 1. **P1: delete removes ordinary reference bullets, not just owners and session relationships.** `crates/notes/src/task_ops.rs:173–177`. A note containing `# Reference reading` followed by a top-level `- [Task](<Task.md>)` loses that bullet when Task is deleted. There is no owning checkbox or event/session parent. The non-checkbox branch checks only the link target, so unrelated lists are edited. **Fix:** classify the parent/container and remove standalone relationship bullets only under an owning checkbox or a supported session; preserve ordinary references. Probe: `review_r1_delete_keeps_ordinary_reference_bullets`. 2. **P1: duplicate reuses block IDs on paragraph continuation lines.** `crates/notes/src/task_blocks.rs:111–121`, consumed by `crates/notes/src/task_ops.rs:516–530`. A task body `first line\nlast line ^bbbbbbbbbbbbbbbb\n` is copied with that exact ID still present. `marker_blocks` examines only each parsed block's first physical line, so the copy filter misses this marker and creates a second Home-wide identity. The same omission affects hidden-block filtering. **Fix:** recognize marker positions throughout supported block ranges with the shared marker rules, and remove/re-key copied identities; otherwise refuse unsupported marker layouts before creating anything. Probe: `review_r1_duplicate_multiline_block_id_is_fresh`. 3. **P1: a stale promotion still mutates the source and reports success.** `crates/notes/src/task_ops.rs:100–105,122–129`; `crates/notes/src/bulk/steps.rs:108–125`. Pause at `splice-before-turn`, change `- [ ] Task ^bbbbbbbbbbbbbbbb` to `- [ ] Changed ^bbbbbbbbbbbbbbbb`, then resume. The job returns `state: done` and inserts the old `[Task]` relationship beneath Changed. For an existing marker the splice is insertion-only, so the destructive-undo guard has no deleted span to validate; the original source hash/version is never rechecked at mutation time. **Fix:** enforce the source block/version precondition inside the actor turn for insertion-only promotion too, propagate refusal rather than a successful kept result, and ensure a failed source claim cannot publish an orphan owner. Probe: `review_r1_promote_stale_source_must_not_report_success`. 4. **P1: delete Undo is not byte-exact even without intervening edits.** `crates/notes/src/task_ops/restore.rs:202–222`. Starting with ` - [Task](Task.md)`, delete then `notes.undo` restores ` - [Task](<Task.md>)`. Every bound link is unconditionally regenerated despite unchanged source/target paths, so the stored exact span is not actually restored. **Fix:** reuse the saved raw link spelling when it still resolves to the retained UUID; rewrite only destinations that must change after relocation/path reuse, preserving other bytes. Probe: `review_r1_delete_undo_preserves_link_spelling`. 5. **P2: the YAML refusal guard still has the plain-list interior-quote bypass.** `crates/notes/src/task_ops.rs:575–578`. `custom:\n - has "quote" within\n` passes `simple_frontmatter`; the scalar branch rejects interior quotes, but the list branch merely compares the existing writer's rendering. That writer also emits interior quotes bare, so round-trip equality is not a safety proof. This contradicts the plan's refusal policy and leaves the specifically requested calendar-regression bypass open. **Fix:** apply lexical checks to each raw plain list item too, including quote/comment interactions; accept properly quoted supported scalars or refuse before planning. Probe: `review_r1_yaml_list_quotes_must_refuse`. ### Validation All commands ran on dev-02 in an isolated, uncommitted worktree, using Nix and `~/bin/slot`; no code was pushed. - `cargo test -j4 -p calternal-notes task_ops::tests -- --skip review_r1`: **4 passed**, including kill-9/replay, deletion generations, renamed/deleted sources and moved anchors. - `cargo test -j4 -p calternal-tasks mutation_tests`: **7 passed**, including the existing YAML/code/comment corpus and BOM/CRLF/exact-undo fixture. - `cargo test -j4 -p calternal-notes review_r1 -- --nocapture`: **2 passed, 5 failed**. The passing independent cases cover exact restore around surviving children and promotion with an existing descriptive → child, including a second request returning the same owning task. Probe source: `cua@calternal-dev-02-std:~/Developer/Worktrees/review-798-r1/crates/notes/src/task_ops/tests.rs` (uncommitted). Logs: `~/review-798-r1-finalprobes.log`, `~/review-798-r1-existing.log`, `~/review-798-r1-actions.log` on that VM. Run from the worktree with `export PATH=/nix/var/nix/profiles/default/bin:$PATH; nix develop -c bash -c '~/bin/slot cargo test -j4 -p calternal-notes review_r1 -- --nocapture'`. Full lint/workspace gates were not rerun for this review.
Author
Owner

round 1 fixes

head 0968bbff9751862b4fad4de41e59bcd580636d42; addresses review 71163. never merged.

finding change regression status
P1 ordinary reference bullets removed parsed owner/session containers; remove only task relationships and owning prose; preserve ordinary child subtrees, including children below a relationship pass: review_r1_delete_keeps_ordinary_reference_bullets, continuation/child and session cases
P1 continuation IDs copied inspect every physical prose line within parsed block ranges; rekey copied identities; remove complete hidden blocks; retain literal code pass: review_r1_duplicate_multiline_block_id_is_fresh, hidden/code case
P1 stale promotion succeeds durable exact-source precondition checked after outside ingestion inside the actor; changed/missing sources fail; claim source before creating owner pass: review_r1_promote_stale_source_must_not_report_success, strengthened to check unchanged source prose and no orphan owner
P1 Undo rewrites link spelling restore captured raw spans; preserve original links while their destinations still resolve to retained UUIDs; only replace destinations when relocation requires it pass: review_r1_delete_undo_preserves_link_spelling, renamed/deleted-source and moved-anchor cases
P2 YAML list quote bypass explicit plain-value allowlist applied to scalars and every raw list item; supported JSON quotes checked separately; other formatting refuses before planning pass: review_r1_yaml_list_quotes_must_refuse

The exact reviewer probe set was reproduced first: 2 passed, 5 failed. All seven probes are retained. The final task-operation run passes 14/14, including existing crash/replay, source rename/deletion and deletion-generation tests, plus continuation/nested-child, hidden/code and open/legacy session cases. The added nested-child and legacy-session probes were also observed failing before their fixes.

Decisions: settle pending server title/metadata before pinning snapshots, then refuse subsequent source changes. Whole-source preconditions deliberately also refuse unrelated edits. Promotion/duplication claim the source before creating an owner, retaining durable replay order. Move the existing daily log reader unchanged into notes and re-export its API from daily, so session classification shares all supported clock forms without a dependency cycle. cmp confirmed the moved reader is byte-identical. Full design/failure notes are in docs/plans/tasks-b3.md.

All final commands ran on dev-01, through ~/bin/slot, with the Nix dev shell. The temporary flake files were not committed.

gate exact command result
operation regressions/replay cargo test -j4 -p calternal-notes task_ops::tests -- --nocapture pass, 14/14
lint cargo clippy -j4 -p calternal-tasks -p calternal-notes -p calternal -p calternal-daily --all-targets -- -D warnings pass
package tests, including daily shared vectors cargo test -j4 -p calternal-tasks -p calternal-notes -p calternal -p calternal-daily pass, 589 passed / 10 existing ignored
action contract cargo test -j4 -p calternal contract pass
action types node apps/web/scripts/action-types.mjs --check pass
format cargo fmt --all -- --check pass

Final log: claude@calternal-dev-01-compute02:~/tasks-b3-r1-shared-final.log (GATES_PASS). Baseline probe log: ~/tasks-b3-r1-red.log.

Limits: intentionally strict YAML/source refusal; existing ambiguous-backtick refusal remains; no performance claim. Independent re-review is pending. This is the response to review round 1 of the maximum 3; no merge was performed.

## round 1 fixes head `0968bbff9751862b4fad4de41e59bcd580636d42`; addresses review [71163](https://git.kayg.org/kayg/calternal/pulls/798#issuecomment-71163). never merged. | finding | change | regression status | | --- | --- | --- | | P1 ordinary reference bullets removed | parsed owner/session containers; remove only task relationships and owning prose; preserve ordinary child subtrees, including children below a relationship | pass: `review_r1_delete_keeps_ordinary_reference_bullets`, continuation/child and session cases | | P1 continuation IDs copied | inspect every physical prose line within parsed block ranges; rekey copied identities; remove complete hidden blocks; retain literal code | pass: `review_r1_duplicate_multiline_block_id_is_fresh`, hidden/code case | | P1 stale promotion succeeds | durable exact-source precondition checked after outside ingestion inside the actor; changed/missing sources fail; claim source before creating owner | pass: `review_r1_promote_stale_source_must_not_report_success`, strengthened to check unchanged source prose and no orphan owner | | P1 Undo rewrites link spelling | restore captured raw spans; preserve original links while their destinations still resolve to retained UUIDs; only replace destinations when relocation requires it | pass: `review_r1_delete_undo_preserves_link_spelling`, renamed/deleted-source and moved-anchor cases | | P2 YAML list quote bypass | explicit plain-value allowlist applied to scalars and every raw list item; supported JSON quotes checked separately; other formatting refuses before planning | pass: `review_r1_yaml_list_quotes_must_refuse` | The exact reviewer probe set was reproduced first: **2 passed, 5 failed**. All seven probes are retained. The final task-operation run passes **14/14**, including existing crash/replay, source rename/deletion and deletion-generation tests, plus continuation/nested-child, hidden/code and open/legacy session cases. The added nested-child and legacy-session probes were also observed failing before their fixes. Decisions: settle pending server title/metadata before pinning snapshots, then refuse subsequent source changes. Whole-source preconditions deliberately also refuse unrelated edits. Promotion/duplication claim the source before creating an owner, retaining durable replay order. Move the existing daily log reader unchanged into notes and re-export its API from daily, so session classification shares all supported clock forms without a dependency cycle. `cmp` confirmed the moved reader is byte-identical. Full design/failure notes are in `docs/plans/tasks-b3.md`. All final commands ran on **dev-01**, through `~/bin/slot`, with the Nix dev shell. The temporary flake files were not committed. | gate | exact command | result | | --- | --- | --- | | operation regressions/replay | `cargo test -j4 -p calternal-notes task_ops::tests -- --nocapture` | pass, 14/14 | | lint | `cargo clippy -j4 -p calternal-tasks -p calternal-notes -p calternal -p calternal-daily --all-targets -- -D warnings` | pass | | package tests, including daily shared vectors | `cargo test -j4 -p calternal-tasks -p calternal-notes -p calternal -p calternal-daily` | pass, 589 passed / 10 existing ignored | | action contract | `cargo test -j4 -p calternal contract` | pass | | action types | `node apps/web/scripts/action-types.mjs --check` | pass | | format | `cargo fmt --all -- --check` | pass | Final log: `claude@calternal-dev-01-compute02:~/tasks-b3-r1-shared-final.log` (`GATES_PASS`). Baseline probe log: `~/tasks-b3-r1-red.log`. Limits: intentionally strict YAML/source refusal; existing ambiguous-backtick refusal remains; no performance claim. Independent re-review is pending. This is the response to review round 1 of the maximum 3; no merge was performed.
Author
Owner

Independent review, round 2

verdict: fix first. reviewed 0968bbff9751862b4fad4de41e59bcd580636d42. all five round 1 findings are verified fixed for their original reproductions. two new P1 findings remain; no P0 findings.

round 1 verification

round 1 finding status does its regression prove it?
P1 delete removed ordinary reference bullets verified yes. review_r1_delete_keeps_ordinary_reference_bullets compares exact source bytes; continuation/nested-child and shared-session tests also pass.
P1 duplicate retained continuation-line IDs verified yes. review_r1_duplicate_multiline_block_id_is_fresh exercises real duplication and rejects the original ID; the hidden-continuation/code fixture also passes.
P1 stale promotion mutated changed prose and reported success verified yes. review_r1_promote_stale_source_must_not_report_success changes the source at splice-before-turn, verifies the changed prose remains, no old relationship or orphan task appears, and success is refused. this does not cover the separate Trash write path below.
P1 delete Undo changed original link bytes verified yes. review_r1_delete_undo_preserves_link_spelling compares the whole source before/after Undo, including the bare destination spelling. rename/deleted-source/moved-anchor tests also pass.
P2 plain-list interior-quote refusal bypass verified yes. review_r1_yaml_list_quotes_must_refuse tests single/double interior quotes and the quote/comment interaction. independent expanded refusal probes also pass.

new findings

  1. P1: delete skips the exact-source guard on the task note being trashed. crates/notes/src/task_ops.rs:211–217, crates/notes/src/bulk/steps.rs:203. only the backlink sources receive guarded splices; Step::Trash carries just the task UUID. pause deletion at the source's splice-before-turn, change the task from status: todo to status: did-not-happen, ingest the outside change into its room, and verify the new status is present before resuming. deletion still removes the owning source lines, trashes the now non-task note, and reports state: done. the original require_task decision is stale, and the changed note is destructively acted on without a source precondition. fix: retain the task snapshot in the durable plan and enforce its exact text/identity precondition inside the Trash actor turn before committing deletion; propagate refusal and make partial-job recovery/Undo preserve intervening edits. probe: review_r2_delete_changed_task_must_refuse.

  2. P1: a block marker on the task-link child hides the existing owner and promotion creates a second one. crates/notes/src/task_blocks.rs:82–91. start with:

    - [ ] Task ^bbbbbbbbbbbbbbbb
      - [Task](<Task.md>) ^cccccccccccccccc
    

    where Task.md has status: todo. relationship() compares the entire child text with the raw link without removing its trailing block marker, so owner() misses the existing task. promoting the checkbox succeeds with a fresh task UUID and inserts another task-link child while retaining the first one. the task index already recognizes the original child target (crates/tasks/src/found.rs:295–307), so this also disagrees with the read model. ADR 0068 O01 requires structural tokens to be removed before standalone-relationship classification; the one-owner invariant must hold. fix: use shared structural/marker parsing when classifying relationships, or refuse unsupported child layouts before making a new owner. apply the same classification to delete/session relationships. probe: review_r2_relationship_marker_retains_owner.

validation and scope

all execution was on a dev VM, through Nix and ~/bin/slot; no code was pushed. the final uncommitted probe worktree is claude@calternal-dev-01-compute02:~/Developer/Worktrees/review-798-r2. dev-02's queued runs were stopped before moving verification to dev-01.

  • cargo test -j4 -p calternal-notes task_ops::tests -- --nocapture: 18 passed, 2 failed, exactly the two new P1 probes above. all 14 existing task-operation tests, including round 1 regressions and kill-9/replay, passed. final log: ~/review-798-r2-confirm.log.
  • cargo test -j4 -p calternal-tasks mutation_tests: 7 passed, including BOM/CRLF exact Undo and action-level YAML/code/comment corpus. log: ~/review-798-r2-all.log.
  • expanded probes passed for plain-scalar/list interior quotes, list inline comments, flow lists/maps, anchors, folded/literal/multiline scalars, normalized BOM/CRLF variants, and checkboxes inside multi-line inline code, fences, HTML/comments and indented code. a concurrent duplicate observation also confirms it preserves the original's intervening edit while copying its captured description snapshot; that read-only snapshot behavior is not reported as a defect.

reproduction environment: export CARGO_TARGET_DIR=/home/claude/Developer/Worktrees/tasks-b3-r1-fix/target; nix develop -c bash -c '~/bin/slot cargo test -j4 -p calternal-notes task_ops::tests -- --nocapture' from the final worktree. no full workspace/lint/performance gate rerun is claimed. this is round 2 of the maximum 3; never merged.

## Independent review, round 2 **verdict: fix first.** reviewed `0968bbff9751862b4fad4de41e59bcd580636d42`. all five round 1 findings are verified fixed for their original reproductions. two new P1 findings remain; no P0 findings. ### round 1 verification | round 1 finding | status | does its regression prove it? | | --- | --- | --- | | P1 delete removed ordinary reference bullets | verified | yes. `review_r1_delete_keeps_ordinary_reference_bullets` compares exact source bytes; continuation/nested-child and shared-session tests also pass. | | P1 duplicate retained continuation-line IDs | verified | yes. `review_r1_duplicate_multiline_block_id_is_fresh` exercises real duplication and rejects the original ID; the hidden-continuation/code fixture also passes. | | P1 stale promotion mutated changed prose and reported success | verified | yes. `review_r1_promote_stale_source_must_not_report_success` changes the source at `splice-before-turn`, verifies the changed prose remains, no old relationship or orphan task appears, and success is refused. this does not cover the separate Trash write path below. | | P1 delete Undo changed original link bytes | verified | yes. `review_r1_delete_undo_preserves_link_spelling` compares the whole source before/after Undo, including the bare destination spelling. rename/deleted-source/moved-anchor tests also pass. | | P2 plain-list interior-quote refusal bypass | verified | yes. `review_r1_yaml_list_quotes_must_refuse` tests single/double interior quotes and the quote/comment interaction. independent expanded refusal probes also pass. | ### new findings 1. **P1: delete skips the exact-source guard on the task note being trashed.** `crates/notes/src/task_ops.rs:211–217`, `crates/notes/src/bulk/steps.rs:203`. only the backlink sources receive guarded splices; `Step::Trash` carries just the task UUID. pause deletion at the source's `splice-before-turn`, change the task from `status: todo` to `status: did-not-happen`, ingest the outside change into its room, and verify the new status is present before resuming. deletion still removes the owning source lines, trashes the now non-task note, and reports `state: done`. the original `require_task` decision is stale, and the changed note is destructively acted on without a source precondition. **fix:** retain the task snapshot in the durable plan and enforce its exact text/identity precondition inside the Trash actor turn before committing deletion; propagate refusal and make partial-job recovery/Undo preserve intervening edits. probe: `review_r2_delete_changed_task_must_refuse`. 2. **P1: a block marker on the task-link child hides the existing owner and promotion creates a second one.** `crates/notes/src/task_blocks.rs:82–91`. start with: ```markdown - [ ] Task ^bbbbbbbbbbbbbbbb - [Task](<Task.md>) ^cccccccccccccccc ``` where `Task.md` has `status: todo`. `relationship()` compares the entire child text with the raw link without removing its trailing block marker, so `owner()` misses the existing task. promoting the checkbox succeeds with a fresh task UUID and inserts another task-link child while retaining the first one. the task index already recognizes the original child target (`crates/tasks/src/found.rs:295–307`), so this also disagrees with the read model. ADR 0068 O01 requires structural tokens to be removed before standalone-relationship classification; the one-owner invariant must hold. **fix:** use shared structural/marker parsing when classifying relationships, or refuse unsupported child layouts before making a new owner. apply the same classification to delete/session relationships. probe: `review_r2_relationship_marker_retains_owner`. ### validation and scope all execution was on a dev VM, through Nix and `~/bin/slot`; no code was pushed. the final uncommitted probe worktree is `claude@calternal-dev-01-compute02:~/Developer/Worktrees/review-798-r2`. dev-02's queued runs were stopped before moving verification to dev-01. - `cargo test -j4 -p calternal-notes task_ops::tests -- --nocapture`: **18 passed, 2 failed**, exactly the two new P1 probes above. all 14 existing task-operation tests, including round 1 regressions and kill-9/replay, passed. final log: `~/review-798-r2-confirm.log`. - `cargo test -j4 -p calternal-tasks mutation_tests`: **7 passed**, including BOM/CRLF exact Undo and action-level YAML/code/comment corpus. log: `~/review-798-r2-all.log`. - expanded probes passed for plain-scalar/list interior quotes, list inline comments, flow lists/maps, anchors, folded/literal/multiline scalars, normalized BOM/CRLF variants, and checkboxes inside multi-line inline code, fences, HTML/comments and indented code. a concurrent duplicate observation also confirms it preserves the original's intervening edit while copying its captured description snapshot; that read-only snapshot behavior is not reported as a defect. reproduction environment: `export CARGO_TARGET_DIR=/home/claude/Developer/Worktrees/tasks-b3-r1-fix/target; nix develop -c bash -c '~/bin/slot cargo test -j4 -p calternal-notes task_ops::tests -- --nocapture'` from the final worktree. no full workspace/lint/performance gate rerun is claimed. this is round 2 of the maximum 3; never merged.
Author
Owner

round 2 implementation plan: retain the reviewer probes; centralize exact-text and parsed task/non-task validation in verify_target(note, expected_source_bytes, expected_kind), called inside the actor after outside ingestion, immediately before guarded splices and Trash moves. Persist the Trash snapshot; force guarded Trash through the actor even for closed notes. Ownership reads the owner marker from its own head line only; strip structural child markers for relationships, and refuse promotion when any other task link exists in the block extent. B1 move/set-status are absent from this branch; their shared mutation boundary is the guarded splice actor. Expected delta: M, roughly 200 application lines, linear byte comparison plus existing parsed block/link scan; stored snapshots cost O(source bytes).

failure effect detection recovery test
task edited/reclassified during delete stale destructive write actor byte/kind guard failed job, Undo restores removed lines while retaining task edit permanent r2 delete probe plus partial Undo
child marker or nested task link second owner owner-head marker and full-extent link scan return existing direct owner or refuse permanent r2 owner probe plus child/nested cases
crash/replay repeated mutation durable step receipts and Trash state resume stored steps existing kill/replay tests

Gates: fmt, clippy with -D warnings, tasks/notes/calternal/daily tests, contract, action-types. No merge.

round 2 implementation plan: retain the reviewer probes; centralize exact-text and parsed task/non-task validation in `verify_target(note, expected_source_bytes, expected_kind)`, called inside the actor after outside ingestion, immediately before guarded splices and Trash moves. Persist the Trash snapshot; force guarded Trash through the actor even for closed notes. Ownership reads the owner marker from its own head line only; strip structural child markers for relationships, and refuse promotion when any other task link exists in the block extent. B1 move/set-status are absent from this branch; their shared mutation boundary is the guarded splice actor. Expected delta: M, roughly 200 application lines, linear byte comparison plus existing parsed block/link scan; stored snapshots cost O(source bytes). | failure | effect | detection | recovery | test | | --- | --- | --- | --- | --- | | task edited/reclassified during delete | stale destructive write | actor byte/kind guard | failed job, Undo restores removed lines while retaining task edit | permanent r2 delete probe plus partial Undo | | child marker or nested task link | second owner | owner-head marker and full-extent link scan | return existing direct owner or refuse | permanent r2 owner probe plus child/nested cases | | crash/replay | repeated mutation | durable step receipts and Trash state | resume stored steps | existing kill/replay tests | Gates: fmt, clippy with -D warnings, tasks/notes/calternal/daily tests, contract, action-types. No merge.
Author
Owner

round 2 fixes, ready for final review

head f1600ed05109566241d1daa19c91be1d2b81ed1c. addresses review 71308. never merged.

finding / requirement implementation status
P1 changed task still trashed persist the task snapshot in the Trash step; force guarded Trash through the room actor, including closed notes; ingest outside edits, then call the shared byte/kind guard before moving pass: permanent review_r2_delete_changed_task_must_refuse, strengthened to Undo the partial job and retain the edited task
P1 child marker hides owner strip the child line's structural marker before standalone-relationship classification, shared by promotion and delete/session cleanup pass: permanent review_r2_relationship_marker_retains_owner
structural mutation guard one verify_target(note, expected_source_bytes, expected_kind) used by guarded splices and Trash; existing daily-note duplicate placement also uses a guarded splice pass: independent byte/kind test and existing stale-promotion probe
owner identity and full extent read the parent marker only from its head line; inspect every task link in the parsed block extent; return one existing direct owner or refuse nested/prose/ambiguous bindings pass: child-marker identity and nested/prose/continuation-link regressions
replay / preservation retain step receipts, Trash replay detection and exact-span Undo pass: all task-operation crash/replay and round 1 regressions

The baseline reviewer probes were reproduced on dev-01: 4 passed, 2 failed, exactly the two reported P1s (~/tasks-b3-r2-red.log). The final task-operation suite passes 23/23. An initial green invocation reused the baseline binary from the shared Cargo target; it was discarded, and the corrected run explicitly rebuilt the current notes crate and confirmed all 23 tests. The validated VM source tree matches the pushed head exactly; temporary Nix flake files are excluded and were not committed.

All final gates ran on dev-01, using Nix and ~/bin/slot:

gate exact command result
format cargo fmt --all -- --check pass
task operations cargo test -j4 -p calternal-notes task_ops::tests -- --nocapture pass, 23/23
lint cargo clippy -j4 -p calternal-tasks -p calternal-notes -p calternal -p calternal-daily --all-targets -- -D warnings pass
package tests cargo test -j4 -p calternal-tasks -p calternal-notes -p calternal -p calternal-daily pass, 598 passed / 10 existing ignored
contract cargo test -j4 -p calternal contract pass
action types node apps/web/scripts/action-types.mjs --check pass

Final log: claude@calternal-dev-01-compute02:~/tasks-b3-r2-gates.log, ending GATES_PASS.

Decisions / limits: move and set-status are not present on this B3 branch; the shared actor splice guard is available for their integration. A duplicate's original task is a read-only captured snapshot, and intervening edits to that original remain intact. New identities have no existing bytes to validate; every B3 write to existing text uses the shared guard. A late Trash refusal can leave earlier relationship removals applied; the job fails, and Undo restores them without replacing the intervening task edit. Existing strict YAML/ambiguous-Markdown refusals remain. No performance claim. Final independent review is pending; no merge.

## round 2 fixes, ready for final review head `f1600ed05109566241d1daa19c91be1d2b81ed1c`. addresses [review 71308](https://git.kayg.org/kayg/calternal/pulls/798#issuecomment-71308). never merged. | finding / requirement | implementation | status | | --- | --- | --- | | P1 changed task still trashed | persist the task snapshot in the Trash step; force guarded Trash through the room actor, including closed notes; ingest outside edits, then call the shared byte/kind guard before moving | pass: permanent `review_r2_delete_changed_task_must_refuse`, strengthened to Undo the partial job and retain the edited task | | P1 child marker hides owner | strip the child line's structural marker before standalone-relationship classification, shared by promotion and delete/session cleanup | pass: permanent `review_r2_relationship_marker_retains_owner` | | structural mutation guard | one `verify_target(note, expected_source_bytes, expected_kind)` used by guarded splices and Trash; existing daily-note duplicate placement also uses a guarded splice | pass: independent byte/kind test and existing stale-promotion probe | | owner identity and full extent | read the parent marker only from its head line; inspect every task link in the parsed block extent; return one existing direct owner or refuse nested/prose/ambiguous bindings | pass: child-marker identity and nested/prose/continuation-link regressions | | replay / preservation | retain step receipts, Trash replay detection and exact-span Undo | pass: all task-operation crash/replay and round 1 regressions | The baseline reviewer probes were reproduced on dev-01: **4 passed, 2 failed**, exactly the two reported P1s (`~/tasks-b3-r2-red.log`). The final task-operation suite passes **23/23**. An initial green invocation reused the baseline binary from the shared Cargo target; it was discarded, and the corrected run explicitly rebuilt the current notes crate and confirmed all 23 tests. The validated VM source tree matches the pushed head exactly; temporary Nix flake files are excluded and were not committed. All final gates ran on **dev-01**, using Nix and `~/bin/slot`: | gate | exact command | result | | --- | --- | --- | | format | `cargo fmt --all -- --check` | pass | | task operations | `cargo test -j4 -p calternal-notes task_ops::tests -- --nocapture` | pass, 23/23 | | lint | `cargo clippy -j4 -p calternal-tasks -p calternal-notes -p calternal -p calternal-daily --all-targets -- -D warnings` | pass | | package tests | `cargo test -j4 -p calternal-tasks -p calternal-notes -p calternal -p calternal-daily` | pass, 598 passed / 10 existing ignored | | contract | `cargo test -j4 -p calternal contract` | pass | | action types | `node apps/web/scripts/action-types.mjs --check` | pass | Final log: `claude@calternal-dev-01-compute02:~/tasks-b3-r2-gates.log`, ending `GATES_PASS`. Decisions / limits: move and set-status are not present on this B3 branch; the shared actor splice guard is available for their integration. A duplicate's original task is a read-only captured snapshot, and intervening edits to that original remain intact. New identities have no existing bytes to validate; every B3 write to existing text uses the shared guard. A late Trash refusal can leave earlier relationship removals applied; the job fails, and Undo restores them without replacing the intervening task edit. Existing strict YAML/ambiguous-Markdown refusals remain. No performance claim. Final independent review is pending; no merge.
Author
Owner

Independent review, round 3 (final)

verdict: stop and simplify. reviewed f1600ed05109566241d1daa19c91be1d2b81ed1c. both round-2 reproductions pass, but a new P1 remains in failed-delete recovery. no P0 findings. this is the final round under the three-round rule; do not start another patch/review loop on this design.

status

requirement / path status evidence
round 2: changed task must not be trashed verified for the reported reproduction permanent review_r2_delete_changed_task_must_refuse passes, including Undo preserving the intervening task edit; guarded Trash enters the room even for closed notes
round 2: child marker must not hide its owner verified review_r2_relationship_marker_retains_owner passes; relationship classification strips the child marker
parent identity and promotion extent verified for supplied cases owner-head identity and nested/prose/continuation task-link refusal regressions pass
initial apply to existing text guarded forward Splice and TaskRemove supply expected bytes; room undo_edit_retained calls the shared guard; task Trash supplies expected bytes and explicitly requires Task
flush / recovery publication incomplete room/save.rs has no shared task guard; its outside-file conflict loop ingests and retries. the new post-guard flush probe returns done after ingesting a changed source
recovery / replay of steps partly verified unapplied forward guarded steps retain their expected snapshot; applied receipts intentionally skip reapplying. existing kill-9/replay tests pass. receipt-driven recovery saves use generic save, without the shared guard
promotion / duplication Undo fails the all-write-path requirement bulk/undo.rs constructs inverse Splice with expected: None; the actor consequently skips verify_target. the new source-kind-change probe confirms the write proceeds
delete Undo / Trash line restore byte guard present, semantic recovery unsafe task_ops/restore.rs:238 supplies a fresh destination snapshot to the shared guard, but restores planned removals without checking whether their removal actually applied or whether the block now has another owner; new P1 below
YAML bypasses and literal checkboxes no new finding in exercised corpus existing scalar/list quote/comment, flow, anchor, multiline, BOM/CRLF refusal matrices and code/fence/HTML/comment/indented-code checkbox matrices all pass

shared API for B1 #801 / B2 #799: crate::task_ops::verify_target(note, expected_source_bytes, expected_kind) at crates/notes/src/task_ops.rs:475, with TargetKind at line 458. both are pub(crate), reusable by helpers inside calternal-notes; they are not an exported API callable directly from calternal-tasks. the name/location are clear, but the assertion that every existing-text B3 write uses it is false. do not copy the optional-expected Undo bypass into B1/B2.

new findings

  1. P1: Undo of a refused deletion restores a removal that never happened and creates two task owners. crates/notes/src/task_ops/restore.rs:72–83,137–156,230–257; crates/notes/src/bulk/undo.rs:138–143. remove() persists every planned row before any source splice succeeds. Undo includes TaskRestore even when TaskRemove failed, and restore treats all those planned rows as removed. reproduction: Task.md and Other.md are both task notes; Source contains - [ ] Task ^bbbbbbbbbbbbbbbb with child [Task](<Task.md>). pause delete at splice-before-turn, change that child to [Other](<Other.md>), ingest/save it, then release. delete correctly refuses and changes no source bytes. now Undo reports state: done and inserts the old [Task](<Task.md>) child before [Other](<Other.md>). one checkbox now has two owning task targets. the fresh byte guard passes because it snapshots the already-retargeted destination; it does not validate recovery provenance or ownership. independent probe: review_r3_refused_delete_undo_does_not_add_second_relationship. a separate moved-marker probe also reproduces duplicate relationship insertion after an existing relationship's label changes, showing raw prefix equality is not semantic deduplication. required design property: restore only removals proven applied by durable receipts, and refuse/reconcile a conflicting current owner before inserting any relationship. the author's late-refusal recovery claim does not cover this case.

  2. P2: shared-guard coverage stops before several requested write paths. crates/notes/src/bulk/undo.rs:105–115; crates/notes/src/room/delete.rs:104–124; crates/notes/src/room/save.rs:232–237. inverse task splices drop expected bytes entirely; only the older span guard runs. review_r3_undo_promote_source_kind_changed promotes inside a task note, changes that source's status to did-not-happen, then confirms Undo still removes the relationship and reports done. the new status itself is preserved; this probe demonstrates the requested guard-policy gap, not loss of that status. separately, review_r3_promote_flush_revalidates_outside_change pauses the post-apply save at BeforeWrite, changes the on-disk checkbox title Task to Changed, and resumes. the save ingests/retries and promotion reports done with Changed plus the old Task relationship. generic recovery publication also has no verify_target call. required design property: define one task mutation boundary covering apply, inverse, and publication/recovery, with explicit preconditions and receipt semantics; generic save/CRDT preservation is not evidence that this shared guard ran. no additional data-loss severity is inferred from these two probes.

concrete simpler design

Replace the nested TaskRemove loop plus all-planned-rows recovery sidecar with one ordinary guarded top-level Splice per source in the existing durable bulk plan. Retain each source's exact removed bytes, bindings, and before/after state with that step's durable receipt. Undo must use those receipts, including a receipt committed just before a crash, to select only edits that actually applied. Route forward and inverse writes through one mandatory task guard boundary.

For v1 restoration, automatically restore only an unchanged, unambiguously owned destination. If a block moved or its binding changed, retain the pinned recovery span and report a conflict for explicit restoration instead of guessing placement or adding an owner. If moved-block reuse is retained, resolve its current owner by UUID first: same owner means no new relationship; another owner or ambiguous structure means refuse without consuming the recovery record. This removes speculative all-row restoration and raw-prefix ownership guesses while retaining the existing bulk engine and exact recovery evidence.

validation

all execution ran on dev-01, through Nix and ~/bin/slot; no builds/tests ran on the control host and no code was pushed. isolated uncommitted probe worktree: claude@calternal-dev-01-compute02:~/Developer/Worktrees/review-798-r3, based on the reviewed head, with only probe tests and temporary Nix flake files changed.

Exact final command from that worktree:

export PATH=/nix/var/nix/profiles/default/bin:$PATH
export CARGO_TARGET_DIR=/home/claude/Developer/Worktrees/tasks-b3-r1-fix/target
nix develop -c bash -c '~/bin/slot cargo test -j4 -p calternal-notes task_ops::tests -- --nocapture'

23 existing tests passed; 4 independent probes failed: refused-delete Undo, moved-owner relationship reuse, Undo source-kind guard, and post-guard flush. final log: ~/review-798-r3-confirm.log on dev-01. full lint/workspace/action gates and performance were not rerun. never merged.

## Independent review, round 3 (final) **verdict: stop and simplify.** reviewed `f1600ed05109566241d1daa19c91be1d2b81ed1c`. both round-2 reproductions pass, but a new P1 remains in failed-delete recovery. no P0 findings. this is the final round under the three-round rule; do not start another patch/review loop on this design. ### status | requirement / path | status | evidence | | --- | --- | --- | | round 2: changed task must not be trashed | verified for the reported reproduction | permanent `review_r2_delete_changed_task_must_refuse` passes, including Undo preserving the intervening task edit; guarded Trash enters the room even for closed notes | | round 2: child marker must not hide its owner | verified | `review_r2_relationship_marker_retains_owner` passes; relationship classification strips the child marker | | parent identity and promotion extent | verified for supplied cases | owner-head identity and nested/prose/continuation task-link refusal regressions pass | | initial apply to existing text | guarded | forward `Splice` and `TaskRemove` supply expected bytes; room `undo_edit_retained` calls the shared guard; task Trash supplies expected bytes and explicitly requires Task | | flush / recovery publication | incomplete | `room/save.rs` has no shared task guard; its outside-file conflict loop ingests and retries. the new post-guard flush probe returns done after ingesting a changed source | | recovery / replay of steps | partly verified | unapplied forward guarded steps retain their expected snapshot; applied receipts intentionally skip reapplying. existing kill-9/replay tests pass. receipt-driven recovery saves use generic save, without the shared guard | | promotion / duplication Undo | fails the all-write-path requirement | `bulk/undo.rs` constructs inverse Splice with `expected: None`; the actor consequently skips `verify_target`. the new source-kind-change probe confirms the write proceeds | | delete Undo / Trash line restore | byte guard present, semantic recovery unsafe | `task_ops/restore.rs:238` supplies a fresh destination snapshot to the shared guard, but restores planned removals without checking whether their removal actually applied or whether the block now has another owner; new P1 below | | YAML bypasses and literal checkboxes | no new finding in exercised corpus | existing scalar/list quote/comment, flow, anchor, multiline, BOM/CRLF refusal matrices and code/fence/HTML/comment/indented-code checkbox matrices all pass | **shared API for B1 #801 / B2 #799:** `crate::task_ops::verify_target(note, expected_source_bytes, expected_kind)` at `crates/notes/src/task_ops.rs:475`, with `TargetKind` at line 458. both are `pub(crate)`, reusable by helpers inside `calternal-notes`; they are not an exported API callable directly from `calternal-tasks`. the name/location are clear, but the assertion that every existing-text B3 write uses it is false. do not copy the optional-expected Undo bypass into B1/B2. ### new findings 1. **P1: Undo of a refused deletion restores a removal that never happened and creates two task owners.** `crates/notes/src/task_ops/restore.rs:72–83,137–156,230–257`; `crates/notes/src/bulk/undo.rs:138–143`. `remove()` persists every planned row before any source splice succeeds. Undo includes TaskRestore even when TaskRemove failed, and restore treats all those planned rows as removed. reproduction: Task.md and Other.md are both task notes; Source contains `- [ ] Task ^bbbbbbbbbbbbbbbb` with child `[Task](<Task.md>)`. pause delete at `splice-before-turn`, change that child to `[Other](<Other.md>)`, ingest/save it, then release. delete correctly refuses and changes no source bytes. now Undo reports `state: done` and inserts the old `[Task](<Task.md>)` child before `[Other](<Other.md>)`. one checkbox now has two owning task targets. the fresh byte guard passes because it snapshots the already-retargeted destination; it does not validate recovery provenance or ownership. independent probe: `review_r3_refused_delete_undo_does_not_add_second_relationship`. a separate moved-marker probe also reproduces duplicate relationship insertion after an existing relationship's label changes, showing raw prefix equality is not semantic deduplication. **required design property:** restore only removals proven applied by durable receipts, and refuse/reconcile a conflicting current owner before inserting any relationship. the author's late-refusal recovery claim does not cover this case. 2. **P2: shared-guard coverage stops before several requested write paths.** `crates/notes/src/bulk/undo.rs:105–115`; `crates/notes/src/room/delete.rs:104–124`; `crates/notes/src/room/save.rs:232–237`. inverse task splices drop expected bytes entirely; only the older span guard runs. `review_r3_undo_promote_source_kind_changed` promotes inside a task note, changes that source's status to `did-not-happen`, then confirms Undo still removes the relationship and reports done. the new status itself is preserved; this probe demonstrates the requested guard-policy gap, not loss of that status. separately, `review_r3_promote_flush_revalidates_outside_change` pauses the post-apply save at BeforeWrite, changes the on-disk checkbox title Task to Changed, and resumes. the save ingests/retries and promotion reports done with Changed plus the old Task relationship. generic recovery publication also has no `verify_target` call. **required design property:** define one task mutation boundary covering apply, inverse, and publication/recovery, with explicit preconditions and receipt semantics; generic save/CRDT preservation is not evidence that this shared guard ran. no additional data-loss severity is inferred from these two probes. ### concrete simpler design Replace the nested TaskRemove loop plus all-planned-rows recovery sidecar with one ordinary guarded top-level Splice per source in the existing durable bulk plan. Retain each source's exact removed bytes, bindings, and before/after state with that step's durable receipt. Undo must use those receipts, including a receipt committed just before a crash, to select only edits that actually applied. Route forward and inverse writes through one mandatory task guard boundary. For v1 restoration, automatically restore only an unchanged, unambiguously owned destination. If a block moved or its binding changed, retain the pinned recovery span and report a conflict for explicit restoration instead of guessing placement or adding an owner. If moved-block reuse is retained, resolve its current owner by UUID first: same owner means no new relationship; another owner or ambiguous structure means refuse without consuming the recovery record. This removes speculative all-row restoration and raw-prefix ownership guesses while retaining the existing bulk engine and exact recovery evidence. ### validation all execution ran on dev-01, through Nix and `~/bin/slot`; no builds/tests ran on the control host and no code was pushed. isolated uncommitted probe worktree: `claude@calternal-dev-01-compute02:~/Developer/Worktrees/review-798-r3`, based on the reviewed head, with only probe tests and temporary Nix flake files changed. Exact final command from that worktree: ```sh export PATH=/nix/var/nix/profiles/default/bin:$PATH export CARGO_TARGET_DIR=/home/claude/Developer/Worktrees/tasks-b3-r1-fix/target nix develop -c bash -c '~/bin/slot cargo test -j4 -p calternal-notes task_ops::tests -- --nocapture' ``` **23 existing tests passed; 4 independent probes failed**: refused-delete Undo, moved-owner relationship reuse, Undo source-kind guard, and post-guard flush. final log: `~/review-798-r3-confirm.log` on dev-01. full lint/workspace/action gates and performance were not rerun. never merged.
Author
Owner

on hold pending the shared Tasks writes v2 plan, PR #808, branch docs/tasks-writes-v2-plan, head 8d32f656049061a195653e950c9db097d10285c7.

docs/plans/0066-tasks-writes-v2.md covers all task writes: pure exact-byte splice planners plus ONE journaled executor for apply, recovery and recorded inverse undo, reusing calendar v2’s parsed view and frontmatter writer. Undo requires a committed change and the exact recorded post-image; refused removals cannot be restored. Executor first, then B1, B2, B3. Keep existing work/probes preserved; no further patch/review loop or merge of this held implementation pending the plan. Docs only; no implementation started.

on hold pending the shared [Tasks writes v2 plan, PR #808](https://git.kayg.org/kayg/calternal/pulls/808), branch `docs/tasks-writes-v2-plan`, head `8d32f656049061a195653e950c9db097d10285c7`. `docs/plans/0066-tasks-writes-v2.md` covers all task writes: pure exact-byte splice planners plus ONE journaled executor for apply, recovery and recorded inverse undo, reusing calendar v2’s parsed view and frontmatter writer. Undo requires a committed change and the exact recorded post-image; refused removals cannot be restored. Executor first, then B1, B2, B3. Keep existing work/probes preserved; no further patch/review loop or merge of this held implementation pending the plan. Docs only; no implementation started.
Author
Owner

Pointer: #894 (feat/tasks-actions) overlaps this PR. The review that compares the two is here: #894 (comment)

Recommendation (c): keep #894's API, version, reorder and status-only move. Rebuild delete/duplicate (and later any line move) as v2 planners on the shared executor (#808). From this PR: owner/relationship classification, the full-extent block-ID re-key, the YAML refusal corpus and all review_r1/r2/r3 probes. #894's own duplicate repeats r1 P1-2 and r2 P1-2, and its delete repeats r2 P1-1.

After those pieces are ported to the v2 rebuild, close this PR. Keep the branch until then.

Pointer: #894 (`feat/tasks-actions`) overlaps this PR. The review that compares the two is here: https://git.kayg.org/kayg/calternal/pulls/894#issuecomment-76844 Recommendation **(c)**: keep #894's API, `version`, reorder and status-only move. Rebuild delete/duplicate (and later any line move) as v2 planners on the shared executor (#808). From this PR: owner/relationship classification, the full-extent block-ID re-key, the YAML refusal corpus and all `review_r1/r2/r3` probes. #894's own duplicate repeats r1 P1-2 and r2 P1-2, and its delete repeats r2 P1-1. After those pieces are ported to the v2 rebuild, close this PR. Keep the branch until then.
Author
Owner

Keeper proposal after reading #798, #799, #894 and all their review comments: gate #894 (cac156dd68c222da099343628a75abfa97a75504), the replacement following review 76844, rather than revive the held bulk/room implementations. #798 round 3 explicitly requires stop-and-simplify; #799 is held for the same executor design. Propose closing #798/#799 as superseded only after recording the missing promotion/physical-line-move scope and preserving/porting their review probes. Do not close #894 as a duplicate: it carries the reviewed replacement API and the executor rework. Neither earlier PR is approved for merge. No PR closed or merged. I will merge current dev into #894, run fresh gates, add crash/retry regressions and report remaining scope explicitly.

Keeper proposal after reading #798, #799, #894 and all their review comments: gate #894 (`cac156dd68c222da099343628a75abfa97a75504`), the replacement following review 76844, rather than revive the held bulk/room implementations. #798 round 3 explicitly requires stop-and-simplify; #799 is held for the same executor design. Propose closing #798/#799 as superseded only after recording the missing promotion/physical-line-move scope and preserving/porting their review probes. Do not close #894 as a duplicate: it carries the reviewed replacement API and the executor rework. Neither earlier PR is approved for merge. No PR closed or merged. I will merge current dev into #894, run fresh gates, add crash/retry regressions and report remaining scope explicitly.
Author
Owner

housekeeping #1009: needs-decision.

Explicit three-round hold; Tasks writes v2 plan #808 and executor work #894 replace the action direction. Preserve promotion and physical-line scope regressions.

original head: f1600ed05109566241d1daa19c91be1d2b81ed1c. against dev cd7f84432febfc63b788fbb9b9f9950452982d74, git cherry reports 11 unmatched and 0 equivalent patches. the landing audit lists each candidate missing commit and stable patch match in pr-798.md; unmatched patches can include superseded behavior.

left open, source branch unchanged. no rebase or new test claim for this decision lane.

housekeeping #1009: needs-decision. Explicit three-round hold; Tasks writes v2 plan #808 and executor work #894 replace the action direction. Preserve promotion and physical-line scope regressions. original head: `f1600ed05109566241d1daa19c91be1d2b81ed1c`. against dev `cd7f84432febfc63b788fbb9b9f9950452982d74`, git cherry reports 11 unmatched and 0 equivalent patches. the [landing audit](https://git.kayg.org/attachments/fe3c09dc-dea3-47a0-92e2-b44a777083e5) lists each candidate missing commit and stable patch match in `pr-798.md`; unmatched patches can include superseded behavior. left open, source branch unchanged. no rebase or new test claim for this decision lane.
Author
Owner

Superseded by #808/#894 (Tasks writes v2). The branch and original tips are archived under archive/ (see #1009), so nothing is lost; useful regression vectors are noted there for the active lanes. Closing.

Superseded by #808/#894 (Tasks writes v2). The branch and original tips are archived under `archive/` (see #1009), so nothing is lost; useful regression vectors are noted there for the active lanes. Closing.
kayg closed this pull request 2026-11-07 21:21:09 +00:00
Some checks failed
CI / server (pull_request) Has been cancelled
CI / rust (pull_request) Has been cancelled
CI / web (pull_request) Has been cancelled

Pull request closed

Sign in to join this conversation.
No reviewers
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
kayg/calternal!798
No description provided.