schema: add session_anchor_at to actor_profile for simulation clock

This commit is contained in:
otivm
2026-05-02 21:20:27 +00:00
parent 6e1cde5ad0
commit 2e611ff78e

View File

@@ -1,6 +1,6 @@
-- OTIVM Per-Player Database Schema -- OTIVM Per-Player Database Schema
-- File: data/create_player_db.sql -- File: data/create_player_db.sql
-- Version: OTIVM-III -- Version: OTIVM-IV (schema version 4)
-- Status: Game schema — transitional, designed to be throw-away -- Status: Game schema — transitional, designed to be throw-away
-- Replaces: data/saves/{session_id}.json (OTIVM-I/II) -- Replaces: data/saves/{session_id}.json (OTIVM-I/II)
-- Parallel to: data/create_otivm_db.sql (TESSERA world substrate) -- Parallel to: data/create_otivm_db.sql (TESSERA world substrate)
@@ -11,6 +11,12 @@
-- value_true and value_perceived are always separate columns -- value_true and value_perceived are always separate columns
-- confidence_tag is always a first-class column, never a comment -- confidence_tag is always a first-class column, never a comment
-- --
-- Simulation clock: see docs/architecture/simulation-clock.md
-- session_anchor_at is the real UTC timestamp at which the player's
-- simulation clock started (set at first dispatch, NULL until then).
-- All simulated date arithmetic uses this field as the epoch anchor
-- per the integer constraint documented in simulation-clock.md.
--
-- When this schema is retired, it becomes a read-only layer -- When this schema is retired, it becomes a read-only layer
-- readable by the Simulator via the existing API translation layer. -- readable by the Simulator via the existing API translation layer.
-- The API is the universal wiring — this schema does not need to -- The API is the universal wiring — this schema does not need to
@@ -21,31 +27,39 @@
PRAGMA journal_mode = WAL; PRAGMA journal_mode = WAL;
PRAGMA foreign_keys = ON; PRAGMA foreign_keys = ON;
PRAGMA user_version = 3; -- OTIVM-III schema version PRAGMA user_version = 4; -- OTIVM-IV schema version
-- ============================================================ -- ================================================================
-- TABLE: actor_profile -- TABLE: actor_profile
-- One row per actor. The static anchor. -- One row per actor. The static anchor.
-- Created at session initialisation. Never updated — superseded -- Created at session initialisation. Never updated — superseded
-- by a new row if background is changed (not currently possible -- by a new row if background is changed (not currently possible
-- in OTIVM-III but the structure supports it). -- in OTIVM-IV but the structure supports it).
-- ============================================================ --
-- session_anchor_at: NULL until first dispatch. Set once at the
-- moment the player fires their first venture. All simulation
-- clock arithmetic is relative to this timestamp.
-- See docs/architecture/simulation-clock.md Section 7.
-- ================================================================
CREATE TABLE IF NOT EXISTS actor_profile ( CREATE TABLE IF NOT EXISTS actor_profile (
actor_id TEXT NOT NULL, -- uuid, matches save file naming actor_id TEXT NOT NULL, -- uuid, matches save file naming
session_id TEXT NOT NULL, -- uuid, links to session chain session_id TEXT NOT NULL, -- uuid, links to session chain
background_id TEXT NOT NULL, -- former_legionary | freedman_trader | background_id TEXT NOT NULL, -- former_legionary | freedman_trader |
-- noble_younger_son | failed_magistrate | -- noble_younger_son | failed_magistrate |
-- camp_logistician | guild_scribe -- camp_logistician | guild_scribe
actor_name TEXT NOT NULL, -- display name chosen by participant actor_name TEXT NOT NULL, -- display name chosen by participant
epoch TEXT NOT NULL -- roman_14bce (Layer 3 code token) epoch TEXT NOT NULL -- roman_14bce (Layer 3 code token)
DEFAULT 'roman_14bce', DEFAULT 'roman_14bce',
schema_version INTEGER NOT NULL DEFAULT 3, schema_version INTEGER NOT NULL DEFAULT 4,
recorded_at TEXT NOT NULL, -- ISO 8601 UTC recorded_at TEXT NOT NULL, -- ISO 8601 UTC — row creation time
session_anchor_at TEXT, -- ISO 8601 UTC — first dispatch time
-- NULL until player fires first venture
-- Never updated after it is set
PRIMARY KEY (actor_id, recorded_at) PRIMARY KEY (actor_id, recorded_at)
); );
-- ============================================================ -- ================================================================
-- TABLE: actor_parameters -- TABLE: actor_parameters
-- The core parameter table. -- The core parameter table.
-- One row per parameter per actor per recorded moment. -- One row per parameter per actor per recorded moment.
@@ -68,23 +82,23 @@ CREATE TABLE IF NOT EXISTS actor_profile (
-- value columns are TEXT to support ordinal bands (low/medium/high), -- value columns are TEXT to support ordinal bands (low/medium/high),
-- enums, floats, integers, and JSON structures without schema changes. -- enums, floats, integers, and JSON structures without schema changes.
-- The parameter_token identifies what type to expect. -- The parameter_token identifies what type to expect.
-- ============================================================ -- ================================================================
CREATE TABLE IF NOT EXISTS actor_parameters ( CREATE TABLE IF NOT EXISTS actor_parameters (
id INTEGER PRIMARY KEY AUTOINCREMENT, id INTEGER PRIMARY KEY AUTOINCREMENT,
actor_id TEXT NOT NULL, actor_id TEXT NOT NULL,
parameter_token TEXT NOT NULL, -- from parameter-registry.md token field parameter_token TEXT NOT NULL, -- from parameter-registry.md token field
scope TEXT NOT NULL, -- actor | scenario | relation scope TEXT NOT NULL, -- actor | scenario | relation
layer TEXT NOT NULL, -- roman | mesolithic | universal layer TEXT NOT NULL, -- roman | mesolithic | universal
value_true TEXT NOT NULL, -- server-side ground truth value_true TEXT NOT NULL, -- server-side ground truth
value_perceived TEXT NOT NULL, -- actor-side belief (= value_true when full) value_perceived TEXT NOT NULL, -- actor-side belief (= value_true when full)
value_social TEXT, -- market consensus (auctoritas only) value_social TEXT, -- market consensus (auctoritas only)
confidence_tag TEXT NOT NULL -- measured | indicated | inferred | confidence_tag TEXT NOT NULL -- measured | indicated | inferred |
DEFAULT 'estimated', -- estimated | unknown DEFAULT 'estimated', -- estimated | unknown
observable_level TEXT NOT NULL, -- full | partial | hidden observable_level TEXT NOT NULL, -- full | partial | hidden
drift_source TEXT, -- what caused this value (event_id, or NULL if initial) drift_source TEXT, -- what caused this value (event_id, or NULL if initial)
recorded_at TEXT NOT NULL, -- ISO 8601 UTC recorded_at TEXT NOT NULL, -- ISO 8601 UTC
superseded_at TEXT, -- NULL if current; set when a newer row replaces this one superseded_at TEXT, -- NULL if current; set when a newer row replaces this one
FOREIGN KEY (actor_id) REFERENCES actor_profile(actor_id) FOREIGN KEY (actor_id) REFERENCES actor_profile(actor_id)
); );
@@ -92,121 +106,127 @@ CREATE TABLE IF NOT EXISTS actor_parameters (
CREATE INDEX IF NOT EXISTS idx_actor_parameters_current CREATE INDEX IF NOT EXISTS idx_actor_parameters_current
ON actor_parameters (actor_id, parameter_token, superseded_at); ON actor_parameters (actor_id, parameter_token, superseded_at);
-- ============================================================ -- ================================================================
-- TABLE: parameter_drift_log -- TABLE: parameter_drift_log
-- Append-only event log of every parameter change. -- Append-only event log of every parameter change.
-- What changed, why, what triggered it, old and new values. -- What changed, why, what triggered it, old and new values.
-- The behavioral record. Rows are never deleted. -- The behavioral record. Rows are never deleted.
-- This is the source of truth for the Simulator when it -- This is the source of truth for the Simulator when it
-- reads this database as a read-only layer. -- reads this database as a read-only layer.
-- ============================================================ -- ================================================================
CREATE TABLE IF NOT EXISTS parameter_drift_log ( CREATE TABLE IF NOT EXISTS parameter_drift_log (
id INTEGER PRIMARY KEY AUTOINCREMENT, id INTEGER PRIMARY KEY AUTOINCREMENT,
actor_id TEXT NOT NULL, actor_id TEXT NOT NULL,
parameter_token TEXT NOT NULL, parameter_token TEXT NOT NULL,
trigger_type TEXT NOT NULL, -- venture_complete | leg_complete | trigger_type TEXT NOT NULL, -- venture_complete | leg_complete |
-- interval_complete | exchange_complete | -- interval_complete | exchange_complete |
-- scenario_event | session_start | -- scenario_event | session_start |
-- background_drift | manual -- background_drift | manual
trigger_ref TEXT, -- venture_id, leg_id, scenario_id, or NULL trigger_ref TEXT, -- venture_id, leg_id, scenario_id, or NULL
value_before TEXT NOT NULL, value_before TEXT NOT NULL,
value_after TEXT NOT NULL, value_after TEXT NOT NULL,
delta_note TEXT, -- human-readable reason for drift delta_note TEXT, -- human-readable reason for drift
recorded_at TEXT NOT NULL, -- ISO 8601 UTC recorded_at TEXT NOT NULL, -- ISO 8601 UTC
FOREIGN KEY (actor_id) REFERENCES actor_profile(actor_id) FOREIGN KEY (actor_id) REFERENCES actor_profile(actor_id)
); );
CREATE INDEX IF NOT EXISTS idx_drift_log_actor CREATE INDEX IF NOT EXISTS idx_drift_log_actor
ON parameter_drift_log (actor_id, recorded_at); ON parameter_drift_log (actor_id, recorded_at);
-- ============================================================ -- ================================================================
-- TABLE: ventures -- TABLE: ventures
-- One row per venture (NEGOTIVM in Roman layer). -- One row per venture (NEGOTIVM in Roman layer).
-- Status tracks the lifecycle. Outcome recorded on completion. -- Status tracks the lifecycle. Outcome recorded on completion.
-- ============================================================ -- ================================================================
CREATE TABLE IF NOT EXISTS ventures ( CREATE TABLE IF NOT EXISTS ventures (
venture_id TEXT PRIMARY KEY, -- uuid venture_id TEXT PRIMARY KEY, -- uuid
actor_id TEXT NOT NULL, actor_id TEXT NOT NULL,
venture_label TEXT NOT NULL, -- human-readable, e.g. "Ostia to Capua" venture_label TEXT NOT NULL, -- human-readable, e.g. "Ostia to Capua"
status TEXT NOT NULL -- planned | active | complete | abandoned status TEXT NOT NULL -- planned | active | complete | abandoned
DEFAULT 'planned', DEFAULT 'planned',
cargo_type TEXT, -- amphora type, modius goods, etc. cargo_type TEXT, -- amphora type, modius goods, etc.
cargo_quantity REAL, cargo_quantity REAL,
cargo_unit TEXT, -- amphora | modius | talent | unit cargo_unit TEXT, -- amphora | modius | talent | unit
cost_total REAL, -- denarii-equivalent, sum of all legs cost_total REAL, -- denarii-equivalent, sum of all legs
revenue_total REAL, -- denarii-equivalent, at settlement revenue_total REAL, -- denarii-equivalent, at settlement
outcome_net REAL, -- revenue_total - cost_total (NULL until complete) outcome_net REAL, -- revenue_total - cost_total (NULL until complete)
outcome_note TEXT, -- narrative summary of outcome outcome_note TEXT, -- narrative summary of outcome
recorded_at TEXT NOT NULL, -- ISO 8601 UTC — venture created recorded_at TEXT NOT NULL, -- ISO 8601 UTC — venture created
started_at TEXT, -- ISO 8601 UTC — first leg begun started_at TEXT, -- ISO 8601 UTC — first leg begun
completed_at TEXT, -- ISO 8601 UTC — final settlement completed_at TEXT, -- ISO 8601 UTC — final settlement
FOREIGN KEY (actor_id) REFERENCES actor_profile(actor_id) FOREIGN KEY (actor_id) REFERENCES actor_profile(actor_id)
); );
CREATE INDEX IF NOT EXISTS idx_ventures_actor CREATE INDEX IF NOT EXISTS idx_ventures_actor
ON ventures (actor_id, status); ON ventures (actor_id, status);
-- ============================================================ -- ================================================================
-- TABLE: venture_legs -- TABLE: venture_legs
-- One row per leg (ITER in Roman layer). -- One row per leg (ITER in Roman layer).
-- Indivisible unit of movement within a venture. -- Indivisible unit of movement within a venture.
-- Cost components recorded separately for Simulator readability. -- Cost components recorded separately for Simulator readability.
-- ============================================================ --
-- duration_days: integer simulated days per simulation-clock.md.
-- Must be a positive integer. Never a decimal.
-- Derived from duration_ms / MS_PER_SIM_DAY at write time.
-- See docs/architecture/simulation-clock.md Section 2.
-- ================================================================
CREATE TABLE IF NOT EXISTS venture_legs ( CREATE TABLE IF NOT EXISTS venture_legs (
leg_id TEXT PRIMARY KEY, -- uuid leg_id TEXT PRIMARY KEY, -- uuid
venture_id TEXT NOT NULL, venture_id TEXT NOT NULL,
leg_sequence INTEGER NOT NULL, -- 1, 2, 3... within the venture leg_sequence INTEGER NOT NULL, -- 1, 2, 3... within the venture
origin_h3 TEXT NOT NULL, -- H3 cell ID (TESSERA-compatible) origin_h3 TEXT NOT NULL, -- H3 cell ID (TESSERA-compatible)
destination_h3 TEXT NOT NULL, -- H3 cell ID (TESSERA-compatible) destination_h3 TEXT NOT NULL, -- H3 cell ID (TESSERA-compatible)
mode TEXT NOT NULL, -- road | sea | river | overland mode TEXT NOT NULL, -- road | sea | river | overland
vessel_type TEXT, -- navis_oneraria | actuaria | NULL if land vessel_type TEXT, -- navis_oneraria | actuaria | NULL if land
duration_days REAL, -- actual duration (NULL until complete) duration_days INTEGER, -- simulated days (INTEGER, never REAL)
cost_vectura REAL, -- freight charge (VECTVRA) -- NULL until leg completes
cost_portoria REAL, -- customs duty (PORTORIVM) cost_vectura REAL, -- freight charge (VECTVRA)
cost_other REAL, -- horreum, incidentals cost_portoria REAL, -- customs duty (PORTORIVM)
cost_total REAL, -- sum of above cost_other REAL, -- horreum, incidentals
status TEXT NOT NULL -- planned | active | complete | failed cost_total REAL, -- sum of above
DEFAULT 'planned', status TEXT NOT NULL -- planned | active | complete | failed
delay_days REAL, -- deviation from expected duration DEFAULT 'planned',
delay_cause TEXT, -- weather | congestion | dispute | NULL delay_days INTEGER, -- deviation from expected duration (INTEGER)
recorded_at TEXT NOT NULL, -- ISO 8601 UTC — leg created delay_cause TEXT, -- weather | congestion | dispute | NULL
started_at TEXT, recorded_at TEXT NOT NULL, -- ISO 8601 UTC — leg created
completed_at TEXT, started_at TEXT,
completed_at TEXT,
FOREIGN KEY (venture_id) REFERENCES ventures(venture_id) FOREIGN KEY (venture_id) REFERENCES ventures(venture_id)
); );
CREATE INDEX IF NOT EXISTS idx_legs_venture CREATE INDEX IF NOT EXISTS idx_legs_venture
ON venture_legs (venture_id, leg_sequence); ON venture_legs (venture_id, leg_sequence);
-- ============================================================ -- ================================================================
-- TABLE: scenario_state -- TABLE: scenario_state
-- Active and archived scenario parameters. -- Active and archived scenario parameters.
-- Transient during active window, archived on close. -- Transient during active window, archived on close.
-- Must not persist into actor_parameters as permanent values — -- Must not persist into actor_parameters as permanent values —
-- scenario parameters are pressures, not conditions. -- scenario parameters are pressures, not conditions.
-- ============================================================ -- ================================================================
CREATE TABLE IF NOT EXISTS scenario_state ( CREATE TABLE IF NOT EXISTS scenario_state (
id INTEGER PRIMARY KEY AUTOINCREMENT, id INTEGER PRIMARY KEY AUTOINCREMENT,
scenario_id TEXT NOT NULL, -- e.g. SCENARIO-MERCHANT-0001 scenario_id TEXT NOT NULL, -- e.g. SCENARIO-MERCHANT-0001
actor_id TEXT NOT NULL, actor_id TEXT NOT NULL,
parameter_token TEXT NOT NULL, -- from parameter-registry.md parameter_token TEXT NOT NULL, -- from parameter-registry.md
value_true TEXT NOT NULL, value_true TEXT NOT NULL,
value_perceived TEXT NOT NULL, value_perceived TEXT NOT NULL,
confidence_tag TEXT NOT NULL DEFAULT 'estimated', confidence_tag TEXT NOT NULL DEFAULT 'estimated',
observable_level TEXT NOT NULL, observable_level TEXT NOT NULL,
recorded_at TEXT NOT NULL, -- ISO 8601 UTC — parameter set recorded_at TEXT NOT NULL, -- ISO 8601 UTC — parameter set
archived_at TEXT, -- NULL if active; set on scenario close archived_at TEXT, -- NULL if active; set on scenario close
FOREIGN KEY (actor_id) REFERENCES actor_profile(actor_id) FOREIGN KEY (actor_id) REFERENCES actor_profile(actor_id)
); );
CREATE INDEX IF NOT EXISTS idx_scenario_active CREATE INDEX IF NOT EXISTS idx_scenario_active
ON scenario_state (actor_id, scenario_id, archived_at); ON scenario_state (actor_id, scenario_id, archived_at);
-- ============================================================ -- ================================================================
-- TABLE: events -- TABLE: events
-- Append-only chronological record of all simulation state -- Append-only chronological record of all simulation state
-- changes. The atomic unit of history. -- changes. The atomic unit of history.
@@ -214,23 +234,23 @@ CREATE INDEX IF NOT EXISTS idx_scenario_active
-- This table is the substrate for the behavioral model — -- This table is the substrate for the behavioral model —
-- when this database becomes a read-only Simulator layer, -- when this database becomes a read-only Simulator layer,
-- this table is the primary read target. -- this table is the primary read target.
-- ============================================================ -- ================================================================
CREATE TABLE IF NOT EXISTS events ( CREATE TABLE IF NOT EXISTS events (
id INTEGER PRIMARY KEY AUTOINCREMENT, id INTEGER PRIMARY KEY AUTOINCREMENT,
actor_id TEXT NOT NULL, actor_id TEXT NOT NULL,
event_type TEXT NOT NULL, -- from terminology.md Layer 3 event_type values: event_type TEXT NOT NULL, -- from terminology.md Layer 3 event_type values:
-- venture_start | venture_complete | -- venture_start | venture_complete |
-- leg_start | leg_complete | -- leg_start | leg_complete |
-- interval_start | interval_complete | -- interval_start | interval_complete |
-- exchange_complete | session_abandoned | -- exchange_complete | session_abandoned |
-- chapter_advance | journal_unlock | -- chapter_advance | journal_unlock |
-- scenario_trigger | scenario_close | -- scenario_trigger | scenario_close |
-- parameter_drift -- parameter_drift
ref_id TEXT, -- venture_id, leg_id, scenario_id, or NULL ref_id TEXT, -- venture_id, leg_id, scenario_id, or NULL
ref_type TEXT, -- venture | leg | scenario | actor | NULL ref_type TEXT, -- venture | leg | scenario | actor | NULL
payload TEXT, -- JSON — event-specific detail payload TEXT, -- JSON — event-specific detail
recorded_at TEXT NOT NULL, -- ISO 8601 UTC recorded_at TEXT NOT NULL, -- ISO 8601 UTC
FOREIGN KEY (actor_id) REFERENCES actor_profile(actor_id) FOREIGN KEY (actor_id) REFERENCES actor_profile(actor_id)
); );
@@ -240,136 +260,137 @@ CREATE INDEX IF NOT EXISTS idx_events_actor_time
CREATE INDEX IF NOT EXISTS idx_events_type CREATE INDEX IF NOT EXISTS idx_events_type
ON events (event_type, recorded_at); ON events (event_type, recorded_at);
-- ============================================================ -- ================================================================
-- TABLE: background_starting_values -- TABLE: background_starting_values
-- Seed data — the canonical starting parameter values for -- Seed data — the canonical starting parameter values for
-- each of the six backgrounds, per parameter-registry.md. -- each of the six backgrounds, per parameter-registry.md.
-- Read at actor initialisation to populate actor_parameters. -- Read at actor initialisation to populate actor_parameters.
-- Never modified after initial insert. -- Never modified after initial insert.
-- ============================================================ -- ================================================================
CREATE TABLE IF NOT EXISTS background_starting_values ( CREATE TABLE IF NOT EXISTS background_starting_values (
background_id TEXT NOT NULL, background_id TEXT NOT NULL,
parameter_token TEXT NOT NULL, parameter_token TEXT NOT NULL,
value_true TEXT NOT NULL, value_true TEXT NOT NULL,
value_perceived TEXT NOT NULL, value_perceived TEXT NOT NULL,
confidence_tag TEXT NOT NULL DEFAULT 'indicated', confidence_tag TEXT NOT NULL DEFAULT 'indicated',
observable_level TEXT NOT NULL, observable_level TEXT NOT NULL,
notes TEXT, notes TEXT,
PRIMARY KEY (background_id, parameter_token) PRIMARY KEY (background_id, parameter_token)
); );
-- ============================================================ -- ================================================================
-- SEED DATA: background_starting_values -- SEED DATA: background_starting_values
-- Source: parameter-registry.md, section 1 (Actor Parameters) -- Source: parameter-registry.md, section 1 (Actor Parameters)
-- Ordinal bands: low | medium | high | distinguished | extensive -- Ordinal bands: low | medium | high | distinguished | extensive
-- ============================================================ -- ================================================================
INSERT OR IGNORE INTO background_starting_values VALUES INSERT OR IGNORE INTO background_starting_values VALUES
-- FORMER LEGIONARY -- FORMER LEGIONARY
('former_legionary', 'auctoritas', 'medium', 'medium', 'indicated', 'partial', 'Reliable but not distinguished'), ('former_legionary', 'auctoritas', 'medium', 'medium', 'indicated', 'partial', 'Reliable but not distinguished'),
('former_legionary', 'clientela', 'low', 'low', 'indicated', 'partial', 'Military network, limited commercial reach'), ('former_legionary', 'clientela', 'low', 'low', 'indicated', 'partial', 'Military network, limited commercial reach'),
('former_legionary', 'liquiditas', '200', '200', 'measured', 'full', 'Denarii — modest savings'), ('former_legionary', 'liquiditas', '200', '200', 'measured', 'full', 'Denarii — modest savings'),
('former_legionary', 'fama', 'neutral','neutral','indicated', 'partial', NULL), ('former_legionary', 'fama', 'neutral','neutral','indicated', 'partial', NULL),
('former_legionary', 'disciplina', 'high', 'high', 'indicated', 'full', NULL), ('former_legionary', 'disciplina', 'high', 'high', 'indicated', 'full', NULL),
('former_legionary', 'mercatus_scientia', 'low', 'low', 'indicated', 'full', 'Military logistics, not commercial markets'), ('former_legionary', 'mercatus_scientia', 'low', 'low', 'indicated', 'full', 'Military logistics, not commercial markets'),
('former_legionary', 'itineris_scientia', 'high', 'high', 'indicated', 'full', 'Extensive road and route knowledge'), ('former_legionary', 'itineris_scientia', 'high', 'high', 'indicated', 'full', 'Extensive road and route knowledge'),
('former_legionary', 'ius_accessus', 'medium', 'medium', 'indicated', 'partial', 'Citizen standing'), ('former_legionary', 'ius_accessus', 'medium', 'medium', 'indicated', 'partial', 'Citizen standing'),
('former_legionary', 'periculum_tolerantia', 'high', 'high', 'indicated', 'full', NULL), ('former_legionary', 'periculum_tolerantia', 'high', 'high', 'indicated', 'full', NULL),
('former_legionary', 'negotiatio', 'low', 'low', 'indicated', 'full', NULL), ('former_legionary', 'negotiatio', 'low', 'low', 'indicated', 'full', NULL),
('former_legionary', 'litterae', 'medium', 'medium', 'indicated', 'full', NULL), ('former_legionary', 'litterae', 'medium', 'medium', 'indicated', 'full', NULL),
('former_legionary', 'officia_burden', 'low', 'low', 'indicated', 'partial', NULL), ('former_legionary', 'officia_burden', 'low', 'low', 'indicated', 'partial', NULL),
-- FREEDMAN TRADER -- FREEDMAN TRADER
('freedman_trader', 'auctoritas', 'low', 'low', 'indicated', 'partial', 'Practical reputation growing faster than social recognition'), ('freedman_trader', 'auctoritas', 'low', 'low', 'indicated', 'partial', 'Practical reputation growing faster than social recognition'),
('freedman_trader', 'clientela', 'medium', 'medium', 'indicated', 'partial', 'Commercial network, active'), ('freedman_trader', 'clientela', 'medium', 'medium', 'indicated', 'partial', 'Commercial network, active'),
('freedman_trader', 'liquiditas', '350', '350', 'measured', 'full', 'Denarii — working capital'), ('freedman_trader', 'liquiditas', '350', '350', 'measured', 'full', 'Denarii — working capital'),
('freedman_trader', 'fama', 'neutral','neutral','indicated', 'partial', NULL), ('freedman_trader', 'fama', 'neutral','neutral','indicated', 'partial', NULL),
('freedman_trader', 'disciplina', 'medium', 'medium', 'indicated', 'full', NULL), ('freedman_trader', 'disciplina', 'medium', 'medium', 'indicated', 'full', NULL),
('freedman_trader', 'mercatus_scientia', 'high', 'high', 'indicated', 'full', NULL), ('freedman_trader', 'mercatus_scientia', 'high', 'high', 'indicated', 'full', NULL),
('freedman_trader', 'itineris_scientia', 'medium', 'medium', 'indicated', 'full', NULL), ('freedman_trader', 'itineris_scientia', 'medium', 'medium', 'indicated', 'full', NULL),
('freedman_trader', 'ius_accessus', 'low', 'low', 'indicated', 'partial', 'Freedman limits on contract enforceability'), ('freedman_trader', 'ius_accessus', 'low', 'low', 'indicated', 'partial', 'Freedman limits on contract enforceability'),
('freedman_trader', 'periculum_tolerantia', 'medium', 'medium', 'indicated', 'full', NULL), ('freedman_trader', 'periculum_tolerantia', 'medium', 'medium', 'indicated', 'full', NULL),
('freedman_trader', 'negotiatio', 'high', 'high', 'indicated', 'full', NULL), ('freedman_trader', 'negotiatio', 'high', 'high', 'indicated', 'full', NULL),
('freedman_trader', 'litterae', 'high', 'high', 'indicated', 'full', NULL), ('freedman_trader', 'litterae', 'high', 'high', 'indicated', 'full', NULL),
('freedman_trader', 'officia_burden', 'low', 'low', 'indicated', 'partial', NULL), ('freedman_trader', 'officia_burden', 'low', 'low', 'indicated', 'partial', NULL),
-- NOBLE YOUNGER SON -- NOBLE YOUNGER SON
('noble_younger_son','auctoritas', 'high', 'high', 'indicated', 'partial', NULL), ('noble_younger_son','auctoritas', 'high', 'high', 'indicated', 'partial', NULL),
('noble_younger_son','clientela', 'high', 'high', 'indicated', 'partial', 'Inherited network, not personally built'), ('noble_younger_son','clientela', 'high', 'high', 'indicated', 'partial', 'Inherited network, not personally built'),
('noble_younger_son','liquiditas', '150', '150', 'measured', 'full', 'Denarii — constrained by elder sibling priority'), ('noble_younger_son','liquiditas', '150', '150', 'measured', 'full', 'Denarii — constrained by elder sibling priority'),
('noble_younger_son','fama', 'good', 'good', 'indicated', 'partial', NULL), ('noble_younger_son','fama', 'good', 'good', 'indicated', 'partial', NULL),
('noble_younger_son','disciplina', 'low', 'low', 'indicated', 'full', NULL), ('noble_younger_son','disciplina', 'low', 'low', 'indicated', 'full', NULL),
('noble_younger_son','mercatus_scientia', 'low', 'low', 'indicated', 'full', NULL), ('noble_younger_son','mercatus_scientia', 'low', 'low', 'indicated', 'full', NULL),
('noble_younger_son','itineris_scientia', 'low', 'low', 'indicated', 'full', NULL), ('noble_younger_son','itineris_scientia', 'low', 'low', 'indicated', 'full', NULL),
('noble_younger_son','ius_accessus', 'high', 'high', 'indicated', 'partial', NULL), ('noble_younger_son','ius_accessus', 'high', 'high', 'indicated', 'partial', NULL),
('noble_younger_son','periculum_tolerantia', 'low', 'low', 'indicated', 'full', NULL), ('noble_younger_son','periculum_tolerantia', 'low', 'low', 'indicated', 'full', NULL),
('noble_younger_son','negotiatio', 'medium', 'medium', 'indicated', 'full', NULL), ('noble_younger_son','negotiatio', 'medium', 'medium', 'indicated', 'full', NULL),
('noble_younger_son','litterae', 'high', 'high', 'indicated', 'full', NULL), ('noble_younger_son','litterae', 'high', 'high', 'indicated', 'full', NULL),
('noble_younger_son','officia_burden', 'high', 'medium', 'indicated', 'partial', 'Underestimates informal obligations'), ('noble_younger_son','officia_burden', 'high', 'medium', 'indicated', 'partial', 'Underestimates informal obligations'),
-- FAILED MAGISTRATE -- FAILED MAGISTRATE
('failed_magistrate','auctoritas', 'low', 'medium', 'indicated', 'partial', 'True value lower than perceived — falling'), ('failed_magistrate','auctoritas', 'low', 'medium', 'indicated', 'partial', 'True value lower than perceived — falling'),
('failed_magistrate','clientela', 'medium', 'medium', 'indicated', 'partial', 'Legal contacts, politically exposed'), ('failed_magistrate','clientela', 'medium', 'medium', 'indicated', 'partial', 'Legal contacts, politically exposed'),
('failed_magistrate','liquiditas', '100', '100', 'measured', 'full', 'Denarii — depleted by failed campaign'), ('failed_magistrate','liquiditas', '100', '100', 'measured', 'full', 'Denarii — depleted by failed campaign'),
('failed_magistrate','fama', 'mixed', 'mixed', 'indicated', 'partial', NULL), ('failed_magistrate','fama', 'mixed', 'mixed', 'indicated', 'partial', NULL),
('failed_magistrate','disciplina', 'medium', 'medium', 'indicated', 'full', NULL), ('failed_magistrate','disciplina', 'medium', 'medium', 'indicated', 'full', NULL),
('failed_magistrate','mercatus_scientia', 'low', 'low', 'indicated', 'full', NULL), ('failed_magistrate','mercatus_scientia', 'low', 'low', 'indicated', 'full', NULL),
('failed_magistrate','itineris_scientia', 'low', 'low', 'indicated', 'full', NULL), ('failed_magistrate','itineris_scientia', 'low', 'low', 'indicated', 'full', NULL),
('failed_magistrate','ius_accessus', 'high', 'high', 'indicated', 'partial', 'Formal standing intact, practical access eroding'), ('failed_magistrate','ius_accessus', 'high', 'high', 'indicated', 'partial', 'Formal standing intact, practical access eroding'),
('failed_magistrate','periculum_tolerantia', 'low', 'low', 'indicated', 'full', NULL), ('failed_magistrate','periculum_tolerantia', 'low', 'low', 'indicated', 'full', NULL),
('failed_magistrate','negotiatio', 'medium', 'medium', 'indicated', 'full', NULL), ('failed_magistrate','negotiatio', 'medium', 'medium', 'indicated', 'full', NULL),
('failed_magistrate','litterae', 'high', 'high', 'indicated', 'full', NULL), ('failed_magistrate','litterae', 'high', 'high', 'indicated', 'full', NULL),
('failed_magistrate','officia_burden', 'high', 'high', 'indicated', 'partial', NULL), ('failed_magistrate','officia_burden', 'high', 'high', 'indicated', 'partial', NULL),
-- CAMP LOGISTICIAN -- CAMP LOGISTICIAN
('camp_logistician', 'auctoritas', 'low', 'low', 'indicated', 'partial', NULL), ('camp_logistician', 'auctoritas', 'low', 'low', 'indicated', 'partial', NULL),
('camp_logistician', 'clientela', 'low', 'low', 'indicated', 'partial', 'Supply chain contacts, narrow'), ('camp_logistician', 'clientela', 'low', 'low', 'indicated', 'partial', 'Supply chain contacts, narrow'),
('camp_logistician', 'liquiditas', '180', '180', 'measured', 'full', 'Denarii — steady savings'), ('camp_logistician', 'liquiditas', '180', '180', 'measured', 'full', 'Denarii — steady savings'),
('camp_logistician', 'fama', 'neutral','neutral','indicated', 'partial', NULL), ('camp_logistician', 'fama', 'neutral','neutral','indicated', 'partial', NULL),
('camp_logistician', 'disciplina', 'high', 'high', 'indicated', 'full', NULL), ('camp_logistician', 'disciplina', 'high', 'high', 'indicated', 'full', NULL),
('camp_logistician', 'mercatus_scientia', 'high', 'high', 'indicated', 'full', 'Bulk goods, logistics pricing'), ('camp_logistician', 'mercatus_scientia', 'high', 'high', 'indicated', 'full', 'Bulk goods, logistics pricing'),
('camp_logistician', 'itineris_scientia', 'high', 'high', 'indicated', 'full', NULL), ('camp_logistician', 'itineris_scientia', 'high', 'high', 'indicated', 'full', NULL),
('camp_logistician', 'ius_accessus', 'medium', 'medium', 'indicated', 'partial', NULL), ('camp_logistician', 'ius_accessus', 'medium', 'medium', 'indicated', 'partial', NULL),
('camp_logistician', 'periculum_tolerantia', 'medium', 'medium', 'indicated', 'full', NULL), ('camp_logistician', 'periculum_tolerantia', 'medium', 'medium', 'indicated', 'full', NULL),
('camp_logistician', 'negotiatio', 'medium', 'medium', 'indicated', 'full', NULL), ('camp_logistician', 'negotiatio', 'medium', 'medium', 'indicated', 'full', NULL),
('camp_logistician', 'litterae', 'medium', 'medium', 'indicated', 'full', NULL), ('camp_logistician', 'litterae', 'medium', 'medium', 'indicated', 'full', NULL),
('camp_logistician', 'officia_burden', 'low', 'low', 'indicated', 'partial', NULL), ('camp_logistician', 'officia_burden', 'low', 'low', 'indicated', 'partial', NULL),
-- GUILD SCRIBE -- GUILD SCRIBE
('guild_scribe', 'auctoritas', 'low', 'low', 'indicated', 'partial', NULL), ('guild_scribe', 'auctoritas', 'low', 'low', 'indicated', 'partial', NULL),
('guild_scribe', 'clientela', 'low', 'low', 'indicated', 'partial', 'Document and account network'), ('guild_scribe', 'clientela', 'low', 'low', 'indicated', 'partial', 'Document and account network'),
('guild_scribe', 'liquiditas', '120', '120', 'measured', 'full', 'Denarii — modest, careful'), ('guild_scribe', 'liquiditas', '120', '120', 'measured', 'full', 'Denarii — modest, careful'),
('guild_scribe', 'fama', 'neutral','neutral','indicated', 'partial', NULL), ('guild_scribe', 'fama', 'neutral','neutral','indicated', 'partial', NULL),
('guild_scribe', 'disciplina', 'medium', 'medium', 'indicated', 'full', NULL), ('guild_scribe', 'disciplina', 'medium', 'medium', 'indicated', 'full', NULL),
('guild_scribe', 'mercatus_scientia', 'medium', 'medium', 'indicated', 'full', 'Account knowledge, not market instinct'), ('guild_scribe', 'mercatus_scientia', 'medium', 'medium', 'indicated', 'full', 'Account knowledge, not market instinct'),
('guild_scribe', 'itineris_scientia', 'low', 'low', 'indicated', 'full', NULL), ('guild_scribe', 'itineris_scientia', 'low', 'low', 'indicated', 'full', NULL),
('guild_scribe', 'ius_accessus', 'medium', 'medium', 'indicated', 'partial', 'Document access, limited enforcement'), ('guild_scribe', 'ius_accessus', 'medium', 'medium', 'indicated', 'partial', 'Document access, limited enforcement'),
('guild_scribe', 'periculum_tolerantia', 'low', 'low', 'indicated', 'full', NULL), ('guild_scribe', 'periculum_tolerantia', 'low', 'low', 'indicated', 'full', NULL),
('guild_scribe', 'negotiatio', 'low', 'low', 'indicated', 'full', NULL), ('guild_scribe', 'negotiatio', 'low', 'low', 'indicated', 'full', NULL),
('guild_scribe', 'litterae', 'high', 'high', 'indicated', 'full', NULL), ('guild_scribe', 'litterae', 'high', 'high', 'indicated', 'full', NULL),
('guild_scribe', 'officia_burden', 'low', 'low', 'indicated', 'partial', NULL); ('guild_scribe', 'officia_burden', 'low', 'low', 'indicated', 'partial', NULL);
-- ============================================================ -- ================================================================
-- TABLE: schema_changelog -- TABLE: schema_changelog
-- Documents schema version history. -- Documents schema version history.
-- Required: if this database becomes a read-only Simulator -- Required: if this database becomes a read-only Simulator
-- layer, the Simulator must be able to identify schema version -- layer, the Simulator must be able to identify schema version
-- and adapt its reader accordingly. -- and adapt its reader accordingly.
-- ============================================================ -- ================================================================
CREATE TABLE IF NOT EXISTS schema_changelog ( CREATE TABLE IF NOT EXISTS schema_changelog (
version INTEGER PRIMARY KEY, version INTEGER PRIMARY KEY,
description TEXT NOT NULL, description TEXT NOT NULL,
applied_at TEXT NOT NULL -- ISO 8601 UTC applied_at TEXT NOT NULL -- ISO 8601 UTC
); );
INSERT OR IGNORE INTO schema_changelog VALUES INSERT OR IGNORE INTO schema_changelog VALUES
(1, 'OTIVM-I: JSON save files (not SQLite — recorded here for continuity)', '2026-04-25T00:00:00Z'), (1, 'OTIVM-I: JSON save files (not SQLite — recorded here for continuity)', '2026-04-25T00:00:00Z'),
(2, 'OTIVM-II: JSON save files with TESSERA map integration', '2026-04-28T00:00:00Z'), (2, 'OTIVM-II: JSON save files with TESSERA map integration', '2026-04-28T00:00:00Z'),
(3, 'OTIVM-III: Per-player SQLite schema, parameter registry v1', '2026-05-02T00:00:00Z'); (3, 'OTIVM-III: Per-player SQLite schema, parameter registry v1', '2026-05-02T00:00:00Z'),
(4, 'OTIVM-IV: Add session_anchor_at to actor_profile for simulation clock','2026-05-02T00:00:00Z');
-- ============================================================ -- ================================================================
-- END OF SCHEMA -- END OF SCHEMA
-- To initialise a player database: -- To initialise a player database:
-- sqlite3 data/saves/{actor_id}.sqlite3 < data/create_player_db.sql -- sqlite3 data/saves/{actor_id}.sqlite3 < data/create_player_db.sql
@@ -378,4 +399,9 @@ INSERT OR IGNORE INTO schema_changelog VALUES
-- sqlite3 data/saves/{actor_id}.sqlite3 "PRAGMA integrity_check;" -- sqlite3 data/saves/{actor_id}.sqlite3 "PRAGMA integrity_check;"
-- sqlite3 data/saves/{actor_id}.sqlite3 "SELECT COUNT(*) FROM background_starting_values;" -- sqlite3 data/saves/{actor_id}.sqlite3 "SELECT COUNT(*) FROM background_starting_values;"
-- Expected: 72 rows (12 parameters × 6 backgrounds) -- Expected: 72 rows (12 parameters × 6 backgrounds)
-- ============================================================ --
-- Note on venture_legs.duration_days and delay_days:
-- Declared INTEGER, not REAL. This enforces the integer clock
-- constraint from docs/architecture/simulation-clock.md.
-- A fractional value here is a schema violation.
-- ================================================================