asciichem-model 0.3.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.
Files changed (78) hide show
  1. checksums.yaml +7 -0
  2. data/LICENSE +21 -0
  3. data/README.adoc +80 -0
  4. data/docs/adr/0001-language-bindings.adoc +78 -0
  5. data/exe/generate-types-from-schemas +6 -0
  6. data/lib/asciichem_model/schema_type_generator.rb +163 -0
  7. data/lib/asciichem_model/validators.rb +79 -0
  8. data/lib/asciichem_model/version.rb +5 -0
  9. data/lib/asciichem_model.rb +18 -0
  10. data/models/asciichem/Atom.lutaml +49 -0
  11. data/models/asciichem/Bond.lutaml +24 -0
  12. data/models/asciichem/Calculation.lutaml +34 -0
  13. data/models/asciichem/Crystal.lutaml +34 -0
  14. data/models/asciichem/ElectronConfiguration.lutaml +35 -0
  15. data/models/asciichem/EmbeddedMath.lutaml +10 -0
  16. data/models/asciichem/Formula.lutaml +13 -0
  17. data/models/asciichem/Group.lutaml +22 -0
  18. data/models/asciichem/Identifier.lutaml +56 -0
  19. data/models/asciichem/Mechanism.lutaml +22 -0
  20. data/models/asciichem/Molecule.lutaml +25 -0
  21. data/models/asciichem/Name.lutaml +15 -0
  22. data/models/asciichem/Provenance.lutaml +20 -0
  23. data/models/asciichem/Reaction.lutaml +36 -0
  24. data/models/asciichem/ReactionCascade.lutaml +6 -0
  25. data/models/asciichem/Spectrum.lutaml +31 -0
  26. data/models/asciichem/SubstanceRecord.lutaml +52 -0
  27. data/models/asciichem/Text.lutaml +9 -0
  28. data/models/asciichem/ZMatrix.lutaml +34 -0
  29. data/schemas/v1/atom.yaml +45 -0
  30. data/schemas/v1/bond.yaml +20 -0
  31. data/schemas/v1/calculation.yaml +37 -0
  32. data/schemas/v1/crystal.yaml +30 -0
  33. data/schemas/v1/electron-configuration.yaml +43 -0
  34. data/schemas/v1/embedded-math.yaml +18 -0
  35. data/schemas/v1/examples/01-atom-isotope.yaml +7 -0
  36. data/schemas/v1/examples/02-molecule-water.yaml +18 -0
  37. data/schemas/v1/examples/03-group-multiplicity.yaml +11 -0
  38. data/schemas/v1/examples/04-reaction-conditions.yaml +30 -0
  39. data/schemas/v1/examples/05-substance-record-aspirin.yaml +40 -0
  40. data/schemas/v1/examples/06-electron-configuration.yaml +16 -0
  41. data/schemas/v1/examples/07-spectrum.yaml +18 -0
  42. data/schemas/v1/examples/99-01-negative-atom-bad-element.yaml +5 -0
  43. data/schemas/v1/examples/99-02-negative-identifier-bad-convention.yaml +5 -0
  44. data/schemas/v1/examples/99-03-negative-group-empty.yaml +4 -0
  45. data/schemas/v1/formula.yaml +30 -0
  46. data/schemas/v1/group.yaml +29 -0
  47. data/schemas/v1/identifier.yaml +32 -0
  48. data/schemas/v1/mechanism.yaml +30 -0
  49. data/schemas/v1/molecule.yaml +28 -0
  50. data/schemas/v1/name.yaml +22 -0
  51. data/schemas/v1/provenance.yaml +28 -0
  52. data/schemas/v1/reaction-cascade.yaml +17 -0
  53. data/schemas/v1/reaction.yaml +34 -0
  54. data/schemas/v1/spectrum.yaml +39 -0
  55. data/schemas/v1/substance-record.yaml +57 -0
  56. data/schemas/v1/text.yaml +16 -0
  57. data/schemas/v1/types/atom.ts +12 -0
  58. data/schemas/v1/types/bond.ts +5 -0
  59. data/schemas/v1/types/calculation.ts +13 -0
  60. data/schemas/v1/types/crystal.ts +13 -0
  61. data/schemas/v1/types/electron-configuration.ts +13 -0
  62. data/schemas/v1/types/embedded-math.ts +5 -0
  63. data/schemas/v1/types/formula.ts +5 -0
  64. data/schemas/v1/types/group.ts +7 -0
  65. data/schemas/v1/types/identifier.ts +7 -0
  66. data/schemas/v1/types/index.ts +21 -0
  67. data/schemas/v1/types/mechanism.ts +9 -0
  68. data/schemas/v1/types/molecule.ts +7 -0
  69. data/schemas/v1/types/name.ts +7 -0
  70. data/schemas/v1/types/provenance.ts +8 -0
  71. data/schemas/v1/types/reaction-cascade.ts +5 -0
  72. data/schemas/v1/types/reaction.ts +11 -0
  73. data/schemas/v1/types/spectrum.ts +14 -0
  74. data/schemas/v1/types/substance-record.ts +18 -0
  75. data/schemas/v1/types/text.ts +5 -0
  76. data/schemas/v1/types/zmatrix.ts +13 -0
  77. data/schemas/v1/zmatrix.yaml +41 -0
  78. metadata +136 -0
checksums.yaml ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA256:
3
+ metadata.gz: bba211156f3a53f7dd9988918fd567d7e0d396f16bf9a22f92016c5fd27eda5a
4
+ data.tar.gz: e6c09514b77410c3e55a17dad56b50fa3fb1be870c3172dc9250224c171224b3
5
+ SHA512:
6
+ metadata.gz: 9749212ecff6cac96f1a307446ee485ad456dcc022546cac3a4f00cc04374c93ab3693607b6d5a77e665323d9deb8fa160e7239d04e457f1b68461b816693f83
7
+ data.tar.gz: 848fffe56cb6d8a494431899b4d43a5bd200fd68d54bb61df2fbd7147d62bacdeb5b4f592f9fe0b4fb10d4bb1ee038ef85e5eeb2d507f9cc529504acf2df672e
data/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 AsciiChem contributors
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.
data/README.adoc ADDED
@@ -0,0 +1,80 @@
1
+ = AsciiChem Semantic Model
2
+
3
+ == General
4
+
5
+ This repository defines the **normative semantic model** for the
6
+ AsciiChem ecosystem: what a parsed chemical *means*, independent of any
7
+ implementation language. Every implementation —
8
+ https://github.com/asciichem/asciichem-ruby[asciichem-ruby] (reference),
9
+ the planned TypeScript and Python APIs, any future Rust core — consumes
10
+ these definitions and conforms to the shared corpus in
11
+ `asciichem-tests`.
12
+
13
+ The architecture follows the proven
14
+ https://github.com/glossarist/concept-model[glossarist concept-model]
15
+ pattern: LutaML model definitions as the normative source, versioned
16
+ JSON Schemas as the machine-checkable form, numbered examples (positive
17
+ and negative) that seed the conformance corpus, and generated types for
18
+ downstream languages.
19
+
20
+ The canonical serialisation is a JSON wire form with a node `type`
21
+ discriminator; every implementation must parse and emit it. Identifier
22
+ conventions (CAS RN, InChI, InChIKey, SMILES, …) carry format
23
+ constraints and provenance so that substance identity — validate,
24
+ resolve, cite — is first-class.
25
+
26
+ == Layout
27
+
28
+ [cols="1,3"]
29
+ |===
30
+ |Path |Contents
31
+
32
+ |`models/asciichem/*.lutaml` |Normative LutaML class/data-type definitions
33
+ |`schemas/v1/*.yaml` |JSON Schema (draft 2020-12) in YAML, one per node type, repo-pinned `$id`
34
+ |`schemas/v1/examples/` |Numbered instance examples (positive now; negative-examples validator lands with the generator work)
35
+ |`lib/` |Gem packaging: schema self-checks and the schema→TypeScript type generator
36
+ |`docs/adr/` |Architecture decision records
37
+ |===
38
+
39
+ == Status
40
+
41
+ Scaffold (2026-09-09): seed schemas for `atom`, `molecule`,
42
+ `identifier`. The full node inventory (`formula`, `group`, `bond`,
43
+ `reaction`, … plus the identity model `provenance`/`substance-record`)
44
+ is tracked in the ecosystem backlog
45
+ (`asciichem.github.io/TODO.impl/30`, `31`, `32`).
46
+
47
+ == Versioning and releases
48
+
49
+ This repo follows semantic versioning. Consumers (asciichem-ruby,
50
+ asciichem-ts, asciichem-py) vendor or depend on the artifacts and pin
51
+ to tags.
52
+
53
+ [cols="1,3",options="header"]
54
+ |===
55
+ |Bump |When
56
+
57
+ |MAJOR (vN+1.0.0)
58
+ |Removed or renamed shape, class, property, prefix binding, or enum value
59
+
60
+ |MINOR (vN.N+1.0)
61
+ |New shapes, classes, properties, enum values; backward-compatible additive
62
+
63
+ |PATCH (vN.N.N+1)
64
+ |Shape bug fixes, doc/comment-only changes, example updates
65
+ |===
66
+
67
+ The model version appears in the wire form root
68
+ (`"modelVersion": "1.x"`). Breaking model changes are gated here, not
69
+ in each implementation.
70
+
71
+ == Consumer protocol
72
+
73
+ - **Ruby** (asciichem-ruby): depends on the `asciichem-model` gem;
74
+ model classes serialise via `lutaml-model` mappings against the
75
+ schemas — never hand-rolled `to_h`.
76
+ - **TypeScript/JS** (asciichem-ts): vendors `schemas/` via a
77
+ `sync-asciichem-model` script pinned to a git tag; types are
78
+ generated from the schemas (single source of truth → typed output).
79
+ - **Python** (asciichem-py): consumes the schemas directly or via
80
+ generated dataclasses.
@@ -0,0 +1,78 @@
1
+ = ADR-0001: Language-binding strategy
2
+
3
+ Date: 2026-09-09
4
+ Status: Accepted
5
+
6
+ == Context
7
+
8
+ AsciiChem needs TypeScript and Python APIs in addition to the Ruby
9
+ reference implementation. Wrapping the Ruby gem (shelling out,
10
+ transpiling) is a non-option: the gem is the reference implementation,
11
+ not a runtime dependency for other ecosystems. Two viable shapes:
12
+
13
+ *A — Per-language implementations against the shared model + corpus*
14
+ (the CommonMark pattern):: Each language implements the parser, model,
15
+ and formatters natively, conforming to `asciichem-model` schemas and
16
+ passing the `asciichem-tests` corpus.
17
+
18
+ *B — One Rust core, generated/FFI bindings per language* (the tiktoken
19
+ / fastembed pattern):: Parser + model + formatters live in Rust; Ruby
20
+ uses rb-sys/magnus, Python PyO3/maturin, TypeScript napi-rs, the
21
+ browser wasm-bindgen.
22
+
23
+ == Analysis
24
+
25
+ [cols="1,2,2",options="header"]
26
+ |===
27
+ |Dimension |A — per-language |B — Rust core
28
+
29
+ |Parity risk
30
+ |Real; mitigated by the corpus, needs discipline
31
+ |Minimal — one implementation
32
+
33
+ |Effort
34
+ |~2× parser work (TS, PY); Ruby untouched
35
+ |1× Rust rewrite + 3 bindings + native CI matrix; would eventually replace the parslet *reference* parser
36
+
37
+ |Ruby story
38
+ |None required
39
+ |Large, risky migration of the reference implementation
40
+
41
+ |Deployment
42
+ |Pure-language packages
43
+ |Native artifacts per platform/arch; compiled gem extension
44
+
45
+ |Performance
46
+ |Fine at document scale (parslet is not fast and it does not matter)
47
+ |Better, but performance is not a requirement
48
+
49
+ |Browser
50
+ |TS API suffices for text/MathML
51
+ |WASM enables client-side structural SVG later
52
+ |===
53
+
54
+ == Decision
55
+
56
+ **A now; B deferred with explicit triggers.** Ship TypeScript and
57
+ Python as native implementations conforming to the shared model and
58
+ corpus. The single-parser invariant ("the site never reimplements the
59
+ parser") evolves into a *single-contract* rule: every implementation
60
+ passes the same corpus, verified in CI via each implementation's
61
+ `conformance.json` (implementation, version, corpus version, per-level
62
+ pass/total).
63
+
64
+ Revisit (and only then activate the parked `11-rust-core.md` plan)
65
+ when any of:
66
+
67
+ . Performance becomes a stated requirement.
68
+ . A third or fourth language lands and per-language drift cost exceeds
69
+ one Rust rewrite plus bindings.
70
+ . Client-side WASM needs exceed what the TS API provides (e.g.
71
+ structural SVG in the playground).
72
+
73
+ == Consequences
74
+
75
+ - `asciichem-model` and `asciichem-tests` are load-bearing: conformance
76
+ claims are only as strong as the corpus.
77
+ - The TS/PY scaffolds (TODO.impl 46/47) proceed under strategy A.
78
+ - No Rust work starts as a side effect of any other TODO.
@@ -0,0 +1,6 @@
1
+ #!/usr/bin/env ruby
2
+ # frozen_string_literal: true
3
+
4
+ require "asciichem_model"
5
+
6
+ exit AsciiChemModel::SchemaTypeGenerator.new(check_mode: ARGV.include?("--check")).run
@@ -0,0 +1,163 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "pathname"
4
+ require "yaml"
5
+
6
+ module AsciiChemModel
7
+ # Emits TypeScript interfaces from the v1 JSON Schemas — single
8
+ # source of truth (schemas) → typed output for downstream TS
9
+ # consumers. Glossarist's concept-model generator is the
10
+ # architectural reference.
11
+ #
12
+ # Supports the schema features this repository uses: type, const,
13
+ # enum, required, properties, items, anyOf, $ref (sibling files),
14
+ # and object-valued properties (emitted as inline structural
15
+ # types). `check_mode` compares against committed output so CI
16
+ # fails on drift.
17
+ class SchemaTypeGenerator
18
+ SCHEMAS_DIR = Pathname.new(File.join(AsciiChemModel.root, "schemas", "v1"))
19
+ OUTPUT_DIR = SCHEMAS_DIR.join("types")
20
+
21
+ # Schema basenames whose model class spelling differs from the
22
+ # mechanical kebab→Camel conversion.
23
+ NAME_EXCEPTIONS = { "zmatrix" => "ZMatrix" }.freeze
24
+
25
+ class << self
26
+ def type_name(schema_basename)
27
+ NAME_EXCEPTIONS.fetch(schema_basename) do
28
+ schema_basename.split("-").map(&:capitalize).join
29
+ end
30
+ end
31
+ end
32
+
33
+ def initialize(check_mode: false)
34
+ @check_mode = check_mode
35
+ end
36
+
37
+ def run
38
+ OUTPUT_DIR.mkpath
39
+ schema_files.each { |path| emit_one(path) }
40
+ emit_index
41
+ @check_mode ? fail_if_drift : 0
42
+ end
43
+
44
+ private
45
+
46
+ def schema_files
47
+ Dir[SCHEMAS_DIR.join("*.yaml").to_s].sort
48
+ end
49
+
50
+ def emit_one(path)
51
+ basename = File.basename(path, ".yaml")
52
+ schema = YAML.safe_load_file(path)
53
+ write_output("#{basename}.ts", render_header(basename) + render_schema(schema))
54
+ end
55
+
56
+ def emit_index
57
+ names = schema_files.map { |p| self.class.type_name(File.basename(p, ".yaml")) }
58
+ body = +render_comment("Union of every node type in the v1 model.")
59
+ body << "export type Node =\n"
60
+ body << names.map { |n| " | #{n}" }.join("\n")
61
+ body << ";\n"
62
+ write_output("index.ts", body)
63
+ end
64
+
65
+ def render_header(basename)
66
+ render_comment("Generated from schemas/v1/#{basename}.yaml — do not edit; regenerate.")
67
+ end
68
+
69
+ def render_schema(schema)
70
+ name = self.class.type_name(schema["$id"].split("/").last)
71
+ out = +"export interface #{name} {\n"
72
+ required = schema.fetch("required", [])
73
+ props = schema.fetch("properties", {})
74
+ out << render_property("type", props.fetch("type"), true, name) if props.key?("type")
75
+ props.reject { |key, _| key == "type" }.each do |key, prop|
76
+ out << render_property(ts_key(key), prop, required.include?(key), name)
77
+ end
78
+ out << "}\n"
79
+ end
80
+
81
+ def render_property(key, prop, required, scope)
82
+ optional = required ? "" : "?"
83
+ "#{indent}readonly #{key}#{optional}: #{ts_type(prop, scope)};\n"
84
+ end
85
+
86
+ def ts_type(prop, scope)
87
+ return ts_const(prop["const"]) if prop["const"]
88
+ return ts_enum(prop["enum"]) if prop["enum"]
89
+ return ts_ref(prop["$ref"], scope) if prop["$ref"]
90
+ return ts_array(prop, scope) if prop["type"] == "array"
91
+ return ts_inline_object(prop, scope) if prop["type"] == "object"
92
+
93
+ case prop["type"]
94
+ when "number" then "number"
95
+ when "integer" then "number"
96
+ else "string"
97
+ end
98
+ end
99
+
100
+ def ts_const(value)
101
+ value.is_a?(String) ? %("#{value}") : value.to_s
102
+ end
103
+
104
+ def ts_enum(values)
105
+ values.map { |v| ts_const(v) }.join(" | ")
106
+ end
107
+
108
+ def ts_ref(ref, _scope)
109
+ self.class.type_name(File.basename(ref.to_s, ".yaml"))
110
+ end
111
+
112
+ def ts_array(prop, scope)
113
+ items = prop["items"]
114
+ inner = if items.key?("anyOf")
115
+ "(#{items["anyOf"].map { |sub| ts_type(sub, scope) }.uniq.join(" | ")})"
116
+ else
117
+ ts_type(items, scope)
118
+ end
119
+ "#{inner}[]"
120
+ end
121
+
122
+ def ts_inline_object(prop, scope)
123
+ required = prop.fetch("required", [])
124
+ members = prop.fetch("properties", {}).map do |key, sub|
125
+ optional = required.include?(key) ? "" : "?"
126
+ "#{indent}#{ts_key(key)}#{optional}: #{ts_type(sub, scope)};"
127
+ end
128
+ "{\n#{members.map { |m| " #{m}" }.join("\n")}\n#{indent}}"
129
+ end
130
+
131
+ def ts_key(key)
132
+ return %("#{key}") unless key.match?(/\A[A-Za-z_$][A-Za-z0-9_$]*\z/)
133
+
134
+ key
135
+ end
136
+
137
+ def indent
138
+ " "
139
+ end
140
+
141
+ def render_comment(text)
142
+ "// #{text}\n"
143
+ end
144
+
145
+ def write_output(filename, content)
146
+ path = OUTPUT_DIR.join(filename)
147
+ @written ||= {}
148
+ @written[filename] = content
149
+ File.write(path, content) unless @check_mode
150
+ end
151
+
152
+ def fail_if_drift
153
+ drifted = @written.select do |filename, content|
154
+ path = OUTPUT_DIR.join(filename)
155
+ !File.exist?(path) || File.read(path) != content
156
+ end
157
+ return 0 if drifted.empty?
158
+
159
+ warn "Generated types drifted from schemas/v1/types (run `rake generate:types`): #{drifted.keys.join(', ')}"
160
+ 1
161
+ end
162
+ end
163
+ end
@@ -0,0 +1,79 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "json_schemer"
4
+ require "yaml"
5
+
6
+ module AsciiChemModel
7
+ # Schema self-checks for the model repository: every schema is
8
+ # loadable and structurally coherent, positive examples validate
9
+ # against their node's schema, and negative examples (99-*) are
10
+ # rejected. The same machinery serves downstream consumers that
11
+ # want to validate canonical-JSON payloads against the shipped
12
+ # schemas.
13
+ module Validators
14
+ SCHEMAS_DIR = File.join(AsciiChemModel.root, "schemas", "v1")
15
+ NEGATIVE_MARKER = "99-".freeze
16
+ private_constant :NEGATIVE_MARKER
17
+
18
+ class << self
19
+ # A hash of schema name (file basename) => JSONSchemer schema,
20
+ # with sibling-file $refs resolvable.
21
+ def schemers
22
+ @schemers ||= schema_files.to_h do |path|
23
+ [File.basename(path, ".yaml"), schemer_for(path)]
24
+ end
25
+ end
26
+
27
+ def schema_names
28
+ schemers.keys.sort
29
+ end
30
+
31
+ def schema_file(name)
32
+ File.join(SCHEMAS_DIR, "#{name}.yaml")
33
+ end
34
+
35
+ # Validates a wire-form node hash against its node schema
36
+ # (chosen by the `type` discriminator). Returns an Array of
37
+ # error strings (empty when valid); raises KeyError for an
38
+ # unknown schema name.
39
+ def validate(node)
40
+ schema_name = node.fetch("type").tr("_", "-")
41
+ schemer = schemers.fetch(schema_name) do
42
+ raise KeyError, "no schema for node type #{schema_name.inspect}"
43
+ end
44
+ schemer.validate(node).map(&:to_s)
45
+ end
46
+
47
+ def example_files
48
+ Dir[File.join(SCHEMAS_DIR, "examples", "*.yaml")].sort
49
+ end
50
+
51
+ def positive_examples
52
+ example_files.reject { |p| File.basename(p).start_with?(NEGATIVE_MARKER) }
53
+ end
54
+
55
+ def negative_examples
56
+ example_files.select { |p| File.basename(p).start_with?(NEGATIVE_MARKER) }
57
+ end
58
+
59
+ private
60
+
61
+ def schema_files
62
+ Dir[File.join(SCHEMAS_DIR, "*.yaml")].sort
63
+ end
64
+
65
+ def schemer_for(path)
66
+ JSONSchemer.schema(YAML.safe_load_file(path), ref_resolver: method(:resolve_ref))
67
+ end
68
+
69
+ # Sibling-file $refs ("atom.yaml") resolve against the schemas
70
+ # directory; external pointers are not supported by design.
71
+ def resolve_ref(uri)
72
+ candidate = File.join(SCHEMAS_DIR, File.basename(uri.to_s))
73
+ return YAML.safe_load_file(candidate) if File.exist?(candidate)
74
+
75
+ raise KeyError, "unresolvable $ref #{uri} (only sibling schema files are supported)"
76
+ end
77
+ end
78
+ end
79
+ end
@@ -0,0 +1,5 @@
1
+ # frozen_string_literal: true
2
+
3
+ module AsciiChemModel
4
+ VERSION = "0.3.2"
5
+ end
@@ -0,0 +1,18 @@
1
+ # frozen_string_literal: true
2
+
3
+ # AsciiChemModel packages the normative semantic model artifacts
4
+ # (LutaML definitions, JSON Schemas, examples) for the AsciiChem
5
+ # ecosystem. The gem's lib currently carries only the version and
6
+ # schema tooling entry points; the schema self-check suite and the
7
+ # schema→TypeScript type generator land with the backlog items
8
+ # tracked in the ecosystem TODO.impl (32).
9
+ module AsciiChemModel
10
+ autoload :VERSION, "asciichem_model/version"
11
+ autoload :SchemaTypeGenerator, "asciichem_model/schema_type_generator"
12
+ autoload :Validators, "asciichem_model/validators"
13
+
14
+ # Root path of the gem (schemas/, models/, examples/ live here).
15
+ def self.root
16
+ File.expand_path("..", __dir__)
17
+ end
18
+ end
@@ -0,0 +1,49 @@
1
+ class Atom {
2
+ definition {
3
+ A chemical atom: an element symbol with optional isotope (prefix
4
+ superscript), optional charge, optional subscript multiplicity,
5
+ and optional annotations. The defining semantic fix over AsciiMath:
6
+ a prefix superscript binds to the atom (`^14C` is
7
+ Atom(element: "C", isotope: "14")), never to a phantom carrier.
8
+ }
9
+ +element: String {
10
+ definition {
11
+ Element symbol from the periodic table (e.g. "C", "Cl").
12
+ }
13
+ }
14
+ +isotope: String {
15
+ definition {
16
+ Isotope mass number bound to this atom (e.g. "14" in `^14C`).
17
+ }
18
+ }
19
+ +charge: String {
20
+ definition {
21
+ Charge annotation (e.g. "2+" in `Ca^2+`).
22
+ }
23
+ }
24
+ +subscript: String {
25
+ definition {
26
+ Subscript multiplicity (e.g. "2" in `H_2O`).
27
+ }
28
+ }
29
+ +oxidationState: String {
30
+ definition {
31
+ Oxidation state annotation ("II" in Ca^(II)).
32
+ }
33
+ }
34
+ +lonePairs: Integer {
35
+ definition {
36
+ Lewis lone-pair markers ("::O" carries 2).
37
+ }
38
+ }
39
+ +radicalElectrons: Integer {
40
+ definition {
41
+ Radical-electron markers ("N." carries 1).
42
+ }
43
+ }
44
+ +ringClosures: String {
45
+ definition {
46
+ SMILES-style ring closure digits ("1" in C1...C1).
47
+ }
48
+ }
49
+ }
@@ -0,0 +1,24 @@
1
+ class Bond {
2
+ definition {
3
+ A bond between adjacent atoms in a linear structure. The kind
4
+ enum is the single source of truth shared by all implementations.
5
+ }
6
+ +kind: BondKind {
7
+ definition { The bond kind; single by default. }
8
+ }
9
+ }
10
+
11
+ enum BondKind {
12
+ definition {
13
+ single (-), double (=), triple (#), quadruple (##), wedge (>-),
14
+ hash (-<), dative (~>), wavy (~~).
15
+ }
16
+ single {}
17
+ double {}
18
+ triple {}
19
+ quadruple {}
20
+ wedge {}
21
+ hash {}
22
+ dative {}
23
+ wavy {}
24
+ }
@@ -0,0 +1,34 @@
1
+ class Calculation {
2
+ definition {
3
+ A computational-chemistry result: method, basis set, and
4
+ computed properties.
5
+ }
6
+ +method: String {
7
+ definition { The computational method, e.g. "B3LYP". }
8
+ }
9
+ +basis: String {
10
+ definition { The basis set, e.g. "6-31G(d)". }
11
+ }
12
+ +properties: CalculatedProperty[0..*] {
13
+ definition { The computed properties. }
14
+ }
15
+
16
+ class CalculatedProperty {
17
+ definition { One computed property with units and provenance keys. }
18
+ +title: String {
19
+ definition { Property name, e.g. "dipole". }
20
+ }
21
+ +value: String {
22
+ definition { The computed value. }
23
+ }
24
+ +units: String {
25
+ definition { Units of the value. }
26
+ }
27
+ +dictRef: String {
28
+ definition { Optional dictionary reference. }
29
+ }
30
+ +convention: String {
31
+ definition { Optional convention marker. }
32
+ }
33
+ }
34
+ }
@@ -0,0 +1,34 @@
1
+ class Crystal {
2
+ definition {
3
+ A crystallographic cell: unit-cell constants, space group, and
4
+ the asymmetric-unit atoms (fractional coordinates ride on the
5
+ atoms' coordinate annotations).
6
+ }
7
+ +name: String {
8
+ definition { Mineral or compound name. }
9
+ }
10
+ +a: Real {
11
+ definition { Cell length a in Angstrom. }
12
+ }
13
+ +b: Real {
14
+ definition { Cell length b. }
15
+ }
16
+ +c: Real {
17
+ definition { Cell length c. }
18
+ }
19
+ +alpha: Real {
20
+ definition { Cell angle alpha in degrees. }
21
+ }
22
+ +beta: Real {
23
+ definition { Cell angle beta. }
24
+ }
25
+ +gamma: Real {
26
+ definition { Cell angle gamma. }
27
+ }
28
+ +spacegroup: String {
29
+ definition { Hermann-Mauguin symbol, e.g. "Fm-3m". }
30
+ }
31
+ +atoms: Atom[0..*] {
32
+ definition { Asymmetric-unit atoms. }
33
+ }
34
+ }
@@ -0,0 +1,35 @@
1
+ class ElectronConfiguration {
2
+ definition {
3
+ Orbital occupancy (1s^2 2s^2 2p^6) with an optional atomic term
4
+ symbol (^{multiplicity}L_J).
5
+ }
6
+ +orbitals: OrbitalOccupancy[0..*] {
7
+ definition { Ordered orbital occupancies. }
8
+ }
9
+ +termSymbol: TermSymbol {
10
+ definition { Optional atomic term symbol. }
11
+ }
12
+
13
+ class OrbitalOccupancy {
14
+ definition { One orbital and its electron count. }
15
+ +orbital: String {
16
+ definition { Orbital label, e.g. "1s", "2p". }
17
+ }
18
+ +occupancy: String {
19
+ definition { Electron count in this orbital. }
20
+ }
21
+ }
22
+
23
+ class TermSymbol {
24
+ definition { Atomic term symbol: spin multiplicity, letter, J. }
25
+ +multiplicity: String {
26
+ definition { Spin multiplicity (2S+1). }
27
+ }
28
+ +letter: String {
29
+ definition { Orbital angular momentum letter (S, P, D, ...). }
30
+ }
31
+ +jValue: String {
32
+ definition { Total angular momentum J. }
33
+ }
34
+ }
35
+ }
@@ -0,0 +1,10 @@
1
+ class EmbeddedMath {
2
+ definition {
3
+ Math embedded in chemistry (backtick syntax). The wire form
4
+ carries the AsciiMath source; the parsed formula is an
5
+ implementation detail.
6
+ }
7
+ +source: String {
8
+ definition { The backticked AsciiMath source text. }
9
+ }
10
+ }
@@ -0,0 +1,13 @@
1
+ class Formula {
2
+ definition {
3
+ The document root of a parsed AsciiChem input: an ordered
4
+ sequence of top-level nodes. Every implementation emits this as
5
+ the canonical wire form's root.
6
+ }
7
+ +nodes: Node[0..*] {
8
+ definition {
9
+ Top-level nodes: molecules, reactions, cascades, groups,
10
+ electron configurations, embedded math, or text.
11
+ }
12
+ }
13
+ }