six-degrees 0.4.10 → 0.5.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 +9 -9
- package/.next/standalone/.next/routes-manifest.json +6 -0
- 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 +13 -13
- package/.next/standalone/.next/server/app/_not-found.segments/_full.segment.rsc +13 -13
- package/.next/standalone/.next/server/app/_not-found.segments/_not-found/__PAGE__.segment.rsc +12 -12
- 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 +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 +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 +1 -1
- 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 +3 -2
- package/.next/standalone/.next/server/app/api/scraper/route.js.nft.json +1 -1
- package/.next/standalone/.next/server/app/api/settings/route.js +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 +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 +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/social/crm/route/app-paths-manifest.json +3 -0
- package/.next/standalone/.next/server/app/api/social/crm/route/build-manifest.json +12 -0
- package/.next/standalone/.next/server/app/api/social/crm/route/server-reference-manifest.json +4 -0
- package/.next/standalone/.next/server/app/api/social/crm/route.js +10 -0
- package/.next/standalone/.next/server/app/api/social/crm/route.js.map +5 -0
- package/.next/standalone/.next/server/app/api/social/crm/route.js.nft.json +1 -0
- package/.next/standalone/.next/server/app/api/social/crm/route_client-reference-manifest.js +3 -0
- package/.next/standalone/.next/server/app/api/social/messages/route.js +4 -3
- package/.next/standalone/.next/server/app/api/social/messages/route.js.nft.json +1 -1
- package/.next/standalone/.next/server/app/api/social/route.js +4 -3
- package/.next/standalone/.next/server/app/api/social/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 +15 -15
- package/.next/standalone/.next/server/app/import.segments/_full.segment.rsc +15 -15
- 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 +14 -14
- package/.next/standalone/.next/server/app/index.html +1 -1
- package/.next/standalone/.next/server/app/index.rsc +15 -15
- package/.next/standalone/.next/server/app/index.segments/__PAGE__.segment.rsc +14 -14
- package/.next/standalone/.next/server/app/index.segments/_full.segment.rsc +15 -15
- 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 +15 -15
- package/.next/standalone/.next/server/app/launch.segments/_full.segment.rsc +15 -15
- 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 +14 -14
- 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 +15 -15
- package/.next/standalone/.next/server/app/paths.segments/_full.segment.rsc +15 -15
- 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 +14 -14
- 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 +15 -15
- package/.next/standalone/.next/server/app/profile.segments/_full.segment.rsc +15 -15
- 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 +14 -14
- 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 +15 -15
- package/.next/standalone/.next/server/app/queue.segments/_full.segment.rsc +15 -15
- 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 +14 -14
- package/.next/standalone/.next/server/app/scores/page.js.nft.json +1 -1
- package/.next/standalone/.next/server/app/scores/page_client-reference-manifest.js +1 -1
- package/.next/standalone/.next/server/app/scores.html +1 -1
- package/.next/standalone/.next/server/app/scores.rsc +15 -15
- package/.next/standalone/.next/server/app/scores.segments/_full.segment.rsc +15 -15
- package/.next/standalone/.next/server/app/scores.segments/_tree.segment.rsc +2 -2
- package/.next/standalone/.next/server/app/scores.segments/scores/__PAGE__.segment.rsc +14 -14
- 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 +15 -15
- package/.next/standalone/.next/server/app/settings.segments/_full.segment.rsc +15 -15
- 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 +14 -14
- 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 +15 -15
- package/.next/standalone/.next/server/app/setup.segments/_full.segment.rsc +15 -15
- 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 +14 -14
- package/.next/standalone/.next/server/app/social/page.js.nft.json +1 -1
- package/.next/standalone/.next/server/app/social/page_client-reference-manifest.js +1 -1
- package/.next/standalone/.next/server/app/social.html +1 -1
- package/.next/standalone/.next/server/app/social.rsc +15 -15
- package/.next/standalone/.next/server/app/social.segments/_full.segment.rsc +15 -15
- package/.next/standalone/.next/server/app/social.segments/_tree.segment.rsc +2 -2
- package/.next/standalone/.next/server/app/social.segments/social/__PAGE__.segment.rsc +14 -14
- package/.next/standalone/.next/server/app-paths-manifest.json +1 -0
- package/.next/standalone/.next/server/chunks/{[root-of-the-server]__0bat8nx._.js → [root-of-the-server]__05_3-22._.js} +2 -2
- package/.next/standalone/.next/server/chunks/[root-of-the-server]__0t3v1a1._.js +1 -1
- package/.next/standalone/.next/server/chunks/{[root-of-the-server]__06mi9k2._.js → [root-of-the-server]__13dx4sd._.js} +7 -2
- package/.next/standalone/.next/server/chunks/[root-of-the-server]__15wc9e-._.js +3 -0
- package/.next/standalone/.next/server/chunks/[root-of-the-server]__1kbl21f._.js +1 -1
- package/.next/standalone/.next/server/chunks/[root-of-the-server]__1tir0vc._.js +8 -0
- package/.next/standalone/.next/server/chunks/[root-of-the-server]__20rwfdw._.js +8 -0
- package/.next/standalone/.next/server/chunks/_072jl0y._.js +1 -1
- package/.next/standalone/.next/server/chunks/_0ea9qwy._.js +1 -1
- package/.next/standalone/.next/server/chunks/_1_fynia._.js +7 -7
- 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/_1oqfuh8._.js +3 -0
- package/.next/standalone/.next/server/chunks/_1xh07bp._.js +1 -1
- package/.next/standalone/.next/server/chunks/_next-internal_server_app_api_social_crm_route_actions_0d1aflv.js +3 -0
- package/.next/standalone/.next/server/chunks/lib_0851sk0._.js +8 -0
- package/.next/standalone/.next/server/chunks/lib_1q7p2ro._.js +8 -0
- package/.next/standalone/.next/server/chunks/lib_1r6l86l._.js +8 -0
- package/.next/standalone/.next/server/chunks/lib_db-client_15le7cw.js +8 -8
- package/.next/standalone/.next/server/chunks/lib_linkedin-export_12imqxg.js +3 -0
- package/.next/standalone/.next/server/chunks/lib_rpc_04s8mzr.js +3 -0
- package/.next/standalone/.next/server/chunks/lib_rpc_053z7as.js +3 -0
- package/.next/standalone/.next/server/chunks/lib_rpc_0b_-2te.js +3 -0
- package/.next/standalone/.next/server/chunks/lib_rpc_0dhbxdm.js +3 -0
- package/.next/standalone/.next/server/chunks/lib_rpc_0dxlk31.js +3 -0
- package/.next/standalone/.next/server/chunks/lib_rpc_0f8b-qc.js +3 -0
- package/.next/standalone/.next/server/chunks/lib_rpc_0pzie7_.js +3 -0
- package/.next/standalone/.next/server/chunks/lib_rpc_0r76fq_.js +3 -0
- package/.next/standalone/.next/server/chunks/lib_rpc_0ut7pjp.js +3 -0
- package/.next/standalone/.next/server/chunks/lib_rpc_15zyqaz.js +3 -0
- package/.next/standalone/.next/server/chunks/lib_rpc_18zpjtq.js +3 -0
- package/.next/standalone/.next/server/chunks/lib_rpc_1a8xkiz.js +3 -0
- package/.next/standalone/.next/server/chunks/lib_rpc_1e_55xo.js +3 -0
- package/.next/standalone/.next/server/chunks/lib_rpc_1oxfd1_.js +3 -0
- package/.next/standalone/.next/server/chunks/lib_rpc_1s0lod0.js +3 -0
- package/.next/standalone/.next/server/chunks/lib_scoring_1-e4ur0.js +1 -1
- package/.next/standalone/.next/server/chunks/ssr/_01kacen._.js +3 -0
- package/.next/standalone/.next/server/chunks/ssr/_03l7vge._.js +3 -0
- package/.next/standalone/.next/server/chunks/ssr/{_1ap1tu_._.js → _0gtv9jb._.js} +2 -2
- package/.next/standalone/.next/server/chunks/ssr/_0jdkqgi._.js +3 -0
- package/.next/standalone/.next/server/chunks/ssr/_0xi-fbs._.js +3 -0
- package/.next/standalone/.next/server/chunks/ssr/_0xmwmfv._.js +3 -0
- package/.next/standalone/.next/server/chunks/ssr/_1-b9hj7._.js +9 -0
- package/.next/standalone/.next/server/chunks/ssr/_112ewem._.js +3 -0
- package/.next/standalone/.next/server/chunks/ssr/_16ab63q._.js +3 -0
- package/.next/standalone/.next/server/chunks/ssr/_1fxm0-s._.js +3 -0
- package/.next/standalone/.next/server/chunks/ssr/_1ra1d3r._.js +9 -0
- package/.next/standalone/.next/server/chunks/ssr/app_141w189._.js +3 -0
- package/.next/standalone/.next/server/chunks/ssr/app_page_1fazqbf.js +22 -10
- package/.next/standalone/.next/server/chunks/ssr/app_paths_page_1ol90m7.js +2 -2
- 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 +6 -5
- 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 +19 -3
- package/.next/standalone/.next/server/chunks/ssr/app_social_page_0h7zxda.js +1 -1
- package/.next/standalone/.next/server/chunks/ssr/lib_0i8v-s3._.js +1 -1
- package/.next/standalone/.next/server/chunks/ssr/lib_13rapwj._.js +3 -0
- package/.next/standalone/.next/server/chunks/ssr/lib_galaxy-lab_108_lu-.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/pages/404.html +1 -1
- package/.next/standalone/.next/server/pages/500.html +1 -1
- 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/03bziijw5srol.js +1 -0
- package/.next/standalone/.next/static/chunks/0bgq8wmopl_ig.js +7 -0
- package/.next/standalone/.next/static/chunks/0ezus6p2eb-vg.js +1 -0
- package/.next/standalone/.next/static/chunks/0g_fy2034bp0x.js +34 -0
- package/.next/standalone/.next/static/chunks/0q5dru4q6gxl5.js +22 -0
- package/.next/standalone/.next/static/chunks/0w-byiwb2zax8.js +2 -0
- package/.next/standalone/.next/static/chunks/1g7jcovdejc9-.js +1 -0
- package/.next/standalone/.next/static/chunks/1i51yylxdzht1.js +1 -0
- package/.next/{static/chunks/1xptlvtcz6654.css → standalone/.next/static/chunks/1x_ysly42-teq.css} +1 -1
- package/.next/standalone/.next/static/chunks/1zf8swaasjf1e.js +43 -0
- package/.next/standalone/.next/static/chunks/2rqtpccruavf2.js +1 -0
- package/.next/standalone/.next/static/chunks/2wgz9ietkqqe6.js +1 -0
- package/.next/standalone/.next/static/chunks/30x3oq_st7a5r.js +1 -0
- package/.next/standalone/.next/static/chunks/355ryio1b7sm1.js +1 -0
- package/.next/standalone/.next/static/chunks/3ap5hgma7n0o7.js +1 -0
- package/.next/standalone/.next/static/chunks/3fci6qi39-1n5.js +1 -0
- package/.next/standalone/.next/static/chunks/{24tvdun1cb3g1.js → 3sowbvbaj1s6n.js} +1 -1
- package/.next/standalone/CHANGELOG.md +235 -0
- package/.next/standalone/app/api/data/export/route.js +2 -1
- package/.next/standalone/app/api/ingest/route.js +40 -1
- package/.next/standalone/app/api/scraper/route.js +12 -2
- package/.next/standalone/app/api/settings/sector-preview/route.js +2 -1
- package/.next/standalone/app/api/social/crm/route.js +37 -0
- package/.next/standalone/app/api/social/messages/route.js +44 -8
- package/.next/standalone/app/api/social/route.js +80 -22
- package/.next/standalone/app/components/AppTabs.js +59 -0
- package/.next/standalone/app/components/ChainView.js +303 -89
- package/.next/standalone/app/components/CirclePanel.js +162 -0
- package/.next/standalone/app/components/CompanyScores.js +2 -2
- package/.next/standalone/app/components/FieldStep.js +4 -4
- package/.next/standalone/app/components/FilterPanel.js +121 -183
- package/.next/standalone/app/components/ForceGraph.js +41 -3
- package/.next/standalone/app/components/GalaxyLab.js +26 -31
- package/.next/standalone/app/components/LegacyScoresCard.js +2 -2
- package/.next/standalone/app/components/MapControls.js +154 -0
- package/.next/standalone/app/components/NoteDetail.js +118 -0
- package/.next/standalone/app/components/PathsAnalyzer.js +147 -110
- package/.next/standalone/app/components/PeopleMap.js +308 -0
- package/.next/standalone/app/components/ScanRadar.js +129 -0
- package/.next/standalone/app/components/ScanStatusBar.js +72 -27
- package/.next/standalone/app/components/SeparationView.js +626 -118
- package/.next/standalone/app/components/Sidebar.js +28 -2
- package/.next/standalone/app/components/settings/DataSection.js +12 -3
- package/.next/standalone/app/components/settings/NetworkHealthSection.js +1 -1
- package/.next/standalone/app/components/settings/SectorSection.js +2 -2
- package/.next/standalone/app/components/settings/TierSection.js +1 -1
- package/.next/standalone/app/components/settings/TitleSection.js +149 -0
- package/.next/standalone/app/components/views.js +15 -14
- package/.next/standalone/app/globals.css +11 -0
- package/.next/standalone/app/page.js +89 -156
- package/.next/standalone/app/paths/page.js +39 -27
- package/.next/standalone/app/profile/page.js +33 -5
- package/.next/standalone/app/queue/page.js +56 -34
- package/.next/standalone/app/scores/page.js +16 -66
- package/.next/standalone/app/settings/page.js +27 -13
- package/.next/standalone/app/setup/page.js +298 -205
- package/.next/standalone/app/social/Crm.js +683 -0
- package/.next/standalone/app/social/SocialHub.js +374 -0
- package/.next/standalone/app/social/Thread.js +90 -0
- package/.next/standalone/app/social/page.js +10 -351
- package/.next/standalone/lib/chain-layout.js +105 -35
- package/.next/standalone/lib/data-export.js +10 -4
- package/.next/standalone/lib/data-folder.js +9 -0
- package/.next/standalone/lib/data-import.js +13 -3
- package/.next/standalone/lib/degrees.js +0 -6
- package/.next/standalone/lib/dot-rings.js +25 -0
- package/.next/standalone/lib/galaxy-lab.js +65 -6
- package/.next/standalone/lib/ingest.js +2 -0
- package/.next/standalone/lib/island.js +40 -0
- package/.next/standalone/lib/linkedin-export.js +267 -46
- package/.next/standalone/lib/linkedin-limits.js +6 -2
- package/.next/standalone/lib/map-heat.js +20 -0
- package/.next/standalone/lib/network-health.js +1 -1
- package/.next/standalone/lib/notifications.js +121 -0
- package/.next/standalone/lib/people-map.js +82 -0
- package/.next/standalone/lib/rarity.js +45 -3
- package/.next/standalone/lib/reach.js +7 -6
- package/.next/standalone/lib/rpc.js +15 -4
- package/.next/standalone/lib/scan-pace.js +46 -0
- package/.next/standalone/lib/scoring.js +39 -8
- package/.next/standalone/lib/scraper-client.js +19 -6
- package/.next/standalone/lib/sector-focus.js +4 -4
- package/.next/standalone/lib/separation.js +137 -10
- package/.next/standalone/lib/settings-effects.js +20 -11
- package/.next/standalone/lib/settings.js +3 -0
- package/.next/standalone/lib/social-crm.js +415 -0
- package/.next/standalone/lib/social-store.js +13 -6
- package/.next/standalone/lib/tier-grid.js +99 -0
- package/.next/standalone/lib/title-ranking.js +83 -0
- package/.next/standalone/package-lock.json +2 -2
- package/.next/standalone/package.json +1 -1
- package/.next/standalone/scripts/scrape.py +177 -35
- package/.next/static/chunks/03bziijw5srol.js +1 -0
- package/.next/static/chunks/0bgq8wmopl_ig.js +7 -0
- package/.next/static/chunks/0ezus6p2eb-vg.js +1 -0
- package/.next/static/chunks/0g_fy2034bp0x.js +34 -0
- package/.next/static/chunks/0q5dru4q6gxl5.js +22 -0
- package/.next/static/chunks/0w-byiwb2zax8.js +2 -0
- package/.next/static/chunks/1g7jcovdejc9-.js +1 -0
- package/.next/static/chunks/1i51yylxdzht1.js +1 -0
- package/.next/{standalone/.next/static/chunks/1xptlvtcz6654.css → static/chunks/1x_ysly42-teq.css} +1 -1
- package/.next/static/chunks/1zf8swaasjf1e.js +43 -0
- package/.next/static/chunks/2rqtpccruavf2.js +1 -0
- package/.next/static/chunks/2wgz9ietkqqe6.js +1 -0
- package/.next/static/chunks/30x3oq_st7a5r.js +1 -0
- package/.next/static/chunks/355ryio1b7sm1.js +1 -0
- package/.next/static/chunks/3ap5hgma7n0o7.js +1 -0
- package/.next/static/chunks/3fci6qi39-1n5.js +1 -0
- package/.next/static/chunks/{24tvdun1cb3g1.js → 3sowbvbaj1s6n.js} +1 -1
- package/CHANGELOG.md +235 -0
- package/lib/chain-layout.js +105 -35
- package/lib/data-export.js +10 -4
- package/lib/data-folder.js +9 -0
- package/lib/data-import.js +13 -3
- package/lib/degrees.js +0 -6
- package/lib/dot-rings.js +25 -0
- package/lib/galaxy-lab.js +65 -6
- package/lib/ingest.js +2 -0
- package/lib/island.js +40 -0
- package/lib/linkedin-export.js +267 -46
- package/lib/linkedin-limits.js +6 -2
- package/lib/map-heat.js +20 -0
- package/lib/network-health.js +1 -1
- package/lib/notifications.js +121 -0
- package/lib/people-map.js +82 -0
- package/lib/rarity.js +45 -3
- package/lib/reach.js +7 -6
- package/lib/rpc.js +15 -4
- package/lib/scan-pace.js +46 -0
- package/lib/scoring.js +39 -8
- package/lib/scraper-client.js +19 -6
- package/lib/sector-focus.js +4 -4
- package/lib/separation.js +137 -10
- package/lib/settings-effects.js +20 -11
- package/lib/settings.js +3 -0
- package/lib/social-crm.js +415 -0
- package/lib/social-store.js +13 -6
- package/lib/tier-grid.js +99 -0
- package/lib/title-ranking.js +83 -0
- package/package.json +1 -1
- package/scripts/scrape.py +177 -35
- package/.next/standalone/.next/server/chunks/[root-of-the-server]__08f5awg._.js +0 -8
- package/.next/standalone/.next/server/chunks/[root-of-the-server]__17hjtzl._.js +0 -8
- package/.next/standalone/.next/server/chunks/[root-of-the-server]__20tmem7._.js +0 -3
- package/.next/standalone/.next/server/chunks/lib_18cr1gi._.js +0 -8
- package/.next/standalone/.next/server/chunks/lib_rpc_03i8xef.js +0 -3
- package/.next/standalone/.next/server/chunks/lib_rpc_08fc3rp.js +0 -3
- package/.next/standalone/.next/server/chunks/lib_rpc_08r2t_q.js +0 -3
- package/.next/standalone/.next/server/chunks/lib_rpc_0btpv1t.js +0 -3
- package/.next/standalone/.next/server/chunks/lib_rpc_0c9d9ti.js +0 -8
- package/.next/standalone/.next/server/chunks/lib_rpc_0cm2ev3.js +0 -3
- package/.next/standalone/.next/server/chunks/lib_rpc_0iovswz.js +0 -3
- package/.next/standalone/.next/server/chunks/lib_rpc_0is8flp.js +0 -3
- package/.next/standalone/.next/server/chunks/lib_rpc_0v8ajtd.js +0 -3
- package/.next/standalone/.next/server/chunks/lib_rpc_0xgmvvt.js +0 -3
- package/.next/standalone/.next/server/chunks/lib_rpc_144mzbf.js +0 -3
- package/.next/standalone/.next/server/chunks/lib_rpc_1bggi0n.js +0 -3
- package/.next/standalone/.next/server/chunks/lib_rpc_1dkcu9f.js +0 -3
- package/.next/standalone/.next/server/chunks/lib_rpc_1o0jb6w.js +0 -3
- package/.next/standalone/.next/server/chunks/ssr/_0c_y09g._.js +0 -3
- package/.next/standalone/.next/server/chunks/ssr/_0ezj4e0._.js +0 -3
- package/.next/standalone/.next/server/chunks/ssr/_0j4-lc3._.js +0 -3
- package/.next/standalone/.next/server/chunks/ssr/_0l8cxmo._.js +0 -3
- package/.next/standalone/.next/server/chunks/ssr/_0xkur4p._.js +0 -3
- package/.next/standalone/.next/server/chunks/ssr/_14iv_zy._.js +0 -3
- package/.next/standalone/.next/server/chunks/ssr/_15x6q32._.js +0 -3
- package/.next/standalone/.next/server/chunks/ssr/_1errbuf._.js +0 -3
- package/.next/standalone/.next/server/chunks/ssr/_212xw8q._.js +0 -3
- package/.next/standalone/.next/server/chunks/ssr/app_components_19c2d9c._.js +0 -3
- package/.next/standalone/.next/server/chunks/ssr/app_scores_page_09wepvv.js +0 -3
- package/.next/standalone/.next/static/chunks/07dyt47mp4s9-.js +0 -1
- package/.next/standalone/.next/static/chunks/0a5iadnmt9u9k.js +0 -1
- package/.next/standalone/.next/static/chunks/160wjb73ddwqd.js +0 -6
- package/.next/standalone/.next/static/chunks/1c_n0973t07l0.js +0 -2
- package/.next/standalone/.next/static/chunks/1e1n0kkz-psdb.js +0 -1
- package/.next/standalone/.next/static/chunks/206rm6j42gl_a.js +0 -1
- package/.next/standalone/.next/static/chunks/236f0jtp68j_h.js +0 -1
- package/.next/standalone/.next/static/chunks/2coxzjm_k6ilr.js +0 -1
- package/.next/standalone/.next/static/chunks/2kfhc58df2xyo.js +0 -1
- package/.next/standalone/.next/static/chunks/2pw6_osz1n_te.js +0 -1
- package/.next/standalone/.next/static/chunks/39hr8d-b2ibtw.js +0 -31
- package/.next/standalone/.next/static/chunks/3zw8bv7-mc4n6.js +0 -33
- package/.next/standalone/app/components/OrbitGraph.js +0 -737
- package/.next/standalone/app/social/Conversations.js +0 -246
- package/.next/static/chunks/07dyt47mp4s9-.js +0 -1
- package/.next/static/chunks/0a5iadnmt9u9k.js +0 -1
- package/.next/static/chunks/160wjb73ddwqd.js +0 -6
- package/.next/static/chunks/1c_n0973t07l0.js +0 -2
- package/.next/static/chunks/1e1n0kkz-psdb.js +0 -1
- package/.next/static/chunks/206rm6j42gl_a.js +0 -1
- package/.next/static/chunks/236f0jtp68j_h.js +0 -1
- package/.next/static/chunks/2coxzjm_k6ilr.js +0 -1
- package/.next/static/chunks/2kfhc58df2xyo.js +0 -1
- package/.next/static/chunks/2pw6_osz1n_te.js +0 -1
- package/.next/static/chunks/39hr8d-b2ibtw.js +0 -31
- package/.next/static/chunks/3zw8bv7-mc4n6.js +0 -33
- /package/.next/standalone/.next/static/{7zWJnSkfSoRhRsqY6ZKtD → I-cj79yE2IZ8taX2dZGp3}/_buildManifest.js +0 -0
- /package/.next/standalone/.next/static/{7zWJnSkfSoRhRsqY6ZKtD → I-cj79yE2IZ8taX2dZGp3}/_clientMiddlewareManifest.js +0 -0
- /package/.next/standalone/.next/static/{7zWJnSkfSoRhRsqY6ZKtD → I-cj79yE2IZ8taX2dZGp3}/_ssgManifest.js +0 -0
- /package/.next/static/{7zWJnSkfSoRhRsqY6ZKtD → I-cj79yE2IZ8taX2dZGp3}/_buildManifest.js +0 -0
- /package/.next/static/{7zWJnSkfSoRhRsqY6ZKtD → I-cj79yE2IZ8taX2dZGp3}/_clientMiddlewareManifest.js +0 -0
- /package/.next/static/{7zWJnSkfSoRhRsqY6ZKtD → I-cj79yE2IZ8taX2dZGp3}/_ssgManifest.js +0 -0
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
// Paths → People: the Paths map with people instead of companies (Blake,
|
|
2
|
+
// 2026-10-02: "basically going to look like map but people based and the
|
|
3
|
+
// bigger circle is their cluster with value"). One bubble per connection of
|
|
4
|
+
// yours, sized by the cluster behind them; a line joins two connections whose
|
|
5
|
+
// clusters share people.
|
|
6
|
+
//
|
|
7
|
+
// Plain functions over the rows the app already has, no React, so
|
|
8
|
+
// tests/people-map.test.mjs runs them as they are.
|
|
9
|
+
|
|
10
|
+
import { keyFor } from './separation.js';
|
|
11
|
+
|
|
12
|
+
/** What a cluster is worth: S tier counts 3, A tier 2, everyone else 1. */
|
|
13
|
+
export const clusterValue = ({ size = 0, S = 0, A = 0 } = {}) => size + 2 * S + A;
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* One entry per connection: { person, id, key, size, S, A, value, only, share }.
|
|
17
|
+
* `size` is the people in their cluster (each once), `only` how many of those
|
|
18
|
+
* none of your other connections' clusters hold, `share` = only / size.
|
|
19
|
+
* Also `links`: [{ a, b, shared }] for every two connections whose clusters
|
|
20
|
+
* share people, most shared first, and `rank(id)`: the share of your scanned
|
|
21
|
+
* connections whose cluster is worth less.
|
|
22
|
+
*/
|
|
23
|
+
export function peopleMap(d1 = [], d2 = [], { maxLinks = 400 } = {}) {
|
|
24
|
+
const ids = new Set((d1 || []).map((c) => c.id));
|
|
25
|
+
// Each person once per connection whose scan found them.
|
|
26
|
+
const byConnection = new Map(); // id -> Map(key -> row)
|
|
27
|
+
const holders = new Map(); // key -> Set(connection id)
|
|
28
|
+
for (const row of d2 || []) {
|
|
29
|
+
const from = row?.source_connection_id;
|
|
30
|
+
if (from == null || !ids.has(from)) continue;
|
|
31
|
+
const key = keyFor(row);
|
|
32
|
+
let circle = byConnection.get(from);
|
|
33
|
+
if (!circle) byConnection.set(from, (circle = new Map()));
|
|
34
|
+
if (!circle.has(key)) circle.set(key, row);
|
|
35
|
+
let who = holders.get(key);
|
|
36
|
+
if (!who) holders.set(key, (who = new Set()));
|
|
37
|
+
who.add(from);
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
const people = (d1 || []).map((c) => {
|
|
41
|
+
const circle = byConnection.get(c.id);
|
|
42
|
+
let S = 0, A = 0, only = 0;
|
|
43
|
+
if (circle) {
|
|
44
|
+
for (const [key, row] of circle) {
|
|
45
|
+
if (row.tier === 'S') S++;
|
|
46
|
+
else if (row.tier === 'A') A++;
|
|
47
|
+
if (holders.get(key)?.size === 1) only++;
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
const size = circle ? circle.size : 0;
|
|
51
|
+
return { person: c, id: c.id, key: keyFor(c), size, S, A, value: clusterValue({ size, S, A }), only, share: size ? only / size : 0 };
|
|
52
|
+
});
|
|
53
|
+
|
|
54
|
+
// Two connections whose clusters share people. A person known to many of
|
|
55
|
+
// your connections would make a line between every two of them, so past
|
|
56
|
+
// eight holders they are left out of the lines (they still count in `only`).
|
|
57
|
+
const pairs = new Map();
|
|
58
|
+
for (const who of holders.values()) {
|
|
59
|
+
if (who.size < 2 || who.size > 8) continue;
|
|
60
|
+
const list = [...who].sort();
|
|
61
|
+
for (let i = 0; i < list.length; i++) {
|
|
62
|
+
for (let j = i + 1; j < list.length; j++) {
|
|
63
|
+
const k = `${list[i]}\u0000${list[j]}`;
|
|
64
|
+
pairs.set(k, (pairs.get(k) || 0) + 1);
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
const links = [...pairs].map(([k, shared]) => {
|
|
69
|
+
const [a, b] = k.split('\u0000');
|
|
70
|
+
return { a, b, shared };
|
|
71
|
+
}).sort((x, y) => y.shared - x.shared || (x.a < y.a ? -1 : 1)).slice(0, maxLinks);
|
|
72
|
+
|
|
73
|
+
const values = people.filter((p) => p.size > 0).map((p) => p.value).sort((x, y) => x - y);
|
|
74
|
+
const rank = (id) => {
|
|
75
|
+
const p = people.find((x) => x.id === id);
|
|
76
|
+
if (!p || !p.size || values.length < 2) return null;
|
|
77
|
+
let below = 0;
|
|
78
|
+
while (below < values.length && values[below] < p.value) below++;
|
|
79
|
+
return below / (values.length - 1);
|
|
80
|
+
};
|
|
81
|
+
return { people, links, rank };
|
|
82
|
+
}
|
|
@@ -39,12 +39,15 @@ export function rarityInfo(key) {
|
|
|
39
39
|
/**
|
|
40
40
|
* How many mutual connections you share with someone, and where that number
|
|
41
41
|
* came from: { count, from: 'linkedin' | 'scans' }. `waysIn` is how many of
|
|
42
|
-
* your scanned connections know them (lib/separation.js).
|
|
42
|
+
* your scanned connections know them (lib/separation.js): never fewer than that.
|
|
43
43
|
*/
|
|
44
44
|
export function mutualsOf(row, waysIn = 1) {
|
|
45
45
|
const saved = Number(row?.mutual_count);
|
|
46
|
-
|
|
47
|
-
|
|
46
|
+
const seen = Math.max(1, Math.floor(Number(waysIn) || 1));
|
|
47
|
+
// Everyone the app found them through is a mutual connection, so a saved
|
|
48
|
+
// count lower than that is out of date: "only way in" with two ways drawn.
|
|
49
|
+
if (Number.isFinite(saved) && saved >= seen) return { count: Math.floor(saved), from: 'linkedin' };
|
|
50
|
+
return { count: seen, from: 'scans' };
|
|
48
51
|
}
|
|
49
52
|
|
|
50
53
|
/** Someone's rarity: { key, count, from }. */
|
|
@@ -78,3 +81,42 @@ export function toggle(set, key) {
|
|
|
78
81
|
else next.add(key);
|
|
79
82
|
return next;
|
|
80
83
|
}
|
|
84
|
+
|
|
85
|
+
// ── The Separation slider ──────────────────────────────────────────────────
|
|
86
|
+
// Blake, 2026-10-01: the rare person reachable through one door at one end,
|
|
87
|
+
// the easier, well-connected ones at the other, "so it's seamless"; 2026-10-02:
|
|
88
|
+
// "i want it to be slider ajusted and can be dynmaic". One number, 0 to 100:
|
|
89
|
+
// 0 puts the rarest ways in first, 100 the easiest, and 50 is each person's own
|
|
90
|
+
// score alone, as the list has always been. It reorders; it never changes a
|
|
91
|
+
// score, a tier or a rank. No odds are claimed: the app hasn't measured how
|
|
92
|
+
// often anyone accepts.
|
|
93
|
+
|
|
94
|
+
export const SLIDER_MIDDLE = 50;
|
|
95
|
+
|
|
96
|
+
/** How easy the way in is: 0 with one mutual connection, 1 with 31 or more, on a log scale (1→3 counts like 10→30). */
|
|
97
|
+
export function easeOf(count) {
|
|
98
|
+
const n = Math.max(1, Math.floor(Number(count) || 1));
|
|
99
|
+
return Math.min(1, Math.log(n) / Math.log(31));
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
/**
|
|
103
|
+
* Where someone sorts with the slider at `at`: their score, weighted by how
|
|
104
|
+
* well their way in fits the end the slider leans to. At 50 it is the score.
|
|
105
|
+
*/
|
|
106
|
+
export function slideValue(score, count, at = SLIDER_MIDDLE) {
|
|
107
|
+
const lean = (Math.max(0, Math.min(100, Number(at) || 0)) - SLIDER_MIDDLE) / SLIDER_MIDDLE; // -1 rare … +1 easy
|
|
108
|
+
if (!lean) return score;
|
|
109
|
+
const ease = easeOf(count);
|
|
110
|
+
const pull = Math.abs(lean);
|
|
111
|
+
return score * (1 - pull + pull * (lean < 0 ? 1 - ease : ease));
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
/** What the slider is doing at `at`, in words: { name, detail }. */
|
|
115
|
+
export function slideLabel(at = SLIDER_MIDDLE) {
|
|
116
|
+
const v = Math.max(0, Math.min(100, Math.round(Number(at) || 0)));
|
|
117
|
+
if (v <= 10) return { name: 'Rarest first', detail: 'Strong people with one way in: the connection who knows them is the only door you have.' };
|
|
118
|
+
if (v < 45) return { name: 'Leaning rare', detail: 'Strong people with few mutual connections move up; the ones everybody knows move down.' };
|
|
119
|
+
if (v <= 55) return { name: 'By power', detail: 'Each person’s own score alone. How many of your connections know them doesn’t change the order.' };
|
|
120
|
+
if (v < 90) return { name: 'Leaning easy', detail: 'Strong people you share more mutual connections with move up: more people who could introduce you.' };
|
|
121
|
+
return { name: 'Easiest first', detail: 'Strong people you share the most mutual connections with: the warmest introductions. All the way right, the map aims at whoever you share the most with, with the ones in circles you haven’t scanned yet as grey dots.' };
|
|
122
|
+
}
|
|
@@ -20,6 +20,7 @@
|
|
|
20
20
|
//
|
|
21
21
|
// Plain functions, no React; tests/reach.test.mjs.
|
|
22
22
|
|
|
23
|
+
import { paceSeconds } from './scan-pace.js';
|
|
23
24
|
import { keyFor, compareBridges } from './separation.js';
|
|
24
25
|
|
|
25
26
|
/**
|
|
@@ -107,13 +108,13 @@ export function readyByCircle(reach) {
|
|
|
107
108
|
* page, a minute more after every 10), so about 55 minutes for a whole list.
|
|
108
109
|
* The Scan page and the Degrees panel both say it from here.
|
|
109
110
|
*/
|
|
110
|
-
export function circleScanCost(pages = 100) {
|
|
111
|
+
export function circleScanCost(pages = 100, pace = 'fast') {
|
|
111
112
|
const searches = Math.max(1, Math.min(100, Math.round(pages) || 100));
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
searches,
|
|
115
|
-
|
|
116
|
-
};
|
|
113
|
+
// At Fast, as it has always been said; slower speeds add their waits (lib/scan-pace.js).
|
|
114
|
+
const minutes = !pace || pace === 'fast'
|
|
115
|
+
? (searches >= 100 ? 55 : Math.max(5, Math.round(searches * 0.55)))
|
|
116
|
+
: Math.max(5, Math.round(paceSeconds(pace, searches) / 60));
|
|
117
|
+
return { profileViews: 1, searches, minutes };
|
|
117
118
|
}
|
|
118
119
|
|
|
119
120
|
/**
|
|
@@ -7,6 +7,7 @@ import { getDb, newId, nowIso } from './db-client.js';
|
|
|
7
7
|
// for the routes that call it; it rescores with lib/scoring.js like the rest.
|
|
8
8
|
|
|
9
9
|
import { readNetwork, scoreNetwork, explainScore, SCORING_VERSION, KNOWN_COMPANIES, COMPANY_DATA, RULE_TABLES, TIER_SCALE_SETTING } from './scoring.js';
|
|
10
|
+
import { TITLE_RANKING_SETTING, isDefaultRanking, rankingFingerprint } from './title-ranking.js';
|
|
10
11
|
import { industryKeyOf, INDUSTRIES } from './companies.js';
|
|
11
12
|
import { readSettings } from './settings.js';
|
|
12
13
|
import { SECTOR_FOCUS_SETTING, focusFingerprint, previewSectorFocus } from './sector-focus.js';
|
|
@@ -69,6 +70,12 @@ export function tierScaleOf(db = getDb()) {
|
|
|
69
70
|
return readSettings(db, { tierScale: TIER_SCALE_SETTING }).tierScale;
|
|
70
71
|
}
|
|
71
72
|
|
|
73
|
+
/** Your title ranking (Settings → Scores → Titles), or null when you haven't changed one. */
|
|
74
|
+
export function titleRankingOf(db = getDb()) {
|
|
75
|
+
const r = readSettings(db, { titleRanking: TITLE_RANKING_SETTING }).titleRanking;
|
|
76
|
+
return isDefaultRanking(r) ? null : r;
|
|
77
|
+
}
|
|
78
|
+
|
|
72
79
|
/**
|
|
73
80
|
* What rescoring reads: every row, as scoring needs it. `withPeople` adds each
|
|
74
81
|
* row's name, for naming who a change would move.
|
|
@@ -111,12 +118,13 @@ export function rescoreAll({ compareWith } = {}) {
|
|
|
111
118
|
const overrides = companyOverrides(db);
|
|
112
119
|
const focus = sectorFocusOf(db);
|
|
113
120
|
const tierScale = tierScaleOf(db);
|
|
121
|
+
const titles = titleRankingOf(db);
|
|
114
122
|
// Every headline is read once, however many ways the rows are then scored.
|
|
115
123
|
const read = readForScoring(rows);
|
|
116
|
-
const { scores } = scoreNetwork(rows, { overrides, focus, tierScale, read });
|
|
124
|
+
const { scores } = scoreNetwork(rows, { overrides, focus, tierScale, titles, read });
|
|
117
125
|
// Worked out before anything is written, so a failure leaves nothing half done.
|
|
118
126
|
const change = compareWith === undefined ? null
|
|
119
|
-
: previewSectorFocus(rows, { overrides, read, from: compareWith, to: focus, tierScale, examples: 0 });
|
|
127
|
+
: previewSectorFocus(rows, { overrides, read, from: compareWith, to: focus, tierScale, titles, examples: 0 });
|
|
120
128
|
const update = db.prepare(`UPDATE linkedin_connections SET seniority_score = ?, company_prestige_score = ?, power_score = ?, tier = ?,
|
|
121
129
|
score_why = ?, circle_power = ?, circle_s_count = ?, circle_a_count = ?, circle_elite_pct = ?, is_catalyst = ?, catalyst_score = ?, updated_at = ?
|
|
122
130
|
WHERE id = ?`);
|
|
@@ -144,6 +152,7 @@ export function rescoreAll({ compareWith } = {}) {
|
|
|
144
152
|
meta.run('scoring_list', KNOWN_LIST_STAMP);
|
|
145
153
|
meta.run('scoring_focus', focusFingerprint(focus));
|
|
146
154
|
meta.run('scoring_tiers', tierScale);
|
|
155
|
+
meta.run('scoring_titles', rankingFingerprint(titles || undefined));
|
|
147
156
|
db.exec('COMMIT');
|
|
148
157
|
} catch (err) {
|
|
149
158
|
db.exec('ROLLBACK');
|
|
@@ -154,7 +163,7 @@ export function rescoreAll({ compareWith } = {}) {
|
|
|
154
163
|
|
|
155
164
|
/**
|
|
156
165
|
* Rescore once if the stored scores came from an older scoring model or
|
|
157
|
-
* curated list, or from a sector focus
|
|
166
|
+
* curated list, or from a sector focus, tier scale or title ranking other than the one saved
|
|
158
167
|
* now: an update, a database restored or brought from another computer, or a
|
|
159
168
|
* Settings save whose rescore never finished.
|
|
160
169
|
*/
|
|
@@ -164,7 +173,9 @@ export function rescoreIfStale() {
|
|
|
164
173
|
const fresh = meta('scoring_version') === String(SCORING_VERSION)
|
|
165
174
|
&& meta('scoring_list') === KNOWN_LIST_STAMP
|
|
166
175
|
&& meta('scoring_focus') === focusFingerprint(sectorFocusOf(db))
|
|
167
|
-
&& meta('scoring_tiers') === tierScaleOf(db)
|
|
176
|
+
&& meta('scoring_tiers') === tierScaleOf(db)
|
|
177
|
+
// Scores from before title rankings existed have no stamp, and none is the same as no ranking.
|
|
178
|
+
&& (meta('scoring_titles') ?? 'none') === rankingFingerprint(titleRankingOf(db) || undefined);
|
|
168
179
|
return fresh ? { scored: 0 } : rescoreAll();
|
|
169
180
|
}
|
|
170
181
|
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
// The Scan page's speed: Fast, Medium or Slow (Blake, 2026-09-30: "a bar that
|
|
2
|
+
// will adjust the scanner from slow, medium and fast… the current is
|
|
3
|
+
// considered fast, and then we lower to medium and slow, bogging it down,
|
|
4
|
+
// making it less likely to get caught, and estimated time for each").
|
|
5
|
+
//
|
|
6
|
+
// Fast is today's pacing and nothing is faster; Medium and Slow only add
|
|
7
|
+
// waiting. Speed changes searches an hour, never searches a day: the daily
|
|
8
|
+
// budget is still the volume cap. scripts/scrape.py SCAN_PACES holds the same
|
|
9
|
+
// numbers (tests/scan-pace.test.mjs checks they agree). Fixed waits, never
|
|
10
|
+
// random "human" timing: the point is fewer searches an hour, not disguise.
|
|
11
|
+
|
|
12
|
+
export const PACES = {
|
|
13
|
+
fast: { label: 'Fast', pagePause: 20, chunkCooldown: 60, profileGap: 60 },
|
|
14
|
+
medium: { label: 'Medium', pagePause: 45, chunkCooldown: 180, profileGap: 90 },
|
|
15
|
+
slow: { label: 'Slow', pagePause: 90, chunkCooldown: 300, profileGap: 120 },
|
|
16
|
+
};
|
|
17
|
+
export const PACE_NAMES = ['slow', 'medium', 'fast'];
|
|
18
|
+
export const DEFAULT_PACE = 'fast';
|
|
19
|
+
// About how long reading one page of results takes, measured in 0.1.6.
|
|
20
|
+
export const WORK_SECONDS = 6;
|
|
21
|
+
const EVERY = 10; // the longer rest comes after every 10 pages (scrape.py SAVE_EVERY_PAGES)
|
|
22
|
+
|
|
23
|
+
export const paceOf = (name) => PACES[name] || PACES[DEFAULT_PACE];
|
|
24
|
+
|
|
25
|
+
/** Seconds to run `searches` searches at a speed: the work, the rest before each, the longer rest every 10. */
|
|
26
|
+
export function paceSeconds(name, searches) {
|
|
27
|
+
const p = paceOf(name);
|
|
28
|
+
const n = Math.max(0, Math.floor(Number(searches) || 0));
|
|
29
|
+
return n * (WORK_SECONDS + p.pagePause) + Math.floor(n / EVERY) * p.chunkCooldown;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
/** About how many searches an hour a speed makes. */
|
|
33
|
+
export function searchesPerHour(name) {
|
|
34
|
+
const p = paceOf(name);
|
|
35
|
+
return Math.round(3600 / (WORK_SECONDS + p.pagePause + p.chunkCooldown / EVERY));
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
/** "about 1 h 7 min", "about 27 min", "under a minute" */
|
|
39
|
+
export function durationText(seconds) {
|
|
40
|
+
if (!(seconds >= 60)) return 'under a minute';
|
|
41
|
+
const m = Math.round(seconds / 60);
|
|
42
|
+
if (m < 60) return `about ${m} min`;
|
|
43
|
+
const h = Math.floor(m / 60);
|
|
44
|
+
const rest = m % 60;
|
|
45
|
+
return `about ${h} h${rest ? ` ${rest} min` : ''}`;
|
|
46
|
+
}
|
|
@@ -2512,18 +2512,47 @@ export function readPerson(row) {
|
|
|
2512
2512
|
* points whatever else they list. `companyFor(name)` returns companyScore();
|
|
2513
2513
|
* `read` is the row's readPerson(), when it has been read already.
|
|
2514
2514
|
*/
|
|
2515
|
-
/**
|
|
2516
|
-
|
|
2517
|
-
|
|
2515
|
+
/** Does this title text have the words? Whole words only: "vc" isn't in "dvc", "cto" isn't in "doctor". */
|
|
2516
|
+
const titlePatterns = new Map();
|
|
2517
|
+
export function titleHas(text, words) {
|
|
2518
|
+
let re = titlePatterns.get(words);
|
|
2519
|
+
if (!re) {
|
|
2520
|
+
const esc = words.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
|
2521
|
+
re = new RegExp(`(^|[^a-z0-9])${esc}($|[^a-z0-9])`);
|
|
2522
|
+
if (titlePatterns.size > 500) titlePatterns.clear();
|
|
2523
|
+
titlePatterns.set(words, re);
|
|
2524
|
+
}
|
|
2525
|
+
return re.test(String(text || '').toLowerCase());
|
|
2526
|
+
}
|
|
2527
|
+
|
|
2528
|
+
/**
|
|
2529
|
+
* A title's points under your title ranking (Settings → Scores → Titles,
|
|
2530
|
+
* lib/title-ranking.js): a role whose words are in it counts what you gave
|
|
2531
|
+
* that role (the highest, if several match), else the level counts your
|
|
2532
|
+
* points for it, else the model's. No ranking: the model's.
|
|
2533
|
+
*/
|
|
2534
|
+
export function rankedPoints(role, titles) {
|
|
2535
|
+
if (!titles) return role.title.points;
|
|
2536
|
+
let match = null;
|
|
2537
|
+
for (const r of titles.roles || []) {
|
|
2538
|
+
if (titleHas(role.part, r.text) && (match == null || r.points > match)) match = r.points;
|
|
2539
|
+
}
|
|
2540
|
+
if (match != null) return match;
|
|
2541
|
+
return titles.points?.[role.title.key] ?? role.title.points;
|
|
2542
|
+
}
|
|
2543
|
+
|
|
2544
|
+
/** A role's title points as they count: yours where you ranked them, 70% for a former role, capped for a student. */
|
|
2545
|
+
function rolePoints(role, student, titles) {
|
|
2546
|
+
const points = rankedPoints(role, titles) * (role.former ? FORMER_WEIGHT : 1);
|
|
2518
2547
|
return student ? Math.min(points, STUDENT_CAP) : points;
|
|
2519
2548
|
}
|
|
2520
2549
|
|
|
2521
|
-
export function scorePerson(row, companyFor = (name) => companyScore(name), read = readPerson(row)) {
|
|
2550
|
+
export function scorePerson(row, companyFor = (name) => companyScore(name), read = readPerson(row), titles = null) {
|
|
2522
2551
|
const { roles, student, reach } = read;
|
|
2523
2552
|
let best = null;
|
|
2524
2553
|
for (const role of roles) {
|
|
2525
2554
|
const co = companyFor(role.company);
|
|
2526
|
-
const points = rolePoints(role, student);
|
|
2555
|
+
const points = rolePoints(role, student, titles);
|
|
2527
2556
|
const core = points * (0.45 + 0.055 * co.score);
|
|
2528
2557
|
if (!best || core > best.core) best = { role, co, points, core };
|
|
2529
2558
|
}
|
|
@@ -2535,6 +2564,8 @@ export function scorePerson(row, companyFor = (name) => companyScore(name), read
|
|
|
2535
2564
|
const bonus = reachBonus(row?.headline, unknown ? 4 : best.co.base ?? best.co.score, reach);
|
|
2536
2565
|
const power = round1(best.core + bonus.points);
|
|
2537
2566
|
const title = { ...best.role.title, former: best.role.former, student, points: round1(best.points) };
|
|
2567
|
+
// Your ranking moved it: the card's "why" says so.
|
|
2568
|
+
if (titles && rankedPoints(best.role, titles) !== best.role.title.points) title.ranked = true;
|
|
2538
2569
|
if (best.role.former) title.label = `Former ${title.label}`;
|
|
2539
2570
|
if (student && best.role.title.level > 0) title.label = `Student · ${title.label}`;
|
|
2540
2571
|
const out = { title, company: best.role.company, companyScore: best.co.score, companySource: best.co.source, bonus, power, tier: tierFor(power) };
|
|
@@ -2652,7 +2683,7 @@ export function explainScore(s, boost = 0, { sectorLabel } = {}) {
|
|
|
2652
2683
|
const co = s.companySector
|
|
2653
2684
|
? `${s.company} (${s.companyScore}/10: ${s.companySector.base} + ${s.companySector.bonus} your sector${which ? `: ${which}` : ''})`
|
|
2654
2685
|
: `${s.company || 'no company found'} (${s.companyScore}/10${s.companySource === 'yours' ? ', your score' : ''})`;
|
|
2655
|
-
const bits = [`${s.title.label} (${s.title.points})`, co];
|
|
2686
|
+
const bits = [`${s.title.label} (${s.title.points}${s.title.ranked ? ', your ranking' : ''})`, co];
|
|
2656
2687
|
if (s.bonus.points) bits.push(`+${s.bonus.points} ${s.bonus.reasons.join(', ')}`);
|
|
2657
2688
|
if (boost) bits.push(`+${boost} strong circle`);
|
|
2658
2689
|
return bits.join(' · ');
|
|
@@ -2716,7 +2747,7 @@ export function readNetwork(rows = [], { industryOf, sectorsOf, groupOf } = {})
|
|
|
2716
2747
|
* (companyScore(), by name) and the curve's cut-offs (null on the fixed scale).
|
|
2717
2748
|
*/
|
|
2718
2749
|
export function scoreNetwork(rows = [], {
|
|
2719
|
-
overrides = new Map(), industryOf, sectorsOf, groupOf, focus, tierScale = 'fixed',
|
|
2750
|
+
overrides = new Map(), industryOf, sectorsOf, groupOf, focus, tierScale = 'fixed', titles = null,
|
|
2720
2751
|
read = readNetwork(rows, { industryOf, sectorsOf, groupOf }),
|
|
2721
2752
|
} = {}) {
|
|
2722
2753
|
// Each company is scored once; everyone there shares the answer.
|
|
@@ -2726,7 +2757,7 @@ export function scoreNetwork(rows = [], {
|
|
|
2726
2757
|
return companyScores.get(name);
|
|
2727
2758
|
};
|
|
2728
2759
|
const out = new Map();
|
|
2729
|
-
for (const r of rows) out.set(r.id, { ...scorePerson(r, companyFor, read.people.get(r)), boost: 0 });
|
|
2760
|
+
for (const r of rows) out.set(r.id, { ...scorePerson(r, companyFor, read.people.get(r), titles), boost: 0 });
|
|
2730
2761
|
// The curve's cut-offs come from your 1st-degree people before any circle
|
|
2731
2762
|
// boost, so a boost (which reads the graded tiers) can't move its own cut-off.
|
|
2732
2763
|
const cutoffs = tierScale === 'fixed' ? null
|
|
@@ -15,11 +15,27 @@ export async function scraperStatus() {
|
|
|
15
15
|
return r.json();
|
|
16
16
|
}
|
|
17
17
|
|
|
18
|
+
/**
|
|
19
|
+
* Scan page → "Hide the Chrome window while scanning", remembered in this
|
|
20
|
+
* browser. Every way a scan starts asks here: the Scan page's own buttons used
|
|
21
|
+
* to post straight to the scanner without it, so the switch did nothing for
|
|
22
|
+
* the scans started from the very page it sits on.
|
|
23
|
+
*/
|
|
24
|
+
export const HIDE_CHROME_KEY = 'six-degrees-hide-chrome';
|
|
25
|
+
export function hideChromeOn(storage = typeof localStorage !== 'undefined' ? localStorage : null) {
|
|
26
|
+
try { return JSON.parse(storage?.getItem(HIDE_CHROME_KEY) || 'false') === true; } catch { return false; }
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
/** What a scan start sends: the action, its details, and `headless` when Chrome is to stay hidden. */
|
|
30
|
+
export function scanRequest(action, extra = {}, hidden = hideChromeOn()) {
|
|
31
|
+
return { action, ...extra, ...(hidden ? { headless: true } : {}) };
|
|
32
|
+
}
|
|
33
|
+
|
|
18
34
|
export async function startScrape(action, name, extra = {}) {
|
|
19
35
|
const r = await fetch('/api/scraper', {
|
|
20
36
|
method: 'POST',
|
|
21
37
|
headers: { 'Content-Type': 'application/json' },
|
|
22
|
-
body: JSON.stringify(
|
|
38
|
+
body: JSON.stringify(scanRequest(action, { ...(name ? { name } : {}), ...extra })),
|
|
23
39
|
});
|
|
24
40
|
const d = await r.json().catch(() => ({}));
|
|
25
41
|
if (!r.ok) throw new Error(d.error || 'Could not start the scanner.');
|
|
@@ -211,11 +227,8 @@ export async function beginScrape(action, { name, id, onLog, ...extra } = {}) {
|
|
|
211
227
|
});
|
|
212
228
|
let started;
|
|
213
229
|
try {
|
|
214
|
-
//
|
|
215
|
-
|
|
216
|
-
let hidden = false;
|
|
217
|
-
try { hidden = JSON.parse(localStorage.getItem('six-degrees-hide-chrome') || 'false') === true; } catch {}
|
|
218
|
-
started = await startScrape(action, name, { ...(id ? { id } : {}), ...(hidden ? { headless: true } : {}), ...extra });
|
|
230
|
+
// startScrape adds "Hide the Chrome window" itself (scanRequest), however the scan was started.
|
|
231
|
+
started = await startScrape(action, name, { ...(id ? { id } : {}), ...extra });
|
|
219
232
|
} catch (err) {
|
|
220
233
|
// Refused, often because something else runs: ask what, straight away.
|
|
221
234
|
// Until the answer comes, only our own claim is taken back.
|
|
@@ -141,11 +141,11 @@ export function tierMoves(rows, tierBefore, tierAfter, { examples = 5, companyOf
|
|
|
141
141
|
*/
|
|
142
142
|
export function previewSectorFocus(rows = [], {
|
|
143
143
|
overrides = new Map(), industryOf, sectorsOf = sectorMatcher(), groupOf = sectorGroup, from = NO_FOCUS, to = NO_FOCUS, examples = 5,
|
|
144
|
-
tierScale, read = readNetwork(rows, { industryOf, sectorsOf, groupOf }),
|
|
144
|
+
tierScale, titles = null, read = readNetwork(rows, { industryOf, sectorsOf, groupOf }),
|
|
145
145
|
} = {}) {
|
|
146
|
-
// Both on the tier scale saved now (Scores → Tiers), as rescoring grades them.
|
|
147
|
-
const before = scoreNetwork(rows, { overrides, focus: from, tierScale, read });
|
|
148
|
-
const after = scoreNetwork(rows, { overrides, focus: to, tierScale, read });
|
|
146
|
+
// Both on the tier scale and title ranking saved now (Scores → Tiers, Titles), as rescoring grades them.
|
|
147
|
+
const before = scoreNetwork(rows, { overrides, focus: from, tierScale, titles, read });
|
|
148
|
+
const after = scoreNetwork(rows, { overrides, focus: to, tierScale, titles, read });
|
|
149
149
|
|
|
150
150
|
// Companies whose score moves, biggest first (by your people there now).
|
|
151
151
|
// Every company anyone names counts, a former employer too: its score moves
|
|
@@ -86,11 +86,6 @@ export function comparePower(a, b) {
|
|
|
86
86
|
}
|
|
87
87
|
const NO_ROUTE = { id: null, bridge: null };
|
|
88
88
|
|
|
89
|
-
/** "Ways in" order: most ways in first, then the power order. The rank shown stays the power rank. */
|
|
90
|
-
export function compareWaysIn(a, b) {
|
|
91
|
-
return b.waysIn - a.waysIn || comparePower(a, b);
|
|
92
|
-
}
|
|
93
|
-
|
|
94
89
|
/**
|
|
95
90
|
* @param {object[]} degree2 2nd-degree rows (one per bridge per person)
|
|
96
91
|
* @param {object[]} connections 1st-degree rows, the bridges
|
|
@@ -236,6 +231,10 @@ export function dotRadius(waysIn) {
|
|
|
236
231
|
|
|
237
232
|
export const MIN_BRIDGE_GAP = 18;
|
|
238
233
|
const TOP_PAD = 16;
|
|
234
|
+
/** Grey dots for mutual connections in circles not scanned yet, at most this many drawn. */
|
|
235
|
+
export const GHOST_MAX = 60;
|
|
236
|
+
const GHOST_LABEL = 40; // room under the last connection for the line that counts them
|
|
237
|
+
const PILL_W_LAYOUT = 236; // SeparationView's PILL_W: the connection pills' width as cards
|
|
239
238
|
|
|
240
239
|
/**
|
|
241
240
|
* The summit map: You → the bridges → the top of the list, every route drawn.
|
|
@@ -249,10 +248,13 @@ const TOP_PAD = 16;
|
|
|
249
248
|
*
|
|
250
249
|
* @param {object[]} top the first K people of whatever the list shows
|
|
251
250
|
* @param {number} width map width in px
|
|
251
|
+
* @param {{ cards?: boolean }} opts cards: people drawn as cards and
|
|
252
|
+
* connections as pills (Blake, 2026-10-02), which need more room per row
|
|
252
253
|
* @returns {{ width, height, you, people, bridges, links, spokes }}
|
|
253
254
|
*/
|
|
254
|
-
export function summitLayout(top = [], width = 800, isMobile = false) {
|
|
255
|
-
const gap = isMobile ? 36 : 26;
|
|
255
|
+
export function summitLayout(top = [], width = 800, isMobile = false, { cards = false } = {}) {
|
|
256
|
+
const gap = cards ? 50 : isMobile ? 36 : 26;
|
|
257
|
+
const bridgeGap = cards ? 36 : MIN_BRIDGE_GAP;
|
|
256
258
|
const youX = 20;
|
|
257
259
|
const bx = Math.round(width * 0.42);
|
|
258
260
|
const px = Math.round(width * 0.58);
|
|
@@ -262,7 +264,7 @@ export function summitLayout(top = [], width = 800, isMobile = false) {
|
|
|
262
264
|
p,
|
|
263
265
|
x: px,
|
|
264
266
|
y: TOP_PAD + i * gap,
|
|
265
|
-
r: dotRadius(p.waysIn),
|
|
267
|
+
r: cards ? 0 : dotRadius(p.waysIn),
|
|
266
268
|
label: fit(`#${p.rank}${p.tied ? '=' : ''} ${p.person.name || ''}`, Math.floor((width - px - 60) / 6.2)),
|
|
267
269
|
}));
|
|
268
270
|
|
|
@@ -288,11 +290,16 @@ export function summitLayout(top = [], width = 800, isMobile = false) {
|
|
|
288
290
|
|
|
289
291
|
const y0 = TOP_PAD;
|
|
290
292
|
const y1 = people.length ? people[people.length - 1].y : TOP_PAD;
|
|
291
|
-
|
|
293
|
+
// Each connection sits level with the people they lead to (the middle of
|
|
294
|
+
// them), moved no more than it takes to keep `bridgeGap` from the next one,
|
|
295
|
+
// so a connection with one person sits on that person's row and the line
|
|
296
|
+
// between them runs straight (Blake, 2026-10-02: "the lines should line up
|
|
297
|
+
// with every box").
|
|
298
|
+
const ys = stackColumn(bridges.map((b) => b.bary), bridgeGap, y0);
|
|
292
299
|
const nameBudget = Math.max(4, Math.floor((bx - 10 - (youX + 22)) / 6));
|
|
293
300
|
bridges.forEach((b, j) => {
|
|
294
301
|
b.x = bx;
|
|
295
|
-
b.y =
|
|
302
|
+
b.y = ys[j];
|
|
296
303
|
b.r = 5;
|
|
297
304
|
if (b.unresolved) b.label = fit(`can't be named (${b.count})`, nameBudget);
|
|
298
305
|
else {
|
|
@@ -302,6 +309,15 @@ export function summitLayout(top = [], width = 800, isMobile = false) {
|
|
|
302
309
|
delete b.ys;
|
|
303
310
|
});
|
|
304
311
|
const nodeFor = new Map(bridges.map((b) => [b.unresolved ? UNRESOLVED : b.id, b]));
|
|
312
|
+
// What each connection is to the people on the map: the only door to some,
|
|
313
|
+
// the best way in to others, or only another way in.
|
|
314
|
+
for (const b of bridges) { b.onlyFor = 0; b.bestFor = 0; }
|
|
315
|
+
for (const pp of people) {
|
|
316
|
+
const first = pp.p.routes[0];
|
|
317
|
+
const bestNode = first ? nodeFor.get(first.bridge ? first.id : UNRESOLVED) : null;
|
|
318
|
+
if (bestNode) bestNode.bestFor++;
|
|
319
|
+
if (pp.p.waysIn === 1 && bestNode) bestNode.onlyFor++;
|
|
320
|
+
}
|
|
305
321
|
|
|
306
322
|
// Routes, per person: routes[0] is the top-scored bridge. Two unnamed routes
|
|
307
323
|
// for one person are one line to the one grey node.
|
|
@@ -326,3 +342,114 @@ export function summitLayout(top = [], width = 800, isMobile = false) {
|
|
|
326
342
|
const spokes = bridges.map((b) => ({ id: b.id, x1: you.x, y1: you.y, x2: b.x, y2: b.y }));
|
|
327
343
|
return { width, height, you, people, bridges, links, spokes };
|
|
328
344
|
}
|
|
345
|
+
|
|
346
|
+
/**
|
|
347
|
+
* Heights as near the ones asked for as they can be while staying `gap`
|
|
348
|
+
* apart, in the order given (already sorted), never above `top`: neighbours
|
|
349
|
+
* that would overlap move apart by the same amount each way.
|
|
350
|
+
*/
|
|
351
|
+
export function stackColumn(wanted = [], gap = MIN_BRIDGE_GAP, top = 0) {
|
|
352
|
+
const blocks = [];
|
|
353
|
+
wanted.forEach((y, k) => {
|
|
354
|
+
blocks.push({ sum: y - k * gap, count: 1 });
|
|
355
|
+
while (blocks.length > 1 && blocks[blocks.length - 2].sum / blocks[blocks.length - 2].count > blocks[blocks.length - 1].sum / blocks[blocks.length - 1].count) {
|
|
356
|
+
const last = blocks.pop();
|
|
357
|
+
blocks[blocks.length - 1].sum += last.sum;
|
|
358
|
+
blocks[blocks.length - 1].count += last.count;
|
|
359
|
+
}
|
|
360
|
+
});
|
|
361
|
+
const out = blocks.flatMap((b) => new Array(b.count).fill(b.sum / b.count)).map((q, k) => q + k * gap);
|
|
362
|
+
const lift = out.length && out[0] < top ? top - out[0] : 0;
|
|
363
|
+
return out.map((y) => y + lift);
|
|
364
|
+
}
|
|
365
|
+
|
|
366
|
+
/**
|
|
367
|
+
* Who the map aims at with the slider all the way to easy: whoever you share the
|
|
368
|
+
* most mutual connections with, scanned or not (Blake, 2026-10-02: "if we have
|
|
369
|
+
* mutuals but not scanned thats still a high probability … i dont want to hinder
|
|
370
|
+
* the algorithm since we havent scanned that person"). `mutualsOf` is LinkedIn's
|
|
371
|
+
* count where a scan saved one, never less than the ways in the app has drawn
|
|
372
|
+
* (lib/rarity.js). Then the most drawn, then the stronger person. Narrowing the
|
|
373
|
+
* tiers first (S only, say) keeps it to the people worth it.
|
|
374
|
+
*/
|
|
375
|
+
export function mostMutuals(people = [], mutualsOf = () => 0, limit = 7) {
|
|
376
|
+
return [...people].sort((a, b) => (Math.max(mutualsOf(b) || 0, b.waysIn) - Math.max(mutualsOf(a) || 0, a.waysIn))
|
|
377
|
+
|| b.waysIn - a.waysIn
|
|
378
|
+
|| comparePower(a, b)).slice(0, limit);
|
|
379
|
+
}
|
|
380
|
+
|
|
381
|
+
/**
|
|
382
|
+
* How many people the map draws with the slider at `at` (lib/rarity.js): all
|
|
383
|
+
* `k` from the rare end to the middle, then fewer and fewer, down to the one
|
|
384
|
+
* person you're aiming at with the slider all the way to easy. Blake,
|
|
385
|
+
* 2026-10-02: at the rare end "it will show that one door"; at the other "all
|
|
386
|
+
* the connections going into that one person".
|
|
387
|
+
*/
|
|
388
|
+
export function mapCount(at, k = 10) {
|
|
389
|
+
const v = Math.max(0, Math.min(100, Number(at) || 0));
|
|
390
|
+
if (v <= 50) return k;
|
|
391
|
+
return Math.max(1, Math.round((k * (100 - v)) / 50));
|
|
392
|
+
}
|
|
393
|
+
|
|
394
|
+
/**
|
|
395
|
+
* The map for one person: You → every connection of yours who leads to them →
|
|
396
|
+
* them, with rings round them. Routes the map can't name share one grey node,
|
|
397
|
+
* as in summitLayout, and it comes back in summitLayout's shape (`people` holds
|
|
398
|
+
* the one person, marked `big`), so one map draws both and can glide from one
|
|
399
|
+
* to the other. Pure, so it is tested without a browser.
|
|
400
|
+
*
|
|
401
|
+
* @returns {{ width, height, you, target, people, bridges, links, spokes }}
|
|
402
|
+
*/
|
|
403
|
+
export function convergeLayout(p, width = 800, isMobile = false, { cards = false, mutuals = 0 } = {}) {
|
|
404
|
+
const gap = cards ? 46 : isMobile ? 40 : 34;
|
|
405
|
+
const youX = 20;
|
|
406
|
+
const bx = Math.round(width * (isMobile ? 0.44 : 0.36));
|
|
407
|
+
const tx = Math.round(width * (isMobile ? 0.8 : cards ? 0.6 : 0.62));
|
|
408
|
+
const nodes = [];
|
|
409
|
+
let unnamed = null;
|
|
410
|
+
for (const r of p?.routes || []) {
|
|
411
|
+
if (r.bridge) nodes.push({ id: r.id, bridge: r.bridge, unresolved: false, count: 1 });
|
|
412
|
+
else if (unnamed) unnamed.count++;
|
|
413
|
+
else nodes.push((unnamed = { id: null, bridge: null, unresolved: true, count: 1 }));
|
|
414
|
+
}
|
|
415
|
+
// Mutual connections the app can't draw yet: they're in circles you haven't
|
|
416
|
+
// scanned. Grey dots under the connections who do lead there, each with a faint
|
|
417
|
+
// line in, so the count reads at a glance (GHOST_MAX at most, then "+N").
|
|
418
|
+
const unscanned = Math.max(0, Math.floor(Number(mutuals) || 0) - (p?.waysIn || 0));
|
|
419
|
+
const ghostCount = Math.min(GHOST_MAX, unscanned);
|
|
420
|
+
const ghostStep = cards ? 12 : 11;
|
|
421
|
+
const ghostW = cards ? PILL_W_LAYOUT - 16 : isMobile ? 96 : 120;
|
|
422
|
+
const perRow = Math.max(1, Math.floor(ghostW / ghostStep) + 1);
|
|
423
|
+
const ghostRows = Math.ceil(ghostCount / perRow);
|
|
424
|
+
const ghostH = ghostCount ? GHOST_LABEL + ghostRows * ghostStep : 0;
|
|
425
|
+
const block = Math.max(0, nodes.length - 1) * gap + ghostH;
|
|
426
|
+
const height = Math.max(isMobile ? 190 : cards ? 250 : 220, TOP_PAD * 2 + block + 48);
|
|
427
|
+
const mid = height / 2;
|
|
428
|
+
const top = mid - block / 2;
|
|
429
|
+
const nameBudget = Math.max(4, Math.floor((bx - 10 - (youX + 22)) / 6));
|
|
430
|
+
const bridges = nodes.map((n, j) => {
|
|
431
|
+
const full = n.unresolved ? `can't be named (${n.count})` : n.bridge.name || '';
|
|
432
|
+
return {
|
|
433
|
+
...n, x: bx, y: top + j * gap, r: 6,
|
|
434
|
+
primary: j === 0 && !n.unresolved,
|
|
435
|
+
label: full.length <= nameBudget ? full : fit(n.unresolved ? full : shortName(full), nameBudget),
|
|
436
|
+
};
|
|
437
|
+
});
|
|
438
|
+
const target = { x: tx, y: mid, r: cards ? 0 : 17, rings: cards ? [] : [32, 50, 70].filter((r) => r <= mid - 4) };
|
|
439
|
+
const links = bridges.map((b) => ({
|
|
440
|
+
key: p?.key, bridgeId: b.id, primary: b.primary, unresolved: b.unresolved, tier: b.bridge?.tier ?? null,
|
|
441
|
+
x1: b.x, y1: b.y, x2: target.x, y2: target.y,
|
|
442
|
+
}));
|
|
443
|
+
const you = { x: youX, y: mid, r: 14 };
|
|
444
|
+
const spokes = bridges.map((b) => ({ id: b.id, x1: you.x, y1: you.y, x2: b.x, y2: b.y }));
|
|
445
|
+
const people = p ? [{ key: p.key, p, x: target.x, y: target.y, r: target.r, rings: target.rings, big: true, label: '' }] : [];
|
|
446
|
+
const gy = top + Math.max(0, nodes.length - 1) * gap + GHOST_LABEL;
|
|
447
|
+
const gx = cards ? bx - PILL_W_LAYOUT + 8 : bx - ghostW;
|
|
448
|
+
const ghosts = Array.from({ length: ghostCount }, (_, i) => ({ x: gx + (i % perRow) * ghostStep, y: gy + Math.floor(i / perRow) * ghostStep }));
|
|
449
|
+
// How bright the person is: the share of your mutual connections with them that the map can draw.
|
|
450
|
+
const drawn = p?.waysIn ? p.waysIn / (p.waysIn + unscanned) : 1;
|
|
451
|
+
return {
|
|
452
|
+
width, height, you, target, people, bridges, links, spokes,
|
|
453
|
+
ghosts, unscanned, ghostLabel: ghostCount ? { x: gx, y: gy - 13 } : null, drawn,
|
|
454
|
+
};
|
|
455
|
+
}
|