sts 0.6.1 → 0.6.3

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 (77) hide show
  1. checksums.yaml +4 -4
  2. data/.rubocop_todo.yml +2 -0
  3. data/TODO.finalize/00-overview.md +56 -0
  4. data/TODO.finalize/01-investigation-findings.md +39 -0
  5. data/TODO.finalize/02-delete-tbxisotml-fn-fngroup.md +20 -0
  6. data/TODO.finalize/03-repoint-isosts-call-sites.md +27 -0
  7. data/TODO.finalize/04-repoint-tbxisotml-call-sites.md +24 -0
  8. data/TODO.finalize/05-sts-regression-specs.md +27 -0
  9. data/TODO.finalize/06-lutaml-model-element-builder-fix.md +45 -0
  10. data/TODO.finalize/07-lutaml-model-specs.md +12 -0
  11. data/TODO.finalize/08-sts-ruby-pr.md +20 -0
  12. data/TODO.finalize/09-lutaml-model-pr.md +5 -0
  13. data/TODO.finalize/10-audit-other-duplicates.md +29 -0
  14. data/TODO.finalize/11-strengthen-anti-patterns-spec.md +24 -0
  15. data/TODO.sts-refactor/00-overview.md +22 -13
  16. data/TODO.sts-refactor/03-namespace-coupling.md +17 -15
  17. data/lib/sts/iso_sts/body.rb +2 -0
  18. data/lib/sts/iso_sts/bold.rb +1 -1
  19. data/lib/sts/iso_sts/copyright_holder.rb +6 -0
  20. data/lib/sts/iso_sts/copyright_statement.rb +6 -0
  21. data/lib/sts/iso_sts/copyright_year.rb +4 -0
  22. data/lib/sts/iso_sts/disp_formula.rb +1 -1
  23. data/lib/sts/iso_sts/doc_number.rb +4 -1
  24. data/lib/sts/iso_sts/doc_type.rb +4 -1
  25. data/lib/sts/iso_sts/edition.rb +6 -0
  26. data/lib/sts/iso_sts/ext_link.rb +10 -0
  27. data/lib/sts/iso_sts/fpage.rb +2 -0
  28. data/lib/sts/iso_sts/graphic.rb +10 -0
  29. data/lib/sts/iso_sts/ics.rb +4 -1
  30. data/lib/sts/iso_sts/inline_formula.rb +1 -1
  31. data/lib/sts/iso_sts/is_proof.rb +7 -2
  32. data/lib/sts/iso_sts/issue.rb +2 -0
  33. data/lib/sts/iso_sts/italic.rb +1 -1
  34. data/lib/sts/iso_sts/label.rb +4 -0
  35. data/lib/sts/iso_sts/lpage.rb +2 -0
  36. data/lib/sts/iso_sts/mixed_citation.rb +14 -2
  37. data/lib/sts/iso_sts/named_content.rb +2 -0
  38. data/lib/sts/iso_sts/originator.rb +4 -1
  39. data/lib/sts/iso_sts/page_range.rb +2 -0
  40. data/lib/sts/iso_sts/paragraph.rb +2 -2
  41. data/lib/sts/iso_sts/part_number.rb +4 -1
  42. data/lib/sts/iso_sts/proj_id.rb +4 -1
  43. data/lib/sts/iso_sts/pub_date.rb +2 -0
  44. data/lib/sts/iso_sts/pub_id.rb +2 -0
  45. data/lib/sts/iso_sts/release_version.rb +4 -1
  46. data/lib/sts/iso_sts/sdo.rb +4 -1
  47. data/lib/sts/iso_sts/sec.rb +2 -2
  48. data/lib/sts/iso_sts/secretariat.rb +4 -1
  49. data/lib/sts/iso_sts/standard.rb +2 -2
  50. data/lib/sts/iso_sts/std.rb +1 -1
  51. data/lib/sts/iso_sts/sub.rb +4 -0
  52. data/lib/sts/iso_sts/sup.rb +4 -0
  53. data/lib/sts/iso_sts/suppl_number.rb +4 -1
  54. data/lib/sts/iso_sts/suppl_type.rb +4 -1
  55. data/lib/sts/iso_sts/suppl_version.rb +4 -1
  56. data/lib/sts/iso_sts/td.rb +2 -2
  57. data/lib/sts/iso_sts/term.rb +1 -1
  58. data/lib/sts/iso_sts/term_sec.rb +1 -1
  59. data/lib/sts/iso_sts/th.rb +2 -2
  60. data/lib/sts/iso_sts/title.rb +4 -0
  61. data/lib/sts/iso_sts/underline.rb +2 -0
  62. data/lib/sts/iso_sts/uri.rb +2 -0
  63. data/lib/sts/iso_sts/urn.rb +4 -1
  64. data/lib/sts/iso_sts/version.rb +4 -1
  65. data/lib/sts/iso_sts/volume.rb +2 -0
  66. data/lib/sts/iso_sts/year.rb +2 -0
  67. data/lib/sts/tbx_iso_tml/definition.rb +1 -1
  68. data/lib/sts/tbx_iso_tml/note.rb +1 -1
  69. data/lib/sts/tbx_iso_tml/table_wrap_foot.rb +2 -2
  70. data/lib/sts/tbx_iso_tml/td.rb +1 -1
  71. data/lib/sts/tbx_iso_tml/term.rb +1 -1
  72. data/lib/sts/tbx_iso_tml/th.rb +1 -1
  73. data/lib/sts/tbx_iso_tml.rb +0 -2
  74. data/lib/sts/version.rb +1 -1
  75. metadata +13 -3
  76. data/lib/sts/tbx_iso_tml/fn.rb +0 -20
  77. data/lib/sts/tbx_iso_tml/fn_group.rb +0 -18
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 36f88ede902806ba5a3471e0032a6d9d9543157ab150af32e4b139cc75b77b14
4
- data.tar.gz: 93840c45252bb723bc58bb6e78c204075ea94d00a87bb0625a98b16ccfc29bda
3
+ metadata.gz: 27e3b4850a13bbff3b16e63ad2293589079e4c29b4f25d2fa120a797b9f6ff7c
4
+ data.tar.gz: 572e19d6b4e428dca7a277d836f73b6121627d094fd1d0e492ef0ba4b82b905c
5
5
  SHA512:
6
- metadata.gz: 3aad32ee67d2f5ef93f8b4de1b8c7ace1ae073d8cbf6e903f1c9ef0836b9b6ddc7fa5b24cc2157c9bfb431f5860a50944a8f5df3d46ebf1a7444efc0537b37c6
7
- data.tar.gz: 7d1aaa5f6c206801500a46b008d72ae194c8c9e0efd148d0e6efc85f72d024d699483510c238fc1342c536957619da4ad7e59e8eb2d0f2f2d9620b133e15c08c
6
+ metadata.gz: ab688a012341d43a0daa2a2b79d7e68b7e2b10a9e4ee4f30824d357e1c3329721ccfe6fe071e331265ca79de26dc6d9deb54ad269519bc1a6e164f38d76bbd41
7
+ data.tar.gz: 1cd96829661efa371b501e25dac9f56e5aeb9b6ba3cac72c5de47e1b6d6377c2193eaf29842c36ae68a70321796ceb06079671df81ed848d2805c66256aed3e4
data/.rubocop_todo.yml CHANGED
@@ -70,6 +70,8 @@ RSpec/DescribeClass:
70
70
  - 'spec/elements/new_elements_spec.rb'
71
71
  - 'spec/elements/reference_models_spec.rb'
72
72
  - 'spec/elements/structural_models_spec.rb'
73
+ - 'spec/fn_namespace_spec.rb'
74
+ - 'spec/mathml_version_spec.rb'
73
75
  - 'spec/round_trip/reference_docs_spec.rb'
74
76
 
75
77
  # Offense count: 1
@@ -0,0 +1,56 @@
1
+ # sts-ruby + lutaml-model Finalization Plan
2
+
3
+ **Date**: 2026-07-25
4
+ **Trigger**: User-reported `NoMethodError: paragraph` on `Sts::TbxIsoTml::Fn` nested inside `Sts::NisoSts::Back` (rice.xml e2e).
5
+ **Status**: PR #1 (sts-ruby dedup) in flight; PR #2 (lutaml-model) deferred per downstream maintainer feedback.
6
+
7
+ ## Root Cause Summary
8
+
9
+ Two layered bugs:
10
+
11
+ 1. **sts-ruby**: `Sts::TbxIsoTml::Fn` (Ruby attr `:p`) and `Sts::NisoSts::Fn` (Ruby attr `:paragraph`) are duplicate `<fn>` classes with conflicting Ruby attribute names. `TbxIsoTml::FnGroup` similarly duplicates `NisoSts::FnGroup`. Six `Fn`/`FnGroup` classes exist across three namespaces; only four are schema-correct.
12
+ 2. **lutaml-model**: `Lutaml::Xml::TransformationSupport::ElementBuilder#create_nested_model_element` uses the rule's cached `child_transformation` (keyed on declared `attribute_type`) regardless of the value's actual class. When a caller assigns an unrelated `Serializable` to a typed slot, the wrong transformation is applied silently, producing confusing `NoMethodError`s.
13
+
14
+ The cache itself is correct (`transformation_key` includes `object_id`). The bug is in the dispatch logic.
15
+
16
+ ## Architecture Decision (PR #1)
17
+
18
+ `<fn>` and `<fn-group>` are JATS / NISO STS elements (per ISOSTS.xsd and NISO-STS-extended-1.xsd). They are **not** TBX-ISO-TML elements (TBX-ISO-TML is ISO 30042, a terminology model). The presence of `TbxIsoTml::Fn` and `TbxIsoTml::FnGroup` is a misclassification — same pattern as the prior `TbxIsoTml::Math` removal in commit `9e977a5`. Remove them.
19
+
20
+ Per [[architecture-namespace-independence]]:
21
+ - `IsoSts::*` references use `IsoSts::Fn` / `IsoSts::FnGroup` (independence from NisoSts).
22
+ - `NisoSts::*` references continue to use `NisoSts::Fn` / `NisoSts::FnGroup`.
23
+ - `TbxIsoTml::*` references follow the existing `TbxIsoTml::Bold` / `TbxIsoTml::Italic` precedent (both already use `NisoSts::Fn`): use `NisoSts::Fn` / `NisoSts::FnGroup`. TBX is a shared namespace.
24
+
25
+ ## Decision: Defer PR #2 (lutaml-model)
26
+
27
+ Downstream metanorma-iso maintainer reviewed the plan and recommended defer:
28
+ > #2 is a real robustness win but introduces a new contract that may break
29
+ > other downstream consumers who've been quietly relying on the current
30
+ > lenient behavior. Worth doing eventually, but urgency drops once #1 lands.
31
+
32
+ Once #1 lands, the trap cannot be triggered via the original repo's classes. The lenient dispatch remains a robustness gap for future duplicates — see TODO 10 (audit other potential duplicates).
33
+
34
+ ## Execution Order
35
+
36
+ | # | File | Status |
37
+ |---|---|---|
38
+ | 01 | investigation findings | complete |
39
+ | 02 | delete `TbxIsoTml::Fn` / `TbxIsoTml::FnGroup` | complete |
40
+ | 03 | repoint IsoSts call sites to `IsoSts::Fn` | complete |
41
+ | 04 | repoint TbxIsoTml call sites to `NisoSts::Fn` | complete |
42
+ | 05 | sts-ruby regression specs | complete |
43
+ | 06 | lutaml-model `ElementBuilder` dispatch fix | **deferred** |
44
+ | 07 | lutaml-model specs | **deferred** |
45
+ | 08 | sts-ruby PR | in flight |
46
+ | 09 | lutaml-model PR | **deferred** |
47
+ | 10 | audit other potential duplicates | deferred (future) |
48
+ | 11 | strengthen `anti_patterns_spec.rb` | deferred (future) |
49
+
50
+ ## Principles enforced (PR #1)
51
+
52
+ - **MECE**: each Fn class lives in exactly one namespace; no parallel hierarchies.
53
+ - **Model-driven**: every `<fn>` reference is a typed `attribute :fn, ::Sts::<Namespace>::Fn`.
54
+ - **OCP**: adding a new host namespace = adding new Fn classes there, not editing existing ones.
55
+ - **DRY**: one class per (namespace, XML element) pair.
56
+ - **No anti-patterns**: no `send`, no `instance_variable_set/get`, no `respond_to?`, no `require_relative` in lib.
@@ -0,0 +1,39 @@
1
+ # 01: Investigation Findings — Fn/FnGroup Duplication
2
+
3
+ **Status**: complete (findings documented for reference)
4
+
5
+ ## Six classes discovered
6
+
7
+ | Class | File | Ruby attr for `<p>` | Other notable attrs |
8
+ |---|---|---|---|
9
+ | `Sts::IsoSts::Fn` | `lib/sts/iso_sts/fn.rb` | `:paragraph` | fn_type, specific_use, symbol, xml_lang, non_normative_note, non_normative_example |
10
+ | `Sts::IsoSts::FnGroup` | `lib/sts/iso_sts/fn_group.rb` | — | content_type, specific_use, xml_lang, originator, label, title |
11
+ | `Sts::NisoSts::Fn` | `lib/sts/niso_sts/fn.rb` | `:paragraph` | (minimal) |
12
+ | `Sts::NisoSts::FnGroup` | `lib/sts/niso_sts/fn_group.rb` | — | (minimal) |
13
+ | `Sts::TbxIsoTml::Fn` | `lib/sts/tbx_iso_tml/fn.rb` | `:p` ← **outlier** | (minimal) |
14
+ | `Sts::TbxIsoTml::FnGroup` | `lib/sts/tbx_iso_tml/fn_group.rb` | — | (minimal) |
15
+
16
+ ## Call sites (24 total)
17
+
18
+ ### IsoSts (8 sites — all wrong, should be IsoSts::Fn)
19
+ - `lib/sts/iso_sts/{td,bold,paragraph,mixed_citation,sec,th,std,italic}.rb`
20
+
21
+ ### IsoSts (correct — already uses IsoSts::Fn/IsoSts::FnGroup)
22
+ - `lib/sts/iso_sts/{fig,table_wrap_foot,fn_group,app,back,term_sec}.rb`
23
+
24
+ ### TbxIsoTml (4 sites — should be NisoSts::Fn following Bold/Italic pattern)
25
+ - `lib/sts/tbx_iso_tml/{td,th,table_wrap_foot,fn_group}.rb`
26
+
27
+ ### TbxIsoTml (correct — already uses NisoSts::Fn)
28
+ - `lib/sts/tbx_iso_tml/{bold,italic}.rb`
29
+
30
+ ### NisoSts (correct — uses NisoSts::Fn/NisoSts::FnGroup)
31
+ - `lib/sts/niso_sts/{author_notes,source,data_title,named_content,mixed_citation,paragraph,section,back,display_formula,role,reference_standard,contrib,styled_content,toc,boxed_text,fn_group}.rb`
32
+
33
+ ## Verification of root cause
34
+
35
+ Patch on `Lutaml::Xml::Transformation#apply_standard_rules` showed: when `Sts::TbxIsoTml::Fn` is serialized inside a `Sts::NisoSts::Back` (whose `:fn_group` is typed `NisoSts::FnGroup`), the transformation applied has `model_class = Sts::NisoSts::Fn` (not `TbxIsoTml::Fn`). Its compiled rules include `attribute_name=:paragraph`, which is then `public_send`-ed on the TbxIsoTml::Fn value, raising `NoMethodError`.
36
+
37
+ The reverse also fails: `NisoSts::Fn` inside `TbxIsoTml::FnGroup` raises `private method 'p' called`.
38
+
39
+ The cache key in `TransformationRegistry#transformation_key` includes `object_id`, so cache collision is NOT the cause. The bug is in `ElementBuilder#create_nested_model_element` — it reuses `rule.child_transformation` (cached at compile time for the declared `attribute_type`) without consulting `value.class`.
@@ -0,0 +1,20 @@
1
+ # 02: Delete `Sts::TbxIsoTml::Fn` and `Sts::TbxIsoTml::FnGroup`
2
+
3
+ **Status**: pending
4
+ **Blocks**: 03, 04, 05
5
+
6
+ ## Why
7
+
8
+ `<fn>` and `<fn-group>` are JATS / NISO STS elements. They are not defined by TBX-ISO-TML (ISO 30042). The `TbxIsoTml::Fn` and `TbxIsoTml::FnGroup` classes are misclassified duplicates of `NisoSts::Fn` / `NisoSts::FnGroup` with one schema-breaking difference: the Ruby attribute for `<p>` is `:p` in TbxIsoTml and `:paragraph` in every other namespace. This mismatch is the root cause of the user-reported `NoMethodError`.
9
+
10
+ ## Actions
11
+
12
+ - [x] Delete `lib/sts/tbx_iso_tml/fn.rb`
13
+ - [x] Delete `lib/sts/tbx_iso_tml/fn_group.rb`
14
+ - [x] Remove `autoload :Fn` and `autoload :FnGroup` from `lib/sts/tbx_iso_tml.rb`
15
+ - [x] Run `bundle exec rspec` — confirm no spec references the deleted constants
16
+
17
+ ## Verification
18
+
19
+ - `grep -rn "TbxIsoTml::Fn\b\|TbxIsoTml::FnGroup\b" lib/ spec/` returns nothing after steps 03 and 04 complete.
20
+ - `Sts::TbxIsoTml.const_defined?(:Fn)` is `false`.
@@ -0,0 +1,27 @@
1
+ # 03: Repoint IsoSts call sites to `IsoSts::Fn`
2
+
3
+ **Status**: pending
4
+ **Blocked by**: 02
5
+
6
+ ## Why
7
+
8
+ Per [[architecture-namespace-independence]], `IsoSts` must be independent of `NisoSts`. The 8 IsoSts files that currently reference `TbxIsoTml::Fn` violate this. They should reference `IsoSts::Fn`, which already exists with the correct ISOSTS schema (5 attributes + label + paragraph + non_normative_note + non_normative_example).
9
+
10
+ ## Actions
11
+
12
+ For each file, swap `::Sts::TbxIsoTml::Fn` → `::Sts::IsoSts::Fn`:
13
+
14
+ - [x] `lib/sts/iso_sts/td.rb:28`
15
+ - [x] `lib/sts/iso_sts/bold.rb:25`
16
+ - [x] `lib/sts/iso_sts/paragraph.rb:27`
17
+ - [x] `lib/sts/iso_sts/mixed_citation.rb:22`
18
+ - [x] `lib/sts/iso_sts/sec.rb:37`
19
+ - [x] `lib/sts/iso_sts/th.rb:28`
20
+ - [x] `lib/sts/iso_sts/std.rb:14`
21
+ - [x] `lib/sts/iso_sts/italic.rb:25`
22
+
23
+ ## Verification
24
+
25
+ - `grep -rn "TbxIsoTml::Fn\b" lib/sts/iso_sts/` returns nothing.
26
+ - `bundle exec rspec spec/iso_sts/` is green.
27
+ - Full suite green (no spec depends on the old typing).
@@ -0,0 +1,24 @@
1
+ # 04: Repoint TbxIsoTml call sites to `NisoSts::Fn`
2
+
3
+ **Status**: pending
4
+ **Blocked by**: 02
5
+
6
+ ## Why
7
+
8
+ After step 02 deletes `TbxIsoTml::Fn` / `TbxIsoTml::FnGroup`, the 4 TbxIsoTml files that referenced them must point somewhere. Following the established `TbxIsoTml::Bold` / `TbxIsoTml::Italic` precedent (both already use `NisoSts::Fn`), use `NisoSts::Fn` / `NisoSts::FnGroup`.
9
+
10
+ TBX-ISO-TML is a shared namespace (per [[architecture-namespace-independence]]: "independence from NisoSts is not independence in general"). `<fn>` inside a TBX table cell is a NISO STS element borrowed into TBX context, so the NISO STS type is the correct reference.
11
+
12
+ ## Actions
13
+
14
+ - [x] `lib/sts/tbx_iso_tml/td.rb:94` — `::Sts::TbxIsoTml::Fn` → `::Sts::NisoSts::Fn`
15
+ - [x] `lib/sts/tbx_iso_tml/th.rb:95` — same
16
+ - [x] `lib/sts/tbx_iso_tml/table_wrap_foot.rb:7` — `::Sts::TbxIsoTml::FnGroup` → `::Sts::NisoSts::FnGroup`
17
+ - [x] `lib/sts/tbx_iso_tml/table_wrap_foot.rb:13` — `::Sts::TbxIsoTml::Fn` → `::Sts::NisoSts::Fn`
18
+
19
+ `lib/sts/tbx_iso_tml/fn_group.rb` is deleted in step 02 — no repoint needed.
20
+
21
+ ## Verification
22
+
23
+ - `grep -rn "TbxIsoTml::Fn\b\|TbxIsoTml::FnGroup\b" lib/sts/tbx_iso_tml/` returns nothing.
24
+ - `bundle exec rspec` green.
@@ -0,0 +1,27 @@
1
+ # 05: Add sts-ruby regression specs
2
+
3
+ **Status**: pending
4
+ **Blocked by**: 02, 03, 04
5
+
6
+ ## Why
7
+
8
+ Round-tripping alone cannot catch this class of bug — the wrong-type assignment only fails when nested two levels deep, and a future contributor could re-introduce `TbxIsoTml::Fn` thinking it is a reasonable place for a TBX footnote. Specs must assert:
9
+
10
+ 1. The deleted constants stay deleted.
11
+ 2. Each call site uses the correct namespace's Fn.
12
+ 3. The original bug repro (NisoSts::Back wrapping TbxIsoTml-style Fn usage) no longer raises — but this is now impossible to express directly because TbxIsoTml::Fn is gone. Instead, assert that a NisoSts::Back → NisoSts::FnGroup → NisoSts::Fn serialization round-trips, and the original error message ("undefined method 'paragraph'") cannot occur.
13
+
14
+ ## Actions
15
+
16
+ - [x] Add `spec/fn_namespace_spec.rb` asserting:
17
+ - `Sts::TbxIsoTml.const_defined?(:Fn)` is `false`
18
+ - `Sts::TbxIsoTml.const_defined?(:FnGroup)` is `false`
19
+ - `lib/sts/iso_sts/**/*.rb` files reference `IsoSts::Fn` / `IsoSts::FnGroup`, never `TbxIsoTml::Fn`
20
+ - `lib/sts/tbx_iso_tml/**/*.rb` files reference `NisoSts::Fn` / `NisoSts::FnGroup`, never `TbxIsoTml::Fn`
21
+ - Cross-namespace `Fn`/`FnGroup` instantiation and round-trip works through `NisoSts::Back → NisoSts::FnGroup → NisoSts::Fn`
22
+ - [x] Run full suite, confirm green.
23
+
24
+ ## Verification
25
+
26
+ - New spec file passes (14+ examples).
27
+ - Full suite: 2300+ examples, 0 failures.
@@ -0,0 +1,45 @@
1
+ # 06: lutaml-model `ElementBuilder#create_nested_model_element` dispatch fix
2
+
3
+ **Status**: DEFERRED (per downstream maintainer feedback 2026-07-25)
4
+ **Repo**: `lutaml/lutaml-model`
5
+ **File**: `lib/lutaml/xml/transformation/element_builder.rb`
6
+
7
+ ## Why deferred
8
+
9
+ The downstream metanorma-iso maintainer (who originally reported the bug) reviewed the proposed two-PR plan and recommended:
10
+
11
+ > Defer #2. The fix is a real robustness win but introduces a new contract
12
+ > ("declared type must match value class") that may break other downstream
13
+ > consumers who've been quietly relying on the current lenient behavior. Worth
14
+ > doing eventually, but the urgency drops once #1 lands — without the
15
+ > duplicate classes there's no easy way to trigger the trap.
16
+
17
+ ## Original analysis (kept for future reference)
18
+
19
+ Currently `create_nested_model_element` reuses `rule.child_transformation` (compiled for the declared `attribute_type`) for any value that is not a declared subtype. When a caller assigns an unrelated `Lutaml::Model::Serialize` to a typed slot, the wrong transformation is applied to the value, producing confusing `NoMethodError`s on whichever Ruby attribute name differs.
20
+
21
+ The cache itself is fine — `transformation_key` includes `object_id`. The dispatch logic is the bug.
22
+
23
+ ## When to revisit
24
+
25
+ - Track upstream consumers: if a second case of cross-class assignment emerges (e.g., another gem duplicates a Serializable with different Ruby attribute names), the urgency rises.
26
+ - Pair with a proper audit of existing polymorphic specs to ensure the lenient dispatch wouldn't break tests.
27
+ - Consider an opt-in flag (`strict_type_check: true`) before flipping default behavior.
28
+
29
+ ## Sketch of the fix (when revisited)
30
+
31
+ In `lib/lutaml/xml/transformation/element_builder.rb#create_nested_model_element`:
32
+
33
+ ```ruby
34
+ dispatch_on_value_class = union || is_polymorphic ||
35
+ (value.is_a?(Lutaml::Model::Serialize) && value.class != rule.attribute_type)
36
+
37
+ child_transformation = if dispatch_on_value_class
38
+ value.class.transformation_for(:xml, register)
39
+ else
40
+ rule.child_transformation ||
41
+ rule.attribute_type.transformation_for(:xml, register)
42
+ end
43
+ ```
44
+
45
+ This subsumes the existing `is_polymorphic_subtype` check (subtypes already have `value.class != rule.attribute_type`).
@@ -0,0 +1,12 @@
1
+ # 07: lutaml-model specs for cross-class dispatch
2
+
3
+ **Status**: DEFERRED (follows step 06)
4
+
5
+ See step 06 for the deferral rationale. When 06 is revisited, the specs described below should accompany it.
6
+
7
+ ## Originally planned specs
8
+
9
+ - Cross-class dispatch: parent declares `attribute :child, ParentClass`; caller assigns `OtherSerializable.new` (not a subtype). Serialization uses `OtherSerializable`'s mapping rules.
10
+ - Round-trip preserved: parsing the serialized XML yields an instance whose attributes match the original.
11
+ - Declared-type fast path unchanged: assigning a `ParentClass` instance uses the cached `child_transformation`.
12
+ - Subtype polymorphism (existing `is_polymorphic_subtype` behaviour) still works.
@@ -0,0 +1,20 @@
1
+ # 08: Open sts-ruby PR
2
+
3
+ **Status**: pending
4
+ **Blocked by**: 02, 03, 04, 05
5
+
6
+ ## Actions
7
+
8
+ - [x] Branch `fix/remove-tbxisotml-fn-duplicate` off latest `main`.
9
+ - [x] Commit steps 02–05 with a clear message.
10
+ - [x] Push, open PR against `metanorma/sts-ruby`.
11
+ - [x] PR body via `--body-file` (contains backticks — never inline).
12
+
13
+ ## PR description points
14
+
15
+ - Root cause: duplicate `<fn>` classes across namespaces with conflicting Ruby attr names.
16
+ - Why TbxIsoTml loses its Fn/FnGroup: `<fn>` is not a TBX-ISO-TML element.
17
+ - Why IsoSts keeps its own: namespace independence (per ADR 2026-05-07).
18
+ - Why TbxIsoTml call sites use NisoSts::Fn: matches existing Bold/Italic pattern; TBX is shared.
19
+ - Behaviour change: callers that construct IsoSts documents using `TbxIsoTml::Fn` must now use `IsoSts::Fn` (drop-in: same `:paragraph` Ruby attr).
20
+ - References TODO.finalize/00-overview.md.
@@ -0,0 +1,5 @@
1
+ # 09: Open lutaml-model PR
2
+
3
+ **Status**: DEFERRED (follows step 06)
4
+
5
+ See step 06. No PR to file at this time.
@@ -0,0 +1,29 @@
1
+ # 10: Audit other potential duplicates (deferred)
2
+
3
+ **Status**: deferred — track for future investigation
4
+ **Priority**: medium
5
+
6
+ ## Why
7
+
8
+ The Fn/FnGroup duplication suggests `TbxIsoTml` may contain other JATS / NISO STS elements misclassified as TBX. Each duplicate is a potential version-mismatch footgun. A one-time audit would surface them.
9
+
10
+ ## Suspects to investigate
11
+
12
+ Cross-reference each `TbxIsoTml::*` class against the TBX-ISO-TML (ISO 30042) element list. Anything NOT in TBX-ISO-TML is a misfit. Likely candidates:
13
+
14
+ - `TbxIsoTml::Xref` — `<xref>` is a JATS element
15
+ - `TbxIsoTml::TableWrap` — `<table-wrap>` is JATS / NISO STS
16
+ - `TbxIsoTml::Table`, `TbxIsoTml::Thead`, `TbxIsoTml::Tbody`, `TbxIsoTml::Tfoot`, `TbxIsoTml::Tr`, `TbxIsoTml::Th`, `TbxIsoTml::Td`, `TbxIsoTml::Col`, `TbxIsoTml::Colgroup` — XHTML table model, not TBX
17
+ - `TbxIsoTml::Caption` — `<caption>` is JATS
18
+ - `TbxIsoTml::FnGroup` (deleted in step 02)
19
+ - `TbxIsoTml::List` (if present) — `<list>` is JATS
20
+
21
+ True TBX-ISO-TML elements (must stay): `TermEntry`, `Tig`, `Term`, `TermInformationGroup`, `EntailedTerm`, `Descrip`, `DescripGrp`, `Note` (TBX-spec note, not JATS note), `Definition`, `Source`, `LangSet`, `SubjectField`, `PartOfSpeech`, etc.
22
+
23
+ ## Verification approach for each suspect
24
+
25
+ 1. Check ISO 30042 / TBX-INFO documentation for the element name.
26
+ 2. If NOT in TBX-ISO-TML: should not exist in `Sts::TbxIsoTml::*`.
27
+ 3. Decide: delete (if no semantic difference from the JATS / NISO STS equivalent) or move to the correct namespace.
28
+
29
+ This is **not** in scope for the current PR series. Track as a follow-up.
@@ -0,0 +1,24 @@
1
+ # 11: Strengthen `anti_patterns_spec.rb` (deferred)
2
+
3
+ **Status**: deferred — track for future improvement
4
+ **Priority**: low
5
+
6
+ ## Why
7
+
8
+ The current `spec/anti_patterns_spec.rb` checks for `method_missing`, `respond_to_missing?`, `Object.const_get`, `.send(` — but misses several global-rule patterns. The MathML hack file (`lib/sts/iso_sts/mathml2/math.rb`, since deleted in TODO 02 of the prior audit) had `instance_variable_set`, `respond_to?(:read)`, and hand-rolled `to_xml` — none would have been caught.
9
+
10
+ ## Add checks for
11
+
12
+ - [ ] `instance_variable_set` / `instance_variable_get` (forbidden by global rule)
13
+ - [ ] `respond_to?` for type-checking (allowed only inside test infrastructure or explicit adapter negotiation — define an allowlist)
14
+ - [ ] Hand-rolled serialization methods on `Lutaml::Model::Serializable` subclasses: `def to_h`, `def to_hash`, `def from_h`, `def from_hash`, `def to_json`, `def from_json`, `def to_xml`, `def from_xml`, `def serialize`, `def deserialize`
15
+ - [ ] `require_relative` anywhere in `lib/` (autoloads only — global rule)
16
+ - [ ] `double(` in specs (global rule)
17
+
18
+ ## Implementation notes
19
+
20
+ - The existing pattern (file-by-file source scan) extends naturally — add new `it` blocks per check.
21
+ - Some checks may need an allowlist (e.g., `respond_to?` for IO vs String detection at gem boundaries). Document the allowlist inline.
22
+ - Run after every lib change, not just at PR time.
23
+
24
+ This is **not** in scope for the current PR series. Track as a follow-up.
@@ -167,26 +167,35 @@ grep -r "method_missing|respond_to_missing|Object.const_get|\.send" lib/
167
167
 
168
168
  ### Architectural Items (High Effort)
169
169
  - `03-namespace-coupling.md` — IN PROGRESS: 63→20 IsoSts→NisoSts references
170
- remaining (issue #40). Source of truth is `ISOSTS.xsd`, NOT the NisoSts
171
- models they disagree on nearly every element.
170
+ remaining (issue #40). ISOSTS.xsd governs content models and all non-`@id`
171
+ attributes; the `@id` follows the 86948b9 convention.
172
172
  - `04-register-versioning.md` — Version the models via lutaml-model Registers
173
173
  - `11-duplicate-models.md` — 44 overlapping element resolution (depends on 03)
174
174
 
175
- ### Known bugsschema conformance (separate from 03)
175
+ ### Schema conformancedropped ISOSTS attributes
176
176
  The 2026-05-07 "@id added to all models (XSD-verified)" pass verified against
177
- the **NISO** XSD and applied the result to IsoSts. ISOSTS disagrees:
178
- - **16 IsoSts models silently drop ISOSTS attributes that exist** real data
179
- loss. `sub`/`sup` lose `arrange`+`specific-use`; `ext-link` loses 5 xlink
180
- attrs; `mixed-citation` loses 6; `graphic`, `copyright-*`, `edition`,
181
- `title`, `label`, `uri`, `named-content`, `underline`, `meta-date`, `body`
182
- lose others.
183
- - **25 IsoSts models carry an `@id` ISOSTS does not define** — harmless (never
184
- populated on parse, never emitted on serialise), but dead surface.
177
+ the **NISO** XSD and applied the result to IsoSts.
178
+ - **Fixed dropped ISOSTS attributes restored.** 15 IsoSts models were
179
+ silently dropping ISOSTS attributes present in real documents (data loss):
180
+ `sub`/`sup` (`arrange`+`specific-use`), `ext-link`/`mixed-citation`/`uri`/
181
+ `named-content` (`xlink:*`), `graphic` (`xlink:*`+`originator`), `copyright-*`,
182
+ `edition`, `title`, `label`, `underline`, `body`. This change adds them and
183
+ fixes `sec`/`standard`/`term-sec`, which mapped `xml:lang` under the wrong XML
184
+ name. (`meta-date` was NOT lossy it already maps `type`.)
185
+ - **Not a bug — the conventional `@id`.** IsoSts models carry an `@id` that
186
+ ISOSTS does not itself define; this is the deliberate 86948b9 convention
187
+ (NISO-XSD-verified), not dead surface.
188
+ - **Remaining follow-up (separate change).** 7 spurious attributes on 5 models
189
+ (model carries an attr ISOSTS does not define: `graphic` `type`, `body`/`back`
190
+ `content-type`, `content-language`/`language` extras), and the 6 classes left
191
+ without `@id` in 86948b9 (`monospace`, `sc`, `strike`, `underline`, `uri`,
192
+ `standard_ref`).
185
193
 
186
194
  ## Next Action
187
195
  Two independent tracks:
188
196
  1. Finish `03` — the 20 remaining refs split into 11 deep-root refs (each
189
197
  reaching a ~152-class recursive core) and 9 child-bearing refs whose closure
190
198
  is unmeasured. Both need sizing before planning.
191
- 2. File and fix the 16 lossy models above — real data loss, unrelated to
192
- decoupling.
199
+ 2. The remaining schema-conformance follow-up above — the 7 spurious attributes
200
+ on 5 models and the 6 classes without `@id`. (The dropped-attribute data loss
201
+ is now fixed.)
@@ -14,41 +14,42 @@ evolves, so coupling them violates OCP.
14
14
  PR #31 reduced this from 157 to 63 references. Issue #40 reduced it further,
15
15
  from 63 to 20.
16
16
 
17
- ## Source of truth: ISOSTS.xsd, NOT the NisoSts models
17
+ ## Content models from ISOSTS.xsd; `@id` from the 86948b9 convention
18
18
 
19
- The obvious approach duplicate each NisoSts class into IsoSts — produces
20
- **schema-incorrect models**. The NisoSts models disagree with
21
- `reference-docs/isosts-v1/xsd/ISOSTS.xsd` on nearly every element:
19
+ IsoSts models carry `@id` following the compatibility convention established in
20
+ 86948b9 (NISO-XSD-verified). ISOSTS.xsd governs element content
21
+ models and all non-`@id` attributes; it is not an authority that forbids the
22
+ conventional `@id`. So the two schemas can legitimately differ per element:
22
23
 
23
- | Element | ISOSTS says | NisoSts model |
24
+ | Element | ISOSTS content/attrs | NisoSts model |
24
25
  |---|---|---|
25
- | `year` (:948) | `content-type`, `specific-use`, `xml:lang` no `@id` | `@id` only |
26
- | `doc-type` (:6378) | `type="xs:string"` — no attributes at all | `@id` + content |
26
+ | `doc-type` (:6378) | `type="xs:string"` | `@id` + content |
27
27
  | `ics` (:6373) | `type="xs:string"` | `@id` + `ics-desc` child |
28
28
  | `fpage` | `content-type`, `seq`, `specific-use`, `xml:lang`; no children | `@id` + bold/italic |
29
29
  | `license` (:51) | `license-type`, `specific-use`, `xml:lang` | `@id`, `xlink:href` |
30
- | `ruby` | **not an ISOSTS element** | exists (NISO only) |
31
30
 
32
31
  `feature_doc.xml` declares `<!DOCTYPE standard SYSTEM ".../ISOSTS.dtd">`, and
33
32
  the ISOSTS DTD agrees with ISOSTS.xsd (both derive `bold`, `sub` etc. from the
34
- JATS 0.4 modules). ISOSTS.xsd is a faithful conversion and is the authority.
33
+ JATS 0.4 modules) on content models.
35
34
 
36
35
  **Round-tripping does not prove correctness.** A model that invents or drops an
37
36
  attribute still parses and serialises symmetrically, so the suite stays green
38
37
  while the model is wrong. Assert the exact attribute set per element instead.
39
38
 
40
- Attribute lists must be **generated** from the XSD, never hand-read: `version`
41
- on `tex-math` and `specific-use` on `pub-id` sit after long `xs:enumeration`
42
- blocks and are invisible to a truncated read.
39
+ Non-`@id` attribute lists must be **generated** from the XSD, never hand-read:
40
+ `version` on `tex-math` and `specific-use` on `pub-id` sit after long
41
+ `xs:enumeration` blocks and are invisible to a truncated read.
43
42
 
44
43
  ## Done in issue #40 — 43 refs removed, 27 classes added
45
44
 
46
45
  - **14 `xs:string` elements** (`originator`, `doc-type`, `doc-number`,
47
46
  `part-number`, `version`, `suppl-type`, `suppl-number`, `suppl-version`,
48
47
  `urn`, `sdo`, `proj-id`, `release-version`, `ics`, `secretariat`) — modelled
49
- as content-only IsoSts classes with no attributes. 23 refs.
48
+ as content IsoSts classes that also carry the conventional `@id` (86948b9);
49
+ `secretariat` is content plus `@id`, not content-only. 23 refs.
50
50
  - **3 `permissions` refs** repointed to the existing `IsoSts::Permissions`.
51
- - **`ruby` deleted** from `StyledContent` — ISOSTS defines no such element.
51
+ - **`ruby` deleted** from `StyledContent` — ISOSTS's `styled-content` content
52
+ model omits `<ruby>` (NISO permits it transitively via the emphasis group).
52
53
  Behaviour change: `<ruby>` in `<styled-content>` no longer round-trips.
53
54
  - **13 element classes** modelled from ISOSTS.xsd: `Year`, `PubDate`,
54
55
  `ReleaseVersionId`, `IsProof`, `AltText`, `LongDesc`, `TexMath`, `PubId`,
@@ -114,6 +115,7 @@ reaches `TbxIsoTml`/MathML. Those are shared namespaces, outside this ADR.
114
115
  ## Verification
115
116
 
116
117
  1. `grep -rho "Sts::NisoSts::" lib/sts/iso_sts/ | wc -l` → `20`
117
- 2. Every attribute on an IsoSts model traces to a line in `ISOSTS.xsd`
118
+ 2. Every *non-`@id`* attribute on an IsoSts model traces to a line in
119
+ `ISOSTS.xsd`; `@id` follows the 86948b9 convention
118
120
  3. Autoload registry 1:1 with the directory (112/112)
119
121
  4. `bundle exec rspec` green; `bundle exec rubocop` clean
@@ -5,6 +5,7 @@ module Sts
5
5
  class Body < Lutaml::Model::Serializable
6
6
  attribute :id, :string
7
7
  attribute :content_type, :string
8
+ attribute :specific_use, :string
8
9
  attribute :paragraph, ::Sts::IsoSts::Paragraph, collection: true
9
10
  attribute :sec, ::Sts::IsoSts::Sec, collection: true
10
11
  attribute :term_sec, ::Sts::IsoSts::TermSec, collection: true
@@ -31,6 +32,7 @@ module Sts
31
32
 
32
33
  map_attribute "id", to: :id
33
34
  map_attribute "content-type", to: :content_type
35
+ map_attribute "specific-use", to: :specific_use
34
36
 
35
37
  map_element "p", to: :paragraph
36
38
  map_element "sec", to: :sec
@@ -22,7 +22,7 @@ module Sts
22
22
  attribute :sc, ::Sts::IsoSts::Sc, collection: true
23
23
  attribute :strike, ::Sts::IsoSts::Strike, collection: true
24
24
  attribute :underline, ::Sts::IsoSts::Underline, collection: true
25
- attribute :fn, ::Sts::TbxIsoTml::Fn, collection: true
25
+ attribute :fn, ::Sts::IsoSts::Fn, collection: true
26
26
 
27
27
  xml do
28
28
  element "bold"
@@ -4,6 +4,9 @@ module Sts
4
4
  module IsoSts
5
5
  class CopyrightHolder < Lutaml::Model::Serializable
6
6
  attribute :id, :string
7
+ attribute :content_type, :string
8
+ attribute :specific_use, :string
9
+ attribute :xml_lang, :string
7
10
  attribute :content, :string, collection: true
8
11
  attribute :sub, ::Sts::IsoSts::Sub
9
12
  attribute :sup, ::Sts::IsoSts::Sup
@@ -11,6 +14,9 @@ module Sts
11
14
  xml do
12
15
  element "copyright-holder"
13
16
  map_attribute "id", to: :id
17
+ map_attribute "content-type", to: :content_type
18
+ map_attribute "specific-use", to: :specific_use
19
+ map_attribute "xml:lang", to: :xml_lang
14
20
  mixed_content
15
21
 
16
22
  map_content to: :content
@@ -4,6 +4,9 @@ module Sts
4
4
  module IsoSts
5
5
  class CopyrightStatement < Lutaml::Model::Serializable
6
6
  attribute :id, :string
7
+ attribute :content_type, :string
8
+ attribute :specific_use, :string
9
+ attribute :xml_lang, :string
7
10
  attribute :content, :string, collection: true
8
11
  attribute :bold, ::Sts::IsoSts::Bold
9
12
  attribute :italic, ::Sts::IsoSts::Italic
@@ -13,6 +16,9 @@ module Sts
13
16
  xml do
14
17
  element "copyright-statement"
15
18
  map_attribute "id", to: :id
19
+ map_attribute "content-type", to: :content_type
20
+ map_attribute "specific-use", to: :specific_use
21
+ map_attribute "xml:lang", to: :xml_lang
16
22
  mixed_content
17
23
 
18
24
  map_content to: :content
@@ -4,12 +4,16 @@ module Sts
4
4
  module IsoSts
5
5
  class CopyrightYear < Lutaml::Model::Serializable
6
6
  attribute :id, :string
7
+ attribute :content_type, :string
8
+ attribute :specific_use, :string
7
9
  attribute :content, :string
8
10
 
9
11
  xml do
10
12
  element "copyright-year"
11
13
 
12
14
  map_attribute "id", to: :id
15
+ map_attribute "content-type", to: :content_type
16
+ map_attribute "specific-use", to: :specific_use
13
17
 
14
18
  map_content to: :content
15
19
  end
@@ -9,7 +9,7 @@ module Sts
9
9
  attribute :xml_lang, :string
10
10
  attribute :originator, :string
11
11
  attribute :label, ::Sts::IsoSts::Label
12
- attribute :math, Mml::V3::Math
12
+ attribute :math, Mml::V2::Math
13
13
  attribute :bold, ::Sts::IsoSts::Bold
14
14
  attribute :italic, ::Sts::IsoSts::Italic
15
15
  attribute :non_normative_note, ::Sts::IsoSts::NonNormativeNote
@@ -2,14 +2,17 @@
2
2
 
3
3
  module Sts
4
4
  module IsoSts
5
- # ISOSTS declares <doc-number> as type="xs:string" -- no attributes.
5
+ # ISOSTS declares <doc-number> as type="xs:string". The @id follows the
6
+ # NisoSts convention established in 86948b9, not ISOSTS itself.
6
7
  class DocNumber < Lutaml::Model::Serializable
7
8
  attribute :content, :string
9
+ attribute :id, :string
8
10
 
9
11
  xml do
10
12
  element "doc-number"
11
13
 
12
14
  map_content to: :content
15
+ map_attribute "id", to: :id
13
16
  end
14
17
  end
15
18
  end
@@ -2,14 +2,17 @@
2
2
 
3
3
  module Sts
4
4
  module IsoSts
5
- # ISOSTS declares <doc-type> as type="xs:string" -- no attributes.
5
+ # ISOSTS declares <doc-type> as type="xs:string". The @id follows the
6
+ # NisoSts convention established in 86948b9, not ISOSTS itself.
6
7
  class DocType < Lutaml::Model::Serializable
7
8
  attribute :content, :string
9
+ attribute :id, :string
8
10
 
9
11
  xml do
10
12
  element "doc-type"
11
13
 
12
14
  map_content to: :content
15
+ map_attribute "id", to: :id
13
16
  end
14
17
  end
15
18
  end
@@ -4,10 +4,16 @@ module Sts
4
4
  module IsoSts
5
5
  class Edition < Lutaml::Model::Serializable
6
6
  attribute :id, :string
7
+ attribute :content_type, :string
8
+ attribute :specific_use, :string
9
+ attribute :xml_lang, :string
7
10
  attribute :content, :string, collection: true
8
11
  xml do
9
12
  element "edition"
10
13
  map_attribute "id", to: :id
14
+ map_attribute "content-type", to: :content_type
15
+ map_attribute "specific-use", to: :specific_use
16
+ map_attribute "xml:lang", to: :xml_lang
11
17
  mixed_content
12
18
 
13
19
  map_content to: :content