@remnic/core 9.63.12 → 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 +15 -15
- package/dist/access-authorization-probe.d.ts +11 -11
- package/dist/access-authorization-probe.js +16 -16
- package/dist/access-boundary.d.ts +11 -11
- package/dist/access-boundary.js +15 -15
- package/dist/access-cli.js +49 -49
- 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 +15 -15
- 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 +17 -17
- package/dist/access-http.d.ts +12 -12
- package/dist/access-http.js +26 -26
- package/dist/access-identity-continuity-surface.d.ts +9 -9
- package/dist/access-identity-continuity-surface.js +15 -15
- package/dist/access-lcm-surface.d.ts +11 -11
- package/dist/access-lcm-surface.js +15 -15
- package/dist/access-mcp.d.ts +11 -11
- package/dist/access-mcp.js +20 -20
- 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 +15 -15
- package/dist/access-observe-write-surface.d.ts +11 -11
- package/dist/access-observe-write-surface.js +15 -15
- package/dist/access-offline-manifest.d.ts +9 -9
- package/dist/access-operations-batch.js +16 -16
- package/dist/access-operations.d.ts +11 -11
- package/dist/access-operations.js +19 -19
- package/dist/access-recall-concurrency.d.ts +11 -11
- package/dist/access-recall-concurrency.js +15 -15
- package/dist/access-recall-response.d.ts +11 -11
- package/dist/access-recall-response.js +15 -15
- package/dist/access-recall-surface.d.ts +11 -11
- package/dist/access-recall-surface.js +15 -15
- package/dist/{access-service-U6jiERic.d.ts → access-service-Bd0lzaHp.d.ts} +7 -7
- package/dist/access-service-helpers.d.ts +11 -11
- package/dist/access-service.d.ts +11 -11
- package/dist/access-service.js +15 -15
- package/dist/access-surface-catalog.d.ts +11 -11
- package/dist/access-surface-catalog.js +17 -17
- 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-HP6B732E.js → chunk-4KKVDOCD.js} +24 -24
- package/dist/{chunk-36XS2ASP.js → chunk-5Q5BEWJ3.js} +2 -2
- package/dist/{chunk-4XN35YQD.js → chunk-5TBO7CDR.js} +29 -1
- package/dist/chunk-5TBO7CDR.js.map +1 -0
- package/dist/{chunk-BYFKJXIR.js → chunk-6I6IOMKS.js} +2 -2
- package/dist/{chunk-YI25J2JD.js → chunk-7D3TTGHY.js} +22 -13
- 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-NWHKHBL4.js → chunk-FMGTFMT3.js} +9 -9
- package/dist/{chunk-57WMVU4D.js → chunk-GSQDPL45.js} +3 -3
- package/dist/{chunk-UUYQWKHO.js → chunk-IHPYTG6F.js} +2 -2
- package/dist/{chunk-XREXYQ5U.js → chunk-IPUUMLSM.js} +21 -21
- package/dist/{chunk-SQ3EGGII.js → chunk-J3JEW5CT.js} +2 -2
- package/dist/{chunk-VCEE4YNA.js → chunk-JE3AKPOO.js} +2 -2
- package/dist/{chunk-IUSV4XJC.js → chunk-KOE3RLNN.js} +2 -2
- package/dist/{chunk-WQ6TRWAE.js → chunk-NMQXUGTU.js} +3 -3
- package/dist/{chunk-V2YH7T6T.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-BS75WJBH.js → chunk-UIYYS2YI.js} +4 -4
- package/dist/{chunk-SYU5JOBQ.js → chunk-UWSETW6O.js} +2 -2
- package/dist/{chunk-CK3DZ6OE.js → chunk-VCNGVU62.js} +2 -2
- package/dist/{chunk-DIJCMHUE.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-RFFCBYBD.js → chunk-XBRGJJK7.js} +2 -2
- package/dist/{chunk-R5AWIILH.js → chunk-XFAWGUNX.js} +2 -2
- package/dist/{chunk-JASFZQ76.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-BsaYHAH9.d.ts → cli-0dDmOsJ8.d.ts} +5 -5
- package/dist/cli.d.ts +13 -13
- package/dist/cli.js +39 -39
- 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 +4 -4
- 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 +16 -16
- 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 +8 -8
- 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 +21 -21
- package/dist/index.js +49 -49
- 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 +49 -49
- 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-jMlmLny4.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/{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 +1 -1
- package/dist/signal.d.ts +1 -1
- package/dist/source-agent-qualifier.d.ts +1 -1
- package/dist/source-agent-qualifier.js +7 -7
- 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 +18 -18
- 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/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/connectors/index.test.ts +24 -0
- package/src/connectors/index.ts +2 -0
- package/src/connectors/prime-agent.ts +38 -0
- 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-4XN35YQD.js.map +0 -1
- package/dist/chunk-AR253JBM.js.map +0 -1
- package/dist/chunk-BB2LH5JM.js.map +0 -1
- package/dist/chunk-YI25J2JD.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-HP6B732E.js.map → chunk-4KKVDOCD.js.map} +0 -0
- /package/dist/{chunk-36XS2ASP.js.map → chunk-5Q5BEWJ3.js.map} +0 -0
- /package/dist/{chunk-BYFKJXIR.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-NWHKHBL4.js.map → chunk-FMGTFMT3.js.map} +0 -0
- /package/dist/{chunk-57WMVU4D.js.map → chunk-GSQDPL45.js.map} +0 -0
- /package/dist/{chunk-UUYQWKHO.js.map → chunk-IHPYTG6F.js.map} +0 -0
- /package/dist/{chunk-XREXYQ5U.js.map → chunk-IPUUMLSM.js.map} +0 -0
- /package/dist/{chunk-SQ3EGGII.js.map → chunk-J3JEW5CT.js.map} +0 -0
- /package/dist/{chunk-VCEE4YNA.js.map → chunk-JE3AKPOO.js.map} +0 -0
- /package/dist/{chunk-IUSV4XJC.js.map → chunk-KOE3RLNN.js.map} +0 -0
- /package/dist/{chunk-WQ6TRWAE.js.map → chunk-NMQXUGTU.js.map} +0 -0
- /package/dist/{chunk-V2YH7T6T.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-BS75WJBH.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-DIJCMHUE.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-RFFCBYBD.js.map → chunk-XBRGJJK7.js.map} +0 -0
- /package/dist/{chunk-R5AWIILH.js.map → chunk-XFAWGUNX.js.map} +0 -0
- /package/dist/{chunk-JASFZQ76.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,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
|
+
});
|
|
@@ -0,0 +1,245 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Location day-sync pipeline (issue #2044).
|
|
3
|
+
*
|
|
4
|
+
* For one local day, each configured (enabled, registered) provider's
|
|
5
|
+
* observations are fetched over the day's half-open UTC window, folded into
|
|
6
|
+
* place-visit segments, merged with the other sources' last-known day
|
|
7
|
+
* payloads from sync state, and rendered to the day document. Sync state
|
|
8
|
+
* advances only AFTER the day document (when any) is durable, so a failed
|
|
9
|
+
* write never moves the watermark past data that did not land. An empty
|
|
10
|
+
* fetch is a successful sync — never conflated with a provider failure
|
|
11
|
+
* (AGENTS.md rule 22).
|
|
12
|
+
*/
|
|
13
|
+
|
|
14
|
+
import { getLocationProvider } from "./registry.js";
|
|
15
|
+
import { locationDayWindow, observationSegments } from "./intervals.js";
|
|
16
|
+
import {
|
|
17
|
+
composeLocationDayBody,
|
|
18
|
+
composeLocationDayMeta,
|
|
19
|
+
loadLocationSyncState,
|
|
20
|
+
serializeLocationDay,
|
|
21
|
+
updateLocationSourceDay,
|
|
22
|
+
writeLocationDay,
|
|
23
|
+
type LocationDaySourceEntry,
|
|
24
|
+
} from "./store.js";
|
|
25
|
+
import type { LocationConfig, LocationObservation, LocationProvider, LocationSegment } from "./types.js";
|
|
26
|
+
|
|
27
|
+
const MAX_SYNC_PAGES = 10_000;
|
|
28
|
+
|
|
29
|
+
export interface LocationDaySyncOptions {
|
|
30
|
+
config: LocationConfig;
|
|
31
|
+
memoryDir: string;
|
|
32
|
+
date: string;
|
|
33
|
+
signal?: AbortSignal;
|
|
34
|
+
/** Runaway-pagination guard; defaults to MAX_SYNC_PAGES. */
|
|
35
|
+
maxPages?: number;
|
|
36
|
+
now?: () => Date;
|
|
37
|
+
/** Provider lookup seam (defaults to the registry); tests inject doubles. */
|
|
38
|
+
getProvider?: (id: string) => LocationProvider | undefined;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
export interface LocationSourceSyncResult {
|
|
42
|
+
sourceId: string;
|
|
43
|
+
status: "synced" | "skipped" | "failed";
|
|
44
|
+
skipReason?: "source-disabled" | "provider-not-registered";
|
|
45
|
+
fetched: number;
|
|
46
|
+
dayWritten: boolean;
|
|
47
|
+
stateSaved: boolean;
|
|
48
|
+
/** Sanitized provider error (no tokens, URLs, or coordinates). */
|
|
49
|
+
error?: string;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
/** Redact credential, URL query, and coordinate shapes from a provider error. */
|
|
53
|
+
export function sanitizeProviderError(error: unknown): string {
|
|
54
|
+
const raw = error instanceof Error ? error.message : String(error);
|
|
55
|
+
return raw
|
|
56
|
+
.replace(/Bearer\s+[A-Za-z0-9._~+/=-]+/gi, "Bearer [redacted]")
|
|
57
|
+
.replace(/[?#][^ \t"']*/g, "[redacted]")
|
|
58
|
+
.replace(/[+-]?\d+\.\d{3,},\s?[+-]?\d+\.\d{3,}/g, "[coordinates]")
|
|
59
|
+
.slice(0, 300);
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
function assertValidObservation(observation: LocationObservation): void {
|
|
63
|
+
if (typeof observation !== "object" || observation === null) {
|
|
64
|
+
throw new TypeError("location provider returned a non-object observation");
|
|
65
|
+
}
|
|
66
|
+
if (typeof observation.observedAtUtc !== "string" || !Number.isFinite(Date.parse(observation.observedAtUtc))) {
|
|
67
|
+
throw new TypeError("location provider returned an observation with an invalid observedAtUtc");
|
|
68
|
+
}
|
|
69
|
+
const place = observation.place;
|
|
70
|
+
if (typeof place !== "object" || place === null || typeof place.id !== "string" || place.id.length === 0) {
|
|
71
|
+
throw new TypeError("location provider returned an observation without a valid place");
|
|
72
|
+
}
|
|
73
|
+
if (typeof place.label !== "string" || place.label.trim().length === 0) {
|
|
74
|
+
throw new TypeError(`location provider returned place '${place.id}' without a label`);
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
/** Fetch every observation one provider holds for the window, all pages. */
|
|
79
|
+
async function fetchWindow(
|
|
80
|
+
provider: LocationProvider,
|
|
81
|
+
window: { startUtc: string; endUtc: string },
|
|
82
|
+
options: LocationDaySyncOptions,
|
|
83
|
+
): Promise<LocationObservation[]> {
|
|
84
|
+
const maxPages = options.maxPages ?? MAX_SYNC_PAGES;
|
|
85
|
+
if (!Number.isInteger(maxPages) || maxPages < 1) {
|
|
86
|
+
throw new RangeError("location sync maxPages must be a positive integer");
|
|
87
|
+
}
|
|
88
|
+
const observations: LocationObservation[] = [];
|
|
89
|
+
const seenCursors = new Set<string>();
|
|
90
|
+
let cursor: string | null = null;
|
|
91
|
+
let pages = 0;
|
|
92
|
+
let completed = false;
|
|
93
|
+
while (pages < maxPages) {
|
|
94
|
+
options.signal?.throwIfAborted();
|
|
95
|
+
const page = await provider.fetchObservations({
|
|
96
|
+
startUtc: window.startUtc,
|
|
97
|
+
endUtc: window.endUtc,
|
|
98
|
+
cursor,
|
|
99
|
+
signal: options.signal,
|
|
100
|
+
});
|
|
101
|
+
pages += 1;
|
|
102
|
+
for (const observation of page.observations) assertValidObservation(observation);
|
|
103
|
+
observations.push(...page.observations);
|
|
104
|
+
if (page.nextCursor === null) {
|
|
105
|
+
completed = true;
|
|
106
|
+
break;
|
|
107
|
+
}
|
|
108
|
+
if (typeof page.nextCursor !== "string" || page.nextCursor.length === 0) {
|
|
109
|
+
throw new TypeError("location provider returned an invalid cursor");
|
|
110
|
+
}
|
|
111
|
+
if (seenCursors.has(page.nextCursor)) {
|
|
112
|
+
throw new Error("location provider returned a repeated cursor");
|
|
113
|
+
}
|
|
114
|
+
seenCursors.add(page.nextCursor);
|
|
115
|
+
cursor = page.nextCursor;
|
|
116
|
+
}
|
|
117
|
+
// Only a loop that ran out of its page budget without a terminal null
|
|
118
|
+
// cursor is a runaway; completing on the final allowed page is normal.
|
|
119
|
+
if (!completed) {
|
|
120
|
+
throw new Error(`location provider exceeded ${maxPages} pages`);
|
|
121
|
+
}
|
|
122
|
+
return observations;
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
/**
|
|
126
|
+
* Sync every configured source for one local day. The master gate
|
|
127
|
+
* (`config.enabled === false`) short-circuits every path: no providers are
|
|
128
|
+
* contacted and nothing touches the filesystem. A provider failure fails
|
|
129
|
+
* that source only (state not advanced) and never blocks the others.
|
|
130
|
+
*/
|
|
131
|
+
export async function syncLocationDay(options: LocationDaySyncOptions): Promise<LocationSourceSyncResult[]> {
|
|
132
|
+
if (!options.config.enabled) return [];
|
|
133
|
+
const window = locationDayWindow(options.date, options.config.timezone);
|
|
134
|
+
const now = options.now ?? (() => new Date());
|
|
135
|
+
const resolveProvider = options.getProvider ?? getLocationProvider;
|
|
136
|
+
|
|
137
|
+
const results: LocationSourceSyncResult[] = [];
|
|
138
|
+
for (const source of options.config.sources) {
|
|
139
|
+
if (!source.enabled) {
|
|
140
|
+
results.push({
|
|
141
|
+
sourceId: source.id,
|
|
142
|
+
status: "skipped",
|
|
143
|
+
skipReason: "source-disabled",
|
|
144
|
+
fetched: 0,
|
|
145
|
+
dayWritten: false,
|
|
146
|
+
stateSaved: false,
|
|
147
|
+
});
|
|
148
|
+
continue;
|
|
149
|
+
}
|
|
150
|
+
const provider = resolveProvider(source.id);
|
|
151
|
+
if (provider === undefined) {
|
|
152
|
+
results.push({
|
|
153
|
+
sourceId: source.id,
|
|
154
|
+
status: "skipped",
|
|
155
|
+
skipReason: "provider-not-registered",
|
|
156
|
+
fetched: 0,
|
|
157
|
+
dayWritten: false,
|
|
158
|
+
stateSaved: false,
|
|
159
|
+
});
|
|
160
|
+
continue;
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
let observations: LocationObservation[];
|
|
164
|
+
try {
|
|
165
|
+
observations = await fetchWindow(provider, window, options);
|
|
166
|
+
} catch (error) {
|
|
167
|
+
results.push({
|
|
168
|
+
sourceId: source.id,
|
|
169
|
+
status: "failed",
|
|
170
|
+
fetched: 0,
|
|
171
|
+
dayWritten: false,
|
|
172
|
+
stateSaved: false,
|
|
173
|
+
...(error instanceof Error && error.name === "AbortError"
|
|
174
|
+
? { error: "aborted" }
|
|
175
|
+
: { error: sanitizeProviderError(error) }),
|
|
176
|
+
});
|
|
177
|
+
continue;
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
try {
|
|
181
|
+
options.signal?.throwIfAborted();
|
|
182
|
+
const segments = observationSegments(observations, window, {
|
|
183
|
+
retainCoordinates: options.config.retainCoordinates,
|
|
184
|
+
});
|
|
185
|
+
const payload = { observationCount: observations.length, segments, providerDisplayName: provider.displayName };
|
|
186
|
+
const merged = await mergeDaySources(options.memoryDir, source.id, options.date, payload);
|
|
187
|
+
let dayWritten = false;
|
|
188
|
+
if (Object.values(merged).some((entry) => entry.observationCount > 0)) {
|
|
189
|
+
const body = composeLocationDayBody(options.date, options.config.timezone, merged);
|
|
190
|
+
const meta = composeLocationDayMeta(options.date, options.config.timezone, merged, body);
|
|
191
|
+
dayWritten = await writeLocationDay(options.memoryDir, options.date, serializeLocationDay(meta, body));
|
|
192
|
+
}
|
|
193
|
+
// State advances only after the durable day write above succeeded.
|
|
194
|
+
await updateLocationSourceDay(
|
|
195
|
+
options.memoryDir,
|
|
196
|
+
source.id,
|
|
197
|
+
options.date,
|
|
198
|
+
payload,
|
|
199
|
+
now().toISOString(),
|
|
200
|
+
);
|
|
201
|
+
results.push({
|
|
202
|
+
sourceId: source.id,
|
|
203
|
+
status: "synced",
|
|
204
|
+
fetched: observations.length,
|
|
205
|
+
dayWritten,
|
|
206
|
+
stateSaved: true,
|
|
207
|
+
});
|
|
208
|
+
} catch (error) {
|
|
209
|
+
results.push({
|
|
210
|
+
sourceId: source.id,
|
|
211
|
+
status: "failed",
|
|
212
|
+
fetched: observations.length,
|
|
213
|
+
dayWritten: false,
|
|
214
|
+
stateSaved: false,
|
|
215
|
+
...(error instanceof Error && error.name === "AbortError"
|
|
216
|
+
? { error: "aborted" }
|
|
217
|
+
: { error: sanitizeProviderError(error) }),
|
|
218
|
+
});
|
|
219
|
+
}
|
|
220
|
+
}
|
|
221
|
+
return results;
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
/**
|
|
225
|
+
* The day-document source map: this source's fresh payload plus every OTHER
|
|
226
|
+
* source's last-known payload from sync state. Rendered documents therefore
|
|
227
|
+
* survive a re-sync of one source without re-fetching the others.
|
|
228
|
+
*/
|
|
229
|
+
async function mergeDaySources(
|
|
230
|
+
memoryDir: string,
|
|
231
|
+
sourceId: string,
|
|
232
|
+
date: string,
|
|
233
|
+
payload: { observationCount: number; segments: LocationSegment[] },
|
|
234
|
+
): Promise<Record<string, LocationDaySourceEntry>> {
|
|
235
|
+
const state = await loadLocationSyncState(memoryDir);
|
|
236
|
+
const merged: Record<string, LocationDaySourceEntry> = {};
|
|
237
|
+
for (const [otherId, otherState] of Object.entries(state.sources)) {
|
|
238
|
+
if (otherId === sourceId) continue;
|
|
239
|
+
const day = otherState.days?.[date];
|
|
240
|
+
if (day === undefined) continue;
|
|
241
|
+
merged[otherId] = { ...day, segments: [...day.segments] };
|
|
242
|
+
}
|
|
243
|
+
merged[sourceId] = payload;
|
|
244
|
+
return merged;
|
|
245
|
+
}
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
import assert from "node:assert/strict";
|
|
2
|
+
import test from "node:test";
|
|
3
|
+
|
|
4
|
+
import { isSearchExcludedPath } from "../orchestration/generic-recall-paths.js";
|
|
5
|
+
import { isLocationDayPath } from "../orchestration/orchestrator-helpers.js";
|
|
6
|
+
|
|
7
|
+
const MEMORY_DIR = "/tmp/remnic-fixture-memory";
|
|
8
|
+
|
|
9
|
+
test("isLocationDayPath matches only the top-level day-file shape", () => {
|
|
10
|
+
assert.equal(isLocationDayPath("locations/2026-08-17.md", MEMORY_DIR), true);
|
|
11
|
+
assert.equal(isLocationDayPath(`${MEMORY_DIR}/locations/2026-08-17.md`, MEMORY_DIR), true);
|
|
12
|
+
assert.equal(isLocationDayPath("facts/notes/locations.md", MEMORY_DIR), false);
|
|
13
|
+
assert.equal(isLocationDayPath("facts/locations/2026-08-17.md", MEMORY_DIR), false, "nested stays recallable");
|
|
14
|
+
assert.equal(isLocationDayPath("locations/2026-08-17.md"), true, "root-unaware best-effort fallback");
|
|
15
|
+
assert.equal(isLocationDayPath("locations/not-a-date.md"), false);
|
|
16
|
+
});
|
|
17
|
+
|
|
18
|
+
test("isSearchExcludedPath keeps location day documents out of generic search and recall", () => {
|
|
19
|
+
assert.equal(isSearchExcludedPath("locations/2026-08-17.md"), true);
|
|
20
|
+
assert.equal(
|
|
21
|
+
isSearchExcludedPath("qmd://collection/locations/2026-08-17.md", {}, "qmd"),
|
|
22
|
+
true,
|
|
23
|
+
"collection-qualified QMD paths are stripped and re-tested",
|
|
24
|
+
);
|
|
25
|
+
assert.equal(
|
|
26
|
+
isSearchExcludedPath(`${MEMORY_DIR}/namespaces/team/locations/2026-08-17.md`, { memoryDir: MEMORY_DIR }),
|
|
27
|
+
true,
|
|
28
|
+
"a namespaced day document is excluded via the namespace root",
|
|
29
|
+
);
|
|
30
|
+
assert.equal(isSearchExcludedPath("facts/locations-tips.md"), false);
|
|
31
|
+
assert.equal(
|
|
32
|
+
isSearchExcludedPath("namespaces/locations/facts/a.md", { memoryDir: MEMORY_DIR }),
|
|
33
|
+
false,
|
|
34
|
+
"a namespace literally named locations keeps its memories recallable",
|
|
35
|
+
);
|
|
36
|
+
});
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
import assert from "node:assert/strict";
|
|
2
|
+
import test from "node:test";
|
|
3
|
+
|
|
4
|
+
import {
|
|
5
|
+
clearLocationProviders,
|
|
6
|
+
getLocationProvider,
|
|
7
|
+
listLocationProviders,
|
|
8
|
+
registerLocationProvider,
|
|
9
|
+
} from "./registry.js";
|
|
10
|
+
import type { LocationProvider } from "./types.js";
|
|
11
|
+
|
|
12
|
+
function provider(overrides: Partial<LocationProvider> = {}): LocationProvider {
|
|
13
|
+
return {
|
|
14
|
+
id: "fixture-provider",
|
|
15
|
+
displayName: "Fixture Provider",
|
|
16
|
+
async verify() {
|
|
17
|
+
return { ok: true };
|
|
18
|
+
},
|
|
19
|
+
async fetchObservations() {
|
|
20
|
+
return { observations: [], nextCursor: null };
|
|
21
|
+
},
|
|
22
|
+
...overrides,
|
|
23
|
+
};
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
test("registerLocationProvider registers, looks up, and lists sorted", () => {
|
|
27
|
+
clearLocationProviders();
|
|
28
|
+
registerLocationProvider(provider({ id: "zeta", displayName: "Z" }));
|
|
29
|
+
registerLocationProvider(provider({ id: "alpha", displayName: "A" }));
|
|
30
|
+
assert.deepEqual(listLocationProviders(), ["alpha", "zeta"]);
|
|
31
|
+
assert.equal(getLocationProvider("alpha")?.displayName, "A");
|
|
32
|
+
assert.equal(getLocationProvider("missing"), undefined);
|
|
33
|
+
});
|
|
34
|
+
|
|
35
|
+
test("registerLocationProvider rejects duplicate ids", () => {
|
|
36
|
+
clearLocationProviders();
|
|
37
|
+
registerLocationProvider(provider());
|
|
38
|
+
assert.throws(() => registerLocationProvider(provider()), /already registered/);
|
|
39
|
+
});
|
|
40
|
+
|
|
41
|
+
test("registerLocationProvider rejects invalid ids", () => {
|
|
42
|
+
clearLocationProviders();
|
|
43
|
+
assert.throws(() => registerLocationProvider(provider({ id: "" })), RangeError);
|
|
44
|
+
assert.throws(() => registerLocationProvider(provider({ id: "Upper" })), RangeError);
|
|
45
|
+
assert.throws(() => registerLocationProvider(provider({ id: "has space" })), RangeError);
|
|
46
|
+
assert.throws(() => registerLocationProvider(provider({ id: "../traversal" })), RangeError);
|
|
47
|
+
assert.throws(
|
|
48
|
+
() => registerLocationProvider(provider({ id: "x", displayName: "" as unknown as string })),
|
|
49
|
+
TypeError,
|
|
50
|
+
);
|
|
51
|
+
assert.throws(
|
|
52
|
+
() =>
|
|
53
|
+
registerLocationProvider(
|
|
54
|
+
provider({ id: "x", verify: undefined as unknown as LocationProvider["verify"] }),
|
|
55
|
+
),
|
|
56
|
+
TypeError,
|
|
57
|
+
);
|
|
58
|
+
assert.throws(
|
|
59
|
+
() =>
|
|
60
|
+
registerLocationProvider(
|
|
61
|
+
provider({ id: "x", fetchObservations: undefined as unknown as LocationProvider["fetchObservations"] }),
|
|
62
|
+
),
|
|
63
|
+
TypeError,
|
|
64
|
+
);
|
|
65
|
+
assert.deepEqual(listLocationProviders(), []);
|
|
66
|
+
});
|
|
67
|
+
|
|
68
|
+
test("clearLocationProviders resets the registry", () => {
|
|
69
|
+
clearLocationProviders();
|
|
70
|
+
registerLocationProvider(provider());
|
|
71
|
+
clearLocationProviders();
|
|
72
|
+
assert.deepEqual(listLocationProviders(), []);
|
|
73
|
+
assert.equal(getLocationProvider("fixture-provider"), undefined);
|
|
74
|
+
});
|