From 02909675aa930e7e3d9f5611eabc1e2a1a6bac7b Mon Sep 17 00:00:00 2001 From: Hannes Suhr Date: Wed, 24 Jun 2026 16:06:54 +0200 Subject: [PATCH 1/5] api: list valid widget types in addWidget unknown-type error MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Backward-compatible, additive: only the error message text changes — the 'DashboardEngine:unknownType' error ID and all behavior are unchanged. addWidget('linechart', ...) previously threw the dead-end "Unknown widget type: linechart" with no list of valid types and no pointer to find them, on the #1 dashboard-building entry point. It now enumerates the registered types (from DashboardWidgetRegistry.types(), the single source of truth, so custom-registered types appear too) and points to DashboardEngine.widgetTypes() — matching the same method's existing "Valid options: %s" message for unknown name-value keys. Proven: 18/18 pass across TestDashboardEngineWidgetTypes + TestDashboardWidgetRegistry (incl. the unknownType-ID assertion); valid adds and the 'kpi' deprecation alias still work. Also adds the API-usability audit report (docs/.api-usability/) marking this finding resolved and recording the next-worst additive finding. Co-Authored-By: Claude Opus 4.8 --- docs/.api-usability/audit-2026-06-24.md | 63 +++++++++++++++++++++++++ libs/Dashboard/DashboardEngine.m | 3 +- 2 files changed, 65 insertions(+), 1 deletion(-) create mode 100644 docs/.api-usability/audit-2026-06-24.md diff --git a/docs/.api-usability/audit-2026-06-24.md b/docs/.api-usability/audit-2026-06-24.md new file mode 100644 index 00000000..95576f98 --- /dev/null +++ b/docs/.api-usability/audit-2026-06-24.md @@ -0,0 +1,63 @@ +# API Usability Audit — 2026-06-24 + +Scope: public surface only — `FastSense` (`addLine`/`addThreshold`/`addTag`/…/`render`/`updateData`), +`DashboardEngine` (`addWidget`/`Theme`/`render`/`save`/`load`), Tag constructors +(`SensorTag`/`StateTag`/`MonitorTag`/`CompositeTag`/`DerivedTag`), `TagRegistry`, +`toStruct`/`fromStruct`. Internals out of scope. + +Runtime: headless `matlab -batch` (R2025b) — the live matlab MCP was not connected this session. +Friction was reproduced by running real code through the public API, not by reading internals. + +## Score + +| Metric | Value | +|---|---| +| Entry points with helpful "valid options" validation | DashboardEngine ctor ✅, attachPlantLog ✅, `addWidget` type ✅ (fixed this run); Tag ctors ❌ | +| Confusing / dead-end errors (no recovery path) | **0** open (was 1) | +| Discoverability: `functionSignatures.json` for tab-completion | **absent** | +| Lines-to-first-plot (example_basic) | ~4 (`FastSense` → `addLine` → `addThreshold` → `render`) — good | + +## Findings (severity-ordered) + +### 🔴 F-1 — `addWidget` unknown type was a dead-end — ✅ RESOLVED (this run) +- **Where:** `DashboardEngine.addWidget` → `error('DashboardEngine:unknownType', ...)`. +- **What a user hit:** any wrong guess (`'linechart'`, `'line'`, `'plot'`, `'numbers'`, `'bar'`) + returned `Unknown widget type: ` with **no list of valid types and no pointer** to how to + find them — on the #1 dashboard-building entry point. It was also inconsistent with the *same + method's* unknown-name-value error (line 164: `... Valid options: %s`). +- **Fix (additive, message-only — ID `DashboardEngine:unknownType` unchanged):** + now `Unknown widget type 'linechart'. Valid types: barchart, chipbar, divider, fastsense, gauge, + group, heatmap, histogram, iconcard, image, multistatus, number, rawaxes, scatter, sparkline, + status, table, text, timeline. Call DashboardEngine.widgetTypes() for descriptions.` + List is derived from `DashboardWidgetRegistry.types()` (single source of truth — custom-registered + types appear automatically). +- **Proof:** 18/18 passed across `TestDashboardEngineWidgetTypes` + `TestDashboardWidgetRegistry` + (incl. `testUnknownTypeStillErrors` which asserts the ID); valid `number` add and the `kpi` + deprecation alias both still work. + +### 🟠 F-2 — Tag constructors' `unknownOption` doesn't list valid keys *(additive — next target)* +- **Where:** `SensorTag`, `StateTag`, `MonitorTag`, `CompositeTag`, `DerivedTag`, and `Tag` base all + throw `Unknown option ''.` with **no list of valid name-value keys**. +- **Why it matters:** contradicts the project's own convention (CLAUDE.md: *"unknown keys throw + immediately with helpful message listing valid options"*) and is now the only inconsistency left + versus `DashboardEngine`'s `Valid options: %s`. A user who mistypes `'Unit'` for `'Units'` or + `'name'` for `'Name'` gets no hint of the correct spelling. +- **Fix shape:** message-only — append `Valid options: %s` built from the class's known key list + (each `splitArgs_` already has the `tagKeys`/`sensorKeys`/etc. arrays in hand). Touches 6 files, so + per the one-fix-per-run rule it is a follow-up; could start with `SensorTag` (most-used) or sweep + all six for consistency. + +### 🟢 F-3 — No `functionSignatures.json` (tab-completion discoverability) *(additive)* +- **Where:** repo root / library roots — none present. +- **Why it matters:** MATLAB editor tab-completion can't suggest `addWidget` types or `addThreshold` + name-value options. A `functionSignatures.json` would make the public API self-describing in the + editor. Purely additive (new file). + +### 🟢 F-4 — Example bootstrap boilerplate *(minor / informational)* +- Every example repeats `projectRoot = fileparts(fileparts(fileparts(mfilename('fullpath'))));` + + `run(fullfile(projectRoot,'install.m'))`. This is example-script bootstrap, not public-API + friction, so it is informational only — no API change warranted. + +## Next-worst additive finding for `/api-polish` +**F-2** — make the Tag constructors' `unknownOption` error list the valid name-value keys (start with +`SensorTag`), bringing the Tag layer in line with the convention already applied to `DashboardEngine`. diff --git a/libs/Dashboard/DashboardEngine.m b/libs/Dashboard/DashboardEngine.m index a13e5992..f3e7370d 100644 --- a/libs/Dashboard/DashboardEngine.m +++ b/libs/Dashboard/DashboardEngine.m @@ -395,7 +395,8 @@ function switchPage(obj, pageIdx) w = ctor(varargin{:}); else error('DashboardEngine:unknownType', ... - 'Unknown widget type: %s', type); + 'Unknown widget type ''%s''. Valid types: %s. Call DashboardEngine.widgetTypes() for descriptions.', ... + type, strjoin(DashboardWidgetRegistry.types(), ', ')); end % Preserve timeline no-store warning From 80f2b44cc70df949ed6128aa4cdb54dc0115dd91 Mon Sep 17 00:00:00 2001 From: Hannes Suhr Date: Wed, 24 Jun 2026 16:44:38 +0200 Subject: [PATCH 2/5] api: list valid name-value keys in Tag-family unknownOption errors MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Backward-compatible, additive: only error-message text changes — every ':unknownOption' error ID, signature, and behavior is unchanged, and no test asserts message text (all assert the ID). All six Tag constructors (Tag base, SensorTag, StateTag, MonitorTag, CompositeTag, DerivedTag) previously threw a bare "Unknown option 'X'." (DerivedTag: "Unknown NV key 'X'.") with no list of valid keys — so a user mistyping 'Unit' for 'Units' got no hint of the correct spelling. This contradicted the project's own convention and was the last spot inconsistent with DashboardEngine's "Valid options: %s" message. Each unknown-option error now appends "Valid options: %s", built via strjoin from the key arrays already local to each splitArgs_ (so the list cannot drift from what the constructor actually accepts). The unreachable defensive 'otherwise' branches at MonitorTag/DerivedTag are left untouched. DerivedTag's wording is normalized to "Unknown option" to match its siblings. Proven: 156/156 pass across TestTag, TestSensorTag, TestStateTag, TestMonitorTag, TestCompositeTag, TestDerivedTag (incl. every unknownOption ID assertion); before/after reproduced for all five concrete subclasses. Resolves audit finding F-2. Co-Authored-By: Claude Opus 4.8 --- docs/.api-usability/audit-2026-06-24.md | 36 ++++++++++++++++--------- libs/SensorThreshold/CompositeTag.m | 3 ++- libs/SensorThreshold/DerivedTag.m | 3 ++- libs/SensorThreshold/MonitorTag.m | 3 ++- libs/SensorThreshold/SensorTag.m | 3 ++- libs/SensorThreshold/StateTag.m | 3 ++- libs/SensorThreshold/Tag.m | 5 +++- 7 files changed, 37 insertions(+), 19 deletions(-) diff --git a/docs/.api-usability/audit-2026-06-24.md b/docs/.api-usability/audit-2026-06-24.md index 95576f98..33f31dbc 100644 --- a/docs/.api-usability/audit-2026-06-24.md +++ b/docs/.api-usability/audit-2026-06-24.md @@ -12,7 +12,7 @@ Friction was reproduced by running real code through the public API, not by read | Metric | Value | |---|---| -| Entry points with helpful "valid options" validation | DashboardEngine ctor ✅, attachPlantLog ✅, `addWidget` type ✅ (fixed this run); Tag ctors ❌ | +| Entry points with helpful "valid options" validation | DashboardEngine ctor ✅, attachPlantLog ✅, `addWidget` type ✅; Tag ctors ✅ (all six fixed) | | Confusing / dead-end errors (no recovery path) | **0** open (was 1) | | Discoverability: `functionSignatures.json` for tab-completion | **absent** | | Lines-to-first-plot (example_basic) | ~4 (`FastSense` → `addLine` → `addThreshold` → `render`) — good | @@ -35,17 +35,25 @@ Friction was reproduced by running real code through the public API, not by read (incl. `testUnknownTypeStillErrors` which asserts the ID); valid `number` add and the `kpi` deprecation alias both still work. -### 🟠 F-2 — Tag constructors' `unknownOption` doesn't list valid keys *(additive — next target)* +### 🟠 F-2 — Tag constructors' `unknownOption` didn't list valid keys — ✅ RESOLVED (2nd run) - **Where:** `SensorTag`, `StateTag`, `MonitorTag`, `CompositeTag`, `DerivedTag`, and `Tag` base all - throw `Unknown option ''.` with **no list of valid name-value keys**. -- **Why it matters:** contradicts the project's own convention (CLAUDE.md: *"unknown keys throw - immediately with helpful message listing valid options"*) and is now the only inconsistency left - versus `DashboardEngine`'s `Valid options: %s`. A user who mistypes `'Unit'` for `'Units'` or - `'name'` for `'Name'` gets no hint of the correct spelling. -- **Fix shape:** message-only — append `Valid options: %s` built from the class's known key list - (each `splitArgs_` already has the `tagKeys`/`sensorKeys`/etc. arrays in hand). Touches 6 files, so - per the one-fix-per-run rule it is a follow-up; could start with `SensorTag` (most-used) or sweep - all six for consistency. + threw `Unknown option ''.` (DerivedTag: `Unknown NV key ''.`) with **no list of valid + name-value keys** — contradicting the project's own convention (CLAUDE.md: *"unknown keys throw + immediately with helpful message listing valid options"*) and inconsistent with `DashboardEngine`'s + `Valid options: %s`. A user who mistyped `'Unit'` for `'Units'` got no hint of the correct spelling. +- **Fix (additive, message-only — all six `*:unknownOption` IDs unchanged):** each unknown-option + error now appends `Valid options: %s`, built via `strjoin` from the key arrays already local to each + `splitArgs_` (so the list cannot drift from what the constructor accepts). Examples: + - `SensorTag('s', 'Unit', 'bar')` → `Unknown option 'Unit'. Valid options: Name, Units, Description, + Labels, Metadata, Criticality, SourceRef, ID, Source, MatFile, KeyName, RawSource, X, Y.` + - `MonitorTag(...,'NotARealKey',5)` → lists `... AlarmOffConditionFn, MinDuration, EventStore, + OnEventStart, OnEventEnd, Persist, DataStore, EventLog.` + DerivedTag's wording was normalized from `Unknown NV key` to `Unknown option` for family consistency. +- **Proof:** 156/156 passed across `TestTag`, `TestSensorTag`, `TestStateTag`, `TestMonitorTag`, + `TestCompositeTag`, `TestDerivedTag` (incl. every `unknownOption` ID assertion); before/after + reproduced for all five concrete subclasses. The unreachable defensive `otherwise` branches at + `MonitorTag:199` / `DerivedTag:218` were intentionally left untouched (they never see a user key — + `monArgs`/`ownArgs` only hold already-recognized keys). ### 🟢 F-3 — No `functionSignatures.json` (tab-completion discoverability) *(additive)* - **Where:** repo root / library roots — none present. @@ -59,5 +67,7 @@ Friction was reproduced by running real code through the public API, not by read friction, so it is informational only — no API change warranted. ## Next-worst additive finding for `/api-polish` -**F-2** — make the Tag constructors' `unknownOption` error list the valid name-value keys (start with -`SensorTag`), bringing the Tag layer in line with the convention already applied to `DashboardEngine`. +**F-3** — add a `functionSignatures.json` so the MATLAB editor can tab-complete the public API +(`addWidget` types, `addThreshold` / Tag-constructor name-value options). Purely additive (new file); +the main design question is scope (which classes/methods to cover first — likely `DashboardEngine.addWidget` +and the `FastSense` `add*` family, since those have the richest name-value surfaces). diff --git a/libs/SensorThreshold/CompositeTag.m b/libs/SensorThreshold/CompositeTag.m index ffbb3d26..79907f48 100644 --- a/libs/SensorThreshold/CompositeTag.m +++ b/libs/SensorThreshold/CompositeTag.m @@ -704,7 +704,8 @@ function validateMode_(mode) cmpArgs{end+1} = v; %#ok else error('CompositeTag:unknownOption', ... - 'Unknown option ''%s''.', k); + 'Unknown option ''%s''. Valid options: %s.', ... + k, strjoin([tagKeys, cmpKeys], ', ')); end i = i + 2; end diff --git a/libs/SensorThreshold/DerivedTag.m b/libs/SensorThreshold/DerivedTag.m index 63128c06..ac0b270d 100644 --- a/libs/SensorThreshold/DerivedTag.m +++ b/libs/SensorThreshold/DerivedTag.m @@ -592,7 +592,8 @@ function notifyListeners_(obj) ownArgs{end+1} = v; %#ok else error('DerivedTag:unknownOption', ... - 'Unknown NV key ''%s''.', k); + 'Unknown option ''%s''. Valid options: %s.', ... + k, strjoin([tagKeys, ownKeys], ', ')); end i = i + 2; end diff --git a/libs/SensorThreshold/MonitorTag.m b/libs/SensorThreshold/MonitorTag.m index 65f057b0..5bbe8c43 100644 --- a/libs/SensorThreshold/MonitorTag.m +++ b/libs/SensorThreshold/MonitorTag.m @@ -1223,7 +1223,8 @@ function fireEventsOnRisingEdges_(obj, px, bin) monArgs{end+1} = v; %#ok else error('MonitorTag:unknownOption', ... - 'Unknown option ''%s''.', k); + 'Unknown option ''%s''. Valid options: %s.', ... + k, strjoin([tagKeys, monKeys], ', ')); end end end diff --git a/libs/SensorThreshold/SensorTag.m b/libs/SensorThreshold/SensorTag.m index 4f4f4ff8..0761ca97 100644 --- a/libs/SensorThreshold/SensorTag.m +++ b/libs/SensorThreshold/SensorTag.m @@ -436,7 +436,8 @@ function notifyListeners_(obj) inlineY = v; else error('SensorTag:unknownOption', ... - 'Unknown option ''%s''.', k); + 'Unknown option ''%s''. Valid options: %s.', ... + k, strjoin([tagKeys, sensorKeys, {'X', 'Y'}], ', ')); end end end diff --git a/libs/SensorThreshold/StateTag.m b/libs/SensorThreshold/StateTag.m index 59553c14..6868f637 100644 --- a/libs/SensorThreshold/StateTag.m +++ b/libs/SensorThreshold/StateTag.m @@ -288,7 +288,8 @@ function notifyListeners_(obj) hasRs = true; else error('StateTag:unknownOption', ... - 'Unknown option ''%s''.', char(k)); + 'Unknown option ''%s''. Valid options: %s.', ... + char(k), strjoin([tagKeys, {'X', 'Y', 'RawSource'}], ', ')); end i = i + 2; end diff --git a/libs/SensorThreshold/Tag.m b/libs/SensorThreshold/Tag.m index 4cd28912..da2fa847 100644 --- a/libs/SensorThreshold/Tag.m +++ b/libs/SensorThreshold/Tag.m @@ -87,6 +87,8 @@ obj.Key = key; obj.Name = key; % default Name = Key + validKeys = {'Name', 'Units', 'Description', 'Labels', ... + 'Metadata', 'Criticality', 'SourceRef'}; for i = 1:2:numel(varargin) switch varargin{i} case 'Name', obj.Name = varargin{i+1}; @@ -98,7 +100,8 @@ case 'SourceRef', obj.SourceRef = varargin{i+1}; otherwise error('Tag:unknownOption', ... - 'Unknown option ''%s''.', varargin{i}); + 'Unknown option ''%s''. Valid options: %s.', ... + varargin{i}, strjoin(validKeys, ', ')); end end end From fed93842042b19bf8c8ca96c26f1c372a38aa8c4 Mon Sep 17 00:00:00 2001 From: Hannes Suhr Date: Wed, 24 Jun 2026 17:26:31 +0200 Subject: [PATCH 3/5] api: warn on unknown options in FastSense addThreshold/Band/Marker/Shaded MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Backward-compatible, additive: these four methods previously discarded misspelled/unknown name-value options silently (no error, no warning unless Verbose), so addThreshold(5, 'Colour', 'r') styled nothing and said nothing. That violated the project's own house convention — the FastSense constructor rejects unknown keys immediately "rather than silently discarding them" — and was inconsistent with addLine, which already errors on unknown keys. Each method now calls a new private helper warnUnknownOpts_ that WARNS (does not error) for each unrecognized option, naming the method and listing the valid options. A warning, not an error, was chosen so existing scripts keep working unchanged — they already ignored the option; now the mistake is merely visible. addFill is untouched: it legitimately forwards its extra options to addShaded. Proven: all four typos now warn with a helpful message; 0 false positives on valid + case-insensitive option calls; 48/48 pass across TestAddThreshold, TestAddBand, TestAddMarker, TestAddShaded, TestThresholdLabels, TestRender, plus 5/5 flat add-* function tests. Resolves audit finding F-5. Co-Authored-By: Claude Opus 4.8 --- docs/.api-usability/audit-2026-06-24.md | 24 +++++++++++++++++ libs/FastSense/FastSense.m | 12 ++++++--- libs/FastSense/private/warnUnknownOpts_.m | 33 +++++++++++++++++++++++ 3 files changed, 65 insertions(+), 4 deletions(-) create mode 100644 libs/FastSense/private/warnUnknownOpts_.m diff --git a/docs/.api-usability/audit-2026-06-24.md b/docs/.api-usability/audit-2026-06-24.md index 33f31dbc..40f45183 100644 --- a/docs/.api-usability/audit-2026-06-24.md +++ b/docs/.api-usability/audit-2026-06-24.md @@ -14,6 +14,7 @@ Friction was reproduced by running real code through the public API, not by read |---|---| | Entry points with helpful "valid options" validation | DashboardEngine ctor ✅, attachPlantLog ✅, `addWidget` type ✅; Tag ctors ✅ (all six fixed) | | Confusing / dead-end errors (no recovery path) | **0** open (was 1) | +| Methods that silently discard unknown options | **0** (was 4: addThreshold/Band/Marker/Shaded — now warn) | | Discoverability: `functionSignatures.json` for tab-completion | **absent** | | Lines-to-first-plot (example_basic) | ~4 (`FastSense` → `addLine` → `addThreshold` → `render`) — good | @@ -55,6 +56,29 @@ Friction was reproduced by running real code through the public API, not by read `MonitorTag:199` / `DerivedTag:218` were intentionally left untouched (they never see a user key — `monArgs`/`ownArgs` only hold already-recognized keys). +### 🟠 F-5 — `FastSense` add* methods silently discarded unknown options — ✅ RESOLVED (3rd run) +- **Where:** `FastSense.addThreshold`, `addBand`, `addMarker`, `addShaded` — each has a *closed* + option set but called `parseOpts(defaults, args, obj.Verbose)` and **discarded** the unmatched + output (`[parsed, ~]`). A misspelled option (`addThreshold(5, 'Colour', 'r')`, `addMarker(.., + 'Symbol','x')`) was silently dropped: no error, no warning (unless `Verbose`), and the intended + styling silently did nothing. (Found during this run — not in the original F-1..F-4 list.) +- **Why it matters:** silent failure is worse than a loud one, and it directly violated the project's + own *house convention* — the `FastSense` constructor's comment: *"closed option set, so reject + unknown/misspelled keys immediately rather than silently discarding them."* `addLine` and the + constructor already reject unknown keys; these four were the inconsistent holdouts. +- **Fix (additive, non-breaking):** new private helper `warnUnknownOpts_` warns (does **not** error) + for each unrecognized option, naming the method and listing valid options. Wired into the four + sites; scripts still run identically (the option was already being ignored — now the mistake is + visible). Error would have been breaking (silent→throw); a **warning** keeps existing scripts + working while surfacing the bug. `addFill` was left alone — it legitimately forwards its unmatched + options to `addShaded`. +- **Proof:** all four typos now warn (e.g. `addMarker: unknown option 'Symbol' ignored. Valid + options: Marker, MarkerSize, Color, Label`); **0 false positives** on valid + case-insensitive + calls; 48/48 pass across `TestAddThreshold`, `TestAddBand`, `TestAddMarker`, `TestAddShaded`, + `TestThresholdLabels`, `TestRender`, plus 5/5 flat add-* tests. +- **Future (breaking, report-only):** a major version could promote the warning to a hard error to + fully match the constructor's reject-immediately convention. + ### 🟢 F-3 — No `functionSignatures.json` (tab-completion discoverability) *(additive)* - **Where:** repo root / library roots — none present. - **Why it matters:** MATLAB editor tab-completion can't suggest `addWidget` types or `addThreshold` diff --git a/libs/FastSense/FastSense.m b/libs/FastSense/FastSense.m index fc98c7fc..d84ab098 100644 --- a/libs/FastSense/FastSense.m +++ b/libs/FastSense/FastSense.m @@ -655,7 +655,8 @@ function addThreshold(obj, varargin) defaults.Color = obj.Theme.ThresholdColor; defaults.LineStyle = obj.Theme.ThresholdStyle; defaults.Label = ''; - [parsed, ~] = parseOpts(defaults, nvPairs, obj.Verbose); + [parsed, unmatched] = parseOpts(defaults, nvPairs); + warnUnknownOpts_('addThreshold', unmatched, fieldnames(defaults)); t.Value = []; t.X = []; @@ -722,7 +723,8 @@ function addBand(obj, yLow, yHigh, varargin) defaults.FaceAlpha = obj.Theme.BandAlpha; defaults.EdgeColor = 'none'; defaults.Label = ''; - [parsed, ~] = parseOpts(defaults, varargin, obj.Verbose); + [parsed, unmatched] = parseOpts(defaults, varargin); + warnUnknownOpts_('addBand', unmatched, fieldnames(defaults)); b.YLow = yLow; b.YHigh = yHigh; @@ -784,7 +786,8 @@ function addMarker(obj, x, y, varargin) defaults.MarkerSize = 6; defaults.Color = obj.Theme.ThresholdColor; defaults.Label = ''; - [parsed, ~] = parseOpts(defaults, varargin, obj.Verbose); + [parsed, unmatched] = parseOpts(defaults, varargin); + warnUnknownOpts_('addMarker', unmatched, fieldnames(defaults)); m.X = x; m.Y = y; @@ -894,7 +897,8 @@ function addShaded(obj, x, y1, y2, varargin) defaults.FaceAlpha = 0.15; defaults.EdgeColor = 'none'; defaults.DisplayName = ''; - [parsed, ~] = parseOpts(defaults, varargin, obj.Verbose); + [parsed, unmatched] = parseOpts(defaults, varargin); + warnUnknownOpts_('addShaded', unmatched, fieldnames(defaults)); s.X = x; s.Y1 = y1; diff --git a/libs/FastSense/private/warnUnknownOpts_.m b/libs/FastSense/private/warnUnknownOpts_.m new file mode 100644 index 00000000..3e98334a --- /dev/null +++ b/libs/FastSense/private/warnUnknownOpts_.m @@ -0,0 +1,33 @@ +function warnUnknownOpts_(method, unmatched, validFields) +%WARNUNKNOWNOPTS_ Warn (non-fatally) for unrecognized name-value options. +% warnUnknownOpts_(METHOD, UNMATCHED, VALIDFIELDS) emits a +% FastSense:unknownOption warning for each field of the UNMATCHED struct +% returned by parseOpts, naming the calling METHOD and listing the +% VALIDFIELDS the method accepts. +% +% Closed-option-set FastSense methods (addThreshold, addBand, addMarker, +% addShaded) previously discarded misspelled options silently — e.g. +% addThreshold(5, 'Colour', 'r') did nothing and said nothing. This +% surfaces the mistake, mirroring the FastSense constructor's +% reject-unknown-keys house convention, but as a WARNING rather than an +% error so existing scripts keep running unchanged (backward-compatible). +% +% Inputs: +% method — char, the calling method name (for the message context) +% unmatched — struct of unrecognized name-value pairs (parseOpts output) +% validFields — cellstr of the option names the method accepts +% +% This is a private helper for FastSense. +% +% See also parseOpts, FastSense. + + if nargin < 2 || isempty(unmatched) + return; + end + keys = fieldnames(unmatched); + for i = 1:numel(keys) + warning('FastSense:unknownOption', ... + '%s: unknown option ''%s'' ignored. Valid options: %s', ... + method, keys{i}, strjoin(validFields, ', ')); + end +end From 8d7af2ce649626c3da10f575a599e27210e41f20 Mon Sep 17 00:00:00 2001 From: Hannes Suhr Date: Wed, 24 Jun 2026 18:02:38 +0200 Subject: [PATCH 4/5] feat: add functionSignatures.json for FastSense editor tab-completion MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Backward-compatible, additive: a new editor-metadata file with zero runtime effect — MATLAB reads it only for code completion, and Octave ignores it. No existing signature, behavior, or serialization changes. Adds libs/FastSense/functionSignatures.json covering the core plotting surface — the FastSense constructor plus addLine, addThreshold, addBand, addMarker, addShaded, setScale — with enumerated `choices` for the discrete options (Direction upper/lower, XScale/YScale linear/log, DownsampleMethod minmax/lttb, Marker, LineStyle). The editor now suggests valid option names and values as the user types, directly mitigating the option-typo friction. Proven: validateFunctionSignaturesJSON returns 0 messages (schema-valid; all method and type names resolve against the real class metadata with FastSense on the path); a runtime smoke (construct + add* + setScale) is clean with no error or warning; no .m files changed so existing tests and examples are inherently unaffected. Resolves audit finding F-3 for the FastSense scope (Dashboard/Tag signatures are queued follow-ups). Co-Authored-By: Claude Opus 4.8 --- docs/.api-usability/audit-2026-06-24.md | 34 ++++++--- libs/FastSense/functionSignatures.json | 97 +++++++++++++++++++++++++ 2 files changed, 121 insertions(+), 10 deletions(-) create mode 100644 libs/FastSense/functionSignatures.json diff --git a/docs/.api-usability/audit-2026-06-24.md b/docs/.api-usability/audit-2026-06-24.md index 40f45183..04e9f624 100644 --- a/docs/.api-usability/audit-2026-06-24.md +++ b/docs/.api-usability/audit-2026-06-24.md @@ -15,7 +15,7 @@ Friction was reproduced by running real code through the public API, not by read | Entry points with helpful "valid options" validation | DashboardEngine ctor ✅, attachPlantLog ✅, `addWidget` type ✅; Tag ctors ✅ (all six fixed) | | Confusing / dead-end errors (no recovery path) | **0** open (was 1) | | Methods that silently discard unknown options | **0** (was 4: addThreshold/Band/Marker/Shaded — now warn) | -| Discoverability: `functionSignatures.json` for tab-completion | **absent** | +| Discoverability: `functionSignatures.json` for tab-completion | **present for FastSense** (ctor + add* + setScale); Dashboard/Tag pending | | Lines-to-first-plot (example_basic) | ~4 (`FastSense` → `addLine` → `addThreshold` → `render`) — good | ## Findings (severity-ordered) @@ -79,11 +79,23 @@ Friction was reproduced by running real code through the public API, not by read - **Future (breaking, report-only):** a major version could promote the warning to a hard error to fully match the constructor's reject-immediately convention. -### 🟢 F-3 — No `functionSignatures.json` (tab-completion discoverability) *(additive)* -- **Where:** repo root / library roots — none present. -- **Why it matters:** MATLAB editor tab-completion can't suggest `addWidget` types or `addThreshold` - name-value options. A `functionSignatures.json` would make the public API self-describing in the - editor. Purely additive (new file). +### 🟢 F-3 — No `functionSignatures.json` (tab-completion discoverability) — ✅ RESOLVED (4th run, FastSense scope) +- **Where:** library roots had none — MATLAB editor tab-completion couldn't suggest any public-API + name-value option. +- **Fix (additive, new file — zero runtime change, ignored by Octave):** added + `libs/FastSense/functionSignatures.json` covering the core plotting surface — the `FastSense` + constructor plus `addLine`, `addThreshold`, `addBand`, `addMarker`, `addShaded`, `setScale` — with + enumerated `choices` for the discrete options (`Direction` upper/lower, `XScale`/`YScale` + linear/log, `DownsampleMethod` minmax/lttb, `Marker`, `LineStyle`). The editor now offers these as + you type. +- **Proof:** `validateFunctionSignaturesJSON` returns **0 messages** (schema-valid, all method/type + names resolve against the real class metadata with FastSense on the path); runtime smoke + (construct + add* + setScale) clean — no error, no warning; no `.m` changed so existing + tests/examples are inherently unaffected. +- **Remaining (additive follow-ups):** `libs/Dashboard/functionSignatures.json` for + `DashboardEngine.addWidget` (the `type` arg as a `choices` enum — the single highest-value + completion, directly preventing the run-1 wrong-type trap at typing time) and + `libs/SensorThreshold/functionSignatures.json` for the Tag constructors. ### 🟢 F-4 — Example bootstrap boilerplate *(minor / informational)* - Every example repeats `projectRoot = fileparts(fileparts(fileparts(mfilename('fullpath'))));` @@ -91,7 +103,9 @@ Friction was reproduced by running real code through the public API, not by read friction, so it is informational only — no API change warranted. ## Next-worst additive finding for `/api-polish` -**F-3** — add a `functionSignatures.json` so the MATLAB editor can tab-complete the public API -(`addWidget` types, `addThreshold` / Tag-constructor name-value options). Purely additive (new file); -the main design question is scope (which classes/methods to cover first — likely `DashboardEngine.addWidget` -and the `FastSense` `add*` family, since those have the richest name-value surfaces). +**F-3b** — add `libs/Dashboard/functionSignatures.json` for `DashboardEngine.addWidget`, modeling the +`type` argument as a `choices` enum of the registered widget types (derived from the same set +`DashboardWidgetRegistry.types()` returns). This is the single highest-value completion: it surfaces +the valid widget types *as the user types*, complementing the run-1 error-message fix. Purely +additive (new file). After that, a Tag-constructor `functionSignatures.json` under +`libs/SensorThreshold/`. diff --git a/libs/FastSense/functionSignatures.json b/libs/FastSense/functionSignatures.json new file mode 100644 index 00000000..ca0e7133 --- /dev/null +++ b/libs/FastSense/functionSignatures.json @@ -0,0 +1,97 @@ +{ + "_schemaVersion": "1.0.0", + + "FastSense.FastSense": { + "inputs": [ + {"name": "Parent", "kind": "namevalue", "type": ["matlab.graphics.axis.Axes"]}, + {"name": "LinkGroup", "kind": "namevalue"}, + {"name": "Theme", "kind": "namevalue", "type": ["choices={'light','dark'}"]}, + {"name": "Verbose", "kind": "namevalue", "type": ["logical", "scalar"]}, + {"name": "XScale", "kind": "namevalue", "type": ["choices={'linear','log'}"]}, + {"name": "YScale", "kind": "namevalue", "type": ["choices={'linear','log'}"]}, + {"name": "DefaultDownsampleMethod","kind": "namevalue", "type": ["choices={'minmax','lttb'}"]}, + {"name": "LiveInterval", "kind": "namevalue", "type": ["numeric", "scalar"]}, + {"name": "HoverCrosshair", "kind": "namevalue", "type": ["logical", "scalar"]}, + {"name": "MinPointsForDownsample", "kind": "namevalue", "type": ["numeric", "scalar"]}, + {"name": "DownsampleFactor", "kind": "namevalue", "type": ["numeric", "scalar"]}, + {"name": "PyramidReduction", "kind": "namevalue", "type": ["numeric", "scalar"]}, + {"name": "StorageMode", "kind": "namevalue", "type": ["char"]}, + {"name": "MemoryLimit", "kind": "namevalue", "type": ["numeric", "scalar"]} + ] + }, + + "FastSense.addLine": { + "inputs": [ + {"name": "obj", "kind": "required", "type": ["FastSense"]}, + {"name": "x", "kind": "required", "type": ["numeric"]}, + {"name": "y", "kind": "required", "type": ["numeric"]}, + {"name": "DownsampleMethod", "kind": "namevalue", "type": ["choices={'minmax','lttb'}"]}, + {"name": "AssumeSorted", "kind": "namevalue", "type": ["logical", "scalar"]}, + {"name": "HasNaN", "kind": "namevalue", "type": ["logical", "scalar"]}, + {"name": "XType", "kind": "namevalue", "type": ["choices={'numeric','datenum'}"]}, + {"name": "Metadata", "kind": "namevalue", "type": ["struct"]}, + {"name": "Color", "kind": "namevalue"}, + {"name": "LineStyle", "kind": "namevalue", "type": ["choices={'-','--',':','-.','none'}"]}, + {"name": "LineWidth", "kind": "namevalue", "type": ["numeric", "scalar"]}, + {"name": "DisplayName", "kind": "namevalue", "type": ["char"]}, + {"name": "Marker", "kind": "namevalue"} + ] + }, + + "FastSense.addThreshold": { + "inputs": [ + {"name": "obj", "kind": "required", "type": ["FastSense"]}, + {"name": "value", "kind": "required", "type": ["numeric"]}, + {"name": "Direction", "kind": "namevalue", "type": ["choices={'upper','lower'}"]}, + {"name": "ShowViolations", "kind": "namevalue", "type": ["logical", "scalar"]}, + {"name": "Color", "kind": "namevalue"}, + {"name": "LineStyle", "kind": "namevalue", "type": ["choices={'-','--',':','-.'}"]}, + {"name": "Label", "kind": "namevalue", "type": ["char"]} + ] + }, + + "FastSense.addBand": { + "inputs": [ + {"name": "obj", "kind": "required", "type": ["FastSense"]}, + {"name": "yLow", "kind": "required", "type": ["numeric", "scalar"]}, + {"name": "yHigh", "kind": "required", "type": ["numeric", "scalar"]}, + {"name": "FaceColor", "kind": "namevalue"}, + {"name": "FaceAlpha", "kind": "namevalue", "type": ["numeric", "scalar"]}, + {"name": "EdgeColor", "kind": "namevalue"}, + {"name": "Label", "kind": "namevalue", "type": ["char"]} + ] + }, + + "FastSense.addMarker": { + "inputs": [ + {"name": "obj", "kind": "required", "type": ["FastSense"]}, + {"name": "x", "kind": "required", "type": ["numeric"]}, + {"name": "y", "kind": "required", "type": ["numeric"]}, + {"name": "Marker", "kind": "namevalue", "type": ["choices={'o','+','*','.','x','s','d','^','v'}"]}, + {"name": "MarkerSize", "kind": "namevalue", "type": ["numeric", "scalar"]}, + {"name": "Color", "kind": "namevalue"}, + {"name": "Label", "kind": "namevalue", "type": ["char"]} + ] + }, + + "FastSense.addShaded": { + "inputs": [ + {"name": "obj", "kind": "required", "type": ["FastSense"]}, + {"name": "x", "kind": "required", "type": ["numeric"]}, + {"name": "y1", "kind": "required", "type": ["numeric"]}, + {"name": "y2", "kind": "required", "type": ["numeric"]}, + {"name": "FaceColor", "kind": "namevalue"}, + {"name": "FaceAlpha", "kind": "namevalue", "type": ["numeric", "scalar"]}, + {"name": "EdgeColor", "kind": "namevalue"}, + {"name": "DisplayName", "kind": "namevalue", "type": ["char"]} + ] + }, + + "FastSense.setScale": { + "inputs": [ + {"name": "obj", "kind": "required", "type": ["FastSense"]}, + {"name": "XScale", "kind": "namevalue", "type": ["choices={'linear','log'}"]}, + {"name": "YScale", "kind": "namevalue", "type": ["choices={'linear','log'}"]} + ] + } +} From 90d0579f4c7b73b2b0c5548fe62fa9cf05b9e51d Mon Sep 17 00:00:00 2001 From: Hannes Suhr Date: Wed, 24 Jun 2026 18:36:22 +0200 Subject: [PATCH 5/5] feat: add functionSignatures.json for DashboardEngine editor tab-completion MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Backward-compatible, additive: a new editor-metadata file with zero runtime effect — MATLAB reads it only for code completion, Octave ignores it. No existing signature, behavior, or serialization changes. Adds libs/Dashboard/functionSignatures.json covering the DashboardEngine constructor (Name + Theme/LiveInterval/InfoFile/ProgressMode/ ShowTimePanel) and addWidget. The addWidget `type` argument is modeled as a `choices` enum of all 19 built-in widget types, so the editor now suggests the valid types as the user types — surfacing the same list the run-1 unknown-type error lists, but at the point of entry. Common options (Position, Title, Tag, Label) are offered too. Proven: validateFunctionSignaturesJSON returns 0 messages (schema-valid; the optional positional Name uses the R2025b-required "ordered" kind, not the deprecated "optional"); runtime smoke confirms addWidget('number') still returns a NumberWidget and an unknown type still errors DashboardEngine:unknownType — the file is runtime-inert. No .m changed, so existing tests and examples are unaffected. Resolves audit finding F-3b. Co-Authored-By: Claude Opus 4.8 --- docs/.api-usability/audit-2026-06-24.md | 26 ++++++++++++++++++------- libs/Dashboard/functionSignatures.json | 25 ++++++++++++++++++++++++ 2 files changed, 44 insertions(+), 7 deletions(-) create mode 100644 libs/Dashboard/functionSignatures.json diff --git a/docs/.api-usability/audit-2026-06-24.md b/docs/.api-usability/audit-2026-06-24.md index 04e9f624..152a980f 100644 --- a/docs/.api-usability/audit-2026-06-24.md +++ b/docs/.api-usability/audit-2026-06-24.md @@ -15,7 +15,7 @@ Friction was reproduced by running real code through the public API, not by read | Entry points with helpful "valid options" validation | DashboardEngine ctor ✅, attachPlantLog ✅, `addWidget` type ✅; Tag ctors ✅ (all six fixed) | | Confusing / dead-end errors (no recovery path) | **0** open (was 1) | | Methods that silently discard unknown options | **0** (was 4: addThreshold/Band/Marker/Shaded — now warn) | -| Discoverability: `functionSignatures.json` for tab-completion | **present for FastSense** (ctor + add* + setScale); Dashboard/Tag pending | +| Discoverability: `functionSignatures.json` for tab-completion | **FastSense** (ctor + add* + setScale) + **Dashboard** (ctor + addWidget type enum); Tag pending | | Lines-to-first-plot (example_basic) | ~4 (`FastSense` → `addLine` → `addThreshold` → `render`) — good | ## Findings (severity-ordered) @@ -102,10 +102,22 @@ Friction was reproduced by running real code through the public API, not by read + `run(fullfile(projectRoot,'install.m'))`. This is example-script bootstrap, not public-API friction, so it is informational only — no API change warranted. +### 🟢 F-3b — `DashboardEngine.addWidget` had no type tab-completion — ✅ RESOLVED (5th run) +- **Fix (additive, new file — zero runtime change):** added `libs/Dashboard/functionSignatures.json` + covering the `DashboardEngine` constructor (Name + Theme/LiveInterval/InfoFile/ProgressMode/ + ShowTimePanel) and `addWidget`, with the `type` argument modeled as a `choices` enum of all 19 + built-in widget types — so the editor now lists the valid types *as you type*, complementing the + run-1 error-message fix at the point of entry. Common `addWidget` options (Position, Title, Tag, + Label) are offered too. +- **Proof:** `validateFunctionSignaturesJSON` returns **0 messages** (after switching the optional + positional `Name` from the deprecated `"optional"` kind to `"ordered"`, which R2025b now requires); + runtime smoke confirms `addWidget('number', ...)` still returns a `NumberWidget` and an unknown + type still errors `DashboardEngine:unknownType` — the metadata file is runtime-inert. + ## Next-worst additive finding for `/api-polish` -**F-3b** — add `libs/Dashboard/functionSignatures.json` for `DashboardEngine.addWidget`, modeling the -`type` argument as a `choices` enum of the registered widget types (derived from the same set -`DashboardWidgetRegistry.types()` returns). This is the single highest-value completion: it surfaces -the valid widget types *as the user types*, complementing the run-1 error-message fix. Purely -additive (new file). After that, a Tag-constructor `functionSignatures.json` under -`libs/SensorThreshold/`. +**F-3c** — add `libs/SensorThreshold/functionSignatures.json` for the Tag constructors +(`SensorTag`, `StateTag`, `MonitorTag`, `CompositeTag`, `DerivedTag`), completing the +tab-completion coverage for the third core public surface. The valid name-value keys are already +enumerated per class (from the run-2 work) — `Name`/`Units`/`Description`/`Labels`/`Metadata`/ +`Criticality`/`SourceRef` universals plus each class's extras (e.g. SensorTag `ID`/`Source`/`X`/`Y`, +MonitorTag `MinDuration`/`Persist`/…). Purely additive (new file). diff --git a/libs/Dashboard/functionSignatures.json b/libs/Dashboard/functionSignatures.json new file mode 100644 index 00000000..46640379 --- /dev/null +++ b/libs/Dashboard/functionSignatures.json @@ -0,0 +1,25 @@ +{ + "_schemaVersion": "1.0.0", + + "DashboardEngine.DashboardEngine": { + "inputs": [ + {"name": "Name", "kind": "ordered", "type": ["char"]}, + {"name": "Theme", "kind": "namevalue", "type": ["choices={'light','dark'}"]}, + {"name": "LiveInterval", "kind": "namevalue", "type": ["numeric", "scalar"]}, + {"name": "InfoFile", "kind": "namevalue", "type": ["char"]}, + {"name": "ProgressMode", "kind": "namevalue", "type": ["choices={'auto','on','off'}"]}, + {"name": "ShowTimePanel", "kind": "namevalue", "type": ["logical", "scalar"]} + ] + }, + + "DashboardEngine.addWidget": { + "inputs": [ + {"name": "obj", "kind": "required", "type": ["DashboardEngine"]}, + {"name": "type", "kind": "required", "type": ["choices={'barchart','chipbar','divider','fastsense','gauge','group','heatmap','histogram','iconcard','image','multistatus','number','rawaxes','scatter','sparkline','status','table','text','timeline'}"]}, + {"name": "Position", "kind": "namevalue", "type": ["numeric", "size=1,4"]}, + {"name": "Title", "kind": "namevalue", "type": ["char"]}, + {"name": "Tag", "kind": "namevalue"}, + {"name": "Label", "kind": "namevalue", "type": ["char"]} + ] + } +}