Files
Duriin-API/src/autonomy/coordinator.js
T

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 };