Comments, phase 3 — search, the thread find, announcements, and a11y

Board search reaches comment bodies through a search-owned transient
index: the first live-query keystroke sweeps comments/*/index.md
off-actor (.draft and comments/.trash excluded), keystrokes re-filter
in memory, the index discards on clear — the snapshot stays O(cards).
⌘F routes by focus: the comments pane gets an app-owned find bar
spanning the whole rendered thread (next/prev cross rows with
wraparound); body and composer keep NSTextFinder; Find Next/Previous
graduate from FutureCommands. Foreign comment changes speak
path-shaped beside the announcer's ladder ("New comment on 'X'",
plural folds), narrowed by EchoLedger receipts consumed through
CommentPath.classify — and that read fixed a latent footprint bug
where a comment receipt resolved against the card's attachment
listing, read .absent, and classified the user's own write as
foreign. The pane completes its a11y story: flattened comment
elements with Edit/Delete/Reveal custom actions (un-flattening
during inline edit), phrase-table vocabulary, labeled composer and
sort control, and an audit over the open pane on a comment-seeded
fixture (runnable only where automation permission exists).

Claude-Session: https://claude.ai/code/session_01SR4XGjmBE16ZUYWpfFHXwY
This commit is contained in:
2026-07-30 21:30:22 -04:00
parent fe3ffac48e
commit 9588f7b1f0
31 changed files with 3036 additions and 73 deletions
+38
View File
@@ -199,6 +199,44 @@ struct UITestFixtureBoardTests {
#expect(richCard.attachments == [UITestLaunch.attachmentName])
}
/// The rich card's **comment thread** what the comments-pane audit is an audit *of*
/// (10-accessibility.md Comments).
///
/// The three shapes are the point: an ordinary comment, an unattributed one, and an edited one.
/// Each drives a different branch of the author line a flattened comment element wears
/// (`CommentAuthorLine`), and a fixture that quietly lost one of them would leave the audit
/// passing over a thread of three identical rows.
@Test("The rich card carries a thread the pane's audit can read")
func fixtureSeedsAThread() throws {
UITestLaunch.prepareScratchDirectory()
defer { try? FileManager.default.removeItem(at: UITestLaunch.scratchRoot) }
let root = try UITestLaunch.materializeFixtureBoard()
let model = try BoardLoader.load(boardRoot: root).model
let lane = model.lanes[UITestLaunch.richCardIndex.lane]
let card = try #require(lane.cards.first)
let cardFolder = ItemPath.card(lane: lane.id, id: card.id).folder(under: root)
let thread = CommentThread.load(inCard: cardFolder, path: "\(lane.id.rawValue)/\(card.id.rawValue)")
#expect(thread.comments.count == UITestLaunch.commentBodies.count)
#expect(thread.strays.isEmpty)
// Posting restamps `created`, so the thread's chronological order is the order they were made.
#expect(thread.comments.map(\.body) != [])
let unattributed = try #require(
thread.comments.first { $0.body.contains("no `author` key") }
)
#expect(unattributed.author.value == nil)
#expect(!unattributed.isEdited)
let edited = try #require(thread.comments.first { $0.body == UITestLaunch.editedCommentBody })
#expect(edited.isEdited, "the edited marker is `modified` differing from `created`, and no extra field")
#expect(edited.author.value != nil)
// The board walk never loads any of it the pane reads its own thread (01 Enhanced schema).
#expect(!card.body.contains("specimen thread"))
}
/// Everything the fixture launch writes stays inside the app's own container the sandbox
/// constraint that decided the whole design (a path handed over on the command line would not be
/// readable), stated as a test so a future "just use `/tmp`" cannot land quietly.