resuml 3.0.0 → 3.1.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.
@@ -0,0 +1,1836 @@
1
+ import {
2
+ getSkillIndex
3
+ } from "./chunk-QR77BRMN.js";
4
+
5
+ // src/ats/checks/parsing.ts
6
+ var ISO_DATE = /^\d{4}(-\d{2})?(-\d{2})?$/;
7
+ var conventionalSections = (resume) => {
8
+ const required = ["basics", "work", "education"];
9
+ const missing = required.filter((s) => {
10
+ const v = resume[s];
11
+ if (Array.isArray(v)) return v.length === 0;
12
+ return v === void 0;
13
+ });
14
+ const passed = missing.length === 0;
15
+ return {
16
+ id: "conventional-sections",
17
+ tier: "parsing",
18
+ weight: "high",
19
+ status: passed ? "pass" : "fail",
20
+ score: Math.round((required.length - missing.length) / required.length * 100),
21
+ message: passed ? "All conventional sections present." : `Missing required sections: ${missing.join(", ")}.`,
22
+ hints: passed ? [] : [`Add ${missing.join(", ")} to your YAML.`]
23
+ };
24
+ };
25
+ var dateFormatConsistency = (resume) => {
26
+ const all = [];
27
+ for (const w of resume.work || []) {
28
+ if (w.startDate) all.push({ date: w.startDate, where: `work "${w.name || ""}".startDate` });
29
+ if (w.endDate) all.push({ date: w.endDate, where: `work "${w.name || ""}".endDate` });
30
+ }
31
+ for (const e of resume.education || []) {
32
+ if (e.startDate)
33
+ all.push({ date: e.startDate, where: `education "${e.institution || ""}".startDate` });
34
+ if (e.endDate)
35
+ all.push({ date: e.endDate, where: `education "${e.institution || ""}".endDate` });
36
+ }
37
+ if (all.length === 0) {
38
+ return {
39
+ id: "date-format-consistency",
40
+ tier: "parsing",
41
+ weight: "medium",
42
+ status: "skipped",
43
+ score: 0,
44
+ message: "No dates to check.",
45
+ hints: []
46
+ };
47
+ }
48
+ const bad = all.filter((d) => !ISO_DATE.test(d.date));
49
+ const passed = bad.length === 0;
50
+ return {
51
+ id: "date-format-consistency",
52
+ tier: "parsing",
53
+ weight: "medium",
54
+ status: passed ? "pass" : bad.length <= 1 ? "warn" : "fail",
55
+ score: Math.round((all.length - bad.length) / all.length * 100),
56
+ message: passed ? "All dates use ISO-8601 format." : `Non-ISO dates: ${bad.slice(0, 3).map((b) => `${b.where}=${b.date}`).join("; ")}.`,
57
+ hints: passed ? [] : ["Use YYYY-MM or YYYY-MM-DD for every date field."]
58
+ };
59
+ };
60
+ var contactInBody = (resume) => {
61
+ const b = resume.basics;
62
+ const checks = [
63
+ { ok: !!b?.name, field: "name" },
64
+ { ok: !!b?.email, field: "email" },
65
+ { ok: !!b?.phone, field: "phone" },
66
+ { ok: !!b?.location?.city, field: "location.city" }
67
+ ];
68
+ const missing = checks.filter((c) => !c.ok).map((c) => c.field);
69
+ const passed = missing.length === 0;
70
+ return {
71
+ id: "contact-in-body",
72
+ tier: "parsing",
73
+ weight: "high",
74
+ status: passed ? "pass" : "fail",
75
+ score: Math.round((checks.length - missing.length) / checks.length * 100),
76
+ message: passed ? "Contact information present in basics." : `Missing contact fields: ${missing.join(", ")}.`,
77
+ hints: passed ? [] : [`Add ${missing.join(", ")} to basics.`]
78
+ };
79
+ };
80
+ var reverseChronOrder = (resume) => {
81
+ const work = resume.work || [];
82
+ if (work.length < 2) {
83
+ return {
84
+ id: "reverse-chron-order",
85
+ tier: "parsing",
86
+ weight: "medium",
87
+ status: "skipped",
88
+ score: 100,
89
+ message: "Single or no work entry.",
90
+ hints: []
91
+ };
92
+ }
93
+ let outOfOrder = 0;
94
+ for (let i = 0; i < work.length - 1; i++) {
95
+ const curr = work[i];
96
+ const next = work[i + 1];
97
+ const a = curr?.startDate || "";
98
+ const b = next?.startDate || "";
99
+ if (a && b && a < b) outOfOrder++;
100
+ }
101
+ const passed = outOfOrder === 0;
102
+ return {
103
+ id: "reverse-chron-order",
104
+ tier: "parsing",
105
+ weight: "medium",
106
+ status: passed ? "pass" : "fail",
107
+ score: passed ? 100 : Math.max(0, 100 - outOfOrder * 50),
108
+ message: passed ? "Work entries in reverse-chronological order." : `${outOfOrder} pair(s) out of reverse-chronological order.`,
109
+ hints: passed ? [] : ["Reorder work[] so the most recent role is first."]
110
+ };
111
+ };
112
+ var educationComplete = (resume) => {
113
+ const edu = resume.education || [];
114
+ if (edu.length === 0) {
115
+ return {
116
+ id: "education-complete",
117
+ tier: "parsing",
118
+ weight: "low",
119
+ status: "fail",
120
+ score: 0,
121
+ message: "No education entries.",
122
+ hints: ["Add at least one education entry with institution, area, and studyType."]
123
+ };
124
+ }
125
+ const incomplete = edu.filter((e) => !e.institution || !e.area || !e.studyType);
126
+ const passed = incomplete.length === 0;
127
+ return {
128
+ id: "education-complete",
129
+ tier: "parsing",
130
+ weight: "low",
131
+ status: passed ? "pass" : "fail",
132
+ score: Math.round((edu.length - incomplete.length) / edu.length * 100),
133
+ message: passed ? "All education entries complete." : `${incomplete.length} education entry(ies) missing institution/area/studyType.`,
134
+ hints: passed ? [] : ["Fill in institution, area and studyType for every education entry."]
135
+ };
136
+ };
137
+ var allParsingChecks = [
138
+ conventionalSections,
139
+ dateFormatConsistency,
140
+ contactInBody,
141
+ reverseChronOrder,
142
+ educationComplete
143
+ ];
144
+
145
+ // src/ats/i18n/en.ts
146
+ var en = {
147
+ actionVerbs: [
148
+ // Leadership & Management
149
+ "achieved",
150
+ "administered",
151
+ "advanced",
152
+ "allocated",
153
+ "approved",
154
+ "assigned",
155
+ "authorized",
156
+ "chaired",
157
+ "consolidated",
158
+ "coordinated",
159
+ "delegated",
160
+ "directed",
161
+ "established",
162
+ "executed",
163
+ "headed",
164
+ "hired",
165
+ "hosted",
166
+ "led",
167
+ "managed",
168
+ "mentored",
169
+ "motivated",
170
+ "orchestrated",
171
+ "organized",
172
+ "oversaw",
173
+ "planned",
174
+ "presided",
175
+ "prioritized",
176
+ "produced",
177
+ "recruited",
178
+ "spearheaded",
179
+ "supervised",
180
+ // Technical & Engineering
181
+ "architected",
182
+ "automated",
183
+ "built",
184
+ "coded",
185
+ "configured",
186
+ "debugged",
187
+ "deployed",
188
+ "designed",
189
+ "developed",
190
+ "devised",
191
+ "engineered",
192
+ "implemented",
193
+ "installed",
194
+ "integrated",
195
+ "launched",
196
+ "maintained",
197
+ "migrated",
198
+ "modernized",
199
+ "optimized",
200
+ "overhauled",
201
+ "programmed",
202
+ "prototyped",
203
+ "refactored",
204
+ "reengineered",
205
+ "resolved",
206
+ "restructured",
207
+ "revamped",
208
+ "scaled",
209
+ "shipped",
210
+ "standardized",
211
+ "streamlined",
212
+ "tested",
213
+ "troubleshot",
214
+ "upgraded",
215
+ // Achievement & Impact
216
+ "accelerated",
217
+ "accomplished",
218
+ "boosted",
219
+ "completed",
220
+ "contributed",
221
+ "converted",
222
+ "decreased",
223
+ "delivered",
224
+ "doubled",
225
+ "earned",
226
+ "eliminated",
227
+ "exceeded",
228
+ "expanded",
229
+ "expedited",
230
+ "generated",
231
+ "grew",
232
+ "improved",
233
+ "increased",
234
+ "maximized",
235
+ "minimized",
236
+ "outperformed",
237
+ "pioneered",
238
+ "recovered",
239
+ "reduced",
240
+ "saved",
241
+ "simplified",
242
+ "solved",
243
+ "surpassed",
244
+ "transformed",
245
+ "tripled",
246
+ // Communication & Collaboration
247
+ "advised",
248
+ "advocated",
249
+ "briefed",
250
+ "collaborated",
251
+ "communicated",
252
+ "consulted",
253
+ "convinced",
254
+ "counseled",
255
+ "defined",
256
+ "demonstrated",
257
+ "documented",
258
+ "educated",
259
+ "facilitated",
260
+ "guided",
261
+ "influenced",
262
+ "informed",
263
+ "instructed",
264
+ "liaised",
265
+ "negotiated",
266
+ "partnered",
267
+ "persuaded",
268
+ "presented",
269
+ "promoted",
270
+ "proposed",
271
+ "published",
272
+ "recommended",
273
+ "represented",
274
+ "trained",
275
+ // Analysis & Research
276
+ "analyzed",
277
+ "assessed",
278
+ "audited",
279
+ "benchmarked",
280
+ "calculated",
281
+ "compared",
282
+ "compiled",
283
+ "conducted",
284
+ "discovered",
285
+ "evaluated",
286
+ "examined",
287
+ "explored",
288
+ "forecasted",
289
+ "identified",
290
+ "inspected",
291
+ "interpreted",
292
+ "investigated",
293
+ "mapped",
294
+ "measured",
295
+ "modeled",
296
+ "monitored",
297
+ "quantified",
298
+ "researched",
299
+ "reviewed",
300
+ "surveyed",
301
+ "synthesized",
302
+ "tracked",
303
+ "validated",
304
+ "verified",
305
+ // Creation & Innovation
306
+ "conceptualized",
307
+ "crafted",
308
+ "created",
309
+ "customized",
310
+ "formulated",
311
+ "founded",
312
+ "initiated",
313
+ "innovated",
314
+ "introduced",
315
+ "invented",
316
+ "originated",
317
+ "shaped"
318
+ ],
319
+ pronouns: ["i", "me", "my", "mine", "myself", "we", "our", "ours"],
320
+ stopWords: [
321
+ // Articles & determiners
322
+ "a",
323
+ "an",
324
+ "the",
325
+ "and",
326
+ "or",
327
+ "but",
328
+ "in",
329
+ "on",
330
+ "at",
331
+ "to",
332
+ "for",
333
+ "of",
334
+ "with",
335
+ "by",
336
+ "from",
337
+ "is",
338
+ "was",
339
+ "are",
340
+ "were",
341
+ "be",
342
+ "been",
343
+ "being",
344
+ "have",
345
+ "has",
346
+ "had",
347
+ "do",
348
+ "does",
349
+ "did",
350
+ "will",
351
+ "would",
352
+ "could",
353
+ "should",
354
+ "may",
355
+ "might",
356
+ "shall",
357
+ "can",
358
+ "this",
359
+ "that",
360
+ "these",
361
+ "those",
362
+ "it",
363
+ "its",
364
+ "as",
365
+ "if",
366
+ "not",
367
+ "no",
368
+ "so",
369
+ "up",
370
+ "out",
371
+ "about",
372
+ "into",
373
+ "over",
374
+ "after",
375
+ "before",
376
+ "between",
377
+ "under",
378
+ "above",
379
+ "below",
380
+ "all",
381
+ "each",
382
+ "every",
383
+ "both",
384
+ "few",
385
+ "more",
386
+ "most",
387
+ "other",
388
+ "some",
389
+ "such",
390
+ "than",
391
+ "too",
392
+ "very",
393
+ // Pronouns & possessives (also checked by pronoun check, but filter from JD keywords)
394
+ "you",
395
+ "your",
396
+ "yours",
397
+ "yourself",
398
+ "we",
399
+ "our",
400
+ "ours",
401
+ "ourselves",
402
+ "they",
403
+ "them",
404
+ "their",
405
+ "theirs",
406
+ "he",
407
+ "she",
408
+ "his",
409
+ "her",
410
+ "hers",
411
+ "who",
412
+ "whom",
413
+ "whose",
414
+ "which",
415
+ "what",
416
+ "where",
417
+ "when",
418
+ "how",
419
+ "why",
420
+ // Common JD filler words (not meaningful for skill matching)
421
+ "able",
422
+ "also",
423
+ "across",
424
+ "already",
425
+ "always",
426
+ "among",
427
+ "any",
428
+ "apply",
429
+ "become",
430
+ "believe",
431
+ "best",
432
+ "bring",
433
+ "change",
434
+ "come",
435
+ "committed",
436
+ "company",
437
+ "comfortable",
438
+ "critical",
439
+ "current",
440
+ "day",
441
+ "desired",
442
+ "either",
443
+ "end",
444
+ "ensure",
445
+ "environment",
446
+ "equal",
447
+ "even",
448
+ "excellent",
449
+ "exciting",
450
+ "exceptional",
451
+ "expected",
452
+ "experience",
453
+ "fast",
454
+ "field",
455
+ "find",
456
+ "first",
457
+ "focused",
458
+ "follow",
459
+ "get",
460
+ "give",
461
+ "go",
462
+ "going",
463
+ "good",
464
+ "great",
465
+ "group",
466
+ "grow",
467
+ "growing",
468
+ "growth",
469
+ "help",
470
+ "here",
471
+ "high",
472
+ "highly",
473
+ "ideal",
474
+ "impact",
475
+ "important",
476
+ "include",
477
+ "includes",
478
+ "including",
479
+ "industry",
480
+ "interested",
481
+ "job",
482
+ "join",
483
+ "just",
484
+ "keep",
485
+ "key",
486
+ "know",
487
+ "large",
488
+ "latest",
489
+ "lead",
490
+ "level",
491
+ "like",
492
+ "location",
493
+ "long",
494
+ "look",
495
+ "looking",
496
+ "love",
497
+ "make",
498
+ "many",
499
+ "much",
500
+ "must",
501
+ "need",
502
+ "new",
503
+ "next",
504
+ "offer",
505
+ "one",
506
+ "only",
507
+ "open",
508
+ "opportunity",
509
+ "order",
510
+ "others",
511
+ "own",
512
+ "pace",
513
+ "part",
514
+ "partner",
515
+ "passionate",
516
+ "people",
517
+ "per",
518
+ "play",
519
+ "plus",
520
+ "position",
521
+ "preferred",
522
+ "provide",
523
+ "put",
524
+ "qualifications",
525
+ "quickly",
526
+ "range",
527
+ "related",
528
+ "required",
529
+ "requirements",
530
+ "requirement",
531
+ "responsible",
532
+ "responsibilities",
533
+ "responsibility",
534
+ "result",
535
+ "right",
536
+ "role",
537
+ "run",
538
+ "same",
539
+ "see",
540
+ "seek",
541
+ "seeking",
542
+ "set",
543
+ "several",
544
+ "since",
545
+ "skills",
546
+ "someone",
547
+ "start",
548
+ "state",
549
+ "still",
550
+ "strong",
551
+ "success",
552
+ "successful",
553
+ "support",
554
+ "sure",
555
+ "take",
556
+ "team",
557
+ "then",
558
+ "there",
559
+ "thing",
560
+ "think",
561
+ "through",
562
+ "time",
563
+ "together",
564
+ "top",
565
+ "truly",
566
+ "try",
567
+ "two",
568
+ "type",
569
+ "use",
570
+ "used",
571
+ "using",
572
+ "value",
573
+ "want",
574
+ "way",
575
+ "well",
576
+ "while",
577
+ "within",
578
+ "without",
579
+ "work",
580
+ "working",
581
+ "world",
582
+ "would",
583
+ "year",
584
+ "years",
585
+ // Section headers & structural words (not technical skills)
586
+ "description",
587
+ "overview",
588
+ "summary",
589
+ "duties",
590
+ "bachelor",
591
+ "bachelors",
592
+ "master",
593
+ "masters",
594
+ "degree",
595
+ "phd",
596
+ "minimum",
597
+ "preferred",
598
+ "implement",
599
+ "process",
600
+ "robust",
601
+ "consistent",
602
+ "operations",
603
+ // URL/email/domain fragments
604
+ "http",
605
+ "https",
606
+ "www",
607
+ "com",
608
+ "org",
609
+ "net",
610
+ "mailto",
611
+ // Resume/YAML schema field names (in case raw YAML is pasted)
612
+ "name",
613
+ "keywords",
614
+ "highlights",
615
+ "startdate",
616
+ "enddate",
617
+ "website",
618
+ "profiles",
619
+ "basics",
620
+ "position",
621
+ "institution",
622
+ "studytype",
623
+ "fluency",
624
+ "issuer",
625
+ "network",
626
+ "username",
627
+ "countrycode",
628
+ "region",
629
+ // Generic nouns that aren't skills
630
+ "product",
631
+ "company",
632
+ "service",
633
+ "services",
634
+ "platform",
635
+ "solutions",
636
+ "ability",
637
+ "opportunity",
638
+ "candidate",
639
+ "applicant",
640
+ "position",
641
+ "salary",
642
+ "compensation",
643
+ "benefits",
644
+ "perks",
645
+ "bonus",
646
+ "development",
647
+ "management",
648
+ "knowledge",
649
+ "modern",
650
+ "advanced",
651
+ "practices",
652
+ "nice",
653
+ "technologies",
654
+ "technology",
655
+ "frameworks",
656
+ "framework",
657
+ "tools",
658
+ "data",
659
+ "based",
660
+ "contribute",
661
+ "contributions",
662
+ "migration",
663
+ "leading",
664
+ "source",
665
+ "visit",
666
+ // Common verbs & verb forms (not technical skills, supplement action verbs list)
667
+ "collaborate",
668
+ "collaborating",
669
+ "collaboratively",
670
+ "communicate",
671
+ "communicating",
672
+ "contributing",
673
+ "coordinate",
674
+ "coordinating",
675
+ "demonstrate",
676
+ "demonstrating",
677
+ "design",
678
+ "designing",
679
+ "designed",
680
+ "develop",
681
+ "developing",
682
+ "developed",
683
+ "drive",
684
+ "driving",
685
+ "driven",
686
+ "enable",
687
+ "enabling",
688
+ "evaluate",
689
+ "evaluating",
690
+ "execute",
691
+ "executing",
692
+ "facilitate",
693
+ "facilitating",
694
+ "identify",
695
+ "identifying",
696
+ "influence",
697
+ "influencing",
698
+ "interact",
699
+ "interacting",
700
+ "lead",
701
+ "leverage",
702
+ "leveraging",
703
+ "manage",
704
+ "managing",
705
+ "mentor",
706
+ "mentoring",
707
+ "operate",
708
+ "operating",
709
+ "optimize",
710
+ "optimizing",
711
+ "participate",
712
+ "participating",
713
+ "report",
714
+ "reporting",
715
+ "solve",
716
+ "solving",
717
+ "understand",
718
+ "understanding",
719
+ // Common adjectives & descriptors (not technical skills)
720
+ "fluent",
721
+ "proficient",
722
+ "deep",
723
+ "solid",
724
+ "proven",
725
+ "hands-on",
726
+ "detail-oriented",
727
+ "results-driven",
728
+ "self-motivated",
729
+ "proactive",
730
+ "creative",
731
+ "innovative",
732
+ "dynamic",
733
+ "strategic",
734
+ "analytical",
735
+ "collaborative",
736
+ "effective",
737
+ "efficient",
738
+ "reliable",
739
+ "flexible",
740
+ "adaptable",
741
+ "motivated",
742
+ "dedicated",
743
+ "capable",
744
+ "qualified",
745
+ "diverse",
746
+ "inclusive",
747
+ "global",
748
+ "local",
749
+ "remote",
750
+ "hybrid",
751
+ "onsite",
752
+ "full-time",
753
+ "part-time",
754
+ "contract",
755
+ "permanent",
756
+ // Role titles & department names (not skills themselves)
757
+ "designer",
758
+ "designers",
759
+ "developer",
760
+ "developers",
761
+ "engineer",
762
+ "engineers",
763
+ "manager",
764
+ "managers",
765
+ "director",
766
+ "analyst",
767
+ "analysts",
768
+ "architect",
769
+ "architects",
770
+ "consultant",
771
+ "consultants",
772
+ "specialist",
773
+ "specialists",
774
+ "coordinator",
775
+ "lead",
776
+ "principal",
777
+ "staff",
778
+ "junior",
779
+ "mid",
780
+ "department",
781
+ "organization",
782
+ "division",
783
+ "stakeholder",
784
+ "stakeholders",
785
+ "client",
786
+ "clients",
787
+ "customer",
788
+ "customers",
789
+ // Date & time words
790
+ "date",
791
+ "dates",
792
+ "month",
793
+ "months",
794
+ "week",
795
+ "weeks",
796
+ "daily",
797
+ "weekly",
798
+ "monthly",
799
+ "quarterly",
800
+ "annual",
801
+ "annually",
802
+ // More generic words that aren't skills
803
+ "code",
804
+ "coding",
805
+ "url",
806
+ "contact",
807
+ "information",
808
+ "apply",
809
+ "application",
810
+ "review",
811
+ "reviews",
812
+ "quality",
813
+ "scale",
814
+ "scalable",
815
+ "system",
816
+ "systems",
817
+ "solution",
818
+ "feature",
819
+ "features",
820
+ "project",
821
+ "projects",
822
+ "build",
823
+ "building",
824
+ "deliver",
825
+ "delivery",
826
+ "cross-functional"
827
+ ]
828
+ };
829
+ var en_default = en;
830
+
831
+ // src/ats/i18n/de.ts
832
+ var de = {
833
+ actionVerbs: [
834
+ // Führung & Management
835
+ "geleitet",
836
+ "gef\xFChrt",
837
+ "koordiniert",
838
+ "organisiert",
839
+ "verwaltet",
840
+ "delegiert",
841
+ "beaufsichtigt",
842
+ "betreut",
843
+ "eingestellt",
844
+ "motiviert",
845
+ "verantwortet",
846
+ "gesteuert",
847
+ "\xFCberwacht",
848
+ "priorisiert",
849
+ "geplant",
850
+ // Technik & Entwicklung
851
+ "entwickelt",
852
+ "implementiert",
853
+ "programmiert",
854
+ "konfiguriert",
855
+ "automatisiert",
856
+ "deployt",
857
+ "gebaut",
858
+ "entworfen",
859
+ "integriert",
860
+ "migriert",
861
+ "modernisiert",
862
+ "optimiert",
863
+ "refaktoriert",
864
+ "skaliert",
865
+ "standardisiert",
866
+ "getestet",
867
+ "aufgebaut",
868
+ "eingef\xFChrt",
869
+ "bereitgestellt",
870
+ "umgesetzt",
871
+ // Leistung & Ergebnisse
872
+ "verbessert",
873
+ "gesteigert",
874
+ "reduziert",
875
+ "beschleunigt",
876
+ "erreicht",
877
+ "\xFCbertroffen",
878
+ "erweitert",
879
+ "vereinfacht",
880
+ "gel\xF6st",
881
+ "transformiert",
882
+ "erh\xF6ht",
883
+ "verdoppelt",
884
+ "verdreifacht",
885
+ "generiert",
886
+ "gespart",
887
+ "maximiert",
888
+ "minimiert",
889
+ "eliminiert",
890
+ "geliefert",
891
+ "abgeschlossen",
892
+ // Kommunikation & Zusammenarbeit
893
+ "beraten",
894
+ "pr\xE4sentiert",
895
+ "dokumentiert",
896
+ "geschult",
897
+ "trainiert",
898
+ "vermittelt",
899
+ "kommuniziert",
900
+ "verhandelt",
901
+ "zusammengearbeitet",
902
+ "unterst\xFCtzt",
903
+ "gef\xF6rdert",
904
+ "empfohlen",
905
+ "vorgestellt",
906
+ "publiziert",
907
+ // Analyse & Forschung
908
+ "analysiert",
909
+ "bewertet",
910
+ "evaluiert",
911
+ "untersucht",
912
+ "erforscht",
913
+ "identifiziert",
914
+ "gemessen",
915
+ "\xFCberwacht",
916
+ "validiert",
917
+ "verifiziert",
918
+ "gepr\xFCft",
919
+ "verglichen",
920
+ "recherchiert",
921
+ "quantifiziert",
922
+ // Kreation & Innovation
923
+ "konzipiert",
924
+ "erstellt",
925
+ "gestaltet",
926
+ "initiiert",
927
+ "innoviert",
928
+ "eingef\xFChrt",
929
+ "gegr\xFCndet",
930
+ "formuliert"
931
+ ],
932
+ pronouns: [
933
+ "ich",
934
+ "mich",
935
+ "mir",
936
+ "mein",
937
+ "meine",
938
+ "meinem",
939
+ "meiner",
940
+ "meines",
941
+ "wir",
942
+ "unser",
943
+ "unsere"
944
+ ],
945
+ stopWords: [
946
+ "ein",
947
+ "eine",
948
+ "einer",
949
+ "eines",
950
+ "einem",
951
+ "der",
952
+ "die",
953
+ "das",
954
+ "den",
955
+ "dem",
956
+ "des",
957
+ "und",
958
+ "oder",
959
+ "aber",
960
+ "in",
961
+ "an",
962
+ "auf",
963
+ "zu",
964
+ "f\xFCr",
965
+ "von",
966
+ "mit",
967
+ "bei",
968
+ "aus",
969
+ "ist",
970
+ "war",
971
+ "sind",
972
+ "waren",
973
+ "wird",
974
+ "wurde",
975
+ "werden",
976
+ "hat",
977
+ "hatte",
978
+ "haben",
979
+ "hatten",
980
+ "sein",
981
+ "kann",
982
+ "k\xF6nnte",
983
+ "soll",
984
+ "sollte",
985
+ "muss",
986
+ "musste",
987
+ "darf",
988
+ "diese",
989
+ "dieser",
990
+ "dieses",
991
+ "diesem",
992
+ "diesen",
993
+ "als",
994
+ "wenn",
995
+ "nicht",
996
+ "kein",
997
+ "keine",
998
+ "so",
999
+ "auch",
1000
+ "noch",
1001
+ "schon",
1002
+ "nach",
1003
+ "vor",
1004
+ "\xFCber",
1005
+ "unter",
1006
+ "zwischen",
1007
+ "durch",
1008
+ "ohne",
1009
+ "um",
1010
+ "bis",
1011
+ "alle",
1012
+ "jede",
1013
+ "jeder",
1014
+ "jedes",
1015
+ "mehr",
1016
+ "viel",
1017
+ "sehr"
1018
+ ]
1019
+ };
1020
+ var de_default = de;
1021
+
1022
+ // src/ats/i18n/index.ts
1023
+ var languages = { en: en_default, de: de_default };
1024
+ function getLanguageData(language) {
1025
+ return languages[language] ?? languages["en"] ?? en_default;
1026
+ }
1027
+
1028
+ // src/ats/checks/yoe.ts
1029
+ function computeYoeYears(work) {
1030
+ const ranges = (work || []).filter((w) => w.startDate).map((w) => {
1031
+ const s = new Date(w.startDate).getTime();
1032
+ const e = w.endDate ? new Date(w.endDate).getTime() : Date.now();
1033
+ return [s, e];
1034
+ }).sort((a, b) => a[0] - b[0]);
1035
+ if (ranges.length === 0) return 0;
1036
+ const merged = [ranges[0]];
1037
+ for (let i = 1; i < ranges.length; i++) {
1038
+ const last = merged[merged.length - 1];
1039
+ const curr = ranges[i];
1040
+ if (curr[0] <= last[1]) last[1] = Math.max(last[1], curr[1]);
1041
+ else merged.push(curr);
1042
+ }
1043
+ const ms = merged.reduce((sum, [s, e]) => sum + (e - s), 0);
1044
+ return ms / (1e3 * 60 * 60 * 24 * 365.25);
1045
+ }
1046
+
1047
+ // src/ats/checks/recruiter.ts
1048
+ var wordCount = (s) => s.trim().split(/\s+/).filter(Boolean).length;
1049
+ var firstWord = (s) => s.trim().split(/\s+/)[0]?.toLowerCase().replace(/[^a-zA-ZäöüßÄÖÜàáâãéèêëíìîïóòôõúùûñç]/g, "") || "";
1050
+ function isSenior(resume, cfg) {
1051
+ return computeYoeYears(resume.work) >= cfg.thresholds.seniorYoeCutoff;
1052
+ }
1053
+ var summaryLength = (resume) => {
1054
+ const s = resume.basics?.summary?.trim();
1055
+ if (!s) {
1056
+ return {
1057
+ id: "summary-length",
1058
+ tier: "recruiter",
1059
+ weight: "medium",
1060
+ status: "fail",
1061
+ score: 0,
1062
+ message: "No professional summary.",
1063
+ hints: ["Add a 2-4 sentence summary (20-50 words) to basics.summary."]
1064
+ };
1065
+ }
1066
+ const w = wordCount(s);
1067
+ if (w >= 20 && w <= 50) {
1068
+ return {
1069
+ id: "summary-length",
1070
+ tier: "recruiter",
1071
+ weight: "medium",
1072
+ status: "pass",
1073
+ score: 100,
1074
+ message: `Summary length good (${w} words).`,
1075
+ hints: []
1076
+ };
1077
+ }
1078
+ if (w >= 10 && w <= 80) {
1079
+ return {
1080
+ id: "summary-length",
1081
+ tier: "recruiter",
1082
+ weight: "medium",
1083
+ status: "warn",
1084
+ score: 70,
1085
+ message: `Summary ${w < 20 ? "short" : "long"} (${w} words). Aim for 20-50.`,
1086
+ hints: [w < 20 ? "Expand to 20-50 words." : "Trim to the most impactful 20-50 words."]
1087
+ };
1088
+ }
1089
+ return {
1090
+ id: "summary-length",
1091
+ tier: "recruiter",
1092
+ weight: "medium",
1093
+ status: "fail",
1094
+ score: Math.max(0, 100 - Math.abs(w - 35) * 2),
1095
+ message: `Summary length ${w} words is far from target.`,
1096
+ hints: ["Rewrite the summary in 2-4 sentences (20-50 words)."]
1097
+ };
1098
+ };
1099
+ var actionVerbStart = (resume, language) => {
1100
+ const verbs = new Set(getLanguageData(language).actionVerbs);
1101
+ const all = [];
1102
+ (resume.work || []).forEach((w, i) => {
1103
+ (w.highlights || []).forEach(
1104
+ (h, j) => all.push({ text: h, path: `work[${i}].highlights[${j}]` })
1105
+ );
1106
+ });
1107
+ (resume.projects || []).forEach((p, i) => {
1108
+ (p.highlights || []).forEach(
1109
+ (h, j) => all.push({ text: h, path: `projects[${i}].highlights[${j}]` })
1110
+ );
1111
+ });
1112
+ if (all.length === 0) {
1113
+ return {
1114
+ id: "action-verb-start",
1115
+ tier: "recruiter",
1116
+ weight: "medium",
1117
+ status: "skipped",
1118
+ score: 0,
1119
+ message: "No highlights.",
1120
+ hints: []
1121
+ };
1122
+ }
1123
+ const without = all.filter((h) => !verbs.has(firstWord(h.text)));
1124
+ const passed = without.length === 0;
1125
+ return {
1126
+ id: "action-verb-start",
1127
+ tier: "recruiter",
1128
+ weight: "medium",
1129
+ status: passed ? "pass" : without.length / all.length > 0.3 ? "fail" : "warn",
1130
+ score: Math.round((all.length - without.length) / all.length * 100),
1131
+ message: passed ? "All highlights start with action verbs." : `${without.length} of ${all.length} highlights miss an action verb.`,
1132
+ hints: passed ? [] : without.slice(0, 3).map((h) => `${h.path}: start with an action verb instead of "${firstWord(h.text)}".`)
1133
+ };
1134
+ };
1135
+ var QUANT_RE = /\d+%?|\$[\d,]+|[\d,]+\+?\s*(users|clients|customers|people|team|members|projects|applications|servers|services|endpoints|requests|transactions)/i;
1136
+ var quantificationDensity = (resume) => {
1137
+ const all = [];
1138
+ (resume.work || []).forEach((w, i) => {
1139
+ (w.highlights || []).forEach(
1140
+ (h, j) => all.push({ text: h, path: `work[${i}].highlights[${j}]` })
1141
+ );
1142
+ });
1143
+ (resume.projects || []).forEach((p, i) => {
1144
+ (p.highlights || []).forEach(
1145
+ (h, j) => all.push({ text: h, path: `projects[${i}].highlights[${j}]` })
1146
+ );
1147
+ });
1148
+ if (all.length === 0) {
1149
+ return {
1150
+ id: "quantification-density",
1151
+ tier: "recruiter",
1152
+ weight: "high",
1153
+ status: "skipped",
1154
+ score: 0,
1155
+ message: "No highlights.",
1156
+ hints: []
1157
+ };
1158
+ }
1159
+ const quantified = all.filter((h) => QUANT_RE.test(h.text));
1160
+ const ratio = quantified.length / all.length;
1161
+ const status = ratio >= 0.5 ? "pass" : ratio >= 0.3 ? "warn" : "fail";
1162
+ return {
1163
+ id: "quantification-density",
1164
+ tier: "recruiter",
1165
+ weight: "high",
1166
+ status,
1167
+ score: Math.min(100, Math.round(ratio * 200)),
1168
+ message: `${quantified.length}/${all.length} highlights quantified (${Math.round(ratio * 100)}%).`,
1169
+ hints: status === "pass" ? [] : all.filter((h) => !QUANT_RE.test(h.text)).slice(0, 3).map((h) => `${h.path}: add a number/metric.`)
1170
+ };
1171
+ };
1172
+ var pronounLeakage = (resume, language) => {
1173
+ const set = new Set(getLanguageData(language).pronouns);
1174
+ const blocks = [];
1175
+ if (resume.basics?.summary) blocks.push({ text: resume.basics.summary, path: "basics.summary" });
1176
+ (resume.work || []).forEach((w, i) => {
1177
+ if (w.summary) blocks.push({ text: w.summary, path: `work[${i}].summary` });
1178
+ (w.highlights || []).forEach(
1179
+ (h, j) => blocks.push({ text: h, path: `work[${i}].highlights[${j}]` })
1180
+ );
1181
+ });
1182
+ const hits = [];
1183
+ for (const b of blocks) {
1184
+ for (const w of b.text.toLowerCase().split(/\s+/)) {
1185
+ const clean = w.replace(/[^a-zA-ZäöüßÄÖÜ]/g, "");
1186
+ if (set.has(clean)) hits.push({ pronoun: clean, path: b.path });
1187
+ }
1188
+ }
1189
+ const passed = hits.length === 0;
1190
+ return {
1191
+ id: "pronoun-leakage",
1192
+ tier: "recruiter",
1193
+ weight: "low",
1194
+ status: passed ? "pass" : hits.length > 3 ? "fail" : "warn",
1195
+ score: passed ? 100 : Math.max(0, 100 - hits.length * 15),
1196
+ message: passed ? "No first-person pronouns." : `${hits.length} pronoun(s): ${[...new Set(hits.map((h) => h.pronoun))].join(", ")}.`,
1197
+ hints: passed ? [] : hits.slice(0, 3).map((h) => `${h.path}: drop "${h.pronoun}" (convention, not ATS).`)
1198
+ };
1199
+ };
1200
+ var bulletsPerRole = (resume, _l, cfg) => {
1201
+ const work = resume.work || [];
1202
+ if (work.length === 0) {
1203
+ return {
1204
+ id: "bullets-per-role",
1205
+ tier: "recruiter",
1206
+ weight: "medium",
1207
+ status: "skipped",
1208
+ score: 0,
1209
+ message: "No work entries.",
1210
+ hints: []
1211
+ };
1212
+ }
1213
+ const senior = isSenior(resume, cfg);
1214
+ const min = cfg.thresholds.bulletsPerRole.min;
1215
+ const max = senior ? cfg.thresholds.bulletsPerRole.seniorMax : cfg.thresholds.bulletsPerRole.max;
1216
+ const offenders = [];
1217
+ work.forEach((w, i) => {
1218
+ const n = (w.highlights || []).length;
1219
+ if (n < min || n > max) offenders.push({ path: `work[${i}]`, n });
1220
+ });
1221
+ const passed = offenders.length === 0;
1222
+ return {
1223
+ id: "bullets-per-role",
1224
+ tier: "recruiter",
1225
+ weight: "medium",
1226
+ status: passed ? "pass" : work.length === 1 ? "warn" : offenders.length > work.length / 2 ? "fail" : "warn",
1227
+ score: Math.round((work.length - offenders.length) / work.length * 100),
1228
+ message: passed ? `All roles have ${min}-${max} highlights.` : `${offenders.length} role(s) outside ${min}-${max} highlights.`,
1229
+ hints: passed ? [] : offenders.slice(0, 3).map((o) => `${o.path}: ${o.n} highlights, target ${min}-${max}.`)
1230
+ };
1231
+ };
1232
+ var wordCountTotal = (resume, _l, cfg) => {
1233
+ const senior = isSenior(resume, cfg);
1234
+ const min = cfg.thresholds.wordCount.min;
1235
+ const max = senior ? cfg.thresholds.wordCount.seniorMax : cfg.thresholds.wordCount.max;
1236
+ const parts = [];
1237
+ if (resume.basics?.summary) parts.push(resume.basics.summary);
1238
+ for (const w of resume.work || []) {
1239
+ if (w.summary) parts.push(w.summary);
1240
+ parts.push(...w.highlights || []);
1241
+ }
1242
+ for (const p of resume.projects || []) {
1243
+ if (p.description) parts.push(p.description);
1244
+ parts.push(...p.highlights || []);
1245
+ }
1246
+ const total = wordCount(parts.join(" "));
1247
+ const passed = total >= min && total <= max;
1248
+ const status = passed ? "pass" : total < min * 0.7 || total > max * 1.5 ? "fail" : "warn";
1249
+ return {
1250
+ id: "word-count-total",
1251
+ tier: "recruiter",
1252
+ weight: "low",
1253
+ status,
1254
+ score: passed ? 100 : Math.max(0, 100 - Math.round(Math.abs(total - (min + max) / 2) / 10)),
1255
+ message: `Resume body: ${total} words (target ${min}-${max}${senior ? ", senior" : ""}).`,
1256
+ hints: passed ? [] : [total < min ? "Add more depth to highlights." : "Trim less impactful highlights."]
1257
+ };
1258
+ };
1259
+ var highlightLength = (resume) => {
1260
+ const all = [];
1261
+ (resume.work || []).forEach((w, i) => {
1262
+ (w.highlights || []).forEach(
1263
+ (h, j) => all.push({ text: h, path: `work[${i}].highlights[${j}]` })
1264
+ );
1265
+ });
1266
+ if (all.length === 0) {
1267
+ return {
1268
+ id: "highlight-length",
1269
+ tier: "recruiter",
1270
+ weight: "low",
1271
+ status: "skipped",
1272
+ score: 0,
1273
+ message: "No highlights.",
1274
+ hints: []
1275
+ };
1276
+ }
1277
+ const long = all.filter((h) => wordCount(h.text) > 30);
1278
+ const passed = long.length === 0;
1279
+ return {
1280
+ id: "highlight-length",
1281
+ tier: "recruiter",
1282
+ weight: "low",
1283
+ status: passed ? "pass" : long.length / all.length > 0.3 ? "fail" : "warn",
1284
+ score: Math.round((all.length - long.length) / all.length * 100),
1285
+ message: passed ? "All highlights at most 30 words." : `${long.length} highlight(s) over 30 words.`,
1286
+ hints: passed ? [] : long.slice(0, 3).map((h) => `${h.path}: trim to 30 words or fewer.`)
1287
+ };
1288
+ };
1289
+ function isLinkedInUrl(url) {
1290
+ if (!url) return false;
1291
+ try {
1292
+ const host = new URL(url).hostname.toLowerCase();
1293
+ return host === "linkedin.com" || host.endsWith(".linkedin.com");
1294
+ } catch {
1295
+ return false;
1296
+ }
1297
+ }
1298
+ var hasLinkedin = (resume) => {
1299
+ const profiles = resume.basics?.profiles || [];
1300
+ const found = profiles.some(
1301
+ (p) => p.network?.toLowerCase() === "linkedin" || isLinkedInUrl(p.url)
1302
+ );
1303
+ return {
1304
+ id: "has-linkedin",
1305
+ tier: "recruiter",
1306
+ weight: "low",
1307
+ status: found ? "pass" : "warn",
1308
+ score: found ? 100 : 0,
1309
+ message: found ? "LinkedIn profile present." : "No LinkedIn profile.",
1310
+ hints: found ? [] : ["Add a LinkedIn profile to basics.profiles."]
1311
+ };
1312
+ };
1313
+ var skillsPopulated = (resume) => {
1314
+ const skills = resume.skills || [];
1315
+ const withKeywords = skills.filter((s) => (s.keywords?.length ?? 0) > 0);
1316
+ const passed = withKeywords.length >= 3;
1317
+ return {
1318
+ id: "skills-populated",
1319
+ tier: "recruiter",
1320
+ weight: "medium",
1321
+ status: passed ? "pass" : withKeywords.length < 2 ? "fail" : "warn",
1322
+ score: Math.min(100, Math.round(withKeywords.length / 3 * 100)),
1323
+ message: passed ? `${withKeywords.length} skill categories with keywords.` : `Only ${withKeywords.length} skill categories with keywords (need 3+).`,
1324
+ hints: passed ? [] : ["Add at least 3 skill categories with keywords."]
1325
+ };
1326
+ };
1327
+ var allRecruiterChecks = [
1328
+ summaryLength,
1329
+ actionVerbStart,
1330
+ quantificationDensity,
1331
+ pronounLeakage,
1332
+ bulletsPerRole,
1333
+ wordCountTotal,
1334
+ highlightLength,
1335
+ hasLinkedin,
1336
+ skillsPopulated
1337
+ ];
1338
+
1339
+ // src/ats/jdMatcher.ts
1340
+ function extractResumeText(resume) {
1341
+ const parts = [];
1342
+ if (resume.basics?.summary) parts.push(resume.basics.summary);
1343
+ if (resume.basics?.label) parts.push(resume.basics.label);
1344
+ for (const w of resume.work || []) {
1345
+ if (w.position) parts.push(w.position);
1346
+ if (w.summary) parts.push(w.summary);
1347
+ parts.push(...w.highlights || []);
1348
+ }
1349
+ for (const s of resume.skills || []) {
1350
+ if (s.name) parts.push(s.name);
1351
+ parts.push(...s.keywords || []);
1352
+ }
1353
+ for (const p of resume.projects || []) {
1354
+ if (p.name) parts.push(p.name);
1355
+ if (p.description) parts.push(p.description);
1356
+ parts.push(...p.highlights || []);
1357
+ }
1358
+ for (const e of resume.education || []) {
1359
+ if (e.area) parts.push(e.area);
1360
+ if (e.studyType) parts.push(e.studyType);
1361
+ parts.push(...e.courses || []);
1362
+ }
1363
+ for (const c of resume.certificates || []) {
1364
+ if (c.name) parts.push(c.name);
1365
+ }
1366
+ return parts.join(" ");
1367
+ }
1368
+ function splitJdSections(text) {
1369
+ const lines = text.split("\n");
1370
+ const reqPatterns = /^(required|requirements?|minimum|preferred|qualifications?|must[\s-]have|nice[\s-]to[\s-]have|what you.?ll|what we.?re looking|skills|technical|you.?ll need|responsibilities|you will)/i;
1371
+ const nonReqPatterns = /^(about|summary|who we are|our (company|team|mission)|description|overview|benefits|perks|compensation|salary)/i;
1372
+ let inReqSection = false;
1373
+ const reqLines = [];
1374
+ for (const line of lines) {
1375
+ const header = line.trim().replace(/[:#*-]/g, "").trim();
1376
+ if (reqPatterns.test(header)) inReqSection = true;
1377
+ else if (nonReqPatterns.test(header)) inReqSection = false;
1378
+ if (inReqSection) reqLines.push(line);
1379
+ }
1380
+ return {
1381
+ requirementText: reqLines.join("\n"),
1382
+ fullText: text
1383
+ };
1384
+ }
1385
+ function rankSkills(fullMatches, requirementMatches) {
1386
+ const reqIds = new Set(requirementMatches.map((m) => m.skill.id));
1387
+ return fullMatches.map((m) => {
1388
+ const inRequirementSection = reqIds.has(m.skill.id);
1389
+ const score = m.occurrences * 1 + (inRequirementSection ? 3 : 0) + (m.skill.hot ? 1.5 : 0);
1390
+ return {
1391
+ canonical: m.skill.canonical,
1392
+ occurrences: m.occurrences,
1393
+ inRequirementSection,
1394
+ hot: m.skill.hot,
1395
+ score
1396
+ };
1397
+ }).sort((a, b) => {
1398
+ if (a.inRequirementSection !== b.inRequirementSection) return a.inRequirementSection ? -1 : 1;
1399
+ if (a.score !== b.score) return b.score - a.score;
1400
+ return a.canonical.localeCompare(b.canonical);
1401
+ });
1402
+ }
1403
+ function matchJobDescription(resume, jobDescription, _language = "en") {
1404
+ if (!jobDescription.trim()) {
1405
+ return { matched: [], missing: [], extra: [], matchPercentage: 0 };
1406
+ }
1407
+ const skillIndex = getSkillIndex();
1408
+ const fullMatches = skillIndex.scan(jobDescription);
1409
+ const { requirementText } = splitJdSections(jobDescription);
1410
+ const requirementMatches = requirementText.trim() ? skillIndex.scan(requirementText) : fullMatches;
1411
+ const ranked = rankSkills(fullMatches, requirementMatches).slice(0, 25);
1412
+ const resumeText = extractResumeText(resume);
1413
+ const resumeMatches = skillIndex.scan(resumeText);
1414
+ const resumeSkillIds = new Set(resumeMatches.map((m) => m.skill.id));
1415
+ const matched = [];
1416
+ const missing = [];
1417
+ for (const r of ranked) {
1418
+ const match = fullMatches.find((m) => m.skill.canonical === r.canonical);
1419
+ if (!match) continue;
1420
+ if (resumeSkillIds.has(match.skill.id)) matched.push(r.canonical);
1421
+ else missing.push(r.canonical);
1422
+ }
1423
+ const matchPercentage = ranked.length > 0 ? Math.round(matched.length / ranked.length * 100) : 0;
1424
+ const jdSkillIds = new Set(fullMatches.map((m) => m.skill.id));
1425
+ const extra = [];
1426
+ for (const m of resumeMatches) {
1427
+ if (!jdSkillIds.has(m.skill.id)) extra.push(m.skill.canonical);
1428
+ }
1429
+ extra.splice(25);
1430
+ return { matched, missing, extra, matchPercentage };
1431
+ }
1432
+
1433
+ // src/ats/checks/match.ts
1434
+ var SENIORITY = /\b(junior|senior|lead|staff|principal|head of|vp|chief)\b/gi;
1435
+ var STOPWORDS = /* @__PURE__ */ new Set([
1436
+ "a",
1437
+ "an",
1438
+ "the",
1439
+ "of",
1440
+ "at",
1441
+ "for",
1442
+ "in",
1443
+ "on",
1444
+ "to",
1445
+ "with",
1446
+ "and",
1447
+ "or"
1448
+ ]);
1449
+ function tokenize(s) {
1450
+ return s.toLowerCase().replace(SENIORITY, "").replace(/[^a-z0-9 ]/g, " ").split(/\s+/).filter((w) => w && !STOPWORDS.has(w));
1451
+ }
1452
+ function jaccard(a, b) {
1453
+ const A = new Set(a), B = new Set(b);
1454
+ let inter = 0;
1455
+ for (const x of A) if (B.has(x)) inter++;
1456
+ const union = A.size + B.size - inter;
1457
+ return union === 0 ? 0 : inter / union;
1458
+ }
1459
+ function trimTitle(t) {
1460
+ return t.trim().replace(/\s*[,\-–—]\s.*$/, "").trim();
1461
+ }
1462
+ function extractJdTitle(jd) {
1463
+ const lines = jd.split("\n").slice(0, 8);
1464
+ for (const l of lines) {
1465
+ const m = l.match(/(?:role|position|title)[\s:-]+(.+)/i) || l.match(/looking for (?:an? )?(.+?)(?:\s+with|\s+to|$)/i);
1466
+ if (m?.[1]) return trimTitle(m[1]);
1467
+ }
1468
+ const fallback = lines.find(
1469
+ (l) => /\b(engineer|developer|manager|designer|analyst|scientist|architect|lead)\b/i.test(l)
1470
+ );
1471
+ return fallback ? trimTitle(fallback) : void 0;
1472
+ }
1473
+ var titleAlignment = (resume, _l, { jobDescription }) => {
1474
+ if (!jobDescription) {
1475
+ return {
1476
+ id: "title-alignment",
1477
+ tier: "match",
1478
+ weight: "high",
1479
+ status: "skipped",
1480
+ score: 0,
1481
+ message: "No JD.",
1482
+ hints: []
1483
+ };
1484
+ }
1485
+ const resumeTitle = resume.work?.[0]?.position || resume.basics?.label;
1486
+ const jdTitle = extractJdTitle(jobDescription);
1487
+ if (!resumeTitle || !jdTitle) {
1488
+ return {
1489
+ id: "title-alignment",
1490
+ tier: "match",
1491
+ weight: "high",
1492
+ status: "warn",
1493
+ score: 50,
1494
+ message: "Could not extract title from JD or resume.",
1495
+ hints: ["Set basics.label to your target title."]
1496
+ };
1497
+ }
1498
+ const j = jaccard(tokenize(resumeTitle), tokenize(jdTitle));
1499
+ const status = j >= 0.6 ? "pass" : j >= 0.3 ? "warn" : "fail";
1500
+ return {
1501
+ id: "title-alignment",
1502
+ tier: "match",
1503
+ weight: "high",
1504
+ status,
1505
+ score: Math.round(j * 100),
1506
+ message: `Title overlap ${Math.round(j * 100)}% (resume "${resumeTitle}" vs JD "${jdTitle}").`,
1507
+ hints: status === "pass" ? [] : [`Consider aligning basics.label closer to "${jdTitle}".`]
1508
+ };
1509
+ };
1510
+ var EDU_RE = {
1511
+ 3: /\b(phd|ph\.?d|doctorate|doctoral)\b/i,
1512
+ 2: /\b(master|m\.?s|m\.?a|mba|graduate degree)\b/i,
1513
+ 1: /\b(bachelor|b\.?s|b\.?a|undergraduate)\b/i
1514
+ };
1515
+ function eduLevel(text) {
1516
+ if (!text) return 0;
1517
+ if (EDU_RE[3].test(text)) return 3;
1518
+ if (EDU_RE[2].test(text)) return 2;
1519
+ if (EDU_RE[1].test(text)) return 1;
1520
+ return 0;
1521
+ }
1522
+ var educationLevel = (resume, _l, { jobDescription }) => {
1523
+ if (!jobDescription) {
1524
+ return {
1525
+ id: "education-level",
1526
+ tier: "match",
1527
+ weight: "medium",
1528
+ status: "skipped",
1529
+ score: 0,
1530
+ message: "No JD.",
1531
+ hints: []
1532
+ };
1533
+ }
1534
+ const required = eduLevel(jobDescription);
1535
+ if (required === 0) {
1536
+ return {
1537
+ id: "education-level",
1538
+ tier: "match",
1539
+ weight: "medium",
1540
+ status: "skipped",
1541
+ score: 0,
1542
+ message: "JD does not specify education level.",
1543
+ hints: []
1544
+ };
1545
+ }
1546
+ const have = Math.max(0, ...(resume.education || []).map((e) => eduLevel(e.studyType || "")));
1547
+ const passed = have >= required;
1548
+ return {
1549
+ id: "education-level",
1550
+ tier: "match",
1551
+ weight: "medium",
1552
+ status: passed ? "pass" : "fail",
1553
+ score: passed ? 100 : Math.round(have / required * 100),
1554
+ message: `Resume level ${have}, JD required ${required}.`,
1555
+ hints: passed ? [] : ["JD requires a higher degree level than the resume reports."]
1556
+ };
1557
+ };
1558
+ var YOE_RE = /(\d+)\s*(?:\+|[-–—]\s*\d+|to\s*\d+)?\s*years?/i;
1559
+ var yoeMatch = (resume, _l, { jobDescription }) => {
1560
+ if (!jobDescription) {
1561
+ return {
1562
+ id: "yoe-match",
1563
+ tier: "match",
1564
+ weight: "high",
1565
+ status: "skipped",
1566
+ score: 0,
1567
+ message: "No JD.",
1568
+ hints: []
1569
+ };
1570
+ }
1571
+ const m = jobDescription.match(YOE_RE);
1572
+ if (!m) {
1573
+ return {
1574
+ id: "yoe-match",
1575
+ tier: "match",
1576
+ weight: "high",
1577
+ status: "skipped",
1578
+ score: 0,
1579
+ message: "JD does not specify years requirement.",
1580
+ hints: []
1581
+ };
1582
+ }
1583
+ const required = parseInt(m[1] ?? "0", 10);
1584
+ const have = Math.floor(computeYoeYears(resume.work || []));
1585
+ const status = have >= required ? "pass" : have >= required - 1 ? "warn" : "fail";
1586
+ return {
1587
+ id: "yoe-match",
1588
+ tier: "match",
1589
+ weight: "high",
1590
+ status,
1591
+ score: Math.min(100, Math.round(have / required * 100)),
1592
+ message: `${have} YOE detected vs ${required} required.`,
1593
+ hints: status === "pass" ? [] : ["Highlight relevant earlier roles or projects to fill the gap."]
1594
+ };
1595
+ };
1596
+ var hardSkillOverlap = (resume, language, { jobDescription }) => {
1597
+ if (!jobDescription) {
1598
+ return {
1599
+ id: "hard-skill-overlap",
1600
+ tier: "match",
1601
+ weight: "high",
1602
+ status: "skipped",
1603
+ score: 0,
1604
+ message: "No JD.",
1605
+ hints: []
1606
+ };
1607
+ }
1608
+ const km = matchJobDescription(resume, jobDescription, language);
1609
+ const pct = km.matchPercentage;
1610
+ const status = pct >= 70 ? "pass" : pct >= 50 ? "warn" : "fail";
1611
+ return {
1612
+ id: "hard-skill-overlap",
1613
+ tier: "match",
1614
+ weight: "high",
1615
+ status,
1616
+ score: pct,
1617
+ message: `${km.matched.length}/${km.matched.length + km.missing.length} hard skills matched (${pct}%).`,
1618
+ hints: status === "pass" ? [] : km.missing.slice(0, 5).map((s) => `Add evidence of "${s}" to skills/highlights.`)
1619
+ };
1620
+ };
1621
+ var allMatchChecks = [hardSkillOverlap, titleAlignment, educationLevel, yoeMatch];
1622
+ var KNOCKOUTS = [
1623
+ {
1624
+ signal: "work-auth",
1625
+ jdPattern: /(work\s*auth|authorization to work|right to work|us citizen|green card|h-?1b|visa sponsorship)/i,
1626
+ resumeMatch: (r) => /(work auth|authorized|citizen|green card|visa)/i.test(r.basics?.summary || ""),
1627
+ recommendation: "Confirm authorization status in the application form."
1628
+ },
1629
+ {
1630
+ signal: "location",
1631
+ jdPattern: /(must be located|on-?site|relocate|based in)\s+([a-zA-Z ,]+)/i,
1632
+ resumeMatch: (r, m) => {
1633
+ const jdLoc = (m[2] ?? "").toLowerCase().trim();
1634
+ const resumeCity = (r.basics?.location?.city ?? "").toLowerCase().trim();
1635
+ if (!jdLoc || !resumeCity) return false;
1636
+ return jdLoc.includes(resumeCity) || resumeCity.includes(jdLoc.split(/[, ]+/)[0] ?? "");
1637
+ },
1638
+ recommendation: "Verify location requirement against your basics.location.city."
1639
+ },
1640
+ {
1641
+ signal: "clearance",
1642
+ jdPattern: /(ts\/sci|secret clearance|security clearance|active clearance)/i,
1643
+ resumeMatch: (r) => /clearance/i.test(JSON.stringify(r)),
1644
+ recommendation: "Confirm clearance level on the application form."
1645
+ },
1646
+ {
1647
+ signal: "certification",
1648
+ jdPattern: /(cissp|aws certified|pmp|ccna|cpa|required certification)/i,
1649
+ resumeMatch: (r) => (r.certificates?.length ?? 0) > 0,
1650
+ recommendation: "List required certificates in the certificates section if held."
1651
+ }
1652
+ ];
1653
+ function extractKnockouts(resume, jobDescription) {
1654
+ const out = [];
1655
+ for (const k of KNOCKOUTS) {
1656
+ const m = jobDescription.match(k.jdPattern);
1657
+ if (!m) continue;
1658
+ if (k.resumeMatch(resume, m)) continue;
1659
+ out.push({
1660
+ signal: k.signal,
1661
+ evidence: `JD: "${m[0]}"; resume silent.`,
1662
+ recommendation: k.recommendation
1663
+ });
1664
+ }
1665
+ return out;
1666
+ }
1667
+
1668
+ // src/ats/scoring.ts
1669
+ var weightMultiplier = {
1670
+ high: 3,
1671
+ medium: 2,
1672
+ low: 1
1673
+ };
1674
+ function gradeFromScore(score, t) {
1675
+ if (score >= t.A) return "A";
1676
+ if (score >= t.B) return "B";
1677
+ if (score >= t.C) return "C";
1678
+ if (score >= t.D) return "D";
1679
+ return "F";
1680
+ }
1681
+ function computeTierScore(checks) {
1682
+ const active = checks.filter((c) => c.status !== "skipped");
1683
+ if (active.length === 0) return 0;
1684
+ let weighted = 0;
1685
+ let total = 0;
1686
+ for (const c of active) {
1687
+ const m = weightMultiplier[c.weight];
1688
+ weighted += c.score * m;
1689
+ total += 100 * m;
1690
+ }
1691
+ return total > 0 ? Math.round(weighted / total * 100) : 0;
1692
+ }
1693
+ function computeTotalScore(tiers, weights) {
1694
+ if (tiers.match === void 0) {
1695
+ const sum2 = weights.parsing + weights.recruiter;
1696
+ const wp = weights.parsing / sum2;
1697
+ const wr = weights.recruiter / sum2;
1698
+ return Math.round(
1699
+ tiers.parsing * (sum2 === 50 ? 0.4 : wp) + tiers.recruiter * (sum2 === 50 ? 0.6 : wr)
1700
+ );
1701
+ }
1702
+ const sum = weights.parsing + weights.match + weights.recruiter;
1703
+ return Math.round(
1704
+ (tiers.parsing * weights.parsing + tiers.match * weights.match + tiers.recruiter * weights.recruiter) / sum
1705
+ );
1706
+ }
1707
+ function scoreToRating(score, t) {
1708
+ if (score >= t.excellent) return "excellent";
1709
+ if (score >= t.good) return "good";
1710
+ if (score >= t.needsWork) return "needs-work";
1711
+ return "poor";
1712
+ }
1713
+ function generateSummary(score, rating, hasJd, knockouts) {
1714
+ const ratingLabel = {
1715
+ excellent: "Excellent",
1716
+ good: "Good",
1717
+ "needs-work": "Needs Work",
1718
+ poor: "Poor"
1719
+ }[rating];
1720
+ const knockoutNote = knockouts > 0 ? ` ${knockouts} knockout signal${knockouts === 1 ? "" : "s"} flagged.` : "";
1721
+ return `ATS ${score}/100 (${ratingLabel}).${hasJd ? " Includes JD match." : ""}${knockoutNote}`;
1722
+ }
1723
+
1724
+ // src/utils/config.ts
1725
+ import fs from "fs";
1726
+ import path from "path";
1727
+ import yaml from "yaml";
1728
+ import { z } from "zod";
1729
+ import merge from "lodash.merge";
1730
+ var weightEnum = z.enum(["high", "medium", "low"]);
1731
+ var atsConfigSchema = z.object({
1732
+ weights: z.object({
1733
+ tiers: z.object({
1734
+ parsing: z.number().int().min(0).max(100),
1735
+ match: z.number().int().min(0).max(100),
1736
+ recruiter: z.number().int().min(0).max(100)
1737
+ }).partial().optional(),
1738
+ checks: z.record(z.string(), weightEnum).optional()
1739
+ }).partial().optional(),
1740
+ thresholds: z.object({
1741
+ rating: z.object({
1742
+ excellent: z.number(),
1743
+ good: z.number(),
1744
+ needsWork: z.number()
1745
+ }).partial().optional(),
1746
+ grade: z.object({ A: z.number(), B: z.number(), C: z.number(), D: z.number() }).partial().optional(),
1747
+ seniorYoeCutoff: z.number().int().min(0).optional(),
1748
+ wordCount: z.object({ min: z.number(), max: z.number(), seniorMax: z.number() }).partial().optional(),
1749
+ bulletsPerRole: z.object({ min: z.number(), max: z.number(), seniorMax: z.number() }).partial().optional()
1750
+ }).partial().optional(),
1751
+ disable: z.array(z.string()).optional(),
1752
+ locale: z.string().optional()
1753
+ });
1754
+ var fileSchema = z.object({ ats: atsConfigSchema.optional() });
1755
+ var defaultConfig = {
1756
+ weights: {
1757
+ tiers: { parsing: 30, match: 50, recruiter: 20 },
1758
+ checks: {}
1759
+ },
1760
+ thresholds: {
1761
+ rating: { excellent: 90, good: 75, needsWork: 60 },
1762
+ grade: { A: 90, B: 80, C: 70, D: 60 },
1763
+ seniorYoeCutoff: 10,
1764
+ wordCount: { min: 400, max: 800, seniorMax: 1600 },
1765
+ bulletsPerRole: { min: 3, max: 6, seniorMax: 10 }
1766
+ },
1767
+ disable: [],
1768
+ locale: "en"
1769
+ };
1770
+ function loadConfig(opts = {}) {
1771
+ const cwd = opts.cwd ?? process.cwd();
1772
+ const file = opts.configPath ?? path.join(cwd, "resuml.config.yaml");
1773
+ if (!fs.existsSync(file)) return defaultConfig;
1774
+ const raw = fs.readFileSync(file, "utf8");
1775
+ const parsed = yaml.parse(raw) ?? {};
1776
+ const result = fileSchema.safeParse(parsed);
1777
+ if (!result.success) {
1778
+ const issue = result.error.issues[0];
1779
+ const where = issue?.path.join(".") ?? "<root>";
1780
+ throw new Error(`Invalid resuml.config.yaml at "${where}": ${issue?.message}`);
1781
+ }
1782
+ return merge({}, defaultConfig, result.data.ats ?? {});
1783
+ }
1784
+ function effectiveWeight(checkId, defaultWeight, config) {
1785
+ return config.weights.checks[checkId] ?? defaultWeight;
1786
+ }
1787
+
1788
+ // src/ats/index.ts
1789
+ function applyConfig(checks, cfg) {
1790
+ return checks.filter((c) => !cfg.disable.includes(c.id)).map((c) => ({ ...c, weight: effectiveWeight(c.id, c.weight, cfg) }));
1791
+ }
1792
+ function buildTier(_tier, checks, cfg) {
1793
+ const filtered = applyConfig(checks, cfg);
1794
+ const score = computeTierScore(filtered);
1795
+ return {
1796
+ score,
1797
+ grade: gradeFromScore(score, cfg.thresholds.grade),
1798
+ checks: filtered
1799
+ };
1800
+ }
1801
+ function analyzeAts(resume, options = {}) {
1802
+ const cfg = options.config ?? defaultConfig;
1803
+ const language = options.language ?? cfg.locale;
1804
+ const parsingChecks = allParsingChecks.map((fn) => fn(resume, language));
1805
+ const recruiterChecks = allRecruiterChecks.map((fn) => fn(resume, language, cfg));
1806
+ const parsing = buildTier("parsing", parsingChecks, cfg);
1807
+ const recruiter = buildTier("recruiter", recruiterChecks, cfg);
1808
+ let match;
1809
+ let knockouts = [];
1810
+ if (options.jobDescription) {
1811
+ const matchChecks = allMatchChecks.map(
1812
+ (fn) => fn(resume, language, { jobDescription: options.jobDescription })
1813
+ );
1814
+ match = buildTier("match", matchChecks, cfg);
1815
+ knockouts = extractKnockouts(resume, options.jobDescription);
1816
+ }
1817
+ const totalScore = computeTotalScore(
1818
+ { parsing: parsing.score, match: match?.score, recruiter: recruiter.score },
1819
+ cfg.weights.tiers
1820
+ );
1821
+ const rating = scoreToRating(totalScore, cfg.thresholds.rating);
1822
+ const summary = generateSummary(totalScore, rating, !!options.jobDescription, knockouts.length);
1823
+ return {
1824
+ score: totalScore,
1825
+ rating,
1826
+ tiers: match ? { parsing, match, recruiter } : { parsing, recruiter },
1827
+ knockouts,
1828
+ summary
1829
+ };
1830
+ }
1831
+
1832
+ export {
1833
+ loadConfig,
1834
+ analyzeAts
1835
+ };
1836
+ //# sourceMappingURL=chunk-N55EPZ2N.js.map