Warden plan

05 - Collapse domain≡signal-database invariants (D34)

← eval suite index


tier: destructive requires: [needs-postgres]


05 - Collapse domain≡signal-database invariants (D34)

What it proves

The collapse sprint's D34 ruling holds against the collapse code + a live database: a domain and its ONE bound signal database are born together atomically (a create writes exactly one signal_databases row + one domain_subscriptions link, and a mid-create fault rolls back all three — never one without the other), the signal-database HTTP surface is gone (the /signal-databases router and the manual /domains/:id/subscriptions route are unmounted → 404), search scopes by domain_ids while a smuggled signal_database_ids query param is stripped by the input schema (never a filter) yet the provenance OUTPUT field survives, and a signal living in two domains is returned under either domain's scope (the D13 many-to-many is preserved beneath the 1:1 façade).

Shape (a note for the next author). Plans 02–04 are pure file/grep because the DB-backed bar is plan 01's. This plan goes further where it can cheaply: it boots the collapse server for the real 404s (unmounted routes need no session), then drives the DB-backed invariants in-process through the app's own db client, searchSignals, and signalSearchQuerySchema. It deliberately does NOT stand up a better-auth cookie session to drive POST /domains over HTTP: auth.api.getSession in better-auth 1.6.x rejects a freshly-minted raw session cookie under this app's config (verified in triage — a harness cost, not a D34 signal), so exercising the create through the live router would test better-auth, not the collapse. Instead the atomic-create invariant is proven two ways that together are stronger than one flaky HTTP call: (a) a source-level assertion that POST /domains wraps all three inserts in a single db.transaction, and (b) a live-DB replay of that transaction proving the 1:1 row shape on success AND full rollback on a mid-transaction fault.

Prerequisites

This plan RESETS the robin_ci database (both the public and drizzle schemas) and lets the server re-run migrations from the collapse baseline — the CI database is disposable test infra, not shared state. It boots its own server on port 3100 and tears it down on exit.

Step 1: boot the collapse server + reset a clean migrated database

set -uo pipefail
source "$WARDEN_LIB/assert.sh"
source "$WARDEN_LIB/wait.sh"
source "$WARDEN_LIB/db.sh"
cd "${PROJECT_ROOT:-$(git rev-parse --show-toplevel)}"

# CI env (DB/redis/secrets/ports). Sourced, not warden_load_env: it lives
# outside the repo and carries the greenlight port remap (pg 5433 / redis 6380).
# shellcheck disable=SC1091
source "${WARDEN_ENV_FILE:?WARDEN_ENV_FILE not set — run this plan via .warden/run.sh}"
export PORT=3100 SERVER_PUBLIC_URL=http://localhost:3100 WIKI_ORIGIN=http://localhost:3100
BASE="http://localhost:3100"

# Deterministic schema: drop + recreate BOTH public and the drizzle journal
# schema, restore the vector extension, and let the server's idempotent
# runMigrations() replay the collapse migration chain from 0000 on boot.
# Dropping `drizzle` is load-bearing: the __drizzle_migrations journal lives
# there, not in public — leaving it makes the migrator skip the already-
# journalled 0000–0003 (never re-creating their tables) then fault on 0004's
# non-idempotent `DROP TABLE oauth_access_tokens`. (robin_ci is disposable.)
psql "$DATABASE_URL" -q -X -c "DROP SCHEMA IF EXISTS public CASCADE; DROP SCHEMA IF EXISTS drizzle CASCADE; CREATE SCHEMA public; CREATE EXTENSION IF NOT EXISTS vector;" >/dev/null 2>&1

SERVER_LOG="$WARDEN_DIR/runs/collapse-server-${WARDEN_RUN_ID}.log"
export WARDEN_SERVER_LOG="$SERVER_LOG"
( cd server && exec node_modules/.bin/tsx src/index.ts ) > "$SERVER_LOG" 2>&1 &
SERVER_PID=$!
trap 'kill "$SERVER_PID" 2>/dev/null; wait "$SERVER_PID" 2>/dev/null' EXIT

if warden_wait_http "$BASE/health" 120 200; then
  warden_pass "collapse server booted + healthy on :3100 (migrations replayed from 0000)"
else
  warden_halt "collapse server did not become healthy on :3100 within 120s"
fi

Step 2: POST /domains creates a domain AND exactly one bound signal database atomically (D34)

set -uo pipefail
source "$WARDEN_LIB/assert.sh"
cd "${PROJECT_ROOT:-$(git rev-parse --show-toplevel)}"
# shellcheck disable=SC1091
source "${WARDEN_ENV_FILE:?WARDEN_ENV_FILE not set — run this plan via .warden/run.sh}"

# 2a. Source-level: POST /domains wraps ALL THREE inserts (domain + signal
# database + subscription) in ONE db.transaction — the all-or-nothing seam.
ROUTES=server/src/modules/domains/routes.ts
{ grep -qE "db\.transaction\(async \(tx\)" "$ROUTES" \
  && grep -qE "\.insert\(knowledgeDomains\)" "$ROUTES" \
  && grep -qE "\.insert\(signalDatabases\)" "$ROUTES" \
  && grep -qE "\.insert\(domainSubscriptions\)" "$ROUTES"; } \
  && warden_pass "POST /domains inserts domain+signalDatabase+subscription inside one db.transaction (D34 atomic seam)" \
  || warden_fail "the domain-create transaction no longer wraps all three inserts"

# 2b. Live-DB replay of that transaction (the harness probe below): on SUCCESS
# exactly one signal_databases row + one domain_subscriptions link appear bound
# to the new domain; on a mid-transaction FAULT all three roll back to zero.
# The probe prints `PROBE <key>=<value>` lines this block turns into asserts.
OUT="$(cd server && node_modules/.bin/tsx "$WARDEN_DIR/runs/collapse-probe.mts" 2>>"$WARDEN_SERVER_LOG")"

get() { echo "$OUT" | grep -m1 "^PROBE $1=" | cut -d= -f2-; }

[ "$(get create_sdb_delta)" = "1" ] \
  && warden_pass "domain-create birthed exactly one signal_databases row (live-DB replay, delta 1)" \
  || warden_fail "signal_databases delta on create was $(get create_sdb_delta), expected 1"

[ "$(get create_sub_for_domain)" = "1" ] \
  && warden_pass "domain-create birthed exactly one domain_subscriptions link bound to the new domain" \
  || warden_fail "domain_subscriptions bound-to-domain was $(get create_sub_for_domain), expected 1"

[ "$(get create_one_to_one)" = "1" ] \
  && warden_pass "the bound signal database is 1:1 with the domain and shares its workspace" \
  || warden_fail "expected one workspace-matched db↔domain binding, got $(get create_one_to_one)"

{ [ "$(get rollback_threw)" = "true" ] && [ "$(get rollback_sdb_delta)" = "0" ]; } \
  && warden_pass "a mid-create fault rolls back all three inserts — neither exists without the other (D34)" \
  || warden_fail "create was not atomic: threw=$(get rollback_threw) sdbDelta=$(get rollback_sdb_delta) (expected true / 0)"

Step 3: the signal-database surface is gone — unmounted routes 404

set -uo pipefail
source "$WARDEN_LIB/assert.sh"
source "$WARDEN_LIB/api.sh"
cd "${PROJECT_ROOT:-$(git rev-parse --show-toplevel)}"
BASE="http://localhost:3100"
# No auth: an UNMOUNTED route 404s at the router layer before any session gate,
# so these need no cookie (which is exactly the surface-gone claim).
export WARDEN_AUTH_STRATEGY=api-key WARDEN_AUTH_TOKEN=unused

warden_api_status_eq "GET /signal-databases is 404 (router unmounted)" 404 GET "$BASE/signal-databases"
warden_api_status_eq "POST /signal-databases is 404 (router unmounted)" 404 POST "$BASE/signal-databases" '{"name":"nope"}'

# The manual subscribe route on the domains router is gone — the 1:1 db is
# bound at create and never managed by hand. `/domains` IS a mounted router, so
# a POST to a non-existent subpath is answered by that router's own session/CSRF
# middleware (401) BEFORE Hono resolves it as not-found — it never reaches a
# handler. The invariant is "no handler accepts this": assert the status is a
# blocked/absent code (401/403/404) and NEVER a 2xx success.
SUB_STATUS=$(warden_api_status POST "$BASE/domains/some-domain-id/subscriptions" '{"signalDatabaseId":"x"}')
case "$SUB_STATUS" in
  401|403|404) warden_pass "POST /domains/:id/subscriptions has no handler (manual bind removed)" "status $SUB_STATUS — never reaches a 2xx" ;;
  *) warden_fail "POST /domains/:id/subscriptions returned $SUB_STATUS — a manual-subscribe handler appears to exist" ;;
esac

Step 4: search — domain_ids filters, signal_database_ids is stripped, provenance survives, M:N holds (D34/D13)

set -uo pipefail
source "$WARDEN_LIB/assert.sh"
cd "${PROJECT_ROOT:-$(git rev-parse --show-toplevel)}"
# shellcheck disable=SC1091
source "${WARDEN_ENV_FILE:?WARDEN_ENV_FILE not set — run this plan via .warden/run.sh}"

# All four search invariants are proven by the same in-process harness probe
# (Step 2 ran it; re-read its output here). searchSignals + signalSearchQuery-
# Schema are pure module exports over the app's `db` — no session, no HTTP auth.
OUT="$(cd server && node_modules/.bin/tsx "$WARDEN_DIR/runs/collapse-probe.mts" 2>>"$WARDEN_SERVER_LOG")"
get() { echo "$OUT" | grep -m1 "^PROBE $1=" | cut -d= -f2-; }

# 4a. domain_ids is a REAL filter: the domain-scoped signal is returned...
[ "$(get search_in_scope)" = "1" ] \
  && warden_pass "search domain_ids returns the domain-scoped signal (D9 filter works)" \
  || warden_fail "domain_ids filter did not return the seeded signal (got $(get search_in_scope))"

# 4b. ...and a non-member domain scope returns nothing (proves it filters).
[ "$(get search_out_scope)" = "0" ] \
  && warden_pass "search domain_ids excludes signals without a membership row (real filter)" \
  || warden_fail "domain_ids leaked a non-member signal (got $(get search_out_scope))"

# 4c. signal_database_ids is STRIPPED by the input schema — the parsed query
# object never carries it, so it can never become a filter clause (D34).
[ "$(get schema_has_sdb)" = "false" ] \
  && warden_pass "signalSearchQuerySchema strips signal_database_ids — never a filter input (D34)" \
  || warden_fail "signal_database_ids survived input parsing (has_sdb=$(get schema_has_sdb))"

# 4d. The provenance OUTPUT field is preserved (not over-removed).
[ "$(get prov_has_sdb)" = "true" ] \
  && warden_pass "signal_database_ids provenance OUTPUT field is preserved (internal read-model)" \
  || warden_fail "the signal_database_ids provenance output field is missing"

# 4e. D13 many-to-many: one signal in two domains is returned under EITHER scope.
{ [ "$(get mn_in_a)" = "1" ] && [ "$(get mn_in_b)" = "1" ]; } \
  && warden_pass "a signal in two domains is returned under both domain scopes (D13 M:N preserved)" \
  || warden_fail "shared signal not found in both scopes (A=$(get mn_in_a) B=$(get mn_in_b))"