// Relationship context for the coordinator. // // The graph has been built for months and fed nothing but a dashboard: no // prediction, decision or order has ever seen an edge. That is the one piece of // context a per-event coordinator genuinely cannot derive from its own article // set, because "NVDA supplies X, so a capacity story at X matters for NVDA" is // knowledge about companies rather than about this event. // // The hard rule here is the cutoff. company_relationships.first_seen_at is // derived from article dates rather than processing time, so filtering on it // keeps a historical proposal from seeing a relationship the world had not yet // revealed. Without that filter this feature would quietly reintroduce exactly // the lookahead the evidence check exists to prevent. const MAX_COMPANIES = 3; const MAX_PER_COMPANY = 8; function buildGraphContext(intelligenceDb, eventId, informationCutoff, options = {}) { if (!eventId || !informationCutoff) return ''; const maxCompanies = Number(options.maxCompanies) || MAX_COMPANIES; const maxPerCompany = Number(options.maxPerCompany) || MAX_PER_COMPANY; try { const companies = intelligenceDb.prepare(` SELECT DISTINCT tc.id, tc.name, tc.ticker FROM event_knowledge ek JOIN tracked_companies tc ON tc.id = ek.company_id WHERE ek.event_id = ? LIMIT ? `).all(eventId, maxCompanies); if (!companies.length) return ''; const relationships = intelligenceDb.prepare(` SELECT relationship_type, to_entity, confidence, confirmation_count FROM company_relationships WHERE from_company_id = ? AND first_seen_at IS NOT NULL AND datetime(first_seen_at) <= datetime(?) ORDER BY confirmation_count DESC, id ASC LIMIT ? `); const blocks = []; for (const company of companies) { const edges = relationships.all(company.id, informationCutoff, maxPerCompany); if (!edges.length) continue; // de-duplicate on the entity name, the graph stores both casings for some const seen = new Set(); const lines = []; for (const edge of edges) { const key = `${edge.relationship_type}:${String(edge.to_entity || '').toLowerCase()}`; if (seen.has(key)) continue; seen.add(key); lines.push(` - ${edge.relationship_type}: ${edge.to_entity}` + ` (seen ${edge.confirmation_count}x, ${edge.confidence || 'unrated'})`); } const label = company.ticker ? `${company.name} (${company.ticker})` : company.name; blocks.push(`${label}:\n${lines.join('\n')}`); } if (!blocks.length) return ''; return `Known company relationships, as they stood at the information cutoff:\n\n${blocks.join('\n\n')}\n\n` + `These are background knowledge, not evidence. They exist so you can reason about second order effects: ` + `a story about one company may be the tradable event for a supplier, customer or competitor. ` + `If you use one, say so in causal_channel, and still cite the article ids the story itself came from. ` + `Do not predict an instrument the articles give you no reason to believe is affected, and do not treat a ` + `relationship as evidence on its own.`; } catch (error) { // Context is an enhancement. Losing it should never cost us the prediction, // but it must never be lost silently either. console.error(`[graph-context] unavailable for event ${eventId}:`, error.message); return ''; } } module.exports = { buildGraphContext, MAX_COMPANIES, MAX_PER_COMPANY };