Repository navigation
spec(AST-086): a status is replaced by its owner's next status, not joined - #9070
Conversation
…oined FR9 joins every message written to a region before the previous one is exposed, so one owner's statuses written within one frame are all spoken: a search that states its result count on each keystroke says `1 result Create "Urg" Create "Urge" ...` when input outpaces a frame, as a barcode scanner, dictation or automation types. FR9 now replaces a status, a message that states its owner's current state, by its owner's next status: the previous one is withdrawn while it is not yet exposed. Messages from different owners still join, which is what the join is for.
|
The latest updates on your projects. Learn more about Vercel for GitHub.
|
|
AI review status for this pull request.
|
…d the modal containers The record's "status message" covers every message, so the replaced kind is a current-state message: one that states its owner's current state and is superseded by the owner's next one. FR9 says the owner is the announcing component instance and that messages sent with the public `useAnnounce` keep joining. FR13 makes a picker's empty-panel message its search's current-state message, so the empty panel and the search's count replace each other instead of joining. FR14's modal containers replace the same way. The FR9 and FR14 verification rows check the replacement and no longer fail a superseded message by design; Current-state impact names the owners that change, and DEC-4 records the decision.
Current records already decide that an owner's superseded current-state message is not spoken: `architecture:interaction-modality` INV8 (superseded work must not change the current status or announcement) and FR8's dropped busy start. DEC-4 now states that basis and names its recorder, instead of crediting a decider.
There was a problem hiding this comment.
This pull request changes a current requirement: spec:AST-086 FR9 says messages written before the previous one is exposed are joined and "none is dropped", and FR14 applies the same join inside modals. Approval therefore waits on an engineering owner deciding the change. Any engineering owner, the author included if they are one, records that decision by commenting /approve-spec <full head SHA> on the final head. The decision to settle is whether a component's pending current-state message (a result count, a create row or a picker's empty-panel message) is withdrawn by its next one, while other components' and useAnnounce messages keep joining. Two fixes are needed whatever the ruling:
- The
useAnnounceexception conflicts withspec:AST-075DEC-2 and Tokenizer's record, which send Astryx's own result counts throughuseAnnounce. Either amend them in the same decision or giveuseAnnouncethe same replacement. - DEC-4 says INV8 and FR8 already decide this rule, but INV8 leaves supersession to component contracts and FR8 covers busy starts only. Name the approving owner as Decider and keep INV8 and FR8 as the reason.
[Automated review]
| exposed has been heard and stays as it is. The owner is the announcing | ||
| component instance (FR1), so two pickers on one page each keep their own, | ||
| and messages from different owners still join. Messages sent with the | ||
| public `useAnnounce` keep joining: the channel cannot tell which of an |
There was a problem hiding this comment.
spec:AST-075 DEC-2 says Astryx's own comboboxes announce their result count with useAnnounce, the same hook products use, and Tokenizer's record says its sheet's count and empty-state text go through useAnnounce. If useAnnounce messages keep joining, either those counts keep joining (which contradicts the Impact entry at lines 312-320) or those records need amending in this decision. Alternatively, useAnnounce gets a way to mark a current-state message.
| **Reference:** `spec:AST-086/DEC-4` | ||
| **Decider:** vjeux (recorder), 2026-10-30 | ||
|
|
||
| Decided by current authority: `architecture:interaction-modality` INV8 |
There was a problem hiding this comment.
INV8 leaves supersession to component and family contracts, and its Restorative-change routing says it authorizes no new public delta. FR8 covers busy starts only, and FR9 currently says "none is dropped". This is an owner decision rather than a consequence of current authority, so the Decider line should name the approving owner, with INV8 and FR8 kept as the reason.
| except a current-state message its owner replaces. A current-state message | ||
| states its owner's current state and is superseded by the owner's next one, | ||
| such as a search's result count or create row; FR8 governs busy starts. It | ||
| is replaced by its owner's next current-state message, not joined: when the |
There was a problem hiding this comment.
As written, a component has one replacement slot. MultiSelector announces its selection count and its result count from the same instance, so a pending "2 of 5 selected" would be withdrawn by a result count in the same frame, although the count does not supersede it. Consider scoping the replacement to the next message about the same state, as the FR13 clause does with "its search's".
… per state Astryx's comboboxes compose `useAnnounce` for their result count, the same hook products use (`spec:AST-075` DEC-2), and Tokenizer's sheet sends its count and empty-state text through it, so an exception for `useAnnounce` would have left those counts joining. `useAnnounce` now takes one optional field, `currentState`, in an options argument after the urgency: a message marked with it replaces its owner's previous message about the same state, and a message without it keeps joining. Replacement is scoped to the same state, so one picker's selection count is not withdrawn by its result count. AST-075 FR7 and Tokenizer's record mark their counts. DEC-4's decider is pending the approving engineering owner, with INV8 and FR8 as its reasons.
…ce instance; a newer marked message wins across urgencies FR9 says what `currentState` takes: a string naming the state, compared by equality within one `useAnnounce` instance, so two marked messages are about the same state when that instance sends both with equal strings. A newer marked message replaces an older one about the same state whatever their urgencies: a polite message followed by an assertive one withdraws the polite one while it is not yet exposed, and the newer is spoken in the region of its own urgency. FR14's containers replace the same way across their two containers, the impact line names the type, and the FR9 verification row covers both.
There was a problem hiding this comment.
Approval waits on an engineering owner's decision: this pull request intentionally changes spec:AST-086 FR9 and FR14, which today join every message written before the previous one is exposed ("none is dropped"), and adds an optional currentState field to the public useAnnounce. An engineering owner records that decision by commenting /approve-spec <full head SHA> on the final head; the author may do so if they are an engineering owner. Independently of that decision, these record fixes are needed:
- FR9 defines the owner as "the component instance that sends it", which
useAnnouncecannot observe; define it as theuseAnnouncecall that returned theannounce, and scope FR13's own-content sentence to that same owner. - State the type of
currentState, for example a string the owner chooses that only replaces the same owner's message with the same string. - Reword the Intent's "hears each status change once" so it describes the current status.
[Automated review]
| exposed. | ||
| one is exposed are joined in order into one announcement; none is dropped, | ||
| except a current-state message its owner replaces. A current-state message | ||
| states one current state of its owner, the component instance that sends it |
There was a problem hiding this comment.
useAnnounce cannot see which component it runs in: each call returns its own announce. As written, a component that renders two comboboxes, each with its own useAnnounce() and {currentState: 'resultCount'}, is one owner announcing one state, so one count must withdraw the other's, which the hook cannot do. Defining the owner of a useAnnounce message as the call that returned its announce matches "two pickers … do not replace each other" and what an implementation can key on.
| replace each other, and every other message still joins. `useAnnounce` offers | ||
| the mark to every caller, Astryx's components and apps alike (`spec:AST-075` | ||
| DEC-2), as one optional field, `currentState`, in an options argument after | ||
| the urgency: `announce(message, 'polite', {currentState: 'resultCount'})`. A |
There was a problem hiding this comment.
The record shows one value, 'resultCount', but not the field's type. Please state whether currentState takes any string the owner chooses (and that only the same owner's message with the same string replaces it), so a product can tell whether {currentState: 'fromCount'} is valid (spec:AST-002 FR16, FR20).
| element is hidden, and leaves nothing behind after unmount. Action | ||
| announcements, such as a result count from a query handler or FR8 busy | ||
| starts, keep their own owner. Content revealed without a re-render of its | ||
| starts, keep their own owner. A message a component announces as its own |
There was a problem hiding this comment.
This sentence covers any component's own-content message that states the next state of "an owner's" message. An Astryx EmptyState inside a product's own combobox would then have to replace the product's pending count, but nothing links EmptyState to the product's announce. Scoping it to an empty-panel message from the same owner keeps the promise one the channel can keep.
There was a problem hiding this comment.
Approval waits on an engineering owner's decision: this pull request intentionally changes spec:AST-086 FR9 and FR14, which today join every message written before the previous one is exposed ("none is dropped"), adds an optional public currentState field to useAnnounce, and lets a newer marked message replace an older one across urgencies. An engineering owner records that decision by commenting /approve-spec <full head SHA> on the final head; the author may do so if they are an engineering owner. Independently of that decision, these record fixes are needed:
- Scope FR13's own-content sentence to an empty-panel message the owner renders for its own search, since nothing links another component's message, such as an EmptyState inside a product's combobox, to the owner's
announce. - Add the modal cases this head introduces to the FR14 Verification row: a marked polite message replaced by a marked assertive one, and a count replaced by the empty-panel message.
- Define FR9's owner as the
useAnnounceinstance rather than the component instance, and reword the Intent's "hears each status change once".
[Automated review]
| | FR8 | Shared test: no live role inside a busy owner; channel spy on the owner | Busy TextInput, Selector, Switch thumb, ChatMessageList loader, busy CommandPalette (region inside its dialog), operation that settles before its busy announcement is exposed | A composed indicator with a live role or its own announcement, or a busy owner that announces nothing, fails, or a busy start announced after its outcome, fails | | ||
| | FR9 | `useAnnounce` tests | First use, many components, region removed, server render, two messages in one frame, clear after a joined message, the same message twice in one frame, one owner's current-state messages written in one frame, two pickers' result counts in one frame, a result count followed by its picker's empty-panel message and the reverse, a current-state message already exposed when the next is written, unmarked `useAnnounce` messages in one frame, one picker's marked selection and result counts in one frame, a marked polite message followed by a marked assertive one about the same state, equal `currentState` strings from two `useAnnounce` instances | A second document-level region pair, stale text after the clear delay, a dropped same-frame message from another owner, about another state, or unmarked, a superseded current-state message spoken with its owner's next one, a polite message about a state still spoken after an assertive one about it, the newer spoken at the older's urgency, two `useAnnounce` instances' equal strings replacing each other, an exposed current-state message taken back, or a joined message surviving a clear fails, or an identical same-frame repeat announced twice, fails | | ||
| | FR10 | Accessibility-tree check with a Dialog open | Info and error toast shown over an open Dialog | A toast that reaches only the body-level channel, or no exposed live container, fails | | ||
| | FR14 | Hook tests and an accessibility-tree check with an Astryx modal open | Owner outside any modal, inside an open non-modal dialog, inside an open modal (polite and assertive, repeat of the same words, empty message), busy start inside a modal, busy start inside a native modal an app renders without Astryx, alone and opened inside an Astryx Dialog; a modal sheet's row toggle in a real engine, app code calling `useAnnounce` inside a Dialog, CommandPalette result count and Lightbox position message while each is open, a message followed by closing its Dialog before it is exposed (inside another open modal and on the page), a message pending when a CommandPalette opened over a Dialog closes, app code calling `useAnnounce` outside the Dialog it renders (body-level channel); a search's result counts written in one frame inside a modal sheet | A channel message from inside an open modal that reaches only the body-level channel, an FR14 container mounted holding its text, a second FR14 container of one urgency in one dialog, an FR14 container the engine's accessibility tree omits, a dropped message of two from different owners written before the first is exposed, a superseded current-state message spoken from an FR14 container, an empty message that leaves text in the modal's FR14 container, an app `useAnnounce` message from inside an Astryx modal that reaches only the body-level channel, a CommandPalette or Lightbox message about its open modal that reaches only the body-level channel, a message lost when its modal closes before it is exposed, or a pending message handed to a channel still inert after the close, a busy start inside an app-rendered native modal that reaches only the body-level channel, or one inside an app-rendered native modal nested in an Astryx Dialog that is delivered to the Dialog's FR14 container fails | |
There was a problem hiding this comment.
FR14 now replaces "in either container" and speaks the newer message "in the container of its own urgency" (lines 199-202), and Tokenizer's sheet, a modal, makes its count and empty-state text one current-state message. This row only adds "a search's result counts written in one frame inside a modal sheet". The modal containers have their own writer, so please add a marked polite message followed by a marked assertive one about the same state, and a count followed by the empty-panel message, both inside a modal, with their failures.
…ending message
The mark moves from a string key on each message to the instance:
`useAnnounce` takes an optional options object with one field,
`hasReplace` (default false), and the `announce(message, politeness)` it
returns is unchanged. A `useAnnounce` instance created with `hasReplace`
states one current state of its owner, such as a search's result count:
each message it writes withdraws that instance's previous message while it
is not yet exposed, whatever their urgencies, and is spoken in the region
of its own urgency; a message already exposed has been heard and stays.
Messages from other instances, and from instances without `hasReplace`,
still join. FR14's containers withdraw the same way, and FR13's picker
empty-panel message comes from its search's `hasReplace` instance.
AST-075 FR7, Tokenizer's sheet and MultiSelector's record send the search's
counts (and MultiSelector's create row) through a separate
`useAnnounce({hasReplace: true})` instance; selection keeps its own plain
one. DEC-4's decider stays pending, and the verification rows follow.
|
/approve-spec 26ab4b2 |
There was a problem hiding this comment.
An engineering owner has approved the direction of this change; two fixes to the records are still needed before it lands.
- Scope AST-086 FR13's new sentence to a picker's own empty-panel message: as written it also binds an Astryx
EmptyStatethat a product renders beside its ownuseComboboxfield, andEmptyStatehas no way to send through the product'shasReplaceinstance. - Add the modal cases this head introduces to the FR14 Verification row: a polite then an assertive message from one
hasReplaceinstance, and a result count followed by the empty-panel message, both inside a modal.
Editing the record text needs a fresh /approve-spec from an engineering owner on the final head.
[Automated review]
| starts, keep their own owner. An own-content message (FR11) that states the | ||
| next value of a state a `hasReplace` instance states comes from that instance | ||
| (FR9): a picker's empty-panel message comes from its search's `hasReplace` | ||
| instance, so it replaces the search's result count not yet exposed, and the | ||
| search's next count replaces it while it is not yet exposed. Content revealed |
There was a problem hiding this comment.
This sentence covers every own-content message that states the next value of a hasReplace instance's state, not only a picker's own panel. A product that renders an Astryx EmptyState beside its own useCombobox field (AST-075 FR7 gives that field its own hasReplace instance for its count and for "none") would need EmptyState's message to come from the product's instance, and EmptyState has no prop that reaches it. Could this be scoped to the owner's own message, for example "A picker's own empty-panel message (FR11) comes from its search's hasReplace instance (FR9), so it replaces …"?
| | FR8 | Shared test: no live role inside a busy owner; channel spy on the owner | Busy TextInput, Selector, Switch thumb, ChatMessageList loader, busy CommandPalette (region inside its dialog), operation that settles before its busy announcement is exposed | A composed indicator with a live role or its own announcement, or a busy owner that announces nothing, fails, or a busy start announced after its outcome, fails | | ||
| | FR9 | `useAnnounce` tests | First use, many components, region removed, server render, two messages in one frame, clear after a joined message, the same message twice in one frame, a `hasReplace` instance's messages written in one frame, two pickers' result counts in one frame, a result count followed by its picker's empty-panel message and the reverse, a `hasReplace` message already exposed when the next is written, messages from instances without `hasReplace` in one frame, one picker's result count and selection message in one frame (separate instances), a polite message followed by an assertive one from one `hasReplace` instance | A second document-level region pair, stale text after the clear delay, a dropped same-frame message from another instance or from an instance without `hasReplace`, a `hasReplace` instance's superseded message spoken with its next one, its polite message still spoken after its assertive one, the newer spoken at the older's urgency, an exposed message taken back, or a joined message surviving a clear fails, or an identical same-frame repeat announced twice, fails | | ||
| | FR10 | Accessibility-tree check with a Dialog open | Info and error toast shown over an open Dialog | A toast that reaches only the body-level channel, or no exposed live container, fails | | ||
| | FR14 | Hook tests and an accessibility-tree check with an Astryx modal open | Owner outside any modal, inside an open non-modal dialog, inside an open modal (polite and assertive, repeat of the same words, empty message), busy start inside a modal, busy start inside a native modal an app renders without Astryx, alone and opened inside an Astryx Dialog; a modal sheet's row toggle in a real engine, app code calling `useAnnounce` inside a Dialog, CommandPalette result count and Lightbox position message while each is open, a message followed by closing its Dialog before it is exposed (inside another open modal and on the page), a message pending when a CommandPalette opened over a Dialog closes, app code calling `useAnnounce` outside the Dialog it renders (body-level channel); a search's result counts written in one frame inside a modal sheet | A channel message from inside an open modal that reaches only the body-level channel, an FR14 container mounted holding its text, a second FR14 container of one urgency in one dialog, an FR14 container the engine's accessibility tree omits, a dropped message of two from different instances written before the first is exposed, a `hasReplace` instance's superseded message spoken from an FR14 container, an empty message that leaves text in the modal's FR14 container, an app `useAnnounce` message from inside an Astryx modal that reaches only the body-level channel, a CommandPalette or Lightbox message about its open modal that reaches only the body-level channel, a message lost when its modal closes before it is exposed, or a pending message handed to a channel still inert after the close, a busy start inside an app-rendered native modal that reaches only the body-level channel, or one inside an app-rendered native modal nested in an Astryx Dialog that is delivered to the Dialog's FR14 container fails | |
There was a problem hiding this comment.
FR14 now withdraws "in either container", but this row's new representative state is only a search's counts in one frame inside a modal sheet. Please add a polite message followed by an assertive one from one hasReplace instance inside a modal, and a result count followed by its picker's empty-panel message inside a modal sheet (Tokenizer's case). The modal containers are written by their own code path, so the FR9 row's page-channel states do not cover them.
…row covers the modal cases FR13's sentence bound every own-content message that states the next value of a `hasReplace` instance's state, so an Astryx `EmptyState` beside a product's own `useCombobox` field would have had to send through the product's instance, which it cannot reach. It now binds a picker's own empty-panel message. FR14's verification row gains the modal cases FR14 introduces: a polite then an assertive message from one `hasReplace` instance inside a modal, and a result count followed by its picker's empty-panel message inside a modal sheet.
|
/approve-spec 2b84901 |
There was a problem hiding this comment.
The records are ready to land: an engineering owner approved this head, and both fixes from the last review are in, with FR13 now covering only a picker's own empty-panel message and the FR14 Verification row naming the modal cases. Two optional wording tweaks can wait for a later edit:
- The AST-086 impact entry credits FR9, FR13 and FR14 for PowerSearch's adoption, but PowerSearch's count is its caller's
resultCount, which no requirement routes through ahasReplaceinstance. - The Intent's "hears each status change once" could read "hears each current status once" to match DEC-4.
[Automated review]
…thdrawn, not joined `spec:AST-086` FR9 joins messages written to a region before the previous one is exposed, so MultiSelector, which states its search's status on every keystroke, said every superseded status when input outpaced a frame, as a barcode scanner, dictation or automation types: `1 result Create "Urg" Create "Urge" ...`. meta#9070 records the fix. `useAnnounce` takes an optional `{hasReplace}`: an instance created with it withdraws its previous message while it is not yet exposed, whatever their urgencies, and the newer is spoken in the region of its own urgency; messages from other instances, and from instances without it, still join. Each write now tells its announcer how to take it back, wherever the route put it: the shared region, a modal's container, words held for an exiting modal, or a message waiting for its modal to be presented. `useAnnounceRenderedText` can speak through an owner's announcer. MultiSelector sends its result count, its create row and its empty-panel message (FR13) through one `hasReplace` instance; selection changes keep their own. The CreateRow spec reads each search status whole again instead of accepting the joined text. Red first: on main, four statuses in one frame were all said (now the last); a count then an empty panel, and an empty panel then a count, said both (now the newer), on the page and inside a Dialog; a `hasReplace` instance's messages joined in one frame, polite then assertive, inside a modal and in the commit that presents one; and the tightened CreateRow spec failed 6 of 10 runs alone and 35 of 48 at 8 workers.
…thdrawn, not joined `spec:AST-086` FR9 joins messages written to a region before the previous one is exposed, so MultiSelector, which states its search's status on every keystroke, said every superseded status when input outpaced a frame, as a barcode scanner, dictation or automation types: `1 result Create "Urg" Create "Urge" ...`. meta#9070 records the fix. `useAnnounce` takes an optional `{hasReplace}`: an instance created with it withdraws its previous message while it is not yet exposed, whatever their urgencies, and the newer is spoken in the region of its own urgency; messages from other instances, and from instances without it, still join. Each write now tells its announcer how to take it back, wherever the route put it: the shared region, a modal's container, words held for an exiting modal, or a message waiting for its modal to be presented, and only while those words still wait there: a handle never takes back equal words another announcer wrote or held after its own were exposed or delivered, and words held for an exiting modal count their announcers. `useAnnounceRenderedText` can speak through an owner's announcer. MultiSelector sends its result count, its create row and its empty-panel message (FR13) through one `hasReplace` instance; selection changes keep their own. The CreateRow spec reads each search status whole again instead of accepting the joined text. Red first: on main, four statuses in one frame were all said (now the last); a count then an empty panel, and an empty panel then a count, said both (now the newer), on the page and inside a Dialog; a `hasReplace` instance's messages joined in one frame, polite then assertive, inside a modal and in the commit that presents one; inside a modal, a count's handle took back another announcer's equal words written after its own were spoken, and every holder's share of words held through an exit; and the tightened CreateRow spec failed 6 of 10 runs alone and 35 of 48 at 8 workers.
…thdrawn, not joined `spec:AST-086` FR9 joins messages written to a region before the previous one is exposed, so MultiSelector, which states its search's status on every keystroke, said every superseded status when input outpaced a frame, as a barcode scanner, dictation or automation types: `1 result Create "Urg" Create "Urge" ...`. meta#9070 records the fix. `useAnnounce` takes an optional `{hasReplace}`: an instance created with it withdraws its previous message while it is not yet exposed, whatever their urgencies, and the newer is spoken in the region of its own urgency; messages from other instances, and from instances without it, still join. Each write gives its announcer a receipt kept beside the words: in the shared region, a modal's container, words held for an exiting modal, or a message waiting for its modal to be presented. When a modal hands its waiting words on (it leaves the accessibility tree, exits, closes, a step is left, or its write frame finds it unexposed), they move one delivery per announcer, and each receipt follows its words. A receipt takes back only its own share and only while the words wait, so it never reaches equal words another announcer wrote after its own were exposed. The busy-announcement hook takes its start back through the same receipt. `useAnnounceRenderedText` can speak through an owner's announcer. MultiSelector sends its result count, its create row and its empty-panel message (FR13) through one `hasReplace` instance; selection changes keep their own. The CreateRow spec reads each search status whole again instead of accepting the joined text. Red first: on main, four statuses in one frame were all said (now the last); a count then an empty panel, and an empty panel then a count, said both (now the newer), on the page and inside a Dialog; a `hasReplace` instance's messages joined in one frame, polite then assertive, inside a modal and in the commit that presents one, and when its modal handed its first message on before it was exposed; and the tightened CreateRow spec failed 6 of 10 runs alone and 35 of 48 at 8 workers.
…thdrawn, not joined (#8640) ## Why When input outpaces a frame (a barcode scanner, dictation, automation), a screen-reader user hears MultiSelector's stale statuses joined together: `1 result Create "Urg" Create "Urge" Create "Urgen" Create "Urgent"`. ## What led to it Found while checking the text-entry components' accessibility tests (part of bringing Astryx's touch behavior to parity with iOS's UIKit, text entry first). #8244 made the announcer join every message written before the previous one is read out (`spec:AST-086` FR9), so two components speaking in the same frame are both heard. MultiSelector states its search's status on every keystroke, so fast typing joins statuses that no longer apply. Its CreateRow accessibility spec failed on that, and now accepts the joined text. ## The record #9070, approved by the spec owner, amends `spec:AST-086` FR9, FR13 and FR14: a `useAnnounce` instance created with `hasReplace` withdraws its own previous message while it is not yet exposed, whatever their urgencies, and the newer is spoken in the region of its own urgency; messages from other instances still join. This pull request implements it and lands after #9070, so its public option is settled by that record, not decided here (`spec:AST-002` FR19). ## Options and tradeoffs - Debounce MultiSelector's status: delays every announcement, and every component would repeat it. - Keep the joined text: scanner and dictation users keep hearing stale counts. - Replace per announcer instance, in the shared announcer: fixes it once for every caller, and different instances still join. ## Why this choice It is the shape #9070 records. `useAnnounce({hasReplace: true})` is one optional field; the `announce(message, politeness)` it returns is unchanged. Each write gives its announcer a receipt kept beside the words, wherever the route put them: the shared region, a modal's container, words held for an exiting modal, or a message waiting for its modal to be presented. When a modal hands its waiting words on (it leaves the accessibility tree, exits, closes, a step is left, or its write frame finds it unexposed), they move one delivery per announcer and each receipt follows its words. A receipt takes back only its own share and only while the words wait, so it never reaches equal words another announcer wrote after its own were spoken. The busy-announcement hook takes its start back through the same receipt. MultiSelector sends its result count, its create row and its empty-panel message through one such instance; selection changes keep their own. Selector, Typeahead and the others adopt it separately. ## How to check it 1. Run `MultiSelector/CreateRow.a11y.chromium.spec.ts` in Chromium. It types into the create row at about four keystrokes per frame. It now reads the two search statuses exactly, in place of the `newestMessage` regex; the creation message, which comes from another announcer and can still join, keeps the regex. Expected: on main it fails on the joined statuses; with this change the polite region reads `1 result`, then `Create "Urgent"`. 2. With a screen reader, type into MultiSelector at a normal pace. Expected: each status is read as before. ## Evidence - CreateRow spec, tightened as above: on main 560c666 it failed 6 of 10 runs alone and 35 of 48 at 8 workers, reading `Create "Urge" Create "Urgen" Create "Urgent"`; with this change it passes 10 of 10 alone and 48 of 48 at 8 workers. - Unit tests that fail on main first: four statuses in one frame say the last; a count then an empty panel, and an empty panel then a count, say the newer; the same inside a Dialog's container; a `hasReplace` instance's messages in one frame, polite then assertive, inside a modal and in the commit that presents one; and its first message, handed on by its modal before it was exposed (the Dialog leaving the accessibility tree and coming back, a sheet's exit and close, the Dialog closing, a step left for the next one, or its write frame finding it unexposed), is taken back by its next. Guards that pass on main and with the change: other instances and plain instances still join, a shared share stays, an exposed message stays, a selection change and a count in one frame are both said, and inside a modal an equal message another announcer writes after the count's was spoken, and another announcer's share of words held while a sheet exits, both stay. - Local gates at 4e89f16 (one commit on main bb18c81): build, typecheck, repo checks and Prettier pass; the core UI suite passes (16,616 tests); the node suite passes except the two `cache.test.mjs` lock tests that fail on this machine without the change too (#8520).
Exact decision needed
Whether
spec:AST-086FR9's shared channel joins a component's superseded message about its current state with its next one, and the optional publicuseAnnounceoption that asks for replacement. Today every message written to a region before the previous one is exposed is joined, so a component that states its current state on every input, such as a search's result count, has all its stale states spoken together when input outpaces a frame (a barcode scanner, dictation, automation):1 result Create "Urg" Create "Urge" Create "Urgen" Create "Urgent".Existing authority searched
spec:AST-086FR9 (the join), FR6 (an identical message repeated in a frame is one message), FR8 (a busy start its outcome overtakes is dropped), FR13 and FR14 (own-content messages and modal containers).architecture:interaction-modalityINV8: superseded work must not change the current status or announcement, with supersession left to component contracts.spec:AST-075DEC-2 and FR7: Astryx's comboboxes announce their result count withuseAnnounce, the same hook products use. Tokenizer's record: its sheet's count and empty-state text go throughuseAnnounce. MultiSelector's record (draft): its result count and create row label go through the polite channel.spec:AST-002FR1, FR2, FR5, FR7, FR16 and FR20 for the new public option;has*booleans such ashasClearandhasPagingfor its name.Decision
A
useAnnounceinstance created withhasReplacestates one current state of its owner, such as a search's result count: each message it writes withdraws that instance's previous message while it is not yet exposed, whatever their urgencies, and is spoken in the region of its own urgency; a message already exposed has been heard and stays. Messages from other instances, and from instances withouthasReplace, still join.useAnnouncetakes an optional options object with one field,hasReplace(defaultfalse), and theannounce(message, politeness)it returns is unchanged. FR14's modal containers withdraw the same way, and a picker's own empty-panel message comes from its search'shasReplaceinstance (FR13). Astryx's search counts and MultiSelector's create row use a separateuseAnnounce({hasReplace: true})instance (spec:AST-075FR7, Tokenizer's and MultiSelector's records); selection messages keep their own plain instance. DEC-4 records it; its decider is the approving engineering owner, with INV8 and FR8 as its reasons.Public option (
spec:AST-002FR5, FR20): two otherwise identical messages,"7 results"written twice in one frame, need different outcomes. As a search's count it replaces the earlier one; as two distinct notices both are spoken. Only the caller knows which it sends, and the channel cannot derive it. Before:const announce = useAnnounce(); announce(t('resultCount', {count}));. After:const announceCount = useAnnounce({hasReplace: true}); announceCount(t('resultCount', {count}));. Existing calls are unchanged.Rejected alternatives
useAnnouncemessage: Astryx's comboboxes and Tokenizer announce throughuseAnnounce, so their counts would keep joining.announce(message, politeness, {currentState})): a third argument whose strings every call must keep consistent, where one instance already states one state.Claim scope and non-goals
FR9's replacement within one
hasReplaceinstance, the option that asks for it, and the AST-075, Tokenizer and MultiSelector sentences that describe their counts' announcement. Which messages a component sends through ahasReplaceinstance stays with its own record.Evidence and implementation relationship
useAnnounce({hasReplace: true}).Writing rubric results
Scope
Validation
Local gates at 2b84901:
pnpm check:knowledge(records and templates aligned), Prettier,git diff --check,pnpm check:repo.