iv: add otium expenditure debits — three drift log entries per otium cycle

This commit is contained in:
otivm
2026-05-03 15:31:58 +00:00
parent f7865bb09a
commit e13b54a697

View File

@@ -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 = '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)