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
13 changes: 13 additions & 0 deletions .github/workflows/ci-docs.yml
Original file line number Diff line number Diff line change
Expand Up @@ -247,6 +247,19 @@ jobs:
path: code-fence-output.txt
retention-days: 7

# ============================================================================
# DOC CLAIMS - Check doc comments for misleading claims
# ============================================================================
doc-claims:
name: Doc Claims Check
runs-on: ubuntu-latest

steps:
- uses: actions/checkout@v6

- name: Check doc comment accuracy
run: ./scripts/ci/check-doc-claims.sh

link-check:
name: Link Check
runs-on: ubuntu-latest
Expand Down
5 changes: 5 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -62,3 +62,8 @@ package.json
node_modules/
*.pyc
*.pyo

pre-commit.txt
pre-commit.log
pre-push.txt
pre-push.log
5 changes: 5 additions & 0 deletions .llm/context.md
Original file line number Diff line number Diff line change
Expand Up @@ -216,18 +216,23 @@ For protocol tests that poll in loops (`poll_remote_clients()` / protocol `poll(

**Exclude:** internal refactoring, test improvements, doc-only changes, CI/tooling, lint fixes.

**Unreleased code rule:** Never add separate "Fixed" or "Changed" entries for code that has not yet been released. Fixes to unreleased features should be folded into the existing "Added" entry describing that feature. The changelog should describe the final shipped state, not intermediate development history.

## Mandatory Linting

- **After Rust changes:** `cargo fmt && cargo clippy --all-targets --features tokio,json` (or `cargo c`)
- **After workflow changes:** `actionlint` (no exceptions)
- **After doc changes:** `cargo doc --no-deps`
- **After markdown changes:** `npx markdownlint 'file.md' --config .markdownlint.json --fix`
- **After shell-script changes:** `bash scripts/ci/check-shell-portability.sh`
- **After `.llm/` changes:** All `.md` files under `.llm/` must be **300 lines or fewer** (enforced by pre-commit hook `llm-line-limit`)
- **Link validation:** `./scripts/docs/check-links.sh`
- **Spell check:** `typos`
- **Vale (advisory):** `vale docs/` -- checks prose quality, non-blocking in CI
- **Full pre-commit:** `cargo fmt && cargo clippy --all-targets --features tokio,json && cargo nextest run --no-capture`

Shell regex portability rule: avoid PCRE-style escapes in `grep -E`/`sed -E` (`\b`, `\s`, `\w`, etc.). Use POSIX-safe classes like `[[:space:]]`, `[[:alnum:]_]`, and token boundaries `(^|[^[:alnum:]_])word([^[:alnum:]_]|$)`.

## Skill Code Examples

Code examples in `.llm/skills/` must follow zero-panic rules with these exceptions:
Expand Down
59 changes: 39 additions & 20 deletions .llm/skills/ci-cd-tooling/wiki-sync.md
Original file line number Diff line number Diff line change
Expand Up @@ -15,12 +15,12 @@ Fix content in `docs/`, then re-sync.

## Link Format Rules

| Context | Format | Example |
|---------|--------|---------|
| `docs/` files | Lowercase with `.md` | `[Guide](user-guide.md)` |
| `wiki/` files | PascalCase, no extension | `[Guide](User-Guide)` |
| `wiki/_Sidebar.md` | PascalCase, no extension | `[User Guide](User-Guide)` |
| External (non-docs) | Full GitHub URL | `[Code](https://github.com/.../blob/main/src/lib.rs)` |
| Context | Format | Example |
| ------------------- | ------------------------ | ----------------------------------------------------- |
| `docs/` files | Lowercase with `.md` | `[Guide](user-guide.md)` |
| `wiki/` files | PascalCase, no extension | `[Guide](User-Guide)` |
| `wiki/_Sidebar.md` | PascalCase, no extension | `[User Guide](User-Guide)` |
| External (non-docs) | Full GitHub URL | `[Code](https://github.com/.../blob/main/src/lib.rs)` |

Use standard markdown `[Text](Page)` syntax in sidebar -- NOT wiki-link `[[Page|Text]]` syntax (has URL generation bugs).

Expand Down Expand Up @@ -57,6 +57,14 @@ docs/*.md --> sync-wiki.py
+-- Add SYNC comment header
+-- Generate _Sidebar.md
--> wiki/*.md

Pre-commit enforcement now runs this sequence for docs/wiki changes:

1. `sync-wiki` regenerates `wiki/*.md` from `docs/`
2. `wiki-consistency` validates links/sidebar/mapping integrity
3. `check-sync-headers` validates reciprocal `<!-- SYNC: ... -->` headers

This prevents committing stale or manually-edited wiki mirrors.
```

## MkDocs Conversion Patterns
Expand Down Expand Up @@ -124,26 +132,37 @@ python3 scripts/docs/validate-wiki-output.py # Check rendering issues
3. All wiki pages have sidebar entries
4. No special characters in wiki-link display text

`sync-wiki.py` also validates that every `WIKI_STRUCTURE` page appears in
the generated sidebar and fails fast if any mapped page is missing.

`sync-wiki.py` now enforces deterministic writer normalization for generated
markdown: non-empty outputs end with exactly one LF newline (trailing
whitespace/newlines are normalized). This prevents churn with
`end-of-file-fixer` and keeps repeated sync runs idempotent.

Sidebar coverage validation runs before any wiki writes, so an invalid sidebar
template fails early without leaving partial regenerated output.

### Common Errors

| Error | Fix |
|-------|-----|
| Error | Fix |
| -------------------------------- | ---------------------------- |
| Link points to non-existent page | Add file or fix sidebar link |
| `docs/file.md` not mapped | Add to `WIKI_STRUCTURE` |
| Wiki page has no sidebar entry | Add to `generate_sidebar()` |
| Stale WIKI_STRUCTURE mapping | Remove entry |
| `docs/file.md` not mapped | Add to `WIKI_STRUCTURE` |
| Wiki page has no sidebar entry | Add to `generate_sidebar()` |
| Stale WIKI_STRUCTURE mapping | Remove entry |

## Markdown Link Validation

### Relative Path Resolution

Links resolve from the directory containing the markdown file:

| From | To Root | Example |
|------|---------|---------|
| `docs/` | `../` | `[README](../README.md)` |
| `.github/` | `../` | `[Context](../.llm/context.md)` |
| `.llm/skills/<category>/` | `../../../` | `[README](../../../README.md)` |
| From | To Root | Example |
| ------------------------- | ----------- | ------------------------------- |
| `docs/` | `../` | `[README](../README.md)` |
| `.github/` | `../` | `[Context](../.llm/context.md)` |
| `.llm/skills/<category>/` | `../../../` | `[README](../../../README.md)` |

### Heading Anchor Generation Rules

Expand All @@ -153,11 +172,11 @@ Links resolve from the directory containing the markdown file:
4. ` / ` becomes `--`
5. `~` removed

| Heading | Anchor |
|---------|--------|
| `## Quick Start` | `#quick-start` |
| Heading | Anchor |
| ------------------------------------ | ------------------------------ |
| `## Quick Start` | `#quick-start` |
| `## LAN / Local Network (~20ms RTT)` | `#lan--local-network-20ms-rtt` |
| `## Web / WASM Integration` | `#web--wasm-integration` |
| `## Web / WASM Integration` | `#web--wasm-integration` |

### Pipe Escaping in Tables

Expand Down
35 changes: 35 additions & 0 deletions .pre-commit-config.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -102,6 +102,20 @@ repos:
pass_filenames: false
files: '\.rs$'

- id: check-doc-claims
name: check doc comment accuracy
entry: bash scripts/ci/check-doc-claims.sh
language: system
pass_filenames: false
files: '\.rs$'

- id: check-derive-bounds
name: check derive bounds
entry: bash scripts/ci/check-derive-bounds.sh
language: system
pass_filenames: false
files: '\.rs$'

# ══════════════════════════════════════════════════════════════════════
# Documentation checks
# ══════════════════════════════════════════════════════════════════════
Expand Down Expand Up @@ -134,6 +148,13 @@ repos:
pass_filenames: false
files: '\.md$'

- id: sync-wiki
name: sync wiki mirrors
entry: python scripts/docs/sync-wiki.py
language: python
pass_filenames: false
files: '^(docs/.*\.md|wiki/.*\.md|scripts/docs/sync-wiki\.py)$'

- id: wiki-consistency
name: wiki consistency check
entry: python scripts/docs/check-wiki-consistency.py
Expand All @@ -148,6 +169,13 @@ repos:
files: '^(docs/.*\.md|wiki/.*\.md)$'
pass_filenames: false

- id: changelog-unreleased-rule
name: changelog unreleased code rule
entry: python scripts/hooks/check-changelog-unreleased.py
language: python
pass_filenames: false
files: '^CHANGELOG\.md$'

- id: llm-line-limit
name: check .llm file line limit (300)
entry: python scripts/hooks/check-llm-line-limit.py
Expand Down Expand Up @@ -189,6 +217,13 @@ repos:
files: '^scripts/(hooks/|build/|docs/|verification/|ci/)?[a-z][^/]*\.py$'
pass_filenames: true

- id: check-shell-portability
name: check shell portability
entry: bash scripts/ci/check-shell-portability.sh
language: system
files: '\.sh$'
pass_filenames: false

# ══════════════════════════════════════════════════════════════════════
# CI validation (skip gracefully if tools not installed)
# ══════════════════════════════════════════════════════════════════════
Expand Down
3 changes: 2 additions & 1 deletion .typos.toml
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,8 @@ resimulates = "resimulates"
resimulating = "resimulating"

# Mathematical/algorithmic variable names
ba = "ba" # b minus a
ba = "ba" # b minus a
ser = "ser" # Mermaid state diagram alias in docs/replay.md and wiki/Replay.md

# Technical terms
clonable = "clonable"
Expand Down
28 changes: 28 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,34 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0

## [Unreleased]

## [0.8.0]

### Changed

- **Breaking:** `FortressEvent::ReplayDesync` — new variant added. Since `FortressEvent` is not `#[non_exhaustive]`, exhaustive matches must now handle this variant.
- **Breaking:** `InvalidFrameReason::ReplayExhausted` — new variant added. Since `InvalidFrameReason` is not `#[non_exhaustive]`, exhaustive matches must now handle this variant.
- **Breaking:** `Config::Input` now requires `Eq` in addition to `PartialEq`. Types used as `Config::Input` must derive or implement `Eq`. This ensures reflexive equality, which is a correctness requirement for deterministic rollback — non-reflexive types (e.g., floats with `NaN`) would cause phantom prediction misses and unnecessary rollbacks. All integer and struct-of-integer types already implement `Eq`; add `#[derive(Eq)]` to any custom input types that are missing it.

### Added

- `ReplaySession::new_with_validation()` constructor that enables checksum validation mode, emitting `SaveGameState` requests, comparing checksums against the replay recording, and flushing final-frame validation when `events()` is drained after completion
- `ReplaySession::is_validating()` accessor to check if checksum validation mode is enabled
- `SessionBuilder::start_replay_session_with_validation()` builder method for creating a validation-enabled replay session
- `SessionTelemetry` trait for observing session performance events (rollbacks, prediction misses, frame advances, network stats)
- `CollectingTelemetry` test helper that accumulates `TelemetryEvent` values for assertions
- `TelemetryEvent` enum with `Rollback`, `PredictionMiss`, `NetworkStatsUpdate`, and `FrameAdvance` variants
- `SessionBuilder::with_telemetry()` to attach a telemetry observer to P2P sessions
- `Replay<I>` type for recorded match data with `to_bytes()` / `from_bytes()` serialization using deterministic bincode codec
- `ReplayMetadata` type containing library version, player count, total frame count, and skipped frame count
- `ReplaySession<T>` session type implementing `Session<T>` for deterministic replay playback
- `SessionBuilder::with_recording(bool)` to enable input recording (including game state checksums) during P2P sessions
- `SessionBuilder::start_replay_session(replay)` to create a replay playback session
- `P2PSession::is_recording()` to check if replay recording is active
- `P2PSession::into_replay()` to extract the recorded `Replay` after a session ends (consumes the session)
- `P2PSession::take_replay()` to extract the recorded `Replay` without consuming the session (recording stops after extraction)
- `Replay::validate()` to verify internal consistency of replay data
- Re-exports `Replay`, `ReplayMetadata`, and `ReplaySession` in prelude

## [0.7.0]

### Added
Expand Down
21 changes: 0 additions & 21 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

8 changes: 6 additions & 2 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -320,6 +320,12 @@ renamed_function_params = "warn" # Consistent parameter names across
try_err = "warn" # Use ? instead of Err(e)?
undocumented_unsafe_blocks = "warn" # Require SAFETY comments on unsafe

# cargo-shear: macroquad and z3 are optional deps behind feature flags
# (graphical-examples and z3-verification). They cannot be dev-dependencies
# because dev-deps cannot be feature-gated in Cargo.
[package.metadata.cargo-shear]
ignored = ["macroquad", "z3"]

[features]
sync-send = []
# Enable runtime invariant checking in release builds (for debugging production issues)
Expand Down Expand Up @@ -379,8 +385,6 @@ proptest = "1.11"
serde_json = "1.0"
# Benchmarking
criterion = { version = "0.8", features = ["html_reports"] }
# Additional testing utilities
arbitrary = { version = "1.3", features = ["derive"] }
# Macro utilities for data-driven tests
pastey = "0.2"

Expand Down
2 changes: 1 addition & 1 deletion benches/p2p_session.rs
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@ use std::hint::black_box;
use std::net::SocketAddr;

/// Simple test input type for benchmarking
#[derive(Copy, Clone, PartialEq, Default, Serialize, Deserialize, Debug)]
#[derive(Copy, Clone, PartialEq, Eq, Default, Serialize, Deserialize, Debug)]
struct BenchInput {
buttons: u8,
stick_x: i8,
Expand Down
2 changes: 1 addition & 1 deletion docs/architecture.md
Original file line number Diff line number Diff line change
Expand Up @@ -1099,7 +1099,7 @@ Compile-time parameterization bundles all type requirements:
```rust
// Default (without `sync-send` feature):
pub trait Config: 'static {
type Input: Copy + Clone + PartialEq + Default + Serialize + DeserializeOwned;
type Input: Copy + Clone + PartialEq + Eq + Default + Serialize + DeserializeOwned;
type State;
type Address: Clone + PartialEq + Eq + PartialOrd + Ord + Hash + Debug;
}
Expand Down
1 change: 1 addition & 0 deletions docs/fortress-vs-ggrs.md
Original file line number Diff line number Diff line change
Expand Up @@ -329,6 +329,7 @@ pub enum InvalidFrameReason {
NotConfirmed { confirmed_frame: Frame },
NullOrNegative,
MissingState,
ReplayExhausted { last_frame: Frame },
Custom(&'static str),
}
```
Expand Down
2 changes: 1 addition & 1 deletion docs/index.md
Original file line number Diff line number Diff line change
Expand Up @@ -83,7 +83,7 @@ Get up and running with Fortress Rollback in minutes.
use std::net::SocketAddr;

// Define your input and state types
#[derive(Copy, Clone, PartialEq, Default, Serialize, Deserialize)]
#[derive(Copy, Clone, PartialEq, Eq, Default, Serialize, Deserialize)]
struct MyInput { buttons: u8 }

#[derive(Clone, Serialize, Deserialize)]
Expand Down
29 changes: 29 additions & 0 deletions docs/migration.md
Original file line number Diff line number Diff line change
Expand Up @@ -151,6 +151,35 @@ struct MyAddress {
}
```

## Input Trait Bounds (Breaking Change)

`Config::Input` now requires `Eq` in addition to `PartialEq`. This ensures reflexive
equality for deterministic rollback; non-reflexive types (e.g., `f32`, `f64`) would cause
phantom prediction misses because `NaN != NaN` can make the engine treat identical inputs
as different, triggering unnecessary rollbacks.

Most custom input types only need an extra derive:

```rust
// Before
#[derive(Copy, Clone, PartialEq, Default, Serialize, Deserialize)]
struct MyInput {
buttons: u8,
stick_x: i8,
}

// After
#[derive(Copy, Clone, PartialEq, Eq, Default, Serialize, Deserialize)]
struct MyInput {
buttons: u8,
stick_x: i8,
}
```

> **Note:** All primitive integer types (`u8`, `i8`, `u16`, `i16`, `u32`, `i32`, `u64`,
> `i64`, `u128`, `i128`, `usize`, `isize`) and `bool` already implement `Eq`, so input
> structs composed entirely of these types only need the added derive.

## Features

The `sync-send` feature flag remains compatible. Fortress Rollback adds several new features:
Expand Down
Loading
Loading