Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
19 commits
Select commit Hold shift + click to select a range
10c6936
fix(view): leave layout annotations and render members out of renderings
devin-ai-integration[bot] Sep 23, 2026
a24f053
fix(view): head a member drawn under its owner by its name below that…
devin-ai-integration[bot] Sep 23, 2026
109f8dc
feat(view): fit a stated box's label to it and draw symbol kinds as t…
devin-ai-integration[bot] Sep 23, 2026
09618b9
docs(view): describe the fitted labels, symbols, owner-relative heads…
devin-ai-integration[bot] Sep 23, 2026
102cce9
fix(view): draw an initial or final node with a synthesized name as t…
devin-ai-integration[bot] Sep 23, 2026
506eca9
fix(view): clip an edge at a stated junction's radius
devin-ai-integration[bot] Sep 23, 2026
d6cc2de
fix(view): head a member below its owner's drawn type, and write an e…
devin-ai-integration[bot] Sep 24, 2026
765bd18
feat(migrate): mark the names a migration makes up and draw those ele…
devin-ai-integration[bot] Sep 24, 2026
090f74e
chore(docs): re-record the examples digest in the pilot differential …
devin-ai-integration[bot] Sep 24, 2026
c4c7c25
feat(view): keep words whole in a fitted head, shrinking before break…
devin-ai-integration[bot] Sep 24, 2026
f3434d7
fix(lower): let a parallel state own a metadata usage in its body
devin-ai-integration[bot] Sep 24, 2026
5f5a126
docs(changelog): note the parallel state's metadata member
devin-ai-integration[bot] Sep 24, 2026
e8238ba
feat(view): head a stated box above the stated boxes it holds, and na…
devin-ai-integration[bot] Sep 24, 2026
9166da2
feat(view): leave a positioned drawing's unplaced nodes undrawn, or s…
devin-ai-integration[bot] Sep 24, 2026
374dbfd
Merge remote-tracking branch 'origin/develop' into feature/positioned…
devin-ai-integration[bot] Sep 24, 2026
db455d5
Merge remote-tracking branch 'origin/develop' into feature/positioned…
devin-ai-integration[bot] Sep 24, 2026
e98c1cd
test(docpdf): read the tall figure's first node back by its kind head
devin-ai-integration[bot] Sep 24, 2026
b0a3916
fix(view): give a stated box's whole width and height to its fitted l…
devin-ai-integration[bot] Sep 24, 2026
914afc2
fix(view,migrate): keep a typed port's type outside its symbol and ma…
devin-ai-integration[bot] Sep 24, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 4 additions & 4 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand All @@ -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 |
Expand Down Expand Up @@ -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 (<!-- doc-counts:begin conformance-passing -->every conformance case passing<!-- doc-counts:end conformance-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.
Expand Down Expand Up @@ -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
Expand Down
7 changes: 7 additions & 0 deletions changes/unreleased/positioned-label-fitting.fixed.md
Original file line number Diff line number Diff line change
@@ -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.
7 changes: 6 additions & 1 deletion cmd/sysml/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -129,6 +129,7 @@ var (
renderAllDir string
renderForm string
renderPalette string
renderUnplaced string
renderDoc string
renderDocsDir string
docForm string
Expand Down Expand Up @@ -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.
Expand Down Expand Up @@ -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():
Expand Down
22 changes: 21 additions & 1 deletion cmd/sysml/render.go
Original file line number Diff line number Diff line change
Expand Up @@ -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 != "" {
Expand All @@ -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) {
Expand Down
10 changes: 8 additions & 2 deletions cmd/sysml/render_document.go
Original file line number Diff line number Diff line change
Expand Up @@ -82,6 +82,7 @@ func pdfOptions() (docpdf.Options, error) {
Stylesheets: page.Stylesheets,
BaseDir: filepath.Dir(outputPath),
DiagramForm: page.DiagramForm,
Unplaced: page.Unplaced,
}, nil
}

Expand Down Expand Up @@ -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
}
Expand Down
Loading
Loading