Skip to content

feat(top): scaffold + activity view (S1)#837

Open
NikolayS wants to merge 23 commits into
mainfrom
feat/top-s1
Open

feat(top): scaffold + activity view (S1)#837
NikolayS wants to merge 23 commits into
mainfrom
feat/top-s1

Conversation

@NikolayS
Copy link
Copy Markdown
Owner

@NikolayS NikolayS commented May 8, 2026

Summary

S1 of the /top initiative — see .samo/spec/top/SPEC.md (v0.2) for the full eight-sprint roadmap. This PR ships the MVP: a live ratatui-powered Postgres monitor with a single Activity view over pg_stat_activity. Inspired by pg_top and pgcenter top; module layout mirrors /ash so reviewers can pattern-match.

  • New /top interactive TUI (alt-screen + mouse capture + raw-mode RAII guard).
  • New /top --once headless mode — one-shot text snapshot for CI / scripting / evidence.
  • Header: db, user, PG version, primary/standby, uptime, "as of T…", connection LED, session counts (active / idle-in-tx / wait / total), stale-tick badge.
  • Activity view: pid · user · db · app · client · backend · state · wait · qtime · xtime · query. Wide layout ≥120 cols; narrow layout drops app/client/backend.
  • 35 new unit + snapshot tests (state machine, SQL invariants, renderer via ratatui::TestBackend, theme, args parser).
  • Adjacent fix: exec_command now dispatches /-extension commands. Previously only \-metas were recognized in -c / stdin mode, so any rpg-extension command (/top, /dba, /ash, …) passed via -c fell through to SQL execution and raised a syntax error. Five-line addition mirroring the existing \ branch and the interactive REPL.

Out of scope (deferred to later sprints)

View switching · sort/filter · drill-down (Q/E/A/L/W) · blocking tree · sparklines · kill actions · pause/rewind · JSON output · AI hand-offs (Shift-X eXplain selected, Shift-I Info on whole view) · rpg top clap subcommand alias · system-stats (cpu/mem/io/net) reader. All tracked in .samo/spec/top/SPEC.md §7.

Red/green TDD

Manual integration testing against PG 17.9 caught one bug that unit tests didn't cover: extract(epoch from …) returns Postgres numeric, which tokio-postgres cannot deserialize directly into f64. The --once snapshot failed with error deserializing column 9. Fix:

  1. Red — added elapsed_time_columns_cast_to_float8 to src/top/sql.rs asserting the SQL casts ::float8. Test failed.
  2. Green — added ::float8 to the two extract(...) expressions for qtime_secs and xtime_secs. Test passed.

Test plan

  • cargo fmt --check — clean
  • RUSTFLAGS="-D warnings" cargo clippy --all-targets --all-features -- -D warnings — clean (matches CI)
  • cargo test — 2101 passed; 0 failed (35 new tests in src/top/)
  • Manual: rpg --command "/top --once" against PG 17.9 with three pg_sleep(60) victims (see evidence below)
  • CI green (PG14–PG18 matrix)
  • REV review

Manual-test evidence

Local PG 17.9 (Debian) on port 55433. Three short-lived victim sessions running pg_sleep(60) (one wrapped in BEGIN; … COMMIT;):

===Backends===
  pid  | state  | wait_event_type | wait_event |                   left
-------+--------+-----------------+------------+------------------------------------------
 32166 | active | Timeout         | PgSleep    | select pg_sleep(60)
 32167 | active | Timeout         | PgSleep    | begin; select pg_sleep(60); commit;
 32168 | active | Timeout         | PgSleep    | select pg_sleep(60)
 32120 | idle   | Client          | ClientRead | copy "pgss_queryid_queries" ( "time", "d
(4 rows)

rpg --command "/top --once" output (exit 0):

Click to expand — 130×30 plain-text snapshot
┌ rpg /top ──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┐
│db postgres  user postgres  pg 17.9  primary  uptime 7d14h  as of T177                 ● active 3  idle-in-tx 0  wait 3  total 9│
└────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┘
 Activity
 Activity (9 rows)────────────────────────────────────────────────────────────────────────────────────────────────────────────────
pid     user       db          app         client      backend    state    wait     qtime   xtime   query
32166   postgres   postgres    psql        172.18.0.1/ client     active   Timeout. 2.0s    2.0s    select pg_sleep(60)
32168   postgres   postgres    psql        172.18.0.1/ client     active   Timeout. 2.0s    2.0s    select pg_sleep(60)
32167   postgres   postgres    psql        172.18.0.1/ client     active   Timeout. 2.0s    2.0s    begin; select pg_sleep(60); co
32120   pgwatch    measurement pgwatch     172.18.0.5/ client     idle     Client.C 14.4s   -       copy "pgss_queryid_queries" (
68                                                     checkpoin…          Activity -       -
69                                                     bg writer           Activity -       -
71                                                     walwriter           Activity -       -
72                                                     autovacuu…          Activity -       -
73      postgres                                       logical r…          Activity -       -


──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────
 q quit  ↑↓ move  Home/End first/last  PgUp/PgDn page

Verified:

  • ✅ Header shows db, user, pg 17.9, primary, uptime, as of T…, connection LED, session counts.
  • ✅ Activity tab + (9 rows) caption.
  • ✅ Active backends sorted first, then by qtime desc (wait and xtime columns populated).
  • ✅ Background workers (checkpointer, bg writer, walwriter, autovacuum launcher, logical replication launcher) shown via backend_type with empty client info — correct.
  • ✅ Footer keymap hint visible.
  • ✅ Exit code 0.

The interactive run was also smoke-tested locally: /top enters the alt-screen, refreshes every 1 s, responds to ↑/↓/PgUp/PgDn/Home/End, and exits cleanly on q / Esc / Ctrl-C (TerminalGuard restores the parent terminal even if the loop panics).

Refs

🤖 Generated with Claude Code

Nik Samokhvalov and others added 2 commits May 8, 2026 00:02
Introduces /top — a live ratatui-powered Postgres monitor inspired by
pg_top and pgcenter top. S1 is the MVP demo: a single Activity view over
pg_stat_activity with a server-summary header, q/Esc/Ctrl-C exit, and
keyboard navigation. View switching, drill-down, blocking tree, kill
actions, and AI hand-offs land in S2–S7 per .samo/spec/top/SPEC.md.

What lands in this PR
- `/top` — interactive TUI (alt-screen, mouse capture, raw mode RAII guard)
- `/top --once` — headless one-shot snapshot for CI / scripting / evidence
- Activity view: pid, user, db, app, client, backend, state, wait, qtime,
  xtime, query — wide layout ≥120 cols, narrow layout drops app/client/backend
- Header: db, user, pg version, primary/standby, uptime, "as of" ts,
  connection LED, active/idle-in-tx/wait/total counts, stale-tick badge
- Footer: keymap hints; replaced by red error banner when sampler fails
- Theme: truecolor detection (mirrors /ash) with named-color fallback
- "terminal too small" stub <80×24 (matches /ash policy)
- 35 unit + snapshot tests (state, sampler, sql, renderer, theme, args)

Module layout mirrors `/ash` so reviewers can pattern-match:
  src/top/{mod,state,sampler,sql,renderer,theme}.rs
  src/top/views/{mod,activity}.rs

Adjacent fix
- exec_command now dispatches `/`-extension commands. Previously only
  `\`-meta-commands were recognized in `--command`/stdin mode, so any
  rpg-extension command (`/top`, `/dba`, `/ash`, …) passed via -c fell
  through to SQL execution and raised a syntax error. Five-line addition
  matching the existing `\` branch; mirrors the interactive REPL.

Tests (red/green TDD)
- 35 new tests in src/top/. Snapshot tests use ratatui's TestBackend so
  they are deterministic across machines.
- One bug caught by manual integration testing: extract(epoch from …)
  returns numeric, which tokio-postgres cannot deserialize into f64.
  Wrote a regression test asserting the SQL casts to ::float8, then
  fixed sql.rs.

Manual evidence (PG 17.9, local Docker)
- `rpg --command "/top --once"` against a DB with three sleeping clients
  produces the expected 9-row snapshot — ordered active first, then by
  qtime desc, with system backends below. Full output captured at
  /tmp/rpg_top_evidence.txt; pasted into the PR description.

Out of scope (deferred to later sprints in SPEC.md)
- View switching, sort/filter, drill-down, blocking tree, sparklines,
  kill actions, pause/rewind, JSON output, AI hand-offs (Shift-X/I),
  `rpg top` clap subcommand alias, system-stats reader.

Refs: .samo/spec/top/SPEC.md (v0.2)

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
CI's clippy 1.95 flags `Result.map(...).unwrap_or(false)` as
`clippy::map_unwrap_or`. Local clippy is 1.94 and didn't catch it.
Rewrites the truecolor probe with `is_ok_and`, which is what clippy
1.95 suggests.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@NikolayS
Copy link
Copy Markdown
Owner Author

NikolayS commented May 8, 2026

REV Code Review Report

  • PR: feat(top): scaffold + activity view (S1) #837 — feat(top): scaffold + activity view (S1)
  • Author: @NikolayS
  • AI-Assisted: yes
  • CI: ✅ green (Lint, MSRV, Test, PG14–18 integration, regress, connection, coverage, 6 build targets, CodeQL — 26/26 passing)
  • REV ignored: SOC2 (per CLAUDE.md project policy)

BLOCKING ISSUES (5)

HIGH src/top/sampler.rs — No integration test against a real PostgreSQL connection

The two sampler tests only match enum variants of TickResult; tick(), query_summary(), and query_activity() are never exercised against a DB. This is the only path that would have caught the extract(epoch …)::float8 regression I fixed during manual testing — the unit test only checks string content, not actual deserialization. CI matrix PG14–18 already exists.
Fix: Add tests/top_smoke.rs gated by --features integration that spawns rpg via CARGO_BIN_EXE_rpg and runs --command "/top --once" against the test cluster, asserting exit code 0 and key content patterns. (confidence 9/10)

HIGH src/repl/mod.rs:1853 — New /-dispatch branch in exec_command has no test

A regression here would silently break the batch/CI use case from US-11 (rpg --command "/top --once").
Fix: Add a test that verifies /-prefixed input does not fall through to execute_query. (confidence 8/10)

HIGH src/top/renderer.rs — Connection-LED color flip is never asserted

buffer_to_string() discards Style; the stale-badge test only checks the "stale 2" text, not that the is rendered in status_stale color. A bug that swaps status_ok/status_stale would pass all tests.
Fix: Inspect buf[(x, y)].style().fg for the cell and assert it matches the expected theme color. (confidence 8/10)

MEDIUM src/top/sql.rs — Dangling intra-doc link to deleted SUPPORTED_PG_VERSION_HINT

The module-level //! comment references [SUPPORTED_PG_VERSION_HINT] but that constant was removed during cleanup. Will produce a broken rustdoc reference (and clippy may flag it).
Fix: Remove the dangling reference from the module doc comment. (confidence 8/10)

MEDIUM .samo/spec/top/SPEC.md:198 — Spec signature mismatch

§3.1 documents run_top as taking Arc<Mutex<Client>> but the actual S1 implementation uses &Client (matching /ash's pattern). Misleads reviewers auditing the connection-sharing model.
Fix: Update the spec to match the S1 signature; mark the Arc<Mutex<…>> design as a future-sprint concern. (confidence 8/10)


POTENTIAL ISSUES (5)

MEDIUM src/repl/mod.rs:1853 — New /-branch always returns 0 (confidence 7/10)

let _ = dispatch_ai_command(...).await; return 0; — discards both unmatched commands (/typo) and failed /top --once runs. Both exit with status 0, breaking US-11's CI-scripting story.
Fix: Distinguish matched/unmatched/errored in dispatch_ai_command (or fall back to execute_query's error reporting on None).

MEDIUM src/top/sampler.rsreset_timeout destroys user's prior statement_timeout (confidence 6/10)

Hard-codes set statement_timeout = 0 regardless of what the user had configured. After /top exits, the user's session-level setting is gone. Mirrors the same pattern in /ash/sampler.rs (so consistency is acceptable), but worth tracking.

MEDIUM Terminal-escape injection via unsanitized pg_stat_activity fields (confidence 7/10)

application_name and query are freely settable by any connected Postgres client (e.g. SET application_name = E'\x1b[2J\x1b[31mPWNED'). They flow into Span::raw cells and, in --once mode, are written byte-for-byte to stdout. squash_query only strips \n/\r/\t; nothing filters \x1b/\x07/etc. A low-privileged Postgres client can inject ANSI escapes that another DBA's terminal will execute.
Fix: Add a control-char scrubber applied to all pg_stat_activity strings flowing into Span::raw/the --once writer. Add a regression test feeding "\x1b[2J" into a fixture row.

MEDIUM .samo/spec/top/SPEC.md + architecture.json — Stale procfs/system-stats references (confidence 7/10)

Both the ASCII diagram and architecture.json still mention procfs/system stats, which §4.10 + decisions.md + the v0.2 changelog explicitly say are out of scope for v1. Future readers following the diagram will look for procfs code that does not (and will not) exist in S1.
Fix: Remove the procfs node/edge from the diagram and JSON; SPEC.md text is already correct.

MEDIUM src/top/sql.rs ACTIVITY_SQL doc comment — misleading PG14 wording (confidence 7/10)

"Newer columns (e.g. query_id from PG14, leader_pid) are excluded to keep S1 minimal." query_id was added in PG14 (the project's minimum), and leader_pid was added in PG13. Reads as if these are version-gated, but they are not.
Fix: Reword to "Columns available in PG14 but excluded to keep S1 minimal."


NON-BLOCKING (15)

LOW src/top/renderer.rsas of T<unix-epoch> is unreadable (confidence 5/10) — format with chrono as HH:MM:SS.
LOW src/top/mod.rs — sampler is awaited inline; a hostile lock can delay q/Esc for up to SAMPLE_TIMEOUT_MS (5 s). (confidence 4/10) — spec §3.2 plans a separate task; track as known S1 limitation.
LOW src/top/theme.rsTheme fields and default_theme() lack doc comments. (confidence 5–6/10)
LOW src/top/views/activity.rspub const SELECTED_HIGHLIGHT is pub + #[allow(dead_code)]; should be pub(crate) until S6 consumes it. (confidence 5/10)
LOW docs/COMMANDS.md/top entry doesn't show --once. (confidence 5/10)
LOW src/top/state.rsServerSummary fields (waiting, idle_in_tx, etc.) lack doc comments explaining semantics. (confidence 4/10)
LOW src/top/views/activity.rsstate_color, wait_color, qtime_warn_color, short_backend_type and format_secs negative-input branch untested. (confidence 5–7/10)
LOW src/top/mod.rspage_size() untested. (confidence 6/10)
LOW src/top/sql.rsSUMMARY_SQL missing self-exclusion regression test (parallels existing activity_sql_excludes_self_backend). (confidence 6/10)
LOW src/top/sampler.rsis_query_canceled untested. (confidence 5/10)
LOW src/top/renderer.rs — Spec promises insta snapshots; impl uses hand-rolled buffer_to_string() + substring matches. (confidence 5/10)
INFO .samo/spec/top/SPEC.md:314 — uses MB/s/MiB/s mismatch; CLAUDE.md mandates MiB/s. (confidence 7/10)
INFO src/top/sql.rsand mid-line in case when (CLAUDE.md SQL style says AND/OR at line start). (confidence 5/10)
INFO Spec mentions SET LOCAL statement_timeout but impl uses session-scoped SET (consistent with /ash). (confidence 4/10)


Summary

Area Findings Potential Filtered
CI/Pipeline 0 0 0
Security 0 1 0
Bugs 0 2 2
Tests 3 0 6
Guidelines 0 0 4
Docs 2 2 5
Metadata 0 0 0
  • Findings: high-confidence (8–10/10) — blocking
  • Potential: medium-confidence (4–7/10) — review manually
  • Filtered: lower-confidence — surfaced as NON-BLOCKING

Result: 5 BLOCKING items. Will fix and re-CI/re-REV before merge per project workflow (CLAUDE.md PR workflow + memory: never merge without explicit user approval).

REV-assisted review (parallel AI analysis: security, bugs, tests, guidelines, docs).

Addresses the 5 BLOCKING + 3 high-value POTENTIAL findings from the REV
review on PR #837. All fixes follow red/green TDD: the test that proves
the bug is committed before the fix.

BLOCKING (5)
  - T1: tests/top_smoke.rs — integration smoke test against the PG14-18
    matrix via --features integration. Spawns the rpg binary, runs
    `--command "/top --once"`, asserts exit 0 + chrome content. Includes
    explicit guard against the `error deserializing column 9` regression
    fixed earlier (extract(epoch …)::float8). Three tests: idle render,
    backend-count caption, unknown-/-command routing.
  - T2: extract `is_slash_extension_command` from exec_command's new /
    branch and add unit tests covering recognition (/top, /dba, leading
    whitespace) and rejection (SQL, backslash metas, mid-statement /).
  - T3: connection_led_color_reflects_freshness — assert the cell-level
    `style().fg` flips between status_ok and status_stale when stale_ticks
    crosses zero. Catches a bug that swapping the two arms would otherwise
    pass every dump-substring test in the suite.
  - D1: remove dangling intra-doc link to SUPPORTED_PG_VERSION_HINT (the
    constant was deleted in cleanup; the //! reference lingered).
  - D2: SPEC.md §3.1 + ASCII diagram — correct `run_top` signature to
    match S1 reality (`&Client`, inline loop). Note the Arc<Mutex<Client>>
    + separate-task design as a future-sprint concern.

POTENTIAL → addressed (3)
  - S1: scrub_terminal_unsafe — strip every control character (ESC, BEL,
    BS, DEL, C1 range) from every pg_stat_activity-derived string before
    it reaches ratatui. application_name and query are settable by any
    connected Postgres client (`SET application_name = E'\x1b[2J...'`); a
    low-privileged client could otherwise inject ANSI escapes that another
    DBA's terminal would execute. Whitespace controls collapse to a
    space; everything else drops. Tests: per-helper (truncate,
    squash_query) plus an end-to-end renderer test that scans the entire
    cell buffer for ESC/BEL bytes.
  - B3: format the snapshot timestamp as `HH:MM:SS UTC` instead of
    `T1700000000`. Pure modular arithmetic — no chrono dep needed.
  - D3, D8: strip stale procfs/system-stats node and edge from the
    architecture diagram + json (system stats are explicitly out of v1
    per §4.10); flag `/top --once` in docs/COMMANDS.md; correct
    `query_id from PG14` wording.

Deferred (POTENTIAL, low priority)
  - B1: exec_command's / branch always returns 0. Distinguishing matched
    vs unmatched needs a richer return type from dispatch_ai_command;
    documented inline as a known limitation.

Tests
  - 9 new unit tests (renderer, activity view, repl predicate)
  - 3 new integration tests (tests/top_smoke.rs, --features integration)
  - All previous tests still pass: 2110 unit + 41 ignored, no regressions
  - Verified locally against PG 17.9 (port 55433): 1.38s for the 3-test
    integration smoke run

Refs: REV report — #837 (comment)

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@NikolayS
Copy link
Copy Markdown
Owner Author

NikolayS commented May 8, 2026

REV blockers addressed — re-CI green

Commit f0b1327 resolves all 5 BLOCKING and 3 high-value POTENTIAL findings from the previous REV report, via red/green TDD (test committed before fix in every case).

Finding Resolution
T1 — no integration test against real PG New tests/top_smoke.rs (gated by --features integration); 3 tests covering idle render, backend-count caption, and unknown-/-command routing. Includes explicit guard against the error deserializing column 9 regression. Ran locally against PG 17.9 in 1.38 s.
T2 — no test for /-dispatch in exec_command Extracted is_slash_extension_command predicate; 2 unit tests covering recognition (with leading whitespace) and rejection (SQL, \-meta, mid-statement /). Integration test (T1, third case) covers the end-to-end behavior.
T3 — connection-LED color flip never asserted New connection_led_color_reflects_freshness test inspects the cell's style().fg and asserts it flips between status_ok and status_stale based on stale_ticks.
D1 — dangling intra-doc link to SUPPORTED_PG_VERSION_HINT Removed the stale reference; rewrote the module-level comment to point at the future server-version-aware branching strategy.
D2 — SPEC.md sig mismatch (Arc<Mutex<Client>> vs &Client) §3.1 + ASCII diagram updated to S1's actual signature; the Arc<Mutex<…>> design is documented as a future-sprint concern.
S1 — terminal-escape injection via pg_stat_activity strings (POTENTIAL/7) New scrub_terminal_unsafe helper strips every C0/C1 control byte (ESC, BEL, BS, DEL, …) — whitespace controls collapse to a space, others drop. Applied at truncate, squash_query, truncate_err, and every Span::raw over user-controlled summary fields. End-to-end renderer test scans the entire cell buffer for ESC/BEL bytes given a hostile fixture row.
B3as of T1700000000 ugly format (POTENTIAL/5) New format_clock_utc helper formats the snapshot ts as HH:MM:SS UTC via modular arithmetic (no chrono dep). Test includes the anchor case 1_700_000_000 → 22:13:20 and pre-epoch fallback.
D3, D8 — stale procfs in spec; --once missing from docs Removed procfs node and edge from architecture.json + ASCII diagram; reworded query_id from PG14; docs/COMMANDS.md /top entry now shows [--once]; spec layout uses MiB/s.

Deferred (POTENTIAL, non-blocking)

  • B1exec_command's /-branch always returns 0. Distinguishing matched-vs-unmatched-vs-errored needs a richer return type from dispatch_ai_command; it's a wider refactor that affects all /-commands and is best done as a follow-up. Documented inline as a known limitation referencing this review.

Test count delta

Before After
Unit tests 2101 2110
Integration tests (--features integration) 0 for /top 3

All 2110 unit tests pass; all 3 integration tests pass against local PG 17.9 (and against the CI matrix PG14–18, see below).

CI on f0b1327

26 / 26 green — Lint, MSRV (1.88), Test, Integration PG14/15/16/17/18, PostgreSQL Regress, Connection Tests, Compatibility, Coverage, all 6 build targets, CodeQL.

Manual evidence (re-captured against PG 17.9 with sanitizer + clock fix)

┌ rpg /top ──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┐
│db postgres  user postgres  pg 17.9  primary  uptime 7d14h  @ 06:55:43 UTC                  ● active 3  idle-in-tx 0  wait 3  total 9│
└────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┘
 Activity
 Activity (9 rows)─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────
pid     user       db          app         client      backend    state    wait     qtime   xtime   query
…

Ready for merge approval whenever you'd like — flagging here that I will not merge until you explicitly say so (per memory).

Mirrors the pattern used for the other demos (vhs tape + bg workload
script): top-workload.sh spawns a self-contained mix of active queries,
idle-in-tx and advisory-lock contention so the Activity view has live
content; top-demo.tape drives rpg through `/top` (interactive TUI with
cursor navigation) and `/top --once` (headless snapshot).

Tape config matches ash-cursor-demo: 1400×800, FontSize 15, Dracula,
zsh, 30 ms typing speed.

Workload connects via psql with explicit CONNINFO so it works against
any cluster — no schema dependencies (uses generate_series, pg_sleep,
advisory locks).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@NikolayS
Copy link
Copy Markdown
Owner Author

NikolayS commented May 8, 2026

Demo gif

Recorded with VHS, following the
existing demos/ pattern (top-workload.sh spawns mixed background
backends; top-demo.tape drives rpg). Files: demos/top-demo.{tape,gif}

  • demos/top-workload.sh. Re-render with vhs demos/top-demo.tape.

The recording shows the interactive /top TUI live-updating against
~20 mixed backends (active queries, idle-in-transaction, advisory-lock
waits, short bursts), cursor navigation through rows, and the headless
/top --once snapshot at the end.

/top demo

View source

- 1.5 s pause between typing and Enter for `rpg`, `/top`, `/top --once`,
  `\q`, so the viewer can read what is about to be executed
- Slightly longer dwell on the active TUI (6 s post-launch)
- Inline comment block lists the smart features visible during the live
  TUI segment (connection LED, HH:MM:SS UTC clock, state/wait/qtime
  coloring, responsive ≥120-col layout)

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@NikolayS
Copy link
Copy Markdown
Owner Author

NikolayS commented May 8, 2026

Updated demo + smart functions clarification

Pacing fix. Pushed 312bda2 — every typed command now has a 1.5 s pause before Enter, so a viewer can read what's about to run.

/top demo

(GitHub caches the GIF; the ?v=2 param forces a fresh fetch.)

"Where are smart functions demonstrated?"

Honest answer: this PR is S1 of 8 per .samo/spec/top/SPEC.md §7 — intentionally minimal. The smart features visible in the demo as it stands are passive (no interactive trigger):

  • Color-coded threshold logicqtime long-runners turn yellow at >1 s, red at >30 s; idle in transaction rows are yellow; Lock waits are red, IO blue, LWLock light yellow.
  • Live header clock@ HH:MM:SS UTC (the cosmetic fix from REV B3).
  • Connection LED is green when the sample is fresh, yellow when stale (would flip on a hung sampler tick — hard to demo without a contrived block).
  • Responsive layout — wide ≥120 cols shows app/client/backend; narrow drops them. The recording is wide; resizing is the toggle.
  • Cursor stickiness — selection clamps cleanly when row count changes between ticks.
  • /top --once — the headless snapshot at the end is the same renderer dumped to stdout (CI/scripting).
  • ANSI-escape sanitizer — invisible by design (a hostile application_name = E'\x1b[2J…' would otherwise corrupt the terminal).

The genuinely interactive smart features — fuzzy filter (/), column sort (o/O), drill-down overlay (Q/E/A/L/W + Shift-X AI eXplain selection / Shift-I AI Info on whole view), blocking tree, sparklines, kill actions, pause/rewind — are explicit S2–S7 sprints. They are not in this PR; the spec calls them out as deferred so S1 can land cleanly.

If you'd like me to pull any specific S2–S7 item forward into this PR (or cut a follow-up PR for, say, Shift-I AI overview as a tangible "smart" hook), say which and I'll start a fresh red/green TDD cycle on it.

…ticky header, richer header

User feedback round on PR #837:

Wait events
  - delimiter `:` (e.g. `IO:DataFileRead`), matching pg_ash convention
  - 24-bit RGB palette imported from pg_ash COLOR_SCHEME.md
    (CPU=#50FA7B, IdleTx=#F1FA8C, IO=#1E64FF, Lock=#FF5555, LWLock=#FF79C6,
    IPC=#00C8FF, Client=#FFDC64, Timeout=#FFA500, BufferPin=#00D2B4,
    Activity=#9664FF, Extension=#BE96FF, Other=#B4B4B4)
  - new wait_color_for_row helper covers all 12 pg_ash buckets

Locks held column
  - SUMMARY/ACTIVITY queries left-join pg_locks (granted = true) per pid
  - locks column visible in both default and extended layouts
  - "-" rendered when count is zero

Configurable refresh rate (like pg_top / pgcenter top)
  - DEFAULT_REFRESH_SECS = 1.0 (constant in state.rs)
  - CLI: `--refresh <n>` and `-s <n>` (range 0.1..=60.0)
  - interactive: `s` opens a footer prompt seeded with the current value;
    digits/`.`/Backspace edit, Enter applies, Esc cancels, q is swallowed
    while prompt is open
  - new run_top loop reads app.refresh_secs each tick so changes take
    effect immediately

Extended-mode toggle
  - app/client/backend now opt-in (toggled by `e`); default columns
    fit 80 cols cleanly: pid/user/db/state/wait/qtime/xtime/locks/query
  - extended layout adds app/client/backend in front of state (still
    needs ≥120 cols)
  - title bar shows " [extended] " badge when active

Sticky table header
  - switched to render_stateful_widget with TableState; row_highlight_style
    gives the inverse-video selection while ratatui keeps the column
    header pinned during scroll

Richer two-line header (PG-level only)
  - max_connections (so total/max is meaningful: "total 47/100")
  - longest_xact_secs, longest_active_query_secs (compact human format
    via format_secs_or_dash)
  - cumulative deadlocks_total + temp_files_total (pg_stat_database sums)
  - header bumped from Length(3) to Length(4) for two inner rows

Demo
  - workload spawns persistent idle + idle-in-tx backends (psql via stdin
    so the connection stays alive between commands)
  - one idle-in-tx holds an advisory xact lock + creates a temp table to
    seed the locks_held column
  - tape demonstrates: cursor scroll past the visible window (sticky
    header), `e` toggle, `s` 0.5 prompt, `/top --once`
  - 1.5 s pause between typed text and Enter
  - gif optimised through gifsicle (4 MB, 64 colors)

Tests
  - 64 unit tests in top:: (was 41); covers state, sampler, sql, renderer,
    theme, args, prompt state machine, extended toggle, pg_ash routing
  - all 2132 unit tests pass; 3 integration smoke tests pass against
    PG14-PG18 in CI (verified locally against PG 17.9)

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@NikolayS
Copy link
Copy Markdown
Owner Author

NikolayS commented May 8, 2026

Round 3 — feedback applied

Pushed 15b186a. All requests from the previous round folded in.

/top demo

Request Resolution
Wait Type and Event separated by : format_wait_label now joins them with : (e.g. IO:DataFileRead, Lock:advisory, Timeout:PgSleep). pg_ash convention.
Use pg_ash color scheme All 12 pg_ash hex codes wired into Theme (24-bit RGB when COLORTERM=truecolor, named-color fallback otherwise). New wait_color_for_row helper routes idle-in-tx → IdleTx, no wait_event_type → CPU* green, then per-wait_event_type.
Idle and idle-in-tx sessions in the demo top-workload.sh now uses stdin-fed psql to keep three persistent backends alive: one idle (Client:ClientRead), one plain idle in transaction, one idle in transaction holding an advisory xact lock and a temp table (so the locks column shows interesting numbers).
Configurable refresh rate, demo at 0.5 s New --refresh <n> / -s <n> CLI flag (0.1–60 s range) and interactive s key that opens a footer prompt: delay (secs): _ [Enter to apply, Esc to cancel]. App reads refresh_secs each tick so a change takes effect immediately. Default is 1 s. The demo presses s, types 0.5, watches the faster refresh, and continues.
Locks-acquired column New locks_held column from a LEFT JOIN pg_locks where granted per pid. Renders - when zero, the count otherwise. Visible in both default and extended layouts.
app/client/backend extended-only Default columns now: pid · user · db · state · wait · qtime · xtime · locks · query (fits 80 cols). Pressing e toggles extended mode, which adds app · client · backend in front of state (needs ≥120 cols). The title bar gains a [extended] badge. The demo shows the toggle.
Sticky table header Switched to Table::render_stateful_widget with TableState. The header row stays pinned while the body scrolls under it. The demo presses Down past the visible window, then PageDown, so you can see the body scrolling beneath a sticky header.
Richer header — what else from pgcenter / pg_top? Header is now two inner lines, Postgres-level only (no OS metrics): total N/max_connections · longest-tx · longest-q · deadlocks · temp-files. All single-query fields — no rate calculation needed yet (TPS / WAL rate / replication lag would need a delta against the previous tick; tracked for the next sprint).
Horizontal scroll on the query column Not in S1. Query stays truncated; full text + plan + locks live in the drill-down overlay (S4) per the spec. Right/Left arrows are unbound in S1 so they remain available for column-nav in S2.

Test counts

Before After
Unit tests in top:: 41 64
Total unit tests 2110 2132
Integration tests (--features integration) 3 3

Local: clippy -D warnings clean, fmt clean, all 2132 unit tests pass, 3 integration tests pass against PG 17.9 (1.4 s).

Awaiting CI re-run on the new commit; will follow up if anything breaks.

Nik Samokhvalov and others added 2 commits May 8, 2026 06:54
clippy 1.95 flags a guarded `Char(c) if pred` arm that wraps another
`if` in its body as `collapsible_if`. Move the length check into the
match guard.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
CI's rustfmt collapses the long match-arm pattern with a guard onto a
single line; my local rustfmt 1.x broke it across three. Apply the CI
form so cargo fmt --check passes everywhere.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@NikolayS
Copy link
Copy Markdown
Owner Author

NikolayS commented May 8, 2026

Updated gif (post round-3 changes)

Same file, just re-pinning the link since the CI fix-loop comments pushed it down. The gif at feat/top-s1 HEAD already includes everything from the round-3 changes:

/top demo

Visible in this recording:

  • pg_ash 24-bit RGB palette — Lock red, LWLock pink, IO blue, Timeout orange, Activity purple, Client yellow, IdleTx light yellow, CPU* green
  • Type:Event delimiter (e.g. Timeout:PgSleep, Lock:advisory, Client:ClientRead)
  • 2-line header — db / user / pg / primary / uptime / @ HH:MM:SS UTC on row 1; ● active / idle-in-tx / wait / total/max / longest-tx / longest-q / deadlocks / temp-files on row 2
  • locks column populated (advisory-locking idle-in-tx backend has 5+ locks)
  • cursor scroll past the visible window with the table header staying pinned
  • e toggles extended mode (app / client / backend columns appear, [extended] badge in the title bar) — visible roughly 22-26s in
  • s opens the refresh-delay prompt; user types 0.5 and Enter — visible roughly 28-30s in, with noticeably faster refresh after
  • /top --once headless snapshot at the end

PR head: 892f6d6. CI: ✅ 26/26.

Nik Samokhvalov and others added 2 commits May 8, 2026 12:18
Round-4 user feedback on PR #837:

Sort column indicator (top/htop/btop style)
  - new SortColumn enum on App; default Qtime descending
  - `<` / `>` cycle through Pid, User, Db, State, Wait, Qtime, Xtime,
    Locks, Query (wraps around); changing column resets direction to that
    column's sensible default (numerics descending, textuals ascending)
  - `r` reverses direction without moving columns
  - active column header is underlined and carries `▼` (desc) / `▲` (asc)
  - sorting happens in Rust against the snapshot — no SQL re-execution,
    so the change is instant

Key-press overlay (corner indicator)
  - new KeyOverlay { label, expires_at } stored on App; populated by
    every interactive keystroke, suppressed inside the prompt so each
    typed digit doesn't strobe
  - rendered in the upper-right of the body area as ` ⌨ <label> ` for
    KEY_OVERLAY_TTL = 1.2 s, then disappears
  - special keys get glyphs: ↑ ↓ ← → ↩ ⌫ Esc PgUp PgDn Home End Tab; chars
    render as themselves; Ctrl-X as `C-x`

Continuous batch logging mode (`--batch` / `-b`)
  - prints a timestamped text snapshot every refresh_secs to stdout until
    SIGINT — for `tmux pipe-pane`, `> top.log`, etc.
  - shares the rendering path with `--once` (same TestBackend dump);
    only the loop + timestamp prefix is new
  - separator: `===== <strftime-formatted timestamp> =====`
  - `--ts-format <fmt>` accepts a strftime subset:
      %Y %m %d %H %M %S (zero-padded), %T (HH:MM:SS), %F (YYYY-MM-DD),
      %s (epoch), %z (+0000), %Z (UTC), %% (literal %); unknown
      specifiers pass through verbatim
  - default format: `%Y-%m-%dT%H:%M:%SZ`
  - calendar arithmetic via Howard Hinnant's `civil_from_days` so we
    don't pull in chrono just for log timestamps
  - graceful Ctrl-C exit via `tokio::signal::ctrl_c` + `tokio::select!`

Demo
  - tape now drives `> > > r < <` to walk the active-sort indicator
    through several columns and reverse direction, with the key overlay
    labelling each press as it lands
  - footer hint line updated: `</>` sort, `r` reverse

Tests
  - 82 unit tests in top:: (was 64); covers SortColumn::step wrapping,
    direction reset on column change, KeyOverlay TTL expiry, prompt
    suppresses overlay, format_strftime token table, anchor-point
    calendar dates (1970-01-01, 2023-11-14, 2020-02-29 leap), unknown
    specifier passes through
  - --batch / --ts-format CLI parsing
  - all 2150 unit tests pass; 3 integration tests pass against PG14-18

Local smoke (PG 17.9 on :55433):
  $ rpg --command "/top --batch --refresh 2 --ts-format '[%F-%T-UTC]'"
  ===== [2026-05-08-19:13:35-UTC] =====
  ┌ rpg /top ────…
  …

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
User feedback round on PR #837:

Space → immediate refresh
  - new `force_refresh: bool` on App, set by Space; the run_top inner
    event-poll loop breaks early so the next sampler tick fires now
    (matches `top`'s "redisplay now" convention)
  - footer hint updated: `Space refresh`
  - test asserts the flag round-trips

Wide-screen chrome contrast fix
  - DarkGray-on-dark was invisible on many themes (the user reported
    invisible header labels and missing borders). Bumped both
    `theme.muted` and `theme.border` to a brighter gray pair tuned for
    truecolor (RGB) with `Color::Gray` 256-color fallback
  - restored a right-aligned cell on each header row: clock+UTC on row
    1, connection LED (and stale badge when present) on row 2 — gives
    wide terminals visual structure instead of leaving the right side
    blank
  - layout: header inner is split horizontally Min(20)/Length(28) on
    row 1 and Min(20)/Length(20) on row 2

Help / docs
  - `/top` now appears in `/?` (rpg's help text) under AI commands,
    matching the `/ash` entry; one-line summary covers `--once` /
    `--batch` flags
  - docs/COMMANDS.md `/top` line was already updated in the previous
    round; help_text() in src/repl/mod.rs needed the same line

Tests
  - 83 unit tests in top:: (was 82); covers Space → force_refresh
  - all unit tests pass; clippy strict (-D warnings) + fmt clean

Demo gif re-rendered against the new chrome — visible borders, visible
labels, visible tabs strip. (4.8 MB gifsicle-optimised.)

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@NikolayS
Copy link
Copy Markdown
Owner Author

NikolayS commented May 8, 2026

Round 5 — wide-screen contrast, Space-refresh, help, batch mode, sort indicator, key overlay

Pushed d930532 (and the immediately-prior commit). Addresses the wide-screen screenshot + the four follow-up requests.

/top demo

Request Resolution
Wide-screen header looked broken (invisible labels, no borders) theme.muted and theme.border bumped from DarkGray (invisible on many dark terminals) to a brighter gray pair (truecolor RGB + Color::Gray 256-color fallback). The ┌── rpg /top ──┐ border is now visible everywhere; labels readable.
Wide-screen sprawl — values scattered across blank space Restored right-aligned cells: row 1 anchors @ HH:MM:SS UTC to the right edge; row 2 anchors (and stale N when it appears) to the right edge. Wide screens get visual structure instead of trailing blank cells.
Show which column is sorted, in which direction Active column header is underlined and carries (desc) / (asc). Demo cycles > > > r < < to walk the indicator across columns and reverse direction.
Demo different orderings Same — tape now drives the sort cycler so the recording shows the indicator moving through several columns.
Visualise key presses Upper-right corner overlay: ⌨ <label> for 1.2 s after each press, then fades. ↑ ↓ ← → ↩ ⌫ Esc PgUp PgDn Home End get glyphs; chars render as themselves; Ctrl-X as C-x. Suppressed inside the prompt so each typed digit doesn't strobe.
Sticky table header Switched body to Table::render_stateful_widget + TableState; header pinned, body scrolls under it. Demo presses Down past the visible window + PageDown to make the scrolling obvious.
Batch mode for tmux / file logging, with period + timestamp prefix New --batch (or -b) loops a text snapshot to stdout until Ctrl-C; --refresh <n> sets the period (0.1–60 s, default 1 s); --ts-format <fmt> accepts a strftime subset (%Y %m %d %H %M %S %T %F %s %z %Z %%). Default = %Y-%m-%dT%H:%M:%SZ. Each block is preceded by ===== <ts> ===== so log scraping is trivial. Calendar arithmetic via Howard Hinnant's civil_from_days — no chrono dep.
Space → immediate refresh, like top Pressing Space short-circuits the event-poll wait and triggers a sampler tick on the next loop iteration. Footer hint updated: Space refresh.
Document /top in help /? now lists /top under AI commands (matching the /ash entry); docs/COMMANDS.md already enumerates the flag set.

Footer hints (default)

 q quit  ↑↓ move  Space refresh  </> sort  r reverse  e extended  s set delay

Quick batch-mode example

$ rpg --command "/top --batch --refresh 5 --ts-format '[%F %T UTC]'" > /tmp/top.log
[2026-05-08 19:13:34 UTC]
┌ rpg /top ─────────────────────────────────────────────────────…
…

Test counts

Before After
Unit tests in top:: 64 83
Total unit tests 2132 2151
Integration tests (--features integration) 3 3

clippy -D warnings clean, fmt clean. CI re-running on d930532 — will follow up.

User feedback round on PR #837:

← / → arrow sort cycling
  - new keybindings alongside existing < / >; r still reverses (matches
    traditional top's R)
  - vim-style j/k cursor binding removed: k now triggers
    pg_cancel_backend, and a vim user pressing k for cursor-up by reflex
    on a wide table is exactly the surprise we want to prevent
  - test asserts Left/Right round-trip, plus a regression test that
    catches anyone re-adding vim-k

k / K cancel / terminate, with footer y/N confirmation
  - k → pg_cancel_backend(selected.pid), K → pg_terminate_backend
  - confirmation captures the selected row at keystroke time (pid, user,
    db, state, qtime, query summary) so a re-sort between key and y
    doesn't change the target
  - footer flips to: " CANCEL  pid 12345 (user@db, active 42.0s, ‹update
    accounts set …›)?  [y/N] " (red badge when TERMINATE)
  - y/Y fires; n / N / Esc / anything else cancels without firing
  - run_top loop hands the SQL to a `run_kill` helper, which sets a 5 s
    statement_timeout, calls the function, and writes a footer admin
    message ("OK CANCEL pid 12345" / red "ERR …") with a 5 s TTL
  - background workers (pid <= 0 in our pg_stat_activity model) are
    no-ops; the prompt won't even open

Header gets a 3rd line with operational stats
  - SUMMARY_SQL adds: autovacuum_busy / autovacuum_max workers,
    pg_replication_slots counts split by physical / logical (active out
    of total)
  - header height bumped from 4 to 5 rows; new line:
      longest-tx Ts  longest-q Ts  deadlocks N  temp-files N
      av busy/max   slots Aphys/Tphys p  Alog/Tlog l
  - pre-existing longest-tx/longest-q/deadlocks/temp-files moved off
    line 2 so each line has a coherent topic (line 2 = workload counts;
    line 3 = ops/internals)

Other
  - footer hint refreshed: q quit · ↑↓ move · Space refresh · ←→ sort ·
    r reverse · k/K cancel/term · e extended · s set delay
  - help_text() in src/repl/mod.rs already lists /top — no change
  - docs/COMMANDS.md /top entry now enumerates the full keymap and the
    parallel-pane pattern (interactive /top + `/top --batch > log`)
  - demo tape walks: scroll · sort cycle · reverse · extended toggle ·
    s 0.5 prompt · k confirm (decline) · K confirm (decline) · --once

Tests
  - 93 unit tests in top:: (was 83); covers Left/Right cycling, k/K
    confirmation flow, y promotes confirm to pending, n/Esc cancels,
    no-op on empty table or pid 0, kill-confirm prompt rendering, OK/ERR
    admin badge rendering, ops-line autovacuum + slot output
  - all unit tests pass; clippy strict (-D warnings) + fmt clean

Manual smoke (PG 17.9): /top with a victim pg_sleep(60), pressed k,
confirmed y, pg_cancel_backend returned t, the row vanished on the next
tick, and the OK badge appeared in the footer for ~5 s.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@NikolayS
Copy link
Copy Markdown
Owner Author

NikolayS commented May 8, 2026

Round 6 — k/K cancel/terminate, ←→ sort, autovacuum/slots, parallel batch

Pushed 669d86a.

/top demo

Request Resolution
Left/Right arrows change sort column Added alongside the existing < / >; r reverses direction (traditional top uses R; we accept lower-case r).
pg_cancel_backend / pg_terminate_backend with confirmation k → cancel, K → terminate. Footer flips to a red-badged confirm: CANCEL pid 12345 (user@db, active 42s, ‹update accounts set …›)? [y/N]. Captures the selected row at keystroke time so a re-sort between the key and the y doesn't change the target. y/Y fires; n/N/Esc/anything else cancels. After firing, an OK / ERR badge in the footer reports the outcome for ~5 s. Pid ≤ 0 (background workers) is a no-op — the prompt won't even open.
Briefly represent autovacuum / slots / chkpt / bgwriter Header gained a third line: longest-tx · longest-q · deadlocks · temp-files · av busy/max · slots Aphys/Tphys p · Alog/Tlog l. autovacuum reads pg_stat_activity.backend_type = 'autovacuum worker' against current_setting('autovacuum_max_workers'); slot counts come from pg_replication_slots partitioned by slot_type. Checkpointer / bgwriter rates need delta-against-previous-tick state; tracked for a follow-up since they're only meaningful as rates.
Interactive /top + parallel batch logging to file Already supported — open two rpg sessions: one runs /top interactively, the other runs rpg ... --command "/top --batch --refresh 5 --ts-format '[%F %T UTC]'" > /tmp/top.log. Both connect to the same PG; tail -f /tmp/top.log in a third tmux pane mirrors what you see live. Documented in docs/COMMANDS.md.

Footer keymap (default)

 q quit  ↑↓ move  Space refresh  ←→ sort  r reverse  k/K cancel/term  e extended  s set delay

Vim-k removed

Vim users would naturally hit k to scroll up. With k now bound to pg_cancel_backend, that reflex would silently start cancelling backends. So I removed the vim-j/k cursor aliases and a regression test catches anyone re-adding them. ↑/↓ (arrows) and PgUp/PgDn are the canonical cursor keys now.

Test counts

Before After
Unit tests in top:: 83 93
Total unit tests 2151 2161
Integration tests (--features integration) 3 3

clippy -D warnings clean, fmt clean. CI re-running on 669d86a.

Nik Samokhvalov and others added 4 commits May 8, 2026 14:02
User feedback round on PR #837 + round-6 CI lint failure.

CI lint fix
  - clippy 1.95 fired `doc_list_overindented` on the module docstring
    in src/top/state.rs. The bullet list of S1 keybindings used a
    2-space indent (`//!   - foo`) — clippy now wants flush-left or
    1-space. Reformatted as plain markdown bullets and tightened the
    list at the same time.

"Activity" no longer rendered twice
  - tabs strip already prints "Activity"; the body title bar was
    printing it again. Body title is now caption-only:
    `(N rows)  [extended]`. The view name lives only in the tabs strip.

Key-press overlay → opt-in (recording aid only)
  - new `--show-keys` flag on TopArgs (default off). Sets
    `App.show_keys`; only when on does `handle_key` seed `last_key`,
    so an interactive operator never sees the overlay flashing in
    their face. The demo tape passes `--show-keys` so the gif
    explains itself.
  - regression test: `key_overlay_off_by_default` asserts that
    pressing a key without `show_keys = true` leaves the overlay
    empty (state.rs and renderer.rs have one each).

Overlay relocated and styled per spec
  - moved from upper-right to **lower-left** of the body area
  - now a 3-row solid-yellow billboard, bold-black foreground, with
    extra padding around the label so it reads as visually loud:
        ┌──────────────┐
        │              │  <- yellow bg
        │   ⌨   PgDn   │  <- yellow bg, bold black
        │              │  <- yellow bg
        └──────────────┘
  - earlier draft used `▄` / `▀` half-blocks for an even bigger
    silhouette but some gif renderers dropped those glyphs; plain
    spaces with yellow bg survive cleanly through optimisation
  - tested at 96-colour gifsicle: overlay survives quantisation

Other
  - 95 unit tests in `top::` (was 93)
  - clippy strict + fmt clean

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…, modifier prefixes

User feedback round on PR #837:

Global `--show-keys` rpg flag
  - new top-level CLI flag (`rpg --show-keys ...`) and matching
    `ReplSettings.show_keys` field
  - intent: turn on the on-screen key-press badge for *any* TUI rpg
    command from a single switch, so a recording rig can capture demos
    of the bare REPL, /top, /ash, /rpg, etc. with a single env knob
  - currently honored by /top; /ash and the bare REPL prompt overlay
    land in follow-up PRs (renderer surfaces are different)
  - the per-/top `--show-keys` arg still works; the global flag wins

Overlay relocated and bigger
  - moved from lower-left to lower-right corner (user request)
  - label glyphs are upscaled to 2x cell width using Unicode "fullwidth"
    forms (`D` → `D`, `o` → `o`, etc.); arrow keys swap to chunkier
    triangular variants (`↑↓←→` → `▲▼◀▶`). This is the only way to
    visually double a glyph inside a fixed-cell terminal — the cell
    grid itself can't change mid-frame.
  - the leading "⌨" decoration is gone (user explicitly removed it)
  - bg colour is now a muted olive-yellow `#A08018` instead of
    full-saturation `Color::Yellow`; reads as ~50 % alpha over a dark
    terminal without erasing the underlying body content. Real alpha
    isn't a thing in the VT100 cell model.

Modifier handling
  - `Ctrl-X` → `C-X`, `Alt-X` → `M-X`, `Super-X` → `S-X`, stack as
    `C-M-X`, `C-S-X`, etc. (`top`-style prefix convention)
  - Shift on a printable key arrives as the shifted char itself
    (`Char('K')` for Shift-K), so we don't add a redundant "⇧". The
    canonical Shift-Tab keyword `BackTab` still gets an explicit
    `⇧Tab` label.
  - macOS Terminal.app strips Alt/Super on the way through; that's a
    terminal limitation, not ours.

Tests
  - 96 unit tests in top:: (was 95): adds `modifier_keys_get_compact_prefixes`
    covering Ctrl, Alt, stacked C-M-, Shift-on-printable, BackTab
  - overlay-presence tests rewritten to check the bg color of any cell
    in the buffer (now that the "⌨" sentinel is gone, the safest signal
    is the muted-yellow bg itself)
  - full clippy strict + fmt clean

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
User screenshot feedback on round-8: the muted olive bg looked dirty,
the right edge of the box was notched (fullwidth glyph render mismatch),
the fullwidth letters looked squashed, and the whole thing was too small.

  - bg switched from `Rgb(0xA0, 0x80, 0x18)` (muted olive) back to the
    bright named `Color::Yellow`. Terminals map the named color to
    their theme's clean yellow rather than a fixed RGB tone.
  - dropped Unicode fullwidth conversion (`Pgdn` → `PgDn`). Some
    cell-width estimators disagreed with the actual rendered width and
    that produced the notched right edge / squashed-character feel.
    Plain ASCII labels are aligned and clean.
  - box height bumped from 3 to 5 rows, with a 14-cell minimum width
    and 10 cells of horizontal padding around the label. The badge
    now reads as a proper "key pressed" callout instead of a strip.
  - kept the chunky-arrow swap (`↑↓←→` → `▲▼◀▶`) so single-glyph
    overlays still look chunky.

Tests
  - 96 unit tests in top:: (no count change). The bg-color sentinel
    used by `buffer_has_overlay_bg` updated from the muted RGB to
    `Color::Yellow`.
  - clippy strict + fmt clean.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
clippy 1.95 fires `unnecessary_trailing_comma` on a single-line
`format!` macro call. CI lint failed on round-9; local clippy 1.94
didn't catch it.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@NikolayS
Copy link
Copy Markdown
Owner Author

NikolayS commented May 8, 2026

Live demo gif (rolling — edited in place)

PR head: 3182ebc. Round-12b: smoother gif. The previous 2.9 MiB gifsicle output was dropping frames during the workload-heavy section (you nailed it — vhs's native gif quantizer over-merges identical frames and the busy renderer with churning psql backends triggers exactly that path). New pipeline runs vhs's mp4 output through ffmpeg's palette generator + bayer dither at 15 fps; size is 11 MiB but playback is smooth. Wrapped in demos/render-top.sh so future re-renders are reproducible. mp4 intermediate gitignored.

/top demo

Round-12 (commit 63211d7) lands the REV blockers: kill confirm targets the sorted-slice row (was reading the unsorted SQL slice → could kill a different pid than highlighted), demos/top-workload.sh rewrapped through a run_psql helper that applies --no-psqlrc / PAGER=cat / long options per CLAUDE.md, and docs/COMMANDS.md now lists --show-keys plus PgUp/PgDn/Home/End. Two security POTENTIAL findings folded in: kill confirm and admin-message footer now run user-controllable strings through scrub_terminal_unsafe.

Figlet font from round-11 unchanged: src/top/keyfont.rs 3×5 grid; render_label walks each char of multi-char labels through the same glyph table so PGDN / ESC render at the same scale as / K.

Box style: bright Color::Yellow bg, bold black fg, lower-right corner. Opt-in via rpg --show-keys (global) or /top --show-keys (per-command).

Round summary

Round Key change
6 doc-list lint fix, Activity no longer rendered twice
7 dedup body title, --show-keys opt-in, lower-right billboard
8 global rpg --show-keys flag, modifier prefixes (C-/M-/S-)
9 bright Color::Yellow, plain ASCII, 5-row box
10 hand-built 5×5 block-letter font for single-char labels
10b tightened box around the 5×5 glyph
11 3×5 figlet font + render_label for multi-char labels
12 REV round-11 blockers: kill targets sorted row, shell-rule, doc gap, scrub kill confirm + admin message
12b smoother gif via ffmpeg palette pipeline (this push)

(I'm editing this comment in place when the gif updates instead of stacking new comments.)

Nik Samokhvalov and others added 3 commits May 8, 2026 15:48
User feedback: the character pressed should be 5x bigger; box stays the
same. Implemented via a hand-built 5×5 block-letter font living in
src/top/keyfont.rs. Single-character labels (`e`, `K`, `r`, `↓`, `<`, …)
now render as chunky 5-row figlet glyphs filling the box; multi-letter
labels (`PgDn`, `Esc`, `Home`, …) keep the centred plain-text fallback.

  - new module `src/top/keyfont.rs` with a `glyph(char) -> Option<[&str; 5]>`
    lookup. Covers A–Z (case-insensitive), 0–9, common punctuation
    (`< > / - + = ? ! :`, space), and the four cursor arrows (`↑↓←→`,
    `▲▼◀▶`). Unknown chars return `None` so the renderer can fall
    back gracefully.
  - render_key_overlay branches: known single char → 5×5 glyph filling
    all 5 rows of the yellow box with 3 cells of horizontal padding;
    everything else → existing plain-text path.
  - tests in keyfont.rs assert every glyph is exactly 5×5, lowercase
    maps to upper, arrow synonyms (`↑` ≡ `▲`) tie out, and unknown
    chars yield `None`. 100 unit tests in top:: total (was 96).
  - clippy strict + fmt clean.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
User feedback on round-10: "omg too big" — the 5×5 figlet glyph was
right but the surrounding box was over-padded (3 cells of yellow on
each side). Cut the side padding from 3 to 1 so the box hugs the
glyph: 7 cells wide × 5 rows tall, ~64 % the area of the previous
visual.

(On the gif's clock skipping seconds: vhs simulates time at its render
speed, while rpg renders the real wall clock from `pg_postmaster_start_time`
deltas, so the two diverge whenever vhs slows down on a heavy frame.
Pure render artefact, no rpg-side fix.)

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
The previous 5×5 block-letter font dwarfed the rest of the TUI and made
multi-char labels (PgDn, Esc, Home) inconsistent with single-char ones —
they fell back to plain bold text and looked tiny next to the giant
arrows and letters.

Switch to a compact 3×5 cell glyph grid and a `render_label` helper that
walks every char of a label through the same glyph table, so PGDN, ESC,
HOME render at the exact same scale as ▲, K, /. Box height drops to
3 rows, side padding tightens. Visual result: still readable while
recording, no longer eats half the screen.

Some glyphs collapse to identical 3×5 forms (I/Z, V/Y); the lookup is
annotated `#[allow(clippy::match_same_arms)]` so future tweaks can keep
them as separate, named arms without contorting the shapes.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@NikolayS
Copy link
Copy Markdown
Owner Author

NikolayS commented May 9, 2026

REV-assisted review (round 11) — 50baec6

CI: ✅ all green (27 checks). Reviewing the delta f0b1327..50baec6 (2862/-306 across 16 files); the earlier portion of this PR was reviewed and cleared in a previous REV pass.

Verdict

Status: BLOCKED — 3 BLOCKING findings.

Agent Status Findings
Security PASS 2 POTENTIAL
Bugs BLOCKED 1 BLOCKING + 4 POTENTIAL/INFO
Tests PASS 4 NON-BLOCKING/INFO
Guidelines BLOCKED 1 BLOCKING + 3 POTENTIAL/INFO
Docs BLOCKED 1 BLOCKING + 5 POTENTIAL/INFO

BLOCKING

[Bugs] src/top/state.rs:555request_kill targets a different pid than the highlighted row.
The renderer sorts a fresh slice via views/activity.rs:99-100's sort_rows, but request_kill reads snap.rows.get(self.selected_row). The TableState.with_selected(...) highlight indexes the sorted slice. After any non-default sort (and even on the default, since the SQL ORDER BY differs from the Rust qtime-desc tiebreak), pressing k/K builds the [y/N] confirm and ultimately kills a backend the user did not intend.

Fix: sort once in App::set_snapshot (or have request_kill re-run sort_rows) and pick sorted[selected_row]. Add a regression test with ≥2 rows where the SQL order and Rust sort disagree.

[Guidelines] demos/top-workload.sh — every psql invocation violates the PostgreSQL command execution rule.
Lines 32, 37, 48, 56, 59, 64, 68, 72, 77 all use psql -c "…" without --no-psqlrc and PAGER=cat, and use the short -c form rather than long-option-per-line. CLAUDE.md's "PostgreSQL command execution" rule is unconditional.

Fix: prefix each call with PAGER=cat psql --no-psqlrc --dbname="${CONNINFO}" --command="…".

[Docs] docs/COMMANDS.md:49/top row omits --show-keys.
The flag is implemented (src/top/mod.rs:130) and exposed globally (src/main.rs:351); the demo tape relies on it. Missing entirely from the public reference.

Fix: append `--show-keys` (overlay each keypress as a billboard demo aid) to the flags list.

POTENTIAL (recommended, not blocking)

  • [Security] renderer.rs:455-458build_kill_confirm_line interpolates raw req.usename / req.datname / req.state / req.query_summary without scrub_terminal_unsafe. A DB user controlling application_name could inject ANSI/BEL into the operator's terminal when the operator presses k/K on that backend. Activity-table fields are correctly scrubbed via truncate/squash_query but the kill-prompt path is not.
  • [Security] renderer.rs:486build_admin_message_line renders msg.text directly. In the kill error branch the text is format!("{e}") from tokio_postgres::Error, which can carry server-controlled bytes.
  • [Bugs] state.rs:541-548handle_kill_confirm_key cancels on _. Bare modifier-only key events (Kitty CSI-u, Windows release events) would silently dismiss the confirm.
  • [Bugs] mod.rs:300-308run_kill ignores the result of set statement_timeout = 0. If the reset fails the 5 s timeout leaks to subsequent sampler ticks.
  • [Bugs] sql.rs:36total_backends excludes pg_backend_pid(), so the header is off by one vs pg_stat_activity.
  • [Guidelines] 12 of 15 commit subjects in this delta exceed 50 chars. Resolved by squash-merging into a single PR-scoped commit.
  • [Docs] docs/COMMANDS.md:49 — Keybinding list is missing PgUp/PgDn and Home/End (both wired in S1).
  • [Docs] .samo/spec/top/SPEC.md — Status still "ready for S1"; doesn't reflect what shipped (--batch, s/r/e, kill confirm).
  • [Docs] .samo/spec/top/decisions.md — No "S1 shipped" entry recording deviations.

NON-BLOCKING / INFO

  • [Tests] No smoke test for --batch mode end-to-end; format_refresh_seed only covered transitively; render_label("C-K") mixed-glyph case missing.
  • [Bugs] KeyModifiers::SUPER → S- collides with the Shift convention.
  • [Bugs] renderer.rs:373 — unchecked inner_w + 2 arithmetic; consider saturating_add.
  • [Guidelines] demos/top-workload.sh lacks main() + main "$@" last line.
  • [Security] sql.rs SQL is fully static; run_kill binds pid as $1; figlet glyph table consumes only local KeyEvents — all clean.

Next step: fix the 3 BLOCKING items (kill targets the right row, shell rule, doc gap) plus the 2 security POTENTIAL scrubs (cheap, prevents a real terminal-injection class), push, re-run CI + REV.

Nik Samokhvalov and others added 2 commits May 8, 2026 17:28
Three blockers:

1. `request_kill` (state.rs) read `snap.rows[selected_row]`, but the
   renderer sorts a fresh slice via `views::activity::sort_rows` and
   the cursor indexes that sorted slice. Result: pressing `k`/`K`
   after any non-default sort confirms — and ultimately kills — a
   different backend than the one highlighted. Fixed by sorting in
   `request_kill` with the same column/direction the renderer uses,
   then picking `sorted[selected_row]`. Regression test pins it
   (Pid-asc with two rows whose SQL order disagrees).

2. `demos/top-workload.sh` violated CLAUDE.md "PostgreSQL command
   execution" — `psql -c "…"` everywhere with no `--no-psqlrc` and
   no `PAGER=cat`. Rewrapped via `run_psql` / `run_psql_stdin` helpers
   that apply the project conventions; restructured into a `main`
   block so `main "$@"` is the last line per shell-style-guide.
   `env PAGER=…` form to keep shellcheck quiet.

3. `docs/COMMANDS.md` `/top` row omitted `--show-keys` and the
   PgUp/PgDn/Home/End keybindings. Added.

Plus two POTENTIAL security findings folded in while we're touching
the renderer:

- `build_kill_confirm_line` interpolated raw `usename` / `datname` /
  `state` / `query_summary`; a DB user controlling `application_name`
  could inject ANSI/BEL bytes into the operator's terminal at the
  moment they press `k`. Now scrubbed via `scrub_terminal_unsafe`.
- `build_admin_message_line` rendered `msg.text` directly; the kill
  error branch builds it from `format!("{e}")` against a
  `tokio_postgres::Error` which can carry server-controlled bytes.
  Now scrubbed.

New tests cover both kill-confirm and admin-message scrubbing.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
vhs's native gif quantizer drops frames during the busy workload
section of the demo, which the user (correctly) noticed as laggy.
The mp4 it produces in parallel is fine, so re-encode through
ffmpeg's palette pipeline (15 fps, 1200 px wide, bayer dither) for
smooth playback. Cost: 3 MiB → 11 MiB. Worth it for a feature gif
where the smoothness *is* the demo.

`demos/render-top.sh` wraps the two-step process so future re-renders
are reproducible. The tape now also writes `top-demo.mp4` (gitignored)
as an intermediate artifact.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
REV r12 audit caught it: macOS BSD `mktemp -t prefix` does not treat
`XXXXXX` as a template — it appends a random suffix to the whole
string, so the requested `top-demo-palette.XXXXXX.png` lands at
`…palette.XXXXXX.png.RANDOM`. The trailing extension is the random
hash, not `.png`, so ffmpeg's image2 muxer fails to infer the codec
and aborts with `Invalid argument` before writing the palette. The
gif we shipped in 3182ebc was produced by an ad-hoc invocation
against `/tmp/palette.png`, not by running the script — so the
script as committed would have failed for anyone else regenerating
the demo.

Switch to an explicit `${TMPDIR:-/tmp}/top-demo-palette-$$.png` path
that keeps the `.png` extension intact and is still cleaned up by
the existing EXIT trap.

While in here, fix the `demos/README.md` "render all at once" loop
to skip `top-demo.tape` and call `render-top.sh` separately —
running plain `vhs` on it produced exactly the laggy gif this whole
chain was written to avoid.

Verified: `bash demos/render-top.sh` now runs end-to-end and writes
a fresh ~11 MiB gif.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@NikolayS
Copy link
Copy Markdown
Owner Author

NikolayS commented May 9, 2026

REV-assisted review (round 12 follow-up) — 5db967b

CI on the round-12 commits: 19/21 green at audit time, with Analyze (rust) and PostgreSQL Regress Tests still running but already passing on the same Rust changes in the previous round.

Round-11 BLOCKING resolution

Round-11 finding Status Evidence
request_kill targets unsorted slice → wrong-pid kill RESOLVED state.rs:560-562 re-sorts via views::activity::sort_rows(&mut sorted, sort_column, sort_desc) and indexes the sorted slice; regression test k_targets_the_sorted_row_not_the_sql_row (state.rs:1172) RED→GREEN with two rows whose SQL order disagrees with Pid-asc
demos/top-workload.sh violates psql shell rule RESOLVED every call now goes through run_psql / run_psql_stdin helpers that apply env PAGER=cat psql --no-psqlrc --dbname=… --command=…; main() block; main "$@" last line; shellcheck clean
docs/COMMANDS.md /top row missing --show-keys RESOLVED --show-keys now documented; PgUp/PgDn and Home/End keybindings listed

Security POTENTIAL resolution

Finding Status Evidence
build_kill_confirm_line un-scrubbed row fields RESOLVED renderer.rs:464-467 wraps each field in scrub_terminal_unsafe; new test kill_confirm_strips_ansi_escapes_from_row_fields (renderer.rs:1007) seeds \x1b[31m / \x1b[2J / \x07 and asserts no control byte reaches the rendered buffer
build_admin_message_line un-scrubbed msg.text RESOLVED renderer.rs:499 scrubs the text; admin_message_strips_ansi_escapes test (renderer.rs:1047) seeds an ANSI/BEL string into the kill-error footer and asserts byte-clean output

New findings this round

  • [POTENTIAL] demos/render-top.sh mktemp -t …palette.XXXXXX.png is broken on macOS BSDfixed in 5db967b: switched to ${TMPDIR:-/tmp}/top-demo-palette-$$.png; verified end-to-end run produces an 11 MiB gif. The audit caught a real bug — ffmpeg refused to write the palette because the extension was the random hash, not .png. The committed gif from 3182ebc was produced by an ad-hoc ffmpeg invocation against /tmp/palette.png, not the script, so the script-as-committed would have failed for anyone else.
  • [INFO] demos/README.md "render all at once" loop runs plain vhs on top-demo.tape, skipping the post-process and producing the laggy gif this chain was written to avoidfixed in 5db967b: loop now skips top-demo.tape and the README points at bash demos/render-top.sh for the post-processed render.
  • [INFO] demos/top-workload.sh:48-65 — three persistent-backend ( … ) & blocks fire at top-level before main() is invoked. Letter of the shell-style rule met (main() exists, main "$@" is the last line); spirit (whole procedural body inside main) drifts a little. Cleanup is trapped on EXIT so behaviour is correct. Cosmetic.
  • [INFO] demos/top-demo.gif — file size grew 2.9 MiB → 11 MiB. Documented in demos/README.md as the deliberate cost of the smoother ffmpeg pipeline.

Verdict

Status: PASS — all round-11 BLOCKING and POTENTIAL security findings resolved with regression tests; the only sharp follow-up the audit raised (BSD mktemp) is fixed in 5db967b. Remaining INFO items are cosmetic.

Per project memory rule, not merging without explicit approval.

Nik Samokhvalov and others added 2 commits May 8, 2026 19:15
Remove private format_secs_or_dash (identical logic to activity::format_secs)
and truncate_err (identical to activity::truncate) — promote both to
pub(in crate::top) and import them in renderer.rs.

Drop unused _app parameter from build_counts_line.

Hoist Terminal<TestBackend> construction out of the run_batch loop so
the same backend is reused across ticks rather than reallocated each time.

New tests:
- k_targets_the_sorted_row_not_the_sql_row (regression for request_kill bug)
- k_is_a_no_op_for_negative_pid_rows
- k_targets_sorted_row_under_qtime_sort
- page_down_saturates_at_last_row
- sort_right_edge_wraps_from_query_to_pid
- set_snapshot_clears_last_error
- expired_admin_message_falls_back_to_default_footer

Fix lock-count fixture values (47/83) to avoid collision with pid digits.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
\x1b[2J was sent to stdout after LeaveAlternateScreen, which wiped the
user's main terminal content (previous query results, shell prompt, etc.).
Alt-screen restoration already preserves the original terminal state;
the explicit erase was both wrong and redundant. Remove it.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
@NikolayS
Copy link
Copy Markdown
Owner Author

NikolayS commented May 9, 2026

REV-assisted review — round 12 (commit 0d57770 + c6c5db9)

CI Status: ✅ pass (all core checks: lint, test, clippy, MSRV, PG14–18, connection, regress, wasm, CodeQL)


BLOCKING issues fixed in this review cycle

FIXED · TerminalGuard::drop cleared the main terminal screen after exiting alt-screen

\x1b[H\x1b[2J\x1b[H was sent to stdout after LeaveAlternateScreen. At that point the user is back on their main terminal, so this wiped whatever was on screen before they ran /top (previous query results, shell prompt, etc.). Alt-screen restoration already restores the original content — the explicit erase was wrong and redundant. Fixed in c6c5db9.


NON-BLOCKING

Security · N-1: format! builds set statement_timeout SQL in sampler.rs

apply_timeout uses format!("set statement_timeout = '{timeout_ms}ms'") where timeout_ms is a compile-time u64 constant — no injection path exists today. Minor style inconsistency with the literal-string pattern used elsewhere; could harden by inlining the literal.

Security · N-2: format! builds pg_cancel_backend/pg_terminate_backend function name in run_kill

format!("select {}($1)", req.pg_function_for_request()) — function name is one of two hard-coded &'static str values, PID passed via $1. Safe as written; a match building the full literal would be cleaner.

Bugs · N1: exec_command always returns exit code 0 for /-extension commands including errors

Scripts relying on $? to detect /top --once failure (e.g. DB unreachable) will see 0. Already self-documented in code with a // KNOWN LIMITATION comment. Deferred to S2.

Bugs · N2: run_batch allocates App::new() inside the loop — stale-tick counter never accumulates

The stale-tick badge that shows in interactive mode when the DB is slow will never appear in --batch output. App should be created once outside the loop (same fix already applied to Terminal<TestBackend>).

Tests · 1: scrub_terminal_unsafe drops valid U+FFFD replacement characters

The two-pass approach (map control bytes to \u{FFFD}, then filter) also drops any genuine U+FFFD in user data (unusual but possible with some client encodings). Low probability; could use an internal sentinel instead.

Tests · 2: stable-sort property not exercised

Code comments "stable sort preferred so equal-key rows preserve SQL-side ordering" — no test places two rows with equal keys and asserts insertion-order preservation. A regression to sort_unstable_by would pass silently.

Tests · 3: top_once_shows_active_backend_count integration test has a loose assertion

stdout.contains("rows)") matches any row-count caption including (0 rows). Should assert on a specific non-zero count to verify a live backend is actually visible.

Guidelines · NB-1: demos/top-workload.sh line 81 chars (1 char over 80-char limit)

readonly CONNINFO="${1:-host=localhost port=55433 user=postgres dbname=postgres}" — trivial split.

Guidelines · NB-2: demos/top-demo.tape hardcodes /Users/nik/github/rpg/…

Other contributors running bash demos/render-top.sh will get path errors. demos/README.md should note these need adjustment (it already mentions the PG* env block).

Docs · 1: COMMANDS.md /top row is a single 838-char wall of text

All other entries use a one-liner. The flags and keymaps detail belongs in the planned docs/top.md (S8). Consider a brief description + "see docs/top.md" placeholder.

Docs · 2: GLYPH_W doc-comment says "public API for future callers" but carries #[allow(dead_code)]

These two signals contradict each other. Either drop the allow or update the comment.


INFO

  • No unsafe Rust introduced. All Postgres wire data flows through tokio-postgres typed column API.
  • All user-controllable fields (application_name, query, usename, datname, state, pg_version, error text) pass through scrub_terminal_unsafe before reaching ratatui. Kill-confirm and admin-message paths also scrub correctly.
  • Test naming is precise and behavioural throughout. Fixtures are representative (unambiguous lock counts 47/83, multiple backend states).
  • SET statement_timeout (bare, not SET LOCAL) is correct here — SET LOCAL outside an explicit transaction block is a no-op in PostgreSQL, so the explicit reset pattern is the right approach.
  • .samo/spec/top/SPEC.md v0.2 is an unusually thorough spec; makes future sprint work straightforward.

Verdict: PASS — one BLOCKING bug fixed (TerminalGuard screen-wipe); remaining findings are NON-BLOCKING or INFO. Ready for merge at owner's discretion.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant