@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,227 @@
1
+ // COMPATIBILITY READ PATH for the mutation journal CeraUI already writes.
2
+ //
3
+ // WHY THIS EXISTS AT ALL. CeraUI has kept a durable modem-mutation journal since
4
+ // long before this package had one, and devices in the field have those files on
5
+ // disk right now. The two shapes are genuinely different — this package's journal
6
+ // is an append-only EVENT LOG in one file, CeraUI's is a directory of per-modem
7
+ // LATEST-STATE SNAPSHOTS, one JSON document per modem, rewritten whole on every
8
+ // transition. Neither can be re-labelled into the other, so the bridge is a READER:
9
+ // it decodes CeraUI's shape into the SAME `JournalOperationRecord` model
10
+ // `recovery.ts` produces, so a consumer enumerates pending and unknown-outcome work
11
+ // across both without CeraUI having to change its file format first.
12
+ //
13
+ // NOTHING HERE WRITES. This module reads and decodes; it never rewrites, repairs,
14
+ // migrates in place, or deletes a slot. CeraUI's own reader leaves an unreadable
15
+ // slot on disk deliberately, and a second reader that "cleaned up" behind it would
16
+ // destroy the evidence CeraUI kept on purpose.
17
+ //
18
+ // THE DIRECTORY IS INJECTED, exactly like the native store's path — this module
19
+ // hardcodes no location, and the path-injection gate covers it.
20
+ //
21
+ // THE SLOT FILENAME IS A HASH, and `legacyMutationSlotName` mirrors that rule so a
22
+ // consumer can address ONE modem's slot without scanning. It is a RULE-D MIRROR of
23
+ // CeraUI's helper, never a shared import — the same relationship the support-claim
24
+ // ladder and the redaction key sets already have with their CeraUI twins.
25
+ //
26
+ // TWO MAPPING DECISIONS CARRY WEIGHT:
27
+ //
28
+ // * `armed` maps to `pending` and `executing` maps to `unknown-outcome`. They are
29
+ // not the same fact. `armed` says the pre-state was captured and the write had
30
+ // not been dispatched, so the device is untouched. `executing` says the write
31
+ // WAS dispatched and no terminal state was ever recorded — which is precisely
32
+ // what this package calls an unknown outcome. Collapsing them would either
33
+ // invent certainty about a dispatched write or manufacture doubt about one that
34
+ // never left.
35
+ //
36
+ // * `kind` is validated as a NON-EMPTY STRING, not against a frozen enum.
37
+ // CeraUI's runtime enum spreads its capability-module mutation kinds into the
38
+ // list, so the vocabulary grows on CeraUI's release cycle. Freezing a copy here
39
+ // would make this reader reject a perfectly valid file the day CeraUI adds a
40
+ // capability module — a compatibility reader that fails closed on new-but-valid
41
+ // input is worse than no reader at all.
42
+ import { createHash } from 'node:crypto';
43
+ import { readdir, readFile } from 'node:fs/promises';
44
+ import { join } from 'node:path';
45
+ import { decodeJournalDocument, journalSchema } from './codec.js';
46
+ import { summarizeJournalRecords, } from './recovery.js';
47
+ /** The `version` literal CeraUI's schema pins. */
48
+ export const LEGACY_CERAUI_JOURNAL_VERSION = 1;
49
+ /** CeraUI's cap on retained history entries per slot. */
50
+ export const LEGACY_CERAUI_HISTORY_CAP = 32;
51
+ export const LEGACY_CERAUI_MUTATION_STATES = [
52
+ 'armed',
53
+ 'executing',
54
+ 'completed',
55
+ 'failed',
56
+ 'acknowledged',
57
+ 'device-absent-quarantine',
58
+ 'decommissioned',
59
+ 'recommission-pending',
60
+ ];
61
+ export const LEGACY_CERAUI_ACK_MODES = ['verified-rollback', 'force-rebaseline'];
62
+ /**
63
+ * The slot filename CeraUI derives from a stable key.
64
+ *
65
+ * RULE-D MIRROR of CeraUI's own helper. It is a plain lowercase-hex SHA-256 of the
66
+ * key's UTF-8 bytes; the key itself never appears in the filename in plaintext.
67
+ */
68
+ export function legacyMutationSlotName(stableKey) {
69
+ return createHash('sha256').update(stableKey, 'utf8').digest('hex');
70
+ }
71
+ function parseHistoryEntry(raw) {
72
+ const source = journalSchema.record(raw, 'history[]');
73
+ const detail = journalSchema.optionalString(source, 'detail');
74
+ return {
75
+ state: journalSchema.member(source, 'state', LEGACY_CERAUI_MUTATION_STATES),
76
+ at: journalSchema.requiredNonNegativeInteger(source, 'at'),
77
+ ...(detail === undefined ? {} : { detail }),
78
+ };
79
+ }
80
+ /** Validate one slot document. Throws the codec's metadata-only schema errors. */
81
+ export function validateLegacyCeraUiEntry(raw) {
82
+ const source = journalSchema.record(raw, 'entry');
83
+ if (source.version !== LEGACY_CERAUI_JOURNAL_VERSION)
84
+ throw journalSchema.schemaVersionError();
85
+ const history = source.history;
86
+ if (!Array.isArray(history) || history.length > LEGACY_CERAUI_HISTORY_CAP) {
87
+ throw journalSchema.schemaError('history');
88
+ }
89
+ const detail = journalSchema.optionalString(source, 'detail');
90
+ const acknowledgedMode = source.acknowledgedMode === undefined
91
+ ? undefined
92
+ : journalSchema.member(source, 'acknowledgedMode', LEGACY_CERAUI_ACK_MODES);
93
+ return {
94
+ version: LEGACY_CERAUI_JOURNAL_VERSION,
95
+ stableKey: journalSchema.requiredString(source, 'stableKey'),
96
+ kind: journalSchema.requiredString(source, 'kind'),
97
+ state: journalSchema.member(source, 'state', LEGACY_CERAUI_MUTATION_STATES),
98
+ attemptId: journalSchema.requiredString(source, 'attemptId'),
99
+ startedAt: journalSchema.requiredNonNegativeInteger(source, 'startedAt'),
100
+ updatedAt: journalSchema.requiredNonNegativeInteger(source, 'updatedAt'),
101
+ preState: journalSchema.record(source.preState, 'preState'),
102
+ ...(detail === undefined ? {} : { detail }),
103
+ ...(acknowledgedMode === undefined ? {} : { acknowledgedMode }),
104
+ history: history.map(parseHistoryEntry),
105
+ };
106
+ }
107
+ /** Decode one slot document's text. Never throws; returns a typed failure. */
108
+ export function decodeLegacyCeraUiEntry(text) {
109
+ return decodeJournalDocument(text, validateLegacyCeraUiEntry);
110
+ }
111
+ const DISPOSITION_BY_STATE = {
112
+ // Pre-state captured, write never dispatched: the device is untouched.
113
+ armed: 'pending',
114
+ // Dispatched with no terminal record — this package's `unknown-outcome`.
115
+ executing: 'unknown-outcome',
116
+ completed: 'resolved',
117
+ // CeraUI's replay keeps these on disk and refuses further mutations until an
118
+ // operator acknowledges. That is a KNOWN bad ending, not an unknown one.
119
+ failed: 'blocked',
120
+ acknowledged: 'resolved',
121
+ 'device-absent-quarantine': 'blocked',
122
+ decommissioned: 'blocked',
123
+ 'recommission-pending': 'blocked',
124
+ };
125
+ /**
126
+ * The descriptor evidence a legacy entry can honestly supply.
127
+ *
128
+ * CeraUI's journal predates `OperationDescriptor`, so there is no descriptor to
129
+ * flatten. Every field below is either a fact the file actually carries (the
130
+ * mutation kind) or an explicit statement that the file carries nothing:
131
+ * `confidence: 'unknown'` rather than a borrowed default, and empty evidence
132
+ * arrays rather than invented profiles. `mutationImpact: 'write'` is a fact, not a
133
+ * guess — CeraUI's file is a MUTATION journal and records nothing else.
134
+ */
135
+ export function legacyDescriptorEvidence(kind) {
136
+ return {
137
+ descriptorId: kind,
138
+ provider: 'ceraui-legacy',
139
+ authority: 'controller',
140
+ mutationImpact: 'write',
141
+ profiles: [],
142
+ firmware: [],
143
+ confidence: 'unknown',
144
+ };
145
+ }
146
+ /** Project one decoded slot onto the shared recovery record model. */
147
+ export function legacyOperationRecord(entry) {
148
+ const disposition = DISPOSITION_BY_STATE[entry.state];
149
+ return {
150
+ operationId: entry.attemptId,
151
+ // CeraUI's `stableKey`, NOT a `PhysicalModemId` — see the note on the field.
152
+ physicalModemId: entry.stableKey,
153
+ // CeraUI's journal has no device-generation fence; 0 records "unfenced"
154
+ // rather than claiming a generation the file never carried.
155
+ generation: 0,
156
+ descriptor: legacyDescriptorEvidence(entry.kind),
157
+ disposition,
158
+ origin: 'legacy-ceraui',
159
+ startedAtMs: entry.startedAt,
160
+ updatedAtMs: entry.updatedAt,
161
+ // One slot is one attempt; CeraUI rewrites the slot rather than appending.
162
+ attempts: 1,
163
+ // `outcome` is deliberately ABSENT for every legacy record, including the
164
+ // `unknown-outcome` one. `JournalOutcome`'s unknown reason union is the
165
+ // frozen domain vocabulary — `stale-generation` / `write-reply-timed-out` /
166
+ // `write-reply-dropped` — and CeraUI's `executing` state asserts none of
167
+ // them: it says a write was dispatched and never concluded, not why. Naming
168
+ // one anyway would be the invented reading this package refuses everywhere
169
+ // else. The disposition carries the fact; the reason stays unclaimed.
170
+ };
171
+ }
172
+ /**
173
+ * Read a whole CeraUI journal directory.
174
+ *
175
+ * An unreadable or non-conforming slot is reported as damage and LEFT IN PLACE;
176
+ * every readable slot still comes back. That is the same non-truncating contract
177
+ * the native store makes, applied to a directory instead of a file.
178
+ */
179
+ export async function readLegacyCeraUiJournal(options) {
180
+ let names;
181
+ try {
182
+ names = await readdir(options.dir);
183
+ }
184
+ catch (error) {
185
+ if (error.code === 'ENOENT') {
186
+ return { recovery: summarizeJournalRecords([], []), entries: [] };
187
+ }
188
+ return {
189
+ recovery: summarizeJournalRecords([], [{ location: { kind: 'file' }, bytes: 0, failure: { code: 'unreadable' } }]),
190
+ entries: [],
191
+ };
192
+ }
193
+ const entries = [];
194
+ const damage = [];
195
+ // Sorted so a recovery report is stable across filesystems that do not order
196
+ // `readdir`; the slot names are hashes, so the order carries no other meaning.
197
+ for (const name of [...names].sort()) {
198
+ if (!name.endsWith('.json'))
199
+ continue;
200
+ let text;
201
+ try {
202
+ text = await readFile(join(options.dir, name), 'utf8');
203
+ }
204
+ catch {
205
+ damage.push({
206
+ location: { kind: 'slot', slot: name },
207
+ bytes: 0,
208
+ failure: { code: 'unreadable' },
209
+ });
210
+ continue;
211
+ }
212
+ const decoded = decodeLegacyCeraUiEntry(text);
213
+ if (decoded.ok) {
214
+ entries.push(decoded.value);
215
+ continue;
216
+ }
217
+ damage.push({
218
+ location: { kind: 'slot', slot: name },
219
+ bytes: Buffer.byteLength(text, 'utf8'),
220
+ failure: decoded.failure,
221
+ });
222
+ }
223
+ return {
224
+ recovery: summarizeJournalRecords(entries.map(legacyOperationRecord), damage),
225
+ entries,
226
+ };
227
+ }
@@ -0,0 +1,58 @@
1
+ import { DomainError } from '../domain/index.js';
2
+ import type { JournalDescriptorEvidence, JournalOutcome } from './entry.js';
3
+ import type { JournalDamageRecord, JournalReadResult } from './store.js';
4
+ export type JournalRecoveryDisposition = 'pending' | 'unknown-outcome' | 'resolved' | 'blocked';
5
+ /** Where a record came from. Also tells a consumer how to read `physicalModemId`. */
6
+ export type JournalRecordOrigin = 'native' | 'legacy-ceraui';
7
+ /**
8
+ * One operation reconstructed from the journal.
9
+ *
10
+ * `physicalModemId` is TEXT, not the branded `PhysicalModemId`, and that is
11
+ * deliberate: for a `legacy-ceraui` record it holds CeraUI's own `stableKey`,
12
+ * which is a different identity vocabulary and would be REFUSED by
13
+ * `physicalModemId()`'s constructor. Coercing it would either throw on a
14
+ * perfectly valid legacy file or launder a foreign identity into a branded type
15
+ * that promises it came from the serial / ID_PATH ladder. `origin` is what tells
16
+ * a consumer which vocabulary it is holding.
17
+ */
18
+ export interface JournalOperationRecord {
19
+ readonly operationId: string;
20
+ readonly physicalModemId: string;
21
+ readonly generation: number;
22
+ readonly descriptor: JournalDescriptorEvidence;
23
+ readonly disposition: JournalRecoveryDisposition;
24
+ readonly origin: JournalRecordOrigin;
25
+ readonly startedAtMs: number;
26
+ readonly updatedAtMs: number;
27
+ readonly attempts: number;
28
+ readonly outcome?: JournalOutcome;
29
+ }
30
+ export interface JournalRecovery {
31
+ /** Every reconstructed operation, in first-appearance order. */
32
+ readonly records: readonly JournalOperationRecord[];
33
+ readonly pending: readonly JournalOperationRecord[];
34
+ readonly unknownOutcome: readonly JournalOperationRecord[];
35
+ readonly blocked: readonly JournalOperationRecord[];
36
+ /** Distinct modem identities that must be reconciled before the next mutation. */
37
+ readonly reconciliationRequired: readonly string[];
38
+ readonly damage: readonly JournalDamageRecord[];
39
+ }
40
+ /** Raised only by `assertJournalIntact`; recovery itself always returns a report. */
41
+ export declare class JournalRecoveryError extends DomainError {
42
+ readonly name = "JournalRecoveryError";
43
+ readonly damage: readonly JournalDamageRecord[];
44
+ constructor(damage: readonly JournalDamageRecord[]);
45
+ }
46
+ /**
47
+ * Escalate a damaged journal to a throw, for a caller that wants fail-closed.
48
+ *
49
+ * Kept SEPARATE from `reconstructJournalRecovery` on purpose: recovery must be
50
+ * able to hand back the survivors even when part of the file is unreadable, so
51
+ * the decision to refuse to proceed belongs to the caller, after it has seen
52
+ * what did survive.
53
+ */
54
+ export declare function assertJournalIntact(recovery: JournalRecovery): void;
55
+ /** Fold decoded entries into per-operation records. Pure; no clock, no I/O. */
56
+ export declare function reconstructJournalRecovery(read: JournalReadResult): JournalRecovery;
57
+ /** Build the summary views over already-reconstructed records (native or legacy). */
58
+ export declare function summarizeJournalRecords(records: readonly JournalOperationRecord[], damage: readonly JournalDamageRecord[]): JournalRecovery;
@@ -0,0 +1,117 @@
1
+ // Replay: fold a journal back into "what was in flight when we died".
2
+ //
3
+ // This is the whole reason the journal exists. `operations/operation-engine.ts`
4
+ // keeps its uncertainty fence in a `Set<PhysicalModemId>` on the engine instance,
5
+ // so a process death drops it and the next mutation proceeds as if nothing were
6
+ // outstanding. Folding the journal reconstructs that set from disk.
7
+ //
8
+ // THE FOLD IS ORDER-SENSITIVE AND LAST-WRITE-WINS PER OPERATION. Entries are
9
+ // applied in file order, keyed by (physical modem, operation id, generation). A
10
+ // second `started` on a live key is a re-run and re-opens the record rather than
11
+ // being deduplicated away — the journal records attempts, and hiding a second
12
+ // attempt would make a retry loop invisible in exactly the forensics the file is
13
+ // kept for.
14
+ //
15
+ // FOUR DISPOSITIONS, AND TWO OF THEM MEAN "RECONCILE BEFORE MUTATING AGAIN":
16
+ //
17
+ // pending a start with no completion — the process died inside the
18
+ // operation, so whether the device changed is unknown.
19
+ // unknown-outcome a completion the engine itself classified unknown: a stale
20
+ // generation, or a write reply that timed out or was dropped.
21
+ // resolved a definite ending (applied / refused / failed). The engine
22
+ // treats a definite failure as definite; so does this.
23
+ // blocked a terminal state a human must clear. Native journals never
24
+ // produce it today — it exists because CeraUI's own mutation
25
+ // journal HAS such states (`failed`, quarantine, decommission)
26
+ // and `legacy-ceraui.ts` maps onto this same model. Folding
27
+ // those into `resolved` would report an operator-blocked device
28
+ // as healthy; folding them into `unknown-outcome` would claim
29
+ // uncertainty about an outcome that is actually known.
30
+ //
31
+ // `reconciliationRequired` deliberately covers `pending` + `unknown-outcome` only.
32
+ // A blocked record is a KNOWN bad ending awaiting acknowledgement, and answering
33
+ // it with a reconciliation pass would silently clear a state that exists precisely
34
+ // so it cannot be cleared silently.
35
+ import { DomainError } from '../domain/index.js';
36
+ /** Raised only by `assertJournalIntact`; recovery itself always returns a report. */
37
+ export class JournalRecoveryError extends DomainError {
38
+ name = 'JournalRecoveryError';
39
+ damage;
40
+ constructor(damage) {
41
+ super(`journal recovery found ${damage.length} damaged record(s)`);
42
+ this.damage = damage;
43
+ }
44
+ }
45
+ /**
46
+ * Escalate a damaged journal to a throw, for a caller that wants fail-closed.
47
+ *
48
+ * Kept SEPARATE from `reconstructJournalRecovery` on purpose: recovery must be
49
+ * able to hand back the survivors even when part of the file is unreadable, so
50
+ * the decision to refuse to proceed belongs to the caller, after it has seen
51
+ * what did survive.
52
+ */
53
+ export function assertJournalIntact(recovery) {
54
+ if (recovery.damage.length > 0)
55
+ throw new JournalRecoveryError(recovery.damage);
56
+ }
57
+ function keyOf(entry) {
58
+ return `${entry.physicalModemId}\u0000${entry.operationId}\u0000${entry.generation}`;
59
+ }
60
+ function dispositionOf(outcome) {
61
+ return outcome.status === 'unknown-outcome' ? 'unknown-outcome' : 'resolved';
62
+ }
63
+ /** Fold decoded entries into per-operation records. Pure; no clock, no I/O. */
64
+ export function reconstructJournalRecovery(read) {
65
+ const byKey = new Map();
66
+ const order = [];
67
+ for (const { entry } of read.entries) {
68
+ const key = keyOf(entry);
69
+ const previous = byKey.get(key);
70
+ if (previous === undefined)
71
+ order.push(key);
72
+ if (entry.phase === 'started') {
73
+ byKey.set(key, {
74
+ operationId: entry.operationId,
75
+ physicalModemId: entry.physicalModemId,
76
+ generation: entry.generation,
77
+ descriptor: entry.descriptor,
78
+ disposition: 'pending',
79
+ origin: 'native',
80
+ // The FIRST start is when this operation began; a re-run does not
81
+ // rewrite history, it increments the attempt count.
82
+ startedAtMs: previous?.startedAtMs ?? entry.recordedAtMs,
83
+ updatedAtMs: entry.recordedAtMs,
84
+ attempts: (previous?.attempts ?? 0) + 1,
85
+ });
86
+ continue;
87
+ }
88
+ byKey.set(key, {
89
+ operationId: entry.operationId,
90
+ physicalModemId: entry.physicalModemId,
91
+ generation: entry.generation,
92
+ descriptor: entry.descriptor,
93
+ disposition: dispositionOf(entry.outcome),
94
+ origin: 'native',
95
+ startedAtMs: previous?.startedAtMs ?? entry.recordedAtMs,
96
+ updatedAtMs: entry.recordedAtMs,
97
+ // A completion with no start is a journal whose head was never written;
98
+ // counting it as one attempt is more honest than reporting zero.
99
+ attempts: previous?.attempts ?? 1,
100
+ outcome: entry.outcome,
101
+ });
102
+ }
103
+ const records = order
104
+ .map((key) => byKey.get(key))
105
+ .filter((value) => value !== undefined);
106
+ return summarizeJournalRecords(records, read.damage);
107
+ }
108
+ /** Build the summary views over already-reconstructed records (native or legacy). */
109
+ export function summarizeJournalRecords(records, damage) {
110
+ const pending = records.filter((record) => record.disposition === 'pending');
111
+ const unknownOutcome = records.filter((record) => record.disposition === 'unknown-outcome');
112
+ const blocked = records.filter((record) => record.disposition === 'blocked');
113
+ const reconciliationRequired = [
114
+ ...new Set([...pending, ...unknownOutcome].map((record) => record.physicalModemId)),
115
+ ].sort();
116
+ return { records, pending, unknownOutcome, blocked, reconciliationRequired, damage };
117
+ }
@@ -0,0 +1,55 @@
1
+ import { type JournalDecodeFailure } from './codec.js';
2
+ import type { JournalEntry } from './entry.js';
3
+ /** Default file mode. `0600` for the same reason the two policy stores use it. */
4
+ export declare const JOURNAL_FILE_MODE = 384;
5
+ /** Where a damaged record was found. Line-based, slot-based, or the whole file. */
6
+ export type JournalDamageLocation = {
7
+ readonly kind: 'line';
8
+ readonly line: number;
9
+ readonly trailing: boolean;
10
+ } | {
11
+ readonly kind: 'slot';
12
+ readonly slot: string;
13
+ } | {
14
+ readonly kind: 'file';
15
+ };
16
+ /** One record that could not be read, reported rather than dropped. */
17
+ export interface JournalDamageRecord {
18
+ readonly location: JournalDamageLocation;
19
+ readonly bytes: number;
20
+ readonly failure: JournalDecodeFailure;
21
+ }
22
+ /** One decoded entry with the 1-based line it came from. */
23
+ export interface JournalLineRecord {
24
+ readonly line: number;
25
+ readonly entry: JournalEntry;
26
+ }
27
+ export interface JournalReadResult {
28
+ readonly entries: readonly JournalLineRecord[];
29
+ readonly damage: readonly JournalDamageRecord[];
30
+ }
31
+ export interface JournalStore {
32
+ /** The injected path, echoed back so a caller can report where it recovered from. */
33
+ readonly path: string;
34
+ append(entry: JournalEntry): Promise<void>;
35
+ read(): Promise<JournalReadResult>;
36
+ }
37
+ export interface FileJournalStoreOptions {
38
+ /** REQUIRED. There is no default and no fallback. */
39
+ readonly path: string;
40
+ readonly mode?: number;
41
+ }
42
+ export declare class JournalPathError extends Error {
43
+ readonly name = "JournalPathError";
44
+ constructor();
45
+ }
46
+ /**
47
+ * Decode a whole journal document.
48
+ *
49
+ * Exported because the corruption fixtures assert against it directly — proving
50
+ * the "survivors are kept" property without needing a filesystem to prove it.
51
+ * Blank lines carry no record and are skipped rather than reported: a trailing
52
+ * newline is how every line ends, so the final split member is always empty.
53
+ */
54
+ export declare function decodeJournalText(text: string): JournalReadResult;
55
+ export declare function createFileJournalStore(options: FileJournalStoreOptions): JournalStore;
@@ -0,0 +1,150 @@
1
+ // The append-only journal store. THE PATH IS INJECTED AND HAS NO DEFAULT.
2
+ //
3
+ // This package never learns where a journal lives. The embedding process owns that
4
+ // decision because it owns the filesystem contract: on a CeraLive device the
5
+ // update-surviving partition is the right home, on a bench box a scratch directory
6
+ // is, and in a test a `mkdtemp` directory is. A default here would be a policy this
7
+ // library has no standing to set, and — worse — a default is what turns "the
8
+ // embedder forgot to configure it" into "we silently wrote somewhere plausible".
9
+ // `journal-path-injection.test.ts` fails the build if an absolute path literal ever
10
+ // appears in this directory's executable source.
11
+ //
12
+ // THREE PROPERTIES THIS STORE GUARANTEES, ALL LOAD-BEARING:
13
+ //
14
+ // 1. APPEND-ONLY. There is no verb here that rewrites or truncates the file.
15
+ // A rewrite is the one operation that can lose a fact that was already
16
+ // durable, and a journal that can lose a fact answers nothing after a crash.
17
+ //
18
+ // 2. A DAMAGED RECORD NEVER DISCARDS ITS NEIGHBOURS. `read()` decodes every line
19
+ // independently and returns the survivors alongside a typed damage report.
20
+ // Stopping at the first bad line — the natural thing a `for` loop with a throw
21
+ // does — silently truncates the journal to its first corruption, which is
22
+ // exactly the failure mode this store exists to make impossible.
23
+ //
24
+ // 3. A TORN TRAILING LINE IS CLOSED BEFORE THE NEXT APPEND. A process killed
25
+ // mid-write leaves a final line with no terminator. Appending straight onto it
26
+ // would glue the new entry to the garbage and corrupt a SECOND record — one
27
+ // that was never in flight when the crash happened. The store probes the last
28
+ // byte once and emits a leading terminator when the file does not end in one,
29
+ // so the damage stays confined to the record that actually tore.
30
+ import { appendFile, chmod, mkdir, open, readFile } from 'node:fs/promises';
31
+ import { dirname } from 'node:path';
32
+ import { decodeJournalEntry, encodeJournalEntry } from './codec.js';
33
+ const NEWLINE = 0x0a;
34
+ /** Default file mode. `0600` for the same reason the two policy stores use it. */
35
+ export const JOURNAL_FILE_MODE = 0o600;
36
+ export class JournalPathError extends Error {
37
+ name = 'JournalPathError';
38
+ constructor() {
39
+ super('journal path refused: an explicit, non-empty path must be injected');
40
+ }
41
+ }
42
+ /**
43
+ * Decode a whole journal document.
44
+ *
45
+ * Exported because the corruption fixtures assert against it directly — proving
46
+ * the "survivors are kept" property without needing a filesystem to prove it.
47
+ * Blank lines carry no record and are skipped rather than reported: a trailing
48
+ * newline is how every line ends, so the final split member is always empty.
49
+ */
50
+ export function decodeJournalText(text) {
51
+ const lines = text.split('\n');
52
+ let lastPopulated = -1;
53
+ for (let index = lines.length - 1; index >= 0; index -= 1) {
54
+ if ((lines[index] ?? '').trim().length > 0) {
55
+ lastPopulated = index;
56
+ break;
57
+ }
58
+ }
59
+ const entries = [];
60
+ const damage = [];
61
+ for (const [index, raw] of lines.entries()) {
62
+ if (raw.trim().length === 0)
63
+ continue;
64
+ const decoded = decodeJournalEntry(raw);
65
+ if (decoded.ok) {
66
+ entries.push({ line: index + 1, entry: decoded.value });
67
+ continue;
68
+ }
69
+ damage.push({
70
+ location: { kind: 'line', line: index + 1, trailing: index === lastPopulated },
71
+ bytes: Buffer.byteLength(raw, 'utf8'),
72
+ failure: decoded.failure,
73
+ });
74
+ }
75
+ return { entries, damage };
76
+ }
77
+ class FileJournalStore {
78
+ path;
79
+ #mode;
80
+ #tail = Promise.resolve();
81
+ /** Undefined until the first append probes the existing file's final byte. */
82
+ #terminated;
83
+ constructor(options) {
84
+ if (typeof options.path !== 'string' || options.path.trim().length === 0) {
85
+ throw new JournalPathError();
86
+ }
87
+ this.path = options.path;
88
+ this.#mode = options.mode ?? JOURNAL_FILE_MODE;
89
+ }
90
+ append(entry) {
91
+ // Chained so two concurrent appends cannot interleave, and recovered from so
92
+ // one failed append does not poison every later one with its rejection.
93
+ const next = this.#tail.catch(() => undefined).then(() => this.#appendNow(entry));
94
+ this.#tail = next.catch(() => undefined);
95
+ return next;
96
+ }
97
+ async read() {
98
+ let text;
99
+ try {
100
+ text = await readFile(this.path, 'utf8');
101
+ }
102
+ catch (error) {
103
+ // An ABSENT journal is an empty one — nothing was ever written. Anything
104
+ // else (permissions, a directory in the way) is reported as damage: a
105
+ // journal we could not open is not evidence that no mutation was pending.
106
+ if (error.code === 'ENOENT') {
107
+ return { entries: [], damage: [] };
108
+ }
109
+ return {
110
+ entries: [],
111
+ damage: [{ location: { kind: 'file' }, bytes: 0, failure: { code: 'unreadable' } }],
112
+ };
113
+ }
114
+ return decodeJournalText(text);
115
+ }
116
+ async #appendNow(entry) {
117
+ await mkdir(dirname(this.path), { recursive: true });
118
+ this.#terminated ??= await this.#probeTerminated();
119
+ const line = `${this.#terminated ? '' : '\n'}${encodeJournalEntry(entry)}\n`;
120
+ await appendFile(this.path, line, { mode: this.#mode });
121
+ // chmod AFTER the write, not as an open flag, so the mode holds regardless of
122
+ // umask — the same reason the usage and FCC policy stores do it this way.
123
+ await chmod(this.path, this.#mode);
124
+ this.#terminated = true;
125
+ }
126
+ async #probeTerminated() {
127
+ let handle;
128
+ try {
129
+ handle = await open(this.path, 'r');
130
+ }
131
+ catch {
132
+ // No file yet: the first line starts the document, nothing to close.
133
+ return true;
134
+ }
135
+ try {
136
+ const { size } = await handle.stat();
137
+ if (size === 0)
138
+ return true;
139
+ const tail = Buffer.alloc(1);
140
+ await handle.read(tail, 0, 1, size - 1);
141
+ return tail[0] === NEWLINE;
142
+ }
143
+ finally {
144
+ await handle.close();
145
+ }
146
+ }
147
+ }
148
+ export function createFileJournalStore(options) {
149
+ return new FileJournalStore(options);
150
+ }
@@ -0,0 +1,53 @@
1
+ import type { EpochMillis } from '../domain/index.js';
2
+ import type { FixRead, GnssFix } from '../ports/location.js';
3
+ export interface GnssFixStateConfig {
4
+ /** How long acquisition may run before the state turns to an honest `no-fix`. */
5
+ readonly acquireTimeoutMs: number;
6
+ /** How long a fix stays current before it expires and is dropped. */
7
+ readonly fixTtlMs: number;
8
+ }
9
+ /**
10
+ * Defaults chosen against the fleet's own numbers: a cold GNSS start on the
11
+ * bench modems is a low-minutes affair, so 120 s is long enough to be a fair
12
+ * attempt and short enough that a missing antenna is reported inside a support
13
+ * call rather than after one. A fix older than 30 s is not "current" on a moving
14
+ * vehicle, which is the only context this display has.
15
+ */
16
+ export declare const DEFAULT_FIX_STATE_CONFIG: GnssFixStateConfig;
17
+ export type NoFixReason = 'acquire-timeout' | 'reported-no-fix' | 'fix-expired';
18
+ export type GnssFixState = {
19
+ readonly kind: 'off';
20
+ } | {
21
+ readonly kind: 'acquiring';
22
+ readonly since: EpochMillis;
23
+ } | {
24
+ readonly kind: 'no-fix';
25
+ readonly since: EpochMillis;
26
+ readonly reason: NoFixReason;
27
+ } | {
28
+ readonly kind: 'fix';
29
+ readonly fix: GnssFix;
30
+ } | {
31
+ readonly kind: 'unavailable';
32
+ readonly reason: string;
33
+ };
34
+ export type GnssFixEvent = {
35
+ readonly kind: 'gnss-enabled';
36
+ readonly at: EpochMillis;
37
+ } | {
38
+ readonly kind: 'gnss-disabled';
39
+ } | {
40
+ readonly kind: 'read';
41
+ readonly at: EpochMillis;
42
+ readonly read: FixRead;
43
+ } | {
44
+ readonly kind: 'tick';
45
+ readonly at: EpochMillis;
46
+ };
47
+ export declare const GNSS_OFF: GnssFixState;
48
+ /** A fix is reachable ONLY here, and only while the state actually holds one. */
49
+ export declare function renderableFix(state: GnssFixState): GnssFix | undefined;
50
+ /** True while a bounded wait is legitimately in progress — the only spinner state. */
51
+ export declare function isAcquiring(state: GnssFixState): boolean;
52
+ /** Pure, total transition. Every exit from `fix` drops the coordinates. */
53
+ export declare function advanceGnssFixState(state: GnssFixState, event: GnssFixEvent, config?: GnssFixStateConfig): GnssFixState;