terminalhire 0.3.5 → 0.4.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -361,11 +361,11 @@ var init_graph_data = __esm({
361
361
  { id: "spark", parents: ["data-engineering"], synonyms: ["apache-spark"] },
362
362
  { id: "airflow", parents: ["data-engineering"], synonyms: ["apache-airflow"] },
363
363
  { id: "dbt", parents: ["data-engineering"] },
364
- { id: "ml", synonyms: ["machine-learning"], related: [{ to: "pytorch", w: 0.5 }, { to: "tensorflow", w: 0.5 }, { to: "scikit-learn", w: 0.5 }] },
364
+ { id: "ml", synonyms: ["machine-learning"], related: [{ to: "pytorch", w: 0.5 }, { to: "tensorflow", w: 0.5 }, { to: "scikit-learn", w: 0.5 }, { to: "data-engineering", w: 0.4 }] },
365
365
  { id: "llm", parents: ["ml"], synonyms: ["llms", "genai", "generative-ai"], related: [{ to: "langchain", w: 0.5 }, { to: "rag", w: 0.55 }, { to: "openai", w: 0.45 }, { to: "anthropic", w: 0.45 }] },
366
366
  { id: "pytorch", parents: ["ml"], synonyms: ["torch"], related: [{ to: "tensorflow", w: 0.5 }] },
367
367
  { id: "tensorflow", parents: ["ml"], synonyms: ["keras", "tf-keras"] },
368
- { id: "pandas", parents: ["python"], related: [{ to: "numpy", w: 0.6 }] },
368
+ { id: "pandas", parents: ["python"], related: [{ to: "numpy", w: 0.6 }, { to: "data-engineering", w: 0.45 }, { to: "spark", w: 0.4 }] },
369
369
  { id: "numpy", parents: ["python"] },
370
370
  { id: "scikit-learn", parents: ["ml"], synonyms: ["sklearn"] },
371
371
  { id: "jupyter", parents: ["python"] },
@@ -540,6 +540,207 @@ var init_types2 = __esm({
540
540
  }
541
541
  });
542
542
 
543
+ // ../../packages/core/src/vocab/extract.ts
544
+ function tokenize(text) {
545
+ return text.toLowerCase().replace(/[^a-z0-9.\-+#]/g, " ").split(/\s+/).filter(Boolean);
546
+ }
547
+ function looksLikeEngRole(title) {
548
+ return !NON_ENG_TITLE.test(title) && ENG_INTENT.test(title);
549
+ }
550
+ function resolveToken(token) {
551
+ const tryOne = (t) => {
552
+ if (GRAPH.ids.has(t)) return { id: t, viaSynonym: false };
553
+ const mapped = GRAPH.synonyms.get(t);
554
+ return mapped ? { id: mapped, viaSynonym: true } : null;
555
+ };
556
+ return tryOne(token) ?? tryOne(token.replace(/^[.\-+#]+|[.\-+#]+$/g, ""));
557
+ }
558
+ function extractSkillTags(title, body = "") {
559
+ if (!looksLikeEngRole(title)) return [];
560
+ const text = `${title}
561
+ ${body}`;
562
+ const tokens = tokenize(text);
563
+ const ids = /* @__PURE__ */ new Set();
564
+ const ambiguousPending = /* @__PURE__ */ new Set();
565
+ for (const tok of tokens) {
566
+ const r = resolveToken(tok);
567
+ if (!r) continue;
568
+ if (NON_EXTRACTABLE.has(r.id)) continue;
569
+ if (SYNONYM_ONLY.has(r.id) && !r.viaSynonym) continue;
570
+ const cue = AMBIGUOUS[r.id];
571
+ if (cue) {
572
+ if (cue.test(text)) ids.add(r.id);
573
+ else ambiguousPending.add(r.id);
574
+ continue;
575
+ }
576
+ ids.add(r.id);
577
+ }
578
+ const hardCount = [...ids].filter((id) => !SOFT_DOMAIN.has(id)).length;
579
+ if (hardCount >= 2) for (const id of ambiguousPending) ids.add(id);
580
+ return [...ids];
581
+ }
582
+ function coreTagsFromTitle(title) {
583
+ return extractSkillTags(title, "").filter((t) => !SOFT_DOMAIN.has(t));
584
+ }
585
+ var SOFT_DOMAIN, SYNONYM_ONLY, NON_EXTRACTABLE, AMBIGUOUS, ENG_INTENT, NON_ENG_TITLE;
586
+ var init_extract = __esm({
587
+ "../../packages/core/src/vocab/extract.ts"() {
588
+ "use strict";
589
+ init_vocab();
590
+ SOFT_DOMAIN = /* @__PURE__ */ new Set([
591
+ "frontend",
592
+ "backend",
593
+ "devops",
594
+ "security",
595
+ "payments",
596
+ "billing",
597
+ "microservices",
598
+ "caching",
599
+ "search",
600
+ "observability",
601
+ "monitoring",
602
+ "testing",
603
+ "accessibility",
604
+ "seo",
605
+ "performance",
606
+ "realtime",
607
+ "authentication",
608
+ "api-design"
609
+ ]);
610
+ SYNONYM_ONLY = /* @__PURE__ */ new Set(["performance", "security", "seo"]);
611
+ NON_EXTRACTABLE = /* @__PURE__ */ new Set(["payments", "billing"]);
612
+ for (const id of SYNONYM_ONLY) {
613
+ if (!SOFT_DOMAIN.has(id)) throw new Error(`extract: SYNONYM_ONLY "${id}" not in SOFT_DOMAIN`);
614
+ }
615
+ AMBIGUOUS = {
616
+ // Accept "go" with an ecosystem cue OR an explicit-skill phrasing ("Go developer",
617
+ // "in Go", "experience with Go"). Rejects prose: "ready to go", "go above", "go live".
618
+ go: /\b(golang|goroutines?|go\.mod|gin framework|gorm)\b|\bgo\b\s+(developer|engineer|programmer|microservices?|backend|services?|lang)|\b(in|with|using|written in|built in|experience (?:in|with)|proficient in|fluent in)\s+go\b/i,
619
+ r: /\b(rstudio|tidyverse|ggplot|shiny|dplyr|cran|r-lang|rlang)\b/i,
620
+ ml: /\b(machine[\s-]?learning|pytorch|tensorflow|scikit|sklearn|keras|neural|model training|deep[\s-]?learning|numpy|pandas|ml\s+(?:engineer|platform|researcher|infrastructure)|(?:ml|ai)\s+research)\b/i
621
+ };
622
+ ENG_INTENT = /\b(engineer|engineering|developer|dev\b|swe|sde|programmer|architect|full[\s-]?stack|front[\s-]?end|back[\s-]?end|devops|sre|software|coding|codebase|technical staff|tech(?:nical)? lead)\b/i;
623
+ NON_ENG_TITLE = /\b(account executive|account manager|sales (?:rep|representative|development|manager|lead)|sdr|bdr|recruiter|recruiting|talent|marketing|administrative|business partner|billing coordinator|operations (?:administrator|coordinator)|customer success|project finance|controller|bookkeeper|graphic|brand)\b/i;
624
+ }
625
+ });
626
+
627
+ // ../../packages/core/src/vocab/idf-background.ts
628
+ var IDF_BACKGROUND;
629
+ var init_idf_background = __esm({
630
+ "../../packages/core/src/vocab/idf-background.ts"() {
631
+ "use strict";
632
+ IDF_BACKGROUND = {
633
+ N: 244,
634
+ df: {
635
+ "backend": 71,
636
+ "python": 57,
637
+ "monitoring": 44,
638
+ "nextjs": 40,
639
+ "testing": 40,
640
+ "observability": 38,
641
+ "llm": 38,
642
+ "go": 36,
643
+ "aws": 36,
644
+ "react": 33,
645
+ "frontend": 30,
646
+ "ml": 28,
647
+ "mobile": 24,
648
+ "realtime": 24,
649
+ "typescript": 23,
650
+ "devops": 22,
651
+ "kubernetes": 22,
652
+ "javascript": 21,
653
+ "java": 20,
654
+ "rag": 20,
655
+ "api-design": 20,
656
+ "linux": 19,
657
+ "postgresql": 19,
658
+ "search": 17,
659
+ "azure": 16,
660
+ "snowflake": 15,
661
+ "spark": 15,
662
+ "kotlin": 14,
663
+ "gcp": 14,
664
+ "accessibility": 14,
665
+ "nodejs": 14,
666
+ "graphql": 14,
667
+ "airflow": 14,
668
+ "docker": 14,
669
+ "ci-cd": 13,
670
+ "android": 12,
671
+ "cpp": 12,
672
+ "gitlab-ci": 11,
673
+ "anthropic": 11,
674
+ "terraform": 11,
675
+ "mysql": 11,
676
+ "r": 10,
677
+ "dbt": 9,
678
+ "langchain": 9,
679
+ "pytorch": 9,
680
+ "ruby": 9,
681
+ "rails": 9,
682
+ "cloudflare": 7,
683
+ "datadog": 7,
684
+ "css": 7,
685
+ "ansible": 7,
686
+ "openai": 6,
687
+ "kafka": 6,
688
+ "rust": 5,
689
+ "grpc": 5,
690
+ "microservices": 5,
691
+ "serverless": 5,
692
+ "scala": 5,
693
+ "prometheus": 5,
694
+ "grafana": 5,
695
+ "php": 5,
696
+ "redis": 5,
697
+ "huggingface": 4,
698
+ "pandas": 4,
699
+ "scikit-learn": 4,
700
+ "html": 4,
701
+ "ios": 4,
702
+ "authentication": 4,
703
+ "vue": 4,
704
+ "mlops": 3,
705
+ "spring": 3,
706
+ "mongodb": 3,
707
+ "csharp": 3,
708
+ "swift": 2,
709
+ "caching": 2,
710
+ "haskell": 2,
711
+ "pulumi": 2,
712
+ "argocd": 2,
713
+ "tensorflow": 2,
714
+ "express": 2,
715
+ "elasticsearch": 2,
716
+ "clickhouse": 2,
717
+ "nestjs": 2,
718
+ "vite": 2,
719
+ "svelte": 2,
720
+ "phoenix": 2,
721
+ "angular": 2,
722
+ "django": 2,
723
+ "dotnet": 2,
724
+ "elixir": 2,
725
+ "bun": 1,
726
+ "oauth": 1,
727
+ "dynamodb": 1,
728
+ "helm": 1,
729
+ "playwright": 1,
730
+ "cypress": 1,
731
+ "jest": 1,
732
+ "mocha": 1,
733
+ "typeorm": 1,
734
+ "tailwind": 1,
735
+ "prisma": 1,
736
+ "expo": 1,
737
+ "rabbitmq": 1,
738
+ "redux": 1
739
+ }
740
+ };
741
+ }
742
+ });
743
+
543
744
  // ../../packages/core/src/vocab/index.ts
544
745
  function normalize(tokens) {
545
746
  const result = /* @__PURE__ */ new Set();
@@ -576,6 +777,8 @@ var init_vocab = __esm({
576
777
  init_types2();
577
778
  init_closure();
578
779
  init_graph_data();
780
+ init_extract();
781
+ init_idf_background();
579
782
  GRAPH = buildGraph(VOCAB_NODES);
580
783
  VOCABULARY = [...GRAPH.ids];
581
784
  SYNONYMS = Object.fromEntries(GRAPH.synonyms);
@@ -590,23 +793,250 @@ var init_vocabulary = __esm({
590
793
  }
591
794
  });
592
795
 
593
- // ../../packages/core/src/matcher.ts
594
- function computeIdf(jobs) {
595
- const docFreq = /* @__PURE__ */ new Map();
596
- const N = jobs.length;
597
- for (const job of jobs) {
598
- const unique = new Set(job.tags);
599
- for (const tag of unique) {
600
- docFreq.set(tag, (docFreq.get(tag) ?? 0) + 1);
796
+ // ../../packages/core/src/github.ts
797
+ function ghHeaders(token) {
798
+ const headers = {
799
+ Accept: "application/vnd.github+json",
800
+ "X-GitHub-Api-Version": "2022-11-28"
801
+ };
802
+ if (token) headers["Authorization"] = `Bearer ${token}`;
803
+ return headers;
804
+ }
805
+ async function ghFetch(path, token) {
806
+ const url = `https://api.github.com${path}`;
807
+ const res = await fetch(url, { headers: ghHeaders(token) });
808
+ if (!res.ok) {
809
+ throw new Error(`GitHub API ${path}: HTTP ${res.status} ${res.statusText}`);
810
+ }
811
+ return res.json();
812
+ }
813
+ async function fetchGitHubProfile(login, token) {
814
+ const user = await ghFetch(`/users/${login}`, token);
815
+ let repos = [];
816
+ try {
817
+ repos = await ghFetch(
818
+ `/users/${login}/repos?sort=pushed&per_page=100`,
819
+ token
820
+ );
821
+ } catch (err) {
822
+ console.warn(`[github] ${login}: repos fetch failed, continuing \u2014`, err);
823
+ }
824
+ const langCount = {};
825
+ for (const repo of repos) {
826
+ if (repo.fork) continue;
827
+ if (repo.language) {
828
+ langCount[repo.language.toLowerCase()] = (langCount[repo.language.toLowerCase()] ?? 0) + 1;
829
+ }
830
+ }
831
+ const topLanguages = Object.entries(langCount).sort(([, a], [, b]) => b - a).slice(0, 10).map(([lang]) => lang);
832
+ const topicSet = /* @__PURE__ */ new Set();
833
+ for (const repo of repos) {
834
+ if (repo.fork) continue;
835
+ for (const t of repo.topics ?? []) topicSet.add(t.toLowerCase());
836
+ }
837
+ const topics = Array.from(topicSet).slice(0, 30);
838
+ let recentPRorgs;
839
+ try {
840
+ const q = encodeURIComponent(
841
+ `type:pr is:merged author:${login} sort:updated`
842
+ );
843
+ const result = await ghFetch(
844
+ `/search/issues?q=${q}&per_page=30`,
845
+ token
846
+ );
847
+ const orgs = /* @__PURE__ */ new Set();
848
+ for (const item of result.items ?? []) {
849
+ const orgLogin = item.repository?.owner?.login;
850
+ if (orgLogin && orgLogin !== login) orgs.add(orgLogin);
851
+ }
852
+ if (orgs.size > 0) recentPRorgs = Array.from(orgs);
853
+ } catch {
854
+ }
855
+ return {
856
+ login: user.login,
857
+ name: user.name ?? void 0,
858
+ publicEmail: user.email ?? void 0,
859
+ avatarUrl: user.avatar_url,
860
+ accountCreatedAt: user.created_at,
861
+ publicRepos: user.public_repos,
862
+ followers: user.followers,
863
+ topLanguages,
864
+ topics,
865
+ recentPRorgs
866
+ };
867
+ }
868
+ function inferSeniority(p) {
869
+ const ageMs = Date.now() - new Date(p.accountCreatedAt).getTime();
870
+ const ageYears = ageMs / (1e3 * 60 * 60 * 24 * 365.25);
871
+ if (ageYears >= 9 && (p.publicRepos >= 40 || p.followers >= 500)) return "staff";
872
+ if (ageYears >= 5 && (p.publicRepos >= 20 || p.followers >= 100)) return "senior";
873
+ if (ageYears >= 2 && p.publicRepos >= 5) return "mid";
874
+ return "junior";
875
+ }
876
+ function githubToFingerprint(p) {
877
+ const rawTokens = [
878
+ ...p.topLanguages,
879
+ ...p.topics
880
+ // recentPRorgs intentionally excluded — org names are not skill tags
881
+ ];
882
+ const skillTags = normalize(rawTokens);
883
+ const seniorityBand = inferSeniority(p);
884
+ return { skillTags, seniorityBand };
885
+ }
886
+ async function ghFetchRaw(path, token) {
887
+ return fetch(`https://api.github.com${path}`, { headers: ghHeaders(token) });
888
+ }
889
+ function parseRepoUrl(repoUrl) {
890
+ const m = repoUrl.match(/\/repos\/([^/]+)\/([^/]+)\/?$/);
891
+ return m ? { owner: m[1], name: m[2] } : null;
892
+ }
893
+ function isTrivialPRTitle(title) {
894
+ return TRIVIAL_PR_TITLE.test(title);
895
+ }
896
+ async function fetchOwnedOrgs(token) {
897
+ try {
898
+ const memberships = await ghFetch(`/user/memberships/orgs?per_page=100`, token);
899
+ return new Set(
900
+ memberships.filter((m) => m.role === "admin").map((m) => m.organization.login.toLowerCase())
901
+ );
902
+ } catch {
903
+ return /* @__PURE__ */ new Set();
904
+ }
905
+ }
906
+ async function repoContributorCount(owner, name, token) {
907
+ try {
908
+ const res = await ghFetchRaw(
909
+ `/repos/${owner}/${name}/contributors?per_page=1&anon=false`,
910
+ token
911
+ );
912
+ if (!res.ok) return void 0;
913
+ const link = res.headers.get("link");
914
+ const m = link?.match(/[?&]page=(\d+)>;\s*rel="last"/);
915
+ if (m) return Number(m[1]);
916
+ const body = await res.json();
917
+ return Array.isArray(body) ? body.length : 0;
918
+ } catch {
919
+ return void 0;
920
+ }
921
+ }
922
+ async function fetchRepoMeta(owner, name, token, cache) {
923
+ const key = `${owner}/${name}`.toLowerCase();
924
+ const cached = cache.get(key);
925
+ if (cached !== void 0) return cached;
926
+ let meta = null;
927
+ try {
928
+ const r = await ghFetch(`/repos/${owner}/${name}`, token);
929
+ const contributors = await repoContributorCount(owner, name, token);
930
+ meta = {
931
+ stars: r.stargazers_count ?? 0,
932
+ archived: !!r.archived,
933
+ fork: !!r.fork,
934
+ language: r.language ?? null,
935
+ topics: r.topics ?? [],
936
+ contributors
937
+ };
938
+ } catch {
939
+ meta = null;
940
+ }
941
+ cache.set(key, meta);
942
+ return meta;
943
+ }
944
+ async function computeAcceptanceCredential(login, token, cache = /* @__PURE__ */ new Map()) {
945
+ const computedAt = (/* @__PURE__ */ new Date()).toISOString();
946
+ const empty = (status) => ({
947
+ status,
948
+ byDomain: {},
949
+ qualifyingTotal: 0,
950
+ computedAt
951
+ });
952
+ if (!token) return empty("no-token");
953
+ const ownedOrgs = await fetchOwnedOrgs(token);
954
+ const loginLc = login.toLowerCase();
955
+ let items;
956
+ try {
957
+ const q = encodeURIComponent(`type:pr is:merged author:${login} -user:${login} sort:updated`);
958
+ const res = await ghFetch(
959
+ `/search/issues?q=${q}&per_page=${CANDIDATE_PR_PAGE}`,
960
+ token
961
+ );
962
+ items = res.items ?? [];
963
+ } catch (err) {
964
+ const msg = String(err);
965
+ return empty(/HTTP 403|HTTP 429|rate limit/i.test(msg) ? "rate-limited" : "failed");
966
+ }
967
+ const byDomain = {};
968
+ let qualifyingTotal = 0;
969
+ for (const item of items) {
970
+ const repo = parseRepoUrl(item.repository_url);
971
+ if (!repo) continue;
972
+ const ownerLc = repo.owner.toLowerCase();
973
+ if (ownerLc === loginLc) continue;
974
+ if (ownedOrgs.has(ownerLc)) continue;
975
+ if (isTrivialPRTitle(item.title)) continue;
976
+ const meta = await fetchRepoMeta(repo.owner, repo.name, token, cache);
977
+ if (!meta) continue;
978
+ if (meta.archived || meta.fork) continue;
979
+ if (meta.stars < MIN_STARS) continue;
980
+ if (meta.contributors !== void 0 && meta.contributors < MIN_CONTRIBUTORS) continue;
981
+ qualifyingTotal += 1;
982
+ const mergedAt = item.pull_request?.merged_at ?? item.closed_at ?? item.created_at;
983
+ const rawDomains = [meta.language ?? "", ...meta.topics].filter(Boolean);
984
+ for (const d of new Set(normalize(rawDomains))) {
985
+ const b = byDomain[d] ?? (byDomain[d] = { mergedPRs: 0, distinctOrgs: 0, lastMergedAt: mergedAt, orgs: /* @__PURE__ */ new Set() });
986
+ b.mergedPRs += 1;
987
+ b.orgs.add(ownerLc);
988
+ if (mergedAt > b.lastMergedAt) b.lastMergedAt = mergedAt;
601
989
  }
602
990
  }
603
- const idf = /* @__PURE__ */ new Map();
604
- for (const [tag, df] of docFreq) {
605
- idf.set(tag, Math.log((N + 1) / (df + 1)) + 1);
991
+ const finalDomains = {};
992
+ for (const [d, b] of Object.entries(byDomain)) {
993
+ finalDomains[d] = {
994
+ mergedPRs: b.mergedPRs,
995
+ distinctOrgs: b.orgs.size,
996
+ lastMergedAt: b.lastMergedAt
997
+ };
606
998
  }
607
- return idf;
999
+ return { status: "ok", byDomain: finalDomains, qualifyingTotal, computedAt };
608
1000
  }
609
- function inferSeniority(title) {
1001
+ function acceptanceCountForDomains(cred, domains) {
1002
+ if (cred.status !== "ok") return 0;
1003
+ let max = 0;
1004
+ for (const d of domains) {
1005
+ const c = cred.byDomain[d]?.mergedPRs ?? 0;
1006
+ if (c > max) max = c;
1007
+ }
1008
+ return max;
1009
+ }
1010
+ function bestAcceptanceDomain(cred, domains) {
1011
+ if (cred.status !== "ok") return null;
1012
+ let best = null;
1013
+ for (const d of domains) {
1014
+ const count = cred.byDomain[d]?.mergedPRs ?? 0;
1015
+ if (count > 0 && (best === null || count > best.count)) best = { domain: d, count };
1016
+ }
1017
+ return best;
1018
+ }
1019
+ var MIN_STARS, MIN_CONTRIBUTORS, CANDIDATE_PR_PAGE, TRIVIAL_PR_TITLE;
1020
+ var init_github = __esm({
1021
+ "../../packages/core/src/github.ts"() {
1022
+ "use strict";
1023
+ init_vocabulary();
1024
+ MIN_STARS = 50;
1025
+ MIN_CONTRIBUTORS = 10;
1026
+ CANDIDATE_PR_PAGE = 50;
1027
+ TRIVIAL_PR_TITLE = /^\s*(fix\s+typo|typo\b|update\s+readme|readme\b|docs?:|docs?\(|chore:|chore\(|style:|ci:|build:|bump\b|update\s+dependenc)/i;
1028
+ }
1029
+ });
1030
+
1031
+ // ../../packages/core/src/matcher.ts
1032
+ function acceptanceDomainsOf(job) {
1033
+ return job.coreTags && job.coreTags.length > 0 ? job.coreTags : job.tags;
1034
+ }
1035
+ function backgroundIdf(tag) {
1036
+ const df = IDF_BACKGROUND.df[tag] ?? 0;
1037
+ return Math.log((IDF_BACKGROUND.N + 1) / (df + 1)) + 1;
1038
+ }
1039
+ function inferSeniority2(title) {
610
1040
  if (!ENG_TITLE.test(title)) return void 0;
611
1041
  for (const [re, level] of SENIORITY_PATTERNS) {
612
1042
  if (re.test(title)) return level;
@@ -615,7 +1045,7 @@ function inferSeniority(title) {
615
1045
  }
616
1046
  function seniorityScore(fp, job) {
617
1047
  if (!fp.seniorityBand) return 1;
618
- const jobLevel = inferSeniority(job.title);
1048
+ const jobLevel = inferSeniority2(job.title);
619
1049
  if (!jobLevel) return 0.85;
620
1050
  const wanted = SENIORITY_RANK[fp.seniorityBand] ?? 1;
621
1051
  const got = SENIORITY_RANK[jobLevel] ?? 1;
@@ -625,8 +1055,10 @@ function seniorityScore(fp, job) {
625
1055
  return 0.4;
626
1056
  }
627
1057
  function recencyScore(postedAt, now) {
628
- if (!postedAt) return 0.75;
629
- const ageDays2 = (now - new Date(postedAt).getTime()) / 864e5;
1058
+ if (!postedAt) return UNKNOWN_RECENCY;
1059
+ const ms = new Date(postedAt).getTime();
1060
+ if (Number.isNaN(ms)) return UNKNOWN_RECENCY;
1061
+ const ageDays2 = (now - ms) / 864e5;
630
1062
  if (ageDays2 < 7) return 1;
631
1063
  if (ageDays2 < 30) return 0.9;
632
1064
  if (ageDays2 < 90) return 0.75;
@@ -657,9 +1089,8 @@ function harmonicMean(a, b) {
657
1089
  if (a <= 0 || b <= 0) return 0;
658
1090
  return 2 * a * b / (a + b);
659
1091
  }
660
- function match(fp, jobs, limit = 5, now = Date.now()) {
661
- const idf = computeIdf(jobs);
662
- const idfOf = (t) => idf.get(t) ?? 0;
1092
+ function match(fp, jobs, limit = 5, now = Date.now(), opts = {}) {
1093
+ const idfOf = backgroundIdf;
663
1094
  const expanded = expandWeighted(fp.skillTags);
664
1095
  const maxDevScore = fp.skillTags.reduce((acc, t) => acc + idfOf(t), 0);
665
1096
  const candidates = jobs.filter((j) => passesFilters(fp, j));
@@ -685,32 +1116,45 @@ function match(fp, jobs, limit = 5, now = Date.now()) {
685
1116
  const jobCov = jobMaxScore > 0 ? Math.min(1, jobMatchScore / jobMaxScore) : 0;
686
1117
  const tagComponent = harmonicMean(devCov, jobCov);
687
1118
  if (tagComponent === 0) return null;
1119
+ const coreTags = job.coreTags ?? coreTagsFromTitle(job.title);
1120
+ let coreComponent = tagComponent;
1121
+ if (coreTags.length > 0) {
1122
+ const coreCov = Math.max(0, ...coreTags.map((ct) => expanded.get(ct)?.weight ?? 0));
1123
+ if (coreCov === 0) coreComponent = tagComponent * CORE_MISS_PENALTY;
1124
+ }
688
1125
  details.sort((a, b) => idfOf(b.tag) * b.weight - idfOf(a.tag) * a.weight);
689
1126
  const sScore = seniorityScore(fp, job);
690
1127
  const rScore = recencyScore(job.postedAt, now);
691
- const score = tagComponent * 0.6 + sScore * 0.25 + rScore * 0.15;
1128
+ const score = coreComponent * 0.6 + sScore * 0.25 + rScore * 0.15;
692
1129
  const matchedTags = [...new Set(details.map((d) => d.via ?? d.tag))];
1130
+ const badge = opts.acceptance ? bestAcceptanceDomain(opts.acceptance, acceptanceDomainsOf(job)) : null;
693
1131
  return {
694
1132
  job,
695
1133
  score: Math.round(score * 1e3) / 1e3,
696
1134
  matchedTags,
697
1135
  matchDetails: details,
1136
+ ...badge ? { acceptance: { status: "ok", domain: badge.domain, count: badge.count } } : {},
698
1137
  reason: buildReason(details)
699
1138
  };
700
1139
  });
701
- return scored.filter((r) => r !== null && r.score >= MIN_SCORE).sort((a, b) => b.score - a.score).slice(0, limit);
702
- }
703
- function matchOne(fp, job) {
704
- const results = match(fp, [job], 1);
705
- return results.length > 0 ? results[0] : null;
706
- }
707
- var MIN_SCORE, SHARPEN, SENIORITY_RANK, SENIORITY_PATTERNS, ENG_TITLE;
1140
+ return scored.filter((r) => r !== null && r.score >= MIN_SCORE).sort((a, b) => {
1141
+ const byScore = b.score - a.score;
1142
+ if (Math.abs(byScore) > TIEBREAK_EPS) return byScore;
1143
+ const byAcceptance = (b.acceptance?.count ?? 0) - (a.acceptance?.count ?? 0);
1144
+ if (byAcceptance !== 0) return byAcceptance;
1145
+ return byScore;
1146
+ }).slice(0, limit);
1147
+ }
1148
+ var MIN_SCORE, TIEBREAK_EPS, SHARPEN, CORE_MISS_PENALTY, SENIORITY_RANK, SENIORITY_PATTERNS, ENG_TITLE, UNKNOWN_RECENCY;
708
1149
  var init_matcher = __esm({
709
1150
  "../../packages/core/src/matcher.ts"() {
710
1151
  "use strict";
711
1152
  init_vocabulary();
1153
+ init_github();
712
1154
  MIN_SCORE = 0.15;
1155
+ TIEBREAK_EPS = 5e-3;
713
1156
  SHARPEN = 1.6;
1157
+ CORE_MISS_PENALTY = 0.4;
714
1158
  SENIORITY_RANK = {
715
1159
  junior: 0,
716
1160
  mid: 1,
@@ -724,24 +1168,19 @@ var init_matcher = __esm({
724
1168
  [/\bmid[\s-]?level\b|\bmid\b/i, "mid"]
725
1169
  ];
726
1170
  ENG_TITLE = /\b(engineer|engineering|developer|dev|swe|sde|programmer|architect)\b/i;
1171
+ UNKNOWN_RECENCY = 0.75;
727
1172
  }
728
1173
  });
729
1174
 
730
1175
  // ../../packages/core/src/feeds/greenhouse.ts
731
- function tokenize(text) {
732
- return text.toLowerCase().replace(/[^a-z0-9.\-+#]/g, " ").split(/\s+/).filter(Boolean);
733
- }
734
1176
  function extractTags(job) {
735
- const texts = [
736
- job.title,
1177
+ const body = [
737
1178
  ...(job.departments ?? []).map((d) => d.name),
738
1179
  job.location?.name ?? "",
739
1180
  ...(job.offices ?? []).map((o) => o.name),
740
- // mine the full HTML description for additional signal when present
741
1181
  ...job.content ? [job.content.replace(/<[^>]*>/g, " ")] : []
742
- ].filter(Boolean);
743
- const tokens = texts.flatMap(tokenize);
744
- return normalize(tokens);
1182
+ ].filter(Boolean).join(" ");
1183
+ return extractSkillTags(job.title, body);
745
1184
  }
746
1185
  function inferRemote(location) {
747
1186
  const l = location.toLowerCase();
@@ -839,17 +1278,15 @@ var init_greenhouse = __esm({
839
1278
  });
840
1279
 
841
1280
  // ../../packages/core/src/feeds/ashby.ts
842
- function tokenize2(text) {
843
- return text.toLowerCase().replace(/[^a-z0-9.\-+#]/g, " ").split(/\s+/).filter(Boolean);
844
- }
845
1281
  function extractTags2(job) {
846
- const texts = [
847
- job.title,
848
- job.teamName ?? "",
849
- job.locationName ?? "",
850
- ...(job.secondaryLocations ?? []).map((l) => l.locationName ?? "")
851
- ];
852
- return normalize(texts.flatMap(tokenize2));
1282
+ const body = [
1283
+ job.team ?? "",
1284
+ job.department ?? "",
1285
+ job.location ?? "",
1286
+ ...(job.secondaryLocations ?? []).map((l) => l.location ?? ""),
1287
+ job.descriptionPlain ?? ""
1288
+ ].join(" ");
1289
+ return extractSkillTags(job.title, body);
853
1290
  }
854
1291
  function mapEmploymentType(raw) {
855
1292
  if (!raw) return "full_time";
@@ -860,7 +1297,7 @@ function mapEmploymentType(raw) {
860
1297
  }
861
1298
  function inferRemote2(job) {
862
1299
  if (job.isRemote === true) return true;
863
- const loc = (job.locationName ?? "").toLowerCase();
1300
+ const loc = (job.location ?? "").toLowerCase();
864
1301
  return loc.includes("remote") || loc.includes("anywhere");
865
1302
  }
866
1303
  async function fetchSlug2(slug) {
@@ -879,14 +1316,14 @@ async function fetchSlug2(slug) {
879
1316
  source: "ashby",
880
1317
  title: j.title,
881
1318
  company: slug,
882
- url: j.applyUrl ?? `https://jobs.ashbyhq.com/${slug}/${j.id}`,
1319
+ url: j.jobUrl ?? j.applyUrl ?? `https://jobs.ashbyhq.com/${slug}/${j.id}`,
883
1320
  remote: inferRemote2(j),
884
- location: j.locationName,
1321
+ location: j.location,
885
1322
  compMin: comp?.minValue,
886
1323
  compMax: comp?.maxValue,
887
1324
  tags: extractTags2(j),
888
1325
  roleType: mapEmploymentType(j.employmentType),
889
- postedAt: j.publishedDate,
1326
+ postedAt: j.publishedAt,
890
1327
  applyMode: "direct",
891
1328
  raw: j
892
1329
  };
@@ -913,20 +1350,16 @@ var init_ashby = __esm({
913
1350
  });
914
1351
 
915
1352
  // ../../packages/core/src/feeds/lever.ts
916
- function tokenize3(text) {
917
- return text.toLowerCase().replace(/[^a-z0-9.\-+#]/g, " ").split(/\s+/).filter(Boolean);
918
- }
919
1353
  function extractTags3(p) {
920
1354
  const cat = p.categories ?? {};
921
- const texts = [
922
- p.text,
1355
+ const body = [
923
1356
  cat.team ?? "",
924
1357
  cat.department ?? "",
925
1358
  cat.location ?? "",
926
1359
  ...cat.allLocations ?? [],
927
1360
  p.descriptionPlain ?? ""
928
- ];
929
- return normalize(texts.flatMap(tokenize3));
1361
+ ].join(" ");
1362
+ return extractSkillTags(p.text, body);
930
1363
  }
931
1364
  function mapCommitment(raw) {
932
1365
  if (!raw) return "full_time";
@@ -999,15 +1432,8 @@ var init_lever = __esm({
999
1432
  });
1000
1433
 
1001
1434
  // ../../packages/core/src/feeds/himalayas.ts
1002
- function tokenize4(text) {
1003
- return text.toLowerCase().replace(/[^a-z0-9.\-+#]/g, " ").split(/\s+/).filter(Boolean);
1004
- }
1005
1435
  function extractTags4(job) {
1006
- const texts = [
1007
- job.title,
1008
- ...job.tags ?? []
1009
- ];
1010
- return normalize(texts.flatMap(tokenize4));
1436
+ return extractSkillTags(job.title, (job.tags ?? []).join(" "));
1011
1437
  }
1012
1438
  function mapJobType(raw) {
1013
1439
  if (!raw) return "full_time";
@@ -1092,9 +1518,6 @@ var init_entities = __esm({
1092
1518
  });
1093
1519
 
1094
1520
  // ../../packages/core/src/feeds/wwr.ts
1095
- function tokenize5(text) {
1096
- return text.toLowerCase().replace(/[^a-z0-9.\-+#]/g, " ").split(/\s+/).filter(Boolean);
1097
- }
1098
1521
  function stripHtml(html) {
1099
1522
  return html.replace(/<[^>]*>/g, " ").replace(/\s+/g, " ").trim();
1100
1523
  }
@@ -1138,8 +1561,8 @@ function parseRss(xml) {
1138
1561
  return items;
1139
1562
  }
1140
1563
  function extractTags5(item) {
1141
- const text = [item.title, item.category, stripHtml(item.description)].join(" ");
1142
- return normalize(tokenize5(text));
1564
+ const body = [item.category, stripHtml(item.description)].join(" ");
1565
+ return extractSkillTags(item.title, body);
1143
1566
  }
1144
1567
  var WWR_RSS_URL, wwr;
1145
1568
  var init_wwr = __esm({
@@ -1181,9 +1604,6 @@ var init_wwr = __esm({
1181
1604
  });
1182
1605
 
1183
1606
  // ../../packages/core/src/feeds/hn.ts
1184
- function tokenize6(text) {
1185
- return text.toLowerCase().replace(/[^a-z0-9.\-+#]/g, " ").split(/\s+/).filter(Boolean);
1186
- }
1187
1607
  function stripHtml2(html) {
1188
1608
  return decodeEntities(html.replace(/<p>/gi, " ").replace(/<[^>]*>/g, "")).replace(/\s+/g, " ").trim();
1189
1609
  }
@@ -1214,7 +1634,7 @@ function parseComment(item) {
1214
1634
  return null;
1215
1635
  }
1216
1636
  const url = extractUrl(raw) || `https://news.ycombinator.com/item?id=${item.id}`;
1217
- const tags = extractTags6(raw);
1637
+ const tags = extractTags6(title, raw);
1218
1638
  if (tags.length === 0) return null;
1219
1639
  return {
1220
1640
  id: `hn:${item.id}`,
@@ -1231,8 +1651,8 @@ function parseComment(item) {
1231
1651
  raw: item
1232
1652
  };
1233
1653
  }
1234
- function extractTags6(text) {
1235
- return normalize(tokenize6(text));
1654
+ function extractTags6(title, text) {
1655
+ return extractSkillTags(title, text);
1236
1656
  }
1237
1657
  var ALGOLIA_SEARCH, ALGOLIA_ITEMS, hn;
1238
1658
  var init_hn = __esm({
@@ -1322,7 +1742,7 @@ function authHeaders() {
1322
1742
  if (token) h["Authorization"] = `Bearer ${token}`;
1323
1743
  return h;
1324
1744
  }
1325
- function tokenize7(text) {
1745
+ function tokenize2(text) {
1326
1746
  return text.toLowerCase().replace(/[^a-z0-9.\-+#]/g, " ").split(/\s+/).filter(Boolean);
1327
1747
  }
1328
1748
  function parseAmountUSD(text) {
@@ -1407,7 +1827,7 @@ async function fetchRepoBounties(repoFullName) {
1407
1827
  const body = issue.body ? decodeEntities(issue.body) : "";
1408
1828
  const amountUSD = parseAmountUSD(title) ?? parseAmountUSD(body) ?? await fetchCommentAmount(repoFullName, issue.number);
1409
1829
  const labels = labelNames(issue);
1410
- const tags = normalize(tokenize7([title, labels.join(" "), body.slice(0, 2e3)].join(" ")));
1830
+ const tags = normalize(tokenize2([title, labels.join(" "), body.slice(0, 2e3)].join(" ")));
1411
1831
  return {
1412
1832
  id: `bounty:${repoFullName}#${issue.number}`,
1413
1833
  source: "bounty",
@@ -1724,103 +2144,6 @@ var init_indexer = __esm({
1724
2144
  }
1725
2145
  });
1726
2146
 
1727
- // ../../packages/core/src/github.ts
1728
- function ghHeaders(token) {
1729
- const headers = {
1730
- Accept: "application/vnd.github+json",
1731
- "X-GitHub-Api-Version": "2022-11-28"
1732
- };
1733
- if (token) headers["Authorization"] = `Bearer ${token}`;
1734
- return headers;
1735
- }
1736
- async function ghFetch(path, token) {
1737
- const url = `https://api.github.com${path}`;
1738
- const res = await fetch(url, { headers: ghHeaders(token) });
1739
- if (!res.ok) {
1740
- throw new Error(`GitHub API ${path}: HTTP ${res.status} ${res.statusText}`);
1741
- }
1742
- return res.json();
1743
- }
1744
- async function fetchGitHubProfile(login, token) {
1745
- const user = await ghFetch(`/users/${login}`, token);
1746
- let repos = [];
1747
- try {
1748
- repos = await ghFetch(
1749
- `/users/${login}/repos?sort=pushed&per_page=100`,
1750
- token
1751
- );
1752
- } catch (err) {
1753
- console.warn(`[github] ${login}: repos fetch failed, continuing \u2014`, err);
1754
- }
1755
- const langCount = {};
1756
- for (const repo of repos) {
1757
- if (repo.fork) continue;
1758
- if (repo.language) {
1759
- langCount[repo.language.toLowerCase()] = (langCount[repo.language.toLowerCase()] ?? 0) + 1;
1760
- }
1761
- }
1762
- const topLanguages = Object.entries(langCount).sort(([, a], [, b]) => b - a).slice(0, 10).map(([lang]) => lang);
1763
- const topicSet = /* @__PURE__ */ new Set();
1764
- for (const repo of repos) {
1765
- if (repo.fork) continue;
1766
- for (const t of repo.topics ?? []) topicSet.add(t.toLowerCase());
1767
- }
1768
- const topics = Array.from(topicSet).slice(0, 30);
1769
- let recentPRorgs;
1770
- try {
1771
- const q = encodeURIComponent(
1772
- `type:pr is:merged author:${login} sort:updated`
1773
- );
1774
- const result = await ghFetch(
1775
- `/search/issues?q=${q}&per_page=30`,
1776
- token
1777
- );
1778
- const orgs = /* @__PURE__ */ new Set();
1779
- for (const item of result.items ?? []) {
1780
- const orgLogin = item.repository?.owner?.login;
1781
- if (orgLogin && orgLogin !== login) orgs.add(orgLogin);
1782
- }
1783
- if (orgs.size > 0) recentPRorgs = Array.from(orgs);
1784
- } catch {
1785
- }
1786
- return {
1787
- login: user.login,
1788
- name: user.name ?? void 0,
1789
- publicEmail: user.email ?? void 0,
1790
- avatarUrl: user.avatar_url,
1791
- accountCreatedAt: user.created_at,
1792
- publicRepos: user.public_repos,
1793
- followers: user.followers,
1794
- topLanguages,
1795
- topics,
1796
- recentPRorgs
1797
- };
1798
- }
1799
- function inferSeniority2(p) {
1800
- const ageMs = Date.now() - new Date(p.accountCreatedAt).getTime();
1801
- const ageYears = ageMs / (1e3 * 60 * 60 * 24 * 365.25);
1802
- if (ageYears >= 9 && (p.publicRepos >= 40 || p.followers >= 500)) return "staff";
1803
- if (ageYears >= 5 && (p.publicRepos >= 20 || p.followers >= 100)) return "senior";
1804
- if (ageYears >= 2 && p.publicRepos >= 5) return "mid";
1805
- return "junior";
1806
- }
1807
- function githubToFingerprint(p) {
1808
- const rawTokens = [
1809
- ...p.topLanguages,
1810
- ...p.topics
1811
- // recentPRorgs intentionally excluded — org names are not skill tags
1812
- ];
1813
- const skillTags = normalize(rawTokens);
1814
- const seniorityBand = inferSeniority2(p);
1815
- return { skillTags, seniorityBand };
1816
- }
1817
- var init_github = __esm({
1818
- "../../packages/core/src/github.ts"() {
1819
- "use strict";
1820
- init_vocabulary();
1821
- }
1822
- });
1823
-
1824
2147
  // ../../packages/core/src/index.ts
1825
2148
  var src_exports = {};
1826
2149
  __export(src_exports, {
@@ -1834,17 +2157,23 @@ __export(src_exports, {
1834
2157
  FEEDS: () => FEEDS,
1835
2158
  GRAPH: () => GRAPH,
1836
2159
  GREENHOUSE_SLUGS_BY_TIER: () => GREENHOUSE_SLUGS_BY_TIER,
2160
+ IDF_BACKGROUND: () => IDF_BACKGROUND,
1837
2161
  LEVER_SLUGS_BY_TIER: () => LEVER_SLUGS_BY_TIER,
1838
2162
  SYNONYMS: () => SYNONYMS,
1839
2163
  VOCABULARY: () => VOCABULARY,
1840
2164
  VOCAB_NODES: () => VOCAB_NODES,
2165
+ acceptanceCountForDomains: () => acceptanceCountForDomains,
1841
2166
  aggregate: () => aggregate,
1842
2167
  aggregateBounties: () => aggregateBounties,
1843
2168
  ashby: () => ashby,
2169
+ bestAcceptanceDomain: () => bestAcceptanceDomain,
1844
2170
  buildGraph: () => buildGraph,
1845
2171
  buildIndex: () => buildIndex,
1846
2172
  buildReason: () => buildReason,
2173
+ computeAcceptanceCredential: () => computeAcceptanceCredential,
2174
+ coreTagsFromTitle: () => coreTagsFromTitle,
1847
2175
  expandWeighted: () => expandWeighted,
2176
+ extractSkillTags: () => extractSkillTags,
1848
2177
  fetchGitHubProfile: () => fetchGitHubProfile,
1849
2178
  flattenTiers: () => flattenTiers,
1850
2179
  getBuyer: () => getBuyer,
@@ -1856,10 +2185,11 @@ __export(src_exports, {
1856
2185
  isBounty: () => isBounty,
1857
2186
  lever: () => lever,
1858
2187
  loadPartnerRoles: () => loadPartnerRoles,
2188
+ looksLikeEngRole: () => looksLikeEngRole,
1859
2189
  match: () => match,
1860
- matchOne: () => matchOne,
1861
2190
  normalize: () => normalize,
1862
2191
  passesMaturityGate: () => passesMaturityGate,
2192
+ tokenize: () => tokenize,
1863
2193
  validateGraph: () => validateGraph,
1864
2194
  wwr: () => wwr
1865
2195
  });
@@ -2131,7 +2461,7 @@ async function run() {
2131
2461
  }
2132
2462
  async function runLogin() {
2133
2463
  const { runDeviceFlow: runDeviceFlow2, readGitHubToken: readGitHubToken2 } = await Promise.resolve().then(() => (init_github_auth(), github_auth_exports));
2134
- const { fetchGitHubProfile: fetchGitHubProfile2, githubToFingerprint: githubToFingerprint2 } = await Promise.resolve().then(() => (init_src(), src_exports));
2464
+ const { fetchGitHubProfile: fetchGitHubProfile2, githubToFingerprint: githubToFingerprint2, computeAcceptanceCredential: computeAcceptanceCredential2 } = await Promise.resolve().then(() => (init_src(), src_exports));
2135
2465
  const { readProfile: readProfile2, writeProfile: writeProfile2, accumulateGitHubTags: accumulateGitHubTags2 } = await Promise.resolve().then(() => (init_profile(), profile_exports));
2136
2466
  console.log("");
2137
2467
  console.log(" terminalhire \u2014 Sign in with GitHub");
@@ -2147,7 +2477,7 @@ async function runLogin() {
2147
2477
  console.log(`
2148
2478
  Fetching public profile for @${login}...`);
2149
2479
  let ghProfile;
2150
- if (process.env["TERMINALHIRE_GITHUB_MOCK"] === "1" || process.env["TERMINALHIRE_GITHUB_MOCK"] === "1" || process.env["JPI_GITHUB_MOCK"] === "1") {
2480
+ if (process.env["TERMINALHIRE_GITHUB_MOCK"] === "1" || process.env["JPI_GITHUB_MOCK"] === "1") {
2151
2481
  const { createRequire: createRequire2 } = await import("module");
2152
2482
  const { fileURLToPath: fileURLToPath7 } = await import("url");
2153
2483
  const { join: join15, dirname: dirname3 } = await import("path");
@@ -2173,6 +2503,15 @@ async function runLogin() {
2173
2503
  topLanguages: ghProfile.topLanguages.slice(0, 5),
2174
2504
  publicRepos: ghProfile.publicRepos
2175
2505
  };
2506
+ const isMock = process.env["TERMINALHIRE_GITHUB_MOCK"] === "1" || process.env["JPI_GITHUB_MOCK"] === "1";
2507
+ if (!isMock) {
2508
+ try {
2509
+ console.log(" Computing proof-of-work acceptance credential...");
2510
+ profile.acceptance = await computeAcceptanceCredential2(ghProfile.login, token);
2511
+ } catch (err) {
2512
+ if (process.env["DEBUG"]) console.warn(" [acceptance] credential compute failed:", err);
2513
+ }
2514
+ }
2176
2515
  await writeProfile2(profile);
2177
2516
  console.log("");
2178
2517
  console.log(" GitHub profile merged into local encrypted profile:");
@@ -2189,6 +2528,9 @@ async function runLogin() {
2189
2528
  if (fragment.skillTags.length === 0) {
2190
2529
  console.log(" (No matching vocabulary tags found in public repos/topics)");
2191
2530
  }
2531
+ if (profile.acceptance?.status === "ok" && profile.acceptance.qualifyingTotal > 0) {
2532
+ console.log(` Proof-of-work: ${profile.acceptance.qualifyingTotal} merged PR${profile.acceptance.qualifyingTotal === 1 ? "" : "s"} into external repos`);
2533
+ }
2192
2534
  console.log("");
2193
2535
  console.log(" Profile updated at ~/.terminalhire/profile.enc (encrypted at rest)");
2194
2536
  console.log(" GitHub data stays on your machine unless you consent to share it in a lead.");
@@ -2210,10 +2552,18 @@ async function runLogout() {
2210
2552
  }
2211
2553
  await deleteGitHubToken2();
2212
2554
  const profile = await readProfile2();
2555
+ let changed = false;
2213
2556
  if (profile.github) {
2214
2557
  delete profile.github;
2558
+ changed = true;
2559
+ }
2560
+ if (profile.acceptance) {
2561
+ delete profile.acceptance;
2562
+ changed = true;
2563
+ }
2564
+ if (changed) {
2215
2565
  await writeProfile2(profile);
2216
- console.log("\n GitHub identity cleared from local profile.");
2566
+ console.log("\n GitHub identity + proof-of-work credential cleared from local profile.");
2217
2567
  }
2218
2568
  console.log(" GitHub token deleted from ~/.terminalhire/github-token.enc");
2219
2569
  console.log(" Skill tags accumulated from GitHub remain in your profile.");
@@ -2336,7 +2686,7 @@ function linkTitle(title, url) {
2336
2686
  return url ? `${title} (${url})` : title;
2337
2687
  }
2338
2688
  function printResult(i, result) {
2339
- const { job, score, matchedTags, reason } = result;
2689
+ const { job, score, matchedTags, reason, acceptance } = result;
2340
2690
  const comp = formatComp(job);
2341
2691
  const remote = job.remote ? "remote" : job.location ?? "onsite";
2342
2692
  const compStr = comp ? ` \xB7 ${comp}` : "";
@@ -2348,6 +2698,9 @@ ${i + 1}. ${titleStr} \u2014 ${job.company}${mode}`);
2348
2698
  console.log(` ${remote}${compStr} \xB7 ${job.roleType} \xB7 score: ${formatScore(score)}`);
2349
2699
  console.log(` ${reason}`);
2350
2700
  console.log(` Tags matched: ${matchedTags.slice(0, 5).join(", ")}`);
2701
+ if (acceptance && acceptance.status === "ok" && acceptance.count > 0) {
2702
+ console.log(` \u2713 proof-of-work: ${acceptance.count} merged PR${acceptance.count === 1 ? "" : "s"} into external ${acceptance.domain} repos`);
2703
+ }
2351
2704
  if (job.applyMode === "direct") {
2352
2705
  console.log(` Apply: ${job.url}`);
2353
2706
  } else {
@@ -2405,7 +2758,9 @@ async function run2() {
2405
2758
  }
2406
2759
  const fp = profileToFingerprint2(profile);
2407
2760
  if (REMOTE_ONLY) fp.prefs = { ...fp.prefs, remoteOnly: true };
2408
- const results = match2(fp, jobs, SHOW_ALL ? jobs.length : LIMIT);
2761
+ const results = match2(fp, jobs, SHOW_ALL ? jobs.length : LIMIT, Date.now(), {
2762
+ acceptance: profile.acceptance
2763
+ });
2409
2764
  try {
2410
2765
  const cacheRaw = readFileSync4(INDEX_CACHE_FILE, "utf8");
2411
2766
  const cacheEntry = JSON.parse(cacheRaw);