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:
+86
-4
@@ -908,10 +908,30 @@ async function main() {
|
||||
+ `$where=${encodeURIComponent(where.join(" AND "))}`
|
||||
+ `&$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 {
|
||||
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_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 consume = (rows: any[], role: string) => {
|
||||
@@ -931,16 +951,78 @@ async function main() {
|
||||
};
|
||||
consume(byC1, "applicant");
|
||||
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) }))
|
||||
.sort((a, b) => b.total_cost - a.total_cost)
|
||||
.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({
|
||||
count: rows.length,
|
||||
count: enriched.length,
|
||||
since: sinceDate,
|
||||
min_cost: minCost,
|
||||
search,
|
||||
contractors: rows,
|
||||
contractors: enriched,
|
||||
duration_ms: Date.now() - start,
|
||||
});
|
||||
} catch (e: any) {
|
||||
|
||||
Reference in New Issue
Block a user