@ceralive/modem-control 1.0.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 (474) 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 -236
  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/uhubctl-power-hook.test.ts +0 -274
  406. package/src/backend/uhubctl-power-hook.ts +0 -377
  407. package/src/backend/usage/accounting.test.ts +0 -147
  408. package/src/backend/usage/accounting.ts +0 -123
  409. package/src/backend/usage/billing-cycle.test.ts +0 -62
  410. package/src/backend/usage/billing-cycle.ts +0 -45
  411. package/src/backend/usage/index.ts +0 -60
  412. package/src/backend/usage/policy-store.test.ts +0 -164
  413. package/src/backend/usage/policy-store.ts +0 -216
  414. package/src/backend/usage/policy-write.test.ts +0 -198
  415. package/src/backend/usage/policy-write.ts +0 -207
  416. package/src/backend/usage/proc-net-dev.test.ts +0 -56
  417. package/src/backend/usage/sampler.test.ts +0 -327
  418. package/src/backend/usage/sampler.ts +0 -282
  419. package/src/backend/usage/store.test.ts +0 -148
  420. package/src/backend/usage/store.ts +0 -177
  421. package/src/backend/usb-enumerator.test.ts +0 -87
  422. package/src/backend/usb-enumerator.ts +0 -181
  423. package/src/backend/usb-mode-transition.test.ts +0 -323
  424. package/src/backend/usb-mode-transition.ts +0 -253
  425. package/src/domain/brand.ts +0 -29
  426. package/src/domain/errors.ts +0 -77
  427. package/src/domain/guards.test.ts +0 -218
  428. package/src/domain/guards.ts +0 -144
  429. package/src/domain/identity.test.ts +0 -83
  430. package/src/domain/identity.ts +0 -165
  431. package/src/domain/index.ts +0 -12
  432. package/src/domain/snapshot.test.ts +0 -266
  433. package/src/domain/snapshot.ts +0 -120
  434. package/src/index.test.ts +0 -6
  435. package/src/index.ts +0 -15
  436. package/src/ports/README.md +0 -61
  437. package/src/ports/forbidden-surface.test.ts +0 -241
  438. package/src/ports/modem-manager.ts +0 -72
  439. package/src/ports/network-manager.ts +0 -87
  440. package/src/ports/ops.ts +0 -60
  441. package/src/ports/ops.type-test.ts +0 -39
  442. package/src/ports/receipts.test.ts +0 -153
  443. package/src/ports/reconcile.test.ts +0 -152
  444. package/src/ports/reconcile.ts +0 -338
  445. package/src/ports/router.ts +0 -29
  446. package/src/redact.test.ts +0 -82
  447. package/src/redact.ts +0 -73
  448. package/src/transport/README.md +0 -65
  449. package/src/transport/calls.ts +0 -113
  450. package/src/transport/characterization.test.ts +0 -260
  451. package/src/transport/codec.test.ts +0 -118
  452. package/src/transport/codec.ts +0 -240
  453. package/src/transport/conformance-python.test.ts +0 -152
  454. package/src/transport/conformance-same-lib.test.ts +0 -115
  455. package/src/transport/dbus-native-lib.d.ts +0 -19
  456. package/src/transport/dbus-native.ts +0 -85
  457. package/src/transport/index.ts +0 -30
  458. package/src/transport/no-library-leak.test.ts +0 -61
  459. package/src/transport/reliability.test.ts +0 -173
  460. package/src/transport/signals.ts +0 -150
  461. package/src/transport/signature.ts +0 -110
  462. package/src/transport/test-support/fake-service.ts +0 -168
  463. package/src/transport/test-support/independent-producer.py +0 -110
  464. package/src/transport/test-support/private-bus.ts +0 -66
  465. package/src/transport/transport.ts +0 -250
  466. package/src/transport/types.ts +0 -118
  467. package/src/usb-mode/catalog-schema.test.ts +0 -181
  468. package/src/usb-mode/catalog-schema.ts +0 -113
  469. package/src/usb-mode/certified-catalog.json +0 -67
  470. package/src/usb-mode/index.ts +0 -58
  471. package/src/usb-mode/ingestion.test.ts +0 -268
  472. package/src/usb-mode/ingestion.ts +0 -297
  473. package/src/usb-mode/promotion-review.ts +0 -117
  474. 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
- }