demo: profiler index — ticker associations (direct, parent, co-permit)

J's framing: "if a contractor works for Target, future Target contracts
mean money flows back to the contractor — the ticker is an associated
indicator." Now the profiler index attaches three flavors of ticker per
contractor and renders them as colored pills:

  green DIRECT    contractor IS the public issuer (Target Corp → TGT)
  amber PARENT    contractor is a subsidiary of a public parent
                    (Turner Construction → HOC.DE via Hochtief AG)
  blue  ASSOCIATED contractor co-appears on permits with a public
                    entity (TORNOW, KYLE F → TGT, 3 shared permits with
                    TARGET CORPORATION)

The associated flavor is the correlation signal J described — it pulls
the ticker for whoever the contractor has been working *with*, not
just what they are themselves. Most contractors are private; the
associated link is how the moat shows up.

Server-side:
- entity.ts new export `lookupTickerLite(name)` — cheap in-memory
  resolver that does only the SEC tickers index lookup + curated
  KNOWN_PARENT_MAP check, no per-call SEC profile or Stooq fetch.
  ~10ms per name after the index is loaded once.
- /intelligence/profiler_index now runs a third Socrata pull
  (5K permit pairs in window) to build a co-occurrence map. For each
  contractor in the result, attaches:
    .tickers.direct[]      — name matches a public issuer
    .tickers.associated[]  — top 5 co-permit partners that resolve
                              to a ticker, with partner_name +
                              co_permits count + partner_via reason

Front-end:
- mcp-server/profiler.html — new .ticker-pill styles (3 colors per
  attribution kind), pills render under the contractor name in the
  table. Hover title gives the full reason path.

Verified end-to-end on the public URL:
  search="tornow" → blue TGT pill, hint "Associated via co-permits
                    with TARGET CORPORATION (3 shared permits) —
                    TARGET CORP"
  search="target" → green TGT × 2 (TARGET CORPORATION +
                    CORPORATION TARGET name variants both resolve
                    direct to the same issuer)
  default top 200 → 15 ticker pills surface across the page including
                    JPM (via JPMORGAN CHASE BANK co-permits) and
                    parent-link tickers for the construction majors.
This commit is contained in:
root 2026-04-27 22:08:24 -05:00
parent f6a7621b2d
commit ba41ad2846
3 changed files with 156 additions and 4 deletions

View File

@ -1140,6 +1140,46 @@ async function fetchStooqQuote(ticker: string): Promise<{
} }
} }
// Cheap name-to-ticker lookup for batch use (e.g. profiler index over
// 200 contractors). Hits the in-memory SEC tickers index and the
// KNOWN_PARENT_MAP. No SEC profile fetch, no Stooq fetch. Returns
// every ticker we can attribute to a name, with a tag describing the
// link (direct = name matches a public issuer, parent = the company is
// a subsidiary with a public parent in our curated map).
export type LiteTicker = {
ticker: string;
via: "direct" | "parent" | "exact";
matched_name?: string;
exchange?: string;
reason?: string;
};
export async function lookupTickerLite(name: string): Promise<LiteTicker[]> {
const out: LiteTicker[] = [];
// Direct SEC match (in-memory)
const direct = await resolveSecTicker(name);
if (direct) {
out.push({
ticker: direct.ticker,
via: normalizeEntityName(direct.title) === normalizeEntityName(name) ? "exact" : "direct",
matched_name: direct.title,
});
}
// Parent map (curated)
const key = normalizeEntityName(name);
for (const [k, v] of Object.entries(KNOWN_PARENT_MAP)) {
if (key && key.includes(normalizeEntityName(k)) && v.status === "ok" && v.parent_ticker) {
out.push({
ticker: v.parent_ticker,
via: "parent",
matched_name: v.parent_name,
exchange: v.parent_exchange,
reason: v.link_source,
});
}
}
return out;
}
export async function fetchTickerBrief(name: string): Promise<TickerBrief> { export async function fetchTickerBrief(name: string): Promise<TickerBrief> {
const now = new Date().toISOString(); const now = new Date().toISOString();
const cached = await cacheGet(normalizeEntityName(name), "ticker"); const cached = await cacheGet(normalizeEntityName(name), "ticker");

View File

@ -908,10 +908,30 @@ async function main() {
+ `$where=${encodeURIComponent(where.join(" AND "))}` + `$where=${encodeURIComponent(where.join(" AND "))}`
+ `&$group=${col}&$order=total_cost DESC&$limit=${limit}`; + `&$group=${col}&$order=total_cost DESC&$limit=${limit}`;
}; };
// Co-occurrence query — pulls the contact_1+contact_2 pairs in
// the window so we can attribute tickers across associations
// ("Bob's Electric works for Target → show TGT"). Capped 5K
// permits, ~200ms cold; resolved tickers are in-memory after.
const buildCoQuery = () => {
const where = [
`reported_cost>${minCost}`,
`issue_date>'${sinceDate.replace(/'/g, "")}'`,
"contact_1_name IS NOT NULL",
"contact_2_name IS NOT NULL",
];
if (search) {
const s = search.replace(/'/g, "''").toUpperCase();
where.push(`(upper(contact_1_name) like '%${s}%' OR upper(contact_2_name) like '%${s}%')`);
}
return `${permitUrl}?$select=contact_1_name,contact_2_name`
+ `&$where=${encodeURIComponent(where.join(" AND "))}`
+ `&$limit=5000`;
};
try { try {
const [byC1, byC2] = await Promise.all([ const [byC1, byC2, coRows] = await Promise.all([
fetch(buildQuery("contact_1_name")).then((r) => r.json()).catch(() => []), fetch(buildQuery("contact_1_name")).then((r) => r.json()).catch(() => []),
fetch(buildQuery("contact_2_name")).then((r) => r.json()).catch(() => []), fetch(buildQuery("contact_2_name")).then((r) => r.json()).catch(() => []),
fetch(buildCoQuery()).then((r) => r.json()).catch(() => []),
]); ]);
const merged: Record<string, { name: string; permits: number; total_cost: number; last_filed: string; roles: Set<string> }> = {}; const merged: Record<string, { name: string; permits: number; total_cost: number; last_filed: string; roles: Set<string> }> = {};
const consume = (rows: any[], role: string) => { const consume = (rows: any[], role: string) => {
@ -931,16 +951,78 @@ async function main() {
}; };
consume(byC1, "applicant"); consume(byC1, "applicant");
consume(byC2, "contractor"); consume(byC2, "contractor");
const rows = Object.values(merged)
// Build co-occurrence map from the permit pairs. For each
// contractor key, count how many permits they co-appeared
// on with each other party.
const coMap: Record<string, Record<string, number>> = {};
for (const r of (Array.isArray(coRows) ? coRows : []) as any[]) {
const a = String(r.contact_1_name || "").trim();
const b = String(r.contact_2_name || "").trim();
if (!a || !b) continue;
const ka = a.toUpperCase();
const kb = b.toUpperCase();
if (ka === kb) continue;
(coMap[ka] = coMap[ka] || {})[kb] = (coMap[ka][kb] || 0) + 1;
(coMap[kb] = coMap[kb] || {})[ka] = (coMap[kb][ka] || 0) + 1;
}
// Attach tickers per contractor — direct, parent, and any
// tickers attributable to top co-occurring partners ("works
// with TARGET CORPORATION → TGT shown as associated"). Resolves
// via the in-memory SEC tickers index + curated parent map,
// so the cost is per-name index-lookup, not a network call.
const { lookupTickerLite } = await import("./entity.js");
// Memoize per-name to skip duplicate lookups across the
// associated step.
const tickerCache: Record<string, any[]> = {};
const lookupCached = async (n: string) => {
const k = n.toUpperCase();
if (tickerCache[k]) return tickerCache[k];
tickerCache[k] = await lookupTickerLite(n);
return tickerCache[k];
};
const rowsBase = Object.values(merged)
.map((r) => ({ ...r, roles: Array.from(r.roles) })) .map((r) => ({ ...r, roles: Array.from(r.roles) }))
.sort((a, b) => b.total_cost - a.total_cost) .sort((a, b) => b.total_cost - a.total_cost)
.slice(0, limit); .slice(0, limit);
// Resolve tickers concurrently (in-memory ops, but Promise.all
// keeps the code uniform with future remote ticker sources).
const enriched = await Promise.all(rowsBase.map(async (r) => {
const direct = await lookupCached(r.name);
const partners = Object.entries(coMap[r.name.toUpperCase()] || {})
.sort((a, b) => b[1] - a[1])
.slice(0, 6);
const associated: any[] = [];
const seen = new Set(direct.map((t: any) => t.ticker));
for (const [partnerName, occurrences] of partners) {
const ts = await lookupCached(partnerName);
for (const t of ts) {
if (seen.has(t.ticker)) continue;
seen.add(t.ticker);
associated.push({
ticker: t.ticker,
via: "associated",
partner_name: partnerName,
co_permits: occurrences,
partner_via: t.via,
matched_name: t.matched_name,
});
if (associated.length >= 5) break;
}
if (associated.length >= 5) break;
}
return { ...r, tickers: { direct, associated } };
}));
return ok({ return ok({
count: rows.length, count: enriched.length,
since: sinceDate, since: sinceDate,
min_cost: minCost, min_cost: minCost,
search, search,
contractors: rows, contractors: enriched,
duration_ms: Date.now() - start, duration_ms: Date.now() - start,
}); });
} catch (e: any) { } catch (e: any) {

View File

@ -30,6 +30,12 @@ td.name a:hover{text-decoration:underline}
td.right{text-align:right;font-family:ui-monospace,monospace;font-variant-numeric:tabular-nums} td.right{text-align:right;font-family:ui-monospace,monospace;font-variant-numeric:tabular-nums}
td.role{font-size:10px;color:#8b949e} td.role{font-size:10px;color:#8b949e}
td.role .pill{display:inline-block;padding:2px 7px;border-radius:9px;font-size:9px;font-weight:600;background:#161b22;border:1px solid #21262d;color:#8b949e;margin-right:4px;text-transform:uppercase;letter-spacing:0.5px} td.role .pill{display:inline-block;padding:2px 7px;border-radius:9px;font-size:9px;font-weight:600;background:#161b22;border:1px solid #21262d;color:#8b949e;margin-right:4px;text-transform:uppercase;letter-spacing:0.5px}
.tickers{display:flex;gap:4px;flex-wrap:wrap;margin-top:3px}
.ticker-pill{display:inline-block;padding:1px 7px;border-radius:5px;font-size:10px;font-weight:700;font-family:ui-monospace,SFMono-Regular,monospace;letter-spacing:0.3px;cursor:help}
.ticker-pill.direct{background:#0d2818;border:1px solid #2ea04388;color:#3fb950}
.ticker-pill.parent{background:#1a1410;border:1px solid #d2992288;color:#d29922}
.ticker-pill.associated{background:#0d1830;border:1px solid #58a6ff66;color:#58a6ff}
.ticker-pill.exact{background:#0d2818;border:1px solid #2ea043;color:#3fb950}
.cost-band-1{color:#3fb950} .cost-band-1{color:#3fb950}
.cost-band-2{color:#d29922} .cost-band-2{color:#d29922}
.cost-band-3{color:#f85149} .cost-band-3{color:#f85149}
@ -181,6 +187,30 @@ function render(){
a.target='_blank'; a.rel='noopener'; a.target='_blank'; a.rel='noopener';
a.textContent = r.name; a.textContent = r.name;
ntd.appendChild(a); ntd.appendChild(a);
// Ticker association pills — direct (green) = the contractor is a
// public issuer; parent (amber) = subsidiary of a public parent;
// associated (blue) = co-appears on permits with a public entity.
// Shows the correlation indicator J described — when Bob's Electric
// works permits with Target, TGT renders here as associated.
var t = r.tickers || {direct:[], associated:[]};
var allTk = (t.direct||[]).concat(t.associated||[]);
if(allTk.length){
var trk = document.createElement('div'); trk.className='tickers';
allTk.forEach(function(x){
var p = document.createElement('span');
p.className = 'ticker-pill ' + (x.via||'direct');
p.textContent = x.ticker;
// Tooltip shows the full reason path
var hint = x.via === 'associated'
? 'Associated via co-permits with '+x.partner_name+' ('+(x.co_permits||0)+' shared permits)' + (x.matched_name ? ' — '+x.matched_name : '')
: x.via === 'parent'
? 'Subsidiary of '+(x.matched_name||x.ticker) + (x.exchange ? ' · '+x.exchange : '')
: 'Direct match: '+(x.matched_name||r.name);
p.title = hint;
trk.appendChild(p);
});
ntd.appendChild(trk);
}
tr.appendChild(ntd); tr.appendChild(ntd);
var ptd=document.createElement('td'); ptd.className='right'; var ptd=document.createElement('td'); ptd.className='right';
ptd.textContent=(r.permits||0).toLocaleString(); ptd.textContent=(r.permits||0).toLocaleString();