pgn2 1.5.0 → 2.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (63) hide show
  1. checksums.yaml +4 -4
  2. data/.github/workflows/ci.yml +34 -3
  3. data/.github/workflows/native.yml +32 -0
  4. data/.github/workflows/publish.yml +44 -2
  5. data/.github/workflows/release-gems.yml +40 -0
  6. data/.github/workflows/release.yml +52 -5
  7. data/.gitignore +9 -1
  8. data/.rubocop.yml +46 -6
  9. data/CHANGELOG.md +148 -0
  10. data/Gemfile +3 -0
  11. data/NOTICE.md +21 -0
  12. data/README.md +107 -5
  13. data/Rakefile +35 -10
  14. data/TODO.md +64 -0
  15. data/bench/baseline_moves.txt +38 -4
  16. data/bench/baseline_parse.txt +4 -4
  17. data/bench/cross_check.rb +61 -0
  18. data/bench/legal_moves.rb +38 -0
  19. data/bench/perft.rb +32 -0
  20. data/bench/profile_moves.rb +93 -0
  21. data/docs/superpowers/plans/2026-08-13-attack-masks-plan.md +51 -0
  22. data/docs/superpowers/plans/2026-08-13-perf-internals-plan.md +883 -0
  23. data/docs/superpowers/plans/2026-08-13-rust-bitboard-perft-plan.md +2442 -0
  24. data/docs/superpowers/plans/2026-08-14-chessie-migration.md +722 -0
  25. data/docs/superpowers/plans/2026-08-14-rust-integration-plan.md +444 -0
  26. data/docs/superpowers/specs/2026-08-13-attack-masks-design.md +57 -0
  27. data/docs/superpowers/specs/2026-08-13-perf-internals-design.md +111 -0
  28. data/docs/superpowers/specs/2026-08-13-rust-bitboard-perft-design.md +270 -0
  29. data/docs/superpowers/specs/2026-08-14-rust-integration-design.md +217 -0
  30. data/ext/pgn2_native/Cargo.lock +321 -0
  31. data/ext/pgn2_native/Cargo.toml +19 -0
  32. data/ext/pgn2_native/extconf.rb +8 -0
  33. data/ext/pgn2_native/pgn2-bitboard/Cargo.toml +10 -0
  34. data/ext/pgn2_native/pgn2-bitboard/src/board.rs +32 -0
  35. data/ext/pgn2_native/pgn2-bitboard/src/lib.rs +12 -0
  36. data/ext/pgn2_native/pgn2-bitboard/src/moves.rs +121 -0
  37. data/ext/pgn2_native/pgn2-bitboard/src/perft.rs +81 -0
  38. data/ext/pgn2_native/pgn2_native/Cargo.toml +11 -0
  39. data/ext/pgn2_native/pgn2_native/src/lib.rs +54 -0
  40. data/lib/pgn/bitboard.rb +13 -0
  41. data/lib/pgn/board.rb +64 -0
  42. data/lib/pgn/fen.rb +35 -46
  43. data/lib/pgn/game.rb +22 -13
  44. data/lib/pgn/move.rb +19 -15
  45. data/lib/pgn/move_calculator.rb +13 -1
  46. data/lib/pgn/notation.rb +24 -29
  47. data/lib/pgn/position.rb +60 -19
  48. data/lib/pgn/serializer.rb +13 -18
  49. data/lib/pgn/version.rb +1 -1
  50. data/lib/pgn/zobrist.rb +53 -0
  51. data/lib/pgn.rb +2 -0
  52. data/pgn2.gemspec +17 -10
  53. data/spec/bitboard_spec.rb +54 -0
  54. data/spec/board_spec.rb +53 -0
  55. data/spec/fen_spec.rb +65 -65
  56. data/spec/game_spec.rb +52 -15
  57. data/spec/lexer_spec.rb +5 -5
  58. data/spec/notation_spec.rb +5 -0
  59. data/spec/parser_spec.rb +8 -1
  60. data/spec/position_spec.rb +128 -27
  61. data/spec/serializer_spec.rb +4 -4
  62. data/spec/zobrist_spec.rb +46 -0
  63. metadata +97 -35
@@ -0,0 +1,722 @@
1
+ # Chessie Backend Migration Implementation Plan
2
+
3
+ > **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking.
4
+
5
+ **Goal:** Replace the hand-rolled `pgn2-bitboard` chess engine on `main`
6
+ with a thin adapter over the `chessie` crate (MPL-2.0), keeping the
7
+ `pgn2_native` magnus binding and the Ruby `PGN::Bitboard::Engine`
8
+ surface byte-for-byte identical and ~4–6× faster.
9
+
10
+ **Architecture:** `pgn2-bitboard` becomes a ~3-file adapter crate
11
+ (`board.rs`, `moves.rs`, `perft.rs` tests) that wraps `chessie::Game` and
12
+ exposes exactly the public API the `pgn2_native` binding already
13
+ consumes (`Board::from_fen`/`perft`/`legal_moves`, `moves::uci_parse`,
14
+ `Move::to_uci`/`same_target`, `MoveList`). The five hand-rolled
15
+ chess-logic modules (`square`, `piece`, `attacks`, `magics`, the old
16
+ `moves`/`legality`/`perft` impls) are deleted. The `pgn2_native`
17
+ binding crate is **not modified** — it keeps importing
18
+ `pgn2_bitboard::Board` and `pgn2_bitboard::moves::uci_parse`. The Ruby
19
+ surface (`PGN::Bitboard::Engine#perft`/`#legal_moves`/`#legal?`,
20
+ `PGN::Position#perft`/`#legal_moves` delegations, `bench/perft.rb`) is
21
+ unchanged. License compliance is handled by a `NOTICE`/license
22
+ declaration addition.
23
+
24
+ **Tech Stack:** Rust 2021, `chessie` 2.0 (MPL-2.0, pure-Rust, no C deps),
25
+ `magnus` 0.8, `rb_sys`, RSpec, `cargo test` (perft oracle).
26
+
27
+ ## Global Constraints
28
+
29
+ - The `pgn2_native` binding crate source (`ext/pgn2_native/pgn2_native/src/lib.rs`)
30
+ is **not modified** — the adapter must preserve its consumed API
31
+ verbatim: `pgn2_bitboard::Board` (with `Board::from_fen(&str) -> Result<Board, String>`,
32
+ `Board::perft(&self, depth: u32) -> u64`, `Board::legal_moves(&self) -> MoveList`,
33
+ and `#[derive]` of at least `Default`), `pgn2_bitboard::moves::uci_parse(&str) -> Option<Move>`,
34
+ and `Move` (with `Move::to_uci(self) -> String`, `Move::same_target(self, Move) -> bool`,
35
+ `Move: Copy`).
36
+ - The pure-Ruby suite and the byte-identical FEN/PGN guarantees stay intact;
37
+ no change to `lib/pgn/board.rb`, `notation.rb`, `move_calculator.rb`,
38
+ `position.rb` delegations, or `bench/perft.rb`.
39
+ - The published perft values remain the oracle; `spec/bitboard_spec.rb`
40
+ and `ext/.../pgn2-bitboard/src/perft.rs` both stay green unchanged.
41
+ - `chessie` is MPL-2.0; the gem stays MIT, but MPL attribution is added
42
+ (Task 2). The gem's own source is not relicensed.
43
+ - Commit per task. `Cargo.lock` is committed (gemspec uses `git ls-files`).
44
+ - Only strings and integers cross the Ruby↔Rust boundary (unchanged).
45
+
46
+ ---
47
+
48
+ ## File Structure
49
+
50
+ **Deleted** (hand-rolled chess logic, replaced by `chessie`):
51
+ - `ext/pgn2_native/pgn2-bitboard/src/square.rs`
52
+ - `ext/pgn2_native/pgn2-bitboard/src/piece.rs`
53
+ - `ext/pgn2_native/pgn2-bitboard/src/attacks.rs`
54
+ - `ext/pgn2_native/pgn2-bitboard/src/magics.rs`
55
+
56
+ **Rewritten** (adapter):
57
+ - `ext/pgn2_native/pgn2-bitboard/src/lib.rs` — re-exports only.
58
+ - `ext/pgn2_native/pgn2-bitboard/src/board.rs` — `Board { game: chessie::Game }`
59
+ + `from_fen`/`perft`/`legal_moves`.
60
+ - `ext/pgn2_native/pgn2-bitboard/src/moves.rs` — `Move`/`MoveList` adapter +
61
+ `uci_parse`.
62
+ - `ext/pgn2_native/pgn2-bitboard/src/perft.rs` — perft oracle tests only
63
+ (no engine impl, no `attacks::init`).
64
+
65
+ **Modified:**
66
+ - `ext/pgn2_native/pgn2-bitboard/Cargo.toml` — add `chessie = "2.0"`.
67
+ - `ext/pgn2_native/Cargo.lock` — regenerated (chessie + chessie_types + anyhow + arrayvec).
68
+ - `ext/pgn2_native/Cargo.toml` — update the stale `[profile.test]` comment.
69
+ - `pgn2.gemspec` — declare MPL-2.0 alongside MIT.
70
+ - `NOTICE.md` (new) — chessie MPL-2.0 attribution + source pointer.
71
+ - `docs/superpowers/specs/2026-08-13-rust-bitboard-perft-design.md`,
72
+ `README.md`, `CHANGELOG.md` — reflect chessie backend.
73
+ - `lib/pgn/bitboard.rb`, `.github/workflows/native.yml` — comment cleanup.
74
+
75
+ **Unchanged (verified, not edited):**
76
+ - `ext/pgn2_native/pgn2_native/src/lib.rs` (binding), `pgn2_native/Cargo.toml`,
77
+ `lib/pgn/position.rb`, `bench/perft.rb`, `spec/bitboard_spec.rb`,
78
+ `.github/workflows/release-gems.yml`.
79
+
80
+ ---
81
+
82
+ ## Task 1: Replace `pgn2-bitboard` with a `chessie`-backed adapter
83
+
84
+ The migration core. One cohesive change — the adapter's pieces can't
85
+ be independently rejected (Board/Move/legal_moves are one contract). The
86
+ existing perft oracle (`perft.rs`) and `spec/bitboard_spec.rb` are the
87
+ regression guards; they stay green.
88
+
89
+ **Files:**
90
+ - Create: `ext/pgn2_native/pgn2-bitboard/src/lib.rs` (rewrite)
91
+ - Create: `ext/pgn2_native/pgn2-bitboard/src/board.rs` (rewrite)
92
+ - Create: `ext/pgn2_native/pgn2-bitboard/src/moves.rs` (rewrite)
93
+ - Create: `ext/pgn2_native/pgn2-bitboard/src/perft.rs` (rewrite, tests only)
94
+ - Delete: `square.rs`, `piece.rs`, `attacks.rs`, `magics.rs`
95
+ - Modify: `ext/pgn2_native/pgn2-bitboard/Cargo.toml`
96
+ - Regenerate: `ext/pgn2_native/Cargo.lock`
97
+
98
+ **Interfaces:**
99
+ - Consumes: `chessie 2.0` — `chessie::Game` (`Game::from_fen(&str) -> Result<Game>`,
100
+ `Game::perft(&self, usize) -> u64`, `Game: Clone + Copy + PartialEq + Eq + Default`),
101
+ `chessie::MoveGenIter` (`MoveGenIter::new(&Game) -> impl Iterator<Item = chessie::Move>`),
102
+ `chessie::Move` (`Move::from(&self) -> Square`, `Move::to(&self) -> Square`,
103
+ `Move::promotion(&self) -> Option<PieceKind>`), `chessie::Square` (`Square::index(&self) -> usize`),
104
+ `chessie::PieceKind` (`{Pawn, Knight, Bishop, Rook, Queen, King}`).
105
+ - Produces (preserved verbatim for the binding): `Board` (`from_fen`, `perft`, `legal_moves`,
106
+ `Default`), `Move` (`to_uci`, `same_target`, `Copy`), `MoveList(pub Vec<Move>)` with `iter()`,
107
+ `moves::uci_parse(&str) -> Option<Move>`.
108
+
109
+ - [ ] **Step 1: Add the `chessie` dependency**
110
+
111
+ `ext/pgn2_native/pgn2-bitboard/Cargo.toml`:
112
+ ```toml
113
+ [package]
114
+ name = "pgn2-bitboard"
115
+ version = "0.1.0"
116
+ edition = "2021"
117
+
118
+ [lib]
119
+ crate-type = ["lib"]
120
+
121
+ [dependencies]
122
+ chessie = "2.0"
123
+ ```
124
+
125
+ - [ ] **Step 2: Rewrite `lib.rs` to re-export only the adapter**
126
+
127
+ `ext/pgn2_native/pgn2-bitboard/src/lib.rs`:
128
+ ```rust
129
+ //! Thin adapter over the `chessie` crate exposing the small surface the
130
+ //! `pgn2_native` magnus binding consumes. No chess logic lives here —
131
+ //! `chessie` is the engine. Kept as a separate crate so the perft oracle
132
+ //! stays testable in pure Rust (`cargo test -p pgn2-bitboard`) with no
133
+ //! Ruby in the loop.
134
+
135
+ pub mod board;
136
+ pub mod moves;
137
+ pub mod perft;
138
+
139
+ pub use board::Board;
140
+ pub use moves::{Move, MoveList};
141
+ ```
142
+
143
+ - [ ] **Step 3: Write the `Board` adapter**
144
+
145
+ `ext/pgn2_native/pgn2-bitboard/src/board.rs`:
146
+ ```rust
147
+ use crate::moves::{Move, MoveList};
148
+
149
+ /// A chess position backed by `chessie::Game`. Holds no chess logic of
150
+ /// its own; every operation delegates. `Copy` because `chessie::Game`
151
+ /// is `Copy`; `Default` because the magnus `Engine` wraps it in a
152
+ /// `RefCell` and `#[derive(Default)]`s it.
153
+ #[derive(Clone, Copy, PartialEq, Eq, Default)]
154
+ pub struct Board {
155
+ game: chessie::Game,
156
+ }
157
+
158
+ impl Board {
159
+ pub fn from_fen(fen: &str) -> Result<Board, String> {
160
+ chessie::Game::from_fen(fen)
161
+ .map(|game| Board { game })
162
+ .map_err(|e| e.to_string())
163
+ }
164
+
165
+ pub fn perft(&self, depth: u32) -> u64 {
166
+ self.game.perft(depth as usize)
167
+ }
168
+
169
+ pub fn legal_moves(&self) -> MoveList {
170
+ use chessie::MoveGenIter;
171
+ let moves: Vec<Move> = MoveGenIter::new(&self.game)
172
+ .map(Move::from_chessie)
173
+ .collect();
174
+ MoveList(moves)
175
+ }
176
+ }
177
+ ```
178
+
179
+ > `chessie::Game` derives `Clone, Copy, PartialEq, Eq` (verified in
180
+ > `chessie-2.0.0/src/game.rs:28`) but **not** `Debug`, so `Board` does
181
+ > not derive `Debug` either — nothing the binding or the oracle uses
182
+ > requires `Board: Debug`.
183
+
184
+ - [ ] **Step 4: Write the `Move` / `MoveList` adapter + `uci_parse`**
185
+
186
+ `ext/pgn2_native/pgn2-bitboard/src/moves.rs`:
187
+ ```rust
188
+ /// Adapter move carrying exactly what `to_uci` and `same_target` need:
189
+ /// from-square, to-square, and optional promotion kind. No flag bits,
190
+ /// so a position-free `uci_parse` can produce a comparable token — this
191
+ /// preserves the existing `same_target` semantics (match on
192
+ /// from + to + promo only), so `legal?("e1g1")` finds the castle and
193
+ /// `legal?("e7e8q")` finds that exact promotion.
194
+ #[derive(Clone, Copy, PartialEq, Eq)]
195
+ pub struct Move {
196
+ from: u8, // 0..=63, index = rank * 8 + file
197
+ to: u8,
198
+ promo: Option<Promo>,
199
+ }
200
+
201
+ #[derive(Clone, Copy, PartialEq, Eq)]
202
+ enum Promo { Knight, Bishop, Rook, Queen }
203
+
204
+ impl Move {
205
+ /// Build an adapter `Move` from a `chessie::Move` (a legal move).
206
+ pub(crate) fn from_chessie(m: chessie::Move) -> Self {
207
+ Move {
208
+ from: m.from().index() as u8,
209
+ to: m.to().index() as u8,
210
+ promo: m.promotion().map(|kind| match kind {
211
+ chessie::PieceKind::Knight => Promo::Knight,
212
+ chessie::PieceKind::Bishop => Promo::Bishop,
213
+ chessie::PieceKind::Rook => Promo::Rook,
214
+ chessie::PieceKind::Queen => Promo::Queen,
215
+ // Pawns/Kings never appear as a promotion kind.
216
+ _ => unreachable!("non-promotion PieceKind in Move::promotion"),
217
+ }),
218
+ }
219
+ }
220
+
221
+ /// UCI string: `"e2e4"`, `"e1g1"` (castle, king from→to),
222
+ /// `"e7e8q"` (promotion). Equal to `chessie::Move::to_uci` for
223
+ /// every legal move (castle and en-passant both reduce to from+to
224
+ /// in UCI), so the sorted-UCI output is unchanged.
225
+ pub fn to_uci(self) -> String {
226
+ let mut s = String::with_capacity(5);
227
+ s.push_str(&sq_name(self.from));
228
+ s.push_str(&sq_name(self.to));
229
+ if let Some(p) = self.promo {
230
+ s.push(match p {
231
+ Promo::Knight => 'n',
232
+ Promo::Bishop => 'b',
233
+ Promo::Rook => 'r',
234
+ Promo::Queen => 'q',
235
+ });
236
+ }
237
+ s
238
+ }
239
+
240
+ /// Match on from + to + promo (the pre-existing semantics).
241
+ pub fn same_target(self, other: Move) -> bool {
242
+ self.from == other.from && self.to == other.to && self.promo == other.promo
243
+ }
244
+ }
245
+
246
+ /// Thin wrapper around `Vec<Move>`; the binding calls `.iter()` on it.
247
+ pub struct MoveList(pub Vec<Move>);
248
+ impl MoveList {
249
+ pub fn iter(&self) -> std::slice::Iter<'_, Move> {
250
+ self.0.iter()
251
+ }
252
+ }
253
+
254
+ /// Parse a UCI string into a `Move` token **without** a position.
255
+ /// Only `to_uci`/`same_target` consume the result, so from + to + promo
256
+ /// is all that is needed. Returns `None` on any malformed input.
257
+ pub fn uci_parse(s: &str) -> Option<Move> {
258
+ let b = s.as_bytes();
259
+ if b.len() < 4 { return None; }
260
+ let from = parse_sq(&b[0..2])?;
261
+ let to = parse_sq(&b[2..4])?;
262
+ let promo = if b.len() >= 5 {
263
+ Some(match b[4] {
264
+ b'n' => Promo::Knight,
265
+ b'b' => Promo::Bishop,
266
+ b'r' => Promo::Rook,
267
+ b'q' => Promo::Queen,
268
+ _ => return None,
269
+ })
270
+ } else { None };
271
+ Some(Move { from, to, promo })
272
+ }
273
+
274
+ fn parse_sq(t: &[u8]) -> Option<u8> {
275
+ let f = t[0].checked_sub(b'a')?;
276
+ let r = t[1].checked_sub(b'1')?;
277
+ if f > 7 || r > 7 { return None; }
278
+ Some(r * 8 + f)
279
+ }
280
+
281
+ fn sq_name(idx: u8) -> String {
282
+ let file = (b'a' + (idx & 7)) as char;
283
+ let rank = (b'1' + (idx >> 3)) as char;
284
+ let mut s = String::with_capacity(2);
285
+ s.push(file);
286
+ s.push(rank);
287
+ s
288
+ }
289
+ ```
290
+
291
+ - [ ] **Step 5: Rewrite `perft.rs` as the oracle tests (no engine impl)**
292
+
293
+ `ext/pgn2_native/pgn2-bitboard/src/perft.rs`:
294
+ ```rust
295
+ //! Perft oracle — the published node counts are the test. The chess
296
+ //! logic lives in `chessie`; this file only exercises the adapter's
297
+ //! `Board::from_fen` + `Board::perft` end-to-end in pure Rust (no Ruby).
298
+
299
+ #[cfg(test)]
300
+ mod tests {
301
+ use crate::board::Board;
302
+
303
+ struct Case { fen: &'static str, depth: u32, nodes: u64 }
304
+
305
+ fn run(c: Case) {
306
+ let b = Board::from_fen(c.fen).unwrap();
307
+ assert_eq!(b.perft(c.depth), c.nodes, "fen={} depth={}", c.fen, c.depth);
308
+ }
309
+
310
+ #[test]
311
+ fn perft_startpos() {
312
+ let f = "rnbqkbnr/pppppppp/8/8/8/8/PPPPPPPP/RNBQKBNR w KQkq - 0 1";
313
+ run(Case { fen: f, depth: 1, nodes: 20 });
314
+ run(Case { fen: f, depth: 2, nodes: 400 });
315
+ run(Case { fen: f, depth: 3, nodes: 8902 });
316
+ run(Case { fen: f, depth: 4, nodes: 197281 });
317
+ run(Case { fen: f, depth: 5, nodes: 4865609 });
318
+ }
319
+
320
+ #[test]
321
+ fn perft_kiwipete() {
322
+ let f = "r3k2r/p1ppqpb1/bn2pnp1/3PN3/1p2P3/2N2Q1p/PPPBBPPP/R3K2R w KQkq - 0 1";
323
+ run(Case { fen: f, depth: 1, nodes: 48 });
324
+ run(Case { fen: f, depth: 2, nodes: 2039 });
325
+ run(Case { fen: f, depth: 3, nodes: 97862 });
326
+ run(Case { fen: f, depth: 4, nodes: 4085603 });
327
+ }
328
+
329
+ #[test]
330
+ fn perft_pos3() {
331
+ let f = "8/2p5/3p4/KP5r/1R3p1k/8/4P1P1/8 w - - 0 1";
332
+ run(Case { fen: f, depth: 1, nodes: 14 });
333
+ run(Case { fen: f, depth: 4, nodes: 43238 });
334
+ run(Case { fen: f, depth: 5, nodes: 674624 });
335
+ }
336
+
337
+ #[test]
338
+ fn perft_pos4() {
339
+ let f = "r3k2r/Pppp1ppp/1b3nbN/nP6/BBP1P3/q4N2/Pp1P2PP/R2Q1RK1 w kq - 0 1";
340
+ run(Case { fen: f, depth: 1, nodes: 6 });
341
+ run(Case { fen: f, depth: 3, nodes: 9467 });
342
+ run(Case { fen: f, depth: 4, nodes: 422333 });
343
+ }
344
+
345
+ #[test]
346
+ fn perft_pos5() {
347
+ let f = "rnbq1k1r/pp1Pbppp/2p5/8/2B5/8/PPP1NnPP/RNBQK2R w KQ - 1 8";
348
+ run(Case { fen: f, depth: 3, nodes: 62379 });
349
+ run(Case { fen: f, depth: 4, nodes: 2103487 });
350
+ }
351
+
352
+ #[test]
353
+ fn perft_pos6() {
354
+ let f = "r4rk1/1pp1qppp/p1np1n2/2b1p1B1/2B1P1b1/P1NP1N2/1PP1QPPP/R4RK1 w - - 0 10";
355
+ run(Case { fen: f, depth: 3, nodes: 89890 });
356
+ run(Case { fen: f, depth: 4, nodes: 3894594 });
357
+ }
358
+
359
+ #[test]
360
+ #[ignore] // slow; run with `cargo test -- --ignored`
361
+ fn perft_deep() {
362
+ let s = "rnbqkbnr/pppppppp/8/8/8/8/PPPPPPPP/RNBQKBNR w KQkq - 0 1";
363
+ run(Case { fen: s, depth: 6, nodes: 119060324 });
364
+ let k = "r3k2r/p1ppqpb1/bn2pnp1/3PN3/1p2P3/2N2Q1p/PPPBBPPP/R3K2R w KQkq - 0 1";
365
+ run(Case { fen: k, depth: 5, nodes: 193690690 });
366
+ }
367
+
368
+ // The old `make_unmake_symmetry_via_perft` test is intentionally
369
+ // removed: it exercised the hand-rolled make/unmake internals, which
370
+ // no longer exist. chessie's own test suite covers its make/unmake.
371
+ }
372
+ ```
373
+
374
+ - [ ] **Step 6: Delete the hand-rolled chess-logic modules**
375
+
376
+ ```bash
377
+ cd ext/pgn2_native/pgn2-bitboard
378
+ git rm src/square.rs src/piece.rs src/attacks.rs src/magics.rs
379
+ ```
380
+
381
+ - [ ] **Step 7: Regenerate `Cargo.lock` and run the perft oracle (pure Rust)**
382
+
383
+ ```bash
384
+ cd ext/pgn2_native
385
+ cargo update -p pgn2-bitboard
386
+ cargo test -p pgn2-bitboard
387
+ ```
388
+ Expected: PASS — `perft_startpos`, `perft_kiwipete`, `perft_pos3`,
389
+ `perft_pos4`, `perft_pos5`, `perft_pos6` all green. (These are the same
390
+ canonical counts chessie was verified against in the `exp/chessie-smoke`
391
+ run; pos5 d3 = 62379 is the repo's own canonical value.)
392
+ `perft_deep` is `#[ignore]`; optionally `cargo test -p pgn2-bitboard -- --ignored`.
393
+
394
+ - [ ] **Step 8: Build the whole workspace (binding compiles unchanged)**
395
+
396
+ ```bash
397
+ cd ext/pgn2_native
398
+ cargo build
399
+ ```
400
+ Expected: the `pgn2_native` cdylib compiles with **no edits** to
401
+ `pgn2_native/src/lib.rs` — it still imports `pgn2_bitboard::Board` and
402
+ `pgn2_bitboard::moves::uci_parse`, both preserved by the adapter.
403
+
404
+ - [ ] **Step 9: Compile the extension and run the full Ruby suite**
405
+
406
+ ```bash
407
+ bundle exec rake compile
408
+ bundle exec rspec
409
+ ```
410
+ Expected: all green, including `spec/bitboard_spec.rb` (perft values,
411
+ ArgumentError on bad FEN, sorted UCI legal_moves incl. `e2e4`/`g1f3`/`d2d4`,
412
+ `legal?` true/false, promotions `a7a8q/r/b/n`, castling `e1g1`/`e1c1`).
413
+
414
+ - [ ] **Step 10: Commit**
415
+
416
+ ```bash
417
+ git add ext/pgn2_native/pgn2-bitboard/src ext/pgn2_native/pgn2-bitboard/Cargo.toml ext/pgn2_native/Cargo.lock
418
+ git commit -m "refactor(native): replace hand-rolled bitboard engine with chessie adapter
419
+
420
+ pgn2-bitboard becomes a ~3-file adapter over the chessie 2.0 crate
421
+ (MPL-2.0): Board wraps chessie::Game; perft/legal_moves delegate;
422
+ Move/MoveList/uci_parse preserve the exact API the pgn2_native magnus
423
+ binding consumes, so the binding source is unchanged. Deletes
424
+ square/piece/attacks/magics and the old moves/legality/perft impls.
425
+
426
+ Verified: cargo test -p pgn2-bitboard (perft oracle, all 6 positions)
427
+ + cargo build (workspace) + rake compile + rspec (spec/bitboard_spec.rb)
428
+ all green. Head-to-head on znver5/BMI2: ~4.4-6.1x faster than the
429
+ hand-rolled engine (startpos d6 21.6->95.7 Mnps; kiwipete d5
430
+ 23.2->142.6 Mnps; pos3 d6 18.8->90.7 Mnps)."
431
+ ```
432
+
433
+ ---
434
+
435
+ ## Task 2: MPL-2.0 license compliance
436
+
437
+ `chessie` (and `chessie_types`) are MPL-2.0. The gem stays MIT; this
438
+ task only adds the required attribution and declares the aggregated
439
+ component. No source is relicensed.
440
+
441
+ **Files:**
442
+ - Create: `NOTICE.md`
443
+ - Modify: `pgn2.gemspec`
444
+
445
+ **Interfaces:** none.
446
+
447
+ - [ ] **Step 1: Add the attribution notice**
448
+
449
+ `NOTICE.md`:
450
+ ```markdown
451
+ # Third-Party Notices
452
+
453
+ This gem bundles a compiled Rust extension (`pgn2_native`) that links
454
+ the `chessie` crate.
455
+
456
+ ## chessie
457
+
458
+ - Source: https://crates.io/crates/chessie
459
+ - Repository: https://github.com/duck2/chessie
460
+ - Version: 2.0.x (see `ext/pgn2_native/Cargo.lock` for the exact pinned
461
+ version)
462
+ - License: Mozilla Public License 2.0 (MPL-2.0)
463
+
464
+ `chessie` and its dependency `chessie_types` are MPL-2.0. They are used
465
+ unmodified. The MPL-2.0 license is file-level copyleft: it applies to
466
+ `chessie`'s own source files only and does not change the license of
467
+ this gem's code (MIT). Per MPL-2.0 §3.3, the source of the MPL-licensed
468
+ files is available at the repository URL above (and is reproducibly
469
+ pinned in `ext/pgn2_native/Cargo.lock`).
470
+
471
+ pgn2's own code remains MIT-licensed; see `LICENSE.txt`.
472
+ ```
473
+
474
+ - [ ] **Step 2: Declare the aggregated license in the gemspec**
475
+
476
+ In `pgn2.gemspec`, replace the single `spec.license = 'MIT'` line with a
477
+ plural declaration covering the bundled component:
478
+
479
+ ```ruby
480
+ spec.licenses = ['MIT', 'MPL-2.0']
481
+ ```
482
+
483
+ > RubyGems accepts `spec.licenses` (plural) for aggregated works. The
484
+ > gem's own code is MIT; the bundled `chessie` component is MPL-2.0
485
+ > (documented in `NOTICE.md`). If a single-string license is required by
486
+ > an older toolchain, keep `spec.license = 'MIT'` and rely on
487
+ > `NOTICE.md` for the MPL declaration — but prefer the plural form.
488
+
489
+ - [ ] **Step 3: Verify the gemspec still loads and `bundle install` is clean**
490
+
491
+ ```bash
492
+ bundle exec ruby -e "puts Gem::Specification.load('pgn2.gemspec').licenses.inspect"
493
+ ```
494
+ Expected: prints `["MIT", "MPL-2.0"]`. Then:
495
+
496
+ ```bash
497
+ bundle install
498
+ bundle exec rake compile
499
+ bundle exec rspec
500
+ ```
501
+ Expected: all green (no behavioral change from Task 1).
502
+
503
+ - [ ] **Step 4: Commit**
504
+
505
+ ```bash
506
+ git add NOTICE.md pgn2.gemspec
507
+ git commit -m "docs(license): add MPL-2.0 notice for bundled chessie; declare aggregated license
508
+
509
+ chessie/chessie_types are MPL-2.0 (file-level copyleft). The gem stays
510
+ MIT; NOTICE.md credits chessie and points to its source (pinned in
511
+ Cargo.lock per MPL-2.0 §3.3). gemspec declares both licenses."
512
+ ```
513
+
514
+ ---
515
+
516
+ ## Task 3: Update design spec, README, and CHANGELOG
517
+
518
+ The design doc and README describe the hand-rolled magic/pext engine;
519
+ that is now superseded by the chessie adapter. This task records the
520
+ change honestly without rewriting every stale line — a status banner +
521
+ targeted section updates + a CHANGELOG entry.
522
+
523
+ **Files:**
524
+ - Modify: `docs/superpowers/specs/2026-08-13-rust-bitboard-perft-design.md`
525
+ - Modify: `README.md`
526
+ - Modify: `CHANGELOG.md`
527
+
528
+ **Interfaces:** none.
529
+
530
+ - [ ] **Step 1: Prepend a status banner to the design spec**
531
+
532
+ At the very top of `docs/superpowers/specs/2026-08-13-rust-bitboard-perft-design.md`,
533
+ above the `**Goal:**` line, insert:
534
+
535
+ ```markdown
536
+ > **Status (2026-08-14):** The hand-rolled magic/pext engine described
537
+ > below has been replaced by a thin adapter over the `chessie` crate
538
+ > (MPL-2.0). See `docs/superpowers/plans/2026-08-14-chessie-migration.md`.
539
+ > The "Engine details (magic bitboards)" section is retained as the
540
+ > historical design rationale for the since-removed hand-rolled engine;
541
+ > the shipped engine is now `chessie` via the `pgn2-bitboard` adapter.
542
+ > The Ruby API surface, FEN-keyed boundary, and global constraints
543
+ > below are unchanged.
544
+ ```
545
+
546
+ - [ ] **Step 2: Update the Architecture section's crate descriptions**
547
+
548
+ In `docs/superpowers/specs/2026-08-13-rust-bitboard-perft-design.md`, in
549
+ the `## Architecture` numbered list, replace the `pgn2-bitboard` item
550
+ (item 1) with:
551
+
552
+ ```markdown
553
+ 1. `pgn2-bitboard` (**lib**, no Ruby dependency): a thin adapter over
554
+ the `chessie` crate (MPL-2.0). `Board` wraps `chessie::Game`; `perft`
555
+ and `legal_moves` delegate; `Move`/`MoveList`/`uci_parse` expose the
556
+ small surface the binding consumes. No chess logic lives in this
557
+ crate — `chessie` is the engine. **Unit-testable in pure Rust**
558
+ (`cargo test`) against published perft values — no Ruby in the loop.
559
+ ```
560
+ Leave item 2 (`pgn2_native`) and the boundary principle unchanged.
561
+
562
+ - [ ] **Step 3: Add a CHANGELOG entry**
563
+
564
+ In `CHANGELOG.md`, under the next-unreleased section at the top, add:
565
+
566
+ ```markdown
567
+ ### Changed (native)
568
+
569
+ - The native bitboard engine is now a thin adapter over the `chessie`
570
+ crate (MPL-2.0) instead of a hand-rolled magic/pext engine. The Ruby
571
+ `PGN::Bitboard::Engine` surface (`#perft`, `#legal_moves`, `#legal?`)
572
+ and `PGN::Position#perft`/`#legal_moves` delegations are unchanged.
573
+ ~4–6× faster perft on x86-64/BMI2 (e.g. Kiwipete d5: ~23 → ~143 Mnps).
574
+ - The gem now bundles an MPL-2.0 component (`chessie`); the gem's own
575
+ code remains MIT. See `NOTICE.md`. `pgn2.gemspec` declares both
576
+ licenses.
577
+ ```
578
+
579
+ - [ ] **Step 4: Update the README's native-engine description**
580
+
581
+ In `README.md`, find the native-engine section (the one added in commit
582
+ `1940baa` "ships with the gem, not 'optional'") and replace any phrase
583
+ describing the engine as "magic bitboard"/"pext"/"hand-rolled" with
584
+ "`chessie`-backed bitboard engine (MPL-2.0)". Add one line: "The engine
585
+ is the `chessie` crate; see `NOTICE.md` for license attribution."
586
+
587
+ > If the README has no engine-internals paragraph, just ensure it does
588
+ > not claim "magic bitboards" or "pext"; the public API description
589
+ > stays valid.
590
+
591
+ - [ ] **Step 5: Verify docs render and the suite still passes**
592
+
593
+ ```bash
594
+ bundle exec rake compile && bundle exec rspec
595
+ ```
596
+ Expected: green (docs-only change; no code touched).
597
+
598
+ - [ ] **Step 6: Commit**
599
+
600
+ ```bash
601
+ git add docs/superpowers/specs/2026-08-13-rust-bitboard-perft-design.md README.md CHANGELOG.md
602
+ git commit -m "docs: record chessie backend migration (spec banner, README, CHANGELOG)"
603
+ ```
604
+
605
+ ---
606
+
607
+ ## Task 4: Clean up stale references and verify CI
608
+
609
+ Comments and a workspace-manifest note still describe the removed
610
+ hand-rolled engine ("magic-bitboard search", "optional"). This task
611
+ fixes the stale strings and runs the full verification matrix the CI
612
+ uses, so the tree is release-ready.
613
+
614
+ **Files:**
615
+ - Modify: `ext/pgn2_native/Cargo.toml`
616
+ - Modify: `lib/pgn/bitboard.rb`
617
+ - Modify: `.github/workflows/native.yml`
618
+
619
+ **Interfaces:** none.
620
+
621
+ - [ ] **Step 1: Fix the workspace manifest's stale comment**
622
+
623
+ In `ext/pgn2_native/Cargo.toml`, replace the comment above
624
+ `[profile.test]`:
625
+
626
+ ```toml
627
+ # The perft oracle does real work; optimize tests so `cargo test` runs
628
+ # in release-like time instead of minutes.
629
+ [profile.test]
630
+ opt-level = 2
631
+ ```
632
+ (The old comment referenced "magic-bitboard search", which no longer
633
+ exists.)
634
+
635
+ - [ ] **Step 2: Fix the Ruby shim's stale comment**
636
+
637
+ In `lib/pgn/bitboard.rb`, change the header comment
638
+ "Load the native bitboard engine" / any "magic/pext" wording to
639
+ "Load the `chessie`-backed native bitboard engine". Do not change the
640
+ `require` line or the `rescue` behavior.
641
+
642
+ - [ ] **Step 3: Fix the CI workflow's stale framing**
643
+
644
+ In `.github/workflows/native.yml`, change the top comment block and the
645
+ job name/comment that say "optional Rust bitboard perft backend" /
646
+ "pure-Rust engine's `cargo test`" to reflect that the backend is
647
+ required and `chessie`-backed, e.g.:
648
+
649
+ ```yaml
650
+ # Builds and tests the required Rust bitboard perft backend (a thin
651
+ # adapter over the `chessie` crate): runs the perft oracle via
652
+ # `cargo test`, compiles the native extension via `rake compile`, and
653
+ # runs the full RSpec suite (which loads the ext).
654
+ ```
655
+ Do not change the step commands (`cargo test --manifest-path ...`,
656
+ `bundle exec rake compile`, `bundle exec rspec`) — only the comments.
657
+
658
+ - [ ] **Step 4: Run the full CI-equivalent verification matrix**
659
+
660
+ ```bash
661
+ cargo test --manifest-path ext/pgn2_native/Cargo.toml
662
+ bundle exec rake compile
663
+ bundle exec rspec
664
+ bundle exec rubocop
665
+ ```
666
+ Expected: `cargo test` green (perft oracle); `rake compile` builds the
667
+ cdylib; `rspec` all green; `rubocop` clean (no Ruby code changed, so no
668
+ new offenses).
669
+
670
+ - [ ] **Step 5: Sanity-check the cross-compile path is unaffected**
671
+
672
+ `chessie` is pure Rust (`chessie_types`, `anyhow`, `arrayvec` — no C
673
+ deps), so `rake-compiler-dock` cross-compilation in
674
+ `.github/workflows/release-gems.yml` is unaffected. Confirm no
675
+ workspace/Cargo.toml change re-introduces a C dependency:
676
+
677
+ ```bash
678
+ cd ext/pgn2_native && cargo tree | grep -iE "\[build-dependencies\]|cc |cmake |libc " | head
679
+ ```
680
+ Expected: empty (no C/link build-deps introduced).
681
+
682
+ - [ ] **Step 6: Commit**
683
+
684
+ ```bash
685
+ git add ext/pgn2_native/Cargo.toml lib/pgn/bitboard.rb .github/workflows/native.yml
686
+ git commit -m "chore(native): drop stale magic/pext references in comments; verify CI matrix
687
+
688
+ Workspace manifest, Ruby shim, and native.yml comments now describe the
689
+ chessie-backed (required) backend instead of the removed hand-rolled
690
+ magic/pext engine. chessie is pure Rust, so cross-compile via
691
+ rake-compiler-dock is unaffected."
692
+ ```
693
+
694
+ ---
695
+
696
+ ## Self-Review
697
+
698
+ **1. Spec coverage.** The migration decision (accept MPL-2.0, adopt
699
+ chessie) is fully covered: Task 1 = the adapter (the engine swap),
700
+ Task 2 = the MPL-2.0 obligation the decision accepted, Task 3 = docs
701
+ honesty, Task 4 = release-readiness. The binding crate and Ruby surface
702
+ are explicitly unchanged (Global Constraints + each task's "unchanged"
703
+ list). The perft oracle (`perft.rs`) and `spec/bitboard_spec.rb` are
704
+ the regression guards and are referenced in Task 1's verification.
705
+
706
+ **2. Placeholder scan.** No TBD/TODO/"implement later". Every code step
707
+ contains full file contents. The two conditional phrasings ("If the
708
+ README has no engine-internals paragraph…", "If a single-string license
709
+ is required…") give concrete fallbacks, not placeholders.
710
+
711
+ **3. Type consistency.** `Board` (`from_fen`, `perft(&self, u32)`,
712
+ `legal_moves(&self) -> MoveList`, `Default`) — matches the binding's
713
+ `Board::from_fen(&fen)`, `self.0.borrow().perft(depth)`,
714
+ `self.0.borrow().legal_moves()`, and `RefCell<Board>` `Default`. `Move`
715
+ (`to_uci(self)`, `same_target(self, Move)`, `Copy`) — matches the
716
+ binding's `m.to_uci()` and `m.same_target(parsed)` called on `&Move`
717
+ from `MoveList::iter()` (works because `Move: Copy`). `MoveList(pub
718
+ Vec<Move>)` with `iter()` — matches the binding's
719
+ `legal_moves().iter()`. `moves::uci_parse(&str) -> Option<Move>` —
720
+ matches `pgn2_bitboard::moves::uci_parse(&uci)`. Adapter `Move` uses
721
+ `from`/`to`/`promo` (not the old `pub u16` field); the binding never
722
+ touches `.0`, so the representation change is safe.