sysmlpy 0.55.0__tar.gz → 0.58.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 (148) hide show
  1. {sysmlpy-0.55.0 → sysmlpy-0.58.0}/PKG-INFO +6 -4
  2. {sysmlpy-0.55.0 → sysmlpy-0.58.0}/README.md +5 -3
  3. {sysmlpy-0.55.0 → sysmlpy-0.58.0}/pyproject.toml +1 -1
  4. {sysmlpy-0.55.0 → sysmlpy-0.58.0}/src/sysmlpy/__init__.py +1 -1
  5. {sysmlpy-0.55.0 → sysmlpy-0.58.0}/src/sysmlpy/antlr_parser.py +67 -32
  6. {sysmlpy-0.55.0 → sysmlpy-0.58.0}/src/sysmlpy/definition.py +58 -19
  7. {sysmlpy-0.55.0 → sysmlpy-0.58.0}/src/sysmlpy/examples/attribute_values.py +7 -5
  8. {sysmlpy-0.55.0 → sysmlpy-0.58.0}/src/sysmlpy/examples/part_attributes.py +6 -4
  9. {sysmlpy-0.55.0 → sysmlpy-0.58.0}/src/sysmlpy/store.py +268 -0
  10. {sysmlpy-0.55.0 → sysmlpy-0.58.0}/src/sysmlpy/usage.py +134 -71
  11. {sysmlpy-0.55.0 → sysmlpy-0.58.0}/LICENSE +0 -0
  12. {sysmlpy-0.55.0 → sysmlpy-0.58.0}/src/sysmlpy/__main__.py +0 -0
  13. {sysmlpy-0.55.0 → sysmlpy-0.58.0}/src/sysmlpy/antlr/README.md +0 -0
  14. {sysmlpy-0.55.0 → sysmlpy-0.58.0}/src/sysmlpy/antlr/SysMLv2Lexer.interp +0 -0
  15. {sysmlpy-0.55.0 → sysmlpy-0.58.0}/src/sysmlpy/antlr/SysMLv2Lexer.py +0 -0
  16. {sysmlpy-0.55.0 → sysmlpy-0.58.0}/src/sysmlpy/antlr/SysMLv2Lexer.tokens +0 -0
  17. {sysmlpy-0.55.0 → sysmlpy-0.58.0}/src/sysmlpy/antlr/SysMLv2Parser.interp +0 -0
  18. {sysmlpy-0.55.0 → sysmlpy-0.58.0}/src/sysmlpy/antlr/SysMLv2Parser.py +0 -0
  19. {sysmlpy-0.55.0 → sysmlpy-0.58.0}/src/sysmlpy/antlr/SysMLv2Parser.tokens +0 -0
  20. {sysmlpy-0.55.0 → sysmlpy-0.58.0}/src/sysmlpy/antlr/SysMLv2ParserListener.py +0 -0
  21. {sysmlpy-0.55.0 → sysmlpy-0.58.0}/src/sysmlpy/antlr/SysMLv2ParserVisitor.py +0 -0
  22. {sysmlpy-0.55.0 → sysmlpy-0.58.0}/src/sysmlpy/antlr/src/sysmlpy/grammar/antlr4/SysMLv2Lexer.interp +0 -0
  23. {sysmlpy-0.55.0 → sysmlpy-0.58.0}/src/sysmlpy/antlr/src/sysmlpy/grammar/antlr4/SysMLv2Lexer.py +0 -0
  24. {sysmlpy-0.55.0 → sysmlpy-0.58.0}/src/sysmlpy/antlr/src/sysmlpy/grammar/antlr4/SysMLv2Lexer.tokens +0 -0
  25. {sysmlpy-0.55.0 → sysmlpy-0.58.0}/src/sysmlpy/antlr_visitor.py +0 -0
  26. {sysmlpy-0.55.0 → sysmlpy-0.58.0}/src/sysmlpy/boxes_view.py +0 -0
  27. {sysmlpy-0.55.0 → sysmlpy-0.58.0}/src/sysmlpy/examples/tuples_sequences.py +0 -0
  28. {sysmlpy-0.55.0 → sysmlpy-0.58.0}/src/sysmlpy/formatting.py +0 -0
  29. {sysmlpy-0.55.0 → sysmlpy-0.58.0}/src/sysmlpy/grammar/antlr4/README.md +0 -0
  30. {sysmlpy-0.55.0 → sysmlpy-0.58.0}/src/sysmlpy/grammar/antlr4/SysMLv2Lexer.g4 +0 -0
  31. {sysmlpy-0.55.0 → sysmlpy-0.58.0}/src/sysmlpy/grammar/antlr4/SysMLv2Lexer.interp +0 -0
  32. {sysmlpy-0.55.0 → sysmlpy-0.58.0}/src/sysmlpy/grammar/antlr4/SysMLv2Lexer.py +0 -0
  33. {sysmlpy-0.55.0 → sysmlpy-0.58.0}/src/sysmlpy/grammar/antlr4/SysMLv2Lexer.tokens +0 -0
  34. {sysmlpy-0.55.0 → sysmlpy-0.58.0}/src/sysmlpy/grammar/antlr4/SysMLv2Parser.g4 +0 -0
  35. {sysmlpy-0.55.0 → sysmlpy-0.58.0}/src/sysmlpy/grammar/antlr4/SysMLv2Parser.interp +0 -0
  36. {sysmlpy-0.55.0 → sysmlpy-0.58.0}/src/sysmlpy/grammar/antlr4/SysMLv2Parser.py +0 -0
  37. {sysmlpy-0.55.0 → sysmlpy-0.58.0}/src/sysmlpy/grammar/antlr4/SysMLv2Parser.tokens +0 -0
  38. {sysmlpy-0.55.0 → sysmlpy-0.58.0}/src/sysmlpy/grammar/antlr4/SysMLv2ParserListener.py +0 -0
  39. {sysmlpy-0.55.0 → sysmlpy-0.58.0}/src/sysmlpy/grammar/antlr4/SysMLv2ParserVisitor.py +0 -0
  40. {sysmlpy-0.55.0 → sysmlpy-0.58.0}/src/sysmlpy/grammar/antlr4/desc.xml +0 -0
  41. {sysmlpy-0.55.0 → sysmlpy-0.58.0}/src/sysmlpy/grammar/antlr4/examples/camera.sysml +0 -0
  42. {sysmlpy-0.55.0 → sysmlpy-0.58.0}/src/sysmlpy/grammar/antlr4/examples/toaster-system.sysml +0 -0
  43. {sysmlpy-0.55.0 → sysmlpy-0.58.0}/src/sysmlpy/grammar/antlr4/examples/vehicle-model.sysml +0 -0
  44. {sysmlpy-0.55.0 → sysmlpy-0.58.0}/src/sysmlpy/grammar/antlr4/pom.xml +0 -0
  45. {sysmlpy-0.55.0 → sysmlpy-0.58.0}/src/sysmlpy/grammar/classes.py +0 -0
  46. {sysmlpy-0.55.0 → sysmlpy-0.58.0}/src/sysmlpy/library/LICENSE +0 -0
  47. {sysmlpy-0.55.0 → sysmlpy-0.58.0}/src/sysmlpy/library/README.md +0 -0
  48. {sysmlpy-0.55.0 → sysmlpy-0.58.0}/src/sysmlpy/library/__init__.py +0 -0
  49. {sysmlpy-0.55.0 → sysmlpy-0.58.0}/src/sysmlpy/library/domain/Analysis/AnalysisTooling.sysml +0 -0
  50. {sysmlpy-0.55.0 → sysmlpy-0.58.0}/src/sysmlpy/library/domain/Analysis/SampledFunctions.sysml +0 -0
  51. {sysmlpy-0.55.0 → sysmlpy-0.58.0}/src/sysmlpy/library/domain/Analysis/StateSpaceRepresentation.sysml +0 -0
  52. {sysmlpy-0.55.0 → sysmlpy-0.58.0}/src/sysmlpy/library/domain/Analysis/TradeStudies.sysml +0 -0
  53. {sysmlpy-0.55.0 → sysmlpy-0.58.0}/src/sysmlpy/library/domain/Cause and Effect/CausationConnections.sysml +0 -0
  54. {sysmlpy-0.55.0 → sysmlpy-0.58.0}/src/sysmlpy/library/domain/Cause and Effect/CauseAndEffect.sysml +0 -0
  55. {sysmlpy-0.55.0 → sysmlpy-0.58.0}/src/sysmlpy/library/domain/Geometry/ShapeItems.sysml +0 -0
  56. {sysmlpy-0.55.0 → sysmlpy-0.58.0}/src/sysmlpy/library/domain/Geometry/SpatialItems.sysml +0 -0
  57. {sysmlpy-0.55.0 → sysmlpy-0.58.0}/src/sysmlpy/library/domain/Metadata/ImageMetadata.sysml +0 -0
  58. {sysmlpy-0.55.0 → sysmlpy-0.58.0}/src/sysmlpy/library/domain/Metadata/ModelingMetadata.sysml +0 -0
  59. {sysmlpy-0.55.0 → sysmlpy-0.58.0}/src/sysmlpy/library/domain/Metadata/ParametersOfInterestMetadata.sysml +0 -0
  60. {sysmlpy-0.55.0 → sysmlpy-0.58.0}/src/sysmlpy/library/domain/Metadata/RiskMetadata.sysml +0 -0
  61. {sysmlpy-0.55.0 → sysmlpy-0.58.0}/src/sysmlpy/library/domain/Quantities and Units/ISQ.sysml +0 -0
  62. {sysmlpy-0.55.0 → sysmlpy-0.58.0}/src/sysmlpy/library/domain/Quantities and Units/ISQAcoustics.sysml +0 -0
  63. {sysmlpy-0.55.0 → sysmlpy-0.58.0}/src/sysmlpy/library/domain/Quantities and Units/ISQAtomicNuclear.sysml +0 -0
  64. {sysmlpy-0.55.0 → sysmlpy-0.58.0}/src/sysmlpy/library/domain/Quantities and Units/ISQBase.sysml +0 -0
  65. {sysmlpy-0.55.0 → sysmlpy-0.58.0}/src/sysmlpy/library/domain/Quantities and Units/ISQCharacteristicNumbers.sysml +0 -0
  66. {sysmlpy-0.55.0 → sysmlpy-0.58.0}/src/sysmlpy/library/domain/Quantities and Units/ISQChemistryMolecular.sysml +0 -0
  67. {sysmlpy-0.55.0 → sysmlpy-0.58.0}/src/sysmlpy/library/domain/Quantities and Units/ISQCondensedMatter.sysml +0 -0
  68. {sysmlpy-0.55.0 → sysmlpy-0.58.0}/src/sysmlpy/library/domain/Quantities and Units/ISQElectromagnetism.sysml +0 -0
  69. {sysmlpy-0.55.0 → sysmlpy-0.58.0}/src/sysmlpy/library/domain/Quantities and Units/ISQInformation.sysml +0 -0
  70. {sysmlpy-0.55.0 → sysmlpy-0.58.0}/src/sysmlpy/library/domain/Quantities and Units/ISQLight.sysml +0 -0
  71. {sysmlpy-0.55.0 → sysmlpy-0.58.0}/src/sysmlpy/library/domain/Quantities and Units/ISQMechanics.sysml +0 -0
  72. {sysmlpy-0.55.0 → sysmlpy-0.58.0}/src/sysmlpy/library/domain/Quantities and Units/ISQSpaceTime.sysml +0 -0
  73. {sysmlpy-0.55.0 → sysmlpy-0.58.0}/src/sysmlpy/library/domain/Quantities and Units/ISQThermodynamics.sysml +0 -0
  74. {sysmlpy-0.55.0 → sysmlpy-0.58.0}/src/sysmlpy/library/domain/Quantities and Units/MeasurementRefCalculations.sysml +0 -0
  75. {sysmlpy-0.55.0 → sysmlpy-0.58.0}/src/sysmlpy/library/domain/Quantities and Units/MeasurementReferences.sysml +0 -0
  76. {sysmlpy-0.55.0 → sysmlpy-0.58.0}/src/sysmlpy/library/domain/Quantities and Units/Quantities.sysml +0 -0
  77. {sysmlpy-0.55.0 → sysmlpy-0.58.0}/src/sysmlpy/library/domain/Quantities and Units/QuantityCalculations.sysml +0 -0
  78. {sysmlpy-0.55.0 → sysmlpy-0.58.0}/src/sysmlpy/library/domain/Quantities and Units/SI.sysml +0 -0
  79. {sysmlpy-0.55.0 → sysmlpy-0.58.0}/src/sysmlpy/library/domain/Quantities and Units/SIPrefixes.sysml +0 -0
  80. {sysmlpy-0.55.0 → sysmlpy-0.58.0}/src/sysmlpy/library/domain/Quantities and Units/TensorCalculations.sysml +0 -0
  81. {sysmlpy-0.55.0 → sysmlpy-0.58.0}/src/sysmlpy/library/domain/Quantities and Units/Time.sysml +0 -0
  82. {sysmlpy-0.55.0 → sysmlpy-0.58.0}/src/sysmlpy/library/domain/Quantities and Units/USCustomaryUnits.sysml +0 -0
  83. {sysmlpy-0.55.0 → sysmlpy-0.58.0}/src/sysmlpy/library/domain/Quantities and Units/VectorCalculations.sysml +0 -0
  84. {sysmlpy-0.55.0 → sysmlpy-0.58.0}/src/sysmlpy/library/domain/Requirement Derivation/DerivationConnections.sysml +0 -0
  85. {sysmlpy-0.55.0 → sysmlpy-0.58.0}/src/sysmlpy/library/domain/Requirement Derivation/RequirementDerivation.sysml +0 -0
  86. {sysmlpy-0.55.0 → sysmlpy-0.58.0}/src/sysmlpy/library/kernel/Base.kerml +0 -0
  87. {sysmlpy-0.55.0 → sysmlpy-0.58.0}/src/sysmlpy/library/kernel/BaseFunctions.kerml +0 -0
  88. {sysmlpy-0.55.0 → sysmlpy-0.58.0}/src/sysmlpy/library/kernel/BooleanFunctions.kerml +0 -0
  89. {sysmlpy-0.55.0 → sysmlpy-0.58.0}/src/sysmlpy/library/kernel/Clocks.kerml +0 -0
  90. {sysmlpy-0.55.0 → sysmlpy-0.58.0}/src/sysmlpy/library/kernel/CollectionFunctions.kerml +0 -0
  91. {sysmlpy-0.55.0 → sysmlpy-0.58.0}/src/sysmlpy/library/kernel/Collections.kerml +0 -0
  92. {sysmlpy-0.55.0 → sysmlpy-0.58.0}/src/sysmlpy/library/kernel/ComplexFunctions.kerml +0 -0
  93. {sysmlpy-0.55.0 → sysmlpy-0.58.0}/src/sysmlpy/library/kernel/ControlFunctions.kerml +0 -0
  94. {sysmlpy-0.55.0 → sysmlpy-0.58.0}/src/sysmlpy/library/kernel/ControlPerformances.kerml +0 -0
  95. {sysmlpy-0.55.0 → sysmlpy-0.58.0}/src/sysmlpy/library/kernel/DataFunctions.kerml +0 -0
  96. {sysmlpy-0.55.0 → sysmlpy-0.58.0}/src/sysmlpy/library/kernel/FeatureReferencingPerformances.kerml +0 -0
  97. {sysmlpy-0.55.0 → sysmlpy-0.58.0}/src/sysmlpy/library/kernel/IntegerFunctions.kerml +0 -0
  98. {sysmlpy-0.55.0 → sysmlpy-0.58.0}/src/sysmlpy/library/kernel/KerML.kerml +0 -0
  99. {sysmlpy-0.55.0 → sysmlpy-0.58.0}/src/sysmlpy/library/kernel/Links.kerml +0 -0
  100. {sysmlpy-0.55.0 → sysmlpy-0.58.0}/src/sysmlpy/library/kernel/Metaobjects.kerml +0 -0
  101. {sysmlpy-0.55.0 → sysmlpy-0.58.0}/src/sysmlpy/library/kernel/NaturalFunctions.kerml +0 -0
  102. {sysmlpy-0.55.0 → sysmlpy-0.58.0}/src/sysmlpy/library/kernel/NumericalFunctions.kerml +0 -0
  103. {sysmlpy-0.55.0 → sysmlpy-0.58.0}/src/sysmlpy/library/kernel/Objects.kerml +0 -0
  104. {sysmlpy-0.55.0 → sysmlpy-0.58.0}/src/sysmlpy/library/kernel/Observation.kerml +0 -0
  105. {sysmlpy-0.55.0 → sysmlpy-0.58.0}/src/sysmlpy/library/kernel/OccurrenceFunctions.kerml +0 -0
  106. {sysmlpy-0.55.0 → sysmlpy-0.58.0}/src/sysmlpy/library/kernel/Occurrences.kerml +0 -0
  107. {sysmlpy-0.55.0 → sysmlpy-0.58.0}/src/sysmlpy/library/kernel/Performances.kerml +0 -0
  108. {sysmlpy-0.55.0 → sysmlpy-0.58.0}/src/sysmlpy/library/kernel/RationalFunctions.kerml +0 -0
  109. {sysmlpy-0.55.0 → sysmlpy-0.58.0}/src/sysmlpy/library/kernel/RealFunctions.kerml +0 -0
  110. {sysmlpy-0.55.0 → sysmlpy-0.58.0}/src/sysmlpy/library/kernel/ScalarFunctions.kerml +0 -0
  111. {sysmlpy-0.55.0 → sysmlpy-0.58.0}/src/sysmlpy/library/kernel/ScalarValues.kerml +0 -0
  112. {sysmlpy-0.55.0 → sysmlpy-0.58.0}/src/sysmlpy/library/kernel/SequenceFunctions.kerml +0 -0
  113. {sysmlpy-0.55.0 → sysmlpy-0.58.0}/src/sysmlpy/library/kernel/SpatialFrames.kerml +0 -0
  114. {sysmlpy-0.55.0 → sysmlpy-0.58.0}/src/sysmlpy/library/kernel/StatePerformances.kerml +0 -0
  115. {sysmlpy-0.55.0 → sysmlpy-0.58.0}/src/sysmlpy/library/kernel/StringFunctions.kerml +0 -0
  116. {sysmlpy-0.55.0 → sysmlpy-0.58.0}/src/sysmlpy/library/kernel/Transfers.kerml +0 -0
  117. {sysmlpy-0.55.0 → sysmlpy-0.58.0}/src/sysmlpy/library/kernel/TransitionPerformances.kerml +0 -0
  118. {sysmlpy-0.55.0 → sysmlpy-0.58.0}/src/sysmlpy/library/kernel/TrigFunctions.kerml +0 -0
  119. {sysmlpy-0.55.0 → sysmlpy-0.58.0}/src/sysmlpy/library/kernel/Triggers.kerml +0 -0
  120. {sysmlpy-0.55.0 → sysmlpy-0.58.0}/src/sysmlpy/library/kernel/VectorFunctions.kerml +0 -0
  121. {sysmlpy-0.55.0 → sysmlpy-0.58.0}/src/sysmlpy/library/kernel/VectorValues.kerml +0 -0
  122. {sysmlpy-0.55.0 → sysmlpy-0.58.0}/src/sysmlpy/library/systems/Actions.sysml +0 -0
  123. {sysmlpy-0.55.0 → sysmlpy-0.58.0}/src/sysmlpy/library/systems/Allocations.sysml +0 -0
  124. {sysmlpy-0.55.0 → sysmlpy-0.58.0}/src/sysmlpy/library/systems/AnalysisCases.sysml +0 -0
  125. {sysmlpy-0.55.0 → sysmlpy-0.58.0}/src/sysmlpy/library/systems/Attributes.sysml +0 -0
  126. {sysmlpy-0.55.0 → sysmlpy-0.58.0}/src/sysmlpy/library/systems/Calculations.sysml +0 -0
  127. {sysmlpy-0.55.0 → sysmlpy-0.58.0}/src/sysmlpy/library/systems/Cases.sysml +0 -0
  128. {sysmlpy-0.55.0 → sysmlpy-0.58.0}/src/sysmlpy/library/systems/Connections.sysml +0 -0
  129. {sysmlpy-0.55.0 → sysmlpy-0.58.0}/src/sysmlpy/library/systems/Constraints.sysml +0 -0
  130. {sysmlpy-0.55.0 → sysmlpy-0.58.0}/src/sysmlpy/library/systems/Flows.sysml +0 -0
  131. {sysmlpy-0.55.0 → sysmlpy-0.58.0}/src/sysmlpy/library/systems/Interfaces.sysml +0 -0
  132. {sysmlpy-0.55.0 → sysmlpy-0.58.0}/src/sysmlpy/library/systems/Items.sysml +0 -0
  133. {sysmlpy-0.55.0 → sysmlpy-0.58.0}/src/sysmlpy/library/systems/Metadata.sysml +0 -0
  134. {sysmlpy-0.55.0 → sysmlpy-0.58.0}/src/sysmlpy/library/systems/Parts.sysml +0 -0
  135. {sysmlpy-0.55.0 → sysmlpy-0.58.0}/src/sysmlpy/library/systems/Ports.sysml +0 -0
  136. {sysmlpy-0.55.0 → sysmlpy-0.58.0}/src/sysmlpy/library/systems/Requirements.sysml +0 -0
  137. {sysmlpy-0.55.0 → sysmlpy-0.58.0}/src/sysmlpy/library/systems/StandardViewDefinitions.sysml +0 -0
  138. {sysmlpy-0.55.0 → sysmlpy-0.58.0}/src/sysmlpy/library/systems/States.sysml +0 -0
  139. {sysmlpy-0.55.0 → sysmlpy-0.58.0}/src/sysmlpy/library/systems/SysML.sysml +0 -0
  140. {sysmlpy-0.55.0 → sysmlpy-0.58.0}/src/sysmlpy/library/systems/UseCases.sysml +0 -0
  141. {sysmlpy-0.55.0 → sysmlpy-0.58.0}/src/sysmlpy/library/systems/VerificationCases.sysml +0 -0
  142. {sysmlpy-0.55.0 → sysmlpy-0.58.0}/src/sysmlpy/library/systems/Views.sysml +0 -0
  143. {sysmlpy-0.55.0 → sysmlpy-0.58.0}/src/sysmlpy/navigate.py +0 -0
  144. {sysmlpy-0.55.0 → sysmlpy-0.58.0}/src/sysmlpy/plantuml.py +0 -0
  145. {sysmlpy-0.55.0 → sysmlpy-0.58.0}/src/sysmlpy/project.py +0 -0
  146. {sysmlpy-0.55.0 → sysmlpy-0.58.0}/src/sysmlpy/semantic.py +0 -0
  147. {sysmlpy-0.55.0 → sysmlpy-0.58.0}/src/sysmlpy/us_customary_units.txt +0 -0
  148. {sysmlpy-0.55.0 → sysmlpy-0.58.0}/src/sysmlpy/validator.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: sysmlpy
3
- Version: 0.55.0
3
+ Version: 0.58.0
4
4
  Summary:
5
5
  License-File: LICENSE
6
6
  Author: Jon R. Fox (mycr0ft)
@@ -44,9 +44,11 @@ The project had diverged so much from sysml2py that a new name, sysmlpy, was sel
44
44
  > symbol table** (v0.54.0) and operators are **type- and
45
45
  > unit-checked** — `flag and n`, `"a" + 5`, and `[m] + [kg]` all raise
46
46
  > `OPERAND_TYPE_MISMATCH` / `UNIT_DIMENSION_MISMATCH` errors. Deterministic
47
- > literal expressions can be reduced with `const_fold()`. Coming next
48
- > (Phase D): parsing performance and graph-store query extensions.
49
- > Track progress in [docs/DEVELOPMENT_PLAN.md](docs/DEVELOPMENT_PLAN.md).
47
+ > literal expressions can be reduced with `const_fold()` (v0.55.0).
48
+ > v0.56.0 adds two-stage SLL→LL parsing (38% faster on large models)
49
+ > and graph query extensions (`all_paths`, `impact_analysis`,
50
+ > `execute_cypher`, …). Track progress in
51
+ > [docs/DEVELOPMENT_PLAN.md](docs/DEVELOPMENT_PLAN.md).
50
52
 
51
53
  > **Scope reminder** — a green parse result means the file **parsed
52
54
  > syntactically**. Expression validation (name resolution, type and
@@ -19,9 +19,11 @@ The project had diverged so much from sysml2py that a new name, sysmlpy, was sel
19
19
  > symbol table** (v0.54.0) and operators are **type- and
20
20
  > unit-checked** — `flag and n`, `"a" + 5`, and `[m] + [kg]` all raise
21
21
  > `OPERAND_TYPE_MISMATCH` / `UNIT_DIMENSION_MISMATCH` errors. Deterministic
22
- > literal expressions can be reduced with `const_fold()`. Coming next
23
- > (Phase D): parsing performance and graph-store query extensions.
24
- > Track progress in [docs/DEVELOPMENT_PLAN.md](docs/DEVELOPMENT_PLAN.md).
22
+ > literal expressions can be reduced with `const_fold()` (v0.55.0).
23
+ > v0.56.0 adds two-stage SLL→LL parsing (38% faster on large models)
24
+ > and graph query extensions (`all_paths`, `impact_analysis`,
25
+ > `execute_cypher`, …). Track progress in
26
+ > [docs/DEVELOPMENT_PLAN.md](docs/DEVELOPMENT_PLAN.md).
25
27
 
26
28
  > **Scope reminder** — a green parse result means the file **parsed
27
29
  > syntactically**. Expression validation (name resolution, type and
@@ -6,7 +6,7 @@ build-backend = "poetry.core.masonry.api"
6
6
 
7
7
  [project]
8
8
  name = "sysmlpy"
9
- version = "0.55.0"
9
+ version = "0.58.0"
10
10
  description = ""
11
11
  authors = [{ name = "Jon R. Fox (mycr0ft)", email = "jon.fox@drfox.com" }]
12
12
  readme = "README.md"
@@ -25,7 +25,7 @@ __all__ = [
25
25
  "render_state_transition_view_svg", "boxes_view",
26
26
  ]
27
27
  __author__ = "Jon Fox"
28
- __version__ = "0.55.0"
28
+ __version__ = "0.58.0"
29
29
 
30
30
  from sysmlpy.usage import (
31
31
  Item, Attribute, Part, Port, Action, Reference, UseCase, Requirement, Interface, Message,
@@ -9,6 +9,7 @@ import sys
9
9
  import os
10
10
 
11
11
  from antlr4 import InputStream, CommonTokenStream
12
+ from antlr4.atn.PredictionMode import PredictionMode
12
13
  from antlr4.error.ErrorListener import ErrorListener
13
14
 
14
15
  from sysmlpy.antlr.SysMLv2Lexer import SysMLv2Lexer
@@ -39,9 +40,25 @@ class ANTLRErrorListener(ErrorListener):
39
40
  pass
40
41
 
41
42
 
42
- def parse(source, library=None, recover=False):
43
+ def _make_parser(content):
44
+ """Build a lexer/parser pair (with our error listener attached)."""
45
+ input_stream = InputStream(content)
46
+ lexer = SysMLv2Lexer(input_stream)
47
+ lexer.removeErrorListeners()
48
+ token_stream = CommonTokenStream(lexer)
49
+ parser = SysMLv2Parser(token_stream)
50
+ parser.removeErrorListeners()
51
+ return lexer, token_stream, parser
52
+
53
+
54
+ def parse(source, library=None, recover=False, prediction_mode="sll"):
43
55
  """Parse SysML v2.0 source and return a parse tree.
44
56
 
57
+ Uses two-stage parsing by default (v0.56.0): a fast SLL prediction
58
+ pass first, falling back to the full LL pass only when the SLL pass
59
+ reports syntax errors. This substantially accelerates large models
60
+ (10k+ elements) while producing identical trees for valid input.
61
+
45
62
  Parameters
46
63
  ----------
47
64
  source : str or file-like
@@ -49,12 +66,16 @@ def parse(source, library=None, recover=False):
49
66
  library : str or Path, optional
50
67
  Path to SysML v2 library files for resolving imports.
51
68
  recover : bool, default False
52
- When True, ANTLR's default error strategy is used and the parse tree
53
- is returned even when there are syntax errors. Errors are collected
54
- and returned alongside the tree. The returned tuple shape is
55
- ``(tree, errors)``; ``tree`` may be ``None`` if nothing parsed.
56
- When False (default), the function raises :class:`SysMLSyntaxError`
57
- on the first syntax error, preserving the historical strict behavior.
69
+ When True, ANTLR's error recovery is used and ``(tree, errors)``
70
+ is returned instead of raising. ``tree`` may be ``None`` if
71
+ nothing parsed. When False (default), the function raises
72
+ :class:`SysMLSyntaxError` on syntax errors, preserving the
73
+ historical strict behavior.
74
+ prediction_mode : {"sll", "ll"}, default "sll"
75
+ ``"sll"`` (default) runs the fast SLL pass with LL fallback.
76
+ ``"ll"`` forces the full-context pass directly (slower; useful
77
+ for debugging). ``"sll"`` without fallback can be forced with
78
+ ``prediction_mode="sll_only"``.
58
79
 
59
80
  Returns
60
81
  -------
@@ -64,7 +85,8 @@ def parse(source, library=None, recover=False):
64
85
  Raises
65
86
  ------
66
87
  SysMLSyntaxError
67
- If ``recover`` is False and the source contains syntax errors.
88
+ If ``recover`` is False and the source contains syntax errors
89
+ (after both parse stages).
68
90
  """
69
91
  from pathlib import Path
70
92
 
@@ -73,33 +95,46 @@ def parse(source, library=None, recover=False):
73
95
  content = source.read()
74
96
  else:
75
97
  content = source
76
-
77
- # Library parameter — previously prepended library files to the parse input.
78
- # Bundled library files use KerML syntax (datatype, classifier, standard library package)
79
- # which the SysML v2 ANTLR grammar cannot parse. Library symbol resolution is handled
80
- # by LibrarySymbolIndex (regex scanning) during semantic analysis (analyze()).
81
- # The library parameter is accepted for API compatibility but the content is not prepended.
82
-
83
- # Create input stream
84
- input_stream = InputStream(content)
85
-
86
- # Create lexer
87
- lexer = SysMLv2Lexer(input_stream)
88
-
89
- # Set up error listener
98
+
99
+ force_ll = prediction_mode == "ll"
100
+ sll_only = prediction_mode == "sll_only" or force_ll
101
+
102
+ if not force_ll:
103
+ # ── Stage 1: SLL fast-path ────────────────────────────────────
104
+ lexer, token_stream, parser = _make_parser(content)
105
+ error_listener = ANTLRErrorListener()
106
+ lexer.addErrorListener(error_listener)
107
+ parser.addErrorListener(error_listener)
108
+ parser._interp.predictionMode = PredictionMode.SLL
109
+ # Bail-out strategy: abort as soon as an SLL conflict is found —
110
+ # any error means we redo the whole parse in LL mode.
111
+ from antlr4.error.ErrorStrategy import BailErrorStrategy
112
+ tree = None
113
+ sll_errors = []
114
+ try:
115
+ parser._errHandler = BailErrorStrategy()
116
+ tree = parser.rootNamespace()
117
+ except Exception:
118
+ tree = None
119
+ sll_errors = ["<sll>"]
120
+ if tree is not None and not error_listener.errors:
121
+ # ── fast path succeeded ──────────────────────────────────
122
+ if recover:
123
+ return tree, []
124
+ return tree
125
+ if sll_only:
126
+ # Caller forced SLL-only; fall through with the collected
127
+ # errors but re-run without bail to build the partial tree.
128
+ pass
129
+
130
+ # ── Stage 2: full LL parse (fallback) ─────────────────────────────
131
+ lexer, token_stream, parser = _make_parser(content)
90
132
  error_listener = ANTLRErrorListener()
91
- lexer.removeErrorListeners()
92
133
  lexer.addErrorListener(error_listener)
93
-
94
- # Create token stream
95
- token_stream = CommonTokenStream(lexer)
96
-
97
- # Create parser
98
- parser = SysMLv2Parser(token_stream)
99
- parser.removeErrorListeners()
100
134
  parser.addErrorListener(error_listener)
101
-
102
- # Parse the source - use rootNamespace to support multiple top-level packages
135
+ from antlr4.error.ErrorStrategy import DefaultErrorStrategy
136
+ parser._errHandler = DefaultErrorStrategy()
137
+ parser._interp.predictionMode = PredictionMode.LL
103
138
  tree = parser.rootNamespace()
104
139
 
105
140
  # Check for errors
@@ -170,28 +170,46 @@ class Model(Searchable):
170
170
  def _ensure_body(self):
171
171
  """Rebuild the grammar body from current children and imports.
172
172
 
173
- Serializes all child elements into PackageMember wrappers and
174
- preserves any existing Import or AliasMember entries. Updates
175
- self.grammar with the reconstructed RootNamespace.
173
+ Serializes all child elements into PackageMember wrappers while
174
+ preserving ``Import`` and ``AliasMember`` declarations **at their
175
+ original source positions** (v0.58.0). The existing grammar body
176
+ is used as the ordering template: each ``PackageMember`` slot is
177
+ refilled with the fresh serialization of the next child, and any
178
+ children beyond the recorded slots are appended at the end.
176
179
 
177
180
  Returns
178
181
  -------
179
182
  Model
180
183
  Self for chaining.
181
184
  """
182
- body = []
185
+ # Serialize all current children into PackageMember definitions
186
+ child_defs = []
183
187
  for abc in self.children:
184
188
  v = abc._get_definition(child="PackageBody")
185
189
  if isinstance(v, list):
186
190
  for subchild in v:
187
- body.append(PackageMember(subchild).get_definition())
191
+ child_defs.append(PackageMember(subchild).get_definition())
188
192
  else:
189
- body.append(PackageMember(v).get_definition())
193
+ child_defs.append(PackageMember(v).get_definition())
190
194
 
191
- if hasattr(self, 'grammar') and self.grammar:
192
- for child in self.grammar.children:
193
- if child.__class__.__name__ in ('Import', 'AliasMember'):
194
- body.append(child.get_definition())
195
+ existing = getattr(getattr(self, 'grammar', None), 'children', None)
196
+
197
+ body = []
198
+ if existing:
199
+ member_iter = iter(child_defs)
200
+ for item in existing:
201
+ cn = item.__class__.__name__
202
+ if cn in ('Import', 'AliasMember'):
203
+ body.append(item.get_definition())
204
+ elif cn == 'PackageMember':
205
+ try:
206
+ body.append(next(member_iter))
207
+ except StopIteration:
208
+ break # children shrank; drop stale members
209
+ # children added programmatically beyond recorded slots
210
+ body.extend(member_iter)
211
+ else:
212
+ body = child_defs
195
213
 
196
214
  if len(body) > 0:
197
215
  self.grammar = RootNamespace(
@@ -567,22 +585,43 @@ class Package(Searchable):
567
585
  def _ensure_body(self):
568
586
  """Rebuild the package grammar body from current children and imports.
569
587
 
570
- Serializes all child elements into PackageMember wrappers and
571
- preserves any existing Import or AliasMember entries.
588
+ Serializes all child elements into PackageMember wrappers while
589
+ preserving ``Import`` and ``AliasMember`` declarations **at their
590
+ original source positions** (v0.58.0). The existing grammar body
591
+ is used as the ordering template: each ``PackageMember`` slot is
592
+ refilled with the fresh serialization of the next child, and any
593
+ children beyond the recorded slots are appended at the end.
572
594
  """
573
- body = []
595
+ # Serialize all current children into PackageMember definitions
596
+ child_defs = []
574
597
  for abc in self.children:
575
598
  v = abc._get_definition(child="PackageBody")
576
599
  if isinstance(v, list):
577
600
  for subchild in v:
578
- body.append(PackageMember(subchild).get_definition())
601
+ child_defs.append(PackageMember(subchild).get_definition())
579
602
  else:
580
- body.append(PackageMember(v).get_definition())
603
+ child_defs.append(PackageMember(v).get_definition())
581
604
 
582
- if hasattr(self.grammar, 'body') and self.grammar.body:
583
- for child in self.grammar.body.children:
584
- if child.__class__.__name__ in ('Import', 'AliasMember'):
585
- body.append(child.get_definition())
605
+ body_children = getattr(
606
+ getattr(getattr(self, 'grammar', None), 'body', None), 'children', None
607
+ )
608
+
609
+ body = []
610
+ if body_children:
611
+ member_iter = iter(child_defs)
612
+ for item in body_children:
613
+ cn = item.__class__.__name__
614
+ if cn in ('Import', 'AliasMember'):
615
+ body.append(item.get_definition())
616
+ elif cn == 'PackageMember':
617
+ try:
618
+ body.append(next(member_iter))
619
+ except StopIteration:
620
+ break # children shrank; drop stale members
621
+ # children added programmatically beyond recorded slots
622
+ body.extend(member_iter)
623
+ else:
624
+ body = child_defs
586
625
 
587
626
  if len(body) > 0:
588
627
  self.grammar.body = PackageBody(
@@ -107,8 +107,9 @@ print(f"\nColors as list: {colors}")
107
107
  # 5. Type Resolution
108
108
  # =============================================================================
109
109
 
110
- # The type is stored in the grammar structure.
111
- # typedby may not be populated during load_from_grammar (known issue).
110
+ # The declared type name is preserved on load (v0.57.0): use
111
+ # attr.typed_by_name. The resolved definition object (`typedby`) still
112
+ # requires programmatic wiring via set_typed_by().
112
113
 
113
114
  model = loads('''
114
115
  package Example {
@@ -122,15 +123,16 @@ package Example {
122
123
  vehicle = model.children[0].children[0]
123
124
 
124
125
  for attr in vehicle.children:
125
- # Access grammar to see type info
126
- print(f"{attr.name}: grammar type = {type(attr.grammar).__name__}")
126
+ # typed_by_name carries the declared type name since v0.57.0
127
+ print(f"{attr.name}: declared type = {attr.typed_by_name}")
127
128
 
128
129
  # =============================================================================
129
130
  # 6. Attribute Multiplicity
130
131
  # =============================================================================
131
132
 
132
133
  # Attributes can have multiplicity like Real[3] for arrays
133
- # The type information is stored in the grammar but not yet exposed via Python API
134
+ # Note: top-level (package-level) attribute multiplicity is still a known
135
+ # visitor gap; multiplicity on attributes inside definitions is preserved.
134
136
 
135
137
  model = loads('''
136
138
  package Example {
@@ -97,7 +97,9 @@ print(f"\nAfter setting: {attr.get_value()}")
97
97
  # 5. Checking Type
98
98
  # =============================================================================
99
99
 
100
- # NOTE: typedby is populated during load_from_grammar but may need verification
100
+ # NOTE (v0.57.0): the declared type name is preserved on load via
101
+ # `typed_by_name`; the resolved definition object (`typedby`) requires
102
+ # programmatic wiring via set_typed_by().
101
103
 
102
104
  model = loads('''
103
105
  package Example {
@@ -111,11 +113,11 @@ package Example {
111
113
  vehicle = model.children[0].children[0]
112
114
 
113
115
  for attr in vehicle.children:
114
- if attr.typedby:
115
- print(f"{attr.name} : {attr.typedby.name}")
116
+ if attr.typed_by_name:
117
+ print(f"{attr.name} : {attr.typed_by_name}")
116
118
  else:
117
119
  # Access type from grammar structure
118
- print(f"{attr.name} : (type in grammar, typedby not populated)")
120
+ print(f"{attr.name} : (type in grammar, typing not declared)")
119
121
 
120
122
  # =============================================================================
121
123
  # 6. Nested Parts
@@ -833,6 +833,153 @@ class NetworkXStore(Store):
833
833
  "avg_degree": sum(dict(self._graph.degree()).values()) / max(self._graph.number_of_nodes(), 1),
834
834
  }
835
835
 
836
+ # ── Phase D query extensions (v0.56.0) ──────────────────────────────
837
+
838
+ def all_paths(self, source_id: str, target_id: str,
839
+ rel_type: Optional[str] = None,
840
+ max_paths: int = 20) -> list[list[str]]:
841
+ """Find up to *max_paths* simple paths between two elements.
842
+
843
+ Useful for impact analysis: every route a flow/connection can
844
+ take between two parts.
845
+
846
+ Parameters
847
+ ----------
848
+ source_id, target_id : str
849
+ Endpoint node IDs.
850
+ rel_type : str, optional
851
+ Restrict traversal to edges of this relationship type.
852
+ max_paths : int
853
+ Safety cap on returned paths (default 20 — path enumeration
854
+ is exponential in dense graphs).
855
+
856
+ Returns
857
+ -------
858
+ list[list[str]]
859
+ Each path is a list of node IDs from source to target.
860
+ Empty list when either endpoint is missing or unconnected.
861
+ """
862
+ if not self._graph.has_node(source_id) or not self._graph.has_node(target_id):
863
+ return []
864
+ graph = self._filtered_graph(rel_type)
865
+ try:
866
+ count = 0
867
+ paths = []
868
+ for p in self._nx.all_simple_paths(graph, source_id, target_id):
869
+ paths.append(list(p))
870
+ count += 1
871
+ if count >= max_paths:
872
+ break
873
+ return paths
874
+ except self._nx.NetworkXNoPath:
875
+ return []
876
+ except self._nx.NodeNotFound:
877
+ return []
878
+
879
+ def in_degree_centrality(self, rel_type: Optional[str] = None) -> dict[str, float]:
880
+ """Compute in-degree centrality (how many elements reference *me*).
881
+
882
+ High in-degree = hub element that many others depend on.
883
+
884
+ Returns
885
+ -------
886
+ dict[str, float]
887
+ Mapping of node ID to in-degree fraction (0.0–1.0).
888
+ """
889
+ graph = self._filtered_graph(rel_type)
890
+ return self._nx.in_degree_centrality(graph)
891
+
892
+ def out_degree_centrality(self, rel_type: Optional[str] = None) -> dict[str, float]:
893
+ """Compute out-degree centrality (how many elements *I* reference)."""
894
+ graph = self._filtered_graph(rel_type)
895
+ return self._nx.out_degree_centrality(graph)
896
+
897
+ def descendants_depth_limited(self, root_id: str, max_depth: int = 3,
898
+ rel_type: str = REL_PARENT_CHILD) -> list[str]:
899
+ """Return descendants within *max_depth* levels of *root_id*.
900
+
901
+ Useful for "direct children of this hierarchy level" queries.
902
+ """
903
+ if not self._graph.has_node(root_id):
904
+ return []
905
+ result: list[str] = []
906
+ frontier = {root_id}
907
+ visited = {root_id}
908
+ for _ in range(max_depth):
909
+ nxt: set[str] = set()
910
+ for node in frontier:
911
+ for succ in self._graph.successors(node):
912
+ edge_ok = any(
913
+ self._graph.edges[node, succ, k].get("rel_type") == rel_type
914
+ for k in self._graph[node][succ]
915
+ ) if self._graph.has_edge(node, succ) else False
916
+ if edge_ok and succ not in visited:
917
+ nxt.add(succ)
918
+ visited |= nxt
919
+ result.extend(sorted(nxt))
920
+ frontier = nxt
921
+ if not frontier:
922
+ break
923
+ return result
924
+
925
+ def neighborhood(self, element_id: str, radius: int = 2,
926
+ rel_type: Optional[str] = None) -> set[str]:
927
+ """Return the ego-neighborhood of *element_id* within *radius* hops
928
+ (following edges in both directions)."""
929
+ if not self._graph.has_node(element_id):
930
+ return set()
931
+ graph = self._filtered_graph(rel_type)
932
+ return set(self._nx.ego_graph(graph, element_id, radius=radius, undirected=True).nodes())
933
+
934
+ def _filtered_graph(self, rel_type: Optional[str]):
935
+ """Return a view restricted to *rel_type* edges, or the full graph."""
936
+ if not rel_type:
937
+ return self._graph
938
+ return self._nx.subgraph_view(
939
+ self._graph,
940
+ edge_filter=lambda e: self._graph.edges[e].get("rel_type") == rel_type,
941
+ )
942
+
943
+ def impact_analysis(self, element_id: str, rel_types: Optional[list[str]] = None,
944
+ direction: str = "downstream") -> set[str]:
945
+ """Elements transitively affected by changes to *element_id*.
946
+
947
+ Follows reverse structural edges (anything that inherits from,
948
+ types by, or flows from *element_id* depends on it).
949
+
950
+ Parameters
951
+ ----------
952
+ element_id : str
953
+ Changed element.
954
+ rel_types : list[str], optional
955
+ Relationship types to consider (default: all).
956
+ direction : {"downstream", "upstream"}
957
+ ``downstream`` follows edges outward (what I affect);
958
+ ``upstream`` follows edges inward (what affects me).
959
+ """
960
+ if not self._graph.has_node(element_id):
961
+ return set()
962
+ if rel_types:
963
+ graph = self._nx.MultiDiGraph()
964
+ for u, v, k, d in self._graph.edges(keys=True):
965
+ if self._graph.edges[u, v, k].get("rel_type") in rel_types:
966
+ graph.add_edge(u, v, rel_type=self._graph.edges[u, v, k].get("rel_type"))
967
+ for n in self._graph.nodes:
968
+ graph.add_node(n, **self._graph.nodes[n])
969
+ else:
970
+ graph = self._graph
971
+ successors = graph.successors if direction == "downstream" else graph.predecessors
972
+ seen = {element_id}
973
+ frontier = [element_id]
974
+ while frontier:
975
+ current = frontier.pop()
976
+ for nxt in successors(current):
977
+ if nxt not in seen:
978
+ seen.add(nxt)
979
+ frontier.append(nxt)
980
+ seen.discard(element_id)
981
+ return seen
982
+
836
983
 
837
984
  # ── Kuzu Store ──────────────────────────────────────────────────────────────
838
985
 
@@ -1400,6 +1547,127 @@ class KuzuStore(Store):
1400
1547
  "avg_degree": (2 * e) / n if n > 0 else 0,
1401
1548
  }
1402
1549
 
1550
+ # ── Phase D query extensions (v0.56.0) ──────────────────────────────
1551
+
1552
+ def execute_cypher(self, query: str) -> list[dict]:
1553
+ """Run a raw Cypher query and return rows as dicts.
1554
+
1555
+ Escape user data before interpolating into the query string
1556
+ (``_escape``) or better, keep property matches static as in the
1557
+ built-in methods.
1558
+
1559
+ Parameters
1560
+ ----------
1561
+ query : str
1562
+ A Cypher query against the Element/Relationship schema.
1563
+
1564
+ Returns
1565
+ -------
1566
+ list[dict]
1567
+ One dict per row, keyed by the RETURN column names.
1568
+ """
1569
+ result = self._conn.execute(query)
1570
+ names = result.get_column_names()
1571
+ rows = []
1572
+ while result.has_next():
1573
+ raw = result.get_next()
1574
+ row = {}
1575
+ for i, name in enumerate(names):
1576
+ value = raw[i]
1577
+ # Node/table objects: extract id + common properties
1578
+ if hasattr(value, "get"):
1579
+ try:
1580
+ value = {
1581
+ "id": value["id"],
1582
+ "name": value["name"],
1583
+ "sysml_type": value["sysml_type"],
1584
+ }
1585
+ except Exception:
1586
+ pass
1587
+ # lists of node values (variable-length paths)
1588
+ if isinstance(value, list):
1589
+ value = [
1590
+ item["id"] if hasattr(item, "get") else item
1591
+ for item in value
1592
+ ]
1593
+ row[name] = value
1594
+ rows.append(row)
1595
+ return rows
1596
+
1597
+ def shortest_path_between_named(self, source_name: str, target_name: str,
1598
+ max_hops: int = 10) -> Optional[list[str]]:
1599
+ """Shortest structural path between two named elements via Cypher.
1600
+
1601
+ Convenience wrapper over :meth:`execute_cypher` for the common
1602
+ "how is A connected to B" question.
1603
+
1604
+ Notes
1605
+ -----
1606
+ Kùzu (as of 0.4.x) has no ``shortestPath()`` function; this method
1607
+ enumerates variable-length paths ordered by length and returns
1608
+ the shortest.
1609
+ """
1610
+ sid = self._escape(source_name)
1611
+ tid = self._escape(target_name)
1612
+ # Ordered by hop count: expand one hop length at a time until found
1613
+ for hops in range(1, max_hops + 1):
1614
+ try:
1615
+ rows = self.execute_cypher(
1616
+ f'MATCH p = (s:Element {{name: "{sid}"}})-[:Relationship*{hops}..{hops}]->(t:Element {{name: "{tid}"}}) '
1617
+ f'RETURN nodes(p) AS ns '
1618
+ )
1619
+ except Exception:
1620
+ return None
1621
+ if rows:
1622
+ return list(rows[0].get("ns", []))
1623
+ return None
1624
+
1625
+ def siblings(self, element_id: str) -> list[str]:
1626
+ """Elements sharing a parent with *element_id* (structural siblings)."""
1627
+ eid = self._escape(element_id)
1628
+ rows = self.execute_cypher(
1629
+ f'MATCH (parent:Element)-[:Relationship {{rel_type: "parent_child"}}]->(me:Element {{id: "{eid}"}}) '
1630
+ f'MATCH (parent)-[:Relationship {{rel_type: "parent_child"}}]->(sib:Element) '
1631
+ f'WHERE sib.id <> me.id '
1632
+ f'RETURN DISTINCT sib.id AS sibling'
1633
+ )
1634
+ return [r["sibling"] for r in rows]
1635
+
1636
+ def hub_elements(self, min_degree: int = 3,
1637
+ direction: str = "outgoing") -> list[tuple[str, int]]:
1638
+ """Elements with high reference degree (many relationships).
1639
+
1640
+ Parameters
1641
+ ----------
1642
+ min_degree : int
1643
+ Minimum degree to qualify as a hub (default 3).
1644
+ direction : {"outgoing", "incoming", "both"}
1645
+ Which edge direction to count. ``outgoing`` finds elements
1646
+ that own/contain many others (structural hubs); ``incoming``
1647
+ finds heavily-referenced (dependency sinks). Default
1648
+ ``outgoing``.
1649
+
1650
+ Returns
1651
+ -------
1652
+ list[tuple[str, int]]
1653
+ (element_id, degree) pairs with degree ≥ *min_degree*,
1654
+ sorted by degree descending.
1655
+ """
1656
+ min_degree = int(min_degree)
1657
+ if direction == "outgoing":
1658
+ pattern = "MATCH (hub:Element)-[r:Relationship]->() WITH hub, count(r) AS deg"
1659
+ elif direction == "incoming":
1660
+ pattern = "MATCH ()-[r:Relationship]->(hub:Element) WITH hub, count(r) AS deg"
1661
+ else:
1662
+ pattern = (
1663
+ "MATCH (hub:Element)-[r:Relationship]-() WITH hub, count(r) AS deg"
1664
+ )
1665
+ rows = self.execute_cypher(
1666
+ f'{pattern} WHERE deg >= {min_degree} '
1667
+ f'RETURN hub.id AS element_id, deg ORDER BY deg DESC'
1668
+ )
1669
+ return [(r["element_id"], r["deg"]) for r in rows]
1670
+
1403
1671
 
1404
1672
  # ── Cayley Store ──────────────────────────────────────────────────────────────
1405
1673