chemicalml 0.1.0

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 (248) hide show
  1. checksums.yaml +7 -0
  2. data/.gitignore +11 -0
  3. data/.rubocop.yml +20 -0
  4. data/CHANGELOG.md +21 -0
  5. data/CLAUDE.md +198 -0
  6. data/Gemfile +14 -0
  7. data/LICENSE +24 -0
  8. data/NOTICES.adoc +115 -0
  9. data/README.adoc +87 -0
  10. data/Rakefile +8 -0
  11. data/TODO.align/01-cml-base-mixins.md +122 -0
  12. data/TODO.align/02-real-schema-hierarchies.md +88 -0
  13. data/TODO.align/03-configuration.md +50 -0
  14. data/TODO.align/04-parse-entry-points.md +59 -0
  15. data/TODO.align/05-schema-aware-translator.md +33 -0
  16. data/TODO.align/06-no-respond-to.md +44 -0
  17. data/TODO.align/07-canon-xml-comparison.md +43 -0
  18. data/TODO.align/08-expand-fixtures.md +37 -0
  19. data/TODO.align/09-final-spec-lint.md +29 -0
  20. data/TODO.align/10-role-marker-modules.md +54 -0
  21. data/TODO.align/11-dry-configuration.md +63 -0
  22. data/TODO.align/12-wire-class-macro.md +51 -0
  23. data/TODO.align/13-wire-namespace.md +44 -0
  24. data/TODO.align/14-schema24-correctness.md +37 -0
  25. data/TODO.align/15-canon-fixture-roundtrip.md +38 -0
  26. data/TODO.align/16-schema24-convention-coverage.md +33 -0
  27. data/TODO.align/17-final-spec-lint.md +28 -0
  28. data/TODO.align/README.md +27 -0
  29. data/TODO.align/README.round2.md +24 -0
  30. data/TODO.cml-full/01-reference-docs.md +43 -0
  31. data/TODO.cml-full/02-schema-versioned-model.md +70 -0
  32. data/TODO.cml-full/03-fixtures-scrape.md +52 -0
  33. data/TODO.cml-full/04-convention-framework.md +69 -0
  34. data/TODO.cml-full/05-dictionary-layer.md +77 -0
  35. data/TODO.cml-full/06-extend-canonical-translator.md +60 -0
  36. data/TODO.cml-full/07-attribution-notices.md +42 -0
  37. data/TODO.cml-full/08-autoload-only.md +48 -0
  38. data/TODO.cml-full/09-spec-coverage.md +38 -0
  39. data/TODO.cml-full/README.md +23 -0
  40. data/chemicalml.gemspec +35 -0
  41. data/data/dictionaries/_index.yaml +14 -0
  42. data/data/dictionaries/compchem.yaml +481 -0
  43. data/data/dictionaries/unit_non_si.yaml +53 -0
  44. data/data/dictionaries/unit_si.yaml +87 -0
  45. data/data/dictionaries/unit_type.yaml +113 -0
  46. data/lib/chemicalml/cml/array.rb +10 -0
  47. data/lib/chemicalml/cml/atom.rb +10 -0
  48. data/lib/chemicalml/cml/atom_array.rb +10 -0
  49. data/lib/chemicalml/cml/atom_parity.rb +10 -0
  50. data/lib/chemicalml/cml/base/array.rb +36 -0
  51. data/lib/chemicalml/cml/base/atom.rb +38 -0
  52. data/lib/chemicalml/cml/base/atom_array.rb +22 -0
  53. data/lib/chemicalml/cml/base/atom_parity.rb +24 -0
  54. data/lib/chemicalml/cml/base/bond.rb +28 -0
  55. data/lib/chemicalml/cml/base/bond_array.rb +22 -0
  56. data/lib/chemicalml/cml/base/bond_stereo.rb +28 -0
  57. data/lib/chemicalml/cml/base/cml_module.rb +40 -0
  58. data/lib/chemicalml/cml/base/dictionary.rb +32 -0
  59. data/lib/chemicalml/cml/base/dictionary_entry.rb +34 -0
  60. data/lib/chemicalml/cml/base/document.rb +26 -0
  61. data/lib/chemicalml/cml/base/formula.rb +38 -0
  62. data/lib/chemicalml/cml/base/identifier.rb +26 -0
  63. data/lib/chemicalml/cml/base/label.rb +28 -0
  64. data/lib/chemicalml/cml/base/list.rb +36 -0
  65. data/lib/chemicalml/cml/base/matrix.rb +38 -0
  66. data/lib/chemicalml/cml/base/metadata.rb +30 -0
  67. data/lib/chemicalml/cml/base/metadata_list.rb +28 -0
  68. data/lib/chemicalml/cml/base/molecule.rb +40 -0
  69. data/lib/chemicalml/cml/base/name.rb +26 -0
  70. data/lib/chemicalml/cml/base/parameter.rb +34 -0
  71. data/lib/chemicalml/cml/base/parameter_list.rb +28 -0
  72. data/lib/chemicalml/cml/base/product.rb +22 -0
  73. data/lib/chemicalml/cml/base/product_list.rb +22 -0
  74. data/lib/chemicalml/cml/base/property.rb +34 -0
  75. data/lib/chemicalml/cml/base/property_list.rb +28 -0
  76. data/lib/chemicalml/cml/base/reactant.rb +22 -0
  77. data/lib/chemicalml/cml/base/reactant_list.rb +22 -0
  78. data/lib/chemicalml/cml/base/reaction.rb +32 -0
  79. data/lib/chemicalml/cml/base/reaction_list.rb +22 -0
  80. data/lib/chemicalml/cml/base/scalar.rb +32 -0
  81. data/lib/chemicalml/cml/base/substance.rb +30 -0
  82. data/lib/chemicalml/cml/base/unit.rb +38 -0
  83. data/lib/chemicalml/cml/base/unit_list.rb +30 -0
  84. data/lib/chemicalml/cml/base/unit_type.rb +28 -0
  85. data/lib/chemicalml/cml/base/unit_type_list.rb +30 -0
  86. data/lib/chemicalml/cml/base.rb +51 -0
  87. data/lib/chemicalml/cml/bond.rb +10 -0
  88. data/lib/chemicalml/cml/bond_array.rb +10 -0
  89. data/lib/chemicalml/cml/bond_stereo.rb +10 -0
  90. data/lib/chemicalml/cml/cml_module.rb +10 -0
  91. data/lib/chemicalml/cml/dictionary.rb +10 -0
  92. data/lib/chemicalml/cml/dictionary_entry.rb +10 -0
  93. data/lib/chemicalml/cml/document.rb +10 -0
  94. data/lib/chemicalml/cml/elements.rb +58 -0
  95. data/lib/chemicalml/cml/formula.rb +10 -0
  96. data/lib/chemicalml/cml/identifier.rb +10 -0
  97. data/lib/chemicalml/cml/label.rb +10 -0
  98. data/lib/chemicalml/cml/list.rb +10 -0
  99. data/lib/chemicalml/cml/matrix.rb +10 -0
  100. data/lib/chemicalml/cml/metadata.rb +10 -0
  101. data/lib/chemicalml/cml/metadata_list.rb +10 -0
  102. data/lib/chemicalml/cml/molecule.rb +10 -0
  103. data/lib/chemicalml/cml/name.rb +10 -0
  104. data/lib/chemicalml/cml/namespace.rb +17 -0
  105. data/lib/chemicalml/cml/parameter.rb +10 -0
  106. data/lib/chemicalml/cml/parameter_list.rb +10 -0
  107. data/lib/chemicalml/cml/product.rb +10 -0
  108. data/lib/chemicalml/cml/product_list.rb +10 -0
  109. data/lib/chemicalml/cml/property.rb +10 -0
  110. data/lib/chemicalml/cml/property_list.rb +10 -0
  111. data/lib/chemicalml/cml/reactant.rb +10 -0
  112. data/lib/chemicalml/cml/reactant_list.rb +10 -0
  113. data/lib/chemicalml/cml/reaction.rb +10 -0
  114. data/lib/chemicalml/cml/reaction_list.rb +10 -0
  115. data/lib/chemicalml/cml/role/array.rb +13 -0
  116. data/lib/chemicalml/cml/role/atom.rb +13 -0
  117. data/lib/chemicalml/cml/role/atom_array.rb +13 -0
  118. data/lib/chemicalml/cml/role/atom_parity.rb +13 -0
  119. data/lib/chemicalml/cml/role/bond.rb +13 -0
  120. data/lib/chemicalml/cml/role/bond_array.rb +13 -0
  121. data/lib/chemicalml/cml/role/bond_stereo.rb +13 -0
  122. data/lib/chemicalml/cml/role/cml_module.rb +14 -0
  123. data/lib/chemicalml/cml/role/dictionary.rb +13 -0
  124. data/lib/chemicalml/cml/role/dictionary_entry.rb +13 -0
  125. data/lib/chemicalml/cml/role/document.rb +13 -0
  126. data/lib/chemicalml/cml/role/formula.rb +13 -0
  127. data/lib/chemicalml/cml/role/identifier.rb +13 -0
  128. data/lib/chemicalml/cml/role/label.rb +13 -0
  129. data/lib/chemicalml/cml/role/list.rb +13 -0
  130. data/lib/chemicalml/cml/role/matrix.rb +13 -0
  131. data/lib/chemicalml/cml/role/metadata.rb +13 -0
  132. data/lib/chemicalml/cml/role/metadata_list.rb +13 -0
  133. data/lib/chemicalml/cml/role/molecule.rb +13 -0
  134. data/lib/chemicalml/cml/role/name.rb +13 -0
  135. data/lib/chemicalml/cml/role/parameter.rb +13 -0
  136. data/lib/chemicalml/cml/role/parameter_list.rb +13 -0
  137. data/lib/chemicalml/cml/role/product.rb +13 -0
  138. data/lib/chemicalml/cml/role/product_list.rb +13 -0
  139. data/lib/chemicalml/cml/role/property.rb +13 -0
  140. data/lib/chemicalml/cml/role/property_list.rb +13 -0
  141. data/lib/chemicalml/cml/role/reactant.rb +13 -0
  142. data/lib/chemicalml/cml/role/reactant_list.rb +13 -0
  143. data/lib/chemicalml/cml/role/reaction.rb +13 -0
  144. data/lib/chemicalml/cml/role/reaction_list.rb +13 -0
  145. data/lib/chemicalml/cml/role/scalar.rb +13 -0
  146. data/lib/chemicalml/cml/role/substance.rb +13 -0
  147. data/lib/chemicalml/cml/role/unit.rb +13 -0
  148. data/lib/chemicalml/cml/role/unit_list.rb +13 -0
  149. data/lib/chemicalml/cml/role/unit_type.rb +13 -0
  150. data/lib/chemicalml/cml/role/unit_type_list.rb +13 -0
  151. data/lib/chemicalml/cml/role.rb +54 -0
  152. data/lib/chemicalml/cml/scalar.rb +10 -0
  153. data/lib/chemicalml/cml/schema24/configuration.rb +19 -0
  154. data/lib/chemicalml/cml/schema24.rb +40 -0
  155. data/lib/chemicalml/cml/schema3/configuration.rb +17 -0
  156. data/lib/chemicalml/cml/schema3.rb +40 -0
  157. data/lib/chemicalml/cml/substance.rb +10 -0
  158. data/lib/chemicalml/cml/translator/value_translations.rb +265 -0
  159. data/lib/chemicalml/cml/translator.rb +233 -0
  160. data/lib/chemicalml/cml/unit.rb +10 -0
  161. data/lib/chemicalml/cml/unit_list.rb +10 -0
  162. data/lib/chemicalml/cml/unit_type.rb +10 -0
  163. data/lib/chemicalml/cml/unit_type_list.rb +10 -0
  164. data/lib/chemicalml/cml/visitable.rb +65 -0
  165. data/lib/chemicalml/cml/wire_class_macro.rb +45 -0
  166. data/lib/chemicalml/cml.rb +55 -0
  167. data/lib/chemicalml/context_configuration.rb +127 -0
  168. data/lib/chemicalml/convention/base.rb +48 -0
  169. data/lib/chemicalml/convention/compchem/constraints/compchem_module_must_contain_job_list.rb +33 -0
  170. data/lib/chemicalml/convention/compchem/constraints/job_must_contain_initialization.rb +38 -0
  171. data/lib/chemicalml/convention/compchem/constraints.rb +14 -0
  172. data/lib/chemicalml/convention/compchem.rb +28 -0
  173. data/lib/chemicalml/convention/constraint.rb +79 -0
  174. data/lib/chemicalml/convention/dictionary/constraints/entry_ids_unique_within_dictionary.rb +33 -0
  175. data/lib/chemicalml/convention/dictionary/constraints/entry_must_have_id_and_term.rb +34 -0
  176. data/lib/chemicalml/convention/dictionary/constraints.rb +14 -0
  177. data/lib/chemicalml/convention/dictionary.rb +28 -0
  178. data/lib/chemicalml/convention/molecular/constraints/atom_array_must_contain_atoms.rb +30 -0
  179. data/lib/chemicalml/convention/molecular/constraints/atom_ids_unique_within_molecule.rb +42 -0
  180. data/lib/chemicalml/convention/molecular/constraints/bond_must_reference_atoms_in_same_molecule.rb +54 -0
  181. data/lib/chemicalml/convention/molecular/constraints.rb +19 -0
  182. data/lib/chemicalml/convention/molecular.rb +29 -0
  183. data/lib/chemicalml/convention/registry.rb +50 -0
  184. data/lib/chemicalml/convention/unit_dictionary/constraints/unit_must_have_symbol_and_unit_type.rb +46 -0
  185. data/lib/chemicalml/convention/unit_dictionary/constraints.rb +12 -0
  186. data/lib/chemicalml/convention/unit_dictionary.rb +27 -0
  187. data/lib/chemicalml/convention/unit_type_dictionary/constraints/unit_type_must_have_id_and_name.rb +34 -0
  188. data/lib/chemicalml/convention/unit_type_dictionary/constraints.rb +12 -0
  189. data/lib/chemicalml/convention/unit_type_dictionary.rb +27 -0
  190. data/lib/chemicalml/convention/violation.rb +36 -0
  191. data/lib/chemicalml/convention.rb +39 -0
  192. data/lib/chemicalml/dictionary/entry.rb +59 -0
  193. data/lib/chemicalml/dictionary/enum.rb +51 -0
  194. data/lib/chemicalml/dictionary/link.rb +32 -0
  195. data/lib/chemicalml/dictionary/loader.rb +60 -0
  196. data/lib/chemicalml/dictionary/model.rb +64 -0
  197. data/lib/chemicalml/dictionary/registry.rb +75 -0
  198. data/lib/chemicalml/dictionary.rb +26 -0
  199. data/lib/chemicalml/errors.rb +7 -0
  200. data/lib/chemicalml/model/atom.rb +40 -0
  201. data/lib/chemicalml/model/bond.rb +33 -0
  202. data/lib/chemicalml/model/cml_array.rb +27 -0
  203. data/lib/chemicalml/model/cml_module.rb +45 -0
  204. data/lib/chemicalml/model/document.rb +29 -0
  205. data/lib/chemicalml/model/formula.rb +34 -0
  206. data/lib/chemicalml/model/identifier.rb +21 -0
  207. data/lib/chemicalml/model/label.rb +21 -0
  208. data/lib/chemicalml/model/matrix.rb +29 -0
  209. data/lib/chemicalml/model/metadata.rb +23 -0
  210. data/lib/chemicalml/model/metadata_list.rb +25 -0
  211. data/lib/chemicalml/model/molecule.rb +38 -0
  212. data/lib/chemicalml/model/name.rb +21 -0
  213. data/lib/chemicalml/model/node.rb +45 -0
  214. data/lib/chemicalml/model/parameter.rb +27 -0
  215. data/lib/chemicalml/model/parameter_list.rb +25 -0
  216. data/lib/chemicalml/model/product.rb +21 -0
  217. data/lib/chemicalml/model/product_list.rb +21 -0
  218. data/lib/chemicalml/model/property.rb +28 -0
  219. data/lib/chemicalml/model/property_list.rb +25 -0
  220. data/lib/chemicalml/model/reactant.rb +21 -0
  221. data/lib/chemicalml/model/reactant_list.rb +21 -0
  222. data/lib/chemicalml/model/reaction.rb +41 -0
  223. data/lib/chemicalml/model/reaction_list.rb +23 -0
  224. data/lib/chemicalml/model/scalar.rb +25 -0
  225. data/lib/chemicalml/model/substance.rb +25 -0
  226. data/lib/chemicalml/model.rb +38 -0
  227. data/lib/chemicalml/schema/definition.rb +41 -0
  228. data/lib/chemicalml/schema/registry.rb +30 -0
  229. data/lib/chemicalml/schema.rb +35 -0
  230. data/lib/chemicalml/version.rb +5 -0
  231. data/lib/chemicalml/versioned_parser.rb +43 -0
  232. data/lib/chemicalml.rb +47 -0
  233. data/lib/tasks/reference_fetch.rake +105 -0
  234. data/lib/tasks/scrape_fixtures.rake +133 -0
  235. data/reference-docs/conventions/compchem.md +141 -0
  236. data/reference-docs/conventions/dictionary.md +119 -0
  237. data/reference-docs/conventions/index.md +56 -0
  238. data/reference-docs/conventions/molecular.md +115 -0
  239. data/reference-docs/conventions/unit-dictionary.md +92 -0
  240. data/reference-docs/conventions/unitType-dictionary.md +51 -0
  241. data/reference-docs/dictionaries/compchem.md +335 -0
  242. data/reference-docs/dictionaries/index.md +52 -0
  243. data/reference-docs/examples/schema24-index.md +50 -0
  244. data/reference-docs/examples/schema3-compchem-index.md +14 -0
  245. data/reference-docs/examples/schema3-molecular-index.md +28 -0
  246. data/reference-docs/schemas/schema24/schema.xsd +10624 -0
  247. data/reference-docs/schemas/schema3/schema.xsd +11459 -0
  248. metadata +306 -0
@@ -0,0 +1,88 @@
1
+ # 02 — Real Schema3 / Schema24 class hierarchies
2
+
3
+ **Status:** complete
4
+ **Depends on:** 01
5
+
6
+ ## Goal
7
+
8
+ Today `Chemicalml::Cml::Schema3::Document` is just an alias for
9
+ `Chemicalml::Cml::Document` — they are literally the same class. This
10
+ means schema-version-specific behavior is impossible to add without
11
+ modifying the shared class (OCP violation).
12
+
13
+ Promote both namespaces to genuine class hierarchies. Each
14
+ schema-versioned class is a `Lutaml::Model::Serializable` subclass that
15
+ includes the corresponding `Cml::Base::*` mixin (from task 01) and may
16
+ add version-specific declarations.
17
+
18
+ ## Why
19
+
20
+ The CML Schema 2.4 and Schema 3 XSDs differ in content models —
21
+ Schema 3 loosened many of Schema 2.4's restrictions and added the
22
+ generic `<module>` element. Today's aliasing hides this. Real class
23
+ hierarchies let each version evolve independently.
24
+
25
+ ## Deliverables
26
+
27
+ - [ ] `lib/chemicalml/cml/schema3/` directory with one file per
28
+ element, each `class Foo < Lutaml::Model::Serializable;
29
+ include Base::Foo; end`
30
+ - [ ] `lib/chemicalml/cml/schema24/` directory mirroring the same set
31
+ - [ ] `lib/chemicalml/cml/schema3.rb` — namespace file with autoloads
32
+ (replaces the current alias module)
33
+ - [ ] `lib/chemicalml/cml/schema24.rb` — same
34
+ - [ ] Backward compatibility: `Chemicalml::Cml::Atom`,
35
+ `Cml::Molecule`, etc. at the top level become aliases for
36
+ `Cml::Schema3::*` (so existing user code and specs still load).
37
+ Document this in `CLAUDE.md` and `README.adoc`.
38
+
39
+ ## Schema-specific deviations to honor
40
+
41
+ - Schema 2.4 has stricter content models; some elements (e.g.
42
+ `<module>`) do not exist. `Schema24::Module` therefore does not
43
+ exist.
44
+ - Schema 3 added `<module>` and loosened `<cml>` content; that
45
+ behavior is the default in Schema3 wire classes.
46
+ - Future Schema 2.4-specific deviations are added in
47
+ `Base::Schema24Only::*` modules included only by Schema24 classes
48
+ (OCP-friendly extension point).
49
+
50
+ ## Pattern
51
+
52
+ ```ruby
53
+ # lib/chemicalml/cml/schema3/molecule.rb
54
+ module Chemicalml
55
+ module Cml
56
+ module Schema3
57
+ class Molecule < Lutaml::Model::Serializable
58
+ include Base::Molecule
59
+ end
60
+ end
61
+ end
62
+ end
63
+ ```
64
+
65
+ ```ruby
66
+ # lib/chemicalml/cml/schema24/molecule.rb (deviates: no <module> child)
67
+ module Chemicalml
68
+ module Cml
69
+ module Schema24
70
+ class Molecule < Lutaml::Model::Serializable
71
+ include Base::Molecule
72
+ # Schema 2.4 doesn't allow child <module> elements;
73
+ # the Base::Molecule mixin doesn't declare one, so this class
74
+ # is already correct. If we needed to *remove* an attribute
75
+ # the Base declares, we'd use `undef_attribute` here.
76
+ end
77
+ end
78
+ end
79
+ end
80
+ ```
81
+
82
+ ## Acceptance
83
+
84
+ - `Chemicalml::Cml::Schema3::Atom` and `Chemicalml::Cml::Schema24::Atom`
85
+ are different classes (different `object_id`).
86
+ - `Chemicalml::Cml::Atom` is an alias for `Chemicalml::Cml::Schema3::Atom`.
87
+ - All existing specs continue to pass.
88
+ - Round-trip through either schema works on the existing fixtures.
@@ -0,0 +1,50 @@
1
+ # 03 — Configuration + lutaml_default_register
2
+
3
+ **Status:** complete
4
+ **Depends on:** 02
5
+
6
+ ## Goal
7
+
8
+ Wire `lutaml-model`'s `GlobalContext` type-resolution machinery into
9
+ the schema-versioned classes. Each schema gets its own context id
10
+ (`:chemicalml_schema3`, `:chemicalml_schema24`); each wire class
11
+ declares which context it belongs to via `lutaml_default_register`.
12
+
13
+ This is the same pattern `mml` uses to let `from_xml` resolve child
14
+ element types based on the active context.
15
+
16
+ ## Why
17
+
18
+ Without context-aware type resolution, parsing `<atom>` inside a Schema3
19
+ document and inside a Schema24 document both yield the same Ruby class.
20
+ Once task 02 introduces real hierarchies, the framework needs to know
21
+ which `Atom` class to instantiate based on which schema's `Document`
22
+ is being parsed.
23
+
24
+ ## Deliverables
25
+
26
+ - [ ] `lib/chemicalml/cml/schema3/configuration.rb` —
27
+ `module Schema3::Configuration; extend Chemicalml::ContextConfiguration;
28
+ CONTEXT_ID = :chemicalml_schema3; end`
29
+ - [ ] `lib/chemicalml/cml/schema24/configuration.rb` — same shape
30
+ with `CONTEXT_ID = :chemicalml_schema24`
31
+ - [ ] `lib/chemicalml/context_configuration.rb` — shared mixin
32
+ providing `register_model`, `populate_context!`, `context`,
33
+ `default_context_id` (modeled on `mml`'s
34
+ `lib/mml/context_configuration.rb`)
35
+ - [ ] Each `Schema3::*` and `Schema24::*` class defines
36
+ `def self.lutaml_default_register; :chemicalml_schema3; end`
37
+ (or `:chemicalml_schema24`)
38
+ - [ ] `lib/chemicalml/versioned_parser.rb` — `extend`-able mixin with
39
+ `parse(xml, namespace_exist:, context:)` that calls
40
+ `ensure_registered!` then `GlobalContext.resolve_type(:cml, ctx)`
41
+ then `root_class.from_xml(xml, register: ctx)`
42
+ - [ ] Schema3 and Schema24 modules `extend VersionedParser`
43
+
44
+ ## Acceptance
45
+
46
+ - `Chemicalml::Cml::Schema3.parse(xml)` returns a Schema3 document.
47
+ - `Chemicalml::Cml::Schema24.parse(xml)` returns a Schema24 document.
48
+ - `Chemicalml.parse(xml, schema: :schema24)` dispatches correctly.
49
+ - `Lutaml::Model::GlobalContext.context(:chemicalml_schema3)` is
50
+ populated after first parse.
@@ -0,0 +1,59 @@
1
+ # 04 — Top-level parse / serialize entry points
2
+
3
+ **Status:** complete
4
+ **Depends on:** 02, 03
5
+
6
+ ## Goal
7
+
8
+ Provide a single, obvious entry point for users: `Chemicalml.parse(xml,
9
+ schema: :schema3)` returns the right wire document. Symmetrically,
10
+ `Chemicalml.serialize(doc)` produces XML. This mirrors `Mml.parse(input,
11
+ version:)`.
12
+
13
+ ## Why
14
+
15
+ Today users must know which class to call `.from_xml` on:
16
+ `Chemicalml::Cml::Schema3::Document.from_xml(xml)`. After task 02 this
17
+ gets worse — there are now two real hierarchies. A single dispatch
18
+ function is the obvious UX.
19
+
20
+ ## Deliverables
21
+
22
+ - [ ] `Chemicalml.parse(xml, schema: :schema3, namespace_exist: true)`
23
+ → dispatches via `Schema::Registry` to the right VersionedParser.
24
+ - [ ] `Chemicalml.serialize(document)` → just calls `document.to_xml`.
25
+ Exists for API symmetry.
26
+ - [ ] `Chemicalml.parser_for(schema)` → returns the Schema3 or Schema24
27
+ module (raises `ArgumentError` on unknown).
28
+ - [ ] Document in `README.adoc` and `CLAUDE.md`.
29
+
30
+ ## Pattern
31
+
32
+ ```ruby
33
+ # lib/chemicalml.rb (additions)
34
+ def parse(xml, schema: :schema3, **opts)
35
+ parser_for(schema).parse(xml, **opts)
36
+ end
37
+
38
+ def serialize(document, **opts)
39
+ document.to_xml(**opts)
40
+ end
41
+
42
+ def parser_for(schema)
43
+ case schema
44
+ when :schema3 then Chemicalml::Cml::Schema3
45
+ when :schema24 then Chemicalml::Cml::Schema24
46
+ else
47
+ raise ArgumentError, "unsupported schema: #{schema.inspect} " \
48
+ "(supported: :schema3, :schema24)"
49
+ end
50
+ end
51
+ module_function :parse, :serialize, :parser_for
52
+ ```
53
+
54
+ ## Acceptance
55
+
56
+ - `Chemicalml.parse("<cml>...</cml>")` works (defaults to Schema3).
57
+ - `Chemicalml.parse("<cml>...</cml>", schema: :schema24)` returns a
58
+ `Schema24::Document`.
59
+ - Unknown schema raises `ArgumentError`.
@@ -0,0 +1,33 @@
1
+ # 05 — Schema-aware Translator
2
+
3
+ **Status:** complete
4
+ **Depends on:** 02
5
+
6
+ ## Goal
7
+
8
+ Update `Chemicalml::Cml::Translator` so it can serialize the canonical
9
+ model into either Schema3 or Schema24 wire format, and so it can parse
10
+ either schema's wire format back into the canonical model.
11
+
12
+ ## Why
13
+
14
+ Today the translator only knows about `Cml::Document` (now an alias for
15
+ Schema3). Once Schema3 and Schema24 are real class hierarchies, the
16
+ translator needs to know which one to instantiate when going from
17
+ canonical → wire.
18
+
19
+ ## Deliverables
20
+
21
+ - [ ] `Translator.from_canonical(model_doc, schema: :schema3)` →
22
+ returns a `Schema3::Document` or `Schema24::Document`
23
+ - [ ] `Translator.to_canonical(wire_doc)` → unchanged: detects the
24
+ wire class via `is_a?` and dispatches correctly
25
+ - [ ] Existing two-method public API (`to_canonical`,
26
+ `from_canonical`) preserved; new behavior is opt-in via the
27
+ `schema:` keyword (defaults to `:schema3`)
28
+
29
+ ## Acceptance
30
+
31
+ - Round-trip canonical → Schema3 wire → canonical yields an equal doc.
32
+ - Round-trip canonical → Schema24 wire → canonical yields an equal doc.
33
+ - Specs cover both schemas.
@@ -0,0 +1,44 @@
1
+ # 06 — Replace respond_to? with is_a?
2
+
3
+ **Status:** complete
4
+ **Depends on:** 02
5
+
6
+ ## Goal
7
+
8
+ The user's global rule forbids `respond_to?` for type checking. Today
9
+ the convention constraint walker uses `respond_to?` heavily to ask
10
+ heterogeneous wire nodes whether they expose a particular attribute.
11
+ Replace every occurrence with `is_a?` checks against the concrete wire
12
+ classes (or a marker module that those classes include).
13
+
14
+ ## Why
15
+
16
+ `respond_to?` is duck-typing that hides type errors until runtime. The
17
+ right pattern is either:
18
+
19
+ - Use `is_a?(SpecificClass)` when the constraint genuinely only applies
20
+ to one class.
21
+ - Introduce a marker module (e.g. `Cml::HasAtomArray`) that the
22
+ relevant classes include, then check `node.is_a?(HasAtomArray)`. This
23
+ is OCP-friendly — new classes opt in by including the marker, not by
24
+ editing a switch.
25
+
26
+ ## Deliverables
27
+
28
+ - [ ] Introduce `Chemicalml::Cml::Visitable` mixin included by every
29
+ wire class. Provides a uniform `accept_constraint(visitor)`
30
+ interface that delegates to `visitor.visit_<class_short_name>`.
31
+ - [ ] Add marker modules where duck-typing is currently used:
32
+ `Cml::HasAtomArray`, `Cml::HasBondArray`, `Cml::HasEntries`,
33
+ `Cml::HasUnits`, etc. — included by the relevant wire classes.
34
+ - [ ] Rewrite `Convention::Constraint#wire_children` to walk via
35
+ `is_a?` checks against marker modules, not `respond_to?`.
36
+ - [ ] Audit `grep -rn 'respond_to?' lib/` — should return zero hits
37
+ outside spec_helper boilerplate.
38
+
39
+ ## Acceptance
40
+
41
+ - `grep -rn 'respond_to?' lib/` returns nothing.
42
+ - All convention specs still pass.
43
+ - Constraint walker behavior unchanged (still detects the same
44
+ violations on the same fixtures).
@@ -0,0 +1,43 @@
1
+ # 07 — Canon-based semantic XML comparison
2
+
3
+ **Status:** complete
4
+ **Depends on:** —
5
+
6
+ ## Goal
7
+
8
+ Replace brittle string-equality assertions in round-trip specs with
9
+ semantic XML comparison via the
10
+ [`canon`](https://github.com/plurimath/canon) gem. This is what `mml`
11
+ uses for its `be_xml_equivalent_to` matcher.
12
+
13
+ ## Why
14
+
15
+ Plain string equality breaks on:
16
+
17
+ - attribute order (`<a x="1" y="2"/>` vs `<a y="2" x="1"/>`)
18
+ - whitespace differences inside tags
19
+ - namespace prefix aliases
20
+ - self-closing vs empty-tag forms
21
+
22
+ Semantic comparison treats all of these as equivalent, which is what
23
+ round-trip tests actually want to verify.
24
+
25
+ ## Deliverables
26
+
27
+ - [ ] Add `gem "canon"` to the development group in `Gemfile`
28
+ - [ ] Configure `Canon::Config` in `spec/spec_helper.rb`:
29
+ ```ruby
30
+ Canon::Config.configure do |config|
31
+ config.xml.match.profile = :spec_friendly
32
+ config.xml.diff.algorithm = :semantic
33
+ end
34
+ ```
35
+ - [ ] Existing round-trip specs use `expect(out).to be_xml_equivalent_to(in)`
36
+ instead of `expect(out).to eq(in)` or attribute-by-attribute
37
+ equality.
38
+
39
+ ## Acceptance
40
+
41
+ - `bundle exec rspec` is green with the new matcher.
42
+ - Round-trip specs catch real regressions (e.g. dropped attributes)
43
+ rather than formatting noise.
@@ -0,0 +1,37 @@
1
+ # 08 — Expand fixtures corpus
2
+
3
+ **Status:** complete
4
+ **Depends on:** 02
5
+
6
+ ## Goal
7
+
8
+ Grow the fixture corpus so it covers the breadth of CML: stereo,
9
+ parity, properties, parameters, modules, dictionaries, units,
10
+ crystals, lattices. Each fixture exercises one feature so a regression
11
+ is easy to localize.
12
+
13
+ ## Deliverables
14
+
15
+ Add the following seed fixtures under `spec/fixtures/`:
16
+
17
+ - [ ] `schema3/molecular/ethene_with_bond_stereo.cml` — cis/trans
18
+ - [ ] `schema3/molecular/chiral_center.cml` — atomParity
19
+ - [ ] `schema3/molecular/with_property.cml` — property + scalar
20
+ - [ ] `schema3/molecular/nested_molecules.cml` — parent/child
21
+ - [ ] `schema3/molecular/with_formula_inline.cml` — LaTeX-style inline
22
+ - [ ] `schema3/compchem/full_nwchem.cml` — full jobList with
23
+ initialization / calculation / finalization
24
+ - [ ] `schema3/compchem/with_basis_set.cml` — basisSet list
25
+ - [ ] `schema3/dictionary/example.cml` — dictionary with entries
26
+ - [ ] `schema3/unit/si_units.cml` — unitList with SI units
27
+ - [ ] `schema24/crystal.cml` — crystal + lattice
28
+ - [ ] `schema24/spectrum.cml` — peakList
29
+ - [ ] `spec/fixtures/README.md` — documents provenance (synthetic vs
30
+ scraped) and licensing
31
+
32
+ ## Acceptance
33
+
34
+ - All new fixtures round-trip through both Schema3 and Schema24 wire
35
+ classes.
36
+ - `bundle exec rspec spec/chemicalml/fixtures_round_trip_spec.rb` is
37
+ green.
@@ -0,0 +1,29 @@
1
+ # 09 — Final spec + lint pass
2
+
3
+ **Status:** complete
4
+ **Depends on:** all
5
+
6
+ ## Goal
7
+
8
+ Run the full suite, fix any regressions, run rubocop, document any
9
+ remaining gaps in this file.
10
+
11
+ ## Deliverables
12
+
13
+ - [ ] `bundle exec rspec` green
14
+ - [ ] `bundle exec rubocop` clean (or only pre-existing offenses,
15
+ documented)
16
+ - [ ] `grep -rn 'require_relative' lib/` returns nothing (except the
17
+ gemspec line which is legitimate)
18
+ - [ ] `grep -rn 'respond_to?' lib/` returns nothing
19
+ - [ ] `grep -rn 'instance_variable_set\|instance_variable_get' lib/`
20
+ returns nothing
21
+ - [ ] `grep -rn '\.send(' lib/` reviewed — no private-method bypasses
22
+ - [ ] This file updated with final status
23
+
24
+ ## Acceptance
25
+
26
+ - All four grep checks return clean.
27
+ - Full spec suite green.
28
+ - The TODO.align index (`README.md`) shows every workstream as
29
+ `complete`.
@@ -0,0 +1,54 @@
1
+ # 10 — Role marker modules
2
+
3
+ **Status:** complete
4
+ **Depends on:** —
5
+
6
+ ## Goal
7
+
8
+ Every constraint currently does:
9
+
10
+ ```ruby
11
+ def molecule?(node)
12
+ node.is_a?(Chemicalml::Cml::Schema3::Molecule) ||
13
+ node.is_a?(Chemicalml::Cml::Schema24::Molecule)
14
+ end
15
+ ```
16
+
17
+ This is an OCP violation: adding `Schema5` later means editing every
18
+ constraint. Replace with a single marker module per role:
19
+
20
+ ```ruby
21
+ def molecule?(node)
22
+ node.is_a?(Chemicalml::Cml::Role::Molecule)
23
+ end
24
+ ```
25
+
26
+ Both `Schema3::Molecule` and `Schema24::Molecule` (and any future
27
+ version) include `Cml::Role::Molecule`. Constraints never change when
28
+ new schema versions are added.
29
+
30
+ ## Why
31
+
32
+ Type checks against concrete classes couple constraints to the set of
33
+ supported schema versions. Marker modules decouple them: the constraint
34
+ asks "is this a molecule?" not "is this one of the known molecule
35
+ classes?".
36
+
37
+ ## Deliverables
38
+
39
+ - [ ] `lib/chemicalml/cml/role.rb` — namespace file with autoloads
40
+ - [ ] One module per CML element role under `lib/chemicalml/cml/role/`:
41
+ `molecule.rb`, `atom.rb`, `atom_array.rb`, `bond.rb`,
42
+ `bond_array.rb`, `module.rb`, `dictionary.rb`,
43
+ `dictionary_entry.rb`, `unit.rb`, `unit_type.rb`, etc.
44
+ - [ ] Each `Base::*` module's `included` hook adds `include
45
+ Chemicalml::Cml::Role::Foo` to the including class.
46
+ - [ ] Every constraint file uses `is_a?(Cml::Role::Foo)` instead of
47
+ `is_a?(Schema3::Foo) || is_a?(Schema24::Foo)`.
48
+
49
+ ## Acceptance
50
+
51
+ - `grep -rn 'Schema3.*||.*Schema24' lib/` returns nothing.
52
+ - `grep -rn 'is_a?(Chemicalml::Cml::Schema' lib/chemicalml/convention/`
53
+ returns nothing.
54
+ - All convention specs pass.
@@ -0,0 +1,63 @@
1
+ # 11 — DRY Configuration registration
2
+
3
+ **Status:** complete
4
+ **Depends on:** —
5
+
6
+ ## Goal
7
+
8
+ `Schema3::Configuration.register_models!` and
9
+ `Schema24::Configuration.register_models!` are 99% identical — only
10
+ the parent module differs. Both list every CML element class with the
11
+ same id. This is a DRY / SSOT violation.
12
+
13
+ Extract the registration loop into `ContextConfiguration` so each
14
+ schema's Configuration is just a few lines:
15
+
16
+ ```ruby
17
+ module Schema3
18
+ module Configuration
19
+ extend Chemicalml::ContextConfiguration
20
+
21
+ CONTEXT_ID = :chemicalml_schema3
22
+ REGISTERED_ELEMENTS = %i[
23
+ atom atomArray atomParity bond bondArray bondStereo
24
+ document formula identifier label list matrix metadata
25
+ ...
26
+ ].freeze
27
+
28
+ register_element_classes!
29
+ end
30
+ end
31
+ ```
32
+
33
+ The shared `register_element_classes!` walks the list, looks up each
34
+ class on the parent module, and calls `register_model`.
35
+
36
+ ## Why
37
+
38
+ Today, adding a new CML element means:
39
+
40
+ 1. Creating `Base::Foo`
41
+ 2. Creating `Schema3::Foo` and `Schema24::Foo`
42
+ 3. **Adding `register_model Foo, id: :foo` to BOTH Configuration files**
43
+ 4. Adding translator rules
44
+ 5. Adding specs
45
+
46
+ Step 3 is duplicated work. After this fix, the element name goes in one
47
+ list (`REGISTERED_ELEMENTS`) and both schemas pick it up automatically.
48
+
49
+ ## Deliverables
50
+
51
+ - [ ] `Chemicalml::ContextConfiguration#register_element_classes!`
52
+ iterates `REGISTERED_ELEMENTS`, looks up each class on
53
+ `version_parent_module`, and registers it.
54
+ - [ ] `Schema3::Configuration` and `Schema24::Configuration` reduced
55
+ to ~10 lines each: `extend`, `CONTEXT_ID =`, `REGISTERED_ELEMENTS =`,
56
+ `register_element_classes!`.
57
+
58
+ ## Acceptance
59
+
60
+ - Both Configuration files under 20 lines.
61
+ - Adding a new element requires touching exactly ONE place
62
+ (the `REGISTERED_ELEMENTS` array).
63
+ - All specs still pass.
@@ -0,0 +1,51 @@
1
+ # 12 — Wire class definition macro
2
+
3
+ **Status:** complete
4
+ **Depends on:** 11
5
+
6
+ ## Goal
7
+
8
+ Each `lib/chemicalml/cml/schema3/foo.rb` and `schema24/foo.rb` file is
9
+ 12 lines of boilerplate differing only by the class name and context
10
+ id. That's 36 × 2 = 72 files of nearly-identical code.
11
+
12
+ Reduce to a single declarative call per schema:
13
+
14
+ ```ruby
15
+ # lib/chemicalml/cml/schema3.rb
16
+ Chemicalml::Cml.define_wire_classes(
17
+ in: Schema3,
18
+ base: Base,
19
+ context_id: :chemicalml_schema3,
20
+ elements: REGISTERED_ELEMENTS
21
+ )
22
+ ```
23
+
24
+ The macro creates one class per element name, includes the
25
+ corresponding Base and Visitable mixins, and defines
26
+ `lutaml_default_register`.
27
+
28
+ ## Why
29
+
30
+ - DRY: one place to change the wire-class shape (e.g. adding another
31
+ mixin, changing the parent class).
32
+ - SSOT: the per-element declaration lives in `REGISTERED_ELEMENTS` (from
33
+ task 11), not duplicated across 72 files.
34
+ - New schema version: just add `define_wire_classes(in: Schema5, ...)`
35
+ with no per-class boilerplate.
36
+
37
+ ## Deliverables
38
+
39
+ - [ ] `Chemicalml::Cml.define_wire_classes` class method on the `Cml`
40
+ module. Takes `in:`, `base:`, `context_id:`, `elements:` keyword
41
+ args.
42
+ - [ ] Delete the 72 per-class files.
43
+ - [ ] Update `schema3.rb` / `schema24.rb` to call the macro.
44
+
45
+ ## Acceptance
46
+
47
+ - The 72 thin class files are gone.
48
+ - `lib/chemicalml/cml/schema3/*.rb` and `schema24/*.rb` directories
49
+ contain only `configuration.rb` (and any future schema-specific
50
+ overrides).
51
+ - All specs still pass.
@@ -0,0 +1,44 @@
1
+ # 13 — Wire XML namespace setup
2
+
3
+ **Status:** complete
4
+ **Depends on:** —
5
+
6
+ ## Goal
7
+
8
+ Today, round-tripping a namespaced CML document produces inner
9
+ elements with spurious `xmlns=""` attributes:
10
+
11
+ ```xml
12
+ <molecule xmlns="http://www.xml-cml.org/schema" id="m1">
13
+ <atomArray xmlns=""> <!-- ← bug -->
14
+ <atom id="a1" elementType="C"/>
15
+ </atomArray>
16
+ </molecule>
17
+ ```
18
+
19
+ This is because the wire classes don't declare a namespace on their
20
+ `xml do ... end` block, so the framework assumes the empty namespace.
21
+
22
+ Add `namespace Chemicalml::Cml::Namespace` to each Base module's xml
23
+ block so output preserves the CML namespace.
24
+
25
+ ## Why
26
+
27
+ - Real bug — the round-trip isn't faithful.
28
+ - `canon` semantic comparison will catch this once we switch the
29
+ fixture spec to it (task 15).
30
+
31
+ ## Deliverables
32
+
33
+ - [ ] Verify `Chemicalml::Cml::Namespace` is a proper lutaml namespace
34
+ declaration.
35
+ - [ ] Add `namespace Chemicalml::Cml::Namespace` to the `xml do` block
36
+ in every `lib/chemicalml/cml/base/*.rb` mixin.
37
+ - [ ] Round-trip a fixture with namespace declaration and confirm no
38
+ `xmlns=""` appears on inner elements.
39
+
40
+ ## Acceptance
41
+
42
+ - A round-tripped `<cml xmlns="..."><molecule>...</molecule></cml>` is
43
+ byte-equivalent in namespace structure to the input.
44
+ - Canon matcher specs pass on full-document round-trip.
@@ -0,0 +1,37 @@
1
+ # 14 — Schema 2.4 correctness
2
+
3
+ **Status:** complete
4
+ **Depends on:** 11
5
+
6
+ ## Goal
7
+
8
+ Schema 2.4 does NOT have the generic `<module>` element — it was
9
+ introduced in Schema 3. Currently `Chemicalml::Cml::Schema24::Module`
10
+ exists anyway, which is incorrect.
11
+
12
+ Drop `Schema24::Module` and document the divergence in the schema24.rb
13
+ file's header comment.
14
+
15
+ ## Why
16
+
17
+ - SSOT — the set of valid Schema 2.4 elements is defined by the XSD,
18
+ not by Ruby convenience. Including extra elements makes the wire
19
+ classes lie about what schema they implement.
20
+ - Compchem-convention documents (which use `<module>` heavily) MUST be
21
+ parsed as Schema 3, not Schema 2.4. This makes that explicit.
22
+
23
+ ## Deliverables
24
+
25
+ - [ ] Remove `lib/chemicalml/cml/schema24/cml_module.rb`.
26
+ - [ ] Remove `Schema24::Module` autoload from `schema24.rb`.
27
+ - [ ] Ensure `Schema24::Configuration.register_models!` does NOT
28
+ register `:module`.
29
+ - [ ] Document in `schema24.rb` header that Schema 2.4 lacks `<module>`.
30
+ - [ ] Add a spec verifying `Chemicalml::Cml::Schema24` does not have
31
+ `Module` defined.
32
+
33
+ ## Acceptance
34
+
35
+ - `defined?(Chemicalml::Cml::Schema24::Module)` returns `nil`.
36
+ - All other Schema24 classes load and round-trip normally.
37
+ - Specs pass.
@@ -0,0 +1,38 @@
1
+ # 15 — Canon-based fixture round-trip
2
+
3
+ **Status:** complete
4
+ **Depends on:** 13
5
+
6
+ ## Goal
7
+
8
+ The current `spec/chemicalml/fixtures_round_trip_spec.rb` only checks
9
+ that `.molecules.length` is preserved across a round-trip. It misses
10
+ attribute-level regressions (dropped attributes, wrong order, changed
11
+ values).
12
+
13
+ Switch to `be_xml_equivalent_to` (from the `canon` gem) for semantic
14
+ XML comparison. Depends on task 13 (namespace fix) so the comparison
15
+ isn't tripped up by spurious `xmlns=""` attributes.
16
+
17
+ ## Why
18
+
19
+ - Catches real regressions: a dropped `elementType` attribute or a
20
+ reordered atom would slip past today's check.
21
+ - Tolerates formatting differences (attribute order, whitespace) that
22
+ round-trip serialization legitimately introduces.
23
+ - Matches the spec idioms used in the sibling `mml` gem.
24
+
25
+ ## Deliverables
26
+
27
+ - [ ] `fixtures_round_trip_spec.rb` rewritten to use
28
+ `expect(round_tripped_xml).to be_xml_equivalent_to(input_xml)`.
29
+ - [ ] One example per fixture file.
30
+ - [ ] Document any fixtures that can't yet round-trip cleanly (known
31
+ limitations) with `skip "reason"` rather than removing the test.
32
+
33
+ ## Acceptance
34
+
35
+ - `bundle exec rspec spec/chemicalml/fixtures_round_trip_spec.rb` is
36
+ green.
37
+ - Manually introducing a regression (e.g. dropping an attribute from
38
+ a Base module) causes a spec failure.