sysmlpy 0.96.2__tar.gz → 0.96.3__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 (177) hide show
  1. {sysmlpy-0.96.2 → sysmlpy-0.96.3}/PKG-INFO +65 -1
  2. {sysmlpy-0.96.2 → sysmlpy-0.96.3}/README.md +64 -0
  3. {sysmlpy-0.96.2 → sysmlpy-0.96.3}/pyproject.toml +1 -1
  4. {sysmlpy-0.96.2 → sysmlpy-0.96.3}/src/sysmlpy/__init__.py +3 -1
  5. {sysmlpy-0.96.2 → sysmlpy-0.96.3}/src/sysmlpy/__main__.py +29 -1
  6. {sysmlpy-0.96.2 → sysmlpy-0.96.3}/src/sysmlpy/interchange.py +291 -9
  7. {sysmlpy-0.96.2 → sysmlpy-0.96.3}/LICENSE +0 -0
  8. {sysmlpy-0.96.2 → sysmlpy-0.96.3}/src/sysmlpy/antlr/README.md +0 -0
  9. {sysmlpy-0.96.2 → sysmlpy-0.96.3}/src/sysmlpy/antlr/SysMLv2Lexer.interp +0 -0
  10. {sysmlpy-0.96.2 → sysmlpy-0.96.3}/src/sysmlpy/antlr/SysMLv2Lexer.py +0 -0
  11. {sysmlpy-0.96.2 → sysmlpy-0.96.3}/src/sysmlpy/antlr/SysMLv2Lexer.tokens +0 -0
  12. {sysmlpy-0.96.2 → sysmlpy-0.96.3}/src/sysmlpy/antlr/SysMLv2Parser.interp +0 -0
  13. {sysmlpy-0.96.2 → sysmlpy-0.96.3}/src/sysmlpy/antlr/SysMLv2Parser.py +0 -0
  14. {sysmlpy-0.96.2 → sysmlpy-0.96.3}/src/sysmlpy/antlr/SysMLv2Parser.tokens +0 -0
  15. {sysmlpy-0.96.2 → sysmlpy-0.96.3}/src/sysmlpy/antlr/SysMLv2ParserListener.py +0 -0
  16. {sysmlpy-0.96.2 → sysmlpy-0.96.3}/src/sysmlpy/antlr/SysMLv2ParserVisitor.py +0 -0
  17. {sysmlpy-0.96.2 → sysmlpy-0.96.3}/src/sysmlpy/antlr/src/sysmlpy/grammar/antlr4/SysMLv2Lexer.interp +0 -0
  18. {sysmlpy-0.96.2 → sysmlpy-0.96.3}/src/sysmlpy/antlr/src/sysmlpy/grammar/antlr4/SysMLv2Lexer.py +0 -0
  19. {sysmlpy-0.96.2 → sysmlpy-0.96.3}/src/sysmlpy/antlr/src/sysmlpy/grammar/antlr4/SysMLv2Lexer.tokens +0 -0
  20. {sysmlpy-0.96.2 → sysmlpy-0.96.3}/src/sysmlpy/antlr_parser.py +0 -0
  21. {sysmlpy-0.96.2 → sysmlpy-0.96.3}/src/sysmlpy/antlr_visitor.py +0 -0
  22. {sysmlpy-0.96.2 → sysmlpy-0.96.3}/src/sysmlpy/boxes_view.py +0 -0
  23. {sysmlpy-0.96.2 → sysmlpy-0.96.3}/src/sysmlpy/definition.py +0 -0
  24. {sysmlpy-0.96.2 → sysmlpy-0.96.3}/src/sysmlpy/dfa_cache.py +0 -0
  25. {sysmlpy-0.96.2 → sysmlpy-0.96.3}/src/sysmlpy/diff.py +0 -0
  26. {sysmlpy-0.96.2 → sysmlpy-0.96.3}/src/sysmlpy/evaluator.py +0 -0
  27. {sysmlpy-0.96.2 → sysmlpy-0.96.3}/src/sysmlpy/examples/attribute_values.py +0 -0
  28. {sysmlpy-0.96.2 → sysmlpy-0.96.3}/src/sysmlpy/examples/part_attributes.py +0 -0
  29. {sysmlpy-0.96.2 → sysmlpy-0.96.3}/src/sysmlpy/examples/tuples_sequences.py +0 -0
  30. {sysmlpy-0.96.2 → sysmlpy-0.96.3}/src/sysmlpy/formatting.py +0 -0
  31. {sysmlpy-0.96.2 → sysmlpy-0.96.3}/src/sysmlpy/grammar/antlr4/README.md +0 -0
  32. {sysmlpy-0.96.2 → sysmlpy-0.96.3}/src/sysmlpy/grammar/antlr4/SysMLv2Lexer.g4 +0 -0
  33. {sysmlpy-0.96.2 → sysmlpy-0.96.3}/src/sysmlpy/grammar/antlr4/SysMLv2Lexer.interp +0 -0
  34. {sysmlpy-0.96.2 → sysmlpy-0.96.3}/src/sysmlpy/grammar/antlr4/SysMLv2Lexer.py +0 -0
  35. {sysmlpy-0.96.2 → sysmlpy-0.96.3}/src/sysmlpy/grammar/antlr4/SysMLv2Lexer.tokens +0 -0
  36. {sysmlpy-0.96.2 → sysmlpy-0.96.3}/src/sysmlpy/grammar/antlr4/SysMLv2Parser.g4 +0 -0
  37. {sysmlpy-0.96.2 → sysmlpy-0.96.3}/src/sysmlpy/grammar/antlr4/SysMLv2Parser.interp +0 -0
  38. {sysmlpy-0.96.2 → sysmlpy-0.96.3}/src/sysmlpy/grammar/antlr4/SysMLv2Parser.py +0 -0
  39. {sysmlpy-0.96.2 → sysmlpy-0.96.3}/src/sysmlpy/grammar/antlr4/SysMLv2Parser.tokens +0 -0
  40. {sysmlpy-0.96.2 → sysmlpy-0.96.3}/src/sysmlpy/grammar/antlr4/SysMLv2ParserListener.py +0 -0
  41. {sysmlpy-0.96.2 → sysmlpy-0.96.3}/src/sysmlpy/grammar/antlr4/SysMLv2ParserVisitor.py +0 -0
  42. {sysmlpy-0.96.2 → sysmlpy-0.96.3}/src/sysmlpy/grammar/antlr4/desc.xml +0 -0
  43. {sysmlpy-0.96.2 → sysmlpy-0.96.3}/src/sysmlpy/grammar/antlr4/examples/camera.sysml +0 -0
  44. {sysmlpy-0.96.2 → sysmlpy-0.96.3}/src/sysmlpy/grammar/antlr4/examples/toaster-system.sysml +0 -0
  45. {sysmlpy-0.96.2 → sysmlpy-0.96.3}/src/sysmlpy/grammar/antlr4/examples/vehicle-model.sysml +0 -0
  46. {sysmlpy-0.96.2 → sysmlpy-0.96.3}/src/sysmlpy/grammar/antlr4/pom.xml +0 -0
  47. {sysmlpy-0.96.2 → sysmlpy-0.96.3}/src/sysmlpy/grammar/classes.py +0 -0
  48. {sysmlpy-0.96.2 → sysmlpy-0.96.3}/src/sysmlpy/ipython_magic.py +0 -0
  49. {sysmlpy-0.96.2 → sysmlpy-0.96.3}/src/sysmlpy/kerml/KerMLLexer.g4 +0 -0
  50. {sysmlpy-0.96.2 → sysmlpy-0.96.3}/src/sysmlpy/kerml/KerMLLexer.interp +0 -0
  51. {sysmlpy-0.96.2 → sysmlpy-0.96.3}/src/sysmlpy/kerml/KerMLLexer.py +0 -0
  52. {sysmlpy-0.96.2 → sysmlpy-0.96.3}/src/sysmlpy/kerml/KerMLLexer.tokens +0 -0
  53. {sysmlpy-0.96.2 → sysmlpy-0.96.3}/src/sysmlpy/kerml/KerMLParser.g4 +0 -0
  54. {sysmlpy-0.96.2 → sysmlpy-0.96.3}/src/sysmlpy/kerml/KerMLParser.interp +0 -0
  55. {sysmlpy-0.96.2 → sysmlpy-0.96.3}/src/sysmlpy/kerml/KerMLParser.py +0 -0
  56. {sysmlpy-0.96.2 → sysmlpy-0.96.3}/src/sysmlpy/kerml/KerMLParser.tokens +0 -0
  57. {sysmlpy-0.96.2 → sysmlpy-0.96.3}/src/sysmlpy/kerml/KerMLParserVisitor.py +0 -0
  58. {sysmlpy-0.96.2 → sysmlpy-0.96.3}/src/sysmlpy/kerml/README.md +0 -0
  59. {sysmlpy-0.96.2 → sysmlpy-0.96.3}/src/sysmlpy/kerml/__init__.py +0 -0
  60. {sysmlpy-0.96.2 → sysmlpy-0.96.3}/src/sysmlpy/kerml/kerml.py +0 -0
  61. {sysmlpy-0.96.2 → sysmlpy-0.96.3}/src/sysmlpy/kerml/kerml_visitor.py +0 -0
  62. {sysmlpy-0.96.2 → sysmlpy-0.96.3}/src/sysmlpy/library/LICENSE +0 -0
  63. {sysmlpy-0.96.2 → sysmlpy-0.96.3}/src/sysmlpy/library/README.md +0 -0
  64. {sysmlpy-0.96.2 → sysmlpy-0.96.3}/src/sysmlpy/library/__init__.py +0 -0
  65. {sysmlpy-0.96.2 → sysmlpy-0.96.3}/src/sysmlpy/library/domain/Analysis/AnalysisTooling.sysml +0 -0
  66. {sysmlpy-0.96.2 → sysmlpy-0.96.3}/src/sysmlpy/library/domain/Analysis/SampledFunctions.sysml +0 -0
  67. {sysmlpy-0.96.2 → sysmlpy-0.96.3}/src/sysmlpy/library/domain/Analysis/StateSpaceRepresentation.sysml +0 -0
  68. {sysmlpy-0.96.2 → sysmlpy-0.96.3}/src/sysmlpy/library/domain/Analysis/TradeStudies.sysml +0 -0
  69. {sysmlpy-0.96.2 → sysmlpy-0.96.3}/src/sysmlpy/library/domain/Cause and Effect/CausationConnections.sysml +0 -0
  70. {sysmlpy-0.96.2 → sysmlpy-0.96.3}/src/sysmlpy/library/domain/Cause and Effect/CauseAndEffect.sysml +0 -0
  71. {sysmlpy-0.96.2 → sysmlpy-0.96.3}/src/sysmlpy/library/domain/Geometry/ShapeItems.sysml +0 -0
  72. {sysmlpy-0.96.2 → sysmlpy-0.96.3}/src/sysmlpy/library/domain/Geometry/SpatialItems.sysml +0 -0
  73. {sysmlpy-0.96.2 → sysmlpy-0.96.3}/src/sysmlpy/library/domain/Metadata/ImageMetadata.sysml +0 -0
  74. {sysmlpy-0.96.2 → sysmlpy-0.96.3}/src/sysmlpy/library/domain/Metadata/ModelingMetadata.sysml +0 -0
  75. {sysmlpy-0.96.2 → sysmlpy-0.96.3}/src/sysmlpy/library/domain/Metadata/ParametersOfInterestMetadata.sysml +0 -0
  76. {sysmlpy-0.96.2 → sysmlpy-0.96.3}/src/sysmlpy/library/domain/Metadata/RiskMetadata.sysml +0 -0
  77. {sysmlpy-0.96.2 → sysmlpy-0.96.3}/src/sysmlpy/library/domain/Quantities and Units/ISQ.sysml +0 -0
  78. {sysmlpy-0.96.2 → sysmlpy-0.96.3}/src/sysmlpy/library/domain/Quantities and Units/ISQAcoustics.sysml +0 -0
  79. {sysmlpy-0.96.2 → sysmlpy-0.96.3}/src/sysmlpy/library/domain/Quantities and Units/ISQAtomicNuclear.sysml +0 -0
  80. {sysmlpy-0.96.2 → sysmlpy-0.96.3}/src/sysmlpy/library/domain/Quantities and Units/ISQBase.sysml +0 -0
  81. {sysmlpy-0.96.2 → sysmlpy-0.96.3}/src/sysmlpy/library/domain/Quantities and Units/ISQCharacteristicNumbers.sysml +0 -0
  82. {sysmlpy-0.96.2 → sysmlpy-0.96.3}/src/sysmlpy/library/domain/Quantities and Units/ISQChemistryMolecular.sysml +0 -0
  83. {sysmlpy-0.96.2 → sysmlpy-0.96.3}/src/sysmlpy/library/domain/Quantities and Units/ISQCondensedMatter.sysml +0 -0
  84. {sysmlpy-0.96.2 → sysmlpy-0.96.3}/src/sysmlpy/library/domain/Quantities and Units/ISQElectromagnetism.sysml +0 -0
  85. {sysmlpy-0.96.2 → sysmlpy-0.96.3}/src/sysmlpy/library/domain/Quantities and Units/ISQInformation.sysml +0 -0
  86. {sysmlpy-0.96.2 → sysmlpy-0.96.3}/src/sysmlpy/library/domain/Quantities and Units/ISQLight.sysml +0 -0
  87. {sysmlpy-0.96.2 → sysmlpy-0.96.3}/src/sysmlpy/library/domain/Quantities and Units/ISQMechanics.sysml +0 -0
  88. {sysmlpy-0.96.2 → sysmlpy-0.96.3}/src/sysmlpy/library/domain/Quantities and Units/ISQSpaceTime.sysml +0 -0
  89. {sysmlpy-0.96.2 → sysmlpy-0.96.3}/src/sysmlpy/library/domain/Quantities and Units/ISQThermodynamics.sysml +0 -0
  90. {sysmlpy-0.96.2 → sysmlpy-0.96.3}/src/sysmlpy/library/domain/Quantities and Units/MeasurementRefCalculations.sysml +0 -0
  91. {sysmlpy-0.96.2 → sysmlpy-0.96.3}/src/sysmlpy/library/domain/Quantities and Units/MeasurementReferences.sysml +0 -0
  92. {sysmlpy-0.96.2 → sysmlpy-0.96.3}/src/sysmlpy/library/domain/Quantities and Units/Quantities.sysml +0 -0
  93. {sysmlpy-0.96.2 → sysmlpy-0.96.3}/src/sysmlpy/library/domain/Quantities and Units/QuantityCalculations.sysml +0 -0
  94. {sysmlpy-0.96.2 → sysmlpy-0.96.3}/src/sysmlpy/library/domain/Quantities and Units/SI.sysml +0 -0
  95. {sysmlpy-0.96.2 → sysmlpy-0.96.3}/src/sysmlpy/library/domain/Quantities and Units/SIPrefixes.sysml +0 -0
  96. {sysmlpy-0.96.2 → sysmlpy-0.96.3}/src/sysmlpy/library/domain/Quantities and Units/TensorCalculations.sysml +0 -0
  97. {sysmlpy-0.96.2 → sysmlpy-0.96.3}/src/sysmlpy/library/domain/Quantities and Units/Time.sysml +0 -0
  98. {sysmlpy-0.96.2 → sysmlpy-0.96.3}/src/sysmlpy/library/domain/Quantities and Units/USCustomaryUnits.sysml +0 -0
  99. {sysmlpy-0.96.2 → sysmlpy-0.96.3}/src/sysmlpy/library/domain/Quantities and Units/VectorCalculations.sysml +0 -0
  100. {sysmlpy-0.96.2 → sysmlpy-0.96.3}/src/sysmlpy/library/domain/Requirement Derivation/DerivationConnections.sysml +0 -0
  101. {sysmlpy-0.96.2 → sysmlpy-0.96.3}/src/sysmlpy/library/domain/Requirement Derivation/RequirementDerivation.sysml +0 -0
  102. {sysmlpy-0.96.2 → sysmlpy-0.96.3}/src/sysmlpy/library/kernel/Base.kerml +0 -0
  103. {sysmlpy-0.96.2 → sysmlpy-0.96.3}/src/sysmlpy/library/kernel/BaseFunctions.kerml +0 -0
  104. {sysmlpy-0.96.2 → sysmlpy-0.96.3}/src/sysmlpy/library/kernel/BooleanFunctions.kerml +0 -0
  105. {sysmlpy-0.96.2 → sysmlpy-0.96.3}/src/sysmlpy/library/kernel/Clocks.kerml +0 -0
  106. {sysmlpy-0.96.2 → sysmlpy-0.96.3}/src/sysmlpy/library/kernel/CollectionFunctions.kerml +0 -0
  107. {sysmlpy-0.96.2 → sysmlpy-0.96.3}/src/sysmlpy/library/kernel/Collections.kerml +0 -0
  108. {sysmlpy-0.96.2 → sysmlpy-0.96.3}/src/sysmlpy/library/kernel/ComplexFunctions.kerml +0 -0
  109. {sysmlpy-0.96.2 → sysmlpy-0.96.3}/src/sysmlpy/library/kernel/ControlFunctions.kerml +0 -0
  110. {sysmlpy-0.96.2 → sysmlpy-0.96.3}/src/sysmlpy/library/kernel/ControlPerformances.kerml +0 -0
  111. {sysmlpy-0.96.2 → sysmlpy-0.96.3}/src/sysmlpy/library/kernel/DataFunctions.kerml +0 -0
  112. {sysmlpy-0.96.2 → sysmlpy-0.96.3}/src/sysmlpy/library/kernel/FeatureReferencingPerformances.kerml +0 -0
  113. {sysmlpy-0.96.2 → sysmlpy-0.96.3}/src/sysmlpy/library/kernel/IntegerFunctions.kerml +0 -0
  114. {sysmlpy-0.96.2 → sysmlpy-0.96.3}/src/sysmlpy/library/kernel/KerML.kerml +0 -0
  115. {sysmlpy-0.96.2 → sysmlpy-0.96.3}/src/sysmlpy/library/kernel/Links.kerml +0 -0
  116. {sysmlpy-0.96.2 → sysmlpy-0.96.3}/src/sysmlpy/library/kernel/Metaobjects.kerml +0 -0
  117. {sysmlpy-0.96.2 → sysmlpy-0.96.3}/src/sysmlpy/library/kernel/NaturalFunctions.kerml +0 -0
  118. {sysmlpy-0.96.2 → sysmlpy-0.96.3}/src/sysmlpy/library/kernel/NumericalFunctions.kerml +0 -0
  119. {sysmlpy-0.96.2 → sysmlpy-0.96.3}/src/sysmlpy/library/kernel/Objects.kerml +0 -0
  120. {sysmlpy-0.96.2 → sysmlpy-0.96.3}/src/sysmlpy/library/kernel/Observation.kerml +0 -0
  121. {sysmlpy-0.96.2 → sysmlpy-0.96.3}/src/sysmlpy/library/kernel/OccurrenceFunctions.kerml +0 -0
  122. {sysmlpy-0.96.2 → sysmlpy-0.96.3}/src/sysmlpy/library/kernel/Occurrences.kerml +0 -0
  123. {sysmlpy-0.96.2 → sysmlpy-0.96.3}/src/sysmlpy/library/kernel/Performances.kerml +0 -0
  124. {sysmlpy-0.96.2 → sysmlpy-0.96.3}/src/sysmlpy/library/kernel/RationalFunctions.kerml +0 -0
  125. {sysmlpy-0.96.2 → sysmlpy-0.96.3}/src/sysmlpy/library/kernel/RealFunctions.kerml +0 -0
  126. {sysmlpy-0.96.2 → sysmlpy-0.96.3}/src/sysmlpy/library/kernel/ScalarFunctions.kerml +0 -0
  127. {sysmlpy-0.96.2 → sysmlpy-0.96.3}/src/sysmlpy/library/kernel/ScalarValues.kerml +0 -0
  128. {sysmlpy-0.96.2 → sysmlpy-0.96.3}/src/sysmlpy/library/kernel/SequenceFunctions.kerml +0 -0
  129. {sysmlpy-0.96.2 → sysmlpy-0.96.3}/src/sysmlpy/library/kernel/SpatialFrames.kerml +0 -0
  130. {sysmlpy-0.96.2 → sysmlpy-0.96.3}/src/sysmlpy/library/kernel/StatePerformances.kerml +0 -0
  131. {sysmlpy-0.96.2 → sysmlpy-0.96.3}/src/sysmlpy/library/kernel/StringFunctions.kerml +0 -0
  132. {sysmlpy-0.96.2 → sysmlpy-0.96.3}/src/sysmlpy/library/kernel/Transfers.kerml +0 -0
  133. {sysmlpy-0.96.2 → sysmlpy-0.96.3}/src/sysmlpy/library/kernel/TransitionPerformances.kerml +0 -0
  134. {sysmlpy-0.96.2 → sysmlpy-0.96.3}/src/sysmlpy/library/kernel/TrigFunctions.kerml +0 -0
  135. {sysmlpy-0.96.2 → sysmlpy-0.96.3}/src/sysmlpy/library/kernel/Triggers.kerml +0 -0
  136. {sysmlpy-0.96.2 → sysmlpy-0.96.3}/src/sysmlpy/library/kernel/VectorFunctions.kerml +0 -0
  137. {sysmlpy-0.96.2 → sysmlpy-0.96.3}/src/sysmlpy/library/kernel/VectorValues.kerml +0 -0
  138. {sysmlpy-0.96.2 → sysmlpy-0.96.3}/src/sysmlpy/library/systems/Actions.sysml +0 -0
  139. {sysmlpy-0.96.2 → sysmlpy-0.96.3}/src/sysmlpy/library/systems/Allocations.sysml +0 -0
  140. {sysmlpy-0.96.2 → sysmlpy-0.96.3}/src/sysmlpy/library/systems/AnalysisCases.sysml +0 -0
  141. {sysmlpy-0.96.2 → sysmlpy-0.96.3}/src/sysmlpy/library/systems/Attributes.sysml +0 -0
  142. {sysmlpy-0.96.2 → sysmlpy-0.96.3}/src/sysmlpy/library/systems/Calculations.sysml +0 -0
  143. {sysmlpy-0.96.2 → sysmlpy-0.96.3}/src/sysmlpy/library/systems/Cases.sysml +0 -0
  144. {sysmlpy-0.96.2 → sysmlpy-0.96.3}/src/sysmlpy/library/systems/Connections.sysml +0 -0
  145. {sysmlpy-0.96.2 → sysmlpy-0.96.3}/src/sysmlpy/library/systems/Constraints.sysml +0 -0
  146. {sysmlpy-0.96.2 → sysmlpy-0.96.3}/src/sysmlpy/library/systems/Flows.sysml +0 -0
  147. {sysmlpy-0.96.2 → sysmlpy-0.96.3}/src/sysmlpy/library/systems/Interfaces.sysml +0 -0
  148. {sysmlpy-0.96.2 → sysmlpy-0.96.3}/src/sysmlpy/library/systems/Items.sysml +0 -0
  149. {sysmlpy-0.96.2 → sysmlpy-0.96.3}/src/sysmlpy/library/systems/Metadata.sysml +0 -0
  150. {sysmlpy-0.96.2 → sysmlpy-0.96.3}/src/sysmlpy/library/systems/Parts.sysml +0 -0
  151. {sysmlpy-0.96.2 → sysmlpy-0.96.3}/src/sysmlpy/library/systems/Ports.sysml +0 -0
  152. {sysmlpy-0.96.2 → sysmlpy-0.96.3}/src/sysmlpy/library/systems/Requirements.sysml +0 -0
  153. {sysmlpy-0.96.2 → sysmlpy-0.96.3}/src/sysmlpy/library/systems/StandardViewDefinitions.sysml +0 -0
  154. {sysmlpy-0.96.2 → sysmlpy-0.96.3}/src/sysmlpy/library/systems/States.sysml +0 -0
  155. {sysmlpy-0.96.2 → sysmlpy-0.96.3}/src/sysmlpy/library/systems/SysML.sysml +0 -0
  156. {sysmlpy-0.96.2 → sysmlpy-0.96.3}/src/sysmlpy/library/systems/UseCases.sysml +0 -0
  157. {sysmlpy-0.96.2 → sysmlpy-0.96.3}/src/sysmlpy/library/systems/VerificationCases.sysml +0 -0
  158. {sysmlpy-0.96.2 → sysmlpy-0.96.3}/src/sysmlpy/library/systems/Views.sysml +0 -0
  159. {sysmlpy-0.96.2 → sysmlpy-0.96.3}/src/sysmlpy/lsp/__init__.py +0 -0
  160. {sysmlpy-0.96.2 → sysmlpy-0.96.3}/src/sysmlpy/lsp/__main__.py +0 -0
  161. {sysmlpy-0.96.2 → sysmlpy-0.96.3}/src/sysmlpy/lsp/protocol.py +0 -0
  162. {sysmlpy-0.96.2 → sysmlpy-0.96.3}/src/sysmlpy/lsp/server.py +0 -0
  163. {sysmlpy-0.96.2 → sysmlpy-0.96.3}/src/sysmlpy/lsp/stdio.py +0 -0
  164. {sysmlpy-0.96.2 → sysmlpy-0.96.3}/src/sysmlpy/mdtables.py +0 -0
  165. {sysmlpy-0.96.2 → sysmlpy-0.96.3}/src/sysmlpy/navigate.py +0 -0
  166. {sysmlpy-0.96.2 → sysmlpy-0.96.3}/src/sysmlpy/plantuml.py +0 -0
  167. {sysmlpy-0.96.2 → sysmlpy-0.96.3}/src/sysmlpy/project.py +0 -0
  168. {sysmlpy-0.96.2 → sysmlpy-0.96.3}/src/sysmlpy/repl.py +0 -0
  169. {sysmlpy-0.96.2 → sysmlpy-0.96.3}/src/sysmlpy/reqif_io.py +0 -0
  170. {sysmlpy-0.96.2 → sysmlpy-0.96.3}/src/sysmlpy/semantic.py +0 -0
  171. {sysmlpy-0.96.2 → sysmlpy-0.96.3}/src/sysmlpy/sim.py +0 -0
  172. {sysmlpy-0.96.2 → sysmlpy-0.96.3}/src/sysmlpy/spreadsheet.py +0 -0
  173. {sysmlpy-0.96.2 → sysmlpy-0.96.3}/src/sysmlpy/store.py +0 -0
  174. {sysmlpy-0.96.2 → sysmlpy-0.96.3}/src/sysmlpy/traceability.py +0 -0
  175. {sysmlpy-0.96.2 → sysmlpy-0.96.3}/src/sysmlpy/us_customary_units.txt +0 -0
  176. {sysmlpy-0.96.2 → sysmlpy-0.96.3}/src/sysmlpy/usage.py +0 -0
  177. {sysmlpy-0.96.2 → sysmlpy-0.96.3}/src/sysmlpy/validator.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: sysmlpy
3
- Version: 0.96.2
3
+ Version: 0.96.3
4
4
  Summary:
5
5
  License-File: LICENSE
6
6
  Author: Jon R. Fox (mycr0ft)
@@ -987,6 +987,70 @@ See [`docs/plantuml-examples/`](docs/plantuml-examples/) for all rendered exampl
987
987
  | 15 | Relationship Matrix (GridView) | Relationship Matrix |
988
988
  | 16 | Tabular View — Color | Tabular View (color) |
989
989
 
990
+ ## Element Identity: Stable Interchange IDs
991
+
992
+ Element `@id`s in interchange exports follow one of two schemes, and a
993
+ reconciliation mechanism that follows the model across edits:
994
+
995
+ **Default — position-derived (deterministic per source text).** Every
996
+ `@graph` element gets `uuid5` from its tree position (parent id + key +
997
+ index). Same source text ⇒ byte-identical JSON in any process.
998
+
999
+ ```python
1000
+ from sysmlpy import to_interchange
1001
+ document = to_interchange(model) # position-derived @ids
1002
+ ```
1003
+
1004
+ **`stable_ids=True` — content-addressed (L2 idempotency).** Named
1005
+ elements hash their declared qualified-name path + metaclass type;
1006
+ unnamed elements hash the nearest named anchor plus the structural path
1007
+ from it. Inserting or reordering unrelated elements elsewhere leaves
1008
+ untouched ids unchanged:
1009
+
1010
+ ```python
1011
+ document = to_interchange(model, stable_ids=True)
1012
+ ```
1013
+
1014
+ ```bash
1015
+ sysmlpy export model.sysml --stable-ids
1016
+ ```
1017
+
1018
+ **`reconcile_ids` — a registry sidecar (L3 identity).** Content ids
1019
+ still re-mint when an *ancestor is renamed* (the qualified-name path
1020
+ changes) or an element moves to a different parent. To follow a model
1021
+ across its lifetime, keep the previous export as a **registry file**
1022
+ and reconcile each new export against it: every element whose
1023
+ (declared name, type) matches exactly one registry entry adopts the
1024
+ registry's id. Matching is injective — ambiguous (duplicated) keys
1025
+ match nothing and keep derived ids, so the document stays unique.
1026
+
1027
+ ```python
1028
+ from pathlib import Path
1029
+ from sysmlpy import loads, to_interchange, interchange_to_json_text
1030
+
1031
+ registry_path = Path("model.json")
1032
+ document = (to_interchange(model, stable_ids=True)
1033
+ if not registry_path.exists() else
1034
+ to_interchange(model, reconcile_with=registry_path))
1035
+ registry_path.write_text(interchange_to_json_text(document))
1036
+ ```
1037
+
1038
+ Or as a one-liner on the CLI — the registry file is typically also the
1039
+ output file, making every export a fixed point after the first:
1040
+
1041
+ ```bash
1042
+ sysmlpy export model.sysml --reconcile-with model.json -o model.json
1043
+ ```
1044
+
1045
+ Round-trip guarantee: `from_interchange` on a reconciled document
1046
+ rebuilds the live model; re-exporting it *with the reconciled document
1047
+ as its registry* reproduces the identical `@graph`. Elements renamed
1048
+ so thoroughly that no name matches fall back to fresh derived ids
1049
+ (becoming new registry entries on the next write). See
1050
+ `docs/stable-identities.md` for the full analysis and
1051
+ `examples/element_identity.py` for a runnable walkthrough of all
1052
+ identity layers.
1053
+
990
1054
  ## ReqIF Interchange (Requirements Interchange Format)
991
1055
 
992
1056
  sysmlpy imports and exports **ReqIF 1.0** — the OMG requirements interchange
@@ -951,6 +951,70 @@ See [`docs/plantuml-examples/`](docs/plantuml-examples/) for all rendered exampl
951
951
  | 15 | Relationship Matrix (GridView) | Relationship Matrix |
952
952
  | 16 | Tabular View — Color | Tabular View (color) |
953
953
 
954
+ ## Element Identity: Stable Interchange IDs
955
+
956
+ Element `@id`s in interchange exports follow one of two schemes, and a
957
+ reconciliation mechanism that follows the model across edits:
958
+
959
+ **Default — position-derived (deterministic per source text).** Every
960
+ `@graph` element gets `uuid5` from its tree position (parent id + key +
961
+ index). Same source text ⇒ byte-identical JSON in any process.
962
+
963
+ ```python
964
+ from sysmlpy import to_interchange
965
+ document = to_interchange(model) # position-derived @ids
966
+ ```
967
+
968
+ **`stable_ids=True` — content-addressed (L2 idempotency).** Named
969
+ elements hash their declared qualified-name path + metaclass type;
970
+ unnamed elements hash the nearest named anchor plus the structural path
971
+ from it. Inserting or reordering unrelated elements elsewhere leaves
972
+ untouched ids unchanged:
973
+
974
+ ```python
975
+ document = to_interchange(model, stable_ids=True)
976
+ ```
977
+
978
+ ```bash
979
+ sysmlpy export model.sysml --stable-ids
980
+ ```
981
+
982
+ **`reconcile_ids` — a registry sidecar (L3 identity).** Content ids
983
+ still re-mint when an *ancestor is renamed* (the qualified-name path
984
+ changes) or an element moves to a different parent. To follow a model
985
+ across its lifetime, keep the previous export as a **registry file**
986
+ and reconcile each new export against it: every element whose
987
+ (declared name, type) matches exactly one registry entry adopts the
988
+ registry's id. Matching is injective — ambiguous (duplicated) keys
989
+ match nothing and keep derived ids, so the document stays unique.
990
+
991
+ ```python
992
+ from pathlib import Path
993
+ from sysmlpy import loads, to_interchange, interchange_to_json_text
994
+
995
+ registry_path = Path("model.json")
996
+ document = (to_interchange(model, stable_ids=True)
997
+ if not registry_path.exists() else
998
+ to_interchange(model, reconcile_with=registry_path))
999
+ registry_path.write_text(interchange_to_json_text(document))
1000
+ ```
1001
+
1002
+ Or as a one-liner on the CLI — the registry file is typically also the
1003
+ output file, making every export a fixed point after the first:
1004
+
1005
+ ```bash
1006
+ sysmlpy export model.sysml --reconcile-with model.json -o model.json
1007
+ ```
1008
+
1009
+ Round-trip guarantee: `from_interchange` on a reconciled document
1010
+ rebuilds the live model; re-exporting it *with the reconciled document
1011
+ as its registry* reproduces the identical `@graph`. Elements renamed
1012
+ so thoroughly that no name matches fall back to fresh derived ids
1013
+ (becoming new registry entries on the next write). See
1014
+ `docs/stable-identities.md` for the full analysis and
1015
+ `examples/element_identity.py` for a runnable walkthrough of all
1016
+ identity layers.
1017
+
954
1018
  ## ReqIF Interchange (Requirements Interchange Format)
955
1019
 
956
1020
  sysmlpy imports and exports **ReqIF 1.0** — the OMG requirements interchange
@@ -6,7 +6,7 @@ build-backend = "poetry.core.masonry.api"
6
6
 
7
7
  [project]
8
8
  name = "sysmlpy"
9
- version = "0.96.2"
9
+ version = "0.96.3"
10
10
  description = ""
11
11
  authors = [{ name = "Jon R. Fox (mycr0ft)", email = "jon.fox@drfox.com" }]
12
12
  readme = "README.md"
@@ -20,6 +20,7 @@ __all__ = [
20
20
  "as_sequence_view", "as_case_view", "as_browser_view",
21
21
  "extract_traceability", "TraceabilityReport", "RequirementTrace",
22
22
  "to_interchange", "from_interchange", "interchange_to_json_text",
23
+ "reconcile_ids",
23
24
  "reqif_import", "reqif_import_model", "reqif_export",
24
25
  "evaluate_expression", "evaluate_calculation", "collect_values",
25
26
  "check_constraints", "ConstraintReport", "ConstraintResult",
@@ -35,7 +36,7 @@ __all__ = [
35
36
  "render_state_transition_view_svg", "boxes_view",
36
37
  ]
37
38
  __author__ = "Jon Fox"
38
- __version__ = "0.96.2"
39
+ __version__ = "0.96.3"
39
40
 
40
41
  from sysmlpy.usage import (
41
42
  Item, Attribute, Part, Port, Action, Reference, UseCase, Requirement, Interface, Message,
@@ -57,6 +58,7 @@ from sysmlpy.traceability import (
57
58
  )
58
59
  from sysmlpy.interchange import (
59
60
  to_interchange, from_interchange, interchange_to_json_text,
61
+ reconcile_ids,
60
62
  )
61
63
  from sysmlpy.reqif_io import (
62
64
  reqif_import, reqif_import_model, reqif_export,
@@ -393,8 +393,22 @@ def cmd_export(args) -> int:
393
393
  print(f"Parse error: {e}", file=sys.stderr)
394
394
  return 2
395
395
 
396
+ # --reconcile-with <registry>: adopt ids from the previous export
397
+ # (the registry file itself is typically the output path — ids stay
398
+ # stable from then on, even across edits that re-mint derived ids).
399
+ reconcile_with = None
400
+ if getattr(args, "reconcile_with", None):
401
+ reg_path = Path(args.reconcile_with)
402
+ if reg_path.exists():
403
+ reconcile_with = reg_path
404
+ else:
405
+ print(f"Registry file '{reg_path}' not found — minting fresh "
406
+ "stable ids (delete the flag or create the file to "
407
+ "reconcile).", file=sys.stderr)
408
+
396
409
  try:
397
- document = to_interchange(model)
410
+ document = to_interchange(model, stable_ids=args.stable_ids,
411
+ reconcile_with=reconcile_with)
398
412
  except Exception as e:
399
413
  print(f"Error: export failed on this model: {e}", file=sys.stderr)
400
414
  return 1
@@ -967,6 +981,20 @@ def build_parser() -> argparse.ArgumentParser:
967
981
  "--compact", action="store_true",
968
982
  help="Emit compact JSON (no indentation)",
969
983
  )
984
+ p_export.add_argument(
985
+ "--stable-ids", action="store_true",
986
+ help="Content-address element @ids (L2 idempotency): ids follow "
987
+ "declared qualified-name paths, so inserting/reordering "
988
+ "unrelated elements elsewhere leaves untouched ids unchanged",
989
+ )
990
+ p_export.add_argument(
991
+ "--reconcile-with", metavar="REGISTRY",
992
+ help="Adopt element @ids from a previous export document (implies "
993
+ "--stable-ids); ids of name-matched elements survive edits "
994
+ "(L3 identity): re-export repeatedly with "
995
+ "'--reconcile-with model.json -o model.json' to keep ids "
996
+ "stable across the model's lifetime",
997
+ )
970
998
  p_export.add_argument(
971
999
  "-l", "--library",
972
1000
  help="Path to SysML v2 library files to use for parsing",
@@ -63,14 +63,18 @@ from __future__ import annotations
63
63
 
64
64
  import json as _json
65
65
  import uuid as _uuid
66
+ from collections import Counter as _Counter
67
+ from pathlib import Path as _Path
66
68
 
67
69
  __all__ = [
68
70
  "INTERCHANGE_CONTEXT",
69
71
  "INTERCHANGE_VOCABULARY",
70
72
  "INTERCHANGE_NAMESPACE",
73
+ "STABLE_NAMESPACE",
71
74
  "to_interchange",
72
75
  "from_interchange",
73
76
  "interchange_to_json_text",
77
+ "reconcile_ids",
74
78
  "build_jsonld_context",
75
79
  ]
76
80
 
@@ -108,33 +112,157 @@ def _child_id(parent_id: str, key: str, index: int) -> str:
108
112
  )
109
113
 
110
114
 
115
+ # ---------------------------------------------------------------------------
116
+ # stable (content-addressed) identifiers — L2 idempotency
117
+ # ---------------------------------------------------------------------------
118
+
119
+ #: UUID namespace for content-addressed ``stable_ids=True`` identifiers
120
+ #: (distinct from :data:`INTERCHANGE_NAMESPACE` so the two schemes never
121
+ #: collide even in theory).
122
+ STABLE_NAMESPACE = _uuid.uuid5(
123
+ _uuid.NAMESPACE_URL,
124
+ "https://github.com/mycr0ft/sysmlpy/interchange/stable-ids",
125
+ )
126
+
127
+
128
+ def _identification_of(node: dict):
129
+ """The ``Identification`` node declared *by* this element, if any.
130
+
131
+ The parser models names as ``Identification`` dicts nested under
132
+ ``declaration``/``definition`` nodes; usages nest theirs under
133
+ ``declaration.declaration.identification``. A recursive search
134
+ finds any shape without hard-coding the path — but it must not
135
+ descend through the containment edges (``ownedRelatedElement``,
136
+ ``body``): a wrapper (``PackageMember``, ``DefinitionElement``…)
137
+ merely *contains* named elements and must not claim their names.
138
+ Descending into them would let the root claim every name in the
139
+ model, corrupting every identity path.
140
+ """
141
+ if not isinstance(node, dict):
142
+ return None
143
+ if node.get("name") == "Identification":
144
+ return node
145
+ for k, v in node.items():
146
+ if k in ("ownedRelatedElement", "body"):
147
+ continue # containment edges: names below belong to children
148
+ if isinstance(v, list):
149
+ for item in v:
150
+ found = _identification_of(item)
151
+ if found is not None:
152
+ return found
153
+ elif isinstance(v, dict):
154
+ found = _identification_of(v)
155
+ if found is not None:
156
+ return found
157
+ return None
158
+
159
+
160
+ class _StableIdAssigner:
161
+ """Content-addressed ``@id`` assignment over the visitor-dict tree.
162
+
163
+ An element's id hashes its *identity path*: the chain of declared
164
+ names from the root down to the nearest declared ancestor, plus the
165
+ element's own declared name (if any) and ``@type``. Concretely:
166
+
167
+ - Named element: ``uuid5(STABLE_NAMESPACE, qn + "|" + type)``
168
+ where ``qn`` is the dot-joined declared names of the element and
169
+ all its declared ancestors (e.g. ``P.Engine.power``).
170
+ - Unnamed element below a declared ancestor: hashes the anchor QN,
171
+ the structural path from that ancestor (dictionary keys +
172
+ sibling indexes), and the ``@type`` — so it moves only if the
173
+ structure *between* it and its named anchor changes, never when
174
+ unrelated siblings elsewhere shift indexes.
175
+ - Root: hashes the tree path under the fixed ``"root"`` anchor.
176
+
177
+ Same-name sibling collisions (two ``part def Engine`` under one
178
+ parent) share a base hash; the occurrence index among content-key
179
+ siblings is appended so ids stay unique. Elements in a collision
180
+ group shift when a group sibling is inserted before them — the
181
+ documented degradation for L2 (see docs/stable-identities.md).
182
+ """
183
+
184
+ def __init__(self):
185
+ #: base content-key -> next occurrence index
186
+ self._occurrence: dict = {}
187
+ #: node id() -> assigned @id (each dict node is visited once)
188
+ self._assigned: dict = {}
189
+
190
+ def id_for(self, node: dict, qn_path: tuple, struct_path: tuple) -> str:
191
+ ident = _identification_of(node) if isinstance(node, dict) else None
192
+ own_name = (ident or {}).get("declaredName")
193
+ type_name = node.get("name") if isinstance(node, dict) else None
194
+ if own_name:
195
+ qn = "|".join(qn_path + (own_name,))
196
+ key = f"{qn}|{type_name}"
197
+ else:
198
+ key = "root" if not qn_path and not struct_path else (
199
+ "|".join(qn_path) + "||" + _struct_text(struct_path)
200
+ + f"|{type_name}"
201
+ )
202
+ n = self._occurrence.get(key, 0)
203
+ self._occurrence[key] = n + 1
204
+ if n:
205
+ key = f"{key}|#{n}"
206
+ return "sysml:" + str(_uuid.uuid5(STABLE_NAMESPACE, key))
207
+
208
+
209
+ def _struct_text(struct_path: tuple) -> str:
210
+ return ">".join(f"{key}:{i}" for key, i in struct_path)
211
+
212
+
111
213
  def _flatten(node: dict, parent_id: str, key: str, index: int,
112
- graph: list) -> str:
214
+ graph: list, assigner: "_StableIdAssigner | None" = None,
215
+ parent_qn: tuple = (), parent_struct: tuple = ()) -> str:
113
216
  """Recursively flatten a visitor-dict node into the graph.
114
217
 
218
+ With ``assigner=None`` (default) ids are position-derived
219
+ (``_child_id``). With an :class:`_StableIdAssigner`, ids are
220
+ content-addressed: named elements hash their qualified-name path,
221
+ unnamed elements hash the nearest named anchor plus the structural
222
+ path from it — see :class:`_StableIdAssigner`.
223
+
115
224
  Returns the ``@id`` assigned to this node.
116
225
  """
117
- elem_id = _child_id(parent_id, key, index)
226
+ if assigner is not None:
227
+ struct_path = parent_struct + ((key, index),)
228
+ elem_id = assigner.id_for(node, parent_qn, struct_path)
229
+ ident = _identification_of(node)
230
+ own_name = (ident or {}).get("declaredName")
231
+ child_qn = parent_qn + ((own_name,) if own_name else ())
232
+ child_struct = struct_path
233
+ else:
234
+ elem_id = _child_id(parent_id, key, index)
235
+ child_qn = ()
236
+ child_struct = ()
118
237
  elem = {"@id": elem_id, "@type": node.get("name")}
119
238
  for k, v in node.items():
120
239
  if k == "name":
121
240
  continue # captured as @type
122
- elem[k] = _flatten_value(v, elem_id, k, graph)
241
+ elem[k] = _flatten_value(v, elem_id, k, graph,
242
+ assigner=assigner, parent_qn=child_qn,
243
+ parent_struct=child_struct)
123
244
  graph.append(elem)
124
245
  return elem_id
125
246
 
126
247
 
127
- def _flatten_value(v, elem_id: str, key: str, graph: list):
248
+ def _flatten_value(v, elem_id: str, key: str, graph: list,
249
+ assigner: "_StableIdAssigner | None" = None,
250
+ parent_qn: tuple = (), parent_struct: tuple = ()):
128
251
  """Flatten one property value: dicts become @id refs, scalars inline."""
129
252
  if v is None or isinstance(v, (str, int, float, bool)):
130
253
  return v
131
254
  if isinstance(v, dict):
132
- return {"@id": _flatten(v, elem_id, key, 0, graph)}
255
+ return {"@id": _flatten(v, elem_id, key, 0, graph,
256
+ assigner=assigner, parent_qn=parent_qn,
257
+ parent_struct=parent_struct)}
133
258
  if isinstance(v, list):
134
259
  out = []
135
260
  for i, item in enumerate(v):
136
261
  if isinstance(item, dict):
137
- out.append({"@id": _flatten(item, elem_id, key, i, graph)})
262
+ out.append({"@id": _flatten(item, elem_id, key, i, graph,
263
+ assigner=assigner,
264
+ parent_qn=parent_qn,
265
+ parent_struct=parent_struct)})
138
266
  else:
139
267
  out.append(item)
140
268
  return out
@@ -230,7 +358,8 @@ def _extract_properties(source):
230
358
  return props, types
231
359
 
232
360
 
233
- def to_interchange(source, *, vocabulary=None, explicit_terms=False):
361
+ def to_interchange(source, *, vocabulary=None, explicit_terms=False,
362
+ stable_ids=False, reconcile_with=None):
234
363
  """Export a model to the SysML v2 JSON interchange representation.
235
364
 
236
365
  Parameters
@@ -246,6 +375,30 @@ def to_interchange(source, *, vocabulary=None, explicit_terms=False):
246
375
  ``property → <vocabulary>#property`` term definitions for
247
376
  every property and metaclass in the document (see
248
377
  :func:`build_jsonld_context`).
378
+ stable_ids : bool
379
+ When true, ``@id``s are **content-addressed** (L2 idempotency):
380
+ named elements hash their qualified-name path and ``@type``,
381
+ unnamed elements hash the nearest named ancestor plus the
382
+ structural path from it. Inserting or reordering unrelated
383
+ siblings elsewhere in the document then leaves every
384
+ untouched element's id unchanged — with the default
385
+ position-derived ids those ids all shift. Same-name sibling
386
+ groups (two ``part def Engine`` under one parent) are
387
+ disambiguated by occurrence index; ids inside such a group do
388
+ shift on group-local insertion. See
389
+ ``docs/stable-identities.md`` for the full analysis.
390
+ reconcile_with : dict or str, optional
391
+ A previous export (document dict or JSON text of one) to adopt
392
+ ids from — see :func:`reconcile_ids`. Implies
393
+ ``stable_ids=True`` (reconciled re-exports must replay the
394
+ same id scheme the registry was written with). The typical
395
+ workflow keeps the *previous* export file as the registry::
396
+
397
+ if Path("model.json").exists():
398
+ doc = to_interchange(model, reconcile_with="model.json")
399
+ else:
400
+ doc = to_interchange(model, stable_ids=True)
401
+ Path("model.json").write_text(interchange_to_json_text(doc))
249
402
 
250
403
  Returns
251
404
  -------
@@ -285,7 +438,9 @@ def to_interchange(source, *, vocabulary=None, explicit_terms=False):
285
438
  )
286
439
 
287
440
  graph: list = []
288
- root_id = _flatten(root_dict, "root", "ownedRelationship", 0, graph)
441
+ assigner = _StableIdAssigner() if (stable_ids or reconcile_with) else None
442
+ root_id = _flatten(root_dict, "root", "ownedRelationship", 0, graph,
443
+ assigner=assigner)
289
444
  context = dict(INTERCHANGE_CONTEXT)
290
445
  if vocabulary:
291
446
  context["@vocab"] = vocabulary.rstrip("/#") + "#"
@@ -293,11 +448,138 @@ def to_interchange(source, *, vocabulary=None, explicit_terms=False):
293
448
  context = build_jsonld_context(
294
449
  {"@graph": graph}, vocabulary=vocabulary
295
450
  )
296
- return {
451
+ document = {
297
452
  "@context": context,
298
453
  "@id": root_id,
299
454
  "@graph": graph,
300
455
  }
456
+ if reconcile_with is not None:
457
+ document = reconcile_ids(document, reconcile_with)
458
+ return document
459
+
460
+
461
+ # ---------------------------------------------------------------------------
462
+ # id registry reconciliation — L3 identity across edits (Phase 2 sidecar)
463
+ # ---------------------------------------------------------------------------
464
+
465
+
466
+ def _remap_refs(value, remap: dict):
467
+ """Rewrite every ``{"@id": old}`` ref to ``{"@id": new}``. Top-level
468
+ element ids are rewritten by :func:`reconcile_ids` directly; this
469
+ walks nested property values (references, chains) so adopted ids
470
+ are consistent everywhere the document is rebuilt from."""
471
+ if isinstance(value, dict):
472
+ if set(value.keys()) == {"@id"}:
473
+ new_id = remap.get(value["@id"])
474
+ if new_id is not None:
475
+ value["@id"] = new_id
476
+ return
477
+ for v in value.values():
478
+ _remap_refs(v, remap)
479
+ elif isinstance(value, list):
480
+ for item in value:
481
+ _remap_refs(item, remap)
482
+
483
+
484
+ def reconcile_ids(document: dict, registry) -> dict:
485
+ """Adopt element ids from a previous interchange export (the registry).
486
+
487
+ The registry is a previously exported document (``to_interchange``
488
+ output, as dict or JSON text) whose ``@graph`` maps ``@id`` to
489
+ ``(declaredName, @type)``. Every element of ``document`` whose
490
+ ``(declaredName, @type)`` matches **exactly one** registry entry —
491
+ and exactly one same-key element in ``document`` — inherits the
492
+ registry's ``@id`` instead of its freshly derived one. This is the
493
+ mechanism that keeps ids stable across edits that re-mint derived
494
+ ids (ancestor renames, re-nesting): identity follows the declared
495
+ name wherever the name survives the edit.
496
+
497
+ Matching is deliberately **injective**:
498
+
499
+ - the key must be unique on BOTH sides — when the model grows a
500
+ second ``PartDefinition`` while the registry has one, neither is
501
+ matched (a naive many-to-one adoption produced duplicate ids in
502
+ every rename-plus-insert scenario during the design spike);
503
+ - an old id already adopted by an earlier element is never adopted
504
+ again (first match wins in registry order);
505
+ - unmatched elements keep their freshly derived ids and become new
506
+ registry entries when the document is written back out.
507
+
508
+ Intended workflow (the export file doubles as the registry)::
509
+
510
+ document = to_interchange(model, stable_ids=True)
511
+ if Path("model.json").exists():
512
+ document = reconcile_ids(document, Path("model.json"))
513
+ Path("model.json").write_text(interchange_to_json_text(document))
514
+
515
+ Parameters
516
+ ----------
517
+ document : dict
518
+ A freshly exported document (``to_interchange`` output).
519
+ registry : dict or str
520
+ The previous export: a document dict or a JSON string (the
521
+ text form loads the sidecar file directly).
522
+
523
+ Returns
524
+ -------
525
+ dict
526
+ ``document`` with adopted ids (modified in place and returned).
527
+ """
528
+ if isinstance(registry, (_Path, str)):
529
+ # Accept a filesystem path to a registry file (including
530
+ # ``Path`` objects), OR the previous export as JSON text /
531
+ # document dict. A str that names an existing, plausibly
532
+ # small file loads from disk; anything else is JSON text.
533
+ # (A str is only treated as a path if its length is a sane
534
+ # filename length — JSON documents can be megabytes.)
535
+ if isinstance(registry, _Path) or (
536
+ isinstance(registry, str) and len(registry) < 4097
537
+ and "\n" not in registry and _Path(registry).exists()):
538
+ registry = _Path(registry).read_text(encoding="utf-8-sig")
539
+ registry = _json.loads(registry)
540
+ if not isinstance(registry, dict):
541
+ raise ValueError(
542
+ "registry must be an interchange document dict or JSON "
543
+ f"string, got {type(registry).__name__}")
544
+ old_graph = registry.get("@graph")
545
+ new_graph = document.get("@graph")
546
+ if not isinstance(old_graph, list) or not isinstance(new_graph, list):
547
+ raise ValueError("both documents need an @graph array")
548
+
549
+ def _key(e):
550
+ return (e.get("declaredName"), e.get("@type"))
551
+
552
+ old_counts = _Counter(_key(e) for e in old_graph)
553
+ new_counts = _Counter(_key(e) for e in new_graph)
554
+ by_key: dict = {}
555
+ for e in old_graph:
556
+ k = _key(e)
557
+ # unique on BOTH sides — injective matching (see docstring)
558
+ if k in old_counts and old_counts[k] == 1 and new_counts.get(k) == 1 \
559
+ and k not in by_key:
560
+ by_key[k] = e["@id"]
561
+ used: set = set()
562
+
563
+ # id re-map for refs: a matched element's *derived* id (assigned
564
+ # during this export) must yield to the adopted registry id in
565
+ # EVERY nested ``{"@id": ...}`` reference too — element ids are
566
+ # only the top level; typed-by references, target refs and child
567
+ # chains all point at the derived ids and would dangle after a
568
+ # top-level-only rewrite.
569
+ remap: dict = {}
570
+ for e in new_graph:
571
+ k = _key(e)
572
+ old_id = by_key.get(k)
573
+ if old_id is not None and old_id not in used:
574
+ used.add(old_id)
575
+ derived_id = e["@id"]
576
+ e["@id"] = old_id
577
+ remap[derived_id] = old_id
578
+
579
+ if remap:
580
+ for e in new_graph:
581
+ _remap_refs(e, remap)
582
+ return document
301
583
 
302
584
 
303
585
  def interchange_to_json_text(document: dict, *, indent: int = 2) -> str:
File without changes
File without changes