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
139 changes: 139 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,139 @@
# Relavium CI — the green gate every push and PR must pass (Phase 0 · 0.G).
#
# Runs lint + typecheck + test across every workspace in dependency order with the
# Turborepo cache, plus the formatting check and the no-vendor-type-across-the-seam fence.
#
# Branch protection (set in the GitHub repo settings, not here): the `ci` job below is the
# REQUIRED status check to merge into `main`. The `peer-dep-gate` job is advisory until the
# surface packages land their peers in Phase 1.
#
# Caching: the always-on layer is the GitHub Actions `.turbo` cache (restored/saved below),
# which makes a no-change re-run a Turborepo cache hit — the M0 "demonstrably hitting"
# criterion. The Vercel-style Turborepo REMOTE cache (cross-runner sharing) is opt-in: add
# repo secrets `TURBO_TOKEN` and `TURBO_TEAM` and Turbo picks them up automatically below.
#
# Third-party actions are pinned to a full commit SHA (the `# vX.Y.Z` comment tracks the
# human-readable release) so a moved tag can't inject unreviewed code; bumps are deliberate.
name: CI

on:
push:
branches: [main, development]
pull_request:

# Cancel superseded runs on the same ref so CI tracks the latest push.
concurrency:
group: ci-${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: true

permissions:
contents: read

env:
# Picked up by Turbo for remote caching when the secrets are configured; harmless if empty.
TURBO_TOKEN: ${{ secrets.TURBO_TOKEN }}
TURBO_TEAM: ${{ secrets.TURBO_TEAM }}

jobs:
ci:
name: lint · typecheck · test
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4.3.1
with:
persist-credentials: false

- name: Set up pnpm
# pnpm version comes from the root package.json `packageManager`.
uses: pnpm/action-setup@b906affcce14559ad1aafd4ab0e942779e9f58b1 # v4.4.0

- name: Set up Node
uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4.4.0
with:
node-version-file: .nvmrc
cache: pnpm

- name: Install (frozen lockfile)
run: pnpm install --frozen-lockfile

- name: Restore Turborepo cache
uses: actions/cache@0057852bfaa89a56745cba8c7296529d2fc39830 # v4.3.0
with:
path: .turbo
key: turbo-${{ runner.os }}-${{ github.sha }}
restore-keys: |
turbo-${{ runner.os }}-

# The required gate: lint, typecheck, and test every workspace, then build.
- name: Lint, typecheck, test
run: pnpm turbo run lint typecheck test
# Not redundant with typecheck: `build` runs each package's emit (tsconfig.build.json
# → dist), which `typecheck` (tsconfig.json --noEmit) does not — it catches
# emit-only failures and proves the published artifact compiles.
- name: Build
run: pnpm turbo run build

# Formatting is run, not argued (code-style-typescript.md).
- name: Format check
run: pnpm format:check

# Prove the no-vendor-type-across-the-@relavium/llm-seam fence is live (0.F).
- name: Seam fence is enforced
run: pnpm lint:fence-check

# CI-only strict peer-dependency gate (.npmrc keeps this OFF for local installs so fresh
# checkouts resolve while the surface packages' peers are not all in the tree yet). Catch
# peer drift here without breaking local dev.
peer-dep-gate:
name: strict peer-dependency check
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4.3.1
with:
persist-credentials: false
- uses: pnpm/action-setup@b906affcce14559ad1aafd4ab0e942779e9f58b1 # v4.4.0
- uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4.4.0
with:
node-version-file: .nvmrc
cache: pnpm
- name: Install with strict peers
run: pnpm install --frozen-lockfile --config.strict-peer-dependencies=true

# --- Reserved Phase-1 lanes (TODO: enable with the first provider adapter) ------------

Check warning on line 102 in .github/workflows/ci.yml

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Complete the task associated to this "TODO" comment.

See more on https://sonarcloud.io/project/issues?id=HodeTech_Relavium&issues=AZ6TmiwVe7WIcbmI9IPB&open=AZ6TmiwVe7WIcbmI9IPB&pullRequest=3
# The per-provider conformance suite and the nightly live-API lane land WITH the adapters
# in Phase 1 (testing.md); only their CI slots are reserved here so the testing standard
# maps cleanly onto lanes from day one. Do not enable until `packages/llm` exists — and
# pin each action to a commit SHA (as above) when uncommenting.
#
# conformance:
# name: provider conformance (fixtures)
# runs-on: ubuntu-latest
# steps:
# - uses: actions/checkout@<sha> # pin on enable
# - uses: pnpm/action-setup@<sha>
# - uses: actions/setup-node@<sha>
# with: { node-version-file: .nvmrc, cache: pnpm }
# - run: pnpm install --frozen-lockfile
# - run: pnpm turbo run test:conformance # fixture mode — no network, no keys
#
# Nightly live-API lane — runs the conformance suite against real providers using keys
# from CI secrets. Separate workflow trigger so it never gates a PR:
# # on:
# # schedule:
# # - cron: '0 7 * * *' # 07:00 UTC nightly
# live-api:
# name: provider conformance (live, nightly)
# if: github.event_name == 'schedule'
# runs-on: ubuntu-latest
# steps:
# - uses: actions/checkout@<sha> # pin on enable
# - uses: pnpm/action-setup@<sha>
# - uses: actions/setup-node@<sha>
# with: { node-version-file: .nvmrc, cache: pnpm }
# - run: pnpm install --frozen-lockfile
# - run: pnpm turbo run test:conformance:live
# env:
# ANTHROPIC_API_KEY: ${{ secrets.ANTHROPIC_API_KEY }}
# OPENAI_API_KEY: ${{ secrets.OPENAI_API_KEY }}
# GOOGLE_API_KEY: ${{ secrets.GOOGLE_API_KEY }}
# --------------------------------------------------------------------------------------
4 changes: 4 additions & 0 deletions .prettierignore
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,10 @@
**/coverage/**
**/node_modules/**

# Generated drizzle-kit migration artifacts (SQL + snapshot/journal JSON) — owned by
# drizzle-kit's serializer, regenerated by `pnpm --filter @relavium/db db:generate`.
**/drizzle/**

# Lockfile
pnpm-lock.yaml

Expand Down
8 changes: 7 additions & 1 deletion docs/decisions/0005-sqlite-drizzle-local-postgres-cloud.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,13 @@

- **Status**: Accepted
- **Date**: 2026-06-03
- **Related**: [0001-tauri-v2-over-electron.md](0001-tauri-v2-over-electron.md), [0006-os-keychain-for-api-keys.md](0006-os-keychain-for-api-keys.md), [0008-local-first-phase-1-cloud-phase-2.md](0008-local-first-phase-1-cloud-phase-2.md), [tech-stack.md](../tech-stack.md)
- **Related**: [0001-tauri-v2-over-electron.md](0001-tauri-v2-over-electron.md), [0006-os-keychain-for-api-keys.md](0006-os-keychain-for-api-keys.md), [0008-local-first-phase-1-cloud-phase-2.md](0008-local-first-phase-1-cloud-phase-2.md), [0021-node-sqlite-driver-better-sqlite3.md](0021-node-sqlite-driver-better-sqlite3.md), [tech-stack.md](../tech-stack.md)

> Amended 2026-06-04: this ADR fixed "SQLite + Drizzle" and the desktop's encrypted
> `tauri-plugin-sql` path but left the **Node-side** SQLite driver open. That gap is now
> closed by [ADR-0021](0021-node-sqlite-driver-better-sqlite3.md): `@relavium/db` uses
> `better-sqlite3` for its Node consumers (the migration runner, tests, and the Phase-2
> CLI). The decision here is unchanged.

## Context

Expand Down
79 changes: 79 additions & 0 deletions docs/decisions/0021-node-sqlite-driver-better-sqlite3.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,79 @@
# ADR-0021: better-sqlite3 as the Node-side SQLite driver for `@relavium/db`

- **Status**: Accepted
- **Date**: 2026-06-04
- **Related**: [ADR-0005](0005-sqlite-drizzle-local-postgres-cloud.md), [ADR-0001](0001-tauri-v2-over-electron.md), [ADR-0018](0018-desktop-execution-and-rust-egress.md), [tech-stack.md](../tech-stack.md)

## Context

[ADR-0005](0005-sqlite-drizzle-local-postgres-cloud.md) settled the store —
**SQLite + Drizzle locally, PostgreSQL cloud, one Drizzle schema in `packages/db`** —
but it named only the *desktop's* SQLite access path (`tauri-plugin-sql`, Rust-side,
SQLCipher-encrypted; see [ADR-0001](0001-tauri-v2-over-electron.md) and
[ADR-0018](0018-desktop-execution-and-rust-egress.md)). It left **open** which driver
the *Node-side* consumers use. That gap has to be closed in Phase 0 (workstream 0.I),
because the `@relavium/db` scaffold needs a concrete driver for two things that run in
Node, not in the Tauri WebView:

- the **migration runner / client factory** the package exports, and
- the **Vitest smoke test** that opens a fresh database, applies every `drizzle-kit`
migration, and round-trips a row.

Phase-2's CLI run-history reader is also a Node consumer. The desktop reaches SQLite
through the Rust plugin and does **not** load this driver, so the choice is scoped to
Node contexts (tests now, CLI later), not the desktop binary.

Constraints that frame the choice: the repo's runtime floor is `engines.node >= 20.11.0`
(`.nvmrc` pins the 22 line); CI must build the driver without a bespoke native toolchain;
and CLAUDE.md non-negotiable #2 forbids a new runtime dependency without an ADR — hence
this record. Drizzle is fixed by ADR-0005; only the underlying driver is being decided.

## Decision

**We use `better-sqlite3` as the Node-side SQLite driver for `@relavium/db`, wired
through Drizzle's `drizzle-orm/better-sqlite3` adapter and its migrator.** Versions are
pinned in [tech-stack.md](../tech-stack.md) via the pnpm catalog.

Considered options:

1. **`better-sqlite3` + `drizzle-orm/better-sqlite3`** — Drizzle's reference SQLite
driver. Synchronous, which keeps the client factory and the migration runner trivial
(no async ceremony around schema setup); runs on Node ≥ 20.11 (matches `engines`); and
ships prebuilt binaries for common platforms so CI needs no native compiler. *Chosen.*
2. **Node's built-in `node:sqlite`** — zero added dependency, but it requires Node ≥ 22.5
(above our 20.11 floor), is still flagged experimental, and Drizzle's adapter for it is
immature. Adopting it would force raising the Node floor for a Phase-0 scaffold and bet
on an unstable API. *Rejected.*
3. **`@libsql/client` + `drizzle-orm/libsql`** — cross-platform and async, but oriented at
remote / embedded-replica libSQL use we do not need for a single local file in Phase 1,
and heavier than `better-sqlite3` for no Phase-1 benefit. *Rejected.*

This decision concerns only the Node-side **driver**. It does not touch the desktop's
encrypted `tauri-plugin-sql` path, the dialect-identical schema, or the Phase-2 Postgres
port — those remain as set by ADR-0005.

## Consequences

### Positive

- The `@relavium/db` client factory and migration runner are synchronous and small; the
0.I smoke test can open a fresh DB, run every migration, and assert a round-trip with no
async plumbing.
- Node ≥ 20.11 compatibility holds, so the documented `engines` floor stays honest and a
fresh checkout builds on the pinned toolchain without extra setup.
- Prebuilt binaries mean CI installs the driver without a native build step in the common
case, keeping the 0.G pipeline fast and deterministic.
- It is the most documented Drizzle SQLite path, so Phase-1/2 Node consumers (CLI run
history) inherit a well-trodden integration.

### Negative

- `better-sqlite3` is a native module: platforms without a prebuilt binary fall back to a
source build (needs a C++ toolchain), and the binary is ABI-bound to the Node version —
mitigated by pinning Node via `.nvmrc` and installing with a frozen lockfile in CI.
- It is a Node-only driver and is **not** the desktop's runtime path (the desktop uses the
Rust `tauri-plugin-sql`), so the persistence layer is exercised through two different
SQLite bindings; the single Drizzle schema is the shared contract that keeps them honest,
and migrations must be validated against the desktop path when that surface lands.
- Synchronous I/O blocks the calling thread; acceptable for the CLI and tests, and a
non-issue for the desktop, which never loads this driver.
1 change: 1 addition & 0 deletions docs/decisions/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -64,6 +64,7 @@ flowchart TD
| 0018 | [Desktop execution model — engine in WebView, Rust-delegated LLM egress](0018-desktop-execution-and-rust-egress.md) | Accepted | 2026-06-04 |
| 0019 | [Node-side OS-keychain access for the CLI — a maintained library, not the archived keytar](0019-cli-node-keychain-library.md) | Accepted | 2026-06-04 |
| 0020 | [Zod as the runtime schema and validation library](0020-zod-runtime-schema-library.md) | Accepted | 2026-06-04 |
| 0021 | [better-sqlite3 as the Node-side SQLite driver for `@relavium/db`](0021-node-sqlite-driver-better-sqlite3.md) | Accepted | 2026-06-04 |

## Creating a new ADR

Expand Down
2 changes: 1 addition & 1 deletion docs/tech-stack.md
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ locked.
| Frontend (all surfaces) | **Vite + React 19 + TanStack Router** | Not Next.js — SSE streaming + ReactFlow canvas are incompatible with RSC / edge runtime. See [ADR-0002](decisions/0002-vite-react-tanstack-not-nextjs.md). |
| Multi-LLM | **Internal `@relavium/llm` abstraction** over official provider SDKs (`@anthropic-ai/sdk`, `openai`, `@google/genai`; DeepSeek via the OpenAI-compatible adapter) | No 3rd-party framework, no Vercel, no Python sidecar; an owned provider-agnostic seam gives unified streaming + tool calling + cost across Anthropic / OpenAI / Gemini / DeepSeek with no vendor lock-in. See [ADR-0011](decisions/0011-internal-llm-abstraction.md). |
| Orchestration engine | **Pure TypeScript** (`packages/core`) | No LangGraph-Python; the same concepts are implementable in TS. Engine is framework-agnostic and runs identically on every surface. |
| Database (local, Phase 1) | **SQLite + Drizzle ORM** (SQLCipher) | Tauri plugin available; encrypted at rest with SQLCipher. |
| Database (local, Phase 1) | **SQLite + Drizzle ORM** (SQLCipher) | Tauri plugin available; encrypted at rest with SQLCipher. Node-side consumers (CLI, `@relavium/db` tests) use the **`better-sqlite3`** driver — see [ADR-0021](decisions/0021-node-sqlite-driver-better-sqlite3.md). |
| Database (cloud, Phase 2) | **PostgreSQL 16 + Redis 7 + BullMQ** | *Phase 2 only.* Same Drizzle schema, different driver. |
| API key storage | **OS keychain**, one `KeychainStore` interface with a per-surface accessor — desktop `tauri-plugin-keychain` (Rust), **CLI `@napi-rs/keyring`** (Node; *not* the archived `keytar`, see [ADR-0019](decisions/0019-cli-node-keychain-library.md)), VS Code `vscode.SecretStorage` | macOS Keychain / Windows Credential Manager / libsecret. Never plaintext, never sent to the frontend. See [ADR-0006](decisions/0006-os-keychain-for-api-keys.md). |
| CLI | **TypeScript + commander.js + ink** (`@clack/prompts` setup wizards; bundled to a single ESM bundle with `tsup`) | Same language as the engine; React for the TUI. |
Expand Down
Loading
Loading