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.
- checksums.yaml +7 -0
- data/LICENSE +21 -0
- data/README.adoc +80 -0
- data/docs/adr/0001-language-bindings.adoc +78 -0
- data/exe/generate-types-from-schemas +6 -0
- data/lib/asciichem_model/schema_type_generator.rb +163 -0
- data/lib/asciichem_model/validators.rb +79 -0
- data/lib/asciichem_model/version.rb +5 -0
- data/lib/asciichem_model.rb +18 -0
- data/models/asciichem/Atom.lutaml +49 -0
- data/models/asciichem/Bond.lutaml +24 -0
- data/models/asciichem/Calculation.lutaml +34 -0
- data/models/asciichem/Crystal.lutaml +34 -0
- data/models/asciichem/ElectronConfiguration.lutaml +35 -0
- data/models/asciichem/EmbeddedMath.lutaml +10 -0
- data/models/asciichem/Formula.lutaml +13 -0
- data/models/asciichem/Group.lutaml +22 -0
- data/models/asciichem/Identifier.lutaml +56 -0
- data/models/asciichem/Mechanism.lutaml +22 -0
- data/models/asciichem/Molecule.lutaml +25 -0
- data/models/asciichem/Name.lutaml +15 -0
- data/models/asciichem/Provenance.lutaml +20 -0
- data/models/asciichem/Reaction.lutaml +36 -0
- data/models/asciichem/ReactionCascade.lutaml +6 -0
- data/models/asciichem/Spectrum.lutaml +31 -0
- data/models/asciichem/SubstanceRecord.lutaml +52 -0
- data/models/asciichem/Text.lutaml +9 -0
- data/models/asciichem/ZMatrix.lutaml +34 -0
- data/schemas/v1/atom.yaml +45 -0
- data/schemas/v1/bond.yaml +20 -0
- data/schemas/v1/calculation.yaml +37 -0
- data/schemas/v1/crystal.yaml +30 -0
- data/schemas/v1/electron-configuration.yaml +43 -0
- data/schemas/v1/embedded-math.yaml +18 -0
- data/schemas/v1/examples/01-atom-isotope.yaml +7 -0
- data/schemas/v1/examples/02-molecule-water.yaml +18 -0
- data/schemas/v1/examples/03-group-multiplicity.yaml +11 -0
- data/schemas/v1/examples/04-reaction-conditions.yaml +30 -0
- data/schemas/v1/examples/05-substance-record-aspirin.yaml +40 -0
- data/schemas/v1/examples/06-electron-configuration.yaml +16 -0
- data/schemas/v1/examples/07-spectrum.yaml +18 -0
- data/schemas/v1/examples/99-01-negative-atom-bad-element.yaml +5 -0
- data/schemas/v1/examples/99-02-negative-identifier-bad-convention.yaml +5 -0
- data/schemas/v1/examples/99-03-negative-group-empty.yaml +4 -0
- data/schemas/v1/formula.yaml +30 -0
- data/schemas/v1/group.yaml +29 -0
- data/schemas/v1/identifier.yaml +32 -0
- data/schemas/v1/mechanism.yaml +30 -0
- data/schemas/v1/molecule.yaml +28 -0
- data/schemas/v1/name.yaml +22 -0
- data/schemas/v1/provenance.yaml +28 -0
- data/schemas/v1/reaction-cascade.yaml +17 -0
- data/schemas/v1/reaction.yaml +34 -0
- data/schemas/v1/spectrum.yaml +39 -0
- data/schemas/v1/substance-record.yaml +57 -0
- data/schemas/v1/text.yaml +16 -0
- data/schemas/v1/types/atom.ts +12 -0
- data/schemas/v1/types/bond.ts +5 -0
- data/schemas/v1/types/calculation.ts +13 -0
- data/schemas/v1/types/crystal.ts +13 -0
- data/schemas/v1/types/electron-configuration.ts +13 -0
- data/schemas/v1/types/embedded-math.ts +5 -0
- data/schemas/v1/types/formula.ts +5 -0
- data/schemas/v1/types/group.ts +7 -0
- data/schemas/v1/types/identifier.ts +7 -0
- data/schemas/v1/types/index.ts +21 -0
- data/schemas/v1/types/mechanism.ts +9 -0
- data/schemas/v1/types/molecule.ts +7 -0
- data/schemas/v1/types/name.ts +7 -0
- data/schemas/v1/types/provenance.ts +8 -0
- data/schemas/v1/types/reaction-cascade.ts +5 -0
- data/schemas/v1/types/reaction.ts +11 -0
- data/schemas/v1/types/spectrum.ts +14 -0
- data/schemas/v1/types/substance-record.ts +18 -0
- data/schemas/v1/types/text.ts +5 -0
- data/schemas/v1/types/zmatrix.ts +13 -0
- data/schemas/v1/zmatrix.yaml +41 -0
- 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,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,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
|
+
}
|