SysML2Tools is a free, open-source .NET CLI tool and library that parses SysML v2 textual model files and renders them as professional nested block diagrams. It is designed for .NET teams in regulated industries who author SysML v2 models as part of a Model-Based Systems Engineering (MBSE) practice and need to generate diagram images programmatically — without a paid GUI tool or a non-.NET runtime dependency.
A secondary audience is AI agents iterating on SysML v2 models: the lint command provides
structured diagnostic output (file, line, column, severity) that enables a model-fix loop
without requiring a rendered diagram, and the query command lets an AI agent answer
architecture and traceability questions (dependencies, requirement trace links, structure,
behavior) directly from the semantic model instead of reading raw .sysml files.
This guide covers the installation, configuration, and use of SysML2Tools. It describes
the lint, render, query, and help commands, the global CLI flags, view selection
behavior, output formats, and depth limiting.
This user guide covers:
- Installation via
dotnet tool install - Linting SysML v2 workspaces with the
lintcommand - Rendering diagrams with the
rendercommand - Querying the semantic model with the
querycommand - Getting command and verb-specific help with the
helpcommand - Global CLI options
- View selection and depth limiting
- Self-validation for tool qualification evidence
Install the tool globally using the .NET CLI:
dotnet tool install -g DemaConsulting.SysML2Tools.ToolSysML2Tools operates on a workspace — a set of .sysml files loaded together. The OMG
standard library (stdlib) is always implicitly included. You specify workspace files as
glob patterns on the command line:
# Single file
sysml2tools lint model.sysml
# All .sysml files under a directory
sysml2tools lint "src/**/*.sysml"
# Multiple patterns
sysml2tools render "common/**/*.sysml" "system/**/*.sysml" --output out
# Recursive match with an exclusion (files under src/generated are skipped)
sysml2tools lint "src/**/*.sysml" "!src/generated/**"The lint command loads a workspace, resolves the semantic model, and reports all
diagnostics. It exits with a non-zero code if any errors are present.
sysml2tools lint "src/**/*.sysml"Diagnostic output includes file path, line, column, severity, and message:
model.sysml:12:5: error: unresolved reference 'VehicleSystem'
model.sysml:34:1: warning: view 'Overview' references unsupported viewpoint kind
This structured output is suitable for:
- CI/CD pipelines that fail the build on model errors
- AI-assisted model authoring loops that parse diagnostics and propose fixes
The render command loads a workspace, resolves a view, and renders it to SVG or PNG.
--output names an output directory (default: current directory); --format selects
svg (default) or png.
# Render to SVG
sysml2tools render model.sysml --output out --format svg
# Render to PNG
sysml2tools render model.sysml --output out --format png
# Render a named view from a multi-view workspace
sysml2tools render "src/**/*.sysml" --view SystemContext --output out --format svg
# Auto-render the top-level part def when no view is defined
sysml2tools render model.sysml --auto --output out --format svg| Condition | Behavior |
|---|---|
| Exactly one view in workspace | Render it |
Zero views, --auto specified |
Auto-render BDD of top-level part def silently |
Zero views, no --auto |
Informational message; no output files written |
| Multiple views, none specified | Render every declared view (one output file per view) |
Multiple views, --view <name> |
Render only the named view |
--view <name> names a view that does not exist |
Error: lists available view names, exits non-zero |
Rendering normally requires the SysML source to declare a view. --view-type/--view-target
(with an optional --filter) instead render any resolvable element on demand, without
requiring any model changes:
# Render an interconnection-style view of a part def, with no view def in the model
sysml2tools render model.sysml --view-type interconnection --view-target Pkg::Engine --output out
# Render a general view narrowed to elements carrying a @Safety metadata annotation
sysml2tools render model.sysml --view-type general --view-target Pkg::Vehicle --filter @Safety --output out-
--view-type <kind>— one ofgeneral,interconnection,state,action,sequence,grid,browser. Selects the same layout strategyDiagramTypeRouterwould select for a declared view'srender asGeneralDiagram;/asInterconnectionDiagram;/etc. member — see "View Body Statements" below. -
--view-target <qualified-name>— the element to render. Must resolve in the workspace, must not be a standard-library element, and must not be aview/viewpoint/import/metadata/transition/connectionnode (these kinds cannot serve as a dynamic view's rendered content). -
--view-typeand--view-targetmust be supplied together;--filteris valid only alongside both of them; none of the three may be combined with--viewor--auto. Violating any of these rules reports a specific error and a non-zero exit code, rather than silently picking one option over another. -
Each
--view-typekind runs a cheap, necessary-but-not-sufficient structural compatibility pre-check against the target before rendering, so an obviously incompatible target reports a clear diagnostic instead of an empty or broken diagram:--view-typeCompatibility pre-check general,grid,browserNone — any resolvable, non-stdlib definition or usage is accepted interconnectionTarget must be a part defwith at least one nestedpartfeaturestateTarget must have at least one nested state transition or statefeatureactionTarget must have at least one succession or nested actionfeaturesequenceTarget must have at least one nested messageusageKnown limitation — sequence view. The AST has no dedicated "lifeline" node;
SequenceViewLayoutStrategyderives lifelines purely from eachmessageusage's endpoint references. Thesequencepre-check therefore approximates "at least one lifeline" as "at least one nestedmessageusage" — necessary (zero messages guarantees zero lifelines) but not sufficient: a target whose message endpoints fail to resolve to any lifeline still passes this pre-check yet still renders the near-blank canonicalLayoutTreesentinel. A full message-edge-walk validation was deliberately not implemented for this check (seeROADMAP.md's "View dynamics refinements" item); this is a documented gap, not a silent omission.
A view def/view declaration's body may contain render <target>; and filter [<expr>];
statements; a named view usage's body may additionally contain expose <name>; statements
(per the SysML v2 grammar, expose is only valid inside a view usage's body, not a view def
definition's body). For the General View strategy (the diagram produced when no more specialized
view kind applies), expose now scopes the rendered diagram instead of always rendering the
entire workspace:
-
expose <name>;(valid only inside a namedviewusage's body, not aview defdefinition's body) — per the SysML v2 grammar,exposehas four distinct forms with independent scoping behavior, driven by whether the grammar alternative is MembershipExpose or NamespaceExpose and whether a trailing::**requests recursion:expose X;(bare MembershipExpose) — scopes toXitself only, not its containment subtree. IfXresolves to a usage (e.g.part myVehicle : Vehicle;) rather than a definition, its resolved type (Vehicle) is also included, itself only (not the type's subtree either).expose X::**;(recursive MembershipExpose) — scopes toXand its entire containment subtree:Xplus every declaration whose qualified name isXor is contained within it.expose X::*;(bare NamespaceExpose) — scopes to onlyX's direct (one-level) children, notXitself and not deeper descendants.expose X::*::**;(recursive NamespaceExpose) — likeexpose X::*;, still excludesXitself (a NamespaceExpose only ever exposesX's Memberships — its members — neverXas a member of itself), but additionally includes descendants beyond direct children, at any depth — unlikeNamespaceDirectChildren, which stops at one level.
If
Xdoes not resolve to any declaration in the workspace (for example, a typo), the tool falls back to rendering the full workspace for that view — but now also reports a diagnostic identifying the unresolved name, so the mistake is visible instead of silently rendering everything with no signal. The bracket-filter formexpose <path>::**[<expr>];is now (Phase 2a) evaluated using the same supported subset described below for standalonefilter <expr>;: when the bracketed expression parses and evaluates successfully, that entry narrows to only the matched descendant definitions within<path>'s own containment subtree, instead of the whole subtree — eachexposeentry in a view is evaluated independently, so one bracket-filtered entry's narrowing never affects any otherexposeentry in the same view. A bracket expression that fails to parse or falls outside the supported subset degrades gracefully to whole-subtree inclusion for that entry (regardless of whether the entry's own form was otherwise non-recursive), with a diagnostic identifying the failed expression and reason. -
render <target>;— per the SysML v2 grammar, this names a rendering style/format (e.g.asTreeDiagram,asElementTable).render asTreeDiagram;,render asInterconnectionDiagram;,render asGeneralDiagram;,render asStateTransitionDiagram;,render asActionFlowDiagram;,render asSequenceDiagram;, andrender asGridDiagram;now each select their corresponding layout strategy, taking precedence over the name/supertype heuristicDiagramTypeRouterotherwise applies (the same tokens the--view-typedynamic-view flag maps to — see "Dynamic (Ad-Hoc) Views" above). Every other rendering-style name (asElementTable,asTextualNotation, or an unrecognized name) — and a view declaring norendermember at all — has no effect on which strategy renders the view; seeROADMAP.mdfor further rendering-style selectors that may be added in future. -
filter <expr>;— a standalone view-body filter statement is now evaluated for a supported subset of SysML v2 filter-expression syntax (Phase 1): metadata classification tests (@Type,@Pkg::Type), boolean connectives (and,or,not,xor,&,|), parenthesization, and(as Type).attributereads (bare, or compared with==/!=against a scalar literal). When the expression parses and evaluates successfully, the rendered scope is narrowed to the definitions the predicate matches. Any construct outside this subset (istype/hastype/all, arithmetic, conditional expressions, general feature-chain navigation, etc.) — or any syntax error — produces an explicit "unsupported filter construct" (or syntax-error) diagnostic and falls back to rendering the resolved (expose) scope unfiltered, exactly as before. The bracketedexpose <path>::**[<expr>]filter form (Phase 2a) is evaluated using this identical supported subset, perexposeentry — see above. Full evaluation of the remaining Phase 1-excluded constructs (istype/hastype/all, arithmetic, conditional expressions, general feature-chain navigation) is planned future work — seeROADMAP.md. -
A view with no
exposestatement (including the--auto-synthesized view) renders the full workspace, exactly as before this scoping behavior was introduced.
Every view kind honors expose scoping: General, Grid, and Browser Views apply the resolved
scope directly as a filter over their full applicable content. Interconnection, State
Transition, Action Flow, and Sequence Views each render exactly one selected root's contents, so
they instead use the resolved scope in two steps: first, restricting which root the view's own
heuristic selects to one relevant to the scope (the current heuristic root itself, an inner
element of it, or a definition that contains it) — an expose statement naming an unrelated
definition yields no root and an empty diagram; second, narrowing that selected root's own
children (parts, states, actions, or lifelines) to those within the resolved scope. A view with
no expose statement (including the --auto-synthesized view) renders unchanged, exactly as
before this scoping behavior was introduced, for every view kind.
For an Interconnection View specifically, a scope that names no single root definition is not
always an empty diagram: when the scope directly includes one or more concrete top-level part
feature usages instead — for example expose Subsystem::*; where Subsystem is itself only a
namespace-like part def whose only nested content is a single part feature usage, so no
single part def qualifies as "the" root — those feature usages render directly, side by side,
with no enclosing frame around them. A scope that matches neither a root definition nor any
top-level part feature usage still renders the empty diagram described above.
Also specifically for the Interconnection View: when a nested part's own type is itself a
container (a part def with its own nested parts), how deep the diagram recurses into that
part's own interior depends on whether that specific part's own branch is matched by a recursive
expose subject — decided independently for each top-level part, not once for the whole
diagram. A part matched by at least one recursive form (expose X::**; or expose X::*::**;),
or any part at all when there is no expose statement, has its own branch recurse fully — every
nested container's own interior is shown within that branch, at any depth. A part matched only
by non-recursive forms (expose X; and/or expose X::*;) has its own branch limited to that
part's own direct part children: a deeper nested part still renders as its own box within that
branch, but its own interior is not drawn. Because this decision is per-branch, a single view can
mix both outcomes: expose SystemA::**; expose SystemB; fully recurses into SystemA's own
composed structure while stopping SystemB's branch at its own direct children, in the very same
diagram — a sibling part matched by a different subject is never affected by another part's own
recursion kind. For example, expose System; expose System::*; shows System's direct part
children as boxes, but does not expand into any of those parts' own nested structure, even if
their types have one — whereas expose System::**; shows every level.
When the no-single-root scoped fallback (above) collects top-level parts from more than one containing definition, a connection declared inside one of those definitions always resolves against that definition's own top-level parts only — even if a different exposed definition happens to declare a same-named part.
Named view Name { ... } usages (not just view def declarations) are also now recognized as
their own renderable declarations: a workspace containing both view def declarations and named
view usages surfaces both kinds as views the render command discovers and renders.
An Interconnection View's connector endpoints now show the SysML port name from the connection's
endpoint reference (for example a connection between StepperMotorX.encoder and
LBO3AxisGantry.J40 labels its two ports encoder and J40), instead of leaving every port
unlabeled. When several distinct connections wire the same two parts (for example separate
power, encoder, and sensor connections between one controller and one motor), each
connection now renders as its own independently-routed connector line, rather than visually
collapsing onto a single shared line. A connection whose endpoint reaches into a part nested
inside a container (for example connect psu to board.cpu) shows the port label for the true
nested target (cpu), but the connector line itself still terminates at the containing board
box's own boundary rather than continuing on to the inner part — routing a connector all the way
into a nested container remains a known limitation.
The three view body statements look similar but do very different jobs. This is a common point of confusion, so it is worth stating plainly:
render <target>;looks like it should select what's shown, but it does not — useexposefor that.
| Statement | What it actually does |
|---|---|
expose <name>; |
The only mechanism scoping which model content appears in the diagram (see above). |
render <renderingKind>; |
Selects a rendering style — see "View Body Statements" above. Never scopes content. |
filter <expr>; |
Narrows scope (Phase 1, and Phase 2a per bracketed expose); unsupported falls back unfiltered. |
package Vehicle {
part def Engine {
part cylinder[4];
}
part def Vehicle {
part engine : Engine;
part wheel[4];
}
part myVehicle : Vehicle;
view EngineOnlyView {
expose Engine;
render asTreeDiagram;
}
view EngineRecursiveView {
expose Engine::**;
render asTreeDiagram;
}
}
Both views declare render asTreeDiagram;, so they render via BrowserViewLayoutStrategy as an
indented tree of rows rather than the General View's nested boxes. render never narrows the
scope, only expose does — BrowserViewLayoutStrategy honors expose scoping identically to
every other layout strategy.
EngineOnlyView's bareexpose Engine;is non-recursive (MembershipExact): it renders only theEnginedefinition itself —cylinderis not included, since a bareexpose X;no longer implies the whole containment subtree. Confirmed by hand-rendering this exact fixture: the tree contains a single row,Engine.EngineRecursiveView'sexpose Engine::**;is recursive (MembershipRecursive): it rendersEngine's entire containment subtree —Engineand itscylinderpart (two rows) — the unchanged whole-subtree behavior from before this fix.
In both views, Vehicle, myVehicle, and wheel are excluded entirely, since neither Engine
nor its subtree contains them. Removing the expose statement (leaving only render asTreeDiagram;, or an empty view body) renders the full workspace instead.
Note:
exposetargets are qualified names (::-separated), not dotted member-access chains.expose myVehicle.engine;is a syntax error, not merely an unresolved reference — the grammar'squalifiedNamerule does not accept.. To scope to a specific part usage, expose the usage itself by its own name (as in Example B below), not a dotted path into it.
package Vehicle {
part def Engine {
part cylinder[4];
}
part def Vehicle {
part engine : Engine;
part wheel[4];
}
part myVehicle : Vehicle;
view UsageExposeView {
expose myVehicle;
render asTreeDiagram;
}
}
Here expose myVehicle; names a usage (myVehicle : Vehicle), not a def, and is
non-recursive (MembershipExact). The tool resolves myVehicle's own Typing edge to find the
definition it is typed by (Vehicle), and adds that resolved type to the scope too — using the
same exact-match (not whole-subtree) recursion kind, since the usage's own expose was itself
non-recursive. Confirmed by hand-rendering this exact fixture: the tree contains the myVehicle
row and the Vehicle row, but neither Vehicle's own engine/wheel parts nor Engine's
cylinder are included, because exact-match scoping does not pull in either exposed name's
descendants.
To render myVehicle's and Vehicle's full nested structure instead, expose recursively —
expose myVehicle::**; — which scopes to the union of myVehicle's and Vehicle's entire
containment subtrees (unchanged whole-subtree behavior), including engine, wheel, and (via
engine's own type) Engine's cylinder part.
Contrast this with expose Vehicle; (exposing the definition directly): that scopes to just
Vehicle itself (exact match), without needing to resolve any Typing edge, since a
definition's own qualified name is already the exact-match subject. Exposing a usage takes one
extra hop — through the usage's type reference — to add the same kind of definition to the scope
that exposing a def reaches directly; in both cases, recursion (::**) is what controls
whether descendants are included, independent of whether the initial target was a usage or a
definition.
Use --walk-depth <n> to limit the nesting depth rendered. Parts beyond the limit are replaced
with an ellipsis footer (+N more…). Silent omission is never used — truncation is always
visible in the output.
sysml2tools render model.sysml --output out --walk-depth 3| Extension | Format | Notes |
|---|---|---|
.svg |
SVG | Zero external dependencies |
.png |
PNG | SkiaSharp (MIT); pixel-identical across platforms |
PNG output uses an embedded Noto Sans font to guarantee pixel-identical output across Windows, Linux, and macOS.
The query command loads a workspace, resolves the semantic model, and answers
model-comprehension and analysis questions via 12 verbs. Every verb accepts
--format markdown (default) or --format json, and --include-stdlib to include
standard-library elements (excluded by default). Output is always sorted alphabetically by
qualified name, regardless of format, for stable and reproducible results.
query <verb> --help (and help query <verb>) shows a real example invocation for that
verb and a schema hint describing the Markdown/JSON output shape; query --help (and
help query, with no verb) shows a "typical workflow" note recommending list/find first
to discover exact qualified names before using an element-scoped verb.
# What does this element depend on? (outgoing edges: supertypes, typing, imports)
sysml2tools query uses --element Model::Vehicle "src/**/*.sysml"
# What depends on this element? (incoming edges)
sysml2tools query used-by --element Model::Engine "src/**/*.sysml"
# Combined "Dependencies" section prose: what it depends on, and what depends on it
sysml2tools query dependencies --element Model::Engine "src/**/*.sysml"
# Transitive blast radius of a change, optionally bounded
sysml2tools query impact --element Model::Engine --walk-depth 2 "src/**/*.sysml"
# Also follow connect/bind edges, so a part joined to the rest of the assembly only by
# connectors is no longer reported as impacting nothing
sysml2tools query impact --element Model::System::motorA --include-connections "src/**/*.sysml"
# A single-element "fact sheet": kind, supertypes, typing, annotations, applied metadata, children
sysml2tools query describe --element Model::Vehicle "src/**/*.sysml"
# Supertype/subtype tree
sysml2tools query hierarchy --element Model::Vehicle --direction both "src/**/*.sysml"
# Requirement satisfy/verify/allocate relationships
sysml2tools query requirements --element Model::Requirements::TopSpeed "src/**/*.sysml"
# Ports and typed features exposed by a definition
sysml2tools query interface --element Model::Vehicle "src/**/*.sysml"
# Resolved connection endpoints (including dotted feature chains)
sysml2tools query connections --element Model::Vehicle "src/**/*.sysml"
# States and guarded transitions
sysml2tools query states --element Model::VehicleStates "src/**/*.sysml"
# Enumerate elements matching a kind and/or name substring
sysml2tools query list --kind requirement "src/**/*.sysml"
sysml2tools query find --name Engine "src/**/*.sysml" --format json
# Embed the report under a custom heading (e.g., when appending to a larger document)
sysml2tools query describe --element Model::Vehicle --depth 2 --heading "Vehicle Report" "src/**/*.sysml"By default, query impact follows only reference relationships — specialization, typing,
imports, and similar resolved references — in the reverse direction. A part that is joined to
the rest of an assembly purely by connect statements therefore reports no impacted elements,
because a connector is not a reference.
Upgrade note — the default
impactresult changed. In releases before connection-aware impact analysis existed,query impactfollowedconnectandbindrelationships as if they were ordinary references, whenever a connector named a directly declared element (for exampleconnect b to a;orconnect hub.J1 to motorA;). Those connectors were reported as their raw endpoint — including nested port names such asModel::System::hub::J1, which cannot themselves be used as an--elementsubject. That was never intended, is inconsistent with the reference-only default described above, and has been corrected. As a result, aquery impactcommand that you have not changed may now report fewer rows than it used to — often none — on models that rely onconnectstatements. To get those elements back, deliberately add--include-connections, which now reports them correctly rolled up to their owning part.Upgrade note — connection-aware depth changed since
0.2.0-beta.1. In0.2.0-beta.1,--include-connectionsfollowed at most one connector per traversal path unless you supplied--walk-depth, so leaving--walk-depthoff silently meant "one hop" for connectors while meaning "unlimited" for references.--walk-depthis now the single depth control and counts every relationship equally, so--include-connectionswith no--walk-depthnow follows a connector chain all the way to its end. A command you have not changed may therefore report more rows than it did in0.2.0-beta.1. There is no longer an exact equivalent of the old behavior, because the one-hop bound applied to connectors only while references stayed unlimited; pass the proximity you actually want instead, such as--walk-depth 1for immediate neighbors of every kind.
Adding --include-connections makes impact follow connect and bind relationships as
well. Three rules apply:
- Connectors are followed in both directions. A connector's two ends carry no "source causes target" meaning, so the connected element is reported no matter which end you query from. Querying a motor reports the hub it is plugged into, and querying that hub reports every motor plugged into it.
- Port endpoints roll up to the part that owns them. Connectors join nested ports, but you
normally ask about parts. An endpoint such as
System::hub::J1is reported asSystem::hub, with the actual port named in the entry's notes (and in theViaQualifiedNamefield of--format jsonoutput) so nothing is lost. Roll-up applies only to endpoints that are not themselves declared elements — typically ports inherited through a typed usage such aspart hub : Hub. An endpoint that is a declared element, such as a directly connected sibling part (connect alpha to beta;) or a port declared inline on a usage, is reported as-is, andViaQualifiedNameis then absent. - One uniform depth.
--walk-depth <n>counts every relationship equally — aconnecthop, abindhop, a specialization, and a typing reference each cost exactly one. So--walk-depth 3means "everything within 3 relationships of this element", whatever kinds of relationship those turn out to be. Omitting--walk-depthmeans unlimited for connectors exactly as it always has for references.--include-connectionsdecides only which relationships exist in the graph, never how far the walk goes.
Because real models connect many parts to a shared hub or bus, an unlimited connection-aware
walk on such an assembly can reach the whole assembly. That is worth knowing when you want
proximity rather than reachability: pass --walk-depth <n> to ask for the neighborhood you
actually care about. Every reported element carries the distance at which it was found, and
that distance is always the shortest number of relationships separating it from your subject.
Omitting the flag leaves reference-only results completely unchanged, and adding it never
removes an element: every element reported without the flag is still reported with it. It can,
however, change how an already-reported element is described. An element that a reference path
reaches at depth 2 may be reached over a connector at depth 1, and is then reported at the
lower depth with Relation Connect and the connector named in its notes, instead of at the
higher depth with its reference relation. --format json entries
additionally carry Depth (the traversal depth), Relation (Connect/Binding for a
connector, or the reference edge kind otherwise), and ViaQualifiedName, so scripts can
distinguish "referenced by" from "connected to" without parsing the human-readable detail text.
| Format | Flag | Notes |
|---|---|---|
| Markdown | default, or --format markdown |
Heading, summary bullets, table (prose bullets for dependencies) |
| JSON | --format json |
Source-generated (AOT-safe) serialization of the same result shape |
Markdown and JSON renderings of the same query always contain the same qualified names in the same order, so either format can be relied on for automated comparisons.
| Verb | Requires --element |
Answers |
|---|---|---|
uses |
yes | What does this element depend on? |
used-by |
yes | What depends on this element? |
dependencies |
yes | What does this element depend on, and what depends on it (combined, as prose)? |
impact |
yes | What is transitively affected by a change (--walk-depth, --include-connections)? |
describe |
yes | What is this element (kind, supertypes, typing, annotations, applied metadata, children)? |
hierarchy |
yes | What is the supertype/subtype tree (--direction up|down|both)? |
requirements |
yes | What satisfy/verify/allocate relationships involve this element? |
interface |
yes | What ports/typed features does this definition expose? |
connections |
yes | What is this element connected to? |
states |
yes | What states and transitions does this element contain? |
list |
no | Enumerate elements, optionally filtered by --kind/--name |
find |
no | Search elements — requires --kind and/or --name |
| Option | Description |
|---|---|
--element <name>, -e <name> |
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 <file> |
Write to this file (default: stdout); render's --output is a directory instead |
--walk-depth <#> |
Max impact-walk depth (impact only); all relationship kinds count equally, unlimited default |
--include-connections |
impact only: also follow connect/bind edges, undirected (see above) |
--direction up|down|both |
Traversal direction (hierarchy verb only) |
--kind <kind> |
Element-kind filter (list/find verbs only) |
--name <substring> |
Name substring filter (list/find verbs only) |
--include-stdlib |
Include OMG standard library elements in results |
--depth <#> |
Markdown heading depth (1-6, default: 1); Markdown output only, no effect on --format json |
--heading <text> |
Replaces default # query <verb>[: <element>]; Markdown only, no effect on JSON |
The export command loads a workspace, resolves the semantic model, and dumps the entire
model — every declaration, every semantic edge, and every diagnostic — as a single JSON
document or as JSON Lines (JSONL). Unlike query, which answers a targeted analysis
question about one element, export is a lossless, bulk dump intended for offline/AI-
assisted analysis of a whole workspace at once (e.g., loading it into a separate tool,
jq-based scripting, or feeding an entire workspace's facts to an LLM in one shot).
# Export the whole workspace as a single indented JSON document (default format)
sysml2tools export "src/**/*.sysml"
# Export as JSON Lines (one compact JSON object per declaration/edge/diagnostic)
sysml2tools export "src/**/*.sysml" --format jsonl
# Write to a file instead of stdout
sysml2tools export "src/**/*.sysml" --format jsonl --output model.jsonl
# Include OMG standard library declarations/edges in the export
sysml2tools export "src/**/*.sysml" --include-stdlib
# Restrict output to a containment subtree, then narrow it with a filter expression
sysml2tools export "src/**/*.sysml" --target Vehicle::Engine --filter "@Deprecated"| Option | Description |
|---|---|
<globs> |
One or more glob patterns for .sysml input files |
--format json|jsonl |
Output format (default: json) |
--output <file> |
Write to this file (default: stdout); render's --output is a directory instead |
--include-stdlib |
Include OMG stdlib decls/edges (excluded by default); diagnostics are never stdlib-filtered |
--target <qualified-name> |
Restrict output to the containment subtree rooted at this element |
--filter <expr> |
Narrow output using a Phase 1 filter expression |
--target <qualified-name> and --filter <expr> compose the same way render's dynamic-view
--view-target/--filter pair does (see "Dynamic (Ad-Hoc) Views" above), but for export
instead of rendering: --target scopes the export to one element's containment subtree,
--filter narrows the declaration/edge set using the same Phase 1 filter-expression subset
(classification tests, boolean connectives, (as Type).attribute reads) — and, when both are
supplied, --target is applied first, with --filter narrowing the already-scoped result
second:
# Only the Engine subtree
sysml2tools export "src/**/*.sysml" --target Vehicle::Engine
# The whole workspace, narrowed to elements carrying @Deprecated
sysml2tools export "src/**/*.sysml" --filter @Deprecated
# The Engine subtree, further narrowed to elements carrying @Deprecated
sysml2tools export "src/**/*.sysml" --target Vehicle::Engine --filter @Deprecated- If
--targetnames a usage/feature (e.g.part myEngine : Engine;) rather than a definition, its resolved type's subtree is included too, so scoping to a usage still yields useful content instead of a near-empty result. - An unresolvable
--target(not present in the workspace, or a standard-library element without--include-stdlib) reports a cleanexport: --target '<name>' was not found in the workspace.error and produces no export — both cases share the same message. - An unparsable or unsupported
--filterexpression does not abort the export: it falls back to the unfiltered (still--target-scoped, if applicable) result, appending a synthetic warning diagnostic ("FilePath": "<--filter>") to the output'sDiagnosticsarray and printing a matchingexport: warning: ...console message — the same graceful-degradation behavior views apply to a non-evaluatablefilter [<expr>];statement. - Exported edges always require both endpoints (source and target, when the source is
non-null) to survive every active narrowing step (stdlib filtering,
--targetscoping, and--filtermatching) — never just one endpoint.
--format json (default) is a single indented document:
{
"Declarations": {
"Model::Vehicle": { "$type": "definition", "Kind": "part def", "...": "..." },
"Model::Engine": { "$type": "definition", "Kind": "part def", "...": "..." }
},
"Edges": [
{ "SourceQualifiedName": "Model::Engine", "TargetQualifiedName": "Model::Vehicle", "Kind": "Composition" }
],
"Diagnostics": []
}Declarationsis a JSON object keyed by qualified name (not an array), so a caller can look up a specific element directly instead of scanning an array.- Each declaration is serialized using its own existing polymorphic
$typediscriminator (the same node types used internally by the parser/semantic model), so the export is a faithful, round-trip-capable dump — not a separate, narrower summary shape likequery's result. EdgesandDiagnosticsare plain JSON arrays.
--format jsonl emits one compact JSON object per line, each tagged with a "Kind"
discriminator so a line-oriented consumer (grep, jq -c, streaming parsers) can process
records without buffering the whole document:
{"Kind":"declaration","QualifiedName":"Model::Vehicle","Node":{"$type":"definition","...":"..."}}
{"Kind":"edge","SourceQualifiedName":"Model::Engine","TargetQualifiedName":"Model::Vehicle","EdgeKind":"Composition"}
{"Kind":"diagnostic","FilePath":"model.sysml","Line":1,"Column":1,"Severity":"Error","Message":"..."}Declarations are emitted first, then edges, then diagnostics. Both output shapes exclude
OMG standard-library declarations and edges by default (mirroring query's
--include-stdlib convention exactly); diagnostics are always included, since
WorkspaceLoader diagnostics only ever come from the user's own supplied files (the stdlib
symbol table is a pre-resolved seed, never re-parsed).
The following global options are accepted before the verb:
| Option | Description |
|---|---|
-v, --version |
Display version information |
-?, -h, --help |
Display help |
--silent |
Suppress console output |
--validate |
Run self-validation tests |
--results <file>, --result <file> |
Write validation results to .trx or .xml |
--depth <#> |
Set heading depth for validation output (default: 1) |
--log <file> |
Write all output to a log file |
In addition to the global -?/-h/--help flag (see the table above), help is also a
first-class top-level command: sysml2tools help [command] [verb]. Both forms produce
identical output for the same target — help <command> and <command> --help share a
single source of truth for each command's help text, so neither can drift out of sync with
the other.
# Top-level help (same as bare --help)
sysml2tools help
# Command-specific help (identical to `lint --help`/`render --help`/`export --help`)
sysml2tools help lint
sysml2tools help render
sysml2tools help export
# Query verb overview (identical to `query --help`)
sysml2tools help query
# Query verb-specific help (identical to `query <verb> --help`)
sysml2tools help query hierarchyAn unrecognized command or verb (e.g., sysml2tools help bogus, sysml2tools help query bogus-verb) reports a clear error naming the invalid token rather than crashing. Note that
--silent suppresses help's console output exactly as it suppresses every other command's
output — there is no special case that lets help bypass --silent.
Self-validation exercises the tool against embedded test models and produces a structured report. This provides tool qualification evidence for regulated environments.
sysml2tools --validate
sysml2tools --validate --results results.trx
sysml2tools --validate --results results.xmlThe results file format is determined by the extension: .trx for MSTest TRX format,
.xml for JUnit XML format.
Use --depth <#> to embed the validation report at a specific heading level within a
larger markdown document:
sysml2tools --validate --depth 2SysML2Tools is structured as four NuGet packages. Library consumers can take a dependency on the core library alone, without pulling in the full CLI tool:
| Package | Contents |
|---|---|
DemaConsulting.SysML2Tools.Language |
Library: SysML v2/KerML parser, AST, semantic model |
DemaConsulting.SysML2Tools.Stdlib |
Library: pre-compiled SysML v2 standard library |
DemaConsulting.SysML2Tools.Core |
Library: parser, semantic model, layout, IRenderer interface |
DemaConsulting.SysML2Tools.Tool |
CLI tool: lint, render, query, and help commands |
Consumers who need only the parsed semantic model, LayoutTree, or rendering interfaces
take a dependency on DemaConsulting.SysML2Tools.Core only, which automatically pulls in
DemaConsulting.SysML2Tools.Language and DemaConsulting.SysML2Tools.Stdlib as NuGet
dependencies. Consumers who need the CLI install DemaConsulting.SysML2Tools.Tool as a
dotnet tool. Each package ships its own generated Markdown API reference documentation
alongside its assembly.
This project follows the Continuous Compliance methodology. Compliance evidence (requirements, trace matrix, quality reports) is generated automatically on every CI run.
N/A