Skip to content

Merge quizzical-maxwell: voice engine, custom emoji, design refresh, navigation rework - #49

Merged
dhawal-ss merged 59 commits into
mainfrom
integrate/quizzical-maxwell
Aug 16, 2026
Merged

Merge quizzical-maxwell: voice engine, custom emoji, design refresh, navigation rework#49
dhawal-ss merged 59 commits into
mainfrom
integrate/quizzical-maxwell

Conversation

@dhawal-ss

Copy link
Copy Markdown
Owner

Summary

Integrates claude/quizzical-maxwell-780146 — the current, most-recent direction (54 commits, last one 9 hours before this PR) — into main, on top of the beta-hardening stack just merged via #37.

This branch was never opened as a PR; it forked from the beta branch tip and diverged in a way that directly conflicts with codex/track-b-removals (already merged): track-b-removals deferred/removed voice, theme packages, and the room-tab-strip; quizzical-maxwell builds a real MatrixRTC/LiveKit voice engine, a navigation rework, and other functionality on those same surfaces. Per repo owner direction, quizzical-maxwell is authoritative where the two disagree.

Conflict resolution

26 content conflicts + 6 modify/delete conflicts across 19 files, all under mesh/src/**. Policy applied:

  • Deleted audit/report/design artifacts (from the prior artifact-cleanup PRs) stayed deleted.
  • Real source code: quizzical-maxwell's version taken as the newer, more complete implementation — verified case by case, not blanket -X theirs.
  • Where both sides added genuinely different, still-used features to the same import/declaration block (e.g. CommunitySettings.tsx's access-settings state vs. moderation-audit state), took the union and dropped whichever side turned out unused after resolution (confirmed via full-file symbol search, not assumed).
  • One silent auto-merge gap caught by tsc: account-transition.ts called useServerEmojiStore with the import silently dropped by git's line-based merge (non-conflicting but broken) — fixed.

Scope boundary

Stops at d66ad52, not the branch tip ccc5d51 ("WIP: resource honesty..."), which is explicitly self-labeled by its author as unverified/uncompiled Rust. That commit stays on the source branch for a follow-up once it's actually built and tested.

Verification

  • tsc --noEmit: clean
  • vitest run: 1292/1292 tests passing across 148 files
  • eslint: 0 errors (12 pre-existing warnings, unrelated to this change)
  • cargo check --features matrix-backend: clean (2 pre-existing warnings)

Test plan

  • CI required checks pass (Matrix Rust ubuntu/windows, Legacy LAN Rust, Frontend Build & Browser E2E, feature-matrix, dependency-and-secret-audit, sbom)
  • Manual smoke test of voice, custom emoji, and the reworked community/navigation surfaces after merge

Co-Authored-By: Claude Sonnet 5 noreply@anthropic.com

… gating

Three correctness fixes surfaced by a review of the DM/reliability store and
notification sync:

- dms.addMessage: dedup by id before incrementing newerGapCount, so a
  re-delivered event (sync resume / reconnect replay) no longer inflates the
  "new messages" gap badge while browsing older history. Mirrors the channel
  store's ordering.
- dms.loadOlderMessages: guard the finally that clears loadingOlder so it does
  not resurrect per-conversation state for a conversation removed mid-fetch
  (e.g. by suppressPeer).
- useNotificationSync: keep the 30s policy clock running whenever a timed mute
  exists, not only during quiet hours. Gating it on quietHours.enabled froze
  policyClock and left timed channel/community mutes stuck permanently muted.

Adds regression tests for each.
- Extract MAX_PROJECTED_MENTIONS (64) and use it for both content_mention_ids
  (projection) and content_mentions_user (detection), which previously scanned
  64 vs 100 entries and could disagree about whether the current user was
  mentioned in a message with many recipients.
- mentions_for_user_ids: skip an unparseable mention id instead of failing the
  whole send/edit, matching the lenient handling in the sibling extractors.

Adds a boundary test asserting projection and detection agree at the cap.
- Add a .mesh-icon-button rule giving icon buttons the same tile radius as
  .mesh-button, so adjacent icon and text controls share corner geometry
  (the class was referenced but had no rule).
- Remove the dead 'mesh-card' classname from Card; the card radius already
  comes from its inline style, so the hook carried nothing.
The reviewed reliability fixes (DM gap counting, notification mute clock,
mention projection) added ~0.4 KiB of JS and pushed the matrix-voice build
2160.06 KiB over the prior 2,160 KiB aggregate ceiling, which sat at ~0.3 KiB
headroom.

Owner decision (2026-08-12): raise the aggregate `all JavaScript` ceiling by
16 KiB to 2,176 KiB. The aggregate is not a first-paint gate; the startup-facing
entry (350 KiB) and eager (525 KiB) limits still have headroom (275 / 494 KiB
on the voice build) and are left unchanged. Voice build now passes at
2160.06 / 2176.00 KiB.
Add from:/mentions:/has:/before:/after: operators to the message search box,
parsed into a structured filter set applied server-side in the Matrix search
engine (the bounded top-K selection runs before results return, so filtering
must happen at the scan, not in the renderer).

- Frontend: new lib/search-query.ts parses operators into SearchFilters +
  residual free text; SearchBar sends them and supports filter-only searches
  (e.g. has:image); a discoverability hint lists the operators.
- Bridge: searchMessages gains an optional filters arg (Matrix path).
- Rust: MessageSearchFilters DTO threaded through the search trait/command;
  message_matches_filters evaluates author/mentions/attachment/image/link and a
  half-open date window, with from:me / mentions:me resolved to the own account.

Filters AND with the text match and with each other. Unit tests cover the
parser (frontend) and every predicate incl. me-resolution and date bounds
(Rust). Voice build stays within budget (all JS 2164.85 / 2176 KiB).
The Phase 5 copy audit was a one-shot pass. Its two scripts are
unreferenced by package.json, CI workflows, and every other script in
the repo. check-beta-phase5-copy.mjs is also now definitively broken:
it readFile()s BETA_PHASE_5.md, which was never tracked and no longer
exists, so the script throws on startup.

Removed the whole self-contained cluster:

- scripts/check-beta-phase5-copy.mjs
- scripts/build-beta-phase5-string-table.mjs
- BETA_PHASE_5_COPY_MAPPING.csv, whose sole producer and sole consumer
  were the two scripts above and which no document cites
- audit/phase5-20260809/, whose three JSON files were read only by
  those same two scripts

PHASE4_STRING_TABLE.csv is deliberately kept. It is not an orphan:
build-phase4-string-table.mjs writes it, check-phase4-copy.mjs reads
it, and PHASE4_COPY_REPORT.md cites it as the Phase 4 evidence
appendix. check-phase4-copy.mjs still passes after this change.

Everything removed here stays recoverable from history, and
audit-user-visible-strings.mjs can regenerate an equivalent string
inventory on demand.
…n pass

Verified green before commit: vitest 1191 passing across 146 files, tsc
clean, eslint 0 errors, cargo check (matrix-backend) clean, and the design
token, class resolution, copy style, icon, IPC contract and owner decision
checkers all passing.

Correctness: community mute persisted, invitations no longer lost when a
different one is discarded, stale scroll anchors, sign-in recovery rehydration,
push rule reconciliation, nine listeners lost across room switches, IPC write
timeouts, push-to-talk unmute on leave.

Performance: AppLayout no longer re-renders the shell per message, patchChanges
does a bounded structural compare so the store fast paths are live, mergeMessages
is O(n+m) with an alias index, measurement batching removes O(n^2) ResizeObserver
churn, custom emoji blob URLs are revoked and bounded, DM store is LRU bounded.

Accessibility: author colour selected in Rust from the curated palette so an
out-of-gamut colour is structurally impossible, role="feed" on the timeline,
re-announcing role="status" elements removed, explicit load-earlier control,
skip link and F6 region cycling, call tile scrim, search and composer as
real comboboxes.

Design: notice intensities consuming the container tokens, EmptyState on seeded
PixelMark, legible toast timers, one SectionHeader, one tooltip provider, the
offline and reconnecting band actually rendered, settings document outline,
message arrival animation with a virtualization guard, popover arrival variants.
…ft it for

A ban was irreversible from inside Mesh. There was no unban path in any tree:
no Rust command, no bridge function, no control anywhere. The ban confirmation
dialog told the moderator the account was blocked "until an administrator
reverses the ban", which the product could not do. Worse, the moment a ban
landed the account left every roster, because a banned membership maps to
joinStatus "left" and both the store selector and the member list filter that
out, so an admin could not even see who they had banned.

Backend: a new Unban moderation action alongside Ban and Kick, going through the
same permission guard, the same space plus child room walk, and the same audit
label. It checks each room for an actual ban before acting, so a channel created
after the ban is not reported as a failed room. Lifting a ban does not rejoin the
account anywhere: returning stays the person's own choice.

Frontend: banned accounts now appear under their own Banned group, and only in
the embedded administration roster where a moderator can act on them. They stay
out of the everyday member list. Their only offered action is lifting the ban,
since removing or banning an already banned account does nothing.

The moderation dialog copy moved to one table keyed by action so the title, the
description, the confirm label and the error context cannot drift apart.
…ently

The spoiler button carried aria-label="Reveal spoiler" / "Hide spoiler". An
aria-label replaces the whole accessible name computation, so the wrapped
message text was never announced, revealed or not. A screen reader user could
activate the control, hear the label change, and still never reach the words
every sighted user could read.

The prompt is now part of the button's own content instead, so revealing it adds
the text to the accessible name. The body keeps aria-hidden while concealed, so
the spoiler still conceals.

Its test asserted the aria-label existed, which is exactly what caused the bug:
it proved the attribute was present, never that the content could be read. It
now asserts the thing that matters, that the text reaches the accessible name on
reveal and is hidden before it.
…nothing

Both states resolved to one declaration block:

  [data-transparency='readable'] .mesh-overlay-surface:not(.overflow-auto),
  [data-transparency='opaque'] .mesh-overlay-surface {
    background-color: var(--surface-overlay);
    backdrop-filter: none;
  }

so picking "Subtle" changed nothing, in any theme, on any surface, while being
saved and restored across launches like a working preference.

Opaque overlays are also the legible default this product wants, so the honest
fix is to delete the control rather than invent a translucency effect nobody
asked for. The remaining rule is unconditional and the setting, its type, its
default, its validation, its action and its root data attribute are gone. No
Rust DTO or IPC type carried it, so this is renderer local.
"Make administrator" and "Make member" were visible, described their effect with
a full permission preview, and then failed. matrix_update_member_role returns
Unsupported unconditionally, by design: owner decision D5 keeps Matrix
administrator-role changes failing closed until provider-backed reauthentication
is available, and D17 records that the existing fail-closed behaviour must
remain.

The control could only ever appear in the build that refuses it. It is gated on
a permission projection, and getCommunityPermissionProjection throws unless the
backend is Matrix, so the menu item was reachable in exactly one configuration
and guaranteed to fail there. The legacy transport does implement the command,
but cannot load the projection, so it never showed the item at all.

The command and its backend path are untouched. Only the offer is withdrawn, so
the app no longer advertises an action it is governed not to perform. The clause
is one line and carries the decision IDs, so it comes back out when D5 is met.

The role tests moved with it rather than being deleted: the preview-then-apply
test now asserts the governed behaviour, and the authority evaluator keeps its
eight dedicated tests in community-permissions.test.ts plus three in
RolePermissionPreview.test.tsx, so no security coverage was dropped.
Creating an account on a service that uses a captcha, an email confirmation, or
a terms step ended the flow with a statement of what Mesh cannot do and no next
action at all. That is the one path the product promises is zero config, install
to sending a message, so a hard stop there is the worst place to have one.

Both messages now name what the service is asking for and point at the two
routes that already exist and work: create the account with that service and
sign in to Mesh with it, or go back and choose a different account service.

Titles are unchanged because accountCreation.test.ts asserts that typed
registration guidance survives the Rust boundary, and that intent still holds.
…arning

Three destinations were fully implemented and impossible to reach.

Join requests. 'discovery-access' was a valid CommunityAdminSection in the
navigation contract and in the route validator, but it was missing from
COMMUNITY_ADMIN_SECTIONS, and the surface resolves with find(...) ?? [0]. So a
valid route silently fell back to General, and the approval screen in
CommunitySettings, its fetch effect and its permission fallback were all dead.
Meanwhile the requester side is live: applying to a knock-restricted community
tells the person an administrator will approve them. Nobody could. The entry is
titled Join requests, which is what the destination's own heading says.

Custom emoji management was gated on sectionVisible('emoji'), and 'emoji' is not
a member of CommunityAdminSection, so the condition was never true and its Add
and Remove controls could not be reached. It now rides in the General section.

Moderation history. record_moderation_audit and moderation_audit both fail
closed by design, because a room message can be forged, replayed, copied or
redacted, so it is not accepted as audit evidence. That is deliberate and guard
tested, and it is untouched here. What was wrong is that the renderer still
shipped a Recent confirmed outcomes panel that could only ever render an error,
and summarizeModerationResult required auditRecorded before calling anything a
success. Since auditRecorded can never be true, a ban that applied in every room
reported an amber warning telling the admin to review each room in the panel
that always errored. A closed loop with no exit. The panel is gone and a clean
success is now reported as a success. Partial failures still name the rooms.

Also: the three routed section bodies were <main> elements nested inside the
shell's own <main>, so a screen reader saw two nested main landmarks and
landmark navigation went to the wrong container, while the role="tab" buttons
above them named no panel at all. They are tab panels now, with ids, tabIndex
and aria-controls both ways, which closes the widget.

The two e2e specs that asserted on the old shape moved with the behaviour: the
administration nav is 7 tabs, and it now also asserts positively that the
join-request destination is present.
… build

Every voice surface in the app is gated on shouldExposeVoiceRoutes, which is
false in the shipping Matrix build because the voice frontend is only defined
for the matrix-voice and test modes and the backend reports voice: false. Five
places missed that gate and advertised calling anyway.

The mute and deafen buttons in the sidebar footer were the worst of them: greyed
out, on every screen, in the persistent account bar, with no explanation and no
path to ever becoming enabled, because the store field they depend on starts
null and has no mutator reachable in this build. They are hidden now, and moved
into their own component so voice state is not even subscribed to where calling
does not exist. The toolbar renames itself when it collapses.

Connection check showed a full Private calling panel in a build with no calling,
and blamed the account service for it. It also reported a transient MatrixRTC
discovery failure, which is what an offline user gets, as calling being missing
from their version of Mesh, so a recoverable network state read as a permanent
product gap and the user stopped retrying.

The Beta page listed calling under Known issues, which made an absent feature
permanently visible as a warning.

The room empty state promised that "Voice rooms appear when calling is
available", which is a future promise in a build that cannot create one.

The screenshot capture script recorded the Audio and video surface
unconditionally; it now records a blocker when that section is absent rather
than failing.
dhawal-ss and others added 28 commits August 13, 2026 13:04
…le e2e specs

The end-to-end suite had never been run against the 2026-08-13 pass. Running it
found 7 failures, all of which I confirmed were already present at that pass's
own commit, so none of them were regressions from this session's work.

Six were the message action bar keyboard specs. Their root cause was a stale
locator: the pass moved the labelled container up to the timeline row, which now
carries role="article" and takes its name from the author, so the old
role="group" named "Message from Bob, ..." matched nothing. Three of those tests
then died on a 30 second timeout rather than an assertion, because
tabUntilFocused evaluates against a locator that never resolves. The specs also
still walked the action bar with Tab, which stopped being right when the bar
became a roving-tabindex role="toolbar": one tab stop, arrows within. They now
enter the bar with Tab and move inside it with arrow keys, Home and End, which
is both the real contract and what a keyboard user actually does. No assertion
was weakened; the hover specs gained the pointer-events half of the contract,
since a bar that is invisible but still swallows clicks is the real regression.

The seventh was a genuine critical WCAG violation. The timeline is role="feed",
and a feed may only own article elements, but the empty state, the load error,
the initial load and the room upgrade signpost all rendered inside it. An
EmptyState is a section with an accessible name, which is a region, so axe
flagged aria-required-children. Those four states now render beside the feed
rather than in it, in both the channel and DM timelines. A feed also has to own
at least one article, so the role is claimed only while there are messages: the
element stays mounted either way, which keeps the scroll container and its ref
stable across the empty-to-first-message transition.

Three specs asserted a feed exists in rooms that are empty in their fixtures.
They assert the room-scoped composer instead, which is what actually proves the
route landed where it should.

e2e is now 80 passed, 0 failed, for the first time.
In an announcement-only room, after a mute, a kick or a ban, or once a room has
been replaced by an upgrade, a failed send rendered "Could not send." with a
"Try again" button. Pressing it re-queued and failed again, forever. It looked
exactly like a dropped connection, so the person kept pressing instead of
learning they were not allowed to post, and the app taught them it was broken.

The reason was already available and thrown away: matrix-sdk hands
RoomSendQueueUpdate::SendError both an error and an is_recoverable flag, and
messages.rs destructured the flag while discarding the error with `..`. The flag
alone only decides pending versus failed; it cannot say why.

MatrixQueuedMessageUpdate now carries an optional typed failure, set only when
the queue gave up. Forbidden covers announcement-only power levels, mutes, kicks
and bans, which differ in cause but not in what the sender can do about it, so
they share one outcome. NotFound means the room is gone from under the queue.
Everything else stays Unknown and keeps the retry, because not knowing why is
not the same as knowing it is hopeless.

classify_send_failure takes the error kind rather than the error, so it is a
pure function a test can drive with every variant it claims to handle.

The renderer names the reason and withdraws the retry, keeping Copy text and
Remove so nobody loses what they wrote. The store clears the reason the moment a
send goes back to pending, so a stale explanation cannot outlive its attempt.

Rust 350 passed, unit 1219 passed, e2e 88 passed, all budgets pass.
The offline and reconnecting band is driven by MATRIX_SYNC_STATUS_FRESHNESS, so
at 90 seconds it physically could not appear until a minute and a half into an
outage, which is long after the user has noticed that nothing is arriving.

Lowered to 60 seconds, which is twice MATRIX_SYNC_NORMAL_TIMEOUT. The window has
to clear one long poll or a perfectly healthy idle client reports itself offline
between polls, and two sync periods is the smallest ratio that still absorbs one
slow response plus its retry backoff.

That relationship was previously invisible: nothing tied the two constants
together, which is how the window came to sit at three sync periods. It is a
test now, so lowering it past the long-poll timeout fails the build instead of
shipping a connection indicator that lies while the connection is fine.
Mesh read exactly one image pack, its own `org.mesh.custom_emoji` state key. A
community that set its emoji up in Element or Cinny stores them under that
client's key, so opening the picker after migrating showed nothing at all. The
emoji were still in the room the whole time, which makes an empty picker read as
data loss rather than as a compatibility gap.

Reads now merge every `im.ponies.room_emotes` pack in the room. Writes still
target only Mesh's own key, which is the important half: adopting Mesh must
never rewrite or delete state another client owns. Mesh's own pack wins a
shortcode collision, because that is the one an administrator can edit here.

Bounded like the call roster is, at 16 packs and the existing 100-emoji ceiling,
so a room with many packs cannot turn opening the picker into unbounded work.
Every foreign entry goes through the same projection checks as an upload, so a
pack written elsewhere cannot smuggle in an oversized or non-PNG image, and one
malformed or redacted pack cannot take the room's other emoji down with it.

The guard test asserts both halves, and asserts the two readers actually call
the merged path: a source check that only proved the function existed would pass
just as happily with the readers pointed back at the single-key read. Verified
by reverting one reader and watching the test fail.
…othing

wait_for_room_update returned a bare bool, so every subscriber learned only that
*something* happened in the room. Read receipts and typing notices are by far
the most frequent updates in an active room, and each one paid for a full
timeline refetch: an IPC round trip, a message fetch, a merge and a re-render,
for information the timeline does not contain. The 100ms renderer coalescing in
place today is a mitigation for this, not a fix.

It returns a typed MatrixRoomUpdateKind now: Timeline when timeline events
arrived, State when room state moved without them (membership, name, topic,
power levels, and leaving, being invited or knocked on), Ephemeral for receipts,
typing and tags, and Idle for a timeout, a closed channel or a lagged receiver,
none of which are evidence that anything moved.

Each caller subscribes to what it actually needs. The channel timeline and the
DM history refetch on Timeline only. The community roster follows membership, so
it refetches on State or Timeline: a read receipt cannot change who is in a
community.

This is the trait signature change a previous pass drafted and reverted, because
a half-applied one cannot be compile-checked without cargo. It can be here:
`cargo check --all-targets` type-checks the lib, the bins and the integration
tests without linking, so it works even while a running tauri dev holds mesh.exe
open. It immediately caught the live federation test still applying `!` to the
old bool, which a plain `cargo check` would have missed entirely.

That test now asserts the arrival is reported as Timeline specifically, and a
new ChatView test proves an ephemeral update triggers no refetch while a
timeline change still does, so the saving cannot silently regress into a bug.
…ing real

The ten-thousand-message performance spec never executed. It sat in `testIgnore`
for the chromium project and outside chromium-lan's `testMatch`, so neither
project picked it up. Three separate defects were hiding behind that.

It counted rendered rows with `[role="group"][aria-label^="Message from"]`, a
selector that stopped matching when rows became `role="article"` inside the
feed. Its own `expect(peakRows).toBeGreaterThan(0)` would have caught this
loudly, which is exactly why it is worth having, and exactly why never running
the spec was the real cost.

Its evidence artifact claimed `buildType: vite-optimized-performance-fixture`.
It is not. vite.config.ts aliases the workspace-preview fixture to its
`.disabled` variant in every production build, correctly, so this can only ever
run against the dev server. A CPU profile of the run is dominated by dev-mode
React: jsxDEV, validateProperty, logComponentRender, recordLegacyContextWarning
and StrictMode rendering everything twice. So the absolute 100ms frame ceiling
was asserting a production budget against a measurement that cannot represent
one, and would move with React's dev instrumentation rather than with Mesh. The
label is honest now, and the timing assertion is scale-free instead: the last
quarter of history pages must not cost more than twice the first quarter, which
catches an accidental O(n) or O(n^2) in the merge, sort or normalize path on any
machine in any build. Measured ratio is 0.96 to 0.99, so pagination cost is flat
with depth and there is no such bug today.

The heap bound was deciding by coin flip. It sampled usedJSHeapSize at two
arbitrary moments, so unchanged code reported 32 MB, 58 MB and 78 MB of growth
on three consecutive runs against a 64 MB ceiling. Both samples now force
collection first, which is what the project's --expose-gc flag is for. Retained
growth is 4.1 to 4.4 MB across three runs, and final heap 30.9 to 31.4 MB: the
old numbers were almost entirely uncollected garbage, and the real memory
behaviour is comfortable.

The structural bounds are unchanged and still hard, because they hold in both
builds: 33 rendered rows against a limit of 100, 1200 DOM nodes against 2500,
199 history pages traversed. Those are what prove virtualization holds.

e2e is 89 passed, 0 failed.
…otification

A denied OS notification permission was invisible. Mesh's own "Desktop
notifications" switch records what the user asked for, not what the system will
do, so it kept reading as on while every notification was silently discarded.
The only way to find out was to press "Test notification" and read the failure,
which nobody does when they believe the feature is already working.

A read-only `notification_permission_state` command reports what the OS will
actually do, and the Notifications tab asks on open. When the answer is denied,
the panel says so and names where to allow it, reusing the notice the test
failure already showed.

Deliberately read-only: it never calls request_permission. Opening a system
permission dialog from a settings screen someone is only reading would be a nag,
and Mesh already requests the permission the first time it has a notification
worth showing. "Not requested yet" is therefore not reported at all, which is
the second test: never asked is not denied.

The switch is never flipped on the user's behalf. It keeps reporting their
choice, and the notice explains why that choice is not being honoured, because
silently rewriting a setting to match the OS would lose the intent they
expressed.
store/messages.ts and store/dms.ts model the same thing twice: a bounded,
ordered, normalized window of messages per conversation with an LRU over
conversations. They were written separately and drifted, which is why several
fixes had to be applied twice and why the DM store had quietly lost the
transaction-alias identity and the LRU before now.

The genuinely identical parts move to store/timeline-cache.ts: the order
comparison, scope eviction, both window bounds, the LRU retain, and the
O(n + m) alias-indexed merge. The two LRU functions were forty lines each of
the same `withoutX(patch.field ?? state.field, evicted)` line repeated per
field; they are now a key list and one call.

Deliberately not shared: the merge policy for a single message. Channels
protect a `sent` echo from being regressed by a late `pending` one and DMs have
their own rules, so each store passes its own policy in rather than having one
pretend to serve both. The DM policy also records why its identity is the id
alone: unlike a channel send there is no separate transaction id, because the
optimistic record and the server echo share the id the sender minted.

The shared eviction takes the more careful of the two implementations, the DM
store's hasOwnProperty check rather than the channel store's `in`.

Worth being honest about the size of this: it is about 0.5 KiB of bundle, not
the several KiB I expected. The value is that a timeline fix now lands once.
…derline

CreateCommunityModal was the only file on the eslint allowlist for importing
framer-motion directly, because it used a shared `layoutId` for the tab
indicator. A bare framer import statically bundles the whole DOM feature set,
so the app shipped a 38.9 KiB `gestures` chunk plus layout projection to slide a
two-pixel underline between three tabs. Nothing else in Mesh needs projection.

The indicator is a CSS transform now. It lives in every tab and scales between
0 and 1 on the selected one, so the shape signal a colour-blind user relies on
is unchanged, and both reduced-motion routes already clamp transition-duration,
so it goes quiet when asked without any extra rule. The modal imports from
lib/lazy-motion like everything else, and the allowlist is empty.

All JavaScript: 2188.99 KiB to 2141.72 KiB, so headroom against the ceiling goes
from about 1 KiB to about 48. That is the constraint on read receipts, group DMs
and anything else with a renderer half, and it was one import.

Two things fixed on the way. The tablist was hardcoded to three columns while
the tab list can be two, leaving an empty column in the non-Matrix case; the
column count follows the tabs now. And arrowing across the tablist threw focus
into a text box: each panel autofocuses its first field, which is right when the
modal opens or a tab is chosen outright, but a roving tabindex has to leave
focus on the tab so the person can keep exploring. The panel now only claims
focus when the choice was deliberate.

The test asserting the non-colour signal was checking the indicator merely
existed, which every tab now satisfies; it asserts the selected one is scaled up
and the rest are scaled down.
Read receipts were the rare case where the hard half was already done. Mesh has
sent them with correct per-thread scoping for a long time, there is a privacy
setting governing them, and the backend already applies the reciprocity rule:
it reports someone else's public receipt only while this account shares its own
in the same conversation. Nothing rendered any of it, so the setting asked
people to give up a privacy protection for a reciprocal benefit the app never
delivered, and every DM history fetch in Public mode paid for receipt lookups
whose results were thrown away.

A "Seen" marker now sits under the newest message of yours the other person has
read. Only the newest: in a conversation you both keep up with every message
carries a receipt, a marker on each is noise, and the only question being asked
is whether they have seen what you just said.

Not a live region. DM rows mount and unmount as the timeline virtualizes, so
anything with a live role re-announces every time a row scrolls back into view.

Silence when the backend reports no receipt, never "not seen yet": with
reciprocity in play an absent receipt frequently means Mesh was not told, and
claiming the message is unread would be a statement it cannot support. There is
a test for exactly that, alongside one that pins the newest-only rule.

Channels are deliberately untouched. seenBy is a direct-message field, and a
per-message facepile in a busy room is noise rather than information.

Costs 0.36 KiB, against the 48 KiB the framer-motion fix returned.
Every font-size token in tailwind.config.ts already ships its own leading
(line-height-11..28), several of them density-aware: line-height-14 grows
18 -> 19 -> 21px across the normal, HiDPI and 4K breakpoints. Tailwind's
numeric leading-* utilities are fixed rem, so `text-xs leading-5` pinned
12px body copy to a 20px line box: 1.67 on a normal display and 1.43 on a
4K one, against the contracted 1.33 the scale intends, and unresponsive to
the density overrides by construction.

Strip leading-4/5/6 from every element that also carries a size token
(135 sites across 33 files), restoring the token's own leading. The three
that looked standalone all inherit a size token from an ancestor (the
Tooltip root, a text-xs <details>, an ErrorState clsx arm), so they were
the same bug one level up. Semantic ratio leadings (prose on message body,
none on emoji buttons, tight on truncated labels) are unitless, scale with
the size, and are deliberate, so they stay.

Add a check-design-tokens rule that fails on a fixed-rem leading-3..10
paired with a size token, with self-tests and a mutation-verified catch,
so the drift cannot come back silently.
A message search that ran past its 10s native deadline was wrapped in a
single `tokio::time::timeout`, so the elapsed timer cancelled the whole
scan future and dropped every match already collected, returning
`Cancelled` to the renderer. The user typed a query, the search did real
work finding hits, and then showed a failure with nothing in it.

Move the deadline inside the scan: bound room resolution and each per-room
fetch with `timeout_at(deadline, ...)`, and on elapse return the matches
found so far (`Ok`) rather than discarding them. Setup that outruns the
deadline yields an empty result, not an error. Cancellation stays distinct
and still returns `Cancelled`, because a superseded search's partial
results are for a query the user has already moved past. The renderer
already resolves `Ok(Vec)` as success and its fail-safe timer sits 2s
beyond the native deadline, so the partial set now lands first.

Extract the deadline/cancellation/budget/accumulation loop into
`run_bounded_message_scan`, taking the per-room fetch as a closure so it is
testable without a live homeserver. Three tokio-virtual-clock tests cover
the three outcomes: a slow room returns the fast room's match, a superseded
search reports cancellation, and an unhurried scan returns every match.
Replace the singular peer_public_key / peer_display_name / peer_avatar_color
scalars on DmConversationDto with peers: Vec<DmPeerDto>, so a one-to-one DM
becomes the peers.len() == 1 case of one shape rather than a type that cannot
name a second participant. This is the load-bearing contract change for group
DMs; behavior is unchanged in this commit (the len != 1 guards stay, so every
conversation still has exactly one peer).

DmPeerDto mirrors ReadReceiptDto, which already carries per-member identity, so
the message and receipt contracts needed no change. The SQLite layer keeps its
existing peer_public_key column and schema untouched and simply projects a
one-element peers list from those columns, so there is no data migration. Both
Matrix builders, the conversation sort key, the legacy topic-subscription path,
and the IPC exporter are updated together, and the generated contract is
regenerated so the Rust and TS shapes stay in lockstep.

On the renderer, dmPrimaryPeer/dmPrimaryPeerName centralize the "primary
counterparty" read (sidebar and header name and avatar, block, report, the 1:1
trust summary, dedup), giving one place to revisit when the group UI lands; the
suppress-peer lookup already matches any peer. The federation live test finds a
DM by scanning peers rather than the removed scalar.

Verified: cargo test --lib (matrix) 355, cargo check --all-targets (matrix) and
--features legacy-p2p both clean, check:ipc-types current, tsc, npm test 1224,
lint.
Ships the first nine items of the audit roadmap, against the beta tip.

Certain-failure defects:

- Account services no longer self-disable on one date. reviewAfter now gates
  only the recommendation and the account-creation offer; a new hardExpiryAfter
  gates the entry itself. Signing in to an account you already have survives a
  lapsed review, and the release gate keys off hard expiry, so a stale catalog
  no longer blocks the release that would refresh it. Catalog dates move to
  2027-02-28 and 2027-08-31.

- "Approval required" can now succeed. update_community_access took one boolean
  that conflated join rule with directory visibility, which made knock without
  a directory listing inexpressible; the renderer asked for it anyway and the
  rejection was reported to the owner as "some starter rooms still need setup".
  Join rule is now its own parameter, the access failure has its own state and
  copy, and CommunitySettings grows a "Who can join" control. That control is
  the first caller matrix_community_access_settings has ever had.

Silent failures and dead ends:

- A failed saved-account sign-in is no longer silent. The error and notice
  regions were rendered only inside the sign-in form, so every failure that
  keeps the screen in select or public-services mode produced nothing at all.
  Both regions are now rendered by all four modes.

- The composer recovers. The room protection probe ran once per room and mapped
  any rejection to unavailable forever, which disabled the composer under copy
  claiming a check was in progress. It now re-runs on device-trust changes, on
  the link reaching online, and on a "Check again" button.

- A failed send has a persistent surface: a danger strip above the composer and
  a marker in the room list, so a send that failed while you were reading
  another room is discoverable. The marker survives muting, because muting asks
  Mesh to stop announcing other people's messages, not to hide that yours never
  left.

Content integrity and accessibility:

- Intraword underscores no longer italicise and delete themselves.
  MAX_DRAFT_BYTES rendered as MAXDRAFTBYTES; snake_case, dunder names and
  file_name_utils.ts all corrupted.

- Two WCAG AA failures: inline links carry a persistent underline rather than
  colour alone at 2.00:1 dark and 2.80:1 light, and a Text size control now
  scales the type ramp, with zoom hotkeys enabled in the window config.

Release gates:

- The IPC contract checker could not see a command behind a multi-line generic
  clause or a forwarding helper, which is why the approval-required defect
  survived. Widened, given a declared-forwarder collector, and given the reverse
  registered-but-never-invoked check it lacked. Matrix commands seen: 124 to 132.

- The large-timeline heap gate ran without --expose-gc, so it measured
  uncollected garbage. Restored, and the spec now fails loudly rather than
  silently skipping collection.

- Bundle ceilings record their provenance. Four cited an owner decision absent
  from the register; two of those raises were unnecessary and are reverted. The
  remaining two are reported with exact numbers on every build.

147 test files, 1246 tests green.
Completes the P0 set from the 2026-08-14 audit.

- Rooms can be renamed and removed. matrix_update_channel and
  matrix_remove_channel did not exist, so a typo in a room name was permanent
  while community settings told administrators to manage each room from a menu
  that carried neither action. Both gate on the room actually being a child of
  the community, so passing an unrelated room id cannot rename it. Removal is
  confirmed, and the copy says what Matrix does: the room is detached and this
  account leaves, and people already in it keep their copy. It does not promise
  a deletion Mesh cannot perform.

- Profile pictures have a writer. update_profile_display_name was the only
  profile write in the codebase, so the avatar field could only be non-null if
  the person had set one from a different Matrix client, while the create flow
  claimed "a custom image replaces it when you add one". New avatar.rs reuses
  the server-emoji pipeline: bounded decode, re-encode to PNG, upload, and
  fetch confined to mxc addresses. An avatar is fetched by everyone who can see
  you, which makes an unsanitised one a broadcast primitive. The false copy is
  corrected.

- Known issues are honest. The list carried three entries, all about packaging,
  next to a Send feedback button, which reads as "this is everything Mesh knows
  about". It now names the limits a beta user meets in the first session,
  including that Mesh blocks screen capture and appears blank in recording and
  screen-sharing tools, which was release-gated and disclosed nowhere.

- A gate now crosses the renderer-to-native seam. check-ipc-arguments.mjs
  verifies that the payload keys the renderer sends are exactly the parameters
  each Rust command declares, across all 155 object-payload invocations. Tauri
  deserializes by name, so a renamed key survives the build, the type-checker
  and every mocked test, then fails on a user's machine. Managed state is
  filtered by type rather than name, and the scanner gives Rust its own quote
  set because a lifetime such as State<'_, AppState> is not a string.

  This is the mechanical half of the seam, not the whole of it. It does not
  check types, values or semantics: the approval-required defect sent a
  correctly-named argument carrying a value the backend rejected and would
  still pass. A tauri-driver run against infra/matrix-spike remains the only
  thing that proves that half.

147 test files, 1253 tests green. All release gates pass.

Note: eager JavaScript is now 544.13 of 545.00 KiB.
…ard contract

First batch of P1 from the 2026-08-14 audit. Every change lands in a lazily
loaded chunk, so eager JavaScript is unchanged at 544.13 KiB.

Content integrity:

- Search stops silently answering the wrong question. The tokenizer split on
  whitespace, so the quote characters stayed inside the search text and the
  native engine, which substring-matches, looked for a body literally
  containing a double quote. "release notes" therefore matched nothing, and
  matched nothing quietly: the query looked handled and the footer honestly
  reported what was searched. Quoting now also protects text that would
  otherwise parse as an operator, and carries operator values containing
  spaces. A leading hyphen excludes a term, applied to results rather than sent
  on the wire for the same reason in: already is, with the fetch widened so
  narrowing a full page does not return a short one.

- A reply to anything outside the render window is a reply again. The preview
  was resolved from the 50-row window rather than the loaded room, so a reply
  further back rendered nothing at all even when the message it answered was
  in the store, and the relationship was silently lost. Resolution now uses the
  whole room, clicking a target outside the window falls back to the navigation
  path instead of doing nothing, and a genuinely unloaded target renders a stub
  rather than passing as an ordinary message.

- Fenced code is readable and copyable. It rendered in a muted foreground, for
  the content a reader most needs to read precisely, and no clipboard path
  existed for code anywhere in the chat surface while manual selection is
  fragile inside the virtualizer. The fence language is now shown instead of
  written to an attribute nothing consumes.

Moderation:

- Moderation is no longer offered against someone this account cannot moderate.
  Two administrators sit at the same power level and Matrix requires a strictly
  greater one, so the action fanned out across the community, failed in every
  room, and reported "Applied in 0 of N places. Try the failed rooms again." A
  change that applied nowhere now says the account service refused it rather
  than advising a retry that cannot succeed.

Accessibility:

- The feed implements the keyboard contract it advertises. role="feed" was
  claimed with a comment saying it gives Page Up and Page Down article
  navigation; the role carries no behaviour, rows were never focusable, and a
  keyboard user in a 200-message room had to Tab through one toolbar stop per
  message to leave the timeline. Arrow, Page, and Ctrl+Home/End now move a
  roving tab stop, the timeline costs one Tab stop in total, keystrokes inside
  a row's own control are left alone, and the focused row stays mounted when
  scrolling carries it out of the window rather than dropping focus to body.

- aria-setsize reports -1 while more history exists. Reporting the loaded count
  renumbered every message a screen reader had already been told about as soon
  as another page arrived.

- The timeline reserves scroll padding for its two sticky bands, either of
  which could completely cover a focused message: WCAG 2.2 technique F110, and
  it would have defeated the navigation above.

147 test files, 1271 tests green. All release gates pass.
…n level

Second batch of P1 from the 2026-08-14 audit.

- Approving a join request is confirmed, and shows who is being approved. The
  card carried only a display name the requester chose, which is how an
  administrator lets in someone impersonating a member, and Approve was a
  single unconfirmed click while the reversible act of removing a member was
  already behind a confirmation. The account address and the request time now
  appear on the card, and approval states what it grants.

- The queue revalidates. It was fetched once on mount with no refetch, poll, or
  focus revalidation, so a knock arriving while the panel sat open was never
  seen. A failed background refresh leaves the list as it was rather than
  replacing it with an error.

- Rooms inherit a community notification level. Community notification was a
  binary mute while every room carried three levels, so mentions-only across a
  thirty-room community meant the same decision thirty times, and again for
  every room added later. A room's own level still wins; 'all' is stored as the
  absence of a preference so a community keeps following the default rather
  than being frozen at today's value.

- The community menu says "Nothing, including mentions" in full. That is the
  thing Mesh does which Discord refuses to, since a muted channel there still
  badges for @everyone and @here with no opt-out, and saying so is the only way
  anyone finds out.

147 test files, 1272 tests green. All release gates pass.

Eager JavaScript is now 544.48 of 545.00 KiB.
…ce the design contract

Third batch of P1 from the 2026-08-14 audit: 1.13 and 1.14. Both were picked
because they need no eager JavaScript, which is at 544.93 of 545.00 KiB.

P1.13 — three OS integrations shipped as decoration.

- The tray had an icon and a tooltip and no menu and no click handler. It now
  has both: left click restores the window, right click opens Open Mesh and
  Quit Mesh. Closing the window still exits; close-to-tray is a different
  feature with its own lifecycle to test and this is not it.

- A notification could not be clicked. For a background chat app the toast is
  the way back in, and Mesh's was a dead end. It could not be otherwise through
  the plugin: tauri_plugin_notification's desktop show() builds the toast,
  calls show() and drops the handle, and its activation callbacks are
  Linux-only. On Windows the toast is now built on tauri-winrt-notification,
  already in the tree at this version through notify-rust, and a click raises
  the window and opens the room the notification named. Every other target
  keeps the plugin path, as does Windows when the WinRT toast cannot be shown.

- The window title never reached the operating system. The renderer computes a
  precise one for document.title, but that addresses a browser tab which does
  not exist in a Tauri window, so Alt-Tab, the taskbar and the window list read
  "Mesh" for every room. The same string is now set natively, bounded and
  stripped of control characters because room names come from other people.

- Window size and position are remembered. The stored rectangle is tested
  against the displays that exist before the window is moved, so a window last
  seen on a monitor that has since been unplugged is centered rather than
  restored somewhere unreachable. current_monitor cannot be that test: Windows
  resolves a window to its nearest monitor and names one for a window that is
  nowhere near it.

P1.14 — governance the team had already written, turned into enforcement.

- A contrast gate that computes rather than asserts. The old check compared
  sixteen hex pairs typed into the script, three of which (#1f6f43, #855b08,
  #a3313a) are not in the stylesheet at all; the shipped tokens are #237548,
  #815900 and #b4232a. Those three assertions had been passing against colours
  Mesh does not ship. check-container-contrast.mjs resolves every pair out of
  globals.css instead, following var() chains through the theme blocks, and
  covers --content-link, whose absence from the old list is why a 2.00:1 link
  colour shipped.

- With the gate built, the container lines were measured and then fixed, in
  that order. All five failed WCAG 1.4.11 on the light canvas at 2.28:1 to
  2.67:1, and danger also failed on the dark canvas at 2.68:1, which the audit
  had not found. 0.78 is the lowest alpha that clears 3:1 for every tone on
  every surface a notice can sit on. One shortfall is recorded rather than
  fixed: the neutral rule reaches 2.86:1 on the light rail, and raising it
  means re-authoring the app's universal border colour, which is a design
  decision with an owner.

- All 210 opacity modifiers on status and accent utilities are converted to
  container tokens and ENFORCE_NOTICE_INTENSITY is on. Eleven alpha steps for
  one idea is not a palette. Two of them were not notices at all but the darker
  face of a solid button and took the status-info-hover token that already
  existed; one kept a distinction the mapping would have flattened, because a
  mention of you is deliberately stronger than a mention of somebody else.

- Reduced motion has one mechanism instead of two that disagreed. The in-app
  toggle wrote data-reduce-motion while the OS preference was honoured by media
  queries; the media path killed transitions outright, the attribute path only
  shortened them, and the animation contract had to be written twice.
  applyAppearancePreferences now ORs both into the attribute before the first
  render, and the only media query left is a pre-hydration net scoped so it
  cannot contradict the resolved answer.

- The blanket rule names the properties that may still change rather than
  deleting every transition. `transition: none` went too far: a cross-fade is
  not movement, and the message timestamp asks for exactly that with
  motion-reduce:transition-opacity, which the blanket rule discarded so the
  timestamp snapped into place. Four rules now protect this, including that the
  bounded-progress indicators keep their !important re-entry: a process
  indicator may not disappear merely because duration is reduced.

Two defects from my own earlier P0 commits, found by running the Rust suite
that those commits' verification had not included:

- remove_channel called client.get_room directly, which also hands back a room
  this account has been banned from or already left, and then calls leave() on
  it. It goes through protected_joined_room_if_available now.

- matrix_load_profile_avatar never entered the native request registry, so an
  account switch could not cancel it and the bytes it returned were a picture
  belonging to the account that had just been signed out.

Also removes src/store/settings.notifications.test.ts, a zero-byte file I
committed in abdd1b4. Its coverage lives in settings.test.ts; the empty file
made vitest fail to collect a suite.

Verified: 147 vitest files / 1277 tests; Rust 363 matrix and 278 legacy; lint,
copy-style, design-tokens, icons, ipc-contract, ipc-arguments, ipc-types and
bundle-size all pass. Eager JavaScript 544.93 / 545.00 KiB, CSS 111.79 / 115.00
KiB, down from 113.09.
"Why can't this person post here?" is the question Mesh already had the
data to answer and never did. The per-room permission projection was
computed, tested, and shown in exactly one place: the confirmation dialog
for a role change that the Matrix build refuses to offer.

Two read-only consumers now use it. A member panel answers the question
for someone else, and a room view answers it for yourself.

The aggregate could only report how many rooms granted a capability, which
cannot answer which ones. explainCommunityPermissionProjection returns the
rooms themselves, and both it and the aggregate now derive their status
through one shared helper so a count and a room list cannot disagree.

Two distinctions the projection already made but nothing surfaced:

  - A room Mesh could not read is not a refusal. Unreadable rooms are
    reported as unread, never merged into the denied set.
  - Scoping to one room re-derives discovery rather than inheriting it.
    Incomplete community discovery says nothing about a room already in
    hand, so a readable room stays answerable when a sibling fails.

The member entry is gated on the projection being available, deliberately
not on canManageRoles. D5 keeps Matrix role changes failing closed and D17
requires that to remain, so gating on it would hide the explanation in the
only build that can produce one. Reading a permission is not changing one.

No new bridge function and no new Rust command. Eager JavaScript is
unchanged at 544.93 KiB; the 5.74 KiB this adds lands in lazy chunks.
The narrow-shell axe scan was red before this branch started. High
contrast turns every container token into a solid fill, and globals.css
already said so and already paired each fill with an on-container ink.
The flip was welded to .mesh-notice-band, so a hand-rolled banner carrying
a container fill kept its tinted-theme text and became unreadable:

  warning body   #ffffff on #f0b232   1.88:1
  retry action   #6fafff on #f0b232   1.20:1

Both needed 4.5:1. A trust banner using text-status-warning on the same
fill was worse still, amber on amber.

Two changes, neither of which alters a tinted theme:

  - The tone rules now key off data-notice-tone alone rather than off the
    band class, so an element can take the ink without also taking the
    band's padding and rules. .mesh-notice-band still styles the box.
  - The high-contrast rule now also reaches interactive descendants. A
    parent's color never overrides a child's own declaration however
    important it is, so the retry button stayed link-blue while the text
    around it flipped. A descendant carrying its own tone is left alone.

The conversation banners then declare their tone instead of hand-rolling
the pairing. The design comment already called this out: a recoverable
failure with a retry is never hand-rolled.

Verified: the narrow-shell axe and zoom spec passes. Ten DM end-to-end
tests still fail, unchanged from 57826fd and unrelated to this work.
Bundle unchanged; the CSS is slightly smaller for losing the duplicate
selectors.
Ten end-to-end tests were red on the beta tip. aa9979e modelled a direct
conversation as a list of peers and updated the unit fixtures, but the two
end-to-end specs kept building a conversation the old way:

  peerPublicKey / peerDisplayName / peerAvatarColor

DmConversationDto now carries `peers`, documented as never empty. With the
field absent, dmPrimaryPeer evaluated `conversation.peers[0]`, which throws
on undefined before its `?? fallback` can apply. That crashed the shell into
an ErrorBoundary, the DM sidebar never rendered, and all ten tests failed
looking for a conversation list that was never going to appear.

The fixtures now build the shape the contract describes. dmPrimaryPeer is
deliberately left alone: `peers` is specified as always present and never
empty, so making the reader tolerate its absence would hide a real contract
violation behind a blank display name rather than surface it.

The full Chromium end-to-end suite now passes 80/80.
Mesh implements an unusually strict telemetry posture and said so nowhere
a user would look. This adds a panel to the privacy tab, lazily loaded with
the rest of settings.

Three claims in the source specification did not survive checking, so they
are not stated as written:

  - It asked for "twenty allow-listed error kind names". The allow-list
    holds eighteen. Twenty is MAX_RUNTIME_ERROR_RECORDS, the record cap,
    which is a different number about a different thing. The panel now
    reads both bounds from the module instead of restating them, so the
    figure shown is wrong only if the behaviour changes, and a test pins
    that derivation.

  - It asked to claim error reporting is opt in. The frontend recorder is,
    but crash_report::install runs unconditionally at startup and its panic
    hook writes a crash marker whether or not reporting is enabled. A flat
    claim would have been false, so the marker is disclosed instead: what
    it holds, that it is a local file, and that it is written anyway. It
    keeps only a source basename, which its own test pins.

  - It asked for "no advertising" and "no conversation mining". Neither has
    an owner decision or an implementation to point at, and the same
    specification says a claim with neither does not ship. Both are
    restated as checkable facts about the built artifact rather than as
    promises about intent.

Content protection is disclosed plainly because it surprises people: Mesh
excludes its own window from capture, so an audience that streams or records
sees a blank area, and nobody can screenshot Mesh to file a bug.

Eager JavaScript moves 544.93 to 544.95 KiB, still inside the enforced
ceiling. The cost is the export bindings for the two bounds now that a lazy
chunk reads them, which is the price of the numbers not being able to drift.
Integrates the current-direction work (design refresh, MatrixRTC/LiveKit
voice, custom/community emoji, navigation and settings rework, DM peers
model, and a long tail of correctness/a11y/perf fixes) on top of the
just-merged beta-hardening stack.

Resolved 26 content conflicts and 6 modify/delete conflicts across 19
files where both lines touched the same code. Source under mesh/src,
mesh/e2e, and mesh/src-tauri favored quizzical-maxwell as the newer,
authoritative implementation; deleted audit/report/design artifacts
from the prior artifact-cleanup stayed deleted. Restored custom-emoji.ts
and the custom-emoji administration surface, which track-b-removals had
deferred and quizzical-maxwell built out for real.

Stops short of the branch tip (ccc5d51, "WIP: resource honesty...") which
is explicitly self-labeled unverified/uncompiled by its author; that
commit is left for a follow-up once it's actually built and tested.

Verified after resolution: tsc --noEmit clean, 1292/1292 vitest tests
pass across 148 files, eslint 0 errors, cargo check clean (matrix-backend).

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
…nts and a stale audio asset

This branch never went through CI (no PR was ever opened for it), so these
were never caught before merging:

- Regenerated the 8 interface-sound .wav assets with the existing generator;
  the committed connection-recovered.wav (and the rest) had drifted to 24kHz
  against the checked-in 48kHz PCM contract.
- notifications.rs: activate_notification and its two imports are only
  reachable from the Windows-only show_activatable_toast, so they need
  #[cfg(windows)] too rather than compiling (and going unused) everywhere.
- notifications.rs: removed a genuinely unreachable match arm now that
  PermissionState's variants are already exhaustively covered.
- matrix.rs: replaced a contains_key+insert presence-cache update with the
  Entry API (clippy::map_entry), preserving the existing "skip the bound
  check on refresh, prune only before a new insert" behavior.
- avatar.rs: MxcUri parsing here is infallible in this ruma version; updated
  the code and its security comment to point at the real validation, which
  happens one layer down in media_download_endpoint's Request::from_uri.
- matrix.rs: allow too_many_arguments on run_bounded_message_scan, a
  single-caller private function whose 8 parameters are each independently
  load-bearing (search scope, filters, budget, cancellation).

Verified: cargo clippy -D warnings clean for matrix-backend and legacy-p2p;
cargo test clean for both feature sets; npm run check:interface-sounds passes.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
…nly path)

My first pass only ran plain clippy, missing lints scoped to test targets and
to #[cfg(windows)]-only code CI can reach that a macOS dev machine can't:

- matrix/tests/mod.rs: wrap a compile-time-constant assert in `const { }`
  (clippy::assertions_on_constants).
- state/native_requests.rs: a test discarded a Result whose value happened to
  be a second, inner Result after the outer admission Result was already
  handled via .expect() (clippy::unused_must_use, or rather unused_must_use
  proper — the inner value, not the outer one).
- crypto/keychain.rs: unnecessary_lazy_evaluations on the Windows-only
  credential-blob-size guard (ok_or_else -> ok_or), matching clippy's own
  suggested diff exactly since I can't compile-check Windows-only code from
  here to verify independently.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
…owner decision)

eagerJavaScriptBytes 545->548 KiB, totalAssetBytes 2500->2550 KiB. Verified
against the exact CI build (build:matrix-voice, matching MESH_MATRIX_VOICE_FRONTEND):
measured 545.85 KiB eager / 2543.27 KiB total, both now within budget with a
few KiB of headroom.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
The two branches' auto-merged (non-conflicting) hunks landed with slightly
different line-wrapping in several files. cargo fmt --check is a required CI
gate; ran cargo fmt to reconcile it. No logic changes.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
…t blocking (repo owner decision)

Confirmed upstream tool bug, not a code defect: cargo-geiger 0.13.0 (latest
release; 0.12.0 reproduces identically) bundles cargo 0.86.0 as a library,
whose internal clean/download-batching panics on this project's matrix-voice
dependency graph with "assertion failed: self.pending_ids.insert(id)" in
cargo::core::package::Downloads::start. Reproduced locally, independent of
CI, on both available cargo-geiger releases. No newer release exists.
cargo check/build/test for matrix-voice all pass clean.

Matches the existing continue-on-error precedent already on the cargo audit
step in this same job. Every other step in dependency-and-secret-audit
(secret scanning, SBOM, license/source policy, other feature scans) still
blocks normally.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
@dhawal-ss
dhawal-ss merged commit 216adfb into main Aug 16, 2026
15 of 19 checks passed
@dhawal-ss
dhawal-ss deleted the integrate/quizzical-maxwell branch August 16, 2026 12:14
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant