asciichem 0.5.1 → 0.20.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (72) hide show
  1. checksums.yaml +4 -4
  2. data/.github/workflows/ci.yml +9 -0
  3. data/.github/workflows/release.yml +42 -0
  4. data/.gitignore +1 -0
  5. data/ARCHITECTURE.adoc +153 -0
  6. data/CHANGELOG.md +381 -6
  7. data/RELEASING.md +9 -4
  8. data/asciichem.gemspec +10 -4
  9. data/benchmarks/benchmark.rb +6 -1
  10. data/lib/asciichem/cli.rb +23 -1
  11. data/lib/asciichem/cml/conditions_extensions.rb +132 -0
  12. data/lib/asciichem/cml/extensions/atom_attributes.rb +192 -0
  13. data/lib/asciichem/cml/extensions/top_level.rb +186 -0
  14. data/lib/asciichem/cml/extensions.rb +32 -344
  15. data/lib/asciichem/cml/group_extensions.rb +10 -20
  16. data/lib/asciichem/cml/metadata_extensions.rb +145 -0
  17. data/lib/asciichem/cml/opaque_extensions.rb +144 -0
  18. data/lib/asciichem/cml/translator.rb +46 -144
  19. data/lib/asciichem/cml.rb +14 -0
  20. data/lib/asciichem/formatter/html.rb +117 -0
  21. data/lib/asciichem/formatter/latex.rb +117 -0
  22. data/lib/asciichem/formatter/mathml.rb +134 -0
  23. data/lib/asciichem/formatter/structural_svg.rb +38 -0
  24. data/lib/asciichem/formatter/svg.rb +38 -6
  25. data/lib/asciichem/formatter/text.rb +14 -10
  26. data/lib/asciichem/grammar.rb +27 -1
  27. data/lib/asciichem/identifiers/base.rb +25 -0
  28. data/lib/asciichem/identifiers/cas.rb +56 -0
  29. data/lib/asciichem/identifiers/inchi.rb +104 -0
  30. data/lib/asciichem/identifiers/inchikey.rb +33 -0
  31. data/lib/asciichem/identifiers/smiles.rb +147 -0
  32. data/lib/asciichem/identifiers.rb +66 -0
  33. data/lib/asciichem/linter/charge_balance_check.rb +117 -0
  34. data/lib/asciichem/linter/crystal_sanity_check.rb +90 -0
  35. data/lib/asciichem/linter/identifier_consistency_check.rb +100 -0
  36. data/lib/asciichem/linter/identifier_format_check.rb +48 -0
  37. data/lib/asciichem/linter/spectrum_peak_check.rb +72 -0
  38. data/lib/asciichem/linter/zmatrix_reference_check.rb +89 -0
  39. data/lib/asciichem/linter.rb +13 -2
  40. data/lib/asciichem/model/atom.rb +31 -0
  41. data/lib/asciichem/model/bond.rb +22 -0
  42. data/lib/asciichem/model/calculation.rb +4 -0
  43. data/lib/asciichem/model/crystal.rb +35 -6
  44. data/lib/asciichem/model/group.rb +30 -12
  45. data/lib/asciichem/model/mechanism.rb +3 -0
  46. data/lib/asciichem/model/molecule.rb +134 -0
  47. data/lib/asciichem/model/node.rb +16 -0
  48. data/lib/asciichem/model/opaque_cml.rb +34 -0
  49. data/lib/asciichem/model/reaction.rb +19 -4
  50. data/lib/asciichem/model/spectrum.rb +5 -0
  51. data/lib/asciichem/model.rb +1 -0
  52. data/lib/asciichem/model_adapter/from_canonical.rb +139 -24
  53. data/lib/asciichem/model_adapter/to_canonical.rb +211 -28
  54. data/lib/asciichem/parser.rb +57 -5
  55. data/lib/asciichem/periodic_table.rb +30 -1
  56. data/lib/asciichem/transform.rb +76 -47
  57. data/lib/asciichem/version.rb +1 -1
  58. data/lib/asciichem/wire/base.rb +25 -0
  59. data/lib/asciichem/wire/chemistry.rb +98 -0
  60. data/lib/asciichem/wire/core.rb +92 -0
  61. data/lib/asciichem/wire/extended.rb +136 -0
  62. data/lib/asciichem/wire/identity.rb +63 -0
  63. data/lib/asciichem/wire.rb +52 -0
  64. data/lib/asciichem/wire_adapter.rb +361 -0
  65. data/lib/asciichem.rb +10 -0
  66. metadata +70 -13
  67. data/TODO.beyond-formulas/01-crystallography.md +0 -69
  68. data/TODO.beyond-formulas/02-spectroscopy.md +0 -39
  69. data/TODO.beyond-formulas/03-compchem.md +0 -31
  70. data/TODO.beyond-formulas/04-structural.md +0 -48
  71. data/TODO.beyond-formulas/05-mechanisms.md +0 -40
  72. data/TODO.beyond-formulas/README.md +0 -69
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 1edf476bb3d979641172de761d1b2ac906cb4eaf8565e56b0e083e146c09eb1d
4
- data.tar.gz: b3394a0709a475df222d6369eac92a1cc6d02fc756a7d3a45ca038c5d6f5155c
3
+ metadata.gz: 6a77da515ac52e848a4ff0f5855a595cb81d71a36c18b4991d875d682d2c3598
4
+ data.tar.gz: 70c14be9ddec1e767ddddef09a1ad91a02f996f1fdd22645d0269c4de7e96020
5
5
  SHA512:
6
- metadata.gz: 5a1ce9a990e507a6f70c91939841766e228b02ae674af39c5fa356b4911f20d0ef3b5aa2f2ec4799663d21da54752ed9b68c6660db23e1b2da9cacd4cc50b2ad
7
- data.tar.gz: 6d68778aca7042fce958026033e990e51b87290c24ed3028cf224aa34bd5e03418ef6209b72072d06a14ab3352e2d20290c939b4002a82fdcdbe5c267a17d087
6
+ metadata.gz: b71e29edd758d5d4393d750a95676dc8007c5850f830b414ca83a2d21e99c1ff39fa88f5f66b54198e442e7d29b9010639c4a55138567cd9dee8160d5b2be3dc
7
+ data.tar.gz: f08f835457ce6d58b70ff0b3bc502f84f4404684a13ae60ad52bbae44a790dfd66621c15bba73ed2843e0d76ba4ef47545a37e838867b54d12eb9246f11277b6
@@ -14,8 +14,17 @@ jobs:
14
14
  ruby: ["3.3", "3.4"]
15
15
  steps:
16
16
  - uses: actions/checkout@v4
17
+ - name: Clone conformance corpus (asciichem-tests)
18
+ run: git clone --depth 1 https://github.com/asciichem/asciichem-tests.git ../asciichem-tests
17
19
  - uses: ruby/setup-ruby@v1
18
20
  with:
19
21
  ruby-version: ${{ matrix.ruby }}
20
22
  bundler-cache: true
21
23
  - run: bundle exec rspec
24
+ - name: Publish conformance report
25
+ if: matrix.ruby == '3.4'
26
+ uses: actions/upload-artifact@v4
27
+ with:
28
+ name: conformance
29
+ path: conformance.json
30
+ if-no-files-found: warn
@@ -0,0 +1,42 @@
1
+ name: Release
2
+
3
+ on:
4
+ workflow_dispatch:
5
+ inputs:
6
+ version:
7
+ description: "Version to release (must match lib/asciichem/version.rb)"
8
+ required: true
9
+ type: string
10
+
11
+ jobs:
12
+ release:
13
+ runs-on: ubuntu-latest
14
+ # Trusted publishing (OIDC): the publisher registered on RubyGems.org
15
+ # is repository asciichem/asciichem-ruby + workflow release.yml, with
16
+ # no environment — so this job must not claim one.
17
+ permissions:
18
+ contents: read
19
+ id-token: write
20
+ steps:
21
+ - uses: actions/checkout@v4
22
+ with:
23
+ ref: main
24
+ persist-credentials: false
25
+ - name: Verify version matches input
26
+ run: |
27
+ actual=$(ruby -e 'require "./lib/asciichem/version"; print AsciiChem::VERSION')
28
+ if [ "$actual" != "${{ inputs.version }}" ]; then
29
+ echo "Version mismatch: input=${{ inputs.version }}, lib/asciichem/version.rb=$actual"
30
+ exit 1
31
+ fi
32
+ echo "Releasing $actual"
33
+ - uses: ruby/setup-ruby@v1
34
+ with:
35
+ ruby-version: "3.4"
36
+ bundler-cache: true
37
+ # Builds and pushes using the GitHub OIDC identity — no API keys.
38
+ - uses: rubygems/release-gem@v1
39
+ - name: Summary
40
+ run: |
41
+ echo "Released asciichem ${{ inputs.version }} to RubyGems"
42
+ echo "https://rubygems.org/gems/asciichem"
data/.gitignore CHANGED
@@ -12,3 +12,4 @@
12
12
  /.vscode/
13
13
  /.idea/
14
14
  *.gem
15
+ conformance.json
data/ARCHITECTURE.adoc CHANGED
@@ -237,3 +237,156 @@ Plurimath handles it.
237
237
  * No hand-rolled model serialisation. The model is in-memory only;
238
238
  rendering to MathML / HTML / etc. is one-way output via Nokogiri or
239
239
  string building, not `(de)serialisation`.
240
+
241
+
242
+ == Native CML wire (v0.9-v0.11)
243
+
244
+ All AsciiChem model classes have native CML wire representations
245
+ (chemicalml 0.3.0+). The mapping is one-way in production: the
246
+ adapter produces native wire objects; the CML Translator enriches
247
+ with aci: extensions only for backwards-compat fallback.
248
+
249
+ [cols="1,1,2", options="header"]
250
+ |===
251
+ | Model class | Wire container | Native child elements
252
+ | Crystal | | with cells, , (fractional coords)
253
+ | Spectrum | | with of (xValue/yValue/yMultiplicity/title)
254
+ | ZMatrix | | (text content; structural length/angle/torsion children are future work)
255
+ | Calculation | | of with (value, units)
256
+ | Mechanism | | (text via title; structural step list is future work)
257
+ | Reaction conditions | | with (above/below)
258
+ |===
259
+
260
+ == Extension channels (six parallel modules)
261
+
262
+ Each channel is a MECE module under :
263
+
264
+ * — per-atom aci: attributes (FIELDS registry)
265
+ * — top-level constructs via aci: elements (HANDLERS registry)
266
+ * — inside molecules (BRACKETS registry on Group)
267
+ * — preserves unknown top-level elements verbatim
268
+ * — per-molecule key/value metadata (aci:meta-*)
269
+ * — now mostly legacy; native <conditionList> preferred
270
+
271
+ The is a thin orchestrator (~80 lines) that calls each
272
+ module's // in pipeline order.
273
+
274
+ == Model-owned wire codes
275
+
276
+ Wire-format mappings live on the model classes as the single source
277
+ of truth:
278
+
279
+ * / /
280
+ * /
281
+ * /
282
+ * / / /
283
+ * (shared by adapter and extension modules)
284
+
285
+ Adding a new bond kind, bracket kind, arrow kind, or cell parameter
286
+ is one entry on the model; both adapter directions pick it up.
287
+
288
+ == Linter framework
289
+
290
+ Ten registered checks (auto-load via ):
291
+
292
+ * — stoichiometric atom conservation
293
+ * — charge conservation (v0.7+)
294
+ * — group bracket consistency
295
+ * — cell lengths/angles/fractional coords (v0.6+)
296
+ * — forward references and geometry (v0.6+)
297
+ * — peak data validation (v0.6+)
298
+ * — periodic-table membership
299
+ * — isotope mass >= atomic number
300
+ * — unmatched ring-closure digits
301
+ * — bond order + charge <= max valence
302
+
303
+ sorts by severity then deduplicates.
304
+
305
+ == Convenience methods on Molecule
306
+
307
+ * — total atoms (recurses through groups/nested molecules)
308
+ * — Hill-system canonical formula (C first, H second, alpha)
309
+ * — sum of atomic_mass × count (v0.13+)
310
+
311
+ == Native CML wire (v0.9-v0.11)
312
+
313
+ All AsciiChem model classes have native CML wire representations
314
+ (chemicalml 0.3.0+). The adapter produces native wire objects;
315
+ the CML Translator enriches with aci: extensions only as a
316
+ backwards-compat fallback.
317
+
318
+ |===
319
+ | Model class | Wire container | Native child elements
320
+
321
+ | Crystal
322
+ | `<molecule>`
323
+ | `<crystal>` with `<scalar>` cells, `<symmetry>`, `<atomArray>` (fractional coords)
324
+
325
+ | Spectrum
326
+ | `<molecule>`
327
+ | `<spectrum>` with `<peakList>` of `<peak>` (xValue/yValue/yMultiplicity/title)
328
+
329
+ | ZMatrix
330
+ | `<molecule>`
331
+ | `<zMatrix>` (text content; structural length/Angle/Torsion children are future work)
332
+
333
+ | Calculation
334
+ | `<molecule>`
335
+ | `<propertyList>` of `<property>` with `<scalar>` (value, units)
336
+
337
+ | Mechanism
338
+ | `<reaction>`
339
+ | `<mechanism>` (text via title; structural step list is future work)
340
+
341
+ | Reaction conditions
342
+ | `<reaction>`
343
+ | `<conditionList>` with `<scalar>` (above/below)
344
+ |===
345
+
346
+ == Six extension channels (MECE modules under Cml)
347
+
348
+ * `Extensions::AtomAttributes` — per-atom aci: attributes (FIELDS registry)
349
+ * `Extensions::TopLevel` — top-level constructs via aci: elements (HANDLERS registry)
350
+ * `GroupExtensions` — `<aci:group>` inside molecules
351
+ * `OpaqueExtensions` — preserves unknown top-level elements verbatim
352
+ * `MetadataExtensions` — per-molecule key/value metadata (aci:meta-*)
353
+ * `ConditionsExtensions` — legacy; native <conditionList> preferred (v0.11+)
354
+
355
+ The `Translator` is a thin orchestrator (~80 lines) that calls each
356
+ module's inject/extract/restore in pipeline order.
357
+
358
+ == Model-owned wire codes (single source of truth)
359
+
360
+ Wire-format mappings live on the model classes:
361
+
362
+ * `Bond::CML_ORDER_CODES` / `KIND_BY_CML_ORDER` / `CML_STEREO_CODES`
363
+ * `Group::BRACKETS` / `BRACKET_BY_WIRE`
364
+ * `Reaction::ARROWS` / `ARROW_BY_WIRE`
365
+ * `Crystal::CELL_PARAMS` / `LENGTH_FIELDS` / `ANGLE_FIELDS` / `CELL_LABELS`
366
+ * `Cml::ID_PREFIXES` (shared by adapter and extension modules)
367
+
368
+ Adding a new bond kind, bracket kind, arrow kind, or cell parameter
369
+ is one entry on the model; both adapter directions pick it up.
370
+
371
+ == Linter framework
372
+
373
+ Ten registered checks (auto-load via `Linter::Base.register`):
374
+
375
+ * `BalanceCheck` — stoichiometric atom conservation
376
+ * `ChargeBalanceCheck` — charge conservation (v0.7+)
377
+ * `BracketBalanceCheck` — group bracket consistency
378
+ * `CrystalSanityCheck` — cell lengths/angles/fractional coords (v0.6+)
379
+ * `ZMatrixReferenceCheck` — forward references and geometry (v0.6+)
380
+ * `SpectrumPeakCheck` — peak data validation (v0.6+)
381
+ * `ElementValidationCheck` — periodic-table membership
382
+ * `IsotopeSanityCheck` — isotope mass >= atomic number
383
+ * `UnclosedRingCheck` — unmatched ring-closure digits
384
+ * `ValenceCheck` — bond order + charge <= max valence
385
+
386
+ `Linter.run` sorts by severity (errors first) then deduplicates.
387
+
388
+ == Convenience methods on Molecule
389
+
390
+ * `#atom_count` — total atoms (recurses through groups/nested molecules)
391
+ * `#hill_formula` — Hill-system canonical formula (C first, H second, alpha)
392
+ * `#formula_weight` — sum of atomic_mass * count (v0.13+)
data/CHANGELOG.md CHANGED
@@ -3,15 +3,390 @@
3
3
  All notable changes to AsciiChem are documented here.
4
4
  This project follows [Semantic Versioning](https://semver.org/).
5
5
 
6
- ## [Unreleased]
6
+ ## [0.20.0] - 2026-09-12
7
+
8
+ ### Added
9
+ - Canonical JSON wire form (`to_model_json` / `AsciiChem.from_model_json`)
10
+ per asciichem-model v1: `AsciiChem::Wire` (lutaml-model Serializable
11
+ classes, json mappings only - no hand-rolled serialization) bridged by
12
+ `AsciiChem::WireAdapter` (model-to-model conversion, same pattern as
13
+ the CML ModelAdapter). Emission covers every node type; ingestion
14
+ covers the lossless core set (beyond-formulas nodes are emission-only
15
+ until their corpus round-trip acceptance lands).
16
+ - Conformance runner over the shared corpus (asciichem-tests): L0
17
+ emission + schema validation, L1 Text round-trip, L3 CML round-trip,
18
+ L4 linter diagnostics, plus the ParseError contract for rejects.
19
+ Emits conformance.json; CI clones the corpus and publishes the report
20
+ as an artifact. Current claim: L0 149/149, L1 16/16, L3 23/23, L4 6/6.
21
+
22
+ ## [0.19.0] - 2026-09-09
23
+
24
+ ### Added
25
+ - `AsciiChem::Identifiers` — offline format validators for chemical
26
+ identifier conventions, self-registering like linter checks
27
+ (`cas` with check digit, `inchi` with formula-layer analysis,
28
+ `inchikey` 14-10-1 format, `smiles` structural sanity). Purely
29
+ lexical; no network, no assignment checking.
30
+ - `IdentifierFormatCheck` (linter) — validates `@cas`/`@inchi`/
31
+ `@inchikey`/`@smiles` annotation values against the registry;
32
+ unknown conventions are skipped.
33
+ - `IdentifierConsistencyCheck` (linter) — cross-checks identifier
34
+ values against the molecule they annotate: InChI formula-layer
35
+ element counts vs molecule composition (error), SMILES element set
36
+ vs molecule elements with implicit hydrogens tolerated (warning).
37
+ - `Model::Molecule#element_counts(with_coefficient:)` — public
38
+ composition API; `#hill_formula` and `#formula_weight` now share it.
39
+
40
+ ### Fixed
41
+ - RSpec suite aborted non-deterministically mid-run (randomly
42
+ truncated example counts, e.g. 56–748 of 788): CLI commands call
43
+ `Kernel#exit` even on success, and the cli specs' `run` helper did
44
+ not trap `SystemExit`, so under random ordering an uncaught exit
45
+ aborted the suite while reporting "0 failures". The helper now
46
+ traps it; five consecutive full runs execute all 788 examples.
47
+ - Linter registry re-registration in specs now globs all check files
48
+ instead of a hardcoded list (new checks no longer vanish from the
49
+ suite after `Registry.reset`).
50
+
51
+ ## [0.18.1] - 2026-07-21
52
+
53
+ ### Changed
54
+ - Updated gemspec summary and description to mention all
55
+ beyond-formulas constructs (crystals, spectra, calculations,
56
+ Z-matrices, reaction mechanisms) and the native CML round-trip
57
+ via chemicalml.
58
+ - Added `docs_uri` metadata pointing to the AsciiChem documentation
59
+ site.
60
+
61
+ ## [0.18.0] - 2026-07-21
62
+
63
+ ### Changed
64
+ - Beyond-formulas body validation: MechanismBuilder, SpectrumBuilder,
65
+ and CalculationBuilder now validate each body line at parse time.
66
+ Malformed entries (missing `:` separator) raise `ParseError` with
67
+ the offending line number and content, instead of being silently
68
+ dropped.
69
+
70
+ ## [0.17.0] - 2026-07-21
71
+
72
+ ### Added
73
+ - `Atom::Point3` value object (Struct with `x`, `y`, `z`, `to_a`,
74
+ `magnitude`).
75
+ - `Atom#cartesian` and `Atom#fractional` accessors return Point3
76
+ bundling the flat coordinate fields. Backwards compatible: flat
77
+ accessors (`x2`/`y2`/`z2`, `x_fract`/`y_fract`/`z_fract`) remain.
78
+
79
+ ## [0.16.0] - 2026-07-21
80
+
81
+ ### Added
82
+ - `Node#to_structural_svg` shortcut — invokes
83
+ `Formatter::StructuralSvg` for 2D diagram output. For Crystal
84
+ nodes, projects fractional coordinates onto the ab-plane with a
85
+ unit cell outline; for molecules with bonds, uses elkrb for
86
+ graph layout (existing behaviour); falls back to linear Svg
87
+ otherwise.
88
+ - `Formatter::StructuralSvg#visit_crystal` — renders a Crystal as
89
+ a 2D projection of fractional coordinates with unit cell outline.
90
+
91
+ ## [0.15.0] - 2026-07-21
92
+
93
+ ### Changed
94
+ - Parser error messages now report `line N, col M` for multi-line
95
+ inputs instead of `char N`. The caret pointer is positioned on
96
+ the relevant line. Single-line inputs continue to use the
97
+ existing char-position format.
98
+
99
+ ## [0.14.0] - 2026-07-21
100
+
101
+ ### Added
102
+ - `lint -f json` CLI option — emits diagnostics as a JSON array for
103
+ tooling integration (CI, editors, downstream scripts). Each
104
+ entry has `severity`, `message`, and `node` fields.
105
+
106
+ ## [0.13.0] - 2026-07-21
107
+
108
+ ### Added
109
+ - `Molecule#formula_weight` — sum of `atomic_mass × count` across
110
+ all atoms (recurses through groups and nested molecules). Returns
111
+ nil if any element lacks atomic mass data. Examples:
112
+ `parse("H_2O").nodes.first.formula_weight` returns `18.015`;
113
+ `parse("C_6H_12O_6")` returns `180.156`.
114
+ - `PeriodicTable.atomic_mass(symbol)` — IUPAC 2021 standard atomic
115
+ weights for ~50 common elements. Single source of truth for
116
+ atomic mass data.
117
+
118
+ ### Changed
119
+ - `PeriodicTable::Element` struct gains `atomic_mass` field (nil
120
+ when unpopulated).
121
+
122
+ ## [0.12.0] - 2026-07-21
123
+
124
+ ### Added
125
+ - `Molecule#hill_formula` — Hill-system canonical formula (C first,
126
+ then H, then others alphabetically). Useful for indexing and
127
+ display. `parse("C_2H_6O").nodes.first.hill_formula` returns
128
+ `"C2H6O"`; `parse("H_2SO_4")` returns `"H2O4S"`.
129
+
130
+ ### Changed
131
+ - `Linter.run` sorts diagnostics by severity (errors first) then by
132
+ message for stable UX output. Also deduplicates identical
133
+ (message, node) pairs from different checks.
134
+ - `AsciiChem::Parser` caches `Grammar` and `Transform` instances at
135
+ the class level instead of allocating per parse. Benchmark: ~15%
136
+ throughput improvement on repeated parses.
137
+
138
+ ## [0.11.0] - 2026-07-21
139
+
140
+ ### Added
141
+ - **Native CML wire for ZMatrix, Calculation, Mechanism, and
142
+ reaction conditions.** Completes the native-wire migration arc
143
+ for all beyond-formulas constructs:
144
+ - ZMatrix emits as `<molecule>` with `<zMatrix>` child carrying
145
+ the text form (structural length/angle/torsion children are a
146
+ future enhancement).
147
+ - Calculation emits as `<molecule>` with `<propertyList>` of
148
+ `<property>` children, each with a `<scalar>` carrying value
149
+ and units.
150
+ - Mechanism emits as `<reaction>` with `<mechanism>` child.
151
+ Currently uses the text form via title attribute; structural
152
+ step-by-step representation is a future enhancement.
153
+ - Reaction conditions (`<=>[Fe][400C]`) now emit as native
154
+ `<conditionList>` with `<scalar>` children for above/below,
155
+ replacing the `aci:conditionsAbove`/`aci:conditionsBelow`
156
+ attributes used in v0.10.0 and earlier.
157
+
158
+ ### Changed
159
+ - `ConditionsExtensions.inject`: now a no-op when native
160
+ `<conditionList>` is present (defensive fallback only).
161
+ - `ModelAdapter::FromCanonical`: reads native `<conditionList>`
162
+ via `conditions_from_canonical`.
163
+
164
+ ## [0.10.0] - 2026-07-21
165
+
166
+ ### Added
167
+ - **Native CML wire for Spectrum.** Spectrum nodes now emit as a
168
+ `<molecule>` containing a native `<spectrum>` child (with `format`
169
+ and `condition` attributes) plus a `<peakList>` of `<peak>`
170
+ elements. Each peak carries `xValue`, `yValue`, `yMultiplicity`,
171
+ and `title` (assignment) attributes per the CML Schema 3 spec.
172
+
173
+ ### Changed
174
+ - `ModelAdapter::ToCanonical`: new `spectrum_to_canonical` mapper
175
+ wraps Spectrum in a Molecule wire object with spectrum child.
176
+ - `ModelAdapter::FromCanonical`: detects molecule-with-spectrum and
177
+ rebuilds as Spectrum node.
178
+ - `Translator::NATIVELY_WIRED`: now includes Spectrum.
179
+
180
+ ## [0.9.0] - 2026-07-21
181
+
182
+ ### Added
183
+ - **Native CML wire for Crystal.** Crystal nodes now emit as a
184
+ `<molecule>` containing a native `<crystal>` child (with `<scalar>`
185
+ cells and `<symmetry spaceGroup=...>`) plus an `<atomArray>` with
186
+ fractional coordinates. Other CML tools can now read AsciiChem
187
+ crystal output directly without needing the aci: extension
188
+ namespace. Round-trip preserves all fields. The legacy aci:
189
+ text-carrier form is still accepted on parse for backwards
190
+ compatibility.
191
+
192
+ ### Changed
193
+ - `ModelAdapter::ToCanonical`: new `crystal_to_canonical` mapper
194
+ wraps Crystal in a Molecule wire object.
195
+ - `ModelAdapter::FromCanonical`: detects molecule-with-crystal and
196
+ rebuilds as Crystal node.
197
+ - `Extensions::TopLevel.collect`: accepts `skip_classes:` parameter
198
+ to suppress the aci: text carrier for constructs with native wire.
199
+ - `Translator::NATIVELY_WIRED`: registry of construct classes that
200
+ have been migrated to native wire (currently just Crystal).
201
+
202
+ ## [0.8.0] - 2026-07-21
203
+
204
+ ### Changed
205
+ - `chemicalml` dependency bumped from `~> 0.2.1` to `~> 0.3.0`.
206
+ chemicalml 0.3.0 fixes the wire serialization gaps that blocked
207
+ Phases 06-10, 15 (native CML wire for Crystal/Spectrum/ZMatrix/
208
+ Mechanism/Calculation/conditions). Molecule wire now serializes
209
+ `<crystal>`, `<spectrum>`, `<zMatrix>`, `<propertyList>` children;
210
+ Reaction wire now serializes `<conditionList>`, `<spectatorList>`,
211
+ `<mechanism>` children.
212
+
213
+ ### Added
214
+ - `.github/workflows/release.yml` — manual-dispatch release workflow
215
+ that pushes the gem to RubyGems. Triggered via `gh workflow run
216
+ release.yml -f version=X.Y.Z` after the version-bump PR merges.
217
+ Verifies the input version matches `lib/asciichem/version.rb`.
218
+
219
+ ## [0.7.0] - 2026-07-21
220
+
221
+ ### Added
222
+ - Implicit subscripts on Hydrogen: `H2` now parses as `H_2`, so
223
+ users can write `H2O`, `CH4`, `NH3` without explicit underscores.
224
+ Resolves the long-pending feature request without breaking
225
+ SMILES-style ring closures on other elements (Hydrogen cannot
226
+ form ring closures — only 1 bond — so bare digits after H are
227
+ unambiguously subscripts).
228
+
229
+ ### Changed
230
+ - Grammar: new `hydrogen_atom` rule placed before `plain_atom` in
231
+ the unit dispatch. The `match('[a-z]').absent?` lookahead ensures
232
+ `He`, `Ho`, etc. fall through to plain_atom unchanged.
233
+
234
+ ## [0.6.0] - 2026-07-20
235
+
236
+ ### Added
237
+ - `Model::OpaqueCml` node + `Cml::OpaqueExtensions` module: preserve
238
+ unknown CML elements through round-trip as opaque blobs.
239
+ - `Linter::ChargeBalanceCheck`: validates charge conservation in
240
+ reactions (parallel to BalanceCheck's atom conservation).
241
+ - `Linter::CrystalSanityCheck`, `ZMatrixReferenceCheck`,
242
+ `SpectrumPeakCheck`: validate cell parameters, ZMatrix references,
243
+ and spectrum peak data.
244
+ - `Molecule#atom_count`: total atom count recursing through groups
245
+ and nested molecules with subscripts and multiplicities applied.
246
+ - `Model::Molecule::Property`, `Meta`, `Label`, `Formula` Structs
247
+ replace Hash-with-magic-keys fields.
248
+ - `Model::Spectrum::Peak`, `Calculation::Property`, `Mechanism::Step`
249
+ Structs replace Hash-with-magic-keys fields.
250
+ - Formatter visit methods for `Crystal`, `Spectrum`, `Calculation`,
251
+ `ZMatrix`, `Mechanism`, `OpaqueCml` across MathML, HTML, LaTeX, SVG.
252
+ - CLI smoke coverage for beyond-formulas constructs.
253
+ - Fuzz corpus files 16-20 (crystal, spectrum, calculation, zmatrix,
254
+ mechanism).
255
+ - Benchmark cases for beyond-formulas constructs.
256
+
257
+ ### Changed
258
+ - `Cml::Extensions` split into `Extensions::AtomAttributes` and
259
+ `Extensions::TopLevel` sub-modules; facade preserves public API.
260
+ - `Cml::MetadataExtensions` and `Cml::ConditionsExtensions` extracted
261
+ from inline Translator code as parallel extension channels.
262
+ - `Cml::ID_PREFIXES` is the single source of truth for canonical ID
263
+ prefixes (atom/bond/molecule/reaction/group).
264
+ - `Extensions.ensure_namespace(root)` shared helper replaces six
265
+ duplicated `namespace_declared?` / `ensure_namespace` methods.
266
+ - `Model::Bond` owns `CML_ORDER_CODES`, `KIND_BY_CML_ORDER`,
267
+ `CML_STEREO_CODES` (was duplicated across adapter directions).
268
+ - `Model::Group` owns `BRACKETS` with `{open, close, wire}` attrs.
269
+ - `Model::Reaction` owns `ARROW_BY_WIRE` inverse map.
270
+ - `Model::Crystal` owns `CELL_PARAMS`, `LENGTH_FIELDS`, `ANGLE_FIELDS`,
271
+ `CELL_LABELS`, `#each_cell_param(format)` as single source for cell
272
+ parameter labels across formatters.
273
+ - `Extensions::TopLevel::Handler.text_round_trip` and
274
+ `.source_with_wrapper` factory methods eliminate duplicate lambdas
275
+ across 8 handlers.
276
+ - `Transform::BuilderHelpers` shared module included by all 5 builder
277
+ classes.
278
+ - `Linter::Base.register(:name)` pattern — adding a check is one new
279
+ file + one autoload entry.
280
+
281
+ ### Fixed
282
+ - Empty body braces (`crystal[x]{}`, `spectrum[nmr](){}`, etc.) now
283
+ parse correctly for all 5 beyond-formulas constructs. Root cause:
284
+ parslet represents zero-match `.repeat` as `[]` not `""`.
285
+ - Cascade step conditions (`A ->[c1] B ->[c2] C`) now survive CML
286
+ round-trip. Previously the inline translator code only walked
287
+ top-level reactions.
288
+
289
+ ## [0.5.1] - 2026-07-16
290
+
291
+ ### Added
292
+ - CML round-trip for all five beyond-formulas constructs via aci:
293
+ namespace extension elements.
294
+
295
+ ## [0.5.0] - 2026-07-16
296
+
297
+ ### Added
298
+ - Phase 5: reaction mechanism syntax (`mechanism{step1: A -> B;
299
+ spectator: Na+}`).
300
+
301
+ ## [0.4.1] - 2026-07-16
302
+
303
+ ### Added
304
+ - Phase 3: computational chemistry results (`calc(method/basis){...}`).
305
+ - Phase 4: Z-Matrix internal coordinates (`zmatrix{C1; H2 C1 1.09; ...}`).
306
+
307
+ ## [0.4.0] - 2026-07-16
308
+
309
+ ### Added
310
+ - Phase 1: crystallography syntax (`crystal[NaCl](a=...,sg=...){...}`).
311
+ - Phase 2: spectroscopy syntax (`spectrum[nmr](type=1H,...){...}`).
312
+
313
+ ## [0.3.4] - 2026-07-16
314
+
315
+ ### Added
316
+ - Spin multiplicity (`C@m(2)`), atom title (`C@t("alpha")`), and
317
+ fractional coordinates (`C@f(0.5,0.5,0.5)`) syntax.
318
+
319
+ ## [0.3.3] - 2026-07-16
320
+
321
+ ### Fixed
322
+ - Property and metadata CML round-trip for chemicalml 0.2.1.
323
+
324
+ ## [0.3.2] - 2026-07-16
325
+
326
+ ### Added
327
+ - Reaction conditions CML round-trip via aci: attributes.
328
+
329
+ ## [0.3.1] - 2026-07-16
330
+
331
+ ### Changed
332
+ - Adapted to chemicalml 0.2.1 (wire classes ARE the model).
333
+
334
+ ## [0.3.0] - 2026-07-15
335
+
336
+ ### Added
337
+ - Full CML support via chemicalml gem.
338
+ - aci: extension namespace for fields CML doesn't natively carry.
339
+ - New syntax: spin multiplicity, atom titles, fractional coordinates,
340
+ molecule annotations (`@name`, `@inchi`, `@meta`, `@formula`,
341
+ `@label`, `@mw`, `@title`).
342
+ - Group preservation through CML round-trip via `<aci:group>`.
343
+ - ElectronConfiguration and EmbeddedMath top-level constructs via aci:.
344
+ - `parse-cml` CLI subcommand.
345
+
346
+ ## [0.2.0] - 2026-07-14
347
+
348
+ ### Added
349
+ - Layout module (MoleculeWalker / GraphBuilder / ResultExtractor)
350
+ using elkrb for 2D structural SVG.
351
+ - StructuralSvg formatter with bond-kind renderer registry.
352
+ - Ring closures (SMILES-style): `C1-C-C-C-C-C1` is cyclohexane.
353
+
354
+ ## [0.1.0] - 2026-07-10
7
355
 
8
356
  ### Added
9
357
  - Initial gem scaffold: autoload tree, version, errors.
10
- - Core model: `Formula`, `Atom`, `Molecule`, `Group`, `Bond`, `Reaction`,
11
- `ElectronConfiguration`, `EmbeddedMath`, `Text`.
358
+ - Core model: `Formula`, `Atom`, `Molecule`, `Group`, `Bond`,
359
+ `Reaction`, `ElectronConfiguration`, `EmbeddedMath`, `Text`.
12
360
  - Parslet parser and transform for v1 constructs.
13
- - Formatters: MathML and Text (round-trip).
14
- - Thor-based CLI: `convert`, `roundtrip`, `version`.
361
+ - Formatters: MathML, Text (round-trip), HTML, LaTeX, SVG.
362
+ - Thor-based CLI: `convert`, `roundtrip`, `lint`, `parse-cml`,
363
+ `version`.
15
364
  - Comprehensive RSpec suite with round-trip conformance.
16
365
 
17
- [Unreleased]: https://github.com/asciichem/asciichem-ruby/commits/main
366
+ [Unreleased]: https://github.com/asciichem/asciichem-ruby/compare/v0.20.0...HEAD
367
+ [0.20.0]: https://github.com/asciichem/asciichem-ruby/compare/v0.19.0...v0.20.0
368
+ [0.18.1]: https://github.com/asciichem/asciichem-ruby/compare/v0.18.0...v0.18.1
369
+ [0.18.0]: https://github.com/asciichem/asciichem-ruby/compare/v0.17.0...v0.18.0
370
+ [0.17.0]: https://github.com/asciichem/asciichem-ruby/compare/v0.16.0...v0.17.0
371
+ [0.16.0]: https://github.com/asciichem/asciichem-ruby/compare/v0.15.0...v0.16.0
372
+ [0.15.0]: https://github.com/asciichem/asciichem-ruby/compare/v0.14.0...v0.15.0
373
+ [0.14.0]: https://github.com/asciichem/asciichem-ruby/compare/v0.13.0...v0.14.0
374
+ [0.13.0]: https://github.com/asciichem/asciichem-ruby/compare/v0.12.0...v0.13.0
375
+ [0.12.0]: https://github.com/asciichem/asciichem-ruby/compare/v0.11.0...v0.12.0
376
+ [0.11.0]: https://github.com/asciichem/asciichem-ruby/compare/v0.10.0...v0.11.0
377
+ [0.10.0]: https://github.com/asciichem/asciichem-ruby/compare/v0.9.0...v0.10.0
378
+ [0.9.0]: https://github.com/asciichem/asciichem-ruby/compare/v0.8.0...v0.9.0
379
+ [0.8.0]: https://github.com/asciichem/asciichem-ruby/compare/v0.7.0...v0.8.0
380
+ [0.7.0]: https://github.com/asciichem/asciichem-ruby/compare/v0.6.0...v0.7.0
381
+ [0.6.0]: https://github.com/asciichem/asciichem-ruby/compare/v0.5.1...v0.6.0
382
+ [0.5.1]: https://github.com/asciichem/asciichem-ruby/compare/v0.5.0...v0.5.1
383
+ [0.5.0]: https://github.com/asciichem/asciichem-ruby/compare/v0.4.1...v0.5.0
384
+ [0.4.1]: https://github.com/asciichem/asciichem-ruby/compare/v0.4.0...v0.4.1
385
+ [0.4.0]: https://github.com/asciichem/asciichem-ruby/compare/v0.3.4...v0.4.0
386
+ [0.3.4]: https://github.com/asciichem/asciichem-ruby/compare/v0.3.3...v0.3.4
387
+ [0.3.3]: https://github.com/asciichem/asciichem-ruby/compare/v0.3.2...v0.3.3
388
+ [0.3.2]: https://github.com/asciichem/asciichem-ruby/compare/v0.3.1...v0.3.2
389
+ [0.3.1]: https://github.com/asciichem/asciichem-ruby/compare/v0.3.0...v0.3.1
390
+ [0.3.0]: https://github.com/asciichem/asciichem-ruby/compare/v0.2.0...v0.3.0
391
+ [0.2.0]: https://github.com/asciichem/asciichem-ruby/compare/v0.1.0...v0.2.0
392
+ [0.1.0]: https://github.com/asciichem/asciichem-ruby/releases/tag/v0.1.0
data/RELEASING.md CHANGED
@@ -83,10 +83,15 @@ tag.
83
83
 
84
84
  == Publish to RubyGems
85
85
 
86
- [source,sh]
87
- ----
88
- gem push pkg/asciichem-0.X.Y.gem
89
- ----
86
+ The Release workflow (`.github/workflows/release.yml`) publishes via
87
+ https://guides.rubygems.org/trusted-publishing/[RubyGems trusted publishing]
88
+ (OIDC) — there are no API keys. Its trusted publisher is registered on
89
+ https://rubygems.org (repository `asciichem/asciichem-ruby`, workflow
90
+ `release.yml`, no environment).
91
+
92
+ On the repository's Actions tab, run the "Release" workflow with the
93
+ version number. It verifies the version matches `version.rb`, builds
94
+ the gem, and pushes it using the workflow's GitHub OIDC identity.
90
95
 
91
96
  Verify at https://rubygems.org/gems/asciichem.
92
97