@whiteslove/parsing-lexicon 0.2.0 → 0.2.2

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.
Files changed (94) hide show
  1. package/LICENSE +21 -21
  2. package/README.md +137 -137
  3. package/index.d.ts +374 -374
  4. package/package.json +77 -75
  5. package/src/central-asia-locations.js +211 -211
  6. package/src/central-asia.js +233 -233
  7. package/src/contact.d.ts +44 -44
  8. package/src/contact.js +159 -159
  9. package/src/countries.js +175 -175
  10. package/src/country-context.d.ts +10 -10
  11. package/src/country-context.js +22 -22
  12. package/src/currency.d.ts +14 -14
  13. package/src/currency.js +66 -66
  14. package/src/geo.js +169 -169
  15. package/src/geography-central-asia.js +42 -42
  16. package/src/geography-detection.d.ts +14 -14
  17. package/src/geography-detection.js +64 -64
  18. package/src/geography-display.d.ts +13 -13
  19. package/src/geography-display.js +113 -113
  20. package/src/geography.js +210 -210
  21. package/src/hiring-advanced.js +147 -147
  22. package/src/hiring-candidate-fields.d.ts +9 -9
  23. package/src/hiring-candidate-fields.js +92 -92
  24. package/src/hiring-context.d.ts +64 -64
  25. package/src/hiring-context.js +412 -412
  26. package/src/hiring-language-extensions.d.ts +19 -19
  27. package/src/hiring-language-extensions.js +139 -139
  28. package/src/hiring-languages.js +135 -135
  29. package/src/hiring-location-fields.d.ts +3 -3
  30. package/src/hiring-location-fields.js +53 -53
  31. package/src/hiring-professions-ro.js +191 -191
  32. package/src/hiring-professions.js +437 -437
  33. package/src/hiring-requirements.d.ts +16 -16
  34. package/src/hiring-requirements.js +170 -170
  35. package/src/hiring-salary-context.d.ts +27 -27
  36. package/src/hiring-salary-context.js +76 -76
  37. package/src/hiring-semantics.d.ts +29 -29
  38. package/src/hiring-semantics.js +218 -218
  39. package/src/hiring-skills.d.ts +21 -21
  40. package/src/hiring-skills.js +315 -315
  41. package/src/hiring-source-aliases.d.ts +32 -32
  42. package/src/hiring-source-aliases.js +185 -185
  43. package/src/hiring-source-semantics.d.ts +31 -31
  44. package/src/hiring-source-semantics.js +293 -293
  45. package/src/hiring-temporal.d.ts +15 -15
  46. package/src/hiring-temporal.js +67 -67
  47. package/src/hiring-vacancy-fields.d.ts +5 -5
  48. package/src/hiring-vacancy-fields.js +23 -23
  49. package/src/hiring-work-semantics.d.ts +34 -34
  50. package/src/hiring-work-semantics.js +72 -72
  51. package/src/hiring.js +180 -180
  52. package/src/housing-address.d.ts +18 -18
  53. package/src/housing-address.js +217 -210
  54. package/src/housing-commercial.d.ts +1 -0
  55. package/src/housing-commercial.js +9 -0
  56. package/src/housing-context.d.ts +36 -36
  57. package/src/housing-context.js +183 -183
  58. package/src/housing-features.js +30 -30
  59. package/src/housing-intent.d.ts +9 -9
  60. package/src/housing-intent.js +132 -132
  61. package/src/housing-listing-fields.js +131 -131
  62. package/src/housing-money.d.ts +7 -7
  63. package/src/housing-money.js +102 -102
  64. package/src/housing-source-aliases.d.ts +8 -8
  65. package/src/housing-source-aliases.js +51 -51
  66. package/src/housing-structured.d.ts +55 -55
  67. package/src/housing-structured.js +219 -219
  68. package/src/housing.js +200 -200
  69. package/src/index.js +45 -44
  70. package/src/kz-location-extensions.js +307 -307
  71. package/src/landmarks.js +21 -21
  72. package/src/lexicon-core.js +58 -58
  73. package/src/location-merge.js +106 -106
  74. package/src/locations.js +456 -456
  75. package/src/money-core.d.ts +10 -10
  76. package/src/money-core.js +81 -81
  77. package/src/money-lexicon.d.ts +5 -5
  78. package/src/money-lexicon.js +79 -79
  79. package/src/money.js +154 -154
  80. package/src/normalization.js +384 -384
  81. package/src/odesa-metropolitan.js +144 -144
  82. package/src/romania-geography.js +47 -47
  83. package/src/tashkent-colloquial.js +80 -72
  84. package/src/tashkent-housing-geography.d.ts +38 -38
  85. package/src/tashkent-housing-geography.js +211 -211
  86. package/src/tashkent-pois.js +215 -215
  87. package/src/tashkent-residential-complexes.js +272 -272
  88. package/src/ua-location-extensions-major.js +118 -118
  89. package/src/ua-location-extensions-metro.js +9 -9
  90. package/src/ua-location-extensions-regional.js +246 -246
  91. package/src/ua-secondary-cities.d.ts +2 -2
  92. package/src/ua-secondary-cities.js +57 -57
  93. package/src/ukraine.js +75 -75
  94. package/src/uz-location-extensions.js +259 -259
@@ -1,29 +1,29 @@
1
- import type { DegreeLevel } from './hiring-requirements.js';
2
-
3
- export type CandidateFeatureCode = 'student' | 'parentalLeave' | 'noExperience' | 'partTime' | 'nightShift' | 'openToRelocation';
4
- export type HiringScopeCode = 'architecture' | 'leadership' | 'mentoring' | 'scale' | 'ownership';
5
-
6
- export function isCandidateNonTargetContext(value: unknown): boolean;
7
- export function extractCandidateTargetContext(value: unknown): string;
8
- export function extractCandidateSkillField(value: unknown): string | null;
9
- export function extractCandidateRoleField(value: unknown): string | null;
10
- export function extractCandidateGoalField(value: unknown): string | null;
11
- export function extractCandidateLocationField(value: unknown): string | null;
12
- export function isCandidateStatusOnly(value: unknown): boolean;
13
- export function isFlexibleCandidateRole(value: unknown): boolean;
14
- export function isCandidateNonRoleValue(value: unknown): boolean;
15
- export function extractCandidateGoalRole(value: unknown): string | null;
16
- export function extractCandidateWorkHistory(value: unknown): string;
17
- export function detectCandidateFeatureCodes(value: unknown): readonly CandidateFeatureCode[];
18
- export function extractCandidateContactHours(value: unknown): string | null;
19
- export function extractCandidateSalaryField(value: unknown): string | null;
20
- export function detectCandidateRelocationPreference(value: unknown): boolean | null;
21
- export function detectCandidateRemotePreference(value: unknown): boolean | null;
22
- export function detectCandidateProfessionLabels(targetValue: unknown, skillValue?: unknown): readonly string[];
23
- export function detectManagementRole(title: unknown, value?: unknown): true | null;
24
- export function detectHiringScopeSignals(value: unknown, options?: { mode?: 'vacancy' | 'candidate' }): readonly HiringScopeCode[];
25
- export function detectDegreeRequirement(value: unknown): Readonly<{
26
- level?: DegreeLevel;
27
- field?: string;
28
- equivalentExperience: boolean;
29
- }>;
1
+ import type { DegreeLevel } from './hiring-requirements.js';
2
+
3
+ export type CandidateFeatureCode = 'student' | 'parentalLeave' | 'noExperience' | 'partTime' | 'nightShift' | 'openToRelocation';
4
+ export type HiringScopeCode = 'architecture' | 'leadership' | 'mentoring' | 'scale' | 'ownership';
5
+
6
+ export function isCandidateNonTargetContext(value: unknown): boolean;
7
+ export function extractCandidateTargetContext(value: unknown): string;
8
+ export function extractCandidateSkillField(value: unknown): string | null;
9
+ export function extractCandidateRoleField(value: unknown): string | null;
10
+ export function extractCandidateGoalField(value: unknown): string | null;
11
+ export function extractCandidateLocationField(value: unknown): string | null;
12
+ export function isCandidateStatusOnly(value: unknown): boolean;
13
+ export function isFlexibleCandidateRole(value: unknown): boolean;
14
+ export function isCandidateNonRoleValue(value: unknown): boolean;
15
+ export function extractCandidateGoalRole(value: unknown): string | null;
16
+ export function extractCandidateWorkHistory(value: unknown): string;
17
+ export function detectCandidateFeatureCodes(value: unknown): readonly CandidateFeatureCode[];
18
+ export function extractCandidateContactHours(value: unknown): string | null;
19
+ export function extractCandidateSalaryField(value: unknown): string | null;
20
+ export function detectCandidateRelocationPreference(value: unknown): boolean | null;
21
+ export function detectCandidateRemotePreference(value: unknown): boolean | null;
22
+ export function detectCandidateProfessionLabels(targetValue: unknown, skillValue?: unknown): readonly string[];
23
+ export function detectManagementRole(title: unknown, value?: unknown): true | null;
24
+ export function detectHiringScopeSignals(value: unknown, options?: { mode?: 'vacancy' | 'candidate' }): readonly HiringScopeCode[];
25
+ export function detectDegreeRequirement(value: unknown): Readonly<{
26
+ level?: DegreeLevel;
27
+ field?: string;
28
+ equivalentExperience: boolean;
29
+ }>;
@@ -1,218 +1,218 @@
1
- import { detectDegreeFields, detectDegreeLevel } from './hiring-requirements.js';
2
- import { matchExtendedProfessions } from './hiring-source-aliases.js';
3
-
4
- const NON_TARGET_CONTEXT_RE = /(?:опыт\s+работы|досвід\s+роботи|work\s+experience|previous|раньше|ранее|прежде|работал|работала|працював|працювала|worked\s+(?:as|at)|oldin|avval|ishlagan|ishladim|tajriba|диплом|diplom|mutaxassisligim)/iu;
5
- const TARGET_CONTEXT_RE = /(?:ищу\s+(?:работу|подработку)|шукаю\s+(?:роботу|підробіток)|желаемая\s+(?:должность|работа)|бажана\s+(?:посада|робота)|target\s+role|desired\s+(?:role|position)|looking\s+for\s+(?:a\s+)?(?:job|work)|open\s+to\s+work|menga\s+ish\s+kerak|ish\s+(?:kerak|qidiryapman|qidiraman|izlayapman)|ish\s+joyi\s+kerak|lavozim|kasb|soha|soxa|maqsad(?:im)?)/iu;
6
-
7
- const SKILLS_FIELD_RE = /(?:^|\n)[^\p{L}\p{N}\n]{0,10}(?:skills|навыки|навички|стек|stack|technologies|texnologiya(?:lar)?|ko(?:'|’)nikmalar)\s*[:—-]\s*([^\n]{2,500})/iu;
8
- const WORK_HISTORY_BLOCK_RE = /(?:^|\n)\s*(?:опыт\s+работы|досвід\s+роботи|work\s+experience|previous\s+(?:jobs?|positions?)|tajriba|ish\s+tajribasi)\s*[:—-]?\s*([\s\S]{1,2600}?)(?=\n\s*(?:навыки|навички|skills|образование|освіта|education|контакт|contact|ожидания|salary|языки|мови|languages)\s*[:—-]|$)/iu;
9
- const WORK_HISTORY_LINE_RE = /(?:работал[аи]?|працюва(?:в|ла)|worked\s+(?:as|at)|oldin|avval|ishlagan|ishladim|ishlaganman|tajriba(?:m)?\s+bor)/iu;
10
- const GOAL_ROLE_RE = /\b((?:(?:frontend|front-end|backend|back-end|full[- ]?stack|mobile|android|ios)\s+)?(?:dasturchi|developer|programmer))\s+sifatida\s+(?:ish\s+(?:topish|qidirish|izlash)|ishlash)\b/iu;
11
- const CONTACT_HOURS_RE = /(?:^|\n)[^\p{L}\p{N}\n]{0,10}(?:murojaat\s+qilish\s+vaqti|aloqa\s+vaqti|qo(?:'|’)ng(?:'|’)iroq\s+vaqti|bog(?:'|’)lanish\s+vaqti|время\s+(?:связи|звонков|для\s+связи)|звонить\s+(?:с|в)|contact\s+(?:hours|time)|call\s+time)\s*[:—-]?\s*([^\n]{3,60})/iu;
12
- const SALARY_FIELD_RE = /(?:^|\n)[^\p{L}\p{N}\n]{0,10}(?:narxi?|salary|expected\s+salary|зарплата|зп|бажана\s+зарплата|oylik|maosh|ish\s+haqi)\s*[:—-]\s*([^\n]{1,120})/iu;
13
- const ROLE_FIELD_RE = /(?:^|\n)[^\p{L}\p{N}\n]{0,8}(?:желаемая\s+(?:работа|должность)|бажана\s+(?:робота|посада)|target\s+role|desired\s+(?:role|position)|position|role|должность|посада|lavozim|kasbi|kasb|qidirayotgan\s+kasb|so(?:'|’)ralgan\s+ish\s+turi)\s*[::—-]\s*([^\n]{1,220})/iu;
14
- const GOAL_FIELD_RE = /(?:^|\n)[^\p{L}\p{N}\n]{0,8}(?:maqsad|мақсад|goal|цель|мета)\s*[::—-]\s*([^\n]{1,220})/iu;
15
- const LOCATION_FIELD_RE = /(?:^|\n)[^\p{L}\p{N}\n]{0,8}(?:location|локация|локація|country|страна|країна)\s*[::—-]\s*([^\n]{1,220})/iu;
16
- const LOCATION_COMPACT_RE = /(?:^|\n)[^\p{L}\p{N}\n]{0,8}(?:location|локация|локація|country|страна|країна)\s+(#?[\p{L}][^\n]{0,120})/iu;
17
-
18
- const STATUS_ONLY_RE = /^(?:talaba|student|студент(?:ка)?|студент(?:ка)?ка|o(?:'|’)quvchi|учащ(?:ийся|аяся))$/iu;
19
- const FLEXIBLE_ROLE_RE = /^(?:нет|без)\s+разницы(?:\s+.*)?$|^не\s*важно(?:\s+.*)?$|^farqi\s+yo['’ʻʼ‘`]?q$|^любая\s+(?:работа|занятость)(?:\s+.*)?$/iu;
20
- const NON_ROLE_RE = /^(?:удал[её]нно|работа\s+на\s+удал[её]н\p{L}*\s+основе|remote|onlayn|online|онлайн|farqi\s+yo['’ʻʼ‘`]?q|bilmaym\p{L}*|ish\s+ker(?:e|a)\s+onlayn|любая\s+(?:работа|занятость)|немає|нет|не\s+указано|not\s+specified)$/iu;
21
- const REMOTE_NEGATIVE_RE = /(?:onsite|on-site|office\s+only|тільки\s+офіс|только\s+офис|офисн(?:ый|ая)\s+формат|офлайн|без\s+удал[её]нк|удал[её]нк\p{L}*\s+не\s+рассматрива|не\s+рассматрива\p{L}*\s+удал[её]н|remote\s+(?:not|no)|faqat\s+ofis|ofisda\s+ish(?:lash)?)/iu;
22
- const REMOTE_POSITIVE_RE = /(?:\bremote\b|\bremotely\b|удал[её]н(?:но|ка|ный|ная|н\p{L}*)?|віддален(?:о|а|ий)?|дистанц(?:ионно|ійно)|masofaviy|(?<!\p{L})onlayn(?!\p{L})|online\s+(?:work|job)|онлайн\s+работ)/iu;
23
-
24
- const FEATURE_RULES = Object.freeze([
25
- ['student', /\bstudent\b|студент|студентк|talaba/iu],
26
- ['parentalLeave', /декрет|в\s+декрете|у\s+декреті|maternity\s+leave|parental\s+leave/iu],
27
- ['noExperience', /без\s+опыта|без\s+досвіду|no\s+experience|tajriba\s+yo(?:'|’)q/iu],
28
- ['partTime', /подработк|підробіт|part[-\s]?time|неполный\s+день|неповн(?:ий|а)\s+день|yarim\s+stavka/iu],
29
- ['nightShift', /ночн(?:ая|ую|ой)\s+смен|нічн(?:а|у|ої)\s+змін|night\s+shift|tungi\s+smena/iu],
30
- ['openToRelocation', /готов\p{L}*\s+к\s+переезду|готов\p{L}*\s+переехать|готов\p{L}*\s+до\s+переїзду|relocat(?:e|ion)|ko(?:'|’)chib\s+o(?:'|’)tish/iu],
31
- ]);
32
-
33
- const RELOCATION_NEGATIVE_RE = /не\s+готов\p{L}*\s+к\s+переезду|не\s+розгляда\p{L}*\s+переїзд|not\s+(?:open|ready)\s+to\s+relocat/iu;
34
- const RELOCATION_POSITIVE_RE = /готов\p{L}*\s+к\s+переезду|готов\p{L}*\s+переехать|готов\p{L}*\s+до\s+переїзду|relocat(?:e|ion)|ko(?:'|’)chib\s+o(?:'|’)tish/iu;
35
-
36
- const SPECIAL_CANDIDATE_PROFESSIONS = Object.freeze([
37
- ['HR / Recruiter', /#(?:hr|hrd|hrbp|hrgeneralist|peopleops)\b|\b(?:hr\s+lead|head\s+of\s+hr|people\s+partner|talent\s+operations|кадров\p{L}*\s+аудит)\b/iu],
38
- ['Sales Manager', /(?:sotuv|savdo)\s+(?:menejer|menejr|menedjer|manager)|\bsales\s+manager\b|менеджер\s+(?:по\s+)?продаж|менеджер\s+з\s+продаж/iu],
39
- ['Backend Developer', /\bbackend\s+(?:developer|engineer)\b|\bback[- ]?end\s+(?:developer|engineer)\b|backend\s+dasturchi|серверн(?:ый|ий)\s+разработчик/iu],
40
- ['Frontend Developer', /\bfrontend\s+(?:developer|engineer)\b|\bfront[- ]?end\s+(?:developer|engineer)\b|frontend\s+dasturchi/iu],
41
- ['AI / ML Engineer', /\b(?:ai|artificial\s+intelligence|machine\s+learning|ml)\s*(?:\/\s*(?:ai|ml))?\s*(?:engineer|developer)\b|\b(?:engineer|developer)\s+(?:ai|ml|machine\s+learning)\b/iu],
42
- ['Penetration Tester', /\b(?:penetration\s+tester|penetration\s+testing|pentest(?:er|ing)?)\b|пентест(?:ер|инг)?/iu],
43
- ['Network Administrator', /\b(?:network\s+administrator|network\s+admin)\b|(?:tarmoq|тармоқ)[^\n]{0,70}(?:administrator|admin(?:strator)?i?|администратор)/iu],
44
- ['System Administrator', /\b(?:system\s+administrator|system\s+admin|sysadmin)\b|(?:tizim|тизим)[^\n]{0,50}(?:administrator|admin(?:strator)?i?|администратор)|системн(?:ый|ий)\s+администратор/iu],
45
- ['Cybersecurity Specialist', /\b(?:cybersecurity|cyber\s+security|information\s+security)\b|кибербезопасност|кібербезпек|информационн(?:ая|ой)\s+безопасност|інформаційн(?:а|ої)\s+безпек|axborot\s+xavfsizligi/iu],
46
- ['Data Scientist', /\bdata\s+scientist\b|\bdata\s+science\b|дата\s+саентист/iu],
47
- ['Data Engineer', /\bdata\s+engineer\b|инженер\s+данных|інженер\s+даних/iu],
48
- ]);
49
-
50
- const SCOPE_SIGNALS = Object.freeze([
51
- Object.freeze({
52
- code: 'architecture',
53
- vacancy: /\barchitect(?:ure|ing)?\b|\bsystem design\b|\btechnical roadmap\b|\blong[- ]term solutions?\b/i,
54
- candidate: /\barchitect(?:ed|ure|ing)?\b|\bsystem design\b|\btechnical roadmap\b|\bsolution architecture\b/i,
55
- }),
56
- Object.freeze({
57
- code: 'leadership',
58
- vacancy: /\btechnical leader(?:ship)?\b|\bengineering standards\b|\bdrive product vision\b|\blead technical\b/i,
59
- candidate: /\btechnical leader(?:ship)?\b|\bengineering standards\b|\bled\s+(?:a\s+)?(?:team|project|initiative)\b|\btech(?:nical)? lead\b/i,
60
- }),
61
- Object.freeze({
62
- code: 'mentoring',
63
- vacancy: /\bmentor(?:ing|ed)?\s+(?:other\s+)?(?:engineers?|developers?|team members?)\b|\bcoach(?:ing|ed)?\s+(?:engineers?|developers?)\b/i,
64
- candidate: /\bmentor(?:ing|ed)?\s+(?:other\s+)?(?:engineers?|developers?|team members?)\b|\bcoach(?:ing|ed)?\s+(?:engineers?|developers?)\b/i,
65
- }),
66
- Object.freeze({
67
- code: 'scale',
68
- vacancy: /\b(?:millions?|billions?)\s+of\s+(?:users|people|requests|events)\b|\bat scale\b|\bhigh[- ]scale\b|\blarge[- ]scale\b/i,
69
- candidate: /\b(?:millions?|billions?)\s+of\s+(?:users|requests|events)\b|\bat scale\b|\bhigh[- ]scale\b|\blarge[- ]scale\b|\bhigh[- ]traffic\b/i,
70
- }),
71
- Object.freeze({
72
- code: 'ownership',
73
- vacancy: /\bown\s+(?:critical\s+)?(?:features?|systems?|services?|roadmap)\b|\bproduct owner\b|\bdrive product\b/i,
74
- candidate: /\bown(?:ed|ership)?\s+(?:features?|systems?|services?|roadmap|product)\b|\bproduct owner\b|\bdrove\s+(?:a\s+)?(?:feature|product|initiative)\b/i,
75
- }),
76
- ]);
77
-
78
- const fieldValue = (value, re) => String(value || '').match(re)?.[1]?.trim() || null;
79
-
80
- export function isCandidateNonTargetContext(value) {
81
- return NON_TARGET_CONTEXT_RE.test(String(value || ''));
82
- }
83
-
84
- export function extractCandidateTargetContext(value) {
85
- const lines = String(value || '').split('\n').map((line) => line.trim()).filter(Boolean);
86
- const picked = [];
87
- for (let i = 0; i < lines.length; i += 1) {
88
- if (!TARGET_CONTEXT_RE.test(lines[i])) continue;
89
- for (let offset = 0; offset < 3 && i + offset < lines.length; offset += 1) {
90
- const line = lines[i + offset];
91
- if (offset > 0 && NON_TARGET_CONTEXT_RE.test(line)) break;
92
- picked.push(line);
93
- }
94
- }
95
- return picked.join('\n');
96
- }
97
-
98
- export function extractCandidateSkillField(value) {
99
- return fieldValue(value, SKILLS_FIELD_RE);
100
- }
101
-
102
- export function extractCandidateRoleField(value) {
103
- return fieldValue(value, ROLE_FIELD_RE);
104
- }
105
-
106
- export function extractCandidateGoalField(value) {
107
- return fieldValue(value, GOAL_FIELD_RE);
108
- }
109
-
110
- export function extractCandidateLocationField(value) {
111
- return fieldValue(value, LOCATION_FIELD_RE) || fieldValue(value, LOCATION_COMPACT_RE);
112
- }
113
-
114
- export function isCandidateStatusOnly(value) {
115
- return STATUS_ONLY_RE.test(String(value || '').trim());
116
- }
117
-
118
- export function isFlexibleCandidateRole(value) {
119
- return FLEXIBLE_ROLE_RE.test(String(value || '').trim());
120
- }
121
-
122
- export function isCandidateNonRoleValue(value) {
123
- return NON_ROLE_RE.test(String(value || '').trim());
124
- }
125
-
126
- export function extractCandidateGoalRole(value) {
127
- return String(value || '').match(GOAL_ROLE_RE)?.[1]?.trim() || null;
128
- }
129
-
130
- export function extractCandidateWorkHistory(value) {
131
- const text = String(value || '');
132
- const explicit = text.match(WORK_HISTORY_BLOCK_RE)?.[1];
133
- if (explicit) return explicit;
134
- return text.split(/\n|(?<=[.!?])\s+/u).filter((line) => WORK_HISTORY_LINE_RE.test(line)).join('\n');
135
- }
136
-
137
- export function detectCandidateFeatureCodes(value) {
138
- const text = String(value || '');
139
- return Object.freeze(FEATURE_RULES.filter(([, re]) => re.test(text)).map(([code]) => code));
140
- }
141
-
142
- export function extractCandidateContactHours(value) {
143
- const raw = String(value || '').match(CONTACT_HOURS_RE)?.[1];
144
- if (!raw) return null;
145
- const cleaned = raw.replace(/\s{2,}/g, ' ').replace(/[.;,]+$/, '').trim();
146
- return /\b24\s*\/\s*7\b|\d{1,2}[:.]\d{2}|\d{1,2}\s*[-–—]\s*\d{1,2}/.test(cleaned)
147
- ? cleaned.slice(0, 60)
148
- : null;
149
- }
150
-
151
- export function extractCandidateSalaryField(value) {
152
- return fieldValue(value, SALARY_FIELD_RE);
153
- }
154
-
155
- export function detectCandidateRelocationPreference(value) {
156
- const text = String(value || '');
157
- if (RELOCATION_NEGATIVE_RE.test(text)) return false;
158
- if (RELOCATION_POSITIVE_RE.test(text)) return true;
159
- return null;
160
- }
161
-
162
- export function detectCandidateRemotePreference(value) {
163
- const text = String(value || '');
164
- if (REMOTE_NEGATIVE_RE.test(text)) return false;
165
- if (REMOTE_POSITIVE_RE.test(text)) return true;
166
- return null;
167
- }
168
-
169
- export function detectCandidateProfessionLabels(targetValue, skillValue = '') {
170
- const target = String(targetValue || '');
171
- const special = SPECIAL_CANDIDATE_PROFESSIONS
172
- .filter(([, re]) => re.test(target))
173
- .map(([label]) => label);
174
- if (special.length) return Object.freeze([...new Set(special)]);
175
-
176
- const matched = matchExtendedProfessions(target, { limit: 8, allowWeak: true }).map((item) => item.label);
177
- if (matched.length) return Object.freeze([...new Set(matched)]);
178
-
179
- const skills = String(skillValue || '');
180
- if (!skills) return Object.freeze([]);
181
- if (/\bflutter\b|\bdart\b/iu.test(skills)) return Object.freeze(['Mobile Developer']);
182
-
183
- const sysadminBase = /\bcisco\b/iu.test(skills)
184
- && /\b(?:linux|windows\s+server|active\s+directory|network(?:ing)?)\b/iu.test(skills);
185
- const specializedTools = /\b(?:devops|sre|docker|kubernetes|k8s|terraform|ansible|jenkins|gitlab\s+ci|github\s+actions|ci\/?cd|aws|azure|gcp|ai|ml|mlops|artificial\s+intelligence|machine\s+learning|deep\s+learning|data\s+scien(?:ce|tist)|tensorflow|pytorch|scikit[- ]?learn|llm|langchain|developer|software\s+engineer|programmer|разработчик|программист|react|angular|vue(?:\.js)?|next(?:\.js)?|node(?:\.js)?|django|laravel|spring\s+boot|asp\.net)\b/iu.test(skills);
186
- if (sysadminBase && !specializedTools) return Object.freeze(['System Administrator']);
187
-
188
- const strongDeveloperSkill = /\b(?:react|vue\.?\s*js|angular|node\.?\s*js|next\.?\s*js|django|laravel|fastapi|flask|spring|asp\.?net|ruby\s+on\s+rails)\b/iu.test(skills);
189
- const softwareSignals = (skills.match(/\b(?:python|java(?:script)?|typescript|php|react|vue\.?\s*js|angular|node\.?\s*js|next\.?\s*js|django|laravel|fastapi|flask|spring|asp\.?net|sql|html|css|c\+\+|c#|golang)\b/giu) || []).length;
190
- return strongDeveloperSkill || softwareSignals >= 2
191
- ? Object.freeze(['Software Developer'])
192
- : Object.freeze([]);
193
- }
194
-
195
- export function detectManagementRole(title, value = '') {
196
- const heading = String(title || '');
197
- const text = String(value || '');
198
- if (/\b(?:head|director|chief|manager|supervisor|team\s*lead|tech\s*lead)\b|руководител|начальник|директор|тимлид|заведующ/i.test(heading)) return true;
199
- if (/manage(?:s|ment|ing)?\s+(?:a\s+)?(?:team|people|staff)|people management|direct reports|руковод(?:ить|ство)\s+(?:команд|сотруд)|управлени[ея]\s+(?:команд|персонал)/i.test(text)) return true;
200
- return null;
201
- }
202
-
203
- export function detectHiringScopeSignals(value, options = {}) {
204
- const mode = options.mode === 'candidate' ? 'candidate' : 'vacancy';
205
- const text = String(value || '');
206
- return Object.freeze(SCOPE_SIGNALS.filter((signal) => signal[mode].test(text)).map((signal) => signal.code));
207
- }
208
-
209
- export function detectDegreeRequirement(value) {
210
- const text = String(value || '');
211
- const level = detectDegreeLevel(text) || undefined;
212
- const fields = detectDegreeFields(text);
213
- let field;
214
- if (fields.includes('computer_science')) field = 'computer_science';
215
- else if (fields.includes('engineering') && /\bengineering\b[^.;\n]{0,60}\bdegree\b|\bdegree\b[^.;\n]{0,60}\bengineering\b/i.test(text)) field = 'engineering';
216
- const equivalentExperience = /equivalent\s+(?:professional\s+|work\s+)?experience|or\s+equivalent\s+experience/i.test(text);
217
- return Object.freeze({ level, field, equivalentExperience });
218
- }
1
+ import { detectDegreeFields, detectDegreeLevel } from './hiring-requirements.js';
2
+ import { matchExtendedProfessions } from './hiring-source-aliases.js';
3
+
4
+ const NON_TARGET_CONTEXT_RE = /(?:опыт\s+работы|досвід\s+роботи|work\s+experience|previous|раньше|ранее|прежде|работал|работала|працював|працювала|worked\s+(?:as|at)|oldin|avval|ishlagan|ishladim|tajriba|диплом|diplom|mutaxassisligim)/iu;
5
+ const TARGET_CONTEXT_RE = /(?:ищу\s+(?:работу|подработку)|шукаю\s+(?:роботу|підробіток)|желаемая\s+(?:должность|работа)|бажана\s+(?:посада|робота)|target\s+role|desired\s+(?:role|position)|looking\s+for\s+(?:a\s+)?(?:job|work)|open\s+to\s+work|menga\s+ish\s+kerak|ish\s+(?:kerak|qidiryapman|qidiraman|izlayapman)|ish\s+joyi\s+kerak|lavozim|kasb|soha|soxa|maqsad(?:im)?)/iu;
6
+
7
+ const SKILLS_FIELD_RE = /(?:^|\n)[^\p{L}\p{N}\n]{0,10}(?:skills|навыки|навички|стек|stack|technologies|texnologiya(?:lar)?|ko(?:'|’)nikmalar)\s*[:—-]\s*([^\n]{2,500})/iu;
8
+ const WORK_HISTORY_BLOCK_RE = /(?:^|\n)\s*(?:опыт\s+работы|досвід\s+роботи|work\s+experience|previous\s+(?:jobs?|positions?)|tajriba|ish\s+tajribasi)\s*[:—-]?\s*([\s\S]{1,2600}?)(?=\n\s*(?:навыки|навички|skills|образование|освіта|education|контакт|contact|ожидания|salary|языки|мови|languages)\s*[:—-]|$)/iu;
9
+ const WORK_HISTORY_LINE_RE = /(?:работал[аи]?|працюва(?:в|ла)|worked\s+(?:as|at)|oldin|avval|ishlagan|ishladim|ishlaganman|tajriba(?:m)?\s+bor)/iu;
10
+ const GOAL_ROLE_RE = /\b((?:(?:frontend|front-end|backend|back-end|full[- ]?stack|mobile|android|ios)\s+)?(?:dasturchi|developer|programmer))\s+sifatida\s+(?:ish\s+(?:topish|qidirish|izlash)|ishlash)\b/iu;
11
+ const CONTACT_HOURS_RE = /(?:^|\n)[^\p{L}\p{N}\n]{0,10}(?:murojaat\s+qilish\s+vaqti|aloqa\s+vaqti|qo(?:'|’)ng(?:'|’)iroq\s+vaqti|bog(?:'|’)lanish\s+vaqti|время\s+(?:связи|звонков|для\s+связи)|звонить\s+(?:с|в)|contact\s+(?:hours|time)|call\s+time)\s*[:—-]?\s*([^\n]{3,60})/iu;
12
+ const SALARY_FIELD_RE = /(?:^|\n)[^\p{L}\p{N}\n]{0,10}(?:narxi?|salary|expected\s+salary|зарплата|зп|бажана\s+зарплата|oylik|maosh|ish\s+haqi)\s*[:—-]\s*([^\n]{1,120})/iu;
13
+ const ROLE_FIELD_RE = /(?:^|\n)[^\p{L}\p{N}\n]{0,8}(?:желаемая\s+(?:работа|должность)|бажана\s+(?:робота|посада)|target\s+role|desired\s+(?:role|position)|position|role|должность|посада|lavozim|kasbi|kasb|qidirayotgan\s+kasb|so(?:'|’)ralgan\s+ish\s+turi)\s*[::—-]\s*([^\n]{1,220})/iu;
14
+ const GOAL_FIELD_RE = /(?:^|\n)[^\p{L}\p{N}\n]{0,8}(?:maqsad|мақсад|goal|цель|мета)\s*[::—-]\s*([^\n]{1,220})/iu;
15
+ const LOCATION_FIELD_RE = /(?:^|\n)[^\p{L}\p{N}\n]{0,8}(?:location|локация|локація|country|страна|країна)\s*[::—-]\s*([^\n]{1,220})/iu;
16
+ const LOCATION_COMPACT_RE = /(?:^|\n)[^\p{L}\p{N}\n]{0,8}(?:location|локация|локація|country|страна|країна)\s+(#?[\p{L}][^\n]{0,120})/iu;
17
+
18
+ const STATUS_ONLY_RE = /^(?:talaba|student|студент(?:ка)?|студент(?:ка)?ка|o(?:'|’)quvchi|учащ(?:ийся|аяся))$/iu;
19
+ const FLEXIBLE_ROLE_RE = /^(?:нет|без)\s+разницы(?:\s+.*)?$|^не\s*важно(?:\s+.*)?$|^farqi\s+yo['’ʻʼ‘`]?q$|^любая\s+(?:работа|занятость)(?:\s+.*)?$/iu;
20
+ const NON_ROLE_RE = /^(?:удал[её]нно|работа\s+на\s+удал[её]н\p{L}*\s+основе|remote|onlayn|online|онлайн|farqi\s+yo['’ʻʼ‘`]?q|bilmaym\p{L}*|ish\s+ker(?:e|a)\s+onlayn|любая\s+(?:работа|занятость)|немає|нет|не\s+указано|not\s+specified)$/iu;
21
+ const REMOTE_NEGATIVE_RE = /(?:onsite|on-site|office\s+only|тільки\s+офіс|только\s+офис|офисн(?:ый|ая)\s+формат|офлайн|без\s+удал[её]нк|удал[её]нк\p{L}*\s+не\s+рассматрива|не\s+рассматрива\p{L}*\s+удал[её]н|remote\s+(?:not|no)|faqat\s+ofis|ofisda\s+ish(?:lash)?)/iu;
22
+ const REMOTE_POSITIVE_RE = /(?:\bremote\b|\bremotely\b|удал[её]н(?:но|ка|ный|ная|н\p{L}*)?|віддален(?:о|а|ий)?|дистанц(?:ионно|ійно)|masofaviy|(?<!\p{L})onlayn(?!\p{L})|online\s+(?:work|job)|онлайн\s+работ)/iu;
23
+
24
+ const FEATURE_RULES = Object.freeze([
25
+ ['student', /\bstudent\b|студент|студентк|talaba/iu],
26
+ ['parentalLeave', /декрет|в\s+декрете|у\s+декреті|maternity\s+leave|parental\s+leave/iu],
27
+ ['noExperience', /без\s+опыта|без\s+досвіду|no\s+experience|tajriba\s+yo(?:'|’)q/iu],
28
+ ['partTime', /подработк|підробіт|part[-\s]?time|неполный\s+день|неповн(?:ий|а)\s+день|yarim\s+stavka/iu],
29
+ ['nightShift', /ночн(?:ая|ую|ой)\s+смен|нічн(?:а|у|ої)\s+змін|night\s+shift|tungi\s+smena/iu],
30
+ ['openToRelocation', /готов\p{L}*\s+к\s+переезду|готов\p{L}*\s+переехать|готов\p{L}*\s+до\s+переїзду|relocat(?:e|ion)|ko(?:'|’)chib\s+o(?:'|’)tish/iu],
31
+ ]);
32
+
33
+ const RELOCATION_NEGATIVE_RE = /не\s+готов\p{L}*\s+к\s+переезду|не\s+розгляда\p{L}*\s+переїзд|not\s+(?:open|ready)\s+to\s+relocat/iu;
34
+ const RELOCATION_POSITIVE_RE = /готов\p{L}*\s+к\s+переезду|готов\p{L}*\s+переехать|готов\p{L}*\s+до\s+переїзду|relocat(?:e|ion)|ko(?:'|’)chib\s+o(?:'|’)tish/iu;
35
+
36
+ const SPECIAL_CANDIDATE_PROFESSIONS = Object.freeze([
37
+ ['HR / Recruiter', /#(?:hr|hrd|hrbp|hrgeneralist|peopleops)\b|\b(?:hr\s+lead|head\s+of\s+hr|people\s+partner|talent\s+operations|кадров\p{L}*\s+аудит)\b/iu],
38
+ ['Sales Manager', /(?:sotuv|savdo)\s+(?:menejer|menejr|menedjer|manager)|\bsales\s+manager\b|менеджер\s+(?:по\s+)?продаж|менеджер\s+з\s+продаж/iu],
39
+ ['Backend Developer', /\bbackend\s+(?:developer|engineer)\b|\bback[- ]?end\s+(?:developer|engineer)\b|backend\s+dasturchi|серверн(?:ый|ий)\s+разработчик/iu],
40
+ ['Frontend Developer', /\bfrontend\s+(?:developer|engineer)\b|\bfront[- ]?end\s+(?:developer|engineer)\b|frontend\s+dasturchi/iu],
41
+ ['AI / ML Engineer', /\b(?:ai|artificial\s+intelligence|machine\s+learning|ml)\s*(?:\/\s*(?:ai|ml))?\s*(?:engineer|developer)\b|\b(?:engineer|developer)\s+(?:ai|ml|machine\s+learning)\b/iu],
42
+ ['Penetration Tester', /\b(?:penetration\s+tester|penetration\s+testing|pentest(?:er|ing)?)\b|пентест(?:ер|инг)?/iu],
43
+ ['Network Administrator', /\b(?:network\s+administrator|network\s+admin)\b|(?:tarmoq|тармоқ)[^\n]{0,70}(?:administrator|admin(?:strator)?i?|администратор)/iu],
44
+ ['System Administrator', /\b(?:system\s+administrator|system\s+admin|sysadmin)\b|(?:tizim|тизим)[^\n]{0,50}(?:administrator|admin(?:strator)?i?|администратор)|системн(?:ый|ий)\s+администратор/iu],
45
+ ['Cybersecurity Specialist', /\b(?:cybersecurity|cyber\s+security|information\s+security)\b|кибербезопасност|кібербезпек|информационн(?:ая|ой)\s+безопасност|інформаційн(?:а|ої)\s+безпек|axborot\s+xavfsizligi/iu],
46
+ ['Data Scientist', /\bdata\s+scientist\b|\bdata\s+science\b|дата\s+саентист/iu],
47
+ ['Data Engineer', /\bdata\s+engineer\b|инженер\s+данных|інженер\s+даних/iu],
48
+ ]);
49
+
50
+ const SCOPE_SIGNALS = Object.freeze([
51
+ Object.freeze({
52
+ code: 'architecture',
53
+ vacancy: /\barchitect(?:ure|ing)?\b|\bsystem design\b|\btechnical roadmap\b|\blong[- ]term solutions?\b/i,
54
+ candidate: /\barchitect(?:ed|ure|ing)?\b|\bsystem design\b|\btechnical roadmap\b|\bsolution architecture\b/i,
55
+ }),
56
+ Object.freeze({
57
+ code: 'leadership',
58
+ vacancy: /\btechnical leader(?:ship)?\b|\bengineering standards\b|\bdrive product vision\b|\blead technical\b/i,
59
+ candidate: /\btechnical leader(?:ship)?\b|\bengineering standards\b|\bled\s+(?:a\s+)?(?:team|project|initiative)\b|\btech(?:nical)? lead\b/i,
60
+ }),
61
+ Object.freeze({
62
+ code: 'mentoring',
63
+ vacancy: /\bmentor(?:ing|ed)?\s+(?:other\s+)?(?:engineers?|developers?|team members?)\b|\bcoach(?:ing|ed)?\s+(?:engineers?|developers?)\b/i,
64
+ candidate: /\bmentor(?:ing|ed)?\s+(?:other\s+)?(?:engineers?|developers?|team members?)\b|\bcoach(?:ing|ed)?\s+(?:engineers?|developers?)\b/i,
65
+ }),
66
+ Object.freeze({
67
+ code: 'scale',
68
+ vacancy: /\b(?:millions?|billions?)\s+of\s+(?:users|people|requests|events)\b|\bat scale\b|\bhigh[- ]scale\b|\blarge[- ]scale\b/i,
69
+ candidate: /\b(?:millions?|billions?)\s+of\s+(?:users|requests|events)\b|\bat scale\b|\bhigh[- ]scale\b|\blarge[- ]scale\b|\bhigh[- ]traffic\b/i,
70
+ }),
71
+ Object.freeze({
72
+ code: 'ownership',
73
+ vacancy: /\bown\s+(?:critical\s+)?(?:features?|systems?|services?|roadmap)\b|\bproduct owner\b|\bdrive product\b/i,
74
+ candidate: /\bown(?:ed|ership)?\s+(?:features?|systems?|services?|roadmap|product)\b|\bproduct owner\b|\bdrove\s+(?:a\s+)?(?:feature|product|initiative)\b/i,
75
+ }),
76
+ ]);
77
+
78
+ const fieldValue = (value, re) => String(value || '').match(re)?.[1]?.trim() || null;
79
+
80
+ export function isCandidateNonTargetContext(value) {
81
+ return NON_TARGET_CONTEXT_RE.test(String(value || ''));
82
+ }
83
+
84
+ export function extractCandidateTargetContext(value) {
85
+ const lines = String(value || '').split('\n').map((line) => line.trim()).filter(Boolean);
86
+ const picked = [];
87
+ for (let i = 0; i < lines.length; i += 1) {
88
+ if (!TARGET_CONTEXT_RE.test(lines[i])) continue;
89
+ for (let offset = 0; offset < 3 && i + offset < lines.length; offset += 1) {
90
+ const line = lines[i + offset];
91
+ if (offset > 0 && NON_TARGET_CONTEXT_RE.test(line)) break;
92
+ picked.push(line);
93
+ }
94
+ }
95
+ return picked.join('\n');
96
+ }
97
+
98
+ export function extractCandidateSkillField(value) {
99
+ return fieldValue(value, SKILLS_FIELD_RE);
100
+ }
101
+
102
+ export function extractCandidateRoleField(value) {
103
+ return fieldValue(value, ROLE_FIELD_RE);
104
+ }
105
+
106
+ export function extractCandidateGoalField(value) {
107
+ return fieldValue(value, GOAL_FIELD_RE);
108
+ }
109
+
110
+ export function extractCandidateLocationField(value) {
111
+ return fieldValue(value, LOCATION_FIELD_RE) || fieldValue(value, LOCATION_COMPACT_RE);
112
+ }
113
+
114
+ export function isCandidateStatusOnly(value) {
115
+ return STATUS_ONLY_RE.test(String(value || '').trim());
116
+ }
117
+
118
+ export function isFlexibleCandidateRole(value) {
119
+ return FLEXIBLE_ROLE_RE.test(String(value || '').trim());
120
+ }
121
+
122
+ export function isCandidateNonRoleValue(value) {
123
+ return NON_ROLE_RE.test(String(value || '').trim());
124
+ }
125
+
126
+ export function extractCandidateGoalRole(value) {
127
+ return String(value || '').match(GOAL_ROLE_RE)?.[1]?.trim() || null;
128
+ }
129
+
130
+ export function extractCandidateWorkHistory(value) {
131
+ const text = String(value || '');
132
+ const explicit = text.match(WORK_HISTORY_BLOCK_RE)?.[1];
133
+ if (explicit) return explicit;
134
+ return text.split(/\n|(?<=[.!?])\s+/u).filter((line) => WORK_HISTORY_LINE_RE.test(line)).join('\n');
135
+ }
136
+
137
+ export function detectCandidateFeatureCodes(value) {
138
+ const text = String(value || '');
139
+ return Object.freeze(FEATURE_RULES.filter(([, re]) => re.test(text)).map(([code]) => code));
140
+ }
141
+
142
+ export function extractCandidateContactHours(value) {
143
+ const raw = String(value || '').match(CONTACT_HOURS_RE)?.[1];
144
+ if (!raw) return null;
145
+ const cleaned = raw.replace(/\s{2,}/g, ' ').replace(/[.;,]+$/, '').trim();
146
+ return /\b24\s*\/\s*7\b|\d{1,2}[:.]\d{2}|\d{1,2}\s*[-–—]\s*\d{1,2}/.test(cleaned)
147
+ ? cleaned.slice(0, 60)
148
+ : null;
149
+ }
150
+
151
+ export function extractCandidateSalaryField(value) {
152
+ return fieldValue(value, SALARY_FIELD_RE);
153
+ }
154
+
155
+ export function detectCandidateRelocationPreference(value) {
156
+ const text = String(value || '');
157
+ if (RELOCATION_NEGATIVE_RE.test(text)) return false;
158
+ if (RELOCATION_POSITIVE_RE.test(text)) return true;
159
+ return null;
160
+ }
161
+
162
+ export function detectCandidateRemotePreference(value) {
163
+ const text = String(value || '');
164
+ if (REMOTE_NEGATIVE_RE.test(text)) return false;
165
+ if (REMOTE_POSITIVE_RE.test(text)) return true;
166
+ return null;
167
+ }
168
+
169
+ export function detectCandidateProfessionLabels(targetValue, skillValue = '') {
170
+ const target = String(targetValue || '');
171
+ const special = SPECIAL_CANDIDATE_PROFESSIONS
172
+ .filter(([, re]) => re.test(target))
173
+ .map(([label]) => label);
174
+ if (special.length) return Object.freeze([...new Set(special)]);
175
+
176
+ const matched = matchExtendedProfessions(target, { limit: 8, allowWeak: true }).map((item) => item.label);
177
+ if (matched.length) return Object.freeze([...new Set(matched)]);
178
+
179
+ const skills = String(skillValue || '');
180
+ if (!skills) return Object.freeze([]);
181
+ if (/\bflutter\b|\bdart\b/iu.test(skills)) return Object.freeze(['Mobile Developer']);
182
+
183
+ const sysadminBase = /\bcisco\b/iu.test(skills)
184
+ && /\b(?:linux|windows\s+server|active\s+directory|network(?:ing)?)\b/iu.test(skills);
185
+ const specializedTools = /\b(?:devops|sre|docker|kubernetes|k8s|terraform|ansible|jenkins|gitlab\s+ci|github\s+actions|ci\/?cd|aws|azure|gcp|ai|ml|mlops|artificial\s+intelligence|machine\s+learning|deep\s+learning|data\s+scien(?:ce|tist)|tensorflow|pytorch|scikit[- ]?learn|llm|langchain|developer|software\s+engineer|programmer|разработчик|программист|react|angular|vue(?:\.js)?|next(?:\.js)?|node(?:\.js)?|django|laravel|spring\s+boot|asp\.net)\b/iu.test(skills);
186
+ if (sysadminBase && !specializedTools) return Object.freeze(['System Administrator']);
187
+
188
+ const strongDeveloperSkill = /\b(?:react|vue\.?\s*js|angular|node\.?\s*js|next\.?\s*js|django|laravel|fastapi|flask|spring|asp\.?net|ruby\s+on\s+rails)\b/iu.test(skills);
189
+ const softwareSignals = (skills.match(/\b(?:python|java(?:script)?|typescript|php|react|vue\.?\s*js|angular|node\.?\s*js|next\.?\s*js|django|laravel|fastapi|flask|spring|asp\.?net|sql|html|css|c\+\+|c#|golang)\b/giu) || []).length;
190
+ return strongDeveloperSkill || softwareSignals >= 2
191
+ ? Object.freeze(['Software Developer'])
192
+ : Object.freeze([]);
193
+ }
194
+
195
+ export function detectManagementRole(title, value = '') {
196
+ const heading = String(title || '');
197
+ const text = String(value || '');
198
+ if (/\b(?:head|director|chief|manager|supervisor|team\s*lead|tech\s*lead)\b|руководител|начальник|директор|тимлид|заведующ/i.test(heading)) return true;
199
+ if (/manage(?:s|ment|ing)?\s+(?:a\s+)?(?:team|people|staff)|people management|direct reports|руковод(?:ить|ство)\s+(?:команд|сотруд)|управлени[ея]\s+(?:команд|персонал)/i.test(text)) return true;
200
+ return null;
201
+ }
202
+
203
+ export function detectHiringScopeSignals(value, options = {}) {
204
+ const mode = options.mode === 'candidate' ? 'candidate' : 'vacancy';
205
+ const text = String(value || '');
206
+ return Object.freeze(SCOPE_SIGNALS.filter((signal) => signal[mode].test(text)).map((signal) => signal.code));
207
+ }
208
+
209
+ export function detectDegreeRequirement(value) {
210
+ const text = String(value || '');
211
+ const level = detectDegreeLevel(text) || undefined;
212
+ const fields = detectDegreeFields(text);
213
+ let field;
214
+ if (fields.includes('computer_science')) field = 'computer_science';
215
+ else if (fields.includes('engineering') && /\bengineering\b[^.;\n]{0,60}\bdegree\b|\bdegree\b[^.;\n]{0,60}\bengineering\b/i.test(text)) field = 'engineering';
216
+ const equivalentExperience = /equivalent\s+(?:professional\s+|work\s+)?experience|or\s+equivalent\s+experience/i.test(text);
217
+ return Object.freeze({ level, field, equivalentExperience });
218
+ }
@@ -1,21 +1,21 @@
1
- export interface SkillMeta {
2
- category: string;
3
- subcategory: string;
4
- }
5
- export interface SkillDefinition extends SkillMeta {
6
- name: string;
7
- aliases: string[];
8
- }
9
- export interface SkillDetail extends SkillMeta {
10
- name: string;
11
- }
12
- export const SKILL_CATALOG: SkillDefinition[];
13
- export const SKILL_KEYWORDS: Readonly<Record<string, readonly string[]>>;
14
- export const SKILL_META: Readonly<Record<string, SkillMeta>>;
15
- export function normalizeSkillText(value: string): string;
16
- export function escapeRegex(value: string): string;
17
- export function buildSkillRegex(alias: string): RegExp;
18
- export function canonicalSkillName(value: string): string | undefined;
19
- export function extractSkillDetails(text: string): SkillDetail[];
20
- export function extractSkillNames(text: string): string[];
21
- export function getSkillMeta(name: string): SkillMeta | undefined;
1
+ export interface SkillMeta {
2
+ category: string;
3
+ subcategory: string;
4
+ }
5
+ export interface SkillDefinition extends SkillMeta {
6
+ name: string;
7
+ aliases: string[];
8
+ }
9
+ export interface SkillDetail extends SkillMeta {
10
+ name: string;
11
+ }
12
+ export const SKILL_CATALOG: SkillDefinition[];
13
+ export const SKILL_KEYWORDS: Readonly<Record<string, readonly string[]>>;
14
+ export const SKILL_META: Readonly<Record<string, SkillMeta>>;
15
+ export function normalizeSkillText(value: string): string;
16
+ export function escapeRegex(value: string): string;
17
+ export function buildSkillRegex(alias: string): RegExp;
18
+ export function canonicalSkillName(value: string): string | undefined;
19
+ export function extractSkillDetails(text: string): SkillDetail[];
20
+ export function extractSkillNames(text: string): string[];
21
+ export function getSkillMeta(name: string): SkillMeta | undefined;