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