@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.
- package/README.md +317 -0
- package/dist/backend/at-lease.d.ts +57 -0
- package/dist/backend/at-lease.js +109 -0
- package/dist/backend/cell-info.d.ts +46 -0
- package/dist/backend/cell-info.js +124 -0
- package/dist/backend/constants.d.ts +24 -0
- package/{src/backend/constants.ts → dist/backend/constants.js} +4 -9
- package/dist/backend/device-classifier.d.ts +46 -0
- package/dist/backend/device-classifier.js +258 -0
- package/dist/backend/enrichment.d.ts +28 -0
- package/dist/backend/enrichment.js +59 -0
- package/dist/backend/features.d.ts +61 -0
- package/dist/backend/features.js +109 -0
- package/dist/backend/identity-ladder.d.ts +57 -0
- package/dist/backend/identity-ladder.js +158 -0
- package/dist/backend/identity-registry.d.ts +51 -0
- package/dist/backend/identity-registry.js +101 -0
- package/dist/backend/index.d.ts +30 -0
- package/dist/backend/index.js +35 -0
- package/dist/backend/lifecycle-interlock.d.ts +25 -0
- package/dist/backend/lifecycle-interlock.js +18 -0
- package/dist/backend/managed-objects.d.ts +39 -0
- package/dist/backend/managed-objects.js +82 -0
- package/dist/backend/mapping.d.ts +11 -0
- package/dist/backend/mapping.js +148 -0
- package/dist/backend/mm-backend.d.ts +44 -0
- package/dist/backend/mm-backend.js +154 -0
- package/dist/backend/mm-location.d.ts +21 -0
- package/dist/backend/mm-location.js +237 -0
- package/dist/backend/mm-mutations.d.ts +42 -0
- package/dist/backend/mm-mutations.js +259 -0
- package/dist/backend/modem-actor.d.ts +41 -0
- package/dist/backend/modem-actor.js +78 -0
- package/dist/backend/nm-auto-apn.d.ts +54 -0
- package/dist/backend/nm-auto-apn.js +124 -0
- package/dist/backend/nm-gsm-fields.d.ts +23 -0
- package/dist/backend/nm-gsm-fields.js +107 -0
- package/dist/backend/nmcli-nm-port.d.ts +28 -0
- package/dist/backend/nmcli-nm-port.js +173 -0
- package/dist/backend/nmcli-runner.d.ts +24 -0
- package/dist/backend/nmcli-runner.js +35 -0
- package/dist/backend/observer.d.ts +37 -0
- package/dist/backend/observer.js +219 -0
- package/dist/backend/power-contract.d.ts +49 -0
- package/dist/backend/power-contract.js +34 -0
- package/dist/backend/recovery-attribution.d.ts +32 -0
- package/dist/backend/recovery-attribution.js +57 -0
- package/dist/backend/recovery-budget.d.ts +45 -0
- package/dist/backend/recovery-budget.js +44 -0
- package/dist/backend/recovery-ladder.d.ts +94 -0
- package/dist/backend/recovery-ladder.js +116 -0
- package/dist/backend/router-ethernet.d.ts +19 -0
- package/dist/backend/router-ethernet.js +66 -0
- package/dist/backend/row-store.d.ts +13 -0
- package/dist/backend/row-store.js +82 -0
- package/dist/backend/signal-setup.d.ts +30 -0
- package/dist/backend/signal-setup.js +92 -0
- package/dist/backend/sim-unlock.d.ts +13 -0
- package/dist/backend/sim-unlock.js +153 -0
- package/dist/backend/transition-preconditions.d.ts +76 -0
- package/dist/backend/transition-preconditions.js +64 -0
- package/dist/backend/usage/accounting.d.ts +39 -0
- package/dist/backend/usage/accounting.js +73 -0
- package/dist/backend/usage/billing-cycle.d.ts +11 -0
- package/dist/backend/usage/billing-cycle.js +39 -0
- package/dist/backend/usage/boot-id.d.ts +6 -0
- package/{src/backend/usage/boot-id.ts → dist/backend/usage/boot-id.js} +7 -7
- package/dist/backend/usage/index.d.ts +8 -0
- package/dist/backend/usage/index.js +11 -0
- package/dist/backend/usage/policy-store.d.ts +50 -0
- package/dist/backend/usage/policy-store.js +161 -0
- package/dist/backend/usage/policy-write.d.ts +65 -0
- package/dist/backend/usage/policy-write.js +112 -0
- package/dist/backend/usage/proc-net-dev.d.ts +18 -0
- package/{src/backend/usage/proc-net-dev.ts → dist/backend/usage/proc-net-dev.js} +39 -47
- package/dist/backend/usage/sampler.d.ts +75 -0
- package/dist/backend/usage/sampler.js +211 -0
- package/dist/backend/usage/store.d.ts +38 -0
- package/dist/backend/usage/store.js +126 -0
- package/dist/backend/usb-device-snapshot.d.ts +31 -0
- package/dist/backend/usb-device-snapshot.js +1 -0
- package/dist/backend/usb-enumerator.d.ts +21 -0
- package/dist/backend/usb-enumerator.js +153 -0
- package/dist/backend/usb-mode-transition.d.ts +29 -0
- package/dist/backend/usb-mode-transition.js +196 -0
- package/dist/band/band-names.d.ts +42 -0
- package/dist/band/band-names.js +150 -0
- package/dist/band/certification.d.ts +84 -0
- package/dist/band/certification.js +127 -0
- package/dist/band/certified-bands.json +4 -0
- package/dist/band/index.d.ts +2 -0
- package/dist/band/index.js +8 -0
- package/dist/capability/detect.d.ts +52 -0
- package/dist/capability/detect.js +86 -0
- package/dist/capability/five-g-preference.d.ts +104 -0
- package/dist/capability/five-g-preference.js +171 -0
- package/dist/capability/index.d.ts +3 -0
- package/dist/capability/index.js +10 -0
- package/dist/capability/support-claim.d.ts +39 -0
- package/dist/capability/support-claim.js +81 -0
- package/dist/domain/brand.d.ts +10 -0
- package/dist/domain/brand.js +21 -0
- package/dist/domain/errors.d.ts +32 -0
- package/dist/domain/errors.js +48 -0
- package/dist/domain/generation.d.ts +8 -0
- package/dist/domain/generation.js +12 -0
- package/dist/domain/guards.d.ts +6 -0
- package/dist/domain/guards.js +127 -0
- package/dist/domain/identity.d.ts +109 -0
- package/dist/domain/identity.js +86 -0
- package/dist/domain/index.d.ts +14 -0
- package/dist/domain/index.js +18 -0
- package/dist/domain/mm-enums.d.ts +12 -0
- package/dist/domain/mm-enums.js +139 -0
- package/dist/domain/modem-presentation.d.ts +10 -0
- package/dist/domain/modem-presentation.js +39 -0
- package/dist/domain/observation.d.ts +39 -0
- package/dist/domain/observation.js +4 -0
- package/dist/domain/operation.d.ts +118 -0
- package/dist/domain/operation.js +85 -0
- package/dist/domain/physical-identity.d.ts +43 -0
- package/dist/domain/physical-identity.js +113 -0
- package/{src/domain/policy.ts → dist/domain/policy.d.ts} +34 -69
- package/dist/domain/policy.js +38 -0
- package/dist/domain/shadow-divergence.d.ts +27 -0
- package/dist/domain/shadow-divergence.js +70 -0
- package/dist/domain/snapshot.d.ts +53 -0
- package/dist/domain/snapshot.js +75 -0
- package/{src/domain/state.ts → dist/domain/state.d.ts} +23 -122
- package/dist/domain/state.js +38 -0
- package/dist/fcc/coverage.d.ts +63 -0
- package/dist/fcc/coverage.js +102 -0
- package/dist/fcc/index.d.ts +3 -0
- package/dist/fcc/index.js +12 -0
- package/dist/fcc/policy-store.d.ts +40 -0
- package/dist/fcc/policy-store.js +146 -0
- package/dist/fcc/policy-write.d.ts +32 -0
- package/dist/fcc/policy-write.js +37 -0
- package/dist/hardware/hilink-protocol.d.ts +39 -0
- package/dist/hardware/hilink-protocol.js +58 -0
- package/dist/hardware/index.d.ts +3 -0
- package/dist/hardware/index.js +15 -0
- package/dist/hardware/router-parsers.d.ts +89 -0
- package/dist/hardware/router-parsers.js +234 -0
- package/dist/index.d.ts +19 -0
- package/dist/index.js +26 -0
- package/dist/journal/codec.d.ts +44 -0
- package/dist/journal/codec.js +198 -0
- package/dist/journal/engine.d.ts +28 -0
- package/dist/journal/engine.js +68 -0
- package/dist/journal/entry.d.ts +74 -0
- package/dist/journal/entry.js +56 -0
- package/dist/journal/index.d.ts +6 -0
- package/dist/journal/index.js +6 -0
- package/dist/journal/legacy-ceraui.d.ts +73 -0
- package/dist/journal/legacy-ceraui.js +227 -0
- package/dist/journal/recovery.d.ts +58 -0
- package/dist/journal/recovery.js +117 -0
- package/dist/journal/store.d.ts +55 -0
- package/dist/journal/store.js +150 -0
- package/dist/location/fix-state.d.ts +53 -0
- package/dist/location/fix-state.js +75 -0
- package/dist/location/index.d.ts +2 -0
- package/dist/location/index.js +8 -0
- package/dist/location/nmea.d.ts +10 -0
- package/dist/location/nmea.js +89 -0
- package/dist/observations/envelope.d.ts +63 -0
- package/dist/observations/envelope.js +77 -0
- package/dist/observations/freshness.d.ts +28 -0
- package/dist/observations/freshness.js +76 -0
- package/dist/observations/index.d.ts +13 -0
- package/dist/observations/index.js +24 -0
- package/dist/observations/metric.d.ts +47 -0
- package/dist/observations/metric.js +73 -0
- package/dist/observations/model.d.ts +80 -0
- package/dist/observations/model.js +11 -0
- package/dist/observations/provenance.d.ts +94 -0
- package/dist/observations/provenance.js +67 -0
- package/dist/observations/raw.d.ts +42 -0
- package/dist/observations/raw.js +146 -0
- package/dist/observations/reading.d.ts +51 -0
- package/dist/observations/reading.js +65 -0
- package/dist/observations/sources/hilink.d.ts +10 -0
- package/dist/observations/sources/hilink.js +84 -0
- package/dist/observations/sources/modemmanager.d.ts +18 -0
- package/dist/observations/sources/modemmanager.js +239 -0
- package/dist/observations/sources/router-shared.d.ts +29 -0
- package/dist/observations/sources/router-shared.js +68 -0
- package/dist/observations/sources/ufi.d.ts +10 -0
- package/dist/observations/sources/ufi.js +111 -0
- package/dist/observations/sources/zte.d.ts +7 -0
- package/dist/observations/sources/zte.js +71 -0
- package/dist/observations/state-separation.d.ts +64 -0
- package/dist/observations/state-separation.js +52 -0
- package/dist/operations/contracts.d.ts +72 -0
- package/dist/operations/contracts.js +1 -0
- package/dist/operations/index.d.ts +1 -0
- package/dist/operations/index.js +1 -0
- package/dist/operations/operation-engine.d.ts +16 -0
- package/dist/operations/operation-engine.js +194 -0
- package/dist/ports/index.d.ts +12 -0
- package/{src/ports/index.ts → dist/ports/index.js} +12 -8
- package/dist/ports/location.d.ts +87 -0
- package/dist/ports/location.js +36 -0
- package/dist/ports/modem-manager.d.ts +89 -0
- package/dist/ports/modem-manager.js +9 -0
- package/dist/ports/mutation-admission.d.ts +27 -0
- package/dist/ports/mutation-admission.js +9 -0
- package/dist/ports/network-manager.d.ts +68 -0
- package/dist/ports/network-manager.js +13 -0
- package/{src/ports/observation.ts → dist/ports/observation.d.ts} +16 -27
- package/dist/ports/observation.js +7 -0
- package/dist/ports/ops.d.ts +44 -0
- package/dist/ports/ops.js +16 -0
- package/{src/ports/receipts.ts → dist/ports/receipts.d.ts} +5 -27
- package/dist/ports/receipts.js +9 -0
- package/dist/ports/reconcile.d.ts +33 -0
- package/dist/ports/reconcile.js +200 -0
- package/dist/ports/resource-ownership.d.ts +29 -0
- package/dist/ports/resource-ownership.js +1 -0
- package/dist/ports/router.d.ts +19 -0
- package/dist/ports/router.js +7 -0
- package/dist/ports/sms.d.ts +64 -0
- package/dist/ports/sms.js +24 -0
- package/dist/ports/uhubctl.d.ts +6 -0
- package/dist/ports/uhubctl.js +1 -0
- package/dist/providers/contracts.d.ts +124 -0
- package/dist/providers/contracts.js +10 -0
- package/dist/providers/huawei-hilink/index.d.ts +2 -0
- package/dist/providers/huawei-hilink/index.js +2 -0
- package/dist/providers/huawei-hilink/operations.d.ts +20 -0
- package/dist/providers/huawei-hilink/operations.js +56 -0
- package/dist/providers/huawei-hilink/provider.d.ts +52 -0
- package/dist/providers/huawei-hilink/provider.js +76 -0
- package/dist/providers/huawei-hilink/runtime.d.ts +22 -0
- package/dist/providers/huawei-hilink/runtime.js +171 -0
- package/dist/providers/huawei-hilink/session.d.ts +28 -0
- package/dist/providers/huawei-hilink/session.js +120 -0
- package/dist/providers/huawei-hilink/transport.d.ts +19 -0
- package/dist/providers/huawei-hilink/transport.js +1 -0
- package/dist/providers/index.d.ts +8 -0
- package/dist/providers/index.js +8 -0
- package/dist/providers/matcher.d.ts +3 -0
- package/dist/providers/matcher.js +205 -0
- package/dist/providers/modem-manager/errors.d.ts +7 -0
- package/dist/providers/modem-manager/errors.js +37 -0
- package/dist/providers/modem-manager/generic-operations.d.ts +8 -0
- package/dist/providers/modem-manager/generic-operations.js +207 -0
- package/dist/providers/modem-manager/index.d.ts +3 -0
- package/dist/providers/modem-manager/index.js +3 -0
- package/dist/providers/modem-manager/module-operations.d.ts +21 -0
- package/dist/providers/modem-manager/module-operations.js +118 -0
- package/dist/providers/modem-manager/provider.d.ts +39 -0
- package/dist/providers/modem-manager/provider.js +152 -0
- package/dist/providers/modem-manager/snapshot.d.ts +5 -0
- package/dist/providers/modem-manager/snapshot.js +204 -0
- package/dist/providers/modem-manager/types.d.ts +135 -0
- package/dist/providers/modem-manager/types.js +1 -0
- package/dist/providers/network-manager/adapter.d.ts +71 -0
- package/dist/providers/network-manager/adapter.js +348 -0
- package/dist/providers/network-manager/index.d.ts +2 -0
- package/dist/providers/network-manager/index.js +2 -0
- package/dist/providers/network-manager/types.d.ts +163 -0
- package/dist/providers/network-manager/types.js +77 -0
- package/dist/providers/registry.d.ts +13 -0
- package/dist/providers/registry.js +33 -0
- package/dist/providers/ufi-himi/index.d.ts +6 -0
- package/dist/providers/ufi-himi/index.js +6 -0
- package/dist/providers/ufi-himi/operations.d.ts +41 -0
- package/dist/providers/ufi-himi/operations.js +66 -0
- package/dist/providers/ufi-himi/prohibitions.d.ts +62 -0
- package/dist/providers/ufi-himi/prohibitions.js +88 -0
- package/dist/providers/ufi-himi/provider.d.ts +41 -0
- package/dist/providers/ufi-himi/provider.js +204 -0
- package/dist/providers/ufi-himi/qualcomm-evidence.d.ts +32 -0
- package/dist/providers/ufi-himi/qualcomm-evidence.js +51 -0
- package/dist/providers/ufi-himi/session.d.ts +46 -0
- package/dist/providers/ufi-himi/session.js +92 -0
- package/dist/providers/ufi-himi/transport.d.ts +29 -0
- package/dist/providers/ufi-himi/transport.js +25 -0
- package/dist/providers/zte-goform/index.d.ts +2 -0
- package/dist/providers/zte-goform/index.js +2 -0
- package/dist/providers/zte-goform/provider.d.ts +51 -0
- package/dist/providers/zte-goform/provider.js +101 -0
- package/dist/providers/zte-goform/session.d.ts +17 -0
- package/dist/providers/zte-goform/session.js +133 -0
- package/dist/providers/zte-goform/transport.d.ts +16 -0
- package/dist/providers/zte-goform/transport.js +1 -0
- package/dist/radio/band-truth.d.ts +50 -0
- package/dist/radio/band-truth.js +92 -0
- package/dist/radio/index.d.ts +3 -0
- package/dist/radio/index.js +10 -0
- package/dist/radio/mode-combinations.d.ts +78 -0
- package/dist/radio/mode-combinations.js +198 -0
- package/dist/radio/mode-truth.d.ts +67 -0
- package/dist/radio/mode-truth.js +112 -0
- package/dist/redact.d.ts +15 -0
- package/dist/redact.js +189 -0
- package/dist/safety/composition-root.d.ts +28 -0
- package/dist/safety/composition-root.js +65 -0
- package/dist/safety/flock-resource-ownership.d.ts +11 -0
- package/dist/safety/flock-resource-ownership.js +151 -0
- package/dist/safety/index.d.ts +2 -0
- package/dist/safety/index.js +2 -0
- package/dist/sms/dbus-messaging.d.ts +17 -0
- package/dist/sms/dbus-messaging.js +185 -0
- package/dist/sms/inbox-store.d.ts +10 -0
- package/dist/sms/inbox-store.js +82 -0
- package/dist/sms/index.d.ts +4 -0
- package/dist/sms/index.js +10 -0
- package/dist/sms/mmcli-parse.d.ts +54 -0
- package/dist/sms/mmcli-parse.js +224 -0
- package/dist/sms/normalize.d.ts +42 -0
- package/dist/sms/normalize.js +95 -0
- package/dist/testing/domain-fakes.d.ts +58 -0
- package/dist/testing/domain-fakes.js +98 -0
- package/dist/testing/index.d.ts +2 -0
- package/dist/testing/index.js +17 -0
- package/dist/testing/provider-fakes.d.ts +45 -0
- package/dist/testing/provider-fakes.js +64 -0
- package/dist/transport/calls.d.ts +9 -0
- package/dist/transport/calls.js +88 -0
- package/dist/transport/codec.d.ts +3 -0
- package/dist/transport/codec.js +207 -0
- package/dist/transport/dbus-native.d.ts +57 -0
- package/dist/transport/dbus-native.js +17 -0
- package/dist/transport/errors.d.ts +21 -0
- package/{src/transport/errors.ts → dist/transport/errors.js} +34 -46
- package/dist/transport/index.d.ts +4 -0
- package/dist/transport/index.js +9 -0
- package/dist/transport/signals.d.ts +15 -0
- package/dist/transport/signals.js +123 -0
- package/dist/transport/signature.d.ts +7 -0
- package/dist/transport/signature.js +94 -0
- package/dist/transport/transport.d.ts +2 -0
- package/dist/transport/transport.js +202 -0
- package/dist/transport/types.d.ts +61 -0
- package/dist/transport/types.js +19 -0
- package/dist/usb-mode/catalog-schema.d.ts +139 -0
- package/dist/usb-mode/catalog-schema.js +97 -0
- package/dist/usb-mode/catalog.d.ts +21 -0
- package/{src/usb-mode/catalog.ts → dist/usb-mode/catalog.js} +10 -32
- package/dist/usb-mode/certified-catalog.json +67 -0
- package/dist/usb-mode/index.d.ts +5 -0
- package/dist/usb-mode/index.js +15 -0
- package/dist/usb-mode/ingestion.d.ts +111 -0
- package/dist/usb-mode/ingestion.js +187 -0
- package/dist/usb-mode/promotion-review.d.ts +21 -0
- package/dist/usb-mode/promotion-review.js +87 -0
- package/dist/usb-mode/usb-devices-parse.d.ts +36 -0
- package/dist/usb-mode/usb-devices-parse.js +157 -0
- package/dist/ussd/calls.d.ts +32 -0
- package/dist/ussd/calls.js +96 -0
- package/dist/ussd/index.d.ts +5 -0
- package/dist/ussd/index.js +12 -0
- package/dist/ussd/mm-ussd.d.ts +37 -0
- package/dist/ussd/mm-ussd.js +205 -0
- package/dist/ussd/refusal.d.ts +53 -0
- package/dist/ussd/refusal.js +154 -0
- package/dist/ussd/registration.d.ts +20 -0
- package/dist/ussd/registration.js +101 -0
- package/dist/ussd/session.d.ts +84 -0
- package/dist/ussd/session.js +163 -0
- package/package.json +38 -4
- package/src/backend/at-lease.test.ts +0 -106
- package/src/backend/at-lease.ts +0 -158
- package/src/backend/cell-info.test.ts +0 -154
- package/src/backend/cell-info.ts +0 -160
- package/src/backend/device-classifier.test.ts +0 -168
- package/src/backend/device-classifier.ts +0 -248
- package/src/backend/enrichment.ts +0 -96
- package/src/backend/features.test.ts +0 -162
- package/src/backend/features.ts +0 -179
- package/src/backend/identity-ladder.test.ts +0 -117
- package/src/backend/identity-ladder.ts +0 -221
- package/src/backend/identity-registry.test.ts +0 -89
- package/src/backend/identity-registry.ts +0 -151
- package/src/backend/index.ts +0 -236
- package/src/backend/lifecycle-interlock.ts +0 -38
- package/src/backend/managed-objects.ts +0 -108
- package/src/backend/mapping.ts +0 -160
- package/src/backend/mm-backend.ts +0 -191
- package/src/backend/mm-mutations.ts +0 -228
- package/src/backend/modem-actor.test.ts +0 -95
- package/src/backend/modem-actor.ts +0 -112
- package/src/backend/nm-auto-apn.ts +0 -161
- package/src/backend/nm-gsm-fields.ts +0 -122
- package/src/backend/nmcli-nm-port.ts +0 -228
- package/src/backend/nmcli-runner.ts +0 -52
- package/src/backend/observer.ts +0 -297
- package/src/backend/power-contract.test.ts +0 -40
- package/src/backend/power-contract.ts +0 -83
- package/src/backend/recovery-attribution.test.ts +0 -102
- package/src/backend/recovery-attribution.ts +0 -86
- package/src/backend/recovery-budget.test.ts +0 -64
- package/src/backend/recovery-budget.ts +0 -84
- package/src/backend/recovery-ladder.test.ts +0 -257
- package/src/backend/recovery-ladder.ts +0 -249
- package/src/backend/router-ethernet.test.ts +0 -71
- package/src/backend/router-ethernet.ts +0 -90
- package/src/backend/row-store.ts +0 -105
- package/src/backend/signal-setup.ts +0 -112
- package/src/backend/sim-unlock.ts +0 -193
- package/src/backend/transition-preconditions.ts +0 -149
- package/src/backend/uhubctl-power-hook.test.ts +0 -274
- package/src/backend/uhubctl-power-hook.ts +0 -377
- package/src/backend/usage/accounting.test.ts +0 -147
- package/src/backend/usage/accounting.ts +0 -123
- package/src/backend/usage/billing-cycle.test.ts +0 -62
- package/src/backend/usage/billing-cycle.ts +0 -45
- package/src/backend/usage/index.ts +0 -60
- package/src/backend/usage/policy-store.test.ts +0 -164
- package/src/backend/usage/policy-store.ts +0 -216
- package/src/backend/usage/policy-write.test.ts +0 -198
- package/src/backend/usage/policy-write.ts +0 -207
- package/src/backend/usage/proc-net-dev.test.ts +0 -56
- package/src/backend/usage/sampler.test.ts +0 -327
- package/src/backend/usage/sampler.ts +0 -282
- package/src/backend/usage/store.test.ts +0 -148
- package/src/backend/usage/store.ts +0 -177
- package/src/backend/usb-enumerator.test.ts +0 -87
- package/src/backend/usb-enumerator.ts +0 -181
- package/src/backend/usb-mode-transition.test.ts +0 -323
- package/src/backend/usb-mode-transition.ts +0 -253
- package/src/domain/brand.ts +0 -29
- package/src/domain/errors.ts +0 -77
- package/src/domain/guards.test.ts +0 -218
- package/src/domain/guards.ts +0 -144
- package/src/domain/identity.test.ts +0 -83
- package/src/domain/identity.ts +0 -165
- package/src/domain/index.ts +0 -12
- package/src/domain/snapshot.test.ts +0 -266
- package/src/domain/snapshot.ts +0 -120
- package/src/index.test.ts +0 -6
- package/src/index.ts +0 -15
- package/src/ports/README.md +0 -61
- package/src/ports/forbidden-surface.test.ts +0 -241
- package/src/ports/modem-manager.ts +0 -72
- package/src/ports/network-manager.ts +0 -87
- package/src/ports/ops.ts +0 -60
- package/src/ports/ops.type-test.ts +0 -39
- package/src/ports/receipts.test.ts +0 -153
- package/src/ports/reconcile.test.ts +0 -152
- package/src/ports/reconcile.ts +0 -338
- package/src/ports/router.ts +0 -29
- package/src/redact.test.ts +0 -82
- package/src/redact.ts +0 -73
- package/src/transport/README.md +0 -65
- package/src/transport/calls.ts +0 -113
- package/src/transport/characterization.test.ts +0 -260
- package/src/transport/codec.test.ts +0 -118
- package/src/transport/codec.ts +0 -240
- package/src/transport/conformance-python.test.ts +0 -152
- package/src/transport/conformance-same-lib.test.ts +0 -115
- package/src/transport/dbus-native-lib.d.ts +0 -19
- package/src/transport/dbus-native.ts +0 -85
- package/src/transport/index.ts +0 -30
- package/src/transport/no-library-leak.test.ts +0 -61
- package/src/transport/reliability.test.ts +0 -173
- package/src/transport/signals.ts +0 -150
- package/src/transport/signature.ts +0 -110
- package/src/transport/test-support/fake-service.ts +0 -168
- package/src/transport/test-support/independent-producer.py +0 -110
- package/src/transport/test-support/private-bus.ts +0 -66
- package/src/transport/transport.ts +0 -250
- package/src/transport/types.ts +0 -118
- package/src/usb-mode/catalog-schema.test.ts +0 -181
- package/src/usb-mode/catalog-schema.ts +0 -113
- package/src/usb-mode/certified-catalog.json +0 -67
- package/src/usb-mode/index.ts +0 -58
- package/src/usb-mode/ingestion.test.ts +0 -268
- package/src/usb-mode/ingestion.ts +0 -297
- package/src/usb-mode/promotion-review.ts +0 -117
- 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
|
-
}
|