diff --git a/README.md b/README.md index 11006da9f..9caf7cee1 100644 --- a/README.md +++ b/README.md @@ -271,7 +271,7 @@ Guidance on selecting a client, the coverage of the four newer clients, and the ## Goals - **Performance:** sub-millisecond parsing, a single static binary, and no JVM or Eclipse runtime -- **Completeness:** SysML v2 textual notation support (104 of 104 standard library files parse cleanly: 94 vendored OMG files and 10 OpenSysML extensions) +- **Completeness:** SysML v2 textual notation support (105 of 105 standard library files parse cleanly: 94 vendored OMG files and 11 OpenSysML extensions) - **Executable models:** instantiate, evaluate and simulate, turning specifications into running systems - **Practical ergonomics:** multi-file workspaces, incremental analysis and detailed diagnostics @@ -281,7 +281,7 @@ The project is under active development, with the core infrastructure operationa | Component | Status | |-----------|--------| -| Lexer/Parser (structural + behavioral grammar) | ✅ Operational (104/104 stdlib clean - see [conformance gate](internal/workspace/libs/stdlib_conformance_test.go)) | +| Lexer/Parser (structural + behavioral grammar) | ✅ Operational (105/105 stdlib clean - see [conformance gate](internal/workspace/libs/stdlib_conformance_test.go)) | | Symbol resolution & type system | ✅ Complete | | Semantic layer (operators, builtins, validation) | ✅ Complete | | Feature chain resolution (member access) | ✅ Complete | @@ -327,7 +327,7 @@ What these numbers cannot show: the OMG corpora are demonstrations rather than a **Current commit:** All tests pass (`go test -race ./...`), builds clean (`go build ./...`). **Test coverage:** top-level `Test` functions (counted from the `_test.go` files, as `go test ./...` runs them) covering parsers, semantics, runtime (actions, states, instances, operators, validation), behind golden ASTs, negatives, execution conformance cases, golden traces, runtime robustness cases and gRPC conformance and robustness cases. The figures are counted from the tree when the documentation site is built into the test inventory of [spec compliance](docs/project/spec-compliance.md), never committed, so a branch adding a test does not rewrite this page. A test skips only for want of something the run did not provide, and says what: the held-image round trip declines a conformance case that creates no instance, a few gate on a PDF or Mermaid toolchain, a pinned pilot artifact, the PSSM suite, a locale, a case-insensitive filesystem or a live Flexo stack, and the OMG corpus gates skip until the corpora are downloaded unless asked to fail. -**Parser coverage:** 104/104 bundled library files parse cleanly — the 94 official SysML v2 standard library files and the non-normative `OpenSysML Libraries/OpenSysMLMathFunctions.kerml`, `OpenSysML Libraries/DocumentQueries.sysml`, `OpenSysML Libraries/IdentityMetadata.sysml`, `OpenSysML Libraries/DiagramLayout.sysml`, `OpenSysML Libraries/OOSEM.sysml`, `OpenSysML Libraries/MOSA.sysml`, `OpenSysML Libraries/StateSpaceIntegration.sysml`, `OpenSysML Libraries/Stochastic.sysml`, `OpenSysML Libraries/RandomFunctions.kerml` and `OpenSysML Libraries/Simulation.sysml` extensions. Conformance verified by [stdlib_conformance_test.go](internal/workspace/libs/stdlib_conformance_test.go). Grammar reference: [OMG Xtext grammar](https://github.com/Systems-Modeling/SysML-v2-Pilot-Implementation/tree/master/org.omg.kerml.xtext/src/org/omg/kerml/xtext). +**Parser coverage:** 105/105 bundled library files parse cleanly — the 94 official SysML v2 standard library files and the non-normative `OpenSysML Libraries/OpenSysMLMathFunctions.kerml`, `OpenSysML Libraries/DocumentQueries.sysml`, `OpenSysML Libraries/IdentityMetadata.sysml`, `OpenSysML Libraries/DiagramLayout.sysml`, `OpenSysML Libraries/OOSEM.sysml`, `OpenSysML Libraries/MOSA.sysml`, `OpenSysML Libraries/StateSpaceIntegration.sysml`, `OpenSysML Libraries/Stochastic.sysml`, `OpenSysML Libraries/RandomFunctions.kerml`, `OpenSysML Libraries/Simulation.sysml` and `OpenSysML Libraries/MigrationMetadata.sysml` extensions. Conformance verified by [stdlib_conformance_test.go](internal/workspace/libs/stdlib_conformance_test.go). Grammar reference: [OMG Xtext grammar](https://github.com/Systems-Modeling/SysML-v2-Pilot-Implementation/tree/master/org.omg.kerml.xtext/src/org/omg/kerml/xtext). **Behavioral execution:** Calc/constraint/requirement/satisfy functional. Action/state executors handle nested invocation, control flow keywords, loop and conditional statements and the send statement (every conformance case passing). Coverage is self-assessed against the specification text and the normative library: the pinned OMG pilot implementation evaluates expressions but does not execute actions or state machines headlessly, so no external implementation currently adjudicates these rows. See [spec compliance](docs/project/spec-compliance.md). **Reference differential:** 378 files compared diagnostic-by-diagnostic against the pinned OMG pilot implementation (`2026-08`), 347 in full agreement; every divergence is enumerated and adjudicated in [the differential](docs/project/pilot-differential.md), reproducible with `go run -C tools ./cmd/pilot-diff`. **Rejection oracle:** the reverse direction — do we reject what the reference rejects? 306 hand-written invalid models validated by both implementations, 297 rejected by both, 0 the pinned pilot rejects and we accept; the remainder only we reject — the control-node succession rules the pinned pilot leaves unimplemented and a non-Boolean succession guard it accepts once the standard library types it — and every permissiveness gap is enumerated with a reproducer and likely root cause in [the rejection oracle](docs/project/pilot-rejection.md), reproducible with `go run -C tools ./cmd/pilot-reject`. We wrote every case, so the count measures our coverage of the rejection surface, not our conformance — a sample, not a proof. @@ -391,7 +391,7 @@ github.com/Open-MBEE/OpenSysML - **Parser:** hand-written recursive descent (no framework overhead, full error recovery, sub-millisecond parses) - **Grammar source:** OMG pilot Xtext grammars (`SysML.xtext` and `KerMLExpressions`) - **Spec compliance:** [OMG SysML v2.1 Beta 1 / KerML 1.1](https://www.omg.org/spec/SysML/2.0) (2026-08 release) -- **Standard library:** 94 files from [SysML v2 Pilot Implementation 2026-08](https://github.com/Systems-Modeling/SysML-v2-Pilot-Implementation/releases/tag/2026-08), byte-identical, plus the ten non-normative `OpenSysML Libraries/` extensions (`OpenSysMLMathFunctions`, `DocumentQueries`, `IdentityMetadata`, `DiagramLayout`, [`OOSEM`](docs/project/oosem-library.md), [`MOSA`](docs/project/mosa-library.md), `StateSpaceIntegration`, `Stochastic`, `RandomFunctions` and `Simulation`) +- **Standard library:** 94 files from [SysML v2 Pilot Implementation 2026-08](https://github.com/Systems-Modeling/SysML-v2-Pilot-Implementation/releases/tag/2026-08), byte-identical, plus the eleven non-normative `OpenSysML Libraries/` extensions (`OpenSysMLMathFunctions`, `DocumentQueries`, `IdentityMetadata`, `DiagramLayout`, [`OOSEM`](docs/project/oosem-library.md), [`MOSA`](docs/project/mosa-library.md), `StateSpaceIntegration`, `Stochastic`, `RandomFunctions`, `Simulation` and `MigrationMetadata`) - **CI/CD:** GitHub Actions checks pull requests; CircleCI builds and tests `main` and `develop` and publishes releases from tags ## Releases diff --git a/changes/unreleased/positioned-label-fitting.fixed.md b/changes/unreleased/positioned-label-fitting.fixed.md new file mode 100644 index 000000000..537b8ba82 --- /dev/null +++ b/changes/unreleased/positioned-label-fitting.fixed.md @@ -0,0 +1,7 @@ +- **A positioned node's label fits the box its `DiagramLayout::Layout` states; the box is never grown to the label.** The DOT writer word-wraps the head at the stated width and draws it at the largest font size from 14 pt down to 8 pt at which the wrapped lines fit the height, keeps the `«keyword»` and detail lines only while height remains, and cuts and ellipsizes a head that overruns even at 8 pt, using the same glyph estimate the unsized boxes are fitted with; the box's `margin=0` gives the whole of it to the label, as the fit assumes. A migrated Cameo diagram, whose boxes were sized for the name alone, reads as it did: a 449×14 px attribute row holds its one line, `call : doTracking` no longer spills out of its action box, and Graphviz's `size too small for label` warnings on such a model drop to none. A stated box that holds other stated boxes — a part drawn round its members, a definition over its compartment rows — sets its title in the strip above the topmost of them, fitted to that strip, so the title is read as the frame's header rather than covered by the members, which stay where the Layout put them; a box drawn as a cluster round its children is fitted the same way. A stated box, or the strip its members leave it, too short for one 8 pt line or too narrow for one glyph holds no text, and sets its head beside the box instead. A node without a stated size keeps its label-fitted box. +- **A control node or port in a stated box is drawn as its notation symbol, with no text inside.** A decision, merge or choice is a diamond, a fork or join the filled bar, an initial node the filled dot, a final node or terminate action the double ring, and a port its small square; the node's name is set beside the symbol as an `xlabel`, and left out when the view IR marks it as one the model did not give (`Node.NameSynthesized`). Without a stated box these kinds keep their labelled shapes, except that an action's `start` and `done` — the language's names, not the body's — are now the filled dot and the double ring in every graphical form, where they were a large labelled circle. +- **A name the SysML v1 migration made up is not drawn.** The migrator now records every name it spells for an element its source left unnamed — `'start to call'`, `fork2`, `decide`, an `unnamed` ref — once per body, as `metadata MigrationMetadata::SynthesizedName about …;` from the new bundled `MigrationMetadata` library. The renderings read the marker from the model: such a node is drawn as its source drew it — a control node as its bare symbol, a typed usage as `: Type` alone — and an edge whose only text would be such a name carries none, while a triggered transition or a guarded succession keeps its trigger and guard. Names the source gave, however spelled, are never marked; the migration report and results are unchanged. +- **A member drawn under its owner is headed by its name below that owner.** A nested node, or an exposed element whose owner is drawn in the same rendering, no longer repeats the owner's qualified path: `'K-Mirror Offset'::'interpolation Error' : 'Interpolation Error'` inside the `'K-Mirror Offset'` box reads `'interpolation Error' : 'Interpolation Error'`, as a diagram frame shows it. Only the graphical forms' heads change; the text and JSON forms and the LSP keep the qualified name. +- **A positioned DOT drawing leaves the nodes no `Layout` places undrawn, so none lands on a placed box.** When some nodes of a view are positioned and others are not, the DOT writer left the others to `neato`, which set them wherever it found room — over the positioned boxes, in a migrated diagram whose source never drew them. They are now left out, with the edges at them, under a `// not represented:` notice that counts them, so the drawing shows what the source diagram showed; every node drawn is pinned, so the `// layout:` header names `neato -n` or `neato -n2` and never plain `neato`. The new `-render-unplaced strip` (`Options.Unplaced` in the view API) keeps them instead, boxed and packed in rows below the canvas or the positioned boxes' extent, clear of them and of one another; it applies to `-render`, `-render-all` and the `dot` diagrams of `-render-document` and `-render-documents`, and an unknown placement is refused with the two there are. A view with no positioned node is laid out by `dot` as before. +- **A view's layout annotations and `render` members are not drawn as nodes.** The member walk every rendering kind shares leaves out `DiagramLayout::Canvas`, `Layout` and `Route` annotations, wherever they are owned, the `MigrationMetadata::SynthesizedName` markers a migration leaves in a body, and the `render` members a view holds, so a tree over a package of migrated views no longer fills with `metadata`, `x`, `y`, `width`, `height` and `asTreeDiagram` nodes. Every other metadata usage, and a rendering usage outside a view, is drawn as before. +- **A parallel state may carry a metadata usage in its body.** Lowering a `state … parallel` body treated a `metadata` member as unsupported content and refused the whole state machine, so its state rendering came out empty; the annotation is now the state's own, like an attribute or a port, and its substates alone are the regions. diff --git a/cmd/sysml/main.go b/cmd/sysml/main.go index c62067464..b75601323 100644 --- a/cmd/sysml/main.go +++ b/cmd/sysml/main.go @@ -129,6 +129,7 @@ var ( renderAllDir string renderForm string renderPalette string + renderUnplaced string renderDoc string renderDocsDir string docForm string @@ -386,6 +387,10 @@ func runCLI() int { fmt.Fprintln(os.Stderr, "sysml: -render-palette is the palette -render or -render-all fills DOT or PlantUML with; name the view to render with -render or a directory with -render-all") return 2 } + if renderUnplaced != "" && renderView == "" && renderAllDir == "" && renderDoc == "" && renderDocsDir == "" { + fmt.Fprintln(os.Stderr, "sysml: -render-unplaced places the unplaced nodes of a positioned DOT drawing; name what to render with -render, -render-all, -render-document or -render-documents") + return 2 + } // The default stylesheet is asked for on its own; it needs no model, and // writing it is the whole run, so it cannot stand in for another. @@ -510,7 +515,7 @@ func runCLI() int { case convertFormat != "" || renderView != "" || renderDoc != "" || renderAllDir != "" || renderDocsDir != "" || queryText != "" || len(evalExprs) > 0: fmt.Fprintf(os.Stderr, "sysml: %s syncs a change set; it cannot be combined with -convert, -render, -render-all, -render-document, -render-documents, -query or -eval\n", mode) return 2 - case outputPath != "" || fromFormat != "" || renderForm != "" || renderPalette != "" || docForm != "" || diagramForm != "" || pdfEngine != "" || pdfTitlePage || pdfTOC || pdfNumbering: + case outputPath != "" || fromFormat != "" || renderForm != "" || renderPalette != "" || renderUnplaced != "" || docForm != "" || diagramForm != "" || pdfEngine != "" || pdfTitlePage || pdfTOC || pdfNumbering: fmt.Fprintf(os.Stderr, "sysml: %s reads SysML or Turtle inputs and reports the change set; -output, -from and the render options do not apply\n", mode) return 2 case modelChecks.requested(): diff --git a/cmd/sysml/render.go b/cmd/sysml/render.go index 5a225edf5..add4c18a6 100644 --- a/cmd/sysml/render.go +++ b/cmd/sysml/render.go @@ -189,7 +189,8 @@ func caseFolded(text string) string { } // renderOptions is what -render and -render-all write with: the text width, -// and the palette -render-palette names, which must be one there is. +// the palette -render-palette names and the placement -render-unplaced names, +// each of which must be one there is. func renderOptions(width int) (view.Options, error) { options := view.Options{Width: width} if renderPalette != "" { @@ -199,9 +200,28 @@ func renderOptions(width int) (view.Options, error) { } options.Palette = palette } + unplaced, err := unplacedOption() + if err != nil { + return view.Options{}, err + } + options.Unplaced = unplaced return options, nil } +// unplacedOption is the placement -render-unplaced names for the nodes a +// positioned DOT drawing leaves unplaced, which must be one there is; none +// named is the default, leaving them undrawn. +func unplacedOption() (view.Unplaced, error) { + if renderUnplaced == "" { + return "", nil + } + unplaced, ok := view.ParseUnplaced(renderUnplaced) + if !ok { + return "", fmt.Errorf("-render-unplaced: %w", &view.UnknownUnplacedError{Name: renderUnplaced}) + } + return unplaced, nil +} + // loadRenderingModel loads and reports a model whose stdout is reserved for // rendering artifacts. func loadRenderingModel(files []string) (*repl.Session, error) { diff --git a/cmd/sysml/render_document.go b/cmd/sysml/render_document.go index 4864140d4..6e01856c1 100644 --- a/cmd/sysml/render_document.go +++ b/cmd/sysml/render_document.go @@ -82,6 +82,7 @@ func pdfOptions() (docpdf.Options, error) { Stylesheets: page.Stylesheets, BaseDir: filepath.Dir(outputPath), DiagramForm: page.DiagramForm, + Unplaced: page.Unplaced, }, nil } @@ -147,16 +148,21 @@ func documentOptions() docrender.HTMLOptions { MermaidScript: mermaidScriptURL(), MathScript: mathScriptURL(), DiagramForm: view.Form(diagramForm), + Unplaced: view.Unplaced(renderUnplaced), } } // markdownOptions carries the flags shaping a Markdown document. func markdownOptions() docrender.MarkdownOptions { - return docrender.MarkdownOptions{DiagramForm: view.Form(diagramForm)} + return docrender.MarkdownOptions{DiagramForm: view.Form(diagramForm), Unplaced: view.Unplaced(renderUnplaced)} } -// checkDiagramForm rejects a -diagram-form value naming no diagram form. +// checkDiagramForm rejects a -diagram-form value naming no diagram form, and +// a -render-unplaced value naming no placement. func checkDiagramForm() error { + if _, err := unplacedOption(); err != nil { + return err + } if diagramForm == "" || slices.Contains(view.DiagramForms(), view.Form(diagramForm)) { return nil } diff --git a/cmd/sysml/render_unplaced_test.go b/cmd/sysml/render_unplaced_test.go new file mode 100644 index 000000000..5798d60fd --- /dev/null +++ b/cmd/sysml/render_unplaced_test.go @@ -0,0 +1,101 @@ +package main + +import ( + "os" + "path/filepath" + "strings" + "testing" +) + +// TestRenderUnplaced checks -render-unplaced on every path that draws a DOT +// figure: a positioned drawing leaves the nodes no Layout places undrawn by +// default and sets them in a strip below it under `strip`, the same on +// -render, -render-all, -render-document and -render-documents; an unknown +// placement and a placement with nothing to render are refused. +func TestRenderUnplaced(t *testing.T) { + binary := buildCLI(t) + fixture := filepath.Join("..", "..", "internal", "doc", "docrender", "testdata", "placed_report.sysml") + const ( + omitted = "// not represented: 1 node(s) without a position, left undrawn\n" + striped = "// not represented: 1 node(s) without a position, drawn in a strip below the drawing\n" + spare = "spare : Recorder" + ) + check := func(t *testing.T, path, got string, strip bool) { + t.Helper() + notice := omitted + if strip { + notice = striped + } + if !strings.Contains(got, notice) { + t.Errorf("%s: notice %q missing:\n%s", path, notice, got) + } + if strings.Contains(got, spare) != strip { + t.Errorf("%s: spare drawn = %v, want %v:\n%s", path, !strip, strip, got) + } + if strings.Contains(got, "neato -n\n") && strings.Contains(got, `pos="82,-42.5!"`) != strip { + t.Errorf("%s: the strip pins spare below the canvas only when asked:\n%s", path, got) + } + } + readFile := func(t *testing.T, path string) string { + t.Helper() + content, err := os.ReadFile(path) // #nosec G304 -- the test chose this path. + if err != nil { + t.Fatal(err) + } + return string(content) + } + + for _, strip := range []bool{false, true} { + var placement []string + if strip { + placement = []string{"-render-unplaced", "strip"} + } + one := runFiles(t, binary, []string{fixture}, append([]string{"-render", "Placed::placedView", "-render-form", "dot"}, placement...)...) + if one.status != exitHolds { + t.Fatalf("-render exit status = %d, want %d\n%s", one.status, exitHolds, one.output()) + } + check(t, "-render", one.stdout, strip) + + dir := filepath.Join(t.TempDir(), "rendered") + all := runFiles(t, binary, []string{fixture}, append([]string{"-render-all", dir, "-render-form", "dot"}, placement...)...) + if all.status != exitHolds { + t.Fatalf("-render-all exit status = %d, want %d\n%s", all.status, exitHolds, all.output()) + } + check(t, "-render-all", readFile(t, filepath.Join(dir, "Placed.placedView.dot")), strip) + + doc := runFiles(t, binary, []string{fixture}, append([]string{"-render-document", "Placed::PlacedReport", "-diagram-form", "dot"}, placement...)...) + if doc.status != exitHolds { + t.Fatalf("-render-document exit status = %d, want %d\n%s", doc.status, exitHolds, doc.output()) + } + check(t, "-render-document", doc.stdout, strip) + + site := filepath.Join(t.TempDir(), "docs") + docs := runFiles(t, binary, []string{fixture}, append([]string{"-render-documents", site, "-diagram-form", "dot"}, placement...)...) + if docs.status != exitHolds { + t.Fatalf("-render-documents exit status = %d, want %d\n%s", docs.status, exitHolds, docs.output()) + } + pages, err := filepath.Glob(filepath.Join(site, "*.md")) + if err != nil || len(pages) != 1 { + t.Fatalf("-render-documents wrote %v, want one page: %v", pages, err) + } + check(t, "-render-documents", readFile(t, pages[0]), strip) + } + + mermaid := runFiles(t, binary, []string{fixture}, "-render", "Placed::placedView", "-render-form", "mermaid", "-render-unplaced", "strip") + if mermaid.status != exitHolds || strings.Contains(mermaid.stdout, "pos=") { + t.Errorf("Mermaid under -render-unplaced = %d\n%s", mermaid.status, mermaid.output()) + } + + unknown := runFiles(t, binary, []string{fixture}, "-render", "Placed::placedView", "-render-form", "dot", "-render-unplaced", "below") + if unknown.status != exitUnevaluable || !strings.Contains(unknown.stderr, `-render-unplaced: unknown placement "below" of unplaced nodes; the placements are omit, strip`) { + t.Errorf("an unknown placement = %d\n%s", unknown.status, unknown.output()) + } + if unknown.stdout != "" { + t.Errorf("an unknown placement wrote an artifact:\n%s", unknown.stdout) + } + + alone := runFiles(t, binary, []string{fixture}, "-render-unplaced", "strip") + if alone.status != 2 || !strings.Contains(alone.stderr, "-render-unplaced places the unplaced nodes of a positioned DOT drawing") { + t.Errorf("a placement without a view = %d\n%s", alone.status, alone.output()) + } +} diff --git a/cmd/sysml/usage.go b/cmd/sysml/usage.go index 2bbf857e7..505c8bf24 100644 --- a/cmd/sysml/usage.go +++ b/cmd/sysml/usage.go @@ -360,6 +360,7 @@ func doc() usage.Doc { usage.Ex("sysml model.sysml -render Views::vehicleView -o view.mmd", ""), usage.Ex("sysml model.sysml -render Views::vehicleView -render-form dot", ""), usage.Ex("sysml model.sysml -render Views::vehicleView -render-form dot -render-palette okabe-ito", ""), + usage.Ex("sysml model.sysml -render Views::vehicleView -render-form dot -render-unplaced strip", "unpositioned nodes in a strip below"), usage.Ex("sysml model.sysml -render Views::vehicleView -render-form plantuml -o view.puml", ""), usage.Ex("sysml types.sysml model.sysml -render Views::vehicleView", "several files, loaded as one model"), usage.Ex("sysml model.sysml -render-all rendered", ""), @@ -384,7 +385,13 @@ func doc() usage.Doc { "black-and-white style of the SysML v2 Pilot visualizer; -render-palette " + "fills their nodes by keyword family from a colourblind-safe palette " + "(okabe-ito, tol-bright, tol-muted, tol-light, brewer-set2, brewer-dark2, " + - "viridis or cividis), keeping black text legible on every fill.", + "viridis or cividis), keeping black text legible on every fill. " + + "A DOT drawing of a view whose members carry DiagramLayout positions " + + "pins each at its stated place and leaves a member with no position " + + "undrawn, so nothing lands on a positioned box; -render-unplaced strip " + + "draws those members instead, in rows in a strip below the drawing. " + + "The same setting shapes the DOT diagrams of -render-document and " + + "-render-documents.", }, }, { Title: "Rendering a document", @@ -586,6 +593,7 @@ func registerFlags(fs *flag.FlagSet) { fs.StringVar(&renderAllDir, "render-all", "", "Render every declared view into this directory") fs.StringVar(&renderForm, "render-form", "", "Form -render or -render-all writes: text, mermaid, markdown, dot or plantuml; default from the destination for -render, each kind's machine form for -render-all") fs.StringVar(&renderPalette, "render-palette", "", "Palette the dot or plantuml form fills nodes from, by keyword family: okabe-ito, tol-bright, tol-muted, tol-light, brewer-set2, brewer-dark2, viridis or cividis; default black and white") + fs.StringVar(&renderUnplaced, "render-unplaced", "", "Where the dot form of a view some Layout positions puts the nodes none does: omit (default) leaves them undrawn, strip draws them in rows below the drawing; applies to -render, -render-all and document diagrams") fs.StringVar(&renderDoc, "render-document", "", "Compile this document definition, run its queries and write the rendered document") fs.StringVar(&renderDocsDir, "render-documents", "", "Render every document definition, linked to one another, into this directory") @@ -719,6 +727,7 @@ func optionGroups() []usage.OptionGroup { usage.Opt("render-all", ""), usage.Opt("render-form", formArg), usage.Opt("render-palette", ""), + usage.Opt("render-unplaced", ""), }, }, { Title: "Rendering documents", diff --git a/docs/internals/architecture.md b/docs/internals/architecture.md index 137507962..6b78b70c1 100644 --- a/docs/internals/architecture.md +++ b/docs/internals/architecture.md @@ -662,7 +662,7 @@ See [the guide](../guide/) for VS Code configuration. | Standard library bundling | ✅ Complete | | LSP server implementation | ✅ Complete | -**Parser coverage:** 105/105 bundled library files parse cleanly — the 94 official SysML v2 standard library files and the 11 non-normative OpenSysML extensions: `OpenSysML Libraries/OpenSysMLMathFunctions.kerml`, `DocumentQueries.sysml`, `IdentityMetadata.sysml`, `OOSEM.sysml`, `DiagramLayout.sysml`, `MOSA.sysml`, `RandomFunctions.kerml`, `Simulation.sysml`, `StateSpaceIntegration.sysml`, `Stochastic.sysml` and `AnalysisRecords.sysml`. Conformance verified by [stdlib_conformance_test.go](../../internal/workspace/libs/stdlib_conformance_test.go). Grammar reference available at [OMG Xtext grammar](https://github.com/Systems-Modeling/SysML-v2-Pilot-Implementation/tree/master/org.omg.kerml.xtext/src/org/omg/kerml/xtext). +**Parser coverage:** 106/106 bundled library files parse cleanly — the 94 official SysML v2 standard library files and the 12 non-normative OpenSysML extensions: `OpenSysML Libraries/OpenSysMLMathFunctions.kerml`, `DocumentQueries.sysml`, `IdentityMetadata.sysml`, `OOSEM.sysml`, `DiagramLayout.sysml`, `MOSA.sysml`, `RandomFunctions.kerml`, `Simulation.sysml`, `StateSpaceIntegration.sysml`, `Stochastic.sysml`, `AnalysisRecords.sysml` and `MigrationMetadata.sysml`. Conformance verified by [stdlib_conformance_test.go](../../internal/workspace/libs/stdlib_conformance_test.go). Grammar reference available at [OMG Xtext grammar](https://github.com/Systems-Modeling/SysML-v2-Pilot-Implementation/tree/master/org.omg.kerml.xtext/src/org/omg/kerml/xtext). --- @@ -675,8 +675,8 @@ New grammar features require a **four-layer test contract** to ensure correctnes #### 1. Conformance Gate - **Purpose:** Ensure stdlib continues to parse cleanly - **Location:** `internal/workspace/libs/stdlib_conformance_test.go` -- **Test:** `TestStdlibConformance` loads all 105 bundled library files -- **Acceptance:** 105/105 files parse without errors +- **Test:** `TestStdlibConformance` loads all 106 bundled library files +- **Acceptance:** 106/106 files parse without errors - **Allowlist:** `testdata/stdlib_known_failures.txt` (currently empty) - **Failure mode:** Regression breaks previously-working stdlib files diff --git a/docs/internals/testing.md b/docs/internals/testing.md index d43b22338..d94e88fa4 100644 --- a/docs/internals/testing.md +++ b/docs/internals/testing.md @@ -34,7 +34,7 @@ New grammar features require a **four-layer test contract**: **Purpose:** Ensure standard library continues to parse cleanly - **Test:** `TestStdlibConformance` (internal/workspace/libs/) -- **Coverage:** 104/104 bundled library files — the 94 official SysML v2 standard library files and ten non-normative OpenSysML extensions +- **Coverage:** 106/106 bundled library files — the 94 official SysML v2 standard library files and twelve non-normative OpenSysML extensions - **Acceptance:** All stdlib files parse without errors - **Allowlist:** `testdata/stdlib_known_failures.txt` (currently empty) diff --git a/docs/project/pilot-differential-baseline.json b/docs/project/pilot-differential-baseline.json index 9a0b2695b..7b76b7f2a 100644 --- a/docs/project/pilot-differential-baseline.json +++ b/docs/project/pilot-differential-baseline.json @@ -61,7 +61,7 @@ "dir": "examples", "origin": "ours", "files": 43, - "digest": "sha256:18b2fbd67ec48deea2777902bd8a7f6bbb238f44107f28c52a7cda2ef0ff9e83" + "digest": "sha256:3e4522077d7c4903aa3c6cad01dc84abc79588f9f13c62e7c5aee8c9bee1a02f" }, { "name": "probes", diff --git a/docs/project/spec-compliance.md b/docs/project/spec-compliance.md index 000268b21..d9e12653f 100644 --- a/docs/project/spec-compliance.md +++ b/docs/project/spec-compliance.md @@ -2010,8 +2010,8 @@ semantics layer over the conjugation parity of the typing/specialization chain. | A view's exposed elements are queryable (SysML v2 7.24 Views and Viewpoints, 8.3.26 Expose) | `semantics/expose.go` `Model.ExposedElements` (a view's own `expose` relationships, then those of the views it specializes since an Expose is protected, in declaration order and once each) and `Model.NestedViews` (the views in its body, to walk a view tree), enumerated by `resolve/filter.go` `Resolver.ImportedElements` — the same admission, visibility and filter gating a lookup through that import makes, so the exposed set is what the view body actually resolves | `semantics/expose_test.go` (`TestExposedElementsNamespaceWildcard`, `TestExposedElementsRecursiveExpose`, `TestExposedElementsWithAnElementFilter`, `TestExposedElementsWithAViewBodyFilter`, `TestExposedElementsOfNestedViews`, `TestExposedElementsExposingAnotherView`, `TestExposedElementsInheritedFromAViewDefinition`, `TestExposedElementsOfAViewExposingNothing`, `TestExposedElementsOfANonView`) | ✅ Faithful (an empty exposed set is no error; asking a non-view is `semantics.ErrNotAView`. The REPL surface is `%view` — `repl/view.go` `doView`) | | A view's exposed set is rendered as the rendering its `render` member states, and as a containment tree where it states none (SysML v2 7.24 Views and Viewpoints, §10.2 — the rendering is tool-defined) | `semantics/rendering.go` `Model.ViewRenderings` (the `render` members of the view and of the views it specializes) and `Model.RenderingTarget` (the rendering the member references or declares); `view/view.go` `Renderer.KindOf`, `Renderer.Render` (the kind, then the exposed set from `Model.ExposedElements`) and `Renderer.RenderExposed` (an arbitrary selected set); `view/pseudo.go` derives the `#` vocabulary from the kinds this build supports; `view/tree.go`, `view/interconnection.go` (the model's own connector and flow ends, not source text), `view/behavior.go` (the lowered `lower.StateGraph`/`lower.ActionGraph`, never a re-parse of `symbol.Decl`), `view/table.go` (the exposed elements, the elements declared in them and the nested views, as rows), `view/sequence.go` `Renderer.renderSequence` (the occurrences an interaction declares as lifelines, the model's own flow ends as directed messages, ordered by the successions between the events those messages run between), `view/text.go`, `view/mermaid.go` and `view/markdown.go` (the forms, chosen per kind by `Kind.MachineForm` in `view/form.go`) | `view/render_test.go` `TestGoldenRenderings` (text and machine-readable goldens for tree, interconnection, state, action, a filtered view and a table, from `.sysml` fixtures), `TestTreeRenderingShowsNestedViewsAndDefaults`, `TestInterconnectionRenderingDrawsConnections`, `TestStateRenderingComesFromTheLoweredGraph`, `TestActionRenderingComesFromTheLoweredGraph`, `TestRenderingUsesFilteredAndInheritedExposure`, `TestTableRenderingRows`, `TestTableFormsAreMarkdownNotMermaid`, `TestMermaidLabelsAreEscaped`; `view/sequence_test.go` `TestSequenceRenderingDrawsLifelinesAndMessages`, `TestSequenceRenderingFromTheShortViewName`, `TestSequenceRenderingReportsWhatItCannotShow`, `TestSequenceRenderingHonoursStatedOrder`, `TestSequenceRenderingReportsASuccessionCycle`, `TestSequenceMermaidDeclaresParticipantsFirst`; `view/pseudo_test.go` | ✅ Faithful to the notation, tool-defined in output (the kinds produced are a tree, an interconnection diagram, a state machine, an action flow, a sequence diagram and a table; state and action renderings read the graphs the runtime executes, so a rendering cannot drift from what runs. Mermaid is the machine-readable form of the graph-shaped kinds and Markdown that of a table; SysML §10.2 specifies no artifact) | | A rendering this build does not produce, a name that is no view, a view exposing nothing, and an exposed element a rendering cannot represent are each explicit | `view/view.go` `UnsupportedKindError` (wrapping `view.ErrUnsupportedKind`, naming the kind, the view and the rendering it stated), `Renderer.Render` (`semantics.ErrNotAView` for a non-view, as `%view` answers), `Rendering.Empty` and `view/text.go` (an empty artifact saying whether the view exposes nothing or nothing exposed was representable), `Rendering.Notices` (what a kind could not draw); `cmd/sysml/render.go` reports and skips unsupported declared views and incompatible forced forms during `-render-all`, prefixing each notice with its source view | `view/render_test.go` `TestUnsupportedRenderingKinds`, `TestRenderingSomethingThatIsNoView`, `TestRenderingAViewExposingNothing`, `TestRenderingReportsWhatItCannotRepresent`; `repl/view_render_test.go` `TestRenderOfAnUnsupportedKindNamesIt`, `TestRenderOfANonViewIsTyped`, `TestRenderOfAViewExposingNothingSaysSo`; `cmd/sysml/render_test.go` `TestRenderReportsWhatItCouldNotDo`, `TestRenderAllSkipsUnsupportedKindsAndWrongForcedForms`, `TestRenderAllPrefixesRenderingNoticesWithTheirView` | ✅ Faithful (a stated kind that is not produced is a typed error naming it, never a substituted rendering; a form the kind is not written in is a `WrongFormError` naming the one it is; an element that cannot be drawn is reported, not dropped. A one-view render stops on either typed error; a render-all run skips only that view and renders the rest) | -| A graph-shaped rendering (tree, interconnection, state, action) is also written as Graphviz DOT, an alternative to Mermaid for Graphviz toolchains and large-graph layouts, without a Graphviz installation: a `digraph` with `// view:`, `// kind:`, `// stated:`, `// not represented:`, `// canvas:` and `// layout:` header comments, `rankdir` from the direction, containment as `subgraph "cluster_"` (a tree as `arrowhead=none` edges, as its Mermaid form), an edge at a cluster drawn to a node inside it and clipped with `lhead`/`ltail`, state pseudo-states as `point`/`circle`/`doublecircle`, states as rounded boxes, regions as dashed clusters, transition labels as the state writer's trigger/guard/effect text, and edge kinds parallel to the Mermaid arrows (connection `arrowhead=none`, flow `style=dashed`, transition and succession solid); every identifier and label is quoted through one helper; the DiagramLayout geometry is written as Graphviz reads it, one pixel to one point with y flipped from the library's y-down origin (`inputscale=72`, `dpi=72`; y measured from the canvas's bottom edge, negated with no canvas height): a positioned node pinned at the centre of its box with `pos="x,y!", pin=true`, its size as `width`/`height` in inches — a stated size with `fixedsize=true`, an unstated one fitted to the label so the box's corner stays where the Layout put it — `collapsed` as `comment="collapsed"`, a positioned cluster's `bb` stated (its stated box, or the one from its corner round its positioned members) and its anchor pinned at the centre, a `Route` as the edge's `pos` B-spline through its waypoints (a route of one waypoint drawn as no line and noticed as `// not represented:`), a sized `Canvas` as an invisible point pinned at each corner so the drawing's bounding box is the canvas, and the `// layout:` header naming `neato -n2` when every node is placed and any edge routed, `neato -n` when every node is placed and none routed, `neato` when some nodes are, `dot` when none; sequence and table have no DOT form and are the same `WrongFormError` the other forms raise | `view/dot.go` `Rendering.DOT`, `Rendering.DOTWith`, `dotWriter` (`engine`, `graphAttributes`, `dotNodeAttributes`, `dotPin`, `dotBox`, `dotClusterAttributes`, `dotAnchorAttributes`, `clusterBox`, `dotEdgeAttributes`, `dotSpline`, `flipY`, `dotInches`), `dotQuote`; `view/form.go` `FormDot`, `Forms`, `DiagramForms`, `Kind.SupportsForm`, `Options`, `Write`, `WriteWith`; `cmd/sysml/render.go` (`-render-form dot`, `.dot` under `-render-all`); `repl/meta.go` (`%render dot`); `lsp/render.go` `renderForm` (`form: "dot"`) | `view/dot_test.go` `TestGoldenDOT` (`testdata/*.dot.golden` beside the Mermaid goldens, each checked by an in-test DOT syntax walker: balanced braces, every edge endpoint declared as a node or cluster, quoted identifiers), `TestDOTFormSupport`, `TestDOTQuotesEveryIdentifierAndLabel`, `TestDOTNestedClusters`, `TestDOTDirections`, `TestDOTEdgeKinds`, `TestDOTStateShapesAndLabels`, `TestDOTEmptyAndNotices`, `TestDOTWritesTheGeometry` (`testdata/layout.dot.golden` beside the Mermaid and text goldens of the layout fixture; the flipped axis with and without a canvas height; states and transitions placed), `TestDOTPinsEveryNode` (`neato -n` and `neato -n2`, the one-waypoint notice, a stated, a member-fitted and a corner-only cluster's `bb` and anchor, a tree's positioned parent, pseudo-state centring, the zero-extent, unit-only and unpositioned canvas); the syntax walker parses every `pos` and `bb`; `cmd/sysml/render_test.go` `TestRenderDotForm`; `repl/view_render_test.go` `TestRenderWritesDotWhenAskedFor`; `lsp/render_test.go` `TestRenderWritesDotWhenAskedFor` | ✅ Faithful to the notation, tool-defined in output (Mermaid stays the machine-readable form `Kind.MachineForm` chooses; DOT is written on request. Producing the DOT needs no `dot` binary, and the goldens are validated by the in-test syntax walker rather than by `dot -Tsvg`; the one place Graphviz runs is the PDF backend, drawing the block on request when a Graphviz is installed (the document-rendering rows below). A `Route` is written as the polyline through its waypoints, not smoothed; a node with no stated size is given the writer's estimate of its label's extent (8.4 pt a glyph, 16.8 pt a line), not `fixedsize`, so Graphviz may grow the box for its own font and move the corner by the difference. The gRPC API has no view-render RPC — `RenderDocument` alone, to Markdown — so no wire contract carries a form) | -| The DOT form draws in the Standard B&W style of the OMG SysML v2 Pilot Implementation's PlantUML visualizer (SysML v2 §8.2.2.2, the graphical notation's rendering being tool-defined), after the `sysmlbw` PlantUML skin by Hisashi Miyashita (Mgnite Inc.) shipped with the Pilot and the edge rules of its `SysML2PlantUMLStyle.java`, reproducing the skin's visual parameters rather than its text: Helvetica text at 14 pt on nodes and 13 pt on edges, white fills, `#181818` lines at `penwidth=0.5` on nodes and `1` on edges, a definition (`… def`, or a KerML classifier keyword) square and a usage `style="rounded,filled"`, the name in bold over the `«keyword»` line in italics at its 10 pt size, clusters unfilled with black borders — `penwidth=1.5` for a package, `0.5` for an element's cluster and a region (which keeps `style=dashed`) — a connection at `penwidth=3` with `arrowhead=none`, flow, succession and transition as before, and an unnamed initial or final pseudo-state as the filled black UML dot (`shape=circle`/`doublecircle`, `fillcolor=black`, `label=""`, `width=0.2` unless a Layout sizes it) while a named one keeps its labelled ring; every style attribute precedes the geometry in a node's list, and geometry, node IDs, label text, escaping, routes, cluster anchors, `lhead`/`ltail`, the header comments and the order of nodes and edges are the ones the DOT form always wrote | `view/dot.go` `dotNodeDefaults`, `dotEdgeDefaults`, `dotControlKinds`, `dotNodeAttributes`, `dotPseudostateAttributes`, `dotClusterAttributes`, `dotClusterPenwidth`, `dotEdgeAttributes`, `dotLabel`; `view/palette.go` `isDefinitionKind`, `kermlClassifierKinds` | `view/dot_style_test.go` `TestDOTStandardDefaults`, `TestDOTDefinitionsSquareUsagesRounded`, `TestDOTPseudostateRules`, `TestDOTClusterBorders`, `TestDOTConnectionPenwidth`, `TestDOTEscapesNamesInStyledLabels`; every `view/testdata/*.dot.golden`, reviewed so that only style attributes and the italic keyword markup moved; `docrender/testdata/*.golden.*`, `repl/view_render_test.go`, `cmd/sysml/render_test.go`, `lsp/render_test.go` | ⚠️ Approximate (the skin's 20-unit `UsageRoundCorner` is Graphviz's fixed `rounded` radius; its `Shadowing 0`, `hide circle` and `wrapWidth 300` have no Graphviz counterpart and nothing to turn off; the skin's plain-weight state title is not followed — a state's name stays bold like every other kind's, so the text, Mermaid and DOT forms read alike; the Pilot's `-[thickness=5]-` binding connectors are not drawn apart from connections because the interconnection rendering has no edge kind for them; the skin's notes, sequence, gantt, mindmap and wbs sections are out of the DOT form's scope. Producing DOT still runs no Graphviz binary; the goldens are checked by the in-test syntax walker, and a Graphviz installation is used only by hand to look at them) | +| A graph-shaped rendering (tree, interconnection, state, action) is also written as Graphviz DOT, an alternative to Mermaid for Graphviz toolchains and large-graph layouts, without a Graphviz installation: a `digraph` with `// view:`, `// kind:`, `// stated:`, `// not represented:`, `// canvas:` and `// layout:` header comments, `rankdir` from the direction, containment as `subgraph "cluster_"` (a tree as `arrowhead=none` edges, as its Mermaid form), an edge at a cluster drawn to a node inside it and clipped with `lhead`/`ltail`, state pseudo-states as `point`/`circle`/`doublecircle`, states as rounded boxes, regions as dashed clusters, transition labels as the state writer's trigger/guard/effect text, and edge kinds parallel to the Mermaid arrows (connection `arrowhead=none`, flow `style=dashed`, transition and succession solid); every identifier and label is quoted through one helper; the DiagramLayout geometry is written as Graphviz reads it, one pixel to one point with y flipped from the library's y-down origin (`inputscale=72`, `dpi=72`; y measured from the canvas's bottom edge, negated with no canvas height): a positioned node pinned at the centre of its box with `pos="x,y!", pin=true`, its size as `width`/`height` in inches — a stated size with `fixedsize=true` and the label fitted to it (the head wrapped at the width and shrunk from 14 pt to 8 pt until it fits, the keyword and detail lines kept only while height remains, an overrunning head ellipsized), an unstated one fitted to the label so the box's corner stays where the Layout put it — `collapsed` as `comment="collapsed"`, a positioned cluster's `bb` stated (its stated box, or the one from its corner round its positioned members) and its anchor pinned at the centre, a `Route` as the edge's `pos` B-spline through its waypoints (a route of one waypoint drawn as no line and noticed as `// not represented:`), a sized `Canvas` as an invisible point pinned at each corner so the drawing's bounding box is the canvas, and the `// layout:` header naming `neato -n2` when every node is placed and any edge routed, `neato -n` when every node is placed and none routed, `neato` when some nodes are, `dot` when none; sequence and table have no DOT form and are the same `WrongFormError` the other forms raise | `view/dot.go` `Rendering.DOT`, `Rendering.DOTWith`, `dotWriter` (`engine`, `graphAttributes`, `dotNodeAttributes`, `dotPin`, `dotBox`, `dotClusterAttributes`, `dotAnchorAttributes`, `clusterBox`, `dotEdgeAttributes`, `dotSpline`, `flipY`, `dotInches`), `dotQuote`; `view/form.go` `FormDot`, `Forms`, `DiagramForms`, `Kind.SupportsForm`, `Options`, `Write`, `WriteWith`; `cmd/sysml/render.go` (`-render-form dot`, `.dot` under `-render-all`); `repl/meta.go` (`%render dot`); `lsp/render.go` `renderForm` (`form: "dot"`) | `view/dot_test.go` `TestGoldenDOT` (`testdata/*.dot.golden` beside the Mermaid goldens, each checked by an in-test DOT syntax walker: balanced braces, every edge endpoint declared as a node or cluster, quoted identifiers), `TestDOTFormSupport`, `TestDOTQuotesEveryIdentifierAndLabel`, `TestDOTNestedClusters`, `TestDOTDirections`, `TestDOTEdgeKinds`, `TestDOTStateShapesAndLabels`, `TestDOTEmptyAndNotices`, `TestDOTWritesTheGeometry` (`testdata/layout.dot.golden` beside the Mermaid and text goldens of the layout fixture; the flipped axis with and without a canvas height; states and transitions placed), `TestDOTPinsEveryNode` (`neato -n` and `neato -n2`, the one-waypoint notice, a stated, a member-fitted and a corner-only cluster's `bb` and anchor, a tree's positioned parent, pseudo-state centring, the zero-extent, unit-only and unpositioned canvas); the syntax walker parses every `pos` and `bb`; `cmd/sysml/render_test.go` `TestRenderDotForm`; `repl/view_render_test.go` `TestRenderWritesDotWhenAskedFor`; `lsp/render_test.go` `TestRenderWritesDotWhenAskedFor` | ✅ Faithful to the notation, tool-defined in output (Mermaid stays the machine-readable form `Kind.MachineForm` chooses; DOT is written on request. Producing the DOT needs no `dot` binary, and the goldens are validated by the in-test syntax walker rather than by `dot -Tsvg`; the one place Graphviz runs is the PDF backend, drawing the block on request when a Graphviz is installed (the document-rendering rows below). A `Route` is written as the polyline through its waypoints, not smoothed; a node with no stated size is given the writer's estimate of its label's extent (8.4 pt a glyph, 16.8 pt a line), not `fixedsize`, so Graphviz may grow the box for its own font and move the corner by the difference. The gRPC API has no view-render RPC — `RenderDocument` alone, to Markdown — so no wire contract carries a form) | +| The DOT form draws in the Standard B&W style of the OMG SysML v2 Pilot Implementation's PlantUML visualizer (SysML v2 §8.2.2.2, the graphical notation's rendering being tool-defined), after the `sysmlbw` PlantUML skin by Hisashi Miyashita (Mgnite Inc.) shipped with the Pilot and the edge rules of its `SysML2PlantUMLStyle.java`, reproducing the skin's visual parameters rather than its text: Helvetica text at 14 pt on nodes and 13 pt on edges, white fills, `#181818` lines at `penwidth=0.5` on nodes and `1` on edges, a definition (`… def`, or a KerML classifier keyword) square and a usage `style="rounded,filled"`, the name in bold over the `«keyword»` line in italics at its 10 pt size, clusters unfilled with black borders — `penwidth=1.5` for a package, `0.5` for an element's cluster and a region (which keeps `style=dashed`) — a connection at `penwidth=3` with `arrowhead=none`, flow, succession and transition as before, and an unnamed initial or final pseudo-state as the filled black UML dot (`shape=circle`/`doublecircle`, `fillcolor=black`, `label=""`, `width=0.2` unless a Layout sizes it) while a named one keeps its labelled ring unless a Layout sizes it; a decision, merge, choice, fork, join, initial, final or port a Layout sizes drawn as its symbol with no inner text, its name as an `xlabel` unless the view IR marks it synthesized; every style attribute precedes the geometry in a node's list, and geometry, node IDs, label text, escaping, routes, cluster anchors, `lhead`/`ltail`, the header comments and the order of nodes and edges are the ones the DOT form always wrote | `view/dot.go` `dotNodeDefaults`, `dotEdgeDefaults`, `dotControlKinds`, `dotNodeAttributes`, `dotPseudostateAttributes`, `dotClusterAttributes`, `dotClusterPenwidth`, `dotEdgeAttributes`, `dotLabel`; `view/palette.go` `isDefinitionKind`, `kermlClassifierKinds` | `view/dot_style_test.go` `TestDOTStandardDefaults`, `TestDOTDefinitionsSquareUsagesRounded`, `TestDOTPseudostateRules`, `TestDOTClusterBorders`, `TestDOTConnectionPenwidth`, `TestDOTEscapesNamesInStyledLabels`; every `view/testdata/*.dot.golden`, reviewed so that only style attributes and the italic keyword markup moved; `docrender/testdata/*.golden.*`, `repl/view_render_test.go`, `cmd/sysml/render_test.go`, `lsp/render_test.go` | ⚠️ Approximate (the skin's 20-unit `UsageRoundCorner` is Graphviz's fixed `rounded` radius; its `Shadowing 0`, `hide circle` and `wrapWidth 300` have no Graphviz counterpart and nothing to turn off; the skin's plain-weight state title is not followed — a state's name stays bold like every other kind's, so the text, Mermaid and DOT forms read alike; the Pilot's `-[thickness=5]-` binding connectors are not drawn apart from connections because the interconnection rendering has no edge kind for them; the skin's notes, sequence, gantt, mindmap and wbs sections are out of the DOT form's scope. Producing DOT still runs no Graphviz binary; the goldens are checked by the in-test syntax walker, and a Graphviz installation is used only by hand to look at them) | | A graph-shaped rendering (tree, interconnection, state, action) and a sequence are also written as PlantUML, the language the OMG Pilot's own visualizer draws with, without Java or a PlantUML jar: an `@startuml` … `@enduml` file with the `' — rendering ()` header comment and one `' not represented:` line per notice and per loss the writer itself incurs, the Standard B&W style inline as a `