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.
- {sysmlpy-0.55.0 → sysmlpy-0.58.0}/PKG-INFO +6 -4
- {sysmlpy-0.55.0 → sysmlpy-0.58.0}/README.md +5 -3
- {sysmlpy-0.55.0 → sysmlpy-0.58.0}/pyproject.toml +1 -1
- {sysmlpy-0.55.0 → sysmlpy-0.58.0}/src/sysmlpy/__init__.py +1 -1
- {sysmlpy-0.55.0 → sysmlpy-0.58.0}/src/sysmlpy/antlr_parser.py +67 -32
- {sysmlpy-0.55.0 → sysmlpy-0.58.0}/src/sysmlpy/definition.py +58 -19
- {sysmlpy-0.55.0 → sysmlpy-0.58.0}/src/sysmlpy/examples/attribute_values.py +7 -5
- {sysmlpy-0.55.0 → sysmlpy-0.58.0}/src/sysmlpy/examples/part_attributes.py +6 -4
- {sysmlpy-0.55.0 → sysmlpy-0.58.0}/src/sysmlpy/store.py +268 -0
- {sysmlpy-0.55.0 → sysmlpy-0.58.0}/src/sysmlpy/usage.py +134 -71
- {sysmlpy-0.55.0 → sysmlpy-0.58.0}/LICENSE +0 -0
- {sysmlpy-0.55.0 → sysmlpy-0.58.0}/src/sysmlpy/__main__.py +0 -0
- {sysmlpy-0.55.0 → sysmlpy-0.58.0}/src/sysmlpy/antlr/README.md +0 -0
- {sysmlpy-0.55.0 → sysmlpy-0.58.0}/src/sysmlpy/antlr/SysMLv2Lexer.interp +0 -0
- {sysmlpy-0.55.0 → sysmlpy-0.58.0}/src/sysmlpy/antlr/SysMLv2Lexer.py +0 -0
- {sysmlpy-0.55.0 → sysmlpy-0.58.0}/src/sysmlpy/antlr/SysMLv2Lexer.tokens +0 -0
- {sysmlpy-0.55.0 → sysmlpy-0.58.0}/src/sysmlpy/antlr/SysMLv2Parser.interp +0 -0
- {sysmlpy-0.55.0 → sysmlpy-0.58.0}/src/sysmlpy/antlr/SysMLv2Parser.py +0 -0
- {sysmlpy-0.55.0 → sysmlpy-0.58.0}/src/sysmlpy/antlr/SysMLv2Parser.tokens +0 -0
- {sysmlpy-0.55.0 → sysmlpy-0.58.0}/src/sysmlpy/antlr/SysMLv2ParserListener.py +0 -0
- {sysmlpy-0.55.0 → sysmlpy-0.58.0}/src/sysmlpy/antlr/SysMLv2ParserVisitor.py +0 -0
- {sysmlpy-0.55.0 → sysmlpy-0.58.0}/src/sysmlpy/antlr/src/sysmlpy/grammar/antlr4/SysMLv2Lexer.interp +0 -0
- {sysmlpy-0.55.0 → sysmlpy-0.58.0}/src/sysmlpy/antlr/src/sysmlpy/grammar/antlr4/SysMLv2Lexer.py +0 -0
- {sysmlpy-0.55.0 → sysmlpy-0.58.0}/src/sysmlpy/antlr/src/sysmlpy/grammar/antlr4/SysMLv2Lexer.tokens +0 -0
- {sysmlpy-0.55.0 → sysmlpy-0.58.0}/src/sysmlpy/antlr_visitor.py +0 -0
- {sysmlpy-0.55.0 → sysmlpy-0.58.0}/src/sysmlpy/boxes_view.py +0 -0
- {sysmlpy-0.55.0 → sysmlpy-0.58.0}/src/sysmlpy/examples/tuples_sequences.py +0 -0
- {sysmlpy-0.55.0 → sysmlpy-0.58.0}/src/sysmlpy/formatting.py +0 -0
- {sysmlpy-0.55.0 → sysmlpy-0.58.0}/src/sysmlpy/grammar/antlr4/README.md +0 -0
- {sysmlpy-0.55.0 → sysmlpy-0.58.0}/src/sysmlpy/grammar/antlr4/SysMLv2Lexer.g4 +0 -0
- {sysmlpy-0.55.0 → sysmlpy-0.58.0}/src/sysmlpy/grammar/antlr4/SysMLv2Lexer.interp +0 -0
- {sysmlpy-0.55.0 → sysmlpy-0.58.0}/src/sysmlpy/grammar/antlr4/SysMLv2Lexer.py +0 -0
- {sysmlpy-0.55.0 → sysmlpy-0.58.0}/src/sysmlpy/grammar/antlr4/SysMLv2Lexer.tokens +0 -0
- {sysmlpy-0.55.0 → sysmlpy-0.58.0}/src/sysmlpy/grammar/antlr4/SysMLv2Parser.g4 +0 -0
- {sysmlpy-0.55.0 → sysmlpy-0.58.0}/src/sysmlpy/grammar/antlr4/SysMLv2Parser.interp +0 -0
- {sysmlpy-0.55.0 → sysmlpy-0.58.0}/src/sysmlpy/grammar/antlr4/SysMLv2Parser.py +0 -0
- {sysmlpy-0.55.0 → sysmlpy-0.58.0}/src/sysmlpy/grammar/antlr4/SysMLv2Parser.tokens +0 -0
- {sysmlpy-0.55.0 → sysmlpy-0.58.0}/src/sysmlpy/grammar/antlr4/SysMLv2ParserListener.py +0 -0
- {sysmlpy-0.55.0 → sysmlpy-0.58.0}/src/sysmlpy/grammar/antlr4/SysMLv2ParserVisitor.py +0 -0
- {sysmlpy-0.55.0 → sysmlpy-0.58.0}/src/sysmlpy/grammar/antlr4/desc.xml +0 -0
- {sysmlpy-0.55.0 → sysmlpy-0.58.0}/src/sysmlpy/grammar/antlr4/examples/camera.sysml +0 -0
- {sysmlpy-0.55.0 → sysmlpy-0.58.0}/src/sysmlpy/grammar/antlr4/examples/toaster-system.sysml +0 -0
- {sysmlpy-0.55.0 → sysmlpy-0.58.0}/src/sysmlpy/grammar/antlr4/examples/vehicle-model.sysml +0 -0
- {sysmlpy-0.55.0 → sysmlpy-0.58.0}/src/sysmlpy/grammar/antlr4/pom.xml +0 -0
- {sysmlpy-0.55.0 → sysmlpy-0.58.0}/src/sysmlpy/grammar/classes.py +0 -0
- {sysmlpy-0.55.0 → sysmlpy-0.58.0}/src/sysmlpy/library/LICENSE +0 -0
- {sysmlpy-0.55.0 → sysmlpy-0.58.0}/src/sysmlpy/library/README.md +0 -0
- {sysmlpy-0.55.0 → sysmlpy-0.58.0}/src/sysmlpy/library/__init__.py +0 -0
- {sysmlpy-0.55.0 → sysmlpy-0.58.0}/src/sysmlpy/library/domain/Analysis/AnalysisTooling.sysml +0 -0
- {sysmlpy-0.55.0 → sysmlpy-0.58.0}/src/sysmlpy/library/domain/Analysis/SampledFunctions.sysml +0 -0
- {sysmlpy-0.55.0 → sysmlpy-0.58.0}/src/sysmlpy/library/domain/Analysis/StateSpaceRepresentation.sysml +0 -0
- {sysmlpy-0.55.0 → sysmlpy-0.58.0}/src/sysmlpy/library/domain/Analysis/TradeStudies.sysml +0 -0
- {sysmlpy-0.55.0 → sysmlpy-0.58.0}/src/sysmlpy/library/domain/Cause and Effect/CausationConnections.sysml +0 -0
- {sysmlpy-0.55.0 → sysmlpy-0.58.0}/src/sysmlpy/library/domain/Cause and Effect/CauseAndEffect.sysml +0 -0
- {sysmlpy-0.55.0 → sysmlpy-0.58.0}/src/sysmlpy/library/domain/Geometry/ShapeItems.sysml +0 -0
- {sysmlpy-0.55.0 → sysmlpy-0.58.0}/src/sysmlpy/library/domain/Geometry/SpatialItems.sysml +0 -0
- {sysmlpy-0.55.0 → sysmlpy-0.58.0}/src/sysmlpy/library/domain/Metadata/ImageMetadata.sysml +0 -0
- {sysmlpy-0.55.0 → sysmlpy-0.58.0}/src/sysmlpy/library/domain/Metadata/ModelingMetadata.sysml +0 -0
- {sysmlpy-0.55.0 → sysmlpy-0.58.0}/src/sysmlpy/library/domain/Metadata/ParametersOfInterestMetadata.sysml +0 -0
- {sysmlpy-0.55.0 → sysmlpy-0.58.0}/src/sysmlpy/library/domain/Metadata/RiskMetadata.sysml +0 -0
- {sysmlpy-0.55.0 → sysmlpy-0.58.0}/src/sysmlpy/library/domain/Quantities and Units/ISQ.sysml +0 -0
- {sysmlpy-0.55.0 → sysmlpy-0.58.0}/src/sysmlpy/library/domain/Quantities and Units/ISQAcoustics.sysml +0 -0
- {sysmlpy-0.55.0 → sysmlpy-0.58.0}/src/sysmlpy/library/domain/Quantities and Units/ISQAtomicNuclear.sysml +0 -0
- {sysmlpy-0.55.0 → sysmlpy-0.58.0}/src/sysmlpy/library/domain/Quantities and Units/ISQBase.sysml +0 -0
- {sysmlpy-0.55.0 → sysmlpy-0.58.0}/src/sysmlpy/library/domain/Quantities and Units/ISQCharacteristicNumbers.sysml +0 -0
- {sysmlpy-0.55.0 → sysmlpy-0.58.0}/src/sysmlpy/library/domain/Quantities and Units/ISQChemistryMolecular.sysml +0 -0
- {sysmlpy-0.55.0 → sysmlpy-0.58.0}/src/sysmlpy/library/domain/Quantities and Units/ISQCondensedMatter.sysml +0 -0
- {sysmlpy-0.55.0 → sysmlpy-0.58.0}/src/sysmlpy/library/domain/Quantities and Units/ISQElectromagnetism.sysml +0 -0
- {sysmlpy-0.55.0 → sysmlpy-0.58.0}/src/sysmlpy/library/domain/Quantities and Units/ISQInformation.sysml +0 -0
- {sysmlpy-0.55.0 → sysmlpy-0.58.0}/src/sysmlpy/library/domain/Quantities and Units/ISQLight.sysml +0 -0
- {sysmlpy-0.55.0 → sysmlpy-0.58.0}/src/sysmlpy/library/domain/Quantities and Units/ISQMechanics.sysml +0 -0
- {sysmlpy-0.55.0 → sysmlpy-0.58.0}/src/sysmlpy/library/domain/Quantities and Units/ISQSpaceTime.sysml +0 -0
- {sysmlpy-0.55.0 → sysmlpy-0.58.0}/src/sysmlpy/library/domain/Quantities and Units/ISQThermodynamics.sysml +0 -0
- {sysmlpy-0.55.0 → sysmlpy-0.58.0}/src/sysmlpy/library/domain/Quantities and Units/MeasurementRefCalculations.sysml +0 -0
- {sysmlpy-0.55.0 → sysmlpy-0.58.0}/src/sysmlpy/library/domain/Quantities and Units/MeasurementReferences.sysml +0 -0
- {sysmlpy-0.55.0 → sysmlpy-0.58.0}/src/sysmlpy/library/domain/Quantities and Units/Quantities.sysml +0 -0
- {sysmlpy-0.55.0 → sysmlpy-0.58.0}/src/sysmlpy/library/domain/Quantities and Units/QuantityCalculations.sysml +0 -0
- {sysmlpy-0.55.0 → sysmlpy-0.58.0}/src/sysmlpy/library/domain/Quantities and Units/SI.sysml +0 -0
- {sysmlpy-0.55.0 → sysmlpy-0.58.0}/src/sysmlpy/library/domain/Quantities and Units/SIPrefixes.sysml +0 -0
- {sysmlpy-0.55.0 → sysmlpy-0.58.0}/src/sysmlpy/library/domain/Quantities and Units/TensorCalculations.sysml +0 -0
- {sysmlpy-0.55.0 → sysmlpy-0.58.0}/src/sysmlpy/library/domain/Quantities and Units/Time.sysml +0 -0
- {sysmlpy-0.55.0 → sysmlpy-0.58.0}/src/sysmlpy/library/domain/Quantities and Units/USCustomaryUnits.sysml +0 -0
- {sysmlpy-0.55.0 → sysmlpy-0.58.0}/src/sysmlpy/library/domain/Quantities and Units/VectorCalculations.sysml +0 -0
- {sysmlpy-0.55.0 → sysmlpy-0.58.0}/src/sysmlpy/library/domain/Requirement Derivation/DerivationConnections.sysml +0 -0
- {sysmlpy-0.55.0 → sysmlpy-0.58.0}/src/sysmlpy/library/domain/Requirement Derivation/RequirementDerivation.sysml +0 -0
- {sysmlpy-0.55.0 → sysmlpy-0.58.0}/src/sysmlpy/library/kernel/Base.kerml +0 -0
- {sysmlpy-0.55.0 → sysmlpy-0.58.0}/src/sysmlpy/library/kernel/BaseFunctions.kerml +0 -0
- {sysmlpy-0.55.0 → sysmlpy-0.58.0}/src/sysmlpy/library/kernel/BooleanFunctions.kerml +0 -0
- {sysmlpy-0.55.0 → sysmlpy-0.58.0}/src/sysmlpy/library/kernel/Clocks.kerml +0 -0
- {sysmlpy-0.55.0 → sysmlpy-0.58.0}/src/sysmlpy/library/kernel/CollectionFunctions.kerml +0 -0
- {sysmlpy-0.55.0 → sysmlpy-0.58.0}/src/sysmlpy/library/kernel/Collections.kerml +0 -0
- {sysmlpy-0.55.0 → sysmlpy-0.58.0}/src/sysmlpy/library/kernel/ComplexFunctions.kerml +0 -0
- {sysmlpy-0.55.0 → sysmlpy-0.58.0}/src/sysmlpy/library/kernel/ControlFunctions.kerml +0 -0
- {sysmlpy-0.55.0 → sysmlpy-0.58.0}/src/sysmlpy/library/kernel/ControlPerformances.kerml +0 -0
- {sysmlpy-0.55.0 → sysmlpy-0.58.0}/src/sysmlpy/library/kernel/DataFunctions.kerml +0 -0
- {sysmlpy-0.55.0 → sysmlpy-0.58.0}/src/sysmlpy/library/kernel/FeatureReferencingPerformances.kerml +0 -0
- {sysmlpy-0.55.0 → sysmlpy-0.58.0}/src/sysmlpy/library/kernel/IntegerFunctions.kerml +0 -0
- {sysmlpy-0.55.0 → sysmlpy-0.58.0}/src/sysmlpy/library/kernel/KerML.kerml +0 -0
- {sysmlpy-0.55.0 → sysmlpy-0.58.0}/src/sysmlpy/library/kernel/Links.kerml +0 -0
- {sysmlpy-0.55.0 → sysmlpy-0.58.0}/src/sysmlpy/library/kernel/Metaobjects.kerml +0 -0
- {sysmlpy-0.55.0 → sysmlpy-0.58.0}/src/sysmlpy/library/kernel/NaturalFunctions.kerml +0 -0
- {sysmlpy-0.55.0 → sysmlpy-0.58.0}/src/sysmlpy/library/kernel/NumericalFunctions.kerml +0 -0
- {sysmlpy-0.55.0 → sysmlpy-0.58.0}/src/sysmlpy/library/kernel/Objects.kerml +0 -0
- {sysmlpy-0.55.0 → sysmlpy-0.58.0}/src/sysmlpy/library/kernel/Observation.kerml +0 -0
- {sysmlpy-0.55.0 → sysmlpy-0.58.0}/src/sysmlpy/library/kernel/OccurrenceFunctions.kerml +0 -0
- {sysmlpy-0.55.0 → sysmlpy-0.58.0}/src/sysmlpy/library/kernel/Occurrences.kerml +0 -0
- {sysmlpy-0.55.0 → sysmlpy-0.58.0}/src/sysmlpy/library/kernel/Performances.kerml +0 -0
- {sysmlpy-0.55.0 → sysmlpy-0.58.0}/src/sysmlpy/library/kernel/RationalFunctions.kerml +0 -0
- {sysmlpy-0.55.0 → sysmlpy-0.58.0}/src/sysmlpy/library/kernel/RealFunctions.kerml +0 -0
- {sysmlpy-0.55.0 → sysmlpy-0.58.0}/src/sysmlpy/library/kernel/ScalarFunctions.kerml +0 -0
- {sysmlpy-0.55.0 → sysmlpy-0.58.0}/src/sysmlpy/library/kernel/ScalarValues.kerml +0 -0
- {sysmlpy-0.55.0 → sysmlpy-0.58.0}/src/sysmlpy/library/kernel/SequenceFunctions.kerml +0 -0
- {sysmlpy-0.55.0 → sysmlpy-0.58.0}/src/sysmlpy/library/kernel/SpatialFrames.kerml +0 -0
- {sysmlpy-0.55.0 → sysmlpy-0.58.0}/src/sysmlpy/library/kernel/StatePerformances.kerml +0 -0
- {sysmlpy-0.55.0 → sysmlpy-0.58.0}/src/sysmlpy/library/kernel/StringFunctions.kerml +0 -0
- {sysmlpy-0.55.0 → sysmlpy-0.58.0}/src/sysmlpy/library/kernel/Transfers.kerml +0 -0
- {sysmlpy-0.55.0 → sysmlpy-0.58.0}/src/sysmlpy/library/kernel/TransitionPerformances.kerml +0 -0
- {sysmlpy-0.55.0 → sysmlpy-0.58.0}/src/sysmlpy/library/kernel/TrigFunctions.kerml +0 -0
- {sysmlpy-0.55.0 → sysmlpy-0.58.0}/src/sysmlpy/library/kernel/Triggers.kerml +0 -0
- {sysmlpy-0.55.0 → sysmlpy-0.58.0}/src/sysmlpy/library/kernel/VectorFunctions.kerml +0 -0
- {sysmlpy-0.55.0 → sysmlpy-0.58.0}/src/sysmlpy/library/kernel/VectorValues.kerml +0 -0
- {sysmlpy-0.55.0 → sysmlpy-0.58.0}/src/sysmlpy/library/systems/Actions.sysml +0 -0
- {sysmlpy-0.55.0 → sysmlpy-0.58.0}/src/sysmlpy/library/systems/Allocations.sysml +0 -0
- {sysmlpy-0.55.0 → sysmlpy-0.58.0}/src/sysmlpy/library/systems/AnalysisCases.sysml +0 -0
- {sysmlpy-0.55.0 → sysmlpy-0.58.0}/src/sysmlpy/library/systems/Attributes.sysml +0 -0
- {sysmlpy-0.55.0 → sysmlpy-0.58.0}/src/sysmlpy/library/systems/Calculations.sysml +0 -0
- {sysmlpy-0.55.0 → sysmlpy-0.58.0}/src/sysmlpy/library/systems/Cases.sysml +0 -0
- {sysmlpy-0.55.0 → sysmlpy-0.58.0}/src/sysmlpy/library/systems/Connections.sysml +0 -0
- {sysmlpy-0.55.0 → sysmlpy-0.58.0}/src/sysmlpy/library/systems/Constraints.sysml +0 -0
- {sysmlpy-0.55.0 → sysmlpy-0.58.0}/src/sysmlpy/library/systems/Flows.sysml +0 -0
- {sysmlpy-0.55.0 → sysmlpy-0.58.0}/src/sysmlpy/library/systems/Interfaces.sysml +0 -0
- {sysmlpy-0.55.0 → sysmlpy-0.58.0}/src/sysmlpy/library/systems/Items.sysml +0 -0
- {sysmlpy-0.55.0 → sysmlpy-0.58.0}/src/sysmlpy/library/systems/Metadata.sysml +0 -0
- {sysmlpy-0.55.0 → sysmlpy-0.58.0}/src/sysmlpy/library/systems/Parts.sysml +0 -0
- {sysmlpy-0.55.0 → sysmlpy-0.58.0}/src/sysmlpy/library/systems/Ports.sysml +0 -0
- {sysmlpy-0.55.0 → sysmlpy-0.58.0}/src/sysmlpy/library/systems/Requirements.sysml +0 -0
- {sysmlpy-0.55.0 → sysmlpy-0.58.0}/src/sysmlpy/library/systems/StandardViewDefinitions.sysml +0 -0
- {sysmlpy-0.55.0 → sysmlpy-0.58.0}/src/sysmlpy/library/systems/States.sysml +0 -0
- {sysmlpy-0.55.0 → sysmlpy-0.58.0}/src/sysmlpy/library/systems/SysML.sysml +0 -0
- {sysmlpy-0.55.0 → sysmlpy-0.58.0}/src/sysmlpy/library/systems/UseCases.sysml +0 -0
- {sysmlpy-0.55.0 → sysmlpy-0.58.0}/src/sysmlpy/library/systems/VerificationCases.sysml +0 -0
- {sysmlpy-0.55.0 → sysmlpy-0.58.0}/src/sysmlpy/library/systems/Views.sysml +0 -0
- {sysmlpy-0.55.0 → sysmlpy-0.58.0}/src/sysmlpy/navigate.py +0 -0
- {sysmlpy-0.55.0 → sysmlpy-0.58.0}/src/sysmlpy/plantuml.py +0 -0
- {sysmlpy-0.55.0 → sysmlpy-0.58.0}/src/sysmlpy/project.py +0 -0
- {sysmlpy-0.55.0 → sysmlpy-0.58.0}/src/sysmlpy/semantic.py +0 -0
- {sysmlpy-0.55.0 → sysmlpy-0.58.0}/src/sysmlpy/us_customary_units.txt +0 -0
- {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.
|
|
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()
|
|
48
|
-
>
|
|
49
|
-
>
|
|
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()
|
|
23
|
-
>
|
|
24
|
-
>
|
|
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
|
|
@@ -25,7 +25,7 @@ __all__ = [
|
|
|
25
25
|
"render_state_transition_view_svg", "boxes_view",
|
|
26
26
|
]
|
|
27
27
|
__author__ = "Jon Fox"
|
|
28
|
-
__version__ = "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
|
|
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
|
|
53
|
-
is returned
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
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
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
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
|
-
|
|
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
|
|
174
|
-
|
|
175
|
-
|
|
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
|
-
|
|
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
|
-
|
|
191
|
+
child_defs.append(PackageMember(subchild).get_definition())
|
|
188
192
|
else:
|
|
189
|
-
|
|
193
|
+
child_defs.append(PackageMember(v).get_definition())
|
|
190
194
|
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
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
|
|
571
|
-
|
|
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
|
-
|
|
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
|
-
|
|
601
|
+
child_defs.append(PackageMember(subchild).get_definition())
|
|
579
602
|
else:
|
|
580
|
-
|
|
603
|
+
child_defs.append(PackageMember(v).get_definition())
|
|
581
604
|
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
|
|
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
|
|
111
|
-
#
|
|
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
|
-
#
|
|
126
|
-
print(f"{attr.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
|
-
#
|
|
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:
|
|
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.
|
|
115
|
-
print(f"{attr.name} : {attr.
|
|
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,
|
|
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
|
|