@remnic/core 9.12.0 → 9.13.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/index.d.ts CHANGED
@@ -6970,6 +6970,110 @@ declare function composeActivityDigestMeta(date: string, machines: string[], sna
6970
6970
  declare function serializeActivityDigest(meta: ActivityDayMeta, body: string): string;
6971
6971
  declare function parseActivityDigest(raw: string): ActivityDayDigest | null;
6972
6972
 
6973
+ /**
6974
+ * Meeting-intelligence subsystem — shared types (issue #1900).
6975
+ *
6976
+ * Retrospective meeting detection over already-ingested day signals: audio
6977
+ * conversation windows (from wearable day transcripts, any source) plus
6978
+ * meeting-app foreground spans (derived from screen activity in a later slice).
6979
+ * This slice is pure detection — no store, no fusion, no surfaces. All
6980
+ * timestamps are UTC ISO-8601; windows are half-open [startUtc, endUtc).
6981
+ */
6982
+ /** A contiguous meeting-app foreground span (derived from activity in a later slice). */
6983
+ interface MeetingAppSpan {
6984
+ /** Meeting app label (e.g. "Zoom", "Google Meet"). */
6985
+ app: string;
6986
+ startUtc: string;
6987
+ endUtc: string;
6988
+ }
6989
+ /** An audio conversation window from a wearable/connector day transcript. */
6990
+ interface MeetingAudioWindow {
6991
+ /** Wearable source id the conversation came from (desktop, limitless, granola, …). */
6992
+ source: string;
6993
+ startUtc: string;
6994
+ endUtc: string;
6995
+ /** Distinct non-wearer speakers in the conversation (drives the audio-only rule). */
6996
+ distinctNonWearerSpeakers: number;
6997
+ /**
6998
+ * True when the source is a cloud meeting provider that supplies explicit
6999
+ * meeting boundaries (Granola/Fireflies): such a window is a meeting on its
7000
+ * own, without a matching app span.
7001
+ */
7002
+ providerMeeting?: boolean;
7003
+ /** Provider-supplied meeting title, when available. */
7004
+ title?: string;
7005
+ }
7006
+ /** How a meeting was detected. */
7007
+ type MeetingDetectionSource = "app+audio" | "audio" | "provider";
7008
+ /** One detected meeting for a day (non-overlapping after merge). */
7009
+ interface DetectedMeeting {
7010
+ /** Stable id `mtg-<date>-<hash>`, hashed from date + the exact START instant
7011
+ * (app + end deliberately excluded) so a resync that grows the meeting never
7012
+ * renumbers it, while non-overlapping meetings keep distinct ids. */
7013
+ id: string;
7014
+ /** Local day YYYY-MM-DD. */
7015
+ date: string;
7016
+ startUtc: string;
7017
+ endUtc: string;
7018
+ /** Meeting app, when app context contributed to detection. */
7019
+ app?: string;
7020
+ detectionSource: MeetingDetectionSource;
7021
+ /** Contributing wearable source ids, sorted, de-duplicated. */
7022
+ sources: string[];
7023
+ /** Title, when a provider supplied one. */
7024
+ title?: string;
7025
+ }
7026
+ /** Per-day detection input (assembled by a later wiring slice). */
7027
+ interface MeetingsDetectionInput {
7028
+ date: string;
7029
+ appSpans: MeetingAppSpan[];
7030
+ audioWindows: MeetingAudioWindow[];
7031
+ }
7032
+ /** Detection-relevant configuration (full parseConfig wiring lands in a later slice). */
7033
+ interface MeetingsDetectionConfig {
7034
+ /** Meeting-app match patterns (used when deriving app spans from activity). */
7035
+ appPatterns: string[];
7036
+ /** Min app-span ∩ audio-window overlap to pair them (minutes). */
7037
+ minOverlapMinutes: number;
7038
+ /** Audio-only fallback: min conversation length (minutes). */
7039
+ audioOnlyMinMinutes: number;
7040
+ /** Merge candidates within this gap of each other (minutes). */
7041
+ mergeGapMinutes: number;
7042
+ }
7043
+
7044
+ /**
7045
+ * Retrospective meeting detection (issue #1900, Phase 4 slice 1).
7046
+ *
7047
+ * Pure functions over a day's already-ingested signals. A meeting candidate is
7048
+ * either (a) an audio conversation overlapping a meeting-app foreground span
7049
+ * (`app+audio`), (b) a provider meeting with its own boundaries (`provider`),
7050
+ * or (c) a long enough multi-speaker conversation with no app span
7051
+ * (`audio`, the phone-call/in-person fallback). App spans with no overlapping
7052
+ * audio are NOT meetings (you were watching a recording). Candidates are then
7053
+ * merged so a day's meetings never overlap, and each gets a re-run-stable id.
7054
+ */
7055
+
7056
+ /** Shipped meeting-app patterns (used by the later activity-span derivation). */
7057
+ declare const DEFAULT_MEETING_APP_PATTERNS: readonly string[];
7058
+ declare const DEFAULT_MEETINGS_DETECTION_CONFIG: MeetingsDetectionConfig;
7059
+ /** Re-run-stable id: same date + exact START instant ⇒ same id. Anchored on the
7060
+ * start ONLY (end + app both excluded from the hash) so a resync that extends
7061
+ * the meeting's end (a late source / rejoin) or reassigns its app never
7062
+ * renumbers an existing record — the start is the stable identity. Full start
7063
+ * precision (NOT minute-rounded) keeps ids unique even for short provider
7064
+ * meetings that share a start minute: post-merge meetings are non-overlapping,
7065
+ * so their start instants are always distinct.
7066
+ *
7067
+ * A resync that moves a meeting's START earlier (a late source beginning before
7068
+ * the first-ingested one) does change the id — a stateless pure detector cannot
7069
+ * know the prior id. Preserving ids across a shifted start is cross-run identity
7070
+ * work that needs prior-emission state, so it belongs to the fusion/store slice
7071
+ * (#1900), which matches a re-detected meeting to its stored record by overlap
7072
+ * and keeps the original id. This function stays pure and deterministic. */
7073
+ declare function meetingId(date: string, startUtc: string): string;
7074
+ /** Detect the day's non-overlapping meetings from its audio + app-span signals. */
7075
+ declare function detectMeetings(input: MeetingsDetectionInput, config?: MeetingsDetectionConfig): DetectedMeeting[];
7076
+
6973
7077
  type LocalSessionRole = "user" | "assistant" | "tool" | "system" | "other";
6974
7078
  interface LocalSessionTurn {
6975
7079
  role: LocalSessionRole;
@@ -7286,4 +7390,4 @@ declare function forkCapsule(opts: ForkCapsuleOptions): Promise<ForkCapsuleResul
7286
7390
  */
7287
7391
  declare function readForkLineage(targetRoot: string, forkId: string): Promise<ForkLineage | null>;
7288
7392
 
7289
- export { ACTIVITY_DIGEST_FORMAT_VERSION, ACTIVITY_DIR_NAME, type ActivityDayDigest, type ActivityDayMeta, type ActivitySnapshot, type ActivitySnapshotPage, type ActivitySourceCheck, type ActivitySourceClient, ActivityStore, type ArchitectureHintsResult, type AuditEntry, type BinaryAssetRecord, type BinaryAssetStatus, type BinaryLifecycleConfig, type BinaryLifecycleManifest, type BinaryStorageBackend, type BinaryStorageBackendConfig, ClaudeCodeMemoryExtensionPublisher, type CleanupResult, CodexMemoryExtensionPublisher, type CodingNamespaceOverlay, type CodingScopeDescription, type CollectLocalSessionSummariesOptions, type CompiledCorrectionRule, type CompiledRedactionRule, type ConflictEntry, type ContradictionOptions, type ContradictionPair, type ContradictionResult$1 as CurateContradictionResult, type DuplicateResult as CurateDuplicateResult, type CurateOptions, type CurateResult, type CuratedStatement, DEFAULT_GRACE_PERIOD_DAYS, DEFAULT_MAX_BINARY_SIZE_BYTES, DEFAULT_PROXIMITY_GAP_MS, DEFAULT_REDACTION_RULES, DEFAULT_SCAN_PATTERNS, DEFAULT_SOURCE_TRUST, DEFAULT_TAXONOMY, DEFAULT_WINDOW_TOLERANCE_MS, type DedupOptions, type DedupResult, DiscoveredExtension, type DocFile, type DriveChange, type DriveChangesPage, type DriveFileMetadata, type DuplicatePair, EngramAccessService, type FileChange, FilesystemBackend, type ForkCapsuleOptions, type ForkCapsuleResult, type ForkLineage, type FuseClusterResult, FusedDisagreement, FusedSegment, FusedSpeaker, FusionConversationInput, FusionDayResult, FusionOptions, GOOGLE_DRIVE_CONNECTOR_ID, GOOGLE_DRIVE_CURSOR_KIND, DEFAULT_POLL_INTERVAL_MS as GOOGLE_DRIVE_DEFAULT_POLL_INTERVAL_MS, type GenerateOptions, type GenerateResult, type GitContext, type GitInvoker, type GoogleDriveClient, type GoogleDriveClientFactory, type GoogleDriveConnectorConfig, type GoogleDriveSyncResult, HermesMemoryExtensionPublisher, type IngestionPlan, KNOWN_WEARABLE_SOURCE_IDS, type LanguageInfo, LiveConnector, LiveConnectorRegistry, LiveConnectorRegistryError, type LocalSessionAdapterInput, type LocalSessionAdapterOptions, type LocalSessionParseWarning, type LocalSessionParsedFile, type LocalSessionRole, type LocalSessionSourceAdapter, type LocalSessionSummaryCliOptions, type LocalSessionSummaryReport, type LocalSessionTurn, MemoryCategory, type MemoryEntry, type MemoryExtensionPublisher, type MergeResult, NOTION_CONNECTOR_ID, NOTION_CURSOR_KIND, NOTION_DEFAULT_POLL_INTERVAL_MS, NoneBackend, type NotionConnectorConfig, type OnboardOptions, type OnboardResult, PUBLISHERS, type PackReviewContextStructuralInput, type PipelineResult, PluginConfig, type ProjectShape, type ProvenanceEntry, type PublishContext, type PublishResult, type PublisherCapabilities, REDACTION_PLACEHOLDER, RELAY_DEMO_MISSION_ID, RELAY_DEMO_NAMESPACE, RELAY_MISSION_DEFAULT_EVENT_LIMIT, RELAY_MISSION_MAX_EVENTS, RELAY_MISSION_MAX_EVENT_LIMIT, RELAY_MISSION_MAX_FILE_BYTES, RELAY_MISSION_MAX_LINE_BYTES, RELAY_MISSION_SCHEMA_VERSION, REMNIC_CITATION_FORMAT, REMNIC_MCP_TOOL_INVENTORY, REMNIC_RECALL_DECISION_RULES, REMNIC_SEMANTIC_OVERVIEW, type RedactionConfig, type RedactionReport, type RedactionRuleConfig, type RelayAgentOutputSnapshot, type RelayAgentSnapshot, type RelayConflictSnapshot, type RelayCorrectionSnapshot, type RelayDecisionSnapshot, type RelayEvidenceRef, RelayEvidenceRefSchema, type RelayMissionAppendAccessInput, type RelayMissionAppendAccessOutput, type RelayMissionAppendResult, type RelayMissionEvent, type RelayMissionEventInput, RelayMissionEventInputSchema, RelayMissionEventSchema, type RelayMissionPayload, RelayMissionPayloadSchema, type RelayMissionReadAccessInput, type RelayMissionReadAccessOutput, type RelayMissionReadOptions, RelayMissionReadOptionsSchema, type RelayMissionSnapshot, type RelayMissionStatus, RelayMissionStore, RelayMissionStoreError, type RelayMissionStoreErrorCode, type RelayMissionStoreOptions, type RelayPropagationSnapshot, type RelayRecallSnapshot, type RelayTestSnapshot, ResolverDecision, type ReviewAction, type ReviewCandidate, type ReviewContext, type ReviewItem, type ReviewListResult, type ReviewOptions, type ReviewResult, type SessionSummaryDraft, type SessionSummaryExcerpt, type Space, type SpaceKind, type SpaceManifest, type SpacePromoteResult, type SpacePullResult, type SpacePushResult, type SpaceShareResult, type SpaceSwitchResult, SpeakerRegistry, type StatementProvenance, StorageManager, type StructuralContextDegradation, type StructuralContextErrorCode, type StructuralContextProvider, type StructuralProviderStatus, type StructuralSpawnFn, type StructuralSymbol, type SubprocessProviderOptions, type SymbolsForDiffErr, type SymbolsForDiffOk, type SymbolsForDiffResult, SyncIncrementalResult, type SyncOptions, type SyncResult, type SyncState, TRANSCRIPT_FORMAT_VERSION, Taxonomy, TokenEntry, type TrainingExportAdapter, type TrainingExportOptions, type TrainingExportRecord, type TranscriptSegmentMatch, type TreeNode, WEARABLES_DIR_NAME, WearableCleanupSettings, type WearableConnectorFactory, type WearableConnectorFactoryOptions, type WearableConnectorRegistration, WearableConversation, WearableCorrectionRule, WearableDayTranscript, WearableDayTranscriptMeta, WearableFusionConfig, WearableSourceConnector, WearableSourceSettings, type WearableSourceSyncState, type WearableSyncStateFile, type WearablesCliIo, WearablesConfig, WearablesInputError, WearablesService, activityDatabasePath, activityDayWindow, activityDigestPath, appendRelayMissionEvent, applyActivitySchema, applyCorrections, applyOffTheRecord, bodyIsEscaped, branchNamespaceName, buildProcedureMarkdownBody, buildProcedureRecallSection, cleanConversation, clearStructuralContextProvidersForTest, clearTrainingExportAdapters, clearWearableConnectors, clusterConversations, collapseImmediateRepeats, collectLocalSessionSummaries, compareRelayEvents, compileCorrectionRule, compileCorrectionRules, compileRedactionPatterns, compileRedactionRules, composeActivityDigestBody, composeActivityDigestMeta, composeDayTranscriptBody, composeDayTranscriptMeta, convertMemoriesToRecords, correctionsFilePath, createBackend, createGoogleDriveConnector, createNotionConnector, createRelayMissionFixture, createSpace, createSubprocessStructuralProvider, curate, decodeTranscriptBody, defaultGoogleDriveClientFactory, defaultWearableCleanupSettings, defaultWearableFusionConfig, defaultWearableSourceSettings, defaultWearablesConfig, deleteSpace, describeCodingScope, describeErrorForOperator, describeStructuralProviderStatus, emptyManifest, emptySyncState, ensureActivityStateDir, ensureBuiltInWearableConnectors, escapeSegmentText, findContradictions, findDuplicates, forkCapsule, fuseCluster, fuseDay, fusionInputsFromConversations, generateContextTree, generateResolverDocument, getActiveSpace, getAuditLog, getLocalSessionSourceAdapter, getManifestPath, getSpacesDir, getStructuralContextProvider, getTaxonomyDir, getTaxonomyFilePath, getTrainingExportAdapter, getWearableConnector, hashActivityBody, hashTranscriptBody, hostIdForConnector, isLowQualitySegment, isReviewPrompt, isValidActivityDate, isValidTranscriptDate, listLocalSessionSourceAdapters, listReviewItems, listSpaces, listTrainingExportAdapters, listWearableConnectors, loadCorrectionsFile, loadManifest, loadSyncState, loadTaxonomy, manifestDir, manifestPath, matchesPatterns, mergeSpaces, normalizeOriginUrl, onboard, openActivityDatabase, packReviewContext, packReviewContextStructural, parseActivityDigest, parseDayTranscript, parseFlexibleIsoTimestamp, parseIsoOffsetTimestamp, parseIsoUtcTimestamp, parseProcedureStepsFromBody, parseTouchedFiles, parseTranscriptSegmentLine, parseWearablesConfig, performReview, probeStructuralProviderForDoctor, projectNamespaceName, promoteSpace, publisherFor, publisherForConnector, pullFromSpace, pushToSpace, rankReviewCandidates, readForkLineage, readManifest, readRelayMission, reconstructFusionInputs, redactSessionSummaryText, redactText, reduceRelayMission, registerLocalSessionSourceAdapter, registerPublisher, registerStructuralContextProvider, registerTrainingExportAdapter, registerWearableConnector, relayMissionAppendOperation, relayMissionReadOperation, relayMissionReceiptDigest, renderExtensionsBlock, renderExtensionsFooter, renderStructuralProviderStatusLine, resolveCategory, resolveCodingNamespaceOverlay, resolveGitContext, runBinaryLifecyclePipeline, runLocalSessionSummaryCliCommand, runWearablesCliCommand, saveCorrectionsFile, saveManifest, saveSyncState, saveTaxonomy, scanForBinaries, serializeActivityDigest, serializeDayTranscript, shareSpace, stableHash, stripFillerTokens, structuralProviderActive, switchSpace, syncChanges, syncStateFilePath, toStructuralContextDegradation, unescapeSegmentText, unescapeSpeakerLabel, updateSourceSyncState, validateGoogleDriveConfig, validateNotionConfig, validateSlug, validateTaxonomy, watchForChanges, writeManifest };
7393
+ export { ACTIVITY_DIGEST_FORMAT_VERSION, ACTIVITY_DIR_NAME, type ActivityDayDigest, type ActivityDayMeta, type ActivitySnapshot, type ActivitySnapshotPage, type ActivitySourceCheck, type ActivitySourceClient, ActivityStore, type ArchitectureHintsResult, type AuditEntry, type BinaryAssetRecord, type BinaryAssetStatus, type BinaryLifecycleConfig, type BinaryLifecycleManifest, type BinaryStorageBackend, type BinaryStorageBackendConfig, ClaudeCodeMemoryExtensionPublisher, type CleanupResult, CodexMemoryExtensionPublisher, type CodingNamespaceOverlay, type CodingScopeDescription, type CollectLocalSessionSummariesOptions, type CompiledCorrectionRule, type CompiledRedactionRule, type ConflictEntry, type ContradictionOptions, type ContradictionPair, type ContradictionResult$1 as CurateContradictionResult, type DuplicateResult as CurateDuplicateResult, type CurateOptions, type CurateResult, type CuratedStatement, DEFAULT_GRACE_PERIOD_DAYS, DEFAULT_MAX_BINARY_SIZE_BYTES, DEFAULT_MEETINGS_DETECTION_CONFIG, DEFAULT_MEETING_APP_PATTERNS, DEFAULT_PROXIMITY_GAP_MS, DEFAULT_REDACTION_RULES, DEFAULT_SCAN_PATTERNS, DEFAULT_SOURCE_TRUST, DEFAULT_TAXONOMY, DEFAULT_WINDOW_TOLERANCE_MS, type DedupOptions, type DedupResult, type DetectedMeeting, DiscoveredExtension, type DocFile, type DriveChange, type DriveChangesPage, type DriveFileMetadata, type DuplicatePair, EngramAccessService, type FileChange, FilesystemBackend, type ForkCapsuleOptions, type ForkCapsuleResult, type ForkLineage, type FuseClusterResult, FusedDisagreement, FusedSegment, FusedSpeaker, FusionConversationInput, FusionDayResult, FusionOptions, GOOGLE_DRIVE_CONNECTOR_ID, GOOGLE_DRIVE_CURSOR_KIND, DEFAULT_POLL_INTERVAL_MS as GOOGLE_DRIVE_DEFAULT_POLL_INTERVAL_MS, type GenerateOptions, type GenerateResult, type GitContext, type GitInvoker, type GoogleDriveClient, type GoogleDriveClientFactory, type GoogleDriveConnectorConfig, type GoogleDriveSyncResult, HermesMemoryExtensionPublisher, type IngestionPlan, KNOWN_WEARABLE_SOURCE_IDS, type LanguageInfo, LiveConnector, LiveConnectorRegistry, LiveConnectorRegistryError, type LocalSessionAdapterInput, type LocalSessionAdapterOptions, type LocalSessionParseWarning, type LocalSessionParsedFile, type LocalSessionRole, type LocalSessionSourceAdapter, type LocalSessionSummaryCliOptions, type LocalSessionSummaryReport, type LocalSessionTurn, type MeetingAppSpan, type MeetingAudioWindow, type MeetingDetectionSource, type MeetingsDetectionConfig, type MeetingsDetectionInput, MemoryCategory, type MemoryEntry, type MemoryExtensionPublisher, type MergeResult, NOTION_CONNECTOR_ID, NOTION_CURSOR_KIND, NOTION_DEFAULT_POLL_INTERVAL_MS, NoneBackend, type NotionConnectorConfig, type OnboardOptions, type OnboardResult, PUBLISHERS, type PackReviewContextStructuralInput, type PipelineResult, PluginConfig, type ProjectShape, type ProvenanceEntry, type PublishContext, type PublishResult, type PublisherCapabilities, REDACTION_PLACEHOLDER, RELAY_DEMO_MISSION_ID, RELAY_DEMO_NAMESPACE, RELAY_MISSION_DEFAULT_EVENT_LIMIT, RELAY_MISSION_MAX_EVENTS, RELAY_MISSION_MAX_EVENT_LIMIT, RELAY_MISSION_MAX_FILE_BYTES, RELAY_MISSION_MAX_LINE_BYTES, RELAY_MISSION_SCHEMA_VERSION, REMNIC_CITATION_FORMAT, REMNIC_MCP_TOOL_INVENTORY, REMNIC_RECALL_DECISION_RULES, REMNIC_SEMANTIC_OVERVIEW, type RedactionConfig, type RedactionReport, type RedactionRuleConfig, type RelayAgentOutputSnapshot, type RelayAgentSnapshot, type RelayConflictSnapshot, type RelayCorrectionSnapshot, type RelayDecisionSnapshot, type RelayEvidenceRef, RelayEvidenceRefSchema, type RelayMissionAppendAccessInput, type RelayMissionAppendAccessOutput, type RelayMissionAppendResult, type RelayMissionEvent, type RelayMissionEventInput, RelayMissionEventInputSchema, RelayMissionEventSchema, type RelayMissionPayload, RelayMissionPayloadSchema, type RelayMissionReadAccessInput, type RelayMissionReadAccessOutput, type RelayMissionReadOptions, RelayMissionReadOptionsSchema, type RelayMissionSnapshot, type RelayMissionStatus, RelayMissionStore, RelayMissionStoreError, type RelayMissionStoreErrorCode, type RelayMissionStoreOptions, type RelayPropagationSnapshot, type RelayRecallSnapshot, type RelayTestSnapshot, ResolverDecision, type ReviewAction, type ReviewCandidate, type ReviewContext, type ReviewItem, type ReviewListResult, type ReviewOptions, type ReviewResult, type SessionSummaryDraft, type SessionSummaryExcerpt, type Space, type SpaceKind, type SpaceManifest, type SpacePromoteResult, type SpacePullResult, type SpacePushResult, type SpaceShareResult, type SpaceSwitchResult, SpeakerRegistry, type StatementProvenance, StorageManager, type StructuralContextDegradation, type StructuralContextErrorCode, type StructuralContextProvider, type StructuralProviderStatus, type StructuralSpawnFn, type StructuralSymbol, type SubprocessProviderOptions, type SymbolsForDiffErr, type SymbolsForDiffOk, type SymbolsForDiffResult, SyncIncrementalResult, type SyncOptions, type SyncResult, type SyncState, TRANSCRIPT_FORMAT_VERSION, Taxonomy, TokenEntry, type TrainingExportAdapter, type TrainingExportOptions, type TrainingExportRecord, type TranscriptSegmentMatch, type TreeNode, WEARABLES_DIR_NAME, WearableCleanupSettings, type WearableConnectorFactory, type WearableConnectorFactoryOptions, type WearableConnectorRegistration, WearableConversation, WearableCorrectionRule, WearableDayTranscript, WearableDayTranscriptMeta, WearableFusionConfig, WearableSourceConnector, WearableSourceSettings, type WearableSourceSyncState, type WearableSyncStateFile, type WearablesCliIo, WearablesConfig, WearablesInputError, WearablesService, activityDatabasePath, activityDayWindow, activityDigestPath, appendRelayMissionEvent, applyActivitySchema, applyCorrections, applyOffTheRecord, bodyIsEscaped, branchNamespaceName, buildProcedureMarkdownBody, buildProcedureRecallSection, cleanConversation, clearStructuralContextProvidersForTest, clearTrainingExportAdapters, clearWearableConnectors, clusterConversations, collapseImmediateRepeats, collectLocalSessionSummaries, compareRelayEvents, compileCorrectionRule, compileCorrectionRules, compileRedactionPatterns, compileRedactionRules, composeActivityDigestBody, composeActivityDigestMeta, composeDayTranscriptBody, composeDayTranscriptMeta, convertMemoriesToRecords, correctionsFilePath, createBackend, createGoogleDriveConnector, createNotionConnector, createRelayMissionFixture, createSpace, createSubprocessStructuralProvider, curate, decodeTranscriptBody, defaultGoogleDriveClientFactory, defaultWearableCleanupSettings, defaultWearableFusionConfig, defaultWearableSourceSettings, defaultWearablesConfig, deleteSpace, describeCodingScope, describeErrorForOperator, describeStructuralProviderStatus, detectMeetings, emptyManifest, emptySyncState, ensureActivityStateDir, ensureBuiltInWearableConnectors, escapeSegmentText, findContradictions, findDuplicates, forkCapsule, fuseCluster, fuseDay, fusionInputsFromConversations, generateContextTree, generateResolverDocument, getActiveSpace, getAuditLog, getLocalSessionSourceAdapter, getManifestPath, getSpacesDir, getStructuralContextProvider, getTaxonomyDir, getTaxonomyFilePath, getTrainingExportAdapter, getWearableConnector, hashActivityBody, hashTranscriptBody, hostIdForConnector, isLowQualitySegment, isReviewPrompt, isValidActivityDate, isValidTranscriptDate, listLocalSessionSourceAdapters, listReviewItems, listSpaces, listTrainingExportAdapters, listWearableConnectors, loadCorrectionsFile, loadManifest, loadSyncState, loadTaxonomy, manifestDir, manifestPath, matchesPatterns, meetingId, mergeSpaces, normalizeOriginUrl, onboard, openActivityDatabase, packReviewContext, packReviewContextStructural, parseActivityDigest, parseDayTranscript, parseFlexibleIsoTimestamp, parseIsoOffsetTimestamp, parseIsoUtcTimestamp, parseProcedureStepsFromBody, parseTouchedFiles, parseTranscriptSegmentLine, parseWearablesConfig, performReview, probeStructuralProviderForDoctor, projectNamespaceName, promoteSpace, publisherFor, publisherForConnector, pullFromSpace, pushToSpace, rankReviewCandidates, readForkLineage, readManifest, readRelayMission, reconstructFusionInputs, redactSessionSummaryText, redactText, reduceRelayMission, registerLocalSessionSourceAdapter, registerPublisher, registerStructuralContextProvider, registerTrainingExportAdapter, registerWearableConnector, relayMissionAppendOperation, relayMissionReadOperation, relayMissionReceiptDigest, renderExtensionsBlock, renderExtensionsFooter, renderStructuralProviderStatusLine, resolveCategory, resolveCodingNamespaceOverlay, resolveGitContext, runBinaryLifecyclePipeline, runLocalSessionSummaryCliCommand, runWearablesCliCommand, saveCorrectionsFile, saveManifest, saveSyncState, saveTaxonomy, scanForBinaries, serializeActivityDigest, serializeDayTranscript, shareSpace, stableHash, stripFillerTokens, structuralProviderActive, switchSpace, syncChanges, syncStateFilePath, toStructuralContextDegradation, unescapeSegmentText, unescapeSpeakerLabel, updateSourceSyncState, validateGoogleDriveConfig, validateNotionConfig, validateSlug, validateTaxonomy, watchForChanges, writeManifest };
package/dist/index.js CHANGED
@@ -6,6 +6,8 @@ import {
6
6
  CodexMemoryExtensionPublisher,
7
7
  DEFAULT_GRACE_PERIOD_DAYS,
8
8
  DEFAULT_MAX_BINARY_SIZE_BYTES,
9
+ DEFAULT_MEETINGS_DETECTION_CONFIG,
10
+ DEFAULT_MEETING_APP_PATTERNS,
9
11
  DEFAULT_REDACTION_RULES,
10
12
  DEFAULT_SCAN_PATTERNS,
11
13
  DEFAULT_TAXONOMY,
@@ -41,6 +43,7 @@ import {
41
43
  defaultWorkspaceDir,
42
44
  deleteSpace,
43
45
  describeStructuralProviderStatus,
46
+ detectMeetings,
44
47
  emptyManifest,
45
48
  ensureActivityStateDir,
46
49
  ensureBuiltInWearableConnectors,
@@ -72,6 +75,7 @@ import {
72
75
  manifestDir,
73
76
  manifestPath,
74
77
  matchesPatterns,
78
+ meetingId,
75
79
  mergeSpaces,
76
80
  onboard,
77
81
  openActivityDatabase,
@@ -111,7 +115,7 @@ import {
111
115
  validateTaxonomy,
112
116
  watchForChanges,
113
117
  writeManifest
114
- } from "./chunk-3VBXP5HS.js";
118
+ } from "./chunk-5RZHHANR.js";
115
119
  import {
116
120
  WEARABLE_SOURCE_PREFIX,
117
121
  buildExtractionTurns,
@@ -1074,6 +1078,8 @@ export {
1074
1078
  DEFAULT_GRACE_PERIOD_DAYS,
1075
1079
  DEFAULT_IMPORT_BATCH_SIZE,
1076
1080
  DEFAULT_MAX_BINARY_SIZE_BYTES,
1081
+ DEFAULT_MEETINGS_DETECTION_CONFIG,
1082
+ DEFAULT_MEETING_APP_PATTERNS,
1077
1083
  DEFAULT_PPR_DAMPING,
1078
1084
  DEFAULT_PPR_ITERATIONS,
1079
1085
  DEFAULT_PPR_TOLERANCE,
@@ -1302,6 +1308,7 @@ export {
1302
1308
  describeErrorForOperator,
1303
1309
  describeMemoriesDir,
1304
1310
  describeStructuralProviderStatus,
1311
+ detectMeetings,
1305
1312
  detectRecallAnomalies,
1306
1313
  diffVersions,
1307
1314
  discoverMemoryExtensions,
@@ -1433,6 +1440,7 @@ export {
1433
1440
  manifestPath,
1434
1441
  matchesPatterns,
1435
1442
  materializeForNamespace,
1443
+ meetingId,
1436
1444
  memoryStatusForMode,
1437
1445
  memoryStoreRequestSchema,
1438
1446
  memoryWorthOutcomeEligibleCategories,
@@ -41,7 +41,7 @@ import {
41
41
  throwIfRecallAborted,
42
42
  tokenizeRecallQuery,
43
43
  utcDateKeysForLocalDay
44
- } from "./chunk-3VBXP5HS.js";
44
+ } from "./chunk-5RZHHANR.js";
45
45
  import "./chunk-5SB7SC2G.js";
46
46
  import "./chunk-3332THSR.js";
47
47
  import "./chunk-I74SUMNI.js";
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@remnic/core",
3
- "version": "9.12.0",
3
+ "version": "9.13.1",
4
4
  "description": "Framework-agnostic Remnic memory engine — orchestrator, storage, extraction, search, trust zones",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",
@@ -3045,7 +3045,7 @@
3045
3045
  "core"
3046
3046
  ],
3047
3047
  "peerDependencies": {
3048
- "@remnic/coding-graph": "^9.12.0"
3048
+ "@remnic/coding-graph": "^9.13.1"
3049
3049
  },
3050
3050
  "peerDependenciesMeta": {
3051
3051
  "@remnic/coding-graph": {
package/src/index.ts CHANGED
@@ -1203,10 +1203,10 @@ export {
1203
1203
 
1204
1204
  // ---------------------------------------------------------------------------
1205
1205
  // Wearable transcript subsystem (Limitless / Bee / Omi connectors).
1206
- // Connector packages import the registry + types from here.
1207
1206
  // ---------------------------------------------------------------------------
1208
1207
  export * from "./wearables/index.js";
1209
1208
  export * from "./activity/index.js";
1209
+ export * from "./meetings/index.js";
1210
1210
 
1211
1211
  // ---------------------------------------------------------------------------
1212
1212
  // Shared importer base (issue #568)
@@ -0,0 +1,366 @@
1
+ import assert from "node:assert/strict";
2
+ import { test } from "node:test";
3
+
4
+ import { DEFAULT_MEETINGS_DETECTION_CONFIG, detectMeetings, meetingId } from "./detect.js";
5
+ import type { MeetingAppSpan, MeetingAudioWindow, MeetingsDetectionInput } from "./types.js";
6
+
7
+ const DATE = "2026-03-10";
8
+
9
+ function span(app: string, startUtc: string, endUtc: string): MeetingAppSpan {
10
+ return { app, startUtc, endUtc };
11
+ }
12
+
13
+ function audio(overrides: Partial<MeetingAudioWindow> & { startUtc: string; endUtc: string }): MeetingAudioWindow {
14
+ return { source: "desktop", distinctNonWearerSpeakers: 2, ...overrides };
15
+ }
16
+
17
+ function input(overrides: Partial<MeetingsDetectionInput> = {}): MeetingsDetectionInput {
18
+ return { date: DATE, appSpans: [], audioWindows: [], ...overrides };
19
+ }
20
+
21
+ test("app+audio: an app span overlapping a conversation yields one meeting", () => {
22
+ const meetings = detectMeetings(
23
+ input({
24
+ appSpans: [span("Zoom", "2026-03-10T14:00:00.000Z", "2026-03-10T15:00:00.000Z")],
25
+ audioWindows: [audio({ source: "desktop", startUtc: "2026-03-10T14:01:00.000Z", endUtc: "2026-03-10T14:55:00.000Z" })],
26
+ }),
27
+ );
28
+ assert.equal(meetings.length, 1);
29
+ assert.equal(meetings[0]?.detectionSource, "app+audio");
30
+ assert.equal(meetings[0]?.app, "Zoom");
31
+ assert.deepEqual(meetings[0]?.sources, ["desktop"]);
32
+ });
33
+
34
+ test("activity only (app span, zero audio) → NO meeting (watching a recording)", () => {
35
+ const meetings = detectMeetings(
36
+ input({ appSpans: [span("Zoom", "2026-03-10T14:00:00.000Z", "2026-03-10T15:00:00.000Z")] }),
37
+ );
38
+ assert.equal(meetings.length, 0);
39
+ });
40
+
41
+ test("audio only: a long multi-speaker conversation with no app span is a meeting", () => {
42
+ const meetings = detectMeetings(
43
+ input({
44
+ audioWindows: [
45
+ audio({ source: "limitless", startUtc: "2026-03-10T09:00:00.000Z", endUtc: "2026-03-10T09:20:00.000Z", distinctNonWearerSpeakers: 3 }),
46
+ ],
47
+ }),
48
+ );
49
+ assert.equal(meetings.length, 1);
50
+ assert.equal(meetings[0]?.detectionSource, "audio");
51
+ assert.equal(meetings[0]?.app, undefined);
52
+ });
53
+
54
+ test("audio only: too short OR too few speakers is NOT a meeting", () => {
55
+ const short = detectMeetings(
56
+ input({ audioWindows: [audio({ startUtc: "2026-03-10T09:00:00.000Z", endUtc: "2026-03-10T09:05:00.000Z", distinctNonWearerSpeakers: 3 })] }),
57
+ );
58
+ assert.equal(short.length, 0);
59
+ const solo = detectMeetings(
60
+ input({ audioWindows: [audio({ startUtc: "2026-03-10T09:00:00.000Z", endUtc: "2026-03-10T09:30:00.000Z", distinctNonWearerSpeakers: 1 })] }),
61
+ );
62
+ assert.equal(solo.length, 0);
63
+ });
64
+
65
+ test("provider meeting is detected from its own boundaries without an app span", () => {
66
+ const meetings = detectMeetings(
67
+ input({
68
+ audioWindows: [
69
+ audio({ source: "granola", startUtc: "2026-03-10T16:00:00.000Z", endUtc: "2026-03-10T16:30:00.000Z", providerMeeting: true, title: "Roadmap", distinctNonWearerSpeakers: 0 }),
70
+ ],
71
+ }),
72
+ );
73
+ assert.equal(meetings.length, 1);
74
+ assert.equal(meetings[0]?.detectionSource, "provider");
75
+ assert.equal(meetings[0]?.title, "Roadmap");
76
+ });
77
+
78
+ test("rejoin within the merge gap collapses into ONE meeting; a 10-min gap stays TWO", () => {
79
+ const rejoin = detectMeetings(
80
+ input({
81
+ appSpans: [
82
+ span("Zoom", "2026-03-10T14:00:00.000Z", "2026-03-10T14:20:00.000Z"),
83
+ span("Zoom", "2026-03-10T14:21:00.000Z", "2026-03-10T14:40:00.000Z"),
84
+ ],
85
+ audioWindows: [
86
+ audio({ startUtc: "2026-03-10T14:00:00.000Z", endUtc: "2026-03-10T14:19:00.000Z" }),
87
+ audio({ startUtc: "2026-03-10T14:20:30.000Z", endUtc: "2026-03-10T14:39:00.000Z" }),
88
+ ],
89
+ }),
90
+ );
91
+ assert.equal(rejoin.length, 1);
92
+
93
+ const twoMeetings = detectMeetings(
94
+ input({
95
+ appSpans: [
96
+ span("Zoom", "2026-03-10T14:00:00.000Z", "2026-03-10T14:20:00.000Z"),
97
+ span("Zoom", "2026-03-10T14:30:00.000Z", "2026-03-10T14:50:00.000Z"),
98
+ ],
99
+ audioWindows: [
100
+ audio({ startUtc: "2026-03-10T14:00:00.000Z", endUtc: "2026-03-10T14:19:00.000Z" }),
101
+ audio({ startUtc: "2026-03-10T14:30:00.000Z", endUtc: "2026-03-10T14:49:00.000Z" }),
102
+ ],
103
+ }),
104
+ );
105
+ assert.equal(twoMeetings.length, 2);
106
+ });
107
+
108
+ test("multiple audio sources over the same window fuse into one meeting with both sources", () => {
109
+ const meetings = detectMeetings(
110
+ input({
111
+ appSpans: [span("Zoom", "2026-03-10T14:00:00.000Z", "2026-03-10T15:00:00.000Z")],
112
+ audioWindows: [
113
+ audio({ source: "desktop", startUtc: "2026-03-10T14:00:00.000Z", endUtc: "2026-03-10T14:55:00.000Z" }),
114
+ audio({ source: "limitless", startUtc: "2026-03-10T14:02:00.000Z", endUtc: "2026-03-10T14:58:00.000Z" }),
115
+ ],
116
+ }),
117
+ );
118
+ assert.equal(meetings.length, 1);
119
+ assert.deepEqual(meetings[0]?.sources, ["desktop", "limitless"]);
120
+ });
121
+
122
+ test("detected meetings never overlap and stay ordered after merge", () => {
123
+ const meetings = detectMeetings(
124
+ input({
125
+ appSpans: [span("Zoom", "2026-03-10T14:00:00.000Z", "2026-03-10T15:30:00.000Z")],
126
+ audioWindows: [
127
+ audio({ startUtc: "2026-03-10T14:00:00.000Z", endUtc: "2026-03-10T14:40:00.000Z" }),
128
+ audio({ startUtc: "2026-03-10T14:30:00.000Z", endUtc: "2026-03-10T15:10:00.000Z" }),
129
+ // A second, well-separated meeting so the ordering assertion is non-vacuous.
130
+ audio({
131
+ source: "granola",
132
+ startUtc: "2026-03-10T18:00:00.000Z",
133
+ endUtc: "2026-03-10T18:30:00.000Z",
134
+ providerMeeting: true,
135
+ title: "Sync",
136
+ }),
137
+ ],
138
+ }),
139
+ );
140
+ assert.equal(meetings.length, 2);
141
+ for (let i = 1; i < meetings.length; i++) {
142
+ assert.ok((meetings[i - 1]?.endUtc ?? "") <= (meetings[i]?.startUtc ?? ""), "meetings must not overlap");
143
+ }
144
+ });
145
+
146
+ test("sub-threshold overlap does not pair an app span with a conversation", () => {
147
+ // Only 1 minute of overlap; minOverlapMinutes default is 2 → audio-only rules apply.
148
+ const meetings = detectMeetings(
149
+ input({
150
+ appSpans: [span("Zoom", "2026-03-10T14:00:00.000Z", "2026-03-10T14:10:00.000Z")],
151
+ audioWindows: [audio({ startUtc: "2026-03-10T14:09:00.000Z", endUtc: "2026-03-10T14:16:00.000Z", distinctNonWearerSpeakers: 2 })],
152
+ }),
153
+ );
154
+ // 7-min conversation < 15-min audio-only floor and no qualifying app overlap → no meeting.
155
+ assert.equal(meetings.length, 0);
156
+ });
157
+
158
+ test("ids are stable across a re-run with 10% more fixture data appended", () => {
159
+ const base = input({
160
+ appSpans: [span("Zoom", "2026-03-10T14:00:00.000Z", "2026-03-10T15:00:00.000Z")],
161
+ audioWindows: [audio({ startUtc: "2026-03-10T14:00:00.000Z", endUtc: "2026-03-10T14:55:00.000Z" })],
162
+ });
163
+ const first = detectMeetings(base);
164
+ // Re-run with an extra, later meeting appended (more data, same early meeting).
165
+ const more = detectMeetings(
166
+ input({
167
+ appSpans: [...base.appSpans, span("Zoom", "2026-03-10T17:00:00.000Z", "2026-03-10T17:30:00.000Z")],
168
+ audioWindows: [...base.audioWindows, audio({ startUtc: "2026-03-10T17:01:00.000Z", endUtc: "2026-03-10T17:28:00.000Z" })],
169
+ }),
170
+ );
171
+ const firstId = first[0]?.id;
172
+ const sameId = more.find((m) => m.startUtc === first[0]?.startUtc)?.id;
173
+ assert.ok(firstId);
174
+ assert.equal(sameId, firstId, "the earlier meeting keeps its id when later data is added");
175
+ });
176
+
177
+ test("meetingId is deterministic and anchored on the exact start instant", () => {
178
+ const a = meetingId(DATE, "2026-03-10T14:00:05.000Z");
179
+ assert.equal(a, meetingId(DATE, "2026-03-10T14:00:05.000Z")); // same start instant → same id
180
+ assert.match(a, /^mtg-2026-03-10-[0-9a-f]{8}$/);
181
+ // Distinct start instants — even within the same minute — get distinct ids.
182
+ assert.notEqual(a, meetingId(DATE, "2026-03-10T14:00:06.000Z"));
183
+ assert.notEqual(a, meetingId(DATE, "2026-03-10T14:01:00.000Z"));
184
+ });
185
+
186
+ test("two short provider meetings in the same minute get distinct ids", () => {
187
+ const meetings = detectMeetings(
188
+ input({
189
+ audioWindows: [
190
+ audio({ source: "granola", startUtc: "2026-03-10T14:00:05.000Z", endUtc: "2026-03-10T14:00:20.000Z", providerMeeting: true, title: "A", distinctNonWearerSpeakers: 0 }),
191
+ audio({ source: "granola", startUtc: "2026-03-10T14:00:40.000Z", endUtc: "2026-03-10T14:00:55.000Z", providerMeeting: true, title: "B", distinctNonWearerSpeakers: 0 }),
192
+ ],
193
+ }),
194
+ );
195
+ // Provider candidates bypass the duration floors and don't merge (no shared
196
+ // app, disjoint), so both survive — and must not collide on id.
197
+ assert.equal(meetings.length, 2);
198
+ assert.notEqual(meetings[0]?.id, meetings[1]?.id);
199
+ });
200
+
201
+ test("a rolled-over calendar timestamp is dropped, not silently shifted", () => {
202
+ const meetings = detectMeetings(
203
+ input({
204
+ audioWindows: [
205
+ audio({ startUtc: "2026-02-30T14:00:00.000Z", endUtc: "2026-02-30T14:30:00.000Z", distinctNonWearerSpeakers: 3 }),
206
+ audio({ startUtc: "2026-03-10T10:00:00.000Z", endUtc: "2026-03-10T10:20:00.000Z", distinctNonWearerSpeakers: 3 }),
207
+ ],
208
+ }),
209
+ );
210
+ // Feb 30 is invalid → dropped, never rolled into Mar 2.
211
+ assert.equal(meetings.length, 1);
212
+ assert.equal(meetings[0]?.startUtc, "2026-03-10T10:00:00.000Z");
213
+ });
214
+
215
+ test("meeting id is unchanged when a late source extends the end (resync stability)", () => {
216
+ const first = detectMeetings(
217
+ input({
218
+ audioWindows: [
219
+ audio({ startUtc: "2026-03-10T14:00:00.000Z", endUtc: "2026-03-10T14:20:00.000Z", distinctNonWearerSpeakers: 3 }),
220
+ ],
221
+ }),
222
+ );
223
+ const extended = detectMeetings(
224
+ input({
225
+ audioWindows: [
226
+ audio({ source: "desktop", startUtc: "2026-03-10T14:00:00.000Z", endUtc: "2026-03-10T14:20:00.000Z", distinctNonWearerSpeakers: 3 }),
227
+ audio({ source: "limitless", startUtc: "2026-03-10T14:18:00.000Z", endUtc: "2026-03-10T14:45:00.000Z", distinctNonWearerSpeakers: 3 }),
228
+ ],
229
+ }),
230
+ );
231
+ assert.equal(first.length, 1);
232
+ assert.equal(extended.length, 1);
233
+ assert.equal(extended[0]?.endUtc, "2026-03-10T14:45:00.000Z"); // end grew
234
+ assert.equal(extended[0]?.id, first[0]?.id); // …but the id is unchanged
235
+ });
236
+
237
+ test("default config matches the issue-specified thresholds", () => {
238
+ assert.equal(DEFAULT_MEETINGS_DETECTION_CONFIG.minOverlapMinutes, 2);
239
+ assert.equal(DEFAULT_MEETINGS_DETECTION_CONFIG.audioOnlyMinMinutes, 15);
240
+ assert.equal(DEFAULT_MEETINGS_DETECTION_CONFIG.mergeGapMinutes, 2);
241
+ });
242
+
243
+ test("audio-only exactly at the 15-min floor qualifies", () => {
244
+ const meetings = detectMeetings(
245
+ input({
246
+ audioWindows: [
247
+ audio({ startUtc: "2026-03-10T09:00:00.000Z", endUtc: "2026-03-10T09:15:00.000Z", distinctNonWearerSpeakers: 2 }),
248
+ ],
249
+ }),
250
+ );
251
+ assert.equal(meetings.length, 1);
252
+ assert.equal(meetings[0]?.detectionSource, "audio");
253
+ });
254
+
255
+ test("app+audio exactly at the 2-min overlap threshold qualifies", () => {
256
+ const meetings = detectMeetings(
257
+ input({
258
+ appSpans: [span("Zoom", "2026-03-10T14:00:00.000Z", "2026-03-10T14:12:00.000Z")],
259
+ audioWindows: [
260
+ audio({ startUtc: "2026-03-10T14:10:00.000Z", endUtc: "2026-03-10T14:17:00.000Z", distinctNonWearerSpeakers: 2 }),
261
+ ],
262
+ }),
263
+ );
264
+ // Exactly 2 min overlap (14:10–14:12) → pairs as app+audio.
265
+ assert.equal(meetings.length, 1);
266
+ assert.equal(meetings[0]?.detectionSource, "app+audio");
267
+ assert.equal(meetings[0]?.app, "Zoom");
268
+ });
269
+
270
+ test("disjoint app + audio never pair even when minOverlapMinutes is 0", () => {
271
+ const meetings = detectMeetings(
272
+ input({
273
+ appSpans: [span("Zoom", "2026-03-10T14:00:00.000Z", "2026-03-10T14:05:00.000Z")],
274
+ audioWindows: [
275
+ audio({ startUtc: "2026-03-10T14:10:00.000Z", endUtc: "2026-03-10T14:17:00.000Z", distinctNonWearerSpeakers: 2 }),
276
+ ],
277
+ }),
278
+ { ...DEFAULT_MEETINGS_DETECTION_CONFIG, minOverlapMinutes: 0 },
279
+ );
280
+ // Disjoint windows (overlap 0) must not pair; 7-min audio < 15-min floor → no meeting.
281
+ assert.equal(meetings.length, 0);
282
+ });
283
+
284
+ test("invalid detection thresholds are rejected", () => {
285
+ for (const bad of [Number.NaN, -1, Number.POSITIVE_INFINITY]) {
286
+ assert.throws(
287
+ () => detectMeetings(input(), { ...DEFAULT_MEETINGS_DETECTION_CONFIG, minOverlapMinutes: bad }),
288
+ RangeError,
289
+ );
290
+ }
291
+ assert.throws(
292
+ () => detectMeetings(input(), { ...DEFAULT_MEETINGS_DETECTION_CONFIG, mergeGapMinutes: Number.NaN }),
293
+ RangeError,
294
+ );
295
+ });
296
+
297
+ test("a malformed timestamp is skipped defensively without dropping valid meetings", () => {
298
+ const meetings = detectMeetings(
299
+ input({
300
+ audioWindows: [
301
+ audio({ startUtc: "not-a-date", endUtc: "2026-03-10T09:20:00.000Z", distinctNonWearerSpeakers: 3 }),
302
+ audio({ startUtc: "2026-03-10T10:00:00.000Z", endUtc: "2026-03-10T10:20:00.000Z", distinctNonWearerSpeakers: 3 }),
303
+ ],
304
+ }),
305
+ );
306
+ // The malformed window is dropped; the valid conversation is still detected.
307
+ assert.equal(meetings.length, 1);
308
+ assert.equal(meetings[0]?.startUtc, "2026-03-10T10:00:00.000Z");
309
+ });
310
+
311
+ test("an offset-form rolled-over timestamp is also dropped", () => {
312
+ const meetings = detectMeetings(
313
+ input({
314
+ audioWindows: [
315
+ audio({ startUtc: "2026-02-30T14:00:00.000+00:00", endUtc: "2026-02-30T14:30:00.000+00:00", distinctNonWearerSpeakers: 3 }),
316
+ audio({ startUtc: "2026-03-10T10:00:00.000Z", endUtc: "2026-03-10T10:20:00.000Z", distinctNonWearerSpeakers: 3 }),
317
+ ],
318
+ }),
319
+ );
320
+ assert.equal(meetings.length, 1);
321
+ assert.equal(meetings[0]?.startUtc, "2026-03-10T10:00:00.000Z");
322
+ });
323
+
324
+ test("a timestamp offset beyond ±14:00 is dropped", () => {
325
+ const meetings = detectMeetings(
326
+ input({
327
+ audioWindows: [
328
+ audio({ startUtc: "2026-03-10T00:30:00.000+14:59", endUtc: "2026-03-10T01:00:00.000+14:59", distinctNonWearerSpeakers: 3 }),
329
+ audio({ startUtc: "2026-03-10T10:00:00.000Z", endUtc: "2026-03-10T10:20:00.000Z", distinctNonWearerSpeakers: 3 }),
330
+ ],
331
+ }),
332
+ );
333
+ assert.equal(meetings.length, 1);
334
+ assert.equal(meetings[0]?.startUtc, "2026-03-10T10:00:00.000Z");
335
+ });
336
+
337
+ test("a non-integer speaker count does not qualify an audio-only meeting", () => {
338
+ const meetings = detectMeetings(
339
+ input({
340
+ audioWindows: [
341
+ audio({ startUtc: "2026-03-10T09:00:00.000Z", endUtc: "2026-03-10T09:30:00.000Z", distinctNonWearerSpeakers: Number.POSITIVE_INFINITY }),
342
+ ],
343
+ }),
344
+ );
345
+ assert.equal(meetings.length, 0);
346
+ });
347
+
348
+ test("detectMeetings rejects a non-YYYY-MM-DD day (kept out of ids)", () => {
349
+ assert.throws(() => detectMeetings(input({ date: "2026/03/10" })), RangeError);
350
+ assert.throws(() => detectMeetings(input({ date: "2026-02-30" })), RangeError);
351
+ });
352
+
353
+ test("meetingId validates its inputs for direct callers", () => {
354
+ assert.throws(() => meetingId("2026/03/10", "2026-03-10T14:00:00.000Z"), RangeError);
355
+ assert.throws(() => meetingId("2026-03-10", "not-a-date"), RangeError);
356
+ });
357
+
358
+ test("equal-overlap app selection is deterministic regardless of input order", () => {
359
+ const win = { startUtc: "2026-03-10T14:00:00.000Z", endUtc: "2026-03-10T15:00:00.000Z", distinctNonWearerSpeakers: 2 };
360
+ const zoom = span("Zoom", "2026-03-10T14:00:00.000Z", "2026-03-10T15:00:00.000Z");
361
+ const meet = span("Meet", "2026-03-10T14:00:00.000Z", "2026-03-10T15:00:00.000Z");
362
+ const a = detectMeetings(input({ appSpans: [zoom, meet], audioWindows: [audio(win)] }));
363
+ const b = detectMeetings(input({ appSpans: [meet, zoom], audioWindows: [audio(win)] }));
364
+ assert.equal(a[0]?.app, b[0]?.app);
365
+ assert.equal(a[0]?.app, "Meet"); // tie broken by app name ("Meet" < "Zoom")
366
+ });