six-degrees 0.4.0 → 0.4.2

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 (209) hide show
  1. package/.next/standalone/.next/BUILD_ID +1 -1
  2. package/.next/standalone/.next/build-manifest.json +3 -3
  3. package/.next/standalone/.next/prerender-manifest.json +14 -14
  4. package/.next/standalone/.next/server/app/_global-error/page.js.nft.json +1 -1
  5. package/.next/standalone/.next/server/app/_global-error.html +1 -1
  6. package/.next/standalone/.next/server/app/_global-error.rsc +2 -2
  7. package/.next/standalone/.next/server/app/_global-error.segments/__PAGE__.segment.rsc +1 -1
  8. package/.next/standalone/.next/server/app/_global-error.segments/_full.segment.rsc +2 -2
  9. package/.next/standalone/.next/server/app/_global-error.segments/_tree.segment.rsc +1 -1
  10. package/.next/standalone/.next/server/app/_not-found/page.js.nft.json +1 -1
  11. package/.next/standalone/.next/server/app/_not-found.html +1 -1
  12. package/.next/standalone/.next/server/app/_not-found.rsc +2 -2
  13. package/.next/standalone/.next/server/app/_not-found.segments/_full.segment.rsc +2 -2
  14. package/.next/standalone/.next/server/app/_not-found.segments/_not-found/__PAGE__.segment.rsc +1 -1
  15. package/.next/standalone/.next/server/app/_not-found.segments/_tree.segment.rsc +1 -1
  16. package/.next/standalone/.next/server/app/api/admin-delete/route.js.nft.json +1 -1
  17. package/.next/standalone/.next/server/app/api/admin-update/route.js.nft.json +1 -1
  18. package/.next/standalone/.next/server/app/api/bridges/route.js.nft.json +1 -1
  19. package/.next/standalone/.next/server/app/api/bulk-import/route.js.nft.json +1 -1
  20. package/.next/standalone/.next/server/app/api/company-scores/legacy/route.js.nft.json +1 -1
  21. package/.next/standalone/.next/server/app/api/company-scores/route.js.nft.json +1 -1
  22. package/.next/standalone/.next/server/app/api/connections/route.js.nft.json +1 -1
  23. package/.next/standalone/.next/server/app/api/data/export/route.js.nft.json +1 -1
  24. package/.next/standalone/.next/server/app/api/data/import/route.js.nft.json +1 -1
  25. package/.next/standalone/.next/server/app/api/data/restart/route.js.nft.json +1 -1
  26. package/.next/standalone/.next/server/app/api/data/reveal/route.js.nft.json +1 -1
  27. package/.next/standalone/.next/server/app/api/data/route.js.nft.json +1 -1
  28. package/.next/standalone/.next/server/app/api/delete-cluster/route.js.nft.json +1 -1
  29. package/.next/standalone/.next/server/app/api/ingest/route.js +1 -1
  30. package/.next/standalone/.next/server/app/api/ingest/route.js.nft.json +1 -1
  31. package/.next/standalone/.next/server/app/api/network/route.js.nft.json +1 -1
  32. package/.next/standalone/.next/server/app/api/notifications/route.js.nft.json +1 -1
  33. package/.next/standalone/.next/server/app/api/outreach/route.js.nft.json +1 -1
  34. package/.next/standalone/.next/server/app/api/queue/route.js.nft.json +1 -1
  35. package/.next/standalone/.next/server/app/api/scraper/route.js.nft.json +1 -1
  36. package/.next/standalone/.next/server/app/api/settings/route.js.nft.json +1 -1
  37. package/.next/standalone/.next/server/app/api/settings/sector-preview/route.js.nft.json +1 -1
  38. package/.next/standalone/.next/server/app/api/settings/sector-suggestions/route.js.nft.json +1 -1
  39. package/.next/standalone/.next/server/app/api/setup-profile/route.js.nft.json +1 -1
  40. package/.next/standalone/.next/server/app/api/unlock/route.js.nft.json +1 -1
  41. package/.next/standalone/.next/server/app/api/update/route.js.nft.json +1 -1
  42. package/.next/standalone/.next/server/app/api/update-images/route.js.nft.json +1 -1
  43. package/.next/standalone/.next/server/app/api/users/route.js.nft.json +1 -1
  44. package/.next/standalone/.next/server/app/avatars/[file]/route.js.nft.json +1 -1
  45. package/.next/standalone/.next/server/app/favicon.ico/route.js.nft.json +1 -1
  46. package/.next/standalone/.next/server/app/favicon.ico.body +0 -0
  47. package/.next/standalone/.next/server/app/import/page.js.nft.json +1 -1
  48. package/.next/standalone/.next/server/app/import.html +1 -1
  49. package/.next/standalone/.next/server/app/import.rsc +2 -2
  50. package/.next/standalone/.next/server/app/import.segments/_full.segment.rsc +2 -2
  51. package/.next/standalone/.next/server/app/import.segments/_tree.segment.rsc +1 -1
  52. package/.next/standalone/.next/server/app/import.segments/import/__PAGE__.segment.rsc +1 -1
  53. package/.next/standalone/.next/server/app/index.html +1 -1
  54. package/.next/standalone/.next/server/app/index.rsc +3 -3
  55. package/.next/standalone/.next/server/app/index.segments/__PAGE__.segment.rsc +2 -2
  56. package/.next/standalone/.next/server/app/index.segments/_full.segment.rsc +3 -3
  57. package/.next/standalone/.next/server/app/index.segments/_tree.segment.rsc +1 -1
  58. package/.next/standalone/.next/server/app/launch/page.js.nft.json +1 -1
  59. package/.next/standalone/.next/server/app/launch.html +2 -2
  60. package/.next/standalone/.next/server/app/launch.rsc +2 -2
  61. package/.next/standalone/.next/server/app/launch.segments/_full.segment.rsc +2 -2
  62. package/.next/standalone/.next/server/app/launch.segments/_tree.segment.rsc +1 -1
  63. package/.next/standalone/.next/server/app/launch.segments/launch/__PAGE__.segment.rsc +1 -1
  64. package/.next/standalone/.next/server/app/page.js.nft.json +1 -1
  65. package/.next/standalone/.next/server/app/page_client-reference-manifest.js +1 -1
  66. package/.next/standalone/.next/server/app/paths/page.js.nft.json +1 -1
  67. package/.next/standalone/.next/server/app/paths/page_client-reference-manifest.js +1 -1
  68. package/.next/standalone/.next/server/app/paths.html +1 -1
  69. package/.next/standalone/.next/server/app/paths.rsc +3 -3
  70. package/.next/standalone/.next/server/app/paths.segments/_full.segment.rsc +3 -3
  71. package/.next/standalone/.next/server/app/paths.segments/_tree.segment.rsc +1 -1
  72. package/.next/standalone/.next/server/app/paths.segments/paths/__PAGE__.segment.rsc +2 -2
  73. package/.next/standalone/.next/server/app/profile/page.js.nft.json +1 -1
  74. package/.next/standalone/.next/server/app/profile/page_client-reference-manifest.js +1 -1
  75. package/.next/standalone/.next/server/app/profile.html +1 -1
  76. package/.next/standalone/.next/server/app/profile.rsc +3 -3
  77. package/.next/standalone/.next/server/app/profile.segments/_full.segment.rsc +3 -3
  78. package/.next/standalone/.next/server/app/profile.segments/_tree.segment.rsc +1 -1
  79. package/.next/standalone/.next/server/app/profile.segments/profile/__PAGE__.segment.rsc +2 -2
  80. package/.next/standalone/.next/server/app/queue/page.js.nft.json +1 -1
  81. package/.next/standalone/.next/server/app/queue/page_client-reference-manifest.js +1 -1
  82. package/.next/standalone/.next/server/app/queue.html +1 -1
  83. package/.next/standalone/.next/server/app/queue.rsc +3 -3
  84. package/.next/standalone/.next/server/app/queue.segments/_full.segment.rsc +3 -3
  85. package/.next/standalone/.next/server/app/queue.segments/_tree.segment.rsc +1 -1
  86. package/.next/standalone/.next/server/app/queue.segments/queue/__PAGE__.segment.rsc +2 -2
  87. package/.next/standalone/.next/server/app/scores/page.js.nft.json +1 -1
  88. package/.next/standalone/.next/server/app/scores.html +1 -1
  89. package/.next/standalone/.next/server/app/scores.rsc +2 -2
  90. package/.next/standalone/.next/server/app/scores.segments/_full.segment.rsc +2 -2
  91. package/.next/standalone/.next/server/app/scores.segments/_tree.segment.rsc +1 -1
  92. package/.next/standalone/.next/server/app/scores.segments/scores/__PAGE__.segment.rsc +1 -1
  93. package/.next/standalone/.next/server/app/settings/page.js.nft.json +1 -1
  94. package/.next/standalone/.next/server/app/settings.html +1 -1
  95. package/.next/standalone/.next/server/app/settings.rsc +2 -2
  96. package/.next/standalone/.next/server/app/settings.segments/_full.segment.rsc +2 -2
  97. package/.next/standalone/.next/server/app/settings.segments/_tree.segment.rsc +1 -1
  98. package/.next/standalone/.next/server/app/settings.segments/settings/__PAGE__.segment.rsc +1 -1
  99. package/.next/standalone/.next/server/app/setup/page.js.nft.json +1 -1
  100. package/.next/standalone/.next/server/app/setup/page_client-reference-manifest.js +1 -1
  101. package/.next/standalone/.next/server/app/setup.html +1 -1
  102. package/.next/standalone/.next/server/app/setup.rsc +3 -3
  103. package/.next/standalone/.next/server/app/setup.segments/_full.segment.rsc +3 -3
  104. package/.next/standalone/.next/server/app/setup.segments/_tree.segment.rsc +1 -1
  105. package/.next/standalone/.next/server/app/setup.segments/setup/__PAGE__.segment.rsc +2 -2
  106. package/.next/standalone/.next/server/chunks/[root-of-the-server]__0e0fei3._.js +1 -1
  107. package/.next/standalone/.next/server/chunks/{[root-of-the-server]__03ydxxh._.js → [root-of-the-server]__20tmem7._.js} +1 -1
  108. package/.next/standalone/.next/server/chunks/_1_fynia._.js +6 -6
  109. package/.next/standalone/.next/server/chunks/_1itd1o_._.js +1 -1
  110. package/.next/standalone/.next/server/chunks/_1iwkh8t._.js +1 -1
  111. package/.next/standalone/.next/server/chunks/_1mxn_lb._.js +1 -1
  112. package/.next/standalone/.next/server/chunks/lib_db-client_15le7cw.js +21 -7
  113. package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__00d8snt._.js +1 -1
  114. package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__00g61b8._.js +1 -1
  115. package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__08lf3gy._.js +1 -1
  116. package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__0hg5gib._.js +1 -1
  117. package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__0ho-u0w._.js +1 -1
  118. package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__0ze473p._.js +1 -1
  119. package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__1194jps._.js +1 -1
  120. package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__1cnxxc9._.js +1 -1
  121. package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__1ll97ry._.js +1 -1
  122. package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__1m0255u._.js +1 -1
  123. package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__1w9z0zv._.js +1 -1
  124. package/.next/standalone/.next/server/chunks/ssr/_15xuwkv._.js +1 -1
  125. package/.next/standalone/.next/server/chunks/ssr/_1hgd5dp._.js +1 -1
  126. package/.next/standalone/.next/server/chunks/ssr/_20w06i-._.js +1 -1
  127. package/.next/standalone/.next/server/chunks/ssr/app_page_1fazqbf.js +4 -4
  128. package/.next/standalone/.next/server/chunks/ssr/app_paths_page_1ol90m7.js +1 -1
  129. package/.next/standalone/.next/server/chunks/ssr/app_setup_page_1-282zx.js +6 -2
  130. package/.next/standalone/.next/server/middleware-build-manifest.js +3 -3
  131. package/.next/standalone/.next/server/middleware-manifest.json +5 -5
  132. package/.next/standalone/.next/server/pages/404.html +1 -1
  133. package/.next/standalone/.next/server/pages/500.html +1 -1
  134. package/.next/standalone/.next/server/server-reference-manifest.js +1 -1
  135. package/.next/standalone/.next/server/server-reference-manifest.json +1 -1
  136. package/.next/standalone/.next/static/chunks/{3e8zg6ctvvb8p.js → 049vavjsmqgmn.js} +3 -3
  137. package/.next/standalone/.next/static/chunks/0ljxa-wk_iu__.js +18 -0
  138. package/.next/standalone/.next/static/chunks/0mp9b72fm-jp6.js +6 -0
  139. package/.next/standalone/.next/static/chunks/1pre1cotntk6-.js +1 -0
  140. package/.next/standalone/.next/static/chunks/{18xu0fbgo_qz-.js → 22x2_r768-u5i.js} +1 -1
  141. package/.next/standalone/.next/static/chunks/2p5yt1o9tnxjn.js +2 -0
  142. package/.next/standalone/.next/static/chunks/3zs_6-nn0dapb.js +1 -0
  143. package/.next/standalone/.next/static/media/favicon.0zdzw5kurn8m-.ico +0 -0
  144. package/.next/standalone/CHANGELOG.md +86 -0
  145. package/.next/standalone/app/api/admin-delete/route.js +3 -0
  146. package/.next/standalone/app/api/ingest/route.js +13 -0
  147. package/.next/standalone/app/api/scraper/route.js +10 -2
  148. package/.next/standalone/app/components/ChainView.js +76 -15
  149. package/.next/standalone/app/components/FilterPanel.js +37 -1
  150. package/.next/standalone/app/components/ForceGraph.js +144 -12
  151. package/.next/standalone/app/components/LinkedInLimits.js +20 -3
  152. package/.next/standalone/app/components/OrbitGraph.js +92 -30
  153. package/.next/standalone/app/components/Sidebar.js +37 -0
  154. package/.next/standalone/app/components/views.js +7 -5
  155. package/.next/standalone/app/favicon.ico +0 -0
  156. package/.next/standalone/app/page.js +24 -7
  157. package/.next/standalone/app/paths/page.js +8 -1
  158. package/.next/standalone/db/schema.js +14 -0
  159. package/.next/standalone/lib/brokerage.js +94 -0
  160. package/.next/standalone/lib/chain-layout.js +89 -0
  161. package/.next/standalone/lib/circle.js +23 -4
  162. package/.next/standalone/lib/degrees.js +30 -0
  163. package/.next/standalone/lib/dot-rings.js +21 -0
  164. package/.next/standalone/lib/ingest.js +3 -3
  165. package/.next/standalone/lib/linkedin-limits.js +9 -4
  166. package/.next/standalone/lib/reach.js +22 -1
  167. package/.next/standalone/lib/search-risk.js +38 -0
  168. package/.next/standalone/lib/ties.js +23 -0
  169. package/.next/standalone/package-lock.json +2 -2
  170. package/.next/standalone/package.json +1 -1
  171. package/.next/standalone/scripts/scrape.py +50 -12
  172. package/.next/static/chunks/{3e8zg6ctvvb8p.js → 049vavjsmqgmn.js} +3 -3
  173. package/.next/static/chunks/0ljxa-wk_iu__.js +18 -0
  174. package/.next/static/chunks/0mp9b72fm-jp6.js +6 -0
  175. package/.next/static/chunks/1pre1cotntk6-.js +1 -0
  176. package/.next/static/chunks/{18xu0fbgo_qz-.js → 22x2_r768-u5i.js} +1 -1
  177. package/.next/static/chunks/2p5yt1o9tnxjn.js +2 -0
  178. package/.next/static/chunks/3zs_6-nn0dapb.js +1 -0
  179. package/.next/static/media/favicon.0zdzw5kurn8m-.ico +0 -0
  180. package/CHANGELOG.md +86 -0
  181. package/db/schema.js +14 -0
  182. package/lib/brokerage.js +94 -0
  183. package/lib/chain-layout.js +89 -0
  184. package/lib/circle.js +23 -4
  185. package/lib/degrees.js +30 -0
  186. package/lib/dot-rings.js +21 -0
  187. package/lib/ingest.js +3 -3
  188. package/lib/linkedin-limits.js +9 -4
  189. package/lib/reach.js +22 -1
  190. package/lib/search-risk.js +38 -0
  191. package/lib/ties.js +23 -0
  192. package/package.json +1 -1
  193. package/scripts/scrape.py +50 -12
  194. package/.next/standalone/.next/static/chunks/0w76iraw7ij3k.js +0 -18
  195. package/.next/standalone/.next/static/chunks/135n87ydxjrra.js +0 -2
  196. package/.next/standalone/.next/static/chunks/2pnbm1z6j7xab.js +0 -1
  197. package/.next/standalone/.next/static/chunks/3qh78g2aqhprd.js +0 -2
  198. package/.next/standalone/.next/static/media/favicon.2vob68tjqpejf.ico +0 -0
  199. package/.next/static/chunks/0w76iraw7ij3k.js +0 -18
  200. package/.next/static/chunks/135n87ydxjrra.js +0 -2
  201. package/.next/static/chunks/2pnbm1z6j7xab.js +0 -1
  202. package/.next/static/chunks/3qh78g2aqhprd.js +0 -2
  203. package/.next/static/media/favicon.2vob68tjqpejf.ico +0 -0
  204. /package/.next/standalone/.next/static/{WseE_5HuU-1q0ywC9CP3U → wGVrXBkFLmEB4snfhtOcO}/_buildManifest.js +0 -0
  205. /package/.next/standalone/.next/static/{WseE_5HuU-1q0ywC9CP3U → wGVrXBkFLmEB4snfhtOcO}/_clientMiddlewareManifest.js +0 -0
  206. /package/.next/standalone/.next/static/{WseE_5HuU-1q0ywC9CP3U → wGVrXBkFLmEB4snfhtOcO}/_ssgManifest.js +0 -0
  207. /package/.next/static/{WseE_5HuU-1q0ywC9CP3U → wGVrXBkFLmEB4snfhtOcO}/_buildManifest.js +0 -0
  208. /package/.next/static/{WseE_5HuU-1q0ywC9CP3U → wGVrXBkFLmEB4snfhtOcO}/_clientMiddlewareManifest.js +0 -0
  209. /package/.next/static/{WseE_5HuU-1q0ywC9CP3U → wGVrXBkFLmEB4snfhtOcO}/_ssgManifest.js +0 -0
@@ -5,8 +5,9 @@
5
5
  // changes here — the budget, and lifting a cooldown.
6
6
  //
7
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.
8
+ // views got an account restricted (TRAPS §16), so their cap has no "no limit",
9
+ // and the scanner also keeps any two a minute apart. Searches did too, at 373 in
10
+ // 24 hours: the page asks before a risky search budget (lib/search-risk.js).
10
11
  //
11
12
  // They belong to the LinkedIn account, not to a profile in the app. TRAPS §35.
12
13
 
@@ -58,14 +59,18 @@ export function nextMonthStartPacific(ms = Date.now()) {
58
59
  return monthStartPacific(start + 40 * DAY * 1000);
59
60
  }
60
61
 
62
+ const profileChoice = (n) => PROFILE_CHOICES.filter((c) => c <= n).pop() ?? PROFILE_CHOICES[0];
63
+
61
64
  export function readLimits(dir) {
62
65
  const raw = readJson(path.join(dir, 'scan-limits.json')) || {};
63
66
  const num = (v, d) => (Number.isInteger(v) && v >= 0 ? v : d);
64
67
  return {
65
68
  daily: num(raw.daily, DEFAULT_LIMITS.daily),
66
69
  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,
70
+ // 0 is "no limit" for searches; for profile views the scanner reads it as the
71
+ // default. A number typed in by hand counts as the largest choice under it,
72
+ // as the scanner reads it (scrape.py search_limits).
73
+ profiles: profileChoice(num(raw.profiles, 0) || DEFAULT_LIMITS.profiles),
69
74
  };
70
75
  }
71
76
 
package/lib/reach.js CHANGED
@@ -45,6 +45,10 @@ export function reachIndex(connections = [], degree2 = [], notes = {}) {
45
45
  for (const url of notes?.read || []) {
46
46
  if (url) read.add(keyFor({ profile_url: url }));
47
47
  }
48
+ const lists = new Map();
49
+ for (const [url, list] of Object.entries(notes?.lists || {})) {
50
+ if (url && list) lists.set(keyFor({ profile_url: url }), list);
51
+ }
48
52
  const reachedFrom = new Map();
49
53
  for (const row of connections || []) {
50
54
  const from = row?.unlocked_from_bridge_id;
@@ -53,7 +57,7 @@ export function reachIndex(connections = [], degree2 = [], notes = {}) {
53
57
  if (!list) reachedFrom.set(from, (list = []));
54
58
  list.push(row);
55
59
  }
56
- return { scanned, read, hidden, reachedFrom };
60
+ return { scanned, read, hidden, reachedFrom, lists };
57
61
  }
58
62
 
59
63
  /**
@@ -111,3 +115,20 @@ export function circleScanCost(pages = 100) {
111
115
  minutes: searches >= 100 ? 55 : Math.max(5, Math.round(searches * 0.55)),
112
116
  };
113
117
  }
118
+
119
+ /**
120
+ * How much of someone's circle is scanned, as 0–5 bars for the ring round
121
+ * their dot; null for no ring (not scanned, or their list is hidden).
122
+ * A list read to the end is 5. Part-read, it's the pages read against
123
+ * LinkedIn's own count of the list (about 10 a page), 1 to 4; with no count
124
+ * (read before the scanner kept one), 2.
125
+ */
126
+ export function scanBars(person, reach) {
127
+ const state = circleState(person, reach);
128
+ const list = person?.profile_url ? reach.lists?.get(keyFor(person)) : null;
129
+ if (state === 'hidden') return null;
130
+ if (!list) return state === 'scanned' ? 2 : null;
131
+ if (!list.more) return 5;
132
+ if (!list.total) return 2;
133
+ return Math.max(1, Math.min(4, Math.round((5 * Math.min(1, (list.pages * 10) / list.total)))));
134
+ }
@@ -0,0 +1,38 @@
1
+ // How much searching is asking for trouble, for the Scan page's budget picker.
2
+ // No Node imports, so the page and the tests share it (lib/linkedin-limits.js
3
+ // reads files and can't load in the browser).
4
+ //
5
+ // The number behind it (TRAPS §16): on 2026-09-28 a real account was restricted
6
+ // after 373 searches in 24 hours, run back to back, with the daily budget raised
7
+ // to 500 and no monthly cap. It opened 7 profiles that day, so searches alone
8
+ // were enough.
9
+
10
+ export const RISKY_DAILY = 100; // above this, the picker asks first
11
+ export const RESTRICTED_AT = 373; // searches in 24 hours when that account was restricted
12
+ export const SAFE_LIMITS = { daily: 50, monthly: 250 };
13
+
14
+ export const riskyDaily = (n) => Number(n) > RISKY_DAILY;
15
+ export const riskyLimits = ({ daily, monthly } = {}) => riskyDaily(daily) || monthly === 0;
16
+
17
+ /** What the picker asks before a change that raises the risk; null when there's nothing to ask. */
18
+ export function limitQuestion(from = {}, to = {}) {
19
+ if (riskyDaily(to.daily) && Number(to.daily) > Number(from.daily)) {
20
+ return `${to.daily} searches a day is more than LinkedIn has put up with. A real account was `
21
+ + `restricted after ${RESTRICTED_AT} searches in 24 hours, run back to back. The default is `
22
+ + `${SAFE_LIMITS.daily} a day.\n\nSet ${to.daily} a day anyway?`;
23
+ }
24
+ if (to.monthly === 0 && from.monthly !== 0) {
25
+ return 'With no monthly cap, only the daily budget stops a scan. A real account was restricted '
26
+ + `after ${RESTRICTED_AT} searches in 24 hours with no monthly cap set.\n\nTurn the monthly cap off anyway?`;
27
+ }
28
+ return null;
29
+ }
30
+
31
+ /** The note under the picker while the budget is risky; null when it isn't. */
32
+ export function limitNote({ daily, monthly } = {}) {
33
+ if (!riskyLimits({ daily, monthly })) return null;
34
+ const parts = [`A real account was restricted after ${RESTRICTED_AT} searches in 24 hours.`];
35
+ if (riskyDaily(daily)) parts.push(`${daily} a day can use up a free account's month in a day or two.`);
36
+ if (monthly === 0) parts.push('With no monthly cap, nothing but the daily budget stops a long run.');
37
+ return parts.join(' ');
38
+ }
package/lib/ties.js ADDED
@@ -0,0 +1,23 @@
1
+ // Ties between your own connections: two people you know who know each other.
2
+ //
3
+ // A circle scan reads everyone in someone's list, your own connections among
4
+ // them, and the app sets those aside rather than saving them as 2nd degree
5
+ // (lib/ingest.js splitAlreadyConnected). Each one is a tie between that
6
+ // person and the one whose circle it is. Kept in connection_ties (db/schema.js),
7
+ // for clustering and communities later; nothing reads them yet.
8
+
9
+ /** The pairs to keep: the circle's person with each of your connections in it, each pair once, smaller URL first. */
10
+ export function tiePairs(ownerUrl, urls = []) {
11
+ const seen = new Set();
12
+ const out = [];
13
+ if (!ownerUrl) return out;
14
+ for (const url of urls) {
15
+ if (!url || url === ownerUrl) continue;
16
+ const [a, b] = url < ownerUrl ? [url, ownerUrl] : [ownerUrl, url];
17
+ const key = `${a} ${b}`;
18
+ if (seen.has(key)) continue;
19
+ seen.add(key);
20
+ out.push({ a_url: a, b_url: b });
21
+ }
22
+ return out;
23
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "six-degrees",
3
- "version": "0.4.0",
3
+ "version": "0.4.2",
4
4
  "description": "Map your professional network as a galaxy — leverage tiers, bridges into new circles, and the shortest path to anyone. Runs locally on your own data.",
5
5
  "license": "MIT",
6
6
  "author": "Blake Burford",
package/scripts/scrape.py CHANGED
@@ -1504,16 +1504,29 @@ def take_profile_view(now=None):
1504
1504
  with _Locked("linkedin-activity"):
1505
1505
  now = now if now is not None else time.time()
1506
1506
  data = _read_activity()
1507
- views = [t for t in data["profiles"] if t > now - DAY_SECONDS]
1508
- if len(views) >= search_limits()["profiles"]:
1509
- return None
1510
- # A time ahead of this clock (another computer's, imported) is left
1511
- # out of the gap: waiting for it could never end.
1512
- last = max((t for t in views if t <= now), default=None)
1513
- if last is not None and now - last < PROFILE_GAP:
1514
- return int(last + PROFILE_GAP - now) + 1
1515
- _charge(data, "profiles", 1, now)
1516
- return 0
1507
+ wait = _profile_wait(data, now)
1508
+ if wait == 0:
1509
+ _charge(data, "profiles", 1, now)
1510
+ return wait
1511
+
1512
+
1513
+ def profile_wait(now=None):
1514
+ """take_profile_view's answer, writing nothing down: 0, seconds to wait, or None."""
1515
+ with _Locked("linkedin-activity"):
1516
+ now = now if now is not None else time.time()
1517
+ return _profile_wait(_read_activity(), now)
1518
+
1519
+
1520
+ def _profile_wait(data, now):
1521
+ views = [t for t in data["profiles"] if t > now - DAY_SECONDS]
1522
+ if len(views) >= search_limits()["profiles"]:
1523
+ return None
1524
+ # A time ahead of this clock (another computer's, imported) is left
1525
+ # out of the gap: waiting for it could never end.
1526
+ last = max((t for t in views if t <= now), default=None)
1527
+ if last is not None and now - last < PROFILE_GAP:
1528
+ return int(last + PROFILE_GAP - now) + 1
1529
+ return 0
1517
1530
 
1518
1531
 
1519
1532
  def search_limits():
@@ -1529,8 +1542,12 @@ def search_limits():
1529
1542
  return default
1530
1543
  return {"daily": num(data.get("daily"), DEFAULT_DAILY_SEARCHES),
1531
1544
  "monthly": num(data.get("monthly"), DEFAULT_MONTHLY_SEARCHES),
1532
- # 0 is no cap for searches, never for profile views.
1533
- "profiles": num(data.get("profiles"), DEFAULT_DAILY_PROFILES) or DEFAULT_DAILY_PROFILES}
1545
+ # 0 is no cap for searches, never for profile views. A number typed
1546
+ # in by hand counts as the largest choice under it: never past 100,
1547
+ # and the Scan page shows the same one.
1548
+ "profiles": max((c for c in (10, 25, 50, 100)
1549
+ if c <= (num(data.get("profiles"), DEFAULT_DAILY_PROFILES) or DEFAULT_DAILY_PROFILES)),
1550
+ default=10)}
1534
1551
 
1535
1552
 
1536
1553
  def linkedin_usage(now=None):
@@ -1684,6 +1701,12 @@ def _change_progress(change):
1684
1701
  print(f" (could not note how far this read got: {exc})")
1685
1702
 
1686
1703
 
1704
+ # How long each list is, from LinkedIn's "About N results" line on the first
1705
+ # page read (RESULT_COUNT_JS), kept until the read's progress is written: the
1706
+ # app's rings round a dot show pages read against it. Approximate, like the line.
1707
+ _LIST_TOTALS = {}
1708
+
1709
+
1687
1710
  def record_bridge_progress(profile_url, name, pages, more, urn=None):
1688
1711
  """Note that someone's connections have been read through page `pages`.
1689
1712
 
@@ -1700,6 +1723,9 @@ def record_bridge_progress(profile_url, name, pages, more, urn=None):
1700
1723
  "at": datetime.now().isoformat(timespec="seconds")}
1701
1724
  if urn or old.get("urn"):
1702
1725
  entry["urn"] = urn or old.get("urn")
1726
+ total = _LIST_TOTALS.get(profile_url) or old.get("total")
1727
+ if total:
1728
+ entry["total"] = int(total)
1703
1729
  mine[profile_url] = entry
1704
1730
  _change_progress(change)
1705
1731
 
@@ -2461,6 +2487,7 @@ def _scrape_one_bridge(page, bridge_name, bridge_id, profile_url, max_pages=LINK
2461
2487
  except Exception:
2462
2488
  total = None
2463
2489
  if total:
2490
+ _LIST_TOTALS[profile_url] = int(total)
2464
2491
  pages = -(-total // 10)
2465
2492
  print(f" About {total:,} results in their list: {pages} pages"
2466
2493
  + (f", and LinkedIn shows the first {LINKEDIN_MAX_PAGES}." if pages > LINKEDIN_MAX_PAGES else "."))
@@ -3475,6 +3502,17 @@ def rescrape_bridge(bridge_name, headless=False, max_pages=LINKEDIN_MAX_PAGES):
3475
3502
  raise BudgetReached(0, searches_left()[1])
3476
3503
  if profiles_left() <= 0: # a fresh read opens their profile
3477
3504
  raise BudgetReached(0, "profiles")
3505
+ # The minute between profile views is waited out before anything is
3506
+ # deleted: a Stop during it would otherwise leave the circle deleted and unread.
3507
+ wait = profile_wait()
3508
+ if wait is None:
3509
+ raise BudgetReached(0, "profiles")
3510
+ if wait:
3511
+ print(f" Waiting {wait}s before re-mapping: at least {PROFILE_GAP}s pass between any "
3512
+ f"two profile views.", flush=True)
3513
+ if not interruptible_sleep(wait, on_tick=lambda left: print(f" {left}s to go", flush=True), step=10):
3514
+ print(" Stopped before anything was deleted.")
3515
+ return
3478
3516
 
3479
3517
  print(f"Deleting old cluster data...")
3480
3518
  bridge_id = delete_bridge_cluster(bridge_name)