@ceralive/modem-control 0.2.0 → 1.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (464) hide show
  1. package/README.md +317 -0
  2. package/dist/backend/at-lease.d.ts +57 -0
  3. package/dist/backend/at-lease.js +109 -0
  4. package/dist/backend/cell-info.d.ts +46 -0
  5. package/dist/backend/cell-info.js +124 -0
  6. package/dist/backend/constants.d.ts +24 -0
  7. package/{src/backend/constants.ts → dist/backend/constants.js} +4 -9
  8. package/dist/backend/device-classifier.d.ts +46 -0
  9. package/dist/backend/device-classifier.js +258 -0
  10. package/dist/backend/enrichment.d.ts +28 -0
  11. package/dist/backend/enrichment.js +59 -0
  12. package/dist/backend/features.d.ts +61 -0
  13. package/dist/backend/features.js +109 -0
  14. package/dist/backend/identity-ladder.d.ts +57 -0
  15. package/dist/backend/identity-ladder.js +158 -0
  16. package/dist/backend/identity-registry.d.ts +51 -0
  17. package/dist/backend/identity-registry.js +101 -0
  18. package/dist/backend/index.d.ts +30 -0
  19. package/dist/backend/index.js +35 -0
  20. package/dist/backend/lifecycle-interlock.d.ts +25 -0
  21. package/dist/backend/lifecycle-interlock.js +18 -0
  22. package/dist/backend/managed-objects.d.ts +39 -0
  23. package/dist/backend/managed-objects.js +82 -0
  24. package/dist/backend/mapping.d.ts +11 -0
  25. package/dist/backend/mapping.js +148 -0
  26. package/dist/backend/mm-backend.d.ts +44 -0
  27. package/dist/backend/mm-backend.js +154 -0
  28. package/dist/backend/mm-location.d.ts +21 -0
  29. package/dist/backend/mm-location.js +237 -0
  30. package/dist/backend/mm-mutations.d.ts +42 -0
  31. package/dist/backend/mm-mutations.js +259 -0
  32. package/dist/backend/modem-actor.d.ts +41 -0
  33. package/dist/backend/modem-actor.js +78 -0
  34. package/dist/backend/nm-auto-apn.d.ts +54 -0
  35. package/dist/backend/nm-auto-apn.js +124 -0
  36. package/dist/backend/nm-gsm-fields.d.ts +23 -0
  37. package/dist/backend/nm-gsm-fields.js +107 -0
  38. package/dist/backend/nmcli-nm-port.d.ts +28 -0
  39. package/dist/backend/nmcli-nm-port.js +173 -0
  40. package/dist/backend/nmcli-runner.d.ts +24 -0
  41. package/dist/backend/nmcli-runner.js +35 -0
  42. package/dist/backend/observer.d.ts +37 -0
  43. package/dist/backend/observer.js +219 -0
  44. package/dist/backend/power-contract.d.ts +49 -0
  45. package/dist/backend/power-contract.js +34 -0
  46. package/dist/backend/recovery-attribution.d.ts +32 -0
  47. package/dist/backend/recovery-attribution.js +57 -0
  48. package/dist/backend/recovery-budget.d.ts +45 -0
  49. package/dist/backend/recovery-budget.js +44 -0
  50. package/dist/backend/recovery-ladder.d.ts +94 -0
  51. package/dist/backend/recovery-ladder.js +116 -0
  52. package/dist/backend/router-ethernet.d.ts +19 -0
  53. package/dist/backend/router-ethernet.js +66 -0
  54. package/dist/backend/row-store.d.ts +13 -0
  55. package/dist/backend/row-store.js +82 -0
  56. package/dist/backend/signal-setup.d.ts +30 -0
  57. package/dist/backend/signal-setup.js +92 -0
  58. package/dist/backend/sim-unlock.d.ts +13 -0
  59. package/dist/backend/sim-unlock.js +153 -0
  60. package/dist/backend/transition-preconditions.d.ts +76 -0
  61. package/dist/backend/transition-preconditions.js +64 -0
  62. package/dist/backend/usage/accounting.d.ts +39 -0
  63. package/dist/backend/usage/accounting.js +73 -0
  64. package/dist/backend/usage/billing-cycle.d.ts +11 -0
  65. package/dist/backend/usage/billing-cycle.js +39 -0
  66. package/dist/backend/usage/boot-id.d.ts +6 -0
  67. package/{src/backend/usage/boot-id.ts → dist/backend/usage/boot-id.js} +7 -7
  68. package/dist/backend/usage/index.d.ts +8 -0
  69. package/dist/backend/usage/index.js +11 -0
  70. package/dist/backend/usage/policy-store.d.ts +50 -0
  71. package/dist/backend/usage/policy-store.js +161 -0
  72. package/dist/backend/usage/policy-write.d.ts +65 -0
  73. package/dist/backend/usage/policy-write.js +112 -0
  74. package/dist/backend/usage/proc-net-dev.d.ts +18 -0
  75. package/{src/backend/usage/proc-net-dev.ts → dist/backend/usage/proc-net-dev.js} +39 -47
  76. package/dist/backend/usage/sampler.d.ts +75 -0
  77. package/dist/backend/usage/sampler.js +211 -0
  78. package/dist/backend/usage/store.d.ts +38 -0
  79. package/dist/backend/usage/store.js +126 -0
  80. package/dist/backend/usb-device-snapshot.d.ts +31 -0
  81. package/dist/backend/usb-device-snapshot.js +1 -0
  82. package/dist/backend/usb-enumerator.d.ts +21 -0
  83. package/dist/backend/usb-enumerator.js +153 -0
  84. package/dist/backend/usb-mode-transition.d.ts +29 -0
  85. package/dist/backend/usb-mode-transition.js +196 -0
  86. package/dist/band/band-names.d.ts +42 -0
  87. package/dist/band/band-names.js +150 -0
  88. package/dist/band/certification.d.ts +84 -0
  89. package/dist/band/certification.js +127 -0
  90. package/dist/band/certified-bands.json +4 -0
  91. package/dist/band/index.d.ts +2 -0
  92. package/dist/band/index.js +8 -0
  93. package/dist/capability/detect.d.ts +52 -0
  94. package/dist/capability/detect.js +86 -0
  95. package/dist/capability/five-g-preference.d.ts +104 -0
  96. package/dist/capability/five-g-preference.js +171 -0
  97. package/dist/capability/index.d.ts +3 -0
  98. package/dist/capability/index.js +10 -0
  99. package/dist/capability/support-claim.d.ts +39 -0
  100. package/dist/capability/support-claim.js +81 -0
  101. package/dist/domain/brand.d.ts +10 -0
  102. package/dist/domain/brand.js +21 -0
  103. package/dist/domain/errors.d.ts +32 -0
  104. package/dist/domain/errors.js +48 -0
  105. package/dist/domain/generation.d.ts +8 -0
  106. package/dist/domain/generation.js +12 -0
  107. package/dist/domain/guards.d.ts +6 -0
  108. package/dist/domain/guards.js +127 -0
  109. package/dist/domain/identity.d.ts +109 -0
  110. package/dist/domain/identity.js +86 -0
  111. package/dist/domain/index.d.ts +14 -0
  112. package/dist/domain/index.js +18 -0
  113. package/dist/domain/mm-enums.d.ts +12 -0
  114. package/dist/domain/mm-enums.js +139 -0
  115. package/dist/domain/modem-presentation.d.ts +10 -0
  116. package/dist/domain/modem-presentation.js +39 -0
  117. package/dist/domain/observation.d.ts +39 -0
  118. package/dist/domain/observation.js +4 -0
  119. package/dist/domain/operation.d.ts +118 -0
  120. package/dist/domain/operation.js +85 -0
  121. package/dist/domain/physical-identity.d.ts +43 -0
  122. package/dist/domain/physical-identity.js +113 -0
  123. package/{src/domain/policy.ts → dist/domain/policy.d.ts} +34 -69
  124. package/dist/domain/policy.js +38 -0
  125. package/dist/domain/shadow-divergence.d.ts +27 -0
  126. package/dist/domain/shadow-divergence.js +70 -0
  127. package/dist/domain/snapshot.d.ts +53 -0
  128. package/dist/domain/snapshot.js +75 -0
  129. package/{src/domain/state.ts → dist/domain/state.d.ts} +23 -122
  130. package/dist/domain/state.js +38 -0
  131. package/dist/fcc/coverage.d.ts +63 -0
  132. package/dist/fcc/coverage.js +102 -0
  133. package/dist/fcc/index.d.ts +3 -0
  134. package/dist/fcc/index.js +12 -0
  135. package/dist/fcc/policy-store.d.ts +40 -0
  136. package/dist/fcc/policy-store.js +146 -0
  137. package/dist/fcc/policy-write.d.ts +32 -0
  138. package/dist/fcc/policy-write.js +37 -0
  139. package/dist/hardware/hilink-protocol.d.ts +39 -0
  140. package/dist/hardware/hilink-protocol.js +58 -0
  141. package/dist/hardware/index.d.ts +3 -0
  142. package/dist/hardware/index.js +15 -0
  143. package/dist/hardware/router-parsers.d.ts +89 -0
  144. package/dist/hardware/router-parsers.js +234 -0
  145. package/dist/index.d.ts +19 -0
  146. package/dist/index.js +26 -0
  147. package/dist/journal/codec.d.ts +44 -0
  148. package/dist/journal/codec.js +198 -0
  149. package/dist/journal/engine.d.ts +28 -0
  150. package/dist/journal/engine.js +68 -0
  151. package/dist/journal/entry.d.ts +74 -0
  152. package/dist/journal/entry.js +56 -0
  153. package/dist/journal/index.d.ts +6 -0
  154. package/dist/journal/index.js +6 -0
  155. package/dist/journal/legacy-ceraui.d.ts +73 -0
  156. package/dist/journal/legacy-ceraui.js +227 -0
  157. package/dist/journal/recovery.d.ts +58 -0
  158. package/dist/journal/recovery.js +117 -0
  159. package/dist/journal/store.d.ts +55 -0
  160. package/dist/journal/store.js +150 -0
  161. package/dist/location/fix-state.d.ts +53 -0
  162. package/dist/location/fix-state.js +75 -0
  163. package/dist/location/index.d.ts +2 -0
  164. package/dist/location/index.js +8 -0
  165. package/dist/location/nmea.d.ts +10 -0
  166. package/dist/location/nmea.js +89 -0
  167. package/dist/observations/envelope.d.ts +63 -0
  168. package/dist/observations/envelope.js +77 -0
  169. package/dist/observations/freshness.d.ts +28 -0
  170. package/dist/observations/freshness.js +76 -0
  171. package/dist/observations/index.d.ts +13 -0
  172. package/dist/observations/index.js +24 -0
  173. package/dist/observations/metric.d.ts +47 -0
  174. package/dist/observations/metric.js +73 -0
  175. package/dist/observations/model.d.ts +80 -0
  176. package/dist/observations/model.js +11 -0
  177. package/dist/observations/provenance.d.ts +94 -0
  178. package/dist/observations/provenance.js +67 -0
  179. package/dist/observations/raw.d.ts +42 -0
  180. package/dist/observations/raw.js +146 -0
  181. package/dist/observations/reading.d.ts +51 -0
  182. package/dist/observations/reading.js +65 -0
  183. package/dist/observations/sources/hilink.d.ts +10 -0
  184. package/dist/observations/sources/hilink.js +84 -0
  185. package/dist/observations/sources/modemmanager.d.ts +18 -0
  186. package/dist/observations/sources/modemmanager.js +239 -0
  187. package/dist/observations/sources/router-shared.d.ts +29 -0
  188. package/dist/observations/sources/router-shared.js +68 -0
  189. package/dist/observations/sources/ufi.d.ts +10 -0
  190. package/dist/observations/sources/ufi.js +111 -0
  191. package/dist/observations/sources/zte.d.ts +7 -0
  192. package/dist/observations/sources/zte.js +71 -0
  193. package/dist/observations/state-separation.d.ts +64 -0
  194. package/dist/observations/state-separation.js +52 -0
  195. package/dist/operations/contracts.d.ts +72 -0
  196. package/dist/operations/contracts.js +1 -0
  197. package/dist/operations/index.d.ts +1 -0
  198. package/dist/operations/index.js +1 -0
  199. package/dist/operations/operation-engine.d.ts +16 -0
  200. package/dist/operations/operation-engine.js +194 -0
  201. package/dist/ports/index.d.ts +12 -0
  202. package/{src/ports/index.ts → dist/ports/index.js} +12 -8
  203. package/dist/ports/location.d.ts +87 -0
  204. package/dist/ports/location.js +36 -0
  205. package/dist/ports/modem-manager.d.ts +89 -0
  206. package/dist/ports/modem-manager.js +9 -0
  207. package/dist/ports/mutation-admission.d.ts +27 -0
  208. package/dist/ports/mutation-admission.js +9 -0
  209. package/dist/ports/network-manager.d.ts +68 -0
  210. package/dist/ports/network-manager.js +13 -0
  211. package/{src/ports/observation.ts → dist/ports/observation.d.ts} +16 -27
  212. package/dist/ports/observation.js +7 -0
  213. package/dist/ports/ops.d.ts +44 -0
  214. package/dist/ports/ops.js +16 -0
  215. package/{src/ports/receipts.ts → dist/ports/receipts.d.ts} +5 -27
  216. package/dist/ports/receipts.js +9 -0
  217. package/dist/ports/reconcile.d.ts +33 -0
  218. package/dist/ports/reconcile.js +200 -0
  219. package/dist/ports/resource-ownership.d.ts +29 -0
  220. package/dist/ports/resource-ownership.js +1 -0
  221. package/dist/ports/router.d.ts +19 -0
  222. package/dist/ports/router.js +7 -0
  223. package/dist/ports/sms.d.ts +64 -0
  224. package/dist/ports/sms.js +24 -0
  225. package/dist/ports/uhubctl.d.ts +6 -0
  226. package/dist/ports/uhubctl.js +1 -0
  227. package/dist/providers/contracts.d.ts +124 -0
  228. package/dist/providers/contracts.js +10 -0
  229. package/dist/providers/huawei-hilink/index.d.ts +2 -0
  230. package/dist/providers/huawei-hilink/index.js +2 -0
  231. package/dist/providers/huawei-hilink/operations.d.ts +20 -0
  232. package/dist/providers/huawei-hilink/operations.js +56 -0
  233. package/dist/providers/huawei-hilink/provider.d.ts +52 -0
  234. package/dist/providers/huawei-hilink/provider.js +76 -0
  235. package/dist/providers/huawei-hilink/runtime.d.ts +22 -0
  236. package/dist/providers/huawei-hilink/runtime.js +171 -0
  237. package/dist/providers/huawei-hilink/session.d.ts +28 -0
  238. package/dist/providers/huawei-hilink/session.js +120 -0
  239. package/dist/providers/huawei-hilink/transport.d.ts +19 -0
  240. package/dist/providers/huawei-hilink/transport.js +1 -0
  241. package/dist/providers/index.d.ts +8 -0
  242. package/dist/providers/index.js +8 -0
  243. package/dist/providers/matcher.d.ts +3 -0
  244. package/dist/providers/matcher.js +205 -0
  245. package/dist/providers/modem-manager/errors.d.ts +7 -0
  246. package/dist/providers/modem-manager/errors.js +37 -0
  247. package/dist/providers/modem-manager/generic-operations.d.ts +8 -0
  248. package/dist/providers/modem-manager/generic-operations.js +207 -0
  249. package/dist/providers/modem-manager/index.d.ts +3 -0
  250. package/dist/providers/modem-manager/index.js +3 -0
  251. package/dist/providers/modem-manager/module-operations.d.ts +21 -0
  252. package/dist/providers/modem-manager/module-operations.js +118 -0
  253. package/dist/providers/modem-manager/provider.d.ts +39 -0
  254. package/dist/providers/modem-manager/provider.js +152 -0
  255. package/dist/providers/modem-manager/snapshot.d.ts +5 -0
  256. package/dist/providers/modem-manager/snapshot.js +204 -0
  257. package/dist/providers/modem-manager/types.d.ts +135 -0
  258. package/dist/providers/modem-manager/types.js +1 -0
  259. package/dist/providers/network-manager/adapter.d.ts +71 -0
  260. package/dist/providers/network-manager/adapter.js +348 -0
  261. package/dist/providers/network-manager/index.d.ts +2 -0
  262. package/dist/providers/network-manager/index.js +2 -0
  263. package/dist/providers/network-manager/types.d.ts +163 -0
  264. package/dist/providers/network-manager/types.js +77 -0
  265. package/dist/providers/registry.d.ts +13 -0
  266. package/dist/providers/registry.js +33 -0
  267. package/dist/providers/ufi-himi/index.d.ts +6 -0
  268. package/dist/providers/ufi-himi/index.js +6 -0
  269. package/dist/providers/ufi-himi/operations.d.ts +41 -0
  270. package/dist/providers/ufi-himi/operations.js +66 -0
  271. package/dist/providers/ufi-himi/prohibitions.d.ts +62 -0
  272. package/dist/providers/ufi-himi/prohibitions.js +88 -0
  273. package/dist/providers/ufi-himi/provider.d.ts +41 -0
  274. package/dist/providers/ufi-himi/provider.js +204 -0
  275. package/dist/providers/ufi-himi/qualcomm-evidence.d.ts +32 -0
  276. package/dist/providers/ufi-himi/qualcomm-evidence.js +51 -0
  277. package/dist/providers/ufi-himi/session.d.ts +46 -0
  278. package/dist/providers/ufi-himi/session.js +92 -0
  279. package/dist/providers/ufi-himi/transport.d.ts +29 -0
  280. package/dist/providers/ufi-himi/transport.js +25 -0
  281. package/dist/providers/zte-goform/index.d.ts +2 -0
  282. package/dist/providers/zte-goform/index.js +2 -0
  283. package/dist/providers/zte-goform/provider.d.ts +51 -0
  284. package/dist/providers/zte-goform/provider.js +101 -0
  285. package/dist/providers/zte-goform/session.d.ts +17 -0
  286. package/dist/providers/zte-goform/session.js +133 -0
  287. package/dist/providers/zte-goform/transport.d.ts +16 -0
  288. package/dist/providers/zte-goform/transport.js +1 -0
  289. package/dist/radio/band-truth.d.ts +50 -0
  290. package/dist/radio/band-truth.js +92 -0
  291. package/dist/radio/index.d.ts +3 -0
  292. package/dist/radio/index.js +10 -0
  293. package/dist/radio/mode-combinations.d.ts +78 -0
  294. package/dist/radio/mode-combinations.js +198 -0
  295. package/dist/radio/mode-truth.d.ts +67 -0
  296. package/dist/radio/mode-truth.js +112 -0
  297. package/dist/redact.d.ts +15 -0
  298. package/dist/redact.js +189 -0
  299. package/dist/safety/composition-root.d.ts +28 -0
  300. package/dist/safety/composition-root.js +65 -0
  301. package/dist/safety/flock-resource-ownership.d.ts +11 -0
  302. package/dist/safety/flock-resource-ownership.js +151 -0
  303. package/dist/safety/index.d.ts +2 -0
  304. package/dist/safety/index.js +2 -0
  305. package/dist/sms/dbus-messaging.d.ts +17 -0
  306. package/dist/sms/dbus-messaging.js +185 -0
  307. package/dist/sms/inbox-store.d.ts +10 -0
  308. package/dist/sms/inbox-store.js +82 -0
  309. package/dist/sms/index.d.ts +4 -0
  310. package/dist/sms/index.js +10 -0
  311. package/dist/sms/mmcli-parse.d.ts +54 -0
  312. package/dist/sms/mmcli-parse.js +224 -0
  313. package/dist/sms/normalize.d.ts +42 -0
  314. package/dist/sms/normalize.js +95 -0
  315. package/dist/testing/domain-fakes.d.ts +58 -0
  316. package/dist/testing/domain-fakes.js +98 -0
  317. package/dist/testing/index.d.ts +2 -0
  318. package/dist/testing/index.js +17 -0
  319. package/dist/testing/provider-fakes.d.ts +45 -0
  320. package/dist/testing/provider-fakes.js +64 -0
  321. package/dist/transport/calls.d.ts +9 -0
  322. package/dist/transport/calls.js +88 -0
  323. package/dist/transport/codec.d.ts +3 -0
  324. package/dist/transport/codec.js +207 -0
  325. package/dist/transport/dbus-native.d.ts +57 -0
  326. package/dist/transport/dbus-native.js +17 -0
  327. package/dist/transport/errors.d.ts +21 -0
  328. package/{src/transport/errors.ts → dist/transport/errors.js} +34 -46
  329. package/dist/transport/index.d.ts +4 -0
  330. package/dist/transport/index.js +9 -0
  331. package/dist/transport/signals.d.ts +15 -0
  332. package/dist/transport/signals.js +123 -0
  333. package/dist/transport/signature.d.ts +7 -0
  334. package/dist/transport/signature.js +94 -0
  335. package/dist/transport/transport.d.ts +2 -0
  336. package/dist/transport/transport.js +202 -0
  337. package/dist/transport/types.d.ts +61 -0
  338. package/dist/transport/types.js +19 -0
  339. package/dist/usb-mode/catalog-schema.d.ts +139 -0
  340. package/dist/usb-mode/catalog-schema.js +97 -0
  341. package/dist/usb-mode/catalog.d.ts +21 -0
  342. package/{src/usb-mode/catalog.ts → dist/usb-mode/catalog.js} +10 -32
  343. package/dist/usb-mode/certified-catalog.json +67 -0
  344. package/dist/usb-mode/index.d.ts +5 -0
  345. package/dist/usb-mode/index.js +15 -0
  346. package/dist/usb-mode/ingestion.d.ts +111 -0
  347. package/dist/usb-mode/ingestion.js +187 -0
  348. package/dist/usb-mode/promotion-review.d.ts +21 -0
  349. package/dist/usb-mode/promotion-review.js +87 -0
  350. package/dist/usb-mode/usb-devices-parse.d.ts +36 -0
  351. package/dist/usb-mode/usb-devices-parse.js +157 -0
  352. package/dist/ussd/calls.d.ts +32 -0
  353. package/dist/ussd/calls.js +96 -0
  354. package/dist/ussd/index.d.ts +5 -0
  355. package/dist/ussd/index.js +12 -0
  356. package/dist/ussd/mm-ussd.d.ts +37 -0
  357. package/dist/ussd/mm-ussd.js +205 -0
  358. package/dist/ussd/refusal.d.ts +53 -0
  359. package/dist/ussd/refusal.js +154 -0
  360. package/dist/ussd/registration.d.ts +20 -0
  361. package/dist/ussd/registration.js +101 -0
  362. package/dist/ussd/session.d.ts +84 -0
  363. package/dist/ussd/session.js +163 -0
  364. package/package.json +38 -4
  365. package/src/backend/at-lease.test.ts +0 -106
  366. package/src/backend/at-lease.ts +0 -158
  367. package/src/backend/cell-info.test.ts +0 -154
  368. package/src/backend/cell-info.ts +0 -160
  369. package/src/backend/device-classifier.test.ts +0 -168
  370. package/src/backend/device-classifier.ts +0 -248
  371. package/src/backend/enrichment.ts +0 -96
  372. package/src/backend/features.test.ts +0 -162
  373. package/src/backend/features.ts +0 -179
  374. package/src/backend/identity-ladder.test.ts +0 -117
  375. package/src/backend/identity-ladder.ts +0 -221
  376. package/src/backend/identity-registry.test.ts +0 -89
  377. package/src/backend/identity-registry.ts +0 -151
  378. package/src/backend/index.ts +0 -221
  379. package/src/backend/lifecycle-interlock.ts +0 -38
  380. package/src/backend/managed-objects.ts +0 -108
  381. package/src/backend/mapping.ts +0 -160
  382. package/src/backend/mm-backend.ts +0 -191
  383. package/src/backend/mm-mutations.ts +0 -228
  384. package/src/backend/modem-actor.test.ts +0 -95
  385. package/src/backend/modem-actor.ts +0 -112
  386. package/src/backend/nm-auto-apn.ts +0 -161
  387. package/src/backend/nm-gsm-fields.ts +0 -122
  388. package/src/backend/nmcli-nm-port.ts +0 -228
  389. package/src/backend/nmcli-runner.ts +0 -52
  390. package/src/backend/observer.ts +0 -297
  391. package/src/backend/power-contract.test.ts +0 -40
  392. package/src/backend/power-contract.ts +0 -83
  393. package/src/backend/recovery-attribution.test.ts +0 -102
  394. package/src/backend/recovery-attribution.ts +0 -86
  395. package/src/backend/recovery-budget.test.ts +0 -64
  396. package/src/backend/recovery-budget.ts +0 -84
  397. package/src/backend/recovery-ladder.test.ts +0 -257
  398. package/src/backend/recovery-ladder.ts +0 -249
  399. package/src/backend/router-ethernet.test.ts +0 -71
  400. package/src/backend/router-ethernet.ts +0 -90
  401. package/src/backend/row-store.ts +0 -105
  402. package/src/backend/signal-setup.ts +0 -112
  403. package/src/backend/sim-unlock.ts +0 -193
  404. package/src/backend/transition-preconditions.ts +0 -149
  405. package/src/backend/usage/accounting.test.ts +0 -147
  406. package/src/backend/usage/accounting.ts +0 -123
  407. package/src/backend/usage/billing-cycle.test.ts +0 -62
  408. package/src/backend/usage/billing-cycle.ts +0 -45
  409. package/src/backend/usage/index.ts +0 -37
  410. package/src/backend/usage/proc-net-dev.test.ts +0 -56
  411. package/src/backend/usage/sampler.test.ts +0 -219
  412. package/src/backend/usage/sampler.ts +0 -228
  413. package/src/backend/usage/store.test.ts +0 -148
  414. package/src/backend/usage/store.ts +0 -177
  415. package/src/backend/usb-enumerator.test.ts +0 -87
  416. package/src/backend/usb-enumerator.ts +0 -181
  417. package/src/backend/usb-mode-transition.test.ts +0 -323
  418. package/src/backend/usb-mode-transition.ts +0 -253
  419. package/src/domain/brand.ts +0 -29
  420. package/src/domain/errors.ts +0 -77
  421. package/src/domain/guards.test.ts +0 -218
  422. package/src/domain/guards.ts +0 -144
  423. package/src/domain/identity.test.ts +0 -83
  424. package/src/domain/identity.ts +0 -165
  425. package/src/domain/index.ts +0 -12
  426. package/src/domain/snapshot.test.ts +0 -266
  427. package/src/domain/snapshot.ts +0 -120
  428. package/src/index.test.ts +0 -6
  429. package/src/index.ts +0 -15
  430. package/src/ports/README.md +0 -61
  431. package/src/ports/forbidden-surface.test.ts +0 -241
  432. package/src/ports/modem-manager.ts +0 -72
  433. package/src/ports/network-manager.ts +0 -87
  434. package/src/ports/ops.ts +0 -60
  435. package/src/ports/ops.type-test.ts +0 -39
  436. package/src/ports/receipts.test.ts +0 -153
  437. package/src/ports/reconcile.test.ts +0 -152
  438. package/src/ports/reconcile.ts +0 -338
  439. package/src/ports/router.ts +0 -29
  440. package/src/redact.test.ts +0 -82
  441. package/src/redact.ts +0 -73
  442. package/src/transport/README.md +0 -65
  443. package/src/transport/calls.ts +0 -113
  444. package/src/transport/characterization.test.ts +0 -260
  445. package/src/transport/codec.test.ts +0 -118
  446. package/src/transport/codec.ts +0 -240
  447. package/src/transport/conformance-python.test.ts +0 -152
  448. package/src/transport/conformance-same-lib.test.ts +0 -115
  449. package/src/transport/dbus-native-lib.d.ts +0 -19
  450. package/src/transport/dbus-native.ts +0 -85
  451. package/src/transport/index.ts +0 -30
  452. package/src/transport/no-library-leak.test.ts +0 -61
  453. package/src/transport/reliability.test.ts +0 -173
  454. package/src/transport/signals.ts +0 -150
  455. package/src/transport/signature.ts +0 -110
  456. package/src/transport/test-support/fake-service.ts +0 -168
  457. package/src/transport/test-support/independent-producer.py +0 -110
  458. package/src/transport/test-support/private-bus.ts +0 -66
  459. package/src/transport/transport.ts +0 -250
  460. package/src/transport/types.ts +0 -118
  461. package/src/usb-mode/catalog-schema.test.ts +0 -181
  462. package/src/usb-mode/catalog-schema.ts +0 -113
  463. package/src/usb-mode/certified-catalog.json +0 -67
  464. package/src/usb-mode/index.ts +0 -27
@@ -1,297 +0,0 @@
1
- // The epoch-scoped ModemManager observer.
2
- //
3
- // `MmDbusObserver` implements the read-only `ModemObservationPort` over the A2.4
4
- // transport, tested against the A2.3 fake. `start()` connects, subscribes to the four
5
- // lifecycle signals, THEN takes the first authoritative `GetManagedObjects` snapshot —
6
- // reconciling any signal that raced in between.
7
- //
8
- // SAFETY-CRITICAL — epoch-scoped removal (draft §Oracle round-3 #5). An "epoch" is one
9
- // continuous ownership period of the MM bus name, tracked via `NameOwnerChanged`. A
10
- // modem is REMOVED only when it is missing from a CURRENT-epoch authoritative snapshot.
11
- // Owner loss, bus disconnect, and any signal whose `sender` is not the current owner
12
- // (an OLD-epoch straggler) never remove a modem — they only ever mark it
13
- // `sourceUnavailable`. The false-removal class is dead by construction: even the
14
- // `ObservationList` failure arm retains its rows. Row bookkeeping lives in
15
- // `ObservationRowStore`; this file owns epoch tracking, subscriptions, and refresh.
16
-
17
- import type {
18
- ModemObservationPort,
19
- ObservationFailureReason,
20
- ObservationList,
21
- ObservationListener,
22
- Unsubscribe,
23
- } from '../ports';
24
- import type { DbusTransport, SignalEvent, Subscription } from '../transport';
25
- import {
26
- DBUS_DESTINATION,
27
- DBUS_IFACE,
28
- DBUS_PATH,
29
- MM_BUS_NAME,
30
- MM_ROOT_PATH,
31
- OBJECT_MANAGER_IFACE,
32
- PROPERTIES_IFACE,
33
- } from './constants';
34
- import { asManagedObjects, type DecodedManagedObjects } from './managed-objects';
35
- import { ObservationRowStore } from './row-store';
36
-
37
- /**
38
- * A current-epoch authoritative snapshot, delivered to `onEpochRefresh` AFTER the
39
- * epoch guard passes. `epoch` is the owning MM unique bus name; `tree` is the decoded
40
- * `GetManagedObjects` payload the snapshot was reconciled from. The Signal.Setup
41
- * manager (A3.3) hooks this to (re-)apply cadence per modem per epoch, and the D-Bus
42
- * backend uses it to refresh its path→stable-key map.
43
- */
44
- export interface EpochRefreshEvent {
45
- readonly epoch: string;
46
- readonly tree: DecodedManagedObjects;
47
- }
48
-
49
- export interface MmDbusObserverOptions {
50
- /** The transport to talk D-Bus over (A2.4). The observer connects it on `start()`. */
51
- readonly transport: DbusTransport;
52
- /** MM bus name override (defaults to `org.freedesktop.ModemManager1`). */
53
- readonly destination?: string;
54
- /**
55
- * Called after EVERY successful current-epoch authoritative snapshot (start,
56
- * hot-plug, epoch change, property change) — never for a superseded epoch. The
57
- * hook fires whether or not any row changed, so a consumer always sees the live
58
- * epoch + tree. It must not throw; a throw is swallowed so it can never break the
59
- * observer's refresh loop.
60
- */
61
- readonly onEpochRefresh?: (event: EpochRefreshEvent) => void;
62
- }
63
-
64
- export class MmDbusObserver implements ModemObservationPort {
65
- readonly #transport: DbusTransport;
66
- readonly #destination: string;
67
- readonly #onEpochRefresh: ((event: EpochRefreshEvent) => void) | undefined;
68
- readonly #store = new ObservationRowStore();
69
- readonly #listeners = new Set<ObservationListener>();
70
- readonly #subscriptions: Subscription[] = [];
71
-
72
- #currentOwner: string | undefined;
73
- #started = false;
74
- #stopped = false;
75
- #priming = false;
76
- #refreshDuringPrime = false;
77
- #refreshing = false;
78
- #refreshQueued = false;
79
-
80
- readonly #onDisconnected = (): void => this.#handleSourceGone('source-unavailable');
81
- readonly #onReconnected = (): void => {
82
- void this.#adoptCurrentOwner();
83
- };
84
-
85
- constructor(options: MmDbusObserverOptions) {
86
- this.#transport = options.transport;
87
- this.#destination = options.destination ?? MM_BUS_NAME;
88
- this.#onEpochRefresh = options.onEpochRefresh;
89
- }
90
-
91
- async start(): Promise<ObservationList> {
92
- if (this.#started) {
93
- return this.#store.list();
94
- }
95
- this.#started = true;
96
- this.#priming = true;
97
- await this.#transport.connect();
98
- this.#transport.on('disconnected', this.#onDisconnected);
99
- this.#transport.on('reconnected', this.#onReconnected);
100
- await this.#subscribeAll();
101
- await this.#adoptCurrentOwner();
102
- this.#priming = false;
103
- if (this.#refreshDuringPrime) {
104
- this.#refreshDuringPrime = false;
105
- this.#scheduleRefresh();
106
- }
107
- return this.#store.list();
108
- }
109
-
110
- observe(listener: ObservationListener): Unsubscribe {
111
- this.#listeners.add(listener);
112
- return () => {
113
- this.#listeners.delete(listener);
114
- };
115
- }
116
-
117
- async stop(): Promise<void> {
118
- if (this.#stopped) {
119
- return;
120
- }
121
- this.#stopped = true;
122
- this.#transport.off('disconnected', this.#onDisconnected);
123
- this.#transport.off('reconnected', this.#onReconnected);
124
- const subs = this.#subscriptions.splice(0);
125
- await Promise.all(subs.map((sub) => sub.unsubscribe().catch(() => undefined)));
126
- this.#listeners.clear();
127
- }
128
-
129
- // ── signal subscription ────────────────────────────────────────────────────────
130
-
131
- async #subscribeAll(): Promise<void> {
132
- const om = { interface: OBJECT_MANAGER_IFACE, path: MM_ROOT_PATH } as const;
133
- this.#subscriptions.push(
134
- await this.#transport.subscribeSignal({ ...om, member: 'InterfacesAdded' }, (event) =>
135
- this.#onObjectSignal(event),
136
- ),
137
- await this.#transport.subscribeSignal({ ...om, member: 'InterfacesRemoved' }, (event) =>
138
- this.#onObjectSignal(event),
139
- ),
140
- await this.#transport.subscribeSignal(
141
- { interface: PROPERTIES_IFACE, member: 'PropertiesChanged' },
142
- (event) => this.#onObjectSignal(event),
143
- ),
144
- await this.#transport.subscribeSignal(
145
- { interface: DBUS_IFACE, member: 'NameOwnerChanged' },
146
- (event) => this.#onNameOwnerChanged(event),
147
- ),
148
- );
149
- }
150
-
151
- // ── epoch tracking ───────────────────────────────────────────────────────────
152
-
153
- async #adoptCurrentOwner(): Promise<void> {
154
- const owner = await this.#queryOwner();
155
- if (owner === undefined) {
156
- this.#handleSourceGone('source-unavailable');
157
- return;
158
- }
159
- this.#currentOwner = owner;
160
- await this.#runRefresh(owner);
161
- }
162
-
163
- async #queryOwner(): Promise<string | undefined> {
164
- try {
165
- const reply = await this.#transport.callMethod({
166
- destination: DBUS_DESTINATION,
167
- path: DBUS_PATH,
168
- interface: DBUS_IFACE,
169
- member: 'GetNameOwner',
170
- signature: 's',
171
- args: [MM_BUS_NAME],
172
- });
173
- const owner = reply.body[0];
174
- return typeof owner === 'string' && owner.length > 0 ? owner : undefined;
175
- } catch {
176
- // NameHasNoOwner (or a transient failure) → no current epoch yet.
177
- return undefined;
178
- }
179
- }
180
-
181
- #onNameOwnerChanged(event: SignalEvent): void {
182
- if (event.body[0] !== MM_BUS_NAME) {
183
- return;
184
- }
185
- const newOwner = typeof event.body[2] === 'string' ? event.body[2] : '';
186
- if (newOwner.length === 0) {
187
- // Owner lost — stale, never a removal.
188
- this.#handleSourceGone('source-unavailable');
189
- return;
190
- }
191
- if (newOwner === this.#currentOwner) {
192
- return;
193
- }
194
- // New epoch: everything goes stale until the fresh snapshot restores it.
195
- if (this.#store.markUnavailable('source-unavailable')) {
196
- this.#emit();
197
- }
198
- this.#currentOwner = newOwner;
199
- this.#scheduleRefresh();
200
- }
201
-
202
- #onObjectSignal(event: SignalEvent): void {
203
- // Epoch guard: a signal from anyone but the current owner is an OLD-epoch
204
- // straggler and must never drive a removal (draft §Oracle round-3 #5).
205
- if (this.#currentOwner === undefined || event.sender !== this.#currentOwner) {
206
- return;
207
- }
208
- if (this.#priming) {
209
- this.#refreshDuringPrime = true;
210
- return;
211
- }
212
- this.#scheduleRefresh();
213
- }
214
-
215
- // ── authoritative refresh ──────────────────────────────────────────────────────
216
-
217
- #scheduleRefresh(): void {
218
- const owner = this.#currentOwner;
219
- if (owner === undefined || this.#stopped) {
220
- return;
221
- }
222
- if (this.#refreshing) {
223
- this.#refreshQueued = true;
224
- return;
225
- }
226
- void this.#runRefresh(owner);
227
- }
228
-
229
- async #runRefresh(epochOwner: string): Promise<void> {
230
- this.#refreshing = true;
231
- try {
232
- const reply = await this.#transport.callMethod({
233
- destination: this.#destination,
234
- path: MM_ROOT_PATH,
235
- interface: OBJECT_MANAGER_IFACE,
236
- member: 'GetManagedObjects',
237
- });
238
- // Late reply from a superseded epoch — discard (draft §Oracle round-3 #5).
239
- if (this.#currentOwner !== epochOwner || this.#stopped) {
240
- return;
241
- }
242
- const tree = asManagedObjects(reply.body[0]);
243
- const rowsChanged = this.#store.reconcile(tree);
244
- const healthChanged = this.#store.markHealthy();
245
- this.#notifyEpochRefresh(epochOwner, tree);
246
- if (rowsChanged || healthChanged) {
247
- this.#emit();
248
- }
249
- } catch {
250
- if (this.#currentOwner === epochOwner && !this.#stopped) {
251
- this.#markUnavailable('bus-error');
252
- }
253
- } finally {
254
- this.#refreshing = false;
255
- if (this.#refreshQueued && !this.#stopped) {
256
- this.#refreshQueued = false;
257
- this.#scheduleRefresh();
258
- }
259
- }
260
- }
261
-
262
- // ── source-unavailable transitions ─────────────────────────────────────────────
263
-
264
- #handleSourceGone(reason: ObservationFailureReason): void {
265
- this.#currentOwner = undefined;
266
- this.#markUnavailable(reason);
267
- }
268
-
269
- #markUnavailable(reason: ObservationFailureReason): void {
270
- if (this.#store.markUnavailable(reason)) {
271
- this.#emit();
272
- }
273
- }
274
-
275
- #notifyEpochRefresh(epoch: string, tree: DecodedManagedObjects): void {
276
- if (this.#onEpochRefresh === undefined) {
277
- return;
278
- }
279
- try {
280
- this.#onEpochRefresh({ epoch, tree });
281
- } catch {
282
- // A consumer's hook must never break the observer's refresh loop.
283
- }
284
- }
285
-
286
- #emit(): void {
287
- const list = this.#store.list();
288
- for (const listener of [...this.#listeners]) {
289
- listener(list);
290
- }
291
- }
292
- }
293
-
294
- /** Construct an epoch-scoped ModemManager observer over an A2.4 transport. */
295
- export function createMmDbusObserver(options: MmDbusObserverOptions): MmDbusObserver {
296
- return new MmDbusObserver(options);
297
- }
@@ -1,40 +0,0 @@
1
- // Power contract — only `none` is implemented, so every capability returns
2
- // `unsupported` in Phase A (ladder rung 4 never actuates on today's hardware).
3
-
4
- import { describe, expect, test } from 'bun:test';
5
- import { epochMillis } from '../domain';
6
- import {
7
- NONE_POWER_CAPABILITY,
8
- NONE_POWER_HOOK,
9
- type PowerCapability,
10
- unsupportedPowerHook,
11
- } from './power-contract';
12
-
13
- const context = { stableKey: 'slot:a', at: epochMillis(0) };
14
-
15
- describe('power contract — Phase A', () => {
16
- test('the none capability describes a no-op with an enumeration timeout', () => {
17
- expect(NONE_POWER_CAPABILITY.power).toBe('none');
18
- expect(NONE_POWER_CAPABILITY.enumerationTimeoutMs).toBeGreaterThan(0);
19
- });
20
-
21
- test('NONE_POWER_HOOK.cycle always returns unsupported', async () => {
22
- const result = await NONE_POWER_HOOK.cycle(context);
23
- expect(result.status).toBe('unsupported');
24
- expect(result.reason).toContain('none');
25
- });
26
-
27
- test('a real capability is typed but unsupported (declared, not implemented)', async () => {
28
- const gpio: PowerCapability = {
29
- power: 'gpio-cut',
30
- usbReset: true,
31
- enumerationTimeoutMs: 15_000,
32
- preferredUsbMode: 'qmi',
33
- };
34
- const hook = unsupportedPowerHook(gpio);
35
- expect(hook.capability.power).toBe('gpio-cut');
36
- const result = await hook.cycle(context);
37
- expect(result.status).toBe('unsupported');
38
- expect(result.reason).toContain('gpio-cut');
39
- });
40
- });
@@ -1,83 +0,0 @@
1
- // The modem power-control capability contract — recovery ladder rung 4.
2
- //
3
- // Power control becomes a first-class v1 CONTRACT (draft §gap sweep: Sixfab GPIO26
4
- // power-cut + PWRKEY-pulse boards, BELABOX-reported RM520N USB instability needing a
5
- // powered carrier). The FIELDS ship in Phase A; the board-specific GPIO / USB-hub
6
- // IMPLEMENTATIONS stay hardware-gated. Only the `none` capability is implemented (a
7
- // no-op), so ladder rung 4 always returns `unsupported` on today's hardware — every
8
- // real mechanism is a typed-but-unsupported placeholder.
9
-
10
- import type { EpochMillis } from '../domain';
11
-
12
- /** How a board can power-cycle a modem. Only `none` is implemented in Phase A. */
13
- export type PowerCapabilityKind = 'none' | 'gpio-cut' | 'pwrkey-pulse' | 'usb-hub-port-cycle';
14
-
15
- /**
16
- * The USB data mode a modem should re-enumerate into after a power cycle. A4.2 owns
17
- * the certified USB-mode catalog; the power contract only records the operator's
18
- * preferred post-cycle enumeration mode.
19
- */
20
- export type PreferredUsbMode = 'qmi' | 'mbim' | 'ecm-ncm' | 'rndis' | 'router-ethernet';
21
-
22
- /** A board's modem power-control capability description. */
23
- export interface PowerCapability {
24
- readonly power: PowerCapabilityKind;
25
- /** The board can pulse a USB-level reset on the modem's port. */
26
- readonly usbReset?: boolean;
27
- /** How long to wait for the modem to re-enumerate after a cycle. */
28
- readonly enumerationTimeoutMs: number;
29
- /** Which USB mode to prefer when the modem re-enumerates (A4.2 catalog vocabulary). */
30
- readonly preferredUsbMode?: PreferredUsbMode;
31
- }
32
-
33
- /** The Phase-A capability: no board power control exists — a no-op. */
34
- export const NONE_POWER_CAPABILITY: PowerCapability = {
35
- power: 'none',
36
- enumerationTimeoutMs: 30_000,
37
- };
38
-
39
- /** Outcome of asking the power hook to cycle a modem. */
40
- export interface PowerCycleResult {
41
- readonly status: 'applied' | 'unsupported' | 'failed';
42
- readonly reason: string;
43
- }
44
-
45
- /** The minimal context handed to the power hook. */
46
- export interface PowerCycleContext {
47
- readonly stableKey: string;
48
- readonly at: EpochMillis;
49
- }
50
-
51
- /**
52
- * The pluggable power-cycle hook (recovery ladder rung 4). Phase A ships only the
53
- * `none` no-op; a real GPIO / PWRKEY / USB-hub implementation is hardware-gated and
54
- * injected later without changing the ladder.
55
- */
56
- export interface PowerHook {
57
- readonly capability: PowerCapability;
58
- cycle(context: PowerCycleContext): Promise<PowerCycleResult>;
59
- }
60
-
61
- /**
62
- * Build a Phase-A power hook for `capability`. EVERY capability returns
63
- * `unsupported`: `none` because there is nothing to cut, and every real mechanism
64
- * because its hardware driver is not implemented yet — the contract field exists so
65
- * a board can DECLARE the capability, but rung 4 never actuates in Phase A.
66
- */
67
- export function unsupportedPowerHook(capability: PowerCapability): PowerHook {
68
- return {
69
- capability,
70
- cycle(): Promise<PowerCycleResult> {
71
- return Promise.resolve({
72
- status: 'unsupported',
73
- reason:
74
- capability.power === 'none'
75
- ? "power capability 'none' — no board power control exists"
76
- : `power capability '${capability.power}' is declared but not implemented in Phase A`,
77
- });
78
- },
79
- };
80
- }
81
-
82
- /** The Phase-A default power hook: describes `none` and always returns `unsupported`. */
83
- export const NONE_POWER_HOOK: PowerHook = unsupportedPowerHook(NONE_POWER_CAPABILITY);
@@ -1,102 +0,0 @@
1
- // Fault attribution — pure classification with the stale-forces-indeterminate
2
- // invariant as the headline safety test.
3
-
4
- import { describe, expect, test } from 'bun:test';
5
- import { imeiEquipmentId, initialSnapshot, type ModemIdentity, runtimePath } from '../domain';
6
- import {
7
- attributeFault,
8
- attributeSnapshot,
9
- type FaultSymptoms,
10
- symptomsFromSnapshot,
11
- } from './recovery-attribution';
12
-
13
- /** A live, present, otherwise-unremarkable symptom baseline. */
14
- function symptoms(overrides: Partial<FaultSymptoms> = {}): FaultSymptoms {
15
- return {
16
- sourceHealth: 'live',
17
- presence: 'present',
18
- mmState: 'connected',
19
- registration: 'home',
20
- nmActivation: 'activated',
21
- ...overrides,
22
- };
23
- }
24
-
25
- describe('attributeFault — confident classification', () => {
26
- test('MM reporting a failed modem on a live source → modem-fault', () => {
27
- expect(attributeFault(symptoms({ mmState: 'failed' }))).toBe('modem-fault');
28
- });
29
-
30
- test('denied registration → network-fault', () => {
31
- expect(attributeFault(symptoms({ registration: 'denied' }))).toBe('network-fault');
32
- });
33
-
34
- test('registered but NM not activated (registered-but-no-data) → network-fault', () => {
35
- expect(attributeFault(symptoms({ registration: 'home', nmActivation: 'failed' }))).toBe(
36
- 'network-fault',
37
- );
38
- expect(
39
- attributeFault(symptoms({ registration: 'roaming', nmActivation: 'disconnected' })),
40
- ).toBe('network-fault');
41
- });
42
-
43
- test('healthy registered+activated modem → indeterminate (no clear fault)', () => {
44
- expect(attributeFault(symptoms())).toBe('indeterminate');
45
- });
46
-
47
- test('a searching modem is ambiguous → indeterminate', () => {
48
- expect(attributeFault(symptoms({ mmState: 'searching', registration: 'searching' }))).toBe(
49
- 'indeterminate',
50
- );
51
- });
52
-
53
- test('absent modem on a live source → indeterminate (nothing to attribute)', () => {
54
- expect(attributeFault(symptoms({ presence: 'absent', mmState: 'unknown' }))).toBe(
55
- 'indeterminate',
56
- );
57
- });
58
- });
59
-
60
- describe('attributeFault — HARD INVARIANT: stale / sourceUnavailable forces indeterminate', () => {
61
- test('a STALE source forces indeterminate even when MM says the modem failed', () => {
62
- // Without the invariant this would classify modem-fault and could authorise a
63
- // disruptive step off data we no longer trust.
64
- expect(attributeFault(symptoms({ sourceHealth: 'stale', mmState: 'failed' }))).toBe(
65
- 'indeterminate',
66
- );
67
- });
68
-
69
- test('a sourceUnavailable source forces indeterminate even for a clear modem-fault', () => {
70
- expect(attributeFault(symptoms({ sourceHealth: 'sourceUnavailable', mmState: 'failed' }))).toBe(
71
- 'indeterminate',
72
- );
73
- });
74
-
75
- test('stale input never yields network-fault either', () => {
76
- expect(attributeFault(symptoms({ sourceHealth: 'stale', registration: 'denied' }))).toBe(
77
- 'indeterminate',
78
- );
79
- });
80
- });
81
-
82
- describe('symptomsFromSnapshot / attributeSnapshot', () => {
83
- const identity: ModemIdentity = {
84
- equipmentId: imeiEquipmentId('359000000000001'),
85
- runtimePath: runtimePath('/org/freedesktop/ModemManager1/Modem/0'),
86
- };
87
-
88
- test('projects the attribution-relevant dimensions out of a snapshot', () => {
89
- const snapshot = initialSnapshot(identity);
90
- expect(symptomsFromSnapshot(snapshot)).toEqual({
91
- sourceHealth: 'live',
92
- presence: 'absent',
93
- mmState: 'unknown',
94
- registration: 'unknown',
95
- nmActivation: 'unavailable',
96
- });
97
- });
98
-
99
- test('attributeSnapshot on a fresh (absent) snapshot → indeterminate', () => {
100
- expect(attributeSnapshot(initialSnapshot(identity))).toBe('indeterminate');
101
- });
102
- });
@@ -1,86 +0,0 @@
1
- // Fault attribution — the safety gate the recovery ladder is built around.
2
- //
3
- // Before any disruptive recovery action, a fault MUST be attributed. Recovery may
4
- // ONLY ever act on a confident `modem-fault`; `network-fault` and `indeterminate`
5
- // never authorise a disruptive step (draft §Oracle recovery: "fault attribution
6
- // required before disruptive action"). This module is pure — it classifies from a
7
- // narrow projection of one snapshot and never performs I/O.
8
-
9
- import type {
10
- CellularSnapshot,
11
- MmState,
12
- NmActivation,
13
- Presence,
14
- RegistrationStatus,
15
- SourceHealth,
16
- } from '../domain';
17
- import { isRegistered } from '../domain';
18
-
19
- /**
20
- * The confident classification of a modem fault:
21
- * - `modem-fault` — the modem itself is broken (only this may be disruptive).
22
- * - `network-fault` — attached to (or refused by) the network; the modem is fine.
23
- * - `indeterminate` — ambiguous, or observed from an unreliable source.
24
- */
25
- export type FaultAttribution = 'modem-fault' | 'network-fault' | 'indeterminate';
26
-
27
- /** The narrow set of symptoms attribution reasons over. */
28
- export interface FaultSymptoms {
29
- readonly sourceHealth: SourceHealth;
30
- readonly presence: Presence;
31
- readonly mmState: MmState;
32
- readonly registration: RegistrationStatus;
33
- readonly nmActivation: NmActivation;
34
- }
35
-
36
- /**
37
- * Classify a modem fault from observed symptoms.
38
- *
39
- * HARD SAFETY INVARIANT (draft §round-5 stale-forces-indeterminate): if the
40
- * observation SOURCE is not `live` — i.e. `stale` or `sourceUnavailable`, as A3.1's
41
- * epoch observer reports on owner loss / bus disconnect / old-epoch signals — the
42
- * attribution is FORCED to `indeterminate`. We never guess `modem-fault` or
43
- * `network-fault` from unreliable data. Because only a confident `modem-fault` can
44
- * later authorise a disruptive step, forcing indeterminate here makes stale input
45
- * strictly safe: no ladder step can fire off data we do not trust.
46
- */
47
- export function attributeFault(symptoms: FaultSymptoms): FaultAttribution {
48
- // 1. Unreliable source → never guess. (The invariant — checked first.)
49
- if (symptoms.sourceHealth !== 'live') {
50
- return 'indeterminate';
51
- }
52
- // 2. Nothing present to attribute (and nothing to act on via D-Bus).
53
- if (symptoms.presence === 'absent') {
54
- return 'indeterminate';
55
- }
56
- // 3. MM — a healthy source — reports THIS modem terminally failed → modem-fault.
57
- if (symptoms.mmState === 'failed') {
58
- return 'modem-fault';
59
- }
60
- // 4. The network refused registration → network-fault (not the modem's fault).
61
- if (symptoms.registration === 'denied') {
62
- return 'network-fault';
63
- }
64
- // 5. Registered to a network but data is not up → registered-but-no-data.
65
- if (isRegistered(symptoms.registration) && symptoms.nmActivation !== 'activated') {
66
- return 'network-fault';
67
- }
68
- // 6. Anything else is ambiguous — stay safe.
69
- return 'indeterminate';
70
- }
71
-
72
- /** Project the symptoms attribution needs out of a full snapshot. */
73
- export function symptomsFromSnapshot(snapshot: CellularSnapshot): FaultSymptoms {
74
- return {
75
- sourceHealth: snapshot.sourceHealth,
76
- presence: snapshot.presence,
77
- mmState: snapshot.mmState,
78
- registration: snapshot.registration.status,
79
- nmActivation: snapshot.nmActivation,
80
- };
81
- }
82
-
83
- /** Attribute a fault directly from a snapshot (symptoms projection + classify). */
84
- export function attributeSnapshot(snapshot: CellularSnapshot): FaultAttribution {
85
- return attributeFault(symptomsFromSnapshot(snapshot));
86
- }
@@ -1,64 +0,0 @@
1
- // Recovery budget — pure reducer proof: bounded attempts, cooldown refusal, and the
2
- // loop-stop latch that marks a flapping modem degraded.
3
-
4
- import { describe, expect, test } from 'bun:test';
5
- import { epochMillis } from '../domain';
6
- import {
7
- beginAttempt,
8
- INITIAL_BUDGET_STATE,
9
- markRecovered,
10
- type RecoveryBudget,
11
- type RecoveryBudgetState,
12
- } from './recovery-budget';
13
-
14
- const budget: RecoveryBudget = { maxAttempts: 2, cooldownMs: 1000 };
15
-
16
- describe('beginAttempt — budget / cooldown / loop-stop', () => {
17
- test('a fresh modem proceeds and increments attempts', () => {
18
- const decision = beginAttempt(INITIAL_BUDGET_STATE, budget, epochMillis(0));
19
- expect(decision.kind).toBe('proceed');
20
- expect(decision.state.attempts).toBe(1);
21
- expect(decision.state.lastAttemptAt).toBe(epochMillis(0));
22
- });
23
-
24
- test('a second attempt sooner than cooldownMs is refused (not counted)', () => {
25
- const first = beginAttempt(INITIAL_BUDGET_STATE, budget, epochMillis(0));
26
- const second = beginAttempt(first.state, budget, epochMillis(500));
27
- expect(second.kind).toBe('cooldown');
28
- // Attempts unchanged — a refused attempt does not spend budget.
29
- expect(second.state.attempts).toBe(1);
30
- if (second.kind === 'cooldown') {
31
- expect(second.retryAfter).toBe(epochMillis(1000));
32
- }
33
- });
34
-
35
- test('exactly maxAttempts proceed, then the loop-stop latches degraded', () => {
36
- let state: RecoveryBudgetState = INITIAL_BUDGET_STATE;
37
- // Attempt 1 at t=0, attempt 2 at t=1000 (past cooldown) both proceed.
38
- const a1 = beginAttempt(state, budget, epochMillis(0));
39
- expect(a1.kind).toBe('proceed');
40
- state = a1.state;
41
- const a2 = beginAttempt(state, budget, epochMillis(1000));
42
- expect(a2.kind).toBe('proceed');
43
- state = a2.state;
44
- expect(state.attempts).toBe(2);
45
- // Attempt 3 past cooldown: budget spent → loop-stop, degraded latched.
46
- const a3 = beginAttempt(state, budget, epochMillis(2000));
47
- expect(a3.kind).toBe('loop-stop');
48
- expect(a3.state.degraded).toBe(true);
49
- });
50
-
51
- test('once degraded, every further attempt is loop-stopped (latched)', () => {
52
- const degraded: RecoveryBudgetState = { attempts: 2, degraded: true };
53
- const again = beginAttempt(degraded, budget, epochMillis(999_999));
54
- expect(again.kind).toBe('loop-stop');
55
- expect(again.state.degraded).toBe(true);
56
- });
57
-
58
- test('markRecovered clears the counter and the degraded latch', () => {
59
- expect(markRecovered()).toEqual(INITIAL_BUDGET_STATE);
60
- const reset = markRecovered();
61
- expect(reset.attempts).toBe(0);
62
- expect(reset.degraded).toBe(false);
63
- });
64
- });