six-degrees 0.3.0 → 0.4.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 (427) 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 +35 -7
  5. package/.next/standalone/.next/required-server-files.json +6 -1
  6. package/.next/standalone/.next/routes-manifest.json +18 -1
  7. package/.next/standalone/.next/server/app/_global-error/page.js.nft.json +1 -1
  8. package/.next/standalone/.next/server/app/_global-error/page_client-reference-manifest.js +1 -1
  9. package/.next/standalone/.next/server/app/_global-error.html +1 -1
  10. package/.next/standalone/.next/server/app/_global-error.rsc +1 -1
  11. package/.next/standalone/.next/server/app/_global-error.segments/__PAGE__.segment.rsc +1 -1
  12. package/.next/standalone/.next/server/app/_global-error.segments/_full.segment.rsc +1 -1
  13. package/.next/standalone/.next/server/app/_global-error.segments/_tree.segment.rsc +1 -1
  14. package/.next/standalone/.next/server/app/_not-found/page.js.nft.json +1 -1
  15. package/.next/standalone/.next/server/app/_not-found/page_client-reference-manifest.js +1 -1
  16. package/.next/standalone/.next/server/app/_not-found.html +1 -1
  17. package/.next/standalone/.next/server/app/_not-found.rsc +11 -11
  18. package/.next/standalone/.next/server/app/_not-found.segments/_full.segment.rsc +11 -11
  19. package/.next/standalone/.next/server/app/_not-found.segments/_not-found/__PAGE__.segment.rsc +10 -10
  20. package/.next/standalone/.next/server/app/_not-found.segments/_tree.segment.rsc +2 -2
  21. package/.next/standalone/.next/server/app/api/admin-delete/route.js +1 -1
  22. package/.next/standalone/.next/server/app/api/admin-delete/route.js.nft.json +1 -1
  23. package/.next/standalone/.next/server/app/api/admin-update/route.js +1 -1
  24. package/.next/standalone/.next/server/app/api/admin-update/route.js.nft.json +1 -1
  25. package/.next/standalone/.next/server/app/api/bridges/route.js +1 -1
  26. package/.next/standalone/.next/server/app/api/bridges/route.js.nft.json +1 -1
  27. package/.next/standalone/.next/server/app/api/bulk-import/route.js +1 -1
  28. package/.next/standalone/.next/server/app/api/bulk-import/route.js.nft.json +1 -1
  29. package/.next/standalone/.next/server/app/api/company-scores/legacy/route.js.nft.json +1 -1
  30. package/.next/standalone/.next/server/app/api/company-scores/route.js.nft.json +1 -1
  31. package/.next/standalone/.next/server/app/api/connections/route.js +1 -1
  32. package/.next/standalone/.next/server/app/api/connections/route.js.nft.json +1 -1
  33. package/.next/standalone/.next/server/app/api/data/export/route.js.nft.json +1 -1
  34. package/.next/standalone/.next/server/app/api/data/import/route.js.nft.json +1 -1
  35. package/.next/standalone/.next/server/app/api/data/restart/route.js.nft.json +1 -1
  36. package/.next/standalone/.next/server/app/api/data/reveal/route.js.nft.json +1 -1
  37. package/.next/standalone/.next/server/app/api/data/route.js.nft.json +1 -1
  38. package/.next/standalone/.next/server/app/api/delete-cluster/route.js +1 -1
  39. package/.next/standalone/.next/server/app/api/delete-cluster/route.js.nft.json +1 -1
  40. package/.next/standalone/.next/server/app/api/ingest/route.js +1 -1
  41. package/.next/standalone/.next/server/app/api/ingest/route.js.nft.json +1 -1
  42. package/.next/standalone/.next/server/app/api/network/route.js +1 -1
  43. package/.next/standalone/.next/server/app/api/network/route.js.nft.json +1 -1
  44. package/.next/standalone/.next/server/app/api/notifications/route.js +1 -1
  45. package/.next/standalone/.next/server/app/api/notifications/route.js.nft.json +1 -1
  46. package/.next/standalone/.next/server/app/api/outreach/route.js +3 -3
  47. package/.next/standalone/.next/server/app/api/outreach/route.js.nft.json +1 -1
  48. package/.next/standalone/.next/server/app/api/queue/route.js +1 -1
  49. package/.next/standalone/.next/server/app/api/queue/route.js.nft.json +1 -1
  50. package/.next/standalone/.next/server/app/api/scraper/route.js +1 -1
  51. package/.next/standalone/.next/server/app/api/scraper/route.js.nft.json +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.nft.json +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/unlock/route.js +1 -1
  58. package/.next/standalone/.next/server/app/api/unlock/route.js.nft.json +1 -1
  59. package/.next/standalone/.next/server/app/api/update/route.js.nft.json +1 -1
  60. package/.next/standalone/.next/server/app/api/update-images/route.js +1 -1
  61. package/.next/standalone/.next/server/app/api/update-images/route.js.nft.json +1 -1
  62. package/.next/standalone/.next/server/app/api/users/route.js +1 -1
  63. package/.next/standalone/.next/server/app/api/users/route.js.nft.json +1 -1
  64. package/.next/standalone/.next/server/app/avatars/[file]/route.js.nft.json +1 -1
  65. package/.next/standalone/.next/server/app/import/page.js.nft.json +1 -1
  66. package/.next/standalone/.next/server/app/import/page_client-reference-manifest.js +1 -1
  67. package/.next/standalone/.next/server/app/import.html +1 -1
  68. package/.next/standalone/.next/server/app/import.rsc +13 -13
  69. package/.next/standalone/.next/server/app/import.segments/_full.segment.rsc +13 -13
  70. package/.next/standalone/.next/server/app/import.segments/_tree.segment.rsc +2 -2
  71. package/.next/standalone/.next/server/app/import.segments/import/__PAGE__.segment.rsc +12 -12
  72. package/.next/standalone/.next/server/app/index.html +1 -1
  73. package/.next/standalone/.next/server/app/index.rsc +13 -13
  74. package/.next/standalone/.next/server/app/index.segments/__PAGE__.segment.rsc +12 -12
  75. package/.next/standalone/.next/server/app/index.segments/_full.segment.rsc +13 -13
  76. package/.next/standalone/.next/server/app/index.segments/_tree.segment.rsc +2 -2
  77. package/.next/standalone/.next/server/app/launch/page.js.nft.json +1 -1
  78. package/.next/standalone/.next/server/app/launch/page_client-reference-manifest.js +1 -1
  79. package/.next/standalone/.next/server/app/launch.html +2 -2
  80. package/.next/standalone/.next/server/app/launch.rsc +13 -13
  81. package/.next/standalone/.next/server/app/launch.segments/_full.segment.rsc +13 -13
  82. package/.next/standalone/.next/server/app/launch.segments/_tree.segment.rsc +2 -2
  83. package/.next/standalone/.next/server/app/launch.segments/launch/__PAGE__.segment.rsc +12 -12
  84. package/.next/standalone/.next/server/app/page.js.nft.json +1 -1
  85. package/.next/standalone/.next/server/app/page_client-reference-manifest.js +1 -1
  86. package/.next/standalone/.next/server/app/paths/page.js.nft.json +1 -1
  87. package/.next/standalone/.next/server/app/paths/page_client-reference-manifest.js +1 -1
  88. package/.next/standalone/.next/server/app/paths.html +1 -1
  89. package/.next/standalone/.next/server/app/paths.rsc +13 -13
  90. package/.next/standalone/.next/server/app/paths.segments/_full.segment.rsc +13 -13
  91. package/.next/standalone/.next/server/app/paths.segments/_tree.segment.rsc +2 -2
  92. package/.next/standalone/.next/server/app/paths.segments/paths/__PAGE__.segment.rsc +12 -12
  93. package/.next/standalone/.next/server/app/profile/page.js.nft.json +1 -1
  94. package/.next/standalone/.next/server/app/profile/page_client-reference-manifest.js +1 -1
  95. package/.next/standalone/.next/server/app/profile.html +1 -1
  96. package/.next/standalone/.next/server/app/profile.rsc +13 -13
  97. package/.next/standalone/.next/server/app/profile.segments/_full.segment.rsc +13 -13
  98. package/.next/standalone/.next/server/app/profile.segments/_tree.segment.rsc +2 -2
  99. package/.next/standalone/.next/server/app/profile.segments/profile/__PAGE__.segment.rsc +12 -12
  100. package/.next/standalone/.next/server/app/queue/page.js.nft.json +1 -1
  101. package/.next/standalone/.next/server/app/queue/page_client-reference-manifest.js +1 -1
  102. package/.next/standalone/.next/server/app/queue.html +1 -1
  103. package/.next/standalone/.next/server/app/queue.rsc +13 -13
  104. package/.next/standalone/.next/server/app/queue.segments/_full.segment.rsc +13 -13
  105. package/.next/standalone/.next/server/app/queue.segments/_tree.segment.rsc +2 -2
  106. package/.next/standalone/.next/server/app/queue.segments/queue/__PAGE__.segment.rsc +12 -12
  107. package/.next/standalone/.next/server/app/scores/page/app-paths-manifest.json +3 -0
  108. package/.next/standalone/.next/server/app/scores/page/build-manifest.json +20 -0
  109. package/.next/standalone/.next/server/app/scores/page/next-font-manifest.json +11 -0
  110. package/.next/standalone/.next/server/app/scores/page/react-loadable-manifest.json +1 -0
  111. package/.next/standalone/.next/server/app/scores/page/server-reference-manifest.json +4 -0
  112. package/.next/standalone/.next/server/app/scores/page.js +13 -0
  113. package/.next/standalone/.next/server/app/scores/page.js.map +5 -0
  114. package/.next/standalone/.next/server/app/scores/page.js.nft.json +1 -0
  115. package/.next/standalone/.next/server/app/scores/page_client-reference-manifest.js +3 -0
  116. package/.next/standalone/.next/server/app/scores.html +1 -0
  117. package/.next/standalone/.next/server/app/scores.meta +26 -0
  118. package/.next/standalone/.next/server/app/scores.rsc +24 -0
  119. package/.next/standalone/.next/server/app/scores.segments/_full.segment.rsc +24 -0
  120. package/.next/standalone/.next/server/app/scores.segments/_tree.segment.rsc +4 -0
  121. package/.next/standalone/.next/server/app/scores.segments/scores/__PAGE__.segment.rsc +28 -0
  122. package/.next/standalone/.next/server/app/settings/page.js.nft.json +1 -1
  123. package/.next/standalone/.next/server/app/settings/page_client-reference-manifest.js +1 -1
  124. package/.next/standalone/.next/server/app/settings.html +1 -1
  125. package/.next/standalone/.next/server/app/settings.rsc +13 -13
  126. package/.next/standalone/.next/server/app/settings.segments/_full.segment.rsc +13 -13
  127. package/.next/standalone/.next/server/app/settings.segments/_tree.segment.rsc +2 -2
  128. package/.next/standalone/.next/server/app/settings.segments/settings/__PAGE__.segment.rsc +12 -12
  129. package/.next/standalone/.next/server/app/setup/page.js.nft.json +1 -1
  130. package/.next/standalone/.next/server/app/setup/page_client-reference-manifest.js +1 -1
  131. package/.next/standalone/.next/server/app/setup.html +1 -1
  132. package/.next/standalone/.next/server/app/setup.rsc +13 -13
  133. package/.next/standalone/.next/server/app/setup.segments/_full.segment.rsc +13 -13
  134. package/.next/standalone/.next/server/app/setup.segments/_tree.segment.rsc +2 -2
  135. package/.next/standalone/.next/server/app/setup.segments/setup/__PAGE__.segment.rsc +12 -12
  136. package/.next/standalone/.next/server/app-paths-manifest.json +1 -0
  137. package/.next/standalone/.next/server/chunks/[root-of-the-server]__0-rnla2._.js +1 -1
  138. package/.next/standalone/.next/server/chunks/[root-of-the-server]__00la_y0._.js +1 -1
  139. package/.next/standalone/.next/server/chunks/{[root-of-the-server]__0i5hdu4._.js → [root-of-the-server]__03ydxxh._.js} +2 -2
  140. package/.next/standalone/.next/server/chunks/[root-of-the-server]__07t47cm._.js +1 -1
  141. package/.next/standalone/.next/server/chunks/[root-of-the-server]__0bat8nx._.js +8 -0
  142. package/.next/standalone/.next/server/chunks/[root-of-the-server]__0cj5jqc._.js +1 -1
  143. package/.next/standalone/.next/server/chunks/[root-of-the-server]__0e0fei3._.js +1 -1
  144. package/.next/standalone/.next/server/chunks/[root-of-the-server]__0ljcioj._.js +1 -1
  145. package/.next/standalone/.next/server/chunks/[root-of-the-server]__0ljnyho._.js +1 -1
  146. package/.next/standalone/.next/server/chunks/[root-of-the-server]__0o5-tcg._.js +1 -1
  147. package/.next/standalone/.next/server/chunks/[root-of-the-server]__0qrnous._.js +1 -1
  148. package/.next/standalone/.next/server/chunks/[root-of-the-server]__0t3v1a1._.js +1 -1
  149. package/.next/standalone/.next/server/chunks/[root-of-the-server]__0uag-k5._.js +1 -1
  150. package/.next/standalone/.next/server/chunks/[root-of-the-server]__0um1uey._.js +1 -1
  151. package/.next/standalone/.next/server/chunks/[root-of-the-server]__0wmexuc._.js +1 -1
  152. package/.next/standalone/.next/server/chunks/[root-of-the-server]__0xu34sz._.js +1 -1
  153. package/.next/standalone/.next/server/chunks/[root-of-the-server]__133knx1._.js +1 -1
  154. package/.next/standalone/.next/server/chunks/[root-of-the-server]__14ou7j5._.js +1 -1
  155. package/.next/standalone/.next/server/chunks/[root-of-the-server]__15-17hl._.js +1 -1
  156. package/.next/standalone/.next/server/chunks/[root-of-the-server]__16iu0hv._.js +1 -1
  157. package/.next/standalone/.next/server/chunks/[root-of-the-server]__19b7mig._.js +1 -1
  158. package/.next/standalone/.next/server/chunks/[root-of-the-server]__1bbcwub._.js +2 -2
  159. package/.next/standalone/.next/server/chunks/[root-of-the-server]__1eris29._.js +1 -1
  160. package/.next/standalone/.next/server/chunks/[root-of-the-server]__1gjo_cq._.js +1 -1
  161. package/.next/standalone/.next/server/chunks/[root-of-the-server]__1kbl21f._.js +1 -1
  162. package/.next/standalone/.next/server/chunks/[root-of-the-server]__1qcm08_._.js +1 -1
  163. package/.next/standalone/.next/server/chunks/[root-of-the-server]__1rphsdk._.js +1 -1
  164. package/.next/standalone/.next/server/chunks/[root-of-the-server]__1w25q0m._.js +1 -1
  165. package/.next/standalone/.next/server/chunks/_02twlca._.js +10 -0
  166. package/.next/standalone/.next/server/chunks/_072jl0y._.js +1 -1
  167. package/.next/standalone/.next/server/chunks/_0lw8_i0._.js +1 -1
  168. package/.next/standalone/.next/server/chunks/_1_fynia._.js +8 -6
  169. package/.next/standalone/.next/server/chunks/_1fn30_l._.js +1 -1
  170. package/.next/standalone/.next/server/chunks/_1itd1o_._.js +1 -1
  171. package/.next/standalone/.next/server/chunks/_1iwkh8t._.js +1 -1
  172. package/.next/standalone/.next/server/chunks/lib_db-client_15le7cw.js +13 -8
  173. package/.next/standalone/.next/server/chunks/{lib_rpc_02ndsg2.js → lib_rpc_03i8xef.js} +1 -1
  174. package/.next/standalone/.next/server/chunks/{lib_rpc_08q5vp6.js → lib_rpc_08fc3rp.js} +1 -1
  175. package/.next/standalone/.next/server/chunks/{lib_rpc_00tx2sm.js → lib_rpc_08r2t_q.js} +1 -1
  176. package/.next/standalone/.next/server/chunks/{lib_rpc_02-qa4d.js → lib_rpc_0btpv1t.js} +1 -1
  177. package/.next/standalone/.next/server/chunks/lib_rpc_0c9d9ti.js +5 -5
  178. package/.next/standalone/.next/server/chunks/lib_rpc_0cm2ev3.js +3 -0
  179. package/.next/standalone/.next/server/chunks/lib_rpc_0iovswz.js +3 -0
  180. package/.next/standalone/.next/server/chunks/lib_rpc_0is8flp.js +3 -0
  181. package/.next/standalone/.next/server/chunks/lib_rpc_0v8ajtd.js +3 -0
  182. package/.next/standalone/.next/server/chunks/lib_rpc_0xgmvvt.js +3 -0
  183. package/.next/standalone/.next/server/chunks/lib_rpc_144mzbf.js +3 -0
  184. package/.next/standalone/.next/server/chunks/lib_rpc_1bggi0n.js +3 -0
  185. package/.next/standalone/.next/server/chunks/lib_rpc_1dkcu9f.js +3 -0
  186. package/.next/standalone/.next/server/chunks/lib_rpc_1o0jb6w.js +3 -0
  187. package/.next/standalone/.next/server/chunks/ssr/{[root-of-the-server]__0an6blh._.js → [root-of-the-server]__0zzcbqg._.js} +2 -2
  188. package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__1194jps._.js +3 -0
  189. package/.next/standalone/.next/server/chunks/ssr/{[root-of-the-server]__1z1j3ek._.js → [root-of-the-server]__19ip1qq._.js} +2 -2
  190. package/.next/standalone/.next/server/chunks/ssr/_01f38ty._.js +3 -0
  191. package/.next/standalone/.next/server/chunks/ssr/_06w_gl4._.js +3 -0
  192. package/.next/standalone/.next/server/chunks/ssr/_0hw_qnj._.js +3 -0
  193. package/.next/standalone/.next/server/chunks/ssr/_13kzrwv._.js +3 -0
  194. package/.next/standalone/.next/server/chunks/ssr/_15xuwkv._.js +3 -0
  195. package/.next/standalone/.next/server/chunks/ssr/_1cyxocj._.js +3 -0
  196. package/.next/standalone/.next/server/chunks/ssr/_1gpbbw9._.js +3 -0
  197. package/.next/standalone/.next/server/chunks/ssr/_1hgd5dp._.js +3 -0
  198. package/.next/standalone/.next/server/chunks/ssr/_1vpfyd-._.js +3 -0
  199. package/.next/standalone/.next/server/chunks/ssr/_20w06i-._.js +3 -0
  200. package/.next/standalone/.next/server/chunks/ssr/_next-internal_server_app_scores_page_actions_0x-w58n.js +3 -0
  201. package/.next/standalone/.next/server/chunks/ssr/app_components_settings_SectorPicker_1c-29qw.js +3 -0
  202. package/.next/standalone/.next/server/chunks/ssr/app_launch_page_19t0dyd.js +1 -1
  203. package/.next/standalone/.next/server/chunks/ssr/app_page_1fazqbf.js +11 -9
  204. package/.next/standalone/.next/server/chunks/ssr/app_paths_page_1ol90m7.js +4 -0
  205. package/.next/standalone/.next/server/chunks/ssr/app_profile_page_0u3-l5y.js +1 -1
  206. package/.next/standalone/.next/server/chunks/ssr/app_queue_page_0c6znd6.js +5 -5
  207. package/.next/standalone/.next/server/chunks/ssr/app_scores_page_09wepvv.js +3 -0
  208. package/.next/standalone/.next/server/chunks/ssr/app_settings_page_1f8mjj_.js +1 -1
  209. package/.next/standalone/.next/server/chunks/ssr/app_setup_page_1-282zx.js +2 -2
  210. package/.next/standalone/.next/server/chunks/ssr/lib_1bn38gs._.js +3 -0
  211. package/.next/standalone/.next/server/chunks/ssr/node_modules_next_0x3i8za._.js +131 -0
  212. package/.next/standalone/.next/server/chunks/ssr/{node_modules_next_dist_1bunoni._.js → node_modules_next_dist_1v8aef8._.js} +2 -2
  213. package/.next/standalone/.next/server/chunks/ssr/node_modules_next_dist_client_components_server-async-storage_0t1dato.js +3 -0
  214. package/.next/standalone/.next/server/middleware-build-manifest.js +3 -3
  215. package/.next/standalone/.next/server/middleware-manifest.json +5 -5
  216. package/.next/standalone/.next/server/next-font-manifest.js +1 -1
  217. package/.next/standalone/.next/server/next-font-manifest.json +4 -0
  218. package/.next/standalone/.next/server/pages/404.html +1 -1
  219. package/.next/standalone/.next/server/pages/500.html +1 -1
  220. package/.next/standalone/.next/server/prefetch-hints.json +14 -0
  221. package/.next/standalone/.next/server/server-reference-manifest.js +1 -1
  222. package/.next/standalone/.next/server/server-reference-manifest.json +1 -1
  223. package/.next/standalone/.next/static/chunks/0ax_9zzboyv6d.js +44 -0
  224. package/.next/standalone/.next/static/chunks/0cuuo2jwc9h94.js +1 -0
  225. package/.next/standalone/.next/static/chunks/0l1568qff0efj.js +1 -0
  226. package/.next/standalone/.next/static/chunks/0muwt9bykzr-1.js +1 -0
  227. package/.next/standalone/.next/static/chunks/0w76iraw7ij3k.js +18 -0
  228. package/.next/standalone/.next/static/chunks/135n87ydxjrra.js +2 -0
  229. package/.next/standalone/.next/static/chunks/18xu0fbgo_qz-.js +1 -0
  230. package/.next/standalone/.next/static/chunks/28bu26ausqzmn.js +1 -0
  231. package/.next/standalone/.next/static/chunks/{40nvbgfn7-072.css → 2p6o90z0vgcwr.css} +1 -1
  232. package/.next/standalone/.next/static/chunks/2pnbm1z6j7xab.js +1 -0
  233. package/.next/standalone/.next/static/chunks/2xjho0lroiwlq.js +1 -0
  234. package/.next/standalone/.next/static/chunks/3e8zg6ctvvb8p.js +33 -0
  235. package/.next/standalone/.next/static/chunks/3qh78g2aqhprd.js +2 -0
  236. package/.next/standalone/CHANGELOG.md +286 -0
  237. package/.next/standalone/CONTRIBUTING.md +15 -4
  238. package/.next/standalone/README.md +50 -29
  239. package/.next/standalone/SECURITY.md +10 -0
  240. package/.next/standalone/app/api/company-scores/legacy/route.js +1 -1
  241. package/.next/standalone/app/api/company-scores/route.js +1 -1
  242. package/.next/standalone/app/api/ingest/route.js +23 -2
  243. package/.next/standalone/app/api/outreach/route.js +107 -44
  244. package/.next/standalone/app/api/scraper/route.js +146 -17
  245. package/.next/standalone/app/api/settings/sector-preview/route.js +3 -2
  246. package/.next/standalone/app/api/settings/sector-suggestions/route.js +1 -1
  247. package/.next/standalone/app/api/update-images/route.js +41 -7
  248. package/.next/standalone/app/avatars/[file]/route.js +6 -1
  249. package/.next/standalone/app/components/Avatar.js +5 -2
  250. package/.next/standalone/app/components/ChainView.js +659 -469
  251. package/.next/standalone/app/components/CompanyScores.js +16 -10
  252. package/.next/standalone/app/components/EmptyState.js +1 -1
  253. package/.next/standalone/app/components/FieldStep.js +94 -0
  254. package/.next/standalone/app/components/ForceGraph.js +183 -768
  255. package/.next/standalone/app/components/GridView.js +9 -7
  256. package/.next/standalone/app/components/LegacyScoresCard.js +1 -1
  257. package/.next/standalone/app/components/LinkedInLimits.js +22 -7
  258. package/.next/standalone/app/components/ListView.js +6 -4
  259. package/.next/standalone/app/components/MappingProgress.js +5 -6
  260. package/.next/standalone/app/components/OnboardingGate.js +1 -1
  261. package/.next/standalone/app/components/OrbitGraph.js +33 -1
  262. package/.next/standalone/app/components/OutlinkQuest.js +5 -4
  263. package/.next/standalone/app/components/PathsAnalyzer.js +1 -1
  264. package/.next/standalone/app/components/RingsView.js +9 -8
  265. package/.next/standalone/app/components/SeparationView.js +96 -14
  266. package/.next/standalone/app/components/Sidebar.js +366 -287
  267. package/.next/standalone/app/components/TheirCircle.js +300 -0
  268. package/.next/standalone/app/components/settings/SectorPicker.js +202 -0
  269. package/.next/standalone/app/components/settings/SectorSection.js +21 -195
  270. package/.next/standalone/app/components/settings/TierSection.js +99 -0
  271. package/.next/standalone/app/components/useRequests.js +12 -0
  272. package/.next/standalone/app/components/useScanner.js +13 -0
  273. package/.next/standalone/app/components/views.js +3 -5
  274. package/.next/standalone/app/globals.css +7 -0
  275. package/.next/standalone/app/launch/page.js +6 -3
  276. package/.next/standalone/app/page.js +183 -74
  277. package/.next/standalone/app/paths/page.js +20 -14
  278. package/.next/standalone/app/profile/page.js +3 -3
  279. package/.next/standalone/app/queue/page.js +56 -60
  280. package/.next/standalone/app/scores/page.js +72 -0
  281. package/.next/standalone/app/settings/page.js +15 -3
  282. package/.next/standalone/app/setup/page.js +423 -260
  283. package/.next/standalone/db/schema.js +2 -1
  284. package/.next/standalone/eslint.config.mjs +3 -3
  285. package/.next/standalone/install.sh +3 -3
  286. package/.next/standalone/lib/chain-layout.js +139 -0
  287. package/.next/standalone/lib/circle.js +169 -0
  288. package/.next/standalone/lib/companies.js +1 -1
  289. package/.next/standalone/lib/csv.js +21 -10
  290. package/.next/standalone/lib/data-export.js +2 -1
  291. package/.next/standalone/lib/data-folder.js +4 -2
  292. package/.next/standalone/lib/db-client.js +1 -0
  293. package/.next/standalone/lib/galaxy.js +12 -0
  294. package/.next/standalone/lib/ingest.js +27 -8
  295. package/.next/standalone/lib/legacy-offer.js +7 -6
  296. package/.next/standalone/lib/legacy-scores.js +2 -2
  297. package/.next/standalone/lib/linkedin-limits.js +30 -8
  298. package/.next/standalone/lib/photos.js +47 -0
  299. package/.next/standalone/lib/quest.js +8 -3
  300. package/.next/standalone/lib/rarity.js +80 -0
  301. package/.next/standalone/lib/reach.js +113 -0
  302. package/.next/standalone/lib/requests-client.js +113 -0
  303. package/.next/standalone/lib/rpc.js +26 -15
  304. package/.next/standalone/lib/scan-state.js +1 -0
  305. package/.next/standalone/lib/scanner-setup.js +20 -1
  306. package/.next/standalone/lib/score-guess.js +60 -0
  307. package/.next/standalone/lib/scoring.js +1761 -69
  308. package/.next/standalone/lib/scraper-client.js +235 -17
  309. package/.next/standalone/lib/sector-directory.js +1 -1
  310. package/.next/standalone/lib/sector-focus.js +17 -3
  311. package/.next/standalone/lib/separation.js +4 -2
  312. package/.next/standalone/lib/settings-client.js +21 -0
  313. package/.next/standalone/lib/settings-effects.js +19 -1
  314. package/.next/standalone/lib/settings.js +7 -2
  315. package/.next/standalone/next.config.mjs +25 -3
  316. package/.next/standalone/package-lock.json +2 -2
  317. package/.next/standalone/package.json +2 -2
  318. package/.next/standalone/public/demo-data.json +1 -1
  319. package/.next/standalone/scripts/build-app.mjs +11 -2
  320. package/.next/standalone/scripts/check-node.mjs +1 -1
  321. package/.next/standalone/scripts/gen-synthetic.mjs +1 -1
  322. package/.next/standalone/scripts/image_store.py +77 -21
  323. package/.next/standalone/scripts/scrape.py +305 -29
  324. package/.next/standalone/server.js +1 -1
  325. package/.next/static/chunks/0ax_9zzboyv6d.js +44 -0
  326. package/.next/static/chunks/0cuuo2jwc9h94.js +1 -0
  327. package/.next/static/chunks/0l1568qff0efj.js +1 -0
  328. package/.next/static/chunks/0muwt9bykzr-1.js +1 -0
  329. package/.next/static/chunks/0w76iraw7ij3k.js +18 -0
  330. package/.next/static/chunks/135n87ydxjrra.js +2 -0
  331. package/.next/static/chunks/18xu0fbgo_qz-.js +1 -0
  332. package/.next/static/chunks/28bu26ausqzmn.js +1 -0
  333. package/.next/static/chunks/{40nvbgfn7-072.css → 2p6o90z0vgcwr.css} +1 -1
  334. package/.next/static/chunks/2pnbm1z6j7xab.js +1 -0
  335. package/.next/static/chunks/2xjho0lroiwlq.js +1 -0
  336. package/.next/static/chunks/3e8zg6ctvvb8p.js +33 -0
  337. package/.next/static/chunks/3qh78g2aqhprd.js +2 -0
  338. package/CHANGELOG.md +286 -0
  339. package/README.md +50 -29
  340. package/SECURITY.md +10 -0
  341. package/db/schema.js +2 -1
  342. package/lib/chain-layout.js +139 -0
  343. package/lib/circle.js +169 -0
  344. package/lib/companies.js +1 -1
  345. package/lib/csv.js +21 -10
  346. package/lib/data-export.js +2 -1
  347. package/lib/data-folder.js +4 -2
  348. package/lib/db-client.js +1 -0
  349. package/lib/galaxy.js +12 -0
  350. package/lib/ingest.js +27 -8
  351. package/lib/legacy-offer.js +7 -6
  352. package/lib/legacy-scores.js +2 -2
  353. package/lib/linkedin-limits.js +30 -8
  354. package/lib/photos.js +47 -0
  355. package/lib/quest.js +8 -3
  356. package/lib/rarity.js +80 -0
  357. package/lib/reach.js +113 -0
  358. package/lib/requests-client.js +113 -0
  359. package/lib/rpc.js +26 -15
  360. package/lib/scan-state.js +1 -0
  361. package/lib/scanner-setup.js +20 -1
  362. package/lib/score-guess.js +60 -0
  363. package/lib/scoring.js +1761 -69
  364. package/lib/scraper-client.js +235 -17
  365. package/lib/sector-directory.js +1 -1
  366. package/lib/sector-focus.js +17 -3
  367. package/lib/separation.js +4 -2
  368. package/lib/settings-client.js +21 -0
  369. package/lib/settings-effects.js +19 -1
  370. package/lib/settings.js +7 -2
  371. package/package.json +2 -2
  372. package/public/demo-data.json +1 -1
  373. package/scripts/check-node.mjs +1 -1
  374. package/scripts/image_store.py +77 -21
  375. package/scripts/scrape.py +305 -29
  376. package/.next/standalone/.next/server/chunks/[root-of-the-server]__1t3rugi._.js +0 -3
  377. package/.next/standalone/.next/server/chunks/lib_rpc_0_8i3ft.js +0 -3
  378. package/.next/standalone/.next/server/chunks/lib_rpc_0huvw-3.js +0 -3
  379. package/.next/standalone/.next/server/chunks/lib_rpc_0ip2vdh.js +0 -3
  380. package/.next/standalone/.next/server/chunks/lib_rpc_0jzisgh.js +0 -3
  381. package/.next/standalone/.next/server/chunks/lib_rpc_0yi9xvd.js +0 -3
  382. package/.next/standalone/.next/server/chunks/lib_rpc_1jyhd0o.js +0 -3
  383. package/.next/standalone/.next/server/chunks/lib_rpc_1kn59-i.js +0 -3
  384. package/.next/standalone/.next/server/chunks/lib_rpc_1q2jkza.js +0 -3
  385. package/.next/standalone/.next/server/chunks/lib_rpc_1vpm1sk.js +0 -3
  386. package/.next/standalone/.next/server/chunks/lib_rpc_1x6p_31.js +0 -3
  387. package/.next/standalone/.next/server/chunks/ssr/_07u8byv._.js +0 -3
  388. package/.next/standalone/.next/server/chunks/ssr/_08r_mlf._.js +0 -3
  389. package/.next/standalone/.next/server/chunks/ssr/_0942v8n._.js +0 -3
  390. package/.next/standalone/.next/server/chunks/ssr/_0hl_t7r._.js +0 -3
  391. package/.next/standalone/.next/server/chunks/ssr/_0tb83dw._.js +0 -132
  392. package/.next/standalone/.next/server/chunks/ssr/_1-87bs1._.js +0 -3
  393. package/.next/standalone/.next/server/chunks/ssr/_17gui6a._.js +0 -3
  394. package/.next/standalone/.next/server/chunks/ssr/lib_0i8v-s3._.js +0 -3
  395. package/.next/standalone/.next/server/chunks/ssr/node_modules_16vay6l._.js +0 -3
  396. package/.next/standalone/.next/server/chunks/ssr/node_modules_1kciwn4._.js +0 -3
  397. package/.next/standalone/.next/static/chunks/06x3jiiz96m5t.js +0 -1
  398. package/.next/standalone/.next/static/chunks/08k1crid-05dt.js +0 -1
  399. package/.next/standalone/.next/static/chunks/0ngx9s6-zgshe.js +0 -33
  400. package/.next/standalone/.next/static/chunks/0q-_khndfxubv.js +0 -2
  401. package/.next/standalone/.next/static/chunks/1c79veqedvubt.js +0 -1
  402. package/.next/standalone/.next/static/chunks/1chp3nx45meg5.js +0 -44
  403. package/.next/standalone/.next/static/chunks/1irqj0lf-fu3d.js +0 -1
  404. package/.next/standalone/.next/static/chunks/1uadqylrod5_v.js +0 -1
  405. package/.next/standalone/.next/static/chunks/23u0zclgwpc4d.js +0 -1
  406. package/.next/standalone/.next/static/chunks/289ktj97di6zg.js +0 -1
  407. package/.next/standalone/.next/static/chunks/2b3lmf8e6uj8m.js +0 -16
  408. package/.next/standalone/.next/static/chunks/3npq5yd7c4dmn.js +0 -2
  409. package/.next/standalone/app/components/BridgeRing.js +0 -753
  410. package/.next/static/chunks/06x3jiiz96m5t.js +0 -1
  411. package/.next/static/chunks/08k1crid-05dt.js +0 -1
  412. package/.next/static/chunks/0ngx9s6-zgshe.js +0 -33
  413. package/.next/static/chunks/0q-_khndfxubv.js +0 -2
  414. package/.next/static/chunks/1c79veqedvubt.js +0 -1
  415. package/.next/static/chunks/1chp3nx45meg5.js +0 -44
  416. package/.next/static/chunks/1irqj0lf-fu3d.js +0 -1
  417. package/.next/static/chunks/1uadqylrod5_v.js +0 -1
  418. package/.next/static/chunks/23u0zclgwpc4d.js +0 -1
  419. package/.next/static/chunks/289ktj97di6zg.js +0 -1
  420. package/.next/static/chunks/2b3lmf8e6uj8m.js +0 -16
  421. package/.next/static/chunks/3npq5yd7c4dmn.js +0 -2
  422. /package/.next/standalone/.next/static/{qqqcxvzBUizkECrT4swyK → WseE_5HuU-1q0ywC9CP3U}/_buildManifest.js +0 -0
  423. /package/.next/standalone/.next/static/{qqqcxvzBUizkECrT4swyK → WseE_5HuU-1q0ywC9CP3U}/_clientMiddlewareManifest.js +0 -0
  424. /package/.next/standalone/.next/static/{qqqcxvzBUizkECrT4swyK → WseE_5HuU-1q0ywC9CP3U}/_ssgManifest.js +0 -0
  425. /package/.next/static/{qqqcxvzBUizkECrT4swyK → WseE_5HuU-1q0ywC9CP3U}/_buildManifest.js +0 -0
  426. /package/.next/static/{qqqcxvzBUizkECrT4swyK → WseE_5HuU-1q0ywC9CP3U}/_clientMiddlewareManifest.js +0 -0
  427. /package/.next/static/{qqqcxvzBUizkECrT4swyK → WseE_5HuU-1q0ywC9CP3U}/_ssgManifest.js +0 -0
package/lib/circle.js ADDED
@@ -0,0 +1,169 @@
1
+ // Someone's circle as rings, for the top of their profile card.
2
+ //
3
+ // Blake, 2026-09-28: "for every profile card we can see their network circle
4
+ // … all of their current connections scraped and graded but then outside of
5
+ // that sits the connections all from d2 that we added and scanned coming from
6
+ // that person as the provider of the cluster … and then for d3,d4,d5,d6".
7
+ //
8
+ // Two facts the app already keeps make the whole chain, with nothing new stored:
9
+ // source_connection_id whose circle a row sits in: a scan of that person found it
10
+ // unlocked_from_bridge_id who introduced someone you connected with, kept
11
+ // after they became your connection (lib/promote.js)
12
+ // So from a person P: P's circle (D2, through P) → the people in it you
13
+ // connected with or asked → their circles (D3) → who you added from those →
14
+ // their circles (D4) … to six degrees. Everyone appears once, in the nearest
15
+ // ring they reach.
16
+
17
+ import { keyFor, score1 } from './separation.js';
18
+
19
+ export const MAX_DEGREE = 6;
20
+
21
+ /** A request is out to them and they haven't accepted (Sidebar's "Connect to Unlock Path"). */
22
+ export const requestedByDefault = (row) => row?.unlock_status === 'pending';
23
+
24
+ /**
25
+ * Rows grouped two ways: `circles`, id → the rows a scan of that person found;
26
+ * `introduced`, id → your connections you met through them.
27
+ */
28
+ export function circleIndex(connections = [], degree2 = []) {
29
+ const circles = new Map();
30
+ for (const row of degree2 || []) {
31
+ const id = row?.source_connection_id;
32
+ if (id == null) continue;
33
+ let list = circles.get(id);
34
+ if (!list) circles.set(id, (list = []));
35
+ list.push(row);
36
+ }
37
+ const introduced = new Map();
38
+ for (const row of connections || []) {
39
+ const id = row?.unlocked_from_bridge_id;
40
+ if (id == null || row.degree !== 1) continue;
41
+ let list = introduced.get(id);
42
+ if (!list) introduced.set(id, (list = []));
43
+ list.push(row);
44
+ }
45
+ return { circles, introduced };
46
+ }
47
+
48
+ const STATE_ORDER = { connected: 0, requested: 1 };
49
+ const byStateThenPower = (a, b) => (STATE_ORDER[a.state] ?? 2) - (STATE_ORDER[b.state] ?? 2)
50
+ || score1(b.row) - score1(a.row)
51
+ || String(a.row.name || '').localeCompare(String(b.row.name || ''));
52
+
53
+ /**
54
+ * The rings around `person`: [{ degree, people: [{ row, key, parentId, state }] }],
55
+ * degree 2 first. `state` is 'connected' (you added them, so their own circle
56
+ * is the next ring out), 'requested' (a request is out: drawn dotted) or null.
57
+ * Also the totals: { people, connected, requested }.
58
+ */
59
+ export function circleRings(person, index, { maxDegree = MAX_DEGREE, isRequested = requestedByDefault } = {}) {
60
+ const rings = [];
61
+ const totals = { people: 0, connected: 0, requested: 0 };
62
+ if (!person || !index) return { rings, totals };
63
+ const seen = new Set([keyFor(person)]);
64
+ let frontier = [person];
65
+ for (let degree = 2; degree <= maxDegree && frontier.length; degree++) {
66
+ const ring = [];
67
+ const next = [];
68
+ for (const parent of frontier) {
69
+ const kids = [];
70
+ // The people you connected with first: they have circles of their own.
71
+ for (const row of index.introduced.get(parent.id) || []) {
72
+ const key = keyFor(row);
73
+ if (seen.has(key)) continue;
74
+ seen.add(key);
75
+ kids.push({ row, key, parentId: parent.id, state: 'connected' });
76
+ next.push(row);
77
+ }
78
+ for (const row of index.circles.get(parent.id) || []) {
79
+ const key = keyFor(row);
80
+ if (seen.has(key)) continue;
81
+ seen.add(key);
82
+ kids.push({ row, key, parentId: parent.id, state: isRequested(row) ? 'requested' : null });
83
+ }
84
+ kids.sort(byStateThenPower);
85
+ ring.push(...kids);
86
+ }
87
+ if (!ring.length) break;
88
+ for (const p of ring) {
89
+ totals.people++;
90
+ if (p.state === 'connected') totals.connected++;
91
+ else if (p.state === 'requested') totals.requested++;
92
+ }
93
+ rings.push({ degree, people: ring });
94
+ frontier = next;
95
+ }
96
+ return { rings, totals };
97
+ }
98
+
99
+ const clamp = (v, lo, hi) => Math.min(hi, Math.max(lo, v));
100
+
101
+ /**
102
+ * Where every dot goes, in a square `size` wide: a radial tree. Each person
103
+ * gets a slice of the circle in proportion to how much of the next rings
104
+ * hangs off them, so whoever opened the most reach shows it at a glance, and
105
+ * their circle fans out behind them.
106
+ * Returns { size, center, rings: [{ degree, radius, dotR, items }], spokes }.
107
+ */
108
+ export function circleLayout(rings = [], size = 240) {
109
+ const half = size / 2;
110
+ const R = rings.length;
111
+ const radiusOf = (k) => half * (R === 1 ? 0.78 : 0.4 + 0.52 * (k / (R - 1)));
112
+
113
+ // Each ring's people by who they hang off, in the ring's order.
114
+ const byParent = rings.map((ring) => {
115
+ const m = new Map();
116
+ for (const p of ring.people) {
117
+ if (!m.has(p.parentId)) m.set(p.parentId, []);
118
+ m.get(p.parentId).push(p);
119
+ }
120
+ return m;
121
+ });
122
+ const kidsOf = (k, id) => byParent[k + 1]?.get(id) || [];
123
+
124
+ // Weights, outermost ring first: at least 1, else what hangs off them.
125
+ const weight = new Map();
126
+ for (let k = R - 1; k >= 0; k--) {
127
+ for (const p of rings[k].people) {
128
+ const w = p.state === 'connected' ? kidsOf(k, p.row.id).reduce((s, c) => s + weight.get(c), 0) : 0;
129
+ weight.set(p, Math.max(1, w));
130
+ }
131
+ }
132
+
133
+ const out = [];
134
+ const spokes = [];
135
+ const span = new Map(); // row id → [start, end] angle for its children
136
+ const START = -Math.PI / 2;
137
+ for (let k = 0; k < R; k++) {
138
+ const radius = radiusOf(k);
139
+ const people = rings[k].people;
140
+ const dotR = clamp((2 * Math.PI * radius) / Math.max(1, people.length) * 0.32, 0.8, size / 60);
141
+ const items = [];
142
+ for (const [parentId, group] of byParent[k]) {
143
+ const [a0, a1] = k === 0 ? [START, START + 2 * Math.PI] : span.get(parentId) || [START, START + 2 * Math.PI];
144
+ const total = group.reduce((s, p) => s + weight.get(p), 0);
145
+ let a = a0;
146
+ for (const p of group) {
147
+ const w = ((a1 - a0) * weight.get(p)) / total;
148
+ const angle = a + w / 2;
149
+ const x = half + radius * Math.cos(angle);
150
+ const y = half + radius * Math.sin(angle);
151
+ items.push({ ...p, angle, x, y });
152
+ if (p.state === 'connected') span.set(p.row.id, [a, a + w]);
153
+ a += w;
154
+ }
155
+ }
156
+ // A line from each person you added out to the middle of their own circle.
157
+ if (k + 1 < R) {
158
+ const nextR = radiusOf(k + 1);
159
+ for (const it of items) {
160
+ if (it.state !== 'connected' || !kidsOf(k, it.row.id).length) continue;
161
+ const [s, e] = span.get(it.row.id);
162
+ const mid = (s + e) / 2;
163
+ spokes.push({ x1: it.x, y1: it.y, x2: half + nextR * Math.cos(mid), y2: half + nextR * Math.sin(mid) });
164
+ }
165
+ }
166
+ out.push({ degree: rings[k].degree, radius, dotR, items });
167
+ }
168
+ return { size, center: { x: half, y: half }, rings: out, spokes };
169
+ }
package/lib/companies.js CHANGED
@@ -85,7 +85,7 @@ export const isSenior = (headline) => getSeniority(headline).level >= 4;
85
85
  * A company's industry is the one scoring uses (lib/scoring.js
86
86
  * networkCompanies over the same rows): the curated list's, else the name's,
87
87
  * else what most of its people say. So a company's colour here is the sector
88
- * Settings → Your sector leans on, and each company is the one scoring
88
+ * Scores → Your sector leans on, and each company is the one scoring
89
89
  * scores: both name it with cleanCompany().
90
90
  */
91
91
  export function buildCompanyIndex(rows = []) {
package/lib/csv.js CHANGED
@@ -1,4 +1,5 @@
1
- import { scorePerson } from './scoring.js';
1
+ import { scorePerson, curveCutoffs, curvedTier } from './scoring.js';
2
+ import { toIsoDate } from './ingest.js';
2
3
  // LinkedIn Connections.csv import — parsed ENTIRELY in the browser.
3
4
  //
4
5
  // Nothing is uploaded and nothing is written to the database: an imported
@@ -49,6 +50,20 @@ export function scoreRecord(headline, company) {
49
50
  return { seniority_score: s.title.points, company_prestige_score: s.companyScore, power_score: s.power, tier: s.tier };
50
51
  }
51
52
 
53
+ /**
54
+ * Tiers for a whole import, graded on its own curve (lib/scoring.js CURVE):
55
+ * a CSV import uses the app's defaults, never your settings (no company
56
+ * scores, no sector), and the curve is the default, so the first look at any
57
+ * export has a top. Scores → Tiers says so while one is open. Run on the
58
+ * parse (the import page's counts) and on every load (the tab keeps only
59
+ * what the export says, packConnections), so the two always agree.
60
+ */
61
+ function gradeOnCurve(connections) {
62
+ const cut = curveCutoffs(connections.map((c) => c.power_score));
63
+ for (const c of connections) c.tier = curvedTier(c.power_score, cut);
64
+ return connections;
65
+ }
66
+
52
67
  // Stable id from the profile URL so re-importing keeps node identity.
53
68
  function hashId(input) {
54
69
  let h = 5381;
@@ -122,12 +137,9 @@ export function parseConnectionsCsv(text) {
122
137
  if (seen.has(id)) { skipped++; continue; }
123
138
  seen.add(id);
124
139
 
125
- let connected_date = null;
126
- const raw = cell(iConnected);
127
- if (raw) {
128
- const d = new Date(raw);
129
- if (!isNaN(d.getTime())) connected_date = d.toISOString().split('T')[0];
130
- }
140
+ // "28 Sep 2026", read as the calendar date it is. new Date(raw).toISOString()
141
+ // made it local midnight in UTC, a day early anywhere east of London.
142
+ const connected_date = toIsoDate(cell(iConnected));
131
143
 
132
144
  connections.push(connectionFrom({ id, name, role, company, profileUrl, connected_date }));
133
145
  }
@@ -138,7 +150,7 @@ export function parseConnectionsCsv(text) {
138
150
  );
139
151
  }
140
152
 
141
- connections.sort((a, b) => b.power_score - a.power_score);
153
+ gradeOnCurve(connections).sort((a, b) => b.power_score - a.power_score);
142
154
  return { connections, skipped, total: connections.length + skipped };
143
155
  }
144
156
 
@@ -161,8 +173,7 @@ export function packConnections(connections) {
161
173
  }
162
174
 
163
175
  export function unpackConnections(rows = []) {
164
- return rows
165
- .map(([id, name, role, company, profileUrl, connected_date]) => connectionFrom({ id, name, role, company, profileUrl, connected_date }))
176
+ return gradeOnCurve(rows.map(([id, name, role, company, profileUrl, connected_date]) => connectionFrom({ id, name, role, company, profileUrl, connected_date })))
166
177
  .sort((a, b) => b.power_score - a.power_score);
167
178
  }
168
179
 
@@ -111,7 +111,8 @@ export function buildExport(db, { dir, outFile, includePhotos = true, appVersion
111
111
  // file the other computer doesn't have: some views draw an empty circle
112
112
  // for it. With no photo, they show initials, and the next scan of that
113
113
  // person saves a new one. A photo not saved yet (LinkedIn's own link,
114
- // which expires in a few weeks) is left as it is, as it is here.
114
+ // which expires in a few weeks) is left as it is, as it is here: neither
115
+ // computer shows it, and either saves it once (lib/photos.js).
115
116
  if (!includePhotos) {
116
117
  out.exec("UPDATE linkedin_connections SET profile_image_url = NULL WHERE profile_image_url LIKE '/avatars/%'");
117
118
  }
@@ -11,9 +11,11 @@
11
11
  import { readdirSync, lstatSync, existsSync, rmSync } from 'node:fs';
12
12
  import { spawn } from 'node:child_process';
13
13
  import path from 'node:path';
14
+ import { AVATAR_FILE } from './photos.js';
14
15
 
15
- /** A photo the /avatars route serves: a plain file name, no folders. The route uses this same pattern. */
16
- export const AVATAR_FILE = /^[A-Za-z0-9_-]+\.(webp|jpg|jpeg|png)$/;
16
+ // A photo the /avatars route serves (lib/photos.js). An export carries and an
17
+ // import accepts exactly those names.
18
+ export { AVATAR_FILE };
17
19
 
18
20
  /**
19
21
  * The scanner's notes about the network itself: without them, people whose
package/lib/db-client.js CHANGED
@@ -108,6 +108,7 @@ export function applySchema(db) {
108
108
  // exists, so columns added after first release are added here, once.
109
109
  const ADDED_COLUMNS = [
110
110
  ['linkedin_connections', 'score_why', 'TEXT'],
111
+ ['linkedin_connections', 'mutual_count', 'INTEGER'],
111
112
  ];
112
113
 
113
114
  function addMissingColumns(db) {
package/lib/galaxy.js ADDED
@@ -0,0 +1,12 @@
1
+ // The Galaxy's view, as numbers, kept apart from the drawing so it can be tested
2
+ // (app/components/ForceGraph.js).
3
+
4
+ /**
5
+ * The same view in a box of a new size. It moves by half the change, at the
6
+ * same zoom, so whatever was in the middle of the box is in the middle still.
7
+ * Opening a 320-pixel panel slides the view 160 pixels left, and closing it
8
+ * slides the view back to exactly where it was.
9
+ */
10
+ export function recentre({ x, y, k }, from, to) {
11
+ return { x: x + (to.width - from.width) / 2, y: y + (to.height - from.height) / 2, k };
12
+ }
package/lib/ingest.js CHANGED
@@ -43,22 +43,29 @@ const MONTHS = ['january', 'february', 'march', 'april', 'may', 'june', 'july',
43
43
  'august', 'september', 'october', 'november', 'december'];
44
44
 
45
45
  /**
46
- * "September 22, 2026" (as LinkedIn prints "Connected on …") → "2026-09-22".
46
+ * A date as LinkedIn writes it → "2026-09-22": "September 22, 2026" (a profile's
47
+ * "Connected on …", read by the scanner) or "22 Sep 2026" (the "Connected On"
48
+ * column of the Connections.csv export, read by lib/csv.js).
47
49
  *
48
50
  * Parsed by hand rather than with new Date(text).toISOString(): that reads the
49
51
  * text as local midnight and converts to UTC, which moves the date back a day
50
52
  * anywhere east of Greenwich — and throws outright on text it cannot read.
51
- * Returns null for anything it does not recognise.
53
+ * A calendar date has no time zone, so nothing here uses one.
54
+ * Returns null for anything it does not recognise, or a day the month doesn't have.
52
55
  */
53
56
  export function toIsoDate(text) {
54
57
  const t = String(text || '').trim();
55
58
  if (/^\d{4}-\d{2}-\d{2}$/.test(t)) return t;
56
- const m = t.match(/^([A-Za-z]+)\.?\s+(\d{1,2}),?\s+(\d{4})$/);
57
- if (!m) return null;
58
- const month = MONTHS.findIndex((name) => name.startsWith(m[1].toLowerCase()) && m[1].length >= 3);
59
- const day = Number(m[2]);
60
- if (month < 0 || day < 1 || day > 31) return null;
61
- return `${m[3]}-${String(month + 1).padStart(2, '0')}-${String(day).padStart(2, '0')}`;
59
+ let name, day, year;
60
+ let m = t.match(/^([A-Za-z]+)\.?\s+(\d{1,2}),?\s+(\d{4})$/); // September 22, 2026
61
+ if (m) [, name, day, year] = m;
62
+ else if ((m = t.match(/^(\d{1,2})\s+([A-Za-z]+)\.?\s+(\d{4})$/))) [, day, name, year] = m; // 22 Sep 2026
63
+ else return null;
64
+ const month = MONTHS.findIndex((full) => full.startsWith(name.toLowerCase()) && name.length >= 3);
65
+ day = Number(day);
66
+ // Day 0 of the next month is this month's last, counted in UTC so no zone moves it.
67
+ if (month < 0 || day < 1 || day > new Date(Date.UTC(Number(year), month + 1, 0)).getUTCDate()) return null;
68
+ return `${year}-${String(month + 1).padStart(2, '0')}-${String(day).padStart(2, '0')}`;
62
69
  }
63
70
 
64
71
  // Tiers that make a new connection worth a notification of its own.
@@ -100,3 +107,15 @@ export function refreshNotifications({ added = [], checked = 0, tierOf = () => n
100
107
  }
101
108
  return out;
102
109
  }
110
+
111
+ /**
112
+ * LinkedIn's own count of the mutual connections you share with someone, as
113
+ * the scanner read it off their result card (scripts/scrape.py
114
+ * BRIDGE_RESULTS_JS), or null. A whole number from 1 (the bridge is always
115
+ * one) to 30,000 (LinkedIn's cap on connections); anything else is a misread
116
+ * and is dropped, never stored.
117
+ */
118
+ export function mutualCountOf(value) {
119
+ const n = typeof value === 'string' && /^\d[\d,]*$/.test(value.trim()) ? Number(value.replace(/,/g, '')) : value;
120
+ return Number.isInteger(n) && n >= 1 && n <= 30000 ? n : null;
121
+ }
@@ -1,4 +1,4 @@
1
- // Paths → Scores: the one-time offer to keep the curated list's old scores as
1
+ // The Scores tab: the one-time offer to keep the curated list's old scores as
2
2
  // your own, after the list was made neutral (lib/legacy-scores.js has the old
3
3
  // scores and why each changed).
4
4
  //
@@ -10,7 +10,7 @@
10
10
  // scored on the server, so the page doesn't ask while one is open.
11
11
  //
12
12
  // Keeping writes the old score under every name scoring uses for the entry's
13
- // companies, as Paths → Scores sets a score (setCompanyScores), then rescores
13
+ // companies, as the Scores tab sets a score (setCompanyScores), then rescores
14
14
  // everyone once for the whole batch. Keep or No thanks, it is never offered
15
15
  // again, and an offer whose first look finds nothing to offer closes itself.
16
16
 
@@ -62,14 +62,14 @@ export function legacyOfferState(db = getDb()) {
62
62
  }
63
63
 
64
64
  /**
65
- * What Paths → Scores offers, or null: nothing when the offer isn't open
65
+ * What the Scores tab offers, or null: nothing when the offer isn't open
66
66
  * (never was, or was answered) or no company in the network lost an old score
67
67
  * that you haven't replaced with one of your own. That last closes it for
68
68
  * good ('none'): the network the old list scored is the one read here, so
69
- * nothing could turn up later, and every visit to Paths → Scores would
69
+ * nothing could turn up later, and every visit to the Scores tab would
70
70
  * otherwise read the whole network again to find nothing.
71
71
  *
72
- * The companies are the ones Paths → Scores lists, those someone works at now,
72
+ * The companies are the ones the Scores tab lists, those someone works at now,
73
73
  * so a kept score shows there as yours and Auto can hand it back. (A name only
74
74
  * in former roles would be a score nothing lists; its people's former role
75
75
  * counts at 70% and moves them little.) Each is compared by its built-in score
@@ -116,7 +116,8 @@ export function legacyOffer(db = getDb()) {
116
116
  name: o.name,
117
117
  was: o.was,
118
118
  now: main.now.score,
119
- estimated: main.now.source !== 'known',
119
+ // A score from the curated list or the public dataset is a fact, not an estimate.
120
+ estimated: !['known', 'data'].includes(main.now.source),
120
121
  people: o.people.size,
121
122
  names: o.names.map((n) => n.name).sort(),
122
123
  };
@@ -1,5 +1,5 @@
1
1
  // The curated company list's scores from before it was made neutral, kept for
2
- // one thing only: the one-time offer on Paths → Scores to keep them as your
2
+ // one thing only: the one-time offer on the Scores tab to keep them as your
3
3
  // own (lib/legacy-offer.js). Nothing scores with these. lib/scoring.js never
4
4
  // imports this file, and the model must never read it.
5
5
  //
@@ -24,7 +24,7 @@
24
24
 
25
25
  export const LEGACY_SCORES = [
26
26
  // Rescored: scored like its peers Pinterest, Reddit and X.
27
- ['Snap', 9, /^(snap|snapchat)( inc)?$/, 'media'],
27
+ ['Snap', 9, /^(snap|snapchat)( inc)?( (specs|spectacles|(mena|emea|apac|latam|north america)( region)?))?$/, 'media'],
28
28
  // Removed: private companies that are neither household names nor Fortune
29
29
  // 500-sized, and small startups.
30
30
  ['Anduril', 9, /^anduril( industries)?$/, 'defense'],
@@ -4,15 +4,20 @@
4
4
  // reads them with the same rules, and writes only the two things a person
5
5
  // changes here — the budget, and lifting a cooldown.
6
6
  //
7
+ // The budget is searches (a day and a month) and profile views (a day). Profile
8
+ // views are what got an account restricted (TRAPS §16), so their cap has no
9
+ // "no limit", and the scanner also keeps any two a minute apart.
10
+ //
7
11
  // They belong to the LinkedIn account, not to a profile in the app. TRAPS §35.
8
12
 
9
13
  import { readFileSync, writeFileSync, renameSync, unlinkSync, existsSync, lstatSync } from 'node:fs';
10
14
  import path from 'node:path';
11
15
  import { durable } from './durable.js';
12
16
 
13
- export const DEFAULT_LIMITS = { daily: 50, monthly: 250 };
17
+ export const DEFAULT_LIMITS = { daily: 50, monthly: 250, profiles: 50 };
14
18
  export const DAILY_CHOICES = [25, 50, 100, 200, 500];
15
19
  export const MONTHLY_CHOICES = [100, 250, 500, 1000, 0]; // 0 = no monthly cap (Premium)
20
+ export const PROFILE_CHOICES = [10, 25, 50, 100]; // profile views a day; never unlimited
16
21
  const DAY = 24 * 3600;
17
22
  const PACIFIC = 'America/Los_Angeles';
18
23
 
@@ -56,15 +61,25 @@ export function nextMonthStartPacific(ms = Date.now()) {
56
61
  export function readLimits(dir) {
57
62
  const raw = readJson(path.join(dir, 'scan-limits.json')) || {};
58
63
  const num = (v, d) => (Number.isInteger(v) && v >= 0 ? v : d);
59
- return { daily: num(raw.daily, DEFAULT_LIMITS.daily), monthly: num(raw.monthly, DEFAULT_LIMITS.monthly) };
64
+ return {
65
+ daily: num(raw.daily, DEFAULT_LIMITS.daily),
66
+ monthly: num(raw.monthly, DEFAULT_LIMITS.monthly),
67
+ // 0 is "no limit" for searches; for profile views the scanner reads it as the default.
68
+ profiles: num(raw.profiles, 0) || DEFAULT_LIMITS.profiles,
69
+ };
60
70
  }
61
71
 
62
- /** Only the offered choices are accepted, like everything else that reaches the scanner. */
63
- export function writeLimits(dir, { daily, monthly }) {
72
+ /**
73
+ * Only the offered choices are accepted, like everything else that reaches the
74
+ * scanner. A value not given keeps what is saved: every key is written back,
75
+ * or saving one would drop the others to their defaults.
76
+ */
77
+ export function writeLimits(dir, { daily, monthly, profiles } = {}) {
64
78
  const cur = readLimits(dir);
65
79
  const next = {
66
80
  daily: DAILY_CHOICES.includes(daily) ? daily : cur.daily,
67
81
  monthly: MONTHLY_CHOICES.includes(monthly) ? monthly : cur.monthly,
82
+ profiles: PROFILE_CHOICES.includes(profiles) ? profiles : cur.profiles,
68
83
  };
69
84
  writeJsonAtomic(path.join(dir, 'scan-limits.json'), next);
70
85
  return next;
@@ -72,13 +87,14 @@ export function writeLimits(dir, { daily, monthly }) {
72
87
 
73
88
  /**
74
89
  * Searches in the last 24 hours and since LinkedIn's month began, and profile
75
- * views today. An unreadable record reads as the day used up, as the scanner
76
- * treats it — failing open would say nothing had been searched.
90
+ * views in the last 24 hours. An unreadable record reads as the day used up,
91
+ * searches and profile views both, as the scanner treats it — failing open
92
+ * would say nothing had been searched.
77
93
  */
78
94
  export function usage(dir, now = Date.now()) {
79
95
  const file = path.join(dir, 'linkedin-activity.json');
80
96
  const data = existsSync(file) ? readJson(file) : { searches: [], profiles: [] };
81
- if (!data || typeof data !== 'object') return { searchesToday: Infinity, searchesMonth: 0, profilesToday: 0, unreadable: true };
97
+ if (!data || typeof data !== 'object') return { searchesToday: Infinity, searchesMonth: 0, profilesToday: Infinity, unreadable: true };
82
98
  const s = (Array.isArray(data.searches) ? data.searches : []).map(Number);
83
99
  const p = (Array.isArray(data.profiles) ? data.profiles : []).map(Number);
84
100
  const dayAgo = now / 1000 - DAY;
@@ -113,6 +129,7 @@ export function linkedinState(dir, now = Date.now()) {
113
129
  limits,
114
130
  leftToday: left(limits.daily, use.searchesToday),
115
131
  leftMonth: left(limits.monthly, use.searchesMonth),
132
+ profilesLeftToday: left(limits.profiles, use.profilesToday),
116
133
  monthResets: nextMonthStartPacific(now),
117
134
  cooldown: readCooldown(dir, now),
118
135
  };
@@ -138,7 +155,8 @@ const isTime = (v) => typeof v === 'number' && Number.isFinite(v) && v >= 0;
138
155
  * Why one of these files from another computer can't be used here, in words
139
156
  * for the page, or null when it can. The limits follow writeLimits: only the
140
157
  * menu's own choices. A value the app never offers is refused rather than
141
- * passed to the scanner, which reads a daily limit of 0 as "no limit at all".
158
+ * passed to the scanner, which reads a daily limit of 0 as "no limit at all"
159
+ * (and 1000 profile views a day would be as good as none).
142
160
  */
143
161
  export function budgetFileProblem(name, data) {
144
162
  if (!isObject(data)) return 'it is not a JSON object';
@@ -155,6 +173,9 @@ export function budgetFileProblem(name, data) {
155
173
  if (data.monthly !== undefined && !MONTHLY_CHOICES.includes(data.monthly)) {
156
174
  return `its monthly limit (${String(data.monthly).slice(0, 20)}) is not one Six Degrees offers`;
157
175
  }
176
+ if (data.profiles !== undefined && !PROFILE_CHOICES.includes(data.profiles)) {
177
+ return `its limit on profile views (${String(data.profiles).slice(0, 20)}) is not one Six Degrees offers`;
178
+ }
158
179
  }
159
180
  return null;
160
181
  }
@@ -236,6 +257,7 @@ export function mergeBudgetFiles({ dir, from }) {
236
257
  durable.writeJsonDurably(here(LIMITS), {
237
258
  daily: limits.daily ?? DEFAULT_LIMITS.daily,
238
259
  monthly: limits.monthly ?? DEFAULT_LIMITS.monthly,
260
+ profiles: limits.profiles ?? DEFAULT_LIMITS.profiles,
239
261
  });
240
262
  }
241
263
  }
package/lib/photos.js ADDED
@@ -0,0 +1,47 @@
1
+ // Profile photos are shown from this computer only.
2
+ //
3
+ // The scanner saves each photo into the data folder as it reads it
4
+ // (scripts/image_store.py), and the row points at the saved file,
5
+ // `/avatars/<name>`, which app/avatars/[file] serves. A row can also hold a
6
+ // link: LinkedIn's own, which older versions stored until the scanner replaced
7
+ // it, and which a copy of a network made by one carries too. A page never loads
8
+ // a link: it would contact LinkedIn while someone is only looking at their map,
9
+ // and the app goes online only when you act. That person shows initials until
10
+ // the photo is saved, at the next scan or with Save photos on the Scan page.
11
+ // The Content-Security-Policy in next.config.mjs holds the same line in the
12
+ // browser.
13
+ //
14
+ // No imports, so the views and the server share it: every view and the routes
15
+ // that store photos check with localPhoto, and the /avatars route serves the
16
+ // names AVATAR_FILE allows.
17
+
18
+ /** A photo the /avatars route serves: a plain file name, no folders. The route uses this same pattern. */
19
+ export const AVATAR_FILE = /^[A-Za-z0-9_-]+\.(webp|jpg|jpeg|png)$/;
20
+
21
+ const PREFIX = '/avatars/';
22
+
23
+ /** The photo to show for a row's `profile_image_url`: its saved file's path, or null. */
24
+ export function localPhoto(url) {
25
+ if (typeof url !== 'string' || !url.startsWith(PREFIX)) return null;
26
+ return AVATAR_FILE.test(url.slice(PREFIX.length)) ? url : null;
27
+ }
28
+
29
+ /**
30
+ * The photos still kept as links, one per person and link:
31
+ * [{ profileUrl, imageUrl }]. `db` is an open node:sqlite database. The
32
+ * scanner saves each (`GET /api/update-images`), or forgets the link on a
33
+ * definite no, so a link is tried until there's an answer.
34
+ */
35
+ export function waitingPhotos(db) {
36
+ return db.prepare(`SELECT DISTINCT profile_url AS profileUrl, profile_image_url AS imageUrl
37
+ FROM linkedin_connections
38
+ WHERE profile_image_url IS NOT NULL AND profile_image_url <> ''
39
+ AND substr(profile_image_url, 1, 9) <> '/avatars/'
40
+ ORDER BY profile_url`).all()
41
+ .filter((r) => r.profileUrl && !localPhoto(r.imageUrl));
42
+ }
43
+
44
+ /** How many people have a photo waiting to be saved. */
45
+ export function waitingPhotoCount(db) {
46
+ return new Set(waitingPhotos(db).map((r) => r.profileUrl)).size;
47
+ }
package/lib/quest.js CHANGED
@@ -9,6 +9,8 @@
9
9
  // sent, people who actually joined your network), never from browsing.
10
10
  // Plain functions; tests/quest.test.mjs.
11
11
 
12
+ import { circleState } from './reach.js';
13
+
12
14
  export const STAGE_SIZE = 5;
13
15
  export const XP_SEND = { S: 25, A: 15, B: 10, C: 5, D: 2 };
14
16
  export const XP_ADDED = { S: 100, A: 60, B: 35, C: 15, D: 5 };
@@ -26,8 +28,9 @@ export function levelFor(points) {
26
28
  * @param sentIds ids marked "invite sent"
27
29
  * @param added 1st-degree people who came through a cluster (unlocked_from_bridge_id set)
28
30
  * @param mappedIds ids of 1st-degree people whose own circle is mapped
31
+ * @param reach lib/reach.js reachIndex(), which also knows hidden lists; mappedIds stands in without it
29
32
  */
30
- export function buildQuest({ recs = [], sentIds = new Set(), added = [], mappedIds = new Set() }) {
33
+ export function buildQuest({ recs = [], sentIds = new Set(), added = [], mappedIds = new Set(), reach = null }) {
31
34
  const byBridge = new Map();
32
35
  const seen = new Set();
33
36
  for (const r of [...recs].sort((a, b) => (b.priority || 0) - (a.priority || 0))) {
@@ -112,8 +115,10 @@ export function buildQuest({ recs = [], sentIds = new Set(), added = [], mappedI
112
115
  level: levelFor(points),
113
116
  sentTotal,
114
117
  addedTotal,
115
- // People you've added whose own circle isn't mapped yet: the next degree.
116
- newDoors: added.filter((a) => a.unlocked_from_bridge_id && !mappedIds.has(a.id)),
118
+ // People you've added whose own circle can be scanned: the next degree.
119
+ // Hidden lists aren't doors (lib/reach.js, the rule Bridge Chains uses).
120
+ newDoors: added.filter((a) => a.unlocked_from_bridge_id
121
+ && circleState(a, reach || { scanned: mappedIds, hidden: new Set() }) === 'todo'),
117
122
  clearedStages: clusters.reduce((s, c) => s + (c.stage - 1) + (c.complete ? 1 : 0), 0),
118
123
  };
119
124
  }