@ceralive/modem-control 1.0.0 → 1.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (480) hide show
  1. package/README.md +341 -0
  2. package/dist/backend/at-lease.d.ts +59 -0
  3. package/dist/backend/at-lease.js +118 -0
  4. package/dist/backend/cell-info.d.ts +46 -0
  5. package/dist/backend/cell-info.js +124 -0
  6. package/dist/backend/constants.d.ts +24 -0
  7. package/{src/backend/constants.ts → dist/backend/constants.js} +4 -9
  8. package/dist/backend/device-classifier.d.ts +46 -0
  9. package/dist/backend/device-classifier.js +258 -0
  10. package/dist/backend/enrichment.d.ts +28 -0
  11. package/dist/backend/enrichment.js +59 -0
  12. package/dist/backend/features.d.ts +61 -0
  13. package/dist/backend/features.js +109 -0
  14. package/dist/backend/identity-ladder.d.ts +57 -0
  15. package/dist/backend/identity-ladder.js +158 -0
  16. package/dist/backend/identity-registry.d.ts +51 -0
  17. package/dist/backend/identity-registry.js +101 -0
  18. package/dist/backend/index.d.ts +30 -0
  19. package/dist/backend/index.js +35 -0
  20. package/dist/backend/lifecycle-interlock.d.ts +25 -0
  21. package/dist/backend/lifecycle-interlock.js +18 -0
  22. package/dist/backend/managed-objects.d.ts +39 -0
  23. package/dist/backend/managed-objects.js +82 -0
  24. package/dist/backend/mapping.d.ts +11 -0
  25. package/dist/backend/mapping.js +148 -0
  26. package/dist/backend/mm-backend.d.ts +44 -0
  27. package/dist/backend/mm-backend.js +154 -0
  28. package/dist/backend/mm-location.d.ts +21 -0
  29. package/dist/backend/mm-location.js +237 -0
  30. package/dist/backend/mm-mutations.d.ts +42 -0
  31. package/dist/backend/mm-mutations.js +259 -0
  32. package/dist/backend/modem-actor.d.ts +41 -0
  33. package/dist/backend/modem-actor.js +78 -0
  34. package/dist/backend/nm-auto-apn.d.ts +54 -0
  35. package/dist/backend/nm-auto-apn.js +124 -0
  36. package/dist/backend/nm-gsm-fields.d.ts +23 -0
  37. package/dist/backend/nm-gsm-fields.js +107 -0
  38. package/dist/backend/nmcli-nm-port.d.ts +28 -0
  39. package/dist/backend/nmcli-nm-port.js +173 -0
  40. package/dist/backend/nmcli-runner.d.ts +24 -0
  41. package/dist/backend/nmcli-runner.js +35 -0
  42. package/dist/backend/observer.d.ts +37 -0
  43. package/dist/backend/observer.js +219 -0
  44. package/dist/backend/power-contract.d.ts +49 -0
  45. package/dist/backend/power-contract.js +34 -0
  46. package/dist/backend/recovery-attribution.d.ts +32 -0
  47. package/dist/backend/recovery-attribution.js +57 -0
  48. package/dist/backend/recovery-budget.d.ts +45 -0
  49. package/dist/backend/recovery-budget.js +44 -0
  50. package/dist/backend/recovery-ladder.d.ts +94 -0
  51. package/dist/backend/recovery-ladder.js +116 -0
  52. package/dist/backend/router-ethernet.d.ts +19 -0
  53. package/dist/backend/router-ethernet.js +66 -0
  54. package/dist/backend/row-store.d.ts +13 -0
  55. package/dist/backend/row-store.js +82 -0
  56. package/dist/backend/signal-setup.d.ts +30 -0
  57. package/dist/backend/signal-setup.js +92 -0
  58. package/dist/backend/sim-unlock.d.ts +13 -0
  59. package/dist/backend/sim-unlock.js +153 -0
  60. package/dist/backend/transition-preconditions.d.ts +101 -0
  61. package/dist/backend/transition-preconditions.js +132 -0
  62. package/dist/backend/usage/accounting.d.ts +39 -0
  63. package/dist/backend/usage/accounting.js +73 -0
  64. package/dist/backend/usage/billing-cycle.d.ts +11 -0
  65. package/dist/backend/usage/billing-cycle.js +39 -0
  66. package/dist/backend/usage/boot-id.d.ts +6 -0
  67. package/{src/backend/usage/boot-id.ts → dist/backend/usage/boot-id.js} +7 -7
  68. package/dist/backend/usage/index.d.ts +8 -0
  69. package/dist/backend/usage/index.js +11 -0
  70. package/dist/backend/usage/policy-store.d.ts +50 -0
  71. package/dist/backend/usage/policy-store.js +161 -0
  72. package/dist/backend/usage/policy-write.d.ts +65 -0
  73. package/dist/backend/usage/policy-write.js +112 -0
  74. package/dist/backend/usage/proc-net-dev.d.ts +18 -0
  75. package/{src/backend/usage/proc-net-dev.ts → dist/backend/usage/proc-net-dev.js} +39 -47
  76. package/dist/backend/usage/sampler.d.ts +75 -0
  77. package/dist/backend/usage/sampler.js +211 -0
  78. package/dist/backend/usage/store.d.ts +38 -0
  79. package/dist/backend/usage/store.js +126 -0
  80. package/dist/backend/usb-device-snapshot.d.ts +31 -0
  81. package/dist/backend/usb-device-snapshot.js +1 -0
  82. package/dist/backend/usb-enumerator.d.ts +21 -0
  83. package/dist/backend/usb-enumerator.js +153 -0
  84. package/dist/backend/usb-mode-transition.d.ts +29 -0
  85. package/dist/backend/usb-mode-transition.js +216 -0
  86. package/dist/band/band-names.d.ts +42 -0
  87. package/dist/band/band-names.js +150 -0
  88. package/dist/band/certification.d.ts +84 -0
  89. package/dist/band/certification.js +127 -0
  90. package/dist/band/certified-bands.json +4 -0
  91. package/dist/band/index.d.ts +2 -0
  92. package/dist/band/index.js +8 -0
  93. package/dist/capability/detect.d.ts +52 -0
  94. package/dist/capability/detect.js +86 -0
  95. package/dist/capability/five-g-preference.d.ts +104 -0
  96. package/dist/capability/five-g-preference.js +171 -0
  97. package/dist/capability/index.d.ts +3 -0
  98. package/dist/capability/index.js +10 -0
  99. package/dist/capability/support-claim.d.ts +39 -0
  100. package/dist/capability/support-claim.js +81 -0
  101. package/dist/domain/brand.d.ts +10 -0
  102. package/dist/domain/brand.js +21 -0
  103. package/dist/domain/errors.d.ts +32 -0
  104. package/dist/domain/errors.js +48 -0
  105. package/dist/domain/generation.d.ts +8 -0
  106. package/dist/domain/generation.js +12 -0
  107. package/dist/domain/guards.d.ts +6 -0
  108. package/dist/domain/guards.js +127 -0
  109. package/dist/domain/identity.d.ts +109 -0
  110. package/dist/domain/identity.js +86 -0
  111. package/dist/domain/index.d.ts +14 -0
  112. package/dist/domain/index.js +18 -0
  113. package/dist/domain/mm-enums.d.ts +12 -0
  114. package/dist/domain/mm-enums.js +139 -0
  115. package/dist/domain/modem-presentation.d.ts +10 -0
  116. package/dist/domain/modem-presentation.js +39 -0
  117. package/dist/domain/observation.d.ts +39 -0
  118. package/dist/domain/observation.js +4 -0
  119. package/dist/domain/operation.d.ts +118 -0
  120. package/dist/domain/operation.js +85 -0
  121. package/dist/domain/physical-identity.d.ts +43 -0
  122. package/dist/domain/physical-identity.js +113 -0
  123. package/{src/domain/policy.ts → dist/domain/policy.d.ts} +34 -69
  124. package/dist/domain/policy.js +38 -0
  125. package/dist/domain/shadow-divergence.d.ts +27 -0
  126. package/dist/domain/shadow-divergence.js +70 -0
  127. package/dist/domain/snapshot.d.ts +53 -0
  128. package/dist/domain/snapshot.js +75 -0
  129. package/{src/domain/state.ts → dist/domain/state.d.ts} +23 -122
  130. package/dist/domain/state.js +38 -0
  131. package/dist/fcc/coverage.d.ts +63 -0
  132. package/dist/fcc/coverage.js +102 -0
  133. package/dist/fcc/index.d.ts +3 -0
  134. package/dist/fcc/index.js +12 -0
  135. package/dist/fcc/policy-store.d.ts +40 -0
  136. package/dist/fcc/policy-store.js +146 -0
  137. package/dist/fcc/policy-write.d.ts +32 -0
  138. package/dist/fcc/policy-write.js +37 -0
  139. package/dist/hardware/hilink-protocol.d.ts +39 -0
  140. package/dist/hardware/hilink-protocol.js +58 -0
  141. package/dist/hardware/index.d.ts +3 -0
  142. package/dist/hardware/index.js +15 -0
  143. package/dist/hardware/router-parsers.d.ts +89 -0
  144. package/dist/hardware/router-parsers.js +234 -0
  145. package/dist/index.d.ts +20 -0
  146. package/dist/index.js +27 -0
  147. package/dist/journal/codec.d.ts +44 -0
  148. package/dist/journal/codec.js +198 -0
  149. package/dist/journal/engine.d.ts +28 -0
  150. package/dist/journal/engine.js +68 -0
  151. package/dist/journal/entry.d.ts +74 -0
  152. package/dist/journal/entry.js +56 -0
  153. package/dist/journal/index.d.ts +6 -0
  154. package/dist/journal/index.js +6 -0
  155. package/dist/journal/legacy-ceraui.d.ts +73 -0
  156. package/dist/journal/legacy-ceraui.js +227 -0
  157. package/dist/journal/recovery.d.ts +58 -0
  158. package/dist/journal/recovery.js +117 -0
  159. package/dist/journal/store.d.ts +55 -0
  160. package/dist/journal/store.js +150 -0
  161. package/dist/location/fix-state.d.ts +53 -0
  162. package/dist/location/fix-state.js +75 -0
  163. package/dist/location/index.d.ts +2 -0
  164. package/dist/location/index.js +8 -0
  165. package/dist/location/nmea.d.ts +10 -0
  166. package/dist/location/nmea.js +89 -0
  167. package/dist/observations/envelope.d.ts +63 -0
  168. package/dist/observations/envelope.js +77 -0
  169. package/dist/observations/freshness.d.ts +28 -0
  170. package/dist/observations/freshness.js +76 -0
  171. package/dist/observations/index.d.ts +13 -0
  172. package/dist/observations/index.js +24 -0
  173. package/dist/observations/metric.d.ts +61 -0
  174. package/dist/observations/metric.js +73 -0
  175. package/dist/observations/model.d.ts +80 -0
  176. package/dist/observations/model.js +11 -0
  177. package/dist/observations/provenance.d.ts +94 -0
  178. package/dist/observations/provenance.js +67 -0
  179. package/dist/observations/raw.d.ts +42 -0
  180. package/dist/observations/raw.js +146 -0
  181. package/dist/observations/reading.d.ts +51 -0
  182. package/dist/observations/reading.js +65 -0
  183. package/dist/observations/sources/hilink.d.ts +10 -0
  184. package/dist/observations/sources/hilink.js +84 -0
  185. package/dist/observations/sources/modemmanager.d.ts +18 -0
  186. package/dist/observations/sources/modemmanager.js +239 -0
  187. package/dist/observations/sources/router-shared.d.ts +29 -0
  188. package/dist/observations/sources/router-shared.js +68 -0
  189. package/dist/observations/sources/ufi.d.ts +10 -0
  190. package/dist/observations/sources/ufi.js +111 -0
  191. package/dist/observations/sources/zte.d.ts +7 -0
  192. package/dist/observations/sources/zte.js +71 -0
  193. package/dist/observations/state-separation.d.ts +64 -0
  194. package/dist/observations/state-separation.js +52 -0
  195. package/dist/operation-ids.d.ts +2 -0
  196. package/dist/operation-ids.js +26 -0
  197. package/dist/operations/contracts.d.ts +72 -0
  198. package/dist/operations/contracts.js +1 -0
  199. package/dist/operations/index.d.ts +1 -0
  200. package/dist/operations/index.js +1 -0
  201. package/dist/operations/operation-engine.d.ts +16 -0
  202. package/dist/operations/operation-engine.js +194 -0
  203. package/dist/ports/index.d.ts +12 -0
  204. package/{src/ports/index.ts → dist/ports/index.js} +12 -8
  205. package/dist/ports/location.d.ts +87 -0
  206. package/dist/ports/location.js +36 -0
  207. package/dist/ports/modem-manager.d.ts +89 -0
  208. package/dist/ports/modem-manager.js +9 -0
  209. package/dist/ports/mutation-admission.d.ts +27 -0
  210. package/dist/ports/mutation-admission.js +9 -0
  211. package/dist/ports/network-manager.d.ts +68 -0
  212. package/dist/ports/network-manager.js +13 -0
  213. package/{src/ports/observation.ts → dist/ports/observation.d.ts} +16 -27
  214. package/dist/ports/observation.js +7 -0
  215. package/dist/ports/ops.d.ts +44 -0
  216. package/dist/ports/ops.js +16 -0
  217. package/{src/ports/receipts.ts → dist/ports/receipts.d.ts} +5 -27
  218. package/dist/ports/receipts.js +9 -0
  219. package/dist/ports/reconcile.d.ts +33 -0
  220. package/dist/ports/reconcile.js +200 -0
  221. package/dist/ports/resource-ownership.d.ts +29 -0
  222. package/dist/ports/resource-ownership.js +1 -0
  223. package/dist/ports/router.d.ts +19 -0
  224. package/dist/ports/router.js +7 -0
  225. package/dist/ports/sms.d.ts +64 -0
  226. package/dist/ports/sms.js +24 -0
  227. package/dist/ports/uhubctl.d.ts +6 -0
  228. package/dist/ports/uhubctl.js +1 -0
  229. package/dist/providers/contracts.d.ts +124 -0
  230. package/dist/providers/contracts.js +10 -0
  231. package/dist/providers/huawei-hilink/index.d.ts +2 -0
  232. package/dist/providers/huawei-hilink/index.js +2 -0
  233. package/dist/providers/huawei-hilink/operations.d.ts +20 -0
  234. package/dist/providers/huawei-hilink/operations.js +56 -0
  235. package/dist/providers/huawei-hilink/provider.d.ts +52 -0
  236. package/dist/providers/huawei-hilink/provider.js +76 -0
  237. package/dist/providers/huawei-hilink/runtime.d.ts +22 -0
  238. package/dist/providers/huawei-hilink/runtime.js +171 -0
  239. package/dist/providers/huawei-hilink/session.d.ts +28 -0
  240. package/dist/providers/huawei-hilink/session.js +120 -0
  241. package/dist/providers/huawei-hilink/transport.d.ts +19 -0
  242. package/dist/providers/huawei-hilink/transport.js +1 -0
  243. package/dist/providers/index.d.ts +8 -0
  244. package/dist/providers/index.js +8 -0
  245. package/dist/providers/matcher.d.ts +3 -0
  246. package/dist/providers/matcher.js +205 -0
  247. package/dist/providers/modem-manager/errors.d.ts +7 -0
  248. package/dist/providers/modem-manager/errors.js +37 -0
  249. package/dist/providers/modem-manager/generic-operations.d.ts +10 -0
  250. package/dist/providers/modem-manager/generic-operations.js +209 -0
  251. package/dist/providers/modem-manager/index.d.ts +4 -0
  252. package/dist/providers/modem-manager/index.js +4 -0
  253. package/dist/providers/modem-manager/module-operations.d.ts +21 -0
  254. package/dist/providers/modem-manager/module-operations.js +118 -0
  255. package/dist/providers/modem-manager/provider.d.ts +41 -0
  256. package/dist/providers/modem-manager/provider.js +155 -0
  257. package/dist/providers/modem-manager/runtime-composition-operation.d.ts +32 -0
  258. package/dist/providers/modem-manager/runtime-composition-operation.js +151 -0
  259. package/dist/providers/modem-manager/snapshot.d.ts +5 -0
  260. package/dist/providers/modem-manager/snapshot.js +204 -0
  261. package/dist/providers/modem-manager/types.d.ts +137 -0
  262. package/dist/providers/modem-manager/types.js +1 -0
  263. package/dist/providers/network-manager/adapter.d.ts +71 -0
  264. package/dist/providers/network-manager/adapter.js +348 -0
  265. package/dist/providers/network-manager/index.d.ts +2 -0
  266. package/dist/providers/network-manager/index.js +2 -0
  267. package/dist/providers/network-manager/types.d.ts +171 -0
  268. package/dist/providers/network-manager/types.js +77 -0
  269. package/dist/providers/registry.d.ts +13 -0
  270. package/dist/providers/registry.js +33 -0
  271. package/dist/providers/ufi-himi/index.d.ts +6 -0
  272. package/dist/providers/ufi-himi/index.js +6 -0
  273. package/dist/providers/ufi-himi/operations.d.ts +41 -0
  274. package/dist/providers/ufi-himi/operations.js +66 -0
  275. package/dist/providers/ufi-himi/prohibitions.d.ts +62 -0
  276. package/dist/providers/ufi-himi/prohibitions.js +88 -0
  277. package/dist/providers/ufi-himi/provider.d.ts +41 -0
  278. package/dist/providers/ufi-himi/provider.js +204 -0
  279. package/dist/providers/ufi-himi/qualcomm-evidence.d.ts +32 -0
  280. package/dist/providers/ufi-himi/qualcomm-evidence.js +51 -0
  281. package/dist/providers/ufi-himi/session.d.ts +46 -0
  282. package/dist/providers/ufi-himi/session.js +92 -0
  283. package/dist/providers/ufi-himi/transport.d.ts +29 -0
  284. package/dist/providers/ufi-himi/transport.js +25 -0
  285. package/dist/providers/zte-goform/index.d.ts +2 -0
  286. package/dist/providers/zte-goform/index.js +2 -0
  287. package/dist/providers/zte-goform/provider.d.ts +56 -0
  288. package/dist/providers/zte-goform/provider.js +101 -0
  289. package/dist/providers/zte-goform/session.d.ts +23 -0
  290. package/dist/providers/zte-goform/session.js +197 -0
  291. package/dist/providers/zte-goform/transport.d.ts +16 -0
  292. package/dist/providers/zte-goform/transport.js +1 -0
  293. package/dist/radio/band-truth.d.ts +50 -0
  294. package/dist/radio/band-truth.js +92 -0
  295. package/dist/radio/index.d.ts +3 -0
  296. package/dist/radio/index.js +10 -0
  297. package/dist/radio/mode-combinations.d.ts +88 -0
  298. package/dist/radio/mode-combinations.js +198 -0
  299. package/dist/radio/mode-truth.d.ts +67 -0
  300. package/dist/radio/mode-truth.js +112 -0
  301. package/dist/redact.d.ts +15 -0
  302. package/dist/redact.js +189 -0
  303. package/dist/safety/composition-root.d.ts +28 -0
  304. package/dist/safety/composition-root.js +65 -0
  305. package/dist/safety/flock-resource-ownership.d.ts +11 -0
  306. package/dist/safety/flock-resource-ownership.js +138 -0
  307. package/dist/safety/index.d.ts +2 -0
  308. package/dist/safety/index.js +2 -0
  309. package/dist/sms/dbus-messaging.d.ts +17 -0
  310. package/dist/sms/dbus-messaging.js +185 -0
  311. package/dist/sms/inbox-store.d.ts +10 -0
  312. package/dist/sms/inbox-store.js +82 -0
  313. package/dist/sms/index.d.ts +4 -0
  314. package/dist/sms/index.js +10 -0
  315. package/dist/sms/mmcli-parse.d.ts +54 -0
  316. package/dist/sms/mmcli-parse.js +224 -0
  317. package/dist/sms/normalize.d.ts +42 -0
  318. package/dist/sms/normalize.js +95 -0
  319. package/dist/testing/domain-fakes.d.ts +58 -0
  320. package/dist/testing/domain-fakes.js +98 -0
  321. package/dist/testing/index.d.ts +2 -0
  322. package/dist/testing/index.js +17 -0
  323. package/dist/testing/provider-fakes.d.ts +45 -0
  324. package/dist/testing/provider-fakes.js +64 -0
  325. package/dist/transport/calls.d.ts +9 -0
  326. package/dist/transport/calls.js +88 -0
  327. package/dist/transport/codec.d.ts +3 -0
  328. package/dist/transport/codec.js +207 -0
  329. package/dist/transport/dbus-native.d.ts +57 -0
  330. package/dist/transport/dbus-native.js +17 -0
  331. package/dist/transport/errors.d.ts +21 -0
  332. package/{src/transport/errors.ts → dist/transport/errors.js} +34 -46
  333. package/dist/transport/index.d.ts +4 -0
  334. package/dist/transport/index.js +9 -0
  335. package/dist/transport/signals.d.ts +15 -0
  336. package/dist/transport/signals.js +123 -0
  337. package/dist/transport/signature.d.ts +7 -0
  338. package/dist/transport/signature.js +94 -0
  339. package/dist/transport/transport.d.ts +2 -0
  340. package/dist/transport/transport.js +202 -0
  341. package/dist/transport/types.d.ts +61 -0
  342. package/dist/transport/types.js +19 -0
  343. package/dist/usb-mode/catalog-schema.d.ts +139 -0
  344. package/dist/usb-mode/catalog-schema.js +97 -0
  345. package/dist/usb-mode/catalog.d.ts +21 -0
  346. package/{src/usb-mode/catalog.ts → dist/usb-mode/catalog.js} +10 -32
  347. package/dist/usb-mode/certified-catalog.json +67 -0
  348. package/dist/usb-mode/index.d.ts +6 -0
  349. package/dist/usb-mode/index.js +16 -0
  350. package/dist/usb-mode/ingestion.d.ts +111 -0
  351. package/dist/usb-mode/ingestion.js +187 -0
  352. package/dist/usb-mode/promotion-review.d.ts +21 -0
  353. package/dist/usb-mode/promotion-review.js +87 -0
  354. package/dist/usb-mode/runtime-capability.d.ts +59 -0
  355. package/dist/usb-mode/runtime-capability.js +157 -0
  356. package/dist/usb-mode/usb-devices-parse.d.ts +36 -0
  357. package/dist/usb-mode/usb-devices-parse.js +157 -0
  358. package/dist/ussd/calls.d.ts +32 -0
  359. package/dist/ussd/calls.js +96 -0
  360. package/dist/ussd/index.d.ts +5 -0
  361. package/dist/ussd/index.js +12 -0
  362. package/dist/ussd/mm-ussd.d.ts +37 -0
  363. package/dist/ussd/mm-ussd.js +205 -0
  364. package/dist/ussd/refusal.d.ts +53 -0
  365. package/dist/ussd/refusal.js +154 -0
  366. package/dist/ussd/registration.d.ts +20 -0
  367. package/dist/ussd/registration.js +101 -0
  368. package/dist/ussd/session.d.ts +106 -0
  369. package/dist/ussd/session.js +163 -0
  370. package/package.json +38 -4
  371. package/src/backend/at-lease.test.ts +0 -106
  372. package/src/backend/at-lease.ts +0 -158
  373. package/src/backend/cell-info.test.ts +0 -154
  374. package/src/backend/cell-info.ts +0 -160
  375. package/src/backend/device-classifier.test.ts +0 -168
  376. package/src/backend/device-classifier.ts +0 -248
  377. package/src/backend/enrichment.ts +0 -96
  378. package/src/backend/features.test.ts +0 -162
  379. package/src/backend/features.ts +0 -179
  380. package/src/backend/identity-ladder.test.ts +0 -117
  381. package/src/backend/identity-ladder.ts +0 -221
  382. package/src/backend/identity-registry.test.ts +0 -89
  383. package/src/backend/identity-registry.ts +0 -151
  384. package/src/backend/index.ts +0 -236
  385. package/src/backend/lifecycle-interlock.ts +0 -38
  386. package/src/backend/managed-objects.ts +0 -108
  387. package/src/backend/mapping.ts +0 -160
  388. package/src/backend/mm-backend.ts +0 -191
  389. package/src/backend/mm-mutations.ts +0 -228
  390. package/src/backend/modem-actor.test.ts +0 -95
  391. package/src/backend/modem-actor.ts +0 -112
  392. package/src/backend/nm-auto-apn.ts +0 -161
  393. package/src/backend/nm-gsm-fields.ts +0 -122
  394. package/src/backend/nmcli-nm-port.ts +0 -228
  395. package/src/backend/nmcli-runner.ts +0 -52
  396. package/src/backend/observer.ts +0 -297
  397. package/src/backend/power-contract.test.ts +0 -40
  398. package/src/backend/power-contract.ts +0 -83
  399. package/src/backend/recovery-attribution.test.ts +0 -102
  400. package/src/backend/recovery-attribution.ts +0 -86
  401. package/src/backend/recovery-budget.test.ts +0 -64
  402. package/src/backend/recovery-budget.ts +0 -84
  403. package/src/backend/recovery-ladder.test.ts +0 -257
  404. package/src/backend/recovery-ladder.ts +0 -249
  405. package/src/backend/router-ethernet.test.ts +0 -71
  406. package/src/backend/router-ethernet.ts +0 -90
  407. package/src/backend/row-store.ts +0 -105
  408. package/src/backend/signal-setup.ts +0 -112
  409. package/src/backend/sim-unlock.ts +0 -193
  410. package/src/backend/transition-preconditions.ts +0 -149
  411. package/src/backend/uhubctl-power-hook.test.ts +0 -274
  412. package/src/backend/uhubctl-power-hook.ts +0 -377
  413. package/src/backend/usage/accounting.test.ts +0 -147
  414. package/src/backend/usage/accounting.ts +0 -123
  415. package/src/backend/usage/billing-cycle.test.ts +0 -62
  416. package/src/backend/usage/billing-cycle.ts +0 -45
  417. package/src/backend/usage/index.ts +0 -60
  418. package/src/backend/usage/policy-store.test.ts +0 -164
  419. package/src/backend/usage/policy-store.ts +0 -216
  420. package/src/backend/usage/policy-write.test.ts +0 -198
  421. package/src/backend/usage/policy-write.ts +0 -207
  422. package/src/backend/usage/proc-net-dev.test.ts +0 -56
  423. package/src/backend/usage/sampler.test.ts +0 -327
  424. package/src/backend/usage/sampler.ts +0 -282
  425. package/src/backend/usage/store.test.ts +0 -148
  426. package/src/backend/usage/store.ts +0 -177
  427. package/src/backend/usb-enumerator.test.ts +0 -87
  428. package/src/backend/usb-enumerator.ts +0 -181
  429. package/src/backend/usb-mode-transition.test.ts +0 -323
  430. package/src/backend/usb-mode-transition.ts +0 -253
  431. package/src/domain/brand.ts +0 -29
  432. package/src/domain/errors.ts +0 -77
  433. package/src/domain/guards.test.ts +0 -218
  434. package/src/domain/guards.ts +0 -144
  435. package/src/domain/identity.test.ts +0 -83
  436. package/src/domain/identity.ts +0 -165
  437. package/src/domain/index.ts +0 -12
  438. package/src/domain/snapshot.test.ts +0 -266
  439. package/src/domain/snapshot.ts +0 -120
  440. package/src/index.test.ts +0 -6
  441. package/src/index.ts +0 -15
  442. package/src/ports/README.md +0 -61
  443. package/src/ports/forbidden-surface.test.ts +0 -241
  444. package/src/ports/modem-manager.ts +0 -72
  445. package/src/ports/network-manager.ts +0 -87
  446. package/src/ports/ops.ts +0 -60
  447. package/src/ports/ops.type-test.ts +0 -39
  448. package/src/ports/receipts.test.ts +0 -153
  449. package/src/ports/reconcile.test.ts +0 -152
  450. package/src/ports/reconcile.ts +0 -338
  451. package/src/ports/router.ts +0 -29
  452. package/src/redact.test.ts +0 -82
  453. package/src/redact.ts +0 -73
  454. package/src/transport/README.md +0 -65
  455. package/src/transport/calls.ts +0 -113
  456. package/src/transport/characterization.test.ts +0 -260
  457. package/src/transport/codec.test.ts +0 -118
  458. package/src/transport/codec.ts +0 -240
  459. package/src/transport/conformance-python.test.ts +0 -152
  460. package/src/transport/conformance-same-lib.test.ts +0 -115
  461. package/src/transport/dbus-native-lib.d.ts +0 -19
  462. package/src/transport/dbus-native.ts +0 -85
  463. package/src/transport/index.ts +0 -30
  464. package/src/transport/no-library-leak.test.ts +0 -61
  465. package/src/transport/reliability.test.ts +0 -173
  466. package/src/transport/signals.ts +0 -150
  467. package/src/transport/signature.ts +0 -110
  468. package/src/transport/test-support/fake-service.ts +0 -168
  469. package/src/transport/test-support/independent-producer.py +0 -110
  470. package/src/transport/test-support/private-bus.ts +0 -66
  471. package/src/transport/transport.ts +0 -250
  472. package/src/transport/types.ts +0 -118
  473. package/src/usb-mode/catalog-schema.test.ts +0 -181
  474. package/src/usb-mode/catalog-schema.ts +0 -113
  475. package/src/usb-mode/certified-catalog.json +0 -67
  476. package/src/usb-mode/index.ts +0 -58
  477. package/src/usb-mode/ingestion.test.ts +0 -268
  478. package/src/usb-mode/ingestion.ts +0 -297
  479. package/src/usb-mode/promotion-review.ts +0 -117
  480. package/src/usb-mode/usb-devices-parse.ts +0 -196
@@ -1,327 +0,0 @@
1
- // The sampler orchestration — ambiguous-identity pause, threshold advisory
2
- // appear/clear, rate-limited persistence + shutdown flush, reboot re-baselining, and
3
- // the queryable snapshot API the A6.1 bench CLI `usage` command consumes.
4
-
5
- import { describe, expect, test } from 'bun:test';
6
- import { type DesiredUsage, logicalSlotId } from '../../domain';
7
- import type { CounterSource } from './proc-net-dev';
8
- import { createUsageSampler, type UsageObservation } from './sampler';
9
- import { type PersistedUsage, USAGE_SCHEMA_VERSION, type UsageStore } from './store';
10
-
11
- const SLOT_A = logicalSlotId('slot-a');
12
- const SLOT_B = logicalSlotId('slot-b');
13
-
14
- class FakeCounters implements CounterSource {
15
- readonly #map = new Map<string, number>();
16
- set(ifname: string, value: number): void {
17
- this.#map.set(ifname, value);
18
- }
19
- async read(): Promise<ReadonlyMap<string, number>> {
20
- return new Map(this.#map);
21
- }
22
- }
23
-
24
- class MemStore implements UsageStore {
25
- doc: PersistedUsage | null = null;
26
- saves = 0;
27
- async load(bootId: string, nowMs: number): Promise<PersistedUsage> {
28
- return this.doc ?? { schemaVersion: USAGE_SCHEMA_VERSION, bootId, savedAtMs: nowMs, slots: [] };
29
- }
30
- async save(state: PersistedUsage): Promise<void> {
31
- this.saves += 1;
32
- this.doc = state;
33
- }
34
- }
35
-
36
- function obs(
37
- slot: string,
38
- ifname: string,
39
- usage: DesiredUsage = {},
40
- confidence: 'high' | 'medium' | 'low' = 'high',
41
- mappingGeneration = 0,
42
- ): UsageObservation {
43
- return { logicalSlotId: logicalSlotId(slot), mappingGeneration, ifname, confidence, usage };
44
- }
45
-
46
- describe('UsageSampler — snapshot API (A6.1 usage command)', () => {
47
- test('reports independent per-slot cumulative usage', async () => {
48
- const counters = new FakeCounters();
49
- const sampler = await createUsageSampler({
50
- bootId: 'boot-1',
51
- source: counters,
52
- store: new MemStore(),
53
- now: () => 1000,
54
- });
55
- counters.set('wwan0', 100);
56
- counters.set('wwan1', 5000);
57
- await sampler.sample([obs(SLOT_A, 'wwan0'), obs(SLOT_B, 'wwan1')]);
58
- counters.set('wwan0', 300);
59
- counters.set('wwan1', 5001);
60
- await sampler.sample([obs(SLOT_A, 'wwan0'), obs(SLOT_B, 'wwan1')]);
61
-
62
- const snap = sampler.snapshot();
63
- expect(snap.bootId).toBe('boot-1');
64
- expect(snap.slots).toHaveLength(2);
65
- const a = snap.slots.find((s) => s.logicalSlotId === 'slot-a');
66
- const b = snap.slots.find((s) => s.logicalSlotId === 'slot-b');
67
- expect(a?.cycleBytes).toBe(200);
68
- expect(a?.paused).toBe(false);
69
- expect(a?.thresholdExceeded).toBe(false);
70
- expect(b?.cycleBytes).toBe(1);
71
- });
72
- });
73
-
74
- describe('UsageSampler — ambiguous identity pauses sampling', () => {
75
- test('a low-confidence slot attributes nothing and reports paused', async () => {
76
- const counters = new FakeCounters();
77
- const sampler = await createUsageSampler({
78
- bootId: 'boot-1',
79
- source: counters,
80
- store: new MemStore(),
81
- now: () => 1000,
82
- });
83
- counters.set('wwan0', 100);
84
- await sampler.sample([obs(SLOT_A, 'wwan0')]);
85
- counters.set('wwan0', 400);
86
- await sampler.sample([obs(SLOT_A, 'wwan0')]);
87
- expect(sampler.snapshot().slots[0]?.cycleBytes).toBe(300);
88
-
89
- // Identity turns ambiguous — counter keeps climbing, nothing is attributed.
90
- counters.set('wwan0', 9999);
91
- await sampler.sample([obs(SLOT_A, 'wwan0', {}, 'low')]);
92
- const paused = sampler.snapshot().slots[0];
93
- expect(paused?.paused).toBe(true);
94
- expect(paused?.cycleBytes).toBe(300);
95
- });
96
- });
97
-
98
- describe('UsageSampler — threshold advisory appears and clears', () => {
99
- test('crossing thresholdBytes flags an advisory that clears on cycle rollover', async () => {
100
- const counters = new FakeCounters();
101
- let clock = Date.UTC(2024, 5, 15); // mid-June
102
- const usage: DesiredUsage = { cycleDay: 1, thresholdBytes: 1000 };
103
- const sampler = await createUsageSampler({
104
- bootId: 'boot-1',
105
- source: counters,
106
- store: new MemStore(),
107
- now: () => clock,
108
- });
109
-
110
- counters.set('wwan0', 0);
111
- await sampler.sample([obs(SLOT_A, 'wwan0', usage)]);
112
- counters.set('wwan0', 2000); // +2000 > 1000 threshold
113
- await sampler.sample([obs(SLOT_A, 'wwan0', usage)]);
114
- const exceeded = sampler.snapshot().slots[0];
115
- expect(exceeded?.cycleBytes).toBe(2000);
116
- expect(exceeded?.thresholdBytes).toBe(1000);
117
- expect(exceeded?.thresholdExceeded).toBe(true);
118
-
119
- // New billing cycle (July 1) → per-cycle total resets → advisory clears.
120
- clock = Date.UTC(2024, 6, 2);
121
- counters.set('wwan0', 2100); // +100 into the fresh cycle
122
- await sampler.sample([obs(SLOT_A, 'wwan0', usage)]);
123
- const cleared = sampler.snapshot().slots[0];
124
- expect(cleared?.cycleBytes).toBe(100);
125
- expect(cleared?.thresholdExceeded).toBe(false);
126
- });
127
- });
128
-
129
- describe('UsageSampler — rate-limited persistence + shutdown flush', () => {
130
- test('persists at most once per minute; flush forces an immediate write', async () => {
131
- const counters = new FakeCounters();
132
- const store = new MemStore();
133
- let clock = 1000;
134
- const sampler = await createUsageSampler({
135
- bootId: 'boot-1',
136
- source: counters,
137
- store,
138
- now: () => clock,
139
- persistIntervalMs: 60_000,
140
- });
141
- counters.set('wwan0', 100);
142
- await sampler.sample([obs(SLOT_A, 'wwan0')]); // t=1000, within interval → no save
143
- clock = 2000;
144
- await sampler.sample([obs(SLOT_A, 'wwan0')]); // t=2000, still within → no save
145
- expect(store.saves).toBe(0);
146
-
147
- await sampler.flush(); // shutdown hook — forces a write despite the rate limit
148
- expect(store.saves).toBe(1);
149
-
150
- clock = 62_000; // > 60s since the flush persist → next sample persists
151
- counters.set('wwan0', 150);
152
- await sampler.sample([obs(SLOT_A, 'wwan0')]);
153
- expect(store.saves).toBe(2);
154
- });
155
-
156
- test('a clean flush persists the latest cycle total', async () => {
157
- const counters = new FakeCounters();
158
- const store = new MemStore();
159
- const sampler = await createUsageSampler({
160
- bootId: 'boot-1',
161
- source: counters,
162
- store,
163
- now: () => 1000,
164
- });
165
- counters.set('wwan0', 100);
166
- await sampler.sample([obs(SLOT_A, 'wwan0')]);
167
- counters.set('wwan0', 500);
168
- await sampler.sample([obs(SLOT_A, 'wwan0')]);
169
- await sampler.flush();
170
- expect(store.doc?.slots[0]?.cycleBytes).toBe(400);
171
- expect(store.doc?.slots[0]?.lastObserved).toBe(500);
172
- });
173
- });
174
-
175
- describe('UsageSampler — reboot (new boot id) re-baselines without losing the cycle total', () => {
176
- test('same-boot reload resumes the baseline; a new boot id drops it', async () => {
177
- const counters = new FakeCounters();
178
- const store = new MemStore();
179
- const first = await createUsageSampler({
180
- bootId: 'boot-1',
181
- source: counters,
182
- store,
183
- now: () => 1000,
184
- });
185
- counters.set('wwan0', 100);
186
- await first.sample([obs(SLOT_A, 'wwan0')]);
187
- counters.set('wwan0', 300);
188
- await first.sample([obs(SLOT_A, 'wwan0')]);
189
- await first.flush();
190
- expect(store.doc?.slots[0]?.cycleBytes).toBe(200);
191
-
192
- // Same boot id → baseline resumes; counter continues from 300.
193
- const resumed = await createUsageSampler({
194
- bootId: 'boot-1',
195
- source: counters,
196
- store,
197
- now: () => 1000,
198
- });
199
- counters.set('wwan0', 350);
200
- await resumed.sample([obs(SLOT_A, 'wwan0')]);
201
- expect(resumed.snapshot().slots[0]?.cycleBytes).toBe(250);
202
- await resumed.flush();
203
-
204
- // Reboot: new boot id + counters reset to a low value. The cycle total is kept,
205
- // but the baseline is dropped so the reset is not mis-attributed.
206
- const rebooted = await createUsageSampler({
207
- bootId: 'boot-2',
208
- source: counters,
209
- store,
210
- now: () => 1000,
211
- });
212
- counters.set('wwan0', 50); // kernel counters reset on reboot
213
- await rebooted.sample([obs(SLOT_A, 'wwan0')]); // zero-delta re-baseline
214
- expect(rebooted.snapshot().slots[0]?.cycleBytes).toBe(250);
215
- counters.set('wwan0', 150);
216
- await rebooted.sample([obs(SLOT_A, 'wwan0')]);
217
- expect(rebooted.snapshot().slots[0]?.cycleBytes).toBe(350);
218
- });
219
- });
220
-
221
- describe('UsageSampler — applyUsagePolicy (the setUsagePolicy live-apply seam)', () => {
222
- const AUG_16 = Date.UTC(2026, 7, 16, 12, 0, 0);
223
-
224
- async function samplerAt(now: number) {
225
- const counters = new FakeCounters();
226
- const sampler = await createUsageSampler({
227
- bootId: 'boot-1',
228
- source: counters,
229
- store: new MemStore(),
230
- now: () => now,
231
- });
232
- return { counters, sampler };
233
- }
234
-
235
- test('a threshold-only change takes effect immediately and resets nothing', async () => {
236
- const { counters, sampler } = await samplerAt(AUG_16);
237
- counters.set('wwan0', 100);
238
- await sampler.sample([obs(SLOT_A, 'wwan0')]);
239
- counters.set('wwan0', 900);
240
- await sampler.sample([obs(SLOT_A, 'wwan0')]);
241
-
242
- const applied = sampler.applyUsagePolicy(SLOT_A, { thresholdBytes: 500 });
243
-
244
- expect(applied.cycleReset).toBe(false);
245
- const slot = sampler.snapshot().slots[0];
246
- expect(slot?.cycleBytes).toBe(800);
247
- expect(slot?.thresholdBytes).toBe(500);
248
- expect(slot?.thresholdExceeded).toBe(true);
249
- });
250
-
251
- test('a CHANGED cycle day restarts the window at zero and re-anchors it', async () => {
252
- const { counters, sampler } = await samplerAt(AUG_16);
253
- counters.set('wwan0', 100);
254
- await sampler.sample([obs(SLOT_A, 'wwan0')]);
255
- counters.set('wwan0', 900);
256
- await sampler.sample([obs(SLOT_A, 'wwan0')]);
257
- expect(sampler.snapshot().slots[0]?.cycleBytes).toBe(800);
258
-
259
- const applied = sampler.applyUsagePolicy(SLOT_A, { cycleDay: 20 });
260
-
261
- expect(applied.cycleReset).toBe(true);
262
- expect(applied.cycleStartMs).toBe(Date.UTC(2026, 6, 20));
263
- const slot = sampler.snapshot().slots[0];
264
- expect(slot?.cycleBytes).toBe(0);
265
- expect(slot?.cycleStartMs).toBe(Date.UTC(2026, 6, 20));
266
- expect(slot?.cycleDay).toBe(20);
267
- });
268
-
269
- test('the BASELINE survives the reset, so the next sample attributes no jump', async () => {
270
- const { counters, sampler } = await samplerAt(AUG_16);
271
- counters.set('wwan0', 1_000_000);
272
- await sampler.sample([obs(SLOT_A, 'wwan0')]);
273
-
274
- sampler.applyUsagePolicy(SLOT_A, { cycleDay: 20 });
275
- counters.set('wwan0', 1_000_150);
276
- await sampler.sample([obs(SLOT_A, 'wwan0', { cycleDay: 20 })]);
277
-
278
- expect(sampler.snapshot().slots[0]?.cycleBytes).toBe(150);
279
- });
280
-
281
- test('re-applying the SAME cycle day is a no-op — repeated saves never zero a window', async () => {
282
- const { counters, sampler } = await samplerAt(AUG_16);
283
- counters.set('wwan0', 100);
284
- await sampler.sample([obs(SLOT_A, 'wwan0', { cycleDay: 20 })]);
285
- counters.set('wwan0', 400);
286
- await sampler.sample([obs(SLOT_A, 'wwan0', { cycleDay: 20 })]);
287
-
288
- const applied = sampler.applyUsagePolicy(SLOT_A, { cycleDay: 20 });
289
-
290
- expect(applied.cycleReset).toBe(false);
291
- expect(sampler.snapshot().slots[0]?.cycleBytes).toBe(300);
292
- });
293
-
294
- test('an applied policy OUTRANKS a stale observation on the next sample', async () => {
295
- const { counters, sampler } = await samplerAt(AUG_16);
296
- counters.set('wwan0', 100);
297
- await sampler.sample([obs(SLOT_A, 'wwan0', { thresholdBytes: 10 })]);
298
-
299
- sampler.applyUsagePolicy(SLOT_A, { thresholdBytes: 999 });
300
- // The composition root has not rebuilt its observations yet and still
301
- // carries the OLD policy — the write must not silently revert.
302
- await sampler.sample([obs(SLOT_A, 'wwan0', { thresholdBytes: 10 })]);
303
-
304
- expect(sampler.snapshot().slots[0]?.thresholdBytes).toBe(999);
305
- });
306
-
307
- test('applying to a slot never sampled creates it without claiming any bytes', async () => {
308
- const { sampler } = await samplerAt(AUG_16);
309
-
310
- const applied = sampler.applyUsagePolicy(SLOT_B, { cycleDay: 3, thresholdBytes: 7 });
311
-
312
- expect(applied.cycleReset).toBe(false);
313
- const slot = sampler.snapshot().slots[0];
314
- expect(slot?.logicalSlotId).toBe('slot-b');
315
- expect(slot?.cycleBytes).toBe(0);
316
- expect(slot?.cycleDay).toBe(3);
317
- expect(slot?.thresholdBytes).toBe(7);
318
- });
319
-
320
- test('a slot with no policy reports no cycleDay rather than the sampler default', async () => {
321
- const { counters, sampler } = await samplerAt(AUG_16);
322
- counters.set('wwan0', 10);
323
- await sampler.sample([obs(SLOT_A, 'wwan0')]);
324
-
325
- expect(sampler.snapshot().slots[0]?.cycleDay).toBeUndefined();
326
- });
327
- });
@@ -1,282 +0,0 @@
1
- // The usage sampler — orchestrates the counter source, pure accounting reducer,
2
- // billing-cycle math, and fail-soft persistence into one internal service.
3
- //
4
- // SESSION = the kernel boot id. Interface byte counters reset to zero on reboot, so
5
- // the boot id both scopes every baseline key AND lets a reload detect a reboot: a
6
- // persisted document from a different boot keeps its per-cycle totals but drops its
7
- // baselines, so the first post-reboot sample re-baselines zero-delta.
8
- //
9
- // PERSISTENCE: state is written at most once per minute (rate-limited) plus a
10
- // `flush()` shutdown hook. A crash therefore loses AT MOST ~1 minute of unpersisted
11
- // deltas (the window since the last rate-limited write); a clean shutdown calls
12
- // `flush()` and loses effectively nothing.
13
-
14
- import { type DesiredUsage, epochMillis, type LogicalSlotId } from '../../domain';
15
- import { applySample, type BaselineKey, initialAccount, type SlotAccount } from './accounting';
16
- import { cycleStart } from './billing-cycle';
17
- import type { CounterSource } from './proc-net-dev';
18
- import type { PersistedSlot, PersistedUsage, UsageStore } from './store';
19
- import { USAGE_SCHEMA_VERSION } from './store';
20
-
21
- /** One slot's observation for a sampling pass — identity + mapping + local policy. */
22
- export interface UsageObservation {
23
- readonly logicalSlotId: LogicalSlotId;
24
- /** Bumped by A4.2 whenever the ifname-to-slot mapping changes (→ zero-delta rebase). */
25
- readonly mappingGeneration: number;
26
- readonly ifname: string;
27
- /** A3.2 identity-ladder confidence; `low` pauses sampling for this slot. */
28
- readonly confidence: 'high' | 'medium' | 'low';
29
- /** Local-controller-owned usage policy (cycle day + advisory threshold). */
30
- readonly usage: DesiredUsage;
31
- }
32
-
33
- /** A queryable per-slot usage figure — consumed by the A6.1 bench CLI `usage` command. */
34
- export interface SlotUsageSnapshot {
35
- readonly logicalSlotId: string;
36
- readonly cycleBytes: number;
37
- readonly cycleStartMs: number;
38
- readonly paused: boolean;
39
- /** The cycle day in force for this slot, when the operator set one. */
40
- readonly cycleDay?: number;
41
- readonly thresholdBytes?: number;
42
- /** Advisory-only: `cycleBytes > thresholdBytes`. Never gates the connection. */
43
- readonly thresholdExceeded: boolean;
44
- }
45
-
46
- /** The sampler's current state, per slot, at a point in time. */
47
- export interface UsageSnapshot {
48
- readonly bootId: string;
49
- readonly generatedAtMs: number;
50
- readonly slots: readonly SlotUsageSnapshot[];
51
- }
52
-
53
- export interface UsageSamplerOptions {
54
- /** The kernel boot id (see `readBootId`) — the session identity. */
55
- readonly bootId: string;
56
- readonly source: CounterSource;
57
- readonly store: UsageStore;
58
- /** Injectable clock (defaults to `Date.now`). */
59
- readonly now?: () => number;
60
- /** Minimum spacing between persists (default 60_000 ms = the ≤1-min loss bound). */
61
- readonly persistIntervalMs?: number;
62
- /** Cycle day used when a slot's policy omits `cycleDay` (default 1 = 1st of month). */
63
- readonly defaultCycleDay?: number;
64
- }
65
-
66
- const DEFAULT_PERSIST_INTERVAL_MS = 60_000;
67
- const DEFAULT_CYCLE_DAY = 1;
68
-
69
- function toPersistedSlot(logicalSlotId: string, account: SlotAccount): PersistedSlot {
70
- return {
71
- logicalSlotId,
72
- cycleBytes: account.cycleBytes,
73
- cycleStartMs: account.cycleStartMs,
74
- ...(account.key !== undefined
75
- ? { mappingGeneration: account.key.mappingGeneration, ifname: account.key.ifname }
76
- : {}),
77
- ...(account.lastObserved !== undefined ? { lastObserved: account.lastObserved } : {}),
78
- };
79
- }
80
-
81
- export class UsageSampler {
82
- readonly #bootId: string;
83
- readonly #source: CounterSource;
84
- readonly #store: UsageStore;
85
- readonly #now: () => number;
86
- readonly #persistIntervalMs: number;
87
- readonly #defaultCycleDay: number;
88
- readonly #accounts = new Map<string, SlotAccount>();
89
- readonly #policies = new Map<string, DesiredUsage>();
90
- // Policies written through `applyUsagePolicy` OUTRANK whatever an observation
91
- // carries, for the life of the process. Without this, the next `sample()` would
92
- // clobber a just-applied write with the policy the composition root happened to
93
- // build its observation from — and the operator would watch their setting
94
- // revert. The durable store is the source of truth for both, so an override and
95
- // an observation can only ever disagree inside that window.
96
- readonly #policyOverrides = new Map<string, DesiredUsage>();
97
- #lastPersistMs: number;
98
- #dirty = false;
99
-
100
- private constructor(options: UsageSamplerOptions, initial: PersistedUsage) {
101
- this.#bootId = options.bootId;
102
- this.#source = options.source;
103
- this.#store = options.store;
104
- this.#now = options.now ?? Date.now;
105
- this.#persistIntervalMs = options.persistIntervalMs ?? DEFAULT_PERSIST_INTERVAL_MS;
106
- this.#defaultCycleDay = options.defaultCycleDay ?? DEFAULT_CYCLE_DAY;
107
- this.#lastPersistMs = this.#now();
108
- this.#hydrate(initial);
109
- }
110
-
111
- /** Load persisted state (recreating a fresh file if absent/corrupt) then build the sampler. */
112
- static async create(options: UsageSamplerOptions): Promise<UsageSampler> {
113
- const now = options.now ?? Date.now;
114
- const initial = await options.store.load(options.bootId, now());
115
- return new UsageSampler(options, initial);
116
- }
117
-
118
- /** Rebuild in-memory accounts. A reboot (differing boot id) drops the baselines. */
119
- #hydrate(initial: PersistedUsage): void {
120
- const sameBoot = initial.bootId === this.#bootId;
121
- for (const slot of initial.slots) {
122
- const canResume =
123
- sameBoot &&
124
- slot.ifname !== undefined &&
125
- slot.mappingGeneration !== undefined &&
126
- slot.lastObserved !== undefined;
127
- if (canResume) {
128
- const key: BaselineKey = {
129
- logicalSlotId: slot.logicalSlotId,
130
- mappingGeneration: slot.mappingGeneration as number,
131
- ifname: slot.ifname as string,
132
- bootId: this.#bootId,
133
- };
134
- this.#accounts.set(slot.logicalSlotId, {
135
- cycleBytes: slot.cycleBytes,
136
- cycleStartMs: slot.cycleStartMs,
137
- paused: false,
138
- key,
139
- lastObserved: slot.lastObserved as number,
140
- });
141
- } else {
142
- this.#accounts.set(slot.logicalSlotId, {
143
- cycleBytes: slot.cycleBytes,
144
- cycleStartMs: slot.cycleStartMs,
145
- paused: false,
146
- });
147
- }
148
- }
149
- }
150
-
151
- /** Take one sampling pass over the current counters for the given observations. */
152
- async sample(observations: readonly UsageObservation[]): Promise<void> {
153
- const counters = await this.#source.read();
154
- const now = this.#now();
155
- for (const obs of observations) {
156
- const slotId = obs.logicalSlotId as string;
157
- const usage = this.#policyOverrides.get(slotId) ?? obs.usage;
158
- this.#policies.set(slotId, usage);
159
- const cycleDay = usage.cycleDay ?? this.#defaultCycleDay;
160
- const cycleStartMs = cycleStart(epochMillis(now), cycleDay);
161
- const current = counters.get(obs.ifname);
162
- if (current === undefined) {
163
- // No reading for this interface — ensure the slot exists, attribute nothing.
164
- if (!this.#accounts.has(slotId)) {
165
- this.#accounts.set(slotId, initialAccount(cycleStartMs));
166
- }
167
- continue;
168
- }
169
- const key: BaselineKey = {
170
- logicalSlotId: slotId,
171
- mappingGeneration: obs.mappingGeneration,
172
- ifname: obs.ifname,
173
- bootId: this.#bootId,
174
- };
175
- const next = applySample(this.#accounts.get(slotId), {
176
- key,
177
- current,
178
- confidence: obs.confidence,
179
- cycleStartMs,
180
- });
181
- this.#accounts.set(slotId, next);
182
- }
183
- this.#dirty = true;
184
- await this.#maybePersist(now);
185
- }
186
-
187
- /** Current per-slot usage — the queryable snapshot the CLI and platform read. */
188
- snapshot(): UsageSnapshot {
189
- const generatedAtMs = this.#now();
190
- const slots: SlotUsageSnapshot[] = [];
191
- for (const [slotId, account] of this.#accounts) {
192
- const policy = this.#policies.get(slotId);
193
- const thresholdBytes = policy?.thresholdBytes;
194
- slots.push({
195
- logicalSlotId: slotId,
196
- cycleBytes: account.cycleBytes,
197
- cycleStartMs: account.cycleStartMs,
198
- paused: account.paused,
199
- ...(policy?.cycleDay !== undefined ? { cycleDay: policy.cycleDay } : {}),
200
- ...(thresholdBytes !== undefined ? { thresholdBytes } : {}),
201
- thresholdExceeded: thresholdBytes !== undefined && account.cycleBytes > thresholdBytes,
202
- });
203
- }
204
- return { bootId: this.#bootId, generatedAtMs, slots };
205
- }
206
-
207
- /**
208
- * Apply an operator's usage policy to this slot immediately, without waiting
209
- * for the next sampling pass.
210
- *
211
- * A CHANGED CYCLE ANCHOR RESTARTS THE WINDOW AT ZERO, and keeps the counter
212
- * BASELINE. Those two halves are the honest answer to a question with no
213
- * truthful one: bytes already accrued were measured under the OLD window, so
214
- * carrying them into the new one over-reports it, and there is no record of
215
- * how they were distributed within it. Starting fresh states plainly that the
216
- * new window began now; keeping `lastObserved` means the next sample still
217
- * attributes only genuinely new bytes, never a jump. A threshold-only change
218
- * moves no anchor and therefore resets nothing.
219
- */
220
- applyUsagePolicy(
221
- logicalSlotId: string,
222
- usage: DesiredUsage,
223
- atMs?: number,
224
- ): {
225
- cycleStartMs: number;
226
- cycleReset: boolean;
227
- } {
228
- const now = atMs ?? this.#now();
229
- this.#policyOverrides.set(logicalSlotId, usage);
230
- this.#policies.set(logicalSlotId, usage);
231
- const cycleStartMs = cycleStart(
232
- epochMillis(now),
233
- usage.cycleDay ?? this.#defaultCycleDay,
234
- ) as number;
235
- const account = this.#accounts.get(logicalSlotId);
236
- if (account === undefined) {
237
- this.#accounts.set(logicalSlotId, initialAccount(cycleStartMs));
238
- this.#dirty = true;
239
- return { cycleStartMs, cycleReset: false };
240
- }
241
- if (account.cycleStartMs === cycleStartMs) {
242
- return { cycleStartMs, cycleReset: false };
243
- }
244
- this.#accounts.set(logicalSlotId, { ...account, cycleBytes: 0, cycleStartMs });
245
- this.#dirty = true;
246
- return { cycleStartMs, cycleReset: true };
247
- }
248
-
249
- /** Flush unpersisted state immediately — the shutdown hook (bounds loss to ≤1 min). */
250
- async flush(): Promise<void> {
251
- if (this.#dirty) {
252
- await this.#persist(this.#now());
253
- }
254
- }
255
-
256
- async #maybePersist(now: number): Promise<void> {
257
- if (now - this.#lastPersistMs >= this.#persistIntervalMs) {
258
- await this.#persist(now);
259
- }
260
- }
261
-
262
- async #persist(now: number): Promise<void> {
263
- const slots: PersistedSlot[] = [];
264
- for (const [slotId, account] of this.#accounts) {
265
- slots.push(toPersistedSlot(slotId, account));
266
- }
267
- const state: PersistedUsage = {
268
- schemaVersion: USAGE_SCHEMA_VERSION,
269
- bootId: this.#bootId,
270
- savedAtMs: now,
271
- slots,
272
- };
273
- await this.#store.save(state);
274
- this.#lastPersistMs = now;
275
- this.#dirty = false;
276
- }
277
- }
278
-
279
- /** Load persisted state and build a ready sampler. See `UsageSampler.create`. */
280
- export function createUsageSampler(options: UsageSamplerOptions): Promise<UsageSampler> {
281
- return UsageSampler.create(options);
282
- }