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/README.md +46 -22
- package/package.json +3 -2
- package/src/cli.js +266 -52
- package/src/clip.js +18 -0
- package/src/data.js +200 -53
- package/src/diff.js +103 -0
- package/src/fit.js +41 -0
- package/src/lint.js +481 -12
- package/src/reject.js +114 -12
- package/src/screens.js +250 -50
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
|
|
19
|
-
'npx jobless —
|
|
20
|
-
|
|
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
|
|
25
|
-
{ value: '~30%', label: '
|
|
26
|
-
{ value: '3B+', label: 'ETB
|
|
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
|
-
'
|
|
33
|
-
'Traced it through BullMQ, ioredis,
|
|
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
|
|
41
|
-
'
|
|
42
|
-
'
|
|
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
|
-
'
|
|
94
|
-
'
|
|
95
|
-
'
|
|
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
|
|
106
|
-
'
|
|
107
|
-
'
|
|
108
|
-
'
|
|
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
|
-
'
|
|
121
|
-
'
|
|
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
|
-
'
|
|
132
|
-
'
|
|
133
|
-
'
|
|
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
|
|
214
|
+
export const cases = [
|
|
140
215
|
{
|
|
141
|
-
|
|
216
|
+
id: 'crons',
|
|
217
|
+
title: 'The deploy that killed 17 cron jobs',
|
|
142
218
|
company: 'TPK Synergy Limited',
|
|
143
|
-
|
|
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
|
-
|
|
242
|
+
id: 'reports',
|
|
243
|
+
title: 'Reports that used to take two hours',
|
|
147
244
|
company: 'Mereb Technologies',
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
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
|
-
|
|
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
|
-
{
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
{
|
|
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
|
+
};
|