@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,221 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Location interval math (issue #2044).
|
|
3
|
+
*
|
|
4
|
+
* Pure, deterministic helpers over UTC ISO-8601 instants: half-open
|
|
5
|
+
* [start, end) day windows (reusing the DST-correct activity day-window), IANA
|
|
6
|
+
* local-day bucketing, overlap, observation→segment dwell, and dominant-place
|
|
7
|
+
* selection with stable tie-breaking. No IO, no clocks, no randomness.
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
import { activityDayWindow, isValidActivityDate } from "../activity/digest.js";
|
|
11
|
+
import type { LocationObservation, LocationPlace, LocationSegment } from "./types.js";
|
|
12
|
+
|
|
13
|
+
export function isValidLocationDate(date: string): boolean {
|
|
14
|
+
return isValidActivityDate(date);
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
/** Half-open [start, end) UTC ISO bounds of a local day. */
|
|
18
|
+
export function locationDayWindow(date: string, timezone: string): { startUtc: string; endUtc: string } {
|
|
19
|
+
if (!isValidActivityDate(date)) {
|
|
20
|
+
throw new RangeError(`Invalid location date "${date}"; expected a real YYYY-MM-DD day.`);
|
|
21
|
+
}
|
|
22
|
+
return activityDayWindow(date, timezone);
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
function assertFiniteInstant(iso: string, what: string): number {
|
|
26
|
+
const ms = Date.parse(iso);
|
|
27
|
+
if (typeof iso !== "string" || !Number.isFinite(ms)) {
|
|
28
|
+
throw new RangeError(`${what} must be a UTC ISO-8601 instant; got "${String(iso)}"`);
|
|
29
|
+
}
|
|
30
|
+
return ms;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
/** The IANA local calendar day (YYYY-MM-DD) an instant falls in. */
|
|
34
|
+
export function localDayKey(instantUtc: string, timezone: string): string {
|
|
35
|
+
assertFiniteInstant(instantUtc, "instantUtc");
|
|
36
|
+
return new Intl.DateTimeFormat("en-CA", {
|
|
37
|
+
timeZone: timezone,
|
|
38
|
+
year: "numeric",
|
|
39
|
+
month: "2-digit",
|
|
40
|
+
day: "2-digit",
|
|
41
|
+
}).format(new Date(instantUtc));
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
function compareObservations(a: LocationObservation, b: LocationObservation): number {
|
|
45
|
+
if (a.observedAtUtc !== b.observedAtUtc) {
|
|
46
|
+
return a.observedAtUtc < b.observedAtUtc ? -1 : 1;
|
|
47
|
+
}
|
|
48
|
+
// Total comparator (§12): equal-key observations fall back to place id so
|
|
49
|
+
// ordering — and therefore dwell attribution — is deterministic.
|
|
50
|
+
if (a.place.id !== b.place.id) {
|
|
51
|
+
return a.place.id < b.place.id ? -1 : 1;
|
|
52
|
+
}
|
|
53
|
+
return 0;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
/** Bucket observations into their IANA local days, sorted within each day. */
|
|
57
|
+
export function bucketObservationsByDay(
|
|
58
|
+
observations: readonly LocationObservation[],
|
|
59
|
+
timezone: string,
|
|
60
|
+
): Map<string, LocationObservation[]> {
|
|
61
|
+
const buckets = new Map<string, LocationObservation[]>();
|
|
62
|
+
for (const observation of observations) {
|
|
63
|
+
assertFiniteInstant(observation.observedAtUtc, "observation observedAtUtc");
|
|
64
|
+
const key = localDayKey(observation.observedAtUtc, timezone);
|
|
65
|
+
const bucket = buckets.get(key);
|
|
66
|
+
if (bucket === undefined) buckets.set(key, [observation]);
|
|
67
|
+
else bucket.push(observation);
|
|
68
|
+
}
|
|
69
|
+
for (const bucket of buckets.values()) bucket.sort(compareObservations);
|
|
70
|
+
return buckets;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
/** Overlap in ms of two half-open intervals; 0 when disjoint. */
|
|
74
|
+
export function intervalOverlapMs(
|
|
75
|
+
a: { startUtc: string; endUtc: string },
|
|
76
|
+
b: { startUtc: string; endUtc: string },
|
|
77
|
+
): number {
|
|
78
|
+
const overlap =
|
|
79
|
+
Math.min(Date.parse(a.endUtc), Date.parse(b.endUtc)) - Math.max(Date.parse(a.startUtc), Date.parse(b.startUtc));
|
|
80
|
+
return Math.max(0, overlap);
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
function placeForSegment(place: LocationPlace, retainCoordinates: boolean): LocationPlace {
|
|
84
|
+
if (retainCoordinates || place.latitude === undefined || place.longitude === undefined) {
|
|
85
|
+
return place;
|
|
86
|
+
}
|
|
87
|
+
return {
|
|
88
|
+
id: place.id,
|
|
89
|
+
label: place.label,
|
|
90
|
+
...(place.kind === undefined ? {} : { kind: place.kind }),
|
|
91
|
+
};
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
/**
|
|
95
|
+
* Derive place-visit segments from a day's observations: each observation
|
|
96
|
+
* covers [its instant, the next observation's instant ∩ window end). The last
|
|
97
|
+
* observation is capped by the window end, so dwell never crosses the day
|
|
98
|
+
* boundary. Out-of-window observations are a provider contract violation and
|
|
99
|
+
* are rejected loudly rather than silently reshaping the day. Coordinates are
|
|
100
|
+
* stripped unless `retainCoordinates` is set.
|
|
101
|
+
*/
|
|
102
|
+
export function observationSegments(
|
|
103
|
+
observations: readonly LocationObservation[],
|
|
104
|
+
window: { startUtc: string; endUtc: string },
|
|
105
|
+
options: { retainCoordinates: boolean },
|
|
106
|
+
): LocationSegment[] {
|
|
107
|
+
const startMs = assertFiniteInstant(window.startUtc, "window startUtc");
|
|
108
|
+
const endMs = assertFiniteInstant(window.endUtc, "window endUtc");
|
|
109
|
+
if (startMs >= endMs) {
|
|
110
|
+
throw new RangeError("location window must satisfy startUtc < endUtc");
|
|
111
|
+
}
|
|
112
|
+
const ordered = [...observations].sort(compareObservations);
|
|
113
|
+
const segments: LocationSegment[] = [];
|
|
114
|
+
for (let index = 0; index < ordered.length; index += 1) {
|
|
115
|
+
const current = ordered[index]!;
|
|
116
|
+
const currentMs = Date.parse(current.observedAtUtc);
|
|
117
|
+
if (currentMs < startMs || currentMs >= endMs) {
|
|
118
|
+
throw new RangeError(
|
|
119
|
+
`location observation at "${current.observedAtUtc}" falls outside the requested [${window.startUtc}, ${window.endUtc}) window`,
|
|
120
|
+
);
|
|
121
|
+
}
|
|
122
|
+
const nextMs =
|
|
123
|
+
index + 1 < ordered.length ? Date.parse(ordered[index + 1]!.observedAtUtc) : endMs;
|
|
124
|
+
const segmentEndMs = Math.min(nextMs, endMs);
|
|
125
|
+
if (currentMs >= segmentEndMs) continue;
|
|
126
|
+
segments.push({
|
|
127
|
+
startUtc: new Date(currentMs).toISOString(),
|
|
128
|
+
endUtc: new Date(segmentEndMs).toISOString(),
|
|
129
|
+
place: placeForSegment(current.place, options.retainCoordinates),
|
|
130
|
+
});
|
|
131
|
+
}
|
|
132
|
+
return segments;
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
/** Total time a place was occupied (union of its segment intervals, ms). */
|
|
136
|
+
export interface PlaceDuration {
|
|
137
|
+
place: LocationPlace;
|
|
138
|
+
totalMs: number;
|
|
139
|
+
/** Earliest segment start contributing to this place's total. */
|
|
140
|
+
firstStartUtc: string;
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
function unionIntervalMs(segments: readonly LocationSegment[]): number {
|
|
144
|
+
// Merge overlapping [start, end) intervals per place so two providers
|
|
145
|
+
// observing the same visit do not double-count its duration.
|
|
146
|
+
const bounds = segments
|
|
147
|
+
.map((segment) => ({ start: Date.parse(segment.startUtc), end: Date.parse(segment.endUtc) }))
|
|
148
|
+
.sort((a, b) => (a.start !== b.start ? a.start - b.start : a.end - b.end));
|
|
149
|
+
let total = 0;
|
|
150
|
+
let sweepStart: number | null = null;
|
|
151
|
+
let sweepEnd = 0;
|
|
152
|
+
for (const bound of bounds) {
|
|
153
|
+
if (sweepStart === null || bound.start > sweepEnd) {
|
|
154
|
+
if (sweepStart !== null) total += sweepEnd - sweepStart;
|
|
155
|
+
sweepStart = bound.start;
|
|
156
|
+
sweepEnd = bound.end;
|
|
157
|
+
} else if (bound.end > sweepEnd) {
|
|
158
|
+
sweepEnd = bound.end;
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
if (sweepStart !== null) total += sweepEnd - sweepStart;
|
|
162
|
+
return total;
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
/** Per-place totals across every segment given (multi-source safe). */
|
|
166
|
+
export function placeDurations(segments: readonly LocationSegment[]): PlaceDuration[] {
|
|
167
|
+
const byPlace = new Map<string, { place: LocationPlace; segments: LocationSegment[]; firstStartUtc: string }>();
|
|
168
|
+
for (const segment of segments) {
|
|
169
|
+
const existing = byPlace.get(segment.place.id);
|
|
170
|
+
if (existing === undefined) {
|
|
171
|
+
byPlace.set(segment.place.id, { place: segment.place, segments: [segment], firstStartUtc: segment.startUtc });
|
|
172
|
+
continue;
|
|
173
|
+
}
|
|
174
|
+
existing.segments.push(segment);
|
|
175
|
+
if (segment.startUtc < existing.firstStartUtc) existing.firstStartUtc = segment.startUtc;
|
|
176
|
+
// Two providers may label the same place id differently; keep the
|
|
177
|
+
// lexicographically smallest label so rendering stays deterministic.
|
|
178
|
+
if (segment.place.label < existing.place.label) existing.place = segment.place;
|
|
179
|
+
}
|
|
180
|
+
const durations: PlaceDuration[] = [...byPlace.values()].map((entry) => ({
|
|
181
|
+
place: entry.place,
|
|
182
|
+
totalMs: unionIntervalMs(entry.segments),
|
|
183
|
+
firstStartUtc: entry.firstStartUtc,
|
|
184
|
+
}));
|
|
185
|
+
durations.sort((a, b) => {
|
|
186
|
+
if (a.place.id !== b.place.id) return a.place.id < b.place.id ? -1 : 1;
|
|
187
|
+
return 0;
|
|
188
|
+
});
|
|
189
|
+
return durations;
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
export interface DominantPlaceResult {
|
|
193
|
+
place: LocationPlace;
|
|
194
|
+
totalMs: number;
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
/**
|
|
198
|
+
* The place with the longest total occupancy. Ties are stable and
|
|
199
|
+
* deterministic: earlier first start wins, then the lexicographically smaller
|
|
200
|
+
* place id — never insertion order.
|
|
201
|
+
*/
|
|
202
|
+
export function dominantPlace(segments: readonly LocationSegment[]): DominantPlaceResult | null {
|
|
203
|
+
const durations = placeDurations(segments);
|
|
204
|
+
let best: PlaceDuration | null = null;
|
|
205
|
+
for (const duration of durations) {
|
|
206
|
+
if (best === null) {
|
|
207
|
+
best = duration;
|
|
208
|
+
continue;
|
|
209
|
+
}
|
|
210
|
+
if (duration.totalMs !== best.totalMs) {
|
|
211
|
+
if (duration.totalMs > best.totalMs) best = duration;
|
|
212
|
+
continue;
|
|
213
|
+
}
|
|
214
|
+
if (duration.firstStartUtc !== best.firstStartUtc) {
|
|
215
|
+
if (duration.firstStartUtc < best.firstStartUtc) best = duration;
|
|
216
|
+
continue;
|
|
217
|
+
}
|
|
218
|
+
if (duration.place.id < best.place.id) best = duration;
|
|
219
|
+
}
|
|
220
|
+
return best === null ? null : { place: best.place, totalMs: best.totalMs };
|
|
221
|
+
}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import { parseActivityConfig } from "../activity/config.js";
|
|
2
|
+
import { parseMeetingsConfig } from "../meetings/config.js";
|
|
3
|
+
import { parseProvenanceConfig } from "../provenance.js";
|
|
4
|
+
|
|
5
|
+
/** Parse capture-adjacent config blocks without location (#2044). */
|
|
6
|
+
export function parseCaptureCompanionConfigs(cfg: Record<string, unknown>) {
|
|
7
|
+
return {
|
|
8
|
+
activity: parseActivityConfig(cfg.activity),
|
|
9
|
+
meetings: parseMeetingsConfig(cfg.meetings),
|
|
10
|
+
provenance: parseProvenanceConfig(cfg.provenance),
|
|
11
|
+
};
|
|
12
|
+
}
|
|
@@ -0,0 +1,324 @@
|
|
|
1
|
+
import assert from "node:assert/strict";
|
|
2
|
+
import { access, mkdir, mkdtemp, readFile, rm, symlink } from "node:fs/promises";
|
|
3
|
+
import os from "node:os";
|
|
4
|
+
import path from "node:path";
|
|
5
|
+
import test from "node:test";
|
|
6
|
+
|
|
7
|
+
import { syncLocationDay, sanitizeProviderError } from "./pipeline.js";
|
|
8
|
+
import { parseLocationConfig } from "./config.js";
|
|
9
|
+
import { locationDayFilePath, locationSyncStateFilePath, loadLocationSyncState } from "./store.js";
|
|
10
|
+
import type { LocationConfig, LocationObservation, LocationProvider } from "./types.js";
|
|
11
|
+
|
|
12
|
+
function config(overrides: Record<string, unknown> = {}): LocationConfig {
|
|
13
|
+
return parseLocationConfig({
|
|
14
|
+
enabled: true,
|
|
15
|
+
sources: [{ id: "fixture" }],
|
|
16
|
+
...overrides,
|
|
17
|
+
});
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
function observationAt(observedAtUtc: string, placeId: string): LocationObservation {
|
|
21
|
+
return {
|
|
22
|
+
observedAtUtc,
|
|
23
|
+
place: { id: placeId, label: placeId === "home" ? "Home" : placeId, latitude: 41.8781, longitude: -87.6298 },
|
|
24
|
+
};
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
function provider(
|
|
28
|
+
pages: LocationObservation[][],
|
|
29
|
+
options: { failWith?: Error } = {},
|
|
30
|
+
): LocationProvider {
|
|
31
|
+
return {
|
|
32
|
+
id: "fixture",
|
|
33
|
+
displayName: "Fixture",
|
|
34
|
+
async verify() {
|
|
35
|
+
return { ok: true };
|
|
36
|
+
},
|
|
37
|
+
async fetchObservations() {
|
|
38
|
+
if (options.failWith !== undefined) throw options.failWith;
|
|
39
|
+
const remaining = pages.splice(0, 1);
|
|
40
|
+
return { observations: remaining[0] ?? [], nextCursor: null };
|
|
41
|
+
},
|
|
42
|
+
};
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
const REGISTRY = new Map<string, LocationProvider>();
|
|
46
|
+
function withProviders(providers: LocationProvider[]): (id: string) => LocationProvider | undefined {
|
|
47
|
+
REGISTRY.clear();
|
|
48
|
+
for (const item of providers) REGISTRY.set(item.id, item);
|
|
49
|
+
return (id: string) => REGISTRY.get(id);
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
test("the master gate short-circuits every path: no providers, no filesystem", async () => {
|
|
53
|
+
const memoryDir = await mkdtemp(path.join(os.tmpdir(), "remnic-location-pipeline-"));
|
|
54
|
+
try {
|
|
55
|
+
let fetches = 0;
|
|
56
|
+
const getProvider = withProviders([
|
|
57
|
+
{
|
|
58
|
+
id: "fixture",
|
|
59
|
+
displayName: "Fixture",
|
|
60
|
+
async verify() {
|
|
61
|
+
return { ok: true };
|
|
62
|
+
},
|
|
63
|
+
async fetchObservations() {
|
|
64
|
+
fetches += 1;
|
|
65
|
+
return { observations: [], nextCursor: null };
|
|
66
|
+
},
|
|
67
|
+
},
|
|
68
|
+
]);
|
|
69
|
+
const disabled = parseLocationConfig({ enabled: false, sources: [{ id: "fixture" }] });
|
|
70
|
+
const results = await syncLocationDay({ config: disabled, memoryDir, date: "2026-08-17", getProvider });
|
|
71
|
+
assert.deepEqual(results, []);
|
|
72
|
+
assert.equal(fetches, 0);
|
|
73
|
+
await assert.rejects(() => access(locationSyncStateFilePath(memoryDir)), /ENOENT/);
|
|
74
|
+
await assert.rejects(() => access(locationDayFilePath(memoryDir, "2026-08-17")), /ENOENT/);
|
|
75
|
+
} finally {
|
|
76
|
+
await rm(memoryDir, { recursive: true, force: true });
|
|
77
|
+
}
|
|
78
|
+
});
|
|
79
|
+
|
|
80
|
+
test("a disabled source is skipped without contacting its provider", async () => {
|
|
81
|
+
const memoryDir = await mkdtemp(path.join(os.tmpdir(), "remnic-location-pipeline-"));
|
|
82
|
+
try {
|
|
83
|
+
let fetches = 0;
|
|
84
|
+
const getProvider = withProviders([
|
|
85
|
+
{
|
|
86
|
+
id: "fixture",
|
|
87
|
+
displayName: "Fixture",
|
|
88
|
+
async verify() {
|
|
89
|
+
return { ok: true };
|
|
90
|
+
},
|
|
91
|
+
async fetchObservations() {
|
|
92
|
+
fetches += 1;
|
|
93
|
+
return { observations: [], nextCursor: null };
|
|
94
|
+
},
|
|
95
|
+
},
|
|
96
|
+
]);
|
|
97
|
+
const results = await syncLocationDay({
|
|
98
|
+
config: config({ sources: [{ id: "fixture", enabled: false }] }),
|
|
99
|
+
memoryDir,
|
|
100
|
+
date: "2026-08-17",
|
|
101
|
+
getProvider,
|
|
102
|
+
});
|
|
103
|
+
assert.deepEqual(results, [
|
|
104
|
+
{
|
|
105
|
+
sourceId: "fixture",
|
|
106
|
+
status: "skipped",
|
|
107
|
+
skipReason: "source-disabled",
|
|
108
|
+
fetched: 0,
|
|
109
|
+
dayWritten: false,
|
|
110
|
+
stateSaved: false,
|
|
111
|
+
},
|
|
112
|
+
]);
|
|
113
|
+
assert.equal(fetches, 0);
|
|
114
|
+
} finally {
|
|
115
|
+
await rm(memoryDir, { recursive: true, force: true });
|
|
116
|
+
}
|
|
117
|
+
});
|
|
118
|
+
|
|
119
|
+
test("a configured source whose provider is not registered is skipped, not failed", async () => {
|
|
120
|
+
const memoryDir = await mkdtemp(path.join(os.tmpdir(), "remnic-location-pipeline-"));
|
|
121
|
+
try {
|
|
122
|
+
const results = await syncLocationDay({
|
|
123
|
+
config: config(),
|
|
124
|
+
memoryDir,
|
|
125
|
+
date: "2026-08-17",
|
|
126
|
+
getProvider: () => undefined,
|
|
127
|
+
});
|
|
128
|
+
assert.equal(results[0]?.status, "skipped");
|
|
129
|
+
assert.equal(results[0]?.skipReason, "provider-not-registered");
|
|
130
|
+
} finally {
|
|
131
|
+
await rm(memoryDir, { recursive: true, force: true });
|
|
132
|
+
}
|
|
133
|
+
});
|
|
134
|
+
|
|
135
|
+
test("a successful sync writes the day document and only then advances state", async () => {
|
|
136
|
+
const memoryDir = await mkdtemp(path.join(os.tmpdir(), "remnic-location-pipeline-"));
|
|
137
|
+
try {
|
|
138
|
+
const getProvider = withProviders([
|
|
139
|
+
provider([[observationAt("2026-08-17T09:00:00Z", "home"), observationAt("2026-08-17T12:00:00Z", "office")]]),
|
|
140
|
+
]);
|
|
141
|
+
const results = await syncLocationDay({
|
|
142
|
+
config: config(),
|
|
143
|
+
memoryDir,
|
|
144
|
+
date: "2026-08-17",
|
|
145
|
+
getProvider,
|
|
146
|
+
now: () => new Date("2026-08-18T00:00:05.000Z"),
|
|
147
|
+
});
|
|
148
|
+
assert.deepEqual(
|
|
149
|
+
results.map((result) => [result.status, result.fetched, result.dayWritten, result.stateSaved]),
|
|
150
|
+
[["synced", 2, true, true]],
|
|
151
|
+
);
|
|
152
|
+
const day = await readFile(locationDayFilePath(memoryDir, "2026-08-17"), "utf-8");
|
|
153
|
+
assert.match(day, /kind: "location-day"/);
|
|
154
|
+
assert.match(day, /Home: 3h 00m/);
|
|
155
|
+
assert.doesNotMatch(day, /41\.8781/, "coordinates are not persisted unless retention is enabled");
|
|
156
|
+
const state = await loadLocationSyncState(memoryDir);
|
|
157
|
+
assert.equal(state.sources.fixture?.lastSyncedAtUtc, "2026-08-18T00:00:05.000Z");
|
|
158
|
+
assert.equal(state.sources.fixture?.days?.["2026-08-17"]?.observationCount, 2);
|
|
159
|
+
} finally {
|
|
160
|
+
await rm(memoryDir, { recursive: true, force: true });
|
|
161
|
+
}
|
|
162
|
+
});
|
|
163
|
+
|
|
164
|
+
test("an empty fetch is a successful sync, not a provider failure", async () => {
|
|
165
|
+
const memoryDir = await mkdtemp(path.join(os.tmpdir(), "remnic-location-pipeline-"));
|
|
166
|
+
try {
|
|
167
|
+
const getProvider = withProviders([provider([[]])]);
|
|
168
|
+
const results = await syncLocationDay({ config: config(), memoryDir, date: "2026-08-17", getProvider });
|
|
169
|
+
assert.deepEqual(
|
|
170
|
+
results.map((result) => [result.status, result.fetched, result.dayWritten, result.stateSaved, result.error]),
|
|
171
|
+
[["synced", 0, false, true, undefined]],
|
|
172
|
+
);
|
|
173
|
+
await assert.rejects(() => access(locationDayFilePath(memoryDir, "2026-08-17")), /ENOENT/);
|
|
174
|
+
const state = await loadLocationSyncState(memoryDir);
|
|
175
|
+
assert.notEqual(state.sources.fixture, undefined, "state advances for an empty day");
|
|
176
|
+
} finally {
|
|
177
|
+
await rm(memoryDir, { recursive: true, force: true });
|
|
178
|
+
}
|
|
179
|
+
});
|
|
180
|
+
|
|
181
|
+
test("a provider failure fails only that source and never advances its state", async () => {
|
|
182
|
+
const memoryDir = await mkdtemp(path.join(os.tmpdir(), "remnic-location-pipeline-"));
|
|
183
|
+
try {
|
|
184
|
+
const getProvider = withProviders([
|
|
185
|
+
provider([], { failWith: new Error("Bearer sk-live-abc123 failed at http://api.example/v1?key=hunter2 near 41.8781,-87.6298") }),
|
|
186
|
+
(() => {
|
|
187
|
+
const healthy = provider([[observationAt("2026-08-17T08:00:00Z", "gym")]]);
|
|
188
|
+
return { ...healthy, id: "healthy", displayName: "Healthy" };
|
|
189
|
+
})(),
|
|
190
|
+
]);
|
|
191
|
+
const results = await syncLocationDay({
|
|
192
|
+
config: config({ sources: [{ id: "fixture" }, { id: "healthy" }] }),
|
|
193
|
+
memoryDir,
|
|
194
|
+
date: "2026-08-17",
|
|
195
|
+
getProvider,
|
|
196
|
+
});
|
|
197
|
+
const failed = results.find((result) => result.sourceId === "fixture")!;
|
|
198
|
+
assert.equal(failed.status, "failed");
|
|
199
|
+
assert.equal(failed.stateSaved, false);
|
|
200
|
+
assert.doesNotMatch(failed.error ?? "", /sk-live-abc123|hunter2|41\.8781/);
|
|
201
|
+
assert.match(failed.error ?? "", /\[redacted\]/);
|
|
202
|
+
assert.match(failed.error ?? "", /\[coordinates\]/);
|
|
203
|
+
const healthy = results.find((result) => result.sourceId === "healthy")!;
|
|
204
|
+
assert.equal(healthy.status, "synced");
|
|
205
|
+
const state = await loadLocationSyncState(memoryDir);
|
|
206
|
+
assert.equal(state.sources.fixture, undefined, "the failed source's state was never written");
|
|
207
|
+
assert.notEqual(state.sources.healthy, undefined);
|
|
208
|
+
} finally {
|
|
209
|
+
await rm(memoryDir, { recursive: true, force: true });
|
|
210
|
+
}
|
|
211
|
+
});
|
|
212
|
+
|
|
213
|
+
test("state is not advanced when the day write fails (durable write before state)", async () => {
|
|
214
|
+
const root = await mkdtemp(path.join(os.tmpdir(), "remnic-location-pipeline-"));
|
|
215
|
+
try {
|
|
216
|
+
const memoryDir = path.join(root, "memory");
|
|
217
|
+
await mkdir(memoryDir, { recursive: true });
|
|
218
|
+
// A symlinked locations root fails containment BEFORE any write lands.
|
|
219
|
+
await mkdir(path.join(root, "elsewhere"), { recursive: true });
|
|
220
|
+
await symlink(path.join(root, "elsewhere"), path.join(memoryDir, "locations"));
|
|
221
|
+
const getProvider = withProviders([provider([[observationAt("2026-08-17T09:00:00Z", "home")]])]);
|
|
222
|
+
const results = await syncLocationDay({ config: config(), memoryDir, date: "2026-08-17", getProvider });
|
|
223
|
+
assert.equal(results[0]?.status, "failed");
|
|
224
|
+
assert.equal(results[0]?.stateSaved, false);
|
|
225
|
+
await assert.rejects(() => access(locationSyncStateFilePath(memoryDir)), /ENOENT/);
|
|
226
|
+
} finally {
|
|
227
|
+
await rm(root, { recursive: true, force: true });
|
|
228
|
+
}
|
|
229
|
+
});
|
|
230
|
+
|
|
231
|
+
test("a re-sync of one source preserves the other sources' sections in the day file", async () => {
|
|
232
|
+
const memoryDir = await mkdtemp(path.join(os.tmpdir(), "remnic-location-pipeline-"));
|
|
233
|
+
try {
|
|
234
|
+
const alpha = { ...provider([[observationAt("2026-08-17T09:00:00Z", "home")]]), id: "alpha", displayName: "Alpha" };
|
|
235
|
+
const beta = { ...provider([[observationAt("2026-08-17T13:00:00Z", "office")]]), id: "beta", displayName: "Beta" };
|
|
236
|
+
let getProvider = withProviders([alpha, beta]);
|
|
237
|
+
await syncLocationDay({
|
|
238
|
+
config: config({ sources: [{ id: "alpha" }, { id: "beta" }] }),
|
|
239
|
+
memoryDir,
|
|
240
|
+
date: "2026-08-17",
|
|
241
|
+
getProvider,
|
|
242
|
+
});
|
|
243
|
+
let day = await readFile(locationDayFilePath(memoryDir, "2026-08-17"), "utf-8");
|
|
244
|
+
assert.match(day, /## Timeline — Alpha/);
|
|
245
|
+
assert.match(day, /## Timeline — Beta/);
|
|
246
|
+
|
|
247
|
+
// Re-sync only alpha with fresh observations; beta's section must survive
|
|
248
|
+
// from sync state without contacting beta again.
|
|
249
|
+
const alphaRefreshed = {
|
|
250
|
+
...provider([[observationAt("2026-08-17T09:00:00Z", "cafe")]]),
|
|
251
|
+
id: "alpha",
|
|
252
|
+
displayName: "Alpha",
|
|
253
|
+
};
|
|
254
|
+
getProvider = withProviders([alphaRefreshed]);
|
|
255
|
+
const results = await syncLocationDay({
|
|
256
|
+
config: config({ sources: [{ id: "alpha" }] }),
|
|
257
|
+
memoryDir,
|
|
258
|
+
date: "2026-08-17",
|
|
259
|
+
getProvider,
|
|
260
|
+
});
|
|
261
|
+
assert.equal(results[0]?.status, "synced");
|
|
262
|
+
day = await readFile(locationDayFilePath(memoryDir, "2026-08-17"), "utf-8");
|
|
263
|
+
assert.match(day, /## Timeline — Alpha/);
|
|
264
|
+
assert.match(day, /## Timeline — Beta/, "the untouched source's day payload survives from state");
|
|
265
|
+
assert.match(day, /cafe/);
|
|
266
|
+
} finally {
|
|
267
|
+
await rm(memoryDir, { recursive: true, force: true });
|
|
268
|
+
}
|
|
269
|
+
});
|
|
270
|
+
|
|
271
|
+
test("retainCoordinates: true persists coordinates end to end", async () => {
|
|
272
|
+
const memoryDir = await mkdtemp(path.join(os.tmpdir(), "remnic-location-pipeline-"));
|
|
273
|
+
try {
|
|
274
|
+
const getProvider = withProviders([provider([[observationAt("2026-08-17T09:00:00Z", "home")]])]);
|
|
275
|
+
await syncLocationDay({
|
|
276
|
+
config: config({ retainCoordinates: true }),
|
|
277
|
+
memoryDir,
|
|
278
|
+
date: "2026-08-17",
|
|
279
|
+
getProvider,
|
|
280
|
+
});
|
|
281
|
+
const day = await readFile(locationDayFilePath(memoryDir, "2026-08-17"), "utf-8");
|
|
282
|
+
assert.match(day, /@ 41\.8781,-87\.6298/);
|
|
283
|
+
} finally {
|
|
284
|
+
await rm(memoryDir, { recursive: true, force: true });
|
|
285
|
+
}
|
|
286
|
+
});
|
|
287
|
+
|
|
288
|
+
test("malformed provider output is rejected loudly", async () => {
|
|
289
|
+
const memoryDir = await mkdtemp(path.join(os.tmpdir(), "remnic-location-pipeline-"));
|
|
290
|
+
try {
|
|
291
|
+
const broken: LocationProvider = {
|
|
292
|
+
id: "fixture",
|
|
293
|
+
displayName: "Fixture",
|
|
294
|
+
async verify() {
|
|
295
|
+
return { ok: true };
|
|
296
|
+
},
|
|
297
|
+
async fetchObservations() {
|
|
298
|
+
return {
|
|
299
|
+
observations: [{ observedAtUtc: "garbage", place: { id: "home", label: "Home" } }],
|
|
300
|
+
nextCursor: null,
|
|
301
|
+
};
|
|
302
|
+
},
|
|
303
|
+
};
|
|
304
|
+
const results = await syncLocationDay({
|
|
305
|
+
config: config(),
|
|
306
|
+
memoryDir,
|
|
307
|
+
date: "2026-08-17",
|
|
308
|
+
getProvider: withProviders([broken]),
|
|
309
|
+
});
|
|
310
|
+
assert.equal(results[0]?.status, "failed");
|
|
311
|
+
await assert.rejects(() => access(locationSyncStateFilePath(memoryDir)), /ENOENT/);
|
|
312
|
+
} finally {
|
|
313
|
+
await rm(memoryDir, { recursive: true, force: true });
|
|
314
|
+
}
|
|
315
|
+
});
|
|
316
|
+
|
|
317
|
+
test("sanitizeProviderError strips credentials, query strings, and coordinates", () => {
|
|
318
|
+
const sanitized = sanitizeProviderError(
|
|
319
|
+
new Error("GET http://x.example/v1?token=sekrit failed: Bearer abc.def.ghi at 41.878113,-87.629798"),
|
|
320
|
+
);
|
|
321
|
+
assert.doesNotMatch(sanitized, /sekrit|abc\.def\.ghi|41\.878113/);
|
|
322
|
+
assert.match(sanitized, /\[redacted\]/);
|
|
323
|
+
assert.match(sanitized, /\[coordinates\]/);
|
|
324
|
+
});
|