@remnic/core 9.64.0 → 9.64.1
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/dist/access-admin-ops-surface.d.ts +11 -11
- package/dist/access-admin-ops-surface.js +13 -13
- package/dist/access-authorization-probe.d.ts +11 -11
- package/dist/access-authorization-probe.js +14 -14
- package/dist/access-boundary.d.ts +11 -11
- package/dist/access-boundary.js +13 -13
- package/dist/access-cli.js +47 -47
- package/dist/access-coding-context-resolution.d.ts +1 -1
- package/dist/access-extraction-force-flush.d.ts +11 -11
- package/dist/access-extraction-force-flush.js +13 -13
- package/dist/access-health-types.d.ts +1 -1
- package/dist/access-http-lcm-compaction.d.ts +11 -11
- package/dist/access-http-lifecycle-flush.d.ts +11 -11
- package/dist/access-http-offline-stream.d.ts +11 -11
- package/dist/access-http-query.d.ts +2 -2
- package/dist/access-http-query.js +15 -15
- package/dist/access-http.d.ts +12 -12
- package/dist/access-http.js +24 -24
- package/dist/access-identity-continuity-surface.d.ts +9 -9
- package/dist/access-identity-continuity-surface.js +13 -13
- package/dist/access-lcm-surface.d.ts +11 -11
- package/dist/access-lcm-surface.js +13 -13
- package/dist/access-mcp.d.ts +11 -11
- package/dist/access-mcp.js +18 -18
- package/dist/access-memory-search-fanout.d.ts +2 -2
- package/dist/{access-namespace-preflight-CrF0FKaz.d.ts → access-namespace-preflight-Dj9uR4Cq.d.ts} +1 -1
- package/dist/access-namespace-preflight.d.ts +3 -3
- package/dist/access-namespace-preflight.js +13 -13
- package/dist/access-observe-write-surface.d.ts +11 -11
- package/dist/access-observe-write-surface.js +13 -13
- package/dist/access-offline-manifest.d.ts +9 -9
- package/dist/access-operations-batch.js +14 -14
- package/dist/access-operations.d.ts +14 -14
- package/dist/access-operations.js +17 -17
- package/dist/access-recall-concurrency.d.ts +11 -11
- package/dist/access-recall-concurrency.js +13 -13
- package/dist/access-recall-response.d.ts +11 -11
- package/dist/access-recall-response.js +13 -13
- package/dist/access-recall-surface.d.ts +11 -11
- package/dist/access-recall-surface.js +13 -13
- package/dist/access-schema.d.ts +88 -88
- package/dist/{access-service-BqF4UK8Z.d.ts → access-service-Bd0lzaHp.d.ts} +71 -71
- package/dist/access-service-helpers.d.ts +11 -11
- package/dist/access-service.d.ts +11 -11
- package/dist/access-service.js +13 -13
- package/dist/access-surface-catalog.d.ts +11 -11
- package/dist/access-surface-catalog.js +15 -15
- package/dist/access-wearables-meetings-surface.d.ts +3 -3
- package/dist/action-confidence.d.ts +1 -1
- package/dist/active-memory-bridge.d.ts +1 -1
- package/dist/active-recall.d.ts +1 -1
- package/dist/active-recall.js +1 -1
- package/dist/ambient-provenance.d.ts +1 -1
- package/dist/artifact-search.d.ts +1 -1
- package/dist/behavior-learner.d.ts +1 -1
- package/dist/behavior-signals.d.ts +1 -1
- package/dist/bootstrap.d.ts +9 -9
- package/dist/briefing-window.d.ts +2 -2
- package/dist/briefing.d.ts +2 -2
- package/dist/briefing.js +3 -3
- package/dist/buffer-surprise-report.d.ts +1 -1
- package/dist/buffer-turn-helpers.d.ts +1 -1
- package/dist/buffer.d.ts +2 -2
- package/dist/buffer.js +2 -2
- package/dist/bulk-import/index.d.ts +3 -3
- package/dist/calibration.d.ts +1 -1
- package/dist/capabilities.d.ts +1 -1
- package/dist/{catalog-C0KIwZqR.d.ts → catalog-rKYbN7zF.d.ts} +1 -1
- package/dist/causal-behavior.d.ts +1 -1
- package/dist/causal-consolidation.d.ts +1 -1
- package/dist/causal-consolidation.js +4 -4
- package/dist/causal-trajectory-graph.d.ts +1 -1
- package/dist/{chunk-5SZ6CLGJ.js → chunk-2RZCOG63.js} +3 -2
- package/dist/{chunk-5SZ6CLGJ.js.map → chunk-2RZCOG63.js.map} +1 -1
- package/dist/{chunk-7DRSWPUV.js → chunk-3CYVTO6A.js} +3 -3
- package/dist/{chunk-LJQGQUNS.js → chunk-4FSCGYTQ.js} +2 -2
- package/dist/{chunk-ML4MI2RR.js → chunk-4KKVDOCD.js} +24 -24
- package/dist/{chunk-Z3Q5I5F6.js → chunk-5Q5BEWJ3.js} +2 -2
- package/dist/{chunk-6BV6ZZZA.js → chunk-6I6IOMKS.js} +2 -2
- package/dist/{chunk-GIMQJ5RX.js → chunk-7D3TTGHY.js} +20 -11
- package/dist/chunk-7D3TTGHY.js.map +1 -0
- package/dist/{chunk-4IIDTPSK.js → chunk-7X35RPN3.js} +2 -2
- package/dist/{chunk-FEP45MYY.js → chunk-AGC3Q53R.js} +2 -2
- package/dist/{chunk-ZWYXSF2T.js → chunk-AL43367I.js} +96 -4
- package/dist/chunk-AL43367I.js.map +1 -0
- package/dist/{chunk-WCWIV2JH.js → chunk-B2GICNOJ.js} +2 -2
- package/dist/{chunk-BTBAHTQ5.js → chunk-CI6YIT5S.js} +2 -2
- package/dist/{chunk-CUDSA46O.js → chunk-CUIAHZPZ.js} +2 -2
- package/dist/{chunk-BTUIHRRR.js → chunk-D267K4V5.js} +2 -2
- package/dist/{chunk-QICOU4RY.js → chunk-DLZBOEJY.js} +2 -2
- package/dist/{chunk-AR253JBM.js → chunk-DPPTG6VJ.js} +1 -1
- package/dist/chunk-DPPTG6VJ.js.map +1 -0
- package/dist/{chunk-BPLPGOUJ.js → chunk-DRS3OX77.js} +2 -2
- package/dist/{chunk-3F3CVVDS.js → chunk-DTQ3OB7O.js} +2 -2
- package/dist/{chunk-Z4UPRZR6.js → chunk-DZ2AVS2H.js} +2 -2
- package/dist/{chunk-YINSSARY.js → chunk-EAWJSY2Z.js} +1 -1
- package/dist/{chunk-QFPX7QZG.js → chunk-FMGTFMT3.js} +9 -9
- package/dist/{chunk-CTD5CPD2.js → chunk-GSQDPL45.js} +3 -3
- package/dist/{chunk-UUYQWKHO.js → chunk-IHPYTG6F.js} +2 -2
- package/dist/{chunk-QATILW4B.js → chunk-IPUUMLSM.js} +21 -21
- package/dist/{chunk-2BRCV2IV.js → chunk-JE3AKPOO.js} +2 -2
- package/dist/{chunk-DZFPHARJ.js → chunk-KOE3RLNN.js} +2 -2
- package/dist/{chunk-WQ6TRWAE.js → chunk-NMQXUGTU.js} +3 -3
- package/dist/{chunk-65NQFK7D.js → chunk-OTVHRZZD.js} +2 -2
- package/dist/{chunk-77E7LLMV.js → chunk-OWAYQK5V.js} +8 -8
- package/dist/{chunk-YGSDWO3U.js → chunk-P4SD7BSA.js} +2 -2
- package/dist/{chunk-G5F4MXXC.js → chunk-Q3IRE6E4.js} +2 -2
- package/dist/{chunk-HZXO2CJ6.js → chunk-QHNO6AND.js} +2 -2
- package/dist/{chunk-5OOTHWGJ.js → chunk-RMB22Q3R.js} +2 -2
- package/dist/{chunk-BB2LH5JM.js → chunk-RNORDDD6.js} +11 -1
- package/dist/chunk-RNORDDD6.js.map +1 -0
- package/dist/{chunk-R7XE5ZUR.js → chunk-UIYYS2YI.js} +2 -2
- package/dist/{chunk-SYU5JOBQ.js → chunk-UWSETW6O.js} +2 -2
- package/dist/{chunk-CK3DZ6OE.js → chunk-VCNGVU62.js} +2 -2
- package/dist/{chunk-KG2CT762.js → chunk-VSKA2ATE.js} +2 -2
- package/dist/{chunk-PQODT53H.js → chunk-WDFXRHOZ.js} +3 -3
- package/dist/{chunk-6NMZXEJ7.js → chunk-WNO5W2TP.js} +2 -2
- package/dist/{chunk-63JMI45N.js → chunk-XBRGJJK7.js} +2 -2
- package/dist/{chunk-S6KMN6MJ.js → chunk-XFAWGUNX.js} +2 -2
- package/dist/{chunk-SRLQROQJ.js → chunk-XSPRZZEX.js} +3 -3
- package/dist/{chunk-EHC3FIJ4.js → chunk-Y5ZIQFWH.js} +2 -2
- package/dist/{chunk-6IKXAAJI.js → chunk-YY63AMGP.js} +2 -2
- package/dist/{chunk-7ML7STOD.js → chunk-ZGGTIBWN.js} +2 -2
- package/dist/{chunk-X6HNXVXS.js → chunk-ZUIVJ3BB.js} +4 -4
- package/dist/{cli-BX0O688c.d.ts → cli-0dDmOsJ8.d.ts} +5 -5
- package/dist/cli.d.ts +13 -13
- package/dist/cli.js +37 -37
- package/dist/coding/pre-action-gate.d.ts +1 -1
- package/dist/compounding/engine.d.ts +2 -2
- package/dist/compounding/engine.js +3 -3
- package/dist/compounding/preference-consolidator.d.ts +1 -1
- package/dist/compression-optimizer.d.ts +1 -1
- package/dist/config.d.ts +1 -1
- package/dist/config.js +1 -1
- package/dist/connectors/codex-materialize-runner.d.ts +1 -1
- package/dist/connectors/codex-materialize-runner.js +3 -3
- package/dist/connectors/codex-materialize.d.ts +1 -1
- package/dist/connectors/index.d.ts +1 -1
- package/dist/connectors/index.js +3 -3
- package/dist/consolidation-provenance-check.d.ts +2 -2
- package/dist/consolidation-undo.d.ts +2 -2
- package/dist/contradiction/index.d.ts +2 -2
- package/dist/converge-config.d.ts +1 -1
- package/dist/convergence-refresh.d.ts +2 -2
- package/dist/convergence-refresh.js +5 -5
- package/dist/conversation-index/backend.d.ts +1 -1
- package/dist/conversation-index/chunker.d.ts +1 -1
- package/dist/conversation-index/faiss-adapter.d.ts +1 -1
- package/dist/conversation-index/indexer.d.ts +1 -1
- package/dist/conversation-index/search.d.ts +1 -1
- package/dist/corpus-watermark.d.ts +1 -1
- package/dist/corpus-watermark.js +5 -5
- package/dist/day-summary.d.ts +1 -1
- package/dist/delinearize.d.ts +1 -1
- package/dist/dependency-propagation-config.d.ts +1 -1
- package/dist/{dependency-propagation-delivery-Cph4PnBe.d.ts → dependency-propagation-delivery-Cr8u-XwC.d.ts} +1 -1
- package/dist/direct-answer-wiring.d.ts +1 -1
- package/dist/direct-answer.d.ts +1 -1
- package/dist/embedding-fallback.d.ts +1 -1
- package/dist/enrichment/index.d.ts +1 -1
- package/dist/entity-origin-fields.d.ts +1 -1
- package/dist/entity-retrieval.d.ts +2 -2
- package/dist/entity-retrieval.js +3 -3
- package/dist/entity-schema.d.ts +1 -1
- package/dist/episodic-context.d.ts +1 -1
- package/dist/explicit-capture.d.ts +9 -9
- package/dist/external-wiki-access.d.ts +11 -11
- package/dist/external-wiki-access.js +14 -14
- package/dist/external-wiki-collection-registration.d.ts +1 -1
- package/dist/external-wiki-collection.d.ts +1 -1
- package/dist/external-wiki-mcp-tools.d.ts +11 -11
- package/dist/extraction-error-classification.d.ts +1 -1
- package/dist/extraction-faithfulness.d.ts +1 -1
- package/dist/extraction-judge-telemetry.d.ts +1 -1
- package/dist/extraction-judge-training.d.ts +1 -1
- package/dist/extraction-judge.d.ts +1 -1
- package/dist/extraction-liveness.d.ts +1 -1
- package/dist/extraction-normalization.d.ts +1 -1
- package/dist/extraction-prompt.d.ts +1 -1
- package/dist/extraction-source-grounding-rules.d.ts +1 -1
- package/dist/extraction-source-grounding.d.ts +1 -1
- package/dist/extraction.d.ts +1 -1
- package/dist/extraction.js +6 -6
- package/dist/fallback-llm.d.ts +1 -1
- package/dist/graph-dashboard-diff.d.ts +1 -1
- package/dist/graph-dashboard-key.d.ts +1 -1
- package/dist/graph-dashboard-parser.d.ts +1 -1
- package/dist/graph-edge-reinforcement.d.ts +1 -1
- package/dist/graph-path-reconstruction.d.ts +1 -1
- package/dist/graph-path-scoring.d.ts +1 -1
- package/dist/graph-snapshot.d.ts +1 -1
- package/dist/graph.d.ts +1 -1
- package/dist/harmonic-retrieval.js +1 -1
- package/dist/importance.d.ts +1 -1
- package/dist/importers/index.d.ts +1 -1
- package/dist/in-flight-reads.d.ts +1 -1
- package/dist/index.d.ts +697 -697
- package/dist/index.js +47 -47
- package/dist/intent.d.ts +1 -1
- package/dist/lcm/engine.d.ts +1 -1
- package/dist/lcm/index.d.ts +1 -1
- package/dist/lcm/tools.d.ts +1 -1
- package/dist/lifecycle.d.ts +1 -1
- package/dist/live-connectors-runner.d.ts +1 -1
- package/dist/local-llm.d.ts +1 -1
- package/dist/local-model-endpoint.d.ts +1 -1
- package/dist/maintenance/memory-governance.d.ts +1 -1
- package/dist/maintenance/memory-governance.js +3 -3
- package/dist/maintenance/rebuild-memory-lifecycle-ledger.d.ts +2 -2
- package/dist/maintenance/rebuild-memory-lifecycle-ledger.js +3 -3
- package/dist/maintenance/rebuild-memory-projection.d.ts +2 -2
- package/dist/maintenance/rebuild-memory-projection.js +4 -4
- package/dist/{maintenance-BNq31xpo.d.ts → maintenance-BpNjuZk4.d.ts} +3 -3
- package/dist/mcp-memory-inspector-app.d.ts +11 -11
- package/dist/memory-action-policy.d.ts +1 -1
- package/dist/memory-cache.d.ts +1 -1
- package/dist/memory-lifecycle-ledger-utils.d.ts +1 -1
- package/dist/memory-projection-store.d.ts +1 -1
- package/dist/memory-provenance.d.ts +1 -1
- package/dist/memory-snapshot.d.ts +1 -1
- package/dist/memory-worth-outcomes.d.ts +2 -2
- package/dist/models-json.d.ts +1 -1
- package/dist/namespaces/migrate.d.ts +3 -3
- package/dist/namespaces/migrate.js +4 -4
- package/dist/namespaces/principal.d.ts +1 -1
- package/dist/namespaces/search.d.ts +1 -1
- package/dist/namespaces/storage.d.ts +3 -3
- package/dist/namespaces/storage.js +3 -3
- package/dist/native-knowledge.d.ts +1 -1
- package/dist/offline-sync-impression-drain.d.ts +1 -1
- package/dist/operator-doctor-corpus.d.ts +1 -1
- package/dist/operator-doctor-corpus.js +6 -6
- package/dist/operator-doctor-replica.d.ts +1 -1
- package/dist/operator-toolkit.d.ts +3 -3
- package/dist/operator-toolkit.js +12 -12
- package/dist/orchestration/compression-guideline-coordinator.d.ts +2 -2
- package/dist/orchestration/maintenance.d.ts +4 -4
- package/dist/orchestration/maintenance.js +9 -9
- package/dist/{orchestrator-CBuUOP2J.d.ts → orchestrator-B3ZQ8CTJ.d.ts} +8 -8
- package/dist/orchestrator.d.ts +9 -9
- package/dist/orchestrator.js +47 -47
- package/dist/patterns-cli.d.ts +1 -1
- package/dist/{pipeline-CW_Q4WvX.d.ts → pipeline-q5emoXD6.d.ts} +1 -1
- package/dist/policy-runtime.d.ts +1 -1
- package/dist/proactive-contention.d.ts +1 -1
- package/dist/provenance.d.ts +1 -1
- package/dist/{public-http-BeJ1_JI1.d.ts → public-http-CQN-i7Ai.d.ts} +1 -1
- package/dist/{qmd-q-cAzYuC.d.ts → qmd-YC6YGphs.d.ts} +1 -1
- package/dist/qmd-preflight.d.ts +2 -2
- package/dist/qmd-recall-cache.d.ts +1 -1
- package/dist/qmd.d.ts +2 -2
- package/dist/recall-concurrency-config.d.ts +1 -1
- package/dist/recall-disclosure-escalation.d.ts +1 -1
- package/dist/recall-explain-renderer.d.ts +1 -1
- package/dist/recall-explain-renderer.js +3 -3
- package/dist/recall-memory-map.d.ts +1 -1
- package/dist/recall-planner-llm.d.ts +1 -1
- package/dist/recall-state.d.ts +1 -1
- package/dist/recall-tag-filter.d.ts +1 -1
- package/dist/recall-timings.d.ts +1 -1
- package/dist/recall-xray-cli.d.ts +1 -1
- package/dist/recall-xray-cli.js +4 -4
- package/dist/recall-xray-renderer.d.ts +1 -1
- package/dist/recall-xray-renderer.js +3 -3
- package/dist/recall-xray.d.ts +1 -1
- package/dist/recall-xray.js +2 -2
- package/dist/reconcile/cursor.d.ts +1 -1
- package/dist/reconcile/manifest.d.ts +1 -1
- package/dist/reconcile/plan.d.ts +1 -1
- package/dist/replay/normalizers/chatgpt.d.ts +1 -1
- package/dist/replay/normalizers/claude.d.ts +1 -1
- package/dist/replay/normalizers/openclaw.d.ts +1 -1
- package/dist/replay/normalizers/shared.d.ts +1 -1
- package/dist/replay/runner.d.ts +1 -1
- package/dist/replay/types.d.ts +1 -1
- package/dist/replica-divergence.d.ts +1 -1
- package/dist/replica-peers-config.d.ts +1 -1
- package/dist/resolve-auth-token.d.ts +1 -1
- package/dist/resume-bundles.js +2 -2
- package/dist/retrieval-agents.d.ts +2 -2
- package/dist/retrieval-tiers.d.ts +1 -1
- package/dist/routing/engine.d.ts +1 -1
- package/dist/routing/store.d.ts +1 -1
- package/dist/salvage-envelope.d.ts +1 -1
- package/dist/schemas.d.ts +108 -108
- package/dist/{scope-profiles-CIr4mXgp.d.ts → scope-profiles-De8F7YQ6.d.ts} +1 -1
- package/dist/search/embed-helper.d.ts +1 -1
- package/dist/search/factory.d.ts +1 -1
- package/dist/search/index.d.ts +1 -1
- package/dist/search/lancedb-backend.d.ts +1 -1
- package/dist/search/meilisearch-backend.d.ts +1 -1
- package/dist/search/noop-backend.d.ts +1 -1
- package/dist/search/orama-backend.d.ts +1 -1
- package/dist/search/port.d.ts +1 -1
- package/dist/search/remote-backend.d.ts +1 -1
- package/dist/{semantic-consolidation-BZ6J46j-.d.ts → semantic-consolidation-Bf-hm2LQ.d.ts} +1 -1
- package/dist/semantic-consolidation.d.ts +2 -2
- package/dist/semantic-consolidation.js +4 -4
- package/dist/semantic-rule-promotion.js +3 -3
- package/dist/semantic-rule-verifier.d.ts +1 -1
- package/dist/semantic-rule-verifier.js +3 -3
- package/dist/{service-CCl4iyyY.d.ts → service-D83osSRn.d.ts} +2 -2
- package/dist/session-observer-bands.d.ts +1 -1
- package/dist/session-observer-state.d.ts +1 -1
- package/dist/shared-context/manager.d.ts +9 -9
- package/dist/signal.d.ts +1 -1
- package/dist/source-agent-qualifier.d.ts +1 -1
- package/dist/source-agent-qualifier.js +5 -5
- package/dist/{storage-BPXmJKIt.d.ts → storage-BHWbpbhj.d.ts} +1 -1
- package/dist/storage.d.ts +2 -2
- package/dist/storage.js +2 -2
- package/dist/summarizer.d.ts +1 -1
- package/dist/summary-snapshot.d.ts +1 -1
- package/dist/support-passport/index.d.ts +14 -14
- package/dist/support-passport/index.js +16 -16
- package/dist/temporal-supersession.d.ts +2 -2
- package/dist/temporal-timeline-recall.d.ts +1 -1
- package/dist/temporal-validity.d.ts +1 -1
- package/dist/threading.d.ts +1 -1
- package/dist/tier-migration.d.ts +2 -2
- package/dist/tier-routing.d.ts +1 -1
- package/dist/topics.d.ts +1 -1
- package/dist/transcript.d.ts +1 -1
- package/dist/transfer/types.d.ts +66 -66
- package/dist/trust-score-stage.d.ts +1 -1
- package/dist/trust-score.d.ts +1 -1
- package/dist/{types-CltSWcND.d.ts → types-CpgsN-Gb.d.ts} +24 -3
- package/dist/types.d.ts +1 -1
- package/dist/types.js +1 -1
- package/dist/utility-runtime.d.ts +1 -1
- package/dist/verified-recall.js +3 -3
- package/dist/write-envelope.d.ts +1 -1
- package/package.json +2 -2
- package/src/config.ts +5 -6
- package/src/location/cli.ts +163 -0
- package/src/location/config.test.ts +75 -0
- package/src/location/config.ts +105 -0
- package/src/location/intervals.test.ts +202 -0
- package/src/location/intervals.ts +221 -0
- package/src/location/parse-companions.ts +12 -0
- package/src/location/pipeline.test.ts +324 -0
- package/src/location/pipeline.ts +245 -0
- package/src/location/recall-exclusion.test.ts +36 -0
- package/src/location/registry.test.ts +74 -0
- package/src/location/registry.ts +54 -0
- package/src/location/store.test.ts +170 -0
- package/src/location/store.ts +478 -0
- package/src/location/types.ts +95 -0
- package/src/orchestration/generic-recall-paths.ts +17 -0
- package/src/orchestration/orchestrator-helpers.ts +22 -0
- package/src/orchestration/qmd-result-resolver.ts +1 -0
- package/src/types.ts +2 -3
- package/dist/chunk-AR253JBM.js.map +0 -1
- package/dist/chunk-BB2LH5JM.js.map +0 -1
- package/dist/chunk-GIMQJ5RX.js.map +0 -1
- package/dist/chunk-ZWYXSF2T.js.map +0 -1
- /package/dist/{chunk-7DRSWPUV.js.map → chunk-3CYVTO6A.js.map} +0 -0
- /package/dist/{chunk-LJQGQUNS.js.map → chunk-4FSCGYTQ.js.map} +0 -0
- /package/dist/{chunk-ML4MI2RR.js.map → chunk-4KKVDOCD.js.map} +0 -0
- /package/dist/{chunk-Z3Q5I5F6.js.map → chunk-5Q5BEWJ3.js.map} +0 -0
- /package/dist/{chunk-6BV6ZZZA.js.map → chunk-6I6IOMKS.js.map} +0 -0
- /package/dist/{chunk-4IIDTPSK.js.map → chunk-7X35RPN3.js.map} +0 -0
- /package/dist/{chunk-FEP45MYY.js.map → chunk-AGC3Q53R.js.map} +0 -0
- /package/dist/{chunk-WCWIV2JH.js.map → chunk-B2GICNOJ.js.map} +0 -0
- /package/dist/{chunk-BTBAHTQ5.js.map → chunk-CI6YIT5S.js.map} +0 -0
- /package/dist/{chunk-CUDSA46O.js.map → chunk-CUIAHZPZ.js.map} +0 -0
- /package/dist/{chunk-BTUIHRRR.js.map → chunk-D267K4V5.js.map} +0 -0
- /package/dist/{chunk-QICOU4RY.js.map → chunk-DLZBOEJY.js.map} +0 -0
- /package/dist/{chunk-BPLPGOUJ.js.map → chunk-DRS3OX77.js.map} +0 -0
- /package/dist/{chunk-3F3CVVDS.js.map → chunk-DTQ3OB7O.js.map} +0 -0
- /package/dist/{chunk-Z4UPRZR6.js.map → chunk-DZ2AVS2H.js.map} +0 -0
- /package/dist/{chunk-YINSSARY.js.map → chunk-EAWJSY2Z.js.map} +0 -0
- /package/dist/{chunk-QFPX7QZG.js.map → chunk-FMGTFMT3.js.map} +0 -0
- /package/dist/{chunk-CTD5CPD2.js.map → chunk-GSQDPL45.js.map} +0 -0
- /package/dist/{chunk-UUYQWKHO.js.map → chunk-IHPYTG6F.js.map} +0 -0
- /package/dist/{chunk-QATILW4B.js.map → chunk-IPUUMLSM.js.map} +0 -0
- /package/dist/{chunk-2BRCV2IV.js.map → chunk-JE3AKPOO.js.map} +0 -0
- /package/dist/{chunk-DZFPHARJ.js.map → chunk-KOE3RLNN.js.map} +0 -0
- /package/dist/{chunk-WQ6TRWAE.js.map → chunk-NMQXUGTU.js.map} +0 -0
- /package/dist/{chunk-65NQFK7D.js.map → chunk-OTVHRZZD.js.map} +0 -0
- /package/dist/{chunk-77E7LLMV.js.map → chunk-OWAYQK5V.js.map} +0 -0
- /package/dist/{chunk-YGSDWO3U.js.map → chunk-P4SD7BSA.js.map} +0 -0
- /package/dist/{chunk-G5F4MXXC.js.map → chunk-Q3IRE6E4.js.map} +0 -0
- /package/dist/{chunk-HZXO2CJ6.js.map → chunk-QHNO6AND.js.map} +0 -0
- /package/dist/{chunk-5OOTHWGJ.js.map → chunk-RMB22Q3R.js.map} +0 -0
- /package/dist/{chunk-R7XE5ZUR.js.map → chunk-UIYYS2YI.js.map} +0 -0
- /package/dist/{chunk-SYU5JOBQ.js.map → chunk-UWSETW6O.js.map} +0 -0
- /package/dist/{chunk-CK3DZ6OE.js.map → chunk-VCNGVU62.js.map} +0 -0
- /package/dist/{chunk-KG2CT762.js.map → chunk-VSKA2ATE.js.map} +0 -0
- /package/dist/{chunk-PQODT53H.js.map → chunk-WDFXRHOZ.js.map} +0 -0
- /package/dist/{chunk-6NMZXEJ7.js.map → chunk-WNO5W2TP.js.map} +0 -0
- /package/dist/{chunk-63JMI45N.js.map → chunk-XBRGJJK7.js.map} +0 -0
- /package/dist/{chunk-S6KMN6MJ.js.map → chunk-XFAWGUNX.js.map} +0 -0
- /package/dist/{chunk-SRLQROQJ.js.map → chunk-XSPRZZEX.js.map} +0 -0
- /package/dist/{chunk-EHC3FIJ4.js.map → chunk-Y5ZIQFWH.js.map} +0 -0
- /package/dist/{chunk-6IKXAAJI.js.map → chunk-YY63AMGP.js.map} +0 -0
- /package/dist/{chunk-7ML7STOD.js.map → chunk-ZGGTIBWN.js.map} +0 -0
- /package/dist/{chunk-X6HNXVXS.js.map → chunk-ZUIVJ3BB.js.map} +0 -0
|
@@ -0,0 +1,163 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Location surface helpers (issue #2044): status, provider health checks, and
|
|
3
|
+
* multi-day sync. Pure `@remnic/core` — no optional packages, no host CLI
|
|
4
|
+
* wiring; a host adapter (or the core CLI, in a later slice) renders these
|
|
5
|
+
* structured results into its own command surface.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import { getLocationProvider } from "./registry.js";
|
|
9
|
+
import { isValidLocationDate, localDayKey } from "./intervals.js";
|
|
10
|
+
import { listLocationDayDates, loadLocationSyncState, parseLocationDaySummary, readLocationDay } from "./store.js";
|
|
11
|
+
import { syncLocationDay, type LocationDaySyncOptions, type LocationSourceSyncResult } from "./pipeline.js";
|
|
12
|
+
import type { LocationConfig, LocationProvider } from "./types.js";
|
|
13
|
+
|
|
14
|
+
export interface LocationSourceStatus {
|
|
15
|
+
id: string;
|
|
16
|
+
enabled: boolean;
|
|
17
|
+
providerRegistered: boolean;
|
|
18
|
+
lastSyncedAtUtc?: string;
|
|
19
|
+
trackedDays: number;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export interface LocationDayStatus {
|
|
23
|
+
date: string;
|
|
24
|
+
sources: string[];
|
|
25
|
+
observationCount: number;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
export interface LocationStatusReport {
|
|
29
|
+
enabled: boolean;
|
|
30
|
+
timezone: string;
|
|
31
|
+
sources: LocationSourceStatus[];
|
|
32
|
+
/** Stored day documents, newest first (at most `maxDays`). */
|
|
33
|
+
recentDays: LocationDayStatus[];
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
export interface LocationStatusOptions {
|
|
37
|
+
/** Provider lookup seam (defaults to the registry); tests inject doubles. */
|
|
38
|
+
getProvider?: (id: string) => LocationProvider | undefined;
|
|
39
|
+
/** Cap on day documents listed; defaults to 7. */
|
|
40
|
+
maxDays?: number;
|
|
41
|
+
now?: () => Date;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
export async function locationStatus(
|
|
45
|
+
config: LocationConfig,
|
|
46
|
+
memoryDir: string,
|
|
47
|
+
options: LocationStatusOptions = {},
|
|
48
|
+
): Promise<LocationStatusReport> {
|
|
49
|
+
const resolveProvider = options.getProvider ?? getLocationProvider;
|
|
50
|
+
const state = await loadLocationSyncState(memoryDir);
|
|
51
|
+
const sources: LocationSourceStatus[] = config.sources.map((source) => {
|
|
52
|
+
const sourceState = state.sources[source.id];
|
|
53
|
+
return {
|
|
54
|
+
id: source.id,
|
|
55
|
+
enabled: source.enabled,
|
|
56
|
+
providerRegistered: resolveProvider(source.id) !== undefined,
|
|
57
|
+
...(sourceState?.lastSyncedAtUtc === undefined ? {} : { lastSyncedAtUtc: sourceState.lastSyncedAtUtc }),
|
|
58
|
+
trackedDays: Object.keys(sourceState?.days ?? {}).length,
|
|
59
|
+
};
|
|
60
|
+
});
|
|
61
|
+
|
|
62
|
+
const maxDays = options.maxDays ?? 7;
|
|
63
|
+
const recentDays: LocationDayStatus[] = [];
|
|
64
|
+
for (const date of (await listLocationDayDates(memoryDir)).slice(0, Math.max(0, maxDays))) {
|
|
65
|
+
const raw = await readLocationDay(memoryDir, date);
|
|
66
|
+
const summary = raw === null ? null : parseLocationDaySummary(raw);
|
|
67
|
+
recentDays.push({
|
|
68
|
+
date,
|
|
69
|
+
sources: summary?.sources ?? [],
|
|
70
|
+
observationCount: summary?.observationCount ?? 0,
|
|
71
|
+
});
|
|
72
|
+
}
|
|
73
|
+
return { enabled: config.enabled, timezone: config.timezone, sources, recentDays };
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
export interface LocationSourceCheckResult {
|
|
77
|
+
id: string;
|
|
78
|
+
ok: boolean;
|
|
79
|
+
detail?: string;
|
|
80
|
+
skipped?: "location-disabled" | "source-disabled" | "provider-not-registered";
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
export interface LocationCheckOptions {
|
|
84
|
+
signal?: AbortSignal;
|
|
85
|
+
getProvider?: (id: string) => LocationProvider | undefined;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
/** Probe every configured provider's health. The master gate short-circuits. */
|
|
89
|
+
export async function checkLocationSources(
|
|
90
|
+
config: LocationConfig,
|
|
91
|
+
options: LocationCheckOptions = {},
|
|
92
|
+
): Promise<LocationSourceCheckResult[]> {
|
|
93
|
+
if (!config.enabled) {
|
|
94
|
+
return config.sources.map((source) => ({ id: source.id, ok: false, skipped: "location-disabled" as const }));
|
|
95
|
+
}
|
|
96
|
+
const resolveProvider = options.getProvider ?? getLocationProvider;
|
|
97
|
+
const results: LocationSourceCheckResult[] = [];
|
|
98
|
+
for (const source of config.sources) {
|
|
99
|
+
if (!source.enabled) {
|
|
100
|
+
results.push({ id: source.id, ok: false, skipped: "source-disabled" });
|
|
101
|
+
continue;
|
|
102
|
+
}
|
|
103
|
+
const provider = resolveProvider(source.id);
|
|
104
|
+
if (provider === undefined) {
|
|
105
|
+
results.push({ id: source.id, ok: false, skipped: "provider-not-registered" });
|
|
106
|
+
continue;
|
|
107
|
+
}
|
|
108
|
+
try {
|
|
109
|
+
const check = await provider.verify(options.signal);
|
|
110
|
+
results.push({
|
|
111
|
+
id: source.id,
|
|
112
|
+
ok: check.ok,
|
|
113
|
+
...(check.detail === undefined || check.detail.length === 0 ? {} : { detail: check.detail }),
|
|
114
|
+
});
|
|
115
|
+
} catch (error) {
|
|
116
|
+
results.push({ id: source.id, ok: false, detail: error instanceof Error ? error.name : "provider error" });
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
return results;
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
function shiftIsoDate(date: string, days: number): string {
|
|
123
|
+
const parsed = new Date(`${date}T00:00:00Z`);
|
|
124
|
+
parsed.setUTCDate(parsed.getUTCDate() + days);
|
|
125
|
+
return parsed.toISOString().slice(0, 10);
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
export interface LocationSyncRunOptions
|
|
129
|
+
extends Omit<LocationDaySyncOptions, "config" | "date" | "memoryDir"> {
|
|
130
|
+
config: LocationConfig;
|
|
131
|
+
memoryDir: string;
|
|
132
|
+
/** Exclusive end date (local YYYY-MM-DD); defaults to today in config.timezone. */
|
|
133
|
+
endDate?: string;
|
|
134
|
+
/** Number of days to sync ending at endDate; defaults to config.syncDays. */
|
|
135
|
+
days?: number;
|
|
136
|
+
now?: () => Date;
|
|
137
|
+
}
|
|
138
|
+
/**
|
|
139
|
+
* Sync the trailing `days` local days ending AT `endDate` (inclusive).
|
|
140
|
+
* `endDate` defaults to yesterday in config.timezone (today is incomplete);
|
|
141
|
+
* days run oldest-first so a failure stops before newer days, matching the
|
|
142
|
+
* incremental-sync intent of `syncDays`.
|
|
143
|
+
*/
|
|
144
|
+
export async function syncLocation(
|
|
145
|
+
options: LocationSyncRunOptions,
|
|
146
|
+
): Promise<Array<{ date: string; results: LocationSourceSyncResult[] }>> {
|
|
147
|
+
const { config, memoryDir, endDate, days, ...rest } = options;
|
|
148
|
+
if (endDate !== undefined && !isValidLocationDate(endDate)) {
|
|
149
|
+
throw new RangeError(`Invalid location endDate "${endDate}"; expected a real YYYY-MM-DD day.`);
|
|
150
|
+
}
|
|
151
|
+
const now = rest.now ?? (() => new Date());
|
|
152
|
+
const end = endDate ?? shiftIsoDate(localDayKey(now().toISOString(), config.timezone), -1);
|
|
153
|
+
const count = days ?? config.syncDays;
|
|
154
|
+
if (!Number.isInteger(count) || count < 1 || count > 90) {
|
|
155
|
+
throw new RangeError("location sync days must be an integer from 1 to 90");
|
|
156
|
+
}
|
|
157
|
+
const runs: Array<{ date: string; results: LocationSourceSyncResult[] }> = [];
|
|
158
|
+
for (let offset = count; offset >= 1; offset -= 1) {
|
|
159
|
+
const date = shiftIsoDate(end, -(offset - 1));
|
|
160
|
+
runs.push({ date, results: await syncLocationDay({ ...rest, config, memoryDir, date, now }) });
|
|
161
|
+
}
|
|
162
|
+
return runs;
|
|
163
|
+
}
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
import assert from "node:assert/strict";
|
|
2
|
+
import test from "node:test";
|
|
3
|
+
|
|
4
|
+
import { defaultLocationConfig, parseLocationConfig } from "./config.js";
|
|
5
|
+
|
|
6
|
+
test("parseLocationConfig defaults to an inert configuration", () => {
|
|
7
|
+
assert.deepEqual(defaultLocationConfig(), {
|
|
8
|
+
enabled: false,
|
|
9
|
+
timezone: "UTC",
|
|
10
|
+
syncDays: 1,
|
|
11
|
+
retainCoordinates: false,
|
|
12
|
+
sources: [],
|
|
13
|
+
});
|
|
14
|
+
assert.deepEqual(parseLocationConfig(undefined), defaultLocationConfig());
|
|
15
|
+
assert.deepEqual(parseLocationConfig(null), defaultLocationConfig());
|
|
16
|
+
});
|
|
17
|
+
|
|
18
|
+
test("parseLocationConfig treats string false gates as disabled", () => {
|
|
19
|
+
assert.equal(parseLocationConfig({ enabled: "false", sources: [{ id: "reitti" }] }).enabled, false);
|
|
20
|
+
assert.equal(
|
|
21
|
+
parseLocationConfig({
|
|
22
|
+
enabled: true,
|
|
23
|
+
retainCoordinates: "0",
|
|
24
|
+
sources: [{ id: "reitti", enabled: "false" }],
|
|
25
|
+
}).retainCoordinates,
|
|
26
|
+
false,
|
|
27
|
+
);
|
|
28
|
+
});
|
|
29
|
+
|
|
30
|
+
test("parseLocationConfig rejects unrecognized gate values instead of defaulting", () => {
|
|
31
|
+
assert.throws(() => parseLocationConfig({ enabled: "maybe" }), TypeError);
|
|
32
|
+
assert.throws(() => parseLocationConfig({ retainCoordinates: 42 }), TypeError);
|
|
33
|
+
assert.throws(() => parseLocationConfig({ sources: [{ id: "reitti", enabled: "nope" }] }), TypeError);
|
|
34
|
+
});
|
|
35
|
+
|
|
36
|
+
test("parseLocationConfig validates the timezone at parse time", () => {
|
|
37
|
+
assert.throws(() => parseLocationConfig({ timezone: "Mars/Olympus" }), RangeError);
|
|
38
|
+
assert.equal(parseLocationConfig({ timezone: "America/Chicago" }).timezone, "America/Chicago");
|
|
39
|
+
assert.throws(() => parseLocationConfig({ timezone: "" }), TypeError);
|
|
40
|
+
});
|
|
41
|
+
|
|
42
|
+
test("parseLocationConfig enforces the syncDays integer range", () => {
|
|
43
|
+
assert.equal(parseLocationConfig({ syncDays: "3" }).syncDays, 3);
|
|
44
|
+
assert.throws(() => parseLocationConfig({ syncDays: 0 }), RangeError);
|
|
45
|
+
assert.throws(() => parseLocationConfig({ syncDays: 1.5 }), RangeError);
|
|
46
|
+
assert.throws(() => parseLocationConfig({ syncDays: 91 }), RangeError);
|
|
47
|
+
assert.throws(() => parseLocationConfig({ syncDays: "abc" }), TypeError);
|
|
48
|
+
});
|
|
49
|
+
|
|
50
|
+
test("parseLocationConfig requires a source when enabled", () => {
|
|
51
|
+
assert.throws(() => parseLocationConfig({ enabled: true }), RangeError);
|
|
52
|
+
assert.throws(() => parseLocationConfig({ enabled: true, sources: "reitti" }), TypeError);
|
|
53
|
+
const parsed = parseLocationConfig({ enabled: true, sources: [{ id: "reitti" }] });
|
|
54
|
+
assert.deepEqual(parsed.sources, [{ id: "reitti", enabled: true }]);
|
|
55
|
+
});
|
|
56
|
+
|
|
57
|
+
test("parseLocationConfig rejects malformed source entries", () => {
|
|
58
|
+
assert.throws(() => parseLocationConfig({ sources: [{ id: "" }] }), TypeError);
|
|
59
|
+
assert.throws(() => parseLocationConfig({ sources: [{}] }), TypeError);
|
|
60
|
+
assert.throws(() => parseLocationConfig({ sources: [{ id: "Bad_Id" }] }), RangeError);
|
|
61
|
+
assert.throws(() => parseLocationConfig({ sources: [{ id: "../escape" }] }), RangeError);
|
|
62
|
+
assert.throws(() => parseLocationConfig({ sources: ["reitti"] }), TypeError);
|
|
63
|
+
});
|
|
64
|
+
|
|
65
|
+
test("parseLocationConfig rejects duplicate source ids", () => {
|
|
66
|
+
assert.throws(
|
|
67
|
+
() => parseLocationConfig({ enabled: true, sources: [{ id: "reitti" }, { id: "reitti", enabled: false }] }),
|
|
68
|
+
RangeError,
|
|
69
|
+
);
|
|
70
|
+
});
|
|
71
|
+
|
|
72
|
+
test("parseLocationConfig rejects non-object blocks", () => {
|
|
73
|
+
assert.throws(() => parseLocationConfig("nope"), TypeError);
|
|
74
|
+
assert.throws(() => parseLocationConfig([1, 2]), TypeError);
|
|
75
|
+
});
|
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
import { coerceBooleanLike, coerceNumber } from "../connectors/coerce.js";
|
|
2
|
+
import { assertValidTimezone } from "../activity/digest.js";
|
|
3
|
+
import type { LocationConfig, LocationSourceConfig } from "./types.js";
|
|
4
|
+
|
|
5
|
+
const PROVIDER_ID_PATTERN = /^[a-z0-9][a-z0-9-]*$/;
|
|
6
|
+
|
|
7
|
+
/** The inert default: disabled, UTC days, one day per run, no sources, no coordinates. */
|
|
8
|
+
export function defaultLocationConfig(): LocationConfig {
|
|
9
|
+
return {
|
|
10
|
+
enabled: false,
|
|
11
|
+
timezone: "UTC",
|
|
12
|
+
syncDays: 1,
|
|
13
|
+
retainCoordinates: false,
|
|
14
|
+
sources: [],
|
|
15
|
+
};
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
function asRecord(value: unknown, name: string): Record<string, unknown> {
|
|
19
|
+
if (typeof value !== "object" || value === null || Array.isArray(value)) {
|
|
20
|
+
throw new TypeError(`${name} must be an object`);
|
|
21
|
+
}
|
|
22
|
+
return value as Record<string, unknown>;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
function optionalNonEmptyString(value: unknown, name: string): string | undefined {
|
|
26
|
+
if (value === undefined) return undefined;
|
|
27
|
+
if (typeof value !== "string" || value.length === 0) throw new TypeError(`${name} must be a non-empty string`);
|
|
28
|
+
return value;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
function parseLocationSource(value: unknown): LocationSourceConfig {
|
|
32
|
+
const raw = asRecord(value, "location source");
|
|
33
|
+
const id = optionalNonEmptyString(raw.id, "location source id");
|
|
34
|
+
if (id === undefined) {
|
|
35
|
+
throw new TypeError("location source requires id");
|
|
36
|
+
}
|
|
37
|
+
if (!PROVIDER_ID_PATTERN.test(id)) {
|
|
38
|
+
throw new RangeError(
|
|
39
|
+
`location source id must be a lowercase kebab string (a-z, 0-9, hyphens): got "${id}"`,
|
|
40
|
+
);
|
|
41
|
+
}
|
|
42
|
+
const enabledValue = coerceBooleanLike(raw.enabled, "location source enabled");
|
|
43
|
+
if (raw.enabled !== undefined && enabledValue === undefined) {
|
|
44
|
+
throw new TypeError(`location source '${id}' enabled must be a boolean`);
|
|
45
|
+
}
|
|
46
|
+
return { id, enabled: enabledValue ?? true };
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/**
|
|
50
|
+
* Parse the unified `location.*` block. `enabled` gates everything and
|
|
51
|
+
* requires at least one source; a source's own `enabled` gates that source
|
|
52
|
+
* only. `retainCoordinates` defaults to `false` — coordinates are privacy
|
|
53
|
+
* data and are dropped everywhere unless explicitly retained.
|
|
54
|
+
*/
|
|
55
|
+
export function parseLocationConfig(raw: unknown): LocationConfig {
|
|
56
|
+
const defaults = defaultLocationConfig();
|
|
57
|
+
if (raw === undefined || raw === null) {
|
|
58
|
+
return { ...defaults };
|
|
59
|
+
}
|
|
60
|
+
const config = asRecord(raw, "location");
|
|
61
|
+
const enabledValue = coerceBooleanLike(config.enabled, "location.enabled");
|
|
62
|
+
if (config.enabled !== undefined && enabledValue === undefined) {
|
|
63
|
+
throw new TypeError("location.enabled must be a boolean");
|
|
64
|
+
}
|
|
65
|
+
const enabled = enabledValue ?? false;
|
|
66
|
+
|
|
67
|
+
const timezone = optionalNonEmptyString(config.timezone, "location.timezone") ?? "UTC";
|
|
68
|
+
// Validate the IANA zone at parse so a typo fails at config load, not
|
|
69
|
+
// mid-sync after some sources may already have advanced their state.
|
|
70
|
+
assertValidTimezone(timezone);
|
|
71
|
+
|
|
72
|
+
const syncDaysValue = coerceNumber(config.syncDays, "location.syncDays");
|
|
73
|
+
if (config.syncDays !== undefined && syncDaysValue === undefined) {
|
|
74
|
+
throw new TypeError("location.syncDays must be a finite number");
|
|
75
|
+
}
|
|
76
|
+
const syncDays = syncDaysValue ?? 1;
|
|
77
|
+
if (!Number.isInteger(syncDays) || syncDays < 1 || syncDays > 90) {
|
|
78
|
+
throw new RangeError("location.syncDays must be an integer from 1 to 90");
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
const retainCoordinatesValue = coerceBooleanLike(config.retainCoordinates, "location.retainCoordinates");
|
|
82
|
+
if (config.retainCoordinates !== undefined && retainCoordinatesValue === undefined) {
|
|
83
|
+
throw new TypeError("location.retainCoordinates must be a boolean");
|
|
84
|
+
}
|
|
85
|
+
const retainCoordinates = retainCoordinatesValue ?? false;
|
|
86
|
+
|
|
87
|
+
if (config.sources !== undefined && !Array.isArray(config.sources)) {
|
|
88
|
+
throw new TypeError("location.sources must be an array");
|
|
89
|
+
}
|
|
90
|
+
const sources = (config.sources ?? []).map(parseLocationSource);
|
|
91
|
+
// Day payloads and sync state are keyed on source id; two sources sharing
|
|
92
|
+
// an id would overwrite each other's state. Reject the collision at parse.
|
|
93
|
+
const ids = new Set<string>();
|
|
94
|
+
for (const source of sources) {
|
|
95
|
+
if (ids.has(source.id)) {
|
|
96
|
+
throw new RangeError(`location source id must be unique: ${source.id}`);
|
|
97
|
+
}
|
|
98
|
+
ids.add(source.id);
|
|
99
|
+
}
|
|
100
|
+
if (enabled && sources.length === 0) {
|
|
101
|
+
throw new RangeError("location.enabled requires at least one source");
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
return { enabled, timezone, syncDays, retainCoordinates, sources };
|
|
105
|
+
}
|
|
@@ -0,0 +1,202 @@
|
|
|
1
|
+
import assert from "node:assert/strict";
|
|
2
|
+
import test from "node:test";
|
|
3
|
+
|
|
4
|
+
import {
|
|
5
|
+
bucketObservationsByDay,
|
|
6
|
+
dominantPlace,
|
|
7
|
+
intervalOverlapMs,
|
|
8
|
+
localDayKey,
|
|
9
|
+
locationDayWindow,
|
|
10
|
+
observationSegments,
|
|
11
|
+
} from "./intervals.js";
|
|
12
|
+
import type { LocationObservation, LocationSegment } from "./types.js";
|
|
13
|
+
|
|
14
|
+
const HOUR_MS = 3_600_000;
|
|
15
|
+
|
|
16
|
+
function observation(observedAtUtc: string, placeId: string, label = placeId): LocationObservation {
|
|
17
|
+
return { observedAtUtc, place: { id: placeId, label } };
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
function segment(startUtc: string, endUtc: string, placeId: string): LocationSegment {
|
|
21
|
+
return { startUtc, endUtc, place: { id: placeId, label: placeId } };
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
test("intervalOverlapMs is 0 for disjoint intervals and exact for partial overlap", () => {
|
|
25
|
+
assert.equal(
|
|
26
|
+
intervalOverlapMs(
|
|
27
|
+
{ startUtc: "2026-08-17T09:00:00Z", endUtc: "2026-08-17T10:00:00Z" },
|
|
28
|
+
{ startUtc: "2026-08-17T10:00:00Z", endUtc: "2026-08-17T11:00:00Z" },
|
|
29
|
+
),
|
|
30
|
+
0,
|
|
31
|
+
"half-open intervals that touch at one instant do not overlap",
|
|
32
|
+
);
|
|
33
|
+
assert.equal(
|
|
34
|
+
intervalOverlapMs(
|
|
35
|
+
{ startUtc: "2026-08-17T09:00:00Z", endUtc: "2026-08-17T10:30:00Z" },
|
|
36
|
+
{ startUtc: "2026-08-17T10:00:00Z", endUtc: "2026-08-17T11:00:00Z" },
|
|
37
|
+
),
|
|
38
|
+
30 * 60_000,
|
|
39
|
+
);
|
|
40
|
+
assert.equal(
|
|
41
|
+
intervalOverlapMs(
|
|
42
|
+
{ startUtc: "2026-08-17T09:00:00Z", endUtc: "2026-08-17T12:00:00Z" },
|
|
43
|
+
{ startUtc: "2026-08-17T09:30:00Z", endUtc: "2026-08-17T10:00:00Z" },
|
|
44
|
+
),
|
|
45
|
+
30 * 60_000,
|
|
46
|
+
"a contained interval overlaps by its own length",
|
|
47
|
+
);
|
|
48
|
+
assert.equal(
|
|
49
|
+
intervalOverlapMs(
|
|
50
|
+
{ startUtc: "2026-08-17T09:00:00Z", endUtc: "2026-08-17T10:00:00Z" },
|
|
51
|
+
{ startUtc: "2026-08-17T09:00:00Z", endUtc: "2026-08-17T10:00:00Z" },
|
|
52
|
+
),
|
|
53
|
+
HOUR_MS,
|
|
54
|
+
"identical intervals overlap by their full length",
|
|
55
|
+
);
|
|
56
|
+
});
|
|
57
|
+
|
|
58
|
+
test("locationDayWindow produces 23h spring-forward and 25h fall-back days in America/Chicago", () => {
|
|
59
|
+
// US DST 2026: spring forward 2026-03-08, fall back 2026-11-01.
|
|
60
|
+
const spring = locationDayWindow("2026-03-08", "America/Chicago");
|
|
61
|
+
assert.equal(
|
|
62
|
+
Date.parse(spring.endUtc) - Date.parse(spring.startUtc),
|
|
63
|
+
23 * HOUR_MS,
|
|
64
|
+
"spring-forward local day is 23 hours",
|
|
65
|
+
);
|
|
66
|
+
assert.equal(spring.startUtc, "2026-03-08T06:00:00.000Z");
|
|
67
|
+
|
|
68
|
+
const fall = locationDayWindow("2026-11-01", "America/Chicago");
|
|
69
|
+
assert.equal(
|
|
70
|
+
Date.parse(fall.endUtc) - Date.parse(fall.startUtc),
|
|
71
|
+
25 * HOUR_MS,
|
|
72
|
+
"fall-back local day is 25 hours",
|
|
73
|
+
);
|
|
74
|
+
assert.equal(fall.startUtc, "2026-11-01T05:00:00.000Z");
|
|
75
|
+
|
|
76
|
+
const utc = locationDayWindow("2026-08-17", "UTC");
|
|
77
|
+
assert.equal(Date.parse(utc.endUtc) - Date.parse(utc.startUtc), 24 * HOUR_MS);
|
|
78
|
+
assert.throws(() => locationDayWindow("2026-02-30", "UTC"), RangeError);
|
|
79
|
+
assert.throws(() => locationDayWindow("2026-8-7", "UTC"), RangeError);
|
|
80
|
+
});
|
|
81
|
+
|
|
82
|
+
test("localDayKey buckets midnight exactly on the owning day (half-open)", () => {
|
|
83
|
+
assert.equal(localDayKey("2026-08-17T00:00:00.000Z", "UTC"), "2026-08-17");
|
|
84
|
+
assert.equal(localDayKey("2026-08-17T23:59:59.999Z", "UTC"), "2026-08-17");
|
|
85
|
+
assert.equal(localDayKey("2026-08-18T00:00:00.000Z", "UTC"), "2026-08-18");
|
|
86
|
+
// Zones east of UTC roll over mid-UTC-afternoon.
|
|
87
|
+
assert.equal(localDayKey("2026-08-17T18:30:00.000Z", "Asia/Tokyo"), "2026-08-18");
|
|
88
|
+
assert.throws(() => localDayKey("not-an-instant", "UTC"), RangeError);
|
|
89
|
+
});
|
|
90
|
+
|
|
91
|
+
test("bucketObservationsByDay sorts within each local day bucket", () => {
|
|
92
|
+
const buckets = bucketObservationsByDay(
|
|
93
|
+
[
|
|
94
|
+
observation("2026-08-17T20:00:00Z", "b"),
|
|
95
|
+
observation("2026-08-17T09:00:00Z", "a"),
|
|
96
|
+
observation("2026-08-18T01:00:00Z", "c"),
|
|
97
|
+
],
|
|
98
|
+
"UTC",
|
|
99
|
+
);
|
|
100
|
+
assert.deepEqual([...buckets.keys()].sort(), ["2026-08-17", "2026-08-18"]);
|
|
101
|
+
assert.deepEqual(
|
|
102
|
+
buckets.get("2026-08-17")!.map((item) => item.observedAtUtc),
|
|
103
|
+
["2026-08-17T09:00:00Z", "2026-08-17T20:00:00Z"],
|
|
104
|
+
);
|
|
105
|
+
assert.throws(
|
|
106
|
+
() => bucketObservationsByDay([observation("garbage", "a")], "UTC"),
|
|
107
|
+
RangeError,
|
|
108
|
+
"a malformed instant is rejected loudly, never bucketed",
|
|
109
|
+
);
|
|
110
|
+
});
|
|
111
|
+
|
|
112
|
+
test("observationSegments attributes dwell to the next observation and caps at window end", () => {
|
|
113
|
+
const window = locationDayWindow("2026-08-17", "UTC");
|
|
114
|
+
const segments = observationSegments(
|
|
115
|
+
[observation("2026-08-17T09:00:00Z", "home"), observation("2026-08-17T09:30:00Z", "office")],
|
|
116
|
+
window,
|
|
117
|
+
{ retainCoordinates: false },
|
|
118
|
+
);
|
|
119
|
+
assert.deepEqual(
|
|
120
|
+
segments.map((item) => [item.startUtc, item.endUtc, item.place.id]),
|
|
121
|
+
[
|
|
122
|
+
["2026-08-17T09:00:00.000Z", "2026-08-17T09:30:00.000Z", "home"],
|
|
123
|
+
["2026-08-17T09:30:00.000Z", window.endUtc, "office"],
|
|
124
|
+
],
|
|
125
|
+
"the last observation dwells until the day window ends, never across it",
|
|
126
|
+
);
|
|
127
|
+
});
|
|
128
|
+
|
|
129
|
+
test("observationSegments drops coordinates unless retention is enabled", () => {
|
|
130
|
+
const window = locationDayWindow("2026-08-17", "UTC");
|
|
131
|
+
const withCoords: LocationObservation[] = [
|
|
132
|
+
{
|
|
133
|
+
observedAtUtc: "2026-08-17T09:00:00Z",
|
|
134
|
+
place: { id: "home", label: "Home", latitude: 41.8781, longitude: -87.6298 },
|
|
135
|
+
},
|
|
136
|
+
];
|
|
137
|
+
const stripped = observationSegments(withCoords, window, { retainCoordinates: false });
|
|
138
|
+
assert.equal(stripped[0]!.place.latitude, undefined);
|
|
139
|
+
assert.equal(stripped[0]!.place.longitude, undefined);
|
|
140
|
+
const kept = observationSegments(withCoords, window, { retainCoordinates: true });
|
|
141
|
+
assert.equal(kept[0]!.place.latitude, 41.8781);
|
|
142
|
+
assert.equal(kept[0]!.place.longitude, -87.6298);
|
|
143
|
+
});
|
|
144
|
+
|
|
145
|
+
test("observationSegments rejects observations outside the requested window", () => {
|
|
146
|
+
const window = locationDayWindow("2026-08-17", "UTC");
|
|
147
|
+
assert.throws(
|
|
148
|
+
() => observationSegments([observation("2026-08-18T00:00:00Z", "home")], window, { retainCoordinates: false }),
|
|
149
|
+
RangeError,
|
|
150
|
+
"the window end is exclusive",
|
|
151
|
+
);
|
|
152
|
+
assert.throws(
|
|
153
|
+
() =>
|
|
154
|
+
observationSegments([observation("2026-08-16T23:59:59.999Z", "home")], window, { retainCoordinates: false }),
|
|
155
|
+
RangeError,
|
|
156
|
+
);
|
|
157
|
+
assert.throws(
|
|
158
|
+
() =>
|
|
159
|
+
observationSegments(
|
|
160
|
+
[observation("2026-08-17T09:00:00Z", "home")],
|
|
161
|
+
{ startUtc: "2026-08-17T10:00:00Z", endUtc: "2026-08-17T09:00:00Z" },
|
|
162
|
+
{ retainCoordinates: false },
|
|
163
|
+
),
|
|
164
|
+
RangeError,
|
|
165
|
+
"an inverted window is rejected",
|
|
166
|
+
);
|
|
167
|
+
});
|
|
168
|
+
|
|
169
|
+
test("dominantPlace breaks duration ties by earlier first start, then place id", () => {
|
|
170
|
+
const tieEarlierStart = [
|
|
171
|
+
segment("2026-08-17T09:00:00Z", "2026-08-17T10:00:00Z", "gym"),
|
|
172
|
+
segment("2026-08-17T10:00:00Z", "2026-08-17T11:00:00Z", "alpha"),
|
|
173
|
+
segment("2026-08-17T11:00:00Z", "2026-08-17T12:00:00Z", "alpha"),
|
|
174
|
+
];
|
|
175
|
+
assert.equal(dominantPlace(tieEarlierStart)?.place.id, "alpha");
|
|
176
|
+
|
|
177
|
+
const tieSameStartLexicographic = [
|
|
178
|
+
segment("2026-08-17T09:00:00Z", "2026-08-17T10:00:00Z", "zeta"),
|
|
179
|
+
segment("2026-08-17T09:00:00Z", "2026-08-17T10:00:00Z", "beta"),
|
|
180
|
+
];
|
|
181
|
+
assert.equal(
|
|
182
|
+
dominantPlace(tieSameStartLexicographic)?.place.id,
|
|
183
|
+
"beta",
|
|
184
|
+
"equal totals and equal first starts fall back to the smaller place id",
|
|
185
|
+
);
|
|
186
|
+
|
|
187
|
+
// Deterministic across orderings (insertion order must never win).
|
|
188
|
+
const shuffled = [...tieEarlierStart].reverse();
|
|
189
|
+
assert.equal(dominantPlace(shuffled)?.place.id, "alpha");
|
|
190
|
+
assert.equal(dominantPlace([]), null);
|
|
191
|
+
});
|
|
192
|
+
|
|
193
|
+
test("dominantPlace unions overlapping segments so two sources do not double-count", () => {
|
|
194
|
+
const overlapping = [
|
|
195
|
+
segment("2026-08-17T09:00:00Z", "2026-08-17T12:00:00Z", "home"),
|
|
196
|
+
segment("2026-08-17T10:00:00Z", "2026-08-17T11:00:00Z", "office"),
|
|
197
|
+
segment("2026-08-17T14:00:00Z", "2026-08-17T18:00:00Z", "office"),
|
|
198
|
+
];
|
|
199
|
+
const dominant = dominantPlace(overlapping)!;
|
|
200
|
+
assert.equal(dominant.place.id, "office");
|
|
201
|
+
assert.equal(dominant.totalMs, 5 * HOUR_MS, "1h overlapping office + 4h afternoon office");
|
|
202
|
+
});
|