@remnic/core 9.19.0 → 9.21.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 +8 -8
- package/dist/access-admin-ops-surface.js +8 -8
- package/dist/access-boundary.d.ts +8 -8
- package/dist/access-boundary.js +8 -8
- package/dist/access-cli.js +32 -32
- package/dist/access-http.d.ts +8 -8
- package/dist/access-http.js +12 -12
- package/dist/access-identity-continuity-surface.d.ts +6 -6
- package/dist/access-identity-continuity-surface.js +8 -8
- package/dist/access-lcm-surface.d.ts +8 -8
- package/dist/access-lcm-surface.js +8 -8
- package/dist/access-mcp.d.ts +8 -8
- package/dist/access-mcp.js +11 -11
- package/dist/access-memory-search-fanout.d.ts +2 -2
- package/dist/{access-namespace-preflight-BsawJa-L.d.ts → access-namespace-preflight-tmwQgCT6.d.ts} +1 -1
- package/dist/access-namespace-preflight.d.ts +3 -3
- package/dist/access-namespace-preflight.js +8 -8
- package/dist/access-observe-write-surface.d.ts +8 -8
- package/dist/access-observe-write-surface.js +8 -8
- package/dist/access-operations-batch.js +9 -9
- package/dist/access-operations.d.ts +8 -8
- package/dist/access-operations.js +10 -10
- package/dist/access-recall-concurrency.d.ts +8 -8
- package/dist/access-recall-concurrency.js +8 -8
- package/dist/access-recall-response.d.ts +8 -8
- package/dist/access-recall-response.js +8 -8
- package/dist/access-recall-surface.d.ts +8 -8
- package/dist/access-recall-surface.js +8 -8
- package/dist/{access-service-DLuVnV6a.d.ts → access-service-BqUii7LY.d.ts} +6 -6
- package/dist/access-service.d.ts +8 -8
- package/dist/access-service.js +8 -8
- package/dist/access-surface-catalog.d.ts +8 -8
- 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-IH3G25MT.js → auto-sync-UPTUVE4X.js} +3 -3
- package/dist/behavior-learner.d.ts +1 -1
- package/dist/behavior-signals.d.ts +1 -1
- package/dist/bootstrap.d.ts +6 -6
- 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/bulk-import/index.d.ts +3 -3
- package/dist/calibration.d.ts +1 -1
- package/dist/capabilities.d.ts +1 -1
- package/dist/{catalog-O6P8694A.d.ts → catalog-iWvWz61I.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-YUWFASAK.js → chunk-275ENJDK.js} +2 -2
- package/dist/{chunk-EZFVVEQG.js → chunk-3ZUY2VQF.js} +2 -2
- package/dist/{chunk-HRREDJSL.js → chunk-4F6AANKS.js} +3 -3
- package/dist/{chunk-U7D5HWNG.js → chunk-4PYLBXX6.js} +2 -2
- package/dist/{chunk-3LZSS77C.js → chunk-53WDAG7X.js} +2 -2
- package/dist/{chunk-QEEMLGCO.js → chunk-5IXPWEQB.js} +5 -5
- package/dist/{chunk-VSVX777Y.js → chunk-5JQLAQUZ.js} +2 -2
- package/dist/{chunk-DR65EEXG.js → chunk-62QM5MKY.js} +7 -7
- package/dist/{chunk-WYL5ODHJ.js → chunk-6NZ7U4R4.js} +1 -1
- package/dist/chunk-6NZ7U4R4.js.map +1 -0
- package/dist/{chunk-OFQUY5NZ.js → chunk-7M6APKJL.js} +331 -5
- package/dist/chunk-7M6APKJL.js.map +1 -0
- package/dist/{chunk-OOJQ6ID5.js → chunk-AT3TDN6O.js} +1017 -296
- package/dist/chunk-AT3TDN6O.js.map +1 -0
- package/dist/{chunk-DQCUBRTZ.js → chunk-B7C3TF5H.js} +2 -2
- package/dist/{chunk-RPOOJPMB.js → chunk-EILX7SQR.js} +3 -3
- package/dist/{chunk-D5H3B36B.js → chunk-EXU5IQ3S.js} +2 -2
- package/dist/{chunk-KUF7OIFK.js → chunk-EZKKHVFY.js} +2 -2
- package/dist/{chunk-S5HCKQ7Q.js → chunk-GEUDRENF.js} +2 -2
- package/dist/{chunk-X7GOH7Y6.js → chunk-HJEEAO4O.js} +2 -2
- package/dist/{chunk-FL7MNSH3.js → chunk-HPOY2PHQ.js} +3 -3
- package/dist/{chunk-Q55XM4SU.js → chunk-JFDEYXF7.js} +2 -2
- package/dist/{chunk-UVSDFM7R.js → chunk-JN3IUA2Q.js} +2 -2
- package/dist/{chunk-PLRDZ2OO.js → chunk-JWMYVQAM.js} +2 -2
- package/dist/{chunk-KOFVOVJG.js → chunk-KHRKEELC.js} +17 -17
- package/dist/{chunk-Y4JHE2ML.js → chunk-LI4ZOFHW.js} +2 -2
- package/dist/{chunk-MLW3J7IC.js → chunk-LMHDFOVV.js} +3 -3
- package/dist/{chunk-X6E5WAGX.js → chunk-N7EVMKP6.js} +6 -6
- package/dist/{chunk-HCNG42P3.js → chunk-Q3FWRDCP.js} +2 -2
- package/dist/{chunk-5MFHOX5T.js → chunk-QO2KODU6.js} +2 -2
- package/dist/{chunk-W3DJOT22.js → chunk-TCZWBJBO.js} +2 -2
- package/dist/{chunk-B3ABVQ36.js → chunk-UHVXE24Y.js} +2 -1
- package/dist/{chunk-UX3ZXN34.js → chunk-UV72DEZD.js} +2 -2
- package/dist/{chunk-OZSL2QZC.js → chunk-V5TSUGRB.js} +5 -5
- package/dist/{chunk-CH3BRJ3P.js → chunk-X5QS5C2O.js} +2 -2
- package/dist/{cli-0dQHR6e6.d.ts → cli-DPEhq0Cp.d.ts} +5 -5
- package/dist/cli.d.ts +10 -10
- package/dist/cli.js +25 -25
- 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 +6 -6
- 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/importance.d.ts +1 -1
- package/dist/importers/index.d.ts +1 -1
- package/dist/in-flight-reads.d.ts +1 -1
- package/dist/index.d.ts +468 -88
- package/dist/index.js +66 -36
- 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.d.ts +2 -2
- package/dist/maintenance/rebuild-memory-lifecycle-ledger.js +4 -4
- package/dist/maintenance/rebuild-memory-projection.d.ts +2 -2
- package/dist/maintenance/rebuild-memory-projection.js +5 -5
- package/dist/mcp-memory-inspector-app.d.ts +8 -8
- 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 +5 -5
- package/dist/namespaces/principal.d.ts +1 -1
- package/dist/namespaces/search.d.ts +1 -1
- package/dist/namespaces/storage.d.ts +3 -3
- package/dist/namespaces/storage.js +4 -4
- package/dist/native-knowledge.d.ts +1 -1
- package/dist/offline-sync-impression-drain.d.ts +1 -1
- package/dist/operator-toolkit.d.ts +2 -2
- package/dist/operator-toolkit.js +9 -9
- package/dist/orchestration/compression-guideline-coordinator.d.ts +2 -2
- package/dist/orchestration/maintenance.d.ts +3 -3
- package/dist/orchestration/maintenance.js +9 -9
- package/dist/{orchestrator-B8ickLkd.d.ts → orchestrator-DB93S48I.d.ts} +5 -5
- package/dist/orchestrator.d.ts +6 -6
- package/dist/orchestrator.js +32 -32
- package/dist/patterns-cli.d.ts +1 -1
- package/dist/{pipeline-B669unGm.d.ts → pipeline-DmGc-H66.d.ts} +1 -1
- package/dist/policy-runtime.d.ts +1 -1
- package/dist/proactive-contention.d.ts +1 -1
- package/dist/provenance.d.ts +1 -1
- package/dist/{qmd-D0PVXmea.d.ts → qmd-cJT0crkF.d.ts} +1 -1
- package/dist/qmd-preflight.d.ts +2 -2
- package/dist/qmd-recall-cache.d.ts +1 -1
- package/dist/qmd.d.ts +2 -2
- package/dist/recall-concurrency-config.d.ts +1 -1
- package/dist/recall-disclosure-escalation.d.ts +1 -1
- package/dist/recall-explain-renderer.d.ts +1 -1
- package/dist/recall-explain-renderer.js +3 -3
- package/dist/recall-memory-map.d.ts +1 -1
- package/dist/recall-planner-llm.d.ts +1 -1
- package/dist/recall-state.d.ts +1 -1
- package/dist/recall-tag-filter.d.ts +1 -1
- package/dist/recall-timings.d.ts +1 -1
- package/dist/recall-xray-cli.d.ts +1 -1
- package/dist/recall-xray-cli.js +4 -4
- package/dist/recall-xray-renderer.d.ts +1 -1
- package/dist/recall-xray-renderer.js +3 -3
- package/dist/recall-xray.d.ts +1 -1
- package/dist/recall-xray.js +2 -2
- package/dist/replay/normalizers/chatgpt.d.ts +1 -1
- package/dist/replay/normalizers/claude.d.ts +1 -1
- package/dist/replay/normalizers/openclaw.d.ts +1 -1
- package/dist/replay/normalizers/shared.d.ts +1 -1
- package/dist/replay/runner.d.ts +1 -1
- package/dist/replay/types.d.ts +1 -1
- package/dist/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/salvage-envelope.d.ts +1 -1
- package/dist/{scope-profiles-BOp3gSzp.d.ts → scope-profiles-KG2nODX7.d.ts} +1 -1
- package/dist/search/embed-helper.d.ts +1 -1
- package/dist/search/factory.d.ts +1 -1
- package/dist/search/index.d.ts +1 -1
- package/dist/search/lancedb-backend.d.ts +1 -1
- package/dist/search/meilisearch-backend.d.ts +1 -1
- package/dist/search/noop-backend.d.ts +1 -1
- package/dist/search/orama-backend.d.ts +1 -1
- package/dist/search/port.d.ts +1 -1
- package/dist/search/remote-backend.d.ts +1 -1
- package/dist/{semantic-consolidation-C7SZNzUJ.d.ts → semantic-consolidation-BaZlg0D8.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-CJ_Fia4H.d.ts → storage-BKhHHnRo.d.ts} +2 -221
- package/dist/storage.d.ts +2 -2
- 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-timeline-recall.d.ts +1 -1
- package/dist/temporal-validity.d.ts +1 -1
- package/dist/threading.d.ts +1 -1
- package/dist/tier-migration.d.ts +2 -2
- package/dist/tier-routing.d.ts +1 -1
- package/dist/topics.d.ts +1 -1
- package/dist/transcript.d.ts +1 -1
- package/dist/trust-score-stage.d.ts +1 -1
- package/dist/trust-score.d.ts +1 -1
- package/dist/{types-GeWGXq9E.d.ts → types-GBQZuT7l.d.ts} +395 -1
- package/dist/types.d.ts +1 -1
- package/dist/types.js +1 -1
- package/dist/utility-runtime.d.ts +1 -1
- package/dist/verified-recall.js +4 -4
- package/dist/write-envelope.d.ts +1 -1
- package/package.json +2 -2
- package/src/config.ts +3 -3
- package/src/meetings/build.test.ts +646 -0
- package/src/meetings/build.ts +365 -0
- package/src/meetings/cli.test.ts +276 -0
- package/src/meetings/cli.ts +242 -0
- package/src/meetings/config.test.ts +72 -0
- package/src/meetings/config.ts +183 -0
- package/src/meetings/errors.ts +13 -0
- package/src/meetings/fuse.test.ts +208 -0
- package/src/meetings/fuse.ts +297 -0
- package/src/meetings/index.ts +11 -1
- package/src/meetings/memory-generator.ts +116 -0
- package/src/meetings/recall-exclusion.test.ts +88 -0
- package/src/meetings/store.test.ts +232 -0
- package/src/meetings/store.ts +465 -0
- package/src/meetings/types.ts +111 -0
- package/src/orchestration/orchestrator-helpers.ts +23 -3
- package/src/orchestration/recall-search-pipeline.test.ts +182 -0
- package/src/orchestration/recall-search-pipeline.ts +36 -15
- package/src/types.ts +6 -0
- package/dist/chunk-OFQUY5NZ.js.map +0 -1
- package/dist/chunk-OOJQ6ID5.js.map +0 -1
- package/dist/chunk-WYL5ODHJ.js.map +0 -1
- /package/dist/{auto-sync-IH3G25MT.js.map → auto-sync-UPTUVE4X.js.map} +0 -0
- /package/dist/{chunk-YUWFASAK.js.map → chunk-275ENJDK.js.map} +0 -0
- /package/dist/{chunk-EZFVVEQG.js.map → chunk-3ZUY2VQF.js.map} +0 -0
- /package/dist/{chunk-HRREDJSL.js.map → chunk-4F6AANKS.js.map} +0 -0
- /package/dist/{chunk-U7D5HWNG.js.map → chunk-4PYLBXX6.js.map} +0 -0
- /package/dist/{chunk-3LZSS77C.js.map → chunk-53WDAG7X.js.map} +0 -0
- /package/dist/{chunk-QEEMLGCO.js.map → chunk-5IXPWEQB.js.map} +0 -0
- /package/dist/{chunk-VSVX777Y.js.map → chunk-5JQLAQUZ.js.map} +0 -0
- /package/dist/{chunk-DR65EEXG.js.map → chunk-62QM5MKY.js.map} +0 -0
- /package/dist/{chunk-DQCUBRTZ.js.map → chunk-B7C3TF5H.js.map} +0 -0
- /package/dist/{chunk-RPOOJPMB.js.map → chunk-EILX7SQR.js.map} +0 -0
- /package/dist/{chunk-D5H3B36B.js.map → chunk-EXU5IQ3S.js.map} +0 -0
- /package/dist/{chunk-KUF7OIFK.js.map → chunk-EZKKHVFY.js.map} +0 -0
- /package/dist/{chunk-S5HCKQ7Q.js.map → chunk-GEUDRENF.js.map} +0 -0
- /package/dist/{chunk-X7GOH7Y6.js.map → chunk-HJEEAO4O.js.map} +0 -0
- /package/dist/{chunk-FL7MNSH3.js.map → chunk-HPOY2PHQ.js.map} +0 -0
- /package/dist/{chunk-Q55XM4SU.js.map → chunk-JFDEYXF7.js.map} +0 -0
- /package/dist/{chunk-UVSDFM7R.js.map → chunk-JN3IUA2Q.js.map} +0 -0
- /package/dist/{chunk-PLRDZ2OO.js.map → chunk-JWMYVQAM.js.map} +0 -0
- /package/dist/{chunk-KOFVOVJG.js.map → chunk-KHRKEELC.js.map} +0 -0
- /package/dist/{chunk-Y4JHE2ML.js.map → chunk-LI4ZOFHW.js.map} +0 -0
- /package/dist/{chunk-MLW3J7IC.js.map → chunk-LMHDFOVV.js.map} +0 -0
- /package/dist/{chunk-X6E5WAGX.js.map → chunk-N7EVMKP6.js.map} +0 -0
- /package/dist/{chunk-HCNG42P3.js.map → chunk-Q3FWRDCP.js.map} +0 -0
- /package/dist/{chunk-5MFHOX5T.js.map → chunk-QO2KODU6.js.map} +0 -0
- /package/dist/{chunk-W3DJOT22.js.map → chunk-TCZWBJBO.js.map} +0 -0
- /package/dist/{chunk-B3ABVQ36.js.map → chunk-UHVXE24Y.js.map} +0 -0
- /package/dist/{chunk-UX3ZXN34.js.map → chunk-UV72DEZD.js.map} +0 -0
- /package/dist/{chunk-OZSL2QZC.js.map → chunk-V5TSUGRB.js.map} +0 -0
- /package/dist/{chunk-CH3BRJ3P.js.map → chunk-X5QS5C2O.js.map} +0 -0
|
@@ -0,0 +1,365 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Meeting building orchestration (issue #1900, Phase 4 slice 2).
|
|
3
|
+
*
|
|
4
|
+
* Ties the pure pieces together: detect the day's meetings, fuse each window's
|
|
5
|
+
* transcript + screen context, and compose idempotent records. Day data is
|
|
6
|
+
* supplied through an injected source so fixtures (and the later activity/
|
|
7
|
+
* wearables wiring) share one code path — no hidden IO here.
|
|
8
|
+
*
|
|
9
|
+
* Gated on `meetings.enabled`: a disabled subsystem builds nothing on every
|
|
10
|
+
* surface (degradation-matrix row). Degrades within an enabled build too — a
|
|
11
|
+
* window with no audio yields an empty transcript, and no activity yields a
|
|
12
|
+
* record with no screen-context section — because `fuseMeeting` handles both.
|
|
13
|
+
*/
|
|
14
|
+
|
|
15
|
+
import { detectMeetings } from "./detect.js";
|
|
16
|
+
import { fuseMeeting, type MeetingFusionOptions } from "./fuse.js";
|
|
17
|
+
import { composeMeetingRecord, type MeetingRecordStore, type MeetingRecordSummary } from "./store.js";
|
|
18
|
+
import { log } from "../logger.js";
|
|
19
|
+
import type {
|
|
20
|
+
MeetingEpisodeGenResult,
|
|
21
|
+
MeetingMemoryGenerator,
|
|
22
|
+
MeetingMemoryOutcome,
|
|
23
|
+
MeetingsDayFactGenResult,
|
|
24
|
+
} from "./memory-generator.js";
|
|
25
|
+
import type { FusionConversationInput } from "../wearables/fusion/types.js";
|
|
26
|
+
import type {
|
|
27
|
+
DetectedMeeting,
|
|
28
|
+
MeetingActivitySnapshot,
|
|
29
|
+
MeetingRecord,
|
|
30
|
+
MeetingsConfig,
|
|
31
|
+
MeetingsDetectionInput,
|
|
32
|
+
} from "./types.js";
|
|
33
|
+
|
|
34
|
+
/** All of a day's already-loaded signals a build needs. */
|
|
35
|
+
export interface MeetingDayData {
|
|
36
|
+
/** App spans + audio windows for the day (fed to the detector). */
|
|
37
|
+
detection: MeetingsDetectionInput;
|
|
38
|
+
/** Fusion conversation inputs across all wearable sources for the day. */
|
|
39
|
+
conversations: FusionConversationInput[];
|
|
40
|
+
/** Screen-activity snapshots for the day. */
|
|
41
|
+
activity?: MeetingActivitySnapshot[];
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
/** Supplies a day's signals. Backed by fixtures in tests; by the activity +
|
|
45
|
+
* wearables stores in the wiring slice. */
|
|
46
|
+
export interface MeetingsDaySource {
|
|
47
|
+
loadDayData(date: string): Promise<MeetingDayData> | MeetingDayData;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
export function buildMeetingRecordsForDay(
|
|
51
|
+
data: MeetingDayData,
|
|
52
|
+
config: MeetingsConfig,
|
|
53
|
+
options: MeetingFusionOptions = {},
|
|
54
|
+
/** Map a detected meeting to the id its record should carry. Defaults to the
|
|
55
|
+
* detector's start-anchored id; the builder overrides this to preserve an
|
|
56
|
+
* existing overlapping record's id across shifted-start resyncs. */
|
|
57
|
+
resolveMeetingId: (meeting: DetectedMeeting) => string = (meeting) => meeting.id,
|
|
58
|
+
): MeetingRecord[] {
|
|
59
|
+
const meetings = detectMeetings(data.detection, {
|
|
60
|
+
appPatterns: config.appPatterns,
|
|
61
|
+
minOverlapMinutes: config.minOverlapMinutes,
|
|
62
|
+
audioOnlyMinMinutes: config.audioOnlyMinMinutes,
|
|
63
|
+
mergeGapMinutes: config.mergeGapMinutes,
|
|
64
|
+
});
|
|
65
|
+
return meetings.map((meeting) => {
|
|
66
|
+
const fused = fuseMeeting(
|
|
67
|
+
{ meeting, conversations: data.conversations, ...(data.activity !== undefined ? { activity: data.activity } : {}) },
|
|
68
|
+
config,
|
|
69
|
+
options,
|
|
70
|
+
);
|
|
71
|
+
return composeMeetingRecord(
|
|
72
|
+
{
|
|
73
|
+
id: resolveMeetingId(meeting),
|
|
74
|
+
date: meeting.date,
|
|
75
|
+
startUtc: meeting.startUtc,
|
|
76
|
+
endUtc: meeting.endUtc,
|
|
77
|
+
...(meeting.app !== undefined ? { app: meeting.app } : {}),
|
|
78
|
+
detectionSource: meeting.detectionSource,
|
|
79
|
+
...(meeting.title !== undefined ? { title: meeting.title } : {}),
|
|
80
|
+
},
|
|
81
|
+
fused,
|
|
82
|
+
);
|
|
83
|
+
});
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
/** Per-meeting outcome of a day build. */
|
|
87
|
+
export interface MeetingBuildOutcome {
|
|
88
|
+
id: string;
|
|
89
|
+
startUtc: string;
|
|
90
|
+
endUtc: string;
|
|
91
|
+
detectionSource: string;
|
|
92
|
+
sources: string[];
|
|
93
|
+
attendees: string[];
|
|
94
|
+
snapshotCount: number;
|
|
95
|
+
/** False when the record's contentHash was unchanged (nothing rewritten). */
|
|
96
|
+
written: boolean;
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
/** Result of building one day's meetings. */
|
|
100
|
+
export interface MeetingsDayBuildSummary {
|
|
101
|
+
date: string;
|
|
102
|
+
/** False when `meetings.enabled` is off (zero behavior). */
|
|
103
|
+
enabled: boolean;
|
|
104
|
+
meetings: MeetingBuildOutcome[];
|
|
105
|
+
/** Records written (new or changed). */
|
|
106
|
+
built: number;
|
|
107
|
+
/** Records skipped (unchanged contentHash). */
|
|
108
|
+
skipped: number;
|
|
109
|
+
/** Ids of stale same-day records deleted because they no longer detect. */
|
|
110
|
+
removed: string[];
|
|
111
|
+
/**
|
|
112
|
+
* Set when the optional reindex hook rejected AFTER records were already
|
|
113
|
+
* persisted. The store mutation still succeeded — the build is reported as
|
|
114
|
+
* successful with this generic warning, and the next scheduled index catches
|
|
115
|
+
* up. Deliberately does NOT embed the raw error text: this string is printed
|
|
116
|
+
* to the CLI's stdout, and internal error detail is logged separately.
|
|
117
|
+
*/
|
|
118
|
+
reindexWarning?: string;
|
|
119
|
+
/**
|
|
120
|
+
* Set when the injected `memoryGenerator` rejected AFTER records were already
|
|
121
|
+
* persisted, OR when a successful generation returned non-fatal warnings. Like
|
|
122
|
+
* `reindexWarning`, the record writes still succeeded, so the build is reported
|
|
123
|
+
* successful with this warning; on rejection the next build (or scheduled
|
|
124
|
+
* maintenance) regenerates the missing memories. Carries no raw exception text —
|
|
125
|
+
* only the generic rejection message or the generator's own warning strings.
|
|
126
|
+
*/
|
|
127
|
+
memoryWarning?: string;
|
|
128
|
+
/**
|
|
129
|
+
* Deterministic episode-memory counts, present only when a `memoryGenerator`
|
|
130
|
+
* was injected. One recall-anchor episode is written per built record (idempotent).
|
|
131
|
+
*/
|
|
132
|
+
episodes?: MeetingEpisodeGenResult;
|
|
133
|
+
/**
|
|
134
|
+
* Trust-gated summary/fact counts (decisions, commitments, questions, and the
|
|
135
|
+
* synthesized summary), present only when a `memoryGenerator` with summary deps
|
|
136
|
+
* ran and `summaryMode !== "off"`. Aggregated across the day's records.
|
|
137
|
+
*/
|
|
138
|
+
facts?: MeetingsDayFactGenResult;
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
/** Optional side effects the builder fires after a day build. */
|
|
142
|
+
export interface MeetingsBuilderHooks {
|
|
143
|
+
/**
|
|
144
|
+
* Invoked once per build when records changed (`built > 0 || removed > 0`).
|
|
145
|
+
* Meeting records live inside the QMD collection root for full-text search,
|
|
146
|
+
* so a host that wants them discoverable immediately (rather than at the next
|
|
147
|
+
* scheduled reindex) supplies a reindex here. No-op when omitted — records are
|
|
148
|
+
* still picked up by the next index update.
|
|
149
|
+
*/
|
|
150
|
+
reindex?(summary: MeetingsDayBuildSummary): void | Promise<void>;
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
/** Orchestrates a store-backed build of a day's meetings. */
|
|
154
|
+
export class MeetingsBuilder {
|
|
155
|
+
constructor(
|
|
156
|
+
private readonly opts: {
|
|
157
|
+
source: MeetingsDaySource;
|
|
158
|
+
store: MeetingRecordStore;
|
|
159
|
+
config: MeetingsConfig;
|
|
160
|
+
fusionOptions?: MeetingFusionOptions;
|
|
161
|
+
hooks?: MeetingsBuilderHooks;
|
|
162
|
+
/**
|
|
163
|
+
* When present, the memory-generation layer runs after the record-store
|
|
164
|
+
* writes: deterministic recall-anchor episodes per built record, the
|
|
165
|
+
* trust-gated summary/facts layer, and retract-on-removal — all behind the
|
|
166
|
+
* `MeetingMemoryGenerator` seam (issue #1900). Omitted → pure
|
|
167
|
+
* detect+fuse+store with no memory writes. The engine depends only on this
|
|
168
|
+
* interface, never on the memory-gen module.
|
|
169
|
+
*/
|
|
170
|
+
memoryGenerator?: MeetingMemoryGenerator;
|
|
171
|
+
},
|
|
172
|
+
) {}
|
|
173
|
+
|
|
174
|
+
async buildDay(date: string): Promise<MeetingsDayBuildSummary> {
|
|
175
|
+
if (!this.opts.config.enabled) {
|
|
176
|
+
return { date, enabled: false, meetings: [], built: 0, skipped: 0, removed: [] };
|
|
177
|
+
}
|
|
178
|
+
const data = await this.opts.source.loadDayData(date);
|
|
179
|
+
// The source contract is loadDayData(date) → THAT day's signals. If the
|
|
180
|
+
// returned detection day disagrees, records would be written under
|
|
181
|
+
// data.detection.date while listing/adoption/reconciliation run on `date`,
|
|
182
|
+
// orphaning records and mis-keying the summary — reject the desync loudly.
|
|
183
|
+
if (data.detection.date !== date) {
|
|
184
|
+
throw new RangeError(
|
|
185
|
+
`meetings: day source returned detection.date "${data.detection.date}" for requested day "${date}".`,
|
|
186
|
+
);
|
|
187
|
+
}
|
|
188
|
+
// Existing records for the day, so an overlapping shifted-start resync keeps
|
|
189
|
+
// its original id instead of being deleted + re-created under a new one.
|
|
190
|
+
const existing = await this.opts.store.listMeetingSummaries(date);
|
|
191
|
+
const claimed = new Set<string>();
|
|
192
|
+
const resolveMeetingId = (meeting: DetectedMeeting): string =>
|
|
193
|
+
this.adoptExistingId(meeting, existing, claimed);
|
|
194
|
+
const records = buildMeetingRecordsForDay(
|
|
195
|
+
data,
|
|
196
|
+
this.opts.config,
|
|
197
|
+
this.opts.fusionOptions ?? {},
|
|
198
|
+
resolveMeetingId,
|
|
199
|
+
);
|
|
200
|
+
const meetings: MeetingBuildOutcome[] = [];
|
|
201
|
+
let built = 0;
|
|
202
|
+
let skipped = 0;
|
|
203
|
+
const builtIds = new Set<string>();
|
|
204
|
+
// Ids that existed before THIS build, so a same-id rewrite (contentHash
|
|
205
|
+
// changed) is distinguished from a brand-new record and from an untouched one.
|
|
206
|
+
const priorIds = new Set(existing.map((summary) => summary.id));
|
|
207
|
+
// Built ids whose stored record was IDENTICAL this build (nothing rewritten):
|
|
208
|
+
// the generator must skip them so an idempotent rebuild triggers no LLM calls
|
|
209
|
+
// and no duplicate memories.
|
|
210
|
+
const unchangedIds: string[] = [];
|
|
211
|
+
// Built ids that already existed and were rewritten this build (same id, new
|
|
212
|
+
// contentHash): the generator must refresh (retract + regenerate) their stale
|
|
213
|
+
// episode/summary memories.
|
|
214
|
+
const updatedIds: string[] = [];
|
|
215
|
+
for (const record of records) {
|
|
216
|
+
const save = await this.opts.store.saveMeetingRecord(record);
|
|
217
|
+
if (save.written) {
|
|
218
|
+
built++;
|
|
219
|
+
if (priorIds.has(record.id)) updatedIds.push(record.id);
|
|
220
|
+
} else {
|
|
221
|
+
skipped++;
|
|
222
|
+
unchangedIds.push(record.id);
|
|
223
|
+
}
|
|
224
|
+
builtIds.add(record.id);
|
|
225
|
+
meetings.push({
|
|
226
|
+
id: record.id,
|
|
227
|
+
startUtc: record.startUtc,
|
|
228
|
+
endUtc: record.endUtc,
|
|
229
|
+
detectionSource: record.detectionSource,
|
|
230
|
+
sources: record.sources,
|
|
231
|
+
attendees: record.attendees,
|
|
232
|
+
snapshotCount: record.snapshotCount,
|
|
233
|
+
written: save.written,
|
|
234
|
+
});
|
|
235
|
+
}
|
|
236
|
+
// Reconcile genuinely stale records: a prior build's meeting the current
|
|
237
|
+
// rebuild neither reproduced nor adopted (by overlap) is deleted so it does
|
|
238
|
+
// not linger and orphan its provenance. Delete only within THIS day's dir.
|
|
239
|
+
const removed: string[] = [];
|
|
240
|
+
for (const existingId of await this.opts.store.listMeetingIds(date)) {
|
|
241
|
+
if (builtIds.has(existingId)) continue;
|
|
242
|
+
await this.opts.store.deleteMeetingRecord(date, existingId);
|
|
243
|
+
removed.push(existingId);
|
|
244
|
+
}
|
|
245
|
+
const summary: MeetingsDayBuildSummary = { date, enabled: true, meetings, built, skipped, removed };
|
|
246
|
+
// Memory generation runs behind the seam: the deterministic engine hands the
|
|
247
|
+
// built records, removed ids, and the unchanged/updated id sets to the
|
|
248
|
+
// injected generator and folds the returned counts + reindex signal into the
|
|
249
|
+
// summary. The engine never imports the memory-gen module.
|
|
250
|
+
//
|
|
251
|
+
// The record writes above already succeeded; a generator rejection must NOT
|
|
252
|
+
// fail the whole build and lose those persisted records. Isolate it exactly
|
|
253
|
+
// like the reindex hook: surface a generic warning, log the detail, and let
|
|
254
|
+
// the next build regenerate the missing memories.
|
|
255
|
+
let outcome: MeetingMemoryOutcome | undefined;
|
|
256
|
+
if (this.opts.memoryGenerator !== undefined) {
|
|
257
|
+
try {
|
|
258
|
+
outcome = await this.opts.memoryGenerator.onRecordsBuilt({
|
|
259
|
+
built: records,
|
|
260
|
+
removedIds: removed,
|
|
261
|
+
unchangedIds,
|
|
262
|
+
updatedIds,
|
|
263
|
+
});
|
|
264
|
+
} catch (err) {
|
|
265
|
+
log.error("meetings: memory generation failed after records were persisted", err);
|
|
266
|
+
summary.memoryWarning = "memory generation failed after records were persisted; see logs for detail";
|
|
267
|
+
}
|
|
268
|
+
}
|
|
269
|
+
if (outcome?.episodes !== undefined) summary.episodes = outcome.episodes;
|
|
270
|
+
if (outcome?.facts !== undefined) summary.facts = outcome.facts;
|
|
271
|
+
// A SUCCESSFUL generation can still raise non-fatal warnings (e.g. a summary
|
|
272
|
+
// dep degraded). The fatal catch above leaves `outcome` undefined, so these
|
|
273
|
+
// paths are mutually exclusive: fold the generator's own warning strings into
|
|
274
|
+
// the same `memoryWarning` the CLI/JSON already surfaces so a green build does
|
|
275
|
+
// not silently hide them. These are generator-authored strings, not raw error
|
|
276
|
+
// text, so surfacing them is safe.
|
|
277
|
+
if (outcome?.warnings !== undefined && outcome.warnings.length > 0) {
|
|
278
|
+
summary.memoryWarning = outcome.warnings.join("; ");
|
|
279
|
+
}
|
|
280
|
+
// Meeting records AND their episode/summary memories live inside the QMD
|
|
281
|
+
// collection root. Fire the isolated reindex whenever anything discoverable
|
|
282
|
+
// changed — records written/removed (engine) OR newly written episodes/facts
|
|
283
|
+
// (folded into outcome.reindexNeeded). A rebuild whose records were unchanged
|
|
284
|
+
// (built == 0 && removed == 0) but whose prior episode write had failed still
|
|
285
|
+
// creates active memories that must be indexed now, not left dark until the
|
|
286
|
+
// next scheduled maintenance pass.
|
|
287
|
+
if (built > 0 || removed.length > 0 || (outcome?.reindexNeeded ?? false)) {
|
|
288
|
+
// The store mutation already succeeded; a reindex-hook rejection must not
|
|
289
|
+
// fail the whole build. Log the detail and surface a GENERIC warning — the
|
|
290
|
+
// raw error must not leak to the CLI's stdout — then let the next scheduled
|
|
291
|
+
// index update catch up.
|
|
292
|
+
try {
|
|
293
|
+
await this.opts.hooks?.reindex?.(summary);
|
|
294
|
+
} catch (err) {
|
|
295
|
+
log.error("meetings: reindex hook failed after records were persisted", err);
|
|
296
|
+
summary.reindexWarning = "reindex hook failed after records were persisted; see logs for detail";
|
|
297
|
+
}
|
|
298
|
+
}
|
|
299
|
+
return summary;
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
/**
|
|
303
|
+
* Pick the existing same-day record whose window best overlaps `meeting` and
|
|
304
|
+
* reuse its id — but ONLY when adoption is safe: the windows substantially
|
|
305
|
+
* coincide (overlap covers a majority of the shorter window), OR a corroborating
|
|
306
|
+
* identity signal (same app, same title, or a shared transcript source) is
|
|
307
|
+
* BACKED by a non-marginal overlap (>= a quarter of the shorter window). Coarse
|
|
308
|
+
* identity signals alone never license adoption across a thin sliver of overlap,
|
|
309
|
+
* or two genuinely different back-to-back same-app meetings would overwrite each
|
|
310
|
+
* other's record and `show <old-id>` would point at the wrong meeting. Candidate
|
|
311
|
+
* selection is deterministic (max overlap, then earliest start, then id).
|
|
312
|
+
* Returns the detector's fresh id when nothing qualifies.
|
|
313
|
+
*/
|
|
314
|
+
private adoptExistingId(
|
|
315
|
+
meeting: DetectedMeeting,
|
|
316
|
+
existing: readonly MeetingRecordSummary[],
|
|
317
|
+
claimed: Set<string>,
|
|
318
|
+
): string {
|
|
319
|
+
const dStart = Date.parse(meeting.startUtc);
|
|
320
|
+
const dEnd = Date.parse(meeting.endUtc);
|
|
321
|
+
if (!Number.isFinite(dStart) || !Number.isFinite(dEnd)) return meeting.id;
|
|
322
|
+
let best: { record: MeetingRecordSummary; overlap: number; existingDurationMs: number } | undefined;
|
|
323
|
+
for (const record of existing) {
|
|
324
|
+
if (claimed.has(record.id)) continue;
|
|
325
|
+
const eStart = Date.parse(record.startUtc);
|
|
326
|
+
const eEnd = Date.parse(record.endUtc);
|
|
327
|
+
if (!Number.isFinite(eStart) || !Number.isFinite(eEnd)) continue;
|
|
328
|
+
const overlap = Math.min(dEnd, eEnd) - Math.max(dStart, eStart);
|
|
329
|
+
if (overlap <= 0) continue;
|
|
330
|
+
const better =
|
|
331
|
+
best === undefined ||
|
|
332
|
+
overlap > best.overlap ||
|
|
333
|
+
(overlap === best.overlap &&
|
|
334
|
+
(record.startUtc < best.record.startUtc ||
|
|
335
|
+
(record.startUtc === best.record.startUtc && record.id < best.record.id)));
|
|
336
|
+
if (better) best = { record, overlap, existingDurationMs: eEnd - eStart };
|
|
337
|
+
}
|
|
338
|
+
if (best === undefined) return meeting.id;
|
|
339
|
+
// Adoption bar (issue #1900): substantial overlap OR a corroborating identity
|
|
340
|
+
// signal that is BACKED by a non-marginal overlap — otherwise treat as
|
|
341
|
+
// removed + new. App name and transcript-source label are COARSE (many
|
|
342
|
+
// distinct meetings carry "Zoom"/"desktop"), so an identity match on its own
|
|
343
|
+
// must not license adoption across a thin sliver of overlap, or two genuinely
|
|
344
|
+
// different back-to-back same-app meetings would clobber each other's record.
|
|
345
|
+
const shorterWindowMs = Math.min(dEnd - dStart, best.existingDurationMs);
|
|
346
|
+
if (shorterWindowMs <= 0) return meeting.id;
|
|
347
|
+
// A shifted-start resync of the SAME meeting: overlap covers a majority of the
|
|
348
|
+
// shorter window. Adopts on its own.
|
|
349
|
+
const substantial = best.overlap * 2 >= shorterWindowMs;
|
|
350
|
+
const sharedSource = meeting.sources.some((source) => best!.record.sources.includes(source));
|
|
351
|
+
const sameApp =
|
|
352
|
+
meeting.app !== undefined &&
|
|
353
|
+
best.record.app !== undefined &&
|
|
354
|
+
meeting.app.toLowerCase() === best.record.app.toLowerCase();
|
|
355
|
+
const meetingTitle = meeting.title?.trim() ?? "";
|
|
356
|
+
const sameTitle = meetingTitle.length > 0 && meetingTitle === (best.record.title?.trim() ?? "");
|
|
357
|
+
const hasIdentitySignal = sharedSource || sameApp || sameTitle;
|
|
358
|
+
// Non-marginal overlap gate: an identity signal only corroborates when the
|
|
359
|
+
// windows overlap by at least a quarter of the shorter window.
|
|
360
|
+
const nonMarginalOverlap = best.overlap * 4 >= shorterWindowMs;
|
|
361
|
+
if (!substantial && !(hasIdentitySignal && nonMarginalOverlap)) return meeting.id;
|
|
362
|
+
claimed.add(best.record.id);
|
|
363
|
+
return best.record.id;
|
|
364
|
+
}
|
|
365
|
+
}
|
|
@@ -0,0 +1,276 @@
|
|
|
1
|
+
import assert from "node:assert/strict";
|
|
2
|
+
import { test } from "node:test";
|
|
3
|
+
|
|
4
|
+
import { MeetingsBuilder, type MeetingDayData, type MeetingsDaySource } from "./build.js";
|
|
5
|
+
import { runMeetingsCliCommand, type MeetingsCliDeps } from "./cli.js";
|
|
6
|
+
import { DEFAULT_MEETINGS_CONFIG } from "./config.js";
|
|
7
|
+
import { MeetingRecordStore, type MeetingRecordFileIo } from "./store.js";
|
|
8
|
+
import type { MeetingsConfig } from "./types.js";
|
|
9
|
+
|
|
10
|
+
const MEMORY_DIR = "/mem";
|
|
11
|
+
const DATE = "2026-03-10";
|
|
12
|
+
const START = "2026-03-10T14:00:00.000Z";
|
|
13
|
+
const END = "2026-03-10T15:00:00.000Z";
|
|
14
|
+
|
|
15
|
+
class InMemoryIo implements MeetingRecordFileIo {
|
|
16
|
+
files = new Map<string, string>();
|
|
17
|
+
async writeFile(p: string, c: string): Promise<void> {
|
|
18
|
+
this.files.set(p, c);
|
|
19
|
+
}
|
|
20
|
+
async readFile(p: string): Promise<string> {
|
|
21
|
+
const v = this.files.get(p);
|
|
22
|
+
if (v === undefined) throw enoent();
|
|
23
|
+
return v;
|
|
24
|
+
}
|
|
25
|
+
async readDir(dirPath: string): Promise<string[]> {
|
|
26
|
+
const prefix = dirPath.endsWith("/") ? dirPath : `${dirPath}/`;
|
|
27
|
+
const names = new Set<string>();
|
|
28
|
+
let found = false;
|
|
29
|
+
for (const key of this.files.keys()) {
|
|
30
|
+
if (!key.startsWith(prefix)) continue;
|
|
31
|
+
found = true;
|
|
32
|
+
names.add(key.slice(prefix.length).split("/")[0]!);
|
|
33
|
+
}
|
|
34
|
+
if (!found) throw enoent();
|
|
35
|
+
return [...names];
|
|
36
|
+
}
|
|
37
|
+
async deleteFile(p: string): Promise<void> {
|
|
38
|
+
if (!this.files.delete(p)) throw enoent();
|
|
39
|
+
}
|
|
40
|
+
async realpath(p: string): Promise<string> {
|
|
41
|
+
return p;
|
|
42
|
+
}
|
|
43
|
+
async lstat(): Promise<{ isSymbolicLink: boolean }> {
|
|
44
|
+
return { isSymbolicLink: false };
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
function enoent(): NodeJS.ErrnoException {
|
|
49
|
+
const err = new Error("ENOENT") as NodeJS.ErrnoException;
|
|
50
|
+
err.code = "ENOENT";
|
|
51
|
+
return err;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
function config(overrides: Partial<MeetingsConfig> = {}): MeetingsConfig {
|
|
55
|
+
return { ...DEFAULT_MEETINGS_CONFIG, appPatterns: [...DEFAULT_MEETINGS_CONFIG.appPatterns], enabled: true, ...overrides };
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
function dayData(): MeetingDayData {
|
|
59
|
+
return {
|
|
60
|
+
detection: { date: DATE, appSpans: [{ app: "Zoom", startUtc: START, endUtc: END }], audioWindows: [{ source: "desktop", startUtc: START, endUtc: END, distinctNonWearerSpeakers: 2 }] },
|
|
61
|
+
conversations: [
|
|
62
|
+
{ source: "desktop", conversationId: "d1", startIso: START, endIso: END, segments: [{ speaker: "Jane", isSelf: false, text: "hello everyone", startIso: "2026-03-10T14:05:00.000Z" }] },
|
|
63
|
+
],
|
|
64
|
+
};
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
function makeDeps(data: MeetingDayData = dayData(), cfg: MeetingsConfig = config()): MeetingsCliDeps {
|
|
68
|
+
const source: MeetingsDaySource = { loadDayData: () => data };
|
|
69
|
+
const store = new MeetingRecordStore(MEMORY_DIR, new InMemoryIo());
|
|
70
|
+
return { store, builder: new MeetingsBuilder({ source, store, config: cfg }), config: cfg };
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
interface Captured {
|
|
74
|
+
out: string;
|
|
75
|
+
err: string;
|
|
76
|
+
code: number;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
async function run(deps: MeetingsCliDeps, args: string[]): Promise<Captured> {
|
|
80
|
+
let out = "";
|
|
81
|
+
let err = "";
|
|
82
|
+
const code = await runMeetingsCliCommand(deps, args, {
|
|
83
|
+
stdout: { write: (c: string) => (out += c) },
|
|
84
|
+
stderr: { write: (c: string) => (err += c) },
|
|
85
|
+
});
|
|
86
|
+
return { out, err, code };
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
test("build --date requires a valid date", async () => {
|
|
90
|
+
const deps = makeDeps();
|
|
91
|
+
const missing = await run(deps, ["build"]);
|
|
92
|
+
assert.equal(missing.code, 1);
|
|
93
|
+
assert.match(missing.err, /--date <YYYY-MM-DD> is required/);
|
|
94
|
+
|
|
95
|
+
const bad = await run(deps, ["build", "--date", "2026-13-40"]);
|
|
96
|
+
assert.equal(bad.code, 1);
|
|
97
|
+
assert.match(bad.err, /real YYYY-MM-DD/);
|
|
98
|
+
});
|
|
99
|
+
|
|
100
|
+
test("list --date rejects a malformed date and a missing flag value", async () => {
|
|
101
|
+
const deps = makeDeps();
|
|
102
|
+
const bad = await run(deps, ["list", "--date", "nope"]);
|
|
103
|
+
assert.equal(bad.code, 1);
|
|
104
|
+
assert.match(bad.err, /real YYYY-MM-DD/);
|
|
105
|
+
|
|
106
|
+
const noValue = await run(deps, ["list", "--date"]);
|
|
107
|
+
assert.equal(noValue.code, 1);
|
|
108
|
+
assert.match(noValue.err, /requires a value/);
|
|
109
|
+
});
|
|
110
|
+
|
|
111
|
+
test("unknown flags and commands error with guidance", async () => {
|
|
112
|
+
const deps = makeDeps();
|
|
113
|
+
const flag = await run(deps, ["list", "--bogus"]);
|
|
114
|
+
assert.equal(flag.code, 1);
|
|
115
|
+
assert.match(flag.err, /unknown flag --bogus/);
|
|
116
|
+
|
|
117
|
+
const cmd = await run(deps, ["frobnicate"]);
|
|
118
|
+
assert.equal(cmd.code, 1);
|
|
119
|
+
assert.match(cmd.err, /unknown meetings command 'frobnicate'/);
|
|
120
|
+
});
|
|
121
|
+
|
|
122
|
+
test("show validates the id and reports a missing record", async () => {
|
|
123
|
+
const deps = makeDeps();
|
|
124
|
+
const noId = await run(deps, ["show"]);
|
|
125
|
+
assert.equal(noId.code, 1);
|
|
126
|
+
assert.match(noId.err, /requires a meeting id/);
|
|
127
|
+
|
|
128
|
+
const badId = await run(deps, ["show", "not-an-id"]);
|
|
129
|
+
assert.equal(badId.code, 1);
|
|
130
|
+
assert.match(badId.err, /invalid meeting id/);
|
|
131
|
+
|
|
132
|
+
const missing = await run(deps, ["show", "mtg-2026-03-10-abcdef01"]);
|
|
133
|
+
assert.equal(missing.code, 1);
|
|
134
|
+
assert.match(missing.err, /not found/);
|
|
135
|
+
});
|
|
136
|
+
|
|
137
|
+
test("build then list then show round-trips a real record", async () => {
|
|
138
|
+
const deps = makeDeps();
|
|
139
|
+
const built = await run(deps, ["build", "--date", DATE]);
|
|
140
|
+
assert.equal(built.code, 0);
|
|
141
|
+
assert.match(built.out, /1 meeting \(1 written/);
|
|
142
|
+
|
|
143
|
+
const ids = await deps.store.listMeetingIds(DATE);
|
|
144
|
+
assert.equal(ids.length, 1);
|
|
145
|
+
const id = ids[0]!;
|
|
146
|
+
|
|
147
|
+
const listed = await run(deps, ["list", "--date", DATE]);
|
|
148
|
+
assert.equal(listed.code, 0);
|
|
149
|
+
assert.match(listed.out, new RegExp(id));
|
|
150
|
+
assert.match(listed.out, /sources=desktop/);
|
|
151
|
+
|
|
152
|
+
const shown = await run(deps, ["show", id]);
|
|
153
|
+
assert.equal(shown.code, 0);
|
|
154
|
+
assert.match(shown.out, /kind: meeting/);
|
|
155
|
+
assert.match(shown.out, /\*\*Jane\*\* \[14:05\]: hello everyone/);
|
|
156
|
+
});
|
|
157
|
+
|
|
158
|
+
test("list --json emits machine-readable output", async () => {
|
|
159
|
+
const deps = makeDeps();
|
|
160
|
+
await run(deps, ["build", "--date", DATE]);
|
|
161
|
+
const json = await run(deps, ["list", "--date", DATE, "--json"]);
|
|
162
|
+
assert.equal(json.code, 0);
|
|
163
|
+
const parsed = JSON.parse(json.out) as Array<{ id: string; sources: string[] }>;
|
|
164
|
+
assert.equal(parsed.length, 1);
|
|
165
|
+
assert.deepEqual(parsed[0]?.sources, ["desktop"]);
|
|
166
|
+
});
|
|
167
|
+
|
|
168
|
+
test("build reports the disabled state when the subsystem is off", async () => {
|
|
169
|
+
const deps = makeDeps(dayData(), config({ enabled: false }));
|
|
170
|
+
const result = await run(deps, ["build", "--date", DATE]);
|
|
171
|
+
assert.equal(result.code, 0);
|
|
172
|
+
assert.match(result.out, /meetings disabled/);
|
|
173
|
+
});
|
|
174
|
+
|
|
175
|
+
test("show rejects a syntactically valid but impossible calendar id via MeetingsInputError", async () => {
|
|
176
|
+
const deps = makeDeps();
|
|
177
|
+
// mtg-2026-13-40-... matches the id SHAPE but 2026-13-40 is not a real date;
|
|
178
|
+
// this must be a clean input error (exit 1), never a 500 from the path validator.
|
|
179
|
+
const result = await run(deps, ["show", "mtg-2026-13-40-abcdef01"]);
|
|
180
|
+
assert.equal(result.code, 1);
|
|
181
|
+
assert.match(result.err, /not a real calendar date/);
|
|
182
|
+
});
|
|
183
|
+
|
|
184
|
+
test("build text output surfaces a SANITIZED reindex-hook warning (no raw error text)", async () => {
|
|
185
|
+
const source: MeetingsDaySource = { loadDayData: () => dayData() };
|
|
186
|
+
const store = new MeetingRecordStore(MEMORY_DIR, new InMemoryIo());
|
|
187
|
+
const deps: MeetingsCliDeps = {
|
|
188
|
+
store,
|
|
189
|
+
builder: new MeetingsBuilder({
|
|
190
|
+
source,
|
|
191
|
+
store,
|
|
192
|
+
config: config(),
|
|
193
|
+
hooks: { reindex: () => { throw new Error("index offline at 10.0.0.5:9200"); } },
|
|
194
|
+
}),
|
|
195
|
+
config: config(),
|
|
196
|
+
};
|
|
197
|
+
const result = await run(deps, ["build", "--date", DATE]);
|
|
198
|
+
assert.equal(result.code, 0, "reindex failure must not fail the build");
|
|
199
|
+
assert.match(result.out, /warning: reindex hook failed after records were persisted/);
|
|
200
|
+
assert.doesNotMatch(result.out, /index offline|10\.0\.0\.5|9200/, "raw internal reindex error must not leak to CLI stdout");
|
|
201
|
+
});
|
|
202
|
+
|
|
203
|
+
test("findings 7+8 — list/show expose a record when enabled, nothing when meetings.enabled is false", async () => {
|
|
204
|
+
// Build a real record with the subsystem enabled, then prove EACH read surface
|
|
205
|
+
// surfaces that record when enabled (so the disabled negatives below are not
|
|
206
|
+
// vacuous), then flip config off and confirm the same surfaces expose nothing.
|
|
207
|
+
const store = new MeetingRecordStore(MEMORY_DIR, new InMemoryIo());
|
|
208
|
+
const source: MeetingsDaySource = { loadDayData: () => dayData() };
|
|
209
|
+
const enabled: MeetingsCliDeps = { store, builder: new MeetingsBuilder({ source, store, config: config() }), config: config() };
|
|
210
|
+
await run(enabled, ["build", "--date", DATE]);
|
|
211
|
+
const id = (await store.listMeetingIds(DATE))[0]!;
|
|
212
|
+
|
|
213
|
+
// Positive baseline: enabled reads DO surface the record on every path.
|
|
214
|
+
const listAllEnabled = await run(enabled, ["list"]);
|
|
215
|
+
assert.match(listAllEnabled.out, new RegExp(id), "enabled list surfaces the stored record");
|
|
216
|
+
const listDayEnabled = await run(enabled, ["list", "--date", DATE]);
|
|
217
|
+
assert.match(listDayEnabled.out, new RegExp(id), "enabled date-scoped list surfaces the stored record");
|
|
218
|
+
const listJsonEnabled = await run(enabled, ["list", "--date", DATE, "--json"]);
|
|
219
|
+
assert.match(listJsonEnabled.out, new RegExp(id), "enabled date-scoped json list surfaces the stored record");
|
|
220
|
+
const shownEnabled = await run(enabled, ["show", id]);
|
|
221
|
+
assert.match(shownEnabled.out, /kind: meeting/, "enabled show prints the record");
|
|
222
|
+
assert.match(shownEnabled.out, new RegExp(id), "enabled show prints the requested record's id");
|
|
223
|
+
|
|
224
|
+
// Now the same surfaces with the subsystem OFF must expose nothing.
|
|
225
|
+
const disabled: MeetingsCliDeps = {
|
|
226
|
+
store,
|
|
227
|
+
builder: new MeetingsBuilder({ source, store, config: config({ enabled: false }) }),
|
|
228
|
+
config: config({ enabled: false }),
|
|
229
|
+
};
|
|
230
|
+
const listAll = await run(disabled, ["list"]);
|
|
231
|
+
assert.equal(listAll.code, 0);
|
|
232
|
+
assert.doesNotMatch(listAll.out, new RegExp(id), "disabled list must not expose a stored record");
|
|
233
|
+
assert.match(listAll.out, /meetings disabled/);
|
|
234
|
+
|
|
235
|
+
const listDay = await run(disabled, ["list", "--date", DATE]);
|
|
236
|
+
assert.equal(listDay.code, 0);
|
|
237
|
+
assert.doesNotMatch(listDay.out, new RegExp(id), "disabled date-scoped list must not expose the record it surfaces when enabled");
|
|
238
|
+
|
|
239
|
+
const listJson = await run(disabled, ["list", "--date", DATE, "--json"]);
|
|
240
|
+
assert.equal(listJson.out.trim(), "[]", "disabled json list exposes nothing");
|
|
241
|
+
|
|
242
|
+
const shown = await run(disabled, ["show", id]);
|
|
243
|
+
assert.equal(shown.code, 0);
|
|
244
|
+
assert.doesNotMatch(shown.out, /kind: meeting/, "disabled show must not print the record it prints when enabled");
|
|
245
|
+
assert.doesNotMatch(shown.out, new RegExp(id), "disabled show must not echo the record id");
|
|
246
|
+
assert.match(shown.out, /meetings disabled/);
|
|
247
|
+
});
|
|
248
|
+
|
|
249
|
+
test("build text output surfaces a SANITIZED memory-generation warning (no raw error text)", async () => {
|
|
250
|
+
const source: MeetingsDaySource = { loadDayData: () => dayData() };
|
|
251
|
+
const store = new MeetingRecordStore(MEMORY_DIR, new InMemoryIo());
|
|
252
|
+
const deps: MeetingsCliDeps = {
|
|
253
|
+
store,
|
|
254
|
+
builder: new MeetingsBuilder({
|
|
255
|
+
source,
|
|
256
|
+
store,
|
|
257
|
+
config: config(),
|
|
258
|
+
// The injected generator rejects AFTER records persist; buildDay isolates
|
|
259
|
+
// it into summary.memoryWarning. The text renderer must surface that line.
|
|
260
|
+
memoryGenerator: { onRecordsBuilt: async () => { throw new Error("qdrant offline at 10.0.0.9:6333"); } },
|
|
261
|
+
}),
|
|
262
|
+
config: config(),
|
|
263
|
+
};
|
|
264
|
+
const result = await run(deps, ["build", "--date", DATE]);
|
|
265
|
+
assert.equal(result.code, 0, "memory-generation failure must not fail the build");
|
|
266
|
+
assert.match(result.out, /warning: memory generation failed after records were persisted/);
|
|
267
|
+
assert.doesNotMatch(result.out, /qdrant offline|10\.0\.0\.9|6333/, "raw internal memory-gen error must not leak to CLI stdout");
|
|
268
|
+
});
|
|
269
|
+
|
|
270
|
+
test("list --date rejects a malformed date even when meetings.enabled is false", async () => {
|
|
271
|
+
const deps = makeDeps(dayData(), config({ enabled: false }));
|
|
272
|
+
const bad = await run(deps, ["list", "--date", "nope", "--json"]);
|
|
273
|
+
assert.equal(bad.code, 1, "a malformed --date must reject regardless of enabled state");
|
|
274
|
+
assert.match(bad.err, /real YYYY-MM-DD/);
|
|
275
|
+
assert.doesNotMatch(bad.out, /\[\]/, "malformed --date must not short-circuit to an empty disabled result");
|
|
276
|
+
});
|