nx_career_parse.nx source
↩ module page · 182 lines · 8304 B
1// nx_career_parse.nx -- POSTING / RESUME -> SKILLS (career ring rung 2; frontier momentum 91). Turns real job
2// text into the skill-csv the matcher consumes, so "real opportunities" runs on live postings, not hand-entry.
3// v1 = SOVEREIGN LEXICON EXTRACTION (honest: term extraction, not an LLM): the skills lexicon is DATA
4// (knowledge/career/skills.lex, one line "skill|alias|alias..."), matching is case-insensitive with WORD
5// BOUNDARIES (salesforce / salesmanship do NOT count as sales), level = mention count clamped 1..5 (v1 heuristic,
6// stated). JD-PARSE extracts a posting's requirements; RESUME-PARSE extracts a person's profile; end-to-end the
7// pair feeds MATCH-SCORE. Composes the nx_career pure core. Argless = selftest gate; CLI: extract <textfile>.
8// expect_exit: 0 license_tier: ORIGINAL
9import "nx_career.nx"
10const K_MAGIC_65536: i64 = 65536
11const K_MAGIC_2048: i64 = 2048
12const K_MAGIC_1048576: i64 = 1048576
13const K_MAGIC_4096: i64 = 4096
14
15func cp_read(path: *u8, buf: *u8, cap: i64) -> i64 {
16 let fd: i64 = sys_openat_rd(path); if fd < 0 { return 0 - 1 }
17 var tot: i64 = 0
18 while tot < cap { let r: i64 = sys_read(fd, (buf as i64 + tot) as *u8, cap - tot); if r <= 0 { break } tot = tot + r }
19 sys_close(fd); return tot
20}
21func cp_alpha(c: i64) -> i64 {
22 if c >= 97 { if c <= 122 { return 1 } }
23 if c >= 48 { if c <= 57 { return 1 } }
24 return 0
25}
26// lowercase copy of NUL-terminated src into dst; returns length
27func cp_lower(dst: *u8, src: *u8, cap: i64) -> i64 {
28 var i: i64 = 0
29 while src[i] != (0 as u8) {
30 if i >= cap - 1 { break }
31 var c: i64 = src[i] as i64
32 if c >= 65 { if c <= 90 { c = c + 32 } }
33 dst[i] = c as u8
34 i = i + 1
35 }
36 dst[i] = 0 as u8
37 return i
38}
39// boundary-aware count of term in t[0,n): char before + after must be non-alphanumeric
40func cp_count_term(t: *u8, n: i64, term: *u8) -> i64 {
41 let m: i64 = slen(term)
42 if m == 0 { return 0 }
43 var cnt: i64 = 0
44 var i: i64 = 0
45 while i + m <= n {
46 var hit: i64 = 1
47 var k: i64 = 0
48 while k < m { if t[i+k] != term[k] { hit = 0; k = m } else { k = k + 1 } }
49 if hit == 1 {
50 var lb: i64 = 1
51 if i > 0 { if cp_alpha(t[i-1] as i64) == 1 { lb = 0 } }
52 var rb: i64 = 1
53 if i + m < n { if cp_alpha(t[i+m] as i64) == 1 { rb = 0 } }
54 if lb == 1 { if rb == 1 { cnt = cnt + 1 } }
55 i = i + m
56 } else { i = i + 1 }
57 }
58 return cnt
59}
60// extract skills from text using the lexicon -> "name:lvl,name:lvl" csv in out; returns csv length
61func cp_extract(text: *u8, lex: *u8, lexn: i64, out: *u8, cap: i64) -> i64 {
62 let tl: i64 = slen(text)
63 let tb: *u8 = sys_mmap(tl + 4)
64 cp_lower(tb, text, tl + 2)
65 let termb: *u8 = sys_mmap(160)
66 let nameb: *u8 = sys_mmap(160)
67 var o: i64 = 0
68 var i: i64 = 0
69 while i < lexn {
70 var j: i64 = i
71 while j < lexn { if lex[j] == (10 as u8) { break } j = j + 1 }
72 if lex[i] != (35 as u8) {
73 if j > i {
74 var total: i64 = 0
75 var first: i64 = 1
76 var s: i64 = i
77 while s < j {
78 var e: i64 = s
79 while e < j { if lex[e] == (124 as u8) { break } e = e + 1 }
80 var t: i64 = 0
81 while s + t < e { if t < 158 { termb[t] = lex[s + t] } t = t + 1 }
82 if t > 158 { t = 158 }
83 // trim a trailing CR
84 if t > 0 { if termb[t-1] == (13 as u8) { t = t - 1 } }
85 termb[t] = 0 as u8
86 if t > 0 {
87 if first == 1 {
88 var q: i64 = 0
89 while q <= t { nameb[q] = termb[q]; q = q + 1 }
90 first = 0
91 }
92 total = total + cp_count_term(tb, tl, termb)
93 }
94 s = e + 1
95 }
96 if total > 0 {
97 var lvl: i64 = total
98 if lvl > 5 { lvl = 5 }
99 if o > 0 { if o < cap - 1 { out[o] = 44 as u8; o = o + 1 } }
100 var q2: i64 = 0
101 while nameb[q2] != (0 as u8) { if o < cap - 1 { out[o] = nameb[q2]; o = o + 1 } q2 = q2 + 1 }
102 if o < cap - 1 { out[o] = 58 as u8; o = o + 1 }
103 if o < cap - 1 { out[o] = (48 + lvl) as u8; o = o + 1 }
104 }
105 }
106 }
107 i = j + 1
108 }
109 out[o] = 0 as u8
110 return o
111}
112
113func cp_selftest() -> i64 {
114 p("=== NX-CAREER-PARSE SELFTEST (JD-PARSE + RESUME-PARSE, lexicon + word boundaries, end-to-end match) ===\n" as *u8)
115 var ok: i64 = 1
116 let lex: *u8 = sys_mmap(K_MAGIC_65536)
117 let lexn: i64 = cp_read("knowledge/career/skills.lex" as *u8, lex, K_MAGIC_65536)
118 if lexn <= 0 { p("NX-CAREER-PARSE lexicon knowledge/career/skills.lex MISSING -- fail loud\n" as *u8); return 1 }
119
120 let csv: *u8 = sys_mmap(K_MAGIC_2048)
121 // JD-PARSE: a posting with boundary traps (Salesforce/salesmanship must NOT count as sales)
122 let posting: *u8 = "Account Executive at Westfield. We need sales talent: own the sales pipeline and hit the sales quota. CRM required: keep crm hygiene and crm reporting daily. Salesforce experience welcome; salesmanship cliches not required." as *u8
123 cp_extract(posting, lex, lexn, csv, K_MAGIC_2048)
124 p(" JD-PARSE posting -> " as *u8); p(csv); p("\n" as *u8)
125 if seq(csv, "sales:3,crm:3" as *u8) != 1 { ok = 0 }
126
127 // RESUME-PARSE: Emma's resume text
128 let resume: *u8 = "Emma. sales rep of the year with sales awards; grew enterprise sales and channel sales. crm admin who built crm workflows and crm dashboards. writing daily: writing blogs, writing docs, writing scripts, and writing newsletters." as *u8
129 let rcsv: *u8 = sys_mmap(K_MAGIC_2048)
130 cp_extract(resume, lex, lexn, rcsv, K_MAGIC_2048)
131 p(" RESUME-PARSE resume -> " as *u8); p(rcsv); p("\n" as *u8)
132 if seq(rcsv, "sales:4,crm:3,writing:5" as *u8) != 1 { ok = 0 }
133
134 // alias: "selling" maps to sales
135 let al: *u8 = sys_mmap(256)
136 cp_extract("I am great at selling." as *u8, lex, lexn, al, 256)
137 p(" alias selling -> " as *u8); p(al); p("\n" as *u8)
138 if seq(al, "sales:1" as *u8) != 1 { ok = 0 }
139
140 // boundary negative: substrings alone extract NOTHING
141 let ng: *u8 = sys_mmap(256)
142 let ngn: i64 = cp_extract("salesforce salesmanship crmx" as *u8, lex, lexn, ng, 256)
143 p(" boundary-negative extracted_len=" as *u8); pn(ngn); p("\n" as *u8)
144 if ngn != 0 { ok = 0 }
145
146 // END-TO-END: parsed resume vs parsed posting -> MATCH-SCORE
147 let nb: *u8 = sys_mmap(64 * 16)
148 let nb2: *u8 = sys_mmap(64 * 16)
149 let hn: *i64 = sys_mmap(8 * 16) as *i64
150 let hl: *i64 = sys_mmap(8 * 16) as *i64
151 let rn: *i64 = sys_mmap(8 * 16) as *i64
152 let rl: *i64 = sys_mmap(8 * 16) as *i64
153 let hc: i64 = skills_parse(rcsv, hn, hl, 16, nb)
154 let rc: i64 = skills_parse(csv, rn, rl, 16, nb2)
155 let sc: i64 = match_score(hn, hl, hc, rn, rl, rc)
156 p(" end-to-end: parsed resume vs parsed posting MATCH-SCORE = " as *u8); pn(sc); p("/100\n" as *u8)
157 if sc != 100 { ok = 0 }
158
159 p("NX-CAREER-PARSE-SELFTEST lexicon_bytes=" as *u8); pn(lexn); p(" " as *u8)
160 if ok == 1 { p("verdict=GREEN\n" as *u8); return 0 }
161 p("verdict=RED\n" as *u8)
162 return 1
163}
164
165func main(argc: i64, argv: *i64) -> i64 {
166 if argc < 2 { return cp_selftest() }
167 if seq(argv[1] as *u8, "extract" as *u8) == 1 {
168 if argc < 3 { p("usage: extract <textfile>\n" as *u8); return 1 }
169 let lex: *u8 = sys_mmap(K_MAGIC_65536)
170 let lexn: i64 = cp_read("knowledge/career/skills.lex" as *u8, lex, K_MAGIC_65536)
171 if lexn <= 0 { p("lexicon missing -- fail loud\n" as *u8); return 1 }
172 let txt: *u8 = sys_mmap(K_MAGIC_1048576)
173 let tn: i64 = cp_read(argv[2] as *u8, txt, K_MAGIC_1048576)
174 if tn <= 0 { p("text file missing -- fail loud\n" as *u8); return 1 }
175 txt[tn] = 0 as u8
176 let csv: *u8 = sys_mmap(K_MAGIC_4096)
177 cp_extract(txt, lex, lexn, csv, K_MAGIC_4096)
178 p("EXTRACT " as *u8); p(csv); p("\n" as *u8)
179 return 0
180 }
181 return cp_selftest()
182}