Files
reasampler/PLAN.md
T
daniel b931dada0a Scope sample-remove (B5) and prune (Phase R) into the plan
Adds the two missing removal capabilities: sample-level remove (Phase B
point B5, index-only) and the prune file-lifecycle path (new Phase R, the
sole file-deleting op), resolving the dangling capture/prune reference.
Adds CONTEXT.md spec, PLAN.md points, and a product note; surfaces forks
R-A..R-E for decision.
2026-07-23 20:21:50 -04:00

406 lines
26 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# PLAN.md — ReaSampler milestone roadmap
Living milestone roadmap for ReaSampler. Derived from CONTEXT.md's 11-step build
order; CONTEXT.md remains the authoritative spec — this file is the tickable
checklist, not a re-statement of the spec. When a point lands, doc-keeper removes
it here and appends it to `COMPLETED.md`.
**Conventions**
- One checkbox `- [ ]` = one discrete, independently-landable point.
- Each milestone opens with a **Goal** (one line) and a **Verify** criterion
(the acceptance gate; precision invariants pulled in where one applies).
- Verify-in-DAW points require a manual REAPER run; pure points are gated by CTest.
- "See CONTEXT.md §…" points at the authoritative detail — do not duplicate it here.
---
## Milestone 9 — slots (MPC-style)
**Goal:** "Capture to slot N" / "insert slot N", MIDI-bindable. CONTEXT.md
Build order 9.
**Verify (in DAW):** Slot capture and slot insert fire from MIDI bindings; slot
state persists via the index.
- [ ] Slot model + slot↔sample assignment.
- [ ] "Capture to slot N" / "insert slot N" actions, MIDI-bindable.
## Milestone 10 — provenance (re-capture from source)
**Goal:** Populate `Sample.provenance` (parent sample id + a capture-recipe
fingerprint) on resample-from-sample, and ship a **"re-capture from source"**
action that regenerates a sample from its recorded source. Reconciled with the
dual-canvas (Phase D2) model. CONTEXT.md §Data model, §capture; product framing +
the settled reconciliation in `docs/product/provenance.md`.
**Verify (in DAW):** A sample resampled from a bank sample carries its parent id +
recipe fingerprint; "re-capture from source" regenerates the file into the bank
(never auto-inserting into the timeline — load-bearing principle); re-capture with
an unchanged source + request is byte-identical to the original (bit-identical
repeats); non-destructive to source items/tracks.
> **Reshaped from the old "provenance + null-test verify" M10.** **Cut (fixed by
> Daniel):** the null-test verification *action* and the true-pre-FX-dry *mechanism*
> the old note required — both dropped, see `docs/product/provenance.md` §What was
> cut. **Kept:** provenance + re-capture. The `Sample.provenance` struct and its JSON
> round-trip **already exist** (M1) — M10 populates and consumes the field, it does
> not add it. Fork picks settled by Daniel (2026-07-23): **P1=a thin fingerprint,
> P2=a bank-only re-capture**; P3/P4 moot under P2=a. The points below are locked to
> that path.
- [ ] Populate `Sample.provenance` on resample-from-sample: `parentSampleId` (the
bank sample the capture derived from) + `fxChainSnapshot` as a **thin capture-recipe
fingerprint** (scope + source FX-chain identity/hash at capture time — a drift/repro
fingerprint, NOT a serialized pre-FX-dry chain to restore; P1=a settled).
- [ ] "Re-capture from source" action: regenerate a provenanced sample by re-running
its recorded capture request against the source's **current** state; update the
bank file + Sample in place. **Bank-only — never inserts/re-places into the
timeline** (load-bearing principle). Reports if the source drifted since capture.
- [ ] Verify: re-capture of an unchanged source is byte-identical to the original
capture (bit-identical repeats); non-destructive (`FxBypassGuard` snapshot/restore
as M7); relative-paths-only preserved.
**Dual-canvas reconciliation (settled — `docs/product/provenance.md`):** With
bank-only re-capture (P2=a), provenance is **pure per-sample bank metadata**,
`bank_model` and `view_mode_model` **stay decoupled**, and M10 touches **no** canvas
code. Dual-canvas compliance is satisfied by staying on the right side of the
capture-never-places line — not by any new coupling. Forks P3 (canvas/lane memory in
provenance) and P4 (re-capture auto-tag interaction) were only live under
re-capture-and-replace (P2=b) and are **closed as moot**; if the user manually
re-places a regenerated sample, the existing D2 mode-aware placement rule governs.
## Milestone 11 — polish
**Goal:** Batch capture (per selected item / per razor area),
resample-and-mute-source, conform-on-insert, native OS drag-out. CONTEXT.md
Build order 11, §Non-goals (drag-out deferred to last).
**Verify (in DAW):** Each polish action works without regressing the precision
invariants; drag-out places a valid file in the OS target.
- [ ] Batch capture: per selected item / per razor area.
- [ ] Resample-and-mute-source.
- [ ] Conform-on-insert (explicit).
- [ ] Native OS drag-out (deferred final; `InsertMedia` path must already work).
- [ ] Keybinding help labels: in the docked bank_panel, surface the current key
binding for each capture/provenance action (e.g. "Capture Item → <key>") by
querying the SDK for the key bound to the action's command id
(`kbd_getTextFromCmd(cmd, SectionFromUniqueID(0))` — main section) and formatting
a reminder label. Unbound case degrades to the action name with a clear
"unbound"/"—" marker (empty/blank return handled explicitly). Split: label-text
formatting (binding string + fallback → label) is **pure/testable**; the SDK
binding query + label draw is bank_panel shell.
- [ ] Action trigger buttons: clickable bank_panel buttons that fire the capture and
provenance actions directly, routing through the **existing** command-id contract
(`Main_OnCommand`/`KBD_OnMainActionEx` with the registered command id — the same id
minted at `registerAction`), never re-implementing capture. Split: button
hit-testing/layout math is **pure/testable** (mirror of `mode_switch`/`bank_grid`);
draw + command dispatch is bank_panel shell.
---
## Open questions to resolve during build
Carried from CONTEXT.md §Open questions — keep visible until each is closed by a
landed milestone.
- **`parseInt` narrowing hardening:** `src/bank_model.cpp` `parseInt` casts
`int64_t → int` via `static_cast` without a range check; integers that fit
in int64 but exceed `INT_MAX` are implementation-defined. Hardening candidate
— add bounds check before the cast when integer-field validation is in scope.
- **Capture send/routing isolation (TODO):** The FX-scope capture neutralizes out-of-scope FX, gain, and pan — but NOT aux **sends**. So a downstream coloring send (e.g. a folder → reverb-track send) still routes and blends the reverb into an item/track capture, past the intended isolation point. A true item-level capture should be taken at the isolated graph point — the target scope's output before out-of-scope track FX/gain/pan **and** before out-of-scope aux/parallel sends. The hard part: distinguish **source routing that must be preserved** (e.g. a MIDI send T1→T2 where T2's synth is where a MIDI item's audio is actually produced — the "item level" for that MIDI item is T2's synth output) from **coloring sends that must be excluded** (folder→reverb). Repro: folder F1; T1 (MIDI) sends MIDI to T2 (synth); T1+T2 → F1; F1 sends to reverb T3; capturing the MIDI item on T1 currently includes the reverb, should be isolated to T2's synth output pre-F1 with the MIDI send preserved and the reverb send excluded. Likely approach: snapshot + mute out-of-scope tracks' aux sends during the render while preserving the main/source signal path — needs a rule for which sends are load-bearing.
---
# Phase D2 — Two-canvas (item-level mode projection; additive to D1)
> **Design View sub-phase.** Extends D1's track-level mode projection to **item
> level** via REAPER 7 fixed lanes: on a track present in both stances, each mode
> owns a fixed lane — the active mode's lane shows and plays, the inactive mode's is
> hidden and silenced — so a Design take and an Arrange take can share the same
> track and time position without colliding on the view. Nothing in D1 changes.
> Runtime floor rises to **REAPER 7** for this sub-phase (no version-gate branch;
> below v7 it is simply unavailable). Authoritative spec: **CONTEXT.md §Two-canvas
> sub-phase (Phase D2 / Phase E)** and the surrounding §Design View — additive phase
> spec. Product framing: `docs/product/design-view.md` §Two-canvas direction. When a
> point lands, doc-keeper moves it to `COMPLETED.md`.
>
> **D2-W1 (pure lane extension), D2-W2 (shell: lane application + new-content
> detection), and D2-W3-A (lane minting + item→lane assignment + persist
> round-trip) have landed** — see `COMPLETED.md`.
## D2-W3-B — two-canvas actions + panel UI indicators
**Goal:** Any new lane/mode-management actions (bindable in the Actions list), and
any panel UI indicator for lane/mode state. The persist slice and the
lane-ownership index round-trip were completed in D2-W3-A; this wave closes the
remaining UI and action surface.
See CONTEXT.md §Two-canvas sub-phase (Module architecture — persistence).
**Verify (in DAW):** Lane/mode-management actions are registered and bindable in
the Actions list; the panel UI indicator reflects the current lane/mode state.
**Depends on:** D2-W3-A.
- [ ] Any new lane/mode-management actions (`command_id`/`gaccel`/`hookcommand`);
bindable in the Actions list.
- [ ] Any panel UI indicator for lane/mode state.
**D2-W3-A code-review polish (fold into this wave):**
- [ ] Simplify the redundant per-mint `I_NUMFIXEDLANES` re-read in `view.cpp`
(`applyMintPlan`): a single grow-and-track pass removes the second
`GetMediaTrackInfo_Value` call inside the mint loop.
- [ ] Optional shared item-read helper to remove duplicated `itemGuid`/`itemLaneName`
read logic between `view.cpp` and `bank_panel.cpp`.
- [ ] Optional defensive note/check in `reconcileManagedLanes` for the edge case
where a managed lane name encodes an unregistered mode id (log and skip rather
than silently record an orphaned ownership entry).
---
# Phase B — Multi-bank (parallel to the M0M11 capture roadmap and Phase D)
> **Separate phase namespace.** The M-numbers belong to the capture pillar
> (M0M11); the D-letters belong to Design View. Multi-bank is a third orthogonal
> pillar — generalizing the single bank into a pool + named banks — so it takes its
> own **lettered** namespace (B1, B2, …). "B" reads for **Banks** and, like Phase D,
> keeps the roadmaps from colliding on numbering: Phase B is not "the twelfth
> capture step," it is a different pillar. Authoritative spec: **CONTEXT.md
> §Multi-bank**. Product framing: `docs/product/multi-bank.md`. When a point lands,
> doc-keeper moves it to `COMPLETED.md`.
## B1 — bank_book (pure)
**Goal:** REAPER-free bank registry wrapping N `BankIndex` instances: pool seeded +
privileged, create/rename/reorder/delete named banks, active-bank id, move/copy a
sample between banks, JSON round-trip + legacy-migration. The heart of the phase;
mirror of `bank_model` / `view_mode_model`; **`BankIndex` untouched (additive)**.
CONTEXT.md §Multi-bank (Module architecture — pure).
**Verify:** CTest green. Pool always present, un-deletable, un-renamable,
un-evacuable (rules rejected in-model). Active-bank defaults to pool. Move is
index-only (source loses entry, destination gains it) and observes destination
collapse-by-hash; copy leaves source intact. Delete drops member index entries.
Evacuate moves all members to the pool, leaving the bank empty. JSON round-trip
lossless across pool-as-bank-zero + named banks + per-bank indices + ordinals +
active id. Legacy `bank_index` JSON parses into `{ pool }` with zero named banks.
- [ ] Bank registry: ordered `{ bank id, display name, ordinal, BankIndex }`; pool
seeded with fixed id + fixed name; create / rename / reorder / delete named banks
(delete drops the bank's member index entries).
- [ ] Pool-privilege rules enforced in-model: reject delete-pool, reject
rename-pool, reject evacuate-pool, never allow zero banks.
- [ ] Active-bank id (get/set; defaults to pool); resolve active bank's `BankIndex`.
- [ ] Move sample between banks (index-only; destination collapse-by-hash observed;
source entry removed).
- [ ] Copy sample between banks (index-only; source entry retained; destination
collapse-by-hash observed).
- [ ] Evacuate bank: move every member to the pool (index-only; destination
collapse-by-hash observed), leaving the bank empty; pool cannot be evacuated.
- [ ] JSON round-trip: pool-as-bank-zero inside the blob + named banks + per-bank
indices + ordinals + active id.
- [ ] Legacy migration: a bare `bank_index` JSON promotes to the pool's index with
zero named banks (one-way, lossless; blob authoritative thereafter).
- [ ] Tests: pool privileges (delete/rename/evacuate rejected); move
source-loses/dest-gains; copy source-retained; evacuate empties source into pool
with dest collapse; cross-bank same-hash coexistence; dest collapse on move into a
bank already holding the hash; JSON lossless; legacy migration.
## B2 — persist slice (banks ↔ project ext state)
**Goal:** Serialize the book under the `banks` key in `"reasampler"` alongside the
existing sections, with the pool folded in as bank-zero; migrate a legacy
`bank_index` key into the pool on first load and retire the legacy key; reload-on-open
and Save-As survival via the existing M4 machinery. CONTEXT.md §Multi-bank (persist).
**Verify (in DAW):** Banks + named banks + active bank + all per-bank samples survive
Save / Save As / close+reopen; **relative paths only**; bank travels with the `.rpp`;
a project saved before this phase (legacy `bank_index` only) loads as pool + zero
named banks with no sample loss, and after save carries `banks` with no `bank_index`
written.
**Depends on:** B1. (Persistence-key fork settled — fork 1 (a): pool inside the
`banks` blob, legacy key retired after one-way migration.)
- [ ] Serialize/deserialize the book under the `banks` key (pool-as-bank-zero inside
the blob; distinct section from `view_state`; no `bank_index` key written going
forward).
- [ ] Legacy-migration path on load: absent `banks` + present `bank_index` → promote
into pool, mint the blob, treat blob as authoritative (legacy key retired).
- [ ] Session exposes the book; the active bank's `BankIndex` is the capture add
target (route the M7 capture family through it — additive to M7, no M7 rewrite).
- [ ] Confirm survival across Save / Save As; confirm legacy-project load path.
## B3 — actions
**Goal:** Bindable action set for the multi-bank workflow. CONTEXT.md §Multi-bank
(actions). **Verify (in DAW):** Each action registered (bindable in Actions list);
bank-activate + move/copy + evacuate MIDI-bindable; create/rename/delete/evacuate
drive the B1 model via the B2-persisted session.
**Depends on:** B1, B2.
- [ ] Create bank / rename bank / delete bank (delete drops member index entries;
confirm-on-non-empty offered at the UI layer in B4).
- [ ] Evacuate bank → pool (move all members back to the pool; refuses on the pool).
- [ ] Activate bank (direct-by-id + cycle).
- [ ] Move selected samples → bank / copy selected samples → bank (move is default).
- [ ] Pool full-height / banks full-height toggles.
- [ ] Register each (`command_id`/`gaccel`/`hookcommand`); bank-activate + move/copy
+ evacuate MIDI-bindable.
## B4 — bank_panel vertical split (UI)
**Goal:** The vertical-split bank window — pool on top, named-banks tab-page region
below, full-height toggles — extending the M5 docked grid. CONTEXT.md §Multi-bank
(bank_panel). **Verify (in DAW):** Pool grid renders on top; named-banks tab strip
below (empty when no named banks, one tab per named bank); active-bank **unmistakably**
indicated; both full-height toggles collapse the split correctly; sample move/copy
affordance works; non-empty delete confirms and offers evacuate; the Design View mode
switch in the header is unaffected.
**Depends on:** B1, B2, B3. (Tab rendering + move-affordance mechanics — fork 5 —
settled 2026-07-23: LICE-drawn tabs + both move affordances; see Phase B open questions
and product notes → *Fork 5 — settled*.)
- [ ] Vertical split: pool grid region (top) + named-banks tab-page region (bottom).
- [ ] Named-banks tab strip: **LICE-drawn** (matching the M5 grid + Design View
segmented switch, not SWELL-native — fork 5a); one tab per named bank; empty state
when none. **Verify LICE tab draw against the M5 reference before use.**
- [ ] Tab-strip overflow/scroll affordance — **in scope from the start** (fork 5a): a
naive fixed-width LICE strip breaks down at ~812 tabs, so ship scroll/chevron
overflow with the strip, do not defer it.
- [ ] Pool full-height / banks full-height toggle affordances wired to B3.
- [ ] Active-bank indicator — **visually unmistakable** (settled constraint);
placement (per-region header / single readout / lit-tab) is the residual polish
detail.
- [ ] Create / rename / delete / activate / evacuate affordances driving B3 actions.
- [ ] Delete confirms on a non-empty bank, naming the evacuate alternative.
- [ ] Sample move affordance — **both** (fork 5b): a "move to bank" menu on the current
selection (bindable front-end for the B3 move action) **and** drag-between-regions.
Copy is the deliberate secondary act, offered on the menu.
- [ ] Drag mis-drop mitigation (fork 5b): clear drop-target highlighting on the
destination region/tab during a drag; a mis-drop is recoverable by design (move is
index-only and reversible). **Verify the drag hit-test doesn't collide with the M5
grid's multi-select drag.**
## B5 — sample-remove (the missing sample-level verb)
**Goal:** Drop an individual `Sample`'s index entry from a bank or the pool —
the sample-level companion to move/copy/evacuate/delete-bank. Index-only,
non-destructive to the file; exposes the `BankIndex::remove` primitive that
`bank_model` already has (wires it, does not add it). CONTEXT.md §Sample removal.
Product framing + open forks: `docs/product/removal-and-prune.md` §Sample-remove.
**Verify (in DAW):** Remove drops the selected sample's entry from the target
bank; a same-hash entry in another bank is untouched (no cross-bank dedup);
pool *contents* are removable while pool-container privileges hold; removing the
last index reference to a file leaves that file on disk (orphaned until prune —
never deleted by remove); non-destructive (index + ext-state only, no file, no
timeline item).
**Depends on:** B1, B2, B3 (action set), B4 (panel affordance).
- [ ] Surface `BankIndex::remove` through `bank_book`: remove a `Sample` from a
bank's index; pool contents removable, pool-container privileges unchanged.
- [ ] "Remove selected sample(s)" action (`command_id`/`gaccel`/`hookcommand`),
MIDI-bindable; carries a `scope: this-bank | all-banks` seam (fork R-A — default
this-bank until Daniel settles).
- [ ] `bank_panel` remove affordance on the current selection (reuse M5 selection
model, as move/copy do).
- [ ] Confirm-on-last-reference guardrail: remove that orphans a file (no other
bank references it) confirms, naming the orphaned-until-prune consequence;
remove of a still-referenced sample does not confirm.
- [ ] Tests: remove drops the target entry; same-hash entry in another bank
survives; remove-from-pool allowed; last-reference remove leaves an orphan (file
untouched); non-destructive (no file/timeline mutation).
## Phase B open questions
All five forks settled by Daniel (2026-07-23): persistence key = fold pool into `banks`,
retire legacy key (1a); delete drops members + add evacuate verb (2); move is the
default gesture (3); active-bank/shown-tab distinct with an unmistakable indicator (4);
LICE-drawn tabs + overflow, and both move affordances with drop-highlighting (5).
Folded into CONTEXT.md §Multi-bank + the B1B4 points above. Phase B is fully settled and
ready to scope into implementation waves. One polish detail remains:
- **Active-bank indicator placement** — per-region headers vs. single header readout
vs. lit-tab. "Unmistakable" is settled; only placement is open. Polish detail.
(touches B4)
- **B5 sample-remove forks (NEW, unsettled — need Daniel):** **R-A** — remove scope
(this-bank | all-banks | both); spec-of-record is this-bank-primary with a
scope-parameter seam. **R-B** — undo model for `"reasampler"` ext-state index
mutations (affects *all* of Phase B, surfaced by remove; move/copy/evacuate/
delete-bank/remove are not on REAPER Ctrl-Z as specced). Lean: a single-snapshot
ReaSampler-internal "undo last bank change." Both in `docs/product/removal-and-prune.md`.
---
# Phase R — Reclaim (file lifecycle: the prune path)
> **New pillar, own lettered namespace.** Prune is the file-lifecycle path the
> capture and multi-bank specs forward-reference throughout ("files persist on disk
> until prune") but that had no phase, module, or point. It is the **only** operation
> in ReaSampler that deletes bytes off disk. Namespaced **`R` (Reclaim)** alongside
> `M`/`D`/`B` because it is a distinct pillar — it serves *every* orphan-producing
> path (delete-bank, sample-remove B5, potentially M10 re-capture), not just
> Multi-bank, and it carries a new risk class (file deletion) with its own
> invariants. Authoritative spec: **CONTEXT.md §Prune — file-lifecycle spec**.
> Product framing + phase-placement justification + forks:
> `docs/product/removal-and-prune.md` §Prune. When a point lands, doc-keeper moves it
> to `COMPLETED.md`.
>
> **Boundary (load-bearing):** *remove creates orphans; prune reclaims them.* No
> operation other than prune deletes a file; prune deletes only files no index
> references. A bank op that deletes a file is still a bug.
>
> **Depends on:** B1, B2 (needs the multi-bank book to union the referenced-set
> across all banks) and B5 conceptually (sample-remove is a primary orphan-producer,
> so remove-then-prune is the coherent pair — mirror of evacuate-then-delete). Does
> **not** depend on the B3/B4 UI.
## R1 — prune-reconcile core (pure)
**Goal:** REAPER-free, filesystem-free reconciler — given the set of files present
in the bank folder and the set of files referenced by the book (unioned across all
banks, pool included), compute the orphan set. The mirror of
`ViewModeModel::reconcile(liveGuids)`, one level down (files instead of GUIDs).
CONTEXT.md §Prune (Module architecture — pure).
**Verify:** CTest green. **Prune null test:** a folder whose every file is
referenced deletes nothing; prune returns exactly `present referenced` and
nothing else. Referenced-set unioned across every bank (a file referenced by any
bank — including via a copy — is never an orphan).
- [ ] Prune-reconcile pure function: `(present, referenced) → orphans`, referenced
unioned across the whole book (copies keep a file alive).
- [ ] Tests: prune null test (all-referenced → empty); orphan = presentreferenced;
a copied file referenced by a second bank survives; empty folder / empty book edge
cases.
## R2 — prune shell + persist wiring (filesystem I/O, thin)
**Goal:** Enumerate the current project bank folder (M4 project-relative resolution),
supply the referenced-set (and, per fork R-D, the owned-file set) from the session,
feed the pure core, and produce a dry-run manifest. No deletion in this wave — the
report path only. CONTEXT.md §Prune (persist / prune shell).
**Verify (in DAW):** Dry-run reports the orphan count + reclaimed size (+ file list
for a small set) against the resolved current bank folder; resolves paths the same
way the index does (survives a Save-As relocation); deletes nothing.
**Depends on:** R1, B1, B2.
- [ ] Prune shell: enumerate the resolved current bank folder; feed the pure core.
- [ ] Session supplies the referenced-set (union across the book); resolve the bank
folder via the M4 project-relative machinery.
- [ ] Dry-run manifest: orphan count + reclaimed size (+ files for a small set);
**no deletion in this wave.**
## R3 — deletion + action (the destructive step, guarded)
**Goal:** The confirmed deletion step and the bindable "Prune bank folder" action:
dry-run-first, confirm-with-manifest, then reclaim the orphan set — via OS trash if
portably available (fork R-C), else unlink. CONTEXT.md §Prune (guardrails, API).
**Verify (in DAW):** "Prune bank folder" reports first, deletes only on explicit
confirm, and reclaims exactly the orphan set — never a referenced file, never a
hand-dropped non-bank file; the referenced/owned-set safety holds; non-bank and
capture invariants untouched.
**Depends on:** R2. **Forks R-C / R-D / R-E must be settled before this wave.**
- [ ] "Prune bank folder" action (`command_id`/`gaccel`/`hookcommand`),
dry-run-first, confirm-to-delete.
- [ ] Deletion mechanism (fork R-C): OS trash if a portable move-to-trash is
verified available, else unlink with the dry-run/confirm guardrail. **Verify the
platform move-to-trash surface before use.**
- [ ] Orphan attribution (fork R-D): reclaim only the bank system's own leavings,
not hand-dropped folder files (owned-file manifest per the lean — see below).
- [ ] Trigger (fork R-E): manual-primary; optional "…and prune now" offered at the
delete-bank confirmation; **no** background sweep.
## Phase R open questions (unsettled — need Daniel)
- **Fork R-C — deletion mechanism (OS trash vs. unlink).** Lean: trash if portably
available (recoverable), else unlink with strong dry-run/confirm. Needs Daniel +
a platform to-verify (SWELL / per-platform trash APIs).
- **Fork R-D — orphan attribution (owned-file manifest vs. index-diff vs.
folder-sweep).** Lean: **owned-file manifest**. **Design-the-seam call:** the
manifest is cheap to maintain from capture onward but a backfill cliff to
reconstruct later — so **start tracking owned files as part of capture / Phase B
now, even though prune ships in Phase R.** Folder-sweep rejected as unsafe (deletes
hand-dropped files). Needs Daniel (touches the persisted shape + ideally lands
earlier than R3).
- **Fork R-E — trigger (manual-only vs. offer-on-orphaning vs. periodic).** Lean:
manual-primary + optional delete-time "prune now"; no background sweep. Needs Daniel.