jobless 1.3.0 → 1.5.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/src/data.js CHANGED
@@ -15,31 +15,96 @@ export const profile = {
15
15
  resume: 'https://yonasalem.vercel.app/assets/resume.pdf',
16
16
  };
17
17
 
18
- export const hireMailto = `mailto:${profile.email}?subject=${encodeURIComponent(
19
- 'npx jobless — Yonas Alem',
20
- )}&body=${encodeURIComponent('I ran npx jobless. I want to talk about a role.\n')}`;
18
+ export const hireSubjects = {
19
+ recruiter: 'npx jobless — recruiter intro for a role',
20
+ manager: 'npx jobless — hiring manager, skip the form',
21
+ default: 'npx jobless — Yonas Alem',
22
+ };
23
+
24
+ export const mailtoFor = ({ report, to } = {}) => {
25
+ const subject = hireSubjects[to] || hireSubjects.default;
26
+ let body = 'I ran npx jobless. I want to talk about a role.\n';
27
+ if (report?.letter) {
28
+ body += `\nI linted a posting.\nRESULT: ${report.letter.result} (${report.score}/100)\n`;
29
+ body += report.letter.reasons.map((reason) => `- ${reason}`).join('\n');
30
+ body += '\n';
31
+ }
32
+ if (report?.fit?.matched?.length) {
33
+ body += `\nOverlap: ${report.fit.matched.join(', ')}\n`;
34
+ }
35
+ return `mailto:${profile.email}?subject=${encodeURIComponent(subject)}&body=${encodeURIComponent(body)}`;
36
+ };
37
+
38
+ export const hireMailto = mailtoFor();
39
+
40
+ export const vcard = () =>
41
+ [
42
+ 'BEGIN:VCARD',
43
+ 'VERSION:3.0',
44
+ `FN:${profile.name}`,
45
+ 'N:Alem;Yonas;;;',
46
+ `TITLE:${profile.title}`,
47
+ `EMAIL;TYPE=INTERNET,PREF:${profile.email}`,
48
+ `TEL;TYPE=CELL:${profile.phone.replace(/\s+/g, '')}`,
49
+ `ADR;TYPE=WORK:;;${profile.location};;;;`,
50
+ `URL:${profile.website}`,
51
+ `URL:${profile.github}`,
52
+ `URL:${profile.linkedin}`,
53
+ `NOTE:${signature.lines.at(-1)}`,
54
+ 'END:VCARD',
55
+ '',
56
+ ].join('\n');
21
57
 
22
58
  export const stats = [
23
59
  { value: '5+', label: 'years in production' },
24
- { value: '35%', label: 'infra cost cut' },
25
- { value: '~30%', label: 'performance gain' },
26
- { value: '3B+', label: 'ETB payment volume' },
60
+ { value: '~35%', label: 'infra cut (Mereb)' },
61
+ { value: '~30%', label: 'perf gain (Mereb)' },
62
+ { value: '3B+', label: 'ETB on rails I integrated' },
63
+ ];
64
+
65
+ export const claims = [
66
+ {
67
+ value: '~35% / ~$100K a year',
68
+ label: 'infra cost',
69
+ who: 'Senior Software Engineer, Mereb Technologies',
70
+ method:
71
+ 'Database, caching, and resource optimization on a production gaming platform. An engineering estimate, not a finance-signed audit.',
72
+ },
73
+ {
74
+ value: '~30%',
75
+ label: 'platform performance',
76
+ who: 'Senior Software Engineer, Mereb Technologies',
77
+ method: 'Indexing, read/write separation, caching, pagination, transactional operations, redesigned status-checking.',
78
+ },
79
+ {
80
+ value: '2 hours → under 30 minutes',
81
+ label: 'reports on ~1M-row datasets',
82
+ who: 'Senior Software Engineer, Mereb Technologies',
83
+ method: 'Moved aggregation off ad-hoc application paths onto the database.',
84
+ },
85
+ {
86
+ value: '3B+ ETB / 100K+ downloads',
87
+ label: 'payment volume on products I integrated',
88
+ who: 'Frontend Developer, Eaglelion — client payment workflows, not the bank and not the ledger',
89
+ method:
90
+ 'Mastercard, Visa, Telebirr, and bank APIs: states, callbacks, failures, retries, verification, SOAP→REST. The products handled the volume. I built the client rails.',
91
+ },
27
92
  ];
28
93
 
29
94
  export const signature = {
30
95
  title: 'The deploy that killed 17 cron jobs',
31
96
  lines: [
32
- 'Recurring Amazon sync jobs silently stopped after every deploy.',
33
- 'Traced it through BullMQ, ioredis, the Node process lifecycle, and the Railway scheduler.',
34
- 'Then added explicit shutdown handling across 17 cron processes.',
97
+ 'After every deploy, recurring Amazon sync jobs died quietly. Dashboards looked fine until the data was a day old.',
98
+ 'Traced it through BullMQ job state, ioredis connections, Node process shutdown, and the Railway scheduler.',
99
+ 'Then added explicit shutdown handling across 17 cron processes so a deploy could not leave work stranded.',
35
100
  'I debug silent production failures. That is the job.',
36
101
  ],
37
102
  };
38
103
 
39
104
  export const about = [
40
- 'I build and fix production systems across payments, gaming, ecommerce, advertising, and government services — from PostgreSQL and backend services through React, AWS, and the last mile of production debugging.',
41
- 'I have cut infrastructure spend by about 35% (roughly $100K a year), improved platform performance around 30%, and integrated payment rails that have handled more than 3 billion ETB in transaction volume.',
42
- 'Day to day I work across the stack: schema design, APIs, React frontends, AWS infrastructure, and the kind of debugging that starts with a silent failure and ends with a safer system.',
105
+ 'I am a senior full-stack engineer in Addis Ababa. I work in Node.js, PostgreSQL, and React, usually on systems where a silent failure costs real money.',
106
+ 'The numbers have owners. Infra and performance: Mereb, as senior engineer on a real-money gaming platform. Payment volume: Eaglelion, as the frontend engineer who integrated the client rails — not the bank.',
107
+ 'Production code lives in private employer repos. The case studies in `work` are the public record. I will walk through the systems on a call. Remote from Ethiopia, EAT (UTC+3).',
43
108
  ];
44
109
 
45
110
  export const lookingFor = {
@@ -90,9 +155,9 @@ export const experience = [
90
155
  place: 'Remote',
91
156
  points: [
92
157
  'Amazon seller analytics: marketplace analytics, FBA, finance, PPC, and inventory.',
93
- 'Traced recurring sync jobs that silently died after every deploy; added explicit shutdown handling across 17 cron processes.',
94
- 'Integrated Amazon SP-API and Ads auth end to end: OAuth, LWA, encrypted credentials.',
95
- 'Extended the platform to UK and EU marketplaces with VAT-aware margins.',
158
+ 'Diagnosed sync jobs that died after every deploy; shutdown handling across 17 cron processes. See `proof`.',
159
+ 'Amazon SP-API and Ads auth end to end: OAuth, LWA, encrypted credentials.',
160
+ 'UK and EU marketplaces with VAT-aware margins. Moved expensive aggregations into SQL.',
96
161
  ],
97
162
  stack: ['Node.js', 'PostgreSQL', 'BullMQ', 'Redis', 'React', 'Amazon SP-API'],
98
163
  },
@@ -102,11 +167,10 @@ export const experience = [
102
167
  dates: 'Apr 2022 – Nov 2025',
103
168
  place: 'Addis Ababa',
104
169
  points: [
105
- 'Owned a real-money gaming platform: loyalty, payouts, fraud, reporting, infra.',
106
- 'Improved platform performance ~30% with indexing, caching, and read/write separation.',
107
- 'Cut infrastructure cost ~35%, about $100K a year.',
108
- 'Dropped report generation on ~1M-row datasets from 2 hours to under 30 minutes.',
109
- 'Mentored 3 junior engineers.',
170
+ 'Owned the production gaming platform end to end in these domains: loyalty, payouts, fraud, reporting — not the whole company.',
171
+ '~30% platform performance: indexing, read/write separation, caching, pagination, redesigned status-checking.',
172
+ '~35% infra cost, about $100K a year: database, cache, and resource optimization. Engineering estimate.',
173
+ 'Reports on ~1M-row datasets: 2 hours to under 30 minutes. Mentored 3 juniors on APIs, debugging, and design.',
110
174
  ],
111
175
  stack: ['PostgreSQL', 'gRPC', 'PHP', 'React', 'AWS', 'Datadog'],
112
176
  },
@@ -114,72 +178,155 @@ export const experience = [
114
178
  role: 'Full Stack Developer (Contract)',
115
179
  company: 'Affiliate.com',
116
180
  dates: 'Feb 2023 – Aug 2023',
117
- place: 'Remote',
181
+ place: 'Remote · alongside Mereb',
118
182
  points: [
119
183
  'Migrated a legacy PHP API to Node.js and tRPC without breaking the React UI.',
120
- 'Cut report generation time ~40% by moving it onto Google Cloud Storage.',
121
- 'Rebuilt validation in Zod and reached ~85% test coverage on migrated code.',
184
+ 'Reimplemented legacy SQL in Prisma while both platforms ran in parallel.',
185
+ 'Cut report generation ~40% by moving it onto Google Cloud Storage. ~85% coverage on migrated code.',
122
186
  ],
123
187
  stack: ['Node.js', 'tRPC', 'Prisma', 'React', 'Zod', 'GCS'],
124
188
  },
189
+ {
190
+ role: 'Backend Engineer (Contract)',
191
+ company: 'Autobridge Systems',
192
+ dates: 'Contract',
193
+ place: 'Addis Ababa · government constituent platform',
194
+ points: [
195
+ 'Backend for complaints, document approvals, and internal communication, with access control and encryption.',
196
+ 'Traced multi-minute email-record loads to missing indexes. Those operations improved more than 10x.',
197
+ ],
198
+ stack: ['PostgreSQL', 'Node.js', 'Encryption'],
199
+ },
125
200
  {
126
201
  role: 'Frontend Developer',
127
202
  company: 'Eaglelion Systems Technology',
128
203
  dates: 'Feb 2021 – Apr 2022',
129
204
  place: 'Addis Ababa',
130
205
  points: [
131
- 'Integrated Mastercard, Visa, Telebirr, and bank APIs into products with 100K+ downloads.',
132
- 'Payment workflows across 3B+ ETB in transaction volume: callbacks, retries, reconciliation.',
133
- 'Raised GTmetrix from 45 to 97 and cut page load ~25%.',
206
+ 'Client payment workflows on products with 100K+ downloads that have handled 3B+ ETB. I integrated the rails, not the ledger.',
207
+ 'Mastercard, Visa, Telebirr, bank APIs: states, callbacks, failures, retries, verification. SOAP→REST against incomplete docs.',
208
+ 'GTmetrix 45 → 97. Page load ~25% faster: code splitting, lazy loading, assets.',
134
209
  ],
135
210
  stack: ['React', 'Next.js', 'Tailwind', 'Payment APIs'],
136
211
  },
137
212
  ];
138
213
 
139
- export const work = [
214
+ export const cases = [
140
215
  {
141
- title: 'Amazon seller analytics platform',
216
+ id: 'crons',
217
+ title: 'The deploy that killed 17 cron jobs',
142
218
  company: 'TPK Synergy Limited',
143
- blurb: 'Production reliability, Amazon integrations, and the data path behind the dashboard.',
219
+ role: 'Senior Full Stack Engineer',
220
+ kind: 'Reliability · private production',
221
+ problem:
222
+ 'After every deploy, recurring Amazon sync jobs died quietly. Dashboards looked fine until the data was a day old.',
223
+ trace: [
224
+ 'BullMQ job state — work marked active, then gone',
225
+ 'ioredis connections closing with the Node process',
226
+ 'Node shutdown: no drain, no SIGTERM handler that waited',
227
+ 'Railway scheduler: the next tick assumed the last run had finished',
228
+ ],
229
+ change: 'Explicit shutdown handling across 17 cron processes so a deploy could not leave work stranded.',
230
+ result: 'Sync jobs survive deploys instead of going silent until a seller notices bad numbers.',
231
+ still:
232
+ 'A worker that exits without draining still looks healthy on the dashboard. The failure mode is silence, not a page. That is what I watch for.',
233
+ undo: 'I would put a queue-depth and last-success timestamp on the dashboard the same week. Shutdown handling without a visible lag is how the next silent failure hides.',
234
+ ask: [
235
+ 'What did SIGTERM actually do before the patch?',
236
+ 'How did you know it was 17 processes, not one flaky job?',
237
+ 'What still pages you, and what still does not?',
238
+ ],
239
+ stack: ['Node.js', 'BullMQ', 'Redis', 'Railway'],
144
240
  },
145
241
  {
146
- title: 'Real-money gaming platform',
242
+ id: 'reports',
243
+ title: 'Reports that used to take two hours',
147
244
  company: 'Mereb Technologies',
148
- blurb: 'Loyalty, payouts, fraud, reporting, and the infrastructure underneath it.',
149
- },
150
- {
151
- title: 'Government constituent platform',
152
- company: 'Autobridge Systems',
153
- blurb: 'Complaints, document approvals, access control, encryption, 10x faster email ingest.',
154
- },
155
- {
156
- title: 'Upplai',
157
- company: 'AI resume platform',
158
- blurb: 'Resume generation and ATS feedback with OpenAI and Gemini, plus per-resume cost tracking.',
245
+ role: 'Senior Software Engineer',
246
+ kind: 'Performance · private production',
247
+ problem: 'Reporting on about a million records ran for two hours and blocked people who needed the numbers.',
248
+ trace: [
249
+ 'Ad-hoc application paths doing aggregation the database should do',
250
+ 'Missing indexes, no read replica, cache that did not cover the hot report',
251
+ 'Status-checking that polled work already in flight',
252
+ ],
253
+ change:
254
+ 'Indexing, read/write separation, caching, and a redesigned status-checking flow so the database did the aggregation.',
255
+ result:
256
+ 'Same reports, under 30 minutes. Platform performance up about 30%. Infra cost down about 35% (~$100K/year, engineering estimate).',
257
+ still: 'I cannot show the queries here. They are in a private repo. I will walk through them on a call.',
258
+ undo: 'I would have split the hottest report into an async job on day one instead of waiting until it was a two-hour meeting blocker.',
259
+ ask: [
260
+ 'Which index moved the needle, and how did you measure it?',
261
+ 'What did you refuse to cache?',
262
+ 'Where does the 30% / $100K estimate come from?',
263
+ ],
264
+ stack: ['PostgreSQL', 'PHP', 'Redis', 'AWS'],
159
265
  },
160
266
  {
267
+ id: 'nedaj',
161
268
  title: 'Nedaj payments',
162
- company: 'Eaglelion',
163
- blurb: 'Fuel-purchase payments for 100K+ downloads: Mastercard, Visa, Telebirr, bank APIs.',
269
+ company: 'Eaglelion Systems Technology',
270
+ role: 'Frontend Developer',
271
+ kind: 'Shipped product · client rails',
272
+ problem:
273
+ 'A fuel-purchase app had to talk to Mastercard, Visa, Telebirr, and commercial banks, with incomplete provider docs and a SOAP past.',
274
+ trace: [
275
+ 'I built the client payment workflows: states, callbacks, failures, retries, verification',
276
+ 'Not the acquiring bank, not the ledger, not the 3B ETB as a personal metric — the products handled that volume',
277
+ 'SOAP replaced with REST against docs that did not match production',
278
+ ],
279
+ change: 'Reliable payment states on a product with 100K+ downloads. Live on Google Play.',
280
+ result: 'Payment rails that hold up in production. GTmetrix 45 → 97 on the web surface.',
281
+ still: 'Provider callbacks still fail in ways the docs do not mention. Retries and verification are the job.',
282
+ undo: 'I would have treated provider docs as hostile from the first week and recorded every callback mismatch in one log, not in Slack.',
283
+ ask: [
284
+ 'What did you own on the client, and what did you not own?',
285
+ 'How did a failed Telebirr callback become a user-visible state?',
286
+ 'Why is 3B ETB not your personal metric?',
287
+ ],
288
+ stack: ['React', 'Next.js', 'REST', 'Payment APIs'],
289
+ url: 'https://play.google.com/store/apps/details?id=com.eaglelionsystems.nedaj',
164
290
  },
165
291
  ];
166
292
 
293
+ export const work = cases;
294
+
295
+ export const mentorship = {
296
+ where: 'Mereb Technologies',
297
+ who: '3 junior engineers',
298
+ on: ['API development', 'debugging', 'code quality', 'system design'],
299
+ still: 'They can speak to the reviews. I will not invent a quote for them. Ask me who, and I will name the work.',
300
+ };
301
+
167
302
  export const skills = [
168
- { name: 'Languages', items: ['TypeScript', 'JavaScript', 'Go', 'Python', 'PHP', 'SQL'] },
169
- { name: 'Backend', items: ['Node.js', 'Express', 'FastAPI', 'Prisma', 'gRPC', 'tRPC', 'REST', 'BullMQ'] },
170
- { name: 'Data', items: ['PostgreSQL', 'MySQL', 'MongoDB', 'Redis'] },
171
- { name: 'Frontend', items: ['React', 'Next.js', 'Tailwind CSS', 'Material UI'] },
172
- { name: 'Cloud', items: ['AWS', 'Docker', 'Kubernetes', 'Nginx', 'GitHub Actions', 'Datadog'] },
173
- { name: 'Integrations', items: ['Amazon SP-API', 'Mastercard', 'Visa', 'Telebirr', 'OAuth 2.0', 'OpenAI'] },
303
+ {
304
+ name: 'Dangerous at',
305
+ note: 'I will own this in production',
306
+ items: ['Node.js', 'PostgreSQL', 'TypeScript / JavaScript', 'Redis / BullMQ', 'React / Next.js', 'AWS'],
307
+ },
308
+ {
309
+ name: 'Used in production',
310
+ note: 'I have shipped it. I am not the expert in the room.',
311
+ items: ['PHP', 'gRPC', 'Prisma', 'tRPC', 'Docker', 'Datadog', 'SQL', 'Tailwind CSS'],
312
+ },
313
+ {
314
+ name: 'Not the job',
315
+ note: 'I will say so in the first screen',
316
+ items: ['Rust', 'native iOS / Android', 'Kubernetes as a daily driver'],
317
+ },
174
318
  ];
175
319
 
176
320
  export const quotes = [
177
- { name: 'Dan Warner', role: 'AppLand Inc', text: 'Yonas did a great job! I would work with him again on a larger project.' },
178
- { name: 'Muly Oved', role: '', text: 'Excellent freelancer, dedicated, hard worker, was joy to work with.' },
179
321
  {
180
322
  name: 'Mekidem Getaneh',
181
- role: '',
182
- text: 'He goes above and beyond for solving a problem and to find optimal solutions.',
323
+ role: 'Engineering colleague · ~1 year',
324
+ text: 'I have worked with Yonas for almost a year. He goes above and beyond for solving a problem and to find optimal solutions.',
325
+ },
326
+ {
327
+ name: 'Kalvin',
328
+ role: 'Client · data analytics frontend',
329
+ text: 'Exceptional expertise and creativity, with attention to detail, responsiveness to feedback, and proactive communication.',
183
330
  },
184
331
  ];
185
332
 
package/src/diff.js ADDED
@@ -0,0 +1,103 @@
1
+ export const diffLines = (before, after) => {
2
+ const a = String(before).split('\n');
3
+ const b = String(after).split('\n');
4
+ const n = a.length;
5
+ const m = b.length;
6
+ const dp = Array.from({ length: n + 1 }, () => Array(m + 1).fill(0));
7
+
8
+ for (let i = n - 1; i >= 0; i -= 1) {
9
+ for (let j = m - 1; j >= 0; j -= 1) {
10
+ dp[i][j] = a[i] === b[j] ? dp[i + 1][j + 1] + 1 : Math.max(dp[i + 1][j], dp[i][j + 1]);
11
+ }
12
+ }
13
+
14
+ const ops = [];
15
+ let i = 0;
16
+ let j = 0;
17
+ while (i < n && j < m) {
18
+ if (a[i] === b[j]) {
19
+ ops.push({ type: 'eq', line: a[i] });
20
+ i += 1;
21
+ j += 1;
22
+ } else if (dp[i + 1][j] >= dp[i][j + 1]) {
23
+ ops.push({ type: 'del', line: a[i] });
24
+ i += 1;
25
+ } else {
26
+ ops.push({ type: 'add', line: b[j] });
27
+ j += 1;
28
+ }
29
+ }
30
+ while (i < n) {
31
+ ops.push({ type: 'del', line: a[i] });
32
+ i += 1;
33
+ }
34
+ while (j < m) {
35
+ ops.push({ type: 'add', line: b[j] });
36
+ j += 1;
37
+ }
38
+ return ops;
39
+ };
40
+
41
+ export const unifiedDiff = (before, after, path = 'posting.txt') => {
42
+ const ops = diffLines(before, after);
43
+ if (!ops.some((op) => op.type !== 'eq')) return '';
44
+
45
+ const lines = [`--- a/${path}`, `+++ b/${path}`];
46
+ let oldLine = 1;
47
+ let newLine = 1;
48
+ let idx = 0;
49
+
50
+ while (idx < ops.length) {
51
+ while (idx < ops.length && ops[idx].type === 'eq') {
52
+ oldLine += 1;
53
+ newLine += 1;
54
+ idx += 1;
55
+ }
56
+ if (idx >= ops.length) break;
57
+
58
+ let start = idx;
59
+ let cursor = idx;
60
+ let trailingEq = 0;
61
+ while (cursor < ops.length) {
62
+ if (ops[cursor].type === 'eq') {
63
+ trailingEq += 1;
64
+ if (trailingEq > 2) break;
65
+ } else {
66
+ trailingEq = 0;
67
+ }
68
+ cursor += 1;
69
+ }
70
+ if (trailingEq > 2) cursor -= trailingEq - 2;
71
+
72
+ const hunk = ops.slice(Math.max(0, start - 2), cursor);
73
+ const pre = Math.min(2, start);
74
+ const hunkOldStart = oldLine - pre;
75
+ const hunkNewStart = newLine - pre;
76
+ let oldCount = 0;
77
+ let newCount = 0;
78
+ const body = [];
79
+ for (const op of hunk) {
80
+ if (op.type === 'eq') {
81
+ body.push(` ${op.line}`);
82
+ oldCount += 1;
83
+ newCount += 1;
84
+ } else if (op.type === 'del') {
85
+ body.push(`-${op.line}`);
86
+ oldCount += 1;
87
+ } else {
88
+ body.push(`+${op.line}`);
89
+ newCount += 1;
90
+ }
91
+ }
92
+ lines.push(`@@ -${Math.max(hunkOldStart, 1)},${oldCount} +${Math.max(hunkNewStart, 1)},${newCount} @@`);
93
+ lines.push(...body);
94
+
95
+ for (const op of ops.slice(start, cursor)) {
96
+ if (op.type === 'eq' || op.type === 'del') oldLine += 1;
97
+ if (op.type === 'eq' || op.type === 'add') newLine += 1;
98
+ }
99
+ idx = cursor;
100
+ }
101
+
102
+ return lines.join('\n');
103
+ };
package/src/fit.js ADDED
@@ -0,0 +1,41 @@
1
+ export const FIT_SIGNALS = [
2
+ { id: 'node', label: 'Node.js', have: true, core: true, test: /node\.?js|\bjavascript\b|\btypescript\b|\bexpress\b/i },
3
+ { id: 'postgres', label: 'PostgreSQL', have: true, core: true, test: /\bpostgres(ql)?\b|\bprisma\b/i },
4
+ { id: 'aws', label: 'AWS', have: true, core: true, test: /\baws\b|amazon web services/i },
5
+ { id: 'react', label: 'React', have: true, core: true, test: /\breact\b|\bnext\.js\b/i },
6
+ { id: 'remote', label: 'Remote', have: true, core: false, test: /\bremote\b/i },
7
+ { id: 'senior', label: 'Senior', have: true, core: false, test: /\b(senior|staff|principal)\b/i },
8
+ { id: 'payments', label: 'Payments (client rails)', have: true, core: true, test: /\b(payment|fintech|mastercard|visa)\b/i },
9
+ { id: 'production', label: 'Production', have: true, core: true, test: /\b(production|on-call|incident|uptime)\b/i },
10
+ { id: 'redis', label: 'Redis / queues', have: true, core: true, test: /\b(redis|bullmq|queue)\b/i },
11
+ { id: 'docker', label: 'Docker', have: true, core: false, test: /\bdocker\b/i },
12
+ { id: 'php', label: 'PHP', have: true, core: false, test: /\bphp\b/i },
13
+ { id: 'go', label: 'Go', have: false, core: false, test: /\bgolang\b|\bgo developer\b|\bgo engineer\b/i },
14
+ { id: 'kubernetes', label: 'Kubernetes', have: false, core: false, test: /\bkubernetes\b|\bk8s\b/i },
15
+ { id: 'rust', label: 'Rust', have: false, core: false, test: /\brust\b/i },
16
+ { id: 'mobile', label: 'iOS/Android native', have: false, core: false, test: /\b(swift|kotlin|android native|ios native)\b/i },
17
+ ];
18
+
19
+ export const fitAgainst = (text) => {
20
+ const hay = String(text || '');
21
+ const asked = FIT_SIGNALS.filter((signal) => signal.test.test(hay));
22
+ const matched = asked.filter((signal) => signal.have);
23
+ const missed = asked.filter((signal) => !signal.have);
24
+ const extra = FIT_SIGNALS.filter((signal) => signal.core && signal.have && !asked.includes(signal)).slice(0, 3);
25
+ const denom = matched.length + missed.length;
26
+ const score = denom === 0 ? null : Math.round((matched.length / denom) * 100);
27
+
28
+ return {
29
+ score,
30
+ matched: matched.map((signal) => signal.label),
31
+ missed: missed.map((signal) => signal.label),
32
+ extra: extra.map((signal) => signal.label),
33
+ };
34
+ };
35
+
36
+ export const tweetFromReport = (report) => {
37
+ if (report && typeof report.score === 'number') {
38
+ return `this posting scored ${report.score}/100\nnpx jobless lint`;
39
+ }
40
+ return 'npx jobless';
41
+ };