feat: let the coordinator see the relationship graph
The graph has been built for months and fed nothing but a dashboard. Nothing in the autonomy pipeline has ever read an edge: not the coordinator, not calibration, not execution. Its only downstream consumer, trade_signals, last produced anything in April. It was roughly 70% of the llm bill and informed no prediction, decision or order. Relationships are the one piece of context a per-event coordinator genuinely cannot derive from its own articles, because "this company supplies that one" is knowledge about companies rather than about this event. So the coordinator now receives the relationships of the companies the event is about, and is told to name the relationship in causal_channel when it reasons through one. The cutoff filter is the part that matters. first_seen_at on a relationship is derived from article dates rather than processing time, so a historical proposal only sees what the world had actually revealed by its own cutoff. Without that this feature would quietly reintroduce the lookahead the evidence check exists to prevent, and it is pinned by a test rather than left to review. Relationships are explicitly background rather than evidence: predictions still have to cite the article ids the story came from, and an instrument the articles give no reason to care about is still not a prediction. strategy_version moves to autonomy-2, because a prompt change this material changes what a prediction means and the two populations should be comparable later rather than silently blended. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01WnNxwxfXSbeNtjvtz5gayb
This commit is contained in:
@@ -0,0 +1,76 @@
|
||||
// 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 };
|
||||
Reference in New Issue
Block a user