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.
///