SIGN IN SIGN UP

feat: Rust CLI — report subcommand, a bounded digest for LLM consumption (LEO-337) (#27)

* feat: Rust CLI — report subcommand, a bounded digest for LLM consumption (LEO-337)

Add `sensorwatch report`: condense logged JSONL history into one bounded JSON
digest an agent can read on a small context budget — the sanctioned alternative
to parsing raw logs, which the monitoring protocol forbids.

- `digest.rs` — pure, clock-free logic: `--last` duration parsing, `--since`/
  `--until` window parsing (RFC 3339 / local datetime / bare date with
  start/end edges), candidate-file selection (±1 day tz padding), per-(sensor,
  reading) window aggregation over finite values (HWiNFO's source-lifetime
  min/max/avg deliberately ignored), gap detection (>3× interval), forced-tier
  ranking, and the hard `--max-bytes` fitting loop.
- `report.rs` — wiring: config/rules resolution (report works with zero rules),
  a single streaming pass feeding every in-window sample to both the aggregator
  and a fresh replay engine, violation marking, and the digest emit.
- Violations reuse the frozen `watch` Event shape with a digest-local seq;
  read-only (never touches watch.seq); `ReplaySource::skipped_lines` surfaced in
  meta.
- 21 digest unit tests + 19 integration tests (exact bytes, sub-window
  re-derivation, display-only filters, byte-cap, hostile input, golden replay).
- Docs in lockstep: both READMEs, SKILL.md Recipe 4, openai.yaml, ROADMAP (report
  shipped + truncation question decided), agent-monitoring, SECURITY.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* fix: address LEO-345 round-1 review of report (LEO-337)

Contract + correctness fixes from four reviewers (codex, claude, kimi, Copilot):

- --top no longer drops in-violation reading rows: the selector caps the mover
  tier only; violation rows are always kept (max(#in_violation, top)), and only
  the byte cap can drop them. (codex#1, claude#1)
- VIOLATION_CAP no longer hides data: violations_total is now the exact
  post-filter count from the full scan (mirrors gaps_total), and in_violation is
  marked during the scan so a series whose transitions were evicted keeps its
  forced tier. (codex#2, claude#2, kimi#1)
- Out-of-order/stale records no longer fabricate a phantom gap: the gap baseline
  advances only on in-order samples. (claude#4)
- Duplicate (sensor,reading) within one sample now first-wins in the aggregator,
  matching the engine. (claude#5)
- score() no longer explodes for zero-start series: denominator floored by
  max(|first|,|last|,1.0). (claude#6)
- report defaults to the `warn` log level and warns when the sampling interval
  is defaulted (no config), so gap-threshold provenance and a missing log dir
  reach stderr; stdout stays pure JSON. (claude#3)
- Doc/help fixes: "meta/violations/summary always survive" corrected (only meta
  is guaranteed; violations are droppable, no "summary" section); -v debug log
  now reports the real series count; --match strings lowercased once per call.
  (claude#7/#8, Copilot, kimi#2)

+5 regression tests (239 total). fmt/clippy(-D warnings)/msrv-1.82 green;
re-verified against real HWiNFO data.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* fix: address LEO-345 round-2 review of report (LEO-337)

Round-2 verdicts were LGTM / no-blockers; folded in the remaining hardening and
doc fixes:

- meta.files_scanned now counts files actually opened, not merely selected: a
  candidate that exists but fails File::open (e.g. permissions) is warned and
  excluded, so meta stays an honest coverage signal (codex). ReplaySource gains
  a files_opened() counter; +cfg(unix) regression test.
- Hardening for untrusted input (kimi): gap seconds use a saturating i128→i64
  conversion; score() divides each value by the floor before subtracting so an
  f64-extreme pair can't overflow (last - first) to +inf and outrank everything.
  Score stays finite in [0, 2]; +test.
- ROADMAP truncation bullet: dropped the stale "and summary" wording introduced
  in round 1 (codex/claude) — only the meta block is guaranteed to survive.
- SKILL Recipe 4: use ./target/release/sensorwatch (a bare `sensorwatch` on PATH
  may be the Python console script, which has no report subcommand; codex), and
  state the aggregate-only trade explicitly with a pointer to the future SQL
  surface, LEO-349 (claude).
- --match needles lowercased once per run instead of per row/transition (kimi).

241 tests (+2). fmt / clippy -D warnings / cargo +1.82.0 check --locked green;
digest output re-verified exact.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* perf: pre-size the per-sample dedup HashSet (LEO-337 round-3)

Copilot round-3 nit: `Aggregator::observe` allocated the duplicate-detection
`HashSet` at default capacity, rehashing on records with many readings.
Pre-size to `sample.readings.len()` — behavior identical, one fewer growth.

The broader "reuse the set across samples / stamp pattern" cleanup and kimi's
other round-3 nits (config double-parse, type_filter_label / render_json dedup,
fit() re-render) remain tracked, non-blocking, in LEO-350.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

---------

Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
L
lcj-claude-coder committed
0aee66a7e313eadc0fe3c4a162e43db2a8992065
Parent: 63077dc
Committed by GitHub <noreply@github.com> on 7/4/2026, 12:30:24 AM