asciichem 0.31.5 → 0.31.6

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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 6c9a131b0a052014c60f5be8b74972de24db882f7db088b82bdea1c506709a46
4
- data.tar.gz: 9b280851bdc1fb8c4f4681f1887796f2eb688298f8c495cd81a766a879fd8a09
3
+ metadata.gz: ac2c54a74f212244c74efeb88156c34398cdfe87751de14a597833533f4ccb92
4
+ data.tar.gz: 8f5614c2703ae6ee1440be65fa2c7654bc3cc12d2c8b1207f43a974ab804e3f2
5
5
  SHA512:
6
- metadata.gz: d5580a3f5f314447b3f1f932b37396bb5741bd5363d96e4ae97cc95fcbcf7cfbcd0690ae16253c532d21b2c790093fda0652cd3d1a330f3bcbce9e1801c66e66
7
- data.tar.gz: 44a0d45dfca800429e648a5a3c3adab685c5813c7c23d5be68afb55c7dc4cf34410b8cb16ff43681a439d9ec258c7e4f71aa141e7d3561a6a05c0ccc2cbc3f5b
6
+ metadata.gz: 487d422ca183453e7e9a82de50aaa1c6e671383047838ca0acab124678520bf08edbe20a115f3c416407683e45b07cc5654b46bdcb71559004ef22271989d90b
7
+ data.tar.gz: ae81c712c9d31ffd440b8ad49200aea40c52251f8a6017344d0ce6bbd70b3b236ac89c850e2aac57c6866f87cec986a0c26694989b5f4b40eed7ffc147c1bbdb
@@ -5,6 +5,12 @@ on:
5
5
  push:
6
6
  branches: [main]
7
7
 
8
+ env:
9
+ # Hermetic CI: the corpus ref is pinned, never floating. Bump this
10
+ # one line deliberately with each corpus release (the tag carries
11
+ # what changed).
12
+ CORPUS_REF: v0.8.0
13
+
8
14
  jobs:
9
15
  test:
10
16
  runs-on: ubuntu-latest
@@ -18,12 +24,12 @@ jobs:
18
24
  uses: actions/checkout@v7
19
25
  with:
20
26
  repository: asciichem/asciichem-tests
27
+ ref: ${{ env.CORPUS_REF }}
21
28
  path: .asciichem-tests
22
29
  - name: Point the suite at the corpus and record its version
23
30
  run: |
24
31
  echo "ASCIICHEM_CORPUS=$(pwd)/.asciichem-tests/corpus/fixtures" >> "$GITHUB_ENV"
25
- git -C .asciichem-tests fetch --depth 1 --tags --quiet
26
- echo "ASCIICHEM_CORPUS_VERSION=$(git -C .asciichem-tests describe --tags --abbrev=0 2>/dev/null || echo main)" >> "$GITHUB_ENV"
32
+ echo "ASCIICHEM_CORPUS_VERSION=${{ env.CORPUS_REF }}" >> "$GITHUB_ENV"
27
33
  - uses: ruby/setup-ruby@v1
28
34
  with:
29
35
  ruby-version: ${{ matrix.ruby }}
data/CHANGELOG.md CHANGED
@@ -3,6 +3,27 @@
3
3
  All notable changes to AsciiChem are documented here.
4
4
  This project follows [Semantic Versioning](https://semver.org/).
5
5
 
6
+ ## [0.31.6] - 2026-10-21
7
+
8
+ ### Added
9
+ - **Mechanism wire ingestion** (TODO.v2/13 item 4): `from_model_json`
10
+ restores mechanisms — steps return as canonical reaction text, flows
11
+ and spectators intact; cascade steps have no wire form and raise
12
+ instead of silently flattening. Mechanism round-trip is asserted
13
+ corpus-wide (the `modelJsonRoundTrip` flag is gone).
14
+
15
+ ### Changed
16
+ - **BalanceCheck diagnostics carry structured details**: each
17
+ imbalance reports `element => [reactants, products]` deltas for
18
+ every element whose counts differ, alongside the message.
19
+ - **RInChI engine matches the real `rinchi_cmdline` contract**
20
+ (validated end-to-end against the IUPAC binary built from source):
21
+ the RXN file is the positional argument (no flags), direction-distinct
22
+ identities for a reaction and its reverse.
23
+ - Hermetic CI: the conformance corpus is pinned (`CORPUS_REF: v0.8.0`)
24
+ and the schema re-vendor script defaults to the released
25
+ asciichem-model tag (v0.6.0) — CI inputs no longer float at `main`.
26
+
6
27
  ## [0.31.5] - 2026-10-20
7
28
 
8
29
  ### Changed
@@ -33,7 +33,8 @@ module AsciiChem
33
33
  diffs = compute_diffs(reactants, products)
34
34
  diagnostics << error(
35
35
  "Reaction is not balanced: #{diffs}",
36
- node: reaction
36
+ node: reaction,
37
+ details: element_deltas(reactants, products)
37
38
  )
38
39
  end
39
40
 
@@ -87,6 +88,16 @@ module AsciiChem
87
88
  result
88
89
  end
89
90
 
91
+ # Element => [reactant_count, product_count] for every element
92
+ # whose counts differ — the numbers behind the message.
93
+ def element_deltas(reactants, products)
94
+ (reactants.keys | products.keys).sort.each_with_object({}) do |element, out|
95
+ r = reactants.fetch(element, 0)
96
+ p = products.fetch(element, 0)
97
+ out[element] = [r, p] if r != p
98
+ end
99
+ end
100
+
90
101
  def compute_diffs(reactants, products)
91
102
  all_elements = (reactants.keys + products.keys).uniq.sort
92
103
  all_elements.map do |element|
@@ -22,16 +22,16 @@ module AsciiChem
22
22
 
23
23
  protected
24
24
 
25
- def error(message, node: nil)
26
- Diagnostic.new(severity: :error, message: message, node: node)
25
+ def error(message, node: nil, details: nil)
26
+ Diagnostic.new(severity: :error, message: message, node: node, details: details)
27
27
  end
28
28
 
29
- def warning(message, node: nil)
30
- Diagnostic.new(severity: :warning, message: message, node: node)
29
+ def warning(message, node: nil, details: nil)
30
+ Diagnostic.new(severity: :warning, message: message, node: node, details: details)
31
31
  end
32
32
 
33
- def info(message, node: nil)
34
- Diagnostic.new(severity: :info, message: message, node: node)
33
+ def info(message, node: nil, details: nil)
34
+ Diagnostic.new(severity: :info, message: message, node: node, details: details)
35
35
  end
36
36
 
37
37
  # Depth-first walk over every node in the formula. Yields each
@@ -2,7 +2,12 @@
2
2
 
3
3
  module AsciiChem
4
4
  module Linter
5
- Diagnostic = Struct.new(:severity, :message, :node, keyword_init: true) do
5
+ # `details` carries optional structured data behind the message
6
+ # (e.g. BalanceCheck's per-element [reactants, products] counts)
7
+ # for consumers that want the numbers, not the prose. Human-facing
8
+ # output stays the message.
9
+ Diagnostic = Struct.new(:severity, :message, :node, :details,
10
+ keyword_init: true) do
6
11
  def to_s
7
12
  context = node&.diagnostic_label
8
13
  if context
@@ -1,6 +1,7 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  require 'open3'
4
+ require 'tempfile'
4
5
 
5
6
  module AsciiChem
6
7
  module Rinchi
@@ -10,24 +11,28 @@ module AsciiChem
10
11
  # TODO.v2/10, a maintainer decision).
11
12
  #
12
13
  # AsciiChem::Rinchi.engine = AsciiChem::Rinchi::BinaryEngine.new
13
- # AsciiChem::Rinchi.engine = AsciiChem::Rinchi::BinaryEngine.new(bin: "/usr/local/bin/rinchi-1")
14
+ # AsciiChem::Rinchi.engine = AsciiChem::Rinchi::BinaryEngine.new(bin: "/usr/local/bin/rinchi_cmdline")
15
+ # The reference CLI (IUPAC-InChI/RInChI's `rinchi_cmdline`) takes a
16
+ # FILE argument, not stdin, needs no flags, and prints RInChI=,
17
+ # RAuxInfo=, Long-RInChIKey=, Short-RInChIKey= and Web-RInChIKey=
18
+ # lines. The engine writes the RXN to a temp file, runs the binary
19
+ # on it, and reads the identity lines back.
14
20
  class BinaryEngine
15
- DEFAULT_FLAGS = %w[-STDIO -AuxNone -NoLabels -Key].freeze
21
+ DEFAULT_FLAGS = [].freeze
16
22
 
17
23
  attr_reader :bin, :flags
18
24
 
19
- def initialize(bin: 'rinchi-1', flags: DEFAULT_FLAGS)
25
+ def initialize(bin: 'rinchi_cmdline', flags: DEFAULT_FLAGS)
20
26
  super()
21
27
  @bin = bin
22
28
  @flags = flags.freeze
23
29
  end
24
30
 
25
- # reaction → RXN → rinchi-1 → Identity. The RXN emission is the
26
- # pipe: reactants and products cross into the engine as V2000
31
+ # reaction → RXN → rinchi binary → Identity. The RXN emission is
32
+ # the pipe: reactants and products cross into the engine as V2000
27
33
  # mol blocks inside one $RXN container.
28
34
  def identity(reaction)
29
- rxn = AsciiChem::Rxn.write(reaction)
30
- out = execute(rxn)
35
+ out = execute(AsciiChem::Rxn.write(reaction))
31
36
  rinchi = out[/^RInChI=\S+/]
32
37
  raise AsciiChem::Error, "#{bin} produced no RInChI:\n#{out}" unless rinchi
33
38
 
@@ -40,16 +45,26 @@ module AsciiChem
40
45
 
41
46
  private
42
47
 
43
- def execute(input)
44
- out, err, status = Open3.capture3(bin, *flags, stdin_data: input)
45
- unless status.success?
46
- raise AsciiChem::Error, "#{bin} failed (exit #{status.exitstatus}): #{(err + out).strip[0, 500]}"
48
+ def execute(rxn)
49
+ out = err = nil
50
+ status = nil
51
+ Tempfile.create(['asciichem', '.rxn']) do |file|
52
+ file.write(rxn)
53
+ file.flush
54
+ out, err, status = Open3.capture3(bin, file.path, *flags)
47
55
  end
48
-
56
+ report_failure(status, err, out)
49
57
  out
50
58
  rescue Errno::ENOENT
51
59
  raise EngineMissingError, "RInChI engine binary #{bin.inspect} not found — #{AsciiChem::Rinchi::INSTALL_GUIDE}"
52
60
  end
61
+
62
+ def report_failure(status, err, out)
63
+ return if status&.success?
64
+
65
+ raise AsciiChem::Error,
66
+ "#{bin} failed (exit #{status&.exitstatus}): #{((err || '') + (out || '')).strip[0, 500]}"
67
+ end
53
68
  end
54
69
  end
55
70
  end
@@ -15,9 +15,11 @@ module AsciiChem
15
15
  autoload :Identity, 'asciichem/rinchi/identity'
16
16
 
17
17
  INSTALL_GUIDE = 'install the IUPAC RInChI software ' \
18
- '(https://github.com/IUPAC-InChI/RInChI) and set ' \
19
- 'AsciiChem::Rinchi.engine = AsciiChem::Rinchi::BinaryEngine.new ' \
20
- '(or pass bin: "path/to/rinchi-1")'
18
+ '(https://github.com/IUPAC-InChI/RInChI — build ' \
19
+ 'src/example_apps/rinchi_cmdline against InChI v1.07.5) ' \
20
+ 'and set AsciiChem::Rinchi.engine = ' \
21
+ 'AsciiChem::Rinchi::BinaryEngine.new ' \
22
+ '(or pass bin: "path/to/rinchi_cmdline")'
21
23
 
22
24
  class << self
23
25
  attr_accessor :engine
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module AsciiChem
4
- VERSION = "0.31.5"
4
+ VERSION = "0.31.6"
5
5
  end
@@ -9,17 +9,19 @@ module AsciiChem
9
9
  # fields.
10
10
  #
11
11
  # Emission (to_wire/to_model_json) covers every wire type.
12
- # Ingestion (from_wire/from_model_json) currently covers the
13
- # lossless core set: formula, atom, molecule, group, bond,
12
+ # Ingestion (from_wire/from_model_json) covers the lossless core
13
+ # set plus mechanism: formula, atom, molecule, group, bond,
14
14
  # identifier, name, reaction (+conditions), reaction-cascade,
15
- # electron-configuration, embedded-math, text. The beyond-formulas
16
- # nodes (mechanism, spectrum, crystal, zmatrix, calculation) are
15
+ # electron-configuration, embedded-math, text, mechanism (steps as
16
+ # canonical reaction text, flows, spectators; cascade steps have no
17
+ # wire form). Spectrum, crystal, zmatrix and calculation remain
17
18
  # emission-only until their round-trip acceptance lands in the
18
19
  # corpus.
19
20
  module WireAdapter
20
21
  CORE_INGEST_TYPES = %w[
21
22
  formula atom molecule group bond identifier name reaction
22
23
  reaction-cascade electron-configuration embedded-math text
24
+ mechanism
23
25
  ].freeze
24
26
 
25
27
  class << self
@@ -72,20 +74,27 @@ module AsciiChem
72
74
  # only routes `type` and re-injects routed children into
73
75
  # polymorphic collections (which lutaml deserialises flat).
74
76
  def wire_tree_from_hash(hash)
75
- type = hash.fetch("type") { raise KeyError, "wire node missing type discriminator" }
77
+ type = hash.fetch('type') { raise KeyError, 'wire node missing type discriminator' }
76
78
  klass = AsciiChem::Wire::REGISTRY.fetch(type) do
77
79
  raise KeyError, "unknown wire node type #{type.inspect}"
78
80
  end
79
81
 
80
82
  wire = klass.from_json(JSON.generate(hash))
81
83
  case type
82
- when "formula", "molecule", "group"
83
- wire.nodes = hash.fetch("nodes", []).map { |child| wire_tree_from_hash(child) }
84
- when "reaction"
85
- wire.reactants = hash.fetch("reactants", []).map { |child| wire_tree_from_hash(child) }
86
- wire.products = hash.fetch("products", []).map { |child| wire_tree_from_hash(child) }
87
- when "reaction-cascade"
88
- wire.steps = hash.fetch("steps", []).map { |child| wire_tree_from_hash(child) }
84
+ when 'formula', 'molecule', 'group'
85
+ wire.nodes = hash.fetch('nodes', []).map { |child| wire_tree_from_hash(child) }
86
+ when 'reaction'
87
+ wire.reactants = hash.fetch('reactants', []).map { |child| wire_tree_from_hash(child) }
88
+ wire.products = hash.fetch('products', []).map { |child| wire_tree_from_hash(child) }
89
+ when 'reaction-cascade'
90
+ wire.steps = hash.fetch('steps', []).map { |child| wire_tree_from_hash(child) }
91
+ when 'mechanism'
92
+ wire.steps = hash.fetch('steps', []).map do |step_hash|
93
+ step = AsciiChem::Wire::MechanismStep.from_json(JSON.generate(step_hash))
94
+ step.reaction = wire_tree_from_hash(step_hash.fetch('reaction'))
95
+ step
96
+ end
97
+ wire.spectators = hash.fetch('spectators', []).map { |child| wire_tree_from_hash(child) }
89
98
  end
90
99
  wire
91
100
  end
@@ -93,12 +102,12 @@ module AsciiChem
93
102
  private
94
103
 
95
104
  def formula_to_wire(node)
96
- AsciiChem::Wire::Formula.new(type: "formula", nodes: node.nodes.map { |n| to_wire(n) })
105
+ AsciiChem::Wire::Formula.new(type: 'formula', nodes: node.nodes.map { |n| to_wire(n) })
97
106
  end
98
107
 
99
108
  def atom_to_wire(atom)
100
109
  AsciiChem::Wire::Atom.new(
101
- type: "atom",
110
+ type: 'atom',
102
111
  element: atom.element,
103
112
  isotope: digits_or_nil(atom.isotope), charge: str_or_nil(atom.charge),
104
113
  subscript: digits_or_nil(atom.subscript),
@@ -134,7 +143,7 @@ module AsciiChem
134
143
 
135
144
  def molecule_to_wire(node)
136
145
  AsciiChem::Wire::Molecule.new(
137
- type: "molecule",
146
+ type: 'molecule',
138
147
  nodes: node.nodes.map { |n| to_wire(n) },
139
148
  coefficient: node.coefficient,
140
149
  stereo: stereo_to_wire(node.stereo),
@@ -158,7 +167,7 @@ module AsciiChem
158
167
 
159
168
  def group_to_wire(node)
160
169
  AsciiChem::Wire::Group.new(
161
- type: "group",
170
+ type: 'group',
162
171
  nodes: node.nodes.map { |n| to_wire(n) },
163
172
  multiplicity: node.multiplicity,
164
173
  bracket: node.bracket.to_s
@@ -166,27 +175,27 @@ module AsciiChem
166
175
  end
167
176
 
168
177
  def bond_to_wire(node)
169
- AsciiChem::Wire::Bond.new(type: "bond", kind: node.kind.to_s,
178
+ AsciiChem::Wire::Bond.new(type: 'bond', kind: node.kind.to_s,
170
179
  direction: str_or_nil(node.direction&.to_s))
171
180
  end
172
181
 
173
182
  def identifier_to_wire(node)
174
183
  AsciiChem::Wire::Identifier.new(
175
- type: "identifier",
184
+ type: 'identifier',
176
185
  value: node.value, convention: node.convention.to_s, dict_ref: node.dict_ref
177
186
  )
178
187
  end
179
188
 
180
189
  def name_to_wire(node)
181
190
  AsciiChem::Wire::Name.new(
182
- type: "name",
191
+ type: 'name',
183
192
  content: node.content, convention: node.convention, dict_ref: node.dict_ref
184
193
  )
185
194
  end
186
195
 
187
196
  def reaction_to_wire(node)
188
197
  AsciiChem::Wire::Reaction.new(
189
- type: "reaction",
198
+ type: 'reaction',
190
199
  reactants: node.reactants.map { |m| molecule_to_wire(m) },
191
200
  products: node.products.map { |m| molecule_to_wire(m) },
192
201
  arrow: node.arrow.to_s,
@@ -202,14 +211,14 @@ module AsciiChem
202
211
 
203
212
  def cascade_to_wire(node)
204
213
  AsciiChem::Wire::ReactionCascade.new(
205
- type: "reaction-cascade",
214
+ type: 'reaction-cascade',
206
215
  steps: node.steps.map { |r| reaction_to_wire(r) }
207
216
  )
208
217
  end
209
218
 
210
219
  def econfig_to_wire(node)
211
220
  AsciiChem::Wire::ElectronConfiguration.new(
212
- type: "electron-configuration",
221
+ type: 'electron-configuration',
213
222
  orbitals: node.orbitals.map do |(label, count)|
214
223
  AsciiChem::Wire::OrbitalOccupancy.new(orbital: label.to_s, occupancy: count.to_s)
215
224
  end,
@@ -226,16 +235,16 @@ module AsciiChem
226
235
  end
227
236
 
228
237
  def math_to_wire(node)
229
- AsciiChem::Wire::EmbeddedMath.new(type: "embedded-math", source: node.source)
238
+ AsciiChem::Wire::EmbeddedMath.new(type: 'embedded-math', source: node.source)
230
239
  end
231
240
 
232
241
  def text_to_wire(node)
233
- AsciiChem::Wire::Text.new(type: "text", content: node.content)
242
+ AsciiChem::Wire::Text.new(type: 'text', content: node.content)
234
243
  end
235
244
 
236
245
  def mechanism_to_wire(node)
237
246
  AsciiChem::Wire::Mechanism.new(
238
- type: "mechanism",
247
+ type: 'mechanism',
239
248
  steps: node.steps.map { |step| mechanism_step_to_wire(step) },
240
249
  spectators: node.spectators.map { |sp| mechanism_spectator_to_wire(sp) }
241
250
  )
@@ -264,12 +273,16 @@ module AsciiChem
264
273
 
265
274
  # The model keeps a step's reaction as TEXT (the mechanism body
266
275
  # is verbatim); the wire carries the typed node, so the same
267
- # grammar parses it here.
276
+ # grammar parses it here. A cascade step has no wire form —
277
+ # MechanismStep.reaction is typed Reaction — and says so instead
278
+ # of silently flattening the chain.
268
279
  def mechanism_reaction_to_wire(text)
269
280
  node = AsciiChem.parse(text).nodes.first
270
281
  case node
271
282
  when AsciiChem::Model::Reaction then reaction_to_wire(node)
272
- when AsciiChem::Model::ReactionCascade then cascade_to_wire(node)
283
+ when AsciiChem::Model::ReactionCascade
284
+ raise AsciiChem::ParseError,
285
+ "mechanism step cascades have no wire form: #{text.inspect}"
273
286
  else
274
287
  raise AsciiChem::ParseError,
275
288
  "mechanism step is not a reaction: #{text.inspect}"
@@ -278,7 +291,7 @@ module AsciiChem
278
291
 
279
292
  def spectrum_to_wire(node)
280
293
  AsciiChem::Wire::Spectrum.new(
281
- type: "spectrum",
294
+ type: 'spectrum',
282
295
  technique: node.type,
283
296
  params: node.params,
284
297
  peaks: node.peaks.map do |peak|
@@ -292,7 +305,7 @@ module AsciiChem
292
305
 
293
306
  def crystal_to_wire(node)
294
307
  AsciiChem::Wire::Crystal.new(
295
- type: "crystal",
308
+ type: 'crystal',
296
309
  name: node.name, a: node.a, b: node.b, c: node.c,
297
310
  alpha: node.alpha, beta: node.beta, gamma: node.gamma,
298
311
  spacegroup: node.spacegroup,
@@ -302,7 +315,7 @@ module AsciiChem
302
315
 
303
316
  def zmatrix_to_wire(node)
304
317
  AsciiChem::Wire::ZMatrix.new(
305
- type: "zmatrix",
318
+ type: 'zmatrix',
306
319
  rows: node.rows.map do |row|
307
320
  AsciiChem::Wire::ZRow.new(
308
321
  atom: row.atom, ref1: row.ref1, distance: row.distance,
@@ -314,7 +327,7 @@ module AsciiChem
314
327
 
315
328
  def calculation_to_wire(node)
316
329
  AsciiChem::Wire::Calculation.new(
317
- type: "calculation",
330
+ type: 'calculation',
318
331
  method: node.method, basis: node.basis,
319
332
  properties: node.properties.map do |prop|
320
333
  AsciiChem::Wire::CalculatedProperty.new(
@@ -394,6 +407,26 @@ module AsciiChem
394
407
  )
395
408
  end
396
409
 
410
+ def mechanism_from_wire(wire)
411
+ AsciiChem::Model::Mechanism.new(
412
+ steps: Array(wire.steps).map { |s| mechanism_step_from_wire(s) },
413
+ spectators: Array(wire.spectators).map { |sp| molecule_from_wire(sp).to_text }
414
+ )
415
+ end
416
+
417
+ # The model keeps a step's reaction as TEXT; the wire carries the
418
+ # typed node, so ingestion renders it back through the canonical
419
+ # text form.
420
+ def mechanism_step_from_wire(step)
421
+ AsciiChem::Model::Mechanism::Step.new(
422
+ label: step.label,
423
+ reaction: from_wire(step.reaction).to_text,
424
+ flows: Array(step.flows).map do |flow|
425
+ AsciiChem::Model::Mechanism::Flow.new(source: flow.source, target: flow.target)
426
+ end
427
+ )
428
+ end
429
+
397
430
  def electron_configuration_from_wire(wire)
398
431
  AsciiChem::Model::ElectronConfiguration.new(
399
432
  orbitals: Array(wire.orbitals).map { |o| [o.orbital, o.occupancy] },
@@ -1,9 +1,10 @@
1
1
  #!/usr/bin/env bash
2
2
  # Re-vendor the asciichem-model v1 JSON Schemas (YAML) and examples.
3
- # Usage: scripts/update-model-schemas.sh [model-tag] (default: main).
3
+ # Usage: scripts/update-model-schemas.sh [model-tag]. Default is the
4
+ # released tag — vendored schemas stay hermetic, never floating.
4
5
  set -euo pipefail
5
6
 
6
- tag="${1:-main}"
7
+ tag="${1:-v0.6.0}"
7
8
  work="$(mktemp -d)"
8
9
  trap 'rm -rf "$work"' EXIT
9
10
 
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: asciichem
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.31.5
4
+ version: 0.31.6
5
5
  platform: ruby
6
6
  authors:
7
7
  - Ribose Inc.