@remnic/core 9.4.2 → 9.6.0
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 +6 -6
- package/dist/access-admin-ops-surface.js +28 -28
- package/dist/access-boundary.d.ts +27 -8
- package/dist/access-boundary.js +33 -29
- package/dist/access-cli.js +83 -81
- package/dist/access-cli.js.map +1 -1
- package/dist/access-http.d.ts +52 -6
- package/dist/access-http.js +40 -38
- package/dist/access-identity-continuity-surface.d.ts +5 -5
- package/dist/access-identity-continuity-surface.js +28 -28
- package/dist/access-lcm-surface.d.ts +6 -6
- package/dist/access-lcm-surface.js +28 -28
- package/dist/access-mcp.d.ts +16 -6
- package/dist/access-mcp.js +37 -35
- package/dist/access-observe-write-surface.d.ts +6 -6
- package/dist/access-observe-write-surface.js +28 -28
- package/dist/access-operations-batch.js +33 -32
- package/dist/access-operations.d.ts +9 -9
- package/dist/access-operations.js +35 -34
- package/dist/access-recall-surface.d.ts +6 -6
- package/dist/access-recall-surface.js +28 -28
- package/dist/access-schema.d.ts +4 -4
- package/dist/access-schema.js +4 -4
- package/dist/{access-service-m-2VO_Ig.d.ts → access-service-CXJaql3A.d.ts} +4 -4
- package/dist/access-service.d.ts +6 -6
- package/dist/access-service.js +28 -28
- package/dist/access-surface-catalog.d.ts +6 -6
- package/dist/access-surface-catalog.js +1 -0
- package/dist/access-surface-catalog.js.map +1 -1
- package/dist/access-token-capabilities.d.ts +174 -0
- package/dist/access-token-capabilities.js +33 -0
- 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 +2 -2
- package/dist/adapters/index.js +4 -4
- package/dist/adapters/registry.js +2 -2
- package/dist/{auto-sync-HMHQHUAG.js → auto-sync-4A5YBWYO.js} +4 -4
- package/dist/behavior-learner.d.ts +1 -1
- package/dist/behavior-signals.d.ts +1 -1
- package/dist/bootstrap.d.ts +5 -5
- package/dist/briefing.d.ts +2 -2
- package/dist/briefing.js +9 -9
- package/dist/buffer-surprise-report.d.ts +1 -1
- package/dist/buffer.d.ts +2 -2
- package/dist/calibration.d.ts +1 -1
- package/dist/capabilities.d.ts +1 -1
- package/dist/{capsule-crypto-CZJSLEFG.js → capsule-crypto-RAARDY3M.js} +2 -2
- package/dist/capsule-crypto-RAARDY3M.js.map +1 -0
- package/dist/{catalog-BxWokdC2.d.ts → catalog-C9cu67Ig.d.ts} +1 -1
- package/dist/causal-behavior.d.ts +1 -1
- package/dist/causal-behavior.js +5 -5
- package/dist/causal-chain.js +3 -3
- package/dist/causal-consolidation.d.ts +1 -1
- package/dist/causal-consolidation.js +13 -13
- package/dist/causal-retrieval.js +3 -3
- package/dist/causal-trajectory-graph.d.ts +1 -1
- package/dist/causal-trajectory.js +1 -1
- package/dist/{chunk-6ZIETXZ2.js → chunk-23JFRB73.js} +2 -2
- package/dist/{chunk-IC6QX5TC.js → chunk-3XIY7MDQ.js} +6 -6
- package/dist/{chunk-FZGGZAAU.js → chunk-4GNEDXDI.js} +146 -2
- package/dist/chunk-4GNEDXDI.js.map +1 -0
- package/dist/{chunk-LOR6ZM5R.js → chunk-4LV23CHK.js} +2 -2
- package/dist/{chunk-E2SPGGUI.js → chunk-4OKLXWDZ.js} +13 -6
- package/dist/chunk-4OKLXWDZ.js.map +1 -0
- package/dist/{chunk-AER6MT24.js → chunk-6SXVCD7W.js} +2 -2
- package/dist/{chunk-OHDJ34CC.js → chunk-72WG5QKN.js} +2 -2
- package/dist/{chunk-CZAZX7JP.js → chunk-7MW3CVLD.js} +4 -4
- package/dist/{chunk-CXLNZXG5.js → chunk-7XGSIUP5.js} +1 -1
- package/dist/{chunk-CZAA5N5S.js → chunk-AYZJID4S.js} +2 -2
- package/dist/{chunk-M7XQSUBB.js → chunk-B3ABVQ36.js} +116 -9
- package/dist/chunk-B3ABVQ36.js.map +1 -0
- package/dist/{chunk-6BISDEN6.js → chunk-BEM4D2QG.js} +14 -14
- package/dist/{chunk-ZPOROUTX.js → chunk-CYWA2CXR.js} +5 -5
- package/dist/{chunk-M5QKGHCR.js → chunk-CYWCKFCL.js} +5 -5
- package/dist/chunk-D7FOKIY5.js +173 -0
- package/dist/chunk-D7FOKIY5.js.map +1 -0
- package/dist/{chunk-4BFRBP45.js → chunk-DEG5ULFJ.js} +2 -2
- package/dist/{chunk-JLR3JSTL.js → chunk-DS7MM2ES.js} +3 -3
- package/dist/{chunk-2BFLDH5F.js → chunk-EEIROWEG.js} +2 -2
- package/dist/{chunk-5QZDMSSN.js → chunk-FKCRGLRD.js} +237 -82
- package/dist/{chunk-5QZDMSSN.js.map → chunk-FKCRGLRD.js.map} +1 -1
- package/dist/{chunk-X4A62ET7.js → chunk-H2DXDQCQ.js} +17 -17
- package/dist/{chunk-27FLVMDC.js → chunk-HLN7RROI.js} +7 -7
- package/dist/chunk-IC2TCZJ2.js +54 -0
- package/dist/chunk-IC2TCZJ2.js.map +1 -0
- package/dist/{chunk-2U2A5HF4.js → chunk-J3XHPFEU.js} +6 -6
- package/dist/{chunk-AKUCU7CI.js → chunk-JI5AVXYO.js} +5 -5
- package/dist/{chunk-KCJ5H6XQ.js → chunk-JSNVWNGX.js} +2 -2
- package/dist/{chunk-7Z2DZNWN.js → chunk-KL4UAXUY.js} +43 -2
- package/dist/{chunk-7Z2DZNWN.js.map → chunk-KL4UAXUY.js.map} +1 -1
- package/dist/{chunk-FLVL4L3L.js → chunk-KTULX4YO.js} +4 -4
- package/dist/{chunk-ILFJ7AVS.js → chunk-LADFWYHY.js} +35 -21
- package/dist/chunk-LADFWYHY.js.map +1 -0
- package/dist/{chunk-437S3G37.js → chunk-LNQUAJNC.js} +1 -1
- package/dist/{chunk-EPGWWCZF.js → chunk-LR5DZ56F.js} +6 -6
- package/dist/{chunk-YDSWRD33.js → chunk-N3JDIRBW.js} +4 -4
- package/dist/{chunk-OUMTBFSQ.js → chunk-NKCPUHCI.js} +5 -5
- package/dist/{chunk-7QWMTOIF.js → chunk-P454KXD6.js} +112 -34
- package/dist/chunk-P454KXD6.js.map +1 -0
- package/dist/{chunk-DR6PEJU3.js → chunk-PCLIWBGD.js} +44 -59
- package/dist/chunk-PCLIWBGD.js.map +1 -0
- package/dist/{chunk-BGTAL46Y.js → chunk-PNLFN2PZ.js} +2 -2
- package/dist/{chunk-P6SYWE4G.js → chunk-PVUJ6WVV.js} +10 -10
- package/dist/{chunk-YPEMNNNY.js → chunk-RGK27ELN.js} +6 -6
- package/dist/{chunk-2QSZNTDO.js → chunk-RKNJBZ55.js} +4 -4
- package/dist/{chunk-66FLU5BT.js → chunk-SIRPLF54.js} +6 -6
- package/dist/{chunk-M7GGMGMZ.js → chunk-SVOHGUMI.js} +1202 -126
- package/dist/chunk-SVOHGUMI.js.map +1 -0
- package/dist/{chunk-HRCACVHL.js → chunk-TLUDFPZL.js} +2 -2
- package/dist/{chunk-2FOD2YY3.js → chunk-UFUKHGZD.js} +236 -103
- package/dist/chunk-UFUKHGZD.js.map +1 -0
- package/dist/{chunk-2MR3MFQB.js → chunk-UPSAT4PY.js} +2 -2
- package/dist/{chunk-4P4626RY.js → chunk-UTRJFDP2.js} +4 -4
- package/dist/{chunk-CGS7UKWM.js → chunk-VCW74PNH.js} +2 -2
- package/dist/{chunk-5GPPACXK.js → chunk-VSMRDBFT.js} +2 -1
- package/dist/{chunk-NG7YBCJO.js → chunk-XCL57M6Q.js} +4 -4
- package/dist/{chunk-CLX5VDNY.js → chunk-Y4RKAQZ7.js} +4 -4
- package/dist/{chunk-DGYCWCQ7.js → chunk-Y7UNNAZD.js} +2 -2
- package/dist/{chunk-EZ25VE3G.js → chunk-YNDLCWXS.js} +4 -4
- package/dist/{chunk-XN3TUUX2.js → chunk-YQB32INI.js} +2 -2
- package/dist/{cli-EiBBOt3r.d.ts → cli-CqdPA6z9.d.ts} +3 -3
- package/dist/cli.d.ts +7 -7
- package/dist/cli.js +61 -59
- package/dist/compounding/engine.d.ts +2 -2
- package/dist/compounding/engine.js +9 -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 +2 -2
- package/dist/connectors/codex-materialize-runner.d.ts +1 -1
- package/dist/connectors/codex-materialize-runner.js +9 -9
- package/dist/connectors/codex-materialize.d.ts +1 -1
- package/dist/connectors/index.d.ts +1 -1
- package/dist/connectors/index.js +13 -11
- 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/contradiction/index.js +4 -4
- package/dist/conversation-index/backend.d.ts +1 -1
- package/dist/conversation-index/backend.js +2 -2
- 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/dashboard-runtime.js +2 -2
- 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 +9 -9
- package/dist/entity-schema.d.ts +1 -1
- package/dist/explicit-capture.d.ts +5 -5
- 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.d.ts +1 -1
- package/dist/fallback-llm.d.ts +1 -1
- package/dist/{first-start-migration-DY6YCRC2.js → first-start-migration-VBJQL6OI.js} +5 -5
- 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-snapshot.d.ts +1 -1
- package/dist/graph.d.ts +1 -1
- package/dist/identity-continuity.d.ts +1 -1
- package/dist/importance.d.ts +1 -1
- package/dist/index.d.ts +271 -17
- package/dist/index.js +163 -99
- 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 +2 -2
- 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 -10
- package/dist/maintenance/rebuild-memory-lifecycle-ledger.js +9 -9
- package/dist/maintenance/rebuild-memory-projection.js +11 -11
- package/dist/mcp-memory-inspector-app.d.ts +6 -6
- package/dist/mcp-read-only-tools.d.ts +16 -0
- package/dist/mcp-read-only-tools.js +8 -0
- package/dist/mcp-read-only-tools.js.map +1 -0
- 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 +19 -19
- package/dist/namespaces/principal.d.ts +1 -1
- package/dist/namespaces/search.d.ts +1 -1
- package/dist/namespaces/search.js +10 -10
- package/dist/namespaces/storage.d.ts +3 -3
- package/dist/namespaces/storage.js +9 -9
- package/dist/native-knowledge.d.ts +1 -1
- package/dist/operator-toolkit.d.ts +2 -2
- package/dist/operator-toolkit.js +25 -25
- package/dist/orchestration/compression-guideline-coordinator.d.ts +2 -2
- package/dist/orchestration/maintenance.d.ts +2 -2
- package/dist/orchestration/maintenance.js +11 -11
- package/dist/{orchestrator-B2Y28Z9t.d.ts → orchestrator-CWFrT8AF.d.ts} +91 -60
- package/dist/orchestrator.d.ts +5 -5
- package/dist/orchestrator.js +85 -83
- package/dist/patterns-cli.d.ts +1 -1
- package/dist/policy-runtime.d.ts +1 -1
- package/dist/provenance.d.ts +1 -1
- package/dist/{qmd-Db9mOkzB.d.ts → qmd--fnPMK60.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-disclosure-escalation.d.ts +1 -1
- package/dist/recall-explain-renderer.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/resolve-auth-token.d.ts +1 -1
- package/dist/resume-bundles.js +4 -4
- 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/schemas.d.ts +22 -22
- package/dist/search/document-scanner.js +2 -2
- package/dist/search/embed-helper.d.ts +1 -1
- package/dist/search/factory.d.ts +1 -1
- package/dist/search/factory.js +9 -9
- package/dist/search/index.d.ts +1 -1
- package/dist/search/index.js +13 -13
- package/dist/search/lancedb-backend.d.ts +1 -1
- package/dist/search/lancedb-backend.js +3 -3
- package/dist/search/meilisearch-backend.d.ts +1 -1
- package/dist/search/meilisearch-backend.js +3 -3
- package/dist/search/noop-backend.d.ts +1 -1
- package/dist/search/orama-backend.d.ts +1 -1
- package/dist/search/orama-backend.js +3 -3
- package/dist/search/port.d.ts +1 -1
- package/dist/search/remote-backend.d.ts +1 -1
- package/dist/{semantic-consolidation-BDVIbLRw.d.ts → semantic-consolidation-O5lMZ4LG.d.ts} +1 -1
- package/dist/semantic-consolidation.d.ts +2 -2
- package/dist/semantic-consolidation.js +11 -11
- package/dist/semantic-rule-promotion.js +9 -9
- package/dist/semantic-rule-verifier.d.ts +1 -1
- package/dist/semantic-rule-verifier.js +10 -10
- 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/storage-C1maCNbc.d.ts +1591 -0
- package/dist/storage.d.ts +5 -1239
- package/dist/storage.js +8 -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-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/{tier-stats-TG6GUZ6N.js → tier-stats-5XF4UBRQ.js} +4 -4
- package/dist/tokens.d.ts +17 -2
- package/dist/tokens.js +3 -1
- package/dist/topics.d.ts +1 -1
- package/dist/transcript.d.ts +1 -1
- package/dist/transfer/backup.js +2 -2
- package/dist/transfer/capsule-export.js +3 -3
- package/dist/transfer/capsule-import.js +3 -3
- package/dist/transfer/types.d.ts +12 -12
- package/dist/trust-score-stage.d.ts +1 -1
- package/dist/trust-score.d.ts +1 -1
- package/dist/{types-ljkd82rn.d.ts → types-CGgcNWCG.d.ts} +31 -1
- package/dist/types.d.ts +1 -1
- package/dist/utility-runtime.d.ts +1 -1
- package/dist/verified-recall.js +10 -10
- package/package.json +2 -2
- package/src/access-boundary.test.ts +62 -0
- package/src/access-boundary.ts +165 -126
- package/src/access-http.test.ts +781 -0
- package/src/access-http.ts +334 -108
- package/src/access-mcp.test.ts +264 -0
- package/src/access-mcp.ts +45 -70
- package/src/access-operations-batch.ts +35 -12
- package/src/access-surface-catalog.test.ts +22 -14
- package/src/access-surface-catalog.ts +6 -3
- package/src/access-token-capabilities.test.ts +844 -0
- package/src/access-token-capabilities.ts +391 -0
- package/src/chat/chat-mcp.test.ts +232 -0
- package/src/chat/chat-session.ts +34 -0
- package/src/cli-wearables-fuse.test.ts +197 -0
- package/src/cli.ts +25 -0
- package/src/index.ts +14 -0
- package/src/mcp-read-only-tools.ts +61 -0
- package/src/storage.ts +16 -0
- package/src/tokens.ts +32 -3
- package/src/wearables/cli.test.ts +102 -0
- package/src/wearables/cli.ts +65 -0
- package/src/wearables/config.test.ts +32 -0
- package/src/wearables/config.ts +52 -0
- package/src/wearables/day-store.test.ts +246 -0
- package/src/wearables/day-store.ts +272 -3
- package/src/wearables/fusion/cluster.ts +230 -0
- package/src/wearables/fusion/fuse.ts +218 -0
- package/src/wearables/fusion/fusion.test.ts +2575 -0
- package/src/wearables/fusion/index.ts +50 -0
- package/src/wearables/fusion/reconcile.ts +831 -0
- package/src/wearables/fusion/reconstruct.ts +251 -0
- package/src/wearables/fusion/store.ts +480 -0
- package/src/wearables/fusion/types.ts +228 -0
- package/src/wearables/index.ts +41 -0
- package/src/wearables/service.test.ts +1030 -2
- package/src/wearables/service.ts +258 -16
- package/src/wearables/speakers.test.ts +128 -0
- package/src/wearables/speakers.ts +58 -4
- package/src/wearables/storage-io.test.ts +146 -0
- package/src/wearables/types.ts +31 -0
- package/dist/chunk-2FOD2YY3.js.map +0 -1
- package/dist/chunk-7QWMTOIF.js.map +0 -1
- package/dist/chunk-DR6PEJU3.js.map +0 -1
- package/dist/chunk-E2SPGGUI.js.map +0 -1
- package/dist/chunk-FZGGZAAU.js.map +0 -1
- package/dist/chunk-ILFJ7AVS.js.map +0 -1
- package/dist/chunk-M7GGMGMZ.js.map +0 -1
- package/dist/chunk-M7XQSUBB.js.map +0 -1
- /package/dist/{capsule-crypto-CZJSLEFG.js.map → access-token-capabilities.js.map} +0 -0
- /package/dist/{auto-sync-HMHQHUAG.js.map → auto-sync-4A5YBWYO.js.map} +0 -0
- /package/dist/{chunk-6ZIETXZ2.js.map → chunk-23JFRB73.js.map} +0 -0
- /package/dist/{chunk-IC6QX5TC.js.map → chunk-3XIY7MDQ.js.map} +0 -0
- /package/dist/{chunk-LOR6ZM5R.js.map → chunk-4LV23CHK.js.map} +0 -0
- /package/dist/{chunk-AER6MT24.js.map → chunk-6SXVCD7W.js.map} +0 -0
- /package/dist/{chunk-OHDJ34CC.js.map → chunk-72WG5QKN.js.map} +0 -0
- /package/dist/{chunk-CZAZX7JP.js.map → chunk-7MW3CVLD.js.map} +0 -0
- /package/dist/{chunk-CXLNZXG5.js.map → chunk-7XGSIUP5.js.map} +0 -0
- /package/dist/{chunk-CZAA5N5S.js.map → chunk-AYZJID4S.js.map} +0 -0
- /package/dist/{chunk-6BISDEN6.js.map → chunk-BEM4D2QG.js.map} +0 -0
- /package/dist/{chunk-ZPOROUTX.js.map → chunk-CYWA2CXR.js.map} +0 -0
- /package/dist/{chunk-M5QKGHCR.js.map → chunk-CYWCKFCL.js.map} +0 -0
- /package/dist/{chunk-4BFRBP45.js.map → chunk-DEG5ULFJ.js.map} +0 -0
- /package/dist/{chunk-JLR3JSTL.js.map → chunk-DS7MM2ES.js.map} +0 -0
- /package/dist/{chunk-2BFLDH5F.js.map → chunk-EEIROWEG.js.map} +0 -0
- /package/dist/{chunk-X4A62ET7.js.map → chunk-H2DXDQCQ.js.map} +0 -0
- /package/dist/{chunk-27FLVMDC.js.map → chunk-HLN7RROI.js.map} +0 -0
- /package/dist/{chunk-2U2A5HF4.js.map → chunk-J3XHPFEU.js.map} +0 -0
- /package/dist/{chunk-AKUCU7CI.js.map → chunk-JI5AVXYO.js.map} +0 -0
- /package/dist/{chunk-KCJ5H6XQ.js.map → chunk-JSNVWNGX.js.map} +0 -0
- /package/dist/{chunk-FLVL4L3L.js.map → chunk-KTULX4YO.js.map} +0 -0
- /package/dist/{chunk-437S3G37.js.map → chunk-LNQUAJNC.js.map} +0 -0
- /package/dist/{chunk-EPGWWCZF.js.map → chunk-LR5DZ56F.js.map} +0 -0
- /package/dist/{chunk-YDSWRD33.js.map → chunk-N3JDIRBW.js.map} +0 -0
- /package/dist/{chunk-OUMTBFSQ.js.map → chunk-NKCPUHCI.js.map} +0 -0
- /package/dist/{chunk-BGTAL46Y.js.map → chunk-PNLFN2PZ.js.map} +0 -0
- /package/dist/{chunk-P6SYWE4G.js.map → chunk-PVUJ6WVV.js.map} +0 -0
- /package/dist/{chunk-YPEMNNNY.js.map → chunk-RGK27ELN.js.map} +0 -0
- /package/dist/{chunk-2QSZNTDO.js.map → chunk-RKNJBZ55.js.map} +0 -0
- /package/dist/{chunk-66FLU5BT.js.map → chunk-SIRPLF54.js.map} +0 -0
- /package/dist/{chunk-HRCACVHL.js.map → chunk-TLUDFPZL.js.map} +0 -0
- /package/dist/{chunk-2MR3MFQB.js.map → chunk-UPSAT4PY.js.map} +0 -0
- /package/dist/{chunk-4P4626RY.js.map → chunk-UTRJFDP2.js.map} +0 -0
- /package/dist/{chunk-CGS7UKWM.js.map → chunk-VCW74PNH.js.map} +0 -0
- /package/dist/{chunk-5GPPACXK.js.map → chunk-VSMRDBFT.js.map} +0 -0
- /package/dist/{chunk-NG7YBCJO.js.map → chunk-XCL57M6Q.js.map} +0 -0
- /package/dist/{chunk-CLX5VDNY.js.map → chunk-Y4RKAQZ7.js.map} +0 -0
- /package/dist/{chunk-DGYCWCQ7.js.map → chunk-Y7UNNAZD.js.map} +0 -0
- /package/dist/{chunk-EZ25VE3G.js.map → chunk-YNDLCWXS.js.map} +0 -0
- /package/dist/{chunk-XN3TUUX2.js.map → chunk-YQB32INI.js.map} +0 -0
- /package/dist/{first-start-migration-DY6YCRC2.js.map → first-start-migration-VBJQL6OI.js.map} +0 -0
- /package/dist/{tier-stats-TG6GUZ6N.js.map → tier-stats-5XF4UBRQ.js.map} +0 -0
package/src/wearables/config.ts
CHANGED
|
@@ -16,6 +16,7 @@ import type {
|
|
|
16
16
|
WearableCleanupSettings,
|
|
17
17
|
WearableCorrectionRule,
|
|
18
18
|
WearableMemoryMode,
|
|
19
|
+
WearableFusionConfig,
|
|
19
20
|
WearableSourceSettings,
|
|
20
21
|
WearablesConfig,
|
|
21
22
|
} from "./types.js";
|
|
@@ -47,6 +48,11 @@ const MAX_AUTO_SYNC_WINDOW_DAYS = 90;
|
|
|
47
48
|
const DEFAULT_SOURCE_TRUST = 0.8;
|
|
48
49
|
const DEFAULT_AUTO_APPROVE_TRUST = 0.7;
|
|
49
50
|
const DEFAULT_REVIEW_TRUST = 0.45;
|
|
51
|
+
// Cross-source fusion (#1810) — disabled by default; enabling is the
|
|
52
|
+
// only behavior change fusion introduces.
|
|
53
|
+
const DEFAULT_FUSION_ENABLED = false;
|
|
54
|
+
const DEFAULT_FUSION_PROXIMITY_GAP_MS = 300_000;
|
|
55
|
+
const DEFAULT_FUSION_WINDOW_TOLERANCE_MS = 30_000;
|
|
50
56
|
|
|
51
57
|
// Sources whose provider emits per-utterance text but unreliable
|
|
52
58
|
// speaker labels (Bee falls back to a generic "unknown" label when
|
|
@@ -91,6 +97,14 @@ export function defaultWearableSourceSettings(
|
|
|
91
97
|
};
|
|
92
98
|
}
|
|
93
99
|
|
|
100
|
+
export function defaultWearableFusionConfig(): WearableFusionConfig {
|
|
101
|
+
return {
|
|
102
|
+
enabled: DEFAULT_FUSION_ENABLED,
|
|
103
|
+
proximityGapMs: DEFAULT_FUSION_PROXIMITY_GAP_MS,
|
|
104
|
+
windowToleranceMs: DEFAULT_FUSION_WINDOW_TOLERANCE_MS,
|
|
105
|
+
};
|
|
106
|
+
}
|
|
107
|
+
|
|
94
108
|
export function defaultWearablesConfig(): WearablesConfig {
|
|
95
109
|
return {
|
|
96
110
|
enabled: false,
|
|
@@ -104,6 +118,7 @@ export function defaultWearablesConfig(): WearablesConfig {
|
|
|
104
118
|
autoSyncDeepDays: DEFAULT_AUTO_SYNC_DEEP_DAYS,
|
|
105
119
|
corrections: [],
|
|
106
120
|
sources: {},
|
|
121
|
+
fusion: defaultWearableFusionConfig(),
|
|
107
122
|
};
|
|
108
123
|
}
|
|
109
124
|
|
|
@@ -331,6 +346,42 @@ function parseSourceSettings(
|
|
|
331
346
|
* Parse the `wearables` config block. `undefined` yields the disabled
|
|
332
347
|
* default config; any present-but-malformed value throws.
|
|
333
348
|
*/
|
|
349
|
+
function parseFusionSettings(
|
|
350
|
+
value: unknown,
|
|
351
|
+
keyPath: string,
|
|
352
|
+
): WearableFusionConfig {
|
|
353
|
+
if (value === undefined) return defaultWearableFusionConfig();
|
|
354
|
+
const raw = requireObject(value, keyPath);
|
|
355
|
+
const defaults = defaultWearableFusionConfig();
|
|
356
|
+
const parsePositiveInt = (
|
|
357
|
+
value: unknown,
|
|
358
|
+
name: string,
|
|
359
|
+
fallback: number,
|
|
360
|
+
): number => {
|
|
361
|
+
if (value === undefined) return fallback;
|
|
362
|
+
const coerced = coerceNumber(value);
|
|
363
|
+
if (coerced === undefined || !Number.isInteger(coerced) || coerced <= 0) {
|
|
364
|
+
throw new Error(
|
|
365
|
+
`${keyPath}.${name} must be a positive integer (got ${JSON.stringify(value)})`,
|
|
366
|
+
);
|
|
367
|
+
}
|
|
368
|
+
return coerced;
|
|
369
|
+
};
|
|
370
|
+
return {
|
|
371
|
+
enabled: parseBool(raw.enabled, `${keyPath}.enabled`, defaults.enabled),
|
|
372
|
+
proximityGapMs: parsePositiveInt(
|
|
373
|
+
raw.proximityGapMs,
|
|
374
|
+
"proximityGapMs",
|
|
375
|
+
defaults.proximityGapMs,
|
|
376
|
+
),
|
|
377
|
+
windowToleranceMs: parsePositiveInt(
|
|
378
|
+
raw.windowToleranceMs,
|
|
379
|
+
"windowToleranceMs",
|
|
380
|
+
defaults.windowToleranceMs,
|
|
381
|
+
),
|
|
382
|
+
};
|
|
383
|
+
}
|
|
384
|
+
|
|
334
385
|
export function parseWearablesConfig(value: unknown): WearablesConfig {
|
|
335
386
|
if (value === undefined) return defaultWearablesConfig();
|
|
336
387
|
const raw = requireObject(value, "wearables");
|
|
@@ -455,6 +506,7 @@ export function parseWearablesConfig(value: unknown): WearablesConfig {
|
|
|
455
506
|
autoSyncIntervalMinutes,
|
|
456
507
|
autoSyncDays,
|
|
457
508
|
autoSyncDeepDays,
|
|
509
|
+
fusion: parseFusionSettings(raw.fusion, "wearables.fusion"),
|
|
458
510
|
corrections: parseCorrectionRules(raw.corrections, "wearables.corrections"),
|
|
459
511
|
sources,
|
|
460
512
|
};
|
|
@@ -2,12 +2,16 @@ import assert from "node:assert/strict";
|
|
|
2
2
|
import { test } from "node:test";
|
|
3
3
|
|
|
4
4
|
import {
|
|
5
|
+
bodyIsEscaped,
|
|
5
6
|
composeDayTranscriptBody,
|
|
6
7
|
composeDayTranscriptMeta,
|
|
8
|
+
decodeTranscriptBody,
|
|
7
9
|
hashTranscriptBody,
|
|
8
10
|
isValidTranscriptDate,
|
|
9
11
|
parseDayTranscript,
|
|
12
|
+
parseTranscriptSegmentLine,
|
|
10
13
|
serializeDayTranscript,
|
|
14
|
+
TRANSCRIPT_FORMAT_VERSION,
|
|
11
15
|
} from "./day-store.js";
|
|
12
16
|
import { emptySpeakerRegistry } from "./speakers.js";
|
|
13
17
|
import type { WearableConversation } from "./types.js";
|
|
@@ -141,3 +145,245 @@ test("invalid timezone falls back to UTC clock rendering instead of crashing", (
|
|
|
141
145
|
);
|
|
142
146
|
assert.match(body, /## \d{2}:\d{2}–\d{2}:\d{2} · Morning coffee/);
|
|
143
147
|
});
|
|
148
|
+
|
|
149
|
+
|
|
150
|
+
test("decodeTranscriptBody decodes segment text but leaves headings/locations untouched (#1849)", () => {
|
|
151
|
+
// Hand-written stored body: segment text carries the escaped forms
|
|
152
|
+
// (backslash-n for a newline) exactly as composeDayTranscriptBody
|
|
153
|
+
// serializes them, while the title/location are NOT escaped on write.
|
|
154
|
+
const body =
|
|
155
|
+
"# bee transcript — 2026-06-10\n" +
|
|
156
|
+
"\n" +
|
|
157
|
+
"## 09:00–09:10 · C:\\roadmap (conversation c1)\n" +
|
|
158
|
+
"\n" +
|
|
159
|
+
"*Location: D:\\data*\n" +
|
|
160
|
+
"\n" +
|
|
161
|
+
'**Me (you)** [09:00]: Line one.\\nLine two.\n';
|
|
162
|
+
const decoded = decodeTranscriptBody(body, true);
|
|
163
|
+
// Segment text IS decoded: backslash-n -> real newline.
|
|
164
|
+
assert.ok(decoded.includes("Line one.\nLine two."), "segment newline decoded");
|
|
165
|
+
assert.ok(!decoded.includes("Line one.\\nLine two."), "no escaped-newline leak in segment");
|
|
166
|
+
// Title and location are NOT escaped on write, so a literal backslash
|
|
167
|
+
// there must pass through verbatim (only segment text is decoded).
|
|
168
|
+
assert.ok(decoded.includes("· C:\\roadmap (conversation c1)"), "title backslash untouched");
|
|
169
|
+
assert.ok(decoded.includes("*Location: D:\\data*"), "location backslash untouched");
|
|
170
|
+
});
|
|
171
|
+
|
|
172
|
+
test("decodeTranscriptBody recovers original segment text from a composed body (#1849)", () => {
|
|
173
|
+
const original = "First line.\nSecond line with a backslash \\ here.";
|
|
174
|
+
const conversations: WearableConversation[] = [
|
|
175
|
+
{
|
|
176
|
+
id: "c1",
|
|
177
|
+
source: "bee",
|
|
178
|
+
startIso: "2026-06-10T09:00:00Z",
|
|
179
|
+
segments: [{ speakerKey: "user", isWearer: true, text: original }],
|
|
180
|
+
},
|
|
181
|
+
];
|
|
182
|
+
const body = composeDayTranscriptBody("bee", "2026-06-10", "UTC", conversations, REGISTRY);
|
|
183
|
+
// The stored body carries the ESCAPED form, not the original...
|
|
184
|
+
assert.ok(!body.includes(original), "stored body is escaped, not the raw original");
|
|
185
|
+
assert.ok(body.includes("First line.\\nSecond line"), "stored body has the escaped newline");
|
|
186
|
+
// ...and decoding recovers the original utterance verbatim.
|
|
187
|
+
assert.ok(
|
|
188
|
+
decodeTranscriptBody(body, true).includes(original),
|
|
189
|
+
"decoded body recovers the original segment text",
|
|
190
|
+
);
|
|
191
|
+
});
|
|
192
|
+
|
|
193
|
+
|
|
194
|
+
test("speaker label with markdown delimiters round-trips through compose → parse and decodeTranscriptBody (#1849)", () => {
|
|
195
|
+
// A speaker label containing `**`, `[`, `]` — the exact characters that
|
|
196
|
+
// delimit a segment line. Without safe serialization the label breaks
|
|
197
|
+
// parseTranscriptSegmentLine parsing; with escape/unescape it round-trips
|
|
198
|
+
// losslessly through composition, parsing, and display decoding.
|
|
199
|
+
const registry = emptySpeakerRegistry();
|
|
200
|
+
registry.selfName = "Me";
|
|
201
|
+
registry.speakers["bee:SPEAKER_01"] = {
|
|
202
|
+
name: "A**B",
|
|
203
|
+
updatedAt: "2026-06-10T00:00:00Z",
|
|
204
|
+
};
|
|
205
|
+
const conversations: WearableConversation[] = [
|
|
206
|
+
{
|
|
207
|
+
id: "c1",
|
|
208
|
+
source: "bee",
|
|
209
|
+
startIso: "2026-06-10T09:00:00Z",
|
|
210
|
+
segments: [
|
|
211
|
+
{ speakerKey: "user", isWearer: true, text: "Hi there.", startIso: "2026-06-10T09:00:00Z" },
|
|
212
|
+
{ speakerKey: "SPEAKER_01", text: "Delimited label.", startIso: "2026-06-10T09:01:00Z" },
|
|
213
|
+
],
|
|
214
|
+
},
|
|
215
|
+
];
|
|
216
|
+
const body = composeDayTranscriptBody("bee", "2026-06-10", "UTC", conversations, registry);
|
|
217
|
+
// The raw stored body escapes the delimiters so the parser never sees
|
|
218
|
+
// a forged `**` or `[` boundary inside the label.
|
|
219
|
+
assert.ok(
|
|
220
|
+
body.includes("**A\\*\\*B**"),
|
|
221
|
+
"label delimiters are escaped in the stored body: " + body,
|
|
222
|
+
);
|
|
223
|
+
// Display decoding recovers the ORIGINAL label for view/search surfaces.
|
|
224
|
+
const decoded = decodeTranscriptBody(body, true);
|
|
225
|
+
assert.ok(
|
|
226
|
+
decoded.includes("**A**B** [09:01]: Delimited label."),
|
|
227
|
+
"decoded body shows the original unescaped label: " + decoded,
|
|
228
|
+
);
|
|
229
|
+
// parseDayTranscript preserves the body bytes verbatim.
|
|
230
|
+
const serialized = serializeDayTranscript(
|
|
231
|
+
composeDayTranscriptMeta("bee", "2026-06-10", "UTC", conversations, registry, body, "2026-06-11T01:00:00.000Z"),
|
|
232
|
+
body,
|
|
233
|
+
);
|
|
234
|
+
const parsed = parseDayTranscript(serialized);
|
|
235
|
+
assert.ok(parsed);
|
|
236
|
+
assert.equal(parsed!.body, body, "body survives serialization round-trip");
|
|
237
|
+
});
|
|
238
|
+
|
|
239
|
+
test("legacy raw label without escape sequences parses and decodes verbatim (#1849)", () => {
|
|
240
|
+
// A hand-written stored body whose label was NEVER escaped (a pre-
|
|
241
|
+
// escaper transcript). decodeTranscriptBody and the segment-line parser
|
|
242
|
+
// must treat it as a no-op: no backslash sequences => unchanged label.
|
|
243
|
+
const body =
|
|
244
|
+
"# bee transcript — 2026-06-10\n" +
|
|
245
|
+
"\n" +
|
|
246
|
+
"## 09:00–09:10 (conversation c1)\n" +
|
|
247
|
+
"\n" +
|
|
248
|
+
"**Me (you)** [09:00]: Hello world.\n" +
|
|
249
|
+
"**Guest** [09:01]: Good morning.\n";
|
|
250
|
+
const decoded = decodeTranscriptBody(body);
|
|
251
|
+
assert.equal(decoded, body, "legacy body with no escapes decodes verbatim");
|
|
252
|
+
});
|
|
253
|
+
|
|
254
|
+
test("legacy body with literal backslash-n/backslash-r is never decoded (#1849)", () => {
|
|
255
|
+
// A pre-escaper transcript whose segment text contains the LITERAL
|
|
256
|
+
// two-character sequences \n and \r (not escape sequences). The
|
|
257
|
+
// format-aware decoder MUST leave them byte-for-byte unchanged so the
|
|
258
|
+
// original content is never corrupted.
|
|
259
|
+
const legacyBody =
|
|
260
|
+
"# bee transcript — 2026-06-10\n" +
|
|
261
|
+
"\n" +
|
|
262
|
+
"## 09:00–09:10 (conversation c1)\n" +
|
|
263
|
+
"\n" +
|
|
264
|
+
"**Guest** [09:00]: A literal backslash-n \\n and backslash-r \\r here.\n";
|
|
265
|
+
// Default (escaped=false): legacy body is a no-op.
|
|
266
|
+
assert.equal(
|
|
267
|
+
decodeTranscriptBody(legacyBody),
|
|
268
|
+
legacyBody,
|
|
269
|
+
"legacy body must not be decoded without an explicit format marker",
|
|
270
|
+
);
|
|
271
|
+
// Even when explicitly told the body IS escaped, the literal sequences
|
|
272
|
+
// \n / \r WOULD be decoded — but only NEW-format bodies (meta carries
|
|
273
|
+
// formatVersion >= 2) are ever passed escaped=true by the service.
|
|
274
|
+
// Here we verify the SAFETY of the default: callers that forget the
|
|
275
|
+
// flag never corrupt legacy content.
|
|
276
|
+
assert.equal(
|
|
277
|
+
decodeTranscriptBody(legacyBody, false),
|
|
278
|
+
legacyBody,
|
|
279
|
+
"escaped=false leaves legacy literal escapes unchanged",
|
|
280
|
+
);
|
|
281
|
+
});
|
|
282
|
+
|
|
283
|
+
test("new-format body with formatVersion marker decodes correctly (#1849)", () => {
|
|
284
|
+
const conversations: WearableConversation[] = [
|
|
285
|
+
{
|
|
286
|
+
id: "c1",
|
|
287
|
+
source: "bee",
|
|
288
|
+
startIso: "2026-06-10T09:00:00Z",
|
|
289
|
+
segments: [{ speakerKey: "user", isWearer: true, text: "Line one.\nLine two." }],
|
|
290
|
+
},
|
|
291
|
+
];
|
|
292
|
+
const body = composeDayTranscriptBody("bee", "2026-06-10", "UTC", conversations, REGISTRY);
|
|
293
|
+
const meta = composeDayTranscriptMeta(
|
|
294
|
+
"bee", "2026-06-10", "UTC", conversations, REGISTRY, body, "2026-06-11T01:00:00.000Z",
|
|
295
|
+
);
|
|
296
|
+
// The meta carries the format version marker.
|
|
297
|
+
assert.equal(meta.formatVersion, TRANSCRIPT_FORMAT_VERSION);
|
|
298
|
+
assert.ok(bodyIsEscaped(meta), "new-format meta is recognized as escaped");
|
|
299
|
+
// bodyIsEscaped returns false for legacy (no marker) and null.
|
|
300
|
+
assert.equal(bodyIsEscaped(null), false);
|
|
301
|
+
assert.equal(bodyIsEscaped(undefined), false);
|
|
302
|
+
assert.equal(bodyIsEscaped({}), false);
|
|
303
|
+
assert.equal(bodyIsEscaped({ formatVersion: 1 }), false);
|
|
304
|
+
// Decoding with the escaped flag recovers the original newline.
|
|
305
|
+
const decoded = decodeTranscriptBody(body, bodyIsEscaped(meta));
|
|
306
|
+
assert.ok(decoded.includes("Line one.\nLine two."), "new-format body decodes the newline");
|
|
307
|
+
});
|
|
308
|
+
|
|
309
|
+
test("composeDayTranscriptMeta stamps formatVersion and serialize/parse round-trips it (#1849)", () => {
|
|
310
|
+
const body = composeDayTranscriptBody(
|
|
311
|
+
"limitless", "2026-06-10", "UTC", CONVERSATIONS, REGISTRY,
|
|
312
|
+
);
|
|
313
|
+
const meta = composeDayTranscriptMeta(
|
|
314
|
+
"limitless", "2026-06-10", "UTC", CONVERSATIONS, REGISTRY, body, "2026-06-11T01:00:00.000Z",
|
|
315
|
+
);
|
|
316
|
+
assert.equal(meta.formatVersion, TRANSCRIPT_FORMAT_VERSION);
|
|
317
|
+
const serialized = serializeDayTranscript(meta, body);
|
|
318
|
+
assert.ok(serialized.includes("formatVersion: 2"), "serialized file carries the marker");
|
|
319
|
+
const parsed = parseDayTranscript(serialized);
|
|
320
|
+
assert.ok(parsed);
|
|
321
|
+
assert.equal(parsed!.meta.formatVersion, TRANSCRIPT_FORMAT_VERSION);
|
|
322
|
+
// A legacy file (hand-written, no formatVersion line) parses with
|
|
323
|
+
// formatVersion undefined so bodyIsEscaped returns false.
|
|
324
|
+
const legacyFile =
|
|
325
|
+
"---\nkind: wearable-transcript\nsource: \"bee\"\ndate: \"2026-06-10\"\n" +
|
|
326
|
+
"timezone: \"UTC\"\nconversationCount: 1\nsegmentCount: 1\n" +
|
|
327
|
+
"speakers: []\ndurationMinutes: 0\ncontentHash: \"x\"\nsyncedAt: \"y\"\n" +
|
|
328
|
+
"---\n\n# bee transcript — 2026-06-10\n";
|
|
329
|
+
const legacyParsed = parseDayTranscript(legacyFile);
|
|
330
|
+
assert.ok(legacyParsed);
|
|
331
|
+
assert.equal(legacyParsed!.meta.formatVersion, undefined);
|
|
332
|
+
assert.equal(bodyIsEscaped(legacyParsed!.meta), false);
|
|
333
|
+
});
|
|
334
|
+
|
|
335
|
+
test("parseTranscriptSegmentLine extracts label, clock, and text (#1849)", () => {
|
|
336
|
+
const m = parseTranscriptSegmentLine("**Pat** [09:30]: Hello world.");
|
|
337
|
+
assert.deepEqual(m, { label: "Pat", clock: "09:30", text: "Hello world." });
|
|
338
|
+
});
|
|
339
|
+
|
|
340
|
+
test("parseTranscriptSegmentLine handles single-char label and empty text", () => {
|
|
341
|
+
const m = parseTranscriptSegmentLine("**X** [00:00]: ");
|
|
342
|
+
assert.deepEqual(m, { label: "X", clock: "00:00", text: "" });
|
|
343
|
+
});
|
|
344
|
+
|
|
345
|
+
test("parseTranscriptSegmentLine preserves backslash escape sequences in label/text", () => {
|
|
346
|
+
// Escaped labels/text from the escape-aware serializer carry literal
|
|
347
|
+
// backslash sequences that must survive the linear parser unchanged.
|
|
348
|
+
const m = parseTranscriptSegmentLine("**Pat \\*Smith\\*** [09:00]: Hello \\n world.");
|
|
349
|
+
assert.ok(m);
|
|
350
|
+
assert.equal(m!.label, "Pat \\*Smith\\*");
|
|
351
|
+
assert.equal(m!.clock, "09:00");
|
|
352
|
+
assert.equal(m!.text, "Hello \\n world.");
|
|
353
|
+
});
|
|
354
|
+
|
|
355
|
+
test("parseTranscriptSegmentLine returns null for non-segment lines", () => {
|
|
356
|
+
assert.equal(parseTranscriptSegmentLine(""), null);
|
|
357
|
+
assert.equal(parseTranscriptSegmentLine("not a segment"), null);
|
|
358
|
+
assert.equal(parseTranscriptSegmentLine("**no close"), null);
|
|
359
|
+
assert.equal(parseTranscriptSegmentLine("**label** no bracket"), null);
|
|
360
|
+
assert.equal(parseTranscriptSegmentLine("**label** [clock] no colon-space"), null);
|
|
361
|
+
assert.equal(parseTranscriptSegmentLine("**label** [clock]:no space"), null);
|
|
362
|
+
assert.equal(parseTranscriptSegmentLine("**label** []: text"), null);
|
|
363
|
+
});
|
|
364
|
+
|
|
365
|
+
test("parseTranscriptSegmentLine is linear-safe on adversarial repeated delimiters (#1849)", () => {
|
|
366
|
+
// A line with many '*' chars must not cause polynomial backtracking.
|
|
367
|
+
// The linear scan finds the first '**' followed by \s[ — or returns
|
|
368
|
+
// null — without exploring exponential alternatives.
|
|
369
|
+
const longLabel = "a".repeat(10_000);
|
|
370
|
+
const adversarial = `**${longLabel}** [09:00]: text`;
|
|
371
|
+
const m = parseTranscriptSegmentLine(adversarial);
|
|
372
|
+
assert.ok(m, "long label with valid delimiters must parse");
|
|
373
|
+
assert.equal(m!.label, longLabel);
|
|
374
|
+
assert.equal(m!.clock, "09:00");
|
|
375
|
+
assert.equal(m!.text, "text");
|
|
376
|
+
|
|
377
|
+
// All stars, no closing \s[ — must return null quickly.
|
|
378
|
+
const noClose = "**" + "*".repeat(10_000);
|
|
379
|
+
assert.equal(parseTranscriptSegmentLine(noClose), null);
|
|
380
|
+
|
|
381
|
+
// All stars followed by valid suffix — the first '**' positions are
|
|
382
|
+
// all stars (not followed by \s), so the scan walks the entire run
|
|
383
|
+
// in O(n) and finds the '** ' before '['.
|
|
384
|
+
const allStars = "**" + "*".repeat(10_000) + "** [09:00]: text";
|
|
385
|
+
const m2 = parseTranscriptSegmentLine(allStars);
|
|
386
|
+
assert.ok(m2, "star-prefixed line with valid delimiters must parse");
|
|
387
|
+
assert.equal(m2!.clock, "09:00");
|
|
388
|
+
assert.equal(m2!.text, "text");
|
|
389
|
+
});
|
|
@@ -34,6 +34,127 @@ export const WEARABLES_DIR_NAME = "wearables";
|
|
|
34
34
|
|
|
35
35
|
const DATE_PATTERN = /^\d{4}-\d{2}-\d{2}$/;
|
|
36
36
|
|
|
37
|
+
/**
|
|
38
|
+
* Day-transcript body serialization format version. Folded into
|
|
39
|
+
* `hashTranscriptBody` so a version bump invalidates files written by an
|
|
40
|
+
* older serializer and forces an idempotent rewrite. Decoders gate on this:
|
|
41
|
+
* only bodies whose parsed meta carries >= this version have escape
|
|
42
|
+
* sequences decoded; legacy bodies (absent / older) are left byte-for-byte
|
|
43
|
+
* unchanged so a literal two-character `\n`/`\r` in a pre-escaper
|
|
44
|
+
* transcript is never altered (issue #1849).
|
|
45
|
+
*/
|
|
46
|
+
export const TRANSCRIPT_FORMAT_VERSION = 2;
|
|
47
|
+
|
|
48
|
+
/**
|
|
49
|
+
* Result of parsing a rendered transcript segment line.
|
|
50
|
+
*/
|
|
51
|
+
export interface TranscriptSegmentMatch {
|
|
52
|
+
/** Speaker label text between the `**` delimiters. */
|
|
53
|
+
label: string;
|
|
54
|
+
/** Clock text between the `[` `]` delimiters. */
|
|
55
|
+
clock: string;
|
|
56
|
+
/** Segment text after the colon. */
|
|
57
|
+
text: string;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
/**
|
|
61
|
+
* Test a single character code point against the JavaScript regex `\s`
|
|
62
|
+
* whitespace class without invoking a regex. Used by the linear segment
|
|
63
|
+
* line parser so no quantified pattern is ever evaluated on untrusted
|
|
64
|
+
* transcript content (CodeQL polynomial-redos, issue #1849).
|
|
65
|
+
*/
|
|
66
|
+
function isRegexWhitespace(code: number): boolean {
|
|
67
|
+
return (
|
|
68
|
+
(code >= 0x09 && code <= 0x0d) ||
|
|
69
|
+
code === 0x20 ||
|
|
70
|
+
code === 0xa0 ||
|
|
71
|
+
code === 0x1680 ||
|
|
72
|
+
(code >= 0x2000 && code <= 0x200a) ||
|
|
73
|
+
code === 0x2028 ||
|
|
74
|
+
code === 0x2029 ||
|
|
75
|
+
code === 0x202f ||
|
|
76
|
+
code === 0x205f ||
|
|
77
|
+
code === 0x3000 ||
|
|
78
|
+
code === 0xfeff
|
|
79
|
+
);
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
/**
|
|
83
|
+
* Parse a rendered transcript segment line (`**label** [clock]: text`)
|
|
84
|
+
* into its three components using a linear, bounded scan — a CodeQL-safe
|
|
85
|
+
* replacement for the polynomial-risk regex that previously matched this
|
|
86
|
+
* format. Returns null when the line is not a segment line.
|
|
87
|
+
*
|
|
88
|
+
* The scan finds the FIRST occurrence of each structural delimiter
|
|
89
|
+
* (`**` after the label, `]` after the clock) exactly as the original
|
|
90
|
+
* non-greedy regex did, and verifies the fixed separator characters
|
|
91
|
+
* (`\s`, `[`, `:`, `\s`) by single-character code-point comparison.
|
|
92
|
+
* No quantified regex is evaluated on the line (#1849).
|
|
93
|
+
*
|
|
94
|
+
* Escaped labels (formatVersion >= 2) never contain an unescaped `**`,
|
|
95
|
+
* `[`, or `]`, and rendered clocks are always `HH:MM` / `--:--`, so the
|
|
96
|
+
* first delimiter is always the correct one for every well-formed
|
|
97
|
+
* transcript. Legacy unescaped labels are used verbatim by the caller
|
|
98
|
+
* and parsed identically here.
|
|
99
|
+
*/
|
|
100
|
+
export function parseTranscriptSegmentLine(
|
|
101
|
+
line: string,
|
|
102
|
+
): TranscriptSegmentMatch | null {
|
|
103
|
+
// Must start with '**'.
|
|
104
|
+
if (!line.startsWith("**")) return null;
|
|
105
|
+
|
|
106
|
+
// Find the closing '**' of the label: the first '**' at index >= 3
|
|
107
|
+
// (label is at least 1 char) that is immediately followed by \s and
|
|
108
|
+
// then '['. This mirrors the non-greedy regex: the engine tries the
|
|
109
|
+
// shortest label first and extends only when the subsequent fixed
|
|
110
|
+
// delimiters do not line up. An escaped label ending with '\*' is
|
|
111
|
+
// correctly handled because the spurious '**' (escape-star + close-
|
|
112
|
+
// star) is not followed by \s and the scan continues.
|
|
113
|
+
let labelEnd = -1;
|
|
114
|
+
for (let i = 3; i < line.length; i++) {
|
|
115
|
+
if (
|
|
116
|
+
line.charCodeAt(i) !== 0x2a /* '*' */ ||
|
|
117
|
+
i + 3 >= line.length ||
|
|
118
|
+
line.charCodeAt(i + 1) !== 0x2a /* '*' */ ||
|
|
119
|
+
!isRegexWhitespace(line.charCodeAt(i + 2)) ||
|
|
120
|
+
line.charCodeAt(i + 3) !== 0x5b /* '[' */
|
|
121
|
+
) {
|
|
122
|
+
continue;
|
|
123
|
+
}
|
|
124
|
+
labelEnd = i;
|
|
125
|
+
break;
|
|
126
|
+
}
|
|
127
|
+
if (labelEnd === -1) return null;
|
|
128
|
+
|
|
129
|
+
const label = line.slice(2, labelEnd);
|
|
130
|
+
// Clock content starts after the closing '**', the \s, and the '['.
|
|
131
|
+
const clockStart = labelEnd + 4;
|
|
132
|
+
|
|
133
|
+
// Find the closing ']' of the clock: the first ']' at index >=
|
|
134
|
+
// clockStart + 1 (clock is at least 1 char) that is immediately
|
|
135
|
+
// followed by ':' and \s — matching the non-greedy regex.
|
|
136
|
+
let clockEnd = -1;
|
|
137
|
+
for (let i = clockStart + 1; i < line.length; i++) {
|
|
138
|
+
if (
|
|
139
|
+
line.charCodeAt(i) !== 0x5d /* ']' */ ||
|
|
140
|
+
i + 2 >= line.length ||
|
|
141
|
+
line.charCodeAt(i + 1) !== 0x3a /* ':' */ ||
|
|
142
|
+
!isRegexWhitespace(line.charCodeAt(i + 2))
|
|
143
|
+
) {
|
|
144
|
+
continue;
|
|
145
|
+
}
|
|
146
|
+
clockEnd = i;
|
|
147
|
+
break;
|
|
148
|
+
}
|
|
149
|
+
if (clockEnd === -1) return null;
|
|
150
|
+
|
|
151
|
+
return {
|
|
152
|
+
label,
|
|
153
|
+
clock: line.slice(clockStart, clockEnd),
|
|
154
|
+
text: line.slice(clockEnd + 3),
|
|
155
|
+
};
|
|
156
|
+
}
|
|
157
|
+
|
|
37
158
|
export function isValidTranscriptDate(date: string): boolean {
|
|
38
159
|
if (!DATE_PATTERN.test(date)) return false;
|
|
39
160
|
const parsed = new Date(`${date}T00:00:00Z`);
|
|
@@ -41,7 +162,14 @@ export function isValidTranscriptDate(date: string): boolean {
|
|
|
41
162
|
}
|
|
42
163
|
|
|
43
164
|
export function hashTranscriptBody(body: string): string {
|
|
44
|
-
|
|
165
|
+
// The version prefix makes the hash an idempotency key for the body AS
|
|
166
|
+
// SERIALIZED under the current format: when the escape encoding changes
|
|
167
|
+
// (version bump) every existing file's stored hash no longer matches and
|
|
168
|
+
// the pipeline rewrites it with the new marker — no separate migration
|
|
169
|
+
// pass needed (issue #1849).
|
|
170
|
+
return createHash("sha256")
|
|
171
|
+
.update(`v${TRANSCRIPT_FORMAT_VERSION}\n${body}`, "utf-8")
|
|
172
|
+
.digest("hex");
|
|
45
173
|
}
|
|
46
174
|
|
|
47
175
|
function formatClockTime(iso: string | undefined, timezone: string): string {
|
|
@@ -69,6 +197,135 @@ function conversationDurationMinutes(conversation: WearableConversation): number
|
|
|
69
197
|
return (end - start) / 60_000;
|
|
70
198
|
}
|
|
71
199
|
|
|
200
|
+
/**
|
|
201
|
+
* Escape segment text for the line-based markdown format so that embedded
|
|
202
|
+
* newlines, carriage returns, and backslashes survive the serialize →
|
|
203
|
+
* reconstruct round-trip losslessly. Reversed by `unescapeSegmentText`
|
|
204
|
+
* (this module — the single decode primitive shared with the fusion
|
|
205
|
+
* reconstruct path and every view/search/index surface).
|
|
206
|
+
*
|
|
207
|
+
* Without this, a segment whose text contains a newline is split across
|
|
208
|
+
* multiple physical lines; the reconstruct path treats each line
|
|
209
|
+
* independently and the continuation is silently dropped (or worse, a
|
|
210
|
+
* continuation line that looks like a heading/clock is mis-parsed).
|
|
211
|
+
*/
|
|
212
|
+
export function escapeSegmentText(text: string): string {
|
|
213
|
+
return text
|
|
214
|
+
.replace(/\\/g, "\\\\")
|
|
215
|
+
.replace(/\n/g, "\\n")
|
|
216
|
+
.replace(/\r/g, "\\r");
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
/**
|
|
220
|
+
* Reverse `escapeSegmentText`. Unknown escape sequences (a lone
|
|
221
|
+
* backslash followed by a character the escaper never emits) are passed
|
|
222
|
+
* through literally so legacy transcripts that never went through the
|
|
223
|
+
* escaper still round-trip their original text. This is the SINGLE
|
|
224
|
+
* decode primitive: the fusion reconstruct path and every user-facing
|
|
225
|
+
* view/search/index surface call it so escaped storage never leaks to
|
|
226
|
+
* display (#1849).
|
|
227
|
+
*/
|
|
228
|
+
export function unescapeSegmentText(text: string): string {
|
|
229
|
+
return text.replace(/\\(.)/g, (_match, ch: string) => {
|
|
230
|
+
if (ch === "n") return "\n";
|
|
231
|
+
if (ch === "r") return "\r";
|
|
232
|
+
if (ch === "\\") return "\\";
|
|
233
|
+
return "\\" + ch;
|
|
234
|
+
});
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
/**
|
|
238
|
+
* Escape a speaker label for the line-based markdown format so that
|
|
239
|
+
* markdown-delimiter characters in an arbitrary user/provider label
|
|
240
|
+
* (`**`, `[`, `]`), the escape character (`\`), and embedded newlines/
|
|
241
|
+
* carriage returns cannot break `parseTranscriptSegmentLine` parsing.
|
|
242
|
+
* Without this, a label containing `**` (or `** [clock]:`) can make the
|
|
243
|
+
* non-greedy delimiter match land on the wrong `**` and mis-parse the
|
|
244
|
+
* clock/text — or fail to parse the line at all. Reversed by
|
|
245
|
+
* `unescapeSpeakerLabel` (this module). Legacy transcripts whose labels
|
|
246
|
+
* were never escaped still parse: `unescapeSpeakerLabel` is a no-op on
|
|
247
|
+
* any label without a backslash escape sequence (#1849).
|
|
248
|
+
*/
|
|
249
|
+
export function escapeSpeakerLabel(label: string): string {
|
|
250
|
+
return label
|
|
251
|
+
.replace(/\\/g, "\\\\")
|
|
252
|
+
.replace(/\*/g, "\\*")
|
|
253
|
+
.replace(/\[/g, "\\[")
|
|
254
|
+
.replace(/\]/g, "\\]")
|
|
255
|
+
.replace(/\n/g, "\\n")
|
|
256
|
+
.replace(/\r/g, "\\r");
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
/**
|
|
260
|
+
* Reverse `escapeSpeakerLabel`. Unknown escape sequences (a backslash
|
|
261
|
+
* followed by a character the escaper never emits) are passed through
|
|
262
|
+
* literally so legacy transcripts with raw labels still round-trip
|
|
263
|
+
* their original text, mirroring `unescapeSegmentText` for segment text.
|
|
264
|
+
*/
|
|
265
|
+
export function unescapeSpeakerLabel(label: string): string {
|
|
266
|
+
return label.replace(/\\(.)/g, (_match, ch: string) => {
|
|
267
|
+
if (ch === "\\") return "\\";
|
|
268
|
+
if (ch === "*") return "*";
|
|
269
|
+
if (ch === "[") return "[";
|
|
270
|
+
if (ch === "]") return "]";
|
|
271
|
+
if (ch === "n") return "\n";
|
|
272
|
+
if (ch === "r") return "\r";
|
|
273
|
+
return "\\" + ch;
|
|
274
|
+
});
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
/**
|
|
278
|
+
* Whether a parsed day-transcript's body was written by the escape-aware
|
|
279
|
+
* serializer (meta carries `formatVersion` >= `TRANSCRIPT_FORMAT_VERSION`).
|
|
280
|
+
* Legacy bodies (no marker, or an older version) must NOT be decoded: their
|
|
281
|
+
* literal two-character `\n`, `\r`, and lone backslashes are original
|
|
282
|
+
* content, not escape sequences (issue #1849).
|
|
283
|
+
*/
|
|
284
|
+
export function bodyIsEscaped(
|
|
285
|
+
meta: { formatVersion?: number } | null | undefined,
|
|
286
|
+
): boolean {
|
|
287
|
+
return (
|
|
288
|
+
meta !== null &&
|
|
289
|
+
meta !== undefined &&
|
|
290
|
+
typeof meta.formatVersion === "number" &&
|
|
291
|
+
meta.formatVersion >= TRANSCRIPT_FORMAT_VERSION
|
|
292
|
+
);
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
/**
|
|
296
|
+
* Decode the escaped segment text AND speaker label of a stored
|
|
297
|
+
* transcript body into the ORIGINAL forms for user-facing
|
|
298
|
+
* view/search/index surfaces. Only segment lines are touched: the text
|
|
299
|
+
* is decoded via `unescapeSegmentText` and the label via
|
|
300
|
+
* `unescapeSpeakerLabel`; headings, locations, and the H1 header are
|
|
301
|
+
* NOT escaped on write and pass through unchanged, so a title or
|
|
302
|
+
* location containing a literal backslash is never altered. The fusion
|
|
303
|
+
* reconstruct path does NOT use this — it decodes each segment once
|
|
304
|
+
* during parse — so callers feeding bodies back into reconstruct must
|
|
305
|
+
* pass the RAW stored body, not a decoded one, to avoid double-decoding.
|
|
306
|
+
*
|
|
307
|
+
* FORMAT-AWARE (issue #1849): when `escaped` is falsy (the default) the
|
|
308
|
+
* body is returned byte-for-byte unchanged. Only bodies written by the
|
|
309
|
+
* escape-aware serializer (`escaped = true`, derived from
|
|
310
|
+
* `bodyIsEscaped(meta)`) are decoded, so a legacy transcript's literal
|
|
311
|
+
* two-character `\n`/`\r` or lone backslash is never altered. Callers
|
|
312
|
+
* that have the parsed meta should pass `bodyIsEscaped(meta)`.
|
|
313
|
+
*/
|
|
314
|
+
export function decodeTranscriptBody(body: string, escaped = false): string {
|
|
315
|
+
if (!escaped) return body;
|
|
316
|
+
const lines = body.split("\n");
|
|
317
|
+
for (let i = 0; i < lines.length; i++) {
|
|
318
|
+
const match = parseTranscriptSegmentLine(lines[i]);
|
|
319
|
+
if (match === null) continue;
|
|
320
|
+
const { label, clock, text: rawText } = match;
|
|
321
|
+
const decodedText = unescapeSegmentText(rawText);
|
|
322
|
+
const decodedLabel = unescapeSpeakerLabel(label);
|
|
323
|
+
if (decodedText === rawText && decodedLabel === label) continue;
|
|
324
|
+
lines[i] = `**${decodedLabel}** [${clock}]: ${decodedText}`;
|
|
325
|
+
}
|
|
326
|
+
return lines.join("\n");
|
|
327
|
+
}
|
|
328
|
+
|
|
72
329
|
/** Compose the markdown body (no frontmatter) for one source/day. */
|
|
73
330
|
export function composeDayTranscriptBody(
|
|
74
331
|
sourceId: string,
|
|
@@ -101,7 +358,9 @@ export function composeDayTranscriptBody(
|
|
|
101
358
|
for (const segment of conversation.segments) {
|
|
102
359
|
const { label } = resolveSpeaker(sourceId, segment, registry);
|
|
103
360
|
const at = formatClockTime(segment.startIso, timezone);
|
|
104
|
-
lines.push(
|
|
361
|
+
lines.push(
|
|
362
|
+
`**${escapeSpeakerLabel(label)}** [${at}]: ${escapeSegmentText(segment.text)}`,
|
|
363
|
+
);
|
|
105
364
|
}
|
|
106
365
|
lines.push("");
|
|
107
366
|
}
|
|
@@ -132,6 +391,7 @@ export function composeDayTranscriptMeta(
|
|
|
132
391
|
durationMinutes,
|
|
133
392
|
contentHash: hashTranscriptBody(body),
|
|
134
393
|
syncedAt,
|
|
394
|
+
formatVersion: TRANSCRIPT_FORMAT_VERSION,
|
|
135
395
|
};
|
|
136
396
|
}
|
|
137
397
|
|
|
@@ -142,6 +402,9 @@ export function serializeDayTranscript(
|
|
|
142
402
|
): string {
|
|
143
403
|
const lines: string[] = ["---"];
|
|
144
404
|
lines.push(`kind: ${meta.kind}`);
|
|
405
|
+
if (typeof meta.formatVersion === "number") {
|
|
406
|
+
lines.push(`formatVersion: ${meta.formatVersion}`);
|
|
407
|
+
}
|
|
145
408
|
lines.push(`source: ${JSON.stringify(meta.source)}`);
|
|
146
409
|
lines.push(`date: ${JSON.stringify(meta.date)}`);
|
|
147
410
|
lines.push(`timezone: ${JSON.stringify(meta.timezone)}`);
|
|
@@ -204,9 +467,15 @@ export function parseDayTranscript(raw: string): WearableDayTranscript | null {
|
|
|
204
467
|
|
|
205
468
|
const meta: WearableDayTranscriptMeta = {
|
|
206
469
|
kind: "wearable-transcript",
|
|
470
|
+
...(scalars.has("formatVersion")
|
|
471
|
+
? { formatVersion: parseNonNegativeInt(scalars.get("formatVersion")) }
|
|
472
|
+
: {}),
|
|
207
473
|
source,
|
|
208
474
|
date,
|
|
209
|
-
timezone
|
|
475
|
+
// Preserve a missing timezone field as "" rather than coercing to a
|
|
476
|
+
// default: the fusion identity guard must see "no resolvable tz id"
|
|
477
|
+
// and skip, never silently match a known zone.
|
|
478
|
+
timezone: scalars.get("timezone") ?? "",
|
|
210
479
|
conversationCount: parseNonNegativeInt(scalars.get("conversationCount")),
|
|
211
480
|
segmentCount: parseNonNegativeInt(scalars.get("segmentCount")),
|
|
212
481
|
speakers,
|