refactor: rename sh_events table to sh (migration 0008)
Simple ALTER TABLE rename. Rust struct/field names (ShEvent, sh_events)
are left as-is. SQL strings and doc references updated throughout.

https://claude.ai/code/session_01SpFto4X7xN2xnqgHbP62eB
change
commit a2cf05cd731d1eb2ddb868fa8485c150c3e3f627
author Claude <noreply@anthropic.com>
date
parent 71279bce
diff --git a/docs/CI-STATE.md b/docs/CI-STATE.md
index bf448a3..0791cb5 100644
--- a/docs/CI-STATE.md
+++ b/docs/CI-STATE.md
@@ -179,7 +179,7 @@ sequenceDiagram
     CI-->>Run: exit status
     Run->>Run: ingest_events(events.jsonl)
     Run->>DB: INSERT jobs (pass 1)
-    Run->>DB: INSERT sh_events (pass 2)
+    Run->>DB: INSERT sh (pass 2)
     alt RunFinished(Success) + exit 0
         Run->>DB: UPDATE runs SET state='complete'
     else RunFinished(PipelineFailure) + exit 0
@@ -195,7 +195,7 @@ Wire events (`quire-core/src/ci/event.rs`):
 * `JobFinished { job_id, outcome: complete | failed }` — `JobOutcome` is the closed set, not the full job-state enum.
 * `ShStarted { job_id, cmd }` / `ShFinished { job_id, exit_code }`
 
-`Run::ingest_events` reads the file in two passes (jobs first to satisfy the FK on `(run_id, job_id)`, then sh_events). Ingest failures are logged but never demote the run's own outcome — a partial DB write is preferable to losing the pass/fail signal.
+`Run::ingest_events` reads the file in two passes (jobs first to satisfy the FK on `(run_id, job_id)`, then sh). Ingest failures are logged but never demote the run's own outcome — a partial DB write is preferable to losing the pass/fail signal.
 
 ## Orchestration today
 
@@ -255,7 +255,7 @@ All six columns (`run_id`, `job_id`, `state`, `exit_code`, `started_at_ms`, `fin
 
 The schema permits six states (`pending`, `active`, `complete`, `failed`, `skipped`, `aborted`) but `ingest_events` only writes `complete` and `failed`. The other four states have no producer today — see Gaps below.
 
-### `sh_events` table
+### `sh` table
 
 All columns (`run_id`, `job_id`, `started_at_ms`, `finished_at_ms`, `exit_code`, `cmd`) are written by `Run::ingest_events` (pass 2) and read by the web detail view. All live.
 
diff --git a/docs/PLAN.md b/docs/PLAN.md
index ecca0cf..1293490 100644
--- a/docs/PLAN.md
+++ b/docs/PLAN.md
@@ -210,7 +210,7 @@ Keyboard navigation in the web UI. Atom feeds for recent commits (public, subjec
 - **CI network policy.** Default on (you'll want it for `cargo`, `npm`), with a per-pipeline `(network false)` opt-out. Or default off with explicit `(network true)`? Default on is more ergonomic; default off is more principled.
 - **Artifact size limits.** Probably want a per-run cap (1 GB?) and a per-repo cap (10 GB?). Values TBD after real use.
 - **Push-time feedback for CI.** When post-receive kicks off CI, should the push block until the run starts (not completes)? Probably yes, so the client sees "CI run #42 queued" in push output.
-- **Secrets for CI.** Declared in the global `:secrets` map, exposed to jobs via `(runtime.secret :name)`. Each value is either a plain string or a `{:file "/run/secrets/<name>"}` reference (Docker-secrets convention; one trailing newline stripped on read). Resolved values are redacted from CI output surfaces — run logs, recorded command strings, the `sh_events.cmd` column — by a per-run registry that replaces matches with `{{ name }}`. Values shorter than 8 bytes are not registered (false-positive risk; a `WARN` trace event names the skip). Tracing/application logs are not covered in v1 — audit existing trace call sites instead. Encrypted-at-rest for the secrets file is deferred until there's a reason.
+- **Secrets for CI.** Declared in the global `:secrets` map, exposed to jobs via `(runtime.secret :name)`. Each value is either a plain string or a `{:file "/run/secrets/<name>"}` reference (Docker-secrets convention; one trailing newline stripped on read). Resolved values are redacted from CI output surfaces — run logs, recorded command strings, the `sh.cmd` column — by a per-run registry that replaces matches with `{{ name }}`. Values shorter than 8 bytes are not registered (false-positive risk; a `WARN` trace event names the skip). Tracing/application logs are not covered in v1 — audit existing trace call sites instead. Encrypted-at-rest for the secrets file is deferred until there's a reason.
 - **Backup story.** `tar` the data volume. Deploy keys are in the volume, so they travel with the backup — convenient but also means the backup is sensitive. Worth thinking about encryption-at-rest for the backup, not just the source volume. Defer, but don't forget.
 - **`docker exec` performance.** Each git push spawns a new `docker exec`. Container startup is not involved (the container is already running), but there's still some latency — tens to hundreds of milliseconds. Probably fine for interactive use, possibly noticeable if something scripts many pushes. Measure, don't optimize preemptively.
 - **Reverse-proxy auth scheme.** Which auth mechanism does the proxy actually run? Candidates:
diff --git a/docs/config.md b/docs/config.md
index fd21568..720b30e 100644
--- a/docs/config.md
+++ b/docs/config.md
@@ -66,7 +66,7 @@ for the run; later appearances in `(sh ...)` stdout, stderr, or
 recorded command strings are replaced with `{{ name }}` in:
 
 - The CRI log files written under each run's workspace.
-- The `sh_events.cmd` column.
+- The `sh.cmd` column.
 - Any other `ShOutput`-derived persistence.
 
 Limits worth knowing:
diff --git a/docs/plans/2026-05-07-secret-redaction.md b/docs/plans/2026-05-07-secret-redaction.md
index 3af360e..6dd3633 100644
--- a/docs/plans/2026-05-07-secret-redaction.md
+++ b/docs/plans/2026-05-07-secret-redaction.md
@@ -64,14 +64,14 @@ The `Runtime::sh` method records `ShOutput` into `self.outputs`. Redact `stdout`
 No code changes needed. Since Task 3 redacts the `ShOutput` before it reaches `write_cri_log` and the DB insert path, both surfaces are covered.
 
 Schema audit — text columns that could carry user-derived text:
-- `sh_events.cmd` — covered by Task 3 (redacted before insert)
+- `sh.cmd` — covered by Task 3 (redacted before insert)
 - `runs.repo` — git repo name, system-generated
 - `runs.ref_name` — git ref, system-generated
 - `runs.sha` — commit hash, system-generated
 - `runs.failure_kind` — enum tag, not user text
 - `runs.container_id`, `image_tag`, `workspace_path` — system-generated
 
-No additional redaction sites needed beyond `sh_events.cmd`.
+No additional redaction sites needed beyond `sh.cmd`.
 
 - [x] **Step 1: Verify CRI logs and DB receive redacted content**
 - [x] **Step 2: Commit**
diff --git a/quire-ci/src/main.rs b/quire-ci/src/main.rs
index 6ac0f77..6dcba35 100644
--- a/quire-ci/src/main.rs
+++ b/quire-ci/src/main.rs
@@ -98,7 +98,7 @@ enum Commands {
         ///   `stdout` — write JSONL to stdout.
         ///   `<path>` — write JSONL to this file. The orchestrator
         ///              reads the file post-run to populate `jobs`
-        ///              and `sh_events` database rows.
+        ///              and `sh` database rows.
         #[facet(args::named, default = "null")]
         events: String,
 
diff --git a/quire-server/migrations/0008_rename_sh.sql b/quire-server/migrations/0008_rename_sh.sql
new file mode 100644
index 0000000..0e70f1e
--- /dev/null
+++ b/quire-server/migrations/0008_rename_sh.sql
@@ -0,0 +1 @@
+ALTER TABLE sh_events RENAME TO sh;
diff --git a/quire-server/src/bin/quire/commands/dev.rs b/quire-server/src/bin/quire/commands/dev.rs
index 0add0df..ba780c9 100644
--- a/quire-server/src/bin/quire/commands/dev.rs
+++ b/quire-server/src/bin/quire/commands/dev.rs
@@ -167,7 +167,7 @@ impl Seeder {
                 let finished_at = started_at + event.duration_ms;
                 self.db
                     .execute(
-                        "INSERT INTO sh_events (run_id, job_id, started_at_ms, finished_at_ms, exit_code, cmd)
+                        "INSERT INTO sh (run_id, job_id, started_at_ms, finished_at_ms, exit_code, cmd)
                          VALUES (?1, ?2, ?3, ?4, ?5, ?6)",
                         params![
                             run_id,
diff --git a/quire-server/src/ci/run.rs b/quire-server/src/ci/run.rs
index 262cd53..38a04d7 100644
--- a/quire-server/src/ci/run.rs
+++ b/quire-server/src/ci/run.rs
@@ -247,7 +247,7 @@ impl Run {
     /// Layout under the run dir on disk:
     /// * `quire-ci.log` — combined stdout+stderr of the subprocess.
     /// * `events.jsonl` — structured event stream (one JSON object per
-    ///   line). Ingested into `jobs` and `sh_events` after the
+    ///   line). Ingested into `jobs` and `sh` after the
     ///   subprocess exits.
     /// * `jobs/<job>/sh-<n>.log` — per-sh CRI logs, written by quire-ci
     ///   via `--out-dir`.
@@ -332,7 +332,7 @@ impl Run {
                 tracing::warn!(
                     run_id = %self.id,
                     error = %e,
-                    "failed to ingest quire-ci events; jobs/sh_events rows may be incomplete"
+                    "failed to ingest quire-ci events; jobs/sh rows may be incomplete"
                 );
                 None
             }
@@ -367,7 +367,7 @@ impl Run {
 
     /// Read `events.jsonl` and replay it into the database.
     ///
-    /// Done in two passes because `sh_events` has a foreign key on
+    /// Done in two passes because `sh` has a foreign key on
     /// `(run_id, job_id)` in `jobs`, and the wire format interleaves
     /// sh events with their owning job. Pass 1 inserts every job row
     /// (paired by `job_id`); pass 2 inserts sh events.
@@ -418,7 +418,7 @@ impl Run {
             }
         }
 
-        // Pass 2: sh_events rows. Pair ShStarted with ShFinished by job_id
+        // Pass 2: sh rows. Pair ShStarted with ShFinished by job_id
         // (sequential within a run-fn, so a single buffer slot per job
         // is enough).
         let mut pending_sh: HashMap<&str, (i64, &str)> = HashMap::new();
@@ -432,7 +432,7 @@ impl Run {
                         continue;
                     };
                     db.execute(
-                        "INSERT INTO sh_events (run_id, job_id, started_at_ms, finished_at_ms, exit_code, cmd) \
+                        "INSERT INTO sh (run_id, job_id, started_at_ms, finished_at_ms, exit_code, cmd) \
                          VALUES (?1, ?2, ?3, ?4, ?5, ?6)",
                         rusqlite::params![&self.id, job_id, started_at, event.at_ms, exit_code, cmd],
                     )?;
@@ -1170,7 +1170,7 @@ mod tests {
 
         let sh_events: Vec<(String, i64, i64, i32, String)> = db
             .prepare(
-                "SELECT job_id, started_at_ms, finished_at_ms, exit_code, cmd FROM sh_events \
+                "SELECT job_id, started_at_ms, finished_at_ms, exit_code, cmd FROM sh \
                  WHERE run_id = ?1 ORDER BY started_at_ms",
             )
             .unwrap()
diff --git a/quire-server/src/db.rs b/quire-server/src/db.rs
index e26819d..3f08a20 100644
--- a/quire-server/src/db.rs
+++ b/quire-server/src/db.rs
@@ -19,6 +19,7 @@ static MIGRATIONS: std::sync::LazyLock<Migrations<'static>> = std::sync::LazyLoc
         M::up(include_str!("../migrations/0005_rename_run_token.sql")),
         M::up(include_str!("../migrations/0006_traceparent.sql")),
         M::up(include_str!("../migrations/0007_schema_cleanup.sql")),
+        M::up(include_str!("../migrations/0008_rename_sh.sql")),
     ])
 });
 
diff --git a/quire-server/src/quire/web/db.rs b/quire-server/src/quire/web/db.rs
index 20198f8..a395469 100644
--- a/quire-server/src/quire/web/db.rs
+++ b/quire-server/src/quire/web/db.rs
@@ -110,7 +110,7 @@ pub fn load_run_detail(quire: &Quire, repo: &str, run_id: &str) -> Result<RunDet
 
     let mut sh_stmt = db.prepare(
         "SELECT job_id, started_at_ms, finished_at_ms, exit_code, cmd
-         FROM sh_events WHERE run_id = ?1
+         FROM sh WHERE run_id = ?1
          ORDER BY job_id, started_at_ms",
     )?;