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 time —
roster.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(default1) — how many intra-step interaction rounds the loop runs before the panel row is written.1for move-type studies;K > 1for discussion and contagion. See Multi-Round Interaction.warm_start— set by/sv-iterateto inherit a parent version's already-run steps instead of paying for them again. Requiresinteraction_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
- Never edit a reference study (
reference: true) or any existing study in place — reuse forks, changes go through/sv-iterate. - Run outputs are local:
versions/,runs/,*.duckdbare gitignored history, not repository content.