From c9938ceae3107744a1f6b8b1290c8981d17a31d8 Mon Sep 17 00:00:00 2001 From: Malcolm Nixon Date: Mon, 20 Jul 2026 18:52:25 -0400 Subject: [PATCH 1/2] Promote query engine from Tool to Core as a public reusable API Moves the `query` CLI command's implementation from the internal `DemaConsulting.SysML2Tools.Tool` project into the public `DemaConsulting.SysML2Tools.Core` library, so it can be called in-process by other applications (e.g. the SysML2Workbench Avalonia app) without shelling out to the CLI. The Tool project becomes a thin adapter over the new public Core API; CLI observable behavior is unchanged except for one new addition. Moved to Core (namespace DemaConsulting.SysML2Tools.Query, all public): - QueryEngine: the 12 verb implementations, plus a new Execute(...) dispatcher that centralizes verb-selection logic for reuse by both the CLI and future library callers. - QueryResult / QueryResultEntry / QueryEntryDirection: plain JSON-serializable result model. - QueryResultRenderer: Markdown/JSON rendering, now public. - QueryResultSerializerContext: source-generated JSON serialization. - QueryVerb / QueryVerbParsing: the 12-verb vocabulary and parsing. - QueryOptions: the shared option record, with the CLI-only `Files` glob-pattern property dropped (library callers pass an already-loaded SysmlWorkspace). - QueryArgumentParser: token parsing, adjusted to return files alongside options since Files no longer lives on QueryOptions. - QualifiedNameShortener: moved from Tool/Utilities to Core/Utilities. New in Core: - QueryResultExporter: WriteMarkdown/WriteJson and async variants that render a QueryResult and write it to a file, for callers (CLI or library) that want file output instead of an in-memory string. Exceptions propagate to the caller since Core has no CLI context to report through. Tool changes (thin adapter): - QueryCommand now calls the public Core QueryEngine/QueryResultRenderer APIs instead of hosting the logic itself. - New QueryCliArgumentParser wraps Core's QueryArgumentParser to also track the CLI-only Files/Output values. - New `--output ` flag on the `query` command, mirroring `export`'s `--output` semantics exactly (single output file, not a directory like `render`). When supplied, output is written via QueryResultExporter instead of stdout. Broadened exception handling covers ArgumentException/PathTooLongException for malformed paths. - Context.cs gained a QueryOutput property to carry the new flag. Tests: - Pure QueryEngine/QueryResultRenderer/QueryResultExporter tests moved to a new test/DemaConsulting.SysML2Tools.Tests/Query/ folder in the Core test project; QualifiedNameShortenerTests moved alongside it. - CLI-specific tests (arg parsing, validation errors, stdout formatting, the new --output end-to-end file-writing test) remain in Tool.Tests. - Added ContextTests coverage for the new QueryOutput property. Documentation: - New docs/design, docs/verification, and docs/reqstream entries for the Core Query subsystem, mirroring sibling Core subsystem docs. - Updated docs/design/introduction.md decomposition tree, the Tool query docs (thin-adapter description + --output flag), and the Tool Cli/Context companion docs/tests for the new QueryOutput property. Verified: build.ps1 (0 warnings/errors, full test suite green across net8.0/net9.0/net10.0), fix.ps1, lint.ps1 all clean; manually confirmed `--output` produces byte-identical content to stdout for both Markdown and JSON formats. Ran formal-review sub-agents across all 25 review-sets affected by this change's file list (computed via wcmatch GLOBSTAR|NEGATE|BRACE matching against .reviewmark.yaml); fixed every finding genuinely caused by this change (missing Query entry in the design decomposition tree, missing Core Query verb verification scenarios, wrong heading depth in the Tool query docs, narrow --output exception handling, missing QueryOutput requirement/design/test coverage) and re-verified each fix with a fresh review. Remaining FAILED review-sets were confirmed via `git diff main` to reference files untouched by this branch (pre-existing, unrelated issues such as Template-* placeholder requirement IDs, OTS rendering dependency documentation gaps, and a pre-existing QueryTestFixtures.cs temp-file leak) and were left deferred per instructions. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .reviewmark.yaml | 41 +- README.md | 1 + docs/design/introduction.md | 21 + docs/design/sysml2-tools-core.md | 65 ++- docs/design/sysml2-tools-core/query.md | 194 +++++++ docs/design/sysml2-tools-tool/cli/context.md | 27 +- docs/design/sysml2-tools-tool/query.md | 489 ++++-------------- docs/reqstream/sysml2-tools-core.yaml | 4 +- docs/reqstream/sysml2-tools-core/query.yaml | 216 ++++++++ .../sysml2-tools-tool/cli/context.yaml | 14 + docs/reqstream/sysml2-tools-tool/query.yaml | 239 +-------- docs/user_guide/introduction.md | 1 + docs/verification/introduction.md | 4 + docs/verification/sysml2-tools-core.md | 20 +- docs/verification/sysml2-tools-core/query.md | 202 ++++++++ .../sysml2-tools-tool/cli/context.md | 11 + docs/verification/sysml2-tools-tool/query.md | 409 +++++---------- requirements.yaml | 1 + .../Query/NamespaceDoc.cs | 52 ++ .../Query/QueryArgumentParser.cs | 239 +++++++++ .../Query/QueryEngine.cs | 87 +++- .../Query/QueryOptions.cs | 101 ++++ .../Query/QueryResult.cs | 28 +- .../Query/QueryResultExporter.cs | 113 ++++ .../Query/QueryResultRenderer.cs | 40 +- .../Query/QueryResultSerializerContext.cs | 19 + .../Query/QueryVerb.cs | 26 +- .../Utilities/QualifiedNameShortener.cs | 26 +- .../Cli/Context.cs | 33 +- .../Export/ExportCommand.cs | 18 +- .../Help/HelpArgumentParser.cs | 4 +- .../Query/QueryArgumentParser.cs | 180 ------- .../Query/QueryCliArgumentParser.cs | 89 ++++ .../Query/QueryCommand.cs | 85 ++- .../Query/QueryOptions.cs | 127 ----- .../Query/QueryResultSerializerContext.cs | 35 -- .../Query/QueryStrings.cs | 9 + .../Query/QueryStrings.resx | 9 + .../Query/QueryOmgFixtureTests.cs | 24 +- .../Query/QueryRenderingTests.cs | 84 +-- .../Query/QueryResultExporterTests.cs | 154 ++++++ .../Utilities/QualifiedNameShortenerTests.cs | 24 +- .../Cli/ContextTests.cs | 32 +- .../Query/QuerySubsystemTests.cs | 106 ++++ 44 files changed, 2182 insertions(+), 1521 deletions(-) create mode 100644 docs/design/sysml2-tools-core/query.md create mode 100644 docs/reqstream/sysml2-tools-core/query.yaml create mode 100644 docs/verification/sysml2-tools-core/query.md create mode 100644 src/DemaConsulting.SysML2Tools.Core/Query/NamespaceDoc.cs create mode 100644 src/DemaConsulting.SysML2Tools.Core/Query/QueryArgumentParser.cs rename src/{DemaConsulting.SysML2Tools.Tool => DemaConsulting.SysML2Tools.Core}/Query/QueryEngine.cs (89%) create mode 100644 src/DemaConsulting.SysML2Tools.Core/Query/QueryOptions.cs rename src/{DemaConsulting.SysML2Tools.Tool => DemaConsulting.SysML2Tools.Core}/Query/QueryResult.cs (76%) create mode 100644 src/DemaConsulting.SysML2Tools.Core/Query/QueryResultExporter.cs rename src/{DemaConsulting.SysML2Tools.Tool => DemaConsulting.SysML2Tools.Core}/Query/QueryResultRenderer.cs (82%) create mode 100644 src/DemaConsulting.SysML2Tools.Core/Query/QueryResultSerializerContext.cs rename src/{DemaConsulting.SysML2Tools.Tool => DemaConsulting.SysML2Tools.Core}/Query/QueryVerb.cs (82%) rename src/{DemaConsulting.SysML2Tools.Tool => DemaConsulting.SysML2Tools.Core}/Utilities/QualifiedNameShortener.cs (80%) delete mode 100644 src/DemaConsulting.SysML2Tools.Tool/Query/QueryArgumentParser.cs create mode 100644 src/DemaConsulting.SysML2Tools.Tool/Query/QueryCliArgumentParser.cs delete mode 100644 src/DemaConsulting.SysML2Tools.Tool/Query/QueryOptions.cs delete mode 100644 src/DemaConsulting.SysML2Tools.Tool/Query/QueryResultSerializerContext.cs rename test/{DemaConsulting.SysML2Tools.Tool.Tests => DemaConsulting.SysML2Tools.Tests}/Query/QueryOmgFixtureTests.cs (88%) rename test/{DemaConsulting.SysML2Tools.Tool.Tests => DemaConsulting.SysML2Tools.Tests}/Query/QueryRenderingTests.cs (78%) create mode 100644 test/DemaConsulting.SysML2Tools.Tests/Query/QueryResultExporterTests.cs rename test/{DemaConsulting.SysML2Tools.Tool.Tests => DemaConsulting.SysML2Tools.Tests}/Utilities/QualifiedNameShortenerTests.cs (85%) diff --git a/.reviewmark.yaml b/.reviewmark.yaml index 1e07a12..1fe1d58 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: @@ -959,14 +994,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..eea6dee 100644 --- a/docs/design/introduction.md +++ b/docs/design/introduction.md @@ -114,6 +114,24 @@ 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 - **DemaConsulting.SysML2Tools.Tool** (System) — dotnet tool: thin CLI wrapper and orchestration - **Program** (Unit) — entry point and execution orchestrator @@ -186,6 +204,9 @@ 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`) - **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/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..f6e4cbf --- /dev/null +++ b/docs/reqstream/sysml2-tools-core/query.yaml @@ -0,0 +1,216 @@ +--- +# 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-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/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..b31e15a --- /dev/null +++ b/docs/verification/sysml2-tools-core/query.md @@ -0,0 +1,202 @@ +## 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`. These tests call +`QueryEngine`, `QueryResultRenderer`, and `QueryResultExporter` 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`, and `QueryResultExporterTests` 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. + +### 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. + +### 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/requirements.yaml b/requirements.yaml index 5264936..61e497d 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 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); + } + } } From 3d9e44240a3b38900e7973d55e158ef67f3feb32 Mon Sep 17 00:00:00 2001 From: Malcolm Nixon Date: Mon, 20 Jul 2026 20:36:19 -0400 Subject: [PATCH 2/2] Fix QualifiedNameShortener decomposition/doc gap left by Core Query move QualifiedNameShortener physically moved from Tool/Utilities to Core/Utilities as part of the public Query API refactor, but its decomposition-tree entry and legacy per-unit docs/reqstream/review-set were never updated to match: - docs/design/introduction.md: moved the unit's decomposition-tree bullet from Tool's Utilities subsystem to Core's Query subsystem; added Core's Utilities/ folder to the source-tree map. - docs/design/sysml2-tools-tool/utilities.md and docs/verification/sysml2-tools-tool/utilities.md: now describe PathHelpers as the subsystem's sole remaining unit, with a pointer to the new Core location. - Deleted the orphaned Tool-side per-unit docs (design/verification/reqstream) for QualifiedNameShortener; moved their 9 QualifiedNameShortenerTests.cs test-scenario write-ups into docs/verification/sysml2-tools-core/query.md. - docs/reqstream/sysml2-tools-tool/utilities.yaml: dropped the duplicate QualifiedNameShortening requirement; requirements.yaml: removed the now-deleted file's include. - docs/reqstream/sysml2-tools-core/query.yaml: added a new SysML2Tools-Core-Query-QualifiedNameShortening requirement tracing the 9 unit-level tests (previously untracked after the old Tool-side requirements were removed). - .reviewmark.yaml: removed the stale SysML2Tools-Tool-Utilities-QualifiedNameShortener review-set (now redundant with SysML2Tools-Core-Query, which already covers the unit's new location). Verified: build.ps1 (367 + 551x3 tests) and lint.ps1 both green. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .reviewmark.yaml | 14 ---- docs/design/introduction.md | 7 +- docs/design/sysml2-tools-tool/utilities.md | 46 ++++-------- .../utilities/qualified-name-shortener.md | 55 -------------- docs/reqstream/sysml2-tools-core/query.yaml | 22 ++++++ .../sysml2-tools-tool/utilities.yaml | 19 +---- .../utilities/qualified-name-shortener.yaml | 31 -------- docs/verification/sysml2-tools-core/query.md | 56 ++++++++++++-- .../sysml2-tools-tool/utilities.md | 15 ++-- .../utilities/qualified-name-shortener.md | 74 ------------------- requirements.yaml | 1 - 11 files changed, 98 insertions(+), 242 deletions(-) delete mode 100644 docs/design/sysml2-tools-tool/utilities/qualified-name-shortener.md delete mode 100644 docs/reqstream/sysml2-tools-tool/utilities/qualified-name-shortener.yaml delete mode 100644 docs/verification/sysml2-tools-tool/utilities/qualified-name-shortener.md diff --git a/.reviewmark.yaml b/.reviewmark.yaml index 1fe1d58..7c338ca 100644 --- a/.reviewmark.yaml +++ b/.reviewmark.yaml @@ -937,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: diff --git a/docs/design/introduction.md b/docs/design/introduction.md index eea6dee..f11eba2 100644 --- a/docs/design/introduction.md +++ b/docs/design/introduction.md @@ -132,6 +132,9 @@ system, subsystem, and unit levels: 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 @@ -158,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:** @@ -207,6 +207,7 @@ reviewers an explicit navigation aid from design to code: - **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-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/query.yaml b/docs/reqstream/sysml2-tools-core/query.yaml index f6e4cbf..057cd68 100644 --- a/docs/reqstream/sysml2-tools-core/query.yaml +++ b/docs/reqstream/sysml2-tools-core/query.yaml @@ -68,6 +68,28 @@ sections: - 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 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/verification/sysml2-tools-core/query.md b/docs/verification/sysml2-tools-core/query.md index b31e15a..d1a4ad1 100644 --- a/docs/verification/sysml2-tools-core/query.md +++ b/docs/verification/sysml2-tools-core/query.md @@ -4,10 +4,13 @@ The Query subsystem is verified by direct tests in `test/DemaConsulting.SysML2Tools.Tests/Query/QueryOmgFixtureTests.cs`, -`QueryRenderingTests.cs`, and `QueryResultExporterTests.cs`. These tests call -`QueryEngine`, `QueryResultRenderer`, and `QueryResultExporter` 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. +`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 @@ -32,8 +35,8 @@ scenarios listed under "Test Scenarios (Tool Test Project)" below run via `dotne ### Acceptance Criteria -- All `QueryOmgFixtureTests`, `QueryRenderingTests`, and `QueryResultExporterTests` pass with - zero failures across all three target frameworks. +- 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; @@ -48,6 +51,9 @@ scenarios listed under "Test Scenarios (Tool Test Project)" below run via `dotne `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 @@ -138,6 +144,44 @@ exporter does not create parent directories or suppress the resulting write fail **`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`, 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 61e497d..fce45d9 100644 --- a/requirements.yaml +++ b/requirements.yaml @@ -54,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