iv: fix drift log trigger type — dispatch_cost vs venture_complete

This commit is contained in:
otivm
2026-05-03 09:27:56 +00:00
parent 40f2e59e14
commit d1e1b98fa5

View File

@@ -1,20 +1,17 @@
// OTIVM server — OTIVM-IV
// OTIVM server — OTIVM-III complete
// Per-player SQLite integration.
//
// Fix 1: if actor_profile exists but background_id = 'unknown' and the
// incoming body has a real background_id, patch the profile and seed
// actor_parameters from background_starting_values. This handles the
// race where the first POST (background selection save) arrives before
// the background_id is set in the body.
//
// Fix 2: drift log entries are suppressed when there is no prior
// parameter row (prevDen = null / first write). This eliminates noise
// rows from the initial seeding sequence.
// 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)
//
// Cost split — placeholder pending a proper cost model:
// cost_vectura = 60% of route cost (VECTVRA — freight charge)
// cost_portoria = 25% of route cost (PORTORIUM — customs duty)
// cost_other = 15% of route cost (horreum, incidentals)
//
// JSON migration: retained per roadmap — JSON files are never deleted.
import Fastify from 'fastify'
import fastifyStatic from '@fastify/static'
@@ -116,9 +113,11 @@ 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()
const now = new Date().toISOString()
const actorId = token
const sessionId = token
const background = json.background_id || 'unknown'
@@ -293,9 +292,7 @@ function autToInt(band) {
}
}
// ── Seed actor_parameters from background_starting_values ────────────────────
// Extracted as a standalone function so it can be called both at new-player
// creation and at the background-patch step (Fix 1).
// ── Seed parameters ──────────────────────────────────────────────────────────
function seedParameters(pdb, actorId, backgroundId, denOverride, now) {
pdb.prepare(`
@@ -316,12 +313,9 @@ function seedParameters(pdb, actorId, backgroundId, denOverride, now) {
WHERE background_id = ?
`).run(actorId, now, backgroundId)
// Override liquiditas with the confirmed starting den from the frontend.
// Supersede the seed row immediately so the true starting value is authoritative.
if (denOverride !== undefined) {
pdb.prepare(`
UPDATE actor_parameters
SET superseded_at = ?
UPDATE actor_parameters SET superseded_at = ?
WHERE actor_id = ? AND parameter_token = 'liquiditas' AND superseded_at IS NULL
`).run(now, actorId)
pdb.prepare(`
@@ -344,6 +338,14 @@ function findActiveVenture(pdb, actorId) {
`).get(actorId)
}
function findLastCompletedVenture(pdb, actorId) {
return pdb.prepare(`
SELECT venture_id, venture_label FROM ventures
WHERE actor_id = ? AND status = 'complete'
ORDER BY completed_at DESC LIMIT 1
`).get(actorId)
}
function createVenture(pdb, actorId, routeId, now) {
const eco = ROUTE_ECONOMICS[routeId]
const cargo = ROUTE_CARGO[routeId]
@@ -392,8 +394,7 @@ function closeVenture(pdb, actorId, routeId, ventureId, now) {
WHERE venture_id = ?
`).run(eco.profit, outcomeNet, now, ventureId)
pdb.prepare(`
UPDATE venture_legs
SET status = 'complete', completed_at = ?
UPDATE venture_legs SET status = 'complete', completed_at = ?
WHERE venture_id = ? AND status = 'active'
`).run(now, ventureId)
}
@@ -420,15 +421,11 @@ function writePlayerState(token, pdb, body) {
const incomingBackground = body.background_id
const isRealBackground = incomingBackground && incomingBackground !== 'unknown'
// Load existing profile (may or may not exist)
const profile = pdb.prepare(
'SELECT actor_id, background_id FROM actor_profile WHERE actor_id = ? LIMIT 1'
).get(actorId)
if (!profile) {
// Brand new player — create profile regardless of background_id.
// If background is still 'unknown', parameters are not seeded yet;
// they will be seeded on the next POST that arrives with a real background.
const backgroundId = incomingBackground || 'unknown'
const actorName = body.actor_name || 'Mercator'
@@ -443,18 +440,15 @@ function writePlayerState(token, pdb, body) {
}
} else if (profile.background_id === 'unknown' && isRealBackground) {
// Fix 1: profile exists but background was not set on first write.
// Patch the profile and seed parameters now.
pdb.prepare(`
UPDATE actor_profile SET background_id = ?, schema_version = 5 WHERE actor_id = ?
`).run(incomingBackground, actorId)
seedParameters(pdb, actorId, incomingBackground, body.den, now)
console.log(`[profile] Patched background_id for ${token}: unknown → ${incomingBackground}`)
}
// Read current parameter values — needed for drift log (Fix 2: null check)
// Read current values before any update — null if no prior row exists
const currentLiq = pdb.prepare(`
SELECT value_true FROM actor_parameters
WHERE actor_id = ? AND parameter_token = 'liquiditas' AND superseded_at IS NULL
@@ -466,7 +460,6 @@ function writePlayerState(token, pdb, body) {
ORDER BY recorded_at DESC LIMIT 1
`).get(actorId)
// Fix 2: only record drift when a prior row exists (not during initial seeding)
const prevDen = currentLiq?.value_true ?? null
const prevAut = currentAut?.value_true ?? null
@@ -524,35 +517,55 @@ function writePlayerState(token, pdb, body) {
}
// ── Update parameters + drift log ────────────────────────────────────────
//
// Drift log trigger types:
// newDen < prevDen → 'dispatch_cost' (cost deducted at dispatch)
// newDen > prevDen → 'venture_complete' (profit returned on completion)
// aut band change → 'interval_complete' (otium rest)
//
// No drift log entry when prevDen/prevAut is null (initial seeding).
if (body.den !== undefined) {
const newDen = String(body.den)
if (prevDen !== null && newDen !== prevDen) {
const lastVenture = pdb.prepare(`
SELECT venture_id, venture_label FROM ventures
WHERE actor_id = ? AND status = 'complete'
ORDER BY completed_at DESC LIMIT 1
`).get(actorId)
if (prevDen === null) {
// Initial write — no drift log
updateParam(pdb, actorId, 'liquiditas', newDen, newDen, now)
} else if (newDen !== prevDen) {
const numNew = parseFloat(newDen)
const numPrev = parseFloat(prevDen)
updateParam(pdb, actorId, 'liquiditas', newDen, newDen, now)
writeDriftLog(
pdb, actorId, 'liquiditas',
'venture_complete',
lastVenture?.venture_id || null,
prevDen, newDen,
lastVenture?.venture_label || null,
now
)
} else if (prevDen === null) {
// No prior row — update silently, no drift log entry
updateParam(pdb, actorId, 'liquiditas', newDen, newDen, now)
if (numNew < numPrev) {
// Cost deduction — dispatch_start fired
const active = findActiveVenture(pdb, actorId)
writeDriftLog(
pdb, actorId, 'liquiditas',
'dispatch_cost',
active?.venture_id || null,
prevDen, 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,
lastVenture?.venture_label || null,
now
)
}
}
}
if (body.aut !== undefined) {
const newBand = autBand(body.aut)
if (prevAut !== null && newBand !== prevAut) {
if (prevAut === null) {
updateParamWithSocial(pdb, actorId, 'auctoritas', newBand, newBand, newBand, now)
} else if (newBand !== prevAut) {
updateParamWithSocial(pdb, actorId, 'auctoritas', newBand, newBand, newBand, now)
writeDriftLog(
pdb, actorId, 'auctoritas',
@@ -562,8 +575,6 @@ function writePlayerState(token, pdb, body) {
'otium rest',
now
)
} else if (prevAut === null) {
updateParamWithSocial(pdb, actorId, 'auctoritas', newBand, newBand, newBand, now)
}
}
}
@@ -696,7 +707,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-IV (fix: background_id patch)')
console.log('OTIVM server running on port 3000 — OTIVM-III complete')
} catch (err) {
console.error(err)
process.exit(1)