@remnic/core 9.66.1 → 9.66.2
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 +1 -1
- package/dist/access-admin-ops-surface.js +26 -26
- package/dist/access-audit.js +2 -2
- package/dist/access-authorization-probe.d.ts +1 -1
- package/dist/access-authorization-probe.js +27 -27
- package/dist/access-boundary.d.ts +1 -1
- package/dist/access-boundary.js +26 -26
- package/dist/access-cli.js +90 -88
- package/dist/access-cli.js.map +1 -1
- package/dist/access-extraction-force-flush.d.ts +1 -1
- package/dist/access-extraction-force-flush.js +26 -26
- package/dist/access-http-lcm-compaction.d.ts +1 -1
- package/dist/access-http-lifecycle-flush.d.ts +1 -1
- package/dist/access-http-offline-stream.d.ts +1 -1
- package/dist/access-http-query.js +28 -28
- package/dist/access-http.d.ts +2 -2
- package/dist/access-http.js +41 -41
- package/dist/access-identity-continuity-surface.js +26 -26
- package/dist/access-lcm-surface.d.ts +1 -1
- package/dist/access-lcm-surface.js +26 -26
- package/dist/access-mcp.d.ts +1 -1
- package/dist/access-mcp.js +33 -33
- package/dist/access-namespace-preflight.js +26 -26
- package/dist/access-observe-write-surface.d.ts +1 -1
- package/dist/access-observe-write-surface.js +26 -26
- package/dist/access-operations-batch.js +28 -28
- package/dist/access-operations.d.ts +6 -6
- package/dist/access-operations.js +32 -32
- package/dist/access-recall-concurrency.d.ts +1 -1
- package/dist/access-recall-concurrency.js +26 -26
- package/dist/access-recall-response.d.ts +1 -1
- package/dist/access-recall-response.js +26 -26
- package/dist/access-recall-surface.d.ts +1 -1
- package/dist/access-recall-surface.js +26 -26
- package/dist/access-schema.d.ts +89 -89
- package/dist/access-service-helpers.d.ts +1 -1
- package/dist/access-service.d.ts +1 -1
- package/dist/access-service.js +26 -26
- package/dist/access-surface-catalog.d.ts +1 -1
- package/dist/access-surface-catalog.js +28 -28
- package/dist/active-recall.js +3 -3
- package/dist/adapters/index.js +4 -4
- package/dist/adapters/registry.js +2 -2
- package/dist/briefing.js +10 -10
- package/dist/buffer.js +4 -4
- package/dist/causal-behavior.js +2 -2
- package/dist/causal-chain.js +2 -2
- package/dist/causal-consolidation.js +13 -13
- package/dist/causal-retrieval.js +2 -2
- package/dist/causal-trajectory-graph.js +2 -2
- package/dist/causal-trajectory.js +1 -1
- package/dist/{chunk-LYUUA6QB.js → chunk-25OLNWYG.js} +2 -2
- package/dist/{chunk-2X6BGKQL.js → chunk-2QSGJTDG.js} +10 -10
- package/dist/{chunk-ZZZWEDF4.js → chunk-2VZHB5US.js} +2 -2
- package/dist/{chunk-JKTV3JNX.js → chunk-36HJ36FK.js} +3 -3
- package/dist/{chunk-XKNHFDTT.js → chunk-3A2Z4OOS.js} +2 -2
- package/dist/{chunk-TCF4MU7L.js → chunk-47WWTCJY.js} +1001 -101
- package/dist/chunk-47WWTCJY.js.map +1 -0
- package/dist/{chunk-RDGKVH6T.js → chunk-4NKIGSJM.js} +2 -2
- package/dist/{chunk-QBNE55VA.js → chunk-4PG2CKI6.js} +2 -2
- package/dist/{chunk-S4CDUXQY.js → chunk-6545JDIM.js} +2 -2
- package/dist/{chunk-2FA7U5XY.js → chunk-65HXVEVG.js} +2 -2
- package/dist/{chunk-U6NGKS32.js → chunk-6BYJQ3L2.js} +8 -8
- package/dist/{chunk-OMX6TKXH.js → chunk-6PUF7LUO.js} +2 -2
- package/dist/{chunk-TVVEYCNW.js → chunk-7K5Q6COX.js} +4 -4
- package/dist/{chunk-VG4SSPAW.js → chunk-AI5M4FVQ.js} +2 -2
- package/dist/{chunk-MUEGIKKY.js → chunk-B4B3QDV3.js} +21 -20
- package/dist/chunk-B4B3QDV3.js.map +1 -0
- package/dist/{chunk-YMOPYG3K.js → chunk-BIJAWZYW.js} +6 -6
- package/dist/{chunk-M5ERTI4I.js → chunk-BLHKR4GJ.js} +2 -2
- package/dist/chunk-BLUYQFRH.js +36 -0
- package/dist/chunk-BLUYQFRH.js.map +1 -0
- package/dist/{chunk-CEH2AFXG.js → chunk-C24UZSFW.js} +6 -6
- package/dist/{chunk-EH65UV2Z.js → chunk-C3ZYZKDJ.js} +2 -2
- package/dist/{chunk-NFP4KPTA.js → chunk-CDLPF3PX.js} +2 -2
- package/dist/{chunk-FFUMTH27.js → chunk-CQ6IWZDI.js} +2 -2
- package/dist/{chunk-KWIGDKM2.js → chunk-D2YNNDML.js} +2 -2
- package/dist/{chunk-65JOGLIS.js → chunk-D3DUA64S.js} +2 -2
- package/dist/{chunk-2ITZ74A6.js → chunk-DSEHPUID.js} +11 -11
- package/dist/chunk-E4X2YX2F.js +22 -0
- package/dist/chunk-E4X2YX2F.js.map +1 -0
- package/dist/{chunk-AF5H7TKI.js → chunk-EH7BNIUN.js} +2 -2
- package/dist/{chunk-IGZCAHFL.js → chunk-EQRD4NMH.js} +2 -2
- package/dist/{chunk-CX4OLNIH.js → chunk-ER2UPUMG.js} +2 -2
- package/dist/{chunk-SZMYGDSM.js → chunk-F2JXJABQ.js} +2 -2
- package/dist/{chunk-CHGG74CY.js → chunk-F4W5HTED.js} +2 -2
- package/dist/{chunk-H2IRSXZN.js → chunk-FFWONAAW.js} +3 -3
- package/dist/chunk-FYDW33ZB.js +13 -0
- package/dist/chunk-FYDW33ZB.js.map +1 -0
- package/dist/{chunk-TNGH4ZYI.js → chunk-GNWLTCKC.js} +2 -2
- package/dist/{chunk-OJUEUQDJ.js → chunk-JJ6ZEFXC.js} +2 -2
- package/dist/{chunk-U65VJSCJ.js → chunk-JMXHVJ2V.js} +10 -10
- package/dist/{chunk-NACLNNFQ.js → chunk-KSZKQTZS.js} +2 -2
- package/dist/{chunk-TCQRIIIN.js → chunk-LSR5LQXN.js} +2 -2
- package/dist/chunk-MAG6M3B6.js +51 -0
- package/dist/chunk-MAG6M3B6.js.map +1 -0
- package/dist/{chunk-B4AOWRYF.js → chunk-NTFXBILO.js} +4 -4
- package/dist/{chunk-2PSX5BJU.js → chunk-NZPFCP5A.js} +2 -2
- package/dist/{chunk-C5IUOBCN.js → chunk-OERZXNSK.js} +2 -2
- package/dist/{chunk-ZPS724LT.js → chunk-OIDICRH7.js} +2 -2
- package/dist/{chunk-LTBKKSCX.js → chunk-OLBZ2WGG.js} +2 -2
- package/dist/{chunk-PD66CVSD.js → chunk-OUKBMQ2O.js} +2 -2
- package/dist/{chunk-GPFZFCRN.js → chunk-PP7R754B.js} +1 -1
- package/dist/{chunk-GAJK3DQ5.js → chunk-PRRUK2ZH.js} +2 -2
- package/dist/{chunk-TBCDF556.js → chunk-PW3GKWH3.js} +3 -3
- package/dist/chunk-Q3SHEAXR.js +80 -0
- package/dist/chunk-Q3SHEAXR.js.map +1 -0
- package/dist/{chunk-KI64TTBL.js → chunk-R2Q6FTOX.js} +2 -2
- package/dist/{chunk-KJXCD7VR.js → chunk-RCPKBW7K.js} +4 -4
- package/dist/{chunk-INUBU6I5.js → chunk-RH3XLQG4.js} +2 -2
- package/dist/{chunk-JBPKEARU.js → chunk-RKNJBZ55.js} +4 -4
- package/dist/{chunk-NSTNGMSS.js → chunk-U4HZEW2B.js} +5 -5
- package/dist/{chunk-XOHYGIWF.js → chunk-UHQE37NI.js} +2 -2
- package/dist/{chunk-D3UXGE5D.js → chunk-UMZUKOJU.js} +7 -7
- package/dist/{chunk-S4Z2OBTX.js → chunk-UVYL5G7A.js} +3 -3
- package/dist/{chunk-4XLTUR6R.js → chunk-VKDBDJIX.js} +3 -3
- package/dist/{chunk-UW4RYPWC.js → chunk-VSMSYDIH.js} +2 -2
- package/dist/{chunk-LJABGXC4.js → chunk-W5IYHQA5.js} +3 -3
- package/dist/{chunk-7UOXCCCI.js → chunk-WKWY7YF3.js} +2 -2
- package/dist/{chunk-YWPLOJ7T.js → chunk-Y2YM4C75.js} +2 -2
- package/dist/{chunk-NUF7VSEX.js → chunk-YZ5VC2JJ.js} +2 -2
- package/dist/{chunk-EWKVYCFP.js → chunk-ZBIEX4XN.js} +1 -1
- package/dist/{chunk-GBEOUCBB.js → chunk-ZMVW2HJJ.js} +2 -2
- package/dist/{chunk-RPIS5FQ7.js → chunk-ZXROBS53.js} +44 -44
- package/dist/{cli-CsfFMe6n.d.ts → cli-BOuEb4Ia.d.ts} +1 -1
- package/dist/cli.d.ts +2 -2
- package/dist/cli.js +61 -61
- package/dist/coding/export-okf-codegraph.js +10 -10
- package/dist/coding/pre-action-gate.js +2 -2
- package/dist/compounding/engine.js +10 -10
- package/dist/config.js +3 -3
- package/dist/connectors/codex-materialize-runner.js +10 -10
- package/dist/connectors/index.js +10 -10
- package/dist/convergence-refresh.js +12 -12
- package/dist/corpus-watermark.js +12 -12
- package/dist/entity-retrieval.js +10 -10
- package/dist/external-wiki-access.d.ts +1 -1
- package/dist/external-wiki-access.js +27 -27
- package/dist/external-wiki-mcp-tools.d.ts +1 -1
- package/dist/extraction-span-wire.d.ts +17 -0
- package/dist/extraction-span-wire.js +13 -0
- package/dist/extraction-span-wire.js.map +1 -0
- package/dist/extraction-span.d.ts +17 -0
- package/dist/extraction-span.js +8 -0
- package/dist/extraction.js +13 -13
- package/dist/{graph-edge-decay-A4SDFMMS.js → graph-edge-decay-FXGELBFA.js} +4 -4
- package/dist/graph-edge-decay-FXGELBFA.js.map +1 -0
- package/dist/graph-snapshot.js +3 -3
- package/dist/graph.js +3 -3
- package/dist/harmonic-retrieval.js +1 -1
- package/dist/index.d.ts +887 -681
- package/dist/index.js +161 -108
- package/dist/lcm/engine.js +3 -3
- package/dist/lcm/index.js +11 -11
- package/dist/location/index.js +12 -12
- package/dist/maintenance/memory-governance.js +10 -10
- package/dist/maintenance/rebuild-memory-lifecycle-ledger.js +10 -10
- package/dist/maintenance/rebuild-memory-projection.js +11 -11
- package/dist/mcp-memory-inspector-app.d.ts +1 -1
- package/dist/memory-subject-cli.d.ts +1 -1
- package/dist/memory-subject.d.ts +1 -1
- package/dist/namespaces/migrate.js +11 -11
- package/dist/namespaces/search.js +6 -6
- package/dist/namespaces/storage.js +10 -10
- package/dist/operator-doctor-corpus.js +13 -13
- package/dist/operator-toolkit.js +23 -23
- package/dist/orchestration/maintenance.js +18 -18
- package/dist/orchestrator.js +90 -88
- package/dist/{public-http-Cwvl4QZr.d.ts → public-http-CmQRjQCD.d.ts} +1 -1
- package/dist/qmd.js +4 -4
- package/dist/recall-deep-access.d.ts +30 -0
- package/dist/recall-deep-access.js +50 -0
- package/dist/recall-deep-access.js.map +1 -0
- package/dist/recall-deep-render.d.ts +11 -0
- package/dist/recall-deep-render.js +28 -0
- package/dist/recall-deep-render.js.map +1 -0
- package/dist/recall-deep.js +6 -73
- package/dist/recall-deep.js.map +1 -1
- package/dist/recall-navigate.js +6 -42
- package/dist/recall-navigate.js.map +1 -1
- package/dist/recall-state-view-render.d.ts +18 -0
- package/dist/recall-state-view-render.js +22 -0
- package/dist/recall-state-view-render.js.map +1 -0
- package/dist/recall-state-view-widen.d.ts +13 -0
- package/dist/recall-state-view-widen.js +11 -0
- package/dist/recall-state-view-widen.js.map +1 -0
- package/dist/recall-state-view-wire.d.ts +3 -4
- package/dist/recall-state-view-wire.js +2 -1
- package/dist/resume-bundles.js +4 -4
- package/dist/schemas.d.ts +176 -176
- package/dist/search/factory.js +5 -5
- package/dist/search/index.js +9 -9
- package/dist/semantic-consolidation.js +11 -11
- package/dist/semantic-rule-promotion.js +10 -10
- package/dist/semantic-rule-verifier.js +10 -10
- package/dist/shared-context/manager.d.ts +6 -6
- package/dist/source-agent-qualifier.js +12 -12
- package/dist/storage.js +9 -9
- package/dist/support-passport/index.d.ts +4 -4
- package/dist/support-passport/index.js +29 -29
- package/dist/transfer/export-okf.js +11 -11
- package/dist/transfer/types.d.ts +78 -78
- package/dist/verified-recall.js +10 -10
- package/dist/who-knows-cli.d.ts +1 -1
- package/dist/who-knows-cli.js +11 -11
- package/dist/who-knows.d.ts +1 -1
- package/dist/who-knows.js +10 -10
- package/package.json +2 -2
- package/src/activity/index.ts +3 -0
- package/src/activity/journal-vault-read.test.ts +153 -0
- package/src/activity/journal-vault-read.ts +214 -0
- package/src/activity/journal.ts +1 -1
- package/src/activity/timeline/analysis-batch.test.ts +56 -0
- package/src/activity/timeline/analysis-batch.ts +45 -0
- package/src/activity/timeline/analysis.test.ts +413 -0
- package/src/activity/timeline/analysis.ts +362 -0
- package/src/activity/timeline/index.ts +5 -0
- package/src/activity/timeline/journal-recap-persist.test.ts +148 -0
- package/src/activity/timeline/journal-recap-persist.ts +37 -0
- package/src/activity/timeline/weekly-persist.test.ts +121 -0
- package/src/activity/timeline/weekly-persist.ts +103 -0
- package/src/activity/vault-path.test.ts +38 -0
- package/src/activity/vault-path.ts +59 -0
- package/src/activity/vault-publish.test.ts +102 -0
- package/src/activity/vault-publish.ts +92 -13
- package/src/dedup/merge-on-write-wire.test.ts +101 -0
- package/src/dedup/merge-on-write-wire.ts +34 -0
- package/src/extraction-span-wire.test.ts +20 -0
- package/src/extraction-span-wire.ts +17 -0
- package/src/extraction-span.test.ts +59 -0
- package/src/extraction-span.ts +39 -0
- package/src/index.ts +1 -0
- package/src/recall-deep-access.test.ts +58 -0
- package/src/recall-deep-access.ts +71 -0
- package/src/recall-deep-render.test.ts +68 -0
- package/src/recall-deep-render.ts +32 -0
- package/src/recall-state-view-render.test.ts +94 -0
- package/src/recall-state-view-render.ts +24 -0
- package/src/recall-state-view-widen.test.ts +66 -0
- package/src/recall-state-view-widen.ts +49 -0
- package/src/recall-state-view-wire.ts +6 -16
- package/src/shared-context/staleness.test.ts +36 -0
- package/src/shared-context/staleness.ts +38 -0
- package/src/shared-context/trajectories.test.ts +77 -0
- package/src/shared-context/trajectories.ts +49 -0
- package/dist/chunk-MUEGIKKY.js.map +0 -1
- package/dist/chunk-TCF4MU7L.js.map +0 -1
- package/dist/chunk-WFCWP4XU.js +0 -17
- package/dist/chunk-WFCWP4XU.js.map +0 -1
- package/dist/{access-service-C2tV2-uL.d.ts → access-service-Dz6I9AZ-.d.ts} +64 -64
- /package/dist/{chunk-LYUUA6QB.js.map → chunk-25OLNWYG.js.map} +0 -0
- /package/dist/{chunk-2X6BGKQL.js.map → chunk-2QSGJTDG.js.map} +0 -0
- /package/dist/{chunk-ZZZWEDF4.js.map → chunk-2VZHB5US.js.map} +0 -0
- /package/dist/{chunk-JKTV3JNX.js.map → chunk-36HJ36FK.js.map} +0 -0
- /package/dist/{chunk-XKNHFDTT.js.map → chunk-3A2Z4OOS.js.map} +0 -0
- /package/dist/{chunk-RDGKVH6T.js.map → chunk-4NKIGSJM.js.map} +0 -0
- /package/dist/{chunk-QBNE55VA.js.map → chunk-4PG2CKI6.js.map} +0 -0
- /package/dist/{chunk-S4CDUXQY.js.map → chunk-6545JDIM.js.map} +0 -0
- /package/dist/{chunk-2FA7U5XY.js.map → chunk-65HXVEVG.js.map} +0 -0
- /package/dist/{chunk-U6NGKS32.js.map → chunk-6BYJQ3L2.js.map} +0 -0
- /package/dist/{chunk-OMX6TKXH.js.map → chunk-6PUF7LUO.js.map} +0 -0
- /package/dist/{chunk-TVVEYCNW.js.map → chunk-7K5Q6COX.js.map} +0 -0
- /package/dist/{chunk-VG4SSPAW.js.map → chunk-AI5M4FVQ.js.map} +0 -0
- /package/dist/{chunk-YMOPYG3K.js.map → chunk-BIJAWZYW.js.map} +0 -0
- /package/dist/{chunk-M5ERTI4I.js.map → chunk-BLHKR4GJ.js.map} +0 -0
- /package/dist/{chunk-CEH2AFXG.js.map → chunk-C24UZSFW.js.map} +0 -0
- /package/dist/{chunk-EH65UV2Z.js.map → chunk-C3ZYZKDJ.js.map} +0 -0
- /package/dist/{chunk-NFP4KPTA.js.map → chunk-CDLPF3PX.js.map} +0 -0
- /package/dist/{chunk-FFUMTH27.js.map → chunk-CQ6IWZDI.js.map} +0 -0
- /package/dist/{chunk-KWIGDKM2.js.map → chunk-D2YNNDML.js.map} +0 -0
- /package/dist/{chunk-65JOGLIS.js.map → chunk-D3DUA64S.js.map} +0 -0
- /package/dist/{chunk-2ITZ74A6.js.map → chunk-DSEHPUID.js.map} +0 -0
- /package/dist/{chunk-AF5H7TKI.js.map → chunk-EH7BNIUN.js.map} +0 -0
- /package/dist/{chunk-IGZCAHFL.js.map → chunk-EQRD4NMH.js.map} +0 -0
- /package/dist/{chunk-CX4OLNIH.js.map → chunk-ER2UPUMG.js.map} +0 -0
- /package/dist/{chunk-SZMYGDSM.js.map → chunk-F2JXJABQ.js.map} +0 -0
- /package/dist/{chunk-CHGG74CY.js.map → chunk-F4W5HTED.js.map} +0 -0
- /package/dist/{chunk-H2IRSXZN.js.map → chunk-FFWONAAW.js.map} +0 -0
- /package/dist/{chunk-TNGH4ZYI.js.map → chunk-GNWLTCKC.js.map} +0 -0
- /package/dist/{chunk-OJUEUQDJ.js.map → chunk-JJ6ZEFXC.js.map} +0 -0
- /package/dist/{chunk-U65VJSCJ.js.map → chunk-JMXHVJ2V.js.map} +0 -0
- /package/dist/{chunk-NACLNNFQ.js.map → chunk-KSZKQTZS.js.map} +0 -0
- /package/dist/{chunk-TCQRIIIN.js.map → chunk-LSR5LQXN.js.map} +0 -0
- /package/dist/{chunk-B4AOWRYF.js.map → chunk-NTFXBILO.js.map} +0 -0
- /package/dist/{chunk-2PSX5BJU.js.map → chunk-NZPFCP5A.js.map} +0 -0
- /package/dist/{chunk-C5IUOBCN.js.map → chunk-OERZXNSK.js.map} +0 -0
- /package/dist/{chunk-ZPS724LT.js.map → chunk-OIDICRH7.js.map} +0 -0
- /package/dist/{chunk-LTBKKSCX.js.map → chunk-OLBZ2WGG.js.map} +0 -0
- /package/dist/{chunk-PD66CVSD.js.map → chunk-OUKBMQ2O.js.map} +0 -0
- /package/dist/{chunk-GPFZFCRN.js.map → chunk-PP7R754B.js.map} +0 -0
- /package/dist/{chunk-GAJK3DQ5.js.map → chunk-PRRUK2ZH.js.map} +0 -0
- /package/dist/{chunk-TBCDF556.js.map → chunk-PW3GKWH3.js.map} +0 -0
- /package/dist/{chunk-KI64TTBL.js.map → chunk-R2Q6FTOX.js.map} +0 -0
- /package/dist/{chunk-KJXCD7VR.js.map → chunk-RCPKBW7K.js.map} +0 -0
- /package/dist/{chunk-INUBU6I5.js.map → chunk-RH3XLQG4.js.map} +0 -0
- /package/dist/{chunk-JBPKEARU.js.map → chunk-RKNJBZ55.js.map} +0 -0
- /package/dist/{chunk-NSTNGMSS.js.map → chunk-U4HZEW2B.js.map} +0 -0
- /package/dist/{chunk-XOHYGIWF.js.map → chunk-UHQE37NI.js.map} +0 -0
- /package/dist/{chunk-D3UXGE5D.js.map → chunk-UMZUKOJU.js.map} +0 -0
- /package/dist/{chunk-S4Z2OBTX.js.map → chunk-UVYL5G7A.js.map} +0 -0
- /package/dist/{chunk-4XLTUR6R.js.map → chunk-VKDBDJIX.js.map} +0 -0
- /package/dist/{chunk-UW4RYPWC.js.map → chunk-VSMSYDIH.js.map} +0 -0
- /package/dist/{chunk-LJABGXC4.js.map → chunk-W5IYHQA5.js.map} +0 -0
- /package/dist/{chunk-7UOXCCCI.js.map → chunk-WKWY7YF3.js.map} +0 -0
- /package/dist/{chunk-YWPLOJ7T.js.map → chunk-Y2YM4C75.js.map} +0 -0
- /package/dist/{chunk-NUF7VSEX.js.map → chunk-YZ5VC2JJ.js.map} +0 -0
- /package/dist/{chunk-EWKVYCFP.js.map → chunk-ZBIEX4XN.js.map} +0 -0
- /package/dist/{chunk-GBEOUCBB.js.map → chunk-ZMVW2HJJ.js.map} +0 -0
- /package/dist/{chunk-RPIS5FQ7.js.map → chunk-ZXROBS53.js.map} +0 -0
- /package/dist/{graph-edge-decay-A4SDFMMS.js.map → extraction-span.js.map} +0 -0
|
@@ -0,0 +1,153 @@
|
|
|
1
|
+
import assert from "node:assert/strict";
|
|
2
|
+
import test from "node:test";
|
|
3
|
+
|
|
4
|
+
import { readVaultJournal, stripRemnicOwnedRegions } from "./journal-vault-read.js";
|
|
5
|
+
|
|
6
|
+
const START = "<!-- remnic:timeline:start -->";
|
|
7
|
+
const END = "<!-- remnic:timeline:end -->";
|
|
8
|
+
|
|
9
|
+
test("missing file is exists false, not an error", () => {
|
|
10
|
+
assert.deepEqual(readVaultJournal({ fileText: null, journalSection: "Journal" }), {
|
|
11
|
+
ok: true,
|
|
12
|
+
exists: false,
|
|
13
|
+
reason: "missing_file",
|
|
14
|
+
});
|
|
15
|
+
assert.deepEqual(readVaultJournal({ fileText: undefined, journalSection: "Journal" }), {
|
|
16
|
+
ok: true,
|
|
17
|
+
exists: false,
|
|
18
|
+
reason: "missing_file",
|
|
19
|
+
});
|
|
20
|
+
});
|
|
21
|
+
|
|
22
|
+
test("missing heading is exists false, not an error", () => {
|
|
23
|
+
const fileText = ["---", "date: 2026-08-17", "---", "", "## Timeline", "cards", ""].join("\n");
|
|
24
|
+
assert.deepEqual(readVaultJournal({ fileText, journalSection: "Journal" }), {
|
|
25
|
+
ok: true,
|
|
26
|
+
exists: false,
|
|
27
|
+
reason: "missing_heading",
|
|
28
|
+
});
|
|
29
|
+
});
|
|
30
|
+
|
|
31
|
+
test("empty day returns exists true with empty text", () => {
|
|
32
|
+
const fileText = ["## Journal", "## Next", ""].join("\n");
|
|
33
|
+
const result = readVaultJournal({ fileText, journalSection: "Journal" });
|
|
34
|
+
assert.deepEqual(result, {
|
|
35
|
+
ok: true,
|
|
36
|
+
exists: true,
|
|
37
|
+
text: "",
|
|
38
|
+
heading: "Journal",
|
|
39
|
+
warnings: [],
|
|
40
|
+
});
|
|
41
|
+
});
|
|
42
|
+
|
|
43
|
+
test("duplicate heading refuses with line numbers", () => {
|
|
44
|
+
const fileText = ["# Day", "## Journal", "first", "## Notes", "## Journal", "second", ""].join("\n");
|
|
45
|
+
const result = readVaultJournal({ fileText, journalSection: "Journal" });
|
|
46
|
+
assert.deepEqual(result, { ok: false, reason: "duplicate_heading", lines: [2, 5] });
|
|
47
|
+
});
|
|
48
|
+
|
|
49
|
+
test("strips remnic marker regions and publish section headings", () => {
|
|
50
|
+
const fileText = [
|
|
51
|
+
"---",
|
|
52
|
+
"date: 2026-08-17",
|
|
53
|
+
"---",
|
|
54
|
+
"",
|
|
55
|
+
"## Journal",
|
|
56
|
+
"I walked the dog.",
|
|
57
|
+
START,
|
|
58
|
+
"- 09:00 standup",
|
|
59
|
+
END,
|
|
60
|
+
"Still thinking about the meeting.",
|
|
61
|
+
"### Timeline",
|
|
62
|
+
"nested publish",
|
|
63
|
+
"### Notes",
|
|
64
|
+
"more user text",
|
|
65
|
+
"",
|
|
66
|
+
"## Timeline",
|
|
67
|
+
START,
|
|
68
|
+
"sibling publish",
|
|
69
|
+
END,
|
|
70
|
+
"",
|
|
71
|
+
].join("\n");
|
|
72
|
+
|
|
73
|
+
const result = readVaultJournal({
|
|
74
|
+
fileText,
|
|
75
|
+
journalSection: "Journal",
|
|
76
|
+
publishSectionNames: ["Timeline"],
|
|
77
|
+
});
|
|
78
|
+
assert.equal(result.ok, true);
|
|
79
|
+
if (!result.ok || !result.exists) {
|
|
80
|
+
assert.fail("expected journal text");
|
|
81
|
+
return;
|
|
82
|
+
}
|
|
83
|
+
assert.equal(result.heading, "Journal");
|
|
84
|
+
assert.equal(result.warnings.length, 0);
|
|
85
|
+
assert.match(result.text, /I walked the dog\./);
|
|
86
|
+
assert.match(result.text, /Still thinking about the meeting\./);
|
|
87
|
+
assert.match(result.text, /more user text/);
|
|
88
|
+
assert.doesNotMatch(result.text, /standup/);
|
|
89
|
+
assert.doesNotMatch(result.text, /nested publish/);
|
|
90
|
+
assert.doesNotMatch(result.text, /sibling publish/);
|
|
91
|
+
assert.doesNotMatch(result.text, /remnic:/);
|
|
92
|
+
assert.doesNotMatch(result.text, /### Timeline/);
|
|
93
|
+
});
|
|
94
|
+
|
|
95
|
+
test("non-ASCII journal heading matches exactly", () => {
|
|
96
|
+
const fileText = ["## Tagebuch 📝", "Heute ruhig.", "## Andere", ""].join("\n");
|
|
97
|
+
const result = readVaultJournal({ fileText, journalSection: "Tagebuch 📝" });
|
|
98
|
+
assert.deepEqual(result, {
|
|
99
|
+
ok: true,
|
|
100
|
+
exists: true,
|
|
101
|
+
text: "Heute ruhig.",
|
|
102
|
+
heading: "Tagebuch 📝",
|
|
103
|
+
warnings: [],
|
|
104
|
+
});
|
|
105
|
+
});
|
|
106
|
+
|
|
107
|
+
test("unclosed start marker strips to end of section and warns", () => {
|
|
108
|
+
const fileText = ["## Journal", "keep me", START, "leaked publish", "## Next", "outside", ""].join(
|
|
109
|
+
"\n",
|
|
110
|
+
);
|
|
111
|
+
const result = readVaultJournal({ fileText, journalSection: "Journal" });
|
|
112
|
+
assert.equal(result.ok, true);
|
|
113
|
+
if (!result.ok || !result.exists) {
|
|
114
|
+
assert.fail("expected journal text");
|
|
115
|
+
return;
|
|
116
|
+
}
|
|
117
|
+
assert.equal(result.text, "keep me");
|
|
118
|
+
assert.equal(result.warnings.length, 1);
|
|
119
|
+
assert.match(result.warnings[0]!, /timeline/);
|
|
120
|
+
});
|
|
121
|
+
|
|
122
|
+
test("marker pair split across the section boundary does not leak", () => {
|
|
123
|
+
const fileText = [
|
|
124
|
+
START,
|
|
125
|
+
"before journal",
|
|
126
|
+
"## Journal",
|
|
127
|
+
"leaked into journal",
|
|
128
|
+
END,
|
|
129
|
+
"user after",
|
|
130
|
+
"## Next",
|
|
131
|
+
"",
|
|
132
|
+
].join("\n");
|
|
133
|
+
const result = readVaultJournal({ fileText, journalSection: "Journal" });
|
|
134
|
+
assert.equal(result.ok, true);
|
|
135
|
+
if (!result.ok || !result.exists) {
|
|
136
|
+
assert.fail("expected journal text");
|
|
137
|
+
return;
|
|
138
|
+
}
|
|
139
|
+
assert.equal(result.text, "user after");
|
|
140
|
+
assert.doesNotMatch(result.text, /leaked/);
|
|
141
|
+
});
|
|
142
|
+
|
|
143
|
+
test("stripRemnicOwnedRegions returns only user text after injected regions", () => {
|
|
144
|
+
const user = "Day was quiet.\nSecond line.\n";
|
|
145
|
+
const names = ["timeline", "weekly", "digest"] as const;
|
|
146
|
+
for (const name of names) {
|
|
147
|
+
const region = `<!-- remnic:${name}:start -->\nOWNED ${name}\n<!-- remnic:${name}:end -->\n`;
|
|
148
|
+
const mixed = `${region}${user}${region}${user}`;
|
|
149
|
+
const stripped = stripRemnicOwnedRegions(mixed);
|
|
150
|
+
assert.equal(stripped.text, `${user}${user}`);
|
|
151
|
+
assert.deepEqual(stripped.warnings, []);
|
|
152
|
+
}
|
|
153
|
+
});
|
|
@@ -0,0 +1,214 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Read-only vault daily-note journal section (issue #1987 first slice).
|
|
3
|
+
*
|
|
4
|
+
* Pure: file text in, journal body out. Never writes. Strips Remnic-owned
|
|
5
|
+
* marker regions and publisher-owned headings so published timeline text
|
|
6
|
+
* cannot re-enter the journal. Path templates stay in vault-publish.
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
export interface ReadVaultJournalInput {
|
|
10
|
+
fileText: string | null | undefined;
|
|
11
|
+
journalSection: string;
|
|
12
|
+
publishSectionNames?: readonly string[];
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export type ReadVaultJournalResult =
|
|
16
|
+
| { ok: true; exists: false; reason: "missing_file" | "missing_heading" }
|
|
17
|
+
| { ok: true; exists: true; text: string; heading: string; warnings: readonly string[] }
|
|
18
|
+
| { ok: false; reason: "duplicate_heading"; lines: readonly number[] };
|
|
19
|
+
|
|
20
|
+
const REMNIC_OPEN = "<!-- remnic:";
|
|
21
|
+
|
|
22
|
+
export function readVaultJournal(input: ReadVaultJournalInput): ReadVaultJournalResult {
|
|
23
|
+
if (input.fileText === null || input.fileText === undefined) {
|
|
24
|
+
return { ok: true, exists: false, reason: "missing_file" };
|
|
25
|
+
}
|
|
26
|
+
if (input.journalSection.length === 0) {
|
|
27
|
+
return { ok: true, exists: false, reason: "missing_heading" };
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
const lines = splitFileLines(input.fileText);
|
|
31
|
+
const hits: number[] = [];
|
|
32
|
+
const levels: number[] = [];
|
|
33
|
+
for (let i = 0; i < lines.length; i++) {
|
|
34
|
+
const heading = parseAtxHeading(lines[i]!);
|
|
35
|
+
if (heading && heading.text === input.journalSection) {
|
|
36
|
+
hits.push(i + 1);
|
|
37
|
+
levels.push(heading.level);
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
if (hits.length === 0) return { ok: true, exists: false, reason: "missing_heading" };
|
|
41
|
+
if (hits.length > 1) return { ok: false, reason: "duplicate_heading", lines: hits };
|
|
42
|
+
|
|
43
|
+
const headingIndex = hits[0]! - 1;
|
|
44
|
+
const level = levels[0]!;
|
|
45
|
+
let endIndex = lines.length;
|
|
46
|
+
for (let i = headingIndex + 1; i < lines.length; i++) {
|
|
47
|
+
const heading = parseAtxHeading(lines[i]!);
|
|
48
|
+
if (heading && heading.level <= level) {
|
|
49
|
+
endIndex = i;
|
|
50
|
+
break;
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
const body = lines.slice(headingIndex + 1, endIndex).join("\n");
|
|
55
|
+
const stripped = stripRemnicOwnedRegions(body, input.publishSectionNames ?? []);
|
|
56
|
+
return {
|
|
57
|
+
ok: true,
|
|
58
|
+
exists: true,
|
|
59
|
+
text: trimNewlines(stripped.text),
|
|
60
|
+
heading: input.journalSection,
|
|
61
|
+
warnings: stripped.warnings,
|
|
62
|
+
};
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
/** Remove Remnic marker pairs and publisher-owned heading sections. */
|
|
66
|
+
export function stripRemnicOwnedRegions(
|
|
67
|
+
text: string,
|
|
68
|
+
publishSectionNames: readonly string[] = [],
|
|
69
|
+
): { text: string; warnings: readonly string[] } {
|
|
70
|
+
const marked = cutIntervals(text, remnicIntervals(text));
|
|
71
|
+
return {
|
|
72
|
+
text: stripPublishHeadings(marked.text, publishSectionNames),
|
|
73
|
+
warnings: marked.warnings,
|
|
74
|
+
};
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
function splitFileLines(fileText: string): string[] {
|
|
78
|
+
const lines = fileText.split("\n");
|
|
79
|
+
for (let i = 0; i < lines.length; i++) {
|
|
80
|
+
const line = lines[i]!;
|
|
81
|
+
if (line.endsWith("\r")) lines[i] = line.slice(0, -1);
|
|
82
|
+
}
|
|
83
|
+
return lines;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
function parseAtxHeading(line: string): { level: number; text: string } | null {
|
|
87
|
+
let level = 0;
|
|
88
|
+
while (level < line.length && level < 6 && line[level] === "#") level++;
|
|
89
|
+
if (level === 0) return null;
|
|
90
|
+
const after = line[level];
|
|
91
|
+
if (after !== " " && after !== "\t") return null;
|
|
92
|
+
|
|
93
|
+
let start = level + 1;
|
|
94
|
+
let end = line.length;
|
|
95
|
+
while (start < end && (line[start] === " " || line[start] === "\t")) start++;
|
|
96
|
+
while (end > start && (line[end - 1] === " " || line[end - 1] === "\t")) end--;
|
|
97
|
+
|
|
98
|
+
let hashEnd = end;
|
|
99
|
+
while (hashEnd > start && line[hashEnd - 1] === "#") hashEnd--;
|
|
100
|
+
if (hashEnd < end && hashEnd > start && (line[hashEnd - 1] === " " || line[hashEnd - 1] === "\t")) {
|
|
101
|
+
end = hashEnd;
|
|
102
|
+
while (end > start && (line[end - 1] === " " || line[end - 1] === "\t")) end--;
|
|
103
|
+
}
|
|
104
|
+
return { level, text: line.slice(start, end) };
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
function remnicIntervals(text: string): {
|
|
108
|
+
intervals: Array<{ start: number; end: number }>;
|
|
109
|
+
warnings: string[];
|
|
110
|
+
} {
|
|
111
|
+
const starts: Array<{ name: string; start: number; after: number }> = [];
|
|
112
|
+
const ends: Array<{ name: string; start: number; end: number }> = [];
|
|
113
|
+
|
|
114
|
+
let from = 0;
|
|
115
|
+
while (from < text.length) {
|
|
116
|
+
const open = text.indexOf(REMNIC_OPEN, from);
|
|
117
|
+
if (open === -1) break;
|
|
118
|
+
const afterOpen = open + REMNIC_OPEN.length;
|
|
119
|
+
const commentEnd = text.indexOf("-->", afterOpen);
|
|
120
|
+
if (commentEnd === -1) break;
|
|
121
|
+
const inner = text.slice(afterOpen, commentEnd).trim();
|
|
122
|
+
if (inner.endsWith(":start")) {
|
|
123
|
+
const name = inner.slice(0, -":start".length);
|
|
124
|
+
if (name.length > 0) starts.push({ name, start: open, after: commentEnd + 3 });
|
|
125
|
+
} else if (inner.endsWith(":end")) {
|
|
126
|
+
const name = inner.slice(0, -":end".length);
|
|
127
|
+
if (name.length > 0) ends.push({ name, start: open, end: commentEnd + 3 });
|
|
128
|
+
}
|
|
129
|
+
from = commentEnd + 3;
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
const usedEnds = new Set<number>();
|
|
133
|
+
const intervals: Array<{ start: number; end: number }> = [];
|
|
134
|
+
const warnings: string[] = [];
|
|
135
|
+
|
|
136
|
+
for (const start of starts) {
|
|
137
|
+
const match = ends.find((end) => end.name === start.name && end.start >= start.after && !usedEnds.has(end.start));
|
|
138
|
+
if (match) {
|
|
139
|
+
usedEnds.add(match.start);
|
|
140
|
+
intervals.push({ start: start.start, end: eatTrailingNewline(text, match.end) });
|
|
141
|
+
} else {
|
|
142
|
+
intervals.push({ start: start.start, end: text.length });
|
|
143
|
+
warnings.push(`unclosed remnic region "${start.name}"`);
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
for (const end of ends) {
|
|
148
|
+
if (usedEnds.has(end.start)) continue;
|
|
149
|
+
const startBefore = starts.some((start) => start.start < end.start);
|
|
150
|
+
if (!startBefore) intervals.push({ start: 0, end: eatTrailingNewline(text, end.end) });
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
return { intervals: mergeIntervals(intervals), warnings };
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
function eatTrailingNewline(text: string, index: number): number {
|
|
157
|
+
if (text[index] === "\n") return index + 1;
|
|
158
|
+
if (text[index] === "\r" && text[index + 1] === "\n") return index + 2;
|
|
159
|
+
if (text[index] === "\r") return index + 1;
|
|
160
|
+
return index;
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
function mergeIntervals(items: Array<{ start: number; end: number }>): Array<{ start: number; end: number }> {
|
|
164
|
+
if (items.length === 0) return [];
|
|
165
|
+
const sorted = items.slice().sort((a, b) => a.start - b.start || a.end - b.end);
|
|
166
|
+
const out = [{ start: sorted[0]!.start, end: sorted[0]!.end }];
|
|
167
|
+
for (let i = 1; i < sorted.length; i++) {
|
|
168
|
+
const cur = sorted[i]!;
|
|
169
|
+
const last = out[out.length - 1]!;
|
|
170
|
+
if (cur.start <= last.end) last.end = Math.max(last.end, cur.end);
|
|
171
|
+
else out.push({ start: cur.start, end: cur.end });
|
|
172
|
+
}
|
|
173
|
+
return out;
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
function cutIntervals(
|
|
177
|
+
text: string,
|
|
178
|
+
found: { intervals: Array<{ start: number; end: number }>; warnings: string[] },
|
|
179
|
+
): { text: string; warnings: string[] } {
|
|
180
|
+
let out = "";
|
|
181
|
+
let pos = 0;
|
|
182
|
+
for (const interval of found.intervals) {
|
|
183
|
+
if (interval.start > pos) out += text.slice(pos, interval.start);
|
|
184
|
+
pos = Math.max(pos, interval.end);
|
|
185
|
+
}
|
|
186
|
+
if (pos < text.length) out += text.slice(pos);
|
|
187
|
+
return { text: out, warnings: found.warnings };
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
function stripPublishHeadings(text: string, names: readonly string[]): string {
|
|
191
|
+
if (names.length === 0 || text.length === 0) return text;
|
|
192
|
+
const lines = text.split("\n");
|
|
193
|
+
const keep = lines.map(() => true);
|
|
194
|
+
for (let i = 0; i < lines.length; i++) {
|
|
195
|
+
if (!keep[i]) continue;
|
|
196
|
+
const heading = parseAtxHeading(lines[i]!);
|
|
197
|
+
if (!heading || !names.includes(heading.text)) continue;
|
|
198
|
+
keep[i] = false;
|
|
199
|
+
for (let j = i + 1; j < lines.length; j++) {
|
|
200
|
+
const next = parseAtxHeading(lines[j]!);
|
|
201
|
+
if (next && next.level <= heading.level) break;
|
|
202
|
+
keep[j] = false;
|
|
203
|
+
}
|
|
204
|
+
}
|
|
205
|
+
return lines.filter((_, i) => keep[i]).join("\n");
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
function trimNewlines(text: string): string {
|
|
209
|
+
let start = 0;
|
|
210
|
+
let end = text.length;
|
|
211
|
+
while (start < end && (text[start] === "\n" || text[start] === "\r")) start++;
|
|
212
|
+
while (end > start && (text[end - 1] === "\n" || text[end - 1] === "\r")) end--;
|
|
213
|
+
return text.slice(start, end);
|
|
214
|
+
}
|
package/src/activity/journal.ts
CHANGED
|
@@ -87,7 +87,7 @@ function renderJournalSeed(date: string, cards: readonly JournalSeedCard[]): str
|
|
|
87
87
|
}
|
|
88
88
|
|
|
89
89
|
/** The only journal write. Stats first; existing files are a no-op unless force. */
|
|
90
|
-
function writeJournalFile(filePath: string, content: string, force: boolean): boolean {
|
|
90
|
+
export function writeJournalFile(filePath: string, content: string, force: boolean): boolean {
|
|
91
91
|
let exists = false;
|
|
92
92
|
try {
|
|
93
93
|
statSync(filePath);
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
import assert from "node:assert/strict";
|
|
2
|
+
import test from "node:test";
|
|
3
|
+
|
|
4
|
+
import { batchObservations } from "./analysis-batch.js";
|
|
5
|
+
import type { TimelineBatchObservation } from "./analysis-batch.js";
|
|
6
|
+
|
|
7
|
+
function obs(id: number, capturedAtUtc: string): TimelineBatchObservation {
|
|
8
|
+
return { id, capturedAtUtc };
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
test("empty observations return no batches", () => {
|
|
12
|
+
assert.deepEqual(batchObservations([], { maxBatch: 3, overlap: 1 }), []);
|
|
13
|
+
});
|
|
14
|
+
|
|
15
|
+
test("maxBatch 0 returns no batches", () => {
|
|
16
|
+
assert.deepEqual(
|
|
17
|
+
batchObservations([obs(1, "2026-08-17T10:00:00.000Z")], { maxBatch: 0, overlap: 0 }),
|
|
18
|
+
[],
|
|
19
|
+
);
|
|
20
|
+
});
|
|
21
|
+
|
|
22
|
+
test("overlap shares the trailing window with the next batch", () => {
|
|
23
|
+
const items = [1, 2, 3, 4, 5].map((id) => obs(id, `2026-08-17T10:0${id}:00.000Z`));
|
|
24
|
+
const batches = batchObservations(items, { maxBatch: 3, overlap: 1 });
|
|
25
|
+
assert.deepEqual(
|
|
26
|
+
batches.map((batch) => batch.map((item) => item.id)),
|
|
27
|
+
[
|
|
28
|
+
[1, 2, 3],
|
|
29
|
+
[3, 4, 5],
|
|
30
|
+
],
|
|
31
|
+
);
|
|
32
|
+
});
|
|
33
|
+
|
|
34
|
+
test("order is capturedAtUtc then id, not input order", () => {
|
|
35
|
+
const items = [
|
|
36
|
+
obs(3, "2026-08-17T12:00:00.000Z"),
|
|
37
|
+
obs(2, "2026-08-17T10:00:00.000Z"),
|
|
38
|
+
obs(1, "2026-08-17T10:00:00.000Z"),
|
|
39
|
+
];
|
|
40
|
+
const batches = batchObservations(items, { maxBatch: 3, overlap: 0 });
|
|
41
|
+
assert.deepEqual(
|
|
42
|
+
batches.map((batch) => batch.map((item) => item.id)),
|
|
43
|
+
[[1, 2, 3]],
|
|
44
|
+
);
|
|
45
|
+
});
|
|
46
|
+
|
|
47
|
+
test("maxBatch below 1 is rejected except 0", () => {
|
|
48
|
+
assert.throws(() => batchObservations([], { maxBatch: -1, overlap: 0 }), RangeError);
|
|
49
|
+
assert.throws(() => batchObservations([], { maxBatch: 0.5, overlap: 0 }), RangeError);
|
|
50
|
+
});
|
|
51
|
+
|
|
52
|
+
test("negative overlap and overlap covering the batch are rejected", () => {
|
|
53
|
+
assert.throws(() => batchObservations([], { maxBatch: 3, overlap: -1 }), RangeError);
|
|
54
|
+
assert.throws(() => batchObservations([], { maxBatch: 3, overlap: 3 }), RangeError);
|
|
55
|
+
assert.throws(() => batchObservations([], { maxBatch: 3, overlap: 4 }), RangeError);
|
|
56
|
+
});
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Bounded overlapping observation batches for timeline analysis (issue #2050).
|
|
3
|
+
*/
|
|
4
|
+
import type { TimelineObservation } from "./types.js";
|
|
5
|
+
|
|
6
|
+
export type TimelineBatchObservation = Pick<TimelineObservation, "id" | "capturedAtUtc">;
|
|
7
|
+
|
|
8
|
+
export interface BatchObservationsOptions {
|
|
9
|
+
maxBatch: number;
|
|
10
|
+
overlap: number;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
function compareObservations(a: TimelineBatchObservation, b: TimelineBatchObservation): number {
|
|
14
|
+
const time = Date.parse(a.capturedAtUtc) - Date.parse(b.capturedAtUtc);
|
|
15
|
+
if (time !== 0) return time;
|
|
16
|
+
return a.id - b.id;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
/** Split observations into overlapping windows. Order is capturedAtUtc, then id. */
|
|
20
|
+
export function batchObservations<T extends TimelineBatchObservation>(
|
|
21
|
+
observations: readonly T[],
|
|
22
|
+
options: BatchObservationsOptions,
|
|
23
|
+
): T[][] {
|
|
24
|
+
const { maxBatch, overlap } = options;
|
|
25
|
+
if (!Number.isInteger(overlap) || overlap < 0) {
|
|
26
|
+
throw new RangeError("overlap must be a non-negative integer");
|
|
27
|
+
}
|
|
28
|
+
if (maxBatch === 0) return [];
|
|
29
|
+
if (!Number.isInteger(maxBatch) || maxBatch < 1) {
|
|
30
|
+
throw new RangeError("maxBatch must be 0 or a positive integer");
|
|
31
|
+
}
|
|
32
|
+
if (overlap >= maxBatch) {
|
|
33
|
+
throw new RangeError("overlap must be less than maxBatch");
|
|
34
|
+
}
|
|
35
|
+
if (observations.length === 0) return [];
|
|
36
|
+
|
|
37
|
+
const ordered = [...observations].sort(compareObservations);
|
|
38
|
+
const step = maxBatch - overlap;
|
|
39
|
+
const batches: T[][] = [];
|
|
40
|
+
for (let start = 0; start < ordered.length; start += step) {
|
|
41
|
+
batches.push(ordered.slice(start, start + maxBatch));
|
|
42
|
+
if (start + maxBatch >= ordered.length) break;
|
|
43
|
+
}
|
|
44
|
+
return batches;
|
|
45
|
+
}
|