six-degrees 0.3.0 → 0.4.0-beta.1
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 +34 -6
- package/.next/standalone/.next/routes-manifest.json +6 -0
- 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 +10 -10
- package/.next/standalone/.next/server/app/_not-found.segments/_full.segment.rsc +10 -10
- package/.next/standalone/.next/server/app/_not-found.segments/_not-found/__PAGE__.segment.rsc +9 -9
- package/.next/standalone/.next/server/app/_not-found.segments/_tree.segment.rsc +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.nft.json +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.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.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.nft.json +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.nft.json +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.nft.json +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.nft.json +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.nft.json +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 +12 -12
- package/.next/standalone/.next/server/app/import.segments/_full.segment.rsc +12 -12
- package/.next/standalone/.next/server/app/import.segments/_tree.segment.rsc +1 -1
- package/.next/standalone/.next/server/app/import.segments/import/__PAGE__.segment.rsc +11 -11
- package/.next/standalone/.next/server/app/index.html +1 -1
- package/.next/standalone/.next/server/app/index.rsc +12 -12
- package/.next/standalone/.next/server/app/index.segments/__PAGE__.segment.rsc +11 -11
- package/.next/standalone/.next/server/app/index.segments/_full.segment.rsc +12 -12
- package/.next/standalone/.next/server/app/index.segments/_tree.segment.rsc +1 -1
- 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 +12 -12
- package/.next/standalone/.next/server/app/launch.segments/_full.segment.rsc +12 -12
- package/.next/standalone/.next/server/app/launch.segments/_tree.segment.rsc +1 -1
- package/.next/standalone/.next/server/app/launch.segments/launch/__PAGE__.segment.rsc +11 -11
- 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 +12 -12
- package/.next/standalone/.next/server/app/paths.segments/_full.segment.rsc +12 -12
- package/.next/standalone/.next/server/app/paths.segments/_tree.segment.rsc +1 -1
- package/.next/standalone/.next/server/app/paths.segments/paths/__PAGE__.segment.rsc +11 -11
- 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 +12 -12
- package/.next/standalone/.next/server/app/profile.segments/_full.segment.rsc +12 -12
- package/.next/standalone/.next/server/app/profile.segments/_tree.segment.rsc +1 -1
- package/.next/standalone/.next/server/app/profile.segments/profile/__PAGE__.segment.rsc +11 -11
- 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 +12 -12
- package/.next/standalone/.next/server/app/queue.segments/_full.segment.rsc +12 -12
- package/.next/standalone/.next/server/app/queue.segments/_tree.segment.rsc +1 -1
- package/.next/standalone/.next/server/app/queue.segments/queue/__PAGE__.segment.rsc +11 -11
- 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 +12 -12
- package/.next/standalone/.next/server/app/settings.segments/_full.segment.rsc +12 -12
- package/.next/standalone/.next/server/app/settings.segments/_tree.segment.rsc +1 -1
- package/.next/standalone/.next/server/app/settings.segments/settings/__PAGE__.segment.rsc +11 -11
- 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 +12 -12
- package/.next/standalone/.next/server/app/setup.segments/_full.segment.rsc +12 -12
- package/.next/standalone/.next/server/app/setup.segments/_tree.segment.rsc +1 -1
- package/.next/standalone/.next/server/app/setup.segments/setup/__PAGE__.segment.rsc +11 -11
- package/.next/standalone/.next/server/app-paths-manifest.json +1 -0
- package/.next/standalone/.next/server/chunks/[root-of-the-server]__0rc27za._.js +8 -0
- package/.next/standalone/.next/server/chunks/[root-of-the-server]__1kbl21f._.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 +7 -6
- 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 +3 -2
- package/.next/standalone/.next/server/chunks/lib_rpc_0c9d9ti.js +5 -5
- 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/_0223v9s._.js +3 -0
- package/.next/standalone/.next/server/chunks/ssr/_0bcf13g._.js +3 -0
- package/.next/standalone/.next/server/chunks/ssr/_0cu6-an._.js +3 -0
- package/.next/standalone/.next/server/chunks/ssr/_0wxbq78._.js +3 -0
- package/.next/standalone/.next/server/chunks/ssr/_0z88zcl._.js +3 -0
- package/.next/standalone/.next/server/chunks/ssr/_17gui6a._.js +1 -1
- 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/_next-internal_server_app_scores_page_actions_0x-w58n.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 +4 -4
- 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/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/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/0_yurn4q91vq8.js +16 -0
- package/.next/standalone/.next/static/chunks/0d_mh-06srh2k.js +1 -0
- package/.next/standalone/.next/static/chunks/0x3oadfikvb1c.js +1 -0
- package/.next/standalone/.next/static/chunks/1d5hl1io798n2.js +1 -0
- package/.next/standalone/.next/static/chunks/1qt67cvj9sjwi.js +1 -0
- package/.next/{static/chunks/1chp3nx45meg5.js → standalone/.next/static/chunks/1roznk7hkjes1.js} +1 -1
- package/.next/standalone/.next/static/chunks/2_bf2n4gstad9.js +33 -0
- package/.next/standalone/.next/static/chunks/36gbwek52x_w-.js +2 -0
- package/.next/standalone/.next/static/chunks/3a55suosthntc.js +1 -0
- package/.next/standalone/.next/static/chunks/3fq1zq-6cfwbc.js +1 -0
- package/.next/standalone/.next/static/chunks/3nbnio971zlji.js +2 -0
- package/.next/standalone/.next/static/chunks/3s6awp3qq9s7w.js +1 -0
- package/.next/standalone/CHANGELOG.md +134 -0
- package/.next/standalone/README.md +30 -15
- 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 +17 -1
- package/.next/standalone/app/api/outreach/route.js +107 -44
- package/.next/standalone/app/api/scraper/route.js +69 -8
- 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/components/CompanyScores.js +16 -10
- package/.next/standalone/app/components/LegacyScoresCard.js +1 -1
- package/.next/standalone/app/components/OrbitGraph.js +30 -0
- package/.next/standalone/app/components/PathsAnalyzer.js +1 -1
- package/.next/standalone/app/components/SeparationView.js +96 -14
- package/.next/standalone/app/components/Sidebar.js +258 -239
- package/.next/standalone/app/components/TheirCircle.js +293 -0
- package/.next/standalone/app/components/settings/SectorSection.js +5 -5
- package/.next/standalone/app/components/settings/TierSection.js +104 -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 +1 -3
- package/.next/standalone/app/launch/page.js +4 -1
- package/.next/standalone/app/page.js +55 -51
- package/.next/standalone/app/paths/page.js +17 -12
- package/.next/standalone/app/profile/page.js +3 -3
- package/.next/standalone/app/queue/page.js +38 -50
- package/.next/standalone/app/scores/page.js +72 -0
- package/.next/standalone/app/settings/page.js +15 -3
- package/.next/standalone/db/schema.js +1 -0
- package/.next/standalone/lib/circle.js +169 -0
- package/.next/standalone/lib/companies.js +1 -1
- package/.next/standalone/lib/csv.js +17 -4
- package/.next/standalone/lib/db-client.js +1 -0
- package/.next/standalone/lib/ingest.js +12 -0
- package/.next/standalone/lib/legacy-offer.js +7 -6
- package/.next/standalone/lib/legacy-scores.js +2 -2
- package/.next/standalone/lib/rarity.js +80 -0
- package/.next/standalone/lib/requests-client.js +113 -0
- package/.next/standalone/lib/rpc.js +26 -15
- package/.next/standalone/lib/score-guess.js +60 -0
- package/.next/standalone/lib/scoring.js +1002 -61
- package/.next/standalone/lib/scraper-client.js +208 -17
- package/.next/standalone/lib/sector-directory.js +1 -1
- package/.next/standalone/lib/sector-focus.js +4 -3
- package/.next/standalone/lib/settings-effects.js +19 -1
- package/.next/standalone/lib/settings.js +4 -1
- 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/gen-synthetic.mjs +1 -1
- package/.next/standalone/scripts/scrape.py +62 -0
- package/.next/static/chunks/0_yurn4q91vq8.js +16 -0
- package/.next/static/chunks/0d_mh-06srh2k.js +1 -0
- package/.next/static/chunks/0x3oadfikvb1c.js +1 -0
- package/.next/static/chunks/1d5hl1io798n2.js +1 -0
- package/.next/static/chunks/1qt67cvj9sjwi.js +1 -0
- package/.next/{standalone/.next/static/chunks/1chp3nx45meg5.js → static/chunks/1roznk7hkjes1.js} +1 -1
- package/.next/static/chunks/2_bf2n4gstad9.js +33 -0
- package/.next/static/chunks/36gbwek52x_w-.js +2 -0
- package/.next/static/chunks/3a55suosthntc.js +1 -0
- package/.next/static/chunks/3fq1zq-6cfwbc.js +1 -0
- package/.next/static/chunks/3nbnio971zlji.js +2 -0
- package/.next/static/chunks/3s6awp3qq9s7w.js +1 -0
- package/CHANGELOG.md +134 -0
- package/README.md +30 -15
- package/db/schema.js +1 -0
- package/lib/circle.js +169 -0
- package/lib/companies.js +1 -1
- package/lib/csv.js +17 -4
- package/lib/db-client.js +1 -0
- package/lib/ingest.js +12 -0
- package/lib/legacy-offer.js +7 -6
- package/lib/legacy-scores.js +2 -2
- package/lib/rarity.js +80 -0
- package/lib/requests-client.js +113 -0
- package/lib/rpc.js +26 -15
- package/lib/score-guess.js +60 -0
- package/lib/scoring.js +1002 -61
- package/lib/scraper-client.js +208 -17
- package/lib/sector-directory.js +1 -1
- package/lib/sector-focus.js +4 -3
- package/lib/settings-effects.js +19 -1
- package/lib/settings.js +4 -1
- package/package.json +2 -2
- package/public/demo-data.json +1 -1
- package/scripts/scrape.py +62 -0
- package/.next/standalone/.next/server/chunks/[root-of-the-server]__1t3rugi._.js +0 -3
- package/.next/standalone/.next/server/chunks/lib_rpc_0jzisgh.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/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/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/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 → NnWlxbDF24KhrU1eHiEnv}/_buildManifest.js +0 -0
- /package/.next/standalone/.next/static/{qqqcxvzBUizkECrT4swyK → NnWlxbDF24KhrU1eHiEnv}/_clientMiddlewareManifest.js +0 -0
- /package/.next/standalone/.next/static/{qqqcxvzBUizkECrT4swyK → NnWlxbDF24KhrU1eHiEnv}/_ssgManifest.js +0 -0
- /package/.next/static/{qqqcxvzBUizkECrT4swyK → NnWlxbDF24KhrU1eHiEnv}/_buildManifest.js +0 -0
- /package/.next/static/{qqqcxvzBUizkECrT4swyK → NnWlxbDF24KhrU1eHiEnv}/_clientMiddlewareManifest.js +0 -0
- /package/.next/static/{qqqcxvzBUizkECrT4swyK → NnWlxbDF24KhrU1eHiEnv}/_ssgManifest.js +0 -0
|
@@ -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
|
+
}
|
|
@@ -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 = []) {
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { scorePerson } from './scoring.js';
|
|
1
|
+
import { scorePerson, curveCutoffs, curvedTier } from './scoring.js';
|
|
2
2
|
// LinkedIn Connections.csv import — parsed ENTIRELY in the browser.
|
|
3
3
|
//
|
|
4
4
|
// Nothing is uploaded and nothing is written to the database: an imported
|
|
@@ -49,6 +49,20 @@ export function scoreRecord(headline, company) {
|
|
|
49
49
|
return { seniority_score: s.title.points, company_prestige_score: s.companyScore, power_score: s.power, tier: s.tier };
|
|
50
50
|
}
|
|
51
51
|
|
|
52
|
+
/**
|
|
53
|
+
* Tiers for a whole import, graded on its own curve (lib/scoring.js CURVE):
|
|
54
|
+
* a CSV import uses the app's defaults, never your settings (no company
|
|
55
|
+
* scores, no sector), and the curve is the default, so the first look at any
|
|
56
|
+
* export has a top. Scores → Tiers says so while one is open. Run on the
|
|
57
|
+
* parse (the import page's counts) and on every load (the tab keeps only
|
|
58
|
+
* what the export says, packConnections), so the two always agree.
|
|
59
|
+
*/
|
|
60
|
+
function gradeOnCurve(connections) {
|
|
61
|
+
const cut = curveCutoffs(connections.map((c) => c.power_score));
|
|
62
|
+
for (const c of connections) c.tier = curvedTier(c.power_score, cut);
|
|
63
|
+
return connections;
|
|
64
|
+
}
|
|
65
|
+
|
|
52
66
|
// Stable id from the profile URL so re-importing keeps node identity.
|
|
53
67
|
function hashId(input) {
|
|
54
68
|
let h = 5381;
|
|
@@ -138,7 +152,7 @@ export function parseConnectionsCsv(text) {
|
|
|
138
152
|
);
|
|
139
153
|
}
|
|
140
154
|
|
|
141
|
-
connections.sort((a, b) => b.power_score - a.power_score);
|
|
155
|
+
gradeOnCurve(connections).sort((a, b) => b.power_score - a.power_score);
|
|
142
156
|
return { connections, skipped, total: connections.length + skipped };
|
|
143
157
|
}
|
|
144
158
|
|
|
@@ -161,8 +175,7 @@ export function packConnections(connections) {
|
|
|
161
175
|
}
|
|
162
176
|
|
|
163
177
|
export function unpackConnections(rows = []) {
|
|
164
|
-
return rows
|
|
165
|
-
.map(([id, name, role, company, profileUrl, connected_date]) => connectionFrom({ id, name, role, company, profileUrl, connected_date }))
|
|
178
|
+
return gradeOnCurve(rows.map(([id, name, role, company, profileUrl, connected_date]) => connectionFrom({ id, name, role, company, profileUrl, connected_date })))
|
|
166
179
|
.sort((a, b) => b.power_score - a.power_score);
|
|
167
180
|
}
|
|
168
181
|
|
|
@@ -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) {
|
|
@@ -100,3 +100,15 @@ export function refreshNotifications({ added = [], checked = 0, tierOf = () => n
|
|
|
100
100
|
}
|
|
101
101
|
return out;
|
|
102
102
|
}
|
|
103
|
+
|
|
104
|
+
/**
|
|
105
|
+
* LinkedIn's own count of the mutual connections you share with someone, as
|
|
106
|
+
* the scanner read it off their result card (scripts/scrape.py
|
|
107
|
+
* BRIDGE_RESULTS_JS), or null. A whole number from 1 (the bridge is always
|
|
108
|
+
* one) to 30,000 (LinkedIn's cap on connections); anything else is a misread
|
|
109
|
+
* and is dropped, never stored.
|
|
110
|
+
*/
|
|
111
|
+
export function mutualCountOf(value) {
|
|
112
|
+
const n = typeof value === 'string' && /^\d[\d,]*$/.test(value.trim()) ? Number(value.replace(/,/g, '')) : value;
|
|
113
|
+
return Number.isInteger(n) && n >= 1 && n <= 30000 ? n : null;
|
|
114
|
+
}
|
|
@@ -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
|
};
|
|
@@ -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'],
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
// How rare the way in to someone is: a distinction, never a score.
|
|
2
|
+
//
|
|
3
|
+
// Blake, 2026-09-28: "if i have like 20 or 30 mututals with someone … they are
|
|
4
|
+
// most likely to add me but that means they are not that hard to reach … no
|
|
5
|
+
// mutuals at all besides the person im adding from then they must be a rarer
|
|
6
|
+
// connection". Mutual connections say how much your world and theirs overlap.
|
|
7
|
+
// Many means they'll probably accept; few means the path to them is rare and
|
|
8
|
+
// the bridge who knows them is worth more. Neither says anything about who they
|
|
9
|
+
// are, so rarity never enters the power score or the tier. It is shown and
|
|
10
|
+
// filtered beside them: "S and only one way in" is a rare find, "S and warm"
|
|
11
|
+
// an easy win.
|
|
12
|
+
//
|
|
13
|
+
// The count is LinkedIn's own mutual count where a scan saved one
|
|
14
|
+
// (mutual_count), else the ways in the app has seen: how many of your scanned
|
|
15
|
+
// connections know them. That second one is only a floor. With 14 of 150
|
|
16
|
+
// circles scanned, a "1" may really be 20, and it can only go up as you scan
|
|
17
|
+
// more, so everything that shows it says where it came from.
|
|
18
|
+
|
|
19
|
+
export const RARITY = [
|
|
20
|
+
{ key: 'only', label: 'Only way in', range: '1', max: 1, color: '#00E5FF' },
|
|
21
|
+
{ key: 'rare', label: 'Rare', range: '2–3', max: 3, color: '#4FC3F7' },
|
|
22
|
+
{ key: 'uncommon', label: 'Uncommon', range: '4–10', max: 10, color: '#B0BEC5' },
|
|
23
|
+
{ key: 'common', label: 'Common', range: '11–30', max: 30, color: '#FFB74D' },
|
|
24
|
+
{ key: 'warm', label: 'Warm', range: '31+', max: Infinity, color: '#FF7043' },
|
|
25
|
+
];
|
|
26
|
+
|
|
27
|
+
const BY_KEY = new Map(RARITY.map((r) => [r.key, r]));
|
|
28
|
+
|
|
29
|
+
/** The band for a number of mutual connections (the bridge counts: at least 1). */
|
|
30
|
+
export function rarityFor(count) {
|
|
31
|
+
const n = Math.max(1, Math.floor(Number(count) || 1));
|
|
32
|
+
return RARITY.find((r) => n <= r.max).key;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
export function rarityInfo(key) {
|
|
36
|
+
return BY_KEY.get(key) ?? null;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* How many mutual connections you share with someone, and where that number
|
|
41
|
+
* came from: { count, from: 'linkedin' | 'scans' }. `waysIn` is how many of
|
|
42
|
+
* your scanned connections know them (lib/separation.js).
|
|
43
|
+
*/
|
|
44
|
+
export function mutualsOf(row, waysIn = 1) {
|
|
45
|
+
const saved = Number(row?.mutual_count);
|
|
46
|
+
if (Number.isFinite(saved) && saved >= 1) return { count: Math.floor(saved), from: 'linkedin' };
|
|
47
|
+
return { count: Math.max(1, Math.floor(Number(waysIn) || 1)), from: 'scans' };
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
/** Someone's rarity: { key, count, from }. */
|
|
51
|
+
export function rarityOf(row, waysIn = 1) {
|
|
52
|
+
const m = mutualsOf(row, waysIn);
|
|
53
|
+
return { key: rarityFor(m.count), ...m };
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
/**
|
|
57
|
+
* Does a person pass both filters? Each filter is a Set of what's switched on;
|
|
58
|
+
* an empty set lets everyone through. Tier and rarity must BOTH match, so
|
|
59
|
+
* "S" + "Only way in" is exactly the rare finds.
|
|
60
|
+
*/
|
|
61
|
+
export function passes({ tier, rarity }, { tiers = new Set(), rarities = new Set() } = {}) {
|
|
62
|
+
if (tiers.size && !tiers.has(tier)) return false;
|
|
63
|
+
if (rarities.size && !(rarity && rarities.has(rarity))) return false;
|
|
64
|
+
return true;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
/** People per band, for the chips' counts. `items` carry `rarity` (a key). */
|
|
68
|
+
export function countByRarity(items = []) {
|
|
69
|
+
const out = Object.fromEntries(RARITY.map((r) => [r.key, 0]));
|
|
70
|
+
for (const it of items) if (it?.rarity && out[it.rarity] !== undefined) out[it.rarity]++;
|
|
71
|
+
return out;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
/** Turn one key on or off in a filter set, as a new Set (React state). */
|
|
75
|
+
export function toggle(set, key) {
|
|
76
|
+
const next = new Set(set);
|
|
77
|
+
if (next.has(key)) next.delete(key);
|
|
78
|
+
else next.add(key);
|
|
79
|
+
return next;
|
|
80
|
+
}
|
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
// A request you've sent, everywhere at once.
|
|
2
|
+
//
|
|
3
|
+
// Blake, 2026-09-28: "once a person is sent our a connection request hitting
|
|
4
|
+
// that button then it should commuiniacte in the app all around". It didn't.
|
|
5
|
+
// Each place that sent one kept its own note of it: the card's link told
|
|
6
|
+
// nothing else on the page (and kept offering itself), the Outlink queue kept
|
|
7
|
+
// a list of row ids, and Separation, the circle and Orbit read nothing at all,
|
|
8
|
+
// even after a reload. Now there is one list, per person (their profile URL,
|
|
9
|
+
// so every bridge's copy of them agrees), loaded once and changed only through
|
|
10
|
+
// markRequested / undoRequest here, and every view reads it (useRequests).
|
|
11
|
+
|
|
12
|
+
import { keyFor } from './separation.js';
|
|
13
|
+
|
|
14
|
+
export const REQUESTS_UNKNOWN = Object.freeze({ known: false, sent: new Set(), mine: new Map(), version: 0 });
|
|
15
|
+
|
|
16
|
+
const listeners = new Set();
|
|
17
|
+
let now = REQUESTS_UNKNOWN;
|
|
18
|
+
let loading = null;
|
|
19
|
+
// What this page did itself, person → true (sent) / false (undone). It wins
|
|
20
|
+
// over the list as loaded, so a send made while the list was on its way stays.
|
|
21
|
+
const mine = new Map();
|
|
22
|
+
|
|
23
|
+
function publish(sent = now.sent, known = now.known) {
|
|
24
|
+
now = Object.freeze({ known, sent, mine: new Map(mine), version: now.version + 1 });
|
|
25
|
+
for (const listener of [...listeners]) listener();
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
function load() {
|
|
29
|
+
if (loading || now.known) return loading;
|
|
30
|
+
loading = fetch('/api/outreach')
|
|
31
|
+
.then((r) => (r.ok ? r.json() : null))
|
|
32
|
+
.then((d) => { if (d) publish(new Set((d.pending || []).map(keyFor)), true); })
|
|
33
|
+
.catch(() => { /* the sample network, or the app restarting: the rows' own columns stand in */ })
|
|
34
|
+
.finally(() => { loading = null; });
|
|
35
|
+
return loading;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
/** Call `listener` whenever the list changes. Returns the way to stop. */
|
|
39
|
+
export function watchRequests(listener) {
|
|
40
|
+
listeners.add(listener);
|
|
41
|
+
load();
|
|
42
|
+
return () => listeners.delete(listener);
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
export function requestsNow() {
|
|
46
|
+
return now;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/**
|
|
50
|
+
* Is a request out to this person? What this page did wins; then the list
|
|
51
|
+
* as loaded; until that arrives, the row's own columns.
|
|
52
|
+
*/
|
|
53
|
+
export function hasRequest(row, state = now) {
|
|
54
|
+
if (!row) return false;
|
|
55
|
+
const key = keyFor(row);
|
|
56
|
+
if (state.mine.has(key)) return state.mine.get(key);
|
|
57
|
+
if (state.known) return state.sent.has(key);
|
|
58
|
+
return row.unlock_status === 'pending' || row.outreach_status === 'sent';
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
/** How many people have a request out. */
|
|
62
|
+
export function requestCount(state = now) {
|
|
63
|
+
const keys = new Set(state.known ? state.sent : []);
|
|
64
|
+
for (const [key, on] of state.mine) (on ? keys.add(key) : keys.delete(key));
|
|
65
|
+
return keys.size;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
async function post(body) {
|
|
69
|
+
const r = await fetch('/api/outreach', {
|
|
70
|
+
method: 'POST',
|
|
71
|
+
headers: { 'Content-Type': 'application/json' },
|
|
72
|
+
body: JSON.stringify(body),
|
|
73
|
+
});
|
|
74
|
+
const d = await r.json().catch(() => ({}));
|
|
75
|
+
if (!r.ok) throw new Error(d.error || 'Could not save that.');
|
|
76
|
+
return d;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
/**
|
|
80
|
+
* You sent this person a request. Every view shows it at once; the save
|
|
81
|
+
* follows, and is taken back on screen if it fails. `bridgeId` is the
|
|
82
|
+
* connection you asked through, kept for when they accept.
|
|
83
|
+
*/
|
|
84
|
+
export async function markRequested(row, { bridgeId } = {}) {
|
|
85
|
+
const key = keyFor(row);
|
|
86
|
+
const before = mine.has(key) ? mine.get(key) : undefined;
|
|
87
|
+
mine.set(key, true);
|
|
88
|
+
publish();
|
|
89
|
+
try {
|
|
90
|
+
return await post({ action: 'mark-sent', connectionId: row.id, profileUrl: row.profile_url, bridgeId });
|
|
91
|
+
} catch (err) {
|
|
92
|
+
if (before === undefined) mine.delete(key);
|
|
93
|
+
else mine.set(key, before);
|
|
94
|
+
publish();
|
|
95
|
+
throw err;
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
/** Take a request back, everywhere. */
|
|
100
|
+
export async function undoRequest(row) {
|
|
101
|
+
const key = keyFor(row);
|
|
102
|
+
const before = mine.has(key) ? mine.get(key) : undefined;
|
|
103
|
+
mine.set(key, false);
|
|
104
|
+
publish();
|
|
105
|
+
try {
|
|
106
|
+
return await post({ action: 'undo', connectionId: row.id, profileUrl: row.profile_url });
|
|
107
|
+
} catch (err) {
|
|
108
|
+
if (before === undefined) mine.delete(key);
|
|
109
|
+
else mine.set(key, before);
|
|
110
|
+
publish();
|
|
111
|
+
throw err;
|
|
112
|
+
}
|
|
113
|
+
}
|
|
@@ -6,7 +6,7 @@ import { getDb, newId, nowIso } from './db-client.js';
|
|
|
6
6
|
// results back. rpc('score_new_connections') is that procedure's name, kept
|
|
7
7
|
// for the routes that call it; it rescores with lib/scoring.js like the rest.
|
|
8
8
|
|
|
9
|
-
import { readNetwork, scoreNetwork, explainScore, SCORING_VERSION, KNOWN_COMPANIES, RULE_TABLES } from './scoring.js';
|
|
9
|
+
import { readNetwork, scoreNetwork, explainScore, SCORING_VERSION, KNOWN_COMPANIES, COMPANY_DATA, RULE_TABLES, TIER_SCALE_SETTING } from './scoring.js';
|
|
10
10
|
import { industryKeyOf, INDUSTRIES } from './companies.js';
|
|
11
11
|
import { readSettings } from './settings.js';
|
|
12
12
|
import { SECTOR_FOCUS_SETTING, focusFingerprint, previewSectorFocus } from './sector-focus.js';
|
|
@@ -14,7 +14,8 @@ import { LEGACY_OFFER_KEY } from './legacy-scores.js';
|
|
|
14
14
|
import { sectorMatcher, sectorGroup, sectorLabel } from './sector-directory.js';
|
|
15
15
|
|
|
16
16
|
// The word lists and rules the stored scores were computed with: a fingerprint
|
|
17
|
-
// of every entry on the curated company list
|
|
17
|
+
// of every entry on the curated company list and the public dataset (name,
|
|
18
|
+
// score, alias, industry),
|
|
18
19
|
// of the broad industries' words and labels (lib/companies.js INDUSTRIES), and
|
|
19
20
|
// of every rule table scoring reads headlines and names with (lib/scoring.js
|
|
20
21
|
// RULE_TABLES: titles, students and clubs, what isn't a company, the reach
|
|
@@ -26,12 +27,14 @@ import { sectorMatcher, sectorGroup, sectorLabel } from './sector-directory.js';
|
|
|
26
27
|
// does, with no number to remember to bump. Copies from before the list was
|
|
27
28
|
// made neutral stamped none, which is also how rescoreAll() knows to offer
|
|
28
29
|
// keeping their old scores (lib/legacy-offer.js).
|
|
29
|
-
export function scoringStamp({ known = KNOWN_COMPANIES, industries = INDUSTRIES, rules = RULE_TABLES } = {}) {
|
|
30
|
+
export function scoringStamp({ known = KNOWN_COMPANIES, data = COMPANY_DATA, industries = INDUSTRIES, rules = RULE_TABLES } = {}) {
|
|
30
31
|
// JSON has no patterns or sets: a pattern counts by its source and flags, a set by its members.
|
|
31
32
|
const plain = (key, value) => (value instanceof RegExp ? String(value) : value instanceof Set ? [...value] : value);
|
|
32
33
|
return createHash('sha256')
|
|
33
34
|
.update(JSON.stringify([
|
|
34
35
|
known.map(([name, score, alias, industry]) => [name, score, String(alias), industry]),
|
|
36
|
+
// The public dataset too: an entry added or rescored rescores stored scores.
|
|
37
|
+
data.map(([name, score, alias, industry]) => [name, score, String(alias), industry]),
|
|
35
38
|
industries.map(({ key, label, words }) => [key, label, String(words)]),
|
|
36
39
|
rules,
|
|
37
40
|
], plain))
|
|
@@ -45,7 +48,7 @@ export function companyOverrides(db = getDb()) {
|
|
|
45
48
|
}
|
|
46
49
|
|
|
47
50
|
/**
|
|
48
|
-
* Set company scores as your own ([[name, score], …]), the way
|
|
51
|
+
* Set company scores as your own ([[name, score], …]), the way the Scores tab
|
|
49
52
|
* does: one row per name, replacing any you had. Rescoring is the caller's
|
|
50
53
|
* job, so a batch rescores once.
|
|
51
54
|
*/
|
|
@@ -61,6 +64,11 @@ export function sectorFocusOf(db = getDb()) {
|
|
|
61
64
|
return readSettings(db, { sectorFocus: SECTOR_FOCUS_SETTING }).sectorFocus;
|
|
62
65
|
}
|
|
63
66
|
|
|
67
|
+
/** Scores → Tiers: 'curve' (graded on your network's curve, the default) or 'fixed'. */
|
|
68
|
+
export function tierScaleOf(db = getDb()) {
|
|
69
|
+
return readSettings(db, { tierScale: TIER_SCALE_SETTING }).tierScale;
|
|
70
|
+
}
|
|
71
|
+
|
|
64
72
|
/**
|
|
65
73
|
* What rescoring reads: every row, as scoring needs it. `withPeople` adds each
|
|
66
74
|
* row's name, for naming who a change would move.
|
|
@@ -74,7 +82,7 @@ export function scoringRows(db = getDb(), { withPeople = false } = {}) {
|
|
|
74
82
|
* Every headline read once, the way scoring reads it: each company's
|
|
75
83
|
* headcount, its one industry, the sector directory's sectors it matches and
|
|
76
84
|
* the industries those sit under. Rescoring, Settings' preview and
|
|
77
|
-
* suggestions, and
|
|
85
|
+
* suggestions, and the Scores tab all read the rows through this, so they
|
|
78
86
|
* agree on which company is in which sector.
|
|
79
87
|
*/
|
|
80
88
|
export function readForScoring(rows) {
|
|
@@ -85,9 +93,9 @@ export function readForScoring(rows) {
|
|
|
85
93
|
* Rescore everyone. The score depends on the whole network (how many of your
|
|
86
94
|
* people work at a company, how strong a bridge's circle is), so after any
|
|
87
95
|
* ingest or company-score change every row is recomputed — ~0.1s for 4,000.
|
|
88
|
-
* Your company scores
|
|
89
|
-
* every path that rescores (an import, a company
|
|
90
|
-
* model) applies
|
|
96
|
+
* Your company scores, your sector focus and how tiers are graded are read
|
|
97
|
+
* here, not passed in, so every path that rescores (an import, a company
|
|
98
|
+
* score, Settings, a stale model) applies them all.
|
|
91
99
|
*
|
|
92
100
|
* `compareWith` is another sector focus: Settings' save passes the one it
|
|
93
101
|
* replaced. The same rows are then also scored with it, in memory, and the
|
|
@@ -102,12 +110,13 @@ export function rescoreAll({ compareWith } = {}) {
|
|
|
102
110
|
const rows = scoringRows(db);
|
|
103
111
|
const overrides = companyOverrides(db);
|
|
104
112
|
const focus = sectorFocusOf(db);
|
|
113
|
+
const tierScale = tierScaleOf(db);
|
|
105
114
|
// Every headline is read once, however many ways the rows are then scored.
|
|
106
115
|
const read = readForScoring(rows);
|
|
107
|
-
const { scores } = scoreNetwork(rows, { overrides, focus, read });
|
|
116
|
+
const { scores } = scoreNetwork(rows, { overrides, focus, tierScale, read });
|
|
108
117
|
// Worked out before anything is written, so a failure leaves nothing half done.
|
|
109
118
|
const change = compareWith === undefined ? null
|
|
110
|
-
: previewSectorFocus(rows, { overrides, read, from: compareWith, to: focus, examples: 0 });
|
|
119
|
+
: previewSectorFocus(rows, { overrides, read, from: compareWith, to: focus, tierScale, examples: 0 });
|
|
111
120
|
const update = db.prepare(`UPDATE linkedin_connections SET seniority_score = ?, company_prestige_score = ?, power_score = ?, tier = ?,
|
|
112
121
|
score_why = ?, circle_power = ?, circle_s_count = ?, circle_a_count = ?, circle_elite_pct = ?, is_catalyst = ?, catalyst_score = ?, updated_at = ?
|
|
113
122
|
WHERE id = ?`);
|
|
@@ -131,9 +140,10 @@ export function rescoreAll({ compareWith } = {}) {
|
|
|
131
140
|
}
|
|
132
141
|
const meta = db.prepare('INSERT INTO app_meta (key, value) VALUES (?, ?) ON CONFLICT(key) DO UPDATE SET value = excluded.value');
|
|
133
142
|
meta.run('scoring_version', String(SCORING_VERSION));
|
|
134
|
-
// Which curated list
|
|
143
|
+
// Which curated list, sector focus and tier scale these scores were computed with (see rescoreIfStale).
|
|
135
144
|
meta.run('scoring_list', KNOWN_LIST_STAMP);
|
|
136
145
|
meta.run('scoring_focus', focusFingerprint(focus));
|
|
146
|
+
meta.run('scoring_tiers', tierScale);
|
|
137
147
|
db.exec('COMMIT');
|
|
138
148
|
} catch (err) {
|
|
139
149
|
db.exec('ROLLBACK');
|
|
@@ -144,16 +154,17 @@ export function rescoreAll({ compareWith } = {}) {
|
|
|
144
154
|
|
|
145
155
|
/**
|
|
146
156
|
* Rescore once if the stored scores came from an older scoring model or
|
|
147
|
-
* curated list, or from a sector focus other than the one saved
|
|
148
|
-
* update, a database restored or brought from another computer, or a
|
|
149
|
-
* save whose rescore never finished.
|
|
157
|
+
* curated list, or from a sector focus or tier scale other than the one saved
|
|
158
|
+
* now: an update, a database restored or brought from another computer, or a
|
|
159
|
+
* Settings save whose rescore never finished.
|
|
150
160
|
*/
|
|
151
161
|
export function rescoreIfStale() {
|
|
152
162
|
const db = getDb();
|
|
153
163
|
const meta = (key) => db.prepare('SELECT value FROM app_meta WHERE key = ?').get(key)?.value;
|
|
154
164
|
const fresh = meta('scoring_version') === String(SCORING_VERSION)
|
|
155
165
|
&& meta('scoring_list') === KNOWN_LIST_STAMP
|
|
156
|
-
&& meta('scoring_focus') === focusFingerprint(sectorFocusOf(db))
|
|
166
|
+
&& meta('scoring_focus') === focusFingerprint(sectorFocusOf(db))
|
|
167
|
+
&& meta('scoring_tiers') === tierScaleOf(db);
|
|
157
168
|
return fresh ? { scored: 0 } : rescoreAll();
|
|
158
169
|
}
|
|
159
170
|
|