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
@@ -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
@@ -23,9 +23,9 @@ const eslintConfig = defineConfig([
23
23
  // surface in review.
24
24
  "react-hooks/set-state-in-effect": "warn",
25
25
 
26
- // Avatars are user-supplied remote URLs (or local files captured by the
27
- // scraper) with graceful initial-letter fallbacks. next/image would add a
28
- // loader and per-request cost for no benefit here.
26
+ // Avatars are small local files the scanner saved (lib/photos.js), with
27
+ // graceful initial-letter fallbacks. next/image would add a loader and
28
+ // per-request cost for no benefit here.
29
29
  "@next/next/no-img-element": "off",
30
30
  },
31
31
  },
@@ -1,5 +1,5 @@
1
1
  #!/bin/bash
2
- # Install 6 Degrees on a Mac with one line:
2
+ # Install Six Degrees on a Mac with one line:
3
3
  #
4
4
  # curl -fsSL https://raw.githubusercontent.com/blakeb056/six-degrees/main/install.sh | bash
5
5
  #
@@ -108,7 +108,7 @@ stop_running_copy() {
108
108
  }
109
109
 
110
110
  main() {
111
- printf '\n 6 Degrees — installer\n\n'
111
+ printf '\n Six Degrees — installer\n\n'
112
112
 
113
113
  [ "$(uname -s)" = "Darwin" ] || fail "This installer is for macOS. On Linux, run: npx six-degrees (needs Node 22.13 or later). Windows isn't supported yet."
114
114
 
@@ -182,7 +182,7 @@ main() {
182
182
 
183
183
  if [ -z "${SIX_DEGREES_NO_OPEN:-}" ]; then
184
184
  open "$target"
185
- printf '\n ✓ Opening 6 Degrees. Next time, open it from Applications or Spotlight.\n'
185
+ printf '\n ✓ Opening Six Degrees. Next time, open it from Applications or Spotlight.\n'
186
186
  else
187
187
  printf '\n ✓ Done.\n'
188
188
  fi
@@ -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
+ }
@@ -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
@@ -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
+ }
@@ -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
  }
@@ -0,0 +1,47 @@
1
+ // Profile photos are shown from this computer only.
2
+ //
3
+ // The scanner saves each photo into the data folder as it reads it
4
+ // (scripts/image_store.py), and the row points at the saved file,
5
+ // `/avatars/<name>`, which app/avatars/[file] serves. A row can also hold a
6
+ // link: LinkedIn's own, which older versions stored until the scanner replaced
7
+ // it, and which a copy of a network made by one carries too. A page never loads
8
+ // a link: it would contact LinkedIn while someone is only looking at their map,
9
+ // and the app goes online only when you act. That person shows initials until
10
+ // the photo is saved, at the next scan or with Save photos on the Scan page.
11
+ // The Content-Security-Policy in next.config.mjs holds the same line in the
12
+ // browser.
13
+ //
14
+ // No imports, so the views and the server share it: every view and the routes
15
+ // that store photos check with localPhoto, and the /avatars route serves the
16
+ // names AVATAR_FILE allows.
17
+
18
+ /** A photo the /avatars route serves: a plain file name, no folders. The route uses this same pattern. */
19
+ export const AVATAR_FILE = /^[A-Za-z0-9_-]+\.(webp|jpg|jpeg|png)$/;
20
+
21
+ const PREFIX = '/avatars/';
22
+
23
+ /** The photo to show for a row's `profile_image_url`: its saved file's path, or null. */
24
+ export function localPhoto(url) {
25
+ if (typeof url !== 'string' || !url.startsWith(PREFIX)) return null;
26
+ return AVATAR_FILE.test(url.slice(PREFIX.length)) ? url : null;
27
+ }
28
+
29
+ /**
30
+ * The photos still kept as links, one per person and link:
31
+ * [{ profileUrl, imageUrl }]. `db` is an open node:sqlite database. The
32
+ * scanner saves each (`GET /api/update-images`), or forgets the link on a
33
+ * definite no, so a link is tried until there's an answer.
34
+ */
35
+ export function waitingPhotos(db) {
36
+ return db.prepare(`SELECT DISTINCT profile_url AS profileUrl, profile_image_url AS imageUrl
37
+ FROM linkedin_connections
38
+ WHERE profile_image_url IS NOT NULL AND profile_image_url <> ''
39
+ AND substr(profile_image_url, 1, 9) <> '/avatars/'
40
+ ORDER BY profile_url`).all()
41
+ .filter((r) => r.profileUrl && !localPhoto(r.imageUrl));
42
+ }
43
+
44
+ /** How many people have a photo waiting to be saved. */
45
+ export function waitingPhotoCount(db) {
46
+ return new Set(waitingPhotos(db).map((r) => r.profileUrl)).size;
47
+ }
@@ -9,6 +9,8 @@
9
9
  // sent, people who actually joined your network), never from browsing.
10
10
  // Plain functions; tests/quest.test.mjs.
11
11
 
12
+ import { circleState } from './reach.js';
13
+
12
14
  export const STAGE_SIZE = 5;
13
15
  export const XP_SEND = { S: 25, A: 15, B: 10, C: 5, D: 2 };
14
16
  export const XP_ADDED = { S: 100, A: 60, B: 35, C: 15, D: 5 };
@@ -26,8 +28,9 @@ export function levelFor(points) {
26
28
  * @param sentIds ids marked "invite sent"
27
29
  * @param added 1st-degree people who came through a cluster (unlocked_from_bridge_id set)
28
30
  * @param mappedIds ids of 1st-degree people whose own circle is mapped
31
+ * @param reach lib/reach.js reachIndex(), which also knows hidden lists; mappedIds stands in without it
29
32
  */
30
- export function buildQuest({ recs = [], sentIds = new Set(), added = [], mappedIds = new Set() }) {
33
+ export function buildQuest({ recs = [], sentIds = new Set(), added = [], mappedIds = new Set(), reach = null }) {
31
34
  const byBridge = new Map();
32
35
  const seen = new Set();
33
36
  for (const r of [...recs].sort((a, b) => (b.priority || 0) - (a.priority || 0))) {
@@ -112,8 +115,10 @@ export function buildQuest({ recs = [], sentIds = new Set(), added = [], mappedI
112
115
  level: levelFor(points),
113
116
  sentTotal,
114
117
  addedTotal,
115
- // People you've added whose own circle isn't mapped yet: the next degree.
116
- newDoors: added.filter((a) => a.unlocked_from_bridge_id && !mappedIds.has(a.id)),
118
+ // People you've added whose own circle can be scanned: the next degree.
119
+ // Hidden lists aren't doors (lib/reach.js, the rule Bridge Chains uses).
120
+ newDoors: added.filter((a) => a.unlocked_from_bridge_id
121
+ && circleState(a, reach || { scanned: mappedIds, hidden: new Set() }) === 'todo'),
117
122
  clearedStages: clusters.reduce((s, c) => s + (c.stage - 1) + (c.complete ? 1 : 0), 0),
118
123
  };
119
124
  }
@@ -0,0 +1,113 @@
1
+ // Who you reached through a circle, and whether their own circle is next.
2
+ //
3
+ // Blake, 2026-09-25: "i would like to sigingy if a orb in the 2nd degree is
4
+ // added already so it shows they are ready for a 2nd degree scan to get the 3rd
5
+ // degree so it needs to be intuitive with the orb glowing".
6
+ //
7
+ // "Ready" is one rule, kept here once. It used to live in three slightly
8
+ // different copies (Outlink's new doors, the profile page's mapping bar and the
9
+ // Degrees panel's Auto-Bridge Next), and only one of them knew about hidden
10
+ // lists. Someone is ready for a circle scan when all of these are true:
11
+ // - they're your connection now (degree 1);
12
+ // - they came to you through a circle: unlocked_from_bridge_id, which
13
+ // lib/promote.js keeps when it clears source_connection_id, and which a
14
+ // request sent from the app sets to the circle you asked through;
15
+ // - their own circle hasn't been scanned: nobody in it is saved, and the
16
+ // scanner has no note of reading it (bridge-progress.json);
17
+ // - their list isn't known to be hidden (bridge-skips.json).
18
+ // A glow that fired on hidden lists would teach you to ignore it, so a hidden
19
+ // list is its own state, never "ready".
20
+ //
21
+ // Plain functions, no React; tests/reach.test.mjs.
22
+
23
+ import { keyFor, compareBridges } from './separation.js';
24
+
25
+ /**
26
+ * What the rules read, built once per load.
27
+ *
28
+ * @param connections 1st-degree rows
29
+ * @param degree2 2nd-degree rows: whose circle each sits in
30
+ * @param notes what the scanner noted, from /api/scraper?reach=1:
31
+ * { skips: [{ profileUrl }] (hidden lists), read: [profileUrl] (lists it has read) }
32
+ * @returns {{ scanned: Set, read: Set, hidden: Set, reachedFrom: Map }}
33
+ * `reachedFrom` is circle id → the connections you reached through it.
34
+ */
35
+ export function reachIndex(connections = [], degree2 = [], notes = {}) {
36
+ const scanned = new Set();
37
+ for (const row of degree2 || []) {
38
+ if (row?.source_connection_id != null) scanned.add(row.source_connection_id);
39
+ }
40
+ const hidden = new Set();
41
+ for (const s of notes?.skips || []) {
42
+ if (s?.profileUrl) hidden.add(keyFor({ profile_url: s.profileUrl }));
43
+ }
44
+ const read = new Set();
45
+ for (const url of notes?.read || []) {
46
+ if (url) read.add(keyFor({ profile_url: url }));
47
+ }
48
+ const reachedFrom = new Map();
49
+ for (const row of connections || []) {
50
+ const from = row?.unlocked_from_bridge_id;
51
+ if (from == null || row.degree !== 1) continue;
52
+ let list = reachedFrom.get(from);
53
+ if (!list) reachedFrom.set(from, (list = []));
54
+ list.push(row);
55
+ }
56
+ return { scanned, read, hidden, reachedFrom };
57
+ }
58
+
59
+ /**
60
+ * Their own circle: 'scanned' (someone in it is saved, or the scanner read
61
+ * their list and everyone on it was already yours), 'hidden' (they keep their
62
+ * connections private) or 'todo'.
63
+ */
64
+ export function circleState(person, reach) {
65
+ if (!person) return 'todo';
66
+ if (reach.scanned.has(person.id)) return 'scanned';
67
+ if (person.profile_url && reach.read?.has(keyFor(person))) return 'scanned';
68
+ if (person.profile_url && reach.hidden.has(keyFor(person))) return 'hidden';
69
+ return 'todo';
70
+ }
71
+
72
+ /**
73
+ * For someone you reached through a circle: 'ready' (their circle can be
74
+ * scanned next), 'hidden' or 'scanned'. Null for anyone else: people who were
75
+ * always your connections, and 2nd-degree rows (a request sent from the app
76
+ * marks those with the circle it asked through, but they aren't yours yet).
77
+ */
78
+ export function reachState(person, reach) {
79
+ if (!person || person.degree !== 1 || person.unlocked_from_bridge_id == null) return null;
80
+ const state = circleState(person, reach);
81
+ return state === 'todo' ? 'ready' : state;
82
+ }
83
+
84
+ /** Everyone ready for a circle scan, strongest first. */
85
+ export function readyToScan(connections, reach) {
86
+ return (connections || []).filter((c) => reachState(c, reach) === 'ready').sort(compareBridges);
87
+ }
88
+
89
+ /** How many people you reached through each circle are ready, by circle id. */
90
+ export function readyByCircle(reach) {
91
+ const out = new Map();
92
+ for (const [from, people] of reach.reachedFrom) {
93
+ const n = people.filter((p) => reachState(p, reach) === 'ready').length;
94
+ if (n) out.set(from, n);
95
+ }
96
+ return out;
97
+ }
98
+
99
+ /**
100
+ * What scanning one circle asks of LinkedIn: one profile view to find their
101
+ * list, then a search for every page of it, `pages` at most (LinkedIn shows
102
+ * 100). About 0.55 minutes a page with the scanner's rests (20 s before each
103
+ * page, a minute more after every 10), so about 55 minutes for a whole list.
104
+ * The Scan page and the Degrees panel both say it from here.
105
+ */
106
+ export function circleScanCost(pages = 100) {
107
+ const searches = Math.max(1, Math.min(100, Math.round(pages) || 100));
108
+ return {
109
+ profileViews: 1,
110
+ searches,
111
+ minutes: searches >= 100 ? 55 : Math.max(5, Math.round(searches * 0.55)),
112
+ };
113
+ }
@@ -40,6 +40,7 @@ export function runningNow(job) {
40
40
  case 'install': return 'The scanner\'s packages are being installed';
41
41
  case 'setup': return 'The scanner is being set up';
42
42
  case 'login': return 'The LinkedIn sign-in window is open';
43
+ case 'photos': return 'Profile photos are being saved';
43
44
  default: return 'A scan is running';
44
45
  }
45
46
  }
@@ -1,5 +1,6 @@
1
1
  // What the Scan page's first step says and offers, from the checks GET
2
- // /api/scraper reports (app/api/scraper/route.js, lib/scanner-python.js).
2
+ // /api/scraper reports (app/api/scraper/route.js, lib/scanner-python.js), and
3
+ // whether it asks for your field before that (askForField).
3
4
  // Plain data in, plain data out: the page draws it, and
4
5
  // tests/scanner-setup.test.mjs checks every state without a browser. No Node
5
6
  // imports here; the page runs in the browser.
@@ -102,3 +103,21 @@ function stepOnly(status) {
102
103
  button: null,
103
104
  };
104
105
  }
106
+
107
+ /**
108
+ * Whether the Scan page asks for your field first (app/components/FieldStep.js):
109
+ * once, before the first scan. Never to someone who already has a network, has
110
+ * picked a sector (Scores → Your sector), or answered or skipped the question
111
+ * before (`fieldAsked`). `status` is GET /api/scraper's answer (null while it
112
+ * loads), `settings` GET /api/settings' `settings` (undefined while it loads,
113
+ * null when it couldn't be read). null means not known yet: the page waits,
114
+ * rather than show the steps and then swap them for the question.
115
+ */
116
+ export function askForField(status, settings) {
117
+ if (!status) return null;
118
+ const n = status.network;
119
+ // No counts means the app couldn't be asked; a scan running is past asking.
120
+ if (!n || n.first || n.second || n.third || status.running) return false;
121
+ if (settings === undefined) return null;
122
+ return Boolean(settings && !settings.fieldAsked && !settings.sectorFocus?.sectors?.length);
123
+ }