From b843a23433cdcb72f505abbf6e4301c8e446321e Mon Sep 17 00:00:00 2001 From: root Date: Sat, 25 Apr 2026 17:07:23 -0500 Subject: [PATCH] mcp: contractor entity-brief drill-down + mobile UX pass MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Adds /contractor page route plus /intelligence/contractor_profile endpoint that fans out across OSHA, ticker, history, parent_link, federal contracts, debarment, NLRB, ILSOS, news, diversity certs, BLS macro — single per-contractor portfolio view across every wired source. search.html: mobile responsive layout, fixed bottom dock with horizontal scroll-snap, legacy bridge row stacking, viewport overflow guards. Co-Authored-By: Claude Opus 4.7 (1M context) --- mcp-server/index.ts | 121 +++- mcp-server/search.html | 1211 +++++++++++++++++++++++++++++++++++++--- 2 files changed, 1256 insertions(+), 76 deletions(-) diff --git a/mcp-server/index.ts b/mcp-server/index.ts index 9dd1533..de804fa 100644 --- a/mcp-server/index.ts +++ b/mcp-server/index.ts @@ -18,6 +18,7 @@ import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js" import { SSEServerTransport } from "@modelcontextprotocol/sdk/server/sse.js"; import { z } from "zod"; import { startTrace, logSpan, logGeneration, scoreTrace, flush as flushTraces } from "./tracing.js"; +import { buildPermitBrief } from "./entity.js"; const BASE = process.env.LAKEHOUSE_URL || "http://localhost:3100"; const PORT = parseInt(process.env.MCP_PORT || "3700"); @@ -960,6 +961,61 @@ async function main() { return new Response(Bun.file(import.meta.dir + "/console.html")); } + // ─── Contractor / entity drill-down page ─── + // Single-contractor portfolio view across every wired source: + // OSHA national, Chicago history, ticker chart, parent link, + // federal contracts, debarment, unions, training. Click any + // contractor name in a permit Entity Brief to land here. + if (url.pathname === "/contractor") { + return new Response(Bun.file(import.meta.dir + "/contractor.html"), { + headers: { ...cors, "Content-Type": "text/html" }, + }); + } + if (url.pathname === "/intelligence/contractor_profile" && req.method === "POST") { + const start = Date.now(); + try { + const b = (await req.json().catch(() => ({}))) as { name?: string }; + if (!b.name) return err("missing name", 400); + // Use the entity-brief library directly — single entity, all sources. + const { fetchOshaBrief, fetchTickerBrief, fetchContractorHistory, fetchParentLink, fetchFederalContracts, fetchDebarmentBrief, fetchNlrbBriefReal, fetchIlsosBrief, fetchNewsMentions, fetchDiversityCerts, scoreNewsSentiment, fetchBlsConstructionTrend, normalizeEntityName, entityTicker } = await import("./entity.js"); + const [osha, stock, history, parent_link, federal, debarment, nlrb, ilsos, news, diversity, macro] = await Promise.all([ + fetchOshaBrief(b.name), + fetchTickerBrief(b.name), + fetchContractorHistory(b.name), + fetchParentLink(b.name), + fetchFederalContracts(b.name), + fetchDebarmentBrief(b.name), + fetchNlrbBriefReal(b.name), + fetchIlsosBrief(b.name), + fetchNewsMentions(b.name), + fetchDiversityCerts(b.name), + fetchBlsConstructionTrend(), + ]); + const news_sentiment = news ? scoreNewsSentiment(news) : null; + return ok({ + key: normalizeEntityName(b.name), + display_name: b.name, + ticker: entityTicker(b.name), + osha, + stock, + history, + parent_link, + federal, + debarment, + nlrb, + ilsos, + news, + news_sentiment, + diversity, + macro, + generated_at: new Date().toISOString(), + duration_ms: Date.now() - start, + }); + } catch (e: any) { + return err(`contractor_profile: ${e.message}`, 500); + } + } + // Intelligence: Market data — public building permits → staffing demand forecast if (url.pathname === "/intelligence/market" && req.method === "POST") { const start = Date.now(); @@ -1232,8 +1288,12 @@ async function main() { const permitUrl = "https://data.cityofchicago.org/resource/ydr8-5enu.json"; // Recent + substantial permits only — skip tiny ones that // don't imply real staffing demand. + // Include contact_1 + contact_2 fields so the Entity Brief + // panel on each card can populate without a second fetch. + // Contacts identify the applicant / contractor by name — + // those are the keys we pass to OSHA/ILSOS enrichment. const permits: any[] = await fetch( - `${permitUrl}?$select=permit_type,work_type,work_description,reported_cost,street_number,street_direction,street_name,community_area,issue_date&` + `${permitUrl}?$select=id,permit_type,work_type,work_description,reported_cost,street_number,street_direction,street_name,community_area,issue_date,contact_1_name,contact_1_type,contact_2_name,contact_2_type&` + `$where=reported_cost>250000 AND issue_date>'2025-06-01'` + `&$order=issue_date DESC&$limit=6` ).then(r => r.json()).catch(() => []); @@ -1367,12 +1427,19 @@ async function main() { contracts.push({ permit: { + id: p.id, cost, work_type: p.work_type || "General construction", description: (p.work_description || "").substring(0, 140), address: `${p.street_number || ""} ${p.street_direction || ""} ${p.street_name || ""}`.trim(), community_area: p.community_area, issue_date: (p.issue_date || "").substring(0, 10), + // Contacts — used by /intelligence/permit_entities to + // enrich each card with OSHA + ILSOS on expand. + contact_1_name: p.contact_1_name || "", + contact_1_type: p.contact_1_type || "", + contact_2_name: p.contact_2_name || "", + contact_2_type: p.contact_2_type || "", }, implied_bill_rate: contractBillRate, timeline: { @@ -1426,6 +1493,58 @@ async function main() { } } + // Intelligence: per-permit entity brief — OSHA + ILSOS + property + // Takes a permit identifier (we look it up from Chicago Socrata) or + // raw contact fields directly from the client. Returns an "ETF + // basket" shape: property + entities + per-entity risk factors. + // OSHA is live-scraped (cached 30d). ILSOS returns a structured + // placeholder because apps.ilsos.gov blocks our ASN. + if (url.pathname === "/intelligence/permit_entities" && req.method === "POST") { + const start = Date.now(); + try { + const b = await req.json().catch(() => ({})) as { + permit_id?: string; + address?: string; + work_type?: string; + contact_1_name?: string; + contact_1_type?: string; + contact_2_name?: string; + contact_2_type?: string; + fetch_osha?: boolean; + fetch_ilsos?: boolean; + }; + // If the caller didn't pass contact fields but did pass a + // permit_id, go pull the record from Chicago Socrata. + let permit = b; + if (b.permit_id && !b.contact_1_name) { + const u = `https://data.cityofchicago.org/resource/ydr8-5enu.json?$where=id='${encodeURIComponent(b.permit_id)}'`; + const rows = (await fetch(u).then((r) => r.json())) as any[]; + const p = rows?.[0]; + if (p) { + const addr = [p.street_number, p.street_direction, p.street_name] + .filter(Boolean) + .join(" "); + permit = { + permit_id: b.permit_id, + address: addr, + work_type: p.work_type, + contact_1_name: p.contact_1_name, + contact_1_type: p.contact_1_type, + contact_2_name: p.contact_2_name, + contact_2_type: p.contact_2_type, + }; + } + } + const brief = await buildPermitBrief(permit, { + fetchOsha: b.fetch_osha !== false, + fetchIlsos: b.fetch_ilsos !== false, + }); + return ok({ ...brief, duration_ms: Date.now() - start }); + } catch (e: any) { + return err(`permit_entities: ${e.message}`, 500); + } + } + // Removed 2026-04-20: /intelligence/learn was a legacy CSV writer // that destructively re-wrote successful_playbooks. /log and // /log_failure replace it cleanly via /vectors/playbook_memory/seed diff --git a/mcp-server/search.html b/mcp-server/search.html index 7646073..7a15c1c 100644 --- a/mcp-server/search.html +++ b/mcp-server/search.html @@ -6,6 +6,7 @@
@@ -114,24 +212,47 @@ body{font-family:'Inter',-apple-system,system-ui,'Segoe UI',sans-serif;backgroun
+ + + + + + + + + +
+ + ⓪ Not a CRM — an index that learns from you + loading growth numbers… + ▾ click to collapse + +

+ If you've worked on a legacy staffing CRM, your mental model is field inventory — + every concept must be a visible column, dropdown, or checkbox, or it doesn't exist. This + system works the opposite way: concepts don't need to be pre-declared because the + hybrid index + playbook memory learns them when you work a contract. + The rows below translate the familiar legacy surface into what actually happens here, + with real numbers for every claim. +

+
+
+
Analyzing contracts and workers...
- -
+ +
- Substrate Signals — Architecture Live - + ① Live Market — Chicago right now +
-
Probing substrate…
-
- - -
-
- 24/7 Shift Coverage - -
-
Loading shift distribution…
+

+ The clock shows where we sit in the 24-hour cycle. Colored arcs mark the 4 standard + staffing shifts; the red needle is now. The panel beside it summarizes what Chicago's + public permit system is asking for right now — staffing demand before anyone's acted + on it. This is the real world the rest of the page is reacting to. +

+
Loading live market…
@@ -142,11 +263,20 @@ body{font-family:'Inter',-apple-system,system-ui,'Segoe UI',sans-serif;backgroun
Loading forecast...
+
- Live Contracts — Chicago Permits → Proposed Fills + ② Staffer's Console — what's on your plate
+

+ This is what a recruiter or coordinator sees when they open the console. Each card is + one open permit ranked against our 500K worker bench. The fill-probability bar + shows cumulative chance of filling by day; the economics panel projects + gross revenue, margin, and payout window; the over-bill pool flags workers + whose pay exceeds the contract's bill rate — they go into a margin-watch bucket instead + of being rejected outright. +

Loading live contracts...
@@ -158,34 +288,228 @@ body{font-family:'Inter',-apple-system,system-ui,'Segoe UI',sans-serif;backgroun
+ +
+
+ ③ Worker Search — find someone specific + +
+

+ Type a plain-English description — role, location, trait, certification. + The query hits the hybrid SQL + vector index over all 500K worker profiles + and ranks by semantic match, reliability, and availability. Try one of the + sample searches below or write your own. +

+
+ +
+ + + +
+
+
+ +
- System Activity - + ④ System Activity — how the substrate learns +
+

+ Every completed fill, every accepted playbook, every rejected candidate feeds + back into the substrate. This strip shows what the system has learned since + the last run — which patterns are compounding, which memories are fresh, + which indices are being exercised. If it's empty, the system hasn't seen + enough traffic yet to form a memory worth showing. +

-
+ +
- Worker Search - + ⑤ Substrate Signals — architecture health + +
+

+ These tiles measure the architecture itself, not the staffing workload. + Instant-search latency, index shape, playbook-memory depth, pathway-matrix + compounding — four probes that answer "is the substrate healthy right now?" +

+
+
Probing substrate…
-
Search all workers
- -
-
-
+ + + + + + +