@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
@@ -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
+ }
@@ -0,0 +1,116 @@
1
+ // The evidence-gated recovery ladder — disabled by default.
2
+ //
3
+ // A bounded four-rung ladder [1 nm-cycle: NM deactivate→reactivate exact pair; 2
4
+ // mm-cycle: MM disable→enable; 3 reset: MM Reset(); 4 power-cycle: power hook (only
5
+ // `none` → always unsupported)]. Every disruptive rung routes through A3.3's shared
6
+ // per-modem `ModemActor` (serialised behind all other disruptive ops) and consults
7
+ // the `LifecycleInterlock` first so it never disrupts a streaming modem. GATES, in
8
+ // order: recovery.enabled=false → zero steps · attribution≠modem-fault → zero steps ·
9
+ // budget spent / cooldown → zero steps · per-rung allowDisruptive · interlock.
10
+ // Disabled or un-attributed, NOT ONE side-effecting call is made.
11
+ import { ALLOW_ALL_INTERLOCK } from './lifecycle-interlock.js';
12
+ import { NONE_POWER_HOOK } from './power-contract.js';
13
+ import { beginAttempt, DEFAULT_RECOVERY_BUDGET, INITIAL_BUDGET_STATE, markRecovered, } from './recovery-budget.js';
14
+ export const LADDER_ORDER = ['nmCycle', 'mmCycle', 'reset', 'powerCycle'];
15
+ const ALLOW = { allowDisruptive: true };
16
+ /** Default config: every rung permitted, default budget. */
17
+ export const DEFAULT_LADDER_CONFIG = {
18
+ nmCycle: ALLOW,
19
+ mmCycle: ALLOW,
20
+ reset: ALLOW,
21
+ powerCycle: ALLOW,
22
+ budget: DEFAULT_RECOVERY_BUDGET,
23
+ };
24
+ /**
25
+ * The recovery ladder. Holds per-modem budget state keyed by stable key so the
26
+ * loop-stop can latch a flapping modem `degraded` across invocations.
27
+ */
28
+ export class RecoveryLadder {
29
+ #actor;
30
+ #steps;
31
+ #powerHook;
32
+ #interlock;
33
+ #config;
34
+ #states = new Map();
35
+ constructor(deps) {
36
+ this.#actor = deps.actor;
37
+ this.#steps = deps.steps;
38
+ this.#powerHook = deps.powerHook ?? NONE_POWER_HOOK;
39
+ this.#interlock = deps.interlock ?? ALLOW_ALL_INTERLOCK;
40
+ this.#config = deps.config ?? DEFAULT_LADDER_CONFIG;
41
+ }
42
+ /** The current budget state for a modem (for observability / assertions). */
43
+ budgetStateFor(stableKey) {
44
+ return this.#states.get(stableKey) ?? INITIAL_BUDGET_STATE;
45
+ }
46
+ /** Clear a modem's budget / degraded latch (operator un-degrade). */
47
+ clear(stableKey) {
48
+ this.#states.delete(stableKey);
49
+ }
50
+ /** Run one recovery attempt. Enforces every gate before any side effect. */
51
+ async run(recovery, request) {
52
+ const { stableKey, attribution } = request;
53
+ // GATE 1 — master switch. Disabled ⇒ literally zero steps, zero side effects.
54
+ if (!recovery.enabled) {
55
+ return this.#end('disabled', request, [], 'recovery disabled by policy');
56
+ }
57
+ // GATE 2 — attribution. Only a confident modem-fault may ever be disruptive.
58
+ if (attribution !== 'modem-fault') {
59
+ return this.#end('not-attributed', request, [], `attribution '${attribution}' is never disruptive`);
60
+ }
61
+ // GATE 3 — budget / cooldown / loop-stop.
62
+ const decision = beginAttempt(this.budgetStateFor(stableKey), this.#config.budget, request.now);
63
+ if (decision.kind === 'loop-stop') {
64
+ this.#states.set(stableKey, decision.state);
65
+ return this.#end('loop-stop', request, [], 'recovery budget exhausted — modem marked degraded');
66
+ }
67
+ if (decision.kind === 'cooldown') {
68
+ return this.#end('cooldown', request, [], 'within cooldown window — attempt refused');
69
+ }
70
+ this.#states.set(stableKey, decision.state);
71
+ return this.#runRungs(request);
72
+ }
73
+ async #runRungs(request) {
74
+ const { stableKey } = request;
75
+ const context = { stableKey, modem: request.modem, at: request.now };
76
+ const steps = [];
77
+ for (const rung of LADDER_ORDER) {
78
+ if (!this.#config[rung].allowDisruptive) {
79
+ steps.push({ rung, status: 'skipped', reason: 'allowDisruptive is false' });
80
+ continue;
81
+ }
82
+ // Interlock BEFORE any disruptive step — never disrupt a streaming modem.
83
+ const verdict = await this.#interlock.canDisrupt({ stableKey });
84
+ if (!verdict.allow) {
85
+ steps.push({ rung, status: 'blocked', reason: verdict.reason });
86
+ return this.#end('interlock-blocked', request, steps, `interlock blocked '${rung}': ${verdict.reason}`);
87
+ }
88
+ const report = await this.#runRung(rung, context);
89
+ steps.push(report);
90
+ if (report.status === 'applied' && (await request.probeHealthy())) {
91
+ this.#states.set(stableKey, markRecovered());
92
+ return this.#end('recovered', request, steps, `recovered at rung '${rung}'`);
93
+ }
94
+ }
95
+ return this.#end('exhausted', request, steps, 'ladder exhausted without restoring health');
96
+ }
97
+ async #runRung(rung, context) {
98
+ if (rung === 'powerCycle') {
99
+ const result = await this.#powerHook.cycle({ stableKey: context.stableKey, at: context.at });
100
+ return { rung, status: result.status, reason: result.reason };
101
+ }
102
+ // Rungs 1–3 route through the shared per-modem actor for serialisation.
103
+ const step = this.#steps[rung];
104
+ const outcome = await this.#actor.run(context.stableKey, () => step(context));
105
+ return { rung, status: outcome.status, reason: outcome.reason };
106
+ }
107
+ #end(kind, request, steps, reason) {
108
+ return {
109
+ kind,
110
+ attribution: request.attribution,
111
+ steps,
112
+ degraded: this.budgetStateFor(request.stableKey).degraded,
113
+ reason,
114
+ };
115
+ }
116
+ }
@@ -0,0 +1,19 @@
1
+ import { type EpochMillis } from '../domain/index.js';
2
+ import type { DeviceIfname, RouterPort } from '../ports/index.js';
3
+ /** Injectable probes — each defaults to a `Bun.spawn` shell-out; all advisory. */
4
+ export interface RouterEthernetProbeDeps {
5
+ readonly now?: () => EpochMillis;
6
+ /** Whether `ifname` exists and is administratively up. */
7
+ readonly checkLinkUp?: (ifname: DeviceIfname) => Promise<boolean>;
8
+ /** The DHCP-assigned default gateway on `ifname`, if any. */
9
+ readonly resolveGateway?: (ifname: DeviceIfname) => Promise<string | undefined>;
10
+ /** Whether `host` is reachable via `ifname` (ICMP). */
11
+ readonly ping?: (host: string, ifname: DeviceIfname) => Promise<boolean>;
12
+ }
13
+ /**
14
+ * Create a router-ethernet probe implementing the advisory `RouterPort`. Presence is
15
+ * link-up; health additionally reports DHCP-gateway reachability and a basic egress
16
+ * probe — all informational, never a gate. `checkHealth` swallows every error into a
17
+ * `false` field so it can never throw.
18
+ */
19
+ export declare function createRouterEthernetProbe(deps?: RouterEthernetProbeDeps): RouterPort;
@@ -0,0 +1,66 @@
1
+ // Generic router-ethernet detection — presence, DHCP-gateway reachability, and a
2
+ // basic egress-health probe for uplinks ModemManager cannot control (HiLink, RNDIS
3
+ // tether, full router firmware).
4
+ //
5
+ // EVERYTHING here is ADVISORY (ports/router.ts, matrix §1-R): health degradation is
6
+ // informational only. `checkHealth` NEVER throws and NEVER gates anything — a
7
+ // degraded router stays in the routing set; the controller merely reports what it
8
+ // observed. Each probe is an injectable seam (default `Bun.spawn` of `ip` / `ping`)
9
+ // so tests run with no real network.
10
+ import { epochMillis } from '../domain/index.js';
11
+ /** The host used for the basic egress-health probe (public DNS anycast). */
12
+ const EGRESS_PROBE_HOST = '1.1.1.1';
13
+ async function spawnSucceeds(command) {
14
+ try {
15
+ const proc = Bun.spawn([...command], { stdout: 'pipe', stderr: 'pipe' });
16
+ return (await proc.exited) === 0;
17
+ }
18
+ catch {
19
+ return false;
20
+ }
21
+ }
22
+ async function spawnOutput(command) {
23
+ try {
24
+ const proc = Bun.spawn([...command], { stdout: 'pipe', stderr: 'pipe' });
25
+ const [out, code] = await Promise.all([new Response(proc.stdout).text(), proc.exited]);
26
+ return code === 0 ? out : '';
27
+ }
28
+ catch {
29
+ return '';
30
+ }
31
+ }
32
+ function defaultCheckLinkUp(ifname) {
33
+ return spawnSucceeds(['ip', 'link', 'show', 'up', 'dev', String(ifname)]);
34
+ }
35
+ async function defaultResolveGateway(ifname) {
36
+ // `ip -o route show default dev <ifname>` → "default via 192.168.8.1 dev <ifname> …".
37
+ const out = await spawnOutput(['ip', '-o', 'route', 'show', 'default', 'dev', String(ifname)]);
38
+ const match = out.match(/default via (\S+)/);
39
+ return match?.[1];
40
+ }
41
+ function defaultPing(host, ifname) {
42
+ return spawnSucceeds(['ping', '-c', '1', '-W', '2', '-I', String(ifname), host]);
43
+ }
44
+ /**
45
+ * Create a router-ethernet probe implementing the advisory `RouterPort`. Presence is
46
+ * link-up; health additionally reports DHCP-gateway reachability and a basic egress
47
+ * probe — all informational, never a gate. `checkHealth` swallows every error into a
48
+ * `false` field so it can never throw.
49
+ */
50
+ export function createRouterEthernetProbe(deps = {}) {
51
+ const now = deps.now ?? (() => epochMillis(Date.now()));
52
+ const checkLinkUp = deps.checkLinkUp ?? defaultCheckLinkUp;
53
+ const resolveGateway = deps.resolveGateway ?? defaultResolveGateway;
54
+ const ping = deps.ping ?? defaultPing;
55
+ async function probePresence(ifname) {
56
+ return (await checkLinkUp(ifname).catch(() => false)) ? 'present' : 'absent';
57
+ }
58
+ async function checkHealth(ifname) {
59
+ const presence = await probePresence(ifname);
60
+ const gateway = presence === 'present' ? await resolveGateway(ifname).catch(() => undefined) : undefined;
61
+ const gatewayReachable = gateway !== undefined && (await ping(gateway, ifname).catch(() => false));
62
+ const egressHealthy = gatewayReachable && (await ping(EGRESS_PROBE_HOST, ifname).catch(() => false));
63
+ return { presence, gatewayReachable, egressHealthy, observedAt: now() };
64
+ }
65
+ return { probePresence, checkHealth };
66
+ }
@@ -0,0 +1,13 @@
1
+ import type { ObservationFailureReason, ObservationList } from '../ports/index.js';
2
+ import type { DecodedManagedObjects } from './managed-objects.js';
3
+ export declare class ObservationRowStore {
4
+ #private;
5
+ /** Reconcile the authoritative tree into the rows. Returns whether rows changed.
6
+ * This is the SOLE removal path — an omission from a current-epoch snapshot. */
7
+ reconcile(tree: DecodedManagedObjects): boolean;
8
+ /** Mark the source healthy after a successful reconcile. Returns whether health flipped. */
9
+ markHealthy(): boolean;
10
+ /** Flag every row stale (retained, never removed). Returns whether an emission is due. */
11
+ markUnavailable(reason: ObservationFailureReason): boolean;
12
+ list(): ObservationList;
13
+ }
@@ -0,0 +1,82 @@
1
+ // The observer's row bookkeeping — revisions, source health, and the discriminated
2
+ // list result — split out of the observer so each file stays focused.
3
+ //
4
+ // Removal lives ONLY in `reconcile`: a modem is dropped exactly when a current-epoch
5
+ // authoritative snapshot omits it. `markUnavailable` never removes — it flags rows
6
+ // stale and retains them, so the `ObservationList` failure arm always carries its rows.
7
+ import { createSnapshot, revision as makeRevision, markSourceUnavailable, } from '../domain/index.js';
8
+ import { fingerprint, mapModem, modemPaths } from './mapping.js';
9
+ export class ObservationRowStore {
10
+ #rows = new Map();
11
+ #revCounter = 0;
12
+ #sourceHealthy = false;
13
+ #failureReason = 'not-started';
14
+ /** Reconcile the authoritative tree into the rows. Returns whether rows changed.
15
+ * This is the SOLE removal path — an omission from a current-epoch snapshot. */
16
+ reconcile(tree) {
17
+ let changed = false;
18
+ const seen = new Set();
19
+ for (const path of modemPaths(tree)) {
20
+ seen.add(path);
21
+ if (this.#upsert(path, mapModem(tree, path))) {
22
+ changed = true;
23
+ }
24
+ }
25
+ for (const path of [...this.#rows.keys()]) {
26
+ if (!seen.has(path)) {
27
+ this.#rows.delete(path);
28
+ changed = true;
29
+ }
30
+ }
31
+ return changed;
32
+ }
33
+ /** Mark the source healthy after a successful reconcile. Returns whether health flipped. */
34
+ markHealthy() {
35
+ const flipped = !this.#sourceHealthy;
36
+ this.#sourceHealthy = true;
37
+ return flipped;
38
+ }
39
+ /** Flag every row stale (retained, never removed). Returns whether an emission is due. */
40
+ markUnavailable(reason) {
41
+ const wasHealthy = this.#sourceHealthy;
42
+ let changed = false;
43
+ for (const [path, row] of this.#rows) {
44
+ if (row.snapshot.sourceHealth === 'sourceUnavailable') {
45
+ continue;
46
+ }
47
+ this.#rows.set(path, {
48
+ snapshot: markSourceUnavailable(row.snapshot),
49
+ fingerprint: row.fingerprint,
50
+ });
51
+ changed = true;
52
+ }
53
+ this.#sourceHealthy = false;
54
+ this.#failureReason = reason;
55
+ return changed || wasHealthy;
56
+ }
57
+ list() {
58
+ const rows = [...this.#rows.values()].map((row) => row.snapshot);
59
+ if (this.#sourceHealthy) {
60
+ return { ok: true, rows };
61
+ }
62
+ return { ok: false, reason: this.#failureReason, rows };
63
+ }
64
+ #upsert(path, mapped) {
65
+ const fp = fingerprint(mapped);
66
+ const existing = this.#rows.get(path);
67
+ if (existing !== undefined &&
68
+ existing.fingerprint === fp &&
69
+ existing.snapshot.sourceHealth === 'live') {
70
+ return false;
71
+ }
72
+ this.#rows.set(path, {
73
+ snapshot: createSnapshot({ ...mapped, revision: this.#nextRevision() }),
74
+ fingerprint: fp,
75
+ });
76
+ return true;
77
+ }
78
+ #nextRevision() {
79
+ this.#revCounter += 1;
80
+ return makeRevision(this.#revCounter);
81
+ }
82
+ }
@@ -0,0 +1,30 @@
1
+ import type { DbusTransport } from '../transport/index.js';
2
+ import type { DecodedManagedObjects } from './managed-objects.js';
3
+ /** Whether periodic signal reporting is configured for a modem. */
4
+ export type SignalCadence = 'active' | 'unsupported' | 'unknown';
5
+ /** MM's default reporting rate is seconds; callers pass whole seconds. */
6
+ export declare const DEFAULT_SIGNAL_INTERVAL_SECONDS = 5;
7
+ export interface SignalSetupManagerOptions {
8
+ readonly transport: DbusTransport;
9
+ /** MM bus name override (defaults to `org.freedesktop.ModemManager1`). */
10
+ readonly destination?: string;
11
+ /** Reporting interval in seconds passed to `Signal.Setup`. */
12
+ readonly intervalSeconds?: number;
13
+ }
14
+ /**
15
+ * Drives `Signal.Setup` across the modem fleet, keyed to the observer's epochs. Feed
16
+ * it every `onEpochRefresh` event; it applies setup to each modem exactly once per
17
+ * epoch, re-applies to survivors on a new epoch, and never calls for an old one.
18
+ */
19
+ export declare class SignalSetupManager {
20
+ #private;
21
+ constructor(options: SignalSetupManagerOptions);
22
+ /** The last-known cadence for a modem path (`'unknown'` until first applied). */
23
+ cadenceFor(modemPath: string): SignalCadence;
24
+ /**
25
+ * Apply `Signal.Setup` for the current epoch's modems. New epoch ⇒ every survivor
26
+ * is re-applied; an already-applied (epoch, modem) is skipped. Modems absent from
27
+ * this snapshot keep their last cadence but are not re-driven.
28
+ */
29
+ applyForEpoch(epoch: string, tree: DecodedManagedObjects): void;
30
+ }