terminalhire 0.2.5 → 0.3.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.
@@ -10,227 +10,369 @@ var __export = (target, all) => {
10
10
  };
11
11
 
12
12
  // ../../packages/core/src/types.ts
13
+ function isBounty(job) {
14
+ return job.source === "bounty" && job.bounty != null;
15
+ }
13
16
  var init_types = __esm({
14
17
  "../../packages/core/src/types.ts"() {
15
18
  "use strict";
16
19
  }
17
20
  });
18
21
 
19
- // ../../packages/core/src/vocabulary.ts
22
+ // ../../packages/core/src/vocab/graph.data.ts
23
+ var VOCAB_NODES;
24
+ var init_graph_data = __esm({
25
+ "../../packages/core/src/vocab/graph.data.ts"() {
26
+ "use strict";
27
+ VOCAB_NODES = [
28
+ // ── Languages ─────────────────────────────────────────────────────────────
29
+ { id: "javascript", synonyms: ["js"], related: [{ to: "typescript", w: 0.6 }] },
30
+ { id: "typescript", parents: ["javascript"], synonyms: ["ts"] },
31
+ { id: "python", synonyms: ["py"] },
32
+ { id: "go", synonyms: ["golang"] },
33
+ { id: "rust" },
34
+ { id: "java", related: [{ to: "kotlin", w: 0.45 }, { to: "scala", w: 0.4 }] },
35
+ { id: "ruby" },
36
+ { id: "elixir" },
37
+ { id: "scala", related: [{ to: "java", w: 0.4 }] },
38
+ { id: "kotlin", related: [{ to: "java", w: 0.45 }] },
39
+ { id: "swift" },
40
+ { id: "cpp", synonyms: ["c++"] },
41
+ { id: "csharp", synonyms: ["c#"] },
42
+ { id: "php" },
43
+ { id: "haskell" },
44
+ { id: "clojure" },
45
+ { id: "r" },
46
+ { id: "dart" },
47
+ // ── Frontend ──────────────────────────────────────────────────────────────
48
+ {
49
+ id: "react",
50
+ parents: ["javascript"],
51
+ synonyms: ["reactjs"],
52
+ related: [{ to: "nextjs", w: 0.55 }, { to: "vue", w: 0.4 }, { to: "svelte", w: 0.4 }, { to: "solidjs", w: 0.5 }, { to: "angular", w: 0.35 }]
53
+ },
54
+ { id: "nextjs", parents: ["react"], synonyms: ["next", "next.js"], related: [{ to: "remix", w: 0.5 }] },
55
+ { id: "vue", parents: ["javascript"], synonyms: ["vue.js"], related: [{ to: "nuxt", w: 0.6 }] },
56
+ { id: "nuxt", parents: ["vue"], synonyms: ["nuxt.js"] },
57
+ { id: "svelte", parents: ["javascript"], related: [{ to: "sveltekit", w: 0.65 }] },
58
+ { id: "sveltekit", parents: ["svelte"] },
59
+ { id: "angular", parents: ["typescript"], synonyms: ["angular.js", "angularjs"] },
60
+ { id: "solidjs", parents: ["javascript"] },
61
+ { id: "remix", parents: ["react"], synonyms: ["remix.run"] },
62
+ { id: "astro", parents: ["javascript"], related: [{ to: "nextjs", w: 0.4 }] },
63
+ { id: "qwik", parents: ["javascript"] },
64
+ { id: "tailwind", parents: ["css"], synonyms: ["tailwindcss", "tw"] },
65
+ { id: "css" },
66
+ { id: "html" },
67
+ { id: "redux", parents: ["react"] },
68
+ { id: "vite", parents: ["frontend"] },
69
+ { id: "webpack", parents: ["frontend"] },
70
+ { id: "storybook", parents: ["frontend"] },
71
+ // ── Backend frameworks / runtimes ───────────────────────────────────────────
72
+ {
73
+ id: "nodejs",
74
+ parents: ["javascript"],
75
+ synonyms: ["node", "node.js"],
76
+ related: [{ to: "express", w: 0.5 }, { to: "fastify", w: 0.45 }, { to: "nestjs", w: 0.45 }]
77
+ },
78
+ { id: "express", parents: ["nodejs"], synonyms: ["express.js", "expressjs"], related: [{ to: "fastify", w: 0.5 }] },
79
+ { id: "fastify", parents: ["nodejs"] },
80
+ { id: "nestjs", parents: ["nodejs"], synonyms: ["nest", "nest.js"] },
81
+ { id: "hono", parents: ["nodejs"] },
82
+ { id: "deno", parents: ["javascript"], related: [{ to: "nodejs", w: 0.5 }, { to: "bun", w: 0.5 }] },
83
+ { id: "bun", parents: ["javascript"], related: [{ to: "nodejs", w: 0.5 }] },
84
+ { id: "django", parents: ["python"], related: [{ to: "flask", w: 0.5 }, { to: "fastapi", w: 0.45 }] },
85
+ { id: "fastapi", parents: ["python"], related: [{ to: "flask", w: 0.55 }, { to: "django", w: 0.45 }] },
86
+ { id: "flask", parents: ["python"] },
87
+ { id: "rails", parents: ["ruby"], synonyms: ["ruby-on-rails", "ror"] },
88
+ { id: "spring", parents: ["java"], synonyms: ["spring-boot", "springboot"] },
89
+ { id: "actix", parents: ["rust"] },
90
+ { id: "gin", parents: ["go"] },
91
+ { id: "phoenix", parents: ["elixir"] },
92
+ { id: "laravel", parents: ["php"] },
93
+ { id: "dotnet", parents: ["csharp"], synonyms: [".net", "asp.net", "dotnet-core"] },
94
+ // ── Infrastructure & DevOps ─────────────────────────────────────────────────
95
+ { id: "kubernetes", synonyms: ["k8s", "kube"], related: [{ to: "docker", w: 0.5 }, { to: "helm", w: 0.55 }, { to: "terraform", w: 0.4 }, { to: "argocd", w: 0.45 }] },
96
+ { id: "docker", parents: ["devops"], related: [{ to: "kubernetes", w: 0.5 }] },
97
+ { id: "terraform", synonyms: ["tf"], related: [{ to: "pulumi", w: 0.55 }, { to: "ansible", w: 0.4 }, { to: "aws", w: 0.4 }] },
98
+ { id: "pulumi", related: [{ to: "terraform", w: 0.55 }] },
99
+ { id: "ansible" },
100
+ { id: "aws", synonyms: ["amazon-web-services"], related: [{ to: "gcp", w: 0.4 }, { to: "azure", w: 0.4 }] },
101
+ { id: "gcp", synonyms: ["google-cloud", "google-cloud-platform"], related: [{ to: "aws", w: 0.4 }, { to: "azure", w: 0.4 }] },
102
+ { id: "azure", synonyms: ["microsoft-azure"], related: [{ to: "aws", w: 0.4 }] },
103
+ { id: "ci-cd", synonyms: ["cicd", "jenkins", "circleci", "circle-ci", "travis"], related: [{ to: "github-actions", w: 0.6 }, { to: "gitlab-ci", w: 0.6 }] },
104
+ { id: "github-actions", parents: ["ci-cd"], synonyms: ["github-action"] },
105
+ { id: "gitlab-ci", parents: ["ci-cd"], synonyms: ["gitlab"] },
106
+ { id: "linux" },
107
+ { id: "nginx" },
108
+ { id: "prometheus", parents: ["observability"], related: [{ to: "grafana", w: 0.6 }] },
109
+ { id: "grafana", parents: ["observability"] },
110
+ { id: "datadog", parents: ["observability"] },
111
+ { id: "opentelemetry", parents: ["observability"], synonyms: ["otel"] },
112
+ { id: "vercel", related: [{ to: "netlify", w: 0.5 }, { to: "nextjs", w: 0.4 }] },
113
+ { id: "netlify" },
114
+ { id: "fly", synonyms: ["fly.io"], related: [{ to: "railway", w: 0.5 }, { to: "render", w: 0.5 }] },
115
+ { id: "railway", related: [{ to: "render", w: 0.5 }] },
116
+ { id: "render" },
117
+ { id: "cloudflare", synonyms: ["cloudflare-workers"] },
118
+ { id: "helm", parents: ["kubernetes"] },
119
+ { id: "argocd", parents: ["kubernetes"] },
120
+ { id: "serverless", parents: ["devops"] },
121
+ // ── Databases & storage ─────────────────────────────────────────────────────
122
+ { id: "postgresql", synonyms: ["postgres", "pg"], related: [{ to: "mysql", w: 0.45 }, { to: "sqlite", w: 0.4 }] },
123
+ { id: "mysql", related: [{ to: "postgresql", w: 0.45 }] },
124
+ { id: "sqlite" },
125
+ { id: "mongodb", synonyms: ["mongo"] },
126
+ { id: "redis", related: [{ to: "caching", w: 0.5 }] },
127
+ { id: "elasticsearch", synonyms: ["elastic"], related: [{ to: "search", w: 0.55 }] },
128
+ { id: "kafka", synonyms: ["apache-kafka"], related: [{ to: "rabbitmq", w: 0.5 }, { to: "message-queue", w: 0.55 }] },
129
+ { id: "rabbitmq", related: [{ to: "message-queue", w: 0.55 }] },
130
+ { id: "cassandra" },
131
+ { id: "dynamodb", parents: ["aws"] },
132
+ { id: "snowflake", parents: ["data-engineering"], related: [{ to: "clickhouse", w: 0.4 }] },
133
+ { id: "clickhouse", parents: ["data-engineering"], related: [{ to: "duckdb", w: 0.35 }] },
134
+ { id: "duckdb", parents: ["data-engineering"] },
135
+ { id: "supabase", related: [{ to: "postgresql", w: 0.5 }, { to: "neon", w: 0.4 }] },
136
+ { id: "planetscale", related: [{ to: "mysql", w: 0.5 }] },
137
+ { id: "neon", related: [{ to: "postgresql", w: 0.5 }] },
138
+ { id: "turso", related: [{ to: "sqlite", w: 0.5 }] },
139
+ { id: "cockroachdb", related: [{ to: "postgresql", w: 0.45 }] },
140
+ { id: "prisma", parents: ["backend"], synonyms: ["@prisma/client"], related: [{ to: "drizzle", w: 0.5 }, { to: "typeorm", w: 0.45 }, { to: "sequelize", w: 0.4 }] },
141
+ { id: "drizzle", synonyms: ["drizzle-orm"], related: [{ to: "prisma", w: 0.5 }] },
142
+ { id: "sequelize", related: [{ to: "typeorm", w: 0.4 }] },
143
+ { id: "typeorm", related: [{ to: "prisma", w: 0.45 }] },
144
+ { id: "sqlalchemy", parents: ["python"] },
145
+ // ── Data engineering & ML ───────────────────────────────────────────────────
146
+ { id: "data-engineering", synonyms: ["data-eng"], related: [{ to: "spark", w: 0.5 }, { to: "airflow", w: 0.5 }, { to: "dbt", w: 0.45 }] },
147
+ { id: "spark", parents: ["data-engineering"], synonyms: ["apache-spark"] },
148
+ { id: "airflow", parents: ["data-engineering"], synonyms: ["apache-airflow"] },
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 }] },
152
+ { id: "pytorch", parents: ["ml"], synonyms: ["torch"], related: [{ to: "tensorflow", w: 0.5 }] },
153
+ { id: "tensorflow", parents: ["ml"], synonyms: ["keras", "tf-keras"] },
154
+ { id: "pandas", parents: ["python"], related: [{ to: "numpy", w: 0.6 }] },
155
+ { id: "numpy", parents: ["python"] },
156
+ { id: "scikit-learn", parents: ["ml"], synonyms: ["sklearn"] },
157
+ { id: "jupyter", parents: ["python"] },
158
+ { id: "langchain", parents: ["llm"], synonyms: ["llamaindex"] },
159
+ { id: "huggingface", parents: ["ml"], synonyms: ["hugging-face"] },
160
+ { id: "openai", parents: ["llm"] },
161
+ { id: "anthropic", parents: ["llm"], synonyms: ["claude"] },
162
+ { id: "rag", parents: ["llm"], synonyms: ["retrieval-augmented-generation"] },
163
+ { id: "mlops", parents: ["ml"], related: [{ to: "devops", w: 0.4 }] },
164
+ // ── Mobile ──────────────────────────────────────────────────────────────────
165
+ { id: "mobile", related: [{ to: "ios", w: 0.5 }, { to: "android", w: 0.5 }] },
166
+ { id: "ios", parents: ["mobile", "swift"], related: [{ to: "android", w: 0.4 }] },
167
+ { id: "android", parents: ["mobile"], related: [{ to: "kotlin", w: 0.4 }] },
168
+ { id: "swiftui", parents: ["ios", "swift"] },
169
+ { id: "react-native", parents: ["mobile", "react"], synonyms: ["reactnative"], related: [{ to: "flutter", w: 0.4 }, { to: "expo", w: 0.6 }] },
170
+ { id: "flutter", parents: ["mobile", "dart"] },
171
+ { id: "expo", parents: ["react-native"] },
172
+ { id: "kotlin-multiplatform", parents: ["mobile", "kotlin"], synonyms: ["kmp"] },
173
+ // ── Domains / capabilities ──────────────────────────────────────────────────
174
+ { id: "frontend", related: [{ to: "react", w: 0.4 }, { to: "css", w: 0.3 }] },
175
+ { id: "backend", related: [{ to: "api-design", w: 0.4 }, { to: "microservices", w: 0.4 }] },
176
+ { id: "devops", related: [{ to: "kubernetes", w: 0.4 }, { to: "ci-cd", w: 0.4 }, { to: "docker", w: 0.4 }] },
177
+ { id: "authentication", synonyms: ["auth", "jwt", "saml", "passport", "auth0", "clerk", "nextauth"], related: [{ to: "oauth", w: 0.6 }, { to: "security", w: 0.5 }] },
178
+ { id: "oauth", parents: ["authentication"], synonyms: ["oauth2", "oidc"], related: [{ to: "security", w: 0.4 }] },
179
+ { id: "security", related: [{ to: "authentication", w: 0.5 }] },
180
+ { id: "payments", synonyms: ["stripe", "braintree", "paddle", "lemonsqueezy", "@stripe/stripe-js"], related: [{ to: "billing", w: 0.6 }] },
181
+ { id: "billing", synonyms: ["recurly", "chargebee"] },
182
+ { id: "api-design", synonyms: ["rest", "restful", "rest-api"], related: [{ to: "graphql", w: 0.4 }, { to: "grpc", w: 0.4 }, { to: "backend", w: 0.4 }] },
183
+ { id: "graphql", synonyms: ["gql"], related: [{ to: "trpc", w: 0.4 }] },
184
+ { id: "trpc", related: [{ to: "graphql", w: 0.4 }] },
185
+ { id: "grpc", synonyms: ["grpc-web"], related: [{ to: "microservices", w: 0.3 }] },
186
+ { id: "microservices" },
187
+ { id: "websockets", synonyms: ["ws", "socket.io"], related: [{ to: "realtime", w: 0.6 }] },
188
+ { id: "realtime", synonyms: ["real-time"] },
189
+ { id: "message-queue", synonyms: ["mq"] },
190
+ { id: "caching", synonyms: ["cache"] },
191
+ { id: "search", synonyms: ["full-text-search"] },
192
+ { id: "observability", synonyms: ["o11y"], related: [{ to: "monitoring", w: 0.6 }] },
193
+ { id: "monitoring", related: [{ to: "prometheus", w: 0.4 }] },
194
+ { id: "testing", related: [{ to: "unit-testing", w: 0.5 }, { to: "e2e-testing", w: 0.5 }] },
195
+ { id: "unit-testing", parents: ["testing"] },
196
+ { id: "e2e-testing", parents: ["testing"], synonyms: ["e2e", "end-to-end-testing"] },
197
+ { id: "jest", parents: ["testing"], related: [{ to: "vitest", w: 0.6 }, { to: "mocha", w: 0.5 }] },
198
+ { id: "vitest", parents: ["testing"], related: [{ to: "jest", w: 0.6 }] },
199
+ { id: "playwright", parents: ["e2e-testing"], related: [{ to: "cypress", w: 0.6 }] },
200
+ { id: "cypress", parents: ["e2e-testing"] },
201
+ { id: "mocha", parents: ["testing"] },
202
+ { id: "pytest", parents: ["testing", "python"] },
203
+ { id: "accessibility", synonyms: ["a11y"] },
204
+ { id: "seo" },
205
+ { id: "performance", synonyms: ["perf", "web-performance"] }
206
+ ];
207
+ }
208
+ });
209
+
210
+ // ../../packages/core/src/vocab/closure.ts
211
+ function round3(n) {
212
+ return Math.round(n * 1e3) / 1e3;
213
+ }
214
+ function validateGraph(nodes) {
215
+ const ids = /* @__PURE__ */ new Set();
216
+ for (const n of nodes) {
217
+ if (ids.has(n.id)) throw new Error(`vocab: duplicate id "${n.id}"`);
218
+ ids.add(n.id);
219
+ }
220
+ const seenAlias = /* @__PURE__ */ new Map();
221
+ for (const n of nodes) {
222
+ for (const p of n.parents ?? []) {
223
+ if (p === n.id) throw new Error(`vocab: "${n.id}" lists itself as a parent`);
224
+ if (!ids.has(p)) throw new Error(`vocab: "${n.id}" parent "${p}" is not a defined id`);
225
+ }
226
+ for (const e of n.related ?? []) {
227
+ if (e.to === n.id) throw new Error(`vocab: "${n.id}" relates to itself`);
228
+ if (!ids.has(e.to)) throw new Error(`vocab: "${n.id}" related "${e.to}" is not a defined id`);
229
+ if (!(e.w > 0 && e.w <= 1)) throw new Error(`vocab: "${n.id}"\u2192"${e.to}" weight ${e.w} out of (0,1]`);
230
+ }
231
+ for (const s of n.synonyms ?? []) {
232
+ const alias = s.toLowerCase();
233
+ if (ids.has(alias)) throw new Error(`vocab: synonym "${alias}" collides with a canonical id`);
234
+ const prev = seenAlias.get(alias);
235
+ if (prev && prev !== n.id) throw new Error(`vocab: synonym "${alias}" maps to both "${prev}" and "${n.id}"`);
236
+ seenAlias.set(alias, n.id);
237
+ }
238
+ }
239
+ const visiting = /* @__PURE__ */ new Set();
240
+ const done = /* @__PURE__ */ new Set();
241
+ const parentMap = new Map(nodes.map((n) => [n.id, n.parents ?? []]));
242
+ const walk = (id, path) => {
243
+ if (done.has(id)) return;
244
+ if (visiting.has(id)) throw new Error(`vocab: parent cycle ${[...path, id].join(" \u2192 ")}`);
245
+ visiting.add(id);
246
+ for (const p of parentMap.get(id) ?? []) walk(p, [...path, id]);
247
+ visiting.delete(id);
248
+ done.add(id);
249
+ };
250
+ for (const n of nodes) walk(n.id, []);
251
+ }
252
+ function buildAdjacency(nodes) {
253
+ const adj = /* @__PURE__ */ new Map();
254
+ const add = (from, to, w) => {
255
+ let m = adj.get(from);
256
+ if (!m) adj.set(from, m = /* @__PURE__ */ new Map());
257
+ if (w > (m.get(to) ?? 0)) m.set(to, w);
258
+ };
259
+ for (const n of nodes) {
260
+ for (const p of n.parents ?? []) {
261
+ add(n.id, p, PARENT_UP);
262
+ add(p, n.id, PARENT_DOWN);
263
+ }
264
+ for (const e of n.related ?? []) {
265
+ add(n.id, e.to, e.w);
266
+ add(e.to, n.id, e.w);
267
+ }
268
+ }
269
+ return adj;
270
+ }
271
+ function closureFrom(source, adj) {
272
+ const best = /* @__PURE__ */ new Map();
273
+ for (const [t, w] of adj.get(source) ?? []) {
274
+ if (w >= DECAY_FLOOR) best.set(t, { w: round3(w), via: t });
275
+ }
276
+ const settled = /* @__PURE__ */ new Set([source]);
277
+ while (true) {
278
+ let u;
279
+ let uw = 0;
280
+ for (const [t, e] of best) {
281
+ if (!settled.has(t) && e.w > uw) {
282
+ u = t;
283
+ uw = e.w;
284
+ }
285
+ }
286
+ if (!u) break;
287
+ settled.add(u);
288
+ const via = best.get(u).via;
289
+ for (const [t, we] of adj.get(u) ?? []) {
290
+ if (settled.has(t)) continue;
291
+ const cand = round3(uw * we);
292
+ if (cand >= DECAY_FLOOR && cand > (best.get(t)?.w ?? 0)) {
293
+ best.set(t, { w: cand, via });
294
+ }
295
+ }
296
+ }
297
+ best.delete(source);
298
+ return best;
299
+ }
300
+ function buildGraph(nodes) {
301
+ validateGraph(nodes);
302
+ const ids = new Set(nodes.map((n) => n.id));
303
+ const synonyms = /* @__PURE__ */ new Map();
304
+ for (const n of nodes) {
305
+ for (const s of n.synonyms ?? []) synonyms.set(s.toLowerCase(), n.id);
306
+ }
307
+ const adj = buildAdjacency(nodes);
308
+ const closure = /* @__PURE__ */ new Map();
309
+ for (const n of nodes) closure.set(n.id, closureFrom(n.id, adj));
310
+ return { ids, synonyms, closure };
311
+ }
312
+ var PARENT_UP, PARENT_DOWN, DECAY_FLOOR;
313
+ var init_closure = __esm({
314
+ "../../packages/core/src/vocab/closure.ts"() {
315
+ "use strict";
316
+ PARENT_UP = 0.6;
317
+ PARENT_DOWN = 0.35;
318
+ DECAY_FLOOR = 0.25;
319
+ }
320
+ });
321
+
322
+ // ../../packages/core/src/vocab/types.ts
323
+ var init_types2 = __esm({
324
+ "../../packages/core/src/vocab/types.ts"() {
325
+ "use strict";
326
+ }
327
+ });
328
+
329
+ // ../../packages/core/src/vocab/index.ts
20
330
  function normalize(tokens) {
21
331
  const result = /* @__PURE__ */ new Set();
22
332
  for (const raw of tokens) {
23
333
  const lower = raw.toLowerCase().trim();
24
- if (VOCAB_SET.has(lower)) {
334
+ if (GRAPH.ids.has(lower)) {
25
335
  result.add(lower);
26
336
  continue;
27
337
  }
28
- const mapped = SYNONYMS[lower];
29
- if (mapped && VOCAB_SET.has(mapped)) {
30
- result.add(mapped);
31
- }
338
+ const mapped = GRAPH.synonyms.get(lower);
339
+ if (mapped) result.add(mapped);
32
340
  }
33
341
  return Array.from(result);
34
342
  }
35
- var VOCABULARY, SYNONYMS, VOCAB_SET;
343
+ function expandWeighted(tags, graph = GRAPH) {
344
+ const out = /* @__PURE__ */ new Map();
345
+ const put = (tag, weight, via) => {
346
+ const ex = out.get(tag);
347
+ if (!ex || weight > ex.weight) out.set(tag, { tag, weight, via });
348
+ };
349
+ for (const t of tags) {
350
+ put(t, 1, t);
351
+ const near = graph.closure.get(t);
352
+ if (near) for (const [n, edge] of near) put(n, edge.w, t);
353
+ }
354
+ return out;
355
+ }
356
+ var GRAPH, VOCABULARY, SYNONYMS;
357
+ var init_vocab = __esm({
358
+ "../../packages/core/src/vocab/index.ts"() {
359
+ "use strict";
360
+ init_graph_data();
361
+ init_closure();
362
+ init_types2();
363
+ init_closure();
364
+ init_graph_data();
365
+ GRAPH = buildGraph(VOCAB_NODES);
366
+ VOCABULARY = [...GRAPH.ids];
367
+ SYNONYMS = Object.fromEntries(GRAPH.synonyms);
368
+ }
369
+ });
370
+
371
+ // ../../packages/core/src/vocabulary.ts
36
372
  var init_vocabulary = __esm({
37
373
  "../../packages/core/src/vocabulary.ts"() {
38
374
  "use strict";
39
- VOCABULARY = [
40
- // Languages
41
- "typescript",
42
- "javascript",
43
- "python",
44
- "go",
45
- "rust",
46
- "java",
47
- "ruby",
48
- "elixir",
49
- "scala",
50
- "kotlin",
51
- "swift",
52
- "cpp",
53
- "csharp",
54
- "php",
55
- "haskell",
56
- "clojure",
57
- "r",
58
- // Frontend frameworks / libs
59
- "react",
60
- "nextjs",
61
- "vue",
62
- "nuxt",
63
- "svelte",
64
- "angular",
65
- "solidjs",
66
- "tailwind",
67
- "css",
68
- "html",
69
- "graphql",
70
- "trpc",
71
- // Backend frameworks
72
- "nodejs",
73
- "express",
74
- "fastify",
75
- "nestjs",
76
- "django",
77
- "fastapi",
78
- "flask",
79
- "rails",
80
- "spring",
81
- "actix",
82
- "gin",
83
- "phoenix",
84
- "laravel",
85
- "dotnet",
86
- // Infrastructure & DevOps
87
- "kubernetes",
88
- "docker",
89
- "terraform",
90
- "aws",
91
- "gcp",
92
- "azure",
93
- "ci-cd",
94
- "github-actions",
95
- "linux",
96
- "nginx",
97
- "pulumi",
98
- "ansible",
99
- "prometheus",
100
- "grafana",
101
- "datadog",
102
- "opentelemetry",
103
- // Data & ML
104
- "postgresql",
105
- "mysql",
106
- "sqlite",
107
- "mongodb",
108
- "redis",
109
- "elasticsearch",
110
- "kafka",
111
- "rabbitmq",
112
- "data-engineering",
113
- "spark",
114
- "airflow",
115
- "dbt",
116
- "ml",
117
- "llm",
118
- "pytorch",
119
- "tensorflow",
120
- "pandas",
121
- "numpy",
122
- // Domains / capabilities
123
- "oauth",
124
- "authentication",
125
- "security",
126
- "payments",
127
- "billing",
128
- "frontend",
129
- "backend",
130
- "devops",
131
- "mobile",
132
- "ios",
133
- "android",
134
- "api-design",
135
- "microservices",
136
- "websockets",
137
- "testing",
138
- "accessibility",
139
- "seo",
140
- "performance",
141
- "observability",
142
- "search",
143
- "realtime"
144
- ];
145
- SYNONYMS = {
146
- // Kubernetes aliases
147
- "k8s": "kubernetes",
148
- "kube": "kubernetes",
149
- // Auth / identity
150
- "passport": "authentication",
151
- "oauth2": "oauth",
152
- "oidc": "oauth",
153
- "jwt": "authentication",
154
- "saml": "authentication",
155
- "auth0": "authentication",
156
- "clerk": "authentication",
157
- "nextauth": "authentication",
158
- // Payments
159
- "@stripe/stripe-js": "payments",
160
- "stripe": "payments",
161
- "braintree": "payments",
162
- "paddle": "payments",
163
- "lemonsqueezy": "payments",
164
- "recurly": "billing",
165
- "chargebee": "billing",
166
- // Framework / lib aliases
167
- "next": "nextjs",
168
- "next.js": "nextjs",
169
- "nuxt.js": "nuxt",
170
- "vue.js": "vue",
171
- "angular.js": "angular",
172
- "angularjs": "angular",
173
- "express.js": "express",
174
- "expressjs": "express",
175
- "fastapi": "fastapi",
176
- "nest": "nestjs",
177
- "nest.js": "nestjs",
178
- "sveltekit": "svelte",
179
- // Language aliases
180
- "ts": "typescript",
181
- "js": "javascript",
182
- "py": "python",
183
- "golang": "go",
184
- "c++": "cpp",
185
- "c#": "csharp",
186
- ".net": "dotnet",
187
- "asp.net": "dotnet",
188
- // DB aliases
189
- "postgres": "postgresql",
190
- "pg": "postgresql",
191
- "mongo": "mongodb",
192
- "elastic": "elasticsearch",
193
- // Cloud aliases
194
- "amazon web services": "aws",
195
- "google cloud": "gcp",
196
- "google cloud platform": "gcp",
197
- "microsoft azure": "azure",
198
- // CI/CD aliases
199
- "github actions": "github-actions",
200
- "circle ci": "ci-cd",
201
- "circleci": "ci-cd",
202
- "jenkins": "ci-cd",
203
- "gitlab ci": "ci-cd",
204
- "travis": "ci-cd",
205
- // Mobile
206
- "react native": "mobile",
207
- "flutter": "mobile",
208
- "expo": "mobile",
209
- // AI / ML
210
- "openai": "llm",
211
- "anthropic": "llm",
212
- "langchain": "llm",
213
- "llamaindex": "llm",
214
- "hugging face": "ml",
215
- "huggingface": "ml",
216
- "scikit-learn": "ml",
217
- "sklearn": "ml",
218
- // Data pipeline
219
- "apache kafka": "kafka",
220
- "apache spark": "spark",
221
- "apache airflow": "airflow",
222
- // Misc
223
- "tailwindcss": "tailwind",
224
- "tw": "tailwind",
225
- "gql": "graphql",
226
- "ws": "websockets",
227
- "socket.io": "websockets",
228
- "jest": "testing",
229
- "vitest": "testing",
230
- "playwright": "testing",
231
- "cypress": "testing"
232
- };
233
- VOCAB_SET = new Set(VOCABULARY);
375
+ init_vocab();
234
376
  }
235
377
  });
236
378
 
@@ -251,6 +393,7 @@ function computeIdf(jobs) {
251
393
  return idf;
252
394
  }
253
395
  function inferSeniority(title) {
396
+ if (!ENG_TITLE.test(title)) return void 0;
254
397
  for (const [re, level] of SENIORITY_PATTERNS) {
255
398
  if (re.test(title)) return level;
256
399
  }
@@ -264,15 +407,15 @@ function seniorityScore(fp, job) {
264
407
  const got = SENIORITY_RANK[jobLevel] ?? 1;
265
408
  const delta = Math.abs(wanted - got);
266
409
  if (delta === 0) return 1;
267
- if (delta === 1) return 0.5;
268
- return 0.2;
410
+ if (delta === 1) return 0.7;
411
+ return 0.4;
269
412
  }
270
- function recencyScore(postedAt) {
413
+ function recencyScore(postedAt, now) {
271
414
  if (!postedAt) return 0.75;
272
- const ageDays = (Date.now() - new Date(postedAt).getTime()) / 864e5;
273
- if (ageDays < 7) return 1;
274
- if (ageDays < 30) return 0.9;
275
- if (ageDays < 90) return 0.75;
415
+ const ageDays2 = (now - new Date(postedAt).getTime()) / 864e5;
416
+ if (ageDays2 < 7) return 1;
417
+ if (ageDays2 < 30) return 0.9;
418
+ if (ageDays2 < 90) return 0.75;
276
419
  return 0.6;
277
420
  }
278
421
  function passesFilters(fp, job) {
@@ -287,52 +430,73 @@ function passesFilters(fp, job) {
287
430
  }
288
431
  return true;
289
432
  }
290
- function buildReason(matchedTags) {
291
- if (matchedTags.length === 0) return "No direct skill overlap found.";
292
- const top = matchedTags.slice(0, 3);
293
- const rest = matchedTags.length - top.length;
433
+ function buildReason(details) {
434
+ if (details.length === 0) return "No direct skill overlap found.";
435
+ const render = (d) => !d.via || d.via === d.tag ? d.tag : `${d.via}\u2192${d.tag} (${d.weight})`;
436
+ const top = details.slice(0, 3).map(render);
437
+ const rest = details.length - top.length;
294
438
  const listed = top.join(", ");
295
439
  if (rest === 0) return `Matched on ${listed}.`;
296
440
  return `Matched on ${listed} + ${rest} more skill${rest > 1 ? "s" : ""}.`;
297
441
  }
298
- function match(fp, jobs, limit = 5) {
442
+ function harmonicMean(a, b) {
443
+ if (a <= 0 || b <= 0) return 0;
444
+ return 2 * a * b / (a + b);
445
+ }
446
+ function match(fp, jobs, limit = 5, now = Date.now()) {
299
447
  const idf = computeIdf(jobs);
300
- const fpTagSet = new Set(fp.skillTags);
301
- const maxTagScore = fp.skillTags.reduce((acc, t) => acc + (idf.get(t) ?? 1), 0);
448
+ const idfOf = (t) => idf.get(t) ?? 0;
449
+ const expanded = expandWeighted(fp.skillTags);
450
+ const maxDevScore = fp.skillTags.reduce((acc, t) => acc + idfOf(t), 0);
302
451
  const candidates = jobs.filter((j) => passesFilters(fp, j));
303
452
  const scored = candidates.map((job) => {
304
- const jobTagSet = new Set(job.tags);
305
- const matched = [];
306
- let tagScore2 = 0;
307
- for (const tag of fpTagSet) {
308
- if (jobTagSet.has(tag)) {
309
- const w = idf.get(tag) ?? 1;
310
- tagScore2 += w;
311
- matched.push(tag);
453
+ const details = [];
454
+ let jobMatchScore = 0;
455
+ let jobMaxScore = 0;
456
+ const devCovByTag = /* @__PURE__ */ new Map();
457
+ for (const tag of job.tags) {
458
+ const w = idfOf(tag);
459
+ jobMaxScore += w;
460
+ const hit = expanded.get(tag);
461
+ if (hit) {
462
+ const credit = Math.pow(hit.weight, SHARPEN);
463
+ jobMatchScore += w * credit;
464
+ details.push({ tag, weight: hit.weight, via: hit.via });
465
+ if (credit > (devCovByTag.get(hit.via) ?? 0)) devCovByTag.set(hit.via, credit);
312
466
  }
313
467
  }
314
- const normTagScore = maxTagScore > 0 ? tagScore2 / maxTagScore : 0;
315
- matched.sort((a, b) => (idf.get(b) ?? 1) - (idf.get(a) ?? 1));
468
+ let devScore = 0;
469
+ for (const t of fp.skillTags) devScore += idfOf(t) * (devCovByTag.get(t) ?? 0);
470
+ const devCov = maxDevScore > 0 ? Math.min(1, devScore / maxDevScore) : 0;
471
+ const jobCov = jobMaxScore > 0 ? Math.min(1, jobMatchScore / jobMaxScore) : 0;
472
+ const tagComponent = harmonicMean(devCov, jobCov);
473
+ if (tagComponent === 0) return null;
474
+ details.sort((a, b) => idfOf(b.tag) * b.weight - idfOf(a.tag) * a.weight);
316
475
  const sScore = seniorityScore(fp, job);
317
- const rScore = recencyScore(job.postedAt);
318
- const score = normTagScore * 0.6 + sScore * 0.25 + rScore * 0.15;
476
+ const rScore = recencyScore(job.postedAt, now);
477
+ const score = tagComponent * 0.6 + sScore * 0.25 + rScore * 0.15;
478
+ const matchedTags = [...new Set(details.map((d) => d.via ?? d.tag))];
319
479
  return {
320
480
  job,
321
481
  score: Math.round(score * 1e3) / 1e3,
322
- matchedTags: matched,
323
- reason: buildReason(matched)
482
+ matchedTags,
483
+ matchDetails: details,
484
+ reason: buildReason(details)
324
485
  };
325
486
  });
326
- return scored.sort((a, b) => b.score - a.score).slice(0, limit);
487
+ return scored.filter((r) => r !== null && r.score >= MIN_SCORE).sort((a, b) => b.score - a.score).slice(0, limit);
327
488
  }
328
489
  function matchOne(fp, job) {
329
490
  const results = match(fp, [job], 1);
330
491
  return results.length > 0 ? results[0] : null;
331
492
  }
332
- var SENIORITY_RANK, SENIORITY_PATTERNS;
493
+ var MIN_SCORE, SHARPEN, SENIORITY_RANK, SENIORITY_PATTERNS, ENG_TITLE;
333
494
  var init_matcher = __esm({
334
495
  "../../packages/core/src/matcher.ts"() {
335
496
  "use strict";
497
+ init_vocabulary();
498
+ MIN_SCORE = 0.15;
499
+ SHARPEN = 1.6;
336
500
  SENIORITY_RANK = {
337
501
  junior: 0,
338
502
  mid: 1,
@@ -345,6 +509,7 @@ var init_matcher = __esm({
345
509
  [/\bjunior\b|\bjr\.?\b|\bentry[\s-]?level\b/i, "junior"],
346
510
  [/\bmid[\s-]?level\b|\bmid\b/i, "mid"]
347
511
  ];
512
+ ENG_TITLE = /\b(engineer|engineering|developer|dev|swe|sde|programmer|architect)\b/i;
348
513
  }
349
514
  });
350
515
 
@@ -685,6 +850,33 @@ var init_himalayas = __esm({
685
850
  }
686
851
  });
687
852
 
853
+ // ../../packages/core/src/feeds/entities.ts
854
+ function fromCodePoint(cp) {
855
+ if (!Number.isFinite(cp) || cp < 0 || cp > 1114111) return "";
856
+ try {
857
+ return String.fromCodePoint(cp);
858
+ } catch {
859
+ return "";
860
+ }
861
+ }
862
+ function decodeEntities(input) {
863
+ if (!input || !input.includes("&")) return input;
864
+ return input.replace(/&#(\d+);/g, (_, n) => fromCodePoint(parseInt(n, 10))).replace(/&#[xX]([0-9a-fA-F]+);/g, (_, h) => fromCodePoint(parseInt(h, 16))).replace(/&(lt|gt|quot|apos|nbsp);/g, (_, name) => NAMED[name] ?? `&${name};`).replace(/&amp;/g, "&");
865
+ }
866
+ var NAMED;
867
+ var init_entities = __esm({
868
+ "../../packages/core/src/feeds/entities.ts"() {
869
+ "use strict";
870
+ NAMED = {
871
+ lt: "<",
872
+ gt: ">",
873
+ quot: '"',
874
+ apos: "'",
875
+ nbsp: " "
876
+ };
877
+ }
878
+ });
879
+
688
880
  // ../../packages/core/src/feeds/wwr.ts
689
881
  function tokenize5(text) {
690
882
  return text.toLowerCase().replace(/[^a-z0-9.\-+#]/g, " ").split(/\s+/).filter(Boolean);
@@ -708,9 +900,9 @@ function parseRss(xml) {
708
900
  for (const block of itemBlocks) {
709
901
  const get = (tag) => {
710
902
  const cdataMatch = block.match(new RegExp(`<${tag}[^>]*><!\\[CDATA\\[([\\s\\S]*?)\\]\\]><\\/${tag}>`, "i"));
711
- if (cdataMatch) return cdataMatch[1].trim();
903
+ if (cdataMatch) return decodeEntities(cdataMatch[1].trim());
712
904
  const plainMatch = block.match(new RegExp(`<${tag}[^>]*>([\\s\\S]*?)<\\/${tag}>`, "i"));
713
- return plainMatch?.[1].trim() ?? "";
905
+ return decodeEntities(plainMatch?.[1].trim() ?? "");
714
906
  };
715
907
  const rawTitle = get("title");
716
908
  const colonIdx = rawTitle.indexOf(":");
@@ -737,6 +929,7 @@ var init_wwr = __esm({
737
929
  "../../packages/core/src/feeds/wwr.ts"() {
738
930
  "use strict";
739
931
  init_vocabulary();
932
+ init_entities();
740
933
  WWR_RSS_URL = "https://weworkremotely.com/remote-jobs.rss";
741
934
  wwr = {
742
935
  source: "wwr",
@@ -775,7 +968,7 @@ function tokenize6(text) {
775
968
  return text.toLowerCase().replace(/[^a-z0-9.\-+#]/g, " ").split(/\s+/).filter(Boolean);
776
969
  }
777
970
  function stripHtml2(html) {
778
- return html.replace(/<p>/gi, " ").replace(/<[^>]*>/g, "").replace(/&amp;/g, "&").replace(/&lt;/g, "<").replace(/&gt;/g, ">").replace(/&quot;/g, '"').replace(/&#x27;/g, "'").replace(/\s+/g, " ").trim();
971
+ return decodeEntities(html.replace(/<p>/gi, " ").replace(/<[^>]*>/g, "")).replace(/\s+/g, " ").trim();
779
972
  }
780
973
  function extractUrl(text) {
781
974
  const match2 = text.match(/https?:\/\/[^\s<>"']+/);
@@ -829,6 +1022,7 @@ var init_hn = __esm({
829
1022
  "../../packages/core/src/feeds/hn.ts"() {
830
1023
  "use strict";
831
1024
  init_vocabulary();
1025
+ init_entities();
832
1026
  ALGOLIA_SEARCH = "https://hn.algolia.com/api/v1/search?query=Ask+HN%3A+Who+is+Hiring%3F&tags=story,ask_hn&hitsPerPage=1";
833
1027
  ALGOLIA_ITEMS = "https://hn.algolia.com/api/v1/items/";
834
1028
  hn = {
@@ -865,7 +1059,198 @@ var init_hn = __esm({
865
1059
  }
866
1060
  });
867
1061
 
1062
+ // ../../packages/core/src/feeds/bounty-gate.ts
1063
+ function ageDays(createdAtIso) {
1064
+ const created = Date.parse(createdAtIso);
1065
+ if (!Number.isFinite(created)) return 0;
1066
+ return (Date.now() - created) / (1e3 * 60 * 60 * 24);
1067
+ }
1068
+ function passesMaturityGate(repo) {
1069
+ if (repo.archived || repo.disabled) return false;
1070
+ if (repo.stargazers < MIN_REPO_STARS) return false;
1071
+ if (ageDays(repo.createdAt) < MIN_REPO_AGE_DAYS) return false;
1072
+ return true;
1073
+ }
1074
+ var DEFAULT_BOUNTY_REPOS, MAX_BOUNTIES_PER_REPO, MIN_REPO_STARS, MIN_REPO_AGE_DAYS;
1075
+ var init_bounty_gate = __esm({
1076
+ "../../packages/core/src/feeds/bounty-gate.ts"() {
1077
+ "use strict";
1078
+ DEFAULT_BOUNTY_REPOS = [
1079
+ "tenstorrent/tt-metal",
1080
+ "sequelize/sequelize",
1081
+ "commaai/opendbc",
1082
+ "aragon/hack",
1083
+ "spacemeshos/app",
1084
+ "archestra-ai/archestra",
1085
+ "boundlessfi/boundless",
1086
+ "ucfopen/Obojobo",
1087
+ "widgetti/ipyvolume",
1088
+ "moorcheh-ai/memanto",
1089
+ "PrismarineJS/mineflayer"
1090
+ ];
1091
+ MAX_BOUNTIES_PER_REPO = 10;
1092
+ MIN_REPO_STARS = 5;
1093
+ MIN_REPO_AGE_DAYS = 30;
1094
+ }
1095
+ });
1096
+
1097
+ // ../../packages/core/src/feeds/github-bounties.ts
1098
+ function authHeaders() {
1099
+ const token = process.env["GITHUB_TOKEN"] ?? process.env["GH_TOKEN"];
1100
+ const h = {
1101
+ Accept: "application/vnd.github+json",
1102
+ "User-Agent": "terminalhire",
1103
+ "X-GitHub-Api-Version": "2022-11-28"
1104
+ };
1105
+ if (token) h["Authorization"] = `Bearer ${token}`;
1106
+ return h;
1107
+ }
1108
+ function tokenize7(text) {
1109
+ return text.toLowerCase().replace(/[^a-z0-9.\-+#]/g, " ").split(/\s+/).filter(Boolean);
1110
+ }
1111
+ function parseAmountUSD(text) {
1112
+ const m = text.match(/\$\s?([0-9][0-9,]*(?:\.[0-9]+)?)\s?([kK])?/);
1113
+ if (!m) return void 0;
1114
+ let n = parseFloat(m[1].replace(/,/g, ""));
1115
+ if (m[2]) n *= 1e3;
1116
+ if (!Number.isFinite(n) || n <= 0 || n > 1e6) return void 0;
1117
+ return Math.round(n);
1118
+ }
1119
+ function effortFromAmount(amount) {
1120
+ if (amount == null) return void 0;
1121
+ if (amount <= 500) return "small";
1122
+ if (amount <= 2e3) return "medium";
1123
+ return "large";
1124
+ }
1125
+ function labelNames(issue) {
1126
+ return (issue.labels ?? []).map((l) => typeof l === "string" ? l : l.name ?? "").filter(Boolean);
1127
+ }
1128
+ function isBountyIssue(issue) {
1129
+ if (issue.pull_request) return false;
1130
+ const labels = labelNames(issue);
1131
+ if (labels.some((n) => BOUNTY_LABEL_RE.test(n))) return true;
1132
+ return /bounty/i.test(issue.title) && parseAmountUSD(issue.title) != null;
1133
+ }
1134
+ async function ghJson(path) {
1135
+ let res;
1136
+ try {
1137
+ res = await fetch(`${GITHUB_API}${path}`, { headers: authHeaders() });
1138
+ } catch (err) {
1139
+ console.warn(`[github-bounties] network error ${path} \u2014`, err);
1140
+ return null;
1141
+ }
1142
+ if (res.status === 403 && res.headers.get("x-ratelimit-remaining") === "0") {
1143
+ console.warn("[github-bounties] rate-limited (set GITHUB_TOKEN for 5000/hr)");
1144
+ return null;
1145
+ }
1146
+ if (!res.ok) {
1147
+ console.warn(`[github-bounties] HTTP ${res.status} ${path}`);
1148
+ return null;
1149
+ }
1150
+ try {
1151
+ return await res.json();
1152
+ } catch {
1153
+ return null;
1154
+ }
1155
+ }
1156
+ async function fetchCommentAmount(repoFullName, issueNumber) {
1157
+ const comments = await ghJson(
1158
+ `/repos/${repoFullName}/issues/${issueNumber}/comments?per_page=30`
1159
+ );
1160
+ if (!comments) return void 0;
1161
+ for (const c of comments) {
1162
+ const body = c.body ?? "";
1163
+ if (BOUNTY_LABEL_RE.test(body)) {
1164
+ const amt = parseAmountUSD(body);
1165
+ if (amt != null) return amt;
1166
+ }
1167
+ }
1168
+ return void 0;
1169
+ }
1170
+ async function fetchRepoBounties(repoFullName) {
1171
+ const repo = await ghJson(`/repos/${repoFullName}`);
1172
+ if (!repo) return [];
1173
+ const meta = {
1174
+ fullName: repo.full_name,
1175
+ stargazers: repo.stargazers_count,
1176
+ createdAt: repo.created_at,
1177
+ archived: repo.archived,
1178
+ disabled: repo.disabled
1179
+ };
1180
+ if (!passesMaturityGate(meta)) {
1181
+ console.info(`[github-bounties] ${repoFullName}: failed maturity gate, skipping`);
1182
+ return [];
1183
+ }
1184
+ const issues = await ghJson(`/repos/${repoFullName}/issues?state=open&per_page=100`);
1185
+ if (!issues) return [];
1186
+ const bounties = issues.filter(isBountyIssue).slice(0, MAX_BOUNTIES_PER_REPO);
1187
+ const owner = repo.owner.login;
1188
+ return Promise.all(bounties.map(async (issue) => {
1189
+ const title = decodeEntities(issue.title).trim();
1190
+ const body = issue.body ? decodeEntities(issue.body) : "";
1191
+ const amountUSD = parseAmountUSD(title) ?? parseAmountUSD(body) ?? await fetchCommentAmount(repoFullName, issue.number);
1192
+ const labels = labelNames(issue);
1193
+ const tags = normalize(tokenize7([title, labels.join(" "), body.slice(0, 2e3)].join(" ")));
1194
+ return {
1195
+ id: `bounty:${repoFullName}#${issue.number}`,
1196
+ source: "bounty",
1197
+ title,
1198
+ company: owner,
1199
+ url: issue.html_url,
1200
+ remote: true,
1201
+ location: "Remote",
1202
+ tags,
1203
+ roleType: "freelance",
1204
+ postedAt: issue.created_at,
1205
+ applyMode: "direct",
1206
+ bounty: {
1207
+ amountUSD,
1208
+ estimatedEffort: effortFromAmount(amountUSD),
1209
+ bountySource: "github",
1210
+ claimUrl: issue.html_url,
1211
+ repoFullName,
1212
+ repoStars: repo.stargazers_count,
1213
+ issueBody: body.slice(0, 1e3) || void 0
1214
+ },
1215
+ raw: issue
1216
+ };
1217
+ }));
1218
+ }
1219
+ var GITHUB_API, BOUNTY_LABEL_RE, githubBounties;
1220
+ var init_github_bounties = __esm({
1221
+ "../../packages/core/src/feeds/github-bounties.ts"() {
1222
+ "use strict";
1223
+ init_vocabulary();
1224
+ init_entities();
1225
+ init_bounty_gate();
1226
+ GITHUB_API = "https://api.github.com";
1227
+ BOUNTY_LABEL_RE = /bounty|reward|funded|💎|💰/i;
1228
+ githubBounties = {
1229
+ source: "bounty",
1230
+ async fetch(opts) {
1231
+ const repos = opts?.slugs && opts.slugs.length > 0 ? opts.slugs : DEFAULT_BOUNTY_REPOS;
1232
+ console.info(`[github-bounties] scanning ${repos.length} repos`);
1233
+ const settled = await Promise.allSettled(repos.map(fetchRepoBounties));
1234
+ const jobs = [];
1235
+ let failures = 0;
1236
+ for (const r of settled) {
1237
+ if (r.status === "fulfilled") jobs.push(...r.value);
1238
+ else {
1239
+ failures++;
1240
+ console.warn("[github-bounties] repo fetch rejected:", r.reason);
1241
+ }
1242
+ }
1243
+ console.info(`[github-bounties] total: ${jobs.length} bounties, ${failures} repo failures`);
1244
+ return jobs;
1245
+ }
1246
+ };
1247
+ }
1248
+ });
1249
+
868
1250
  // ../../packages/core/src/feeds/index.ts
1251
+ async function aggregateBounties(opts) {
1252
+ return githubBounties.fetch({ slugs: opts?.repos });
1253
+ }
869
1254
  function flattenTiers(t) {
870
1255
  return [.../* @__PURE__ */ new Set([...t.bigco, ...t.scaleup, ...t.startup])];
871
1256
  }
@@ -898,6 +1283,19 @@ async function aggregate(opts) {
898
1283
  }
899
1284
  }
900
1285
  }
1286
+ if (opts?.includeBounties !== false) {
1287
+ try {
1288
+ const bounties = await githubBounties.fetch({ slugs: opts?.slugs?.["bounty"], limit });
1289
+ for (const b of bounties) {
1290
+ if (!seen.has(b.id)) {
1291
+ seen.add(b.id);
1292
+ jobs.push(b);
1293
+ }
1294
+ }
1295
+ } catch (err) {
1296
+ console.warn("[feeds] bounties failed:", err);
1297
+ }
1298
+ }
901
1299
  return jobs;
902
1300
  }
903
1301
  var FEEDS, GREENHOUSE_SLUGS_BY_TIER, ASHBY_SLUGS_BY_TIER, LEVER_SLUGS_BY_TIER, DEFAULT_GREENHOUSE_SLUGS, DEFAULT_ASHBY_SLUGS, DEFAULT_LEVER_SLUGS;
@@ -910,6 +1308,8 @@ var init_feeds = __esm({
910
1308
  init_himalayas();
911
1309
  init_wwr();
912
1310
  init_hn();
1311
+ init_github_bounties();
1312
+ init_bounty_gate();
913
1313
  FEEDS = [greenhouse, ashby, lever, himalayas, wwr, hn];
914
1314
  GREENHOUSE_SLUGS_BY_TIER = {
915
1315
  bigco: [
@@ -1019,72 +1419,78 @@ var init_feeds = __esm({
1019
1419
  }
1020
1420
  });
1021
1421
 
1022
- // ../../packages/core/src/coastal.ts
1422
+ // ../../packages/core/src/partners.ts
1023
1423
  import { readFileSync } from "fs";
1024
1424
  import { join } from "path";
1025
1425
  import { fileURLToPath } from "url";
1026
1426
  function resolveDataPath() {
1027
1427
  try {
1028
1428
  const dir = fileURLToPath(new URL("../../../data", import.meta.url));
1029
- return join(dir, "coastal-roles.json");
1429
+ return join(dir, "partner-roles.json");
1030
1430
  } catch {
1031
- return join(process.cwd(), "data", "coastal-roles.json");
1431
+ return join(process.cwd(), "data", "partner-roles.json");
1032
1432
  }
1033
1433
  }
1034
- function loadCoastalRoles() {
1434
+ function loadPartnerRoles() {
1035
1435
  const filePath = resolveDataPath();
1036
1436
  try {
1037
1437
  const raw = readFileSync(filePath, "utf-8");
1038
1438
  const parsed = JSON.parse(raw);
1039
1439
  if (!Array.isArray(parsed)) {
1040
- console.warn("[coastal] coastal-roles.json is not an array \u2014 skipping");
1440
+ console.warn("[partners] partner-roles.json is not an array \u2014 skipping");
1041
1441
  return [];
1042
1442
  }
1043
1443
  const valid = [];
1044
1444
  for (const entry of parsed) {
1045
- if (typeof entry === "object" && entry !== null && typeof entry.id === "string" && entry.applyMode === "buyer-lead" && entry.buyer === "coastal") {
1445
+ const e = entry;
1446
+ if (typeof entry === "object" && entry !== null && typeof e.id === "string" && e.applyMode === "buyer-lead" && typeof e.buyer === "string" && e.buyer.length > 0) {
1046
1447
  valid.push(entry);
1047
1448
  } else {
1048
- console.warn("[coastal] Skipping malformed role entry:", entry);
1449
+ console.warn("[partners] Skipping malformed role entry:", entry);
1049
1450
  }
1050
1451
  }
1051
1452
  return valid;
1052
1453
  } catch (err) {
1053
1454
  if (err.code === "ENOENT") {
1054
- console.warn(`[coastal] data/coastal-roles.json not found at ${filePath} \u2014 no Coastal roles loaded`);
1455
+ console.warn(`[partners] data/partner-roles.json not found at ${filePath} \u2014 no partner roles loaded`);
1055
1456
  } else {
1056
- console.warn("[coastal] Failed to load coastal-roles.json:", err);
1457
+ console.warn("[partners] Failed to load partner-roles.json:", err);
1057
1458
  }
1058
1459
  return [];
1059
1460
  }
1060
1461
  }
1061
- var COASTAL_BUYER;
1062
- var init_coastal = __esm({
1063
- "../../packages/core/src/coastal.ts"() {
1462
+ function getBuyer(id) {
1463
+ return BUYER_REGISTRY[id];
1464
+ }
1465
+ var EXAMPLE_BUYER, BUYER_REGISTRY;
1466
+ var init_partners = __esm({
1467
+ "../../packages/core/src/partners.ts"() {
1064
1468
  "use strict";
1065
- COASTAL_BUYER = {
1066
- id: "coastal",
1067
- legalName: "Coastal Recruiting LLC",
1068
- matchCriteria: {
1069
- roleTypes: ["full_time"]
1070
- }
1469
+ EXAMPLE_BUYER = {
1470
+ id: "northstar",
1471
+ legalName: "Northstar Talent Partners",
1472
+ matchCriteria: { roleTypes: ["full_time"] }
1473
+ };
1474
+ BUYER_REGISTRY = {
1475
+ [EXAMPLE_BUYER.id]: EXAMPLE_BUYER
1071
1476
  };
1072
1477
  }
1073
1478
  });
1074
1479
 
1075
1480
  // ../../packages/core/src/indexer.ts
1076
1481
  async function buildIndex(opts) {
1077
- const includeCoastal = opts?.includeCoastal ?? true;
1482
+ const includePartners = opts?.includePartners ?? true;
1078
1483
  const publicJobs = await aggregate(opts);
1079
1484
  const allJobs = [...publicJobs];
1080
- if (includeCoastal) {
1081
- const coastalJobs = loadCoastalRoles();
1082
- const seen = new Set(publicJobs.map((j) => j.id));
1083
- for (const job of coastalJobs) {
1084
- if (!seen.has(job.id)) {
1085
- seen.add(job.id);
1086
- allJobs.push(job);
1087
- }
1485
+ const seen = new Set(publicJobs.map((j) => j.id));
1486
+ const partnerJobs = [
1487
+ ...includePartners ? loadPartnerRoles() : [],
1488
+ ...opts?.partnerRoles ?? []
1489
+ ];
1490
+ for (const job of partnerJobs) {
1491
+ if (!seen.has(job.id)) {
1492
+ seen.add(job.id);
1493
+ allJobs.push(job);
1088
1494
  }
1089
1495
  }
1090
1496
  const jobs = allJobs.map(({ raw: _raw, ...rest }) => rest);
@@ -1097,7 +1503,7 @@ var init_indexer = __esm({
1097
1503
  "../../packages/core/src/indexer.ts"() {
1098
1504
  "use strict";
1099
1505
  init_feeds();
1100
- init_coastal();
1506
+ init_partners();
1101
1507
  }
1102
1508
  });
1103
1509
 
@@ -1179,8 +1585,7 @@ function inferSeniority2(p) {
1179
1585
  if (ageYears >= 9 && (p.publicRepos >= 40 || p.followers >= 500)) return "staff";
1180
1586
  if (ageYears >= 5 && (p.publicRepos >= 20 || p.followers >= 100)) return "senior";
1181
1587
  if (ageYears >= 2 && p.publicRepos >= 5) return "mid";
1182
- if (ageYears < 2 || p.publicRepos < 5) return "junior";
1183
- return void 0;
1588
+ return "junior";
1184
1589
  }
1185
1590
  function githubToFingerprint(p) {
1186
1591
  const rawTokens = [
@@ -1203,30 +1608,42 @@ var init_github = __esm({
1203
1608
  var src_exports = {};
1204
1609
  __export(src_exports, {
1205
1610
  ASHBY_SLUGS_BY_TIER: () => ASHBY_SLUGS_BY_TIER,
1206
- COASTAL_BUYER: () => COASTAL_BUYER,
1611
+ DECAY_FLOOR: () => DECAY_FLOOR,
1207
1612
  DEFAULT_ASHBY_SLUGS: () => DEFAULT_ASHBY_SLUGS,
1613
+ DEFAULT_BOUNTY_REPOS: () => DEFAULT_BOUNTY_REPOS,
1208
1614
  DEFAULT_GREENHOUSE_SLUGS: () => DEFAULT_GREENHOUSE_SLUGS,
1209
1615
  DEFAULT_LEVER_SLUGS: () => DEFAULT_LEVER_SLUGS,
1616
+ EXAMPLE_BUYER: () => EXAMPLE_BUYER,
1210
1617
  FEEDS: () => FEEDS,
1618
+ GRAPH: () => GRAPH,
1211
1619
  GREENHOUSE_SLUGS_BY_TIER: () => GREENHOUSE_SLUGS_BY_TIER,
1212
1620
  LEVER_SLUGS_BY_TIER: () => LEVER_SLUGS_BY_TIER,
1213
1621
  SYNONYMS: () => SYNONYMS,
1214
1622
  VOCABULARY: () => VOCABULARY,
1623
+ VOCAB_NODES: () => VOCAB_NODES,
1215
1624
  aggregate: () => aggregate,
1625
+ aggregateBounties: () => aggregateBounties,
1216
1626
  ashby: () => ashby,
1627
+ buildGraph: () => buildGraph,
1217
1628
  buildIndex: () => buildIndex,
1218
1629
  buildReason: () => buildReason,
1630
+ expandWeighted: () => expandWeighted,
1219
1631
  fetchGitHubProfile: () => fetchGitHubProfile,
1220
1632
  flattenTiers: () => flattenTiers,
1633
+ getBuyer: () => getBuyer,
1634
+ githubBounties: () => githubBounties,
1221
1635
  githubToFingerprint: () => githubToFingerprint,
1222
1636
  greenhouse: () => greenhouse,
1223
1637
  himalayas: () => himalayas,
1224
1638
  hn: () => hn,
1639
+ isBounty: () => isBounty,
1225
1640
  lever: () => lever,
1226
- loadCoastalRoles: () => loadCoastalRoles,
1641
+ loadPartnerRoles: () => loadPartnerRoles,
1227
1642
  match: () => match,
1228
1643
  matchOne: () => matchOne,
1229
1644
  normalize: () => normalize,
1645
+ passesMaturityGate: () => passesMaturityGate,
1646
+ validateGraph: () => validateGraph,
1230
1647
  wwr: () => wwr
1231
1648
  });
1232
1649
  var init_src = __esm({
@@ -1237,7 +1654,7 @@ var init_src = __esm({
1237
1654
  init_matcher();
1238
1655
  init_feeds();
1239
1656
  init_indexer();
1240
- init_coastal();
1657
+ init_partners();
1241
1658
  init_github();
1242
1659
  }
1243
1660
  });
@@ -1336,10 +1753,10 @@ function migrateTagWeights(profile) {
1336
1753
  if (!profile.tagWeights) {
1337
1754
  profile.tagWeights = {};
1338
1755
  }
1339
- const now = (/* @__PURE__ */ new Date()).toISOString();
1756
+ const seed = profile.updatedAt ?? (/* @__PURE__ */ new Date()).toISOString();
1340
1757
  for (const tag of profile.skillTags) {
1341
1758
  if (!profile.tagWeights[tag]) {
1342
- profile.tagWeights[tag] = { count: 1, firstSeen: now, lastSeen: now, sessions: 1 };
1759
+ profile.tagWeights[tag] = { count: 1, firstSeen: seed, lastSeen: seed, sessions: 1 };
1343
1760
  }
1344
1761
  }
1345
1762
  }
@@ -1365,7 +1782,7 @@ async function writeProfile(profile) {
1365
1782
  const blob = encrypt(JSON.stringify(profile), key);
1366
1783
  writeFileSync(PROFILE_FILE, JSON.stringify(blob, null, 2), { encoding: "utf8" });
1367
1784
  }
1368
- function accumulateSession(profile, tags, isEmployerContext2, inferredSeniority) {
1785
+ function accumulateSession(profile, tags, isEmployerContext2, inferredSeniority, seniorityIsAuthoritative = false) {
1369
1786
  const now = (/* @__PURE__ */ new Date()).toISOString();
1370
1787
  let filtered = normalize(tags);
1371
1788
  if (isEmployerContext2) {
@@ -1383,7 +1800,9 @@ function accumulateSession(profile, tags, isEmployerContext2, inferredSeniority)
1383
1800
  }
1384
1801
  }
1385
1802
  if (inferredSeniority && !isEmployerContext2) {
1386
- profile.seniority = inferredSeniority;
1803
+ if (seniorityIsAuthoritative || !profile.github) {
1804
+ profile.seniority = inferredSeniority;
1805
+ }
1387
1806
  }
1388
1807
  }
1389
1808
  async function accumulateTags(rawTokens, isEmployerContext2, inferredSeniority) {
@@ -1391,12 +1810,14 @@ async function accumulateTags(rawTokens, isEmployerContext2, inferredSeniority)
1391
1810
  accumulateSession(profile, rawTokens, isEmployerContext2, inferredSeniority);
1392
1811
  await writeProfile(profile);
1393
1812
  }
1394
- function accumulateGitHubTags(profile, tags) {
1813
+ function accumulateGitHubTags(profile, tags, inferredSeniority) {
1395
1814
  accumulateSession(
1396
1815
  profile,
1397
1816
  tags,
1398
1817
  /* isEmployerContext */
1399
- false
1818
+ false,
1819
+ inferredSeniority,
1820
+ true
1400
1821
  );
1401
1822
  }
1402
1823
  async function listSavedJobs() {
@@ -1760,35 +2181,31 @@ __export(signal_exports, {
1760
2181
  extractFingerprint: () => extractFingerprint
1761
2182
  });
1762
2183
  import { readFileSync as readFileSync4, readdirSync } from "fs";
1763
- import { execSync } from "child_process";
2184
+ import { execFileSync } from "child_process";
1764
2185
  import { join as join4 } from "path";
1765
- function safeExec(cmd) {
2186
+ function safeGit(args, cwd) {
1766
2187
  try {
1767
- return execSync(cmd, { timeout: 2e3, stdio: ["ignore", "pipe", "ignore"] }).toString().trim();
2188
+ return execFileSync("git", ["-C", cwd, ...args], {
2189
+ timeout: 2e3,
2190
+ stdio: ["ignore", "pipe", "ignore"]
2191
+ }).toString().trim();
1768
2192
  } catch {
1769
2193
  return "";
1770
2194
  }
1771
2195
  }
1772
2196
  function isEmployerContext(cwd) {
1773
- const remote = safeExec('git -C "' + cwd + '" remote get-url origin 2>/dev/null');
2197
+ const inRepo = safeGit(["rev-parse", "--is-inside-work-tree"], cwd);
2198
+ if (inRepo !== "true") return false;
2199
+ const remote = safeGit(["remote", "get-url", "origin"], cwd);
1774
2200
  if (remote) {
1775
- try {
1776
- const sshMatch = remote.match(/^git@([^:]+):/);
1777
- const httpsMatch = remote.match(/^https?:\/\/([^/]+)/);
1778
- const host = (sshMatch?.[1] ?? httpsMatch?.[1] ?? "").toLowerCase();
1779
- if (host && !PERSONAL_GIT_HOSTS.has(host)) {
1780
- return true;
1781
- }
1782
- } catch {
1783
- }
1784
- }
1785
- const email = safeExec('git -C "' + cwd + '" config user.email 2>/dev/null');
1786
- if (email) {
1787
- const domain = email.split("@")[1]?.toLowerCase() ?? "";
1788
- if (domain && !PERSONAL_EMAIL_DOMAINS.has(domain)) {
1789
- return true;
1790
- }
1791
- }
2201
+ const sshMatch = remote.match(/^git@([^:]+):/);
2202
+ const httpsMatch = remote.match(/^https?:\/\/([^/]+)/);
2203
+ const host = (sshMatch?.[1] ?? httpsMatch?.[1] ?? "").toLowerCase();
2204
+ if (host) return !PERSONAL_GIT_HOSTS.has(host);
2205
+ }
2206
+ const email = safeGit(["config", "user.email"], cwd);
2207
+ const domain = email.split("@")[1]?.toLowerCase() ?? "";
2208
+ if (domain) return !PERSONAL_EMAIL_DOMAINS.has(domain);
1792
2209
  return false;
1793
2210
  }
1794
2211
  function readJsonSafe(path) {
@@ -1811,10 +2228,24 @@ function tokensFromPackageJson(cwd) {
1811
2228
  const p = pkg;
1812
2229
  const deps = {
1813
2230
  ...typeof p["dependencies"] === "object" ? p["dependencies"] : {},
1814
- ...typeof p["devDependencies"] === "object" ? p["devDependencies"] : {}
2231
+ ...typeof p["devDependencies"] === "object" ? p["devDependencies"] : {},
2232
+ ...typeof p["peerDependencies"] === "object" ? p["peerDependencies"] : {}
1815
2233
  };
1816
2234
  return Object.keys(deps);
1817
2235
  }
2236
+ function workspaceMemberDirs(cwd) {
2237
+ const dirs = [cwd];
2238
+ for (const group of ["apps", "packages"]) {
2239
+ try {
2240
+ const groupDir = join4(cwd, group);
2241
+ for (const e of readdirSync(groupDir, { withFileTypes: true })) {
2242
+ if (e.isDirectory() && !e.isSymbolicLink()) dirs.push(join4(groupDir, e.name));
2243
+ }
2244
+ } catch {
2245
+ }
2246
+ }
2247
+ return dirs;
2248
+ }
1818
2249
  function tokensFromRequirementsTxt(cwd) {
1819
2250
  const content = readFileSafe(join4(cwd, "requirements.txt"));
1820
2251
  if (!content) return [];
@@ -1822,14 +2253,26 @@ function tokensFromRequirementsTxt(cwd) {
1822
2253
  }
1823
2254
  function tokensFromGoMod(cwd) {
1824
2255
  const content = readFileSafe(join4(cwd, "go.mod"));
1825
- if (!content) return ["go"];
2256
+ if (!content) return [];
1826
2257
  const requires = Array.from(content.matchAll(/^\s+([^\s]+)\s+v/gm)).map((m) => m[1].split("/").pop() ?? "").filter(Boolean);
1827
2258
  return ["go", ...requires];
1828
2259
  }
1829
2260
  function tokensFromCargoToml(cwd) {
1830
2261
  const content = readFileSafe(join4(cwd, "Cargo.toml"));
1831
2262
  if (!content) return [];
1832
- const deps = Array.from(content.matchAll(/^([a-zA-Z0-9_-]+)\s*=/gm)).map((m) => m[1].toLowerCase());
2263
+ const deps = [];
2264
+ let inDeps = false;
2265
+ for (const line of content.split("\n")) {
2266
+ const trimmed = line.trim();
2267
+ const section = trimmed.match(/^\[([^\]]+)\]/);
2268
+ if (section) {
2269
+ inDeps = /(^|\.)(dependencies|dev-dependencies|build-dependencies)$/.test(section[1].trim());
2270
+ continue;
2271
+ }
2272
+ if (!inDeps) continue;
2273
+ const key = trimmed.match(/^([a-zA-Z0-9_-]+)\s*=/);
2274
+ if (key) deps.push(key[1].toLowerCase());
2275
+ }
1833
2276
  return ["rust", ...deps];
1834
2277
  }
1835
2278
  function tokensFromFileExtensions(cwd) {
@@ -1869,11 +2312,7 @@ function inferSeniority3(rawTokens) {
1869
2312
  "opentelemetry",
1870
2313
  "prometheus",
1871
2314
  "grafana",
1872
- "microservices",
1873
- "api-design",
1874
- "security",
1875
- "oauth",
1876
- "payments"
2315
+ "microservices"
1877
2316
  ]);
1878
2317
  const midSignals = /* @__PURE__ */ new Set([
1879
2318
  "docker",
@@ -1883,7 +2322,11 @@ function inferSeniority3(rawTokens) {
1883
2322
  "postgresql",
1884
2323
  "redis",
1885
2324
  "graphql",
1886
- "trpc"
2325
+ "trpc",
2326
+ "api-design",
2327
+ "security",
2328
+ "oauth",
2329
+ "payments"
1887
2330
  ]);
1888
2331
  const normalized = new Set(normalize(rawTokens));
1889
2332
  const seniorHits = [...normalized].filter((t) => seniorSignals.has(t)).length;
@@ -1894,13 +2337,16 @@ function inferSeniority3(rawTokens) {
1894
2337
  }
1895
2338
  function extractFingerprint(cwd) {
1896
2339
  const employer = isEmployerContext(cwd);
1897
- const rawTokens = [
1898
- ...tokensFromPackageJson(cwd),
1899
- ...tokensFromRequirementsTxt(cwd),
1900
- ...tokensFromGoMod(cwd),
1901
- ...tokensFromCargoToml(cwd),
1902
- ...tokensFromFileExtensions(cwd)
1903
- ];
2340
+ const rawTokens = [];
2341
+ for (const dir of workspaceMemberDirs(cwd)) {
2342
+ rawTokens.push(
2343
+ ...tokensFromPackageJson(dir),
2344
+ ...tokensFromRequirementsTxt(dir),
2345
+ ...tokensFromGoMod(dir),
2346
+ ...tokensFromCargoToml(dir),
2347
+ ...tokensFromFileExtensions(dir)
2348
+ );
2349
+ }
1904
2350
  let skillTags = normalize(rawTokens);
1905
2351
  if (employer) {
1906
2352
  skillTags = skillTags.filter((t) => LANGUAGE_TAGS2.has(t));