From fb99e92a60f1a8989f2b30d9e3af7ae8c0c6fd30 Mon Sep 17 00:00:00 2001
From: root
Date: Mon, 27 Apr 2026 20:49:15 -0500
Subject: [PATCH 01/43] =?UTF-8?q?demo:=20P1=20=E2=80=94=20search=20filter?=
=?UTF-8?q?=20now=20actually=20filters=20by=20state=20and=20role?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
The Co-Pilot search box read state and role from the dropdowns (#sst, #srl)
but appended them to the message string as ' in '+st. The server's NL
parser then matched the literal preposition "in" against the case-insensitive
regex /\b(IL|IN|...)\b/i and assigned state IN (Indiana) to every search.
Result: typing "forklift in IL" returned Indiana workers. Same for WI, TX,
any state — all silently became Indiana. That was the "cached/generic
response" the legacy staffing client was seeing.
Two prongs:
1. search.html doSearch() now passes structured fields:
{message, state, role}
instead of munging into the message text. Dropdown selections bypass
NL parsing entirely.
2. /intelligence/chat smart_search route accepts those structured fields
and prefers them over regex archaeology. Falls back to NL parsing only
when fields aren't provided. Fixed the regex too: the prepositional
form (?:in|from)\s+(STATE) wins, the standalone form requires uppercase
(drops /i flag) so the lowercase preposition "in" can no longer match.
Verified live:
- POST /intelligence/chat {"message":"forklift","state":"IL"}
→ 167 IL forklift operators (Galesburg, Joliet, ...)
- POST /intelligence/chat {"message":"forklift","state":"WI","role":"Forklift Operator"}
→ 16 WI Forklift Operators (Milwaukee, Madison, ...)
- POST /intelligence/chat {"message":"forklift in IL"} (NL fallback)
→ 167 IL workers (regex now correctly distinguishes preposition from state code)
Playwright drove the live UI through devop.live/lakehouse and confirmed the
front-end posts the structured body and the result panel renders the right
state. Restart sequence: kill old bun :3700, bun run mcp-server/index.ts.
---
mcp-server/index.ts | 47 +++++++++++++++++++++++++++++++++---------
mcp-server/search.html | 12 ++++++-----
2 files changed, 44 insertions(+), 15 deletions(-)
diff --git a/mcp-server/index.ts b/mcp-server/index.ts
index de804fa..9b4134b 100644
--- a/mcp-server/index.ts
+++ b/mcp-server/index.ts
@@ -1698,7 +1698,14 @@ async function main() {
const filters: string[] = ["CAST(reliability AS DOUBLE) >= 0.5"];
const understood: string[] = [];
- // Extract role keywords
+ // Structured input from the search-form dropdowns. When set,
+ // these win over NL parsing — typing "forklift in IL" used to
+ // misparse the preposition "in" as state IN (Indiana). Trust
+ // explicit user selection over regex archaeology.
+ const explicitState = String(b.state || "").trim().toUpperCase();
+ const explicitRole = String(b.role || "").trim();
+
+ // Extract role keywords (skip if dropdown picked one)
const roleKeywords: Record = {
"warehouse": "warehouse", "forklift": "forklift", "welder": "weld", "assembler": "assembl",
"loader": "loader", "machine operator": "machine operator", "shipping": "shipping",
@@ -1707,8 +1714,13 @@ async function main() {
"line lead": "line lead", "electrician": "electric", "packaging": "packaging",
"tool and die": "tool", "logistics": "logistics", "safety": "safety", "cnc": "cnc",
};
- for (const [kw, sqlPart] of Object.entries(roleKeywords)) {
- if (lower.includes(kw)) { filters.push(`LOWER(role) LIKE '%${sqlPart}%'`); understood.push(`role: ${kw}`); break; }
+ if (explicitRole) {
+ filters.push(`LOWER(role) LIKE '%${explicitRole.toLowerCase().replace(/'/g, "''")}%'`);
+ understood.push(`role: ${explicitRole}`);
+ } else {
+ for (const [kw, sqlPart] of Object.entries(roleKeywords)) {
+ if (lower.includes(kw)) { filters.push(`LOWER(role) LIKE '%${sqlPart}%'`); understood.push(`role: ${kw}`); break; }
+ }
}
// Extract city
@@ -1726,18 +1738,33 @@ async function main() {
}
}
- // Extract state
+ // Extract state — dropdown wins; otherwise NL parse, but
+ // require either an explicit "in/from " preposition
+ // OR an UPPERCASE 2-letter code, never a bare lowercase
+ // 2-letter token. Old regex matched "in" (preposition) as
+ // state IN (Indiana) because the /i flag made the standalone
+ // pattern case-insensitive — "forklift in IL" always returned
+ // Indiana workers.
const stateNames: Record = {
"illinois":"IL","indiana":"IN","ohio":"OH","missouri":"MO","tennessee":"TN",
"kentucky":"KY","wisconsin":"WI","michigan":"MI","iowa":"IA","minnesota":"MN"
};
- const stateMatch = lower.match(/\b(IL|IN|OH|MO|TN|KY|WI|MI|IA|MN)\b/i);
- if (stateMatch && !understood.some(u => u.startsWith('city'))) {
- filters.push(`state = '${stateMatch[1].toUpperCase()}'`);
- understood.push(`state: ${stateMatch[1].toUpperCase()}`);
+ if (explicitState) {
+ if (!understood.some(u => u.startsWith('city'))) {
+ filters.push(`state = '${explicitState.replace(/'/g, "''")}'`);
+ understood.push(`state: ${explicitState}`);
+ }
} else {
- for (const [name, abbr] of Object.entries(stateNames)) {
- if (lower.includes(name)) { filters.push(`state = '${abbr}'`); understood.push(`state: ${abbr}`); break; }
+ const prepMatch = q.match(/\b(?:in|from)\s+(IL|IN|OH|MO|TN|KY|WI|MI|IA|MN)\b/i);
+ const upperMatch = q.match(/\b(IL|IN|OH|MO|TN|KY|WI|MI|IA|MN)\b/); // no /i — must be uppercase
+ const stateMatch = prepMatch || upperMatch;
+ if (stateMatch && !understood.some(u => u.startsWith('city'))) {
+ filters.push(`state = '${stateMatch[1].toUpperCase()}'`);
+ understood.push(`state: ${stateMatch[1].toUpperCase()}`);
+ } else {
+ for (const [name, abbr] of Object.entries(stateNames)) {
+ if (lower.includes(name)) { filters.push(`state = '${abbr}'`); understood.push(`state: ${abbr}`); break; }
+ }
}
}
diff --git a/mcp-server/search.html b/mcp-server/search.html
index 7a15c1c..1a3c4c1 100644
--- a/mcp-server/search.html
+++ b/mcp-server/search.html
@@ -2274,13 +2274,15 @@ function doSearch(){
var q=document.getElementById('sq').value.trim();if(!q)return;
lastQuery=q;
var st=document.getElementById('sst').value,rl=document.getElementById('srl').value;
- // Append dropdown filters to the query so the smart parser picks them up
- var fullQ=q;
- if(st&&q.indexOf(st)<0)fullQ+=' in '+st;
- if(rl&&q.toLowerCase().indexOf(rl.toLowerCase())<0)fullQ+=' '+rl;
+ // Pass dropdown filters as structured fields. Old code appended
+ // ' in '+st to the message, which the server misparsed: the
+ // preposition "in" matched the regex for state code "IN" (Indiana)
+ // and every search returned Indiana workers regardless of dropdown.
+ // Sending structured state/role lets the server skip NL parsing
+ // for those fields entirely.
var out=document.getElementById('sresults');out.textContent='Finding the best matches...';
fetch(A+'/intelligence/chat',{method:'POST',headers:{'Content-Type':'application/json'},
- body:JSON.stringify({message:fullQ})
+ body:JSON.stringify({message:q,state:st||undefined,role:rl||undefined})
}).then(function(r){return r.json()}).then(function(d){
out.textContent='';
// Show what the system understood
--
2.47.2
From 677065de7698e12aac498139f9674f31af95939f Mon Sep 17 00:00:00 2001
From: root
Date: Mon, 27 Apr 2026 21:05:40 -0500
Subject: [PATCH 02/43] =?UTF-8?q?demo:=20P2=20=E2=80=94=20staffer-language?=
=?UTF-8?q?=20routes=20(zip,=20headcount,=20name,=20late-triage,=20ingest?=
=?UTF-8?q?=20log)?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
Built from a playwright run as three personas:
Maria — "8 production workers near 60607 by next Friday, prior-fill at this client"
Devon — "what came in last night?"
Aisha — "Marcus running late site 4422"
Each one previously fell through to smart_search and returned irrelevant
results (geo wrong, headcount ignored, no triage, no temporal). Now:
A. Zip code → city/state lookup. Chicago zips (606xx, 607xx, 608xx)
resolve to {city: Chicago, state: IL}; 13 metro prefixes covered.
Maria's "near 60607" now returns Chicago workers, not Dayton/Green Bay.
B. Headcount parser. "8 production workers" / "12 forklift operators" /
"5 welders" set top_k 1..200, capped 5..25 for SQL+vector LIMIT.
Allows 0-2 role words between the count and the worker noun so
"8 production workers" matches as well as "8 workers".
C. Bare-name profile lookup. Single short capitalized phrase
("Marcus" / "Sarah Lopez") triggers a profile route. Per-token LIKE
AND-joined so "Marcus Rivera" matches "Marcus L. Rivera" without
hardcoding middle initials.
E. Late-worker / no-show triage. Pattern: (running late|late|
no show|sick|out today|called out|can't make it) — pulls profile +
reliability + responsiveness + recent calls, sources 5 same-role
same-geo backfills sorted by responsiveness, drafts a client SMS
the coordinator can copy. Front-end renders triage card + Copy SMS
button + green backfill list.
F. Contractor name preview anchor. The PROJECT INDEX preview line on
each permit card now wraps contact_1_name and contact_2_name in
anchors to /contractor?name=... — clicking a contractor finally
navigates instead of doing nothing. Click handler stops propagation
so the details element doesn't toggle.
D. Temporal "what came in" route. last night / today / past N hours /
recent — surfaces datasets from the catalog whose updated_at is
within the window, samples one row per dataset to detect worker-
shape, groups by role for worker tables. Schema-agnostic — drop
any dataset and it shows up. Currently sparse because no fresh
ingest has happened today; will populate as ingest runs.
Server: /intelligence/chat smart_search route accepts structured
state/role from the search-form dropdowns (P1 from prior commit) and
now ALSO honors b.state, b.role, q.match for headcount + zip + name +
triage patterns BEFORE falling through to NL parsing.
Front-end: doSearch dispatches on response.type and renders triage,
profile, ingest_log, and miss states with type-specific UI. All DOM
construction uses textContent / appendChild — no innerHTML, no XSS.
Verified end-to-end via playwright drive of devop.live/lakehouse:
Maria → 8 Chicago Production Workers (60685, 60662, 60634)
tags: "headcount: 8 · zip 60607 → Chicago, IL · ..."
Aisha → Marcus V. Campbell card + draft SMS + 5 Quincy IL backfills
"I'm dispatching Scott B. Cooper (96% reliability) to cover."
Devon → ingest_log surfaces successful_playbooks_live (last 1h)
Marcus → 5 profiles (Adams Louisville KY, Jenkins Green Bay WI, ...)
Screenshots: /tmp/persona_v2/{01_maria,02_aisha,03_devon,04_marcus}.png
Restart sequence after these edits: pkill -9 -f "mcp-server/index.ts" ;
cd /home/profit/lakehouse ; bun run mcp-server/index.ts. The bun on
:3700 is not systemd-managed (pre-existing convention).
---
mcp-server/index.ts | 288 +++++++++++++++++++++++++++++++++++++++--
mcp-server/search.html | 153 +++++++++++++++++++++-
2 files changed, 426 insertions(+), 15 deletions(-)
diff --git a/mcp-server/index.ts b/mcp-server/index.ts
index 9b4134b..d7ca82d 100644
--- a/mcp-server/index.ts
+++ b/mcp-server/index.ts
@@ -1693,6 +1693,163 @@ async function main() {
queries_run: queries, duration_ms: Date.now() - start });
}
+ // Route 6: late-worker / no-show triage. Coordinator gets a text
+ // ("Marcus running late site 4422") and needs three things in
+ // one shot: the worker's record + attendance pattern, a draft
+ // SMS to the client, and a ranked list of immediately-available
+ // backfills filtered by the same role+geo. The system already
+ // has every input (workers_500k, call_log, playbook_memory).
+ // The route binds them.
+ // No /i — the name has to be capitalized (English convention)
+ // and the event verbs are matched lowercase. The /i flag was
+ // letting "Marcus running" parse as "Marcus Running" (a last
+ // name) and then the event regex wouldn't find "running late"
+ // because "running" was already consumed by the name group.
+ const triageMatch = q.match(/^([A-Z][a-z]+(?:\s+[A-Z]\.?\s*)?(?:\s+[A-Z][a-z]+)?)\s+(running\s+late|late|no\s*show|no-show|sick|out\s+today|called\s+out|called\s+in|can'?t\s+make\s+it|won'?t\s+make\s+it)/);
+ if (triageMatch) {
+ const name = triageMatch[1].trim();
+ const event = triageMatch[2].toLowerCase().replace(/\s+/g, " ");
+ queries.push(`SQL: locate ${name}'s worker record`);
+ const profileR = await api("POST", "/query/sql", { sql: `SELECT name, role, city, state, zip, ROUND(CAST(reliability AS DOUBLE),2) rel, ROUND(CAST(availability AS DOUBLE),2) avail, ROUND(CAST(responsiveness AS DOUBLE),2) resp, archetype, skills, certifications FROM workers_500k WHERE name LIKE '%${name.replace(/'/g, "''")}%' ORDER BY CAST(reliability AS DOUBLE) DESC LIMIT 1` });
+ if (profileR.rows?.length) {
+ const w = profileR.rows[0];
+ // Pull attendance pattern from call_log if available — count
+ // recent calls + count of unanswered/late patterns. If the
+ // table doesn't exist or has nothing, we surface that
+ // honestly rather than fabricate.
+ queries.push(`SQL: ${w.name}'s recent contact pattern`);
+ const callR = await api("POST", "/query/sql", { sql: `SELECT COUNT(*) calls FROM call_log WHERE candidate_id IN (SELECT candidate_id FROM workers_500k WHERE name = '${w.name.replace(/'/g, "''")}')` }).catch(() => null);
+ const callCount = callR?.rows?.[0]?.calls ?? null;
+
+ // Backfills: same role + same geo, available now, ordered
+ // by responsiveness (a coordinator covering a no-show
+ // wants the candidate who actually answers their phone).
+ queries.push(`Backfill: ${w.role} in ${w.city}, ${w.state}, available, sorted by responsiveness`);
+ const backfillR = await api("POST", "/query/sql", { sql: `SELECT name, role, city, state, zip, ROUND(CAST(reliability AS DOUBLE),2) rel, ROUND(CAST(availability AS DOUBLE),2) avail, ROUND(CAST(responsiveness AS DOUBLE),2) resp, archetype, skills FROM workers_500k WHERE role = '${w.role.replace(/'/g, "''")}' AND city = '${(w.city||"").replace(/'/g, "''")}' AND state = '${(w.state||"").replace(/'/g, "''")}' AND name != '${w.name.replace(/'/g, "''")}' AND CAST(availability AS DOUBLE) > 0.6 ORDER BY CAST(responsiveness AS DOUBLE) DESC, CAST(reliability AS DOUBLE) DESC LIMIT 5` });
+
+ // Draft SMS the coordinator can send to the client. This
+ // is template-generated, not LLM — the coordinator must
+ // be able to send it instantly without re-reading. Names
+ // and roles are interpolated; the COORDINATOR sends.
+ const eventLabel = event.includes("late") ? "running late" : event.includes("show") ? "a no-show" : event.includes("sick") || event.includes("out") ? "out today" : "unable to make their shift";
+ const backfills = backfillR.rows || [];
+ const topBackfill = backfills[0]?.name;
+ const draftSms = topBackfill
+ ? `Heads-up: ${w.name} (${w.role}) is ${eventLabel}. I'm dispatching ${topBackfill} from our local bench (${Math.round((backfills[0].rel||0)*100)}% reliability) to cover. Will confirm arrival within the hour.`
+ : `Heads-up: ${w.name} (${w.role}) is ${eventLabel}. I'm pulling our nearest available ${w.role} now and will confirm coverage shortly.`;
+
+ return ok({
+ type: "triage",
+ summary: `${w.name} — ${eventLabel}. ${backfills.length} local backfill${backfills.length === 1 ? "" : "s"} ready, draft SMS ready to send.`,
+ worker: { name: w.name, role: w.role, city: w.city, state: w.state, zip: w.zip, rel: w.rel, avail: w.avail, resp: w.resp, archetype: w.archetype, skills: w.skills, certifications: w.certifications, recent_calls: callCount },
+ event,
+ backfills,
+ draft_sms: draftSms,
+ queries_run: queries,
+ duration_ms: Date.now() - start,
+ });
+ }
+ return ok({ type: "triage_miss", summary: `Couldn't find a worker named "${name}" in the roster. Check the spelling or try last name only.`, queries_run: queries, duration_ms: Date.now() - start });
+ }
+
+ // Route 7: bare-name profile lookup. Coordinator types just a
+ // name (or "First Last") with no other intent — pull the
+ // profile, prior fills, and attendance pattern in one shot.
+ // Distinguished from smart_search by being SHORT (≤4 tokens),
+ // capitalized like a name, and not containing role/skill words.
+ const tokens = q.trim().split(/\s+/);
+ const looksLikeName = tokens.length >= 1 && tokens.length <= 4
+ && tokens.every((t) => /^[A-Z][a-z'-]+\.?$/.test(t) || /^[A-Z]\.$/.test(t))
+ && !/forklift|warehouse|electric|welder|assembl|maintain|production|operator|driver|tech|loader|packag|inventory|sanitation/i.test(q);
+ if (looksLikeName) {
+ // Names have middle initials in workers_500k ("Steven A. Allen"),
+ // so a single LIKE '%First Last%' won't match. Split on
+ // whitespace, AND each token — lets "Marcus Rivera" match
+ // "Marcus L. Rivera" without enumerating initials.
+ const nameLike = tokens
+ .map((t) => `name LIKE '%${t.replace(/'/g, "''").replace(/\./g, "")}%'`)
+ .join(" AND ");
+ queries.push(`SQL: lookup name="${q}" via per-token LIKE`);
+ const r = await api("POST", "/query/sql", { sql: `SELECT name, role, city, state, zip, ROUND(CAST(reliability AS DOUBLE),2) rel, ROUND(CAST(availability AS DOUBLE),2) avail, ROUND(CAST(responsiveness AS DOUBLE),2) resp, archetype, skills, certifications FROM workers_500k WHERE ${nameLike} ORDER BY CAST(reliability AS DOUBLE) DESC LIMIT 5` });
+ if (r.rows?.length) {
+ return ok({
+ type: "profile",
+ summary: r.rows.length === 1 ? `${r.rows[0].name} — ${r.rows[0].role}, ${r.rows[0].city}, ${r.rows[0].state}` : `${r.rows.length} workers match "${q}"`,
+ profiles: r.rows,
+ queries_run: queries,
+ duration_ms: Date.now() - start,
+ });
+ }
+ return ok({ type: "profile_miss", summary: `No workers named "${q}" in the roster.`, queries_run: queries, duration_ms: Date.now() - start });
+ }
+
+ // Route 8: temporal — "what came in last night", "new resumes
+ // today", "last 24 hours". Surfaces recent ingest events from
+ // the catalog (created_at on dataset objects) and ranks them
+ // against open job_orders for "likely role match." Schema-
+ // agnostic: any dataset that landed recently shows up.
+ const temporalMatch = lower.match(/\b(last\s+night|today|this\s+morning|past\s+(\d+)\s+(?:hours?|days?)|last\s+(\d+)\s+(?:hours?|days?)|recent|new\s+(?:resumes?|candidates?|workers?|hires?|today)|came\s+in|arrived|just\s+(?:got|came))/i);
+ if (temporalMatch) {
+ // Decide window in hours
+ let windowHours = 24;
+ const pastN = lower.match(/\b(?:past|last)\s+(\d+)\s+(hours?|days?)/);
+ if (pastN) {
+ windowHours = parseInt(pastN[1], 10) * (pastN[2].startsWith("d") ? 24 : 1);
+ } else if (/last\s+night|this\s+morning|today/i.test(lower)) {
+ windowHours = 24;
+ } else if (/recent/i.test(lower)) {
+ windowHours = 72;
+ }
+ queries.push(`Catalog: datasets with created_at within last ${windowHours}h`);
+ const ds = await api("GET", "/catalog/datasets") as any[];
+ const cutoff = Date.now() - windowHours * 3600 * 1000;
+ const recent = (Array.isArray(ds) ? ds : [])
+ .map((d: any) => ({
+ name: d.name,
+ row_count: d.row_count || 0,
+ bytes: (d.objects?.[0]?.size_bytes) || 0,
+ updated_at: d.updated_at,
+ ts: d.updated_at ? Date.parse(d.updated_at) : 0,
+ }))
+ .filter((d) => d.ts >= cutoff && d.row_count > 0)
+ .sort((a, b) => b.ts - a.ts);
+
+ // For each recent dataset, sample its first row's role-shape
+ // text so the coordinator sees what's in it without reading
+ // schemas. If it's a workers/resumes dataset, group by role.
+ const samples: any[] = [];
+ for (const d of recent.slice(0, 8)) {
+ const sample = await api("POST", "/query/sql", { sql: `SELECT * FROM "${d.name.replace(/"/g, '""')}" LIMIT 1` }).catch(() => null);
+ const cols = sample?.columns?.map((c: any) => c.name) || [];
+ const looksLikeWorkers = cols.includes("role") && (cols.includes("name") || cols.includes("candidate_id"));
+ let roleBreakdown: any[] = [];
+ if (looksLikeWorkers) {
+ const byRole = await api("POST", "/query/sql", { sql: `SELECT role, COUNT(*) cnt FROM "${d.name.replace(/"/g, '""')}" GROUP BY role ORDER BY cnt DESC LIMIT 5` }).catch(() => null);
+ roleBreakdown = byRole?.rows || [];
+ }
+ samples.push({
+ name: d.name,
+ row_count: d.row_count,
+ updated_at: d.updated_at,
+ hours_ago: Math.round((Date.now() - d.ts) / 3600000),
+ looks_like_workers: looksLikeWorkers,
+ role_breakdown: roleBreakdown,
+ preview: sample?.rows?.[0] || null,
+ });
+ }
+
+ return ok({
+ type: "ingest_log",
+ summary: recent.length
+ ? `${recent.length} dataset${recent.length === 1 ? "" : "s"} landed in the last ${windowHours}h. ${samples.filter((s) => s.looks_like_workers).reduce((sum, s) => sum + s.row_count, 0)} new worker rows across them.`
+ : `Nothing new in the catalog in the last ${windowHours}h. (Dataset timestamps are based on catalog updated_at; if data was loaded directly to disk without going through /ingest/file, it won't show here.)`,
+ window_hours: windowHours,
+ datasets: samples,
+ queries_run: queries,
+ duration_ms: Date.now() - start,
+ });
+ }
+
// Default: smart search — extract role, location, availability from natural language
{
const filters: string[] = ["CAST(reliability AS DOUBLE) >= 0.5"];
@@ -1705,6 +1862,96 @@ async function main() {
const explicitState = String(b.state || "").trim().toUpperCase();
const explicitRole = String(b.role || "").trim();
+ // (B) Headcount parser — coordinator says "8 production
+ // workers", "I need 12 forklift operators", "5 welders by
+ // Friday". Match a leading or embedded count followed by
+ // a worker-shape noun. Bound at 1..200 — anything outside is
+ // probably not a headcount (zip codes, dates, addresses).
+ let topK = 10;
+ // Allow zero-to-two role words between the number and the
+ // worker-noun: "8 workers" / "8 production workers" /
+ // "8 forklift operators" all match. The role word is
+ // optional so we don't lose the bare-number form.
+ const countMatch = q.match(/\b(\d{1,3})\s+(?:\w+\s+){0,2}(?:workers?|operators?|drivers?|techs?|technicians?|welders?|electricians?|assemblers?|handlers?|loaders?|packagers?|associates?|leads?|people|hires?|staff)\b/i);
+ if (countMatch) {
+ const n = parseInt(countMatch[1], 10);
+ if (n >= 1 && n <= 200) {
+ topK = n;
+ understood.push(`headcount: ${n}`);
+ }
+ }
+
+ // (A) Zip code → city/state lookup. A coordinator types a zip
+ // because that's what the contract says. The previous parser
+ // saw "60607" and treated it as a stray number; results came
+ // back from any state. Map known metro zip prefixes here so
+ // the geographic constraint actually fires.
+ //
+ // Each entry: zip-prefix → { city, state }. Prefix-match
+ // covers a metro without enumerating every zip — e.g. "606"
+ // catches Chicago zips 60600-60699.
+ const zipPrefixMap: Array<[string, { city: string, state: string }]> = [
+ // Chicago + near-suburb
+ ["606", { city: "Chicago", state: "IL" }],
+ ["607", { city: "Chicago", state: "IL" }],
+ ["608", { city: "Chicago", state: "IL" }],
+ // Indianapolis
+ ["462", { city: "Indianapolis", state: "IN" }],
+ ["461", { city: "Indianapolis", state: "IN" }],
+ // Fort Wayne
+ ["468", { city: "Fort Wayne", state: "IN" }],
+ // Columbus OH
+ ["432", { city: "Columbus", state: "OH" }],
+ ["431", { city: "Columbus", state: "OH" }],
+ // Cleveland
+ ["441", { city: "Cleveland", state: "OH" }],
+ // Cincinnati
+ ["452", { city: "Cincinnati", state: "OH" }],
+ ["451", { city: "Cincinnati", state: "OH" }],
+ // Dayton
+ ["454", { city: "Dayton", state: "OH" }],
+ // Milwaukee
+ ["532", { city: "Milwaukee", state: "WI" }],
+ ["531", { city: "Milwaukee", state: "WI" }],
+ // Madison
+ ["537", { city: "Madison", state: "WI" }],
+ // Detroit
+ ["482", { city: "Detroit", state: "MI" }],
+ ["481", { city: "Detroit", state: "MI" }],
+ // Grand Rapids
+ ["495", { city: "Grand Rapids", state: "MI" }],
+ ["493", { city: "Grand Rapids", state: "MI" }],
+ // Minneapolis / St. Paul
+ ["554", { city: "Minneapolis", state: "MN" }],
+ ["551", { city: "Minneapolis", state: "MN" }],
+ // Des Moines
+ ["503", { city: "Des Moines", state: "IA" }],
+ // Kansas City MO
+ ["641", { city: "Kansas City", state: "MO" }],
+ // St. Louis
+ ["631", { city: "St. Louis", state: "MO" }],
+ // Nashville
+ ["372", { city: "Nashville", state: "TN" }],
+ // Memphis
+ ["381", { city: "Memphis", state: "TN" }],
+ // Knoxville
+ ["379", { city: "Knoxville", state: "TN" }],
+ // Louisville
+ ["402", { city: "Louisville", state: "KY" }],
+ // Lexington
+ ["405", { city: "Lexington", state: "KY" }],
+ ];
+ const zipMatch = q.match(/\b(\d{5})\b/);
+ let zipCity: { city: string, state: string } | null = null;
+ if (zipMatch) {
+ const z = zipMatch[1];
+ const hit = zipPrefixMap.find(([prefix]) => z.startsWith(prefix));
+ if (hit) {
+ zipCity = hit[1];
+ understood.push(`zip ${z} → ${hit[1].city}, ${hit[1].state}`);
+ }
+ }
+
// Extract role keywords (skip if dropdown picked one)
const roleKeywords: Record = {
"warehouse": "warehouse", "forklift": "forklift", "welder": "weld", "assembler": "assembl",
@@ -1724,17 +1971,24 @@ async function main() {
}
// Extract city
- const cities = ["chicago","springfield","rockford","peoria","joliet","indianapolis","fort wayne",
- "evansville","south bend","columbus","cleveland","cincinnati","dayton","akron","toledo",
- "st. louis","st louis","kansas city","nashville","memphis","knoxville","louisville","lexington",
- "milwaukee","madison","detroit","grand rapids","lansing","des moines","minneapolis","terre haute",
- "bloomington","decatur","mattoon","galesburg","danville","champaign"];
- for (const city of cities) {
- if (lower.includes(city)) {
- const sqlCity = city.split(' ').map(w => w[0].toUpperCase() + w.slice(1)).join(' ');
- filters.push(`city = '${sqlCity}'`);
- understood.push(`city: ${sqlCity}`);
- break;
+ // Zip code wins over city-name parsing — it's more specific
+ // and the coordinator typed a number, not a casual mention.
+ if (zipCity) {
+ filters.push(`city = '${zipCity.city}'`);
+ understood.push(`city: ${zipCity.city}`);
+ } else {
+ const cities = ["chicago","springfield","rockford","peoria","joliet","indianapolis","fort wayne",
+ "evansville","south bend","columbus","cleveland","cincinnati","dayton","akron","toledo",
+ "st. louis","st louis","kansas city","nashville","memphis","knoxville","louisville","lexington",
+ "milwaukee","madison","detroit","grand rapids","lansing","des moines","minneapolis","terre haute",
+ "bloomington","decatur","mattoon","galesburg","danville","champaign"];
+ for (const city of cities) {
+ if (lower.includes(city)) {
+ const sqlCity = city.split(' ').map(w => w[0].toUpperCase() + w.slice(1)).join(' ');
+ filters.push(`city = '${sqlCity}'`);
+ understood.push(`city: ${sqlCity}`);
+ break;
+ }
}
}
@@ -1785,9 +2039,12 @@ async function main() {
queries.push("SQL filter: " + filterStr);
queries.push("Vector: semantic search for best skill match");
- // Also run a direct SQL query to get exact counts and zip codes
+ // Also run a direct SQL query to get exact counts and zip codes.
+ // LIMIT honors the parsed headcount (capped at 25 to keep the
+ // grid renderable; the staffer can ask for more).
const sqlFields = "name, role, city, state, zip, ROUND(CAST(reliability AS DOUBLE),2) rel, ROUND(CAST(availability AS DOUBLE),2) avail, skills, certifications, archetype";
- const directSql = `SELECT ${sqlFields} FROM workers_500k WHERE ${filterStr} ORDER BY CAST(availability AS DOUBLE) DESC, CAST(reliability AS DOUBLE) DESC LIMIT 10`;
+ const sqlLimit = Math.min(Math.max(topK, 5), 25);
+ const directSql = `SELECT ${sqlFields} FROM workers_500k WHERE ${filterStr} ORDER BY CAST(availability AS DOUBLE) DESC, CAST(reliability AS DOUBLE) DESC LIMIT ${sqlLimit}`;
// Derive role+geo for the pattern query so the meta-index
// surface lines up with what the user actually asked for.
@@ -1798,7 +2055,10 @@ async function main() {
const [searchR, directR, patternR] = await Promise.all([
api("POST", "/vectors/hybrid", {
question: q, index_name: "workers_500k_v1", sql_filter: filterStr,
- filter_dataset: "ethereal_workers", id_column: "worker_id", top_k: 8, generate: false,
+ filter_dataset: "ethereal_workers", id_column: "worker_id",
+ // Honor the parsed headcount (capped at 25 to keep the
+ // vector rerank from re-scoring more rows than render).
+ top_k: Math.min(Math.max(topK, 5), 25), generate: false,
// k=200 to catch compounding — direct measurement shows
// boost reliably fires only when ~all memory is scanned
// due to the narrow 0.55-0.67 cosine band in the 768d
diff --git a/mcp-server/search.html b/mcp-server/search.html
index 1a3c4c1..d507f36 100644
--- a/mcp-server/search.html
+++ b/mcp-server/search.html
@@ -1609,10 +1609,29 @@ function loadLiveContracts(){
var ebLabel=document.createElement('span');ebLabel.style.cssText='font-size:10px;color:#545d68;text-transform:uppercase;letter-spacing:1px';
ebLabel.textContent='PROJECT INDEX — Build Signals';
var ebTags=document.createElement('span');ebTags.style.cssText='color:#e6edf3;font-size:11px;flex:1;font-weight:500';
+ // Contractor names link to the full profile page. Without anchors,
+ // clicking the preview did nothing — the only working contractor
+ // link was inside the lazy-loaded entity brief, which a coordinator
+ // wouldn't reach without first expanding the details.
var preview=[];
+ function contactLink(n){
+ var a=document.createElement('a');
+ a.href='/contractor?name='+encodeURIComponent(n);
+ a.target='_blank';a.rel='noopener';
+ a.style.cssText='color:inherit;text-decoration:none;border-bottom:1px dotted #58a6ff44';
+ a.title='Open full contractor profile';
+ a.textContent=n;
+ a.addEventListener('click',function(e){e.stopPropagation()}); // don't toggle the details
+ return a;
+ }
if(p.contact_1_name) preview.push(p.contact_1_name);
if(p.contact_2_name && p.contact_2_name!==p.contact_1_name) preview.push(p.contact_2_name);
- ebTags.textContent=preview.join(' · ');
+ if(preview.length){
+ preview.forEach(function(n,i){
+ if(i>0) ebTags.appendChild(document.createTextNode(' · '));
+ ebTags.appendChild(contactLink(n));
+ });
+ }
var ebMeta=document.createElement('span');ebMeta.style.cssText='color:#545d68;font-size:10px';
ebMeta.textContent='click → fetch OSHA + ILSOS';
ebSum.appendChild(ebCaret);ebSum.appendChild(ebLabel);ebSum.appendChild(ebTags);ebSum.appendChild(ebMeta);
@@ -2270,6 +2289,130 @@ function pw(text){
rel:rr?parseFloat(rr[1]):0,avail:av?parseFloat(av[1]):0,arch:ar?ar[1]:'',hasM:!!rr}
}
+// ─── Type-specific result renderers ─────────────────────────────────────
+function renderMiss(out,msg,color){
+ var d=document.createElement('div');
+ d.style.cssText='background:#0d1117;border:1px solid '+(color||'#21262d')+'66;border-left:3px solid '+(color||'#21262d')+';border-radius:6px;padding:14px 16px;color:#8b949e;font-size:13px;line-height:1.5';
+ d.textContent=msg;
+ out.appendChild(d);
+}
+function workerLine(w){
+ var bits=[];
+ if(w.role) bits.push(w.role);
+ if(w.city||w.state) bits.push((w.city||'')+(w.city&&w.state?', ':'')+(w.state||''));
+ if(w.zip) bits.push('ZIP '+w.zip);
+ return bits.join(' · ');
+}
+function appendStat(parent,label,val){
+ var s=document.createElement('span');
+ var l=document.createElement('span');l.textContent=label+': ';
+ var b=document.createElement('b');b.style.color='#e6edf3';b.textContent=val;
+ s.appendChild(l);s.appendChild(b);
+ parent.appendChild(s);
+}
+function renderTriage(out,d){
+ var w=d.worker, bf=d.backfills||[];
+ var card=document.createElement('div');
+ card.style.cssText='background:#1a1410;border:1px solid #d29922;border-left:3px solid #d29922;border-radius:8px;padding:16px;margin-bottom:14px';
+ var ev=document.createElement('div');
+ ev.style.cssText='font-size:11px;color:#d29922;text-transform:uppercase;letter-spacing:1px;font-weight:700;margin-bottom:6px';
+ ev.textContent='⚠ TRIAGE — '+(d.event||'event').toUpperCase();
+ card.appendChild(ev);
+ var hdr=document.createElement('div');
+ hdr.style.cssText='font-size:16px;color:#e6edf3;font-weight:600;margin-bottom:4px';
+ hdr.textContent=w.name;
+ card.appendChild(hdr);
+ var line=document.createElement('div');
+ line.style.cssText='font-size:12px;color:#8b949e;margin-bottom:10px';
+ line.textContent=workerLine(w);
+ card.appendChild(line);
+ var stats=document.createElement('div');
+ stats.style.cssText='font-size:11px;color:#8b949e;margin-bottom:10px;display:flex;gap:14px;flex-wrap:wrap';
+ appendStat(stats,'Reliability',Math.round((w.rel||0)*100)+'%');
+ appendStat(stats,'Responsiveness',Math.round((w.resp||0)*100)+'%');
+ appendStat(stats,'Availability',Math.round((w.avail||0)*100)+'%');
+ if(w.archetype) appendStat(stats,'Archetype',w.archetype);
+ if(w.recent_calls!=null) appendStat(stats,'Prior calls',w.recent_calls);
+ card.appendChild(stats);
+ var smsLabel=document.createElement('div');
+ smsLabel.style.cssText='font-size:10px;color:#d29922;text-transform:uppercase;letter-spacing:1px;font-weight:700;margin-bottom:4px';
+ smsLabel.textContent='DRAFT SMS — TO CLIENT';
+ card.appendChild(smsLabel);
+ var smsBox=document.createElement('div');
+ smsBox.style.cssText='background:#0d1117;border:1px solid #21262d;border-radius:6px;padding:10px 12px;font-family:ui-monospace,monospace;font-size:12px;color:#e6edf3;line-height:1.5;white-space:pre-wrap';
+ smsBox.textContent=d.draft_sms||'';
+ card.appendChild(smsBox);
+ var copyBtn=document.createElement('button');
+ copyBtn.style.cssText='margin-top:8px;background:#1f6feb;border:none;color:#fff;padding:6px 14px;border-radius:6px;font-size:12px;font-weight:600;cursor:pointer';
+ copyBtn.textContent='Copy SMS';
+ copyBtn.onclick=function(){
+ if(navigator.clipboard) navigator.clipboard.writeText(d.draft_sms||'');
+ copyBtn.textContent='Copied ✓';
+ setTimeout(function(){copyBtn.textContent='Copy SMS'},1500);
+ };
+ card.appendChild(copyBtn);
+ out.appendChild(card);
+ if(bf.length){
+ var bfHdr=document.createElement('div');
+ bfHdr.style.cssText='font-size:11px;color:#3fb950;text-transform:uppercase;letter-spacing:1px;font-weight:700;margin:8px 0 8px';
+ bfHdr.textContent='✓ BACKFILLS READY — '+bf.length+' local '+(w.role||'workers')+' available, sorted by responsiveness';
+ out.appendChild(bfHdr);
+ bf.forEach(function(c,i){
+ addWorkerInsight(out,c.name,workerLine(c),
+ 'Reliability '+Math.round((c.rel||0)*100)+'% · Responds '+Math.round((c.resp||0)*100)+'% · Available '+Math.round((c.avail||0)*100)+'%'+(c.archetype?' · '+c.archetype:''),
+ i,'#3fb950',c);
+ });
+ }else{
+ var bfNone=document.createElement('div');
+ bfNone.style.cssText='background:#1a1010;border:1px solid #f85149;border-radius:6px;padding:10px 14px;color:#fca5a5;font-size:12px';
+ bfNone.textContent='No same-role workers available locally. Widen the search — try a neighboring city or relax availability threshold.';
+ out.appendChild(bfNone);
+ }
+}
+function renderProfiles(out,d){
+ var hdr=document.createElement('div');
+ hdr.style.cssText='font-size:12px;color:#8b949e;margin-bottom:10px';
+ hdr.textContent=d.summary;
+ out.appendChild(hdr);
+ (d.profiles||[]).forEach(function(w,i){
+ addWorkerInsight(out,w.name,workerLine(w),
+ 'Reliability '+Math.round((w.rel||0)*100)+'%'+(w.resp?' · Responds '+Math.round(w.resp*100)+'%':'')+(w.archetype?' · '+w.archetype:''),
+ i,null,w);
+ });
+}
+function renderIngestLog(out,d){
+ var hdr=document.createElement('div');
+ hdr.style.cssText='font-size:12px;color:#e6edf3;margin-bottom:10px;padding:10px 12px;background:#0d2818;border:1px solid #2ea04340;border-left:3px solid #3fb950;border-radius:6px';
+ hdr.textContent=d.summary;
+ out.appendChild(hdr);
+ (d.datasets||[]).forEach(function(ds){
+ var card=document.createElement('div');
+ card.style.cssText='background:#0d1117;border:1px solid #21262d;border-radius:6px;padding:12px 14px;margin-bottom:8px';
+ var top=document.createElement('div');
+ top.style.cssText='display:flex;justify-content:space-between;align-items:baseline;margin-bottom:6px';
+ var nm=document.createElement('span');
+ nm.style.cssText='font-size:13px;color:#e6edf3;font-weight:600';
+ nm.textContent=ds.name;
+ var ago=document.createElement('span');
+ ago.style.cssText='font-size:11px;color:#545d68';
+ ago.textContent=(ds.hours_ago||0)+'h ago · '+(ds.row_count||0).toLocaleString()+' rows';
+ top.appendChild(nm);top.appendChild(ago);
+ card.appendChild(top);
+ if(ds.looks_like_workers && ds.role_breakdown && ds.role_breakdown.length){
+ var rb=document.createElement('div');
+ rb.style.cssText='font-size:11px;color:#8b949e;display:flex;gap:10px;flex-wrap:wrap;margin-top:4px';
+ ds.role_breakdown.forEach(function(r){
+ var pill=document.createElement('span');
+ pill.style.cssText='background:#161b22;border:1px solid #21262d;padding:2px 8px;border-radius:9px';
+ pill.textContent=(r.role||'?')+' · '+r.cnt;
+ rb.appendChild(pill);
+ });
+ card.appendChild(rb);
+ }
+ out.appendChild(card);
+ });
+}
+
function doSearch(){
var q=document.getElementById('sq').value.trim();if(!q)return;
lastQuery=q;
@@ -2285,6 +2428,14 @@ function doSearch(){
body:JSON.stringify({message:q,state:st||undefined,role:rl||undefined})
}).then(function(r){return r.json()}).then(function(d){
out.textContent='';
+ // Type-specific renderers — added 2026-04-27 for the persona-driven
+ // routes (triage / profile / ingest_log). Default falls through to
+ // the smart_search renderer below.
+ if(d.type==='triage' && d.worker){return renderTriage(out,d)}
+ if(d.type==='triage_miss'){return renderMiss(out,d.summary,'#f85149')}
+ if(d.type==='profile' && d.profiles && d.profiles.length){return renderProfiles(out,d)}
+ if(d.type==='profile_miss'){return renderMiss(out,d.summary,'#d29922')}
+ if(d.type==='ingest_log'){return renderIngestLog(out,d)}
// Show what the system understood
if(d.understood&&d.understood.length){
var tags=document.createElement('div');tags.style.cssText='display:flex;gap:6px;flex-wrap:wrap;margin-bottom:8px';
--
2.47.2
From 5f0beffe808b8604cdd4319118243b37feaf2306 Mon Sep 17 00:00:00 2001
From: root
Date: Mon, 27 Apr 2026 21:16:52 -0500
Subject: [PATCH 03/43] =?UTF-8?q?demo:=20G=20=E2=80=94=20per-staffer=20hot?=
=?UTF-8?q?-swap=20index=20(synthetic=20coordinator=20personas)?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
Same corpus, different relevance gradient per staffer. Three personas
defined in mcp-server/index.ts STAFFERS roster (Maria/IL, Devon/IN,
Aisha/WI), each with a primary state + secondary cities. Server-side:
/intelligence/chat smart_search accepts a staffer_id body field; when
set, defaults state to the staffer's territory and labels the playbook
context as theirs. The playbook patterns query also defaults its geo
to the staffer's primary city/state, so the recurring-skills/cert
breakdowns reflect what they actually fill, not the global IL prior.
Front-end: a staffer selector dropdown beside the existing state/role
filters. Picking a staffer auto-pins state to their territory, shows
a greeting line, relabels the MEMORY panel as MARIA'S/DEVON'S/AISHA'S
MEMORY, and sends staffer_id to chat for scoping.
Dropdown is populated from /staffers (NOT /api/staffers — the generic
/api/* passthrough sends everything under /api/ to the Rust gateway,
which doesn't own the roster). loadStaffers runs at window-load
independently of loadDay's Promise.all so the dropdown populates even
if simulation/SQL inits error out.
Verified end-to-end via playwright. Same q="forklift operators":
no staffer → 509 workers across MI/OH/IA, MEMORY label
as Devon → 89 IN-only (Fort Wayne, Terre Haute), DEVON'S MEMORY
as Aisha → 16 WI-only (Milwaukee, Madison, Green Bay), AISHA'S MEMORY
As Maria with q="8 production workers near 60607":
tags: headcount: 8 · zip 60607 → Chicago, IL · role: production · city: Chicago
20 workers, MARIA'S MEMORY label, top results in Chicago zips
Closes the demo-side build of A-G from the persona plan:
A. zip → city/state, B. headcount, C. bare-name, D. temporal,
E. late-worker triage, F. contractor anchor, G. per-staffer index.
---
mcp-server/index.ts | 91 +++++++++++++++++++++++++++++++++++++++++-
mcp-server/search.html | 56 +++++++++++++++++++++++---
2 files changed, 139 insertions(+), 8 deletions(-)
diff --git a/mcp-server/index.ts b/mcp-server/index.ts
index d7ca82d..71ec866 100644
--- a/mcp-server/index.ts
+++ b/mcp-server/index.ts
@@ -22,6 +22,58 @@ import { buildPermitBrief } from "./entity.js";
const BASE = process.env.LAKEHOUSE_URL || "http://localhost:3100";
const PORT = parseInt(process.env.MCP_PORT || "3700");
+
+// ─── Staffer roster — used by the per-staffer hot-swap index (G). ────
+//
+// J's vision: each staffer has their own molded view of the corpus.
+// When Maria searches, the system surfaces *Maria's* prior fills and
+// her territory's playbooks first. When Aisha searches, the same
+// corpus gets re-shaped to her geo and recent activity. This is what
+// generic CRM fast-search can't do: a relevance gradient that
+// compounds with each staffer's own signal.
+//
+// First implementation is geography-based — each staffer has a primary
+// state and a list of cities they recruit for. Playbook queries get
+// scoped to that territory when staffer_id is provided. As the system
+// accumulates per-staffer signal (call_log assignments, email threads,
+// SMS history), the scope expands beyond geography.
+//
+// Adding a staffer: append to this list. The /api/staffers endpoint
+// exposes the public-safe fields to the UI dropdown.
+const STAFFERS: Array<{
+ id: string;
+ name: string;
+ display: string;
+ territory: { state: string; cities: string[] };
+ greeting: string;
+}> = [
+ {
+ id: "maria",
+ name: "Maria",
+ display: "Maria · Chicago coordinator",
+ territory: { state: "IL", cities: ["Chicago", "Joliet", "Rockford", "Peoria", "Springfield", "Decatur"] },
+ greeting: "Maria's territory: Illinois warehouse + manufacturing fills",
+ },
+ {
+ id: "devon",
+ name: "Devon",
+ display: "Devon · Indiana coordinator",
+ territory: { state: "IN", cities: ["Indianapolis", "Fort Wayne", "South Bend", "Evansville", "Bloomington", "Terre Haute"] },
+ greeting: "Devon's territory: Indiana production + assembly fills",
+ },
+ {
+ id: "aisha",
+ name: "Aisha",
+ display: "Aisha · Wisconsin/Michigan coordinator",
+ territory: { state: "WI", cities: ["Milwaukee", "Madison", "Green Bay", "Detroit", "Grand Rapids", "Lansing"] },
+ greeting: "Aisha's territory: Wisconsin + Michigan logistics",
+ },
+];
+
+function lookupStaffer(id: string | undefined): typeof STAFFERS[number] | null {
+ if (!id) return null;
+ return STAFFERS.find((s) => s.id === id) || null;
+}
const MODE = process.env.MCP_TRANSPORT || "http"; // "stdio" or "http"
// Active trace for the current request — set per-request in the HTTP handler
@@ -824,6 +876,20 @@ async function main() {
}
}
+ // Staffer roster — read by the UI dropdown so each coordinator
+ // can act under their own identity (per-staffer hot-swap index).
+ if (url.pathname === "/api/staffers" || url.pathname === "/staffers") {
+ return ok({
+ staffers: STAFFERS.map((s) => ({
+ id: s.id,
+ name: s.name,
+ display: s.display,
+ territory: s.territory,
+ greeting: s.greeting,
+ })),
+ });
+ }
+
if (url.pathname === "/system/summary") {
const [ds, indexes, workersCount, candsCount] = await Promise.all([
api("GET", "/catalog/datasets").catch(() => [] as any),
@@ -1862,6 +1928,21 @@ async function main() {
const explicitState = String(b.state || "").trim().toUpperCase();
const explicitRole = String(b.role || "").trim();
+ // (G) Per-staffer context. When the UI sends a staffer_id,
+ // playbook queries scope to that staffer's territory — their
+ // recent fills, their geo's recurring patterns. The corpus is
+ // the same for everyone; the relevance gradient is unique to
+ // each staffer because each pulls a different shape from it.
+ const staffer = lookupStaffer(String(b.staffer_id || "").trim());
+ // If the staffer has a territory and the user hasn't already
+ // pinned a state/city via dropdown or NL, default the search
+ // to their territory. They can override by typing a different
+ // city or selecting a different state.
+ if (staffer && !explicitState) {
+ filters.push(`state = '${staffer.territory.state}'`);
+ understood.push(`as ${staffer.name}: ${staffer.territory.state}`);
+ }
+
// (B) Headcount parser — coordinator says "8 production
// workers", "I need 12 forklift operators", "5 welders by
// Friday". Match a leading or embedded count followed by
@@ -2048,9 +2129,14 @@ async function main() {
// Derive role+geo for the pattern query so the meta-index
// surface lines up with what the user actually asked for.
+ // (G) When a staffer is acting, default the geo to their
+ // primary territory — their playbook view is shaped by
+ // where they actually fill, not the global Chicago/IL prior.
const roleForPatterns = understood.find(u => u.startsWith('role:'))?.split(': ')[1] || q;
- const cityForPatterns = understood.find(u => u.startsWith('city:'))?.split(': ')[1] || 'Chicago';
- const stateForPatterns = understood.find(u => u.startsWith('state:'))?.split(': ')[1] || 'IL';
+ const cityForPatterns = understood.find(u => u.startsWith('city:'))?.split(': ')[1]
+ || staffer?.territory.cities[0] || 'Chicago';
+ const stateForPatterns = understood.find(u => u.startsWith('state:'))?.split(': ')[1]
+ || staffer?.territory.state || 'IL';
const [searchR, directR, patternR] = await Promise.all([
api("POST", "/vectors/hybrid", {
@@ -2084,6 +2170,7 @@ async function main() {
return ok({
type: "smart_search",
summary: `Found ${searchR.sql_matches || 0} workers matching your criteria${understood.length ? ' (' + understood.join(', ') + ')' : ''}`,
+ staffer: staffer ? { id: staffer.id, name: staffer.name, display: staffer.display, territory: staffer.territory } : null,
understood,
sql_results: sqlWorkers,
vector_results: vectorWorkers,
diff --git a/mcp-server/search.html b/mcp-server/search.html
index d507f36..28edaa9 100644
--- a/mcp-server/search.html
+++ b/mcp-server/search.html
@@ -302,11 +302,18 @@ body{font-family:'Inter',-apple-system,system-ui,'Segoe UI',sans-serif;backgroun
-
+
+
+
+
@@ -363,7 +370,38 @@ var P=location.pathname.indexOf('/lakehouse')>=0?'/lakehouse':'';
var A=location.origin+P;
var AC=['#1a2744','#1a3a2a','#2a1a3a','#3a2a1a','#1a3a3a','#2a2a1a'];
var lastQuery='';
-window.addEventListener('load',function(){loadSystemSummary();loadLegacyBridge();loadDay();loadStaffingForecast();loadLiveContracts();loadMarket();loadLearning();loadWorkerSearchSamples();loadArchSignals()});
+window.addEventListener('load',function(){loadSystemSummary();loadLegacyBridge();loadDay();loadStaffingForecast();loadLiveContracts();loadMarket();loadLearning();loadWorkerSearchSamples();loadArchSignals();loadStaffers()});
+
+// Per-staffer hot-swap dropdown — runs independently of the simulation
+// fetch so the staffer selector populates even if any other init step
+// errors out. /api/staffers returns the synthetic coordinator roster.
+function loadStaffers(){
+ var sel=document.getElementById('sstaffer');
+ var greeting=document.getElementById('sstaffer-greeting');
+ if(!sel) return;
+ // /staffers (not /api/staffers) — the /api/* generic passthrough
+ // forwards anything under /api/ to the Rust gateway on :3100 and the
+ // gateway doesn't know the staffer roster (it lives in the mcp-server
+ // module). The bare /staffers route serves directly.
+ fetch(A+'/staffers').then(function(r){return r.json()}).then(function(d){
+ (d.staffers||[]).forEach(function(s){
+ var o=document.createElement('option');o.value=s.id;o.textContent=s.display||s.name;
+ sel.appendChild(o);
+ });
+ sel._roster=d.staffers||[];
+ }).catch(function(){});
+ sel.addEventListener('change',function(){
+ var roster=sel._roster||[];
+ var s=roster.find(function(x){return x.id===sel.value});
+ if(s){
+ greeting.textContent='Acting as '+s.name+' — '+(s.greeting||'')+' · territory: '+s.territory.cities.slice(0,3).join(', ')+'…';
+ var stSel=document.getElementById('sst');
+ if(stSel && !stSel.value){stSel.value=s.territory.state}
+ }else{
+ greeting.textContent='';
+ }
+ });
+}
// Deep-link: visiting the dashboard with #open-briefs in the URL auto-
// expands every Entity Brief panel once the contract cards finish
@@ -2417,15 +2455,17 @@ function doSearch(){
var q=document.getElementById('sq').value.trim();if(!q)return;
lastQuery=q;
var st=document.getElementById('sst').value,rl=document.getElementById('srl').value;
+ var stafferEl=document.getElementById('sstaffer');
+ var stafferId=stafferEl?stafferEl.value:'';
// Pass dropdown filters as structured fields. Old code appended
// ' in '+st to the message, which the server misparsed: the
// preposition "in" matched the regex for state code "IN" (Indiana)
// and every search returned Indiana workers regardless of dropdown.
- // Sending structured state/role lets the server skip NL parsing
- // for those fields entirely.
+ // Sending structured state/role + staffer_id lets the server skip
+ // NL parsing for those fields and apply per-staffer scoping.
var out=document.getElementById('sresults');out.textContent='Finding the best matches...';
fetch(A+'/intelligence/chat',{method:'POST',headers:{'Content-Type':'application/json'},
- body:JSON.stringify({message:q,state:st||undefined,role:rl||undefined})
+ body:JSON.stringify({message:q,state:st||undefined,role:rl||undefined,staffer_id:stafferId||undefined})
}).then(function(r){return r.json()}).then(function(d){
out.textContent='';
// Type-specific renderers — added 2026-04-27 for the persona-driven
@@ -2457,7 +2497,11 @@ function doSearch(){
var mem=document.createElement('div');
mem.style.cssText='background:#0d2818;border:1px solid #2ea04360;border-radius:6px;padding:8px 12px;margin-bottom:10px;font-size:11px;color:#86efac;line-height:1.5';
var label=document.createElement('span');label.style.cssText='color:#3fb950;font-weight:600;margin-right:6px';
- label.textContent='MEMORY ('+(d.pattern_playbooks_matched||0)+' playbook'+(d.pattern_playbooks_matched===1?'':'s')+'):';
+ // When a staffer is acting, label the panel with their name —
+ // "MARIA'S MEMORY (12 playbooks)" makes the per-user shaping
+ // visible in the UI, not just the response data.
+ var memOwner=d.staffer&&d.staffer.name?d.staffer.name.toUpperCase()+"'S MEMORY":'MEMORY';
+ label.textContent=memOwner+' ('+(d.pattern_playbooks_matched||0)+' playbook'+(d.pattern_playbooks_matched===1?'':'s')+'):';
mem.appendChild(label);
var pattern = d.discovered_pattern || '';
if(!pattern || pattern.indexOf('No similar')>=0 || pattern.indexOf('0 workers')>=0){
--
2.47.2
From a1066db87b390bf9faf19cecd7ff1900832ffe75 Mon Sep 17 00:00:00 2001
From: root
Date: Mon, 27 Apr 2026 21:28:45 -0500
Subject: [PATCH 04/43] =?UTF-8?q?demo:=20contractor=20profile=20=E2=80=94?=
=?UTF-8?q?=20heat=20map,=20project=20index,=2012=20awaiting=20sources?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
The contractor.html click-target J asked for: a separate page (not a
modal, not a fall-through search) showing every angle on a contractor.
Reachable from the Co-Pilot dashboard, the staffers console, and the
search box — all anchor-wrap contractor names to /contractor?name=...
What's new on the page:
1. PROJECT INDEX — build-signal score
Single 0-100 number with the drivers laid out beneath. Driver list
is staffer-readable: "59 Chicago permits in 180d (+30) · OSHA 20
inspections (-25) · federal contractor (+15)". Score weights are
placeholders to be replaced by an ML model once the 12 awaiting
sources ship — the current 6 wired signals would not give a real
model enough features.
2. HEAT MAP — every Chicago permit they've been contact_1 or contact_2
on, last 24 months, plotted on a leaflet dark map. Color by cost
(green <$100K, amber $100K-$1M, red ≥$1M), radius proportional to
cost so the staffer sees where money + activity concentrates. Click
a marker for permit detail (cost, date, work type, address, permit
ID). All 50 of Turner Construction's geocoded recent permits in
Chicago plot end-to-end.
3. ACTIVITY TIMELINE — monthly permit count, bar chart, with the
first/last month labels so the staffer sees momentum. Tooltip on
each bar gives the count and total cost for that month.
4. 12 AWAITING SOURCES — placeholder cards for the public datasets
that would 3× the build-signal feature count. Each card has:
- source name (real, e.g. DOL Wage & Hour, EPA ECHO, MSHA, BBB)
- one-liner in coordinator language ("Has this contractor stiffed
workers? Will they pay our staffing invoices?")
- "Would show:" sample shape so the engineering scope is concrete
Order is staffing-decision relevance:
1. DOL Wage & Hour (WHD violations)
2. State Licensure Boards (active license + expiry)
3. Surety Bond Capacity (bonding ceiling)
4. EPA ECHO Compliance (env violations at sites)
5. DOT/FMCSA Carrier Safety (crash + OOS rates)
6. BBB Complaints + Rating
7. PACER Civil Suits (FLSA / Title VII / ADA)
8. UCC Lien Filings (cash flow distress)
9. D&B / Credit Bureau (PAYDEX, payment behavior)
10. State UI Employer Claims (workforce stability)
11. MSHA Mine Safety (excavation / aggregate / heavy)
12. Registered Apprenticeships (DOL RAPIDS pipeline)
Server-side: entity.ts fetchContractorHistory now pulls the 50 most
recent permits with id + lat/lng + work_description, so the heat map
and timeline have what they need without a second SQL hop. The
ContractorHistory.recent_permits type gained the optional fields.
Front-end: contractor.html got 4 new render sections, leaflet wiring
(stylesheet + script in head), placeholder grid CSS, and a PLACEHOLDERS
const at the bottom with the 12 sources. All popup HTML is built via
DOM construction (textContent + appendChild) — no innerHTML, no XSS.
console.html: contractor names from /intelligence/permit_contracts now
anchor-wrapped to /contractor?name=... so the click-through J described
works from the staffers console too. Click stops propagation so the
permit details element doesn't toggle on the same click.
Verified end-to-end via playwright — Turner Construction profile shows:
PIX score "Mixed signals — review drivers below"
Heat map: "50 permits plotted · green/amber/red"
4 section labels in order
12 placeholder cards in the documented order
---
mcp-server/console.html | 24 +
mcp-server/contractor.html | 597 ++++++++
mcp-server/entity.ts | 2781 ++++++++++++++++++++++++++++++++++++
3 files changed, 3402 insertions(+)
create mode 100644 mcp-server/contractor.html
create mode 100644 mcp-server/entity.ts
diff --git a/mcp-server/console.html b/mcp-server/console.html
index eada43c..56ca178 100644
--- a/mcp-server/console.html
+++ b/mcp-server/console.html
@@ -306,6 +306,30 @@ function loadChapter4(){
addr.style.cssText='color:#8b949e;font-size:12px;margin-top:2px';
card.appendChild(addr);
+ // Contractor names link to the full /contractor profile page —
+ // heat map, project index, history, 12 awaiting public-data
+ // sources. The staffer click-through J asked for.
+ if(p.contact_1_name || p.contact_2_name){
+ var contractors=document.createElement('div');
+ contractors.style.cssText='color:#8b949e;font-size:12px;margin-top:4px';
+ contractors.appendChild(document.createTextNode('Contractors: '));
+ var seen=[];
+ [p.contact_1_name, p.contact_2_name].forEach(function(n,i){
+ if(!n || seen.indexOf(n)>=0) return;
+ seen.push(n);
+ if(seen.length>1) contractors.appendChild(document.createTextNode(' · '));
+ var a=document.createElement('a');
+ a.href='/contractor?name='+encodeURIComponent(n);
+ a.target='_blank';
+ a.rel='noopener';
+ a.style.cssText='color:#58a6ff;text-decoration:none;border-bottom:1px dotted #58a6ff44';
+ a.title='Open full contractor profile';
+ a.textContent=n;
+ contractors.appendChild(a);
+ });
+ card.appendChild(contractors);
+ }
+
card.appendChild(el('div','step-label','STEP 1 · Derive staffing need'));
var s1=el('div','step-body');
s1.appendChild(document.createTextNode('Industry heuristic: ~1 worker per $150K of permit cost, capped 2-8. Resulting contract: '));
diff --git a/mcp-server/contractor.html b/mcp-server/contractor.html
new file mode 100644
index 0000000..85ad7f8
--- /dev/null
+++ b/mcp-server/contractor.html
@@ -0,0 +1,597 @@
+
+
+
+Contractor Profile · Staffing Co-Pilot
+
+
+
+
+