const ALLOWED_DIRECTIONS = new Set(['positive', 'negative']); const ALLOWED_HORIZONS = new Set([1, 5, 10, 20, 30, 60, 90]); function normalizeProposal(raw, { informationCutoff, model = 'unknown', promptVersion = 'unknown' } = {}) { if (!raw || typeof raw !== 'object') throw new Error('coordinator output must be an object'); const predictions = Array.isArray(raw.predictions) ? raw.predictions : []; const normalized = predictions.map((item) => { const instrument = String(item.instrument || item.ticker || '').trim().toUpperCase(); const direction = String(item.direction || '').trim().toLowerCase(); const horizonDays = Number(item.horizon_days || item.horizonDays); if (!instrument) throw new Error('prediction instrument is required'); if (!ALLOWED_DIRECTIONS.has(direction)) throw new Error(`invalid direction: ${direction}`); if (!ALLOWED_HORIZONS.has(horizonDays)) throw new Error(`invalid horizon_days: ${horizonDays}`); const articleIds = Array.isArray(item.evidence_article_ids) ? item.evidence_article_ids.map(Number).filter(Number.isInteger) : []; if (articleIds.length === 0) throw new Error(`prediction for ${instrument} has no evidence`); return { instrument, direction, eventType: String(item.event_type || 'unknown').trim().toLowerCase(), causalChannel: item.causal_channel ? String(item.causal_channel).trim() : null, horizonDays, evidenceArticleIds: [...new Set(articleIds)], invalidationCondition: item.invalidation_condition ? String(item.invalidation_condition).trim() : null, }; }); return { schemaVersion: 1, informationCutoff: informationCutoff || new Date().toISOString(), coordinatorModel: model, promptVersion, predictions: normalized, }; } function verifyEvidence(archiveDb, articleIds) { const placeholders = articleIds.map(() => '?').join(','); const rows = archiveDb.prepare(`SELECT id FROM articles WHERE id IN (${placeholders})`).all(...articleIds); const found = new Set(rows.map((row) => row.id)); return articleIds.every((id) => found.has(id)); } function acceptProposal(intelligenceDb, archiveDb, raw, metadata = {}) { const proposal = normalizeProposal(raw, metadata); for (const prediction of proposal.predictions) { if (!verifyEvidence(archiveDb, prediction.evidenceArticleIds)) { throw new Error(`proposal references missing evidence for ${prediction.instrument}`); } const instrument = intelligenceDb.prepare( "SELECT tradable FROM autonomy_instruments WHERE symbol = ? AND active = 1 AND tradable = 1" ).get(prediction.instrument); if (!instrument) throw new Error(`instrument is not currently allowlisted: ${prediction.instrument}`); } const insert = intelligenceDb.prepare(` INSERT INTO autonomy_proposals (event_id, payload, information_cutoff, coordinator_model, prompt_version, status) VALUES (?, ?, ?, ?, ?, 'accepted') `); const insertPrediction = intelligenceDb.prepare(` INSERT INTO autonomy_predictions (proposal_id, event_id, instrument, direction, event_type, causal_channel, horizon_days, information_cutoff, evidence_article_ids, invalidation_condition, learning_eligible, strategy_version) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?) `); const tx = intelligenceDb.transaction(() => { const proposalResult = insert.run(metadata.eventId || null, JSON.stringify(proposal), proposal.informationCutoff, proposal.coordinatorModel, proposal.promptVersion); for (const prediction of proposal.predictions) { insertPrediction.run(proposalResult.lastInsertRowid, metadata.eventId || null, prediction.instrument, prediction.direction, prediction.eventType, prediction.causalChannel, prediction.horizonDays, proposal.informationCutoff, JSON.stringify(prediction.evidenceArticleIds), prediction.invalidationCondition, metadata.learningEligible ? 1 : 0, metadata.strategyVersion || 'autonomy-1'); } return Number(proposalResult.lastInsertRowid); }); return { proposalId: tx(), predictionCount: proposal.predictions.length }; } function recordRejectedProposal(intelligenceDb, raw, metadata = {}, reason = 'validation failed') { const payload = raw && typeof raw === 'object' ? raw : { raw: String(raw) }; return intelligenceDb.prepare(` INSERT INTO autonomy_proposals (event_id, payload, information_cutoff, coordinator_model, prompt_version, status, rejection_reason, reviewed_at) VALUES (?, ?, ?, ?, ?, 'rejected', ?, datetime('now')) `).run(metadata.eventId || null, JSON.stringify(payload), metadata.informationCutoff || new Date().toISOString(), metadata.model || 'unknown', metadata.promptVersion || 'unknown', String(reason).slice(0, 1000)).lastInsertRowid; } module.exports = { normalizeProposal, verifyEvidence, acceptProposal, recordRejectedProposal };