asciichem 0.29.1 → 0.29.2

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: 9579311feee35b70f9d15879379582cb67a8b6b9fe004e7c714e12448835e53c
4
- data.tar.gz: 6f732f510f6d8923f2d1c855113ec34db33910a258135ee36a954035c90a252b
3
+ metadata.gz: f372a8539906c658a399b015255e574de5feb73d1b5e7183958901681bfbce34
4
+ data.tar.gz: 7269dc4ad6e39e7c5ea6726b54a8ffe1508c039bdf9ced5c0a74e23efdc9b00f
5
5
  SHA512:
6
- metadata.gz: 509996644be4e271c9432f8c5cc7baee5120f58b75af97f3f0bc6050b776dca6c567b1f69586bd87258237f12a56eeabccd4085e45b41772506ae099230d261c
7
- data.tar.gz: e4b197451e71cc877398d6ce00130d1ac8dc0f2d73878b05dae4f849f145d99ac7de3e4e8e098db574eeb0c528f67030005a3538ed219f2178fb3ee7bc94c6cc
6
+ metadata.gz: 6f936bc0e95907d589c0095d34e4e65c554dcbdccd276aab7186db8f1218cbb179652b082f088f1b70a76391e830db53de2259ea8d333b64dd85ba367b16c610
7
+ data.tar.gz: 63d979830e4a8ab9aca450006a9740e83428e26073323af0ed4f8ddeed4be6f7aecb8bcfe1e5173f7a6049f92ff8b0a8e0a2b374fbd13630ae00b0e9e7a0db60
@@ -34,6 +34,15 @@ jobs:
34
34
  with:
35
35
  ruby-version: "3.4"
36
36
  bundler-cache: true
37
+ # CI never pushes to git (read-only contents). `rake release`
38
+ # attempts `git push origin main` after publishing unless the
39
+ # version tag already exists locally — bundler then prints
40
+ # "Tag vX has already been created" and skips its git stage
41
+ # entirely (this is how 0.29.0/0.29.1 released). Pre-create the
42
+ # tag so the gem push is the only remote operation; tags on the
43
+ # remote remain the maintainer's.
44
+ - name: Pre-create the release tag (skips rake's git stage)
45
+ run: git tag "v${{ inputs.version }}"
37
46
  # Builds and pushes using the GitHub OIDC identity — no API keys.
38
47
  - uses: rubygems/release-gem@v1
39
48
  - name: Summary
data/CHANGELOG.md CHANGED
@@ -3,6 +3,18 @@
3
3
  All notable changes to AsciiChem are documented here.
4
4
  This project follows [Semantic Versioning](https://semver.org/).
5
5
 
6
+ ## [0.29.2] - 2026-09-17
7
+
8
+ ### Changed
9
+ - `relaton-bib` constraint widened to `>= 0.1, < 3`: asciichem now
10
+ co-resolves with current metanorma gems (metanorma-standoc and
11
+ friends require relaton-bib 2). The citation track speaks both
12
+ major lines through a single `Citation::RelatonApi` seam —
13
+ relaton-bib 1 (`RelatonBib`) and relaton-bib 2
14
+ (`Relaton::Bib` typed models) both build and serialize the
15
+ dataset-type bibitems; profile data is version-independent.
16
+ - nil resolver links no longer emit an empty `<uri>` element.
17
+
6
18
  ## [0.29.1] - 2026-09-16
7
19
 
8
20
  ### Added
data/asciichem.gemspec CHANGED
@@ -39,7 +39,7 @@ Gem::Specification.new do |spec|
39
39
  spec.add_dependency "mml", "~> 2.3"
40
40
  spec.add_dependency "nokogiri", "~> 1.16"
41
41
  spec.add_dependency "parslet", "~> 2.0"
42
- spec.add_dependency "relaton-bib", ">= 0.1", "< 2"
42
+ spec.add_dependency "relaton-bib", ">= 0.1", "< 3"
43
43
  spec.add_dependency "plurimath", "~> 0.8"
44
44
  spec.add_dependency "thor", "~> 1.3"
45
45
 
data/benchmarks/README.md CHANGED
@@ -113,9 +113,58 @@ unaffected; `@next_id` remains unfixed upstream but no longer fires
113
113
  on corpus inputs. The re-check-3 verdict below is superseded — the
114
114
  engine IS switchable and shipped (TODO.impl 64).
115
115
 
116
- **Verdict: one upstream one-liner from adoption evaluation.** With
117
- `@next_id += 1` fixed, the entire corpus passes under native at
118
- 3x parslet speed — at that point the decision is whether to make the
119
- engine switchable (opt-in, soft dependency) in the gem.
120
-
116
+ **Verdict: superseded the engine IS switchable and shipped**
117
+ (asciichem 0.29.0, TODO.impl 64).
118
+
119
+ ### Re-check 6 (2026-09-16, parsanol 1.3.18)
120
+
121
+ Gate still **221/221** through the shipped engine, but the "one
122
+ decode path" rework **regressed compat-layer throughput ~60%** for
123
+ this grammar: 7.2 ms/batch (138 i/s) vs 4.3-4.6 ms on 1.3.15/16,
124
+ with the parslet control stable across sessions (11-14 ms
125
+ throughout). The `H2`/`_2O` acceptance divergence also persists.
126
+ Reported upstream (parsanol-ruby#25, fourth comment). We stay on
127
+ the shipped engine; users pinning parsanol for speed should prefer
128
+ 1.3.16/1.3.17 until the regression is addressed.
129
+
130
+ ### Re-check 7 (2026-09-16, parsanol 1.3.20)
131
+
132
+ Three upstream issues closed since 1.3.18:
133
+
134
+ - **#38** (`Dynamic.register` `@next_id` collision panicking the
135
+ Rust core) — fixed; no panic during full-corpus run.
136
+ - **#37** ("one decode path" throughput regression) — fixed as a
137
+ side effect of the optimizer acceptance fix in #39; throughput on
138
+ this grammar is back to and ahead of 1.3.15/16 levels.
139
+ - **#39** (optimizer Str/Re run-merging changed sequence-boundary
140
+ acceptance) — root-caused to Re-run regex-source concatenation
141
+ (proven unsafe: `"a|"+"b"` → `"a|b"` accepts `"a"`); Re runs now
142
+ stay unmerged, Str-run merging stays. Spec-level decision
143
+ recorded: the optimizer may never alter acceptance.
144
+
145
+ Validation against 1.3.20:
146
+
147
+ - Gate **221/221** through the shipped `ParsanolEngine` (fork-per-case,
148
+ no Rust aborts).
149
+ - Head-to-head vs parslet, same Ruby process (3 runs, ±3-15%):
150
+ parsanol **2.6x faster** (4.65–5.19 ms/batch vs 12.18–13.65 ms for
151
+ parslet). Up from the 1.7x under 1.3.18 — the #37 regression is
152
+ gone.
153
+ - Direct `H2` / `_2O` / `Ca2+` / `H22` / `O2` probe across both
154
+ parslet and parsanol (native and ruby backends) shows **identical
155
+ parse outcomes**. The earlier "divergence" framing in re-checks
156
+ 3-6 was a misreading: AsciiChem's `hydrogen_atom` grammar rule
157
+ intentionally permits bare-digit subscripts after `H` ("lets users
158
+ write `H2O` instead of `H_2O`" — grammar_rules.rb:228-231) and
159
+ `isotope_marker` accepts both `^digits` and `_digits`, so `_2O`
160
+ parses as the isotope of `O` and round-trips as `^2O`. The
161
+ parsanol optimizer bug in #39 was real and is fixed, but the
162
+ AsciiChem repro was a misleading example — both engines agree on
163
+ these inputs because they share the same grammar rules.
164
+
165
+ **Verdict: shipped engine fully validated.** 2.6x speedup, 100%
166
+ corpus gate, all four reported upstream issues now resolved or
167
+ non-blocking (#36 bare repeated sibling captures remains open but
168
+ is worked around in `ParsanolEngine` via single `.as(...)` capture
169
+ wrapping).
121
170
 
@@ -1,6 +1,14 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require "relaton_bib"
3
+ # relaton-bib 2 renamed the entry file (relaton_bib -> relaton/bib)
4
+ # and reworked the namespace (RelatonBib -> Relaton::Bib). The
5
+ # gemspec admits both major lines, so load whichever is resolved and
6
+ # speak to it through RelatonApi below.
7
+ begin
8
+ require 'relaton/bib'
9
+ rescue LoadError
10
+ require 'relaton_bib'
11
+ end
4
12
 
5
13
  module AsciiChem
6
14
  # Citation track (TODO.v2 08; TODO.impl 44): a bibitem is a function
@@ -16,25 +24,25 @@ module AsciiChem
16
24
  Profile = Struct.new(:publisher, :link_for, :identifier_for, keyword_init: true)
17
25
 
18
26
  PROFILES = {
19
- "pubchem" => Profile.new(
20
- publisher: "PubChem, U.S. National Library of Medicine",
21
- link_for: ->(substance) do
22
- cid = substance.identifier_value("pubchem-cid")
27
+ 'pubchem' => Profile.new(
28
+ publisher: 'PubChem, U.S. National Library of Medicine',
29
+ link_for: lambda do |substance|
30
+ cid = substance.identifier_value('pubchem-cid')
23
31
  "https://pubchem.ncbi.nlm.nih.gov/compound/#{cid}" if cid
24
32
  end,
25
- identifier_for: ->(substance) do
26
- cid = substance.identifier_value("pubchem-cid")
33
+ identifier_for: lambda do |substance|
34
+ cid = substance.identifier_value('pubchem-cid')
27
35
  "PubChem CID #{cid}" if cid
28
36
  end
29
37
  ),
30
- "common_chemistry" => Profile.new(
31
- publisher: "CAS Common Chemistry",
32
- link_for: ->(substance) do
33
- cas = substance.identifier_value("cas")
38
+ 'common_chemistry' => Profile.new(
39
+ publisher: 'CAS Common Chemistry',
40
+ link_for: lambda do |substance|
41
+ cas = substance.identifier_value('cas')
34
42
  "https://commonchemistry.cas.org/detail?cas_rn=#{cas}" if cas
35
43
  end,
36
- identifier_for: ->(substance) do
37
- cas = substance.identifier_value("cas")
44
+ identifier_for: lambda do |substance|
45
+ cas = substance.identifier_value('cas')
38
46
  "CAS RN #{cas}" if cas
39
47
  end
40
48
  )
@@ -42,8 +50,8 @@ module AsciiChem
42
50
 
43
51
  DEFAULT_PROFILE = Profile.new(
44
52
  publisher: nil,
45
- link_for: ->(_substance) { nil },
46
- identifier_for: ->(substance) do
53
+ link_for: ->(_substance) {},
54
+ identifier_for: lambda do |substance|
47
55
  key = substance.identifiers.first
48
56
  "#{key.convention}: #{key.value}" if key
49
57
  end
@@ -56,34 +64,20 @@ module AsciiChem
56
64
  # carries no provenance (hand-built, not resolved).
57
65
  def bibitem(substance)
58
66
  provenance = substance.provenance
59
- unless provenance&.source
60
- raise Error, "substance has no provenance - resolve it first (AsciiChem::Resolver)"
61
- end
67
+ raise Error, 'substance has no provenance - resolve it first (AsciiChem::Resolver)' unless provenance&.source
62
68
 
63
69
  profile = PROFILES.fetch(provenance.source, DEFAULT_PROFILE)
64
- RelatonBib::BibliographicItem.new(
65
- type: "dataset",
66
- title: [{ type: "main",
67
- content: "#{title_base(substance)} - #{profile.publisher || provenance.source} substance record" }],
68
- docid: [RelatonBib::DocumentIdentifier.new(
69
- id: profile.identifier_for.call(substance) || "#{provenance.source} substance",
70
- type: provenance.source)],
71
- contributor: [{ entity: RelatonBib::Organization.new(name: profile.publisher || provenance.source),
72
- role: [{ type: "publisher" }] }],
73
- date: [{ type: "accessed", on: accessed_on(provenance) }],
74
- link: [{ type: "src", content: profile.link_for.call(substance) }].compact,
75
- keyword: substance.identifiers.map { |i| "#{i.convention}=#{i.value}" }
76
- )
70
+ RelatonApi.dataset_bibitem(fields(substance, profile, provenance))
77
71
  end
78
72
 
79
73
  # Convenience: bibitem XML (what a document pipeline embeds).
80
74
  def to_xml(substance)
81
- bibitem(substance).to_xml
75
+ RelatonApi.to_xml(bibitem(substance))
82
76
  end
83
77
 
84
78
  # The cite syntax (TODO.impl 45): a molecule annotated
85
79
  # `@cite("pubchem")` (a property annotation — the grammar needs
86
- # no extension) declares *which source to cite it from*. This
80
+ # no extension) declares *which source to cite it from. This
87
81
  # resolves the molecule's identifiers and emits one bibitem per
88
82
  # cited source. Returns [[source, bibitem]] pairs; empty when the
89
83
  # molecule has no @cite annotations.
@@ -97,13 +91,14 @@ module AsciiChem
97
91
  convention, value = lookup_key(molecule)
98
92
  unless value
99
93
  raise Error,
100
- "molecule carries no resolvable identifier for citation " \
101
- "(annotate @cas/@inchikey/@smiles or @name)"
94
+ 'molecule carries no resolvable identifier for citation ' \
95
+ '(annotate @cas/@inchikey/@smiles or @name)'
102
96
  end
103
97
 
104
98
  sources.filter_map do |source|
105
99
  substance = AsciiChem::Resolver[source].new.resolve(
106
- value: value, convention: convention, cache: cache, fetch: fetch)
100
+ value: value, convention: convention, cache: cache, fetch: fetch
101
+ )
107
102
  next unless substance
108
103
 
109
104
  [source, bibitem(substance)]
@@ -112,11 +107,27 @@ module AsciiChem
112
107
 
113
108
  private
114
109
 
110
+ # The version-independent field payload: one hash describing the
111
+ # citation, translated to Relaton objects by RelatonApi.
112
+ def fields(substance, profile, provenance)
113
+ publisher = profile.publisher || provenance.source
114
+ {
115
+ type: 'dataset',
116
+ title: "#{title_base(substance)} - #{publisher} substance record",
117
+ docid: { id: profile.identifier_for.call(substance) || "#{provenance.source} substance",
118
+ type: provenance.source },
119
+ publisher: publisher,
120
+ accessed_on: accessed_on(provenance),
121
+ link: profile.link_for.call(substance),
122
+ keywords: substance.identifiers.map { |i| "#{i.convention}=#{i.value}" }
123
+ }
124
+ end
125
+
115
126
  # The property annotation whose title is "cite": values are the
116
127
  # source names to cite from.
117
128
  def citation_sources(molecule)
118
129
  molecule.properties
119
- .select { |p| p.title == "cite" && p.value }
130
+ .select { |p| p.title == 'cite' && p.value }
120
131
  .map(&:value)
121
132
  end
122
133
 
@@ -127,23 +138,96 @@ module AsciiChem
127
138
  return [identifier.convention, identifier.value] if identifier
128
139
 
129
140
  name = molecule.names.first
130
- return ["name", name.content] if name
141
+ return ['name', name.content] if name
131
142
 
132
143
  nil
133
144
  end
134
145
 
135
- private
136
-
137
146
  def title_base(substance)
138
- substance.preferred_name || substance.identifier_value("cas") ||
139
- substance.identifier_value("inchikey") || "Substance"
147
+ substance.preferred_name || substance.identifier_value('cas') ||
148
+ substance.identifier_value('inchikey') || 'Substance'
140
149
  end
141
150
 
142
151
  def accessed_on(provenance)
143
152
  return provenance.retrieved_at[0, 10] if provenance.retrieved_at
144
153
 
145
- Time.now.utc.strftime("%Y-%m-%d")
154
+ Time.now.utc.strftime('%Y-%m-%d')
155
+ end
156
+ end
157
+
158
+ # The relaton-bib version seam. Both major lines accept the same
159
+ # field hash (see Citation#fields) and serialize through their own
160
+ # API; the rest of the citation track stays version-agnostic.
161
+ # Adding a future major = one more module here (OCP).
162
+ module RelatonApi
163
+ module_function
164
+
165
+ def dataset_bibitem(fields)
166
+ (defined?(::Relaton::Bib) ? V2 : V1).build(fields)
167
+ end
168
+
169
+ def to_xml(item)
170
+ item.to_xml
171
+ end
172
+
173
+ # relaton-bib 1: RelatonBib::* with hash-argument constructors.
174
+ module V1
175
+ module_function
176
+
177
+ def build(fields)
178
+ RelatonBib::BibliographicItem.new(
179
+ type: fields[:type],
180
+ title: [{ type: 'main', content: fields[:title] }],
181
+ docid: [RelatonBib::DocumentIdentifier.new(id: fields[:docid][:id],
182
+ type: fields[:docid][:type])],
183
+ contributor: [{ entity: RelatonBib::Organization.new(name: fields[:publisher]),
184
+ role: [{ type: 'publisher' }] }],
185
+ date: [{ type: 'accessed', on: fields[:accessed_on] }],
186
+ link: fields[:link] ? [{ type: 'src', content: fields[:link] }] : [],
187
+ keyword: fields[:keywords]
188
+ )
189
+ end
190
+ end
191
+
192
+ # relaton-bib 2: Relaton::Bib::* typed models (lutaml-model).
193
+ # Date's XML <on> element maps to the Ruby `at` attribute;
194
+ # keywords carry their text in a nested vocab LocalizedString;
195
+ # links are source Uri entries serializing to <uri type="src">.
196
+ module V2
197
+ module_function
198
+
199
+ def build(fields)
200
+ Relaton::Bib::ItemData.new(
201
+ type: fields[:type],
202
+ title: [Relaton::Bib::Title.new(type: 'main', content: fields[:title])],
203
+ docidentifier: [docidentifier(fields[:docid])],
204
+ contributor: [contributor(fields[:publisher])],
205
+ date: [Relaton::Bib::Date.new(type: 'accessed', at: fields[:accessed_on])],
206
+ source: fields[:link] ? [Relaton::Bib::Uri.new(type: 'src', content: fields[:link])] : [],
207
+ keyword: fields[:keywords].map { |text| keyword(text) }
208
+ )
209
+ end
210
+
211
+ def docidentifier(docid)
212
+ Relaton::Bib::Docidentifier.new(type: docid[:type], content: docid[:id])
213
+ end
214
+
215
+ def contributor(publisher)
216
+ Relaton::Bib::Contributor.new(
217
+ organization: Relaton::Bib::Organization.new(
218
+ name: [Relaton::Bib::TypedLocalizedString.new(content: publisher)]
219
+ ),
220
+ role: [Relaton::Bib::Contributor::Role.new(type: 'publisher')]
221
+ )
222
+ end
223
+
224
+ def keyword(text)
225
+ Relaton::Bib::Keyword.new(
226
+ vocab: Relaton::Bib::LocalizedString.new(content: text)
227
+ )
228
+ end
146
229
  end
147
230
  end
231
+ private_constant :RelatonApi
148
232
  end
149
233
  end
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module AsciiChem
4
- VERSION = "0.29.1"
4
+ VERSION = "0.29.2"
5
5
  end
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.29.1
4
+ version: 0.29.2
5
5
  platform: ruby
6
6
  authors:
7
7
  - Ribose Inc.
@@ -108,7 +108,7 @@ dependencies:
108
108
  version: '0.1'
109
109
  - - "<"
110
110
  - !ruby/object:Gem::Version
111
- version: '2'
111
+ version: '3'
112
112
  type: :runtime
113
113
  prerelease: false
114
114
  version_requirements: !ruby/object:Gem::Requirement
@@ -118,7 +118,7 @@ dependencies:
118
118
  version: '0.1'
119
119
  - - "<"
120
120
  - !ruby/object:Gem::Version
121
- version: '2'
121
+ version: '3'
122
122
  - !ruby/object:Gem::Dependency
123
123
  name: plurimath
124
124
  requirement: !ruby/object:Gem::Requirement