figure-gate 0.1.0__tar.gz

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.
@@ -0,0 +1,15 @@
1
+ __pycache__/
2
+ *.py[cod]
3
+ .pytest_cache/
4
+ .ruff_cache/
5
+ .venv/
6
+ venv/
7
+ uv.lock
8
+ *.egg-info/
9
+ dist/
10
+ build/
11
+ .DS_Store
12
+
13
+ # Agent state, not project state.
14
+ .claude/
15
+ .codebase-memory/
@@ -0,0 +1,350 @@
1
+ # Changelog
2
+
3
+ ## 0.1.0 — 2026-07-27
4
+
5
+ First release on PyPI. Everything below is the work that preceded it.
6
+
7
+ ### Four gates that were measuring the wrong thing
8
+
9
+ Found by running the checker over ordinary matplotlib figures nobody built for
10
+ it — the corpus check CONTRIBUTING asks for, applied to the checker itself
11
+ rather than to a new gate.
12
+
13
+ - **Polar radial tick labels are no longer judged by `check_text_readability`.**
14
+ Every ordinary polar plot failed. matplotlib places radial labels inside the
15
+ disc and `set_rlabel_position` only moves them to another angle, so "move the
16
+ label to clear ground" named a move that does not exist. The first fix
17
+ exempted only the clutter clause and was not enough: the contrast clause went
18
+ on failing **eight of eight** polar plots built on this project's own
19
+ `figure.mplstyle`, at 2.0:1 against a curve the radial axis crosses by
20
+ construction. A gate the bundled sheet cannot satisfy is measuring the
21
+ projection, not the figure. Both clauses now skip these labels and the row
22
+ reports how many went unjudged, so the author is told rather than left to
23
+ assume they passed. Author-placed text on a polar axes is still judged.
24
+
25
+ - **`check_redundancy` no longer reads furniture that never renders.** Three
26
+ `imshow` panels at `axis("off")` were told to "use sharex/sharey" on the
27
+ strength of the tick `Text` objects matplotlib keeps for an axis it is not
28
+ drawing. Axes with `axison` false are skipped, and the tick clause now counts
29
+ only visible labels.
30
+
31
+ - **`check_ink` can see an empty panel again.** The README has always claimed
32
+ the row catches "a panel is empty", and the pixel fraction could not: a blank
33
+ axes measures about 0.03 from its frame and ticks alone, over the 0.02 floor.
34
+ The blank subplot in a grid — the case that actually ships in papers — read as
35
+ merely sparse. Emptiness is now structural: was anything drawn into this axes.
36
+ The fraction clause is unchanged and still governs sparse and saturated
37
+ panels.
38
+
39
+ - **`check_line_weight` has tests.** It shipped with none, which is the one
40
+ thing CONTRIBUTING says a gate may not do. Four now: a hairline fails, a
41
+ legal stroke passes, a 1.2pt stroke fails once placement thins it to 0.6pt on
42
+ the page, and the grid is not held to the data floor. The gate was correct —
43
+ it had simply never been watched work.
44
+
45
+ ### Text readability — the gate the demo needed
46
+
47
+ - **`check_text_readability` measures whether each string can be read where it
48
+ sits.** Two clauses, both off rendered pixels. *Clutter*: the figure is drawn
49
+ a second time with every string hidden, and that render is the backdrop each
50
+ label was placed onto; pixels inside a label's box that no blend of {its
51
+ surface, the grid, the axis rule} explains are data ink passing through the
52
+ text. *Contrast*: the text against the backdrop it actually got, at the WCAG
53
+ **text** threshold (4.5:1, or 3:1 large) rather than the 3:1 a mark gets,
54
+ because a glyph stem is thinner than a mark.
55
+ - **Measuring the backdrop rather than the finished render is the point.**
56
+ Casing hides the evidence: a white halo over an orange curve renders as clean
57
+ white while punching a visible gap through the data. Both halves of that are
58
+ defects and this reads them as one number.
59
+ - **Clutter is an *edge*, not a deviation from the dominant color.** The first
60
+ version tested each pixel against the box's modal color and failed every
61
+ annotation ever placed on a heatmap, because a smooth ramp differs from its
62
+ own mode everywhere while being, to a reader, one surface. Now each pixel is
63
+ tested against a local average of its neighbours, so a viridis field is
64
+ ground and a hairline is a mark.
65
+ - **It found the sheet's own ticks.** `xtick.color`/`ytick.color` shipped at
66
+ `898781`, which is 3.59:1 on white — under the text threshold on every figure
67
+ in the repo. Now `777570` at 4.6:1, still below the 7.94:1 axis label, so the
68
+ hierarchy axis label > tick label > grid survives.
69
+ - **`examples/demo.py` was the first thing it failed.** All three direct labels
70
+ sat *on* their own curves — `'Baseline'` on data ink over 17% of its box —
71
+ while `check_label_attribution` passed them, correctly, because a label
72
+ printed on its own line is attributed perfectly. The cause was alignment:
73
+ `ha="center"` clears a sloped curve at the anchor and puts both ends of the
74
+ box back down on the line. Fixed by aligning toward the side the curve is
75
+ leaving, and by anchoring on the extreme of the noise across the label's own
76
+ span rather than on one point.
77
+
78
+ ### Research-standard gates
79
+
80
+ - **`check_line_weight`: 1pt on the page, per SIAM's instructions for authors**
81
+ ("lines one point or thicker; thinner lines may break up or disappear").
82
+ Measured through `page_scale` like the type floor, since it is the same
83
+ failure. Gridlines are held to a lower floor than data on purpose.
84
+ - **`check_fonts`: Type 42 embedding, and the face you named.** matplotlib
85
+ defaults `pdf.fonttype`/`ps.fonttype` to 3; IEEE PDF eXpress rejects the
86
+ upload and ACM/Elsevier reject the submission, with nothing warning you
87
+ because the figure renders identically. Also warns when none of the faces in
88
+ `font.<family>` is installed and matplotlib has silently substituted its own.
89
+ Advisory, because it reads global rcParams rather than anything the figure
90
+ carries — the hard gate is on the shipped sheet, in the test suite.
91
+ - **`figure.mplstyle` now sets `pdf.fonttype: 42` and `ps.fonttype: 42`.**
92
+
93
+ ### Venue content widths
94
+
95
+ - **`audit(fig, venue="neurips")`** replaces hand-measuring `CONTENT_WIDTH_PT`
96
+ for twelve known venues (NeurIPS, ICLR, ICML, ACL, IEEE, Nature, LaTeX
97
+ `article`, and the column widths of the two-column ones). `python
98
+ check_figure.py --venues` lists them. `CONTENT_WIDTH_PT` still works and still
99
+ wins for anything not in the table.
100
+
101
+ ### Alt text
102
+
103
+ - **`describe(fig, ...)` / `alt_metadata(fig)` / `check_alt_text`.** Across
104
+ 100,000 public Jupyter notebooks, 99.81% of programmatically generated images
105
+ shipped with no alt text, nearly all matplotlib. `alt_metadata` produces the
106
+ `metadata=` dict for `savefig`, so the description survives into the PNG, PDF
107
+ or SVG. Advisory: on a paper the description frequently *is* the caption, and
108
+ the caption lives where this cannot see it.
109
+
110
+ ### `check_label_attribution` was passing nearly everything
111
+
112
+ - **Fixed a regression that had disabled the gate in its common case.** The
113
+ comparison distance had been changed from "the minimum over every other curve"
114
+ to a KD-tree query for the *k* nearest pooled points. For a label sitting near
115
+ its own dense curve, every one of those points belongs to that curve, no other
116
+ curve is ever reached, and `d_other` stays infinite — so the gate passed every
117
+ label whose own curve was nearest, which is nearly all of them. Back to the
118
+ explicit minimum, with a regression test for a label in the corridor between
119
+ two curves.
120
+ - **It found a real defect the moment it worked.** `gallery-convergence.png` had
121
+ its direct labels past the right-hand end of each curve, 29px from their own
122
+ and 35px from a neighbour's: a label outside the data is not resolved by
123
+ proximity to anything. Moved to the left of the panel where the log-log fan is
124
+ three decades wide.
125
+
126
+ ### No hard scipy dependency
127
+
128
+ - The README promises three files and no install, and a hard `scipy` import had
129
+ quietly broken it. `scipy.ndimage.uniform_filter` is replaced by a separable
130
+ cumulative-sum box blur in numpy; `scipy.spatial.KDTree` is gone from label
131
+ attribution entirely; `check_overplotting` uses `cKDTree` when it is
132
+ importable and an O(n²) numpy path when it is not. scipy is now an optional
133
+ `fast` extra. Tested with the import forced to fail.
134
+
135
+ ### `examples/gallery.py` — six harder figures
136
+
137
+ - Small multiples on shared scales; a filled field with isolines and a colorbar;
138
+ an axis-free schematic; the three forms `choosing-a-form.md` argues for; a
139
+ log-log convergence plot with a slope triangle; a dense orbit diagram. Each is
140
+ audited, and CI fails if any figure fails.
141
+ - **Writing them found six defects in the checks themselves**, which is what
142
+ they are for:
143
+ - the readability gate reported a schematic's tick labels, which
144
+ `ax.axis("off")` means never reach the page — it now skips `_ghost_ticks`
145
+ the way `check_clipping` already did;
146
+ - `check_ink` measured colorbar axes, which are a solid ramp by construction,
147
+ so every figure with a colorbar stood at WARN for the one axes in it whose
148
+ density nobody chose;
149
+ - `check_line_weight` measured a colorbar's own 0.4pt dividers;
150
+ - `_artist_kind` called `plot(..., linestyle="none", marker="o")` a line, so a
151
+ path and its own start marker in one hue read as a wrapped color cycle;
152
+ - the clutter metric failed every heatmap annotation, as above;
153
+ - `check_label_attribution` was passing nearly everything, as above.
154
+ - **And two defects in the figures that no gate caught**, which is what step 7
155
+ of the procedure is for: the schematic's feedback loop ran off the bottom of
156
+ the canvas (`connectionstyle="bar"` drops by a *fraction* of the span, so a
157
+ wide loop went fifteen units below an axes that stops at zero), and the
158
+ convergence plot's slope triangle sat in the only corner its direct labels
159
+ could use. Both were obvious in the PNG and invisible to every check.
160
+
161
+ ### Overplotting / mark-density WARN
162
+
163
+ - **`check_overplotting` detects scatter marks that merge into a blob.** For
164
+ each scatter (`PathCollection` with offsets), estimates the fraction of points
165
+ whose nearest neighbour in display pixels is within one marker radius — points
166
+ that visually overlap. Above ~50% overlap the WARN fires: reduce counts, use
167
+ hollow markers, add transparency, or switch to hexbin. The 0.5 threshold keeps
168
+ the entire existing corpus clean (all well-separated scatters pass). A WARN,
169
+ not a FAIL — dense marks are legitimate for some forms (e.g. a swarm plot).
170
+
171
+ ### Context-surface ink stops a standing WARN
172
+
173
+ - **Ink coverage accepts `context_axes`.** A filled contourf backdrop (loss
174
+ landscape, terrain) saturated 100% of the axes pixels, triggering a standing
175
+ ink WARN on every such figure — the "advisory everyone learns to ignore." New
176
+ `audit(fig, context_axes=[ax])` declaration tells the checker the fill is a
177
+ context surface, not data-ink. The pixel buffer is split into two clusters via
178
+ 2-means on color; the larger cluster (the surface) is subtracted from the ink
179
+ count, so only marks *on top* of the backdrop are measured. A terrain panel
180
+ with a few sparse marks now PASSes instead of WARNing. Existing heatmap
181
+ behavior is unchanged (no `context_axes` → same as before).
182
+
183
+ ### Two false-signal fixes, no new gates
184
+
185
+ - **Contour dash warns on auto-dashed negative levels.** A monochrome contour
186
+ with all-negative Z ships dashed isolines via matplotlib default
187
+ (`contour.negative_linestyle`). The skill's own convention is dashing =
188
+ unobserved / projected / threshold, so this is a silent semantic error no
189
+ existing gate saw. New `check_contour_dash` warns; pass `linestyles="solid"`
190
+ on signed data. Style guide note added.
191
+ - **`check_palette.py` learns `--ink`.** Ink/neutral hexes were flagged by the
192
+ chroma-floor and lightness-band rows, but an ink token is not a series hue.
193
+ New `--ink "#52514e,#898781"` flag exempts listed hexes from those checks
194
+ while keeping them in CVD/normal separation and contrast-vs-surface coverage.
195
+ Mirrors `check_figure`'s `INK_TOKENS`.
196
+
197
+ ### The series-color and label gates learn the figure's structure
198
+
199
+ The new gates above read a flat bag of hues and text, and a bag throws away the
200
+ structure the author encoded — which panel a hue lives in, what kind of artist
201
+ drew it, whether a piece of text is a direct label or a legend key. Four figures
202
+ that were mechanically correct got failed for it. Each fix gives the harvester
203
+ back one piece of that structure.
204
+
205
+ - **Series color is scoped per panel.** The comparison, the hue count and the
206
+ adjacent-versus-all-pairs mode are now asked of one axes at a time. A
207
+ figure-wide harvest gated hues that never share a frame — a flow-chart node in
208
+ one panel measured against a regression curve in another — and let a single
209
+ scatter flip every line-only panel into the stricter all-pairs regime. The
210
+ panel is the unit a reader separates hues within.
211
+ - **One series drawn several ways is one identity.** The wrap check — one hue,
212
+ two labels — is narrowed to labels on artists of the *same kind*. A cycler that
213
+ wraps reuses one artist type; a posterior shown as a credible band, its mean
214
+ line and its observed points is three kinds in one hue, one series shown three
215
+ ways. Two lines in one hue with two labels is still the wrap it always caught.
216
+ - **Label attribution ignores legend entries.** A legend is a lookup key placed
217
+ away from the curves by design, so judging its entries by proximity to those
218
+ curves could only ever fail. They slipped the `t.axes is ax` guard because a
219
+ legend child's `.axes` is the parent axes; they are now dropped explicitly.
220
+ - **Ordinal ramps on marks belong in a colormap** (guidance). A ramp is built to
221
+ violate the categorical floor, and a `scatter(c=[rgba, …])` list reads as that
222
+ many independent hues. The style guide now says to draw it
223
+ `scatter(c=values, cmap=ListedColormap(ramp))`, which the harvester already
224
+ exempts as a value encoding — the intent lands in the code instead of a flat
225
+ bag of hues.
226
+
227
+ ### Five new gates, and the two scripts finally speak
228
+
229
+ The motivating failure, reproduced end to end: a figure drawn on matplotlib's
230
+ default `tab10` cycle with a `twinx` second y axis passed every check in
231
+ `check_figure.py` and printed `-> COMPOSED`. The same three hues through
232
+ `check_palette.py` reported `CVD separation (adjacent) worst #ff7f0e vs
233
+ #2ca02c dE 1.4 (protan)` — one hue to a protanopic reader. Two scripts in one
234
+ project, and nothing connected them.
235
+
236
+ - **`figure.mplstyle` sets `axes.prop_cycle`** to the six Okabe-Ito series slots,
237
+ in canonical order. Until now the sheet set every visual default except the one
238
+ that decides whether a colorblind reader can separate two curves, so a figure
239
+ built on it inherited `tab10`. Yellow and black stay held out — yellow at 1.32:1
240
+ vanishes as a hairline, black is the ink token.
241
+ - **Series color** reads the hues off the figure's own artists and puts them
242
+ through the palette gates, inferring adjacent-versus-all-pairs from whether the
243
+ marks are scatter. Also fails a seventh distinct hue, and one hue carrying two
244
+ labelled identities — which is what the cycler wrapping looks like, and was
245
+ prose only before. Harvest excludes colormapped artists and the sheet's ink
246
+ tokens; the lightness-band and chroma rows are deliberately not applied, because
247
+ a gray control curve is legal.
248
+ - **Dual axis** fails two axes sharing a frame when both carry data: the crossing
249
+ point of the curves is then set by the limits rather than the data. A bare unit
250
+ relabel — `secondary_yaxis` with no artists of its own — still passes.
251
+ - **Form** fails the mechanical subset of form choice: pie/donut, 3D axes, and
252
+ bars on a truncated baseline. Log-scale bars are exempt, since a log axis cannot
253
+ contain zero.
254
+ - **Identity channel** warns when two or more labelled hues carry identity with no
255
+ legend and no in-axes text. A warning rather than a gate: the script cannot tell
256
+ a direct label from any other annotation, and guessing would fire on correct work.
257
+ - **Style sheet** warns when the keys in `figure.mplstyle` are not the ones in
258
+ effect. Catches the `#`-is-a-comment trap, a forgotten `plt.style.use`, and a
259
+ later rcParams override in one row.
260
+ - **Label attribution** fails a direct label that is not plainly nearest the
261
+ curve it names. `Text collision` compares text against text, so a label alone
262
+ in the corridor between two curves clears it; text against *data* is a
263
+ different question and nothing was asking it. Measured in display space as a
264
+ ratio — the reader's judgement is comparative, not a distance in points — and
265
+ harvested only from text matching exactly one series label, because a callout
266
+ or a panel letter attributes nothing to a curve.
267
+
268
+ ### Decided: identity does not ride on label color
269
+
270
+ The usual advice is to color a direct label to match its series. Measured on
271
+ this palette, against this project's own thresholds, it cannot be done: text
272
+ needs 4.5:1 to be legible, and darkening the Okabe-Ito hues that far puts orange
273
+ at dE 18.6 from its own line and sky blue at 17.1 — both past the `NORMAL_FLOOR`
274
+ of 15 at which `check_palette` calls two colors different series. The label
275
+ would read as a fourth hue. Below 15 the text is not legible. No setting
276
+ satisfies both, so labels stay ink black and identity rides on proximity, which
277
+ `Label attribution` now gates rather than assumes.
278
+
279
+ `examples/demo.py` gains cartographic casing on its labels — a surface-colored
280
+ stroke under the glyphs — so a gridline crossing behind a label stops breaking
281
+ its edges.
282
+
283
+ ### Decided: a legend entry is not a direct label
284
+
285
+ The guide required a "visible direct label" for a sub-3:1 hue and `examples/demo.py`
286
+ used a legend, so one of the two was wrong. Settled the strict way, because the
287
+ obligation follows from the measurement: a faint mark plus a legend leaves the
288
+ reader matching a small faint swatch to a small faint curve, which is the step a
289
+ direct label removes. The demo now labels its curves directly.
290
+
291
+ ### New reference
292
+
293
+ `skill/references/choosing-a-form.md`, plus a step in `SKILL.md`'s procedure.
294
+ Grounded in statistical graphics rather than general information design —
295
+ Cleveland & McGill's ordering of the elementary perceptual tasks, and the
296
+ statistical results behind the rules that matter most in teaching material: what a
297
+ box plot hides at small n, why a cut baseline misstates every ratio, why two bars
298
+ are the wrong form for paired data, and why overlapping confidence intervals are
299
+ not a significance test.
300
+
301
+ ## Earlier in Unreleased
302
+
303
+ Corrections, no new gates. Nothing that passed before fails now.
304
+
305
+ - **Surface is white everywhere.** `check_palette.py` defaulted to `#fcfcfb`, a
306
+ surface `figure.mplstyle` never rendered, so every contrast ratio in the style
307
+ guide was measured against a page that did not exist. Reddish purple was
308
+ listed as needing a mandatory direct label at 2.98:1; against the surface
309
+ actually used it clears 3:1 at 3.06. Table recomputed, `--surface` still
310
+ available for a genuinely tinted page.
311
+ - **`tests/test_docs_match_code.py`** parses the guide's contrast table and
312
+ checks every quoted number, and every † marker, against `contrast()`. The
313
+ drift above is now a test failure rather than something noticed in a year.
314
+ - **`placed_frac` on `audit()`/`report()`.** A figure placed at
315
+ `0.48\textwidth` was measured as if it were full width and certified at twice
316
+ the type size it shipped at. Passing `placed_frac` without `CONTENT_WIDTH_PT`
317
+ raises rather than silently assuming 1.0.
318
+ - **Mark ratio sees line markers.** It read only `ax.collections`, so
319
+ `markersize=3` beside `markersize=30` passed clean. Bars stay excluded on
320
+ purpose — a bar 30× another bar is the encoding working, and the docstring now
321
+ says so instead of leaving it to look like an oversight.
322
+ - **`figure.mplstyle` carries the mark and output defaults**: `lines.linewidth`,
323
+ `lines.markersize`, `axes.linewidth`, `patch.linewidth`,
324
+ `legend.handlelength`, `errorbar.capsize`, `axes.axisbelow`, plus
325
+ `figure.dpi`, `savefig.dpi`, `savefig.bbox`, `savefig.facecolor`. These were
326
+ prose passed by hand at each call site; at matplotlib's default 100 dpi the
327
+ "render a PNG and look at it" step produced a PNG too soft to show the
328
+ defects it was opened to find.
329
+ - **`bbox_inches="tight"` documented as a trap** beside the `#`-is-a-comment
330
+ one, and pinned in the sheet. It trims to drawn content, so the saved width
331
+ stops being the authored width the type gate derives its floor from — the
332
+ gate then certifies a size the shipped file does not have.
333
+ - `axes.titlesize`/`titleweight` annotated as panel-title settings, resolving a
334
+ contradiction with the guide's ban on in-figure titles.
335
+
336
+ ## 0.1.0 — 2026-07-22
337
+
338
+ First release.
339
+
340
+ - `check_palette.py` — lightness band, chroma floor, CVD separation (protanopia
341
+ and deuteranopia, OKLab ΔE), normal-vision separation, contrast against the
342
+ surface, and a four-gate mode for ordered ramps. Standard library only.
343
+ - `check_figure.py` — clipping, text collision, alpha stacking, mark ratio, axis
344
+ redundancy, type size on the printed page, and ink coverage, read off a built
345
+ matplotlib figure's own artists.
346
+ - `figure.mplstyle` — typeface, ink, type sizes, spines, grid, frameless
347
+ legends.
348
+ - `skill/` — Agent Skill wrapper (`SKILL.md` plus the full style guide).
349
+ - 49 tests, including per-gate failure cases and a guard against the style
350
+ sheet's colors silently not applying.
@@ -0,0 +1,49 @@
1
+ # Contributing
2
+
3
+ ## The bar for a new gate
4
+
5
+ This project is a pile of elimination gates, and the failure mode it guards
6
+ against hardest is a gate that sounds sensible and never fires. So a new check
7
+ ships with three things:
8
+
9
+ 1. **A named failure.** Not "figures should have good contrast" — a real figure
10
+ that was really broken, and how. The style guide is written this way
11
+ throughout, and it's the reason the thresholds are arguable rather than
12
+ arbitrary.
13
+ 2. **A test proving it fails.** Build a figure with exactly that defect and
14
+ assert *that* gate is the one that catches it. Asserting only that `audit`
15
+ returned `False` would pass even if every other check had silently broken.
16
+ 3. **A test proving it doesn't over-fire.** Whatever legitimate case sits
17
+ nearest the line — side-by-side panels each needing their own x label, a
18
+ heatmap at 0.98 ink coverage. Every false positive spends the credibility the
19
+ gate runs on, and three of the existing checks fired on things that never
20
+ render before they were tuned.
21
+
22
+ If the right density or spacing genuinely depends on the form, make it a `WARN`
23
+ rather than a `FAIL`. A gate people learn to skip is worse than no gate.
24
+
25
+ ## Changing a threshold
26
+
27
+ Thresholds are measured, and several are quoted in the guide and pinned in
28
+ `tests/test_palette.py`. If you change one, update both — a reader who catches
29
+ the docs quoting a number the code doesn't produce has no reason to trust
30
+ anything else in them.
31
+
32
+ ## Style
33
+
34
+ Comments explain *why*, since the *what* is usually legible from the code.
35
+ Several of the longer comments exist to stop a future reader from "fixing"
36
+ something that was already tried and reverted for a measured reason; keep them.
37
+
38
+ ## Running things
39
+
40
+ ```bash
41
+ pip install -e ".[dev]"
42
+ pytest -q # the suite
43
+ python skill/scripts/check_figure.py # the checker rejects a bad figure
44
+ python examples/demo.py # end-to-end
45
+ ```
46
+
47
+ `check_palette.py` must keep importing nothing outside the standard library.
48
+ That's what makes it usable from a non-Python toolchain, and CI has a job with
49
+ no `pip install` in it to make sure the claim stays true.
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Naren Prakash
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.