Преглед изворни кода

Test: MedtrumKit message classification catalog

Augment the MedtrumKit emission suite with a complete (identifier, message) ->
TrioAlertClassifier.categorize table sourced from the loopkit-manager-synthesis
audit: every reported alert/error string paired with its identifier, asserting
the current classifier category (green) plus a coverage-gap ratchet for
messages whose taxonomy implies a bucket the substring classifier misses.
Spec note: production feeds the classifier String(describing: error) (case
names), not these display strings.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
James Woglom пре 2 недеља
родитељ
комит
64cd310fa8
1 измењених фајлова са 728 додато и 0 уклоњено
  1. 728 0
      TrioTests/MedtrumKitAlertEmissionTests.swift

+ 728 - 0
TrioTests/MedtrumKitAlertEmissionTests.swift

@@ -153,3 +153,731 @@ import Testing
         #expect(Self.knownEscalationGaps == auditedGaps)
     }
 }
+
+/// SPEC — Message-text classification catalog for **MedtrumKit**
+/// (`managers`/`pump`/`MedtrumKit`).
+///
+/// MedtrumKit does NOT use the LoopKit `Alert`/`AlertIssuer` system at all:
+/// there are zero `issueAlert`/`Alert(identifier:)` calls. Every pump alarm
+/// surfaces via `UNUserNotificationCenter` local notifications, LoopKit
+/// status surfaces (`pumpStatusHighlight`, `didError`,
+/// `completion(.failure)`), and in-app SwiftUI text / published
+/// `errorMessage` fields. So there are no real LoopKit `alertIdentifier`s to
+/// attach — the `identifier` column below is the error enum case name /
+/// source symbol per the synthesis instructions.
+///
+/// IMPORTANT: in production the classifier is fed `String(describing: error)`
+/// (i.e. the error *case name*, e.g. `"uncertainDelivery"`), NOT these
+/// user-facing display strings. This suite instead feeds each emission's
+/// EXACT display string through a `StubError` so we can pin how
+/// `TrioAlertClassifier`'s substring matcher handles the real natural-language
+/// text a user would see. Because the classifier matches compressed/
+/// concatenated tokens ("occlusion", "reservoirempty", "lowreservoir",
+/// "fault", "timeout", …) while the Medtrum strings are prose, almost every
+/// meaningful alarm string falls through to `.other`. Only a handful match:
+/// occlusion via PatchState "Occlusion", fault via "Fault"/"in Fault state",
+/// and the BLE "timeout" strings. Notably the occlusion notification body is
+/// misspelled "occlussion" (double-s), which does NOT contain "occlusion" and
+/// thus mis-routes to `.other`.
+@Suite("Message Classification: MedtrumKit") struct MedtrumKitMessageClassificationTests {
+    /// `String(describing:)` returns `description` verbatim, reproducing the
+    /// exact text the classifier's substring matcher sees.
+    private struct StubError: Error, CustomStringConvertible {
+        let description: String
+    }
+
+    struct Row {
+        let identifier: String
+        let message: String
+        let role: String
+        let taxonomy: String
+        let expected: TrioAlertCategory
+    }
+
+    /// Every distinct user-facing emission, paired with the classifier
+    /// category it currently resolves to. `currentCategory == "other"` pins
+    /// `.other(message)` with the EXACT original (non-lowercased) string.
+    static let rows: [Row] = [
+        // MedtrumKit/PumpManager/NotificationManager.swift:15
+        Row(
+            identifier: "NotificationManager.Identifiers.patchExpiredNotification",
+            message: "Your patch will expire soon! / Your patch has %lld hours left",
+            role: "notificationBody",
+            taxonomy: "F3",
+            expected: .other("Your patch will expire soon! / Your patch has %lld hours left")
+        ),
+        // MedtrumKit/PumpManager/NotificationManager.swift:38
+        Row(
+            identifier: "NotificationManager.Identifiers.patchDailyMaxNotification",
+            message: "Insulin has been suspended! / Your patch has reached its daily maximum!",
+            role: "notificationBody",
+            taxonomy: "N2",
+            expected: .other("Insulin has been suspended! / Your patch has reached its daily maximum!")
+        ),
+        // MedtrumKit/PumpManager/NotificationManager.swift:48
+        Row(
+            identifier: "NotificationManager.Identifiers.patchHourlyMaxNotification",
+            message: "Insulin has been suspended! / Your patch has reached its hourly maximum!",
+            role: "notificationBody",
+            taxonomy: "N2",
+            expected: .other("Insulin has been suspended! / Your patch has reached its hourly maximum!")
+        ),
+        // MedtrumKit/PumpManager/NotificationManager.swift:61
+        // Misspelled "occlussion" (double-s) does NOT contain "occlusion" -> .other.
+        Row(
+            identifier: "NotificationManager.Identifiers.occlusionNotification",
+            message: "Replace your patch now! / Your patch has detected an occlussion!",
+            role: "notificationBody",
+            taxonomy: "N1",
+            expected: .other("Replace your patch now! / Your patch has detected an occlussion!")
+        ),
+        // MedtrumKit/PumpManager/NotificationManager.swift:71
+        // Contains "Fault" -> .hardwareFault.
+        Row(
+            identifier: "NotificationManager.Identifiers.patchFaultNotification",
+            message: "Replace your patch now! / Your patch is in Fault state!",
+            role: "notificationBody",
+            taxonomy: "N1",
+            expected: .hardwareFault
+        ),
+        // MedtrumKit/PumpManager/NotificationManager.swift:81
+        Row(
+            identifier: "NotificationManager.Identifiers.reservoirEmptyNotification",
+            message: "Replace your patch now! / Your patch is out of insulin!",
+            role: "notificationBody",
+            taxonomy: "N4",
+            expected: .other("Replace your patch now! / Your patch is out of insulin!")
+        ),
+        // MedtrumKit/PumpManager/NotificationManager.swift:91
+        Row(
+            identifier: "NotificationManager.Identifiers.reservoirEmptyNotification",
+            message: "Reservoir low (%lld U) / Your patch is running out of insulin!",
+            role: "notificationBody",
+            taxonomy: "F1",
+            expected: .other("Reservoir low (%lld U) / Your patch is running out of insulin!")
+        ),
+        // MedtrumKitUI/MedtrumKitPumpManager+UI.swift:76
+        Row(
+            identifier: "PumpStatusHighlight",
+            message: "No patch",
+            role: "errorMessage",
+            taxonomy: "N14",
+            expected: .other("No patch")
+        ),
+        // MedtrumKitUI/MedtrumKitPumpManager+UI.swift:85
+        Row(
+            identifier: "PumpStatusHighlight",
+            message: "No Insulin",
+            role: "errorMessage",
+            taxonomy: "N4",
+            expected: .other("No Insulin")
+        ),
+        // MedtrumKitUI/MedtrumKitPumpManager+UI.swift:91
+        Row(
+            identifier: "PumpStatusHighlight",
+            message: "Insulin Suspended",
+            role: "errorMessage",
+            taxonomy: "N2",
+            expected: .other("Insulin Suspended")
+        ),
+        // MedtrumKitUI/MedtrumKitPumpManager+UI.swift:100
+        Row(
+            identifier: "PumpStatusHighlight",
+            message: "Patch expired. Basal only.",
+            role: "errorMessage",
+            taxonomy: "N6",
+            expected: .other("Patch expired. Basal only.")
+        ),
+        // MedtrumKitUI/MedtrumKitPumpManager+UI.swift:109
+        Row(
+            identifier: "PumpStatusHighlight",
+            message: "Signal Loss",
+            role: "errorMessage",
+            taxonomy: "N8",
+            expected: .other("Signal Loss")
+        ),
+        // MedtrumKitUI/MedtrumKitPumpManager+UI.swift:118
+        Row(
+            identifier: "PumpStatusHighlight",
+            message: "Patch Error",
+            role: "errorMessage",
+            taxonomy: "N1",
+            expected: .other("Patch Error")
+        ),
+        // MedtrumKit/PumpManager/PeripheralManager.swift:126
+        Row(
+            identifier: "MedtrumConnectError.failedToCompleteAuthorizationFlow",
+            message: "invalid response",
+            role: "errorMessage",
+            taxonomy: "N10",
+            expected: .other("invalid response")
+        ),
+        // MedtrumKit/PumpManager/PeripheralManager.swift:209
+        Row(
+            identifier: "MedtrumConnectError.failedToDiscoverServices",
+            message: "No Medtrum service found - <discovered service UUIDs>",
+            role: "errorMessage",
+            taxonomy: "N8",
+            expected: .other("No Medtrum service found - <discovered service UUIDs>")
+        ),
+        // MedtrumKit/PumpManager/PeripheralManager.swift:231
+        Row(
+            identifier: "MedtrumConnectError.failedToDiscoverCharacteristics",
+            message: "Failed to discover read, write or config characteristic - <UUIDs>",
+            role: "errorMessage",
+            taxonomy: "N8",
+            expected: .other("Failed to discover read, write or config characteristic - <UUIDs>")
+        ),
+        // MedtrumKit/PumpManager/BluetoothManager.swift:76,162,333
+        // Contains "Timeout" -> .commsTransient.
+        Row(
+            identifier: "MedtrumConnectError.failedToConnectToDevice",
+            message: "Failed to connect to patch -> Timeout reached",
+            role: "errorMessage",
+            taxonomy: "N8",
+            expected: .commsTransient
+        ),
+        // MedtrumKit/PumpManager/BluetoothManager.swift:114,128; MedtrumPumpManager.swift:1009
+        Row(
+            identifier: "MedtrumConnectError.failedToFindDevice",
+            message: "Failed to connect to patch",
+            role: "errorMessage",
+            taxonomy: "N8",
+            expected: .other("Failed to connect to patch")
+        ),
+        // MedtrumKit/PumpManager/BluetoothManager.swift:45
+        Row(
+            identifier: "MedtrumScanError.invalidBluetoothState",
+            message: "Invalid Bluetooth state: <state>",
+            role: "errorMessage",
+            taxonomy: "N8",
+            expected: .other("Invalid Bluetooth state: <state>")
+        ),
+        // MedtrumKit/PumpManager/PeripheralManager.swift:39,71
+        Row(
+            identifier: "MedtrumWriteError.noData",
+            message: "No data",
+            role: "errorMessage",
+            taxonomy: "N8",
+            expected: .other("No data")
+        ),
+        // MedtrumKit/PumpManager/PeripheralManager.swift:58
+        Row(
+            identifier: "MedtrumWriteError.noWriteCharacteristic",
+            message: "No write characteristic. Device might be disconnected",
+            role: "errorMessage",
+            taxonomy: "N8",
+            expected: .other("No write characteristic. Device might be disconnected")
+        ),
+        // MedtrumKit/PumpManager/PeripheralManager.swift:99
+        // Contains "timeout" -> .commsTransient.
+        Row(
+            identifier: "MedtrumWriteError.timeout",
+            message: "A command timeout is hit",
+            role: "errorMessage",
+            taxonomy: "N8",
+            expected: .commsTransient
+        ),
+        // MedtrumKit/PumpManager/PeripheralManager.swift:314,321
+        Row(
+            identifier: "MedtrumWriteError.invalidData",
+            message: "Invalid data received",
+            role: "errorMessage",
+            taxonomy: "N8",
+            expected: .other("Invalid data received")
+        ),
+        // MedtrumKit/PumpManager/BluetoothManager.swift:170
+        Row(
+            identifier: "MedtrumWriteError.noManager",
+            message: "No peripheral manager",
+            role: "errorMessage",
+            taxonomy: "N8",
+            expected: .other("No peripheral manager")
+        ),
+        // MedtrumKit/PumpManager/MedtrumPumpManager.swift:992
+        // "uncertain delivery" (with a space) != "uncertaindelivery" -> .other.
+        Row(
+            identifier: "PumpManagerError.uncertainDelivery",
+            message: "uncertain delivery",
+            role: "errorMessage",
+            taxonomy: "N3",
+            expected: .other("uncertain delivery")
+        ),
+        // MedtrumKit/PumpManager/MedtrumPumpManager.swift:281,405
+        Row(
+            identifier: "MedtrumConnectError.isBolussing",
+            message: "Bolus issue. Patch is already bolussing",
+            role: "errorMessage",
+            taxonomy: "N9",
+            expected: .other("Bolus issue. Patch is already bolussing")
+        ),
+        // MedtrumKit/PumpManager/MedtrumPumpManager.swift:287
+        Row(
+            identifier: "MedtrumConnectError.isSuspended",
+            message: "Bolus issue. Patch is suspended. Resume delivery",
+            role: "errorMessage",
+            taxonomy: "N9",
+            expected: .other("Bolus issue. Patch is suspended. Resume delivery")
+        ),
+        // MedtrumKit/PumpManager/PeripheralManager.swift:53
+        Row(
+            identifier: "MedtrumWriteError.alreadyRunning",
+            message: "A command is already running",
+            role: "errorMessage",
+            taxonomy: "N9",
+            expected: .other("A command is already running")
+        ),
+        // MedtrumKit/PumpManager/PeripheralManager.swift:311
+        Row(
+            identifier: "MedtrumWriteError.invalidResponse",
+            message: "Invalid response code: <code>",
+            role: "errorMessage",
+            taxonomy: "N9",
+            expected: .other("Invalid response code: <code>")
+        ),
+        // MedtrumKit/PumpManager/MedtrumPumpManager.swift:630
+        Row(
+            identifier: "MedtrumPrimePatchError.noKnownPumpBase",
+            message: "No known pump base found.",
+            role: "errorMessage",
+            taxonomy: "N12",
+            expected: .other("No known pump base found.")
+        ),
+        // MedtrumKit/PumpManager/MedtrumPumpManager.swift:645
+        Row(
+            identifier: "MedtrumPrimePatchError.connectionFailure",
+            message: "Failed to connect to pump base: <reason>",
+            role: "errorMessage",
+            taxonomy: "N12",
+            expected: .other("Failed to connect to pump base: <reason>")
+        ),
+        // MedtrumKit/PumpManager/MedtrumPumpManager.swift:659
+        Row(
+            identifier: "MedtrumPrimePatchError.unknownError",
+            message: "Unknown error: <write error>",
+            role: "errorMessage",
+            taxonomy: "N12",
+            expected: .other("Unknown error: <write error>")
+        ),
+        // MedtrumKit/PumpManager/MedtrumPumpManager.swift:674
+        Row(
+            identifier: "MedtrumActivatePatchError.connectionFailure",
+            message: "Connection failure: <reason>",
+            role: "errorMessage",
+            taxonomy: "N12",
+            expected: .other("Connection failure: <reason>")
+        ),
+        // MedtrumKit/PumpManager/MedtrumPumpManager.swift:698
+        Row(
+            identifier: "MedtrumActivatePatchError.unknownError",
+            message: "Unknown error: <error description>",
+            role: "errorMessage",
+            taxonomy: "N12",
+            expected: .other("Unknown error: <error description>")
+        ),
+        // MedtrumKit/PumpManager/MedtrumPumpManager.swift:704
+        Row(
+            identifier: "MedtrumActivatePatchError.unknownError",
+            message: "Unknown error: Failed to parse response...",
+            role: "errorMessage",
+            taxonomy: "N12",
+            expected: .other("Unknown error: Failed to parse response...")
+        ),
+        // MedtrumKit/PumpManager/BluetoothManager.swift:41
+        Row(
+            identifier: "MedtrumScanError.noSerialNumberAvailable",
+            message: "No Serial number setup. Please complete activation flow...",
+            role: "errorMessage",
+            taxonomy: "N12",
+            expected: .other("No Serial number setup. Please complete activation flow...")
+        ),
+        // MedtrumKitUI/ViewModels/Onboarding/PumpBaseSettingsViewModel.swift:44
+        Row(
+            identifier: "PumpBaseSettingsViewModel.errorMessage",
+            message: "No pump manager available",
+            role: "validation",
+            taxonomy: "N12",
+            expected: .other("No pump manager available")
+        ),
+        // MedtrumKitUI/ViewModels/Settings/PatchSettingsViewModel.swift:79; DeactivatePatchViewModel.swift:23,40,77
+        Row(
+            identifier: "PatchSettingsViewModel.errorMessage",
+            message: "Authentication failure",
+            role: "errorMessage",
+            taxonomy: "N10",
+            expected: .other("Authentication failure")
+        ),
+        // MedtrumKitUI/AuthorizeBiometrics.swift:11-13
+        Row(
+            identifier: "AuthorizeBiometrics.evaluatePolicyReason",
+            message: "We need to unlock your data.",
+            role: "notificationBody",
+            taxonomy: "N10",
+            expected: .other("We need to unlock your data.")
+        ),
+        // MedtrumKit/PumpManager/MedtrumPumpManager.swift:585
+        Row(
+            identifier: "NSError.syncBasalRateSchedule",
+            message: "Basal schedule is empty...",
+            role: "validation",
+            taxonomy: "N13",
+            expected: .other("Basal schedule is empty...")
+        ),
+        // MedtrumKitUI/ViewModels/Onboarding/PumpBaseSettingsViewModel.swift:32
+        Row(
+            identifier: "PumpBaseSettingsViewModel.errorMessage",
+            message: "Serial Number is too short",
+            role: "validation",
+            taxonomy: "N13",
+            expected: .other("Serial Number is too short")
+        ),
+        // MedtrumKitUI/ViewModels/Onboarding/PumpBaseSettingsViewModel.swift:38
+        Row(
+            identifier: "PumpBaseSettingsViewModel.errorMessage",
+            message: "Serial Number is invalid hex format",
+            role: "validation",
+            taxonomy: "N13",
+            expected: .other("Serial Number is invalid hex format")
+        ),
+        // MedtrumKitUI/ViewModels/Onboarding/PumpBaseSettingsViewModel.swift:55
+        Row(
+            identifier: "PumpBaseSettingsViewModel.errorMessage",
+            message: "Incorrect serial number received",
+            role: "validation",
+            taxonomy: "N13",
+            expected: .other("Incorrect serial number received")
+        ),
+        // MedtrumKit/Packets/Enums/PatchState.swift:73
+        Row(
+            identifier: "PatchState.noCalibration",
+            message: "No Calibration",
+            role: "errorMessage",
+            taxonomy: "N11",
+            expected: .other("No Calibration")
+        ),
+        // MedtrumKit/Packets/Enums/PatchState.swift:31
+        Row(
+            identifier: "PatchState.none",
+            message: "None",
+            role: "errorMessage",
+            taxonomy: "N14",
+            expected: .other("None")
+        ),
+        // MedtrumKit/Packets/Enums/PatchState.swift:33
+        Row(
+            identifier: "PatchState.idle",
+            message: "Idle",
+            role: "errorMessage",
+            taxonomy: "N14",
+            expected: .other("Idle")
+        ),
+        // MedtrumKit/Packets/Enums/PatchState.swift:35
+        Row(
+            identifier: "PatchState.filled",
+            message: "Filled",
+            role: "errorMessage",
+            taxonomy: "N14",
+            expected: .other("Filled")
+        ),
+        // MedtrumKit/Packets/Enums/PatchState.swift:37
+        Row(
+            identifier: "PatchState.priming",
+            message: "Priming",
+            role: "errorMessage",
+            taxonomy: "N14",
+            expected: .other("Priming")
+        ),
+        // MedtrumKit/Packets/Enums/PatchState.swift:39
+        Row(
+            identifier: "PatchState.primed",
+            message: "Primed",
+            role: "errorMessage",
+            taxonomy: "N14",
+            expected: .other("Primed")
+        ),
+        // MedtrumKit/Packets/Enums/PatchState.swift:41
+        Row(
+            identifier: "PatchState.ejecting",
+            message: "Ejecting",
+            role: "errorMessage",
+            taxonomy: "N14",
+            expected: .other("Ejecting")
+        ),
+        // MedtrumKit/Packets/Enums/PatchState.swift:43
+        Row(
+            identifier: "PatchState.ejected",
+            message: "Ejected",
+            role: "errorMessage",
+            taxonomy: "N14",
+            expected: .other("Ejected")
+        ),
+        // MedtrumKit/Packets/Enums/PatchState.swift:46
+        Row(
+            identifier: "PatchState.active",
+            message: "Active",
+            role: "errorMessage",
+            taxonomy: "N14",
+            expected: .other("Active")
+        ),
+        // MedtrumKit/Packets/Enums/PatchState.swift:49
+        Row(
+            identifier: "PatchState.lowBgSuspended",
+            message: "Suspended - Low BG",
+            role: "errorMessage",
+            taxonomy: "N2",
+            expected: .other("Suspended - Low BG")
+        ),
+        // MedtrumKit/Packets/Enums/PatchState.swift:51
+        Row(
+            identifier: "PatchState.autoSuspended",
+            message: "Suspended - Auto",
+            role: "errorMessage",
+            taxonomy: "N2",
+            expected: .other("Suspended - Auto")
+        ),
+        // MedtrumKit/Packets/Enums/PatchState.swift:53
+        Row(
+            identifier: "PatchState.hourlyMaxSuspended",
+            message: "Suspended - Hourly Max",
+            role: "errorMessage",
+            taxonomy: "N2",
+            expected: .other("Suspended - Hourly Max")
+        ),
+        // MedtrumKit/Packets/Enums/PatchState.swift:55
+        Row(
+            identifier: "PatchState.dailyMaxSuspended",
+            message: "Suspended - Daily Max",
+            role: "errorMessage",
+            taxonomy: "N2",
+            expected: .other("Suspended - Daily Max")
+        ),
+        // MedtrumKit/Packets/Enums/PatchState.swift:57
+        Row(
+            identifier: "PatchState.suspended",
+            message: "Suspended",
+            role: "errorMessage",
+            taxonomy: "N2",
+            expected: .other("Suspended")
+        ),
+        // MedtrumKit/Packets/Enums/PatchState.swift:59
+        Row(
+            identifier: "PatchState.paused",
+            message: "Paused",
+            role: "errorMessage",
+            taxonomy: "N2",
+            expected: .other("Paused")
+        ),
+        // MedtrumKit/Packets/Enums/PatchState.swift:61
+        // Contains "Occlusion" -> .occlusion.
+        Row(
+            identifier: "PatchState.occlusion",
+            message: "Occlusion",
+            role: "errorMessage",
+            taxonomy: "N1",
+            expected: .occlusion
+        ),
+        // MedtrumKit/Packets/Enums/PatchState.swift:63
+        Row(
+            identifier: "PatchState.expired",
+            message: "Expired",
+            role: "errorMessage",
+            taxonomy: "N6",
+            expected: .other("Expired")
+        ),
+        // MedtrumKit/Packets/Enums/PatchState.swift:65
+        Row(
+            identifier: "PatchState.reservoirEmpty",
+            message: "Reservoir Empty",
+            role: "errorMessage",
+            taxonomy: "N4",
+            expected: .other("Reservoir Empty")
+        ),
+        // MedtrumKit/Packets/Enums/PatchState.swift:69
+        // Contains "Fault" -> .hardwareFault.
+        Row(
+            identifier: "PatchState.baseFault",
+            message: "Fault",
+            role: "errorMessage",
+            taxonomy: "N1",
+            expected: .hardwareFault
+        ),
+        // MedtrumKit/Packets/Enums/PatchState.swift:71
+        Row(
+            identifier: "PatchState.batteryOut",
+            message: "Battery Empty",
+            role: "errorMessage",
+            taxonomy: "N5",
+            expected: .other("Battery Empty")
+        ),
+        // MedtrumKit/Packets/Enums/PatchState.swift:75
+        Row(
+            identifier: "PatchState.stopped",
+            message: "Stopped",
+            role: "errorMessage",
+            taxonomy: "N14",
+            expected: .other("Stopped")
+        ),
+        // MedtrumKitUI/Views/Settings/MedtrumKitSettings.swift:85-91
+        Row(
+            identifier: "MedtrumKitSettings.gracePeriodBanner",
+            message: "Change your Patch now. Insulin delivery will stop in %@ or when no more insulin remains.",
+            role: "alertBody",
+            taxonomy: "F3",
+            expected: .other("Change your Patch now. Insulin delivery will stop in %@ or when no more insulin remains.")
+        ),
+        // MedtrumKitUI/Views/Settings/MedtrumKitSettings.swift:96-107
+        Row(
+            identifier: "MedtrumKitSettings.hourlyMaxBanner",
+            message: "Alert: Hourly max insulin / Patch is suspended. Limit of %lld U exceeded. If you increase the limit, you can clear the alert now. If you wait, patch will resume when enough time passes. / Clear alert",
+            role: "alertBody",
+            taxonomy: "N2",
+            expected: .other(
+                "Alert: Hourly max insulin / Patch is suspended. Limit of %lld U exceeded. If you increase the limit, you can clear the alert now. If you wait, patch will resume when enough time passes. / Clear alert"
+            )
+        ),
+        // MedtrumKitUI/Views/Settings/MedtrumKitSettings.swift:126-137
+        Row(
+            identifier: "MedtrumKitSettings.dailyMaxBanner",
+            message: "Alert: Daily max insulin / Patch is suspended. Limit of %lld U exceeded. If you increase the limit, you can clear the alert now. If you wait, patch will resume when enough time passes. / Clear alert",
+            role: "alertBody",
+            taxonomy: "N2",
+            expected: .other(
+                "Alert: Daily max insulin / Patch is suspended. Limit of %lld U exceeded. If you increase the limit, you can clear the alert now. If you wait, patch will resume when enough time passes. / Clear alert"
+            )
+        ),
+        // MedtrumKitUI/Views/Onboarding/PatchDeactivationView.swift:43-48
+        Row(
+            identifier: "PatchDeactivationView.confirmAlert",
+            message: "Are you sure? / It is recommended to deactivate first / Confirm",
+            role: "alertBody",
+            taxonomy: "N14",
+            expected: .other("Are you sure? / It is recommended to deactivate first / Confirm")
+        ),
+        // MedtrumKitUI/Views/DeleteDriverActionSheet.swift:3-19
+        Row(
+            identifier: "DeleteDriverActionSheet",
+            message: "Remove Pump / Are you sure you want to stop using Medtrum TouchCare Nano 200u/300u? / Delete Pump",
+            role: "alertBody",
+            taxonomy: "N14",
+            expected: .other("Remove Pump / Are you sure you want to stop using Medtrum TouchCare Nano 200u/300u? / Delete Pump")
+        ),
+        // MedtrumKitUI/Views/Settings/MedtrumKitSettings.swift:14-28
+        Row(
+            identifier: "MedtrumKitSettings.timeSyncActionSheet",
+            message: "Time Change Detected / The time on your pump is different from the current time. Do you want to update the time on your pump to the current time? / Yes, Sync to Current Time / No, Keep Pump As Is",
+            role: "alertBody",
+            taxonomy: "N14",
+            expected: .other(
+                "Time Change Detected / The time on your pump is different from the current time. Do you want to update the time on your pump to the current time? / Yes, Sync to Current Time / No, Keep Pump As Is"
+            )
+        ),
+        // MedtrumKitUI/Views/Settings/MedtrumKitSettings.swift:30-45
+        Row(
+            identifier: "MedtrumKitSettings.suspendPickerActionSheet",
+            message: "Suspend Insulin Delivery / How long you wish to suspend your patch maximum? It will resume automaticly after this time.",
+            role: "alertBody",
+            taxonomy: "N14",
+            expected: .other(
+                "Suspend Insulin Delivery / How long you wish to suspend your patch maximum? It will resume automaticly after this time."
+            )
+        ),
+        // MedtrumKitUI/Views/Settings/MedtrumKitSettings.swift:74-81
+        Row(
+            identifier: "MedtrumKitSettings.timeSyncBanner",
+            message: "Time Change Detected / The time on your pump is different from the current time. Your pump's time controls your scheduled therapy settings. Scroll down to Pump Time row...",
+            role: "alertBody",
+            taxonomy: "N14",
+            expected: .other(
+                "Time Change Detected / The time on your pump is different from the current time. Your pump's time controls your scheduled therapy settings. Scroll down to Pump Time row..."
+            )
+        )
+    ]
+
+    @Test("each (identifier, message) classifies as pinned", arguments: rows) func eachMessageClassifiesAsPinned(_ row: Row) {
+        #expect(TrioAlertClassifier.categorize(error: StubError(description: row.message)) == row.expected)
+    }
+
+    // MARK: - Classifier coverage gaps (ratchet)
+
+    /// `"identifier — message"` keys where the audit marks `isGap == true`:
+    /// the message currently falls through to `.other` but SHOULD route to a
+    /// meaningful bucket. This ratchet stays green today and FAILS when the
+    /// classifier is improved to catch the real prose — prompting an update.
+    ///
+    /// Per gap: bucket it SHOULD hit, and why the substring tokens miss it.
+    ///  - occlusionNotification: SHOULD .occlusion. Body is misspelled
+    ///    "occlussion" (double-s) — does NOT contain "occlusion"
+    ///    (NotificationManager.swift:61).
+    ///  - reservoirEmptyNotification ("...out of insulin!"): SHOULD
+    ///    .reservoirEmpty. Prose has no "reservoirempty"/"emptyreservoir"
+    ///    token (NotificationManager.swift:81).
+    ///  - reservoirEmptyNotification ("Reservoir low..."): SHOULD
+    ///    .reservoirLow. "Reservoir low" (with space) != "lowreservoir"
+    ///    (NotificationManager.swift:91).
+    ///  - PumpStatusHighlight "No Insulin": SHOULD .reservoirEmpty. No
+    ///    "reservoirempty" token (MedtrumKitPumpManager+UI.swift:85).
+    ///  - PumpStatusHighlight "Patch expired. Basal only.": SHOULD
+    ///    .deviceExpired. Only "podexpired"/"sensorexpired" tokens match,
+    ///    not "patch expired" (MedtrumKitPumpManager+UI.swift:100).
+    ///  - PumpStatusHighlight "Signal Loss": SHOULD .commsTransient. No
+    ///    "communication"/"timeout"/etc. token (MedtrumKitPumpManager+UI.swift:109).
+    ///  - PumpStatusHighlight "Patch Error": SHOULD .hardwareFault. "Error"
+    ///    is not "fault" (MedtrumKitPumpManager+UI.swift:118).
+    ///  - failedToDiscoverServices: SHOULD .commsTransient. No comms token
+    ///    in the prose (PeripheralManager.swift:209).
+    ///  - failedToDiscoverCharacteristics: SHOULD .commsTransient. No comms
+    ///    token (PeripheralManager.swift:231).
+    ///  - failedToFindDevice "Failed to connect to patch": SHOULD
+    ///    .commsTransient. No "timeout"/"notconnected" token; "connect" is
+    ///    not "notconnected" (BluetoothManager.swift:114,128).
+    ///  - invalidBluetoothState: SHOULD .commsTransient. No comms token
+    ///    (BluetoothManager.swift:45).
+    ///  - noData "No data": SHOULD .commsTransient. No comms token
+    ///    (PeripheralManager.swift:39,71).
+    ///  - noWriteCharacteristic: SHOULD .commsTransient. "disconnected" !=
+    ///    "notconnected" (PeripheralManager.swift:58).
+    ///  - invalidData "Invalid data received": SHOULD .commsTransient. No
+    ///    comms token (PeripheralManager.swift:314,321).
+    ///  - noManager "No peripheral manager": SHOULD .commsTransient. No
+    ///    comms token (BluetoothManager.swift:170).
+    ///  - uncertainDelivery "uncertain delivery": SHOULD .deliveryUncertain.
+    ///    "uncertain delivery" (with space) != "uncertaindelivery"
+    ///    (MedtrumPumpManager.swift:992).
+    static let classifierCoverageGaps: Set<String> = [
+        "NotificationManager.Identifiers.occlusionNotification — Replace your patch now! / Your patch has detected an occlussion!",
+        "NotificationManager.Identifiers.reservoirEmptyNotification — Replace your patch now! / Your patch is out of insulin!",
+        "NotificationManager.Identifiers.reservoirEmptyNotification — Reservoir low (%lld U) / Your patch is running out of insulin!",
+        "PumpStatusHighlight — No Insulin",
+        "PumpStatusHighlight — Patch expired. Basal only.",
+        "PumpStatusHighlight — Signal Loss",
+        "PumpStatusHighlight — Patch Error",
+        "MedtrumConnectError.failedToDiscoverServices — No Medtrum service found - <discovered service UUIDs>",
+        "MedtrumConnectError.failedToDiscoverCharacteristics — Failed to discover read, write or config characteristic - <UUIDs>",
+        "MedtrumConnectError.failedToFindDevice — Failed to connect to patch",
+        "MedtrumScanError.invalidBluetoothState — Invalid Bluetooth state: <state>",
+        "MedtrumWriteError.noData — No data",
+        "MedtrumWriteError.noWriteCharacteristic — No write characteristic. Device might be disconnected",
+        "MedtrumWriteError.invalidData — Invalid data received",
+        "MedtrumWriteError.noManager — No peripheral manager",
+        "PumpManagerError.uncertainDelivery — uncertain delivery"
+    ]
+
+    @Test("classifier coverage gaps are exactly as documented") func classifierCoverageGapsAreExactlyAsDocumented() {
+        // A coverage gap = a row that currently resolves to `.other(message)`
+        // even though the audit says it SHOULD route to a meaningful bucket
+        // (taxonomyBucket != "other"). The bucket-vs-other distinction lives
+        // in the audit, encoded here as `classifierCoverageGaps`. Recompute
+        // the falls-through-to-other set among gap-flagged rows and confirm
+        // it matches verbatim; this FAILS when the classifier is improved to
+        // catch the prose, forcing the documented set to be updated.
+        let fellThroughToOther = Set(
+            Self.rows
+                .filter { row in
+                    TrioAlertClassifier.categorize(error: StubError(description: row.message)) == .other(row.message)
+                }
+                .map { "\($0.identifier) — \($0.message)" }
+        )
+        let documentedGapsThatFellThrough = fellThroughToOther.intersection(Self.classifierCoverageGaps)
+        #expect(documentedGapsThatFellThrough == Self.classifierCoverageGaps)
+    }
+}