sysmlpy 0.81.0__tar.gz → 0.83.0__tar.gz

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 (160) hide show
  1. {sysmlpy-0.81.0 → sysmlpy-0.83.0}/PKG-INFO +1 -1
  2. {sysmlpy-0.81.0 → sysmlpy-0.83.0}/pyproject.toml +1 -1
  3. {sysmlpy-0.81.0 → sysmlpy-0.83.0}/src/sysmlpy/__init__.py +2 -2
  4. {sysmlpy-0.81.0 → sysmlpy-0.83.0}/src/sysmlpy/__main__.py +25 -0
  5. sysmlpy-0.83.0/src/sysmlpy/diff.py +541 -0
  6. {sysmlpy-0.81.0 → sysmlpy-0.83.0}/src/sysmlpy/lsp/server.py +466 -46
  7. {sysmlpy-0.81.0 → sysmlpy-0.83.0}/src/sysmlpy/usage.py +9 -0
  8. sysmlpy-0.81.0/src/sysmlpy/diff.py +0 -280
  9. {sysmlpy-0.81.0 → sysmlpy-0.83.0}/LICENSE +0 -0
  10. {sysmlpy-0.81.0 → sysmlpy-0.83.0}/README.md +0 -0
  11. {sysmlpy-0.81.0 → sysmlpy-0.83.0}/src/sysmlpy/antlr/README.md +0 -0
  12. {sysmlpy-0.81.0 → sysmlpy-0.83.0}/src/sysmlpy/antlr/SysMLv2Lexer.interp +0 -0
  13. {sysmlpy-0.81.0 → sysmlpy-0.83.0}/src/sysmlpy/antlr/SysMLv2Lexer.py +0 -0
  14. {sysmlpy-0.81.0 → sysmlpy-0.83.0}/src/sysmlpy/antlr/SysMLv2Lexer.tokens +0 -0
  15. {sysmlpy-0.81.0 → sysmlpy-0.83.0}/src/sysmlpy/antlr/SysMLv2Parser.interp +0 -0
  16. {sysmlpy-0.81.0 → sysmlpy-0.83.0}/src/sysmlpy/antlr/SysMLv2Parser.py +0 -0
  17. {sysmlpy-0.81.0 → sysmlpy-0.83.0}/src/sysmlpy/antlr/SysMLv2Parser.tokens +0 -0
  18. {sysmlpy-0.81.0 → sysmlpy-0.83.0}/src/sysmlpy/antlr/SysMLv2ParserListener.py +0 -0
  19. {sysmlpy-0.81.0 → sysmlpy-0.83.0}/src/sysmlpy/antlr/SysMLv2ParserVisitor.py +0 -0
  20. {sysmlpy-0.81.0 → sysmlpy-0.83.0}/src/sysmlpy/antlr/src/sysmlpy/grammar/antlr4/SysMLv2Lexer.interp +0 -0
  21. {sysmlpy-0.81.0 → sysmlpy-0.83.0}/src/sysmlpy/antlr/src/sysmlpy/grammar/antlr4/SysMLv2Lexer.py +0 -0
  22. {sysmlpy-0.81.0 → sysmlpy-0.83.0}/src/sysmlpy/antlr/src/sysmlpy/grammar/antlr4/SysMLv2Lexer.tokens +0 -0
  23. {sysmlpy-0.81.0 → sysmlpy-0.83.0}/src/sysmlpy/antlr_parser.py +0 -0
  24. {sysmlpy-0.81.0 → sysmlpy-0.83.0}/src/sysmlpy/antlr_visitor.py +0 -0
  25. {sysmlpy-0.81.0 → sysmlpy-0.83.0}/src/sysmlpy/boxes_view.py +0 -0
  26. {sysmlpy-0.81.0 → sysmlpy-0.83.0}/src/sysmlpy/definition.py +0 -0
  27. {sysmlpy-0.81.0 → sysmlpy-0.83.0}/src/sysmlpy/evaluator.py +0 -0
  28. {sysmlpy-0.81.0 → sysmlpy-0.83.0}/src/sysmlpy/examples/attribute_values.py +0 -0
  29. {sysmlpy-0.81.0 → sysmlpy-0.83.0}/src/sysmlpy/examples/part_attributes.py +0 -0
  30. {sysmlpy-0.81.0 → sysmlpy-0.83.0}/src/sysmlpy/examples/tuples_sequences.py +0 -0
  31. {sysmlpy-0.81.0 → sysmlpy-0.83.0}/src/sysmlpy/formatting.py +0 -0
  32. {sysmlpy-0.81.0 → sysmlpy-0.83.0}/src/sysmlpy/grammar/antlr4/README.md +0 -0
  33. {sysmlpy-0.81.0 → sysmlpy-0.83.0}/src/sysmlpy/grammar/antlr4/SysMLv2Lexer.g4 +0 -0
  34. {sysmlpy-0.81.0 → sysmlpy-0.83.0}/src/sysmlpy/grammar/antlr4/SysMLv2Lexer.interp +0 -0
  35. {sysmlpy-0.81.0 → sysmlpy-0.83.0}/src/sysmlpy/grammar/antlr4/SysMLv2Lexer.py +0 -0
  36. {sysmlpy-0.81.0 → sysmlpy-0.83.0}/src/sysmlpy/grammar/antlr4/SysMLv2Lexer.tokens +0 -0
  37. {sysmlpy-0.81.0 → sysmlpy-0.83.0}/src/sysmlpy/grammar/antlr4/SysMLv2Parser.g4 +0 -0
  38. {sysmlpy-0.81.0 → sysmlpy-0.83.0}/src/sysmlpy/grammar/antlr4/SysMLv2Parser.interp +0 -0
  39. {sysmlpy-0.81.0 → sysmlpy-0.83.0}/src/sysmlpy/grammar/antlr4/SysMLv2Parser.py +0 -0
  40. {sysmlpy-0.81.0 → sysmlpy-0.83.0}/src/sysmlpy/grammar/antlr4/SysMLv2Parser.tokens +0 -0
  41. {sysmlpy-0.81.0 → sysmlpy-0.83.0}/src/sysmlpy/grammar/antlr4/SysMLv2ParserListener.py +0 -0
  42. {sysmlpy-0.81.0 → sysmlpy-0.83.0}/src/sysmlpy/grammar/antlr4/SysMLv2ParserVisitor.py +0 -0
  43. {sysmlpy-0.81.0 → sysmlpy-0.83.0}/src/sysmlpy/grammar/antlr4/desc.xml +0 -0
  44. {sysmlpy-0.81.0 → sysmlpy-0.83.0}/src/sysmlpy/grammar/antlr4/examples/camera.sysml +0 -0
  45. {sysmlpy-0.81.0 → sysmlpy-0.83.0}/src/sysmlpy/grammar/antlr4/examples/toaster-system.sysml +0 -0
  46. {sysmlpy-0.81.0 → sysmlpy-0.83.0}/src/sysmlpy/grammar/antlr4/examples/vehicle-model.sysml +0 -0
  47. {sysmlpy-0.81.0 → sysmlpy-0.83.0}/src/sysmlpy/grammar/antlr4/pom.xml +0 -0
  48. {sysmlpy-0.81.0 → sysmlpy-0.83.0}/src/sysmlpy/grammar/classes.py +0 -0
  49. {sysmlpy-0.81.0 → sysmlpy-0.83.0}/src/sysmlpy/interchange.py +0 -0
  50. {sysmlpy-0.81.0 → sysmlpy-0.83.0}/src/sysmlpy/library/LICENSE +0 -0
  51. {sysmlpy-0.81.0 → sysmlpy-0.83.0}/src/sysmlpy/library/README.md +0 -0
  52. {sysmlpy-0.81.0 → sysmlpy-0.83.0}/src/sysmlpy/library/__init__.py +0 -0
  53. {sysmlpy-0.81.0 → sysmlpy-0.83.0}/src/sysmlpy/library/domain/Analysis/AnalysisTooling.sysml +0 -0
  54. {sysmlpy-0.81.0 → sysmlpy-0.83.0}/src/sysmlpy/library/domain/Analysis/SampledFunctions.sysml +0 -0
  55. {sysmlpy-0.81.0 → sysmlpy-0.83.0}/src/sysmlpy/library/domain/Analysis/StateSpaceRepresentation.sysml +0 -0
  56. {sysmlpy-0.81.0 → sysmlpy-0.83.0}/src/sysmlpy/library/domain/Analysis/TradeStudies.sysml +0 -0
  57. {sysmlpy-0.81.0 → sysmlpy-0.83.0}/src/sysmlpy/library/domain/Cause and Effect/CausationConnections.sysml +0 -0
  58. {sysmlpy-0.81.0 → sysmlpy-0.83.0}/src/sysmlpy/library/domain/Cause and Effect/CauseAndEffect.sysml +0 -0
  59. {sysmlpy-0.81.0 → sysmlpy-0.83.0}/src/sysmlpy/library/domain/Geometry/ShapeItems.sysml +0 -0
  60. {sysmlpy-0.81.0 → sysmlpy-0.83.0}/src/sysmlpy/library/domain/Geometry/SpatialItems.sysml +0 -0
  61. {sysmlpy-0.81.0 → sysmlpy-0.83.0}/src/sysmlpy/library/domain/Metadata/ImageMetadata.sysml +0 -0
  62. {sysmlpy-0.81.0 → sysmlpy-0.83.0}/src/sysmlpy/library/domain/Metadata/ModelingMetadata.sysml +0 -0
  63. {sysmlpy-0.81.0 → sysmlpy-0.83.0}/src/sysmlpy/library/domain/Metadata/ParametersOfInterestMetadata.sysml +0 -0
  64. {sysmlpy-0.81.0 → sysmlpy-0.83.0}/src/sysmlpy/library/domain/Metadata/RiskMetadata.sysml +0 -0
  65. {sysmlpy-0.81.0 → sysmlpy-0.83.0}/src/sysmlpy/library/domain/Quantities and Units/ISQ.sysml +0 -0
  66. {sysmlpy-0.81.0 → sysmlpy-0.83.0}/src/sysmlpy/library/domain/Quantities and Units/ISQAcoustics.sysml +0 -0
  67. {sysmlpy-0.81.0 → sysmlpy-0.83.0}/src/sysmlpy/library/domain/Quantities and Units/ISQAtomicNuclear.sysml +0 -0
  68. {sysmlpy-0.81.0 → sysmlpy-0.83.0}/src/sysmlpy/library/domain/Quantities and Units/ISQBase.sysml +0 -0
  69. {sysmlpy-0.81.0 → sysmlpy-0.83.0}/src/sysmlpy/library/domain/Quantities and Units/ISQCharacteristicNumbers.sysml +0 -0
  70. {sysmlpy-0.81.0 → sysmlpy-0.83.0}/src/sysmlpy/library/domain/Quantities and Units/ISQChemistryMolecular.sysml +0 -0
  71. {sysmlpy-0.81.0 → sysmlpy-0.83.0}/src/sysmlpy/library/domain/Quantities and Units/ISQCondensedMatter.sysml +0 -0
  72. {sysmlpy-0.81.0 → sysmlpy-0.83.0}/src/sysmlpy/library/domain/Quantities and Units/ISQElectromagnetism.sysml +0 -0
  73. {sysmlpy-0.81.0 → sysmlpy-0.83.0}/src/sysmlpy/library/domain/Quantities and Units/ISQInformation.sysml +0 -0
  74. {sysmlpy-0.81.0 → sysmlpy-0.83.0}/src/sysmlpy/library/domain/Quantities and Units/ISQLight.sysml +0 -0
  75. {sysmlpy-0.81.0 → sysmlpy-0.83.0}/src/sysmlpy/library/domain/Quantities and Units/ISQMechanics.sysml +0 -0
  76. {sysmlpy-0.81.0 → sysmlpy-0.83.0}/src/sysmlpy/library/domain/Quantities and Units/ISQSpaceTime.sysml +0 -0
  77. {sysmlpy-0.81.0 → sysmlpy-0.83.0}/src/sysmlpy/library/domain/Quantities and Units/ISQThermodynamics.sysml +0 -0
  78. {sysmlpy-0.81.0 → sysmlpy-0.83.0}/src/sysmlpy/library/domain/Quantities and Units/MeasurementRefCalculations.sysml +0 -0
  79. {sysmlpy-0.81.0 → sysmlpy-0.83.0}/src/sysmlpy/library/domain/Quantities and Units/MeasurementReferences.sysml +0 -0
  80. {sysmlpy-0.81.0 → sysmlpy-0.83.0}/src/sysmlpy/library/domain/Quantities and Units/Quantities.sysml +0 -0
  81. {sysmlpy-0.81.0 → sysmlpy-0.83.0}/src/sysmlpy/library/domain/Quantities and Units/QuantityCalculations.sysml +0 -0
  82. {sysmlpy-0.81.0 → sysmlpy-0.83.0}/src/sysmlpy/library/domain/Quantities and Units/SI.sysml +0 -0
  83. {sysmlpy-0.81.0 → sysmlpy-0.83.0}/src/sysmlpy/library/domain/Quantities and Units/SIPrefixes.sysml +0 -0
  84. {sysmlpy-0.81.0 → sysmlpy-0.83.0}/src/sysmlpy/library/domain/Quantities and Units/TensorCalculations.sysml +0 -0
  85. {sysmlpy-0.81.0 → sysmlpy-0.83.0}/src/sysmlpy/library/domain/Quantities and Units/Time.sysml +0 -0
  86. {sysmlpy-0.81.0 → sysmlpy-0.83.0}/src/sysmlpy/library/domain/Quantities and Units/USCustomaryUnits.sysml +0 -0
  87. {sysmlpy-0.81.0 → sysmlpy-0.83.0}/src/sysmlpy/library/domain/Quantities and Units/VectorCalculations.sysml +0 -0
  88. {sysmlpy-0.81.0 → sysmlpy-0.83.0}/src/sysmlpy/library/domain/Requirement Derivation/DerivationConnections.sysml +0 -0
  89. {sysmlpy-0.81.0 → sysmlpy-0.83.0}/src/sysmlpy/library/domain/Requirement Derivation/RequirementDerivation.sysml +0 -0
  90. {sysmlpy-0.81.0 → sysmlpy-0.83.0}/src/sysmlpy/library/kernel/Base.kerml +0 -0
  91. {sysmlpy-0.81.0 → sysmlpy-0.83.0}/src/sysmlpy/library/kernel/BaseFunctions.kerml +0 -0
  92. {sysmlpy-0.81.0 → sysmlpy-0.83.0}/src/sysmlpy/library/kernel/BooleanFunctions.kerml +0 -0
  93. {sysmlpy-0.81.0 → sysmlpy-0.83.0}/src/sysmlpy/library/kernel/Clocks.kerml +0 -0
  94. {sysmlpy-0.81.0 → sysmlpy-0.83.0}/src/sysmlpy/library/kernel/CollectionFunctions.kerml +0 -0
  95. {sysmlpy-0.81.0 → sysmlpy-0.83.0}/src/sysmlpy/library/kernel/Collections.kerml +0 -0
  96. {sysmlpy-0.81.0 → sysmlpy-0.83.0}/src/sysmlpy/library/kernel/ComplexFunctions.kerml +0 -0
  97. {sysmlpy-0.81.0 → sysmlpy-0.83.0}/src/sysmlpy/library/kernel/ControlFunctions.kerml +0 -0
  98. {sysmlpy-0.81.0 → sysmlpy-0.83.0}/src/sysmlpy/library/kernel/ControlPerformances.kerml +0 -0
  99. {sysmlpy-0.81.0 → sysmlpy-0.83.0}/src/sysmlpy/library/kernel/DataFunctions.kerml +0 -0
  100. {sysmlpy-0.81.0 → sysmlpy-0.83.0}/src/sysmlpy/library/kernel/FeatureReferencingPerformances.kerml +0 -0
  101. {sysmlpy-0.81.0 → sysmlpy-0.83.0}/src/sysmlpy/library/kernel/IntegerFunctions.kerml +0 -0
  102. {sysmlpy-0.81.0 → sysmlpy-0.83.0}/src/sysmlpy/library/kernel/KerML.kerml +0 -0
  103. {sysmlpy-0.81.0 → sysmlpy-0.83.0}/src/sysmlpy/library/kernel/Links.kerml +0 -0
  104. {sysmlpy-0.81.0 → sysmlpy-0.83.0}/src/sysmlpy/library/kernel/Metaobjects.kerml +0 -0
  105. {sysmlpy-0.81.0 → sysmlpy-0.83.0}/src/sysmlpy/library/kernel/NaturalFunctions.kerml +0 -0
  106. {sysmlpy-0.81.0 → sysmlpy-0.83.0}/src/sysmlpy/library/kernel/NumericalFunctions.kerml +0 -0
  107. {sysmlpy-0.81.0 → sysmlpy-0.83.0}/src/sysmlpy/library/kernel/Objects.kerml +0 -0
  108. {sysmlpy-0.81.0 → sysmlpy-0.83.0}/src/sysmlpy/library/kernel/Observation.kerml +0 -0
  109. {sysmlpy-0.81.0 → sysmlpy-0.83.0}/src/sysmlpy/library/kernel/OccurrenceFunctions.kerml +0 -0
  110. {sysmlpy-0.81.0 → sysmlpy-0.83.0}/src/sysmlpy/library/kernel/Occurrences.kerml +0 -0
  111. {sysmlpy-0.81.0 → sysmlpy-0.83.0}/src/sysmlpy/library/kernel/Performances.kerml +0 -0
  112. {sysmlpy-0.81.0 → sysmlpy-0.83.0}/src/sysmlpy/library/kernel/RationalFunctions.kerml +0 -0
  113. {sysmlpy-0.81.0 → sysmlpy-0.83.0}/src/sysmlpy/library/kernel/RealFunctions.kerml +0 -0
  114. {sysmlpy-0.81.0 → sysmlpy-0.83.0}/src/sysmlpy/library/kernel/ScalarFunctions.kerml +0 -0
  115. {sysmlpy-0.81.0 → sysmlpy-0.83.0}/src/sysmlpy/library/kernel/ScalarValues.kerml +0 -0
  116. {sysmlpy-0.81.0 → sysmlpy-0.83.0}/src/sysmlpy/library/kernel/SequenceFunctions.kerml +0 -0
  117. {sysmlpy-0.81.0 → sysmlpy-0.83.0}/src/sysmlpy/library/kernel/SpatialFrames.kerml +0 -0
  118. {sysmlpy-0.81.0 → sysmlpy-0.83.0}/src/sysmlpy/library/kernel/StatePerformances.kerml +0 -0
  119. {sysmlpy-0.81.0 → sysmlpy-0.83.0}/src/sysmlpy/library/kernel/StringFunctions.kerml +0 -0
  120. {sysmlpy-0.81.0 → sysmlpy-0.83.0}/src/sysmlpy/library/kernel/Transfers.kerml +0 -0
  121. {sysmlpy-0.81.0 → sysmlpy-0.83.0}/src/sysmlpy/library/kernel/TransitionPerformances.kerml +0 -0
  122. {sysmlpy-0.81.0 → sysmlpy-0.83.0}/src/sysmlpy/library/kernel/TrigFunctions.kerml +0 -0
  123. {sysmlpy-0.81.0 → sysmlpy-0.83.0}/src/sysmlpy/library/kernel/Triggers.kerml +0 -0
  124. {sysmlpy-0.81.0 → sysmlpy-0.83.0}/src/sysmlpy/library/kernel/VectorFunctions.kerml +0 -0
  125. {sysmlpy-0.81.0 → sysmlpy-0.83.0}/src/sysmlpy/library/kernel/VectorValues.kerml +0 -0
  126. {sysmlpy-0.81.0 → sysmlpy-0.83.0}/src/sysmlpy/library/systems/Actions.sysml +0 -0
  127. {sysmlpy-0.81.0 → sysmlpy-0.83.0}/src/sysmlpy/library/systems/Allocations.sysml +0 -0
  128. {sysmlpy-0.81.0 → sysmlpy-0.83.0}/src/sysmlpy/library/systems/AnalysisCases.sysml +0 -0
  129. {sysmlpy-0.81.0 → sysmlpy-0.83.0}/src/sysmlpy/library/systems/Attributes.sysml +0 -0
  130. {sysmlpy-0.81.0 → sysmlpy-0.83.0}/src/sysmlpy/library/systems/Calculations.sysml +0 -0
  131. {sysmlpy-0.81.0 → sysmlpy-0.83.0}/src/sysmlpy/library/systems/Cases.sysml +0 -0
  132. {sysmlpy-0.81.0 → sysmlpy-0.83.0}/src/sysmlpy/library/systems/Connections.sysml +0 -0
  133. {sysmlpy-0.81.0 → sysmlpy-0.83.0}/src/sysmlpy/library/systems/Constraints.sysml +0 -0
  134. {sysmlpy-0.81.0 → sysmlpy-0.83.0}/src/sysmlpy/library/systems/Flows.sysml +0 -0
  135. {sysmlpy-0.81.0 → sysmlpy-0.83.0}/src/sysmlpy/library/systems/Interfaces.sysml +0 -0
  136. {sysmlpy-0.81.0 → sysmlpy-0.83.0}/src/sysmlpy/library/systems/Items.sysml +0 -0
  137. {sysmlpy-0.81.0 → sysmlpy-0.83.0}/src/sysmlpy/library/systems/Metadata.sysml +0 -0
  138. {sysmlpy-0.81.0 → sysmlpy-0.83.0}/src/sysmlpy/library/systems/Parts.sysml +0 -0
  139. {sysmlpy-0.81.0 → sysmlpy-0.83.0}/src/sysmlpy/library/systems/Ports.sysml +0 -0
  140. {sysmlpy-0.81.0 → sysmlpy-0.83.0}/src/sysmlpy/library/systems/Requirements.sysml +0 -0
  141. {sysmlpy-0.81.0 → sysmlpy-0.83.0}/src/sysmlpy/library/systems/StandardViewDefinitions.sysml +0 -0
  142. {sysmlpy-0.81.0 → sysmlpy-0.83.0}/src/sysmlpy/library/systems/States.sysml +0 -0
  143. {sysmlpy-0.81.0 → sysmlpy-0.83.0}/src/sysmlpy/library/systems/SysML.sysml +0 -0
  144. {sysmlpy-0.81.0 → sysmlpy-0.83.0}/src/sysmlpy/library/systems/UseCases.sysml +0 -0
  145. {sysmlpy-0.81.0 → sysmlpy-0.83.0}/src/sysmlpy/library/systems/VerificationCases.sysml +0 -0
  146. {sysmlpy-0.81.0 → sysmlpy-0.83.0}/src/sysmlpy/library/systems/Views.sysml +0 -0
  147. {sysmlpy-0.81.0 → sysmlpy-0.83.0}/src/sysmlpy/lsp/__init__.py +0 -0
  148. {sysmlpy-0.81.0 → sysmlpy-0.83.0}/src/sysmlpy/lsp/__main__.py +0 -0
  149. {sysmlpy-0.81.0 → sysmlpy-0.83.0}/src/sysmlpy/lsp/protocol.py +0 -0
  150. {sysmlpy-0.81.0 → sysmlpy-0.83.0}/src/sysmlpy/lsp/stdio.py +0 -0
  151. {sysmlpy-0.81.0 → sysmlpy-0.83.0}/src/sysmlpy/navigate.py +0 -0
  152. {sysmlpy-0.81.0 → sysmlpy-0.83.0}/src/sysmlpy/plantuml.py +0 -0
  153. {sysmlpy-0.81.0 → sysmlpy-0.83.0}/src/sysmlpy/project.py +0 -0
  154. {sysmlpy-0.81.0 → sysmlpy-0.83.0}/src/sysmlpy/semantic.py +0 -0
  155. {sysmlpy-0.81.0 → sysmlpy-0.83.0}/src/sysmlpy/sim.py +0 -0
  156. {sysmlpy-0.81.0 → sysmlpy-0.83.0}/src/sysmlpy/spreadsheet.py +0 -0
  157. {sysmlpy-0.81.0 → sysmlpy-0.83.0}/src/sysmlpy/store.py +0 -0
  158. {sysmlpy-0.81.0 → sysmlpy-0.83.0}/src/sysmlpy/traceability.py +0 -0
  159. {sysmlpy-0.81.0 → sysmlpy-0.83.0}/src/sysmlpy/us_customary_units.txt +0 -0
  160. {sysmlpy-0.81.0 → sysmlpy-0.83.0}/src/sysmlpy/validator.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: sysmlpy
3
- Version: 0.81.0
3
+ Version: 0.83.0
4
4
  Summary:
5
5
  License-File: LICENSE
6
6
  Author: Jon R. Fox (mycr0ft)
@@ -6,7 +6,7 @@ build-backend = "poetry.core.masonry.api"
6
6
 
7
7
  [project]
8
8
  name = "sysmlpy"
9
- version = "0.81.0"
9
+ version = "0.83.0"
10
10
  description = ""
11
11
  authors = [{ name = "Jon R. Fox (mycr0ft)", email = "jon.fox@drfox.com" }]
12
12
  readme = "README.md"
@@ -34,7 +34,7 @@ __all__ = [
34
34
  "render_state_transition_view_svg", "boxes_view",
35
35
  ]
36
36
  __author__ = "Jon Fox"
37
- __version__ = "0.81.0"
37
+ __version__ = "0.83.0"
38
38
 
39
39
  from sysmlpy.usage import (
40
40
  Item, Attribute, Part, Port, Action, Reference, UseCase, Requirement, Interface, Message,
@@ -449,7 +449,7 @@ def __getattr__(name):
449
449
  globals()[name] = fn
450
450
  return fn
451
451
  if name in ("ModelDiff", "ElementChange", "FieldChange",
452
- "diff_models", "diff_files"):
452
+ "diff_models", "diff_files", "diff_state_machines"):
453
453
  from sysmlpy import diff as _diff
454
454
  fn = getattr(_diff, name)
455
455
  globals()[name] = fn
@@ -117,15 +117,25 @@ def cmd_diff(args) -> int:
117
117
  print(f"Parse error: {e}", file=sys.stderr)
118
118
  return 2
119
119
 
120
+ threshold = getattr(args, "threshold", None)
121
+ if threshold is not None and not 0 <= threshold <= 1:
122
+ print(f"Error: --threshold expects a fraction in 0..1, "
123
+ f"got {threshold}", file=sys.stderr)
124
+ return 2
125
+
120
126
  if args.format == "json":
121
127
  import json as _json
122
128
  print(_json.dumps({
123
129
  "summary": d.summary(),
130
+ "elements_old": d.elements_old,
131
+ "elements_new": d.elements_new,
132
+ "change_rate": round(d.change_rate, 6),
124
133
  "changes": [
125
134
  {
126
135
  "change": c.change,
127
136
  "kind": c.kind,
128
137
  "qualified_name": c.qualified_name,
138
+ "old_qualified_name": c.old_qualified_name,
129
139
  "fields": [
130
140
  {"field": fc.field, "old": fc.old,
131
141
  "new": fc.new} for fc in c.fields
@@ -139,6 +149,14 @@ def cmd_diff(args) -> int:
139
149
  else:
140
150
  print(d.as_text(), end="")
141
151
 
152
+ if threshold is not None:
153
+ rate = d.change_rate
154
+ failed = rate > threshold
155
+ print(f"change gate: {len(d.changes)} changes / "
156
+ f"{d.elements_old} elements = {rate:.2%}; threshold "
157
+ f"{threshold:.2%} -> {'FAIL' if failed else 'OK'}")
158
+ return 1 if failed else 0
159
+
142
160
  # CI-friendly: differences are findings
143
161
  return 1 if not d.is_empty() else 0
144
162
 
@@ -762,6 +780,13 @@ def build_parser() -> argparse.ArgumentParser:
762
780
  "--format", choices=("text", "markdown", "json"), default="text",
763
781
  help="Output format: text (default), markdown or JSON",
764
782
  )
783
+ p_diff.add_argument(
784
+ "--threshold", type=float, default=None, metavar="FRAC",
785
+ help="CI gate: exit 1 only when the change rate (changes / old "
786
+ "elements) exceeds FRAC (0..1); exit 0 under the gate "
787
+ "even when the models differ. Without this flag any "
788
+ "difference exits 1.",
789
+ )
765
790
  p_diff.set_defaults(func=cmd_diff)
766
791
 
767
792
  # -- view ----------------------------------------------------------------
@@ -0,0 +1,541 @@
1
+ """Semantic model diff for review workflows (Goal 8 + Goal 11 Batch 3).
2
+
3
+ Compares two loaded models element-by-element and reports what
4
+ changed, so review workflows (PR gates, design comparisons) can see
5
+ *semantic* changes rather than whitespace/formatting noise.
6
+
7
+ Element identity is ``(kind, qualified name)`` — ``Fleet::Vehicle``
8
+ as a ``PartDef`` is a different identity from a ``PartUsage`` with
9
+ the same name. Every element carries a *signature* — the semantic
10
+ attributes the library tracks reliably:
11
+
12
+ - ``typing`` — the declared type name (``typed_by_name``, qualified)
13
+ - ``subject`` — requirement subjects (``(name, type)`` tuples)
14
+ - ``doc`` — documentation text (tree attribute; populated for
15
+ requirement-style kinds)
16
+ - ``value`` — the default-value expression (``= 70``), from the
17
+ element's dump text
18
+ - ``multiplicity`` — ``[2]``, ``[1..3] ordered nonunique`` etc.
19
+ - ``direction`` — ``in`` / ``out`` / ``inout`` feature direction
20
+ - ``abstract`` — ``abstract`` flag on definitions
21
+ - ``traces`` — requirement trace edges (``satisfy:x``, ``verify:y``)
22
+ via :func:`sysmlpy.traceability.extract_traceability`
23
+
24
+ Value / multiplicity / direction / abstract are *heuristic* reads of
25
+ the element's canonical ``dump()`` text — the dump is the library's
26
+ own canonical serialization, so both sides of the diff extract the
27
+ same way. The value regex does not span strings containing ``;``.
28
+ Documentation is *not* extracted from dumps — ``doc /* ... */``
29
+ members do not survive the grammar round-trip on usage/definition
30
+ kinds — so the ``doc`` field reflects the tree attribute (populated
31
+ for requirements).
32
+
33
+ Change kinds:
34
+
35
+ - ``added`` — element exists only in the new model
36
+ - ``removed`` — element exists only in the old model
37
+ - ``renamed`` — a removed/added pair matched by *rename detection*:
38
+ same kind and same structural signature (everything except
39
+ ``doc``, which often changes alongside a rename) with exactly one
40
+ candidate. Ambiguous matches (several candidates) stay
41
+ removed + added. ``old_name``/``old_qualified_name`` carry the
42
+ previous identity; ``fields`` lists any signature drift.
43
+ - ``changed`` — same identity, different signature; ``fields`` lists
44
+ the field-level old/new values
45
+
46
+ State machines: :func:`diff_state_machines` diffs two models' state
47
+ machines through the simulator's :class:`~sysmlpy.sim.MachineDescriptor`
48
+ — states, transitions (source/target/trigger/guard/effect/history)
49
+ and the initial state.
50
+
51
+ Excluded by design: ``Model`` objects carry a random UUID name per
52
+ parse, so they are never compared.
53
+
54
+ Usage::
55
+
56
+ from sysmlpy.diff import diff_models, diff_files, diff_state_machines
57
+
58
+ d = diff_models(old_model, new_model)
59
+ print(d.as_text())
60
+ d2 = diff_files("old.sysml", "new.sysml")
61
+ md = diff_state_machines(old_model, new_model, focus="Cruise")
62
+
63
+ CLI: ``sysmlpy diff OLD NEW [--format text|markdown|json]
64
+ [--threshold 0.1]`` — with ``--threshold``, exit 1 only when the
65
+ change rate (changes / old elements) exceeds the gate.
66
+ """
67
+
68
+ from __future__ import annotations
69
+
70
+ import re
71
+ from dataclasses import dataclass, field
72
+ from pathlib import Path
73
+ from typing import Any
74
+
75
+ __all__ = [
76
+ "FieldChange",
77
+ "ElementChange",
78
+ "ModelDiff",
79
+ "diff_models",
80
+ "diff_files",
81
+ "diff_state_machines",
82
+ ]
83
+
84
+
85
+ # ---------------------------------------------------------------------------
86
+ # data types
87
+ # ---------------------------------------------------------------------------
88
+
89
+
90
+ @dataclass
91
+ class FieldChange:
92
+ """One changed field within an element."""
93
+
94
+ field: str
95
+ old: str | None
96
+ new: str | None
97
+
98
+
99
+ @dataclass
100
+ class ElementChange:
101
+ """One element-level semantic change."""
102
+
103
+ change: str # "added" | "removed" | "renamed" | "changed"
104
+ kind: str # e.g. "PartDef", "PartUsage", "Package", "Transition"
105
+ name: str
106
+ qualified_name: str
107
+ fields: list = field(default_factory=list) # list[FieldChange]
108
+ #: previous name/qname — populated for ``renamed`` entries
109
+ old_name: str | None = None
110
+ old_qualified_name: str | None = None
111
+
112
+
113
+ @dataclass
114
+ class ModelDiff:
115
+ """The semantic diff between two models."""
116
+
117
+ changes: list = field(default_factory=list) # list[ElementChange]
118
+ #: number of comparable elements in the old/new models (for the
119
+ #: ``--threshold`` change-rate gate)
120
+ elements_old: int = 0
121
+ elements_new: int = 0
122
+
123
+ # ------------------------------------------------------------------
124
+ # queries
125
+ # ------------------------------------------------------------------
126
+
127
+ @property
128
+ def added(self) -> list:
129
+ return [c for c in self.changes if c.change == "added"]
130
+
131
+ @property
132
+ def removed(self) -> list:
133
+ return [c for c in self.changes if c.change == "removed"]
134
+
135
+ @property
136
+ def renamed(self) -> list:
137
+ return [c for c in self.changes if c.change == "renamed"]
138
+
139
+ @property
140
+ def changed(self) -> list:
141
+ return [c for c in self.changes if c.change == "changed"]
142
+
143
+ def is_empty(self) -> bool:
144
+ """True when the models are semantically identical."""
145
+ return not self.changes
146
+
147
+ @property
148
+ def change_rate(self) -> float:
149
+ """Fraction of the old model affected by this diff.
150
+
151
+ ``changes / elements_old`` (0.0 when the old model is empty).
152
+ Used by the ``--threshold`` CI gate.
153
+ """
154
+ if not self.elements_old:
155
+ return 0.0 if not self.changes else 1.0
156
+ return len(self.changes) / self.elements_old
157
+
158
+ def summary(self) -> str:
159
+ """One-line human summary, e.g. ``2 added, 1 removed, 3 changed``."""
160
+ if self.is_empty():
161
+ return "models are semantically identical"
162
+ counts = [f"{len(self.added)} added",
163
+ f"{len(self.removed)} removed",
164
+ f"{len(self.renamed)} renamed",
165
+ f"{len(self.changed)} changed"]
166
+ return ", ".join(counts)
167
+
168
+ # ------------------------------------------------------------------
169
+ # rendering
170
+ # ------------------------------------------------------------------
171
+
172
+ def as_text(self) -> str:
173
+ """Plain-text rendering (monochrome — review-log friendly)."""
174
+ if self.is_empty():
175
+ return "models are semantically identical\n"
176
+ marker = {"added": "+", "removed": "-", "renamed": ">",
177
+ "changed": "~"}
178
+ out = [f"Model diff: {self.summary()}", ""]
179
+ for c in self.changes:
180
+ if c.change == "renamed":
181
+ out.append(f"> {c.kind} {c.old_qualified_name}"
182
+ f" -> {c.qualified_name}")
183
+ else:
184
+ out.append(f"{marker[c.change]} {c.kind} "
185
+ f"{c.qualified_name}")
186
+ for fc in c.fields:
187
+ old_v = fc.old if fc.old is not None else "(none)"
188
+ new_v = fc.new if fc.new is not None else "(none)"
189
+ out.append(f" {fc.field}: {old_v} -> {new_v}")
190
+ return "\n".join(out) + "\n"
191
+
192
+ def as_markdown(self) -> str:
193
+ """Markdown rendering for review workflows (PR comments etc.)."""
194
+ if self.is_empty():
195
+ return "Models are semantically identical.\n"
196
+ out = [f"## Model diff", "",
197
+ f"**{self.summary()}**", ""]
198
+ sections = [
199
+ ("added", "### Added"),
200
+ ("removed", "### Removed"),
201
+ ("renamed", "### Renamed"),
202
+ ("changed", "### Changed"),
203
+ ]
204
+ for kind, header in sections:
205
+ items = [c for c in self.changes if c.change == kind]
206
+ if not items:
207
+ continue
208
+ out.append(header)
209
+ out.append("")
210
+ for c in items:
211
+ if kind == "renamed":
212
+ out.append(f"- `{c.old_qualified_name}` → "
213
+ f"`{c.qualified_name}` ({c.kind})")
214
+ else:
215
+ out.append(f"- `{c.qualified_name}` ({c.kind})")
216
+ for fc in c.fields:
217
+ old_v = fc.old if fc.old is not None else "—"
218
+ new_v = fc.new if fc.new is not None else "—"
219
+ out.append(
220
+ f" - `{fc.field}`: `{old_v}` → `{new_v}`")
221
+ out.append("")
222
+ return "\n".join(out).rstrip() + "\n"
223
+
224
+
225
+ # ---------------------------------------------------------------------------
226
+ # signature extraction
227
+ # ---------------------------------------------------------------------------
228
+
229
+ _FIELDS = ("typing", "subject", "doc", "value", "multiplicity",
230
+ "direction", "abstract", "traces")
231
+ #: fields used to match renames (``doc`` often changes with the name)
232
+ _STRUCTURAL = tuple(f for f in _FIELDS if f != "doc")
233
+
234
+ _DIR_RE = re.compile(r"^(?:in|out|inout)\b")
235
+ _MULT_RE = re.compile(r"\[([^\]]+)\]")
236
+ _VALUE_RE = re.compile(r"(?<![<>=!])=\s*([^;]+);\s*$")
237
+
238
+
239
+ def _kind(obj) -> str:
240
+ """Element kind with a definition/usage suffix.
241
+
242
+ ``PartDef`` / ``PartUsage`` — the same qualified name under both
243
+ roles is a different identity, so e.g. turning ``part def X`` into
244
+ an inline ``part x : X`` reports as removed + added, not changed.
245
+ """
246
+ base = type(obj).__name__
247
+ return base + ("Def" if getattr(obj, "is_definition", False)
248
+ else "Usage")
249
+
250
+
251
+ def _dump_fields(obj) -> dict:
252
+ """Heuristic signature fields read from the element's canonical dump.
253
+
254
+ The dump is the library's own canonical serialization, so the same
255
+ element dumps identically on both sides of a diff. Only the first
256
+ line is inspected for direction / multiplicity / value (usages are
257
+ single-line); the first ``doc /* ... */`` comment anywhere is the
258
+ element's own documentation in the common declaration order.
259
+ """
260
+ try:
261
+ text = obj.dump()
262
+ except Exception:
263
+ return {}
264
+ if not text or not text.strip():
265
+ return {}
266
+ first = text.strip().splitlines()[0].strip()
267
+ out = {}
268
+
269
+
270
+ m = _DIR_RE.match(first)
271
+ out["direction"] = m.group(0) if m else None
272
+
273
+ m = _MULT_RE.search(first)
274
+ if m:
275
+ flags = [f for f in ("ordered", "nonunique")
276
+ if re.search(rf"\b{f}\b", first)]
277
+ out["multiplicity"] = f"[{m.group(1)}]" + (
278
+ f" {' '.join(flags)}" if flags else "")
279
+ else:
280
+ out["multiplicity"] = None
281
+
282
+ m = _VALUE_RE.search(first)
283
+ out["value"] = m.group(1).strip() if m else None
284
+
285
+ out["abstract"] = "abstract" if re.match(r"^abstract\b", first) else None
286
+
287
+ return out
288
+
289
+
290
+ def _signature(obj, traces: dict | None = None) -> dict:
291
+ """The comparable semantic attributes of a tree object."""
292
+ sig = {}
293
+
294
+ typing = getattr(obj, "typed_by_name", None)
295
+ sig["typing"] = str(typing) if typing else None
296
+
297
+ subject = getattr(obj, "subject", None)
298
+ if subject:
299
+ name, type_name = subject
300
+ sig["subject"] = f"{name} : {type_name}" if type_name else str(name)
301
+ else:
302
+ sig["subject"] = None
303
+
304
+ for f, v in _dump_fields(obj).items():
305
+ sig[f] = v
306
+
307
+ doc = getattr(obj, "doc", None)
308
+ if doc:
309
+ sig["doc"] = str(doc)
310
+
311
+ sig["traces"] = None
312
+ if traces:
313
+ from sysmlpy.traceability import _qualified_name
314
+ trace = traces.get(_qualified_name(obj))
315
+ if trace is not None:
316
+ edges = ([f"satisfy:{x}" for x in trace.satisfied_by] +
317
+ [f"verify:{x}" for x in trace.verified_by])
318
+ if edges:
319
+ sig["traces"] = ", ".join(sorted(edges))
320
+
321
+ return sig
322
+
323
+
324
+ def _collect(model, traces: dict | None = None) -> dict:
325
+ """Map ``(kind, qualified name)`` -> ``(name, signature)``."""
326
+ from sysmlpy.traceability import _walk, _qualified_name
327
+
328
+ out = {}
329
+ for obj in _walk(model):
330
+ if type(obj).__name__ == "Model":
331
+ continue # random UUID name — not comparable
332
+ qname = _qualified_name(obj)
333
+ if not qname:
334
+ continue
335
+ key = (_kind(obj), qname)
336
+ out[key] = (getattr(obj, "name", ""), _signature(obj, traces))
337
+ return out
338
+
339
+
340
+ # ---------------------------------------------------------------------------
341
+ # public API
342
+ # ---------------------------------------------------------------------------
343
+
344
+
345
+ def diff_models(old_model: Any, new_model: Any) -> ModelDiff:
346
+ """Diff two loaded models semantically.
347
+
348
+ Parameters
349
+ ----------
350
+ old_model, new_model : Model
351
+ Two models loaded via :func:`sysmlpy.loads` /
352
+ :func:`sysmlpy.load_files` (or any Model tree).
353
+ """
354
+ from sysmlpy.traceability import extract_traceability
355
+
356
+ old_traces = new_traces = None
357
+ try:
358
+ old_traces = {t.qualified_name: t
359
+ for t in extract_traceability(old_model).requirements}
360
+ new_traces = {t.qualified_name: t
361
+ for t in extract_traceability(new_model).requirements}
362
+ except Exception:
363
+ old_traces = new_traces = None # traces are best-effort
364
+
365
+ old = _collect(old_model, old_traces)
366
+ new = _collect(new_model, new_traces)
367
+
368
+ # Rename detection: a removed element matches an added one when
369
+ # kind AND structural signature are equal and the candidate is
370
+ # unique — otherwise the pair stays removed + added.
371
+ removed_keys = sorted(k for k in old if k not in new)
372
+ added_keys = sorted(k for k in new if k not in old)
373
+ matches: dict = {}
374
+ used: set = set()
375
+ for rk in removed_keys:
376
+ sig_old = tuple(old[rk][1].get(f) for f in _STRUCTURAL)
377
+ candidates = [
378
+ ak for ak in added_keys
379
+ if ak not in used and ak[0] == rk[0]
380
+ and tuple(new[ak][1].get(f) for f in _STRUCTURAL) == sig_old
381
+ ]
382
+ if len(candidates) == 1:
383
+ matches[rk] = candidates[0]
384
+ used.add(candidates[0])
385
+
386
+ matched_removed = set(matches)
387
+ matched_added = set(matches.values())
388
+
389
+ changes = []
390
+ for key in sorted((set(old) | set(new))
391
+ - matched_removed - matched_added):
392
+ in_old = key in old
393
+ in_new = key in new
394
+ kind, qname = key
395
+ if in_old and not in_new:
396
+ changes.append(ElementChange(
397
+ change="removed", kind=kind,
398
+ name=old[key][0], qualified_name=qname))
399
+ elif in_new and not in_old:
400
+ changes.append(ElementChange(
401
+ change="added", kind=kind,
402
+ name=new[key][0], qualified_name=qname))
403
+ else:
404
+ old_sig = old[key][1]
405
+ new_sig = new[key][1]
406
+ field_changes = [
407
+ FieldChange(field=f, old=old_sig.get(f),
408
+ new=new_sig.get(f))
409
+ for f in _FIELDS
410
+ if old_sig.get(f) != new_sig.get(f)
411
+ ]
412
+ if field_changes:
413
+ changes.append(ElementChange(
414
+ change="changed", kind=kind,
415
+ name=new[key][0], qualified_name=qname,
416
+ fields=field_changes))
417
+ for rk in sorted(matched_removed):
418
+ ak = matches[rk]
419
+ kind = rk[0]
420
+ old_sig = old[rk][1]
421
+ new_sig = new[ak][1]
422
+ field_changes = [
423
+ FieldChange(field=f, old=old_sig.get(f), new=new_sig.get(f))
424
+ for f in _FIELDS if old_sig.get(f) != new_sig.get(f)
425
+ ]
426
+ changes.append(ElementChange(
427
+ change="renamed", kind=kind,
428
+ name=new[ak][0], qualified_name=ak[1],
429
+ old_name=old[rk][0], old_qualified_name=rk[1],
430
+ fields=field_changes))
431
+
432
+ return ModelDiff(changes=changes, elements_old=len(old),
433
+ elements_new=len(new))
434
+
435
+
436
+ def diff_files(old_path, new_path) -> ModelDiff:
437
+ """Diff two SysML source files.
438
+
439
+ Parameters
440
+ ----------
441
+ old_path, new_path : str | Path
442
+ Paths to ``.sysml`` files.
443
+ """
444
+ import sysmlpy
445
+
446
+ old_model = sysmlpy.loads(Path(old_path).read_text(encoding="utf-8"))
447
+ new_model = sysmlpy.loads(Path(new_path).read_text(encoding="utf-8"))
448
+ return diff_models(old_model, new_model)
449
+
450
+
451
+ # ---------------------------------------------------------------------------
452
+ # state-machine diff (Goal 11 Batch 3)
453
+ # ---------------------------------------------------------------------------
454
+
455
+ _TRANSITION_FIELDS = ("source", "target", "trigger", "guard", "effect",
456
+ "history_region")
457
+
458
+
459
+ def _transition_keys(descriptor) -> dict:
460
+ """Identity for each transition (declaration order).
461
+
462
+ Named transitions use their name; anonymous ones use
463
+ ``transition@<source>-><target>``. Duplicates get a ``#2``-style
464
+ suffix so every identity is unique.
465
+ """
466
+ keys = {}
467
+ seen: dict = {}
468
+ for i, t in enumerate(descriptor.transitions):
469
+ base = t.name or f"transition@{t.source}->{t.target}"
470
+ n = seen.get(base, 0)
471
+ seen[base] = n + 1
472
+ keys[i] = base if n == 0 else f"{base}#{n + 1}"
473
+ return keys
474
+
475
+
476
+ def diff_state_machines(old_model: Any, new_model: Any,
477
+ focus: str | None = None) -> ModelDiff:
478
+ """Diff the state machines of two models.
479
+
480
+ Compares the machines through the simulator's extraction
481
+ (:func:`sysmlpy.sim.build_state_machine`): states, transitions
482
+ (source/target/trigger/guard/effect/history) and the initial
483
+ state. Transitions are identified by declaration name (anonymous
484
+ ones by their endpoints).
485
+
486
+ Raises :class:`sysmlpy.sim.SimulationError` when either model has
487
+ no (matching) state machine or declares parallel regions.
488
+ """
489
+ from sysmlpy.sim import build_state_machine
490
+
491
+ old_md = build_state_machine(old_model, focus=focus)
492
+ new_md = build_state_machine(new_model, focus=focus)
493
+
494
+ changes = []
495
+ machine_name = new_md.name or old_md.name or ""
496
+ if old_md.initial != new_md.initial:
497
+ changes.append(ElementChange(
498
+ change="changed", kind="StateMachine", name=machine_name,
499
+ qualified_name=machine_name,
500
+ fields=[FieldChange(field="initial", old=old_md.initial,
501
+ new=new_md.initial)]))
502
+
503
+ old_states = set(old_md.states)
504
+ new_states = set(new_md.states)
505
+ for s in sorted(old_states - new_states):
506
+ changes.append(ElementChange(change="removed", kind="State",
507
+ name=s, qualified_name=s))
508
+ for s in sorted(new_states - old_states):
509
+ changes.append(ElementChange(change="added", kind="State",
510
+ name=s, qualified_name=s))
511
+
512
+ old_keys = _transition_keys(old_md)
513
+ new_keys = _transition_keys(new_md)
514
+ old_t = {old_keys[i]: t for i, t in enumerate(old_md.transitions)}
515
+ new_t = {new_keys[i]: t for i, t in enumerate(new_md.transitions)}
516
+
517
+ for key in sorted(set(old_t) | set(new_t)):
518
+ in_old, in_new = key in old_t, key in new_t
519
+ if in_old and not in_new:
520
+ changes.append(ElementChange(
521
+ change="removed", kind="Transition", name=key,
522
+ qualified_name=key))
523
+ elif in_new and not in_old:
524
+ changes.append(ElementChange(
525
+ change="added", kind="Transition", name=key,
526
+ qualified_name=key))
527
+ else:
528
+ o, n = old_t[key], new_t[key]
529
+ field_changes = [
530
+ FieldChange(field=f, old=getattr(o, f),
531
+ new=getattr(n, f))
532
+ for f in _TRANSITION_FIELDS
533
+ if getattr(o, f) != getattr(n, f)
534
+ ]
535
+ if field_changes:
536
+ changes.append(ElementChange(
537
+ change="changed", kind="Transition", name=key,
538
+ qualified_name=key, fields=field_changes))
539
+
540
+ return ModelDiff(changes=changes, elements_old=len(old_md.states),
541
+ elements_new=len(new_md.states))