pangram 0.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.
- checksums.yaml +7 -0
- data/.github/workflows/ci.yml +31 -0
- data/.github/workflows/release.yml +80 -0
- data/.gitignore +11 -0
- data/.mise.toml +3 -0
- data/.rspec +2 -0
- data/.rubocop.yml +31 -0
- data/CHANGELOG.md +10 -0
- data/Gemfile +5 -0
- data/Gemfile.lock +110 -0
- data/LICENSE +21 -0
- data/Makefile +66 -0
- data/README.md +228 -0
- data/Rakefile +8 -0
- data/docs/API_REFERENCE.md +254 -0
- data/docs/DEVELOPMENT.md +113 -0
- data/docs/README.md +25 -0
- data/docs/RELEASING.md +72 -0
- data/lib/pangram/client.rb +587 -0
- data/lib/pangram/errors.rb +37 -0
- data/lib/pangram/version.rb +7 -0
- data/lib/pangram.rb +15 -0
- data/pangram.gemspec +51 -0
- metadata +270 -0
|
@@ -0,0 +1,254 @@
|
|
|
1
|
+
# API Reference
|
|
2
|
+
|
|
3
|
+
The public API is exposed through `Pangram::Client`. `Pangram.new` is a
|
|
4
|
+
convenience constructor for the same client.
|
|
5
|
+
|
|
6
|
+
```ruby
|
|
7
|
+
require 'pangram'
|
|
8
|
+
|
|
9
|
+
client = Pangram.new(api_key: ENV.fetch('PANGRAM_API_KEY'))
|
|
10
|
+
# Equivalent to Pangram::Client.new(...)
|
|
11
|
+
```
|
|
12
|
+
|
|
13
|
+
All successful API responses remain ordinary Ruby `Hash` and `Array` values.
|
|
14
|
+
Hash keys are strings and match Pangram's JSON response fields.
|
|
15
|
+
|
|
16
|
+
## Authentication
|
|
17
|
+
|
|
18
|
+
```ruby
|
|
19
|
+
Pangram.new(api_key: 'your-api-key')
|
|
20
|
+
```
|
|
21
|
+
|
|
22
|
+
When `api_key:` is omitted, the client reads `PANGRAM_API_KEY`. An explicit
|
|
23
|
+
argument takes precedence over the environment. If neither is available,
|
|
24
|
+
construction raises `Pangram::AuthenticationError`.
|
|
25
|
+
|
|
26
|
+
Every request sends the key through the `x-api-key` header.
|
|
27
|
+
|
|
28
|
+
## Model discovery
|
|
29
|
+
|
|
30
|
+
### `list_models`
|
|
31
|
+
|
|
32
|
+
```ruby
|
|
33
|
+
models = client.list_models
|
|
34
|
+
# => ["default", "pangram-4"]
|
|
35
|
+
```
|
|
36
|
+
|
|
37
|
+
Returns the server-ordered selectors available to the current API key. The
|
|
38
|
+
catalog is entitlement- and availability-aware, so callers should not
|
|
39
|
+
hard-code it.
|
|
40
|
+
|
|
41
|
+
The client validates that the response contains unique, non-blank model names
|
|
42
|
+
and the `default` selector.
|
|
43
|
+
|
|
44
|
+
## Text prediction
|
|
45
|
+
|
|
46
|
+
### `predict`
|
|
47
|
+
|
|
48
|
+
```ruby
|
|
49
|
+
result = client.predict(
|
|
50
|
+
text,
|
|
51
|
+
model: 'pangram-4',
|
|
52
|
+
public_dashboard_link: false,
|
|
53
|
+
timeout: 300,
|
|
54
|
+
poll_interval: 0.5
|
|
55
|
+
)
|
|
56
|
+
```
|
|
57
|
+
|
|
58
|
+
Submits `POST /task`, polls `GET /task/{task_id}`, and returns when the task
|
|
59
|
+
reaches `STAGE_SUCCESS`.
|
|
60
|
+
|
|
61
|
+
| Keyword | Default | Description |
|
|
62
|
+
| --- | --- | --- |
|
|
63
|
+
| `model` | `nil` | Selector returned by `list_models` |
|
|
64
|
+
| `public_dashboard_link` | `false` | Request a public result link |
|
|
65
|
+
| `timeout` | `300` | Total deadline for submission and polling, in seconds |
|
|
66
|
+
| `poll_interval` | `0.5` | Delay between polls, clamped to at least 0.1 seconds |
|
|
67
|
+
|
|
68
|
+
Omitting `model` currently preserves Pangram's legacy wire payload and emits a
|
|
69
|
+
deprecation warning. New integrations should pass `model: "default"` or
|
|
70
|
+
another selector returned by `list_models`.
|
|
71
|
+
|
|
72
|
+
A successful result can include:
|
|
73
|
+
|
|
74
|
+
- `stage`, `text`, `version`, `headline`, `prediction`, and `prediction_short`
|
|
75
|
+
- `fraction_ai`, `fraction_ai_assisted`, and `fraction_human`
|
|
76
|
+
- segment counters and `dashboard_link` when requested
|
|
77
|
+
- `windows`, including labels, scores, confidence, character offsets, word and
|
|
78
|
+
token counts, and Pangram 4 humanizer fields
|
|
79
|
+
|
|
80
|
+
The SDK returns the server payload without renaming or symbolizing fields.
|
|
81
|
+
|
|
82
|
+
### `predict_with_dashboard_link`
|
|
83
|
+
|
|
84
|
+
```ruby
|
|
85
|
+
result = client.predict_with_dashboard_link(
|
|
86
|
+
text,
|
|
87
|
+
model: 'pangram-4',
|
|
88
|
+
timeout: 300,
|
|
89
|
+
poll_interval: 0.5
|
|
90
|
+
)
|
|
91
|
+
```
|
|
92
|
+
|
|
93
|
+
Equivalent to `predict(..., public_dashboard_link: true)`.
|
|
94
|
+
|
|
95
|
+
## Bulk jobs
|
|
96
|
+
|
|
97
|
+
### `submit_bulk`
|
|
98
|
+
|
|
99
|
+
Submit exactly one payload shape:
|
|
100
|
+
|
|
101
|
+
```ruby
|
|
102
|
+
client.submit_bulk(text: ['first', 'second'], model: 'pangram-4')
|
|
103
|
+
|
|
104
|
+
client.submit_bulk(
|
|
105
|
+
items: [
|
|
106
|
+
{ id: 'row-1', text: 'first' },
|
|
107
|
+
{ id: 'row-2', text: 'second' }
|
|
108
|
+
],
|
|
109
|
+
model: 'pangram-4'
|
|
110
|
+
)
|
|
111
|
+
```
|
|
112
|
+
|
|
113
|
+
Returns the accepted `202` response, including `bulk_id`, initial status,
|
|
114
|
+
accepted items, and immediate validation failures.
|
|
115
|
+
|
|
116
|
+
One model applies to the whole bulk job. Per-item model selectors are not
|
|
117
|
+
supported.
|
|
118
|
+
|
|
119
|
+
### `get_bulk_status`
|
|
120
|
+
|
|
121
|
+
```ruby
|
|
122
|
+
status = client.get_bulk_status(bulk_id)
|
|
123
|
+
```
|
|
124
|
+
|
|
125
|
+
Returns job status, counters, and creation/completion timestamps. Terminal
|
|
126
|
+
statuses are `succeeded`, `failed`, and `partial`.
|
|
127
|
+
|
|
128
|
+
### `wait_for_bulk`
|
|
129
|
+
|
|
130
|
+
```ruby
|
|
131
|
+
status = client.wait_for_bulk(
|
|
132
|
+
bulk_id,
|
|
133
|
+
timeout: 3600,
|
|
134
|
+
poll_interval: 0.5
|
|
135
|
+
)
|
|
136
|
+
```
|
|
137
|
+
|
|
138
|
+
Polls until the job reaches a terminal status. Transient network failures are
|
|
139
|
+
retried within the total deadline.
|
|
140
|
+
|
|
141
|
+
### `get_bulk_items`
|
|
142
|
+
|
|
143
|
+
```ruby
|
|
144
|
+
page = client.get_bulk_items(bulk_id, offset: 0, limit: 100)
|
|
145
|
+
```
|
|
146
|
+
|
|
147
|
+
Returns one page of item metadata.
|
|
148
|
+
|
|
149
|
+
### `get_bulk_results_page`
|
|
150
|
+
|
|
151
|
+
```ruby
|
|
152
|
+
page = client.get_bulk_results_page(bulk_id, offset: 0, limit: 100)
|
|
153
|
+
```
|
|
154
|
+
|
|
155
|
+
Returns one submitted-item page with successful or in-progress items in
|
|
156
|
+
`items` and failures in `failed_items`.
|
|
157
|
+
|
|
158
|
+
### `get_bulk_results`
|
|
159
|
+
|
|
160
|
+
```ruby
|
|
161
|
+
results = client.get_bulk_results(bulk_id, page_size: 1000, timeout: 3600)
|
|
162
|
+
```
|
|
163
|
+
|
|
164
|
+
Fetches every results page and returns a single aggregate containing
|
|
165
|
+
`bulk_id`, `total_items`, `items`, and `failed_items`. `page_size` must be from
|
|
166
|
+
1 through 1,000. `timeout` is the total deadline in seconds for fetching all
|
|
167
|
+
pages; transient failures are retried until that deadline. The aggregate's
|
|
168
|
+
`total_items` is locked to the first page so in-progress jobs cannot truncate
|
|
169
|
+
the result mid-pagination.
|
|
170
|
+
|
|
171
|
+
This method materializes all pages in memory. Process
|
|
172
|
+
`get_bulk_results_page` incrementally for very large jobs.
|
|
173
|
+
|
|
174
|
+
## File prediction
|
|
175
|
+
|
|
176
|
+
File prediction uses Pangram's default model and does not accept `model:`.
|
|
177
|
+
|
|
178
|
+
### `predict_file`
|
|
179
|
+
|
|
180
|
+
```ruby
|
|
181
|
+
result = client.predict_file(
|
|
182
|
+
'document.pdf',
|
|
183
|
+
public_dashboard_link: true,
|
|
184
|
+
timeout: 300
|
|
185
|
+
)
|
|
186
|
+
```
|
|
187
|
+
|
|
188
|
+
Uploads one file and returns its result.
|
|
189
|
+
|
|
190
|
+
### `predict_files`
|
|
191
|
+
|
|
192
|
+
```ruby
|
|
193
|
+
results = client.predict_files(
|
|
194
|
+
['first.docx', 'second.pdf'],
|
|
195
|
+
public_dashboard_link: true,
|
|
196
|
+
timeout: 300
|
|
197
|
+
)
|
|
198
|
+
```
|
|
199
|
+
|
|
200
|
+
Uploads multiple files in one multipart request. Each path is sent as a
|
|
201
|
+
repeated form field named `files`, not `files[]`. Every file handle is closed
|
|
202
|
+
after success or failure, including partial file-open failures.
|
|
203
|
+
|
|
204
|
+
## Plagiarism detection
|
|
205
|
+
|
|
206
|
+
### `check_plagiarism`
|
|
207
|
+
|
|
208
|
+
```ruby
|
|
209
|
+
result = client.check_plagiarism('Text to check')
|
|
210
|
+
```
|
|
211
|
+
|
|
212
|
+
Returns Pangram's plagiarism response, including detection status, matched
|
|
213
|
+
content, sentence counts, and plagiarism percentage.
|
|
214
|
+
|
|
215
|
+
## Deprecated compatibility methods
|
|
216
|
+
|
|
217
|
+
The following methods mirror compatibility behavior still present in the
|
|
218
|
+
Python SDK:
|
|
219
|
+
|
|
220
|
+
- `predict_short(text, model:)` uses the current prediction flow.
|
|
221
|
+
- `batch_predict(texts, model:)` predicts each input sequentially.
|
|
222
|
+
|
|
223
|
+
Use `predict` for one input and `submit_bulk` for many inputs.
|
|
224
|
+
|
|
225
|
+
## Errors
|
|
226
|
+
|
|
227
|
+
All SDK-defined exceptions inherit from `Pangram::Error`.
|
|
228
|
+
|
|
229
|
+
| Exception | Raised when |
|
|
230
|
+
| --- | --- |
|
|
231
|
+
| `Pangram::AuthenticationError` | No API key is configured |
|
|
232
|
+
| `Pangram::ValidationError` | A local argument is invalid |
|
|
233
|
+
| `Pangram::APIError` | Pangram rejects a request or an async task fails |
|
|
234
|
+
| `Pangram::InvalidResponseError` | A response is not valid JSON or has an invalid top-level contract |
|
|
235
|
+
| `Pangram::NetworkError` | An HTTP request fails at the transport layer |
|
|
236
|
+
| `Pangram::TimeoutError` | Prediction or bulk polling exceeds its total deadline |
|
|
237
|
+
|
|
238
|
+
```ruby
|
|
239
|
+
begin
|
|
240
|
+
client.predict(text, model: 'default')
|
|
241
|
+
rescue Pangram::TimeoutError => e
|
|
242
|
+
warn e.message
|
|
243
|
+
rescue Pangram::Error => e
|
|
244
|
+
warn "Pangram failed: #{e.message}"
|
|
245
|
+
end
|
|
246
|
+
```
|
|
247
|
+
|
|
248
|
+
API errors carry the HTTP `status` and raw response `body` when they come from
|
|
249
|
+
a rejected response. `Pangram::InvalidResponseError` is a subclass of
|
|
250
|
+
`Pangram::APIError`, so rescuing `APIError` also catches schema violations.
|
|
251
|
+
|
|
252
|
+
Transport errors and responses with status 408, 429, 500, 502, 503, or 504 are
|
|
253
|
+
retried until the total deadline during prediction polling, bulk polling, and
|
|
254
|
+
bulk results pagination. Other API errors are not retried.
|
data/docs/DEVELOPMENT.md
ADDED
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
# Development Guide
|
|
2
|
+
|
|
3
|
+
## Repository layout
|
|
4
|
+
|
|
5
|
+
```text
|
|
6
|
+
lib/pangram.rb Public entry point and Pangram.new
|
|
7
|
+
lib/pangram/client.rb API client, polling, pagination, and uploads
|
|
8
|
+
lib/pangram/errors.rb SDK exception hierarchy
|
|
9
|
+
lib/pangram/version.rb Gem version
|
|
10
|
+
spec/ Offline contract tests
|
|
11
|
+
docs/ Extended project documentation
|
|
12
|
+
pangram.gemspec Package metadata and dependencies
|
|
13
|
+
```
|
|
14
|
+
|
|
15
|
+
## Setup
|
|
16
|
+
|
|
17
|
+
Ruby is managed through [mise](https://mise.jdx.dev/):
|
|
18
|
+
|
|
19
|
+
```bash
|
|
20
|
+
mise trust
|
|
21
|
+
mise install
|
|
22
|
+
bundle install
|
|
23
|
+
```
|
|
24
|
+
|
|
25
|
+
The gem requires Ruby 3.1 or newer. The lockfile is constrained so all
|
|
26
|
+
development dependencies also remain compatible with Ruby 3.1.
|
|
27
|
+
|
|
28
|
+
## Make targets
|
|
29
|
+
|
|
30
|
+
Run `make help` to list available commands.
|
|
31
|
+
|
|
32
|
+
| Command | Purpose |
|
|
33
|
+
| --- | --- |
|
|
34
|
+
| `make install` | Install Bundler dependencies |
|
|
35
|
+
| `make test` or `make spec` | Run all RSpec tests |
|
|
36
|
+
| `make coverage` | Run tests and print the coverage report path |
|
|
37
|
+
| `make lint` | Run RuboCop |
|
|
38
|
+
| `make lint-fix` | Run RuboCop auto-correction |
|
|
39
|
+
| `make docs` | Generate YARD documentation under `doc/` |
|
|
40
|
+
| `make build` | Build `pkg/pangram-VERSION.gem` |
|
|
41
|
+
| `make verify` | Run tests, lint, docs, and build |
|
|
42
|
+
| `make console` | Start IRB with `pangram` loaded |
|
|
43
|
+
| `make clean` | Remove generated artifacts |
|
|
44
|
+
|
|
45
|
+
## Testing
|
|
46
|
+
|
|
47
|
+
```bash
|
|
48
|
+
make test
|
|
49
|
+
```
|
|
50
|
+
|
|
51
|
+
The test suite uses WebMock and must not contact Pangram's live services. It
|
|
52
|
+
verifies:
|
|
53
|
+
|
|
54
|
+
- request URLs, headers, JSON payloads, and status codes
|
|
55
|
+
- model catalog validation
|
|
56
|
+
- prediction and bulk polling, retry, failure, and timeout paths
|
|
57
|
+
- bulk result pagination and aggregation
|
|
58
|
+
- exact multipart field names and file-handle cleanup
|
|
59
|
+
- API, network, validation, and invalid-response errors
|
|
60
|
+
|
|
61
|
+
SimpleCov writes its report to `coverage/index.html` and enforces at least 90%
|
|
62
|
+
line coverage.
|
|
63
|
+
|
|
64
|
+
## Linting
|
|
65
|
+
|
|
66
|
+
```bash
|
|
67
|
+
make lint
|
|
68
|
+
```
|
|
69
|
+
|
|
70
|
+
RuboCop targets Ruby 3.1. Prefer correcting the implementation over disabling
|
|
71
|
+
a cop for a local exception.
|
|
72
|
+
|
|
73
|
+
## API documentation
|
|
74
|
+
|
|
75
|
+
Public methods use YARD-compatible comments. Generate HTML documentation with:
|
|
76
|
+
|
|
77
|
+
```bash
|
|
78
|
+
make docs
|
|
79
|
+
```
|
|
80
|
+
|
|
81
|
+
Generated files are written to `doc/` and are excluded from source control.
|
|
82
|
+
|
|
83
|
+
## Building and inspecting the gem
|
|
84
|
+
|
|
85
|
+
```bash
|
|
86
|
+
make build
|
|
87
|
+
gem specification pkg/pangram-0.1.0.gem
|
|
88
|
+
gem contents --show-install-dir pangram
|
|
89
|
+
```
|
|
90
|
+
|
|
91
|
+
For an isolated installation smoke test:
|
|
92
|
+
|
|
93
|
+
```bash
|
|
94
|
+
tmp_dir="$(mktemp -d)"
|
|
95
|
+
GEM_HOME="$tmp_dir" GEM_PATH="$tmp_dir" \
|
|
96
|
+
gem install pkg/pangram-0.1.0.gem --no-document
|
|
97
|
+
GEM_HOME="$tmp_dir" GEM_PATH="$tmp_dir" \
|
|
98
|
+
ruby -e 'require "pangram"; puts Pangram::VERSION'
|
|
99
|
+
rm -rf "$tmp_dir"
|
|
100
|
+
```
|
|
101
|
+
|
|
102
|
+
Use the current value from `lib/pangram/version.rb` instead of `0.1.0` when
|
|
103
|
+
building a later release.
|
|
104
|
+
|
|
105
|
+
## Adding or changing an API method
|
|
106
|
+
|
|
107
|
+
1. Confirm the current REST contract in the official Pangram documentation.
|
|
108
|
+
2. Keep HTTP ownership in `Pangram::Client` and reuse existing request helpers.
|
|
109
|
+
3. Preserve response field names and string keys.
|
|
110
|
+
4. Add WebMock coverage for the request and relevant failure paths.
|
|
111
|
+
5. Update `docs/API_REFERENCE.md`, the project README when user-facing, and
|
|
112
|
+
`CHANGELOG.md`.
|
|
113
|
+
6. Run `make verify`.
|
data/docs/README.md
ADDED
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
# Pangram Ruby SDK Documentation
|
|
2
|
+
|
|
3
|
+
This directory contains the extended documentation for the `pangram` gem.
|
|
4
|
+
|
|
5
|
+
## Documentation index
|
|
6
|
+
|
|
7
|
+
- [API reference](API_REFERENCE.md) — client construction, method signatures,
|
|
8
|
+
response contracts, polling behavior, and errors.
|
|
9
|
+
- [Development guide](DEVELOPMENT.md) — repository layout, local setup,
|
|
10
|
+
testing, linting, documentation generation, and gem builds.
|
|
11
|
+
- [Release guide](RELEASING.md) — versioning, verification, package inspection,
|
|
12
|
+
and the manual RubyGems publishing checklist.
|
|
13
|
+
|
|
14
|
+
## Quick links
|
|
15
|
+
|
|
16
|
+
- [Project README](../README.md)
|
|
17
|
+
- [Changelog](../CHANGELOG.md)
|
|
18
|
+
- [Client implementation](../lib/pangram/client.rb)
|
|
19
|
+
- [Contract tests](../spec/pangram/client_spec.rb)
|
|
20
|
+
- [Official Pangram API documentation](https://docs.pangram.com/api-reference/introduction)
|
|
21
|
+
|
|
22
|
+
## Supported Ruby versions
|
|
23
|
+
|
|
24
|
+
The gem requires Ruby 3.1 or newer. CI covers Ruby 3.1, 3.2, 3.3, 3.4,
|
|
25
|
+
and 4.0.
|
data/docs/RELEASING.md
ADDED
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
# Release Guide
|
|
2
|
+
|
|
3
|
+
Publishing a gem is a shared, irreversible action. Complete the checks below,
|
|
4
|
+
review the built package, and publish only with explicit maintainer approval.
|
|
5
|
+
|
|
6
|
+
## 1. Update release metadata
|
|
7
|
+
|
|
8
|
+
1. Change `Pangram::VERSION` in `lib/pangram/version.rb`.
|
|
9
|
+
2. Move the relevant entries in `CHANGELOG.md` from `Unreleased` into a dated
|
|
10
|
+
version section.
|
|
11
|
+
3. Confirm README and API reference examples still match the implementation.
|
|
12
|
+
|
|
13
|
+
The version must follow semantic versioning and must not already exist on
|
|
14
|
+
RubyGems.
|
|
15
|
+
|
|
16
|
+
## 2. Verify the source
|
|
17
|
+
|
|
18
|
+
```bash
|
|
19
|
+
make verify
|
|
20
|
+
```
|
|
21
|
+
|
|
22
|
+
This runs the complete test suite, RuboCop, YARD generation, and gem build.
|
|
23
|
+
Do not publish if any command fails.
|
|
24
|
+
|
|
25
|
+
CI also tests the supported Ruby matrix from 3.1 through 4.0.
|
|
26
|
+
|
|
27
|
+
## 3. Inspect the package
|
|
28
|
+
|
|
29
|
+
```bash
|
|
30
|
+
gem specification pkg/pangram-VERSION.gem
|
|
31
|
+
gem unpack pkg/pangram-VERSION.gem --target /tmp/pangram-gem-review
|
|
32
|
+
```
|
|
33
|
+
|
|
34
|
+
Confirm that:
|
|
35
|
+
|
|
36
|
+
- the name is `pangram` and the version is correct
|
|
37
|
+
- `lib/`, README, license, changelog, and docs are present
|
|
38
|
+
- test artifacts, coverage output, and credentials are absent
|
|
39
|
+
- runtime dependencies and Ruby requirements are correct
|
|
40
|
+
|
|
41
|
+
## 4. Test an isolated install
|
|
42
|
+
|
|
43
|
+
```bash
|
|
44
|
+
tmp_dir="$(mktemp -d)"
|
|
45
|
+
GEM_HOME="$tmp_dir" GEM_PATH="$tmp_dir" \
|
|
46
|
+
gem install pkg/pangram-VERSION.gem --no-document
|
|
47
|
+
GEM_HOME="$tmp_dir" GEM_PATH="$tmp_dir" \
|
|
48
|
+
ruby -e 'require "pangram"; puts Pangram::VERSION'
|
|
49
|
+
rm -rf "$tmp_dir"
|
|
50
|
+
```
|
|
51
|
+
|
|
52
|
+
## 5. Commit and tag
|
|
53
|
+
|
|
54
|
+
After review, commit the version and changelog changes. Create an annotated tag
|
|
55
|
+
using the same version:
|
|
56
|
+
|
|
57
|
+
```bash
|
|
58
|
+
git tag -a vVERSION -m "Release vVERSION"
|
|
59
|
+
```
|
|
60
|
+
|
|
61
|
+
Pushing commits or tags requires explicit approval.
|
|
62
|
+
|
|
63
|
+
## 6. Publish manually
|
|
64
|
+
|
|
65
|
+
With maintainer approval and valid RubyGems credentials:
|
|
66
|
+
|
|
67
|
+
```bash
|
|
68
|
+
gem push pkg/pangram-VERSION.gem
|
|
69
|
+
```
|
|
70
|
+
|
|
71
|
+
After publishing, install `pangram` from RubyGems in a clean environment and
|
|
72
|
+
verify its version and `require "pangram"` entry point.
|