From 36fc712f77a39d800735a5643ad50779b5167879 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Pawe=C5=82=20Pacana?= Date: Wed, 24 Jun 2026 21:17:20 +0200 Subject: [PATCH 01/16] Manage the dev environment with devenv.sh - Pin Ruby 4.0.5 and PostgreSQL 18 tooling (pg_config, pg_regress, pgformatter) through Nix so the toolchain is reproducible rather than relying on system installs. - Move MUTANT_SINCE and PG_REGRESS into devenv and drop the .envrc/direnv setup. - Expose the Rakefile dev tasks as devenv tasks (app:format, :test, :mutate, :pg-regress, :check). - Manage the pre-commit hook via git-hooks.nix (runs the full bin/rake). - Drive the Claude stree-on-save hook from devenv; attribution moves to settings.local.json since devenv now owns settings.json. - Run CI through devenv (Nix + cachix) instead of ruby/setup-ruby and cache devenv's bundle directory. - Ignore devenv-generated files (settings.json, .mcp.json, .githooks, .pre-commit-config.yaml). - Gemfile.lock now records bundler 2.7.2, the version shipped by the Nix Ruby. --- .claude/settings.json | 19 ---- .envrc | 1 - .githooks/pre-commit | 17 ---- .github/workflows/main.yml | 22 ++++- .gitignore | 7 ++ Gemfile.lock | 3 +- devenv.lock | 197 +++++++++++++++++++++++++++++++++++++ devenv.nix | 54 ++++++++++ devenv.yaml | 13 +++ 9 files changed, 289 insertions(+), 44 deletions(-) delete mode 100644 .claude/settings.json delete mode 100644 .envrc delete mode 100755 .githooks/pre-commit create mode 100644 devenv.lock create mode 100644 devenv.nix create mode 100644 devenv.yaml diff --git a/.claude/settings.json b/.claude/settings.json deleted file mode 100644 index cd5372a..0000000 --- a/.claude/settings.json +++ /dev/null @@ -1,19 +0,0 @@ -{ - "attribution": { - "commit": "", - "pr": "" - }, - "hooks": { - "PostToolUse": [ - { - "matcher": "Write|Edit", - "hooks": [ - { - "type": "command", - "command": "jq -r '.tool_response.filePath // .tool_input.file_path' | { read -r f; case \"$f\" in *.rb) bin/stree write \"$f\" ;; esac; } 2>/dev/null || true" - } - ] - } - ] - } -} diff --git a/.envrc b/.envrc deleted file mode 100644 index b50a72f..0000000 --- a/.envrc +++ /dev/null @@ -1 +0,0 @@ -export MUTANT_SINCE=HEAD diff --git a/.githooks/pre-commit b/.githooks/pre-commit deleted file mode 100755 index 3a43294..0000000 --- a/.githooks/pre-commit +++ /dev/null @@ -1,17 +0,0 @@ -#!/usr/bin/env bash -set -euo pipefail - -stash_ref="" -cleanup() { - if [ -n "$stash_ref" ]; then - git stash pop --quiet "$stash_ref" || true - fi -} -trap cleanup EXIT - -if ! git diff --quiet || [ -n "$(git ls-files --others --exclude-standard)" ]; then - git stash push --keep-index --include-untracked --quiet --message "pre-commit: $(date +%s)" - stash_ref="stash@{0}" -fi - -bin/rake diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index ae00924..711e2f3 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -22,10 +22,22 @@ jobs: with: persist-credentials: false fetch-depth: 0 - - name: Set up Ruby - uses: ruby/setup-ruby@v1 + - name: Install Nix + uses: cachix/install-nix-action@v31 with: - ruby-version: '4.0.2' - bundler-cache: true + extra_nix_config: | + accept-flake-config = true + - name: Set up cachix + uses: cachix/cachix-action@v16 + with: + name: devenv + - name: Install devenv + run: nix profile install nixpkgs#devenv + - name: Cache gems + uses: actions/cache@v4 + with: + path: .devenv/state/.bundle + key: ${{ runner.os }}-gems-${{ hashFiles('Gemfile.lock') }} + restore-keys: ${{ runner.os }}-gems- - name: Run the default task - run: bin/rake + run: devenv shell -- bin/rake diff --git a/.gitignore b/.gitignore index eeca105..ca4d092 100644 --- a/.gitignore +++ b/.gitignore @@ -1,5 +1,12 @@ +/.claude/settings.json +/.mcp.json /.bundle/ /.mutant/ /pkg/ /tmp/ /test/pg_regress/results/ +.devenv* +devenv.local.nix +devenv.local.yaml +.pre-commit-config.yaml +/.githooks/ diff --git a/Gemfile.lock b/Gemfile.lock index 9c1ddd0..abbd85f 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -313,7 +313,6 @@ CHECKSUMS benchmark (0.5.0) sha256=465df122341aedcb81a2a24b4d3bd19b6c67c1530713fd533f3ff034e419236c bigdecimal (4.1.2) sha256=53d217666027eab4280346fba98e7d5b66baaae1b9c3c1c0ffe89d48188a3fbd builder (3.3.0) sha256=497918d2f9dca528fdca4b88d84e4ef4387256d984b8154e9d5d3fe5a9c8835f - bundler (4.0.11) sha256=5bcec0fb78302e48d02ee46f10ee6e6942be647ba5b44a6d1ddfda9a240ce785 concurrent-ruby (1.3.6) sha256=6b56837e1e7e5292f9864f34b69c5a2cbc75c0cf5338f1ce9903d10fa762d5ab connection_pool (3.0.2) sha256=33fff5ba71a12d2aa26cb72b1db8bba2a1a01823559fb01d29eb74c286e62e0a crass (1.0.6) sha256=dc516022a56e7b3b156099abc81b6d2b08ea1ed12676ac7a5657617f012bd45d @@ -396,4 +395,4 @@ CHECKSUMS zeitwerk (2.8.2) sha256=7212a61311083c604184b1ea2574b9aa05cd14f855a0841c06985cabe9181d12 BUNDLED WITH - 4.0.11 + 2.7.2 diff --git a/devenv.lock b/devenv.lock new file mode 100644 index 0000000..867955b --- /dev/null +++ b/devenv.lock @@ -0,0 +1,197 @@ +{ + "nodes": { + "devenv": { + "locked": { + "dir": "src/modules", + "lastModified": 1782231411, + "narHash": "sha256-W/Yhek5L3nc0gaGgMHPrzO6DbLGq7Yb9DKw/kvLwij0=", + "owner": "cachix", + "repo": "devenv", + "rev": "a4fd3348220d96f7602385adff0ace482e720586", + "type": "github" + }, + "original": { + "dir": "src/modules", + "owner": "cachix", + "repo": "devenv", + "type": "github" + } + }, + "flake-compat": { + "flake": false, + "locked": { + "lastModified": 1767039857, + "narHash": "sha256-vNpUSpF5Nuw8xvDLj2KCwwksIbjua2LZCqhV1LNRDns=", + "owner": "NixOS", + "repo": "flake-compat", + "rev": "5edf11c44bc78a0d334f6334cdaf7d60d732daab", + "type": "github" + }, + "original": { + "owner": "NixOS", + "repo": "flake-compat", + "type": "github" + } + }, + "flake-compat_2": { + "flake": false, + "locked": { + "lastModified": 1767039857, + "narHash": "sha256-vNpUSpF5Nuw8xvDLj2KCwwksIbjua2LZCqhV1LNRDns=", + "owner": "edolstra", + "repo": "flake-compat", + "rev": "5edf11c44bc78a0d334f6334cdaf7d60d732daab", + "type": "github" + }, + "original": { + "owner": "edolstra", + "repo": "flake-compat", + "type": "github" + } + }, + "flake-utils": { + "inputs": { + "systems": "systems" + }, + "locked": { + "lastModified": 1731533236, + "narHash": "sha256-l0KFg5HjrsfsO/JpG+r7fRrqm12kzFHyUHqHCVpMMbI=", + "owner": "numtide", + "repo": "flake-utils", + "rev": "11707dc2f618dd54ca8739b309ec4fc024de578b", + "type": "github" + }, + "original": { + "owner": "numtide", + "repo": "flake-utils", + "type": "github" + } + }, + "git-hooks": { + "inputs": { + "flake-compat": "flake-compat", + "gitignore": "gitignore", + "nixpkgs": [ + "nixpkgs" + ] + }, + "locked": { + "lastModified": 1781733627, + "narHash": "sha256-U3yTuGBnmXvXoQI3qkpfEDsn9RovQPAjN7ndRco+3u0=", + "owner": "cachix", + "repo": "git-hooks.nix", + "rev": "3bbec39bc90eadfa031e6f3b77272f3f60803e39", + "type": "github" + }, + "original": { + "owner": "cachix", + "repo": "git-hooks.nix", + "type": "github" + } + }, + "gitignore": { + "inputs": { + "nixpkgs": [ + "git-hooks", + "nixpkgs" + ] + }, + "locked": { + "lastModified": 1709087332, + "narHash": "sha256-HG2cCnktfHsKV0s4XW83gU3F57gaTljL9KNSuG6bnQs=", + "owner": "hercules-ci", + "repo": "gitignore.nix", + "rev": "637db329424fd7e46cf4185293b9cc8c88c95394", + "type": "github" + }, + "original": { + "owner": "hercules-ci", + "repo": "gitignore.nix", + "type": "github" + } + }, + "nixpkgs": { + "inputs": { + "nixpkgs-src": "nixpkgs-src" + }, + "locked": { + "lastModified": 1782132010, + "narHash": "sha256-ZnAVHdVrotp80iIMm5CSR1fdxPlw7Uwmwxb+O/wsgZ8=", + "owner": "cachix", + "repo": "devenv-nixpkgs", + "rev": "12866ae2dddbc0ab8b329915f8072bb9c75bde89", + "type": "github" + }, + "original": { + "owner": "cachix", + "ref": "rolling", + "repo": "devenv-nixpkgs", + "type": "github" + } + }, + "nixpkgs-ruby": { + "inputs": { + "flake-compat": "flake-compat_2", + "flake-utils": "flake-utils", + "nixpkgs": [ + "nixpkgs" + ] + }, + "locked": { + "lastModified": 1782291298, + "narHash": "sha256-s5qd1ZWKoc8m6dnTvcHTMWerE8xXOSG82Vgp7eIPesk=", + "owner": "bobvanderlinden", + "repo": "nixpkgs-ruby", + "rev": "5a59e45e96465645c00422fedf5e62e337f6cfea", + "type": "github" + }, + "original": { + "owner": "bobvanderlinden", + "repo": "nixpkgs-ruby", + "type": "github" + } + }, + "nixpkgs-src": { + "flake": false, + "locked": { + "lastModified": 1781607440, + "narHash": "sha256-rxO+uc/KFbSJp+pgyXRuAX6QlG9hJdnt0BXpEQRXY+U=", + "owner": "NixOS", + "repo": "nixpkgs", + "rev": "3e41b24abd260e8f71dbe2f5737d24122f972158", + "type": "github" + }, + "original": { + "owner": "NixOS", + "ref": "nixpkgs-unstable", + "repo": "nixpkgs", + "type": "github" + } + }, + "root": { + "inputs": { + "devenv": "devenv", + "git-hooks": "git-hooks", + "nixpkgs": "nixpkgs", + "nixpkgs-ruby": "nixpkgs-ruby" + } + }, + "systems": { + "locked": { + "lastModified": 1681028828, + "narHash": "sha256-Vy1rq5AaRuLzOxct8nz4T6wlgyUR7zLU309k9mBC768=", + "owner": "nix-systems", + "repo": "default", + "rev": "da67096a3b9bf56a91d16901293e51ba5b49a27e", + "type": "github" + }, + "original": { + "owner": "nix-systems", + "repo": "default", + "type": "github" + } + } + }, + "root": "root", + "version": 7 +} \ No newline at end of file diff --git a/devenv.nix b/devenv.nix new file mode 100644 index 0000000..50f0c47 --- /dev/null +++ b/devenv.nix @@ -0,0 +1,54 @@ +{ pkgs, lib, ... }: + +{ + languages.ruby.enable = true; + languages.ruby.version = "4.0.5"; + + packages = [ + pkgs.postgresql_18 + pkgs.postgresql_18.pg_config + pkgs.pgformatter + pkgs.jq + pkgs.libyaml + ]; + + env.PG_REGRESS = "${pkgs.postgresql_18.dev}/lib/pgxs/src/test/regress/pg_regress"; + + enterShell = '' + export MUTANT_SINCE="''${MUTANT_SINCE:-HEAD}" + bundle install --quiet + ''; + + enterTest = "bin/rake"; + + tasks = { + "app:format".exec = "bin/rake format"; + "app:test".exec = "bin/rake test"; + "app:mutate".exec = "bin/mutant run"; + "app:pg-regress".exec = "bin/rake pg_regress"; + "app:check".exec = "bin/rake"; # default: test + mutate_since + }; + + git-hooks.hooks.rake = { + enable = true; + name = "rake (test + mutate_since)"; + entry = "${pkgs.writeShellScript "rake-precommit" '' + export MUTANT_SINCE="''${MUTANT_SINCE:-HEAD}" + exec bin/rake + ''}"; + pass_filenames = false; + language = "system"; + stages = [ "pre-commit" ]; + }; + + claude.code.enable = true; + claude.code.hooks.git-hooks-run.enable = false; + claude.code.hooks.format-ruby = { + name = "Format Ruby with syntax_tree"; + hookType = "PostToolUse"; + matcher = "Write|Edit"; + command = '' + jq -r '.tool_response.filePath // .tool_input.file_path' | { read -r f; case "$f" in *.rb) bin/stree write "$f" ;; esac; } 2>/dev/null || true + ''; + }; +} diff --git a/devenv.yaml b/devenv.yaml new file mode 100644 index 0000000..5a3d24b --- /dev/null +++ b/devenv.yaml @@ -0,0 +1,13 @@ +inputs: + nixpkgs: + url: github:cachix/devenv-nixpkgs/rolling + git-hooks: + url: github:cachix/git-hooks.nix + inputs: + nixpkgs: + follows: nixpkgs + nixpkgs-ruby: + url: github:bobvanderlinden/nixpkgs-ruby + inputs: + nixpkgs: + follows: nixpkgs From 6ccb1aa8696efd30e6db9081a38acb13045abc02 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Pawe=C5=82=20Pacana?= Date: Wed, 24 Jun 2026 21:37:54 +0200 Subject: [PATCH 02/16] Drop devenv enterTest - `devenv test` only checks that the shell and services come up healthy; with no service daemon to wait on, it has nothing to verify. - The full bin/rake run already lives in the app:check task, the pre-commit hook, and CI (devenv shell -- bin/rake), so enterTest was redundant. --- devenv.nix | 2 -- 1 file changed, 2 deletions(-) diff --git a/devenv.nix b/devenv.nix index 50f0c47..2f8714a 100644 --- a/devenv.nix +++ b/devenv.nix @@ -19,8 +19,6 @@ bundle install --quiet ''; - enterTest = "bin/rake"; - tasks = { "app:format".exec = "bin/rake format"; "app:test".exec = "bin/rake test"; From fd9d88baad21c9d773cf529f4fa53ecc3f75dc84 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Pawe=C5=82=20Pacana?= Date: Wed, 24 Jun 2026 21:51:25 +0200 Subject: [PATCH 03/16] Move pg_regress into a self-contained devenv script - Consolidate the pg_regress run in one place: devenv.nix's scripts.pg-regress, dropping the Rakefile task, the pg-ephemeral gem dependency, and the PG_REGRESS/PG_BINDIR env-var plumbing. - The script spins up an ephemeral PostgreSQL from the Nix server binaries on a private unix socket (no TCP listener), seeds db/schema/0.1.0.sql, runs the schedule_existing suite, and tears it down via a trap. - Inline the pg_regress and bindir paths from Nix instead of probing PATH/Homebrew and passing them through env vars. --- Rakefile | 57 ------------------------------------------------------ devenv.nix | 46 ++++++++++++++++++++++++++++++++++++++++--- 2 files changed, 43 insertions(+), 60 deletions(-) diff --git a/Rakefile b/Rakefile index 153bf89..9c1cb50 100644 --- a/Rakefile +++ b/Rakefile @@ -1,25 +1,11 @@ # frozen_string_literal: true require "bundler/gem_tasks" -require "fileutils" require "minitest/test_task" -require "uri" require_relative "lib/en57/tasks" Minitest::TestTask.create -def pg_regress_bin - candidates = [ - ENV["PG_REGRESS"], - `command -v pg_regress`.strip, - "/opt/homebrew/lib/postgresql@18/pgxs/src/test/regress/pg_regress", - ] - - candidates.find do |candidate| - candidate && !candidate.empty? && File.executable?(candidate) - end -end - task :format do system("stree write **/*.rb") system("pg_format -i db/**/*.sql") @@ -33,47 +19,4 @@ task :mutate_since do system("bin/mutant run --since #{ENV.fetch("MUTANT_SINCE")}") end -desc "Run pg_regress tests against a pg-ephemeral instance" -task :pg_regress do - require "pg_ephemeral" - - pg_regress = pg_regress_bin - unless pg_regress - abort("pg_regress not found. Set PG_REGRESS=/path/to/pg_regress") - end - - server = PgEphemeral.start - uri = URI(server.url) - - host = uri.host - port = uri.port - user = uri.user - dbname = uri.path.delete_prefix("/") - bindir = ENV["PG_BINDIR"] || `pg_config --bindir`.strip - schedule = ENV["PG_REGRESS_SCHEDULE"] || "test/pg_regress/schedule_existing" - - FileUtils.rm_rf("test/pg_regress/results") - FileUtils.mkdir_p("test/pg_regress/results") - - success = - system( - { "PGPASSWORD" => uri.password }, - pg_regress, - "--use-existing", - "--host=#{host}", - "--port=#{port}", - "--user=#{user}", - "--dbname=#{dbname}", - "--inputdir=test/pg_regress", - "--outputdir=test/pg_regress/results", - "--expecteddir=test/pg_regress", - "--bindir=#{bindir}", - "--schedule=#{schedule}", - ) - - abort("pg_regress failed") unless success -ensure - server&.shutdown -end - task default: %i[test mutate_since] diff --git a/devenv.nix b/devenv.nix index 2f8714a..fd4be07 100644 --- a/devenv.nix +++ b/devenv.nix @@ -12,8 +12,6 @@ pkgs.libyaml ]; - env.PG_REGRESS = "${pkgs.postgresql_18.dev}/lib/pgxs/src/test/regress/pg_regress"; - enterShell = '' export MUTANT_SINCE="''${MUTANT_SINCE:-HEAD}" bundle install --quiet @@ -23,10 +21,52 @@ "app:format".exec = "bin/rake format"; "app:test".exec = "bin/rake test"; "app:mutate".exec = "bin/mutant run"; - "app:pg-regress".exec = "bin/rake pg_regress"; "app:check".exec = "bin/rake"; # default: test + mutate_since }; + # Self-contained pg_regress run: spin up an ephemeral PostgreSQL with the + # Nix server binaries, load the schema, run the regression schedule, then + # tear it down. No Rakefile task, no gem, no env-var paths. + scripts.pg-regress.exec = '' + set -euo pipefail + cd "$DEVENV_ROOT" + + pg_regress=${pkgs.postgresql_18.dev}/lib/pgxs/src/test/regress/pg_regress + bindir=${pkgs.postgresql_18}/bin + workdir=$(mktemp -d) + datadir=$workdir/data + socketdir=$workdir/socket + dbname=en57_regress + mkdir -p "$socketdir" + + cleanup() { + "$bindir/pg_ctl" -D "$datadir" -m immediate stop >/dev/null 2>&1 || true + rm -rf "$workdir" + } + trap cleanup EXIT + + "$bindir/initdb" -D "$datadir" -U postgres --auth=trust >/dev/null + "$bindir/pg_ctl" -D "$datadir" -w \ + -o "-k $socketdir -c listen_addresses='''" start >/dev/null + "$bindir/createdb" -h "$socketdir" -U postgres "$dbname" + "$bindir/psql" -h "$socketdir" -U postgres -d "$dbname" \ + -v ON_ERROR_STOP=1 -q -f db/schema/0.1.0.sql + + rm -rf test/pg_regress/results + mkdir -p test/pg_regress/results + + "$pg_regress" \ + --use-existing \ + --host="$socketdir" \ + --user=postgres \ + --dbname="$dbname" \ + --inputdir=test/pg_regress \ + --outputdir=test/pg_regress/results \ + --expecteddir=test/pg_regress \ + --bindir="$bindir" \ + --schedule=test/pg_regress/schedule_existing + ''; + git-hooks.hooks.rake = { enable = true; name = "rake (test + mutate_since)"; From c47d6985858a80e6737f3c8f5547e0ed87416546 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Pawe=C5=82=20Pacana?= Date: Wed, 24 Jun 2026 22:03:05 +0200 Subject: [PATCH 04/16] Use sqlfluff to format SQL - Replace pgformatter (pg_format) with sqlfluff: swap the devenv package, point the rake format task at `sqlfluff format db`, and add .sqlfluff pinning the postgres dialect. --- .sqlfluff | 2 ++ Rakefile | 2 +- devenv.nix | 2 +- 3 files changed, 4 insertions(+), 2 deletions(-) create mode 100644 .sqlfluff diff --git a/.sqlfluff b/.sqlfluff new file mode 100644 index 0000000..7fb4ec3 --- /dev/null +++ b/.sqlfluff @@ -0,0 +1,2 @@ +[sqlfluff] +dialect = postgres diff --git a/Rakefile b/Rakefile index 9c1cb50..9e39bb4 100644 --- a/Rakefile +++ b/Rakefile @@ -8,7 +8,7 @@ Minitest::TestTask.create task :format do system("stree write **/*.rb") - system("pg_format -i db/**/*.sql") + system("sqlfluff format db") end task :mutate do diff --git a/devenv.nix b/devenv.nix index fd4be07..ed8ea7e 100644 --- a/devenv.nix +++ b/devenv.nix @@ -7,7 +7,7 @@ packages = [ pkgs.postgresql_18 pkgs.postgresql_18.pg_config - pkgs.pgformatter + pkgs.sqlfluff pkgs.jq pkgs.libyaml ]; From d490cb1d5d7e3254c5864179f77f38d9204e7d55 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Pawe=C5=82=20Pacana?= Date: Wed, 24 Jun 2026 22:03:10 +0200 Subject: [PATCH 05/16] Reformat SQL with sqlfluff - Apply the new formatter to the schema and seed files. Layout and whitespace only; no semantic change (pg-regress still passes). --- db/schema/0.1.0.sql | 40 +++-- ...ent_append_non_conflicting_tags_seeded.sql | 3 +- db/seeds/res.sql | 154 ++++++++++-------- 3 files changed, 112 insertions(+), 85 deletions(-) diff --git a/db/schema/0.1.0.sql b/db/schema/0.1.0.sql index 4e2ad57..1f947a0 100644 --- a/db/schema/0.1.0.sql +++ b/db/schema/0.1.0.sql @@ -23,7 +23,7 @@ CREATE TYPE en57.event AS ( type text, data jsonb, meta jsonb, - tags text[] + tags text [] ); CREATE TYPE en57.append_result AS ( @@ -31,11 +31,13 @@ CREATE TYPE en57.append_result AS ( conflicting_events jsonb ); -CREATE FUNCTION en57.append_events (new_events en57.event[], append_condition jsonb DEFAULT '{}'::jsonb) - RETURNS en57.append_result - LANGUAGE plpgsql - SET enable_seqscan = OFF - AS $$ +CREATE FUNCTION en57.append_events( + new_events en57.event [], append_condition jsonb DEFAULT '{}'::jsonb +) +RETURNS en57.append_result +LANGUAGE plpgsql +SET enable_seqscan = OFF +AS $$ DECLARE criteria jsonb[] := ARRAY ( SELECT @@ -150,16 +152,21 @@ INSERT INTO en57.events (id, type, data, meta) END; $$; -CREATE FUNCTION en57.read_events (criteria jsonb[], batch_size int DEFAULT NULL, after_position bigint DEFAULT NULL) - RETURNS TABLE ( - "position" bigint, - id uuid, - type text, - data jsonb, - meta jsonb, - tags text[]) - LANGUAGE SQL - AS $$ +CREATE FUNCTION en57.read_events( + criteria jsonb [], + batch_size int DEFAULT NULL, + after_position bigint DEFAULT NULL +) +RETURNS TABLE ( + "position" bigint, + id uuid, + type text, + data jsonb, + meta jsonb, + tags text [] +) +LANGUAGE sql +AS $$ WITH parsed_criteria AS ( SELECT c, @@ -226,4 +233,3 @@ ORDER BY e.position LIMIT batch_size; $$; - diff --git a/db/seeds/concurrent_append_non_conflicting_tags_seeded.sql b/db/seeds/concurrent_append_non_conflicting_tags_seeded.sql index da205ff..bc73bbc 100644 --- a/db/seeds/concurrent_append_non_conflicting_tags_seeded.sql +++ b/db/seeds/concurrent_append_non_conflicting_tags_seeded.sql @@ -4,7 +4,7 @@ -- of the same shape the benchmark queries. INSERT INTO en57.events (id, type) SELECT - gen_random_uuid (), + gen_random_uuid(), 'event_benchmarked' FROM generate_series(1, 1000000); @@ -15,4 +15,3 @@ SELECT 'writer:' || substr(md5(random()::text), 1, 8) FROM en57.events; - diff --git a/db/seeds/res.sql b/db/seeds/res.sql index 6902cb6..5ac9466 100644 --- a/db/seeds/res.sql +++ b/db/seeds/res.sql @@ -10,184 +10,206 @@ SET client_encoding = 'UTF8'; SET standard_conforming_strings = ON; -SELECT - pg_catalog.set_config('search_path', '', FALSE); +SELECT pg_catalog.set_config('search_path', '', FALSE); SET check_function_bodies = FALSE; -SET xmloption = content; +SET xmloption = CONTENT; -SET client_min_messages = warning; +SET client_min_messages = WARNING; SET row_security = OFF; SET default_tablespace = ''; -SET default_table_access_method = heap; +SET default_table_access_method = HEAP; -- -- Name: ar_internal_metadata; Type: TABLE; Schema: public; Owner: - -- -CREATE TABLE public.ar_internal_metadata ( - key character varying NOT NULL, - value character varying, - created_at timestamp(6) without time zone NOT NULL, - updated_at timestamp(6) without time zone NOT NULL +CREATE TABLE PUBLIC.AR_INTERNAL_METADATA ( + KEY character varying NOT NULL, + VALUE character varying, + CREATED_AT timestamp(6) without time zone NOT NULL, + UPDATED_AT timestamp(6) without time zone NOT NULL ); -- -- Name: event_store_events; Type: TABLE; Schema: public; Owner: - -- -CREATE TABLE public.event_store_events ( - id bigint NOT NULL, - event_id uuid NOT NULL, - event_type character varying NOT NULL, - metadata jsonb, - data jsonb NOT NULL, - created_at timestamp(6) without time zone NOT NULL, - valid_at timestamp(6) without time zone +CREATE TABLE PUBLIC.EVENT_STORE_EVENTS ( + ID bigint NOT NULL, + EVENT_ID uuid NOT NULL, + EVENT_TYPE character varying NOT NULL, + METADATA jsonb, + DATA jsonb NOT NULL, + CREATED_AT timestamp(6) without time zone NOT NULL, + VALID_AT timestamp(6) without time zone ); -- -- Name: event_store_events_id_seq; Type: SEQUENCE; Schema: public; Owner: - -- -CREATE SEQUENCE public.event_store_events_id_seq - START WITH 1 - INCREMENT BY 1 - NO MINVALUE - NO MAXVALUE - CACHE 1; +CREATE SEQUENCE PUBLIC.EVENT_STORE_EVENTS_ID_SEQ +START WITH 1 +INCREMENT BY 1 +NO MINVALUE +NO MAXVALUE +CACHE 1; -- -- Name: event_store_events_id_seq; Type: SEQUENCE OWNED BY; Schema: public; Owner: - -- -ALTER SEQUENCE public.event_store_events_id_seq OWNED BY public.event_store_events.id; +ALTER SEQUENCE PUBLIC.EVENT_STORE_EVENTS_ID_SEQ OWNED BY PUBLIC.EVENT_STORE_EVENTS.ID; -- -- Name: event_store_events_in_streams; Type: TABLE; Schema: public; Owner: - -- -CREATE TABLE public.event_store_events_in_streams ( - id bigint NOT NULL, - stream character varying NOT NULL, +CREATE TABLE PUBLIC.EVENT_STORE_EVENTS_IN_STREAMS ( + ID bigint NOT NULL, + STREAM character varying NOT NULL, "position" integer, - event_id uuid NOT NULL, - created_at timestamp(6) without time zone NOT NULL + EVENT_ID uuid NOT NULL, + CREATED_AT timestamp(6) without time zone NOT NULL ); -- -- Name: event_store_events_in_streams_id_seq; Type: SEQUENCE; Schema: public; Owner: - -- -CREATE SEQUENCE public.event_store_events_in_streams_id_seq - START WITH 1 - INCREMENT BY 1 - NO MINVALUE - NO MAXVALUE - CACHE 1; +CREATE SEQUENCE PUBLIC.EVENT_STORE_EVENTS_IN_STREAMS_ID_SEQ +START WITH 1 +INCREMENT BY 1 +NO MINVALUE +NO MAXVALUE +CACHE 1; -- -- Name: event_store_events_in_streams_id_seq; Type: SEQUENCE OWNED BY; Schema: public; Owner: - -- -ALTER SEQUENCE public.event_store_events_in_streams_id_seq OWNED BY public.event_store_events_in_streams.id; +ALTER SEQUENCE PUBLIC.EVENT_STORE_EVENTS_IN_STREAMS_ID_SEQ OWNED BY PUBLIC.EVENT_STORE_EVENTS_IN_STREAMS.ID; -- -- Name: schema_migrations; Type: TABLE; Schema: public; Owner: - -- -CREATE TABLE public.schema_migrations ( - version character varying NOT NULL +CREATE TABLE PUBLIC.SCHEMA_MIGRATIONS ( + VERSION character varying NOT NULL ); -- -- Name: event_store_events id; Type: DEFAULT; Schema: public; Owner: - -- -ALTER TABLE ONLY public.event_store_events - ALTER COLUMN id SET DEFAULT nextval('public.event_store_events_id_seq'::regclass); +ALTER TABLE ONLY PUBLIC.EVENT_STORE_EVENTS +ALTER COLUMN ID SET DEFAULT nextval( + 'public.event_store_events_id_seq'::regclass +); -- -- Name: event_store_events_in_streams id; Type: DEFAULT; Schema: public; Owner: - -- -ALTER TABLE ONLY public.event_store_events_in_streams - ALTER COLUMN id SET DEFAULT nextval('public.event_store_events_in_streams_id_seq'::regclass); +ALTER TABLE ONLY PUBLIC.EVENT_STORE_EVENTS_IN_STREAMS +ALTER COLUMN ID SET DEFAULT nextval( + 'public.event_store_events_in_streams_id_seq'::regclass +); -- -- Name: ar_internal_metadata ar_internal_metadata_pkey; Type: CONSTRAINT; Schema: public; Owner: - -- -ALTER TABLE ONLY public.ar_internal_metadata - ADD CONSTRAINT ar_internal_metadata_pkey PRIMARY KEY (key); +ALTER TABLE ONLY PUBLIC.AR_INTERNAL_METADATA +ADD CONSTRAINT AR_INTERNAL_METADATA_PKEY PRIMARY KEY (KEY); -- -- Name: event_store_events_in_streams event_store_events_in_streams_pkey; Type: CONSTRAINT; Schema: public; Owner: - -- -ALTER TABLE ONLY public.event_store_events_in_streams - ADD CONSTRAINT event_store_events_in_streams_pkey PRIMARY KEY (id); +ALTER TABLE ONLY PUBLIC.EVENT_STORE_EVENTS_IN_STREAMS +ADD CONSTRAINT EVENT_STORE_EVENTS_IN_STREAMS_PKEY PRIMARY KEY (ID); -- -- Name: event_store_events event_store_events_pkey; Type: CONSTRAINT; Schema: public; Owner: - -- -ALTER TABLE ONLY public.event_store_events - ADD CONSTRAINT event_store_events_pkey PRIMARY KEY (id); +ALTER TABLE ONLY PUBLIC.EVENT_STORE_EVENTS +ADD CONSTRAINT EVENT_STORE_EVENTS_PKEY PRIMARY KEY (ID); -- -- Name: schema_migrations schema_migrations_pkey; Type: CONSTRAINT; Schema: public; Owner: - -- -ALTER TABLE ONLY public.schema_migrations - ADD CONSTRAINT schema_migrations_pkey PRIMARY KEY (version); +ALTER TABLE ONLY PUBLIC.SCHEMA_MIGRATIONS +ADD CONSTRAINT SCHEMA_MIGRATIONS_PKEY PRIMARY KEY (VERSION); -- -- Name: index_event_store_events_in_streams_on_created_at; Type: INDEX; Schema: public; Owner: - -- -CREATE INDEX index_event_store_events_in_streams_on_created_at ON public.event_store_events_in_streams USING btree (created_at); +CREATE INDEX INDEX_EVENT_STORE_EVENTS_IN_STREAMS_ON_CREATED_AT ON PUBLIC.EVENT_STORE_EVENTS_IN_STREAMS USING BTREE ( + CREATED_AT +); -- -- Name: index_event_store_events_in_streams_on_event_id; Type: INDEX; Schema: public; Owner: - -- -CREATE INDEX index_event_store_events_in_streams_on_event_id ON public.event_store_events_in_streams USING btree (event_id); +CREATE INDEX INDEX_EVENT_STORE_EVENTS_IN_STREAMS_ON_EVENT_ID ON PUBLIC.EVENT_STORE_EVENTS_IN_STREAMS USING BTREE ( + EVENT_ID +); -- -- Name: index_event_store_events_in_streams_on_stream_and_event_id; Type: INDEX; Schema: public; Owner: - -- -CREATE UNIQUE INDEX index_event_store_events_in_streams_on_stream_and_event_id ON public.event_store_events_in_streams USING btree (stream, event_id); +CREATE UNIQUE INDEX INDEX_EVENT_STORE_EVENTS_IN_STREAMS_ON_STREAM_AND_EVENT_ID ON PUBLIC.EVENT_STORE_EVENTS_IN_STREAMS USING BTREE ( + STREAM, EVENT_ID +); -- -- Name: index_event_store_events_in_streams_on_stream_and_position; Type: INDEX; Schema: public; Owner: - -- -CREATE UNIQUE INDEX index_event_store_events_in_streams_on_stream_and_position ON public.event_store_events_in_streams USING btree (stream, "position"); +CREATE UNIQUE INDEX INDEX_EVENT_STORE_EVENTS_IN_STREAMS_ON_STREAM_AND_POSITION ON PUBLIC.EVENT_STORE_EVENTS_IN_STREAMS USING BTREE ( + STREAM, "position" +); -- -- Name: index_event_store_events_on_as_of; Type: INDEX; Schema: public; Owner: - -- -CREATE INDEX index_event_store_events_on_as_of ON public.event_store_events USING btree (COALESCE(valid_at, created_at)); +CREATE INDEX INDEX_EVENT_STORE_EVENTS_ON_AS_OF ON PUBLIC.EVENT_STORE_EVENTS USING BTREE ( + coalesce(VALID_AT, CREATED_AT) +); -- -- Name: index_event_store_events_on_created_at; Type: INDEX; Schema: public; Owner: - -- -CREATE INDEX index_event_store_events_on_created_at ON public.event_store_events USING btree (created_at); +CREATE INDEX INDEX_EVENT_STORE_EVENTS_ON_CREATED_AT ON PUBLIC.EVENT_STORE_EVENTS USING BTREE ( + CREATED_AT +); -- -- Name: index_event_store_events_on_event_id; Type: INDEX; Schema: public; Owner: - -- -CREATE UNIQUE INDEX index_event_store_events_on_event_id ON public.event_store_events USING btree (event_id); +CREATE UNIQUE INDEX INDEX_EVENT_STORE_EVENTS_ON_EVENT_ID ON PUBLIC.EVENT_STORE_EVENTS USING BTREE ( + EVENT_ID +); -- -- Name: index_event_store_events_on_event_type; Type: INDEX; Schema: public; Owner: - -- -CREATE INDEX index_event_store_events_on_event_type ON public.event_store_events USING btree (event_type); +CREATE INDEX INDEX_EVENT_STORE_EVENTS_ON_EVENT_TYPE ON PUBLIC.EVENT_STORE_EVENTS USING BTREE ( + EVENT_TYPE +); -- -- Name: index_event_store_events_on_valid_at; Type: INDEX; Schema: public; Owner: - -- -CREATE INDEX index_event_store_events_on_valid_at ON public.event_store_events USING btree (valid_at); +CREATE INDEX INDEX_EVENT_STORE_EVENTS_ON_VALID_AT ON PUBLIC.EVENT_STORE_EVENTS USING BTREE ( + VALID_AT +); -- -- Name: event_store_events_in_streams fk_rails_c8d52b5857; Type: FK CONSTRAINT; Schema: public; Owner: - -- -ALTER TABLE ONLY public.event_store_events_in_streams - ADD CONSTRAINT fk_rails_c8d52b5857 FOREIGN KEY (event_id) REFERENCES public.event_store_events (event_id); +ALTER TABLE ONLY PUBLIC.EVENT_STORE_EVENTS_IN_STREAMS +ADD CONSTRAINT FK_RAILS_C8D52B5857 FOREIGN KEY ( + EVENT_ID +) REFERENCES PUBLIC.EVENT_STORE_EVENTS (EVENT_ID); -- -- PostgreSQL database dump complete -- -SET search_path TO "$user", public; - -INSERT INTO "schema_migrations" (version) - VALUES ('20260521134825'); +SET search_path TO "$user", PUBLIC; +INSERT INTO "schema_migrations" (VERSION) +VALUES ('20260521134825'); From 17fe4f88a86e416e80d85a4dad2dacf21feeccde Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Pawe=C5=82=20Pacana?= Date: Wed, 24 Jun 2026 22:21:29 +0200 Subject: [PATCH 06/16] Use treefmt to drive all formatters - Replace the per-tool format invocations with treefmt (devenv's treefmt-nix integration): Ruby via syntax_tree, SQL via sqlfluff, configured in devenv.nix. - Exclude sqlfluff's LT05 (line too long) so `sqlfluff format` exits 0 as a formatter; the long lines it can't wrap no longer fail it. - Point app:format and the Claude on-save hook at treefmt (SQL now formats on save too), drop the Rakefile format task, and update AGENTS.md. --- .sqlfluff | 4 ++++ AGENTS.md | 2 +- Rakefile | 5 ----- devenv.lock | 23 ++++++++++++++++++++++- devenv.nix | 26 ++++++++++++++++++++++---- devenv.yaml | 9 +++++++-- 6 files changed, 56 insertions(+), 13 deletions(-) diff --git a/.sqlfluff b/.sqlfluff index 7fb4ec3..391f553 100644 --- a/.sqlfluff +++ b/.sqlfluff @@ -1,2 +1,6 @@ [sqlfluff] dialect = postgres +# LT05 (line too long) is unfixable for long function signatures and seed +# rows, which makes `sqlfluff format` exit non-zero. Exclude it so it acts +# purely as a formatter under treefmt. +exclude_rules = LT05 diff --git a/AGENTS.md b/AGENTS.md index 6495684..4cc278e 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -1,5 +1,5 @@ # Agent rules - Prefer inlining variables when they are not reused. -- After editing files, run `bin/rake format`. +- After editing files, run `treefmt`. - When adding and building a feature, follow the TDD process. diff --git a/Rakefile b/Rakefile index 9e39bb4..eead8d7 100644 --- a/Rakefile +++ b/Rakefile @@ -6,11 +6,6 @@ require_relative "lib/en57/tasks" Minitest::TestTask.create -task :format do - system("stree write **/*.rb") - system("sqlfluff format db") -end - task :mutate do system("bin/mutant run") end diff --git a/devenv.lock b/devenv.lock index 867955b..66f8095 100644 --- a/devenv.lock +++ b/devenv.lock @@ -173,7 +173,8 @@ "devenv": "devenv", "git-hooks": "git-hooks", "nixpkgs": "nixpkgs", - "nixpkgs-ruby": "nixpkgs-ruby" + "nixpkgs-ruby": "nixpkgs-ruby", + "treefmt-nix": "treefmt-nix" } }, "systems": { @@ -190,6 +191,26 @@ "repo": "default", "type": "github" } + }, + "treefmt-nix": { + "inputs": { + "nixpkgs": [ + "nixpkgs" + ] + }, + "locked": { + "lastModified": 1780220602, + "narHash": "sha256-eynAfOmbmxJnkp7YewvCEbShNnnYJ9gLLqkzsYtBPeM=", + "owner": "numtide", + "repo": "treefmt-nix", + "rev": "db947814a175b7ca6ded66e21383d938df01c227", + "type": "github" + }, + "original": { + "owner": "numtide", + "repo": "treefmt-nix", + "type": "github" + } } }, "root": "root", diff --git a/devenv.nix b/devenv.nix index ed8ea7e..ed18882 100644 --- a/devenv.nix +++ b/devenv.nix @@ -18,12 +18,30 @@ ''; tasks = { - "app:format".exec = "bin/rake format"; + "app:format".exec = "treefmt"; "app:test".exec = "bin/rake test"; "app:mutate".exec = "bin/mutant run"; "app:check".exec = "bin/rake"; # default: test + mutate_since }; + # treefmt drives all formatters (Ruby via syntax_tree, SQL via sqlfluff). + treefmt.enable = true; + treefmt.config = { + projectRootFile = "devenv.nix"; + settings.formatter = { + ruby = { + command = "stree"; + options = [ "write" ]; + includes = [ "*.rb" ]; + }; + sql = { + command = "sqlfluff"; + options = [ "format" ]; + includes = [ "db/**/*.sql" ]; + }; + }; + }; + # Self-contained pg_regress run: spin up an ephemeral PostgreSQL with the # Nix server binaries, load the schema, run the regression schedule, then # tear it down. No Rakefile task, no gem, no env-var paths. @@ -81,12 +99,12 @@ claude.code.enable = true; claude.code.hooks.git-hooks-run.enable = false; - claude.code.hooks.format-ruby = { - name = "Format Ruby with syntax_tree"; + claude.code.hooks.format = { + name = "Format edited files with treefmt"; hookType = "PostToolUse"; matcher = "Write|Edit"; command = '' - jq -r '.tool_response.filePath // .tool_input.file_path' | { read -r f; case "$f" in *.rb) bin/stree write "$f" ;; esac; } 2>/dev/null || true + jq -r '.tool_response.filePath // .tool_input.file_path' | { read -r f; treefmt "$f"; } 2>/dev/null || true ''; }; } diff --git a/devenv.yaml b/devenv.yaml index 5a3d24b..fa85261 100644 --- a/devenv.yaml +++ b/devenv.yaml @@ -1,13 +1,18 @@ inputs: - nixpkgs: - url: github:cachix/devenv-nixpkgs/rolling git-hooks: url: github:cachix/git-hooks.nix inputs: nixpkgs: follows: nixpkgs + nixpkgs: + url: github:cachix/devenv-nixpkgs/rolling nixpkgs-ruby: url: github:bobvanderlinden/nixpkgs-ruby inputs: nixpkgs: follows: nixpkgs + treefmt-nix: + url: github:numtide/treefmt-nix + inputs: + nixpkgs: + follows: nixpkgs From 4e29dbf20d7454768b8eecabf4a3c10ad9ade97a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Pawe=C5=82=20Pacana?= Date: Wed, 24 Jun 2026 22:49:30 +0200 Subject: [PATCH 07/16] Move sqlfluff config into devenv - Drop .sqlfluff; pass the dialect and excluded rules to sqlfluff through the treefmt formatter options in devenv.nix. - Exclude LT05 (unfixable long lines) so `sqlfluff format` exits 0. --- .sqlfluff | 6 ------ devenv.nix | 10 +++++++--- 2 files changed, 7 insertions(+), 9 deletions(-) delete mode 100644 .sqlfluff diff --git a/.sqlfluff b/.sqlfluff deleted file mode 100644 index 391f553..0000000 --- a/.sqlfluff +++ /dev/null @@ -1,6 +0,0 @@ -[sqlfluff] -dialect = postgres -# LT05 (line too long) is unfixable for long function signatures and seed -# rows, which makes `sqlfluff format` exit non-zero. Exclude it so it acts -# purely as a formatter under treefmt. -exclude_rules = LT05 diff --git a/devenv.nix b/devenv.nix index ed18882..ead0fce 100644 --- a/devenv.nix +++ b/devenv.nix @@ -24,10 +24,8 @@ "app:check".exec = "bin/rake"; # default: test + mutate_since }; - # treefmt drives all formatters (Ruby via syntax_tree, SQL via sqlfluff). treefmt.enable = true; treefmt.config = { - projectRootFile = "devenv.nix"; settings.formatter = { ruby = { command = "stree"; @@ -36,7 +34,13 @@ }; sql = { command = "sqlfluff"; - options = [ "format" ]; + options = [ + "format" + "--dialect" + "postgres" + "--exclude-rules" + "LT05" + ]; includes = [ "db/**/*.sql" ]; }; }; From 19476d0fb7df8da96c716c657d5f7a5b091d83de Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Pawe=C5=82=20Pacana?= Date: Wed, 24 Jun 2026 23:10:03 +0200 Subject: [PATCH 08/16] Format devenv.nix as well --- devenv.nix | 3 +++ 1 file changed, 3 insertions(+) diff --git a/devenv.nix b/devenv.nix index ead0fce..de6e214 100644 --- a/devenv.nix +++ b/devenv.nix @@ -26,6 +26,9 @@ treefmt.enable = true; treefmt.config = { + programs = { + nixfmt.enable = true; + }; settings.formatter = { ruby = { command = "stree"; From 5ab745ab14588ac37e462cf3c816b51b5208d7b4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Pawe=C5=82=20Pacana?= Date: Wed, 24 Jun 2026 23:14:32 +0200 Subject: [PATCH 09/16] Run bundle install as a devenv setup task - Move `bundle install` out of enterShell into an app:setup task gated on devenv:enterShell, so it still runs on shell entry. - Drop bin/setup; it only ran bundle install and is now redundant. --- bin/setup | 8 -------- devenv.nix | 3 ++- 2 files changed, 2 insertions(+), 9 deletions(-) delete mode 100755 bin/setup diff --git a/bin/setup b/bin/setup deleted file mode 100755 index dce67d8..0000000 --- a/bin/setup +++ /dev/null @@ -1,8 +0,0 @@ -#!/usr/bin/env bash -set -euo pipefail -IFS=$'\n\t' -set -vx - -bundle install - -# Do any other automated setup that you need to do here diff --git a/devenv.nix b/devenv.nix index de6e214..7644da6 100644 --- a/devenv.nix +++ b/devenv.nix @@ -14,10 +14,11 @@ enterShell = '' export MUTANT_SINCE="''${MUTANT_SINCE:-HEAD}" - bundle install --quiet ''; tasks = { + "app:setup".exec = "bundle install --quiet"; + "devenv:enterShell".after = [ "app:setup" ]; "app:format".exec = "treefmt"; "app:test".exec = "bin/rake test"; "app:mutate".exec = "bin/mutant run"; From 45bbd55ba3bfc06d917c01f6c2f73f8050721846 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Pawe=C5=82=20Pacana?= Date: Wed, 24 Jun 2026 23:36:04 +0200 Subject: [PATCH 10/16] Group test tasks under a test: namespace - Replace the app:test/app:mutate/app:check devenv tasks with test:unit (bin/m test), test:mutate (mutant --since) and test:pg (the pg-regress script). - Drive the pre-commit hook and CI via `devenv tasks run test:unit test:mutate`, keeping the test commands defined in one place. - Slim the Rakefile to gem tasks only; its Minitest/mutate/default tasks now duplicate the devenv tasks. --- .github/workflows/main.yml | 4 ++-- Rakefile | 13 ------------- devenv.nix | 22 ++++++++++++++-------- 3 files changed, 16 insertions(+), 23 deletions(-) diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index 711e2f3..f9a0909 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -39,5 +39,5 @@ jobs: path: .devenv/state/.bundle key: ${{ runner.os }}-gems-${{ hashFiles('Gemfile.lock') }} restore-keys: ${{ runner.os }}-gems- - - name: Run the default task - run: devenv shell -- bin/rake + - name: Run the tests + run: devenv tasks run test:unit test:mutate diff --git a/Rakefile b/Rakefile index eead8d7..6a897fa 100644 --- a/Rakefile +++ b/Rakefile @@ -1,17 +1,4 @@ # frozen_string_literal: true require "bundler/gem_tasks" -require "minitest/test_task" require_relative "lib/en57/tasks" - -Minitest::TestTask.create - -task :mutate do - system("bin/mutant run") -end - -task :mutate_since do - system("bin/mutant run --since #{ENV.fetch("MUTANT_SINCE")}") -end - -task default: %i[test mutate_since] diff --git a/devenv.nix b/devenv.nix index 7644da6..9e725eb 100644 --- a/devenv.nix +++ b/devenv.nix @@ -20,9 +20,16 @@ "app:setup".exec = "bundle install --quiet"; "devenv:enterShell".after = [ "app:setup" ]; "app:format".exec = "treefmt"; - "app:test".exec = "bin/rake test"; - "app:mutate".exec = "bin/mutant run"; - "app:check".exec = "bin/rake"; # default: test + mutate_since + + "test:unit" = { + exec = "bin/m test"; + after = [ "app:setup" ]; + }; + "test:mutate" = { + exec = ''bin/mutant run --since "''${MUTANT_SINCE:-HEAD}"''; + after = [ "app:setup" ]; + }; + "test:pg".exec = "pg-regress"; }; treefmt.enable = true; @@ -93,12 +100,11 @@ --schedule=test/pg_regress/schedule_existing ''; - git-hooks.hooks.rake = { + git-hooks.hooks.test = { enable = true; - name = "rake (test + mutate_since)"; - entry = "${pkgs.writeShellScript "rake-precommit" '' - export MUTANT_SINCE="''${MUTANT_SINCE:-HEAD}" - exec bin/rake + name = "test:unit + test:mutate"; + entry = "${pkgs.writeShellScript "pre-commit-test" '' + exec devenv tasks run test:unit test:mutate ''}"; pass_filenames = false; language = "system"; From ff5d0c0ea59df40bc8c662d7c570fc02087bd6c4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Pawe=C5=82=20Pacana?= Date: Wed, 24 Jun 2026 23:43:31 +0200 Subject: [PATCH 11/16] Rename app: tasks to the dev: namespace - Move setup and format out of the app: namespace into dev:setup and dev:format, leaving test:* as the only other group. --- devenv.nix | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/devenv.nix b/devenv.nix index 9e725eb..8714067 100644 --- a/devenv.nix +++ b/devenv.nix @@ -17,17 +17,17 @@ ''; tasks = { - "app:setup".exec = "bundle install --quiet"; - "devenv:enterShell".after = [ "app:setup" ]; - "app:format".exec = "treefmt"; + "dev:setup".exec = "bundle install --quiet"; + "devenv:enterShell".after = [ "dev:setup" ]; + "dev:format".exec = "treefmt"; "test:unit" = { exec = "bin/m test"; - after = [ "app:setup" ]; + after = [ "dev:setup" ]; }; "test:mutate" = { exec = ''bin/mutant run --since "''${MUTANT_SINCE:-HEAD}"''; - after = [ "app:setup" ]; + after = [ "dev:setup" ]; }; "test:pg".exec = "pg-regress"; }; From 3965f979a876ac2ba22cea4f9b06bd106f8b2534 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Pawe=C5=82=20Pacana?= Date: Wed, 24 Jun 2026 23:48:45 +0200 Subject: [PATCH 12/16] Run the whole test namespace in CI and pre-commit - Replace the explicit test:unit/test:mutate(/test:pg) lists with `devenv tasks run test`, so both CI and the pre-commit hook run every task in the test: namespace (now including test:pg). --- .github/workflows/main.yml | 2 +- devenv.nix | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index f9a0909..e476b87 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -40,4 +40,4 @@ jobs: key: ${{ runner.os }}-gems-${{ hashFiles('Gemfile.lock') }} restore-keys: ${{ runner.os }}-gems- - name: Run the tests - run: devenv tasks run test:unit test:mutate + run: devenv tasks run test diff --git a/devenv.nix b/devenv.nix index 8714067..0bd7f78 100644 --- a/devenv.nix +++ b/devenv.nix @@ -102,9 +102,9 @@ git-hooks.hooks.test = { enable = true; - name = "test:unit + test:mutate"; + name = "test namespace"; entry = "${pkgs.writeShellScript "pre-commit-test" '' - exec devenv tasks run test:unit test:mutate + exec devenv tasks run test ''}"; pass_filenames = false; language = "system"; From cb40f521ff280b8f40e98cd113a274c676f38f0b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Pawe=C5=82=20Pacana?= Date: Wed, 24 Jun 2026 23:52:55 +0200 Subject: [PATCH 13/16] Run dev:setup before dev:format - treefmt formats Ruby with the bundled syntax_tree (stree), so dev:format now depends on dev:setup to ensure the gems are installed before formatting. --- devenv.nix | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/devenv.nix b/devenv.nix index 0bd7f78..6203b3b 100644 --- a/devenv.nix +++ b/devenv.nix @@ -19,8 +19,10 @@ tasks = { "dev:setup".exec = "bundle install --quiet"; "devenv:enterShell".after = [ "dev:setup" ]; - "dev:format".exec = "treefmt"; - + "dev:format" = { + exec = "treefmt"; + after = [ "dev:setup" ]; + }; "test:unit" = { exec = "bin/m test"; after = [ "dev:setup" ]; From 19d3140c16d9d81140082dacb632df6e7e9b681f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Pawe=C5=82=20Pacana?= Date: Wed, 24 Jun 2026 23:57:47 +0200 Subject: [PATCH 14/16] Mutate everything since the last push in CI - On push to master, base MUTANT_SINCE on github.event.before instead of HEAD^ so mutant covers every commit the push introduced, not just the last one. PR runs still mutate against the PR base. --- .github/workflows/main.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index e476b87..f629ca3 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -15,7 +15,7 @@ jobs: runs-on: ubuntu-latest env: - MUTANT_SINCE: ${{ github.event.pull_request.base.sha || 'HEAD^' }} + MUTANT_SINCE: ${{ github.event.pull_request.base.sha || github.event.before }} steps: - uses: actions/checkout@v6 From 34da1e7e32b0a7926e680aa070724936a7bac683 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Pawe=C5=82=20Pacana?= Date: Thu, 25 Jun 2026 00:09:47 +0200 Subject: [PATCH 15/16] Run tests on agent stop instead of pre-commit - Replace the git pre-commit hook (git-hooks.hooks.test) with a Claude Stop hook that runs `devenv tasks run test` at the end of each agent loop. - On failure it sends the output back so the agent fixes it; a stop_hook_active guard avoids looping on the slow suite. - Commits no longer run the suite (the Stop hook is the gate now). --- devenv.nix | 25 ++++++++++++++----------- 1 file changed, 14 insertions(+), 11 deletions(-) diff --git a/devenv.nix b/devenv.nix index 6203b3b..8fd93f4 100644 --- a/devenv.nix +++ b/devenv.nix @@ -102,17 +102,6 @@ --schedule=test/pg_regress/schedule_existing ''; - git-hooks.hooks.test = { - enable = true; - name = "test namespace"; - entry = "${pkgs.writeShellScript "pre-commit-test" '' - exec devenv tasks run test - ''}"; - pass_filenames = false; - language = "system"; - stages = [ "pre-commit" ]; - }; - claude.code.enable = true; claude.code.hooks.git-hooks-run.enable = false; claude.code.hooks.format = { @@ -123,4 +112,18 @@ jq -r '.tool_response.filePath // .tool_input.file_path' | { read -r f; treefmt "$f"; } 2>/dev/null || true ''; }; + # Run the test suite when the agent loop ends. On failure, feed the + # output back so the agent fixes it; the stop_hook_active guard stops a + # second continuation from looping on the (slow) suite indefinitely. + claude.code.hooks.test = { + name = "Run the test namespace on stop"; + hookType = "Stop"; + command = '' + input=$(cat) + cd "''${DEVENV_ROOT:-.}" || exit 0 + devenv tasks run test 1>&2 && exit 0 + [ "$(printf '%s' "$input" | jq -r '.stop_hook_active // false')" = "true" ] && exit 0 + exit 2 + ''; + }; } From cb3657dd4e0409027263f452c8ab483a2c707f6a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Pawe=C5=82=20Pacana?= Date: Thu, 25 Jun 2026 00:18:02 +0200 Subject: [PATCH 16/16] Cut the crap --- devenv.nix | 45 ++++++++++++++++++++------------------------- 1 file changed, 20 insertions(+), 25 deletions(-) diff --git a/devenv.nix b/devenv.nix index 8fd93f4..c7217c1 100644 --- a/devenv.nix +++ b/devenv.nix @@ -59,9 +59,6 @@ }; }; - # Self-contained pg_regress run: spin up an ephemeral PostgreSQL with the - # Nix server binaries, load the schema, run the regression schedule, then - # tear it down. No Rakefile task, no gem, no env-var paths. scripts.pg-regress.exec = '' set -euo pipefail cd "$DEVENV_ROOT" @@ -103,27 +100,25 @@ ''; claude.code.enable = true; - claude.code.hooks.git-hooks-run.enable = false; - claude.code.hooks.format = { - name = "Format edited files with treefmt"; - hookType = "PostToolUse"; - matcher = "Write|Edit"; - command = '' - jq -r '.tool_response.filePath // .tool_input.file_path' | { read -r f; treefmt "$f"; } 2>/dev/null || true - ''; - }; - # Run the test suite when the agent loop ends. On failure, feed the - # output back so the agent fixes it; the stop_hook_active guard stops a - # second continuation from looping on the (slow) suite indefinitely. - claude.code.hooks.test = { - name = "Run the test namespace on stop"; - hookType = "Stop"; - command = '' - input=$(cat) - cd "''${DEVENV_ROOT:-.}" || exit 0 - devenv tasks run test 1>&2 && exit 0 - [ "$(printf '%s' "$input" | jq -r '.stop_hook_active // false')" = "true" ] && exit 0 - exit 2 - ''; + claude.code.hooks = { + format = { + name = "Format edited files with treefmt"; + hookType = "PostToolUse"; + matcher = "Write|Edit"; + command = '' + jq -r '.tool_response.filePath // .tool_input.file_path' | { read -r f; treefmt "$f"; } 2>/dev/null || true + ''; + }; + test = { + name = "Run the test namespace on stop"; + hookType = "Stop"; + command = '' + input=$(cat) + cd "''${DEVENV_ROOT:-.}" || exit 0 + devenv tasks run test 1>&2 && exit 0 + [ "$(printf '%s' "$input" | jq -r '.stop_hook_active // false')" = "true" ] && exit 0 + exit 2 + ''; + }; }; }