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
21 changes: 17 additions & 4 deletions docs/design/sysml2-tools-core/query.md
Original file line number Diff line number Diff line change
Expand Up @@ -110,7 +110,13 @@ flowchart TD
6. `Impact` performs a breadth-first transitive closure over incoming edges, optionally bounded
by `WalkDepth`, with a visited set preventing infinite loops on cyclic graphs.
7. `Describe` reports the target element's own kind and qualified name in `Summary`, then adds
resolved supertypes, typing, annotations, applied metadata annotations, and direct children.
resolved supertypes, typing, annotations, applied metadata annotations, and a `Children: N`
count. `N` is the count of visible, named child entries actually placed in `Entries` (i.e.
direct children with a non-null `QualifiedName` that pass the `IncludeStdlib` visibility
rule), not the raw `element.Children.Count` - so the stated count always matches the number
of rows shown, even though non-element children (comments, metadata annotations, imports)
and, when `IncludeStdlib` is unset, stdlib-seeded children are present in `element.Children`
but excluded from both the count and the table.
Metadata values preserve scalar booleans, numbers, and strings directly, while unsupported
non-scalar values fall back to raw source text so information is never silently dropped.
8. `Hierarchy` walks specialization relationships recursively. `--direction up` follows
Expand All @@ -131,9 +137,16 @@ flowchart TD
supplied.
14. `QueryResultRenderer` sorts entries exactly once by `QualifiedName` (ordinal) for both
Markdown and JSON. All verbs except `dependencies` render Markdown as a heading, optional
summary bullet list, and a shared table. `dependencies` is the one intentional exception:
Markdown is rendered as direction-grouped prose bullets after shortening the subject and
entry names with `QualifiedNameShortener`. `RenderJson` never shortens names and uses
summary bullet list, a verb-specific bold-text label (e.g. `**Children**` for `describe`,
`**Uses**` for `uses`), and then either the shared table or a verb-specific "no entries"
fallback line (e.g. `_No children._`) when there are none. Labeling the (possibly empty)
entries section tells the reader what kind of thing it holds, so a zero-row result reads as
an unremarkable, expected outcome rather than a broken query. The label is always plain bold
text, never an ATX heading, so the whole report stays within the single Markdown section
started by the main heading regardless of the caller's requested heading depth.
`dependencies` is the one intentional exception: Markdown is rendered as direction-grouped
prose bullets after shortening the subject and entry names with `QualifiedNameShortener`,
with no separate label. `RenderJson` never shortens names and uses
`QueryResultSerializerContext` so the JSON shape remains fully qualified and AOT-safe.
15. `QueryResultExporter` renders first, then writes the exact Markdown or JSON text to the
caller-supplied file path. Markdown is joined with `"\n"`; no parent-directory creation or
Expand Down
35 changes: 32 additions & 3 deletions docs/reqstream/sysml2-tools-core/query.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -106,12 +106,17 @@ sections:
- id: SysML2Tools-Core-Query-Describe
title: >-
The Query subsystem shall report a target element's own kind, qualified name, resolved
supertypes, typing, annotations, applied metadata annotations, and direct children
through the `describe` verb.
supertypes, typing, annotations, applied metadata annotations, and a `Children` count
through the `describe` verb, where the reported count always equals the number of
entries actually returned (visible, named children), never the raw count of the
element's underlying AST child nodes.
justification: |
A single-element fact sheet is the most common starting point for programmatic model
exploration, consolidating information otherwise scattered across the raw source and
semantic model.
semantic model. A `Children` count that disagrees with the number of rows shown (as
happens when non-element children, such as comments/metadata/imports, or hidden
stdlib children, are counted but not listed) misleads a reader into thinking rows are
missing or the query is broken.
tests:
- Describe_ReportsKindSupertypesAnnotationsAndChildren
- Describe_MultiLineComment_CollapsesToSingleSummaryLine
Expand All @@ -121,6 +126,7 @@ sections:
- Describe_MetadataWithUnsupportedListAttribute_FallsBackToRawText
- Describe_MultipleMetadataAnnotations_ReportsEachIndependently
- Describe_FormatJson_IncludesMetadataInSummaryArray
- Describe_ChildIncludesNonElementMetadataAnnotation_ChildrenCountMatchesEntryRows

- id: SysML2Tools-Core-Query-Hierarchy
title: >-
Expand Down Expand Up @@ -236,3 +242,26 @@ sections:
- WriteJsonAsync_HappyPath_MatchesRendererOutput
- WriteMarkdown_MissingParentDirectory_PropagatesIoException
- WriteJson_MissingParentDirectory_PropagatesIoException

- id: SysML2Tools-Core-Query-EntriesLabeling
title: >-
For every verb except `dependencies`, Markdown rendering shall precede the entries
table (or its "no entries" fallback) with a verb-specific bold-text label and
fallback message naming what kind of thing the entries represent (e.g. "Children" for
`describe`, "Uses" for `uses`), falling back to a generic "Entries"/"_No entries._"
label for any verb without a specific mapping. The label shall always render as plain
bold text rather than an ATX heading, so the report never branches into a Markdown
sub-section regardless of the caller's requested heading depth.
justification: |
A bare, unlabeled table (or a bare "_No entries._" line) gives a reader no way to tell
whether zero rows is an unremarkable, expected outcome for that verb or a sign the
query is broken. Naming the entries per verb removes that ambiguity without requiring
the reader to already know each verb's semantics by heart. Using bold text instead of
a sub-heading keeps the whole report within a single Markdown section, which is easier
to read and simpler to embed into other documents.
tests:
- RenderMarkdown_NoEntries_ReportsNoEntries
- RenderMarkdown_EntriesPresent_IncludesVerbSpecificBoldLabel
- RenderMarkdown_UnrecognizedVerb_FallsBackToGenericEntriesLabel
- RenderMarkdown_ListOrFindVerb_UsesMatchingElementsLabel
- RenderMarkdown_MaxHeadingDepth_EntriesLabelStaysBoldTextNotHeading
30 changes: 29 additions & 1 deletion docs/verification/sysml2-tools-core/query.md
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,14 @@ scenarios listed under "Test Scenarios (Tool Test Project)" below run via `dotne
- `QualifiedNameShortener.Shorten` strips only the longest shared leading `::`-segment prefix
across a pool of qualified names, always capped so every name keeps at least its own leaf
segment, and rejects `null` pools or `null` pool entries.
- `describe`'s `Children: N` summary line always equals the number of rows in the `Entries`
table (visible, named children only), never the raw count of the element's underlying AST
child nodes.
- Every verb except `dependencies` precedes its entries table (or "no entries" fallback) with a
verb-specific bold-text label and fallback message (e.g. `**Children**` / `_No children._` for
`describe`), falling back to a generic `**Entries**` / `_No entries._` label for any
unrecognized verb. The label is always plain bold text, never an ATX heading, so the report
never branches into a Markdown sub-section regardless of the caller's requested heading depth.

### Test Scenarios

Expand All @@ -77,7 +85,27 @@ supertypes are reported.
`QueryEngine.Describe`, and verifies that annotations appear in the result summary.

**`RenderMarkdown_NoEntries_ReportsNoEntries`**: Verifies that a non-`dependencies` result with
no entries renders the shared Markdown `_No entries._` fallback.
no entries is preceded by its verb-specific bold-text label (e.g. `**Uses**`) and renders the
verb-specific "no entries" fallback text (e.g. `_No outgoing references._`) instead of an empty
table.

**`RenderMarkdown_EntriesPresent_IncludesVerbSpecificBoldLabel`**: Verifies that a
non-`dependencies` result with entries is also preceded by its verb-specific bold-text label
(e.g. `**Children**` for `describe`), not just the "no entries" fallback case, and that the
label is plain bold text rather than an ATX heading.

**`RenderMarkdown_UnrecognizedVerb_FallsBackToGenericEntriesLabel`**: Verifies that a verb with
no specific entries-label mapping falls back to the generic `**Entries**` label and `_No
entries._` fallback text.

**`RenderMarkdown_ListOrFindVerb_UsesMatchingElementsLabel`**: Verifies that both `list`
and `find` share the same `**Matching Elements**` label, since `find` is a filtered form
of `list` and their entries mean the same thing.

**`RenderMarkdown_MaxHeadingDepth_EntriesLabelStaysBoldTextNotHeading`**: Verifies that at the
maximum Markdown heading depth (6), the entries label remains plain bold text (`**Children**`)
rather than becoming (or being mistaken for) an ATX heading, so the report never branches into
a Markdown sub-section at any valid heading depth.

**`RenderMarkdown_DependenciesVerb_RendersBulletProseNotTable`**: Verifies that
`dependencies` Markdown renders as grouped prose bullets rather than the shared table.
Expand Down
4 changes: 2 additions & 2 deletions src/DemaConsulting.SysML2Tools.Core/Query/QueryEngine.cs
Original file line number Diff line number Diff line change
Expand Up @@ -313,13 +313,13 @@ public static QueryResult Describe(SysmlWorkspace workspace, SysmlNode element,
}
}

summary.Add($"Children: {element.Children.Count}");

var entries = element.Children
.Where(c => c.QualifiedName is not null && IsVisible(c.QualifiedName, workspace, options.IncludeStdlib))
.Select(c => new QueryResultEntry { QualifiedName = c.QualifiedName!, Kind = DescribeKind(c) })
.ToList();

summary.Add($"Children: {entries.Count}");

return new QueryResult
{
Verb = "describe",
Expand Down
50 changes: 45 additions & 5 deletions src/DemaConsulting.SysML2Tools.Core/Query/QueryResultRenderer.cs
Original file line number Diff line number Diff line change
Expand Up @@ -18,10 +18,18 @@ public static class QueryResultRenderer
{
/// <summary>
/// Renders a <see cref="QueryResult"/> as Markdown lines: a heading, an optional summary
/// bullet list, then a compact table of entries sorted by
/// <see cref="QueryResultEntry.QualifiedName"/> (ordinal). The <c>dependencies</c> verb
/// is the sole exception: after the heading, its entries are rendered as prose bullets
/// (see <see cref="RenderDependenciesBody"/>) rather than a table.
/// bullet list, then a verb-specific bold-text label (e.g. "Children" for
/// <c>describe</c>, "Uses" for <c>uses</c>) followed by a compact table of entries
/// sorted by <see cref="QueryResultEntry.QualifiedName"/> (ordinal), or a verb-specific
/// "no entries" fallback line (e.g. "_No children._") when there are none. The label and
/// fallback text tell the reader what kind of thing the entries represent, so an empty
/// result reads as an unremarkable, expected outcome rather than a broken query. The
/// label is always plain bold text rather than its own heading, so the whole report
/// stays within the single Markdown section started by the main heading - it never
/// branches into a sub-section, regardless of <paramref name="depth"/>. The
/// <c>dependencies</c> verb is the sole exception: after the heading, its entries are
/// rendered as prose bullets (see <see cref="RenderDependenciesBody"/>) rather than a
/// table, with no separate label.
/// </summary>
/// <param name="result">The result to render.</param>
/// <param name="depth">
Expand Down Expand Up @@ -77,9 +85,16 @@ public static IReadOnlyList<string> RenderMarkdown(
}

var sorted = SortEntries(result.Entries);
var (entriesHeading, noEntriesText) = EntriesLabel(result.Verb);

// Plain bold text (not an ATX heading) keeps the entries label within the same
// Markdown section as the main heading, regardless of the caller's heading depth.
lines.Add($"**{entriesHeading}**");
lines.Add("");

if (sorted.Count == 0)
{
lines.Add("_No entries._");
lines.Add(noEntriesText);
return lines;
}

Expand All @@ -93,6 +108,31 @@ public static IReadOnlyList<string> RenderMarkdown(
return lines;
}

/// <summary>
/// Maps a verb name to a human-readable heading for its entries table (or the fallback
/// text shown when it has no entries), so a reader can tell what kind of thing an empty
/// result means without already knowing the verb's semantics (e.g. "_No children._" for
/// <c>describe</c> makes clear that zero rows is an unremarkable, expected outcome for a
/// childless element, not a broken query).
/// </summary>
/// <param name="verb">The query verb.</param>
/// <returns>The heading text and "no entries" fallback text for that verb.</returns>
private static (string Heading, string NoEntriesText) EntriesLabel(string verb) => verb switch
{
"uses" => ("Uses", "_No outgoing references._"),
"used-by" => ("Used By", "_No incoming references._"),
"impact" => ("Impacted Elements", "_No impacted elements._"),
"describe" => ("Children", "_No children._"),
"hierarchy" => ("Related Types", "_No related types._"),
"requirements" => ("Requirement Relationships", "_No requirement relationships._"),
"interface" => ("Ports & Typed Features", "_No ports or typed features._"),
"connections" => ("Connection Endpoints", "_No connection endpoints._"),
"states" => ("States & Transitions", "_No states or transitions._"),
"list" or "find" => ("Matching Elements", "_No matching elements._"),
_ => ("Entries", "_No entries._")
};


/// <summary>
/// Renders a <see cref="QueryResult"/> as an indented JSON document via the
/// <see cref="QueryResultSerializerContext"/> source-generated serializer, with entries
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -245,7 +245,7 @@
<value> sysml2tools query find --name Wheel test/SysMLModels/OMG/examples/VehicleExample/*.sysml</value>
</data>
<data name="Query_SchemaHint_Markdown" xml:space="preserve">
<value>Markdown output: a '# query &lt;verb&gt;[: &lt;element&gt;]' heading, an optional bullet-list summary, then a '| Qualified Name | Kind | Detail |' table (empty results print '_No entries._').</value>
<value>Markdown output: a '# query &lt;verb&gt;[: &lt;element&gt;]' heading, an optional bullet-list summary, then a verb-specific bold-text label (e.g. '**Children**') and a '| Qualified Name | Kind | Detail |' table (empty results print a verb-specific fallback, e.g. '_No children._').</value>
</data>
<data name="Query_SchemaHint_Json" xml:space="preserve">
<value>JSON output: { "Verb", "Element", "Summary": [...], "Entries": [{ "QualifiedName", "Kind", "Detail", "Notes": [] }] }, entries sorted by QualifiedName.</value>
Expand Down
85 changes: 82 additions & 3 deletions test/DemaConsulting.SysML2Tools.Tests/Query/QueryRenderingTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -16,8 +16,9 @@ namespace DemaConsulting.SysML2Tools.Tests.Query;
public class QueryRenderingTests
{
/// <summary>
/// RenderMarkdown on a result with no entries reports "No entries" rather than an empty
/// table.
/// RenderMarkdown on a result with no entries reports a verb-specific "no entries"
/// fallback line (rather than an empty table), and precedes it with a verb-specific
/// bold-text label naming what kind of entry the (empty) table would have held.
/// </summary>
[Fact]
public void RenderMarkdown_NoEntries_ReportsNoEntries()
Expand All @@ -26,7 +27,85 @@ public void RenderMarkdown_NoEntries_ReportsNoEntries()

var lines = QueryResultRenderer.RenderMarkdown(result);

Assert.Contains(lines, l => l.Contains("No entries"));
Assert.Contains("**Uses**", lines);
Assert.Contains(lines, l => l.Contains("No outgoing references"));
}

/// <summary>
/// RenderMarkdown adds a verb-specific bold-text label before the entries table (not
/// just before the "no entries" fallback), naming what kind of thing the table's rows
/// represent for that verb. The label is plain bold text, not an ATX heading, so the
/// whole report stays within the single Markdown section started by the main heading.
/// </summary>
[Fact]
public void RenderMarkdown_EntriesPresent_IncludesVerbSpecificBoldLabel()
{
var result = new QueryResult
{
Verb = "describe",
Element = "Model::Car",
Entries = [new QueryResultEntry { QualifiedName = "Model::Car::engine" }]
};

var lines = QueryResultRenderer.RenderMarkdown(result);

Assert.Contains("**Children**", lines);
Assert.DoesNotContain(lines, l => l.StartsWith('#') && l.Contains("Children"));
}

/// <summary>
/// RenderMarkdown gives an unrecognized verb the generic "Entries" label and "_No
/// entries._" fallback text, so any future verb added without an explicit label still
/// renders sensibly instead of throwing.
/// </summary>
[Fact]
public void RenderMarkdown_UnrecognizedVerb_FallsBackToGenericEntriesLabel()
{
var result = new QueryResult { Verb = "some-future-verb", Element = "Model::Car" };

var lines = QueryResultRenderer.RenderMarkdown(result);

Assert.Contains("**Entries**", lines);
Assert.Contains("_No entries._", lines);
}

/// <summary>
/// RenderMarkdown's entries label is always plain bold text - never an ATX heading -
/// regardless of the caller's requested heading depth, so the report never branches
/// into a Markdown sub-section even at the maximum heading depth (6).
/// </summary>
[Fact]
public void RenderMarkdown_MaxHeadingDepth_EntriesLabelStaysBoldTextNotHeading()
{
var result = new QueryResult
{
Verb = "describe",
Element = "Model::Car",
Entries = [new QueryResultEntry { QualifiedName = "Model::Car::engine" }]
};

var lines = QueryResultRenderer.RenderMarkdown(result, depth: 6);

Assert.Equal("###### query describe: Model::Car", lines[0]);
Assert.Contains("**Children**", lines);
Assert.DoesNotContain(lines.Skip(1), l => l.StartsWith('#'));
}

/// <summary>
/// RenderMarkdown gives both "list" and "find" the same "Matching Elements" bold-text
/// label, since "find" is a filtered form of "list" and their entries mean the same
/// thing.
/// </summary>
[Theory]
[InlineData("list")]
[InlineData("find")]
public void RenderMarkdown_ListOrFindVerb_UsesMatchingElementsLabel(string verb)
{
var result = new QueryResult { Verb = verb };

var lines = QueryResultRenderer.RenderMarkdown(result);

Assert.Contains("**Matching Elements**", lines);
}

/// <summary>
Expand Down
Loading
Loading