@remnic/core 9.5.0 → 9.6.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/access-admin-ops-surface.d.ts +6 -6
- package/dist/access-admin-ops-surface.js +12 -12
- package/dist/access-boundary.d.ts +6 -6
- package/dist/access-boundary.js +13 -13
- package/dist/access-cli.js +36 -36
- package/dist/access-http.d.ts +6 -6
- package/dist/access-http.js +17 -17
- package/dist/access-identity-continuity-surface.d.ts +5 -5
- package/dist/access-identity-continuity-surface.js +12 -12
- package/dist/access-lcm-surface.d.ts +6 -6
- package/dist/access-lcm-surface.js +12 -12
- package/dist/access-mcp.d.ts +6 -6
- package/dist/access-mcp.js +16 -16
- package/dist/access-observe-write-surface.d.ts +6 -6
- package/dist/access-observe-write-surface.js +12 -12
- package/dist/access-operations-batch.js +14 -14
- package/dist/access-operations.d.ts +7 -7
- package/dist/access-operations.js +15 -15
- package/dist/access-recall-surface.d.ts +6 -6
- package/dist/access-recall-surface.js +12 -12
- package/dist/access-schema.d.ts +4 -4
- package/dist/{access-service-DZX0sYAv.d.ts → access-service-CXJaql3A.d.ts} +4 -4
- package/dist/access-service.d.ts +6 -6
- package/dist/access-service.js +12 -12
- package/dist/access-surface-catalog.d.ts +6 -6
- 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 +1 -1
- package/dist/{auto-sync-G6IU7L6C.js → auto-sync-4A5YBWYO.js} +3 -3
- 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 +4 -4
- 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/{catalog-DoH2b0OR.d.ts → catalog-C9cu67Ig.d.ts} +1 -1
- package/dist/causal-behavior.d.ts +1 -1
- package/dist/causal-consolidation.d.ts +1 -1
- package/dist/causal-consolidation.js +5 -5
- package/dist/causal-trajectory-graph.d.ts +1 -1
- package/dist/{chunk-H5PFR5ZT.js → chunk-23JFRB73.js} +2 -2
- package/dist/{chunk-BGMSRN6I.js → chunk-3XIY7MDQ.js} +3 -3
- package/dist/{chunk-L2QJQSHL.js → chunk-4GNEDXDI.js} +2 -2
- package/dist/{chunk-MFDJ63U5.js → chunk-4LV23CHK.js} +2 -2
- package/dist/{chunk-AER6MT24.js → chunk-6SXVCD7W.js} +2 -2
- package/dist/{chunk-2SFEFR5I.js → chunk-72WG5QKN.js} +2 -2
- package/dist/{chunk-7BAJAXGB.js → chunk-7MW3CVLD.js} +2 -2
- package/dist/{chunk-SNNIUDRS.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-LNHJ32NK.js → chunk-BEM4D2QG.js} +3 -3
- package/dist/{chunk-AOYJVCFN.js → chunk-CYWA2CXR.js} +2 -2
- package/dist/{chunk-2JFDHI2K.js → chunk-DEG5ULFJ.js} +2 -2
- package/dist/{chunk-L6V3WJ55.js → chunk-DS7MM2ES.js} +3 -3
- package/dist/{chunk-CXEL3Y22.js → chunk-FKCRGLRD.js} +184 -29
- package/dist/{chunk-CXEL3Y22.js.map → chunk-FKCRGLRD.js.map} +1 -1
- package/dist/{chunk-TI7IHDBQ.js → chunk-H2DXDQCQ.js} +5 -5
- package/dist/{chunk-KMF67QDQ.js → chunk-HLN7RROI.js} +4 -4
- package/dist/{chunk-D2XYYU5Q.js → chunk-J3XHPFEU.js} +2 -2
- package/dist/{chunk-ODSYJXDU.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-4WNLSSHL.js → chunk-LADFWYHY.js} +4 -4
- package/dist/{chunk-UBGAOYBP.js → chunk-LR5DZ56F.js} +2 -2
- package/dist/{chunk-5K5VEDFY.js → chunk-N3JDIRBW.js} +2 -2
- package/dist/{chunk-2B7DP7AP.js → chunk-P454KXD6.js} +93 -15
- package/dist/chunk-P454KXD6.js.map +1 -0
- package/dist/{chunk-VWEWK2BM.js → chunk-PCLIWBGD.js} +3 -3
- package/dist/{chunk-TV7C3HCB.js → chunk-PNLFN2PZ.js} +2 -2
- package/dist/{chunk-STCCFQU5.js → chunk-PVUJ6WVV.js} +6 -6
- package/dist/{chunk-ITWTKT2R.js → chunk-RGK27ELN.js} +2 -2
- package/dist/{chunk-II6WHCCW.js → chunk-SIRPLF54.js} +2 -2
- package/dist/{chunk-UDCDGP6A.js → chunk-SVOHGUMI.js} +1188 -112
- package/dist/chunk-SVOHGUMI.js.map +1 -0
- package/dist/{chunk-NFTABV56.js → chunk-TLUDFPZL.js} +2 -2
- package/dist/{chunk-I74ZEBMA.js → chunk-UFUKHGZD.js} +4 -4
- package/dist/{chunk-L4UZBJNM.js → chunk-UTRJFDP2.js} +2 -2
- package/dist/{chunk-5GPPACXK.js → chunk-VSMRDBFT.js} +2 -1
- package/dist/{chunk-SNBEVJBX.js → chunk-XCL57M6Q.js} +3 -3
- package/dist/{chunk-DZSAXE3T.js → chunk-Y7UNNAZD.js} +2 -2
- package/dist/{chunk-XN3TUUX2.js → chunk-YQB32INI.js} +2 -2
- package/dist/{cli-1dghPCO2.d.ts → cli-CqdPA6z9.d.ts} +3 -3
- package/dist/cli.d.ts +7 -7
- package/dist/cli.js +29 -29
- package/dist/compounding/engine.d.ts +2 -2
- package/dist/compounding/engine.js +4 -4
- 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 +1 -1
- package/dist/connectors/codex-materialize-runner.d.ts +1 -1
- package/dist/connectors/codex-materialize-runner.js +4 -4
- package/dist/connectors/codex-materialize.d.ts +1 -1
- package/dist/connectors/index.d.ts +1 -1
- package/dist/connectors/index.js +4 -4
- 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/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/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 +4 -4
- 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/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 +268 -17
- package/dist/index.js +80 -38
- 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/maintenance/memory-governance.d.ts +1 -1
- package/dist/maintenance/memory-governance.js +4 -4
- package/dist/maintenance/rebuild-memory-lifecycle-ledger.js +4 -4
- package/dist/maintenance/rebuild-memory-projection.js +5 -5
- package/dist/mcp-memory-inspector-app.d.ts +6 -6
- package/dist/memory-action-policy.d.ts +1 -1
- package/dist/memory-cache.d.ts +1 -1
- package/dist/memory-lifecycle-ledger-utils.d.ts +1 -1
- package/dist/memory-projection-store.d.ts +1 -1
- package/dist/memory-provenance.d.ts +1 -1
- package/dist/memory-worth-outcomes.d.ts +2 -2
- package/dist/models-json.d.ts +1 -1
- package/dist/namespaces/migrate.d.ts +3 -3
- package/dist/namespaces/migrate.js +11 -11
- package/dist/namespaces/principal.d.ts +1 -1
- package/dist/namespaces/search.d.ts +1 -1
- package/dist/namespaces/search.js +7 -7
- package/dist/namespaces/storage.d.ts +3 -3
- package/dist/namespaces/storage.js +4 -4
- package/dist/native-knowledge.d.ts +1 -1
- package/dist/operator-toolkit.d.ts +2 -2
- package/dist/operator-toolkit.js +15 -15
- package/dist/orchestration/compression-guideline-coordinator.d.ts +2 -2
- package/dist/orchestration/maintenance.d.ts +2 -2
- package/dist/orchestration/maintenance.js +6 -6
- package/dist/{orchestrator-Dh3WSW_O.d.ts → orchestrator-CWFrT8AF.d.ts} +91 -60
- package/dist/orchestrator.d.ts +5 -5
- package/dist/orchestrator.js +36 -36
- 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-DoUVxqND.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 +2 -2
- 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/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 +6 -6
- package/dist/search/index.d.ts +1 -1
- package/dist/search/index.js +6 -6
- 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-Dof1865o.d.ts → semantic-consolidation-O5lMZ4LG.d.ts} +1 -1
- package/dist/semantic-consolidation.d.ts +2 -2
- package/dist/semantic-consolidation.js +5 -5
- package/dist/semantic-rule-promotion.js +4 -4
- package/dist/semantic-rule-verifier.d.ts +1 -1
- package/dist/semantic-rule-verifier.js +4 -4
- 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 +3 -3
- 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/topics.d.ts +1 -1
- package/dist/transcript.d.ts +1 -1
- package/dist/trust-score-stage.d.ts +1 -1
- package/dist/trust-score.d.ts +1 -1
- package/dist/{types-C7HUgOuA.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 +4 -4
- package/package.json +2 -2
- package/src/cli-wearables-fuse.test.ts +197 -0
- package/src/cli.ts +25 -0
- package/src/storage.ts +16 -0
- 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-2B7DP7AP.js.map +0 -1
- package/dist/chunk-M7XQSUBB.js.map +0 -1
- package/dist/chunk-UDCDGP6A.js.map +0 -1
- /package/dist/{auto-sync-G6IU7L6C.js.map → auto-sync-4A5YBWYO.js.map} +0 -0
- /package/dist/{chunk-H5PFR5ZT.js.map → chunk-23JFRB73.js.map} +0 -0
- /package/dist/{chunk-BGMSRN6I.js.map → chunk-3XIY7MDQ.js.map} +0 -0
- /package/dist/{chunk-L2QJQSHL.js.map → chunk-4GNEDXDI.js.map} +0 -0
- /package/dist/{chunk-MFDJ63U5.js.map → chunk-4LV23CHK.js.map} +0 -0
- /package/dist/{chunk-AER6MT24.js.map → chunk-6SXVCD7W.js.map} +0 -0
- /package/dist/{chunk-2SFEFR5I.js.map → chunk-72WG5QKN.js.map} +0 -0
- /package/dist/{chunk-7BAJAXGB.js.map → chunk-7MW3CVLD.js.map} +0 -0
- /package/dist/{chunk-SNNIUDRS.js.map → chunk-AYZJID4S.js.map} +0 -0
- /package/dist/{chunk-LNHJ32NK.js.map → chunk-BEM4D2QG.js.map} +0 -0
- /package/dist/{chunk-AOYJVCFN.js.map → chunk-CYWA2CXR.js.map} +0 -0
- /package/dist/{chunk-2JFDHI2K.js.map → chunk-DEG5ULFJ.js.map} +0 -0
- /package/dist/{chunk-L6V3WJ55.js.map → chunk-DS7MM2ES.js.map} +0 -0
- /package/dist/{chunk-TI7IHDBQ.js.map → chunk-H2DXDQCQ.js.map} +0 -0
- /package/dist/{chunk-KMF67QDQ.js.map → chunk-HLN7RROI.js.map} +0 -0
- /package/dist/{chunk-D2XYYU5Q.js.map → chunk-J3XHPFEU.js.map} +0 -0
- /package/dist/{chunk-ODSYJXDU.js.map → chunk-JSNVWNGX.js.map} +0 -0
- /package/dist/{chunk-4WNLSSHL.js.map → chunk-LADFWYHY.js.map} +0 -0
- /package/dist/{chunk-UBGAOYBP.js.map → chunk-LR5DZ56F.js.map} +0 -0
- /package/dist/{chunk-5K5VEDFY.js.map → chunk-N3JDIRBW.js.map} +0 -0
- /package/dist/{chunk-VWEWK2BM.js.map → chunk-PCLIWBGD.js.map} +0 -0
- /package/dist/{chunk-TV7C3HCB.js.map → chunk-PNLFN2PZ.js.map} +0 -0
- /package/dist/{chunk-STCCFQU5.js.map → chunk-PVUJ6WVV.js.map} +0 -0
- /package/dist/{chunk-ITWTKT2R.js.map → chunk-RGK27ELN.js.map} +0 -0
- /package/dist/{chunk-II6WHCCW.js.map → chunk-SIRPLF54.js.map} +0 -0
- /package/dist/{chunk-NFTABV56.js.map → chunk-TLUDFPZL.js.map} +0 -0
- /package/dist/{chunk-I74ZEBMA.js.map → chunk-UFUKHGZD.js.map} +0 -0
- /package/dist/{chunk-L4UZBJNM.js.map → chunk-UTRJFDP2.js.map} +0 -0
- /package/dist/{chunk-5GPPACXK.js.map → chunk-VSMRDBFT.js.map} +0 -0
- /package/dist/{chunk-SNBEVJBX.js.map → chunk-XCL57M6Q.js.map} +0 -0
- /package/dist/{chunk-DZSAXE3T.js.map → chunk-Y7UNNAZD.js.map} +0 -0
- /package/dist/{chunk-XN3TUUX2.js.map → chunk-YQB32INI.js.map} +0 -0
package/src/wearables/cli.ts
CHANGED
|
@@ -33,6 +33,8 @@ Commands:
|
|
|
33
33
|
--source <id> --from <date> --to <date> --limit <n>
|
|
34
34
|
memories [options] List memories created from transcripts
|
|
35
35
|
--source <id> --date <date> --limit <n>
|
|
36
|
+
fuse <YYYY-MM-DD> Fuse enabled sources' transcripts for a day
|
|
37
|
+
fused <YYYY-MM-DD> List fused conversations for a day
|
|
36
38
|
speakers list Show the speaker registry
|
|
37
39
|
speakers self <name> Set the wearer's display name
|
|
38
40
|
speakers set <source> <key> <name> [--self]
|
|
@@ -419,6 +421,69 @@ export async function runWearablesCliCommand(
|
|
|
419
421
|
`unknown corrections action '${action}' — expected list, add, or remove`,
|
|
420
422
|
);
|
|
421
423
|
}
|
|
424
|
+
case "fuse": {
|
|
425
|
+
const parsed = parseFlags(rest);
|
|
426
|
+
const date = flagString(parsed, "--date") ?? parsed.positional[0];
|
|
427
|
+
if (!date) {
|
|
428
|
+
throw new WearablesInputError(
|
|
429
|
+
"fuse requires a date (e.g. wearables fuse 2026-06-10)",
|
|
430
|
+
);
|
|
431
|
+
}
|
|
432
|
+
const result = await service.fuseDay(date);
|
|
433
|
+
if (parsed.flags.has("--json")) {
|
|
434
|
+
io.stdout.write(`${JSON.stringify(result, null, 2)}\n`);
|
|
435
|
+
return 0;
|
|
436
|
+
}
|
|
437
|
+
// `written:false` has two distinct meanings (issue #1849): a genuine
|
|
438
|
+
// idempotent no-op (inputs unchanged) OR a deliberate SKIP — e.g.
|
|
439
|
+
// mixed source timezones — where fusion was refused and any
|
|
440
|
+
// previously-fused artifact for the day was cleared. The JSON path
|
|
441
|
+
// carries `skipped`, but the text path collapsed both into
|
|
442
|
+
// "(unchanged)", hiding a skip (and a possible artifact deletion)
|
|
443
|
+
// behind an idempotent-looking label. Check `skipped` first and emit
|
|
444
|
+
// a distinct skip line; reserve "(unchanged)" for written:false
|
|
445
|
+
// WITHOUT a skip reason. Mirrors the `skipped` field the JSON path
|
|
446
|
+
// serializes.
|
|
447
|
+
if (result.skipped) {
|
|
448
|
+
io.stdout.write(
|
|
449
|
+
`Skipped fusing ${date}: ${result.skipped.reason} — no fused artifact written; any existing fused artifact for the day was cleared.\n`,
|
|
450
|
+
);
|
|
451
|
+
} else {
|
|
452
|
+
io.stdout.write(
|
|
453
|
+
`Fused ${date}: ${result.conversationCount} conversation${result.conversationCount === 1 ? "" : "s"} from ${result.sources.length} source${result.sources.length === 1 ? "" : "s"}.${result.written ? "" : " (unchanged)"}\n`,
|
|
454
|
+
);
|
|
455
|
+
}
|
|
456
|
+
return 0;
|
|
457
|
+
}
|
|
458
|
+
case "fused": {
|
|
459
|
+
const parsed = parseFlags(rest);
|
|
460
|
+
const date = flagString(parsed, "--date") ?? parsed.positional[0];
|
|
461
|
+
if (!date) {
|
|
462
|
+
throw new WearablesInputError(
|
|
463
|
+
"fused requires a date (e.g. wearables fused 2026-06-10)",
|
|
464
|
+
);
|
|
465
|
+
}
|
|
466
|
+
const conversations = await service.fusedConversations(date);
|
|
467
|
+
if (parsed.flags.has("--json")) {
|
|
468
|
+
io.stdout.write(`${JSON.stringify({ date, conversations }, null, 2)}\n`);
|
|
469
|
+
return 0;
|
|
470
|
+
}
|
|
471
|
+
if (conversations.length === 0) {
|
|
472
|
+
io.stdout.write(`No fused conversations for ${date}.\n`);
|
|
473
|
+
return 0;
|
|
474
|
+
}
|
|
475
|
+
for (const conv of conversations) {
|
|
476
|
+
io.stdout.write(
|
|
477
|
+
`${conv.id} ${conv.startIso}${conv.endIso ? ` \u2013 ${conv.endIso}` : ""} (${conv.sources.join(", ")}, ${conv.segments.length} segment${conv.segments.length === 1 ? "" : "s"})\n`,
|
|
478
|
+
);
|
|
479
|
+
for (const disagreement of conv.disagreements) {
|
|
480
|
+
io.stdout.write(
|
|
481
|
+
` disagreement [${disagreement.kind}] @ ${disagreement.subject}: ${disagreement.candidates.length} candidate${disagreement.candidates.length === 1 ? "" : "s"}\n`,
|
|
482
|
+
);
|
|
483
|
+
}
|
|
484
|
+
}
|
|
485
|
+
return 0;
|
|
486
|
+
}
|
|
422
487
|
default:
|
|
423
488
|
throw new WearablesInputError(
|
|
424
489
|
`unknown wearables command '${command}'\n\n${USAGE}`,
|
|
@@ -245,3 +245,35 @@ test("a deep window narrower than the tick window is rejected, 0 disables", () =
|
|
|
245
245
|
5,
|
|
246
246
|
);
|
|
247
247
|
});
|
|
248
|
+
|
|
249
|
+
|
|
250
|
+
test("fusion defaults to disabled with documented knobs", () => {
|
|
251
|
+
const parsed = parseWearablesConfig({});
|
|
252
|
+
assert.equal(parsed.fusion.enabled, false);
|
|
253
|
+
assert.equal(parsed.fusion.proximityGapMs, 300_000);
|
|
254
|
+
assert.equal(parsed.fusion.windowToleranceMs, 30_000);
|
|
255
|
+
});
|
|
256
|
+
|
|
257
|
+
test("fusion enabled flag and knobs parse and validate", () => {
|
|
258
|
+
const parsed = parseWearablesConfig({
|
|
259
|
+
fusion: { enabled: true, proximityGapMs: 120_000, windowToleranceMs: 15_000 },
|
|
260
|
+
});
|
|
261
|
+
assert.equal(parsed.fusion.enabled, true);
|
|
262
|
+
assert.equal(parsed.fusion.proximityGapMs, 120_000);
|
|
263
|
+
assert.equal(parsed.fusion.windowToleranceMs, 15_000);
|
|
264
|
+
});
|
|
265
|
+
|
|
266
|
+
test("fusion knobs reject non-positive integers", () => {
|
|
267
|
+
assert.throws(
|
|
268
|
+
() => parseWearablesConfig({ fusion: { proximityGapMs: 0 } }),
|
|
269
|
+
/proximityGapMs must be a positive integer/,
|
|
270
|
+
);
|
|
271
|
+
assert.throws(
|
|
272
|
+
() => parseWearablesConfig({ fusion: { windowToleranceMs: -5 } }),
|
|
273
|
+
/windowToleranceMs must be a positive integer/,
|
|
274
|
+
);
|
|
275
|
+
});
|
|
276
|
+
|
|
277
|
+
test("fusion block must be an object when present", () => {
|
|
278
|
+
assert.throws(() => parseWearablesConfig({ fusion: "on" }), /must be an object/);
|
|
279
|
+
});
|
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
|
+
});
|