diff --git a/.github/scripts/check-skill-version.sh b/.github/scripts/check-skill-version.sh index bdc2e19..90c9d3c 100755 --- a/.github/scripts/check-skill-version.sh +++ b/.github/scripts/check-skill-version.sh @@ -43,6 +43,8 @@ dep_version="$(sed -n 's/^termlens = .*version = "\([0-9][0-9.]*\)".*/\1/p;s/^te exit 1 } dep_minor="$(echo "$dep_version" | cut -d. -f1,2)" +msrv="$(sed -n 's/^rust-version = "\([0-9][0-9.]*\)"/\1/p' "$manifest" | head -1)" +[ -n "$msrv" ] || { echo "::error::no rust-version found in $manifest"; exit 1; } if [ "$skill_minor" != "$dep_minor" ]; then echo "::error::the vendored termlens skill is written against ${skill_version} but this workspace depends on ${dep_version}." @@ -50,4 +52,19 @@ if [ "$skill_minor" != "$dep_minor" ]; then exit 1 fi +for document in docs/SPEC.md docs/dependencies.md; do + [ -f "$document" ] || { echo "::error::$document does not exist"; exit 1; } + # Matches every spelling these two documents use: the docs.rs URL + # (termlens/0.11), the table cell (`termlens` | 0.11), the manifest + # excerpt (termlens = { version = "0.11" }) and running prose. + grep -Eq "termlens[^0-9]*${dep_minor//./\\.}" "$document" || { + echo "::error::$document does not name termlens ${dep_minor}" + exit 1 + } + grep -Fq "$msrv" "$document" || { + echo "::error::$document does not name rust-version ${msrv}" + exit 1 + } +done + echo "the vendored termlens skill (${skill_version}) matches the dependency (${dep_version})" diff --git a/.github/scripts/test_ci.py b/.github/scripts/test_ci.py index 17e3b1a..2c518fa 100644 --- a/.github/scripts/test_ci.py +++ b/.github/scripts/test_ci.py @@ -235,7 +235,12 @@ def run_check(self, skill_version, dep_line): skill.parent.mkdir(parents=True) header = f"Written against **termlens {skill_version}**.\n" if skill_version else "" skill.write_text("# Testing terminal programs with termlens\n\n" + header) - (root / "Cargo.toml").write_text(f"[workspace.dependencies]\n{dep_line}\n") + (root / "Cargo.toml").write_text( + f'[workspace.package]\nrust-version = "1.94.1"\n[workspace.dependencies]\n{dep_line}\n' + ) + (root / "docs").mkdir() + for document in ("SPEC.md", "dependencies.md"): + (root / "docs" / document).write_text("termlens 0.10\nrust-version 1.94.1\n") return subprocess.run(["/bin/bash", str(script)], text=True, capture_output=True, cwd=directory) def test_a_matching_skill_passes_and_a_patch_release_is_not_drift(self): @@ -259,6 +264,11 @@ def test_an_unreadable_claim_is_a_failure_not_a_pass(self): self.assertNotEqual(self.run_check("", 'termlens = "0.10"').returncode, 0) self.assertNotEqual(self.run_check("0.10.1", "insta = \"1\"").returncode, 0) + def test_stale_documented_dependency_version_fails(self): + run = self.run_check("0.11.0", 'termlens = "0.11"') + self.assertNotEqual(run.returncode, 0) + self.assertIn("docs/SPEC.md does not name termlens 0.11", run.stdout) + def test_the_committed_skill_matches_the_committed_manifest(self): run = subprocess.run(["/bin/bash", str(SCRIPTS / "check-skill-version.sh")], text=True, capture_output=True, cwd=ROOT) diff --git a/docs/SPEC.md b/docs/SPEC.md index 8fe1a6a..70910c4 100644 --- a/docs/SPEC.md +++ b/docs/SPEC.md @@ -33,7 +33,7 @@ Verified facts about the development machine (2026-08-25): Linux 7.0 x86_64, 20 - Every public item has a doc comment; every crate has `//!` docs stating its role and which internal crates it may depend on. - Observability via `tracing` spans/events (operator execution, allocations, spills, plan submission). No `println!` outside the CLI's user-facing output. - Tests: colocated unit tests plus `tests/` integration tests; snapshot tests via `insta` (commit the `.snap` files). Deterministic: seed every RNG; no wall-clock dependence in rendered output. -- Toolchain: **stable Rust**, pinned in `rust-toolchain.toml` (`channel = "1.98.0"`, `components = ["rustfmt", "clippy"]`); `workspace.package.rust-version = "1.88"` (the MSRV of Ballista 54, DataFusion 54 and ratatui 0.30 — the highest in the dependency graph). Do **not** use nightly `std::simd`: CPU vectorization comes from Arrow/DataFusion compute kernels plus `rayon` data parallelism and auto-vectorization. (If profiling later justifies explicit SIMD, the `wide` crate is the stable escape hatch — do not add it now.) +- Toolchain: **stable Rust**, pinned in `rust-toolchain.toml` (`channel = "1.98.0"`, `components = ["rustfmt", "clippy"]`); `workspace.package.rust-version = "1.94.1"` (the lockfile's floor, verified by `make msrv`). Do **not** use nightly `std::simd`: CPU vectorization comes from Arrow/DataFusion compute kernels plus `rayon` data parallelism and auto-vectorization. (If profiling later justifies explicit SIMD, the `wide` crate is the stable escape hatch — do not add it now.) - Workspace lints (every crate sets `[lints] workspace = true`; test modules may locally `#[allow(clippy::unwrap_used, clippy::expect_used)]`): ```toml @@ -144,7 +144,7 @@ There is no private file format (ADR-0014). `oxidelake-storage` owns configurati - A deterministic demo binary `oxidelake-tui-demo` (`[[bin]]` inside `oxidelake-tui`) renders the dashboard from a fixed synthetic telemetry snapshot — no clock, no animation — so PTY tests can spawn it via `env!("CARGO_BIN_EXE_oxidelake-tui-demo")` (`termlens::bin!`). - **Headless tests, two layers:** 1. `crates/oxidelake-tui/tests/tui_render_test.rs` — in-process: `ratatui::backend::TestBackend` + `insta::assert_snapshot!` of the rendered buffer at 80×24 and 120×40; state transitions (`↓`/`↑` selection, `Tab`, `q` → quit) asserted on `AppState`. - 2. `crates/oxidelake-tui/tests/tui_pty_test.rs` — end-to-end with **`termlens`** (dev-dependency; its default feature enables `insta` integration): `termlens::bin!("oxidelake-tui-demo", size(80, 24), timeout(..))`, then `wait_frame(|s| s.contains("OxideLake"))`, `termlens::assert_screen_snapshot!(&first, styles = false)`, `send(Key::Down)` and re-snapshot, `resize(120, 40)` and re-snapshot, `send(Key::Char('q'))` then `wait_exit()?.success()`. Beside it, `tests/emulation.rs` pins what the emulator dropped (`Screen::unsupported`), that no terminal mode was left set and that a screen survives the snapshot text format and JSON; `tests/termlens_cli.rs` drives the committed screens through `termlens-cli` (`#[ignore]`d — it installs the tool); and `crates/oxidelake-runtime/tests/oxide_tui_pty.rs` does the same for the shipped `oxide tui`. Read before writing these — the API is newer than your training data, and the vendored `.claude/skills/termlens/SKILL.md` is the cheat sheet for the version this workspace depends on. Never `sleep`; always use the `wait_*` methods. termlens is a real-PTY harness, so no physical TTY is needed. + 2. `crates/oxidelake-tui/tests/tui_pty_test.rs` — end-to-end with **`termlens`** (dev-dependency; its default feature enables `insta` integration): `termlens::bin!("oxidelake-tui-demo", size(80, 24), timeout(..))`, then `wait_frame(|s| s.contains("OxideLake"))`, `termlens::assert_screen_snapshot!(&first, styles = false)`, `send(Key::Down)` and re-snapshot, `resize(120, 40)` and re-snapshot, `send(Key::Char('q'))` then `wait_exit()?.success()`. Beside it, `tests/emulation.rs` pins what the emulator dropped (`Screen::unsupported`), that no terminal mode was left set and that a screen survives the snapshot text format and JSON; `tests/termlens_cli.rs` drives the committed screens through `termlens-cli` (`#[ignore]`d — it installs the tool); and `crates/oxidelake-runtime/tests/oxide_tui_pty.rs` does the same for the shipped `oxide tui`. Read before writing these — the API is newer than your training data, and the vendored `.claude/skills/termlens/SKILL.md` is the cheat sheet for the version this workspace depends on. Never `sleep`; always use the `wait_*` methods. termlens is a real-PTY harness, so no physical TTY is needed. ### 2.8 API — `oxidelake-api` @@ -204,7 +204,7 @@ Rules: 1. Every external dependency is declared once in `[workspace.dependencies]`; members use `{ workspace = true }`. 2. **Version coherence has one root: Ballista.** Its docs say "Make sure the version of `datafusion` is the same as `ballista`'s!" — so `ballista*` fixes the `datafusion` / `datafusion-proto` major, DataFusion fixes the `arrow` / `parquet` / `object_store` majors, and `arrow-flight` / `tonic` / `prost` arrive transitively through Ballista (not declared directly). Never depend on other `datafusion-*` sub-crates; `datafusion-proto` is the single exception (needed for the plan codec) and is pinned to the identical version (ADR-0009). Gate: `cargo tree --workspace -d -e normal` shows **no duplicate `arrow-*`, `parquet`, `datafusion*`, `object_store`, `tonic` or `prost` majors**. -3. Evidence for the set below (live registry): ballista / ballista-core / ballista-scheduler / ballista-executor 54.1.0 (2026-08-09) require `datafusion ^54`, `datafusion-proto ^54`, `arrow-flight ^58.3`, `object_store ^0.13.2`, `tonic ^0.14`, `prost ^0.14`; datafusion 54.1.0 requires `arrow ^58.3`, `parquet ^58.3`, `object_store ^0.13.2`, `tokio ^1.52`; arrow-flight 58.4 requires `tonic` / `prost ^0.14.1`; ratatui 0.30.2 pairs with crossterm 0.29 through `crossterm_0_29`; cudarc 0.19.9 ships `dynamic-loading`, `nvrtc` and `cuda-*`; objc2-metal 0.3 requires objc2 0.6 and objc2-foundation 0.3; termlens 0.10.1 defaults to `insta` and keeps `serde` / `regex` behind features (MSRV 1.85, far under this workspace's floor). MSRVs: Ballista, DataFusion 54 and ratatui 1.88 → `rust-version = "1.88"`. DataFusion 55 exists but Ballista is on 54 — Ballista wins. When Ballista moves to a new DataFusion major, re-derive the whole chain; never bump one crate alone. +3. Evidence for the set below (live registry): ballista / ballista-core / ballista-scheduler / ballista-executor 54.1.0 (2026-08-09) require `datafusion ^54`, `datafusion-proto ^54`, `arrow-flight ^58.3`, `object_store ^0.13.2`, `tonic ^0.14`, `prost ^0.14`; datafusion 54.1.0 requires `arrow ^58.3`, `parquet ^58.3`, `object_store ^0.13.2`, `tokio ^1.52`; arrow-flight 58.4 requires `tonic` / `prost ^0.14.1`; ratatui 0.30.2 pairs with crossterm 0.29 through `crossterm_0_29`; cudarc 0.19.9 ships `dynamic-loading`, `nvrtc` and `cuda-*`; objc2-metal 0.3 requires objc2 0.6 and objc2-foundation 0.3; termlens 0.11 defaults to `insta` and keeps `serde` / `regex` behind features (MSRV 1.85, far under this workspace's floor). MSRVs: the locked dependency graph requires `rust-version = "1.94.1"`. DataFusion 55 exists but Ballista is on 54 — Ballista wins. When Ballista moves to a new DataFusion major, re-derive the whole chain; never bump one crate alone. ```toml [workspace] @@ -214,7 +214,7 @@ members = ["crates/*"] [workspace.package] version = "0.1.0" edition = "2024" -rust-version = "1.88" +rust-version = "1.94.1" license = "Apache-2.0" authors = ["OxideLake Contributors"] @@ -257,7 +257,7 @@ crossterm = "0.29" clap = { version = "4", features = ["derive"] } # test-only insta = "1" -termlens = { version = "0.10", features = ["serde"] } +termlens = { version = "0.11", features = ["serde"] } assert_cmd = "2" tempfile = "3" rand = "0.10" diff --git a/docs/decisions/ADR-0011-tui-testing-layers.md b/docs/decisions/ADR-0011-tui-testing-layers.md index cc62d51..8b93b14 100644 --- a/docs/decisions/ADR-0011-tui-testing-layers.md +++ b/docs/decisions/ADR-0011-tui-testing-layers.md @@ -25,3 +25,22 @@ The two layers stand; the dependency moved 0.9 → 0.10.1 and the PTY layer grew - `tests/termlens_cli.rs` reads the committed `.snap` files back with `termlens-cli`. It is `#[ignore]`d: these crates are published, and `cargo test` must not install a tool behind a contributor's back. CI runs it (`make test-termlens-cli`). The vendored agent skill is checked against the dependency by `make skill-version`, because it had already drifted two releases behind. + +## Update — 2026-09-11 (termlens 0.11) + +The dependency moved 0.10.1 → 0.11, termlens's stability candidate: from it +no promised item changes incompatibly before its 1.0, so this requirement +should hold for a while. Neither layer changed shape. + +Its one breaking change simplified the invariant the PTY layer rests on. +`Screen::unsupported()` returns a view instead of a slice of `Arc` and +`unsupported_overflow()` folds into it, so the pinned list and "the record +is not truncated" became one assertion in both suites — the dashboard's +(`oxidelake-tui/tests/emulation.rs`) and the shipped binary's +(`oxidelake-runtime/tests/oxide_tui_pty.rs`). The view compares equal to a +slice only when the retained shapes match *and* nothing overflowed the +bound, so a truncated record can no longer pass as a shorter list. + +`make skill-version` now also holds `docs/SPEC.md` and `docs/dependencies.md` +to the dependency's major.minor and to the workspace `rust-version`, because +both had drifted behind this bump. diff --git a/docs/dependencies.md b/docs/dependencies.md index d036993..f34b9e5 100644 --- a/docs/dependencies.md +++ b/docs/dependencies.md @@ -29,7 +29,7 @@ The pins live in [SPEC.md §4](SPEC.md). This page records *why* those numbers, | `objc2-metal` | 0.3 | `objc2 >=0.6.2, <0.8`, `objc2-foundation ^0.3.2` | macOS target-gated, behind `metal` | | `objc2` / `objc2-foundation` | 0.6 / 0.3 | — | | | `io-uring` | 0.7 | — | Linux target-gated, behind `io-uring`; chosen over `tokio-uring` 0.5, which pins `io-uring ^0.6` and needs its own runtime ([ADR-0007](decisions/ADR-0007-io-uring-crate-dedicated-thread.md)) | -| `termlens` | 0.10 (dev) | default feature `insta`; feature `serde` | real-PTY harness driving `oxidelake-tui-demo` and `oxide tui`; MSRV 1.85; `serde` is what `TERMLENS_ARTIFACT_DIR` writes in CI and what `tests/emulation.rs` round-trips; read before use, and refresh `.claude/skills/termlens/SKILL.md` with the dependency (`make skill-version`) | +| `termlens` | 0.11 (dev) | default feature `insta`; feature `serde` | real-PTY harness driving `oxidelake-tui-demo` and `oxide tui`; MSRV 1.85; `serde` is what `TERMLENS_ARTIFACT_DIR` writes in CI and what `tests/emulation.rs` round-trips; read before use, and refresh `.claude/skills/termlens/SKILL.md` with the dependency (`make skill-version`) | | `insta` | 1 (dev) | — | snapshot files are committed | | `assert_cmd` | 2 (dev) | — | CLI end-to-end tests, including the spawned scheduler + worker; it captures pipes, so anything about a *terminal* is a termlens test | | `serde_json` | 1 (dev) | — | reads a serialized termlens `Screen` back in `oxidelake-tui/tests/emulation.rs`; already in the lockfile through DataFusion |