91 lines
4.8 KiB
JavaScript
91 lines
4.8 KiB
JavaScript
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 };
|