> Why one spec, not five. The investigation that motivated this spec (2026-04-24) found that the plumbing for versioned artifacts already exists in production — the artifacts table carries version_number, parent_version_id, content_hash, is_latest, version_tag, changelog, lifecycle_state; artifact_links carries cross-artifact edges; notebook_cells is a real table; the GET /api/artifacts/{id}/versions endpoints work. What's missing is the connections: debates that pin a specific artifact-version, a cell-append API that bumps the notebook version, a "chamber/workspace" pull-in mechanism, and structured metadata on artifact_links rows. This spec wires those four connections together so we don't fork into five overlapping specs.
Parent: [artifact_versioning_spec.md](artifact_versioning_spec.md).
---
The audit confirmed these are correctly built and don't need re-spec:
artifacts table versioning columns — version_number, parent_version_id, content_hash, is_latest, version_tag, changelog, lifecycle_state, deprecated_at, superseded_by. Already populated for new artifacts.GET /api/artifacts/{id}/versions, GET /api/artifacts/{id}/versions/{N}, GET /api/artifacts/{id}/diff. Already serving.artifact_registry.py: create_version(), get_version_history(), diff_versions(), pin_version() are implemented (task 58309097-1f15-4cb6 completed 2026-04-16).artifact_links table: (source_artifact_id, target_artifact_id, link_type, strength, evidence). Link types: derives_from, cites, extends, supports, contradicts.notebooks table + on-disk .ipynb/.html pairs at site/notebooks/.notebook_cells table (notebook_id, cell_index, cell_type, code, output).---
Problem: debate_sessions.target_artifact_version exists but is always NULL/empty; target_content_hash is always ''. Debates effectively reference an unversioned artifact, so a debate that argued about hypothesis-H-89 in March doesn't tell you which version was being argued.
Fix:
debate_session is created with target_artifact_id, the creator function looks up artifacts.version_number + content_hash for the latest version (or the explicitly-passed version) and writes both to the row. NOT NULL going forward; backfill historical rows once with a one-time migration that picks "latest as of debate's started_at" — best-effort, mark backfilled rows in a pinning_note column.debate_rounds.referenced_artifacts JSONB (default '[]'::jsonb). Each entry: {artifact_id, version_number, content_hash, role: 'input'|'output'|'evidence', cited_at_offset_chars: int}. The debate engine populates this whenever a round's prompt or output cites an artifact (the existing skill-citation logic from quest_commentary_curator_spec produces the same kind of edges; reuse).GET /api/debate/{session_id}/artifacts returns the union of the session's pinned target + every round's referenced_artifacts flattened, with version-resolved metadata. UI: debate transcript shows 🔗 hyp-H-89@v3 chips that link to the pinned version (not "latest").JSONB column; no schema break since old rows default to [].Problem: notebooks are immutable after generation. There is no way to ask "add a differential expression analysis to hypothesis-Q-89-notebook" without regenerating from scratch.
Fix:
POST /api/notebooks/{id}/cells — append-only. Body:{
"cell_type": "code|markdown",
"source": "...",
"execute": true,
"agent_id": "ed-lein",
"method": "differential-expression",
"parameters": {"contrast": "AD vs control", "fdr": 0.05},
"rationale": "Why this cell is being added"
}notebook with parent_version_id = current notebook artifact id, version_number += 1, is_latest=TRUE, demotes the parent's is_latest=FALSE; (b) writes the new cell to notebook_cells with cell_index = max+1 AND a foreign key to the new artifact row; (c) optionally executes the cell via nbconvert and caches outputs; (d) renders .html for the new version, stores the path; (e) writes an artifact_links edge new_version --extends--> parent_version; (f) records the agent + method in a new processing_steps table per §2.4 below.GET /api/notebooks/{id}/diff?from=v1&to=v2 returns a diff using nbdime semantics (added/removed/modified cells). Reuse nbdime's protocol JSON; don't roll our own.pin_version(). E.g., the notebook a debate consumed is auto-tagged "debate-{session_id}-input" so the lineage is queryable from the artifact alone.Problem: there's no "workspace" or "chamber" — when a debate or persona-driven task wants to work with hypothesis-H-89@v3 + notebook-NB-12@v2 + paper-P-77@v1, it just names the IDs in prose. No structured pull-in, no isolation.
Fix:
chambers — minimal:id UUID PK
name TEXT
purpose TEXT ('debate' | 'experiment_design' | 'persona_workspace' | 'showcase_review')
owner_actor_type TEXT, owner_actor_id TEXT (matches existing comment author convention)
parent_session_id UUID (debate_sessions.id, NULL ok)
created_at TIMESTAMPTZ DEFAULT now()
closed_at TIMESTAMPTZchamber_artifacts (the pull-in) — pinned versions:chamber_id UUID
artifact_id UUID
version_number INT
content_hash TEXT
role TEXT ('input' | 'reference' | 'workbench')
added_at TIMESTAMPTZ DEFAULT now()
added_by_actor_type TEXT, added_by_actor_id TEXT
PRIMARY KEY (chamber_id, artifact_id, version_number)POST /api/chambers — createPOST /api/chambers/{id}/pull — body: [{artifact_id, version_number?}] (defaults to latest)GET /api/chambers/{id} — full chamber state with all pinned versions hydratedPOST /api/chambers/{id}/close — closes the chamber, optionally writes a result-artifact
role='workbench'. The chamber is a stable referencer for "what was visible to the agents during this debate".purpose='persona_workspace'. The persona's bio + paper corpus + previous debates the persona participated in are pulled in as role='reference'.(artifact_id, version_number, role) set is stored on the parent debate/task as chamber_provenance_hash for later dispute/replay/fork detection. The hash captures exactly which artifact-versions were in scope, without requiring a full copy. A chamber_summary JSON blob (who participated, key turns, final score) is written to the chamber row and linked to the parent. A GET /api/chambers/{id}/replay endpoint returns the chamber's full state (all pinned artifact-versions + summary) so any agent can rehydrate the chamber context and replay or fork from a clean checkpoint.Problem: artifact_links rows have no record of what operation created the link, or what agent/model did it.
Fix:
artifact_links:method TEXT — the operation that produced the link (cell_append, debate_output, cite, derives_from, extends, reproduced_with_diff, contribution_attribution, etc.)agent_id TEXT — which agent contributed the link (null for auto-detected links)processing_step_id UUID — FK to a new processing_steps table (see below)link_metadata JSONB — method-specific extra fields (e.g., for cell_append: {notebook_id, cell_index, cell_hash}, for debate_output: {session_id, round_number})processing_steps table — immutable log of operations that create/extend artifacts:id UUID PK
method TEXT ('cell_append' | 'artifact_fork' | 'debate_consolidation' | 'notebook_regeneration' | 'agent_contribution' | 'manual_edit')
artifact_id UUID
artifact_version_number INT
actor_type TEXT, actor_id TEXT
inputs JSONB -- [{artifact_id, version_number, role, content_hash}]
outputs JSONB -- [{artifact_id, version_number, role, content_hash}]
parameters JSONB
rationale TEXT
started_at TIMESTAMPTZ DEFAULT now()
completed_at TIMESTAMPTZ
status TEXT ('running' | 'completed' | 'failed')
error TEXTprocessing_steps.method='cell_append' is written by the cell-append API (§2.2); method='debate_consolidation' by the debate engine; method='agent_contribution' when a persona edits an artifact directly.GET /api/artifacts/{id}/lineage?depth=5&method=cell_append returns the chain of cells + who added each, using processing_steps.inputs and artifact_links traversed together. The depth param prevents infinite loops on cyclic graphs.---
---
debate_rounds.referenced_artifacts JSONB + GET /api/debates/{session_id}/artifacts + auto-populate target_artifact_version on debate creation. §2.2: POST /api/notebooks/{id}/cells (append-only) + GET /api/notebooks/{id}/diff?from=v1&to=v2 using nbdime semantics; all tests pass. §2.3: chambers + chamber_artifacts tables, POST /api/chambers, POST /api/chambers/{id}/pull, GET /api/chambers/{id}, POST /api/chambers/{id}/close, persona workspace endpoints all live. Feature complete; no new commits needed.