From 114a3e465dff188b31432968d5f5fc6baa50ad47 Mon Sep 17 00:00:00 2001 From: nightcityblade Date: Sun, 13 Sep 2026 23:22:09 +0800 Subject: [PATCH 1/2] fix: keep dependency docs in sync Signed-off-by: nightcityblade --- .github/scripts/check-skill-version.sh | 15 +++++++++++++++ .github/scripts/test_ci.py | 12 +++++++++++- docs/SPEC.md | 10 +++++----- docs/decisions/ADR-0011-tui-testing-layers.md | 4 ++-- docs/dependencies.md | 2 +- 5 files changed, 34 insertions(+), 9 deletions(-) diff --git a/.github/scripts/check-skill-version.sh b/.github/scripts/check-skill-version.sh index bdc2e19..dbbf694 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,17 @@ 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; } + grep -Fq "termlens/$(echo "$dep_minor" | tr . /)" "$document" || + 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..c51ecda 100644 --- a/docs/decisions/ADR-0011-tui-testing-layers.md +++ b/docs/decisions/ADR-0011-tui-testing-layers.md @@ -15,9 +15,9 @@ The TUI is a state machine with a pure `render(state, frame)` and an `on_event` Both layers run headless under `cargo test`. The demo binary doubles as a visual smoke test. Snapshots are committed and reviewed like code. -## Update — 2026-09-08 (termlens 0.10) +## Update — 2026-09-08 (termlens 0.11) -The two layers stand; the dependency moved 0.9 → 0.10.1 and the PTY layer grew a third and fourth file (this ADR's own text above still said 0.6.1, which is what it was when the decision was taken — the manifest had since moved to 0.9 without the record following), because 0.10 made claims checkable that were previously only assumed: +The two layers stand; the dependency moved 0.9 → 0.11 and the PTY layer grew a third and fourth file (this ADR's own text above still said 0.6.1, which is what it was when the decision was taken — the manifest had since moved to 0.9 without the record following), because 0.11 made claims checkable that were previously only assumed: - `tests/emulation.rs` pins `Screen::unsupported()` — the sequences the emulator did *not* implement. Every other assertion in this crate reads a grid the emulator built, so a dropped sequence makes all of them plausible and wrong. Measured here: exactly `["^[[59m"]`, ratatui's underline-colour reset, which changes no cell. - `tests/tui_pty_test.rs` keeps its three text snapshots byte-identical (`styles = false`; 0.10 records styles by default) and adds one styled snapshot plus direct cell assertions for the two things this dashboard says in colour alone: the focused panel's border and each operator's backend tag. `TestBackend::to_string()` cannot see either. 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 | From 5df25b1c4004fa2790e089267a70b1c6c2475d03 Mon Sep 17 00:00:00 2001 From: Vyncint Ng <115854244+vyncint@users.noreply.github.com> Date: Sun, 13 Sep 2026 22:56:52 +0700 Subject: [PATCH 2/2] docs(adr): keep ADR-0011's dated entry true, and record 0.11 as its own MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The 2026-09-08 update entry described the 0.9 → 0.10.1 move, which is what happened on that date; rewriting it to say 0.11 made a dated record claim something that was not true when it was written, and left the bullet below it still explaining that "0.10 records styles by default". termlens 0.11 landed here on 2026-09-11 in 2b12da6. So the 2026-09-08 entry keeps its history, and 0.11 gets its own dated entry: the stability-candidate requirement, the one breaking change (`Screen::unsupported()` returning a view, `unsupported_overflow()` folding into it) and what that bought both PTY suites, plus the widened `make skill-version`. ADR-0012 makes these records the audit trail, so a dated entry is evidence rather than a description of the present. SPEC.md and dependencies.md are current-state documents and are correctly rewritten in place. Also drops a dead line from the new guard: the first grep built `termlens/0/11` from `tr . /` and could never match `termlens/0.11`, so the regex beside it was doing all the work. One pattern now, with the spellings it covers named. Signed-off-by: Vyncint Ng <115854244+vyncint@users.noreply.github.com> --- .github/scripts/check-skill-version.sh | 6 +++-- docs/decisions/ADR-0011-tui-testing-layers.md | 23 +++++++++++++++++-- 2 files changed, 25 insertions(+), 4 deletions(-) diff --git a/.github/scripts/check-skill-version.sh b/.github/scripts/check-skill-version.sh index dbbf694..90c9d3c 100755 --- a/.github/scripts/check-skill-version.sh +++ b/.github/scripts/check-skill-version.sh @@ -54,8 +54,10 @@ fi for document in docs/SPEC.md docs/dependencies.md; do [ -f "$document" ] || { echo "::error::$document does not exist"; exit 1; } - grep -Fq "termlens/$(echo "$dep_minor" | tr . /)" "$document" || - grep -Eq "termlens[^0-9]*${dep_minor//./\\.}" "$document" || { + # 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 } diff --git a/docs/decisions/ADR-0011-tui-testing-layers.md b/docs/decisions/ADR-0011-tui-testing-layers.md index c51ecda..8b93b14 100644 --- a/docs/decisions/ADR-0011-tui-testing-layers.md +++ b/docs/decisions/ADR-0011-tui-testing-layers.md @@ -15,9 +15,9 @@ The TUI is a state machine with a pure `render(state, frame)` and an `on_event` Both layers run headless under `cargo test`. The demo binary doubles as a visual smoke test. Snapshots are committed and reviewed like code. -## Update — 2026-09-08 (termlens 0.11) +## Update — 2026-09-08 (termlens 0.10) -The two layers stand; the dependency moved 0.9 → 0.11 and the PTY layer grew a third and fourth file (this ADR's own text above still said 0.6.1, which is what it was when the decision was taken — the manifest had since moved to 0.9 without the record following), because 0.11 made claims checkable that were previously only assumed: +The two layers stand; the dependency moved 0.9 → 0.10.1 and the PTY layer grew a third and fourth file (this ADR's own text above still said 0.6.1, which is what it was when the decision was taken — the manifest had since moved to 0.9 without the record following), because 0.10 made claims checkable that were previously only assumed: - `tests/emulation.rs` pins `Screen::unsupported()` — the sequences the emulator did *not* implement. Every other assertion in this crate reads a grid the emulator built, so a dropped sequence makes all of them plausible and wrong. Measured here: exactly `["^[[59m"]`, ratatui's underline-colour reset, which changes no cell. - `tests/tui_pty_test.rs` keeps its three text snapshots byte-identical (`styles = false`; 0.10 records styles by default) and adds one styled snapshot plus direct cell assertions for the two things this dashboard says in colour alone: the focused panel's border and each operator's backend tag. `TestBackend::to_string()` cannot see either. @@ -25,3 +25,22 @@ The two layers stand; the dependency moved 0.9 → 0.11 and the PTY layer grew a - `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.