sashite-qpi 1.0.0 → 2.1.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.
data/README.md CHANGED
@@ -1,17 +1,21 @@
1
- # Qpi.rb
1
+ # qpi.rb
2
2
 
3
3
  [![Version](https://img.shields.io/github/v/tag/sashite/qpi.rb?label=Version&logo=github)](https://github.com/sashite/qpi.rb/tags)
4
4
  [![Yard documentation](https://img.shields.io/badge/Yard-documentation-blue.svg?logo=github)](https://rubydoc.info/github/sashite/qpi.rb/main)
5
- ![Ruby](https://github.com/sashite/qpi.rb/actions/workflows/main.yml/badge.svg?branch=main)
6
- [![License](https://img.shields.io/github/license/sashite/qpi.rb?label=License&logo=github)](https://github.com/sashite/qpi.rb/raw/main/LICENSE.md)
5
+ [![CI](https://github.com/sashite/qpi.rb/actions/workflows/ruby.yml/badge.svg?branch=main)](https://github.com/sashite/qpi.rb/actions)
6
+ [![License](https://img.shields.io/github/license/sashite/qpi.rb?label=License&logo=github)](https://github.com/sashite/qpi.rb/raw/main/LICENSE)
7
7
 
8
- > **QPI** (Qualified Piece Identifier) implementation for the Ruby language.
8
+ > **QPI** (Qualified Piece Identifier) implementation for Ruby.
9
9
 
10
- ## What is QPI?
10
+ ## Overview
11
11
 
12
- QPI (Qualified Piece Identifier) provides a rule-agnostic format for identifying game pieces in abstract strategy board games by combining [Style Identifier Notation (SIN)](https://sashite.dev/specs/sin/1.0.0/) and [Piece Identifier Notation (PIN)](https://sashite.dev/specs/pin/1.0.0/) primitives with a colon separator.
12
+ This library implements the [QPI Specification v1.0.0](https://sashite.dev/specs/qpi/1.0.0/).
13
13
 
14
- This gem implements the [QPI Specification v1.0.0](https://sashite.dev/specs/qpi/1.0.0/) exactly, providing complete piece identification with all four fundamental attributes: **Family**, **Type**, **Side**, and **State**.
14
+ QPI provides complete piece identification by combining two primitive notations:
15
+ - [SIN](https://sashite.dev/specs/sin/1.0.0/) (Style Identifier Notation) — identifies the piece style
16
+ - [PIN](https://sashite.dev/specs/pin/1.0.0/) (Piece Identifier Notation) — identifies the piece attributes
17
+
18
+ A QPI identifier is a **pair of (SIN, PIN)** that encodes complete **Piece Identity**.
15
19
 
16
20
  ## Installation
17
21
 
@@ -28,8 +32,6 @@ gem install sashite-qpi
28
32
 
29
33
  ## Dependencies
30
34
 
31
- QPI builds upon two foundational primitive specifications:
32
-
33
35
  ```ruby
34
36
  gem "sashite-sin" # Style Identifier Notation
35
37
  gem "sashite-pin" # Piece Identifier Notation
@@ -37,268 +39,316 @@ gem "sashite-pin" # Piece Identifier Notation
37
39
 
38
40
  ## Usage
39
41
 
40
- ### Basic Operations
42
+ ### Parsing (String → Identifier)
43
+
44
+ Convert a QPI string into an `Identifier` object.
41
45
 
42
46
  ```ruby
43
47
  require "sashite/qpi"
44
48
 
45
- # Parse QPI strings
46
- identifier = Sashite::Qpi.parse("C:K") # Chess king, first player
47
- identifier.to_s # => "C:K"
49
+ # Standard parsing (raises on error)
50
+ qpi = Sashite::Qpi.parse("C:K^")
51
+ qpi.to_s # => "C:K^"
52
+
53
+ # Access the five Piece Identity attributes through components
54
+ qpi.sin.style # => :C (Piece Style)
55
+ qpi.pin.type # => :K (Piece Name)
56
+ qpi.pin.side # => :first (Piece Side)
57
+ qpi.pin.state # => :normal (Piece State)
58
+ qpi.pin.terminal? # => true (Terminal Status)
59
+
60
+ # Components are full SIN and PIN instances
61
+ qpi.sin.first_player? # => true
62
+ qpi.pin.enhanced? # => false
63
+
64
+ # Invalid input raises ArgumentError
65
+ Sashite::Qpi.parse("invalid") # => raises ArgumentError
66
+ ```
67
+
68
+ ### Formatting (Identifier → String)
69
+
70
+ Convert an `Identifier` back to a QPI string.
71
+
72
+ ```ruby
73
+ # From components
74
+ sin = Sashite::Sin.parse("C")
75
+ pin = Sashite::Pin.parse("K^")
76
+ qpi = Sashite::Qpi::Identifier.new(sin, pin)
77
+ qpi.to_s # => "C:K^"
78
+
79
+ # With attributes
80
+ sin = Sashite::Sin.parse("s")
81
+ pin = Sashite::Pin.parse("+r")
82
+ qpi = Sashite::Qpi::Identifier.new(sin, pin)
83
+ qpi.to_s # => "s:+r"
84
+ ```
85
+
86
+ ### Validation
48
87
 
49
- # Create identifiers from parameters (strict validation)
50
- identifier = Sashite::Qpi.identifier(:C, :K, :first, :normal)
51
- identifier = Sashite::Qpi::Identifier.new(:S, :R, :first, :enhanced)
88
+ ```ruby
89
+ # Boolean check
90
+ Sashite::Qpi.valid?("C:K^") # => true
91
+ Sashite::Qpi.valid?("s:+r") # => true
92
+ Sashite::Qpi.valid?("invalid") # => false
93
+ Sashite::Qpi.valid?("C:") # => false
94
+ Sashite::Qpi.valid?(":K") # => false
95
+ ```
52
96
 
53
- # Validate QPI strings
54
- Sashite::Qpi.valid?("C:K") # => true
55
- Sashite::Qpi.valid?("s:+p") # => true
56
- Sashite::Qpi.valid?("C:k") # => false (semantic mismatch)
97
+ ### Accessing Components
98
+
99
+ ```ruby
100
+ qpi = Sashite::Qpi.parse("S:+R^")
101
+
102
+ # Get components
103
+ qpi.sin # => #<Sashite::Sin::Identifier style=:S side=:first>
104
+ qpi.pin # => #<Sashite::Pin::Identifier type=:R state=:enhanced terminal=true>
105
+
106
+ # Serialize components
107
+ qpi.sin.to_s # => "S"
108
+ qpi.pin.to_s # => "+R^"
109
+ qpi.to_s # => "S:+R^"
57
110
  ```
58
111
 
59
- ### Strict Parameter Validation
112
+ ### Five Piece Identity Attributes
60
113
 
61
- **Important**: QPI enforces the same strict validation as its underlying SIN and PIN primitives:
114
+ All attributes come directly from the components:
62
115
 
63
116
  ```ruby
64
- # Valid - uppercase symbols only for family and type parameters
65
- Sashite::Qpi.identifier(:C, :K, :first, :normal) # => "C:K"
66
- Sashite::Qpi.identifier(:C, :K, :second, :normal) # => "c:k"
117
+ qpi = Sashite::Qpi.parse("S:+R^")
118
+
119
+ # From SIN component
120
+ qpi.sin.style # => :S (Piece Style)
67
121
 
68
- # Invalid - lowercase symbols rejected with ArgumentError
69
- Sashite::Qpi.identifier(:c, :K, :first, :normal) # => ArgumentError
70
- Sashite::Qpi.identifier(:C, :k, :first, :normal) # => ArgumentError
122
+ # From PIN component
123
+ qpi.pin.type # => :R (Piece Name)
124
+ qpi.pin.side # => :first (Piece Side)
125
+ qpi.pin.state # => :enhanced (Piece State)
126
+ qpi.pin.terminal? # => true (Terminal Status)
71
127
  ```
72
128
 
73
- **Key principle**: Input parameters must use uppercase symbols (`:A` to `:Z`). The `side` parameter determines the display case, not the input case.
129
+ ### Native and Derived Relationship
74
130
 
75
- ### Attribute Access
131
+ QPI defines a deterministic relationship based on case comparison between SIN and PIN letters.
76
132
 
77
133
  ```ruby
78
- identifier = Sashite::Qpi.parse("S:+R")
79
-
80
- # Four fundamental piece attributes
81
- identifier.family # => :S
82
- identifier.type # => :R
83
- identifier.side # => :first
84
- identifier.state # => :enhanced
85
-
86
- # Component extraction
87
- identifier.to_sin # => "S"
88
- identifier.to_pin # => "+R"
89
- identifier.sin_component # => #<Sashite::Sin::Identifier>
90
- identifier.pin_component # => #<Sashite::Pin::Identifier>
134
+ qpi = Sashite::Qpi.parse("C:K^")
135
+
136
+ # Access the relationship
137
+ qpi.sin.side # => :first (derived from SIN letter case)
138
+ qpi.native? # => true (sin.side == pin.side)
139
+ qpi.derived? # => false
140
+
141
+ # Native: SIN case matches PIN case
142
+ Sashite::Qpi.parse("C:K").native? # => true (both uppercase/first)
143
+ Sashite::Qpi.parse("c:k").native? # => true (both lowercase/second)
144
+
145
+ # Derived: SIN case differs from PIN case
146
+ Sashite::Qpi.parse("C:k").derived? # => true (uppercase vs lowercase)
147
+ Sashite::Qpi.parse("c:K").derived? # => true (lowercase vs uppercase)
91
148
  ```
92
149
 
93
150
  ### Transformations
94
151
 
152
+ All transformations return new immutable instances.
153
+
95
154
  ```ruby
96
- # All transformations return new immutable instances
97
- identifier = Sashite::Qpi.parse("C:K")
155
+ qpi = Sashite::Qpi.parse("C:K^")
98
156
 
99
- # State transformations
100
- enhanced = identifier.enhance # => "C:+K"
101
- diminished = identifier.diminish # => "C:-K"
102
- normalized = identifier.normalize # => "C:K"
157
+ # Replace SIN component
158
+ new_sin = Sashite::Sin.parse("S")
159
+ qpi.with_sin(new_sin).to_s # => "S:K^"
103
160
 
104
- # Attribute transformations
105
- different_type = identifier.with_type(:Q) # => "C:Q"
106
- different_side = identifier.with_side(:second) # => "c:k"
107
- different_state = identifier.with_state(:enhanced) # => "C:+K"
108
- different_family = identifier.with_family(:S) # => "S:K"
161
+ # Replace PIN component
162
+ new_pin = Sashite::Pin.parse("+Q^")
163
+ qpi.with_pin(new_pin).to_s # => "C:+Q^"
109
164
 
110
- # Player assignment flip
111
- flipped = identifier.flip # => "c:k"
165
+ # Transform both
166
+ qpi.with_sin(new_sin).with_pin(new_pin).to_s # => "S:+Q^"
112
167
 
113
- # Chain transformations
114
- result = identifier.flip.enhance.with_type(:Q) # => "c:+q"
168
+ # Flip both components (change player)
169
+ qpi.flip.to_s # => "c:k^"
170
+
171
+ # Native/Derived transformations
172
+ qpi = Sashite::Qpi.parse("C:r")
173
+ qpi.native.to_s # => "C:R" (PIN case aligned with SIN case)
174
+ qpi.derive.to_s # => "C:r" (already derived, unchanged)
175
+
176
+ qpi = Sashite::Qpi.parse("C:R")
177
+ qpi.native.to_s # => "C:R" (already native, unchanged)
178
+ qpi.derive.to_s # => "C:r" (PIN case differs from SIN case)
115
179
  ```
116
180
 
117
- ### State and Comparison Queries
181
+ ### Transform via Components
118
182
 
119
183
  ```ruby
120
- identifier = Sashite::Qpi.parse("S:+P")
121
-
122
- # State queries
123
- identifier.normal? # => false
124
- identifier.enhanced? # => true
125
- identifier.diminished? # => false
126
- identifier.first_player? # => true
127
- identifier.second_player? # => false
128
-
129
- # Comparison methods
130
- other = Sashite::Qpi.parse("C:+P")
131
- identifier.same_family?(other) # => false (S vs C)
132
- identifier.same_type?(other) # => true (both P)
133
- identifier.same_side?(other) # => true (both first player)
134
- identifier.same_state?(other) # => true (both enhanced)
135
- identifier.cross_family?(other) # => true (different families)
136
- ```
184
+ qpi = Sashite::Qpi.parse("C:K^")
137
185
 
138
- ## API Reference
186
+ # Transform SIN via component
187
+ qpi.with_sin(qpi.sin.with_style(:S)).to_s # => "S:K^"
139
188
 
140
- ### Main Module Methods
141
-
142
- - `Sashite::Qpi.parse(qpi_string)` - Parse QPI string into Identifier object
143
- - `Sashite::Qpi.identifier(family, type, side, state = :normal)` - Create identifier from parameters (strict validation)
144
- - `Sashite::Qpi.valid?(qpi_string)` - Check if string is valid QPI notation
145
-
146
- ### Identifier Class
147
-
148
- #### Creation and Parsing
149
- - `Sashite::Qpi::Identifier.new(family, type, side, state = :normal)` - Create from parameters (strict validation)
150
- - `Sashite::Qpi::Identifier.parse(qpi_string)` - Parse QPI string
151
-
152
- #### Parameter Validation
153
- **Strict validation enforced**:
154
- - `family` parameter: Must be symbol `:A` to `:Z` (uppercase only)
155
- - `type` parameter: Must be symbol `:A` to `:Z` (uppercase only)
156
- - `side` parameter: Must be `:first` or `:second`
157
- - `state` parameter: Must be `:normal`, `:enhanced`, or `:diminished`
158
-
159
- #### Attribute Access
160
- - `#family` - Get style family (symbol `:A` to `:Z`)
161
- - `#type` - Get piece type (symbol `:A` to `:Z`)
162
- - `#side` - Get player side (`:first` or `:second`)
163
- - `#state` - Get piece state (`:normal`, `:enhanced`, or `:diminished`)
164
- - `#to_s` - Convert to QPI string representation
165
-
166
- #### Component Access
167
- - `#to_sin` - Get SIN string representation
168
- - `#to_pin` - Get PIN string representation
169
- - `#sin_component` - Get SIN identifier object
170
- - `#pin_component` - Get PIN identifier object
171
-
172
- #### State Queries
173
- - `#normal?` - Check if normal state
174
- - `#enhanced?` - Check if enhanced state
175
- - `#diminished?` - Check if diminished state
176
- - `#first_player?` - Check if first player
177
- - `#second_player?` - Check if second player
178
-
179
- #### Transformations (immutable - return new instances)
180
- - `#enhance` - Create enhanced version
181
- - `#diminish` - Create diminished version
182
- - `#normalize` - Remove state modifiers
183
- - `#with_type(new_type)` - Change piece type
184
- - `#with_side(new_side)` - Change player side
185
- - `#with_state(new_state)` - Change piece state
186
- - `#with_family(new_family)` - Change style family
187
- - `#flip` - Switch player assignment for both components
188
-
189
- #### Comparison Methods
190
- - `#same_family?(other)` - Check if same style family
191
- - `#same_type?(other)` - Check if same piece type
192
- - `#same_side?(other)` - Check if same player side
193
- - `#same_state?(other)` - Check if same piece state
194
- - `#cross_family?(other)` - Check if different style families
195
- - `#==(other)` - Full equality comparison
196
-
197
- ## Format Specification
198
-
199
- ### Structure
200
- ```
201
- <sin>:<pin>
202
- ```
189
+ # Transform PIN via component
190
+ qpi.with_pin(qpi.pin.with_type(:Q)).to_s # => "C:Q^"
191
+ qpi.with_pin(qpi.pin.with_state(:enhanced)).to_s # => "C:+K^"
192
+ qpi.with_pin(qpi.pin.with_terminal(false)).to_s # => "C:K"
203
193
 
204
- ### Grammar (BNF)
205
- ```bnf
206
- <qpi> ::= <uppercase-qpi> | <lowercase-qpi>
207
- <uppercase-qpi> ::= <uppercase-letter> ":" <uppercase-pin>
208
- <lowercase-qpi> ::= <lowercase-letter> ":" <lowercase-pin>
209
- <uppercase-pin> ::= ["+" | "-"] <uppercase-letter>
210
- <lowercase-pin> ::= ["+" | "-"] <lowercase-letter>
194
+ # Chain transformations
195
+ qpi.flip.with_sin(qpi.sin.with_style(:S)).to_s # => "s:k^"
211
196
  ```
212
197
 
213
- ### Regular Expression
198
+ ### Component Queries
199
+
200
+ Since QPI is a composition, use the component APIs directly:
201
+
214
202
  ```ruby
215
- /\A([A-Z]:[-+]?[A-Z]|[a-z]:[-+]?[a-z])\z/
203
+ qpi = Sashite::Qpi.parse("S:+P^")
204
+
205
+ # SIN queries (style and side)
206
+ qpi.sin.style # => :S
207
+ qpi.sin.side # => :first
208
+ qpi.sin.first_player? # => true
209
+ qpi.sin.letter # => "S"
210
+
211
+ # PIN queries (type, state, terminal)
212
+ qpi.pin.type # => :P
213
+ qpi.pin.state # => :enhanced
214
+ qpi.pin.terminal? # => true
215
+ qpi.pin.enhanced? # => true
216
+ qpi.pin.letter # => "P"
217
+ qpi.pin.prefix # => "+"
218
+ qpi.pin.suffix # => "^"
219
+
220
+ # Compare QPIs via components
221
+ other = Sashite::Qpi.parse("C:+P^")
222
+ qpi.sin.same_style?(other.sin) # => false (S vs C)
223
+ qpi.pin.same_type?(other.pin) # => true (both P)
224
+ qpi.sin.same_side?(other.sin) # => true (both first)
225
+ qpi.pin.same_state?(other.pin) # => true (both enhanced)
216
226
  ```
217
227
 
218
- ### Examples
228
+ ## API Reference
219
229
 
220
- - `C:K` - Chess-style king, first player
221
- - `c:k` - Chess-style king, second player
222
- - `S:+R` - Shogi-style enhanced rook, first player
223
- - `x:-s` - Xiangqi-style diminished soldier, second player
230
+ ### Types
224
231
 
225
- ## Semantic Consistency
232
+ ```ruby
233
+ # Identifier represents a parsed QPI with complete Piece Identity.
234
+ class Sashite::Qpi::Identifier
235
+ # Creates an Identifier from SIN and PIN components.
236
+ # Raises ArgumentError if components are invalid.
237
+ #
238
+ # @param sin [Sashite::Sin::Identifier] Style component
239
+ # @param pin [Sashite::Pin::Identifier] Piece component
240
+ # @return [Identifier]
241
+ def initialize(sin, pin)
242
+
243
+ # Returns the SIN component.
244
+ #
245
+ # @return [Sashite::Sin::Identifier]
246
+ def sin
247
+
248
+ # Returns the PIN component.
249
+ #
250
+ # @return [Sashite::Pin::Identifier]
251
+ def pin
252
+
253
+ # Returns true if sin.side equals pin.side (Native relationship).
254
+ #
255
+ # @return [Boolean]
256
+ def native?
257
+
258
+ # Returns true if sin.side differs from pin.side (Derived relationship).
259
+ #
260
+ # @return [Boolean]
261
+ def derived?
262
+
263
+ # Returns the QPI string representation.
264
+ #
265
+ # @return [String]
266
+ def to_s
267
+ end
268
+ ```
226
269
 
227
- QPI enforces semantic consistency: the style and piece components must represent the same player. Both components use case to indicate player assignment, and these must align.
270
+ ### Parsing
228
271
 
229
- **Valid combinations:**
230
272
  ```ruby
231
- Sashite::Qpi.valid?("C:K") # => true (both first player)
232
- Sashite::Qpi.valid?("c:k") # => true (both second player)
273
+ # Parses a QPI string into an Identifier.
274
+ # Raises ArgumentError if the string is not valid.
275
+ #
276
+ # @param string [String] QPI string
277
+ # @return [Identifier]
278
+ # @raise [ArgumentError] if invalid
279
+ def Sashite::Qpi.parse(string)
233
280
  ```
234
281
 
235
- **Invalid combinations:**
282
+ ### Validation
283
+
236
284
  ```ruby
237
- Sashite::Qpi.valid?("C:k") # => false (family=first, piece=second)
238
- Sashite::Qpi.valid?("c:K") # => false (family=second, piece=first)
285
+ # Reports whether string is a valid QPI.
286
+ #
287
+ # @param string [String] QPI string
288
+ # @return [Boolean]
289
+ def Sashite::Qpi.valid?(string)
239
290
  ```
240
291
 
241
- ## Parameter Validation
292
+ ### Transformations
242
293
 
243
- ### Strict Validation Rules
294
+ ```ruby
295
+ # Component replacement (return new Identifier)
296
+ def with_sin(new_sin) # => Identifier with different SIN
297
+ def with_pin(new_pin) # => Identifier with different PIN
244
298
 
245
- QPI enforces strict parameter validation consistent with its underlying SIN and PIN primitives:
299
+ # Flip transformation (transforms both components)
300
+ def flip # => Identifier with both SIN and PIN flipped
246
301
 
247
- ```ruby
248
- # Valid parameter examples
249
- Sashite::Qpi.identifier(:C, :K, :first, :normal) # All uppercase symbols
250
- Sashite::Qpi.identifier(:S, :R, :second, :enhanced) # Display case determined by side
251
-
252
- # ✗ Invalid parameter examples (raise ArgumentError)
253
- Sashite::Qpi.identifier(:c, :K, :first, :normal) # Lowercase family rejected
254
- Sashite::Qpi.identifier(:C, :k, :first, :normal) # Lowercase type rejected
255
- Sashite::Qpi.identifier("C", :K, :first, :normal) # String family rejected
256
- Sashite::Qpi.identifier(:C, "K", :first, :normal) # String type rejected
302
+ # Native/Derived transformations
303
+ def native # => Identifier with PIN case aligned to SIN case
304
+ def derive # => Identifier with PIN case opposite to SIN case
257
305
  ```
258
306
 
259
- ### Error Handling
307
+ ### Errors
260
308
 
261
- QPI delegates validation to its underlying primitives, ensuring consistent error messages:
309
+ All parsing and validation errors raise `ArgumentError` with descriptive messages:
262
310
 
263
- ```ruby
264
- begin
265
- Sashite::Qpi.identifier(:c, :K, :first, :normal)
266
- rescue ArgumentError => e
267
- # Same error message as Sashite::Sin::Identifier.new(:c, :first)
268
- puts e.message # => "Family must be a symbol from :A to :Z representing Style Family, got: :c"
269
- end
311
+ | Message | Cause |
312
+ |---------|-------|
313
+ | `"empty input"` | String length is 0 |
314
+ | `"missing colon separator"` | No `:` found in string |
315
+ | `"missing SIN component"` | Nothing before `:` |
316
+ | `"missing PIN component"` | Nothing after `:` |
317
+ | `"invalid SIN component: ..."` | SIN parsing failed |
318
+ | `"invalid PIN component: ..."` | PIN parsing failed |
270
319
 
271
- begin
272
- Sashite::Qpi.identifier(:C, :k, :first, :normal)
273
- rescue ArgumentError => e
274
- # Same error message as Sashite::Pin::Identifier.new(:k, :first, :normal)
275
- puts e.message # => "Type must be a symbol from :A to :Z, got: :k"
276
- end
277
- ```
320
+ ## Piece Identity Mapping
278
321
 
279
- ## Design Properties
322
+ QPI encodes complete **Piece Identity** as defined in the [Glossary](https://sashite.dev/glossary/):
280
323
 
281
- - **Rule-agnostic**: Independent of specific game mechanics
282
- - **Complete identification**: All four piece attributes represented
283
- - **Cross-style support**: Enables multi-tradition gaming
284
- - **Semantic validation**: Ensures component consistency
285
- - **Primitive foundation**: Built from SIN and PIN specifications
286
- - **Strict validation**: Consistent parameter validation with underlying primitives
287
- - **Immutable**: All instances frozen, transformations return new objects
288
- - **Functional**: Pure functions with no side effects
324
+ | Piece Attribute | QPI Access | Encoding |
325
+ |---------------------|----------------------|--------------------------------------------------------|
326
+ | **Piece Style** | `qpi.sin.style` | SIN letter (case-insensitive identity) |
327
+ | **Piece Name** | `qpi.pin.type` | PIN letter (case-insensitive identity) |
328
+ | **Piece Side** | `qpi.pin.side` | PIN letter case (uppercase = first, lowercase = second)|
329
+ | **Piece State** | `qpi.pin.state` | PIN modifier (`+` = enhanced, `-` = diminished) |
330
+ | **Terminal Status** | `qpi.pin.terminal?` | PIN marker (`^` = terminal) |
289
331
 
290
- ## Related Specifications
332
+ Additionally, QPI provides a **Native/Derived relationship** via `native?`, `derived?`, `native`, and `derive`.
291
333
 
292
- - [QPI Specification v1.0.0](https://sashite.dev/specs/qpi/1.0.0/) - Complete technical specification
293
- - [QPI Examples](https://sashite.dev/specs/qpi/1.0.0/examples/) - Practical implementation examples
294
- - [SIN Specification v1.0.0](https://sashite.dev/specs/sin/1.0.0/) - Style identification component
295
- - [PIN Specification v1.0.0](https://sashite.dev/specs/pin/1.0.0/) - Piece identification component
296
- - [Sashité Protocol](https://sashite.dev/protocol/) - Conceptual foundation
334
+ ## Design Principles
297
335
 
298
- ## License
336
+ - **Pure composition**: QPI composes SIN and PIN without reimplementing features
337
+ - **Minimal API**: Core methods (`sin`, `pin`, `native?`, `derived?`, `native`, `derive`, `to_s`) plus transformations
338
+ - **Component transparency**: Access components directly, no wrapper methods
339
+ - **QPI-specific conveniences**: `flip`, `native`, `derive` (operations that span both components)
340
+ - **Immutable identifiers**: Frozen instances prevent mutation
341
+ - **Ruby idioms**: `valid?` predicate, `to_s` conversion, `ArgumentError` for invalid input
342
+ - **No duplication**: Delegates to `sashite-sin` and `sashite-pin`
299
343
 
300
- Available as open source under the [MIT License](https://opensource.org/licenses/MIT).
344
+ ## Related Specifications
345
+
346
+ - [Game Protocol](https://sashite.dev/game-protocol/) — Conceptual foundation
347
+ - [QPI Specification](https://sashite.dev/specs/qpi/1.0.0/) — Official specification
348
+ - [QPI Examples](https://sashite.dev/specs/qpi/1.0.0/examples/) — Usage examples
349
+ - [SIN Specification](https://sashite.dev/specs/sin/1.0.0/) — Style component
350
+ - [PIN Specification](https://sashite.dev/specs/pin/1.0.0/) — Piece component
301
351
 
302
- ## About
352
+ ## License
303
353
 
304
- Maintained by [Sashité](https://sashite.com/) — promoting chess variants and sharing the beauty of board game cultures.
354
+ Available as open source under the [Apache License 2.0](https://opensource.org/licenses/Apache-2.0).
@@ -0,0 +1,13 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Sashite
4
+ module Qpi
5
+ # Constants for QPI (Qualified Piece Identifier).
6
+ #
7
+ # This module defines the structural constants for QPI tokens.
8
+ module Constants
9
+ # Separator between SIN and PIN components.
10
+ SEPARATOR = ":"
11
+ end
12
+ end
13
+ end
@@ -0,0 +1,23 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Sashite
4
+ module Qpi
5
+ module Errors
6
+ class Argument < ::ArgumentError
7
+ # Centralized error messages for QPI parsing and validation.
8
+ #
9
+ # @example
10
+ # raise Errors::Argument, Messages::EMPTY_INPUT
11
+ module Messages
12
+ # Parsing errors
13
+ EMPTY_INPUT = "empty input"
14
+ MISSING_SEPARATOR = "missing colon separator"
15
+ MISSING_SIN = "missing SIN component"
16
+ MISSING_PIN = "missing PIN component"
17
+ INVALID_SIN = "invalid SIN component"
18
+ INVALID_PIN = "invalid PIN component"
19
+ end
20
+ end
21
+ end
22
+ end
23
+ end
@@ -0,0 +1,16 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "argument/messages"
4
+
5
+ module Sashite
6
+ module Qpi
7
+ module Errors
8
+ # Error raised when QPI parsing or validation fails.
9
+ #
10
+ # @example
11
+ # raise Argument, Argument::Messages::EMPTY_INPUT
12
+ class Argument < ::ArgumentError
13
+ end
14
+ end
15
+ end
16
+ end
@@ -0,0 +1,3 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "errors/argument"