@ceralive/modem-control 1.0.0 → 1.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +341 -0
- package/dist/backend/at-lease.d.ts +59 -0
- package/dist/backend/at-lease.js +118 -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 +101 -0
- package/dist/backend/transition-preconditions.js +132 -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 +216 -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 +20 -0
- package/dist/index.js +27 -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 +61 -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/operation-ids.d.ts +2 -0
- package/dist/operation-ids.js +26 -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 +10 -0
- package/dist/providers/modem-manager/generic-operations.js +209 -0
- package/dist/providers/modem-manager/index.d.ts +4 -0
- package/dist/providers/modem-manager/index.js +4 -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 +41 -0
- package/dist/providers/modem-manager/provider.js +155 -0
- package/dist/providers/modem-manager/runtime-composition-operation.d.ts +32 -0
- package/dist/providers/modem-manager/runtime-composition-operation.js +151 -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 +137 -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 +171 -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 +56 -0
- package/dist/providers/zte-goform/provider.js +101 -0
- package/dist/providers/zte-goform/session.d.ts +23 -0
- package/dist/providers/zte-goform/session.js +197 -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 +88 -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 +138 -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 +6 -0
- package/dist/usb-mode/index.js +16 -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/runtime-capability.d.ts +59 -0
- package/dist/usb-mode/runtime-capability.js +157 -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 +106 -0
- package/dist/ussd/session.js +163 -0
- package/package.json +38 -4
- package/src/backend/at-lease.test.ts +0 -106
- package/src/backend/at-lease.ts +0 -158
- package/src/backend/cell-info.test.ts +0 -154
- package/src/backend/cell-info.ts +0 -160
- package/src/backend/device-classifier.test.ts +0 -168
- package/src/backend/device-classifier.ts +0 -248
- package/src/backend/enrichment.ts +0 -96
- package/src/backend/features.test.ts +0 -162
- package/src/backend/features.ts +0 -179
- package/src/backend/identity-ladder.test.ts +0 -117
- package/src/backend/identity-ladder.ts +0 -221
- package/src/backend/identity-registry.test.ts +0 -89
- package/src/backend/identity-registry.ts +0 -151
- package/src/backend/index.ts +0 -236
- package/src/backend/lifecycle-interlock.ts +0 -38
- package/src/backend/managed-objects.ts +0 -108
- package/src/backend/mapping.ts +0 -160
- package/src/backend/mm-backend.ts +0 -191
- package/src/backend/mm-mutations.ts +0 -228
- package/src/backend/modem-actor.test.ts +0 -95
- package/src/backend/modem-actor.ts +0 -112
- package/src/backend/nm-auto-apn.ts +0 -161
- package/src/backend/nm-gsm-fields.ts +0 -122
- package/src/backend/nmcli-nm-port.ts +0 -228
- package/src/backend/nmcli-runner.ts +0 -52
- package/src/backend/observer.ts +0 -297
- package/src/backend/power-contract.test.ts +0 -40
- package/src/backend/power-contract.ts +0 -83
- package/src/backend/recovery-attribution.test.ts +0 -102
- package/src/backend/recovery-attribution.ts +0 -86
- package/src/backend/recovery-budget.test.ts +0 -64
- package/src/backend/recovery-budget.ts +0 -84
- package/src/backend/recovery-ladder.test.ts +0 -257
- package/src/backend/recovery-ladder.ts +0 -249
- package/src/backend/router-ethernet.test.ts +0 -71
- package/src/backend/router-ethernet.ts +0 -90
- package/src/backend/row-store.ts +0 -105
- package/src/backend/signal-setup.ts +0 -112
- package/src/backend/sim-unlock.ts +0 -193
- package/src/backend/transition-preconditions.ts +0 -149
- package/src/backend/uhubctl-power-hook.test.ts +0 -274
- package/src/backend/uhubctl-power-hook.ts +0 -377
- package/src/backend/usage/accounting.test.ts +0 -147
- package/src/backend/usage/accounting.ts +0 -123
- package/src/backend/usage/billing-cycle.test.ts +0 -62
- package/src/backend/usage/billing-cycle.ts +0 -45
- package/src/backend/usage/index.ts +0 -60
- package/src/backend/usage/policy-store.test.ts +0 -164
- package/src/backend/usage/policy-store.ts +0 -216
- package/src/backend/usage/policy-write.test.ts +0 -198
- package/src/backend/usage/policy-write.ts +0 -207
- package/src/backend/usage/proc-net-dev.test.ts +0 -56
- package/src/backend/usage/sampler.test.ts +0 -327
- package/src/backend/usage/sampler.ts +0 -282
- package/src/backend/usage/store.test.ts +0 -148
- package/src/backend/usage/store.ts +0 -177
- package/src/backend/usb-enumerator.test.ts +0 -87
- package/src/backend/usb-enumerator.ts +0 -181
- package/src/backend/usb-mode-transition.test.ts +0 -323
- package/src/backend/usb-mode-transition.ts +0 -253
- package/src/domain/brand.ts +0 -29
- package/src/domain/errors.ts +0 -77
- package/src/domain/guards.test.ts +0 -218
- package/src/domain/guards.ts +0 -144
- package/src/domain/identity.test.ts +0 -83
- package/src/domain/identity.ts +0 -165
- package/src/domain/index.ts +0 -12
- package/src/domain/snapshot.test.ts +0 -266
- package/src/domain/snapshot.ts +0 -120
- package/src/index.test.ts +0 -6
- package/src/index.ts +0 -15
- package/src/ports/README.md +0 -61
- package/src/ports/forbidden-surface.test.ts +0 -241
- package/src/ports/modem-manager.ts +0 -72
- package/src/ports/network-manager.ts +0 -87
- package/src/ports/ops.ts +0 -60
- package/src/ports/ops.type-test.ts +0 -39
- package/src/ports/receipts.test.ts +0 -153
- package/src/ports/reconcile.test.ts +0 -152
- package/src/ports/reconcile.ts +0 -338
- package/src/ports/router.ts +0 -29
- package/src/redact.test.ts +0 -82
- package/src/redact.ts +0 -73
- package/src/transport/README.md +0 -65
- package/src/transport/calls.ts +0 -113
- package/src/transport/characterization.test.ts +0 -260
- package/src/transport/codec.test.ts +0 -118
- package/src/transport/codec.ts +0 -240
- package/src/transport/conformance-python.test.ts +0 -152
- package/src/transport/conformance-same-lib.test.ts +0 -115
- package/src/transport/dbus-native-lib.d.ts +0 -19
- package/src/transport/dbus-native.ts +0 -85
- package/src/transport/index.ts +0 -30
- package/src/transport/no-library-leak.test.ts +0 -61
- package/src/transport/reliability.test.ts +0 -173
- package/src/transport/signals.ts +0 -150
- package/src/transport/signature.ts +0 -110
- package/src/transport/test-support/fake-service.ts +0 -168
- package/src/transport/test-support/independent-producer.py +0 -110
- package/src/transport/test-support/private-bus.ts +0 -66
- package/src/transport/transport.ts +0 -250
- package/src/transport/types.ts +0 -118
- package/src/usb-mode/catalog-schema.test.ts +0 -181
- package/src/usb-mode/catalog-schema.ts +0 -113
- package/src/usb-mode/certified-catalog.json +0 -67
- package/src/usb-mode/index.ts +0 -58
- package/src/usb-mode/ingestion.test.ts +0 -268
- package/src/usb-mode/ingestion.ts +0 -297
- package/src/usb-mode/promotion-review.ts +0 -117
- package/src/usb-mode/usb-devices-parse.ts +0 -196
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
import type { MutationImpact, OperationConfidence, OperationDescriptor, OperationResult } from '../domain/index.js';
|
|
2
|
+
/** The current on-disk schema version for one journal line. */
|
|
3
|
+
export declare const MODEM_CONTROL_JOURNAL_SCHEMA_VERSION = 1;
|
|
4
|
+
/** The two phases the operation engine emits, mirrored one-to-one on disk. */
|
|
5
|
+
export type JournalPhase = 'started' | 'completed';
|
|
6
|
+
/**
|
|
7
|
+
* How an operation ended, projected from `OperationResult` WITHOUT its value.
|
|
8
|
+
*
|
|
9
|
+
* `unknown-outcome` keeps the frozen domain reason union verbatim rather than
|
|
10
|
+
* widening to `string`, because those three reasons are the entire vocabulary a
|
|
11
|
+
* recovery pass branches on and a fourth spelling would silently read as an
|
|
12
|
+
* ordinary failure.
|
|
13
|
+
*/
|
|
14
|
+
export type JournalOutcome = {
|
|
15
|
+
readonly status: 'applied';
|
|
16
|
+
} | {
|
|
17
|
+
readonly status: 'refused';
|
|
18
|
+
readonly reason: string;
|
|
19
|
+
} | {
|
|
20
|
+
readonly status: 'failed';
|
|
21
|
+
readonly reason: string;
|
|
22
|
+
} | {
|
|
23
|
+
readonly status: 'unknown-outcome';
|
|
24
|
+
readonly reason: 'stale-generation' | 'write-reply-timed-out' | 'write-reply-dropped';
|
|
25
|
+
};
|
|
26
|
+
/**
|
|
27
|
+
* The descriptor facts a recovery pass needs, flattened out of the descriptor.
|
|
28
|
+
*
|
|
29
|
+
* The descriptor itself is not persisted: it carries FUNCTIONS (`readback.matches`,
|
|
30
|
+
* the constraint predicates) that no serialization round-trips, so storing it would
|
|
31
|
+
* produce a document that reads back as a different object than it was written from.
|
|
32
|
+
* These are the fields that answer "what was being changed, by whom, on what
|
|
33
|
+
* evidence" — everything a human or a reconciler needs to judge a stranded write.
|
|
34
|
+
*/
|
|
35
|
+
export interface JournalDescriptorEvidence {
|
|
36
|
+
readonly descriptorId: string;
|
|
37
|
+
readonly provider: string;
|
|
38
|
+
readonly authority: 'provider' | 'controller' | 'hardware';
|
|
39
|
+
readonly mutationImpact: MutationImpact;
|
|
40
|
+
readonly profiles: readonly string[];
|
|
41
|
+
readonly firmware: readonly string[];
|
|
42
|
+
readonly confidence: OperationConfidence;
|
|
43
|
+
}
|
|
44
|
+
interface JournalEntryBase {
|
|
45
|
+
readonly schemaVersion: typeof MODEM_CONTROL_JOURNAL_SCHEMA_VERSION;
|
|
46
|
+
readonly operationId: string;
|
|
47
|
+
/** The serialized `PhysicalModemId`. Stored as text; branding is a compile-time fact. */
|
|
48
|
+
readonly physicalModemId: string;
|
|
49
|
+
/** The serialized `DeviceGeneration` the operation was fenced to. */
|
|
50
|
+
readonly generation: number;
|
|
51
|
+
readonly recordedAtMs: number;
|
|
52
|
+
readonly descriptor: JournalDescriptorEvidence;
|
|
53
|
+
}
|
|
54
|
+
/**
|
|
55
|
+
* One journal line.
|
|
56
|
+
*
|
|
57
|
+
* The two members differ in SHAPE, not just in a label: a `started` entry has no
|
|
58
|
+
* `outcome` KEY at all. That is the same rule `observations/reading.ts` follows —
|
|
59
|
+
* a consumer cannot read an outcome off a phase that has none, so "in flight" can
|
|
60
|
+
* never be mistaken for "ended with an unset outcome".
|
|
61
|
+
*/
|
|
62
|
+
export type JournalEntry = (JournalEntryBase & {
|
|
63
|
+
readonly phase: 'started';
|
|
64
|
+
}) | (JournalEntryBase & {
|
|
65
|
+
readonly phase: 'completed';
|
|
66
|
+
readonly outcome: JournalOutcome;
|
|
67
|
+
});
|
|
68
|
+
/** Flatten a descriptor down to the serializable evidence the journal keeps. */
|
|
69
|
+
export declare function journalDescriptorEvidence<I, O>(descriptor: OperationDescriptor<I, O>): JournalDescriptorEvidence;
|
|
70
|
+
/** Project a result onto its journalable outcome, dropping the value by design. */
|
|
71
|
+
export declare function journalOutcome<O>(result: OperationResult<O>): JournalOutcome;
|
|
72
|
+
/** True when an outcome leaves the device in a state nobody has read back. */
|
|
73
|
+
export declare function outcomeRequiresReconciliation(outcome: JournalOutcome): boolean;
|
|
74
|
+
export {};
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
// The transaction journal's ENTRY vocabulary.
|
|
2
|
+
//
|
|
3
|
+
// The journal exists to answer ONE question after an unclean restart: which
|
|
4
|
+
// mutations were in flight, and which of them ended in an outcome nobody can
|
|
5
|
+
// read off the device. `operations/operation-engine.ts` already closes a
|
|
6
|
+
// per-modem gate when a write classifies `unknown-outcome`, but that gate lives
|
|
7
|
+
// in a `Set` on the engine instance — a process death takes it with it. Writing
|
|
8
|
+
// the same two facts down is what makes the gate survive the process.
|
|
9
|
+
//
|
|
10
|
+
// WHY AN EVENT LOG AND NOT A LATEST-STATE SNAPSHOT. A started event and its
|
|
11
|
+
// completion are two facts separated by exactly the window a crash lands in, so
|
|
12
|
+
// the shape has to be able to hold the first without the second. A document that
|
|
13
|
+
// only ever carries "the current state" cannot distinguish "we never dispatched"
|
|
14
|
+
// from "we dispatched and the reply never came" unless it spends a state name on
|
|
15
|
+
// each — which is how CeraUI's own mutation journal does it (see
|
|
16
|
+
// `legacy-ceraui.ts`, which reads that shape). Both are legitimate; this one is
|
|
17
|
+
// append-only because appending is the only write that cannot lose a prior fact.
|
|
18
|
+
//
|
|
19
|
+
// WHAT IS DELIBERATELY NOT RECORDED: the operation's INPUT and the operation's
|
|
20
|
+
// RETURNED VALUE. An input is routinely a PIN, a PUK, or a USSD command carrying
|
|
21
|
+
// a voucher code, and a returned value is routinely a message body or a location
|
|
22
|
+
// fix — all of them classes `../redact.ts` masks everywhere else. The journal
|
|
23
|
+
// records THAT an operation ran and HOW it ended, never WHAT was sent or read.
|
|
24
|
+
// A caller that needs a rollback payload owns persisting it beside the journal
|
|
25
|
+
// under its own redaction decision.
|
|
26
|
+
/** The current on-disk schema version for one journal line. */
|
|
27
|
+
export const MODEM_CONTROL_JOURNAL_SCHEMA_VERSION = 1;
|
|
28
|
+
/** Flatten a descriptor down to the serializable evidence the journal keeps. */
|
|
29
|
+
export function journalDescriptorEvidence(descriptor) {
|
|
30
|
+
return {
|
|
31
|
+
descriptorId: descriptor.id,
|
|
32
|
+
provider: descriptor.provider,
|
|
33
|
+
authority: descriptor.authority,
|
|
34
|
+
mutationImpact: descriptor.mutationImpact,
|
|
35
|
+
profiles: [...descriptor.evidence.profiles],
|
|
36
|
+
firmware: [...descriptor.evidence.firmware],
|
|
37
|
+
confidence: descriptor.confidence,
|
|
38
|
+
};
|
|
39
|
+
}
|
|
40
|
+
/** Project a result onto its journalable outcome, dropping the value by design. */
|
|
41
|
+
export function journalOutcome(result) {
|
|
42
|
+
switch (result.status) {
|
|
43
|
+
case 'applied':
|
|
44
|
+
return { status: 'applied' };
|
|
45
|
+
case 'refused':
|
|
46
|
+
return { status: 'refused', reason: result.reason };
|
|
47
|
+
case 'failed':
|
|
48
|
+
return { status: 'failed', reason: result.reason };
|
|
49
|
+
case 'unknown-outcome':
|
|
50
|
+
return { status: 'unknown-outcome', reason: result.reason };
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
/** True when an outcome leaves the device in a state nobody has read back. */
|
|
54
|
+
export function outcomeRequiresReconciliation(outcome) {
|
|
55
|
+
return outcome.status === 'unknown-outcome';
|
|
56
|
+
}
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
import { type JournalDecodeResult } from './codec.js';
|
|
2
|
+
import type { JournalDescriptorEvidence } from './entry.js';
|
|
3
|
+
import { type JournalOperationRecord, type JournalRecovery } from './recovery.js';
|
|
4
|
+
/** The `version` literal CeraUI's schema pins. */
|
|
5
|
+
export declare const LEGACY_CERAUI_JOURNAL_VERSION = 1;
|
|
6
|
+
/** CeraUI's cap on retained history entries per slot. */
|
|
7
|
+
export declare const LEGACY_CERAUI_HISTORY_CAP = 32;
|
|
8
|
+
export declare const LEGACY_CERAUI_MUTATION_STATES: readonly ['armed', 'executing', 'completed', 'failed', 'acknowledged', 'device-absent-quarantine', 'decommissioned', 'recommission-pending'];
|
|
9
|
+
export type LegacyCeraUiMutationState = (typeof LEGACY_CERAUI_MUTATION_STATES)[number];
|
|
10
|
+
export declare const LEGACY_CERAUI_ACK_MODES: readonly ['verified-rollback', 'force-rebaseline'];
|
|
11
|
+
export type LegacyCeraUiAckMode = (typeof LEGACY_CERAUI_ACK_MODES)[number];
|
|
12
|
+
export interface LegacyCeraUiHistoryEntry {
|
|
13
|
+
readonly state: LegacyCeraUiMutationState;
|
|
14
|
+
readonly at: number;
|
|
15
|
+
readonly detail?: string;
|
|
16
|
+
}
|
|
17
|
+
/** One CeraUI slot document, decoded verbatim. `preState` is kept opaque. */
|
|
18
|
+
export interface LegacyCeraUiMutationEntry {
|
|
19
|
+
readonly version: typeof LEGACY_CERAUI_JOURNAL_VERSION;
|
|
20
|
+
readonly stableKey: string;
|
|
21
|
+
readonly kind: string;
|
|
22
|
+
readonly state: LegacyCeraUiMutationState;
|
|
23
|
+
readonly attemptId: string;
|
|
24
|
+
readonly startedAt: number;
|
|
25
|
+
readonly updatedAt: number;
|
|
26
|
+
/** The rollback target. Opaque by design — its shape is the mutation kind's. */
|
|
27
|
+
readonly preState: Readonly<Record<string, unknown>>;
|
|
28
|
+
readonly detail?: string;
|
|
29
|
+
readonly acknowledgedMode?: LegacyCeraUiAckMode;
|
|
30
|
+
readonly history: readonly LegacyCeraUiHistoryEntry[];
|
|
31
|
+
}
|
|
32
|
+
export interface LegacyCeraUiJournalRead {
|
|
33
|
+
/** The same recovery model `reconstructJournalRecovery` produces. */
|
|
34
|
+
readonly recovery: JournalRecovery;
|
|
35
|
+
/** The decoded slot documents, verbatim, so `preState` survives the read. */
|
|
36
|
+
readonly entries: readonly LegacyCeraUiMutationEntry[];
|
|
37
|
+
}
|
|
38
|
+
export interface LegacyCeraUiJournalOptions {
|
|
39
|
+
/** REQUIRED. The embedding process owns where CeraUI put its journal. */
|
|
40
|
+
readonly dir: string;
|
|
41
|
+
}
|
|
42
|
+
/**
|
|
43
|
+
* The slot filename CeraUI derives from a stable key.
|
|
44
|
+
*
|
|
45
|
+
* RULE-D MIRROR of CeraUI's own helper. It is a plain lowercase-hex SHA-256 of the
|
|
46
|
+
* key's UTF-8 bytes; the key itself never appears in the filename in plaintext.
|
|
47
|
+
*/
|
|
48
|
+
export declare function legacyMutationSlotName(stableKey: string): string;
|
|
49
|
+
/** Validate one slot document. Throws the codec's metadata-only schema errors. */
|
|
50
|
+
export declare function validateLegacyCeraUiEntry(raw: unknown): LegacyCeraUiMutationEntry;
|
|
51
|
+
/** Decode one slot document's text. Never throws; returns a typed failure. */
|
|
52
|
+
export declare function decodeLegacyCeraUiEntry(text: string): JournalDecodeResult<LegacyCeraUiMutationEntry>;
|
|
53
|
+
/**
|
|
54
|
+
* The descriptor evidence a legacy entry can honestly supply.
|
|
55
|
+
*
|
|
56
|
+
* CeraUI's journal predates `OperationDescriptor`, so there is no descriptor to
|
|
57
|
+
* flatten. Every field below is either a fact the file actually carries (the
|
|
58
|
+
* mutation kind) or an explicit statement that the file carries nothing:
|
|
59
|
+
* `confidence: 'unknown'` rather than a borrowed default, and empty evidence
|
|
60
|
+
* arrays rather than invented profiles. `mutationImpact: 'write'` is a fact, not a
|
|
61
|
+
* guess — CeraUI's file is a MUTATION journal and records nothing else.
|
|
62
|
+
*/
|
|
63
|
+
export declare function legacyDescriptorEvidence(kind: string): JournalDescriptorEvidence;
|
|
64
|
+
/** Project one decoded slot onto the shared recovery record model. */
|
|
65
|
+
export declare function legacyOperationRecord(entry: LegacyCeraUiMutationEntry): JournalOperationRecord;
|
|
66
|
+
/**
|
|
67
|
+
* Read a whole CeraUI journal directory.
|
|
68
|
+
*
|
|
69
|
+
* An unreadable or non-conforming slot is reported as damage and LEFT IN PLACE;
|
|
70
|
+
* every readable slot still comes back. That is the same non-truncating contract
|
|
71
|
+
* the native store makes, applied to a directory instead of a file.
|
|
72
|
+
*/
|
|
73
|
+
export declare function readLegacyCeraUiJournal(options: LegacyCeraUiJournalOptions): Promise<LegacyCeraUiJournalRead>;
|
|
@@ -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
|
+
}
|