six-degrees 0.4.10 → 0.5.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 (414) hide show
  1. package/.next/standalone/.next/BUILD_ID +1 -1
  2. package/.next/standalone/.next/app-path-routes-manifest.json +1 -0
  3. package/.next/standalone/.next/build-manifest.json +3 -3
  4. package/.next/standalone/.next/prerender-manifest.json +9 -9
  5. package/.next/standalone/.next/routes-manifest.json +6 -0
  6. package/.next/standalone/.next/server/app/_global-error.html +1 -1
  7. package/.next/standalone/.next/server/app/_global-error.rsc +1 -1
  8. package/.next/standalone/.next/server/app/_global-error.segments/__PAGE__.segment.rsc +1 -1
  9. package/.next/standalone/.next/server/app/_global-error.segments/_full.segment.rsc +1 -1
  10. package/.next/standalone/.next/server/app/_global-error.segments/_tree.segment.rsc +1 -1
  11. package/.next/standalone/.next/server/app/_not-found/page.js.nft.json +1 -1
  12. package/.next/standalone/.next/server/app/_not-found/page_client-reference-manifest.js +1 -1
  13. package/.next/standalone/.next/server/app/_not-found.html +1 -1
  14. package/.next/standalone/.next/server/app/_not-found.rsc +13 -13
  15. package/.next/standalone/.next/server/app/_not-found.segments/_full.segment.rsc +13 -13
  16. package/.next/standalone/.next/server/app/_not-found.segments/_not-found/__PAGE__.segment.rsc +12 -12
  17. package/.next/standalone/.next/server/app/_not-found.segments/_tree.segment.rsc +2 -2
  18. package/.next/standalone/.next/server/app/api/admin-delete/route.js +1 -1
  19. package/.next/standalone/.next/server/app/api/admin-delete/route.js.nft.json +1 -1
  20. package/.next/standalone/.next/server/app/api/admin-update/route.js +1 -1
  21. package/.next/standalone/.next/server/app/api/admin-update/route.js.nft.json +1 -1
  22. package/.next/standalone/.next/server/app/api/bridges/route.js +1 -1
  23. package/.next/standalone/.next/server/app/api/bridges/route.js.nft.json +1 -1
  24. package/.next/standalone/.next/server/app/api/bulk-import/route.js +1 -1
  25. package/.next/standalone/.next/server/app/api/bulk-import/route.js.nft.json +1 -1
  26. package/.next/standalone/.next/server/app/api/company-scores/legacy/route.js +1 -1
  27. package/.next/standalone/.next/server/app/api/company-scores/legacy/route.js.nft.json +1 -1
  28. package/.next/standalone/.next/server/app/api/company-scores/route.js +1 -1
  29. package/.next/standalone/.next/server/app/api/company-scores/route.js.nft.json +1 -1
  30. package/.next/standalone/.next/server/app/api/connections/route.js +1 -1
  31. package/.next/standalone/.next/server/app/api/connections/route.js.nft.json +1 -1
  32. package/.next/standalone/.next/server/app/api/data/export/route.js.nft.json +1 -1
  33. package/.next/standalone/.next/server/app/api/data/import/route.js.nft.json +1 -1
  34. package/.next/standalone/.next/server/app/api/data/restart/route.js.nft.json +1 -1
  35. package/.next/standalone/.next/server/app/api/data/reveal/route.js.nft.json +1 -1
  36. package/.next/standalone/.next/server/app/api/data/route.js.nft.json +1 -1
  37. package/.next/standalone/.next/server/app/api/delete-cluster/route.js +1 -1
  38. package/.next/standalone/.next/server/app/api/delete-cluster/route.js.nft.json +1 -1
  39. package/.next/standalone/.next/server/app/api/ingest/route.js +1 -1
  40. package/.next/standalone/.next/server/app/api/ingest/route.js.nft.json +1 -1
  41. package/.next/standalone/.next/server/app/api/network/route.js +1 -1
  42. package/.next/standalone/.next/server/app/api/network/route.js.nft.json +1 -1
  43. package/.next/standalone/.next/server/app/api/notifications/route.js +1 -1
  44. package/.next/standalone/.next/server/app/api/notifications/route.js.nft.json +1 -1
  45. package/.next/standalone/.next/server/app/api/outreach/route.js +1 -1
  46. package/.next/standalone/.next/server/app/api/outreach/route.js.nft.json +1 -1
  47. package/.next/standalone/.next/server/app/api/queue/route.js +1 -1
  48. package/.next/standalone/.next/server/app/api/queue/route.js.nft.json +1 -1
  49. package/.next/standalone/.next/server/app/api/scraper/route.js +3 -2
  50. package/.next/standalone/.next/server/app/api/scraper/route.js.nft.json +1 -1
  51. package/.next/standalone/.next/server/app/api/settings/route.js +1 -1
  52. package/.next/standalone/.next/server/app/api/settings/route.js.nft.json +1 -1
  53. package/.next/standalone/.next/server/app/api/settings/sector-preview/route.js +1 -1
  54. package/.next/standalone/.next/server/app/api/settings/sector-preview/route.js.nft.json +1 -1
  55. package/.next/standalone/.next/server/app/api/settings/sector-suggestions/route.js +1 -1
  56. package/.next/standalone/.next/server/app/api/settings/sector-suggestions/route.js.nft.json +1 -1
  57. package/.next/standalone/.next/server/app/api/setup-profile/route.js +1 -1
  58. package/.next/standalone/.next/server/app/api/setup-profile/route.js.nft.json +1 -1
  59. package/.next/standalone/.next/server/app/api/social/crm/route/app-paths-manifest.json +3 -0
  60. package/.next/standalone/.next/server/app/api/social/crm/route/build-manifest.json +12 -0
  61. package/.next/standalone/.next/server/app/api/social/crm/route/server-reference-manifest.json +4 -0
  62. package/.next/standalone/.next/server/app/api/social/crm/route.js +10 -0
  63. package/.next/standalone/.next/server/app/api/social/crm/route.js.map +5 -0
  64. package/.next/standalone/.next/server/app/api/social/crm/route.js.nft.json +1 -0
  65. package/.next/standalone/.next/server/app/api/social/crm/route_client-reference-manifest.js +3 -0
  66. package/.next/standalone/.next/server/app/api/social/messages/route.js +4 -3
  67. package/.next/standalone/.next/server/app/api/social/messages/route.js.nft.json +1 -1
  68. package/.next/standalone/.next/server/app/api/social/route.js +4 -3
  69. package/.next/standalone/.next/server/app/api/social/route.js.nft.json +1 -1
  70. package/.next/standalone/.next/server/app/api/unlock/route.js +1 -1
  71. package/.next/standalone/.next/server/app/api/unlock/route.js.nft.json +1 -1
  72. package/.next/standalone/.next/server/app/api/update/route.js.nft.json +1 -1
  73. package/.next/standalone/.next/server/app/api/update-images/route.js +1 -1
  74. package/.next/standalone/.next/server/app/api/update-images/route.js.nft.json +1 -1
  75. package/.next/standalone/.next/server/app/api/users/route.js +1 -1
  76. package/.next/standalone/.next/server/app/api/users/route.js.nft.json +1 -1
  77. package/.next/standalone/.next/server/app/avatars/[file]/route.js.nft.json +1 -1
  78. package/.next/standalone/.next/server/app/import/page.js.nft.json +1 -1
  79. package/.next/standalone/.next/server/app/import/page_client-reference-manifest.js +1 -1
  80. package/.next/standalone/.next/server/app/import.html +1 -1
  81. package/.next/standalone/.next/server/app/import.rsc +15 -15
  82. package/.next/standalone/.next/server/app/import.segments/_full.segment.rsc +15 -15
  83. package/.next/standalone/.next/server/app/import.segments/_tree.segment.rsc +2 -2
  84. package/.next/standalone/.next/server/app/import.segments/import/__PAGE__.segment.rsc +14 -14
  85. package/.next/standalone/.next/server/app/index.html +1 -1
  86. package/.next/standalone/.next/server/app/index.rsc +15 -15
  87. package/.next/standalone/.next/server/app/index.segments/__PAGE__.segment.rsc +14 -14
  88. package/.next/standalone/.next/server/app/index.segments/_full.segment.rsc +15 -15
  89. package/.next/standalone/.next/server/app/index.segments/_tree.segment.rsc +2 -2
  90. package/.next/standalone/.next/server/app/launch/page.js.nft.json +1 -1
  91. package/.next/standalone/.next/server/app/launch/page_client-reference-manifest.js +1 -1
  92. package/.next/standalone/.next/server/app/launch.html +2 -2
  93. package/.next/standalone/.next/server/app/launch.rsc +15 -15
  94. package/.next/standalone/.next/server/app/launch.segments/_full.segment.rsc +15 -15
  95. package/.next/standalone/.next/server/app/launch.segments/_tree.segment.rsc +2 -2
  96. package/.next/standalone/.next/server/app/launch.segments/launch/__PAGE__.segment.rsc +14 -14
  97. package/.next/standalone/.next/server/app/page.js.nft.json +1 -1
  98. package/.next/standalone/.next/server/app/page_client-reference-manifest.js +1 -1
  99. package/.next/standalone/.next/server/app/paths/page.js.nft.json +1 -1
  100. package/.next/standalone/.next/server/app/paths/page_client-reference-manifest.js +1 -1
  101. package/.next/standalone/.next/server/app/paths.html +1 -1
  102. package/.next/standalone/.next/server/app/paths.rsc +15 -15
  103. package/.next/standalone/.next/server/app/paths.segments/_full.segment.rsc +15 -15
  104. package/.next/standalone/.next/server/app/paths.segments/_tree.segment.rsc +2 -2
  105. package/.next/standalone/.next/server/app/paths.segments/paths/__PAGE__.segment.rsc +14 -14
  106. package/.next/standalone/.next/server/app/profile/page.js.nft.json +1 -1
  107. package/.next/standalone/.next/server/app/profile/page_client-reference-manifest.js +1 -1
  108. package/.next/standalone/.next/server/app/profile.html +1 -1
  109. package/.next/standalone/.next/server/app/profile.rsc +15 -15
  110. package/.next/standalone/.next/server/app/profile.segments/_full.segment.rsc +15 -15
  111. package/.next/standalone/.next/server/app/profile.segments/_tree.segment.rsc +2 -2
  112. package/.next/standalone/.next/server/app/profile.segments/profile/__PAGE__.segment.rsc +14 -14
  113. package/.next/standalone/.next/server/app/queue/page.js.nft.json +1 -1
  114. package/.next/standalone/.next/server/app/queue/page_client-reference-manifest.js +1 -1
  115. package/.next/standalone/.next/server/app/queue.html +1 -1
  116. package/.next/standalone/.next/server/app/queue.rsc +15 -15
  117. package/.next/standalone/.next/server/app/queue.segments/_full.segment.rsc +15 -15
  118. package/.next/standalone/.next/server/app/queue.segments/_tree.segment.rsc +2 -2
  119. package/.next/standalone/.next/server/app/queue.segments/queue/__PAGE__.segment.rsc +14 -14
  120. package/.next/standalone/.next/server/app/scores/page.js.nft.json +1 -1
  121. package/.next/standalone/.next/server/app/scores/page_client-reference-manifest.js +1 -1
  122. package/.next/standalone/.next/server/app/scores.html +1 -1
  123. package/.next/standalone/.next/server/app/scores.rsc +15 -15
  124. package/.next/standalone/.next/server/app/scores.segments/_full.segment.rsc +15 -15
  125. package/.next/standalone/.next/server/app/scores.segments/_tree.segment.rsc +2 -2
  126. package/.next/standalone/.next/server/app/scores.segments/scores/__PAGE__.segment.rsc +14 -14
  127. package/.next/standalone/.next/server/app/settings/page.js.nft.json +1 -1
  128. package/.next/standalone/.next/server/app/settings/page_client-reference-manifest.js +1 -1
  129. package/.next/standalone/.next/server/app/settings.html +1 -1
  130. package/.next/standalone/.next/server/app/settings.rsc +15 -15
  131. package/.next/standalone/.next/server/app/settings.segments/_full.segment.rsc +15 -15
  132. package/.next/standalone/.next/server/app/settings.segments/_tree.segment.rsc +2 -2
  133. package/.next/standalone/.next/server/app/settings.segments/settings/__PAGE__.segment.rsc +14 -14
  134. package/.next/standalone/.next/server/app/setup/page.js.nft.json +1 -1
  135. package/.next/standalone/.next/server/app/setup/page_client-reference-manifest.js +1 -1
  136. package/.next/standalone/.next/server/app/setup.html +1 -1
  137. package/.next/standalone/.next/server/app/setup.rsc +15 -15
  138. package/.next/standalone/.next/server/app/setup.segments/_full.segment.rsc +15 -15
  139. package/.next/standalone/.next/server/app/setup.segments/_tree.segment.rsc +2 -2
  140. package/.next/standalone/.next/server/app/setup.segments/setup/__PAGE__.segment.rsc +14 -14
  141. package/.next/standalone/.next/server/app/social/page.js.nft.json +1 -1
  142. package/.next/standalone/.next/server/app/social/page_client-reference-manifest.js +1 -1
  143. package/.next/standalone/.next/server/app/social.html +1 -1
  144. package/.next/standalone/.next/server/app/social.rsc +15 -15
  145. package/.next/standalone/.next/server/app/social.segments/_full.segment.rsc +15 -15
  146. package/.next/standalone/.next/server/app/social.segments/_tree.segment.rsc +2 -2
  147. package/.next/standalone/.next/server/app/social.segments/social/__PAGE__.segment.rsc +14 -14
  148. package/.next/standalone/.next/server/app-paths-manifest.json +1 -0
  149. package/.next/standalone/.next/server/chunks/{[root-of-the-server]__0bat8nx._.js → [root-of-the-server]__05_3-22._.js} +2 -2
  150. package/.next/standalone/.next/server/chunks/[root-of-the-server]__0t3v1a1._.js +1 -1
  151. package/.next/standalone/.next/server/chunks/{[root-of-the-server]__06mi9k2._.js → [root-of-the-server]__13dx4sd._.js} +7 -2
  152. package/.next/standalone/.next/server/chunks/[root-of-the-server]__15wc9e-._.js +3 -0
  153. package/.next/standalone/.next/server/chunks/[root-of-the-server]__1kbl21f._.js +1 -1
  154. package/.next/standalone/.next/server/chunks/[root-of-the-server]__1tir0vc._.js +8 -0
  155. package/.next/standalone/.next/server/chunks/[root-of-the-server]__20rwfdw._.js +8 -0
  156. package/.next/standalone/.next/server/chunks/_072jl0y._.js +1 -1
  157. package/.next/standalone/.next/server/chunks/_0ea9qwy._.js +1 -1
  158. package/.next/standalone/.next/server/chunks/_1_fynia._.js +7 -7
  159. package/.next/standalone/.next/server/chunks/_1itd1o_._.js +1 -1
  160. package/.next/standalone/.next/server/chunks/_1iwkh8t._.js +1 -1
  161. package/.next/standalone/.next/server/chunks/_1oqfuh8._.js +3 -0
  162. package/.next/standalone/.next/server/chunks/_1xh07bp._.js +1 -1
  163. package/.next/standalone/.next/server/chunks/_next-internal_server_app_api_social_crm_route_actions_0d1aflv.js +3 -0
  164. package/.next/standalone/.next/server/chunks/lib_0851sk0._.js +8 -0
  165. package/.next/standalone/.next/server/chunks/lib_1q7p2ro._.js +8 -0
  166. package/.next/standalone/.next/server/chunks/lib_1r6l86l._.js +8 -0
  167. package/.next/standalone/.next/server/chunks/lib_db-client_15le7cw.js +8 -8
  168. package/.next/standalone/.next/server/chunks/lib_linkedin-export_12imqxg.js +3 -0
  169. package/.next/standalone/.next/server/chunks/lib_rpc_04s8mzr.js +3 -0
  170. package/.next/standalone/.next/server/chunks/lib_rpc_053z7as.js +3 -0
  171. package/.next/standalone/.next/server/chunks/lib_rpc_0b_-2te.js +3 -0
  172. package/.next/standalone/.next/server/chunks/lib_rpc_0dhbxdm.js +3 -0
  173. package/.next/standalone/.next/server/chunks/lib_rpc_0dxlk31.js +3 -0
  174. package/.next/standalone/.next/server/chunks/lib_rpc_0f8b-qc.js +3 -0
  175. package/.next/standalone/.next/server/chunks/lib_rpc_0pzie7_.js +3 -0
  176. package/.next/standalone/.next/server/chunks/lib_rpc_0r76fq_.js +3 -0
  177. package/.next/standalone/.next/server/chunks/lib_rpc_0ut7pjp.js +3 -0
  178. package/.next/standalone/.next/server/chunks/lib_rpc_15zyqaz.js +3 -0
  179. package/.next/standalone/.next/server/chunks/lib_rpc_18zpjtq.js +3 -0
  180. package/.next/standalone/.next/server/chunks/lib_rpc_1a8xkiz.js +3 -0
  181. package/.next/standalone/.next/server/chunks/lib_rpc_1e_55xo.js +3 -0
  182. package/.next/standalone/.next/server/chunks/lib_rpc_1oxfd1_.js +3 -0
  183. package/.next/standalone/.next/server/chunks/lib_rpc_1s0lod0.js +3 -0
  184. package/.next/standalone/.next/server/chunks/lib_scoring_1-e4ur0.js +1 -1
  185. package/.next/standalone/.next/server/chunks/ssr/_01kacen._.js +3 -0
  186. package/.next/standalone/.next/server/chunks/ssr/_03l7vge._.js +3 -0
  187. package/.next/standalone/.next/server/chunks/ssr/{_1ap1tu_._.js → _0gtv9jb._.js} +2 -2
  188. package/.next/standalone/.next/server/chunks/ssr/_0jdkqgi._.js +3 -0
  189. package/.next/standalone/.next/server/chunks/ssr/_0xi-fbs._.js +3 -0
  190. package/.next/standalone/.next/server/chunks/ssr/_0xmwmfv._.js +3 -0
  191. package/.next/standalone/.next/server/chunks/ssr/_1-b9hj7._.js +9 -0
  192. package/.next/standalone/.next/server/chunks/ssr/_112ewem._.js +3 -0
  193. package/.next/standalone/.next/server/chunks/ssr/_16ab63q._.js +3 -0
  194. package/.next/standalone/.next/server/chunks/ssr/_1fxm0-s._.js +3 -0
  195. package/.next/standalone/.next/server/chunks/ssr/_1ra1d3r._.js +9 -0
  196. package/.next/standalone/.next/server/chunks/ssr/app_141w189._.js +3 -0
  197. package/.next/standalone/.next/server/chunks/ssr/app_page_1fazqbf.js +22 -10
  198. package/.next/standalone/.next/server/chunks/ssr/app_paths_page_1ol90m7.js +2 -2
  199. package/.next/standalone/.next/server/chunks/ssr/app_profile_page_0u3-l5y.js +1 -1
  200. package/.next/standalone/.next/server/chunks/ssr/app_queue_page_0c6znd6.js +6 -5
  201. package/.next/standalone/.next/server/chunks/ssr/app_settings_page_1f8mjj_.js +1 -1
  202. package/.next/standalone/.next/server/chunks/ssr/app_setup_page_1-282zx.js +19 -3
  203. package/.next/standalone/.next/server/chunks/ssr/app_social_page_0h7zxda.js +1 -1
  204. package/.next/standalone/.next/server/chunks/ssr/lib_0i8v-s3._.js +1 -1
  205. package/.next/standalone/.next/server/chunks/ssr/lib_13rapwj._.js +3 -0
  206. package/.next/standalone/.next/server/chunks/ssr/lib_galaxy-lab_108_lu-.js +3 -0
  207. package/.next/standalone/.next/server/middleware-build-manifest.js +3 -3
  208. package/.next/standalone/.next/server/middleware-manifest.json +5 -5
  209. package/.next/standalone/.next/server/pages/404.html +1 -1
  210. package/.next/standalone/.next/server/pages/500.html +1 -1
  211. package/.next/standalone/.next/server/server-reference-manifest.js +1 -1
  212. package/.next/standalone/.next/server/server-reference-manifest.json +1 -1
  213. package/.next/standalone/.next/static/chunks/03bziijw5srol.js +1 -0
  214. package/.next/standalone/.next/static/chunks/0bgq8wmopl_ig.js +7 -0
  215. package/.next/standalone/.next/static/chunks/0ezus6p2eb-vg.js +1 -0
  216. package/.next/standalone/.next/static/chunks/0g_fy2034bp0x.js +34 -0
  217. package/.next/standalone/.next/static/chunks/0q5dru4q6gxl5.js +22 -0
  218. package/.next/standalone/.next/static/chunks/0w-byiwb2zax8.js +2 -0
  219. package/.next/standalone/.next/static/chunks/1g7jcovdejc9-.js +1 -0
  220. package/.next/standalone/.next/static/chunks/1i51yylxdzht1.js +1 -0
  221. package/.next/{static/chunks/1xptlvtcz6654.css → standalone/.next/static/chunks/1x_ysly42-teq.css} +1 -1
  222. package/.next/standalone/.next/static/chunks/1zf8swaasjf1e.js +43 -0
  223. package/.next/standalone/.next/static/chunks/2rqtpccruavf2.js +1 -0
  224. package/.next/standalone/.next/static/chunks/2wgz9ietkqqe6.js +1 -0
  225. package/.next/standalone/.next/static/chunks/30x3oq_st7a5r.js +1 -0
  226. package/.next/standalone/.next/static/chunks/355ryio1b7sm1.js +1 -0
  227. package/.next/standalone/.next/static/chunks/3ap5hgma7n0o7.js +1 -0
  228. package/.next/standalone/.next/static/chunks/3fci6qi39-1n5.js +1 -0
  229. package/.next/standalone/.next/static/chunks/{24tvdun1cb3g1.js → 3sowbvbaj1s6n.js} +1 -1
  230. package/.next/standalone/CHANGELOG.md +235 -0
  231. package/.next/standalone/app/api/data/export/route.js +2 -1
  232. package/.next/standalone/app/api/ingest/route.js +40 -1
  233. package/.next/standalone/app/api/scraper/route.js +12 -2
  234. package/.next/standalone/app/api/settings/sector-preview/route.js +2 -1
  235. package/.next/standalone/app/api/social/crm/route.js +37 -0
  236. package/.next/standalone/app/api/social/messages/route.js +44 -8
  237. package/.next/standalone/app/api/social/route.js +80 -22
  238. package/.next/standalone/app/components/AppTabs.js +59 -0
  239. package/.next/standalone/app/components/ChainView.js +303 -89
  240. package/.next/standalone/app/components/CirclePanel.js +162 -0
  241. package/.next/standalone/app/components/CompanyScores.js +2 -2
  242. package/.next/standalone/app/components/FieldStep.js +4 -4
  243. package/.next/standalone/app/components/FilterPanel.js +121 -183
  244. package/.next/standalone/app/components/ForceGraph.js +41 -3
  245. package/.next/standalone/app/components/GalaxyLab.js +26 -31
  246. package/.next/standalone/app/components/LegacyScoresCard.js +2 -2
  247. package/.next/standalone/app/components/MapControls.js +154 -0
  248. package/.next/standalone/app/components/NoteDetail.js +118 -0
  249. package/.next/standalone/app/components/PathsAnalyzer.js +147 -110
  250. package/.next/standalone/app/components/PeopleMap.js +308 -0
  251. package/.next/standalone/app/components/ScanRadar.js +129 -0
  252. package/.next/standalone/app/components/ScanStatusBar.js +72 -27
  253. package/.next/standalone/app/components/SeparationView.js +626 -118
  254. package/.next/standalone/app/components/Sidebar.js +28 -2
  255. package/.next/standalone/app/components/settings/DataSection.js +12 -3
  256. package/.next/standalone/app/components/settings/NetworkHealthSection.js +1 -1
  257. package/.next/standalone/app/components/settings/SectorSection.js +2 -2
  258. package/.next/standalone/app/components/settings/TierSection.js +1 -1
  259. package/.next/standalone/app/components/settings/TitleSection.js +149 -0
  260. package/.next/standalone/app/components/views.js +15 -14
  261. package/.next/standalone/app/globals.css +11 -0
  262. package/.next/standalone/app/page.js +89 -156
  263. package/.next/standalone/app/paths/page.js +39 -27
  264. package/.next/standalone/app/profile/page.js +33 -5
  265. package/.next/standalone/app/queue/page.js +56 -34
  266. package/.next/standalone/app/scores/page.js +16 -66
  267. package/.next/standalone/app/settings/page.js +27 -13
  268. package/.next/standalone/app/setup/page.js +298 -205
  269. package/.next/standalone/app/social/Crm.js +683 -0
  270. package/.next/standalone/app/social/SocialHub.js +374 -0
  271. package/.next/standalone/app/social/Thread.js +90 -0
  272. package/.next/standalone/app/social/page.js +10 -351
  273. package/.next/standalone/lib/chain-layout.js +105 -35
  274. package/.next/standalone/lib/data-export.js +10 -4
  275. package/.next/standalone/lib/data-folder.js +9 -0
  276. package/.next/standalone/lib/data-import.js +13 -3
  277. package/.next/standalone/lib/degrees.js +0 -6
  278. package/.next/standalone/lib/dot-rings.js +25 -0
  279. package/.next/standalone/lib/galaxy-lab.js +65 -6
  280. package/.next/standalone/lib/ingest.js +2 -0
  281. package/.next/standalone/lib/island.js +40 -0
  282. package/.next/standalone/lib/linkedin-export.js +267 -46
  283. package/.next/standalone/lib/linkedin-limits.js +6 -2
  284. package/.next/standalone/lib/map-heat.js +20 -0
  285. package/.next/standalone/lib/network-health.js +1 -1
  286. package/.next/standalone/lib/notifications.js +121 -0
  287. package/.next/standalone/lib/people-map.js +82 -0
  288. package/.next/standalone/lib/rarity.js +45 -3
  289. package/.next/standalone/lib/reach.js +7 -6
  290. package/.next/standalone/lib/rpc.js +15 -4
  291. package/.next/standalone/lib/scan-pace.js +46 -0
  292. package/.next/standalone/lib/scoring.js +39 -8
  293. package/.next/standalone/lib/scraper-client.js +19 -6
  294. package/.next/standalone/lib/sector-focus.js +4 -4
  295. package/.next/standalone/lib/separation.js +137 -10
  296. package/.next/standalone/lib/settings-effects.js +20 -11
  297. package/.next/standalone/lib/settings.js +3 -0
  298. package/.next/standalone/lib/social-crm.js +415 -0
  299. package/.next/standalone/lib/social-store.js +13 -6
  300. package/.next/standalone/lib/tier-grid.js +99 -0
  301. package/.next/standalone/lib/title-ranking.js +83 -0
  302. package/.next/standalone/package-lock.json +2 -2
  303. package/.next/standalone/package.json +1 -1
  304. package/.next/standalone/scripts/scrape.py +177 -35
  305. package/.next/static/chunks/03bziijw5srol.js +1 -0
  306. package/.next/static/chunks/0bgq8wmopl_ig.js +7 -0
  307. package/.next/static/chunks/0ezus6p2eb-vg.js +1 -0
  308. package/.next/static/chunks/0g_fy2034bp0x.js +34 -0
  309. package/.next/static/chunks/0q5dru4q6gxl5.js +22 -0
  310. package/.next/static/chunks/0w-byiwb2zax8.js +2 -0
  311. package/.next/static/chunks/1g7jcovdejc9-.js +1 -0
  312. package/.next/static/chunks/1i51yylxdzht1.js +1 -0
  313. package/.next/{standalone/.next/static/chunks/1xptlvtcz6654.css → static/chunks/1x_ysly42-teq.css} +1 -1
  314. package/.next/static/chunks/1zf8swaasjf1e.js +43 -0
  315. package/.next/static/chunks/2rqtpccruavf2.js +1 -0
  316. package/.next/static/chunks/2wgz9ietkqqe6.js +1 -0
  317. package/.next/static/chunks/30x3oq_st7a5r.js +1 -0
  318. package/.next/static/chunks/355ryio1b7sm1.js +1 -0
  319. package/.next/static/chunks/3ap5hgma7n0o7.js +1 -0
  320. package/.next/static/chunks/3fci6qi39-1n5.js +1 -0
  321. package/.next/static/chunks/{24tvdun1cb3g1.js → 3sowbvbaj1s6n.js} +1 -1
  322. package/CHANGELOG.md +235 -0
  323. package/lib/chain-layout.js +105 -35
  324. package/lib/data-export.js +10 -4
  325. package/lib/data-folder.js +9 -0
  326. package/lib/data-import.js +13 -3
  327. package/lib/degrees.js +0 -6
  328. package/lib/dot-rings.js +25 -0
  329. package/lib/galaxy-lab.js +65 -6
  330. package/lib/ingest.js +2 -0
  331. package/lib/island.js +40 -0
  332. package/lib/linkedin-export.js +267 -46
  333. package/lib/linkedin-limits.js +6 -2
  334. package/lib/map-heat.js +20 -0
  335. package/lib/network-health.js +1 -1
  336. package/lib/notifications.js +121 -0
  337. package/lib/people-map.js +82 -0
  338. package/lib/rarity.js +45 -3
  339. package/lib/reach.js +7 -6
  340. package/lib/rpc.js +15 -4
  341. package/lib/scan-pace.js +46 -0
  342. package/lib/scoring.js +39 -8
  343. package/lib/scraper-client.js +19 -6
  344. package/lib/sector-focus.js +4 -4
  345. package/lib/separation.js +137 -10
  346. package/lib/settings-effects.js +20 -11
  347. package/lib/settings.js +3 -0
  348. package/lib/social-crm.js +415 -0
  349. package/lib/social-store.js +13 -6
  350. package/lib/tier-grid.js +99 -0
  351. package/lib/title-ranking.js +83 -0
  352. package/package.json +1 -1
  353. package/scripts/scrape.py +177 -35
  354. package/.next/standalone/.next/server/chunks/[root-of-the-server]__08f5awg._.js +0 -8
  355. package/.next/standalone/.next/server/chunks/[root-of-the-server]__17hjtzl._.js +0 -8
  356. package/.next/standalone/.next/server/chunks/[root-of-the-server]__20tmem7._.js +0 -3
  357. package/.next/standalone/.next/server/chunks/lib_18cr1gi._.js +0 -8
  358. package/.next/standalone/.next/server/chunks/lib_rpc_03i8xef.js +0 -3
  359. package/.next/standalone/.next/server/chunks/lib_rpc_08fc3rp.js +0 -3
  360. package/.next/standalone/.next/server/chunks/lib_rpc_08r2t_q.js +0 -3
  361. package/.next/standalone/.next/server/chunks/lib_rpc_0btpv1t.js +0 -3
  362. package/.next/standalone/.next/server/chunks/lib_rpc_0c9d9ti.js +0 -8
  363. package/.next/standalone/.next/server/chunks/lib_rpc_0cm2ev3.js +0 -3
  364. package/.next/standalone/.next/server/chunks/lib_rpc_0iovswz.js +0 -3
  365. package/.next/standalone/.next/server/chunks/lib_rpc_0is8flp.js +0 -3
  366. package/.next/standalone/.next/server/chunks/lib_rpc_0v8ajtd.js +0 -3
  367. package/.next/standalone/.next/server/chunks/lib_rpc_0xgmvvt.js +0 -3
  368. package/.next/standalone/.next/server/chunks/lib_rpc_144mzbf.js +0 -3
  369. package/.next/standalone/.next/server/chunks/lib_rpc_1bggi0n.js +0 -3
  370. package/.next/standalone/.next/server/chunks/lib_rpc_1dkcu9f.js +0 -3
  371. package/.next/standalone/.next/server/chunks/lib_rpc_1o0jb6w.js +0 -3
  372. package/.next/standalone/.next/server/chunks/ssr/_0c_y09g._.js +0 -3
  373. package/.next/standalone/.next/server/chunks/ssr/_0ezj4e0._.js +0 -3
  374. package/.next/standalone/.next/server/chunks/ssr/_0j4-lc3._.js +0 -3
  375. package/.next/standalone/.next/server/chunks/ssr/_0l8cxmo._.js +0 -3
  376. package/.next/standalone/.next/server/chunks/ssr/_0xkur4p._.js +0 -3
  377. package/.next/standalone/.next/server/chunks/ssr/_14iv_zy._.js +0 -3
  378. package/.next/standalone/.next/server/chunks/ssr/_15x6q32._.js +0 -3
  379. package/.next/standalone/.next/server/chunks/ssr/_1errbuf._.js +0 -3
  380. package/.next/standalone/.next/server/chunks/ssr/_212xw8q._.js +0 -3
  381. package/.next/standalone/.next/server/chunks/ssr/app_components_19c2d9c._.js +0 -3
  382. package/.next/standalone/.next/server/chunks/ssr/app_scores_page_09wepvv.js +0 -3
  383. package/.next/standalone/.next/static/chunks/07dyt47mp4s9-.js +0 -1
  384. package/.next/standalone/.next/static/chunks/0a5iadnmt9u9k.js +0 -1
  385. package/.next/standalone/.next/static/chunks/160wjb73ddwqd.js +0 -6
  386. package/.next/standalone/.next/static/chunks/1c_n0973t07l0.js +0 -2
  387. package/.next/standalone/.next/static/chunks/1e1n0kkz-psdb.js +0 -1
  388. package/.next/standalone/.next/static/chunks/206rm6j42gl_a.js +0 -1
  389. package/.next/standalone/.next/static/chunks/236f0jtp68j_h.js +0 -1
  390. package/.next/standalone/.next/static/chunks/2coxzjm_k6ilr.js +0 -1
  391. package/.next/standalone/.next/static/chunks/2kfhc58df2xyo.js +0 -1
  392. package/.next/standalone/.next/static/chunks/2pw6_osz1n_te.js +0 -1
  393. package/.next/standalone/.next/static/chunks/39hr8d-b2ibtw.js +0 -31
  394. package/.next/standalone/.next/static/chunks/3zw8bv7-mc4n6.js +0 -33
  395. package/.next/standalone/app/components/OrbitGraph.js +0 -737
  396. package/.next/standalone/app/social/Conversations.js +0 -246
  397. package/.next/static/chunks/07dyt47mp4s9-.js +0 -1
  398. package/.next/static/chunks/0a5iadnmt9u9k.js +0 -1
  399. package/.next/static/chunks/160wjb73ddwqd.js +0 -6
  400. package/.next/static/chunks/1c_n0973t07l0.js +0 -2
  401. package/.next/static/chunks/1e1n0kkz-psdb.js +0 -1
  402. package/.next/static/chunks/206rm6j42gl_a.js +0 -1
  403. package/.next/static/chunks/236f0jtp68j_h.js +0 -1
  404. package/.next/static/chunks/2coxzjm_k6ilr.js +0 -1
  405. package/.next/static/chunks/2kfhc58df2xyo.js +0 -1
  406. package/.next/static/chunks/2pw6_osz1n_te.js +0 -1
  407. package/.next/static/chunks/39hr8d-b2ibtw.js +0 -31
  408. package/.next/static/chunks/3zw8bv7-mc4n6.js +0 -33
  409. /package/.next/standalone/.next/static/{7zWJnSkfSoRhRsqY6ZKtD → I-cj79yE2IZ8taX2dZGp3}/_buildManifest.js +0 -0
  410. /package/.next/standalone/.next/static/{7zWJnSkfSoRhRsqY6ZKtD → I-cj79yE2IZ8taX2dZGp3}/_clientMiddlewareManifest.js +0 -0
  411. /package/.next/standalone/.next/static/{7zWJnSkfSoRhRsqY6ZKtD → I-cj79yE2IZ8taX2dZGp3}/_ssgManifest.js +0 -0
  412. /package/.next/static/{7zWJnSkfSoRhRsqY6ZKtD → I-cj79yE2IZ8taX2dZGp3}/_buildManifest.js +0 -0
  413. /package/.next/static/{7zWJnSkfSoRhRsqY6ZKtD → I-cj79yE2IZ8taX2dZGp3}/_clientMiddlewareManifest.js +0 -0
  414. /package/.next/static/{7zWJnSkfSoRhRsqY6ZKtD → I-cj79yE2IZ8taX2dZGp3}/_ssgManifest.js +0 -0
@@ -0,0 +1,82 @@
1
+ // Paths → People: the Paths map with people instead of companies (Blake,
2
+ // 2026-10-02: "basically going to look like map but people based and the
3
+ // bigger circle is their cluster with value"). One bubble per connection of
4
+ // yours, sized by the cluster behind them; a line joins two connections whose
5
+ // clusters share people.
6
+ //
7
+ // Plain functions over the rows the app already has, no React, so
8
+ // tests/people-map.test.mjs runs them as they are.
9
+
10
+ import { keyFor } from './separation.js';
11
+
12
+ /** What a cluster is worth: S tier counts 3, A tier 2, everyone else 1. */
13
+ export const clusterValue = ({ size = 0, S = 0, A = 0 } = {}) => size + 2 * S + A;
14
+
15
+ /**
16
+ * One entry per connection: { person, id, key, size, S, A, value, only, share }.
17
+ * `size` is the people in their cluster (each once), `only` how many of those
18
+ * none of your other connections' clusters hold, `share` = only / size.
19
+ * Also `links`: [{ a, b, shared }] for every two connections whose clusters
20
+ * share people, most shared first, and `rank(id)`: the share of your scanned
21
+ * connections whose cluster is worth less.
22
+ */
23
+ export function peopleMap(d1 = [], d2 = [], { maxLinks = 400 } = {}) {
24
+ const ids = new Set((d1 || []).map((c) => c.id));
25
+ // Each person once per connection whose scan found them.
26
+ const byConnection = new Map(); // id -> Map(key -> row)
27
+ const holders = new Map(); // key -> Set(connection id)
28
+ for (const row of d2 || []) {
29
+ const from = row?.source_connection_id;
30
+ if (from == null || !ids.has(from)) continue;
31
+ const key = keyFor(row);
32
+ let circle = byConnection.get(from);
33
+ if (!circle) byConnection.set(from, (circle = new Map()));
34
+ if (!circle.has(key)) circle.set(key, row);
35
+ let who = holders.get(key);
36
+ if (!who) holders.set(key, (who = new Set()));
37
+ who.add(from);
38
+ }
39
+
40
+ const people = (d1 || []).map((c) => {
41
+ const circle = byConnection.get(c.id);
42
+ let S = 0, A = 0, only = 0;
43
+ if (circle) {
44
+ for (const [key, row] of circle) {
45
+ if (row.tier === 'S') S++;
46
+ else if (row.tier === 'A') A++;
47
+ if (holders.get(key)?.size === 1) only++;
48
+ }
49
+ }
50
+ const size = circle ? circle.size : 0;
51
+ return { person: c, id: c.id, key: keyFor(c), size, S, A, value: clusterValue({ size, S, A }), only, share: size ? only / size : 0 };
52
+ });
53
+
54
+ // Two connections whose clusters share people. A person known to many of
55
+ // your connections would make a line between every two of them, so past
56
+ // eight holders they are left out of the lines (they still count in `only`).
57
+ const pairs = new Map();
58
+ for (const who of holders.values()) {
59
+ if (who.size < 2 || who.size > 8) continue;
60
+ const list = [...who].sort();
61
+ for (let i = 0; i < list.length; i++) {
62
+ for (let j = i + 1; j < list.length; j++) {
63
+ const k = `${list[i]}\u0000${list[j]}`;
64
+ pairs.set(k, (pairs.get(k) || 0) + 1);
65
+ }
66
+ }
67
+ }
68
+ const links = [...pairs].map(([k, shared]) => {
69
+ const [a, b] = k.split('\u0000');
70
+ return { a, b, shared };
71
+ }).sort((x, y) => y.shared - x.shared || (x.a < y.a ? -1 : 1)).slice(0, maxLinks);
72
+
73
+ const values = people.filter((p) => p.size > 0).map((p) => p.value).sort((x, y) => x - y);
74
+ const rank = (id) => {
75
+ const p = people.find((x) => x.id === id);
76
+ if (!p || !p.size || values.length < 2) return null;
77
+ let below = 0;
78
+ while (below < values.length && values[below] < p.value) below++;
79
+ return below / (values.length - 1);
80
+ };
81
+ return { people, links, rank };
82
+ }
@@ -39,12 +39,15 @@ export function rarityInfo(key) {
39
39
  /**
40
40
  * How many mutual connections you share with someone, and where that number
41
41
  * came from: { count, from: 'linkedin' | 'scans' }. `waysIn` is how many of
42
- * your scanned connections know them (lib/separation.js).
42
+ * your scanned connections know them (lib/separation.js): never fewer than that.
43
43
  */
44
44
  export function mutualsOf(row, waysIn = 1) {
45
45
  const saved = Number(row?.mutual_count);
46
- if (Number.isFinite(saved) && saved >= 1) return { count: Math.floor(saved), from: 'linkedin' };
47
- return { count: Math.max(1, Math.floor(Number(waysIn) || 1)), from: 'scans' };
46
+ const seen = Math.max(1, Math.floor(Number(waysIn) || 1));
47
+ // Everyone the app found them through is a mutual connection, so a saved
48
+ // count lower than that is out of date: "only way in" with two ways drawn.
49
+ if (Number.isFinite(saved) && saved >= seen) return { count: Math.floor(saved), from: 'linkedin' };
50
+ return { count: seen, from: 'scans' };
48
51
  }
49
52
 
50
53
  /** Someone's rarity: { key, count, from }. */
@@ -78,3 +81,42 @@ export function toggle(set, key) {
78
81
  else next.add(key);
79
82
  return next;
80
83
  }
84
+
85
+ // ── The Separation slider ──────────────────────────────────────────────────
86
+ // Blake, 2026-10-01: the rare person reachable through one door at one end,
87
+ // the easier, well-connected ones at the other, "so it's seamless"; 2026-10-02:
88
+ // "i want it to be slider ajusted and can be dynmaic". One number, 0 to 100:
89
+ // 0 puts the rarest ways in first, 100 the easiest, and 50 is each person's own
90
+ // score alone, as the list has always been. It reorders; it never changes a
91
+ // score, a tier or a rank. No odds are claimed: the app hasn't measured how
92
+ // often anyone accepts.
93
+
94
+ export const SLIDER_MIDDLE = 50;
95
+
96
+ /** How easy the way in is: 0 with one mutual connection, 1 with 31 or more, on a log scale (1→3 counts like 10→30). */
97
+ export function easeOf(count) {
98
+ const n = Math.max(1, Math.floor(Number(count) || 1));
99
+ return Math.min(1, Math.log(n) / Math.log(31));
100
+ }
101
+
102
+ /**
103
+ * Where someone sorts with the slider at `at`: their score, weighted by how
104
+ * well their way in fits the end the slider leans to. At 50 it is the score.
105
+ */
106
+ export function slideValue(score, count, at = SLIDER_MIDDLE) {
107
+ const lean = (Math.max(0, Math.min(100, Number(at) || 0)) - SLIDER_MIDDLE) / SLIDER_MIDDLE; // -1 rare … +1 easy
108
+ if (!lean) return score;
109
+ const ease = easeOf(count);
110
+ const pull = Math.abs(lean);
111
+ return score * (1 - pull + pull * (lean < 0 ? 1 - ease : ease));
112
+ }
113
+
114
+ /** What the slider is doing at `at`, in words: { name, detail }. */
115
+ export function slideLabel(at = SLIDER_MIDDLE) {
116
+ const v = Math.max(0, Math.min(100, Math.round(Number(at) || 0)));
117
+ if (v <= 10) return { name: 'Rarest first', detail: 'Strong people with one way in: the connection who knows them is the only door you have.' };
118
+ if (v < 45) return { name: 'Leaning rare', detail: 'Strong people with few mutual connections move up; the ones everybody knows move down.' };
119
+ if (v <= 55) return { name: 'By power', detail: 'Each person’s own score alone. How many of your connections know them doesn’t change the order.' };
120
+ if (v < 90) return { name: 'Leaning easy', detail: 'Strong people you share more mutual connections with move up: more people who could introduce you.' };
121
+ return { name: 'Easiest first', detail: 'Strong people you share the most mutual connections with: the warmest introductions. All the way right, the map aims at whoever you share the most with, with the ones in circles you haven’t scanned yet as grey dots.' };
122
+ }
@@ -20,6 +20,7 @@
20
20
  //
21
21
  // Plain functions, no React; tests/reach.test.mjs.
22
22
 
23
+ import { paceSeconds } from './scan-pace.js';
23
24
  import { keyFor, compareBridges } from './separation.js';
24
25
 
25
26
  /**
@@ -107,13 +108,13 @@ export function readyByCircle(reach) {
107
108
  * page, a minute more after every 10), so about 55 minutes for a whole list.
108
109
  * The Scan page and the Degrees panel both say it from here.
109
110
  */
110
- export function circleScanCost(pages = 100) {
111
+ export function circleScanCost(pages = 100, pace = 'fast') {
111
112
  const searches = Math.max(1, Math.min(100, Math.round(pages) || 100));
112
- return {
113
- profileViews: 1,
114
- searches,
115
- minutes: searches >= 100 ? 55 : Math.max(5, Math.round(searches * 0.55)),
116
- };
113
+ // At Fast, as it has always been said; slower speeds add their waits (lib/scan-pace.js).
114
+ const minutes = !pace || pace === 'fast'
115
+ ? (searches >= 100 ? 55 : Math.max(5, Math.round(searches * 0.55)))
116
+ : Math.max(5, Math.round(paceSeconds(pace, searches) / 60));
117
+ return { profileViews: 1, searches, minutes };
117
118
  }
118
119
 
119
120
  /**
@@ -7,6 +7,7 @@ import { getDb, newId, nowIso } from './db-client.js';
7
7
  // for the routes that call it; it rescores with lib/scoring.js like the rest.
8
8
 
9
9
  import { readNetwork, scoreNetwork, explainScore, SCORING_VERSION, KNOWN_COMPANIES, COMPANY_DATA, RULE_TABLES, TIER_SCALE_SETTING } from './scoring.js';
10
+ import { TITLE_RANKING_SETTING, isDefaultRanking, rankingFingerprint } from './title-ranking.js';
10
11
  import { industryKeyOf, INDUSTRIES } from './companies.js';
11
12
  import { readSettings } from './settings.js';
12
13
  import { SECTOR_FOCUS_SETTING, focusFingerprint, previewSectorFocus } from './sector-focus.js';
@@ -69,6 +70,12 @@ export function tierScaleOf(db = getDb()) {
69
70
  return readSettings(db, { tierScale: TIER_SCALE_SETTING }).tierScale;
70
71
  }
71
72
 
73
+ /** Your title ranking (Settings → Scores → Titles), or null when you haven't changed one. */
74
+ export function titleRankingOf(db = getDb()) {
75
+ const r = readSettings(db, { titleRanking: TITLE_RANKING_SETTING }).titleRanking;
76
+ return isDefaultRanking(r) ? null : r;
77
+ }
78
+
72
79
  /**
73
80
  * What rescoring reads: every row, as scoring needs it. `withPeople` adds each
74
81
  * row's name, for naming who a change would move.
@@ -111,12 +118,13 @@ export function rescoreAll({ compareWith } = {}) {
111
118
  const overrides = companyOverrides(db);
112
119
  const focus = sectorFocusOf(db);
113
120
  const tierScale = tierScaleOf(db);
121
+ const titles = titleRankingOf(db);
114
122
  // Every headline is read once, however many ways the rows are then scored.
115
123
  const read = readForScoring(rows);
116
- const { scores } = scoreNetwork(rows, { overrides, focus, tierScale, read });
124
+ const { scores } = scoreNetwork(rows, { overrides, focus, tierScale, titles, read });
117
125
  // Worked out before anything is written, so a failure leaves nothing half done.
118
126
  const change = compareWith === undefined ? null
119
- : previewSectorFocus(rows, { overrides, read, from: compareWith, to: focus, tierScale, examples: 0 });
127
+ : previewSectorFocus(rows, { overrides, read, from: compareWith, to: focus, tierScale, titles, examples: 0 });
120
128
  const update = db.prepare(`UPDATE linkedin_connections SET seniority_score = ?, company_prestige_score = ?, power_score = ?, tier = ?,
121
129
  score_why = ?, circle_power = ?, circle_s_count = ?, circle_a_count = ?, circle_elite_pct = ?, is_catalyst = ?, catalyst_score = ?, updated_at = ?
122
130
  WHERE id = ?`);
@@ -144,6 +152,7 @@ export function rescoreAll({ compareWith } = {}) {
144
152
  meta.run('scoring_list', KNOWN_LIST_STAMP);
145
153
  meta.run('scoring_focus', focusFingerprint(focus));
146
154
  meta.run('scoring_tiers', tierScale);
155
+ meta.run('scoring_titles', rankingFingerprint(titles || undefined));
147
156
  db.exec('COMMIT');
148
157
  } catch (err) {
149
158
  db.exec('ROLLBACK');
@@ -154,7 +163,7 @@ export function rescoreAll({ compareWith } = {}) {
154
163
 
155
164
  /**
156
165
  * Rescore once if the stored scores came from an older scoring model or
157
- * curated list, or from a sector focus or tier scale other than the one saved
166
+ * curated list, or from a sector focus, tier scale or title ranking other than the one saved
158
167
  * now: an update, a database restored or brought from another computer, or a
159
168
  * Settings save whose rescore never finished.
160
169
  */
@@ -164,7 +173,9 @@ export function rescoreIfStale() {
164
173
  const fresh = meta('scoring_version') === String(SCORING_VERSION)
165
174
  && meta('scoring_list') === KNOWN_LIST_STAMP
166
175
  && meta('scoring_focus') === focusFingerprint(sectorFocusOf(db))
167
- && meta('scoring_tiers') === tierScaleOf(db);
176
+ && meta('scoring_tiers') === tierScaleOf(db)
177
+ // Scores from before title rankings existed have no stamp, and none is the same as no ranking.
178
+ && (meta('scoring_titles') ?? 'none') === rankingFingerprint(titleRankingOf(db) || undefined);
168
179
  return fresh ? { scored: 0 } : rescoreAll();
169
180
  }
170
181
 
@@ -0,0 +1,46 @@
1
+ // The Scan page's speed: Fast, Medium or Slow (Blake, 2026-09-30: "a bar that
2
+ // will adjust the scanner from slow, medium and fast… the current is
3
+ // considered fast, and then we lower to medium and slow, bogging it down,
4
+ // making it less likely to get caught, and estimated time for each").
5
+ //
6
+ // Fast is today's pacing and nothing is faster; Medium and Slow only add
7
+ // waiting. Speed changes searches an hour, never searches a day: the daily
8
+ // budget is still the volume cap. scripts/scrape.py SCAN_PACES holds the same
9
+ // numbers (tests/scan-pace.test.mjs checks they agree). Fixed waits, never
10
+ // random "human" timing: the point is fewer searches an hour, not disguise.
11
+
12
+ export const PACES = {
13
+ fast: { label: 'Fast', pagePause: 20, chunkCooldown: 60, profileGap: 60 },
14
+ medium: { label: 'Medium', pagePause: 45, chunkCooldown: 180, profileGap: 90 },
15
+ slow: { label: 'Slow', pagePause: 90, chunkCooldown: 300, profileGap: 120 },
16
+ };
17
+ export const PACE_NAMES = ['slow', 'medium', 'fast'];
18
+ export const DEFAULT_PACE = 'fast';
19
+ // About how long reading one page of results takes, measured in 0.1.6.
20
+ export const WORK_SECONDS = 6;
21
+ const EVERY = 10; // the longer rest comes after every 10 pages (scrape.py SAVE_EVERY_PAGES)
22
+
23
+ export const paceOf = (name) => PACES[name] || PACES[DEFAULT_PACE];
24
+
25
+ /** Seconds to run `searches` searches at a speed: the work, the rest before each, the longer rest every 10. */
26
+ export function paceSeconds(name, searches) {
27
+ const p = paceOf(name);
28
+ const n = Math.max(0, Math.floor(Number(searches) || 0));
29
+ return n * (WORK_SECONDS + p.pagePause) + Math.floor(n / EVERY) * p.chunkCooldown;
30
+ }
31
+
32
+ /** About how many searches an hour a speed makes. */
33
+ export function searchesPerHour(name) {
34
+ const p = paceOf(name);
35
+ return Math.round(3600 / (WORK_SECONDS + p.pagePause + p.chunkCooldown / EVERY));
36
+ }
37
+
38
+ /** "about 1 h 7 min", "about 27 min", "under a minute" */
39
+ export function durationText(seconds) {
40
+ if (!(seconds >= 60)) return 'under a minute';
41
+ const m = Math.round(seconds / 60);
42
+ if (m < 60) return `about ${m} min`;
43
+ const h = Math.floor(m / 60);
44
+ const rest = m % 60;
45
+ return `about ${h} h${rest ? ` ${rest} min` : ''}`;
46
+ }
@@ -2512,18 +2512,47 @@ export function readPerson(row) {
2512
2512
  * points whatever else they list. `companyFor(name)` returns companyScore();
2513
2513
  * `read` is the row's readPerson(), when it has been read already.
2514
2514
  */
2515
- /** A role's title points as they count: 70% for a former role, capped for a student. */
2516
- function rolePoints(role, student) {
2517
- const points = role.title.points * (role.former ? FORMER_WEIGHT : 1);
2515
+ /** Does this title text have the words? Whole words only: "vc" isn't in "dvc", "cto" isn't in "doctor". */
2516
+ const titlePatterns = new Map();
2517
+ export function titleHas(text, words) {
2518
+ let re = titlePatterns.get(words);
2519
+ if (!re) {
2520
+ const esc = words.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
2521
+ re = new RegExp(`(^|[^a-z0-9])${esc}($|[^a-z0-9])`);
2522
+ if (titlePatterns.size > 500) titlePatterns.clear();
2523
+ titlePatterns.set(words, re);
2524
+ }
2525
+ return re.test(String(text || '').toLowerCase());
2526
+ }
2527
+
2528
+ /**
2529
+ * A title's points under your title ranking (Settings → Scores → Titles,
2530
+ * lib/title-ranking.js): a role whose words are in it counts what you gave
2531
+ * that role (the highest, if several match), else the level counts your
2532
+ * points for it, else the model's. No ranking: the model's.
2533
+ */
2534
+ export function rankedPoints(role, titles) {
2535
+ if (!titles) return role.title.points;
2536
+ let match = null;
2537
+ for (const r of titles.roles || []) {
2538
+ if (titleHas(role.part, r.text) && (match == null || r.points > match)) match = r.points;
2539
+ }
2540
+ if (match != null) return match;
2541
+ return titles.points?.[role.title.key] ?? role.title.points;
2542
+ }
2543
+
2544
+ /** A role's title points as they count: yours where you ranked them, 70% for a former role, capped for a student. */
2545
+ function rolePoints(role, student, titles) {
2546
+ const points = rankedPoints(role, titles) * (role.former ? FORMER_WEIGHT : 1);
2518
2547
  return student ? Math.min(points, STUDENT_CAP) : points;
2519
2548
  }
2520
2549
 
2521
- export function scorePerson(row, companyFor = (name) => companyScore(name), read = readPerson(row)) {
2550
+ export function scorePerson(row, companyFor = (name) => companyScore(name), read = readPerson(row), titles = null) {
2522
2551
  const { roles, student, reach } = read;
2523
2552
  let best = null;
2524
2553
  for (const role of roles) {
2525
2554
  const co = companyFor(role.company);
2526
- const points = rolePoints(role, student);
2555
+ const points = rolePoints(role, student, titles);
2527
2556
  const core = points * (0.45 + 0.055 * co.score);
2528
2557
  if (!best || core > best.core) best = { role, co, points, core };
2529
2558
  }
@@ -2535,6 +2564,8 @@ export function scorePerson(row, companyFor = (name) => companyScore(name), read
2535
2564
  const bonus = reachBonus(row?.headline, unknown ? 4 : best.co.base ?? best.co.score, reach);
2536
2565
  const power = round1(best.core + bonus.points);
2537
2566
  const title = { ...best.role.title, former: best.role.former, student, points: round1(best.points) };
2567
+ // Your ranking moved it: the card's "why" says so.
2568
+ if (titles && rankedPoints(best.role, titles) !== best.role.title.points) title.ranked = true;
2538
2569
  if (best.role.former) title.label = `Former ${title.label}`;
2539
2570
  if (student && best.role.title.level > 0) title.label = `Student · ${title.label}`;
2540
2571
  const out = { title, company: best.role.company, companyScore: best.co.score, companySource: best.co.source, bonus, power, tier: tierFor(power) };
@@ -2652,7 +2683,7 @@ export function explainScore(s, boost = 0, { sectorLabel } = {}) {
2652
2683
  const co = s.companySector
2653
2684
  ? `${s.company} (${s.companyScore}/10: ${s.companySector.base} + ${s.companySector.bonus} your sector${which ? `: ${which}` : ''})`
2654
2685
  : `${s.company || 'no company found'} (${s.companyScore}/10${s.companySource === 'yours' ? ', your score' : ''})`;
2655
- const bits = [`${s.title.label} (${s.title.points})`, co];
2686
+ const bits = [`${s.title.label} (${s.title.points}${s.title.ranked ? ', your ranking' : ''})`, co];
2656
2687
  if (s.bonus.points) bits.push(`+${s.bonus.points} ${s.bonus.reasons.join(', ')}`);
2657
2688
  if (boost) bits.push(`+${boost} strong circle`);
2658
2689
  return bits.join(' · ');
@@ -2716,7 +2747,7 @@ export function readNetwork(rows = [], { industryOf, sectorsOf, groupOf } = {})
2716
2747
  * (companyScore(), by name) and the curve's cut-offs (null on the fixed scale).
2717
2748
  */
2718
2749
  export function scoreNetwork(rows = [], {
2719
- overrides = new Map(), industryOf, sectorsOf, groupOf, focus, tierScale = 'fixed',
2750
+ overrides = new Map(), industryOf, sectorsOf, groupOf, focus, tierScale = 'fixed', titles = null,
2720
2751
  read = readNetwork(rows, { industryOf, sectorsOf, groupOf }),
2721
2752
  } = {}) {
2722
2753
  // Each company is scored once; everyone there shares the answer.
@@ -2726,7 +2757,7 @@ export function scoreNetwork(rows = [], {
2726
2757
  return companyScores.get(name);
2727
2758
  };
2728
2759
  const out = new Map();
2729
- for (const r of rows) out.set(r.id, { ...scorePerson(r, companyFor, read.people.get(r)), boost: 0 });
2760
+ for (const r of rows) out.set(r.id, { ...scorePerson(r, companyFor, read.people.get(r), titles), boost: 0 });
2730
2761
  // The curve's cut-offs come from your 1st-degree people before any circle
2731
2762
  // boost, so a boost (which reads the graded tiers) can't move its own cut-off.
2732
2763
  const cutoffs = tierScale === 'fixed' ? null
@@ -15,11 +15,27 @@ export async function scraperStatus() {
15
15
  return r.json();
16
16
  }
17
17
 
18
+ /**
19
+ * Scan page → "Hide the Chrome window while scanning", remembered in this
20
+ * browser. Every way a scan starts asks here: the Scan page's own buttons used
21
+ * to post straight to the scanner without it, so the switch did nothing for
22
+ * the scans started from the very page it sits on.
23
+ */
24
+ export const HIDE_CHROME_KEY = 'six-degrees-hide-chrome';
25
+ export function hideChromeOn(storage = typeof localStorage !== 'undefined' ? localStorage : null) {
26
+ try { return JSON.parse(storage?.getItem(HIDE_CHROME_KEY) || 'false') === true; } catch { return false; }
27
+ }
28
+
29
+ /** What a scan start sends: the action, its details, and `headless` when Chrome is to stay hidden. */
30
+ export function scanRequest(action, extra = {}, hidden = hideChromeOn()) {
31
+ return { action, ...extra, ...(hidden ? { headless: true } : {}) };
32
+ }
33
+
18
34
  export async function startScrape(action, name, extra = {}) {
19
35
  const r = await fetch('/api/scraper', {
20
36
  method: 'POST',
21
37
  headers: { 'Content-Type': 'application/json' },
22
- body: JSON.stringify({ action, ...(name ? { name } : {}), ...extra }),
38
+ body: JSON.stringify(scanRequest(action, { ...(name ? { name } : {}), ...extra })),
23
39
  });
24
40
  const d = await r.json().catch(() => ({}));
25
41
  if (!r.ok) throw new Error(d.error || 'Could not start the scanner.');
@@ -211,11 +227,8 @@ export async function beginScrape(action, { name, id, onLog, ...extra } = {}) {
211
227
  });
212
228
  let started;
213
229
  try {
214
- // Scan page → "Hide the Chrome window while scanning", remembered in this
215
- // browser, applies to every scan however it was started.
216
- let hidden = false;
217
- try { hidden = JSON.parse(localStorage.getItem('six-degrees-hide-chrome') || 'false') === true; } catch {}
218
- started = await startScrape(action, name, { ...(id ? { id } : {}), ...(hidden ? { headless: true } : {}), ...extra });
230
+ // startScrape adds "Hide the Chrome window" itself (scanRequest), however the scan was started.
231
+ started = await startScrape(action, name, { ...(id ? { id } : {}), ...extra });
219
232
  } catch (err) {
220
233
  // Refused, often because something else runs: ask what, straight away.
221
234
  // Until the answer comes, only our own claim is taken back.
@@ -141,11 +141,11 @@ export function tierMoves(rows, tierBefore, tierAfter, { examples = 5, companyOf
141
141
  */
142
142
  export function previewSectorFocus(rows = [], {
143
143
  overrides = new Map(), industryOf, sectorsOf = sectorMatcher(), groupOf = sectorGroup, from = NO_FOCUS, to = NO_FOCUS, examples = 5,
144
- tierScale, read = readNetwork(rows, { industryOf, sectorsOf, groupOf }),
144
+ tierScale, titles = null, read = readNetwork(rows, { industryOf, sectorsOf, groupOf }),
145
145
  } = {}) {
146
- // Both on the tier scale saved now (Scores → Tiers), as rescoring grades them.
147
- const before = scoreNetwork(rows, { overrides, focus: from, tierScale, read });
148
- const after = scoreNetwork(rows, { overrides, focus: to, tierScale, read });
146
+ // Both on the tier scale and title ranking saved now (Scores → Tiers, Titles), as rescoring grades them.
147
+ const before = scoreNetwork(rows, { overrides, focus: from, tierScale, titles, read });
148
+ const after = scoreNetwork(rows, { overrides, focus: to, tierScale, titles, read });
149
149
 
150
150
  // Companies whose score moves, biggest first (by your people there now).
151
151
  // Every company anyone names counts, a former employer too: its score moves
@@ -86,11 +86,6 @@ export function comparePower(a, b) {
86
86
  }
87
87
  const NO_ROUTE = { id: null, bridge: null };
88
88
 
89
- /** "Ways in" order: most ways in first, then the power order. The rank shown stays the power rank. */
90
- export function compareWaysIn(a, b) {
91
- return b.waysIn - a.waysIn || comparePower(a, b);
92
- }
93
-
94
89
  /**
95
90
  * @param {object[]} degree2 2nd-degree rows (one per bridge per person)
96
91
  * @param {object[]} connections 1st-degree rows, the bridges
@@ -236,6 +231,10 @@ export function dotRadius(waysIn) {
236
231
 
237
232
  export const MIN_BRIDGE_GAP = 18;
238
233
  const TOP_PAD = 16;
234
+ /** Grey dots for mutual connections in circles not scanned yet, at most this many drawn. */
235
+ export const GHOST_MAX = 60;
236
+ const GHOST_LABEL = 40; // room under the last connection for the line that counts them
237
+ const PILL_W_LAYOUT = 236; // SeparationView's PILL_W: the connection pills' width as cards
239
238
 
240
239
  /**
241
240
  * The summit map: You → the bridges → the top of the list, every route drawn.
@@ -249,10 +248,13 @@ const TOP_PAD = 16;
249
248
  *
250
249
  * @param {object[]} top the first K people of whatever the list shows
251
250
  * @param {number} width map width in px
251
+ * @param {{ cards?: boolean }} opts cards: people drawn as cards and
252
+ * connections as pills (Blake, 2026-10-02), which need more room per row
252
253
  * @returns {{ width, height, you, people, bridges, links, spokes }}
253
254
  */
254
- export function summitLayout(top = [], width = 800, isMobile = false) {
255
- const gap = isMobile ? 36 : 26;
255
+ export function summitLayout(top = [], width = 800, isMobile = false, { cards = false } = {}) {
256
+ const gap = cards ? 50 : isMobile ? 36 : 26;
257
+ const bridgeGap = cards ? 36 : MIN_BRIDGE_GAP;
256
258
  const youX = 20;
257
259
  const bx = Math.round(width * 0.42);
258
260
  const px = Math.round(width * 0.58);
@@ -262,7 +264,7 @@ export function summitLayout(top = [], width = 800, isMobile = false) {
262
264
  p,
263
265
  x: px,
264
266
  y: TOP_PAD + i * gap,
265
- r: dotRadius(p.waysIn),
267
+ r: cards ? 0 : dotRadius(p.waysIn),
266
268
  label: fit(`#${p.rank}${p.tied ? '=' : ''} ${p.person.name || ''}`, Math.floor((width - px - 60) / 6.2)),
267
269
  }));
268
270
 
@@ -288,11 +290,16 @@ export function summitLayout(top = [], width = 800, isMobile = false) {
288
290
 
289
291
  const y0 = TOP_PAD;
290
292
  const y1 = people.length ? people[people.length - 1].y : TOP_PAD;
291
- const step = bridges.length > 1 ? Math.max(MIN_BRIDGE_GAP, (y1 - y0) / (bridges.length - 1)) : 0;
293
+ // Each connection sits level with the people they lead to (the middle of
294
+ // them), moved no more than it takes to keep `bridgeGap` from the next one,
295
+ // so a connection with one person sits on that person's row and the line
296
+ // between them runs straight (Blake, 2026-10-02: "the lines should line up
297
+ // with every box").
298
+ const ys = stackColumn(bridges.map((b) => b.bary), bridgeGap, y0);
292
299
  const nameBudget = Math.max(4, Math.floor((bx - 10 - (youX + 22)) / 6));
293
300
  bridges.forEach((b, j) => {
294
301
  b.x = bx;
295
- b.y = bridges.length === 1 ? (y0 + y1) / 2 : y0 + j * step;
302
+ b.y = ys[j];
296
303
  b.r = 5;
297
304
  if (b.unresolved) b.label = fit(`can't be named (${b.count})`, nameBudget);
298
305
  else {
@@ -302,6 +309,15 @@ export function summitLayout(top = [], width = 800, isMobile = false) {
302
309
  delete b.ys;
303
310
  });
304
311
  const nodeFor = new Map(bridges.map((b) => [b.unresolved ? UNRESOLVED : b.id, b]));
312
+ // What each connection is to the people on the map: the only door to some,
313
+ // the best way in to others, or only another way in.
314
+ for (const b of bridges) { b.onlyFor = 0; b.bestFor = 0; }
315
+ for (const pp of people) {
316
+ const first = pp.p.routes[0];
317
+ const bestNode = first ? nodeFor.get(first.bridge ? first.id : UNRESOLVED) : null;
318
+ if (bestNode) bestNode.bestFor++;
319
+ if (pp.p.waysIn === 1 && bestNode) bestNode.onlyFor++;
320
+ }
305
321
 
306
322
  // Routes, per person: routes[0] is the top-scored bridge. Two unnamed routes
307
323
  // for one person are one line to the one grey node.
@@ -326,3 +342,114 @@ export function summitLayout(top = [], width = 800, isMobile = false) {
326
342
  const spokes = bridges.map((b) => ({ id: b.id, x1: you.x, y1: you.y, x2: b.x, y2: b.y }));
327
343
  return { width, height, you, people, bridges, links, spokes };
328
344
  }
345
+
346
+ /**
347
+ * Heights as near the ones asked for as they can be while staying `gap`
348
+ * apart, in the order given (already sorted), never above `top`: neighbours
349
+ * that would overlap move apart by the same amount each way.
350
+ */
351
+ export function stackColumn(wanted = [], gap = MIN_BRIDGE_GAP, top = 0) {
352
+ const blocks = [];
353
+ wanted.forEach((y, k) => {
354
+ blocks.push({ sum: y - k * gap, count: 1 });
355
+ while (blocks.length > 1 && blocks[blocks.length - 2].sum / blocks[blocks.length - 2].count > blocks[blocks.length - 1].sum / blocks[blocks.length - 1].count) {
356
+ const last = blocks.pop();
357
+ blocks[blocks.length - 1].sum += last.sum;
358
+ blocks[blocks.length - 1].count += last.count;
359
+ }
360
+ });
361
+ const out = blocks.flatMap((b) => new Array(b.count).fill(b.sum / b.count)).map((q, k) => q + k * gap);
362
+ const lift = out.length && out[0] < top ? top - out[0] : 0;
363
+ return out.map((y) => y + lift);
364
+ }
365
+
366
+ /**
367
+ * Who the map aims at with the slider all the way to easy: whoever you share the
368
+ * most mutual connections with, scanned or not (Blake, 2026-10-02: "if we have
369
+ * mutuals but not scanned thats still a high probability … i dont want to hinder
370
+ * the algorithm since we havent scanned that person"). `mutualsOf` is LinkedIn's
371
+ * count where a scan saved one, never less than the ways in the app has drawn
372
+ * (lib/rarity.js). Then the most drawn, then the stronger person. Narrowing the
373
+ * tiers first (S only, say) keeps it to the people worth it.
374
+ */
375
+ export function mostMutuals(people = [], mutualsOf = () => 0, limit = 7) {
376
+ return [...people].sort((a, b) => (Math.max(mutualsOf(b) || 0, b.waysIn) - Math.max(mutualsOf(a) || 0, a.waysIn))
377
+ || b.waysIn - a.waysIn
378
+ || comparePower(a, b)).slice(0, limit);
379
+ }
380
+
381
+ /**
382
+ * How many people the map draws with the slider at `at` (lib/rarity.js): all
383
+ * `k` from the rare end to the middle, then fewer and fewer, down to the one
384
+ * person you're aiming at with the slider all the way to easy. Blake,
385
+ * 2026-10-02: at the rare end "it will show that one door"; at the other "all
386
+ * the connections going into that one person".
387
+ */
388
+ export function mapCount(at, k = 10) {
389
+ const v = Math.max(0, Math.min(100, Number(at) || 0));
390
+ if (v <= 50) return k;
391
+ return Math.max(1, Math.round((k * (100 - v)) / 50));
392
+ }
393
+
394
+ /**
395
+ * The map for one person: You → every connection of yours who leads to them →
396
+ * them, with rings round them. Routes the map can't name share one grey node,
397
+ * as in summitLayout, and it comes back in summitLayout's shape (`people` holds
398
+ * the one person, marked `big`), so one map draws both and can glide from one
399
+ * to the other. Pure, so it is tested without a browser.
400
+ *
401
+ * @returns {{ width, height, you, target, people, bridges, links, spokes }}
402
+ */
403
+ export function convergeLayout(p, width = 800, isMobile = false, { cards = false, mutuals = 0 } = {}) {
404
+ const gap = cards ? 46 : isMobile ? 40 : 34;
405
+ const youX = 20;
406
+ const bx = Math.round(width * (isMobile ? 0.44 : 0.36));
407
+ const tx = Math.round(width * (isMobile ? 0.8 : cards ? 0.6 : 0.62));
408
+ const nodes = [];
409
+ let unnamed = null;
410
+ for (const r of p?.routes || []) {
411
+ if (r.bridge) nodes.push({ id: r.id, bridge: r.bridge, unresolved: false, count: 1 });
412
+ else if (unnamed) unnamed.count++;
413
+ else nodes.push((unnamed = { id: null, bridge: null, unresolved: true, count: 1 }));
414
+ }
415
+ // Mutual connections the app can't draw yet: they're in circles you haven't
416
+ // scanned. Grey dots under the connections who do lead there, each with a faint
417
+ // line in, so the count reads at a glance (GHOST_MAX at most, then "+N").
418
+ const unscanned = Math.max(0, Math.floor(Number(mutuals) || 0) - (p?.waysIn || 0));
419
+ const ghostCount = Math.min(GHOST_MAX, unscanned);
420
+ const ghostStep = cards ? 12 : 11;
421
+ const ghostW = cards ? PILL_W_LAYOUT - 16 : isMobile ? 96 : 120;
422
+ const perRow = Math.max(1, Math.floor(ghostW / ghostStep) + 1);
423
+ const ghostRows = Math.ceil(ghostCount / perRow);
424
+ const ghostH = ghostCount ? GHOST_LABEL + ghostRows * ghostStep : 0;
425
+ const block = Math.max(0, nodes.length - 1) * gap + ghostH;
426
+ const height = Math.max(isMobile ? 190 : cards ? 250 : 220, TOP_PAD * 2 + block + 48);
427
+ const mid = height / 2;
428
+ const top = mid - block / 2;
429
+ const nameBudget = Math.max(4, Math.floor((bx - 10 - (youX + 22)) / 6));
430
+ const bridges = nodes.map((n, j) => {
431
+ const full = n.unresolved ? `can't be named (${n.count})` : n.bridge.name || '';
432
+ return {
433
+ ...n, x: bx, y: top + j * gap, r: 6,
434
+ primary: j === 0 && !n.unresolved,
435
+ label: full.length <= nameBudget ? full : fit(n.unresolved ? full : shortName(full), nameBudget),
436
+ };
437
+ });
438
+ const target = { x: tx, y: mid, r: cards ? 0 : 17, rings: cards ? [] : [32, 50, 70].filter((r) => r <= mid - 4) };
439
+ const links = bridges.map((b) => ({
440
+ key: p?.key, bridgeId: b.id, primary: b.primary, unresolved: b.unresolved, tier: b.bridge?.tier ?? null,
441
+ x1: b.x, y1: b.y, x2: target.x, y2: target.y,
442
+ }));
443
+ const you = { x: youX, y: mid, r: 14 };
444
+ const spokes = bridges.map((b) => ({ id: b.id, x1: you.x, y1: you.y, x2: b.x, y2: b.y }));
445
+ const people = p ? [{ key: p.key, p, x: target.x, y: target.y, r: target.r, rings: target.rings, big: true, label: '' }] : [];
446
+ const gy = top + Math.max(0, nodes.length - 1) * gap + GHOST_LABEL;
447
+ const gx = cards ? bx - PILL_W_LAYOUT + 8 : bx - ghostW;
448
+ const ghosts = Array.from({ length: ghostCount }, (_, i) => ({ x: gx + (i % perRow) * ghostStep, y: gy + Math.floor(i / perRow) * ghostStep }));
449
+ // How bright the person is: the share of your mutual connections with them that the map can draw.
450
+ const drawn = p?.waysIn ? p.waysIn / (p.waysIn + unscanned) : 1;
451
+ return {
452
+ width, height, you, target, people, bridges, links, spokes,
453
+ ghosts, unscanned, ghostLabel: ghostCount ? { x: gx, y: gy - 13 } : null, drawn,
454
+ };
455
+ }