diff --git a/server/index.js b/server/index.js index a5d42f4..282fed2 100644 --- a/server/index.js +++ b/server/index.js @@ -1,10 +1,18 @@ -// OTIVM server — OTIVM-III complete +// OTIVM server — OTIVM-IV // Per-player SQLite integration. // // Drift log trigger types: -// newDen < prevDen → trigger_type = 'dispatch_cost' (cost deduction on dispatch) +// newDen < prevDen → trigger_type = 'dispatch_cost' (cost deduction on dispatch) // newDen > prevDen → trigger_type = 'venture_complete' (return profit) -// newAut band change → trigger_type = 'interval_complete' (otium rest) +// aut band change → trigger_type = 'interval_complete' (otium rest) +// otium event → trigger_type = 'otium_access_fee', 'personal_maintenance', +// 'officia_obligation' (three separate entries) +// +// Otium expenditure constants — from docs/economy/cost-calibration-model.md: +// OTIUM_ACCESS_FEE_DN = 2.00 (LOW confidence) +// PERSONAL_MAINTENANCE_DN = 4.00 (MEDIUM confidence) +// OFFICIA_OBLIGATION_DN = 2.00 (LOW confidence) +// OTIUM_CYCLE_TOTAL_DN = 8.00 // // Cost split — placeholder pending a proper cost model: // cost_vectura = 60% of route cost (VECTVRA — freight charge) @@ -31,6 +39,14 @@ const SCHEMA_PATH = join(ROOT, 'data', 'create_player_db.sql') await mkdir(SAVES_DIR, { recursive: true }) +// ── Otium expenditure constants ────────────────────────────────────────────── +// Source: docs/economy/cost-calibration-model.md +// Change only here — three separate drift log entries are written per otium cycle. +const OTIUM_ACCESS_FEE_DN = 2.00 // LOW confidence +const PERSONAL_MAINTENANCE_DN = 4.00 // MEDIUM confidence +const OFFICIA_OBLIGATION_DN = 2.00 // LOW confidence +const OTIUM_CYCLE_TOTAL_DN = OTIUM_ACCESS_FEE_DN + PERSONAL_MAINTENANCE_DN + OFFICIA_OBLIGATION_DN + // ── Cost split constants ───────────────────────────────────────────────────── const COST_VECTURA_RATIO = 0.60 const COST_PORTORIA_RATIO = 0.25 @@ -113,8 +129,6 @@ function uuid() { } // ── JSON → SQLite migration ────────────────────────────────────────────────── -// Retained per roadmap — JSON files are never deleted. -// Migration runs transparently on first SQLite access if a .json exists. function migrateJsonToSqlite(token, pdb, json) { const now = new Date().toISOString() @@ -233,10 +247,7 @@ function readPlayerState(token, pdb) { route_dispatches[ev.route_id] = (route_dispatches[ev.route_id] || 0) + 1 } if (ev.type === 'journal_unlock' && ev.route_id) { - journal_seen.push({ - routeId: ev.route_id, - dispatch: route_dispatches[ev.route_id] || 1, - }) + journal_seen.push({ routeId: ev.route_id, dispatch: route_dispatches[ev.route_id] || 1 }) } if (ev.type === 'venture_start' || ev.type === 'dispatch_start') { try { @@ -508,6 +519,47 @@ function writePlayerState(token, pdb, body) { continue } + // Otium event — debit liquiditas in three named components + // Each component writes a separate drift log entry so the sub-trace + // records the cause of each debit individually. + if (ev.type === 'otium') { + insertEvent.run( + actorId, ev.type, null, null, + JSON.stringify(ev), ts + ) + + // Only debit if we have a prior liquiditas value to work from + if (prevDen !== null) { + let runningDen = parseFloat(prevDen) + + const deductions = [ + { amount: OTIUM_ACCESS_FEE_DN, trigger: 'otium_access_fee', note: 'Commercial information access and factor network maintenance.' }, + { amount: PERSONAL_MAINTENANCE_DN, trigger: 'personal_maintenance', note: 'Food, lodging, clothing upkeep, light, and local movement.' }, + { amount: OFFICIA_OBLIGATION_DN, trigger: 'officia_obligation', note: 'Patronage, tips, gifts, collegial contributions, and unrecovered favors.' }, + ] + + for (const deduction of deductions) { + const denBefore = runningDen + const denAfter = Math.max(0, runningDen - deduction.amount) + runningDen = denAfter + + writeDriftLog( + pdb, actorId, 'liquiditas', + deduction.trigger, + null, + String(denBefore), String(denAfter), + deduction.note, + ts + ) + } + + // Update liquiditas to the final post-otium value + updateParam(pdb, actorId, 'liquiditas', String(runningDen), String(runningDen), ts) + } + + continue + } + insertEvent.run( actorId, ev.type || 'unknown', routeId, @@ -524,36 +576,43 @@ function writePlayerState(token, pdb, body) { // aut band change → 'interval_complete' (otium rest) // // No drift log entry when prevDen/prevAut is null (initial seeding). + // Liquiditas may already have been updated by the otium handler above — + // the body.den update below handles dispatch and venture changes only. if (body.den !== undefined) { const newDen = String(body.den) - if (prevDen === null) { - // Initial write — no drift log + // Re-read current liquiditas in case otium handler already updated it + const freshLiq = pdb.prepare(` + SELECT value_true FROM actor_parameters + WHERE actor_id = ? AND parameter_token = 'liquiditas' AND superseded_at IS NULL + ORDER BY recorded_at DESC LIMIT 1 + `).get(actorId) + const freshDen = freshLiq?.value_true ?? null + + if (freshDen === null) { updateParam(pdb, actorId, 'liquiditas', newDen, newDen, now) - } else if (newDen !== prevDen) { + } else if (newDen !== freshDen) { const numNew = parseFloat(newDen) - const numPrev = parseFloat(prevDen) + const numFresh = parseFloat(freshDen) updateParam(pdb, actorId, 'liquiditas', newDen, newDen, now) - if (numNew < numPrev) { - // Cost deduction — dispatch_start fired + if (numNew < numFresh) { const active = findActiveVenture(pdb, actorId) writeDriftLog( pdb, actorId, 'liquiditas', 'dispatch_cost', active?.venture_id || null, - prevDen, newDen, + freshDen, newDen, active?.venture_label || null, now ) } else { - // Profit return — dispatch_complete fired const lastVenture = findLastCompletedVenture(pdb, actorId) writeDriftLog( pdb, actorId, 'liquiditas', 'venture_complete', lastVenture?.venture_id || null, - prevDen, newDen, + freshDen, newDen, lastVenture?.venture_label || null, now ) @@ -707,7 +766,7 @@ fastify.setNotFoundHandler((req, reply) => { reply.sendFile('index.html') }) try { await fastify.listen({ port: 3000, host: '0.0.0.0' }) - console.log('OTIVM server running on port 3000 — OTIVM-III complete') + console.log('OTIVM server running on port 3000 — OTIVM-IV') } catch (err) { console.error(err) process.exit(1)