six-degrees 0.4.11 → 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 (375) hide show
  1. package/.next/standalone/.next/BUILD_ID +1 -1
  2. package/.next/standalone/.next/build-manifest.json +3 -3
  3. package/.next/standalone/.next/prerender-manifest.json +9 -9
  4. package/.next/standalone/.next/server/app/_global-error.html +1 -1
  5. package/.next/standalone/.next/server/app/_global-error.rsc +1 -1
  6. package/.next/standalone/.next/server/app/_global-error.segments/__PAGE__.segment.rsc +1 -1
  7. package/.next/standalone/.next/server/app/_global-error.segments/_full.segment.rsc +1 -1
  8. package/.next/standalone/.next/server/app/_global-error.segments/_tree.segment.rsc +1 -1
  9. package/.next/standalone/.next/server/app/_not-found/page.js.nft.json +1 -1
  10. package/.next/standalone/.next/server/app/_not-found/page_client-reference-manifest.js +1 -1
  11. package/.next/standalone/.next/server/app/_not-found.html +1 -1
  12. package/.next/standalone/.next/server/app/_not-found.rsc +12 -12
  13. package/.next/standalone/.next/server/app/_not-found.segments/_full.segment.rsc +12 -12
  14. package/.next/standalone/.next/server/app/_not-found.segments/_not-found/__PAGE__.segment.rsc +11 -11
  15. package/.next/standalone/.next/server/app/_not-found.segments/_tree.segment.rsc +1 -1
  16. package/.next/standalone/.next/server/app/api/admin-delete/route.js +1 -1
  17. package/.next/standalone/.next/server/app/api/admin-delete/route.js.nft.json +1 -1
  18. package/.next/standalone/.next/server/app/api/admin-update/route.js +1 -1
  19. package/.next/standalone/.next/server/app/api/admin-update/route.js.nft.json +1 -1
  20. package/.next/standalone/.next/server/app/api/bridges/route.js +1 -1
  21. package/.next/standalone/.next/server/app/api/bridges/route.js.nft.json +1 -1
  22. package/.next/standalone/.next/server/app/api/bulk-import/route.js +1 -1
  23. package/.next/standalone/.next/server/app/api/bulk-import/route.js.nft.json +1 -1
  24. package/.next/standalone/.next/server/app/api/company-scores/legacy/route.js +1 -1
  25. package/.next/standalone/.next/server/app/api/company-scores/legacy/route.js.nft.json +1 -1
  26. package/.next/standalone/.next/server/app/api/company-scores/route.js +1 -1
  27. package/.next/standalone/.next/server/app/api/company-scores/route.js.nft.json +1 -1
  28. package/.next/standalone/.next/server/app/api/connections/route.js +1 -1
  29. package/.next/standalone/.next/server/app/api/connections/route.js.nft.json +1 -1
  30. package/.next/standalone/.next/server/app/api/data/export/route.js.nft.json +1 -1
  31. package/.next/standalone/.next/server/app/api/data/import/route.js.nft.json +1 -1
  32. package/.next/standalone/.next/server/app/api/data/restart/route.js.nft.json +1 -1
  33. package/.next/standalone/.next/server/app/api/data/reveal/route.js.nft.json +1 -1
  34. package/.next/standalone/.next/server/app/api/data/route.js.nft.json +1 -1
  35. package/.next/standalone/.next/server/app/api/delete-cluster/route.js +1 -1
  36. package/.next/standalone/.next/server/app/api/delete-cluster/route.js.nft.json +1 -1
  37. package/.next/standalone/.next/server/app/api/ingest/route.js +1 -1
  38. package/.next/standalone/.next/server/app/api/ingest/route.js.nft.json +1 -1
  39. package/.next/standalone/.next/server/app/api/network/route.js +1 -1
  40. package/.next/standalone/.next/server/app/api/network/route.js.nft.json +1 -1
  41. package/.next/standalone/.next/server/app/api/notifications/route.js +1 -1
  42. package/.next/standalone/.next/server/app/api/notifications/route.js.nft.json +1 -1
  43. package/.next/standalone/.next/server/app/api/outreach/route.js +1 -1
  44. package/.next/standalone/.next/server/app/api/outreach/route.js.nft.json +1 -1
  45. package/.next/standalone/.next/server/app/api/queue/route.js +1 -1
  46. package/.next/standalone/.next/server/app/api/queue/route.js.nft.json +1 -1
  47. package/.next/standalone/.next/server/app/api/scraper/route.js +3 -2
  48. package/.next/standalone/.next/server/app/api/scraper/route.js.nft.json +1 -1
  49. package/.next/standalone/.next/server/app/api/settings/route.js +1 -1
  50. package/.next/standalone/.next/server/app/api/settings/route.js.nft.json +1 -1
  51. package/.next/standalone/.next/server/app/api/settings/sector-preview/route.js +1 -1
  52. package/.next/standalone/.next/server/app/api/settings/sector-preview/route.js.nft.json +1 -1
  53. package/.next/standalone/.next/server/app/api/settings/sector-suggestions/route.js +1 -1
  54. package/.next/standalone/.next/server/app/api/settings/sector-suggestions/route.js.nft.json +1 -1
  55. package/.next/standalone/.next/server/app/api/setup-profile/route.js +1 -1
  56. package/.next/standalone/.next/server/app/api/setup-profile/route.js.nft.json +1 -1
  57. package/.next/standalone/.next/server/app/api/social/crm/route.js.nft.json +1 -1
  58. package/.next/standalone/.next/server/app/api/social/messages/route.js.nft.json +1 -1
  59. package/.next/standalone/.next/server/app/api/social/route.js +3 -3
  60. package/.next/standalone/.next/server/app/api/social/route.js.nft.json +1 -1
  61. package/.next/standalone/.next/server/app/api/unlock/route.js +1 -1
  62. package/.next/standalone/.next/server/app/api/unlock/route.js.nft.json +1 -1
  63. package/.next/standalone/.next/server/app/api/update/route.js.nft.json +1 -1
  64. package/.next/standalone/.next/server/app/api/update-images/route.js +1 -1
  65. package/.next/standalone/.next/server/app/api/update-images/route.js.nft.json +1 -1
  66. package/.next/standalone/.next/server/app/api/users/route.js +1 -1
  67. package/.next/standalone/.next/server/app/api/users/route.js.nft.json +1 -1
  68. package/.next/standalone/.next/server/app/avatars/[file]/route.js.nft.json +1 -1
  69. package/.next/standalone/.next/server/app/import/page.js.nft.json +1 -1
  70. package/.next/standalone/.next/server/app/import/page_client-reference-manifest.js +1 -1
  71. package/.next/standalone/.next/server/app/import.html +1 -1
  72. package/.next/standalone/.next/server/app/import.rsc +14 -14
  73. package/.next/standalone/.next/server/app/import.segments/_full.segment.rsc +14 -14
  74. package/.next/standalone/.next/server/app/import.segments/_tree.segment.rsc +1 -1
  75. package/.next/standalone/.next/server/app/import.segments/import/__PAGE__.segment.rsc +13 -13
  76. package/.next/standalone/.next/server/app/index.html +1 -1
  77. package/.next/standalone/.next/server/app/index.rsc +14 -14
  78. package/.next/standalone/.next/server/app/index.segments/__PAGE__.segment.rsc +13 -13
  79. package/.next/standalone/.next/server/app/index.segments/_full.segment.rsc +14 -14
  80. package/.next/standalone/.next/server/app/index.segments/_tree.segment.rsc +1 -1
  81. package/.next/standalone/.next/server/app/launch/page.js.nft.json +1 -1
  82. package/.next/standalone/.next/server/app/launch/page_client-reference-manifest.js +1 -1
  83. package/.next/standalone/.next/server/app/launch.html +2 -2
  84. package/.next/standalone/.next/server/app/launch.rsc +14 -14
  85. package/.next/standalone/.next/server/app/launch.segments/_full.segment.rsc +14 -14
  86. package/.next/standalone/.next/server/app/launch.segments/_tree.segment.rsc +1 -1
  87. package/.next/standalone/.next/server/app/launch.segments/launch/__PAGE__.segment.rsc +13 -13
  88. package/.next/standalone/.next/server/app/page.js.nft.json +1 -1
  89. package/.next/standalone/.next/server/app/page_client-reference-manifest.js +1 -1
  90. package/.next/standalone/.next/server/app/paths/page.js.nft.json +1 -1
  91. package/.next/standalone/.next/server/app/paths/page_client-reference-manifest.js +1 -1
  92. package/.next/standalone/.next/server/app/paths.html +1 -1
  93. package/.next/standalone/.next/server/app/paths.rsc +14 -14
  94. package/.next/standalone/.next/server/app/paths.segments/_full.segment.rsc +14 -14
  95. package/.next/standalone/.next/server/app/paths.segments/_tree.segment.rsc +1 -1
  96. package/.next/standalone/.next/server/app/paths.segments/paths/__PAGE__.segment.rsc +13 -13
  97. package/.next/standalone/.next/server/app/profile/page.js.nft.json +1 -1
  98. package/.next/standalone/.next/server/app/profile/page_client-reference-manifest.js +1 -1
  99. package/.next/standalone/.next/server/app/profile.html +1 -1
  100. package/.next/standalone/.next/server/app/profile.rsc +14 -14
  101. package/.next/standalone/.next/server/app/profile.segments/_full.segment.rsc +14 -14
  102. package/.next/standalone/.next/server/app/profile.segments/_tree.segment.rsc +1 -1
  103. package/.next/standalone/.next/server/app/profile.segments/profile/__PAGE__.segment.rsc +13 -13
  104. package/.next/standalone/.next/server/app/queue/page.js.nft.json +1 -1
  105. package/.next/standalone/.next/server/app/queue/page_client-reference-manifest.js +1 -1
  106. package/.next/standalone/.next/server/app/queue.html +1 -1
  107. package/.next/standalone/.next/server/app/queue.rsc +14 -14
  108. package/.next/standalone/.next/server/app/queue.segments/_full.segment.rsc +14 -14
  109. package/.next/standalone/.next/server/app/queue.segments/_tree.segment.rsc +1 -1
  110. package/.next/standalone/.next/server/app/queue.segments/queue/__PAGE__.segment.rsc +13 -13
  111. package/.next/standalone/.next/server/app/scores/page.js.nft.json +1 -1
  112. package/.next/standalone/.next/server/app/scores/page_client-reference-manifest.js +1 -1
  113. package/.next/standalone/.next/server/app/scores.html +1 -1
  114. package/.next/standalone/.next/server/app/scores.rsc +14 -14
  115. package/.next/standalone/.next/server/app/scores.segments/_full.segment.rsc +14 -14
  116. package/.next/standalone/.next/server/app/scores.segments/_tree.segment.rsc +1 -1
  117. package/.next/standalone/.next/server/app/scores.segments/scores/__PAGE__.segment.rsc +13 -13
  118. package/.next/standalone/.next/server/app/settings/page.js.nft.json +1 -1
  119. package/.next/standalone/.next/server/app/settings/page_client-reference-manifest.js +1 -1
  120. package/.next/standalone/.next/server/app/settings.html +1 -1
  121. package/.next/standalone/.next/server/app/settings.rsc +14 -14
  122. package/.next/standalone/.next/server/app/settings.segments/_full.segment.rsc +14 -14
  123. package/.next/standalone/.next/server/app/settings.segments/_tree.segment.rsc +1 -1
  124. package/.next/standalone/.next/server/app/settings.segments/settings/__PAGE__.segment.rsc +13 -13
  125. package/.next/standalone/.next/server/app/setup/page.js.nft.json +1 -1
  126. package/.next/standalone/.next/server/app/setup/page_client-reference-manifest.js +1 -1
  127. package/.next/standalone/.next/server/app/setup.html +1 -1
  128. package/.next/standalone/.next/server/app/setup.rsc +14 -14
  129. package/.next/standalone/.next/server/app/setup.segments/_full.segment.rsc +14 -14
  130. package/.next/standalone/.next/server/app/setup.segments/_tree.segment.rsc +1 -1
  131. package/.next/standalone/.next/server/app/setup.segments/setup/__PAGE__.segment.rsc +13 -13
  132. package/.next/standalone/.next/server/app/social/page.js.nft.json +1 -1
  133. package/.next/standalone/.next/server/app/social/page_client-reference-manifest.js +1 -1
  134. package/.next/standalone/.next/server/app/social.html +1 -1
  135. package/.next/standalone/.next/server/app/social.rsc +14 -14
  136. package/.next/standalone/.next/server/app/social.segments/_full.segment.rsc +14 -14
  137. package/.next/standalone/.next/server/app/social.segments/_tree.segment.rsc +1 -1
  138. package/.next/standalone/.next/server/app/social.segments/social/__PAGE__.segment.rsc +13 -13
  139. package/.next/standalone/.next/server/chunks/{[root-of-the-server]__0bat8nx._.js → [root-of-the-server]__13dx4sd._.js} +2 -2
  140. package/.next/standalone/.next/server/chunks/[root-of-the-server]__15wc9e-._.js +3 -0
  141. package/.next/standalone/.next/server/chunks/[root-of-the-server]__1kbl21f._.js +1 -1
  142. package/.next/standalone/.next/server/chunks/[root-of-the-server]__20rwfdw._.js +8 -0
  143. package/.next/standalone/.next/server/chunks/_072jl0y._.js +1 -1
  144. package/.next/standalone/.next/server/chunks/_1_fynia._.js +7 -7
  145. package/.next/standalone/.next/server/chunks/_1itd1o_._.js +1 -1
  146. package/.next/standalone/.next/server/chunks/_1iwkh8t._.js +1 -1
  147. package/.next/standalone/.next/server/chunks/lib_0851sk0._.js +8 -0
  148. package/.next/standalone/.next/server/chunks/lib_1q7p2ro._.js +8 -0
  149. package/.next/standalone/.next/server/chunks/lib_1r6l86l._.js +8 -0
  150. package/.next/standalone/.next/server/chunks/lib_db-client_15le7cw.js +8 -8
  151. package/.next/standalone/.next/server/chunks/lib_rpc_04s8mzr.js +3 -0
  152. package/.next/standalone/.next/server/chunks/lib_rpc_053z7as.js +3 -0
  153. package/.next/standalone/.next/server/chunks/lib_rpc_0b_-2te.js +3 -0
  154. package/.next/standalone/.next/server/chunks/lib_rpc_0dhbxdm.js +3 -0
  155. package/.next/standalone/.next/server/chunks/lib_rpc_0dxlk31.js +3 -0
  156. package/.next/standalone/.next/server/chunks/lib_rpc_0f8b-qc.js +3 -0
  157. package/.next/standalone/.next/server/chunks/lib_rpc_0pzie7_.js +3 -0
  158. package/.next/standalone/.next/server/chunks/lib_rpc_0r76fq_.js +3 -0
  159. package/.next/standalone/.next/server/chunks/lib_rpc_0ut7pjp.js +3 -0
  160. package/.next/standalone/.next/server/chunks/lib_rpc_15zyqaz.js +3 -0
  161. package/.next/standalone/.next/server/chunks/lib_rpc_18zpjtq.js +3 -0
  162. package/.next/standalone/.next/server/chunks/lib_rpc_1a8xkiz.js +3 -0
  163. package/.next/standalone/.next/server/chunks/lib_rpc_1e_55xo.js +3 -0
  164. package/.next/standalone/.next/server/chunks/lib_rpc_1oxfd1_.js +3 -0
  165. package/.next/standalone/.next/server/chunks/lib_rpc_1s0lod0.js +3 -0
  166. package/.next/standalone/.next/server/chunks/lib_scoring_1-e4ur0.js +1 -1
  167. package/.next/standalone/.next/server/chunks/ssr/_01kacen._.js +3 -0
  168. package/.next/standalone/.next/server/chunks/ssr/_03l7vge._.js +3 -0
  169. package/.next/standalone/.next/server/chunks/ssr/{_1ap1tu_._.js → _0gtv9jb._.js} +2 -2
  170. package/.next/standalone/.next/server/chunks/ssr/_0jdkqgi._.js +3 -0
  171. package/.next/standalone/.next/server/chunks/ssr/_0xi-fbs._.js +3 -0
  172. package/.next/standalone/.next/server/chunks/ssr/_0xmwmfv._.js +3 -0
  173. package/.next/standalone/.next/server/chunks/ssr/_1-b9hj7._.js +9 -0
  174. package/.next/standalone/.next/server/chunks/ssr/_112ewem._.js +3 -0
  175. package/.next/standalone/.next/server/chunks/ssr/_16ab63q._.js +3 -0
  176. package/.next/standalone/.next/server/chunks/ssr/_1fxm0-s._.js +3 -0
  177. package/.next/standalone/.next/server/chunks/ssr/_1ra1d3r._.js +9 -0
  178. package/.next/standalone/.next/server/chunks/ssr/app_141w189._.js +3 -0
  179. package/.next/standalone/.next/server/chunks/ssr/app_components_ui_0pdmz2z.js +3 -0
  180. package/.next/standalone/.next/server/chunks/ssr/app_page_1fazqbf.js +22 -10
  181. package/.next/standalone/.next/server/chunks/ssr/app_paths_page_1ol90m7.js +2 -2
  182. package/.next/standalone/.next/server/chunks/ssr/app_profile_page_0u3-l5y.js +1 -1
  183. package/.next/standalone/.next/server/chunks/ssr/app_queue_page_0c6znd6.js +6 -5
  184. package/.next/standalone/.next/server/chunks/ssr/app_settings_page_1f8mjj_.js +1 -1
  185. package/.next/standalone/.next/server/chunks/ssr/app_setup_page_1-282zx.js +19 -3
  186. package/.next/standalone/.next/server/chunks/ssr/app_social_page_0h7zxda.js +1 -2
  187. package/.next/standalone/.next/server/chunks/ssr/lib_0i8v-s3._.js +1 -1
  188. package/.next/standalone/.next/server/chunks/ssr/lib_13rapwj._.js +3 -0
  189. package/.next/standalone/.next/server/chunks/ssr/lib_galaxy-lab_108_lu-.js +1 -1
  190. package/.next/standalone/.next/server/middleware-build-manifest.js +3 -3
  191. package/.next/standalone/.next/server/middleware-manifest.json +5 -5
  192. package/.next/standalone/.next/server/pages/404.html +1 -1
  193. package/.next/standalone/.next/server/pages/500.html +1 -1
  194. package/.next/standalone/.next/server/server-reference-manifest.js +1 -1
  195. package/.next/standalone/.next/server/server-reference-manifest.json +1 -1
  196. package/.next/standalone/.next/static/chunks/03bziijw5srol.js +1 -0
  197. package/.next/standalone/.next/static/chunks/0bgq8wmopl_ig.js +7 -0
  198. package/.next/standalone/.next/static/chunks/0ezus6p2eb-vg.js +1 -0
  199. package/.next/standalone/.next/static/chunks/0g_fy2034bp0x.js +34 -0
  200. package/.next/standalone/.next/static/chunks/0q5dru4q6gxl5.js +22 -0
  201. package/.next/standalone/.next/static/chunks/0w-byiwb2zax8.js +2 -0
  202. package/.next/standalone/.next/static/chunks/1g7jcovdejc9-.js +1 -0
  203. package/.next/standalone/.next/static/chunks/1i51yylxdzht1.js +1 -0
  204. package/.next/standalone/.next/static/chunks/1zf8swaasjf1e.js +43 -0
  205. package/.next/standalone/.next/static/chunks/2rqtpccruavf2.js +1 -0
  206. package/.next/standalone/.next/static/chunks/2wgz9ietkqqe6.js +1 -0
  207. package/.next/standalone/.next/static/chunks/30x3oq_st7a5r.js +1 -0
  208. package/.next/standalone/.next/static/chunks/355ryio1b7sm1.js +1 -0
  209. package/.next/standalone/.next/static/chunks/3ap5hgma7n0o7.js +1 -0
  210. package/.next/standalone/.next/static/chunks/3fci6qi39-1n5.js +1 -0
  211. package/.next/standalone/.next/static/chunks/{24tvdun1cb3g1.js → 3sowbvbaj1s6n.js} +1 -1
  212. package/.next/standalone/CHANGELOG.md +187 -0
  213. package/.next/standalone/app/api/ingest/route.js +40 -1
  214. package/.next/standalone/app/api/scraper/route.js +7 -1
  215. package/.next/standalone/app/api/settings/sector-preview/route.js +2 -1
  216. package/.next/standalone/app/components/AppTabs.js +59 -0
  217. package/.next/standalone/app/components/ChainView.js +303 -89
  218. package/.next/standalone/app/components/CirclePanel.js +162 -0
  219. package/.next/standalone/app/components/CompanyScores.js +2 -2
  220. package/.next/standalone/app/components/FieldStep.js +4 -4
  221. package/.next/standalone/app/components/FilterPanel.js +121 -183
  222. package/.next/standalone/app/components/ForceGraph.js +41 -3
  223. package/.next/standalone/app/components/GalaxyLab.js +26 -31
  224. package/.next/standalone/app/components/LegacyScoresCard.js +2 -2
  225. package/.next/standalone/app/components/MapControls.js +154 -0
  226. package/.next/standalone/app/components/NoteDetail.js +118 -0
  227. package/.next/standalone/app/components/PathsAnalyzer.js +147 -110
  228. package/.next/standalone/app/components/PeopleMap.js +308 -0
  229. package/.next/standalone/app/components/ScanRadar.js +129 -0
  230. package/.next/standalone/app/components/ScanStatusBar.js +71 -27
  231. package/.next/standalone/app/components/SeparationView.js +626 -118
  232. package/.next/standalone/app/components/Sidebar.js +28 -2
  233. package/.next/standalone/app/components/settings/NetworkHealthSection.js +1 -1
  234. package/.next/standalone/app/components/settings/SectorSection.js +2 -2
  235. package/.next/standalone/app/components/settings/TierSection.js +1 -1
  236. package/.next/standalone/app/components/settings/TitleSection.js +149 -0
  237. package/.next/standalone/app/components/views.js +15 -14
  238. package/.next/standalone/app/page.js +89 -156
  239. package/.next/standalone/app/paths/page.js +39 -27
  240. package/.next/standalone/app/profile/page.js +33 -5
  241. package/.next/standalone/app/queue/page.js +56 -34
  242. package/.next/standalone/app/scores/page.js +16 -66
  243. package/.next/standalone/app/settings/page.js +27 -13
  244. package/.next/standalone/app/setup/page.js +298 -205
  245. package/.next/standalone/app/social/SocialHub.js +374 -0
  246. package/.next/standalone/app/social/page.js +10 -362
  247. package/.next/standalone/lib/chain-layout.js +105 -35
  248. package/.next/standalone/lib/degrees.js +0 -6
  249. package/.next/standalone/lib/dot-rings.js +25 -0
  250. package/.next/standalone/lib/galaxy-lab.js +65 -6
  251. package/.next/standalone/lib/ingest.js +2 -0
  252. package/.next/standalone/lib/island.js +40 -0
  253. package/.next/standalone/lib/linkedin-limits.js +6 -2
  254. package/.next/standalone/lib/map-heat.js +20 -0
  255. package/.next/standalone/lib/network-health.js +1 -1
  256. package/.next/standalone/lib/notifications.js +121 -0
  257. package/.next/standalone/lib/people-map.js +82 -0
  258. package/.next/standalone/lib/rarity.js +45 -3
  259. package/.next/standalone/lib/reach.js +7 -6
  260. package/.next/standalone/lib/rpc.js +15 -4
  261. package/.next/standalone/lib/scan-pace.js +46 -0
  262. package/.next/standalone/lib/scoring.js +39 -8
  263. package/.next/standalone/lib/scraper-client.js +19 -6
  264. package/.next/standalone/lib/sector-focus.js +4 -4
  265. package/.next/standalone/lib/separation.js +137 -10
  266. package/.next/standalone/lib/settings-effects.js +20 -11
  267. package/.next/standalone/lib/settings.js +3 -0
  268. package/.next/standalone/lib/tier-grid.js +99 -0
  269. package/.next/standalone/lib/title-ranking.js +83 -0
  270. package/.next/standalone/package-lock.json +2 -2
  271. package/.next/standalone/package.json +1 -1
  272. package/.next/standalone/scripts/scrape.py +51 -13
  273. package/.next/static/chunks/03bziijw5srol.js +1 -0
  274. package/.next/static/chunks/0bgq8wmopl_ig.js +7 -0
  275. package/.next/static/chunks/0ezus6p2eb-vg.js +1 -0
  276. package/.next/static/chunks/0g_fy2034bp0x.js +34 -0
  277. package/.next/static/chunks/0q5dru4q6gxl5.js +22 -0
  278. package/.next/static/chunks/0w-byiwb2zax8.js +2 -0
  279. package/.next/static/chunks/1g7jcovdejc9-.js +1 -0
  280. package/.next/static/chunks/1i51yylxdzht1.js +1 -0
  281. package/.next/static/chunks/1zf8swaasjf1e.js +43 -0
  282. package/.next/static/chunks/2rqtpccruavf2.js +1 -0
  283. package/.next/static/chunks/2wgz9ietkqqe6.js +1 -0
  284. package/.next/static/chunks/30x3oq_st7a5r.js +1 -0
  285. package/.next/static/chunks/355ryio1b7sm1.js +1 -0
  286. package/.next/static/chunks/3ap5hgma7n0o7.js +1 -0
  287. package/.next/static/chunks/3fci6qi39-1n5.js +1 -0
  288. package/.next/static/chunks/{24tvdun1cb3g1.js → 3sowbvbaj1s6n.js} +1 -1
  289. package/CHANGELOG.md +187 -0
  290. package/lib/chain-layout.js +105 -35
  291. package/lib/degrees.js +0 -6
  292. package/lib/dot-rings.js +25 -0
  293. package/lib/galaxy-lab.js +65 -6
  294. package/lib/ingest.js +2 -0
  295. package/lib/island.js +40 -0
  296. package/lib/linkedin-limits.js +6 -2
  297. package/lib/map-heat.js +20 -0
  298. package/lib/network-health.js +1 -1
  299. package/lib/notifications.js +121 -0
  300. package/lib/people-map.js +82 -0
  301. package/lib/rarity.js +45 -3
  302. package/lib/reach.js +7 -6
  303. package/lib/rpc.js +15 -4
  304. package/lib/scan-pace.js +46 -0
  305. package/lib/scoring.js +39 -8
  306. package/lib/scraper-client.js +19 -6
  307. package/lib/sector-focus.js +4 -4
  308. package/lib/separation.js +137 -10
  309. package/lib/settings-effects.js +20 -11
  310. package/lib/settings.js +3 -0
  311. package/lib/tier-grid.js +99 -0
  312. package/lib/title-ranking.js +83 -0
  313. package/package.json +1 -1
  314. package/scripts/scrape.py +51 -13
  315. package/.next/standalone/.next/server/chunks/[root-of-the-server]__17hjtzl._.js +0 -8
  316. package/.next/standalone/.next/server/chunks/[root-of-the-server]__20tmem7._.js +0 -3
  317. package/.next/standalone/.next/server/chunks/lib_rpc_03i8xef.js +0 -3
  318. package/.next/standalone/.next/server/chunks/lib_rpc_05gt5ya.js +0 -3
  319. package/.next/standalone/.next/server/chunks/lib_rpc_08fc3rp.js +0 -3
  320. package/.next/standalone/.next/server/chunks/lib_rpc_08r2t_q.js +0 -3
  321. package/.next/standalone/.next/server/chunks/lib_rpc_0btpv1t.js +0 -3
  322. package/.next/standalone/.next/server/chunks/lib_rpc_0c9d9ti.js +0 -8
  323. package/.next/standalone/.next/server/chunks/lib_rpc_0cm2ev3.js +0 -3
  324. package/.next/standalone/.next/server/chunks/lib_rpc_0iovswz.js +0 -3
  325. package/.next/standalone/.next/server/chunks/lib_rpc_0is8flp.js +0 -3
  326. package/.next/standalone/.next/server/chunks/lib_rpc_0v8ajtd.js +0 -3
  327. package/.next/standalone/.next/server/chunks/lib_rpc_0xgmvvt.js +0 -3
  328. package/.next/standalone/.next/server/chunks/lib_rpc_144mzbf.js +0 -3
  329. package/.next/standalone/.next/server/chunks/lib_rpc_1bggi0n.js +0 -3
  330. package/.next/standalone/.next/server/chunks/lib_rpc_1dkcu9f.js +0 -3
  331. package/.next/standalone/.next/server/chunks/lib_rpc_1o0jb6w.js +0 -3
  332. package/.next/standalone/.next/server/chunks/ssr/_0-85h6g._.js +0 -3
  333. package/.next/standalone/.next/server/chunks/ssr/_08cfj2b._.js +0 -3
  334. package/.next/standalone/.next/server/chunks/ssr/_0c_y09g._.js +0 -3
  335. package/.next/standalone/.next/server/chunks/ssr/_0j4-lc3._.js +0 -3
  336. package/.next/standalone/.next/server/chunks/ssr/_0l8cxmo._.js +0 -3
  337. package/.next/standalone/.next/server/chunks/ssr/_0xkur4p._.js +0 -3
  338. package/.next/standalone/.next/server/chunks/ssr/_14iv_zy._.js +0 -3
  339. package/.next/standalone/.next/server/chunks/ssr/_15x6q32._.js +0 -3
  340. package/.next/standalone/.next/server/chunks/ssr/_212xw8q._.js +0 -3
  341. package/.next/standalone/.next/server/chunks/ssr/app_components_19c2d9c._.js +0 -3
  342. package/.next/standalone/.next/server/chunks/ssr/app_scores_page_09wepvv.js +0 -3
  343. package/.next/standalone/.next/static/chunks/0a5iadnmt9u9k.js +0 -1
  344. package/.next/standalone/.next/static/chunks/15sjx07xhpn6p.js +0 -1
  345. package/.next/standalone/.next/static/chunks/160wjb73ddwqd.js +0 -6
  346. package/.next/standalone/.next/static/chunks/1a74dky22dxd6.js +0 -1
  347. package/.next/standalone/.next/static/chunks/1c_n0973t07l0.js +0 -2
  348. package/.next/standalone/.next/static/chunks/1e1n0kkz-psdb.js +0 -1
  349. package/.next/standalone/.next/static/chunks/236f0jtp68j_h.js +0 -1
  350. package/.next/standalone/.next/static/chunks/297e1z34yi--z.js +0 -1
  351. package/.next/standalone/.next/static/chunks/2_ak6qjxb6oa5.js +0 -1
  352. package/.next/standalone/.next/static/chunks/2kfhc58df2xyo.js +0 -1
  353. package/.next/standalone/.next/static/chunks/39hr8d-b2ibtw.js +0 -31
  354. package/.next/standalone/.next/static/chunks/3ui4d-uqkqj5d.js +0 -2
  355. package/.next/standalone/.next/static/chunks/3zw8bv7-mc4n6.js +0 -33
  356. package/.next/standalone/app/components/OrbitGraph.js +0 -737
  357. package/.next/static/chunks/0a5iadnmt9u9k.js +0 -1
  358. package/.next/static/chunks/15sjx07xhpn6p.js +0 -1
  359. package/.next/static/chunks/160wjb73ddwqd.js +0 -6
  360. package/.next/static/chunks/1a74dky22dxd6.js +0 -1
  361. package/.next/static/chunks/1c_n0973t07l0.js +0 -2
  362. package/.next/static/chunks/1e1n0kkz-psdb.js +0 -1
  363. package/.next/static/chunks/236f0jtp68j_h.js +0 -1
  364. package/.next/static/chunks/297e1z34yi--z.js +0 -1
  365. package/.next/static/chunks/2_ak6qjxb6oa5.js +0 -1
  366. package/.next/static/chunks/2kfhc58df2xyo.js +0 -1
  367. package/.next/static/chunks/39hr8d-b2ibtw.js +0 -31
  368. package/.next/static/chunks/3ui4d-uqkqj5d.js +0 -2
  369. package/.next/static/chunks/3zw8bv7-mc4n6.js +0 -33
  370. /package/.next/standalone/.next/static/{jqE2D3zWqSGz5su948Azf → I-cj79yE2IZ8taX2dZGp3}/_buildManifest.js +0 -0
  371. /package/.next/standalone/.next/static/{jqE2D3zWqSGz5su948Azf → I-cj79yE2IZ8taX2dZGp3}/_clientMiddlewareManifest.js +0 -0
  372. /package/.next/standalone/.next/static/{jqE2D3zWqSGz5su948Azf → I-cj79yE2IZ8taX2dZGp3}/_ssgManifest.js +0 -0
  373. /package/.next/static/{jqE2D3zWqSGz5su948Azf → I-cj79yE2IZ8taX2dZGp3}/_buildManifest.js +0 -0
  374. /package/.next/static/{jqE2D3zWqSGz5su948Azf → I-cj79yE2IZ8taX2dZGp3}/_clientMiddlewareManifest.js +0 -0
  375. /package/.next/static/{jqE2D3zWqSGz5su948Azf → I-cj79yE2IZ8taX2dZGp3}/_ssgManifest.js +0 -0
@@ -7,7 +7,7 @@
7
7
  // which isn't. Both follow useSyncExternalStore's rules: a snapshot only changes
8
8
  // when something in it does.
9
9
 
10
- import { keyFor } from './separation.js';
10
+ import { keyFor, score } from './separation.js';
11
11
  import { warmthOf } from './linkedin-export.js';
12
12
 
13
13
  const KEY = 'six-degrees-galaxy-lab';
@@ -15,7 +15,7 @@ const LAYOUTS_KEY = 'six-degrees-galaxy-layouts';
15
15
 
16
16
  /** The layout as it has always been. Every slider is a multiple of today's value, or 0 for off. */
17
17
  export const LAB_DEFAULTS = Object.freeze({
18
- on: false, // the lab is on: without it, the Galaxy ignores everything below
18
+ on: true, // always: the sliders are the Galaxy's own controls now (it was an experimental switch)
19
19
  gravity: 0, // a pull on every dot towards you, in the middle
20
20
  rings: 1, // how hard each tier (or degree) holds its ring; 0 lets it find its own shape
21
21
  push: 15, // how hard dots push each other apart
@@ -25,11 +25,12 @@ export const LAB_DEFAULTS = Object.freeze({
25
25
  lines: 1,
26
26
  sizeBy: 'power', // 'power' (power score) or 'reach' (how many hang off them)
27
27
  names: 'key', // 'key' (you, S tier, catalysts) or 'all' (all your connections)
28
- branch: true, // hovering a dot lights up its branch and dims the rest
29
- colourBy: 'tier', // 'tier', 'degree', 'company' or 'warmth' (from the Social tab)
28
+ branch: false, // hovering a dot lights up its branch and dims the rest: off to start (Blake, 2026-10-02: it glitched)
29
+ colourBy: 'tier', // 'tier', 'heat' (power as a thermal map), 'degree', 'company' or 'warmth' (from the Social tab)
30
30
  speed: 15, // a replay's length, in seconds
31
31
  loop: false, // start the replay again when it ends
32
32
  labels: true, // names on the Galaxy at all: the Filter panel's switch, honoured with the lab off too
33
+ v: 2, // which defaults a saved setting was made under (2: the branch starts off)
33
34
  });
34
35
 
35
36
  /** The slider settings a saved layout keeps (not whether the lab is on, nor the names switch). */
@@ -38,13 +39,33 @@ const LAYOUT_KEYS = ['gravity', 'rings', 'push', 'pull', 'distance', 'dotSize',
38
39
  /** Let the layout find its own shape: no rings, each connection's circle bunched round them. */
39
40
  export const CLUSTERS = Object.freeze({ gravity: 0.25, rings: 0, push: 25, pull: 2.5, distance: 0.35, sizeBy: 'reach' });
40
41
 
42
+ /**
43
+ * Orbit, the old view, as a layout of the Galaxy (Blake, 2026-10-02: "add that as a
44
+ * visual option instead so the person can interact with it"): every tier held hard
45
+ * on its orbit, and each connection's circle drawn in close behind them. It's still
46
+ * the Galaxy, so dragging and every slider work on it.
47
+ */
48
+ export const ORBIT = Object.freeze({ gravity: 0, rings: 3, push: 6, pull: 4, distance: 0.22, sizeBy: 'power' });
49
+
50
+ /** Which layout the sliders are set to: 'rings', 'clusters', 'orbit', or null for your own. */
51
+ export function layoutOf(lab) {
52
+ const is = (preset) => Object.entries(preset).every(([k, v]) => lab[k] === v);
53
+ if (['gravity', 'rings', 'push', 'pull', 'distance'].every((k) => lab[k] === LAB_DEFAULTS[k])) return 'rings';
54
+ if (is(CLUSTERS)) return 'clusters';
55
+ if (is(ORBIT)) return 'orbit';
56
+ return null;
57
+ }
58
+
41
59
  const listeners = new Set();
42
60
  let cache = null;
43
61
 
44
62
  function read() {
45
63
  try {
46
64
  const raw = JSON.parse(localStorage.getItem(KEY) || 'null');
47
- return raw && typeof raw === 'object' ? { ...LAB_DEFAULTS, ...raw } : LAB_DEFAULTS;
65
+ // `on` was a switch once; a setting saved back then mustn't turn the sliders off.
66
+ // Saved before v2, the branch light-up was on by default; it starts off now.
67
+ if (!raw || typeof raw !== 'object') return LAB_DEFAULTS;
68
+ return { ...LAB_DEFAULTS, ...raw, ...(raw.v === LAB_DEFAULTS.v ? {} : { branch: false }), v: LAB_DEFAULTS.v, on: true };
48
69
  } catch { return LAB_DEFAULTS; }
49
70
  }
50
71
 
@@ -54,7 +75,7 @@ export function labNow() {
54
75
  }
55
76
 
56
77
  export function setLab(patch) {
57
- cache = patch === null ? { ...LAB_DEFAULTS, on: labNow().on, labels: labNow().labels } : { ...labNow(), ...patch };
78
+ cache = patch === null ? { ...LAB_DEFAULTS, labels: labNow().labels } : { ...labNow(), ...patch, on: true };
58
79
  try { localStorage.setItem(KEY, JSON.stringify(cache)); } catch {}
59
80
  listeners.forEach((f) => f());
60
81
  }
@@ -249,6 +270,36 @@ const DEGREE_COLOURS = { 1: '#4FC3F7', 2: '#BA68C8', 3: '#FFB74D' };
249
270
  const COMPANY_COLOURS = ['#FF6B6B', '#4ECDC4', '#FFD93D', '#A29BFE', '#00B894', '#FD79A8', '#74B9FF', '#E17055'];
250
271
  const WARMTH_COLOURS = { warm: '#FF7043', cool: '#4FC3F7', dormant: '#8D6E63', never: '#546E7A' };
251
272
  const OTHER = '#4a5568';
273
+
274
+ // Heat (Blake, 2026-10-02: "a thermal visual of power based on the dots"): a
275
+ // thermal camera's colours, cold violet through red and orange to white hot.
276
+ const HEAT_STOPS = ['#2b1b6b', '#6a1b9a', '#c2185b', '#f4511e', '#ffb300', '#fff3b0'];
277
+
278
+ /** The thermal colour at `t` (0 cold … 1 hottest). */
279
+ export function heatColour(t) {
280
+ const x = Math.max(0, Math.min(1, Number(t) || 0)) * (HEAT_STOPS.length - 1);
281
+ const i = Math.min(HEAT_STOPS.length - 2, Math.floor(x));
282
+ const f = x - i;
283
+ const a = HEAT_STOPS[i], b = HEAT_STOPS[i + 1];
284
+ const mix = (k) => Math.round(parseInt(a.slice(k, k + 2), 16) * (1 - f) + parseInt(b.slice(k, k + 2), 16) * f);
285
+ return `#${[1, 3, 5].map((k) => mix(k).toString(16).padStart(2, '0')).join('')}`;
286
+ }
287
+
288
+ /**
289
+ * Everyone's heat, 0 to 1: where their power score ranks among the people shown,
290
+ * so the colours spread across whoever is on screen (a network of mostly B tier
291
+ * still has its hot spots). Equal scores share a heat.
292
+ */
293
+ export function heatOf(people = []) {
294
+ return heatBy(people, score);
295
+ }
296
+
297
+ /** The same for anything with a value: each one's heat, 0 to 1, from where its value ranks. */
298
+ export function heatBy(items = [], valueOf = () => 0) {
299
+ const values = [...new Set(items.map(valueOf))].sort((a, b) => a - b);
300
+ const at = new Map(values.map((v, i) => [v, values.length > 1 ? i / (values.length - 1) : 1]));
301
+ return (d) => at.get(valueOf(d)) ?? 0;
302
+ }
252
303
  const companyKey = (c) => String(c || '').trim().toLowerCase();
253
304
 
254
305
  /**
@@ -280,6 +331,14 @@ export function colourScheme(mode, people, tierColors, social) {
280
331
  legend: [...top.map(([, c], i) => [c.name, COMPANY_COLOURS[i]]), ['Other', OTHER]],
281
332
  };
282
333
  }
334
+ if (mode === 'heat') {
335
+ const heat = heatOf(people);
336
+ return {
337
+ of: (d) => heatColour(heat(d)),
338
+ heat,
339
+ legend: [['Cold', heatColour(0.1)], ['Warm', heatColour(0.45)], ['Hot', heatColour(0.75)], ['Hottest', heatColour(1)]],
340
+ };
341
+ }
283
342
  if (mode === 'warmth' && social) {
284
343
  return {
285
344
  of: (d) => ((d.degree || 1) === 1 ? WARMTH_COLOURS[warmthOf(social.merged.get(keyFor(d)), social.asOf)] : '#2d3748'),
@@ -103,6 +103,8 @@ export function refreshNotifications({ added = [], checked = 0, tierOf = () => n
103
103
  out.push({
104
104
  user_id, type: 'new_elite_connection', title: `High-value connection: ${r.name}`,
105
105
  message: r.headline?.substring(0, 60), icon: '👑',
106
+ // Who it is, so the notification can open them (lib/notifications.js noteSubject).
107
+ data: { profileUrl: r.profile_url },
106
108
  });
107
109
  }
108
110
  return out;
@@ -34,3 +34,43 @@ export function clearActivity(id) {
34
34
  activities = activities.filter((x) => x.id !== id);
35
35
  listeners.forEach((f) => f());
36
36
  }
37
+
38
+ // The notch's buttons: the views of the page you're on (Bridge Chains,
39
+ // Separation…), or any page's own sub-tabs. A page sets them while it's on
40
+ // screen and clears them when it leaves; the notch draws them under the header's
41
+ // tabs (Blake, 2026-10-02: "the notch… under the ui buttons at top so then we
42
+ // can use that for button selection too").
43
+ //
44
+ // setNotchTabs({ items: [{ key, label, icon?, title? }], current, onPick })
45
+ // setNotchTabs(null)
46
+ const tabListeners = new Set();
47
+ let notchTabs = null;
48
+
49
+ export const notchTabsNow = () => notchTabs;
50
+ export const noNotchTabs = () => null;
51
+
52
+ export function watchNotchTabs(f) {
53
+ tabListeners.add(f);
54
+ return () => tabListeners.delete(f);
55
+ }
56
+
57
+ // Whether the notch is on screen at all (tabs, a scan, or another job), so a
58
+ // page with a heading at the top can leave room for it only when it's there.
59
+ const shownListeners = new Set();
60
+ let notchShown = false;
61
+ export const notchShownNow = () => notchShown;
62
+ export const noNotch = () => false;
63
+ export function watchNotchShown(f) {
64
+ shownListeners.add(f);
65
+ return () => shownListeners.delete(f);
66
+ }
67
+ export function setNotchShown(shown) {
68
+ if (notchShown === Boolean(shown)) return;
69
+ notchShown = Boolean(shown);
70
+ shownListeners.forEach((f) => f());
71
+ }
72
+
73
+ export function setNotchTabs(tabs) {
74
+ notchTabs = tabs && Array.isArray(tabs.items) && tabs.items.length > 1 ? tabs : null;
75
+ tabListeners.forEach((f) => f());
76
+ }
@@ -14,8 +14,9 @@
14
14
  import { readFileSync, writeFileSync, renameSync, unlinkSync, existsSync, lstatSync } from 'node:fs';
15
15
  import path from 'node:path';
16
16
  import { durable } from './durable.js';
17
+ import { PACES, DEFAULT_PACE } from './scan-pace.js';
17
18
 
18
- export const DEFAULT_LIMITS = { daily: 50, monthly: 250, profiles: 50 };
19
+ export const DEFAULT_LIMITS = { daily: 50, monthly: 250, profiles: 50, pace: DEFAULT_PACE };
19
20
  export const DAILY_CHOICES = [25, 50, 100, 200, 500];
20
21
  export const MONTHLY_CHOICES = [100, 250, 500, 1000, 0]; // 0 = no monthly cap (Premium)
21
22
  export const PROFILE_CHOICES = [10, 25, 50, 100]; // profile views a day; never unlimited
@@ -71,6 +72,8 @@ export function readLimits(dir) {
71
72
  // default. A number typed in by hand counts as the largest choice under it,
72
73
  // as the scanner reads it (scrape.py search_limits).
73
74
  profiles: profileChoice(num(raw.profiles, 0) || DEFAULT_LIMITS.profiles),
75
+ // The Scan page's speed (lib/scan-pace.js): how fast a budget is used, never how big it is.
76
+ pace: PACES[raw.pace] ? raw.pace : DEFAULT_PACE,
74
77
  };
75
78
  }
76
79
 
@@ -79,12 +82,13 @@ export function readLimits(dir) {
79
82
  * scanner. A value not given keeps what is saved: every key is written back,
80
83
  * or saving one would drop the others to their defaults.
81
84
  */
82
- export function writeLimits(dir, { daily, monthly, profiles } = {}) {
85
+ export function writeLimits(dir, { daily, monthly, profiles, pace } = {}) {
83
86
  const cur = readLimits(dir);
84
87
  const next = {
85
88
  daily: DAILY_CHOICES.includes(daily) ? daily : cur.daily,
86
89
  monthly: MONTHLY_CHOICES.includes(monthly) ? monthly : cur.monthly,
87
90
  profiles: PROFILE_CHOICES.includes(profiles) ? profiles : cur.profiles,
91
+ pace: PACES[pace] ? pace : cur.pace,
88
92
  };
89
93
  writeJsonAtomic(path.join(dir, 'scan-limits.json'), next);
90
94
  return next;
@@ -0,0 +1,20 @@
1
+ // What "hot" means on Paths' two maps (Blake, 2026-10-02: "change color heat
2
+ // mapping" on Paths, as on Network Circle). A bubble's size already says how
3
+ // many; heat says how strong, so the two never repeat each other.
4
+
5
+ import { score } from './separation.js';
6
+
7
+ /** A company's strength: the average power score of its five strongest people shown. */
8
+ export function companyStrength(people = []) {
9
+ const top = people.map(score).sort((a, b) => b - a).slice(0, 5);
10
+ return top.length ? top.reduce((s, v) => s + v, 0) / top.length : 0;
11
+ }
12
+
13
+ /**
14
+ * How strong a connection's cluster is, person for person: S counts 3, A 2,
15
+ * everyone else 1, over the cluster's size (1 to 3). Not scanned: -1, the coldest.
16
+ */
17
+ export function clusterStrength({ size = 0, S = 0, A = 0 } = {}) {
18
+ if (!size) return -1;
19
+ return (3 * S + 2 * A + Math.max(0, size - S - A)) / size;
20
+ }
@@ -1,4 +1,4 @@
1
- // Network health for the Scores tab (item 41, Graph Study §8.5/§10.2), from
1
+ // Network health for Profile → Insights (item 41, Graph Study §8.5/§10.2), from
2
2
  // what the app already keeps. No percentiles against other people: there is no
3
3
  // data to compare with, so every number is about your own network.
4
4
  //
@@ -0,0 +1,121 @@
1
+ // Notifications worth opening (Blake, 2026-10-02: "the notifications should be
2
+ // able to be clicked and … take over the right panel and show a more detailed
3
+ // version … what person added back from who and that they are a value person
4
+ // if the score is S … a new person added back, or we got a person from a
5
+ // high-ranking company, or that our scan is done").
6
+ //
7
+ // Each notification carries `data` naming who it is about, so the right panel
8
+ // can show them: { personId?, profileUrl?, viaId?, personIds?, action? }.
9
+ // Older notifications have no data; noteSubject finds their person by the name
10
+ // in the title. Plain functions, no React; tests/notifications.test.mjs.
11
+
12
+ const HIGH = new Set(['S', 'A']);
13
+ // A company's own score (company_prestige_score, lib/scoring.js) from here up is "a top company".
14
+ export const TOP_COMPANY = 8;
15
+
16
+ const score1 = (row) => {
17
+ const n = parseFloat(row?.power_score);
18
+ return Number.isFinite(n) ? n.toFixed(1) : null;
19
+ };
20
+
21
+ /** Someone you met through a bridge who is now one of your connections. */
22
+ export function addedBackNotification({ person, via, userId = null }) {
23
+ if (!person) return null;
24
+ const tier = person.tier || null;
25
+ const valued = HIGH.has(tier);
26
+ return {
27
+ user_id: userId || null,
28
+ type: 'added_back',
29
+ title: `${person.name || 'Someone'} added you back${via?.name ? `, through ${via.name}` : ''}`,
30
+ message: [tier ? `${tier}-tier${score1(person) ? ` · ${score1(person)}` : ''}` : null, valued ? 'a valuable person to know' : null, person.headline || null]
31
+ .filter(Boolean).join(' · ').slice(0, 140),
32
+ icon: valued ? '👑' : '🤝',
33
+ data: { personId: person.id, viaId: via?.id ?? null, tier },
34
+ };
35
+ }
36
+
37
+ /** People a circle scan found at top companies, as one notification. */
38
+ export function topCompanyNotification({ people = [], via = null, userId = null, threshold = TOP_COMPANY }) {
39
+ const top = people.filter((p) => Number(p?.company_prestige_score) >= threshold)
40
+ .sort((a, b) => Number(b.company_prestige_score) - Number(a.company_prestige_score) || (parseFloat(b.power_score) || 0) - (parseFloat(a.power_score) || 0));
41
+ if (!top.length) return null;
42
+ const companies = [...new Set(top.map((p) => p.company).filter(Boolean))];
43
+ return {
44
+ user_id: userId || null,
45
+ type: 'top_company',
46
+ title: `${top.length} ${top.length === 1 ? 'person' : 'people'} at top companies${via?.name ? ` in ${via.name}’s circle` : ''}`,
47
+ message: companies.slice(0, 3).join(', ') + (companies.length > 3 ? ` +${companies.length - 3} more` : ''),
48
+ icon: '🏢',
49
+ data: { personIds: top.slice(0, 25).map((p) => p.id), personId: top[0].id, viaId: via?.id ?? null },
50
+ };
51
+ }
52
+
53
+ // What each scanner job is called when it's done. Setting up, signing in and
54
+ // saving settings aren't scans, so they leave nothing.
55
+ const SCAN_DONE = {
56
+ bridge: (t) => `Scan done: ${t?.name ? `${t.name}’s circle` : 'their circle'}`,
57
+ resume: (t) => `Scan done: ${t?.name ? `${t.name}’s circle` : 'their circle'}`,
58
+ rescrape: (t) => `Scan done: ${t?.name ? `${t.name}’s circle` : 'their circle'}`,
59
+ 'auto-bridge': () => 'Mapping the 2nd degree: done for now',
60
+ 'auto-bridge-retry': () => 'Retrying hidden lists: done',
61
+ 'resume-all': () => 'Every paused circle: done for now',
62
+ full: () => 'Your whole network is scanned',
63
+ refresh: () => 'Check for new: done',
64
+ company: (t) => `Company scan done${t?.name ? `: ${t.name}` : ''}`,
65
+ messages: () => 'Messages synced',
66
+ };
67
+
68
+ /** The notification a finished scan leaves, or null (not a scan, stopped, or it failed). */
69
+ export function scanDoneNotification({ action, target = null, exitCode, stopped = false, log = [], userId = null }) {
70
+ const name = SCAN_DONE[action];
71
+ if (!name || stopped || exitCode !== 0) return null;
72
+ // The last thing the scanner said that sums it up ("Done. 5/5 bridges mapped.").
73
+ const last = [...log].reverse().map((l) => String(l).trim()).find((l) => l && l !== 'Finished.' && !/^Speed:/.test(l)) || '';
74
+ return {
75
+ user_id: userId || null,
76
+ type: 'scan_done',
77
+ title: name(target),
78
+ message: last.slice(0, 140),
79
+ icon: '📡',
80
+ data: { action, personId: target?.id ?? null },
81
+ };
82
+ }
83
+
84
+ // Older notifications name their person only in the title.
85
+ const NAME_IN_TITLE = [
86
+ [/^(.+?) accepted!/, 1],
87
+ [/^High-value connection: (.+)$/, 1],
88
+ [/^Catalyst discovered: (.+)$/, 1],
89
+ [/^(.+?) added you back/, 1],
90
+ ];
91
+
92
+ /**
93
+ * Who a notification is about, from the rows on screen: { person, via, people }.
94
+ * `via` is who you met them through: the notification's own, else the person's
95
+ * recorded introducer. `people` are everyone it names (a top-company list).
96
+ */
97
+ export function noteSubject(note, connections = [], degree2 = []) {
98
+ const all = [...(connections || []), ...(degree2 || [])];
99
+ const byId = new Map(all.map((r) => [r.id, r]));
100
+ const data = typeof note?.data === 'string' ? safeJson(note.data) : note?.data || {};
101
+ let person = data.personId != null ? byId.get(data.personId) : null;
102
+ if (!person && data.profileUrl) person = all.find((r) => r.profile_url === data.profileUrl) || null;
103
+ if (!person) {
104
+ for (const [re, i] of NAME_IN_TITLE) {
105
+ const m = String(note?.title || '').match(re);
106
+ if (!m) continue;
107
+ const name = m[i].trim().toLowerCase();
108
+ person = (connections || []).find((r) => String(r.name || '').toLowerCase() === name)
109
+ || (degree2 || []).find((r) => String(r.name || '').toLowerCase() === name) || null;
110
+ if (person) break;
111
+ }
112
+ }
113
+ const viaId = data.viaId ?? person?.unlocked_from_bridge_id ?? (person?.degree === 2 ? person.source_connection_id : null);
114
+ const via = viaId != null ? byId.get(viaId) || null : null;
115
+ const people = (data.personIds || []).map((id) => byId.get(id)).filter(Boolean);
116
+ return { person: person || null, via, people, data };
117
+ }
118
+
119
+ function safeJson(s) {
120
+ try { return JSON.parse(s); } catch { return {}; }
121
+ }
@@ -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
+ }