<?xml version="1.0" encoding="UTF-8"?>
<metanorma xmlns="https://www.metanorma.org/ns/standoc" type="semantic" version="2.9.0" schema-version="v2.2.0" flavor="ribose">
<bibdata type="standard">
<title language="en" type="main">LutaML — Model Evolution</title>
<docidentifier primary="true" type="Ribose Inc.">RS 3007</docidentifier><docnumber>RS 3007</docnumber><date type="published"><on>2025-02-20</on></date><contributor><role type="author"/><organization>
<name>Ribose Inc.</name>
<address><formattedAddress>167-169 Great Portland Street<br/>5th Floor<br/>London<br/>W1W 5PF<br/>United Kingdom</formattedAddress></address></organization></contributor><contributor><role type="author"/><person>
<name><completename>Ronald Tse</completename></name>
<affiliation><organization>
<name>Ribose</name>
</organization></affiliation></person></contributor><contributor><role type="author"><description>committee</description></role><organization>
<name>Ribose Inc.</name>
<subdivision type="Committee" subtype="technical">
<name>LutaML</name>
</subdivision></organization></contributor><contributor><role type="publisher"/><organization>
<name>Ribose Inc.</name>
<address><formattedAddress>167-169 Great Portland Street<br/>5th Floor<br/>London<br/>W1W 5PF<br/>United Kingdom</formattedAddress></address></organization></contributor><edition>1.0</edition><language>en</language><script>Latn</script><status><stage>published</stage></status><copyright><from>2026</from><owner><organization>
<name>Ribose Inc.</name>
<address><formattedAddress>167-169 Great Portland Street<br/>5th Floor<br/>London<br/>W1W 5PF<br/>United Kingdom</formattedAddress></address></organization></owner></copyright><ext><doctype>standard</doctype><flavor>ribose</flavor><security>unrestricted</security></ext></bibdata><metanorma-extension><semantic-metadata><stage-published>true</stage-published></semantic-metadata>
<presentation-metadata><toc-heading-levels>2</toc-heading-levels><html-toc-heading-levels>2</html-toc-heading-levels><doc-toc-heading-levels>2</doc-toc-heading-levels><pdf-toc-heading-levels>2</pdf-toc-heading-levels></presentation-metadata></metanorma-extension>
<boilerplate><copyright-statement>

<clause id="_81bb91f1-8ca7-1476-7150-6c55a0e1276d" obligation="normative"><p id="_2f99fb1a-6c8b-c002-e8cc-ea45a873756c">© Ribose Inc. 2026</p>
</clause>
</copyright-statement>

<legal-statement>

<clause id="_7e0f3c63-3fc9-65cf-37f7-b94afa0dbc5d" obligation="normative"><p id="_50bbfa02-61c3-8332-bad8-61e7389d1808">All rights reserved. Unless otherwise specified, no part of this publication may be reproduced or utilized otherwise in any form or by any means, electronic or mechanical, including photocopying, or posting on the internet or an intranet, without prior written permission. Permission can be requested from the address below.</p>
</clause>
</legal-statement>

<feedback-statement>

<clause id="_310a94ed-9b3b-55ae-882d-7b9734a5474e" obligation="normative"><p id="_e252883d-879c-4220-abfd-4f8ea82738e9" anchor="boilerplate-name" align="left">Ribose Inc.</p>

<p id="_ace8af1b-8faf-1380-6497-ea3b3ea492a9" anchor="boilerplate-address" align="left">167-169 Great Portland Street<br/>5th Floor<br/>London<br/>W1W 5PF<br/>United Kingdom<br/> <br/> <link target="mailto:copyright@ribose.com"/><br/> <link target="https://www.ribose.com">www.ribose.com</link></p>
</clause>
</feedback-statement>
</boilerplate><preface><introduction id="_f041af11-aac7-c096-5e93-afcca304203c" obligation="informative">
<title id="_2b2e98d1-114a-3da4-8556-01ae0a724280">Introduction</title>
<p id="_6184613e-222d-ad41-4d30-691d5f3f7293">This document defines how LutaML models evolve over time. It covers the version surfaces, what each change level means for consumers, and the co-release discipline for keeping them aligned.</p>
</introduction></preface><sections>
<clause id="_88db57dd-7341-4b48-7ed8-19d2f4687cf9" obligation="normative">
<title id="_8050cf45-c4bc-df84-8f05-0f30bb993f80">Version surfaces</title>
<p id="_39fc860b-02cd-348c-6d8d-7685f7b27b4c">A LutaML model has three independently versioned surfaces:</p>

<ul id="_166ba87f-c691-2472-8d15-0e9a15e618f4"><li><p id="_46ab9e20-bd46-0bd2-2e9b-41ee32e5803b">Grammar artifact version: the compiled PARG grammar. Governed by <tt>lutaml-lml</tt>‘s artifact version (semver; capture renames/removals = major, new alternatives = minor, literal fixes = patch).</p>
</li>
<li><p id="_ee070a8f-619c-4021-e22a-d71a43f38552">Model version: declared on the <tt>models</tt> header (<tt>models &lt;Name&gt; version "x.y.z"</tt>, per RS 3001 lutaml-lml#16). This is the  <strong>model schema</strong> version — what downstream code generators (pubid-ts, pubid-rs) materialize from.</p>
</li>
<li><p id="_0abd58a6-c972-a63a-63af-953e43ca900e">Corpus version: the conformance corpus (fixtures) that verify model compatibility. Bumped in lockstep with model version.</p>
</li>
</ul>
</clause>

<clause id="_3fff0801-df55-fa9a-9ffc-cdc8973cba70" obligation="normative">
<title id="_c8c7dde4-5e8c-ab41-558d-108d5db73a16">Change levels</title>
<clause id="_09aad1df-660f-0348-46e8-dc69a82d599c" obligation="normative">
<title id="_e9b9809c-ce72-568d-b59d-f9309d52a1e3">Patch (x.y.z → x.y.z+1)</title>
<p id="_60c289f8-e0a5-51cf-e23b-19e435707547">Safe changes that require no consumer action:</p>

<ul id="_e2178551-b778-4369-9326-e4afbab56eed"><li><p id="_332ec217-bdce-3b81-6317-e4a3d837d282">grammar literal fixes (typo corrections, error message improvements)</p>
</li>
<li><p id="_0126782e-7219-2b10-7ebe-744fbb43bc88">documentation updates</p>
</li>
<li><p id="_568f4f89-1b5f-cc42-60bd-097d8ca7d7a0">corpus fixture additions that do not change model semantics</p>
</li>
</ul>
</clause>

<clause id="_bcab355c-15a6-aab8-d236-94c5f326c505" obligation="normative">
<title id="_96de3946-59fe-f81a-59c3-4710464a920e">Minor (x.y.z → x.y.(z+1))</title>
<p id="_f990377c-c4b4-5f90-184e-f4424b9f0496">Additions that preserve backward compatibility:</p>

<ul id="_3a801180-9de0-147c-f2e8-5c2fef3b9314"><li><p id="_33a0b0a0-03d9-a280-94a9-2cbe78deedbe">new <tt>class</tt>, <tt>enum</tt>, <tt>data_type</tt>, <tt>primitive</tt>, <tt>package</tt>, or <tt>association</tt> declarations in existing models</p>
</li>
<li><p id="_014e7d32-3e98-e331-e861-90586683edd8">new attributes on existing classes (consumers must tolerate unknown fields)</p>
</li>
<li><p id="_569131eb-16a5-2157-a04d-f56c3e7471bf">new <tt>values</tt> members on existing enums</p>
</li>
<li><p id="_3213caf7-27b6-f573-2cbb-f732424e5ba7">new grammar alternatives (previously invalid LML text now parses)</p>
</li>
</ul>
</clause>

<clause id="_6f877c42-6464-646c-3d1b-45ad6bde7251" obligation="normative">
<title id="_d15e8a8e-5074-814c-965a-245e0aa6e5ee">Major (x.y.z → x.(y+1).0)</title>
<p id="_114b19e8-cf00-4516-2504-9dbb0155ea84">Breaking changes that require consumer migration:</p>

<ul id="_f1fe219d-0886-3d45-218d-4cbfa17ab4bb"><li><p id="_afa399ef-05dc-56d8-f86c-78b31e58aee7">removal or rename of a class, attribute, or enum value</p>
</li>
<li><p id="_706000b5-b1f3-bafb-4d37-52ba5143b24b">change of an attribute’s type or cardinality</p>
</li>
<li><p id="_e4631de0-d5e7-53d7-20e1-134fe71fafed">grammar capture renames or removals (the artifact artifact changes shape)</p>
</li>
<li><p id="_bcd90d9d-1879-654a-96de-5bc796a1fef4">semantic changes to hydration behavior</p>
</li>
</ul>
</clause>
</clause>

<clause id="_07c2989d-6d27-5e2f-cd30-97d386d0dc17" obligation="normative">
<title id="_18246cf0-283e-edca-62d1-0d2bd430058f">Co-release discipline</title>
<p id="_d203c7b2-3ec2-aa7b-529d-96a2dd84255e">The model schema version, corpus version, and grammar artifact version move  <strong>together</strong> — one release, no mixed states.</p>

<p id="_18e09cda-0c9f-83b0-2b1a-bced406b00d6">A co-release consists of:</p>

<ol id="_a863de59-96d0-871d-08d4-de3f70b850c2" type="arabic"><li><p id="_61f62d3b-e013-51bd-691f-b1a21e11c5aa">a <tt>models &lt;Name&gt; version "x.y.z"</tt> bump (if the model schema changed)</p>
</li>
<li><p id="_a9655bd0-344e-83d4-72a5-65d3ae2e949d">a conformance corpus update reflecting the change</p>
</li>
<li><p id="_4e813601-1b8e-34a5-1c32-a5d01b02f51e">a grammar artifact rebuild if the grammar changed</p>
</li>
<li><p id="_645dbab8-3271-582d-4b8e-229c68139a9a">a semver-consistent <tt>lutaml-lml</tt> release</p>
</li>
</ol>

<p id="_64123ddc-2cd5-b450-8fc6-1da8e04f30bc">Conformance runners must verify all three versions agree before running the suite.</p>
</clause>

<clause id="_43491415-5f0a-6722-23f8-b8daf6394ded" obligation="normative">
<title id="_0766309e-e928-f16a-1fe4-f70fa8a7a91f">Version declaration</title>
<p id="_b53d880e-206f-c1fb-73a0-886aeaeea3d0">The version is declared in the LML source itself:</p>

<sourcecode id="_060cd19b-2a89-e97a-0d38-14c2bf41acff" lang="lml"><body>models PubidModels version "1.0.0" {
  class IsoIdentifier { }
}</body></sourcecode>


<p id="_8213f7ba-0a29-707e-cf1f-f6f85cdb29c1">This is recorded on <tt>Document#model_versions</tt> (name → version) and is the single source of truth for the model schema version.</p>
</clause>

<clause id="_830aa815-6f5e-d76f-8c1a-a0f0c4dceabf" obligation="normative">
<title id="_3e5317e4-e475-0572-d85a-afdd41b0370a">Runtime model evolution (lutaml-model DSL)</title>
<p id="_4b1df205-48a0-115f-6f6b-c7d6ff67a235">The LML governance above governs the <strong>artifact</strong>. The lutaml-model Ruby DSL implements the  <strong>runtime</strong> counterpart: models declare their version, families of versioned classes share a document detector, and explicit wire-level steps migrate documents between versions.</p>

<clause id="_12eb07b1-fdf8-7d42-de6b-659c49178d4c" obligation="normative">
<title id="_9145af69-4f1a-96fe-7eb8-f43c6a2cfc56">Version declaration</title>
<sourcecode id="_ed7dbcce-d689-b287-f318-e05a86979df6" lang="ruby"><body>class MyModel &lt; Lutaml::Model::Serializable
  version 1
  attribute :name, :string
end

class MyModelV2 &lt; MyModel
  version 2
  attribute :created, Lutaml::Model::Type::DateTime
end</body></sourcecode>


<p id="_bf10a8a4-09ea-eba7-d349-6961252bf299"><tt>version</tt> stamps the model schema version. Subclasses inherit the nearest ancestor’s version;  <tt>MyModelV2.model_version</tt> reads it. A <strong>family</strong> is the ancestor chain of version-declared classes.</p>
</clause>

<clause id="_d090eef0-18f9-3a32-5301-d54f2903f4c5" obligation="normative">
<title id="_e06c1c95-2eae-ffd8-c4e9-663089f0ae35">Version detection</title>
<p id="_32e33094-0d07-c577-43d7-97844df7a726">A family head declares a detector that receives the parsed document and returns the version the document was written against:</p>

<sourcecode id="_7a75c7a5-7ec0-8b4e-5074-a76d830c0640" lang="ruby"><body>class MyModelV2 &lt; MyModel
  version 2
  detect_version do |doc|
    doc["schema_version"] ? doc["schema_version"].to_i : 1
  end
end</body></sourcecode>


<p id="_c77b139c-bf07-7680-edf5-6cfd99b8c7d5"><tt>from_json</tt>, <tt>from_yaml</tt> and the other key-value entry points route to the detected version’s class before parsing, raising <tt>UnsupportedVersionError</tt> when no family member declares the version. Detection is opt-in: without a detector, parsing is unchanged.</p>
</clause>

<clause id="_ea258d66-fa18-aaba-35bb-eaa2bf7be3e7" obligation="normative">
<title id="_aabe4fac-a128-09c5-a0a8-be0f7e7bf6c5">Migration steps</title>
<p id="_d785eb90-f486-391a-169c-ff7526365e4f">An <tt>evolution from:</tt> block declares wire-level steps on the target (newest) class. Steps apply in declaration order to a copy of the source document’s wire hash; the evolved hash then parses as the target model.</p>

<table id="_75e14f60-2622-af61-8c78-d06de4203ed9"><colgroup><col width="30%"/><col width="20%"/><col width="50%"/></colgroup><thead><tr id="_392a648f-0ccd-4f56-2cae-65c4f1052bd6"><th id="_73f1b117-e691-1b9e-0779-f80557b91fa9" valign="top" align="left">Step</th>
<th id="_0fa7eb7e-9dfe-5bfb-cd9c-12ef678a3047" valign="top" align="left">Arguments</th>
<th id="_c118e5f6-4555-c09d-2b26-490674b74fd7" valign="top" align="left">Semantics</th>
</tr></thead>
<tbody><tr id="_4fbea80d-0028-0138-b838-7b3816b4d422"><td id="_50f0f490-17ff-3918-5f3f-1f6ef9da1f34" valign="top" align="left"><tt>wire_rename</tt></td>
<td id="_fbd3e870-836d-671e-c32b-a53ed599c60a" valign="top" align="left"><tt>from_key, to:</tt></td>
<td id="_afa45c53-652d-03cd-d802-1c504d447760" valign="top" align="left">Accept a former wire name for the current attribute; the current name wins when both appear. Not re-emitted on serialization.</td>
</tr><tr id="_94768a8f-dbb4-4456-3ba4-9a7b4a844660"><td id="_550757e9-1292-8176-faba-8cd0a51ec3e6" valign="top" align="left"><tt>wire_move</tt></td>
<td id="_61e2789e-1426-8f73-c844-ce2bdaa4e40d" valign="top" align="left"><tt>from_key, to:</tt></td>
<td id="_7747d91b-fe86-32b8-f5fb-799b2f60dcb3" valign="top" align="left">Relocate a former top-level key into a nested position (dot path or segment array); intermediates are created.</td>
</tr><tr id="_6a4d79ce-80db-2fa8-09bd-06728bd05f6a"><td id="_3a3547c3-aecf-fea1-e635-042e43b35172" valign="top" align="left"><tt>wire_merge</tt></td>
<td id="_c52d553a-9721-7ad3-c896-bd37ebeca3f3" valign="top" align="left"><tt>from_keys, to:, with:</tt></td>
<td id="_4b98fe9e-d0ab-264e-bd21-82f3204811af" valign="top" align="left">Combine former keys into one. <tt>with:</tt> receives the source values in declared order; without it, the sources must all be hashes and are merged in order.</td>
</tr><tr id="_dd304831-7cfe-aa60-11d5-a898b8c68d8c"><td id="_dca1c4b4-3d9f-f077-1741-90592168bcc2" valign="top" align="left"><tt>wire_drop</tt></td>
<td id="_3a4d6008-a18a-cc84-3d90-bed1fcf03cfd" valign="top" align="left"><tt>*keys</tt></td>
<td id="_4c41fe59-f36c-df05-e2d3-c1cde2b2db52" valign="top" align="left">Retire former keys explicitly.</td>
</tr></tbody>
</table>

<sourcecode id="_e150d357-9682-2689-8b98-b47a6d76abd9" lang="ruby"><body>class MyModelV2 &lt; MyModel
  version 2

  evolution from: MyModel do
    wire_rename "author", to: "creator"
    wire_merge %w[first_name last_name], to: "full_name",
               with: -&gt;(first, last) { [first, last].compact.join(" ") }
    wire_drop "deprecated_field"
  end
end

doc = MyModelV2.evolve(old_document_or_instance, from: MyModel)</body></sourcecode>


<p id="_49c6b83f-b777-b399-8808-b864c6463298"><tt>evolve</tt> walks every declared family link between the source version and the target (chains compose transitively), never mutates the caller’s hash, and parses the evolved wire through the hash format. Migration is explicit; detection alone  <strong>accommodates</strong> (routes), <tt>evolve</tt> <strong>migrates</strong>.</p>
</clause>

<clause id="_bfc36d92-314f-9d65-20e9-df4398dbba37" obligation="normative">
<title id="_064bf26d-f124-b949-28f5-2b9a4e5e73de">Scope and guarantees</title>
<ul id="_524c8ebd-c903-94b7-b77d-2b28b1c6c382"><li><p id="_3b3984d6-9528-3f2a-294e-81302f482151">Key-value formats (JSON, YAML, TOML, hash). XML documents keep their structure through the parse complement; XML structural moves are not covered by the wire steps.</p>
</li>
<li><p id="_809c0c1f-a8ec-330e-7dae-09de2e9bf44d">The source hash is never mutated.</p>
</li>
<li><p id="_11025c97-ceca-2789-21e2-126ccb409847">Composed chains are the verified path: upcast then serialize yields a current-version document, and re-parse is idempotent.</p>
</li>
</ul>
</clause>
</clause>
</sections>
</metanorma>
