docs(ci): establish docs/ci/adr namespace (ci-001 pipeline, ci-002 flake)
ci / gate (push) Successful in 2m33s

Records the CI/release pipeline as ADR-ci-001 and relocates the nix-flake
ADR from main's ADR-0049 to ADR-ci-002 (content unchanged, history note
added). Both live in docs/ci/adr/ with a README index — a dated,
ci-segmented namespace disjoint from main's integer ADR sequence, the
same split the website subproject uses to avoid cross-branch number
collisions. Drops the ADR-0049 entry from docs/adr/README.

ci-001 covers the runner model, the baked nix CI image, the clippy+test
gate, the static-musl release on tag, trigger hygiene, auth, and scope.
This commit is contained in:
claude@clouddev1
2026-06-12 22:38:34 +00:00
parent 89b9392c25
commit da8bfebc36
4 changed files with 221 additions and 7 deletions
@@ -1,127 +0,0 @@
# ADR-0049: Nix flake for a reproducible dev + build environment
## Status
**Accepted (2026-06-12).** Implemented the same day on the `ci` branch:
`flake.nix`, `flake.lock`, `rust-toolchain.toml`, `.envrc`. Verified
end-to-end before acceptance — `nix develop` provides the pinned
toolchain; `nix build .#default` produces a working binary; `cargo
clippy --all-targets -- -D warnings` is clean and `cargo test` is
**2424 passed / 0 failed / 1 ignored** (the ignored item is the
intentional ```` ```ignore ```` doctest at `src/friendly/mod.rs:21`),
all run *through the flake*. This ADR is the dev/build-environment
half of the CI work; the CI **pipeline** itself (runner wiring,
release matrix) is decided separately as it settles.
## Context
The project is near feature-complete and CI is finally being set up
(`requirements.md` **TT5**, **CI** in the deferred list). CI must not
depend on whatever Rust/toolchain happens to be installed on the build
machine — that is neither reproducible nor honest about what the build
needs.
The sibling project **datamage** already solved this with a Nix flake
(its ADR 0046): the flake is the single, version-pinned declaration of
the toolchain, and both the dev shell and CI go through it so they
cannot drift. We adopt the same pattern here. Ours is dramatically
simpler than datamage's — this is a pure-Rust TUI with no Tauri /
WebKitGTK / Node / WASM surface — so the flake carries almost no system
dependencies.
Two build facts drove the (tiny) dependency set, confirmed from
`Cargo.lock`:
- **`libsqlite3-sys` is built with `bundled`** → SQLite is compiled
from vendored C, which needs a C compiler. `nixpkgs`' `stdenv`
provides one automatically; nothing is declared for it.
- **`arboard`'s clipboard backend is `x11rb`** — a pure-Rust socket
XCB client that links *no* C X11 libraries. So no X11/`pkg-config`
system inputs are needed to build or test. A live X server is only
required at *runtime* to actually copy; headless sessions fall back
to OSC 52.
## Decision
Adopt a **Nix flake** at the repository root as the canonical
declaration of the dev *and* build environment.
- **`flake.nix`** exposes two outputs (user-chosen 2026-06-12 over a
dev-shell-only variant):
- **`devShells.default`** — the pinned Rust toolchain (from
`rust-toolchain.toml` via `rust-overlay`) plus `cargo-sweep` for
the `target/` build-hygiene discipline (CLAUDE.md / the datamage
ADR 0050 equivalent).
- **`packages.default`** (= `packages.rdbms-playground`) — a
`rustPlatform.buildRustPackage` that produces the binary
reproducibly from the pinned toolchain and the committed
`Cargo.lock` (`cargoLock.lockFile` → `importCargoLock`, which
fetches each dependency by its lockfile checksum: offline,
deterministic, no `cargoHash` to churn). `nix build` yields the
artifact CI's gate/release can consume.
- **`rust-toolchain.toml`** pins an **exact stable release**
(`1.95.0`), not the floating `stable` channel, so `nix flake update`
cannot surprise-bump Rust into new clippy lints that would fail the
`-D warnings` gate (same reasoning as datamage ADR 0046). Components:
`rustfmt` + `clippy`. No coverage/WASM tooling and no
cross-compilation targets yet — those are added when the release
matrix needs them, not before.
- **`flake.lock`** pins every input (`nixpkgs` `nixos-26.05`,
`rust-overlay`, `flake-utils`) to a commit, making the env
bit-reproducible.
- **`.envrc`** contains `use flake` for direnv auto-activation, kept
for parity with datamage even though direnv is not installed on the
current dev VM (entry is via `nix develop`).
- **`packages.default` sets `doCheck = false`.** The test suite is
*not* run during `nix build` — the Nix build sandbox has no `HOME`
and no X server, which fights the project-directory / clipboard
paths the tests touch. Tests run as their own CI stage via
`nix develop -c cargo test`, keeping "build the artifact" and "run
the suite" cleanly separate.
- **The package version is read from `Cargo.toml`** via
`builtins.fromTOML`, so it never drifts from the crate metadata.
## Consequences
- **One toolchain definition.** Dev and CI share the exact pinned
toolchain; they cannot drift. New contributors run `nix develop`
(or get auto-activation via direnv) and have the same Rust as CI.
- **D2 (static binary) is unaffected and still pending.** The
`nix build` artifact links the Nix-store glibc *dynamically* — it is
a reproducible build/test artifact, **not** the single static
release binary D2 calls for. Release binaries will target a static
toolchain (e.g. `x86_64-unknown-linux-musl`) in the forthcoming CI
release work; that is a release-step concern, not a dev-shell one.
- **`fmt` is deliberately *not* gated yet.** The tree is not clean
under stock `rustfmt` (~100 files would change; no `rustfmt.toml` is
committed and the code was shaped by something other than default
`rustfmt`). Reformatting churns blame across every file and would
conflict with the in-flight website branch and ongoing `main` work,
so — user decision 2026-06-12 — the `fmt` gate is left out for now
and revisited on `main`. The CI gate is `clippy` + `test`.
- **Engine-name posture (CLAUDE.md) is respected.** The flake's
comments may name SQLite/`rusqlite` where technically necessary
(build-input rationale); no user-facing string is affected.
## Alternatives considered
- **Dev-shell only (no build package).** Matches datamage exactly; CI
would `cargo build` inside `nix develop -c`. Rejected (user choice):
a `nix build` package gives a reproducible release artifact straight
from the pinned toolchain, which the release job wants.
- **A standard `rust:1.95` image in CI, flake for dev only.** Simpler
in CI (no nix-in-CI caching to solve), but it is a *second* place
that defines the toolchain — exactly the drift this ADR exists to
prevent. Rejected for the unified-env goal; the nix-in-CI caching
cost is solved in the CI pipeline work instead.
- **`rustup` on the build machine.** The status quo CI would replace —
non-reproducible, machine-dependent, the thing we are eliminating.
## Relationship to other decisions
- Mirrors **datamage ADR 0046** (nix flake dev env) and its build
hygiene companion. This is the rdbms-playground analogue, scoped to
a pure-Rust project.
- Feeds the CI pipeline work for `requirements.md` **TT5** (CI runs
the tiers) and the **D1/D2/D3** distribution items (the release
matrix consumes `nix build` / a static target).
-1
View File
@@ -54,4 +54,3 @@ This directory contains the project's ADRs, recorded per
- [ADR-0046 — Schema sidebar focus/navigation mode and responsive input & hint layout (UI #20/#21/#23)](0046-sidebar-navigation-and-responsive-input-hint.md) — **Accepted + implemented 2026-06-10, phased A→B→C** (8 commits `9f5f76b``22bec61`; closes Gitea **#20** hint jumpiness, **#21** left-column improvements, **#23** long input — all forks user-confirmed, including the persistent show/hide toggle which is **deferred**: the Ctrl-O peek covers #21's "keystroke to show and hide"). Two decisions landed differently from the draft (recorded inline): relationship data on **`App`** not `SchemaCache` (DB2); the nav overlay clears **only the sidebar strip + a one-column gutter**, panels staying visible behind (DC2). Treats the three UI issues as one coupled decision because they share the terminal's width/height budget. **Phase A (input & hint):** the hint panel's height becomes a function of **terminal geometry, fixed between resizes** (not of hint content), eliminating the #20 jump at its source — measured catalog shows ≥ ~54-col right-column width never needs > 2 hint lines, so 3 lines is a rare narrow-terminal-only case; height buckets `H<40` compact (input 1 row + horizontal scroll / hint 2) vs `H≥40` comfortable (input 2 rows soft-wrap / hint 2), output `Min(5)` honoured first under degradation; input gains horizontal scroll (`input_scroll_offset`, single logical `String`**not** I1 multi-line) and 2-row soft-wrap display when tall, preserving ADR-0027's 6-col indicator reserve. **Phase B (sidebar):** the 26-col Tables column is **kept but made optional and richer** (not deleted — pedagogy wins ties) — **width-derived session-only** visibility (visible iff width > 90 or a Ctrl-O peek is active — no stored field; hides at width ≤ 90 so the 90-col screencasts drop it; ADR-0015 format untouched), plus a **relationships panel** rendered narrow with endpoints broken at the arrow, ellipsized — a **separate sibling panel** that **overrides S2**'s nested-list extension model (relationships are cross-table). the full records live on a new **`App.relationships`** field (revised from the ADR's original `SchemaCache.relationship_details` at implementation — `SchemaCache` is walker-facing and needs only the names, kept in `relationships: Vec<String>`; details are UI-only, so `App` mirrors `app.tables` and avoids ~23 fixture edits), delivered by `Database::read_all_relationships` + an `AppEvent::RelationshipsRefreshed`; the two left panels split vertically with the relationships panel floored at 5 rows ("(none)" when empty) and capped at 50 % of the column (DB4). **Phase C (navigation mode):** **`Ctrl-O`** enters a focus cycle (Input → Tables → Relationships → Input; `Esc` exits) orthogonal to the ADR-0003 input mode — **`Ctrl-B` was rejected on review as the default tmux prefix** (unreachable inside tmux); the focused panel **expands to ~4050 cols as a `Clear` overlay** (right panels stay unchanging underneath) and scrolls via **Up/Down (line) + PageUp/PageDown (page)** (context-rebind, reusing the output-scroll viewport mechanism), with an accent focus border; all non-nav keys inert in nav mode (and nav keys inert while a modal is open). Forks all user-chosen: keep-optional-richer (vs remove/narrow); navigation-mode (vs modeless modifier scroll); `Ctrl-O` (Ctrl-B rejected = tmux prefix); overlay (vs layout re-split); inert-non-nav-keys; geometry-fixed hint height; `H<40/≥40` thresholds; session-only persistence; Up/Down line-scroll; **separate relationships panel overriding S2**; **no hint-area toggle** (S4's stale "keyboard-toggleable" claim struck — never implemented, unwanted). A pre-build `/runda` DA pass drove these corrections: caught the `Ctrl-B`/tmux collision, the `SchemaCache` retype that would have broken completion, the 2-row-input/indicator placement, the missing nav-mode key disposition + modal gate, and three unreferenced requirements (S1 evolved, S2 overridden, S4 corrected); also cross-checked open issue **#22** (overlay/annotation layer — separate ADR, adjacent). OOS: true multi-line input (I1); readline shortcuts (I1b); cross-session sidebar persistence; output as a third nav focus; relationship search/edit from the panel; hint-area toggle; #22's annotation layer. Accepted consequence: the 90-col visibility threshold makes a terminal's output *narrower* when widened across the boundary (sidebar appears)
- [ADR-0047 — Demonstration overlay layer (keystroke badges + step captions)](0047-demonstration-overlay-layer.md) — **Accepted 2026-06-10; implemented 2026-06-11, phased A→B→C (closes Gitea #22)** (commits `f879d54``2d0f4b2`; no `requirements.md` item — tracked by issue + ADR per convention; all forks user-confirmed + a pre-build `/runda` pass that produced 10 tightening findings and a whole-implementation `/runda` pass that returned PASS, no blockers). An in-app **demonstration mode** (`--demo` flag / `RDBMS_PLAYGROUND_DEMO` env, **off by default, zero footprint when off**) that renders two transient overlays so `autocast` screencasts — and live teaching, and a future guided-lesson system — can show otherwise-invisible interactions. **Keystroke badges** (`[TAB]`, `[ENTER]`, `[UP]`, …): **automatic, app-detected** over a fixed set of glyph-less keys (the app already sees every key, so it re-records for free), label via a pure `demo_badge_label(&KeyEvent)`; the badge **auto-expires on a ~1.5 s timer** that extends the runtime's existing time-boxed-`recv` arm condition (`debounce.is_armed() || badge_pending`; expiry `Instant` in the runtime, `App.demo_badge` the render mirror — mirroring the `input` vs `input_indicator` split). **Step captions**: a **stealth, control-code-delimited input buffer** toggled by **`Ctrl+]`** (byte `0x1D` → arrives as `Char('5')+CONTROL`, verified against crossterm 0.29 `parse.rs:110-113`; chosen over `Ctrl+!`, which is **not a single ASCII byte so autocast cannot send it** — the same wall as arrow keys, R4) — typed characters accumulate **invisibly** (prompt untouched, no echo/history), `Backspace` edits, other keys inert, a second `Ctrl+]` **commits** to the caption box (empty commit dismisses); lives in pure-sync `App::update()`, **intercepted before the modal gate** so captions/badges work **over the load picker** (the `#24` projects cast). Both render as **floating flat black-on-yellow rectangles** (solid fill, **no border glyphs** — a one-cell text margin, deliberately unlike the app's bordered panels; user decision post-build, `2d0f4b2`) **at the output panel's inner bottom-right**, drawn **last over modals**, badge **stacked above** the caption, **no layout reflow**; caption **word-wraps to ≤ 3 lines** (35 rows), badge fixed 3 rows; clamp/skip guard for tiny terminals; a new **`App.last_output_area: Rect`** (set in `render_output_panel`) gives the top-level draw the anchor. Caption persists **until the next keystroke**; badge suppressed while capturing. Forks all user-chosen: `--demo` activation (vs hidden command / chord); automatic badges (vs scripted); stealth buffer (vs typed-command / preloaded-file); floating bottom-right boxes (vs HUD / banner / subtitle); `Ctrl+]` trigger; wrap-to-3-line captions; ~1.5 s badge / next-keystroke caption timing. Tested test-first across Tier 1 (label fn, capture state machine incl. over-modal + demo-off gate, nearest-deadline helper), Tier 2 (insta snapshots: badge/caption/both-stacked at 90×26 light+dark, short-terminal clamp), Tier 3 (`--demo` plumbing, badge set/suppressed, caption-without-input wiring), CLI (`--demo` parse + env fallback) — with an **honest limit** noted: the `tokio` timer wiring inside `run_loop` is exercised via the pure pieces + Tier-3 plumbing, not a standalone integration test of the timeout (same posture as the existing `IndicatorDebounce`). One intentional, user-acknowledged behaviour: `Ctrl-C` is inert while capturing (every non-`Ctrl+]` key is, by spec). Final tally **2290 passing / 0 failing / 0 skipped** (1 long-standing ignored doctest), clippy clean. OOS: scripted/manual badge push; badges for glyph keys; configurable styling/placement; the guided-lesson system itself (own ADR); cross-session/-switch persistence; localised caption content; arrow-only cast interactions (output-pane scroll); wiring the overlays into the website `casts.mjs` scripts (website-branch follow-up). Implementation phased **A** (`--demo` plumbing) → **B** (badges) → **C** (captions) + a flat-rectangle restyle
- [ADR-0048 — `seed` fake-data generation command](0048-seed-fake-data-generation.md) — **Accepted 2026-06-11; Phase 1 + Phase 2 implemented 2026-06-11** (Phase 1 commits `202e25a``fbd219b`; design settled with the user across an extended fork dialogue, hardened by a pre-build `/runda` pass (six blockers folded in), a post-implementation `/runda` pass (eight gaps closed — FK/shortid determinism so **D4 holds with no exceptions**, plus six untested ADR decisions), and a Phase-2 pre-build `/runda` pass (which caught the no-date-literal-token reality → the D2 quoted-dates amendment), and a post-implementation `/runda` pass (which added a friendly error for a bounded override on a UNIQUE column — see D2); **2400 tests pass, clippy clean**). Closes `requirements.md` **SD1** and the core of **SD2**; closes the `seed` half of **A1**. **Phase 1 shipped:** whole-row `seed <table> [count] [--seed <n>]` with realistic name-aware generation (the `fake` crate + a type-gated heuristic catalogue, table-context name disambiguation, hand-rolled `product` generator, bounded dates), identifier + constraint uniqueness incl. junction distinct-combos, FK sampling from existing parent rows (empty-parent error), `IN`-CHECK derivation + complex-CHECK advisory, a required-column block guard, `--seed` reproducibility (serial/FK/shortid all deterministic), undo as one batch step, replay as a data write, a capped auto-show preview, the enum/CHECK advisory, and an O(N) single-transaction insert path. **Phase 2 shipped (2026-06-11):** the `set` override clause (D2 — fixed value / pick-list / `as <generator>` / `between` range, **quoted** dates per the D2 amendment, type-aware, override drops the column from the advisory) and the `<table>.<column>` column-fill form (D1 form 2 — an UPDATE over existing rows, refusing PK/autogen targets, empty-table no-op, FK/unique-respecting, one undo step), with the new `KNOWN_GENERATORS` vocabulary (D9), a range `Generator`, full completion/highlight (`HighlightClass::Function`)/validity (`IdentSource::Generators`)/help/pedagogy wiring, and the D13 advisory's Phase-2/3 wording. Further SD2 increments (custom generators, NULL injection, multi-locale, recursive auto-seed) out of scope. Closes `requirements.md` **SD1** and the core of **SD2**; closes the `seed` half of **A1** (the other being `hint`/**H2**). A dedicated `seed` command (own AST variant + `do_seed` executor, **both modes**) generating **realistic, name-aware** fake data. Two forms: **`seed <table> [count]`** (new rows, default **20**, capped) and **`seed <table>.<column>`** (fill a column on existing rows, an UPDATE). Generation adds the **`fake` crate** (v5, English) driven by a **type-gated, token-matched name-heuristic catalogue** (~30 patterns, documented false-positive guards), with **table-context** disambiguating the `name`/`title` family (`products.name`→product, `users.name`→person, `vendors.name`→company), a **hand-rolled `product` generator** (`fake` has no commerce module), **bounded dates** (`date`/`timestamp`/`dob`/`*_at` recognised, recent windows — never "all of history"), the **identifier family** (`id`/`code`/`ref`/`number`, non-FK/non-PK) → **unique sequential**, and **enum-ish names** (`role`/`status`/`type`/…) left generic + a **post-seed Hint advisory** pointing at `set … in (…)`. A **`set` override clause** — `= value` / `in (a,b,c)` / `as <generator>` / `between a and b` (numeric **and** date), reusing ADR-0026 operators — answers the heuristic-miss case. **`--seed <n>`** makes runs reproducible (and enables exact-value tests). **FK** columns sampled uniformly from existing parent rows (**empty parent → friendly error**, no recursion v1); **junction/compound-PK** tables seeded with **distinct combinations**, capped + noted (SD1). A **required-column block guard** refuses rather than NULL-violate a `NOT NULL` column it can't fill (e.g. `NOT NULL blob`). Full ambient wiring (completion incl. a new generator-name vocabulary highlighted as `tok_function`, hints, `help seed`, ADR-0042 near-miss matrix, ADR-0027 validity); **no DSL→SQL teaching echo** (seed is a utility command, not a SQL twin). Honours **X5**`do_seed` reuses insert/update *mechanics as helpers*, not by emitting `Command::Insert`. Implementation phased: (1) core whole-row seed → (2) `set` overrides → (3) column-fill. Deferred (future SD2): recursive auto-seed, NULL injection, multi-locale, user-defined custom generators, full per-column report
- [ADR-0049 — Nix flake for a reproducible dev + build environment](0049-nix-flake-dev-and-build-env.md) — **Accepted + implemented 2026-06-12** (`ci` branch; first step of the CI work toward `requirements.md` **TT5** + **D1/D2/D3**). Adopts a root **Nix flake** as the single, version-pinned declaration of the dev *and* build toolchain so CI never relies on whatever Rust is installed on the build machine — mirroring **datamage ADR 0046**, but far simpler (pure-Rust TUI: no Tauri/WebKit/Node/WASM). Two outputs (user-chosen over dev-shell-only): **`devShells.default`** (pinned toolchain via `rust-toolchain.toml` + `rust-overlay`, plus `cargo-sweep`) and **`packages.default`** (a `rustPlatform.buildRustPackage` building the binary reproducibly from the committed `Cargo.lock` via `importCargoLock`; `doCheck = false` — the suite runs as its own `nix develop -c cargo test` stage, not in the HOME/X-less build sandbox; version read from `Cargo.toml` via `fromTOML`). Toolchain pinned to **exact `1.95.0`** (not floating `stable`) so `nix flake update` can't surprise-bump clippy lints past the `-D warnings` gate; components `rustfmt` + `clippy`; **no** cross/WASM targets yet (added when the release matrix needs them). System inputs are nearly empty by design — `libsqlite3-sys` `bundled` needs only the stdenv C compiler; `arboard``x11rb` is pure-Rust (no C X11 libs, X server only needed at *runtime*, OSC 52 otherwise). `.envrc` (`use flake`) kept for direnv parity though direnv isn't on the current VM. **Verified before acceptance:** `nix develop` toolchain pinned, `nix build` yields a working binary, clippy clean, **2424 tests pass / 0 fail / 1 intentional ignored doctest** — all through the flake. Consequences: the `nix build` artifact is glibc-**dynamic** (a reproducible build/test artifact, **not** the D2 static release binary — release uses a static target like `x86_64-unknown-linux-musl`, deferred to the CI release work); the **`fmt` gate is deliberately left out for now** (user decision — the tree isn't clean under stock `rustfmt`, ~100 files would churn and conflict with the website/`main` work; revisit on `main`), so the gate is **`clippy` + `test`**. Alternatives rejected: dev-shell-only (no reproducible artifact); a standard `rust:1.95` CI image (a second toolchain definition = drift, the very thing this prevents); `rustup` on the build host (non-reproducible — the status quo being eliminated). The CI **pipeline** itself (runner wiring, release matrix) is decided separately as it settles.