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
16 changes: 9 additions & 7 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,7 @@ jobs:
- uses: actions/checkout@v4
- uses: dtolnay/rust-toolchain@stable
- uses: Swatinem/rust-cache@v2
- run: cargo check --workspace --all-targets --all-features
- run: cargo check --locked --workspace --all-targets --all-features

clippy:
name: Clippy
Expand All @@ -45,7 +45,7 @@ jobs:
with:
components: clippy
- uses: Swatinem/rust-cache@v2
- run: cargo clippy --workspace --all-targets --all-features -- -D warnings
- run: cargo clippy --locked --workspace --all-targets --all-features -- -D warnings

tests:
name: Tests (${{ matrix.features }})
Expand All @@ -55,11 +55,11 @@ jobs:
matrix:
include:
- features: default
command: cargo test --workspace --no-fail-fast
command: cargo test --locked --workspace --no-fail-fast
- features: no-default-features
command: cargo test --workspace --no-default-features --no-fail-fast
command: cargo test --locked --workspace --no-default-features --no-fail-fast
- features: all-features
command: cargo test --workspace --all-features --no-fail-fast
command: cargo test --locked --workspace --all-features --no-fail-fast
steps:
- uses: actions/checkout@v4
- uses: dtolnay/rust-toolchain@stable
Expand All @@ -73,7 +73,7 @@ jobs:
- uses: actions/checkout@v4
- uses: dtolnay/rust-toolchain@stable
- uses: Swatinem/rust-cache@v2
- run: cargo test --doc --workspace --all-features
- run: cargo test --locked --doc --workspace --all-features

docs:
name: Documentation
Expand All @@ -82,7 +82,7 @@ jobs:
- uses: actions/checkout@v4
- uses: dtolnay/rust-toolchain@stable
- uses: Swatinem/rust-cache@v2
- run: cargo doc --workspace --no-deps
- run: cargo doc --locked --workspace --no-deps
env:
RUSTDOCFLAGS: -D rustdoc::broken-intra-doc-links

Expand All @@ -95,6 +95,8 @@ jobs:
- run: bash scripts/check_dep_graph.sh
- run: bash scripts/check_no_todos.sh
- run: bash scripts/check_legacy_name_refs.sh
- run: bash scripts/check_architecture.sh
- run: bash scripts/check_architecture_test.sh

msrv:
name: MSRV (Rust 1.89)
Expand Down
7 changes: 4 additions & 3 deletions docs/architecture/boundaries.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
| `spod::service` | Configuration and validation, canonical job lifecycle/status/error contracts, workflow dispatch, run orchestration, input boundaries, artifact layout, and provenance finalisation using `siderust::pod`. | Reusable numerical algorithms, HTTP semantics, and durable scheduling/persistence. |
| `spod` binaries | `clap`-based commands and the REST server; delegates to `spod::service`. | Any numerical logic. |

The full set of allowed and forbidden dependency edges is in
`dependency-rules.md` and is enforced by `scripts/check_dep_graph.sh`
in CI.
The current dependency and source-ownership rules are in
`dependency-rules.md`. Registry/lockfile provenance is enforced by
`scripts/check_dep_graph.sh`; local scientific implementation drift is
enforced by `scripts/check_architecture.sh` in CI.
53 changes: 30 additions & 23 deletions docs/architecture/dependency-rules.md
Original file line number Diff line number Diff line change
@@ -1,28 +1,35 @@
# Dependency rules

`spod` is a single service/application crate built on the canonical Siderust
0.12 APIs. The dependency direction is intentionally small:

```text
ALLOWED:
qtty, tempoch, affn, cheby (independent foundations)
siderust -> qtty, tempoch, affn, cheby
pod-core -> siderust, qtty, tempoch, affn, cheby
pod-dynamics -> pod-core, siderust, cheby
pod-io -> siderust
pod-observations -> pod-dynamics, siderust
pod-estimation -> siderust, faer
pod-qc -> pod-products, siderust
pod-products -> pod-io, siderust
pod-service -> ALL pod-* crates, siderust
pod-cli -> pod-service (only)

FORBIDDEN:
siderust -> pod-*
qtty/tempoch/affn/cheby -> siderust or pod-*
pod-core -> any other pod-*
pod-estimation -> pod-io, pod-observations
pod-observations -> pod-io
pod-dynamics -> pod-io, pod-observations, pod-estimation
pod-qc -> pod-estimation
pod-cli -> pod-{core,dynamics,io,observations,estimation,qc,products}
spod service/application
|
v
siderust::pod and siderust::formats
```

The forbidden edges are checked by `scripts/check_dep_graph.sh`.
Runtime crates such as Axum, Tokio, Clap, and Serde support the application
boundary. They do not provide or replace POD scientific ownership.

## Allowed exceptions

The direct `sgp4` dependency and `src/sgp4/` implementation are temporary
product-parity exceptions. They provide full Vallado/AFSPC semantics that are
not yet available in Siderust 0.12. Their removal is gated by spod #29 and
Siderust #98, with the fixtures in `tests/sgp4_vallado.rs` preserved until
semantic parity is demonstrated.

## Forbidden drift

- Path or Git dependencies for canonical scientific crates.
- New local generic force, measurement, estimation, QC, format-parser, or
propagation implementations outside the documented SGP4 exception.
- CLI or REST code that assembles scientific pipelines instead of dispatching
through `spod::service`.

`scripts/check_dep_graph.sh` enforces reproducible registry/lockfile
dependencies. `scripts/check_architecture.sh` enforces the source boundary
using narrowly scoped implementation type patterns rather than a filename
blacklist. Both checks run in the CI project-guards job.
26 changes: 11 additions & 15 deletions docs/architecture/dispatch.md
Original file line number Diff line number Diff line change
@@ -1,17 +1,13 @@
# Static vs dynamic dispatch
# Dispatch boundary

- **Static / generic dispatch** is used in inner loops:
- integrator step (`rk4_step` is generic over `F: ForceModel`),
- residual evaluation,
- per-step force evaluation through `CompositeForce`.
`spod::service` performs configuration validation, workflow selection, and
artifact/provenance orchestration. Scientific execution is delegated to the
canonical Siderust APIs; `spod` does not define local generic force,
measurement, estimation, or propagation kernels.

- **Dynamic dispatch** (`Box<dyn ForceModel>`, `Box<dyn MeasurementModel>`)
is used at *configuration assembly* time inside `spod::service`.
The boxed objects are converted into a concrete composite type before
entering the integration / estimation kernels, so the inner loops see a
monomorphised call.

- **Rationale:** generic monomorphisation gives competitive performance
on the hot path; the dynamic seam at the top of the pipeline keeps
configuration ergonomic and avoids a combinatorial explosion of
generic instantiations exposed in the public API.
The source-hygiene guard in
[`scripts/check_architecture.sh`](../../scripts/check_architecture.sh) checks
for newly declared local scientific implementation traits and types while
allowing service code to compose upstream names. The only current local
scientific exception is `src/sgp4/`, retained for full Vallado/AFSPC parity
until Siderust provides equivalent semantics.
1 change: 1 addition & 0 deletions docs/architecture/lints-and-safety.md
Original file line number Diff line number Diff line change
Expand Up @@ -82,6 +82,7 @@ The CI pipeline runs (in order):
see [`../conventions/examples-and-doctests.md`](../conventions/examples-and-doctests.md)).
5. `scripts/snapshot_public_api.sh` — public-API diff (advisory pre-1.0).
6. `cargo deny check` — license / advisory / source policy.
7. `scripts/check_architecture.sh` — service-to-Siderust source boundary.

A red light on any of these blocks merge.

Expand Down
9 changes: 9 additions & 0 deletions docs/architecture/overview.md
Original file line number Diff line number Diff line change
Expand Up @@ -68,3 +68,12 @@ The REST adapter is intentionally asynchronous and experimental. It does not
provide durable persistence, scheduling, retries, or distributed workers.
Those concerns are deferred to later service/API work, including the stable
artifact API tracked by #28.

## Testing boundary

`spod` integration tests validate product and service behavior: configuration,
dispatch, lifecycle, interfaces, artifacts, and provenance. Reusable
scientific correctness belongs in Siderust. The full Vallado SGP4/SDP4 path
is the explicit temporary exception and remains covered by local parity tests
until spod #29 and Siderust #98 are resolved. See
[Testing architecture](testing.md) for the test pyramid and CI guardrails.
43 changes: 43 additions & 0 deletions docs/architecture/testing.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
# Testing architecture

`spod` tests the product and service boundary. The primary integration path
is the synthetic workflow in
[`tests/service_synthetic.rs`](../../tests/service_synthetic.rs): it loads a
configuration, validates and dispatches it through `Runner`, writes the
service artifact set, and verifies manifest provenance, hashes, and sizes.
It intentionally checks application contracts rather than reproducing
scientific reference values owned by Siderust.

## Test ownership

| Layer | Owned by | Examples |
| --- | --- | --- |
| Service integration | `spod` | configuration validation, workflow dispatch, job lifecycle, artifacts, manifests, provenance |
| Interface integration | `spod` | compiled CLI tests, in-process REST router tests, process and Docker startup smoke tests |
| Scientific unit tests | Siderust | force-model mathematics, propagation numerics, estimation kernels, generic observations, standard formats, and generic QC |
| Product parity | `spod` temporarily | Full Vallado SGP4/SDP4 compatibility required by the product |

The CLI and REST adapters use the same `JobStatus` and `JobError` model.
Detailed behavior belongs in fast in-process tests; shell and Docker checks
only cover process, bind-address, image, and health-boundary behavior.

## Architecture guardrails

`scripts/check_architecture.sh` rejects newly declared local generic
scientific implementation types outside the service boundary. It checks
implementation traits/types rather than filenames, so legitimate orchestration
and upstream Siderust type names remain usable. `scripts/check_dep_graph.sh`
also rejects path and Git dependencies so scientific APIs resolve from the
canonical locked dependency graph.

The local `src/sgp4/` implementation is an explicit exception. Siderust 0.12
does not yet provide equivalent full Vallado/AFSPC semantics, so
[`tests/sgp4_vallado.rs`](../../tests/sgp4_vallado.rs) remains the product
parity safety net for near-Earth, deep-space, and propagation-error cases.
The exception is tracked by [VPRamon/spod#29](https://github.com/VPRamon/spod/issues/29)
and [Siderust/siderust#98](https://github.com/Siderust/siderust/issues/98).

When adding a service workflow, add coverage for its configuration contract,
dispatch and lifecycle behavior, generated artifacts, and provenance. Add
scientific correctness tests upstream instead of copying Siderust's unit
tests into this repository.
55 changes: 55 additions & 0 deletions scripts/check_architecture.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
#!/usr/bin/env bash
# Enforce the service -> Siderust dependency direction.
#
# This is deliberately a source-pattern guard, not a filename blacklist:
# service orchestration may mention or compose upstream scientific types.
# New local generic scientific implementations must either move upstream or
# be added to a documented exception. The only current exception is src/sgp4.

set -euo pipefail

repository_root="${SPOD_ARCH_ROOT:-$(cd "$(dirname "$0")/.." && pwd)}"
cd "$repository_root"

exception_dir="src/sgp4"
if [[ ! -d "$exception_dir" ]]; then
echo "Architecture check FAILED: expected SGP4 exception directory '$exception_dir'." >&2
exit 1
fi

mapfile -t source_files < <(
find src tests -type f -name '*.rs' ! -path "$exception_dir/*" -print
)

if ((${#source_files[@]} == 0)); then
echo "Architecture source-hygiene check passed."
exit 0
fi

if grep -InE \
'^[[:space:]]*(pub(\([^)]*\))?[[:space:]]+)?(struct|trait)[[:space:]]+(ForceModel|MeasurementModel|Propagator|Integrator|KalmanFilter|GaussNewton|LeastSquares|OrbitComparator)([[:space:]]|<|{|$)' \
"${source_files[@]}"; then
grep_status=0
else
grep_status=$?
fi

case "$grep_status" in
0)
cat >&2 <<'EOF'
Architecture source-hygiene check FAILED.
New local generic scientific implementations were found outside the
documented SGP4 exception in src/sgp4/. Reuse Siderust APIs or document a
temporary upstream-gap exception before adding local science.
EOF
exit 1
;;
1)
echo "Architecture source-hygiene check passed."
exit 0
;;
*)
echo "Architecture source-hygiene check FAILED: grep exited with status $grep_status." >&2
exit "$grep_status"
;;
esac
92 changes: 92 additions & 0 deletions scripts/check_architecture_test.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,92 @@
#!/usr/bin/env bash
# Regression tests for check_architecture.sh using temporary source trees.

set -euo pipefail

script_dir="$(cd "$(dirname "$0")" && pwd)"
checker="$script_dir/check_architecture.sh"
root="$(mktemp -d)"
trap 'rm -rf "$root"' EXIT

mkdir -p "$root/src/service" "$root/src/sgp4" "$root/tests/sgp4" "$root/scripts"
cp "$checker" "$root/scripts/check_architecture.sh"
chmod +x "$root/scripts/check_architecture.sh"

expect_forbidden_declaration() {
local description="$1"
local declaration="$2"

printf '%s\n' "$declaration" >"$root/src/service/forbidden.rs"
if SPOD_ARCH_ROOT="$root" "$root/scripts/check_architecture.sh" \
>"$root/stdout" 2>"$root/stderr"; then
echo "architecture regression expected failure: $description" >&2
exit 1
fi
grep -q "New local generic scientific implementations" "$root/stderr"
rm "$root/src/service/forbidden.rs"
}

for case in \
"private|struct ForceModel;" \
"public|pub struct MeasurementModel;" \
"pub(crate)|pub(crate) struct Integrator;" \
"pub(super)|pub(super) trait Propagator {}" \
"pub(self)|pub(self) struct KalmanFilter;" \
"pub(in path)|pub(in crate::foo) struct GaussNewton;" \
"private second name|struct LeastSquares;" \
"public second name|pub struct OrbitComparator;"
do
IFS='|' read -r description declaration <<<"$case"
expect_forbidden_declaration "$description" "$declaration"
done

cat >"$root/src/sgp4/exception.rs" <<'EOF'
pub(crate) struct ForceModel;
pub(super) trait Propagator {}
EOF
cat >"$root/tests/sgp4/other.rs" <<'EOF'
pub struct ForceModel;
EOF
if SPOD_ARCH_ROOT="$root" "$root/scripts/check_architecture.sh" \
>"$root/stdout" 2>"$root/stderr"; then
echo "architecture regression expected tests/sgp4 declaration to fail" >&2
exit 1
fi
grep -q "New local generic scientific implementations" "$root/stderr"
rm "$root/tests/sgp4/other.rs"
SPOD_ARCH_ROOT="$root" "$root/scripts/check_architecture.sh" \
>"$root/stdout" 2>"$root/stderr"
grep -qx "Architecture source-hygiene check passed." "$root/stdout"
rm "$root/src/sgp4/exception.rs"

mock_bin="$root/mock-bin"
mkdir "$mock_bin"
cat >"$mock_bin/grep" <<'EOF'
#!/usr/bin/env bash
echo "grep must not run without inspectable Rust files" >&2
exit 99
EOF
chmod +x "$mock_bin/grep"
PATH="$mock_bin:$PATH" SPOD_ARCH_ROOT="$root" \
"$root/scripts/check_architecture.sh" >"$root/stdout" 2>"$root/stderr"
grep -qx "Architecture source-hygiene check passed." "$root/stdout"

cat >"$root/src/service/valid.rs" <<'EOF'
pub(crate) struct ServiceState;
EOF
SPOD_ARCH_ROOT="$root" "$root/scripts/check_architecture.sh" \
>"$root/stdout" 2>"$root/stderr"
grep -qx "Architecture source-hygiene check passed." "$root/stdout"

cat >"$mock_bin/grep" <<'EOF'
#!/usr/bin/env bash
exit 2
EOF
if PATH="$mock_bin:$PATH" SPOD_ARCH_ROOT="$root" \
"$root/scripts/check_architecture.sh" >"$root/stdout" 2>"$root/stderr"; then
echo "architecture regression expected checker error to fail" >&2
exit 1
fi
grep -q "grep exited with status 2" "$root/stderr"

echo "Architecture guard regression tests passed."
Loading
Loading