terminalhire 0.3.5 → 0.4.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.
@@ -147,11 +147,11 @@ var init_graph_data = __esm({
147
147
  { id: "spark", parents: ["data-engineering"], synonyms: ["apache-spark"] },
148
148
  { id: "airflow", parents: ["data-engineering"], synonyms: ["apache-airflow"] },
149
149
  { id: "dbt", parents: ["data-engineering"] },
150
- { id: "ml", synonyms: ["machine-learning"], related: [{ to: "pytorch", w: 0.5 }, { to: "tensorflow", w: 0.5 }, { to: "scikit-learn", w: 0.5 }] },
151
- { 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 }] },
150
+ { 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 }] },
151
+ { id: "llm", parents: ["ml"], synonyms: ["llms", "genai", "generative-ai", "gpt"], related: [{ to: "langchain", w: 0.5 }, { to: "rag", w: 0.55 }, { to: "openai", w: 0.45 }, { to: "anthropic", w: 0.45 }] },
152
152
  { id: "pytorch", parents: ["ml"], synonyms: ["torch"], related: [{ to: "tensorflow", w: 0.5 }] },
153
153
  { id: "tensorflow", parents: ["ml"], synonyms: ["keras", "tf-keras"] },
154
- { id: "pandas", parents: ["python"], related: [{ to: "numpy", w: 0.6 }] },
154
+ { id: "pandas", parents: ["python"], related: [{ to: "numpy", w: 0.6 }, { to: "data-engineering", w: 0.45 }, { to: "spark", w: 0.4 }] },
155
155
  { id: "numpy", parents: ["python"] },
156
156
  { id: "scikit-learn", parents: ["ml"], synonyms: ["sklearn"] },
157
157
  { id: "jupyter", parents: ["python"] },
@@ -161,6 +161,14 @@ var init_graph_data = __esm({
161
161
  { id: "anthropic", parents: ["llm"], synonyms: ["claude"] },
162
162
  { id: "rag", parents: ["llm"], synonyms: ["retrieval-augmented-generation"] },
163
163
  { id: "mlops", parents: ["ml"], related: [{ to: "devops", w: 0.4 }] },
164
+ { id: "agents", parents: ["llm"], synonyms: ["agentic", "ai-agents", "multi-agent"], related: [{ to: "rag", w: 0.4 }] },
165
+ { id: "mcp", parents: ["agents"], synonyms: ["model-context-protocol"], related: [{ to: "llm", w: 0.45 }] },
166
+ { id: "inference", parents: ["ml"], synonyms: ["model-inference", "llm-inference", "model-serving"], related: [{ to: "mlops", w: 0.5 }, { to: "llm", w: 0.4 }] },
167
+ { id: "embeddings", parents: ["ml"], synonyms: ["embedding", "vector-embeddings"], related: [{ to: "rag", w: 0.55 }, { to: "llm", w: 0.45 }] },
168
+ { id: "prompt-engineering", parents: ["llm"], synonyms: ["prompting", "prompt"] },
169
+ { id: "fine-tuning", parents: ["ml"], synonyms: ["finetuning", "fine-tune", "rlhf"], related: [{ to: "llm", w: 0.5 }] },
170
+ { id: "computer-vision", parents: ["ml"], synonyms: ["image-recognition", "object-detection"] },
171
+ { id: "recsys", parents: ["ml"], synonyms: ["recommender-systems", "recommendation-systems", "recommendation"] },
164
172
  // ── Mobile ──────────────────────────────────────────────────────────────────
165
173
  { id: "mobile", related: [{ to: "ios", w: 0.5 }, { to: "android", w: 0.5 }] },
166
174
  { id: "ios", parents: ["mobile", "swift"], related: [{ to: "android", w: 0.4 }] },
@@ -326,6 +334,207 @@ var init_types2 = __esm({
326
334
  }
327
335
  });
328
336
 
337
+ // ../../packages/core/src/vocab/extract.ts
338
+ function tokenize(text) {
339
+ return text.toLowerCase().replace(/[^a-z0-9.\-+#]/g, " ").split(/\s+/).filter(Boolean);
340
+ }
341
+ function looksLikeEngRole(title) {
342
+ return !NON_ENG_TITLE.test(title) && ENG_INTENT.test(title);
343
+ }
344
+ function resolveToken(token) {
345
+ const tryOne = (t) => {
346
+ if (GRAPH.ids.has(t)) return { id: t, viaSynonym: false };
347
+ const mapped = GRAPH.synonyms.get(t);
348
+ return mapped ? { id: mapped, viaSynonym: true } : null;
349
+ };
350
+ return tryOne(token) ?? tryOne(token.replace(/^[.\-+#]+|[.\-+#]+$/g, ""));
351
+ }
352
+ function extractSkillTags(title, body = "") {
353
+ if (!looksLikeEngRole(title)) return [];
354
+ const text = `${title}
355
+ ${body}`;
356
+ const tokens = tokenize(text);
357
+ const ids = /* @__PURE__ */ new Set();
358
+ const ambiguousPending = /* @__PURE__ */ new Set();
359
+ for (const tok of tokens) {
360
+ const r = resolveToken(tok);
361
+ if (!r) continue;
362
+ if (NON_EXTRACTABLE.has(r.id)) continue;
363
+ if (SYNONYM_ONLY.has(r.id) && !r.viaSynonym) continue;
364
+ const cue = AMBIGUOUS[r.id];
365
+ if (cue) {
366
+ if (cue.test(text)) ids.add(r.id);
367
+ else ambiguousPending.add(r.id);
368
+ continue;
369
+ }
370
+ ids.add(r.id);
371
+ }
372
+ const hardCount = [...ids].filter((id) => !SOFT_DOMAIN.has(id)).length;
373
+ if (hardCount >= 2) for (const id of ambiguousPending) ids.add(id);
374
+ return [...ids];
375
+ }
376
+ function coreTagsFromTitle(title) {
377
+ return extractSkillTags(title, "").filter((t) => !SOFT_DOMAIN.has(t));
378
+ }
379
+ var SOFT_DOMAIN, SYNONYM_ONLY, NON_EXTRACTABLE, AMBIGUOUS, ENG_INTENT, NON_ENG_TITLE;
380
+ var init_extract = __esm({
381
+ "../../packages/core/src/vocab/extract.ts"() {
382
+ "use strict";
383
+ init_vocab();
384
+ SOFT_DOMAIN = /* @__PURE__ */ new Set([
385
+ "frontend",
386
+ "backend",
387
+ "devops",
388
+ "security",
389
+ "payments",
390
+ "billing",
391
+ "microservices",
392
+ "caching",
393
+ "search",
394
+ "observability",
395
+ "monitoring",
396
+ "testing",
397
+ "accessibility",
398
+ "seo",
399
+ "performance",
400
+ "realtime",
401
+ "authentication",
402
+ "api-design"
403
+ ]);
404
+ SYNONYM_ONLY = /* @__PURE__ */ new Set(["performance", "security", "seo"]);
405
+ NON_EXTRACTABLE = /* @__PURE__ */ new Set(["payments", "billing"]);
406
+ for (const id of SYNONYM_ONLY) {
407
+ if (!SOFT_DOMAIN.has(id)) throw new Error(`extract: SYNONYM_ONLY "${id}" not in SOFT_DOMAIN`);
408
+ }
409
+ AMBIGUOUS = {
410
+ // Accept "go" with an ecosystem cue OR an explicit-skill phrasing ("Go developer",
411
+ // "in Go", "experience with Go"). Rejects prose: "ready to go", "go above", "go live".
412
+ 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,
413
+ r: /\b(rstudio|tidyverse|ggplot|shiny|dplyr|cran|r-lang|rlang)\b/i,
414
+ 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
415
+ };
416
+ 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;
417
+ 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;
418
+ }
419
+ });
420
+
421
+ // ../../packages/core/src/vocab/idf-background.ts
422
+ var IDF_BACKGROUND;
423
+ var init_idf_background = __esm({
424
+ "../../packages/core/src/vocab/idf-background.ts"() {
425
+ "use strict";
426
+ IDF_BACKGROUND = {
427
+ N: 244,
428
+ df: {
429
+ "backend": 71,
430
+ "python": 57,
431
+ "monitoring": 44,
432
+ "nextjs": 40,
433
+ "testing": 40,
434
+ "observability": 38,
435
+ "llm": 38,
436
+ "go": 36,
437
+ "aws": 36,
438
+ "react": 33,
439
+ "frontend": 30,
440
+ "ml": 28,
441
+ "mobile": 24,
442
+ "realtime": 24,
443
+ "typescript": 23,
444
+ "devops": 22,
445
+ "kubernetes": 22,
446
+ "javascript": 21,
447
+ "java": 20,
448
+ "rag": 20,
449
+ "api-design": 20,
450
+ "linux": 19,
451
+ "postgresql": 19,
452
+ "search": 17,
453
+ "azure": 16,
454
+ "snowflake": 15,
455
+ "spark": 15,
456
+ "kotlin": 14,
457
+ "gcp": 14,
458
+ "accessibility": 14,
459
+ "nodejs": 14,
460
+ "graphql": 14,
461
+ "airflow": 14,
462
+ "docker": 14,
463
+ "ci-cd": 13,
464
+ "android": 12,
465
+ "cpp": 12,
466
+ "gitlab-ci": 11,
467
+ "anthropic": 11,
468
+ "terraform": 11,
469
+ "mysql": 11,
470
+ "r": 10,
471
+ "dbt": 9,
472
+ "langchain": 9,
473
+ "pytorch": 9,
474
+ "ruby": 9,
475
+ "rails": 9,
476
+ "cloudflare": 7,
477
+ "datadog": 7,
478
+ "css": 7,
479
+ "ansible": 7,
480
+ "openai": 6,
481
+ "kafka": 6,
482
+ "rust": 5,
483
+ "grpc": 5,
484
+ "microservices": 5,
485
+ "serverless": 5,
486
+ "scala": 5,
487
+ "prometheus": 5,
488
+ "grafana": 5,
489
+ "php": 5,
490
+ "redis": 5,
491
+ "huggingface": 4,
492
+ "pandas": 4,
493
+ "scikit-learn": 4,
494
+ "html": 4,
495
+ "ios": 4,
496
+ "authentication": 4,
497
+ "vue": 4,
498
+ "mlops": 3,
499
+ "spring": 3,
500
+ "mongodb": 3,
501
+ "csharp": 3,
502
+ "swift": 2,
503
+ "caching": 2,
504
+ "haskell": 2,
505
+ "pulumi": 2,
506
+ "argocd": 2,
507
+ "tensorflow": 2,
508
+ "express": 2,
509
+ "elasticsearch": 2,
510
+ "clickhouse": 2,
511
+ "nestjs": 2,
512
+ "vite": 2,
513
+ "svelte": 2,
514
+ "phoenix": 2,
515
+ "angular": 2,
516
+ "django": 2,
517
+ "dotnet": 2,
518
+ "elixir": 2,
519
+ "bun": 1,
520
+ "oauth": 1,
521
+ "dynamodb": 1,
522
+ "helm": 1,
523
+ "playwright": 1,
524
+ "cypress": 1,
525
+ "jest": 1,
526
+ "mocha": 1,
527
+ "typeorm": 1,
528
+ "tailwind": 1,
529
+ "prisma": 1,
530
+ "expo": 1,
531
+ "rabbitmq": 1,
532
+ "redux": 1
533
+ }
534
+ };
535
+ }
536
+ });
537
+
329
538
  // ../../packages/core/src/vocab/index.ts
330
539
  function normalize(tokens) {
331
540
  const result = /* @__PURE__ */ new Set();
@@ -362,6 +571,8 @@ var init_vocab = __esm({
362
571
  init_types2();
363
572
  init_closure();
364
573
  init_graph_data();
574
+ init_extract();
575
+ init_idf_background();
365
576
  GRAPH = buildGraph(VOCAB_NODES);
366
577
  VOCABULARY = [...GRAPH.ids];
367
578
  SYNONYMS = Object.fromEntries(GRAPH.synonyms);
@@ -376,23 +587,330 @@ var init_vocabulary = __esm({
376
587
  }
377
588
  });
378
589
 
379
- // ../../packages/core/src/matcher.ts
380
- function computeIdf(jobs) {
381
- const docFreq = /* @__PURE__ */ new Map();
382
- const N = jobs.length;
383
- for (const job of jobs) {
384
- const unique = new Set(job.tags);
385
- for (const tag of unique) {
386
- docFreq.set(tag, (docFreq.get(tag) ?? 0) + 1);
590
+ // ../../packages/core/src/github.ts
591
+ function ghHeaders(token) {
592
+ const headers = {
593
+ Accept: "application/vnd.github+json",
594
+ "X-GitHub-Api-Version": "2022-11-28",
595
+ // GitHub's REST API REQUIRES a User-Agent; serverless runtimes don't always
596
+ // send a default (omitting it yields a 403 "administrative rules" error).
597
+ "User-Agent": "terminalhire"
598
+ };
599
+ if (token) headers["Authorization"] = `Bearer ${token}`;
600
+ return headers;
601
+ }
602
+ async function ghFetch(path, token) {
603
+ const url = `https://api.github.com${path}`;
604
+ const res = await fetch(url, { headers: ghHeaders(token) });
605
+ if (!res.ok) {
606
+ throw new Error(`GitHub API ${path}: HTTP ${res.status} ${res.statusText}`);
607
+ }
608
+ return res.json();
609
+ }
610
+ async function fetchGitHubProfile(login, token) {
611
+ const user = await ghFetch(`/users/${login}`, token);
612
+ let repos = [];
613
+ try {
614
+ repos = await ghFetch(
615
+ `/users/${login}/repos?sort=pushed&per_page=100`,
616
+ token
617
+ );
618
+ } catch (err) {
619
+ console.warn(`[github] ${login}: repos fetch failed, continuing \u2014`, err);
620
+ }
621
+ const langCount = {};
622
+ for (const repo of repos) {
623
+ if (repo.fork) continue;
624
+ if (repo.language) {
625
+ langCount[repo.language.toLowerCase()] = (langCount[repo.language.toLowerCase()] ?? 0) + 1;
626
+ }
627
+ }
628
+ const topLanguages = Object.entries(langCount).sort(([, a], [, b]) => b - a).slice(0, 10).map(([lang]) => lang);
629
+ const topicSet = /* @__PURE__ */ new Set();
630
+ for (const repo of repos) {
631
+ if (repo.fork) continue;
632
+ for (const t of repo.topics ?? []) topicSet.add(t.toLowerCase());
633
+ }
634
+ const topics = Array.from(topicSet).slice(0, 30);
635
+ let recentPRorgs;
636
+ try {
637
+ const q = encodeURIComponent(
638
+ `type:pr is:merged author:${login} sort:updated`
639
+ );
640
+ const result = await ghFetch(
641
+ `/search/issues?q=${q}&per_page=30`,
642
+ token
643
+ );
644
+ const orgs = /* @__PURE__ */ new Set();
645
+ for (const item of result.items ?? []) {
646
+ const orgLogin = item.repository?.owner?.login;
647
+ if (orgLogin && orgLogin !== login) orgs.add(orgLogin);
648
+ }
649
+ if (orgs.size > 0) recentPRorgs = Array.from(orgs);
650
+ } catch {
651
+ }
652
+ return {
653
+ login: user.login,
654
+ name: user.name ?? void 0,
655
+ publicEmail: user.email ?? void 0,
656
+ avatarUrl: user.avatar_url,
657
+ accountCreatedAt: user.created_at,
658
+ publicRepos: user.public_repos,
659
+ followers: user.followers,
660
+ topLanguages,
661
+ topics,
662
+ recentPRorgs
663
+ };
664
+ }
665
+ function inferSeniority(p) {
666
+ const ageMs = Date.now() - new Date(p.accountCreatedAt).getTime();
667
+ const ageYears = ageMs / (1e3 * 60 * 60 * 24 * 365.25);
668
+ if (ageYears >= 9 && (p.publicRepos >= 40 || p.followers >= 500)) return "staff";
669
+ if (ageYears >= 5 && (p.publicRepos >= 20 || p.followers >= 100)) return "senior";
670
+ if (ageYears >= 2 && p.publicRepos >= 5) return "mid";
671
+ return "junior";
672
+ }
673
+ function githubToFingerprint(p) {
674
+ const rawTokens = [
675
+ ...p.topLanguages,
676
+ ...p.topics
677
+ // recentPRorgs intentionally excluded — org names are not skill tags
678
+ ];
679
+ const skillTags = normalize(rawTokens);
680
+ const seniorityBand = inferSeniority(p);
681
+ return { skillTags, seniorityBand };
682
+ }
683
+ async function ghFetchRaw(path, token) {
684
+ return fetch(`https://api.github.com${path}`, { headers: ghHeaders(token) });
685
+ }
686
+ function parseRepoUrl(repoUrl) {
687
+ const m = repoUrl.match(/\/repos\/([^/]+)\/([^/]+)\/?$/);
688
+ return m ? { owner: m[1], name: m[2] } : null;
689
+ }
690
+ function isTrivialPRTitle(title) {
691
+ return TRIVIAL_PR_TITLE.test(title);
692
+ }
693
+ async function fetchOwnedOrgs(token) {
694
+ try {
695
+ const memberships = await ghFetch(`/user/memberships/orgs?per_page=100`, token);
696
+ return new Set(
697
+ memberships.filter((m) => m.role === "admin").map((m) => m.organization.login.toLowerCase())
698
+ );
699
+ } catch {
700
+ return /* @__PURE__ */ new Set();
701
+ }
702
+ }
703
+ async function repoContributorCount(owner, name, token) {
704
+ try {
705
+ const res = await ghFetchRaw(
706
+ `/repos/${owner}/${name}/contributors?per_page=1&anon=false`,
707
+ token
708
+ );
709
+ if (!res.ok) return void 0;
710
+ const link = res.headers.get("link");
711
+ const m = link?.match(/[?&]page=(\d+)>;\s*rel="last"/);
712
+ if (m) return Number(m[1]);
713
+ const body = await res.json();
714
+ return Array.isArray(body) ? body.length : 0;
715
+ } catch {
716
+ return void 0;
717
+ }
718
+ }
719
+ async function fetchRepoMeta(owner, name, token, cache) {
720
+ const key = `${owner}/${name}`.toLowerCase();
721
+ const cached = cache.get(key);
722
+ if (cached !== void 0) return cached;
723
+ let meta = null;
724
+ try {
725
+ const r = await ghFetch(`/repos/${owner}/${name}`, token);
726
+ const contributors = await repoContributorCount(owner, name, token);
727
+ meta = {
728
+ stars: r.stargazers_count ?? 0,
729
+ archived: !!r.archived,
730
+ fork: !!r.fork,
731
+ language: r.language ?? null,
732
+ topics: r.topics ?? [],
733
+ contributors
734
+ };
735
+ } catch {
736
+ meta = null;
737
+ }
738
+ cache.set(key, meta);
739
+ return meta;
740
+ }
741
+ function emptyCredential(status) {
742
+ return { status, byDomain: {}, qualifyingTotal: 0, computedAt: (/* @__PURE__ */ new Date()).toISOString() };
743
+ }
744
+ async function fetchPublicOrgs(login, token) {
745
+ try {
746
+ const orgs = await ghFetch(
747
+ `/users/${login}/orgs?per_page=100`,
748
+ token
749
+ );
750
+ return new Set(orgs.map((o) => o.login.toLowerCase()));
751
+ } catch {
752
+ return /* @__PURE__ */ new Set();
753
+ }
754
+ }
755
+ async function computeAcceptanceFromSearch(login, token, ownedOrgs, cache, gates = {
756
+ minStars: MIN_STARS,
757
+ minContributors: MIN_CONTRIBUTORS
758
+ }) {
759
+ const computedAt = (/* @__PURE__ */ new Date()).toISOString();
760
+ const loginLc = login.toLowerCase();
761
+ let items;
762
+ try {
763
+ const q = encodeURIComponent(`type:pr is:merged author:${login} -user:${login} sort:updated`);
764
+ const res = await ghFetch(
765
+ `/search/issues?q=${q}&per_page=${CANDIDATE_PR_PAGE}`,
766
+ token
767
+ );
768
+ items = res.items ?? [];
769
+ } catch (err) {
770
+ const msg = err instanceof Error ? err.message : String(err);
771
+ console.warn("[acceptance] search failed:", msg);
772
+ return emptyCredential(/HTTP 403|HTTP 429|rate limit/i.test(msg) ? "rate-limited" : "failed");
773
+ }
774
+ const byDomain = {};
775
+ let qualifyingTotal = 0;
776
+ for (const item of items) {
777
+ const repo = parseRepoUrl(item.repository_url);
778
+ if (!repo) continue;
779
+ const ownerLc = repo.owner.toLowerCase();
780
+ if (ownerLc === loginLc) continue;
781
+ if (ownedOrgs.has(ownerLc)) continue;
782
+ if (isTrivialPRTitle(item.title)) continue;
783
+ const meta = await fetchRepoMeta(repo.owner, repo.name, token, cache);
784
+ if (!meta) continue;
785
+ if (meta.archived || meta.fork) continue;
786
+ if (meta.stars < gates.minStars) continue;
787
+ if (meta.contributors !== void 0 && meta.contributors < gates.minContributors) continue;
788
+ qualifyingTotal += 1;
789
+ const mergedAt = item.pull_request?.merged_at ?? item.closed_at ?? item.created_at;
790
+ const rawDomains = [meta.language ?? "", ...meta.topics].filter(Boolean);
791
+ for (const d of new Set(normalize(rawDomains))) {
792
+ const b = byDomain[d] ?? (byDomain[d] = { mergedPRs: 0, distinctOrgs: 0, lastMergedAt: mergedAt, orgs: /* @__PURE__ */ new Set() });
793
+ b.mergedPRs += 1;
794
+ b.orgs.add(ownerLc);
795
+ if (mergedAt > b.lastMergedAt) b.lastMergedAt = mergedAt;
796
+ }
797
+ }
798
+ const finalDomains = {};
799
+ for (const [d, b] of Object.entries(byDomain)) {
800
+ finalDomains[d] = {
801
+ mergedPRs: b.mergedPRs,
802
+ distinctOrgs: b.orgs.size,
803
+ lastMergedAt: b.lastMergedAt
804
+ };
805
+ }
806
+ return { status: "ok", byDomain: finalDomains, qualifyingTotal, computedAt };
807
+ }
808
+ async function computeAcceptanceCredential(login, token, cache = /* @__PURE__ */ new Map()) {
809
+ if (!token) return emptyCredential("no-token");
810
+ const ownedOrgs = await fetchOwnedOrgs(token);
811
+ return computeAcceptanceFromSearch(login, token, ownedOrgs, cache);
812
+ }
813
+ async function computeAcceptanceCredentialPublic(login, token, cache = /* @__PURE__ */ new Map(), opts) {
814
+ if (!token) return emptyCredential("no-token");
815
+ const ownedOrgs = await fetchPublicOrgs(login, token);
816
+ for (const org of opts?.includeOrgs ?? []) ownedOrgs.delete(org.toLowerCase());
817
+ const gates = opts?.relaxGates ? { minStars: 0, minContributors: 0 } : void 0;
818
+ return computeAcceptanceFromSearch(login, token, ownedOrgs, cache, gates);
819
+ }
820
+ function acceptanceCountForDomains(cred, domains) {
821
+ if (cred.status !== "ok") return 0;
822
+ let max = 0;
823
+ for (const d of domains) {
824
+ const c = cred.byDomain[d]?.mergedPRs ?? 0;
825
+ if (c > max) max = c;
826
+ }
827
+ return max;
828
+ }
829
+ function bestAcceptanceDomain(cred, domains) {
830
+ if (cred.status !== "ok") return null;
831
+ let best = null;
832
+ for (const d of domains) {
833
+ const count = cred.byDomain[d]?.mergedPRs ?? 0;
834
+ if (count > 0 && (best === null || count > best.count)) best = { domain: d, count };
835
+ }
836
+ return best;
837
+ }
838
+ function resumeRecencyDecay(lastSeenIso, now) {
839
+ const ageMs = now - new Date(lastSeenIso).getTime();
840
+ if (!Number.isFinite(ageMs)) return 0;
841
+ return Math.pow(0.5, ageMs / RESUME_DECAY_HALF_LIFE_MS);
842
+ }
843
+ async function fetchRepoRecency(login, token) {
844
+ try {
845
+ const repos = await ghFetch(`/users/${login}/repos?sort=pushed&per_page=100`, token);
846
+ return repos.filter((r) => !r.fork && !!r.pushed_at).map((r) => ({ pushedAt: r.pushed_at, language: r.language ?? null, topics: r.topics ?? [] }));
847
+ } catch {
848
+ return [];
849
+ }
850
+ }
851
+ function deriveResumeTrend(cred, repoRecency, now = Date.now()) {
852
+ const agg = /* @__PURE__ */ new Map();
853
+ const bump = (domain, when, count, mergedPRs) => {
854
+ const e = agg.get(domain);
855
+ if (!e) {
856
+ agg.set(domain, { count, last: when, earliest: when, mergedPRs });
857
+ } else {
858
+ e.count += count;
859
+ e.mergedPRs += mergedPRs;
860
+ if (when > e.last) e.last = when;
861
+ if (when < e.earliest) e.earliest = when;
862
+ }
863
+ };
864
+ if (cred.status === "ok") {
865
+ for (const [domain, d] of Object.entries(cred.byDomain)) {
866
+ bump(domain, d.lastMergedAt, d.mergedPRs, d.mergedPRs);
387
867
  }
388
868
  }
389
- const idf = /* @__PURE__ */ new Map();
390
- for (const [tag, df] of docFreq) {
391
- idf.set(tag, Math.log((N + 1) / (df + 1)) + 1);
869
+ for (const r of repoRecency) {
870
+ for (const domain of new Set(normalize([r.language ?? "", ...r.topics].filter(Boolean)))) {
871
+ bump(domain, r.pushedAt, 1, 0);
872
+ }
873
+ }
874
+ const oneHalfLifeAgoIso = new Date(now - RESUME_DECAY_HALF_LIFE_MS).toISOString();
875
+ const scored = [];
876
+ for (const [domain, e] of agg.entries()) {
877
+ const recencyScore2 = resumeRecencyDecay(e.last, now);
878
+ const weight = e.count * recencyScore2;
879
+ if (weight < RESUME_MIN_SCORE) continue;
880
+ let direction;
881
+ if (e.earliest > oneHalfLifeAgoIso) direction = "new";
882
+ else if (recencyScore2 >= 0.5) direction = "up";
883
+ else direction = "down";
884
+ scored.push({
885
+ t: { domain, direction, recencyScore: Math.round(recencyScore2 * 1e3) / 1e3, mergedPRs: e.mergedPRs },
886
+ weight
887
+ });
392
888
  }
393
- return idf;
889
+ return scored.sort((a, b) => b.weight - a.weight).slice(0, 12).map((s) => s.t);
394
890
  }
395
- function inferSeniority(title) {
891
+ var MIN_STARS, MIN_CONTRIBUTORS, CANDIDATE_PR_PAGE, TRIVIAL_PR_TITLE, RESUME_DECAY_HALF_LIFE_MS, RESUME_MIN_SCORE;
892
+ var init_github = __esm({
893
+ "../../packages/core/src/github.ts"() {
894
+ "use strict";
895
+ init_vocabulary();
896
+ MIN_STARS = 50;
897
+ MIN_CONTRIBUTORS = 10;
898
+ CANDIDATE_PR_PAGE = 50;
899
+ 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;
900
+ RESUME_DECAY_HALF_LIFE_MS = 30 * 24 * 60 * 60 * 1e3;
901
+ RESUME_MIN_SCORE = 0.05;
902
+ }
903
+ });
904
+
905
+ // ../../packages/core/src/matcher.ts
906
+ function acceptanceDomainsOf(job) {
907
+ return job.coreTags && job.coreTags.length > 0 ? job.coreTags : job.tags;
908
+ }
909
+ function backgroundIdf(tag) {
910
+ const df = IDF_BACKGROUND.df[tag] ?? 0;
911
+ return Math.log((IDF_BACKGROUND.N + 1) / (df + 1)) + 1;
912
+ }
913
+ function inferSeniority2(title) {
396
914
  if (!ENG_TITLE.test(title)) return void 0;
397
915
  for (const [re, level] of SENIORITY_PATTERNS) {
398
916
  if (re.test(title)) return level;
@@ -401,7 +919,7 @@ function inferSeniority(title) {
401
919
  }
402
920
  function seniorityScore(fp, job) {
403
921
  if (!fp.seniorityBand) return 1;
404
- const jobLevel = inferSeniority(job.title);
922
+ const jobLevel = inferSeniority2(job.title);
405
923
  if (!jobLevel) return 0.85;
406
924
  const wanted = SENIORITY_RANK[fp.seniorityBand] ?? 1;
407
925
  const got = SENIORITY_RANK[jobLevel] ?? 1;
@@ -411,8 +929,10 @@ function seniorityScore(fp, job) {
411
929
  return 0.4;
412
930
  }
413
931
  function recencyScore(postedAt, now) {
414
- if (!postedAt) return 0.75;
415
- const ageDays2 = (now - new Date(postedAt).getTime()) / 864e5;
932
+ if (!postedAt) return UNKNOWN_RECENCY;
933
+ const ms = new Date(postedAt).getTime();
934
+ if (Number.isNaN(ms)) return UNKNOWN_RECENCY;
935
+ const ageDays2 = (now - ms) / 864e5;
416
936
  if (ageDays2 < 7) return 1;
417
937
  if (ageDays2 < 30) return 0.9;
418
938
  if (ageDays2 < 90) return 0.75;
@@ -443,9 +963,8 @@ function harmonicMean(a, b) {
443
963
  if (a <= 0 || b <= 0) return 0;
444
964
  return 2 * a * b / (a + b);
445
965
  }
446
- function match(fp, jobs, limit = 5, now = Date.now()) {
447
- const idf = computeIdf(jobs);
448
- const idfOf = (t) => idf.get(t) ?? 0;
966
+ function match(fp, jobs, limit = 5, now = Date.now(), opts = {}) {
967
+ const idfOf = backgroundIdf;
449
968
  const expanded = expandWeighted(fp.skillTags);
450
969
  const maxDevScore = fp.skillTags.reduce((acc, t) => acc + idfOf(t), 0);
451
970
  const candidates = jobs.filter((j) => passesFilters(fp, j));
@@ -471,32 +990,45 @@ function match(fp, jobs, limit = 5, now = Date.now()) {
471
990
  const jobCov = jobMaxScore > 0 ? Math.min(1, jobMatchScore / jobMaxScore) : 0;
472
991
  const tagComponent = harmonicMean(devCov, jobCov);
473
992
  if (tagComponent === 0) return null;
993
+ const coreTags = job.coreTags ?? coreTagsFromTitle(job.title);
994
+ let coreComponent = tagComponent;
995
+ if (coreTags.length > 0) {
996
+ const coreCov = Math.max(0, ...coreTags.map((ct) => expanded.get(ct)?.weight ?? 0));
997
+ if (coreCov === 0) coreComponent = tagComponent * CORE_MISS_PENALTY;
998
+ }
474
999
  details.sort((a, b) => idfOf(b.tag) * b.weight - idfOf(a.tag) * a.weight);
475
1000
  const sScore = seniorityScore(fp, job);
476
1001
  const rScore = recencyScore(job.postedAt, now);
477
- const score = tagComponent * 0.6 + sScore * 0.25 + rScore * 0.15;
1002
+ const score = coreComponent * 0.6 + sScore * 0.25 + rScore * 0.15;
478
1003
  const matchedTags = [...new Set(details.map((d) => d.via ?? d.tag))];
1004
+ const badge = opts.acceptance ? bestAcceptanceDomain(opts.acceptance, acceptanceDomainsOf(job)) : null;
479
1005
  return {
480
1006
  job,
481
1007
  score: Math.round(score * 1e3) / 1e3,
482
1008
  matchedTags,
483
1009
  matchDetails: details,
1010
+ ...badge ? { acceptance: { status: "ok", domain: badge.domain, count: badge.count } } : {},
484
1011
  reason: buildReason(details)
485
1012
  };
486
1013
  });
487
- return scored.filter((r) => r !== null && r.score >= MIN_SCORE).sort((a, b) => b.score - a.score).slice(0, limit);
488
- }
489
- function matchOne(fp, job) {
490
- const results = match(fp, [job], 1);
491
- return results.length > 0 ? results[0] : null;
492
- }
493
- var MIN_SCORE, SHARPEN, SENIORITY_RANK, SENIORITY_PATTERNS, ENG_TITLE;
1014
+ return scored.filter((r) => r !== null && r.score >= MIN_SCORE).sort((a, b) => {
1015
+ const byScore = b.score - a.score;
1016
+ if (Math.abs(byScore) > TIEBREAK_EPS) return byScore;
1017
+ const byAcceptance = (b.acceptance?.count ?? 0) - (a.acceptance?.count ?? 0);
1018
+ if (byAcceptance !== 0) return byAcceptance;
1019
+ return byScore;
1020
+ }).slice(0, limit);
1021
+ }
1022
+ var MIN_SCORE, TIEBREAK_EPS, SHARPEN, CORE_MISS_PENALTY, SENIORITY_RANK, SENIORITY_PATTERNS, ENG_TITLE, UNKNOWN_RECENCY;
494
1023
  var init_matcher = __esm({
495
1024
  "../../packages/core/src/matcher.ts"() {
496
1025
  "use strict";
497
1026
  init_vocabulary();
1027
+ init_github();
498
1028
  MIN_SCORE = 0.15;
1029
+ TIEBREAK_EPS = 5e-3;
499
1030
  SHARPEN = 1.6;
1031
+ CORE_MISS_PENALTY = 0.4;
500
1032
  SENIORITY_RANK = {
501
1033
  junior: 0,
502
1034
  mid: 1,
@@ -510,24 +1042,31 @@ var init_matcher = __esm({
510
1042
  [/\bmid[\s-]?level\b|\bmid\b/i, "mid"]
511
1043
  ];
512
1044
  ENG_TITLE = /\b(engineer|engineering|developer|dev|swe|sde|programmer|architect)\b/i;
1045
+ UNKNOWN_RECENCY = 0.75;
513
1046
  }
514
1047
  });
515
1048
 
516
- // ../../packages/core/src/feeds/greenhouse.ts
517
- function tokenize(text) {
518
- return text.toLowerCase().replace(/[^a-z0-9.\-+#]/g, " ").split(/\s+/).filter(Boolean);
1049
+ // ../../packages/core/src/feeds/http.ts
1050
+ function fetchWithTimeout(input, init, timeoutMs = FEED_FETCH_TIMEOUT_MS) {
1051
+ return fetch(input, { ...init, signal: AbortSignal.timeout(timeoutMs) });
519
1052
  }
1053
+ var FEED_FETCH_TIMEOUT_MS;
1054
+ var init_http = __esm({
1055
+ "../../packages/core/src/feeds/http.ts"() {
1056
+ "use strict";
1057
+ FEED_FETCH_TIMEOUT_MS = 1e4;
1058
+ }
1059
+ });
1060
+
1061
+ // ../../packages/core/src/feeds/greenhouse.ts
520
1062
  function extractTags(job) {
521
- const texts = [
522
- job.title,
1063
+ const body = [
523
1064
  ...(job.departments ?? []).map((d) => d.name),
524
1065
  job.location?.name ?? "",
525
1066
  ...(job.offices ?? []).map((o) => o.name),
526
- // mine the full HTML description for additional signal when present
527
1067
  ...job.content ? [job.content.replace(/<[^>]*>/g, " ")] : []
528
- ].filter(Boolean);
529
- const tokens = texts.flatMap(tokenize);
530
- return normalize(tokens);
1068
+ ].filter(Boolean).join(" ");
1069
+ return extractSkillTags(job.title, body);
531
1070
  }
532
1071
  function inferRemote(location) {
533
1072
  const l = location.toLowerCase();
@@ -537,7 +1076,7 @@ async function fetchSlug(slug) {
537
1076
  const url = `https://boards-api.greenhouse.io/v1/boards/${slug}/jobs?content=true`;
538
1077
  let res;
539
1078
  try {
540
- res = await fetch(url, { headers: { Accept: "application/json" } });
1079
+ res = await fetchWithTimeout(url, { headers: { Accept: "application/json" } });
541
1080
  } catch (err) {
542
1081
  console.warn(`[greenhouse] ${slug}: network error \u2014`, err);
543
1082
  return [];
@@ -579,6 +1118,7 @@ var init_greenhouse = __esm({
579
1118
  "../../packages/core/src/feeds/greenhouse.ts"() {
580
1119
  "use strict";
581
1120
  init_vocabulary();
1121
+ init_http();
582
1122
  FALLBACK_SLUGS = [
583
1123
  "stripe",
584
1124
  "linear",
@@ -625,17 +1165,15 @@ var init_greenhouse = __esm({
625
1165
  });
626
1166
 
627
1167
  // ../../packages/core/src/feeds/ashby.ts
628
- function tokenize2(text) {
629
- return text.toLowerCase().replace(/[^a-z0-9.\-+#]/g, " ").split(/\s+/).filter(Boolean);
630
- }
631
1168
  function extractTags2(job) {
632
- const texts = [
633
- job.title,
634
- job.teamName ?? "",
635
- job.locationName ?? "",
636
- ...(job.secondaryLocations ?? []).map((l) => l.locationName ?? "")
637
- ];
638
- return normalize(texts.flatMap(tokenize2));
1169
+ const body = [
1170
+ job.team ?? "",
1171
+ job.department ?? "",
1172
+ job.location ?? "",
1173
+ ...(job.secondaryLocations ?? []).map((l) => l.location ?? ""),
1174
+ job.descriptionPlain ?? ""
1175
+ ].join(" ");
1176
+ return extractSkillTags(job.title, body);
639
1177
  }
640
1178
  function mapEmploymentType(raw) {
641
1179
  if (!raw) return "full_time";
@@ -646,12 +1184,12 @@ function mapEmploymentType(raw) {
646
1184
  }
647
1185
  function inferRemote2(job) {
648
1186
  if (job.isRemote === true) return true;
649
- const loc = (job.locationName ?? "").toLowerCase();
1187
+ const loc = (job.location ?? "").toLowerCase();
650
1188
  return loc.includes("remote") || loc.includes("anywhere");
651
1189
  }
652
1190
  async function fetchSlug2(slug) {
653
1191
  const url = `https://api.ashbyhq.com/posting-api/job-board/${slug}`;
654
- const res = await fetch(url, {
1192
+ const res = await fetchWithTimeout(url, {
655
1193
  headers: { Accept: "application/json" }
656
1194
  });
657
1195
  if (!res.ok) {
@@ -665,14 +1203,14 @@ async function fetchSlug2(slug) {
665
1203
  source: "ashby",
666
1204
  title: j.title,
667
1205
  company: slug,
668
- url: j.applyUrl ?? `https://jobs.ashbyhq.com/${slug}/${j.id}`,
1206
+ url: j.jobUrl ?? j.applyUrl ?? `https://jobs.ashbyhq.com/${slug}/${j.id}`,
669
1207
  remote: inferRemote2(j),
670
- location: j.locationName,
1208
+ location: j.location,
671
1209
  compMin: comp?.minValue,
672
1210
  compMax: comp?.maxValue,
673
1211
  tags: extractTags2(j),
674
1212
  roleType: mapEmploymentType(j.employmentType),
675
- postedAt: j.publishedDate,
1213
+ postedAt: j.publishedAt,
676
1214
  applyMode: "direct",
677
1215
  raw: j
678
1216
  };
@@ -683,6 +1221,7 @@ var init_ashby = __esm({
683
1221
  "../../packages/core/src/feeds/ashby.ts"() {
684
1222
  "use strict";
685
1223
  init_vocabulary();
1224
+ init_http();
686
1225
  ashby = {
687
1226
  source: "ashby",
688
1227
  async fetch(opts) {
@@ -699,20 +1238,16 @@ var init_ashby = __esm({
699
1238
  });
700
1239
 
701
1240
  // ../../packages/core/src/feeds/lever.ts
702
- function tokenize3(text) {
703
- return text.toLowerCase().replace(/[^a-z0-9.\-+#]/g, " ").split(/\s+/).filter(Boolean);
704
- }
705
1241
  function extractTags3(p) {
706
1242
  const cat = p.categories ?? {};
707
- const texts = [
708
- p.text,
1243
+ const body = [
709
1244
  cat.team ?? "",
710
1245
  cat.department ?? "",
711
1246
  cat.location ?? "",
712
1247
  ...cat.allLocations ?? [],
713
1248
  p.descriptionPlain ?? ""
714
- ];
715
- return normalize(texts.flatMap(tokenize3));
1249
+ ].join(" ");
1250
+ return extractSkillTags(p.text, body);
716
1251
  }
717
1252
  function mapCommitment(raw) {
718
1253
  if (!raw) return "full_time";
@@ -737,7 +1272,7 @@ function toIso(ms) {
737
1272
  }
738
1273
  async function fetchSlug3(slug) {
739
1274
  const url = `https://api.lever.co/v0/postings/${slug}?mode=json`;
740
- const res = await fetch(url, { headers: { Accept: "application/json" } });
1275
+ const res = await fetchWithTimeout(url, { headers: { Accept: "application/json" } });
741
1276
  if (!res.ok) {
742
1277
  throw new Error(`Lever ${slug}: HTTP ${res.status}`);
743
1278
  }
@@ -768,6 +1303,7 @@ var init_lever = __esm({
768
1303
  "../../packages/core/src/feeds/lever.ts"() {
769
1304
  "use strict";
770
1305
  init_vocabulary();
1306
+ init_http();
771
1307
  lever = {
772
1308
  source: "lever",
773
1309
  async fetch(opts) {
@@ -785,15 +1321,8 @@ var init_lever = __esm({
785
1321
  });
786
1322
 
787
1323
  // ../../packages/core/src/feeds/himalayas.ts
788
- function tokenize4(text) {
789
- return text.toLowerCase().replace(/[^a-z0-9.\-+#]/g, " ").split(/\s+/).filter(Boolean);
790
- }
791
1324
  function extractTags4(job) {
792
- const texts = [
793
- job.title,
794
- ...job.tags ?? []
795
- ];
796
- return normalize(texts.flatMap(tokenize4));
1325
+ return extractSkillTags(job.title, (job.tags ?? []).join(" "));
797
1326
  }
798
1327
  function mapJobType(raw) {
799
1328
  if (!raw) return "full_time";
@@ -816,12 +1345,13 @@ var init_himalayas = __esm({
816
1345
  "../../packages/core/src/feeds/himalayas.ts"() {
817
1346
  "use strict";
818
1347
  init_vocabulary();
1348
+ init_http();
819
1349
  himalayas = {
820
1350
  source: "himalayas",
821
1351
  async fetch(opts) {
822
1352
  const limit = opts?.limit ?? 100;
823
1353
  const url = `https://himalayas.app/jobs/api?limit=${limit}`;
824
- const res = await fetch(url, {
1354
+ const res = await fetchWithTimeout(url, {
825
1355
  headers: { Accept: "application/json" }
826
1356
  });
827
1357
  if (!res.ok) {
@@ -878,9 +1408,6 @@ var init_entities = __esm({
878
1408
  });
879
1409
 
880
1410
  // ../../packages/core/src/feeds/wwr.ts
881
- function tokenize5(text) {
882
- return text.toLowerCase().replace(/[^a-z0-9.\-+#]/g, " ").split(/\s+/).filter(Boolean);
883
- }
884
1411
  function stripHtml(html) {
885
1412
  return html.replace(/<[^>]*>/g, " ").replace(/\s+/g, " ").trim();
886
1413
  }
@@ -924,8 +1451,8 @@ function parseRss(xml) {
924
1451
  return items;
925
1452
  }
926
1453
  function extractTags5(item) {
927
- const text = [item.title, item.category, stripHtml(item.description)].join(" ");
928
- return normalize(tokenize5(text));
1454
+ const body = [item.category, stripHtml(item.description)].join(" ");
1455
+ return extractSkillTags(item.title, body);
929
1456
  }
930
1457
  var WWR_RSS_URL, wwr;
931
1458
  var init_wwr = __esm({
@@ -933,12 +1460,13 @@ var init_wwr = __esm({
933
1460
  "use strict";
934
1461
  init_vocabulary();
935
1462
  init_entities();
1463
+ init_http();
936
1464
  WWR_RSS_URL = "https://weworkremotely.com/remote-jobs.rss";
937
1465
  wwr = {
938
1466
  source: "wwr",
939
1467
  async fetch(opts) {
940
1468
  const limit = opts?.limit ?? 200;
941
- const res = await fetch(WWR_RSS_URL, {
1469
+ const res = await fetchWithTimeout(WWR_RSS_URL, {
942
1470
  headers: { Accept: "application/rss+xml, application/xml, text/xml" }
943
1471
  });
944
1472
  if (!res.ok) {
@@ -946,6 +1474,11 @@ var init_wwr = __esm({
946
1474
  }
947
1475
  const xml = await res.text();
948
1476
  const items = parseRss(xml).slice(0, limit);
1477
+ function safeIso(s) {
1478
+ if (!s) return void 0;
1479
+ const d = new Date(s);
1480
+ return Number.isNaN(d.getTime()) ? void 0 : d.toISOString();
1481
+ }
949
1482
  return items.map((item) => ({
950
1483
  id: extractId(item.link),
951
1484
  source: "wwr",
@@ -957,7 +1490,7 @@ var init_wwr = __esm({
957
1490
  location: "Remote",
958
1491
  tags: extractTags5(item),
959
1492
  roleType: inferRoleType(item.category),
960
- postedAt: item.pubDate ? new Date(item.pubDate).toISOString() : void 0,
1493
+ postedAt: safeIso(item.pubDate),
961
1494
  applyMode: "direct",
962
1495
  raw: item
963
1496
  }));
@@ -967,9 +1500,6 @@ var init_wwr = __esm({
967
1500
  });
968
1501
 
969
1502
  // ../../packages/core/src/feeds/hn.ts
970
- function tokenize6(text) {
971
- return text.toLowerCase().replace(/[^a-z0-9.\-+#]/g, " ").split(/\s+/).filter(Boolean);
972
- }
973
1503
  function stripHtml2(html) {
974
1504
  return decodeEntities(html.replace(/<p>/gi, " ").replace(/<[^>]*>/g, "")).replace(/\s+/g, " ").trim();
975
1505
  }
@@ -1000,7 +1530,7 @@ function parseComment(item) {
1000
1530
  return null;
1001
1531
  }
1002
1532
  const url = extractUrl(raw) || `https://news.ycombinator.com/item?id=${item.id}`;
1003
- const tags = extractTags6(raw);
1533
+ const tags = extractTags6(title, raw);
1004
1534
  if (tags.length === 0) return null;
1005
1535
  return {
1006
1536
  id: `hn:${item.id}`,
@@ -1017,8 +1547,8 @@ function parseComment(item) {
1017
1547
  raw: item
1018
1548
  };
1019
1549
  }
1020
- function extractTags6(text) {
1021
- return normalize(tokenize6(text));
1550
+ function extractTags6(title, text) {
1551
+ return extractSkillTags(title, text);
1022
1552
  }
1023
1553
  var ALGOLIA_SEARCH, ALGOLIA_ITEMS, hn;
1024
1554
  var init_hn = __esm({
@@ -1026,13 +1556,14 @@ var init_hn = __esm({
1026
1556
  "use strict";
1027
1557
  init_vocabulary();
1028
1558
  init_entities();
1559
+ init_http();
1029
1560
  ALGOLIA_SEARCH = "https://hn.algolia.com/api/v1/search?query=Ask+HN%3A+Who+is+Hiring%3F&tags=story,ask_hn&hitsPerPage=1";
1030
1561
  ALGOLIA_ITEMS = "https://hn.algolia.com/api/v1/items/";
1031
1562
  hn = {
1032
1563
  source: "hn",
1033
1564
  async fetch(opts) {
1034
1565
  const limit = opts?.limit ?? 150;
1035
- const searchRes = await fetch(ALGOLIA_SEARCH, {
1566
+ const searchRes = await fetchWithTimeout(ALGOLIA_SEARCH, {
1036
1567
  headers: { Accept: "application/json" }
1037
1568
  });
1038
1569
  if (!searchRes.ok) {
@@ -1043,7 +1574,7 @@ var init_hn = __esm({
1043
1574
  if (!story) {
1044
1575
  throw new Error('HN: No "Who is Hiring" story found');
1045
1576
  }
1046
- const itemRes = await fetch(`${ALGOLIA_ITEMS}${story.objectID}`, {
1577
+ const itemRes = await fetchWithTimeout(`${ALGOLIA_ITEMS}${story.objectID}`, {
1047
1578
  headers: { Accept: "application/json" }
1048
1579
  });
1049
1580
  if (!itemRes.ok) {
@@ -1108,7 +1639,7 @@ function authHeaders() {
1108
1639
  if (token) h["Authorization"] = `Bearer ${token}`;
1109
1640
  return h;
1110
1641
  }
1111
- function tokenize7(text) {
1642
+ function tokenize2(text) {
1112
1643
  return text.toLowerCase().replace(/[^a-z0-9.\-+#]/g, " ").split(/\s+/).filter(Boolean);
1113
1644
  }
1114
1645
  function parseAmountUSD(text) {
@@ -1137,7 +1668,7 @@ function isBountyIssue(issue) {
1137
1668
  async function ghJson(path) {
1138
1669
  let res;
1139
1670
  try {
1140
- res = await fetch(`${GITHUB_API}${path}`, { headers: authHeaders() });
1671
+ res = await fetchWithTimeout(`${GITHUB_API}${path}`, { headers: authHeaders() });
1141
1672
  } catch (err) {
1142
1673
  console.warn(`[github-bounties] network error ${path} \u2014`, err);
1143
1674
  return null;
@@ -1193,7 +1724,7 @@ async function fetchRepoBounties(repoFullName) {
1193
1724
  const body = issue.body ? decodeEntities(issue.body) : "";
1194
1725
  const amountUSD = parseAmountUSD(title) ?? parseAmountUSD(body) ?? await fetchCommentAmount(repoFullName, issue.number);
1195
1726
  const labels = labelNames(issue);
1196
- const tags = normalize(tokenize7([title, labels.join(" "), body.slice(0, 2e3)].join(" ")));
1727
+ const tags = normalize(tokenize2([title, labels.join(" "), body.slice(0, 2e3)].join(" ")));
1197
1728
  return {
1198
1729
  id: `bounty:${repoFullName}#${issue.number}`,
1199
1730
  source: "bounty",
@@ -1219,31 +1750,328 @@ async function fetchRepoBounties(repoFullName) {
1219
1750
  };
1220
1751
  }));
1221
1752
  }
1222
- var GITHUB_API, BOUNTY_LABEL_RE, githubBounties;
1753
+ function repoFullNameFromApiUrl(url) {
1754
+ const m = url.match(/\/repos\/([^/]+)\/([^/]+)\/?$/);
1755
+ return m ? `${m[1]}/${m[2]}` : null;
1756
+ }
1757
+ async function searchBountyIssues() {
1758
+ const byUrl = /* @__PURE__ */ new Map();
1759
+ for (const q of SEARCH_QUERIES) {
1760
+ const res = await ghJson(
1761
+ `/search/issues?q=${encodeURIComponent(q)}&sort=created&order=desc&per_page=${SEARCH_PER_PAGE}`
1762
+ );
1763
+ for (const it of res?.items ?? []) {
1764
+ if (it.pull_request) continue;
1765
+ if (!byUrl.has(it.html_url)) byUrl.set(it.html_url, it);
1766
+ }
1767
+ }
1768
+ return [...byUrl.values()].sort(
1769
+ (a, b) => new Date(b.created_at).getTime() - new Date(a.created_at).getTime()
1770
+ );
1771
+ }
1772
+ async function repoMetaCached(fullName) {
1773
+ const hit = repoMetaCache.get(fullName);
1774
+ if (hit !== void 0) return hit;
1775
+ const r = await ghJson(`/repos/${fullName}`) ?? null;
1776
+ repoMetaCache.set(fullName, r);
1777
+ return r;
1778
+ }
1779
+ async function fetchSearchBounties() {
1780
+ const issues = (await searchBountyIssues()).slice(0, MAX_SEARCH_ISSUES_SCANNED);
1781
+ const distinctRepos = [
1782
+ ...new Set(
1783
+ issues.map((i) => repoFullNameFromApiUrl(i.repository_url)).filter((x) => !!x)
1784
+ )
1785
+ ];
1786
+ for (let i = 0; i < distinctRepos.length; i += REPO_META_CONCURRENCY) {
1787
+ await Promise.all(distinctRepos.slice(i, i + REPO_META_CONCURRENCY).map(repoMetaCached));
1788
+ }
1789
+ const jobs = [];
1790
+ const perRepo = /* @__PURE__ */ new Map();
1791
+ for (const issue of issues) {
1792
+ if (jobs.length >= MAX_SEARCH_BOUNTIES) break;
1793
+ const fullName = repoFullNameFromApiUrl(issue.repository_url);
1794
+ if (!fullName) continue;
1795
+ if ((perRepo.get(fullName) ?? 0) >= MAX_BOUNTIES_PER_REPO) continue;
1796
+ const repo = await repoMetaCached(fullName);
1797
+ if (!repo) continue;
1798
+ const passes = passesMaturityGate({
1799
+ fullName: repo.full_name,
1800
+ stargazers: repo.stargazers_count,
1801
+ createdAt: repo.created_at,
1802
+ archived: repo.archived,
1803
+ disabled: repo.disabled
1804
+ });
1805
+ if (!passes) continue;
1806
+ const title = decodeEntities(issue.title).trim();
1807
+ const body = issue.body ? decodeEntities(issue.body) : "";
1808
+ const labels = labelNames(issue);
1809
+ let amountUSD = parseAmountUSD(title) ?? parseAmountUSD(labels.join(" ")) ?? parseAmountUSD(body);
1810
+ if (amountUSD == null && labels.some((n) => /💎|💰/.test(n))) {
1811
+ amountUSD = await fetchCommentAmount(fullName, issue.number);
1812
+ }
1813
+ if (amountUSD == null) continue;
1814
+ if (amountUSD > SEARCH_HIGH_VALUE_USD && repo.stargazers_count < SEARCH_HIGH_VALUE_MIN_STARS) continue;
1815
+ const tags = normalize(
1816
+ tokenize2([title, labels.join(" "), body.slice(0, 2e3)].join(" "))
1817
+ );
1818
+ perRepo.set(fullName, (perRepo.get(fullName) ?? 0) + 1);
1819
+ jobs.push({
1820
+ id: `bounty:${fullName}#${issue.number}`,
1821
+ source: "bounty",
1822
+ title,
1823
+ company: repo.owner.login,
1824
+ url: issue.html_url,
1825
+ remote: true,
1826
+ location: "Remote",
1827
+ tags,
1828
+ roleType: "freelance",
1829
+ postedAt: issue.created_at,
1830
+ applyMode: "direct",
1831
+ bounty: {
1832
+ amountUSD,
1833
+ estimatedEffort: effortFromAmount(amountUSD),
1834
+ bountySource: "github",
1835
+ claimUrl: issue.html_url,
1836
+ repoFullName: fullName,
1837
+ repoStars: repo.stargazers_count,
1838
+ issueBody: body.slice(0, 1e3) || void 0
1839
+ },
1840
+ raw: issue
1841
+ });
1842
+ }
1843
+ return jobs;
1844
+ }
1845
+ var GITHUB_API, BOUNTY_LABEL_RE, SEARCH_QUERIES, SEARCH_PER_PAGE, MAX_SEARCH_BOUNTIES, MAX_SEARCH_ISSUES_SCANNED, REPO_META_CONCURRENCY, SEARCH_HIGH_VALUE_USD, SEARCH_HIGH_VALUE_MIN_STARS, repoMetaCache, githubBounties;
1223
1846
  var init_github_bounties = __esm({
1224
1847
  "../../packages/core/src/feeds/github-bounties.ts"() {
1225
1848
  "use strict";
1226
1849
  init_vocabulary();
1227
1850
  init_entities();
1228
1851
  init_bounty_gate();
1852
+ init_http();
1229
1853
  GITHUB_API = "https://api.github.com";
1230
1854
  BOUNTY_LABEL_RE = /bounty|reward|funded|💎|💰/i;
1855
+ SEARCH_QUERIES = [
1856
+ 'label:"\u{1F48E} Bounty" type:issue state:open',
1857
+ // Algora-applied — highest signal
1858
+ "label:bounty type:issue state:open",
1859
+ 'label:"\u{1F4B0} Bounty" type:issue state:open'
1860
+ ];
1861
+ SEARCH_PER_PAGE = 100;
1862
+ MAX_SEARCH_BOUNTIES = 150;
1863
+ MAX_SEARCH_ISSUES_SCANNED = 300;
1864
+ REPO_META_CONCURRENCY = 15;
1865
+ SEARCH_HIGH_VALUE_USD = 500;
1866
+ SEARCH_HIGH_VALUE_MIN_STARS = 50;
1867
+ repoMetaCache = /* @__PURE__ */ new Map();
1231
1868
  githubBounties = {
1232
1869
  source: "bounty",
1233
1870
  async fetch(opts) {
1234
- const repos = opts?.slugs && opts.slugs.length > 0 ? opts.slugs : DEFAULT_BOUNTY_REPOS;
1235
- console.info(`[github-bounties] scanning ${repos.length} repos`);
1236
- const settled = await Promise.allSettled(repos.map(fetchRepoBounties));
1871
+ const allowlist = opts?.slugs && opts.slugs.length > 0 ? opts.slugs : DEFAULT_BOUNTY_REPOS;
1872
+ const [searched, listed] = await Promise.all([
1873
+ fetchSearchBounties().catch((e) => {
1874
+ console.warn("[github-bounties] search discovery failed:", e);
1875
+ return [];
1876
+ }),
1877
+ Promise.allSettled(allowlist.map(fetchRepoBounties)).then(
1878
+ (settled) => settled.flatMap((r) => r.status === "fulfilled" ? r.value : [])
1879
+ )
1880
+ ]);
1881
+ const seen = /* @__PURE__ */ new Set();
1882
+ const out = [];
1883
+ for (const j of [...searched, ...listed]) {
1884
+ if (!seen.has(j.id)) {
1885
+ seen.add(j.id);
1886
+ out.push(j);
1887
+ }
1888
+ }
1889
+ console.info(
1890
+ `[github-bounties] total: ${out.length} bounties (${searched.length} search + ${listed.length} allowlist, deduped)`
1891
+ );
1892
+ return out;
1893
+ }
1894
+ };
1895
+ }
1896
+ });
1897
+
1898
+ // ../../packages/core/src/feeds/opire.ts
1899
+ function tokenize3(text) {
1900
+ return text.toLowerCase().replace(/[^a-z0-9.\-+#]/g, " ").split(/\s+/).filter((w) => w.length > 1);
1901
+ }
1902
+ function effortFromAmount2(usd) {
1903
+ if (usd == null) return void 0;
1904
+ if (usd < 150) return "small";
1905
+ if (usd < 750) return "medium";
1906
+ return "large";
1907
+ }
1908
+ function priceToUSD(p) {
1909
+ if (!p || typeof p.value !== "number") return void 0;
1910
+ if (p.unit === "USD_CENT") return Math.round(p.value) / 100;
1911
+ if (p.unit === "USD") return p.value;
1912
+ return void 0;
1913
+ }
1914
+ function repoFullNameFromUrl(url) {
1915
+ const m = url?.match(/github\.com\/([^/]+)\/([^/]+)/i);
1916
+ return m ? `${m[1]}/${m[2].replace(/\.git$/, "")}` : void 0;
1917
+ }
1918
+ var OPIRE_REWARDS_URL, MIN_USD, MAX_USD, MAX_OPIRE_BOUNTIES, opire;
1919
+ var init_opire = __esm({
1920
+ "../../packages/core/src/feeds/opire.ts"() {
1921
+ "use strict";
1922
+ init_vocabulary();
1923
+ init_http();
1924
+ OPIRE_REWARDS_URL = "https://api.opire.dev/rewards";
1925
+ MIN_USD = 25;
1926
+ MAX_USD = 25e3;
1927
+ MAX_OPIRE_BOUNTIES = 100;
1928
+ opire = {
1929
+ source: "bounty",
1930
+ async fetch() {
1931
+ let rewards;
1932
+ try {
1933
+ const res = await fetchWithTimeout(OPIRE_REWARDS_URL, {
1934
+ headers: { Accept: "application/json", "User-Agent": "terminalhire" }
1935
+ });
1936
+ if (!res.ok) {
1937
+ console.warn(`[opire] HTTP ${res.status}`);
1938
+ return [];
1939
+ }
1940
+ const json = await res.json();
1941
+ rewards = Array.isArray(json) ? json : json?.data ?? json?.items ?? [];
1942
+ } catch (err) {
1943
+ console.warn("[opire] fetch failed \u2014", err);
1944
+ return [];
1945
+ }
1946
+ const jobs = [];
1947
+ for (const r of rewards) {
1948
+ if (r.platform !== "GitHub") continue;
1949
+ if (r.project && r.project.isPublic === false) continue;
1950
+ const repoFullName = repoFullNameFromUrl(r.project?.url ?? r.url);
1951
+ if (!repoFullName) continue;
1952
+ const amountUSD = priceToUSD(r.pendingPrice);
1953
+ if (amountUSD == null || amountUSD < MIN_USD || amountUSD > MAX_USD) continue;
1954
+ const title = (r.title ?? "").trim();
1955
+ if (title.length < 4) continue;
1956
+ const tags = normalize([...r.programmingLanguages ?? [], ...tokenize3(title)]);
1957
+ jobs.push({
1958
+ id: `bounty:opire:${r.id}`,
1959
+ source: "bounty",
1960
+ title,
1961
+ company: r.organization?.name ?? repoFullName.split("/")[0],
1962
+ url: r.url,
1963
+ remote: true,
1964
+ location: "Remote",
1965
+ tags,
1966
+ roleType: "freelance",
1967
+ postedAt: Number.isFinite(r.createdAt) ? new Date(r.createdAt).toISOString() : void 0,
1968
+ applyMode: "direct",
1969
+ bounty: {
1970
+ amountUSD,
1971
+ estimatedEffort: effortFromAmount2(amountUSD),
1972
+ bountySource: "opire",
1973
+ claimUrl: r.url,
1974
+ repoFullName
1975
+ },
1976
+ raw: r
1977
+ });
1978
+ if (jobs.length >= MAX_OPIRE_BOUNTIES) break;
1979
+ }
1980
+ console.info(`[opire] ${jobs.length} bounties (from ${rewards.length} rewards)`);
1981
+ return jobs;
1982
+ }
1983
+ };
1984
+ }
1985
+ });
1986
+
1987
+ // ../../packages/core/src/feeds/workable.ts
1988
+ function locationStr(loc) {
1989
+ if (!loc) return "";
1990
+ return [loc.city, loc.country].filter(Boolean).join(", ");
1991
+ }
1992
+ function isRemote(j) {
1993
+ return j.remote === true || (j.workplace ?? "").toLowerCase() === "remote";
1994
+ }
1995
+ function extractTags7(j) {
1996
+ const body = [...j.department ?? [], locationStr(j.location)].filter(Boolean).join(" ");
1997
+ return extractSkillTags(j.title, body);
1998
+ }
1999
+ async function fetchAccount(account) {
2000
+ const url = `https://apply.workable.com/api/v3/accounts/${account}/jobs`;
2001
+ const out = [];
2002
+ let token;
2003
+ for (let page = 0; page < MAX_PAGES; page++) {
2004
+ let res;
2005
+ try {
2006
+ res = await fetchWithTimeout(url, {
2007
+ method: "POST",
2008
+ headers: { "Content-Type": "application/json", Accept: "application/json" },
2009
+ body: JSON.stringify(token ? { token } : {})
2010
+ });
2011
+ } catch (err) {
2012
+ console.warn(`[workable] ${account}: network error \u2014`, err);
2013
+ break;
2014
+ }
2015
+ if (!res.ok) {
2016
+ console.warn(`[workable] ${account}: HTTP ${res.status}`);
2017
+ break;
2018
+ }
2019
+ let data;
2020
+ try {
2021
+ data = await res.json();
2022
+ } catch (err) {
2023
+ console.warn(`[workable] ${account}: JSON parse error \u2014`, err);
2024
+ break;
2025
+ }
2026
+ const results = data.results ?? [];
2027
+ for (const j of results) {
2028
+ if (j.state && j.state !== "published") continue;
2029
+ out.push({
2030
+ id: `workable:${j.id}`,
2031
+ source: "workable",
2032
+ title: j.title,
2033
+ company: account,
2034
+ url: `https://apply.workable.com/${account}/j/${j.shortcode}/`,
2035
+ remote: isRemote(j),
2036
+ location: locationStr(j.location) || void 0,
2037
+ tags: extractTags7(j),
2038
+ roleType: "full_time",
2039
+ postedAt: j.published,
2040
+ applyMode: "direct",
2041
+ raw: j
2042
+ });
2043
+ }
2044
+ token = data.token;
2045
+ if (!token || results.length === 0) break;
2046
+ }
2047
+ if (out.length > 0) console.info(`[workable] ${account}: ${out.length} jobs`);
2048
+ return out;
2049
+ }
2050
+ var FALLBACK_ACCOUNTS, MAX_PAGES, workable;
2051
+ var init_workable = __esm({
2052
+ "../../packages/core/src/feeds/workable.ts"() {
2053
+ "use strict";
2054
+ init_vocabulary();
2055
+ init_http();
2056
+ FALLBACK_ACCOUNTS = ["zego", "workmotion"];
2057
+ MAX_PAGES = 5;
2058
+ workable = {
2059
+ source: "workable",
2060
+ async fetch(opts) {
2061
+ const accounts = opts?.slugs && opts.slugs.length > 0 ? opts.slugs : FALLBACK_ACCOUNTS;
2062
+ console.info(`[workable] fetching ${accounts.length} accounts: ${accounts.join(", ")}`);
2063
+ const results = await Promise.allSettled(accounts.map(fetchAccount));
1237
2064
  const jobs = [];
1238
2065
  let failures = 0;
1239
- for (const r of settled) {
1240
- if (r.status === "fulfilled") jobs.push(...r.value);
1241
- else {
2066
+ for (const r of results) {
2067
+ if (r.status === "fulfilled") {
2068
+ jobs.push(...r.value);
2069
+ } else {
1242
2070
  failures++;
1243
- console.warn("[github-bounties] repo fetch rejected:", r.reason);
2071
+ console.warn("[workable] account fetch rejected:", r.reason);
1244
2072
  }
1245
2073
  }
1246
- console.info(`[github-bounties] total: ${jobs.length} bounties, ${failures} repo failures`);
2074
+ console.info(`[workable] total: ${jobs.length} jobs, ${failures} account failures`);
1247
2075
  return jobs;
1248
2076
  }
1249
2077
  };
@@ -1252,7 +2080,19 @@ var init_github_bounties = __esm({
1252
2080
 
1253
2081
  // ../../packages/core/src/feeds/index.ts
1254
2082
  async function aggregateBounties(opts) {
1255
- return githubBounties.fetch({ slugs: opts?.repos });
2083
+ const [gh, op] = await Promise.all([
2084
+ githubBounties.fetch({ slugs: opts?.repos }),
2085
+ opire.fetch()
2086
+ ]);
2087
+ const seen = /* @__PURE__ */ new Set();
2088
+ const out = [];
2089
+ for (const j of [...gh, ...op]) {
2090
+ const key = j.bounty?.claimUrl ?? j.url;
2091
+ if (seen.has(key)) continue;
2092
+ seen.add(key);
2093
+ out.push(j);
2094
+ }
2095
+ return out;
1256
2096
  }
1257
2097
  function flattenTiers(t) {
1258
2098
  return [.../* @__PURE__ */ new Set([...t.bigco, ...t.scaleup, ...t.startup])];
@@ -1261,18 +2101,20 @@ async function aggregate(opts) {
1261
2101
  const ghSlugs = opts?.slugs?.["greenhouse"] ?? DEFAULT_GREENHOUSE_SLUGS;
1262
2102
  const ashbySlugs = opts?.slugs?.["ashby"] ?? DEFAULT_ASHBY_SLUGS;
1263
2103
  const leverSlugs = opts?.slugs?.["lever"] ?? DEFAULT_LEVER_SLUGS;
2104
+ const workableSlugs = opts?.slugs?.["workable"] ?? DEFAULT_WORKABLE_SLUGS;
1264
2105
  const limit = opts?.limit ?? 150;
1265
2106
  const settled = await Promise.allSettled([
1266
2107
  greenhouse.fetch({ slugs: ghSlugs, limit }),
1267
2108
  ashby.fetch({ slugs: ashbySlugs, limit }),
1268
2109
  lever.fetch({ slugs: leverSlugs, limit }),
2110
+ workable.fetch({ slugs: workableSlugs, limit }),
1269
2111
  himalayas.fetch({ limit }),
1270
2112
  wwr.fetch({ limit }),
1271
2113
  hn.fetch({ limit })
1272
2114
  ]);
1273
2115
  const seen = /* @__PURE__ */ new Set();
1274
2116
  const jobs = [];
1275
- const sourceNames = ["greenhouse", "ashby", "lever", "himalayas", "wwr", "hn"];
2117
+ const sourceNames = ["greenhouse", "ashby", "lever", "workable", "himalayas", "wwr", "hn"];
1276
2118
  for (let i = 0; i < settled.length; i++) {
1277
2119
  const result = settled[i];
1278
2120
  if (result.status === "rejected") {
@@ -1288,7 +2130,7 @@ async function aggregate(opts) {
1288
2130
  }
1289
2131
  if (opts?.includeBounties !== false) {
1290
2132
  try {
1291
- const bounties = await githubBounties.fetch({ slugs: opts?.slugs?.["bounty"], limit });
2133
+ const bounties = await aggregateBounties({ repos: opts?.slugs?.["bounty"] });
1292
2134
  for (const b of bounties) {
1293
2135
  if (!seen.has(b.id)) {
1294
2136
  seen.add(b.id);
@@ -1301,7 +2143,7 @@ async function aggregate(opts) {
1301
2143
  }
1302
2144
  return jobs;
1303
2145
  }
1304
- var FEEDS, GREENHOUSE_SLUGS_BY_TIER, ASHBY_SLUGS_BY_TIER, LEVER_SLUGS_BY_TIER, DEFAULT_GREENHOUSE_SLUGS, DEFAULT_ASHBY_SLUGS, DEFAULT_LEVER_SLUGS;
2146
+ var FEEDS, GREENHOUSE_SLUGS_BY_TIER, ASHBY_SLUGS_BY_TIER, LEVER_SLUGS_BY_TIER, DEFAULT_GREENHOUSE_SLUGS, DEFAULT_ASHBY_SLUGS, DEFAULT_LEVER_SLUGS, DEFAULT_WORKABLE_SLUGS;
1305
2147
  var init_feeds = __esm({
1306
2148
  "../../packages/core/src/feeds/index.ts"() {
1307
2149
  "use strict";
@@ -1312,8 +2154,10 @@ var init_feeds = __esm({
1312
2154
  init_wwr();
1313
2155
  init_hn();
1314
2156
  init_github_bounties();
2157
+ init_opire();
2158
+ init_workable();
1315
2159
  init_bounty_gate();
1316
- FEEDS = [greenhouse, ashby, lever, himalayas, wwr, hn];
2160
+ FEEDS = [greenhouse, ashby, lever, workable, himalayas, wwr, hn];
1317
2161
  GREENHOUSE_SLUGS_BY_TIER = {
1318
2162
  bigco: [
1319
2163
  "stripe",
@@ -1419,6 +2263,7 @@ var init_feeds = __esm({
1419
2263
  DEFAULT_GREENHOUSE_SLUGS = flattenTiers(GREENHOUSE_SLUGS_BY_TIER);
1420
2264
  DEFAULT_ASHBY_SLUGS = flattenTiers(ASHBY_SLUGS_BY_TIER);
1421
2265
  DEFAULT_LEVER_SLUGS = flattenTiers(LEVER_SLUGS_BY_TIER);
2266
+ DEFAULT_WORKABLE_SLUGS = ["zego", "workmotion"];
1422
2267
  }
1423
2268
  });
1424
2269
 
@@ -1510,103 +2355,6 @@ var init_indexer = __esm({
1510
2355
  }
1511
2356
  });
1512
2357
 
1513
- // ../../packages/core/src/github.ts
1514
- function ghHeaders(token) {
1515
- const headers = {
1516
- Accept: "application/vnd.github+json",
1517
- "X-GitHub-Api-Version": "2022-11-28"
1518
- };
1519
- if (token) headers["Authorization"] = `Bearer ${token}`;
1520
- return headers;
1521
- }
1522
- async function ghFetch(path, token) {
1523
- const url = `https://api.github.com${path}`;
1524
- const res = await fetch(url, { headers: ghHeaders(token) });
1525
- if (!res.ok) {
1526
- throw new Error(`GitHub API ${path}: HTTP ${res.status} ${res.statusText}`);
1527
- }
1528
- return res.json();
1529
- }
1530
- async function fetchGitHubProfile(login, token) {
1531
- const user = await ghFetch(`/users/${login}`, token);
1532
- let repos = [];
1533
- try {
1534
- repos = await ghFetch(
1535
- `/users/${login}/repos?sort=pushed&per_page=100`,
1536
- token
1537
- );
1538
- } catch (err) {
1539
- console.warn(`[github] ${login}: repos fetch failed, continuing \u2014`, err);
1540
- }
1541
- const langCount = {};
1542
- for (const repo of repos) {
1543
- if (repo.fork) continue;
1544
- if (repo.language) {
1545
- langCount[repo.language.toLowerCase()] = (langCount[repo.language.toLowerCase()] ?? 0) + 1;
1546
- }
1547
- }
1548
- const topLanguages = Object.entries(langCount).sort(([, a], [, b]) => b - a).slice(0, 10).map(([lang]) => lang);
1549
- const topicSet = /* @__PURE__ */ new Set();
1550
- for (const repo of repos) {
1551
- if (repo.fork) continue;
1552
- for (const t of repo.topics ?? []) topicSet.add(t.toLowerCase());
1553
- }
1554
- const topics = Array.from(topicSet).slice(0, 30);
1555
- let recentPRorgs;
1556
- try {
1557
- const q = encodeURIComponent(
1558
- `type:pr is:merged author:${login} sort:updated`
1559
- );
1560
- const result = await ghFetch(
1561
- `/search/issues?q=${q}&per_page=30`,
1562
- token
1563
- );
1564
- const orgs = /* @__PURE__ */ new Set();
1565
- for (const item of result.items ?? []) {
1566
- const orgLogin = item.repository?.owner?.login;
1567
- if (orgLogin && orgLogin !== login) orgs.add(orgLogin);
1568
- }
1569
- if (orgs.size > 0) recentPRorgs = Array.from(orgs);
1570
- } catch {
1571
- }
1572
- return {
1573
- login: user.login,
1574
- name: user.name ?? void 0,
1575
- publicEmail: user.email ?? void 0,
1576
- avatarUrl: user.avatar_url,
1577
- accountCreatedAt: user.created_at,
1578
- publicRepos: user.public_repos,
1579
- followers: user.followers,
1580
- topLanguages,
1581
- topics,
1582
- recentPRorgs
1583
- };
1584
- }
1585
- function inferSeniority2(p) {
1586
- const ageMs = Date.now() - new Date(p.accountCreatedAt).getTime();
1587
- const ageYears = ageMs / (1e3 * 60 * 60 * 24 * 365.25);
1588
- if (ageYears >= 9 && (p.publicRepos >= 40 || p.followers >= 500)) return "staff";
1589
- if (ageYears >= 5 && (p.publicRepos >= 20 || p.followers >= 100)) return "senior";
1590
- if (ageYears >= 2 && p.publicRepos >= 5) return "mid";
1591
- return "junior";
1592
- }
1593
- function githubToFingerprint(p) {
1594
- const rawTokens = [
1595
- ...p.topLanguages,
1596
- ...p.topics
1597
- // recentPRorgs intentionally excluded — org names are not skill tags
1598
- ];
1599
- const skillTags = normalize(rawTokens);
1600
- const seniorityBand = inferSeniority2(p);
1601
- return { skillTags, seniorityBand };
1602
- }
1603
- var init_github = __esm({
1604
- "../../packages/core/src/github.ts"() {
1605
- "use strict";
1606
- init_vocabulary();
1607
- }
1608
- });
1609
-
1610
2358
  // ../../packages/core/src/index.ts
1611
2359
  var src_exports = {};
1612
2360
  __export(src_exports, {
@@ -1616,22 +2364,32 @@ __export(src_exports, {
1616
2364
  DEFAULT_BOUNTY_REPOS: () => DEFAULT_BOUNTY_REPOS,
1617
2365
  DEFAULT_GREENHOUSE_SLUGS: () => DEFAULT_GREENHOUSE_SLUGS,
1618
2366
  DEFAULT_LEVER_SLUGS: () => DEFAULT_LEVER_SLUGS,
2367
+ DEFAULT_WORKABLE_SLUGS: () => DEFAULT_WORKABLE_SLUGS,
1619
2368
  EXAMPLE_BUYER: () => EXAMPLE_BUYER,
1620
2369
  FEEDS: () => FEEDS,
1621
2370
  GRAPH: () => GRAPH,
1622
2371
  GREENHOUSE_SLUGS_BY_TIER: () => GREENHOUSE_SLUGS_BY_TIER,
2372
+ IDF_BACKGROUND: () => IDF_BACKGROUND,
1623
2373
  LEVER_SLUGS_BY_TIER: () => LEVER_SLUGS_BY_TIER,
1624
2374
  SYNONYMS: () => SYNONYMS,
1625
2375
  VOCABULARY: () => VOCABULARY,
1626
2376
  VOCAB_NODES: () => VOCAB_NODES,
2377
+ acceptanceCountForDomains: () => acceptanceCountForDomains,
1627
2378
  aggregate: () => aggregate,
1628
2379
  aggregateBounties: () => aggregateBounties,
1629
2380
  ashby: () => ashby,
2381
+ bestAcceptanceDomain: () => bestAcceptanceDomain,
1630
2382
  buildGraph: () => buildGraph,
1631
2383
  buildIndex: () => buildIndex,
1632
2384
  buildReason: () => buildReason,
2385
+ computeAcceptanceCredential: () => computeAcceptanceCredential,
2386
+ computeAcceptanceCredentialPublic: () => computeAcceptanceCredentialPublic,
2387
+ coreTagsFromTitle: () => coreTagsFromTitle,
2388
+ deriveResumeTrend: () => deriveResumeTrend,
1633
2389
  expandWeighted: () => expandWeighted,
2390
+ extractSkillTags: () => extractSkillTags,
1634
2391
  fetchGitHubProfile: () => fetchGitHubProfile,
2392
+ fetchRepoRecency: () => fetchRepoRecency,
1635
2393
  flattenTiers: () => flattenTiers,
1636
2394
  getBuyer: () => getBuyer,
1637
2395
  githubBounties: () => githubBounties,
@@ -1642,11 +2400,14 @@ __export(src_exports, {
1642
2400
  isBounty: () => isBounty,
1643
2401
  lever: () => lever,
1644
2402
  loadPartnerRoles: () => loadPartnerRoles,
2403
+ looksLikeEngRole: () => looksLikeEngRole,
1645
2404
  match: () => match,
1646
- matchOne: () => matchOne,
1647
2405
  normalize: () => normalize,
2406
+ opire: () => opire,
1648
2407
  passesMaturityGate: () => passesMaturityGate,
2408
+ tokenize: () => tokenize,
1649
2409
  validateGraph: () => validateGraph,
2410
+ workable: () => workable,
1650
2411
  wwr: () => wwr
1651
2412
  });
1652
2413
  var init_src = __esm({