From e4541797ec5ea6a972c1617f5768e059d92c0c74 Mon Sep 17 00:00:00 2001 From: Malcolm Nixon Date: Sun, 19 Jul 2026 13:47:32 -0400 Subject: [PATCH] Surface applied metadata attributes in query describe Summary Query describe now emits Metadata {Type}/{Type}.{Attribute}: {value} lines in the Summary output for SysmlMetadataNode instances attached to an element's Children, mirroring the existing Comment/Documentation annotation lines. Covers scalar (Boolean/Number/String) attribute values, list/unsupported attribute values via raw-text fallback, bare metadata with no attributes, and multiple metadata annotations on the same element. Purely additive to Summary; Children/Entries semantics and existing Comment/Documentation/Supertypes/Typing output are unchanged. Adds 5 unit tests to QueryVerbsTests.cs covering scalar attributes, list-valued fallback, no-metadata regression, bare metadata, multiple annotations, and JSON format shape parity. Updates design, verification, reqstream, and user guide docs for the Query subsystem describe verb to document the new behavior. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- docs/design/sysml2-tools-tool/query.md | 14 +- docs/reqstream/sysml2-tools-tool/query.yaml | 8 +- docs/user_guide/introduction.md | 4 +- docs/verification/sysml2-tools-tool/query.md | 19 ++- .../Query/QueryEngine.cs | 36 +++- .../Query/QueryVerbsTests.cs | 160 ++++++++++++++++++ 6 files changed, 232 insertions(+), 9 deletions(-) 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. ///