diff --git a/.reviewmark.yaml b/.reviewmark.yaml index 1e07a12..7c338ca 100644 --- a/.reviewmark.yaml +++ b/.reviewmark.yaml @@ -533,6 +533,41 @@ reviews: - "src/DemaConsulting.SysML2Tools.Core/Io/NamespaceDoc.cs" - "test/DemaConsulting.SysML2Tools.Tests/Io/GlobFileCollectorTests.cs" + - id: SysML2Tools-Core-Query-Design + title: Review that DemaConsulting.SysML2Tools Query Design is Consistent and Complete + context: + - docs/reqstream/sysml2-tools-core.yaml + - docs/reqstream/sysml2-tools-core/query.yaml + paths: + - "docs/design/introduction.md" + - "docs/design/sysml2-tools-core.md" + - "docs/design/sysml2-tools-core/query.md" + + - id: SysML2Tools-Core-Query-Verification + title: Review that DemaConsulting.SysML2Tools Query Verification is Consistent and Complete + context: + - docs/reqstream/sysml2-tools-core.yaml + - docs/reqstream/sysml2-tools-core/query.yaml + paths: + - "docs/verification/introduction.md" + - "docs/verification/sysml2-tools-core.md" + - "docs/verification/sysml2-tools-core/query.md" + + - id: SysML2Tools-Core-Query + title: Review that DemaConsulting.SysML2Tools Query Implementation is Correct + context: + - docs/design/sysml2-tools-core.md + - docs/reqstream/sysml2-tools-core.yaml + - docs/design/sysml2-tools-core/query.md + paths: + - "docs/reqstream/sysml2-tools-core/query.yaml" + - "docs/design/sysml2-tools-core/query.md" + - "docs/verification/sysml2-tools-core/query.md" + - "src/DemaConsulting.SysML2Tools.Core/Query/*.cs" + - "src/DemaConsulting.SysML2Tools.Core/Utilities/QualifiedNameShortener.cs" + - "test/DemaConsulting.SysML2Tools.Tests/Query/*.cs" + - "test/DemaConsulting.SysML2Tools.Tests/Utilities/QualifiedNameShortenerTests.cs" + - id: SysML2Tools-Core-Filtering-Design title: Review that DemaConsulting.SysML2Tools Filtering Design is Consistent and Complete context: @@ -902,20 +937,6 @@ reviews: - "src/**/Utilities/PathHelpers.cs" - "test/**/Utilities/PathHelpersTests.cs" - - id: SysML2Tools-Tool-Utilities-QualifiedNameShortener - title: Review of SysML2 Tools QualifiedNameShortener unit implementation - context: - - docs/design/sysml2-tools-tool.md - - docs/reqstream/sysml2-tools-tool.yaml - - docs/design/sysml2-tools-tool/utilities.md - - docs/reqstream/sysml2-tools-tool/utilities.yaml - paths: - - "docs/reqstream/sysml2-tools-tool/utilities/qualified-name-shortener.yaml" - - "docs/design/sysml2-tools-tool/utilities/qualified-name-shortener.md" - - "docs/verification/sysml2-tools-tool/utilities/qualified-name-shortener.md" - - "src/**/Utilities/QualifiedNameShortener.cs" - - "test/**/Utilities/QualifiedNameShortenerTests.cs" - - id: SysML2Tools-Tool-Lint title: Review of SysML2 Tools Lint subsystem architecture and interfaces context: @@ -959,14 +980,14 @@ reviews: - "docs/reqstream/sysml2-tools-tool/query.yaml" - "docs/design/sysml2-tools-tool/query.md" - "docs/verification/sysml2-tools-tool/query.md" - - "src/DemaConsulting.SysML2Tools.Tool/Query/*.cs" + - "src/DemaConsulting.SysML2Tools.Tool/Query/QueryCommand.cs" + - "src/DemaConsulting.SysML2Tools.Tool/Query/QueryCliArgumentParser.cs" + - "src/DemaConsulting.SysML2Tools.Tool/Query/QueryStrings.cs" - "src/DemaConsulting.SysML2Tools.Tool/Query/QueryStrings.resx" - "test/DemaConsulting.SysML2Tools.Tool.Tests/Query/QuerySubsystemTests.cs" - "test/DemaConsulting.SysML2Tools.Tool.Tests/Resources/ResxResourceTests.cs" - "test/DemaConsulting.SysML2Tools.Tool.Tests/Query/QueryTestFixtures.cs" - "test/DemaConsulting.SysML2Tools.Tool.Tests/Query/QueryVerbsTests.cs" - - "test/DemaConsulting.SysML2Tools.Tool.Tests/Query/QueryRenderingTests.cs" - - "test/DemaConsulting.SysML2Tools.Tool.Tests/Query/QueryOmgFixtureTests.cs" - "test/DemaConsulting.SysML2Tools.Tool.Tests/Query/QueryErrorPathTests.cs" - id: SysML2Tools-Tool-Export diff --git a/README.md b/README.md index 9810dcc..21a6aea 100644 --- a/README.md +++ b/README.md @@ -257,6 +257,7 @@ sysml2tools help [lint|render|query []|export] | `` | One or more glob patterns for `.sysml` input files | | `--element `, `-e ` | Qualified name of the target element; required for every verb except `list`/`find` | | `--format markdown\|json` | Output format (default: `markdown`); distinct from `render`'s `--format` (`svg`/`png`) | +| `--output ` | Write query report to this **file** (default: stdout); differs from `render`'s `--output` dir | | `--walk-depth <#>` | Maximum impact-walk depth (`impact` verb only) | | `--direction up\|down\|both` | Traversal direction (`hierarchy` verb only) | | `--kind ` | Element-kind filter (`list`/`find` verbs only) | diff --git a/docs/design/introduction.md b/docs/design/introduction.md index a04a659..f11eba2 100644 --- a/docs/design/introduction.md +++ b/docs/design/introduction.md @@ -114,6 +114,27 @@ system, subsystem, and unit levels: - **GlobFileCollector** (Unit) — resolves ordered glob patterns (with `!` exclusions, recursive `**` matching, and bare-`*` extension filtering) to a sorted, deduplicated list of absolute file paths + - **Query** (Subsystem) — public, reusable model-analysis API answering 12 fixed questions + over an already-loaded semantic workspace and returning a uniform `QueryResult` that + callers can render as Markdown or JSON or write to a file + - **QueryVerb** (Unit) — fixed 12-verb vocabulary, token conversion, and the + `RequiresElement` rule + - **QueryOptions** (Unit) — immutable option record shared by every verb + - **QueryArgumentParser** (Unit) — parses a token list into `(QueryOptions?, Files)` for + callers that accept the same verb/option grammar as the CLI + - **QueryEngine** (Unit) — public execution surface: 12 verb methods plus `Execute`, + centralizing the verb switch used by both library callers and the CLI adapter + - **QueryResult** (Unit) — verb-agnostic result model (`QueryResult`, `QueryResultEntry`, + `QueryEntryDirection`) + - **QueryResultRenderer** (Unit) — shared Markdown/JSON rendering and deterministic + sorting layer + - **QueryResultSerializerContext** (Unit) — source-generated `System.Text.Json` context + used by `RenderJson` + - **QueryResultExporter** (Unit) — synchronous and asynchronous file-writing wrappers + around the renderer + - **QualifiedNameShortener** (Unit) — strips the longest shared leading `::`-segment + prefix across a pool of qualified names, used only by the `dependencies` verb's + Markdown rendering - **DemaConsulting.SysML2Tools.Tool** (System) — dotnet tool: thin CLI wrapper and orchestration - **Program** (Unit) — entry point and execution orchestrator @@ -140,9 +161,6 @@ system, subsystem, and unit levels: - **Validation** (Unit) — self-validation test runner - **Utilities** (Subsystem) — shared utilities - **PathHelpers** (Unit) — safe path combination utilities - - **QualifiedNameShortener** (Unit) — strips the longest common leading `::`-segment prefix - shared across a pool of qualified names, used by the `query dependencies` verb's Markdown - rendering **OTS Dependencies:** @@ -186,6 +204,10 @@ reviewers an explicit navigation aid from design to code: - **Rendering/** — SysML-coupled rendering pipeline (`ILayoutStrategy`, `DiagramRenderer`) - **Io/** — shared file glob pattern resolution used by the Tool project's lint/render/query commands (`GlobFileCollector`) + - **Query/** — public, reusable model-analysis API (`QueryVerb`, `QueryOptions`, + `QueryArgumentParser`, `QueryEngine`, `QueryResult`, `QueryResultRenderer`, + `QueryResultSerializerContext`, `QueryResultExporter`) + - **Utilities/** — shared helper used by the Query subsystem (`QualifiedNameShortener`) - **DemaConsulting.SysML2Tools.Tool/** — dotnet tool CLI wrapper - **Cli/** — command-line interface subsystem - **Lint/** — lint command subsystem diff --git a/docs/design/sysml2-tools-core.md b/docs/design/sysml2-tools-core.md index f1ced84..82a0e98 100644 --- a/docs/design/sysml2-tools-core.md +++ b/docs/design/sysml2-tools-core.md @@ -2,23 +2,27 @@ ## Architecture -The `DemaConsulting.SysML2Tools` core library provides the Filtering, Layout, Rendering, and Io -subsystems for SysML v2 diagram generation and shared file-discovery. It depends on -`DemaConsulting.SysML2Tools.Language` for parsing and semantic analysis, and on -`DemaConsulting.SysML2Tools.Stdlib` for the pre-compiled standard library. - -The core library provides four subsystems: **Filtering**, **Layout**, **Rendering**, and **Io**. -The Filtering subsystem parses and evaluates the Phase 1 subset of standalone -`filter [];` statements against metadata annotations captured by the semantic model. The Layout -subsystem maps the SysML semantic model onto the `LayoutTree` intermediate representation — nine -immutable node record types covering all SysML diagram elements — which is provided off-the-shelf -by the `DemaConsulting.Rendering` package, and delegates geometric placement and routing to the -off-the-shelf `DemaConsulting.Rendering.Layout` layered algorithm. The Rendering subsystem -consumes the off-the-shelf rendering contracts (`IRenderer`, `Theme`, `RenderOptions`, -`RenderOutput`) from the `DemaConsulting.Rendering.Abstractions` package and retains the -SysML-coupled `ILayoutStrategy` and `DiagramRenderer` that drive the pipeline. The Io subsystem -provides `GlobFileCollector`, a shared file glob pattern resolver used by the Tool project's -`lint`, `render`, and `query` commands; it has no dependency on the SysML semantic model. +The `DemaConsulting.SysML2Tools` core library provides the Filtering, Layout, Rendering, +Query, and Io subsystems for SysML v2 diagram generation, model analysis, and shared file- +discovery. It depends on `DemaConsulting.SysML2Tools.Language` for parsing and semantic +analysis, and on `DemaConsulting.SysML2Tools.Stdlib` for the pre-compiled standard library. + +The core library provides five subsystems: **Filtering**, **Layout**, **Rendering**, +**Query**, and **Io**. The Filtering subsystem parses and evaluates the Phase 1 subset of +standalone `filter [];` statements against metadata annotations captured by the +semantic model. The Layout subsystem maps the SysML semantic model onto the `LayoutTree` +intermediate representation — nine immutable node record types covering all SysML diagram +elements — which is provided off-the-shelf by the `DemaConsulting.Rendering` package, and +delegates geometric placement and routing to the off-the-shelf +`DemaConsulting.Rendering.Layout` layered algorithm. The Rendering subsystem consumes the +off-the-shelf rendering contracts (`IRenderer`, `Theme`, `RenderOptions`, `RenderOutput`) +from the `DemaConsulting.Rendering.Abstractions` package and retains the SysML-coupled +`ILayoutStrategy` and `DiagramRenderer` that drive the pipeline. The Query subsystem exposes +Core's public model-analysis API — `QueryEngine`, `QueryResultRenderer`, +`QueryResultExporter`, and their shared result model — over an already-loaded +`SysmlWorkspace`; see `docs/design/sysml2-tools-core/query.md`. The Io subsystem provides +`GlobFileCollector`, a shared file glob pattern resolver used by the Tool project's `lint`, +`render`, and `query` commands; it has no dependency on the SysML semantic model. ```mermaid flowchart TD @@ -41,13 +45,21 @@ flowchart TD Theme RenderOptions end + subgraph Query + QueryEngine + QueryResultRenderer + QueryResultExporter + end subgraph Io GlobFileCollector end Language --> FilterExpressionParser Language --> FilterExpressionEvaluator Language --> DiagramRenderer + Language --> QueryEngine Stdlib --> DiagramRenderer + QueryEngine --> QueryResultRenderer + QueryResultRenderer --> QueryResultExporter DiagramRenderer --> FilterExpressionParser DiagramRenderer --> FilterExpressionEvaluator DiagramRenderer --> ILayoutStrategy @@ -90,7 +102,8 @@ flowchart TD ## Dependencies - **DemaConsulting.SysML2Tools.Language** — provides `WorkspaceLoader`, `SysmlWorkspace`, - `SysmlNode` and all subtypes used by `DiagramRenderer` for pattern-matching view declarations. + `SysmlNode` and all subtypes used by `DiagramRenderer` for pattern-matching view declarations, + and by `QueryEngine` for public model-analysis queries. - **DemaConsulting.SysML2Tools.Stdlib** — provides `StdlibProvider.GetSymbolTable()` used by `DiagramRenderer` to seed the semantic workspace with the pre-compiled standard library. @@ -146,9 +159,21 @@ N/A — not a safety-classified software item. expose-scoped candidate definitions by reading their directly-owned `SysmlMetadataNode` children; when parsing fails, layout falls back to the unfiltered scope with a warning. +### Query Data Flow + +1. A library caller supplies an already-loaded `SysmlWorkspace`, `QueryOptions`, and (for + element-scoped verbs) a resolved `SysmlNode` to `QueryEngine.Execute` or a verb-specific + method. +2. `QueryEngine` reads the semantic workspace's declarations, reverse index, resolved edges, + and child nodes to build a uniform `QueryResult` for the requested verb. +3. `QueryResultRenderer` sorts the result's entries once and renders Markdown or JSON; the + `dependencies` verb's Markdown path additionally shortens shared qualified-name prefixes via + `QualifiedNameShortener`. +4. `QueryResultExporter` optionally persists the rendered output to a caller-chosen file path. + ## Design Constraints - Platform: multi-targets net8.0, net9.0, and net10.0 on Windows, Linux, and macOS. - SysML v2 parsing, semantic analysis, and standard library are provided by the Language and - Stdlib assemblies; the Core assembly contains only Filtering, Layout, Rendering, and Io - concerns. + Stdlib assemblies; the Core assembly contains only Filtering, Layout, Rendering, Query, and + Io concerns. diff --git a/docs/design/sysml2-tools-core/query.md b/docs/design/sysml2-tools-core/query.md new file mode 100644 index 0000000..5d7a663 --- /dev/null +++ b/docs/design/sysml2-tools-core/query.md @@ -0,0 +1,194 @@ +## DemaConsulting.SysML2Tools — Query Subsystem + +### Overview + +The Query subsystem is Core's public, reusable model-analysis API. It answers 12 fixed +questions over an already-loaded `SysmlWorkspace` and returns a uniform `QueryResult` that +callers can render as Markdown or JSON or write to a file. The Tool project's `query` +command is now only one caller of this subsystem; CLI-only behavior is documented in +`docs/design/sysml2-tools-tool/query.md`. + +This subsystem contains the following public types: + +- `QueryVerb` / `QueryVerbParsing` — the fixed 12-verb vocabulary, token conversion, and the + `RequiresElement` rule. +- `QueryOptions` — the immutable option record shared by every verb; Core callers supply an + already-loaded workspace, so this record has no input-files property. +- `QueryArgumentParser` — parses a token list into `(QueryOptions? Options, + IReadOnlyList Files)` for callers that accept the same verb/option grammar as the + CLI. +- `QueryEngine` — the public execution surface: 12 verb methods plus `Execute`, which + centralizes the verb switch used by both library callers and the CLI adapter. +- `QueryResult`, `QueryResultEntry`, and `QueryEntryDirection` — the verb-agnostic result + model. +- `QueryResultRenderer` — the shared Markdown/JSON rendering and deterministic sorting layer. +- `QueryResultSerializerContext` — the source-generated `System.Text.Json` context used by + `RenderJson`. +- `QueryResultExporter` — synchronous and asynchronous file-writing wrappers around the + renderer. +- `Utilities.QualifiedNameShortener` — the shared prefix-stripping helper used only by the + `dependencies` verb's Markdown rendering. +- `NamespaceDoc` — the XML-documentation anchor for the `DemaConsulting.SysML2Tools.Query` + namespace and its public usage pattern. + +### Interfaces + +```mermaid +flowchart TD + Caller --> QueryArgumentParser + QueryArgumentParser --> QueryOptions + QueryOptions --> QueryEngine + QueryEngine --> SemanticIndex + QueryEngine --> SysmlNode + QueryEngine --> QueryResult + QueryResult --> QueryResultRenderer + QueryResult --> QueryResultExporter + QueryResultRenderer --> QueryResultSerializerContext + QueryResultRenderer --> QualifiedNameShortener +``` + +**`QueryArgumentParser`**: Shared token parser for query-style callers. + +- *Type*: Static class. +- *Role*: Provider. +- *Contract*: `Parse(IReadOnlyList commandArgs, bool helpRequested)` returns a + `(QueryOptions? Options, IReadOnlyList Files)` tuple. `Files` are returned + separately because file-glob interpretation is a caller concern, not a Core concern. + +**`QueryEngine`**: Workspace analysis entry point. + +- *Type*: Static class. +- *Role*: Provider. +- *Contract*: `Execute(SysmlWorkspace workspace, QueryOptions options, SysmlNode? element)` + dispatches to one of 12 public verb methods. Stateless and thread-safe for concurrent + read-only use. + +**`QueryResult` / `QueryResultEntry` / `QueryEntryDirection`**: Uniform result envelope. + +- *Type*: Sealed records plus enum. +- *Role*: Data container. +- *Contract*: `Verb`, optional `Element`, free-form `Summary` lines, and deterministic + `Entries`; `Direction` is populated only for `dependencies`. + +**`QueryResultRenderer`**: Result-to-text renderer. + +- *Type*: Static class. +- *Role*: Provider. +- *Contract*: `RenderMarkdown(QueryResult, int depth = 1, string? heading = null)` returns + Markdown lines; `RenderJson(QueryResult)` returns one indented JSON string. + +**`QueryResultExporter`**: Result-to-file writer. + +- *Type*: Static class. +- *Role*: Provider. +- *Contract*: `WriteMarkdown`/`WriteMarkdownAsync` and `WriteJson`/`WriteJsonAsync` write the + exact renderer output to a caller-specified file path. + +**`QualifiedNameShortener`**: Shared Markdown compaction helper. + +- *Type*: Static class. +- *Role*: Provider. +- *Contract*: `Shorten(IReadOnlyList qualifiedNames)` returns an ordinal-keyed map + from original qualified names to their shortened forms, retaining every name's leaf segment. + +### Design + +1. `QueryArgumentParser` validates that the first token is one of the 12 verb tokens, captures + `--element`/`--format`/`--walk-depth`/`--direction`/`--kind`/`--name`/ + `--include-stdlib`/`--heading`, and returns any trailing positional tokens separately as the + caller-owned `Files` list. +2. `QueryEngine.Execute` is the public dispatcher. It validates the element-required rule for + library callers, then routes `QueryOptions.Verb` to the matching verb method so the Tool + project and any other caller reuse the exact same switch. +3. `Uses` reads outgoing semantic edges from `workspace.Index.GetOutgoingEdges`, filters + stdlib targets unless `IncludeStdlib` is set, and emits one `QueryResultEntry` per outgoing + relationship. +4. `UsedBy` reads the reverse index via `workspace.Index.GetIncomingEdges`, applies the same + stdlib filter to incoming sources, and emits one entry per incoming relationship. +5. `Dependencies` combines `Uses` and `UsedBy` for the same subject, tagging entries with + `QueryEntryDirection.Outgoing` or `Incoming` rather than performing a third traversal. +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. + 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 + outgoing `Supertype` edges, `down` follows incoming edges, and `both` unions the two walks. +9. `Requirements` reports `Satisfy`, `Verify`, and `Allocate` edges where the subject element is + either the source or the target, using direction-aware detail text. +10. `Interface` reports direct child features that are ports or have a resolved typing, using + the feature keyword as `Kind` and typing plus multiplicity as `Detail`. +11. `Connections` walks nodes reachable from workspace declarations because resolved `Connect` + edges live on each connector node's own `ResolvedEdges` rather than in `SemanticIndex`'s + global edge list. Entries record the other endpoint, connector keyword, and endpoint role. +12. `States` recursively walks descendants, collecting `state` features and transition children. + When a resolved `Transition` edge is present it uses that edge's endpoints; otherwise it + falls back to the transition node's raw source/target text. +13. `List` and `Find` enumerate `workspace.Declarations`, applying the same `IncludeStdlib` + visibility rule and optional `--kind`/`--name` substring filters. `Find` reuses `List`'s + filtering behavior; the Tool project owns the CLI-only rule that at least one filter must be + 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 + `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 + filesystem-exception translation is performed in Core. + +#### Output Model + +- `QueryResult` carries `Verb`, optional `Element`, free-form `Summary` lines, and `Entries`. +- `QueryResultEntry` carries `QualifiedName`, optional `Kind`, optional `Detail`, optional + multi-line `Notes`, and optional `Direction`. +- `QueryEntryDirection` is populated only for `dependencies`, and JSON omits the property when + it is `null`. +- `QueryResultSerializerContext` contains the source-generated JSON metadata for `QueryResult` + and preserves the same sorted order used by Markdown rendering. + +#### Known Model Gaps + +- State-usage bodies containing `accept then ;` trigger-shorthand transitions + still inherit a pre-existing grammar and `AstBuilder` limitation: the shorthand transition can + absorb an adjacent sibling `state` usage instead of producing its own `SysmlTransitionNode`. + Explicit `transition first X if G then Y;` syntax is unaffected. The Query subsystem reports + the model it receives; it does not attempt to repair this upstream semantic-model gap. + +### Design Constraints + +- The subsystem depends only on the loaded semantic model (`SysmlWorkspace`, `SemanticIndex`, + `SysmlNode`, and resolved `SysmlEdge` data). It does not resolve globs, load files, create a + workspace, or perform console I/O. +- `QueryOptions` intentionally has no `Files` property. Core callers provide a workspace + directly, while `QueryArgumentParser` returns trailing positional tokens separately for + callers that want CLI-style parsing. +- `QueryResultExporter` does not create parent directories and does not catch + `IOException`/`UnauthorizedAccessException`/`NotSupportedException`; callers own those + policies. +- Deterministic ordering is centralized in `QueryResultRenderer`; verb methods may emit entries + in traversal order without affecting external output stability. +- `QualifiedNameShortener` affects only `dependencies` Markdown output. JSON output, and every + other verb's Markdown output, remains fully qualified. + +### Requirements Traceability + +| Requirement ID | Satisfied by | +| --- | --- | +| SysML2Tools-Core-Query-Uses | `QueryEngine.Uses` | +| SysML2Tools-Core-Query-UsedBy | `QueryEngine.UsedBy` | +| SysML2Tools-Core-Query-Dependencies | `QueryEngine.Dependencies`; `QueryResultRenderer.RenderMarkdown`/`RenderJson` | +| SysML2Tools-Core-Query-DependenciesNameShortening | `QueryResultRenderer.RenderMarkdown`; `QualifiedNameShortener` | +| SysML2Tools-Core-Query-Impact | `QueryEngine.Impact` | +| SysML2Tools-Core-Query-Describe | `QueryEngine.Describe` | +| SysML2Tools-Core-Query-Hierarchy | `QueryEngine.Hierarchy` | +| SysML2Tools-Core-Query-Requirements | `QueryEngine.Requirements` | +| SysML2Tools-Core-Query-Interface | `QueryEngine.Interface` | +| SysML2Tools-Core-Query-Connections | `QueryEngine.Connections`; `QueryEngine.CollectConnectEdges` | +| SysML2Tools-Core-Query-States | `QueryEngine.States`; `QueryEngine.CollectStates` | +| SysML2Tools-Core-Query-List | `QueryEngine.List` | +| SysML2Tools-Core-Query-Find | `QueryEngine.Find` | +| SysML2Tools-Core-Query-StdlibFilter | `QueryEngine.IsVisible` | +| SysML2Tools-Core-Query-Exporter | `QueryResultExporter.WriteMarkdown`/`WriteJson` (sync and async variants) | diff --git a/docs/design/sysml2-tools-tool/cli/context.md b/docs/design/sysml2-tools-tool/cli/context.md index 750ee20..1dad041 100644 --- a/docs/design/sysml2-tools-tool/cli/context.md +++ b/docs/design/sysml2-tools-tool/cli/context.md @@ -89,6 +89,13 @@ flag name is shared), `WalkDepth` (impact-walk depth, command-scoped, parsed fro `--walk-depth`), `Heading` (custom Markdown heading text from `--heading`), and the query-specific `Files` list. +**QueryOutput**: `string?` — Path supplied after `query`'s `--output`, or `null` when not +supplied (output goes to stdout). Parsed by `QueryCliArgumentParser` alongside `Query` and +`QueryFiles`. Kept as its own property, independent of `Render.OutputDirectory`, because +query's `--output` names a single output **file**, whereas render's `--output` names an output +*directory* for per-view files; read directly by `QueryCommand.RunAsync` to decide whether to +write to stdout or to the named file via `QueryResultExporter`. + **HelpCommand**: `HelpOptions?` — populated only when `Command` is `SysmlCommand.Help`. Named `HelpCommand` (not `Help`) to avoid colliding with the existing `Help` flag property, which reflects the global `-h`/`-?`/`--help` flag independently of which command token was recognized. @@ -112,12 +119,15 @@ switches on `GlobalArguments.Command` to dispatch to exactly one per-command par - `SysmlCommand.Lint` → `LintArgumentParser.Parse(global.CommandArgs)` → `Lint`. - `SysmlCommand.Render` → `RenderArgumentParser.Parse(global.CommandArgs)` → `Render`. -- `SysmlCommand.Query` → `QueryArgumentParser.Parse(global.CommandArgs, global.Help)` - → `Query`. The `query` grammar is **structural**: the first token after the `query` command - token must be a recognized verb (validated eagerly via `QueryVerbParsing.Parse`, which lists all - valid tokens on failure); when no verb token is present, parsing returns `null` if `--help` was - requested, otherwise throws a clear `ArgumentException` ("query: a verb is required...") rather - than silently leaving `Query` null. +- `SysmlCommand.Query` → `QueryCliArgumentParser.Parse(global.CommandArgs, global.Help)` + → `Query`, `QueryFiles`, and `QueryOutput`. The `query` grammar is **structural**: the first + token after the `query` command token must be a recognized verb (validated eagerly via + `QueryVerbParsing.Parse`, which lists all valid tokens on failure); when no verb token is + present, parsing returns `null` if `--help` was requested, otherwise throws a clear + `ArgumentException` ("query: a verb is required...") rather than silently leaving `Query` + null. `QueryCliArgumentParser` also pre-scans for the Tool-only `--output ` flag, + populating `QueryOutput` before delegating the remaining grammar to Core's public + `QueryArgumentParser`. - `SysmlCommand.None` → no per-command parser runs; any leftover `-`-prefixed token in `global.CommandArgs` throws `ArgumentException("Unsupported argument '{arg}'")`, preserving the historical bare-invocation error behavior. @@ -180,8 +190,9 @@ available. the one deliberate piece of DRY sharing across parsers; command scoping/dispatch itself is not shared. - **`LintOptions`/`LintArgumentParser`**, **`RenderCommandOptions`/`RenderArgumentParser`**, - **`QueryOptions`/`QueryArgumentParser`**, **`HelpOptions`/`HelpArgumentParser`** — the - per-command option records and parsers dispatched to by `Create`. + **`QueryOptions`/`QueryCliArgumentParser`**, **`HelpOptions`/`HelpArgumentParser`** — the + per-command option records and parsers dispatched to by `Create`. `QueryCliArgumentParser` + additionally owns parsing `QueryOutput`, which has no equivalent Core `QueryOptions` property. #### Callers diff --git a/docs/design/sysml2-tools-tool/query.md b/docs/design/sysml2-tools-tool/query.md index b2e10c3..b2959be 100644 --- a/docs/design/sysml2-tools-tool/query.md +++ b/docs/design/sysml2-tools-tool/query.md @@ -1,382 +1,123 @@ -### DemaConsulting.SysML2Tools.Tool — Query Subsystem - -#### Overview - -The Query subsystem implements the `query` CLI command: a model-analysis interface exposing -12 verbs (`uses`, `used-by`, `dependencies`, `impact`, `describe`, `hierarchy`, -`requirements`, `interface`, `connections`, `states`, `list`, `find`) over a SysML v2 -workspace. It provides five -cooperating types: - -- `QueryVerb` — an enum identifying which of the 12 operations was requested, plus a - `QueryVerbParsing` helper that converts between kebab-case command-line tokens - (e.g., `used-by`) and enum values, and reports which verbs require a target element. -- `QueryOptions` — an immutable record capturing every verb-specific option (`Element`, - `Format`, `WalkDepth`, `Direction`, `Kind`, `NameFilter`, `IncludeStdlib`, `Heading`, - `Files`) parsed by `Context.Create` for one `query` invocation. -- `QueryCommand` — the entry-point dispatcher, mirroring `LintCommand`/`RenderCommand`'s - `internal static class` shape with a `RunAsync(Context)` method, plus `PrintGeneralHelp` - and `PrintVerbHelp` for `--help` rendering. Loads the workspace, resolves the target - element, dispatches to `QueryEngine`, and renders the result via `QueryResultRenderer`. -- `QueryEngine` — a stateless static class with one public method per verb, each taking - `(SysmlWorkspace workspace, SysmlNode element, QueryOptions options)` (for `list`/`find`, - `element` is an unused placeholder since those verbs operate workspace-wide) and returning - a `QueryResult`. -- `QueryResult`/`QueryResultEntry`/`QueryResultRenderer`/`QueryResultSerializerContext` — the - shared, verb-agnostic output model and rendering layer (see **Output Model** below). - -#### Verb Semantics - -Every verb reads the semantic model built by the `Semantic`/`Semantic.Model` types -(`SemanticIndex`, `SysmlNode.ResolvedEdges`, `SysmlNode.Children`) rather than re-parsing or -re-resolving anything; `QueryEngine` is a pure read-only consumer of the workspace built by -`WorkspaceLoader.LoadAsync`. - -| Verb | Element required | Data source | -| --- | --- | --- | -| `uses` | yes | `Index.GetOutgoingEdges(qn)` | -| `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, 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` | -| `connections` | yes | `QueryEngine.CollectConnectEdges` node-walk (see below) | -| `states` | yes | Recursive descendant walk (`QueryEngine.CollectStates`, see below) | -| `list` | no | `workspace.Declarations`, filtered by `--kind`/`--name` | -| `find` | no | Same as `list`, but requires at least one of `--kind`/`--name` | - -Entry shapes, one row per verb: - -- `uses`/`used-by`: other-side qn, `Kind` = edge kind label, `Detail` = other side's kind. -- `dependencies`: the merged `uses` (outgoing) + `used-by` (incoming) entries, each tagged - with `Direction` (`Outgoing`/`Incoming`); no `Summary` lines (the prose rendering supplies - 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/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. -- `interface`: feature qn, `Kind` = `FeatureKeyword`, `Detail` = typing + multiplicity. -- `connections`: `Connect` edges are **not** exposed via `SemanticIndex.AllEdges` - (feature-chain resolution only mutates the originating connector node's own - `ResolvedEdges`, per `ReferenceResolver`'s design), so `QueryEngine.CollectConnectEdges` - walks every node reachable from `Declarations`, collecting each node's own resolved - `Connect` edges together with its connector keyword (`connect`/`connection`/`message`). - Entries: other-endpoint qn, `Kind` = connector keyword, `Detail` = `"A"`/`"B"` role. -- `states`: a recursive descendant walk (`QueryEngine.CollectStates`) collects - `SysmlFeatureNode` entries with `FeatureKeyword == "state"` and `SysmlTransitionNode` - children (using the transition's own resolved `Transition` edge when present, else its - raw `Source`/`Target` text). States: qn, `Kind` = `"state"`. Transitions: target qn, - `Kind` = `"transition"`, `Detail` = `"{source} -> {target}"` (+ `" if {guard}"`). -- `list`/`find`: qn, `Kind` = element's kind. - -Every verb applies the `--include-stdlib` filter identically via `IsVisible` (checks -`workspace.StdlibNames.Contains(qualifiedName)`), and every entry is emitted unsorted by -`QueryEngine` — deterministic alphabetical-by-qualified-name ordering is applied exactly -once, downstream, in `QueryResultRenderer` (see **Output Model**). - -##### Known Model Gaps - -- **State-usage bodies with `accept X then Y` trigger-shorthand transitions**: a state usage - body item consisting of an accept-triggered transition (`accept SomeSignal then target;`, - with no explicit `first`/`transition` keyword) can, per the current ANTLR grammar/AST - builder, silently absorb a preceding sibling `state` usage instead of producing its own - `SysmlTransitionNode`. Plain `state x;` declarations and explicit - `transition first x if guard then y;` successions are unaffected and fully supported. This - is a pre-existing gap in the grammar/`AstBuilder` (predates this unit) that the `states` - verb inherits; it is out of scope for this unit to fix (see this unit's completion report - for the reproduction and analysis). - -#### Output Model - -##### QueryResult / QueryResultEntry Purpose - -A single, uniform result shape used by all 12 verbs so that `QueryResultRenderer` never -needs verb-specific rendering logic (except the one `dependencies`-only branch documented -below), and so Markdown/JSON output are always structurally identical. - -##### QueryResult / QueryResultEntry Data Model - -- `QueryResult`: `Verb` (string token), `Element` (qualified name, or `null` for - `list`/`find`), `Summary` (`IReadOnlyList`, free-form header lines), `Entries` - (`IReadOnlyList`). -- `QueryResultEntry`: `QualifiedName`, `Kind`, `Detail` (`string?`), `Notes` - (`IReadOnlyList`, currently unused by any verb but reserved for future - multi-line annotations), `Direction` (`QueryEntryDirection?`, `[JsonIgnore(Condition = - JsonIgnoreCondition.WhenWritingNull)]`) — populated only by `dependencies` - (`Outgoing`/`Incoming`); `null` for every other verb, and omitted from JSON output - entirely when `null` (rather than serialized as `"Direction": null`), so adding this - field does not change any other verb's JSON output shape. -- `QueryEntryDirection`: a plain enum, `Outgoing`/`Incoming`, defined alongside - `QueryResult`/`QueryResultEntry`. - -##### QueryResultRenderer Purpose - -The single point of Markdown/JSON rendering and the single point of deterministic ordering, -so no verb implementation can accidentally produce out-of-order or format-inconsistent -output. - -##### QueryResultRenderer Key Methods - -**`RenderMarkdown(QueryResult, int depth = 1, string? heading = null)`**: -Returns `IReadOnlyList` lines — a `new string('#', depth)`-prefixed heading -(default one `#`) whose text is either `heading` (when supplied, verbatim, with no -merging of verb/element information) or the auto-generated `query [: ]` text, -followed by the `Summary` lines as a bullet list, then either `_No entries._` or a Markdown -table (`| Qualified Name | Kind | Detail |`) of `SortEntries`' output. `depth` is sourced from -the global `--depth` flag (`Context.HeadingDepth`, shared with the `--validate` self-report -heading; pre-validated to the range 1-6 by `GlobalArgumentParser`); `heading` is sourced from -`query`'s own `--heading` flag (parsed by `QueryArgumentParser`). Both are Markdown output -only; no effect on `RenderJson`. - -**`dependencies`-only rendering path**: after the heading (and empty `Summary`), when -`result.Verb == "dependencies"`, `RenderMarkdown` first builds the combined pool -`[Element] + sortedEntries.Select(QualifiedName)` and calls -`Utilities.QualifiedNameShortener.Shorten` on it, producing a map from each original qualified -name to its shortened form (see `docs/design/sysml2-tools-tool/utilities/qualified-name-shortener.md` -for the stripping algorithm). It then branches to a private -`RenderDependenciesBody(sortedEntries, element, shortened)` helper instead of the table logic -above — the only per-verb branch in an otherwise verb-agnostic method. It splits the (already -`SortEntries`-sorted) entries by `Direction`, preserving each group's ordinal order, and emits -(using the shortened form of `Element` and every entry's `QualifiedName`): - -```text -{Element} references the following elements: - -- Depends on **{QualifiedName}** ({Kind}) -... - -The following elements reference {Element}: - -- Used by **{QualifiedName}** ({Kind}) -... +## DemaConsulting.SysML2Tools.Tool — Query Subsystem + +### Overview + +The Tool Query subsystem is now a thin CLI adapter over Core's public Query API. It owns only +CLI-specific concerns: parsing `query` command tokens, extracting the Tool-only `--output` +flag, resolving file globs, loading the workspace, resolving `--element`, writing stdout or a +single output file, and printing localized help text. Verb semantics, the shared `QueryResult` +output model, deterministic Markdown/JSON rendering, JSON serialization, and the file-writing +helpers themselves live in `docs/design/sysml2-tools-core/query.md`. + +This subsystem contains three cooperating types: + +- `QueryCliArgumentParser` — pre-scans for the Tool-only `--output ` flag, removes those + two tokens, then delegates the remaining grammar to Core's `QueryArgumentParser`. +- `QueryCommand` — the CLI entry point. It validates CLI-only rules, resolves globs, loads the + workspace, looks up the target element, delegates execution to `QueryEngine.Execute`, and then + either writes rendered output to stdout or to `Context.QueryOutput`. +- `QueryStrings` — the culture-aware `.resx` accessor used by `PrintGeneralHelp` and + `PrintVerbHelp`. + +### Interfaces + +```mermaid +flowchart TD + Context --> QueryCliArgumentParser + QueryCliArgumentParser --> QueryArgumentParser + Context --> QueryCommand + QueryCommand --> GlobFileCollector + QueryCommand --> WorkspaceLoader + QueryCommand --> QueryEngine + QueryCommand --> QueryResultRenderer + QueryCommand --> QueryResultExporter + QueryCommand --> QueryStrings ``` -When the outgoing group is empty, the intro sentence and bullet list are replaced by a -single `"{Element} has no outgoing references."` line; when the incoming group is empty, -by a single `"No elements reference {Element}."` line. No table, and no `"_No entries._"` -fallback, are ever emitted for `dependencies`. `RenderJson` needs no special-casing for -`dependencies`: the `Direction` field already round-trips through the existing -`QueryResultSerializerContext` serialization once the property exists on -`QueryResultEntry`, and `RenderJson` never calls `QualifiedNameShortener` — its -`QualifiedName`/`Element` values remain fully qualified for every verb, including -`dependencies`. - -**`RenderJson(QueryResult)`**: Returns an indented JSON string via -`JsonSerializer.Serialize(sortedResult, QueryResultSerializerContext.Default.QueryResult)`, -where `sortedResult` is a copy of the input with `Entries` replaced by `SortEntries`' output -— guaranteeing the same ordering as `RenderMarkdown` for the same `QueryResult`. - -**`SortEntries(IReadOnlyList)`** (private): `OrderBy(e => e.QualifiedName, -StringComparer.Ordinal)` — the single, shared sort used by both render methods. - -##### QueryResultSerializerContext Purpose - -Mirrors `AstSerializerContext`'s AOT-safe `System.Text.Json` source-generation pattern for -the Tool assembly: `[JsonSerializable(typeof(QueryResult))]` with -`[JsonSourceGenerationOptions(WriteIndented = true)]`. - -#### QueryVerb / QueryVerbParsing - -##### QueryVerb Purpose - -Defines the fixed, ROADMAP-specified vocabulary of 12 query verbs and centralizes the -mapping between command-line tokens and the enum, so no other type re-implements token -parsing or formatting. - -##### QueryVerb Data Model - -`QueryVerb` is a plain enum with no instance state. `QueryVerbParsing` is a static class -exposing `AllTokens` (the ordered list of valid tokens, used in error messages and help -text). - -##### QueryVerb Key Methods - -**`Parse(string token)`**: Converts a kebab-case token to a `QueryVerb`; throws -`ArgumentException` listing all valid tokens when the token is unrecognized. - -**`ToToken(QueryVerb verb)`**: Converts a `QueryVerb` back to its kebab-case token, used by -`QueryCommand`'s stub message and help text. - -**`RequiresElement(QueryVerb verb)`**: Returns `true` for every verb except `List` and -`Find`, which operate over the whole workspace rather than a single target element. - -#### QueryOptions - -##### QueryOptions Purpose - -A single, flat, immutable record carrying every option relevant to any of the 12 verbs. -A shared shape (rather than one record per verb) keeps `Context.Create`'s construction -logic simple, since all 12 verbs share the same CLI grammar (verb token, then options, -then file globs) and differ only in which options are meaningful. - -##### QueryOptions Data Model - -- `Verb` (`QueryVerb`, required) — which operation was requested. -- `Element` (`string?`) — target qualified name from `--element`/`-e`; required for every - verb except `list`/`find`. -- `Format` (`string?`) — `"markdown"` (default) or `"json"` from `--format`; reuses the same - flag name as `render`'s `--format` (which instead accepts `svg`/`png`) — the two commands - interpret the raw string independently. -- `WalkDepth` (`int?`) — impact-walk depth bound from `--walk-depth`, meaningful only for - `impact`; command-scoped (parsed by `QueryArgumentParser`, unrelated to `render`'s own - `--walk-depth` flag). -- `Direction` (`string?`) — `up`/`down`/`both` from `--direction`, meaningful only for - `hierarchy`. -- `Kind` (`string?`) — element-kind filter from `--kind`, meaningful only for `list`/`find`. -- `NameFilter` (`string?`) — name substring filter from `--name`, meaningful only for - `list`/`find`. -- `IncludeStdlib` (`bool`) — from `--include-stdlib`; applies to every verb. -- `Heading` (`string?`) — custom Markdown heading text from `--heading`, replacing the - auto-generated `query [: ]` text; `null` means the auto-generated text is - used. Markdown output only; no effect on `--format json`. The Markdown heading *depth* - (number of leading `#` characters) is a separate concept, read directly from the global - `Context.HeadingDepth` (`--depth`) by `QueryCommand.RunAsync`, not stored on `QueryOptions`. -- `Files` (`IReadOnlyList`) — file glob patterns supplied after the verb token; kept - separate from `Context.Lint`/`Context.Render`'s file lists (used by `lint`/`render`) so - query's file handling cannot affect the other commands. - -#### QueryCommand - -##### QueryCommand Purpose - -`QueryCommand.RunAsync` validates that a verb was successfully parsed and that -`--element` was supplied when required, loads the workspace, resolves the target element, -dispatches to `QueryEngine`, and renders the result via `QueryResultRenderer`. It also -exposes `PrintGeneralHelp`/`PrintVerbHelp` for `Program`'s help handling. - -##### QueryCommand Key Methods - -**`RunAsync(Context context)`** - -1. Throws `ArgumentException` if `context.Query` is `null` (defensive; unreachable when - `Program` dispatches correctly, since `Context.Create` only sets `Command = Query` and - `Query = null` together when `--help` was requested without a verb). -2. Throws `ArgumentException` naming the verb when `QueryVerbParsing.RequiresElement` is - `true` for the verb and `Element` is null/whitespace. -3. Throws `ArgumentException` when `Verb == Find` and neither `Kind` nor `NameFilter` is - supplied (mirrors the `--element`-required validation style). -4. Throws `ArgumentException` when `Format` is neither `null`, `"markdown"`, nor `"json"` - (case-insensitive). -5. `WriteError`s "no input files" and returns (exit code 1) when `options.Files` is empty — - matching `lint`/`render`'s convention. -6. Resolves `options.Files` to concrete file paths via `GlobFileCollector.Collect(options.Files, - [".sysml", ".kerml"], Directory.GetCurrentDirectory())` (`DemaConsulting.SysML2Tools.Io`, Core - `Io` subsystem) — the same shared resolver used by `lint`/`render`. `WriteError`s - `"query {token}: no files matched the given pattern(s)."` and returns when the pattern list - resolved to zero files. -7. Loads the workspace via `StdlibProvider.GetSymbolTable()` + - `WorkspaceLoader.LoadAsync(files, stdlibTable)`, reporting diagnostics via - `WriteLine`/`WriteError` exactly like `RenderCommand`; `WriteError`s "workspace loading - failed" and returns if `Workspace` is `null`. -8. For verbs requiring an element, looks up `workspace.Declarations.TryGetValue(element, - out node)`; `WriteError`s `"query {token}: element '{element}' not found in the - workspace."` and returns (exit code 1) when missing. -9. Dispatches via an explicit 12-arm `switch` on `options.Verb` — one arm per verb, not a - loop or dictionary — to the matching `QueryEngine` method. -10. Renders the resulting `QueryResult` via `QueryResultRenderer.RenderMarkdown`/`RenderJson` - and writes each line/the JSON string via `context.WriteLine`. - -**`PrintGeneralHelp(Context context)`**: Lists all 12 verbs with a one-line description and -the shared option set; used for `query --help` with no verb. Also prints a "typical -workflow" note recommending `list`/`find` first to discover exact qualified names before -using an element-scoped verb, since 10 of the 12 verbs require `--element`. - -**`PrintVerbHelp(Context context, QueryVerb verb)`**: Prints a verb-specific usage line and -only the options relevant to that verb, followed by one real example invocation (drawn from -the bundled `test/SysMLModels/OMG/examples/VehicleExample/*.sysml` fixture, or explicitly -marked "illustrative" for the two verbs — `requirements`, `states` — that fixture has no -matching content for) and the shared Markdown/JSON output-shape schema hint (identical for -every verb, since all 12 share one `QueryResult`/`QueryResultRenderer`); used for -`query --help`. - -#### Localization / Resource Strings - -Every line printed by `PrintGeneralHelp`/`PrintVerbHelp` (including the verb list, options, -the "typical workflow" note, the per-verb example invocations, and the schema hints) is -sourced from `QueryStrings`, a hand-written, culture-aware `ResourceManager` accessor over -`Query/QueryStrings.resx` — see `docs/design/sysml2-tools-tool/program.md`'s "Localization / -Resource Strings" section for the rationale and the zero-code-change future-locale story, -which applies identically here. The 12 per-verb example-invocation keys -(`Query_Example_Uses` … `Query_Example_Find`) are each backed by their own -`public static string` property (so the resx-key/accessor-property parity check in -`ResxResourceTests` needs no special-casing), and are additionally exposed through a single -`QueryStrings.GetExample(QueryVerb)` switch-expression helper so `PrintVerbHelp` only needs -one call site instead of a 12-arm switch of its own. - -#### Error Handling - -- `context.Query is null`: `ArgumentException` (defensive; should not occur via `Program`). -- `--element` required but missing: `ArgumentException` naming the verb token. -- `find` with neither `--kind` nor `--name`: `ArgumentException`. -- Unsupported `--format` value: `ArgumentException` naming the bad value. -- Unrecognized verb token: `ArgumentException` (thrown by `QueryVerbParsing.Parse`, called - from `Cli.QueryArgumentParser`) listing all valid tokens. -- No input files: `context.WriteError`; `Context.ExitCode` becomes 1. -- Patterns given but none matched any files: `context.WriteError` reports - `"query {token}: no files matched the given pattern(s)."`; `Context.ExitCode` becomes 1. -- Workspace failed to load: `context.WriteError`; `Context.ExitCode` becomes 1. -- Target element not found in the workspace: `context.WriteError` naming the element; - `Context.ExitCode` becomes 1. - -#### Dependencies - -- `Context`, `SysmlCommand` (in `DemaConsulting.SysML2Tools.Cli`) — reads `Query` options; - writes output. -- `GlobFileCollector` (in `DemaConsulting.SysML2Tools.Io`) — resolves `options.Files` glob - patterns to concrete file paths before loading the workspace. -- `WorkspaceLoader`, `StdlibProvider`, `SysmlWorkspace`, `SemanticIndex`, `SysmlNode` and - derived types (in `DemaConsulting.SysML2Tools.Semantic`/`.Internal`) — workspace loading - and the model read by `QueryEngine`. -- `Utilities.QualifiedNameShortener` (in `DemaConsulting.SysML2Tools.Utilities`) — - `QueryResultRenderer.RenderMarkdown`'s `dependencies`-only branch calls `Shorten` on the - combined subject-plus-entries pool before rendering. - -#### Callers - -- `Program.RunToolLogicAsync` — dispatches to `QueryCommand.RunAsync` when - `context.Command == SysmlCommand.Query`. -- `Program.RunAsync` — dispatches to `QueryCommand.PrintGeneralHelp`/`PrintVerbHelp` for the - `--help` case. - -#### Requirements Traceability +**`QueryCliArgumentParser`**: Tool-only wrapper around the Core parser. + +- *Type*: Static class. +- *Role*: Provider. +- *Contract*: `Parse(IReadOnlyList commandArgs, bool helpRequested)` returns the parsed + `QueryOptions`, trailing file-glob patterns, and optional output-file path. + +**`QueryCommand`**: CLI command orchestrator. + +- *Type*: Static class. +- *Role*: Provider. +- *Contract*: `RunAsync(Context)` executes the command; `PrintGeneralHelp` and `PrintVerbHelp` + render localized help. + +**`QueryStrings`**: Localized help-text accessor. + +- *Type*: Static class. +- *Role*: Provider. +- *Contract*: `ResourceManager`-backed properties expose every general-help line, + verb-specific option line, example invocation, and schema hint printed by `QueryCommand`. + +### Design + +1. `Context.Create` delegates the `query` command's remaining tokens to + `QueryCliArgumentParser.Parse`. `QueryCliArgumentParser` strips the Tool-only + `--output ` pair, delegates the rest to Core's `QueryArgumentParser`, and stores the + results on `Context.Query`, `Context.QueryFiles`, and `Context.QueryOutput`. +2. `QueryCommand.RunAsync` performs the CLI-only validation that remains outside Core: + `--element` is required for element-scoped verbs, `find` requires at least one of + `--kind`/`--name`, and `--format` must be `markdown` or `json`. +3. The command resolves `Context.QueryFiles` through `GlobFileCollector.Collect`, then loads the + semantic workspace with `StdlibProvider.GetSymbolTable()` and `WorkspaceLoader.LoadAsync`. +4. For verbs that require a target element, `QueryCommand` resolves `QueryOptions.Element` + through `workspace.Declarations.TryGetValue`. Missing elements are reported as a CLI error; + Core never receives a `null` element from this path. +5. The command delegates verb execution to `QueryEngine.Execute`, so the CLI and every library + caller share the same public Core dispatcher and verb implementations. +6. `QueryCommand` renders results through `QueryResultRenderer.RenderMarkdown` or + `RenderJson`. Markdown heading depth still comes from the global `Context.HeadingDepth` + option, and custom heading text still comes from `QueryOptions.Heading`. +7. `PrintGeneralHelp` and `PrintVerbHelp` remain the single source of truth for `query --help` + and `query --help`. Every printed line is sourced from `QueryStrings`, including the + `--output` help text, the workflow note, and the per-verb example/schema-hint enrichment. + +#### Output File Option + +- `Context.QueryOutput` is a **file path** (not a directory) from `--output`; when omitted, + output goes to stdout. This differs in meaning from `render`'s `--output`, which names an + output *directory* for per-view files — the flag name is reused deliberately (avoiding a + differently-named flag for "where does output go") but the meaning is documented explicitly + in both the resource text and help output to prevent confusion between the two commands. +- When `Context.QueryOutput` is non-null, `QueryCommand.RunAsync` creates the parent directory, + then calls `QueryResultExporter.WriteMarkdownAsync` or `WriteJsonAsync` instead of writing to + stdout. +- Write failures are caught at the CLI boundary. `IOException`, `UnauthorizedAccessException`, + and `NotSupportedException` are translated to + `query : failed to write output file '': `. +- On success, the command reports `query : wrote output to ''.`. + +### Design Constraints + +- The Tool subsystem shall not re-implement verb semantics, result rendering, JSON + serialization, or Markdown-only name shortening; those are single-source-of-truth Core + responsibilities documented in `docs/design/sysml2-tools-core/query.md`. +- `QueryCliArgumentParser` is intentionally aware of `--output`; Core's public + `QueryArgumentParser` is intentionally unaware of it. +- Workspace loading, glob resolution, element lookup, and console/log/file reporting remain in + the Tool layer because they depend on `Context`, filesystem conventions, and CLI error + phrasing. + +### Requirements Traceability | Requirement ID | Satisfied by | | --- | --- | -| SysML2Tools-Tool-Query-VerbGrammar | `Cli.QueryArgumentParser` verb-first parsing; `QueryVerbParsing.Parse` | -| SysML2Tools-Tool-Context-QueryVerbRequired | `Cli.QueryArgumentParser`'s required-verb check | -| SysML2Tools-Tool-Query-UnknownVerb | `QueryVerbParsing.Parse`'s `ArgumentException` path | +| SysML2Tools-Tool-Query-VerbGrammar | `QueryCliArgumentParser.Parse`; Core `QueryArgumentParser.Parse` | +| SysML2Tools-Tool-Query-UnknownVerb | Core `QueryVerbParsing.Parse`, surfaced by `QueryCliArgumentParser.Parse` | | SysML2Tools-Tool-Query-ElementRequired | Element-required check at the start of `QueryCommand.RunAsync` | -| SysML2Tools-Tool-Query-Format | `QueryOptions.Format`; `QueryResultRenderer.RenderMarkdown`/`RenderJson` | +| SysML2Tools-Tool-Query-Format | `QueryCommand.RunAsync` format validation | | SysML2Tools-Tool-Query-NoFilesMatched | Zero-resolved-files guard in `QueryCommand.RunAsync` | -| SysML2Tools-Tool-Query-Help | `QueryCommand.PrintGeneralHelp`/`PrintVerbHelp`, called from `Program.RunAsync` | -| SysML2Tools-Tool-Query-Uses | `QueryEngine.Uses` | -| SysML2Tools-Tool-Query-UsedBy | `QueryEngine.UsedBy` | -| SysML2Tools-Tool-Query-Dependencies | `QueryEngine.Dependencies`, `QueryResultRenderer`'s `dependencies` branch | -| SysML2Tools-Tool-Query-DependenciesNameShortening | `Utilities.QualifiedNameShortener.Shorten` | -| SysML2Tools-Tool-Query-Impact | `QueryEngine.Impact` | -| SysML2Tools-Tool-Query-Describe | `QueryEngine.Describe` | -| SysML2Tools-Tool-Query-Hierarchy | `QueryEngine.Hierarchy` | -| SysML2Tools-Tool-Query-Requirements | `QueryEngine.Requirements` | -| SysML2Tools-Tool-Query-Interface | `QueryEngine.Interface` | -| SysML2Tools-Tool-Query-Connections | `QueryEngine.Connections`, `QueryEngine.CollectConnectEdges` | -| SysML2Tools-Tool-Query-States | `QueryEngine.States`, `QueryEngine.CollectStates` | -| SysML2Tools-Tool-Query-List | `QueryEngine.List` | -| SysML2Tools-Tool-Query-Find | `QueryEngine.Find` | -| SysML2Tools-Tool-Query-StdlibFilter | `QueryEngine.IsVisible` | +| SysML2Tools-Tool-Query-OutputFormat | `QueryCommand.RunAsync`; `QueryResultRenderer.RenderMarkdown`/`RenderJson` | | SysML2Tools-Tool-Query-ElementNotFound | Element-lookup check in `QueryCommand.RunAsync` | -| SysML2Tools-Tool-Query-OutputFormat | `QueryResultRenderer.RenderMarkdown`/`RenderJson`/`SortEntries` | -| SysML2Tools-Tool-Query-LocalizableHelpText | `QueryStrings` accessor, used by `PrintGeneralHelp`/`PrintVerbHelp` | -| SysML2Tools-Tool-Query-HelpEnrichment | `QueryStrings.GetExample`/`SchemaHint_*`, used by `PrintVerbHelp` | -| SysML2Tools-Tool-Query-ReportHeading | `QueryOptions.Heading`, `Cli.Context.HeadingDepth`, `RenderMarkdown` | +| SysML2Tools-Tool-Query-Find | `QueryCommand.RunAsync`'s `find`-filter validation; Core `QueryEngine.Find` dispatch | +| SysML2Tools-Tool-Query-Help | `QueryCommand.PrintGeneralHelp`; `QueryCommand.PrintVerbHelp` | +| SysML2Tools-Tool-Query-LocalizableHelpText | `QueryStrings` accessor, used by `PrintGeneralHelp` and `PrintVerbHelp` | +| SysML2Tools-Tool-Query-ReportHeading | `HeadingDepth`; `QueryOptions.Heading`; `QueryResultRenderer.RenderMarkdown` | +| SysML2Tools-Tool-Query-HelpEnrichment | `QueryStrings.GetExample`/`Query_SchemaHint_*`; workflow-note lines | +| SysML2Tools-Tool-Query-OutputFile | `QueryCliArgumentParser.Parse`; `QueryCommand.RunAsync`; `QueryResultExporter` | diff --git a/docs/design/sysml2-tools-tool/utilities.md b/docs/design/sysml2-tools-tool/utilities.md index f4b89b8..80ac3ce 100644 --- a/docs/design/sysml2-tools-tool/utilities.md +++ b/docs/design/sysml2-tools-tool/utilities.md @@ -3,11 +3,14 @@ ### Overview The `Utilities` subsystem provides shared utility functions for the SysML2 Tools. It -supplies reusable, independently testable helpers consumed by other subsystems. Its -responsibilities are safe file-path manipulation (protecting callers from path-traversal -vulnerabilities when constructing paths from caller-supplied inputs) and qualified-name -compaction for Markdown-oriented rendering. The `Utilities` subsystem contains two units: -`PathHelpers` and `QualifiedNameShortener`. +supplies reusable, independently testable helpers consumed by other subsystems. Its sole +responsibility is safe file-path manipulation (protecting callers from path-traversal +vulnerabilities when constructing paths from caller-supplied inputs). The `Utilities` +subsystem contains one unit: `PathHelpers`. + +> Qualified-name compaction for Markdown-oriented rendering (`QualifiedNameShortener`) moved +> to the `DemaConsulting.SysML2Tools.Core` library's `Query` subsystem alongside the rest of +> the public query API — see `docs/design/sysml2-tools-core/query.md`. ### Interfaces @@ -23,30 +26,13 @@ that escapes the base directory. when the combined path escapes the base directory; may propagate `NotSupportedException` or `PathTooLongException` from underlying BCL path operations. -**QualifiedNameShortener.Shorten**: Strips the longest shared leading `::`-segment prefix from a -pool of qualified names, always retaining each name's own leaf segment. - -- *Type*: In-process .NET static method. -- *Role*: Provider. -- *Contract*: Accepts `IReadOnlyList qualifiedNames`. Returns an - `IReadOnlyDictionary` mapping each distinct original name to its shortened - form, stripping the longest run of leading segments common to every name in the pool, capped - so every name always keeps at least its own final segment. -- *Constraints*: Throws `ArgumentNullException` when the pool or any contained name is `null`. - Returns an identity map (no stripping) when the pool has fewer than 2 distinct names, or when - the names share no common leading segment. - ### Design -The `Utilities` subsystem contains the `PathHelpers` and `QualifiedNameShortener` units. Neither -unit depends on the other, and neither depends on any other tool unit or subsystem; both use -only .NET BCL types (`Path`/`ArgumentNullException` for `PathHelpers`; `string.Split`/ -`string.Join`/`ArgumentNullException` for `QualifiedNameShortener`). - -`PathHelpers.SafePathCombine` and `QualifiedNameShortener.Shorten` are both pure utility -methods: they perform no file-system I/O, hold no state, and throw immediately on invalid -input. All calls to `SafePathCombine` in the codebase originate from the `SelfTest` subsystem -(`Validation`), which uses it to construct log and result file paths inside temporary -directories created during self-validation test execution. The sole caller of -`QualifiedNameShortener.Shorten` is the `Query` subsystem's `QueryResultRenderer`, which applies -it only to the `dependencies` verb's Markdown-only prose rendering. +The `Utilities` subsystem contains the `PathHelpers` unit. It does not depend on any other +tool unit or subsystem; it uses only .NET BCL types (`Path`/`ArgumentNullException`). + +`PathHelpers.SafePathCombine` is a pure utility method: it performs no file-system I/O, holds +no state, and throws immediately on invalid input. All calls to `SafePathCombine` in the +codebase originate from the `SelfTest` subsystem (`Validation`), which uses it to construct +log and result file paths inside temporary directories created during self-validation test +execution. diff --git a/docs/design/sysml2-tools-tool/utilities/qualified-name-shortener.md b/docs/design/sysml2-tools-tool/utilities/qualified-name-shortener.md deleted file mode 100644 index 5fe36a8..0000000 --- a/docs/design/sysml2-tools-tool/utilities/qualified-name-shortener.md +++ /dev/null @@ -1,55 +0,0 @@ -### QualifiedNameShortener - -#### Purpose - -`QualifiedNameShortener` is a static utility class that strips the longest leading `::`-segment -prefix shared by every name in a supplied pool of qualified names. Its single responsibility is -to make a set of related qualified names more compact for Markdown-oriented display, without -ever discarding a name's own distinguishing leaf segment. - -#### Data Model - -`QualifiedNameShortener` holds no instance state. The class is `internal static` with no fields -or properties other than the private `SegmentSeparator` constant (`"::"`). - -#### Key Methods - -**Shorten**: Computes a shortened form of every supplied qualified name. - -- *Parameters*: `IReadOnlyList qualifiedNames` — the pool of qualified names to shorten - together. -- *Returns*: `IReadOnlyDictionary` — a map from each distinct original name - (ordinal key comparison) to its shortened form. -- *Preconditions*: `qualifiedNames` and every contained name are non-null. -- *Postconditions*: Every name in the pool keeps at least its own final ("::"-delimited) leaf - segment; when fewer than 2 distinct names are supplied, or the names share no common leading - segment, every value equals its key unchanged. - -Algorithm: (1) reject a null pool or a null entry via `ArgumentNullException.ThrowIfNull`; (2) -reduce the pool to its distinct names (`Distinct(StringComparer.Ordinal)`); (3) if fewer than 2 -distinct names remain, return an identity map (skip stripping entirely — there is nothing to -compare a lone name against); (4) split every distinct name into "::"-segments; (5) compute the -common-prefix length via the private `ComputeCommonPrefixLength` helper, which caps the search at -the shortest name's segment count minus 1 (so no name can ever be stripped down to nothing) and -walks segment indices until the first mismatch (ordinal comparison) across every name; (6) when -the computed length is 0, return an identity map; otherwise return a map of each distinct name to -`string.Join("::", segments[commonPrefixLength..])`. - -#### Error Handling - -`Shorten` throws `ArgumentNullException` when `qualifiedNames` itself is `null`, or when any -contained name is `null`. No other exception is thrown; a pool sharing no common prefix, or -containing only one distinct name, is a normal (non-error) input handled by returning an -identity map. - -#### Dependencies - -- **.NET BCL** — `string.Split`, `string.Join`, `Enumerable.Distinct`/`Min`, and - `ArgumentNullException` are the only dependencies. No other tool units or subsystems are used. - -#### Callers - -- **Query** — `QueryResultRenderer.RenderMarkdown`'s `dependencies`-only rendering branch calls - `QualifiedNameShortener.Shorten` on the pool `[Element] + Entries.Select(QualifiedName)` before - rendering the subject sentence and bullets, so Markdown output for a related set of names is - more compact. No other verb, and no JSON output, calls this utility. diff --git a/docs/reqstream/sysml2-tools-core.yaml b/docs/reqstream/sysml2-tools-core.yaml index c13dc0a..14781c2 100644 --- a/docs/reqstream/sysml2-tools-core.yaml +++ b/docs/reqstream/sysml2-tools-core.yaml @@ -3,12 +3,14 @@ # # PURPOSE: # - Define system-level requirements for the DemaConsulting.SysML2Tools core library -# - Core library provides: Layout, Rendering interfaces, DiagramRenderer +# - Core library provides: Layout, Rendering interfaces, DiagramRenderer, and the public Query API # - Parsing and semantic model are in DemaConsulting.SysML2Tools.Language # - Standard library is in DemaConsulting.SysML2Tools.Stdlib # - Requirements describe observable system behavior (WHAT the library produces), # not the internal orchestration API. System-level evidence comes from the # end-to-end rendering integration tests. +# - Query subsystem requirements are defined separately in +# docs/reqstream/sysml2-tools-core/query.yaml. sections: - title: SysML2ToolsLibrary Requirements diff --git a/docs/reqstream/sysml2-tools-core/query.yaml b/docs/reqstream/sysml2-tools-core/query.yaml new file mode 100644 index 0000000..057cd68 --- /dev/null +++ b/docs/reqstream/sysml2-tools-core/query.yaml @@ -0,0 +1,238 @@ +--- +# Query Subsystem Requirements +# +# PURPOSE: +# - Define requirements for the SysML2Tools Core Query subsystem +# - The Query subsystem exposes a public, reusable model-analysis API over an already-loaded +# SysML semantic workspace +# - QueryEngine implements the 12 analysis verbs; QueryResult/QueryResultRenderer/ +# QueryResultSerializerContext implement the shared output model; QueryResultExporter writes +# rendered output to files +# - Requirements describe observable library behavior, not CLI parsing or console reporting + +sections: + - title: Query Subsystem Requirements + requirements: + - id: SysML2Tools-Core-Query-Uses + title: >- + The Query subsystem shall report a target element's outgoing semantic relationships + through the `uses` verb, including supertypes, typing, imports, and other resolved + outward references, excluding stdlib targets unless `IncludeStdlib` is supplied. + justification: | + "What does this element depend on?" is a foundational model-comprehension question. + Exposing the semantic index's outgoing-edge view directly answers it without forcing + library callers to traverse the semantic model manually. + tests: + - Uses_ReportsOutgoingSupertypeTypingImportEdges + + - id: SysML2Tools-Core-Query-UsedBy + title: >- + The Query subsystem shall report a target element's incoming semantic relationships + through the `used-by` verb by using the semantic index's reverse lookup. + justification: | + The reverse of `uses` is required for change-impact assessment and traceability review; + callers need a first-class answer to "what depends on this element?". + tests: + - UsedBy_ReportsIncomingReferences + + - id: SysML2Tools-Core-Query-Dependencies + title: >- + The Query subsystem shall combine the target element's outgoing and incoming + relationships into one `dependencies` result, tagging each entry with + `Outgoing`/`Incoming` direction and rendering Markdown as direction-grouped prose + bullets rather than the shared table used by other verbs. + justification: | + A combined dependency view is a distinct output need from the separate `uses` and + `used-by` queries. Reusing both traversals while tagging entry direction avoids + duplicate analysis logic and keeps JSON and Markdown representations aligned. + tests: + - Dependencies_CombinesOutgoingAndIncoming_ReportsBothDirections + - Dependencies_NoOutgoingReferences_ReportsProseLineInsteadOfBulletList + - Dependencies_NoIncomingReferences_ReportsProseLineInsteadOfBulletList + - Dependencies_MarkdownOutput_ContainsNoTable + - RenderMarkdown_DependenciesVerb_RendersBulletProseNotTable + - RenderMarkdown_DependenciesVerb_EmptyOutgoingAndIncoming_ReportsBothProseLines + - RenderJson_DependenciesVerb_IncludesDirectionField + - RenderJson_NonDependenciesVerb_DirectionFieldOmittedFromOutput + + - id: SysML2Tools-Core-Query-DependenciesNameShortening + title: >- + The Query subsystem shall shorten qualified names only in the `dependencies` verb's + Markdown output by stripping the longest leading `::`-segment prefix shared by the + subject element and every dependency entry, while leaving JSON output fully qualified. + justification: | + Related dependency sets often share a long package prefix that adds no distinguishing + value in prose output. Applying the compaction only in the one Markdown rendering path + that needs it keeps every other verb and every JSON payload unaffected. + tests: + - RenderMarkdown_DependenciesVerb_NoCommonPrefix_LeavesNamesFullyQualified + - RenderJson_DependenciesVerb_NamesRemainFullyQualified + + - id: SysML2Tools-Core-Query-QualifiedNameShortening + title: >- + The `QualifiedNameShortener` helper shall strip the longest leading `::`-segment + prefix shared across a pool of qualified names, always capping the stripped length so + every name retains at least its own leaf segment, and shall reject a `null` pool or a + `null` pool entry. + justification: | + The prefix-shortening algorithm used by `DependenciesNameShortening` above has edge + cases (single-name pools, all-identical names, differing name lengths, duplicate + names, null inputs) that are best proven directly against the helper rather than only + through the `dependencies` verb's Markdown rendering. + tests: + - QualifiedNameShortener_Shorten_OneSharedLeadingSegment_StripsThatSegment + - QualifiedNameShortener_Shorten_NoCommonPrefix_LeavesNamesUnchanged + - QualifiedNameShortener_Shorten_SingleNamePool_LeavesNameUnchanged + - QualifiedNameShortener_Shorten_AllIdenticalNames_KeepsLeafSegment + - QualifiedNameShortener_Shorten_DeeperCommonPrefix_StripsAllSharedSegments + - QualifiedNameShortener_Shorten_ShortestNameBoundsCap_RetainsShortestNamesLeaf + - QualifiedNameShortener_Shorten_NullPool_ThrowsArgumentNullException + - QualifiedNameShortener_Shorten_NullEntryInPool_ThrowsArgumentNullException + - QualifiedNameShortener_Shorten_DuplicateNamesInPool_ReturnsOneEntryPerDistinctName + + - id: SysML2Tools-Core-Query-Impact + title: >- + The Query subsystem shall compute the transitive closure of incoming references from a + target element through the `impact` verb, optionally bounded by `WalkDepth` and guarded + against cycles. + justification: | + A single-hop reverse-reference view understates true blast radius for deeply-referenced + elements. A bounded transitive closure gives callers a practical, terminating answer to + "what is affected if this changes?". + tests: + - Impact_DepthOne_OnlyReachesDirectReferences + - Impact_Unbounded_ReachesTransitiveClosure + + - 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. + 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. + tests: + - 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-Core-Query-Hierarchy + title: >- + The Query subsystem shall report a target element's supertype and subtype hierarchy, + respecting the requested direction and guarding against recursive cycles. + justification: | + Specialization hierarchies are frequently deep and multi-branch. A dedicated traversal + gives callers a direct answer to "how is this element related by generalization?". + tests: + - Hierarchy_DirectionUp_ReportsOnlySupertypes + - Hierarchy_DirectionDown_ReportsOnlySubtypes + - Hierarchy_DirectionBoth_ReportsSupertypesAndSubtypes + - Hierarchy_GeneralizationExampleFixture_ReportsSupertypes + + - id: SysML2Tools-Core-Query-Requirements + title: >- + The Query subsystem shall report satisfy, verify, and allocate relationships where the + target element is either the source or the target. + justification: | + Requirements traceability is a core systems-engineering question. The public Query API + shall expose these resolved relationships directly from the semantic model. + tests: + - Requirements_ReportsSatisfyVerifyAndAllocateEdges + - Requirements_RequirementSatisfactionFixture_ReportsAtLeastOneRelationship + + - id: SysML2Tools-Core-Query-Interface + title: >- + The Query subsystem shall report a target definition's ports and typed features through + the `interface` verb. + justification: | + A definition's externally visible interface is what other elements connect to and type + against. Library callers need a direct way to inspect that boundary. + tests: + - Interface_ReportsPortsAndTypedFeatures + + - id: SysML2Tools-Core-Query-Connections + title: >- + The Query subsystem shall report a target element's resolved connection endpoints, + including dotted feature-chain resolution, and shall label each entry with the + connector keyword that produced it. + justification: | + Resolved connection edges do not live in the semantic index's global edge list, so the + Query subsystem must perform the node walk on behalf of callers that need connection + topology. + tests: + - Connections_ReportsResolvedEndpointsIncludingFeatureChain + - Connections_ConnectionsExampleFixture_ReportsAtLeastOneEndpoint + + - id: SysML2Tools-Core-Query-States + title: >- + The Query subsystem shall report nested states and guarded transitions reachable from a + target element. + justification: | + State-machine structure is a distinct model dimension from hierarchy or containment. A + first-class `states` verb lets callers inspect that structure without manual descendant + traversal. + tests: + - States_ReportsStatesAndGuardedTransitions + - States_StateDecompositionFixture_ReportsStatesAndTransitions + + - id: SysML2Tools-Core-Query-List + title: >- + The Query subsystem shall enumerate workspace declarations through the `list` verb, + optionally filtering by kind substring and qualified-name substring, without requiring a + target element. + justification: | + Workspace-wide enumeration is the discovery step that lets callers find element names + before issuing an element-scoped query. + tests: + - List_NoFilters_ReturnsAllNonStdlibElements + - List_KindFilter_OnlyMatchesGivenKind + - List_NameFilter_OnlyMatchesGivenSubstring + + - id: SysML2Tools-Core-Query-Find + title: >- + The Query subsystem shall perform the same workspace-wide declaration search as `list` + through the `find` verb, applying the supplied kind and/or name filters without + requiring a target element. + justification: | + `find` is the targeted search form of workspace enumeration. Exposing it in Core keeps + search behavior reusable for non-CLI callers. + tests: + - Find_WithNameFilter_Succeeds + + - id: SysML2Tools-Core-Query-StdlibFilter + title: >- + Every verb shall exclude stdlib-seeded elements from its results by default, and + include them when `IncludeStdlib` is supplied on `QueryOptions`. + justification: | + Most model-analysis questions concern user-authored elements; hiding the large + stdlib surface by default keeps results focused, while `IncludeStdlib` supports the + rarer case of auditing library usage. + tests: + - Context_Create_QueryCommand_WithIncludeStdlibFlag_SetsIncludeStdlibTrue + - List_IncludeStdlib_TogglesStandardLibraryVisibility + + - id: SysML2Tools-Core-Query-Exporter + title: >- + The Query subsystem shall provide synchronous and asynchronous file-export helpers that + write the exact Markdown or JSON text produced by the shared renderer to a caller- + supplied file path, without creating parent directories and without suppressing write + failures. + justification: | + Library callers often want the same persisted output shape as the CLI without taking a + dependency on CLI conventions. Keeping the file writer thin preserves a reusable Core + API while leaving policy decisions, such as parent-directory creation and exception + translation, to the caller. + tests: + - WriteMarkdown_HappyPath_MatchesRendererOutput + - WriteMarkdown_WithDepthAndHeading_MatchesRendererOutput + - WriteMarkdownAsync_HappyPath_MatchesRendererOutput + - WriteJson_HappyPath_MatchesRendererOutput + - WriteJsonAsync_HappyPath_MatchesRendererOutput + - WriteMarkdown_MissingParentDirectory_PropagatesIoException + - WriteJson_MissingParentDirectory_PropagatesIoException diff --git a/docs/reqstream/sysml2-tools-tool/cli/context.yaml b/docs/reqstream/sysml2-tools-tool/cli/context.yaml index 737500c..65d4256 100644 --- a/docs/reqstream/sysml2-tools-tool/cli/context.yaml +++ b/docs/reqstream/sysml2-tools-tool/cli/context.yaml @@ -150,6 +150,20 @@ sections: - Context_Create_QueryCommand_WithoutHeading_LeavesHeadingNull - Context_Create_QueryCommand_WithHeadingFlag_SetsHeading + - id: SysML2Tools-Tool-Context-QueryOutputFile + title: >- + The Context class shall parse the query command's --output argument into the + Context.QueryOutput property, independent of render's OutputDirectory property. + justification: | + The query command's --output names a single output file (not a directory, unlike + render's --output), so it is kept as its own Context property rather than reusing + Render.OutputDirectory, letting QueryCommand write rendered results to a file + instead of stdout without any risk of colliding with render's output-directory + semantics. + tests: + - Context_Create_QueryCommand_WithOutputFlag_SetsQueryOutput + - Context_Create_QueryCommand_OutputWithoutValue_ThrowsArgumentException + - id: SysML2Tools-Tool-Context-CommandScoping title: >- The Context class shall dispatch argument parsing to a dedicated per-command parser diff --git a/docs/reqstream/sysml2-tools-tool/query.yaml b/docs/reqstream/sysml2-tools-tool/query.yaml index ecba399..44c5d17 100644 --- a/docs/reqstream/sysml2-tools-tool/query.yaml +++ b/docs/reqstream/sysml2-tools-tool/query.yaml @@ -3,12 +3,10 @@ # # PURPOSE: # - Define requirements for the DemaConsulting.SysML2Tools.Tool Query subsystem -# - QueryCommand implements the CLI query verb dispatcher (QueryVerb/QueryOptions support it) -# - QueryEngine implements the 12 verbs' real analysis logic against the semantic model -# - QueryResult/QueryResultRenderer/QueryResultSerializerContext implement the shared, -# verb-agnostic Markdown/JSON output layer -# - Requirements describe observable behavior of the query command's parsing, validation, -# dispatch, and per-verb analysis/output +# - QueryCommand and QueryCliArgumentParser implement the CLI query adapter over Core's public +# Query API +# - Requirements describe CLI parsing, validation, dispatch, help text, and stdout/file-output +# behavior, not the Core Query engine's verb semantics sections: - title: Query Subsystem Requirements @@ -82,31 +80,13 @@ sections: - id: SysML2Tools-Tool-Query-OutputFormat title: >- - Markdown and JSON output for the same query shall contain the same set of - qualified names in the same deterministic (alphabetical, ordinal-by-qualified-name) - order, regardless of which verb produced the result. + The query command shall dispatch the same Core query result through Markdown or JSON + rendering without changing the reported entry content or deterministic order. justification: | - A single shared renderer (QueryResultRenderer) sorts entries exactly once, so no - verb implementation can accidentally produce out-of-order or format-inconsistent - output; deterministic ordering is required for stable diffs and reproducible - AI-assisted analysis workflows. + The CLI is responsible for selecting the renderer and emitting the chosen text shape. + Stable ordering and content parity are required for reproducible scripted use. tests: - Query_MarkdownAndJsonFormats_AgreeOnEntryContentAndOrder - - RenderMarkdown_UnorderedEntries_SortsByQualifiedNameOrdinal - - RenderJson_UnorderedEntries_SortsByQualifiedNameOrdinal - - RenderJson_NonDependenciesVerb_DirectionFieldOmittedFromOutput - - - id: SysML2Tools-Tool-Query-StdlibFilter - title: >- - Every verb shall exclude stdlib-seeded elements from its results by default, and - include them when --include-stdlib is supplied. - justification: | - Most model-analysis questions concern user-authored elements; hiding the large - stdlib surface by default keeps output focused, while --include-stdlib supports the - rarer case of auditing library usage. - tests: - - Context_Create_QueryCommand_WithIncludeStdlibFlag_SetsIncludeStdlibTrue - - List_IncludeStdlib_TogglesStandardLibraryVisibility - id: SysML2Tools-Tool-Query-ElementNotFound title: >- @@ -118,192 +98,14 @@ sections: tests: - Query_ElementNotFound_ReportsErrorAndNonZeroExitCode - - id: SysML2Tools-Tool-Query-Uses - title: >- - The uses verb shall report the target element's outgoing edges (supertypes, - typing, imports, and connection/transition/allocation endpoints it originates), - excluding stdlib targets unless --include-stdlib is supplied. - justification: | - "What does this element depend on?" is a foundational model-comprehension question; - exposing the semantic index's outgoing-edge view directly answers it without manual - model inspection. - tests: - - Uses_ReportsOutgoingSupertypeTypingImportEdges - - - id: SysML2Tools-Tool-Query-UsedBy - title: >- - The used-by verb shall report the target element's incoming edges via the semantic - index's reverse index, excluding stdlib sources unless --include-stdlib is - supplied. - justification: | - "What depends on this element?" (the reverse of uses) is essential for assessing - the safety of changing or removing an element. - tests: - - UsedBy_ReportsIncomingReferences - - - id: SysML2Tools-Tool-Query-Dependencies - title: >- - The dependencies verb shall combine the uses (outgoing) and used-by (incoming) - results for the same target element into a single result, tagging each entry's - Direction field (Outgoing/Incoming) accordingly, and shall render its Markdown - output as prose bullets ("Depends on"/"Used by") grouped by direction rather than - the shared table used by every other verb; --depth/--heading shall apply to its - heading exactly as they do for every other verb. - justification: | - A combined, prose-rendered "what does this depend on, and what depends on it" - view is a distinct documentation-embedding need (e.g., a "Dependencies" section - in a generated design document) from the tabular verbs; reusing the uses/used-by - methods avoids duplicating the underlying edge-traversal logic, and confining the - Direction field's JSON serialization to only-when-non-null (via - JsonIgnore(WhenWritingNull)) keeps every other verb's JSON output unaffected. - tests: - - Dependencies_CombinesOutgoingAndIncoming_ReportsBothDirections - - Dependencies_NoOutgoingReferences_ReportsProseLineInsteadOfBulletList - - Dependencies_NoIncomingReferences_ReportsProseLineInsteadOfBulletList - - Dependencies_MarkdownOutput_ContainsNoTable - - RenderMarkdown_DependenciesVerb_RendersBulletProseNotTable - - RenderMarkdown_DependenciesVerb_EmptyOutgoingAndIncoming_ReportsBothProseLines - - RenderJson_DependenciesVerb_IncludesDirectionField - - RenderJson_NonDependenciesVerb_DirectionFieldOmittedFromOutput - - Dependencies_DepthAndHeadingOptions_ApplyToHeadingLikeOtherVerbs - - - id: SysML2Tools-Tool-Query-DependenciesNameShortening - title: >- - The dependencies verb's Markdown output shall shorten every name (the subject - element plus every entry's qualified name) by the longest leading "::"-segment - prefix shared across that combined pool, applied identically to the subject - sentence and both bullet groups, while its JSON output (and every other verb's - Markdown and JSON output) remains fully qualified and unaffected. - justification: | - A "Dependencies" prose section frequently relates several elements from the same - package hierarchy; repeating a long shared package prefix on every bullet and in - the subject sentence adds noise without adding information. Reusing the standalone - QualifiedNameShortener utility (see the Utilities subsystem) keeps this compaction - logic verb/format-agnostic and confined to the one Markdown-only rendering path - that already special-cases dependencies, so no other verb or output format is - affected. - tests: - - RenderMarkdown_DependenciesVerb_NoCommonPrefix_LeavesNamesFullyQualified - - RenderJson_DependenciesVerb_NamesRemainFullyQualified - - - id: SysML2Tools-Tool-Query-Impact - title: >- - The impact verb shall compute the transitive closure of used-by edges from the - target element, optionally bounded by --walk-depth, cycle-guarded against infinite - loops. - justification: | - A single-hop used-by view understates true blast radius for deeply-referenced - elements; a bounded transitive closure gives a practical, terminating answer to - "what breaks if I change this?". - tests: - - Impact_DepthOne_OnlyReachesDirectReferences - - Impact_Unbounded_ReachesTransitiveClosure - - - 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), 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 - exploration, consolidating information otherwise scattered across the raw source. - tests: - - 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: >- - The hierarchy verb shall report the target element's supertype/subtype tree, - respecting --direction (up/down/both), cycle-guarded against infinite loops. - justification: | - Generalization hierarchies are frequently deep and multi-branch; a dedicated - traversal answers "what is this related to via specialization?" without manual - recursive lookups. - tests: - - Hierarchy_DirectionUp_ReportsOnlySupertypes - - Hierarchy_DirectionDown_ReportsOnlySubtypes - - Hierarchy_DirectionBoth_ReportsSupertypesAndSubtypes - - Hierarchy_GeneralizationExampleFixture_ReportsSupertypes - - - id: SysML2Tools-Tool-Query-Requirements - title: >- - The requirements verb shall report satisfy/verify/allocate edges where the target - element is either the source or the target, labeled with edge direction. - justification: | - Requirements traceability (what satisfies/verifies/allocates to what) is a core - systems-engineering question that the requirements verb answers directly from the - semantic model built in unit 3. - tests: - - Requirements_ReportsSatisfyVerifyAndAllocateEdges - - Requirements_RequirementSatisfactionFixture_ReportsAtLeastOneRelationship - - - id: SysML2Tools-Tool-Query-Interface - title: >- - The interface verb shall report the target definition's ports and typed features. - justification: | - A definition's externally-visible interface (ports, typed features) is what other - elements connect to; surfacing it directly supports interface-compatibility - analysis. - tests: - - Interface_ReportsPortsAndTypedFeatures - - - id: SysML2Tools-Tool-Query-Connections - title: >- - The connections verb shall report the target element's resolved connection - endpoints (including dotted feature-chain resolution), labeled with the - connector's keyword (connect/connection/message). - justification: | - Connection topology is not directly visible in the semantic index's global edge - list (Connect edges live only on each connector node's own resolved edges); the - connections verb performs the necessary node walk so callers do not need to know - this implementation detail. - tests: - - Connections_ReportsResolvedEndpointsIncludingFeatureChain - - Connections_ConnectionsExampleFixture_ReportsAtLeastOneEndpoint - - - id: SysML2Tools-Tool-Query-States - title: >- - The states verb shall report the target element's nested states and their guarded - transitions. - justification: | - State machine structure (states and their transitions) is a distinct model - dimension from generalization/composition; the states verb answers "what states - and transitions exist here?" directly. - tests: - - States_ReportsStatesAndGuardedTransitions - - States_StateDecompositionFixture_ReportsStatesAndTransitions - - - id: SysML2Tools-Tool-Query-List - title: >- - The list verb shall enumerate workspace declarations, optionally filtered by - --kind (substring match on element kind) and --name (substring match on qualified - name), without requiring --element. - justification: | - Workspace-wide enumeration is a prerequisite for discovering element names before - querying them individually, and for kind-based inventories (e.g., "list every - requirement"). - tests: - - List_NoFilters_ReturnsAllNonStdlibElements - - List_KindFilter_OnlyMatchesGivenKind - - List_NameFilter_OnlyMatchesGivenSubstring - - id: SysML2Tools-Tool-Query-Find title: >- - The find verb shall perform the same workspace-wide search as list, but shall - require at least one of --kind/--name, reporting a clear error otherwise. + The query command shall require at least one of --kind or --name for the `find` + verb, and shall report a clear error otherwise. justification: | - find is intended as a targeted fuzzy/substring search, not an unfiltered dump; - requiring a filter prevents accidental whole-workspace output when a targeted - search was intended. + `find` is intended as a targeted search, not an unfiltered workspace dump; the + CLI shall reject an invocation that omits both filters. tests: - - Find_WithNameFilter_Succeeds - Find_WithoutFilter_ThrowsArgumentException - id: SysML2Tools-Tool-Query-Help @@ -346,11 +148,8 @@ sections: tests: - Context_Create_QueryCommand_WithoutHeading_LeavesHeadingNull - Context_Create_QueryCommand_WithHeadingFlag_SetsHeading - - RenderMarkdown_DefaultArguments_ProducesUnchangedTopLevelHeading - - RenderMarkdown_CustomDepth_UsesThatManyHeadingHashes - - RenderMarkdown_CustomHeading_ReplacesAutoGeneratedText - - RenderMarkdown_CustomDepthAndHeading_CombinesBothOverrides - QuerySubsystem_FormatJson_UnaffectedByDepthOrHeading + - Dependencies_DepthAndHeadingOptions_ApplyToHeadingLikeOtherVerbs - id: SysML2Tools-Tool-Query-HelpEnrichment title: >- @@ -367,3 +166,15 @@ sections: tests: - QuerySubsystem_QueryVerbHelp_MentionsExampleInvocationAndSchemaHints - QuerySubsystem_QueryHelp_NoVerb_MentionsTypicalWorkflow + + - id: SysML2Tools-Tool-Query-OutputFile + title: >- + The query command shall write its rendered document to the file path supplied via + --output when present, or to stdout otherwise; unlike the render command's + --output (an output directory), query's --output names a single output file. + justification: | + Query produces one report for one invocation, so a single output file path is the + natural shape; reusing the --output flag name keeps the CLI consistent with export + while documenting the different meaning from render. + tests: + - QuerySubsystem_OutputFlag_WritesToFileInsteadOfStdout diff --git a/docs/reqstream/sysml2-tools-tool/utilities.yaml b/docs/reqstream/sysml2-tools-tool/utilities.yaml index 72b284d..ae7cd86 100644 --- a/docs/reqstream/sysml2-tools-tool/utilities.yaml +++ b/docs/reqstream/sysml2-tools-tool/utilities.yaml @@ -3,8 +3,7 @@ # # PURPOSE: # - Define requirements for the SysML2Tools Utilities subsystem -# - The Utilities subsystem spans PathHelpers.cs (safe path-combination utilities) and -# QualifiedNameShortener.cs (qualified-name common-prefix stripping for Markdown display) +# - The Utilities subsystem provides PathHelpers.cs (safe path-combination utilities) # - Subsystem requirements describe the shared utility behavior available to all other subsystems sections: @@ -25,19 +24,3 @@ sections: - UtilitiesSubsystem_DirectoryCreationWorkflow_ValidPaths_CreatesDirectories - UtilitiesSubsystem_PathTraversalValidation_DangerousPaths_ThrowsException - UtilitiesSubsystem_AbsolutePathRejection_ThrowsException - - - id: Template-Utilities-QualifiedNameShortening - title: >- - The Utilities subsystem shall provide Markdown-only qualified-name shortening that - strips the longest common leading "::"-segment prefix shared across a pool of - qualified names, always retaining each name's own leaf segment. - justification: | - The Utilities subsystem provides shared, reusable helpers consumed by other - subsystems. Rendering paths (such as the query command's Markdown output) benefit - from a common, centrally-tested prefix-shortening helper rather than each renderer - re-implementing its own ad hoc logic; this requirement captures the subsystem-level - capability that the QualifiedNameShortener unit implements. - tests: - - QualifiedNameShortener_Shorten_OneSharedLeadingSegment_StripsThatSegment - - QualifiedNameShortener_Shorten_NoCommonPrefix_LeavesNamesUnchanged - - QualifiedNameShortener_Shorten_SingleNamePool_LeavesNameUnchanged diff --git a/docs/reqstream/sysml2-tools-tool/utilities/qualified-name-shortener.yaml b/docs/reqstream/sysml2-tools-tool/utilities/qualified-name-shortener.yaml deleted file mode 100644 index 93e2eff..0000000 --- a/docs/reqstream/sysml2-tools-tool/utilities/qualified-name-shortener.yaml +++ /dev/null @@ -1,31 +0,0 @@ ---- -# Software Unit Requirements for the QualifiedNameShortener Class -# -# The QualifiedNameShortener class strips the longest shared leading "::"-segment -# prefix from a pool of qualified names, always retaining each name's own leaf -# segment, so Markdown-oriented renderers can present related names more compactly. - -sections: - - title: QualifiedNameShortener Unit Requirements - requirements: - - id: Template-QualifiedNameShortener-CommonPrefix - title: >- - The QualifiedNameShortener class shall strip the longest leading "::"-segment - prefix shared by every name in a supplied pool of qualified names, always - retaining each name's own leaf segment. - justification: | - A pool of related qualified names (e.g., a query subject and its dependencies) - often shares a long common package prefix that adds no distinguishing value to - a reader; stripping that shared prefix - while never discarding a name's own - leaf segment - makes Markdown-oriented output more compact without losing - information. - tests: - - QualifiedNameShortener_Shorten_OneSharedLeadingSegment_StripsThatSegment - - QualifiedNameShortener_Shorten_NoCommonPrefix_LeavesNamesUnchanged - - QualifiedNameShortener_Shorten_SingleNamePool_LeavesNameUnchanged - - QualifiedNameShortener_Shorten_AllIdenticalNames_KeepsLeafSegment - - QualifiedNameShortener_Shorten_DeeperCommonPrefix_StripsAllSharedSegments - - QualifiedNameShortener_Shorten_ShortestNameBoundsCap_RetainsShortestNamesLeaf - - QualifiedNameShortener_Shorten_NullPool_ThrowsArgumentNullException - - QualifiedNameShortener_Shorten_NullEntryInPool_ThrowsArgumentNullException - - QualifiedNameShortener_Shorten_DuplicateNamesInPool_ReturnsOneEntryPerDistinctName diff --git a/docs/user_guide/introduction.md b/docs/user_guide/introduction.md index 7dfa23f..685bc63 100644 --- a/docs/user_guide/introduction.md +++ b/docs/user_guide/introduction.md @@ -493,6 +493,7 @@ the same order, so either format can be relied on for automated comparisons. | --- | --- | | `--element `, `-e ` | Qualified name of the target element; required for every verb except `list`/`find` | | `--format markdown\|json` | Output format (default: `markdown`); distinct from `render`'s `--format` (`svg`/`png`) | +| `--output ` | Write to this **file** (default: stdout); `render`'s `--output` is a *directory* instead | | `--walk-depth <#>` | Maximum impact-walk depth (`impact` verb only) | | `--direction up\|down\|both` | Traversal direction (`hierarchy` verb only) | | `--kind ` | Element-kind filter (`list`/`find` verbs only) | diff --git a/docs/verification/introduction.md b/docs/verification/introduction.md index d918ae9..95e330c 100644 --- a/docs/verification/introduction.md +++ b/docs/verification/introduction.md @@ -35,8 +35,12 @@ systems: - **DemaConsulting.SysML2Tools.Tests/** — unit tests for the Language and core systems - **Parser/** — Language system: parsing subsystem tests - **Semantic/** — Language system: semantic model subsystem tests + - **Filtering/** — core system: filtering subsystem tests - **Layout/** — core system: layout subsystem tests - **Rendering/** — core system: rendering subsystem tests + - **Io/** — core system: Io subsystem tests + - **Query/** — core system: Query subsystem tests + - **Utilities/** — core system: Utilities subsystem tests - **DemaConsulting.SysML2Tools.Tool.Tests/** — dotnet tool unit and integration tests ## Companion Artifact Structure diff --git a/docs/verification/sysml2-tools-core.md b/docs/verification/sysml2-tools-core.md index 269eb11..d90d40b 100644 --- a/docs/verification/sysml2-tools-core.md +++ b/docs/verification/sysml2-tools-core.md @@ -3,11 +3,13 @@ ## Verification Approach System-level verification for the `DemaConsulting.SysML2Tools` core library uses unit tests -in `DemaConsulting.SysML2Tools.Tests`. Tests exercise the Filtering, Layout, and Rendering -pipeline via `FilterExpressionParser`, `FilterExpressionEvaluator`, `DiagramRenderer`, and -`GeneralViewLayoutStrategy`, along with the shared `ExposeScopeResolver`-based expose-scoping -path exercised by all seven layout strategies. The xUnit v3 framework discovers and runs all -test methods; results are captured in TRX files consumed by ReqStream. +in `DemaConsulting.SysML2Tools.Tests`. Tests exercise the Filtering, Layout, Rendering, Io, +and Query subsystems via `FilterExpressionParser`, `FilterExpressionEvaluator`, +`DiagramRenderer`, `GeneralViewLayoutStrategy`, `GlobFileCollector`, `QueryEngine`, +`QueryResultRenderer`, and `QueryResultExporter`, along with the shared +`ExposeScopeResolver`-based expose-scoping path exercised by all seven layout strategies. +The xUnit v3 framework discovers and runs all test methods; results are captured in TRX files +consumed by ReqStream. ## Test Environment @@ -25,6 +27,11 @@ SDK installation. - Every layout strategy honors a view's resolved `expose` scope via the shared `ExposeScopeResolver` helper, including the no-`expose` fallback (rendering everything unchanged) and a multi-target `expose` union. +- `GlobFileCollector.Collect` resolves literal paths, recursive globs, exclusions, and + extension-filtered bare-`*` patterns to a stable deduplicated file list. +- `QueryEngine`, `QueryResultRenderer`, and `QueryResultExporter` produce stable, + deterministic query results, renderer output, and file-export behavior for the public Core + Query API. ## Test Scenarios @@ -34,3 +41,6 @@ Primary acceptance evidence is provided by: tests. - `RenderIntegrationTests` — end-to-end rendering tests with stdlib seed workspace. - `GeneralViewLayoutStrategyTests` — layout algorithm tests for general view diagrams. +- `GlobFileCollectorTests` — direct Io subsystem tests for shared file-pattern resolution. +- `QueryOmgFixtureTests`, `QueryRenderingTests`, and `QueryResultExporterTests` — direct Query + subsystem tests for public verb execution, deterministic rendering, and file export. diff --git a/docs/verification/sysml2-tools-core/query.md b/docs/verification/sysml2-tools-core/query.md new file mode 100644 index 0000000..d1a4ad1 --- /dev/null +++ b/docs/verification/sysml2-tools-core/query.md @@ -0,0 +1,246 @@ +## DemaConsulting.SysML2Tools — Query Subsystem Verification + +### Verification Approach + +The Query subsystem is verified by direct tests in +`test/DemaConsulting.SysML2Tools.Tests/Query/QueryOmgFixtureTests.cs`, +`QueryRenderingTests.cs`, and `QueryResultExporterTests.cs`, plus dedicated unit tests for the +`QualifiedNameShortener` helper in +`test/DemaConsulting.SysML2Tools.Tests/Utilities/QualifiedNameShortenerTests.cs`. These tests +call `QueryEngine`, `QueryResultRenderer`, `QueryResultExporter`, and `QualifiedNameShortener` +directly against loaded OMG fixture workspaces and hand-built `QueryResult` instances, so the +public Core API is verified without any dependency on the Tool project's CLI parsing or +`Context` I/O behavior. + +The `uses`, `used-by`, `dependencies`, `impact`, `interface`, `list`, `find`, and stdlib- +filtering behaviors are additionally exercised at the engine level from +`test/DemaConsulting.SysML2Tools.Tool.Tests/Query/QueryVerbsTests.cs` and, for the +`IncludeStdlib` option, `test/DemaConsulting.SysML2Tools.Tool.Tests/Cli/ContextTests.cs`. Those +tests call `Context.Create` and `Program.RunAsync`, but the assertions cited below verify the +public `QueryEngine`/`QueryOptions` result content itself, not CLI parsing or console +reporting. This is an intentional, documented choice to reuse the Tool project's existing +fixture setup for these verbs rather than duplicating it in the Core test project; it is not a +gap in Core-level verification. + +### Test Environment + +Tests run via `dotnet test` against net8.0, net9.0, and net10.0 in the +`DemaConsulting.SysML2Tools.Tests` project. OMG smoke tests read files under +`test/SysMLModels/OMG/` when present in the checkout. Exporter tests create and clean up their +own temporary files; no network access or external services are required. + +The additional `uses`/`used-by`/`dependencies`/`impact`/`interface`/`list`/`find`/stdlib-filter +scenarios listed under "Test Scenarios (Tool Test Project)" below run via `dotnet test` in the +`DemaConsulting.SysML2Tools.Tool.Tests` project instead, targeting net10.0 only. + +### Acceptance Criteria + +- All `QueryOmgFixtureTests`, `QueryRenderingTests`, `QueryResultExporterTests`, and + `QualifiedNameShortenerTests` pass with zero failures across all three target frameworks. +- The `uses`, `used-by`, `dependencies`, `impact`, `interface`, `list`, `find`, and stdlib- + filtering behaviors are proven at the `QueryEngine`/`QueryOptions` level by the + `Tool.Tests`-hosted scenarios cited below, in addition to the Core-project scenarios above; + not every verb's engine-level coverage is required to live in the Core test project. +- Representative real-world OMG fixtures produce non-empty, sensible `requirements`, + `connections`, `states`, `hierarchy`, and `describe` results through the public + `QueryEngine` API. +- Markdown and JSON rendering preserve the same deterministic, ordinal-by-qualified-name entry + ordering. +- The `dependencies` verb renders Markdown as prose bullets, shortens names only in Markdown, + retains fully qualified JSON output, and omits the `Direction` property from non- + `dependencies` JSON results. +- `QueryResultExporter` writes exactly the renderer's Markdown and JSON text and propagates + missing-parent-directory write failures instead of masking them. +- `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. + +### Test Scenarios + +**`Requirements_RequirementSatisfactionFixture_ReportsAtLeastOneRelationship`**: Loads the OMG +`RequirementSatisfaction.sysml` fixture, runs `QueryEngine.Requirements`, and verifies that at +least one satisfy/verify/allocate relationship is reported. + +**`Connections_ConnectionsExampleFixture_ReportsAtLeastOneEndpoint`**: Loads the OMG +`ConnectionsExample.sysml` fixture, runs `QueryEngine.Connections`, and verifies that at least +one resolved connection endpoint is reported. + +**`States_StateDecompositionFixture_ReportsStatesAndTransitions`**: Loads the OMG +`StateDecomposition-1.sysml` fixture, runs `QueryEngine.States`, and verifies that the public +API reports the reliably-produced state-transition content from that model. + +**`Hierarchy_GeneralizationExampleFixture_ReportsSupertypes`**: Loads the OMG +`GeneralizationExample.sysml` fixture, runs `QueryEngine.Hierarchy`, and verifies that resolved +supertypes are reported. + +**`Describe_CommentsFixture_ReportsAnnotations`**: Loads the OMG `Comments.sysml` fixture, runs +`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. + +**`RenderMarkdown_DependenciesVerb_RendersBulletProseNotTable`**: Verifies that +`dependencies` Markdown renders as grouped prose bullets rather than the shared table. + +**`RenderMarkdown_DependenciesVerb_NoCommonPrefix_LeavesNamesFullyQualified`**: Verifies that +`dependencies` Markdown leaves names fully qualified when the subject and entries share no +common leading `::`-segment prefix. + +**`RenderMarkdown_DependenciesVerb_EmptyOutgoingAndIncoming_ReportsBothProseLines`**: +Verifies that `dependencies` Markdown emits the two prose fallback lines when neither direction +contains entries. + +**`RenderMarkdown_UnorderedEntries_SortsByQualifiedNameOrdinal`**: Verifies that Markdown output +sorts entries ordinally by qualified name before rendering. + +**`RenderMarkdown_EntryWithDetailAndNotes_CombinesIntoOneCell`**: Verifies that Markdown table +rendering preserves `Detail` and `Notes` content together in the detail cell. + +**`RenderMarkdown_DefaultArguments_ProducesUnchangedTopLevelHeading`**: Verifies that the +default Markdown heading remains `# query [: ]`. + +**`RenderMarkdown_CustomDepth_UsesThatManyHeadingHashes`**: Verifies that Markdown heading depth +is controlled by the `depth` argument. + +**`RenderMarkdown_CustomHeading_ReplacesAutoGeneratedText`**: Verifies that a custom heading +replaces the auto-generated heading text. + +**`RenderMarkdown_CustomDepthAndHeading_CombinesBothOverrides`**: Verifies that custom heading +text and custom heading depth compose correctly. + +**`RenderJson_RoundTrips_PreservesShape`**: Verifies that JSON rendering round-trips the +`QueryResult` shape through `QueryResultSerializerContext`. + +**`RenderJson_UnorderedEntries_SortsByQualifiedNameOrdinal`**: Verifies that JSON output uses +that same deterministic ordinal entry order. + +**`RenderJson_DependenciesVerb_IncludesDirectionField`**: Verifies that `dependencies` JSON +includes the populated `Direction` field. + +**`RenderJson_DependenciesVerb_NamesRemainFullyQualified`**: Verifies that JSON output never +applies Markdown-only qualified-name shortening. + +**`RenderJson_NonDependenciesVerb_DirectionFieldOmittedFromOutput`**: Verifies that non- +`dependencies` JSON omits the `Direction` property entirely when it is `null`. + +**`WriteMarkdown_HappyPath_MatchesRendererOutput`**: Verifies that `WriteMarkdown` writes the +same Markdown text produced by `QueryResultRenderer.RenderMarkdown`. + +**`WriteMarkdown_WithDepthAndHeading_MatchesRendererOutput`**: Verifies that `WriteMarkdown` +passes custom heading depth and heading text through unchanged. + +**`WriteMarkdownAsync_HappyPath_MatchesRendererOutput`**: Verifies that `WriteMarkdownAsync` +writes the same Markdown text as the synchronous path. + +**`WriteJson_HappyPath_MatchesRendererOutput`**: Verifies that `WriteJson` writes the same JSON +string produced by `QueryResultRenderer.RenderJson`. + +**`WriteJsonAsync_HappyPath_MatchesRendererOutput`**: Verifies that `WriteJsonAsync` writes the +same JSON string as the synchronous path. + +**`WriteMarkdown_MissingParentDirectory_PropagatesIoException`**: Verifies that the Markdown +exporter does not create parent directories or suppress the resulting write failure. + +**`WriteJson_MissingParentDirectory_PropagatesIoException`**: Verifies that the JSON exporter +likewise propagates a missing-parent-directory write failure. + +#### QualifiedNameShortenerTests.cs + +**`QualifiedNameShortener_Shorten_OneSharedLeadingSegment_StripsThatSegment`**: The worked +example `["A::B::x", "A::B::y", "A::T::g"]` is shortened; the shared leading segment `"A"` is +stripped from every name, producing `["B::x", "B::y", "T::g"]`. + +**`QualifiedNameShortener_Shorten_NoCommonPrefix_LeavesNamesUnchanged`**: A pool of names +rooted in different top-level packages (`["A::B::x", "C::D::y"]`) is shortened; every name is +returned unchanged since no leading segment is shared. + +**`QualifiedNameShortener_Shorten_SingleNamePool_LeavesNameUnchanged`**: A pool containing only +one distinct name is shortened; the name is returned unchanged since there is nothing to +compare it against. + +**`QualifiedNameShortener_Shorten_AllIdenticalNames_KeepsLeafSegment`**: A pool where every +entry is the same name (`["A::B::x", "A::B::x", "A::B::x"]`) reduces to a single distinct name +and is returned unchanged, confirming the leaf segment `"x"` is never stripped down to an empty +string. + +**`QualifiedNameShortener_Shorten_DeeperCommonPrefix_StripsAllSharedSegments`**: A pool sharing +the two leading segments `"A::B"` (`["A::B::C::x", "A::B::C::y", "A::B::D::z"]`) is shortened; +both shared segments are stripped from every name. + +**`QualifiedNameShortener_Shorten_ShortestNameBoundsCap_RetainsShortestNamesLeaf`**: A pool +containing `"A::B"` (2 segments) alongside `"A::B::C"` (which shares the 2-segment prefix +`"A::B"`) is shortened; only 1 segment (`"A"`) is stripped, capped by `"A::B"`'s own segment +count, so `"A::B"` becomes `"B"` rather than an empty string. + +**`QualifiedNameShortener_Shorten_NullPool_ThrowsArgumentNullException`**: `null` is passed as +the `qualifiedNames` argument; an `ArgumentNullException` is thrown. + +**`QualifiedNameShortener_Shorten_NullEntryInPool_ThrowsArgumentNullException`**: A pool +containing a `null` entry is passed; an `ArgumentNullException` is thrown. + +**`QualifiedNameShortener_Shorten_DuplicateNamesInPool_ReturnsOneEntryPerDistinctName`**: A pool +where one name is repeated (`["A::B::x", "A::B::x", "A::T::g"]`) is shortened; the returned map +contains exactly one entry per distinct name, both correctly shortened. + +### Test Scenarios (Tool Test Project) + +The scenarios below prove `uses`, `used-by`, `dependencies`, `impact`, `interface`, `list`, +`find`, and stdlib-filter behavior against the public `QueryEngine`/`QueryOptions` API. They run +in `test/DemaConsulting.SysML2Tools.Tool.Tests/Query/QueryVerbsTests.cs` (and, for one +`IncludeStdlib` parsing scenario, `test/DemaConsulting.SysML2Tools.Tool.Tests/Cli/ContextTests.cs`) +rather than the Core test project, because they reuse the Tool project's existing inline +`.sysml` fixture helpers instead of duplicating that setup in Core. Tool-side CLI parsing, +dispatch, and console-reporting concerns for these same verbs are verified separately in +`docs/verification/sysml2-tools-tool/query.md`. + +#### QueryVerbsTests.cs + +**`Uses_ReportsOutgoingSupertypeTypingImportEdges`**: Verifies that `QueryEngine.Uses` reports a +target element's outgoing supertype, typing, and import edges. + +**`UsedBy_ReportsIncomingReferences`**: Verifies that `QueryEngine.UsedBy` reports elements that +reference the target through the semantic index's reverse lookup. + +**`Dependencies_CombinesOutgoingAndIncoming_ReportsBothDirections`**: Verifies that +`QueryEngine.Dependencies` combines outgoing and incoming relationships into one result, each +entry tagged with its direction. + +**`Dependencies_NoOutgoingReferences_ReportsProseLineInsteadOfBulletList`**: Verifies that +`dependencies` Markdown rendering falls back to a prose line when the target has no outgoing +references. + +**`Dependencies_NoIncomingReferences_ReportsProseLineInsteadOfBulletList`**: Verifies that +`dependencies` Markdown rendering falls back to a prose line when the target has no incoming +references. + +**`Impact_DepthOne_OnlyReachesDirectReferences`**: Verifies that `QueryEngine.Impact` with +`WalkDepth` of one reaches only directly-referencing elements. + +**`Impact_Unbounded_ReachesTransitiveClosure`**: Verifies that unbounded `QueryEngine.Impact` +reaches the full transitive closure of incoming references without looping on cycles. + +**`Interface_ReportsPortsAndTypedFeatures`**: Verifies that `QueryEngine.Interface` reports a +target definition's ports and typed features. + +**`List_NoFilters_ReturnsAllNonStdlibElements`**: Verifies that `QueryEngine.List` with no +filters returns every non-stdlib workspace declaration. + +**`List_KindFilter_OnlyMatchesGivenKind`**: Verifies that `QueryEngine.List` with a kind filter +returns only declarations whose kind matches the given substring. + +**`List_NameFilter_OnlyMatchesGivenSubstring`**: Verifies that `QueryEngine.List` with a name +filter returns only declarations whose qualified name contains the given substring. + +**`Find_WithNameFilter_Succeeds`**: Verifies that `QueryEngine.Find` applies a name filter over +the same workspace-wide search used by `list`. + +**`List_IncludeStdlib_TogglesStandardLibraryVisibility`**: Verifies that supplying +`IncludeStdlib` on `QueryOptions` causes `QueryEngine.List` to include stdlib-seeded elements +that are excluded by default. + +#### ContextTests.cs + +**`Context_Create_QueryCommand_WithIncludeStdlibFlag_SetsIncludeStdlibTrue`**: Verifies that the +`--include-stdlib` command-line flag sets `QueryOptions.IncludeStdlib` to `true`, which is the +CLI-side input that drives the `QueryEngine.List` stdlib-filtering behavior confirmed by +`List_IncludeStdlib_TogglesStandardLibraryVisibility` above. diff --git a/docs/verification/sysml2-tools-tool/cli/context.md b/docs/verification/sysml2-tools-tool/cli/context.md index d2f0fa5..7eeb0a9 100644 --- a/docs/verification/sysml2-tools-tool/cli/context.md +++ b/docs/verification/sysml2-tools-tool/cli/context.md @@ -207,6 +207,17 @@ called with `["query", "list", "*.sysml"]`; `Query.Files` contains `"*.sysml"` w separate from `lint`/`render`'s. This scenario is tested by `Context_Create_QueryCommand_WithFiles_SetsQueryFilesNotTopLevelFiles`. +**Context_Create_QueryCommand_WithOutputFlag_SetsQueryOutput**: `Context.Create` is called +with `["query", "list", "--output", "output/path.md"]`; `Context.QueryOutput` equals +`"output/path.md"`, confirming query's `--output` is parsed into its own `Context` property +independent of `Render.OutputDirectory`. This scenario is tested by +`Context_Create_QueryCommand_WithOutputFlag_SetsQueryOutput`. + +**Context_Create_QueryCommand_OutputWithoutValue_ThrowsArgumentException**: `Context.Create` +is called with `["query", "list", "--output"]` (value missing); an `ArgumentException` +containing `"--output"` is thrown. This scenario is tested by +`Context_Create_QueryCommand_OutputWithoutValue_ThrowsArgumentException`. + **Context_Create_LintCommand_OutOfScopeAutoFlag_ThrowsArgumentException**: `Context.Create` is called with `["lint", "--auto", "file.sysml"]`; an `ArgumentException` is thrown naming both `--auto` and `lint`, confirming `lint` rejects flags belonging to other commands instead of diff --git a/docs/verification/sysml2-tools-tool/query.md b/docs/verification/sysml2-tools-tool/query.md index 971ef3c..7a64a7e 100644 --- a/docs/verification/sysml2-tools-tool/query.md +++ b/docs/verification/sysml2-tools-tool/query.md @@ -1,324 +1,175 @@ -### DemaConsulting.SysML2Tools.Tool — Query Subsystem Verification +## DemaConsulting.SysML2Tools.Tool — Query Subsystem Verification -#### Verification Approach +### Verification Approach -The Query subsystem is verified using unit/integration tests in five files under -`test/DemaConsulting.SysML2Tools.Tool.Tests/Query/` (`QuerySubsystemTests.cs`, -`QueryVerbsTests.cs`, `QueryRenderingTests.cs`, `QueryOmgFixtureTests.cs`, -`QueryErrorPathTests.cs`), plus query-specific parsing tests in -`test/DemaConsulting.SysML2Tools.Tool.Tests/Cli/ContextTests.cs`. Tests invoke -`Context.Create`, `QueryCommand.RunAsync`, `Program.RunAsync`, and (for a subset of -real-workspace scenarios) `QueryEngine`'s verb methods directly, asserting on captured -console output, exit code, and `QueryResult` shape. Tests run against the tool's target -framework. +The Tool Query subsystem is verified using unit/integration tests in +`test/DemaConsulting.SysML2Tools.Tool.Tests/Query/QuerySubsystemTests.cs`, +`QueryVerbsTests.cs`, and `QueryErrorPathTests.cs`, plus query-specific parsing tests in +`test/DemaConsulting.SysML2Tools.Tool.Tests/Cli/ContextTests.cs` and shared resx-parity tests +in `test/DemaConsulting.SysML2Tools.Tool.Tests/Resources/ResxResourceTests.cs`. These tests +invoke `Context.Create`, `QueryCommand.RunAsync`, and `Program.RunAsync`, asserting on parsed +state, captured console output, exit code, and file-output behavior. Detailed verb semantics, +renderer behavior, and Core-side file-export helper behavior are verified separately in +`docs/verification/sysml2-tools-core/query.md`. -#### Test Environment +### Test Environment - Framework: xUnit v3 - Target framework: net10.0 - Test project: `DemaConsulting.SysML2Tools.Tool.Tests` - Dependencies: `DemaConsulting.SysML2Tools.Tool` (internal access via `InternalsVisibleTo`); - OMG reference models under `test/SysMLModels/OMG/` (locatable via a repo-root search - upward from the test assembly's output directory). + inline temporary `.sysml` fixtures in the Query test helpers; OMG reference models only where + explicitly used by the Core Query subsystem tests. -#### Acceptance Criteria +### Acceptance Criteria -- All 12 verb tokens parse to the matching `QueryVerb` and dispatch to real `QueryEngine` - logic, producing exit code 0 for valid input. +- All 12 verb tokens parse to the matching `QueryVerb` and dispatch through `QueryCommand` to + the shared Core Query API, producing exit code 0 for valid input. - An unrecognized verb token produces an `ArgumentException` naming the bad token. -- `--element`/`-e` is required for every verb except `list`/`find`; omitting it for a verb - that requires it produces an `ArgumentException`. +- `--element`/`-e` is required for every verb except `list`/`find`; omitting it for a verb that + requires it produces an `ArgumentException`. - `find` without `--kind`/`--name` produces an `ArgumentException`. -- `list`/`find` succeed without `--element`. - `--format markdown` and `--format json` both parse and render without error; an unrecognized `--format` value produces an `ArgumentException`. -- `--depth` (global flag, 1-6, default 1) and `--heading` (default: auto-generated text) - control the Markdown output's top heading depth/text without affecting `--format json` - output (byte-identical JSON with/without the flags). -- Each verb's output correctly reflects the underlying model for representative inline - fixtures: `uses` reports outgoing supertype/typing/import edges; `used-by` reports the - reverse; `dependencies` combines `uses`/`used-by` for one element into a single prose - (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, 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 - connector's keyword; `states` reports states and guarded transitions; `list`/`find` - respect `--kind`/`--name` filtering. -- `dependencies` reports the merged `uses`/`used-by` result as bullet-prose Markdown (not a - table), with entries' `Direction` field populated (`Outgoing`/`Incoming`) only for this - verb; JSON output for every other verb is unaffected (no `Direction` key present when - `null`). -- `dependencies`'s Markdown output shortens every name (the subject element plus every entry's - qualified name) by the longest shared leading `::`-segment prefix across that combined pool, - via `Utilities.QualifiedNameShortener.Shorten`, applied identically to the subject sentence - and both bullet groups; when the pool shares no common prefix, names remain fully qualified. - JSON output for `dependencies` (and every other verb) is unaffected — `QualifiedName`/ - `Element` values remain fully qualified in JSON regardless of this Markdown-only behavior. -- Markdown and JSON renderings of the same `QueryResult` contain the same qualified names - in the same (alphabetical, ordinal) order. -- `--include-stdlib` toggles whether stdlib-seeded elements appear in results. -- Error paths are covered: element not found, a file that fails to parse - (best-effort/graceful degradation), `find` without a filter, unsupported `--format`, no - input files, one or more patterns supplied but none matching any file on disk. -- A glob pattern (e.g. `*.sysml`) resolves to every matching file in the target directory via - the shared `GlobFileCollector` (see `docs/verification/sysml2-tools-core/io.md` for the - underlying glob-semantics verification) and the workspace loads all of them. -- A representative sample of real-world OMG training/example fixtures - (`RequirementSatisfaction.sysml`, `ConnectionsExample.sysml`, `StateDecomposition-1.sysml`, - `GeneralizationExample.sysml`, `Comments.sysml`) produce non-empty, sensible results for - their respective verbs, without asserting brittle exact counts. -- Existing `lint`/`render` test suites continue to pass unmodified, confirming no - regression. -- `query --help` includes a real example invocation for the verb and the shared - Markdown/JSON output-shape schema hint; `query --help` (no verb) includes a "typical - workflow" note recommending `list`/`find` before element-scoped verbs. -- All four subsystems' resx-backed help text (`ProgramStrings`, `LintStrings`, - `RenderStrings`, `QueryStrings`) resolve every key to non-empty text and stay in - bidirectional parity with their accessor classes. - -#### Test Scenarios - -##### QuerySubsystemTests.cs - -**`QuerySubsystem_AnyVerb_WithValidInput_DispatchesToRealLogic`** (theory, 12 cases): -Verifies that each verb, given `--element` when required (and `--kind` for `find`), against -a small shared fixture covering every verb's target element, produces exit code 0 and -output containing `query {verb}`. - -##### QuerySubsystem_ElementRequiredVerb_MissingElement_ThrowsArgumentException - -Verifies that omitting `--element` for any of the 10 verbs that require it (all except -`list`/`find`) throws an `ArgumentException` mentioning `--element`. - -##### QuerySubsystem_ListVerb_NoElementNoFiles_ReportsNoInputFilesError / QuerySubsystem_FindVerb_NoElementNoFiles_ReportsNoInputFilesError - -Verifies that `list`/`find` do not require `--element`, and that omitting input files (the -next validation step) produces the "no input files" error and exit code 1. - -##### QuerySubsystem_FormatMarkdown_DispatchesWithoutError / QuerySubsystem_FormatJson_DispatchesWithoutError +- `--depth` and `--heading` affect Markdown output only and leave `--format json` output + unchanged. +- `--output ` writes the rendered document to the named file instead of stdout. +- Help output remains localized through `QueryStrings`, including the workflow note, example + invocations, schema hints, and the `--output` help text. +- Error paths are covered: no input files, patterns supplied but none matching on disk, target + element not found, invalid `--walk-depth`, invalid `--format`, and parse-error-containing + input files that still complete best-effort. +- Detailed verb semantics, deterministic rendering rules, and Core exporter behavior are owned + by the Core Query subsystem verification; the Tool layer proves orchestration, parsing, + dispatch, and CLI reporting only. + +### Test Scenarios + +#### QuerySubsystemTests.cs + +**`QuerySubsystem_AnyVerb_WithValidInput_DispatchesToRealLogic`** (theory, 12 cases): Verifies +that each verb dispatches successfully through `QueryCommand` and produces non-error output. +This is the Tool-side proof that the CLI adapter reaches the shared Core implementation. + +#### QuerySubsystem_ElementRequiredVerb_MissingElement_ThrowsArgumentException + +Verifies that omitting `--element` for any of the 10 element-scoped verbs throws an +`ArgumentException` mentioning `--element`. + +#### QuerySubsystem_ListVerb_NoElementNoFiles_ReportsNoInputFilesError / + +#### QuerySubsystem_FindVerb_NoElementNoFiles_ReportsNoInputFilesError + +Verifies that `list`/`find` do not require `--element`, and that the next validation step +correctly reports the "no input files" error. + +#### QuerySubsystem_FormatMarkdown_DispatchesWithoutError / + +#### QuerySubsystem_FormatJson_DispatchesWithoutError Verifies that both accepted `--format` values parse and render successfully end-to-end. -##### QuerySubsystem_GlobPattern_ResolvesMultipleFiles +#### QuerySubsystem_FormatJson_UnaffectedByDepthOrHeading -Regression test for the glob-expansion bug fix: verifies that a glob pattern such as -`*.sysml` (previously treated as a literal, never-matching file name) now resolves to every -matching `.sysml` file in the target directory via the shared `GlobFileCollector`, and that -the query dispatches successfully against the resulting multi-file workspace. +Verifies that `--format json` output is unchanged when `--depth` and `--heading` are also +supplied, proving those flags are Markdown-only in the Tool layer as well. -##### QuerySubsystem_UnknownVerb_ThrowsArgumentException +#### QuerySubsystem_GlobPattern_ResolvesMultipleFiles + +Verifies that a glob pattern such as `*.sysml` resolves through the shared `GlobFileCollector` +and that the resulting multi-file workspace is queried successfully. + +#### QuerySubsystem_UnknownVerb_ThrowsArgumentException Verifies that `Context.Create(["query", "bogus"])` throws an `ArgumentException` naming `bogus`. -##### QuerySubsystem_QueryHelp_NoVerb_PrintsGeneralHelpWithoutThrowing +#### QuerySubsystem_QueryHelp_NoVerb_PrintsGeneralHelpWithoutThrowing + +Verifies that `query --help` prints general help and returns exit code 0. + +#### QuerySubsystem_QueryHelp_NoVerb_MentionsTypicalWorkflow + +Verifies that the general-help path includes the workflow note recommending `list`/`find` +before element-scoped verbs. -Verifies that `query --help` prints general help (listing the verbs) and returns exit code 0. +#### QuerySubsystem_QueryVerbHelp_MentionsExampleInvocationAndSchemaHints (theory, 12 cases) -##### QuerySubsystem_QueryVerbHelp_WithVerb_PrintsVerbHelpWithoutThrowing +Verifies that `query --help` prints the real example invocation and shared +Markdown/JSON schema hints for every verb. -Verifies that `query uses --help` prints verb-specific help and returns exit code 0, without +#### QuerySubsystem_QueryVerbHelp_WithVerb_PrintsVerbHelpWithoutThrowing + +Verifies that `query uses --help` prints verb-specific help and returns exit code 0 without requiring `--element`. -##### QuerySubsystem_QueryHelp_NoVerb_MentionsTypicalWorkflow - -Verifies that `query --help` (no verb) includes the "typical workflow" note text (contains -"Typical workflow" and "--element"), confirming `PrintGeneralHelp`'s enrichment content is -actually rendered, not merely present in the resx file. Satisfies -`SysML2Tools-Tool-Query-HelpEnrichment`. - -##### QuerySubsystem_QueryVerbHelp_MentionsExampleInvocationAndSchemaHints (theory, 12 cases) - -Verifies that `query --help`, for every one of the 12 verbs, contains that verb's -real example-invocation substring (drawn from the `VehicleExample` fixture, per the -planning report's verified enrichment content) and the shared Markdown/JSON schema-hint -substrings (`"Qualified Name"` for Markdown, `"QualifiedName"` for JSON — matching the real -`QueryResultRenderer`/`QueryResultSerializerContext` output shape, verified by direct CLI -invocation during implementation). Satisfies `SysML2Tools-Tool-Query-HelpEnrichment`. - -##### ResxResource_EveryKey_ResolvesToNonEmptyText / ResxResource_KeysAndAccessorProperties_AreInBidirectionalParity (ResxResourceTests.cs) - -For the `QueryStrings` resource base name/accessor pair (one of four covered by these theory -tests), every key discovered in `Query/QueryStrings.resx`'s invariant-culture resource set -resolves to non-null/non-empty text via `ResourceManager`, and every such key (including the -12 `Query_Example_*` keys, each backed by its own accessor property) has a matching -`public static string` property on `QueryStrings` (and vice versa). Satisfies -`SysML2Tools-Tool-Query-LocalizableHelpText`. - -##### QueryVerbsTests.cs - -One or more `[Fact]`/`[Theory]` methods per verb, each using a small inline `.sysml` -fixture written to a temp file and run end-to-end via `Context.Create` + -`Program.RunAsync`, asserting on captured stdout content: qualified names of expected -entries, edge/kind labels, `--walk-depth`/`--direction` bounding, annotation text, and -`--include-stdlib` on/off behavior. - -**`Dependencies_CombinesOutgoingAndIncoming_ReportsBothDirections`**, -**`Dependencies_NoOutgoingReferences_ReportsProseLineInsteadOfBulletList`**, -**`Dependencies_NoIncomingReferences_ReportsProseLineInsteadOfBulletList`**, -**`Dependencies_MarkdownOutput_ContainsNoTable`**: Verify, end-to-end, that `dependencies` -reports both a "Depends on" bullet (outgoing) and a "Used by" bullet (incoming) for an -element with both directions populated; that an element with no outgoing references -reports the single `"{Element} has no outgoing references."` prose line instead of a bullet -list; that an element with no incoming references reports the single `"No elements -reference {Element}."` prose line instead of a bullet list; and that the rendered Markdown -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 -`QueryResult` instances (no workspace/model involved): empty-entries rendering, sort-order -correctness, `Detail`/`Notes` rendering, and JSON round-trip via -`QueryResultSerializerContext`. Plus one end-to-end test confirming Markdown and JSON -outputs for the same query contain the same qualified names in the same order. - -**`RenderMarkdown_DefaultArguments_ProducesUnchangedTopLevelHeading`**, -**`RenderMarkdown_CustomDepth_UsesThatManyHeadingHashes`**, -**`RenderMarkdown_CustomHeading_ReplacesAutoGeneratedText`**, -**`RenderMarkdown_CustomDepthAndHeading_CombinesBothOverrides`**: Verify that -`RenderMarkdown`'s default arguments produce the unchanged single `#` heading; that a custom -`depth` changes the number of leading `#` characters; that a custom `heading` -replaces the auto-generated heading text entirely (no merging with verb/element info); and -that both overrides combine correctly when supplied together. - -**`RenderMarkdown_DependenciesVerb_RendersBulletProseNotTable`**, -**`RenderMarkdown_DependenciesVerb_EmptyOutgoingAndIncoming_ReportsBothProseLines`**, -**`RenderJson_DependenciesVerb_IncludesDirectionField`**, -**`RenderJson_NonDependenciesVerb_DirectionFieldOmittedFromOutput`**, -**`Dependencies_DepthAndHeadingOptions_ApplyToHeadingLikeOtherVerbs`**: Unit-test -`dependencies`'s prose-bullet rendering directly against a hand-built, intentionally -unordered `QueryResult` (confirming per-direction ordinal sorting and the exact bullet/intro -text, now using shortened names since the fixture's subject and entries share the common -leading segment `"Model"`); confirm the both-directions-empty edge case reports both prose -lines with no bullets and no `"_No entries._"` fallback; confirm `RenderJson` includes a -populated `Direction` field (`Outgoing`/`Incoming`) for `dependencies` entries; confirm — the -critical regression test — that `RenderJson` for a non-`dependencies` verb (`uses`) never -contains the substring `"Direction"` in its JSON output, proving the `[JsonIgnore(Condition = -JsonIgnoreCondition.WhenWritingNull)]` attribute keeps every other verb's JSON output -unaffected by the new field; and confirm `--depth`/`--heading` apply to `dependencies`'s -heading line exactly like every other verb (now asserting the shortened bullet text). Satisfies -`SysML2Tools-Tool-Query-Dependencies`. - -**`RenderMarkdown_DependenciesVerb_NoCommonPrefix_LeavesNamesFullyQualified`**: Confirms that -when the subject element and its entries share no common leading segment (different top-level -packages), `dependencies`'s Markdown output leaves every name fully qualified in both the -subject sentence and the bullets. Satisfies -`SysML2Tools-Tool-Query-DependenciesNameShortening`. - -**`RenderJson_DependenciesVerb_NamesRemainFullyQualified`**: Explicit before/after regression -test — for a single hand-built `dependencies` `QueryResult` whose subject and entries share the -common leading segment `"Model"` (which Markdown shortens), confirms that `RenderJson`'s output -for the very same result keeps every `QualifiedName`/`Element` value fully qualified, proving -the shortening applies only to `RenderMarkdown`. Satisfies -`SysML2Tools-Tool-Query-DependenciesNameShortening`. - -##### QueryOmgFixtureTests.cs - -Loads real OMG training/example `.sysml` files via `WorkspaceLoader.LoadAsync` directly -(bypassing CLI argument parsing, to avoid quoting ambiguity for qualified names containing -spaces), locates the target element by qualified-name suffix match, and calls the relevant -`QueryEngine` method directly. Assertions are relaxed (non-exact-count) smoke checks: -non-empty results with entries of the expected `Kind`(s). Each test degrades gracefully -(returns without failing) if the reference model files are not present in the checkout, -consistent with prior units' precedent for OMG-fixture-dependent tests. The -`States_StateDecompositionFixture_ReportsStatesAndTransitions` test only asserts on the one -reliably-produced `"transition"`-kind entry, documenting a known, pre-existing (unit 4) -grammar/`AstBuilder` gap where `accept then ;` trigger-shorthand -transitions can silently absorb an adjacent sibling `state` usage; the equivalent inline -fixture test `QueryVerbsTests.States_ReportsStatesAndGuardedTransitions` (using explicit -`transition first X if G then Y;` syntax) validates both `"state"` and `"transition"` entry -kinds together and is unaffected by the gap. - -##### QuerySubsystem_FormatJson_UnaffectedByDepthOrHeading (QuerySubsystemTests.cs) - -Verifies that `--format json` output is byte-identical whether or not `--depth`/ -`--heading` are also supplied, confirming those two options are Markdown-output-only. - -##### QueryErrorPathTests.cs - -Covers: element not found (`context.WriteError` message contains "not found in the -workspace", exit code 1); `find` without `--kind`/`--name` (`ArgumentException`); -unsupported `--format` value (`ArgumentException`); a non-integer `--walk-depth` value -(`ArgumentException` naming `--walk-depth`); a file with parse errors (diagnostics -reported, command completes best-effort); no input files supplied (exit code 1); a file -pattern that matches no file on disk (`context.WriteError` message contains "no files -matched", exit code 1, regression test for the glob-expansion bug fix — see -`QuerySubsystem_GlobPattern_ResolvesMultipleFiles` in `QuerySubsystemTests.cs` for the -corresponding success-path regression proving a glob pattern such as `*.sysml` now resolves -multiple files instead of being treated as a literal, never-matching file name). - -##### Context_Create_QueryCommand_WithVerbToken_SetsQueryVerb (ContextTests.cs) - -Verifies, for each of the 12 verb tokens, that `Context.Create(["query", token, "--element", -"Pkg::Foo"])` sets `Command` to `SysmlCommand.Query` and `Query.Verb` to the matching value. - -##### Context_Create_QueryCommand_UnknownVerb_ThrowsArgumentException (ContextTests.cs) - -Verifies that an unrecognized verb token throws `ArgumentException` naming the token. - -##### Context_Create_QueryCommand_NoVerbWithHelp_LeavesQueryNull (ContextTests.cs) - -Verifies that `query --help` (no verb) leaves `Context.Query` `null`. +#### Dependencies_DepthAndHeadingOptions_ApplyToHeadingLikeOtherVerbs + +Verifies end-to-end that the CLI passes heading-depth and heading-text options through to the +shared Markdown renderer even for the `dependencies` verb's special prose output. + +#### Query_MarkdownAndJsonFormats_AgreeOnEntryContentAndOrder + +Verifies through the full CLI path that Markdown and JSON output contain the same entry content +and deterministic order for the same query. + +#### QuerySubsystem_OutputFlag_WritesToFileInsteadOfStdout -##### Context_Create_QueryCommand_WithElementFlag_SetsElement (ContextTests.cs) +Verifies `--output ` writes the rendered document to the named file and that stdout does +not contain the query body. -##### Context_Create_QueryCommand_WithShortElementFlag_SetsElement (ContextTests.cs) +#### QueryVerbsTests.cs -Verifies that both `--element` and `-e` populate `Query.Element`. +One or more `[Fact]` methods per verb use small inline fixtures and the full CLI path +(`Context.Create` + `Program.RunAsync`) to prove that each verb-specific command-line shape +reaches the Core Query API correctly. The detailed behavior of each verb itself is documented +and verified in `docs/verification/sysml2-tools-core/query.md`. -##### Context_Create_QueryCommand_WithDirectionFlag_SetsDirection (ContextTests.cs) +#### QueryErrorPathTests.cs -##### Context_Create_QueryCommand_WithKindFlag_SetsKind (ContextTests.cs) +Covers: element not found (`context.WriteError` contains "not found in the workspace"); +`find` without `--kind`/`--name`; unsupported `--format`; invalid `--walk-depth`; a file with +parse errors (diagnostics reported, command completes best-effort); no input files supplied; +and a file pattern that matches no file on disk. -##### Context_Create_QueryCommand_WithNameFlag_SetsNameFilter (ContextTests.cs) +#### ContextTests.cs -##### Context_Create_QueryCommand_WithIncludeStdlibFlag_SetsIncludeStdlibTrue (ContextTests.cs) +**`Context_Create_QueryCommand_WithVerbToken_SetsQueryVerb`**: Verifies that each verb token +parses to the matching `QueryVerb`. -Verifies that `--direction`, `--kind`, `--name`, and `--include-stdlib` parse into the -corresponding `QueryOptions` fields. +**`Context_Create_QueryCommand_UnknownVerb_ThrowsArgumentException`**: Verifies that an +unrecognized verb token is rejected during parsing. -##### Context_Create_QueryCommand_WithFormatMarkdown_SetsQueryFormat / WithFormatJson_SetsQueryFormat (ContextTests.cs) +**`Context_Create_QueryCommand_NoVerbWithHelp_LeavesQueryNull`**: Verifies that `query --help` +leaves `Context.Query` null so the general-help path can run without a fake verb. -Verifies that `--format markdown`/`--format json` populate `Query.Format`, and that -`Context.Render` is `null` for a `query` invocation, confirming query's `--format` is -interpreted independently of render's `--format` (they are separate typed properties, not a -shared field). +**`Context_Create_QueryCommand_WithElementFlag_SetsElement`** / +**`Context_Create_QueryCommand_WithShortElementFlag_SetsElement`**: Verify that `--element` and +`-e` populate `Query.Element`. -##### Context_Create_QueryCommand_WithWalkDepthFlag_SetsQueryWalkDepth (ContextTests.cs) +**`Context_Create_QueryCommand_WithDirectionFlag_SetsDirection`** / +**`Context_Create_QueryCommand_WithKindFlag_SetsKind`** / +**`Context_Create_QueryCommand_WithNameFlag_SetsNameFilter`** / +**`Context_Create_QueryCommand_WithIncludeStdlibFlag_SetsIncludeStdlibTrue`**: Verify parsing of +query-specific option fields. -Verifies that `--walk-depth 3` populates `Query.WalkDepth` without affecting the global -`Context.HeadingDepth` (which remains at its default of 1, since `--walk-depth` and -`--depth` are distinct, unrelated flags). +**`Context_Create_QueryCommand_WithFormatMarkdown_SetsQueryFormat`** / +**`Context_Create_QueryCommand_WithFormatJson_SetsQueryFormat`**: Verify that query's +`--format` is parsed independently of render's `--format`. -##### Context_Create_QueryCommand_WithoutHeading_LeavesHeadingNull (ContextTests.cs) +**`Context_Create_QueryCommand_WithWalkDepthFlag_SetsQueryWalkDepth`**: Verifies that +`--walk-depth` populates `Query.WalkDepth` without affecting the global heading depth. -##### Context_Create_QueryCommand_WithHeadingFlag_SetsHeading (ContextTests.cs) +**`Context_Create_QueryCommand_WithoutHeading_LeavesHeadingNull`** / +**`Context_Create_QueryCommand_WithHeadingFlag_SetsHeading`**: Verify heading-text parsing. -Verifies that `--heading` defaults to `null` when not supplied, and populates -`Query.Heading` when supplied. +**`Context_Create_QueryCommand_WithFiles_SetsQueryFilesNotTopLevelFiles`**: Verifies that file +patterns supplied after the verb populate `Context.QueryFiles` rather than the top-level file +list used by other commands. -##### Context_Create_QueryCommand_WithFiles_SetsQueryFilesNotTopLevelFiles (ContextTests.cs) +#### ResxResourceTests.cs -Verifies that file globs supplied after the verb token populate `Query.Files` while -`Context.Files` remains empty. +`ResxResource_EveryKey_ResolvesToNonEmptyText` and +`ResxResource_KeysAndAccessorProperties_AreInBidirectionalParity` verify that `QueryStrings` +remains a complete, culture-aware accessor over `QueryStrings.resx`, including the new +`--output` help text lines. diff --git a/docs/verification/sysml2-tools-tool/utilities.md b/docs/verification/sysml2-tools-tool/utilities.md index 139eea3..1841b56 100644 --- a/docs/verification/sysml2-tools-tool/utilities.md +++ b/docs/verification/sysml2-tools-tool/utilities.md @@ -3,14 +3,11 @@ ### Verification Approach The `Utilities` subsystem is verified by integration tests defined in -`UtilitiesSubsystemTests.cs` (covering `PathHelpers`) plus dedicated unit tests for each unit: -`PathHelpersTests.cs` and `QualifiedNameShortenerTests.cs`. Integration tests exercise -`PathHelpers` through realistic path-combination workflows to confirm that valid paths are -resolved correctly, traversal attacks are rejected, and the resulting paths can be used for -actual directory creation. `QualifiedNameShortener`'s only consumer (`QueryResultRenderer`) is -already covered end-to-end by the `Query` subsystem's own test suite, so no subsystem-level -integration test is added for it — see `docs/verification/sysml2-tools-tool/query.md`. Neither -unit has a dependency on any other tool unit, so no mocking is required for either. +`UtilitiesSubsystemTests.cs` (covering `PathHelpers`) plus a dedicated unit test file: +`PathHelpersTests.cs`. Integration tests exercise `PathHelpers` through realistic +path-combination workflows to confirm that valid paths are resolved correctly, traversal +attacks are rejected, and the resulting paths can be used for actual directory creation. This +unit has no dependency on any other tool unit, so no mocking is required. ### Test Environment @@ -23,8 +20,6 @@ N/A - standard test environment. - Path traversal patterns (e.g., `../`) cause `ArgumentException` to be thrown. - Absolute paths supplied as the relative argument cause `ArgumentException` to be thrown. - Paths produced by `SafePathCombine` can be passed directly to `Directory.CreateDirectory`. -- `QualifiedNameShortener.Shorten`'s unit-level acceptance criteria are documented separately in - `docs/verification/sysml2-tools-tool/utilities/qualified-name-shortener.md`. ### Test Scenarios diff --git a/docs/verification/sysml2-tools-tool/utilities/qualified-name-shortener.md b/docs/verification/sysml2-tools-tool/utilities/qualified-name-shortener.md deleted file mode 100644 index b9059c7..0000000 --- a/docs/verification/sysml2-tools-tool/utilities/qualified-name-shortener.md +++ /dev/null @@ -1,74 +0,0 @@ -### QualifiedNameShortener - -#### Verification Approach - -`QualifiedNameShortener` is verified with unit tests defined in `QualifiedNameShortenerTests.cs`. -Because `QualifiedNameShortener` performs pure string manipulation using only .NET BCL types, no -mocking or test doubles are required. Tests call `QualifiedNameShortener.Shorten` directly with -controlled pools of qualified names and assert on the returned map or the thrown exception type. - -#### Test Environment - -N/A - standard test environment. - -#### Acceptance Criteria - -- All unit tests pass with zero failures. -- A pool of names sharing one leading segment has that segment stripped from every name. -- A pool of names sharing no common leading segment is returned unchanged. -- A pool containing a single distinct name is returned unchanged. -- A pool of all-identical names reduces to a single distinct name (per the "fewer than 2 - distinct names" edge case) and is returned unchanged, so its leaf segment is never stripped - to an empty string. -- A pool sharing a deeper (2+ segment) common prefix has every shared segment stripped. -- The common-prefix length is capped at the shortest name's segment count minus 1, so a short - name in the pool is never stripped down to nothing even when a longer name shares more - leading segments than the short name has to spare. -- Null pools and null pool entries cause `ArgumentNullException`. -- Duplicate names in the pool produce exactly one entry per distinct name in the returned map. - -#### Test Scenarios - -**QualifiedNameShortener_Shorten_OneSharedLeadingSegment_StripsThatSegment**: The worked example -`["A::B::x", "A::B::y", "A::T::g"]` is shortened; the shared leading segment `"A"` is stripped -from every name, producing `["B::x", "B::y", "T::g"]`. This scenario is tested by -`QualifiedNameShortener_Shorten_OneSharedLeadingSegment_StripsThatSegment`. - -**QualifiedNameShortener_Shorten_NoCommonPrefix_LeavesNamesUnchanged**: A pool of names rooted -in different top-level packages (`["A::B::x", "C::D::y"]`) is shortened; every name is returned -unchanged since no leading segment is shared. This scenario is tested by -`QualifiedNameShortener_Shorten_NoCommonPrefix_LeavesNamesUnchanged`. - -**QualifiedNameShortener_Shorten_SingleNamePool_LeavesNameUnchanged**: A pool containing only -one distinct name is shortened; the name is returned unchanged since there is nothing to compare -it against. This scenario is tested by -`QualifiedNameShortener_Shorten_SingleNamePool_LeavesNameUnchanged`. - -**QualifiedNameShortener_Shorten_AllIdenticalNames_KeepsLeafSegment**: A pool where every entry -is the same name (`["A::B::x", "A::B::x", "A::B::x"]`) reduces to a single distinct name and is -returned unchanged, confirming the leaf segment `"x"` is never stripped down to an empty string. -This scenario is tested by `QualifiedNameShortener_Shorten_AllIdenticalNames_KeepsLeafSegment`. - -**QualifiedNameShortener_Shorten_DeeperCommonPrefix_StripsAllSharedSegments**: A pool sharing the -two leading segments `"A::B"` (`["A::B::C::x", "A::B::C::y", "A::B::D::z"]`) is shortened; both -shared segments are stripped from every name. This scenario is tested by -`QualifiedNameShortener_Shorten_DeeperCommonPrefix_StripsAllSharedSegments`. - -**QualifiedNameShortener_Shorten_ShortestNameBoundsCap_RetainsShortestNamesLeaf**: A pool -containing `"A::B"` (2 segments) alongside `"A::B::C"` (which shares the 2-segment prefix -`"A::B"`) is shortened; only 1 segment (`"A"`) is stripped, capped by `"A::B"`'s own segment -count, so `"A::B"` becomes `"B"` rather than an empty string. This scenario is tested by -`QualifiedNameShortener_Shorten_ShortestNameBoundsCap_RetainsShortestNamesLeaf`. - -**QualifiedNameShortener_Shorten_NullPool_ThrowsArgumentNullException**: `null` is passed as the -`qualifiedNames` argument; an `ArgumentNullException` is thrown. This scenario is tested by -`QualifiedNameShortener_Shorten_NullPool_ThrowsArgumentNullException`. - -**QualifiedNameShortener_Shorten_NullEntryInPool_ThrowsArgumentNullException**: A pool -containing a `null` entry is passed; an `ArgumentNullException` is thrown. This scenario is -tested by `QualifiedNameShortener_Shorten_NullEntryInPool_ThrowsArgumentNullException`. - -**QualifiedNameShortener_Shorten_DuplicateNamesInPool_ReturnsOneEntryPerDistinctName**: A pool -where one name is repeated (`["A::B::x", "A::B::x", "A::T::g"]`) is shortened; the returned map -contains exactly one entry per distinct name, both correctly shortened. This scenario is tested -by `QualifiedNameShortener_Shorten_DuplicateNamesInPool_ReturnsOneEntryPerDistinctName`. diff --git a/requirements.yaml b/requirements.yaml index 5264936..fce45d9 100644 --- a/requirements.yaml +++ b/requirements.yaml @@ -35,6 +35,7 @@ includes: - docs/reqstream/sysml2-tools-core/layout/internal/layered-placement.yaml - docs/reqstream/sysml2-tools-core/io.yaml - docs/reqstream/sysml2-tools-core/io/glob-file-collector.yaml + - docs/reqstream/sysml2-tools-core/query.yaml - docs/reqstream/sysml2-tools-core/rendering.yaml - docs/reqstream/sysml2-tools-core/rendering/diagram-renderer.yaml - docs/reqstream/sysml2-tools-core/rendering/internal.yaml @@ -53,7 +54,6 @@ includes: - docs/reqstream/sysml2-tools-tool/self-test/validation.yaml - docs/reqstream/sysml2-tools-tool/utilities.yaml - docs/reqstream/sysml2-tools-tool/utilities/path-helpers.yaml - - docs/reqstream/sysml2-tools-tool/utilities/qualified-name-shortener.yaml - docs/reqstream/sysml2-tools-tool/platform-requirements.yaml - docs/reqstream/ots/antlr4.yaml - docs/reqstream/ots/dema-rendering.yaml diff --git a/src/DemaConsulting.SysML2Tools.Core/Query/NamespaceDoc.cs b/src/DemaConsulting.SysML2Tools.Core/Query/NamespaceDoc.cs new file mode 100644 index 0000000..8e3b471 --- /dev/null +++ b/src/DemaConsulting.SysML2Tools.Core/Query/NamespaceDoc.cs @@ -0,0 +1,52 @@ +// +// Copyright (c) DemaConsulting. All rights reserved. +// + +namespace DemaConsulting.SysML2Tools.Query; + +/// +/// Analyzes a loaded SysML v2 workspace and answers structured questions about its elements. +/// +/// +/// is the entry point: twelve verb methods (Uses, UsedBy, +/// Dependencies, Impact, Describe, Hierarchy, Requirements, +/// Interface, Connections, States, List, Find — see +/// ) each take a loaded SysmlWorkspace, a resolved target +/// SysmlNode (not required by List/Find), and a , +/// returning a uniform . dispatches to +/// the right verb method from a single value, so a caller does +/// not need to write its own verb switch. +/// +/// renders a as either Markdown lines +/// or a JSON string; additionally writes either rendering +/// directly to a file. parses a token list (e.g., CLI arguments) +/// into a instance, for callers that accept the same command-line-style +/// grammar as the Tool project's query command. +/// +/// +/// This namespace has no dependency on any CLI/console concept — it depends only on +/// DemaConsulting.SysML2Tools.Semantic/Semantic.Model (the loaded workspace and its +/// resolved nodes/edges) and this project's own +/// (used by to compact the dependencies verb's Markdown +/// prose). It is reused by the Tool project's query CLI command +/// (QueryCommand/QueryCliArgumentParser), which layers glob-based file resolution, +/// workspace loading, element lookup, and console/log output on top of this namespace's pure, +/// library-friendly API. +/// +/// +/// +/// +/// var (stdlibTable, _) = StdlibProvider.GetSymbolTable(); +/// var loadResult = await WorkspaceLoader.LoadAsync(["Model.sysml"], stdlibTable); +/// var workspace = loadResult.Workspace!; +/// +/// var options = new QueryOptions { Verb = QueryVerb.Uses, Element = "Model::Vehicle" }; +/// workspace.Declarations.TryGetValue(options.Element!, out var element); +/// +/// var result = QueryEngine.Execute(workspace, options, element); +/// QueryResultExporter.WriteMarkdown(result, "uses.md"); +/// +/// +internal static class NamespaceDoc +{ +} diff --git a/src/DemaConsulting.SysML2Tools.Core/Query/QueryArgumentParser.cs b/src/DemaConsulting.SysML2Tools.Core/Query/QueryArgumentParser.cs new file mode 100644 index 0000000..b4ac792 --- /dev/null +++ b/src/DemaConsulting.SysML2Tools.Core/Query/QueryArgumentParser.cs @@ -0,0 +1,239 @@ +// +// Copyright (c) DemaConsulting. All rights reserved. +// + +namespace DemaConsulting.SysML2Tools.Query; + +/// +/// Parses a token list (e.g., words following a query command token) into a +/// instance plus any trailing positional (non-flag) tokens. +/// +/// +/// The grammar is structural: the first token must be a recognized verb (see +/// ) — this is validated eagerly here, not lazily inferred from +/// a shared default case. When no verb is present and the caller's help flag was requested +/// (see the helpRequested parameter of ), parsing returns a +/// (the caller is expected to show general help instead); when no +/// verb is present and help was not requested, a clear is +/// thrown rather than leaving the caller in a silent null/None state. Remaining tokens +/// recognize --element/-e, --direction, --kind, --name, +/// --include-stdlib, --format, --walk-depth, and --heading, plus +/// positional file glob patterns returned separately (this type has no file-glob or CLI-I/O +/// concept of its own — does not carry an input-files property); +/// any other --prefixed token is rejected, including --output, which is a +/// Tool-only, CLI-I/O concept this type is intentionally unaware of (see the Tool project's +/// Query.QueryCliArgumentParser, which pre-scans for --output before delegating +/// the remaining tokens here). --format's value is captured raw and is validated later +/// by the caller. --walk-depth (impact-walk depth, unbounded) is distinct from any +/// caller's own global Markdown heading-depth option (not parsed by this class). --heading +/// (custom Markdown heading text) is also recognized here; both a heading-depth option and +/// --heading are Markdown-output-only and have no effect on JSON output. +/// +public static class QueryArgumentParser +{ + /// + /// Parses a token list into a instance plus any trailing + /// positional (non-flag) tokens. + /// + /// + /// The tokens to parse, starting with the verb token (e.g., the arguments remaining after + /// a caller has stripped its own command token and any cross-cutting flags). + /// + /// + /// when the caller's own help flag was supplied; suppresses the + /// "verb is required" error when no verb token is present. + /// + /// + /// The parsed and any trailing positional tokens (e.g., file + /// glob patterns, interpreted by the caller); the options are when + /// no verb token was supplied and is + /// . + /// + /// + /// Thrown when no verb token is present and is + /// ; when the first token is not a recognized verb; or when an + /// unrecognized flag is supplied. + /// + public static (QueryOptions? Options, IReadOnlyList Files) Parse( + IReadOnlyList commandArgs, bool helpRequested) + { + // The verb is a required structural first argument, validated strictly here rather than + // lazily inferred by a shared default case. + if (commandArgs.Count == 0) + { + if (helpRequested) + { + return (null, []); + } + + throw new ArgumentException( + $"query: a verb is required. Valid verbs are: {string.Join(", ", QueryVerbParsing.AllTokens)}."); + } + + var index = 0; + var verbToken = commandArgs[index++]; + if (verbToken.StartsWith("-", StringComparison.Ordinal)) + { + throw new ArgumentException( + $"query: expected a verb as the first argument, but found '{verbToken}'. " + + $"Valid verbs are: {string.Join(", ", QueryVerbParsing.AllTokens)}."); + } + + var verb = QueryVerbParsing.Parse(verbToken); + + string? element = null; + string? direction = null; + string? kind = null; + string? nameFilter = null; + string? format = null; + int? walkDepth = null; + string? heading = null; + var includeStdlib = false; + var files = new List(); + + while (index < commandArgs.Count) + { + var arg = commandArgs[index++]; + switch (arg) + { + case "--element": + case "-e": + element = GetRequiredStringArgument( + arg, commandArgs, ref index, "an element qualified-name argument"); + break; + + case "--direction": + direction = GetRequiredStringArgument( + arg, commandArgs, ref index, "a direction argument (up, down, or both)"); + break; + + case "--kind": + kind = GetRequiredStringArgument( + arg, commandArgs, ref index, "a kind filter argument"); + break; + + case "--name": + nameFilter = GetRequiredStringArgument( + arg, commandArgs, ref index, "a name filter argument"); + break; + + case "--format": + format = GetRequiredStringArgument( + arg, commandArgs, ref index, "a format argument (markdown or json)"); + break; + + case "--walk-depth": + walkDepth = GetRequiredIntArgument( + arg, commandArgs, ref index, "an impact-walk depth argument", 1); + break; + + case "--heading": + heading = GetRequiredStringArgument( + arg, commandArgs, ref index, "a heading text argument"); + break; + + case "--include-stdlib": + includeStdlib = true; + break; + + default: + if (arg.StartsWith("-", StringComparison.Ordinal)) + { + throw new ArgumentException( + $"Unsupported argument '{arg}' for the 'query' command.", nameof(commandArgs)); + } + + files.Add(arg); + break; + } + } + + var options = new QueryOptions + { + Verb = verb, + Element = element, + Format = format, + WalkDepth = walkDepth, + Direction = direction, + Kind = kind, + NameFilter = nameFilter, + IncludeStdlib = includeStdlib, + Heading = heading + }; + + return (options, files); + } + + /// + /// Gets a required string argument value, advancing past it. + /// + /// The flag token that requires the value (used in error messages). + /// All arguments in the current parsing scope. + /// + /// The index of the value to read; advanced by one on success. + /// + /// Description of what's required, used in error messages. + /// The argument value. + /// Thrown when no value is available at . + /// + /// A minimal, private duplicate of the Tool project's own + /// Cli.CliArgumentHelpers.GetRequiredStringArgument, kept local to this one file + /// rather than sharing a common helper type across the assembly boundary — this project + /// cannot reference the Tool project's Cli namespace, and moving that shared + /// helper into this project would touch three unrelated Tool-only parsers + /// (RenderArgumentParser, ExportArgumentParser, GlobalArgumentParser) + /// purely for this one caller's benefit. + /// + private static string GetRequiredStringArgument( + string arg, + IReadOnlyList args, + ref int index, + string description) + { + if (index >= args.Count) + { + throw new ArgumentException($"{arg} requires {description}", nameof(args)); + } + + return args[index++]; + } + + /// + /// Gets a required integer argument value in the range [, ], + /// advancing past it. + /// + /// The flag token that requires the value (used in error messages). + /// All arguments in the current parsing scope. + /// + /// The index of the value to read; advanced by one on success. + /// + /// Description of what's required, used in error messages. + /// Minimum valid value (inclusive). + /// Maximum valid value (inclusive). + /// The argument value as an integer in [, ]. + /// + /// Thrown when no value is available at , or the value is not an + /// integer within range. + /// + /// + /// A minimal, private duplicate of the Tool project's own + /// Cli.CliArgumentHelpers.GetRequiredIntArgument; see + /// 's remarks for the rationale. + /// + private static int GetRequiredIntArgument( + string arg, + IReadOnlyList args, + ref int index, + string description, + int min = 1, + int max = int.MaxValue) + { + var value = GetRequiredStringArgument(arg, args, ref index, description); + if (!int.TryParse(value, out var result) || result < min || result > max) + { + throw new ArgumentException($"{arg} requires an integer between {min} and {max} for {description}", nameof(args)); + } + + return result; + } +} diff --git a/src/DemaConsulting.SysML2Tools.Tool/Query/QueryEngine.cs b/src/DemaConsulting.SysML2Tools.Core/Query/QueryEngine.cs similarity index 89% rename from src/DemaConsulting.SysML2Tools.Tool/Query/QueryEngine.cs rename to src/DemaConsulting.SysML2Tools.Core/Query/QueryEngine.cs index 6d2d8d7..37a7e1a 100644 --- a/src/DemaConsulting.SysML2Tools.Tool/Query/QueryEngine.cs +++ b/src/DemaConsulting.SysML2Tools.Core/Query/QueryEngine.cs @@ -1,22 +1,6 @@ -// Copyright (c) DEMA Consulting -// -// Permission is hereby granted, free of charge, to any person obtaining a copy -// of this software and associated documentation files (the "Software"), to deal -// in the Software without restriction, including without limitation the rights -// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -// copies of the Software, and to permit persons to whom the Software is -// furnished to do so, subject to the following conditions: -// -// The above copyright notice and this permission notice shall be included in all -// copies or substantial portions of the Software. -// -// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE -// SOFTWARE. +// +// Copyright (c) DemaConsulting. All rights reserved. +// using System.Globalization; using DemaConsulting.SysML2Tools.Semantic; @@ -31,7 +15,7 @@ namespace DemaConsulting.SysML2Tools.Query; /// ), and the parsed , and returning a uniform /// for to render. /// -internal static class QueryEngine +public static class QueryEngine { /// /// The values considered "requirement relationships" by the @@ -44,6 +28,64 @@ internal static class QueryEngine SysmlEdgeKind.Allocate ]; + /// + /// Dispatches to the verb method selected by , the single + /// entry point library callers can use instead of writing their own 12-arm switch (this is + /// the same dispatch previously inlined in the Tool project's QueryCommand.RunAsync + /// before became part of this project's public API). + /// + /// The loaded workspace. + /// The parsed query options, supplying . + /// + /// The resolved target element. Required (non-) for every verb + /// except and (see + /// ); callers that already guarantee this + /// (e.g., because they resolved themselves before calling this + /// method) incur only the cost of one enum check. + /// + /// The query result produced by the dispatched verb. + /// + /// Thrown when or is + /// , or when is + /// for a verb that reports as requiring one. + /// + /// + /// Thrown when is not a recognized + /// value. + /// + public static QueryResult Execute(SysmlWorkspace workspace, QueryOptions options, SysmlNode? element) + { + ArgumentNullException.ThrowIfNull(workspace); + ArgumentNullException.ThrowIfNull(options); + + if (QueryVerbParsing.RequiresElement(options.Verb) && element is null) + { + throw new ArgumentNullException( + nameof(element), + $"The '{QueryVerbParsing.ToToken(options.Verb)}' verb requires a resolved target element."); + } + + // Each verb gets its own switch arm (rather than a lookup/loop) so a future release can + // change one verb's logic without touching the others; mirrors the switch previously + // inlined in the Tool project's QueryCommand.RunAsync. + return options.Verb switch + { + QueryVerb.Uses => Uses(workspace, element!, options), + QueryVerb.UsedBy => UsedBy(workspace, element!, options), + QueryVerb.Dependencies => Dependencies(workspace, element!, options), + QueryVerb.Impact => Impact(workspace, element!, options), + QueryVerb.Describe => Describe(workspace, element!, options), + QueryVerb.Hierarchy => Hierarchy(workspace, element!, options), + QueryVerb.Requirements => Requirements(workspace, element!, options), + QueryVerb.Interface => Interface(workspace, element!, options), + QueryVerb.Connections => Connections(workspace, element!, options), + QueryVerb.States => States(workspace, element!, options), + QueryVerb.List => List(workspace, options), + QueryVerb.Find => Find(workspace, options), + _ => throw new ArgumentOutOfRangeException(nameof(options), options.Verb, "Unrecognized query verb.") + }; + } + /// /// Lists the elements a given element uses (its resolved outgoing supertype, typing, and /// import edges). @@ -616,8 +658,7 @@ public static QueryResult Find(SysmlWorkspace workspace, QueryOptions options) /// /// Determines whether a qualified name should be included in results, applying the - /// --include-stdlib filter via (the - /// Tool project cannot reference Core's internal StdlibFilter). + /// --include-stdlib filter via . /// /// The qualified name to check. /// The loaded workspace. @@ -629,7 +670,7 @@ private static bool IsVisible(string qualifiedName, SysmlWorkspace workspace, bo /// /// Resolves the effective qualified name for a target element, preferring the node's own /// and falling back to the raw - /// string looked up by . + /// string used by the caller to look it up. /// /// The resolved target element. /// The parsed query options. diff --git a/src/DemaConsulting.SysML2Tools.Core/Query/QueryOptions.cs b/src/DemaConsulting.SysML2Tools.Core/Query/QueryOptions.cs new file mode 100644 index 0000000..334c009 --- /dev/null +++ b/src/DemaConsulting.SysML2Tools.Core/Query/QueryOptions.cs @@ -0,0 +1,101 @@ +// +// Copyright (c) DemaConsulting. All rights reserved. +// + +namespace DemaConsulting.SysML2Tools.Query; + +/// +/// Immutable set of options for one invocation. +/// +/// +/// Every field is shared across all 12 values so that a single +/// parsing/construction pass can build one instance regardless of which verb was supplied; +/// not every field is meaningful for every verb (see the per-field remarks below and the +/// verb-grammar table in docs/design/sysml2-tools-core/query.md). Does not carry any +/// file-glob or CLI-I/O concept (e.g., input file patterns, an output file path) — those are +/// entirely the caller's concern (for the Tool project's query CLI command, see +/// Query.QueryCliArgumentParser in the Tool project). +/// +public sealed record QueryOptions +{ + /// + /// Gets the verb selecting which model-analysis operation to perform. + /// + public required QueryVerb Verb { get; init; } + + /// + /// Gets the qualified name of the target element. + /// + /// + /// Required for every verb except and + /// ; when not supplied. + /// + public string? Element { get; init; } + + /// + /// Gets the output format. + /// + /// + /// Accepted values are "markdown" (default when ) and + /// "json"; interpreted by the caller (e.g., the Tool project's query CLI + /// command), not by itself. + /// + public string? Format { get; init; } + + /// + /// Gets the maximum impact-walk traversal depth. + /// + /// + /// Only meaningful for , where it bounds the transitive + /// impact walk; means unlimited. + /// + public int? WalkDepth { get; init; } + + /// + /// Gets the custom Markdown heading text. + /// + /// + /// Markdown output only; has no effect on JSON output. + /// means the auto-generated "query {verb}[: {element}]" + /// text is used instead by . + /// + public string? Heading { get; init; } + + /// + /// Gets the traversal direction. + /// + /// + /// Only meaningful for . Accepted values are + /// "up", "down", and "both"; when not + /// supplied. + /// + public string? Direction { get; init; } + + /// + /// Gets the element-kind filter. + /// + /// + /// Only meaningful for and ; + /// means no kind filtering. + /// + public string? Kind { get; init; } + + /// + /// Gets the name substring filter. + /// + /// + /// Only meaningful for and ; + /// means no name filtering. + /// + public string? NameFilter { get; init; } + + /// + /// Gets a value indicating whether the OMG standard library should be included in + /// results. + /// + /// + /// Applies to every verb; defaults to (stdlib elements are + /// excluded from results unless explicitly requested). + /// + public bool IncludeStdlib { get; init; } +} diff --git a/src/DemaConsulting.SysML2Tools.Tool/Query/QueryResult.cs b/src/DemaConsulting.SysML2Tools.Core/Query/QueryResult.cs similarity index 76% rename from src/DemaConsulting.SysML2Tools.Tool/Query/QueryResult.cs rename to src/DemaConsulting.SysML2Tools.Core/Query/QueryResult.cs index f76b8c6..e1d2915 100644 --- a/src/DemaConsulting.SysML2Tools.Tool/Query/QueryResult.cs +++ b/src/DemaConsulting.SysML2Tools.Core/Query/QueryResult.cs @@ -1,22 +1,6 @@ -// Copyright (c) DEMA Consulting -// -// Permission is hereby granted, free of charge, to any person obtaining a copy -// of this software and associated documentation files (the "Software"), to deal -// in the Software without restriction, including without limitation the rights -// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -// copies of the Software, and to permit persons to whom the Software is -// furnished to do so, subject to the following conditions: -// -// The above copyright notice and this permission notice shall be included in all -// copies or substantial portions of the Software. -// -// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE -// SOFTWARE. +// +// Copyright (c) DemaConsulting. All rights reserved. +// using System.Text.Json.Serialization; @@ -32,7 +16,7 @@ namespace DemaConsulting.SysML2Tools.Query; /// lines (e.g. describe's kind/supertypes/annotations), and /// carries the tabular list of related elements every verb ultimately reports. /// -internal sealed record QueryResult +public sealed record QueryResult { /// /// Gets the kebab-case verb token that produced this result (e.g. "used-by"). @@ -64,7 +48,7 @@ internal sealed record QueryResult /// One row of a : a related element (or transition/state) and its /// relationship to the queried element. /// -internal sealed record QueryResultEntry +public sealed record QueryResultEntry { /// /// Gets the qualified name of the related element. @@ -104,7 +88,7 @@ internal sealed record QueryResultEntry /// The traversal direction of a relative to the queried /// element, populated only by the dependencies verb. /// -internal enum QueryEntryDirection +public enum QueryEntryDirection { /// The entry is an element the queried element depends on (an outgoing reference). Outgoing, diff --git a/src/DemaConsulting.SysML2Tools.Core/Query/QueryResultExporter.cs b/src/DemaConsulting.SysML2Tools.Core/Query/QueryResultExporter.cs new file mode 100644 index 0000000..37b3d89 --- /dev/null +++ b/src/DemaConsulting.SysML2Tools.Core/Query/QueryResultExporter.cs @@ -0,0 +1,113 @@ +// +// Copyright (c) DemaConsulting. All rights reserved. +// + +namespace DemaConsulting.SysML2Tools.Query; + +/// +/// Writes a rendered directly to a file, as either Markdown or +/// JSON, via . +/// +/// +/// +/// A thin convenience wrapper around / +/// plus a plain File.WriteAllText(Async) +/// call. Deliberately minimal: no parent-directory creation, no "clean CLI error" catching of +/// filesystem exceptions — this project has no CLI-I/O convention to justify either behavior, +/// so both are left as caller (e.g., the Tool project's query CLI command) +/// responsibilities, exactly mirroring how the Tool project's own export --output/ +/// render --output handling creates the parent directory and catches +/// / itself before calling +/// into a library method like this one. +/// +/// +/// Markdown output is written by joining 's +/// lines with "\n" (no trailing line terminator), matching this codebase's existing, +/// platform-neutral file-output convention (no path in this codebase normalizes line endings +/// to "\r\n"). +/// +/// +public static class QueryResultExporter +{ + /// + /// Renders as Markdown (via + /// ) and writes it to + /// , overwriting any existing file. + /// + /// The result to render and write. + /// The file path to write to. + /// The Markdown heading depth; see . + /// The custom Markdown heading text; see . + /// Thrown when the file cannot be written; propagates uncaught. + /// + /// Thrown when the caller lacks permission to write to ; propagates + /// uncaught. + /// + public static void WriteMarkdown(QueryResult result, string path, int depth = 1, string? heading = null) + { + var lines = QueryResultRenderer.RenderMarkdown(result, depth, heading); + File.WriteAllText(path, string.Join("\n", lines)); + } + + /// + /// Asynchronously renders as Markdown (via + /// ) and writes it to + /// , overwriting any existing file. + /// + /// The result to render and write. + /// The file path to write to. + /// The Markdown heading depth; see . + /// The custom Markdown heading text; see . + /// A token to cancel the write operation. + /// A task representing the asynchronous write operation. + /// Thrown when the file cannot be written; propagates uncaught. + /// + /// Thrown when the caller lacks permission to write to ; propagates + /// uncaught. + /// + public static async Task WriteMarkdownAsync( + QueryResult result, string path, int depth = 1, string? heading = null, + CancellationToken cancellationToken = default) + { + var lines = QueryResultRenderer.RenderMarkdown(result, depth, heading); + await File.WriteAllTextAsync(path, string.Join("\n", lines), cancellationToken).ConfigureAwait(false); + } + + /// + /// Renders as JSON (via + /// ) and writes it to , + /// overwriting any existing file. + /// + /// The result to render and write. + /// The file path to write to. + /// Thrown when the file cannot be written; propagates uncaught. + /// + /// Thrown when the caller lacks permission to write to ; propagates + /// uncaught. + /// + public static void WriteJson(QueryResult result, string path) + { + File.WriteAllText(path, QueryResultRenderer.RenderJson(result)); + } + + /// + /// Asynchronously renders as JSON (via + /// ) and writes it to , + /// overwriting any existing file. + /// + /// The result to render and write. + /// The file path to write to. + /// A token to cancel the write operation. + /// A task representing the asynchronous write operation. + /// Thrown when the file cannot be written; propagates uncaught. + /// + /// Thrown when the caller lacks permission to write to ; propagates + /// uncaught. + /// + public static async Task WriteJsonAsync( + QueryResult result, string path, CancellationToken cancellationToken = default) + { + await File.WriteAllTextAsync(path, QueryResultRenderer.RenderJson(result), cancellationToken) + .ConfigureAwait(false); + } +} diff --git a/src/DemaConsulting.SysML2Tools.Tool/Query/QueryResultRenderer.cs b/src/DemaConsulting.SysML2Tools.Core/Query/QueryResultRenderer.cs similarity index 82% rename from src/DemaConsulting.SysML2Tools.Tool/Query/QueryResultRenderer.cs rename to src/DemaConsulting.SysML2Tools.Core/Query/QueryResultRenderer.cs index 9d1d8a7..4b18148 100644 --- a/src/DemaConsulting.SysML2Tools.Tool/Query/QueryResultRenderer.cs +++ b/src/DemaConsulting.SysML2Tools.Core/Query/QueryResultRenderer.cs @@ -1,22 +1,6 @@ -// Copyright (c) DEMA Consulting -// -// Permission is hereby granted, free of charge, to any person obtaining a copy -// of this software and associated documentation files (the "Software"), to deal -// in the Software without restriction, including without limitation the rights -// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -// copies of the Software, and to permit persons to whom the Software is -// furnished to do so, subject to the following conditions: -// -// The above copyright notice and this permission notice shall be included in all -// copies or substantial portions of the Software. -// -// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE -// SOFTWARE. +// +// Copyright (c) DemaConsulting. All rights reserved. +// using System.Text.Json; using DemaConsulting.SysML2Tools.Utilities; @@ -25,12 +9,12 @@ namespace DemaConsulting.SysML2Tools.Query; /// /// Shared, non-duplicated rendering layer for : two pure methods -/// converting the uniform result shape into Markdown lines or a JSON string. Every -/// verb arm renders through this type instead of formatting its -/// own output, guaranteeing consistent, deterministically-ordered output across all 12 -/// verbs. +/// converting the uniform result shape into Markdown lines or a JSON string. Every caller +/// (e.g. the Tool project's query CLI command) renders through this type instead of +/// formatting its own output, guaranteeing consistent, deterministically-ordered output +/// across all 12 verbs. /// -internal static class QueryResultRenderer +public static class QueryResultRenderer { /// /// Renders a as Markdown lines: a heading, an optional summary @@ -41,10 +25,10 @@ internal static class QueryResultRenderer /// /// The result to render. /// - /// The Markdown heading depth (number of leading # characters), sourced from the - /// global --depth flag (). Defaults to 1 (a - /// top-level # heading); expected to be pre-validated to the range 1-6 by the - /// caller (). + /// The Markdown heading depth (number of leading # characters), typically sourced + /// from a caller's own global heading-depth option (e.g. the Tool project's --depth + /// flag). Defaults to 1 (a top-level # heading); expected to be pre-validated to + /// the range 1-6 by the caller. /// /// /// A custom heading text, supplied via query's own --heading flag, replacing diff --git a/src/DemaConsulting.SysML2Tools.Core/Query/QueryResultSerializerContext.cs b/src/DemaConsulting.SysML2Tools.Core/Query/QueryResultSerializerContext.cs new file mode 100644 index 0000000..c69d753 --- /dev/null +++ b/src/DemaConsulting.SysML2Tools.Core/Query/QueryResultSerializerContext.cs @@ -0,0 +1,19 @@ +// +// Copyright (c) DemaConsulting. All rights reserved. +// + +using System.Text.Json.Serialization; + +namespace DemaConsulting.SysML2Tools.Query; + +/// +/// Source-generator context for serializing to JSON, mirroring +/// the AOT-safe source-gen pattern used by +/// DemaConsulting.SysML2Tools.Semantic.Model.AstSerializerContext in the Language +/// project. +/// +[JsonSerializable(typeof(QueryResult))] +[JsonSourceGenerationOptions(WriteIndented = true)] +public partial class QueryResultSerializerContext : JsonSerializerContext +{ +} diff --git a/src/DemaConsulting.SysML2Tools.Tool/Query/QueryVerb.cs b/src/DemaConsulting.SysML2Tools.Core/Query/QueryVerb.cs similarity index 82% rename from src/DemaConsulting.SysML2Tools.Tool/Query/QueryVerb.cs rename to src/DemaConsulting.SysML2Tools.Core/Query/QueryVerb.cs index 9a794e5..c110492 100644 --- a/src/DemaConsulting.SysML2Tools.Tool/Query/QueryVerb.cs +++ b/src/DemaConsulting.SysML2Tools.Core/Query/QueryVerb.cs @@ -1,22 +1,6 @@ -// Copyright (c) DEMA Consulting -// -// Permission is hereby granted, free of charge, to any person obtaining a copy -// of this software and associated documentation files (the "Software"), to deal -// in the Software without restriction, including without limitation the rights -// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -// copies of the Software, and to permit persons to whom the Software is -// furnished to do so, subject to the following conditions: -// -// The above copyright notice and this permission notice shall be included in all -// copies or substantial portions of the Software. -// -// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE -// SOFTWARE. +// +// Copyright (c) DemaConsulting. All rights reserved. +// namespace DemaConsulting.SysML2Tools.Query; @@ -24,7 +8,7 @@ namespace DemaConsulting.SysML2Tools.Query; /// Identifies one of the twelve model-analysis operations supported by the /// query command. /// -internal enum QueryVerb +public enum QueryVerb { /// Lists the elements a given element uses (its outgoing dependencies). Uses, @@ -70,7 +54,7 @@ internal enum QueryVerb /// Provides conversion between the kebab-case verb tokens accepted on the command line /// and the enumeration. /// -internal static class QueryVerbParsing +public static class QueryVerbParsing { /// /// Gets the ordered list of all recognized verb tokens, used to build error messages diff --git a/src/DemaConsulting.SysML2Tools.Tool/Utilities/QualifiedNameShortener.cs b/src/DemaConsulting.SysML2Tools.Core/Utilities/QualifiedNameShortener.cs similarity index 80% rename from src/DemaConsulting.SysML2Tools.Tool/Utilities/QualifiedNameShortener.cs rename to src/DemaConsulting.SysML2Tools.Core/Utilities/QualifiedNameShortener.cs index 8f80c41..bcfb094 100644 --- a/src/DemaConsulting.SysML2Tools.Tool/Utilities/QualifiedNameShortener.cs +++ b/src/DemaConsulting.SysML2Tools.Core/Utilities/QualifiedNameShortener.cs @@ -1,22 +1,6 @@ -// Copyright (c) DEMA Consulting -// -// Permission is hereby granted, free of charge, to any person obtaining a copy -// of this software and associated documentation files (the "Software"), to deal -// in the Software without restriction, including without limitation the rights -// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -// copies of the Software, and to permit persons to whom the Software is -// furnished to do so, subject to the following conditions: -// -// The above copyright notice and this permission notice shall be included in all -// copies or substantial portions of the Software. -// -// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE -// SOFTWARE. +// +// Copyright (c) DemaConsulting. All rights reserved. +// namespace DemaConsulting.SysML2Tools.Utilities; @@ -30,7 +14,7 @@ namespace DemaConsulting.SysML2Tools.Utilities; /// future renderer needing the same compaction can reuse it without new coupling. Stateless /// and thread-safe; performs no I/O. /// -internal static class QualifiedNameShortener +public static class QualifiedNameShortener { /// /// The "::" segment delimiter used by SysML qualified names. @@ -59,7 +43,7 @@ internal static class QualifiedNameShortener /// Thrown when or any contained name is /// . /// - internal static IReadOnlyDictionary Shorten(IReadOnlyList qualifiedNames) + public static IReadOnlyDictionary Shorten(IReadOnlyList qualifiedNames) { // Validate inputs - a null pool or a null entry cannot be split into segments ArgumentNullException.ThrowIfNull(qualifiedNames); diff --git a/src/DemaConsulting.SysML2Tools.Tool/Cli/Context.cs b/src/DemaConsulting.SysML2Tools.Tool/Cli/Context.cs index 1cd61a5..baa0d83 100644 --- a/src/DemaConsulting.SysML2Tools.Tool/Cli/Context.cs +++ b/src/DemaConsulting.SysML2Tools.Tool/Cli/Context.cs @@ -33,8 +33,8 @@ namespace DemaConsulting.SysML2Tools.Cli; /// Argument parsing is split into a pass (cross-cutting /// options that apply regardless of command) followed by exactly one per-command parser /// dispatch (, , -/// , or ), so that each -/// command rejects flags outside its own grammar instead of sharing one mega-switch. See +/// , or ), so that +/// each command rejects flags outside its own grammar instead of sharing one mega-switch. See /// docs/design/sysml2-tools-tool/cli/context.md for the full architecture. /// internal sealed class Context : IDisposable @@ -104,6 +104,29 @@ internal sealed class Context : IDisposable /// public QueryOptions? Query { get; private init; } + /// + /// Gets the file glob patterns supplied as positional arguments to the query + /// command; empty unless is . + /// + /// + /// Kept separate from because Core's public QueryOptions no + /// longer carries a file-glob-pattern property (a CLI-only concern) — see + /// . + /// + public IReadOnlyList QueryFiles { get; private init; } = []; + + /// + /// Gets the query command's --output file path; + /// means write to stdout via . + /// + /// + /// Mirrors 's single-output-FILE convention + /// (not a directory, unlike ). + /// Kept separate from because Core's public QueryOptions has no + /// CLI-I/O concept of its own — see . + /// + public string? QueryOutput { get; private init; } + /// /// Gets the parsed options for the export command; unless /// is . @@ -153,6 +176,8 @@ public static Context Create(string[] args) LintOptions? lintOptions = null; RenderCommandOptions? renderOptions = null; QueryOptions? queryOptions = null; + IReadOnlyList queryFiles = []; + string? queryOutput = null; ExportOptions? exportOptions = null; HelpOptions? helpOptions = null; @@ -167,7 +192,7 @@ public static Context Create(string[] args) break; case SysmlCommand.Query: - queryOptions = QueryArgumentParser.Parse(global.CommandArgs, global.Help); + (queryOptions, queryFiles, queryOutput) = QueryCliArgumentParser.Parse(global.CommandArgs, global.Help); break; case SysmlCommand.Export: @@ -203,6 +228,8 @@ public static Context Create(string[] args) Lint = lintOptions, Render = renderOptions, Query = queryOptions, + QueryFiles = queryFiles, + QueryOutput = queryOutput, Export = exportOptions, HelpCommand = helpOptions }; diff --git a/src/DemaConsulting.SysML2Tools.Tool/Export/ExportCommand.cs b/src/DemaConsulting.SysML2Tools.Tool/Export/ExportCommand.cs index 0114e35..af3315f 100644 --- a/src/DemaConsulting.SysML2Tools.Tool/Export/ExportCommand.cs +++ b/src/DemaConsulting.SysML2Tools.Tool/Export/ExportCommand.cs @@ -36,9 +36,12 @@ namespace DemaConsulting.SysML2Tools.Export; /// Lines, for offline consumption by an AI/agent harness. /// /// -/// Stdlib filtering mirrors Query.QueryEngine.IsVisible exactly (replicated locally, -/// since this Tool project cannot reference the Core project's internal -/// StdlibFilter): excludes stdlib keys, and +/// Stdlib filtering mirrors Query.QueryEngine.IsVisible exactly (intentionally +/// duplicated locally — a one-line, no-state check now trivially shareable since +/// QueryEngine.IsVisible became part of Core's public API, but left un-shared here to +/// avoid growing Core's public surface for a one-line convenience and to avoid an +/// out-of-scope edit to this class): excludes stdlib +/// keys, and /// excludes any edge whose source or target is a stdlib /// name, unless --include-stdlib was supplied. Diagnostics are never stdlib-filtered: /// WorkspaceLoader diagnostics only ever originate from the user's own files, because @@ -300,9 +303,12 @@ private static string RenderJsonLines(ExportResult result) /// or when is not a standard-library name. /// /// - /// Mirrors Query.QueryEngine.IsVisible exactly; replicated here (rather than - /// shared) because this Tool project cannot reference the Core project's internal - /// StdlibFilter, and QueryEngine.IsVisible is . + /// Mirrors Query.QueryEngine.IsVisible exactly. Intentionally kept as a small, + /// private duplicate rather than shared: QueryEngine.IsVisible is now part of + /// Core's public Query API surface, but it is a trivial, one-line, no-state check, so + /// making ExportCommand take a dependency on it (or promoting it to + /// purely to let this duplicate be deleted) is not worth the + /// added coupling/public-API-surface growth for this one-line convenience. /// private static bool IsVisible(string qualifiedName, SysmlWorkspace workspace, bool includeStdlib) => includeStdlib || !workspace.StdlibNames.Contains(qualifiedName); diff --git a/src/DemaConsulting.SysML2Tools.Tool/Help/HelpArgumentParser.cs b/src/DemaConsulting.SysML2Tools.Tool/Help/HelpArgumentParser.cs index cd350bc..4f7f00b 100644 --- a/src/DemaConsulting.SysML2Tools.Tool/Help/HelpArgumentParser.cs +++ b/src/DemaConsulting.SysML2Tools.Tool/Help/HelpArgumentParser.cs @@ -33,8 +33,8 @@ namespace DemaConsulting.SysML2Tools.Help; /// per-command parsers, help defines no flags of its own; any --prefixed token is /// rejected, matching the rejection convention shared by /// // -/// . The verb vocabulary is not duplicated here — it is -/// validated by delegating to , reusing that method's +/// . The verb vocabulary is not duplicated here — it +/// is validated by delegating to , reusing that method's /// existing error message and valid-token list. /// internal static class HelpArgumentParser diff --git a/src/DemaConsulting.SysML2Tools.Tool/Query/QueryArgumentParser.cs b/src/DemaConsulting.SysML2Tools.Tool/Query/QueryArgumentParser.cs deleted file mode 100644 index d22bd46..0000000 --- a/src/DemaConsulting.SysML2Tools.Tool/Query/QueryArgumentParser.cs +++ /dev/null @@ -1,180 +0,0 @@ -// Copyright (c) DEMA Consulting -// -// Permission is hereby granted, free of charge, to any person obtaining a copy -// of this software and associated documentation files (the "Software"), to deal -// in the Software without restriction, including without limitation the rights -// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -// copies of the Software, and to permit persons to whom the Software is -// furnished to do so, subject to the following conditions: -// -// The above copyright notice and this permission notice shall be included in all -// copies or substantial portions of the Software. -// -// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE -// SOFTWARE. - -using DemaConsulting.SysML2Tools.Cli; - -namespace DemaConsulting.SysML2Tools.Query; - -/// -/// Parses the arguments remaining after the query command token into a -/// instance. -/// -/// -/// The query grammar is structural: the first token following the query command -/// token must be a recognized verb (see ) — this is validated -/// eagerly here, not lazily inferred from a shared default case. When no verb is present and -/// --help was requested, parsing returns (general help is shown -/// instead); when no verb is present and --help was not requested, a clear -/// is thrown rather than leaving the command in a silent -/// null/None state. Remaining tokens recognize --element/-e, --direction, -/// --kind, --name, --include-stdlib, --format, -/// --walk-depth, and --heading, plus positional file glob patterns; -/// any other --prefixed token is rejected so that flags belonging to -/// other commands (e.g., --auto, --output) are never silently accepted. -/// --format's value is captured raw and validated later by -/// , exactly as it already was before this refactor. -/// --walk-depth (impact-walk depth, unbounded) is a command-scoped flag parsed here, -/// distinct from the global --depth flag (Markdown heading depth, read directly from -/// by , not parsed -/// by this class). --heading (custom Markdown heading text) is also recognized here; -/// both --depth and --heading are Markdown-output-only and have no effect on -/// --format json. -/// -internal static class QueryArgumentParser -{ - /// - /// Parses the query command's arguments. - /// - /// - /// The arguments remaining after the global parser has stripped cross-cutting flags and - /// the query command token. - /// - /// - /// when the global --help/-h/-? flag was - /// supplied; suppresses the "verb is required" error when no verb token is present. - /// - /// - /// The parsed , or when no verb token was - /// supplied and is (e.g., - /// query --help). - /// - /// - /// Thrown when no verb token is present and is - /// ; when the first token is not a recognized verb; or when an - /// unrecognized flag is supplied. - /// - public static QueryOptions? Parse(IReadOnlyList commandArgs, bool helpRequested) - { - // The verb is a required structural first argument, validated strictly here rather than - // lazily inferred by a shared default case. - if (commandArgs.Count == 0) - { - if (helpRequested) - { - return null; - } - - throw new ArgumentException( - $"query: a verb is required. Valid verbs are: {string.Join(", ", QueryVerbParsing.AllTokens)}."); - } - - var index = 0; - var verbToken = commandArgs[index++]; - if (verbToken.StartsWith("-", StringComparison.Ordinal)) - { - throw new ArgumentException( - $"query: expected a verb as the first argument, but found '{verbToken}'. " + - $"Valid verbs are: {string.Join(", ", QueryVerbParsing.AllTokens)}."); - } - - var verb = QueryVerbParsing.Parse(verbToken); - - string? element = null; - string? direction = null; - string? kind = null; - string? nameFilter = null; - string? format = null; - int? walkDepth = null; - string? heading = null; - var includeStdlib = false; - var files = new List(); - - while (index < commandArgs.Count) - { - var arg = commandArgs[index++]; - switch (arg) - { - case "--element": - case "-e": - element = CliArgumentHelpers.GetRequiredStringArgument( - arg, commandArgs, ref index, "an element qualified-name argument"); - break; - - case "--direction": - direction = CliArgumentHelpers.GetRequiredStringArgument( - arg, commandArgs, ref index, "a direction argument (up, down, or both)"); - break; - - case "--kind": - kind = CliArgumentHelpers.GetRequiredStringArgument( - arg, commandArgs, ref index, "a kind filter argument"); - break; - - case "--name": - nameFilter = CliArgumentHelpers.GetRequiredStringArgument( - arg, commandArgs, ref index, "a name filter argument"); - break; - - case "--format": - format = CliArgumentHelpers.GetRequiredStringArgument( - arg, commandArgs, ref index, "a format argument (markdown or json)"); - break; - - case "--walk-depth": - walkDepth = CliArgumentHelpers.GetRequiredIntArgument( - arg, commandArgs, ref index, "an impact-walk depth argument", 1); - break; - - case "--heading": - heading = CliArgumentHelpers.GetRequiredStringArgument( - arg, commandArgs, ref index, "a heading text argument"); - break; - - case "--include-stdlib": - includeStdlib = true; - break; - - default: - if (arg.StartsWith("-", StringComparison.Ordinal)) - { - throw new ArgumentException( - $"Unsupported argument '{arg}' for the 'query' command.", nameof(commandArgs)); - } - - files.Add(arg); - break; - } - } - - return new QueryOptions - { - Verb = verb, - Element = element, - Format = format, - WalkDepth = walkDepth, - Direction = direction, - Kind = kind, - NameFilter = nameFilter, - IncludeStdlib = includeStdlib, - Heading = heading, - Files = files - }; - } -} diff --git a/src/DemaConsulting.SysML2Tools.Tool/Query/QueryCliArgumentParser.cs b/src/DemaConsulting.SysML2Tools.Tool/Query/QueryCliArgumentParser.cs new file mode 100644 index 0000000..b199519 --- /dev/null +++ b/src/DemaConsulting.SysML2Tools.Tool/Query/QueryCliArgumentParser.cs @@ -0,0 +1,89 @@ +// Copyright (c) DEMA Consulting +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in all +// copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +// SOFTWARE. + +using DemaConsulting.SysML2Tools.Cli; + +namespace DemaConsulting.SysML2Tools.Query; + +/// +/// Thin Tool-only wrapper around Core's : extracts the +/// Tool-only --output <file> flag from commandArgs before delegating the +/// remaining tokens to Core's parser. +/// +/// +/// Core's public is intentionally unaware of +/// --output (a CLI-I/O-only concept with no meaning for a library caller), so this +/// type pre-scans for it (using the Tool project's own , +/// exactly as every other Tool-only command parser does) and removes both tokens (the flag +/// and its value) before calling with what remains. +/// This duplicates a small amount of token-scanning logic already present in Core's parser +/// (the loop structure, the --prefix rejection convention) rather than unifying into +/// one parser — teaching Core's public parser about a Tool-only I/O flag would be +/// architecturally worse than this small, isolated, test-covered duplication. +/// +internal static class QueryCliArgumentParser +{ + /// + /// Parses the query command's arguments, extracting the Tool-only + /// --output flag before delegating the remainder to Core's + /// . + /// + /// + /// The arguments remaining after the global parser has stripped cross-cutting flags and + /// the query command token. + /// + /// + /// when the global --help/-h/-? flag was + /// supplied; suppresses the "verb is required" error when no verb token is present. + /// + /// + /// The parsed (or when no verb token was + /// supplied and is ), the file glob + /// patterns supplied as positional arguments, and the --output file path (or + /// when not supplied). + /// + /// + /// Thrown when --output is supplied with no value, or Core's parser rejects the + /// remaining tokens (see 's own exceptions). + /// + public static (QueryOptions? Options, IReadOnlyList Files, string? Output) Parse( + IReadOnlyList commandArgs, bool helpRequested) + { + string? output = null; + var remaining = new List(commandArgs.Count); + + var index = 0; + while (index < commandArgs.Count) + { + var arg = commandArgs[index++]; + if (arg == "--output") + { + output = CliArgumentHelpers.GetRequiredStringArgument( + arg, commandArgs, ref index, "an output file path argument"); + continue; + } + + remaining.Add(arg); + } + + var (options, files) = QueryArgumentParser.Parse(remaining, helpRequested); + return (options, files, output); + } +} diff --git a/src/DemaConsulting.SysML2Tools.Tool/Query/QueryCommand.cs b/src/DemaConsulting.SysML2Tools.Tool/Query/QueryCommand.cs index e19d0a0..5d255c7 100644 --- a/src/DemaConsulting.SysML2Tools.Tool/Query/QueryCommand.cs +++ b/src/DemaConsulting.SysML2Tools.Tool/Query/QueryCommand.cs @@ -30,15 +30,22 @@ namespace DemaConsulting.SysML2Tools.Query; /// /// Implements the query command: loads a SysML v2 workspace and dispatches to one of -/// twelve model-analysis verbs implemented by , rendering the result -/// via as Markdown (default) or JSON. +/// twelve model-analysis verbs implemented by Core's public , +/// rendering the result via as Markdown (default) or JSON — +/// to stdout, or to a file when --output is supplied (via +/// ). A thin CLI adapter: glob resolution, workspace loading, +/// element lookup, and console/log I/O are the only logic that remains in this class; the +/// verb-semantics logic itself lives in Core's public, reusable Query API. /// internal static class QueryCommand { /// /// Runs the query command. /// - /// The CLI context, supplying the parsed and output methods. + /// + /// The CLI context, supplying the parsed , + /// , , and output methods. + /// /// /// Thrown when is (no verb was parsed), when the verb /// requires --element and none was supplied, when find is invoked without --kind or @@ -80,7 +87,7 @@ public static async Task RunAsync(Context context) nameof(context)); } - if (options.Files.Count == 0) + if (context.QueryFiles.Count == 0) { context.WriteError($"query {verbToken}: no input files specified. Provide file glob patterns."); return; @@ -88,15 +95,15 @@ public static async Task RunAsync(Context context) // Resolve the supplied file glob patterns to concrete file paths via the shared // GlobFileCollector, supporting recursive '**' patterns and '!' exclusions. - context.WriteLine($"Loading {options.Files.Count} file pattern(s)..."); - var files = GlobFileCollector.Collect(options.Files, [".sysml", ".kerml"], Directory.GetCurrentDirectory()); + context.WriteLine($"Loading {context.QueryFiles.Count} file pattern(s)..."); + var files = GlobFileCollector.Collect(context.QueryFiles, [".sysml", ".kerml"], Directory.GetCurrentDirectory()); if (files.Count == 0) { context.WriteError($"query {verbToken}: no files matched the given pattern(s)."); return; } - context.WriteLine($"Resolved {files.Count} file(s) from {options.Files.Count} pattern(s)."); + context.WriteLine($"Resolved {files.Count} file(s) from {context.QueryFiles.Count} pattern(s)."); // Load the workspace from the resolved file paths, exactly as 'lint'/'render' do var (stdlibTable, _) = StdlibProvider.GetSymbolTable(); @@ -132,28 +139,51 @@ public static async Task RunAsync(Context context) return; } - // Each verb gets its own switch arm (rather than a lookup/loop) so a future release can - // change one verb's logic without touching the others. - var result = options.Verb switch - { - QueryVerb.Uses => QueryEngine.Uses(workspace, element!, options), - QueryVerb.UsedBy => QueryEngine.UsedBy(workspace, element!, options), - QueryVerb.Dependencies => QueryEngine.Dependencies(workspace, element!, options), - QueryVerb.Impact => QueryEngine.Impact(workspace, element!, options), - QueryVerb.Describe => QueryEngine.Describe(workspace, element!, options), - QueryVerb.Hierarchy => QueryEngine.Hierarchy(workspace, element!, options), - QueryVerb.Requirements => QueryEngine.Requirements(workspace, element!, options), - QueryVerb.Interface => QueryEngine.Interface(workspace, element!, options), - QueryVerb.Connections => QueryEngine.Connections(workspace, element!, options), - QueryVerb.States => QueryEngine.States(workspace, element!, options), - QueryVerb.List => QueryEngine.List(workspace, options), - QueryVerb.Find => QueryEngine.Find(workspace, options), - _ => throw new ArgumentOutOfRangeException(nameof(context), options.Verb, "Unrecognized query verb.") - }; + // Dispatch to Core's public QueryEngine.Execute, which contains the per-verb switch + // (previously inlined here) now shared by any caller of the public Query API. + var result = QueryEngine.Execute(workspace, options, element); // Render via the shared renderer; markdown lines are written one per WriteLine call, // JSON is written as a single chunk (mirroring how other commands emit multi-line output) - if (format.Equals("json", StringComparison.OrdinalIgnoreCase)) + // -- unless '--output' was supplied, in which case the rendered output is written to that + // file instead (mirroring 'export'/'render's own '--output' file-writing behavior). + if (context.QueryOutput is not null) + { + try + { + // Ensure the parent directory exists (mirroring 'export'/'render's + // Directory.CreateDirectory guard for their own --output paths), so a nonexistent + // output path fails cleanly rather than throwing DirectoryNotFoundException. + var outputDir = Path.GetDirectoryName(Path.GetFullPath(context.QueryOutput)); + if (!string.IsNullOrEmpty(outputDir)) + { + Directory.CreateDirectory(outputDir); + } + + if (format.Equals("json", StringComparison.OrdinalIgnoreCase)) + { + await QueryResultExporter.WriteJsonAsync(result, context.QueryOutput).ConfigureAwait(false); + } + else + { + await QueryResultExporter.WriteMarkdownAsync( + result, context.QueryOutput, context.HeadingDepth, options.Heading).ConfigureAwait(false); + } + } + catch (Exception ex) when (ex is IOException or UnauthorizedAccessException or NotSupportedException + or ArgumentException or PathTooLongException) + { + // Covers cases such as '--output' pointing at an existing directory, a + // read-only/locked target, an otherwise-invalid path, or a malformed/too-long + // path string, surfacing a clean error instead of an unhandled exception with a + // stack trace. + context.WriteError($"query {verbToken}: failed to write output file '{context.QueryOutput}': {ex.Message}"); + return; + } + + context.WriteLine($"query {verbToken}: wrote output to '{context.QueryOutput}'."); + } + else if (format.Equals("json", StringComparison.OrdinalIgnoreCase)) { context.WriteLine(QueryResultRenderer.RenderJson(result)); } @@ -205,6 +235,8 @@ public static void PrintGeneralHelp(Context context) context.WriteLine(QueryStrings.Query_GeneralOptionKind); context.WriteLine(QueryStrings.Query_GeneralOptionName); context.WriteLine(QueryStrings.Query_GeneralOptionIncludeStdlib); + context.WriteLine(QueryStrings.Query_GeneralOptionOutput1); + context.WriteLine(QueryStrings.Query_GeneralOptionOutput2); context.WriteLine(""); context.WriteLine(QueryStrings.Query_WorkflowNote1); context.WriteLine(QueryStrings.Query_WorkflowNote2); @@ -259,6 +291,7 @@ public static void PrintVerbHelp(Context context, QueryVerb verb) context.WriteLine(QueryStrings.Query_OptionDepthVerb); context.WriteLine(QueryStrings.Query_OptionHeadingVerb); context.WriteLine(QueryStrings.Query_OptionIncludeStdlibVerb); + context.WriteLine(QueryStrings.Query_OptionOutputVerb); context.WriteLine(""); context.WriteLine(QueryStrings.Query_ExampleHeader); context.WriteLine(QueryStrings.GetExample(verb)); diff --git a/src/DemaConsulting.SysML2Tools.Tool/Query/QueryOptions.cs b/src/DemaConsulting.SysML2Tools.Tool/Query/QueryOptions.cs deleted file mode 100644 index 921a4bb..0000000 --- a/src/DemaConsulting.SysML2Tools.Tool/Query/QueryOptions.cs +++ /dev/null @@ -1,127 +0,0 @@ -// Copyright (c) DEMA Consulting -// -// Permission is hereby granted, free of charge, to any person obtaining a copy -// of this software and associated documentation files (the "Software"), to deal -// in the Software without restriction, including without limitation the rights -// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -// copies of the Software, and to permit persons to whom the Software is -// furnished to do so, subject to the following conditions: -// -// The above copyright notice and this permission notice shall be included in all -// copies or substantial portions of the Software. -// -// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE -// SOFTWARE. - -namespace DemaConsulting.SysML2Tools.Query; - -/// -/// Immutable set of options parsed for one query command invocation. -/// -/// -/// Every field is shared across all 12 values so that a single -/// parsing pass can build one instance regardless of which verb -/// was supplied; not every field is meaningful for every verb (see the per-field remarks -/// below and the verb-grammar table in -/// docs/design/sysml2-tools-tool/query.md). -/// -internal sealed record QueryOptions -{ - /// - /// Gets the verb selecting which model-analysis operation to perform. - /// - public required QueryVerb Verb { get; init; } - - /// - /// Gets the qualified name of the target element, supplied via --element/-e. - /// - /// - /// Required for every verb except and - /// ; when not supplied. - /// - public string? Element { get; init; } - - /// - /// Gets the output format, supplied via --format. - /// - /// - /// Accepted values are "markdown" (default when ) and - /// "json". This reuses the same --format flag as the render command, - /// which instead accepts "svg"/"png"; the two commands interpret the raw - /// string independently. - /// - public string? Format { get; init; } - - /// - /// Gets the maximum impact-walk traversal depth, supplied via --walk-depth. - /// - /// - /// Only meaningful for , where it bounds the transitive - /// impact walk; means unlimited. Command-scoped (parsed locally by - /// ); unrelated to the render command's own - /// --walk-depth flag. - /// - public int? WalkDepth { get; init; } - - /// - /// Gets the custom Markdown heading text, supplied via --heading. - /// - /// - /// Markdown output only; has no effect on --format json. - /// means the auto-generated "query {verb}[: {element}]" text is used instead. - /// - public string? Heading { get; init; } - - /// - /// Gets the traversal direction, supplied via --direction. - /// - /// - /// Only meaningful for . Accepted values are - /// "up", "down", and "both"; when not - /// supplied. - /// - public string? Direction { get; init; } - - /// - /// Gets the element-kind filter, supplied via --kind. - /// - /// - /// Only meaningful for and ; - /// means no kind filtering. - /// - public string? Kind { get; init; } - - /// - /// Gets the name substring filter, supplied via --name. - /// - /// - /// Only meaningful for and ; - /// means no name filtering. - /// - public string? NameFilter { get; init; } - - /// - /// Gets a value indicating whether the OMG standard library should be included in - /// results, supplied via --include-stdlib. - /// - /// - /// Applies to every verb; defaults to (stdlib elements are - /// excluded from results unless explicitly requested). - /// - public bool IncludeStdlib { get; init; } - - /// - /// Gets the file glob patterns supplied as positional arguments after the verb token. - /// - /// - /// Kept separate from /'s - /// file lists so that query-specific file handling cannot interfere with the other - /// commands. - /// - public IReadOnlyList Files { get; init; } = []; -} diff --git a/src/DemaConsulting.SysML2Tools.Tool/Query/QueryResultSerializerContext.cs b/src/DemaConsulting.SysML2Tools.Tool/Query/QueryResultSerializerContext.cs deleted file mode 100644 index c859f35..0000000 --- a/src/DemaConsulting.SysML2Tools.Tool/Query/QueryResultSerializerContext.cs +++ /dev/null @@ -1,35 +0,0 @@ -// Copyright (c) DEMA Consulting -// -// Permission is hereby granted, free of charge, to any person obtaining a copy -// of this software and associated documentation files (the "Software"), to deal -// in the Software without restriction, including without limitation the rights -// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -// copies of the Software, and to permit persons to whom the Software is -// furnished to do so, subject to the following conditions: -// -// The above copyright notice and this permission notice shall be included in all -// copies or substantial portions of the Software. -// -// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE -// SOFTWARE. - -using System.Text.Json.Serialization; - -namespace DemaConsulting.SysML2Tools.Query; - -/// -/// Source-generator context for serializing to JSON, mirroring -/// the AOT-safe source-gen pattern used by -/// DemaConsulting.SysML2Tools.Semantic.Model.AstSerializerContext in the Language -/// project. -/// -[JsonSerializable(typeof(QueryResult))] -[JsonSourceGenerationOptions(WriteIndented = true)] -internal partial class QueryResultSerializerContext : JsonSerializerContext -{ -} diff --git a/src/DemaConsulting.SysML2Tools.Tool/Query/QueryStrings.cs b/src/DemaConsulting.SysML2Tools.Tool/Query/QueryStrings.cs index b4a0d6b..aa16198 100644 --- a/src/DemaConsulting.SysML2Tools.Tool/Query/QueryStrings.cs +++ b/src/DemaConsulting.SysML2Tools.Tool/Query/QueryStrings.cs @@ -128,6 +128,12 @@ internal static class QueryStrings /// Gets the general --include-stdlib option line. public static string Query_GeneralOptionIncludeStdlib => ResourceManager.GetString(nameof(Query_GeneralOptionIncludeStdlib))!; + /// Gets the first line of the general --output option description. + public static string Query_GeneralOptionOutput1 => ResourceManager.GetString(nameof(Query_GeneralOptionOutput1))!; + + /// Gets the second line of the general --output option description. + public static string Query_GeneralOptionOutput2 => ResourceManager.GetString(nameof(Query_GeneralOptionOutput2))!; + /// Gets the first line of the "typical workflow" note. public static string Query_WorkflowNote1 => ResourceManager.GetString(nameof(Query_WorkflowNote1))!; @@ -176,6 +182,9 @@ internal static class QueryStrings /// Gets the --include-stdlib option line shown in verb-specific help. public static string Query_OptionIncludeStdlibVerb => ResourceManager.GetString(nameof(Query_OptionIncludeStdlibVerb))!; + /// Gets the --output option line shown in verb-specific help. + public static string Query_OptionOutputVerb => ResourceManager.GetString(nameof(Query_OptionOutputVerb))!; + /// Gets the "Example:" header line shown in verb-specific help. public static string Query_ExampleHeader => ResourceManager.GetString(nameof(Query_ExampleHeader))!; diff --git a/src/DemaConsulting.SysML2Tools.Tool/Query/QueryStrings.resx b/src/DemaConsulting.SysML2Tools.Tool/Query/QueryStrings.resx index 23951de..79283cc 100644 --- a/src/DemaConsulting.SysML2Tools.Tool/Query/QueryStrings.resx +++ b/src/DemaConsulting.SysML2Tools.Tool/Query/QueryStrings.resx @@ -148,6 +148,12 @@ --include-stdlib Include OMG standard library elements in results + + --output <file> Output file path (default: stdout). Names a FILE, + + + not a directory (same convention as 'export'). + Typical workflow: run 'list' or 'find' first to discover exact qualified names in your @@ -196,6 +202,9 @@ --include-stdlib Include OMG standard library elements in results + + --output <file> Output file path (default: stdout); a FILE, not a directory + Example: diff --git a/test/DemaConsulting.SysML2Tools.Tool.Tests/Query/QueryOmgFixtureTests.cs b/test/DemaConsulting.SysML2Tools.Tests/Query/QueryOmgFixtureTests.cs similarity index 88% rename from test/DemaConsulting.SysML2Tools.Tool.Tests/Query/QueryOmgFixtureTests.cs rename to test/DemaConsulting.SysML2Tools.Tests/Query/QueryOmgFixtureTests.cs index 3c931a6..052738c 100644 --- a/test/DemaConsulting.SysML2Tools.Tool.Tests/Query/QueryOmgFixtureTests.cs +++ b/test/DemaConsulting.SysML2Tools.Tests/Query/QueryOmgFixtureTests.cs @@ -1,22 +1,6 @@ -// Copyright (c) DEMA Consulting -// -// Permission is hereby granted, free of charge, to any person obtaining a copy -// of this software and associated documentation files (the "Software"), to deal -// in the Software without restriction, including without limitation the rights -// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -// copies of the Software, and to permit persons to whom the Software is -// furnished to do so, subject to the following conditions: -// -// The above copyright notice and this permission notice shall be included in all -// copies or substantial portions of the Software. -// -// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE -// SOFTWARE. +// +// Copyright (c) DemaConsulting. All rights reserved. +// using DemaConsulting.SysML2Tools.Query; using DemaConsulting.SysML2Tools.Semantic; @@ -32,7 +16,7 @@ namespace DemaConsulting.SysML2Tools.Tests.Query; /// reported) rather than exact-count, matching the precedent set by unit 4's OMG-fixture /// smoke tests — this avoids brittleness against incidental fixture content or exact /// qualified-name quoting/escaping details, which are exercised precisely by the inline -/// fixtures in instead. +/// fixtures in the Tool project's QueryVerbsTests instead. /// [Collection("Sequential")] public class QueryOmgFixtureTests diff --git a/test/DemaConsulting.SysML2Tools.Tool.Tests/Query/QueryRenderingTests.cs b/test/DemaConsulting.SysML2Tools.Tests/Query/QueryRenderingTests.cs similarity index 78% rename from test/DemaConsulting.SysML2Tools.Tool.Tests/Query/QueryRenderingTests.cs rename to test/DemaConsulting.SysML2Tools.Tests/Query/QueryRenderingTests.cs index ef06f4f..59e1c26 100644 --- a/test/DemaConsulting.SysML2Tools.Tool.Tests/Query/QueryRenderingTests.cs +++ b/test/DemaConsulting.SysML2Tools.Tests/Query/QueryRenderingTests.cs @@ -1,22 +1,6 @@ -// Copyright (c) DEMA Consulting -// -// Permission is hereby granted, free of charge, to any person obtaining a copy -// of this software and associated documentation files (the "Software"), to deal -// in the Software without restriction, including without limitation the rights -// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -// copies of the Software, and to permit persons to whom the Software is -// furnished to do so, subject to the following conditions: -// -// The above copyright notice and this permission notice shall be included in all -// copies or substantial portions of the Software. -// -// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE -// SOFTWARE. +// +// Copyright (c) DemaConsulting. All rights reserved. +// using System.Text.Json; using DemaConsulting.SysML2Tools.Query; @@ -124,28 +108,6 @@ public void RenderMarkdown_DependenciesVerb_EmptyOutgoingAndIncoming_ReportsBoth Assert.DoesNotContain(lines, l => l.Contains("_No entries._")); } - /// - /// 'dependencies' end-to-end: '--depth'/'--heading' apply to its heading line exactly - /// like every other verb, while the bullet-prose body below is unaffected. - /// - [Fact] - public async Task Dependencies_DepthAndHeadingOptions_ApplyToHeadingLikeOtherVerbs() - { - const string sysml = """ - package Model { - part def Vehicle; - part def Car specializes Vehicle; - } - """; - - var (output, exitCode) = await QueryTestFixtures.RunQueryAsync( - sysml, "dependencies", "--element", "Model::Car", "--depth", "3", "--heading", "Custom"); - - Assert.Equal(0, exitCode); - Assert.Contains("### Custom", output); - Assert.Contains("Depends on **Vehicle** (supertype)", output); - } - /// /// RenderMarkdown sorts entries by qualified name (ordinal), regardless of input order. /// @@ -333,7 +295,7 @@ public void RenderJson_DependenciesVerb_IncludesDirectionField() /// /// Regression test: 'dependencies' JSON output remains fully-qualified (unaffected by - /// the Markdown-only shortening), even + /// the Markdown-only shortening), even /// when the subject and entries share a common leading segment that Markdown would /// shorten. /// @@ -388,42 +350,4 @@ public void RenderJson_NonDependenciesVerb_DirectionFieldOmittedFromOutput() Assert.DoesNotContain("Direction", json); } - - /// - /// End-to-end: '--format markdown' and '--format json' for the same 'uses' query report - /// the same qualified names, in the same order. - /// - [Fact] - public async Task Query_MarkdownAndJsonFormats_AgreeOnEntryContentAndOrder() - { - const string sysml = """ - package Model { - part def Alpha; - part def Zeta specializes Alpha; - part def Beta specializes Alpha; - } - """; - - var (markdown, markdownExit) = await QueryTestFixtures.RunQueryAsync( - sysml, "used-by", "--element", "Model::Alpha", "--format", "markdown"); - var (json, jsonExit) = await QueryTestFixtures.RunQueryAsync( - sysml, "used-by", "--element", "Model::Alpha", "--format", "json"); - - Assert.Equal(0, markdownExit); - Assert.Equal(0, jsonExit); - - // The JSON document is the last chunk written; extract it (from the first '{') and parse - var deserialized = JsonSerializer.Deserialize( - json[json.IndexOf('{')..], QueryResultSerializerContext.Default.QueryResult); - - Assert.NotNull(deserialized); - Assert.Equal(2, deserialized!.Entries.Count); - Assert.Equal("Model::Beta", deserialized.Entries[0].QualifiedName); - Assert.Equal("Model::Zeta", deserialized.Entries[1].QualifiedName); - - // Markdown reports the same two qualified names, Beta appearing before Zeta - var betaIndex = markdown.IndexOf("Model::Beta", StringComparison.Ordinal); - var zetaIndex = markdown.IndexOf("Model::Zeta", StringComparison.Ordinal); - Assert.True(betaIndex >= 0 && zetaIndex >= 0 && betaIndex < zetaIndex); - } } diff --git a/test/DemaConsulting.SysML2Tools.Tests/Query/QueryResultExporterTests.cs b/test/DemaConsulting.SysML2Tools.Tests/Query/QueryResultExporterTests.cs new file mode 100644 index 0000000..8292f2a --- /dev/null +++ b/test/DemaConsulting.SysML2Tools.Tests/Query/QueryResultExporterTests.cs @@ -0,0 +1,154 @@ +// +// Copyright (c) DemaConsulting. All rights reserved. +// + +using DemaConsulting.SysML2Tools.Query; + +namespace DemaConsulting.SysML2Tools.Tests.Query; + +/// +/// Tests for , verifying that its file output matches +/// 's direct rendering output exactly. +/// +public sealed class QueryResultExporterTests : IDisposable +{ + private readonly string _path = Path.Combine(Path.GetTempPath(), $"query_export_{Guid.NewGuid():N}.tmp"); + + /// + /// Deletes the temporary file created for this test instance, if it exists. + /// + public void Dispose() + { + if (File.Exists(_path)) + { + File.Delete(_path); + } + } + + private static QueryResult SampleResult() + { + return new QueryResult + { + Verb = "uses", + Element = "Model::Foo", + Summary = ["1 outgoing reference(s)."], + Entries = [new QueryResultEntry { QualifiedName = "Model::Bar", Kind = "supertype" }] + }; + } + + /// + /// WriteMarkdown writes the exact same content as joining + /// 's lines with "\n". + /// + [Fact] + public void WriteMarkdown_HappyPath_MatchesRendererOutput() + { + // Arrange + var result = SampleResult(); + var expected = string.Join("\n", QueryResultRenderer.RenderMarkdown(result)); + + // Act + QueryResultExporter.WriteMarkdown(result, _path); + + // Assert + Assert.Equal(expected, File.ReadAllText(_path)); + } + + /// + /// WriteMarkdown honors the depth and heading overrides, matching + /// 's behavior for the same arguments. + /// + [Fact] + public void WriteMarkdown_WithDepthAndHeading_MatchesRendererOutput() + { + // Arrange + var result = SampleResult(); + var expected = string.Join("\n", QueryResultRenderer.RenderMarkdown(result, depth: 3, heading: "Custom")); + + // Act + QueryResultExporter.WriteMarkdown(result, _path, depth: 3, heading: "Custom"); + + // Assert + Assert.Equal(expected, File.ReadAllText(_path)); + } + + /// + /// WriteMarkdownAsync writes the exact same content as its synchronous counterpart. + /// + [Fact] + public async Task WriteMarkdownAsync_HappyPath_MatchesRendererOutput() + { + // Arrange + var result = SampleResult(); + var expected = string.Join("\n", QueryResultRenderer.RenderMarkdown(result)); + + // Act + await QueryResultExporter.WriteMarkdownAsync(result, _path, cancellationToken: TestContext.Current.CancellationToken); + + // Assert + Assert.Equal(expected, await File.ReadAllTextAsync(_path, TestContext.Current.CancellationToken)); + } + + /// + /// WriteJson writes the exact same content as . + /// + [Fact] + public void WriteJson_HappyPath_MatchesRendererOutput() + { + // Arrange + var result = SampleResult(); + var expected = QueryResultRenderer.RenderJson(result); + + // Act + QueryResultExporter.WriteJson(result, _path); + + // Assert + Assert.Equal(expected, File.ReadAllText(_path)); + } + + /// + /// WriteJsonAsync writes the exact same content as its synchronous counterpart. + /// + [Fact] + public async Task WriteJsonAsync_HappyPath_MatchesRendererOutput() + { + // Arrange + var result = SampleResult(); + var expected = QueryResultRenderer.RenderJson(result); + + // Act + await QueryResultExporter.WriteJsonAsync(result, _path, TestContext.Current.CancellationToken); + + // Assert + Assert.Equal(expected, await File.ReadAllTextAsync(_path, TestContext.Current.CancellationToken)); + } + + /// + /// WriteMarkdown propagates (an + /// subtype) uncaught when the parent directory does not exist, + /// rather than creating it or swallowing the failure. + /// + [Fact] + public void WriteMarkdown_MissingParentDirectory_PropagatesIoException() + { + // Arrange + var missingDirPath = Path.Combine(_path + "_missing_dir", "output.md"); + + // Act & Assert + Assert.Throws(() => QueryResultExporter.WriteMarkdown(SampleResult(), missingDirPath)); + } + + /// + /// WriteJson propagates (an + /// subtype) uncaught when the parent directory does not exist. + /// + [Fact] + public void WriteJson_MissingParentDirectory_PropagatesIoException() + { + // Arrange + var missingDirPath = Path.Combine(_path + "_missing_dir", "output.json"); + + // Act & Assert + Assert.Throws(() => QueryResultExporter.WriteJson(SampleResult(), missingDirPath)); + } +} diff --git a/test/DemaConsulting.SysML2Tools.Tool.Tests/Utilities/QualifiedNameShortenerTests.cs b/test/DemaConsulting.SysML2Tools.Tests/Utilities/QualifiedNameShortenerTests.cs similarity index 85% rename from test/DemaConsulting.SysML2Tools.Tool.Tests/Utilities/QualifiedNameShortenerTests.cs rename to test/DemaConsulting.SysML2Tools.Tests/Utilities/QualifiedNameShortenerTests.cs index f3c9da7..ee3b5b1 100644 --- a/test/DemaConsulting.SysML2Tools.Tool.Tests/Utilities/QualifiedNameShortenerTests.cs +++ b/test/DemaConsulting.SysML2Tools.Tests/Utilities/QualifiedNameShortenerTests.cs @@ -1,26 +1,10 @@ -// Copyright (c) DEMA Consulting -// -// Permission is hereby granted, free of charge, to any person obtaining a copy -// of this software and associated documentation files (the "Software"), to deal -// in the Software without restriction, including without limitation the rights -// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -// copies of the Software, and to permit persons to whom the Software is -// furnished to do so, subject to the following conditions: -// -// The above copyright notice and this permission notice shall be included in all -// copies or substantial portions of the Software. -// -// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE -// SOFTWARE. +// +// Copyright (c) DemaConsulting. All rights reserved. +// using DemaConsulting.SysML2Tools.Utilities; -namespace DemaConsulting.SysML2Tools.Tests; +namespace DemaConsulting.SysML2Tools.Tests.Utilities; /// /// Tests for the QualifiedNameShortener class. diff --git a/test/DemaConsulting.SysML2Tools.Tool.Tests/Cli/ContextTests.cs b/test/DemaConsulting.SysML2Tools.Tool.Tests/Cli/ContextTests.cs index d757af0..4c3887b 100644 --- a/test/DemaConsulting.SysML2Tools.Tool.Tests/Cli/ContextTests.cs +++ b/test/DemaConsulting.SysML2Tools.Tool.Tests/Cli/ContextTests.cs @@ -957,7 +957,7 @@ public void Context_Create_QueryCommand_WithHeadingFlag_SetsHeading() /// /// Test creating a context with the query command and file globs after the verb sets - /// Query.Files, leaving the lint/render option objects null. + /// Context.QueryFiles, leaving the lint/render option objects null. /// [Fact] public void Context_Create_QueryCommand_WithFiles_SetsQueryFilesNotTopLevelFiles() @@ -967,12 +967,38 @@ public void Context_Create_QueryCommand_WithFiles_SetsQueryFilesNotTopLevelFiles // Assert: verify expected behavior Assert.NotNull(context.Query); - Assert.Single(context.Query.Files); - Assert.Equal("*.sysml", context.Query.Files[0]); + Assert.Single(context.QueryFiles); + Assert.Equal("*.sysml", context.QueryFiles[0]); Assert.Null(context.Lint); Assert.Null(context.Render); } + /// + /// Test creating a context with the query command and --output sets Context.QueryOutput. + /// + [Fact] + public void Context_Create_QueryCommand_WithOutputFlag_SetsQueryOutput() + { + // Act: execute the operation being tested + using var context = Context.Create(["query", "list", "--output", "output/path.md"]); + + // Assert: verify expected behavior + Assert.NotNull(context.Query); + Assert.Equal("output/path.md", context.QueryOutput); + } + + /// + /// Test creating a context with query --output but no value throws ArgumentException. + /// + [Fact] + public void Context_Create_QueryCommand_OutputWithoutValue_ThrowsArgumentException() + { + // Act & Assert + var exception = Assert.Throws( + () => Context.Create(["query", "list", "--output"])); + Assert.Contains("--output", exception.Message); + } + /// /// Test creating a context with the lint command and an out-of-scope flag (--auto, /// belonging to render) throws ArgumentException naming the flag and the 'lint' command. diff --git a/test/DemaConsulting.SysML2Tools.Tool.Tests/Query/QuerySubsystemTests.cs b/test/DemaConsulting.SysML2Tools.Tool.Tests/Query/QuerySubsystemTests.cs index 0826ed5..76f3b8a 100644 --- a/test/DemaConsulting.SysML2Tools.Tool.Tests/Query/QuerySubsystemTests.cs +++ b/test/DemaConsulting.SysML2Tools.Tool.Tests/Query/QuerySubsystemTests.cs @@ -18,6 +18,7 @@ // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE // SOFTWARE. +using System.Text.Json; using DemaConsulting.SysML2Tools.Cli; using DemaConsulting.SysML2Tools.Query; @@ -429,4 +430,109 @@ public async Task QuerySubsystem_QueryVerbHelp_WithVerb_PrintsVerbHelpWithoutThr Console.SetOut(originalOut); } } + + /// + /// 'dependencies' end-to-end: '--depth'/'--heading' apply to its heading line exactly + /// like every other verb, while the bullet-prose body below is unaffected. + /// + [Fact] + public async Task Dependencies_DepthAndHeadingOptions_ApplyToHeadingLikeOtherVerbs() + { + const string sysml = """ + package Model { + part def Vehicle; + part def Car specializes Vehicle; + } + """; + + var (output, exitCode) = await QueryTestFixtures.RunQueryAsync( + sysml, "dependencies", "--element", "Model::Car", "--depth", "3", "--heading", "Custom"); + + Assert.Equal(0, exitCode); + Assert.Contains("### Custom", output); + Assert.Contains("Depends on **Vehicle** (supertype)", output); + } + + /// + /// End-to-end: '--format markdown' and '--format json' for the same 'uses' query report + /// the same qualified names, in the same order. + /// + [Fact] + public async Task Query_MarkdownAndJsonFormats_AgreeOnEntryContentAndOrder() + { + const string sysml = """ + package Model { + part def Alpha; + part def Zeta specializes Alpha; + part def Beta specializes Alpha; + } + """; + + var (markdown, markdownExit) = await QueryTestFixtures.RunQueryAsync( + sysml, "used-by", "--element", "Model::Alpha", "--format", "markdown"); + var (json, jsonExit) = await QueryTestFixtures.RunQueryAsync( + sysml, "used-by", "--element", "Model::Alpha", "--format", "json"); + + Assert.Equal(0, markdownExit); + Assert.Equal(0, jsonExit); + + // The JSON document is the last chunk written; extract it (from the first '{') and parse + var deserialized = JsonSerializer.Deserialize( + json[json.IndexOf('{')..], QueryResultSerializerContext.Default.QueryResult); + + Assert.NotNull(deserialized); + Assert.Equal(2, deserialized!.Entries.Count); + Assert.Equal("Model::Beta", deserialized.Entries[0].QualifiedName); + Assert.Equal("Model::Zeta", deserialized.Entries[1].QualifiedName); + + // Markdown reports the same two qualified names, Beta appearing before Zeta + var betaIndex = markdown.IndexOf("Model::Beta", StringComparison.Ordinal); + var zetaIndex = markdown.IndexOf("Model::Zeta", StringComparison.Ordinal); + Assert.True(betaIndex >= 0 && zetaIndex >= 0 && betaIndex < zetaIndex); + } + + /// + /// 'query --output <file>' writes the rendered output to the given file instead of + /// stdout, mirroring 'export --output' semantics exactly (single output file, not a + /// directory). + /// + [Fact] + public async Task QuerySubsystem_OutputFlag_WritesToFileInsteadOfStdout() + { + const string sysml = """ + package Model { + part def Vehicle; + part def Car specializes Vehicle; + } + """; + + var tempFile = Path.GetTempFileName() + ".sysml"; + var outputFile = Path.GetTempFileName(); + await File.WriteAllTextAsync(tempFile, sysml, TestContext.Current.CancellationToken); + + var originalOut = Console.Out; + try + { + using var outWriter = new StringWriter(); + Console.SetOut(outWriter); + + using var context = Context.Create( + ["query", "uses", "--element", "Model::Car", "--output", outputFile, tempFile]); + await Program.RunAsync(context); + + Assert.Equal(0, context.ExitCode); + Assert.DoesNotContain("# query uses", outWriter.ToString()); + Assert.Contains(outputFile, outWriter.ToString()); + + var written = await File.ReadAllTextAsync(outputFile, TestContext.Current.CancellationToken); + Assert.Contains("# query uses: Model::Car", written); + Assert.Contains("Vehicle", written); + } + finally + { + Console.SetOut(originalOut); + File.Delete(tempFile); + File.Delete(outputFile); + } + } }