six-degrees 0.4.0-beta.1 → 0.4.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (389) 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 +14 -14
  4. package/.next/standalone/.next/required-server-files.json +6 -1
  5. package/.next/standalone/.next/routes-manifest.json +12 -1
  6. package/.next/standalone/.next/server/app/_global-error/page.js.nft.json +1 -1
  7. package/.next/standalone/.next/server/app/_global-error.html +1 -1
  8. package/.next/standalone/.next/server/app/_global-error.rsc +2 -2
  9. package/.next/standalone/.next/server/app/_global-error.segments/__PAGE__.segment.rsc +1 -1
  10. package/.next/standalone/.next/server/app/_global-error.segments/_full.segment.rsc +2 -2
  11. package/.next/standalone/.next/server/app/_global-error.segments/_tree.segment.rsc +1 -1
  12. package/.next/standalone/.next/server/app/_not-found/page.js.nft.json +1 -1
  13. package/.next/standalone/.next/server/app/_not-found/page_client-reference-manifest.js +1 -1
  14. package/.next/standalone/.next/server/app/_not-found.html +1 -1
  15. package/.next/standalone/.next/server/app/_not-found.rsc +12 -12
  16. package/.next/standalone/.next/server/app/_not-found.segments/_full.segment.rsc +12 -12
  17. package/.next/standalone/.next/server/app/_not-found.segments/_not-found/__PAGE__.segment.rsc +10 -10
  18. package/.next/standalone/.next/server/app/_not-found.segments/_tree.segment.rsc +2 -2
  19. package/.next/standalone/.next/server/app/api/admin-delete/route.js +1 -1
  20. package/.next/standalone/.next/server/app/api/admin-delete/route.js.nft.json +1 -1
  21. package/.next/standalone/.next/server/app/api/admin-update/route.js +1 -1
  22. package/.next/standalone/.next/server/app/api/admin-update/route.js.nft.json +1 -1
  23. package/.next/standalone/.next/server/app/api/bridges/route.js +1 -1
  24. package/.next/standalone/.next/server/app/api/bridges/route.js.nft.json +1 -1
  25. package/.next/standalone/.next/server/app/api/bulk-import/route.js +1 -1
  26. package/.next/standalone/.next/server/app/api/bulk-import/route.js.nft.json +1 -1
  27. package/.next/standalone/.next/server/app/api/company-scores/legacy/route.js.nft.json +1 -1
  28. package/.next/standalone/.next/server/app/api/company-scores/route.js.nft.json +1 -1
  29. package/.next/standalone/.next/server/app/api/connections/route.js +1 -1
  30. package/.next/standalone/.next/server/app/api/connections/route.js.nft.json +1 -1
  31. package/.next/standalone/.next/server/app/api/data/export/route.js.nft.json +1 -1
  32. package/.next/standalone/.next/server/app/api/data/import/route.js.nft.json +1 -1
  33. package/.next/standalone/.next/server/app/api/data/restart/route.js.nft.json +1 -1
  34. package/.next/standalone/.next/server/app/api/data/reveal/route.js.nft.json +1 -1
  35. package/.next/standalone/.next/server/app/api/data/route.js.nft.json +1 -1
  36. package/.next/standalone/.next/server/app/api/delete-cluster/route.js +1 -1
  37. package/.next/standalone/.next/server/app/api/delete-cluster/route.js.nft.json +1 -1
  38. package/.next/standalone/.next/server/app/api/ingest/route.js +1 -1
  39. package/.next/standalone/.next/server/app/api/ingest/route.js.nft.json +1 -1
  40. package/.next/standalone/.next/server/app/api/network/route.js +1 -1
  41. package/.next/standalone/.next/server/app/api/network/route.js.nft.json +1 -1
  42. package/.next/standalone/.next/server/app/api/notifications/route.js +1 -1
  43. package/.next/standalone/.next/server/app/api/notifications/route.js.nft.json +1 -1
  44. package/.next/standalone/.next/server/app/api/outreach/route.js +1 -1
  45. package/.next/standalone/.next/server/app/api/outreach/route.js.nft.json +1 -1
  46. package/.next/standalone/.next/server/app/api/queue/route.js +1 -1
  47. package/.next/standalone/.next/server/app/api/queue/route.js.nft.json +1 -1
  48. package/.next/standalone/.next/server/app/api/scraper/route.js +1 -1
  49. package/.next/standalone/.next/server/app/api/scraper/route.js.nft.json +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.nft.json +1 -1
  52. package/.next/standalone/.next/server/app/api/settings/sector-suggestions/route.js.nft.json +1 -1
  53. package/.next/standalone/.next/server/app/api/setup-profile/route.js +1 -1
  54. package/.next/standalone/.next/server/app/api/setup-profile/route.js.nft.json +1 -1
  55. package/.next/standalone/.next/server/app/api/unlock/route.js +1 -1
  56. package/.next/standalone/.next/server/app/api/unlock/route.js.nft.json +1 -1
  57. package/.next/standalone/.next/server/app/api/update/route.js.nft.json +1 -1
  58. package/.next/standalone/.next/server/app/api/update-images/route.js +1 -1
  59. package/.next/standalone/.next/server/app/api/update-images/route.js.nft.json +1 -1
  60. package/.next/standalone/.next/server/app/api/users/route.js +1 -1
  61. package/.next/standalone/.next/server/app/api/users/route.js.nft.json +1 -1
  62. package/.next/standalone/.next/server/app/avatars/[file]/route.js.nft.json +1 -1
  63. package/.next/standalone/.next/server/app/favicon.ico/route.js.nft.json +1 -1
  64. package/.next/standalone/.next/server/app/favicon.ico.body +0 -0
  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 +14 -14
  69. package/.next/standalone/.next/server/app/import.segments/_full.segment.rsc +14 -14
  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 +14 -14
  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 +14 -14
  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 +14 -14
  81. package/.next/standalone/.next/server/app/launch.segments/_full.segment.rsc +14 -14
  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 +14 -14
  90. package/.next/standalone/.next/server/app/paths.segments/_full.segment.rsc +14 -14
  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 +14 -14
  97. package/.next/standalone/.next/server/app/profile.segments/_full.segment.rsc +14 -14
  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 +14 -14
  104. package/.next/standalone/.next/server/app/queue.segments/_full.segment.rsc +14 -14
  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.js.nft.json +1 -1
  108. package/.next/standalone/.next/server/app/scores/page_client-reference-manifest.js +1 -1
  109. package/.next/standalone/.next/server/app/scores.html +1 -1
  110. package/.next/standalone/.next/server/app/scores.rsc +14 -14
  111. package/.next/standalone/.next/server/app/scores.segments/_full.segment.rsc +14 -14
  112. package/.next/standalone/.next/server/app/scores.segments/_tree.segment.rsc +2 -2
  113. package/.next/standalone/.next/server/app/scores.segments/scores/__PAGE__.segment.rsc +12 -12
  114. package/.next/standalone/.next/server/app/settings/page.js.nft.json +1 -1
  115. package/.next/standalone/.next/server/app/settings/page_client-reference-manifest.js +1 -1
  116. package/.next/standalone/.next/server/app/settings.html +1 -1
  117. package/.next/standalone/.next/server/app/settings.rsc +14 -14
  118. package/.next/standalone/.next/server/app/settings.segments/_full.segment.rsc +14 -14
  119. package/.next/standalone/.next/server/app/settings.segments/_tree.segment.rsc +2 -2
  120. package/.next/standalone/.next/server/app/settings.segments/settings/__PAGE__.segment.rsc +12 -12
  121. package/.next/standalone/.next/server/app/setup/page.js.nft.json +1 -1
  122. package/.next/standalone/.next/server/app/setup/page_client-reference-manifest.js +1 -1
  123. package/.next/standalone/.next/server/app/setup.html +1 -1
  124. package/.next/standalone/.next/server/app/setup.rsc +14 -14
  125. package/.next/standalone/.next/server/app/setup.segments/_full.segment.rsc +14 -14
  126. package/.next/standalone/.next/server/app/setup.segments/_tree.segment.rsc +2 -2
  127. package/.next/standalone/.next/server/app/setup.segments/setup/__PAGE__.segment.rsc +12 -12
  128. package/.next/standalone/.next/server/chunks/[root-of-the-server]__0-rnla2._.js +1 -1
  129. package/.next/standalone/.next/server/chunks/[root-of-the-server]__00la_y0._.js +1 -1
  130. package/.next/standalone/.next/server/chunks/[root-of-the-server]__07t47cm._.js +1 -1
  131. package/.next/standalone/.next/server/chunks/{[root-of-the-server]__0rc27za._.js → [root-of-the-server]__0bat8nx._.js} +2 -2
  132. package/.next/standalone/.next/server/chunks/[root-of-the-server]__0cj5jqc._.js +1 -1
  133. package/.next/standalone/.next/server/chunks/[root-of-the-server]__0e0fei3._.js +1 -1
  134. package/.next/standalone/.next/server/chunks/[root-of-the-server]__0ljcioj._.js +1 -1
  135. package/.next/standalone/.next/server/chunks/[root-of-the-server]__0ljnyho._.js +1 -1
  136. package/.next/standalone/.next/server/chunks/[root-of-the-server]__0o5-tcg._.js +1 -1
  137. package/.next/standalone/.next/server/chunks/[root-of-the-server]__0qrnous._.js +1 -1
  138. package/.next/standalone/.next/server/chunks/[root-of-the-server]__0t3v1a1._.js +1 -1
  139. package/.next/standalone/.next/server/chunks/[root-of-the-server]__0uag-k5._.js +1 -1
  140. package/.next/standalone/.next/server/chunks/[root-of-the-server]__0um1uey._.js +1 -1
  141. package/.next/standalone/.next/server/chunks/[root-of-the-server]__0wmexuc._.js +1 -1
  142. package/.next/standalone/.next/server/chunks/[root-of-the-server]__0xu34sz._.js +1 -1
  143. package/.next/standalone/.next/server/chunks/[root-of-the-server]__133knx1._.js +1 -1
  144. package/.next/standalone/.next/server/chunks/[root-of-the-server]__14ou7j5._.js +1 -1
  145. package/.next/standalone/.next/server/chunks/[root-of-the-server]__15-17hl._.js +1 -1
  146. package/.next/standalone/.next/server/chunks/[root-of-the-server]__16iu0hv._.js +1 -1
  147. package/.next/standalone/.next/server/chunks/[root-of-the-server]__19b7mig._.js +1 -1
  148. package/.next/standalone/.next/server/chunks/[root-of-the-server]__1bbcwub._.js +2 -2
  149. package/.next/standalone/.next/server/chunks/[root-of-the-server]__1eris29._.js +1 -1
  150. package/.next/standalone/.next/server/chunks/[root-of-the-server]__1gjo_cq._.js +1 -1
  151. package/.next/standalone/.next/server/chunks/[root-of-the-server]__1kbl21f._.js +1 -1
  152. package/.next/standalone/.next/server/chunks/[root-of-the-server]__1qcm08_._.js +1 -1
  153. package/.next/standalone/.next/server/chunks/[root-of-the-server]__1rphsdk._.js +1 -1
  154. package/.next/standalone/.next/server/chunks/[root-of-the-server]__1w25q0m._.js +1 -1
  155. package/.next/standalone/.next/server/chunks/{[root-of-the-server]__0i5hdu4._.js → [root-of-the-server]__20tmem7._.js} +2 -2
  156. package/.next/standalone/.next/server/chunks/_1_fynia._.js +7 -6
  157. package/.next/standalone/.next/server/chunks/_1fn30_l._.js +1 -1
  158. package/.next/standalone/.next/server/chunks/_1itd1o_._.js +1 -1
  159. package/.next/standalone/.next/server/chunks/_1iwkh8t._.js +1 -1
  160. package/.next/standalone/.next/server/chunks/_1mxn_lb._.js +1 -1
  161. package/.next/standalone/.next/server/chunks/lib_db-client_15le7cw.js +26 -8
  162. package/.next/standalone/.next/server/chunks/{lib_rpc_02ndsg2.js → lib_rpc_03i8xef.js} +1 -1
  163. package/.next/standalone/.next/server/chunks/{lib_rpc_08q5vp6.js → lib_rpc_08fc3rp.js} +1 -1
  164. package/.next/standalone/.next/server/chunks/{lib_rpc_00tx2sm.js → lib_rpc_08r2t_q.js} +1 -1
  165. package/.next/standalone/.next/server/chunks/{lib_rpc_02-qa4d.js → lib_rpc_0btpv1t.js} +1 -1
  166. package/.next/standalone/.next/server/chunks/lib_rpc_0c9d9ti.js +5 -5
  167. package/.next/standalone/.next/server/chunks/lib_rpc_0cm2ev3.js +3 -0
  168. package/.next/standalone/.next/server/chunks/lib_rpc_0iovswz.js +3 -0
  169. package/.next/standalone/.next/server/chunks/lib_rpc_0is8flp.js +3 -0
  170. package/.next/standalone/.next/server/chunks/lib_rpc_0v8ajtd.js +3 -0
  171. package/.next/standalone/.next/server/chunks/lib_rpc_0xgmvvt.js +3 -0
  172. package/.next/standalone/.next/server/chunks/lib_rpc_144mzbf.js +3 -0
  173. package/.next/standalone/.next/server/chunks/lib_rpc_1bggi0n.js +3 -0
  174. package/.next/standalone/.next/server/chunks/lib_rpc_1dkcu9f.js +3 -0
  175. package/.next/standalone/.next/server/chunks/lib_rpc_1o0jb6w.js +3 -0
  176. package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__00d8snt._.js +1 -1
  177. package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__00g61b8._.js +1 -1
  178. package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__08lf3gy._.js +1 -1
  179. package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__0hg5gib._.js +1 -1
  180. package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__0ho-u0w._.js +1 -1
  181. package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__0ze473p._.js +1 -1
  182. package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__1194jps._.js +1 -1
  183. package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__1cnxxc9._.js +1 -1
  184. package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__1ll97ry._.js +1 -1
  185. package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__1m0255u._.js +1 -1
  186. package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__1w9z0zv._.js +1 -1
  187. package/.next/standalone/.next/server/chunks/ssr/_01f38ty._.js +1 -1
  188. package/.next/standalone/.next/server/chunks/ssr/_06w_gl4._.js +3 -0
  189. package/.next/standalone/.next/server/chunks/ssr/_0hw_qnj._.js +3 -0
  190. package/.next/standalone/.next/server/chunks/ssr/_13kzrwv._.js +3 -0
  191. package/.next/standalone/.next/server/chunks/ssr/_15xuwkv._.js +3 -0
  192. package/.next/standalone/.next/server/chunks/ssr/_1gpbbw9._.js +1 -1
  193. package/.next/standalone/.next/server/chunks/ssr/_1hgd5dp._.js +3 -0
  194. package/.next/standalone/.next/server/chunks/ssr/{_0cu6-an._.js → _1vpfyd-._.js} +2 -2
  195. package/.next/standalone/.next/server/chunks/ssr/_20w06i-._.js +3 -0
  196. package/.next/standalone/.next/server/chunks/ssr/app_components_settings_SectorPicker_1c-29qw.js +3 -0
  197. package/.next/standalone/.next/server/chunks/ssr/app_launch_page_19t0dyd.js +1 -1
  198. package/.next/standalone/.next/server/chunks/ssr/app_page_1fazqbf.js +11 -9
  199. package/.next/standalone/.next/server/chunks/ssr/app_paths_page_1ol90m7.js +1 -1
  200. package/.next/standalone/.next/server/chunks/ssr/app_profile_page_0u3-l5y.js +1 -1
  201. package/.next/standalone/.next/server/chunks/ssr/app_queue_page_0c6znd6.js +5 -5
  202. package/.next/standalone/.next/server/chunks/ssr/app_scores_page_09wepvv.js +1 -1
  203. package/.next/standalone/.next/server/chunks/ssr/app_setup_page_1-282zx.js +6 -2
  204. package/.next/standalone/.next/server/chunks/ssr/lib_1bn38gs._.js +1 -1
  205. package/.next/standalone/.next/server/chunks/ssr/node_modules_next_dist_client_components_server-async-storage_0t1dato.js +3 -0
  206. package/.next/standalone/.next/server/middleware-build-manifest.js +3 -3
  207. package/.next/standalone/.next/server/middleware-manifest.json +5 -5
  208. package/.next/standalone/.next/server/pages/404.html +1 -1
  209. package/.next/standalone/.next/server/pages/500.html +1 -1
  210. package/.next/standalone/.next/server/server-reference-manifest.js +1 -1
  211. package/.next/standalone/.next/server/server-reference-manifest.json +1 -1
  212. package/.next/standalone/.next/static/chunks/0ax_9zzboyv6d.js +44 -0
  213. package/.next/standalone/.next/static/chunks/0cuuo2jwc9h94.js +1 -0
  214. package/.next/standalone/.next/static/chunks/0l1568qff0efj.js +1 -0
  215. package/.next/standalone/.next/static/chunks/0muwt9bykzr-1.js +1 -0
  216. package/.next/standalone/.next/static/chunks/10784r7w7t5gw.js +18 -0
  217. package/.next/standalone/.next/static/chunks/18xu0fbgo_qz-.js +1 -0
  218. package/.next/standalone/.next/static/chunks/28bu26ausqzmn.js +1 -0
  219. package/.next/standalone/.next/static/chunks/2p5yt1o9tnxjn.js +2 -0
  220. package/.next/standalone/.next/static/chunks/{40nvbgfn7-072.css → 2p6o90z0vgcwr.css} +1 -1
  221. package/.next/standalone/.next/static/chunks/2pnbm1z6j7xab.js +1 -0
  222. package/.next/standalone/.next/static/chunks/2xjho0lroiwlq.js +1 -0
  223. package/.next/standalone/.next/static/chunks/3e8zg6ctvvb8p.js +33 -0
  224. package/.next/standalone/.next/static/chunks/3ol1xux1-ofm4.js +6 -0
  225. package/.next/standalone/.next/static/chunks/3zs_6-nn0dapb.js +1 -0
  226. package/.next/standalone/.next/static/media/favicon.0zdzw5kurn8m-.ico +0 -0
  227. package/.next/standalone/CHANGELOG.md +211 -0
  228. package/.next/standalone/CONTRIBUTING.md +15 -4
  229. package/.next/standalone/README.md +21 -15
  230. package/.next/standalone/SECURITY.md +10 -0
  231. package/.next/standalone/app/api/admin-delete/route.js +3 -0
  232. package/.next/standalone/app/api/ingest/route.js +19 -1
  233. package/.next/standalone/app/api/scraper/route.js +79 -11
  234. package/.next/standalone/app/api/update-images/route.js +41 -7
  235. package/.next/standalone/app/avatars/[file]/route.js +6 -1
  236. package/.next/standalone/app/components/Avatar.js +5 -2
  237. package/.next/standalone/app/components/ChainView.js +713 -469
  238. package/.next/standalone/app/components/EmptyState.js +1 -1
  239. package/.next/standalone/app/components/FieldStep.js +94 -0
  240. package/.next/standalone/app/components/FilterPanel.js +37 -1
  241. package/.next/standalone/app/components/ForceGraph.js +228 -774
  242. package/.next/standalone/app/components/GridView.js +9 -7
  243. package/.next/standalone/app/components/LinkedInLimits.js +40 -8
  244. package/.next/standalone/app/components/ListView.js +6 -4
  245. package/.next/standalone/app/components/MappingProgress.js +5 -6
  246. package/.next/standalone/app/components/OnboardingGate.js +1 -1
  247. package/.next/standalone/app/components/OrbitGraph.js +3 -1
  248. package/.next/standalone/app/components/OutlinkQuest.js +5 -4
  249. package/.next/standalone/app/components/RingsView.js +9 -8
  250. package/.next/standalone/app/components/Sidebar.js +209 -112
  251. package/.next/standalone/app/components/TheirCircle.js +18 -11
  252. package/.next/standalone/app/components/settings/SectorPicker.js +202 -0
  253. package/.next/standalone/app/components/settings/SectorSection.js +17 -191
  254. package/.next/standalone/app/components/settings/TierSection.js +7 -12
  255. package/.next/standalone/app/components/views.js +6 -4
  256. package/.next/standalone/app/favicon.ico +0 -0
  257. package/.next/standalone/app/globals.css +7 -0
  258. package/.next/standalone/app/launch/page.js +2 -2
  259. package/.next/standalone/app/page.js +153 -31
  260. package/.next/standalone/app/paths/page.js +11 -3
  261. package/.next/standalone/app/queue/page.js +18 -10
  262. package/.next/standalone/app/setup/page.js +423 -260
  263. package/.next/standalone/db/schema.js +15 -1
  264. package/.next/standalone/eslint.config.mjs +3 -3
  265. package/.next/standalone/install.sh +3 -3
  266. package/.next/standalone/lib/brokerage.js +94 -0
  267. package/.next/standalone/lib/chain-layout.js +228 -0
  268. package/.next/standalone/lib/circle.js +23 -4
  269. package/.next/standalone/lib/csv.js +4 -6
  270. package/.next/standalone/lib/data-export.js +2 -1
  271. package/.next/standalone/lib/data-folder.js +4 -2
  272. package/.next/standalone/lib/degrees.js +30 -0
  273. package/.next/standalone/lib/galaxy.js +12 -0
  274. package/.next/standalone/lib/ingest.js +18 -11
  275. package/.next/standalone/lib/linkedin-limits.js +35 -8
  276. package/.next/standalone/lib/photos.js +47 -0
  277. package/.next/standalone/lib/quest.js +8 -3
  278. package/.next/standalone/lib/reach.js +113 -0
  279. package/.next/standalone/lib/scan-state.js +1 -0
  280. package/.next/standalone/lib/scanner-setup.js +20 -1
  281. package/.next/standalone/lib/scoring.js +769 -18
  282. package/.next/standalone/lib/scraper-client.js +27 -0
  283. package/.next/standalone/lib/search-risk.js +38 -0
  284. package/.next/standalone/lib/sector-focus.js +13 -0
  285. package/.next/standalone/lib/separation.js +4 -2
  286. package/.next/standalone/lib/settings-client.js +21 -0
  287. package/.next/standalone/lib/settings.js +3 -1
  288. package/.next/standalone/lib/ties.js +23 -0
  289. package/.next/standalone/next.config.mjs +25 -3
  290. package/.next/standalone/package-lock.json +2 -2
  291. package/.next/standalone/package.json +1 -1
  292. package/.next/standalone/scripts/build-app.mjs +11 -2
  293. package/.next/standalone/scripts/check-node.mjs +1 -1
  294. package/.next/standalone/scripts/image_store.py +77 -21
  295. package/.next/standalone/scripts/scrape.py +271 -29
  296. package/.next/standalone/server.js +1 -1
  297. package/.next/static/chunks/0ax_9zzboyv6d.js +44 -0
  298. package/.next/static/chunks/0cuuo2jwc9h94.js +1 -0
  299. package/.next/static/chunks/0l1568qff0efj.js +1 -0
  300. package/.next/static/chunks/0muwt9bykzr-1.js +1 -0
  301. package/.next/static/chunks/10784r7w7t5gw.js +18 -0
  302. package/.next/static/chunks/18xu0fbgo_qz-.js +1 -0
  303. package/.next/static/chunks/28bu26ausqzmn.js +1 -0
  304. package/.next/static/chunks/2p5yt1o9tnxjn.js +2 -0
  305. package/.next/static/chunks/{40nvbgfn7-072.css → 2p6o90z0vgcwr.css} +1 -1
  306. package/.next/static/chunks/2pnbm1z6j7xab.js +1 -0
  307. package/.next/static/chunks/2xjho0lroiwlq.js +1 -0
  308. package/.next/static/chunks/3e8zg6ctvvb8p.js +33 -0
  309. package/.next/static/chunks/3ol1xux1-ofm4.js +6 -0
  310. package/.next/static/chunks/3zs_6-nn0dapb.js +1 -0
  311. package/.next/static/media/favicon.0zdzw5kurn8m-.ico +0 -0
  312. package/CHANGELOG.md +211 -0
  313. package/README.md +21 -15
  314. package/SECURITY.md +10 -0
  315. package/db/schema.js +15 -1
  316. package/lib/brokerage.js +94 -0
  317. package/lib/chain-layout.js +228 -0
  318. package/lib/circle.js +23 -4
  319. package/lib/csv.js +4 -6
  320. package/lib/data-export.js +2 -1
  321. package/lib/data-folder.js +4 -2
  322. package/lib/degrees.js +30 -0
  323. package/lib/galaxy.js +12 -0
  324. package/lib/ingest.js +18 -11
  325. package/lib/linkedin-limits.js +35 -8
  326. package/lib/photos.js +47 -0
  327. package/lib/quest.js +8 -3
  328. package/lib/reach.js +113 -0
  329. package/lib/scan-state.js +1 -0
  330. package/lib/scanner-setup.js +20 -1
  331. package/lib/scoring.js +769 -18
  332. package/lib/scraper-client.js +27 -0
  333. package/lib/search-risk.js +38 -0
  334. package/lib/sector-focus.js +13 -0
  335. package/lib/separation.js +4 -2
  336. package/lib/settings-client.js +21 -0
  337. package/lib/settings.js +3 -1
  338. package/lib/ties.js +23 -0
  339. package/package.json +1 -1
  340. package/scripts/check-node.mjs +1 -1
  341. package/scripts/image_store.py +77 -21
  342. package/scripts/scrape.py +271 -29
  343. package/.next/standalone/.next/server/chunks/lib_rpc_0_8i3ft.js +0 -3
  344. package/.next/standalone/.next/server/chunks/lib_rpc_0huvw-3.js +0 -3
  345. package/.next/standalone/.next/server/chunks/lib_rpc_0ip2vdh.js +0 -3
  346. package/.next/standalone/.next/server/chunks/lib_rpc_0yi9xvd.js +0 -3
  347. package/.next/standalone/.next/server/chunks/lib_rpc_1jyhd0o.js +0 -3
  348. package/.next/standalone/.next/server/chunks/lib_rpc_1kn59-i.js +0 -3
  349. package/.next/standalone/.next/server/chunks/lib_rpc_1q2jkza.js +0 -3
  350. package/.next/standalone/.next/server/chunks/lib_rpc_1vpm1sk.js +0 -3
  351. package/.next/standalone/.next/server/chunks/lib_rpc_1x6p_31.js +0 -3
  352. package/.next/standalone/.next/server/chunks/ssr/_0223v9s._.js +0 -3
  353. package/.next/standalone/.next/server/chunks/ssr/_0bcf13g._.js +0 -3
  354. package/.next/standalone/.next/server/chunks/ssr/_0wxbq78._.js +0 -3
  355. package/.next/standalone/.next/server/chunks/ssr/_0z88zcl._.js +0 -3
  356. package/.next/standalone/.next/server/chunks/ssr/_1-87bs1._.js +0 -3
  357. package/.next/standalone/.next/server/chunks/ssr/_17gui6a._.js +0 -3
  358. package/.next/standalone/.next/static/chunks/0_yurn4q91vq8.js +0 -16
  359. package/.next/standalone/.next/static/chunks/0d_mh-06srh2k.js +0 -1
  360. package/.next/standalone/.next/static/chunks/0x3oadfikvb1c.js +0 -1
  361. package/.next/standalone/.next/static/chunks/1d5hl1io798n2.js +0 -1
  362. package/.next/standalone/.next/static/chunks/1qt67cvj9sjwi.js +0 -1
  363. package/.next/standalone/.next/static/chunks/1roznk7hkjes1.js +0 -44
  364. package/.next/standalone/.next/static/chunks/2_bf2n4gstad9.js +0 -33
  365. package/.next/standalone/.next/static/chunks/36gbwek52x_w-.js +0 -2
  366. package/.next/standalone/.next/static/chunks/3a55suosthntc.js +0 -1
  367. package/.next/standalone/.next/static/chunks/3fq1zq-6cfwbc.js +0 -1
  368. package/.next/standalone/.next/static/chunks/3nbnio971zlji.js +0 -2
  369. package/.next/standalone/.next/static/chunks/3s6awp3qq9s7w.js +0 -1
  370. package/.next/standalone/.next/static/media/favicon.2vob68tjqpejf.ico +0 -0
  371. package/.next/static/chunks/0_yurn4q91vq8.js +0 -16
  372. package/.next/static/chunks/0d_mh-06srh2k.js +0 -1
  373. package/.next/static/chunks/0x3oadfikvb1c.js +0 -1
  374. package/.next/static/chunks/1d5hl1io798n2.js +0 -1
  375. package/.next/static/chunks/1qt67cvj9sjwi.js +0 -1
  376. package/.next/static/chunks/1roznk7hkjes1.js +0 -44
  377. package/.next/static/chunks/2_bf2n4gstad9.js +0 -33
  378. package/.next/static/chunks/36gbwek52x_w-.js +0 -2
  379. package/.next/static/chunks/3a55suosthntc.js +0 -1
  380. package/.next/static/chunks/3fq1zq-6cfwbc.js +0 -1
  381. package/.next/static/chunks/3nbnio971zlji.js +0 -2
  382. package/.next/static/chunks/3s6awp3qq9s7w.js +0 -1
  383. package/.next/static/media/favicon.2vob68tjqpejf.ico +0 -0
  384. /package/.next/standalone/.next/static/{NnWlxbDF24KhrU1eHiEnv → LlDgA9g8ToBkDLlo9Qtov}/_buildManifest.js +0 -0
  385. /package/.next/standalone/.next/static/{NnWlxbDF24KhrU1eHiEnv → LlDgA9g8ToBkDLlo9Qtov}/_clientMiddlewareManifest.js +0 -0
  386. /package/.next/standalone/.next/static/{NnWlxbDF24KhrU1eHiEnv → LlDgA9g8ToBkDLlo9Qtov}/_ssgManifest.js +0 -0
  387. /package/.next/static/{NnWlxbDF24KhrU1eHiEnv → LlDgA9g8ToBkDLlo9Qtov}/_buildManifest.js +0 -0
  388. /package/.next/static/{NnWlxbDF24KhrU1eHiEnv → LlDgA9g8ToBkDLlo9Qtov}/_clientMiddlewareManifest.js +0 -0
  389. /package/.next/static/{NnWlxbDF24KhrU1eHiEnv → LlDgA9g8ToBkDLlo9Qtov}/_ssgManifest.js +0 -0
@@ -6,6 +6,217 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
6
6
 
7
7
  ## [Unreleased]
8
8
 
9
+ ## [0.4.1] - 2026-09-29
10
+
11
+ ### Added
12
+ - **Which connections open the same doors.** A card says whose circle overlaps theirs most, when
13
+ it's real (15% or more, and at least 3 people): "Opens the same doors as Tom: 62% of the
14
+ people either reaches, both do". Bridge Chains' Degrees box adds how much of your 2nd degree
15
+ you reach two or more ways.
16
+ - **Circle scans keep who among your connections knows whom.** Your own connections turn up in
17
+ other people's lists, and were set aside there and forgotten. Each is now kept as a tie
18
+ between the two, in a new table, with no extra LinkedIn traffic, since the scan read them
19
+ anyway. Nothing shows them yet: they're what clusters and communities will be built from.
20
+ Deleting someone deletes their ties too.
21
+ - **Who only one connection reaches.** In Bridge Chains, each connection with a scanned circle
22
+ shows how many of its people none of your other connections reach ("746 only here"), and
23
+ their card says it in a line: those people are reached through them alone. It's their
24
+ exclusive reach, counted across every circle you've scanned (`lib/brokerage.js`).
25
+ - **Network Circle's Filter picks degrees.** *1st*, *2nd* and *3rd* chips choose who the
26
+ Galaxy draws: your connections, anyone in a scanned circle, and anyone only a company scan
27
+ found. Any mix works, and 2nd without 1st shows who's valuable outside your own
28
+ connections. Each person counts once, at the nearest degree. Further out is drawn a little
29
+ smaller and fainter, gathered near the connection whose circle they're in, and the tier
30
+ counts follow what's drawn. It opens on your connections alone, as before.
31
+
32
+ ### Changed
33
+ - **The Galaxy marks its tier bands.** A faint dashed ring, labelled S to D, sits where each
34
+ tier's dots settle, so the bands read at a glance, most of all with a filter on. Not on a
35
+ phone, whose layout has no rings.
36
+ - **Degrees opens on Bridge Chains,** then Separation, then Orbit, in the menu too. Orbit
37
+ used to come first.
38
+ - **Orbit is in Degrees only.** Network Circle's Orbit was slow on a big network and showed
39
+ nothing the Galaxy doesn't; a Network Circle left on it opens the Galaxy.
40
+ - **A new app icon: Orbits.** Your tier rings round you, with one bridge reaching out through
41
+ them. It's the Mac app's icon, the website's tab icon and its home-screen icon.
42
+ - **The Scan page asks before a risky search budget.** Picking more than 100 searches a
43
+ day, or no monthly cap, now asks first, because a real account was restricted after 373
44
+ searches in 24 hours, run back to back (it had opened only 7 profiles that day). While
45
+ the budget stays that high, a note under it says so, with a button back to 50 a day and
46
+ 250 a month. Lowering a budget, or anything up to 100 a day, is never asked about.
47
+
48
+ ### Fixed
49
+ - **A card's circle map never draws a degree as a solid line.** A big circle, or the people
50
+ behind someone you added packed into their slice, used to run together into one line or
51
+ band. When a slice holds more than fit, its dots now take a few rows, neighbours
52
+ alternating between them, each still behind whoever they hang off, and the dots are sized
53
+ for the most crowded slice.
54
+ - **Bridge Chains: a hover preview no longer draws over the bridges.** With many bridges the
55
+ overview grows extra rings, and a bridge's circle, previewed on hover, landed on top of
56
+ them. It now starts beyond the outermost ring of bridges.
57
+ - **Stop during a re-map's wait keeps the circle.** Re-mapping someone deleted their circle
58
+ first and then waited out the minute between profile views, so a Stop in that minute left
59
+ it deleted and unread. It now waits first and deletes only once the read can start.
60
+ - **A profile-view cap typed into `scan-limits.json` by hand stays within the choices.** It
61
+ counts as the largest offered choice under it (10, 25, 50 or 100), in the scanner and on
62
+ the Scan page alike; before, 1,000 was honoured and the picker showed a different number.
63
+ - **The Filter menu highlights the view that's showing.** After *View Bridge →* from the
64
+ Galaxy, Orbit showed but no view was highlighted.
65
+ - **No *Scan Full Company* on the sample or a CSV import.** Paths offered it there, and it
66
+ searched LinkedIn for a network that isn't saved. A line says scanning needs your own network.
67
+
68
+ ## [0.4.0] - 2026-09-28
69
+
70
+ ### Added
71
+ - **A cap on profile views.** A circle scan opens the person's profile once, and profile
72
+ views are what LinkedIn restricted an account for. They now have a cap of their own: 50
73
+ in any 24 hours by default. The Scan page offers 10, 25, 50 or 100, and there is no "no
74
+ limit".
75
+ - **A minute apart:** at least 60 seconds pass between any two profile opens. It's timed
76
+ from the last one written down, so scans started back to back can't open profiles back
77
+ to back. The log counts down while it waits, and Stop ends the wait.
78
+ - **Checked before the profile opens:** at the cap, a scan opens nothing and records
79
+ nothing about that person. Auto-Bridge stops, as it does at the search budget, and the
80
+ next run starts with the same person.
81
+ - **On the Scan page:** "3 of 50 profile views today", with a bar and the picker.
82
+ - **Re-mapping someone** checks it before deleting their circle. A damaged record counts
83
+ the day's views as used. An import brings the other computer's setting only when this
84
+ one has none, like the search budget.
85
+ - **The Scan page asks what field you're in, before your first scan.** One optional step,
86
+ with the same picks as *Scores → Your sector*: tech, government and defense, dental, and
87
+ the rest. Companies in your field count for more, so your first scores already use it.
88
+ *Skip for now* carries on without one. Both stay at the bottom of the window while you
89
+ look through the list. It's asked once, and never if you already have a network or picked
90
+ a sector. Change it anytime on the Scores tab.
91
+ - **Bridge Chains opens any circle, as far as your scans reach.** Click someone in a
92
+ bridge's circle and their own circle opens in place: the people found in it once you
93
+ connected and scanned it (3rd degree, counted along the chain), and on from there. A
94
+ trail at the top, and Esc, go back. An empty circle says why and what gets it: connect,
95
+ then scan their circle. While one is being scanned, it fills in as the scan saves.
96
+ Someone ready for a scan goes to the Scan page with them picked instead, since their
97
+ circle is empty until then.
98
+ - **A soft glow on people ready for a scan.** In Bridge Chains, someone you added through
99
+ a circle whose own circle isn't scanned yet has a soft breathing halo, and their bridge
100
+ says how many are "ready". A hidden list is greyed with a lock instead. The halo stays
101
+ still with Reduce Motion on. The old rainbow glow is gone: it looked for them inside the
102
+ circle they came from, which accepting takes them out of, so it never showed.
103
+ - **Ready to scan (N), in the Degrees panel.** It replaces *Auto-Bridge Next*, which
104
+ started a scan in one click, by name, with no cost shown, and offered hidden lists again
105
+ and again. The list is everyone you added through a circle whose circle can be scanned,
106
+ strongest first, with the circle you found them in and what a scan costs. Hidden lists
107
+ stay in view, greyed with a lock.
108
+ - **Scan one circle, on the Scan page.** A ready person, from the list or Bridge Chains,
109
+ opens the Scan page with them picked: the cost (a profile view, then a search a page, up
110
+ to 100), what's left of the day's budget, how deep to read, and a button. Nothing starts
111
+ until it's pressed.
112
+ - **A release checks the website's version lines.** Before building anything, the release
113
+ workflow runs `scripts/check-site-version.mjs`: the site's `softwareVersion` and
114
+ `llms.txt` must name the version, and its `dateModified` and sitemap `lastmod` can't be
115
+ older than the release's date in the changelog. A stale site stops the release with a
116
+ line for each thing to change. A pre-release skips it. Anyone can run it before tagging.
117
+
118
+ ### Changed
119
+ - **A big circle spreads out.** A bridge's circle used to sit on one ring, so a few hundred
120
+ people were almost a solid line. It now fills rings from the inside out, as many as it
121
+ needs, with the highest tiers nearest the middle; drag to move, scroll or +/− to zoom.
122
+ Lots of bridges spread the same way, and so does the preview on hover.
123
+ - **The map picks up a finished scan by itself.** No refresh needed.
124
+ - **One rule for "ready".** Bridge Chains, the Degrees panel, Outlink's new doors and the
125
+ profile page's mapping bar now agree on who can be scanned next, and all leave hidden
126
+ lists out.
127
+ - **Photos stay on your Mac.** The app shows a profile photo only from the copy saved on
128
+ your computer, so looking at your network never contacts LinkedIn. It used to load a
129
+ photo from LinkedIn until a scan had saved it, and a rescan could put LinkedIn's link
130
+ back in place of a saved photo. Every page now also tells the browser to load pictures
131
+ from the app alone, and the scanner fetches photos from LinkedIn's image servers only.
132
+ - **Photos an older version kept as links are saved once.** Until then those people show
133
+ initials. The next scan saves them at its end, or *Save photos* on the Scan page does it
134
+ now, without opening a browser. A link more than a few weeks old has expired: that
135
+ person's photo comes back when they're next scanned. Offline, or with LinkedIn's image
136
+ server busy, nothing is forgotten, and *Save photos* stops and says why.
137
+ - **Government and military titles are read.** A senator, a governor, a mayor and a cabinet
138
+ secretary score like a C-suite; their deputies, state legislators, commissioners,
139
+ ambassadors and judges like a VP; a city councilmember or a sheriff like a director.
140
+ Officers by rank: a 3–4 star general or admiral 10, a 1–2 star 9, a colonel 7.5, a
141
+ lieutenant colonel 6.5, a major 5, and a commander by their unit. "(Ret.)" and
142
+ "Retired" make a role former. They count only where a title is written, and only where
143
+ their own organization is a government's or a service's: a staffer in an Office of the
144
+ Secretary, a veteran's civilian job, a city manager at Uber, a Rotary district governor,
145
+ a team captain or a Kentucky colonel isn't one. A title that doesn't say whose it is
146
+ ("Mayor", "Police Chief", "Ambassador") needs its government named, or stays "Title
147
+ unclear": a missed official is a neutral 4, a realtor read as a mayor would be an A.
148
+ Until now all of them were "Title unclear" (a U.S. senator scored 2.9, C).
149
+ - **One name: Six Degrees.** The header, the welcome screen, the loading and launch pages,
150
+ the side panel and the Terminal installer said "6 Degrees". They now say Six Degrees, as
151
+ the window title and the app in Applications already did.
152
+ - **Contributor docs and `.gitignore`.** CONTRIBUTING says npm releases go out through
153
+ trusted publishing, with no token to set, and that `npm run build:desktop` builds the app
154
+ releases ship (`build:app` is the old launcher). `.gitignore` keeps a LinkedIn export, a
155
+ database, a saved copy of a network and the scanner's signed-in browser profile out of a
156
+ commit, wherever they sit in the tree. A local build keeps them out of the app too, and
157
+ its list of uncommitted files inside the app now names ignored ones as well.
158
+ - **0.4.0-beta.1, promoted: this is now the Mac app and npm package everyone gets.** It adds:
159
+ - **Their circle, on every card,** with rarity beside the tier and LinkedIn's own mutual count.
160
+ - **One scan at a time,** and *Resume* beside *Rescan*.
161
+ - **Tiers on your own network's curve,** with unknown companies at a neutral 5.
162
+ - **631 public organizations** the built-in list leaves off, and **scoring 6.**
163
+ - **Scores, a tab of its own.**
164
+
165
+ The details are under 0.4.0-beta.1 below.
166
+ - **The download page** says what's new in 0.4.0.
167
+ - **Degrees' corner toggle says Orbit, which is what it shows.** It said "Galaxy", but
168
+ the Galaxy is a Network Circle view, so Degrees fell back to Orbit. It switches
169
+ between Orbit and Bridge Chains, and it's on whenever Orbit is showing.
170
+
171
+ ### Removed
172
+ - **About 630 lines of Galaxy code that never ran:** a Degrees drawing from before Orbit
173
+ and Bridge Chains. Nothing could open it. The Galaxy draws Network Circle only.
174
+
175
+ ### Fixed
176
+ - **A one-person scan notes a hidden list.** A card's Scan or Rescan that found someone's
177
+ list hidden said so and noted nothing, so they stayed "not scanned yet" and were offered
178
+ again forever. Only a batch noted it. Now every read from page 1 does.
179
+ - **A scan reads the list of the person you picked.** A card's Scan and Rescan went by
180
+ name, so with two connections of the same name the scanner could read the other one's
181
+ list. Every Scan button now goes by that person's profile, as Resume already did.
182
+ - **A company's name after "at" is no longer read as a title.** "Server at President Hotel"
183
+ scored as a C-suite (7.3, A), and so did an IT specialist in an "Office of the Chief
184
+ Information Officer". "Executive Assistant to the General Manager" scored as a VP.
185
+ - **↻ asks before it scans.** The round button in the header started *Check for new* the
186
+ moment it was clicked. It now says what it will do (open Chrome on your connections list
187
+ and read it until it reaches people already saved) and what it costs (no search budget,
188
+ but it is LinkedIn traffic from your account), and waits for OK.
189
+ - **No Scan buttons on the sample or a CSV import.** Every card there offered a scan of
190
+ that person's circle, and Degrees offered *Auto-Bridge Next*, but both networks live
191
+ only in the window, so the scan could only fail ("Bridge '…' not found in database").
192
+ In their place a line says scanning needs your own network.
193
+ - **A CSV import's dates are the day LinkedIn says.** Anywhere east of London, each "Connected
194
+ On" date landed a day early (28 Sep 2026 became 27 Sep). The import now reads it as the
195
+ calendar date it is, the way a scan already did, and a date it can't read is left blank
196
+ rather than guessed.
197
+ - **Opening a panel no longer rebuilds the Galaxy.** Opening or closing the side panel or
198
+ the Filter panel used to draw the Galaxy again from scratch: the layout started over,
199
+ the selection ring vanished, and a big network stalled the page. Now the view slides
200
+ over at the same zoom, so what was in the middle stays in the middle.
201
+ - **The selection ring stays on the person you picked.** It is tied to their dot, and it
202
+ comes back after a tier filter that still shows them. It used to follow the first dot
203
+ it found near its old spot, which could be someone else, even You. *Back to list*
204
+ clears it.
205
+ - **The selection ring pulses gently, as it was always meant to.** Its animation had only
206
+ ever been written in code that never ran. With Reduce Motion on, it holds still.
207
+ - **Hovering a dot eases it up to 1.5 times its size,** instead of jumping. With Reduce
208
+ Motion on, it still changes at once.
209
+ - **The Galaxy draws faster on a big network.** Colouring its lines searched every person
210
+ for every line: about 1.4 s at 30,000 people, on every draw. It now takes under a
211
+ millisecond.
212
+ - **The main screen no longer stalls when nothing is selected on a big network.** The
213
+ side panel's Power Rankings counted every tier again for each person in the list, even
214
+ with the panel closed. At 30,000 people that took about 20 s each time the screen
215
+ redrew, opening the Filter panel included.
216
+ - **On a laptop, the Galaxy keeps its usual layout with a panel open.** Its fixed phone
217
+ layout used to switch on whenever the graph was under 768 pixels wide, which a tier
218
+ filter picked with a panel open could do. It now follows the window, as the page does.
219
+
9
220
  ## [0.4.0-beta.1] - 2026-09-28 (beta: a pre-release, never installed automatically)
10
221
 
11
222
  ### Added
@@ -42,16 +42,27 @@ run `node scripts/pin-python-packages.mjs`.
42
42
 
43
43
  Bump the version, tag it, push the tag. `.github/workflows/release.yml` builds the
44
44
  Mac app for Apple Silicon and Intel, publishes both `.dmg` files and a `SHA256SUMS`
45
- file on a GitHub Release (which is what `install.sh` downloads), and publishes to
46
- npm when an `NPM_TOKEN` secret is set.
45
+ file on a GitHub Release (which is what `install.sh` downloads), and publishes the npm
46
+ package through npm's trusted publishing: npm accepts this repository's
47
+ `release.yml`, and GitHub vouches for each run, so there is no npm token to set or
48
+ keep. A tag with a hyphen (`v0.4.0-beta.1`) is a pre-release on GitHub and goes to npm
49
+ under `next`, so neither `install.sh` nor `npx six-degrees` picks it up.
50
+
51
+ A full release also bumps the download website's version lines (`docs/SEO.md`, "Release
52
+ checklist"). `node scripts/check-site-version.mjs` says whether they match
53
+ `package.json` and the release's date in `CHANGELOG.md`; the release workflow runs it
54
+ before building anything and stops if they don't. A pre-release skips it.
47
55
 
48
56
  ```bash
49
57
  npm version 0.2.0 --no-git-tag-version
58
+ node scripts/check-site-version.mjs
50
59
  git commit -am "Release 0.2.0" && git tag v0.2.0 && git push --follow-tags
51
60
  ```
52
61
 
53
- To try a packaged build locally: `npm run build:app`, then
54
- `SIX_DEGREES_DMG=dist/Six-Degrees-<version>-arm64.dmg bash install.sh`.
62
+ To try a packaged build locally: `npm run build:desktop`, then
63
+ `SIX_DEGREES_DMG=dist/Six-Degrees-<version>-arm64.dmg bash install.sh`. That is the
64
+ Electron app releases ship. `npm run build:app` builds the older classic launcher, kept
65
+ only as a fallback.
55
66
 
56
67
  The picture behind the `.dmg` window is `scripts/dmg/background.html`. After changing
57
68
  it, run `node scripts/make-dmg-background.mjs` (needs Google Chrome) and commit the
@@ -123,8 +123,7 @@ whose own circles reach the furthest, and the shortest chain from you to a stran
123
123
  worth meeting.
124
124
 
125
125
  It's a Mac app (or `npx six-degrees` on Linux). It runs on your computer, against your own data, with no account and
126
- no server. (Inside the app the logo reads *6 Degrees*; in Applications it's **Six
127
- Degrees**.)
126
+ no server.
128
127
 
129
128
  ## Before you scan LinkedIn
130
129
 
@@ -137,8 +136,9 @@ Degrees**.)
137
136
  > - Search was blocked after results were read too fast (2026-09-24).
138
137
  >
139
138
  > The scanner now reads slowly and keeps to a search budget (by default 50 a day and 250
140
- > a month; you can change it on the Scan page). It locks itself during a cooldown, and
141
- > stops at the first sign of push-back. The risk is still yours.
139
+ > a month) and a cap on profile views (50 a day, at least a minute apart). You can change
140
+ > both on the Scan page. It locks itself during a cooldown, and stops at the first sign of
141
+ > push-back. The risk is still yours.
142
142
  >
143
143
  > **The CSV import and the sample network carry no LinkedIn risk at all.** Details:
144
144
  > [how scanning works and what it risks](docs/SCRAPING.md).
@@ -147,12 +147,13 @@ Degrees**.)
147
147
 
148
148
  The app opens on a welcome screen with three ways in, and asks nothing about you:
149
149
 
150
- - **Scan my LinkedIn**: a guided page that ticks each step off as it goes. It sets up
151
- the scanner in one click, you sign into LinkedIn yourself in a Chrome window, and then
152
- it scans. Needs Google Chrome. The Mac app brings its own Python; with `npx six-degrees`
153
- the page uses yours, or sets up a private one with a click. It checks and says what's
154
- missing. The app marks this *Recommended* because it's the only way to Degrees and
155
- Outlink. Read the warning above first.
150
+ - **Scan my LinkedIn**: a guided page that ticks each step off as it goes. It first asks
151
+ one optional question, what field you're in (see *Your sector* below), and *Skip for
152
+ now* is fine. Then it sets up the scanner in one click, you sign into LinkedIn yourself
153
+ in a Chrome window, and then it scans. Needs Google Chrome. The Mac app brings its own
154
+ Python; with `npx six-degrees` the page uses yours, or sets up a private one with a
155
+ click. It checks and says what's missing. The app marks this *Recommended* because
156
+ it's the only way to Degrees and Outlink. Read the warning above first.
156
157
  - **Import my LinkedIn CSV**: LinkedIn's official export, read on your machine. On
157
158
  LinkedIn: **Settings & Privacy → Data privacy → Get a copy of your data →
158
159
  Connections**. LinkedIn emails a link in about ten minutes; unzip it and drop
@@ -225,7 +226,9 @@ tiers, on the fixed scale: S ≥ 7.5 A ≥ 5.5 B ≥ 4.0
225
226
 
226
227
  - **Title (1–10)** comes from someone's roles in their headline, with former roles at
227
228
  70%; current students are capped. Academics have their own ladder (an assistant
228
- professor 5 up to a dean 9), an audience of their own counts like a title (100K+
229
+ professor 5 up to a dean 9), and so do government and the services (a city
230
+ councilmember or a colonel 7.5 up to a senator or a four-star general 10), read only
231
+ where a title is written. An audience of their own counts like a title (100K+
229
232
  followers 6.5, 1M+ 7.5, 10M+ 9), and a title it can't read counts as an individual
230
233
  contributor's (4).
231
234
  - **Company score (1–10)** is the one you set (the Scores tab) if there is one. Otherwise
@@ -246,8 +249,9 @@ tiers, on the fixed scale: S ≥ 7.5 A ≥ 5.5 B ≥ 4.0
246
249
  people you know there, where a job every kind of company has (recruiter, accountant,
247
250
  software engineer) doesn't count. An industry includes its sectors, and a company's one
248
251
  industry when the known list or the company's name gives it, not when only its people's
249
- job titles do. Settings suggests the sectors your network is in and shows what would
250
- change before you save.
252
+ job titles do. Scores suggests the sectors your network is in and shows what would
253
+ change before you save. The Scan page asks for it once, before your first scan, so your
254
+ first scores already use it; you can skip it and pick later.
251
255
  - Multiplying means seniority counts for more at a bigger company. A VP at a company
252
256
  scored 10 gets 9.0; a founder at an unknown company, 7.3.
253
257
  - **Reach bonus (up to 1.5)**: investor, YC, an audience in the millions, the top award
@@ -277,8 +281,10 @@ It estimates **network position**, not what anyone is worth as a person.
277
281
  - A CSV import is read on your machine and never added to your network. It's held only
278
282
  in the app window's own storage, and you import it again next time.
279
283
  - The app contacts only these, and only when you act:
280
- - **LinkedIn**, while you scan. Also, until a profile photo has been saved to your
281
- computer, the app shows it straight from LinkedIn's image server.
284
+ - **LinkedIn**, while you scan. Each profile photo is saved on your computer as the
285
+ scan reads it, and the app shows photos only from there, so looking at your network
286
+ never contacts LinkedIn. (Photos an older version kept as links to LinkedIn show
287
+ initials until the next scan, or *Save photos* on the Scan page, saves them.)
282
288
  - Only without the Mac app (which carries the scanner's Python and add-ons inside it),
283
289
  once, when you set the scanner up: **PyPI** (the Python package library) for the
284
290
  scanner's add-ons, and, if the computer has no Python the scanner can use, **GitHub**
@@ -99,6 +99,14 @@ This app is designed to run **on your own machine, against your own network**.
99
99
  - Values stored in the database are treated as untrusted text and escaped
100
100
  before rendering. Do not reintroduce `innerHTML` (including d3's `.html()`)
101
101
  for anything data-bearing.
102
+ - **Profile photos come from this computer only.** A page shows a photo only
103
+ from a file saved in the data folder (`/avatars/<name>`, `lib/photos.js`),
104
+ never from a link, and every page carries
105
+ `Content-Security-Policy: img-src 'self' data: blob:`, so the browser won't
106
+ load a picture from anywhere else. The scanner fetches photos only from
107
+ LinkedIn's image servers (`*.licdn.com`, over https): while it scans, and
108
+ once each for links older versions stored, which a crafted import could also
109
+ carry.
102
110
  - Do not bind this app to `0.0.0.0` or expose it through a tunnel.
103
111
 
104
112
  ## Your data
@@ -108,6 +116,8 @@ Everything stays local:
108
116
  - A CSV import is parsed in the browser and held for that tab only. It is never
109
117
  uploaded and never written to a database.
110
118
  - Scanned data and cached avatars are written to your machine and are gitignored.
119
+ The app shows photos only from those files, so looking at your network never
120
+ contacts LinkedIn.
111
121
  - There is no telemetry, no analytics, and no crash reporting.
112
122
 
113
123
  If you run the scanner, `chrome-profile/` in the data directory holds a live
@@ -11,6 +11,9 @@ export async function POST(request) {
11
11
  .eq('profile_url', profileUrl);
12
12
 
13
13
  if (error) return Response.json({ error: error.message }, { status: 500 });
14
+ // And every tie they're in (lib/ties.js).
15
+ await supabase.from('connection_ties').delete().eq('a_url', profileUrl);
16
+ await supabase.from('connection_ties').delete().eq('b_url', profileUrl);
14
17
  return Response.json({ success: true });
15
18
  } catch (err) {
16
19
  return Response.json({ error: err.message }, { status: 500 });
@@ -1,6 +1,8 @@
1
1
  import { db as supabase } from '../../../lib/db';
2
2
  import { uniqueByProfile, splitAlreadyConnected, toIsoDate, refreshNotifications, mutualCountOf } from '../../../lib/ingest';
3
+ import { tiePairs } from '../../../lib/ties';
3
4
  import { promoteToFirstDegree } from '../../../lib/promote';
5
+ import { localPhoto } from '../../../lib/photos';
4
6
 
5
7
  function parseHeadline(h) {
6
8
  if (!h) return { role: '', company: '' };
@@ -29,7 +31,11 @@ export async function POST(request) {
29
31
  company: (company || (type === 'company' ? companyName : '') || '').substring(0, 100),
30
32
  role: role.substring(0, 100),
31
33
  profile_url: c.profileUrl?.trim(),
32
- profile_image_url: c.imageUrl || null,
34
+ // Only a photo already saved on this computer. The scanner sends
35
+ // LinkedIn's link here, then saves the picture and attaches its file
36
+ // through /api/update-images; kept, the link would replace a saved
37
+ // photo on every rescan, and the app never shows one (lib/photos.js).
38
+ profile_image_url: localPhoto(c.imageUrl),
33
39
  connected_date: toIsoDate(c.connectedDate),
34
40
  // LinkedIn's own count, read off a circle's result card; nothing else sends one.
35
41
  mutual_count: degree === 2 ? mutualCountOf(c.mutualCount) : null,
@@ -69,6 +75,18 @@ export async function POST(request) {
69
75
  const split = splitAlreadyConnected(records, firstDegree);
70
76
  records = split.keep;
71
77
  alreadyConnected = split.alreadyConnected;
78
+ // Each of your connections in someone's circle is a tie between the two
79
+ // (lib/ties.js). Kept quietly: a failure here never costs the scan's save.
80
+ if (degree === 2 && bridgeId && split.connected.length) {
81
+ try {
82
+ const { data: owner } = await supabase.from('linkedin_connections')
83
+ .select('profile_url').eq('id', bridgeId).eq('user_id', userId).limit(1);
84
+ const pairs = tiePairs(owner?.[0]?.profile_url, split.connected).map((p) => ({ user_id: userId, ...p }));
85
+ if (pairs.length) {
86
+ await supabase.from('connection_ties').upsert(pairs, { onConflict: 'user_id,a_url,b_url', ignoreDuplicates: true });
87
+ }
88
+ } catch { /* ties are extra; the circle is what matters */ }
89
+ }
72
90
  }
73
91
 
74
92
  // Pre-filter: find which profile_urls already exist for this specific context
@@ -9,6 +9,8 @@ import { pausedList, readProgress, readUnclear } from '../../../lib/paused';
9
9
  import { getDb } from '../../../lib/db-client';
10
10
  import { registerScanState } from '../../../lib/scan-state';
11
11
  import { pendingImport } from '../../../lib/data-import';
12
+ import { waitingPhotoCount } from '../../../lib/photos';
13
+ import { reachIndex, circleState } from '../../../lib/reach';
12
14
  import {
13
15
  pythonLooker, thisHostKey, scannerCommand, installSteps, downloadedPython, downloadVerified,
14
16
  placeDownloadedPython, sweepSetupLeftovers, megabytes, SETUP_WORK_PREFIX, ScannerSetupError,
@@ -128,6 +130,53 @@ function bridgeSkips() {
128
130
  }
129
131
  }
130
132
 
133
+ /**
134
+ * What lib/reach.js needs from the scanner's files to tell who is ready for a
135
+ * circle scan: whose list is hidden, and whose list has been read even though
136
+ * nothing in it was saved (everyone on it was already yours). Read on its own,
137
+ * without the machine checks, because the map asks for it when it loads and
138
+ * again after every scan.
139
+ */
140
+ function scanNotes() {
141
+ let read = [];
142
+ try {
143
+ const me = resolveProfile({ create: false });
144
+ if (me) read = Object.keys(readProgress(dataDir(), me.id));
145
+ } catch {}
146
+ return { skips: bridgeSkips(), read };
147
+ }
148
+
149
+ /**
150
+ * One of your connections, for the Scan page's "Scan one circle" box (Bridge
151
+ * Chains and the Degrees panel send people there by id): who they are, and how
152
+ * their circle stands by the same rule the map uses. Null when not found.
153
+ */
154
+ function pickedPerson(id) {
155
+ try {
156
+ const me = resolveProfile({ create: false });
157
+ if (!me) return { person: null };
158
+ const db = getDb();
159
+ const row = db.prepare(
160
+ `SELECT id, name, tier, power_score, degree, profile_url, unlocked_from_bridge_id, unlocked_from_name
161
+ FROM linkedin_connections WHERE id = ? AND user_id = ? AND degree = 1`,
162
+ ).get(String(id ?? ''), me.id);
163
+ if (!row) return { person: null };
164
+ const mapped = db.prepare(
165
+ 'SELECT 1 FROM linkedin_connections WHERE user_id = ? AND degree = 2 AND source_connection_id = ? LIMIT 1',
166
+ ).get(me.id, row.id);
167
+ const reach = reachIndex([], mapped ? [{ source_connection_id: row.id }] : [], scanNotes());
168
+ return {
169
+ person: {
170
+ id: row.id, name: row.name, tier: row.tier, power_score: row.power_score,
171
+ unlocked_from_bridge_id: row.unlocked_from_bridge_id, unlocked_from_name: row.unlocked_from_name,
172
+ },
173
+ circle: circleState(row, reach),
174
+ };
175
+ } catch {
176
+ return { person: null };
177
+ }
178
+ }
179
+
131
180
  // The machine checks (Python, Chrome, signed in) spawn processes, so they are
132
181
  // cached for a few seconds, and callers that arrive while a look is under way
133
182
  // share it rather than each starting their own Pythons. Everything about the
@@ -227,6 +276,10 @@ async function status() {
227
276
  me = resolveProfile({ create: false });
228
277
  if (me) network = networkCounts(me.id);
229
278
  } catch {}
279
+ // People whose photo is still a link, which the app doesn't load: Save
280
+ // photos appears while there are any (lib/photos.js).
281
+ let photosWaiting = 0;
282
+ try { photosWaiting = waitingPhotoCount(getDb()); } catch {}
230
283
 
231
284
  const py = m.python;
232
285
  return {
@@ -253,6 +306,7 @@ async function status() {
253
306
  signedIn: m.signedIn,
254
307
  },
255
308
  network,
309
+ photosWaiting,
256
310
  ...job(),
257
311
  skips: bridgeSkips(),
258
312
  linkedin: linkedinState(dataDir()),
@@ -313,6 +367,8 @@ export async function GET(request) {
313
367
  const q = new URL(request?.url || 'http://127.0.0.1/api/scraper').searchParams;
314
368
  if (q.has('job')) return Response.json(job());
315
369
  if (q.has('resume')) return Response.json({ resume: resumePoint(q.get('resume')) });
370
+ if (q.has('reach')) return Response.json(scanNotes());
371
+ if (q.has('person')) return Response.json(pickedPerson(q.get('person')));
316
372
  return Response.json(await status());
317
373
  }
318
374
 
@@ -331,7 +387,10 @@ const ACTIONS = {
331
387
  // Hidden profiles are remembered so they are not retried forever; this is
332
388
  // the way back in without a terminal.
333
389
  'auto-bridge-retry': { flag: '--auto-bridge --retry-private', label: 'Mapping every bridge, hidden ones included' },
334
- bridge: { flag: '--bridge', needsName: true, label: 'Mapping the circle behind' },
390
+ // One person's circle, from page 1. By profile URL when the page sends their
391
+ // id, as every Scan button does: two connections can share a name, and by name
392
+ // the scanner reads whichever was saved first. By name for a caller with no id.
393
+ bridge: { flag: '--bridge', needsName: true, byId: true, label: 'Mapping the circle behind' },
335
394
  // Carry on with one person whose read was cut short. By profile URL, not name:
336
395
  // two connections can share a name, and Resume must reach the one clicked.
337
396
  resume: { flag: '--bridge-url', needsId: true, label: 'Carrying on with', searches: true },
@@ -339,6 +398,9 @@ const ACTIONS = {
339
398
  'resume-all': { flag: '--auto-bridge --only-unfinished', label: 'Carrying on with every paused list', searches: true },
340
399
  rescrape: { flag: '--rescrape', needsName: true, label: 'Re-mapping the circle behind' },
341
400
  company: { flag: '--company', needsName: true, label: 'Scanning' },
401
+ // Photos an older version kept as links to LinkedIn: saved here, once each.
402
+ // No browser, no search; every scan does the same at its end.
403
+ photos: { flag: '--save-photos', label: 'Saving profile photos to this computer' },
342
404
  };
343
405
 
344
406
  /** Only a plain linkedin.com/in/ profile URL becomes an argument. */
@@ -450,7 +512,7 @@ export async function POST(request) {
450
512
 
451
513
  // The two settings a person changes here. Neither starts anything.
452
514
  if (action === 'set-limits') {
453
- const limits = writeLimits(dataDir(), { daily: body.daily, monthly: body.monthly });
515
+ const limits = writeLimits(dataDir(), { daily: body.daily, monthly: body.monthly, profiles: body.profiles });
454
516
  return Response.json({ ok: true, limits });
455
517
  }
456
518
  if (action === 'lift-cooldown') {
@@ -479,16 +541,19 @@ export async function POST(request) {
479
541
  const readsCircles = action.startsWith('auto-bridge') || ['bridge', 'rescrape', 'resume', 'resume-all'].includes(action);
480
542
  // Carry on with people already mapped, from the page each one stopped at.
481
543
  const deeper = body.deeper === true && (action.startsWith('auto-bridge') || action === 'bridge');
544
+ // Resume, and a circle scan of someone picked by id, go by the connection's
545
+ // profile URL rather than a name.
546
+ const byId = spec.needsId || (spec.byId && Boolean(body.id));
482
547
  let name = null;
483
- if (spec.needsName) {
548
+ if (spec.needsName && !byId) {
484
549
  name = cleanName(body.name);
485
550
  if (!name) return Response.json({ error: 'A name is required for this action.' }, { status: 400 });
486
551
  }
487
- // Resume sends the connection's id; the URL is looked up here, for this
488
- // profile, so nothing from the request itself reaches the command line.
552
+ // The id comes from the page; the URL is looked up here, for this profile, so
553
+ // nothing from the request itself reaches the command line.
489
554
  let profileUrl = null;
490
555
  let person = null;
491
- if (spec.needsId) {
556
+ if (byId) {
492
557
  try {
493
558
  const me = resolveProfile({ create: false });
494
559
  person = me && getDb().prepare(
@@ -500,8 +565,8 @@ export async function POST(request) {
500
565
  }
501
566
  // Who the job is of, so every Scan button can say whose scan is running and a
502
567
  // profile card can show its own person's progress. Only ever shown: the id
503
- // sent alongside a name never reaches the command line (the name does, as
504
- // always), and anything not shaped like one of our ids is dropped.
568
+ // never reaches the command line (a name or the URL looked up for it does),
569
+ // and anything not shaped like one of our ids is dropped.
505
570
  const hintId = typeof body.id === 'string' && /^[\w-]{1,64}$/.test(body.id) ? body.id : null;
506
571
  const target = name || profileUrl ? { id: hintId, name: name || person?.name || null } : null;
507
572
  // Nothing that searches LinkedIn starts during a cooldown; the scanner checks
@@ -552,8 +617,11 @@ export async function POST(request) {
552
617
  // app is inside the signed app (lib/scanner-python.js scannerCommand).
553
618
  plan = [scannerCommand(found.run, path.join(root, 'scripts', 'scrape.py'), [
554
619
  // `--flag=value` is one token on purpose: a name beginning with
555
- // "-" can then never be read as a flag of its own.
556
- ...(name ? [`${spec.flag}=${name}`] : profileUrl ? [`${spec.flag}=${profileUrl}`] : spec.flag.split(' ')),
620
+ // "-" can then never be read as a flag of its own. Anyone found by
621
+ // id is --bridge-url, which carries on where their last read stopped
622
+ // unless told to start at page 1.
623
+ ...(name ? [`${spec.flag}=${name}`] : profileUrl ? [`--bridge-url=${profileUrl}`] : spec.flag.split(' ')),
624
+ ...(profileUrl && action === 'bridge' && !deeper ? ['--from-start'] : []),
557
625
  ...(maxBridges && (action.startsWith('auto-bridge') || action === 'resume-all') ? [`--max-bridges=${maxBridges}`] : []),
558
626
  ...(tiers.length && action.startsWith('auto-bridge') ? [`--tiers=${tiers.join(',')}`] : []),
559
627
  ...(action.startsWith('auto-bridge') ? [`--order=${order}`] : []),
@@ -575,7 +643,7 @@ export async function POST(request) {
575
643
  state.target = target;
576
644
  state.exitCode = null;
577
645
  state.startedAt = Date.now();
578
- state.log = [spec.label + (name ? ` ${name}…` : '…')];
646
+ state.log = [spec.label + (target?.name ? ` ${target.name}…` : '…')];
579
647
  state.stderrTail = [];
580
648
  state.failure = null;
581
649
  forgetChecks();