Calibration Arm D — Frozen Joined CSV Schema¶
Status: Frozen at JOINED_SCHEMA_VERSION = "joined.v1"
Canonical source: swarm/calibration/joined.py
Pre-registration: docs/research/calibration-prereg.md#arm-d-freeze-joined-csv-schema
Contract: Adaptive agents arms 1–3 and all downstream studies join calibration results against this schema by run_id.
Schema Overview¶
The joined CSV contains one row per accepted interaction from a calibration run. Each row merges:
- Proxy-side data (from arm D's inline ProxyComputer pass): the proxy's v_hat and p_hat, the interaction's ground-truth label p_true, and metadata.
- Judge-side data (from arm B's judge ensemble): per-judge scores and rationales.
The schema deliberately separates proxy and judge signals so downstream consumers can ask: "where does the proxy track ground-truth but judges diverge?" — i.e., where the proxy may be fooled.
Frozen Base Columns (9 columns)¶
These columns are immutable as part of the joined.v1 contract. Any rename or reorder must bump JOINED_SCHEMA_VERSION and will break downstream consumers loudly.
| Column | Type | Nullability | Format | Provenance | Description |
|---|---|---|---|---|---|
interaction_id |
string | not null | UUID (string repr) | Arm D (inline proxy) | Unique identifier for the interaction. Generated by the fixture when the scenario is created; matches by construction because proxy and judges run in the same process. |
scenario |
string | not null | "mixed", "obfuscation", or "self_optimizer" |
Arm D config | Scenario type; defines which interaction generator was used. Fixed at run time via --scenario flag. |
seed |
integer | not null | non-negative int | Arm D config | Random seed for scenario generation; ensures reproducibility of the interaction pool. Fixed at run time via --seed flag. |
interaction_type |
string | not null | interaction type name (e.g., "REPLY", "TRADE") |
Arm D (interaction object) | Type of interaction. Extracted from interaction.interaction_type.name (or stringified if not an enum). |
accepted |
integer | not null | 0 or 1 |
Arm D (interaction object) | Whether the interaction passed the acceptance filter (1 = accepted, 0 = rejected). Only accepted interactions are included by default (accepted_only=True in build_proxy_rows). Formatted as integer in CSV. |
p_true |
float | not null | Decimal, 6 places (e.g., 0.753421) |
Arm D (interaction object) | Ground-truth probability that the interaction is beneficial, P(v = +1). The latent label the entire calibration study anchors to. Extracted from interaction.p; missing p raises AttributeError (no silent 0.5 fallback). |
v_hat |
float | not null | Decimal, 6 places (e.g., -0.234567) |
Arm D (ProxyComputer) | Raw proxy score before sigmoid normalization, in [-1.0, +1.0]. Computed from observables (task_progress_delta, rework_count, verifier_rejections, tool_misuse_flags, counterparty_engagement_delta) via ProxyComputer.compute_labels(). |
p_hat |
float | not null | Decimal, 6 places (e.g., 0.417890) |
Arm D (ProxyComputer) | Normalized proxy probability estimate, sigmoid(k * v_hat), in [0.0, 1.0]. The proxy's assessed likelihood the interaction is beneficial. k (sigmoid temperature) is configurable; default is 1.0. |
ground_truth |
integer | nullable | -1, +1, or empty string |
Arm D (interaction object) | Realized outcome label, sampled from Bernoulli(p_true). Extracted from interaction.ground_truth; empty string if absent. Represents the latent v ∈ {-1, +1} that p_true was the probability of. |
Dynamic Judge Columns (paired per judge)¶
For each judge in --judges <judge1> <judge2> ..., two columns are appended in the order judges appear:
| Column Pattern | Type | Nullability | Format | Provenance | Description |
|---|---|---|---|---|---|
judge_{judge_name}_score |
float | nullable | Decimal, 6 places, or empty string | Arm B (LLM judge) | Judge's quality score for the interaction, in [0.0, 1.0]. Empty string if the judge did not rate this interaction (e.g., sampling strategy or error). Judged against rubric rubric.v1 (pre-registered) unless overridden. |
judge_{judge_name}_rationale |
string | nullable | Free text, CSV-escaped | Arm B (LLM judge) | Judge's brief written justification for the score. Empty string if absent or if the judge did not rate this interaction. |
Judge names are drawn from --judges argument at run time. Known backends:
- "mock" — deterministic mock judge (no API calls)
- "claude" — Anthropic Claude (via ANTHROPIC_API_KEY)
- "gpt4o_mini" — OpenAI GPT-4o-mini (via OPENAI_API_KEY)
- "llama" — Ollama Llama 3.x (running locally)
- "qwen", "mistral", "glm" — Other Ollama models
Join Key and Reproducibility¶
- Join key:
run_id(implicit from the run directory name; not a column). - Downstream studies load the CSV from
runs/<timestamp>_calibration_join_seed<seed>/joined.csv. - Join on
(scenario, seed, interaction_id)to correlate with other tables. -
No direct
run_idcolumn, butseedis stable across multiple runs of the same scenario. -
Reproducibility guarantees:
- Same
scenario + seedreproduces identicalinteraction_ids andp_truevalues. - Same
scenario + seed + judge backendreproduces identical judge scores (deterministic on mock; LLM judges may vary). - The
config.jsonin the run directory recordsschema_version, judge models, rubric versions, and git rev for full auditability.
Example Row¶
For a hypothetical run with judges ["mock", "claude"]:
interaction_id,scenario,seed,interaction_type,accepted,p_true,v_hat,p_hat,ground_truth,judge_mock_score,judge_mock_rationale,judge_claude_score,judge_claude_rationale
550e8400-e29b-41d4-a716-446655440000,obfuscation,42,REPLY,1,0.753421,0.234567,0.558962,1,0.750000,agent_type=honest,0.720000,"Low effort visible in verbiage."
Nullability and Empty Cell Handling¶
p_true,v_hat,p_hat,scenario,seed: Never empty; these are core contract fields.ground_truth: Empty string ("") wheninteraction.ground_truth is None. This is valid and expected for synthetic scenarios.judge_*_score: Empty string when a judge did not rate the interaction (e.g., stratified sampling, or cross-scenario evaluation of a pre-existing arm B run). Consumers must handle gracefully.judge_*_rationale: Empty string when the rationale is absent (standard for mock judge; some LLM judges may omit this field).
Adaptive studies must treat empty judge cells as missing data, not as zeros.
Data Types and Format Precision¶
- Floating-point fields (
p_true,v_hat,p_hat,judge_*_score): All rendered with exactly 6 decimal places (e.g.,0.750000). When re-loading viacsv.DictReader, parse withfloat()to recover full precision. - Integer fields (
scenario,seed,interaction_type,accepted,ground_truth): Rendered as their natural string representation. acceptedis an integer (0or1), not a boolean string.ground_truthis an integer (-1or+1) or empty string, never"null"or"None".
Provenance Summary¶
| Data | Arm | Computed Where | Reference |
|---|---|---|---|
interaction_id, p_true, ground_truth |
D (fixture) | Interaction generator | tests/fixtures/interactions.py |
scenario, seed, interaction_type, accepted |
D (config + fixture) | calibration_join.py main |
Passed via --scenario / --seed; read from interaction object |
v_hat, p_hat |
D (inline proxy) | ProxyComputer.compute_labels() |
swarm/core/proxy.py |
judge_*_score, judge_*_rationale |
B (external anchor) | Judge backend (MockJudge or LLMJudge) | swarm/judges/judge.py; scores read from judge.score(view) verdict |
Spec / Code / Prereg Deltas¶
Observed Discrepancies¶
- Pre-reg mentions
agreement_metricsin the CSV; code does not include them. - Pre-reg text: "The deliverable is a CSV of accepted interactions with
{v_hat, p, ground_truth, judge_score_claude, judge_score_gpt4o_mini, judge_score_llama, agreement_metrics}that downstream studies can join against by run-id." - Code reality:
agreement_metrics(Krippendorff's α, ICC, Spearman ρ, disagreement-by-bin) are not columns in the joined CSV. They are computed by arm C (inter-rater agreement) and written to separate outputs:runs/<ts>_calibration_agreement/summary.csvandruns/<ts>_calibration_agreement/bins.csv. - Rationale: Agreement metrics are aggregate properties of the judge ensemble, not per-interaction properties. Including them as repeated columns in the joined CSV would wastefully replicate the same values in every row.
-
Impact: Downstream consumers must join the joined CSV with arm C's summary if they need agreement statistics. Separate runs required.
-
No
"run_id"or"rubric_version"column in the joined CSV. - Pre-reg implied: The joined CSV is the "frozen schema downstream studies join against by run_id," suggesting
run_idmight be a column. - Code reality:
run_idis implicit (read from the directory name); not a CSV column.rubric_versionis not included because all rows in a run use the same rubric. - Rationale: Immutable columns (scenario, seed, interaction_type, accepted) plus run-directory metadata are sufficient for joins. Adding redundant columns violates normalization and would force the schema to change if rubric policies evolve.
-
Impact: Downstream consumers must track which run directory (and thus which rubric version) each CSV came from via their own index. The run's
config.jsonrecords the rubric version for auditability. -
Column order is significant.
- Code enforcement: BASE_COLUMNS is a frozen tuple in
swarm/calibration/joined.py. Tests assert it cannot be reordered without bumpingJOINED_SCHEMA_VERSION. - No pre-reg specification: The pre-reg lists column names but does not mandate order. However, the code intentionally freezes order to prevent accidental silent breaking changes.
- Impact: Consumers relying on positional indices must verify they match the schema version.
Breaking Risks (Adaptive Study Integration)¶
The adaptive studies (arms 1–3) join the joined CSV by (scenario, seed, interaction_id) to augment their own runs with the calibration anchor. Potential breaking scenarios:
- Removing
interaction_id: Adaptive studies cannot join without it. Risk: CRITICAL. - Removing or renaming
p_true: The entire study's ground-truth signal is lost. Risk: CRITICAL. - Removing judge columns: Studies lose the external quality anchor. Risk: MAJOR. Partial removal (fewer judges) is acceptable as long as downstream code handles missing columns gracefully.
- Reordering BASE_COLUMNS: Positional-index consumers will fail silently. Mitigated by: Test assertion on schema version.
- Empty rows on stratified sampling: Arm B samples interactions (≥50 per bin), not all. Some rows in the joined CSV will have empty judge cells. Mitigation: Consumed by downstream as "unrated by this judge," not as an error. Essential for cost control on LLM judge runs.
- Different rubric versions: Arm B is pre-registered to
rubric.v1; runs withrubric.v2or later are recorded as pre-reg deviations. Downstream consumers must not assume all rows use the same rubric unless they verify viaconfig.json. Mitigated by: config.json records rubric version and deviation status.
Freeze Readiness¶
Schema is frozen and code is production-ready. The joined CSV is emitted by experiments/calibration_join.py (arm D) with version-locked structure. Tests in tests/test_calibration_joined.py verify:
- ✅ Schema version assertion (joined.v1)
- ✅ BASE_COLUMNS immutability (tuple, frozen order)
- ✅ CSV round-trip (write → read via DictReader → consistency)
- ✅ Missing judge handling (empty cells, not nulls)
- ✅ Missing ground_truth handling (empty string, not null)
Ready for adaptive study integration when:
- ✅ Arm A (proxy fidelity) is run and optimal k is selected.
- ✅ Arm B (judge scoring) produces judge_scores.csv at sufficient scale (≥50 per bin, stratified across scenarios).
- ✅ Arm C (inter-rater agreement) verifies judges converge (α ≥ 0.5).
- ⏳ Arm D joins and validates the CSV schema (this document).
Remaining before declaring final schema frozen: - Confirm adaptive study code can parse the joined CSV without errors (test load from a real arm B/D run). - Verify downstream pre-reg joiners (adaptive arms 1–3) match column expectations (search for hard-coded column names or assumptions in adaptive runner code). - Document any adaptive-study-specific post-processing (e.g., per-bin aggregation of judge scores for toxicity metrics).
References¶
- Code:
swarm/calibration/joined.py(BASE_COLUMNS, JoinedRow, to_row) - Runner:
experiments/calibration_join.py(writes joined.csv) - Tests:
tests/test_calibration_joined.py(schema assertions, CSV round-trip) - Pre-registration:
docs/research/calibration-prereg.md#arm-d-freeze-joined-csv-schema - Arm A (fidelity):
experiments/calibration_fidelity.py,swarm/calibration/fidelity.py - Arm B (judges):
experiments/calibration_judge.py,swarm/judges/judge.py - Arm C (agreement):
experiments/calibration_agreement.py,swarm/judges/__init__.py(run_agreement) - Adaptive pre-reg:
docs/research/adaptive-agents-prereg.md(describes join strategy and confounds)