@ceralive/modem-control 0.2.0 → 1.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- 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 -221
- 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/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 -37
- package/src/backend/usage/proc-net-dev.test.ts +0 -56
- package/src/backend/usage/sampler.test.ts +0 -219
- package/src/backend/usage/sampler.ts +0 -228
- 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 -27
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
// The GNSS display state machine — bounded acquisition and stale-fix expiry.
|
|
2
|
+
//
|
|
3
|
+
// It exists to make two dishonest renders impossible rather than merely unlikely:
|
|
4
|
+
//
|
|
5
|
+
// 1. An endless "acquiring…" spinner. A modem with no antenna answers "no fix"
|
|
6
|
+
// forever, quite correctly, and a naive UI waits forever. Acquisition here is
|
|
7
|
+
// BOUNDED — past `acquireTimeoutMs` the state becomes `no-fix`, which is a
|
|
8
|
+
// terminal render, not a spinner.
|
|
9
|
+
// 2. A stale coordinate shown as current. A fix is only reachable through
|
|
10
|
+
// `renderableFix`, which returns one ONLY in the `fix` state; every exit from
|
|
11
|
+
// that state DROPS the fix rather than carrying it, so there is no code path
|
|
12
|
+
// that can render a position the modem is no longer reporting.
|
|
13
|
+
//
|
|
14
|
+
// Pure and total: no clock, no I/O. The caller supplies `at` on every event, which
|
|
15
|
+
// is what makes both bounds testable without waiting for real time to pass.
|
|
16
|
+
/**
|
|
17
|
+
* Defaults chosen against the fleet's own numbers: a cold GNSS start on the
|
|
18
|
+
* bench modems is a low-minutes affair, so 120 s is long enough to be a fair
|
|
19
|
+
* attempt and short enough that a missing antenna is reported inside a support
|
|
20
|
+
* call rather than after one. A fix older than 30 s is not "current" on a moving
|
|
21
|
+
* vehicle, which is the only context this display has.
|
|
22
|
+
*/
|
|
23
|
+
export const DEFAULT_FIX_STATE_CONFIG = {
|
|
24
|
+
acquireTimeoutMs: 120_000,
|
|
25
|
+
fixTtlMs: 30_000,
|
|
26
|
+
};
|
|
27
|
+
export const GNSS_OFF = { kind: 'off' };
|
|
28
|
+
/** A fix is reachable ONLY here, and only while the state actually holds one. */
|
|
29
|
+
export function renderableFix(state) {
|
|
30
|
+
return state.kind === 'fix' ? state.fix : undefined;
|
|
31
|
+
}
|
|
32
|
+
/** True while a bounded wait is legitimately in progress — the only spinner state. */
|
|
33
|
+
export function isAcquiring(state) {
|
|
34
|
+
return state.kind === 'acquiring';
|
|
35
|
+
}
|
|
36
|
+
function expireIfDue(state, at, config) {
|
|
37
|
+
if (state.kind === 'acquiring' && at - state.since >= config.acquireTimeoutMs) {
|
|
38
|
+
return { kind: 'no-fix', since: at, reason: 'acquire-timeout' };
|
|
39
|
+
}
|
|
40
|
+
if (state.kind === 'fix' && at - state.fix.observedAt >= config.fixTtlMs) {
|
|
41
|
+
return { kind: 'no-fix', since: at, reason: 'fix-expired' };
|
|
42
|
+
}
|
|
43
|
+
return state;
|
|
44
|
+
}
|
|
45
|
+
function applyRead(state, at, read, config) {
|
|
46
|
+
switch (read.outcome) {
|
|
47
|
+
case 'fix':
|
|
48
|
+
return { kind: 'fix', fix: read.fix };
|
|
49
|
+
case 'no-fix':
|
|
50
|
+
// Still inside the bound, the modem simply has not acquired yet — that is
|
|
51
|
+
// what `acquiring` means, so a report of no-fix does not end the wait. Any
|
|
52
|
+
// other state (including a held fix) drops to an honest `no-fix`.
|
|
53
|
+
return state.kind === 'acquiring'
|
|
54
|
+
? expireIfDue(state, at, config)
|
|
55
|
+
: { kind: 'no-fix', since: at, reason: 'reported-no-fix' };
|
|
56
|
+
case 'disabled':
|
|
57
|
+
return GNSS_OFF;
|
|
58
|
+
case 'unsupported':
|
|
59
|
+
case 'error':
|
|
60
|
+
return { kind: 'unavailable', reason: read.reason };
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
/** Pure, total transition. Every exit from `fix` drops the coordinates. */
|
|
64
|
+
export function advanceGnssFixState(state, event, config = DEFAULT_FIX_STATE_CONFIG) {
|
|
65
|
+
switch (event.kind) {
|
|
66
|
+
case 'gnss-enabled':
|
|
67
|
+
return state.kind === 'fix' ? state : { kind: 'acquiring', since: event.at };
|
|
68
|
+
case 'gnss-disabled':
|
|
69
|
+
return GNSS_OFF;
|
|
70
|
+
case 'read':
|
|
71
|
+
return applyRead(state, event.at, event.read, config);
|
|
72
|
+
case 'tick':
|
|
73
|
+
return expireIfDue(state, event.at, config);
|
|
74
|
+
}
|
|
75
|
+
}
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
// The GPS/location module — the GNSS display state machine and NMEA decoding.
|
|
2
|
+
//
|
|
3
|
+
// PRIVACY FENCE (a product rule, not a phase limitation): this module reads the
|
|
4
|
+
// CURRENT fix and holds it in memory for a live display. There is no history, no
|
|
5
|
+
// track log, no persistence, and no upload — and none may be added here. The port
|
|
6
|
+
// contract in `../ports/location.ts` is guarded by `location-fence.test.ts`.
|
|
7
|
+
export * from './fix-state.js';
|
|
8
|
+
export * from './nmea.js';
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
export interface NmeaFix {
|
|
2
|
+
readonly latitude: number;
|
|
3
|
+
readonly longitude: number;
|
|
4
|
+
readonly altitude?: number;
|
|
5
|
+
}
|
|
6
|
+
/**
|
|
7
|
+
* The LAST valid GGA fix in a block of sentences, or `undefined` when none of them
|
|
8
|
+
* carries one. Never throws — a truncated or corrupt blob is simply not a fix.
|
|
9
|
+
*/
|
|
10
|
+
export declare function parseNmeaFix(text: string): NmeaFix | undefined;
|
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
// Minimal NMEA decoding — GGA only, checksum-verified.
|
|
2
|
+
//
|
|
3
|
+
// It exists because `gps-nmea` is the one GNSS source EVERY GNSS-capable modem on
|
|
4
|
+
// the fleet advertises, while `gps-raw` (MM's pre-decoded dict) is not guaranteed:
|
|
5
|
+
// a `gps-unmanaged`-style device hands over sentences and nothing else. Without this
|
|
6
|
+
// the module would have to answer "no fix" to a modem that is reporting one, which
|
|
7
|
+
// is exactly the dishonesty the rest of this module is built to avoid.
|
|
8
|
+
//
|
|
9
|
+
// GGA is the only sentence read: it is the only standard sentence carrying fix
|
|
10
|
+
// QUALITY alongside the position, so "the receiver has not locked on" is decodable
|
|
11
|
+
// rather than inferred. RMC's A/V validity flag would do, but no fleet modem emits
|
|
12
|
+
// RMC without also emitting GGA.
|
|
13
|
+
/** XOR of every character between `$` and `*` — the NMEA checksum. */
|
|
14
|
+
function checksumOf(body) {
|
|
15
|
+
let sum = 0;
|
|
16
|
+
for (let i = 0; i < body.length; i += 1) {
|
|
17
|
+
sum ^= body.charCodeAt(i);
|
|
18
|
+
}
|
|
19
|
+
return sum;
|
|
20
|
+
}
|
|
21
|
+
function verifiedBody(sentence) {
|
|
22
|
+
const trimmed = sentence.trim();
|
|
23
|
+
if (!trimmed.startsWith('$')) {
|
|
24
|
+
return undefined;
|
|
25
|
+
}
|
|
26
|
+
const star = trimmed.lastIndexOf('*');
|
|
27
|
+
if (star < 0) {
|
|
28
|
+
return undefined;
|
|
29
|
+
}
|
|
30
|
+
const body = trimmed.slice(1, star);
|
|
31
|
+
const declared = Number.parseInt(trimmed.slice(star + 1, star + 3), 16);
|
|
32
|
+
return Number.isNaN(declared) || checksumOf(body) !== declared ? undefined : body;
|
|
33
|
+
}
|
|
34
|
+
/** `ddmm.mmmm` + hemisphere → signed degrees. `degreeDigits` is 2 for lat, 3 for lon. */
|
|
35
|
+
function toDegrees(value, hemisphere, degreeDigits) {
|
|
36
|
+
if (value.length < degreeDigits + 1) {
|
|
37
|
+
return undefined;
|
|
38
|
+
}
|
|
39
|
+
const degrees = Number.parseFloat(value.slice(0, degreeDigits));
|
|
40
|
+
const minutes = Number.parseFloat(value.slice(degreeDigits));
|
|
41
|
+
if (!Number.isFinite(degrees) || !Number.isFinite(minutes)) {
|
|
42
|
+
return undefined;
|
|
43
|
+
}
|
|
44
|
+
const magnitude = degrees + minutes / 60;
|
|
45
|
+
const negative = hemisphere === 'S' || hemisphere === 'W';
|
|
46
|
+
return negative ? -magnitude : magnitude;
|
|
47
|
+
}
|
|
48
|
+
function parseGga(fields) {
|
|
49
|
+
const quality = Number.parseInt(fields[6] ?? '', 10);
|
|
50
|
+
// Quality 0 is "fix not available". A receiver that is searching emits GGA with
|
|
51
|
+
// empty position fields and quality 0, so this is the honest no-fix signal.
|
|
52
|
+
if (!Number.isFinite(quality) || quality <= 0) {
|
|
53
|
+
return undefined;
|
|
54
|
+
}
|
|
55
|
+
const latitude = toDegrees(fields[2] ?? '', fields[3] ?? '', 2);
|
|
56
|
+
const longitude = toDegrees(fields[4] ?? '', fields[5] ?? '', 3);
|
|
57
|
+
if (latitude === undefined || longitude === undefined) {
|
|
58
|
+
return undefined;
|
|
59
|
+
}
|
|
60
|
+
if (Math.abs(latitude) > 90 || Math.abs(longitude) > 180) {
|
|
61
|
+
return undefined;
|
|
62
|
+
}
|
|
63
|
+
const altitude = Number.parseFloat(fields[9] ?? '');
|
|
64
|
+
return {
|
|
65
|
+
latitude,
|
|
66
|
+
longitude,
|
|
67
|
+
...(Number.isFinite(altitude) ? { altitude } : {}),
|
|
68
|
+
};
|
|
69
|
+
}
|
|
70
|
+
/**
|
|
71
|
+
* The LAST valid GGA fix in a block of sentences, or `undefined` when none of them
|
|
72
|
+
* carries one. Never throws — a truncated or corrupt blob is simply not a fix.
|
|
73
|
+
*/
|
|
74
|
+
export function parseNmeaFix(text) {
|
|
75
|
+
let latest;
|
|
76
|
+
for (const line of text.split(/[\r\n]+/)) {
|
|
77
|
+
const body = verifiedBody(line);
|
|
78
|
+
if (body === undefined) {
|
|
79
|
+
continue;
|
|
80
|
+
}
|
|
81
|
+
const fields = body.split(',');
|
|
82
|
+
const type = fields[0] ?? '';
|
|
83
|
+
if (type.length !== 5 || !type.endsWith('GGA')) {
|
|
84
|
+
continue;
|
|
85
|
+
}
|
|
86
|
+
latest = parseGga(fields) ?? latest;
|
|
87
|
+
}
|
|
88
|
+
return latest;
|
|
89
|
+
}
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
import type { DeviceGeneration, EpochMillis, ObservationAuthority, ObservationEnvelope, ObservationFreshness, SourceEpoch, StableKey } from '../domain/index.js';
|
|
2
|
+
import type { MetricProvenance, ObservationSourceKind } from './provenance.js';
|
|
3
|
+
export type ObservationStaleReason = Extract<ObservationFreshness, {
|
|
4
|
+
state: 'stale';
|
|
5
|
+
}>['reason'];
|
|
6
|
+
export type ObservationUnavailableReason = Extract<ObservationFreshness, {
|
|
7
|
+
state: 'unavailable';
|
|
8
|
+
}>['reason'];
|
|
9
|
+
export type EnvelopeView<T> = {
|
|
10
|
+
readonly kind: 'valued';
|
|
11
|
+
readonly value: T;
|
|
12
|
+
readonly freshness: Extract<ObservationFreshness, {
|
|
13
|
+
state: 'fresh' | 'stale';
|
|
14
|
+
}>;
|
|
15
|
+
} | {
|
|
16
|
+
readonly kind: 'unavailable';
|
|
17
|
+
readonly freshness: Extract<ObservationFreshness, {
|
|
18
|
+
state: 'unavailable';
|
|
19
|
+
}>;
|
|
20
|
+
};
|
|
21
|
+
/**
|
|
22
|
+
* Split an envelope into its two representable shapes.
|
|
23
|
+
*
|
|
24
|
+
* The package's only cast over an envelope lives here. `ObservationEnvelope<T>` pairs
|
|
25
|
+
* `value: T` with fresh|stale and `value: null` with unavailable BY CONSTRUCTION, but
|
|
26
|
+
* the discriminant sits one level down (`freshness.state`) and TypeScript narrows a
|
|
27
|
+
* union only on a direct property — so the pairing the type already guarantees has to
|
|
28
|
+
* be restated once, here, instead of at every call site.
|
|
29
|
+
*/
|
|
30
|
+
export declare function viewEnvelope<T>(envelope: ObservationEnvelope<T>): EnvelopeView<T>;
|
|
31
|
+
/**
|
|
32
|
+
* Everything a normalizer needs that it cannot derive from the payload.
|
|
33
|
+
*
|
|
34
|
+
* There is no clock and no epoch counter in this layer: `observedAt` and
|
|
35
|
+
* `sourceEpoch` are supplied by whoever performed the read, so a normalizer cannot
|
|
36
|
+
* stamp a payload with a time it did not come from.
|
|
37
|
+
*/
|
|
38
|
+
export type NormalizationContext = {
|
|
39
|
+
readonly stableKey: StableKey;
|
|
40
|
+
readonly generation: DeviceGeneration;
|
|
41
|
+
readonly sourceEpoch: SourceEpoch;
|
|
42
|
+
readonly observedAt: EpochMillis;
|
|
43
|
+
readonly authority?: ObservationAuthority;
|
|
44
|
+
};
|
|
45
|
+
export declare function contextAuthority(context: NormalizationContext): ObservationAuthority;
|
|
46
|
+
/**
|
|
47
|
+
* Provenance for one metric.
|
|
48
|
+
*
|
|
49
|
+
* `authority` is per-METRIC rather than inherited wholesale, because one payload can
|
|
50
|
+
* mix classes: a router's RSRP is a measurement the modem reported, while its bar
|
|
51
|
+
* count is a vendor rendering of that measurement, and calling both `authoritative`
|
|
52
|
+
* would let a consumer treat a marketing scale as a reading.
|
|
53
|
+
*/
|
|
54
|
+
export declare function metricProvenance(source: ObservationSourceKind, context: NormalizationContext, rawFields: readonly string[], authority?: ObservationAuthority): MetricProvenance;
|
|
55
|
+
/** A fresh envelope around a normalized value. */
|
|
56
|
+
export declare function freshObservation<T>(source: ObservationSourceKind, context: NormalizationContext, value: T): ObservationEnvelope<T>;
|
|
57
|
+
/**
|
|
58
|
+
* An envelope for a read that produced no payload at all.
|
|
59
|
+
*
|
|
60
|
+
* It carries `value: null` by construction — there is no overload that could invent
|
|
61
|
+
* one — which is also why a normalizer holding vendor bytes must not use it.
|
|
62
|
+
*/
|
|
63
|
+
export declare function unavailableObservation<T>(source: ObservationSourceKind, context: NormalizationContext, reason: ObservationUnavailableReason): ObservationEnvelope<T>;
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
// Envelope construction for normalizers.
|
|
2
|
+
//
|
|
3
|
+
// One rule shapes this module: **a payload that arrived is an OBSERVATION, however
|
|
4
|
+
// little of it could be read.** A refused HiLink session, an unparseable goform body
|
|
5
|
+
// and a UFI endpoint that answered without the field all produce a FRESH envelope
|
|
6
|
+
// whose metrics are `unknown` with a reason — not an `unavailable` one. That matters
|
|
7
|
+
// for a reason beyond taxonomy: `ObservationEnvelope` pairs `unavailable` with
|
|
8
|
+
// `value: null`, so emitting `unavailable` for a payload we did hold would throw the
|
|
9
|
+
// diagnostics block away with it, and the raw vendor fields with that.
|
|
10
|
+
//
|
|
11
|
+
// `unavailable` is therefore reserved for the case where there is no payload at all —
|
|
12
|
+
// the device is gone, or the provider could not be engaged.
|
|
13
|
+
/**
|
|
14
|
+
* Split an envelope into its two representable shapes.
|
|
15
|
+
*
|
|
16
|
+
* The package's only cast over an envelope lives here. `ObservationEnvelope<T>` pairs
|
|
17
|
+
* `value: T` with fresh|stale and `value: null` with unavailable BY CONSTRUCTION, but
|
|
18
|
+
* the discriminant sits one level down (`freshness.state`) and TypeScript narrows a
|
|
19
|
+
* union only on a direct property — so the pairing the type already guarantees has to
|
|
20
|
+
* be restated once, here, instead of at every call site.
|
|
21
|
+
*/
|
|
22
|
+
export function viewEnvelope(envelope) {
|
|
23
|
+
return envelope.freshness.state === 'unavailable'
|
|
24
|
+
? { kind: 'unavailable', freshness: envelope.freshness }
|
|
25
|
+
: { kind: 'valued', value: envelope.value, freshness: envelope.freshness };
|
|
26
|
+
}
|
|
27
|
+
export function contextAuthority(context) {
|
|
28
|
+
return context.authority ?? 'authoritative';
|
|
29
|
+
}
|
|
30
|
+
/**
|
|
31
|
+
* Provenance for one metric.
|
|
32
|
+
*
|
|
33
|
+
* `authority` is per-METRIC rather than inherited wholesale, because one payload can
|
|
34
|
+
* mix classes: a router's RSRP is a measurement the modem reported, while its bar
|
|
35
|
+
* count is a vendor rendering of that measurement, and calling both `authoritative`
|
|
36
|
+
* would let a consumer treat a marketing scale as a reading.
|
|
37
|
+
*/
|
|
38
|
+
export function metricProvenance(source, context, rawFields, authority) {
|
|
39
|
+
return {
|
|
40
|
+
source,
|
|
41
|
+
sourceEpoch: context.sourceEpoch,
|
|
42
|
+
observedAt: context.observedAt,
|
|
43
|
+
authority: authority ?? contextAuthority(context),
|
|
44
|
+
rawFields,
|
|
45
|
+
};
|
|
46
|
+
}
|
|
47
|
+
/** A fresh envelope around a normalized value. */
|
|
48
|
+
export function freshObservation(source, context, value) {
|
|
49
|
+
return {
|
|
50
|
+
stableKey: context.stableKey,
|
|
51
|
+
generation: context.generation,
|
|
52
|
+
source,
|
|
53
|
+
sourceEpoch: context.sourceEpoch,
|
|
54
|
+
observedAt: context.observedAt,
|
|
55
|
+
authority: contextAuthority(context),
|
|
56
|
+
freshness: { state: 'fresh' },
|
|
57
|
+
value,
|
|
58
|
+
};
|
|
59
|
+
}
|
|
60
|
+
/**
|
|
61
|
+
* An envelope for a read that produced no payload at all.
|
|
62
|
+
*
|
|
63
|
+
* It carries `value: null` by construction — there is no overload that could invent
|
|
64
|
+
* one — which is also why a normalizer holding vendor bytes must not use it.
|
|
65
|
+
*/
|
|
66
|
+
export function unavailableObservation(source, context, reason) {
|
|
67
|
+
return {
|
|
68
|
+
stableKey: context.stableKey,
|
|
69
|
+
generation: context.generation,
|
|
70
|
+
source,
|
|
71
|
+
sourceEpoch: context.sourceEpoch,
|
|
72
|
+
observedAt: context.observedAt,
|
|
73
|
+
authority: contextAuthority(context),
|
|
74
|
+
freshness: { state: 'unavailable', since: context.observedAt, reason },
|
|
75
|
+
value: null,
|
|
76
|
+
};
|
|
77
|
+
}
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import { type DeviceGeneration, type EpochMillis, type ObservationEnvelope, type SourceEpoch } from '../domain/index.js';
|
|
2
|
+
/** How long a retained value is reported fresh before it ages into `stale`. */
|
|
3
|
+
export type FreshnessWindow = {
|
|
4
|
+
readonly ttlMs: number;
|
|
5
|
+
};
|
|
6
|
+
export type FreshnessEvaluation = {
|
|
7
|
+
/** The caller's current time. This module never reads a clock of its own. */
|
|
8
|
+
readonly at: EpochMillis;
|
|
9
|
+
readonly window: FreshnessWindow;
|
|
10
|
+
/** The source's current epoch; an older envelope epoch is superseded. */
|
|
11
|
+
readonly currentSourceEpoch?: SourceEpoch;
|
|
12
|
+
/** The device's current generation; an envelope from an older one is fenced out. */
|
|
13
|
+
readonly currentGeneration?: DeviceGeneration;
|
|
14
|
+
/** `false` marks a source that is answering but degraded (e.g. a reconnecting bus). */
|
|
15
|
+
readonly sourceHealthy?: boolean;
|
|
16
|
+
};
|
|
17
|
+
/** How long ago an envelope was observed, floored at zero for a clock that moved back. */
|
|
18
|
+
export declare function observationAgeMs<T>(envelope: ObservationEnvelope<T>, at: EpochMillis): number;
|
|
19
|
+
/**
|
|
20
|
+
* Re-classify an envelope against the current time, epoch, generation and source
|
|
21
|
+
* health.
|
|
22
|
+
*
|
|
23
|
+
* Trigger precedence, when several apply at once: superseded generation → superseded
|
|
24
|
+
* source epoch → degraded source → TTL expiry. The first three are positive statements
|
|
25
|
+
* that the reading has been overtaken by a newer reality; TTL expiry only says nobody
|
|
26
|
+
* has looked recently, and reporting it over a supersession would understate why.
|
|
27
|
+
*/
|
|
28
|
+
export declare function evaluateFreshness<T>(envelope: ObservationEnvelope<T>, evaluation: FreshnessEvaluation): ObservationEnvelope<T>;
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
// Freshness evaluation — how a retained observation ages, and what it never becomes.
|
|
2
|
+
//
|
|
3
|
+
// Three rules carry the weight here:
|
|
4
|
+
//
|
|
5
|
+
// 1. **Staleness keeps the value.** An aged reading is the last thing the device
|
|
6
|
+
// actually said; discarding it leaves an operator with a blank field and no way
|
|
7
|
+
// to tell "we lost contact" from "the modem reports nothing".
|
|
8
|
+
// 2. **Unavailable is terminal on re-evaluation.** An envelope that carries no value
|
|
9
|
+
// cannot become stale, because there is nothing to age. Re-classifying it would
|
|
10
|
+
// have to invent a value to be stale about.
|
|
11
|
+
// 3. **Staleness is monotonic.** A stale envelope is returned UNCHANGED, so its
|
|
12
|
+
// `since` and `reason` record the FIRST cause. Freshness comes from a new read,
|
|
13
|
+
// never from re-evaluating an old one.
|
|
14
|
+
//
|
|
15
|
+
// This module has no clock: the caller supplies `at` on every evaluation, which is
|
|
16
|
+
// what makes the whole window testable without waiting. Same discipline as
|
|
17
|
+
// `location/fix-state.ts`.
|
|
18
|
+
import { epochMillis, isCurrentGeneration, } from '../domain/index.js';
|
|
19
|
+
import { viewEnvelope } from './envelope.js';
|
|
20
|
+
/** How long ago an envelope was observed, floored at zero for a clock that moved back. */
|
|
21
|
+
export function observationAgeMs(envelope, at) {
|
|
22
|
+
return Math.max(0, at - envelope.observedAt);
|
|
23
|
+
}
|
|
24
|
+
/**
|
|
25
|
+
* Re-classify an envelope against the current time, epoch, generation and source
|
|
26
|
+
* health.
|
|
27
|
+
*
|
|
28
|
+
* Trigger precedence, when several apply at once: superseded generation → superseded
|
|
29
|
+
* source epoch → degraded source → TTL expiry. The first three are positive statements
|
|
30
|
+
* that the reading has been overtaken by a newer reality; TTL expiry only says nobody
|
|
31
|
+
* has looked recently, and reporting it over a supersession would understate why.
|
|
32
|
+
*/
|
|
33
|
+
export function evaluateFreshness(envelope, evaluation) {
|
|
34
|
+
const view = viewEnvelope(envelope);
|
|
35
|
+
if (view.kind === 'unavailable' || view.freshness.state !== 'fresh') {
|
|
36
|
+
return envelope;
|
|
37
|
+
}
|
|
38
|
+
const reason = staleReason(envelope, evaluation);
|
|
39
|
+
if (reason === undefined) {
|
|
40
|
+
return envelope;
|
|
41
|
+
}
|
|
42
|
+
return {
|
|
43
|
+
...envelope,
|
|
44
|
+
freshness: { state: 'stale', since: staleSince(envelope, evaluation, reason), reason },
|
|
45
|
+
value: view.value,
|
|
46
|
+
};
|
|
47
|
+
}
|
|
48
|
+
function staleReason(envelope, evaluation) {
|
|
49
|
+
if (evaluation.currentGeneration !== undefined &&
|
|
50
|
+
!isCurrentGeneration(envelope.generation, evaluation.currentGeneration)) {
|
|
51
|
+
return 'source-epoch-superseded';
|
|
52
|
+
}
|
|
53
|
+
if (evaluation.currentSourceEpoch !== undefined &&
|
|
54
|
+
envelope.sourceEpoch !== evaluation.currentSourceEpoch) {
|
|
55
|
+
return 'source-epoch-superseded';
|
|
56
|
+
}
|
|
57
|
+
if (evaluation.sourceHealthy === false) {
|
|
58
|
+
return 'source-degraded';
|
|
59
|
+
}
|
|
60
|
+
return observationAgeMs(envelope, evaluation.at) > evaluation.window.ttlMs
|
|
61
|
+
? 'ttl-expired'
|
|
62
|
+
: undefined;
|
|
63
|
+
}
|
|
64
|
+
/**
|
|
65
|
+
* When the reading became stale.
|
|
66
|
+
*
|
|
67
|
+
* A TTL expiry has an exact moment — one window after it was observed — and reporting
|
|
68
|
+
* the evaluation time instead would make a reading that expired an hour ago look like
|
|
69
|
+
* it just went stale. A supersession or a degradation is only known at the moment it
|
|
70
|
+
* is evaluated, so those honestly report `at`.
|
|
71
|
+
*/
|
|
72
|
+
function staleSince(envelope, evaluation, reason) {
|
|
73
|
+
return reason === 'ttl-expired'
|
|
74
|
+
? epochMillis(envelope.observedAt + evaluation.window.ttlMs)
|
|
75
|
+
: evaluation.at;
|
|
76
|
+
}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
export * from './envelope.js';
|
|
2
|
+
export * from './freshness.js';
|
|
3
|
+
export * from './metric.js';
|
|
4
|
+
export * from './model.js';
|
|
5
|
+
export * from './provenance.js';
|
|
6
|
+
export * from './raw.js';
|
|
7
|
+
export * from './reading.js';
|
|
8
|
+
export * from './sources/hilink.js';
|
|
9
|
+
export * from './sources/modemmanager.js';
|
|
10
|
+
export * from './sources/router-shared.js';
|
|
11
|
+
export * from './sources/ufi.js';
|
|
12
|
+
export * from './sources/zte.js';
|
|
13
|
+
export * from './state-separation.js';
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
// The observation layer — normalization, provenance, freshness.
|
|
2
|
+
//
|
|
3
|
+
// It sits directly on top of the migrated pure logic (`domain/mm-enums.ts`,
|
|
4
|
+
// `domain/modem-presentation.ts`, `hardware/router-parsers.ts`) and turns a raw
|
|
5
|
+
// per-vendor payload into ONE `ObservationEnvelope<NormalizedModemObservation>` that
|
|
6
|
+
// carries where every value came from and why any value is missing. It opens no
|
|
7
|
+
// transport: a provider performs the read, this layer explains the result.
|
|
8
|
+
//
|
|
9
|
+
// Reachable through the package root, deliberately not through a new subpath — the
|
|
10
|
+
// public specifier set is a frozen contract and this is normalization, which the root
|
|
11
|
+
// entry already owns.
|
|
12
|
+
export * from './envelope.js';
|
|
13
|
+
export * from './freshness.js';
|
|
14
|
+
export * from './metric.js';
|
|
15
|
+
export * from './model.js';
|
|
16
|
+
export * from './provenance.js';
|
|
17
|
+
export * from './raw.js';
|
|
18
|
+
export * from './reading.js';
|
|
19
|
+
export * from './sources/hilink.js';
|
|
20
|
+
export * from './sources/modemmanager.js';
|
|
21
|
+
export * from './sources/router-shared.js';
|
|
22
|
+
export * from './sources/ufi.js';
|
|
23
|
+
export * from './sources/zte.js';
|
|
24
|
+
export * from './state-separation.js';
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
import type { RouterSignalMetric, RouterSignalUnknownReason } from '../hardware/router-parsers.js';
|
|
2
|
+
import type { MetricProvenance } from './provenance.js';
|
|
3
|
+
export declare const METRIC_UNKNOWN_REASONS: readonly ["unsupported", "not-reported", "not-observed", "malformed", "auth-expired", "refused", "unreachable"];
|
|
4
|
+
export type MetricUnknownReason = (typeof METRIC_UNKNOWN_REASONS)[number];
|
|
5
|
+
/**
|
|
6
|
+
* The two classes an unknown reason falls into.
|
|
7
|
+
*
|
|
8
|
+
* `capability` is a durable statement about the source; `read` is a statement about
|
|
9
|
+
* one attempt and may differ on the next one. A consumer deciding whether to HIDE a
|
|
10
|
+
* control (capability) or show it as pending (read) must branch on this, never on the
|
|
11
|
+
* bare fact that a value is missing.
|
|
12
|
+
*/
|
|
13
|
+
export type MetricUnknownClass = 'capability' | 'read';
|
|
14
|
+
export declare function metricUnknownClass(reason: MetricUnknownReason): MetricUnknownClass;
|
|
15
|
+
/** Whether a reason is the positive "this source cannot report it" claim. */
|
|
16
|
+
export declare function isCapabilityUnknown(reason: MetricUnknownReason): reason is 'unsupported';
|
|
17
|
+
export type NormalizedMetric<T> = {
|
|
18
|
+
readonly state: 'known';
|
|
19
|
+
readonly value: T;
|
|
20
|
+
readonly provenance: MetricProvenance;
|
|
21
|
+
} | {
|
|
22
|
+
readonly state: 'unknown';
|
|
23
|
+
readonly reason: MetricUnknownReason;
|
|
24
|
+
readonly provenance: MetricProvenance;
|
|
25
|
+
};
|
|
26
|
+
export declare function knownMetric<T>(value: T, provenance: MetricProvenance): NormalizedMetric<T>;
|
|
27
|
+
export declare function unknownMetric<T>(reason: MetricUnknownReason, provenance: MetricProvenance): NormalizedMetric<T>;
|
|
28
|
+
/**
|
|
29
|
+
* Lift an optional decode result into a metric.
|
|
30
|
+
*
|
|
31
|
+
* `reason` is supplied by the caller precisely so this helper cannot pick one: the
|
|
32
|
+
* difference between "the provider omitted the field" and "this provider has no such
|
|
33
|
+
* field" is knowledge the decoder does not have and the call site does.
|
|
34
|
+
*/
|
|
35
|
+
export declare function metricFromOptional<T>(value: T | undefined, reason: MetricUnknownReason, provenance: MetricProvenance): NormalizedMetric<T>;
|
|
36
|
+
/**
|
|
37
|
+
* The migrated router parsers' own unknown vocabulary, carried across UNCHANGED.
|
|
38
|
+
*
|
|
39
|
+
* Every member of `RouterSignalUnknownReason` is a member of `MetricUnknownReason`
|
|
40
|
+
* with the same meaning, so this is a widening and never a re-classification — in
|
|
41
|
+
* particular `not-reported` stays `not-reported` and does not become `unsupported`.
|
|
42
|
+
*/
|
|
43
|
+
export declare function metricUnknownReasonFromRouter(reason: RouterSignalUnknownReason | 'refused'): MetricUnknownReason;
|
|
44
|
+
/** Wrap one migrated router signal metric with its provenance. */
|
|
45
|
+
export declare function metricFromRouterSignal(metric: RouterSignalMetric, provenance: MetricProvenance): NormalizedMetric<number>;
|
|
46
|
+
/** Transform a known value while preserving state, reason and provenance. */
|
|
47
|
+
export declare function mapMetric<T, U>(metric: NormalizedMetric<T>, transform: (value: T) => U): NormalizedMetric<U>;
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
// A normalized metric: a value, or an explicit reason there is none — with the
|
|
2
|
+
// provenance of the exact reading that produced it.
|
|
3
|
+
//
|
|
4
|
+
// `unknown` is a FIRST-CLASS state here, and its reason is what keeps it from
|
|
5
|
+
// collapsing into `unsupported`. Those two answer different questions: `unsupported`
|
|
6
|
+
// is a positive claim about the SOURCE ("this provider cannot express this datum at
|
|
7
|
+
// all"), while every other reason is a claim about one READ ("the provider could have
|
|
8
|
+
// said, and did not / could not be asked / answered nonsense"). Folding the second
|
|
9
|
+
// class into the first is how a control disappears from an operator's screen because
|
|
10
|
+
// one poll came back empty.
|
|
11
|
+
export const METRIC_UNKNOWN_REASONS = [
|
|
12
|
+
/** A positive claim about the SOURCE: it cannot express this datum at all. */
|
|
13
|
+
'unsupported',
|
|
14
|
+
/** The source answered and simply did not include this field. */
|
|
15
|
+
'not-reported',
|
|
16
|
+
/** Nobody has read this yet. Says nothing about the source or the device. */
|
|
17
|
+
'not-observed',
|
|
18
|
+
/** The source answered with something this layer could not decode. */
|
|
19
|
+
'malformed',
|
|
20
|
+
/** The source refused the read; the session needs re-authentication. */
|
|
21
|
+
'auth-expired',
|
|
22
|
+
/** The source answered with an explicit refusal code for this field. */
|
|
23
|
+
'refused',
|
|
24
|
+
/** The source could not be reached for this read. */
|
|
25
|
+
'unreachable',
|
|
26
|
+
];
|
|
27
|
+
export function metricUnknownClass(reason) {
|
|
28
|
+
return reason === 'unsupported' ? 'capability' : 'read';
|
|
29
|
+
}
|
|
30
|
+
/** Whether a reason is the positive "this source cannot report it" claim. */
|
|
31
|
+
export function isCapabilityUnknown(reason) {
|
|
32
|
+
return metricUnknownClass(reason) === 'capability';
|
|
33
|
+
}
|
|
34
|
+
export function knownMetric(value, provenance) {
|
|
35
|
+
return { state: 'known', value, provenance };
|
|
36
|
+
}
|
|
37
|
+
export function unknownMetric(reason, provenance) {
|
|
38
|
+
return { state: 'unknown', reason, provenance };
|
|
39
|
+
}
|
|
40
|
+
/**
|
|
41
|
+
* Lift an optional decode result into a metric.
|
|
42
|
+
*
|
|
43
|
+
* `reason` is supplied by the caller precisely so this helper cannot pick one: the
|
|
44
|
+
* difference between "the provider omitted the field" and "this provider has no such
|
|
45
|
+
* field" is knowledge the decoder does not have and the call site does.
|
|
46
|
+
*/
|
|
47
|
+
export function metricFromOptional(value, reason, provenance) {
|
|
48
|
+
return value === undefined
|
|
49
|
+
? unknownMetric(reason, provenance)
|
|
50
|
+
: knownMetric(value, provenance);
|
|
51
|
+
}
|
|
52
|
+
/**
|
|
53
|
+
* The migrated router parsers' own unknown vocabulary, carried across UNCHANGED.
|
|
54
|
+
*
|
|
55
|
+
* Every member of `RouterSignalUnknownReason` is a member of `MetricUnknownReason`
|
|
56
|
+
* with the same meaning, so this is a widening and never a re-classification — in
|
|
57
|
+
* particular `not-reported` stays `not-reported` and does not become `unsupported`.
|
|
58
|
+
*/
|
|
59
|
+
export function metricUnknownReasonFromRouter(reason) {
|
|
60
|
+
return reason;
|
|
61
|
+
}
|
|
62
|
+
/** Wrap one migrated router signal metric with its provenance. */
|
|
63
|
+
export function metricFromRouterSignal(metric, provenance) {
|
|
64
|
+
return metric.state === 'known'
|
|
65
|
+
? knownMetric(metric.value, provenance)
|
|
66
|
+
: unknownMetric(metricUnknownReasonFromRouter(metric.reason), provenance);
|
|
67
|
+
}
|
|
68
|
+
/** Transform a known value while preserving state, reason and provenance. */
|
|
69
|
+
export function mapMetric(metric, transform) {
|
|
70
|
+
return metric.state === 'known'
|
|
71
|
+
? knownMetric(transform(metric.value), metric.provenance)
|
|
72
|
+
: unknownMetric(metric.reason, metric.provenance);
|
|
73
|
+
}
|