Skip to content

Commit d2c7768

Browse files
committed
fix(meal): reject omitted rate-only insulin delivery
1 parent bdf7b07 commit d2c7768

6 files changed

Lines changed: 110 additions & 1 deletion

File tree

‎AI_NOTES.md‎

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,20 @@
1+
# Meal Research Interval Inventory - 2026-10-23
2+
3+
- Main bdf7b070 has all three successful Verify jobs in run 39918968049.
4+
- Four failing-first tests reproduced omitted rate-only basal, missing fields
5+
on delivery-only types, old/duplicate intervals and a falsely evaluated meal
6+
assessment. Original 52-test run: four expected failures, no other failures.
7+
- Admit such records to the inventory audit, not the model: unsupported interval
8+
exposure blocks assessment and direct simulation/forward projection. Preserve
9+
unrelated food/exercise duration and explicit null-field behavior. No physical
10+
delivery, upstream completeness or safe meal-wait interval is inferred.
11+
- All 124 focused tests passed. Frozen full test/lint/compile/assemble passed
12+
in 8m15s: 6805 passed, six existing skips, zero errors; real stock SDK TLS
13+
took 2.446s. Lint: zero errors, 382 existing warnings/eight hints. All 1431
14+
source hashes stayed unchanged and all 7176 application classes were retained
15+
in the 40,670,992-byte local APK. Exact-source Verify remains a separate gate.
16+
- No AAPS, schema, phone, server, dose, target, alarm or live countdown change.
17+
118
# Insulin History Release 0.1.12 - 2026-10-23
219

320
- PR27 passed all six exact-source checks and automatic review. Main merge

‎android-app/app/src/main/kotlin/io/aaps/copilot/domain/predict/HybridPredictionEngine.kt‎

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1560,11 +1560,14 @@ class HybridPredictionEngine(
15601560
return type.contains("bolus") || type.contains("correction") || type == "insulin"
15611561
}
15621562

1563+
// Rate-only intervals and pump state records cannot establish absence of insulin.
15631564
private fun mealRecordMayContainInsulin(event: TherapyEvent): Boolean = eventCanCarryInsulin(event) ||
1565+
normalize(event.type).replace("_", "") in setOf("basal", "tempbasal", "temporarybasal",
1566+
"basalrate", "extendedbolus", "combobolus", "pumpsuspend", "pumpresume") ||
15641567
event.payload.any { (key, value) ->
15651568
when (key.lowercase(Locale.US).filter(Char::isLetterOrDigit)) {
15661569
"bolusunits", "insulinunits", "insulin", "enteredinsulin" -> true
1567-
"extendedemulated" -> value.trim() != "null"
1570+
"extendedemulated", "rate", "absolute", "percent", "relative" -> value.trim() != "null"
15681571
"isbasalinsulin", "isemulatingtempbasal" -> value.trim().uppercase(Locale.US) !in setOf("FALSE", "0")
15691572
else -> false
15701573
}

‎android-app/app/src/test/kotlin/io/aaps/copilot/domain/meal/MealTimingResearchAssessmentTest.kt‎

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -59,6 +59,31 @@ class MealTimingResearchAssessmentTest {
5959
}
6060
}
6161

62+
@Test fun rateOnlyDeliveryBlocksResearchAssessmentAndBothDirectSimulationPaths() = runBlocking {
63+
for ((type, fields) in listOf("temp_basal" to mapOf("rate" to "1", "duration" to "30"),
64+
"treatment" to mapOf("percent" to "-100", "duration" to "30"),
65+
"pump_suspend" to emptyMap())) {
66+
val context = context(extraEvents = listOf(TherapyEvent(now - 60_000L, type, fields,
67+
sourceRowId = "interval-delivery")))
68+
val result = MealTimingResearchEvaluator().evaluate(context, belief(), episode, telemetry(), 180)
69+
assertEquals(MealTimingResearchStatus.INSULIN_INVENTORY_UNAVAILABLE, result.status)
70+
assertNull(result.batch)
71+
assertTrue(result.noFoodPaths.isEmpty())
72+
assertNull(result.recommendedStartAtMs)
73+
assertFalse(result.notificationAllowed)
74+
assertFalse(result.therapyAllowed)
75+
try {
76+
MealScenarioSimulator().simulate(context, belief(), 180)
77+
fail("Unaccounted interval delivery returned a scenario batch")
78+
} catch (error: IllegalArgumentException) {
79+
assertEquals("Meal research insulin inventory is unsupported", error.message)
80+
}
81+
val food = MealAbsorptionProjection.project(now, now, 35.0, MealAbsorptionProfile.MIXED, 120, 180)
82+
assertThrows(IllegalArgumentException::class.java) { context.forwardForecast("12", food, 180) }
83+
assertEquals(context.baselineForecasts, context.newEngine().predict(context.glucose, context.therapy))
84+
}
85+
}
86+
6287
@Test fun hiddenOrNonInstantInsulinStopsAssessmentWithoutChangingLiveForecast() = runBlocking {
6388
for ((type, fields) in listOf("treatment" to mapOf("insulin" to "1"),
6489
"bolus" to mapOf("units" to "1", "duration" to "60"),

‎android-app/app/src/test/kotlin/io/aaps/copilot/domain/predict/MealInsulinProjectionTest.kt‎

Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,47 @@ import org.junit.Assert.*
77
import org.junit.Test
88

99
class MealInsulinProjectionTest {
10+
@Test fun intervalDeliveryWithoutDoseUnitsCannotDisappearFromInventory() {
11+
for (type in listOf("temp_basal", "treatment", "carbs", "unknown")) {
12+
for (field in listOf("rate", "absolute", "percent", "relative")) {
13+
for (value in listOf("0", "1.5", "-100", "NaN", "unknown")) {
14+
val event = TherapyEvent(now - 60_000L, type,
15+
mapOf(field to value, "duration" to "30"), sourceRowId = "interval-1")
16+
val result = engine().projectMealTimingInsulin(glucose, listOf(event), 60)
17+
assertEquals("$type/$field/$value", 1, result.unsupportedEventCount)
18+
assertTrue(result.unsupportedReasons.contains(MealInsulinInventoryIssue.NON_INSTANT_OR_AMBIGUOUS_DELIVERY))
19+
assertThrows(IllegalArgumentException::class.java) { result.requireUsableInventory() }
20+
}
21+
}
22+
}
23+
}
24+
25+
@Test fun deliveryOnlyTypesRequireEvidenceEvenWhenFieldsAreMissing() {
26+
for (type in listOf("basal", "Temp Basal", "tempBasal", "temporary_basal", "basal_rate",
27+
"Extended Bolus", "combo_bolus", "pump_suspend", "pump_resume")) {
28+
val event = TherapyEvent(now - 60_000L, type, emptyMap(), sourceRowId = "interval-1")
29+
assertEquals(type, 1, engine().projectMealTimingInsulin(glucose, listOf(event), 60).unsupportedEventCount)
30+
}
31+
}
32+
33+
@Test fun oldOrDuplicateRateOnlyRecordsRemainUnsupportedRatherThanExpiredOrAbsent() {
34+
val old = TherapyEvent(now - 48 * 60 * 60_000L, "treatment",
35+
mapOf("absolute" to "0", "duration" to "2880"), sourceRowId = "interval-1")
36+
val result = engine().projectMealTimingInsulin(glucose, listOf(old, old), 60)
37+
assertEquals(2, result.unsupportedEventCount)
38+
assertTrue(result.unsupportedReasons.contains(MealInsulinInventoryIssue.DUPLICATE_OR_EMPTY_IDENTITY))
39+
}
40+
41+
@Test fun nullIntervalFieldsAndOrdinaryFoodOrExerciseDurationDoNotInventInsulin() {
42+
for (type in listOf("carbs", "exercise", "note")) {
43+
val event = TherapyEvent(now, type, mapOf("duration" to "30", "units" to "g",
44+
"rate" to "null", "absolute" to "null", "percent" to "null", "relative" to "null"))
45+
val result = engine().projectMealTimingInsulin(glucose, listOf(event), 60)
46+
assertEquals(0, result.unsupportedEventCount)
47+
assertEquals(0, result.modeledEventCount)
48+
}
49+
}
50+
1051
@Test fun insulinPayloadCannotDisappearBehindAnUnrecognizedEventType() {
1152
for (type in listOf("treatment", "meal", "note", "unknown")) {
1253
val event = bolus.copy(type = type, payload = mapOf("insulin" to "2"))

‎docs/MEAL_TIMING_EVIDENCE.md‎

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -51,6 +51,17 @@ worker, server route, database schema, model execution or therapy writer is adde
5151

5252
## Requirements For A Recommendation
5353

54+
The research inventory must inspect interval evidence even without a dose-units
55+
field. Rate-only absolute/percentage basal and explicit basal/suspend/resume
56+
records cannot disappear from the bolus-only forecast. Unrecognized forms,
57+
zero-rate suspension, malformed rates and old intervals with unknown support
58+
remain unsupported. No duration or delivery is invented from those fields.
59+
Assessment, direct simulation and direct forward projection all refuse such an
60+
inventory, preserving the separate live prediction. Ordinary food/exercise
61+
duration and null interval fields do not create insulin records.
62+
This refusal closes an omission, not source/pump completeness: independent
63+
upstream coverage and clinical validation below remain necessary.
64+
5465
1. Capture explicit meal intent, actual meal start and confirmed bolus delivery
5566
linkage separately. An entered timestamp, positive IOB, SMB or correction
5667
bolus must not silently become proof of a planned meal.

‎docs/PLAN.md‎

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,17 @@
11
# PLAN
22

3+
## Meal Research Interval Inventory (2026-10-23)
4+
5+
- [x] Confirm main bdf7b070 CI39918968049 passed all three jobs.
6+
- [x] Reproduce four rate-only/missing-form/old-record and assessment omissions.
7+
- [x] Audit interval fields and delivery-only event types without inventing doses.
8+
- [x] All 124 focused tests passed with no failures or skips.
9+
- [x] Frozen full Android quality: 6805 passed/six existing skips, real SDK TLS,
10+
zero lint errors and preserved application classes; no source drift.
11+
- [ ] Publish reviewed source and verify exact-head CI before integration.
12+
- No source/pump completeness or meal recommendation admission follows from
13+
this guard. Independent coverage and prospective validation remain required.
14+
315
## Insulin History Release 0.1.12 (2026-10-23)
416

517
- [x] PR27 passed all six checks and review; merge c2cc714c has the identical

0 commit comments

Comments
 (0)