From 7efb5d07a0ff6eb20b1b17a6eba8d486777c6063 Mon Sep 17 00:00:00 2001 From: Justin Ramos Date: Thu, 23 Apr 2026 20:44:20 -0700 Subject: [PATCH 1/4] docs(agents): document verbose flag, on_wire callback, logging.md Add a "Logging posture" paragraph to the stack overview covering the silent-by-design library, the CLI -v/--verbose flag, and the on_wire: callback kwarg on Miner.new / MinerPool.new with its three directions (:request, :response, :response_repaired). Add a structural fact noting on_wire is best-effort telemetry wrapped in safe_on_wire so a buggy callback cannot break a query. Add -v usage to the manual sandbox block and a docs/logging.md row to the deep-context table. Refresh the SLOC footprint to match post-verbose-flag reality. --- AGENTS.md | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/AGENTS.md b/AGENTS.md index 1699439..4afd18a 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -25,7 +25,9 @@ A pure-Ruby client for the [cgminer](https://github.com/ckolivas/cgminer) JSON-o **Stack:** Ruby 3.2+ only. Zero runtime dependencies beyond Ruby stdlib (`json`, `socket`, `yaml`, `pp`). Dev deps are `rspec`, `rubocop` (+ `-rake` and `-rspec`), `rake`, `simplecov`. -**Footprint:** ~650 SLOC in `lib/`, ~1500 SLOC in `spec/`. Small, deliberately simple. +**Footprint:** ~510 SLOC in `lib/`, ~3000 SLOC in `spec/`. Small, deliberately simple. + +**Logging posture:** silent-by-design — the library has no `Logger` module and emits no structured events. Callers (monitor, manager, CLI) own log call sites. For telemetry during debugging there's a CLI `-v`/`--verbose` flag (prints requests + responses to stderr, operator-facing, not a parse-able contract) and a library-level `on_wire:` callback kwarg on both `Miner.new` and `MinerPool.new` (a `proc.(direction, host, port, payload)` that fires on each request / response / response-repaired event). See [`docs/logging.md`](docs/logging.md) for the full story and for how sibling gems log api_client outcomes. ## Repo layout @@ -84,6 +86,7 @@ Lib ─┼──> MinerPool ──(parallel threads)──> Miner ──> socket 3. **`MinerPool#query` always returns a `PoolResult`** — an Enumerable of per-miner `MinerResult` instances in pool order. Successes and failures are both captured structurally; `MinerPool#query` never raises on a single-miner failure. 4. **`Miner#query` raises on failure:** `ConnectionError` for transport-level problems, `ApiError` for cgminer `STATUS=E`/`F` responses. `TimeoutError < ConnectionError` for connect timeouts specifically. 5. **Library code never writes to stderr.** The CLI owns that channel. `Miner#check_status` does `puts` on cgminer `STATUS=I`/`W` to stdout, which is by design (cgminer advisory messages). +6. **`on_wire:` is best-effort telemetry, not a hard contract.** The callback is invoked from `safe_on_wire` which swallows any exception raised by the block — a buggy logger must not break the miner query. Three directions: `:request` (outbound JSON), `:response` (raw inbound string, pre-parse), `:response_repaired` (the rare `}{` legacy-repair path). The CLI's `-v` flag is implemented by installing a default `on_wire` that writes formatted lines to stderr. ## Conventions that matter when editing code @@ -165,6 +168,7 @@ bundle exec rubocop -A # lint + auto-correct (review diffs!) # terminal 2 cp config/miners.yml.example config/miners.yml bundle exec bin/cgminer_api_client summary +bundle exec bin/cgminer_api_client -v summary # verbose: echo request + response to stderr ``` ## Adding a new cgminer command wrapper @@ -262,6 +266,7 @@ The `.gem` artifact shouldn't be committed to the repo, but one is present at th | What's in a `MinerResult` / `PoolResult`? What errors can be raised? | [`docs/data_models.md`](docs/data_models.md) | | How does a request flow through the code? Parallel fan-out? CLI? | [`docs/workflows.md`](docs/workflows.md) | | Runtime deps? Why Ruby 3.2+? | [`docs/dependencies.md`](docs/dependencies.md) | +| Logging posture: silent-by-design, CLI `-v`/`--verbose`, library `on_wire:` callback, how callers log outcomes | [`docs/logging.md`](docs/logging.md) | | Known doc/code drift, caveats, things I'm not sure about | [`docs/review_notes.md`](docs/review_notes.md) | | Full knowledge-base index | [`docs/index.md`](docs/index.md) | | User-facing docs | [`README.md`](README.md) | From db1566f95e030cf66fc60ec9c3610cf9ba9d5532 Mon Sep 17 00:00:00 2001 From: Justin Ramos Date: Thu, 23 Apr 2026 20:45:41 -0700 Subject: [PATCH 2/4] docs(interfaces,logging): document -v/--verbose and on_wire: hook MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Add on_wire: keyword-arg usage to Miner.new and MinerPool.new signatures in interfaces.md; document the CLI -v/--verbose flag with its actual direction prefixes (>>>, <<<, <<< (repaired)) and mutex-serialized write behavior; cross-link to docs/logging.md for the full posture. Fix error class references in logging.md — the stub's initial wording named SocketReadTimeout / ProtocolError, which are not real classes. Replace with the actual public hierarchy (Error, ConnectionError, TimeoutError, ApiError) and describe the pool's failure-capture contract. Add sections documenting the on_wire: library hook (three directions, safe_on_wire swallowing) and the CLI flag (defensive EPIPE/IOError handling so stderr closure doesn't break the query). --- docs/interfaces.md | 22 +++++++++++++++++++++- docs/logging.md | 15 +++++++++++++-- 2 files changed, 34 insertions(+), 3 deletions(-) diff --git a/docs/interfaces.md b/docs/interfaces.md index aa3c30a..a7a09ab 100644 --- a/docs/interfaces.md +++ b/docs/interfaces.md @@ -33,12 +33,17 @@ miner = CgminerApiClient::Miner.new('10.0.0.5') miner = CgminerApiClient::Miner.new('10.0.0.5', 4028) miner = CgminerApiClient::Miner.new('10.0.0.5', 4028, 3) +# Optional on_wire callback for telemetry (see Logging section below) +miner = CgminerApiClient::Miner.new('10.0.0.5', on_wire: ->(dir, host, port, payload) { + warn "#{dir} #{host}:#{port} #{payload}" +}) + miner.host # "10.0.0.5" miner.port # 4028 miner.timeout # 3 ``` -Any of the three constructor args can be omitted/nil; the corresponding `CgminerApiClient.default_*` value is used. +Any of the three positional args can be omitted/nil; the corresponding `CgminerApiClient.default_*` value is used. `on_wire:` is optional; when omitted, no wire telemetry is emitted. **Commands (all return the parsed, sanitized response on success; raise `ConnectionError` on transport failure, `ApiError` on cgminer rejection):** @@ -112,10 +117,13 @@ miner.respond_to?(:_dump) # false (_* is carved out) ```ruby pool = CgminerApiClient::MinerPool.new # reads ./config/miners.yml +pool = CgminerApiClient::MinerPool.new(on_wire: my_callback) # telemetry hook pool.miners # Array in config order pool.reload_miners! # re-read config/miners.yml ``` +`MinerPool.new` accepts a single `on_wire:` keyword arg; the same callback is forwarded into each `Miner` it constructs, so every per-miner request/response fires through one hook. See the Logging section below. + **Commands (same surface as `Miner`; all return a `PoolResult` of `MinerResult`s):** ```ruby @@ -206,6 +214,14 @@ Invocation happens from a directory containing `config/miners.yml`. The CLI read Library code never writes to stderr. The CLI owns that channel. Existing shell pipelines capturing stdout get clean output even when some miners fail. +### Flags + +| Flag | Purpose | +|---|---| +| `-v` / `--verbose` | Print each outbound request (JSON) and inbound response (raw string, pre-parse) to stderr prefixed with direction (`>>>`, `<<<`, or `<<< (repaired)` for the legacy `}{`-repair path) and `host:port`. Operator-facing diagnostic output, not a parse-able contract. Writes are mutex-serialized so interleaved per-miner lines don't tear. | + +Place the flag before the command: `cgminer_api_client -v summary`. + ### Environment variables - `DEBUG=1` — on a top-level (unhandled) exception, also print the full backtrace via `Exception#full_message(highlight: false)` to stderr. @@ -235,6 +251,10 @@ $ DEBUG=1 cgminer_api_client summary # ... same output plus full backtrace on any unhandled exception ``` +### Logging and telemetry + +The CLI is the only surface that writes diagnostic lines to stderr; the library is silent by design. See [`docs/logging.md`](logging.md) for the full posture, the event names sibling gems (`cgminer_monitor`, `cgminer_manager`) use when they log api_client outcomes, and how the `on_wire:` library hook compares to the CLI's `-v` flag (same mechanism — the CLI installs a default `on_wire` that writes to stderr). + ## 3. `config/miners.yml` schema Path: `./config/miners.yml` (relative to process CWD). diff --git a/docs/logging.md b/docs/logging.md index 4f76f5b..5f15741 100644 --- a/docs/logging.md +++ b/docs/logging.md @@ -1,10 +1,21 @@ # Logging -`cgminer_api_client` is **silent by design.** It has no `Logger` module and emits no structured log events. On failure it raises (`CgminerApiClient::ConnectionError`, `CgminerApiClient::SocketReadTimeout`, `CgminerApiClient::ProtocolError`, etc.); on success it returns `CgminerApiClient::MinerResult` or `CgminerApiClient::PoolResult` instances. +`cgminer_api_client` is **silent by design.** It has no `Logger` module and emits no structured log events. On failure `Miner#query` raises from the public hierarchy in `lib/cgminer_api_client/errors.rb` — `CgminerApiClient::Error`, `ConnectionError` (transport-layer), `TimeoutError < ConnectionError` (connect timeout specifically), `ApiError` (cgminer `STATUS=E`/`F` response). `MinerPool#query` captures these into `MinerResult.failure` rather than raising, so a pool call always returns a `PoolResult` even when every miner fails. Callers — `cgminer_monitor`'s poll loop, `cgminer_manager`'s fleet commander, the `bin/cgminer_api_client` CLI — own the log call sites and decide what's worth emitting. This keeps the library dependency-free of any logging backend and lets each consumer pick its own verbosity / destination / format. -The CLI has a `-v`/`--verbose` flag that prints each request and response to stderr with a direction prefix + `host:port`. This is operator-facing diagnostic output, not a structured-log contract — the lines are not JSON and are not intended to be parsed. Use it to debug a specific miner interactively; don't pipe it into an aggregator. +## Library-level hook: `on_wire:` + +Both `Miner.new` and `MinerPool.new` accept an optional `on_wire:` keyword arg — a `proc.(direction, host, port, payload)` invoked for each request / response / `}{`-repaired response. `MinerPool` forwards the same callback into every `Miner` it constructs so a single hook covers the whole fan-out. The callback is wrapped in `safe_on_wire` — any exception it raises is swallowed with no retry, so a buggy logger cannot break a query. + +Directions: +- `:request` — outbound JSON just before `socket.write`. +- `:response` — raw inbound string, pre-parse. +- `:response_repaired` — the rare legacy path where cgminer returned `}{`-concatenated JSON and the client split it; carries the repaired string. + +## CLI: `-v`/`--verbose` + +The CLI has a `-v`/`--verbose` flag that installs a default `on_wire` writing each direction to stderr with a direction prefix (`>>>`, `<<<`, `<<< (repaired)`) and `host:port`. Writes are mutex-serialized to keep interleaved per-miner lines intact. Handles `Errno::EPIPE` / `IOError` defensively — if stderr closes mid-run (because a downstream `| head` exited), the query fan-out keeps going. This is operator-facing diagnostic output, not a structured-log contract — the lines are not JSON and are not intended to be parsed. Use it to debug a specific miner interactively; don't pipe it into an aggregator. ## How callers log api_client outcomes From 43802f7cf8475a1c4b039346923dac616126c7f2 Mon Sep 17 00:00:00 2001 From: Justin Ramos Date: Thu, 23 Apr 2026 20:46:49 -0700 Subject: [PATCH 3/4] docs(components,workflows): document on_wire: hook + CLI -v wiring Add on_wire: kwarg to the Miner and MinerPool constructor summaries in components.md; note that MinerPool forwards the callback into every Miner it constructs. Describe safe_on_wire's three directions and exception-swallowing contract. Update the bin/cgminer_api_client entry to document the -v/--verbose flag and the mutex-serialized stderr writer with EPIPE/IOError defenses. In workflows.md, extend the single-miner request-flow sequence diagram with the three safe_on_wire call sites (request / response / response_repaired) and add a key observation pointing at logging.md. --- docs/components.md | 10 ++++++---- docs/workflows.md | 6 ++++++ 2 files changed, 12 insertions(+), 4 deletions(-) diff --git a/docs/components.md b/docs/components.md index f1bdbe7..bae6b00 100644 --- a/docs/components.md +++ b/docs/components.md @@ -81,7 +81,9 @@ Mixed into `Miner`. The switch from `IO.select(nil, [socket], nil, timeout)` to ### `CgminerApiClient::Miner` **File:** `lib/cgminer_api_client/miner.rb` -Per-host client. Carries `host`, `port`, `timeout`. Public surface: +Per-host client. Carries `host`, `port`, `timeout`. Constructor: `Miner.new(host = nil, port = nil, timeout = nil, on_wire: nil)` — positional args fall back to `CgminerApiClient.default_*`; `on_wire:` is an optional telemetry callback (see below). + +Public surface: - `query(method, *params)` — marshal request, send, parse, dispatch `check_status`, return sanitized data. Raises `ConnectionError` on transport failure, `ApiError` on cgminer-reported error. - `available?` — true reachability probe. Opens a fresh socket every call (no cache). Returns `false` on `SocketError`/`SystemCallError`/`TimeoutError`; lets other exceptions propagate. Does **not** perform an API handshake. @@ -89,7 +91,7 @@ Per-host client. Carries `host`, `port`, `timeout`. Public surface: - `method_missing` forwards unknown names to `query`. - `respond_to_missing?` says yes for any name that isn't `to_*`/`_*`. -Private methods: `perform_request` (socket write/read/JSON parse, plus control-byte re-escape and `}{` repair), `check_status` (STATUS code dispatch), `sanitized` (recursive key normalization). +Private methods: `perform_request` (socket write/read/JSON parse, plus control-byte re-escape and `}{` repair), `check_status` (STATUS code dispatch), `sanitized` (recursive key normalization), `safe_on_wire` (invokes the `on_wire:` callback if present; swallows any exception the block raises so a buggy logger cannot break a query). `safe_on_wire` fires three directions: `:request` (outbound JSON), `:response` (raw inbound string, pre-parse), `:response_repaired` (only when the `}{` repair path actually runs — rare against modern cgminer). ### `CgminerApiClient::Miner::Commands` (module) **File:** `lib/cgminer_api_client/miner/commands.rb` @@ -111,7 +113,7 @@ Both `Miner` and `MinerPool` `include Miner::Commands`. The `Commands` methods o ### `CgminerApiClient::MinerPool` **File:** `lib/cgminer_api_client/miner_pool.rb` -Parallel fan-out wrapper. Loads `config/miners.yml` on construction (via `load_miners!`, using `YAML.safe_load_file`) and builds one `Miner` per entry. +Parallel fan-out wrapper. Constructor: `MinerPool.new(on_wire: nil)` — optional `on_wire:` callback is forwarded verbatim into every `Miner` the pool constructs, so one hook covers the whole fan-out. Loads `config/miners.yml` on construction (via `load_miners!`, using `YAML.safe_load_file`) and builds one `Miner` per entry. Public surface: @@ -160,7 +162,7 @@ end ### `bin/cgminer_api_client` **File:** `bin/cgminer_api_client` (42 lines, shebang + executable) -Thin driver. Validates command name against `Miner::Commands.instance_methods`, builds a `MinerPool`, runs `pool.query`, prints per-miner output, exits. See [architecture.md](architecture.md#what-the-cli-adds-on-top) and [workflows.md](workflows.md#cli-request-flow) for details. +Thin driver. Validates command name against `Miner::Commands.instance_methods`, builds a `MinerPool`, runs `pool.query`, prints per-miner output, exits. Accepts a `-v`/`--verbose` flag that installs a default `on_wire:` callback into the `MinerPool` it builds — the callback writes each direction (`>>>` request, `<<<` response, `<<< (repaired)` for the `}{` path) to stderr under a `Mutex` so interleaved per-miner lines don't tear, and rescues `Errno::EPIPE` / `IOError` so stderr closure mid-run doesn't break the query fan-out. See [architecture.md](architecture.md#what-the-cli-adds-on-top), [workflows.md](workflows.md#cli-request-flow), and [logging.md](logging.md) for details. ## Test-only components (not packaged in the gem) diff --git a/docs/workflows.md b/docs/workflows.md index befd23c..86f82e8 100644 --- a/docs/workflows.md +++ b/docs/workflows.md @@ -24,10 +24,15 @@ sequenceDiagram SWT--xMiner: SocketError / SystemCallError Miner--xCaller: ConnectionError (re-wrapped) end + Miner->>Miner: safe_on_wire(:request, request.to_json) — fires on_wire: if set Miner->>cgminer: write(request.to_json) cgminer-->>Miner: JSON response + EOF + Miner->>Miner: safe_on_wire(:response, raw_response) — fires on_wire: if set Miner->>Miner: re-escape control bytes < 0x20 Miner->>Miner: gsub repairs for '}{' & '[,{' + opt '}{' repair ran + Miner->>Miner: safe_on_wire(:response_repaired, repaired) + end Miner->>Miner: JSON.parse(response) Miner->>Miner: check_status(data) — raise ApiError on E/F Miner->>Miner: sanitized(data) — keys lowercased + symbolized @@ -39,6 +44,7 @@ sequenceDiagram - No connection reuse. Every call opens and closes a fresh socket. - Read-to-EOF pattern. Server closing the socket is what signals "response complete." - `Miner#query` only raises `ConnectionError` or `ApiError` (or a bug like `ArgumentError` if misused); cgminer `STATUS=I`/`STATUS=W` just print a line and continue. +- `safe_on_wire` is a no-op when `on_wire:` wasn't passed to `Miner.new`; when it was, any exception the callback raises is swallowed so a buggy logger can't break a query. See `docs/logging.md` for the full posture and the CLI's `-v`/`--verbose` flag, which is implemented by installing a default `on_wire` callback. ## 2. Parallel pool request flow (`MinerPool#query`) From 81099db47155eb14e7d895143c4fabb89500e41e Mon Sep 17 00:00:00 2001 From: Justin Ramos Date: Thu, 23 Apr 2026 21:27:10 -0700 Subject: [PATCH 4/4] chore: add script/validate_mermaid for local docs diagram linting MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Bash wrapper around npx @mermaid-js/mermaid-cli that extracts every ```mermaid block under docs/*.md and pipes each through the parser, reporting Parse/Lexical errors. Exits non-zero on any failure so a CI job can opt in later. Not wired into bundle exec rake — first run pulls Puppeteer+Chromium (~300MB) into ~/.npm/_npx, which is too heavy to impose on every test cycle. Documented in AGENTS.md "Running tests and lint" as an opt-in local check for docs PRs. All five api_client diagrams validate clean — the monitor and manager PRs in this docs-refresh round each caught real drift, so bundling the same tool here keeps the three repos symmetric for future edits. --- AGENTS.md | 2 ++ script/validate_mermaid | 40 ++++++++++++++++++++++++++++++++++++++++ 2 files changed, 42 insertions(+) create mode 100755 script/validate_mermaid diff --git a/AGENTS.md b/AGENTS.md index 4afd18a..fd4664c 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -160,6 +160,8 @@ bundle exec rubocop -A # lint + auto-correct (review diffs!) **Coverage** is always on (SimpleCov in `spec_helper.rb`). Reports land in `coverage/` — don't commit that directory (it's `.gitignore`d). +**Mermaid validation.** Every `docs/*.md` may contain ` ```mermaid ` blocks. Run `script/validate_mermaid` to lint them all — the script extracts each block and pipes it through `npx @mermaid-js/mermaid-cli`. Requires `node >= 18` and `npx` on PATH. First run is slow (Puppeteer downloads Chromium into `~/.npm/_npx`, ~300 MB; cached after). Not wired into `bundle exec rake` — it's an opt-in local check for docs PRs. + **Manual sandbox** for exercising the CLI without real miners: ```sh diff --git a/script/validate_mermaid b/script/validate_mermaid new file mode 100755 index 0000000..6c88c6c --- /dev/null +++ b/script/validate_mermaid @@ -0,0 +1,40 @@ +#!/usr/bin/env bash +# Lint every ```mermaid block under docs/ by feeding it to +# @mermaid-js/mermaid-cli via npx. Exits non-zero on any parse error. +# +# Requires: node >= 18 and npx on PATH. First run downloads Puppeteer +# + Chromium into ~/.npm/_npx (~300 MB, cached after). +set -euo pipefail + +cd "$(dirname "$0")/.." + +tmp=$(mktemp -d) +trap 'rm -rf "$tmp"' EXIT +echo '{"args":["--no-sandbox"]}' > "$tmp/puppeteer.json" + +for md in docs/*.md; do + base=$(basename "$md" .md) + awk -v t="$tmp" -v b="$base" ' + BEGIN {n = 0} + /^```mermaid$/ {capture = 1; n++; f = sprintf("%s/%s__%d.mmd", t, b, n); next} + /^```$/ && capture {capture = 0; next} + capture {print > f} + ' "$md" +done + +total=0 +fail=0 +for mmd in "$tmp"/*.mmd; do + [ -e "$mmd" ] || continue + total=$((total + 1)) + if ! npx -y @mermaid-js/mermaid-cli -p "$tmp/puppeteer.json" \ + -i "$mmd" -o "${mmd%.mmd}.svg" >/dev/null 2>&1; then + echo "FAIL: $(basename "$mmd")" + npx -y @mermaid-js/mermaid-cli -p "$tmp/puppeteer.json" \ + -i "$mmd" -o /dev/null 2>&1 | grep -E "Parse|Lexical" | head -2 | sed 's/^/ /' + fail=$((fail + 1)) + fi +done + +echo "$((total - fail)) passed, $fail failed (of $total blocks)" +exit $fail