<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom">
  <channel>
    <title>Yamete: changelog</title>
    <link>https://studnicky.github.io/yamete/</link>
    <description>A macOS menu bar app that reacts when you smack your laptop. Three sensors. Eleven event triggers. Forty languages. Zero shame.</description>
    <language>en-US</language>
    <atom:link href="https://studnicky.github.io/yamete/feed.xml" rel="self" type="application/rss+xml" />
    <item>
      <title>Yamete 2.5.1</title>
      <link>https://studnicky.github.io/yamete/CHANGELOG#251</link>
      <guid isPermaLink="false">https://studnicky.github.io/yamete/changelog/2.5.1</guid>
      <pubDate>Mon, 18 May 2026 12:00:00 GMT</pubDate>
      <description><![CDATA[### Added
- **Docs site SEO infrastructure.** The VitePress site at
  https://studnicky.github.io/yamete now ships a full SEO surface area:
  per-page canonical / `og:` / `twitter:` meta via `transformPageData`,
  JSON-LD blocks for `SoftwareApplication` + `WebSite` + `Organization`
  + per-page `BreadcrumbList`, search-console verification slots
  (suppressed at build time until populated in `package.json` →
  `yamete.seo`), `hreflang` declarations, modern `robots` hints
  (`max-snippet:-1`, `max-image-preview:large`), and a complete favicon
  stack with `manifest.webmanifest` for PWA-style install.
- **Sitemap + RSS feed.** `sitemap.xml` is generated from every rendered
  page; `feed.xml` is composed from `docs/CHANGELOG.md` at build end
  and linked from the head via `<link rel="alternate"
  type="application/rss+xml">` so release watchers, Sparkle-style
  updaters, and GitHub-action notifiers can subscribe to version drops
  without polling.
- **`llms.txt` index.** A semantic table of contents at `/llms.txt`
  surfaces the docs structure to retrieval-augmented language models in
  the format proposed at <https://llmstxt.org>.
- **Release banner image.** New `docs/public/og-image.{svg,png}`
  (1200×630 social card, pink theme, wincing face with impact rays)
  drives the unfurl preview on Discord, Slack, iMessage, Twitter/X, and
  LinkedIn. New `docs/public/yamete-banner.svg.template` is stamped per
  release by `scripts/stamp-version.mjs` (reads `MARKETING_VERSION` from
  `project.yml`) into `yamete-banner.svg` with the version pill visible;
  the release workflow now prepends this banner at the top of every
  GitHub release body via a `raw.githubusercontent.com` URL.
- **GitHub repository social preview.** New
  `docs/public/github-social.{svg,png}` (1280×640, GitHub's recommended
  Repository → Settings → General → Social preview dimensions). Upload
  the PNG via the repo settings UI to control the unfurl card that
  shows when `github.com/Studnicky/yamete` itself is shared on Discord
  / Twitter / Slack / iMessage. GitHub provides no API for this
  surface; the asset lives in `docs/public/` for convenient access.
- **Docs-build CI gate.** `ci.yml` gains a `docs-build` job that runs
  `npm ci`, `stamp-version:check`, `docs:build`, and asserts the SEO
  outputs (sitemap, feed, robots, manifest, OG image, stamped banner)
  exist in `docs/.vitepress/dist`. Docs regressions are now caught at
  PR time instead of after merge to master.

### Fixed
- **GitHub Pages deploy was failing since release 2.3.0** due to bare
  `*<display>*` placeholders in `docs/using.md` and `docs/CHANGELOG.md`
  which Vue's SFC parser read as unclosed HTML start tags during
  VitePress's `vite-plugin-vue` pass. Replaced with `&lt;display&gt;`
  entity escapes so the markdown italicises the placeholder without
  triggering the HTML tokeniser. The new `docs-build` CI gate (above)
  prevents this class of regression from reaching master again.
- **VitePress build no longer chokes on local-only design notes.** The
  gitignored `docs/art/` design-exploration markdown is now declared in
  `srcExclude` so VitePress's srcDir glob skips it. Previously the
  files were silently picked up by local builds and could throw
  interpolation parser errors on `{{ }}` brace pairs unrelated to the
  shipped site.

### Build & release pipeline
- **`stamp-version.mjs` script** reads `MARKETING_VERSION` from
  `project.yml` (the canonical macOS bundle version source of truth)
  and stamps every `docs/public/*.svg.template` into a sibling `.svg`
  with `__VERSION__` replaced by `v<version>`. Hooked into
  `predocs:build`. `--check` mode exits non-zero if any output is
  stale so CI flags pre-tag drift.
- **`render-og.mjs` script** rasterises `og-image.svg` and
  `yamete-banner.svg` to PNG via `sharp` (devDep). Hooked into
  `predocs:build` after stamp.
- **`release.yml` banner verification step** greps the stamped
  `yamete-banner.svg` for the version string and fails the workflow
  if it doesn't match the tag — surfaces missed `npm run stamp-version`
  invocations before the release notes go out.]]></description>
    </item>
    <item>
      <title>Yamete 2.5.0</title>
      <link>https://studnicky.github.io/yamete/CHANGELOG#250</link>
      <guid isPermaLink="false">https://studnicky.github.io/yamete/changelog/2.5.0</guid>
      <pubDate>Thu, 07 May 2026 12:00:00 GMT</pubDate>
      <description><![CDATA[### Added
- **Independent Caps Lock LED and Keyboard Brightness toggles.** The
  Reactions group renders two separate accordion cards instead of one
  bundled "Keyboard" card, and the per-event matrix in Stimuli has a
  dedicated column per output (KbBright + Caps). Each output is gated
  by its own `LEDOutputConfig` flag inside `LEDFlash.action` so a user
  can enable the keyboard backlight without firing the Caps Lock LED,
  or vice versa. The Keyboard Brightness card hides on hosts where the
  backlight isn't available (Mac mini, Mac Pro, etc.).
- **Per-model coverage tables in every sensor source header.**
  `LidAngleSource.swift`, `GyroscopeSource.swift`,
  `AmbientLightSource.swift`, and `HeadphoneMotionAdapter.swift` now
  document expected reach across M1 → M4 silicon and headphone models.
  The runtime probe is the source of truth; the tables are the
  documentation.

### Changed
- **Lid angle now reads the dedicated Apple lid HID device, not the
  SPU IMU stream.** Vendor `0x05AC`, Product `0x8104`, UsagePage
  `0x0020`, Usage `0x008A`, Feature Report 1 polled at 30 Hz,
  decoded as `UInt16 LE` whole degrees from bytes [1..2]. The previous
  SPU `usagePage 0xFF00 / usage 8` subscription was decoding
  uninitialized tail bytes — the IMU report is exhausted by accel/gyro
  at offsets 6/10/14, with no lid channel anywhere in the buffer. M2
  Pro/Max, M3 family, and M4 family laptops have the dedicated device;
  M1 (any), M2 base MBA / 13" MBP, and every desktop have either no
  device or no IMU silicon at all, and the toggle hides accordingly.
- **Ambient light corrected from `usage 7` to `usage 5`.** The M4 Max
  ioreg dump and AsahiLinux's reverse-engineering both confirm ALS
  rides on `usagePage 0xFF00 / usage 5`. The `7` was a guess that
  worked only because the SPU broker fans every report regardless of
  usage tuple — `usage 7` does not exist on the SPU bus.
- **Gyroscope and ALS gain App Store sandbox parity with the
  accelerometer.** `isAvailable` now combines device presence with a
  runtime activity probe via `_last_event_timestamp` on the
  `dispatchGyro` and `dispatchAls` services respectively. On Direct
  builds the probe is bypassed (the unsandboxed process can write the
  activation properties directly); on App Store builds the probe must
  report fresh-streaming for the toggle to surface. A one-shot
  `activelyReporting=` log fires at every source start so silent
  failures are visible in the audit log.
- **`yamete-sensor-kickstart` now activates gyro and ALS alongside
  accel.** The helper previously short-circuited the
  `AppleSPUHIDDriver` iterator on `dispatchAccel`, leaving the gyro
  and ALS kernel-driver instances silent on App Store builds. The
  iterator now walks every service flagged with any of
  `dispatchAccel`, `dispatchGyro`, or `dispatchAls` and writes the
  activation triplet to each. Existing helper installs need a one-time
  `sudo ./install.sh` to pick up the change.
- **`AccelHardware.isSensorActivelyReporting` is now parameterised
  over `dispatchKey`** so gyro and ALS can mirror the accel probe.
  The probe also no longer bails on the first stale sibling — it
  scans the entire `AppleSPUHIDDriver` iterator and returns true if
  any matching service is fresh.

### Fixed
- **Lid sensor no longer fires nonstop `LidSlammed` reactions.** Field
  audit logs from this release showed 3 500+ `lidSlammed` events per
  hour with physically impossible angles (-322°, -244°, etc.) — the
  consequence of the offset-18 garbage decode driving the slam-rate
  state machine into a loop. The dedicated-device rewrite produces
  real angles in the `[0°, 180°]` range, and a defensive sanity gate
  rejects anything outside that envelope.
- **`keyboardBrightnessEnabled` is now wired into pipeline rebuilds.**
  Previously the impact pipeline's `shouldRun` predicate did not list
  `keyboardBrightnessEnabled`, so flipping it on alone (with every
  other output off) would not start sensors. The setting is now
  observed by `Yamete.startSettingsObservation` and included in
  `rebuildSensorPipeline.shouldRun`.
- **40 lprojs synchronised with the new keyboard/Caps Lock string
  keys.** `setting_led_enabled`, `setting_keyboard_leds`,
  `help_keyboard_leds`, and `legend_led` are dropped (replaced by
  `setting_keyboard_brightness`, `setting_caps_lock_led`,
  `help_keyboard_brightness`, `help_caps_lock_led`,
  `desc_caps_lock_led`, `legend_keyboard_brightness`,
  `legend_caps_lock`). Non-en locales receive English copy as a
  fallback for translator follow-up.]]></description>
    </item>
    <item>
      <title>Yamete 2.4.0</title>
      <link>https://studnicky.github.io/yamete/CHANGELOG#240</link>
      <guid isPermaLink="false">https://studnicky.github.io/yamete/changelog/2.4.0</guid>
      <pubDate>Wed, 06 May 2026 12:00:00 GMT</pubDate>
      <description><![CDATA[### Added
- **Yamete+ rebrand for the Direct download.** The notarised
  Developer-ID-signed download is now named **Yamete+** instead of
  "Yamete Direct". The bundle is `Yamete+.app`, the executable is
  `yamete-plus`, the bundle identifier is
  `com.studnicky.yamete.plus`, the DMG asset is `Yamete+.dmg`. The
  App Store variant keeps its existing `Yamete` / `com.studnicky.yamete`
  identity. The menu's header title now reads "Yamete+" with a pink
  `+` glyph for Direct builds; App Store renders an unmarked
  "Yamete". The previous footer build-variant pill is gone — the
  header carries the indicator.
- **Footer reorganisation.** Footer cells render as a single
  priority-ordered list flowing column-major into a 2-column grid:
  Debug Logging (Direct only) → Launch at Login → Version → Reset
  Settings → Info Links → Quit. With Debug Logging absent (App
  Store), Launch at Login slides into the top-left slot. The
  central inter-column divider that read as a table border is gone.

### Changed
- **Auto-update path from "Yamete Direct" to "Yamete+".** Existing
  pre-2.4.0 installs auto-update through their legacy `Yamete.Direct.dmg`
  asset name, which the 2.4.0 release publishes as a transitional
  alias alongside the canonical `Yamete+.dmg`. After the update
  installs, a one-shot bundle-relocation step at app launch detects
  the legacy `/Applications/Yamete Direct.app` path, copies the
  bundle to `/Applications/Yamete+.app`, removes the legacy bundle,
  and relaunches from the canonical path. UserDefaults migrate from
  `com.studnicky.yamete.direct` to `com.studnicky.yamete.plus` on
  first launch under the new identifier — every existing setting
  carries over. Input Monitoring TCC grants do **not** carry over
  (macOS scopes them per-bundle-identifier); the diagnostics row
  surfaces the missing-permission state with an "Open System
  Settings…" deep-link the moment a feature that needs it is used.
- **List containers drop their wrapping background.** The faint
  grey rounded-rect around DeviceToggleList, SelectionList, and
  NotificationLocalePicker is gone; per-row dividers carry the
  "this is a list" affordance. Diagnostics pills lose their stroke
  for the same reason — the tinted fill is enough.
- **Pre-push hook auto-refreshes host-app snapshot baselines.** New
  `scripts/refresh-host-app-snapshots.sh` runs only for pushes to
  `release/*` and `hotfix/*` branches; it wipes the sandbox-mirror
  at `~/Library/Containers/com.studnicky.yamete/Data/tmp/yamete-snapshots/HostApp/`,
  invokes `make test-host-app` twice (recording iteration tolerates
  the standard ".missing"-mode failures, verification iteration
  must pass clean), and syncs freshly-recorded baselines back to
  `Tests/__Snapshots__/HostApp/SnapshotUI_Tests/`. If the source
  tree moved, the push is refused with a clear "stage and commit
  these and re-push" message. Feature branches and CI self-skip.]]></description>
    </item>
    <item>
      <title>Yamete 2.3.0</title>
      <link>https://studnicky.github.io/yamete/CHANGELOG#230</link>
      <guid isPermaLink="false">https://studnicky.github.io/yamete/changelog/2.3.0</guid>
      <pubDate>Wed, 06 May 2026 12:00:00 GMT</pubDate>
      <description><![CDATA[### Added
- **Diagnostics row.** New surface in the menu directly below the header
  that aggregates active warnings/errors (paused state, missing
  Input Monitoring permission, sensor errors). Hidden when no
  diagnostics are active. The "paused" indicator that previously sat
  inside the top header row moved here so the header is back to a clean
  2x2 grid (title | impacts; subtext | last-impact).
- **Input Monitoring permission sentinel.** Yamete now consults
  `IOHIDCheckAccess` at launch and on app foreground. When permission
  is missing (most commonly because `make install`'s ad-hoc resign
  changed the bundle's cdhash and macOS silently revoked the prior
  grant), the diagnostics row surfaces "Input Monitoring required" with
  an "Open System Settings…" button that deep-links to the Privacy &
  Security pane. The previous failure mode was silent — no mouse clicks,
  no keyboard typing, no Caps Lock LED, with only a single warn-level
  log line as evidence.
- **`InputMonitoringAccess` helper** (`Sources/YameteCore/`)
  consolidates the three previously-inline TCC checks
  (`MouseActivitySource`, `KeyboardActivitySource`,
  `RealLEDBrightnessDriver`) onto a single `status()` /
  `requestIfUnknown()` API. Sources never prompt; only
  `Yamete.bootstrap()` does, matching macOS's "one prompt per process
  lifetime" semantics.
- **Active output only toggle for audio.** Mirrors the existing
  active-display-only toggle: when on, impact sounds route to the
  system default audio device only, ignoring the per-device selection.
  Hidden when ≤1 audio device is selected (same gating rule applied
  to the active-display-only toggle for consistency). Useful when the
  user wants the sound to follow "wherever I'm listening right now"
  instead of fanning out across configured outputs.
- **Audio transport class tagging.** Each audio output row in the
  menu now renders an icon derived from
  `kAudioDevicePropertyTransportType`: `laptopcomputer` for built-in
  speakers, `tv` for DisplayPort/HDMI, `cable.connector` for USB,
  `headphones.bluetooth` for wireless, `headphones` for analog jack,
  `bolt.horizontal` for Thunderbolt, `hifispeaker` otherwise. Lets the
  user tell at a glance which outputs are monitor speakers vs USB
  headsets vs wireless devices.
- **EDID-based monitor↔speaker pairing.** Audio devices that ride on a
  display's video cable (DisplayPort, HDMI, MacBook built-in) now get
  paired to the display they belong to. The pairing reads the standard
  EDID identifier triple (vendor, product, serial) from the audio
  device's IORegistry ancestors and matches against
  `CGDisplayVendorNumber`/`ModelNumber`/`SerialNumber`. Built-in
  speakers pair structurally to whatever display reports
  `CGDisplayIsBuiltin == 1`. Audio rows render an "attached to *&lt;display&gt;*"
  footnote when pairing succeeds. See
  [docs/architecture/display-audio-pairing](architecture/display-audio-pairing)
  for failure modes (USB-C docks, KVMs, adapter chains, monitors with
  zero-EDID-serial).

### Changed
- **`AppleSPUDevice` lifecycle race fix.** The multi-subscriber broker
  for the SPU HID device serialised concurrent open/close transitions
  through a new `transitionLock`. Previous behaviour: when the four
  initial subscribers (accel, gyro, ALS, lid) raced into the
  refcount-zero open path, the loser's cleanup called
  `SensorActivation.deactivate()` on the winner's still-live session,
  killing the report stream after one report and tripping the 5-second
  watchdog. The fix eliminates the race-loser teardown path entirely;
  there is now exactly one IOKit lifecycle operation in flight at a
  time across the broker. New regression test
  `testConcurrentSubscribes_openDeviceCalledExactlyOnce` pins the
  invariant.
- **Active-display-only toggle visibility.** The toggle now only
  renders when ≥2 displays are selected. With 0 or 1 displays
  selected the toggle adds no signal and was UI noise. The audio
  side gets the same treatment.
- **Header layout simplification.** The top bar's centre column
  (which held the conditional "Paused" pill) is gone. Header is now a
  clean 2x2: title and impacts counter on the first row, rotating
  subtext and last-impact tier on the second. The paused indicator
  surfaces in the diagnostics row instead.

### Fixed
- **`MainActor` mentions in commit messages and prose.** All future
  mentions in markdown / prose contexts use `MainActor` without the
  `@` prefix to avoid GitHub's @-mention parser indexing an unrelated
  user. Source code (`*.swift`) keeps the `@MainActor` attribute as
  required by the Swift compiler.]]></description>
    </item>
    <item>
      <title>Yamete 2.2.0</title>
      <link>https://studnicky.github.io/yamete/CHANGELOG#220</link>
      <guid isPermaLink="false">https://studnicky.github.io/yamete/changelog/2.2.0</guid>
      <pubDate>Tue, 05 May 2026 12:00:00 GMT</pubDate>
      <description><![CDATA[### Added
- **Thermal sensitivity ratchet.** New `thermalRatchet` tunable gates
  the sensitivity of thermal state transitions to prevent hair-triggering
  during power/load transients. Configurable per-state thresholds (nominal,
  fair, serious, critical) with separate warmup and cooldown windows.

### Changed
- **Documentation reorganization.** ARCHITECTURE and CONFIGURATION guides
  split into dedicated pages in `docs/`. README streamlined to link to
  full documentation.
- **Diagram enhancements.** Architecture diagrams now support zoom and
  improved visual clarity for large sensor topology graphs.

### Fixed
- **Lint configuration alignment.** ESLint and TypeScript strict checks
  unified across the build system.]]></description>
    </item>
    <item>
      <title>Yamete 2.1.1</title>
      <link>https://studnicky.github.io/yamete/CHANGELOG#211</link>
      <guid isPermaLink="false">https://studnicky.github.io/yamete/changelog/2.1.1</guid>
      <pubDate>Mon, 04 May 2026 12:00:00 GMT</pubDate>
      <description><![CDATA[### Fixed
- **Release workflow** now reads `docs/CHANGELOG.md` instead of the
  pre-reorg root `CHANGELOG.md` path. The v2.1.0 tag's release run
  failed at the changelog-extract step (`awk: can't open file
  CHANGELOG.md`) and never published the GitHub Release with the DMG
  attached. CI-only fix; no app behaviour changes between v2.1.0 and
  v2.1.1. v2.1.1 is the version that actually ships a published release.]]></description>
    </item>
    <item>
      <title>Yamete 2.1.0</title>
      <link>https://studnicky.github.io/yamete/CHANGELOG#210</link>
      <guid isPermaLink="false">https://studnicky.github.io/yamete/changelog/2.1.0</guid>
      <pubDate>Mon, 04 May 2026 12:00:00 GMT</pubDate>
      <description><![CDATA[### Added
- **Four new sensor surfaces, each with full menu controls and per-reaction
  matrix integration.** Following gap analysis against four reference repos
  (`olvvier/apple-silicon-accelerometer`, `pirate/mac-hardware-toys`,
  `harttle/macbook-lighter`, `vlasvlasvlas/spank`), Yamete now publishes:
  - **Gyroscope spikes** (`.gyroSpike`) — lid yank, laptop spin, rotation
    transients. Same Apple Silicon SPU HID device as the BMI286 accelerometer,
    different HID usage (9 vs 3). Six-gate consensus pipeline (`GyroDetector`)
    tuned for deg/s magnitude with separate spike threshold, rise rate,
    crest factor, confirmation count, warmup, and cooldown.
  - **Lid angle state transitions** (`.lidOpened`, `.lidClosed`, `.lidSlammed`)
    via SPU HID usage 8. State machine (closed/opening/open/closing) with
    EMA smoothing on Δangle/Δt to suppress jitter. The slam gate fires when
    rate-of-change crosses a configurable negative threshold while the angle
    approaches zero.
  - **Ambient light step changes** (`.lightsOff`, `.lightsOn`, `.alsCovered`)
    via SPU HID usage 7. Two-second ring buffer + step detector with separate
    percent and floor/ceiling gates per direction. The `.alsCovered` path
    has its own faster rate constraint so a hand over the sensor reads
    distinctly from gradual room dimming.
  - **Thermal pressure transitions** (`.thermalNominal`, `.thermalFair`,
    `.thermalSerious`, `.thermalCritical`) via `NSProcessInfo
    .thermalStateDidChangeNotification`. Cold-start suppression: the initial
    state captured at start does not publish. Per-state dedup. No tunable
    thresholds — the levels are OS-defined.
- **AppleSPUDevice broker.** Refactored the previously-direct accelerometer
  IOHID handle into a ref-counted multiplexer with a single input-report
  callback that fans out to every subscriber regardless of `(usagePage,
  usage)` — gyroscope, lid, and ambient-light sources subscribe alongside
  the accelerometer and decode their own byte offsets from the shared
  report. Phase 0 of the v2.1.0 plan. The activation/deactivation
  three-phase teardown invariant is preserved verbatim from the previous
  implementation; only the LAST subscriber's release closes the device.
- **Two emission patterns documented.** Continuous-stream sources (accel,
  microphone, gyroscope, ambient light) run a detector pipeline over a
  high-frequency sample stream and emit one Reaction on threshold cross;
  discrete state-transition sources (USB, power, audio, BT, Thunderbolt,
  display, sleep/wake, lid, thermal) observe an OS notification surface
  and emit one Reaction per user-meaningful state change. Both feed the
  same bus.

- **Menubar redesign.** Header now puts the static "Yamete" title and face
  art on the left with a body-only rotator strip; impact counter folded
  into the header rows; paused indicator centred between title and counter
  and re-renders on fusion-state change. Master kill-switch row per group
  drives a 35→60% dim of the group's contents via semantic color roles.
  Four group accordions (impact / device / response / sensor) default to
  collapsed. Header rotator pulls spicy moans from the Direct overlay
  with non-repeating shuffle. Impact-detection promoted to its own master
  accordion with Impact Consensus rendered below the threshold row.
- **Impact Consensus relabel.** "Sensor Consensus" → "Impact Consensus"
  across all 40 lprojs; help text and unit copy follow. The consensus
  slider only governs impact-sensor fusion (accelerometer, microphone,
  AirPods motion); with eleven event sources in v2.1.0 — most discrete
  state-transition — the original label was misleading.
- **Active-above-inactive stimuli sort.** `StimuliSection` partitions
  enabled stimuli above disabled, alphabetised within each group by
  localised title. Toggling a stimulus relocates it on the next render.
  Impact-sensor consensus group is composed in `SensorSection` above
  `StimuliSection` and is pinned by composition, not by sort.
- **App icon regenerated.** New 10-resolution `AppIcon.appiconset`
  rendered from the canonical face SVG: pink rounded bezel + white
  face. Replaces the inherited template icon.
- **Observation contract tests.** New `Tests/ObservationContractTests.swift`
  asserts that view-consumed Observable types stay coupled to SwiftUI
  re-render — caught at test time instead of "the menu didn't update."

### Changed
- Event source count documented as **eleven** (was: seven).
- Mutation catalog grew from 111 → 130 entries (5 gyro, 5 lid, 5 ALS, 4
  thermal). All caught.
- README slimmed; ARCHITECTURE / CHANGELOG / LICENSES-CONTENT / PRIVACY
  moved into `docs/`.
- Two passes of historical/temporal comment scrubbing across SensorKit,
  YameteApp views, and Tests.

### Fixed
- Paused indicator now re-renders on fusion-state change (was static
  after first paint).]]></description>
    </item>
    <item>
      <title>Yamete 2.0.0</title>
      <link>https://studnicky.github.io/yamete/CHANGELOG#200</link>
      <guid isPermaLink="false">https://studnicky.github.io/yamete/changelog/2.0.0</guid>
      <pubDate>Sat, 02 May 2026 12:00:00 GMT</pubDate>
      <description><![CDATA[### Added
- **Reaction Bus architecture.** Single multi-subscriber `ReactionBus` actor
  in `YameteCore` with `bufferingNewest(8)` per subscriber. Sources publish
  onto the bus; outputs subscribe independently and pattern-match the unified
  `Reaction` envelope. `Reaction.impact(FusedImpact)` is just one case
  alongside cable/power/device events — the case IS the contract, no nested
  payload type, no dispatch table, no event-router. Adding a new event class
  is one new `Reaction` case + one new source class + the compiler tells
  every output what it now needs to handle.
- **LED flash output.** New `LEDFlash` (`Sources/ResponseKit/LEDFlash.swift`)
  pulses the keyboard's Caps Lock LED on every reaction the user has enabled,
  gated through the same intensity envelope as `ScreenFlash`. Brightness is
  PWM-dithered at 60Hz against the shared `Envelope` since Caps Lock is
  binary on/off. Caps Lock state is captured before the pulse and restored
  after — there is a brief window during which Caps Lock toggles rapidly.
  Joke app, accept the inaccuracy. Toggleable in the new LED Flash menu bar
  section with brightness min/max sliders. No new entitlements required
  (uses existing `com.apple.security.device.usb`).
- **Cable / power / device event sources.** Seven new IOKit / CoreAudio /
  IOPS notification sources publishing onto the bus:
  - `USBSource` — IOServiceMatching `kIOUSBDeviceClassName`, suppresses
    initial replay burst, debounces vendor/product 50ms.
  - `PowerSource` — `IOPSNotificationCreateRunLoopSource`, edge-triggered on
    AC connect / disconnect transitions.
  - `AudioPeripheralSource` — `AudioObjectAddPropertyListener` on
    `kAudioHardwarePropertyDevices` with set diffing for per-device events.
  - `BluetoothSource` — IOServiceMatching `IOBluetoothDevice` (pure IOKit
    path, avoids private `IOBluetooth.framework` symbols).
  - `ThunderboltSource` — IOServiceMatching `IOThunderboltPort`.
  - `DisplayHotplugSource` — `CGDisplayRegisterReconfigurationCallback`,
    debounced 200ms to collapse macOS's 3–4 callbacks per real change.
  - `SleepWakeSource` — `IORegisterForSystemPower` for `willSleep` /
    `didWake` (same API the sensor-kickstart helper uses to re-warm the
    BMI286 on wake).
  Per-event default intensities live in `ReactionsConfig` (single tuning
  surface). All seven sources ship enabled by default; user toggles each
  on/off in the new Cable & Power Events menu bar section.
- **Per-(output × event) toggle matrix.** Each of the four configurable
  outputs (sound, screen flash, notification, LED) has an independent
  per-`ReactionKind` enable/disable toggle persisted as a JSON-encoded
  `ReactionToggleMatrix`. The user can have sound-only on USB attach,
  flash-only on AC unplug, notification-only on display reconfiguration,
  etc. — fully orthogonal across outputs.
- **`Events.strings` notification table.** New
  `App/Resources/en.lproj/Events.strings` with `title_<kind>_<n>` /
  `body_<kind>_<n>` pools per reaction kind, mirroring the existing
  `Moans.strings` numbered-suffix convention. Loader caches the table
  separately so impact-pool clears don't blow it away.
- **`com.apple.security.device.bluetooth` entitlement** added to the App
  Store build for `IOBluetoothDevice` IOService matching.

- **`FiredReaction` enriched event envelope.** `ReactionBus` now resolves
  `clipDuration`, `soundURL`, `faceIndices` (one per connected display), and
  `publishedAt` exactly once before fan-out via a registered `ReactionEnricher`
  closure. All subscribers receive identical pre-resolved values — no per-output
  duration math, no duplicate face/clip selection. Enricher has a 0.5 s timeout
  with a safe fallback and a set-once precondition to prevent post-bootstrap
  replacement.
- **`FaceLibrary` shared face cache.** Centralized face image cache and
  per-event recency dedup scoring (`selectIndices(count:)`). Called once by the
  enricher; `ScreenFlash` and `MenuBarFace` both look up by index. Appearance-
  aware (reloads on dark/light mode change). Replaces the two independent caches
  that were in `ScreenFlash` and `MenuBarFace`.
- **Per-display face matching.** `FiredReaction.faceIndices[i]` maps to
  `NSScreen.screens[i]`. `ScreenFlash` shows a different scored face on each
  display; `MenuBarFace` shows `faceIndices[0]` (primary display). Added
  `faceIndex(for:)` bounds-safe accessor to handle display count changes between
  enrichment and rendering.
- **Keyboard brightness spring animation in `LEDFlash`.** `KeyboardBrightnessClient`
  (CoreBrightness private framework) controls keyboard backlight. Animation uses
  a damped spring centred on the user's current brightness level so it never
  drives the backlight to zero. Includes: idle-dimming suspension, launch-time
  brightness snapshot with crash-recovery sentinel file (`kb_dirty`), and
  `validateArgCount` guard on every `unsafeBitCast` dispatch to
  `KeyboardBrightnessClient` selectors.
- **`EventSource` protocol.** Separate from `SensorSource` — the seven
  infrastructure event sources (`USBSource`, `PowerSource`, etc.) now conform to
  `EventSource` rather than the impact-sensor `SensorSource`.
- **`ImpactFusionConfig` rename.** `FusionConfig` renamed to `ImpactFusionConfig`
  for naming consistency with `ImpactFusion` and the `*OutputConfig` family.
- **`Yamete.shutdown()` and app-quit cleanup.** `applicationWillTerminate(_:)`
  calls `yamete.shutdown()`, which cancels all output tasks, stops the fusion
  pipeline, closes the bus, and calls `ledFlash.resetHardware()` to restore
  keyboard brightness before process exit.
- **Canonical docs source system.** `docs/_includes/what-it-does.md` and
  `docs/_includes/under-the-hood.md` are the canonical prose for the project
  description; HTML pages and README reference them. `make docs-check` validates
  all source file references in `docs/*.html` at lint time. `docs/_config.yml`
  enables Jekyll on GitHub Pages without changing the visual design.
- **New test files.** `FiredReactionTests`, `BusEnricherTests`,
  `ReactionsConfigTests` (exhaustiveness check for `eventIntensity` map).

- **Haptic feedback output.** New `HapticResponder` fires rapid Force Touch
  haptic pulses on each reaction. Pulse density scales with the `Envelope`
  level — dense at the attack peak, sparse through the decay tail. Intensity
  is a user-adjustable multiplier (0.5×–3.0×). Works on any Mac with a Force
  Touch trackpad. No entitlements required.
- **Display brightness flash output.** New `DisplayBrightnessFlash` spikes
  the main display's brightness above the user's current level on hard
  impacts, then restores it over the `Envelope` fade window. Peak = current +
  user-configured boost × intensity. Uses `DisplayServicesGetBrightness` /
  `DisplayServicesSetBrightness` loaded at runtime via `dlopen` (private
  framework, no entitlements required). Gated by a minimum-intensity threshold.
- **Screen tint output.** New `DisplayTintFlash` briefly tints the display
  pink by crushing the green and blue gamma channels via
  `CGSetDisplayTransferByTable` (public CoreGraphics). Tint depth scales with
  `Envelope` level. Automatically skipped on macOS 26+ where the gamma table
  path is unreliable. No entitlements required.
- **Volume spike output (Direct build only).** New `VolumeSpikeResponder`
  temporarily raises system output volume to a target level on hard impacts,
  then restores the original volume after the reaction window. Uses
  `kAudioHardwareServiceDeviceProperty_VirtualMainVolume` via AudioObjectAPI.
  Gated by a minimum-intensity threshold. Compiled only with `#if DIRECT_BUILD`.
- **Trackpad activity event source.** New `TrackpadActivitySource` (`EventSource`,
  not part of impact fusion) uses `NSEvent.addGlobalMonitorForEvents` with a
  sliding RMS window to detect two patterns: `trackpadTouching` (sustained
  scroll/gesture activity) and `trackpadSliding` (high-velocity scrolling).
  Both publish directly to the `ReactionBus`, independent of the impact
  pipeline. Configurable window duration and activity threshold. Enabled by
  default. Tunable via the new Trackpad Tuning accordion in the menu bar.
- **Per-(output × event) matrix for new outputs.** Haptic, display brightness,
  display tint, and volume spike each have their own `ReactionToggleMatrix`
  persisted in UserDefaults. Event matrix rows in the menu bar UI now show two
  rows of output toggles: core (audio, flash, notification, LED) and hardware
  (haptic, brightness, tint, volume spike).
- **Trackpad Tuning section.** New collapsible accordion in the menu bar (shown
  when Trackpad Activity source is enabled) with sliders for activity window
  duration, sensitivity threshold, and intensity scale.
- **Test infrastructure overhaul.** Multi-layer test suite landed alongside
  the reactive-output pipeline:
  - **Onion-skin Ring 1 / Ring 2 architecture** for the matrix bus tests.
    Ring 1 cells exercise pure logic with mock outputs; Ring 2 cells round-trip
    through a real `ReactionBus` with `MatrixSpyOutput` fixtures and assert
    cross-source interleavings, drop-not-cancel semantics, and coalesce
    fan-out invariants.
  - **Mutation runner with 111 catalog entries.** `make mutate` walks
    `Tests/Mutation/mutation-catalog.json`, applies each mutation to its
    target source file in-place, runs the suite, and asserts the mutation
    is caught by at least one failing test. Per-target slice runner
    (`scripts/mutation-test-slice.sh`) drives the GitHub Actions
    `Mutation catalog (PR slice)` lane.
  - **Property-based fuzz cells** (`Tests/Property_*Tests.swift`) — bus
    invariants over randomised stimulus streams, locale × plural fuzz
    against the localisation matrix, settings-corruption fuzz against
    `SettingsStore`, and state-machine model checks for the lifecycle
    state transitions.
  - **Concurrent-interleaved fuzz** that publishes from multiple sources
    on overlapping timelines and asserts no double-action / no missed
    drop / no leaked task across a randomised interleave space.
  - **~28 SwiftUI snapshot baselines per build variant** across four
    variants (`AppStore`, `Direct`, `HostApp`, `CI`) covering header,
    device, response, footer, accordion-card row counts, theme palette
    swatches, trackpad-tuning expand/collapse, and (Direct) headphone /
    accel tuning. Records via `SNAPSHOT_RECORD_MODE=true`; reads via
    `precision: 0.99 / perceptualPrecision: 0.98` to tolerate antialiasing
    drift.
  - **Performance baselines** (`Tests/Performance/baselines.json`) with
    absolute thresholds and a `make perf-baseline` regression gate
    (`tolerance_factor: 2.0×` default).
  - **Driver Real-vs-Mock parity tests** that run the same matrix
    against the production driver and the mock and assert byte-equal
    outputs.
  - **Crash-handling boundary audit** and **cross-boundary fault
    injection** cells that verify each Source / Bus / Output boundary
    surfaces typed errors rather than silently swallowing them.
  - **CI-tolerance helpers** (`Tests/Helpers/AwaitUntil.swift`) with
    `CITiming.envelopeMultiplier` (3× under CI), `awaitUntil(...)`
    polling primitive, and `skipIfCIBaselineMissing(...)` snapshot
    bootstrap. Detects CI via either the `CI=true` env var or a
    `/Users/runner/` bundle-path prefix (xcodebuild does not always
    propagate the env var into the test bundle).
  - **Host-app xcodebuild lane** (`make test-host-app`,
    `.github/workflows/host-app-test.yml`) that runs the YameteHostTest
    scheme inside a real `Yamete.app` bundle so cells gated on UN
    center, full Haptic engine, CGEvent.post under Accessibility, and
    Force-Touch trackpad probes execute their Real-driver halves
    rather than skipping under SPM `xctest`.
  - **Snapshot-baseline-seed workflow** (`.github/workflows/snapshot-baseline-seed.yml`)
    — workflow_dispatch helper that records macos-15-runner-rendered
    baselines under `Tests/__Snapshots__/CI/` and opens a PR adding
    them, addressing runner-vs-developer-host pixel drift.

### Changed
- **`SensorAdapter` protocol → `SensorSource` protocol.** Three concrete
  implementations renamed: `SPUAccelerometerAdapter` → `AccelerometerSource`,
  `MicrophoneAdapter` → `MicrophoneSource`, `HeadphoneMotionAdapter` →
  `HeadphoneMotionSource`. Public surface preserved.
- **`ImpactFusionEngine` → `ImpactFusion`.** Now owns the multi-source task
  fan-in lifecycle (start/stop) and publishes `Reaction.impact` onto the
  bus. The old `ingest(_:activeSources:)` API is preserved for tests;
  runtime path uses the engine's internal `activeSources` state.
- **`ImpactController` deleted.** The router-style controller dissolved into
  three pieces: sensor lifecycle moved into each `*Source` class, fusion is
  now `ImpactFusion`, response dispatch became per-output
  `consume(from:configProvider:)` loops on each `ResponseKit` output. The
  new `Yamete` orchestrator (`Sources/YameteApp/Yamete.swift`) owns the
  bus, sources, fusion, outputs, and lifecycle wiring — no routing.
- **`AudioResponder` / `VisualResponder` protocols deleted.** Outputs are
  now concrete classes with `consume(from: ReactionBus, configProvider:)`
  methods — no shared protocol because there's no polymorphic dispatch
  surface.
- **Menu bar reaction face moved to `MenuBarFace`**
  (`Sources/YameteApp/MenuBarFace.swift`), an `@Observable` class that
  subscribes to the bus and updates its own `reactionFace` /
  `lastImpactTier` / `impactCount` state. `StatusBarController` reads from
  `yamete.menuBarFace.reactionFace` instead of `controller.reactionFace`.
- **Sensitivity gate is now `FusedImpact.applySensitivity(...)`** (a static
  method in `YameteCore`). The orchestrator installs it on
  `ImpactFusion.intensityGate` so the bus pipeline applies the user's
  sensitivity band before any output sees the impact.

### Fixed
- **`NSSound` lifetime.** `AudioPlayer` retains `NSSound` instances in
  `activeSounds: [NSSound]` and releases them only in
  `sound(_:didFinishPlaying:)`. Previously sounds were deallocated immediately
  after `.play()`, silently truncating playback.
- **IOKit NULL device guard in `LEDFlash.writeLED`.** `IOHIDElementGetDevice`
  can return nil when the keyboard disconnects mid-animation. Added a service-
  port validity check before calling `IOHIDDeviceSetValue`.
- **`IOHIDManager` closed on deallocation.** `LEDFlash.deinit` now calls
  `IOHIDManagerClose` so the kernel-side manager reference is released promptly
  rather than waiting for process exit.
- **`IONotificationPort` iterator cleanup on partial setup failure.** USB,
  Bluetooth, and Thunderbolt event sources now release any already-created
  IOKit iterators in the failure branch of `start()` before destroying the port.
- **IOKit context lifetime in event sources.** USB, Bluetooth, and Thunderbolt
  sources switched from `passUnretained` to `passRetained` context pointers,
  with explicit `release()` in `stop()` after iterator release, matching the
  accelerometer pattern.
- **`ReactionBus` enricher set-once precondition.** `setEnricher(_:)` now
  asserts `self.enricher == nil`; calling it twice is a programmer error.
- **`LogStore` file I/O errors surfaced.** `createDirectory`, `createFile`, and
  `FileHandle(forWritingTo:)` failures now log to `os.Logger` directly rather
  than silently no-op.
- **`ReactionToggleMatrix` encode/decode failures logged.** Previously returned
  empty data / empty matrix silently; now logs at `.error` level.
- **Consensus clamping logged.** When `consensusRequired` exceeds active source
  count, the effective value and reason are now logged at `.info`.
- **`HeadphoneMotionAdapter` probe timeout weak capture.** Changed from strong
  `[self]` to `[weak self]` in the `asyncAfter` closure, preventing the adapter
  from outliving its expected lifetime if deallocated before the 0.4 s probe
  fires.]]></description>
    </item>
    <item>
      <title>Yamete 1.3.2</title>
      <link>https://studnicky.github.io/yamete/CHANGELOG#132</link>
      <guid isPermaLink="false">https://studnicky.github.io/yamete/changelog/1.3.2</guid>
      <pubDate>Fri, 17 Apr 2026 12:00:00 GMT</pubDate>
      <description><![CDATA[No source-code changes since 1.3.1 — the shipped app binary is behaviorally identical. This release cuts a tag so the accumulated docs, Pages, and CI-hygiene work reaches the Pages site and the repo's default-facing README.

### Added
- **Dependabot config for GitHub Actions dependencies** — PR #40.
  `.github/dependabot.yml` schedules weekly action-version updates with
  minor + patch bumps grouped into a single PR. Swift Package Manager
  is not yet a Dependabot ecosystem (tracked upstream at
  dependabot/dependabot-core#7268) so SPM deps in `Package.swift` still
  require manual bumps.
- **Workflow-YAML linting via actionlint** — PR #40. New `actionlint`
  job in `ci.yml` using `raven-actions/actionlint@v2` catches syntax
  and shellcheck-level bugs in workflow YAML before a push lands on a
  runner. Caught a real SC2129 in `release.yml` on its first run.
- **App icon + an intentionally silly number of badges in the README**
  — PRs #43, #44. The `AppIcon.appiconset` 128x128 asset renders
  centered at the top of the README at 160px. Thirty-eight shields.io
  badges across six themed rows: ship status, platform + stack,
  what's inside, promises, flex, and a row of CI-shaped exclamation
  badges for things that are decidedly not CI status (current face,
  last impact tier, watchdog status, machine spirits, etc.).

### Changed
- **README redesigned: voice match Pages, link instead of duplicate**
  — PR #43. The README was repeating content that lives more
  comfortably on `studnicky.github.io/yamete` (How it works,
  Configuration, Distribution, Project structure, the ASCII detection
  diagram). The rewrite matches the project site's voice, sends
  visitors to the right Pages section via a link list at the top,
  and trims 157 lines down to ~70 + a badge wall. Single source of
  truth per topic lives on Pages; the README orients visitors.
- **User-facing docs refreshed to present tense** — PR #43. Scrubbed
  phrasing about what things "used to be", "was renamed", "now
  ships" across README, `docs/architecture.html`, `docs/support.html`,
  `docs/INSTALLATION.md`. Bumped `docs/assets/sidebar.js` version
  badge to 1.3.2 so every published page surfaces the current
  release consistently.
- **CI workflow concurrency groups** — PR #40. Rapid-fire pushes on
  non-`master` refs now cancel stale in-flight CI runs instead of
  stacking them behind each other. Release workflow uses the group
  for observability only (never cancels a tagged build mid-run).
- **Release workflow opted into Node.js 24** — PR #40. Node.js 20 is
  scheduled for removal from GitHub-hosted runners on 2026-09-16.
  `softprops/action-gh-release@v2` currently runs on Node 20; setting
  `FORCE_JAVASCRIPT_ACTIONS_TO_NODE24: "true"` at the workflow env
  level migrates ahead of the forced cutover.

### Fixed
- **Release notes were three separate `echo >> file` appends instead
  of a grouped redirect** — PR #40 follow-up. actionlint/shellcheck
  flagged the pattern as SC2129 on its first run; consolidated the
  three appends and the `awk` invocation into a single `{ ...; ... }
  > file` block. Same output, one syscall instead of four.]]></description>
    </item>
    <item>
      <title>Yamete 1.3.1</title>
      <link>https://studnicky.github.io/yamete/CHANGELOG#131</link>
      <guid isPermaLink="false">https://studnicky.github.io/yamete/changelog/1.3.1</guid>
      <pubDate>Fri, 17 Apr 2026 12:00:00 GMT</pubDate>
      <description><![CDATA[### Fixed
- **`MicrophoneAdapter` teardown crashed the test process under certain
  CoreAudio states (SIGSEGV)** — PR #38. Observed on CI run 24548266785
  on the 1.3.0-bound `develop`, but the underlying race was latent
  regardless of release.
  - Teardown order in `continuation.onTermination` was reversed:
    `inputNode.removeTap(onBus: 0)` ran BEFORE `engine.stop()`. Removing
    a tap while the engine is still running lets the audio thread fire
    one more buffer callback that dereferences state captured by the
    tap closure after it has been torn down. Correct order is stop
    first (blocks until pending audio-thread callbacks drain) then
    remove. Inline comment documents the invariant.
  - Added a format-validity gate on `inputNode.outputFormat(forBus: 0)`.
    Hosts with no real audio input (headless CI runners, containers,
    virtualized macOS) can return a format with zero channels or zero
    sample rate; `installTap` with such a format is undefined behavior
    in CoreAudio. The adapter now finishes the stream with
    `SensorError.deviceNotFound` up front so the manager falls through
    to other adapters cleanly instead of crashing.
  - The `engine.start()` failure branch now pairs its `continuation.finish`
    with `inputNode.removeTap(onBus: 0)` so a failed start doesn't leak
    an installed tap.]]></description>
    </item>
    <item>
      <title>Yamete 1.3.0</title>
      <link>https://studnicky.github.io/yamete/CHANGELOG#130</link>
      <guid isPermaLink="false">https://studnicky.github.io/yamete/changelog/1.3.0</guid>
      <pubDate>Fri, 17 Apr 2026 12:00:00 GMT</pubDate>
      <description><![CDATA[### Changed
- **Swift 6 language mode + complete strict concurrency checking** — PR #34.
  `project.yml` now sets `SWIFT_VERSION: "6"` and
  `SWIFT_STRICT_CONCURRENCY: complete`; `Package.swift` declares
  `swiftLanguageModes: [.v6]`. The codebase was already clean under
  `-strict-concurrency=complete` (the Makefile's `lint` target has been
  running with that flag for some time), so the bump produced no new
  compiler diagnostics — but it locks the contract going forward.
- **`@unchecked Sendable` surface shrunk from five app-type-level sites
  to two narrow framework-handle wrappers** — PR #34.
  - `AccelResources` is now a real `Sendable`; the unavoidable IOKit
    handle escape is isolated in a tiny private `IOKitHandles` struct
    whose soundness is justified by the phased construction → single-move
    publish → once-teardown lifecycle enforced by `OnceCleanup`.
  - `ReportContext.State` is now genuinely `Sendable`: its contained
    `HighPassFilter` and `LowPassFilter` were converted from `final class`
    to `struct: Sendable` with `mutating process(_:)`.
  - `HeadphoneConnectionTracker` drops `@unchecked`; under Swift 6,
    `OSAllocatedUnfairLock<Bool>` in a final-class NSObject subclass
    synthesizes `Sendable` correctly when every field qualifies.
  - `LogStore.State` drops `@unchecked`; the culprit was
    `ISO8601DateFormatter` (still non-Sendable under Swift 6), replaced
    with a `DateFormatter` using `yyyy-MM-dd'T'HH:mm:ss.SSSXXXXX` +
    `en_US_POSIX` + GMT that produces byte-identical output.
  - `HIDRunLoopThread.State` retains `@unchecked` because `Thread` and
    `CFRunLoop` handles cannot be escaped without redesigning the HID
    thread itself. The rationale comment now cites the concrete
    invariants (every access through `OSAllocatedUnfairLock<State>`,
    documented thread-safety of `Thread.cancel`/`CFRunLoopStop`,
    phased lifecycle) that make it sound.
- **`HighPassFilter` / `LowPassFilter` are now Sendable structs with
  `mutating process(_:)`** — PR #34. API note for any out-of-tree callers:
  these were previously final classes, so callers that held them by `let`
  must now use `var`. All in-tree call sites (AccelerometerReader + the
  test suite) have been updated.

### Fixed
- **`SensorManager` dropped terminal `adaptersChanged([])` snapshot under
  concurrent cancellation** — PR #33.
  `Sources/SensorKit/SensorAdapter.swift`'s per-adapter task body caught
  `CancellationError` and returned immediately, skipping the subsequent
  `activeTracker.remove(adapter) + adaptersChanged(...)` yield at the
  bottom of the task body. Under concurrent cancellation (two adapters
  terminating near-simultaneously) the tracker retained a stale active
  entry and the terminal empty-snapshot the consumer expects was never
  emitted. Surfaced by a flaky CI run on PR #30 where
  `SensorManagerTests.testEventsPublishesAdapterLifecycle` saw
  `snapshots.last == ["B"]` instead of `[]`. Cancellation is a normal
  adapter termination signal, not a failure, so it now falls through to
  the shared lifecycle emission.]]></description>
    </item>
    <item>
      <title>Yamete 1.2.0</title>
      <link>https://studnicky.github.io/yamete/CHANGELOG#120</link>
      <guid isPermaLink="false">https://studnicky.github.io/yamete/changelog/1.2.0</guid>
      <pubDate>Thu, 16 Apr 2026 12:00:00 GMT</pubDate>
      <description><![CDATA[### Added
- **MicrophoneAdapter + HeadphoneMotionAdapter lifecycle test coverage** — PR #30.
  New `MicrophoneAdapterLifecycleTests` and `HeadphoneMotionAdapterLifecycleTests`
  cover open/close symmetry, repeated open/close cycles, mid-stream cancellation,
  and typed-error propagation. Hardware-unavailable paths skip gracefully via
  `XCTSkip` so the suite stays green on CI without real microphones or motion-
  capable headphones. Total test count 129 → 137.
- **Framework-list drift guard** — PR #29. `make lint-frameworks` diffs the
  framework list across `Makefile`, `Package.swift`, and `project.yml` and
  fails on divergence. Wired into `make lint` so CI catches any future drift.
  Caught real drift on first run: `IOKit` was missing from the Makefile's
  `FRAMEWORKS` variable, and `UserNotifications` was missing from both the
  Makefile and `project.yml`. All three sources are now aligned on the same
  eight frameworks.

### Changed
- **`MenuBarView.swift` split into per-section files** — PR #28. The 948-line
  composition root with 14 nested view structs now lives as a 199-line
  `MenuBarView.swift` (composition root + `HeaderSection` + a few shared
  helpers) plus 12 files under `Sources/YameteApp/Views/MenuBar/`. Extracted
  views flipped from `private` → `internal`; the public `MenuBarView` API
  is unchanged. No behavior change, no renames.
- **Version extraction via `yq` instead of `sed`** — PR #29. The Makefile and
  the release workflow now pull `MARKETING_VERSION` / `CURRENT_PROJECT_VERSION`
  out of `project.yml` with `yq '.settings.base.…' project.yml` instead of a
  regex, removing the brittle dependency on a specific indent or quoting style.
  The Makefile errors out with an install hint if `yq` is missing; the release
  workflow installs it via `brew install yq` before the version check runs.
- **`SWIFT_ACTIVE_COMPILATION_CONDITIONS` quoting normalized in `project.yml`.**
  Debug, DebugDirect, and ReleaseDirect all now use double-quoted string
  values consistently (previously two were quoted and one was bare).
- **CI test job routed through `make test`** — PR #31. `.github/workflows/ci.yml`
  now invokes `make test` instead of `swift test`, keeping local and CI
  invocations in sync.
- **`StatusBarController.showPanel()` drops `DispatchQueue.main.async`** —
  PR #31. The class is already `@MainActor` so the dispatch hop was redundant.
  Replaced with `Task { MainActor in await Task.yield(); … }`, preserving the
  intentional deferral (letting SwiftUI re-layout after the
  `.menuBarPanelDidShow` notification) with one fewer scheduling primitive.

### Fixed
- **`SettingsStore.resetToDefaults()` force-cast crash risk and duplicate
  assignments** — PR #27. The method had 37 `as!` force-casts pulling values
  out of an `[String: Any]` defaults dict, and assigned `accelBandpassLowHz`
  and `accelBandpassHighHz` twice. Rewritten to assign every field directly
  from the typed `Defaults.*` constants in `Sources/YameteCore/Defaults.swift` —
  no `Any` round-trip, no duplicate assignments, no crash path if the defaults
  dict is ever mistyped. All 24 SettingsStore tests remain green.

### Documentation
- `project.yml` gains a YAML comment block above `SWIFT_INCLUDE_PATHS` /
  `HEADER_SEARCH_PATHS` explaining why both are required for the `IOHIDPublic`
  bridging module (the Swift driver locates `module.modulemap`, the C
  compiler resolves the headers it references — removing either breaks
  the bridging module).
- `project.yml` gains a comment above the `Yamete-AppStore:` scheme noting
  that its `run` config is `Debug`, so sandbox entitlements only apply via
  `make appstore` or archive, not via Xcode's Run button. A matching comment
  lives above the Makefile's `appstore:` target.]]></description>
    </item>
    <item>
      <title>Yamete 1.1.1</title>
      <link>https://studnicky.github.io/yamete/CHANGELOG#111</link>
      <guid isPermaLink="false">https://studnicky.github.io/yamete/changelog/1.1.1</guid>
      <pubDate>Thu, 16 Apr 2026 12:00:00 GMT</pubDate>
      <description><![CDATA[### Fixed
- **Recursive lock crash on launch-at-login** — PR #23. `AccelerometerReader.surfaceStall()` and `handleReport()` called `AsyncThrowingStream.Continuation.finish()`/`yield()` while holding the `OSAllocatedUnfairLock`. `AsyncThrowingStream._Storage` acquires its own internal `os_unfair_lock` in those methods, causing a non-reentrant double-acquisition (abort cause 89859). Fixed by capturing the continuation reference (and the value to yield) as the lock's return value, releasing the lock, then calling the continuation method outside.]]></description>
    </item>
    <item>
      <title>Yamete 1.1.0</title>
      <link>https://studnicky.github.io/yamete/CHANGELOG#110</link>
      <guid isPermaLink="false">https://studnicky.github.io/yamete/changelog/1.1.0</guid>
      <pubDate>Wed, 15 Apr 2026 12:00:00 GMT</pubDate>
      <description><![CDATA[### Added
- **Auto-update via GitHub Releases (Direct build only)** — PR #18.
  The previously stubbed `Updater` is now a full update lifecycle:
  checks GitHub Releases on launch (throttled to every 4 hours),
  downloads the signed+notarized DMG, installs, and relaunches. The
  menu bar footer surfaces live update state with contextual action
  buttons (check / download / install / restart). All update logic
  is gated behind `#if DIRECT_BUILD`; the App Store build keeps the
  stub because App Store Connect handles distribution there.

### Changed
- **Menu bar shell replaced: `MenuBarExtra` → directly-managed
  `NSStatusItem` + `NSPanel`** — PR #20, closes #19. SwiftUI's
  `MenuBarExtra(.window)` was rendering the popover with a blank gap
  above the content and letting the desktop wallpaper bleed through
  the background, making text unreadable on light wallpapers. The new
  `StatusBarController` manages an `NSStatusItem` directly and hosts
  the SwiftUI content inside an `NSPanel` backed by an
  `NSVisualEffectView` with `.menu` material, sized to the SwiftUI
  content before display. Icon reactivity is preserved via
  `withObservationTracking` rather than embedding `NSHostingView` in
  the status bar button. Escape and outside-click dismiss behavior
  are unchanged from user perspective.

### Fixed
- **Menu bar popover background transparency rendered content
  unreadable** — closes #19. See NSPanel migration above.]]></description>
    </item>
    <item>
      <title>Yamete 1.0.1</title>
      <link>https://studnicky.github.io/yamete/CHANGELOG#101</link>
      <guid isPermaLink="false">https://studnicky.github.io/yamete/changelog/1.0.1</guid>
      <pubDate>Fri, 10 Apr 2026 12:00:00 GMT</pubDate>
      <description><![CDATA[### Fixed
- **Accelerometer not detected on M5 Macs (Direct build)** — closes #15.
  `SPUAccelerometerAdapter.isAvailable` was gating on
  `isSensorActivelyReporting()` for all build variants, creating a
  chicken-and-egg in the Direct build: the UI hid the accel toggle
  because the sensor wasn't reporting, `impacts()` was never called,
  and so the sensor never started reporting. Worked on M4 because
  the sensor happened to be warm at launch; broke on M5 because
  macOS apparently doesn't keep the SPU accel warm by default on
  that silicon. The runtime probe is now gated behind
  `#if !DIRECT_BUILD` — Direct checks only `isSPUDevicePresent()`
  (it has full IOKit write access and can always self-activate via
  `SensorActivation.activate()` at pipeline start), while the App
  Store build keeps the full probe because the sandbox constraint
  is real there.

### Changed
- **Sensor kickstart helper is now a long-lived daemon with a wake
  watcher.** `docs/sensor-kickstart/yamete-sensor-kickstart.swift` has
  a `daemon` subcommand that runs `kickstart()` once on startup, then
  subscribes to IOKit system power notifications via
  `IORegisterForSystemPower` and re-runs the kickstart on every
  `kIOMessageSystemHasPoweredOn` event. The shipping LaunchDaemon plist
  (`com.studnicky.yamete.sensor-kickstart.plist`) ships with `KeepAlive
  = true` + `ProcessType = Background` + `daemon` arg, and a
  `ThrottleInterval = 10` to rate-limit crash-loop respawns. Idle CPU
  cost is effectively zero (the daemon sits parked in `CFRunLoopRun`
  waiting for notifications). Motivation: on the hardware we have
  tested the sensor stays live across sleep/wake, but this is defense
  in depth for hardware or macOS revisions we have not verified — even
  if the driver cools the sensor during sleep, the daemon's wake
  handler re-runs the kickstart before the user notices.
- **Helper renamed** from `docs/community/yamete-accel-warmup` to
  `docs/sensor-kickstart/yamete-sensor-kickstart`. The old "community"
  directory name was a vague dumping-ground; the new name reflects what
  the thing actually does. Rename cascades across the directory, the
  Swift source, the LaunchDaemon plist label
  (`com.studnicky.yamete.sensor-kickstart`), the binary install path
  (`/usr/local/libexec/yamete-sensor-kickstart`), the log path
  (`/var/log/yamete-sensor-kickstart.log`), the Swift function
  (`warmup()` → `kickstart()`), the CLI subcommand (`warmup` →
  `kickstart`), and every prose reference in public docs.
- **GitHub Pages now serves only public content.** `docs/` was the
  publish root but had been used as a dumping-ground for internal
  planning docs. Removed: `APP_STORE_METADATA.md`,
  `APP_STORE_RELEASE.md`, `APP_STORE_REVIEW_CHECKLIST.md`,
  `CODEX_REVIEW_PLAN.md`. Moved to repo root: `ARCHITECTURE.md`. Kept
  in `docs/` as legitimate public content: `index.html`,
  `support.html`, `privacy.html`, `INSTALLATION.md`,
  `sensor-kickstart/`.
- **Published GitHub Pages refreshed with personality.** The three HTML
  pages (`index.html`, `support.html`, `privacy.html`) were rewritten
  to match the app's actual voice — confident self-awareness instead
  of dry marketing speak — while keeping every bit of genuinely useful
  content and adding cheeky-but-helpful FAQs ("Wait, what is this
  app?", "Is this a joke?", "Do I actually need this? — No.", "Can I
  use this as a drum machine?"). The support page's accelerometer FAQ
  walks users through the sandbox situation and links to the
  sensor-kickstart helper for opt-in power users.
- **`@MainActor` → `MainActor` in all prose, commit messages, CHANGELOG,
  and release bodies.** Source code `.swift` files intentionally keep
  the `@MainActor` attribute — it is a compiler-enforced Swift language
  construct and GitHub's @-mention parser does not index source code
  blobs. Prose was scrubbed because GitHub was resolving `@MainActor`
  in commit bodies to an unrelated GitHub user whose login happens to
  collide with the Swift concurrency attribute. Two `git filter-repo`
  passes + force-push on master/develop + v1.0.0 re-tag cleaned the
  history; this release is clean by construction.

### Added
- **CLAUDE.md + scratch-doc patterns** now in `.gitignore`. Project-
  specific Claude Code instructions live in a gitignored
  `CLAUDE.md` at the repo root with the project-specific hard rules
  (no `@MainActor` in prose, no author email anywhere, build system
  quick reference, branch protection convention, helper internals).
  Ad-hoc scratch docs (`*.scratch.md`, `*.tmp.md`, `.plans/`, `.dev/`,
  `scratch/`, `plans/`, etc.) are all gitignored so they never leak
  into the repo.
- **IOKit system power message constants** (`MsgCanSystemSleep`,
  `MsgSystemWillSleep`, `MsgSystemHasPoweredOn`) are defined
  numerically in the helper because Swift's C importer cannot
  translate the `iokit_common_msg(X)` macro expansion. Values are
  `0xe0000000 | X` per `IOKit/IOMessage.h` and are stable across
  every macOS release since IOKit was introduced.
- **CI workflow** now bumps `actions/checkout` from `v4` to `v5` to
  stay off the Node.js 20 deprecation treadmill.

### Removed
- **Email contact** (`support@studnicky.com`) removed from every
  published page. GitHub Issues is now the only support channel, and
  every page that would otherwise say "email X" now says "file an
  issue at github.com/Studnicky/yamete/issues" with the same
  friendliness. `docs/privacy.html` Contact section points at
  `issues/new` instead of a `mailto:` link, with a one-liner
  explaining that because the app collects nothing and sends nothing
  over the network, there's nothing to ask about privately — open
  issues make better documentation anyway.]]></description>
    </item>
    <item>
      <title>Yamete 1.0.0</title>
      <link>https://studnicky.github.io/yamete/CHANGELOG#100</link>
      <guid isPermaLink="false">https://studnicky.github.io/yamete/changelog/1.0.0</guid>
      <pubDate>Fri, 10 Apr 2026 12:00:00 GMT</pubDate>
      <description><![CDATA[### Critical
- **App Store accelerometer: runtime availability probe replaces the
  unconditional passive-read assumption.** The prior "passive HID read
  always works because macOS warms the SPU sensor at boot" hypothesis
  was falsified by a cold-boot verification on a clean Mac with Yamete
  Direct uninstalled: `ioreg -rxc AppleSPUHIDDriver` showed the accel
  service (`dispatchAccel = Yes`, BMI286) with `ReportInterval = 0x0`,
  no `HIDEventServiceProperties` dict, and `DebugState._num_events = 0`.
  Two consecutive App Store launches (BTM auto + manual `open`) both
  produced `Watchdog staleness=5.0s sampleCount=0` — zero samples
  received, zero events in the driver. Conclusion: macOS does NOT
  independently warm the SPU accelerometer at cold boot; in prior
  observations the sensor was warm only because Yamete Direct (which
  *can* write to IORegistry from outside the sandbox) had run earlier
  in the session and left the sensor active.

  **Fix**: `SPUAccelerometerAdapter.isAvailable` now does a runtime
  probe via `AccelHardware.isSensorActivelyReporting()` that reads
  `DebugState._last_event_timestamp` on the `AppleSPUHIDDriver` service
  and compares against `mach_absolute_time()`. The adapter reports
  available only when the sensor has emitted a report within the last
  500ms (`AccelHardwareConstants.sensorActivityStalenessNs`). With this
  probe, `Migration.reconcileSensors` — which already runs on every
  launch and prunes unavailable adapters — correctly drops the
  accelerometer from the pipeline when the sensor is cold, letting the
  microphone + headphone-motion fallback path activate cleanly instead
  of letting the 5s watchdog fire on an empty stream while the user
  sees no impacts at all.

  **Driver internals discovered while building the probe**:
    - `IORegistryEntrySetCFProperty` with `ReportInterval`,
      `SensorPropertyReportingState`, `SensorPropertyPowerState` is a
      command channel, not a stored value. The driver's `setProperty`
      accepts the write, triggers the hardware, and returns
      `KERN_SUCCESS` without updating the IOKit property dict — so
      property read-back on the service always returns `0` regardless
      of whether the sensor is streaming.
    - `DebugState._num_events` is a monotonic counter that freezes at
      deactivation and doesn't reset until reboot — useless as a
      "currently active" signal on its own.
    - `DebugState._last_event_timestamp` (in `mach_absolute_time`
      units) is the only field that decays correctly when the sensor
      goes cold.
    - Sandbox rejection of `IORegistryEntrySetCFProperty` happens
      *before* the driver's `setProperty` is reached — the call
      returns `KERN_SUCCESS` to the client but the write never lands.
      This is why the App Store build cannot activate the sensor
      itself and depends on an external kickstart path.

  **External sensor kickstart for App Store users**: A minimal Swift
  helper (`docs/sensor-kickstart/yamete-sensor-kickstart.swift`) +
  LaunchDaemon plist is provided via support docs. Users who want the
  accelerometer in the App Store build compile it once with `swiftc`,
  install the LaunchDaemon to `/Library/LaunchDaemons/`, and reboot.
  The helper does the same three `IORegistryEntrySetCFProperty` writes
  that the Direct build's `SensorActivation.activate()` does, and
  since it runs outside App Sandbox its writes reach the driver. The
  sensor stays active across subscriber cycles and sleep/wake, and
  the daemon re-runs the kickstart on every wake as defense in depth.
  Verified empirically: after kickstart, the App Store build's probe
  returns true and `adapters=["Accelerometer", ...]` logs alongside
  `sampleCount=` entries at sustained 100Hz.

  **Sleep/wake verified (2026-04-10)**: The BMI286 is in Apple
  Silicon's always-on power domain. Verified empirically that once
  kickstarted, the sensor continues streaming at 100Hz across
  sleep/wake cycles without interruption — a 35-second sleep period
  advanced `_num_events` from 101 to 3615 (100.4 events/sec, exactly
  the awake rate), meaning the driver was emitting reports the entire
  time the lid was closed.

  **Still to verify on other Mac models and macOS revisions**:
  1. **Multiple Apple Silicon models.** All testing so far is on a
     single development MacBook. Should be re-verified on M1 / M2 /
     M3 / M4 across MacBook Air / MacBook Pro before submission to
     confirm `AppleSPUHIDDriver` / `dispatchAccel` / `DebugState` are
     present and behave identically on each generation.
  2. **macOS revisions.** `IORegistryEntrySetCFProperty` on
     `AppleSPUHIDDriver` with these specific property keys is an
     undocumented Apple-internal surface. A future macOS update could
     break it. Re-test before every App Store submission, and monitor
     reports via the support-docs helper link.

- **Accelerometer stream watchdog**: `SPUAccelerometerAdapter` now spawns
  a background `Task` per stream that polls `ReportContext.lastReportAt`
  every 1 second. If no reports arrive for 5 seconds, the watchdog calls
  `surfaceStall()` which terminates the stream with a recoverable
  `SensorError.ioKitError`. The controller's existing fusion path then
  falls back to microphone + headphone-motion automatically. The
  watchdog is cancelled in cleanup phase 0 so it cannot race with
  teardown. Polls do not race with `handleReport` — `lastReportAt` lives
  inside the same `OSAllocatedUnfairLock<State>` block.

- **HID teardown race fix**: `HIDRunLoopThread.join()` now blocks the cleanup
  path until the dedicated HID worker thread has fully exited. CF object
  mutations (`IOHIDManagerUnscheduleFromRunLoop`, `IOHIDManagerClose`) are
  reordered to run only after `join()`. ThreadSanitizer reproduced a SEGV
  in `CF_IS_OBJC` during cancel-before-first-report; the fix is TSAN-clean.
- **Unaligned `Int32` reads in accelerometer decoder**: Replaced
  `withMemoryRebound` (undefined behavior on misaligned offsets 6/10/14)
  with `UnsafeRawPointer.loadUnaligned(fromByteOffset:as:)`.
- **IOHIDEventSystemClient activation restored**: The C bridging header (IOHIDPublic.h) must declare `IOHIDEventSystemClientCreate` (full client). Using `IOHIDEventSystemClientCreateSimpleClient` silently fails to activate the SPU accelerometer — the sensor opens but delivers no reports. The full client is required to set ReportInterval on hardware services.

### Added
- **Dual-build resource architecture**: `App/Resources/{locale}.lproj/Moans.strings`
  ships tame App-Store-safe content; `App/Resources-Direct/{locale}.lproj/Moans.strings`
  overlays spicy content for the notarized direct-download build only. The
  Makefile rsync overlay step is gated on the directory's existence so the
  App Store build never sees the spicy strings. `appstore-lint` Makefile
  target greps the bundle for `daddy` as a leakage gate.
- **Notification mode** (Flash Mode = Notification): posts an impact banner
  with a tier-appropriate flirty title + reaction body. Runtime locale picker
  lets users select the notification language independently of system language.
  40 locales supported.
- **`NotificationPhrase` random pool loader**: parses `Moans.strings` for a
  given locale into prefix-grouped `[String]` arrays at first use, cached
  with `OSAllocatedUnfairLock`. Single-source-of-truth `resolveLocale`
  guarantees title and body always come from the same language per notification.
- **Always-on menu bar face reaction**: the menu bar icon swaps to a face
  image on every detected impact, independent of Flash Mode. Uses a cached
  face library rebuilt only on dark/light mode changes.
- **`AccelerometerLifecycleStressTests`**: 25-cycle repeated open/close +
  10-cycle cancel-before-first-report stress tests. Skips on hosts without
  an SPU device.
- **`NotificationResponderTests`**: 8 unit tests for `NotificationPhrase`
  resolveLocale fallback, pool selection, and random sampling. Uses an
  internal `_testInject` seam since `.lproj` resources are bundled by the
  Makefile, not the SPM test runner.
- **`Makefile lint` target**: `swiftc -typecheck -strict-concurrency=complete
  -warnings-as-errors`. Currently passes clean.
- **`Makefile appstore-lint` target**: bundle-content gate that fails if any
  shipped `Moans.strings` contains the substring `daddy`.

### Changed
- **Renamed `FlashResponder` → `VisualResponder`** to match current responsibilities
  (overlay AND notification both implement it).
- **Rewrote `APP_STORE_METADATA.md`**: age rating 12+, content descriptors
  reflect the dual-build distinction, App Review notes acknowledge that
  the accelerometer driver-property surface is undocumented and offer to
  remove the path entirely if Review prefers.
- **Rewrote `APP_STORE_RELEASE.md` BLOCKER-1**: removed the "Risk: Low"
  framing; now explicitly distinguishes public IOKit symbols (compliant)
  from undocumented driver behavior (gray area under 2.5.1).
- **`screenFlash` is now a computed proxy** over `visualResponseMode`.
  `Key.screenFlash` removed from `SettingsStore.Key`; the persisted
  `screenFlash` UserDefault is consumed once at init for legacy migration
  (`screenFlash == false` forces `visualResponseMode = .off`), then deleted.
- **`ImpactController.shouldBeEnabled`** reads `visualResponseMode != .off`
  directly instead of `screenFlash`.
- **`-strict-concurrency=complete` cleanup**: `FaceRenderer.Palette` is
  `Sendable`; `currentPalette` and `loadFaces(palette:)` are `MainActor`;
  `Fmt` slider formatters are `@Sendable` closures; `arrayToggleBinding`
  is `MainActor` and requires `T: Sendable`; `GateRows` is `MainActor`;
  `Toggle.themeMiniSwitch` is `MainActor`; `OnceCleanup` requires
  `T: Sendable`; `LogStore.State` is `@unchecked Sendable` (lock-protected).
- Removed the `import YameteApp` self-import from `YameteApp.swift` (was a
  Makefile-only no-op warning that became an error under `-warnings-as-errors`).

### Removed
- **`FaceRenderer.composeIcon`**: was used to render a 1024×1024 dock-icon
  variant of the face. Yamete is `LSUIElement` (no Dock icon at all), so
  the compose path was dead code.
- **All `NSApp.applicationIconImage` swap code**: same reason — no Dock.
- **All `moan_*` and `title_*` keys from `App/Resources/*.lproj/Localizable.strings`**.
  These now live in `Moans.strings` (tame) and the Direct overlay (spicy).
- **Editorialized translator comments** ("DDLG kink register", etc.) from
  the App-Store-bound `Localizable.strings` files. Spicy comments survive
  only in `App/Resources-Direct/`, never shipped to the Store.

### Fixed
- **Visual Response = Off no longer animates dock/menu bar in disallowed
  ways**: dock did not exist; menu bar always reacts (now documented).
  Setting renamed UI label to "Flash Mode".
- **Notification copy mismatch**: hint text no longer claims notifications
  "clear when the cooldown ends" — it now correctly says the entry is
  removed from Notification Center, with banner visibility controlled by
  macOS.
- **`notification_title` showing as a literal key**: `LocalizedStrings`
  helper falls back to the main bundle (which goes through
  `CFBundleDevelopmentRegion`) when a key is missing in the requested
  lproj. Title and body now both resolve correctly across locales.
- **Menu bar icon scaling during reaction**: SVG NSImage is resized to
  18pt logical size before storing in `reactionFace`. The menu bar honors
  intrinsic NSImage size, not SwiftUI `.frame()`, inside MenuBarExtra.
- Site, privacy, and support pages aligned with the dual-build reality
  and the actual (non-existent) Dock surface.

### Added
- ImpactTier enum (Tap/Light/Medium/Firm/Hard) with tier display in menu footer
- DetectionConfig struct for atomic configuration of detection parameters
- AudioResponder and VisualResponder protocols for dependency injection
- SensorID type-safe identifier newtype
- NSScreen.displayID extension replacing duplicated NSDeviceDescriptionKey usage
- AccordionCard and SettingHeader reusable UI components
- Per-setting SF Symbol icons with tappable inline help
- Collapsible Device Settings and Sensitivity Settings panels
- Crest factor gate with background RMS tracking (1-second EMA)
- Privacy policy (PRIVACY.md) and PrivacyInfo.xcprivacy manifest
- App Sandbox with device.usb entitlement for Mac App Store
- All user-facing strings wrapped in NSLocalizedString

### Changed
- Accelerometer: IOHIDEventSystemClient activation + IOHIDManager reading (public IOKit API)
- Detection: 4-algorithm voting → 6-gate pipeline (bandpass, spike, rise rate, crest factor, confirmations, rearm)
- Sensitivity renamed to Reactivity with inverted mapping (higher = more reactive)
- Assets loaded from sounds/ and faces/ folders recursively by extension
- Sound selection: pre-sorted by duration at startup, intensity maps to clip length
- ImpactController split into detect() → respond() with DetectedImpact struct
- Debounce merged with rearm into single Cooldown control
- SensorFusionEngine renamed to ImpactFusionEngine
- Frequency band: configurable bandpass (HP 20Hz + LP 25Hz default)
- All detection parameters exposed as user-configurable advanced settings
- Entitlements consolidated to single file (Yamete.entitlements)
- Makefile: hardened runtime, proper process kill cycle, release target cleanup
- Config push to detection engine cached (only on change, not per sample)
- HID thread init: DispatchSemaphore replaces spinlock
- LogStore graceful fallback when Application Support unavailable

### Fixed
- MainActor isolation on ImpactFusionEngine (was unconfined)
- ScreenFlash hide Task missing MainActor (AppKit thread safety)
- Updater Tasks missing MainActor (state mutation isolation)
- EventContext use-after-free on stream termination
- Force unwraps in AccelerometerReader run loop and mode resolution
- Rise rate gate checking instantaneous value instead of window peak
- Settings schema reset wiping all user preferences on every default change

### Removed
- ImpactDetector, SignalDetectors, DetectorConfig (replaced by ImpactFusionEngine)
- Self-updater (updates via Mac App Store)
- Licensing infrastructure (LicenseManager, LicenseStore, trial period)
- Schema version reset mechanism (UserDefaults.register handles new keys)
- Yamete-hardened.entitlements (consolidated into Yamete.entitlements)
- Prefix-based asset naming requirement (sound_*, face_*)
- Unused SliderRow view component and AudioDeviceUID type]]></description>
    </item>
    <item>
      <title>Yamete 0.0.0</title>
      <link>https://studnicky.github.io/yamete/CHANGELOG#000</link>
      <guid isPermaLink="false">https://studnicky.github.io/yamete/changelog/0.0.0</guid>
      <pubDate>Thu, 02 Apr 2026 12:00:00 GMT</pubDate>
      <description><![CDATA[### Added
- Impact detection via BMI286 accelerometer on Apple Silicon Macs
- Menu bar UI with branded pink theme
- Audio and visual response to impacts
- Dual-sink logging with 24-hour retention
- Build via Makefile with swiftc
- MIT license

[Unreleased]: https://github.com/Studnicky/yamete/compare/v0.0.0...HEAD
[0.0.0]: https://github.com/Studnicky/yamete/releases/tag/v0.0.0]]></description>
    </item>
  </channel>
</rss>
