Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 11 additions & 0 deletions .clang-tidy
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
Checks: >
-*,
bugprone-*,
performance-*,
-performance-enum-size,
portability-*,
modernize-use-nullptr,
modernize-use-override
WarningsAsErrors: '*'
HeaderFilterRegex: '^(include|src)/spc/|/spc-cpp/(include|src)/'
FormatStyle: file
85 changes: 80 additions & 5 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ jobs:
build-linux:
runs-on: ubuntu-24.04
steps:
- uses: actions/checkout@v6
- uses: actions/checkout@v7.0.1

- name: Install dependencies
run: |
Expand All @@ -20,26 +20,29 @@ jobs:
libcurl4-openssl-dev

- name: Build
run: make build
run: make build CMAKE_ARGS=-DSPC_WARNINGS_AS_ERRORS=ON

- name: Test
run: make test

- name: Verify fixture provenance
run: make fixtures-check

- name: Lint
run: make lint

build-macos:
runs-on: macos-latest
steps:
- uses: actions/checkout@v6
- uses: actions/checkout@v7.0.1

- name: Install dependencies
run: |
brew install pkg-config curl clang-format
echo "CMAKE_PREFIX_PATH=$(brew --prefix curl)" >> "$GITHUB_ENV"

- name: Build
run: make build
run: make build CMAKE_ARGS=-DSPC_WARNINGS_AS_ERRORS=ON

- name: Test
run: make test
Expand All @@ -50,5 +53,77 @@ jobs:
markdown-lint:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v6
- uses: actions/checkout@v7.0.1
- uses: DavidAnson/markdownlint-cli2-action@v23

sanitizer:
runs-on: ubuntu-24.04
steps:
- uses: actions/checkout@v7.0.1
- name: Install dependencies
run: |
sudo apt-get update
sudo apt-get install -y --no-install-recommends \
build-essential cmake libcurl4-openssl-dev
- name: Build with address and undefined behavior sanitizers
run: >-
cmake -S . -B build-sanitized
-DCMAKE_BUILD_TYPE=Debug
-DSPC_ENABLE_LTO=OFF
-DSPC_ENABLE_SANITIZERS=ON
-DSPC_BUILD_EXAMPLES=OFF
- run: cmake --build build-sanitized --parallel
- run: ctest --test-dir build-sanitized --output-on-failure

thread-sanitizer:
runs-on: ubuntu-24.04
steps:
- uses: actions/checkout@v7.0.1
- name: Install dependencies
run: |
sudo apt-get update
sudo apt-get install -y --no-install-recommends \
build-essential cmake libcurl4-openssl-dev
- name: Build with ThreadSanitizer
run: >-
cmake -S . -B build-thread-sanitized
-DCMAKE_BUILD_TYPE=Debug
-DSPC_ENABLE_LTO=OFF
-DSPC_ENABLE_THREAD_SANITIZER=ON
-DSPC_BUILD_EXAMPLES=OFF
- run: cmake --build build-thread-sanitized --parallel
- run: ctest --test-dir build-thread-sanitized --output-on-failure

clang-tidy:
runs-on: ubuntu-24.04
steps:
- uses: actions/checkout@v7.0.1
- name: Install dependencies
run: |
sudo apt-get update
sudo apt-get install -y --no-install-recommends \
build-essential clang-18 clang-tidy-18 cmake \
libc++-18-dev libc++abi-18-dev libcurl4-openssl-dev
- name: Run clang-tidy while compiling
run: >-
cmake -S . -B build-tidy
-DCMAKE_BUILD_TYPE=Debug
-DCMAKE_CXX_COMPILER=clang++-18
-DCMAKE_CXX_FLAGS=-stdlib=libc++
-DSPC_ENABLE_CLANG_TIDY=ON
-DSPC_CLANG_TIDY_EXECUTABLE=/usr/bin/clang-tidy-18
-DSPC_ENABLE_LTO=OFF
-DSPC_BUILD_TESTS=OFF
-DSPC_BUILD_EXAMPLES=OFF
- run: cmake --build build-tidy --parallel

consumer-smoke:
runs-on: ubuntu-24.04
steps:
- uses: actions/checkout@v7.0.1
- name: Install dependencies
run: |
sudo apt-get update
sudo apt-get install -y --no-install-recommends \
build-essential cmake libcurl4-openssl-dev
- run: ./tools/test_consumers.sh
2 changes: 1 addition & 1 deletion .github/workflows/release.yml
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ jobs:
release:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v6
- uses: actions/checkout@v7.0.1
with:
fetch-depth: 0

Expand Down
37 changes: 37 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,43 @@ uses [Semantic Versioning](https://semver.org/spec/v2.0.0.html).

## [Unreleased]

## [0.2.0] - 2026-09-03

### Added

- ArcGIS access for conditional intensity and day 4 through 8 probability.
- An injectable `HttpTransport` for deterministic client tests and custom
networking.
- Installed-package and FetchContent consumer checks, ASan, UBSan, TSan, and
clang-tidy CI.
- A checked NOAA ArcGIS 11.3 layer contract and opt-in live metadata check.

### Changed

- Fire-weather queries now merge both feature layers for every day from 1
through 8. Each feature records its source layer. Days 1 and 2 expose the
categorical severity; days 3 through 8 expose a normalized probability.
Group layers are never used as feature endpoints.
- Static probabilistic feeds now use NOAA's published `day{N}otlk_*` filenames.
- Glaze is now 8.3.0 and GoogleTest is now 1.18.0.
- libcurl global state now initializes once per process instead of once per
client.

### Fixed

- Corrected the Day 2 tornado and hail probability layers, Day 3 probability,
and every Day 3 through 8 fire-weather layer.
- Decode NOAA's numeric Day 1 and 2 fire-weather `dn` categories without
confusing outlook and dry-thunderstorm labels.
- Percent-encoded ArcGIS query values and parse ArcGIS error and paging fields
as JSON.
- Invalid product combinations now fail before network access.

### Deprecated

- `ArcGISClient::query_active_watches()`. NOAA's WWA polygons do not contain
the SPC fields in `WatchPayload`; use `ArchiveClient::watches()`.

## [0.1.1] - 2026-06-06

### Fixed
Expand Down
97 changes: 42 additions & 55 deletions CLAUDE.md
Original file line number Diff line number Diff line change
@@ -1,62 +1,49 @@
# spc-cpp Development Guide
# spc-cpp development guide

## Build & Test
## Commands

```bash
make build # Release build (CMake + make)
make debug # Debug build
make test # Run unit tests (ctest)
make lint # Check formatting (clang-format --dry-run) + cpp_auto_audit
make format # Format in place
make coverage # lcov coverage report
make clean # Remove build/
make run-<example> # Build + run an example (see examples/)
make build
make test
make lint
make test-consumers
python3 tools/verify_arcgis_metadata.py # live, opt-in
```

Use `-DSPC_ENABLE_SANITIZERS=ON` for ASan and UBSan. Use
`-DSPC_ENABLE_THREAD_SANITIZER=ON` in a separate build for TSan. Use
`-DSPC_ENABLE_CLANG_TIDY=ON` for the configured clang-tidy gate.

## Architecture

- **Layered static libraries**: spc_core -> spc_http -> spc_models ->
spc_api -> spc (INTERFACE). `install(EXPORT)` + `spc::` namespace.
- **C++23**: `std::expected<T, Error>` for all returns, no exceptions.
- **Patterns**: Pimpl (HttpClient, the spc_api clients), non-copyable /
movable clients, `[[nodiscard]]`.
- **JSON**: [Glaze](https://github.com/stephenberry/glaze) v7.6.0 via
FetchContent. This **diverges** from the sibling `nws-cpp` /
`ncei-cpp` SDKs (nlohmann/json) on purpose — SPC's properties block
is shape-loose (case-variant `LABEL`/`label`/`dn`, numeric-as-string)
and geometry is polymorphic (`Polygon`/`MultiPolygon`), so the parser
walks a `glz::generic` AST rather than a static `glz::meta`. The
convective path is lifted verbatim from the internal `spc-data`
service and must stay parity-exact (a downstream byte-identity gate
depends on it). Documented in README + CHANGELOG.
- **Tests**: GoogleTest via FetchContent. Fixture files in
`tests/fixtures/` (live-captured SPC payloads, shared corpus with
`spc-data`).

## Conventions

- Code style: `.clang-format` (LLVM base, tabs, 100 cols).
- Namespace: `spc`.
- **No `auto`**: explicit types except iterators, structured bindings,
range-for. `tools/cpp_auto_audit.py`, empty allowlist.
- Verbatim convective parser is frozen behavior. Net-new product
models carry their own severity mappers — never reuse
`severity_from_label` for fire weather / watches / etc.
- Include order: project headers first, then system (clang-format
enforced).

## Data sources

- **ArcGIS MapServer** (`mapservices.weather.noaa.gov`) — primary,
contract-stable; `ArcGISClient` + `ArcGISPager` (2000-rec paging).
- **Static `www.spc.noaa.gov` `.nolyr.geojson`** — fallback;
`StaticFeedClient` (the 7 feeds `spc-data` ships + day4-8 / fire).
- **IEM** (`mesonet.agron.iastate.edu`) — best-effort historical
backfill; `ArchiveClient` (conservative rate/retry, off any hot path).

## CI

GitHub Actions `.github/workflows/ci.yml`: build + test + lint on
Ubuntu 24.04 and macos-latest, markdown-lint via DavidAnson. Release
workflow auto-creates a GitHub Release on `vX.Y.Z` tag push (notes
from `CHANGELOG.md`; the tag must equal `project(spc-cpp VERSION ...)`).
The static-library chain is `spc_core -> spc_http -> spc_models -> spc_api`.
Consumers normally link `spc::spc`.

All public failures return `Result<T>`, which aliases `std::expected<T,
Error>`. High-level clients use `HttpClient` by default and accept a shared
`HttpTransport` for custom networking and tests.

Glaze 8.3 parses loose SPC JSON. GoogleTest 1.18 runs the unit suite.

## Invariants

- Use C++23 and explicit types. `tools/cpp_auto_audit.py` enforces the narrow
exceptions for `auto`.
- Follow `.clang-format`, use the `spc` namespace, and place project includes
before system includes.
- Keep the convective parser aligned with the internal `spc-data` parser. Its
key-case, number conversion, and Polygon or MultiPolygon behavior are
compatibility requirements.
- Give each new product its own label mapper. Fire weather and watch labels do
not use `severity_from_label`.
- Test client behavior through `HttpTransport`. Unit tests must not require
NOAA or IEM access.
- Treat `tests/fixtures/arcgis_layers_2026-09-03.json` as the ArcGIS routing
contract. Run the live metadata check when changing layer IDs.
- Resolve SPC watch boxes through `ArchiveClient::watches()`. NOAA WWA polygons
have a different schema.

## Release

The tag must match `project(spc-cpp VERSION ...)`. A `vX.Y.Z` tag triggers the
release workflow, which reads the matching `CHANGELOG.md` section.
52 changes: 48 additions & 4 deletions CMakeLists.txt
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
cmake_minimum_required(VERSION 3.20)
project(spc-cpp VERSION 0.1.1 LANGUAGES CXX)
project(spc-cpp VERSION 0.2.0 LANGUAGES CXX)

set(CMAKE_CXX_STANDARD 23)
set(CMAKE_CXX_STANDARD_REQUIRED ON)
Expand All @@ -14,6 +14,30 @@ option(SPC_BUILD_TESTS "Build tests" ON)
option(SPC_BUILD_EXAMPLES "Build examples" ON)
option(SPC_ENABLE_LTO "Enable Link Time Optimization" ON)
option(SPC_NATIVE_ARCH "Use -march=native for CPU-specific tuning" OFF)
option(SPC_ENABLE_CLANG_TIDY "Run clang-tidy while compiling project targets" OFF)
option(SPC_WARNINGS_AS_ERRORS "Treat project warnings as errors" OFF)

if(SPC_ENABLE_CLANG_TIDY)
find_program(SPC_CLANG_TIDY_EXECUTABLE NAMES clang-tidy REQUIRED)
endif()

function(spc_configure_target target)
if(MSVC)
target_compile_options(${target} PRIVATE /W4)
if(SPC_WARNINGS_AS_ERRORS)
target_compile_options(${target} PRIVATE /WX)
endif()
else()
target_compile_options(${target} PRIVATE -Wall -Wextra -Wpedantic)
if(SPC_WARNINGS_AS_ERRORS)
target_compile_options(${target} PRIVATE -Werror)
endif()
endif()
if(SPC_ENABLE_CLANG_TIDY)
set_property(TARGET ${target} PROPERTY CXX_CLANG_TIDY
"${SPC_CLANG_TIDY_EXECUTABLE};--warnings-as-errors=*")
endif()
endfunction()

# Aggressive optimization flags for Release builds. MSVC has its own
# Release defaults; only override for GCC/Clang.
Expand Down Expand Up @@ -51,11 +75,23 @@ endif()

# Sanitizer support
option(SPC_ENABLE_SANITIZERS "Enable AddressSanitizer and UBSanitizer" OFF)
option(SPC_ENABLE_THREAD_SANITIZER "Enable ThreadSanitizer" OFF)
if(SPC_ENABLE_SANITIZERS AND SPC_ENABLE_THREAD_SANITIZER)
message(FATAL_ERROR "AddressSanitizer and ThreadSanitizer require separate builds")
endif()
if(SPC_ENABLE_SANITIZERS)
add_compile_options(-fsanitize=address,undefined -fno-omit-frame-pointer)
add_link_options(-fsanitize=address,undefined)
message(STATUS "Sanitizers enabled (ASan + UBSan)")
endif()
if(SPC_ENABLE_THREAD_SANITIZER)
if(MSVC)
message(FATAL_ERROR "ThreadSanitizer is not supported by this MSVC configuration")
endif()
add_compile_options(-fsanitize=thread -fno-omit-frame-pointer)
add_link_options(-fsanitize=thread)
message(STATUS "ThreadSanitizer enabled")
endif()

# Code coverage support
option(SPC_ENABLE_COVERAGE "Enable code coverage instrumentation (gcov)" OFF)
Expand All @@ -76,12 +112,12 @@ include(FetchContent)
# polymorphic (Polygon/MultiPolygon). This DIVERGES from the nlohmann-based
# nws-cpp / ncei-cpp SDKs on purpose (see README + CHANGELOG). The convective
# parse path is lifted verbatim from the internal spc-data service.
# License: MIT. Pinned to the same v7.6.0 the rest of the estate uses so a
# License: MIT. Pinned to the same v8.3.0 the rest of the estate uses so a
# combined FetchContent build dedupes to a single glaze::glaze target.
FetchContent_Declare(
glaze
GIT_REPOSITORY https://github.com/stephenberry/glaze.git
GIT_TAG v7.6.0
GIT_TAG v8.3.0
GIT_SHALLOW TRUE
)
FetchContent_MakeAvailable(glaze)
Expand All @@ -96,10 +132,11 @@ if(SPC_BUILD_TESTS)
FetchContent_Declare(
googletest
GIT_REPOSITORY https://github.com/google/googletest.git
GIT_TAG v1.15.2
GIT_TAG v1.18.0
GIT_SHALLOW TRUE
)
set(gtest_force_shared_crt ON CACHE BOOL "" FORCE)
set(INSTALL_GTEST OFF CACHE BOOL "" FORCE)
FetchContent_MakeAvailable(googletest)
enable_testing()
add_subdirectory(tests)
Expand All @@ -120,6 +157,13 @@ install(TARGETS spc_core spc_http spc_models spc_api spc
INCLUDES DESTINATION ${CMAKE_INSTALL_INCLUDEDIR}
)
install(DIRECTORY include/spc DESTINATION ${CMAKE_INSTALL_INCLUDEDIR})
# Public model headers use Glaze's generic JSON types. Install the pinned
# headers so find_package consumers do not need a second package manager.
install(DIRECTORY ${glaze_SOURCE_DIR}/include/glaze DESTINATION ${CMAKE_INSTALL_INCLUDEDIR})
install(FILES ${glaze_SOURCE_DIR}/LICENSE
DESTINATION ${CMAKE_INSTALL_DATADIR}/licenses/spc-cpp
RENAME glaze-LICENSE
)

configure_package_config_file(
${CMAKE_CURRENT_SOURCE_DIR}/cmake/spcConfig.cmake.in
Expand Down
Loading
Loading