Skip to content

strictNullChecks slice 2: instrument widgets + context-less misfiling fix (#6, #209)#210

Merged
mairas merged 3 commits into
mainfrom
feat/strictnullchecks-widgets-1
Jul 5, 2026
Merged

strictNullChecks slice 2: instrument widgets + context-less misfiling fix (#6, #209)#210
mairas merged 3 commits into
mainfrom
feat/strictnullchecks-widgets-1

Conversation

@mairas

@mairas mairas commented Jul 5, 2026

Copy link
Copy Markdown
Contributor

Slice 2 of the strictNullChecks migration (#6), plus the folded-in #209 fix you OK'd.

Widget slice (268 issues, betterer baseline 842 → 574)

The mechanical instrument-widget families, migrated by three Sonnet agents on disjoint files under hard guardrails (no !, casts, any, or suppressions — honest type widening + real guards only). I reconciled, scanned every diff, and reviewed the null-semantics.

  • gauges — gauge-steel, gauge-ng-linear, gauge-ng-radial, simple-linear: optional inputs typed T | undefined; theme() guarded before deref; cfg/theme effects early-return during the pre-merge startup window.
  • boolean SVG — button/switch/light: data/theme inputs and colour fields widened to their real null defaults (colour fields feed [attr.*] bindings, which accept null); guards mirror the parent widget.
  • numeric/slider — SK values widened to number | null and skipped (not coerced) when null, so a timeout/sensor-loss null can't corrupt tracked min/max or reach .toFixed(); canvas context threaded as a narrowed local.

Two edge-case behavior fixes (deliberate — flagging per behavior-preservation)

The honest typing surfaced two genuine bugs; both are consciously fixed, not preserved:

  • gauge-steel zone bounds: a zone with an undefined bound previously produced a NaN steelseries section bound (the !== null clamp missed undefinedconvertToUnit(units, undefined) → NaN). Now clamps to min/max, as the adjacent comment always intended. Real-bounded zones are unchanged.
  • minichart.verticalChart: typed boolean | null (was inferred null) so widget-numeric can assign it. Null default and truthy usage unchanged — a one-line cross-file unblock.

#209 — context-less values misfiled

DataService.setPathContext filed a delta value with an empty/absent context under a literal "undefined.<path>" key, so widgets subscribed to self.<path> never saw it. Folded empty/undefined context to the self root (per the ISkPathData/IMeta "empty context assumes Self" contract). Foreign contexts still route to their own root. Two end-to-end DataService tests pin both directions.

Coverage note

The touched widgets have no unit specs (canvas rendering isn't unit-testable and none existed). Validated via AOT build (strictTemplates over every widened binding), lint, and betterer:ci. The #209 fix has real tests.

Remaining under #6

Charts — widget-data-chart (164) and widget-windtrends-chart (100) — are the least mechanical and stay for slice 3, along with widget-autopilot (65) and the remaining components.

Verification

npm run lint, npm run build:dev (AOT/strictTemplates), and betterer:ci (574) all pass; no file regressed vs the baseline; data.service specs pass locally.

Refs #6
Closes #209

mairas added 2 commits July 5, 2026 20:04
setPathContext filed a delta value with an empty/absent context under a literal
"undefined.<path>" key, so widgets subscribed to self.<path> never saw it. Fold
empty/undefined context to the self root, matching the ISkPathData/IMeta "empty
context assumes Self" contract. Foreign contexts still route to their own root.

Closes #209
Slice 2 of #6: the gauge, boolean-SVG, and numeric/slider widget families.
268 issues fixed, betterer baseline 842 -> 574. Honest type widening only —
no non-null assertions, casts, or suppressions.

- gauges (gauge-steel, gauge-ng-linear, gauge-ng-radial, simple-linear):
  optional inputs typed T | undefined; theme() guarded before deref; cfg/theme
  effects early-return during the pre-merge startup window.
- boolean SVG (button/switch/light): data/theme inputs and colour fields
  widened to match their real null defaults; guards mirror the parent widget.
- numeric/slider: SK values widened to number | null and skipped (not
  coerced) when null, so a timeout/sensor-loss null can't corrupt tracked
  min/max or reach .toFixed(); canvas context threaded as a narrowed local.

Two edge-case behaviour fixes the honest typing surfaced (both flagged):
- gauge-steel: a zone with an undefined bound previously produced a NaN
  section bound (the `!== null` clamp missed undefined -> convertToUnit -> NaN);
  now clamps to min/max, as the adjacent comment always intended.
- minichart.verticalChart: typed `boolean | null` (was inferred `null`) so
  widget-numeric can assign it; null default and truthy usage unchanged.

These widgets have no unit specs (canvas rendering); validated via AOT build
(strictTemplates), lint, and betterer. No file regressed vs the baseline.

Refs #6
@mairas

mairas commented Jul 5, 2026

Copy link
Copy Markdown
Contributor Author

Code review — PR #210 (strictNullChecks slice 2: instrument widgets + #209)

Scope: 11 source files + a spec vs main (base 027c6bdd). Mode: interactive, 5 persona reviewers (correctness, adversarial, testing, maintainability, project-standards), each in its own fresh context. Security/api-contract/reliability were not selected — this display-widget diff touches no auth, contract, or error-handling surface.

Headline: no bugs — every null-guard replaces a path where the original code dereferenced undefined and crashed, so the guarded paths are strictly safer, and fallback values were confirmed to match each widget's own DEFAULT_CONFIG. #209 was verified unbreakable (adversarial tried to construct misrouting and couldn't). The findings are four behavior deltas the null-safety work pulled in (two flagged in the PR, two more found here) that need conscious sign-off, one scope-creep config change to revert, and the expected test gap.

P2 — behavior changes to accept or revert

# File Change Reviewers Conf
1 gauge-steel.component.ts Open-ended metadata zones now render as full colored bands. A zone with an unset bound + a recognized unit produced an invisible/NaN steelseries section on base; it now clamps to min/max and paints a band (e.g. a battery low-alarm {upper: 11.5} now shows a red band from gauge-floor→11.5). Open-ended zones are the common SK alarm case, so this is visible on real configs. Also changes zone sort order (base's NaN comparator was non-deterministic). Correct/safety-relevant (alarm zones were silently invisible), but not behavior-preserving. adversarial (0.80), correctness (0.85) 0.85
2 widget-numeric.component.ts Min/Max survives null sensor dropouts. Base wiped Min or Max (by sign) to -- on every null delta (null < 30 < 3 → true → minValue = null); the new if (dataValue !== null) skip preserves the tracked extremes. More correct, but the Min/Max readout visibly differs after any dropout. correctness (0.90), adversarial (0.82), testing (0.82) 0.90
3 widget-numeric / gauge-steel No tests for #1 and #2. The widgets ship no specs; these are exactly the class of behavior change that slipped past type-checking last slice and was caught only by vitest. #209 has good coverage; these don't. testing (0.82), correctness, adversarial, maintainability 0.82
4 widget-simple-linear / widget-gauge-ng-* displayScale fallback solved three ways. widget-slider centralizes it in getScaleBounds(); simple-linear and the gauge-ng widgets inline magic literals (?? 15, ?? 100) that duplicate each widget's DEFAULT_CONFIG.displayScale. Values verified correct today, but nothing links them — editing the config leaves the fallbacks stale. maintainability (0.72) 0.72

P3 — smaller

# File Issue Reviewers Conf
5 widget-slider.component.ts Scope creep: convertUnitTo: null deleted from DEFAULT_CONFIG.paths.gaugePath. IWidgetPath.convertUnitTo is typed `string null`, so strictNullChecks did not require it, and the streams directive treats null/undefined identically (inert). An unrelated shipped-default change in a type-safety PR. → revert. maintainability (0.65), project-standards
6 gauge-steel.component.ts theme-null divergence: most widgets early-return if (!theme) return; gauge-steel keeps rendering with a transparent ?? "rgba(0,0,0,0)" fallback. Arguably intentional (draw zones without a theme) but undocumented. → one-line note. maintainability (0.60) 0.60

Verified clean (adversarial + correctness)

#209 setPathContext fold — foreign/self routing identical to base; only empty/undefined context changes (the intended fix); _selfUrn-mutation path preserved. minichart verticalChart — pure type widening. setMiniChart ?? 1/0/10 — matches DEFAULT_CONFIG. boolean-SVG guards — replace crashes only. No any/!/casts/suppressions; version-bump exempt; serving-path untouched.

Coverage

5/5 reviewers returned. No P0/P1. Suppressed: a couple of pre-existing awareness notes (cfg.paths?.['x'].y guards paths not the entry — pre-existing). Residual: setMiniChart differs only for configs omitting numDecimal/yScale* (low likelihood; fallbacks are the sensible defaults).


Verdict: Ready with decisions. No defects. Decide whether to keep the two behavior improvements (#1 gauge zones, #2 numeric min/max) — both are more correct and #1 is safety-relevant (visible alarm zones) — or preserve base behavior and file them separately. Revert the scope-creep config change (#5). Tests for #1/#2 recommended (extract the numeric min/max reducer to a pure function is the cheap path; gauge-steel zones via a steelseries.Section spy).

…behaviour

Review follow-ups (#210): the two behaviour changes this slice made — kept as
deliberate improvements — now have tests.

- widget-numeric: extract the running min/max fold into a pure reduceMinMax()
  util and unit-test it, including the regression guard that a null sample
  (sensor dropout) preserves the tracked extremes instead of wiping them.
- gauge-steel: a spec drives buildOptions() with an open-ended low-alarm zone
  and asserts it renders as a band clamped to the gauge minimum (was a NaN
  section that did not draw). Also a one-line note on why zones render before
  the theme loads, unlike the other widgets.

Refs #6
@mairas

mairas commented Jul 5, 2026

Copy link
Copy Markdown
Contributor Author

Review addressed (commit d66b2141)

Decision: kept both behavior improvements, with tests (they're more correct; the gauge fix makes marine alarm zones visible).

All four behavior deltas (two were flagged in the PR, two surfaced by review — now all documented):

  1. gauge-steel: open-ended metadata zones (unset bound) render as a band clamped to min/max instead of an invisible NaN section.
  2. gauge-steel: zone sort order is now deterministic (base's a.lower - b.lower was NaN for unset lowers).
  3. widget-numeric: Min/Max survive null sensor dropouts (base wiped min-or-max to -- on every null via null < x0 < x).
  4. minichart.verticalChart: boolean | null type (pure type widening, no runtime change).

Tests added:

  • reduceMinMax() extracted to a pure util + unit test, including the null-preserves-extremes regression guard.
  • gauge-steel spec drives buildOptions() with an open-ended low-alarm zone and asserts the clamped band Section(10, 11.5, …).

Finding #5 (convertUnitTo scope creep) — dismissed after verification. Restoring convertUnitTo: null breaks the build: the governing IWidgetPath.convertUnitTo is typed string (the string | null at widgets-interface.ts:386 is a different interface). The agent's removal was a required strict fix, not scope creep.

Deferred: finding #4 (three widgets inline ?? 15/?? 100 displayScale fallbacks vs slider's getScaleBounds() helper). Values verified to match each widget's DEFAULT_CONFIG today; it's a drift-risk, not a bug, so consolidating is left out to keep this PR scoped. #6 theme-null divergence got a one-line note on gauge-steel.

All gates green: lint, build:dev (AOT/strictTemplates), betterer:ci (574), and the affected specs (numeric-util, gauge-steel, data.service — 21 tests).

@mairas mairas merged commit 9b6599a into main Jul 5, 2026
8 checks passed
@mairas mairas deleted the feat/strictnullchecks-widgets-1 branch July 5, 2026 18:33
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.

DataService misfiles context-less delta values under an "undefined.<path>" key

1 participant