Skip to content

feat: Anomaly Detector toolbox + headless runner + shared toolbox_preview engine#171

Draft
Alvvalencia wants to merge 22 commits into
mainfrom
feature/plot-markers
Draft

feat: Anomaly Detector toolbox + headless runner + shared toolbox_preview engine#171
Alvvalencia wants to merge 22 commits into
mainfrom
feature/plot-markers

Conversation

@Alvvalencia

Copy link
Copy Markdown
Contributor

Summary

Integration PR for the plot-markers feature on the plugins side. Delivers the Anomaly Detector as a two-mode tool plus the shared preview engine it's built on:

  • GUI toolbox (toolbox_anomaly_detector/) — a host-driven, Luau-free plugin. It submits a rule to the host via pj.generators.v1 (GeneratorsHostService); the host runs and re-runs it as data changes and publishes the markers. Live preview is host-driven too (ephemeral generator read back through the object store).
  • Headless CLI (tools/anomaly_runner/) — standalone analyzer running the same rule in-process via the shared Luau engine (pj_scripting_core). CSV/MCAP in → JSON report + exit code out, for CI; webhook/email/command sinks.
  • GUI == headless by construction: both run the rule through the same runMarkerScript core and emit the same PlotMarkers.

Latest in this push

  • New shared lib common/toolbox_preview/ (UI-free): SeriesCatalog (source enumeration + cache + streaming live-refresh), EphemeralPreview (change-gated submit + read-back), AdaptivePoll (cadence policy), behind a PreviewBackend seam. Reusable by the upcoming Transform Editor toolbox; the Anomaly Detector now supplies only the marker-specific render.
  • Large-dataset preview freeze fix: the ephemeral generator declared the entire catalog as inputs, and the host materializes every declared input whole-series per run on the GUI thread. parseSeriesRefs now declares only the series the rule actually reads via series("..."). The marker read-back is change-gated on SeriesCatalog::dataRevision() (deserialize on data change, not every 20 Hz tick); the in-dialog overlay is stride-capped at 2000 markers (committed markers uncapped).

Test plan

  • toolbox_preview_test — 7/7 green (AdaptivePoll cadence + EphemeralPreview change-gate).
  • ./build.sh toolbox_anomaly_detector builds clean (lib + plugin).
  • Manual: nissan_zala → Anomaly Detector → select a real source → preview is smooth (no freeze), markers update live, zoom/grid/pan work.

🤖 Generated with Claude Code

Alvvalencia and others added 15 commits June 16, 2026 14:55
A toolbox plugin (modeled on toolbox_quaternion) whose dialog buttons
create plot markers through the pj.marker_store.v1 service — the
"simulator" producer standing in for the future Lua detector library,
which will emit markers through the same MarkerStoreHostView API.

- Add Region / Add Event: read the first series from the catalog
  (dataset = topic.source.id, time span from the Arrow series) and
  markerHost.add(dataset, "__global__", marker) so it renders on every
  plot of the dataset; Clear: query + remove by id.
- markers_plugin.cpp + markers_dialog.ui + manifest.json + conanfile.py +
  CMakeLists.txt; registered in both dual-mode blocks of the top-level
  CMakeLists.

Building this requires an SDK with pj.marker_store.v1 (currently on the
plotjuggler_sdk feature/plot-markers branch; pin once released to Conan).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
… set)

The pj.marker_store.v1 service was removed. The plugin now owns its marker set and republishes the whole PlotMarkers blob via registerObjectTopicOnDataset + pushOwnedObject on the dataset's global marker object topic.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…lobal scope

Replace the three demo buttons with a real producer UI:
- Drag timeseries into a table (inputFrame drop target) to mark them
  per-series; markers publish to the dropped 'topic/field' object topic so
  the overlay draws them only on plots showing that series.
- Tick 'whole dataset (global)' + a dataset selector to target one or all
  loaded datasets' global marker topic.
- A time RangeSlider (spanning the data's time window) positions Region
  start/end and Event instant.
- Markers are kept per (dataset, topic) and republished wholesale on change.
The per-series 'topic/field' -> dataset index is built lazily at Add time.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…h, retention & deferred Save

Replace the single-range slider with the MarkerTimeline editor: Add time range/event drop draggable marks, edited in place, applied on Save (deferred publish) and removed via Clear. Trash-icon Remove-series; titled Series table.

Correctness from the marker review: store the draft in absolute ns (no remap when the window changes); key series_dataset_ via sdk::markerSeriesKey so per-series markers publish to the right dataset; read existing markers back dataset-scoped (lookupTopicOnDataset) and load on every drop; cap each marker topic to keep-latest via setObjectTopicRetention; Clear now republishes empty to the listed + ever-saved topics (honest tooltip).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
A GUI toolbox to author Lua anomaly-detection rules that emit plot markers,
and a headless anomaly_runner CLI that runs the SAME rules without a GUI for
servers/CI. Both link the GUI-free anomaly_core engine (sol2 Lua, kissfft
bandPower, 13 builtins, JSON report, portable JSON rules), so a rule authored
and tested in the GUI produces identical markers headless.

- toolbox_anomaly_detector/: Filter-Editor-style GUI plugin + core/anomaly_core
  shared static lib + README (GUI guide, builtin table, Lua API).
- tools/anomaly_runner/: SDK-only CLI (no Qt). CSV direct ingest, MCAP delegated
  to the matching parser .so by encoding. JSON report via nlohmann::ordered_json;
  exit 0=pass / 1=fail / 2=usage-error for pipeline gating.
- Series keys use sdk::markerSeriesKey in the runner so GUI and headless resolve
  the identical series name (no spurious double slash on ROS/MCAP fields).
- README plugins table, CLAUDE.md (Task F is new, not a port) updated.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01RcygzPWUrHf9jHGGwzEzZo
…ve sources

- Save/Load rule now use native file dialogs (setSaveFilePicker / setFilePicker);
  the manual rule-path field is gone. Save-as creates a new .json anywhere.
- The preview overlays the rule's detected markers live (setChartMarkers) so you see
  what Apply will publish; markers recompute on source change — a builtin rule
  re-targets the newly selected curve automatically (hand-edited Lua is preserved).
- The source list refreshes live (onTick + a cheap catalog-signature guard) so a
  dataset loaded after the panel opened appears without reopening the toolbox.
- Reworked the dialog .ui: a vertical splitter (preview on top) over grouped,
  bordered Source / Function / Lua panels.
- makeSeriesProvider() removes the duplicated SeriesProvider setup.
- README rewritten: concise, usage-focused.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01RcygzPWUrHf9jHGGwzEzZo
Migrate the Anomaly Detector (GUI toolbox + headless anomaly_runner) off its own
sol2 Lua VM onto the shared pj_scripting_core Luau engine — the same engine
PlotJuggler uses for filters. One engine, GUI == headless, on the boss's Luau
north star. Behavior-preserving: the runner parity holds via Luau (CSV Spike ->
7/fail/exit 1, MCAP /sensor/value/data -> 36/fail/exit 1).

- anomaly_core: SeriesAccessor/SeriesProvider are now aliases of the engine's
  SeriesView/SeriesProvider; runAnomalyScript() is a thin delegate to
  PJ::scripting::runMarkerScript(). The sol2 runtime + marker primitives +
  bandPower (~293 lines) are gone — they live in pj_scripting_core. Kept the
  GUI-free helpers: builtins, substituteSource, markersToReport, rule<->json.
- CMake/Conan: link pj_scripting_core, drop lua/sol2/kissfft from both targets.
  luau/*:fPIC=True so Luau's static libs link into the plugin .so.
- Docs (CLAUDE.md + doc-comments) updated to the shared-Luau reality.

The consumer .cpp (anomaly_detector.cpp, runner main.cpp) needed ZERO changes —
the alias + delegation kept the anomaly_core surface intact.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01RcygzPWUrHf9jHGGwzEzZo
…batch watcher

Completes Task F deliverable #6 (notification integration) and the deploy/docs
half of #7. Opt-in via --notify <config.json>; without it the runner is identical
to before (zero regression). Notifications are a deployment concern, kept OUT of
the portable rule JSON so a rule stays shareable and secrets stay on the server.

- notify.{hpp,cpp}: NotifyConfig (notify_on policy + sinks), ${ENV} expansion for
  secrets, pure shouldNotify predicate, dispatch. Sinks: webhook (libcurl HTTP
  POST), email (libcurl SMTP), command (fork/exec, report on stdin — integrate
  any existing alerting). Decoupled from anomaly_core: operates on the report JSON.
- main.cpp: --notify + --notify-strict (delivery failure -> exit 3, overriding the
  pass/fail verdict). Config validated up front (exit 2 on a typo).
- Build: libcurl/8.10.1 + gtest on the runner; CMake adds an anomaly_notify lib +
  notify_test (14 cases). Root conanfile.py also gains pj_scripting_core (it was
  missing from the full-build union since the Luau migration) + libcurl.
- deploy/watch.sh: dependency-free batch watcher — processes each new MCAP once
  (atomic, de-duped), runs the runner with --rule --notify. README documents
  cron / systemd.path / inotify wrappers.
- examples/: ready-to-run rule + notify configs + a webhook listener, so the CLI
  can be exercised without hand-writing any JSON.
- Docs: runner README "Configuring notifications" + "Deploying the batch watcher";
  cross-links from the toolbox/root README + CLAUDE.md dep table.

Verified: notify_test 14/14; command sink receives the exact report; notify_on
fires on fail but not on pass; --notify-strict -> exit 3; a real webhook POST
delivered the report with the ${ENV}-expanded Authorization header; watcher dedup.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01RcygzPWUrHf9jHGGwzEzZo
…y Detector)

The manual marker-timeline editor is removed; PlotMarkers rendering and the
sdk::PlotMarkers type stay (used by the Anomaly Detector + renderer).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01BeoYQHmF6ixvYv9KXXgyVh
Re-sync to merged SDK 0.12.0 (PlotMarkers builtin). SDK_VERSION 0.10.0 -> 0.12.0;
pj_scripting_core now pins 0.12.0. CMakeLists auto-merged (main's plugin list +
our toolbox_markers removal). Verified: anomaly_runner CSV Spike -> 7 / fail / exit 1.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01BeoYQHmF6ixvYv9KXXgyVh
…ore split; unified README

The Anomaly Detector GUI plugin becomes host-driven and Luau-FREE: it submits
rules to the host via pj.markers.v1 (the host runs + recomputes them) and the live
preview is host-driven too (an ephemeral generator read back through the object
store). The headless anomaly_runner is unchanged (standalone engine). Both run the
rule through the same runMarkerScript core → identical markers ("GUI == headless").

- core split: anomaly_helpers (Luau-free: builtin library + JSON report/rule
  helpers, linked by the plugin) vs anomaly_core (adds runAnomalyScript on
  pj_scripting_core, linked by the runner). The plugin .so no longer links Luau
  (~1.67MB → ~471KB, 0 Lua symbols).
- anomaly_detector.cpp: runScript → createMarkerGenerator; previewMarkers →
  setPreview + read-back; rule errors surfaced in the status line; clearPreview on
  close; removed the dead in-process publish path.
- Global marker scope: per-dataset OR all-datasets (new .ui checkbox; {"scope":"all"}
  in params_json).
- README.md rewritten as a single GUI + headless guide; CLAUDE.md Task F paragraph
  + tools/anomaly_runner/README.md updated to the host-driven architecture.
- Bumps the SDK submodule to the pj.markers.v1 commit.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01QsY1SE2yBsX2qXvkgEnX8U
…eadable marker API

Follows the pj_scripting rename (createMarker + create{Vertical,Horizontal,Point,Band}Marker):
- builtin templates (anomaly_helpers.cpp) now use the explicit shortcuts where they read
  best: createVerticalMarker / createHorizontalMarker / createPointMarker / createBandMarker.
- toolbox + runner READMEs document the new family.
- fixes the runner examples (examples/custom_rule.lua, examples/spike_rule.json) that still
  called the removed createEvent -> they now use createPointMarker.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01QsY1SE2yBsX2qXvkgEnX8U
Pull the SDK 0.11.0 bump + aggregate-build hardening from main into the
markers feature branch. Kept SDK_VERSION at 0.12.0 (feature version
supersedes main's 0.11.0; merged SDK is a superset). extern/plotjuggler_core
recorded at the feature<-main merge.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Rh6GtzBNRVXXBUhFWhXEam
…vice)

The Anomaly Detector submits its rule via GeneratorsHostService
(createMarkerGenerator, kind="markers") instead of the removed MarkersHostService.
No behavior change: the host still runs the rule and publishes markers; the headless
runner is unchanged.

- SDK_VERSION -> 0.13.0.
- Advance extern/plotjuggler_core gitlink -> 9853aa9 (the pushed SDK commit carrying
  pj.generators.v1) so a clean from-source build compiles.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_013N1nzqco4Y9AQhnQgXo5tQ
…preview freeze

Extract the generator-toolbox preview machinery out of the Anomaly Detector into a new
UI-free common/toolbox_preview static lib (SeriesCatalog, EphemeralPreview, AdaptivePoll)
behind a PreviewBackend seam, so the upcoming Transform Editor toolbox can reuse it. The
Anomaly Detector now supplies only the marker-specific render (submit/read-back/teardown).

Fix the live-preview freeze on large datasets (nissan_zala): the ephemeral marker generator
declared the ENTIRE catalog as inputs, and the host materializes every declared input
whole-series on each run on the GUI thread. parseSeriesRefs now declares only the series the
rule actually reads via series("..."). The marker read-back is change-gated on
SeriesCatalog::dataRevision() (deserialize only when data changed, not every 20 Hz tick), and
the in-dialog preview overlay is capped at 2000 markers via stride-decimation. Committed
(Apply) markers are uncapped. GUI==headless preserved (host runs the same rule engine).

Includes unit tests for the cadence policy (AdaptivePoll) and the change-gated read-back.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_013N1nzqco4Y9AQhnQgXo5tQ
@Alvvalencia Alvvalencia marked this pull request as draft June 26, 2026 16:42
Alvvalencia and others added 7 commits June 27, 2026 10:14
…kind=markers)

The Anomaly Detector now submits its rule through the unified host service
pj.data_processors.v1 with kind="markers": DataProcessorsHostService /
DataProcessorsHostView::createMarkers / PJ_DATA_PROCESSOR_FLAG_EPHEMERAL, replacing the
interim pj.generators.v1. The shared common/toolbox_preview seam (PreviewBackend) needed
no change — it is service-agnostic.

- toolbox_anomaly_detector/anomaly_detector.cpp: createMarkers + DataProcessorsHostService.
- CLAUDE.md / toolbox_anomaly_detector/README.md: doc aligned to pj.data_processors.v1.
- Bump extern/plotjuggler_core to bf7eb0f (the unified SDK).

Compiled + linked against the unified SDK 0.13.0; toolbox_preview_test passes; verified
end-to-end in the GUI (markers render via the unified service).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01FHPK8VduUcoenjL8AtoSVd
… contract

The "Save rule as..." picker now declares its extension via the new
setSaveFilePicker(..., "json") arg (SDK PR #138). The host appends ".json" when
the user types a name without one, so the manual extension fix-up in saveRuleTo()
is removed — the host is the single source of truth (verified E2E: "a" -> "a.json").

DEPENDENCY: the 5-arg setSaveFilePicker requires the SDK to carry #138. The
extern/plotjuggler_core gitlink bump to a #138-containing commit follows once #138
merges into the SDK feature/plot-markers branch; held local (not pushed) until then.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0122VUqQqWd1KVQQfhrG7632
… CI is green

The GUI Anomaly Detector is host-driven: it submits its rule to the host via the
SDK data-processors service and the host runs Lua. The plugin .so therefore links
only the Luau-free `anomaly_helpers`, never the Luau engine. But the build still
pulled in `pj_scripting_core` (the private Luau engine package), which:

  - broke the Linux per-plugin leg (toolbox_anomaly_detector/conanfile.py declared
    a `pj_scripting_core` require the .so never links), and
  - broke the macOS/Windows monolithic build, which has no cloudsmith remote and
    compiled `tools/anomaly_runner` (the only genuine engine consumer) unconditionally.

Fix — drop the spurious dependency and gate the engine layer behind the runner:

  - toolbox_anomaly_detector/conanfile.py: remove `pj_scripting_core` + the now-moot
    `luau/*:fPIC` option. The GUI plugin needs only plotjuggler_sdk + nlohmann_json.
  - core/CMakeLists.txt: build `anomaly_core` (+ find_package(pj_scripting_core))
    ONLY when PJ_BUILD_ANOMALY_RUNNER is set. `anomaly_helpers` always builds.
  - CMakeLists.txt (root): add option(PJ_BUILD_ANOMALY_RUNNER OFF) and gate
    add_subdirectory(tools/anomaly_runner) in both the subdirectory and standalone lists.
  - tools/anomaly_runner/CMakeLists.txt: set(PJ_BUILD_ANOMALY_RUNNER ON) before adding
    the core subdir, so building the runner pulls in the engine layer.
  - conanfile.py (root): remove `pj_scripting_core` from the full-repo superset.

The runner is unchanged functionally: build it on demand with
`./build.sh tools/anomaly_runner` (its recipe provides pj_scripting_core) or
-DPJ_BUILD_ANOMALY_RUNNER=ON. Verified end-to-end: the runner builds, lists the 12
builtins, detects a spike, and honors the pipeline exit-code contract (fail=1, pass=0,
usage=2). GUI == headless preserved.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0122VUqQqWd1KVQQfhrG7632
…eeds it)

The standalone root CMakeLists' find_package(GTest REQUIRED) runs for every
per-plugin build, but toolbox_anomaly_detector/conanfile.py never listed gtest
(every other plugin recipe does). The omission was masked because the Linux
per-plugin leg died earlier resolving pj_scripting_core; with that spurious
dependency gone (prev commit), Conan now succeeds and CMake configure failed on
"Could NOT find GTest". Add gtest/1.17.0 to the recipe, matching the other plugins.

Verified via the per-plugin path (./build.sh toolbox_anomaly_detector): GTest
resolves, configure succeeds, the .so links.

NOTE: this leg still fails to COMPILE against the published SDK (0.13.0) because
anomaly_detector.cpp adopts the 5-arg setSaveFilePicker(default_suffix) contract,
which lives in the not-yet-merged SDK default_suffix PR. That is the known, planned
convergence dependency — it greens once the unified SDK PR merges and SDK_VERSION
is bumped. This commit removes the gtest blocker so #138 is the single remaining one.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0122VUqQqWd1KVQQfhrG7632
…th default_suffix)

The Anomaly Detector adopted the SDK default_suffix save-file-picker contract
(anomaly_detector.cpp:184 calls the 5-arg setSaveFilePicker), but the pinned SDK
submodule (4ed853a) predated that API — its setSaveFilePicker took 4 args, so the
plugin failed to compile on all three platforms (the last remaining CI blocker, #3).

Pablo unified the markers PR (#135) and the default_suffix PR (#138) into #135;
a3d15132 = 4ed853a + the single "feat(dialog): add save-file picker (#138)" commit,
which adds the 5-arg setSaveFilePicker (default_suffix="") and the
saveFilePickerDefaultSuffix accessor. Move the gitlink forward to it.

SDK_VERSION stays 0.13.0: cloudsmith has no prebuilt 0.13.0 (confirmed in the
acquire-core log), so CI builds the SDK from this submodule — there is no version
collision, and macOS/Windows build core from source anyway.

Verified with a clean rebuild (not the locally-patched cache): purged 0.13.0 from
the Conan cache, rebuilt the SDK from this submodule via conan create, and built
toolbox_anomaly_detector — anomaly_detector.cpp compiles and the .so links.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0122VUqQqWd1KVQQfhrG7632
#135 (PlotMarkers builtin + unify markers/transforms into pj.data_processors.v1,
incl. the default_suffix save-file-picker contract) is now merged to SDK main as
the squash commit ca052ce. Move the submodule gitlink off the feature-branch
commit (a3d15132) onto the landed main commit. The tree is identical (verified:
git diff a3d15132 ca052ce is empty), so this is a pointer-only change — builds
stay green, no recompile.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0122VUqQqWd1KVQQfhrG7632
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