Implement live accessibility announcements
The board speaks when files change under the user, per DESIGN/10 § Live board announcements. BoardDiff is the pure snapshot summarizer (identity sets for cards/lanes added/edited/moved/deleted — ids, not tallies, so pro-m1's semantic commit engine can build on it; edited = rendered content only, moved beats edited, implied events don't steal the subject). BoardAnnouncer is the decision seam: focusOutcome computes the vanishing-focus sentence and the walk-up-then-sideways recovery (next lane by order, else previous, board container only when none remain, never the trash); speech(for:) is the one-sentence precedence ladder — raised condition > bracket completion > cleared condition > vanished focus > digest — foreign-only for the last two rungs, so app-mediated echoes stay silent. BoardStore.land assembles ReloadFacts and posts exactly one sentence per reload through the injectable announce outlet (AccessibilityAnnouncer, medium priority, never interrupting). Selection recovery layers on top of ItemReferenceSet re-resolution — survivors veto, the emptied selection lands on the vanished item's lane and re-arms ⌘N's active-lane memory. performWholesale(announcing:) arms a completion phrase consumed by the closing reload — nil on every base bracket today; pro-m1 fills git phrasings. Locks raised outside the reload path (vanished root, unwritable location) announce through the same ladder, and the banner strip is a labeled "Board status" container whose row labels are the announced sentences (AccessibilityPhrases.bannerLabel — one string for eye and ear). Announcements classify at reload granularity (WatchOrigin) as a deliberate interim: DESIGN/02's EchoLedger (per-file classification, the announcer's specified input, git-free) was scheduled with the auto-committer that the edition split moved to pro-m1 — filed on the Redesign board for a ruling. 1533 unit tests green, both schemes build. Claude-Session: https://claude.ai/code/session_01SR4XGjmBE16ZUYWpfFHXwY
This commit is contained in:
@@ -118,6 +118,55 @@ struct WriterFixture {
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - Snapshots
|
||||
|
||||
/// The half of the fixture the **snapshot-comparison** suites need (`BoardDiffTests`,
|
||||
/// `BoardAnnouncerTests`): boards written as files and read back through the real loader.
|
||||
///
|
||||
/// They compare `BoardModel` values, and a hand-assembled model would be assembling something
|
||||
/// `BoardLoader` can never produce — a lane with a malformed `order`, a card whose `document` does
|
||||
/// not match its fields. Writing bytes and loading them is the only way the two snapshots in a diff
|
||||
/// are the two snapshots a reload would actually have compared.
|
||||
extension WriterFixture {
|
||||
|
||||
/// `<root>/index.md`, the one file every board must have.
|
||||
@discardableResult
|
||||
func board(title: String = "Board", body: String = "Board description.") throws -> URL {
|
||||
try item("", "---\nschema: 1\ntitle: \(title)\n---\n\(body)\n")
|
||||
}
|
||||
|
||||
@discardableResult
|
||||
func lane(_ id: String, order: String, title: String, width: Int? = nil, body: String = "") throws -> URL {
|
||||
let widthLine = width.map { "width: \($0)\n" } ?? ""
|
||||
return try item(id, "---\nschema: 1\ntitle: \(title)\norder: \(order)\n\(widthLine)---\n\(body)\n")
|
||||
}
|
||||
|
||||
@discardableResult
|
||||
func card(
|
||||
_ id: String,
|
||||
in laneID: String,
|
||||
order: String,
|
||||
title: String,
|
||||
body: String = "",
|
||||
modified: String? = nil
|
||||
) throws -> URL {
|
||||
let modifiedLine = modified.map { "modified: \($0)\n" } ?? ""
|
||||
return try item("\(laneID)/\(id)", "---\nschema: 1\ntitle: \(title)\norder: \(order)\n\(modifiedLine)---\n\(body)\n")
|
||||
}
|
||||
|
||||
/// A card written straight into `<root>/.trash/` — the materialized trash's shape, for the
|
||||
/// diff rule that says churn in there is not a change to the board.
|
||||
@discardableResult
|
||||
func trashCard(_ id: String, order: String, title: String) throws -> URL {
|
||||
try item(".trash/\(id)", "---\nschema: 1\ntitle: \(title)\norder: \(order)\n---\n\n")
|
||||
}
|
||||
|
||||
/// The board as the loader reads it right now — the value a reload would have landed.
|
||||
func snapshot() throws -> BoardModel {
|
||||
try BoardLoader.load(boardRoot: root).model
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - Move/copy identities
|
||||
|
||||
/// Literal UUID-shaped names for the move/copy suites, which need more of them than
|
||||
|
||||
Reference in New Issue
Block a user