@ceralive/modem-control 1.0.0 → 1.2.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 (480) hide show
  1. package/README.md +341 -0
  2. package/dist/backend/at-lease.d.ts +59 -0
  3. package/dist/backend/at-lease.js +118 -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 +101 -0
  61. package/dist/backend/transition-preconditions.js +132 -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 +216 -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 +20 -0
  146. package/dist/index.js +27 -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 +61 -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/operation-ids.d.ts +2 -0
  196. package/dist/operation-ids.js +26 -0
  197. package/dist/operations/contracts.d.ts +72 -0
  198. package/dist/operations/contracts.js +1 -0
  199. package/dist/operations/index.d.ts +1 -0
  200. package/dist/operations/index.js +1 -0
  201. package/dist/operations/operation-engine.d.ts +16 -0
  202. package/dist/operations/operation-engine.js +194 -0
  203. package/dist/ports/index.d.ts +12 -0
  204. package/{src/ports/index.ts → dist/ports/index.js} +12 -8
  205. package/dist/ports/location.d.ts +87 -0
  206. package/dist/ports/location.js +36 -0
  207. package/dist/ports/modem-manager.d.ts +89 -0
  208. package/dist/ports/modem-manager.js +9 -0
  209. package/dist/ports/mutation-admission.d.ts +27 -0
  210. package/dist/ports/mutation-admission.js +9 -0
  211. package/dist/ports/network-manager.d.ts +68 -0
  212. package/dist/ports/network-manager.js +13 -0
  213. package/{src/ports/observation.ts → dist/ports/observation.d.ts} +16 -27
  214. package/dist/ports/observation.js +7 -0
  215. package/dist/ports/ops.d.ts +44 -0
  216. package/dist/ports/ops.js +16 -0
  217. package/{src/ports/receipts.ts → dist/ports/receipts.d.ts} +5 -27
  218. package/dist/ports/receipts.js +9 -0
  219. package/dist/ports/reconcile.d.ts +33 -0
  220. package/dist/ports/reconcile.js +200 -0
  221. package/dist/ports/resource-ownership.d.ts +29 -0
  222. package/dist/ports/resource-ownership.js +1 -0
  223. package/dist/ports/router.d.ts +19 -0
  224. package/dist/ports/router.js +7 -0
  225. package/dist/ports/sms.d.ts +64 -0
  226. package/dist/ports/sms.js +24 -0
  227. package/dist/ports/uhubctl.d.ts +6 -0
  228. package/dist/ports/uhubctl.js +1 -0
  229. package/dist/providers/contracts.d.ts +124 -0
  230. package/dist/providers/contracts.js +10 -0
  231. package/dist/providers/huawei-hilink/index.d.ts +2 -0
  232. package/dist/providers/huawei-hilink/index.js +2 -0
  233. package/dist/providers/huawei-hilink/operations.d.ts +20 -0
  234. package/dist/providers/huawei-hilink/operations.js +56 -0
  235. package/dist/providers/huawei-hilink/provider.d.ts +52 -0
  236. package/dist/providers/huawei-hilink/provider.js +76 -0
  237. package/dist/providers/huawei-hilink/runtime.d.ts +22 -0
  238. package/dist/providers/huawei-hilink/runtime.js +171 -0
  239. package/dist/providers/huawei-hilink/session.d.ts +28 -0
  240. package/dist/providers/huawei-hilink/session.js +120 -0
  241. package/dist/providers/huawei-hilink/transport.d.ts +19 -0
  242. package/dist/providers/huawei-hilink/transport.js +1 -0
  243. package/dist/providers/index.d.ts +8 -0
  244. package/dist/providers/index.js +8 -0
  245. package/dist/providers/matcher.d.ts +3 -0
  246. package/dist/providers/matcher.js +205 -0
  247. package/dist/providers/modem-manager/errors.d.ts +7 -0
  248. package/dist/providers/modem-manager/errors.js +37 -0
  249. package/dist/providers/modem-manager/generic-operations.d.ts +10 -0
  250. package/dist/providers/modem-manager/generic-operations.js +209 -0
  251. package/dist/providers/modem-manager/index.d.ts +4 -0
  252. package/dist/providers/modem-manager/index.js +4 -0
  253. package/dist/providers/modem-manager/module-operations.d.ts +21 -0
  254. package/dist/providers/modem-manager/module-operations.js +118 -0
  255. package/dist/providers/modem-manager/provider.d.ts +41 -0
  256. package/dist/providers/modem-manager/provider.js +155 -0
  257. package/dist/providers/modem-manager/runtime-composition-operation.d.ts +32 -0
  258. package/dist/providers/modem-manager/runtime-composition-operation.js +151 -0
  259. package/dist/providers/modem-manager/snapshot.d.ts +5 -0
  260. package/dist/providers/modem-manager/snapshot.js +204 -0
  261. package/dist/providers/modem-manager/types.d.ts +137 -0
  262. package/dist/providers/modem-manager/types.js +1 -0
  263. package/dist/providers/network-manager/adapter.d.ts +71 -0
  264. package/dist/providers/network-manager/adapter.js +348 -0
  265. package/dist/providers/network-manager/index.d.ts +2 -0
  266. package/dist/providers/network-manager/index.js +2 -0
  267. package/dist/providers/network-manager/types.d.ts +171 -0
  268. package/dist/providers/network-manager/types.js +77 -0
  269. package/dist/providers/registry.d.ts +13 -0
  270. package/dist/providers/registry.js +33 -0
  271. package/dist/providers/ufi-himi/index.d.ts +6 -0
  272. package/dist/providers/ufi-himi/index.js +6 -0
  273. package/dist/providers/ufi-himi/operations.d.ts +41 -0
  274. package/dist/providers/ufi-himi/operations.js +66 -0
  275. package/dist/providers/ufi-himi/prohibitions.d.ts +62 -0
  276. package/dist/providers/ufi-himi/prohibitions.js +88 -0
  277. package/dist/providers/ufi-himi/provider.d.ts +41 -0
  278. package/dist/providers/ufi-himi/provider.js +204 -0
  279. package/dist/providers/ufi-himi/qualcomm-evidence.d.ts +32 -0
  280. package/dist/providers/ufi-himi/qualcomm-evidence.js +51 -0
  281. package/dist/providers/ufi-himi/session.d.ts +46 -0
  282. package/dist/providers/ufi-himi/session.js +92 -0
  283. package/dist/providers/ufi-himi/transport.d.ts +29 -0
  284. package/dist/providers/ufi-himi/transport.js +25 -0
  285. package/dist/providers/zte-goform/index.d.ts +2 -0
  286. package/dist/providers/zte-goform/index.js +2 -0
  287. package/dist/providers/zte-goform/provider.d.ts +56 -0
  288. package/dist/providers/zte-goform/provider.js +101 -0
  289. package/dist/providers/zte-goform/session.d.ts +23 -0
  290. package/dist/providers/zte-goform/session.js +197 -0
  291. package/dist/providers/zte-goform/transport.d.ts +16 -0
  292. package/dist/providers/zte-goform/transport.js +1 -0
  293. package/dist/radio/band-truth.d.ts +50 -0
  294. package/dist/radio/band-truth.js +92 -0
  295. package/dist/radio/index.d.ts +3 -0
  296. package/dist/radio/index.js +10 -0
  297. package/dist/radio/mode-combinations.d.ts +88 -0
  298. package/dist/radio/mode-combinations.js +198 -0
  299. package/dist/radio/mode-truth.d.ts +67 -0
  300. package/dist/radio/mode-truth.js +112 -0
  301. package/dist/redact.d.ts +15 -0
  302. package/dist/redact.js +189 -0
  303. package/dist/safety/composition-root.d.ts +28 -0
  304. package/dist/safety/composition-root.js +65 -0
  305. package/dist/safety/flock-resource-ownership.d.ts +11 -0
  306. package/dist/safety/flock-resource-ownership.js +138 -0
  307. package/dist/safety/index.d.ts +2 -0
  308. package/dist/safety/index.js +2 -0
  309. package/dist/sms/dbus-messaging.d.ts +17 -0
  310. package/dist/sms/dbus-messaging.js +185 -0
  311. package/dist/sms/inbox-store.d.ts +10 -0
  312. package/dist/sms/inbox-store.js +82 -0
  313. package/dist/sms/index.d.ts +4 -0
  314. package/dist/sms/index.js +10 -0
  315. package/dist/sms/mmcli-parse.d.ts +54 -0
  316. package/dist/sms/mmcli-parse.js +224 -0
  317. package/dist/sms/normalize.d.ts +42 -0
  318. package/dist/sms/normalize.js +95 -0
  319. package/dist/testing/domain-fakes.d.ts +58 -0
  320. package/dist/testing/domain-fakes.js +98 -0
  321. package/dist/testing/index.d.ts +2 -0
  322. package/dist/testing/index.js +17 -0
  323. package/dist/testing/provider-fakes.d.ts +45 -0
  324. package/dist/testing/provider-fakes.js +64 -0
  325. package/dist/transport/calls.d.ts +9 -0
  326. package/dist/transport/calls.js +88 -0
  327. package/dist/transport/codec.d.ts +3 -0
  328. package/dist/transport/codec.js +207 -0
  329. package/dist/transport/dbus-native.d.ts +57 -0
  330. package/dist/transport/dbus-native.js +17 -0
  331. package/dist/transport/errors.d.ts +21 -0
  332. package/{src/transport/errors.ts → dist/transport/errors.js} +34 -46
  333. package/dist/transport/index.d.ts +4 -0
  334. package/dist/transport/index.js +9 -0
  335. package/dist/transport/signals.d.ts +15 -0
  336. package/dist/transport/signals.js +123 -0
  337. package/dist/transport/signature.d.ts +7 -0
  338. package/dist/transport/signature.js +94 -0
  339. package/dist/transport/transport.d.ts +2 -0
  340. package/dist/transport/transport.js +202 -0
  341. package/dist/transport/types.d.ts +61 -0
  342. package/dist/transport/types.js +19 -0
  343. package/dist/usb-mode/catalog-schema.d.ts +139 -0
  344. package/dist/usb-mode/catalog-schema.js +97 -0
  345. package/dist/usb-mode/catalog.d.ts +21 -0
  346. package/{src/usb-mode/catalog.ts → dist/usb-mode/catalog.js} +10 -32
  347. package/dist/usb-mode/certified-catalog.json +67 -0
  348. package/dist/usb-mode/index.d.ts +6 -0
  349. package/dist/usb-mode/index.js +16 -0
  350. package/dist/usb-mode/ingestion.d.ts +111 -0
  351. package/dist/usb-mode/ingestion.js +187 -0
  352. package/dist/usb-mode/promotion-review.d.ts +21 -0
  353. package/dist/usb-mode/promotion-review.js +87 -0
  354. package/dist/usb-mode/runtime-capability.d.ts +59 -0
  355. package/dist/usb-mode/runtime-capability.js +157 -0
  356. package/dist/usb-mode/usb-devices-parse.d.ts +36 -0
  357. package/dist/usb-mode/usb-devices-parse.js +157 -0
  358. package/dist/ussd/calls.d.ts +32 -0
  359. package/dist/ussd/calls.js +96 -0
  360. package/dist/ussd/index.d.ts +5 -0
  361. package/dist/ussd/index.js +12 -0
  362. package/dist/ussd/mm-ussd.d.ts +37 -0
  363. package/dist/ussd/mm-ussd.js +205 -0
  364. package/dist/ussd/refusal.d.ts +53 -0
  365. package/dist/ussd/refusal.js +154 -0
  366. package/dist/ussd/registration.d.ts +20 -0
  367. package/dist/ussd/registration.js +101 -0
  368. package/dist/ussd/session.d.ts +106 -0
  369. package/dist/ussd/session.js +163 -0
  370. package/package.json +38 -4
  371. package/src/backend/at-lease.test.ts +0 -106
  372. package/src/backend/at-lease.ts +0 -158
  373. package/src/backend/cell-info.test.ts +0 -154
  374. package/src/backend/cell-info.ts +0 -160
  375. package/src/backend/device-classifier.test.ts +0 -168
  376. package/src/backend/device-classifier.ts +0 -248
  377. package/src/backend/enrichment.ts +0 -96
  378. package/src/backend/features.test.ts +0 -162
  379. package/src/backend/features.ts +0 -179
  380. package/src/backend/identity-ladder.test.ts +0 -117
  381. package/src/backend/identity-ladder.ts +0 -221
  382. package/src/backend/identity-registry.test.ts +0 -89
  383. package/src/backend/identity-registry.ts +0 -151
  384. package/src/backend/index.ts +0 -236
  385. package/src/backend/lifecycle-interlock.ts +0 -38
  386. package/src/backend/managed-objects.ts +0 -108
  387. package/src/backend/mapping.ts +0 -160
  388. package/src/backend/mm-backend.ts +0 -191
  389. package/src/backend/mm-mutations.ts +0 -228
  390. package/src/backend/modem-actor.test.ts +0 -95
  391. package/src/backend/modem-actor.ts +0 -112
  392. package/src/backend/nm-auto-apn.ts +0 -161
  393. package/src/backend/nm-gsm-fields.ts +0 -122
  394. package/src/backend/nmcli-nm-port.ts +0 -228
  395. package/src/backend/nmcli-runner.ts +0 -52
  396. package/src/backend/observer.ts +0 -297
  397. package/src/backend/power-contract.test.ts +0 -40
  398. package/src/backend/power-contract.ts +0 -83
  399. package/src/backend/recovery-attribution.test.ts +0 -102
  400. package/src/backend/recovery-attribution.ts +0 -86
  401. package/src/backend/recovery-budget.test.ts +0 -64
  402. package/src/backend/recovery-budget.ts +0 -84
  403. package/src/backend/recovery-ladder.test.ts +0 -257
  404. package/src/backend/recovery-ladder.ts +0 -249
  405. package/src/backend/router-ethernet.test.ts +0 -71
  406. package/src/backend/router-ethernet.ts +0 -90
  407. package/src/backend/row-store.ts +0 -105
  408. package/src/backend/signal-setup.ts +0 -112
  409. package/src/backend/sim-unlock.ts +0 -193
  410. package/src/backend/transition-preconditions.ts +0 -149
  411. package/src/backend/uhubctl-power-hook.test.ts +0 -274
  412. package/src/backend/uhubctl-power-hook.ts +0 -377
  413. package/src/backend/usage/accounting.test.ts +0 -147
  414. package/src/backend/usage/accounting.ts +0 -123
  415. package/src/backend/usage/billing-cycle.test.ts +0 -62
  416. package/src/backend/usage/billing-cycle.ts +0 -45
  417. package/src/backend/usage/index.ts +0 -60
  418. package/src/backend/usage/policy-store.test.ts +0 -164
  419. package/src/backend/usage/policy-store.ts +0 -216
  420. package/src/backend/usage/policy-write.test.ts +0 -198
  421. package/src/backend/usage/policy-write.ts +0 -207
  422. package/src/backend/usage/proc-net-dev.test.ts +0 -56
  423. package/src/backend/usage/sampler.test.ts +0 -327
  424. package/src/backend/usage/sampler.ts +0 -282
  425. package/src/backend/usage/store.test.ts +0 -148
  426. package/src/backend/usage/store.ts +0 -177
  427. package/src/backend/usb-enumerator.test.ts +0 -87
  428. package/src/backend/usb-enumerator.ts +0 -181
  429. package/src/backend/usb-mode-transition.test.ts +0 -323
  430. package/src/backend/usb-mode-transition.ts +0 -253
  431. package/src/domain/brand.ts +0 -29
  432. package/src/domain/errors.ts +0 -77
  433. package/src/domain/guards.test.ts +0 -218
  434. package/src/domain/guards.ts +0 -144
  435. package/src/domain/identity.test.ts +0 -83
  436. package/src/domain/identity.ts +0 -165
  437. package/src/domain/index.ts +0 -12
  438. package/src/domain/snapshot.test.ts +0 -266
  439. package/src/domain/snapshot.ts +0 -120
  440. package/src/index.test.ts +0 -6
  441. package/src/index.ts +0 -15
  442. package/src/ports/README.md +0 -61
  443. package/src/ports/forbidden-surface.test.ts +0 -241
  444. package/src/ports/modem-manager.ts +0 -72
  445. package/src/ports/network-manager.ts +0 -87
  446. package/src/ports/ops.ts +0 -60
  447. package/src/ports/ops.type-test.ts +0 -39
  448. package/src/ports/receipts.test.ts +0 -153
  449. package/src/ports/reconcile.test.ts +0 -152
  450. package/src/ports/reconcile.ts +0 -338
  451. package/src/ports/router.ts +0 -29
  452. package/src/redact.test.ts +0 -82
  453. package/src/redact.ts +0 -73
  454. package/src/transport/README.md +0 -65
  455. package/src/transport/calls.ts +0 -113
  456. package/src/transport/characterization.test.ts +0 -260
  457. package/src/transport/codec.test.ts +0 -118
  458. package/src/transport/codec.ts +0 -240
  459. package/src/transport/conformance-python.test.ts +0 -152
  460. package/src/transport/conformance-same-lib.test.ts +0 -115
  461. package/src/transport/dbus-native-lib.d.ts +0 -19
  462. package/src/transport/dbus-native.ts +0 -85
  463. package/src/transport/index.ts +0 -30
  464. package/src/transport/no-library-leak.test.ts +0 -61
  465. package/src/transport/reliability.test.ts +0 -173
  466. package/src/transport/signals.ts +0 -150
  467. package/src/transport/signature.ts +0 -110
  468. package/src/transport/test-support/fake-service.ts +0 -168
  469. package/src/transport/test-support/independent-producer.py +0 -110
  470. package/src/transport/test-support/private-bus.ts +0 -66
  471. package/src/transport/transport.ts +0 -250
  472. package/src/transport/types.ts +0 -118
  473. package/src/usb-mode/catalog-schema.test.ts +0 -181
  474. package/src/usb-mode/catalog-schema.ts +0 -113
  475. package/src/usb-mode/certified-catalog.json +0 -67
  476. package/src/usb-mode/index.ts +0 -58
  477. package/src/usb-mode/ingestion.test.ts +0 -268
  478. package/src/usb-mode/ingestion.ts +0 -297
  479. package/src/usb-mode/promotion-review.ts +0 -117
  480. package/src/usb-mode/usb-devices-parse.ts +0 -196
@@ -0,0 +1,219 @@
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
+ import { DBUS_DESTINATION, DBUS_IFACE, DBUS_PATH, MM_BUS_NAME, MM_ROOT_PATH, OBJECT_MANAGER_IFACE, PROPERTIES_IFACE, } from './constants.js';
17
+ import { asManagedObjects } from './managed-objects.js';
18
+ import { ObservationRowStore } from './row-store.js';
19
+ export class MmDbusObserver {
20
+ #transport;
21
+ #destination;
22
+ #onEpochRefresh;
23
+ #store = new ObservationRowStore();
24
+ #listeners = new Set();
25
+ #subscriptions = [];
26
+ #currentOwner;
27
+ #started = false;
28
+ #stopped = false;
29
+ #priming = false;
30
+ #refreshDuringPrime = false;
31
+ #refreshing = false;
32
+ #refreshQueued = false;
33
+ #onDisconnected = () => this.#handleSourceGone('source-unavailable');
34
+ #onReconnected = () => {
35
+ void this.#adoptCurrentOwner();
36
+ };
37
+ constructor(options) {
38
+ this.#transport = options.transport;
39
+ this.#destination = options.destination ?? MM_BUS_NAME;
40
+ this.#onEpochRefresh = options.onEpochRefresh;
41
+ }
42
+ async start() {
43
+ if (this.#started) {
44
+ return this.#store.list();
45
+ }
46
+ this.#started = true;
47
+ this.#priming = true;
48
+ await this.#transport.connect();
49
+ this.#transport.on('disconnected', this.#onDisconnected);
50
+ this.#transport.on('reconnected', this.#onReconnected);
51
+ await this.#subscribeAll();
52
+ await this.#adoptCurrentOwner();
53
+ this.#priming = false;
54
+ if (this.#refreshDuringPrime) {
55
+ this.#refreshDuringPrime = false;
56
+ this.#scheduleRefresh();
57
+ }
58
+ return this.#store.list();
59
+ }
60
+ observe(listener) {
61
+ this.#listeners.add(listener);
62
+ return () => {
63
+ this.#listeners.delete(listener);
64
+ };
65
+ }
66
+ async stop() {
67
+ if (this.#stopped) {
68
+ return;
69
+ }
70
+ this.#stopped = true;
71
+ this.#transport.off('disconnected', this.#onDisconnected);
72
+ this.#transport.off('reconnected', this.#onReconnected);
73
+ const subs = this.#subscriptions.splice(0);
74
+ await Promise.all(subs.map((sub) => sub.unsubscribe().catch(() => undefined)));
75
+ this.#listeners.clear();
76
+ }
77
+ // ── signal subscription ────────────────────────────────────────────────────────
78
+ async #subscribeAll() {
79
+ const om = { interface: OBJECT_MANAGER_IFACE, path: MM_ROOT_PATH };
80
+ this.#subscriptions.push(await this.#transport.subscribeSignal({ ...om, member: 'InterfacesAdded' }, (event) => this.#onObjectSignal(event)), await this.#transport.subscribeSignal({ ...om, member: 'InterfacesRemoved' }, (event) => this.#onObjectSignal(event)), await this.#transport.subscribeSignal({ interface: PROPERTIES_IFACE, member: 'PropertiesChanged' }, (event) => this.#onObjectSignal(event)), await this.#transport.subscribeSignal({ interface: DBUS_IFACE, member: 'NameOwnerChanged' }, (event) => this.#onNameOwnerChanged(event)));
81
+ }
82
+ // ── epoch tracking ───────────────────────────────────────────────────────────
83
+ async #adoptCurrentOwner() {
84
+ const owner = await this.#queryOwner();
85
+ if (owner === undefined) {
86
+ this.#handleSourceGone('source-unavailable');
87
+ return;
88
+ }
89
+ this.#currentOwner = owner;
90
+ await this.#runRefresh(owner);
91
+ }
92
+ async #queryOwner() {
93
+ try {
94
+ const reply = await this.#transport.callMethod({
95
+ destination: DBUS_DESTINATION,
96
+ path: DBUS_PATH,
97
+ interface: DBUS_IFACE,
98
+ member: 'GetNameOwner',
99
+ signature: 's',
100
+ args: [MM_BUS_NAME],
101
+ });
102
+ const owner = reply.body[0];
103
+ return typeof owner === 'string' && owner.length > 0 ? owner : undefined;
104
+ }
105
+ catch {
106
+ // NameHasNoOwner (or a transient failure) → no current epoch yet.
107
+ return undefined;
108
+ }
109
+ }
110
+ #onNameOwnerChanged(event) {
111
+ if (event.body[0] !== MM_BUS_NAME) {
112
+ return;
113
+ }
114
+ const newOwner = typeof event.body[2] === 'string' ? event.body[2] : '';
115
+ if (newOwner.length === 0) {
116
+ // Owner lost — stale, never a removal.
117
+ this.#handleSourceGone('source-unavailable');
118
+ return;
119
+ }
120
+ if (newOwner === this.#currentOwner) {
121
+ return;
122
+ }
123
+ // New epoch: everything goes stale until the fresh snapshot restores it.
124
+ if (this.#store.markUnavailable('source-unavailable')) {
125
+ this.#emit();
126
+ }
127
+ this.#currentOwner = newOwner;
128
+ this.#scheduleRefresh();
129
+ }
130
+ #onObjectSignal(event) {
131
+ // Epoch guard: a signal from anyone but the current owner is an OLD-epoch
132
+ // straggler and must never drive a removal (draft §Oracle round-3 #5).
133
+ if (this.#currentOwner === undefined || event.sender !== this.#currentOwner) {
134
+ return;
135
+ }
136
+ if (this.#priming) {
137
+ this.#refreshDuringPrime = true;
138
+ return;
139
+ }
140
+ this.#scheduleRefresh();
141
+ }
142
+ // ── authoritative refresh ──────────────────────────────────────────────────────
143
+ #scheduleRefresh() {
144
+ const owner = this.#currentOwner;
145
+ if (owner === undefined || this.#stopped) {
146
+ return;
147
+ }
148
+ if (this.#refreshing) {
149
+ this.#refreshQueued = true;
150
+ return;
151
+ }
152
+ void this.#runRefresh(owner);
153
+ }
154
+ async #runRefresh(epochOwner) {
155
+ this.#refreshing = true;
156
+ try {
157
+ const reply = await this.#transport.callMethod({
158
+ destination: this.#destination,
159
+ path: MM_ROOT_PATH,
160
+ interface: OBJECT_MANAGER_IFACE,
161
+ member: 'GetManagedObjects',
162
+ });
163
+ // Late reply from a superseded epoch — discard (draft §Oracle round-3 #5).
164
+ if (this.#currentOwner !== epochOwner || this.#stopped) {
165
+ return;
166
+ }
167
+ const tree = asManagedObjects(reply.body[0]);
168
+ const rowsChanged = this.#store.reconcile(tree);
169
+ const healthChanged = this.#store.markHealthy();
170
+ this.#notifyEpochRefresh(epochOwner, tree);
171
+ if (rowsChanged || healthChanged) {
172
+ this.#emit();
173
+ }
174
+ }
175
+ catch {
176
+ if (this.#currentOwner === epochOwner && !this.#stopped) {
177
+ this.#markUnavailable('bus-error');
178
+ }
179
+ }
180
+ finally {
181
+ this.#refreshing = false;
182
+ if (this.#refreshQueued && !this.#stopped) {
183
+ this.#refreshQueued = false;
184
+ this.#scheduleRefresh();
185
+ }
186
+ }
187
+ }
188
+ // ── source-unavailable transitions ─────────────────────────────────────────────
189
+ #handleSourceGone(reason) {
190
+ this.#currentOwner = undefined;
191
+ this.#markUnavailable(reason);
192
+ }
193
+ #markUnavailable(reason) {
194
+ if (this.#store.markUnavailable(reason)) {
195
+ this.#emit();
196
+ }
197
+ }
198
+ #notifyEpochRefresh(epoch, tree) {
199
+ if (this.#onEpochRefresh === undefined) {
200
+ return;
201
+ }
202
+ try {
203
+ this.#onEpochRefresh({ epoch, tree });
204
+ }
205
+ catch {
206
+ // A consumer's hook must never break the observer's refresh loop.
207
+ }
208
+ }
209
+ #emit() {
210
+ const list = this.#store.list();
211
+ for (const listener of [...this.#listeners]) {
212
+ listener(list);
213
+ }
214
+ }
215
+ }
216
+ /** Construct an epoch-scoped ModemManager observer over an A2.4 transport. */
217
+ export function createMmDbusObserver(options) {
218
+ return new MmDbusObserver(options);
219
+ }
@@ -0,0 +1,49 @@
1
+ import type { EpochMillis } from '../domain/index.js';
2
+ /** How a board can power-cycle a modem. Only `none` is implemented in Phase A. */
3
+ export type PowerCapabilityKind = 'none' | 'gpio-cut' | 'pwrkey-pulse' | 'usb-hub-port-cycle';
4
+ /**
5
+ * The USB data mode a modem should re-enumerate into after a power cycle. A4.2 owns
6
+ * the certified USB-mode catalog; the power contract only records the operator's
7
+ * preferred post-cycle enumeration mode.
8
+ */
9
+ export type PreferredUsbMode = 'qmi' | 'mbim' | 'ecm-ncm' | 'rndis' | 'router-ethernet';
10
+ /** A board's modem power-control capability description. */
11
+ export interface PowerCapability {
12
+ readonly power: PowerCapabilityKind;
13
+ /** The board can pulse a USB-level reset on the modem's port. */
14
+ readonly usbReset?: boolean;
15
+ /** How long to wait for the modem to re-enumerate after a cycle. */
16
+ readonly enumerationTimeoutMs: number;
17
+ /** Which USB mode to prefer when the modem re-enumerates (A4.2 catalog vocabulary). */
18
+ readonly preferredUsbMode?: PreferredUsbMode;
19
+ }
20
+ /** The Phase-A capability: no board power control exists — a no-op. */
21
+ export declare const NONE_POWER_CAPABILITY: PowerCapability;
22
+ /** Outcome of asking the power hook to cycle a modem. */
23
+ export interface PowerCycleResult {
24
+ readonly status: 'applied' | 'unsupported' | 'failed';
25
+ readonly reason: string;
26
+ }
27
+ /** The minimal context handed to the power hook. */
28
+ export interface PowerCycleContext {
29
+ readonly stableKey: string;
30
+ readonly at: EpochMillis;
31
+ }
32
+ /**
33
+ * The pluggable power-cycle hook (recovery ladder rung 4). Phase A ships only the
34
+ * `none` no-op; a real GPIO / PWRKEY / USB-hub implementation is hardware-gated and
35
+ * injected later without changing the ladder.
36
+ */
37
+ export interface PowerHook {
38
+ readonly capability: PowerCapability;
39
+ cycle(context: PowerCycleContext): Promise<PowerCycleResult>;
40
+ }
41
+ /**
42
+ * Build a Phase-A power hook for `capability`. EVERY capability returns
43
+ * `unsupported`: `none` because there is nothing to cut, and every real mechanism
44
+ * because its hardware driver is not implemented yet — the contract field exists so
45
+ * a board can DECLARE the capability, but rung 4 never actuates in Phase A.
46
+ */
47
+ export declare function unsupportedPowerHook(capability: PowerCapability): PowerHook;
48
+ /** The Phase-A default power hook: describes `none` and always returns `unsupported`. */
49
+ export declare const NONE_POWER_HOOK: PowerHook;
@@ -0,0 +1,34 @@
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
+ /** The Phase-A capability: no board power control exists — a no-op. */
10
+ export const NONE_POWER_CAPABILITY = {
11
+ power: 'none',
12
+ enumerationTimeoutMs: 30_000,
13
+ };
14
+ /**
15
+ * Build a Phase-A power hook for `capability`. EVERY capability returns
16
+ * `unsupported`: `none` because there is nothing to cut, and every real mechanism
17
+ * because its hardware driver is not implemented yet — the contract field exists so
18
+ * a board can DECLARE the capability, but rung 4 never actuates in Phase A.
19
+ */
20
+ export function unsupportedPowerHook(capability) {
21
+ return {
22
+ capability,
23
+ cycle() {
24
+ return Promise.resolve({
25
+ status: 'unsupported',
26
+ reason: capability.power === 'none'
27
+ ? "power capability 'none' — no board power control exists"
28
+ : `power capability '${capability.power}' is declared but not implemented in Phase A`,
29
+ });
30
+ },
31
+ };
32
+ }
33
+ /** The Phase-A default power hook: describes `none` and always returns `unsupported`. */
34
+ export const NONE_POWER_HOOK = unsupportedPowerHook(NONE_POWER_CAPABILITY);
@@ -0,0 +1,32 @@
1
+ import type { CellularSnapshot, MmState, NmActivation, Presence, RegistrationStatus, SourceHealth } from '../domain/index.js';
2
+ /**
3
+ * The confident classification of a modem fault:
4
+ * - `modem-fault` — the modem itself is broken (only this may be disruptive).
5
+ * - `network-fault` — attached to (or refused by) the network; the modem is fine.
6
+ * - `indeterminate` — ambiguous, or observed from an unreliable source.
7
+ */
8
+ export type FaultAttribution = 'modem-fault' | 'network-fault' | 'indeterminate';
9
+ /** The narrow set of symptoms attribution reasons over. */
10
+ export interface FaultSymptoms {
11
+ readonly sourceHealth: SourceHealth;
12
+ readonly presence: Presence;
13
+ readonly mmState: MmState;
14
+ readonly registration: RegistrationStatus;
15
+ readonly nmActivation: NmActivation;
16
+ }
17
+ /**
18
+ * Classify a modem fault from observed symptoms.
19
+ *
20
+ * HARD SAFETY INVARIANT (draft §round-5 stale-forces-indeterminate): if the
21
+ * observation SOURCE is not `live` — i.e. `stale` or `sourceUnavailable`, as A3.1's
22
+ * epoch observer reports on owner loss / bus disconnect / old-epoch signals — the
23
+ * attribution is FORCED to `indeterminate`. We never guess `modem-fault` or
24
+ * `network-fault` from unreliable data. Because only a confident `modem-fault` can
25
+ * later authorise a disruptive step, forcing indeterminate here makes stale input
26
+ * strictly safe: no ladder step can fire off data we do not trust.
27
+ */
28
+ export declare function attributeFault(symptoms: FaultSymptoms): FaultAttribution;
29
+ /** Project the symptoms attribution needs out of a full snapshot. */
30
+ export declare function symptomsFromSnapshot(snapshot: CellularSnapshot): FaultSymptoms;
31
+ /** Attribute a fault directly from a snapshot (symptoms projection + classify). */
32
+ export declare function attributeSnapshot(snapshot: CellularSnapshot): FaultAttribution;
@@ -0,0 +1,57 @@
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
+ import { isRegistered } from '../domain/index.js';
9
+ /**
10
+ * Classify a modem fault from observed symptoms.
11
+ *
12
+ * HARD SAFETY INVARIANT (draft §round-5 stale-forces-indeterminate): if the
13
+ * observation SOURCE is not `live` — i.e. `stale` or `sourceUnavailable`, as A3.1's
14
+ * epoch observer reports on owner loss / bus disconnect / old-epoch signals — the
15
+ * attribution is FORCED to `indeterminate`. We never guess `modem-fault` or
16
+ * `network-fault` from unreliable data. Because only a confident `modem-fault` can
17
+ * later authorise a disruptive step, forcing indeterminate here makes stale input
18
+ * strictly safe: no ladder step can fire off data we do not trust.
19
+ */
20
+ export function attributeFault(symptoms) {
21
+ // 1. Unreliable source → never guess. (The invariant — checked first.)
22
+ if (symptoms.sourceHealth !== 'live') {
23
+ return 'indeterminate';
24
+ }
25
+ // 2. Nothing present to attribute (and nothing to act on via D-Bus).
26
+ if (symptoms.presence === 'absent') {
27
+ return 'indeterminate';
28
+ }
29
+ // 3. MM — a healthy source — reports THIS modem terminally failed → modem-fault.
30
+ if (symptoms.mmState === 'failed') {
31
+ return 'modem-fault';
32
+ }
33
+ // 4. The network refused registration → network-fault (not the modem's fault).
34
+ if (symptoms.registration === 'denied') {
35
+ return 'network-fault';
36
+ }
37
+ // 5. Registered to a network but data is not up → registered-but-no-data.
38
+ if (isRegistered(symptoms.registration) && symptoms.nmActivation !== 'activated') {
39
+ return 'network-fault';
40
+ }
41
+ // 6. Anything else is ambiguous — stay safe.
42
+ return 'indeterminate';
43
+ }
44
+ /** Project the symptoms attribution needs out of a full snapshot. */
45
+ export function symptomsFromSnapshot(snapshot) {
46
+ return {
47
+ sourceHealth: snapshot.sourceHealth,
48
+ presence: snapshot.presence,
49
+ mmState: snapshot.mmState,
50
+ registration: snapshot.registration.status,
51
+ nmActivation: snapshot.nmActivation,
52
+ };
53
+ }
54
+ /** Attribute a fault directly from a snapshot (symptoms projection + classify). */
55
+ export function attributeSnapshot(snapshot) {
56
+ return attributeFault(symptomsFromSnapshot(snapshot));
57
+ }
@@ -0,0 +1,45 @@
1
+ import type { EpochMillis } from '../domain/index.js';
2
+ /**
3
+ * Recovery budget. `maxAttempts` recovery attempts are allowed before the loop-stop
4
+ * engages; two attempts sooner than `cooldownMs` apart are refused (cooldown).
5
+ */
6
+ export interface RecoveryBudget {
7
+ readonly maxAttempts: number;
8
+ readonly cooldownMs: number;
9
+ }
10
+ /** The Phase-A default: two attempts, 30s cooldown between them. */
11
+ export declare const DEFAULT_RECOVERY_BUDGET: RecoveryBudget;
12
+ /**
13
+ * Per-modem budget bookkeeping. `degraded` is a LATCH: once the loop-stop fires it
14
+ * stays set until an explicit `markRecovered`, so a flapping modem is left degraded
15
+ * rather than retried forever.
16
+ */
17
+ export interface RecoveryBudgetState {
18
+ readonly attempts: number;
19
+ readonly degraded: boolean;
20
+ readonly lastAttemptAt?: EpochMillis;
21
+ }
22
+ /** A fresh, un-attempted budget state. */
23
+ export declare const INITIAL_BUDGET_STATE: RecoveryBudgetState;
24
+ /** The verdict on whether a recovery attempt may proceed, plus the next state. */
25
+ export type BudgetDecision = {
26
+ readonly kind: 'proceed';
27
+ readonly state: RecoveryBudgetState;
28
+ } | {
29
+ readonly kind: 'cooldown';
30
+ readonly state: RecoveryBudgetState;
31
+ readonly retryAfter: EpochMillis;
32
+ } | {
33
+ readonly kind: 'loop-stop';
34
+ readonly state: RecoveryBudgetState;
35
+ };
36
+ /**
37
+ * Decide whether a recovery attempt may proceed at `now`, returning the NEXT state:
38
+ * - already `degraded` → loop-stop (latched; zero further attempts)
39
+ * - within `cooldownMs` of last → cooldown (refused; attempts NOT incremented)
40
+ * - budget already spent → loop-stop (latch `degraded`)
41
+ * - otherwise → proceed (attempts + 1; stamp `lastAttemptAt`)
42
+ */
43
+ export declare function beginAttempt(state: RecoveryBudgetState, budget: RecoveryBudget, now: EpochMillis): BudgetDecision;
44
+ /** A successful recovery clears the counter and the degraded latch. */
45
+ export declare function markRecovered(): RecoveryBudgetState;
@@ -0,0 +1,44 @@
1
+ // Recovery budget — the bounded-attempts / cooldown / loop-stop circuit breaker.
2
+ //
3
+ // A permanently-broken modem must not be recovered forever. The budget caps how many
4
+ // attempts may fire within a cooldown window; once the cap is spent it latches the
5
+ // modem `degraded` (the loop-stop), and no further attempt is allowed until an
6
+ // explicit reset. This module is a PURE reducer: it owns no clock and mutates
7
+ // nothing — the caller supplies `now` and stores the returned next state.
8
+ /** The Phase-A default: two attempts, 30s cooldown between them. */
9
+ export const DEFAULT_RECOVERY_BUDGET = {
10
+ maxAttempts: 2,
11
+ cooldownMs: 30_000,
12
+ };
13
+ /** A fresh, un-attempted budget state. */
14
+ export const INITIAL_BUDGET_STATE = {
15
+ attempts: 0,
16
+ degraded: false,
17
+ };
18
+ /**
19
+ * Decide whether a recovery attempt may proceed at `now`, returning the NEXT state:
20
+ * - already `degraded` → loop-stop (latched; zero further attempts)
21
+ * - within `cooldownMs` of last → cooldown (refused; attempts NOT incremented)
22
+ * - budget already spent → loop-stop (latch `degraded`)
23
+ * - otherwise → proceed (attempts + 1; stamp `lastAttemptAt`)
24
+ */
25
+ export function beginAttempt(state, budget, now) {
26
+ if (state.degraded) {
27
+ return { kind: 'loop-stop', state };
28
+ }
29
+ if (state.lastAttemptAt !== undefined && now - state.lastAttemptAt < budget.cooldownMs) {
30
+ const retryAfter = (state.lastAttemptAt + budget.cooldownMs);
31
+ return { kind: 'cooldown', state, retryAfter };
32
+ }
33
+ if (state.attempts >= budget.maxAttempts) {
34
+ return { kind: 'loop-stop', state: { ...state, degraded: true } };
35
+ }
36
+ return {
37
+ kind: 'proceed',
38
+ state: { attempts: state.attempts + 1, degraded: false, lastAttemptAt: now },
39
+ };
40
+ }
41
+ /** A successful recovery clears the counter and the degraded latch. */
42
+ export function markRecovered() {
43
+ return INITIAL_BUDGET_STATE;
44
+ }
@@ -0,0 +1,94 @@
1
+ import type { DesiredRecovery, EpochMillis } from '../domain/index.js';
2
+ import type { ModemRef } from '../ports/index.js';
3
+ import { type LifecycleInterlock } from './lifecycle-interlock.js';
4
+ import type { ModemActor } from './modem-actor.js';
5
+ import { type PowerHook } from './power-contract.js';
6
+ import type { FaultAttribution } from './recovery-attribution.js';
7
+ import { type RecoveryBudget, type RecoveryBudgetState } from './recovery-budget.js';
8
+ export declare const LADDER_ORDER: readonly ['nmCycle', 'mmCycle', 'reset', 'powerCycle'];
9
+ export type RecoveryRung = (typeof LADDER_ORDER)[number];
10
+ /** Context handed to each disruptive rung. */
11
+ export interface RecoveryStepContext {
12
+ readonly stableKey: string;
13
+ readonly modem: ModemRef;
14
+ readonly at: EpochMillis;
15
+ }
16
+ /** Outcome of a disruptive rung (1–3). */
17
+ export interface StepOutcome {
18
+ readonly status: 'applied' | 'failed';
19
+ readonly reason: string;
20
+ }
21
+ /**
22
+ * The disruptive recovery actions for rungs 1–3. Each is the raw effect only — the
23
+ * ladder wraps every call in `actor.run(stableKey, …)`, so serialisation is the
24
+ * ladder's job, not the step's. Phase A injects a fake in tests; the real D-Bus / NM
25
+ * implementation (hardware-gated) is wired at the composition root.
26
+ */
27
+ export interface RecoverySteps {
28
+ /** Rung 1: NM deactivate then reactivate — an EXACT pair, never a bare deactivate. */
29
+ nmCycle(context: RecoveryStepContext): Promise<StepOutcome>;
30
+ /** Rung 2: MM disable then enable. */
31
+ mmCycle(context: RecoveryStepContext): Promise<StepOutcome>;
32
+ /** Rung 3: MM `Reset()`. */
33
+ reset(context: RecoveryStepContext): Promise<StepOutcome>;
34
+ }
35
+ export interface RecoveryStepGate {
36
+ readonly allowDisruptive: boolean;
37
+ }
38
+ /** The ladder's operational config: per-rung gates + the attempt budget. */
39
+ export interface RecoveryLadderConfig {
40
+ readonly nmCycle: RecoveryStepGate;
41
+ readonly mmCycle: RecoveryStepGate;
42
+ readonly reset: RecoveryStepGate;
43
+ readonly powerCycle: RecoveryStepGate;
44
+ readonly budget: RecoveryBudget;
45
+ }
46
+ /** Default config: every rung permitted, default budget. */
47
+ export declare const DEFAULT_LADDER_CONFIG: RecoveryLadderConfig;
48
+ export interface RecoveryStepReport {
49
+ readonly rung: RecoveryRung;
50
+ readonly status: 'applied' | 'unsupported' | 'failed' | 'skipped' | 'blocked';
51
+ readonly reason: string;
52
+ }
53
+ /** How a whole ladder invocation ended. */
54
+ export type RecoveryOutcomeKind = 'disabled' | 'not-attributed' | 'cooldown' | 'loop-stop' | 'interlock-blocked' | 'recovered' | 'exhausted';
55
+ /** The result of one ladder invocation. */
56
+ export interface RecoveryOutcome {
57
+ readonly kind: RecoveryOutcomeKind;
58
+ readonly attribution: FaultAttribution;
59
+ readonly steps: readonly RecoveryStepReport[];
60
+ readonly degraded: boolean;
61
+ readonly reason: string;
62
+ }
63
+ /** One recovery request for one modem at one instant. */
64
+ export interface RecoveryRequest {
65
+ readonly stableKey: string;
66
+ readonly modem: ModemRef;
67
+ /** The pre-computed attribution (see `attributeFault`). */
68
+ readonly attribution: FaultAttribution;
69
+ readonly now: EpochMillis;
70
+ /** Re-checked AFTER an applied rung; `true` ⇒ the modem recovered (ladder stops). */
71
+ readonly probeHealthy: () => Promise<boolean>;
72
+ }
73
+ /** Dependencies the ladder is constructed with. */
74
+ export interface RecoveryLadderDeps {
75
+ readonly actor: ModemActor;
76
+ readonly steps: RecoverySteps;
77
+ readonly powerHook?: PowerHook;
78
+ readonly interlock?: LifecycleInterlock;
79
+ readonly config?: RecoveryLadderConfig;
80
+ }
81
+ /**
82
+ * The recovery ladder. Holds per-modem budget state keyed by stable key so the
83
+ * loop-stop can latch a flapping modem `degraded` across invocations.
84
+ */
85
+ export declare class RecoveryLadder {
86
+ #private;
87
+ constructor(deps: RecoveryLadderDeps);
88
+ /** The current budget state for a modem (for observability / assertions). */
89
+ budgetStateFor(stableKey: string): RecoveryBudgetState;
90
+ /** Clear a modem's budget / degraded latch (operator un-degrade). */
91
+ clear(stableKey: string): void;
92
+ /** Run one recovery attempt. Enforces every gate before any side effect. */
93
+ run(recovery: DesiredRecovery, request: RecoveryRequest): Promise<RecoveryOutcome>;
94
+ }