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
+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