@remnic/core 10.0.48 → 10.0.50
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.js +73 -70
- package/dist/access-audit.js +2 -2
- package/dist/access-authorization-probe.js +74 -71
- package/dist/access-boundary.js +73 -70
- package/dist/access-cli.js +170 -167
- package/dist/access-cli.js.map +1 -1
- package/dist/access-extraction-force-flush.js +73 -70
- package/dist/access-http-briefing.d.ts +2 -0
- package/dist/access-http-briefing.d.ts.map +1 -1
- package/dist/access-http-briefing.js +74 -71
- package/dist/access-http-query.js +75 -72
- package/dist/access-http.d.ts.map +1 -1
- package/dist/access-http.js +96 -93
- package/dist/access-identity-continuity-surface.js +73 -70
- package/dist/access-lcm-surface.js +73 -70
- package/dist/access-mcp.js +85 -82
- package/dist/access-namespace-preflight.js +73 -70
- package/dist/access-observe-write-category.d.ts +1 -1
- package/dist/access-observe-write-category.d.ts.map +1 -1
- package/dist/access-observe-write-category.js +15 -14
- package/dist/access-observe-write-surface.d.ts.map +1 -1
- package/dist/access-observe-write-surface.js +73 -70
- package/dist/access-offline-manifest.js +9 -8
- package/dist/access-operations-batch.js +76 -73
- package/dist/access-operations.js +83 -80
- package/dist/access-recall-concurrency.js +73 -70
- package/dist/access-recall-response.js +73 -70
- package/dist/access-recall-surface.js +73 -70
- package/dist/access-schema.js +11 -10
- package/dist/access-service.d.ts +5 -0
- package/dist/access-service.d.ts.map +1 -1
- package/dist/access-service.js +73 -70
- package/dist/access-surface-catalog.js +75 -72
- package/dist/access-surface-catalog.js.map +1 -1
- package/dist/active-recall.js +32 -30
- package/dist/active-recall.js.map +1 -1
- package/dist/adapters/index.js +4 -4
- package/dist/adapters/registry.js +2 -2
- package/dist/{auto-sync-BNCBMU6H.js → auto-sync-UQQ6YJSB.js} +3 -3
- package/dist/briefing-deadline.d.ts +7 -0
- package/dist/briefing-deadline.d.ts.map +1 -0
- package/dist/briefing-deadline.js +13 -0
- package/dist/briefing-window.d.ts +12 -4
- package/dist/briefing-window.d.ts.map +1 -1
- package/dist/briefing-window.js +3 -1
- package/dist/briefing.d.ts +2 -0
- package/dist/briefing.d.ts.map +1 -1
- package/dist/briefing.js +22 -20
- package/dist/buffer.js +6 -6
- package/dist/calibration.js +2 -2
- package/dist/{capsule-crypto-GWVG7LGC.js → capsule-crypto-7FJQINUR.js} +2 -2
- package/dist/{capsule-merge-RNQUP2VL.js → capsule-merge-D4BTIJGK.js} +3 -3
- package/dist/capture-write-lock-scope.d.ts +7 -0
- package/dist/capture-write-lock-scope.d.ts.map +1 -0
- package/dist/capture-write-lock-scope.js +10 -0
- package/dist/causal-behavior.js +2 -2
- package/dist/causal-chain.js +2 -2
- package/dist/causal-consolidation.js +23 -22
- package/dist/causal-consolidation.js.map +1 -1
- package/dist/causal-retrieval.js +2 -2
- package/dist/causal-trajectory-graph.js +3 -3
- package/dist/causal-trajectory.js +1 -1
- package/dist/{chunk-FAKRTS3Y.js → chunk-2AKIVSHP.js} +2 -2
- package/dist/{chunk-EIYJK7OD.js → chunk-2M36PR7R.js} +2 -2
- package/dist/{chunk-OWHERGF2.js → chunk-2NLLXCJG.js} +2 -2
- package/dist/{chunk-6P6MZDBL.js → chunk-3NL4ALUP.js} +2 -2
- package/dist/{chunk-6CC2YCKG.js → chunk-43TUWQNR.js} +3 -3
- package/dist/{chunk-H7BZBSP6.js → chunk-52II7PB6.js} +4 -4
- package/dist/{chunk-2NLCDNGA.js → chunk-564YP3DW.js} +2 -2
- package/dist/{chunk-5KGYR5VN.js → chunk-5DVOBBTV.js} +2 -2
- package/dist/{chunk-RT2GN6KQ.js → chunk-5FUDSK6Z.js} +2 -2
- package/dist/{chunk-B4XSXFKI.js → chunk-5G53JSRO.js} +2 -2
- package/dist/{chunk-BGK3SFCC.js → chunk-5X2H3X4J.js} +2 -2
- package/dist/{chunk-OXSTKCYK.js → chunk-6AEKSOWV.js} +33 -5
- package/dist/chunk-6AEKSOWV.js.map +1 -0
- package/dist/{chunk-4J7BNL7B.js → chunk-6ZQX3R66.js} +2 -2
- package/dist/{chunk-JNLYHJG7.js → chunk-74AY3S7U.js} +4 -4
- package/dist/{chunk-RUAIO5UA.js → chunk-7J6A5MV2.js} +2 -2
- package/dist/{chunk-A45QXZMU.js → chunk-7SAE6T36.js} +2 -2
- package/dist/chunk-7VLBZKNB.js +26 -0
- package/dist/chunk-7VLBZKNB.js.map +1 -0
- package/dist/{chunk-QE2XQM3S.js → chunk-7YVER7ZO.js} +2 -2
- package/dist/{chunk-IJH6METJ.js → chunk-A3UFV4K3.js} +4 -4
- package/dist/{chunk-LWRIGKFM.js → chunk-A5L5KGDI.js} +19 -1
- package/dist/chunk-A5L5KGDI.js.map +1 -0
- package/dist/{chunk-LXN737FX.js → chunk-AAANTXCQ.js} +2 -2
- package/dist/{chunk-I3IPXGFQ.js → chunk-AJJPXUE6.js} +4 -4
- package/dist/{chunk-2DPJFJFH.js → chunk-BNJAVRNJ.js} +11 -11
- package/dist/{chunk-YL564QS2.js → chunk-CCL2OXFO.js} +3 -3
- package/dist/{chunk-CHSRA76M.js → chunk-CRN3EPE2.js} +4 -3
- package/dist/chunk-CRN3EPE2.js.map +1 -0
- package/dist/{chunk-ZX5N5D6M.js → chunk-DAOMWGO2.js} +4 -4
- package/dist/{chunk-YUE7J2HZ.js → chunk-DMKTOCC7.js} +2 -2
- package/dist/chunk-DMKTOCC7.js.map +1 -0
- package/dist/{chunk-FRXS6SC7.js → chunk-EKUI7I2X.js} +20 -4
- package/dist/chunk-EKUI7I2X.js.map +1 -0
- package/dist/{chunk-PKPOO6X5.js → chunk-EKYWZYZJ.js} +25 -17
- package/dist/chunk-EKYWZYZJ.js.map +1 -0
- package/dist/{chunk-23ZMQONL.js → chunk-ERMT5WQM.js} +2 -2
- package/dist/{chunk-VTCNVGM6.js → chunk-EWBXMGXR.js} +2 -2
- package/dist/{chunk-YNDLCWXS.js → chunk-EZ25VE3G.js} +4 -4
- package/dist/{chunk-XTN54NML.js → chunk-G3T6YMBO.js} +4 -4
- package/dist/{chunk-2JNBPOZ2.js → chunk-GDYLZ6FR.js} +3 -3
- package/dist/{chunk-DZPTR2XQ.js → chunk-GFAEKSFR.js} +2 -2
- package/dist/{chunk-L46MG2GE.js → chunk-GQIYL7OD.js} +400 -301
- package/dist/chunk-GQIYL7OD.js.map +1 -0
- package/dist/{chunk-M7LA4QBY.js → chunk-GXVXBYRA.js} +3 -3
- package/dist/{chunk-UXAF4RQM.js → chunk-HKOMHP6D.js} +27 -15
- package/dist/chunk-HKOMHP6D.js.map +1 -0
- package/dist/{chunk-JGMJZBFV.js → chunk-HPNXS6W6.js} +2 -2
- package/dist/{chunk-BFPF5N3H.js → chunk-HXFIILGF.js} +4 -4
- package/dist/{chunk-YBKQAVPZ.js → chunk-IMKOJVD4.js} +101 -81
- package/dist/chunk-IMKOJVD4.js.map +1 -0
- package/dist/{chunk-CMNL3QSL.js → chunk-IN6U2BS5.js} +2 -2
- package/dist/{chunk-BWRX322G.js → chunk-IUYXCTA7.js} +2 -2
- package/dist/{chunk-LU57EEJW.js → chunk-JBWD3J5U.js} +3 -3
- package/dist/chunk-JNA7LFMQ.js +41 -0
- package/dist/chunk-JNA7LFMQ.js.map +1 -0
- package/dist/{chunk-2V664H6O.js → chunk-JS44PBCM.js} +5 -5
- package/dist/{chunk-XKTHJHSQ.js → chunk-JZCBMKR5.js} +3 -3
- package/dist/{chunk-MXPVNDGO.js → chunk-K5QQXVG6.js} +12 -12
- package/dist/{chunk-BUKK5SWA.js → chunk-KQAFEZQX.js} +2 -2
- package/dist/{chunk-WUBGMV3G.js → chunk-KRAMCBIW.js} +9 -9
- package/dist/{chunk-NDMPRT3Z.js → chunk-KW5YZELC.js} +2 -2
- package/dist/{chunk-2RQR4TXV.js → chunk-LIBVANOF.js} +2 -2
- package/dist/{chunk-M7L5A2AT.js → chunk-LMMBV7QC.js} +4 -4
- package/dist/{chunk-PJBYVJKO.js → chunk-LXXIV7Y2.js} +2 -2
- package/dist/{chunk-MZCTS3ZG.js → chunk-M2DOJBH7.js} +2 -2
- package/dist/{chunk-WOPZ2P5Z.js → chunk-M4HRYLOA.js} +2 -2
- package/dist/{chunk-JCYEDMOY.js → chunk-M6TP6UYD.js} +3 -3
- package/dist/{chunk-KAUCVTAW.js → chunk-MLNXATFG.js} +2 -2
- package/dist/{chunk-2NX7JTST.js → chunk-MPWOAIIH.js} +2 -2
- package/dist/{chunk-RQLTCHUL.js → chunk-MWURUSRE.js} +13 -13
- package/dist/{chunk-3EGL7XZO.js → chunk-N74WJ7B2.js} +2 -2
- package/dist/{chunk-DRXTMDAG.js → chunk-N7FNCE7F.js} +2 -2
- package/dist/{chunk-6CWAY4R7.js → chunk-NPLM7AFU.js} +2 -2
- package/dist/{chunk-VECRSYQG.js → chunk-NRGQ5HJO.js} +1 -1
- package/dist/{chunk-PPDNG7WC.js → chunk-NSP5PIQH.js} +2 -2
- package/dist/{chunk-6HVLHIAB.js → chunk-P7OVFGMU.js} +2 -2
- package/dist/{chunk-EX27VKU6.js → chunk-PDTNQLPC.js} +2 -2
- package/dist/{chunk-ME5LVISI.js → chunk-PJCVI44H.js} +2 -2
- package/dist/{chunk-LJCHDFLP.js → chunk-Q3PNQYAC.js} +2 -2
- package/dist/{chunk-YFJHFEET.js → chunk-QS765SBL.js} +2 -2
- package/dist/{chunk-FJ264QBP.js → chunk-QTDZ2NYC.js} +11 -11
- package/dist/{chunk-Q6UZREJK.js → chunk-RKRX4C4H.js} +2 -2
- package/dist/{chunk-YFXRLRKP.js → chunk-S4OZKPBS.js} +7 -11
- package/dist/{chunk-YFXRLRKP.js.map → chunk-S4OZKPBS.js.map} +1 -1
- package/dist/{chunk-GLZWOIU3.js → chunk-SJTSWG2Z.js} +3 -3
- package/dist/{chunk-Y57GJDG5.js → chunk-SWRRZNHM.js} +14 -17
- package/dist/chunk-SWRRZNHM.js.map +1 -0
- package/dist/{chunk-CCTLRRV6.js → chunk-TEH5MDX5.js} +55 -34
- package/dist/chunk-TEH5MDX5.js.map +1 -0
- package/dist/{chunk-XM747YJF.js → chunk-TFWJXWL6.js} +2 -2
- package/dist/{chunk-QFQ4UG3I.js → chunk-TXKQBQYW.js} +4 -4
- package/dist/{chunk-BMOJFGVX.js → chunk-UALXWWGN.js} +2 -2
- package/dist/{chunk-B44NFHWI.js → chunk-UNOAVHV4.js} +2 -2
- package/dist/{chunk-TF5NZGPS.js → chunk-UQBOPCDK.js} +2 -2
- package/dist/{chunk-SUKPM6DV.js → chunk-UWMZT2TY.js} +47 -4
- package/dist/chunk-UWMZT2TY.js.map +1 -0
- package/dist/{chunk-5R2IF2KI.js → chunk-UXGXJUZ5.js} +7 -7
- package/dist/{chunk-FWV2SXSL.js → chunk-UZVQFGAY.js} +101 -98
- package/dist/chunk-UZVQFGAY.js.map +1 -0
- package/dist/{chunk-J6B6FK2X.js → chunk-VCOMACR6.js} +53 -53
- package/dist/{chunk-WCGJK4NQ.js → chunk-VKQI47DJ.js} +7 -7
- package/dist/{chunk-MPJDQC4G.js → chunk-WJCXUE2J.js} +2 -2
- package/dist/{chunk-BCDECPVO.js → chunk-WV4UYDP6.js} +2 -2
- package/dist/{chunk-VWJ56T63.js → chunk-WXJC2L66.js} +2 -2
- package/dist/chunk-X2KO4FK5.js +40 -0
- package/dist/chunk-X2KO4FK5.js.map +1 -0
- package/dist/{chunk-UVYI6VIX.js → chunk-X7Y7WX73.js} +1 -1
- package/dist/{chunk-FPANDKMQ.js → chunk-XC3H7KON.js} +2 -2
- package/dist/{chunk-OOOX5C4X.js → chunk-XL2EE5W3.js} +4 -4
- package/dist/{chunk-3DKKZVXS.js → chunk-XTXWR6KQ.js} +26 -11
- package/dist/chunk-XTXWR6KQ.js.map +1 -0
- package/dist/{chunk-3SL52JKD.js → chunk-XVZA6VXC.js} +4 -4
- package/dist/{chunk-CZVNOTLR.js → chunk-XXR4A33N.js} +2 -2
- package/dist/{chunk-2AV2U2SZ.js → chunk-YDIOMDBW.js} +2 -2
- package/dist/{chunk-XTVH2NAW.js → chunk-YECEEJUU.js} +3 -3
- package/dist/{chunk-QR6W3FSW.js → chunk-Z5277JWF.js} +3 -3
- package/dist/{chunk-CTHYR4QK.js → chunk-ZFNXJEUG.js} +2 -2
- package/dist/{chunk-6Q56WDHG.js → chunk-ZLCBLDJO.js} +2 -2
- package/dist/{chunk-VY6BD5KB.js → chunk-ZMO2S5RT.js} +12 -10
- package/dist/chunk-ZMO2S5RT.js.map +1 -0
- package/dist/{chunk-KRACAE73.js → chunk-ZNRTHYYW.js} +6 -6
- package/dist/{chunk-7AIPWNM7.js → chunk-ZSMA67FV.js} +2 -2
- package/dist/cli.js +123 -120
- package/dist/coding/export-okf-codegraph.js +19 -18
- package/dist/coding/export-okf-codegraph.js.map +1 -1
- package/dist/coding/pre-action-gate.js +2 -2
- package/dist/compounding/engine.js +21 -20
- package/dist/config.js +32 -30
- package/dist/connectors/codex-materialize-runner.js +20 -19
- package/dist/connectors/index.js +20 -19
- package/dist/consolidation-provenance-check.js +4 -4
- package/dist/consolidation-undo.js +2 -2
- package/dist/convergence-refresh.js +22 -21
- package/dist/corpus-watermark.js +22 -21
- package/dist/correction/passive-correction-notifications.d.ts +1 -1
- package/dist/correction/passive-correction-notifications.d.ts.map +1 -1
- package/dist/dashboard-runtime.js +2 -2
- package/dist/deep-recall-http-glue.js +12 -11
- package/dist/deep-recall.js +2 -2
- package/dist/embedding-fallback.js +3 -2
- package/dist/embedding-generation-marker.d.ts +16 -0
- package/dist/embedding-generation-marker.d.ts.map +1 -0
- package/dist/embedding-generation-marker.js +19 -0
- package/dist/embedding-generation-marker.js.map +1 -0
- package/dist/embedding-index-storage.d.ts +2 -4
- package/dist/embedding-index-storage.d.ts.map +1 -1
- package/dist/embedding-index-storage.js +2 -1
- package/dist/{entity-canonical-id-migration-2YOLMOFC.js → entity-canonical-id-migration-ZBNRGUNV.js} +3 -3
- package/dist/entity-canonical-id-migration-ZBNRGUNV.js.map +1 -0
- package/dist/entity-retrieval.js +22 -21
- package/dist/event-order-recall.js +2 -2
- package/dist/explicit-capture.js +3 -3
- package/dist/external-wiki-access.js +74 -71
- package/dist/extraction-span-materialize.js +2 -2
- package/dist/extraction.js +30 -29
- package/dist/{graph-edge-decay-3HVLMT6U.js → graph-edge-decay-IU75NV42.js} +6 -6
- package/dist/graph-edge-decay-IU75NV42.js.map +1 -0
- package/dist/graph-jsonl.js +4 -4
- package/dist/graph-snapshot.js +4 -4
- package/dist/graph.js +5 -5
- package/dist/harmonic-retrieval.js +20 -19
- package/dist/index.d.ts +2 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +230 -218
- package/dist/lcm/engine.js +3 -3
- package/dist/lcm/index.js +5 -5
- package/dist/local-llm.js +2 -2
- package/dist/location/index.js +25 -24
- package/dist/location/tagging.d.ts +7 -5
- package/dist/location/tagging.d.ts.map +1 -1
- package/dist/maintenance/memory-governance.js +20 -19
- package/dist/maintenance/rebuild-memory-lifecycle-ledger.js +20 -19
- package/dist/maintenance/rebuild-memory-projection-runner.js +22 -21
- package/dist/maintenance/rebuild-memory-projection.js +21 -20
- package/dist/memory-browse-operations.js +74 -71
- package/dist/memory-browse.js +5 -5
- package/dist/namespaces/migrate.js +21 -20
- package/dist/namespaces/search.js +6 -6
- package/dist/namespaces/storage.js +20 -19
- package/dist/offline-sync-changeset-generations.js +5 -4
- package/dist/offline-sync-embedding-generation.d.ts +12 -2
- package/dist/offline-sync-embedding-generation.d.ts.map +1 -1
- package/dist/offline-sync-embedding-generation.js +6 -3
- package/dist/offline-sync-file-io.d.ts +15 -0
- package/dist/offline-sync-file-io.d.ts.map +1 -1
- package/dist/offline-sync-file-io.js +9 -1
- package/dist/offline-sync-snapshot-stream.js +9 -8
- package/dist/offline-sync-upload-staging.js +2 -2
- package/dist/offline-sync.d.ts.map +1 -1
- package/dist/offline-sync.js +10 -9
- package/dist/operator-doctor-corpus.js +23 -22
- package/dist/operator-toolkit.js +45 -43
- package/dist/orchestration/maintenance.js +33 -32
- package/dist/orchestrator.d.ts +1 -0
- package/dist/orchestrator.d.ts.map +1 -1
- package/dist/orchestrator.js +170 -167
- package/dist/page-versioning.js +2 -2
- package/dist/qmd.js +2 -2
- package/dist/recall-navigation-operations.js +74 -71
- package/dist/recall-navigation.js +73 -70
- package/dist/recall-state-view-admission.js +5 -5
- package/dist/recall-why-service.js +5 -5
- package/dist/resume-bundles.js +33 -31
- package/dist/search/factory.js +5 -5
- package/dist/search/index.js +9 -9
- package/dist/semantic-consolidation.js +25 -24
- package/dist/semantic-rule-promotion.js +20 -19
- package/dist/semantic-rule-verifier.js +20 -19
- package/dist/source-agent-qualifier.js +24 -23
- package/dist/standup/build.js +20 -19
- package/dist/storage/entity-canonical-id-migration-adapter.d.ts +1 -0
- package/dist/storage/entity-canonical-id-migration-adapter.d.ts.map +1 -1
- package/dist/storage/entity-canonical-id-migration.d.ts +7 -0
- package/dist/storage/entity-canonical-id-migration.d.ts.map +1 -1
- package/dist/storage/entity-canonical-id-references.d.ts +2 -0
- package/dist/storage/entity-canonical-id-references.d.ts.map +1 -1
- package/dist/storage/entity-migration-completion-cache.d.ts +46 -0
- package/dist/storage/entity-migration-completion-cache.d.ts.map +1 -0
- package/dist/storage/entity-store.d.ts +1 -8
- package/dist/storage/entity-store.d.ts.map +1 -1
- package/dist/storage/memory-read-store.d.ts +3 -2
- package/dist/storage/memory-read-store.d.ts.map +1 -1
- package/dist/storage/windowed-memory-read.d.ts +6 -0
- package/dist/storage/windowed-memory-read.d.ts.map +1 -1
- package/dist/storage.d.ts +2 -0
- package/dist/storage.d.ts.map +1 -1
- package/dist/storage.js +19 -18
- package/dist/summarizer.js +3 -3
- package/dist/support-passport/index.js +76 -73
- package/dist/transfer/backup.js +2 -2
- package/dist/transfer/capsule-export.js +2 -2
- package/dist/transfer/capsule-import.js +4 -4
- package/dist/transfer/export-okf.js +20 -19
- package/dist/verified-recall.js +20 -19
- package/dist/who-knows-cli.js +21 -20
- package/dist/who-knows.js +20 -19
- package/package.json +2 -2
- package/src/access-http-briefing.ts +4 -0
- package/src/access-http.test.ts +40 -11
- package/src/access-http.ts +6 -0
- package/src/access-observe-write-category.ts +1 -1
- package/src/access-observe-write-surface.ts +24 -19
- package/src/access-operations-batch.ts +18 -2
- package/src/access-service-peer-capture-backoff.test.ts +236 -0
- package/src/access-service.ts +11 -2
- package/src/briefing-abort.test.ts +173 -0
- package/src/briefing-deadline.test.ts +84 -0
- package/src/briefing-deadline.ts +33 -0
- package/src/briefing-window-abort.test.ts +47 -0
- package/src/briefing-window.ts +33 -4
- package/src/briefing.ts +23 -16
- package/src/capture-write-lock-scope.test.ts +32 -0
- package/src/capture-write-lock-scope.ts +30 -0
- package/src/correction/passive-correction-notifications-abort.test.ts +42 -0
- package/src/correction/passive-correction-notifications.ts +4 -0
- package/src/embedding-generation-marker.ts +51 -0
- package/src/embedding-index-storage.ts +13 -29
- package/src/entity-lazy-compatibility.test.ts +229 -0
- package/src/entity-migration-completion-notification.test.ts +148 -0
- package/src/index.ts +2 -2
- package/src/location/tagging.ts +14 -12
- package/src/offline-sync-embedding-delete-digest.test.ts +58 -0
- package/src/offline-sync-embedding-generation-marker.test.ts +245 -0
- package/src/offline-sync-embedding-generation.ts +47 -2
- package/src/offline-sync-embedding-lock-loss.test.ts +42 -0
- package/src/offline-sync-file-io.ts +61 -2
- package/src/offline-sync.test.ts +313 -20
- package/src/offline-sync.ts +47 -22
- package/src/orchestrator-init.test.ts +49 -1
- package/src/orchestrator.ts +9 -8
- package/src/storage/entity-canonical-id-migration-adapter.ts +16 -1
- package/src/storage/entity-canonical-id-migration.ts +17 -8
- package/src/storage/entity-canonical-id-references.ts +24 -0
- package/src/storage/entity-migration-completion-cache.nofollow.test.ts +223 -0
- package/src/storage/entity-migration-completion-cache.ts +153 -0
- package/src/storage/entity-store.ts +58 -86
- package/src/storage/memory-read-store.ts +9 -3
- package/src/storage/windowed-memory-read.ts +8 -0
- package/src/storage-startup-nonblocking-migration.test.ts +183 -0
- package/src/storage.ts +9 -3
- package/src/testing/subjects/entity-migration-completion-cache.test.ts +90 -0
- package/dist/chunk-3DKKZVXS.js.map +0 -1
- package/dist/chunk-CCTLRRV6.js.map +0 -1
- package/dist/chunk-CHSRA76M.js.map +0 -1
- package/dist/chunk-FRXS6SC7.js.map +0 -1
- package/dist/chunk-FWV2SXSL.js.map +0 -1
- package/dist/chunk-L46MG2GE.js.map +0 -1
- package/dist/chunk-LWRIGKFM.js.map +0 -1
- package/dist/chunk-OXSTKCYK.js.map +0 -1
- package/dist/chunk-PKPOO6X5.js.map +0 -1
- package/dist/chunk-SUKPM6DV.js.map +0 -1
- package/dist/chunk-UXAF4RQM.js.map +0 -1
- package/dist/chunk-VY6BD5KB.js.map +0 -1
- package/dist/chunk-Y57GJDG5.js.map +0 -1
- package/dist/chunk-YBKQAVPZ.js.map +0 -1
- package/dist/chunk-YUE7J2HZ.js.map +0 -1
- /package/dist/{auto-sync-BNCBMU6H.js.map → auto-sync-UQQ6YJSB.js.map} +0 -0
- /package/dist/{capsule-crypto-GWVG7LGC.js.map → briefing-deadline.js.map} +0 -0
- /package/dist/{entity-canonical-id-migration-2YOLMOFC.js.map → capsule-crypto-7FJQINUR.js.map} +0 -0
- /package/dist/{capsule-merge-RNQUP2VL.js.map → capsule-merge-D4BTIJGK.js.map} +0 -0
- /package/dist/{graph-edge-decay-3HVLMT6U.js.map → capture-write-lock-scope.js.map} +0 -0
- /package/dist/{chunk-FAKRTS3Y.js.map → chunk-2AKIVSHP.js.map} +0 -0
- /package/dist/{chunk-EIYJK7OD.js.map → chunk-2M36PR7R.js.map} +0 -0
- /package/dist/{chunk-OWHERGF2.js.map → chunk-2NLLXCJG.js.map} +0 -0
- /package/dist/{chunk-6P6MZDBL.js.map → chunk-3NL4ALUP.js.map} +0 -0
- /package/dist/{chunk-6CC2YCKG.js.map → chunk-43TUWQNR.js.map} +0 -0
- /package/dist/{chunk-H7BZBSP6.js.map → chunk-52II7PB6.js.map} +0 -0
- /package/dist/{chunk-2NLCDNGA.js.map → chunk-564YP3DW.js.map} +0 -0
- /package/dist/{chunk-5KGYR5VN.js.map → chunk-5DVOBBTV.js.map} +0 -0
- /package/dist/{chunk-RT2GN6KQ.js.map → chunk-5FUDSK6Z.js.map} +0 -0
- /package/dist/{chunk-B4XSXFKI.js.map → chunk-5G53JSRO.js.map} +0 -0
- /package/dist/{chunk-BGK3SFCC.js.map → chunk-5X2H3X4J.js.map} +0 -0
- /package/dist/{chunk-4J7BNL7B.js.map → chunk-6ZQX3R66.js.map} +0 -0
- /package/dist/{chunk-JNLYHJG7.js.map → chunk-74AY3S7U.js.map} +0 -0
- /package/dist/{chunk-RUAIO5UA.js.map → chunk-7J6A5MV2.js.map} +0 -0
- /package/dist/{chunk-A45QXZMU.js.map → chunk-7SAE6T36.js.map} +0 -0
- /package/dist/{chunk-QE2XQM3S.js.map → chunk-7YVER7ZO.js.map} +0 -0
- /package/dist/{chunk-IJH6METJ.js.map → chunk-A3UFV4K3.js.map} +0 -0
- /package/dist/{chunk-LXN737FX.js.map → chunk-AAANTXCQ.js.map} +0 -0
- /package/dist/{chunk-I3IPXGFQ.js.map → chunk-AJJPXUE6.js.map} +0 -0
- /package/dist/{chunk-2DPJFJFH.js.map → chunk-BNJAVRNJ.js.map} +0 -0
- /package/dist/{chunk-YL564QS2.js.map → chunk-CCL2OXFO.js.map} +0 -0
- /package/dist/{chunk-ZX5N5D6M.js.map → chunk-DAOMWGO2.js.map} +0 -0
- /package/dist/{chunk-23ZMQONL.js.map → chunk-ERMT5WQM.js.map} +0 -0
- /package/dist/{chunk-VTCNVGM6.js.map → chunk-EWBXMGXR.js.map} +0 -0
- /package/dist/{chunk-YNDLCWXS.js.map → chunk-EZ25VE3G.js.map} +0 -0
- /package/dist/{chunk-XTN54NML.js.map → chunk-G3T6YMBO.js.map} +0 -0
- /package/dist/{chunk-2JNBPOZ2.js.map → chunk-GDYLZ6FR.js.map} +0 -0
- /package/dist/{chunk-DZPTR2XQ.js.map → chunk-GFAEKSFR.js.map} +0 -0
- /package/dist/{chunk-M7LA4QBY.js.map → chunk-GXVXBYRA.js.map} +0 -0
- /package/dist/{chunk-JGMJZBFV.js.map → chunk-HPNXS6W6.js.map} +0 -0
- /package/dist/{chunk-BFPF5N3H.js.map → chunk-HXFIILGF.js.map} +0 -0
- /package/dist/{chunk-CMNL3QSL.js.map → chunk-IN6U2BS5.js.map} +0 -0
- /package/dist/{chunk-BWRX322G.js.map → chunk-IUYXCTA7.js.map} +0 -0
- /package/dist/{chunk-LU57EEJW.js.map → chunk-JBWD3J5U.js.map} +0 -0
- /package/dist/{chunk-2V664H6O.js.map → chunk-JS44PBCM.js.map} +0 -0
- /package/dist/{chunk-XKTHJHSQ.js.map → chunk-JZCBMKR5.js.map} +0 -0
- /package/dist/{chunk-MXPVNDGO.js.map → chunk-K5QQXVG6.js.map} +0 -0
- /package/dist/{chunk-BUKK5SWA.js.map → chunk-KQAFEZQX.js.map} +0 -0
- /package/dist/{chunk-WUBGMV3G.js.map → chunk-KRAMCBIW.js.map} +0 -0
- /package/dist/{chunk-NDMPRT3Z.js.map → chunk-KW5YZELC.js.map} +0 -0
- /package/dist/{chunk-2RQR4TXV.js.map → chunk-LIBVANOF.js.map} +0 -0
- /package/dist/{chunk-M7L5A2AT.js.map → chunk-LMMBV7QC.js.map} +0 -0
- /package/dist/{chunk-PJBYVJKO.js.map → chunk-LXXIV7Y2.js.map} +0 -0
- /package/dist/{chunk-MZCTS3ZG.js.map → chunk-M2DOJBH7.js.map} +0 -0
- /package/dist/{chunk-WOPZ2P5Z.js.map → chunk-M4HRYLOA.js.map} +0 -0
- /package/dist/{chunk-JCYEDMOY.js.map → chunk-M6TP6UYD.js.map} +0 -0
- /package/dist/{chunk-KAUCVTAW.js.map → chunk-MLNXATFG.js.map} +0 -0
- /package/dist/{chunk-2NX7JTST.js.map → chunk-MPWOAIIH.js.map} +0 -0
- /package/dist/{chunk-RQLTCHUL.js.map → chunk-MWURUSRE.js.map} +0 -0
- /package/dist/{chunk-3EGL7XZO.js.map → chunk-N74WJ7B2.js.map} +0 -0
- /package/dist/{chunk-DRXTMDAG.js.map → chunk-N7FNCE7F.js.map} +0 -0
- /package/dist/{chunk-6CWAY4R7.js.map → chunk-NPLM7AFU.js.map} +0 -0
- /package/dist/{chunk-VECRSYQG.js.map → chunk-NRGQ5HJO.js.map} +0 -0
- /package/dist/{chunk-PPDNG7WC.js.map → chunk-NSP5PIQH.js.map} +0 -0
- /package/dist/{chunk-6HVLHIAB.js.map → chunk-P7OVFGMU.js.map} +0 -0
- /package/dist/{chunk-EX27VKU6.js.map → chunk-PDTNQLPC.js.map} +0 -0
- /package/dist/{chunk-ME5LVISI.js.map → chunk-PJCVI44H.js.map} +0 -0
- /package/dist/{chunk-LJCHDFLP.js.map → chunk-Q3PNQYAC.js.map} +0 -0
- /package/dist/{chunk-YFJHFEET.js.map → chunk-QS765SBL.js.map} +0 -0
- /package/dist/{chunk-FJ264QBP.js.map → chunk-QTDZ2NYC.js.map} +0 -0
- /package/dist/{chunk-Q6UZREJK.js.map → chunk-RKRX4C4H.js.map} +0 -0
- /package/dist/{chunk-GLZWOIU3.js.map → chunk-SJTSWG2Z.js.map} +0 -0
- /package/dist/{chunk-XM747YJF.js.map → chunk-TFWJXWL6.js.map} +0 -0
- /package/dist/{chunk-QFQ4UG3I.js.map → chunk-TXKQBQYW.js.map} +0 -0
- /package/dist/{chunk-BMOJFGVX.js.map → chunk-UALXWWGN.js.map} +0 -0
- /package/dist/{chunk-B44NFHWI.js.map → chunk-UNOAVHV4.js.map} +0 -0
- /package/dist/{chunk-TF5NZGPS.js.map → chunk-UQBOPCDK.js.map} +0 -0
- /package/dist/{chunk-5R2IF2KI.js.map → chunk-UXGXJUZ5.js.map} +0 -0
- /package/dist/{chunk-J6B6FK2X.js.map → chunk-VCOMACR6.js.map} +0 -0
- /package/dist/{chunk-WCGJK4NQ.js.map → chunk-VKQI47DJ.js.map} +0 -0
- /package/dist/{chunk-MPJDQC4G.js.map → chunk-WJCXUE2J.js.map} +0 -0
- /package/dist/{chunk-BCDECPVO.js.map → chunk-WV4UYDP6.js.map} +0 -0
- /package/dist/{chunk-VWJ56T63.js.map → chunk-WXJC2L66.js.map} +0 -0
- /package/dist/{chunk-UVYI6VIX.js.map → chunk-X7Y7WX73.js.map} +0 -0
- /package/dist/{chunk-FPANDKMQ.js.map → chunk-XC3H7KON.js.map} +0 -0
- /package/dist/{chunk-OOOX5C4X.js.map → chunk-XL2EE5W3.js.map} +0 -0
- /package/dist/{chunk-3SL52JKD.js.map → chunk-XVZA6VXC.js.map} +0 -0
- /package/dist/{chunk-CZVNOTLR.js.map → chunk-XXR4A33N.js.map} +0 -0
- /package/dist/{chunk-2AV2U2SZ.js.map → chunk-YDIOMDBW.js.map} +0 -0
- /package/dist/{chunk-XTVH2NAW.js.map → chunk-YECEEJUU.js.map} +0 -0
- /package/dist/{chunk-QR6W3FSW.js.map → chunk-Z5277JWF.js.map} +0 -0
- /package/dist/{chunk-CTHYR4QK.js.map → chunk-ZFNXJEUG.js.map} +0 -0
- /package/dist/{chunk-6Q56WDHG.js.map → chunk-ZLCBLDJO.js.map} +0 -0
- /package/dist/{chunk-KRACAE73.js.map → chunk-ZNRTHYYW.js.map} +0 -0
- /package/dist/{chunk-7AIPWNM7.js.map → chunk-ZSMA67FV.js.map} +0 -0
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { log } from "../logger.js";
|
|
1
2
|
import type { EntityFile, MemoryFile } from "../types.js";
|
|
2
3
|
import {
|
|
3
4
|
getFingerprint,
|
|
@@ -30,6 +31,7 @@ export interface EntityCanonicalIdMigrationHost {
|
|
|
30
31
|
invalidateColdMemoriesCache(): void;
|
|
31
32
|
bumpMemoryStatusVersion(): void;
|
|
32
33
|
getEntityMutationVersion(): number;
|
|
34
|
+
onEntityMigrationComplete?: () => void;
|
|
33
35
|
}
|
|
34
36
|
|
|
35
37
|
export async function runLegacyEntityCanonicalIdMigration(
|
|
@@ -40,6 +42,9 @@ export async function runLegacyEntityCanonicalIdMigration(
|
|
|
40
42
|
): Promise<string | undefined> {
|
|
41
43
|
const readMigrationFingerprint = () =>
|
|
42
44
|
getFingerprint(host.baseDir, host.entitiesDir, () => String(host.getEntityMutationVersion()));
|
|
45
|
+
// The migration bumps the status version only after a pass that rewrote
|
|
46
|
+
// references; a no-op replay must not ask search maintenance to run.
|
|
47
|
+
let rewroteReferences = false;
|
|
43
48
|
const completionFingerprint = await migrateLegacyEntityCanonicalIds({
|
|
44
49
|
stateDir: host.stateDir,
|
|
45
50
|
entitiesDir: host.entitiesDir,
|
|
@@ -65,9 +70,19 @@ export async function runLegacyEntityCanonicalIdMigration(
|
|
|
65
70
|
invalidateKnowledgeIndexCache: host.invalidateKnowledgeIndexCache.bind(host),
|
|
66
71
|
invalidateAllMemoriesCache: host.invalidateAllMemoriesCache.bind(host),
|
|
67
72
|
invalidateColdMemoriesCache: host.invalidateColdMemoriesCache.bind(host),
|
|
68
|
-
bumpMemoryStatusVersion:
|
|
73
|
+
bumpMemoryStatusVersion: () => {
|
|
74
|
+
rewroteReferences = true;
|
|
75
|
+
host.bumpMemoryStatusVersion();
|
|
76
|
+
},
|
|
69
77
|
readMigrationFingerprint,
|
|
70
78
|
} satisfies EntityCanonicalIdMigrationDependencies);
|
|
79
|
+
if (completionFingerprint !== undefined && rewroteReferences) {
|
|
80
|
+
try {
|
|
81
|
+
host.onEntityMigrationComplete?.();
|
|
82
|
+
} catch {
|
|
83
|
+
log.warn("entity migration: search maintenance scheduling failed");
|
|
84
|
+
}
|
|
85
|
+
}
|
|
71
86
|
// An undefined completion fingerprint is a deliberate signal (issue #2213):
|
|
72
87
|
// a reconcile marker landed while the run scanned, and recomputing the
|
|
73
88
|
// fingerprint here would seal that pending request into the runner's
|
|
@@ -47,6 +47,7 @@ const EntityCanonicalIdMigrationStateSchema = z.object({
|
|
|
47
47
|
version: z.literal(ENTITY_CANONICAL_ID_MIGRATION_VERSION),
|
|
48
48
|
complete: z.boolean(),
|
|
49
49
|
mappings: z.record(z.string().min(1)),
|
|
50
|
+
completionFingerprint: z.string().optional(),
|
|
50
51
|
/**
|
|
51
52
|
* Pairs the migration refused to resolve, PARKED rather than dropped.
|
|
52
53
|
* Deleting the record would strand the collision: once an operator removes
|
|
@@ -370,6 +371,13 @@ async function assertEntityContentUnchanged(
|
|
|
370
371
|
}
|
|
371
372
|
}
|
|
372
373
|
|
|
374
|
+
/**
|
|
375
|
+
* Fail-closed entity-entry validation for the startup path: refuses a
|
|
376
|
+
* symlinked or non-directory entities root and any symlinked entry, without
|
|
377
|
+
* taking the migration lock. Exported so `ensureDirectories()` can run it
|
|
378
|
+
* before deferring the migration (#3139) — the same check
|
|
379
|
+
* `migrateLegacyEntityCanonicalIds` performs under the lock.
|
|
380
|
+
*/
|
|
373
381
|
export async function readSafeEntityEntries(entitiesDir: string): Promise<string[]> {
|
|
374
382
|
const rootStat = await lstat(entitiesDir);
|
|
375
383
|
if (rootStat.isSymbolicLink() || !rootStat.isDirectory()) {
|
|
@@ -887,14 +895,11 @@ export async function migrateLegacyEntityCanonicalIds(
|
|
|
887
895
|
if (!isErrnoCode(error, "ENOENT")) throw error;
|
|
888
896
|
}
|
|
889
897
|
}
|
|
890
|
-
let state = await readState(deps)
|
|
891
|
-
|
|
892
|
-
|
|
893
|
-
|
|
894
|
-
|
|
895
|
-
mappings: {},
|
|
896
|
-
};
|
|
897
|
-
}
|
|
898
|
+
let state: EntityCanonicalIdMigrationState = await readState(deps) ?? {
|
|
899
|
+
version: ENTITY_CANONICAL_ID_MIGRATION_VERSION,
|
|
900
|
+
complete: false,
|
|
901
|
+
mappings: {},
|
|
902
|
+
};
|
|
898
903
|
const blocked: BlockedEntityPairs = new Map();
|
|
899
904
|
const scanNotes: EntityScanNotes = { junk: new Set(), malformed: new Set() };
|
|
900
905
|
// Parks for pairs whose files are GONE, tracked separately from
|
|
@@ -951,6 +956,10 @@ export async function migrateLegacyEntityCanonicalIds(
|
|
|
951
956
|
// fingerprint could SEAL a marker the check never saw.
|
|
952
957
|
const completionFingerprint = await deps.readMigrationFingerprint?.();
|
|
953
958
|
if (await fileExists(reconcileMarkerPath)) return undefined;
|
|
959
|
+
if (completionFingerprint !== undefined && state.completionFingerprint !== completionFingerprint) {
|
|
960
|
+
state = { ...state, complete: true, completionFingerprint };
|
|
961
|
+
await publishState(deps, state);
|
|
962
|
+
}
|
|
954
963
|
return completionFingerprint;
|
|
955
964
|
};
|
|
956
965
|
/**
|
|
@@ -16,6 +16,7 @@ import { statSync } from "node:fs";
|
|
|
16
16
|
* reconciliation pass instead ({@link requestEntityCanonicalIdReconcile}).
|
|
17
17
|
*/
|
|
18
18
|
import path from "node:path";
|
|
19
|
+
import { z } from "zod";
|
|
19
20
|
import {
|
|
20
21
|
closeSync,
|
|
21
22
|
constants,
|
|
@@ -112,6 +113,29 @@ function readJournalFileNoFollow(statePath: string): JournalRead {
|
|
|
112
113
|
}
|
|
113
114
|
}
|
|
114
115
|
|
|
116
|
+
/** The journal fields completion attestation needs; extra journal fields are ignored. */
|
|
117
|
+
const CompletedJournalAttestationSchema = z.object({
|
|
118
|
+
version: z.literal(1),
|
|
119
|
+
complete: z.literal(true),
|
|
120
|
+
completionFingerprint: z.string(),
|
|
121
|
+
});
|
|
122
|
+
|
|
123
|
+
/** Skip compatibility scans only when journal completion attests to the live inputs. */
|
|
124
|
+
export async function entityCanonicalIdMigrationCompleted(
|
|
125
|
+
stateDir: string,
|
|
126
|
+
readFingerprint: () => Promise<string>,
|
|
127
|
+
): Promise<boolean> {
|
|
128
|
+
const result = readJournalFileNoFollow(path.join(stateDir, ENTITY_CANONICAL_ID_MIGRATION_FILE));
|
|
129
|
+
if (result.kind !== "ok") return false;
|
|
130
|
+
try {
|
|
131
|
+
const parsed = CompletedJournalAttestationSchema.safeParse(JSON.parse(result.raw));
|
|
132
|
+
if (!parsed.success) return false;
|
|
133
|
+
return parsed.data.completionFingerprint === await readFingerprint();
|
|
134
|
+
} catch {
|
|
135
|
+
return false;
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
|
|
115
139
|
export function loadHistoricalEntityCanonicalIds(stateDir: string): Readonly<Record<string, string>> {
|
|
116
140
|
const result = readJournalFileNoFollow(path.join(stateDir, ENTITY_CANONICAL_ID_MIGRATION_FILE));
|
|
117
141
|
if (result.kind !== "ok") return {};
|
|
@@ -0,0 +1,223 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Regression: amortized completion must refuse a symlinked entities root
|
|
3
|
+
* before getFingerprint → fingerprintEntityEntries readdir.
|
|
4
|
+
*
|
|
5
|
+
* inputSig already labels the root "L", but isCompleted still runs the full
|
|
6
|
+
* fingerprint (external tree walk) before any isTrustedEntitiesRoot-style
|
|
7
|
+
* guard. A planted complete journal makes that path reachable after a
|
|
8
|
+
* missing-id ENOENT.
|
|
9
|
+
*
|
|
10
|
+
* Instrumentation (deterministic): spawn a plain child (not nested --test —
|
|
11
|
+
* node skips recursive test runs) with --experimental-test-module-mocks that
|
|
12
|
+
* wraps node:fs/promises.readdir via mock.module, dynamic-imports the real
|
|
13
|
+
* EntityMigrationCompletionCache, and records every readdir whose realpath is
|
|
14
|
+
* the external canary (or whose path is the entities symlink). Assert
|
|
15
|
+
* externalCount === 0 — not merely completed === false. chmod/EACCES is not
|
|
16
|
+
* used (false-green on this host).
|
|
17
|
+
*
|
|
18
|
+
* Dynamic import in the probe is intentional: mock.module must install before
|
|
19
|
+
* the cache module loads fs.promises (module-loading boundary).
|
|
20
|
+
*
|
|
21
|
+
* Temp roots only. Expected today: RED (externalCount >= 1).
|
|
22
|
+
* After root nofollow guard: externalCount === 0 and completed === false.
|
|
23
|
+
*/
|
|
24
|
+
import assert from "node:assert/strict";
|
|
25
|
+
import { spawnSync } from "node:child_process";
|
|
26
|
+
import { existsSync } from "node:fs";
|
|
27
|
+
import {
|
|
28
|
+
lstat,
|
|
29
|
+
mkdir,
|
|
30
|
+
mkdtemp,
|
|
31
|
+
readFile,
|
|
32
|
+
realpath,
|
|
33
|
+
rm,
|
|
34
|
+
symlink,
|
|
35
|
+
writeFile,
|
|
36
|
+
} from "node:fs/promises";
|
|
37
|
+
import os from "node:os";
|
|
38
|
+
import path from "node:path";
|
|
39
|
+
import test from "node:test";
|
|
40
|
+
import { fileURLToPath } from "node:url";
|
|
41
|
+
|
|
42
|
+
import { ENTITY_CANONICAL_ID_MIGRATION_FILE } from "./entity-canonical-id-references.js";
|
|
43
|
+
|
|
44
|
+
function findRepoRoot(startDir: string): string {
|
|
45
|
+
let dir = startDir;
|
|
46
|
+
for (;;) {
|
|
47
|
+
if (existsSync(path.join(dir, "packages", "remnic-core", "package.json"))) return dir;
|
|
48
|
+
const parent = path.dirname(dir);
|
|
49
|
+
if (parent === dir) {
|
|
50
|
+
throw new Error(`remnic repo root not found from ${startDir}`);
|
|
51
|
+
}
|
|
52
|
+
dir = parent;
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
const REPO_ROOT = findRepoRoot(path.dirname(fileURLToPath(import.meta.url)));
|
|
57
|
+
const CACHE_MODULE = path.join(
|
|
58
|
+
REPO_ROOT,
|
|
59
|
+
"packages",
|
|
60
|
+
"remnic-core",
|
|
61
|
+
"src",
|
|
62
|
+
"storage",
|
|
63
|
+
"entity-migration-completion-cache.ts",
|
|
64
|
+
);
|
|
65
|
+
|
|
66
|
+
type ProbeResult = {
|
|
67
|
+
completed: boolean | null;
|
|
68
|
+
err: string | null;
|
|
69
|
+
externalCount: number;
|
|
70
|
+
seen: Array<{ raw: string; real: string }>;
|
|
71
|
+
canaryReal: string;
|
|
72
|
+
};
|
|
73
|
+
|
|
74
|
+
test("isCompleted must not readdir through a symlinked entities root", async () => {
|
|
75
|
+
const root = await mkdtemp(path.join(os.tmpdir(), "remnic-entities-root-nofollow-"));
|
|
76
|
+
const memoryDir = path.join(root, "memory");
|
|
77
|
+
const canaryDir = path.join(root, "canary-external");
|
|
78
|
+
const entitiesDir = path.join(memoryDir, "entities");
|
|
79
|
+
const stateDir = path.join(memoryDir, "state");
|
|
80
|
+
const probeFile = path.join(root, "completion-cache-nofollow.probe.mts");
|
|
81
|
+
const resultFile = path.join(root, "probe-result.json");
|
|
82
|
+
|
|
83
|
+
try {
|
|
84
|
+
await mkdir(stateDir, { recursive: true });
|
|
85
|
+
await mkdir(path.join(memoryDir, "config"), { recursive: true });
|
|
86
|
+
await mkdir(canaryDir, { recursive: true });
|
|
87
|
+
await writeFile(path.join(canaryDir, "outside-only.md"), "canary external entity body\n");
|
|
88
|
+
|
|
89
|
+
await symlink(canaryDir, entitiesDir);
|
|
90
|
+
assert.equal(
|
|
91
|
+
(await lstat(entitiesDir)).isSymbolicLink(),
|
|
92
|
+
true,
|
|
93
|
+
"fixture: entities/ must be a symlink to the external canary directory",
|
|
94
|
+
);
|
|
95
|
+
const canaryReal = await realpath(canaryDir);
|
|
96
|
+
|
|
97
|
+
// Regular complete journal — completionFingerprint compare invokes getFingerprint.
|
|
98
|
+
await writeFile(
|
|
99
|
+
path.join(stateDir, ENTITY_CANONICAL_ID_MIGRATION_FILE),
|
|
100
|
+
JSON.stringify({
|
|
101
|
+
version: 1,
|
|
102
|
+
complete: true,
|
|
103
|
+
mappings: {},
|
|
104
|
+
completionFingerprint: "planted-complete-journal-for-symlink-root-regression",
|
|
105
|
+
}),
|
|
106
|
+
);
|
|
107
|
+
|
|
108
|
+
// Plain probe script (no nested --test: parent already runs under the test
|
|
109
|
+
// runner, and node skips recursive test-file execution).
|
|
110
|
+
// Dynamic import loads cache after mock.module wraps readdir.
|
|
111
|
+
await writeFile(
|
|
112
|
+
probeFile,
|
|
113
|
+
`import { mock } from "node:test";
|
|
114
|
+
import path from "node:path";
|
|
115
|
+
import { writeFile } from "node:fs/promises";
|
|
116
|
+
import { readdir as realReaddir, realpath } from "node:fs/promises";
|
|
117
|
+
|
|
118
|
+
const canaryReal = ${JSON.stringify(canaryReal)};
|
|
119
|
+
const memoryDir = ${JSON.stringify(memoryDir)};
|
|
120
|
+
const entitiesDir = ${JSON.stringify(entitiesDir)};
|
|
121
|
+
const cacheModule = ${JSON.stringify(CACHE_MODULE)};
|
|
122
|
+
const resultFile = ${JSON.stringify(resultFile)};
|
|
123
|
+
const seen = [];
|
|
124
|
+
|
|
125
|
+
const fsp = await import("node:fs/promises");
|
|
126
|
+
mock.module("node:fs/promises", {
|
|
127
|
+
namedExports: {
|
|
128
|
+
...fsp,
|
|
129
|
+
readdir: async (p, ...a) => {
|
|
130
|
+
const raw = path.resolve(String(p));
|
|
131
|
+
let real = raw;
|
|
132
|
+
try { real = await realpath(raw); } catch { /* keep raw */ }
|
|
133
|
+
seen.push({ raw, real });
|
|
134
|
+
return realReaddir(p, ...a);
|
|
135
|
+
},
|
|
136
|
+
},
|
|
137
|
+
});
|
|
138
|
+
|
|
139
|
+
// Dynamic import: mock.module must wrap fs.promises before the cache loads it.
|
|
140
|
+
const { EntityMigrationCompletionCache } = await import(cacheModule);
|
|
141
|
+
const cache = new EntityMigrationCompletionCache();
|
|
142
|
+
let completed = null;
|
|
143
|
+
let err = null;
|
|
144
|
+
try {
|
|
145
|
+
completed = await cache.isCompleted({
|
|
146
|
+
baseDir: memoryDir,
|
|
147
|
+
entitiesDir,
|
|
148
|
+
getEntityMutationVersion: () => 0,
|
|
149
|
+
});
|
|
150
|
+
} catch (e) {
|
|
151
|
+
err = e && typeof e === "object" && "code" in e ? String(e.code) : String(e);
|
|
152
|
+
}
|
|
153
|
+
const entitiesResolved = path.resolve(entitiesDir);
|
|
154
|
+
const external = seen.filter((s) => s.real === canaryReal || s.raw === entitiesResolved);
|
|
155
|
+
const payload = {
|
|
156
|
+
completed,
|
|
157
|
+
err,
|
|
158
|
+
seen,
|
|
159
|
+
externalCount: external.length,
|
|
160
|
+
canaryReal,
|
|
161
|
+
};
|
|
162
|
+
await writeFile(resultFile, JSON.stringify(payload), "utf8");
|
|
163
|
+
console.log(JSON.stringify(payload));
|
|
164
|
+
mock.reset();
|
|
165
|
+
`,
|
|
166
|
+
);
|
|
167
|
+
|
|
168
|
+
const probe = spawnSync(
|
|
169
|
+
process.execPath,
|
|
170
|
+
[
|
|
171
|
+
"--experimental-test-module-mocks",
|
|
172
|
+
"--import",
|
|
173
|
+
"tsx",
|
|
174
|
+
"--conditions=remnic-source",
|
|
175
|
+
probeFile,
|
|
176
|
+
],
|
|
177
|
+
{
|
|
178
|
+
encoding: "utf8",
|
|
179
|
+
cwd: REPO_ROOT,
|
|
180
|
+
env: {
|
|
181
|
+
...process.env,
|
|
182
|
+
NODE_PATH: path.join(REPO_ROOT, "node_modules"),
|
|
183
|
+
},
|
|
184
|
+
timeout: 60_000,
|
|
185
|
+
},
|
|
186
|
+
);
|
|
187
|
+
|
|
188
|
+
if (probe.error) {
|
|
189
|
+
assert.fail(`probe failed to start: ${probe.error.message}`);
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
let result: ProbeResult;
|
|
193
|
+
try {
|
|
194
|
+
// SAFETY: the child probe in this file writes `resultFile` as a ProbeResult before exiting.
|
|
195
|
+
result = JSON.parse(await readFile(resultFile, "utf8")) as ProbeResult;
|
|
196
|
+
} catch {
|
|
197
|
+
assert.fail(
|
|
198
|
+
[
|
|
199
|
+
"probe did not write result file",
|
|
200
|
+
`status=${String(probe.status)} signal=${String(probe.signal)}`,
|
|
201
|
+
`stdout:\n${probe.stdout ?? ""}`,
|
|
202
|
+
`stderr:\n${probe.stderr ?? ""}`,
|
|
203
|
+
].join("\n"),
|
|
204
|
+
);
|
|
205
|
+
return;
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
assert.equal(
|
|
209
|
+
result.externalCount,
|
|
210
|
+
0,
|
|
211
|
+
[
|
|
212
|
+
"isCompleted must not readdir a symlinked entities root (external canary).",
|
|
213
|
+
`externalCount=${result.externalCount} canaryReal=${result.canaryReal}`,
|
|
214
|
+
`seen=${JSON.stringify(result.seen)}`,
|
|
215
|
+
result.err ? `isCompleted err=${result.err}` : `completed=${String(result.completed)}`,
|
|
216
|
+
].join(" "),
|
|
217
|
+
);
|
|
218
|
+
assert.equal(result.err, null, "untrusted root must fail closed without throwing through external IO");
|
|
219
|
+
assert.equal(result.completed, false, "untrusted entities root must not report migration complete");
|
|
220
|
+
} finally {
|
|
221
|
+
await rm(root, { recursive: true, force: true });
|
|
222
|
+
}
|
|
223
|
+
});
|
|
@@ -0,0 +1,153 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Amortized entity canonical-id migration completion verdict for entity reads.
|
|
3
|
+
*
|
|
4
|
+
* Key covers every getFingerprint input that is cheap to lstat: entity mutation
|
|
5
|
+
* version, entities/ dir, config/aliases.json, the journal, and reconcile
|
|
6
|
+
* markers. Full fingerprint (including entity entry enumeration) runs only on
|
|
7
|
+
* key miss.
|
|
8
|
+
*
|
|
9
|
+
* ponytail: in-place entity *content* edits still skip revalidation.
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
import { lstat, realpath } from "node:fs/promises";
|
|
13
|
+
import path from "node:path";
|
|
14
|
+
import { SecureStoreLockedError } from "../secure-store/secure-fs.js";
|
|
15
|
+
import { isErrnoCode } from "../utils/errno.js";
|
|
16
|
+
import { pathIsInside } from "../utils/path-containment.js";
|
|
17
|
+
import { getFingerprint } from "./entity-canonical-id-migration.js";
|
|
18
|
+
import {
|
|
19
|
+
ENTITY_CANONICAL_ID_MIGRATION_FILE,
|
|
20
|
+
ENTITY_CANONICAL_ID_RECONCILE_CONSUMING_MARKER,
|
|
21
|
+
ENTITY_CANONICAL_ID_RECONCILE_MARKER,
|
|
22
|
+
entityCanonicalIdMigrationCompleted,
|
|
23
|
+
resolveHistoricalEntityCanonicalId,
|
|
24
|
+
} from "./entity-canonical-id-references.js";
|
|
25
|
+
|
|
26
|
+
export type EntityMigrationCompletionCacheDeps = {
|
|
27
|
+
readonly baseDir: string;
|
|
28
|
+
readonly entitiesDir: string;
|
|
29
|
+
getEntityMutationVersion(): number;
|
|
30
|
+
};
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* True when `entitiesDir` is a real directory inside `baseDir`.
|
|
34
|
+
* lstat first so a symlinked root is never realpath'd into trust.
|
|
35
|
+
*/
|
|
36
|
+
export async function isTrustedEntitiesRoot(
|
|
37
|
+
baseDir: string,
|
|
38
|
+
entitiesDir: string,
|
|
39
|
+
): Promise<boolean> {
|
|
40
|
+
let rootStat;
|
|
41
|
+
try {
|
|
42
|
+
rootStat = await lstat(entitiesDir);
|
|
43
|
+
} catch (err) {
|
|
44
|
+
if (isErrnoCode(err, "ENOENT")) return false;
|
|
45
|
+
throw err;
|
|
46
|
+
}
|
|
47
|
+
if (rootStat.isSymbolicLink() || !rootStat.isDirectory()) return false;
|
|
48
|
+
let memoryRoot: string;
|
|
49
|
+
let entitiesReal: string;
|
|
50
|
+
try {
|
|
51
|
+
memoryRoot = await realpath(baseDir);
|
|
52
|
+
entitiesReal = await realpath(entitiesDir);
|
|
53
|
+
} catch (err) {
|
|
54
|
+
if (isErrnoCode(err, "ENOENT")) return false;
|
|
55
|
+
throw err;
|
|
56
|
+
}
|
|
57
|
+
return pathIsInside(memoryRoot, entitiesReal);
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
export class EntityMigrationCompletionCache {
|
|
61
|
+
private cache: { version: number; inputSig: string; completed: boolean } | undefined;
|
|
62
|
+
|
|
63
|
+
/** lstat identity for one fingerprint input path; nofollow; missing → stable token. */
|
|
64
|
+
private async inputPathSig(filePath: string): Promise<string> {
|
|
65
|
+
try {
|
|
66
|
+
const s = await lstat(filePath);
|
|
67
|
+
return `${s.dev}:${s.ino}:${s.mtimeMs}:${s.ctimeMs}:${s.size}:${s.isSymbolicLink() ? "L" : s.isDirectory() ? "D" : "F"}`;
|
|
68
|
+
} catch {
|
|
69
|
+
return "missing";
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
/**
|
|
74
|
+
* Cheap coherence key. entities/ absence is uncacheable (random suffix) so
|
|
75
|
+
* we never pin a false complete.
|
|
76
|
+
*/
|
|
77
|
+
private async inputSig(deps: EntityMigrationCompletionCacheDeps): Promise<string> {
|
|
78
|
+
const stateDir = path.join(deps.baseDir, "state");
|
|
79
|
+
const entitiesSig = await this.inputPathSig(deps.entitiesDir);
|
|
80
|
+
if (entitiesSig === "missing") return `entities-missing:${Math.random()}`;
|
|
81
|
+
const [aliasesSig, journalSig, reconcileSig, consumingSig] = await Promise.all([
|
|
82
|
+
this.inputPathSig(path.join(deps.baseDir, "config", "aliases.json")),
|
|
83
|
+
this.inputPathSig(path.join(stateDir, ENTITY_CANONICAL_ID_MIGRATION_FILE)),
|
|
84
|
+
this.inputPathSig(path.join(stateDir, ENTITY_CANONICAL_ID_RECONCILE_MARKER)),
|
|
85
|
+
this.inputPathSig(path.join(stateDir, ENTITY_CANONICAL_ID_RECONCILE_CONSUMING_MARKER)),
|
|
86
|
+
]);
|
|
87
|
+
return [entitiesSig, aliasesSig, journalSig, reconcileSig, consumingSig].join("|");
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
/**
|
|
91
|
+
* True when journal completion attests to live fingerprint inputs.
|
|
92
|
+
* Untrusted entities roots (symlink / outside base) never reach getFingerprint.
|
|
93
|
+
*/
|
|
94
|
+
async isCompleted(deps: EntityMigrationCompletionCacheDeps): Promise<boolean> {
|
|
95
|
+
const version = deps.getEntityMutationVersion();
|
|
96
|
+
const inputSig = await this.inputSig(deps);
|
|
97
|
+
const cached = this.cache;
|
|
98
|
+
if (cached && cached.version === version && cached.inputSig === inputSig) return cached.completed;
|
|
99
|
+
if (!(await isTrustedEntitiesRoot(deps.baseDir, deps.entitiesDir))) {
|
|
100
|
+
this.cache = { version, inputSig, completed: false };
|
|
101
|
+
return false;
|
|
102
|
+
}
|
|
103
|
+
const completed = await entityCanonicalIdMigrationCompleted(path.join(deps.baseDir, "state"), () =>
|
|
104
|
+
getFingerprint(deps.baseDir, deps.entitiesDir, () => String(version)));
|
|
105
|
+
this.cache = { version, inputSig, completed };
|
|
106
|
+
return completed;
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
export type ReadEntityRenameDeps = {
|
|
111
|
+
readonly entitiesDir: string;
|
|
112
|
+
currentHistoricalIds(): Readonly<Record<string, string>>;
|
|
113
|
+
readStorageSecureFile(filePath: string): Promise<string>;
|
|
114
|
+
};
|
|
115
|
+
|
|
116
|
+
/** Resolve a physical entity path through historical id rename candidates. */
|
|
117
|
+
export async function readEntityFileResolvingRename(
|
|
118
|
+
deps: ReadEntityRenameDeps,
|
|
119
|
+
firstPath: string,
|
|
120
|
+
entityId: string,
|
|
121
|
+
): Promise<{ id: string; content: string } | null> {
|
|
122
|
+
try {
|
|
123
|
+
return { id: entityId, content: await deps.readStorageSecureFile(firstPath) };
|
|
124
|
+
} catch (err) {
|
|
125
|
+
if (err instanceof SecureStoreLockedError) throw err;
|
|
126
|
+
// ENOENT: renamed away (retry below). ELOOP: planted symlink — skip.
|
|
127
|
+
if (isErrnoCode(err, "ELOOP")) return null;
|
|
128
|
+
if (!isErrnoCode(err, "ENOENT")) throw err;
|
|
129
|
+
}
|
|
130
|
+
const historicalIds = deps.currentHistoricalIds();
|
|
131
|
+
const canonical = resolveHistoricalEntityCanonicalId(entityId, historicalIds);
|
|
132
|
+
const candidates = new Set([canonical]);
|
|
133
|
+
for (const legacy of Object.keys(historicalIds)) {
|
|
134
|
+
if (resolveHistoricalEntityCanonicalId(legacy, historicalIds) === canonical) candidates.add(legacy);
|
|
135
|
+
}
|
|
136
|
+
candidates.delete(entityId);
|
|
137
|
+
for (const candidate of candidates) {
|
|
138
|
+
try {
|
|
139
|
+
return {
|
|
140
|
+
id: candidate,
|
|
141
|
+
content: await deps.readStorageSecureFile(
|
|
142
|
+
path.join(deps.entitiesDir, `${candidate}.md`),
|
|
143
|
+
),
|
|
144
|
+
};
|
|
145
|
+
} catch (err) {
|
|
146
|
+
if (err instanceof SecureStoreLockedError) throw err;
|
|
147
|
+
// ENOENT/ELOOP: gone or a planted symlink — try the next candidate.
|
|
148
|
+
if (isErrnoCode(err, "ENOENT") || isErrnoCode(err, "ELOOP")) continue;
|
|
149
|
+
throw err;
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
return null;
|
|
153
|
+
}
|