Realign code with the 2026-07-31 findings-resolution rulings

The full bullet list from Implementation card bf080d9a — both ruling
batches, including the three appended mid-session by 16ef377:

- Restore subjects compose the inverse, never nest: crossing "Undo: S"
  emits "Redo: S" and vice versa; parity, not stack depth, reads a
  legacy double prefix (GitHistoryProvider.restoreSubject).
- Git-operation failures join the one-shot failure banner tier:
  BannerCenter.GitFailureBanner (undo/redo/branchSwitch/addGit), error
  tone at failure rank merged with write one-shots by recency; the
  postLoss compromise is retired at both AppModel wirings.
- order/schema optional below the board root: append-at-end reading
  (ordered siblings first, folder-name tie-break among the order-less),
  schema reads 1, both coerce-tier logged; the root keeps its
  requirements. Ranks.resolvedOrders materializes finite ranks so
  models and placement math stay untouched; first Writer rewrite
  stamps a real rank on touch, placement against an order-less sibling
  stamps that sibling inline in the same bracket. Agent guide v10
  teaches optional keys and zero-read filing. Hostile-YAML order
  shapes become coercion tests; Fixtures/Valid/optional-keys.kanban
  replaces the four retired Malformed boards.
- .gitignore is the relocation-heal noise gate: GitignoreRules pure
  matcher (standard semantics, board-root file only), loader consults
  it once per walk so matched loose files keep the stray posture;
  seeded (.DS_Store + .*.lanework-*) at board creation and template
  instantiation, healed in when missing at open — repo-nested
  included; empty file honored, existing files never edited; the
  committer's obedience via libgit2 status is pinned by test.
- Comments crash-residue sweep gates on step ownership: HistoryStep
  derives backing from its own undo expectations, backedContent unions
  both stacks, the sweep purges per-entry only what no live step owns.
- Skip-purge decoupled (16ef377): a stale-skipped coarse step strands
  whole in NativeHistoryProvider.strandedSteps — still backing, retired
  only at session end; clean exits purge as before.
- Coarse close step named "Changes to '<card>'"; the fine body-edit
  wording never leaks onto the board menu.
- Branch-switch settle clears every open card window's fine stack on
  Save All and Discard alike; the empty fold registers no coarse step.
- Close flush awaits its covering snapshot (quiesce + one generation
  bump, 1s bound), and an explicit flush now queues behind an
  in-flight one instead of skipping — the audit-caught interleaving
  could lose a close flush permanently when the debounce fired inside
  the close sequence; regression tests force both races.
- Commit comment bullets sort chronologically by created, not UUID.
- The production-unwired CardBodyEditSession.editSessionDidChange seam
  is deleted with its seam-only tests.
- Composition-root pins: beginSession composes the committer with the
  store's own EchoLedger and binds the announcer (the miswire class).
- Deliberate 06 conformance pass over every 2026-07-31-tagged
  sentence: fixed Change-custom-key subjects (the retired named
  generic was the only producer), the unbuilt Replace attachment
  vocabulary, heal commits now authored Lanework Integrity, the config
  reader scopes identity to plain [user] sections, add-git re-runs
  detection at create (a stale mode-none could initialize inside the
  user's repo), and add-git failures answer at the form or the banner.
  Structural residue filed on the Redesign board.

2554 tests / 439 suites green.

Claude-Session: https://claude.ai/code/session_01CqjXB7ASoWtbyoGod68k97
This commit is contained in:
2026-08-01 07:43:45 -04:00
parent 16ef3779e8
commit 274ccd9ff5
75 changed files with 5619 additions and 791 deletions
+14 -4
View File
@@ -235,8 +235,15 @@ struct BoardGitControls: View {
// MARK: - Commit identity
/// **The name and email that repo-local `.git/config` carries** (06-history-undo.md Interaction
/// with external writers: "The board popover's git section exposes name/email fields that write that
/// repo-local config the setting *is* the file, portable to any git client, per-board by nature").
/// with external writers: "The board settings sheet's identity section exposes name/email fields
/// that write that repo-local config the setting *is* the file, portable to any git client,
/// per-board by nature").
///
/// **Its home is the sheet, and the sheet is not built yet.** The 2026-07-31 popover/sheet split moved
/// every setup-shaped control add-git, branch creation, these fields, remote and credentials out
/// of the popover and into a board settings sheet (03-board-ui.md Board settings sheet), leaving the
/// popover the daily face. This view is the fields, hosted where they were; the sheet's card moves
/// them, along with the visibility-scoped poll below, which 06 says "rides with the fields".
///
/// ### The placeholder is the whole of the identity rule made visible
///
@@ -270,8 +277,11 @@ private struct BoardGitIdentityFields: View {
case email
}
/// How often an open popover re-reads the config file. Slow enough to be free, fast enough that a
/// terminal `git config user.email ` shows up while the user is still looking at the popover.
/// **The fields re-read the config at 2 s while the surface is visible** (06 Interaction with
/// external writers, blessed 2026-07-31): "the watcher never delivers `.git`, so no board event
/// can carry a terminal-side config edit the unfocused-resync courtesy needs its own signal, and
/// a visibility-scoped poll is the 15 s paused-state re-read's shape at sheet cadence (a focused
/// field keeps its keystrokes; dismissing the sheet stops the poll)."
private static let pollInterval: Duration = .seconds(2)
var body: some View {
+7
View File
@@ -424,6 +424,13 @@ private struct BoardGitAddAction: View {
.fixedSize(horizontal: false, vertical: true)
}
}
// **The form add-git answers at** (06 Interaction with external writers, ruled 2026-07-31
// "Form-anchored operations answer at the form first"; its container moved to the board
// settings sheet in the 2026-07-31 split, and these two lines are what that sheet re-points).
// Appearing claims the inline surface; disappearing gives it up, which both dismisses the
// stale error and sends any answer still in flight to the banner instead of to nobody.
.onAppear { git.noteFormVisible(true) }
.onDisappear { git.noteFormVisible(false) }
}
}
+20 -39
View File
@@ -43,12 +43,18 @@ import Observation
/// `NSUndoManager` of its own, which is what makes it session-scoped). What lives here is the other
/// half of 05 Edit's undo sentence: the **session**, whose end is the effective Save.
/// `endEditSession()` is that moment the EditPreview flip, raw-source entry, or the window
/// closing and it is deliberately a named call rather than a side effect of `flush()`, because
/// pro-m1's auto-commit coalesces exactly here: every debounced tick inside one session rides its
/// own `performWrite` bracket, and the committer's rule is one commit per *session*, "never per save
/// tick" (06-history-undo.md Rules Auto-commit). In the free tier there is no committer, so
/// the two calls do the same work today; the seam is what keeps them from having to be pulled apart
/// later.
/// closing and it is deliberately a named call rather than a side effect of `flush()`, because it
/// is where this session's **one undo step** is registered: every debounced tick inside a session
/// rides its own `performWrite` bracket, and 13-native-undo.md's rule is one step per session, never
/// per save tick (`registerUndo`).
///
/// **The commit boundary is no longer here** (06-history-undo.md Rules Auto-commit, widened
/// 2026-07-31 "Board history sees card-window sessions, not gestures"): the committer's
/// stage-around used to open and close with this flip, and it now opens with the *window* and
/// releases when the window's session ends, so a comment posted with the body in Preview is inside
/// the same one commit as the body. What this type still owes that model is `isEditing`, read as
/// unsaved-content state rather than announced (`CardWindowHost`); the announcement seam it used to
/// wire went with the widening.
@MainActor
@Observable
public final class CardBodyEditSession {
@@ -110,23 +116,6 @@ public final class CardBodyEditSession {
@ObservationIgnored
public var registerUndo: ((_ priorBody: String, _ newBody: String) -> Void)?
/// **The session boundary, announced** called with `true` when an Edit session opens and
/// `false` when it ends, and with nothing in between.
///
/// **No longer the committer's stage-around boundary** (06-history-undo.md Rules Auto-commit,
/// widened 2026-07-31 "Board history sees card-window sessions, not gestures"): the exclusion
/// used to open and close with this flip, and it now opens with the *window* and releases when its
/// session ends, so a comment posted with the body in Preview is inside the same one commit as the
/// body. `CardWindowHost` therefore no longer wires this to anything, and the EditPreview flip is
/// a save point rather than a commit point.
///
/// The seam stays, unwired, because it is the only announcement of the boundary this type makes
/// and the ordering it carries that the flush precedes the announcement is a property worth
/// keeping proved (`AutoCommitTests`). A closure for `save`'s reason exactly: this type is a
/// buffer and a clock, and it stays testable by having no idea what a repository is.
@ObservationIgnored
public var editSessionDidChange: ((_ isEditing: Bool) -> Void)?
/// What disk said before this session's **first** landed save the step's before-value, held
/// from the first write until the session ends.
///
@@ -194,16 +183,16 @@ public final class CardBodyEditSession {
/// because its card's body was empty (`CardBodyMode.opening(body:)`).
///
/// Idempotent, because the mode can be re-asserted by a menu validation pass or a re-published
/// focus value, and a second announcement would register a session that is already registered.
/// focus value, and `isEditing` is observed: assigning `true` over `true` would still notify, and
/// the notification is a view rebuild for a session that never changed (`adopt(diskBody:)`'s
/// equal-assignment note, one property along).
public func beginEditSession() {
guard !isEditing else { return }
isEditing = true
editSessionDidChange?(true)
}
/// The end of one Edit session the flip back to Preview, raw-source entry, or the window
/// closing. Flushes, and marks the boundary pro-m1's auto-commit coalesces on (see the type's
/// doc comment).
/// closing. Flushes, then registers the session's one undo step (see the type's doc comment).
@discardableResult
public func endEditSession() -> CardBodyWriteOutcome {
let outcome = flush()
@@ -216,14 +205,9 @@ public final class CardBodyEditSession {
registerUndo?(origin, disk)
}
sessionOriginBody = nil
// **Last**, after the flush and after the undo step: the committer's nudge must find the
// session's final bytes already on disk, or the commit it arms would carry the file as it
// stood one keystroke ago. Guarded on `isEditing` so a window closing from Preview which
// calls this too, and should announces nothing.
if isEditing {
isEditing = false
editSessionDidChange?(false)
}
// Guarded on `isEditing` for `beginEditSession`'s reason a window closing from Preview
// calls this too, and should, but has no session flag to move.
if isEditing { isEditing = false }
return outcome
}
@@ -243,10 +227,7 @@ public final class CardBodyEditSession {
cancelPending()
text = disk
sessionOriginBody = nil
if isEditing {
isEditing = false
editSessionDidChange?(false)
}
if isEditing { isEditing = false }
}
/// `DirtyBufferGuard`'s `attemptSave`: the same flush, with a real failure raised instead of
+8
View File
@@ -105,6 +105,14 @@ public final class CardComments {
/// The crash-residue sweep, run once when the window opens
/// `BoardStore.sweepCommentTrashResidue(inCard:)`.
///
/// **It is a sweep of *residue*, not of the trash** (13-native-undo.md Interaction with the
/// trash, ruled 2026-07-31): content a live board step still restores from is that step's backing
/// and is left standing, so reopening a card can never destroy the last session's undo. **Nor the
/// bytes a collision left**, since the same day's skip-purge decoupling: a coarse step popped as
/// stale applied nothing and destroys nothing, and what it was holding stands until the board
/// session ends. The gate lives store-side, where the stack is; this seam is unchanged and still
/// called exactly once, by `open()`.
@ObservationIgnored
public var sweepTrashResidue: (() -> Void)?