From af3856b1035f83bd30ff4f7b8c9302c375001ad6 Mon Sep 17 00:00:00 2001 From: root Date: Mon, 20 Apr 2026 18:56:51 -0500 Subject: [PATCH] Rate/margin awareness: implied pay rate per worker, bill rate per contract MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Closes one of the Path 1 trust-break gaps. The scenario we kept flagging: recruiter calls the system's top pick, worker quotes $35/hr, contract pays $28/hr. First broken call kills the demo. This fixes it. Heuristic (no schema change, derived at query time): - Per worker: implied_pay_rate = role_base + (reliability × 4) + archetype_bump role_base: Electrician $28, Welder $26, Machine Op $24, Maint $26, Forklift Op $20, Loader $17, Warehouse Assoc $17, Quality Tech $23, Production Worker $18 ... archetype bump: specialist +4, leader +3, reliable +1, else 0 - Per contract: implied_bill_rate = role_base × 1.4 (40% markup — industry norm: pay + overhead + insurance + margin) - Worker is 'over_bill_rate' when implied_pay_rate > contract's bill_rate on a candidate-by-candidate basis Backend (mcp-server/index.ts): - ROLE_BASE_PAY_RATE + BILL_MARKUP constants - impliedPayRate(worker), impliedBillRate(role) functions - parseWorkerChunk() extracts role/reliability/archetype from vector text - enrichWithRates() attaches implied_pay_rate on every /vectors/hybrid source response. Called from /search and /intelligence/permit_contracts. - /search accepts optional max_pay_rate number — if set, filters out workers above that rate and reports pay_rate_filtered_out count. - /intelligence/permit_contracts returns implied_bill_rate per contract AND over_bill_rate boolean per candidate. Frontend (search.html): - Live Contracts cards show 'bill rate: $X/hr' under the headcount line - Each candidate shows 'pay $X/hr' in the sub-line; red 'Over bill rate' chip next to name when their pay exceeds the contract's bill rate (hover reveals the exact numbers and why it's flagged) - Main 'Search all workers' results now include 'pay $X/hr' in the why-text (computeImpliedPayRate mirrored client-side to match Bun) End-to-end verified live: - Masonry Work permit, bill_rate $25.20/hr Kathleen M. Gutierrez pay $25.56/hr → 🔴 OVER Melissa C. Rivera pay $20.88/hr → 🟢 OK - /search with max_pay_rate:32 filtered out 1 Toledo Welder above $32 - Main search shows 'pay $28.64/hr' in each result row When real ATS data replaces synthetic workers_500k, same UI — the client's real pay_rate column substitutes for the heuristic. --- mcp-server/index.ts | 94 +++++++++++++++++++++++++++++++++++++++++- mcp-server/search.html | 38 ++++++++++++++++- 2 files changed, 129 insertions(+), 3 deletions(-) diff --git a/mcp-server/index.ts b/mcp-server/index.ts index f6ec07e..2e55018 100644 --- a/mcp-server/index.ts +++ b/mcp-server/index.ts @@ -442,13 +442,24 @@ async function main() { filter = `(${filter}) AND worker_id NOT IN (${ids.join(",")})`; } } - return ok(await api("POST", "/vectors/hybrid", { + const hybridRes = await api("POST", "/vectors/hybrid", { question: b.question, index_name: b.index || "workers_500k_v1", sql_filter: filter, filter_dataset: b.dataset || "ethereal_workers", id_column: b.id_column || "worker_id", top_k: b.top_k || 5, generate: b.generate !== false, use_playbook_memory: b.use_playbook_memory !== false, playbook_memory_k: b.playbook_memory_k ?? 200, - })); + }); + // Rate enrichment + optional max_pay_rate filter (soft filter, + // preserves result shape). Operator can opt out by omitting. + if (hybridRes && Array.isArray(hybridRes.sources)) { + enrichWithRates(hybridRes.sources); + if (typeof b.max_pay_rate === "number" && b.max_pay_rate > 0) { + const before = hybridRes.sources.length; + hybridRes.sources = hybridRes.sources.filter((s: any) => s.implied_pay_rate <= b.max_pay_rate); + (hybridRes as any).pay_rate_filtered_out = before - hybridRes.sources.length; + } + } + return ok(hybridRes); } // Tool: SQL @@ -1080,6 +1091,9 @@ async function main() { min_trait_frequency: 0.3, }).catch(() => ({} as any)); + // Enrich with implied pay rate before taking the top-5 + enrichWithRates(searchRes.sources || []); + const contractBillRate = impliedBillRate(role); const sources = (searchRes.sources || []).slice(0, 5).map((s: any) => { const name = String(s.chunk_text || "").split("—")[0]?.trim() || s.doc_id; return { @@ -1088,6 +1102,8 @@ async function main() { score: s.score, playbook_boost: s.playbook_boost || 0, playbook_citations: s.playbook_citations || [], + implied_pay_rate: s.implied_pay_rate ?? null, + over_bill_rate: (s.implied_pay_rate ?? 0) > contractBillRate, }; }); @@ -1114,6 +1130,7 @@ async function main() { community_area: p.community_area, issue_date: (p.issue_date || "").substring(0, 10), }, + implied_bill_rate: contractBillRate, timeline: { estimated_construction_start: estStart.toISOString().slice(0, 10), staffing_window_opens: stagingDate.toISOString().slice(0, 10), @@ -1789,6 +1806,79 @@ async function runAlertsOnce() { // Seed playbook_memory from a filled contract so the next hybrid query // ranks against it. Used by both runWeekSimulation (per-day) and the /log // endpoint (per manual logging). Fail-soft — seeding is best-effort. +// ─── Rate/margin awareness ────────────────────────────────────────────── +// Derive implied pay and bill rates per worker / per contract without +// schema changes. Numbers are industry heuristics — a real deployment +// would replace these with the client's actual ATS pay_rate column and +// contract bill_rate. The shape stays the same; only the source changes. + +const ROLE_BASE_PAY_RATE: Record = { + "Electrician": 28, + "Welder": 26, + "Machine Operator": 24, + "Maintenance Tech": 26, + "Forklift Operator": 20, + "Loader": 17, + "Warehouse Associate": 17, + "Material Handler": 18, + "Production Worker": 18, + "Quality Tech": 23, + "Line Lead": 22, + "Assembler": 18, + "Shipping Clerk": 19, +}; +const DEFAULT_BASE_PAY = 19; +// Staffing firm typically marks up pay to bill by 35-45% to cover +// overhead, insurance, and margin. Using 40% as the midpoint. +const BILL_MARKUP = 1.4; + +function impliedPayRate(w: { role?: string | null; reliability?: number | string | null; archetype?: string | null }): number { + const role = w.role || ""; + const base = ROLE_BASE_PAY_RATE[role] ?? DEFAULT_BASE_PAY; + const rel = typeof w.reliability === "string" ? parseFloat(w.reliability) : (w.reliability ?? 0.5); + const relBump = (isFinite(rel) ? rel : 0.5) * 4; + const arch = (w.archetype || "").toLowerCase(); + const archBump = arch === "specialist" ? 4 : arch === "leader" ? 3 : arch === "reliable" ? 1 : 0; + return Math.round((base + relBump + archBump) * 100) / 100; +} + +function impliedBillRate(role: string | null | undefined): number { + const base = ROLE_BASE_PAY_RATE[role || ""] ?? DEFAULT_BASE_PAY; + // Contract bill rate = base pay × markup. This is what a staffing firm + // would typically quote for this role — the worker's rate has to be + // below this to keep margin. + return Math.round((base * BILL_MARKUP) * 100) / 100; +} + +// Parse a worker's role / reliability / archetype from a vector chunk +// shaped like "Name — Role in City, ST. Skills: ... . Certs: ... . +// Archetype: reliable. Reliability: 0.93, Availability: 0.73" +function parseWorkerChunk(chunk: string): { role?: string; reliability?: number; archetype?: string } { + if (!chunk) return {}; + const out: any = {}; + const roleMatch = chunk.match(/—\s*([^\.]+?)\s+in\s+/); + if (roleMatch) out.role = roleMatch[1].trim(); + const relMatch = chunk.match(/Reliability:\s*([\d\.]+)/i); + if (relMatch) out.reliability = parseFloat(relMatch[1]); + const archMatch = chunk.match(/Archetype:\s*([A-Za-z]+)/i); + if (archMatch) out.archetype = archMatch[1]; + return out; +} + +// Attach implied_pay_rate to each hybrid source in place, using either +// the row's native fields (from sql_results) or parsed from chunk_text. +function enrichWithRates(sources: any[]): void { + for (const s of sources || []) { + const parsed = parseWorkerChunk(s.chunk_text || ""); + const w = { + role: s.role ?? parsed.role, + reliability: s.reliability ?? s.rel ?? parsed.reliability, + archetype: s.archetype ?? s.arch ?? parsed.archetype, + }; + s.implied_pay_rate = impliedPayRate(w); + } +} + async function seedPlaybookFromContract(c: any) { const names = (c.matches || []).slice(0, 5) .map((m: any) => m.name || m.doc_id) diff --git a/mcp-server/search.html b/mcp-server/search.html index c4c95f4..47a5850 100644 --- a/mcp-server/search.html +++ b/mcp-server/search.html @@ -222,6 +222,22 @@ function api(path,body){ return fetch(A+path,{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify(body)}).then(function(r){return r.json()}) } +// Mirror of the Bun-side pay-rate formula so client-side renderers +// (main search, modal) can show a rate when they only have worker +// fields, not a pre-enriched hybrid source. Keep in sync with +// impliedPayRate() in mcp-server/index.ts. +var ROLE_BASE_PAY={Electrician:28,Welder:26,'Machine Operator':24,'Maintenance Tech':26, + 'Forklift Operator':20,Loader:17,'Warehouse Associate':17,'Material Handler':18, + 'Production Worker':18,'Quality Tech':23,'Line Lead':22,Assembler:18,'Shipping Clerk':19}; +function computeImpliedPayRate(role,rel,archetype){ + var base=ROLE_BASE_PAY[role||'']||19; + var r=typeof rel==='string'?parseFloat(rel):(rel||0.5); + var relBump=(isFinite(r)?r:0.5)*4; + var a=(archetype||'').toLowerCase(); + var archBump=a==='specialist'?4:a==='leader'?3:a==='reliable'?1:0; + return Math.round((base+relBump+archBump)*100)/100; +} + function loadLiveContracts(){ // Pair live Chicago permits with our 500K worker bench and the // meta-index discovered patterns for each role+geo. This is the @@ -258,6 +274,13 @@ function loadLiveContracts(){ var sub=document.createElement('div');sub.style.cssText='color:#545d68;font-size:10px;text-align:right'; sub.textContent='pool: '+(prop.pool_size||'?').toLocaleString()+' available'; right.appendChild(sub); + // Rate awareness: show implied bill rate per contract + if(c.implied_bill_rate){ + var rate=document.createElement('div'); + rate.style.cssText='color:#d29922;font-size:10px;text-align:right;margin-top:3px'; + rate.textContent='bill rate: $'+c.implied_bill_rate.toFixed(2)+'/hr'; + right.appendChild(rate); + } hdr.appendChild(left);hdr.appendChild(right);card.appendChild(hdr); // Description if(p.description){ @@ -286,8 +309,17 @@ function loadLiveContracts(){ chip.textContent='Endorsed · '+(cand.playbook_citations||[]).length+' playbook'+((cand.playbook_citations||[]).length===1?'':'s'); nm.appendChild(chip); } + // Rate warning chip when worker's pay exceeds the contract's bill rate + if(cand.over_bill_rate){ + var warn=document.createElement('span');warn.style.cssText='margin-left:6px;padding:2px 7px;border-radius:9px;font-size:9px;font-weight:600;background:#3a1a1a;border:1px solid #f85149;color:#fca5a5;vertical-align:middle'; + warn.textContent='Over bill rate'; + warn.title='Worker\'s implied pay rate ($'+(cand.implied_pay_rate||0).toFixed(2)+'/hr) exceeds contract bill rate ($'+(c.implied_bill_rate||0).toFixed(2)+'/hr) — margin at risk'; + nm.appendChild(warn); + } var sub2=document.createElement('div');sub2.style.cssText='color:#545d68;font-size:10px'; - sub2.textContent=cand.doc_id+' · score '+(cand.score||0).toFixed(3); + var subText=cand.doc_id+' · score '+(cand.score||0).toFixed(3); + if(cand.implied_pay_rate) subText+=' · pay $'+cand.implied_pay_rate.toFixed(2)+'/hr'; + sub2.textContent=subText; info.appendChild(nm);info.appendChild(sub2); row.appendChild(av);row.appendChild(info); card.appendChild(row); @@ -933,6 +965,10 @@ function doSearch(){ var why='Reliability: '+Math.round((w.rel||0)*100)+'%'; if(w.avail)why+=' · Available: '+Math.round(w.avail*100)+'%'; if(w.archetype)why+=' · '+w.archetype; + // Derive and show implied pay rate client-side so the main search + // surface matches the live-contracts cards. Same formula as Bun. + var rate=computeImpliedPayRate(w.role,w.rel,w.archetype); + if(rate) why+=' · pay $'+rate.toFixed(2)+'/hr'; addWorkerInsight(out,w.name,detail.join(' · '),why,i,null,wd); }); } else {