@remnic/core 9.44.0 → 9.45.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 +10 -10
- package/dist/access-admin-ops-surface.js +26 -25
- package/dist/access-audit.js +2 -2
- package/dist/access-authorization-probe.d.ts +10 -10
- package/dist/access-authorization-probe.js +27 -26
- package/dist/access-boundary.d.ts +10 -10
- package/dist/access-boundary.js +26 -25
- package/dist/access-cli.js +70 -68
- package/dist/access-cli.js.map +1 -1
- package/dist/access-extraction-force-flush.d.ts +10 -10
- package/dist/access-extraction-force-flush.js +26 -25
- package/dist/access-health-types.d.ts +1 -1
- package/dist/access-http-lcm-compaction.d.ts +10 -10
- package/dist/access-http-lifecycle-flush.d.ts +10 -10
- package/dist/access-http-query.js +26 -25
- package/dist/access-http.d.ts +10 -10
- package/dist/access-http.js +34 -33
- package/dist/access-identity-continuity-surface.d.ts +8 -8
- package/dist/access-identity-continuity-surface.js +26 -25
- package/dist/access-lcm-surface.d.ts +10 -10
- package/dist/access-lcm-surface.js +26 -25
- package/dist/access-mcp.d.ts +10 -10
- package/dist/access-mcp.js +30 -29
- package/dist/access-memory-search-fanout.d.ts +2 -2
- package/dist/{access-namespace-preflight-47dOyty9.d.ts → access-namespace-preflight-5yZ4g6F8.d.ts} +1 -1
- package/dist/access-namespace-preflight.d.ts +3 -3
- package/dist/access-namespace-preflight.js +26 -25
- package/dist/access-observe-write-surface.d.ts +10 -10
- package/dist/access-observe-write-surface.js +26 -25
- package/dist/access-operations-batch.js +28 -27
- package/dist/access-operations.d.ts +10 -10
- package/dist/access-operations.js +29 -28
- package/dist/access-recall-concurrency.d.ts +10 -10
- package/dist/access-recall-concurrency.js +26 -25
- package/dist/access-recall-response.d.ts +10 -10
- package/dist/access-recall-response.js +26 -25
- package/dist/access-recall-surface.d.ts +10 -10
- package/dist/access-recall-surface.js +26 -25
- package/dist/{access-service-BCZJbX0f.d.ts → access-service-CiJRsgAB.d.ts} +7 -7
- package/dist/access-service-helpers.d.ts +10 -10
- package/dist/access-service.d.ts +10 -10
- package/dist/access-service.js +26 -25
- package/dist/access-surface-catalog.d.ts +10 -10
- 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 +3 -2
- package/dist/active-recall.js.map +1 -1
- package/dist/adapters/index.js +4 -4
- package/dist/adapters/registry.js +2 -2
- package/dist/behavior-learner.d.ts +1 -1
- package/dist/behavior-signals.d.ts +1 -1
- package/dist/bootstrap.d.ts +8 -8
- package/dist/briefing.d.ts +2 -2
- package/dist/briefing.js +10 -9
- 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/bulk-import/index.d.ts +3 -3
- package/dist/calibration.d.ts +1 -1
- package/dist/capabilities.d.ts +1 -1
- package/dist/{catalog-DPQynQZG.d.ts → catalog-scBmy0sO.d.ts} +1 -1
- package/dist/causal-behavior.d.ts +1 -1
- package/dist/causal-behavior.js +2 -2
- package/dist/causal-chain.js +2 -2
- package/dist/causal-consolidation.d.ts +1 -1
- package/dist/causal-consolidation.js +13 -12
- package/dist/causal-consolidation.js.map +1 -1
- package/dist/causal-retrieval.js +2 -2
- package/dist/causal-trajectory-graph.d.ts +1 -1
- package/dist/causal-trajectory-graph.js +2 -2
- package/dist/causal-trajectory.js +1 -1
- package/dist/{chunk-CH2WTP6R.js → chunk-33PLLLL7.js} +2 -2
- package/dist/{chunk-TXBU7YDV.js → chunk-3TBLDMJA.js} +2 -2
- package/dist/{chunk-MUKLQDJP.js → chunk-4UH4BTGK.js} +35 -35
- package/dist/{chunk-N7LPS4Z3.js → chunk-5SM35XZR.js} +2 -2
- package/dist/{chunk-TVVEYCNW.js → chunk-7K5Q6COX.js} +4 -4
- package/dist/{chunk-YUOKZ3LJ.js → chunk-A5OJCXZL.js} +2 -2
- package/dist/{chunk-I22U5YYD.js → chunk-AAIUERNB.js} +2 -2
- package/dist/{chunk-UUQLB2HJ.js → chunk-BJKPPZ6Z.js} +2 -2
- package/dist/{chunk-EUVPB5TA.js → chunk-CJ2ZR7UA.js} +17 -17
- package/dist/{chunk-TM72UOX2.js → chunk-CUM2BBPN.js} +5 -1
- package/dist/chunk-CUM2BBPN.js.map +1 -0
- package/dist/{chunk-WMHO3WLC.js → chunk-D3J5SLQK.js} +2 -2
- package/dist/{chunk-PJUHQC4T.js → chunk-DL7ZD3JH.js} +2 -2
- package/dist/{chunk-O4BADU6R.js → chunk-DNWXFTLD.js} +2 -2
- package/dist/{chunk-IXFI6E3Q.js → chunk-E53DSZEN.js} +2 -2
- package/dist/{chunk-WX6KOVVK.js → chunk-E5UK4VPE.js} +2 -2
- package/dist/{chunk-OHLNFI6Z.js → chunk-E6PNLPGJ.js} +25 -23
- package/dist/{chunk-OHLNFI6Z.js.map → chunk-E6PNLPGJ.js.map} +1 -1
- package/dist/{chunk-Z54EM5YZ.js → chunk-GF22HWS2.js} +4 -4
- package/dist/chunk-HBVPYRDW.js +39 -0
- package/dist/chunk-HBVPYRDW.js.map +1 -0
- package/dist/{chunk-W6ROK2KU.js → chunk-HNZKVLAN.js} +2 -2
- package/dist/{chunk-GDC4INBF.js → chunk-HO3QBJEY.js} +2 -2
- package/dist/{chunk-J43XN7N3.js → chunk-HREE4JHI.js} +50 -50
- package/dist/{chunk-GSL47NPM.js → chunk-J7VGGECD.js} +1 -1
- package/dist/chunk-J7VGGECD.js.map +1 -0
- package/dist/{chunk-RKNJBZ55.js → chunk-JBPKEARU.js} +4 -4
- package/dist/{chunk-WUDWRUGK.js → chunk-JE7EK33X.js} +2 -2
- package/dist/{chunk-IADUAH35.js → chunk-JVLCVFQ4.js} +2 -2
- package/dist/{chunk-UZGJFIIG.js → chunk-JYIFGGEM.js} +2 -2
- package/dist/chunk-K442KOID.js +443 -0
- package/dist/chunk-K442KOID.js.map +1 -0
- package/dist/{chunk-3SHB4YAW.js → chunk-L2DPF4DF.js} +4 -4
- package/dist/{chunk-JDDTNK5P.js → chunk-LZ4LDKHZ.js} +2 -2
- package/dist/{chunk-PO5LSKQO.js → chunk-MU7ZHSZE.js} +2 -2
- package/dist/{chunk-CAGPP5NB.js → chunk-NRV4Y3PM.js} +1 -1
- package/dist/{chunk-7PLHD7EO.js → chunk-OAEQLRCY.js} +2 -2
- package/dist/{chunk-HGFILS2Q.js → chunk-OQ5PRDDS.js} +2 -2
- package/dist/{chunk-E6JYVPBK.js → chunk-P4DRWEYC.js} +2 -2
- package/dist/{chunk-MYFFOPO2.js → chunk-PDLBLTOK.js} +2 -2
- package/dist/{chunk-EMC5GT4H.js → chunk-PKNLM44P.js} +4 -4
- package/dist/{chunk-35IC3EVP.js → chunk-QN25OCUF.js} +2 -2
- package/dist/{chunk-OZL7KB6B.js → chunk-RN7DY3FC.js} +5 -5
- package/dist/{chunk-2FTRRQKZ.js → chunk-RVLJEII5.js} +30 -511
- package/dist/chunk-RVLJEII5.js.map +1 -0
- package/dist/{chunk-G24HJJ67.js → chunk-S2NCTZGJ.js} +6 -6
- package/dist/chunk-S5SLVSVY.js +517 -0
- package/dist/chunk-S5SLVSVY.js.map +1 -0
- package/dist/{chunk-R4Q7CQGZ.js → chunk-TDHTVMPG.js} +2 -2
- package/dist/{chunk-FXGLFGIG.js → chunk-TIHMZFIV.js} +3 -3
- package/dist/{chunk-U5SX74DV.js → chunk-U7GW7NE7.js} +7 -7
- package/dist/{chunk-PLQCPMJA.js → chunk-UVIKWC7M.js} +10 -10
- package/dist/{chunk-2QR2RRH6.js → chunk-UYLMNVY6.js} +4 -4
- package/dist/{chunk-OPVOUBHT.js → chunk-VUPTKTDV.js} +2 -2
- package/dist/{chunk-HXTUA7QM.js → chunk-VWAXN2BN.js} +7 -7
- package/dist/{chunk-TRBIJS2T.js → chunk-XYTOI63Z.js} +2 -2
- package/dist/{cli-U30A3jyx.d.ts → cli-BUGN3d6_.d.ts} +5 -5
- package/dist/cli.d.ts +12 -12
- package/dist/cli.js +53 -51
- package/dist/compounding/engine.d.ts +2 -2
- package/dist/compounding/engine.js +10 -9
- 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 +3 -2
- package/dist/connectors/codex-materialize-runner.d.ts +1 -1
- package/dist/connectors/codex-materialize-runner.js +10 -9
- package/dist/connectors/codex-materialize.d.ts +1 -1
- package/dist/connectors/index.d.ts +1 -1
- package/dist/connectors/index.js +10 -9
- 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 +11 -0
- package/dist/converge-config.js +12 -0
- package/dist/convergence-refresh.d.ts +2 -2
- package/dist/convergence-refresh.js +12 -11
- 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 +12 -11
- package/dist/day-summary.d.ts +1 -1
- package/dist/delinearize.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-retrieval.d.ts +2 -2
- package/dist/entity-retrieval.js +10 -9
- package/dist/entity-schema.d.ts +1 -1
- package/dist/explicit-capture.d.ts +8 -8
- package/dist/explicit-capture.js +3 -2
- 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-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 +12 -11
- 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-decay-GF3ZSGSP.js → graph-edge-decay-Z6ENALU2.js} +4 -4
- package/dist/graph-edge-decay-Z6ENALU2.js.map +1 -0
- package/dist/graph-edge-reinforcement.d.ts +1 -1
- package/dist/graph-snapshot.d.ts +1 -1
- package/dist/graph-snapshot.js +3 -3
- package/dist/graph.d.ts +1 -1
- package/dist/graph.js +2 -2
- 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 +20 -19
- package/dist/index.js +78 -71
- package/dist/intent.d.ts +1 -1
- package/dist/lcm/engine.d.ts +1 -1
- package/dist/lcm/engine.js +2 -2
- package/dist/lcm/index.d.ts +1 -1
- package/dist/lcm/index.js +4 -4
- 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 +10 -9
- package/dist/maintenance/rebuild-memory-lifecycle-ledger.d.ts +2 -2
- package/dist/maintenance/rebuild-memory-lifecycle-ledger.js +10 -9
- package/dist/maintenance/rebuild-memory-projection.d.ts +2 -2
- package/dist/maintenance/rebuild-memory-projection.js +11 -10
- package/dist/{maintenance-DVBLXkFw.d.ts → maintenance-LbuS2rUA.d.ts} +3 -3
- package/dist/mcp-memory-inspector-app.d.ts +10 -10
- 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-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 +11 -10
- 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 +10 -9
- 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 +13 -12
- package/dist/operator-doctor-replica.d.ts +1 -1
- package/dist/operator-toolkit.d.ts +3 -3
- package/dist/operator-toolkit.js +21 -19
- package/dist/orchestration/compression-guideline-coordinator.d.ts +2 -2
- package/dist/orchestration/maintenance.d.ts +4 -4
- package/dist/orchestration/maintenance.js +15 -14
- package/dist/{orchestrator-DUxbhhqO.d.ts → orchestrator-rHiWt32X.d.ts} +7 -7
- package/dist/orchestrator.d.ts +8 -8
- package/dist/orchestrator.js +70 -68
- package/dist/patterns-cli.d.ts +1 -1
- package/dist/{pipeline-Cs76XC-p.d.ts → pipeline-W_97O8BV.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/{qmd-WJwvBgiF.d.ts → qmd-LWtMofAf.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/manifest.d.ts +33 -0
- package/dist/reconcile/manifest.js +249 -0
- package/dist/reconcile/manifest.js.map +1 -0
- package/dist/reconcile/plan.d.ts +19 -24
- package/dist/reconcile/plan.js +9 -404
- package/dist/reconcile/plan.js.map +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 +4 -3
- 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-CQ0NAleS.d.ts → scope-profiles-BiQKOE-p.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-BWxOBMtz.d.ts → semantic-consolidation-dHYCyqgh.d.ts} +1 -1
- package/dist/semantic-consolidation.d.ts +2 -2
- package/dist/semantic-consolidation.js +11 -10
- package/dist/semantic-rule-promotion.js +10 -9
- package/dist/semantic-rule-verifier.d.ts +1 -1
- package/dist/semantic-rule-verifier.js +10 -9
- package/dist/{service-CLuMO-pa.d.ts → service-DJG3KCQ4.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 +10 -9
- package/dist/{storage-Blr71GpS.d.ts → storage-F9FVh-w9.d.ts} +13 -1
- package/dist/storage.d.ts +2 -2
- package/dist/storage.js +9 -8
- package/dist/summarizer.d.ts +1 -1
- package/dist/summary-snapshot.d.ts +1 -1
- 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-CqWwMPr_.d.ts → types-rEwubvim.d.ts} +6 -1
- 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 +10 -9
- package/dist/write-envelope.d.ts +1 -1
- package/package.json +12 -2
- package/src/config.test.ts +24 -0
- package/src/config.ts +3 -0
- package/src/converge-config.ts +39 -0
- package/src/index.ts +1 -2
- package/src/reconcile/manifest.test.ts +261 -0
- package/src/reconcile/manifest.ts +278 -0
- package/src/reconcile/plan.test.ts +40 -52
- package/src/reconcile/plan.ts +79 -63
- package/src/storage/content-hash-index.ts +26 -0
- package/src/types.ts +5 -0
- package/dist/chunk-2FTRRQKZ.js.map +0 -1
- package/dist/chunk-GSL47NPM.js.map +0 -1
- package/dist/chunk-TM72UOX2.js.map +0 -1
- /package/dist/{chunk-CH2WTP6R.js.map → chunk-33PLLLL7.js.map} +0 -0
- /package/dist/{chunk-TXBU7YDV.js.map → chunk-3TBLDMJA.js.map} +0 -0
- /package/dist/{chunk-MUKLQDJP.js.map → chunk-4UH4BTGK.js.map} +0 -0
- /package/dist/{chunk-N7LPS4Z3.js.map → chunk-5SM35XZR.js.map} +0 -0
- /package/dist/{chunk-TVVEYCNW.js.map → chunk-7K5Q6COX.js.map} +0 -0
- /package/dist/{chunk-YUOKZ3LJ.js.map → chunk-A5OJCXZL.js.map} +0 -0
- /package/dist/{chunk-I22U5YYD.js.map → chunk-AAIUERNB.js.map} +0 -0
- /package/dist/{chunk-UUQLB2HJ.js.map → chunk-BJKPPZ6Z.js.map} +0 -0
- /package/dist/{chunk-EUVPB5TA.js.map → chunk-CJ2ZR7UA.js.map} +0 -0
- /package/dist/{chunk-WMHO3WLC.js.map → chunk-D3J5SLQK.js.map} +0 -0
- /package/dist/{chunk-PJUHQC4T.js.map → chunk-DL7ZD3JH.js.map} +0 -0
- /package/dist/{chunk-O4BADU6R.js.map → chunk-DNWXFTLD.js.map} +0 -0
- /package/dist/{chunk-IXFI6E3Q.js.map → chunk-E53DSZEN.js.map} +0 -0
- /package/dist/{chunk-WX6KOVVK.js.map → chunk-E5UK4VPE.js.map} +0 -0
- /package/dist/{chunk-Z54EM5YZ.js.map → chunk-GF22HWS2.js.map} +0 -0
- /package/dist/{chunk-W6ROK2KU.js.map → chunk-HNZKVLAN.js.map} +0 -0
- /package/dist/{chunk-GDC4INBF.js.map → chunk-HO3QBJEY.js.map} +0 -0
- /package/dist/{chunk-J43XN7N3.js.map → chunk-HREE4JHI.js.map} +0 -0
- /package/dist/{chunk-RKNJBZ55.js.map → chunk-JBPKEARU.js.map} +0 -0
- /package/dist/{chunk-WUDWRUGK.js.map → chunk-JE7EK33X.js.map} +0 -0
- /package/dist/{chunk-IADUAH35.js.map → chunk-JVLCVFQ4.js.map} +0 -0
- /package/dist/{chunk-UZGJFIIG.js.map → chunk-JYIFGGEM.js.map} +0 -0
- /package/dist/{chunk-3SHB4YAW.js.map → chunk-L2DPF4DF.js.map} +0 -0
- /package/dist/{chunk-JDDTNK5P.js.map → chunk-LZ4LDKHZ.js.map} +0 -0
- /package/dist/{chunk-PO5LSKQO.js.map → chunk-MU7ZHSZE.js.map} +0 -0
- /package/dist/{chunk-CAGPP5NB.js.map → chunk-NRV4Y3PM.js.map} +0 -0
- /package/dist/{chunk-7PLHD7EO.js.map → chunk-OAEQLRCY.js.map} +0 -0
- /package/dist/{chunk-HGFILS2Q.js.map → chunk-OQ5PRDDS.js.map} +0 -0
- /package/dist/{chunk-E6JYVPBK.js.map → chunk-P4DRWEYC.js.map} +0 -0
- /package/dist/{chunk-MYFFOPO2.js.map → chunk-PDLBLTOK.js.map} +0 -0
- /package/dist/{chunk-EMC5GT4H.js.map → chunk-PKNLM44P.js.map} +0 -0
- /package/dist/{chunk-35IC3EVP.js.map → chunk-QN25OCUF.js.map} +0 -0
- /package/dist/{chunk-OZL7KB6B.js.map → chunk-RN7DY3FC.js.map} +0 -0
- /package/dist/{chunk-G24HJJ67.js.map → chunk-S2NCTZGJ.js.map} +0 -0
- /package/dist/{chunk-R4Q7CQGZ.js.map → chunk-TDHTVMPG.js.map} +0 -0
- /package/dist/{chunk-FXGLFGIG.js.map → chunk-TIHMZFIV.js.map} +0 -0
- /package/dist/{chunk-U5SX74DV.js.map → chunk-U7GW7NE7.js.map} +0 -0
- /package/dist/{chunk-PLQCPMJA.js.map → chunk-UVIKWC7M.js.map} +0 -0
- /package/dist/{chunk-2QR2RRH6.js.map → chunk-UYLMNVY6.js.map} +0 -0
- /package/dist/{chunk-OPVOUBHT.js.map → chunk-VUPTKTDV.js.map} +0 -0
- /package/dist/{chunk-HXTUA7QM.js.map → chunk-VWAXN2BN.js.map} +0 -0
- /package/dist/{chunk-TRBIJS2T.js.map → chunk-XYTOI63Z.js.map} +0 -0
- /package/dist/{graph-edge-decay-GF3ZSGSP.js.map → converge-config.js.map} +0 -0
|
@@ -0,0 +1,278 @@
|
|
|
1
|
+
import { createHash } from "node:crypto";
|
|
2
|
+
import { inferMemoryStatus } from "../memory-lifecycle-ledger-utils.js";
|
|
3
|
+
import { ContentHashIndex, type ContentHashPathEntry } from "../storage/content-hash-index.js";
|
|
4
|
+
import type { MemoryFrontmatter, MemoryStatus } from "../types.js";
|
|
5
|
+
import { RECALL_FALLBACK_DIRS } from "../utils/category-dir.js";
|
|
6
|
+
import {
|
|
7
|
+
type ReconcileFileState,
|
|
8
|
+
type ReconcilePlan,
|
|
9
|
+
type ReconcilePlanEntry,
|
|
10
|
+
summarizeReconcilePlan,
|
|
11
|
+
} from "./plan.js";
|
|
12
|
+
|
|
13
|
+
export const RECONCILE_MANIFEST_FORMAT = "remnic-reconcile-manifest";
|
|
14
|
+
export const RECONCILE_MANIFEST_SCHEMA_VERSION = 1;
|
|
15
|
+
|
|
16
|
+
export interface ReconcileMemoryIdentity {
|
|
17
|
+
id: string;
|
|
18
|
+
category: string;
|
|
19
|
+
contentHash: string;
|
|
20
|
+
status: MemoryStatus;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export interface ReconcileManifestFile extends ReconcileFileState {
|
|
24
|
+
memory?: ReconcileMemoryIdentity;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
type ActiveFactManifestFile = ReconcileManifestFile & { memory: ReconcileMemoryIdentity };
|
|
28
|
+
|
|
29
|
+
export interface ReconcileManifest {
|
|
30
|
+
format: typeof RECONCILE_MANIFEST_FORMAT;
|
|
31
|
+
schemaVersion: typeof RECONCILE_MANIFEST_SCHEMA_VERSION;
|
|
32
|
+
files: ReconcileManifestFile[];
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
export interface BuildReconcileManifestOptions {
|
|
36
|
+
files: Iterable<ReconcileFileState>;
|
|
37
|
+
readFile: (file: ReconcileFileState) => Promise<Buffer | string | null>;
|
|
38
|
+
cachedFiles?: Iterable<ReconcileManifestFile>;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
const SHA256_PATTERN = /^[a-f0-9]{64}$/i;
|
|
42
|
+
const MEMORY_DIRS = new Set(RECALL_FALLBACK_DIRS);
|
|
43
|
+
|
|
44
|
+
function isMemoryPath(filePath: string): boolean {
|
|
45
|
+
if (!filePath.endsWith(".md")) return false;
|
|
46
|
+
const segments = filePath.split("/");
|
|
47
|
+
let index = 0;
|
|
48
|
+
if (segments[index] === "cold" || segments[index] === "archive") index += 1;
|
|
49
|
+
return MEMORY_DIRS.has(segments[index] ?? "");
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
function parseScalar(value: string | undefined): string | undefined {
|
|
53
|
+
if (value === undefined) return undefined;
|
|
54
|
+
const trimmed = value.trim();
|
|
55
|
+
if (trimmed.length === 0) return undefined;
|
|
56
|
+
if (trimmed.startsWith('"') && trimmed.endsWith('"')) {
|
|
57
|
+
try {
|
|
58
|
+
const parsed = JSON.parse(trimmed) as unknown;
|
|
59
|
+
return typeof parsed === "string" ? parsed : trimmed;
|
|
60
|
+
} catch {
|
|
61
|
+
return trimmed.slice(1, -1).replace(/\\"/g, '"');
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
if (trimmed.startsWith("'") && trimmed.endsWith("'")) {
|
|
65
|
+
return trimmed.slice(1, -1).replace(/''/g, "'");
|
|
66
|
+
}
|
|
67
|
+
return trimmed;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
function parsedMemoryIdentity(filePath: string, raw: Buffer | string): ReconcileMemoryIdentity | undefined {
|
|
71
|
+
if (!isMemoryPath(filePath)) return undefined;
|
|
72
|
+
const match = (Buffer.isBuffer(raw) ? raw.toString("utf8") : raw).match(/^---\n([\s\S]*?)\n---\n?([\s\S]*)$/);
|
|
73
|
+
if (!match) return undefined;
|
|
74
|
+
const fields = new Map<string, string>();
|
|
75
|
+
for (const line of match[1].split("\n")) {
|
|
76
|
+
const separator = line.indexOf(":");
|
|
77
|
+
if (separator > 0) fields.set(line.slice(0, separator).trim(), line.slice(separator + 1).trim());
|
|
78
|
+
}
|
|
79
|
+
const id = parseScalar(fields.get("id"));
|
|
80
|
+
if (!id) return undefined;
|
|
81
|
+
const category = parseScalar(fields.get("category")) ?? "fact";
|
|
82
|
+
const storedHash = parseScalar(fields.get("contentHash"));
|
|
83
|
+
const contentHash =
|
|
84
|
+
storedHash && SHA256_PATTERN.test(storedHash)
|
|
85
|
+
? storedHash.toLowerCase()
|
|
86
|
+
: ContentHashIndex.computeHash(match[2].trim());
|
|
87
|
+
const status = inferMemoryStatus(
|
|
88
|
+
{
|
|
89
|
+
status: parseScalar(fields.get("status")) as MemoryStatus | undefined,
|
|
90
|
+
archivedAt: parseScalar(fields.get("archivedAt")),
|
|
91
|
+
} as MemoryFrontmatter,
|
|
92
|
+
filePath
|
|
93
|
+
);
|
|
94
|
+
return { id, category, contentHash, status };
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
export async function buildReconcileManifest(options: BuildReconcileManifestOptions): Promise<ReconcileManifest> {
|
|
98
|
+
const cachedByPath = new Map<string, ReconcileManifestFile>();
|
|
99
|
+
for (const cached of options.cachedFiles ?? []) {
|
|
100
|
+
cachedByPath.set(cached.path, cached);
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
const files: ReconcileManifestFile[] = [];
|
|
104
|
+
for (const file of options.files) {
|
|
105
|
+
const cached = cachedByPath.get(file.path);
|
|
106
|
+
if (cached?.sha256.toLowerCase() === file.sha256.toLowerCase()) {
|
|
107
|
+
files.push({ ...file, ...(cached.memory ? { memory: cached.memory } : {}) });
|
|
108
|
+
continue;
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
let raw: Buffer | string | null = null;
|
|
112
|
+
if (isMemoryPath(file.path)) {
|
|
113
|
+
try {
|
|
114
|
+
raw = await options.readFile(file);
|
|
115
|
+
} catch {
|
|
116
|
+
raw = null;
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
if (raw !== null && createHash("sha256").update(raw).digest("hex") !== file.sha256.toLowerCase()) {
|
|
120
|
+
raw = null;
|
|
121
|
+
}
|
|
122
|
+
const memory = raw === null ? undefined : parsedMemoryIdentity(file.path, raw);
|
|
123
|
+
files.push({ ...file, ...(memory ? { memory } : {}) });
|
|
124
|
+
}
|
|
125
|
+
files.sort((left, right) => (left.path < right.path ? -1 : left.path > right.path ? 1 : 0));
|
|
126
|
+
return {
|
|
127
|
+
format: RECONCILE_MANIFEST_FORMAT,
|
|
128
|
+
schemaVersion: RECONCILE_MANIFEST_SCHEMA_VERSION,
|
|
129
|
+
files,
|
|
130
|
+
};
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
function activeFactByPath(manifest: ReconcileManifest | undefined): Map<string, ActiveFactManifestFile> {
|
|
134
|
+
const result = new Map<string, ActiveFactManifestFile>();
|
|
135
|
+
for (const file of manifest?.files ?? []) {
|
|
136
|
+
if (file.memory?.category === "fact" && file.memory.status === "active") {
|
|
137
|
+
result.set(file.path, file as ActiveFactManifestFile);
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
return result;
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
function contentHashRows(files: Iterable<ActiveFactManifestFile>): ContentHashPathEntry[] {
|
|
144
|
+
const rows: ContentHashPathEntry[] = [];
|
|
145
|
+
for (const file of files) {
|
|
146
|
+
rows.push({ path: file.path, contentHash: file.memory.contentHash });
|
|
147
|
+
}
|
|
148
|
+
return rows;
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
function comparePlanEntries(left: ReconcilePlanEntry, right: ReconcilePlanEntry): number {
|
|
152
|
+
if (left.namespace !== right.namespace) return left.namespace < right.namespace ? -1 : 1;
|
|
153
|
+
return left.path < right.path ? -1 : left.path > right.path ? 1 : 0;
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
export function collapseActiveFactDuplicates(
|
|
157
|
+
plan: ReconcilePlan,
|
|
158
|
+
localManifests: ReadonlyMap<string, ReconcileManifest>,
|
|
159
|
+
peerManifests: ReadonlyMap<string, ReconcileManifest>
|
|
160
|
+
): ReconcilePlan {
|
|
161
|
+
const entriesByNamespace = new Map<string, ReconcilePlanEntry[]>();
|
|
162
|
+
for (const entry of plan.entries) {
|
|
163
|
+
const entries = entriesByNamespace.get(entry.namespace) ?? [];
|
|
164
|
+
entries.push({ ...entry });
|
|
165
|
+
entriesByNamespace.set(entry.namespace, entries);
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
let changed = false;
|
|
169
|
+
for (const namespace of new Set([...localManifests.keys(), ...peerManifests.keys()])) {
|
|
170
|
+
const entries = entriesByNamespace.get(namespace) ?? [];
|
|
171
|
+
const localManifest = localManifests.get(namespace);
|
|
172
|
+
const peerManifest = peerManifests.get(namespace);
|
|
173
|
+
const localByPath = activeFactByPath(localManifest);
|
|
174
|
+
const peerByPath = activeFactByPath(peerManifest);
|
|
175
|
+
const localFilesByPath = new Map((localManifest?.files ?? []).map((file) => [file.path, file]));
|
|
176
|
+
const peerFilesByPath = new Map((peerManifest?.files ?? []).map((file) => [file.path, file]));
|
|
177
|
+
const localByHash = new Map<string, ActiveFactManifestFile[]>();
|
|
178
|
+
const peerByHash = new Map<string, ActiveFactManifestFile[]>();
|
|
179
|
+
|
|
180
|
+
for (const file of localByPath.values()) {
|
|
181
|
+
const hash = file.memory.contentHash;
|
|
182
|
+
const bucket = localByHash.get(hash) ?? [];
|
|
183
|
+
bucket.push(file);
|
|
184
|
+
localByHash.set(hash, bucket);
|
|
185
|
+
}
|
|
186
|
+
for (const file of peerByPath.values()) {
|
|
187
|
+
const hash = file.memory.contentHash;
|
|
188
|
+
const bucket = peerByHash.get(hash) ?? [];
|
|
189
|
+
bucket.push(file);
|
|
190
|
+
peerByHash.set(hash, bucket);
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
const removed = new Set<ReconcilePlanEntry>();
|
|
194
|
+
const replacements: ReconcilePlanEntry[] = [];
|
|
195
|
+
for (const hash of new Set([...localByHash.keys(), ...peerByHash.keys()])) {
|
|
196
|
+
const localCandidates = (localByHash.get(hash) ?? []).filter((file) => {
|
|
197
|
+
const opposite = peerFilesByPath.get(file.path);
|
|
198
|
+
const activeOpposite = peerByPath.get(file.path);
|
|
199
|
+
return opposite === undefined || activeOpposite?.memory.contentHash === hash;
|
|
200
|
+
});
|
|
201
|
+
const peerCandidates = (peerByHash.get(hash) ?? []).filter((file) => {
|
|
202
|
+
const opposite = localFilesByPath.get(file.path);
|
|
203
|
+
const activeOpposite = localByPath.get(file.path);
|
|
204
|
+
return opposite === undefined || activeOpposite?.memory.contentHash === hash;
|
|
205
|
+
});
|
|
206
|
+
const localPath = ContentHashIndex.resolvePathByHash(hash, contentHashRows(localCandidates));
|
|
207
|
+
const peerPath = ContentHashIndex.resolvePathByHash(hash, contentHashRows(peerCandidates));
|
|
208
|
+
|
|
209
|
+
if (localPath && peerPath && localPath !== peerPath) {
|
|
210
|
+
const localFile = localByPath.get(localPath);
|
|
211
|
+
const peerFile = peerByPath.get(peerPath);
|
|
212
|
+
if (!localFile || !peerFile) continue;
|
|
213
|
+
const duplicatePaths = new Set([
|
|
214
|
+
...localCandidates.map((file) => file.path),
|
|
215
|
+
...peerCandidates.map((file) => file.path),
|
|
216
|
+
]);
|
|
217
|
+
const unsafeEntry = entries.some(
|
|
218
|
+
(entry) => duplicatePaths.has(entry.path) && (entry.action === "suppress" || entry.action === "conflict")
|
|
219
|
+
);
|
|
220
|
+
if (unsafeEntry) continue;
|
|
221
|
+
for (const entry of entries) {
|
|
222
|
+
if (
|
|
223
|
+
duplicatePaths.has(entry.path) &&
|
|
224
|
+
(entry.action === "pull" || entry.action === "push" || entry.action === "identical")
|
|
225
|
+
) {
|
|
226
|
+
removed.add(entry);
|
|
227
|
+
}
|
|
228
|
+
}
|
|
229
|
+
replacements.push({
|
|
230
|
+
path: localPath < peerPath ? localPath : peerPath,
|
|
231
|
+
namespace,
|
|
232
|
+
action: "identical",
|
|
233
|
+
reason: "semantic_duplicate",
|
|
234
|
+
localSha256: localFile.sha256,
|
|
235
|
+
peerSha256: peerFile.sha256,
|
|
236
|
+
});
|
|
237
|
+
changed = true;
|
|
238
|
+
continue;
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
const sameSideEntries = entries.filter((entry) => {
|
|
242
|
+
if (localPath && entry.reason === "local_only") {
|
|
243
|
+
return localCandidates.some((file) => file.path === entry.path);
|
|
244
|
+
}
|
|
245
|
+
if (peerPath && entry.reason === "peer_only") {
|
|
246
|
+
return peerCandidates.some((file) => file.path === entry.path);
|
|
247
|
+
}
|
|
248
|
+
return false;
|
|
249
|
+
});
|
|
250
|
+
const canonicalPath = localPath ?? peerPath;
|
|
251
|
+
const hasSharedCanonical = localPath !== undefined && localPath === peerPath;
|
|
252
|
+
if (!canonicalPath || sameSideEntries.length === 0 || (!hasSharedCanonical && sameSideEntries.length < 2)) {
|
|
253
|
+
continue;
|
|
254
|
+
}
|
|
255
|
+
for (const entry of sameSideEntries) {
|
|
256
|
+
if (entry.path !== canonicalPath) {
|
|
257
|
+
removed.add(entry);
|
|
258
|
+
changed = true;
|
|
259
|
+
}
|
|
260
|
+
}
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
if (removed.size > 0 || replacements.length > 0) {
|
|
264
|
+
entriesByNamespace.set(
|
|
265
|
+
namespace,
|
|
266
|
+
[...entries.filter((entry) => !removed.has(entry)), ...replacements].sort(comparePlanEntries)
|
|
267
|
+
);
|
|
268
|
+
}
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
if (!changed) return plan;
|
|
272
|
+
const entries = [...entriesByNamespace.values()].flat().sort(comparePlanEntries);
|
|
273
|
+
return {
|
|
274
|
+
entries,
|
|
275
|
+
byNamespace: summarizeReconcilePlan(entries),
|
|
276
|
+
converged: entries.every((entry) => entry.action === "identical"),
|
|
277
|
+
};
|
|
278
|
+
}
|
|
@@ -60,7 +60,7 @@ test("a bootstrap merge moves unique data both ways and deletes nothing", () =>
|
|
|
60
60
|
);
|
|
61
61
|
});
|
|
62
62
|
|
|
63
|
-
test("
|
|
63
|
+
test("newest-wins is the default policy", () => {
|
|
64
64
|
const entries = planNamespaceReconciliation({
|
|
65
65
|
namespace: "default",
|
|
66
66
|
local: [file("facts/a.md", "local", 2000)],
|
|
@@ -69,7 +69,7 @@ test("manual is the default policy: a both-modified path is reported, never auto
|
|
|
69
69
|
const conflict = entryFor(entries, "facts/a.md");
|
|
70
70
|
assert.equal(conflict.action, "conflict");
|
|
71
71
|
assert.equal(conflict.reason, "both_modified");
|
|
72
|
-
assert.equal(conflict.resolution, "
|
|
72
|
+
assert.equal(conflict.resolution, "local-wins");
|
|
73
73
|
});
|
|
74
74
|
|
|
75
75
|
test("newest-wins picks the later side by mtime", () => {
|
|
@@ -101,17 +101,6 @@ test("newest-wins degrades to keeping both when the order is not decidable", ()
|
|
|
101
101
|
assert.equal(entryFor(entries, "facts/tied.md").resolution, "supersede-link");
|
|
102
102
|
});
|
|
103
103
|
|
|
104
|
-
test("keep-both never designates a loser", () => {
|
|
105
|
-
const entries = planNamespaceReconciliation(
|
|
106
|
-
{
|
|
107
|
-
namespace: "default",
|
|
108
|
-
local: [file("facts/a.md", "local", 2000)],
|
|
109
|
-
peer: [file("facts/a.md", "peer", 1000)],
|
|
110
|
-
},
|
|
111
|
-
{ conflictPolicy: "keep-both" },
|
|
112
|
-
);
|
|
113
|
-
assert.equal(entryFor(entries, "facts/a.md").resolution, "supersede-link");
|
|
114
|
-
});
|
|
115
104
|
|
|
116
105
|
test("a base turns a one-sided change back into an ordinary transfer", () => {
|
|
117
106
|
const base = [file("facts/a.md", "v1"), file("facts/b.md", "v1")];
|
|
@@ -200,6 +189,41 @@ test("delete-versus-modify stays a conflict in both directions", () => {
|
|
|
200
189
|
assert.equal(peerEdited.resolution, "unresolved");
|
|
201
190
|
});
|
|
202
191
|
|
|
192
|
+
test("newest-wins compares delete revision times with surviving modifications", () => {
|
|
193
|
+
const entries = planNamespaceReconciliation(
|
|
194
|
+
{
|
|
195
|
+
namespace: "default",
|
|
196
|
+
local: [
|
|
197
|
+
file("facts/local-modification-wins.md", "local-v2", 4000),
|
|
198
|
+
file("facts/peer-deletion-wins.md", "local-v2", 2000),
|
|
199
|
+
],
|
|
200
|
+
peer: [
|
|
201
|
+
file("facts/peer-modification-wins.md", "peer-v2", 4000),
|
|
202
|
+
file("facts/local-deletion-wins.md", "peer-v2", 2000),
|
|
203
|
+
],
|
|
204
|
+
base: [
|
|
205
|
+
file("facts/local-modification-wins.md", "v1"),
|
|
206
|
+
file("facts/peer-deletion-wins.md", "v1"),
|
|
207
|
+
file("facts/peer-modification-wins.md", "v1"),
|
|
208
|
+
file("facts/local-deletion-wins.md", "v1"),
|
|
209
|
+
],
|
|
210
|
+
localDeletionMtimeMs: new Map([
|
|
211
|
+
["facts/peer-modification-wins.md", 3000],
|
|
212
|
+
["facts/local-deletion-wins.md", 3000],
|
|
213
|
+
]),
|
|
214
|
+
peerDeletionMtimeMs: new Map([
|
|
215
|
+
["facts/local-modification-wins.md", 3000],
|
|
216
|
+
["facts/peer-deletion-wins.md", 3000],
|
|
217
|
+
]),
|
|
218
|
+
},
|
|
219
|
+
{ conflictPolicy: "newest-wins" },
|
|
220
|
+
);
|
|
221
|
+
assert.equal(entryFor(entries, "facts/local-modification-wins.md").resolution, "local-wins");
|
|
222
|
+
assert.equal(entryFor(entries, "facts/peer-deletion-wins.md").resolution, "peer-wins");
|
|
223
|
+
assert.equal(entryFor(entries, "facts/peer-modification-wins.md").resolution, "peer-wins");
|
|
224
|
+
assert.equal(entryFor(entries, "facts/local-deletion-wins.md").resolution, "local-wins");
|
|
225
|
+
});
|
|
226
|
+
|
|
203
227
|
test("namespaces are planned independently and reported separately", () => {
|
|
204
228
|
const plan = planReconciliation([
|
|
205
229
|
{ namespace: "alpha", local: [file("facts/a.md", "x")], peer: [] },
|
|
@@ -774,29 +798,6 @@ test("Win32-aliasing path segments are rejected", () => {
|
|
|
774
798
|
}
|
|
775
799
|
});
|
|
776
800
|
|
|
777
|
-
test("a supersede link names the newer revision", () => {
|
|
778
|
-
// Without a direction transport must re-derive it or guess which side is
|
|
779
|
-
// linked as superseding the other.
|
|
780
|
-
const newerPeer = planNamespaceReconciliation(
|
|
781
|
-
{
|
|
782
|
-
namespace: "default",
|
|
783
|
-
local: [file("facts/a.md", "local", 1000)],
|
|
784
|
-
peer: [file("facts/a.md", "peer", 2000)],
|
|
785
|
-
},
|
|
786
|
-
{ conflictPolicy: "keep-both" },
|
|
787
|
-
);
|
|
788
|
-
assert.equal(entryFor(newerPeer, "facts/a.md").newerSide, "peer");
|
|
789
|
-
|
|
790
|
-
const newerLocal = planNamespaceReconciliation(
|
|
791
|
-
{
|
|
792
|
-
namespace: "default",
|
|
793
|
-
local: [file("facts/a.md", "local", 2000)],
|
|
794
|
-
peer: [file("facts/a.md", "peer", 1000)],
|
|
795
|
-
},
|
|
796
|
-
{ conflictPolicy: "keep-both" },
|
|
797
|
-
);
|
|
798
|
-
assert.equal(entryFor(newerLocal, "facts/a.md").newerSide, "local");
|
|
799
|
-
});
|
|
800
801
|
|
|
801
802
|
test("an unorderable supersede link says so instead of picking a side", () => {
|
|
802
803
|
for (const pair of [
|
|
@@ -805,11 +806,10 @@ test("an unorderable supersede link says so instead of picking a side", () => {
|
|
|
805
806
|
] as const) {
|
|
806
807
|
const entries = planNamespaceReconciliation(
|
|
807
808
|
{ namespace: "default", local: [pair[0]], peer: [pair[1]] },
|
|
808
|
-
{ conflictPolicy: "
|
|
809
|
+
{ conflictPolicy: "newest-wins" },
|
|
809
810
|
);
|
|
810
811
|
const entry = entryFor(entries, "facts/a.md");
|
|
811
812
|
assert.equal(entry.resolution, "supersede-link");
|
|
812
|
-
assert.equal(entry.newerSide, undefined, "an unordered link must be visibly unordered");
|
|
813
813
|
}
|
|
814
814
|
});
|
|
815
815
|
|
|
@@ -845,25 +845,13 @@ test("a directionless supersede link is reported as unresolved", () => {
|
|
|
845
845
|
local: [file("facts/a.md", "local", 5000)],
|
|
846
846
|
peer: [file("facts/a.md", "peer", 5000)],
|
|
847
847
|
},
|
|
848
|
-
{ conflictPolicy: "
|
|
848
|
+
{ conflictPolicy: "newest-wins" },
|
|
849
849
|
);
|
|
850
|
-
assert.equal(entryFor(entries, "facts/a.md").newerSide, undefined);
|
|
851
850
|
assert.deepEqual(summarizeReconcilePlan(entries), [
|
|
852
851
|
{ namespace: "default", pull: 0, push: 0, identical: 0, conflict: 1, suppress: 0, unresolved: 1 },
|
|
853
852
|
]);
|
|
854
853
|
});
|
|
855
854
|
|
|
856
|
-
test("an ordered supersede link is not counted as unresolved", () => {
|
|
857
|
-
const entries = planNamespaceReconciliation(
|
|
858
|
-
{
|
|
859
|
-
namespace: "default",
|
|
860
|
-
local: [file("facts/a.md", "local", 1000)],
|
|
861
|
-
peer: [file("facts/a.md", "peer", 2000)],
|
|
862
|
-
},
|
|
863
|
-
{ conflictPolicy: "keep-both" },
|
|
864
|
-
);
|
|
865
|
-
assert.equal(summarizeReconcilePlan(entries)[0]?.unresolved, 0);
|
|
866
|
-
});
|
|
867
855
|
|
|
868
856
|
test("superscript COM and LPT device aliases are rejected", () => {
|
|
869
857
|
for (const bad of ["facts/COM\u00b9.txt", "facts/LPT\u00b2", "facts/com\u00b3.md"]) {
|
|
@@ -926,7 +914,7 @@ test("an empty namespace list still validates its options", () => {
|
|
|
926
914
|
`options ${Object.prototype.toString.call(bad)}`,
|
|
927
915
|
);
|
|
928
916
|
}
|
|
929
|
-
assert.equal(planReconciliation([], { conflictPolicy: "
|
|
917
|
+
assert.equal(planReconciliation([], { conflictPolicy: "manual" }).converged, true);
|
|
930
918
|
});
|
|
931
919
|
|
|
932
920
|
test("sharp-S aliases fold together", () => {
|
package/src/reconcile/plan.ts
CHANGED
|
@@ -2,6 +2,11 @@ import { normalizeNamespaceIdentity } from "../namespaces/identity.js";
|
|
|
2
2
|
import { OFFLINE_SYNC_MAX_MTIME_MS } from "../offline-sync.js";
|
|
3
3
|
import { validateArchiveRelativePath } from "../transfer/fs-utils.js";
|
|
4
4
|
import type { OfflineSyncFileState } from "../offline-sync.js";
|
|
5
|
+
import {
|
|
6
|
+
CONVERGE_CONFLICT_POLICIES,
|
|
7
|
+
DEFAULT_CONVERGE_CONFLICT_POLICY,
|
|
8
|
+
} from "../converge-config.js";
|
|
9
|
+
import type { ConvergeConflictPolicy } from "../types.js";
|
|
5
10
|
|
|
6
11
|
/**
|
|
7
12
|
* Bootstrap reconciliation planner for two peer daemons whose corpora have
|
|
@@ -24,21 +29,11 @@ import type { OfflineSyncFileState } from "../offline-sync.js";
|
|
|
24
29
|
/** Which way a path must move for the two corpora to agree. */
|
|
25
30
|
export type ReconcileAction = "pull" | "push" | "identical" | "conflict" | "suppress";
|
|
26
31
|
|
|
27
|
-
/**
|
|
28
|
-
* How a path present on BOTH sides with different content is settled.
|
|
29
|
-
*
|
|
30
|
-
* `manual` is the safe default: on a bootstrap merge the two sides are equally
|
|
31
|
-
* authoritative, so silently picking one is data loss with extra steps. An
|
|
32
|
-
* operator opts into an automatic rule once they know the shape of their split.
|
|
33
|
-
*/
|
|
34
|
-
export type ReconcileConflictPolicy = "manual" | "newest-wins" | "keep-both";
|
|
35
|
-
|
|
36
32
|
/**
|
|
37
33
|
* What the planner decided for a conflicting path.
|
|
38
|
-
*
|
|
39
|
-
*
|
|
40
|
-
*
|
|
41
|
-
* fact neither corpus has seen.
|
|
34
|
+
* `supersede-link` records that both revisions need durable preservation when
|
|
35
|
+
* timestamps cannot safely select a winner. Apply must stop until transport
|
|
36
|
+
* can assign distinct durable identities to both revisions.
|
|
42
37
|
*/
|
|
43
38
|
export type ReconcileResolution = "local-wins" | "peer-wins" | "supersede-link" | "unresolved";
|
|
44
39
|
|
|
@@ -61,15 +56,6 @@ export interface ReconcilePlanEntry {
|
|
|
61
56
|
* delete the live copy instead of the retracted one.
|
|
62
57
|
*/
|
|
63
58
|
suppressSide?: "local" | "peer" | "both";
|
|
64
|
-
/**
|
|
65
|
-
* Which revision is newer, set only for a `supersede-link` resolution.
|
|
66
|
-
*
|
|
67
|
-
* The contract is that the older revision is linked as superseded by the
|
|
68
|
-
* newer one, so transport needs the direction. Absent when the timestamps
|
|
69
|
-
* cannot order the two - which is exactly when `newest-wins` degrades to
|
|
70
|
-
* `supersede-link` - and the link direction is then an operator decision.
|
|
71
|
-
*/
|
|
72
|
-
newerSide?: "local" | "peer";
|
|
73
59
|
}
|
|
74
60
|
|
|
75
61
|
export type ReconcileReason =
|
|
@@ -79,6 +65,7 @@ export type ReconcileReason =
|
|
|
79
65
|
| "peer_changed"
|
|
80
66
|
| "local_changed"
|
|
81
67
|
| "same_content"
|
|
68
|
+
| "semantic_duplicate"
|
|
82
69
|
| "both_modified"
|
|
83
70
|
| "peer_deleted"
|
|
84
71
|
| "local_deleted"
|
|
@@ -130,6 +117,14 @@ export interface ReconcileNamespaceInput {
|
|
|
130
117
|
* read as "never seen" rather than "deleted".
|
|
131
118
|
*/
|
|
132
119
|
base?: Iterable<ReconcileFileState>;
|
|
120
|
+
/**
|
|
121
|
+
* Revision timestamps for paths deleted locally since the base cursor.
|
|
122
|
+
* A deletion is a revision and must carry a comparable time for
|
|
123
|
+
* `newest-wins`; absence alone has no timestamp.
|
|
124
|
+
*/
|
|
125
|
+
localDeletionMtimeMs?: ReadonlyMap<string, number>;
|
|
126
|
+
/** Revision timestamps for paths deleted by the peer since the base cursor. */
|
|
127
|
+
peerDeletionMtimeMs?: ReadonlyMap<string, number>;
|
|
133
128
|
/**
|
|
134
129
|
* Digests of FILES this side has retracted, in the same form as
|
|
135
130
|
* `ReconcileFileState.sha256` — a hash of the serialized file.
|
|
@@ -162,7 +157,7 @@ export interface ReconcileNamespaceInput {
|
|
|
162
157
|
}
|
|
163
158
|
|
|
164
159
|
export interface ReconcileOptions {
|
|
165
|
-
conflictPolicy?:
|
|
160
|
+
conflictPolicy?: ConvergeConflictPolicy;
|
|
166
161
|
}
|
|
167
162
|
|
|
168
163
|
/**
|
|
@@ -472,13 +467,11 @@ function indexDigestsByPath(
|
|
|
472
467
|
return index;
|
|
473
468
|
}
|
|
474
469
|
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
if (value === undefined) return "manual";
|
|
479
|
-
if (!RECONCILE_CONFLICT_POLICIES.includes(value)) {
|
|
470
|
+
function assertConflictPolicy(value: ConvergeConflictPolicy | undefined): ConvergeConflictPolicy {
|
|
471
|
+
if (value === undefined) return DEFAULT_CONVERGE_CONFLICT_POLICY;
|
|
472
|
+
if (!CONVERGE_CONFLICT_POLICIES.includes(value)) {
|
|
480
473
|
throw new ReconcilePlanInputError(
|
|
481
|
-
`reconcile: unknown conflictPolicy ${JSON.stringify(value)}; expected one of ${
|
|
474
|
+
`reconcile: unknown conflictPolicy ${JSON.stringify(value)}; expected one of ${CONVERGE_CONFLICT_POLICIES.join(", ")}`,
|
|
482
475
|
);
|
|
483
476
|
}
|
|
484
477
|
return value;
|
|
@@ -496,36 +489,42 @@ function compareEntries(a: ReconcilePlanEntry, b: ReconcilePlanEntry): number {
|
|
|
496
489
|
return 0;
|
|
497
490
|
}
|
|
498
491
|
|
|
499
|
-
/**
|
|
500
|
-
* Direction for a supersede link, when the timestamps can order the pair.
|
|
501
|
-
* Omitted otherwise so an unordered link is visibly unordered rather than
|
|
502
|
-
* silently defaulted to one side.
|
|
503
|
-
*/
|
|
504
|
-
function newerSideOf(
|
|
505
|
-
local: ReconcileFileState,
|
|
506
|
-
peer: ReconcileFileState,
|
|
507
|
-
): { newerSide?: "local" | "peer" } {
|
|
508
|
-
const localMs = local.mtimeMs;
|
|
509
|
-
const peerMs = peer.mtimeMs;
|
|
510
|
-
if (typeof localMs !== "number" || typeof peerMs !== "number" || localMs === peerMs) return {};
|
|
511
|
-
return { newerSide: localMs > peerMs ? "local" : "peer" };
|
|
512
|
-
}
|
|
513
492
|
|
|
514
493
|
function resolveConflict(
|
|
515
|
-
policy:
|
|
516
|
-
|
|
517
|
-
|
|
494
|
+
policy: ConvergeConflictPolicy,
|
|
495
|
+
localMtimeMs: number | undefined,
|
|
496
|
+
peerMtimeMs: number | undefined,
|
|
497
|
+
missingTimestampResolution: ReconcileResolution,
|
|
518
498
|
): ReconcileResolution {
|
|
519
|
-
if (policy === "keep-both") return "supersede-link";
|
|
520
499
|
if (policy !== "newest-wins") return "unresolved";
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
500
|
+
if (localMtimeMs === undefined || peerMtimeMs === undefined || localMtimeMs === peerMtimeMs) {
|
|
501
|
+
return missingTimestampResolution;
|
|
502
|
+
}
|
|
503
|
+
return localMtimeMs > peerMtimeMs ? "local-wins" : "peer-wins";
|
|
504
|
+
}
|
|
505
|
+
|
|
506
|
+
function parseDeletionMtimes(
|
|
507
|
+
value: ReadonlyMap<string, number> | undefined,
|
|
508
|
+
side: string,
|
|
509
|
+
namespace: string,
|
|
510
|
+
): ReadonlyMap<string, number> {
|
|
511
|
+
if (value === undefined) return new Map();
|
|
512
|
+
if (!(value instanceof Map)) {
|
|
513
|
+
throw new ReconcilePlanInputError(
|
|
514
|
+
`reconcile: ${side} deletion mtimes for namespace ${namespace} must be a Map`,
|
|
515
|
+
);
|
|
516
|
+
}
|
|
517
|
+
const parsed = new Map<string, number>();
|
|
518
|
+
for (const [path, mtimeMs] of value) {
|
|
519
|
+
try {
|
|
520
|
+
validateArchiveRelativePath(path, `reconcile: ${side} deletion mtimes for namespace ${namespace}`);
|
|
521
|
+
} catch (err) {
|
|
522
|
+
throw new ReconcilePlanInputError(err instanceof Error ? err.message : String(err));
|
|
523
|
+
}
|
|
524
|
+
assertPortablePathSegments(path, side, namespace);
|
|
525
|
+
parsed.set(path, assertMtimeMs(mtimeMs, `${side} deletion for namespace ${namespace} entry ${path}`));
|
|
526
|
+
}
|
|
527
|
+
return parsed;
|
|
529
528
|
}
|
|
530
529
|
|
|
531
530
|
/**
|
|
@@ -571,6 +570,8 @@ export function planNamespaceReconciliation(
|
|
|
571
570
|
namespace,
|
|
572
571
|
"peerTombstonedFileSha256",
|
|
573
572
|
);
|
|
573
|
+
const localDeletionMtimeMs = parseDeletionMtimes(input.localDeletionMtimeMs, "local", namespace);
|
|
574
|
+
const peerDeletionMtimeMs = parseDeletionMtimes(input.peerDeletionMtimeMs, "peer", namespace);
|
|
574
575
|
const entries: ReconcilePlanEntry[] = [];
|
|
575
576
|
|
|
576
577
|
// Path -> digest only, never the file objects: enough to make the stream
|
|
@@ -638,7 +639,14 @@ export function planNamespaceReconciliation(
|
|
|
638
639
|
reason: baseSha256 === localFile.sha256 ? "peer_deleted" : "local_modified_peer_deleted",
|
|
639
640
|
localSha256: localFile.sha256,
|
|
640
641
|
baseSha256,
|
|
641
|
-
resolution:
|
|
642
|
+
resolution: baseSha256 === localFile.sha256
|
|
643
|
+
? "unresolved"
|
|
644
|
+
: resolveConflict(
|
|
645
|
+
policy,
|
|
646
|
+
localFile.mtimeMs,
|
|
647
|
+
peerDeletionMtimeMs.get(path),
|
|
648
|
+
"unresolved",
|
|
649
|
+
),
|
|
642
650
|
});
|
|
643
651
|
continue;
|
|
644
652
|
}
|
|
@@ -689,7 +697,12 @@ export function planNamespaceReconciliation(
|
|
|
689
697
|
});
|
|
690
698
|
continue;
|
|
691
699
|
}
|
|
692
|
-
const resolution = resolveConflict(
|
|
700
|
+
const resolution = resolveConflict(
|
|
701
|
+
policy,
|
|
702
|
+
localFile.mtimeMs,
|
|
703
|
+
peerFile.mtimeMs,
|
|
704
|
+
"supersede-link",
|
|
705
|
+
);
|
|
693
706
|
entries.push({
|
|
694
707
|
path,
|
|
695
708
|
namespace,
|
|
@@ -699,7 +712,6 @@ export function planNamespaceReconciliation(
|
|
|
699
712
|
peerSha256: peerFile.sha256,
|
|
700
713
|
...(baseSha256 === undefined ? {} : { baseSha256 }),
|
|
701
714
|
resolution,
|
|
702
|
-
...(resolution === "supersede-link" ? newerSideOf(localFile, peerFile) : {}),
|
|
703
715
|
});
|
|
704
716
|
}
|
|
705
717
|
|
|
@@ -731,7 +743,14 @@ export function planNamespaceReconciliation(
|
|
|
731
743
|
reason: baseSha256 === peerFile.sha256 ? "local_deleted" : "local_deleted_peer_modified",
|
|
732
744
|
peerSha256: peerFile.sha256,
|
|
733
745
|
baseSha256,
|
|
734
|
-
resolution:
|
|
746
|
+
resolution: baseSha256 === peerFile.sha256
|
|
747
|
+
? "unresolved"
|
|
748
|
+
: resolveConflict(
|
|
749
|
+
policy,
|
|
750
|
+
localDeletionMtimeMs.get(path),
|
|
751
|
+
peerFile.mtimeMs,
|
|
752
|
+
"unresolved",
|
|
753
|
+
),
|
|
735
754
|
});
|
|
736
755
|
continue;
|
|
737
756
|
}
|
|
@@ -757,12 +776,9 @@ export function summarizeReconcilePlan(entries: readonly ReconcilePlanEntry[]):
|
|
|
757
776
|
byNamespace.set(entry.namespace, report);
|
|
758
777
|
}
|
|
759
778
|
report[entry.action] += 1;
|
|
760
|
-
// A supersede link with no `newerSide` could not be ordered, and the link
|
|
761
|
-
// direction is then an operator decision - so it counts as unresolved even
|
|
762
|
-
// though the policy nominally settled it.
|
|
763
779
|
const needsOperator =
|
|
764
780
|
entry.resolution === "unresolved"
|
|
765
|
-
||
|
|
781
|
+
|| entry.resolution === "supersede-link";
|
|
766
782
|
if (entry.action === "conflict" && needsOperator) report.unresolved += 1;
|
|
767
783
|
}
|
|
768
784
|
return [...byNamespace.values()].sort((a, b) => (a.namespace === b.namespace ? 0 : a.namespace < b.namespace ? -1 : 1));
|