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", "