P0: workspace scaffold, CI gates, tc-domain, tc-config, tc-db vs real Postgres

- Cargo workspace with 1250-line and no-placeholder CI gates wired first
- tc-domain: id newtypes, SessionKey codec (proptest round-trip), Role,
  GatedCategory (spec §15), AccessPolicy, core entities
- tc-config: figment TOML+env config, DeployTarget/provider/auth selection
  with semantic validation
- migrations/0001: full spec §14 schema incl. DB-enforced append-only audit_log
- tc-db: compile-time-checked sqlx repos (workspaces, users, agents+policies,
  credits, audit) with committed .sqlx offline metadata
- tc-testkit: per-test real-Postgres databases (testcontainers or
  TC_TEST_DATABASE_URL), embedded migrations

Co-Authored-By: Claude Fable 5 <[email protected]>
This commit is contained in:
Omar Sobh
2026-06-09 22:25:47 -05:00
co-authored by Claude Fable 5
commit 0afb359183
49 changed files with 7171 additions and 0 deletions
+43
View File
@@ -0,0 +1,43 @@
//! Postgres persistence for TeamClaw.
//!
//! Queries are compile-time checked (`sqlx::query!`) against the schema in
//! `/migrations`, and every repository is tested only against a real
//! Postgres via `tc-testkit` — no in-memory store exists.
pub mod repo;
use sqlx::postgres::PgPoolOptions;
use sqlx::PgPool;
/// All schema migrations, embedded so binaries can self-migrate at boot.
pub static MIGRATOR: sqlx::migrate::Migrator = sqlx::migrate!("../../migrations");
#[derive(Debug, thiserror::Error)]
pub enum DbError {
#[error("not found")]
NotFound,
#[error("conflict: {0}")]
Conflict(String),
#[error(transparent)]
Other(sqlx::Error),
}
impl From<sqlx::Error> for DbError {
fn from(err: sqlx::Error) -> Self {
match &err {
sqlx::Error::RowNotFound => DbError::NotFound,
sqlx::Error::Database(db) if db.is_unique_violation() => {
DbError::Conflict(db.message().to_owned())
}
_ => DbError::Other(err),
}
}
}
/// Connects a pool sized from configuration. Callers run `MIGRATOR` at boot.
pub async fn connect(url: &str, max_connections: u32) -> Result<PgPool, DbError> {
Ok(PgPoolOptions::new()
.max_connections(max_connections)
.connect(url)
.await?)
}