asciichem 0.31.16 → 0.31.17

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: 1b985064f84599e157131e30ca2ae73555383b6e273c6ea3ae561f02d52df5da
4
- data.tar.gz: bb08895e055a18b16fe280b30da829666bf9f646d269f9a3b5312b61fbf86578
3
+ metadata.gz: 96ad0bea754dd62a7b5b92f44cf87d1446a7afc1594634f903fd744c7c7c0326
4
+ data.tar.gz: 00d721ea5ba129cbd6f6b606167469178c83ec17ca53ecf1749160d337ea5639
5
5
  SHA512:
6
- metadata.gz: 12604d14213ae4bc6beba2507b6fcd27e540b32d3be966cbdc56816c844f73309e5b6518ea632441e17b7610d7321fcaf25bc2be6ba46659d2165907039c5375
7
- data.tar.gz: 2676f58b2d881b8a9f321bebcaad712e165f2cc4520f419bdf7b41dab28e4330b72f278456265bd62e3c0101d953501d396435fec84c5281d1379de34637a938
6
+ metadata.gz: a4d8d30b7b44a9b4b8ac460708a38e5eaf122a90a3d53c387bda976390eab2e41cf6750742c88755d5147119fa15f58758f4a55c9f846165b21b7ec0986a6bc3
7
+ data.tar.gz: c3e7da0afbcd8f8cce91335c6f722fc797af05d461952b743913e4e12066c25962a3add7db9deca81e7baccde11ac3fcd5b662f1c6bd787b722f608d84b459d7
@@ -9,7 +9,7 @@ env:
9
9
  # Hermetic CI: the corpus ref is pinned, never floating. Bump this
10
10
  # one line deliberately with each corpus release (the tag carries
11
11
  # what changed).
12
- CORPUS_REF: v0.8.0
12
+ CORPUS_REF: v0.11.0
13
13
 
14
14
  jobs:
15
15
  test:
data/CHANGELOG.md CHANGED
@@ -3,6 +3,23 @@
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.17] - 2026-11-13
7
+
8
+ ### Added
9
+ - The resolver clock seam: `resolve_all` and every adapter accept a
10
+ `now:` ISO-8601 UTC timestamp (nil = the wall clock) that pins
11
+ `retrievedAt` — recorded-fixture runs and the citation goldens that
12
+ consume them compare byte-stably. Mirrors the asciichem-py seam of
13
+ the same name.
14
+
15
+ ### Changed
16
+ - The conformance runner gates the corpus 0.11.0 parity families
17
+ (linter-expectations with engine posture + node matching, resolver
18
+ replay, citation byte-goldens, the 22 CLI contracts); the CI
19
+ corpus pin moves v0.8.0 → v0.11.0. Claims matrix: linter 27 run +
20
+ 3 posture skips, resolver 6/6, citation 4/4 byte-equal, CLI 22/22
21
+ (asciichem-ruby#159).
22
+
6
23
  ## [0.31.16] - 2026-11-12
7
24
 
8
25
  ### Changed
@@ -41,7 +41,11 @@ module AsciiChem
41
41
 
42
42
  # Cache-aware resolution. `fetch` must answer #get(url) -> body
43
43
  # String (NetFetch by default; specs inject recorded responses).
44
- def resolve(value:, convention:, fetch: nil, cache: nil, refresh: false)
44
+ # `now` pins the retrieval clock (an ISO-8601 UTC string) for
45
+ # recorded-fixture runs — the shared corpus pins `retrievedAt`
46
+ # so replayed records compare byte-stably; nil means the wall
47
+ # clock.
48
+ def resolve(value:, convention:, fetch: nil, cache: nil, refresh: false, now: nil)
45
49
  cache ||= Cache.default
46
50
  fetch ||= NetFetch.new
47
51
  key = cache.key_for(self.class.source_name, convention, value)
@@ -51,22 +55,22 @@ module AsciiChem
51
55
  return cached if cached
52
56
  end
53
57
 
54
- substance = fetch_record(value: value, convention: convention, fetch: fetch)
55
- .tap { |s| s&.identifiers&.each { |i| i.provenance ||= default_provenance } }
58
+ substance = fetch_record(value: value, convention: convention, fetch: fetch, now: now)
59
+ .tap { |s| s&.identifiers&.each { |i| i.provenance ||= default_provenance(now) } }
56
60
  cache.write(key, substance) if substance
57
61
  substance
58
62
  end
59
63
 
60
64
  # Not-found is nil, not an error — sources legitimately answer
61
65
  # "unknown substance" during multi-source resolution.
62
- def fetch_record(**)
66
+ def fetch_record(value:, convention:, fetch:, now: nil)
63
67
  raise NotImplementedError
64
68
  end
65
69
 
66
- def default_provenance
70
+ def default_provenance(now: nil)
67
71
  Provenance.new(
68
72
  source: self.class.source_name,
69
- retrieved_at: Time.now.utc.strftime("%Y-%m-%dT%H:%M:%SZ"),
73
+ retrieved_at: now || Time.now.utc.strftime("%Y-%m-%dT%H:%M:%SZ"),
70
74
  attribution: attribution
71
75
  )
72
76
  end
@@ -43,7 +43,7 @@ module AsciiChem
43
43
  @api_key = api_key
44
44
  end
45
45
 
46
- def fetch_record(value:, convention:, fetch:)
46
+ def fetch_record(value:, convention:, fetch:, now: nil)
47
47
  headers = {
48
48
  "Accept" => "application/json",
49
49
  "x-origin" => ORIGIN
@@ -55,7 +55,7 @@ module AsciiChem
55
55
  data = JSON.parse(body)
56
56
  return nil if data.empty?
57
57
 
58
- substance_from(data)
58
+ substance_from(data, now)
59
59
  rescue JSON::ParserError => e
60
60
  raise Error, "common_chemistry returned malformed JSON: #{e.message}"
61
61
  end
@@ -71,8 +71,8 @@ module AsciiChem
71
71
  (File.read(KEY_FILE).strip if File.readable?(KEY_FILE))
72
72
  end
73
73
 
74
- def substance_from(data)
75
- provenance = default_provenance
74
+ def substance_from(data, now)
75
+ provenance = default_provenance(now: now)
76
76
  identifiers = [Identifier.new(value: data["rn"], convention: "cas",
77
77
  provenance: provenance)]
78
78
  add_identifier(identifiers, data, "inchikey", "inchikey", provenance)
@@ -27,11 +27,11 @@ module AsciiChem
27
27
  BASE = 'https://opsin.ch.cam.ac.uk/opsin'
28
28
  private_constant :BASE
29
29
 
30
- def fetch_record(value:, convention: nil, fetch:)
30
+ def fetch_record(value:, convention: nil, fetch:, now: nil)
31
31
  body = fetch.get("#{BASE}/#{CGI.escape(value.to_s)}.smiles")
32
32
  return nil unless body
33
33
 
34
- substance_from(body.strip)
34
+ substance_from(body.strip, now)
35
35
  rescue StandardError => e
36
36
  raise Error, "opsin request failed: #{e.message}"
37
37
  end
@@ -42,10 +42,10 @@ module AsciiChem
42
42
 
43
43
  private
44
44
 
45
- def substance_from(smiles)
45
+ def substance_from(smiles, now)
46
46
  return nil if smiles.empty?
47
47
 
48
- provenance = default_provenance
48
+ provenance = default_provenance(now: now)
49
49
  Substance.new(
50
50
  preferred_name: nil,
51
51
  identifiers: [Identifier.new(value: smiles, convention: 'canonical-smiles',
@@ -26,14 +26,14 @@ module AsciiChem
26
26
  MolecularWeight IUPACName InChI InChIKey].freeze
27
27
  private_constant :BASE, :PROPERTIES
28
28
 
29
- def fetch_record(value:, convention:, fetch:)
29
+ def fetch_record(value:, convention:, fetch:, now: nil)
30
30
  body = fetch.get(url_for(value, convention))
31
31
  return nil unless body
32
32
 
33
33
  props = JSON.parse(body).dig("PropertyTable", "Properties", 0)
34
34
  return nil unless props
35
35
 
36
- substance_from(props)
36
+ substance_from(props, now)
37
37
  rescue JSON::ParserError => e
38
38
  raise Error, "pubchem returned malformed JSON: #{e.message}"
39
39
  end
@@ -53,8 +53,8 @@ module AsciiChem
53
53
  "#{BASE}/#{namespace}/#{CGI.escape(value.to_s)}/property/#{PROPERTIES.join(',')}/JSON"
54
54
  end
55
55
 
56
- def substance_from(props)
57
- provenance = default_provenance
56
+ def substance_from(props, now)
57
+ provenance = default_provenance(now: now)
58
58
  identifiers = []
59
59
  identifiers << Identifier.new(value: props["CID"].to_s, convention: "pubchem-cid", provenance: provenance) if props["CID"]
60
60
  add_identifier(identifiers, props, "InChIKey", "inchikey", provenance)
@@ -48,15 +48,16 @@ module AsciiChem
48
48
  # entries dropped). Sources answer in registration order. A
49
49
  # source-level failure (HTTP error, rejected request) skips that
50
50
  # source — one dead or key-gated endpoint must not poison the
51
- # others; callers see what answered.
52
- def resolve_all(value:, convention:, sources: nil, fetch: nil, cache: nil, refresh: false)
51
+ # others; callers see what answered. `now` pins the retrieval
52
+ # clock for recorded-fixture runs (the adapters' seam).
53
+ def resolve_all(value:, convention:, sources: nil, fetch: nil, cache: nil, refresh: false, now: nil)
53
54
  selected = sources ? Array(sources).map { |s| self[s] } : adapters.values
54
55
  selected.filter_map do |adapter_class|
55
56
  adapter = adapter_class.new
56
57
  next unless adapter.supports?(convention)
57
58
 
58
59
  begin
59
- adapter.resolve(value: value, convention: convention, fetch: fetch, cache: cache, refresh: refresh)
60
+ adapter.resolve(value: value, convention: convention, fetch: fetch, cache: cache, refresh: refresh, now: now)
60
61
  rescue AsciiChem::Error
61
62
  nil
62
63
  end
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module AsciiChem
4
- VERSION = "0.31.16"
4
+ VERSION = "0.31.17"
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.31.16
4
+ version: 0.31.17
5
5
  platform: ruby
6
6
  authors:
7
7
  - Ribose Inc.