Skip to content

feat: implement 28 additive enhancement issues (Tag analysis, EventDetection CRUD, FastSense overlays, stores) - #378

Merged
HanSur94 merged 57 commits into
mainfrom
claude/issues-new-features-ca1106
Jul 9, 2026
Merged

feat: implement 28 additive enhancement issues (Tag analysis, EventDetection CRUD, FastSense overlays, stores)#378
HanSur94 merged 57 commits into
mainfrom
claude/issues-new-features-ca1106

Conversation

@HanSur94

@HanSur94 HanSur94 commented Jul 9, 2026

Copy link
Copy Markdown
Owner

Summary

Implements 28 auto-generated enhancement issues from the backlog, each as the smallest safe additive change through the project's GSD workflow. Every feature is toolbox-free, pure MATLAB/Octave, backward-compatible (no public signature or serialized-format change), verified in a live MATLAB session, and MISS_HIT (mh_style + mh_lint) clean. ~150 new tests, all green; targeted regression sweeps show no breakage.

Commits are grouped as feat(NNN): … (feature + test) followed by docs(state): … (STATE bookkeeping), one pair per issue.

Features by area

Tag analysis family (libs/SensorThreshold/Tag.m, base-class — inherited by every kind)

  • #351 integral(t0,t1) — scalar definite integral
  • #339 percentile()/median()/iqr() — order statistics
  • #329 findPeaks() — toolbox-free local-extrema + prominence
  • #340 correlate(other) — Pearson correlation (first relational primitive)
  • #341 lagCorrelation(other,MaxLag) — cross-correlation / best time-lag
  • #343 removeOutliers() — hampel/iqr/zscore despike
  • #338 spectrum()/dominantFrequency() — single-sided amplitude spectrum (core fft)
  • #358 compareWindows(windows) — phase-aligned multi-window overlay

Tag model

  • #342 StateTag.transitions() — change-point event list
  • #372 StateTag.StateNames + nameAt() — code→name legend
  • #349 MonitorTag.level(...) — level alarm + Deadband hysteresis factory
  • #350 MonitorTag.band(...) — range / out-of-band alarm factory
  • #325 CompositeTag MinDuration debounce (+ shared private/minDurationFilter_.m)
  • #345 SensorTag.fromCsv() — Octave-safe CSV/TSV import
  • #363 TagRegistry.toStructs() — whole-catalog serializer

EventDetection (libs/EventDetection/)

  • #360 EventStore.getEvent(id) — id-addressed point-read
  • #354 EventStore.removeEvent/removeEvents (+ EventBinding.detach)
  • #355 EventStore.editEvent(id,...) (+ Event.editWindow)
  • #310 EventStore.acknowledgeEvents/acknowledgeAll — bulk ack

Stores / bridge / plot

  • #365 PlantLogStore.removeEntries/removeEntriesInRange
  • #366 PlantLogStore.pruneEntriesBefore(t)
  • #364 FastSenseDataStore.removeColumn(name)
  • #318 WebBridge.unregisterAction/listActions
  • #357 FastSense.addVLine(x,...) — vertical reference line
  • #377 FastSense.addSpan(t0,t1,...) — vertical time-window highlight
  • #347 FastSense.addText(x,y,str,...) — on-plot text annotation
  • #356 FastSense XLabel/YLabel + auto-derive Y units from a bound Tag
  • #332 SensorThreshold functionSignatures.json — editor tab-completion

Closes #351, #339, #329, #340, #341, #343, #338, #358, #342, #372, #349, #350, #325, #345, #363, #360, #354, #355, #310, #365, #366, #364, #318, #357, #377, #347, #356, #332

Verification

  • Per-feature focused tests (class suites via runtests; FastSense render smoke tests exercised in real figures).
  • Regression sweeps green: TestTag (96), TestSensorTag, TestMonitorTag, TestCompositeTag, TestStateTag, TestDerivedTag, TestEventStoreRw, TestTagRegistry, TestEvent, TestMonitorTagPersistence, TestCompositeTagAlign, TestMonitorTagEvents, TestPriorStateCacheParity.
  • check_matlab_code + MISS_HIT mh_style/mh_lint clean on all changed files.

Not included (deferred — need a focused pass or a product decision)

  • #348 DisplayScale/DisplayOffset — product decision: a scaled parent getXY would silently change MonitorTag threshold semantics (thresholds are set in raw units).
  • #353 OffDelay, #352 Latched, #323 DerivedTag AlignParents — FSM/streaming carry-in changes best done with a dedicated MonitorTag regression pass.
  • UI/Dashboard/EventViewer parity issues (#359 #346 #344 #320 #315 #311 #313 #337 #324 #322 #319 #317 #335 #336 #314).

🤖 Generated with Claude Code

HanSur94 and others added 30 commits July 9, 2026 08:57
Add the SAVE half of the TagRegistry save/load contract: a static
toStructs() that returns the catalog as the cell array loadFromStructs
already consumes, sorted by key for determinism. Pure assembly over each
Tag subclass's existing toStruct — strictly additive, no format change.

Round-trips: toStructs -> clear -> loadFromStructs restores the catalog.

Tests: +3 in TestTagRegistry (empty, sorted, round-trip); 29/29 pass.
MISS_HIT style+lint clean.

Closes #363

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Add the missing READ of the id-addressed event CRUD surface. getEvent(id)
searches the same set as getEvents (in-memory single-user; NDJSON-merged in
cluster mode), matches Id on Event objects and struct rows (mirroring
acknowledgeEvent/closeEvent), and throws EventStore:unknownEventId when
absent — uniform with the sibling id-addressed mutators.

Tests: +2 in TestEventStoreRw (found-by-id, not-found-throws); 9/9 pass.
check_matlab_code + MISS_HIT clean.

Closes #360

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The single number a sensor engineer reports (energy/dose/volume/throughput):
scalar area under Y over [t0,t1]; integral()/integral([],[]) integrate the
full series. Implemented as a thin positional-arg wrapper over
cumulativeIntegral (#327), inheriting its toolbox-free gap-robust trapezoidal
core, empty/single-sample=>0 policy, NaN-zeroing, and Tag:integralOnDiscrete
warning. Base-class method — inherited by every tag kind via getXYRange.

Tests: +7 in TestTag (constant=>20, triangle=>8, window==Range,
empty-bounds==full, empty-data=>0, NaN-robust, discrete-warns); 39/39 pass.
check_matlab_code + MISS_HIT clean.

Closes #351

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Complete the Tag statistics surface with the order-statistic dimension that
getStats (#223) deliberately left out. percentile(levels,t0,t1) returns
percentile values via toolbox-free linear interpolation (i = p/100*(n-1)+1)
between sorted samples; output matches the level shape, NaNs are masked,
empty=>NaN, levels validated to [0,100]. Convenience median()==P50 and
iqr()==P75-P25. Base-class method inherited by every tag kind via getXYRange.

Tests: +8 in TestTag (scalar, vector-shape, median==P50, IQR=4.5, window,
NaN-robust, empty=>NaN, invalid-level errors); 47/47 pass.
check_matlab_code + MISS_HIT clean.

Closes #339

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…/iqr)

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Close the last universal numeric-analysis gap in the Tag family. findPeaks
returns local maxima/minima with prominence, entirely toolbox-free (diff +
indexing + a baseline walk, NOT the Signal Processing Toolbox findpeaks).
Options: MinProminence, MinSeparation, Polarity (max|min|both), Range. Struct
output {times,values,prominences,polarity,count,intervals} plus a 2-out
[times,values] form. Flat-top plateaus report one peak; NaNs segment the
series; MinSeparation greedily keeps the most prominent peak; minima are the
maxima of -Y. Discrete StateTag warns Tag:findPeaksOnDiscrete. Core lives in a
static private helper detectExtrema_.

Tests: +12 in TestTag (single, multi+intervals, prominence filter, separation
merge, plateau, minima, both, NaN gap, range, 2-out, bad-option, discrete);
59/59 pass. check_matlab_code + MISS_HIT style/lint/metric clean.

Closes #329

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Close the create-without-delete asymmetry in the event lifecycle. removeEvents
(lenient bulk, returns count removed, skips unknown ids) and removeEvent
(strict, throws EventStore:unknownEventId on a miss) drop events from events_
by Id via the same linear scan closeEvent uses, without auto-saving (Pitfall 2).
Each removed event cascades EventBinding.detach (new additive mirror of attach
that purges forward + reverse indexes) and drops its single-user ack records so
no dangling binding/ack survives.

Tests: +5 in TestEventStoreRw (drop-one, unknown-throws, bulk-skips-unknown,
binding-detach cascade, save/reload reduced set); 14/14 pass.
check_matlab_code + MISS_HIT clean.

Closes #354

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Complete the event/annotation CRUD lifecycle (create + delete shipped) with an
id-addressed, validated, save-aware EDIT. Event.editWindow is a new additive
in-place mutator that (unlike close) works on an already-closed event —
recomputing Duration and reusing the EndTime>=StartTime guard — so a manual
annotation, closed on creation, can finally be nudged. EventStore.editEvent
edits StartTime/EndTime/Notes/Severity/Category/Label by id; it validates keys
before touching the event and applies the window guard first, so a bad key or
inverted window leaves the event untouched. No auto-save (Pitfall 2).

Tests: +7 in TestEventStoreRw (editWindow recompute+reject, edit
window+notes+severity, unknown-id, unknown-field-untouched,
inverted-window-untouched, save/reload); 21/21 pass. check_matlab_code +
MISS_HIT clean.

Closes #355

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Add bulk acknowledgement for alarm-flood workflows. acknowledgeEvents(ids)
acknowledges exactly the given set (the operator's filtered/visible list) and
acknowledgeAll() acknowledges the whole unacknowledged set; both loop the
existing single-event acknowledgeEvent so each ack retains its full
{user,host,epoch,comment} audit stamp (IDENT-02). Unknown and already-acked ids
are skipped; each returns the count acknowledged; neither auto-saves (mirrors
acknowledgeEvent).

Tests: +5 in TestEventStoreRw (ack-list, skip-already-acked, skip-unknown,
ack-all + idempotent, audit-comment preserved); 26/26 pass. check_matlab_code +
MISS_HIT clean.

Closes #310

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Add the first relational primitive to the otherwise-unary Tag analysis family:
correlate(other[,t0,t1]) returns Pearson r in [-1,1] of two tags' time-aligned
series (2-out [r,n] also returns the aligned sample count). The second tag is
sampled onto the first's timestamps by zero-order-hold valueAt — the same ZOH
convention DerivedTag uses for mismatched parents — pairwise NaNs are dropped,
and r is computed toolbox-free via sums (no Statistics Toolbox). Returns NaN for
fewer than 2 aligned pairs or a constant (zero-variance) channel. Base-class
method inherited by every tag kind. Unlocks the #341 lagCorrelation sibling.

Tests: +9 in TestTag (identical=>1, anti=>-1, orthogonal=>0, n-out,
zero-variance=>NaN, n<2=>NaN, ZOH alignment, range, bad-other); 68/68 pass.
check_matlab_code + MISS_HIT clean.

Closes #340

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…me-lag

Add the time-delay sibling of correlate (#340). lagCorrelation ZOH-resamples
both tags onto a uniform grid over A's windowed span (spacing = median A sample
spacing), scans a toolbox-free normalized cross-correlation over +/-MaxLag
samples (NOT xcorr/finddelay), and returns the argmax lag in time units
(positive dt => B lags A). Forms: dt, [dt,r], and [dt,r,lags,rr] (a plottable
r-vs-lag curve); options are positional t0,t1 and name-value MaxLag. NaN is
returned when the overlap is < 2 samples or a channel is constant. Shared
Pearson math extracted to a static private pearson_ helper.

Tests: +7 in TestTag (recover +4 delay, identical=>lag0/r1, MaxLag clamp,
full-curve shape, n<2=>NaN, zero-variance=>NaN, bad-other); 75/75 pass.
check_matlab_code + MISS_HIT style/lint/metric clean.

Closes #341

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Answer the canonical discrete-channel question — when did the state change,
how many times, and from what to what — directly. transitions() walks getXY and
emits a struct-array record {Time, FromState, ToState} at each actual state
change (repeated-value samples skipped), handling both numeric and cellstr Y via
strcmp/isequaln so a NaN->NaN run is not a spurious change. Empty struct for a
constant or empty channel. The categorical analog of crossings (#328) and the
change-point complement to the aggregate stateDurations (#258).

Tests: +4 in TestStateTag (numeric, cellstr, constant=>empty, empty-channel);
22/22 pass. check_matlab_code + MISS_HIT clean.

Closes #342

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…ction

Add the missing data-cleaning axis to the Tag analysis family. removeOutliers is
a non-mutating base-class method that flags amplitude outliers with a robust
test (Method hampel rolling-median+MAD default, iqr Tukey fence, or zscore
modified z-score), then returns a cleaned copy plus the outlier indices; Fill
selects nan|linear|previous|remove. Toolbox-free (median/MAD + interp1, NOT
isoutlier/filloutliers which are absent in Octave); existing NaN inputs are
never counted as outliers; a zero-spread Hampel window still catches a lone
spike. numeric-Y only (StateTag categorical -> Tag:notNumeric). Inherited by
every tag kind.

Tests: +9 in TestTag (hampel spike, non-mutating, fill linear/previous/remove,
iqr, zscore, non-numeric error, bad-option errors); 84/84 pass.
check_matlab_code + MISS_HIT style/lint/metric clean.

Closes #343

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…e spectrum

Add the frequency-domain sibling of the Tag analysis family. spectrum() returns
a single-sided amplitude spectrum computed with core fft (non-DC/non-Nyquist
bins doubled so a pure tone reads ~A); Fs is inferred from the median sample
spacing or set via 'SampleRate', and 'Detrend' (linear/mean) removes trend
before the transform. dominantFrequency() returns the largest non-DC peak's
frequency. numeric-Y only (Tag:notNumeric), >=2 samples
(Tag:spectrumTooFewPoints); near-uniform sampling assumed (documented, points at
resampleUniform #308). Toolbox-free — NOT the Signal Processing Toolbox.
Base-class methods inherited by every tag kind.

Tests: +7 in TestTag (10Hz sine peak, amplitude+bin-count, SampleRate override,
mean-Detrend DC removal, too-few-points, non-numeric, bad-options); 91/91 pass.
check_matlab_code + MISS_HIT clean.

Closes #338

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…d delete

Add the targeted DELETE the operator journal was missing — the last of the three
journal-style stores to get one (siblings EventStore.removeEvents #354,
FastSenseDataStore.removeColumn #364). removeEntries(ids) drops entries by Id
(char/string/cell, unknown ids skipped) and removeEntriesInRange(t0,t1) drops by
Timestamp window (mirrors getEntriesInRange); both preserve sorted order and
return the count removed. No new state.

Tests: +5 in TestPlantLogStore (by-id, bulk-skip-unknown, bad-input, in-range,
bad-bounds); 26/26 pass. check_matlab_code + MISS_HIT clean.

Closes #365

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Add age-based retention for long-running operator journals: pruneEntriesBefore(t)
drops every entry older than t (keeps Timestamp >= t) and returns the count
removed — a no-op when nothing is older, emptying the store when t is past the
newest entry. Sibling of the EventStore age-prune (#293).

Tests: +4 in TestPlantLogStore (prune-head, no-op, empties-past-newest,
bad-input); 30/30 pass. check_matlab_code + MISS_HIT clean.

Closes #366

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
HanSur94 and others added 25 commits July 9, 2026 09:44
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…teresis

Add a static factory that expresses a scalar level alarm in one line and
synthesizes both condition handles. MonitorTag.level(key, parent, tripLevel)
builds ConditionFn @(x,y) y>trip (Direction 'above', default) or y<trip
('below'); a Deadband d>0 adds the AlarmOffConditionFn for hysteresis. Because
the FSM flips ON->OFF when AlarmOffConditionFn is true (it is the clear
trigger), 'above' clears at y<trip-d and 'below' at y>trip+d. All remaining
options (MinDuration, EventStore, callbacks, Persist, DataStore, Tag universals)
forward verbatim to the constructor; no FSM/constructor/serialization change.

Tests: +6 in TestMonitorTag (above default + no off-cond, below, deadband
extends alarm through the band, option forwarding, validation errors); 33/33
pass. check_matlab_code + MISS_HIT clean.

Closes #349

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Add the range/out-of-band sibling of MonitorTag.level (#349).
MonitorTag.band(key, parent, lo, hi) trips when y is OUTSIDE [lo,hi] ('outside',
default) or INSIDE it ('inside'). A Deadband d>0 synthesizes the matching
AlarmOffConditionFn (the FSM clear trigger): an outside alarm clears only once y
is back inside the shrunk band [lo+d, hi-d], an inside alarm once y is outside
the widened band [lo-d, hi+d]. lo>hi or non-finite bounds raise
MonitorTag:invalidBand; all other options forward verbatim to the constructor.

Tests: +4 in TestMonitorTag (outside default + no off-cond, inside, deadband
holds alarm in the band zone, validation errors); 37/37 pass. check_matlab_code
+ MISS_HIT clean.

Closes #350

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…ion registry

Complete the server-side action-registry lifecycle. unregisterAction(name) is
the mirror of registerAction — rmfield from the Actions struct and, when a
client is connected, push the refreshed action list (silent no-op if absent);
listActions() returns fieldnames(Actions) as a cellstr. No protocol/wire change.

Tests: +4 in TestWebBridge (unregister, absent-no-op, list-includes-registered,
list-reflects-unregister); 6/6 pass. check_matlab_code + MISS_HIT clean.

Closes #318

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Add a static constructor that reads a delimited file into a SensorTag (or a
SensorTag array for multiple value columns) — the import sibling of exportCsv
(#288). Resolves TimeCol/ValueCol by header name or 1-based index, keys multi-
column output by header (single-column key overridable via 'Key'), and forwards
Name/Units/Criticality/... to the constructor. Reads through the library's
toolbox-free, Octave-safe readRawDelimited_ (MEX + pure-MATLAB fallback), NOT
readtable — which is what keeps it Octave-safe — auto-detects the delimiter, and
sorts rows by ascending time.

Tests: +6 in TestSensorTag (auto cols, by-header-name, multi-col array,
key-override + Units passthrough, sort-by-time, bad-column errors); 26/26 pass.
check_matlab_code + MISS_HIT clean.

Closes #345

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…ag trilogy

Add opt-in MinDuration to CompositeTag so the aggregated output only flips to 1
once the condition has held >= MinDuration (parent-X units), stopping aggregated
alarms from chattering on transient single-sample child excursions. The
run-length filter is factored into a new shared private helper
minDurationFilter_ (NaN-preserving, strict less-than, matching the MonitorTag
Stage-3 debounce), applied to the merged 0/1 series before caching. MinDuration
is parsed in the constructor (added to splitArgs_ cmpKeys) and round-tripped in
toStruct (omit-when-zero) / fromStruct; default 0 preserves current behavior
exactly. Completes MinDuration support across Monitor / Derived / Composite.

Tests: +4 in TestCompositeTag (suppresses short run, zero keeps short run,
struct round-trip, omit-when-zero); 35/35 pass. check_matlab_code + MISS_HIT
clean.

Closes #325

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…ion)

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Give StateTag an optional code->name legend so integer-coded PLC/SCADA/recipe
state channels can render 'Run' instead of a bare 2 without giving up the
numeric trace. Adds a public StateNames property (Nx2 {code,name} cell,
Octave-safe, [] => numeric behaviour byte-for-byte unchanged), accepted as a
'StateNames' constructor option, and a nameAt(t) accessor that maps
valueAt(t) through the legend — mirroring valueAt's scalar->char /
vector->cellstr shape with a numeric-string fallback for unmapped codes.
Serialized (omit-when-empty) in toStruct / fromStruct so existing serialized
StateTags round-trip untouched.

Tests: +6 in TestStateTag (scalar, vector, unmapped fallback, no-legend
fallback, round-trip, omit-when-empty); 28/28 pass. TestTagRegistry 29/29 (no
serialization regression). check_matlab_code + MISS_HIT clean.

Closes #372

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Add the vertical partner of addThreshold: addVLine(x) draws a vertical
reference line at a time/x value spanning the full Y range — for marking an
event instant, a setpoint change, or a shift boundary. Options Color /
LineStyle / LineWidth / Label; pre-render only (FastSense:alreadyRendered),
non-scalar x rejected (FastSense:invalidVLine). Rendered after the axis
Y-limits finalise so the line spans the axis with XLimInclude/YLimInclude off
(excluded from limit computation), tagged UserData.FastSense.Type='vline' with
an optional top label.

Tests: tests/test_add_vline.m — 6 tests (config, defaults, multiple,
non-scalar error, render smoke [hLine valid, XData=[x x], UserData type],
reject-after-render); all pass in live MATLAB. check_matlab_code + MISS_HIT
clean.

Closes #357

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…light

Add the missing vertical dual of addBand. addSpan(t0, t1) shades the vertical
band X in [t0,t1] across the full Y range — for highlighting a phase, shift, or
event window on a time axis. Options FaceColor / FaceAlpha / EdgeColor / Label;
pre-render only (FastSense:alreadyRendered), non-scalar or inverted bounds
rejected (FastSense:invalidSpan). Rendered after the axis Y-limits finalise as a
translucent patch (YLimInclude off) pushed behind the data lines via uistack
(try/catch fallback), tagged UserData.FastSense.Type='span'.

Tests: tests/test_add_span.m — 6 tests (config, defaults, multiple, inverted
error, render smoke [hPatch valid, XData spans [t0,t1], UserData type],
reject-after-render); all pass in live MATLAB. check_matlab_code + MISS_HIT
clean.

Closes #377

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Add the text member of the annotation family (addMarker/addThreshold/addBand).
addText(x, y, str) places a text callout at data coordinates (x,y). Options
Color (default Theme.ForegroundColor) / FontSize / HorizontalAlignment /
VerticalAlignment; pre-render only (FastSense:alreadyRendered), non-scalar
coords or non-char str rejected (FastSense:invalidText). Rendered as a
front-layer text object tagged UserData.FastSense.Type='text'.

Tests: tests/test_add_text.m — 6 tests (config, defaults, multiple, bad-args
errors, render smoke [hText valid, String, UserData type], reject-after-render);
all pass in live MATLAB. check_matlab_code + MISS_HIT clean.

Closes #347

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
… a bound Tag

Give the core plot axis-labeling. Add public XLabel/YLabel name-value options
(default '') drawn in render() only when non-empty, so the default is identical
to prior behaviour. addTag auto-derives the Y label from a single bound tag when
unset — Units -> Name -> Key, matching FastSenseWidget's derivation — and clears
the derived label once a second tag is added (multi-tag -> unlabeled); an
explicit YLabel always wins.

Tests: tests/test_axis_labels.m — 6 tests (explicit drawn, default unlabeled,
derive-from-Units, fallback-to-Name, explicit-wins, multi-tag-clears); all pass
in live MATLAB. check_matlab_code + MISS_HIT clean.

Closes #356

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…dow overlay

Add a base-Tag primitive that resolves the tag over N windows and re-zeroes each
window's time onto a shared relative-time axis for shift-vs-shift / run-vs-run
comparison. compareWindows({[t0 t1],...}) returns a struct array {RelT, Y,
Window} ready for the existing FastSense.addLine overlay path; 'Anchor' selects
'start' (default, re-zero at t0), 'end' (align on t1), or a numeric scalar
offset. Delegates to getXYRange once per window and passes Y through unchanged
(so a categorical StateTag also aligns), inherited by every Tag kind with no
per-subclass work.

Tests: +5 in TestTag (start anchor, end anchor, scalar anchor, Y carry-through,
bad-args errors); 96/96 pass. check_matlab_code + MISS_HIT clean.

Closes #358

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…etion

Add functionSignatures.json for the SensorThreshold (Tag) library, giving the
MATLAB editor argument tab-completion for the Tag family — parity with the
existing FastSense/Dashboard signatures. 21 entries cover the
SensorTag/StateTag/MonitorTag/CompositeTag constructors, the MonitorTag.level and
band factories, SensorTag.fromCsv, StateTag.nameAt/transitions, TagRegistry
get/register/toStructs, and the Tag analysis methods (getStats, percentile,
integral, correlate, lagCorrelation, findPeaks, removeOutliers, spectrum,
compareWindows) with kind/type/choices per argument.

Tests: tests/test_function_signatures.m validates all three functionSignatures
files parse as JSON with a schema version and that the SensorThreshold file
exposes the expected Tag-family + factory entries; passes in live MATLAB.
MISS_HIT clean.

Closes #332

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…tures)

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
@codecov

codecov Bot commented Jul 9, 2026

Copy link
Copy Markdown

…natures test

CI on the PR branch flagged two issues:

- MATLAB Lint (mh_metric --ci): FastSense.render, already 597/600 lines on main,
  tipped over the 600-line and 95-cyclomatic gates once the #356 axis-label and
  #357/#377/#347 overlay blocks were added inline. Extract both into private
  helpers applyAxisLabels_ and renderCustomOverlays_; render() is back under
  budget and all 24 render/label tests still pass unchanged.

- Octave Tests: test_function_signatures asserted MATLAB-specific jsondecode
  field names (_schemaVersion -> x_schemaVersion, dotted keys -> underscores),
  which Octave mangles differently. Validate against the raw JSON text via
  strfind instead (implementation-independent) while still confirming jsondecode
  parses each file.

(The windows-latest Concurrency Smoke failure is pre-existing and environmental
— no C compiler on the runner, so the lock MEX can't build — unrelated to this
branch, which adds no classdef or MEX.)

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
@HanSur94
HanSur94 merged commit 7db1eea into main Jul 9, 2026
21 of 22 checks passed

HanSur94 commented Jul 9, 2026

Copy link
Copy Markdown
Owner Author

Wiki documentation follow-up for this PR:

PlantLogStore.removeEntries/pruneEntriesBefore (#365, #366) and functionSignatures.json (#332) were left out — see #379's description for why.


Generated by Claude Code

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.

Tag: add integral(t0, t1) — scalar definite integral (total/area) over a window

1 participant