chatlab-cli 0.28.1 → 0.29.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (111) hide show
  1. package/dist/{chat-command-ISJ2DC7R.mjs → chat-command-JKG3FY5B.mjs} +8 -5
  2. package/dist/{chat-command-ISJ2DC7R.mjs.map → chat-command-JKG3FY5B.mjs.map} +1 -1
  3. package/dist/chunk-2MZ6G4AC.mjs +1339 -0
  4. package/dist/chunk-2MZ6G4AC.mjs.map +1 -0
  5. package/dist/{chunk-NJB2NNB2.mjs → chunk-6FPEP2ZX.mjs} +2 -2
  6. package/dist/{chunk-YBS3YQB6.mjs → chunk-7RW4CQAO.mjs} +31 -57
  7. package/dist/chunk-7RW4CQAO.mjs.map +1 -0
  8. package/dist/{chunk-GHTGFM6O.mjs → chunk-ARZM6HBX.mjs} +14 -12
  9. package/dist/{chunk-GHTGFM6O.mjs.map → chunk-ARZM6HBX.mjs.map} +1 -1
  10. package/dist/{chunk-3QBVO4J7.mjs → chunk-CIB23U32.mjs} +2 -2
  11. package/dist/chunk-D4WTE7LV.mjs +526 -0
  12. package/dist/chunk-D4WTE7LV.mjs.map +1 -0
  13. package/dist/{chunk-T7IETWHN.mjs → chunk-I35WHWA6.mjs} +5 -3
  14. package/dist/{chunk-T7IETWHN.mjs.map → chunk-I35WHWA6.mjs.map} +1 -1
  15. package/dist/{chunk-AB6TAXTQ.mjs → chunk-KL2NGIMQ.mjs} +171 -47
  16. package/dist/chunk-KL2NGIMQ.mjs.map +1 -0
  17. package/dist/{chunk-ZVDOFWL7.mjs → chunk-QQK7MLCR.mjs} +3926 -10867
  18. package/dist/chunk-QQK7MLCR.mjs.map +1 -0
  19. package/dist/{chunk-ERAAWLEF.mjs → chunk-SKUG5XVD.mjs} +10 -8
  20. package/dist/{chunk-ERAAWLEF.mjs.map → chunk-SKUG5XVD.mjs.map} +1 -1
  21. package/dist/chunk-WPI6YSAR.mjs +8813 -0
  22. package/dist/chunk-WPI6YSAR.mjs.map +1 -0
  23. package/dist/cli.mjs +7 -4
  24. package/dist/contacts-worker.mjs +48 -0
  25. package/dist/contacts-worker.mjs.map +1 -0
  26. package/dist/{http-MBILV37U.mjs → http-5E7ARYFW.mjs} +11 -8
  27. package/dist/{import-EYV3GXHE.mjs → import-3KG6VK6O.mjs} +7 -4
  28. package/dist/index.mjs +11 -8
  29. package/dist/{mcp-H55AEXU5.mjs → mcp-JBI4CUQT.mjs} +8 -5
  30. package/dist/{mcp-H55AEXU5.mjs.map → mcp-JBI4CUQT.mjs.map} +1 -1
  31. package/dist/people-relationships-worker.mjs +48 -0
  32. package/dist/people-relationships-worker.mjs.map +1 -0
  33. package/dist/semantic-index-worker.mjs +1 -1
  34. package/dist/{src-36ZIR65K.mjs → src-UTBQAOJ3.mjs} +62 -44
  35. package/dist/{stream-import-YZMZ56B5.mjs → stream-import-7HZ4IRYJ.mjs} +7 -4
  36. package/dist/{worker-runtime-YDYS2C6T.mjs → worker-runtime-ALS4MR2A.mjs} +7 -3
  37. package/dist/{worker-runtime-YDYS2C6T.mjs.map → worker-runtime-ALS4MR2A.mjs.map} +1 -1
  38. package/dist-web/assets/BufferResource-MsqP5sq3.js +185 -0
  39. package/dist-web/assets/CanvasRenderer-BKCvcc2p.js +1 -0
  40. package/dist-web/assets/DebugToolsPanel-D_lFIW6j.css +1 -0
  41. package/dist-web/assets/DebugToolsPanel.vue_vue_type_script_setup_true_lang-HGJb0Et1.js +110 -0
  42. package/dist-web/assets/{EChartWordcloud-B_YisOW5.js → EChartWordcloud-Clg60FMo.js} +1 -1
  43. package/dist-web/assets/Filter-wRJtdXx9.js +1 -0
  44. package/dist-web/assets/MessageExportModal-BCrMf5Kw.js +1 -0
  45. package/dist-web/assets/PageHeader-Bbn87QZS.css +1 -0
  46. package/dist-web/assets/PageHeader-C_yw54le.js +1 -0
  47. package/dist-web/assets/RelationshipGalaxyCanvas-DTzJq7q7.js +244 -0
  48. package/dist-web/assets/RelationshipGalaxyThreeCanvas-BluPHjCR.css +1 -0
  49. package/dist-web/assets/RelationshipGalaxyThreeCanvas-DIE-0_iM.js +4108 -0
  50. package/dist-web/assets/RenderTargetSystem-BSAQFamI.js +184 -0
  51. package/dist-web/assets/WebGLRenderer-lfzmCZNT.js +156 -0
  52. package/dist-web/assets/WebGPURenderer-BnEdfdr_.js +41 -0
  53. package/dist-web/assets/browserAll-gRNfDyPu.js +14 -0
  54. package/dist-web/assets/{electron-DWDCqx_d.js → electron-BHaEaaZP.js} +1 -1
  55. package/dist-web/assets/{fetch-29ZBtGV0.js → fetch-BW3aO7PS.js} +1 -1
  56. package/dist-web/assets/fetch-BwF5hNuF.js +1 -0
  57. package/dist-web/assets/{fetch-Bh1fo3Df.js → fetch-C1fI_KOX.js} +1 -1
  58. package/dist-web/assets/{fetch-DyD8wDhb.js → fetch-CBZbgZqk.js} +1 -1
  59. package/dist-web/assets/{fetch-C3yhWO2S.js → fetch-CFRoSIAn.js} +1 -1
  60. package/dist-web/assets/fetch-CN91TIwV.js +1 -0
  61. package/dist-web/assets/{fetch-BcxrU2mr.js → fetch-CQt-gb-f.js} +1 -1
  62. package/dist-web/assets/{fetch-TCeykOkO.js → fetch-DPCJ-1l-.js} +1 -1
  63. package/dist-web/assets/{fetch-tIIVdXSv.js → fetch-gMB4wuBd.js} +1 -1
  64. package/dist-web/assets/{fetch-BYczbT4C.js → fetch-mFlEBZyB.js} +1 -1
  65. package/dist-web/assets/index---rXG1hZ.css +1 -0
  66. package/dist-web/assets/index--6C0sK6a.css +1 -0
  67. package/dist-web/assets/index-B6wEGata.js +173 -0
  68. package/dist-web/assets/index-Bg4GGPXv.js +1 -0
  69. package/dist-web/assets/index-BgVbj5qX.js +12 -0
  70. package/dist-web/assets/index-BkoXaBV_.js +64 -0
  71. package/dist-web/assets/index-C56IuV5U.css +1 -0
  72. package/dist-web/assets/index-CLRaKwpT.js +2 -0
  73. package/dist-web/assets/index-Dj1BSD7O.js +1 -0
  74. package/dist-web/assets/index-DosG7LmJ.css +1 -0
  75. package/dist-web/assets/index-DveHdPfK.js +1 -0
  76. package/dist-web/assets/{index-BX3tqfLz.css → index-MBRD3wPd.css} +1 -1
  77. package/dist-web/assets/index-RoDgJcHU.js +2 -0
  78. package/dist-web/assets/{index-fbWrQpgH.js → index-ZdCB7xcf.js} +4 -4
  79. package/dist-web/assets/index-aljYO5ms.js +1 -0
  80. package/dist-web/assets/people-page-header-Mupn-U_b.js +1 -0
  81. package/dist-web/assets/relationship-galaxy-viewport-Cwfa51ER.js +1 -0
  82. package/dist-web/assets/{vendor-echarts-CITQeskI.js → vendor-echarts-hJVnODyU.js} +2 -2
  83. package/dist-web/assets/{vendor-echarts-wordcloud-B1kQMklB.js → vendor-echarts-wordcloud-k5ih2Jl4.js} +1 -1
  84. package/dist-web/assets/{vendor-nuxt-ui-Zy8txNdC.js → vendor-nuxt-ui-Dr2T8_OZ.js} +2 -2
  85. package/dist-web/assets/{vendor-reka-ui-mMvma1EP.js → vendor-reka-ui-CzM4kDQ7.js} +1 -1
  86. package/dist-web/assets/{vendor-zrender-DFl4U2Oe.js → vendor-zrender-5DcYjSZk.js} +1 -1
  87. package/dist-web/assets/{web-H3XLx159.js → web-BakeV58u.js} +1 -1
  88. package/dist-web/assets/webworkerAll-D_6PgURS.js +83 -0
  89. package/dist-web/index.html +4 -4
  90. package/package.json +1 -1
  91. package/dist/chunk-AB6TAXTQ.mjs.map +0 -1
  92. package/dist/chunk-YBS3YQB6.mjs.map +0 -1
  93. package/dist/chunk-ZVDOFWL7.mjs.map +0 -1
  94. package/dist-web/assets/DebugToolsPanel-DFw6_G5V.css +0 -1
  95. package/dist-web/assets/DebugToolsPanel.vue_vue_type_script_setup_true_lang-Do3x4aL3.js +0 -110
  96. package/dist-web/assets/MessageExportModal-rijqUusq.js +0 -1
  97. package/dist-web/assets/fetch-BAxKmbKF.js +0 -1
  98. package/dist-web/assets/fetch-Da_TBxrX.js +0 -1
  99. package/dist-web/assets/index-BzjLqlW_.js +0 -173
  100. package/dist-web/assets/index-C74qEDFh.js +0 -64
  101. package/dist-web/assets/index-CM6xNPFp.css +0 -1
  102. package/dist-web/assets/index-CgK2LLIF.js +0 -12
  103. package/dist-web/assets/index-CjkOvM3N.js +0 -2
  104. package/dist-web/assets/index-Dno7CADA.js +0 -1
  105. package/dist-web/assets/index-I3SWZjnW.css +0 -1
  106. /package/dist/{chunk-NJB2NNB2.mjs.map → chunk-6FPEP2ZX.mjs.map} +0 -0
  107. /package/dist/{chunk-3QBVO4J7.mjs.map → chunk-CIB23U32.mjs.map} +0 -0
  108. /package/dist/{http-MBILV37U.mjs.map → http-5E7ARYFW.mjs.map} +0 -0
  109. /package/dist/{import-EYV3GXHE.mjs.map → import-3KG6VK6O.mjs.map} +0 -0
  110. /package/dist/{src-36ZIR65K.mjs.map → src-UTBQAOJ3.mjs.map} +0 -0
  111. /package/dist/{stream-import-YZMZ56B5.mjs.map → stream-import-7HZ4IRYJ.mjs.map} +0 -0
@@ -0,0 +1,1339 @@
1
+ import { createRequire as __createRequire } from 'module';
2
+ import { dirname as __pathDirname } from 'path';
3
+ import { fileURLToPath as __fileURLToPath } from 'url';
4
+ const require = __createRequire(import.meta.url);
5
+ const __filename = __fileURLToPath(import.meta.url);
6
+ const __dirname = __pathDirname(__filename);
7
+ import {
8
+ CONTACTS_TIME_RANGE_PRESETS,
9
+ appLogger,
10
+ computeFriendScores,
11
+ computeNonFriendScores,
12
+ getCache,
13
+ getDbFileVersion,
14
+ getGroupContactFacts,
15
+ getGroupRelationshipGraphFacts,
16
+ getLatestContactMessageTs,
17
+ getPrivateContactFacts,
18
+ getSessionMeta,
19
+ isChatSessionDb,
20
+ isNameMatchPlatform,
21
+ resolveOwnerMember,
22
+ setCache
23
+ } from "./chunk-WPI6YSAR.mjs";
24
+
25
+ // ../../packages/node-runtime/src/services/people/relationships/facts-cache.ts
26
+ var PEOPLE_RELATIONSHIPS_FACTS_FORMAT_VERSION = 3;
27
+ function createEmptyPeopleRelationshipsFactsCacheStats() {
28
+ return {
29
+ latestHits: 0,
30
+ latestMisses: 0,
31
+ factsHits: 0,
32
+ factsMisses: 0,
33
+ writes: 0
34
+ };
35
+ }
36
+ function buildPeopleRelationshipsSessionLatestCacheKey(algorithmVersion) {
37
+ return `people-relationships:latest:v${PEOPLE_RELATIONSHIPS_FACTS_FORMAT_VERSION}:${algorithmVersion}`;
38
+ }
39
+ function buildPeopleRelationshipsSessionFactsCacheKey(algorithmVersion, timeRange) {
40
+ return [
41
+ "people-relationships:facts",
42
+ `v${PEOPLE_RELATIONSHIPS_FACTS_FORMAT_VERSION}`,
43
+ algorithmVersion,
44
+ `preset:${timeRange.preset}`,
45
+ `start:${timeRange.startTs ?? "all"}`
46
+ ].join(":");
47
+ }
48
+ function readCachedPeopleRelationshipsSessionLatest(sessionId, cacheDir, key, dbVersion) {
49
+ const cached = getCache(
50
+ sessionId,
51
+ key,
52
+ cacheDir
53
+ );
54
+ if (!cached || cached.v !== dbVersion || !isPeopleRelationshipsSessionLatestFacts(cached.data)) return { hit: false };
55
+ return { hit: true, data: cached.data };
56
+ }
57
+ function writeCachedPeopleRelationshipsSessionLatest(sessionId, cacheDir, key, dbVersion, data) {
58
+ setCache(
59
+ sessionId,
60
+ key,
61
+ { v: dbVersion, data },
62
+ cacheDir
63
+ );
64
+ }
65
+ function readCachedPeopleRelationshipsSessionFacts(sessionId, cacheDir, key, dbVersion) {
66
+ const cached = getCache(
67
+ sessionId,
68
+ key,
69
+ cacheDir
70
+ );
71
+ if (!cached || cached.v !== dbVersion || !isPeopleRelationshipsSessionFacts(cached.data)) return { hit: false };
72
+ return { hit: true, data: cached.data };
73
+ }
74
+ function writeCachedPeopleRelationshipsSessionFacts(sessionId, cacheDir, key, dbVersion, data) {
75
+ setCache(
76
+ sessionId,
77
+ key,
78
+ { v: dbVersion, data },
79
+ cacheDir
80
+ );
81
+ }
82
+ function isPeopleRelationshipsSessionLatestFacts(value) {
83
+ return isObject(value) && isNullableNumber(value.latestMessageTs);
84
+ }
85
+ function isPeopleRelationshipsSessionFacts(value) {
86
+ if (!isObject(value) || typeof value.kind !== "string" || !isNullableNumber(value.latestMessageTs)) return false;
87
+ switch (value.kind) {
88
+ case "not_chat_db":
89
+ case "missing_meta":
90
+ case "unsupported_type":
91
+ return true;
92
+ case "missing_owner":
93
+ case "unresolved_owner":
94
+ case "private_missing":
95
+ case "private_ambiguous":
96
+ return isPeopleRelationshipsSessionMetaFacts(value.meta);
97
+ case "private":
98
+ return isPeopleRelationshipsSessionMetaFacts(value.meta) && isPeopleRelationshipsCachedPrivateFacts(value.facts);
99
+ case "group":
100
+ return isPeopleRelationshipsSessionMetaFacts(value.meta) && isPeopleRelationshipsCachedGroupFacts(value.facts);
101
+ default:
102
+ return false;
103
+ }
104
+ }
105
+ function isPeopleRelationshipsSessionMetaFacts(value) {
106
+ return isObject(value) && typeof value.name === "string" && typeof value.platform === "string" && (value.type === "private" /* PRIVATE */ || value.type === "group" /* GROUP */) && (typeof value.ownerId === "string" || value.ownerId === null) && (value.owner === void 0 || isContactMemberRef(value.owner));
107
+ }
108
+ function isPeopleRelationshipsCachedPrivateFacts(value) {
109
+ return isObject(value) && isContactMemberRef(value.contact) && isFiniteNumber(value.privateMessageCount) && Array.isArray(value.activeMonths) && value.activeMonths.every((month) => typeof month === "string") && isNullableNumber(value.lastMessageTs);
110
+ }
111
+ function isPeopleRelationshipsCachedGroupFacts(value) {
112
+ return isObject(value) && Array.isArray(value.members) && value.members.every(isRelationshipGraphMemberFact) && Array.isArray(value.edges) && value.edges.every(isRelationshipGraphEdgeFact) && Array.isArray(value.ownerEdges) && value.ownerEdges.every(isGroupContactFacts) && isFiniteNumber(value.ownerMessageCount);
113
+ }
114
+ function isGroupContactFacts(value) {
115
+ return isObject(value) && isContactMemberRef(value.contact) && isFiniteNumber(value.messageCount) && isFiniteNumber(value.coOccurrenceCount) && isFiniteNumber(value.coOccurrenceRawScore) && isFiniteNumber(value.replyInteractionCount) && isFiniteNumber(value.repliesFromOwnerToContact) && isFiniteNumber(value.repliesFromContactToOwner) && isNullableNumber(value.lastInteractionTs);
116
+ }
117
+ function isRelationshipGraphMemberFact(value) {
118
+ return isObject(value) && isContactMemberRef(value.contact) && isFiniteNumber(value.messageCount) && isNullableNumber(value.lastMessageTs);
119
+ }
120
+ function isRelationshipGraphEdgeFact(value) {
121
+ return isObject(value) && isContactMemberRef(value.source) && isContactMemberRef(value.target) && isFiniteNumber(value.coOccurrenceCount) && isFiniteNumber(value.coOccurrenceRawScore) && isFiniteNumber(value.replyInteractionCount) && isFiniteNumber(value.repliesFromSourceToTarget) && isFiniteNumber(value.repliesFromTargetToSource) && isNullableNumber(value.lastInteractionTs);
122
+ }
123
+ function isContactMemberRef(value) {
124
+ return isObject(value) && isFiniteNumber(value.id) && typeof value.platformId === "string" && typeof value.name === "string" && Array.isArray(value.aliases) && value.aliases.every((alias) => typeof alias === "string") && (typeof value.avatar === "string" || value.avatar === null);
125
+ }
126
+ function isNullableNumber(value) {
127
+ return value === null || isFiniteNumber(value);
128
+ }
129
+ function isFiniteNumber(value) {
130
+ return typeof value === "number" && Number.isFinite(value);
131
+ }
132
+ function isObject(value) {
133
+ return typeof value === "object" && value !== null;
134
+ }
135
+
136
+ // ../../packages/node-runtime/src/services/people/relationships/time-range.ts
137
+ var SECONDS_PER_YEAR = 365 * 24 * 60 * 60;
138
+ var DEFAULT_PEOPLE_RELATIONSHIPS_TIME_RANGE_PRESET = "1y";
139
+ var YEARS_BY_PRESET = {
140
+ "1y": 1,
141
+ "2y": 2,
142
+ "3y": 3,
143
+ "5y": 5
144
+ };
145
+ function normalizePeopleRelationshipsTimeRangePreset(value) {
146
+ return CONTACTS_TIME_RANGE_PRESETS.includes(value) ? value : DEFAULT_PEOPLE_RELATIONSHIPS_TIME_RANGE_PRESET;
147
+ }
148
+ function resolvePeopleRelationshipsTimeRange(presetInput, anchorTs) {
149
+ const preset = normalizePeopleRelationshipsTimeRangePreset(presetInput);
150
+ const normalizedAnchor = typeof anchorTs === "number" ? anchorTs : null;
151
+ const years = YEARS_BY_PRESET[preset];
152
+ return {
153
+ preset,
154
+ anchorTs: normalizedAnchor,
155
+ startTs: normalizedAnchor !== null && years ? normalizedAnchor - years * SECONDS_PER_YEAR : null
156
+ };
157
+ }
158
+
159
+ // ../../packages/node-runtime/src/services/people/relationships/compute.ts
160
+ var PEOPLE_RELATIONSHIPS_ALGORITHM_VERSION = "people-relationships-v2";
161
+ var PRIVATE_COMMUNITY_ID = "private";
162
+ var OWNER_KEY_PREFIX = "owner";
163
+ var REPLY_WEIGHT = 3;
164
+ var CO_OCCURRENCE_COUNT_WEIGHT = 0.05;
165
+ var EDGE_RECENCY_HALF_LIFE_SECONDS = 120 * 24 * 60 * 60;
166
+ var EDGE_RECENCY_FLOOR = 0.1;
167
+ var MIN_GROUP_FRIENDS_FOR_PANORAMA = 3;
168
+ var MIN_GROUP_FRIEND_RATIO_FOR_PANORAMA = 0.2;
169
+ var MIN_OWNER_CONNECTED_MEMBERS_FOR_PANORAMA = 3;
170
+ var MIN_OWNER_GROUP_MESSAGES_FOR_PANORAMA = 20;
171
+ var MIN_OWNER_GROUP_MESSAGE_RATIO_FOR_PANORAMA = 0.05;
172
+ var MIN_OWNER_GROUP_MESSAGES_FOR_RATIO = 8;
173
+ var SMALL_GROUP_MEMBER_LIMIT_FOR_FRIEND_EDGE = 12;
174
+ var MAX_OWNER_CONNECTED_NON_FRIENDS_PER_GROUP = 30;
175
+ var MAX_FRIEND_CONNECTED_NON_FRIENDS_PER_GROUP = 30;
176
+ var MAX_HIGH_SIGNAL_NON_FRIENDS_PER_GROUP = 10;
177
+ var MAX_MEMBERS_PER_GROUP_FOR_PANORAMA = 80;
178
+ var COMMUNITY_COLORS = [
179
+ "#7dd3fc",
180
+ "#f0abfc",
181
+ "#facc15",
182
+ "#34d399",
183
+ "#fb7185",
184
+ "#a78bfa",
185
+ "#2dd4bf",
186
+ "#f97316",
187
+ "#60a5fa",
188
+ "#e879f9"
189
+ ];
190
+ function computePeopleRelationshipsSnapshot(options) {
191
+ const startedAt = options.now?.() ?? Date.now();
192
+ const sessionIds = options.adapter.listSessionIds();
193
+ const limits = normalizeLimits(options.limits);
194
+ const factsCache = options.factsCacheDir ? createFactsCacheContext(options.factsCacheDir) : null;
195
+ const timeRange = resolvePeopleRelationshipsTimeRange(
196
+ options.timeRangePreset,
197
+ findGlobalLatestMessageTs(options.adapter, sessionIds, factsCache)
198
+ );
199
+ const result = computePeopleRelationships({
200
+ adapter: options.adapter,
201
+ sessionIds,
202
+ timeRange,
203
+ factsCache,
204
+ limits,
205
+ onProgress: options.onProgress
206
+ });
207
+ const finishedAt = options.now?.() ?? Date.now();
208
+ if (factsCache) {
209
+ appLogger.info("people-relationships", "people relationships session facts cache summary", factsCache.stats);
210
+ }
211
+ appLogger.info("people-relationships", "people relationships panorama filter summary", {
212
+ includedGroupSessions: result.diagnostics.panoramaIncludedGroupSessions,
213
+ excludedLowValueGroupSessions: result.diagnostics.panoramaExcludedLowValueGroupSessions,
214
+ includedGroupMembers: result.diagnostics.panoramaIncludedGroupMembers,
215
+ excludedGroupMembers: result.diagnostics.panoramaExcludedGroupMembers,
216
+ candidateNodes: result.diagnostics.panoramaCandidateNodes,
217
+ reasons: result.diagnostics.panoramaGroupInclusionReasons
218
+ });
219
+ return {
220
+ ...result,
221
+ algorithmVersion: PEOPLE_RELATIONSHIPS_ALGORITHM_VERSION,
222
+ signature: options.signature,
223
+ timeRange,
224
+ computedAt: finishedAt,
225
+ limits,
226
+ workerStats: {
227
+ durationMs: Math.max(0, finishedAt - startedAt),
228
+ totalSessions: sessionIds.length,
229
+ processedSessions: sessionIds.length,
230
+ skippedFailedSessions: result.diagnostics.skippedFailedSessions
231
+ }
232
+ };
233
+ }
234
+ function buildPeopleRelationshipsNeighborhoodGraph(snapshot, contactKey, limits = {}) {
235
+ const normalizedLimits = normalizeLimits({ ...snapshot.limits, ...limits });
236
+ const center = snapshot.nodes.find((node) => node.key === contactKey);
237
+ if (!center) return { nodes: [], edges: [], communities: [] };
238
+ const relatedEdges = sortEdgesForDisplay(
239
+ snapshot.edges.filter((edge) => edge.sourceKey === contactKey || edge.targetKey === contactKey)
240
+ );
241
+ const { directEdges, nodeKeys } = selectNeighborhoodDirectEdges(relatedEdges, contactKey, normalizedLimits);
242
+ const directEdgeIds = new Set(relatedEdges.map((edge) => edge.id));
243
+ const secondaryEdgeLimit = Math.max(0, normalizedLimits.neighborhoodEdgeLimit - directEdges.length);
244
+ const secondaryEdges = sortEdgesForDisplay(
245
+ snapshot.edges.filter(
246
+ (edge) => !directEdgeIds.has(edge.id) && nodeKeys.has(edge.sourceKey) && nodeKeys.has(edge.targetKey)
247
+ )
248
+ ).slice(0, secondaryEdgeLimit);
249
+ const edges = sortEdgesForDisplay([...directEdges, ...secondaryEdges]);
250
+ const nodes = layoutNeighborhoodNodes(
251
+ snapshot.nodes.filter((node) => nodeKeys.has(node.key)),
252
+ directEdges,
253
+ contactKey
254
+ ).sort(compareNodes);
255
+ return {
256
+ nodes,
257
+ edges,
258
+ communities: filterNeighborhoodCommunities(snapshot.communities, directEdges)
259
+ };
260
+ }
261
+ function selectNeighborhoodDirectEdges(relatedEdges, contactKey, limits) {
262
+ const nodeKeys = /* @__PURE__ */ new Set([contactKey]);
263
+ const directEdges = [];
264
+ for (const edge of relatedEdges) {
265
+ if (directEdges.length >= limits.neighborhoodEdgeLimit) break;
266
+ const neighborKey = getNeighborhoodNeighborKey(edge, contactKey);
267
+ if (!neighborKey) continue;
268
+ if (!nodeKeys.has(neighborKey)) {
269
+ if (nodeKeys.size >= limits.neighborhoodNodeLimit) continue;
270
+ nodeKeys.add(neighborKey);
271
+ }
272
+ directEdges.push(edge);
273
+ }
274
+ return { directEdges, nodeKeys };
275
+ }
276
+ function getNeighborhoodNeighborKey(edge, contactKey) {
277
+ if (edge.sourceKey === contactKey) return edge.targetKey;
278
+ if (edge.targetKey === contactKey) return edge.sourceKey;
279
+ return null;
280
+ }
281
+ function layoutNeighborhoodNodes(nodes, relatedEdges, contactKey) {
282
+ const center = nodes.find((node) => node.key === contactKey);
283
+ if (!center) return nodes.map((node) => ({ ...node }));
284
+ const directEdgeByNodeKey = /* @__PURE__ */ new Map();
285
+ for (const edge of relatedEdges) {
286
+ const neighborKey = edge.sourceKey === contactKey ? edge.targetKey : edge.targetKey === contactKey ? edge.sourceKey : null;
287
+ if (neighborKey) directEdgeByNodeKey.set(neighborKey, edge);
288
+ }
289
+ const maxDirectWeight = Math.max(...[...directEdgeByNodeKey.values()].map((edge) => edge.weight), 1);
290
+ const communityGroups = /* @__PURE__ */ new Map();
291
+ for (const node of nodes) {
292
+ if (node.key === contactKey) continue;
293
+ const group = communityGroups.get(node.communityId) ?? [];
294
+ group.push(node);
295
+ communityGroups.set(node.communityId, group);
296
+ }
297
+ const sortedCommunityGroups = [...communityGroups.entries()].sort((a, b) => {
298
+ if (a[0] === center.communityId) return -1;
299
+ if (b[0] === center.communityId) return 1;
300
+ const weightA = sumNeighborhoodCommunityWeight(a[1], directEdgeByNodeKey);
301
+ const weightB = sumNeighborhoodCommunityWeight(b[1], directEdgeByNodeKey);
302
+ return weightB - weightA || a[0].localeCompare(b[0]);
303
+ });
304
+ const localNodes = [{ ...center, x: 0, y: 0 }];
305
+ for (const [communityIndex, [communityId, communityNodes]] of sortedCommunityGroups.entries()) {
306
+ const baseAngle = neighborhoodCommunityAngle(communityIndex, communityId, center.communityId);
307
+ communityNodes.sort((a, b) => {
308
+ const edgeA = directEdgeByNodeKey.get(a.key)?.weight ?? 0;
309
+ const edgeB = directEdgeByNodeKey.get(b.key)?.weight ?? 0;
310
+ return edgeB - edgeA || compareNodes(a, b);
311
+ });
312
+ for (const [index, node] of communityNodes.entries()) {
313
+ const angle = neighborhoodNodeAngle(baseAngle, communityId, node.key, index, communityNodes.length);
314
+ const radius = neighborhoodNodeRadius(
315
+ node,
316
+ center,
317
+ directEdgeByNodeKey.get(node.key),
318
+ maxDirectWeight,
319
+ communityNodes.length
320
+ );
321
+ localNodes.push({
322
+ ...node,
323
+ x: roundNum(Math.cos(angle) * radius, 2),
324
+ y: roundNum(Math.sin(angle) * radius, 2)
325
+ });
326
+ }
327
+ }
328
+ return localNodes;
329
+ }
330
+ function sumNeighborhoodCommunityWeight(nodes, directEdgeByNodeKey) {
331
+ return nodes.reduce((sum, node) => sum + (directEdgeByNodeKey.get(node.key)?.weight ?? 0), 0);
332
+ }
333
+ function neighborhoodCommunityAngle(index, communityId, centerCommunityId) {
334
+ if (communityId === centerCommunityId) return -Math.PI / 2;
335
+ return index * 2.399963229728653;
336
+ }
337
+ function neighborhoodNodeAngle(baseAngle, communityId, nodeKey, index, communitySize) {
338
+ const seedOffset = (stableHash(`${communityId}:${nodeKey}:neighborhood-angle`) % 1e3 / 1e3 - 0.5) * 0.12;
339
+ const centeredIndex = index - (communitySize - 1) / 2;
340
+ const spacing = communitySize <= 4 ? 0.42 : communitySize <= 10 ? 0.24 : 0.14;
341
+ const maxSpread = communitySize <= 4 ? 0.72 : communitySize <= 20 ? 1.08 : 1.32;
342
+ return baseAngle + clamp(centeredIndex * spacing + seedOffset, -maxSpread, maxSpread);
343
+ }
344
+ function neighborhoodNodeRadius(node, center, directEdge, maxDirectWeight, communitySize) {
345
+ const directStrength = directEdge ? Math.min(1, directEdge.weight / maxDirectWeight) : 0;
346
+ const sameCommunityBonus = node.communityId === center.communityId ? 110 : 0;
347
+ const smallCommunityBonus = node.communityId === center.communityId && communitySize <= 6 ? 80 : 0;
348
+ const friendBonus = node.pool === "friend" ? 70 : 0;
349
+ const scoreBonus = Math.min(90, Math.sqrt(Math.max(0, Math.min(1, node.score))) * 90);
350
+ const rankPenalty = Math.sqrt(Math.max(0, node.rank - center.rank)) * 8;
351
+ return roundNum(
352
+ clamp(
353
+ 260 + (1 - directStrength) * 560 + rankPenalty - sameCommunityBonus - smallCommunityBonus - friendBonus - scoreBonus,
354
+ 120,
355
+ 1100
356
+ ),
357
+ 2
358
+ );
359
+ }
360
+ function computePeopleRelationships(options) {
361
+ const diagnostics = createEmptyDiagnostics();
362
+ const nodes = /* @__PURE__ */ new Map();
363
+ const edges = /* @__PURE__ */ new Map();
364
+ const groupContributions = [];
365
+ const communityLabels = /* @__PURE__ */ new Map([[PRIVATE_COMMUNITY_ID, "Private contacts"]]);
366
+ let processedSessions = 0;
367
+ for (const sessionId of options.sessionIds) {
368
+ options.onProgress?.({ processedSessions, totalSessions: options.sessionIds.length, currentSessionId: sessionId });
369
+ try {
370
+ const facts = getSessionFacts(options.adapter, sessionId, options.timeRange, options.factsCache);
371
+ applySessionFacts(nodes, edges, groupContributions, communityLabels, diagnostics, sessionId, facts);
372
+ } catch (error) {
373
+ diagnostics.skippedFailedSessions++;
374
+ appLogger.error("people-relationships", `failed to process people relationship session: ${sessionId}`, error);
375
+ } finally {
376
+ processedSessions++;
377
+ options.onProgress?.({ processedSessions, totalSessions: options.sessionIds.length, currentSessionId: sessionId });
378
+ }
379
+ }
380
+ applyPanoramaContributions([...nodes.values()], edges, groupContributions, diagnostics);
381
+ return buildGraph(
382
+ [...nodes.values()],
383
+ [...edges.values()],
384
+ communityLabels,
385
+ groupContributions,
386
+ options.limits,
387
+ diagnostics
388
+ );
389
+ }
390
+ function findGlobalLatestMessageTs(adapter, sessionIds, factsCache) {
391
+ let latest = null;
392
+ for (const sessionId of sessionIds) {
393
+ const dbVersion = getSessionDbVersion(adapter, sessionId);
394
+ const cached = readLatestFacts(sessionId, factsCache, dbVersion);
395
+ if (cached) {
396
+ if (cached.latestMessageTs !== null) latest = Math.max(latest ?? 0, cached.latestMessageTs);
397
+ continue;
398
+ }
399
+ try {
400
+ const db = adapter.openReadonly(sessionId);
401
+ const ts = db && isChatSessionDb(db) ? getLatestContactMessageTs(db) : null;
402
+ writeLatestFacts(adapter, sessionId, factsCache, { latestMessageTs: ts }, dbVersion);
403
+ if (ts !== null) latest = Math.max(latest ?? 0, ts);
404
+ } catch (error) {
405
+ appLogger.error(
406
+ "people-relationships",
407
+ `failed to inspect people relationship session range: ${sessionId}`,
408
+ error
409
+ );
410
+ }
411
+ }
412
+ return latest;
413
+ }
414
+ function getSessionFacts(adapter, sessionId, timeRange, factsCache) {
415
+ const dbVersion = getSessionDbVersion(adapter, sessionId);
416
+ const cached = readSessionFacts(sessionId, timeRange, factsCache, dbVersion);
417
+ if (cached) return cached;
418
+ const facts = computeSessionFacts(adapter, sessionId, timeRange);
419
+ writeSessionFacts(adapter, sessionId, timeRange, factsCache, facts, dbVersion);
420
+ return facts;
421
+ }
422
+ function computeSessionFacts(adapter, sessionId, timeRange) {
423
+ const db = adapter.openReadonly(sessionId);
424
+ if (!db || !isChatSessionDb(db)) return { kind: "not_chat_db", latestMessageTs: null };
425
+ const latestMessageTs = getLatestContactMessageTs(db);
426
+ const meta = getSessionMeta(db);
427
+ if (!meta) return { kind: "missing_meta", latestMessageTs };
428
+ if (meta.type !== "private" /* PRIVATE */ && meta.type !== "group" /* GROUP */)
429
+ return { kind: "unsupported_type", latestMessageTs };
430
+ const cachedMeta = toPeopleRelationshipsSessionMetaFacts(meta);
431
+ if (!meta.ownerId?.trim()) return { kind: "missing_owner", meta: cachedMeta, latestMessageTs };
432
+ const owner = resolveOwnerMember(db);
433
+ if (!owner) return { kind: "unresolved_owner", meta: cachedMeta, latestMessageTs };
434
+ const cachedMetaWithOwner = toPeopleRelationshipsSessionMetaFacts(meta, owner);
435
+ if (meta.type === "private" /* PRIVATE */) {
436
+ const facts = getPrivateContactFacts(db, owner.id, { startTs: timeRange.startTs });
437
+ if (facts.type === "missing") return { kind: "private_missing", meta: cachedMetaWithOwner, latestMessageTs };
438
+ if (facts.type === "ambiguous") return { kind: "private_ambiguous", meta: cachedMetaWithOwner, latestMessageTs };
439
+ return { kind: "private", meta: cachedMetaWithOwner, latestMessageTs, facts };
440
+ }
441
+ return {
442
+ kind: "group",
443
+ meta: cachedMetaWithOwner,
444
+ latestMessageTs,
445
+ facts: {
446
+ ...getGroupRelationshipGraphFacts(db, owner.id, { startTs: timeRange.startTs }),
447
+ ownerEdges: getGroupContactFacts(db, owner.id, { startTs: timeRange.startTs })
448
+ }
449
+ };
450
+ }
451
+ function applySessionFacts(nodes, edges, groupContributions, communityLabels, diagnostics, sessionId, sessionFacts) {
452
+ switch (sessionFacts.kind) {
453
+ case "missing_owner":
454
+ diagnostics.skippedMissingOwnerSessions++;
455
+ return;
456
+ case "unresolved_owner":
457
+ diagnostics.skippedUnresolvedOwnerSessions++;
458
+ return;
459
+ case "private_ambiguous":
460
+ diagnostics.skippedAmbiguousPrivateSessions++;
461
+ return;
462
+ case "private":
463
+ diagnostics.processedPrivateSessions++;
464
+ applyPrivateFacts(nodes, edges, sessionId, sessionFacts.meta, sessionFacts.facts);
465
+ return;
466
+ case "group":
467
+ diagnostics.processedGroupSessions++;
468
+ applyGroupFacts(
469
+ nodes,
470
+ edges,
471
+ groupContributions,
472
+ communityLabels,
473
+ sessionId,
474
+ sessionFacts.meta,
475
+ sessionFacts.facts
476
+ );
477
+ return;
478
+ default:
479
+ return;
480
+ }
481
+ }
482
+ function applyPrivateFacts(nodes, edges, sessionId, meta, facts) {
483
+ if (facts.privateMessageCount <= 0) return;
484
+ const ownerNode = getOrCreateOwnerNode(nodes, meta);
485
+ const node = getOrCreateNode(nodes, sessionId, meta, facts.contact);
486
+ node.isFriend = true;
487
+ node.friendSource = "private";
488
+ node.panoramaCandidate = true;
489
+ node.privateMessageCount += facts.privateMessageCount;
490
+ for (const month of facts.activeMonths) node.activePrivateMonths.add(month);
491
+ node.communityWeights.set(PRIVATE_COMMUNITY_ID, (node.communityWeights.get(PRIVATE_COMMUNITY_ID) ?? 0) + 1);
492
+ updateLastInteraction(node, facts.lastMessageTs);
493
+ if (!ownerNode) return;
494
+ ownerNode.isFriend = true;
495
+ ownerNode.panoramaCandidate = true;
496
+ ownerNode.privateMessageCount += facts.privateMessageCount;
497
+ for (const month of facts.activeMonths) ownerNode.activePrivateMonths.add(month);
498
+ ownerNode.communityWeights.set(PRIVATE_COMMUNITY_ID, (ownerNode.communityWeights.get(PRIVATE_COMMUNITY_ID) ?? 0) + 1);
499
+ updateLastInteraction(ownerNode, facts.lastMessageTs);
500
+ applyOwnerContactEdge(edges, ownerNode, node, {
501
+ coOccurrenceCount: facts.privateMessageCount,
502
+ coOccurrenceRawScore: facts.privateMessageCount * 1.8,
503
+ replyInteractionCount: 0,
504
+ repliesFromOwnerToContact: 0,
505
+ repliesFromContactToOwner: 0,
506
+ lastInteractionTs: facts.lastMessageTs,
507
+ sessionId,
508
+ panoramaEligible: true
509
+ });
510
+ }
511
+ function applyGroupFacts(nodes, edges, groupContributions, communityLabels, sessionId, meta, facts) {
512
+ const communityId = `group:${sessionId}`;
513
+ communityLabels.set(communityId, meta.name);
514
+ const ownerNode = getOrCreateOwnerNode(nodes, meta);
515
+ const contribution = {
516
+ sessionId,
517
+ memberKeys: /* @__PURE__ */ new Set(),
518
+ memberMessageCounts: /* @__PURE__ */ new Map(),
519
+ ownerConnectedKeys: /* @__PURE__ */ new Set(),
520
+ ownerEdgeScores: /* @__PURE__ */ new Map(),
521
+ relationshipEdgeIds: /* @__PURE__ */ new Set(),
522
+ relationshipEdgeEndpoints: /* @__PURE__ */ new Map(),
523
+ relationshipEdgeScores: /* @__PURE__ */ new Map(),
524
+ friendConnectedScores: /* @__PURE__ */ new Map(),
525
+ ownerMessageCount: facts.ownerMessageCount
526
+ };
527
+ for (const member of facts.members) {
528
+ const node = getOrCreateNode(nodes, sessionId, meta, member.contact);
529
+ contribution.memberKeys.add(node.key);
530
+ contribution.memberMessageCounts.set(node.key, member.messageCount);
531
+ node.groupMessageCount += member.messageCount;
532
+ node.commonGroupSessionIds.add(sessionId);
533
+ node.communityWeights.set(
534
+ communityId,
535
+ (node.communityWeights.get(communityId) ?? 0) + Math.max(1, member.messageCount)
536
+ );
537
+ updateLastInteraction(node, member.lastMessageTs);
538
+ }
539
+ if (ownerNode) {
540
+ ownerNode.groupMessageCount += facts.ownerMessageCount;
541
+ for (const fact of facts.ownerEdges) {
542
+ if (fact.coOccurrenceCount <= 0 && fact.replyInteractionCount <= 0) continue;
543
+ const contactNode = getOrCreateNode(nodes, sessionId, meta, fact.contact);
544
+ const ownerEdgeScore = computeRawEdgeWeight({
545
+ coOccurrenceCount: fact.coOccurrenceCount,
546
+ coOccurrenceRawScore: fact.coOccurrenceRawScore,
547
+ replyInteractionCount: fact.replyInteractionCount
548
+ });
549
+ contribution.memberKeys.add(contactNode.key);
550
+ contribution.ownerConnectedKeys.add(contactNode.key);
551
+ contribution.ownerEdgeScores.set(
552
+ contactNode.key,
553
+ (contribution.ownerEdgeScores.get(contactNode.key) ?? 0) + ownerEdgeScore
554
+ );
555
+ ownerNode.commonGroupSessionIds.add(sessionId);
556
+ ownerNode.communityWeights.set(communityId, (ownerNode.communityWeights.get(communityId) ?? 0) + 1);
557
+ updateLastInteraction(ownerNode, fact.lastInteractionTs);
558
+ applyOwnerContactEdge(edges, ownerNode, contactNode, {
559
+ coOccurrenceCount: fact.coOccurrenceCount,
560
+ coOccurrenceRawScore: fact.coOccurrenceRawScore,
561
+ replyInteractionCount: fact.replyInteractionCount,
562
+ repliesFromOwnerToContact: fact.repliesFromOwnerToContact,
563
+ repliesFromContactToOwner: fact.repliesFromContactToOwner,
564
+ lastInteractionTs: fact.lastInteractionTs,
565
+ sessionId
566
+ });
567
+ }
568
+ }
569
+ for (const fact of facts.edges) {
570
+ const sourceNode = getOrCreateNode(nodes, sessionId, meta, fact.source);
571
+ const targetNode = getOrCreateNode(nodes, sessionId, meta, fact.target);
572
+ const sourceKey = sourceNode.key < targetNode.key ? sourceNode.key : targetNode.key;
573
+ const targetKey = sourceNode.key < targetNode.key ? targetNode.key : sourceNode.key;
574
+ const edge = getOrCreateEdge(edges, sourceKey, targetKey);
575
+ const currentEdgeId = edgeId(sourceKey, targetKey);
576
+ const relationshipEdgeScore = computeRawEdgeWeight(fact);
577
+ contribution.memberKeys.add(sourceNode.key);
578
+ contribution.memberKeys.add(targetNode.key);
579
+ contribution.relationshipEdgeIds.add(currentEdgeId);
580
+ contribution.relationshipEdgeEndpoints.set(currentEdgeId, [sourceKey, targetKey]);
581
+ contribution.relationshipEdgeScores.set(
582
+ currentEdgeId,
583
+ (contribution.relationshipEdgeScores.get(currentEdgeId) ?? 0) + relationshipEdgeScore
584
+ );
585
+ edge.coOccurrenceCount += fact.coOccurrenceCount;
586
+ edge.coOccurrenceRawScore += fact.coOccurrenceRawScore;
587
+ edge.replyInteractionCount += fact.replyInteractionCount;
588
+ if (sourceNode.key === sourceKey) {
589
+ edge.repliesFromSourceToTarget += fact.repliesFromSourceToTarget;
590
+ edge.repliesFromTargetToSource += fact.repliesFromTargetToSource;
591
+ } else {
592
+ edge.repliesFromSourceToTarget += fact.repliesFromTargetToSource;
593
+ edge.repliesFromTargetToSource += fact.repliesFromSourceToTarget;
594
+ }
595
+ edge.sourceSessionIds.add(sessionId);
596
+ edge.lastInteractionTs = maxNullableTs(edge.lastInteractionTs, fact.lastInteractionTs);
597
+ sourceNode.commonGroupSessionIds.add(sessionId);
598
+ targetNode.commonGroupSessionIds.add(sessionId);
599
+ sourceNode.communityWeights.set(communityId, (sourceNode.communityWeights.get(communityId) ?? 0) + 1);
600
+ targetNode.communityWeights.set(communityId, (targetNode.communityWeights.get(communityId) ?? 0) + 1);
601
+ updateLastInteraction(sourceNode, fact.lastInteractionTs);
602
+ updateLastInteraction(targetNode, fact.lastInteractionTs);
603
+ }
604
+ groupContributions.push(contribution);
605
+ }
606
+ function buildGraph(nodeAccumulators, edgeAccumulators, communityLabels, groupContributions, limits, baseDiagnostics) {
607
+ const edgeWeights = /* @__PURE__ */ new Map();
608
+ for (const edge of edgeAccumulators) {
609
+ const weight = computeEdgeWeight(edge);
610
+ edgeWeights.set(edgeId(edge.sourceKey, edge.targetKey), weight);
611
+ }
612
+ for (const edge of edgeAccumulators) {
613
+ const weight = edgeWeights.get(edgeId(edge.sourceKey, edge.targetKey)) ?? 0;
614
+ const source = nodeAccumulators.find((node) => node.key === edge.sourceKey);
615
+ const target = nodeAccumulators.find((node) => node.key === edge.targetKey);
616
+ if (source) source.edgeWeight += weight;
617
+ if (target) target.edgeWeight += weight;
618
+ }
619
+ const nodeScores = computeContactPriorityScores(nodeAccumulators);
620
+ const rankedNodes = nodeAccumulators.map((node) => ({
621
+ node,
622
+ score: nodeScores.get(node) ?? 0,
623
+ relationshipActivityScore: computeRelationshipActivityScore(node)
624
+ })).sort(compareRankedNodes);
625
+ const panoramaCandidateKeys = new Set(
626
+ rankedNodes.filter((item) => item.node.panoramaCandidate).map((item) => item.node.key)
627
+ );
628
+ const panoramaEligibleEdgeIds = new Set(
629
+ edgeAccumulators.filter((edge) => edge.panoramaEligible).map((edge) => edgeId(edge.sourceKey, edge.targetKey))
630
+ );
631
+ const maxScore = Math.max(...rankedNodes.map((item) => item.score), 1);
632
+ const nodeByKey = /* @__PURE__ */ new Map();
633
+ rankedNodes.forEach((item, index) => {
634
+ const communityId = pickCommunityId(item.node);
635
+ const color = colorForCommunity(communityId);
636
+ nodeByKey.set(item.node.key, {
637
+ key: item.node.key,
638
+ kind: item.node.kind,
639
+ platform: item.node.platform,
640
+ platformId: item.node.platformId,
641
+ sessionScoped: item.node.sessionScoped,
642
+ sessionId: item.node.sessionId,
643
+ displayName: item.node.displayName,
644
+ aliases: [...item.node.aliases].filter((alias) => alias !== item.node.displayName),
645
+ avatar: item.node.avatar,
646
+ pool: item.node.isFriend ? "friend" : "non_friend",
647
+ friendSource: item.node.friendSource,
648
+ score: roundNum(item.score),
649
+ rank: index + 1,
650
+ communityId,
651
+ x: 0,
652
+ y: 0,
653
+ size: roundNum(4 + Math.sqrt(item.score / maxScore) * 18, 2),
654
+ color,
655
+ labelVisibility: item.node.kind === "owner" || index < 30 ? 2 : index < 160 ? 1 : 0,
656
+ lastInteractionTs: item.node.lastInteractionTs,
657
+ privateMessageCount: item.node.privateMessageCount,
658
+ groupMessageCount: item.node.groupMessageCount,
659
+ commonGroupCount: item.node.commonGroupSessionIds.size,
660
+ searchText: [
661
+ item.node.displayName,
662
+ item.node.platformId,
663
+ ...item.node.aliases,
664
+ ...item.node.kind === "owner" ? ["\u6211", "me", "myself", "owner"] : []
665
+ ].join(" ").toLowerCase()
666
+ });
667
+ });
668
+ const allNodes = layoutNodes([...nodeByKey.values()], communityLabels);
669
+ const allEdges = edgeAccumulators.map((edge) => toGraphEdge(edge, edgeWeights.get(edgeId(edge.sourceKey, edge.targetKey)) ?? 0)).filter((edge) => nodeByKey.has(edge.sourceKey) && nodeByKey.has(edge.targetKey)).sort(compareEdges);
670
+ const communities = buildCommunities(allNodes, communityLabels, groupContributions);
671
+ const coreNodeKeys = new Set(
672
+ allNodes.filter((node) => panoramaCandidateKeys.has(node.key)).slice(0, limits.coreNodeLimit).map((node) => node.key)
673
+ );
674
+ const coreEdges = cropEdges(
675
+ allEdges.filter(
676
+ (edge) => coreNodeKeys.has(edge.sourceKey) && coreNodeKeys.has(edge.targetKey) && panoramaEligibleEdgeIds.has(edge.id)
677
+ ),
678
+ limits
679
+ );
680
+ const graphNodes = allNodes.filter((node) => coreNodeKeys.has(node.key));
681
+ const graph = {
682
+ nodes: graphNodes,
683
+ edges: coreEdges,
684
+ communities: filterCommunities(communities, graphNodes)
685
+ };
686
+ const diagnostics = {
687
+ ...baseDiagnostics,
688
+ totalNodes: allNodes.length,
689
+ totalEdges: allEdges.length,
690
+ panoramaCandidateNodes: panoramaCandidateKeys.size,
691
+ coreNodeCount: graph.nodes.length,
692
+ coreEdgeCount: graph.edges.length
693
+ };
694
+ return { nodes: allNodes, edges: allEdges, communities, graph, diagnostics };
695
+ }
696
+ function cropEdges(edges, limits) {
697
+ const counts = /* @__PURE__ */ new Map();
698
+ const kept = [];
699
+ for (const edge of sortEdgesForDisplay(edges)) {
700
+ if (kept.length >= limits.coreEdgeLimit) break;
701
+ const sourceCount = counts.get(edge.sourceKey) ?? 0;
702
+ const targetCount = counts.get(edge.targetKey) ?? 0;
703
+ if (sourceCount >= limits.perNodeEdgeLimit || targetCount >= limits.perNodeEdgeLimit) continue;
704
+ kept.push(edge);
705
+ counts.set(edge.sourceKey, sourceCount + 1);
706
+ counts.set(edge.targetKey, targetCount + 1);
707
+ }
708
+ return kept;
709
+ }
710
+ function layoutNodes(nodes, communityLabels) {
711
+ const byCommunity = /* @__PURE__ */ new Map();
712
+ for (const node of nodes) {
713
+ const group = byCommunity.get(node.communityId) ?? [];
714
+ group.push(node);
715
+ byCommunity.set(node.communityId, group);
716
+ }
717
+ const sortedCommunities = [...byCommunity.entries()].sort((a, b) => {
718
+ if (a[0] === PRIVATE_COMMUNITY_ID) return -1;
719
+ if (b[0] === PRIVATE_COMMUNITY_ID) return 1;
720
+ const scoreA = a[1].reduce((sum, node) => sum + node.score, 0);
721
+ const scoreB = b[1].reduce((sum, node) => sum + node.score, 0);
722
+ return scoreB - scoreA || (communityLabels.get(a[0]) ?? a[0]).localeCompare(communityLabels.get(b[0]) ?? b[0]);
723
+ });
724
+ for (const [communityIndex, [communityId, communityNodes]] of sortedCommunities.entries()) {
725
+ const baseAngle = communityAngle(communityIndex, communityId);
726
+ communityNodes.sort(compareNodes);
727
+ for (const [index, node] of communityNodes.entries()) {
728
+ if (node.kind === "owner") {
729
+ node.x = 0;
730
+ node.y = 0;
731
+ continue;
732
+ }
733
+ const angle = egoNodeAngle(baseAngle, communityId, node.key, index, communityNodes.length);
734
+ const radius = egoNodeRadius(node, communityId, communityNodes.length);
735
+ node.x = roundNum(Math.cos(angle) * radius, 2);
736
+ node.y = roundNum(Math.sin(angle) * radius, 2);
737
+ }
738
+ }
739
+ return nodes.sort(compareNodes);
740
+ }
741
+ function communityAngle(index, communityId) {
742
+ if (communityId === PRIVATE_COMMUNITY_ID) return -Math.PI / 2;
743
+ return index * 2.399963229728653;
744
+ }
745
+ function egoNodeAngle(baseAngle, communityId, nodeKey, index, communitySize) {
746
+ const seedOffset = (stableHash(`${communityId}:${nodeKey}:angle`) % 1e3 / 1e3 - 0.5) * 0.16;
747
+ const centeredIndex = index - (communitySize - 1) / 2;
748
+ const spacing = communitySize <= 4 ? 0.38 : communitySize <= 12 ? 0.24 : 0.12;
749
+ const maxSpread = communitySize <= 4 ? 0.75 : communitySize <= 20 ? 1.1 : 1.35;
750
+ return baseAngle + clamp(centeredIndex * spacing + seedOffset, -maxSpread, maxSpread);
751
+ }
752
+ function egoNodeRadius(node, communityId, communitySize) {
753
+ const rankPenalty = Math.sqrt(Math.max(0, node.rank - 1)) * 26;
754
+ const scoreBonus = Math.min(260, Math.sqrt(Math.max(0, node.score)) * 22);
755
+ const privateBonus = node.privateMessageCount > 0 ? Math.min(220, Math.log1p(node.privateMessageCount) * 48) : 0;
756
+ const friendBonus = node.pool === "friend" ? 280 : 0;
757
+ const commonGroupBonus = Math.min(180, node.commonGroupCount * 18);
758
+ const groupSizePenalty = communityId.startsWith("group:") ? Math.min(1400, Math.max(0, communitySize - 6) * 22 + Math.sqrt(communitySize) * 24) : 0;
759
+ const baseRadius = node.pool === "friend" ? 430 : 760;
760
+ const minRadius = node.pool === "friend" ? 160 : 340;
761
+ return roundNum(
762
+ clamp(
763
+ baseRadius + rankPenalty + groupSizePenalty - scoreBonus - privateBonus - friendBonus - commonGroupBonus,
764
+ minRadius,
765
+ 2600
766
+ ),
767
+ 2
768
+ );
769
+ }
770
+ function buildCommunities(nodes, communityLabels, groupContributions) {
771
+ const groups = /* @__PURE__ */ new Map();
772
+ for (const node of nodes) {
773
+ const group = groups.get(node.communityId) ?? [];
774
+ group.push(node);
775
+ groups.set(node.communityId, group);
776
+ }
777
+ const groupMemberCounts = new Map(groupContributions.map((item) => [`group:${item.sessionId}`, item.memberKeys.size]));
778
+ const communitiesById = /* @__PURE__ */ new Map();
779
+ for (const [id, group] of groups) {
780
+ const x = group.reduce((sum, node) => sum + node.x, 0) / group.length;
781
+ const y = group.reduce((sum, node) => sum + node.y, 0) / group.length;
782
+ communitiesById.set(id, {
783
+ id,
784
+ label: communityLabels.get(id) ?? id,
785
+ size: groupMemberCounts.get(id) ?? group.length,
786
+ x: roundNum(x, 2),
787
+ y: roundNum(y, 2),
788
+ color: colorForCommunity(id)
789
+ });
790
+ }
791
+ for (const [id, label] of communityLabels) {
792
+ if (communitiesById.has(id) || !id.startsWith("group:")) continue;
793
+ communitiesById.set(id, {
794
+ id,
795
+ label,
796
+ size: groupMemberCounts.get(id) ?? 0,
797
+ x: 0,
798
+ y: 0,
799
+ color: colorForCommunity(id)
800
+ });
801
+ }
802
+ return [...communitiesById.values()].sort((a, b) => b.size - a.size || a.label.localeCompare(b.label));
803
+ }
804
+ function filterNeighborhoodCommunities(communities, relatedEdges) {
805
+ const communityById = new Map(communities.map((community) => [community.id, community]));
806
+ const sourceCommunityIds = /* @__PURE__ */ new Set();
807
+ for (const edge of relatedEdges) {
808
+ for (const sessionId of edge.sourceSessionIds) sourceCommunityIds.add(`group:${sessionId}`);
809
+ }
810
+ return [...sourceCommunityIds].map((id) => communityById.get(id)).filter((community) => Boolean(community)).sort((a, b) => b.size - a.size || a.label.localeCompare(b.label));
811
+ }
812
+ function filterCommunities(communities, nodes) {
813
+ const ids = new Set(nodes.map((node) => node.communityId));
814
+ return communities.filter((community) => ids.has(community.id));
815
+ }
816
+ function applyPanoramaContributions(nodes, edges, groupContributions, diagnostics) {
817
+ const nodeByKey = new Map(nodes.map((node) => [node.key, node]));
818
+ for (const contribution of groupContributions) {
819
+ const decision = evaluateGroupPanoramaContribution(contribution, nodeByKey);
820
+ if (!decision.included) {
821
+ diagnostics.panoramaExcludedLowValueGroupSessions++;
822
+ diagnostics.panoramaExcludedGroupMembers += contribution.memberKeys.size;
823
+ continue;
824
+ }
825
+ diagnostics.panoramaIncludedGroupSessions++;
826
+ incrementPanoramaReason(diagnostics, decision.reason);
827
+ for (const key of decision.selectedMemberKeys) {
828
+ const node = nodeByKey.get(key);
829
+ if (node) node.panoramaCandidate = true;
830
+ }
831
+ for (const edgeId2 of contribution.relationshipEdgeIds) {
832
+ const endpoints = contribution.relationshipEdgeEndpoints.get(edgeId2);
833
+ if (!endpoints) continue;
834
+ if (!decision.selectedMemberKeys.has(endpoints[0]) || !decision.selectedMemberKeys.has(endpoints[1])) continue;
835
+ const edge = edges.get(edgeId2);
836
+ if (edge) edge.panoramaEligible = true;
837
+ }
838
+ for (const key of contribution.ownerConnectedKeys) {
839
+ if (!decision.selectedMemberKeys.has(key)) continue;
840
+ const ownerKey = buildOwnerKeyForContribution(nodeByKey, key);
841
+ if (!ownerKey) continue;
842
+ const ownerEdgeId = sortedEdgeId(ownerKey, key);
843
+ const edge = edges.get(ownerEdgeId);
844
+ if (edge) edge.panoramaEligible = true;
845
+ }
846
+ diagnostics.panoramaIncludedGroupMembers += decision.selectedMemberKeys.size;
847
+ diagnostics.panoramaExcludedGroupMembers += Math.max(
848
+ 0,
849
+ contribution.memberKeys.size - decision.selectedMemberKeys.size
850
+ );
851
+ }
852
+ }
853
+ function evaluateGroupPanoramaContribution(contribution, nodeByKey) {
854
+ const friendKeys = [...contribution.memberKeys].filter((key) => nodeByKey.get(key)?.isFriend === true);
855
+ const memberCount = contribution.memberKeys.size;
856
+ const friendCount = friendKeys.length;
857
+ const friendRatio = memberCount > 0 ? friendCount / memberCount : 0;
858
+ const ownerConnectedCount = contribution.ownerConnectedKeys.size;
859
+ const totalGroupMessages = contribution.ownerMessageCount + [...contribution.memberMessageCounts.values()].reduce((sum, count) => sum + count, 0);
860
+ const ownerMessageRatio = totalGroupMessages > 0 ? contribution.ownerMessageCount / totalGroupMessages : 0;
861
+ const friendConnectedScores = computeFriendConnectedScores(contribution, new Set(friendKeys));
862
+ contribution.friendConnectedScores.clear();
863
+ for (const [key, score] of friendConnectedScores) contribution.friendConnectedScores.set(key, score);
864
+ let reason = null;
865
+ if (friendCount === 0) {
866
+ reason = null;
867
+ } else if (friendCount >= MIN_GROUP_FRIENDS_FOR_PANORAMA) {
868
+ reason = "friend_count";
869
+ } else if (friendCount >= 2 && friendRatio >= MIN_GROUP_FRIEND_RATIO_FOR_PANORAMA) {
870
+ reason = "friend_ratio";
871
+ } else if (ownerConnectedCount >= MIN_OWNER_CONNECTED_MEMBERS_FOR_PANORAMA) {
872
+ reason = "owner_connected";
873
+ } else if (contribution.ownerMessageCount >= MIN_OWNER_GROUP_MESSAGES_FOR_PANORAMA || contribution.ownerMessageCount >= MIN_OWNER_GROUP_MESSAGES_FOR_RATIO && ownerMessageRatio >= MIN_OWNER_GROUP_MESSAGE_RATIO_FOR_PANORAMA) {
874
+ reason = "owner_activity";
875
+ } else if (memberCount <= SMALL_GROUP_MEMBER_LIMIT_FOR_FRIEND_EDGE && friendConnectedScores.size > 0) {
876
+ reason = "friend_connected";
877
+ }
878
+ if (!reason) return { included: false, reason: "low_value", selectedMemberKeys: /* @__PURE__ */ new Set() };
879
+ return {
880
+ included: true,
881
+ reason,
882
+ selectedMemberKeys: selectPanoramaMembers(contribution, nodeByKey, new Set(friendKeys))
883
+ };
884
+ }
885
+ function computeFriendConnectedScores(contribution, friendKeys) {
886
+ const scores = /* @__PURE__ */ new Map();
887
+ for (const [edgeId2, endpoints] of contribution.relationshipEdgeEndpoints) {
888
+ const score = contribution.relationshipEdgeScores.get(edgeId2) ?? 0;
889
+ const [sourceKey, targetKey] = endpoints;
890
+ if (friendKeys.has(sourceKey) && !friendKeys.has(targetKey)) {
891
+ scores.set(targetKey, (scores.get(targetKey) ?? 0) + score);
892
+ } else if (friendKeys.has(targetKey) && !friendKeys.has(sourceKey)) {
893
+ scores.set(sourceKey, (scores.get(sourceKey) ?? 0) + score);
894
+ }
895
+ }
896
+ return scores;
897
+ }
898
+ function selectPanoramaMembers(contribution, nodeByKey, friendKeys) {
899
+ const selected = new Set(friendKeys);
900
+ const remainingLimit = () => Math.max(0, MAX_MEMBERS_PER_GROUP_FOR_PANORAMA - selected.size);
901
+ addTopScoredKeys(selected, contribution.ownerEdgeScores, MAX_OWNER_CONNECTED_NON_FRIENDS_PER_GROUP, remainingLimit());
902
+ addTopScoredKeys(
903
+ selected,
904
+ contribution.friendConnectedScores,
905
+ MAX_FRIEND_CONNECTED_NON_FRIENDS_PER_GROUP,
906
+ remainingLimit()
907
+ );
908
+ addTopScoredKeys(
909
+ selected,
910
+ computeHighSignalNonFriendScores(contribution, nodeByKey, friendKeys),
911
+ MAX_HIGH_SIGNAL_NON_FRIENDS_PER_GROUP,
912
+ remainingLimit()
913
+ );
914
+ return selected;
915
+ }
916
+ function computeHighSignalNonFriendScores(contribution, nodeByKey, friendKeys) {
917
+ const scores = /* @__PURE__ */ new Map();
918
+ for (const key of contribution.memberKeys) {
919
+ const node = nodeByKey.get(key);
920
+ if (!node || node.kind === "owner" || friendKeys.has(key)) continue;
921
+ const messageScore = Math.log1p(contribution.memberMessageCounts.get(key) ?? 0);
922
+ const ownerScore = contribution.ownerEdgeScores.get(key) ?? 0;
923
+ const friendScore = contribution.friendConnectedScores.get(key) ?? 0;
924
+ const relationshipScore = sumRelationshipScoresForKey(contribution, key);
925
+ const score = ownerScore * 2 + friendScore * 1.5 + relationshipScore + messageScore;
926
+ if (score > 0) scores.set(key, score);
927
+ }
928
+ return scores;
929
+ }
930
+ function sumRelationshipScoresForKey(contribution, key) {
931
+ let total = 0;
932
+ for (const [edgeId2, endpoints] of contribution.relationshipEdgeEndpoints) {
933
+ if (endpoints[0] === key || endpoints[1] === key) total += contribution.relationshipEdgeScores.get(edgeId2) ?? 0;
934
+ }
935
+ return total;
936
+ }
937
+ function addTopScoredKeys(selected, scores, limit, remainingLimit) {
938
+ if (limit <= 0 || remainingLimit <= 0) return;
939
+ const items = [...scores.entries()].filter(([key, score]) => !selected.has(key) && score > 0).sort((a, b) => b[1] - a[1] || a[0].localeCompare(b[0]));
940
+ for (const [key] of items.slice(0, Math.min(limit, remainingLimit))) {
941
+ selected.add(key);
942
+ }
943
+ }
944
+ function incrementPanoramaReason(diagnostics, reason) {
945
+ diagnostics.panoramaGroupInclusionReasons[reason] = (diagnostics.panoramaGroupInclusionReasons[reason] ?? 0) + 1;
946
+ }
947
+ function buildOwnerKeyForContribution(nodeByKey, contactKey) {
948
+ const contact = nodeByKey.get(contactKey);
949
+ return contact ? buildOwnerKey() : "";
950
+ }
951
+ function toGraphEdge(edge, weight) {
952
+ return {
953
+ id: edgeId(edge.sourceKey, edge.targetKey),
954
+ sourceKey: edge.sourceKey,
955
+ targetKey: edge.targetKey,
956
+ weight: roundNum(weight),
957
+ coOccurrenceCount: edge.coOccurrenceCount,
958
+ coOccurrenceRawScore: roundNum(edge.coOccurrenceRawScore),
959
+ replyInteractionCount: edge.replyInteractionCount,
960
+ repliesFromSourceToTarget: edge.repliesFromSourceToTarget,
961
+ repliesFromTargetToSource: edge.repliesFromTargetToSource,
962
+ sourceGroupCount: edge.sourceSessionIds.size,
963
+ sourceSessionIds: [...edge.sourceSessionIds].sort(),
964
+ lastInteractionTs: edge.lastInteractionTs,
965
+ visibility: weight >= 8 ? 2 : 1
966
+ };
967
+ }
968
+ function createFactsCacheContext(dir) {
969
+ return {
970
+ dir,
971
+ latestKey: buildPeopleRelationshipsSessionLatestCacheKey(PEOPLE_RELATIONSHIPS_ALGORITHM_VERSION),
972
+ stats: createEmptyPeopleRelationshipsFactsCacheStats()
973
+ };
974
+ }
975
+ function readLatestFacts(sessionId, factsCache, dbVersion) {
976
+ if (!factsCache) return null;
977
+ const cached = readCachedPeopleRelationshipsSessionLatest(sessionId, factsCache.dir, factsCache.latestKey, dbVersion);
978
+ if (!cached.hit) {
979
+ factsCache.stats.latestMisses++;
980
+ return null;
981
+ }
982
+ factsCache.stats.latestHits++;
983
+ return cached.data;
984
+ }
985
+ function writeLatestFacts(adapter, sessionId, factsCache, data, expectedDbVersion) {
986
+ if (!factsCache) return;
987
+ const dbVersion = getSessionDbVersion(adapter, sessionId);
988
+ if (dbVersion !== expectedDbVersion) {
989
+ appLogger.debug("people-relationships", "skipped latest facts cache write because db version changed", {
990
+ sessionId
991
+ });
992
+ return;
993
+ }
994
+ writeCachedPeopleRelationshipsSessionLatest(sessionId, factsCache.dir, factsCache.latestKey, dbVersion, data);
995
+ factsCache.stats.writes++;
996
+ }
997
+ function readSessionFacts(sessionId, timeRange, factsCache, dbVersion) {
998
+ if (!factsCache) return null;
999
+ const cached = readCachedPeopleRelationshipsSessionFacts(
1000
+ sessionId,
1001
+ factsCache.dir,
1002
+ buildPeopleRelationshipsSessionFactsCacheKey(PEOPLE_RELATIONSHIPS_ALGORITHM_VERSION, timeRange),
1003
+ dbVersion
1004
+ );
1005
+ if (!cached.hit) {
1006
+ factsCache.stats.factsMisses++;
1007
+ return null;
1008
+ }
1009
+ factsCache.stats.factsHits++;
1010
+ return cached.data;
1011
+ }
1012
+ function writeSessionFacts(adapter, sessionId, timeRange, factsCache, facts, expectedDbVersion) {
1013
+ if (!factsCache) return;
1014
+ const dbVersion = getSessionDbVersion(adapter, sessionId);
1015
+ if (dbVersion !== expectedDbVersion) {
1016
+ appLogger.debug("people-relationships", "skipped session facts cache write because db version changed", {
1017
+ sessionId
1018
+ });
1019
+ return;
1020
+ }
1021
+ writeCachedPeopleRelationshipsSessionFacts(
1022
+ sessionId,
1023
+ factsCache.dir,
1024
+ buildPeopleRelationshipsSessionFactsCacheKey(PEOPLE_RELATIONSHIPS_ALGORITHM_VERSION, timeRange),
1025
+ dbVersion,
1026
+ facts
1027
+ );
1028
+ writeCachedPeopleRelationshipsSessionLatest(sessionId, factsCache.dir, factsCache.latestKey, dbVersion, {
1029
+ latestMessageTs: facts.latestMessageTs
1030
+ });
1031
+ factsCache.stats.writes += 2;
1032
+ }
1033
+ function getSessionDbVersion(adapter, sessionId) {
1034
+ return getDbFileVersion(adapter.getDbPath(sessionId));
1035
+ }
1036
+ function getOrCreateNode(nodes, sessionId, meta, contact) {
1037
+ const sessionScoped = shouldScopeContactToSession(meta.platform, contact);
1038
+ const key = buildContactKey(meta.platform, contact.platformId, sessionScoped ? sessionId : void 0);
1039
+ const existing = nodes.get(key);
1040
+ if (existing) {
1041
+ mergeContactIdentity(existing, contact);
1042
+ return existing;
1043
+ }
1044
+ const created = {
1045
+ key,
1046
+ kind: "contact",
1047
+ platform: meta.platform,
1048
+ platformId: contact.platformId,
1049
+ sessionScoped,
1050
+ sessionId: sessionScoped ? sessionId : void 0,
1051
+ displayName: contact.name || contact.platformId,
1052
+ aliases: new Set([contact.platformId, contact.name, ...contact.aliases].filter(Boolean)),
1053
+ avatar: contact.avatar,
1054
+ isFriend: false,
1055
+ privateMessageCount: 0,
1056
+ activePrivateMonths: /* @__PURE__ */ new Set(),
1057
+ groupMessageCount: 0,
1058
+ commonGroupSessionIds: /* @__PURE__ */ new Set(),
1059
+ ownerCoOccurrenceCount: 0,
1060
+ ownerCoOccurrenceRawScore: 0,
1061
+ ownerReplyInteractionCount: 0,
1062
+ ownerRepliesFromOwnerToContact: 0,
1063
+ ownerRepliesFromContactToOwner: 0,
1064
+ communityWeights: /* @__PURE__ */ new Map(),
1065
+ edgeWeight: 0,
1066
+ panoramaCandidate: false,
1067
+ lastInteractionTs: null
1068
+ };
1069
+ nodes.set(key, created);
1070
+ return created;
1071
+ }
1072
+ function getOrCreateOwnerNode(nodes, meta) {
1073
+ if (!meta.owner) return null;
1074
+ const key = buildOwnerKey();
1075
+ const existing = nodes.get(key);
1076
+ if (existing) {
1077
+ mergeContactIdentity(existing, meta.owner);
1078
+ return existing;
1079
+ }
1080
+ const created = {
1081
+ key,
1082
+ kind: "owner",
1083
+ platform: meta.platform,
1084
+ platformId: meta.owner.platformId || meta.ownerId || OWNER_KEY_PREFIX,
1085
+ sessionScoped: false,
1086
+ displayName: meta.owner.name || meta.owner.platformId || "Me",
1087
+ aliases: new Set(
1088
+ [meta.owner.platformId, meta.owner.name, ...meta.owner.aliases, "\u6211", "me", "owner"].filter(Boolean)
1089
+ ),
1090
+ avatar: meta.owner.avatar,
1091
+ isFriend: true,
1092
+ privateMessageCount: 0,
1093
+ activePrivateMonths: /* @__PURE__ */ new Set(),
1094
+ groupMessageCount: 0,
1095
+ commonGroupSessionIds: /* @__PURE__ */ new Set(),
1096
+ ownerCoOccurrenceCount: 0,
1097
+ ownerCoOccurrenceRawScore: 0,
1098
+ ownerReplyInteractionCount: 0,
1099
+ ownerRepliesFromOwnerToContact: 0,
1100
+ ownerRepliesFromContactToOwner: 0,
1101
+ communityWeights: /* @__PURE__ */ new Map(),
1102
+ edgeWeight: 0,
1103
+ panoramaCandidate: true,
1104
+ lastInteractionTs: null
1105
+ };
1106
+ nodes.set(key, created);
1107
+ return created;
1108
+ }
1109
+ function applyOwnerContactEdge(edges, ownerNode, contactNode, facts) {
1110
+ const sourceKey = ownerNode.key < contactNode.key ? ownerNode.key : contactNode.key;
1111
+ const targetKey = ownerNode.key < contactNode.key ? contactNode.key : ownerNode.key;
1112
+ const edge = getOrCreateEdge(edges, sourceKey, targetKey);
1113
+ edge.coOccurrenceCount += facts.coOccurrenceCount;
1114
+ edge.coOccurrenceRawScore += facts.coOccurrenceRawScore;
1115
+ edge.replyInteractionCount += facts.replyInteractionCount;
1116
+ if (sourceKey === ownerNode.key) {
1117
+ edge.repliesFromSourceToTarget += facts.repliesFromOwnerToContact;
1118
+ edge.repliesFromTargetToSource += facts.repliesFromContactToOwner;
1119
+ } else {
1120
+ edge.repliesFromSourceToTarget += facts.repliesFromContactToOwner;
1121
+ edge.repliesFromTargetToSource += facts.repliesFromOwnerToContact;
1122
+ }
1123
+ edge.sourceSessionIds.add(facts.sessionId);
1124
+ if (facts.panoramaEligible) edge.panoramaEligible = true;
1125
+ edge.lastInteractionTs = maxNullableTs(edge.lastInteractionTs, facts.lastInteractionTs);
1126
+ contactNode.ownerCoOccurrenceCount += facts.coOccurrenceCount;
1127
+ contactNode.ownerCoOccurrenceRawScore += facts.coOccurrenceRawScore;
1128
+ contactNode.ownerReplyInteractionCount += facts.replyInteractionCount;
1129
+ contactNode.ownerRepliesFromOwnerToContact += facts.repliesFromOwnerToContact;
1130
+ contactNode.ownerRepliesFromContactToOwner += facts.repliesFromContactToOwner;
1131
+ updateLastInteraction(contactNode, facts.lastInteractionTs);
1132
+ }
1133
+ function getOrCreateEdge(edges, sourceKey, targetKey) {
1134
+ const id = edgeId(sourceKey, targetKey);
1135
+ const existing = edges.get(id);
1136
+ if (existing) return existing;
1137
+ const created = {
1138
+ sourceKey,
1139
+ targetKey,
1140
+ coOccurrenceCount: 0,
1141
+ coOccurrenceRawScore: 0,
1142
+ replyInteractionCount: 0,
1143
+ repliesFromSourceToTarget: 0,
1144
+ repliesFromTargetToSource: 0,
1145
+ sourceSessionIds: /* @__PURE__ */ new Set(),
1146
+ panoramaEligible: false,
1147
+ lastInteractionTs: null
1148
+ };
1149
+ edges.set(id, created);
1150
+ return created;
1151
+ }
1152
+ function toPeopleRelationshipsSessionMetaFacts(meta, owner) {
1153
+ return {
1154
+ name: meta.name,
1155
+ platform: meta.platform,
1156
+ type: meta.type,
1157
+ ownerId: meta.ownerId,
1158
+ owner
1159
+ };
1160
+ }
1161
+ function shouldScopeContactToSession(platform, contact) {
1162
+ if (isNameMatchPlatform(platform)) return true;
1163
+ return platform.trim().toLowerCase() === "qq" && contact.platformId.trim() === contact.name.trim();
1164
+ }
1165
+ function buildContactKey(platform, platformId, sessionId) {
1166
+ const normalizedPlatform = platform.trim();
1167
+ const normalizedPlatformId = platformId.trim();
1168
+ if (!normalizedPlatform) throw new Error("platform is required");
1169
+ if (!normalizedPlatformId) throw new Error("platformId is required");
1170
+ return sessionId?.trim() ? `${normalizedPlatform}:${sessionId.trim()}:${normalizedPlatformId}` : `${normalizedPlatform}:${normalizedPlatformId}`;
1171
+ }
1172
+ function buildOwnerKey() {
1173
+ return OWNER_KEY_PREFIX;
1174
+ }
1175
+ function mergeContactIdentity(acc, contact) {
1176
+ if (contact.name) acc.aliases.add(contact.name);
1177
+ acc.aliases.add(contact.platformId);
1178
+ for (const alias of contact.aliases) acc.aliases.add(alias);
1179
+ if ((!acc.displayName || acc.displayName === acc.platformId) && contact.name) acc.displayName = contact.name;
1180
+ if (!acc.avatar && contact.avatar) acc.avatar = contact.avatar;
1181
+ }
1182
+ function pickCommunityId(node) {
1183
+ let bestId = node.isFriend ? PRIVATE_COMMUNITY_ID : "";
1184
+ let bestWeight = node.isFriend ? 1 : 0;
1185
+ for (const [id, weight] of node.communityWeights) {
1186
+ if (weight > bestWeight || weight === bestWeight && id < bestId) {
1187
+ bestId = id;
1188
+ bestWeight = weight;
1189
+ }
1190
+ }
1191
+ return bestId || PRIVATE_COMMUNITY_ID;
1192
+ }
1193
+ function computeEdgeWeight(edge) {
1194
+ return computeRawEdgeWeight(edge);
1195
+ }
1196
+ function computeRawEdgeWeight(edge) {
1197
+ return edge.coOccurrenceRawScore + edge.replyInteractionCount * REPLY_WEIGHT + edge.coOccurrenceCount * CO_OCCURRENCE_COUNT_WEIGHT;
1198
+ }
1199
+ function computeContactPriorityScores(nodes) {
1200
+ const result = /* @__PURE__ */ new Map();
1201
+ for (const node of nodes) {
1202
+ if (node.kind === "owner") result.set(node, 1);
1203
+ }
1204
+ const friendInputs = nodes.filter((node) => node.kind !== "owner" && node.isFriend).map((node) => ({
1205
+ node,
1206
+ privateMessageCount: node.privateMessageCount,
1207
+ activeMonths: [...node.activePrivateMonths],
1208
+ commonGroupCount: node.commonGroupSessionIds.size
1209
+ }));
1210
+ const nonFriendInputs = nodes.filter((node) => node.kind !== "owner" && !node.isFriend).map((node) => ({
1211
+ node,
1212
+ coOccurrenceRawScore: node.ownerCoOccurrenceRawScore,
1213
+ commonGroupCount: node.commonGroupSessionIds.size,
1214
+ replyInteractionCount: node.ownerReplyInteractionCount,
1215
+ coOccurrenceCount: node.ownerCoOccurrenceCount,
1216
+ repliesFromOwnerToContact: node.ownerRepliesFromOwnerToContact,
1217
+ repliesFromContactToOwner: node.ownerRepliesFromContactToOwner
1218
+ }));
1219
+ const friendScores = computeFriendScores(friendInputs);
1220
+ const nonFriendScores = computeNonFriendScores(nonFriendInputs);
1221
+ for (const input of friendInputs) {
1222
+ result.set(input.node, friendScores.get(input)?.score ?? 0);
1223
+ }
1224
+ for (const input of nonFriendInputs) {
1225
+ result.set(input.node, nonFriendScores.get(input)?.score ?? 0);
1226
+ }
1227
+ return result;
1228
+ }
1229
+ function compareRankedNodes(a, b) {
1230
+ const poolPriorityDiff = getNodePoolPriority(a.node) - getNodePoolPriority(b.node);
1231
+ if (poolPriorityDiff !== 0) return poolPriorityDiff;
1232
+ return b.score - a.score || b.relationshipActivityScore - a.relationshipActivityScore || a.node.displayName.localeCompare(b.node.displayName);
1233
+ }
1234
+ function getNodePoolPriority(node) {
1235
+ if (node.kind === "owner") return 0;
1236
+ return node.isFriend ? 1 : 2;
1237
+ }
1238
+ function computeRelationshipActivityScore(node) {
1239
+ return (node.isFriend ? 60 : 0) + node.privateMessageCount * 1.8 + node.groupMessageCount * 0.35 + node.commonGroupSessionIds.size * 8 + node.edgeWeight * 6;
1240
+ }
1241
+ function normalizeLimits(limits = {}) {
1242
+ return {
1243
+ coreNodeLimit: normalizePositiveLimit(limits.coreNodeLimit, 1500),
1244
+ coreEdgeLimit: normalizePositiveLimit(limits.coreEdgeLimit, 6e3),
1245
+ perNodeEdgeLimit: normalizePositiveLimit(limits.perNodeEdgeLimit, 12),
1246
+ neighborhoodNodeLimit: normalizePositiveLimit(limits.neighborhoodNodeLimit, 80),
1247
+ neighborhoodEdgeLimit: normalizePositiveLimit(limits.neighborhoodEdgeLimit, 240),
1248
+ searchResultLimit: normalizePositiveLimit(limits.searchResultLimit, 20)
1249
+ };
1250
+ }
1251
+ function normalizePositiveLimit(value, fallback) {
1252
+ if (!Number.isFinite(value)) return fallback;
1253
+ return Math.max(1, Math.trunc(value));
1254
+ }
1255
+ function createEmptyDiagnostics() {
1256
+ return {
1257
+ processedPrivateSessions: 0,
1258
+ processedGroupSessions: 0,
1259
+ skippedMissingOwnerSessions: 0,
1260
+ skippedUnresolvedOwnerSessions: 0,
1261
+ skippedAmbiguousPrivateSessions: 0,
1262
+ skippedFailedSessions: 0,
1263
+ totalNodes: 0,
1264
+ totalEdges: 0,
1265
+ panoramaIncludedGroupSessions: 0,
1266
+ panoramaExcludedLowValueGroupSessions: 0,
1267
+ panoramaIncludedGroupMembers: 0,
1268
+ panoramaExcludedGroupMembers: 0,
1269
+ panoramaCandidateNodes: 0,
1270
+ panoramaGroupInclusionReasons: {},
1271
+ coreNodeCount: 0,
1272
+ coreEdgeCount: 0,
1273
+ warnings: []
1274
+ };
1275
+ }
1276
+ function updateLastInteraction(acc, ts) {
1277
+ acc.lastInteractionTs = maxNullableTs(acc.lastInteractionTs, ts);
1278
+ }
1279
+ function maxNullableTs(current, next) {
1280
+ if (next === null) return current;
1281
+ return Math.max(current ?? 0, next);
1282
+ }
1283
+ function edgeId(sourceKey, targetKey) {
1284
+ return `${sourceKey}__${targetKey}`;
1285
+ }
1286
+ function sortedEdgeId(aKey, bKey) {
1287
+ return aKey < bKey ? edgeId(aKey, bKey) : edgeId(bKey, aKey);
1288
+ }
1289
+ function colorForCommunity(id) {
1290
+ return COMMUNITY_COLORS[stableHash(id) % COMMUNITY_COLORS.length];
1291
+ }
1292
+ function stableHash(value) {
1293
+ let hash = 2166136261;
1294
+ for (let index = 0; index < value.length; index++) {
1295
+ hash ^= value.charCodeAt(index);
1296
+ hash = Math.imul(hash, 16777619);
1297
+ }
1298
+ return hash >>> 0;
1299
+ }
1300
+ function compareNodes(a, b) {
1301
+ return a.rank - b.rank || a.displayName.localeCompare(b.displayName);
1302
+ }
1303
+ function compareEdges(a, b) {
1304
+ return b.weight - a.weight || a.id.localeCompare(b.id);
1305
+ }
1306
+ function sortEdgesForDisplay(edges) {
1307
+ const anchorTs = edges.reduce((max, edge) => Math.max(max, edge.lastInteractionTs ?? 0), 0);
1308
+ return [...edges].sort((a, b) => compareEdgesForDisplay(a, b, anchorTs));
1309
+ }
1310
+ function compareEdgesForDisplay(a, b, anchorTs) {
1311
+ const bRecentWeight = getRecentEdgeWeight(b, anchorTs);
1312
+ const aRecentWeight = getRecentEdgeWeight(a, anchorTs);
1313
+ if (bRecentWeight !== aRecentWeight) return bRecentWeight - aRecentWeight;
1314
+ return compareEdges(a, b);
1315
+ }
1316
+ function getRecentEdgeWeight(edge, anchorTs) {
1317
+ if (!edge.lastInteractionTs || anchorTs <= 0) return edge.weight;
1318
+ const ageSeconds = Math.max(0, anchorTs - edge.lastInteractionTs);
1319
+ const recencyFactor = EDGE_RECENCY_FLOOR + (1 - EDGE_RECENCY_FLOOR) * Math.pow(0.5, ageSeconds / EDGE_RECENCY_HALF_LIFE_SECONDS);
1320
+ return edge.weight * recencyFactor;
1321
+ }
1322
+ function clamp(value, min, max) {
1323
+ return Math.min(max, Math.max(min, value));
1324
+ }
1325
+ function roundNum(value, digits = 4) {
1326
+ const factor = 10 ** digits;
1327
+ return Math.round(value * factor) / factor;
1328
+ }
1329
+
1330
+ export {
1331
+ normalizePeopleRelationshipsTimeRangePreset,
1332
+ resolvePeopleRelationshipsTimeRange,
1333
+ PEOPLE_RELATIONSHIPS_ALGORITHM_VERSION,
1334
+ computePeopleRelationshipsSnapshot,
1335
+ buildPeopleRelationshipsNeighborhoodGraph,
1336
+ compareNodes,
1337
+ compareEdges
1338
+ };
1339
+ //# sourceMappingURL=chunk-2MZ6G4AC.mjs.map