/// 04-interactions.md's **⌘N target rule** (settled), as a pure function of the three things it /// reads — the selection, the last-active lane, and the snapshot (`NewCardTargetTests`). /// /// The rule verbatim, and each clause's branch below: /// /// > with a card selected, the new card is created in that card's lane, immediately after it /// > (paste-anchor consistency); with a lane selected, appended at its bottom (Return consistency); /// > … with nothing selected — or a **tombstoned** selection, which never anchors creation — the /// > **last-active lane** — the lane that most recently held selection or a creation in this window /// > session — falling back to the first lane. … **Zero-lane board**: card creation … disable[s] via /// > menu validation until a lane exists. /// /// **A pure function rather than a method on the store** for the reason every rule in this codebase /// that can be one is: the five branches are five lines of test rather than five UI states to drive, /// and the menu item's `disabled` and its action then read the *same* answer instead of two /// hand-kept-in-sync conditions. /// /// ### What it deliberately does not decide /// /// - **The lane header's new-card button overrides this rule entirely** (11-command-nexus.md ▸ /// Pointer grammar, settled): "the click names its target lane, selection notwithstanding". That /// call site passes its own lane and never comes here. /// - **Return on a selected lane** is the same target as this rule's lane branch, but it is reached /// by grammar rather than by the menu; it also passes its lane directly. /// - **Multi-selections.** The rule speaks of "a card"/"a lane", singular, and a multi-selection has /// no "it" to be immediately after. Anything but a sole selection falls through to the /// last-active lane, which is the same answer an empty selection gets — the honest reading, and /// the one m5's selection-model card can refine if the design ever grows a plural case. enum NewCardTarget { /// Where a new card goes: which lane, and which card it lands immediately after (`nil` = the /// lane's bottom). Exactly `NewCardPlaceholder`'s two anchoring fields, because that is what /// this resolves *into*. struct Resolution: Equatable { let laneID: ItemID let anchorCardID: ItemID? } /// The target, or `nil` when there is none — **the zero-lane board**, where "New Card, /// Return-creation, and Paste with a card payload disable via menu validation until a lane /// exists". `nil` is therefore the menu item's `disabled` condition as well as its refusal, so /// the two can never disagree. /// /// - Parameters: /// - selection: the board's current selection, liveness side included. A `.trashed` selection /// "never anchors creation" and is treated exactly as an empty one — the settled precedent /// 04 ▸ Clipboard cites for paste, applied here to its source rule ("a trashed card's live /// disk-lane never leaks in as 'the selected card's lane'"). /// - lastActiveLaneID: `TransientBoardState.lastActiveLaneID`, already cleared by the reload /// rule if its lane vanished — but re-checked here anyway, because a caller need not have /// reloaded since the lane went. static func resolve( selection: ItemReferenceSet, lastActiveLaneID: ItemID?, snapshot: BoardModel ) -> Resolution? { let lanes = snapshot.lanes.filter { !$0.isDeleted } guard !lanes.isEmpty else { return nil } if selection.liveness == .live, selection.ids.count == 1, let id = selection.ids.first { for lane in lanes { // A lane selected: appended at its bottom, Return consistency. if lane.id == id { return Resolution(laneID: lane.id, anchorCardID: nil) } // A card selected: its lane, immediately after it — paste-anchor consistency. if lane.cards.contains(where: { $0.id == id && !$0.isDeleted }) { return Resolution(laneID: lane.id, anchorCardID: id) } } // The id names nothing the board renders — a selection the next reload will drop. // Falls through to the last-active lane rather than refusing: the user pressed ⌘N and // the board has lanes. } // Nothing selected, a tombstoned selection, a multi-selection, or a stale one: the lane that // most recently held selection or a creation, and the first lane when there is no such lane // (or it has since gone). if let lastActiveLaneID, let lane = lanes.first(where: { $0.id == lastActiveLaneID }) { return Resolution(laneID: lane.id, anchorCardID: nil) } return lanes.first.map { Resolution(laneID: $0.id, anchorCardID: nil) } } }