@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/access-cli.js +1 -1
- package/dist/{chunk-3VBXP5HS.js → chunk-5RZHHANR.js} +195 -14
- package/dist/chunk-5RZHHANR.js.map +1 -0
- package/dist/index.d.ts +105 -1
- package/dist/index.js +9 -1
- package/dist/orchestrator.js +1 -1
- package/package.json +2 -2
- package/src/index.ts +1 -1
- package/src/meetings/detect.test.ts +366 -0
- package/src/meetings/detect.ts +287 -0
- package/src/meetings/index.ts +8 -0
- package/src/meetings/types.ts +76 -0
- package/dist/chunk-3VBXP5HS.js.map +0 -1
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-
|
|
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,
|
package/dist/orchestrator.js
CHANGED
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@remnic/core",
|
|
3
|
-
"version": "9.
|
|
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.
|
|
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
|
+
});
|