Skip to content

Changelog


v1.1 — Seneca Polycentric + Pollution Monitoring (in progress)

Polycentric governance under calibrated Seneca dynamics. Research question: does a meta-agent that observes the hidden pollution coordinate \(P\) avert the cascade collapse that \(P\)-blind player agents cannot diagnose? Branch v1.1-dev opened 2026-05-28 from e4d658a (= v1.0 tag). Scope and plan: V1.1_PLAN.md.

A — Foundations

  • A1 — RCPAgent.baseFraction via strategyParams (d5f0445). Exposes the base harvest fraction as a tunable parameter instead of the hard-coded 0.4 (calibrated for logistic, wrong for Seneca).
  • A2 — Seneca calibration document (fc4af66). docs/experiments/seneca_calibration.md records the S0 sweep and cross-references RCPAgent comments.

S — Seneca experiments

  • S0 — Seneca calibration sweep (6f096c4). baseFraction = 0.035 for 5 RCP × R₀=1.0 canonical Seneca (seed 17, 29 turns in band).
  • S1 — Seneca standalone pilot (8bb3d2b). Mule late entry halves the cliff (24→12); pact without enforcer logs 2 violations but does not save. Mule wins in wealth. Includes E1 (setPollution) and L3 (pollution link).
  • S2 — Polycentric pollution cascade (646c27e). Pollution from arena A strangles arenas B/C: −76% capital, collapse 16 turns earlier, P 7.1× baseline. First consumer of L3 + E1.
  • S3 — Governance P-aware (2617eca). Three levers (cap/levy/fine) × two triggers (pollution/capital). Capital-triggered cap is the win-win: prevents cascade AND saves the emitter. OFAT robustness (9880847): claims A/C 20/20, claim B 5/5 on k1/bf/linkRate, 3/5 on dt (cedes at dt ≥ 3.5; baseline dt=3.0 is the edge).

E — Engine extensions

  • E1 — SenecaEngine.setPollution(p) (62495ff). Mirrors the existing setStock setter.
  • E2 — setCapital + capital_shock (in 2617eca). New perturbation type for the levy instrument (direct action on the capital ODE variable \(C\)).
  • L3 — Link.type='pollution' (da42aee). Directional pollution cascade in _applyLinks: emitter's \(P\) is injected into receiver arenas' Seneca state each world round.

P — Publications (tracked on dev branch)

Papers 01–03 reworked, rendered, and tracked on v1.1-dev:

  • paper_01 — signal-adaptive rework, V5/V6 appendix, 2 figures.
  • paper_02 — Cell C decomposition, 2×2 factorial, Bayesian CI.
  • paper_03 — Seneca polycentric (full draft, §1–§6, 21 BibTeX, alive-lineage Bardi, OFAT robustness figures, code appendix).

v1.0 — Nested Arenas / Polycentric (tagged 2026-05-28)

Cross-arena federation. 27 commits, tag v1.0 on e4d658a, merged to main. Research question: does Vasconcelos' polycentric governance result survive in BDPD when commons are coupled by resource flow and governed by a meta-agent? Scope and plan: V1.0_PLAN.md.

W — World scaffold

  • W1 — platform/world.js (53f1ca7). World, Link, Treaty classes. A World holds N arenas + links + treaties + a global clock.
  • W2 — Super-scheduler (4e55e59). world.tick() + runRounds()
  • aggregated rollup across arenas.
  • W3 — Storage paths (9b57f97). Arena data lands under real worldId; default bucket for back-compatibility.
  • W4 — World API (39510ca). /api/v1/worlds/:wid REST surface.
  • L1 — Link.type='resource' (a5792be). End-of-round resource transfer between linked arenas (constant rate).

C — Governance agents

  • C3.a — Arena-level governance_agent (a63b732). role: 'meta' agent type: non-harvesting, observes state each turn, returns perturbation specs. Registry: noop, sanctioner.
  • C3.b — World-level governance_agent (2bb12bf). WorldSanctionerAgent covers all arenas in a federation.

B — Federation features

  • B1 — Agent migration (6ceaf29). Move an agent between arenas within a world (wealth preserved).
  • B2 — Cross-arena messaging (18051ed). Federated send_private across arenas within a world.

D — Science

  • D1 — Cliff polycentric pilot (b269956). Functional verification of the W+L+C stack on the canonical cliff setup.
  • D2 — Mule polycentric pilot (4d46b22). L1 + sanctions stack; Mule relocation dynamics.
  • D3 — Yoon-style voluntary sanctioner (f7ee90d). Discretionary sanctioning with triggerStockRatio + repeatThreshold.
  • D4 — Treaty enforcer (ef46d0e). Cross-arena treaty with harvest_cap_per_arena_per_round enforcement.
  • D5 — Multi-source exclusion (fbde331, 6c8be98). exclude perturbation + cluster-of-aggressors sweep (junk purity 1.00).

Docs

  • Standard light/dark theme toggle replaces custom BDPD palette (1a4a91a).

v0.9 — Governance (tagged 2026-05-27)

Communication and sanctioning surface for BDPD. Closes the three "Not directly implemented" rows of the Ostrom table from Paper 0 (§4): pact tracking, cheap-talk surface, sanctioning perturbation. Opened 2026-05-22 on branch v0.9-dev from d63436d. Tag v0.9 on commit 4322aa7. Scope and plan: V0.9_PLAN.md.

MVP Paper 1 critical path completed: A1 + A3 + B1 + B3 + C1 + D1. The branch is citable. P1/P2 items (A2 done; C2/C3/D2/D3/D4 deferred to post-v1.0) enrich but do not block submission.

Headline empirical finding (D1)

Pure cheap-talk does not solve the cliff. In a 6-agent canonical cliff setup (5 conservative + 1 aggressive, K=150, regen=0.12, threshold=10, 30-turn max), enabling cheap-talk for all six LLM agents (broadcast, send_private, announce_intended_harvest, pact_propose, pact_accept) results in earlier collapse (T19, commons → 0) than the builtin baseline (T24, commons 4.4/150). The aggressor wins in wealth (105 vs mean 64) even with cheap-talk on, announcements are honoured (lie_score ≈ 0.001) but communication decays from 8 → 1 messages/turn after T7. This is the Farrell-Rabin negative result that motivates the Phase B → C transition in Paper 1: sanctioning is required.

A — State foundations

  • A1 — PactRegistry (platform/pacts.js). Per-arena pact lifecycle: propose / accept / decline / cancel, term types harvest_cap / commons_floor / reciprocity, automatic violation tracking via checkViolations() called by arena.tick.
  • A2 — ReputationRegistry (P1, included). EMA per-player cooperation score, exposed via query_reputation tool. Mirrored in middle-man for prompt observation.
  • A3 — Governance metrics in METRIC_REGISTRY (engine/metrics.js). Five entries: cooperation_index, lie_score, sanction_rate, announce_frequency, silent_defection. All populated per-turn in turnRecord.governance by governanceSnapshot.

B — Communication (cheap talk)

  • B1a — MessageBus (platform/messages.js). Append-only, recipient-filtered cheap-talk channel with broadcast / private / announcement scopes.
  • B1b — Arena action protocol + SDK cheap-talk tools. Agents return actions[] alongside harvest. SDK exposes broadcast, send_private; inbox surfaced in buildObservation with one-turn lag (matches messages.js filter).
  • B1c — Pact actions on both sides. Arena consumes pact_propose / pact_accept / pact_decline / pact_cancel; SDK exposes the same tool surface. Active pacts in the observation.
  • B1d — announce_intended_harvest as both a tool and a JSON field on the structured response (fallback path: empirically agents prefer the JSON field over the tool when both exist).
  • B3 — Lie score + announce_frequency + silent_defection. Bundle: gap, signal frequency, and the "silent over-harvester" pattern that catches the aggressive archetype (lie_score is dead at honour=100%; silent_defection is the diagnostic for D1).

C — Mechanisms (sanctioning)

  • C1 — sanction perturbation + pact_violation trigger (platform/perturbation.js). New perturbation type (amount + factor penalty model) and trigger kind that fires on each violation recorded by PactRegistry. Smoke + DeepSeek pilot confirm 1:1 violation→sanction wiring in vivo.

D — Science

  • D1 — Cliff under active cheap talk (scripts/pilot_d1.mjs). Side-by-side 2-cell mini-pilot at the canonical cliff. Output: data/pilot/d1/{cell_A_off,cell_B_on,comparison}.json + headline figure experiments/figures/pilot/d1_headline__*.png rendered in all three themes by scripts/plot_d1.py.

Smoke coverage

15 smokes green pre-pilot: 11 Node (smoke_governance_arena, smoke_messages, smoke_pact_actions, smoke_pacts, smoke_reputation, smoke_action_protocol, smoke_b1d1, smoke_b3, smoke_c1, smoke_world_prefix, smoke_b1b2) + 4 Python (smoke_b1b2, smoke_b1c2, smoke_b1d2, smoke_b1d3). Plus 3 optional DeepSeek pilots (pilot_c1, pilot_d1, smoke_b1d_pilot).

Post-tag fixes folded in (originally v0.8.1)

The two fixes below shipped on the dev branch after the v0.8 annotated tag was published. Originally seeded as a v0.8.1 patch; folded into v0.9 (no intermediate patch release cut).

  • fix — §5 #8 storage path world-prefix (36c914c). Arena files now write to data/worlds/default/arenas/<id>/ instead of data/arenas/<id>/, honoring BDPD_ultimate_edition.md §5 decision #8 (nested-safe storage). default is the implicit world_id; v1.0 nested-arena dispatch will populate other worlds without code changes.
  • platform/registry.js: DATA_DIR flipped to nested; dead WORLDS_DATA_DIR/worldsDataDir accessor (zero callers) removed.
  • platform/reporter.js: same flip + updated path comments.
  • scripts/migrate_storage_world_prefix.mjs: idempotent migration with --dry-run; removes empty legacy dir post-move.
  • scripts/smoke_world_prefix.mjs: registry-level smoke (no LLM, no network) verifying writes land at the nested path.
  • Regression: hardin_1968 replica re-run unchanged. REPRODUCIBILITY.md pins lockfile sha256s only (not arena outputs), so no manifest updates needed.

  • perf — raise max_history default 6/8 → 32 (61d7d62). Empirical pilot finding 2026-05-20: DeepSeek prompt-cache hit was ~89% T1-T9 but dropped to ~27% from T10 onwards because deque(maxlen=max_history*2) evicted earlier turns and changed the cache-stable prefix. Default 32 covers 30-turn BDPD games without eviction, keeping the hit rate steady across the full sweep (~3-4× cheaper input tokens for full-game runs). Bumped in three places: agents/bdpd_sdk/agent.py (SDK default), and both middle-man fallbacks plus the --max-history argparse default in agents/bdpd_agent.py. Games > 32 turns reintroduce the eviction; token-budget guard / summarisation-based history (candidato 4 options B/C) is the v0.9+ path.


v0.8 (2026-05-20)

Agent SDK. The release that turns BDPD into a citable testbed for LLM cooperation experiments. Single LLM provider supported and tested: DeepSeek, via its OpenAI-compatible API (deepseek-v4-flash thinking-off as the operational default, deepseek-v4-pro available for thinking-on workloads). See Cloud APIs for the rationale.

bdpd_sdk Python package — the v0.8 deliverable surface

  • New: agents/bdpd_sdk/ — a six-module package that gives BDPD's middle-man (and any notebook user) a stable LLM-agent primitive.
  • client.pymake_client() factory pointed at DeepSeek (DEFAULT_BASE_URL, DEFAULT_MODEL = "deepseek-v4-flash"). Reads DEEPSEEK_API_KEY / DEEPSEEK_BASE_URL with a fall-back to OPENAI_API_KEY so the same env works against any OpenAI-compatible endpoint a contributor might want to target.
  • agent.pyBDPDAgent class: system prompt + per-instance history + chat(user_message, *, memory=None, response_format=None). Rolling history bounded via deque(maxlen=max_history*2). Exposes last_reasoning_content and last_usage per call.
  • memory.pyPlayerMemory dataclass with typed fields (beliefsAboutOthers, plannedDefectionTurn, observedAggressorIntensity, notes). Three writers: deterministic update_from_observation (EMA aggressor intensity), LLM-side apply_updates (partial merge with strict semantics: absent = untouched, null = clear, wrong type = ignored, notes capped at 5 oldest-drop), and direct caller-side mutation.
  • trace.pyTraceWriter appends one JSONL entry per /decide turn with a fixed schema (ts, player_id, model, observation, prompt, response, reasoning_content, harvest, scratchpad_changes, tool_calls, elapsed_seconds, usage). Suitable for post-hoc replay and per-call cache-rate analysis.
  • tools.py@tool decorator with auto-schema from inspect.signature + get_type_hints, supporting primitives, list[primitive], and free-form dict. Internal tool-call loop (max_tool_iterations=4) with optional on_tool_call hook. parallel_tool_calls=False enforced.
  • agents/bdpd_tools.py — two engine-agnostic starter tools wired by the middle-man as closures over per-player TURN_LOG: query_history(turn_start, turn_end) and compute_trend(window). A logistic-only simulate_future was prototyped and dropped — the platform stays engine-agnostic; dynamics belong in the JS engine registry, not in Python tools.

Middle-man (bdpd_agent.py) refactor

  • Refactor: bdpd_agent.py now delegates /decide and /reset to per-player BDPDAgent instances (AGENTS dict). The Flask routes stay thin; all LLM logic lives in the SDK.
  • New: --archetype {aggressive, conservative, adaptive, random} flag injects a behavioural nudge into the system prompt; --trace-file enables TraceWriter JSONL output; --no-memory falls back to bare-number responses.
  • JSON mode: the model is asked to respond {"harvest": N, "scratchpad": {...}} via response_format={"type":"json_object"} (disabled in --no-memory mode and on the tool-emitting iterations of the tool loop, where DeepSeek rejects JSON mode together with tools).
  • Parser robustness (commit 76bac3a): v4-flash narrates first and emits JSON last in ~100% of /decide calls. The legacy parser failed json.loads on the prefix prose and fell back to a bare-number regex that grabbed stock percentages — a silent bug found by the eval-harness pilot. parse_response now uses a three-step ladder: full-text json.loadsJSONDecoder.raw_decode scan for embedded JSON → targeted "harvest": N regex. Pinned by examples/agents/parse_response_smoke.py (14 cases including the exact bug patterns).

DeepSeek v4 migration

  • Migration (commit 517ec00): DEFAULT_MODELdeepseek-v4-flash; all chat.completions.create calls pass extra_body={"thinking":{"type":"disabled"}} because v4 defaults thinking ON and the tool-call loop requires reasoning_content replay across tool_call boundaries that we do not yet plumb. Probing test (examples/agents/probing_test.py) showed thinking is marginal for BDPD's calc-heavy observations anyway — full rationale in memory/project_v08_thinking_utility.
  • Cleanup (commit 8f08756): all docs and the cards code path migrate from legacy deepseek-chat / deepseek-reasoner aliases (deprecated by DeepSeek 2026-07-24) to the explicit v4 names.

Prompt-cache optimisation

  • Perf (commit 08cbd6d): PlayerMemory snap is now baked into the user message instead of being sent as a separate system message between the system prompt and the rolling history. Reason: the snap changes every turn (EMA refresh), and DeepSeek caches on the longest stable prefix. With the snap inline, the prefix [system, *history] stays bit-stable and cache hit rate climbs monotonically with turn count — measured ~89% across turns 1–9 of the pilot (vs ~50% pre-fix). After turn 9 the rolling-history FIFO eviction invalidates the prefix again; a fix for that is registered as a v0.8.1 candidate.
  • Telemetry (commit c66bf91): TraceWriter and BDPDAgent.last_usage now capture the full usage block of every call, including DeepSeek-specific prompt_cache_hit_tokens / prompt_cache_miss_tokens (via usage.model_extra). Enables empirical verification of the cache-prefix fix above.

Platform integration

  • New (commit 07b0558): arena.start() fires a POST /reset at every http-callback agent at kickoff (fire-and-forget, 5 s timeout, errors swallowed as agent_reset_failed events). Reset URL is derived by replacing the last path segment of callbackUrl (/decide/reset, etc.). This guarantees the middle-man's per-player state is purged between arenas without operator intervention. Smoke: examples/agents/smoke_arena_reset.mjs.

Eval harness

  • New (commit 3852dd3): examples/pilot/run_pilot.py — Python orchestrator for v0.8 eval. For each (scenario, mix) cell it spawns 3 bdpd_agent.py middle-man processes (one per archetype in the mix), wires them as http agents in a generated sweep.json, runs node experiments/experiment.js, and saves per-cell summary. Scenarios shipped: hardin_3p (symmetric 30/30/30) and olson_3p (asymmetric 50/10/10). Mixes shipped: balanced (a+c+adp), lone-defender (2a+c), lone-defector (a+2c). Output lands in data/pilot/<cell>/.
  • New (commit c252d8a): --n-runs N on the runner + a compare_runs.py diff tool that reports per-cell determinism at three levels (outcome / harvest-sequence / response-text). Backs the v0.8 deliverable #6 finding: at temperature=0.4 without an explicit seed param, DeepSeek v4-flash is fully non-deterministic — 0/4 cells have identical outcomes between paired runs sharing the same arena seed. Documented in memory/project_v08_seed_determinism.
  • New (commit 3df2a19): examples/agents/probing_test.py — 10-probe × 3-config aptitude test (flash-noThink, flash-think, pro-think). Empirical finding (memory project_v08_thinking_utility): thinking helps only on metacognitive probes (recognising underdetermination); on calc-heavy BDPD-style probes it's neutral or worse (truncation). Default v0.8 = flash-noThink.

Pilot findings (preliminary, n=1)

Three behavioural patterns observed in the v0.8 pilot, recorded in memory/project_v08_pilot_findings for promotion to a proper examples/llm_findings/ showcase in v0.8.1:

  • Cooperation does not scale from n=1. In the balanced mix (1 conservative among 3) the conservative restraint collapses after ~7 turns; in lone-defector (2 conservatives + 1 aggressive) the conservatives hold a steady restraint for all 30 turns. Empirical replica of Janssen & Ostrom (2006) small-group robustness.
  • Aggressive self-modulates for survival. Under the "harvest near full capacity" nudge, the agent ramps up linearly for ~14 turns and then drops to ~50% when the stock plateaus — v4-flash-noThink has enough strategic cognition to read stock_direction = stable and infer "commons in danger" without thinking mode.
  • Safe betrayal under inequality. In olson_3p__lone-defector (1 rich aggressive + 2 poor conservatives), the rich captures 6× more wealth than the two poor combined while the commons survives (ratio 0.34). The rich's restraint at T24-25 keeps the system alive; the poor pay the restraint cost for 23 turns and finish marginal winners only of survival, not of wealth. This is the workshop-citable case for the AI safety angle the v0.8 charter targets.

Provider scope

  • Decision (2026-05-20): v0.8 is built and tested against DeepSeek only. The underlying openai SDK is generic (any OpenAI-compatible endpoint will physically respond), but pricing tables, model recommendations, regression suite, and examples target DeepSeek exclusively. Multi-provider support is out of scope for v0.8 and not on any roadmap before v1.x.

Polish carried over from v0.7

  • requirements.txtpyproject.toml [project.dependencies] migration and a real uv.lock (commit 52bb891) — closes the reproducibility polish flagged in v0.7.

v0.7 (2026-05-19)

Replication Library. The release that turns BDPD from "interesting in the abstract" into "practically useful for anyone arriving with a commons-literature bibliography in hand". Four annotated paper replicas cover the four classical axes of the empirical commons literature; the engine, the docs, and the toolchain are pinned so the same plots will re-render five years from now.

Replication Library

  • New: examples/scenarios/ is a Quarto book with four end-to-end paper replicas, each runnable in under 30 seconds:
    • Hardin (1968)Tragedy of the Commons (full): strategy contrast on a homogeneous 6-agent population, commons-ratio gap 0.000 vs 0.361.
    • Olson (1965)Logic of Collective Action (partial): privileged-member restraint delays collapse 36% and lifts welfare 27%, but tragedy still wins without reciprocation.
    • Janssen & Ostrom (2006)Empirically Based ABMs (full): group-size effect at fixed strategy, N=12 collapses 2.8× faster than N=4 (turn 5 vs 14).
    • Dayton-Johnson (2002)Inequality and Conservation (partial): inequality monotonically extends commons life (turns 11 → 16) and reduces welfare equity (57 → 33); no U-shape on the BDPD built-ins.
  • New: examples/scenarios/REPRODUCIBILITY.md (in the repo, outside the mkdocs nav since it ships next to the Quarto book it documents) is the single source of truth for the canonical toolchain (Node 20, Python 3.12, uv, Quarto ≥ 1.5), the locked dependency files (package-lock.json, requirements.txt) with SHA-256 hashes recorded at release time, and the snapshot tag policy. Single tag v0.7 covers all four scenarios as a unit; per-scenario frozen tags were sketched in the design doc but rejected on the basis that the four scenarios share the same engine and freezing them as a unit is honest about the coupling.
  • New: .tool-versions at the repo root (asdf/mise format) declares nodejs 20 + python 3.12. Hosts using either tool pick the versions up automatically.
  • New: package.json declares engines: { node: ">=20", npm: ">=10" }, matching the Docker base image (node:20-bookworm-slim).
  • Refactor: the four scenarios are organized as a Quarto book (examples/scenarios/_quarto.yml) with docked sidebar (left), shared style file (_style.html), and per-chapter TOC (right). Each scenario directory keeps its standalone notebook.qmd so rendering a single chapter still works (quarto render path/to/notebook.qmd).

Plugin API

  • v1.0 formalized. docs/dev/plugin-api-contract.md is no longer a stub: the contract declares Plugin API version 1.0, bound to the BDPD core version (no separate npm package in v0.7), with a formal semver policy, a deprecation policy (survival ≥ 1 major), an explicit native-plugin trust model ("trusted only, admin-loaded"), and recommended optional manifest fields (version, bdpd_plugin_api, requires, …). The contract supersedes the v0.6.5 stub and resolves the three open questions noted there (engine registry exists, manifest requires is optional/informational in v1.0, native plugin signing deferred to v0.8+).
  • Stub corrections. Two inaccuracies in the v0.6.5 stub fixed retroactively while formalizing:
    • Plugin files export default an object { type, name, ...required }, not a register(registry) function. The loader reads mod.default and dispatches on type.
    • The engines registry does exist (engine/engine-registry.js with ENGINE_REGISTRY + buildEngine()). The plugin loader installs plugin engines via factory: (modelSpec) => BaseEngine keyed by name.

Plugin API > Deprecations

(none in v1.0)

Docker

  • New: HEALTHCHECK clause in Dockerfile probes /api/health every 30 s with a 20 s start-period (covers cold Node boot + plugin load). In practice the container transitions starting → healthy within ~8 s on a warm host.
  • New: docs/getting-started/installation-docker.md is a quickstart additive to the from-source installation.md, not a replacement. Covers prerequisites (with a Manjaro/Arch note on the separate docker-compose plugin package), clone → build → wait healthy → run sweep → down, devcontainer pointer for VS Code users, and explicit non-goals (multi-stage, registry push, signed plugins, GPU passthrough — all deferred to v0.8+).
  • Cross-link: installation.md now links to the Docker quickstart at the top so users discover both paths.

Scope decisions (explicit)

Three originally-planned v0.7 deliverables were dropped with explicit rationale (full discussion in the project design doc BDPD_ultimate_edition.md, kept outside the repo by design — see §15 entry "v0.7-dev: scope cut e chiusura imminente"):

  • Colab badges → deferred to v1.1 only if requested. The Docker quickstart now covers "click-to-run without a native toolchain" with a comparable user experience; Colab would have required a GitHub mirror, four .ipynb wrappers per scenario, and a 3–5 min bootstrap cell per Colab session, with the audience-mismatch that v0.7 targets commons researchers (R / Python / NetLogo natives), not the AI/ML audience for whom Colab is the default.
  • Scenarios 5–7 (Vasconcelos full, Yoon, Perolat) → deferred to their gating-feature release (v1.0 cross-arena observability, v0.9 governance plugin, runner per-turn timeline export respectively). Anticipating any of these with workarounds would produce a partial replica that weakens the library's promise.
  • P1–P11 / CT1–CT6 BDPD sweeps as scenarios → dropped. The canonical sweeps already exist under experiments/, are pinned by manifest, and are in the strict regression suite (5/5 PASS in v0.6.5 carried forward). Reformatting them as examples/scenarios/ entries would be cosmetic.

v0.6.5 (2026-05-19)

Provenance & manifests — the "experiment + figure + paper" packaging gap that ate hours during paper review in v0.6 is closed. Manifest infrastructure was bootstrapped in v0.6 (manifest MVP, PNG tEXt embedding); v0.6.5 ships the full quartet of CLI tools that make it usable end-to-end: register-figure (auto-wired in sweep.py), list-orphans (technical-debt detector), audit-paper (verify a .qmd against its declared manifests), snapshot (Zenodo/HAL deposit bundles). One observability refactor (pipeline-based transform dispatch) and Docker container support also land here.

Provenance tooling — the quartet

  • New: tools/manifest_helper.py register-figure is now invoked automatically by experiments/sweep.py for every plot generated. Embeds bdpd_run_id + bdpd_manifest_sha256 + bdpd_manifest_path into the PNG tEXt chunk, and adds the figure as an artifact entry in the parent results manifest. The --no-register flag opts out (rarely needed).
  • New: tools/manifest_helper.py list-orphans scans experiments/results/ and experiments/figures/ for files without manifest provenance or with stale manifest pointers. Six classes: result_no_manifest, manifest_unreachable_commit, manifest_missing_results, manifest_artifact_outside_repo, figure_no_provenance, figure_dangling_manifest. Exit 0 always — never auto-deletes; reports only. Git reachability check is cached per-SHA and fail-open if git is unavailable.
  • New: tools/manifest_helper.py audit-paper <paper.qmd> walks .qmd files for Pandoc attributes {data-manifest="sha256:..."} on ![]() figure refs, verifies each manifest exists, that the referenced figure is present in the manifest's artifact list, and that the SHA matches. Seven statuses including FAIL_MANIFEST_SHA_STALE (a diagnostic that distinguishes "manifest doesn't exist" from "manifest was re-signed after the .qmd was written"). Skips _*.qmd partials by default (--include-underscore to override).
  • New: tools/manifest_helper.py snapshot --out bundle.tar.gz produces a self-contained .tar.gz for Zenodo/HAL deposit: manifests + result JSON + figures + sweep definitions + docs/releases/*.qmd (optional), plus a top-level snapshot.json meta-manifest and an auto-generated README.md. Refuses to bundle artifacts pointing outside the repo (warning
  • exclusion); refuses to overwrite without --force.

Observability refactor

  • Changed: platform/observability.js now dispatches all observation transforms through OBSERVATION_TRANSFORM_REGISTRY via two helpers, compileTransforms(varSpec) and applyPipeline(value, pipeline, ctx). The registry was public since v0.6 but the call sites still routed everything through hardcoded if (noise > 0) ... if (resolution !== 'exact') ... branches; v0.6.5 makes the registry the actual dispatch. New transforms (e.g., quantize, delay) can now be wired in without touching buildObservation(). commonsStock and nPlayers paths split the pipeline into a noise filter + a rest filter to preserve the pre/post-resolution intermediates (commons ratio derivation; Math.round(Math.max(1, ...))).
  • Verified: 5/5 strict regression sweeps bit-identical (P1, P8, P9, P10, P11). P9 (observability_noise) exercises the noise pipeline heavily — the bit-identity confirms the refactor preserves seeding semantics.

Docker compose + devcontainer

  • New: Dockerfile — single-stage node:20-bookworm-slim with uv-managed Python 3.12. Three cache-friendly layers: Node deps (npm ci), Python deps (uv pip install), source. VIRTUAL_ENV
  • PATH set so python resolves inside the venv without manual activation.
  • New: docker-compose.yml — single service arena on port 3000 (override BDPD_PORT). Bind-mounts experiments/results, experiments/figures, data, log so sweep outputs land on the host filesystem and survive container restarts.
  • New: .devcontainer/devcontainer.json — reuses the same Dockerfile, forwardPorts: [3000] with openBrowserOnce, postCreateCommand re-aligns deps. VS Code extensions: Python, Ruff, ESLint, Quarto.
  • New: .dockerignore — excludes node_modules, venvs, regenerable experiment outputs, build artifacts, .git, secrets.

Scope deliberately minimal — Docker is addition, not replacement: scripts/setup.sh + uv venv remains the primary onboarding path. A Docker-as-golden-path rewrite of the getting-started docs is a v0.7 candidate.

Bug fixes during dogfooding

  • Fixed: docs/releases/v0.6.qmd figure #fig-p1 referenced a stale manifest SHA (sha256:e4e4388a...) — the .qmd was written before register-figure re-signed the P1 manifest, leaving the document pointing at a pre-figure manifest. Caught by the first run of audit-paper; corrected to the current SHA sha256:ed824f45.... This was the exact class of bug the quartet was designed to catch.

Smoke tests

  • Verified: end-to-end quartet round-trip on a fresh sweep: sweep.pylist-orphansaudit-papersnapshot → extract → manifest.mjs verify on extracted manifest → audit-paper on extracted bundle. All steps PASS, including 5/5 PASS on the audit-paper round-trip from the deposit bundle.
  • Verified: Docker stack — docker compose build (16 steps OK) → up -dcurl /api/health 200 → 6 plugins loaded → python 3.12.13 + numpy/matplotlib/pandas inside container → Node sweep inside container writes manifest visible on host via bind mount → down clean.

v0.6 (2026-05-19)

Open Platform release. The four-week core platform delivery: every behaviour-bearing component is now plugin-shaped (registry + factory + conformance check), the regression set is bit-identical across 5 canonical sweeps including the seeded-noise one, and the provenance manifest infrastructure (per-run manifest sidecar, PNG tEXt chunk embedding) is bootstrapped — full provenance tooling ships in v0.6.5.

Plugin-shaped registries

  • New: platform/perturbation.jsPERTURBATION_REGISTRY + buildPerturbation(). Registry-based dispatch replaces the prior if-else chain. Built-in perturbations (regen_shock, regen_set, commons_shock, capacity_shock, threshold_shift, agent_lockout, agent_restore) registered on import; third-party perturbations register via plugins/perturbations/<name>.js.
  • New: platform/observability.jsOBSERVATION_TRANSFORM_REGISTRY marker (full dispatch in v0.6.5). The two built-in transforms (gaussian_noise, resolution) registered on import.
  • New: platform/metric.js / platform/victory.jsMETRIC_REGISTRY + VICTORY_REGISTRY with engine context injected at evaluation time. Plug victory conditions can read context.engine.toJSON() to compose decisions.
  • Pre-existing: platform/scheduler.js SCHEDULER_REGISTRY (already shipped in v0.5.0) — left untouched. Note: the non-default SequentialRandom and WealthWeighted schedulers use unseeded Math.random(); in regression they stay outside the strict bit-identity set until they get a seeded RNG (deferred to v0.7).

Provenance manifest (MVP)

  • New: tools/manifest.mjs — library + CLI (generate, verify). ~250 lines, zero deps beyond Node built-ins. Schema 1.0: run_id (uuid v4), executed_at/started_at, code (git_commit, branch, dirty, node_version), definition (path, content_sha256, human_description), seed, artifacts (each with content_sha256), manifest_sha256 as canonical-JSON signature.
  • New: experiments/experiment.js — every run now emits a <sweep>_<ts>.manifest.json sibling next to the results JSON. Side-by-side; no schema change to the results JSON itself.
  • New: PNG tEXt chunk embedding via tools/manifest_helper.py (Python — PngImagePlugin). Three text keys per figure: bdpd_run_id, bdpd_manifest_sha256, bdpd_manifest_path. Recoverable from any lost PNG via identify -verbose foo.png | grep bdpd.
  • New: Quarto/Pandoc convention for citing figures with provenance — ![alt](path){#fig-id data-manifest="sha256:..."}. Used in docs/releases/v0.6.qmd for all 5 platform figures.

Strict regression

  • New: tools/check_regression.sh — bit-identical diff of 5 canonical sweeps (P1, P8, P9, P10, P11) against a frozen baseline at experiments/results_paper/. Runs in ~3 seconds. Sidecar .manifest.json files excluded from the diff (fix 4518230).
  • New: platform/observability.js gaussianNoise(rng) accepts a seedable RNG instance. P9 (observability_noise) — which previously could not be in the strict regression set because of free Math.random() — now reproduces bit-identically.

Plugin loader + conformance

  • New: tools/plugin-loader.mjs — scans plugins/ and examples/plugins/ at startup; calls each file's register() with the relevant registry handle. Six categories: engine, perturbation, scheduler, metric, victory, observation_transform. Error reporting via loaded + errors arrays.
  • New: tools/plugin-check.mjs — runs each plugin through a minimal conformance harness (registry contract + smoke run) and reports per-plugin pass/fail. Used by the third quickstart.
  • New: examples/plugins/ — one example plugin per category, intentionally simple, all 6 pass plugin-check.

Documentation

  • New: docs/getting-started/first-experiment.md revised — the v0.5 single-arena walkthrough now points at the v0.6 sweep CLI for end-to-end reproducibility (Path A).
  • New: docs/getting-started/extend-the-engine.md — second quickstart, walks through registering a custom engine via the plugin loader (Path B).
  • New: docs/getting-started/design-an-experiment.md — third quickstart, walks through building a new sweep definition and running it (Path C).
  • New: docs/releases/v0.6.qmd — first release notebook in the Quarto format that v0.6.5's audit-paper was designed for. Cites all 5 platform figures by manifest SHA.

Bug fixes

  • Fixed: docs/getting-started/extend-the-engine.md — 5 MkDocs --strict warnings (broken anchors, missing nav entry).
  • Fixed: docs/getting-started/design-an-experiment.md — Path C copy-paste fragility (two commands embedded a path that diverged from the canonical layout).

v0.5.1 (2026-05-17)

Pre-live audit + first packaged release. Adds the HTTP server (previously living outside migration/), fixes one critical regression in the perturbation engine, and consolidates terminology across paper + code.

Critical fix — perturbation engine reconnected

  • Fixed: regen_shock, regen_set, commons_shock, capacity_shock, and threshold_shift were silently no-ops against the engine since the v0.5.0 BaseEngine refactor. The perturbation engine mutated the cached arena.commons.* but not the engine's internal _regen/_stock/ _capacity/_threshold. The frozen paper P-sweep results were unaffected (produced on 9 May, pre-regression) but live reproduction from v0.5.0 was broken. Live verification with the canonical P10 config now reproduces the documented 26-turn game length under shock ×0.10.
  • New: BaseEngine setter contract: setStock, setCapacity, setRegenRate, setThreshold.
  • Changed: LogisticEngine and SenecaEngine implement the setters with defensive clamps; platform/perturbation.js invokes them.

HTTP server + web dashboard packaged

  • New: main.js — Node.js entry point, port 3000 (env: PORT).
  • New: api/server.js — Express setup, static web/ mount.
  • New: api/routes/arenas.js — full REST/SSE surface for arenas (create, join, start, tick, stream, summary, perturb, history replay, delete). Fixes a pre-existing require/DATA_DIR ReferenceError in the history-replay endpoint.
  • New: web/ — static dashboard (arena.html, history.html, index.html, join.html, supporting CSS/JS).
  • New: ArenaRegistry.deleteArena() + read-only dataDir accessor.
  • Dropped: legacy /api/game and /api/agents routes (tied to the removed engine/game.js; never exercised by the v0.5 dashboard).

Engine + numerical hygiene

  • Fixed: engine/commons.jscommonsStep and commonsStepWithReserve now clamp newStock to [0, K] via Math.max(0, Math.min(K, ...)). Matches the paper formula verbatim and provides defence in depth against extreme commonsShock perturbations that could push the residual below zero.
  • Fixed: platform/observability.js — bucket-resolution branch produced NaN for obs.commonsStock when resolution: 'bucket' was configured (latent: never exercised by P-sweeps).

Renames + terminology unification

  • Renamed: SenecaAwareAgentRCPAgent (class), display name 'SenecaAware''RCP'. Registry key 'rcp' unchanged. Legacy identifiers retained as historical pointer in docs/platform/agents.md.
  • Renamed: the adaptive < conservative effect is now uniformly reactive < conservative throughout the paper and codebase. The AdaptiveAgent class name in the codebase is retained (config.strategyParams.reductionFactor etc. unchanged).
  • Docs: companion clarifications in docs/platform/api.md, docs/platform/architecture.md, docs/platform/agents.md.

Removed orphan files

  • Removed: engine/game.js (323 LOC, standalone game runner superseded by platform/arena.js, zero importers).
  • Removed: config/game-defaults_00.js (72 LOC, pre-v0.4 config snapshot, zero importers).

Paper alignment

  • §2.1 Gate+Rank Victory now explicitly states the configurable threshold default (\(S_\text{final} > 10\), ≈ 6 % of \(K\)), removing the prior \(S > 0\) overstatement.
  • §2.1 Hidden Reserve and the Forest Die marked as optional engine layer; clarified that the P-sweeps run with useHiddenReserve: false (deterministic), consistent with the B5 variance audit.
  • §A-B B3 regen-extended-sweep heading layout: added LaTeX \clearpage before the heading to prevent an overflow on the rendered PDF.

Smoke tests

  • New: tmp/smoke_test_rcp.mjs — 21-assertion regression suite covering the RCP class identity, registry mapping, decide() paths (logistic obs / Seneca obs), and the canonical Bardi signature emerging from a 3000-turn pure-ODE run.

v0.5.0 (2026-05-13)

Multi-engine plugin registry

  • New: engine/base-engine.jsBaseEngine interface contract (step, stepOne, applyRegen, extraObservations, checkEnd, toJSON)
  • New: engine/logistic.jsLogisticEngine wraps existing commonsStep/commonsStepWithReserve; zero numerical change from v0.4.x
  • New: engine/seneca.jsSenecaEngine wraps senecaStep/buildSenecaState; exposes capitalStock and pollutionLevel via extraObservations()
  • New: engine/engine-registry.jsbuildEngine(modelSpec) dispatch; ENGINE_REGISTRY = { logistic, seneca }; extensible by adding one file and one line
  • Changed: platform/arena.js — replaced hard-wired commonsStep calls with this._engine.step/stepOne/applyRegen/checkEnd; commonsCapacity default is now engine-aware (Seneca defaults to commonsInitial)
  • Changed: platform/observability.jsbuildObservation adds capitalStock and pollutionLevel (null-safe; always null for logistic); describeObservability(obsConfig, engineMode) filters engine-specific fields
  • Changed: platform/registry.jscreateArena returns warnings[] when engineMode='seneca' and commonsCapacitycommonsInitial
  • Docs: new docs/platform/engines.md — engine reference + developer guide for adding new ODE models

API key & tooling fixes

  • New: scripts/_env.shresolve_api_key(provider) with provider-specific precedence and BDPD_API_KEY generic fallback; sets _RESOLVED_KEY_VAR
  • Changed: scripts/run_arena.sh — sources _env.sh; removed silent export OPENAI_API_KEY="$API_KEY" re-export that broke non-OpenAI providers
  • Changed: agents/bdpd_agent.py--api-key-env CLI argument; adds Authorization: Bearer <key> header to LLM calls when configured
  • Changed: requirements.txt — added pandas, statsmodels, tabulate (required for B5 statistical inference)
  • Changed: experiments/run_all.shBOOTSTRAP_N and PERMUTATION_N are now env-overridable; under --fast they default to 1000 instead of 10000
  • New: .env.sandbox — template environment file for sandbox validation runs

v0.4.1

  • Added supplement experiments B1–B5 (aggressor intensity fine-sweep, reductionFactor generalisation, extended regen rescue boundary, pure Seneca shock isolation, cross-sweep statistical inference)
  • Added rcp built-in agent strategy — explicitly models Bardi pollution to anticipate Seneca cliff
  • Added strategyParams.intensity (AggressiveAgent, default 0.5) and strategyParams.reductionFactor (AdaptiveAgent, default 1.0) for parameterised heuristic experiments
  • Added regen_set perturbation type — sets regen rate to an exact value
  • Added statistical inference module: bootstrap CIs, logistic regression, permutation tests
  • Added experiment summary generator (generate_experiment_summaries.py)
  • Added sweep_supplement.py — Python runner for B1–B5 supplement sweeps
  • Added A-B-supplement.qmd appendix to the Quarto paper
  • Card template updated to v5: 16:9 art box, art-clip path, dynamic badge width
  • CITATION.cff version bumped to 0.4.1
  • All 17 experiment definitions committed under experiments/definitions/

v0.4.0

  • Added hidden reserve + stochastic collapse (Forest Die, useHiddenReserve, collapseDie)
  • Added Seneca engine mode — Bardi (2011) three-variable R/C/P ODE model alongside logistic
  • Added perturbation engine: 8 types (regen_shock, regen_set, commons_shock, capacity_shock, wealth_shock, strategy_override, observability_flip, threshold_shift)
  • Added 3 pluggable schedulers: simultaneous, sequential_random, wealth_weighted
  • Added 3 observability resolution modes: exact, bucket, sign
  • Added code sandbox agent type — user-submitted JS with 50ms timeout and persistent private memory
  • Added SSE streaming — real-time event broadcast to web clients
  • Added Arena Registry & Persistence — multi-tenant arena management, JSONL logging, meta.json/summary.json on close
  • Added automated report generation (platform/reporter.js) — narrative report.md + figures
  • Added Stranger-King archetype (The Mule) — dual-mode cooperative/aggressive defection
  • Added LLM agent middleman (agents/bdpd_agent.py) — Flask server bridging Node.js platform to any LLM
  • Added LLM card game tournament runner (agents/cards_ai_play.py, 2143 lines) — full simulator with heuristic and LLM modes
  • Added Card Forest Die and Seneca Clip as physical mechanics in the card game
  • Added card generator tool — SVG template → PNG pipeline (tools/card_generator/)
  • Added mkdocs documentation site scaffold (Material theme, 23 pages)
  • Added CITATION.cff
  • Gate threshold removed in card game (v0.3) — collapse only via stochastic Forest Die

v0.3.0

  • Initial platform: logistic commons, 4 built-in agents (aggressive, conservative, adaptive, random)
  • Card game v0.1: 4 archetype decks, simulation engine
  • Gate+Rank victory model
  • Wealth-scaled harvest capacity