Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
50 changes: 50 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,56 @@
All notable changes to bicameral-mcp are tracked here. Format loosely follows
[Keep a Changelog](https://keepachangelog.com/en/1.1.0/).

## 0.4.12.1 — 2026-04-14 — Team Adapter Signature Drift Hotfix

Hotfix for a class of latent regressions in `events/team_adapter.py`
that have been silently breaking team mode since v0.4.6. Surfaced
during v0.4.12 preflight dogfooding on bicameral's own repo (which
runs in team mode) — every `link_commit` call had been failing with
`TypeError: TeamWriteAdapter.ingest_commit() got an unexpected keyword
argument 'authoritative_ref'`, causing the bicameral ledger's 23
decisions to be stuck `ungrounded` because the grounding sweep never
ran.

Six releases of latent breakage. Caught by dogfooding the v0.4.12
preflight tool against a real team-mode repo for the first time.

### Fixed

- **`TeamWriteAdapter.ingest_commit`** now forwards the
`authoritative_ref` kwarg added by v0.4.6's pollution guard. Without
this, every team-mode `handle_link_commit` call raised TypeError.
- **`TeamWriteAdapter.ingest_payload`** now forwards the `ctx` kwarg
added by v0.4.6's pollution fix. Without this, team-mode ingest
raised TypeError on every call.
- **`TeamWriteAdapter.backfill_empty_hashes`** added as a pass-through.
Used by `handle_link_commit` for the v0.4.5 self-heal sweep. Was
silently degraded via `hasattr()` check — backfill never ran in
team mode.
- **`TeamWriteAdapter.get_all_source_cursors`** added as a pass-through.
Used by `handle_reset` for dry-run summaries. Would have raised
AttributeError on first team-mode reset call.
- **`TeamWriteAdapter.wipe_all_rows`** added as a pass-through. Used
by `handle_reset(confirm=True)`. Would have raised AttributeError on
first team-mode confirmed reset.

### Added

- **`tests/test_v0412_1_team_adapter_drift.py`** — 28 cases that use
`inspect.signature` to assert the wrapper's public methods accept
the same kwargs as the inner adapter. Any future signature drift
fails CI loudly. The exact regression pattern that broke v0.4.6
silently for six releases is now blocked at PR time.

### Migration

No schema changes. No API surface changes. Pure wrapper hardening.
Existing team-mode users will find that `link_commit` actually runs
sweeps now, which means previously-stuck-`ungrounded` decisions will
flip to `reflected` or `drifted` based on real code state. May surface
a backlog of latent drift on first run after the upgrade — that's
expected and correct.

## 0.4.12 — 2026-04-14 — Preflight (Proactive Context Surfacing)

Adds `bicameral.preflight(topic)` — a proactive context-surfacing tool
Expand Down
61 changes: 55 additions & 6 deletions events/team_adapter.py
Original file line number Diff line number Diff line change
Expand Up @@ -45,23 +45,44 @@ async def _ensure_ready(self) -> None:

# ── Write methods (intercepted: event file first, then DB) ───────────

async def ingest_payload(self, payload: dict) -> dict:
"""Write ingest event, then delegate to inner adapter."""
async def ingest_payload(self, payload: dict, ctx=None) -> dict:
"""Write ingest event, then delegate to inner adapter.

v0.4.12.1: forward the ``ctx`` kwarg added in v0.4.6's pollution
fix. Without this, handle_ingest's ``ledger.ingest_payload(payload, ctx=ctx)``
call fails in team mode with TypeError, which means EVERY team-mode
ingest has been broken since v0.4.6.
"""
await self._ensure_ready()
self._writer.write("ingest.completed", payload)
return await self._inner.ingest_payload(payload)
return await self._inner.ingest_payload(payload, ctx=ctx)

async def ingest_commit(
self, commit_hash: str, repo_path: str, drift_analyzer=None,
self,
commit_hash: str,
repo_path: str,
drift_analyzer=None,
authoritative_ref: str = "",
) -> dict:
"""Write link_commit event, then delegate to inner adapter."""
"""Write link_commit event, then delegate to inner adapter.

v0.4.12.1: forward the ``authoritative_ref`` kwarg added in
v0.4.6's pollution guard. Without this, every team-mode
``handle_link_commit`` call silently failed with a TypeError —
bicameral's own bicameral repo (which runs in team mode) had
all 23 decisions stuck ungrounded because the link_commit sweep
never ran. Surfaced during v0.4.12 preflight dogfooding.
"""
await self._ensure_ready()
self._writer.write(
"link_commit.completed",
{"commit_hash": commit_hash, "repo_path": repo_path},
)
return await self._inner.ingest_commit(
commit_hash, repo_path, drift_analyzer=drift_analyzer,
commit_hash,
repo_path,
drift_analyzer=drift_analyzer,
authoritative_ref=authoritative_ref,
)

async def upsert_source_cursor(
Expand Down Expand Up @@ -130,3 +151,31 @@ async def get_source_cursor(
) -> dict | None:
await self._ensure_ready()
return await self._inner.get_source_cursor(repo, source_type, source_scope)

# v0.4.12.1: pass-throughs for adapter methods added since v0.4.5 that
# the team wrapper never gained. handle_link_commit / handle_reset call
# these and silently degraded (or crashed) in team mode pre-v0.4.12.1.

async def backfill_empty_hashes(
self, repo_path: str, drift_analyzer=None,
) -> dict:
"""Self-heal regions with empty content_hash from pre-v0.4.5
ingests. Pure local read+update — no event emitted."""
await self._ensure_ready()
return await self._inner.backfill_empty_hashes(
repo_path, drift_analyzer=drift_analyzer,
)

async def get_all_source_cursors(self, repo: str) -> list[dict]:
"""List every source_cursor row for a repo. Used by handle_reset's
dry-run summary. Pure local read."""
await self._ensure_ready()
return await self._inner.get_all_source_cursors(repo)

async def wipe_all_rows(self, repo: str) -> None:
"""Wipe every bicameral row scoped to ``repo``. Used by
handle_reset(confirm=True). Destructive, no event emitted —
the reset itself is the event from the team's perspective.
"""
await self._ensure_ready()
await self._inner.wipe_all_rows(repo)
2 changes: 1 addition & 1 deletion pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ build-backend = "hatchling.build"

[project]
name = "bicameral-mcp"
version = "0.4.12"
version = "0.4.12.1"
description = "Decision ledger MCP server — ingests meeting transcripts, maps decisions to code, tracks drift"
readme = "README.md"
requires-python = ">=3.10"
Expand Down
109 changes: 109 additions & 0 deletions tests/test_v0412_1_team_adapter_drift.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,109 @@
"""v0.4.12.1 — team adapter signature-drift regression tests.

The TeamWriteAdapter wraps SurrealDBLedgerAdapter via composition. As
new methods + kwargs are added to the inner adapter (v0.4.6 added
``authoritative_ref`` to ``ingest_commit``, v0.4.6 added ``ctx`` to
``ingest_payload``, v0.4.6 added ``backfill_empty_hashes`` /
``get_all_source_cursors`` / ``wipe_all_rows``), the wrapper has to
keep up — otherwise team-mode users hit silent TypeErrors and degraded
behavior on every call.

This silent failure mode was caught during v0.4.12 preflight dogfooding
on bicameral's own repo (which runs in team mode). Every link_commit
call had been failing since v0.4.6 — six releases of latent breakage.

These tests use ``inspect.signature`` to assert the wrapper's public
methods accept the same kwargs as the inner adapter, so any future
signature drift fails CI loudly.
"""

from __future__ import annotations

import inspect

import pytest

from events.team_adapter import TeamWriteAdapter
from ledger.adapter import SurrealDBLedgerAdapter


# Methods the wrapper MUST expose to forward to the inner adapter.
# Each entry is (method_name, kwargs_that_must_be_accepted).
_REQUIRED_FORWARDED = [
# Write paths
("ingest_payload", {"ctx"}), # v0.4.6 added ctx
("ingest_commit", {"drift_analyzer", "authoritative_ref"}), # v0.4.6 added authoritative_ref
# Self-heal
("backfill_empty_hashes", {"drift_analyzer"}), # v0.4.5 added entirely
# Reset machinery
("get_all_source_cursors", set()), # v0.4.6 added entirely
("wipe_all_rows", set()), # v0.4.6 added entirely
# Vocab cache
("lookup_vocab_cache", set()),
("upsert_vocab_cache", set()),
# Read paths
("get_all_decisions", {"filter"}),
("search_by_query", set()),
("get_decisions_for_file", set()),
("get_undocumented_symbols", set()),
("get_source_cursor", set()),
("upsert_source_cursor", set()),
]


@pytest.mark.parametrize("method_name,required_kwargs", _REQUIRED_FORWARDED)
def test_team_adapter_method_exists(method_name, required_kwargs):
"""Every required method must exist on TeamWriteAdapter."""
assert hasattr(TeamWriteAdapter, method_name), (
f"TeamWriteAdapter is missing method {method_name!r} that the inner "
f"SurrealDBLedgerAdapter exposes. Team-mode callers will hit "
f"AttributeError or silent hasattr() degradation."
)


@pytest.mark.parametrize("method_name,required_kwargs", _REQUIRED_FORWARDED)
def test_team_adapter_method_accepts_required_kwargs(method_name, required_kwargs):
"""Every required kwarg must appear in the wrapper's signature."""
if not required_kwargs:
return
method = getattr(TeamWriteAdapter, method_name)
sig = inspect.signature(method)
params = set(sig.parameters.keys())
missing = required_kwargs - params
assert not missing, (
f"TeamWriteAdapter.{method_name} is missing kwargs {missing}. "
f"Inner adapter accepts these but wrapper doesn't — calls forwarding "
f"these kwargs will fail with TypeError in team mode."
)


def test_inner_and_wrapper_ingest_commit_kwargs_aligned():
"""Direct comparison: inner ingest_commit and wrapper ingest_commit
must accept the same set of kwargs (modulo self).
"""
inner_params = set(
inspect.signature(SurrealDBLedgerAdapter.ingest_commit).parameters.keys()
) - {"self"}
wrapper_params = set(
inspect.signature(TeamWriteAdapter.ingest_commit).parameters.keys()
) - {"self"}
missing_in_wrapper = inner_params - wrapper_params
assert not missing_in_wrapper, (
f"TeamWriteAdapter.ingest_commit is missing kwargs that inner "
f"SurrealDBLedgerAdapter.ingest_commit accepts: {missing_in_wrapper}. "
f"This is the exact regression that broke team mode in v0.4.6."
)


def test_inner_and_wrapper_ingest_payload_kwargs_aligned():
inner_params = set(
inspect.signature(SurrealDBLedgerAdapter.ingest_payload).parameters.keys()
) - {"self"}
wrapper_params = set(
inspect.signature(TeamWriteAdapter.ingest_payload).parameters.keys()
) - {"self"}
missing_in_wrapper = inner_params - wrapper_params
assert not missing_in_wrapper, (
f"TeamWriteAdapter.ingest_payload is missing kwargs that inner "
f"SurrealDBLedgerAdapter.ingest_payload accepts: {missing_in_wrapper}."
)