six-degrees 0.4.0-beta.1 → 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 (355) 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 +5 -5
  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.html +1 -1
  7. package/.next/standalone/.next/server/app/_global-error.rsc +1 -1
  8. package/.next/standalone/.next/server/app/_global-error.segments/__PAGE__.segment.rsc +1 -1
  9. package/.next/standalone/.next/server/app/_global-error.segments/_full.segment.rsc +1 -1
  10. package/.next/standalone/.next/server/app/_global-error.segments/_tree.segment.rsc +1 -1
  11. package/.next/standalone/.next/server/app/_not-found/page.js.nft.json +1 -1
  12. package/.next/standalone/.next/server/app/_not-found/page_client-reference-manifest.js +1 -1
  13. package/.next/standalone/.next/server/app/_not-found.html +1 -1
  14. package/.next/standalone/.next/server/app/_not-found.rsc +11 -11
  15. package/.next/standalone/.next/server/app/_not-found.segments/_full.segment.rsc +11 -11
  16. package/.next/standalone/.next/server/app/_not-found.segments/_not-found/__PAGE__.segment.rsc +10 -10
  17. package/.next/standalone/.next/server/app/_not-found.segments/_tree.segment.rsc +2 -2
  18. package/.next/standalone/.next/server/app/api/admin-delete/route.js +1 -1
  19. package/.next/standalone/.next/server/app/api/admin-delete/route.js.nft.json +1 -1
  20. package/.next/standalone/.next/server/app/api/admin-update/route.js +1 -1
  21. package/.next/standalone/.next/server/app/api/admin-update/route.js.nft.json +1 -1
  22. package/.next/standalone/.next/server/app/api/bridges/route.js +1 -1
  23. package/.next/standalone/.next/server/app/api/bridges/route.js.nft.json +1 -1
  24. package/.next/standalone/.next/server/app/api/bulk-import/route.js +1 -1
  25. package/.next/standalone/.next/server/app/api/bulk-import/route.js.nft.json +1 -1
  26. package/.next/standalone/.next/server/app/api/company-scores/legacy/route.js.nft.json +1 -1
  27. package/.next/standalone/.next/server/app/api/company-scores/route.js.nft.json +1 -1
  28. package/.next/standalone/.next/server/app/api/connections/route.js +1 -1
  29. package/.next/standalone/.next/server/app/api/connections/route.js.nft.json +1 -1
  30. package/.next/standalone/.next/server/app/api/data/export/route.js.nft.json +1 -1
  31. package/.next/standalone/.next/server/app/api/data/import/route.js.nft.json +1 -1
  32. package/.next/standalone/.next/server/app/api/data/restart/route.js.nft.json +1 -1
  33. package/.next/standalone/.next/server/app/api/data/reveal/route.js.nft.json +1 -1
  34. package/.next/standalone/.next/server/app/api/data/route.js.nft.json +1 -1
  35. package/.next/standalone/.next/server/app/api/delete-cluster/route.js +1 -1
  36. package/.next/standalone/.next/server/app/api/delete-cluster/route.js.nft.json +1 -1
  37. package/.next/standalone/.next/server/app/api/ingest/route.js +1 -1
  38. package/.next/standalone/.next/server/app/api/ingest/route.js.nft.json +1 -1
  39. package/.next/standalone/.next/server/app/api/network/route.js +1 -1
  40. package/.next/standalone/.next/server/app/api/network/route.js.nft.json +1 -1
  41. package/.next/standalone/.next/server/app/api/notifications/route.js +1 -1
  42. package/.next/standalone/.next/server/app/api/notifications/route.js.nft.json +1 -1
  43. package/.next/standalone/.next/server/app/api/outreach/route.js +1 -1
  44. package/.next/standalone/.next/server/app/api/outreach/route.js.nft.json +1 -1
  45. package/.next/standalone/.next/server/app/api/queue/route.js +1 -1
  46. package/.next/standalone/.next/server/app/api/queue/route.js.nft.json +1 -1
  47. package/.next/standalone/.next/server/app/api/scraper/route.js +1 -1
  48. package/.next/standalone/.next/server/app/api/scraper/route.js.nft.json +1 -1
  49. package/.next/standalone/.next/server/app/api/settings/route.js.nft.json +1 -1
  50. package/.next/standalone/.next/server/app/api/settings/sector-preview/route.js.nft.json +1 -1
  51. package/.next/standalone/.next/server/app/api/settings/sector-suggestions/route.js.nft.json +1 -1
  52. package/.next/standalone/.next/server/app/api/setup-profile/route.js +1 -1
  53. package/.next/standalone/.next/server/app/api/setup-profile/route.js.nft.json +1 -1
  54. package/.next/standalone/.next/server/app/api/unlock/route.js +1 -1
  55. package/.next/standalone/.next/server/app/api/unlock/route.js.nft.json +1 -1
  56. package/.next/standalone/.next/server/app/api/update/route.js.nft.json +1 -1
  57. package/.next/standalone/.next/server/app/api/update-images/route.js +1 -1
  58. package/.next/standalone/.next/server/app/api/update-images/route.js.nft.json +1 -1
  59. package/.next/standalone/.next/server/app/api/users/route.js +1 -1
  60. package/.next/standalone/.next/server/app/api/users/route.js.nft.json +1 -1
  61. package/.next/standalone/.next/server/app/avatars/[file]/route.js.nft.json +1 -1
  62. package/.next/standalone/.next/server/app/import/page.js.nft.json +1 -1
  63. package/.next/standalone/.next/server/app/import/page_client-reference-manifest.js +1 -1
  64. package/.next/standalone/.next/server/app/import.html +1 -1
  65. package/.next/standalone/.next/server/app/import.rsc +13 -13
  66. package/.next/standalone/.next/server/app/import.segments/_full.segment.rsc +13 -13
  67. package/.next/standalone/.next/server/app/import.segments/_tree.segment.rsc +2 -2
  68. package/.next/standalone/.next/server/app/import.segments/import/__PAGE__.segment.rsc +12 -12
  69. package/.next/standalone/.next/server/app/index.html +1 -1
  70. package/.next/standalone/.next/server/app/index.rsc +13 -13
  71. package/.next/standalone/.next/server/app/index.segments/__PAGE__.segment.rsc +12 -12
  72. package/.next/standalone/.next/server/app/index.segments/_full.segment.rsc +13 -13
  73. package/.next/standalone/.next/server/app/index.segments/_tree.segment.rsc +2 -2
  74. package/.next/standalone/.next/server/app/launch/page.js.nft.json +1 -1
  75. package/.next/standalone/.next/server/app/launch/page_client-reference-manifest.js +1 -1
  76. package/.next/standalone/.next/server/app/launch.html +2 -2
  77. package/.next/standalone/.next/server/app/launch.rsc +13 -13
  78. package/.next/standalone/.next/server/app/launch.segments/_full.segment.rsc +13 -13
  79. package/.next/standalone/.next/server/app/launch.segments/_tree.segment.rsc +2 -2
  80. package/.next/standalone/.next/server/app/launch.segments/launch/__PAGE__.segment.rsc +12 -12
  81. package/.next/standalone/.next/server/app/page.js.nft.json +1 -1
  82. package/.next/standalone/.next/server/app/page_client-reference-manifest.js +1 -1
  83. package/.next/standalone/.next/server/app/paths/page.js.nft.json +1 -1
  84. package/.next/standalone/.next/server/app/paths/page_client-reference-manifest.js +1 -1
  85. package/.next/standalone/.next/server/app/paths.html +1 -1
  86. package/.next/standalone/.next/server/app/paths.rsc +13 -13
  87. package/.next/standalone/.next/server/app/paths.segments/_full.segment.rsc +13 -13
  88. package/.next/standalone/.next/server/app/paths.segments/_tree.segment.rsc +2 -2
  89. package/.next/standalone/.next/server/app/paths.segments/paths/__PAGE__.segment.rsc +12 -12
  90. package/.next/standalone/.next/server/app/profile/page.js.nft.json +1 -1
  91. package/.next/standalone/.next/server/app/profile/page_client-reference-manifest.js +1 -1
  92. package/.next/standalone/.next/server/app/profile.html +1 -1
  93. package/.next/standalone/.next/server/app/profile.rsc +13 -13
  94. package/.next/standalone/.next/server/app/profile.segments/_full.segment.rsc +13 -13
  95. package/.next/standalone/.next/server/app/profile.segments/_tree.segment.rsc +2 -2
  96. package/.next/standalone/.next/server/app/profile.segments/profile/__PAGE__.segment.rsc +12 -12
  97. package/.next/standalone/.next/server/app/queue/page.js.nft.json +1 -1
  98. package/.next/standalone/.next/server/app/queue/page_client-reference-manifest.js +1 -1
  99. package/.next/standalone/.next/server/app/queue.html +1 -1
  100. package/.next/standalone/.next/server/app/queue.rsc +13 -13
  101. package/.next/standalone/.next/server/app/queue.segments/_full.segment.rsc +13 -13
  102. package/.next/standalone/.next/server/app/queue.segments/_tree.segment.rsc +2 -2
  103. package/.next/standalone/.next/server/app/queue.segments/queue/__PAGE__.segment.rsc +12 -12
  104. package/.next/standalone/.next/server/app/scores/page.js.nft.json +1 -1
  105. package/.next/standalone/.next/server/app/scores/page_client-reference-manifest.js +1 -1
  106. package/.next/standalone/.next/server/app/scores.html +1 -1
  107. package/.next/standalone/.next/server/app/scores.rsc +13 -13
  108. package/.next/standalone/.next/server/app/scores.segments/_full.segment.rsc +13 -13
  109. package/.next/standalone/.next/server/app/scores.segments/_tree.segment.rsc +2 -2
  110. package/.next/standalone/.next/server/app/scores.segments/scores/__PAGE__.segment.rsc +12 -12
  111. package/.next/standalone/.next/server/app/settings/page.js.nft.json +1 -1
  112. package/.next/standalone/.next/server/app/settings/page_client-reference-manifest.js +1 -1
  113. package/.next/standalone/.next/server/app/settings.html +1 -1
  114. package/.next/standalone/.next/server/app/settings.rsc +13 -13
  115. package/.next/standalone/.next/server/app/settings.segments/_full.segment.rsc +13 -13
  116. package/.next/standalone/.next/server/app/settings.segments/_tree.segment.rsc +2 -2
  117. package/.next/standalone/.next/server/app/settings.segments/settings/__PAGE__.segment.rsc +12 -12
  118. package/.next/standalone/.next/server/app/setup/page.js.nft.json +1 -1
  119. package/.next/standalone/.next/server/app/setup/page_client-reference-manifest.js +1 -1
  120. package/.next/standalone/.next/server/app/setup.html +1 -1
  121. package/.next/standalone/.next/server/app/setup.rsc +13 -13
  122. package/.next/standalone/.next/server/app/setup.segments/_full.segment.rsc +13 -13
  123. package/.next/standalone/.next/server/app/setup.segments/_tree.segment.rsc +2 -2
  124. package/.next/standalone/.next/server/app/setup.segments/setup/__PAGE__.segment.rsc +12 -12
  125. package/.next/standalone/.next/server/chunks/[root-of-the-server]__0-rnla2._.js +1 -1
  126. package/.next/standalone/.next/server/chunks/[root-of-the-server]__00la_y0._.js +1 -1
  127. package/.next/standalone/.next/server/chunks/{[root-of-the-server]__0i5hdu4._.js → [root-of-the-server]__03ydxxh._.js} +2 -2
  128. package/.next/standalone/.next/server/chunks/[root-of-the-server]__07t47cm._.js +1 -1
  129. package/.next/standalone/.next/server/chunks/{[root-of-the-server]__0rc27za._.js → [root-of-the-server]__0bat8nx._.js} +2 -2
  130. package/.next/standalone/.next/server/chunks/[root-of-the-server]__0cj5jqc._.js +1 -1
  131. package/.next/standalone/.next/server/chunks/[root-of-the-server]__0e0fei3._.js +1 -1
  132. package/.next/standalone/.next/server/chunks/[root-of-the-server]__0ljcioj._.js +1 -1
  133. package/.next/standalone/.next/server/chunks/[root-of-the-server]__0ljnyho._.js +1 -1
  134. package/.next/standalone/.next/server/chunks/[root-of-the-server]__0o5-tcg._.js +1 -1
  135. package/.next/standalone/.next/server/chunks/[root-of-the-server]__0qrnous._.js +1 -1
  136. package/.next/standalone/.next/server/chunks/[root-of-the-server]__0t3v1a1._.js +1 -1
  137. package/.next/standalone/.next/server/chunks/[root-of-the-server]__0uag-k5._.js +1 -1
  138. package/.next/standalone/.next/server/chunks/[root-of-the-server]__0um1uey._.js +1 -1
  139. package/.next/standalone/.next/server/chunks/[root-of-the-server]__0wmexuc._.js +1 -1
  140. package/.next/standalone/.next/server/chunks/[root-of-the-server]__0xu34sz._.js +1 -1
  141. package/.next/standalone/.next/server/chunks/[root-of-the-server]__133knx1._.js +1 -1
  142. package/.next/standalone/.next/server/chunks/[root-of-the-server]__14ou7j5._.js +1 -1
  143. package/.next/standalone/.next/server/chunks/[root-of-the-server]__15-17hl._.js +1 -1
  144. package/.next/standalone/.next/server/chunks/[root-of-the-server]__16iu0hv._.js +1 -1
  145. package/.next/standalone/.next/server/chunks/[root-of-the-server]__19b7mig._.js +1 -1
  146. package/.next/standalone/.next/server/chunks/[root-of-the-server]__1bbcwub._.js +2 -2
  147. package/.next/standalone/.next/server/chunks/[root-of-the-server]__1eris29._.js +1 -1
  148. package/.next/standalone/.next/server/chunks/[root-of-the-server]__1gjo_cq._.js +1 -1
  149. package/.next/standalone/.next/server/chunks/[root-of-the-server]__1kbl21f._.js +1 -1
  150. package/.next/standalone/.next/server/chunks/[root-of-the-server]__1qcm08_._.js +1 -1
  151. package/.next/standalone/.next/server/chunks/[root-of-the-server]__1rphsdk._.js +1 -1
  152. package/.next/standalone/.next/server/chunks/[root-of-the-server]__1w25q0m._.js +1 -1
  153. package/.next/standalone/.next/server/chunks/_1_fynia._.js +7 -6
  154. package/.next/standalone/.next/server/chunks/_1fn30_l._.js +1 -1
  155. package/.next/standalone/.next/server/chunks/_1itd1o_._.js +1 -1
  156. package/.next/standalone/.next/server/chunks/_1iwkh8t._.js +1 -1
  157. package/.next/standalone/.next/server/chunks/lib_db-client_15le7cw.js +12 -8
  158. package/.next/standalone/.next/server/chunks/{lib_rpc_02ndsg2.js → lib_rpc_03i8xef.js} +1 -1
  159. package/.next/standalone/.next/server/chunks/{lib_rpc_08q5vp6.js → lib_rpc_08fc3rp.js} +1 -1
  160. package/.next/standalone/.next/server/chunks/{lib_rpc_00tx2sm.js → lib_rpc_08r2t_q.js} +1 -1
  161. package/.next/standalone/.next/server/chunks/{lib_rpc_02-qa4d.js → lib_rpc_0btpv1t.js} +1 -1
  162. package/.next/standalone/.next/server/chunks/lib_rpc_0c9d9ti.js +5 -5
  163. package/.next/standalone/.next/server/chunks/lib_rpc_0cm2ev3.js +3 -0
  164. package/.next/standalone/.next/server/chunks/lib_rpc_0iovswz.js +3 -0
  165. package/.next/standalone/.next/server/chunks/lib_rpc_0is8flp.js +3 -0
  166. package/.next/standalone/.next/server/chunks/lib_rpc_0v8ajtd.js +3 -0
  167. package/.next/standalone/.next/server/chunks/lib_rpc_0xgmvvt.js +3 -0
  168. package/.next/standalone/.next/server/chunks/lib_rpc_144mzbf.js +3 -0
  169. package/.next/standalone/.next/server/chunks/lib_rpc_1bggi0n.js +3 -0
  170. package/.next/standalone/.next/server/chunks/lib_rpc_1dkcu9f.js +3 -0
  171. package/.next/standalone/.next/server/chunks/lib_rpc_1o0jb6w.js +3 -0
  172. package/.next/standalone/.next/server/chunks/ssr/_01f38ty._.js +1 -1
  173. package/.next/standalone/.next/server/chunks/ssr/_06w_gl4._.js +3 -0
  174. package/.next/standalone/.next/server/chunks/ssr/_0hw_qnj._.js +3 -0
  175. package/.next/standalone/.next/server/chunks/ssr/_13kzrwv._.js +3 -0
  176. package/.next/standalone/.next/server/chunks/ssr/_15xuwkv._.js +3 -0
  177. package/.next/standalone/.next/server/chunks/ssr/_1gpbbw9._.js +1 -1
  178. package/.next/standalone/.next/server/chunks/ssr/_1hgd5dp._.js +3 -0
  179. package/.next/standalone/.next/server/chunks/ssr/{_0cu6-an._.js → _1vpfyd-._.js} +2 -2
  180. package/.next/standalone/.next/server/chunks/ssr/_20w06i-._.js +3 -0
  181. package/.next/standalone/.next/server/chunks/ssr/app_components_settings_SectorPicker_1c-29qw.js +3 -0
  182. package/.next/standalone/.next/server/chunks/ssr/app_launch_page_19t0dyd.js +1 -1
  183. package/.next/standalone/.next/server/chunks/ssr/app_page_1fazqbf.js +11 -9
  184. package/.next/standalone/.next/server/chunks/ssr/app_paths_page_1ol90m7.js +1 -1
  185. package/.next/standalone/.next/server/chunks/ssr/app_profile_page_0u3-l5y.js +1 -1
  186. package/.next/standalone/.next/server/chunks/ssr/app_queue_page_0c6znd6.js +5 -5
  187. package/.next/standalone/.next/server/chunks/ssr/app_scores_page_09wepvv.js +1 -1
  188. package/.next/standalone/.next/server/chunks/ssr/app_setup_page_1-282zx.js +2 -2
  189. package/.next/standalone/.next/server/chunks/ssr/lib_1bn38gs._.js +1 -1
  190. package/.next/standalone/.next/server/chunks/ssr/node_modules_next_dist_client_components_server-async-storage_0t1dato.js +3 -0
  191. package/.next/standalone/.next/server/middleware-build-manifest.js +3 -3
  192. package/.next/standalone/.next/server/middleware-manifest.json +5 -5
  193. package/.next/standalone/.next/server/pages/404.html +1 -1
  194. package/.next/standalone/.next/server/pages/500.html +1 -1
  195. package/.next/standalone/.next/server/server-reference-manifest.js +1 -1
  196. package/.next/standalone/.next/server/server-reference-manifest.json +1 -1
  197. package/.next/standalone/.next/static/chunks/0ax_9zzboyv6d.js +44 -0
  198. package/.next/standalone/.next/static/chunks/0cuuo2jwc9h94.js +1 -0
  199. package/.next/standalone/.next/static/chunks/0l1568qff0efj.js +1 -0
  200. package/.next/standalone/.next/static/chunks/0muwt9bykzr-1.js +1 -0
  201. package/.next/standalone/.next/static/chunks/0w76iraw7ij3k.js +18 -0
  202. package/.next/standalone/.next/static/chunks/135n87ydxjrra.js +2 -0
  203. package/.next/standalone/.next/static/chunks/18xu0fbgo_qz-.js +1 -0
  204. package/.next/standalone/.next/static/chunks/28bu26ausqzmn.js +1 -0
  205. package/.next/standalone/.next/static/chunks/{40nvbgfn7-072.css → 2p6o90z0vgcwr.css} +1 -1
  206. package/.next/standalone/.next/static/chunks/2pnbm1z6j7xab.js +1 -0
  207. package/.next/standalone/.next/static/chunks/2xjho0lroiwlq.js +1 -0
  208. package/.next/standalone/.next/static/chunks/3e8zg6ctvvb8p.js +33 -0
  209. package/.next/standalone/.next/static/chunks/3qh78g2aqhprd.js +2 -0
  210. package/.next/standalone/CHANGELOG.md +152 -0
  211. package/.next/standalone/CONTRIBUTING.md +15 -4
  212. package/.next/standalone/README.md +21 -15
  213. package/.next/standalone/SECURITY.md +10 -0
  214. package/.next/standalone/app/api/ingest/route.js +6 -1
  215. package/.next/standalone/app/api/scraper/route.js +79 -11
  216. package/.next/standalone/app/api/update-images/route.js +41 -7
  217. package/.next/standalone/app/avatars/[file]/route.js +6 -1
  218. package/.next/standalone/app/components/Avatar.js +5 -2
  219. package/.next/standalone/app/components/ChainView.js +659 -469
  220. package/.next/standalone/app/components/EmptyState.js +1 -1
  221. package/.next/standalone/app/components/FieldStep.js +94 -0
  222. package/.next/standalone/app/components/ForceGraph.js +183 -768
  223. package/.next/standalone/app/components/GridView.js +9 -7
  224. package/.next/standalone/app/components/LinkedInLimits.js +22 -7
  225. package/.next/standalone/app/components/ListView.js +6 -4
  226. package/.next/standalone/app/components/MappingProgress.js +5 -6
  227. package/.next/standalone/app/components/OnboardingGate.js +1 -1
  228. package/.next/standalone/app/components/OrbitGraph.js +3 -1
  229. package/.next/standalone/app/components/OutlinkQuest.js +5 -4
  230. package/.next/standalone/app/components/RingsView.js +9 -8
  231. package/.next/standalone/app/components/Sidebar.js +172 -112
  232. package/.next/standalone/app/components/TheirCircle.js +18 -11
  233. package/.next/standalone/app/components/settings/SectorPicker.js +202 -0
  234. package/.next/standalone/app/components/settings/SectorSection.js +17 -191
  235. package/.next/standalone/app/components/settings/TierSection.js +7 -12
  236. package/.next/standalone/app/components/views.js +2 -2
  237. package/.next/standalone/app/globals.css +7 -0
  238. package/.next/standalone/app/launch/page.js +2 -2
  239. package/.next/standalone/app/page.js +130 -25
  240. package/.next/standalone/app/paths/page.js +3 -2
  241. package/.next/standalone/app/queue/page.js +18 -10
  242. package/.next/standalone/app/setup/page.js +423 -260
  243. package/.next/standalone/db/schema.js +1 -1
  244. package/.next/standalone/eslint.config.mjs +3 -3
  245. package/.next/standalone/install.sh +3 -3
  246. package/.next/standalone/lib/chain-layout.js +139 -0
  247. package/.next/standalone/lib/csv.js +4 -6
  248. package/.next/standalone/lib/data-export.js +2 -1
  249. package/.next/standalone/lib/data-folder.js +4 -2
  250. package/.next/standalone/lib/galaxy.js +12 -0
  251. package/.next/standalone/lib/ingest.js +15 -8
  252. package/.next/standalone/lib/linkedin-limits.js +30 -8
  253. package/.next/standalone/lib/photos.js +47 -0
  254. package/.next/standalone/lib/quest.js +8 -3
  255. package/.next/standalone/lib/reach.js +113 -0
  256. package/.next/standalone/lib/scan-state.js +1 -0
  257. package/.next/standalone/lib/scanner-setup.js +20 -1
  258. package/.next/standalone/lib/scoring.js +769 -18
  259. package/.next/standalone/lib/scraper-client.js +27 -0
  260. package/.next/standalone/lib/sector-focus.js +13 -0
  261. package/.next/standalone/lib/separation.js +4 -2
  262. package/.next/standalone/lib/settings-client.js +21 -0
  263. package/.next/standalone/lib/settings.js +3 -1
  264. package/.next/standalone/next.config.mjs +25 -3
  265. package/.next/standalone/package-lock.json +2 -2
  266. package/.next/standalone/package.json +1 -1
  267. package/.next/standalone/scripts/build-app.mjs +11 -2
  268. package/.next/standalone/scripts/check-node.mjs +1 -1
  269. package/.next/standalone/scripts/image_store.py +77 -21
  270. package/.next/standalone/scripts/scrape.py +243 -29
  271. package/.next/standalone/server.js +1 -1
  272. package/.next/static/chunks/0ax_9zzboyv6d.js +44 -0
  273. package/.next/static/chunks/0cuuo2jwc9h94.js +1 -0
  274. package/.next/static/chunks/0l1568qff0efj.js +1 -0
  275. package/.next/static/chunks/0muwt9bykzr-1.js +1 -0
  276. package/.next/static/chunks/0w76iraw7ij3k.js +18 -0
  277. package/.next/static/chunks/135n87ydxjrra.js +2 -0
  278. package/.next/static/chunks/18xu0fbgo_qz-.js +1 -0
  279. package/.next/static/chunks/28bu26ausqzmn.js +1 -0
  280. package/.next/static/chunks/{40nvbgfn7-072.css → 2p6o90z0vgcwr.css} +1 -1
  281. package/.next/static/chunks/2pnbm1z6j7xab.js +1 -0
  282. package/.next/static/chunks/2xjho0lroiwlq.js +1 -0
  283. package/.next/static/chunks/3e8zg6ctvvb8p.js +33 -0
  284. package/.next/static/chunks/3qh78g2aqhprd.js +2 -0
  285. package/CHANGELOG.md +152 -0
  286. package/README.md +21 -15
  287. package/SECURITY.md +10 -0
  288. package/db/schema.js +1 -1
  289. package/lib/chain-layout.js +139 -0
  290. package/lib/csv.js +4 -6
  291. package/lib/data-export.js +2 -1
  292. package/lib/data-folder.js +4 -2
  293. package/lib/galaxy.js +12 -0
  294. package/lib/ingest.js +15 -8
  295. package/lib/linkedin-limits.js +30 -8
  296. package/lib/photos.js +47 -0
  297. package/lib/quest.js +8 -3
  298. package/lib/reach.js +113 -0
  299. package/lib/scan-state.js +1 -0
  300. package/lib/scanner-setup.js +20 -1
  301. package/lib/scoring.js +769 -18
  302. package/lib/scraper-client.js +27 -0
  303. package/lib/sector-focus.js +13 -0
  304. package/lib/separation.js +4 -2
  305. package/lib/settings-client.js +21 -0
  306. package/lib/settings.js +3 -1
  307. package/package.json +1 -1
  308. package/scripts/check-node.mjs +1 -1
  309. package/scripts/image_store.py +77 -21
  310. package/scripts/scrape.py +243 -29
  311. package/.next/standalone/.next/server/chunks/lib_rpc_0_8i3ft.js +0 -3
  312. package/.next/standalone/.next/server/chunks/lib_rpc_0huvw-3.js +0 -3
  313. package/.next/standalone/.next/server/chunks/lib_rpc_0ip2vdh.js +0 -3
  314. package/.next/standalone/.next/server/chunks/lib_rpc_0yi9xvd.js +0 -3
  315. package/.next/standalone/.next/server/chunks/lib_rpc_1jyhd0o.js +0 -3
  316. package/.next/standalone/.next/server/chunks/lib_rpc_1kn59-i.js +0 -3
  317. package/.next/standalone/.next/server/chunks/lib_rpc_1q2jkza.js +0 -3
  318. package/.next/standalone/.next/server/chunks/lib_rpc_1vpm1sk.js +0 -3
  319. package/.next/standalone/.next/server/chunks/lib_rpc_1x6p_31.js +0 -3
  320. package/.next/standalone/.next/server/chunks/ssr/_0223v9s._.js +0 -3
  321. package/.next/standalone/.next/server/chunks/ssr/_0bcf13g._.js +0 -3
  322. package/.next/standalone/.next/server/chunks/ssr/_0wxbq78._.js +0 -3
  323. package/.next/standalone/.next/server/chunks/ssr/_0z88zcl._.js +0 -3
  324. package/.next/standalone/.next/server/chunks/ssr/_1-87bs1._.js +0 -3
  325. package/.next/standalone/.next/server/chunks/ssr/_17gui6a._.js +0 -3
  326. package/.next/standalone/.next/static/chunks/0_yurn4q91vq8.js +0 -16
  327. package/.next/standalone/.next/static/chunks/0d_mh-06srh2k.js +0 -1
  328. package/.next/standalone/.next/static/chunks/0x3oadfikvb1c.js +0 -1
  329. package/.next/standalone/.next/static/chunks/1d5hl1io798n2.js +0 -1
  330. package/.next/standalone/.next/static/chunks/1qt67cvj9sjwi.js +0 -1
  331. package/.next/standalone/.next/static/chunks/1roznk7hkjes1.js +0 -44
  332. package/.next/standalone/.next/static/chunks/2_bf2n4gstad9.js +0 -33
  333. package/.next/standalone/.next/static/chunks/36gbwek52x_w-.js +0 -2
  334. package/.next/standalone/.next/static/chunks/3a55suosthntc.js +0 -1
  335. package/.next/standalone/.next/static/chunks/3fq1zq-6cfwbc.js +0 -1
  336. package/.next/standalone/.next/static/chunks/3nbnio971zlji.js +0 -2
  337. package/.next/standalone/.next/static/chunks/3s6awp3qq9s7w.js +0 -1
  338. package/.next/static/chunks/0_yurn4q91vq8.js +0 -16
  339. package/.next/static/chunks/0d_mh-06srh2k.js +0 -1
  340. package/.next/static/chunks/0x3oadfikvb1c.js +0 -1
  341. package/.next/static/chunks/1d5hl1io798n2.js +0 -1
  342. package/.next/static/chunks/1qt67cvj9sjwi.js +0 -1
  343. package/.next/static/chunks/1roznk7hkjes1.js +0 -44
  344. package/.next/static/chunks/2_bf2n4gstad9.js +0 -33
  345. package/.next/static/chunks/36gbwek52x_w-.js +0 -2
  346. package/.next/static/chunks/3a55suosthntc.js +0 -1
  347. package/.next/static/chunks/3fq1zq-6cfwbc.js +0 -1
  348. package/.next/static/chunks/3nbnio971zlji.js +0 -2
  349. package/.next/static/chunks/3s6awp3qq9s7w.js +0 -1
  350. /package/.next/standalone/.next/static/{NnWlxbDF24KhrU1eHiEnv → WseE_5HuU-1q0ywC9CP3U}/_buildManifest.js +0 -0
  351. /package/.next/standalone/.next/static/{NnWlxbDF24KhrU1eHiEnv → WseE_5HuU-1q0ywC9CP3U}/_clientMiddlewareManifest.js +0 -0
  352. /package/.next/standalone/.next/static/{NnWlxbDF24KhrU1eHiEnv → WseE_5HuU-1q0ywC9CP3U}/_ssgManifest.js +0 -0
  353. /package/.next/static/{NnWlxbDF24KhrU1eHiEnv → WseE_5HuU-1q0ywC9CP3U}/_buildManifest.js +0 -0
  354. /package/.next/static/{NnWlxbDF24KhrU1eHiEnv → WseE_5HuU-1q0ywC9CP3U}/_clientMiddlewareManifest.js +0 -0
  355. /package/.next/static/{NnWlxbDF24KhrU1eHiEnv → WseE_5HuU-1q0ywC9CP3U}/_ssgManifest.js +0 -0
package/CHANGELOG.md CHANGED
@@ -6,6 +6,158 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
6
6
 
7
7
  ## [Unreleased]
8
8
 
9
+ ## [0.4.0] - 2026-09-28
10
+
11
+ ### Added
12
+ - **A cap on profile views.** A circle scan opens the person's profile once, and profile
13
+ views are what LinkedIn restricted an account for. They now have a cap of their own: 50
14
+ in any 24 hours by default. The Scan page offers 10, 25, 50 or 100, and there is no "no
15
+ limit".
16
+ - **A minute apart:** at least 60 seconds pass between any two profile opens. It's timed
17
+ from the last one written down, so scans started back to back can't open profiles back
18
+ to back. The log counts down while it waits, and Stop ends the wait.
19
+ - **Checked before the profile opens:** at the cap, a scan opens nothing and records
20
+ nothing about that person. Auto-Bridge stops, as it does at the search budget, and the
21
+ next run starts with the same person.
22
+ - **On the Scan page:** "3 of 50 profile views today", with a bar and the picker.
23
+ - **Re-mapping someone** checks it before deleting their circle. A damaged record counts
24
+ the day's views as used. An import brings the other computer's setting only when this
25
+ one has none, like the search budget.
26
+ - **The Scan page asks what field you're in, before your first scan.** One optional step,
27
+ with the same picks as *Scores → Your sector*: tech, government and defense, dental, and
28
+ the rest. Companies in your field count for more, so your first scores already use it.
29
+ *Skip for now* carries on without one. Both stay at the bottom of the window while you
30
+ look through the list. It's asked once, and never if you already have a network or picked
31
+ a sector. Change it anytime on the Scores tab.
32
+ - **Bridge Chains opens any circle, as far as your scans reach.** Click someone in a
33
+ bridge's circle and their own circle opens in place: the people found in it once you
34
+ connected and scanned it (3rd degree, counted along the chain), and on from there. A
35
+ trail at the top, and Esc, go back. An empty circle says why and what gets it: connect,
36
+ then scan their circle. While one is being scanned, it fills in as the scan saves.
37
+ Someone ready for a scan goes to the Scan page with them picked instead, since their
38
+ circle is empty until then.
39
+ - **A soft glow on people ready for a scan.** In Bridge Chains, someone you added through
40
+ a circle whose own circle isn't scanned yet has a soft breathing halo, and their bridge
41
+ says how many are "ready". A hidden list is greyed with a lock instead. The halo stays
42
+ still with Reduce Motion on. The old rainbow glow is gone: it looked for them inside the
43
+ circle they came from, which accepting takes them out of, so it never showed.
44
+ - **Ready to scan (N), in the Degrees panel.** It replaces *Auto-Bridge Next*, which
45
+ started a scan in one click, by name, with no cost shown, and offered hidden lists again
46
+ and again. The list is everyone you added through a circle whose circle can be scanned,
47
+ strongest first, with the circle you found them in and what a scan costs. Hidden lists
48
+ stay in view, greyed with a lock.
49
+ - **Scan one circle, on the Scan page.** A ready person, from the list or Bridge Chains,
50
+ opens the Scan page with them picked: the cost (a profile view, then a search a page, up
51
+ to 100), what's left of the day's budget, how deep to read, and a button. Nothing starts
52
+ until it's pressed.
53
+ - **A release checks the website's version lines.** Before building anything, the release
54
+ workflow runs `scripts/check-site-version.mjs`: the site's `softwareVersion` and
55
+ `llms.txt` must name the version, and its `dateModified` and sitemap `lastmod` can't be
56
+ older than the release's date in the changelog. A stale site stops the release with a
57
+ line for each thing to change. A pre-release skips it. Anyone can run it before tagging.
58
+
59
+ ### Changed
60
+ - **A big circle spreads out.** A bridge's circle used to sit on one ring, so a few hundred
61
+ people were almost a solid line. It now fills rings from the inside out, as many as it
62
+ needs, with the highest tiers nearest the middle; drag to move, scroll or +/− to zoom.
63
+ Lots of bridges spread the same way, and so does the preview on hover.
64
+ - **The map picks up a finished scan by itself.** No refresh needed.
65
+ - **One rule for "ready".** Bridge Chains, the Degrees panel, Outlink's new doors and the
66
+ profile page's mapping bar now agree on who can be scanned next, and all leave hidden
67
+ lists out.
68
+ - **Photos stay on your Mac.** The app shows a profile photo only from the copy saved on
69
+ your computer, so looking at your network never contacts LinkedIn. It used to load a
70
+ photo from LinkedIn until a scan had saved it, and a rescan could put LinkedIn's link
71
+ back in place of a saved photo. Every page now also tells the browser to load pictures
72
+ from the app alone, and the scanner fetches photos from LinkedIn's image servers only.
73
+ - **Photos an older version kept as links are saved once.** Until then those people show
74
+ initials. The next scan saves them at its end, or *Save photos* on the Scan page does it
75
+ now, without opening a browser. A link more than a few weeks old has expired: that
76
+ person's photo comes back when they're next scanned. Offline, or with LinkedIn's image
77
+ server busy, nothing is forgotten, and *Save photos* stops and says why.
78
+ - **Government and military titles are read.** A senator, a governor, a mayor and a cabinet
79
+ secretary score like a C-suite; their deputies, state legislators, commissioners,
80
+ ambassadors and judges like a VP; a city councilmember or a sheriff like a director.
81
+ Officers by rank: a 3–4 star general or admiral 10, a 1–2 star 9, a colonel 7.5, a
82
+ lieutenant colonel 6.5, a major 5, and a commander by their unit. "(Ret.)" and
83
+ "Retired" make a role former. They count only where a title is written, and only where
84
+ their own organization is a government's or a service's: a staffer in an Office of the
85
+ Secretary, a veteran's civilian job, a city manager at Uber, a Rotary district governor,
86
+ a team captain or a Kentucky colonel isn't one. A title that doesn't say whose it is
87
+ ("Mayor", "Police Chief", "Ambassador") needs its government named, or stays "Title
88
+ unclear": a missed official is a neutral 4, a realtor read as a mayor would be an A.
89
+ Until now all of them were "Title unclear" (a U.S. senator scored 2.9, C).
90
+ - **One name: Six Degrees.** The header, the welcome screen, the loading and launch pages,
91
+ the side panel and the Terminal installer said "6 Degrees". They now say Six Degrees, as
92
+ the window title and the app in Applications already did.
93
+ - **Contributor docs and `.gitignore`.** CONTRIBUTING says npm releases go out through
94
+ trusted publishing, with no token to set, and that `npm run build:desktop` builds the app
95
+ releases ship (`build:app` is the old launcher). `.gitignore` keeps a LinkedIn export, a
96
+ database, a saved copy of a network and the scanner's signed-in browser profile out of a
97
+ commit, wherever they sit in the tree. A local build keeps them out of the app too, and
98
+ its list of uncommitted files inside the app now names ignored ones as well.
99
+ - **0.4.0-beta.1, promoted: this is now the Mac app and npm package everyone gets.** It adds:
100
+ - **Their circle, on every card,** with rarity beside the tier and LinkedIn's own mutual count.
101
+ - **One scan at a time,** and *Resume* beside *Rescan*.
102
+ - **Tiers on your own network's curve,** with unknown companies at a neutral 5.
103
+ - **631 public organizations** the built-in list leaves off, and **scoring 6.**
104
+ - **Scores, a tab of its own.**
105
+
106
+ The details are under 0.4.0-beta.1 below.
107
+ - **The download page** says what's new in 0.4.0.
108
+ - **Degrees' corner toggle says Orbit, which is what it shows.** It said "Galaxy", but
109
+ the Galaxy is a Network Circle view, so Degrees fell back to Orbit. It switches
110
+ between Orbit and Bridge Chains, and it's on whenever Orbit is showing.
111
+
112
+ ### Removed
113
+ - **About 630 lines of Galaxy code that never ran:** a Degrees drawing from before Orbit
114
+ and Bridge Chains. Nothing could open it. The Galaxy draws Network Circle only.
115
+
116
+ ### Fixed
117
+ - **A one-person scan notes a hidden list.** A card's Scan or Rescan that found someone's
118
+ list hidden said so and noted nothing, so they stayed "not scanned yet" and were offered
119
+ again forever. Only a batch noted it. Now every read from page 1 does.
120
+ - **A scan reads the list of the person you picked.** A card's Scan and Rescan went by
121
+ name, so with two connections of the same name the scanner could read the other one's
122
+ list. Every Scan button now goes by that person's profile, as Resume already did.
123
+ - **A company's name after "at" is no longer read as a title.** "Server at President Hotel"
124
+ scored as a C-suite (7.3, A), and so did an IT specialist in an "Office of the Chief
125
+ Information Officer". "Executive Assistant to the General Manager" scored as a VP.
126
+ - **↻ asks before it scans.** The round button in the header started *Check for new* the
127
+ moment it was clicked. It now says what it will do (open Chrome on your connections list
128
+ and read it until it reaches people already saved) and what it costs (no search budget,
129
+ but it is LinkedIn traffic from your account), and waits for OK.
130
+ - **No Scan buttons on the sample or a CSV import.** Every card there offered a scan of
131
+ that person's circle, and Degrees offered *Auto-Bridge Next*, but both networks live
132
+ only in the window, so the scan could only fail ("Bridge '…' not found in database").
133
+ In their place a line says scanning needs your own network.
134
+ - **A CSV import's dates are the day LinkedIn says.** Anywhere east of London, each "Connected
135
+ On" date landed a day early (28 Sep 2026 became 27 Sep). The import now reads it as the
136
+ calendar date it is, the way a scan already did, and a date it can't read is left blank
137
+ rather than guessed.
138
+ - **Opening a panel no longer rebuilds the Galaxy.** Opening or closing the side panel or
139
+ the Filter panel used to draw the Galaxy again from scratch: the layout started over,
140
+ the selection ring vanished, and a big network stalled the page. Now the view slides
141
+ over at the same zoom, so what was in the middle stays in the middle.
142
+ - **The selection ring stays on the person you picked.** It is tied to their dot, and it
143
+ comes back after a tier filter that still shows them. It used to follow the first dot
144
+ it found near its old spot, which could be someone else, even You. *Back to list*
145
+ clears it.
146
+ - **The selection ring pulses gently, as it was always meant to.** Its animation had only
147
+ ever been written in code that never ran. With Reduce Motion on, it holds still.
148
+ - **Hovering a dot eases it up to 1.5 times its size,** instead of jumping. With Reduce
149
+ Motion on, it still changes at once.
150
+ - **The Galaxy draws faster on a big network.** Colouring its lines searched every person
151
+ for every line: about 1.4 s at 30,000 people, on every draw. It now takes under a
152
+ millisecond.
153
+ - **The main screen no longer stalls when nothing is selected on a big network.** The
154
+ side panel's Power Rankings counted every tier again for each person in the list, even
155
+ with the panel closed. At 30,000 people that took about 20 s each time the screen
156
+ redrew, opening the Filter panel included.
157
+ - **On a laptop, the Galaxy keeps its usual layout with a panel open.** Its fixed phone
158
+ layout used to switch on whenever the graph was under 768 pixels wide, which a tier
159
+ filter picked with a panel open could do. It now follows the window, as the page does.
160
+
9
161
  ## [0.4.0-beta.1] - 2026-09-28 (beta: a pre-release, never installed automatically)
10
162
 
11
163
  ### Added
package/README.md CHANGED
@@ -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**
package/SECURITY.md CHANGED
@@ -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
package/db/schema.js CHANGED
@@ -9,7 +9,7 @@
9
9
  // Every statement is CREATE ... IF NOT EXISTS, so applying it is both
10
10
  // first-run setup and a no-op on every later boot.
11
11
 
12
- export const SCHEMA_SQL = `-- 6 Degrees — local SQLite schema.
12
+ export const SCHEMA_SQL = `-- Six Degrees — local SQLite schema.
13
13
  --
14
14
  -- Ported from the Postgres schema this project ran on before it went
15
15
  -- local-first. Type mapping: uuid -> TEXT (generated in JS), jsonb and
@@ -0,0 +1,139 @@
1
+ // Where Bridge Chains puts a circle's dots.
2
+ //
3
+ // Blake, 2026-09-28: "when i scan someone and have a lot of 2nd degree its
4
+ // almost a solid line and needs to expand more". Every dot sat on one ring, so
5
+ // 200 people round one circle were a band and 800 were a solid line. Here a
6
+ // circle fills rings from the inside out instead, as many as its count needs:
7
+ // a small circle is one ring, a big one spreads into more, and neighbours keep
8
+ // their distance. Plain maths, no React; tests/chain-layout.test.mjs.
9
+
10
+ const TAU = Math.PI * 2;
11
+
12
+ /** How many dots fit on a ring of `radius` across `sweep` radians, `spacing` apart. */
13
+ function capacity(radius, spacing, sweep) {
14
+ if (radius <= 0) return 1;
15
+ const arc = sweep * radius;
16
+ // A whole circle closes on itself; an arc has a dot at each end.
17
+ return Math.max(1, sweep >= TAU - 1e-9 ? Math.floor(arc / spacing) : Math.floor(arc / spacing) + 1);
18
+ }
19
+
20
+ /** The rings from `inner` outwards, `spacing` apart, until `n` dots fit. */
21
+ function ringsFor(n, inner, spacing, sweep) {
22
+ const radii = [];
23
+ let room = 0;
24
+ for (let r = inner; room < n; r += spacing) {
25
+ radii.push(r);
26
+ room += capacity(r, spacing, sweep);
27
+ }
28
+ return radii;
29
+ }
30
+
31
+ const outerOf = (n, inner, spacing, sweep) => {
32
+ const radii = ringsFor(n, inner, spacing, sweep);
33
+ return radii[radii.length - 1];
34
+ };
35
+
36
+ /**
37
+ * Places `n` dots in rings round a centre at (0, 0).
38
+ *
39
+ * The first ring sits at `inner`, and each next one `spacing` further out.
40
+ * When that would run past `outer`, the rings first start further in (down to
41
+ * `innerMin`), then close up (down to `minSpacing`); past that they carry on
42
+ * beyond `outer` rather than overlap, and zooming out shows them. The dots on
43
+ * each ring share it evenly, in proportion to its length, so every ring is
44
+ * about as full as the rest; the first `n` of a list sorted best-first land
45
+ * nearest the centre. Every other ring is turned by half a step, so dots on
46
+ * neighbouring rings don't line up in spokes.
47
+ *
48
+ * `start` and `sweep` fill an arc instead of the whole circle, each ring from
49
+ * its middle outwards.
50
+ *
51
+ * @returns {{ spacing: number, inner: number, rings: { radius: number, count: number }[],
52
+ * points: { x: number, y: number, angle: number, ring: number }[] }}
53
+ */
54
+ export function ringLayout(n, {
55
+ inner, outer, spacing, minSpacing = spacing / 3, innerMin = inner,
56
+ start = -Math.PI / 2, sweep = TAU,
57
+ } = {}) {
58
+ const count = Math.max(0, Math.floor(n) || 0);
59
+ if (!count) return { spacing, inner, rings: [], points: [] };
60
+
61
+ let s = spacing;
62
+ let r0 = inner;
63
+ if (outerOf(count, r0, s, sweep) > outer) {
64
+ // Start further in, as far as innerMin, before closing anything up.
65
+ if (outerOf(count, innerMin, s, sweep) <= outer) {
66
+ let lo = innerMin, hi = inner;
67
+ for (let i = 0; i < 24; i++) {
68
+ const mid = (lo + hi) / 2;
69
+ if (outerOf(count, mid, s, sweep) <= outer) lo = mid; else hi = mid;
70
+ }
71
+ r0 = lo;
72
+ } else {
73
+ r0 = innerMin;
74
+ let lo = minSpacing, hi = spacing;
75
+ if (outerOf(count, r0, lo, sweep) <= outer) {
76
+ for (let i = 0; i < 24; i++) {
77
+ const mid = (lo + hi) / 2;
78
+ if (outerOf(count, r0, mid, sweep) <= outer) lo = mid; else hi = mid;
79
+ }
80
+ }
81
+ s = lo;
82
+ }
83
+ }
84
+
85
+ const radii = ringsFor(count, r0, s, sweep);
86
+ const caps = radii.map((r) => capacity(r, s, sweep));
87
+ const room = caps.reduce((a, b) => a + b, 0);
88
+
89
+ // Share the dots in proportion to each ring's room, never over it; what the
90
+ // rounding leaves goes to the rings with the most room left.
91
+ const counts = caps.map((c) => Math.min(c, Math.floor((count * c) / room)));
92
+ let left = count - counts.reduce((a, b) => a + b, 0);
93
+ while (left > 0) {
94
+ let best = -1;
95
+ for (let k = 0; k < caps.length; k++) {
96
+ if (counts[k] < caps[k] && (best < 0 || caps[k] - counts[k] > caps[best] - counts[best])) best = k;
97
+ }
98
+ counts[best]++;
99
+ left--;
100
+ }
101
+
102
+ const whole = sweep >= TAU - 1e-9;
103
+ const points = [];
104
+ const rings = [];
105
+ radii.forEach((radius, k) => {
106
+ const m = counts[k];
107
+ if (!m) return;
108
+ rings.push({ radius, count: m });
109
+ // Round a whole circle evenly; across an arc from one edge to the other, a
110
+ // lone dot in the middle of it. An arc fills from its middle outwards, so
111
+ // the first in the list sit nearest whatever the arc fans out from.
112
+ const step = whole ? TAU / m : m > 1 ? sweep / (m - 1) : 0;
113
+ const from = whole ? start + (k % 2 ? step / 2 : 0) : m > 1 ? start : start + sweep / 2;
114
+ const slots = whole ? null : middleOut(m);
115
+ for (let j = 0; j < m; j++) {
116
+ const angle = from + (slots ? slots[j] : j) * step;
117
+ points.push({ x: radius * Math.cos(angle), y: radius * Math.sin(angle), angle, ring: rings.length - 1 });
118
+ }
119
+ });
120
+ return { spacing: s, inner: r0, rings, points };
121
+ }
122
+
123
+ /** 0 … m-1 from the middle outwards: for 5, 2 3 1 4 0. */
124
+ function middleOut(m) {
125
+ let lo = Math.floor((m - 1) / 2);
126
+ let hi = lo + 1;
127
+ const out = [lo--];
128
+ while (out.length < m) {
129
+ if (hi < m) out.push(hi++);
130
+ if (out.length < m && lo >= 0) out.push(lo--);
131
+ }
132
+ return out;
133
+ }
134
+
135
+ /** A dot's radius for a layout's spacing: bigger for higher tiers, never touching a neighbour. */
136
+ export function dotRadius(spacing, tier) {
137
+ const scale = tier === 'S' ? 1 : tier === 'A' ? 0.86 : tier === 'B' ? 0.72 : 0.6;
138
+ return Math.max(1.4, Math.min(7, spacing * 0.42 * scale));
139
+ }
package/lib/csv.js CHANGED
@@ -1,4 +1,5 @@
1
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
@@ -136,12 +137,9 @@ export function parseConnectionsCsv(text) {
136
137
  if (seen.has(id)) { skipped++; continue; }
137
138
  seen.add(id);
138
139
 
139
- let connected_date = null;
140
- const raw = cell(iConnected);
141
- if (raw) {
142
- const d = new Date(raw);
143
- if (!isNaN(d.getTime())) connected_date = d.toISOString().split('T')[0];
144
- }
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));
145
143
 
146
144
  connections.push(connectionFrom({ id, name, role, company, profileUrl, connected_date }));
147
145
  }
@@ -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/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.
@@ -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
  }