Add strictNullChecks ratchet + fix the core service layer (#6)#208
Conversation
Enabling strictNullChecks surfaces 959 latent null-safety issues across the app sources (issue #6). Rather than flip the flag globally and fix all at once, lock today's error set as a betterer baseline and fail CI only on new issues, so the count ratchets down incrementally per PR. - tsconfig.strict.json: base config + strictNullChecks for local `npm run snc` - tsconfig.betterer.json: commonjs config so ts-node can load .betterer.ts (the app's bundler moduleResolution is incompatible with commonjs) - .betterer.results: committed baseline snapshot (959 issues) - CI: strict-null-checks job runs `betterer:ci` Refs #6 Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Fix all 116 strictNullChecks errors across the core service layer and the two shared Signal K interfaces, ratcheting the betterer baseline 959 -> 843. Types are made honest, not silenced -- no non-null assertions, casts, or suppressions. Where a value is genuinely nullable (SK paths emit null on timeout/sensor loss; config may be absent) the type is widened and the null handled at use; lazy initialisers get real defaults instead. - SK pipeline: widen ISkPathData.type/pathTimestamp and make ISkMetadata.units optional to match real delta/meta ordering; endpoint/version/request fields widened to their genuine pre-connect null states, with explicit throws where a missing HTTP body would otherwise fail obscurely. - Config/persistence: configuration-upgrade now skips-and-logs configs missing their app/theme section instead of throwing TypeError on real device data; lying non-null return types on transformApp/transformTheme corrected. - Dashboards/app: screensPayload keeps its undefined/null "clear" sentinel honestly; toSignal reads guard the undefined-before-first-emit case; nav fallbacks preserve the prior numeric-coercion behaviour. Two out-of-scope call sites (data-inspector, path-control-config) picked up the widened SK types and are handled in kind; path-control-config's guard fixes a latent compare(null) crash the honest typing surfaced. Verified: npm run lint, npm run build:dev (AOT/strictTemplates) and betterer:ci all pass; no file regressed vs the baseline. Refs #6 Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The strictNullChecks slice guarded `processWebsocketMessage` by dropping any updates message whose `context` isn't a string. That changed behaviour: a characterization test (`updates win`) sends a context-less updates message and expects it processed, and real prior behaviour passed `undefined` context straight through to `setPathContext`. Restore the exact behaviour and keep the types honest: `context` is optional (`IMeta`/`IPathValueData` already document "empty context assumes Self"), so widen the context type end-to-end (`parseUpdates`, `parseSkMeta`, `IPathValueData.context`, `IMeta.context`, `setPathContext`) and pass the value through unchanged. No new strictNullChecks issues; the widening is contained to the core data path. Refs #6
Code review — PR #208 (strictNullChecks ratchet + core service layer)Scope: 17 changed source files + 4 infra files vs Intent: enable Headline: the ratchet mechanism and the type-safety work are sound — correctness and api-contract both returned clean, and every widening was confirmed to match pre-existing runtime nullability (these are internal interfaces, not an external API). The findings are two behavior divergences introduced by over-eager agent "improvements", a documentation/consistency gap in the ratchet infra, and a pervasive lack of tests for the behavior this PR made explicit. P2 — should fix
P3 — fix if straightforward
Testing (cross-cutting)The PR adds zero tests, and the delta message-drop regression (caught by vitest, not type-checking) already proved the ratchet guards types, not behavior. Behavior this PR made explicit is uncovered: Pre-existing (action required)
Coverage
Verdict: Ready with fixes. The ratchet and type-safety goal are solid; nothing here undermines them. Before merge: decide on the two behavior divergences (#1 |
Review follow-ups (#208): - betterer's glob tracked src/test.ts, which tsconfig.strict.json (and npm run snc) exclude; align the exclude so both tools check the same set. Baseline 843 -> 842 (the one test-setup issue leaves scope). - tsconfig.strict.json is now the single source of the strict compiler options (dropped the redundant duplication in .betterer.ts). - Document the ratchet + the content-hash baseline-regeneration footgun in CLAUDE.md (regenerate after fixing a file or merging main). Refs #6
…cy path Review follow-up (#208): the version-less legacy upgrade path could wedge the app behind the upgrade overlay. Its outer catch pushed an error but never reset upgrading() (unlike the v11/v12 paths), and transformConfig dereferenced config.app unguarded, throwing a TypeError on a partial/corrupt legacy slot. Reset the flag in the catch and skip app-less slots. Refs #6
Review follow-up (#208): the ratchet guards types, not behavior (a delta regression slipped past type-checking and was caught only by vitest). Add characterization tests for the behavior this slice made explicit: - signalk-connection: processEndpointResponse returns operation-2 for a well-formed response and throws (fail-loud) on a missing body or absent v1 WebSocket URL (new spec). - configuration-upgrade: legacy path clears the overlay on a listing failure, skips an app-less slot without crashing, and startFresh skips an app-less slot while still retiring the rest. Also clear resetSettings between tests. - signalk-delta: the no-context 'updates win' message emits context: undefined. Refs #6
Review findings addressed (commits
|
First slice of #6. Enabling
strictNullCheckssurfaces 959 latent null-safety issues across the app — too many to fix in one PR, and most concentrated in the ~46 widgets. This PR builds an incremental ratchet and clears the highest-value slice: the core service layer, where Signal K null actually flows.Approach
strictNullChecksstays off in the app build (flipping it globally would fail on 959 errors). Instead:tsconfig.strict.jsonenables the flag fornpm run sncand for the ratchet..betterer.resultsbaseline and fails CI (strict-null-checksjob) only on new issues. The count ratchets down per PR; nothing can regress.tsconfig.betterer.jsongives ts-node a commonjs config so it can load.betterer.ts(the app'sbundlermoduleResolution is incompatible with the commonjs ts-node forces).This slice: core service layer (116 fixed, 959 → 843)
Types are made honest, not silenced — zero non-null assertions, casts, or suppressions. Genuinely-nullable values get widened types + handling at use; lazy initialisers get real defaults.
ISkPathData.type/pathTimestampand makeISkMetadata.unitsoptional to match real delta/meta ordering; endpoint/version/request fields typed to their genuine pre-connect null states, with explicit throws where a missing HTTP body would otherwise fail obscurely.ConfigurationUpgradeServicenow skips-and-logs configs missing their app/theme section instead of throwingTypeErroron real device data; lying non-null return types corrected.screensPayloadkeeps itsundefined/null"clear" sentinel honestly;toSignalreads guard the undefined-before-first-emit case; nav fallbacks preserve prior numeric-coercion behaviour.Two out-of-scope call sites (
data-inspector,path-control-config) picked up the widened SK types and are handled in kind —path-control-config's guard fixes a latentcompare(null)crash the honest typing surfaced.Remaining (follow-up PRs under #6)
The ~46 widgets and remaining components (843 issues) migrate in later slices, each ratcheting the baseline down.
widget-data-chart(164) andwidget-windtrends-chart(100) dominate.Verification
npm run lint,npm run build:dev(AOT +strictTemplates), andbetterer:ciall pass. No file regressed vs the baseline.Refs #6