sashite-qpi 1.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.
checksums.yaml ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA256:
3
+ metadata.gz: 3f707276efe5619994fb1f4f3204b3b7bf32654cff14bae674d9ad75764afce3
4
+ data.tar.gz: 8a19a611d441d8e50362449d202e7f49c1298790388d802d9be90b4119ffd237
5
+ SHA512:
6
+ metadata.gz: 84cbe2ee48fa8ef3e3c2e5b3146447f17f9c0f9755fd2376da25baae3e6ee1a7242144682f3ff0de371ee9a2a618d0051f043888c05d6e92bb4dad393c44077d
7
+ data.tar.gz: 12dfafe49bf08c75c915f72f1c9fc4484bd9d1e125fd653c6387d290ce761ed6e7d91233f40728bf9ffb61af4e25b3e2fe524f7bca1433048a889fd3588cbd1e
data/LICENSE.md ADDED
@@ -0,0 +1,22 @@
1
+ Copyright (c) 2014-2025 Sashite
2
+
3
+ MIT License
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining
6
+ a copy of this software and associated documentation files (the
7
+ "Software"), to deal in the Software without restriction, including
8
+ without limitation the rights to use, copy, modify, merge, publish,
9
+ distribute, sublicense, and/or sell copies of the Software, and to
10
+ permit persons to whom the Software is furnished to do so, subject to
11
+ the following conditions:
12
+
13
+ The above copyright notice and this permission notice shall be
14
+ included in all copies or substantial portions of the Software.
15
+
16
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
17
+ EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
18
+ MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
19
+ NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
20
+ LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
21
+ OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
22
+ WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
data/README.md ADDED
@@ -0,0 +1,304 @@
1
+ # Qpi.rb
2
+
3
+ [![Version](https://img.shields.io/github/v/tag/sashite/qpi.rb?label=Version&logo=github)](https://github.com/sashite/qpi.rb/tags)
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)
7
+
8
+ > **QPI** (Qualified Piece Identifier) implementation for the Ruby language.
9
+
10
+ ## What is QPI?
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.
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**.
15
+
16
+ ## Installation
17
+
18
+ ```ruby
19
+ # In your Gemfile
20
+ gem "sashite-qpi"
21
+ ```
22
+
23
+ Or install manually:
24
+
25
+ ```sh
26
+ gem install sashite-qpi
27
+ ```
28
+
29
+ ## Dependencies
30
+
31
+ QPI builds upon two foundational primitive specifications:
32
+
33
+ ```ruby
34
+ gem "sashite-sin" # Style Identifier Notation
35
+ gem "sashite-pin" # Piece Identifier Notation
36
+ ```
37
+
38
+ ## Usage
39
+
40
+ ### Basic Operations
41
+
42
+ ```ruby
43
+ require "sashite/qpi"
44
+
45
+ # Parse QPI strings
46
+ identifier = Sashite::Qpi.parse("C:K") # Chess king, first player
47
+ identifier.to_s # => "C:K"
48
+
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)
52
+
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)
57
+ ```
58
+
59
+ ### Strict Parameter Validation
60
+
61
+ **Important**: QPI enforces the same strict validation as its underlying SIN and PIN primitives:
62
+
63
+ ```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"
67
+
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
71
+ ```
72
+
73
+ **Key principle**: Input parameters must use uppercase symbols (`:A` to `:Z`). The `side` parameter determines the display case, not the input case.
74
+
75
+ ### Attribute Access
76
+
77
+ ```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>
91
+ ```
92
+
93
+ ### Transformations
94
+
95
+ ```ruby
96
+ # All transformations return new immutable instances
97
+ identifier = Sashite::Qpi.parse("C:K")
98
+
99
+ # State transformations
100
+ enhanced = identifier.enhance # => "C:+K"
101
+ diminished = identifier.diminish # => "C:-K"
102
+ normalized = identifier.normalize # => "C:K"
103
+
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"
109
+
110
+ # Player assignment flip
111
+ flipped = identifier.flip # => "c:k"
112
+
113
+ # Chain transformations
114
+ result = identifier.flip.enhance.with_type(:Q) # => "c:+q"
115
+ ```
116
+
117
+ ### State and Comparison Queries
118
+
119
+ ```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
+ ```
137
+
138
+ ## API Reference
139
+
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
+ ```
203
+
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>
211
+ ```
212
+
213
+ ### Regular Expression
214
+ ```ruby
215
+ /\A([A-Z]:[-+]?[A-Z]|[a-z]:[-+]?[a-z])\z/
216
+ ```
217
+
218
+ ### Examples
219
+
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
224
+
225
+ ## Semantic Consistency
226
+
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.
228
+
229
+ **Valid combinations:**
230
+ ```ruby
231
+ Sashite::Qpi.valid?("C:K") # => true (both first player)
232
+ Sashite::Qpi.valid?("c:k") # => true (both second player)
233
+ ```
234
+
235
+ **Invalid combinations:**
236
+ ```ruby
237
+ Sashite::Qpi.valid?("C:k") # => false (family=first, piece=second)
238
+ Sashite::Qpi.valid?("c:K") # => false (family=second, piece=first)
239
+ ```
240
+
241
+ ## Parameter Validation
242
+
243
+ ### Strict Validation Rules
244
+
245
+ QPI enforces strict parameter validation consistent with its underlying SIN and PIN primitives:
246
+
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
257
+ ```
258
+
259
+ ### Error Handling
260
+
261
+ QPI delegates validation to its underlying primitives, ensuring consistent error messages:
262
+
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
270
+
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
+ ```
278
+
279
+ ## Design Properties
280
+
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
289
+
290
+ ## Related Specifications
291
+
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
297
+
298
+ ## License
299
+
300
+ Available as open source under the [MIT License](https://opensource.org/licenses/MIT).
301
+
302
+ ## About
303
+
304
+ Maintained by [Sashité](https://sashite.com/) — promoting chess variants and sharing the beauty of board game cultures.
@@ -0,0 +1,424 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "sashite/pin"
4
+ require "sashite/sin"
5
+
6
+ module Sashite
7
+ module Qpi
8
+ # Represents an identifier in QPI (Qualified Piece Identifier) format.
9
+ #
10
+ # A QPI identifier combines style and piece attributes into a unified representation:
11
+ # - Family: Style family from SIN component (:A to :Z only)
12
+ # - Type: Piece type (:A to :Z) from PIN component
13
+ # - Side: Player assignment (:first or :second) from both components
14
+ # - State: Piece state (:normal, :enhanced, :diminished) from PIN component
15
+ # - Semantic constraint: SIN and PIN components must represent the same player
16
+ #
17
+ # All instances are immutable - transformation methods return new instances.
18
+ # This follows the QPI Specification v1.0.0 with strict parameter validation
19
+ # consistent with the underlying SIN and PIN primitive specifications.
20
+ #
21
+ # ## Strict Parameter Validation
22
+ #
23
+ # QPI enforces the same strict validation as its underlying primitives:
24
+ # - Family parameter must be a symbol from :A to :Z (not :a to :z)
25
+ # - Type parameter must be a symbol from :A to :Z (delegated to PIN)
26
+ # - Side parameter determines the display case, not the input parameters
27
+ #
28
+ # This ensures consistency with SIN and PIN behavior where lowercase symbols
29
+ # are rejected with ArgumentError.
30
+ #
31
+ # @example Strict parameter validation
32
+ # # Valid - uppercase symbols only
33
+ # Sashite::Qpi::Identifier.new(:C, :K, :first, :normal) # => "C:K"
34
+ # Sashite::Qpi::Identifier.new(:C, :K, :second, :normal) # => "c:k"
35
+ #
36
+ # # Invalid - lowercase symbols rejected
37
+ # Sashite::Qpi::Identifier.new(:c, :K, :second, :normal) # => ArgumentError
38
+ # Sashite::Qpi::Identifier.new(:C, :k, :second, :normal) # => ArgumentError
39
+ #
40
+ # @see https://sashite.dev/specs/qpi/1.0.0/ QPI Specification v1.0.0
41
+ class Identifier
42
+ # Component separator for string representation
43
+ SEPARATOR = ":"
44
+
45
+ # Error messages
46
+ ERROR_INVALID_QPI = "Invalid QPI string: %s"
47
+ ERROR_SEMANTIC_MISMATCH = "Family and side must represent the same player: family=%s (side=%s), side=%s"
48
+ ERROR_MISSING_SEPARATOR = "QPI string must contain exactly one colon separator: %s"
49
+
50
+ # @return [Symbol] the style family (:A to :Z based on SIN component)
51
+ def family
52
+ @sin_identifier.family
53
+ end
54
+
55
+ # @return [Symbol] the piece type (:A to :Z)
56
+ def type
57
+ @pin_identifier.type
58
+ end
59
+
60
+ # @return [Symbol] the player side (:first or :second)
61
+ def side
62
+ @pin_identifier.side
63
+ end
64
+
65
+ # @return [Symbol] the piece state (:normal, :enhanced, or :diminished)
66
+ def state
67
+ @pin_identifier.state
68
+ end
69
+
70
+ # Create a new identifier instance
71
+ #
72
+ # @param family [Symbol] style family identifier (:A to :Z only)
73
+ # @param type [Symbol] piece type (:A to :Z only)
74
+ # @param side [Symbol] player side (:first or :second)
75
+ # @param state [Symbol] piece state (:normal, :enhanced, or :diminished)
76
+ # @raise [ArgumentError] if parameters are invalid or semantically inconsistent
77
+ #
78
+ # @example Create identifiers with strict parameter validation
79
+ # # Valid - uppercase symbols only
80
+ # chess_king = Sashite::Qpi::Identifier.new(:C, :K, :first, :normal) # => "C:K"
81
+ # chess_pawn = Sashite::Qpi::Identifier.new(:C, :P, :second, :normal) # => "c:p"
82
+ #
83
+ # # Invalid - lowercase symbols rejected
84
+ # # Sashite::Qpi::Identifier.new(:c, :K, :first, :normal) # => ArgumentError
85
+ # # Sashite::Qpi::Identifier.new(:C, :k, :first, :normal) # => ArgumentError
86
+ def initialize(family, type, side, state = Pin::Identifier::NORMAL_STATE)
87
+ # Strict validation - delegate to underlying primitives for consistency
88
+ Sin::Identifier.validate_family(family)
89
+ Pin::Identifier.validate_type(type)
90
+ Pin::Identifier.validate_side(side)
91
+ Pin::Identifier.validate_state(state)
92
+
93
+ # Create PIN component
94
+ @pin_identifier = Pin::Identifier.new(type, side, state)
95
+
96
+ # Create SIN component - pass family directly without normalization
97
+ @sin_identifier = Sin::Identifier.new(family, side)
98
+
99
+ # Validate semantic consistency
100
+ validate_semantic_consistency
101
+
102
+ freeze
103
+ end
104
+
105
+ # Parse a QPI string into an Identifier object
106
+ #
107
+ # @param qpi_string [String] QPI notation string (format: sin:pin)
108
+ # @return [Identifier] new identifier instance
109
+ # @raise [ArgumentError] if the QPI string is invalid
110
+ #
111
+ # @example Parse QPI strings with automatic component separation
112
+ # Sashite::Qpi::Identifier.parse("C:K") # => #<Qpi::Identifier family=:C type=:K side=:first state=:normal>
113
+ # Sashite::Qpi::Identifier.parse("s:+r") # => #<Qpi::Identifier family=:S type=:R side=:second state=:enhanced>
114
+ # Sashite::Qpi::Identifier.parse("X:-S") # => #<Qpi::Identifier family=:X type=:S side=:first state=:diminished>
115
+ def self.parse(qpi_string)
116
+ string_value = String(qpi_string)
117
+ sin_part, pin_part = split_components(string_value)
118
+
119
+ # Parse components
120
+ sin_identifier = Sin::Identifier.parse(sin_part)
121
+ pin_identifier = Pin::Identifier.parse(pin_part)
122
+
123
+ # Validate semantic consistency BEFORE creating new instance
124
+ unless sin_identifier.side == pin_identifier.side
125
+ raise ::ArgumentError, format(ERROR_SEMANTIC_MISMATCH,
126
+ sin_part, sin_identifier.side, pin_identifier.side)
127
+ end
128
+
129
+ # Extract parameters and create new instance
130
+ new(sin_identifier.family, pin_identifier.type, pin_identifier.side, pin_identifier.state)
131
+ end
132
+
133
+ # Check if a string is a valid QPI notation
134
+ #
135
+ # @param qpi_string [String] the string to validate
136
+ # @return [Boolean] true if valid QPI, false otherwise
137
+ #
138
+ # @example Validate QPI strings with semantic checking
139
+ # Sashite::Qpi::Identifier.valid?("C:K") # => true
140
+ # Sashite::Qpi::Identifier.valid?("s:+r") # => true
141
+ # Sashite::Qpi::Identifier.valid?("C:k") # => false (semantic mismatch)
142
+ # Sashite::Qpi::Identifier.valid?("Chess") # => false (no separator)
143
+ def self.valid?(qpi_string)
144
+ return false unless qpi_string.is_a?(::String)
145
+
146
+ # Split components and validate each part
147
+ sin_part, pin_part = split_components(qpi_string)
148
+ return false unless Sashite::Sin.valid?(sin_part) && Sashite::Pin.valid?(pin_part)
149
+
150
+ # Semantic consistency check
151
+ sin_identifier = Sashite::Sin.parse(sin_part)
152
+ pin_identifier = Sashite::Pin.parse(pin_part)
153
+ sin_identifier.side == pin_identifier.side
154
+ rescue ArgumentError
155
+ false
156
+ end
157
+
158
+ # Convert the identifier to its QPI string representation
159
+ #
160
+ # @return [String] QPI notation string (format: sin:pin)
161
+ # @example Display QPI identifiers
162
+ # identifier.to_s # => "C:K"
163
+ def to_s
164
+ "#{@sin_identifier}#{SEPARATOR}#{@pin_identifier}"
165
+ end
166
+
167
+ # Convert to SIN string representation (style component only)
168
+ #
169
+ # @return [String] SIN notation string
170
+ # @example Extract style component
171
+ # identifier.to_sin # => "C"
172
+ def to_sin
173
+ @sin_identifier.to_s
174
+ end
175
+
176
+ # Convert to PIN string representation (piece component only)
177
+ #
178
+ # @return [String] PIN notation string
179
+ # @example Extract piece component
180
+ # identifier.to_pin # => "+K"
181
+ def to_pin
182
+ @pin_identifier.to_s
183
+ end
184
+
185
+ # Get the parsed SIN identifier object
186
+ #
187
+ # @return [Sashite::Sin::Identifier] SIN component as identifier object
188
+ def sin_component
189
+ @sin_identifier
190
+ end
191
+
192
+ # Get the parsed PIN identifier object
193
+ #
194
+ # @return [Sashite::Pin::Identifier] PIN component as identifier object
195
+ def pin_component
196
+ @pin_identifier
197
+ end
198
+
199
+ # Create a new identifier with enhanced state
200
+ #
201
+ # @return [Identifier] new identifier with enhanced PIN component
202
+ def enhance
203
+ return self if enhanced?
204
+
205
+ self.class.new(family, type, side, Pin::Identifier::ENHANCED_STATE)
206
+ end
207
+
208
+ # Create a new identifier with diminished state
209
+ #
210
+ # @return [Identifier] new identifier with diminished PIN component
211
+ def diminish
212
+ return self if diminished?
213
+
214
+ self.class.new(family, type, side, Pin::Identifier::DIMINISHED_STATE)
215
+ end
216
+
217
+ # Create a new identifier with normal state (no modifiers)
218
+ #
219
+ # @return [Identifier] new identifier with normalized PIN component
220
+ def normalize
221
+ return self if normal?
222
+
223
+ self.class.new(family, type, side, Pin::Identifier::NORMAL_STATE)
224
+ end
225
+
226
+ # Create a new identifier with different piece type
227
+ #
228
+ # @param new_type [Symbol] new piece type (:A to :Z)
229
+ # @return [Identifier] new identifier with different type
230
+ def with_type(new_type)
231
+ return self if type == new_type
232
+
233
+ self.class.new(family, new_type, side, state)
234
+ end
235
+
236
+ # Create a new identifier with different side
237
+ #
238
+ # @param new_side [Symbol] new player side (:first or :second)
239
+ # @return [Identifier] new identifier with different side
240
+ def with_side(new_side)
241
+ return self if side == new_side
242
+
243
+ self.class.new(family, type, new_side, state)
244
+ end
245
+
246
+ # Create a new identifier with different state
247
+ #
248
+ # @param new_state [Symbol] new piece state (:normal, :enhanced, or :diminished)
249
+ # @return [Identifier] new identifier with different state
250
+ def with_state(new_state)
251
+ return self if state == new_state
252
+
253
+ self.class.new(family, type, side, new_state)
254
+ end
255
+
256
+ # Create a new identifier with different family
257
+ #
258
+ # @param new_family [Symbol] new style family identifier (:A to :Z)
259
+ # @return [Identifier] new identifier with different family
260
+ def with_family(new_family)
261
+ return self if family == new_family
262
+
263
+ self.class.new(new_family, type, side, state)
264
+ end
265
+
266
+ # Create a new identifier with opposite player assignment
267
+ #
268
+ # Changes the player assignment (side) while preserving the family and piece attributes.
269
+ # This maintains semantic consistency between the components.
270
+ #
271
+ # @return [Identifier] new identifier with opposite side but same family
272
+ #
273
+ # @example Flip player assignment while preserving family and attributes
274
+ # chess_first = Sashite::Qpi::Identifier.parse("C:K") # Chess king, first player
275
+ # chess_second = chess_first.flip # => "c:k" (Chess king, second player)
276
+ #
277
+ # shogi_first = Sashite::Qpi::Identifier.parse("S:+R") # Shogi enhanced rook, first player
278
+ # shogi_second = shogi_first.flip # => "s:+r" (Shogi enhanced rook, second player)
279
+ def flip
280
+ self.class.new(family, type, opposite_side, state)
281
+ end
282
+
283
+ # Check if the identifier has normal state
284
+ #
285
+ # @return [Boolean] true if normal state
286
+ def normal?
287
+ @pin_identifier.normal?
288
+ end
289
+
290
+ # Check if the identifier has enhanced state
291
+ #
292
+ # @return [Boolean] true if enhanced state
293
+ def enhanced?
294
+ @pin_identifier.enhanced?
295
+ end
296
+
297
+ # Check if the identifier has diminished state
298
+ #
299
+ # @return [Boolean] true if diminished state
300
+ def diminished?
301
+ @pin_identifier.diminished?
302
+ end
303
+
304
+ # Check if the identifier belongs to the first player
305
+ #
306
+ # @return [Boolean] true if first player
307
+ def first_player?
308
+ @pin_identifier.first_player?
309
+ end
310
+
311
+ # Check if the identifier belongs to the second player
312
+ #
313
+ # @return [Boolean] true if second player
314
+ def second_player?
315
+ @pin_identifier.second_player?
316
+ end
317
+
318
+ # Check if this identifier has the same family as another
319
+ #
320
+ # @param other [Identifier] identifier to compare with
321
+ # @return [Boolean] true if same family (case-insensitive)
322
+ def same_family?(other)
323
+ return false unless other.is_a?(self.class)
324
+
325
+ @sin_identifier.same_family?(other.sin_component)
326
+ end
327
+
328
+ # Check if this identifier has different family from another
329
+ #
330
+ # @param other [Identifier] identifier to compare with
331
+ # @return [Boolean] true if different families
332
+ def cross_family?(other)
333
+ return false unless other.is_a?(self.class)
334
+
335
+ !same_family?(other)
336
+ end
337
+
338
+ # Check if this identifier has the same side as another
339
+ #
340
+ # @param other [Identifier] identifier to compare with
341
+ # @return [Boolean] true if same side
342
+ def same_side?(other)
343
+ return false unless other.is_a?(self.class)
344
+
345
+ @pin_identifier.same_side?(other.pin_component)
346
+ end
347
+
348
+ # Check if this identifier has the same type as another
349
+ #
350
+ # @param other [Identifier] identifier to compare with
351
+ # @return [Boolean] true if same type
352
+ def same_type?(other)
353
+ return false unless other.is_a?(self.class)
354
+
355
+ @pin_identifier.same_type?(other.pin_component)
356
+ end
357
+
358
+ # Check if this identifier has the same state as another
359
+ #
360
+ # @param other [Identifier] identifier to compare with
361
+ # @return [Boolean] true if same state
362
+ def same_state?(other)
363
+ return false unless other.is_a?(self.class)
364
+
365
+ @pin_identifier.same_state?(other.pin_component)
366
+ end
367
+
368
+ # Custom equality comparison
369
+ #
370
+ # @param other [Object] object to compare with
371
+ # @return [Boolean] true if identifiers are equal
372
+ def ==(other)
373
+ return false unless other.is_a?(self.class)
374
+
375
+ @sin_identifier == other.sin_component && @pin_identifier == other.pin_component
376
+ end
377
+
378
+ # Alias for == to ensure Set functionality works correctly
379
+ alias eql? ==
380
+
381
+ # Custom hash implementation for use in collections
382
+ #
383
+ # @return [Integer] hash value
384
+ def hash
385
+ [self.class, @sin_identifier, @pin_identifier].hash
386
+ end
387
+
388
+ private
389
+
390
+ # Split QPI string into SIN and PIN components
391
+ #
392
+ # @param qpi_string [String] QPI string to split
393
+ # @return [Array<String>] array containing [sin_part, pin_part]
394
+ def self.split_components(qpi_string)
395
+ parts = qpi_string.split(SEPARATOR, 2)
396
+ raise ::ArgumentError, format(ERROR_MISSING_SEPARATOR, qpi_string) unless parts.size == 2
397
+
398
+ parts
399
+ end
400
+
401
+ private_class_method :split_components
402
+
403
+ # Validate semantic consistency between SIN and PIN components
404
+ #
405
+ # @raise [ArgumentError] if family case doesn't match side
406
+ def validate_semantic_consistency
407
+ expected_side = @sin_identifier.side
408
+ actual_side = @pin_identifier.side
409
+
410
+ return if expected_side == actual_side
411
+
412
+ raise ::ArgumentError, format(ERROR_SEMANTIC_MISMATCH,
413
+ @sin_identifier.letter, expected_side, actual_side)
414
+ end
415
+
416
+ # Get the opposite player side
417
+ #
418
+ # @return [Symbol] the opposite side
419
+ def opposite_side
420
+ first_player? ? Pin::Identifier::SECOND_PLAYER : Pin::Identifier::FIRST_PLAYER
421
+ end
422
+ end
423
+ end
424
+ end
@@ -0,0 +1,220 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "qpi/identifier"
4
+
5
+ module Sashite
6
+ # QPI (Qualified Piece Identifier) implementation for Ruby
7
+ #
8
+ # Provides a rule-agnostic format for identifying game pieces in abstract strategy board games
9
+ # by combining Style Identifier Notation (SIN) and Piece Identifier Notation (PIN) primitives
10
+ # with a colon separator. This combination enables complete piece identification across different
11
+ # game styles and contexts.
12
+ #
13
+ # ## Concept
14
+ #
15
+ # QPI addresses the fundamental need to uniquely identify game pieces across different style
16
+ # systems while maintaining complete attribute information. By combining SIN and PIN primitives,
17
+ # QPI provides explicit representation of all four fundamental piece attributes from the
18
+ # Sashité Protocol.
19
+ #
20
+ # ## Four Fundamental Attributes
21
+ #
22
+ # QPI represents all four piece attributes through primitive combination:
23
+ # - **Family**: Style identification from SIN component
24
+ # - **Type**: Piece type from PIN component
25
+ # - **Side**: Player assignment from both components (must be consistent)
26
+ # - **State**: Piece state from PIN component
27
+ #
28
+ # ## Format Structure
29
+ #
30
+ # A QPI identifier consists of two primitive components separated by a colon:
31
+ # - **SIN component**: Style identification with player assignment
32
+ # - **PIN component**: Piece identification with type, side, and state
33
+ # - **Separator**: Colon (:) provides clear delimitation
34
+ #
35
+ # The components must maintain semantic consistency: both SIN and PIN must represent
36
+ # the same player (first or second) through their respective case encodings.
37
+ #
38
+ # ## Semantic Consistency Constraint
39
+ #
40
+ # QPI enforces a critical constraint: the style identified by the SIN component must be
41
+ # associated with the same player as indicated by the PIN component. This ensures that
42
+ # piece ownership and style ownership remain aligned, preventing impossible combinations
43
+ # like a first player style with a second player piece.
44
+ #
45
+ # Examples of semantic consistency:
46
+ # - SIN "C" (first player) + PIN "K" (first player) = Valid
47
+ # - SIN "c" (second player) + PIN "k" (second player) = Valid
48
+ # - SIN "C" (first player) + PIN "k" (second player) = Invalid
49
+ # - SIN "c" (second player) + PIN "K" (first player) = Invalid
50
+ #
51
+ # ## Cross-Style Gaming Support
52
+ #
53
+ # QPI enables cross-style gaming scenarios where different players use different game
54
+ # traditions. The explicit style identification allows pieces from different systems
55
+ # to coexist while maintaining clear attribution to their respective players.
56
+ #
57
+ # ## Format Specification
58
+ #
59
+ # Structure: `<sin>:<pin>`
60
+ #
61
+ # Grammar (BNF):
62
+ # <qpi> ::= <uppercase-qpi> | <lowercase-qpi>
63
+ # <uppercase-qpi> ::= <uppercase-letter> ":" <uppercase-pin>
64
+ # <lowercase-qpi> ::= <lowercase-letter> ":" <lowercase-pin>
65
+ # <uppercase-pin> ::= ["+" | "-"] <uppercase-letter>
66
+ # <lowercase-pin> ::= ["+" | "-"] <lowercase-letter>
67
+ #
68
+ # Regular Expression: `/\A([A-Z]:[-+]?[A-Z]|[a-z]:[-+]?[a-z])\z/`
69
+ #
70
+ # ## Attribute Mapping
71
+ #
72
+ # QPI encodes piece attributes through primitive combination:
73
+ #
74
+ # | Piece Attribute | QPI Encoding | Examples |
75
+ # |-----------------|--------------|----------|
76
+ # | **Family** | SIN component | `C:K` = Chess family, `O:K` = Ogi family |
77
+ # | **Type** | PIN letter choice | `C:K` = King, `C:P` = Pawn |
78
+ # | **Side** | Component cases | `C:K` = First player, `c:k` = Second player |
79
+ # | **State** | PIN prefix modifier | `O:+P` = Enhanced, `C:-P` = Diminished |
80
+ #
81
+ # ## System Constraints
82
+ #
83
+ # - **Semantic Consistency**: SIN and PIN components must represent the same player
84
+ # - **Component Validation**: Each component must be valid according to its specification
85
+ # - **Complete Attribution**: All four fundamental piece attributes explicitly represented
86
+ # - **Cross-Style Support**: Enables multi-tradition gaming environments
87
+ #
88
+ # ## Examples
89
+ #
90
+ # ### Single-Style Games
91
+ #
92
+ # # Chess (both players use Chess style)
93
+ # white_king = Sashite::Qpi.parse("C:K") # Chess king, first player
94
+ # black_king = Sashite::Qpi.parse("c:k") # Chess king, second player
95
+ #
96
+ # # Ogi (both players use Ogi style)
97
+ # sente_king = Sashite::Qpi.parse("O:K") # Ogi king, first player (sente)
98
+ # gote_rook = Sashite::Qpi.parse("o:+r") # Ogi promoted rook, second player (gote)
99
+ #
100
+ # ### Cross-Style Games
101
+ #
102
+ # # Chess vs. Ogi match
103
+ # chess_player = Sashite::Qpi.parse("C:K") # First player uses Chess
104
+ # ogi_player = Sashite::Qpi.parse("o:k") # Second player uses Ogi
105
+ #
106
+ # # Verify cross-style scenario
107
+ # chess_player.cross_family?(ogi_player) # => true
108
+ #
109
+ # ### Attribute Access and Manipulation
110
+ #
111
+ # identifier = Sashite::Qpi.parse("O:+R")
112
+ #
113
+ # # Four fundamental attributes
114
+ # identifier.family # => :O
115
+ # identifier.type # => :R
116
+ # identifier.side # => :first
117
+ # identifier.state # => :enhanced
118
+ #
119
+ # # Component extraction
120
+ # identifier.to_sin # => "O"
121
+ # identifier.to_pin # => "+R"
122
+ #
123
+ # # Immutable transformations
124
+ # flipped = identifier.flip # => "o:+r"
125
+ # different_type = identifier.with_type(:Q) # => "O:+Q"
126
+ # different_family = identifier.with_family(:C) # => "C:+R"
127
+ #
128
+ # ## Design Properties
129
+ #
130
+ # - **Rule-agnostic**: Independent of specific game mechanics
131
+ # - **Complete identification**: Explicit representation of all four piece attributes
132
+ # - **Cross-style support**: Enables multi-tradition gaming environments
133
+ # - **Semantic validation**: Ensures consistency between style and piece ownership
134
+ # - **Primitive foundation**: Built from foundational SIN and PIN building blocks
135
+ # - **Extension-ready**: Can be enhanced by human-readable naming systems
136
+ # - **Context-flexible**: Adaptable to various identification needs
137
+ # - **Immutable**: All instances are frozen and transformations return new objects
138
+ # - **Functional**: Pure functions with no side effects
139
+ #
140
+ # @see https://sashite.dev/specs/qpi/1.0.0/ QPI Specification v1.0.0
141
+ # @see https://sashite.dev/specs/qpi/1.0.0/examples/ QPI Examples
142
+ # @see https://sashite.dev/specs/sin/1.0.0/ Style Identifier Notation (SIN)
143
+ # @see https://sashite.dev/specs/pin/1.0.0/ Piece Identifier Notation (PIN)
144
+ module Qpi
145
+ # Check if a string is a valid QPI notation
146
+ #
147
+ # Validates the string format and semantic consistency between SIN and PIN components.
148
+ # Both components must be individually valid and represent the same player through
149
+ # their respective case encodings.
150
+ #
151
+ # @param qpi_string [String] the string to validate
152
+ # @return [Boolean] true if valid QPI, false otherwise
153
+ #
154
+ # @example Validate various QPI formats
155
+ # Sashite::Qpi.valid?("C:K") # => true (Chess king, first player)
156
+ # Sashite::Qpi.valid?("c:k") # => true (Chess king, second player)
157
+ # Sashite::Qpi.valid?("O:+P") # => true (Ogi enhanced pawn, first player)
158
+ # Sashite::Qpi.valid?("o:-r") # => true (Ogi diminished rook, second player)
159
+ # Sashite::Qpi.valid?("C:k") # => false (semantic mismatch: first player style, second player piece)
160
+ # Sashite::Qpi.valid?("c:K") # => false (semantic mismatch: second player style, first player piece)
161
+ # Sashite::Qpi.valid?("CHESS:K") # => false (multi-character SIN component)
162
+ # Sashite::Qpi.valid?("C") # => false (missing PIN component)
163
+ def self.valid?(qpi_string)
164
+ Identifier.valid?(qpi_string)
165
+ end
166
+
167
+ # Parse a QPI string into an Identifier object
168
+ #
169
+ # Creates a new QPI identifier by parsing the string into SIN and PIN components,
170
+ # validating each component, and ensuring semantic consistency between them.
171
+ #
172
+ # @param qpi_string [String] QPI notation string (format: sin:pin)
173
+ # @return [Qpi::Identifier] parsed identifier object with family, type, side, and state attributes
174
+ # @raise [ArgumentError] if the QPI string is invalid or semantically inconsistent
175
+ #
176
+ # @example Parse different QPI formats with complete attribute access
177
+ # Sashite::Qpi.parse("C:K") # => #<Qpi::Identifier family=:C type=:K side=:first state=:normal>
178
+ # Sashite::Qpi.parse("c:k") # => #<Qpi::Identifier family=:C type=:K side=:second state=:normal>
179
+ # Sashite::Qpi.parse("O:+R") # => #<Qpi::Identifier family=:O type=:R side=:first state=:enhanced>
180
+ # Sashite::Qpi.parse("x:-s") # => #<Qpi::Identifier family=:X type=:S side=:second state=:diminished>
181
+ #
182
+ # @example Traditional game styles
183
+ # chess_king = Sashite::Qpi.parse("C:K") # Chess king, first player
184
+ # ogi_rook = Sashite::Qpi.parse("o:+r") # Ogi promoted rook, second player
185
+ # xiongqi_king = Sashite::Qpi.parse("X:K") # Xiongqi king, first player
186
+ def self.parse(qpi_string)
187
+ Identifier.parse(qpi_string)
188
+ end
189
+
190
+ # Create a new identifier instance with explicit parameters
191
+ #
192
+ # Constructs a QPI identifier by directly specifying all four fundamental attributes.
193
+ # This method provides parameter-based construction as an alternative to string parsing,
194
+ # enabling immediate validation and clearer API usage.
195
+ #
196
+ # @param family [Symbol] style family identifier (single ASCII letter as symbol)
197
+ # @param type [Symbol] piece type (:A to :Z)
198
+ # @param side [Symbol] player side (:first or :second)
199
+ # @param state [Symbol] piece state (:normal, :enhanced, or :diminished)
200
+ # @return [Qpi::Identifier] new immutable identifier instance
201
+ # @raise [ArgumentError] if parameters are invalid or semantically inconsistent
202
+ #
203
+ # @example Create identifiers with explicit parameters
204
+ # Sashite::Qpi.identifier(:C, :K, :first, :normal) # => "C:K"
205
+ # Sashite::Qpi.identifier(:c, :K, :second, :normal) # => "c:k"
206
+ # Sashite::Qpi.identifier(:O, :R, :first, :enhanced) # => "O:+R"
207
+ # Sashite::Qpi.identifier(:x, :S, :second, :diminished) # => "x:-s"
208
+ #
209
+ # @example Cross-style game setup
210
+ # chess_player = Sashite::Qpi.identifier(:C, :K, :first, :normal) # Chess king, first player
211
+ # ogi_player = Sashite::Qpi.identifier(:o, :K, :second, :normal) # Ogi king, second player
212
+ #
213
+ # chess_player.cross_family?(ogi_player) # => true (different families)
214
+ # chess_player.same_type?(ogi_player) # => true (both kings)
215
+ # chess_player.same_side?(ogi_player) # => false (different players)
216
+ def self.identifier(family, type, side, state = Pin::Identifier::NORMAL_STATE)
217
+ Identifier.new(family, type, side, state)
218
+ end
219
+ end
220
+ end
@@ -0,0 +1,14 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "sashite/qpi"
4
+
5
+ # Sashité namespace for board game notation libraries
6
+ #
7
+ # Sashité provides a collection of libraries for representing and manipulating
8
+ # board game concepts according to the Game Protocol specifications.
9
+ #
10
+ # @see https://sashite.dev/game-protocol/ Game Protocol Foundation
11
+ # @see https://sashite.dev/specs/ Sashité Specifications
12
+ # @author Sashité
13
+ module Sashite
14
+ end
metadata ADDED
@@ -0,0 +1,87 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: sashite-qpi
3
+ version: !ruby/object:Gem::Version
4
+ version: 1.0.0
5
+ platform: ruby
6
+ authors:
7
+ - Cyril Kato
8
+ bindir: bin
9
+ cert_chain: []
10
+ date: 1980-01-02 00:00:00.000000000 Z
11
+ dependencies:
12
+ - !ruby/object:Gem::Dependency
13
+ name: sashite-pin
14
+ requirement: !ruby/object:Gem::Requirement
15
+ requirements:
16
+ - - "~>"
17
+ - !ruby/object:Gem::Version
18
+ version: '3.1'
19
+ type: :runtime
20
+ prerelease: false
21
+ version_requirements: !ruby/object:Gem::Requirement
22
+ requirements:
23
+ - - "~>"
24
+ - !ruby/object:Gem::Version
25
+ version: '3.1'
26
+ - !ruby/object:Gem::Dependency
27
+ name: sashite-sin
28
+ requirement: !ruby/object:Gem::Requirement
29
+ requirements:
30
+ - - "~>"
31
+ - !ruby/object:Gem::Version
32
+ version: '2.1'
33
+ type: :runtime
34
+ prerelease: false
35
+ version_requirements: !ruby/object:Gem::Requirement
36
+ requirements:
37
+ - - "~>"
38
+ - !ruby/object:Gem::Version
39
+ version: '2.1'
40
+ description: |
41
+ QPI (Qualified Piece Identifier) provides a rule-agnostic format for identifying game pieces
42
+ in abstract strategy board games by combining Style Identifier Notation (SIN) and Piece
43
+ Identifier Notation (PIN) primitives. This gem implements the QPI Specification v1.0.0 with
44
+ a modern Ruby interface featuring immutable identifier objects and functional programming
45
+ principles. QPI enables complete piece identification with all four fundamental attributes
46
+ (family, type, side, state) while supporting cross-style gaming environments. Perfect for
47
+ multi-tradition board games, hybrid gaming systems, and game engines requiring comprehensive
48
+ piece identification across different game styles and traditions.
49
+ email: contact@cyril.email
50
+ executables: []
51
+ extensions: []
52
+ extra_rdoc_files: []
53
+ files:
54
+ - LICENSE.md
55
+ - README.md
56
+ - lib/sashite-qpi.rb
57
+ - lib/sashite/qpi.rb
58
+ - lib/sashite/qpi/identifier.rb
59
+ homepage: https://github.com/sashite/qpi.rb
60
+ licenses:
61
+ - MIT
62
+ metadata:
63
+ bug_tracker_uri: https://github.com/sashite/qpi.rb/issues
64
+ documentation_uri: https://rubydoc.info/github/sashite/qpi.rb/main
65
+ homepage_uri: https://github.com/sashite/qpi.rb
66
+ source_code_uri: https://github.com/sashite/qpi.rb
67
+ specification_uri: https://sashite.dev/specs/qpi/1.0.0/
68
+ rubygems_mfa_required: 'true'
69
+ rdoc_options: []
70
+ require_paths:
71
+ - lib
72
+ required_ruby_version: !ruby/object:Gem::Requirement
73
+ requirements:
74
+ - - ">="
75
+ - !ruby/object:Gem::Version
76
+ version: 3.2.0
77
+ required_rubygems_version: !ruby/object:Gem::Requirement
78
+ requirements:
79
+ - - ">="
80
+ - !ruby/object:Gem::Version
81
+ version: '0'
82
+ requirements: []
83
+ rubygems_version: 3.7.1
84
+ specification_version: 4
85
+ summary: QPI (Qualified Piece Identifier) implementation for Ruby with immutable identifier
86
+ objects
87
+ test_files: []