six-degrees 0.3.0 → 0.4.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.next/standalone/.next/BUILD_ID +1 -1
- package/.next/standalone/.next/app-path-routes-manifest.json +1 -0
- package/.next/standalone/.next/build-manifest.json +3 -3
- package/.next/standalone/.next/prerender-manifest.json +35 -7
- package/.next/standalone/.next/required-server-files.json +6 -1
- package/.next/standalone/.next/routes-manifest.json +18 -1
- package/.next/standalone/.next/server/app/_global-error/page.js.nft.json +1 -1
- package/.next/standalone/.next/server/app/_global-error/page_client-reference-manifest.js +1 -1
- package/.next/standalone/.next/server/app/_global-error.html +1 -1
- package/.next/standalone/.next/server/app/_global-error.rsc +1 -1
- package/.next/standalone/.next/server/app/_global-error.segments/__PAGE__.segment.rsc +1 -1
- package/.next/standalone/.next/server/app/_global-error.segments/_full.segment.rsc +1 -1
- package/.next/standalone/.next/server/app/_global-error.segments/_tree.segment.rsc +1 -1
- package/.next/standalone/.next/server/app/_not-found/page.js.nft.json +1 -1
- package/.next/standalone/.next/server/app/_not-found/page_client-reference-manifest.js +1 -1
- package/.next/standalone/.next/server/app/_not-found.html +1 -1
- package/.next/standalone/.next/server/app/_not-found.rsc +11 -11
- package/.next/standalone/.next/server/app/_not-found.segments/_full.segment.rsc +11 -11
- package/.next/standalone/.next/server/app/_not-found.segments/_not-found/__PAGE__.segment.rsc +10 -10
- package/.next/standalone/.next/server/app/_not-found.segments/_tree.segment.rsc +2 -2
- package/.next/standalone/.next/server/app/api/admin-delete/route.js +1 -1
- package/.next/standalone/.next/server/app/api/admin-delete/route.js.nft.json +1 -1
- package/.next/standalone/.next/server/app/api/admin-update/route.js +1 -1
- package/.next/standalone/.next/server/app/api/admin-update/route.js.nft.json +1 -1
- package/.next/standalone/.next/server/app/api/bridges/route.js +1 -1
- package/.next/standalone/.next/server/app/api/bridges/route.js.nft.json +1 -1
- package/.next/standalone/.next/server/app/api/bulk-import/route.js +1 -1
- package/.next/standalone/.next/server/app/api/bulk-import/route.js.nft.json +1 -1
- package/.next/standalone/.next/server/app/api/company-scores/legacy/route.js.nft.json +1 -1
- package/.next/standalone/.next/server/app/api/company-scores/route.js.nft.json +1 -1
- package/.next/standalone/.next/server/app/api/connections/route.js +1 -1
- package/.next/standalone/.next/server/app/api/connections/route.js.nft.json +1 -1
- package/.next/standalone/.next/server/app/api/data/export/route.js.nft.json +1 -1
- package/.next/standalone/.next/server/app/api/data/import/route.js.nft.json +1 -1
- package/.next/standalone/.next/server/app/api/data/restart/route.js.nft.json +1 -1
- package/.next/standalone/.next/server/app/api/data/reveal/route.js.nft.json +1 -1
- package/.next/standalone/.next/server/app/api/data/route.js.nft.json +1 -1
- package/.next/standalone/.next/server/app/api/delete-cluster/route.js +1 -1
- package/.next/standalone/.next/server/app/api/delete-cluster/route.js.nft.json +1 -1
- package/.next/standalone/.next/server/app/api/ingest/route.js +1 -1
- package/.next/standalone/.next/server/app/api/ingest/route.js.nft.json +1 -1
- package/.next/standalone/.next/server/app/api/network/route.js +1 -1
- package/.next/standalone/.next/server/app/api/network/route.js.nft.json +1 -1
- package/.next/standalone/.next/server/app/api/notifications/route.js +1 -1
- package/.next/standalone/.next/server/app/api/notifications/route.js.nft.json +1 -1
- package/.next/standalone/.next/server/app/api/outreach/route.js +3 -3
- package/.next/standalone/.next/server/app/api/outreach/route.js.nft.json +1 -1
- package/.next/standalone/.next/server/app/api/queue/route.js +1 -1
- package/.next/standalone/.next/server/app/api/queue/route.js.nft.json +1 -1
- package/.next/standalone/.next/server/app/api/scraper/route.js +1 -1
- package/.next/standalone/.next/server/app/api/scraper/route.js.nft.json +1 -1
- package/.next/standalone/.next/server/app/api/settings/route.js.nft.json +1 -1
- package/.next/standalone/.next/server/app/api/settings/sector-preview/route.js.nft.json +1 -1
- package/.next/standalone/.next/server/app/api/settings/sector-suggestions/route.js.nft.json +1 -1
- package/.next/standalone/.next/server/app/api/setup-profile/route.js +1 -1
- package/.next/standalone/.next/server/app/api/setup-profile/route.js.nft.json +1 -1
- package/.next/standalone/.next/server/app/api/unlock/route.js +1 -1
- package/.next/standalone/.next/server/app/api/unlock/route.js.nft.json +1 -1
- package/.next/standalone/.next/server/app/api/update/route.js.nft.json +1 -1
- package/.next/standalone/.next/server/app/api/update-images/route.js +1 -1
- package/.next/standalone/.next/server/app/api/update-images/route.js.nft.json +1 -1
- package/.next/standalone/.next/server/app/api/users/route.js +1 -1
- package/.next/standalone/.next/server/app/api/users/route.js.nft.json +1 -1
- package/.next/standalone/.next/server/app/avatars/[file]/route.js.nft.json +1 -1
- package/.next/standalone/.next/server/app/import/page.js.nft.json +1 -1
- package/.next/standalone/.next/server/app/import/page_client-reference-manifest.js +1 -1
- package/.next/standalone/.next/server/app/import.html +1 -1
- package/.next/standalone/.next/server/app/import.rsc +13 -13
- package/.next/standalone/.next/server/app/import.segments/_full.segment.rsc +13 -13
- package/.next/standalone/.next/server/app/import.segments/_tree.segment.rsc +2 -2
- package/.next/standalone/.next/server/app/import.segments/import/__PAGE__.segment.rsc +12 -12
- package/.next/standalone/.next/server/app/index.html +1 -1
- package/.next/standalone/.next/server/app/index.rsc +13 -13
- package/.next/standalone/.next/server/app/index.segments/__PAGE__.segment.rsc +12 -12
- package/.next/standalone/.next/server/app/index.segments/_full.segment.rsc +13 -13
- package/.next/standalone/.next/server/app/index.segments/_tree.segment.rsc +2 -2
- package/.next/standalone/.next/server/app/launch/page.js.nft.json +1 -1
- package/.next/standalone/.next/server/app/launch/page_client-reference-manifest.js +1 -1
- package/.next/standalone/.next/server/app/launch.html +2 -2
- package/.next/standalone/.next/server/app/launch.rsc +13 -13
- package/.next/standalone/.next/server/app/launch.segments/_full.segment.rsc +13 -13
- package/.next/standalone/.next/server/app/launch.segments/_tree.segment.rsc +2 -2
- package/.next/standalone/.next/server/app/launch.segments/launch/__PAGE__.segment.rsc +12 -12
- package/.next/standalone/.next/server/app/page.js.nft.json +1 -1
- package/.next/standalone/.next/server/app/page_client-reference-manifest.js +1 -1
- package/.next/standalone/.next/server/app/paths/page.js.nft.json +1 -1
- package/.next/standalone/.next/server/app/paths/page_client-reference-manifest.js +1 -1
- package/.next/standalone/.next/server/app/paths.html +1 -1
- package/.next/standalone/.next/server/app/paths.rsc +13 -13
- package/.next/standalone/.next/server/app/paths.segments/_full.segment.rsc +13 -13
- package/.next/standalone/.next/server/app/paths.segments/_tree.segment.rsc +2 -2
- package/.next/standalone/.next/server/app/paths.segments/paths/__PAGE__.segment.rsc +12 -12
- package/.next/standalone/.next/server/app/profile/page.js.nft.json +1 -1
- package/.next/standalone/.next/server/app/profile/page_client-reference-manifest.js +1 -1
- package/.next/standalone/.next/server/app/profile.html +1 -1
- package/.next/standalone/.next/server/app/profile.rsc +13 -13
- package/.next/standalone/.next/server/app/profile.segments/_full.segment.rsc +13 -13
- package/.next/standalone/.next/server/app/profile.segments/_tree.segment.rsc +2 -2
- package/.next/standalone/.next/server/app/profile.segments/profile/__PAGE__.segment.rsc +12 -12
- package/.next/standalone/.next/server/app/queue/page.js.nft.json +1 -1
- package/.next/standalone/.next/server/app/queue/page_client-reference-manifest.js +1 -1
- package/.next/standalone/.next/server/app/queue.html +1 -1
- package/.next/standalone/.next/server/app/queue.rsc +13 -13
- package/.next/standalone/.next/server/app/queue.segments/_full.segment.rsc +13 -13
- package/.next/standalone/.next/server/app/queue.segments/_tree.segment.rsc +2 -2
- package/.next/standalone/.next/server/app/queue.segments/queue/__PAGE__.segment.rsc +12 -12
- package/.next/standalone/.next/server/app/scores/page/app-paths-manifest.json +3 -0
- package/.next/standalone/.next/server/app/scores/page/build-manifest.json +20 -0
- package/.next/standalone/.next/server/app/scores/page/next-font-manifest.json +11 -0
- package/.next/standalone/.next/server/app/scores/page/react-loadable-manifest.json +1 -0
- package/.next/standalone/.next/server/app/scores/page/server-reference-manifest.json +4 -0
- package/.next/standalone/.next/server/app/scores/page.js +13 -0
- package/.next/standalone/.next/server/app/scores/page.js.map +5 -0
- package/.next/standalone/.next/server/app/scores/page.js.nft.json +1 -0
- package/.next/standalone/.next/server/app/scores/page_client-reference-manifest.js +3 -0
- package/.next/standalone/.next/server/app/scores.html +1 -0
- package/.next/standalone/.next/server/app/scores.meta +26 -0
- package/.next/standalone/.next/server/app/scores.rsc +24 -0
- package/.next/standalone/.next/server/app/scores.segments/_full.segment.rsc +24 -0
- package/.next/standalone/.next/server/app/scores.segments/_tree.segment.rsc +4 -0
- package/.next/standalone/.next/server/app/scores.segments/scores/__PAGE__.segment.rsc +28 -0
- package/.next/standalone/.next/server/app/settings/page.js.nft.json +1 -1
- package/.next/standalone/.next/server/app/settings/page_client-reference-manifest.js +1 -1
- package/.next/standalone/.next/server/app/settings.html +1 -1
- package/.next/standalone/.next/server/app/settings.rsc +13 -13
- package/.next/standalone/.next/server/app/settings.segments/_full.segment.rsc +13 -13
- package/.next/standalone/.next/server/app/settings.segments/_tree.segment.rsc +2 -2
- package/.next/standalone/.next/server/app/settings.segments/settings/__PAGE__.segment.rsc +12 -12
- package/.next/standalone/.next/server/app/setup/page.js.nft.json +1 -1
- package/.next/standalone/.next/server/app/setup/page_client-reference-manifest.js +1 -1
- package/.next/standalone/.next/server/app/setup.html +1 -1
- package/.next/standalone/.next/server/app/setup.rsc +13 -13
- package/.next/standalone/.next/server/app/setup.segments/_full.segment.rsc +13 -13
- package/.next/standalone/.next/server/app/setup.segments/_tree.segment.rsc +2 -2
- package/.next/standalone/.next/server/app/setup.segments/setup/__PAGE__.segment.rsc +12 -12
- package/.next/standalone/.next/server/app-paths-manifest.json +1 -0
- package/.next/standalone/.next/server/chunks/[root-of-the-server]__0-rnla2._.js +1 -1
- package/.next/standalone/.next/server/chunks/[root-of-the-server]__00la_y0._.js +1 -1
- package/.next/standalone/.next/server/chunks/{[root-of-the-server]__0i5hdu4._.js → [root-of-the-server]__03ydxxh._.js} +2 -2
- package/.next/standalone/.next/server/chunks/[root-of-the-server]__07t47cm._.js +1 -1
- package/.next/standalone/.next/server/chunks/[root-of-the-server]__0bat8nx._.js +8 -0
- package/.next/standalone/.next/server/chunks/[root-of-the-server]__0cj5jqc._.js +1 -1
- package/.next/standalone/.next/server/chunks/[root-of-the-server]__0e0fei3._.js +1 -1
- package/.next/standalone/.next/server/chunks/[root-of-the-server]__0ljcioj._.js +1 -1
- package/.next/standalone/.next/server/chunks/[root-of-the-server]__0ljnyho._.js +1 -1
- package/.next/standalone/.next/server/chunks/[root-of-the-server]__0o5-tcg._.js +1 -1
- package/.next/standalone/.next/server/chunks/[root-of-the-server]__0qrnous._.js +1 -1
- package/.next/standalone/.next/server/chunks/[root-of-the-server]__0t3v1a1._.js +1 -1
- package/.next/standalone/.next/server/chunks/[root-of-the-server]__0uag-k5._.js +1 -1
- package/.next/standalone/.next/server/chunks/[root-of-the-server]__0um1uey._.js +1 -1
- package/.next/standalone/.next/server/chunks/[root-of-the-server]__0wmexuc._.js +1 -1
- package/.next/standalone/.next/server/chunks/[root-of-the-server]__0xu34sz._.js +1 -1
- package/.next/standalone/.next/server/chunks/[root-of-the-server]__133knx1._.js +1 -1
- package/.next/standalone/.next/server/chunks/[root-of-the-server]__14ou7j5._.js +1 -1
- package/.next/standalone/.next/server/chunks/[root-of-the-server]__15-17hl._.js +1 -1
- package/.next/standalone/.next/server/chunks/[root-of-the-server]__16iu0hv._.js +1 -1
- package/.next/standalone/.next/server/chunks/[root-of-the-server]__19b7mig._.js +1 -1
- package/.next/standalone/.next/server/chunks/[root-of-the-server]__1bbcwub._.js +2 -2
- package/.next/standalone/.next/server/chunks/[root-of-the-server]__1eris29._.js +1 -1
- package/.next/standalone/.next/server/chunks/[root-of-the-server]__1gjo_cq._.js +1 -1
- package/.next/standalone/.next/server/chunks/[root-of-the-server]__1kbl21f._.js +1 -1
- package/.next/standalone/.next/server/chunks/[root-of-the-server]__1qcm08_._.js +1 -1
- package/.next/standalone/.next/server/chunks/[root-of-the-server]__1rphsdk._.js +1 -1
- package/.next/standalone/.next/server/chunks/[root-of-the-server]__1w25q0m._.js +1 -1
- package/.next/standalone/.next/server/chunks/_02twlca._.js +10 -0
- package/.next/standalone/.next/server/chunks/_072jl0y._.js +1 -1
- package/.next/standalone/.next/server/chunks/_0lw8_i0._.js +1 -1
- package/.next/standalone/.next/server/chunks/_1_fynia._.js +8 -6
- package/.next/standalone/.next/server/chunks/_1fn30_l._.js +1 -1
- package/.next/standalone/.next/server/chunks/_1itd1o_._.js +1 -1
- package/.next/standalone/.next/server/chunks/_1iwkh8t._.js +1 -1
- package/.next/standalone/.next/server/chunks/lib_db-client_15le7cw.js +13 -8
- package/.next/standalone/.next/server/chunks/{lib_rpc_02ndsg2.js → lib_rpc_03i8xef.js} +1 -1
- package/.next/standalone/.next/server/chunks/{lib_rpc_08q5vp6.js → lib_rpc_08fc3rp.js} +1 -1
- package/.next/standalone/.next/server/chunks/{lib_rpc_00tx2sm.js → lib_rpc_08r2t_q.js} +1 -1
- package/.next/standalone/.next/server/chunks/{lib_rpc_02-qa4d.js → lib_rpc_0btpv1t.js} +1 -1
- package/.next/standalone/.next/server/chunks/lib_rpc_0c9d9ti.js +5 -5
- package/.next/standalone/.next/server/chunks/lib_rpc_0cm2ev3.js +3 -0
- package/.next/standalone/.next/server/chunks/lib_rpc_0iovswz.js +3 -0
- package/.next/standalone/.next/server/chunks/lib_rpc_0is8flp.js +3 -0
- package/.next/standalone/.next/server/chunks/lib_rpc_0v8ajtd.js +3 -0
- package/.next/standalone/.next/server/chunks/lib_rpc_0xgmvvt.js +3 -0
- package/.next/standalone/.next/server/chunks/lib_rpc_144mzbf.js +3 -0
- package/.next/standalone/.next/server/chunks/lib_rpc_1bggi0n.js +3 -0
- package/.next/standalone/.next/server/chunks/lib_rpc_1dkcu9f.js +3 -0
- package/.next/standalone/.next/server/chunks/lib_rpc_1o0jb6w.js +3 -0
- package/.next/standalone/.next/server/chunks/ssr/{[root-of-the-server]__0an6blh._.js → [root-of-the-server]__0zzcbqg._.js} +2 -2
- package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__1194jps._.js +3 -0
- package/.next/standalone/.next/server/chunks/ssr/{[root-of-the-server]__1z1j3ek._.js → [root-of-the-server]__19ip1qq._.js} +2 -2
- package/.next/standalone/.next/server/chunks/ssr/_01f38ty._.js +3 -0
- package/.next/standalone/.next/server/chunks/ssr/_06w_gl4._.js +3 -0
- package/.next/standalone/.next/server/chunks/ssr/_0hw_qnj._.js +3 -0
- package/.next/standalone/.next/server/chunks/ssr/_13kzrwv._.js +3 -0
- package/.next/standalone/.next/server/chunks/ssr/_15xuwkv._.js +3 -0
- package/.next/standalone/.next/server/chunks/ssr/_1cyxocj._.js +3 -0
- package/.next/standalone/.next/server/chunks/ssr/_1gpbbw9._.js +3 -0
- package/.next/standalone/.next/server/chunks/ssr/_1hgd5dp._.js +3 -0
- package/.next/standalone/.next/server/chunks/ssr/_1vpfyd-._.js +3 -0
- package/.next/standalone/.next/server/chunks/ssr/_20w06i-._.js +3 -0
- package/.next/standalone/.next/server/chunks/ssr/_next-internal_server_app_scores_page_actions_0x-w58n.js +3 -0
- package/.next/standalone/.next/server/chunks/ssr/app_components_settings_SectorPicker_1c-29qw.js +3 -0
- package/.next/standalone/.next/server/chunks/ssr/app_launch_page_19t0dyd.js +1 -1
- package/.next/standalone/.next/server/chunks/ssr/app_page_1fazqbf.js +11 -9
- package/.next/standalone/.next/server/chunks/ssr/app_paths_page_1ol90m7.js +4 -0
- package/.next/standalone/.next/server/chunks/ssr/app_profile_page_0u3-l5y.js +1 -1
- package/.next/standalone/.next/server/chunks/ssr/app_queue_page_0c6znd6.js +5 -5
- package/.next/standalone/.next/server/chunks/ssr/app_scores_page_09wepvv.js +3 -0
- package/.next/standalone/.next/server/chunks/ssr/app_settings_page_1f8mjj_.js +1 -1
- package/.next/standalone/.next/server/chunks/ssr/app_setup_page_1-282zx.js +2 -2
- package/.next/standalone/.next/server/chunks/ssr/lib_1bn38gs._.js +3 -0
- package/.next/standalone/.next/server/chunks/ssr/node_modules_next_0x3i8za._.js +131 -0
- package/.next/standalone/.next/server/chunks/ssr/{node_modules_next_dist_1bunoni._.js → node_modules_next_dist_1v8aef8._.js} +2 -2
- package/.next/standalone/.next/server/chunks/ssr/node_modules_next_dist_client_components_server-async-storage_0t1dato.js +3 -0
- package/.next/standalone/.next/server/middleware-build-manifest.js +3 -3
- package/.next/standalone/.next/server/middleware-manifest.json +5 -5
- package/.next/standalone/.next/server/next-font-manifest.js +1 -1
- package/.next/standalone/.next/server/next-font-manifest.json +4 -0
- package/.next/standalone/.next/server/pages/404.html +1 -1
- package/.next/standalone/.next/server/pages/500.html +1 -1
- package/.next/standalone/.next/server/prefetch-hints.json +14 -0
- package/.next/standalone/.next/server/server-reference-manifest.js +1 -1
- package/.next/standalone/.next/server/server-reference-manifest.json +1 -1
- package/.next/standalone/.next/static/chunks/0ax_9zzboyv6d.js +44 -0
- package/.next/standalone/.next/static/chunks/0cuuo2jwc9h94.js +1 -0
- package/.next/standalone/.next/static/chunks/0l1568qff0efj.js +1 -0
- package/.next/standalone/.next/static/chunks/0muwt9bykzr-1.js +1 -0
- package/.next/standalone/.next/static/chunks/0w76iraw7ij3k.js +18 -0
- package/.next/standalone/.next/static/chunks/135n87ydxjrra.js +2 -0
- package/.next/standalone/.next/static/chunks/18xu0fbgo_qz-.js +1 -0
- package/.next/standalone/.next/static/chunks/28bu26ausqzmn.js +1 -0
- package/.next/standalone/.next/static/chunks/{40nvbgfn7-072.css → 2p6o90z0vgcwr.css} +1 -1
- package/.next/standalone/.next/static/chunks/2pnbm1z6j7xab.js +1 -0
- package/.next/standalone/.next/static/chunks/2xjho0lroiwlq.js +1 -0
- package/.next/standalone/.next/static/chunks/3e8zg6ctvvb8p.js +33 -0
- package/.next/standalone/.next/static/chunks/3qh78g2aqhprd.js +2 -0
- package/.next/standalone/CHANGELOG.md +286 -0
- package/.next/standalone/CONTRIBUTING.md +15 -4
- package/.next/standalone/README.md +50 -29
- package/.next/standalone/SECURITY.md +10 -0
- package/.next/standalone/app/api/company-scores/legacy/route.js +1 -1
- package/.next/standalone/app/api/company-scores/route.js +1 -1
- package/.next/standalone/app/api/ingest/route.js +23 -2
- package/.next/standalone/app/api/outreach/route.js +107 -44
- package/.next/standalone/app/api/scraper/route.js +146 -17
- package/.next/standalone/app/api/settings/sector-preview/route.js +3 -2
- package/.next/standalone/app/api/settings/sector-suggestions/route.js +1 -1
- package/.next/standalone/app/api/update-images/route.js +41 -7
- package/.next/standalone/app/avatars/[file]/route.js +6 -1
- package/.next/standalone/app/components/Avatar.js +5 -2
- package/.next/standalone/app/components/ChainView.js +659 -469
- package/.next/standalone/app/components/CompanyScores.js +16 -10
- package/.next/standalone/app/components/EmptyState.js +1 -1
- package/.next/standalone/app/components/FieldStep.js +94 -0
- package/.next/standalone/app/components/ForceGraph.js +183 -768
- package/.next/standalone/app/components/GridView.js +9 -7
- package/.next/standalone/app/components/LegacyScoresCard.js +1 -1
- package/.next/standalone/app/components/LinkedInLimits.js +22 -7
- package/.next/standalone/app/components/ListView.js +6 -4
- package/.next/standalone/app/components/MappingProgress.js +5 -6
- package/.next/standalone/app/components/OnboardingGate.js +1 -1
- package/.next/standalone/app/components/OrbitGraph.js +33 -1
- package/.next/standalone/app/components/OutlinkQuest.js +5 -4
- package/.next/standalone/app/components/PathsAnalyzer.js +1 -1
- package/.next/standalone/app/components/RingsView.js +9 -8
- package/.next/standalone/app/components/SeparationView.js +96 -14
- package/.next/standalone/app/components/Sidebar.js +366 -287
- package/.next/standalone/app/components/TheirCircle.js +300 -0
- package/.next/standalone/app/components/settings/SectorPicker.js +202 -0
- package/.next/standalone/app/components/settings/SectorSection.js +21 -195
- package/.next/standalone/app/components/settings/TierSection.js +99 -0
- package/.next/standalone/app/components/useRequests.js +12 -0
- package/.next/standalone/app/components/useScanner.js +13 -0
- package/.next/standalone/app/components/views.js +3 -5
- package/.next/standalone/app/globals.css +7 -0
- package/.next/standalone/app/launch/page.js +6 -3
- package/.next/standalone/app/page.js +183 -74
- package/.next/standalone/app/paths/page.js +20 -14
- package/.next/standalone/app/profile/page.js +3 -3
- package/.next/standalone/app/queue/page.js +56 -60
- package/.next/standalone/app/scores/page.js +72 -0
- package/.next/standalone/app/settings/page.js +15 -3
- package/.next/standalone/app/setup/page.js +423 -260
- package/.next/standalone/db/schema.js +2 -1
- package/.next/standalone/eslint.config.mjs +3 -3
- package/.next/standalone/install.sh +3 -3
- package/.next/standalone/lib/chain-layout.js +139 -0
- package/.next/standalone/lib/circle.js +169 -0
- package/.next/standalone/lib/companies.js +1 -1
- package/.next/standalone/lib/csv.js +21 -10
- package/.next/standalone/lib/data-export.js +2 -1
- package/.next/standalone/lib/data-folder.js +4 -2
- package/.next/standalone/lib/db-client.js +1 -0
- package/.next/standalone/lib/galaxy.js +12 -0
- package/.next/standalone/lib/ingest.js +27 -8
- package/.next/standalone/lib/legacy-offer.js +7 -6
- package/.next/standalone/lib/legacy-scores.js +2 -2
- package/.next/standalone/lib/linkedin-limits.js +30 -8
- package/.next/standalone/lib/photos.js +47 -0
- package/.next/standalone/lib/quest.js +8 -3
- package/.next/standalone/lib/rarity.js +80 -0
- package/.next/standalone/lib/reach.js +113 -0
- package/.next/standalone/lib/requests-client.js +113 -0
- package/.next/standalone/lib/rpc.js +26 -15
- package/.next/standalone/lib/scan-state.js +1 -0
- package/.next/standalone/lib/scanner-setup.js +20 -1
- package/.next/standalone/lib/score-guess.js +60 -0
- package/.next/standalone/lib/scoring.js +1761 -69
- package/.next/standalone/lib/scraper-client.js +235 -17
- package/.next/standalone/lib/sector-directory.js +1 -1
- package/.next/standalone/lib/sector-focus.js +17 -3
- package/.next/standalone/lib/separation.js +4 -2
- package/.next/standalone/lib/settings-client.js +21 -0
- package/.next/standalone/lib/settings-effects.js +19 -1
- package/.next/standalone/lib/settings.js +7 -2
- package/.next/standalone/next.config.mjs +25 -3
- package/.next/standalone/package-lock.json +2 -2
- package/.next/standalone/package.json +2 -2
- package/.next/standalone/public/demo-data.json +1 -1
- package/.next/standalone/scripts/build-app.mjs +11 -2
- package/.next/standalone/scripts/check-node.mjs +1 -1
- package/.next/standalone/scripts/gen-synthetic.mjs +1 -1
- package/.next/standalone/scripts/image_store.py +77 -21
- package/.next/standalone/scripts/scrape.py +305 -29
- package/.next/standalone/server.js +1 -1
- package/.next/static/chunks/0ax_9zzboyv6d.js +44 -0
- package/.next/static/chunks/0cuuo2jwc9h94.js +1 -0
- package/.next/static/chunks/0l1568qff0efj.js +1 -0
- package/.next/static/chunks/0muwt9bykzr-1.js +1 -0
- package/.next/static/chunks/0w76iraw7ij3k.js +18 -0
- package/.next/static/chunks/135n87ydxjrra.js +2 -0
- package/.next/static/chunks/18xu0fbgo_qz-.js +1 -0
- package/.next/static/chunks/28bu26ausqzmn.js +1 -0
- package/.next/static/chunks/{40nvbgfn7-072.css → 2p6o90z0vgcwr.css} +1 -1
- package/.next/static/chunks/2pnbm1z6j7xab.js +1 -0
- package/.next/static/chunks/2xjho0lroiwlq.js +1 -0
- package/.next/static/chunks/3e8zg6ctvvb8p.js +33 -0
- package/.next/static/chunks/3qh78g2aqhprd.js +2 -0
- package/CHANGELOG.md +286 -0
- package/README.md +50 -29
- package/SECURITY.md +10 -0
- package/db/schema.js +2 -1
- package/lib/chain-layout.js +139 -0
- package/lib/circle.js +169 -0
- package/lib/companies.js +1 -1
- package/lib/csv.js +21 -10
- package/lib/data-export.js +2 -1
- package/lib/data-folder.js +4 -2
- package/lib/db-client.js +1 -0
- package/lib/galaxy.js +12 -0
- package/lib/ingest.js +27 -8
- package/lib/legacy-offer.js +7 -6
- package/lib/legacy-scores.js +2 -2
- package/lib/linkedin-limits.js +30 -8
- package/lib/photos.js +47 -0
- package/lib/quest.js +8 -3
- package/lib/rarity.js +80 -0
- package/lib/reach.js +113 -0
- package/lib/requests-client.js +113 -0
- package/lib/rpc.js +26 -15
- package/lib/scan-state.js +1 -0
- package/lib/scanner-setup.js +20 -1
- package/lib/score-guess.js +60 -0
- package/lib/scoring.js +1761 -69
- package/lib/scraper-client.js +235 -17
- package/lib/sector-directory.js +1 -1
- package/lib/sector-focus.js +17 -3
- package/lib/separation.js +4 -2
- package/lib/settings-client.js +21 -0
- package/lib/settings-effects.js +19 -1
- package/lib/settings.js +7 -2
- package/package.json +2 -2
- package/public/demo-data.json +1 -1
- package/scripts/check-node.mjs +1 -1
- package/scripts/image_store.py +77 -21
- package/scripts/scrape.py +305 -29
- package/.next/standalone/.next/server/chunks/[root-of-the-server]__1t3rugi._.js +0 -3
- package/.next/standalone/.next/server/chunks/lib_rpc_0_8i3ft.js +0 -3
- package/.next/standalone/.next/server/chunks/lib_rpc_0huvw-3.js +0 -3
- package/.next/standalone/.next/server/chunks/lib_rpc_0ip2vdh.js +0 -3
- package/.next/standalone/.next/server/chunks/lib_rpc_0jzisgh.js +0 -3
- package/.next/standalone/.next/server/chunks/lib_rpc_0yi9xvd.js +0 -3
- package/.next/standalone/.next/server/chunks/lib_rpc_1jyhd0o.js +0 -3
- package/.next/standalone/.next/server/chunks/lib_rpc_1kn59-i.js +0 -3
- package/.next/standalone/.next/server/chunks/lib_rpc_1q2jkza.js +0 -3
- package/.next/standalone/.next/server/chunks/lib_rpc_1vpm1sk.js +0 -3
- package/.next/standalone/.next/server/chunks/lib_rpc_1x6p_31.js +0 -3
- package/.next/standalone/.next/server/chunks/ssr/_07u8byv._.js +0 -3
- package/.next/standalone/.next/server/chunks/ssr/_08r_mlf._.js +0 -3
- package/.next/standalone/.next/server/chunks/ssr/_0942v8n._.js +0 -3
- package/.next/standalone/.next/server/chunks/ssr/_0hl_t7r._.js +0 -3
- package/.next/standalone/.next/server/chunks/ssr/_0tb83dw._.js +0 -132
- package/.next/standalone/.next/server/chunks/ssr/_1-87bs1._.js +0 -3
- package/.next/standalone/.next/server/chunks/ssr/_17gui6a._.js +0 -3
- package/.next/standalone/.next/server/chunks/ssr/lib_0i8v-s3._.js +0 -3
- package/.next/standalone/.next/server/chunks/ssr/node_modules_16vay6l._.js +0 -3
- package/.next/standalone/.next/server/chunks/ssr/node_modules_1kciwn4._.js +0 -3
- package/.next/standalone/.next/static/chunks/06x3jiiz96m5t.js +0 -1
- package/.next/standalone/.next/static/chunks/08k1crid-05dt.js +0 -1
- package/.next/standalone/.next/static/chunks/0ngx9s6-zgshe.js +0 -33
- package/.next/standalone/.next/static/chunks/0q-_khndfxubv.js +0 -2
- package/.next/standalone/.next/static/chunks/1c79veqedvubt.js +0 -1
- package/.next/standalone/.next/static/chunks/1chp3nx45meg5.js +0 -44
- package/.next/standalone/.next/static/chunks/1irqj0lf-fu3d.js +0 -1
- package/.next/standalone/.next/static/chunks/1uadqylrod5_v.js +0 -1
- package/.next/standalone/.next/static/chunks/23u0zclgwpc4d.js +0 -1
- package/.next/standalone/.next/static/chunks/289ktj97di6zg.js +0 -1
- package/.next/standalone/.next/static/chunks/2b3lmf8e6uj8m.js +0 -16
- package/.next/standalone/.next/static/chunks/3npq5yd7c4dmn.js +0 -2
- package/.next/standalone/app/components/BridgeRing.js +0 -753
- package/.next/static/chunks/06x3jiiz96m5t.js +0 -1
- package/.next/static/chunks/08k1crid-05dt.js +0 -1
- package/.next/static/chunks/0ngx9s6-zgshe.js +0 -33
- package/.next/static/chunks/0q-_khndfxubv.js +0 -2
- package/.next/static/chunks/1c79veqedvubt.js +0 -1
- package/.next/static/chunks/1chp3nx45meg5.js +0 -44
- package/.next/static/chunks/1irqj0lf-fu3d.js +0 -1
- package/.next/static/chunks/1uadqylrod5_v.js +0 -1
- package/.next/static/chunks/23u0zclgwpc4d.js +0 -1
- package/.next/static/chunks/289ktj97di6zg.js +0 -1
- package/.next/static/chunks/2b3lmf8e6uj8m.js +0 -16
- package/.next/static/chunks/3npq5yd7c4dmn.js +0 -2
- /package/.next/standalone/.next/static/{qqqcxvzBUizkECrT4swyK → WseE_5HuU-1q0ywC9CP3U}/_buildManifest.js +0 -0
- /package/.next/standalone/.next/static/{qqqcxvzBUizkECrT4swyK → WseE_5HuU-1q0ywC9CP3U}/_clientMiddlewareManifest.js +0 -0
- /package/.next/standalone/.next/static/{qqqcxvzBUizkECrT4swyK → WseE_5HuU-1q0ywC9CP3U}/_ssgManifest.js +0 -0
- /package/.next/static/{qqqcxvzBUizkECrT4swyK → WseE_5HuU-1q0ywC9CP3U}/_buildManifest.js +0 -0
- /package/.next/static/{qqqcxvzBUizkECrT4swyK → WseE_5HuU-1q0ywC9CP3U}/_clientMiddlewareManifest.js +0 -0
- /package/.next/static/{qqqcxvzBUizkECrT4swyK → WseE_5HuU-1q0ywC9CP3U}/_ssgManifest.js +0 -0
package/lib/circle.js
ADDED
|
@@ -0,0 +1,169 @@
|
|
|
1
|
+
// Someone's circle as rings, for the top of their profile card.
|
|
2
|
+
//
|
|
3
|
+
// Blake, 2026-09-28: "for every profile card we can see their network circle
|
|
4
|
+
// … all of their current connections scraped and graded but then outside of
|
|
5
|
+
// that sits the connections all from d2 that we added and scanned coming from
|
|
6
|
+
// that person as the provider of the cluster … and then for d3,d4,d5,d6".
|
|
7
|
+
//
|
|
8
|
+
// Two facts the app already keeps make the whole chain, with nothing new stored:
|
|
9
|
+
// source_connection_id whose circle a row sits in: a scan of that person found it
|
|
10
|
+
// unlocked_from_bridge_id who introduced someone you connected with, kept
|
|
11
|
+
// after they became your connection (lib/promote.js)
|
|
12
|
+
// So from a person P: P's circle (D2, through P) → the people in it you
|
|
13
|
+
// connected with or asked → their circles (D3) → who you added from those →
|
|
14
|
+
// their circles (D4) … to six degrees. Everyone appears once, in the nearest
|
|
15
|
+
// ring they reach.
|
|
16
|
+
|
|
17
|
+
import { keyFor, score1 } from './separation.js';
|
|
18
|
+
|
|
19
|
+
export const MAX_DEGREE = 6;
|
|
20
|
+
|
|
21
|
+
/** A request is out to them and they haven't accepted (Sidebar's "Connect to Unlock Path"). */
|
|
22
|
+
export const requestedByDefault = (row) => row?.unlock_status === 'pending';
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* Rows grouped two ways: `circles`, id → the rows a scan of that person found;
|
|
26
|
+
* `introduced`, id → your connections you met through them.
|
|
27
|
+
*/
|
|
28
|
+
export function circleIndex(connections = [], degree2 = []) {
|
|
29
|
+
const circles = new Map();
|
|
30
|
+
for (const row of degree2 || []) {
|
|
31
|
+
const id = row?.source_connection_id;
|
|
32
|
+
if (id == null) continue;
|
|
33
|
+
let list = circles.get(id);
|
|
34
|
+
if (!list) circles.set(id, (list = []));
|
|
35
|
+
list.push(row);
|
|
36
|
+
}
|
|
37
|
+
const introduced = new Map();
|
|
38
|
+
for (const row of connections || []) {
|
|
39
|
+
const id = row?.unlocked_from_bridge_id;
|
|
40
|
+
if (id == null || row.degree !== 1) continue;
|
|
41
|
+
let list = introduced.get(id);
|
|
42
|
+
if (!list) introduced.set(id, (list = []));
|
|
43
|
+
list.push(row);
|
|
44
|
+
}
|
|
45
|
+
return { circles, introduced };
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
const STATE_ORDER = { connected: 0, requested: 1 };
|
|
49
|
+
const byStateThenPower = (a, b) => (STATE_ORDER[a.state] ?? 2) - (STATE_ORDER[b.state] ?? 2)
|
|
50
|
+
|| score1(b.row) - score1(a.row)
|
|
51
|
+
|| String(a.row.name || '').localeCompare(String(b.row.name || ''));
|
|
52
|
+
|
|
53
|
+
/**
|
|
54
|
+
* The rings around `person`: [{ degree, people: [{ row, key, parentId, state }] }],
|
|
55
|
+
* degree 2 first. `state` is 'connected' (you added them, so their own circle
|
|
56
|
+
* is the next ring out), 'requested' (a request is out: drawn dotted) or null.
|
|
57
|
+
* Also the totals: { people, connected, requested }.
|
|
58
|
+
*/
|
|
59
|
+
export function circleRings(person, index, { maxDegree = MAX_DEGREE, isRequested = requestedByDefault } = {}) {
|
|
60
|
+
const rings = [];
|
|
61
|
+
const totals = { people: 0, connected: 0, requested: 0 };
|
|
62
|
+
if (!person || !index) return { rings, totals };
|
|
63
|
+
const seen = new Set([keyFor(person)]);
|
|
64
|
+
let frontier = [person];
|
|
65
|
+
for (let degree = 2; degree <= maxDegree && frontier.length; degree++) {
|
|
66
|
+
const ring = [];
|
|
67
|
+
const next = [];
|
|
68
|
+
for (const parent of frontier) {
|
|
69
|
+
const kids = [];
|
|
70
|
+
// The people you connected with first: they have circles of their own.
|
|
71
|
+
for (const row of index.introduced.get(parent.id) || []) {
|
|
72
|
+
const key = keyFor(row);
|
|
73
|
+
if (seen.has(key)) continue;
|
|
74
|
+
seen.add(key);
|
|
75
|
+
kids.push({ row, key, parentId: parent.id, state: 'connected' });
|
|
76
|
+
next.push(row);
|
|
77
|
+
}
|
|
78
|
+
for (const row of index.circles.get(parent.id) || []) {
|
|
79
|
+
const key = keyFor(row);
|
|
80
|
+
if (seen.has(key)) continue;
|
|
81
|
+
seen.add(key);
|
|
82
|
+
kids.push({ row, key, parentId: parent.id, state: isRequested(row) ? 'requested' : null });
|
|
83
|
+
}
|
|
84
|
+
kids.sort(byStateThenPower);
|
|
85
|
+
ring.push(...kids);
|
|
86
|
+
}
|
|
87
|
+
if (!ring.length) break;
|
|
88
|
+
for (const p of ring) {
|
|
89
|
+
totals.people++;
|
|
90
|
+
if (p.state === 'connected') totals.connected++;
|
|
91
|
+
else if (p.state === 'requested') totals.requested++;
|
|
92
|
+
}
|
|
93
|
+
rings.push({ degree, people: ring });
|
|
94
|
+
frontier = next;
|
|
95
|
+
}
|
|
96
|
+
return { rings, totals };
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
const clamp = (v, lo, hi) => Math.min(hi, Math.max(lo, v));
|
|
100
|
+
|
|
101
|
+
/**
|
|
102
|
+
* Where every dot goes, in a square `size` wide: a radial tree. Each person
|
|
103
|
+
* gets a slice of the circle in proportion to how much of the next rings
|
|
104
|
+
* hangs off them, so whoever opened the most reach shows it at a glance, and
|
|
105
|
+
* their circle fans out behind them.
|
|
106
|
+
* Returns { size, center, rings: [{ degree, radius, dotR, items }], spokes }.
|
|
107
|
+
*/
|
|
108
|
+
export function circleLayout(rings = [], size = 240) {
|
|
109
|
+
const half = size / 2;
|
|
110
|
+
const R = rings.length;
|
|
111
|
+
const radiusOf = (k) => half * (R === 1 ? 0.78 : 0.4 + 0.52 * (k / (R - 1)));
|
|
112
|
+
|
|
113
|
+
// Each ring's people by who they hang off, in the ring's order.
|
|
114
|
+
const byParent = rings.map((ring) => {
|
|
115
|
+
const m = new Map();
|
|
116
|
+
for (const p of ring.people) {
|
|
117
|
+
if (!m.has(p.parentId)) m.set(p.parentId, []);
|
|
118
|
+
m.get(p.parentId).push(p);
|
|
119
|
+
}
|
|
120
|
+
return m;
|
|
121
|
+
});
|
|
122
|
+
const kidsOf = (k, id) => byParent[k + 1]?.get(id) || [];
|
|
123
|
+
|
|
124
|
+
// Weights, outermost ring first: at least 1, else what hangs off them.
|
|
125
|
+
const weight = new Map();
|
|
126
|
+
for (let k = R - 1; k >= 0; k--) {
|
|
127
|
+
for (const p of rings[k].people) {
|
|
128
|
+
const w = p.state === 'connected' ? kidsOf(k, p.row.id).reduce((s, c) => s + weight.get(c), 0) : 0;
|
|
129
|
+
weight.set(p, Math.max(1, w));
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
const out = [];
|
|
134
|
+
const spokes = [];
|
|
135
|
+
const span = new Map(); // row id → [start, end] angle for its children
|
|
136
|
+
const START = -Math.PI / 2;
|
|
137
|
+
for (let k = 0; k < R; k++) {
|
|
138
|
+
const radius = radiusOf(k);
|
|
139
|
+
const people = rings[k].people;
|
|
140
|
+
const dotR = clamp((2 * Math.PI * radius) / Math.max(1, people.length) * 0.32, 0.8, size / 60);
|
|
141
|
+
const items = [];
|
|
142
|
+
for (const [parentId, group] of byParent[k]) {
|
|
143
|
+
const [a0, a1] = k === 0 ? [START, START + 2 * Math.PI] : span.get(parentId) || [START, START + 2 * Math.PI];
|
|
144
|
+
const total = group.reduce((s, p) => s + weight.get(p), 0);
|
|
145
|
+
let a = a0;
|
|
146
|
+
for (const p of group) {
|
|
147
|
+
const w = ((a1 - a0) * weight.get(p)) / total;
|
|
148
|
+
const angle = a + w / 2;
|
|
149
|
+
const x = half + radius * Math.cos(angle);
|
|
150
|
+
const y = half + radius * Math.sin(angle);
|
|
151
|
+
items.push({ ...p, angle, x, y });
|
|
152
|
+
if (p.state === 'connected') span.set(p.row.id, [a, a + w]);
|
|
153
|
+
a += w;
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
// A line from each person you added out to the middle of their own circle.
|
|
157
|
+
if (k + 1 < R) {
|
|
158
|
+
const nextR = radiusOf(k + 1);
|
|
159
|
+
for (const it of items) {
|
|
160
|
+
if (it.state !== 'connected' || !kidsOf(k, it.row.id).length) continue;
|
|
161
|
+
const [s, e] = span.get(it.row.id);
|
|
162
|
+
const mid = (s + e) / 2;
|
|
163
|
+
spokes.push({ x1: it.x, y1: it.y, x2: half + nextR * Math.cos(mid), y2: half + nextR * Math.sin(mid) });
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
out.push({ degree: rings[k].degree, radius, dotR, items });
|
|
167
|
+
}
|
|
168
|
+
return { size, center: { x: half, y: half }, rings: out, spokes };
|
|
169
|
+
}
|
package/lib/companies.js
CHANGED
|
@@ -85,7 +85,7 @@ export const isSenior = (headline) => getSeniority(headline).level >= 4;
|
|
|
85
85
|
* A company's industry is the one scoring uses (lib/scoring.js
|
|
86
86
|
* networkCompanies over the same rows): the curated list's, else the name's,
|
|
87
87
|
* else what most of its people say. So a company's colour here is the sector
|
|
88
|
-
*
|
|
88
|
+
* Scores → Your sector leans on, and each company is the one scoring
|
|
89
89
|
* scores: both name it with cleanCompany().
|
|
90
90
|
*/
|
|
91
91
|
export function buildCompanyIndex(rows = []) {
|
package/lib/csv.js
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
|
-
import { scorePerson } from './scoring.js';
|
|
1
|
+
import { scorePerson, curveCutoffs, curvedTier } from './scoring.js';
|
|
2
|
+
import { toIsoDate } from './ingest.js';
|
|
2
3
|
// LinkedIn Connections.csv import — parsed ENTIRELY in the browser.
|
|
3
4
|
//
|
|
4
5
|
// Nothing is uploaded and nothing is written to the database: an imported
|
|
@@ -49,6 +50,20 @@ export function scoreRecord(headline, company) {
|
|
|
49
50
|
return { seniority_score: s.title.points, company_prestige_score: s.companyScore, power_score: s.power, tier: s.tier };
|
|
50
51
|
}
|
|
51
52
|
|
|
53
|
+
/**
|
|
54
|
+
* Tiers for a whole import, graded on its own curve (lib/scoring.js CURVE):
|
|
55
|
+
* a CSV import uses the app's defaults, never your settings (no company
|
|
56
|
+
* scores, no sector), and the curve is the default, so the first look at any
|
|
57
|
+
* export has a top. Scores → Tiers says so while one is open. Run on the
|
|
58
|
+
* parse (the import page's counts) and on every load (the tab keeps only
|
|
59
|
+
* what the export says, packConnections), so the two always agree.
|
|
60
|
+
*/
|
|
61
|
+
function gradeOnCurve(connections) {
|
|
62
|
+
const cut = curveCutoffs(connections.map((c) => c.power_score));
|
|
63
|
+
for (const c of connections) c.tier = curvedTier(c.power_score, cut);
|
|
64
|
+
return connections;
|
|
65
|
+
}
|
|
66
|
+
|
|
52
67
|
// Stable id from the profile URL so re-importing keeps node identity.
|
|
53
68
|
function hashId(input) {
|
|
54
69
|
let h = 5381;
|
|
@@ -122,12 +137,9 @@ export function parseConnectionsCsv(text) {
|
|
|
122
137
|
if (seen.has(id)) { skipped++; continue; }
|
|
123
138
|
seen.add(id);
|
|
124
139
|
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
const d = new Date(raw);
|
|
129
|
-
if (!isNaN(d.getTime())) connected_date = d.toISOString().split('T')[0];
|
|
130
|
-
}
|
|
140
|
+
// "28 Sep 2026", read as the calendar date it is. new Date(raw).toISOString()
|
|
141
|
+
// made it local midnight in UTC, a day early anywhere east of London.
|
|
142
|
+
const connected_date = toIsoDate(cell(iConnected));
|
|
131
143
|
|
|
132
144
|
connections.push(connectionFrom({ id, name, role, company, profileUrl, connected_date }));
|
|
133
145
|
}
|
|
@@ -138,7 +150,7 @@ export function parseConnectionsCsv(text) {
|
|
|
138
150
|
);
|
|
139
151
|
}
|
|
140
152
|
|
|
141
|
-
connections.sort((a, b) => b.power_score - a.power_score);
|
|
153
|
+
gradeOnCurve(connections).sort((a, b) => b.power_score - a.power_score);
|
|
142
154
|
return { connections, skipped, total: connections.length + skipped };
|
|
143
155
|
}
|
|
144
156
|
|
|
@@ -161,8 +173,7 @@ export function packConnections(connections) {
|
|
|
161
173
|
}
|
|
162
174
|
|
|
163
175
|
export function unpackConnections(rows = []) {
|
|
164
|
-
return rows
|
|
165
|
-
.map(([id, name, role, company, profileUrl, connected_date]) => connectionFrom({ id, name, role, company, profileUrl, connected_date }))
|
|
176
|
+
return gradeOnCurve(rows.map(([id, name, role, company, profileUrl, connected_date]) => connectionFrom({ id, name, role, company, profileUrl, connected_date })))
|
|
166
177
|
.sort((a, b) => b.power_score - a.power_score);
|
|
167
178
|
}
|
|
168
179
|
|
package/lib/data-export.js
CHANGED
|
@@ -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
|
}
|
package/lib/data-folder.js
CHANGED
|
@@ -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
|
-
|
|
16
|
-
|
|
16
|
+
// A photo the /avatars route serves (lib/photos.js). An export carries and an
|
|
17
|
+
// import accepts exactly those names.
|
|
18
|
+
export { AVATAR_FILE };
|
|
17
19
|
|
|
18
20
|
/**
|
|
19
21
|
* The scanner's notes about the network itself: without them, people whose
|
package/lib/db-client.js
CHANGED
|
@@ -108,6 +108,7 @@ export function applySchema(db) {
|
|
|
108
108
|
// exists, so columns added after first release are added here, once.
|
|
109
109
|
const ADDED_COLUMNS = [
|
|
110
110
|
['linkedin_connections', 'score_why', 'TEXT'],
|
|
111
|
+
['linkedin_connections', 'mutual_count', 'INTEGER'],
|
|
111
112
|
];
|
|
112
113
|
|
|
113
114
|
function addMissingColumns(db) {
|
package/lib/galaxy.js
ADDED
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
// The Galaxy's view, as numbers, kept apart from the drawing so it can be tested
|
|
2
|
+
// (app/components/ForceGraph.js).
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* The same view in a box of a new size. It moves by half the change, at the
|
|
6
|
+
* same zoom, so whatever was in the middle of the box is in the middle still.
|
|
7
|
+
* Opening a 320-pixel panel slides the view 160 pixels left, and closing it
|
|
8
|
+
* slides the view back to exactly where it was.
|
|
9
|
+
*/
|
|
10
|
+
export function recentre({ x, y, k }, from, to) {
|
|
11
|
+
return { x: x + (to.width - from.width) / 2, y: y + (to.height - from.height) / 2, k };
|
|
12
|
+
}
|
package/lib/ingest.js
CHANGED
|
@@ -43,22 +43,29 @@ const MONTHS = ['january', 'february', 'march', 'april', 'may', 'june', 'july',
|
|
|
43
43
|
'august', 'september', 'october', 'november', 'december'];
|
|
44
44
|
|
|
45
45
|
/**
|
|
46
|
-
*
|
|
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
|
-
*
|
|
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
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
59
|
+
let name, day, year;
|
|
60
|
+
let m = t.match(/^([A-Za-z]+)\.?\s+(\d{1,2}),?\s+(\d{4})$/); // September 22, 2026
|
|
61
|
+
if (m) [, name, day, year] = m;
|
|
62
|
+
else if ((m = t.match(/^(\d{1,2})\s+([A-Za-z]+)\.?\s+(\d{4})$/))) [, day, name, year] = m; // 22 Sep 2026
|
|
63
|
+
else return null;
|
|
64
|
+
const month = MONTHS.findIndex((full) => full.startsWith(name.toLowerCase()) && name.length >= 3);
|
|
65
|
+
day = Number(day);
|
|
66
|
+
// Day 0 of the next month is this month's last, counted in UTC so no zone moves it.
|
|
67
|
+
if (month < 0 || day < 1 || day > new Date(Date.UTC(Number(year), month + 1, 0)).getUTCDate()) return null;
|
|
68
|
+
return `${year}-${String(month + 1).padStart(2, '0')}-${String(day).padStart(2, '0')}`;
|
|
62
69
|
}
|
|
63
70
|
|
|
64
71
|
// Tiers that make a new connection worth a notification of its own.
|
|
@@ -100,3 +107,15 @@ export function refreshNotifications({ added = [], checked = 0, tierOf = () => n
|
|
|
100
107
|
}
|
|
101
108
|
return out;
|
|
102
109
|
}
|
|
110
|
+
|
|
111
|
+
/**
|
|
112
|
+
* LinkedIn's own count of the mutual connections you share with someone, as
|
|
113
|
+
* the scanner read it off their result card (scripts/scrape.py
|
|
114
|
+
* BRIDGE_RESULTS_JS), or null. A whole number from 1 (the bridge is always
|
|
115
|
+
* one) to 30,000 (LinkedIn's cap on connections); anything else is a misread
|
|
116
|
+
* and is dropped, never stored.
|
|
117
|
+
*/
|
|
118
|
+
export function mutualCountOf(value) {
|
|
119
|
+
const n = typeof value === 'string' && /^\d[\d,]*$/.test(value.trim()) ? Number(value.replace(/,/g, '')) : value;
|
|
120
|
+
return Number.isInteger(n) && n >= 1 && n <= 30000 ? n : null;
|
|
121
|
+
}
|
package/lib/legacy-offer.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
//
|
|
1
|
+
// The Scores tab: the one-time offer to keep the curated list's old scores as
|
|
2
2
|
// your own, after the list was made neutral (lib/legacy-scores.js has the old
|
|
3
3
|
// scores and why each changed).
|
|
4
4
|
//
|
|
@@ -10,7 +10,7 @@
|
|
|
10
10
|
// scored on the server, so the page doesn't ask while one is open.
|
|
11
11
|
//
|
|
12
12
|
// Keeping writes the old score under every name scoring uses for the entry's
|
|
13
|
-
// companies, as
|
|
13
|
+
// companies, as the Scores tab sets a score (setCompanyScores), then rescores
|
|
14
14
|
// everyone once for the whole batch. Keep or No thanks, it is never offered
|
|
15
15
|
// again, and an offer whose first look finds nothing to offer closes itself.
|
|
16
16
|
|
|
@@ -62,14 +62,14 @@ export function legacyOfferState(db = getDb()) {
|
|
|
62
62
|
}
|
|
63
63
|
|
|
64
64
|
/**
|
|
65
|
-
* What
|
|
65
|
+
* What the Scores tab offers, or null: nothing when the offer isn't open
|
|
66
66
|
* (never was, or was answered) or no company in the network lost an old score
|
|
67
67
|
* that you haven't replaced with one of your own. That last closes it for
|
|
68
68
|
* good ('none'): the network the old list scored is the one read here, so
|
|
69
|
-
* nothing could turn up later, and every visit to
|
|
69
|
+
* nothing could turn up later, and every visit to the Scores tab would
|
|
70
70
|
* otherwise read the whole network again to find nothing.
|
|
71
71
|
*
|
|
72
|
-
* The companies are the ones
|
|
72
|
+
* The companies are the ones the Scores tab lists, those someone works at now,
|
|
73
73
|
* so a kept score shows there as yours and Auto can hand it back. (A name only
|
|
74
74
|
* in former roles would be a score nothing lists; its people's former role
|
|
75
75
|
* counts at 70% and moves them little.) Each is compared by its built-in score
|
|
@@ -116,7 +116,8 @@ export function legacyOffer(db = getDb()) {
|
|
|
116
116
|
name: o.name,
|
|
117
117
|
was: o.was,
|
|
118
118
|
now: main.now.score,
|
|
119
|
-
|
|
119
|
+
// A score from the curated list or the public dataset is a fact, not an estimate.
|
|
120
|
+
estimated: !['known', 'data'].includes(main.now.source),
|
|
120
121
|
people: o.people.size,
|
|
121
122
|
names: o.names.map((n) => n.name).sort(),
|
|
122
123
|
};
|
package/lib/legacy-scores.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
// The curated company list's scores from before it was made neutral, kept for
|
|
2
|
-
// one thing only: the one-time offer on
|
|
2
|
+
// one thing only: the one-time offer on the Scores tab to keep them as your
|
|
3
3
|
// own (lib/legacy-offer.js). Nothing scores with these. lib/scoring.js never
|
|
4
4
|
// imports this file, and the model must never read it.
|
|
5
5
|
//
|
|
@@ -24,7 +24,7 @@
|
|
|
24
24
|
|
|
25
25
|
export const LEGACY_SCORES = [
|
|
26
26
|
// Rescored: scored like its peers Pinterest, Reddit and X.
|
|
27
|
-
['Snap', 9, /^(snap|snapchat)( inc)?$/, 'media'],
|
|
27
|
+
['Snap', 9, /^(snap|snapchat)( inc)?( (specs|spectacles|(mena|emea|apac|latam|north america)( region)?))?$/, 'media'],
|
|
28
28
|
// Removed: private companies that are neither household names nor Fortune
|
|
29
29
|
// 500-sized, and small startups.
|
|
30
30
|
['Anduril', 9, /^anduril( industries)?$/, 'defense'],
|
package/lib/linkedin-limits.js
CHANGED
|
@@ -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 {
|
|
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
|
-
/**
|
|
63
|
-
|
|
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
|
|
76
|
-
*
|
|
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:
|
|
97
|
+
if (!data || typeof data !== 'object') return { searchesToday: Infinity, searchesMonth: 0, profilesToday: Infinity, unreadable: true };
|
|
82
98
|
const s = (Array.isArray(data.searches) ? data.searches : []).map(Number);
|
|
83
99
|
const p = (Array.isArray(data.profiles) ? data.profiles : []).map(Number);
|
|
84
100
|
const dayAgo = now / 1000 - DAY;
|
|
@@ -113,6 +129,7 @@ export function linkedinState(dir, now = Date.now()) {
|
|
|
113
129
|
limits,
|
|
114
130
|
leftToday: left(limits.daily, use.searchesToday),
|
|
115
131
|
leftMonth: left(limits.monthly, use.searchesMonth),
|
|
132
|
+
profilesLeftToday: left(limits.profiles, use.profilesToday),
|
|
116
133
|
monthResets: nextMonthStartPacific(now),
|
|
117
134
|
cooldown: readCooldown(dir, now),
|
|
118
135
|
};
|
|
@@ -138,7 +155,8 @@ const isTime = (v) => typeof v === 'number' && Number.isFinite(v) && v >= 0;
|
|
|
138
155
|
* Why one of these files from another computer can't be used here, in words
|
|
139
156
|
* for the page, or null when it can. The limits follow writeLimits: only the
|
|
140
157
|
* menu's own choices. A value the app never offers is refused rather than
|
|
141
|
-
* passed to the scanner, which reads a daily limit of 0 as "no limit at all"
|
|
158
|
+
* passed to the scanner, which reads a daily limit of 0 as "no limit at all"
|
|
159
|
+
* (and 1000 profile views a day would be as good as none).
|
|
142
160
|
*/
|
|
143
161
|
export function budgetFileProblem(name, data) {
|
|
144
162
|
if (!isObject(data)) return 'it is not a JSON object';
|
|
@@ -155,6 +173,9 @@ export function budgetFileProblem(name, data) {
|
|
|
155
173
|
if (data.monthly !== undefined && !MONTHLY_CHOICES.includes(data.monthly)) {
|
|
156
174
|
return `its monthly limit (${String(data.monthly).slice(0, 20)}) is not one Six Degrees offers`;
|
|
157
175
|
}
|
|
176
|
+
if (data.profiles !== undefined && !PROFILE_CHOICES.includes(data.profiles)) {
|
|
177
|
+
return `its limit on profile views (${String(data.profiles).slice(0, 20)}) is not one Six Degrees offers`;
|
|
178
|
+
}
|
|
158
179
|
}
|
|
159
180
|
return null;
|
|
160
181
|
}
|
|
@@ -236,6 +257,7 @@ export function mergeBudgetFiles({ dir, from }) {
|
|
|
236
257
|
durable.writeJsonDurably(here(LIMITS), {
|
|
237
258
|
daily: limits.daily ?? DEFAULT_LIMITS.daily,
|
|
238
259
|
monthly: limits.monthly ?? DEFAULT_LIMITS.monthly,
|
|
260
|
+
profiles: limits.profiles ?? DEFAULT_LIMITS.profiles,
|
|
239
261
|
});
|
|
240
262
|
}
|
|
241
263
|
}
|
package/lib/photos.js
ADDED
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
// Profile photos are shown from this computer only.
|
|
2
|
+
//
|
|
3
|
+
// The scanner saves each photo into the data folder as it reads it
|
|
4
|
+
// (scripts/image_store.py), and the row points at the saved file,
|
|
5
|
+
// `/avatars/<name>`, which app/avatars/[file] serves. A row can also hold a
|
|
6
|
+
// link: LinkedIn's own, which older versions stored until the scanner replaced
|
|
7
|
+
// it, and which a copy of a network made by one carries too. A page never loads
|
|
8
|
+
// a link: it would contact LinkedIn while someone is only looking at their map,
|
|
9
|
+
// and the app goes online only when you act. That person shows initials until
|
|
10
|
+
// the photo is saved, at the next scan or with Save photos on the Scan page.
|
|
11
|
+
// The Content-Security-Policy in next.config.mjs holds the same line in the
|
|
12
|
+
// browser.
|
|
13
|
+
//
|
|
14
|
+
// No imports, so the views and the server share it: every view and the routes
|
|
15
|
+
// that store photos check with localPhoto, and the /avatars route serves the
|
|
16
|
+
// names AVATAR_FILE allows.
|
|
17
|
+
|
|
18
|
+
/** A photo the /avatars route serves: a plain file name, no folders. The route uses this same pattern. */
|
|
19
|
+
export const AVATAR_FILE = /^[A-Za-z0-9_-]+\.(webp|jpg|jpeg|png)$/;
|
|
20
|
+
|
|
21
|
+
const PREFIX = '/avatars/';
|
|
22
|
+
|
|
23
|
+
/** The photo to show for a row's `profile_image_url`: its saved file's path, or null. */
|
|
24
|
+
export function localPhoto(url) {
|
|
25
|
+
if (typeof url !== 'string' || !url.startsWith(PREFIX)) return null;
|
|
26
|
+
return AVATAR_FILE.test(url.slice(PREFIX.length)) ? url : null;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* The photos still kept as links, one per person and link:
|
|
31
|
+
* [{ profileUrl, imageUrl }]. `db` is an open node:sqlite database. The
|
|
32
|
+
* scanner saves each (`GET /api/update-images`), or forgets the link on a
|
|
33
|
+
* definite no, so a link is tried until there's an answer.
|
|
34
|
+
*/
|
|
35
|
+
export function waitingPhotos(db) {
|
|
36
|
+
return db.prepare(`SELECT DISTINCT profile_url AS profileUrl, profile_image_url AS imageUrl
|
|
37
|
+
FROM linkedin_connections
|
|
38
|
+
WHERE profile_image_url IS NOT NULL AND profile_image_url <> ''
|
|
39
|
+
AND substr(profile_image_url, 1, 9) <> '/avatars/'
|
|
40
|
+
ORDER BY profile_url`).all()
|
|
41
|
+
.filter((r) => r.profileUrl && !localPhoto(r.imageUrl));
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
/** How many people have a photo waiting to be saved. */
|
|
45
|
+
export function waitingPhotoCount(db) {
|
|
46
|
+
return new Set(waitingPhotos(db).map((r) => r.profileUrl)).size;
|
|
47
|
+
}
|
package/lib/quest.js
CHANGED
|
@@ -9,6 +9,8 @@
|
|
|
9
9
|
// sent, people who actually joined your network), never from browsing.
|
|
10
10
|
// Plain functions; tests/quest.test.mjs.
|
|
11
11
|
|
|
12
|
+
import { circleState } from './reach.js';
|
|
13
|
+
|
|
12
14
|
export const STAGE_SIZE = 5;
|
|
13
15
|
export const XP_SEND = { S: 25, A: 15, B: 10, C: 5, D: 2 };
|
|
14
16
|
export const XP_ADDED = { S: 100, A: 60, B: 35, C: 15, D: 5 };
|
|
@@ -26,8 +28,9 @@ export function levelFor(points) {
|
|
|
26
28
|
* @param sentIds ids marked "invite sent"
|
|
27
29
|
* @param added 1st-degree people who came through a cluster (unlocked_from_bridge_id set)
|
|
28
30
|
* @param mappedIds ids of 1st-degree people whose own circle is mapped
|
|
31
|
+
* @param reach lib/reach.js reachIndex(), which also knows hidden lists; mappedIds stands in without it
|
|
29
32
|
*/
|
|
30
|
-
export function buildQuest({ recs = [], sentIds = new Set(), added = [], mappedIds = new Set() }) {
|
|
33
|
+
export function buildQuest({ recs = [], sentIds = new Set(), added = [], mappedIds = new Set(), reach = null }) {
|
|
31
34
|
const byBridge = new Map();
|
|
32
35
|
const seen = new Set();
|
|
33
36
|
for (const r of [...recs].sort((a, b) => (b.priority || 0) - (a.priority || 0))) {
|
|
@@ -112,8 +115,10 @@ export function buildQuest({ recs = [], sentIds = new Set(), added = [], mappedI
|
|
|
112
115
|
level: levelFor(points),
|
|
113
116
|
sentTotal,
|
|
114
117
|
addedTotal,
|
|
115
|
-
// People you've added whose own circle
|
|
116
|
-
|
|
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
|
}
|