@remnic/core 9.69.11 → 9.69.13
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/access-admin-ops-surface.d.ts +11 -11
- package/dist/access-admin-ops-surface.js +4 -4
- package/dist/access-authorization-probe.d.ts +11 -11
- package/dist/access-authorization-probe.js +5 -5
- package/dist/access-boundary.d.ts +11 -11
- package/dist/access-boundary.js +4 -4
- package/dist/access-cli.js +28 -28
- package/dist/access-coding-context-resolution.d.ts +1 -1
- package/dist/access-extraction-force-flush.d.ts +11 -11
- package/dist/access-extraction-force-flush.js +4 -4
- package/dist/access-health-types.d.ts +1 -1
- package/dist/access-http-lcm-compaction.d.ts +11 -11
- package/dist/access-http-lifecycle-flush.d.ts +11 -11
- package/dist/access-http-offline-stream.d.ts +11 -11
- package/dist/access-http-query.d.ts +2 -2
- package/dist/access-http-query.js +4 -4
- package/dist/access-http.d.ts +12 -12
- package/dist/access-http.js +15 -15
- package/dist/access-identity-continuity-surface.d.ts +9 -9
- package/dist/access-identity-continuity-surface.js +4 -4
- package/dist/access-lcm-surface.d.ts +11 -11
- package/dist/access-lcm-surface.js +4 -4
- package/dist/access-mcp.d.ts +11 -11
- package/dist/access-mcp.js +11 -11
- package/dist/access-memory-search-fanout.d.ts +2 -2
- package/dist/{access-namespace-preflight-Cy0yPsaE.d.ts → access-namespace-preflight-Rzuo1nGR.d.ts} +1 -1
- package/dist/access-namespace-preflight.d.ts +3 -3
- package/dist/access-namespace-preflight.js +4 -4
- package/dist/access-observe-write-surface.d.ts +11 -11
- package/dist/access-observe-write-surface.js +4 -4
- package/dist/access-offline-manifest.d.ts +9 -9
- package/dist/access-operations-batch.js +5 -5
- package/dist/access-operations.d.ts +17 -17
- package/dist/access-operations.js +10 -10
- package/dist/access-recall-concurrency.d.ts +11 -11
- package/dist/access-recall-concurrency.js +4 -4
- package/dist/access-recall-response.d.ts +11 -11
- package/dist/access-recall-response.js +4 -4
- package/dist/access-recall-surface.d.ts +11 -11
- package/dist/access-recall-surface.js +4 -4
- package/dist/access-schema.d.ts +91 -91
- package/dist/{access-service-Cqfk3dD1.d.ts → access-service-Bb2Fykzc.d.ts} +71 -71
- package/dist/access-service-helpers.d.ts +11 -11
- package/dist/access-service.d.ts +11 -11
- package/dist/access-service.js +4 -4
- package/dist/access-surface-catalog.d.ts +11 -11
- package/dist/access-surface-catalog.js +6 -6
- 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 +4 -4
- package/dist/ambient-provenance.d.ts +1 -1
- package/dist/artifact-search.d.ts +1 -1
- package/dist/behavior-learner.d.ts +1 -1
- package/dist/behavior-signals.d.ts +1 -1
- package/dist/bootstrap.d.ts +9 -9
- package/dist/briefing-window.d.ts +2 -2
- package/dist/briefing.d.ts +2 -2
- package/dist/buffer-surprise-report.d.ts +1 -1
- package/dist/buffer-turn-helpers.d.ts +1 -1
- package/dist/buffer.d.ts +2 -2
- package/dist/buffer.js +4 -4
- 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-9X8WpZNO.d.ts → catalog-DjTLVOYp.d.ts} +1 -1
- package/dist/causal-behavior.d.ts +1 -1
- package/dist/causal-consolidation.d.ts +1 -1
- package/dist/causal-trajectory-graph.d.ts +1 -1
- package/dist/{chunk-TEZ2HTC7.js → chunk-33JKYLPX.js} +2 -2
- package/dist/{chunk-MNH2GM4O.js → chunk-3BS2OGDT.js} +6 -19
- package/dist/{chunk-MNH2GM4O.js.map → chunk-3BS2OGDT.js.map} +1 -1
- package/dist/{chunk-5PTS43CH.js → chunk-4LG4CPOC.js} +2 -2
- package/dist/{chunk-Z33VUMKY.js → chunk-5H2G5X4R.js} +2 -2
- package/dist/{chunk-XFPHV65N.js → chunk-5JQV4T5Q.js} +2 -2
- package/dist/{chunk-OASGWNYL.js → chunk-63I7LMLG.js} +2 -2
- package/dist/{chunk-J3NGQ5SC.js → chunk-7GSSHAXC.js} +2 -2
- package/dist/{chunk-X4FIZAVQ.js → chunk-7JV4JST3.js} +854 -42
- package/dist/chunk-7JV4JST3.js.map +1 -0
- package/dist/{chunk-VHAKBUBD.js → chunk-B64IVEOH.js} +2 -2
- package/dist/{chunk-R7FXPG4T.js → chunk-C25N5HWX.js} +2 -2
- package/dist/{chunk-DH3V7RCQ.js → chunk-F2ERDKY2.js} +7 -7
- package/dist/{chunk-LJNLLNEM.js → chunk-HODFGQGT.js} +2 -2
- package/dist/{chunk-B75VCEX5.js → chunk-K2IJH4D3.js} +2 -2
- package/dist/{chunk-227WJVPY.js → chunk-KLSPKMIO.js} +2 -2
- package/dist/{chunk-5P2KNFJZ.js → chunk-MSDIETFC.js} +3 -3
- package/dist/{chunk-HFRGCNFY.js → chunk-OAKATRAN.js} +2 -2
- package/dist/chunk-OGUKSLYZ.js +560 -0
- package/dist/chunk-OGUKSLYZ.js.map +1 -0
- package/dist/{chunk-M6L7HI7R.js → chunk-P6FHFTZ7.js} +16 -1
- package/dist/chunk-P6FHFTZ7.js.map +1 -0
- package/dist/{chunk-LWBSEYW3.js → chunk-PAWIY5OQ.js} +5 -5
- package/dist/{chunk-3ITTF7YH.js → chunk-QVI63XSO.js} +90 -383
- package/dist/chunk-QVI63XSO.js.map +1 -0
- package/dist/{chunk-6TOWUN2R.js → chunk-RE3H6Y2X.js} +2 -2
- package/dist/{chunk-V3HUBF7T.js → chunk-S35IIP5P.js} +2 -2
- package/dist/{chunk-3YKOFCKP.js → chunk-SXHBZXHK.js} +2 -2
- package/dist/{chunk-7NCUHD5U.js → chunk-VAMZYOOV.js} +2 -2
- package/dist/{chunk-PGCZLHRB.js → chunk-W272SWN7.js} +3 -3
- package/dist/{chunk-5HO3QBAD.js → chunk-W6GEMUZM.js} +2 -2
- package/dist/{chunk-UCNQV43K.js → chunk-XQYTSZW3.js} +2 -2
- package/dist/{chunk-GR4C3FHT.js → chunk-ZKZCJRRO.js} +2 -2
- package/dist/{cli-Dk-lM_tI.d.ts → cli-DXXy60A4.d.ts} +5 -5
- package/dist/cli.d.ts +13 -13
- package/dist/cli.js +21 -21
- package/dist/coding/export-okf-codegraph.d.ts +1 -1
- package/dist/coding/pre-action-gate.d.ts +1 -1
- package/dist/compounding/engine.d.ts +2 -2
- 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 +4 -4
- package/dist/connectors/codex-materialize-runner.d.ts +1 -1
- package/dist/connectors/codex-materialize.d.ts +1 -1
- package/dist/connectors/index.d.ts +1 -1
- package/dist/consolidation-provenance-check.d.ts +2 -2
- package/dist/consolidation-undo.d.ts +2 -2
- package/dist/contradiction/index.d.ts +3 -3
- package/dist/converge-config.d.ts +1 -1
- package/dist/convergence-refresh.d.ts +2 -2
- 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/cross-lingual-recall.d.ts +1 -1
- package/dist/day-summary.d.ts +1 -1
- package/dist/deep-recall-http-glue.d.ts +11 -11
- package/dist/deep-recall-mcp-tools.d.ts +11 -11
- package/dist/deep-recall-policy-llm.d.ts +1 -1
- package/dist/deep-recall-seeds.d.ts +1 -1
- package/dist/delinearize.d.ts +1 -1
- package/dist/dependency-propagation-config.d.ts +1 -1
- package/dist/{dependency-propagation-delivery-BZGUecZf.d.ts → dependency-propagation-delivery-37JdQv_3.d.ts} +1 -1
- package/dist/direct-answer-wiring.d.ts +1 -1
- package/dist/direct-answer.d.ts +1 -1
- package/dist/embedding-fallback.d.ts +1 -1
- package/dist/enrichment/index.d.ts +1 -1
- package/dist/entity-origin-fields.d.ts +1 -1
- package/dist/entity-retrieval.d.ts +2 -2
- package/dist/entity-schema.d.ts +1 -1
- package/dist/episodic-context.d.ts +1 -1
- package/dist/eval-trajectory-contracts.d.ts +1 -1
- package/dist/eval-trajectory-store.d.ts +1 -1
- package/dist/eval-trajectory.d.ts +1 -1
- package/dist/explicit-capture.d.ts +9 -9
- package/dist/external-wiki-access.d.ts +11 -11
- package/dist/external-wiki-access.js +5 -5
- package/dist/external-wiki-collection-registration.d.ts +1 -1
- package/dist/external-wiki-collection.d.ts +1 -1
- package/dist/external-wiki-mcp-tools.d.ts +11 -11
- package/dist/extraction-error-classification.d.ts +1 -1
- package/dist/extraction-faithfulness.d.ts +1 -1
- package/dist/extraction-judge-telemetry.d.ts +1 -1
- package/dist/extraction-judge-training.d.ts +1 -1
- package/dist/extraction-judge.d.ts +1 -1
- package/dist/extraction-liveness.d.ts +1 -1
- package/dist/extraction-normalization.d.ts +1 -1
- package/dist/extraction-prompt.d.ts +1 -1
- package/dist/extraction-source-grounding-rules.d.ts +1 -1
- package/dist/extraction-source-grounding.d.ts +1 -1
- package/dist/extraction.d.ts +1 -1
- package/dist/extraction.js +5 -5
- package/dist/fallback-llm.d.ts +1 -1
- package/dist/graph-dashboard-diff.d.ts +1 -1
- package/dist/graph-dashboard-key.d.ts +1 -1
- package/dist/graph-dashboard-parser.d.ts +1 -1
- package/dist/graph-edge-reinforcement.d.ts +1 -1
- package/dist/graph-path-reconstruction.d.ts +1 -1
- package/dist/graph-path-scoring.d.ts +1 -1
- package/dist/graph-snapshot.d.ts +1 -1
- package/dist/graph.d.ts +1 -1
- package/dist/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-B4iTvT-w.d.ts → index-DquhYqsc.d.ts} +2 -2
- package/dist/index.d.ts +876 -738
- package/dist/index.js +59 -37
- package/dist/intent.d.ts +1 -1
- package/dist/lcm/engine.d.ts +1 -1
- package/dist/lcm/index.d.ts +1 -1
- package/dist/lcm/tools.d.ts +1 -1
- package/dist/lifecycle.d.ts +1 -1
- package/dist/live-connectors-runner.d.ts +1 -1
- package/dist/local-llm.d.ts +1 -1
- package/dist/local-model-endpoint.d.ts +1 -1
- package/dist/location/index.d.ts +3 -3
- package/dist/location/index.js +5 -5
- package/dist/maintenance/memory-governance.d.ts +1 -1
- package/dist/maintenance/rebuild-memory-lifecycle-ledger.d.ts +2 -2
- package/dist/maintenance/rebuild-memory-projection.d.ts +2 -2
- package/dist/{maintenance-BBpNqEeJ.d.ts → maintenance-CizZOycr.d.ts} +3 -3
- package/dist/mcp-memory-inspector-app.d.ts +11 -11
- package/dist/memory-action-policy.d.ts +1 -1
- package/dist/memory-cache.d.ts +1 -1
- package/dist/memory-controller.d.ts +1 -1
- package/dist/memory-lifecycle-ledger-utils.d.ts +1 -1
- package/dist/memory-projection-store.d.ts +1 -1
- package/dist/memory-provenance.d.ts +1 -1
- package/dist/memory-snapshot.d.ts +1 -1
- package/dist/memory-subject-cli.d.ts +11 -11
- package/dist/memory-subject.d.ts +11 -11
- 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/principal.d.ts +1 -1
- package/dist/namespaces/search.d.ts +1 -1
- package/dist/namespaces/storage.d.ts +3 -3
- package/dist/native-knowledge.d.ts +1 -1
- package/dist/offline-sync-impression-drain.d.ts +1 -1
- package/dist/operator-doctor-corpus.d.ts +1 -1
- package/dist/operator-doctor-replica.d.ts +1 -1
- package/dist/operator-toolkit.d.ts +3 -3
- package/dist/operator-toolkit.js +5 -5
- package/dist/orchestration/compression-guideline-coordinator.d.ts +2 -2
- package/dist/orchestration/maintenance.d.ts +4 -4
- package/dist/orchestration/maintenance.js +6 -6
- package/dist/{orchestrator-DglY1H0w.d.ts → orchestrator-DqLcuNMP.d.ts} +8 -8
- package/dist/orchestrator.d.ts +9 -9
- package/dist/orchestrator.js +28 -28
- package/dist/patterns-cli.d.ts +1 -1
- package/dist/{pipeline-C7qRsy6g.d.ts → pipeline-DbJpmcRu.d.ts} +1 -1
- package/dist/policy-runtime.d.ts +1 -1
- package/dist/proactive-contention.d.ts +1 -1
- package/dist/provenance.d.ts +1 -1
- package/dist/{public-http-CPpCsi35.d.ts → public-http-NQ3UUp6W.d.ts} +1 -1
- package/dist/{qmd-YRLTa3AE.d.ts → qmd-DA_7fbSz.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-memory-map.d.ts +1 -1
- package/dist/recall-navigate-access.d.ts +1 -1
- package/dist/recall-navigate-disclosure.d.ts +1 -1
- package/dist/recall-navigate.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-renderer.d.ts +1 -1
- package/dist/recall-xray.d.ts +1 -1
- package/dist/reconcile/cursor.d.ts +1 -1
- package/dist/reconcile/manifest.d.ts +1 -1
- package/dist/reconcile/plan.d.ts +1 -1
- package/dist/replay/normalizers/chatgpt.d.ts +1 -1
- package/dist/replay/normalizers/claude.d.ts +1 -1
- package/dist/replay/normalizers/openclaw.d.ts +1 -1
- package/dist/replay/normalizers/shared.d.ts +1 -1
- package/dist/replay/runner.d.ts +1 -1
- package/dist/replay/types.d.ts +1 -1
- package/dist/replica-divergence.d.ts +1 -1
- package/dist/replica-peers-config.d.ts +1 -1
- package/dist/resolve-auth-token.d.ts +1 -1
- package/dist/resume-bundles.js +5 -5
- package/dist/retrieval-agents.d.ts +2 -2
- package/dist/retrieval-tiers.d.ts +1 -1
- package/dist/routing/engine.d.ts +1 -1
- package/dist/routing/store.d.ts +1 -1
- package/dist/salvage-envelope.d.ts +1 -1
- package/dist/schemas.d.ts +176 -176
- package/dist/{scope-profiles-BVvqVzJX.d.ts → scope-profiles-BpiWKZzx.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-BzCI6Kuu.d.ts → semantic-consolidation-NJpaZpkY.d.ts} +1 -1
- package/dist/semantic-consolidation.d.ts +2 -2
- package/dist/semantic-rule-verifier.d.ts +1 -1
- package/dist/{service-DQ3Gscy5.d.ts → service-DdlZjX1D.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 +7 -7
- package/dist/signal.d.ts +1 -1
- package/dist/source-agent-qualifier.d.ts +1 -1
- package/dist/source-agent-qualifier.js +4 -4
- package/dist/standup/build.js +2 -2
- package/dist/{storage-DmTbQdD0.d.ts → storage-C7BWrpU9.d.ts} +1 -1
- package/dist/storage.d.ts +2 -2
- package/dist/subject-config.d.ts +1 -1
- package/dist/summarizer.d.ts +1 -1
- package/dist/summary-snapshot.d.ts +1 -1
- package/dist/support-passport/index.d.ts +14 -14
- package/dist/support-passport/index.js +7 -7
- package/dist/temporal-supersession.d.ts +2 -2
- package/dist/temporal-timeline-recall.d.ts +1 -1
- package/dist/temporal-validity.d.ts +1 -1
- package/dist/threading.d.ts +1 -1
- package/dist/tier-migration.d.ts +2 -2
- package/dist/tier-routing.d.ts +1 -1
- package/dist/topics.d.ts +1 -1
- package/dist/transcript.d.ts +1 -1
- package/dist/transfer/export-okf.d.ts +1 -1
- package/dist/transfer/types.d.ts +78 -78
- package/dist/trust-score-stage.d.ts +1 -1
- package/dist/trust-score.d.ts +1 -1
- package/dist/{types-CLRTESSD.d.ts → types-Cy1l6fzp.d.ts} +48 -1
- package/dist/types.d.ts +1 -1
- package/dist/utility-runtime.d.ts +1 -1
- package/dist/who-knows-cli.d.ts +11 -11
- package/dist/who-knows.d.ts +11 -11
- package/dist/write-envelope.d.ts +1 -1
- package/package.json +2 -2
- package/src/activity/config.test.ts +94 -1
- package/src/activity/config.ts +256 -3
- package/src/activity/digest.ts +21 -0
- package/src/activity/index.ts +2 -0
- package/src/activity/runner.ts +2 -17
- package/src/activity/timeline/publish-cli.test.ts +169 -0
- package/src/activity/timeline/publish-cli.ts +144 -0
- package/src/activity/timeline/query.ts +51 -15
- package/src/activity/types.ts +45 -0
- package/src/activity/vault-insert.ts +14 -33
- package/src/activity/vault-path.ts +100 -22
- package/src/activity/vault-publish.test.ts +51 -0
- package/src/activity/vault-publish.ts +157 -17
- package/src/activity/vault-publisher.test.ts +881 -0
- package/src/activity/vault-publisher.ts +597 -0
- package/src/activity/vault-region.test.ts +5 -0
- package/src/activity/vault-region.ts +7 -2
- package/src/cli/timeline-publish-commands.ts +34 -0
- package/src/cli.ts +2 -2
- package/src/config.test.ts +24 -1
- package/dist/chunk-3ITTF7YH.js.map +0 -1
- package/dist/chunk-KJUWWN3T.js +0 -237
- package/dist/chunk-KJUWWN3T.js.map +0 -1
- package/dist/chunk-M6L7HI7R.js.map +0 -1
- package/dist/chunk-X4FIZAVQ.js.map +0 -1
- /package/dist/{chunk-TEZ2HTC7.js.map → chunk-33JKYLPX.js.map} +0 -0
- /package/dist/{chunk-5PTS43CH.js.map → chunk-4LG4CPOC.js.map} +0 -0
- /package/dist/{chunk-Z33VUMKY.js.map → chunk-5H2G5X4R.js.map} +0 -0
- /package/dist/{chunk-XFPHV65N.js.map → chunk-5JQV4T5Q.js.map} +0 -0
- /package/dist/{chunk-OASGWNYL.js.map → chunk-63I7LMLG.js.map} +0 -0
- /package/dist/{chunk-J3NGQ5SC.js.map → chunk-7GSSHAXC.js.map} +0 -0
- /package/dist/{chunk-VHAKBUBD.js.map → chunk-B64IVEOH.js.map} +0 -0
- /package/dist/{chunk-R7FXPG4T.js.map → chunk-C25N5HWX.js.map} +0 -0
- /package/dist/{chunk-DH3V7RCQ.js.map → chunk-F2ERDKY2.js.map} +0 -0
- /package/dist/{chunk-LJNLLNEM.js.map → chunk-HODFGQGT.js.map} +0 -0
- /package/dist/{chunk-B75VCEX5.js.map → chunk-K2IJH4D3.js.map} +0 -0
- /package/dist/{chunk-227WJVPY.js.map → chunk-KLSPKMIO.js.map} +0 -0
- /package/dist/{chunk-5P2KNFJZ.js.map → chunk-MSDIETFC.js.map} +0 -0
- /package/dist/{chunk-HFRGCNFY.js.map → chunk-OAKATRAN.js.map} +0 -0
- /package/dist/{chunk-LWBSEYW3.js.map → chunk-PAWIY5OQ.js.map} +0 -0
- /package/dist/{chunk-6TOWUN2R.js.map → chunk-RE3H6Y2X.js.map} +0 -0
- /package/dist/{chunk-V3HUBF7T.js.map → chunk-S35IIP5P.js.map} +0 -0
- /package/dist/{chunk-3YKOFCKP.js.map → chunk-SXHBZXHK.js.map} +0 -0
- /package/dist/{chunk-7NCUHD5U.js.map → chunk-VAMZYOOV.js.map} +0 -0
- /package/dist/{chunk-PGCZLHRB.js.map → chunk-W272SWN7.js.map} +0 -0
- /package/dist/{chunk-5HO3QBAD.js.map → chunk-W6GEMUZM.js.map} +0 -0
- /package/dist/{chunk-UCNQV43K.js.map → chunk-XQYTSZW3.js.map} +0 -0
- /package/dist/{chunk-GR4C3FHT.js.map → chunk-ZKZCJRRO.js.map} +0 -0
|
@@ -0,0 +1,597 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Vault publisher service (issue #1985): composes the landed primitives —
|
|
3
|
+
* path templates (`vault-path.ts`), managed regions (`vault-publish.ts`),
|
|
4
|
+
* marker insertion (`vault-insert.ts`), region-name validation
|
|
5
|
+
* (`vault-region.ts`), begin/end pair assertion (`vault-region-pair.ts`) —
|
|
6
|
+
* into one publish pass over a single vault note.
|
|
7
|
+
*
|
|
8
|
+
* Guarantees: everything outside the managed region (and, in frontmatter
|
|
9
|
+
* mode, everything except `prefix`-owned keys) is byte-identical after
|
|
10
|
+
* publish; unchanged content produces no write; all writes are temp-file +
|
|
11
|
+
* rename in the note's own directory; dry-run performs zero writes; a
|
|
12
|
+
* destination that changed after it was read aborts with `concurrent_write`
|
|
13
|
+
* instead of clobbering an editor's concurrent save; symlinked paths that
|
|
14
|
+
* resolve outside the vault, and symlinked note files, are refused before
|
|
15
|
+
* any write.
|
|
16
|
+
*/
|
|
17
|
+
import { createHash, randomBytes } from "node:crypto";
|
|
18
|
+
import { chmodSync, chownSync, lstatSync, mkdirSync, readFileSync, realpathSync, renameSync, statSync, unlinkSync, writeFileSync, type Stats } from "node:fs";
|
|
19
|
+
import path from "node:path";
|
|
20
|
+
|
|
21
|
+
import { expandTildePath } from "../utils/path.js";
|
|
22
|
+
import { applyManagedRegion, fileLines } from "./vault-publish.js";
|
|
23
|
+
import { insertMarkersUnderHeading } from "./vault-insert.js";
|
|
24
|
+
import { validateRegionName } from "./vault-region.js";
|
|
25
|
+
import { assertBeginEndPair } from "./vault-region-pair.js";
|
|
26
|
+
import { expandVaultTemplateTokens, resolveVaultNotePath } from "./vault-path.js";
|
|
27
|
+
import { summarizeVaultPublish, type VaultPublishResult, type VaultPublishStatus } from "./vault-status.js";
|
|
28
|
+
|
|
29
|
+
export interface VaultSectionProvenance {
|
|
30
|
+
generatorVersion: string;
|
|
31
|
+
/** Local-time generation stamp of the artifact; supplied by the caller so republishing an unchanged artifact stays a no-op. */
|
|
32
|
+
generatedAt: string;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
export interface VaultSectionPublish {
|
|
36
|
+
/** Managed-region name (the heading name under the heading strategy). */
|
|
37
|
+
name: string;
|
|
38
|
+
/** Rendered artifact body the region owns. */
|
|
39
|
+
content: string;
|
|
40
|
+
/** Unprefixed stats, e.g. `{ focus_minutes: "220" }`; keys get the configured prefix. */
|
|
41
|
+
properties?: Readonly<Record<string, string>>;
|
|
42
|
+
/** When present, a provenance footer line is appended inside the region. */
|
|
43
|
+
provenance?: VaultSectionProvenance;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
export interface PublishVaultNoteInput {
|
|
47
|
+
/** Vault root; absolute or `~` path; must be an existing directory. */
|
|
48
|
+
vaultPath: string;
|
|
49
|
+
/** Vault-relative note path template (date tokens). */
|
|
50
|
+
notePathTemplate: string;
|
|
51
|
+
/** Local day, YYYY-MM-DD. */
|
|
52
|
+
date: string;
|
|
53
|
+
sections: readonly VaultSectionPublish[];
|
|
54
|
+
strategy?: "markers" | "heading";
|
|
55
|
+
/** Markers strategy: heading under which missing marker pairs are inserted. */
|
|
56
|
+
insertUnderHeading?: string;
|
|
57
|
+
createMissingNotes?: boolean;
|
|
58
|
+
/** Vault-relative template file used only when creating a missing note. */
|
|
59
|
+
noteTemplate?: string;
|
|
60
|
+
propertiesMode?: "off" | "frontmatter" | "dataview-inline";
|
|
61
|
+
/** Property key prefix, default `remnic_`. */
|
|
62
|
+
propertiesPrefix?: string;
|
|
63
|
+
dryRun?: boolean;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
// Bounded whitespace runs: an unbounded `\s*` chain around a lazy capture is the
|
|
67
|
+
// CodeQL js/polynomial-redos shape `check:regex-safety` rejects (issue #2439).
|
|
68
|
+
// The name is captured up to the TERMINAL `:start`/`:end` suffix, so a marker
|
|
69
|
+
// carrying a stray colon (`remnic:A:B:start`) is still seen by the scanner
|
|
70
|
+
// instead of being silently invisible to it. `validateRegionName` rejects `:`
|
|
71
|
+
// in a configured name, so such a marker can never be a legitimate pair and
|
|
72
|
+
// always resolves to a refusal below.
|
|
73
|
+
const START_MARKER_RE = /^<!--\s{0,8}remnic:(.+):start\s{0,8}-->$/;
|
|
74
|
+
const END_MARKER_RE = /^<!--\s{0,8}remnic:(.+):end\s{0,8}-->$/;
|
|
75
|
+
|
|
76
|
+
type SectionOutcome =
|
|
77
|
+
| { ok: true; text: string }
|
|
78
|
+
| { ok: false; outcome: "skipped" | "error"; reason: string };
|
|
79
|
+
|
|
80
|
+
export function publishVaultNote(input: PublishVaultNoteInput): VaultPublishStatus {
|
|
81
|
+
if (typeof input !== "object" || input === null) {
|
|
82
|
+
throw new TypeError("publishVaultNote requires an input object.");
|
|
83
|
+
}
|
|
84
|
+
if (!Array.isArray(input.sections) || input.sections.length === 0) {
|
|
85
|
+
throw new RangeError("publishVaultNote requires at least one section.");
|
|
86
|
+
}
|
|
87
|
+
const strategy = input.strategy ?? "markers";
|
|
88
|
+
const propertiesMode = input.propertiesMode ?? "off";
|
|
89
|
+
const prefix = input.propertiesPrefix ?? "remnic_";
|
|
90
|
+
if (typeof prefix !== "string" || prefix.length === 0) {
|
|
91
|
+
throw new RangeError("propertiesPrefix must be a non-empty string.");
|
|
92
|
+
}
|
|
93
|
+
for (const section of input.sections) {
|
|
94
|
+
const name = validateRegionName(section.name);
|
|
95
|
+
if (!name.ok) {
|
|
96
|
+
throw new RangeError(`Section name ${JSON.stringify(section.name)} is not a valid region name.`);
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
const relative = resolveVaultNotePath(input.notePathTemplate, input.date);
|
|
101
|
+
const result = publishRelative(input, relative, { strategy, propertiesMode, prefix });
|
|
102
|
+
return summarizeVaultPublish(result);
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
function publishRelative(
|
|
106
|
+
input: PublishVaultNoteInput,
|
|
107
|
+
relative: string,
|
|
108
|
+
opts: { strategy: "markers" | "heading"; propertiesMode: string; prefix: string },
|
|
109
|
+
): VaultPublishResult[] {
|
|
110
|
+
const vault = expandTildePath(input.vaultPath);
|
|
111
|
+
try {
|
|
112
|
+
// A symlinked vault root is rejected outright (security checklist §3):
|
|
113
|
+
// statSync follows links, lstatSync does not.
|
|
114
|
+
const rootStat = lstatSync(vault);
|
|
115
|
+
if (rootStat.isSymbolicLink() || !rootStat.isDirectory()) return refuse(relative, "error", "not_directory");
|
|
116
|
+
} catch {
|
|
117
|
+
return refuse(relative, "error", "not_directory");
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
const root = path.resolve(vault);
|
|
121
|
+
const dest = path.resolve(root, relative);
|
|
122
|
+
const lexical = path.relative(root, dest);
|
|
123
|
+
// `..notes/x.md` is an in-vault name, not parent traversal: only `..`
|
|
124
|
+
// exactly or a `..${path.sep}` prefix escapes.
|
|
125
|
+
if (lexical.length === 0 || lexical === ".." || lexical.startsWith(`..${path.sep}`) || path.isAbsolute(lexical)) {
|
|
126
|
+
return refuse(relative, "error", "path_escape");
|
|
127
|
+
}
|
|
128
|
+
if (!containedBySymlinks(root, dest)) {
|
|
129
|
+
return refuse(relative, "error", "symlink_escape");
|
|
130
|
+
}
|
|
131
|
+
let currentText: string | null = null;
|
|
132
|
+
let expected: DestExpectation | null = null;
|
|
133
|
+
try {
|
|
134
|
+
// A symlinked note is refused even when its target lives inside the
|
|
135
|
+
// vault (issue #1985): writeAtomic renames over the link itself, which
|
|
136
|
+
// would destroy the symlink and leave the target stale while the
|
|
137
|
+
// status still says `updated`.
|
|
138
|
+
if (lstatSync(dest).isSymbolicLink()) {
|
|
139
|
+
return refuse(relative, "error", "symlinked_note");
|
|
140
|
+
}
|
|
141
|
+
const st = statSync(dest);
|
|
142
|
+
if (st.isFile()) {
|
|
143
|
+
currentText = readFileSync(dest, "utf8");
|
|
144
|
+
expected = {
|
|
145
|
+
ino: st.ino,
|
|
146
|
+
mtimeMs: st.mtimeMs,
|
|
147
|
+
size: st.size,
|
|
148
|
+
sha256: createHash("sha256").update(currentText, "utf8").digest("hex"),
|
|
149
|
+
};
|
|
150
|
+
} else return refuse(relative, "error", "not_a_file");
|
|
151
|
+
} catch (err) {
|
|
152
|
+
if ((err as NodeJS.ErrnoException).code !== "ENOENT") throw err;
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
let created = false;
|
|
156
|
+
if (currentText === null) {
|
|
157
|
+
if (input.createMissingNotes === true && typeof input.noteTemplate === "string" && input.noteTemplate.length > 0) {
|
|
158
|
+
const templatePath = path.resolve(root, input.noteTemplate);
|
|
159
|
+
const templateRel = path.relative(root, templatePath);
|
|
160
|
+
if (templateRel === ".." || templateRel.startsWith(`..${path.sep}`) || path.isAbsolute(templateRel) || templateRel.length === 0 || !containedBySymlinks(root, templatePath)) {
|
|
161
|
+
return refuse(relative, "error", "template_escape");
|
|
162
|
+
}
|
|
163
|
+
try {
|
|
164
|
+
const templateText = readFileSync(templatePath, "utf8");
|
|
165
|
+
currentText = expandVaultTemplateTokens(templateText, input.date);
|
|
166
|
+
if (!currentText.endsWith("\n")) currentText += "\n";
|
|
167
|
+
created = true;
|
|
168
|
+
} catch {
|
|
169
|
+
return refuse(relative, "error", "template_unreadable");
|
|
170
|
+
}
|
|
171
|
+
} else {
|
|
172
|
+
return refuse(relative, "skipped", "missing_file");
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
const original = currentText;
|
|
177
|
+
let text = original;
|
|
178
|
+
const failures: Array<{ outcome: "skipped" | "error"; reason: string }> = [];
|
|
179
|
+
const appliedSections: VaultSectionPublish[] = [];
|
|
180
|
+
|
|
181
|
+
for (const section of input.sections) {
|
|
182
|
+
const prefixed =
|
|
183
|
+
opts.propertiesMode === "off" || !section.properties
|
|
184
|
+
? undefined
|
|
185
|
+
: Object.fromEntries(
|
|
186
|
+
Object.entries(section.properties).map(([key, value]) => [`${opts.prefix}${key}`, value]),
|
|
187
|
+
);
|
|
188
|
+
const content = decorate(section, prefixed, opts.propertiesMode);
|
|
189
|
+
const step = applySection(text, section, content, opts.strategy, input.insertUnderHeading, created);
|
|
190
|
+
if (step.ok) {
|
|
191
|
+
text = step.text;
|
|
192
|
+
appliedSections.push(section);
|
|
193
|
+
} else {
|
|
194
|
+
failures.push({ outcome: step.outcome, reason: step.reason });
|
|
195
|
+
}
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
if (failures.length > 0 && appliedSections.length === 0) {
|
|
199
|
+
const worst = failures.find((f) => f.outcome === "error") ?? failures[0]!;
|
|
200
|
+
return [{ path: relative, outcome: worst.outcome, reason: failures.map((f) => f.reason).join("; ") }];
|
|
201
|
+
}
|
|
202
|
+
if (failures.some((f) => f.outcome === "error")) {
|
|
203
|
+
return [{ path: relative, outcome: "error", reason: failures.map((f) => f.reason).join("; ") }];
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
// Partial publish (issue #1985 review): when some sections applied and
|
|
207
|
+
// others were skipped, the write covers ONLY the applied sections — their
|
|
208
|
+
// frontmatter properties, and the reported status. A skipped section's
|
|
209
|
+
// properties must never reach the file, and the omission must be surfaced
|
|
210
|
+
// as its own `skipped` row rather than reported as a plain `updated`.
|
|
211
|
+
const skippedReason = failures.length > 0 ? failures.map((f) => f.reason).join("; ") : undefined;
|
|
212
|
+
const withSkipped = (rows: VaultPublishResult[]): VaultPublishResult[] =>
|
|
213
|
+
skippedReason === undefined ? rows : [...rows, { path: relative, outcome: "skipped", reason: skippedReason }];
|
|
214
|
+
|
|
215
|
+
if (opts.propertiesMode === "frontmatter") {
|
|
216
|
+
const updates = frontmatterUpdates(appliedSections, opts.prefix);
|
|
217
|
+
if (Object.keys(updates).length > 0) {
|
|
218
|
+
const merged = mergeFrontmatterKeys(text, updates);
|
|
219
|
+
if (!merged.ok) {
|
|
220
|
+
return [{ path: relative, outcome: "error", reason: merged.reason }];
|
|
221
|
+
}
|
|
222
|
+
text = merged.text;
|
|
223
|
+
}
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
const prevHash = createHash("sha256").update(original, "utf8").digest("hex");
|
|
227
|
+
const nextHash = createHash("sha256").update(text, "utf8").digest("hex");
|
|
228
|
+
if (prevHash === nextHash) {
|
|
229
|
+
return withSkipped([{ path: relative, outcome: "unchanged" }]);
|
|
230
|
+
}
|
|
231
|
+
if (input.dryRun === true) {
|
|
232
|
+
return withSkipped([{ path: relative, outcome: "updated" }]);
|
|
233
|
+
}
|
|
234
|
+
if (created) {
|
|
235
|
+
// A brand-new note may live in absent folders (Daily/{yyyy}/{MM}). Make
|
|
236
|
+
// the parent chain only here, after every refusal above has passed: the
|
|
237
|
+
// lexical path check and `containedBySymlinks` already proved the deepest
|
|
238
|
+
// existing ancestor is a real directory inside the vault, so every segment
|
|
239
|
+
// `mkdirSync` adds lands beneath that contained ancestor.
|
|
240
|
+
try {
|
|
241
|
+
mkdirSync(path.dirname(dest), { recursive: true });
|
|
242
|
+
} catch {
|
|
243
|
+
return refuse(relative, "error", "mkdir_failed");
|
|
244
|
+
}
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
const write = writeAtomic(dest, text, expected);
|
|
248
|
+
if (write === "concurrent") {
|
|
249
|
+
// The note changed after it was read (editor autosave): keep the user's
|
|
250
|
+
// bytes and surface the abort — never a silent overwrite, never `updated`.
|
|
251
|
+
return [{ path: relative, outcome: "error", reason: "concurrent_write" }];
|
|
252
|
+
}
|
|
253
|
+
if (write !== "written") {
|
|
254
|
+
return [{ path: relative, outcome: "error", reason: "rename_failed" }];
|
|
255
|
+
}
|
|
256
|
+
return withSkipped([{ path: relative, outcome: "updated" }]);
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
function refuse(relative: string, outcome: "skipped" | "error", reason: string): VaultPublishResult[] {
|
|
260
|
+
return [{ path: relative, outcome, reason }];
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
function decorate(
|
|
264
|
+
section: VaultSectionPublish,
|
|
265
|
+
prefixed: Record<string, string> | undefined,
|
|
266
|
+
mode: string,
|
|
267
|
+
): string {
|
|
268
|
+
const parts: string[] = [section.content];
|
|
269
|
+
if (prefixed !== undefined && mode === "dataview-inline") {
|
|
270
|
+
for (const [key, value] of Object.entries(prefixed)) {
|
|
271
|
+
parts.push(`${key}:: ${value}`);
|
|
272
|
+
}
|
|
273
|
+
}
|
|
274
|
+
if (section.provenance !== undefined) {
|
|
275
|
+
parts.push(
|
|
276
|
+
`*(generated by Remnic timeline · ${section.provenance.generatorVersion} · ${section.provenance.generatedAt})*`,
|
|
277
|
+
);
|
|
278
|
+
}
|
|
279
|
+
return parts.join("\n");
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
function applySection(
|
|
283
|
+
text: string,
|
|
284
|
+
section: VaultSectionPublish,
|
|
285
|
+
content: string,
|
|
286
|
+
strategy: "markers" | "heading",
|
|
287
|
+
insertUnderHeading: string | undefined,
|
|
288
|
+
freshNote: boolean,
|
|
289
|
+
): SectionOutcome {
|
|
290
|
+
if (strategy === "markers") {
|
|
291
|
+
const mismatch = findMarkerMismatch(text);
|
|
292
|
+
if (mismatch !== null) {
|
|
293
|
+
return { ok: false, outcome: "skipped", reason: mismatch };
|
|
294
|
+
}
|
|
295
|
+
const applied = applyManagedRegion(text, { strategy: "markers", name: section.name, content });
|
|
296
|
+
if (applied.ok) return { ok: true, text: applied.text };
|
|
297
|
+
if (typeof insertUnderHeading === "string" && insertUnderHeading.length > 0) {
|
|
298
|
+
const inserted = insertMarkersUnderHeading(text, {
|
|
299
|
+
heading: insertUnderHeading,
|
|
300
|
+
name: section.name,
|
|
301
|
+
content,
|
|
302
|
+
});
|
|
303
|
+
if (inserted.ok) return { ok: true, text: inserted.text };
|
|
304
|
+
if (inserted.reason === "duplicate_heading") {
|
|
305
|
+
return {
|
|
306
|
+
ok: false,
|
|
307
|
+
outcome: "error",
|
|
308
|
+
reason: `duplicate_heading:${inserted.lines.join(",")}`,
|
|
309
|
+
};
|
|
310
|
+
}
|
|
311
|
+
return { ok: false, outcome: "skipped", reason: "no_marker" };
|
|
312
|
+
}
|
|
313
|
+
if (freshNote) {
|
|
314
|
+
const suffix = `${text.endsWith("\n") ? "" : "\n"}<!-- remnic:${section.name}:start -->\n${content}\n<!-- remnic:${section.name}:end -->\n`;
|
|
315
|
+
return { ok: true, text: text + suffix };
|
|
316
|
+
}
|
|
317
|
+
return { ok: false, outcome: "skipped", reason: "no_marker" };
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
const applied = applyManagedRegion(text, { strategy: "heading", name: section.name, content });
|
|
321
|
+
if (applied.ok) return { ok: true, text: applied.text };
|
|
322
|
+
if (applied.reason === "duplicate_heading") {
|
|
323
|
+
return { ok: false, outcome: "error", reason: `duplicate_heading:${applied.lines.join(",")}` };
|
|
324
|
+
}
|
|
325
|
+
return { ok: false, outcome: "skipped", reason: "no_heading" };
|
|
326
|
+
}
|
|
327
|
+
|
|
328
|
+
/**
|
|
329
|
+
* Refuse any note whose `remnic:<name>:{start,end}` markers are not a flat
|
|
330
|
+
* sequence of correctly named pairs. Every line outside a code block is
|
|
331
|
+
* scanned — a fenced or indented pair is sample text, invisible to this
|
|
332
|
+
* scan and to replacement —
|
|
333
|
+
* because `applyManagedRegion` pairs a start with the next end of the SAME
|
|
334
|
+
* name and would otherwise span a malformed region and delete every byte
|
|
335
|
+
* in between. Four shapes are malformed:
|
|
336
|
+
* - a crossed pair (`start:A` closed by `end:B`)
|
|
337
|
+
* - a nested start (`start:A`, `start:B`, `end:A`, `end:B`), where the
|
|
338
|
+
* inner region's end is the ONLY end left after A closes, so a
|
|
339
|
+
* replacement spans B's start marker and the user bytes around it
|
|
340
|
+
* - an orphan end with no open region, which cannot be paired at all
|
|
341
|
+
* - a marker line that carries the `remnic:…:start`/`:end` shape but no
|
|
342
|
+
* parseable name (empty name, or whitespace past the bounded run), which
|
|
343
|
+
* the pairing scan cannot classify at all
|
|
344
|
+
* Ambiguity always resolves to "keep the user's bytes".
|
|
345
|
+
*/
|
|
346
|
+
function findMarkerMismatch(text: string): string | null {
|
|
347
|
+
let begin: string | null = null;
|
|
348
|
+
for (const row of fileLines(text)) {
|
|
349
|
+
if (row.fenced) continue;
|
|
350
|
+
const trimmed = row.line.trim();
|
|
351
|
+
const start = START_MARKER_RE.exec(trimmed);
|
|
352
|
+
if (start) {
|
|
353
|
+
const name = start[1]!;
|
|
354
|
+
if (begin !== null) return `nested_start:${begin}:${name}`;
|
|
355
|
+
begin = name;
|
|
356
|
+
continue;
|
|
357
|
+
}
|
|
358
|
+
const match = END_MARKER_RE.exec(trimmed);
|
|
359
|
+
if (!match) {
|
|
360
|
+
if (looksLikeRemnicMarkerLine(trimmed)) return `unparsable_marker:${trimmed}`;
|
|
361
|
+
continue;
|
|
362
|
+
}
|
|
363
|
+
const end = match[1]!;
|
|
364
|
+
if (begin === null) return `orphan_end:${end}`;
|
|
365
|
+
// A blank marker name cannot be paired at all: refuse rather than let
|
|
366
|
+
// assertBeginEndPair throw out of the publisher.
|
|
367
|
+
if (begin.trim().length === 0 || end.trim().length === 0) return `name_mismatch:${begin}:${end}`;
|
|
368
|
+
if (!assertBeginEndPair({ beginName: begin, endName: end }).ok) return `name_mismatch:${begin}:${end}`;
|
|
369
|
+
begin = null;
|
|
370
|
+
}
|
|
371
|
+
return null;
|
|
372
|
+
}
|
|
373
|
+
|
|
374
|
+
/**
|
|
375
|
+
* True for a line shaped like this publisher's own marker — `<!--` … `-->`
|
|
376
|
+
* wrapping `remnic:` … terminal `:start`/`:end` — that neither marker regex
|
|
377
|
+
* accepted. Classification only; the caller refuses the file. The exact
|
|
378
|
+
* trimmed line is inspected, never a normalized rewrite of it, so no
|
|
379
|
+
* "helpful" transform can widen what counts as parseable.
|
|
380
|
+
*/
|
|
381
|
+
function looksLikeRemnicMarkerLine(trimmed: string): boolean {
|
|
382
|
+
if (!trimmed.startsWith("<!--") || !trimmed.endsWith("-->")) return false;
|
|
383
|
+
const inner = trimmed.slice(4, -3).trim();
|
|
384
|
+
if (!inner.startsWith("remnic:")) return false;
|
|
385
|
+
return inner.endsWith(":start") || inner.endsWith(":end");
|
|
386
|
+
}
|
|
387
|
+
|
|
388
|
+
function frontmatterUpdates(
|
|
389
|
+
sections: readonly VaultSectionPublish[],
|
|
390
|
+
prefix: string,
|
|
391
|
+
): Record<string, string> {
|
|
392
|
+
const updates: Record<string, string> = {};
|
|
393
|
+
for (const section of sections) {
|
|
394
|
+
if (!section.properties) continue;
|
|
395
|
+
for (const [key, value] of Object.entries(section.properties)) {
|
|
396
|
+
updates[`${prefix}${key}`] = value;
|
|
397
|
+
}
|
|
398
|
+
}
|
|
399
|
+
return updates;
|
|
400
|
+
}
|
|
401
|
+
|
|
402
|
+
/**
|
|
403
|
+
* Targeted frontmatter edit: only `updates` keys are inserted or replaced;
|
|
404
|
+
* every other byte of the note (key order, comments, anchors, spacing) is
|
|
405
|
+
* preserved. Never parses-and-redumps the YAML block.
|
|
406
|
+
*/
|
|
407
|
+
export function mergeFrontmatterKeys(
|
|
408
|
+
noteText: string,
|
|
409
|
+
updates: Readonly<Record<string, string>>,
|
|
410
|
+
): { ok: true; text: string } | { ok: false; reason: string } {
|
|
411
|
+
const entries = Object.entries(updates);
|
|
412
|
+
if (entries.length === 0) return { ok: true, text: noteText };
|
|
413
|
+
for (const [key] of entries) {
|
|
414
|
+
if (key.includes("\n") || key.includes("\r") || key.trim() !== key || key.length === 0) {
|
|
415
|
+
return { ok: false, reason: "invalid_property_key" };
|
|
416
|
+
}
|
|
417
|
+
}
|
|
418
|
+
|
|
419
|
+
const eol = noteText.includes("\r\n") ? "\r\n" : "\n";
|
|
420
|
+
const openRe = /^---\r?$/;
|
|
421
|
+
const firstLineEnd = noteText.indexOf("\n");
|
|
422
|
+
const firstLine = firstLineEnd === -1 ? noteText : noteText.slice(0, firstLineEnd);
|
|
423
|
+
|
|
424
|
+
if (!openRe.test(firstLine)) {
|
|
425
|
+
const block = ["---", ...entries.map(([key, value]) => `${key}: ${value}`), "---", ""].join(eol);
|
|
426
|
+
return { ok: true, text: block + eol + noteText };
|
|
427
|
+
}
|
|
428
|
+
|
|
429
|
+
const lines = noteText.split(eol);
|
|
430
|
+
let closeIdx = -1;
|
|
431
|
+
for (let i = 1; i < lines.length; i += 1) {
|
|
432
|
+
if (openRe.test(lines[i]!)) {
|
|
433
|
+
closeIdx = i;
|
|
434
|
+
break;
|
|
435
|
+
}
|
|
436
|
+
}
|
|
437
|
+
if (closeIdx === -1) {
|
|
438
|
+
return { ok: false, reason: "malformed_frontmatter" };
|
|
439
|
+
}
|
|
440
|
+
|
|
441
|
+
const pending = new Map(entries);
|
|
442
|
+
for (let i = 1; i < closeIdx; i += 1) {
|
|
443
|
+
const line = lines[i]!;
|
|
444
|
+
const idx = line.indexOf(":");
|
|
445
|
+
if (idx === -1) continue;
|
|
446
|
+
const rawKey = line.slice(0, idx);
|
|
447
|
+
// Leading whitespace is exactly the signal that this is a nested,
|
|
448
|
+
// user-owned child key (`custom:` / ` remnic_focus: old`). Only an
|
|
449
|
+
// unindented top-level mapping may satisfy a pending update, so the
|
|
450
|
+
// key is NOT trimmed before that decision.
|
|
451
|
+
if (rawKey !== rawKey.trimStart()) continue;
|
|
452
|
+
const key = rawKey.trimEnd();
|
|
453
|
+
if (!pending.has(key)) continue;
|
|
454
|
+
lines[i] = `${rawKey}: ${pending.get(key)}`;
|
|
455
|
+
pending.delete(key);
|
|
456
|
+
}
|
|
457
|
+
const insertAt = closeIdx;
|
|
458
|
+
const added: string[] = [];
|
|
459
|
+
for (const [key, value] of pending.entries()) {
|
|
460
|
+
added.push(`${key}: ${value}`);
|
|
461
|
+
}
|
|
462
|
+
lines.splice(insertAt, 0, ...added);
|
|
463
|
+
return { ok: true, text: lines.join(eol) };
|
|
464
|
+
}
|
|
465
|
+
|
|
466
|
+
/**
|
|
467
|
+
* Containment under symlinks: the deepest existing ancestor of `dest` must
|
|
468
|
+
* still resolve inside the (real) vault root. A symlink chain that leaves
|
|
469
|
+
* the vault is refused before any note byte is read.
|
|
470
|
+
*/
|
|
471
|
+
function containedBySymlinks(root: string, dest: string): boolean {
|
|
472
|
+
const realRoot = realpathSync(root);
|
|
473
|
+
let probe = dest;
|
|
474
|
+
for (;;) {
|
|
475
|
+
try {
|
|
476
|
+
if (lstatSync(probe).isSymbolicLink() || statSync(probe).isFile() || statSync(probe).isDirectory()) {
|
|
477
|
+
const real = realpathSync(probe);
|
|
478
|
+
return real === realRoot || real.startsWith(`${realRoot}${path.sep}`);
|
|
479
|
+
}
|
|
480
|
+
} catch {
|
|
481
|
+
// ENOENT: walk up to the parent.
|
|
482
|
+
}
|
|
483
|
+
const parent = path.dirname(probe);
|
|
484
|
+
if (parent === probe) return false;
|
|
485
|
+
probe = parent;
|
|
486
|
+
}
|
|
487
|
+
}
|
|
488
|
+
|
|
489
|
+
/**
|
|
490
|
+
* Destination identity captured when the note was read for this publish:
|
|
491
|
+
* inode + mtime + size + content hash. Any drift by replace time is a
|
|
492
|
+
* concurrent write (an editor that saves in place changes mtimeMs/size/
|
|
493
|
+
* bytes; one that saves atomically changes the inode) and aborts the
|
|
494
|
+
* replace. `null` means the destination did not exist at read time.
|
|
495
|
+
*/
|
|
496
|
+
interface DestExpectation {
|
|
497
|
+
ino: number;
|
|
498
|
+
mtimeMs: number;
|
|
499
|
+
size: number;
|
|
500
|
+
sha256: string;
|
|
501
|
+
}
|
|
502
|
+
|
|
503
|
+
/** Test-only injection point (issue #1985 review): runs after the note was
|
|
504
|
+
* read and rendered but before the pre-replace verification, so tests can
|
|
505
|
+
* simulate a concurrent editor save landing inside that window. Never set
|
|
506
|
+
* in production code. */
|
|
507
|
+
export interface VaultPublisherTestHooks {
|
|
508
|
+
beforeReplaceVerify?: (dest: string) => void;
|
|
509
|
+
}
|
|
510
|
+
|
|
511
|
+
let vaultPublisherTestHooks: VaultPublisherTestHooks | null = null;
|
|
512
|
+
|
|
513
|
+
export function setVaultPublisherTestHooks(hooks: VaultPublisherTestHooks | null): void {
|
|
514
|
+
vaultPublisherTestHooks = hooks;
|
|
515
|
+
}
|
|
516
|
+
|
|
517
|
+
/**
|
|
518
|
+
* Temp file + rename in the note's own directory; one retry on EBUSY/EPERM.
|
|
519
|
+
*
|
|
520
|
+
* The replacement inode must inherit the destination's permissions: a fresh
|
|
521
|
+
* temp file is `0666 & ~umask`, so renaming it over a `0600` note under a
|
|
522
|
+
* `0022` umask silently widens the note to `0644`. Mode is carried via
|
|
523
|
+
* chmodSync (not a writeFileSync mode option, which umask would mask);
|
|
524
|
+
* ownership is carried only as far as the process's privilege allows
|
|
525
|
+
* (chownSync to another owner fails EPERM for non-root and is ignored).
|
|
526
|
+
*
|
|
527
|
+
* Concurrent-write guard (issue #1985 final review): `text` is derived from
|
|
528
|
+
* the snapshot in `expect`; if the destination no longer matches that
|
|
529
|
+
* snapshot — different inode, mtime, size, bytes, or existence — the rename
|
|
530
|
+
* is refused (`"concurrent"`) and the user's bytes are kept. Deliberately
|
|
531
|
+
* abort-only, no re-read/re-render retry: the refusal direction is the
|
|
532
|
+
* contract, and the next scheduled publish converges against fresh content.
|
|
533
|
+
*/
|
|
534
|
+
function writeAtomic(dest: string, text: string, expect: DestExpectation | null): "written" | "concurrent" | "failed" {
|
|
535
|
+
const tmpPath = path.join(path.dirname(dest), `.remnic-vault-${randomBytes(8).toString("hex")}.tmp`);
|
|
536
|
+
writeFileSync(tmpPath, text);
|
|
537
|
+
let prev: Stats | null = null;
|
|
538
|
+
try {
|
|
539
|
+
prev = statSync(dest);
|
|
540
|
+
} catch {
|
|
541
|
+
prev = null; // destination absent (new note) — not an error yet
|
|
542
|
+
}
|
|
543
|
+
if (prev !== null) {
|
|
544
|
+
try {
|
|
545
|
+
chmodSync(tmpPath, prev.mode & 0o7777);
|
|
546
|
+
chownSync(tmpPath, prev.uid, prev.gid);
|
|
547
|
+
} catch {
|
|
548
|
+
// Insufficient privilege (EPERM): the temp file keeps its default
|
|
549
|
+
// mode/owner.
|
|
550
|
+
}
|
|
551
|
+
}
|
|
552
|
+
vaultPublisherTestHooks?.beforeReplaceVerify?.(dest);
|
|
553
|
+
try {
|
|
554
|
+
const concurrent =
|
|
555
|
+
expect === null
|
|
556
|
+
? prev !== null // someone created the note after we planned to
|
|
557
|
+
: prev === null || // the note vanished since it was read
|
|
558
|
+
prev.ino !== expect.ino ||
|
|
559
|
+
prev.mtimeMs !== expect.mtimeMs ||
|
|
560
|
+
prev.size !== expect.size ||
|
|
561
|
+
createHash("sha256").update(readFileSync(dest, "utf8"), "utf8").digest("hex") !== expect.sha256;
|
|
562
|
+
if (concurrent) {
|
|
563
|
+
try {
|
|
564
|
+
unlinkSync(tmpPath);
|
|
565
|
+
} catch {
|
|
566
|
+
// best-effort cleanup
|
|
567
|
+
}
|
|
568
|
+
return "concurrent";
|
|
569
|
+
}
|
|
570
|
+
} catch {
|
|
571
|
+
// Destination unreadable at verify time: refuse to replace.
|
|
572
|
+
try {
|
|
573
|
+
unlinkSync(tmpPath);
|
|
574
|
+
} catch {
|
|
575
|
+
// best-effort cleanup
|
|
576
|
+
}
|
|
577
|
+
return "concurrent";
|
|
578
|
+
}
|
|
579
|
+
for (let attempt = 0; attempt < 2; attempt += 1) {
|
|
580
|
+
try {
|
|
581
|
+
renameSync(tmpPath, dest);
|
|
582
|
+
return "written";
|
|
583
|
+
} catch (err) {
|
|
584
|
+
const code = (err as NodeJS.ErrnoException).code;
|
|
585
|
+
if (attempt === 1 || (code !== "EBUSY" && code !== "EPERM")) {
|
|
586
|
+
try {
|
|
587
|
+
unlinkSync(tmpPath);
|
|
588
|
+
} catch {
|
|
589
|
+
// best-effort cleanup
|
|
590
|
+
}
|
|
591
|
+
if (code === "EBUSY" || code === "EPERM") return "failed";
|
|
592
|
+
throw err;
|
|
593
|
+
}
|
|
594
|
+
}
|
|
595
|
+
}
|
|
596
|
+
return "failed";
|
|
597
|
+
}
|
|
@@ -22,3 +22,8 @@ test("validateRegionName rejects the HTML comment closer", () => {
|
|
|
22
22
|
assert.deepEqual(validateRegionName("foo-->bar"), { ok: false, error: "invalid_name" });
|
|
23
23
|
assert.deepEqual(validateRegionName("timeline-->"), { ok: false, error: "invalid_name" });
|
|
24
24
|
});
|
|
25
|
+
|
|
26
|
+
test("validateRegionName rejects a colon, which the marker grammar delimits with", () => {
|
|
27
|
+
assert.deepEqual(validateRegionName("Work:Timeline"), { ok: false, error: "invalid_name" });
|
|
28
|
+
assert.deepEqual(validateRegionName("timeline:start"), { ok: false, error: "invalid_name" });
|
|
29
|
+
});
|
|
@@ -2,7 +2,12 @@
|
|
|
2
2
|
* Validate a managed-region name for vault markers (issue #1985).
|
|
3
3
|
*
|
|
4
4
|
* Empty, whitespace-only, newline, and `-->` names are rejected so they
|
|
5
|
-
* cannot break `<!-- remnic:<name>:start -->` markers.
|
|
5
|
+
* cannot break `<!-- remnic:<name>:start -->` markers. `:` is rejected
|
|
6
|
+
* too: the marker grammar is colon-delimited, so a colon-bearing name
|
|
7
|
+
* makes a marker line ambiguous (`remnic:A:B:start` reads as name `A:B`
|
|
8
|
+
* or as name `A` with a `B:start` suffix) and a publisher that resolved
|
|
9
|
+
* that ambiguity differently from the note's own markers would pair a
|
|
10
|
+
* start with a later end and delete the user bytes in between.
|
|
6
11
|
*/
|
|
7
12
|
|
|
8
13
|
export type ValidateRegionNameResult =
|
|
@@ -13,7 +18,7 @@ export function validateRegionName(name: string): ValidateRegionNameResult {
|
|
|
13
18
|
if (typeof name !== "string") {
|
|
14
19
|
return { ok: false, error: "invalid_name" };
|
|
15
20
|
}
|
|
16
|
-
if (name.includes("\n") || name.includes("\r") || name.includes("-->")) {
|
|
21
|
+
if (name.includes("\n") || name.includes("\r") || name.includes("-->") || name.includes(":")) {
|
|
17
22
|
return { ok: false, error: "invalid_name" };
|
|
18
23
|
}
|
|
19
24
|
const trimmed = name.trim();
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Timeline command group (issue #1985) — `remnic timeline publish` writes
|
|
3
|
+
* the day's timeline artifact into the user's markdown vault through the
|
|
4
|
+
* managed-region publisher. The command BODY lives in
|
|
5
|
+
* `activity/timeline/publish-cli.ts` so the standalone `@remnic/cli` binary
|
|
6
|
+
* dispatches the same implementation through the shared timeline runner.
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
import type { Orchestrator } from "../orchestrator.js";
|
|
10
|
+
import type { CliCommand } from "../cli.js";
|
|
11
|
+
import { runTimelinePublishCli } from "../activity/timeline/publish-cli.js";
|
|
12
|
+
|
|
13
|
+
export function registerTimelineCommands(cmd: CliCommand, orchestrator: Orchestrator): void {
|
|
14
|
+
const timelineCmd = cmd
|
|
15
|
+
.command("timeline")
|
|
16
|
+
.description("Timeline artifacts: publish the day's recap into a markdown vault (issue #1985)");
|
|
17
|
+
|
|
18
|
+
timelineCmd
|
|
19
|
+
.command("publish")
|
|
20
|
+
.description("Publish timeline artifacts into the vault daily note via managed regions")
|
|
21
|
+
.option("--date <date>", "Local day to publish (YYYY-MM-DD; defaults to today)")
|
|
22
|
+
.option("--week <week>", "ISO week to publish (YYYY-Www)")
|
|
23
|
+
.option("--what <kinds>", "Comma-separated artifacts to publish (default: timeline)")
|
|
24
|
+
.option("--dry-run", "Report per-file outcomes without writing")
|
|
25
|
+
.action((...args: unknown[]) => {
|
|
26
|
+
const options = (args[0] ?? {}) as Record<string, unknown>;
|
|
27
|
+
const code = runTimelinePublishCli(orchestrator.config, options, {
|
|
28
|
+
stdout: process.stdout,
|
|
29
|
+
stderr: process.stderr,
|
|
30
|
+
});
|
|
31
|
+
if (code !== 0) process.exitCode = code;
|
|
32
|
+
});
|
|
33
|
+
}
|
|
34
|
+
|
package/src/cli.ts
CHANGED
|
@@ -18,7 +18,7 @@ import { registerMeetingsCommands } from "./cli/meetings-commands.js"; import {
|
|
|
18
18
|
import { registerExportOkfCommand, registerOkfCommands } from "./cli/okf-commands.js";
|
|
19
19
|
import { registerSkillsCommands } from "./cli/skills-commands.js";
|
|
20
20
|
import { registerResearchStatusCommands } from "./cli/research-status-commands.js";
|
|
21
|
-
import { registerCreationLedgerCommands } from "./cli/creation-ledger-commands.js";
|
|
21
|
+
import { registerCreationLedgerCommands } from "./cli/creation-ledger-commands.js"; import { registerTimelineCommands } from "./cli/timeline-publish-commands.js";
|
|
22
22
|
import type {
|
|
23
23
|
BehaviorSignalEvent,
|
|
24
24
|
ContinuityIncidentRecord,
|
|
@@ -6230,7 +6230,7 @@ export function registerCli(
|
|
|
6230
6230
|
|
|
6231
6231
|
registerResearchStatusCommands(cmd, orchestrator);
|
|
6232
6232
|
|
|
6233
|
-
registerCreationLedgerCommands(cmd, orchestrator); registerMeetingsCommands(cmd, orchestrator); registerOkfCommands(cmd, orchestrator); registerDeepRecallCommands(cmd, orchestrator);
|
|
6233
|
+
registerCreationLedgerCommands(cmd, orchestrator); registerMeetingsCommands(cmd, orchestrator); registerOkfCommands(cmd, orchestrator); registerDeepRecallCommands(cmd, orchestrator); registerTimelineCommands(cmd, orchestrator);
|
|
6234
6234
|
|
|
6235
6235
|
cmd
|
|
6236
6236
|
.command("trust-zone-promote")
|