alice-physics 2.2.0

Deterministic physics engine: 128-bit fixed-point rigid bodies with bit-exact, replayable results, typed observation and rollback for search and planning, plus FEM, CFD and 3D-print analysis modules
Documentation
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
name: CI

on:
  push:
    # `ci/**` is a staging branch: push a batch there, let the full 5-OS suites run
    # off the local machine, then fast-forward main to the same SHA once green
    # (the local gate is `scripts/preflight.sh --fast`).
    branches: [main, 'ci/**']
  pull_request:
    branches: [main]

env:
  CARGO_TERM_COLOR: always

jobs:
  test:
    name: Test (${{ matrix.os }})
    runs-on: ${{ matrix.os }}
    strategy:
      fail-fast: false
      matrix:
        # v1.0 Item E Phase 2: 5 native platform matrix (WASM は独立 job で扱う)
        # - macos-latest       → aarch64-apple-darwin       (Mac ARM, primary dev, golden hash baseline)
        # - macos-15-intel     → x86_64-apple-darwin        (Mac Intel)
        # - ubuntu-latest      → x86_64-unknown-linux-gnu   (Linux x86)
        # - ubuntu-24.04-arm   → aarch64-unknown-linux-gnu  (Linux ARM、GA runner 2025-01+)
        # - windows-latest     → x86_64-pc-windows-msvc     (Windows x86)
        # + wasm-test job below → wasm32-wasip1 (WASI) via wasmtime
        os: [macos-latest, macos-15-intel, ubuntu-latest, ubuntu-24.04-arm, windows-latest]
    steps:
      - uses: actions/checkout@v5

      # README.md / README_JP.md / docs/MODULES.md vs Cargo.toml and src/lib.rs
      # (features, MSRV, dependency versions, example, module list, links).
      # Run on every OS so a locale or path difference in the checker itself
      # shows up here rather than as a silent pass.
      - uses: actions/setup-python@v6
        with:
          python-version: '3.11'
      - name: README sync checker (tests)
        run: python scripts/test_readme_sync.py
      - name: README sync (docs agree with Cargo.toml and src/)
        run: python scripts/readme_sync.py --check
      - name: Version sync checker (tests)
        run: python scripts/test_version_sync.py
      - name: Version sync (metadata and install lines follow Cargo.toml)
        run: python scripts/version_sync.py --check
      - name: Lib-test gate (tests)
        run: python scripts/test_lib_test_gate.py
      - name: Preflight runs every cargo test of ci.yml (tests and check)
        run: |
          python scripts/test_preflight_ci_parity.py
          python scripts/preflight_ci_parity.py
      - name: Fuzz target list (tests and check)
        run: |
          python scripts/test_fuzz_targets.py
          python scripts/fuzz_targets.py --check
      - name: Semver-checks log reader (tests)
        run: python scripts/test_semver_checked.py
      - name: Escape-sequence stripping for tool output (tests)
        run: python scripts/test_ansi.py
      # public documents: no development-process vocabulary or private names;
      # CHANGELOG version headings and [Unreleased] categories well formed
      - name: Docs lint (tests)
        run: python scripts/test_docs_lint.py
      - name: Docs lint (public documents, CHANGELOG structure)
        run: python scripts/docs_lint.py --check
      # coverage tables (docs/coverage/*.toml, hand-written): every cited test and
      # source symbol exists, oracles are not ignored, partial items and the
      # LIMITATION(<id>) comments in src/ point at each other
      - name: Coverage tables (tests)
        run: python scripts/test_coverage_check.py
      - name: Coverage reference converter (tests)
        run: python scripts/test_coverage_refs_to_symbols.py
      - name: Line-coverage ratchet (tests)
        run: python scripts/test_line_coverage_ratchet.py
      - name: Mutation ratchet (tests)
        run: python scripts/test_mutants_ratchet.py
      - name: Mutants exclude_re entries still match a real mutant (tests)
        run: python scripts/test_mutants_exclude_check.py
      - name: Mutation in-diff plan (tests)
        run: python scripts/test_mutants_in_diff_plan.py
      # the weekly ignored-test run fails on a crash, a binary that runs too
      # long, or a run that checked nothing; these keep that runner honest on
      # every push (the run itself is too heavy for the landing path)
      - name: Ignored-test runner (tests)
        run: python scripts/test_run_ignored.py
      - name: Ignored-test table derives
        run: python scripts/run_ignored.py --list
      - name: Bench count check (tests)
        run: python scripts/test_bench_counts_check.py
      - name: Heavy workflows stay off the landing path (tests)
        run: python scripts/test_ci_load_check.py
      - name: Heavy workflows stay off the landing path
        run: python scripts/ci_load_check.py
      - name: Downstream release-tag selection (tests)
        run: python scripts/test_downstream_select_tag.py
      - name: Downstream per-binary test counts (tests)
        run: python scripts/test_downstream_test_counts.py
      - name: Coverage tables (docs/coverage vs src/ and tests/)
        run: python scripts/coverage_check.py
      # SCIP integration ledger (docs/integration-status.md): the analysis is
      # tested on hand-built indexes here; the real index needs rust-analyzer and
      # is built by integration-status.yml
      - name: SCIP reach analysis (tests)
        run: python scripts/test_scip_reach.py
      - name: Known-defect symbol resolution (tests)
        run: python scripts/test_audit_refs.py
      - name: Feature-gated test runner (tests)
        run: python scripts/test_run_feature_gated_tests.py
      # how each module and each C ABI consumer is integrated
      # (docs/integration-levels.md): the analysis on hand-built indexes, and the
      # C ABI coverage, which needs no index (the module part runs in the scip job)
      - name: Integration levels (tests)
        run: python scripts/test_integration_levels.py
      - name: Landing script (tests)
        run: python scripts/test_land.py
      - name: C ABI declared by every consumer or listed with a reason
        run: python scripts/integration_levels.py --no-index --check

      - name: Install Rust toolchain
        uses: dtolnay/rust-toolchain@stable

      - uses: Swatinem/rust-cache@v2

      # Cargo.lock is committed: every job builds the same dependency versions
      # the oracle ledger records, and a Cargo.toml change without its lock
      # update fails here instead of resolving afresh.
      - name: Cargo.lock matches Cargo.toml
        shell: bash
        run: cargo metadata --locked --format-version 1 > /dev/null

      # .cargo/mutants.toml's exclude_re entries are line-anchored proofs of
      # equivalence; code added above an excluded function shifts
      # cargo-mutants' reported line:col, silently making the entry match 0
      # mutants (scripts/mutants_exclude_check.py). `--list` only enumerates
      # candidate sites (no build-and-test per mutant), so this runs on every
      # push, not just the weekly cargo-mutants job in quality-deep.yml.
      # One OS only (ubuntu-latest): the match counts are a property of the
      # source text, not the platform, and installing cargo-mutants on all 5
      # OS every push would spend shared runner time with nothing to show
      # for the other 4 (their `cargo mutants --list` output is unverified
      # against this baseline, so a platform-specific difference there would
      # not be any more caught by running it 5 times than by running it once).
      - name: Install cargo-mutants
        if: matrix.os == 'ubuntu-latest'
        uses: taiki-e/install-action@v2
        with:
          tool: cargo-mutants@27.1.0
      - name: Mutants exclude_re entries still match a real mutant
        if: matrix.os == 'ubuntu-latest'
        run: python scripts/mutants_exclude_check.py

      # PIN markers must name an open known defect; external root causes must
      # name a crate Cargo resolves (docs/oracle-status.md lists both), read from
      # the committed Cargo.lock.
      - name: Oracle ledger links (PIN / root external)
        run: python scripts/gen-oracle-status.py --check

      # `crate-type` includes cdylib/staticlib (FFI consumers, 1.2.0); a no_std
      # cdylib needs a panic_handler + allocator, so the no_std *library* build is
      # checked as rlib only (same workaround as ALICE-ML). Downstream no_std
      # users always get the rlib.
      - name: Build (no_std, rlib)
        run: cargo rustc --lib --no-default-features --crate-type rlib

      - name: Build (std only)
        run: cargo build --lib --no-default-features --features "std"

      - name: Build (core — default features)
        run: cargo build --lib

      - name: Test (unit + integration + doc)
        run: cargo test

      # 1.2.0: the C ABI consumers (Unity P/Invoke, unreal-plugin/) link the
      # cdylib / staticlib — assert both are actually produced, not just the rlib.
      # the ffi test module (goldens the Unity / UE5 bindings replay) was never
      # executed in CI before 1.2.0 — only built
      - name: Test (FFI, lib)
        # quoted: a bare trailing `ffi::` makes YAML read the line as a mapping
        # (actionlint syntax-check; two pushes were red on this, 2026-09-15)
        run: 'cargo test --lib --features "ffi" "ffi::"'

      # The WebAssembly and Python bindings were compiled by no CI step: the
      # wasm unit tests run natively, the Python module (an extension module
      # that links against the interpreter at import time) is type-checked
      - name: Test (WebAssembly binding, lib)
        run: 'cargo test --lib --features "wasm" "wasm::"'

      - name: Check (Python binding)
        run: cargo check --lib --features "python"

      - name: Build (FFI) + assert cdylib / staticlib artifacts
        shell: bash
        run: |
          cargo build --features "ffi"
          ls target/debug/
          # `-f` tests, not `ls a b c | grep`: under `set -o pipefail` a missing
          # sibling name makes `ls` exit 2 and fails the pipeline even when the
          # platform's artifact is present (first run of this step, 2026-09-15).
          cdylib=0; for f in target/debug/libalice_physics.so target/debug/libalice_physics.dylib target/debug/alice_physics.dll; do [ -f "$f" ] && cdylib=1; done
          staticlib=0; for f in target/debug/libalice_physics.a target/debug/alice_physics.lib; do [ -f "$f" ] && staticlib=1; done
          [ "$cdylib" = 1 ] || { echo "no cdylib produced"; exit 1; }
          [ "$staticlib" = 1 ] || { echo "no staticlib produced"; exit 1; }

      - name: Build (parallel)
        run: cargo build --lib --features "parallel"

      - name: Test (parallel — unit + integration)
        run: cargo test --features "parallel"

      # full native feature set: the gpu-solver-bridge tests were only linted
      # (clippy), never run, before 1.2.0 — the quality-deep mutants job found
      # them failing in the unmutated tree
      - name: Test (full native feature set, lib)
        run: cargo test --lib --features "std,simd,parallel,ffi,gpu-solver-bridge"

      # PHYSICS_SEMANTICS_ID and its table are constants no cfg gates: the same
      # file runs in `cargo test` above (default features) and here with the
      # features of the gated stepping paths, and both assert the same hex.
      # scripts/preflight.sh runs the same command
      - name: Test (semantics identifier, parallel + gpu-solver-bridge)
        run: cargo test --test physics_semantics_id --features "parallel,gpu-solver-bridge"

      # Integration tests gated on neural / replay / analytics / gpu-solver-bridge:
      # the steps above enable those features only with --lib, so a file under
      # #![cfg(feature = "replay")] compiled to zero tests and stayed green. The
      # script derives the file list from tests/ and fails when a selected target
      # executes zero tests; preflight.sh calls the same script with no arguments
      - name: Test (feature-gated integration tests, zero executed is red)
        run: python scripts/run_feature_gated_tests.py

      # test, not just build: a feature whose code is only compiled is a feature
      # whose effect nobody verifies (external review 2026-09-15: the SIMD paths
      # were scalar with dead intrinsics and no test caught the mismatch claim).
      - name: Test (SIMD, lib)
        run: cargo test --lib --features "simd"

  # Every example is built in release and executed (scripts/run_examples.py):
  # the examples were compiled by clippy --all-targets but never run, so three
  # of them kept asserting behaviour a library fix had changed and one ran for
  # more than ten minutes with every job green. The script takes the example
  # list and the union of their required-features from `cargo metadata`, fails
  # on a non-zero exit or a per-example timeout, and fails when it executed 0
  # examples; preflight.sh calls the same script with no arguments
  examples:
    name: Run examples (release)
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v5
      - uses: actions/setup-python@v6
        with:
          python-version: '3.11'
      - name: Install Rust toolchain
        uses: dtolnay/rust-toolchain@stable
      - uses: Swatinem/rust-cache@v2
        with:
          shared-key: examples-release
      - name: Example runner (tests)
        run: python scripts/test_run_examples.py
      - name: Run every example (release, timeout per example, zero executed is red)
        run: python scripts/run_examples.py

  # v1.0 Item E Phase 2: WASM determinism check
  # `wasm32-wasip1` target で determinism_golden test を build → wasmtime で
  # 実行 SHA-256 hash が native platform (macOS ARM baseline) と bit-exact
  # 一致することを検証 Fix128 は純粋整数演算のため cross-platform bit-exact、
  # SDF 経路の f32 も IEEE 754 basic ops (+/-/*/sqrt) が platform 非依存
  # rust-analyzer SCIP index of the crate: every symbol a known-defect reason
  # names must exist (scripts/audit_refs.py), and no public item may become
  # unreached by non-test code without updating scripts/integration-baseline.txt
  scip:
    name: SCIP checks (defect references, L0 ratchet)
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v5
      - uses: actions/setup-python@v6
        with:
          python-version: '3.11'
      - name: Install rust-analyzer
        run: |
          rustup show active-toolchain
          rustup component add rust-analyzer
      - uses: Swatinem/rust-cache@v2
      - name: Build SCIP indexes
        run: scripts/scip_index.sh
      - name: Every symbol named by a known-defect reason exists
        run: python3 scripts/audit_refs.py --check
      - name: No new L0 item, no stale baseline entry
        run: python3 scripts/scip_reach.py --check-baseline
      - name: Integration levels ledger, README and MODULES.md figures up to date
        run: python3 scripts/integration_levels.py --check

  sibling-features:
    name: Test (neural / replay / analytics via crates.io siblings)
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v5
      - name: Install Rust toolchain
        uses: dtolnay/rust-toolchain@stable
      - uses: Swatinem/rust-cache@v2
        with:
          shared-key: sibling-features
      # The bridge modules (alice-ml / alice-db / alice-analytics) come from
      # crates.io. Their contract tests never ran in CI before 1.2.0, which is
      # how alice-db 0.2.0-beta.1's archived-path stub (every replay read 0.0)
      # went unnoticed; fixed in alice-db 0.2.0-beta.2.
      - name: Test (lib, neural + replay + analytics)
        run: cargo test --lib --features "neural,replay,analytics"

  js-binding:
    # the wasm-bindgen package called from JavaScript (Node), as an application would
    name: Test (WebAssembly binding from JavaScript)
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v5
      - name: Install Rust toolchain (with wasm32-unknown-unknown)
        uses: dtolnay/rust-toolchain@stable
        with:
          targets: wasm32-unknown-unknown
      - name: Ensure wasm32-unknown-unknown rust-std is installed
        run: rustup target add wasm32-unknown-unknown
      - uses: Swatinem/rust-cache@v2
        with:
          shared-key: wasm-js
      - uses: actions/setup-node@v5
        with:
          node-version: '22'
      - name: Install wasm-pack
        uses: taiki-e/install-action@v2
        with:
          tool: wasm-pack
      - name: Build the package and run tests/js (zero executed is red)
        run: bash scripts/run_js_binding_tests.sh
      # every feature that applies in a browser, built for the browser target
      # (parallel / python / ffi do not apply; simd has no wasm path and falls
      # back to scalar; replay records to memory there)
      - name: Build for wasm32-unknown-unknown with every browser feature
        run: cargo build --lib --target wasm32-unknown-unknown --no-default-features --features "std,wasm,neural,replay,analytics,gpu-solver-bridge,simd"

  wasm-test:
    name: Test (wasm32-wasip1 via wasmtime)
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v5

      - name: Install Rust toolchain (with wasm32-wasip1)
        uses: dtolnay/rust-toolchain@stable
        with:
          targets: wasm32-wasip1

      # dtolnay/rust-toolchain の targets: が rustup target 追加を silent
      # fail することがある fallback として明示 rustup target add を実行
      # (idempotent、既に install 済なら no-op)
      - name: Ensure wasm32-wasip1 rust-std is installed
        run: rustup target add wasm32-wasip1

      - uses: Swatinem/rust-cache@v2
        with:
          shared-key: wasm-wasip1

      # 2026-09-15: `curl … | bash` は curl が network error (35 Connection reset) でも
      # pipeline exit が bash 側 = 0 になり step green のまま次 step で `wasmtime: command not found`
      # (exit 127) になる (run 34863693753) 公式 setup action に置換 (retry 内蔵、pipefail 問題が消える)
      - name: Install wasmtime
        uses: bytecodealliance/actions/wasmtime/setup@v1

      - name: Build determinism_golden test (wasm32-wasip1)
        run: cargo test --test determinism_golden --target wasm32-wasip1 --no-run

      - name: Run determinism_golden test via wasmtime
        run: |
          WASM_BIN=$(find target/wasm32-wasip1/debug/deps -name 'determinism_golden-*.wasm' -not -name '*.d' | head -1)
          echo "Executing: $WASM_BIN"
          wasmtime "$WASM_BIN"

      # v1.1.0: the f32/f64 field modules are pinned too (det_math + IEEE basic ops)
      - name: Build determinism_golden_f32 test (wasm32-wasip1)
        run: cargo test --test determinism_golden_f32 --target wasm32-wasip1 --no-run

      - name: Run determinism_golden_f32 test via wasmtime
        run: |
          WASM_BIN=$(find target/wasm32-wasip1/debug/deps -name 'determinism_golden_f32-*.wasm' -not -name '*.d' | head -1)
          echo "Executing: $WASM_BIN"
          wasmtime "$WASM_BIN"

      # contact scenes (restitution, friction, static friction, pre-solve veto);
      # the scenes above contain no body-body contact
      - name: Build determinism_golden_contacts test (wasm32-wasip1)
        run: cargo test --test determinism_golden_contacts --target wasm32-wasip1 --no-run

      - name: Run determinism_golden_contacts test via wasmtime
        run: |
          WASM_BIN=$(find target/wasm32-wasip1/debug/deps -name 'determinism_golden_contacts-*.wasm' -not -name '*.d' | head -1)
          echo "Executing: $WASM_BIN"
          wasmtime "$WASM_BIN"

  clippy:
    name: Clippy
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v5

      - name: Install Rust toolchain
        uses: dtolnay/rust-toolchain@stable
        with:
          components: clippy

      - uses: Swatinem/rust-cache@v2

      - name: Clippy (default features, lib + all targets)
        run: cargo clippy --all-targets -- -D warnings

      # `--all-features` is not possible: `wasm` × `ffi` is a deliberate
      # `compile_error!`, and `python` needs a Python toolchain. Every
      # other feature is exercised here so lint drift in feature-gated
      # code (parallel / neural / replay / ...) is a red gate, not a `-W`.
      - name: Clippy (full native feature set, lib + all targets)
        run: >-
          cargo clippy
          --features "std,simd,parallel,ffi,gpu-solver-bridge,neural,replay,analytics"
          --all-targets -- -D warnings

  # The declared `rust-version` is compiled with exactly that toolchain from the
  # committed Cargo.lock (generated with resolver = "3", which picks versions
  # that support `rust-version`) for the default, no_std and full native feature
  # sets. Bump `rust-version` and the
  # `@<version>` below in the same commit.
  # The pre-1.2.0 job regenerated the lockfile with stable and checked only the
  # std feature set, so the "1.70.0" claim was never actually compiled.
  msrv:
    name: MSRV (rust-version = 1.85)
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v5
      - name: Install MSRV toolchain
        uses: dtolnay/rust-toolchain@1.85

      - uses: Swatinem/rust-cache@v2

      - name: cargo check on MSRV (lib, default features)
        run: cargo +1.85 check --lib
      - name: cargo check on MSRV (lib, no_std, rlib)
        run: cargo +1.85 rustc --lib --no-default-features --crate-type rlib
      # Library only: dev-dependencies (criterion / serde_derive) and the
      # sibling-crate bridge features (neural / replay / analytics) follow
      # their own MSRV, see README "Minimum supported Rust version".
      - name: cargo check on MSRV (lib, core native feature set)
        run: >-
          cargo +1.85 check --lib
          --features "std,simd,parallel,ffi,gpu-solver-bridge"

  fmt:
    name: Format
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v5

      - name: Install Rust toolchain
        uses: dtolnay/rust-toolchain@stable
        with:
          components: rustfmt

      - name: Check formatting
        run: cargo fmt -- --check

      # README § Determinism lists the f32 / f64 modules; the list is
      # generated from src/ and drifts silently when a module is added (1.2.0)
      - name: README f32 module row matches src/
        run: python3 scripts/f32_modules.py --check

      # 配線ガード: 実装したが production から呼ばれていない pub item と、理由の無い
      # allow(dead_code) の新規追加を止める (既存分は scripts/wiring-baseline.txt)
      - name: wiring-guard oracle (検査器自身の試験)
        run: python3 scripts/test_wiring_guard.py
      - name: wiring-guard (新規の未配線 / 理由の無い dead_code が無い)
        run: python3 scripts/wiring_guard.py
      - name: Predictable randomness guard (test first)
        run: python3 scripts/test_csprng_guard.py
      - name: Predictable randomness guard
        run: python3 scripts/csprng_guard.py
      # 台帳 (docs/wiring-status.md / docs/oracle-status.md) の生成器の試験: 実際の
      # wiring_guard の出力の形を食わせる (違反を 1 件も拾えない空振りを止める)
      - name: status generators oracle (台帳の生成器自身の試験)
        run: python3 scripts/test_gen_status.py

  doc:
    name: Doc (RUSTDOCFLAGS=-Dwarnings)
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v5
      - uses: dtolnay/rust-toolchain@stable
      - uses: Swatinem/rust-cache@v2

      # Both the default and the docs.rs feature set: feature-gated modules
      # (gpu_bridge / ffi) had 9 broken intra-doc links that the default-only
      # job never saw (1.2.0).
      - name: cargo doc (no-deps, default features)
        run: cargo doc --lib --no-deps
        env:
          RUSTDOCFLAGS: "-Dwarnings"
      - name: cargo doc (no-deps, docs.rs feature set)
        run: cargo doc --lib --no-deps --features "std,simd,parallel,ffi,gpu-solver-bridge"
        env:
          RUSTDOCFLAGS: "-Dwarnings"

  actionlint:
    name: actionlint (workflow YAML 検証)
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v5
      - name: Run actionlint
        uses: reviewdog/action-actionlint@v1
        with:
          fail_level: error
          reporter: github-check

  feature-powerset:
    # 2026-09-15 追加: feature 組合せの compile 網羅
    # depth 2 = 単体 + 2 個組合せ (19 通り、Mac 実測 green)
    # - wasm は `std` 必須 (`compile_error!`) なので powerset から外し、std,wasm を別 step で check
    # - python (pyo3 build) / neural / replay / analytics (sibling crate 実体必要) は除外
    name: Feature powerset (cargo-hack)
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v5
      - uses: dtolnay/rust-toolchain@stable
      - uses: Swatinem/rust-cache@v2
      - name: Install cargo-hack
        uses: taiki-e/install-action@cargo-hack
      # rustc --crate-type rlib: combos without `std` cannot build the cdylib
      # (no panic_handler / allocator), see "Build (no_std, rlib)" above.
      - name: Powerset (depth 2, wasm/python/bridge 除外)
        run: cargo hack rustc --lib --crate-type rlib --feature-powerset --depth 2 --exclude-features python,neural,replay,analytics,wasm
      - name: std + wasm (相互排他 ffi なし)
        run: cargo check --lib --no-default-features --features std,wasm