Skip to content

Anatomy of a Study

A study is the unit of work in SocioVerse: one research question, one directory under studies/<study_id>/, a fixed set of artifacts. Every stage of the workflow writes exactly one artifact here, so the directory is the study's state — inspectable, editable, diffable.

studies/<study_id>/
├── study.yaml                      # the StudySpec — question, horizon, metrics, discovery fields
├── grounding/
│   └── grounding.json              # facts + sources + declared assumptions (the provenance sidecar)
├── model.py                        # (from-scratch studies) the four interface implementations
├── environment/
│   └── environment.json            # E — layers, scheduled interventions, broadcasts
├── population/
│   ├── population.json             # P — personas, count, interaction structure, propagation
│   └── roster.jsonl                # the instantiated agents' t=0 state
├── simulation/
│   └── simulation.json             # run config — decision/collector/store refs, rounds, warm start
├── trajectory/
│   ├── study.duckdb                # the durable panel/metrics/events store
│   └── metrics_history.json
├── reports/
│   └── report.md                   # rendered trajectory + figures
├── resources.json                  # which external capabilities this study actually used (by name)
├── versions.json                   # version tree (parents, notes) — written by /sv-iterate
└── versions/                       # v1/, v2/, … full snapshots (local history, gitignored)

The artifacts, stage by stage

study.yaml — the StudySpec

Written by /sv-init, extended by the build stages. It is the only artifact a human reads end-to-end: the question, the clock, the metrics, and the metadata that makes the study findable. Schema: socioverse/schemas/study.py.

Identity & question

field type default meaning
study_id str required the workspace directory name; embedded in every artifact and in every DuckDB row
title / research_question / hypothesis str "" the question, in the user's own language
title_i18n / research_question_i18n / hypothesis_i18n dict[str, str] {} {zh, en} variants; the dashboard picks the viewer's language and falls back to the plain field
study_type "longitudinal" | "cross_sectional" longitudinal panel-over-time vs one-shot

The clock

field type default meaning
n_steps int 5 the horizon
seed int 42 the run seed
time_unit str "abstract" what wall-clock time one step advances: day / week / month / quarter / year, or abstract when no calendar mapping fits. A free string — not validated
step_meaning str "" one human line tying a step to the research question, e.g. "每步=1个月的消费决策周期"

Metrics

field type default meaning
metrics list[str] [] the metric names the run must emit (the collector's columns() has to cover them)
metric_descriptions dict[str, str] {} one line per metric — what it measures, plus unit/direction; rendered on the dashboard cards and as timeline-legend tooltips
display_metrics list[str] [] the 3–5 metrics the dashboard timeline actually plots, so bookkeeping metrics don't drown the story; empty falls back to the first five of metrics
run_note dict[str, str] {zh, en} {} shown on the run card when the study's run happens in an external/heavy pipeline — "clone and run this locally, not in the hosted service"

Artifact refs — relative paths inside the workspace; the defaults are the conventional layout and rarely change.

field default
environment_ref environment/environment.json
population_ref population/population.json
simulation_ref simulation/simulation.json
resources_ref resources.json

Discovery / catalog — what sv-init matches a new query against.

field type default meaning
domain str "" primary routing key, e.g. urban-segregation
tags list[str] [] secondary routing keys, e.g. ["ABM", "Schelling"]
legacy_simulator str "" the wrapped engine's path or name, or "from_scratch" for a Core-native study
provider_refs list[str] [] the registry keys this study registers
adjustable_params list[str] [] what a fork may vary by editing artifacts only — no code. This is the surface a new question is matched against
status str "draft" conventionally draft / demo-only / parity-tested
maintainer str "" who owns this study

Reference / teaching

field type default meaning
demonstrates list[str] [] which reusable patterns this study shows — a controlled kebab-case vocabulary, matched as exact strings (see below)
teaches str "" one line: what you'd copy this study to learn
reference bool False a read-only baseline: it may be forked and read as a template, never edited in place
created_by str "sv-init" forks get "sv-init (fork of <src>)"

The demonstrates vocabulary, extended deliberately:

legacy-seam · from-scratch-core · external-events · real-user-pool
cross-sectional-survey · warm-start · multi-round-messaging
geo-spatial · policy-intervention · info-broadcast

Because these are matched as exact strings, the build skills query the catalog for capabilities (reference: true and from-scratch-core) instead of hard-coding template names — the shipped demo set can change without touching a single skill.

Together the discovery and reference fields form the catalog that sv-init routes new queries against: if an existing study already covers a question, it is forked and adjusted rather than rebuilt.

Anchor the step in time

time_unit + step_meaning are two small strings that decide whether a trajectory reads as time or as abstract ticks. A reader looking at step 7 must know whether that is a week in, or seven years in — and the model's own per-step magnitudes have to agree with the answer.

Pick the scale the real behaviour unfolds on, then make every rate consistent with it:

the behaviour plausible time_unit what must then match
a canteen-vs-delivery meal choice month monthly budget, monthly price drift
an election-opinion shift week weekly media cycle, weekly decay
a housing relocation year annual rent growth, annual move probability

abstract is legitimate — but only when no calendar mapping is meaningful (the step is a pure decision round), and then step_meaning must say so and justify it.

Don't silently default to abstract

time_unit defaults to "abstract" and is not validated, so a study that never sets it looks identical to one that deliberately chose it. /sv-build-model now decides the unit before writing any per-step magnitude. These two fields are recent additions and several of the bundled studies predate them — treat a missing step_meaning on an old study as "not yet stated", not as "deliberately abstract".

grounding/grounding.json — the provenance sidecar

Bootstrapped by /sv-init (a real-world search always runs), then consulted and extended by every build stage. Each entry is a fact with a source, a modeling reference, or a declared assumption (basis: sourced / proxy / assumed). The rule is ground before you invent: load-bearing E and P values cite a fact id or an assumption id.

Deliberately not a Pydantic handoff artifact — it is a flat, merge-friendly JSON that every stage appends to and the dashboard renders read-only. The shape is a convention, enforced by skills/sv_grounding.py rather than by a schema:

field shape
study_id, query, updated_at the study, the originating question, the last merge timestamp
method_notes free text; the sentinel value "stylized" has a specific meaning (below)
implementation_refs[] {title, url, takeaway, accessed} — how similar phenomena are modeled elsewhere
facts[] {id, claim, value, unit, as_of, basis, source, applies_to, note}
assumptions[] {id, claim, rationale}no basis: an assumption is its own basis

Inside a fact:

field values
basis sourced | proxy | assumed
source {title, url, via, accessed, local_path?}local_path points at a downloaded table under grounding/data/
source.via event_service | web_search | provider | user
applies_to the artifact paths this fact backs, e.g. ["environment.provider_args.base"]

Facts and assumptions upsert by id (missing ids get f<N> / a<N>), so a later stage can revise an entry without duplicating it, and a study's constants can cite a stable id in a trailing comment.

stylized is a decision, not a gap

method_notes: "stylized" with an empty facts list is a deliberate record that this study has no real-world anchors — an abstract or toy model whose constants need no fact ids. That is a different state from grounding merely being absent, and the tooling reports it differently ("stylized model, no real-world anchors" vs "no grounding recorded").

environment/environment.json — E

Written by /sv-build-environment. Declares the environment layers (the two-axis state), the scheduled interventions (exogenous changes at given steps), and the information broadcasts with their audiences ("all" → everyone's macro-information quadrant; a selector → matching agents' local quadrant).

population/ — P

Written by /sv-build-population. population.json describes the personas, the pool size, the interaction structure (who can see whom) and the propagation mode. The pool is materialized at build timeroster.jsonl holds every instantiated agent's t=0 state, so you can review the exact population before spending a single simulation step.

field values meaning
personas[] {agent_id, attributes, group_key, weight, init_state} agent_id is the persistent longitudinal key and must be unique — the schema rejects duplicates
materialized_count int \| None the true agent count, authoritative when personas is left empty for large pools; None = not yet materialized
interaction.kind spatial_adjacency | explicit_network | none who can see whom — explicit_network carries edges, spatial_adjacency a provider-computed adjacency_ref
propagation independent | contagion | broadcast_then_local how influence flows: on E only, via neighbour states, or a macro shock followed by local diffusion

Large pools

For big populations the personas list inside population.json stays empty and roster.jsonl is the source of truth for the materialized agents — materialized_count is then the only reliable count.

propagation is a declarative tag

The engine does not read it. It documents intent for the study's own environment implementation and labels the population card on the dashboard; the actual influence path is whatever observe_batch puts in the agents' information quadrants. See Multi-Round Interaction.

simulation/simulation.json — the run config

Written by /sv-run at preflight. Binds the pieces by registry key (decision_ref, collector_ref, store_ref, plus their *_args), repeats n_steps / seed, and carries two things worth knowing:

  • interaction_rounds (default 1) — how many intra-step interaction rounds the loop runs before the panel row is written. 1 for move-type studies; K > 1 for discussion and contagion. See Multi-Round Interaction.
  • warm_start — set by /sv-iterate to inherit a parent version's already-run steps instead of paying for them again. Requires interaction_rounds == 1. See Iterate, Versions & Reports.

trajectory/ — the run output

Written by /sv-run. study.duckdb is a queryable store with the panel rows, the aggregate metrics, and the fired events:

SELECT step, state FROM panel WHERE agent_id = '…' ORDER BY step;
SELECT * FROM metrics ORDER BY step;
SELECT step, note FROM events ORDER BY step;

reports/ — the rendered result

Written by /sv-report: per-step visuals, a metric timeline with intervention markers, and report.md — including a "data basis & references" section rendered from the grounding sidecar.

versions.json + versions/ — history

Written by /sv-iterate, the only entry point for changing an existing study. Each iteration can snapshot the live directory to versions/vN/ (artifacts + code + results) before changing anything, and versions.json records the parent tree — including branches from older versions. See Iterate, Versions & Reports.

Rules of the directory

Two invariants

  1. Never edit a reference study (reference: true) or any existing study in place — reuse forks, changes go through /sv-iterate.
  2. Run outputs are local: versions/, runs/, *.duckdb are gitignored history, not repository content.