sts 0.6.2 → 0.6.4

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 (92) hide show
  1. checksums.yaml +4 -4
  2. data/.rubocop_todo.yml +1 -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 +30 -18
  16. data/TODO.sts-refactor/03-namespace-coupling.md +50 -32
  17. data/lib/sts/iso_sts/abbrev.rb +41 -0
  18. data/lib/sts/iso_sts/annotation.rb +31 -0
  19. data/lib/sts/iso_sts/body.rb +2 -3
  20. data/lib/sts/iso_sts/bold.rb +1 -1
  21. data/lib/sts/iso_sts/copyright_holder.rb +6 -0
  22. data/lib/sts/iso_sts/copyright_statement.rb +6 -0
  23. data/lib/sts/iso_sts/copyright_year.rb +4 -0
  24. data/lib/sts/iso_sts/country.rb +26 -0
  25. data/lib/sts/iso_sts/day.rb +24 -0
  26. data/lib/sts/iso_sts/doc_number.rb +4 -1
  27. data/lib/sts/iso_sts/doc_type.rb +4 -1
  28. data/lib/sts/iso_sts/edition.rb +6 -0
  29. data/lib/sts/iso_sts/email.rb +37 -0
  30. data/lib/sts/iso_sts/etal.rb +22 -0
  31. data/lib/sts/iso_sts/ext_link.rb +10 -0
  32. data/lib/sts/iso_sts/fax.rb +22 -0
  33. data/lib/sts/iso_sts/fpage.rb +2 -0
  34. data/lib/sts/iso_sts/front.rb +0 -2
  35. data/lib/sts/iso_sts/graphic.rb +10 -0
  36. data/lib/sts/iso_sts/ics.rb +4 -1
  37. data/lib/sts/iso_sts/inline_graphic.rb +42 -0
  38. data/lib/sts/iso_sts/institution.rb +41 -0
  39. data/lib/sts/iso_sts/is_proof.rb +7 -2
  40. data/lib/sts/iso_sts/issue.rb +2 -0
  41. data/lib/sts/iso_sts/italic.rb +1 -1
  42. data/lib/sts/iso_sts/label.rb +4 -0
  43. data/lib/sts/iso_sts/lpage.rb +2 -0
  44. data/lib/sts/iso_sts/milestone_end.rb +25 -0
  45. data/lib/sts/iso_sts/milestone_start.rb +25 -0
  46. data/lib/sts/iso_sts/mixed_citation.rb +15 -3
  47. data/lib/sts/iso_sts/month.rb +24 -0
  48. data/lib/sts/iso_sts/named_content.rb +2 -0
  49. data/lib/sts/iso_sts/num.rb +24 -0
  50. data/lib/sts/iso_sts/object_id.rb +24 -0
  51. data/lib/sts/iso_sts/originator.rb +4 -1
  52. data/lib/sts/iso_sts/page_range.rb +2 -0
  53. data/lib/sts/iso_sts/paragraph.rb +1 -1
  54. data/lib/sts/iso_sts/part_number.rb +4 -1
  55. data/lib/sts/iso_sts/phone.rb +22 -0
  56. data/lib/sts/iso_sts/proj_id.rb +4 -1
  57. data/lib/sts/iso_sts/pub_date.rb +2 -0
  58. data/lib/sts/iso_sts/pub_id.rb +2 -0
  59. data/lib/sts/iso_sts/publisher.rb +23 -0
  60. data/lib/sts/iso_sts/publisher_loc.rb +29 -0
  61. data/lib/sts/iso_sts/publisher_name.rb +23 -0
  62. data/lib/sts/iso_sts/release_version.rb +4 -1
  63. data/lib/sts/iso_sts/sdo.rb +4 -1
  64. data/lib/sts/iso_sts/season.rb +24 -0
  65. data/lib/sts/iso_sts/sec.rb +2 -5
  66. data/lib/sts/iso_sts/secretariat.rb +4 -1
  67. data/lib/sts/iso_sts/standard.rb +2 -2
  68. data/lib/sts/iso_sts/std.rb +1 -1
  69. data/lib/sts/iso_sts/sub.rb +4 -0
  70. data/lib/sts/iso_sts/sup.rb +4 -0
  71. data/lib/sts/iso_sts/suppl_number.rb +4 -1
  72. data/lib/sts/iso_sts/suppl_type.rb +4 -1
  73. data/lib/sts/iso_sts/suppl_version.rb +4 -1
  74. data/lib/sts/iso_sts/td.rb +1 -1
  75. data/lib/sts/iso_sts/term_sec.rb +1 -1
  76. data/lib/sts/iso_sts/th.rb +1 -1
  77. data/lib/sts/iso_sts/title.rb +4 -0
  78. data/lib/sts/iso_sts/underline.rb +2 -0
  79. data/lib/sts/iso_sts/uri.rb +2 -0
  80. data/lib/sts/iso_sts/urn.rb +4 -1
  81. data/lib/sts/iso_sts/version.rb +4 -1
  82. data/lib/sts/iso_sts/volume.rb +2 -0
  83. data/lib/sts/iso_sts/year.rb +2 -0
  84. data/lib/sts/iso_sts.rb +23 -2
  85. data/lib/sts/tbx_iso_tml/table_wrap_foot.rb +2 -2
  86. data/lib/sts/tbx_iso_tml/td.rb +1 -1
  87. data/lib/sts/tbx_iso_tml/th.rb +1 -1
  88. data/lib/sts/tbx_iso_tml.rb +0 -2
  89. data/lib/sts/version.rb +1 -1
  90. metadata +32 -3
  91. data/lib/sts/tbx_iso_tml/fn.rb +0 -20
  92. 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: 572accdd400693b1c1320ea6891f5be801f3917d64d98058f6da1b30c92436c2
4
- data.tar.gz: 3bd1daa1cd5335c796c21732e7034b5bfb23f522bdd1d9e7db85df32f07bcf2a
3
+ metadata.gz: c758f04c86a04df1adb4bc66d09aac7f31477d8ce6879bb9cd8935d86a7ae616
4
+ data.tar.gz: 56893d54b3e2193926f95a8a988d0a12dc73b31a85d9223e90edfe46adf80b07
5
5
  SHA512:
6
- metadata.gz: ebf47f3e1a2d0e4b95ccc6cba35d3e749cec5393847f21c420346a031e366ea3f33d9e2f641cc2a03fea4598b57a278d1539c3dcb777b9a1882e9bf45aa0711a
7
- data.tar.gz: 73b5f522a8d2177c58ac7ecb1d618d2c5e270dddac0113383ee567eb32cabb2022c70f1d0559bde2b5c5d613f72dd0608905fde0549fadc4a35e280b2d26fb46
6
+ metadata.gz: e2444f3c73074bd001445a89a5712854e112b429180b7a96feff1e01e303dfd5791a7ab6e81eb27498ba859078a8a647793a8e40276ec6e2cab2d90c9b7a4517
7
+ data.tar.gz: d3b0d7c1ba7721b108ae509b5847e1a21917d4cc5507581d7581af2d71a0f742d16b316fc53a3d8c263b11816ee074a6b5a17547b9773dd3f99e472959db9069
data/.rubocop_todo.yml CHANGED
@@ -70,6 +70,7 @@ 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'
73
74
  - 'spec/mathml_version_spec.rb'
74
75
  - 'spec/round_trip/reference_docs_spec.rb'
75
76
 
@@ -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.
@@ -19,7 +19,7 @@
19
19
  |---|------|----------|----------|--------------|
20
20
  | 01 | `01-mathml-delegation.md` | Anti-Pattern Fix | DONE | — |
21
21
  | 02 | `02-type-resolution.md` | Anti-Pattern Fix | DONE | — |
22
- | 03 | `03-namespace-coupling.md` | Architecture | IN PROGRESS (63→20 refs) | 01, 02 |
22
+ | 03 | `03-namespace-coupling.md` | Architecture | IN PROGRESS (63→16 refs) | 01, 02 |
23
23
  | 04 | `04-register-versioning.md` | Architecture | HIGH | 01, 02 |
24
24
  | 05 | `05-missing-elements.md` | Feature Gap | MOSTLY DONE | 04 |
25
25
  | 06 | `06-missing-attributes.md` | Feature Gap | DONE | 04 |
@@ -166,27 +166,39 @@ grep -r "method_missing|respond_to_missing|Object.const_get|\.send" lib/
166
166
  - ~~Expand StringName usage (in contrib, element-citation, related-article — NISO STS 1.2)~~ → DONE: added to name-alternatives; already in person-group and mixed-citation
167
167
 
168
168
  ### Architectural Items (High Effort)
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.
169
+ - `03-namespace-coupling.md` — IN PROGRESS: 63→16 IsoSts→NisoSts references
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
- 1. Finish `03` — the 20 remaining refs split into 11 deep-root refs (each
189
- reaching a ~152-class recursive core) and 9 child-bearing refs whose closure
190
- is unmeasured. Both need sizing before planning.
191
- 2. File and fix the 16 lossy models above — real data loss, unrelated to
192
- decoupling.
196
+ 1. Finish `03` — the 16 remaining refs split evenly between seven recursive
197
+ roots and five child-bearing roots. The first 15 high-reuse dependency
198
+ leaves are now modelled, reducing the unresolved closures to 78–84
199
+ not-yet-modelled ISOSTS elements before existing IsoSts boundaries. The
200
+ remaining work still needs an architecture decision for the recursive
201
+ content core.
202
+ 2. The remaining schema-conformance follow-up above — the 7 spurious attributes
203
+ on 5 models and the 6 classes without `@id`. (The dropped-attribute data loss
204
+ is now fixed.)
@@ -3,7 +3,7 @@
3
3
  **Priority**: HIGH
4
4
  **Category**: Architecture
5
5
  **Estimated Effort**: High
6
- **Status**: Partially done — 63 → 20 references (GitHub issue #40)
6
+ **Status**: Partially done — 63 → 16 references (GitHub issue #40)
7
7
 
8
8
  ## Problem
9
9
 
@@ -12,49 +12,63 @@ independent of NisoSts (ADR 2026-05-07): ISOSTS is frozen legacy, NISO STS
12
12
  evolves, so coupling them violates OCP.
13
13
 
14
14
  PR #31 reduced this from 157 to 63 references. Issue #40 reduced it further,
15
- from 63 to 20.
15
+ from 63 to 16.
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
- ## Done in issue #40 — 43 refs removed, 27 classes added
43
+ ## Done in issue #40 — 47 refs removed, 46 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`,
55
56
  `Volume`, `Issue`, `Fpage`, `Lpage`, `PageRange`. 16 refs.
56
57
  - **`WiNumber` added** — ISOSTS gives `wi-number` an `@id`; it was typed as a
57
58
  plain string, silently discarding that `@id`.
59
+ - **`Publisher` closure added** — `Publisher`, `PublisherName`, `PublisherLoc`,
60
+ and `Email` are modelled from ISOSTS.xsd; `PublisherLoc` reuses the existing
61
+ IsoSts `ExtLink` and `Uri` boundary models. All four retain the
62
+ project-mandated `@id` model surface. 1 ref.
63
+ - **15 reusable dependency leaves added** — `Abbrev`, `Annotation`, `Country`,
64
+ `Day`, `Etal`, `Fax`, `InlineGraphic`, `Institution`, `MilestoneEnd`,
65
+ `MilestoneStart`, `Month`, `Num`, `ObjectId`, `Phone`, and `Season` are
66
+ modelled from ISOSTS.xsd and close entirely on existing IsoSts types. They
67
+ remove no direct reference yet, but establish the first shared dependency
68
+ layer for the remaining recursive roots.
69
+ - **3 Niso-only child refs deleted** — `std-meta` from `Front` and
70
+ `editing-instruction` from `Body` and `Sec`. Neither element exists in
71
+ ISOSTS.xsd, so adding IsoSts equivalents would incorrectly expand the schema.
58
72
 
59
73
  ### Why classes and not plain `:string`
60
74
 
@@ -65,20 +79,23 @@ round-trip. For a scalar, `render_empty: :empty` recovers it; for a collection
65
79
  to `[]`, destroying the information at parse time before any render option
66
80
  applies. Content-only classes round-trip every case.
67
81
 
68
- ## Remaining — 20 refs
82
+ ## Remaining — 16 refs
69
83
 
70
- **11 refs to 9 deep roots**: `MetadataStd`, `ElementCitation`, `PersonGroup`,
71
- `Collab`, `Source`, `DispQuote`, `EditingInstruction`, `TermDisplay`,
72
- `BoxedText`. Each reaches the same ~152-class mutually-recursive core
73
- (`Section` → `Paragraph` → `DispQuote` → `Paragraph`). `DispQuote` has 3 direct
74
- children but pulls all 152. No incremental path — needs its own decision.
84
+ **8 refs to 7 recursive roots**: `ElementCitation`, `PersonGroup`, `Collab`,
85
+ `Source`, `DispQuote`, `TermDisplay`, and `BoxedText`. Each reaches the same
86
+ 78–79-element mutually-recursive core before existing IsoSts boundaries
87
+ (`sec` → `p` → `disp-quote` → `p`). No direct one-file path remains.
75
88
 
76
- **9 refs whose ISOSTS content models have real children**, closure not measured:
77
- `attrib` (:1082), `term-head` (:4308), `article-title`, `license` `license-p`
78
- (:51), `publisher` `publisher-loc`, `custom-meta-group` → `custom-meta`.
79
- `attrib` and `term-head` are **not** trivial leaves in ISOSTS despite what the
80
- NisoSts models suggest; `license-p` reaches `array`/`alternatives`, so these may
81
- not be cheap. Measure before planning.
89
+ **8 refs to 5 child-bearing roots**, now measured after the first dependency
90
+ layer: `attrib` (:1082) reaches 78 not-yet-modelled ISOSTS elements before
91
+ existing IsoSts boundaries, `term-head` (:4308) reaches 79, `article-title`
92
+ reaches 78, `license` `license-p` (:51) reaches 84, and
93
+ `custom-meta-group` `custom-meta` reaches 82. They converge on the same
94
+ recursive content core as the deep roots, so none is another bounded model
95
+ batch. `publisher` was the only exception: its four-element closure
96
+ (`publisher`, `publisher-name`, `publisher-loc`, `email`) is complete.
97
+ The Niso `Attrib` class is only 17 lines because it omits all 37 ISOSTS child
98
+ types; copying it would silently drop valid ISO content rather than decouple it.
82
99
 
83
100
  ## Rejected: three-tier `Sts::Base` hierarchy
84
101
 
@@ -113,7 +130,8 @@ reaches `TbxIsoTml`/MathML. Those are shared namespaces, outside this ADR.
113
130
 
114
131
  ## Verification
115
132
 
116
- 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
- 3. Autoload registry 1:1 with the directory (112/112)
133
+ 1. `grep -rho "Sts::NisoSts::" lib/sts/iso_sts/ | wc -l` → `16`
134
+ 2. Every *non-`@id`* attribute on an IsoSts model traces to a line in
135
+ `ISOSTS.xsd`; `@id` follows the 86948b9 convention
136
+ 3. Autoload registry 1:1 with the directory (131/131)
119
137
  4. `bundle exec rspec` green; `bundle exec rubocop` clean
@@ -0,0 +1,41 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Sts
4
+ module IsoSts
5
+ class Abbrev < Lutaml::Model::Serializable
6
+ attribute :id, :string
7
+ attribute :content_type, :string
8
+ attribute :specific_use, :string
9
+ attribute :alt, :string
10
+ attribute :xml_lang, :string
11
+ attribute :xlink_type, :string
12
+ attribute :xlink_href, :string
13
+ attribute :xlink_role, :string
14
+ attribute :xlink_title, :string
15
+ attribute :xlink_show, :string
16
+ attribute :xlink_actuate, :string
17
+ attribute :content, :string, collection: true
18
+ attribute :def, ::Sts::IsoSts::Def, collection: true
19
+
20
+ xml do
21
+ element "abbrev"
22
+ mixed_content
23
+
24
+ map_attribute "id", to: :id
25
+ map_attribute "content-type", to: :content_type
26
+ map_attribute "specific-use", to: :specific_use
27
+ map_attribute "alt", to: :alt
28
+ map_attribute "xml:lang", to: :xml_lang
29
+ map_attribute "xlink:type", to: :xlink_type
30
+ map_attribute "xlink:href", to: :xlink_href
31
+ map_attribute "xlink:role", to: :xlink_role
32
+ map_attribute "xlink:title", to: :xlink_title
33
+ map_attribute "xlink:show", to: :xlink_show
34
+ map_attribute "xlink:actuate", to: :xlink_actuate
35
+
36
+ map_content to: :content
37
+ map_element "def", to: :def
38
+ end
39
+ end
40
+ end
41
+ end