Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
14 changes: 10 additions & 4 deletions docs/design/sysml2-tools-tool/query.md
Original file line number Diff line number Diff line change
Expand Up @@ -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` |
Expand All @@ -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.
Expand Down
8 changes: 7 additions & 1 deletion docs/reqstream/sysml2-tools-tool/query.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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: >-
Expand Down
4 changes: 2 additions & 2 deletions docs/user_guide/introduction.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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? |
Expand Down
19 changes: 18 additions & 1 deletion docs/verification/sysml2-tools-tool/query.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand Down
36 changes: 35 additions & 1 deletion src/DemaConsulting.SysML2Tools.Tool/Query/QueryEngine.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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;

Expand Down Expand Up @@ -216,7 +217,8 @@ public static QueryResult Impact(SysmlWorkspace workspace, SysmlNode element, Qu

/// <summary>
/// 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.
/// </summary>
/// <param name="workspace">The loaded workspace.</param>
/// <param name="element">The target element.</param>
Expand Down Expand Up @@ -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<SysmlMetadataNode>())
{
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
Expand Down Expand Up @@ -291,6 +307,24 @@ private static string NormalizeAnnotationText(string text)
return string.Join(" ", lines);
}

/// <summary>
/// Formats a single applied <see cref="MetadataAttributeValue"/> for display on a
/// <c>"Metadata {Type}.{Attribute}: {value}"</c> summary line. Boolean values render as
/// <c>"true"</c>/<c>"false"</c>, numbers via invariant-culture formatting, and strings
/// unquoted; any non-scalar (<see cref="MetadataAttributeValueKind.Unsupported"/>) value
/// falls back to its verbatim <see cref="MetadataAttributeValue.RawText"/> so the value is
/// never silently dropped.
/// </summary>
/// <param name="attribute">The attribute value to format.</param>
/// <returns>The formatted value text.</returns>
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
};

/// <summary>
/// Reports the specialization/generalization hierarchy of a given element, walking
/// resolved <see cref="SysmlEdgeKind.Supertype"/> edges up (toward supertypes), down
Expand Down
160 changes: 160 additions & 0 deletions test/DemaConsulting.SysML2Tools.Tool.Tests/Query/QueryVerbsTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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;

/// <summary>
Expand Down Expand Up @@ -267,6 +270,163 @@ part def Car {
Assert.DoesNotContain(lines, line => line.Trim().StartsWith('*'));
}

/// <summary>
/// 'describe' reports a bare (no attributes) applied metadata annotation as a single
/// <c>"Metadata {Type}"</c> summary line, without a trailing attribute suffix.
/// </summary>
[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);
}

/// <summary>
/// 'describe' reports each scalar (boolean/number/string) attribute of an applied
/// metadata annotation as its own <c>"Metadata {Type}.{Attribute}: {value}"</c> summary
/// line.
/// </summary>
[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);
}

/// <summary>
/// '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.
/// </summary>
[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);
}

/// <summary>
/// 'describe' reports multiple metadata annotations applied to the same element
/// independently: each produces its own summary line(s), and neither overwrites the
/// other.
/// </summary>
[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);
}

/// <summary>
/// 'describe --format json' includes the same "Metadata ..." summary lines in the
/// JSON <c>Summary</c> array as the Markdown output, since Summary is rendered directly
/// from the shared <see cref="QueryResult"/> without any Markdown-only transformation.
/// </summary>
[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);
}

/// <summary>
/// 'hierarchy' with --direction up reports only supertypes.
/// </summary>
Expand Down
Loading