diff --git a/docs/design/sysml2-tools-tool/query.md b/docs/design/sysml2-tools-tool/query.md index 3c71cedc..b2e10c3e 100644 --- a/docs/design/sysml2-tools-tool/query.md +++ b/docs/design/sysml2-tools-tool/query.md @@ -38,7 +38,7 @@ re-resolving anything; `QueryEngine` is a pure read-only consumer of the workspa | `used-by` | yes | `Index.GetIncomingEdges(qn)` | | `dependencies` | yes | Merges `QueryEngine.Uses` + `QueryEngine.UsedBy` (no separate traversal) | | `impact` | yes | Breadth-first transitive closure over `used-by`, cycle-guarded, bounded by `--walk-depth` | -| `describe` | yes | Node's own kind, resolved supertypes, typing, annotations, children | +| `describe` | yes | Node's own kind, resolved supertypes, typing, annotations, applied metadata, children | | `hierarchy` | yes | Recursive `Supertype` edge walk, direction-controlled, cycle-guarded | | `requirements` | yes | `Satisfy`/`Verify`/`Allocate` edges where the element is source or target | | `interface` | yes | Direct `Children` that are ports or have a non-null `FeatureTyping` | @@ -55,9 +55,15 @@ Entry shapes, one row per verb: its own intro sentences instead — see **Output Model** below). - `impact`: qn of every element transitively affected by a change to the target. - `describe`: direct `Children` as entries (child qn, `Kind` = child's kind); the node's - own kind/supertypes/typing/annotations/child-count appear in `Summary`, not `Entries`. - Supertypes are resolved via outgoing `Supertype` edges, falling back to raw - `SupertypeNames` only when no resolved edge exists. + own kind/supertypes/typing/annotations/applied-metadata/child-count appear in `Summary`, + not `Entries`. Supertypes are resolved via outgoing `Supertype` edges, falling back to raw + `SupertypeNames` only when no resolved edge exists. Applied `metadata` annotations (captured + as `SysmlMetadataNode` entries in `element.Children`, per `SysmlNode.cs`) are rendered as one + `"Metadata {TypeReference}"` line per bare annotation (no attributes), or one + `"Metadata {TypeReference}.{Attribute}: {value}"` line per attribute otherwise; boolean values + render as `true`/`false`, numbers via invariant-culture formatting, strings unquoted, and any + non-scalar (list/expression) value falls back to its verbatim raw source text so the value is + never silently dropped. - `hierarchy`: qn, `Kind` = `"supertype"` or `"subtype"`; `--direction up` walks outgoing `Supertype` edges, `down` walks incoming, `both` unions them. - `requirements`: other-side qn, `Kind` = edge kind label, `Detail` = direction arrow. diff --git a/docs/reqstream/sysml2-tools-tool/query.yaml b/docs/reqstream/sysml2-tools-tool/query.yaml index 4b0a3dd4..ecba3994 100644 --- a/docs/reqstream/sysml2-tools-tool/query.yaml +++ b/docs/reqstream/sysml2-tools-tool/query.yaml @@ -202,7 +202,8 @@ sections: - id: SysML2Tools-Tool-Query-Describe title: >- The describe verb shall report the target element's own kind, qualified name, - resolved supertypes, typing, annotations (comments/documentation), and direct + resolved supertypes, typing, annotations (comments/documentation), applied + metadata annotations (type reference and attribute values), and direct children. justification: | A single-element "fact sheet" is the most common starting point for model @@ -211,6 +212,11 @@ sections: - Describe_ReportsKindSupertypesAnnotationsAndChildren - Describe_MultiLineComment_CollapsesToSingleSummaryLine - Describe_CommentsFixture_ReportsAnnotations + - Describe_BareMetadataAnnotation_ReportsMetadataTypeLineOnly + - Describe_MetadataWithScalarAttributes_ReportsOnePerAttributeLine + - Describe_MetadataWithUnsupportedListAttribute_FallsBackToRawText + - Describe_MultipleMetadataAnnotations_ReportsEachIndependently + - Describe_FormatJson_IncludesMetadataInSummaryArray - id: SysML2Tools-Tool-Query-Hierarchy title: >- diff --git a/docs/user_guide/introduction.md b/docs/user_guide/introduction.md index 4ecdd996..7dfa23f1 100644 --- a/docs/user_guide/introduction.md +++ b/docs/user_guide/introduction.md @@ -434,7 +434,7 @@ sysml2tools query dependencies --element Model::Engine "src/**/*.sysml" # Transitive blast radius of a change, optionally bounded sysml2tools query impact --element Model::Engine --walk-depth 2 "src/**/*.sysml" -# A single-element "fact sheet": kind, supertypes, typing, annotations, children +# A single-element "fact sheet": kind, supertypes, typing, annotations, applied metadata, children sysml2tools query describe --element Model::Vehicle "src/**/*.sysml" # Supertype/subtype tree @@ -478,7 +478,7 @@ the same order, so either format can be relied on for automated comparisons. | `used-by` | yes | What depends on this element? | | `dependencies` | yes | What does this element depend on, and what depends on it (combined, as prose)? | | `impact` | yes | What is transitively affected by a change (`--walk-depth` to bound)? | -| `describe` | yes | What is this element (kind, supertypes, typing, annotations, children)? | +| `describe` | yes | What is this element (kind, supertypes, typing, annotations, applied metadata, children)? | | `hierarchy` | yes | What is the supertype/subtype tree (`--direction up`\|`down`\|`both`)? | | `requirements` | yes | What satisfy/verify/allocate relationships involve this element? | | `interface` | yes | What ports/typed features does this definition expose? | diff --git a/docs/verification/sysml2-tools-tool/query.md b/docs/verification/sysml2-tools-tool/query.md index 4471a1df..971ef3c2 100644 --- a/docs/verification/sysml2-tools-tool/query.md +++ b/docs/verification/sysml2-tools-tool/query.md @@ -41,7 +41,9 @@ framework. (non-tabular) Markdown result, with the merged entries' `Direction` field (`Outgoing`/`Incoming`) populated only for this verb; `impact` respects `--walk-depth` bounding and computes the full transitive closure - when unbounded; `describe` reports kind, resolved supertypes, and child count; + when unbounded; `describe` reports kind, resolved supertypes, child count, and applied + metadata annotations (type reference and attribute values, falling back to raw text for + non-scalar values); `hierarchy` respects `--direction up`/`down`/`both`; `requirements` reports satisfy/verify/allocate edges; `interface` reports ports/typed features and excludes plain attributes; `connections` reports resolved feature-chain endpoints with the @@ -168,6 +170,21 @@ reference {Element}."` prose line instead of a bullet list; and that the rendere never contains the `"| Qualified Name | Kind | Detail |"` table header used by every other verb. Satisfies `SysML2Tools-Tool-Query-Dependencies`. +**`Describe_BareMetadataAnnotation_ReportsMetadataTypeLineOnly`**, +**`Describe_MetadataWithScalarAttributes_ReportsOnePerAttributeLine`**, +**`Describe_MetadataWithUnsupportedListAttribute_FallsBackToRawText`**, +**`Describe_MultipleMetadataAnnotations_ReportsEachIndependently`**, +**`Describe_FormatJson_IncludesMetadataInSummaryArray`**: Verify, end-to-end, that +`describe` surfaces applied `metadata` annotations (`SysmlMetadataNode` entries in +`element.Children`) as additive `Summary` lines: a bare annotation with no attributes +reports a single `"Metadata {Type}"` line with no trailing attribute suffix; scalar +boolean/number/string attributes each report their own `"Metadata {Type}.{Attribute}: +{value}"` line; a non-scalar (list-valued) attribute falls back to its verbatim raw source +text rather than being dropped; multiple metadata annotations applied to the same element +each report independently, with neither overwriting the other; and `--format json` carries +the same `"Metadata ..."` lines in the JSON `Summary` array as the Markdown output. Satisfies +`SysML2Tools-Tool-Query-Describe`. + ##### QueryRenderingTests.cs Direct unit tests of `QueryResultRenderer.RenderMarkdown`/`RenderJson` against hand-built diff --git a/src/DemaConsulting.SysML2Tools.Tool/Query/QueryEngine.cs b/src/DemaConsulting.SysML2Tools.Tool/Query/QueryEngine.cs index 650cd9eb..6d2d8d7b 100644 --- a/src/DemaConsulting.SysML2Tools.Tool/Query/QueryEngine.cs +++ b/src/DemaConsulting.SysML2Tools.Tool/Query/QueryEngine.cs @@ -18,6 +18,7 @@ // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE // SOFTWARE. +using System.Globalization; using DemaConsulting.SysML2Tools.Semantic; using DemaConsulting.SysML2Tools.Semantic.Model; @@ -216,7 +217,8 @@ public static QueryResult Impact(SysmlWorkspace workspace, SysmlNode element, Qu /// /// Describes a single element in detail: kind, qualified name, supertypes, typing, - /// annotations (comments/documentation), and a list of its direct children. + /// annotations (comments/documentation), applied metadata annotations (type and attribute + /// values), and a list of its direct children. /// /// The loaded workspace. /// The target element. @@ -255,6 +257,20 @@ public static QueryResult Describe(SysmlWorkspace workspace, SysmlNode element, summary.Add($"{annotation.Kind}: {NormalizeAnnotationText(annotation.Text)}"); } + foreach (var metadata in element.Children.OfType()) + { + if (metadata.Attributes.Count == 0) + { + summary.Add($"Metadata {metadata.TypeReference}"); + continue; + } + + foreach (var attribute in metadata.Attributes) + { + summary.Add($"Metadata {metadata.TypeReference}.{attribute.Name}: {FormatMetadataAttributeValue(attribute)}"); + } + } + summary.Add($"Children: {element.Children.Count}"); var entries = element.Children @@ -291,6 +307,24 @@ private static string NormalizeAnnotationText(string text) return string.Join(" ", lines); } + /// + /// Formats a single applied for display on a + /// "Metadata {Type}.{Attribute}: {value}" summary line. Boolean values render as + /// "true"/"false", numbers via invariant-culture formatting, and strings + /// unquoted; any non-scalar () value + /// falls back to its verbatim so the value is + /// never silently dropped. + /// + /// The attribute value to format. + /// The formatted value text. + private static string FormatMetadataAttributeValue(MetadataAttributeValue attribute) => attribute.Kind switch + { + MetadataAttributeValueKind.Boolean => attribute.BooleanValue!.Value ? "true" : "false", + MetadataAttributeValueKind.Number => attribute.NumberValue!.Value.ToString(CultureInfo.InvariantCulture), + MetadataAttributeValueKind.String => attribute.StringValue!, + _ => attribute.RawText + }; + /// /// Reports the specialization/generalization hierarchy of a given element, walking /// resolved edges up (toward supertypes), down diff --git a/test/DemaConsulting.SysML2Tools.Tool.Tests/Query/QueryVerbsTests.cs b/test/DemaConsulting.SysML2Tools.Tool.Tests/Query/QueryVerbsTests.cs index 83587a70..c1980752 100644 --- a/test/DemaConsulting.SysML2Tools.Tool.Tests/Query/QueryVerbsTests.cs +++ b/test/DemaConsulting.SysML2Tools.Tool.Tests/Query/QueryVerbsTests.cs @@ -18,6 +18,9 @@ // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE // SOFTWARE. +using System.Text.Json; +using DemaConsulting.SysML2Tools.Query; + namespace DemaConsulting.SysML2Tools.Tests.Query; /// @@ -267,6 +270,163 @@ part def Car { Assert.DoesNotContain(lines, line => line.Trim().StartsWith('*')); } + /// + /// 'describe' reports a bare (no attributes) applied metadata annotation as a single + /// "Metadata {Type}" summary line, without a trailing attribute suffix. + /// + [Fact] + public async Task Describe_BareMetadataAnnotation_ReportsMetadataTypeLineOnly() + { + const string sysml = """ + package Model { + metadata def Critical; + + part def Car { + @Critical; + } + } + """; + + var (output, exitCode) = await QueryTestFixtures.RunQueryAsync( + sysml, "describe", "--element", "Model::Car"); + + Assert.Equal(0, exitCode); + Assert.Contains("Metadata Critical", output); + Assert.DoesNotContain("Metadata Critical.", output); + } + + /// + /// 'describe' reports each scalar (boolean/number/string) attribute of an applied + /// metadata annotation as its own "Metadata {Type}.{Attribute}: {value}" summary + /// line. + /// + [Fact] + public async Task Describe_MetadataWithScalarAttributes_ReportsOnePerAttributeLine() + { + const string sysml = """ + package Model { + metadata def SoftwareInfo { + attribute description : String; + attribute priority : Number; + attribute isCritical : Boolean; + } + + part def Car { + @SoftwareInfo { + description = "Engine control"; + priority = 1; + isCritical = true; + } + } + } + """; + + var (output, exitCode) = await QueryTestFixtures.RunQueryAsync( + sysml, "describe", "--element", "Model::Car"); + + Assert.Equal(0, exitCode); + Assert.Contains("Metadata SoftwareInfo.description: Engine control", output); + Assert.Contains("Metadata SoftwareInfo.priority: 1", output); + Assert.Contains("Metadata SoftwareInfo.isCritical: true", output); + } + + /// + /// 'describe' falls back to the verbatim raw text of a non-scalar (e.g. list-valued) + /// metadata attribute value, since only scalar boolean/number/string literals are + /// evaluated - the value is never silently dropped. + /// + [Fact] + public async Task Describe_MetadataWithUnsupportedListAttribute_FallsBackToRawText() + { + const string sysml = """ + package Model { + metadata def SoftwareInfo { + attribute sourceFiles : String[0..*]; + } + + part def Car { + @SoftwareInfo { + sourceFiles = ("a.cs", "b.cs"); + } + } + } + """; + + var (output, exitCode) = await QueryTestFixtures.RunQueryAsync( + sysml, "describe", "--element", "Model::Car"); + + Assert.Equal(0, exitCode); + Assert.Contains("Metadata SoftwareInfo.sourceFiles: (\"a.cs\",\"b.cs\")", output); + } + + /// + /// 'describe' reports multiple metadata annotations applied to the same element + /// independently: each produces its own summary line(s), and neither overwrites the + /// other. + /// + [Fact] + public async Task Describe_MultipleMetadataAnnotations_ReportsEachIndependently() + { + const string sysml = """ + package Model { + metadata def Safety; + metadata def Critical; + + part def Car { + @Safety; + @Critical; + } + } + """; + + var (output, exitCode) = await QueryTestFixtures.RunQueryAsync( + sysml, "describe", "--element", "Model::Car"); + + Assert.Equal(0, exitCode); + Assert.Contains("Metadata Safety", output); + Assert.Contains("Metadata Critical", output); + } + + /// + /// 'describe --format json' includes the same "Metadata ..." summary lines in the + /// JSON Summary array as the Markdown output, since Summary is rendered directly + /// from the shared without any Markdown-only transformation. + /// + [Fact] + public async Task Describe_FormatJson_IncludesMetadataInSummaryArray() + { + const string sysml = """ + package Model { + metadata def SoftwareInfo { + attribute isCritical : Boolean; + } + + part def Car { + @SoftwareInfo { + isCritical = true; + } + } + } + """; + + var (json, exitCode) = await QueryTestFixtures.RunQueryAsync( + sysml, "describe", "--element", "Model::Car", "--format", "json"); + + Assert.Equal(0, exitCode); + + // The unresolved-reference diagnostic for the built-in "Boolean" type is written as a + // single-line "SysmlDiagnostic { ... }" record before the JSON result, so locate the + // JSON document's opening brace via the unambiguous "Verb" property rather than the + // first '{' in the captured output. + var verbIndex = json.IndexOf("\"Verb\":", StringComparison.Ordinal); + var jsonStart = json.LastIndexOf('{', verbIndex); + var deserialized = JsonSerializer.Deserialize( + json[jsonStart..], QueryResultSerializerContext.Default.QueryResult); + + Assert.NotNull(deserialized); + Assert.Contains("Metadata SoftwareInfo.isCritical: true", deserialized!.Summary); + } + /// /// 'hierarchy' with --direction up reports only supertypes. ///